从 JDK 源码角度看 java 并发线程的中断

线程的定义给咱们提供了并发执行多个任务的方式,大多数状况下咱们会让每一个任务都自行执行结束,这样能保证事务的一致性,可是有时咱们但愿在任务执行中取消任务,使线程中止。在java中要让线程安全、快速、可靠地停下来并非一件容易的事,java也没有提供任何可靠的方法终止线程的执行。javascript

线程调度策略中有抢占式和协做式两个概念,与之相似的是中断机制也有协做式和抢占式。html

历史上Java曾经使用stop()方法终止线程的运行,他们属于抢占式中断。但它引来了不少问题,早已被JDK弃用。调用stop()方法则意味着:java

①将释放该线程所持的全部锁,并且锁的释放不可控。
②即刻将抛出ThreadDeath异常,无论程序运行到哪里,但它不老是有效,若是存在被终止线程的锁竞争;node

第一点将致使数据一致性问题,这个很好理解,通常数据加锁就是为了保护数据的一致性,而线程中止伴随所持锁的释放,极可能致使被保护的数据呈现不一致性,最终致使程序运算出现错误。安全

第二点比较模糊,它要说明的问题就是可能存在某种状况stop()方法不能及时终止线程,甚至可能终止不了线程。看以下代码会发生什么状况,看起来线程mt由于执行了stop()方法将中止,按理来讲就算execut方法是一个死循环,只要执行了stop()方法线程将结束,无限循环也将结束。其实不会,由于咱们在execute方法使用了synchronized修饰,同步方法表示在执行execute时将对mt对象进行加锁,另外,Thread的stop()方法也是同步的,因而在调用mt线程的stop()方法前必须获取mt对象锁,但mt对象锁被execute方法占用,且不释放,因而stop()方法永远获取不了mt对象锁,最后获得一个结论,使用stop()方法中止线程不可靠,它未必总能有效终止线程。并发

public class ThreadStop {
    public static void main(String[] args) {
        Thread mt= new MyThread();
        mt.start();
        try {
            Thread.currentThread().sleep(100);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        mt.stop();
    }
static class MyThread extends Thread {
    public void run() {
        execute();
    }
    private synchronized void execute() {
        while(true) {
        }
    }
}
}复制代码

经历了很长时间的发展,Java最终选择用一种协做式的中断机制实现中断。协做式中断的原理很简单,其核心是先对中断标识进行标记,某线程设置某线程的中断标识位,被标记了中断位的线程在适当的时间节点会抛出异常,捕获异常后作相应的处理。实现协做中断有三个要点须要考虑:框架

①是在Java层面实现轮询中断标识仍是在JVM中实现;
②轮询的颗粒度的控制,通常颗粒度要尽可能小周期尽可能短以保证响应的及时性;
③轮询的时间节点的选择,其实就是在哪些方法里面轮询,例如JVM将Thread类的wait()、sleep()、join()等方法都实现中断标识的轮询操做。工具

中断标识放在哪里?中断是针对线程实例而言,从Java层面上看,标识变量放到线程中确定再合适不过了,但因为由JVM维护,因此中断标识具体由本地方法维护。在Java层面仅仅留下几个API用于操做中断标识,以下,优化

public class Thread{
    public void interrupt() {……}
    public Boolean isInterrupted() {……}
    public static Booleaninterrupted() {……}
}复制代码

上面三个方法依次用于设置线程为中断状态、判断线程状态是否中断、清除当前线程中断状态并返回它以前的值。经过interrupt()方法设置中断标识,假如在非阻塞线程则仅仅只是改变了中断状态,线程将继续往下运行,但假如在可取消阻塞线程中,如正在执行sleep()、wait()、join()等方法的线程则会由于被设置了中断状态而抛出InterruptedException异常,程序对此异常捕获处理。ui

上面提到的三个要点:

  • 第一是轮询在哪一个层面实现,这个没有特别的要求,在实际中只要不出现逻辑问题,在Java层面或JVM层面实现都是能够的,例如经常使用的线程睡眠、等待等操做是经过JVM实现,而java并发框架工具里面的中断则放到Java实现,无论在哪一个层面上去实现,在轮询过程当中都必定要能保证不会产生阻塞。
  • 第二是要保证轮询的颗粒度尽量的小周期尽量短,这关系到中断响应的速度。
  • 第三点是关于轮询的时间节点的选取。

针对三要点来看看java并发框架中是如何支持中断的,主要在等待获取锁的过程当中提供中断操做,下面是伪代码。只需增长加红加粗部分逻辑便可实现中断支持,在循环体中每次循环都对当前线程中断标识位进行判断,一旦检查到线程被标记为中断则抛出InterruptedException异常,高层代码对此异常捕获处理即完成中断处理。总结起来就是java并发工具获取锁的中断机制是在Java层面实现的,轮询时间节点选择在不断作尝试获取锁操做过程当中,每一个循环的颗粒度比较小,响应速度得以保证,且循环过程不存在阻塞风险,保证中断检测不会失效。

if(尝试获取锁失败) {
    建立node
    使用CAS方式把node插入到队列尾部
    while(true){
        if(尝试获取锁成功而且 node的前驱节点为头节点){
            把当前节点设置为头节点
            跳出循环
        }else{
            使用CAS方式修改node前驱节点的waitStatus标识为signal
            if(修改为功){
                挂起当前线程
                if(当前线程中断位标识为true)
                    抛出InterruptedException异常
            }
        }
    }
}复制代码

判断线程是否处于中断状态其实很简单,只需使用Thread.interrupted()操做,若是为true则说明线程处于中断位,并清除中断位。至此java并发工具实现了支持中断的获取锁操做。

本文从java发展过程分析了抢占式中断及协做式中断,因为抢占式存在一些缺陷如今已不推荐使用,而协做式中断做为推荐作法,尽管在响应时间较长,但其具备无可比拟的优点。

协做式中断咱们能够在JVM层面实现,一样也能够在Java层面实现,例如JDK并发工具的中断便是在Java层面实现,不过若是继续深究是由于Java留了几个API供咱们操做线程的中断标识位,这才使Java层面实现中断操做得以实现。

对于java的协做式中断机制有人确定有人批评,批评者说java没有抢占式中断机制,且协做式中断机制迫使开发者必须维护中断状态,迫使开发者必须处理InterruptedException。但确定者则认为,虽然协做式中断机制推迟了中断请求的处理,但它为开发人员提供更灵活的中断处理策略,响应性可能不及抢占式,但程序健壮性更强。

====广告时间,可直接跳过====

鄙人的新书《Tomcat内核设计剖析》已经在京东预售了,有须要的朋友能够到 item.jd.com/12185360.ht… 进行预约。感谢各位朋友。

=========================

相关阅读:
从JDK源码角度看并发锁的优化
从JDK源码角度看线程的阻塞和唤醒
从JDK源码角度看并发竞争的超时
从JDK源码角度看Java并发的公平性

欢迎关注:

相关文章
相关标签/搜索