Java 并发之线程中断

一、前言

所谓线程中断,其实就是终止一个线程。在使用 Java 线程时,除了线程自行正常结束,很多时候也需要提前结束一个线程的执行过程。Thread 类中有一个与 start() 相对应的 stop() 方法,可以从外部结束一个线程的执行。但是这个方法是极不推荐使用的,因为从外部强行结束一个线程的执行,会导致不可预知的错误,因为这样往往会在错误的时间结束一个线程的执行。

所以,在 Java 线程机制中,就有了另一种结束线程的方式,那就是中断。中断,简而言之就是让线程外部可以设置一个标记值,而线程内部在执行时则检查这个值,来获知此线程是否应该结束了。

二、可以用来设置中断的方法

除了 Thread.interrupt() 方法以外,下列 JDK 中的方法也会设置中断(也是通过调用 Thread.interrupt() 来实现的):

  • FutureTask.cancel()
  • ExecutorService.shutdownNow() 这个方法会调用线程池中所有线程的中断方法,不论它们是空闲的还是运行中的。而 ExecutorService.shutdown() 方法只能中断空闲的线程。

上面只是举两个 JDK 中应用到了线程中断的例子,这样的例子还有很多,就不一一列举了。当然,为了能响应中断,在你所写的 Runnable 或 Callable 代码中,必须通过 Thread.isInterrupted()Thread.interrupted() 方法,或者捕获 InterruptedException 等的中断异常来发现线程中断并处理,否则线程是不会自行提前结束的。

三、能被中断的方法

在 JDK 和其它类库和框架中,能相应中断的方法是很多的。下面列出几个常见的 JDK 中能响应中断的方法:

  • Thread.sleep()
  • Object.wait()
  • BlockingQueue.put(), BlockingQueue.take()
  • ReentrantLock.lockInterruptibly(), Condition.await()
  • ServerSocketChannel.accept(), SocketChannel.open()

等等

JDK 中能响应中断的方法基本上都是抛出异常。这些方法基本可被分为两类:一类是并发相关的,一类是 IO 相关的。

四、中断的处理

1. 处理 InterruptedException(也包括其它中断异常)

InterruptedException 是最常见的中断表现形式。所以如何处理 InterruptedException 便成为 Java 中断知识中的必修课。在这方面 IBM developerWorks 上有篇文章讲的很好,我在下面的参考文章中会列出链接。我这里就对这篇文章做一个总结,各位看客可以去读那边文章以获得细节知识。

处理 InterruptedException 可有以下几种方式(下面使用的代码均引用自 Java 理论与实践: 处理 InterruptedException):

直接向上抛出

将异常不做任何处理,直接抛向该方法的调用者

public class TaskQueue {
    private static final int MAX_TASKS = 1000;

    private BlockingQueue<Task> queue 
        = new LinkedBlockingQueue<Task>(MAX_TASKS);

    public void putTask(Task r) throws InterruptedException { 
        queue.put(r);
    }

    public Task getTask() throws InterruptedException { 
        return queue.take();
    }
}

在 catch 中做处理后在抛出

因为 InterruptedException 的抛出,会打断方法执行,使正在进行的工作只完成一部分。在有些情况下,你就需要进行诸如回滚的处理。所以在这种情况便需要在 catch 块中进行处理之后在向上抛出 InterruptedException

public class PlayerMatcher {
    private PlayerSource players;

    public PlayerMatcher(PlayerSource players) { 
        this.players = players; 
    }

    public void matchPlayers() throws InterruptedException { 
        try {
             Player playerOne, playerTwo;
             while (true) {
                 playerOne = playerTwo = null;
                 // Wait for two players to arrive and start a new game
                 playerOne = players.waitForPlayer(); // could throw IE
                 playerTwo = players.waitForPlayer(); // could throw IE
                 startNewGame(playerOne, playerTwo);
             }
         }
         catch (InterruptedException e) {  
             // If we got one player and were interrupted, put that player back
             if (playerOne != null)
                 players.addFirst(playerOne);
             // Then propagate the exception
             throw e;
         }
    }
}

不抛出 InterruptedException 时要恢复中断状态

很多时候,由于你所实现的接口定义的限制,你很可能无法抛出 InterruptedException。例如实现 Runnable 接口以编写业务代码。这时,你就无法再向上抛出 InterruptedException 了。此时你应该使用 Thread.currentThread().interrupt() 方法去恢复中断状态。因为阻塞方法在抛出 InterruptedException 时会清除当前线程的中断状态,如果此时不恢复中断状态,也不抛出 InterruptedException,那中断信息便会丢失,上层调用者也就无法得知中断的发生。这样便有可能导致任务无法正确终止的情况方式。

public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;

    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }

    public void run() { 
        try {
             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }
         catch (InterruptedException e) { 
             // Restore the interrupted status
             Thread.currentThread().interrupt();
         }
    }
}

在这里,我们要清楚中断的意义在于并发或异步场景下任务的终止。所以,如果你的代码在吞掉 InterruptedException 而不抛出时并不会造成任务无法被正确终止的情况方式,那也可以不再恢复中断。

还是 Runnable 的例子,大家都知道 Runnable 多数时候是提交到线程池来运行。并且通常是作为任务的顶层容器来使用的,也就是说在线程池和 Runnable 实现之间,没有别的调用层了。那么在 try-catch InterruptedException 之后,便可不用在恢复线程中断了。

但如果不是上述情况,你所写的,带有 try-catch InterruptedException 的方法会被其它的、非线程池类的方法调用。例如有 A, B 两个方法,A 被 B 方法调用,A 中捕获 InterruptedException 后没有恢复线程中断,而 B 方法中有一个循环,通过检查线程中断来决定是否退出,或者 B 方法在调用 A 方法之后,还有个阻塞的方法。如果不恢复线程中断,那便会造成线程无法按照期望被终止的情况发生。

自己抛出 InterruptedException

有时候,你需要“无中生有”地创造出一个 InterruptedException 以表示中断的发生。在这个时候,你需要使用 Thread.isInterrupted()Thread.interrupted() 来检测中断的发生。那究竟是用这两者中的哪一个?其实看了前面的部分我们知道,抛出 InterruptedException 时,线程中断状态便被清除。所以,在你自己实现类似功能的时候,也要遵循这一原则,即抛出 InterruptedException 后需要清除当前线程的中断状态。因此,此时需要使用 Thread.interrupted()

其实,你要是看 JDK 源代码,就会发现,JDK 中并发类也是这么做的

NOTE: 使用 Thread.interrupt() 和 InterruptedException 中的哪种方法表示中断?上面提到了一种情况是由于接口的限制而无法抛出 InterruptedException,这时你别无选择,只能用 Thread.interrupt() 恢复中断。除了这种情况,其它的时候推荐使用 InterruptedException 来表示中断。当方法声明抛出 InterruptedException 时,它就是在告诉调用者,我这个方法可能会花费很多的时间,而你可以通过线程中断来终止调用。通过 InterruptedException 来表示中断,含义更清晰,反应也更迅速。

2. 无法被中断的情况

synchronized

阻塞在 synchronized 的内置锁上是无法被中断的,如果需要可以被中断的锁。可以使用 Java 5 concurrent 中的 Lock。

Java IO(不包含 NIO 和 AIO)

对 Java IO 有了解的人都知道,ServerSocket.accept() 是一个阻塞方法,但是 Thread.interrupt() 对它毫无影响。要想终止 ServerSocket.accept() 的等待,唯一方法就是调用 close() 方法。

3. 线程中断与 Java IO

上面说到传统的 Java IO 中的阻塞方法无法响应线程中断,因为 Java IO 出现的时候还没有中断机制。在 Java 1.4 引入的 Java NIO 已经可以很好地支持 Java 线程中断了。

如果是使用 XXXChannel,线程中断会导致 accept(), open() 等阻塞方法抛出 ClosedByInterruptException。

五、线程中断与微服务的高可用

谈到微服务的高可用就不得不说熔断降级,谈到熔断降级就不得不说 Hystrix。而 Hystrix 超时取消任务其实就是使用的线程中断。当任务执行超过 HystrixCommand 中的超时设置时,Hystrix 便会在中断执行任务的线程。所以,当你使用 Hystrix 时,你的代码一定要能响应中断。

六、参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容