1.8 如何正确终止线程

线程通过start()方法启动后,会在run()方法执行结束后进入终止状态。

那么我们如何终止一个正在运行的线程呢?大家应该都知道stop()方法,这个方法是不安全的,因为该方法会导致两个问题。

• 立即抛出ThreadDeath异常,在run()方法中任何一个执行指令都可能抛出ThreadDeath异常。

• 会释放当前线程所持有的所有的锁,这种锁的释放是不可控的。

下面来看一段示例代码:

ThreadStopExample演示了通过stop()方法中断一个线程造成的问题,该代码的运行结果如下。

从运行结果可以看出两个问题:

• 在run()方法中,代码System.out.println("the code that it will be executed");还未执行,就因为ThreadDeath异常导致线程中断了,造成业务处理的不完整性。

• 观察内容Running..17103java.lang.ThreadDeath,我们使用的是println()方法,但是这里并没有换行,为什么呢?我们来看一下println()方法的代码。

println()方法包含两个操作,一个操作是输出print(x),另一个操作是换行,为了保证两个操作的原子性,增加了synchronized同步锁,理论上来说不应该出现问题。但是stop()方法会释放synchronized同步锁,使得这两个操作不是原子的,从而导致newLine()方法还没执行,线程就被中断了。

因此,在实际应用中,一定不能使用stop()方法来中断线程,那么如何安全地实现线程的中断呢?

1.8.1 关于安全中断线程的思考

在Thread中提供了一个interrupt()方法,从名字上来看表示中断线程,但是实际上它并没有像stop()方法那样提供可以直接中断线程的功能,而是基于一个信号量来进行线程中断的通知。在了解interrupt()方法之前,不妨来思考一下,如果我们想让一个线程安全中断,应该怎么做?

实际上,在线程异步运行过程中,该线程的执行情况只有自己知道,如果想要中断一个正在运行的线程,很显然不能直接从外部强制中断,只能由运行的线程自己来决定,这样才能保证中断过程的安全性。为了达到这个目的,我们需要做两件事情。

• 外部线程需要发送一个中断信号给正在运行的线程。

• 正在运行的线程需要根据这个信号来判断是否终止线程。

如图1-7所示,这是对线程安全中断模型的猜想,InterruptThread线程向正在运行的线程RunningThread发送一个中断信号,RunningThread线程收到该信号之后,在run()方法中提供一个信号判断的逻辑,从而达到线程中断的目的。简单来说,只有把线程中断全力交给正在运行的线程,才能真正意义上达到安全中断的目的。

图1-7 线程安全中断模型猜想

1.8.2 安全中断线程之interrupt

在Thread中提供了一个interrupt()方法,用来向指定线程发送中断信号,收到该信号的线程可以使用isInterrupted()方法来判断是否被中断,具体代码如下。

在上述代码中,创建了一个线程InterruptExample,该线程使用while()循环不断进行空转。while()循环的判断条件是Thread.currentThread().isInterrupted(),它表示当前线程中断的标记状态,默认是false,一旦其他线程通过interrupt()方法对该线程进行中断,那么循环判断条件就会变成true,这会导致while()循环条件被破坏,从而使线程执行结束。

上述代码的输出结果如下。

从这个实例中可以发现,interrupt()方法并没有武断地把运行中的线程停止,而是通过传递标识的方式让运行的线程自己决定是否停止。这意味着该线程在收到该信号后,可以继续把run()方法中的指令运行完成,最后让run()方法安全执行结束,完成线程的中断功能。

1.8.3 如何中断处于阻塞状态下的线程

假设一个线程处于阻塞状态,通过interrupt()方法可以中断吗?答案是可以的,那么怎么做呢?

如果线程因为sleep()、Object.wait()等方法阻塞,而其他线程想通过interrupt()方法对该线程进行中断,那么这个线程必须先被唤醒,否则无法响应中断信号。

在BlockedThreadInterruptExample这个案例中,演示了一个被sleep()方法阻塞的线程的中断过程,代码如下。

上述代码运行后,会输出如下结果,但是线程并没有结束,因为“线程被中断”这句话没有被打印出来。

也就是说,当调用interrupt()方法中断BlockedThreadInterruptExample线程时,该线程抛出了InterruptedException异常,说明interrupt()方法会先唤醒被阻塞的线程。

但是从运行结果来看,中断前后的标识都是false,这是否意味着interrupt()方法无法处理这种情况呢?其实不是,这里仍然涉及中断权问题,当被阻塞的线程被其他线程使用interrupt()方法唤醒时,在抛出InterruptedException异常之前,会先把线程中断状态进行复位,也就是将中断标记变成false。

注意,除InterruptedException被动触发线程复位外,还有一个Thread.interrupted()方法可以主动触发线程中断标识的复位。

这样设计的目的,仍然是把线程中断的选择权交给正在运行的线程,我们可以在捕获的异常中实现一些后置操作,最终决定是否要中断该线程。

因此,我们继续来看BlockedThreadInterruptExample线程,如果在线程抛出InterruptedException异常后,仍然要坚持中断,则再次调用Thread.currentThread().interrupt();即可,具体代码如下。

通过上述代码演示,我们发现对于涉及线程阻塞的方法如Thread.join()、Object.wait()、Thread.sleep()等,都会抛出InterruptedException异常,之所以抛出这个异常,是因为如果需要让一个处于阻塞状态下的线程被中断,那么该线程必然需要先被唤醒并响应中断请求,而InterruptedException就是一种响应方式。

一旦开发者捕获这个异常,就说明当前线程收到了中断请求,我们可以在这个中断异常中,根据实际业务情况进行相应的资源回收及后置处理。需要注意的是,InterruptedException在抛出之前会先对线程中断标识进行复位,目的是让运行的线程自己来决定何时中断。

因此,InterruptedException异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,比如:

• 直接捕获异常不做任何处理。

• 将异常往外抛出。

• 停止当前线程,并打印异常信息。

1.8.4 interrupt()方法的实现原理

在interrupt()方法触发中断之后,从被中断的线程使用Thread.currentThread().isInterrupted()方法来判断中断状态来看,似乎是基于共享一个boolean变量来实现通信的,但是我们通过isInterrupted()方法的源码发现,isInterrupted()方法调用了一个native()方法,返回一个boolean值,代码如下:

native()是一个本地方法,它是非Java语言实现的一个接口,使用C/C++语言在其他文件中定义实现。简单地说,native()方法就是在Java中声明的可以调用非Java语言的方法。

下面我们来看interrupt()方法,发现该方法中也调用了一个interrupt()的本地方法,由此我们不难想象到,线程中断的信号标识是在JVM中实现的,具体代码如下:

于是,笔者下载了hotspot的源码,在Thread.cpp文件中找到interrupt()方法的实现,代码如下。

在Thread::interrupt()方法中最终调用了os::interrupt()方法,这个方法的实现在os_*.cpp文件中,其中“*”代表不同的操作系统,如图1-8所示。由于JVM是跨平台的,所以对于不同的操作系统,线程的调度方式是不一样的。

图1-8 os_*.cpp文件

我们以os_linux.cpp文件为例,找到os::interrupt()方法的定义,代码如下:

上述代码是用C++写的,有些地方读者不一定能看明白,我们主要关注以下两个部分。

• osthread->set_interrupted(true),设置中断标识为true。

• ((JavaThread*)thread)->parker()->unpark();,unpark()方法用来唤醒线程。

这两个部分正好印证了前面我们使用interrupt()方法实现的效果,其中set_interrupted()方法是在OSThread中定义的,于是我们定位到osThread.hpp文件,找到该方法的定义代码如下:

终于,我们在JVM的源码中看到了_interrupted中断标记,它是使用volatile修饰的int类型的变量,该变量有两个值:1和0,其中1代表true,0代表false。另外,这里还提供了一个interrupted()方法,返回一个中断标识的结果。

如图1-9所示,该图表示interrupt()方法的实现原理,当Thread A调用interrupt()方法时,会调用一个native()方法修改JVM中定义的一个interrupted变量,Thread B通过isInterrupted()方法来获得这个变量的值,进而判断当前的中断状态。注意,interrupted字段使用了volatile修饰,表示它提供了可见性保障。

图1-9 interrupt()方法的实现原理