所有的异常都是“负面”异常么?它们只是被用来通知我们程序出现非预期的状况的么?
InterruptedException是个什么样的异常?为什么它不从RuntimeException继承,每次sleep、wait都得进行显式处理?在什么情况下它会被抛出?我可以忽略它么?
如何使用InterruptedException正确地暂停、恢复、中止一个线程?
有关线程的概念,线程与进程之间的区别,网上已经有相当丰富的阐述和讨论了,这里不再赘述。只是强调一点,线程编程的出现,是为了解决进程编程模型中不同进程间难于共享数据(或者说内存)的问题。因为同一个进程的不同线程隶属于同一片内存地址空间,因此可以很方便地在线程间共享数据。换句话说,在一个线程中可以很自然直观地对同一进程的另一个线程中的非私有变量进行存取操作。
对于Java程序员,线程开发相对容易,只需:
Java中的线程对象遵循经典的线程模型定义,具有以下状态:
上面提到了一些与线程操作相关的方法,这里对容易误用的地方做一些简要的说明,详细说明可参阅JDK的Javadoc。
这两个方法定义在Thread类上,且是静态方法。对于程序员而言,yield可能是个相对陌生的方法,Javadoc中的描述是“临时暂停当前线程以允许其它线程执行”。换一个更容易理解的说法,yield方法的主要做作用就是告诉CPU:“我想休息一会儿,让我进入就绪状态,把CPU让给其它线程”。另外,Javadoc中明确指出,yield和sleep方法都是作用于当前线程,对于这一点很容易被忽略,这里以sleep为例举一个例子。
public class SleepThread extends Thread { private Thread thread = new Thread(); public void run () { thread.start(); // 省略异常处理 thread.sleep(10000); // 哪个线程被sleep?thread还是this? } }
这三个方法定义在Object上,作用是实现线程间的通知机制,但是在使用时以下三点需要注意:
public class SleepThread extends Thread { private Object lock = new Object(); Thread thread1 = new Thread() { public void run () { synchronized(lock) { // 进入“lock”对象的上下文 // 省略异常处理 lock.wait(); } } } Thread thread2 = new Thread() { public void run () { synchronized(lock) { // 进入“lock”对象的上下文(与wait方法相同的上下文) lock.notify(); } } } }
结合线程的状态以及上面提到的5个方法,我们可以做出一张Java线程的状态图:
在使用Java进行线程编程时,你会发现很多和线程相关的方法都会显式声明抛出InterrupedException,比如wait、sleep、join。而InterruptedException又是一个“checked exception”,必须处理。因此,大多数Java程序员对这个异常不很待见,当需要处理该异常时直接控制台输出,不做处理。那InterruptedException到底是一个什么样的异常,为什么与线程相关的方法需要声明抛出这个异常呢?
InterruptedException直译为“中断异常”,此处的“中断”与汇编语言中的“中断”很相似,是一种需要及时响应的非预期回调异常。按照字面意思,它有以下几个特点:
那究竟在什么情况下会抛出InterruptedException呢?根据Javadoc的描述,当线程由于调用了wait、sleep或join方法而进入了阻塞状态时,若该线程的interrupt()方法被调用,则上述方法立即抛出InterruptedException。可见,使用interrupt()和InterruptedException,我们可以使一个线程立即跳出阻塞状态,以响应我们的请求。通过一些编程技巧,可以进而终止一个线程。就这个意义来讲,InterruptedException和其它用来表征“异常情况”的异常不同,它是一种正面的提供编程便利的异常。
在平时的编码中,一种比较常见的线程应用场景是实现一个后台线程,周期性地执行一些操作(比如状态检查)。比较常见的实现方法如下:
public class StatusCheckingThread extends Thread { private volatile boolean running = true; public void run () { while (running) { // 具体实现 try { Thread.sleep(60 * 1000 * 1000); // 周期一个小时 } catch (InterruptedException ex) { log.info(ex); } } } public void makeStop (){ this.running = false; } }
这段代码使用了running作为中止标志位,当需要中止该线程时,只需要调用makeStop(),线程便会在下次循环开始时退出,从而中止。而这段代码的问题就在于“下次循环”,一旦线程执行sleep,就会休眠一个小时。而在这一个小时中,即使我们调用了makeStop(),线程也无法响应,只是简单地将running置为false,只有等到下一次循环开始对running进行检查时,线程才会如我们所愿地退出。很显然,wait、join也会遇到相同的问题,而且,周期越长,线程响应得越“慢”。
那如何让线程即使在休眠时也能响应我们的中止请求呢?这个时候“中断”的概念就能发挥作用了。看下面这段修改后的代码:
public class StatusCheckingThread extends Thread { public void run (){ while (!this.isInterrupted()) { // 具体实现 if (this.isInterrupted()) { // 问题:这里为什么要做判断? break; } try { Thread.sleep(60 * 1000 * 1000); // 周期一个小时 } catch (InterruptedException ex) { log.info(ex); this.interrupt(); // 关键步骤 } } } public void makeStop (){ this.interrupt(); } }
这段代码有几处改动:
对于第一、二处改动还比较好理解,isInterrupted方法应该是用来判断线程是不是出于中断状态,而interrupt方法则是用来抛出InterruptedException。但是对于第三处,为什么在catch块中又调用了一次interrupt方法呢?这是因为interrupt方法的实现比较特殊,当线程阻塞在wait、sleep、join方法上时,调用interrupt方法只会抛出异常,但是isInterrupted位不会被置,依然保持false。而在其它时候调用interrupt方法时(比如在catch块里),则会将isInterrupted位置为true,却不会抛出异常。
代码经过上述修改后,线程即使在sleep过程中,只要调用了interrupt方法,线程也会立即被唤醒,继而中止。
这里留一个问题,上面代码中加注释的问题处,为何要做这个判断?
如果你是一位有多年Java开发经验的程序员,你会发现从JDK 1.4开始,原先Thread类上定义的“很有用的”方法stop、suspend、resume被集体标记为废止。有关stop的废止原因,上面已经提到了一部分,即即使调用了这三个方法,线程也不保证马上响应。这篇官方文档也对废止的理由进行了阐述。
我的补充观点是,线程何时应该被中止(或者挂起、恢复),是应该由线程的开发人员决定的,而不是向外提供一个公共方法这么简单。只有线程的开发人员,才能够了解这个线程的内部逻辑,是简单的分支循环,还是有很深的调用栈(如果简单地提供stop方法,线程应该在哪一层调用栈弹出时被中止?),或者混杂着一些不可控的方法调用(比如阻塞式IO操作),甚至于启动了其它线程。就像前面的那个问题,在sleep前加了if判断,是因为“我”(线程开发人员)认为线程在那个点上是可以也是应该能够被中止的。
上面的代码片段展示了如何中止一个线程,其实稍作修改,就可以实现暂停/恢复操作。
public class StatusCheckingThread extends Thread { private volatile boolean running = true; public void run (){ while (!this.isInterrupted()) { if (running) { // 具体实现 } if (this.isInterrupted()) { // 问题:这里为什么要做判断? break; } try { Thread.sleep(60 * 1000 * 1000); // 周期一个小时 } catch (InterruptedException ex) { log.info(ex); this.interrupt(); // 关键步骤 } } } public void makeStop (){ this.interrupt(); } public void makeSuspend(){ running = false; } public void makeResume (){ running = true; } }
-- EOF --
除非注明(如“转载”、“[zz]”等),本博文章皆为原创内容,转载时请注明: 「转载自程序员的信仰©」
本文链接地址:如何正确地暂停、恢复、中止你的线程
Jay Xu
6年前在IBM给DW投稿的文章,昨天刚贴上来~
Xiyue Deng
线程终止还是不如进程可靠,处理不好容易搞死整个进程。如果用程序逻辑来控制, fiber 和 coroutine 在这方面更有效。
另外写这么长辛苦!
EDIT: 刚发现是 6 年前的。。