把Java多学一点——线程中断

对于比较耗时的任务,一般会采用专门的线程来执行,如果在执行过程中想要取消这类的任务的执行,那就需要借助Java线程中断机制。

​​为什么是线程中断而不是线程停止?

线程停止是使得线程终止,线程状态变为TERMINATED,但是对于目标工作者线程来说,可能除了用户想要取消的任务,还有其他任务要执行,或者说在终止线程之前还有工作需要处理,而线程中断则是将对中断的处理交给目标线程,由目标线程对发送给自己的中断请求进行响应,将线程的中断处理转为目标线程主动处理。

线程停止是目标简单但是实现并不简单的一件事情,Java标准库并没有可以直接停止线程的API(stop方法早已被废弃),停止线程的时候还有一些额外的细节需要考虑。

Java线程中断机制其实是线程间的一种协作方式:发起线程请求目标线程停止其正在执行的操作,而目标线程在收到发起线程的中断请求后进行相应的处理,处理的方式我们下文会提到。

中断实现:中断标记

那么发起线程发送中断请求给目标线程是如何实现的呢,这里涉及到一个重要的状态变量:中断标记。

Java平台会为每个线程对象(Thread实例)维护一个布尔类型的状态变量,被称作中断标记,该变量用于表示相应线程是否收到了中断,中断标记为true,则表示收到了中断请求。

关于线程中断操作,Thread类提供了三个方法:

public void interrupt();
复制代码

该方法为线程中断方法,发起线程调用目标线程的interrupt方法可以将目标线程的中断标记置为true。

public static boolean interrupted()
复制代码

通过调用interrupted方法检测线程的中断状态并清除中断状态。

public boolean isInterrupted();
复制代码

isInterrupted方法来判断线程是否收到中断,线程的中断状态不受该方法影响。

中断响应

目标线程在收到中断请求后所执行的操作称作中断响应。目标线程对中断的响应一般包括:

  • 无响应。发起线程通过调用目标线程interrupt()方法请求中断,设置目标线程的中断标记为true,但是目标线程无法对中断请求进行响应。IntputStream.read()、ReentrantLock.lock()和以及申请内部锁等方法就属于这种类型。
  • 取消任务的执行。发起线程通过调用目标线程的interrupt()方法来中断目标线程,目标线程在检测到中断那一刻所执行的任务被取消,但这并不会影响目标线程继续处理其他任务。
  • 工作者线程停止。发起线程通过调用目标线程的interrupt()方法会使得目标线程终止,线程状态变为TERMINATED,相当于线程停止。

InterruptedException

Java标准库中的许多阻塞方法对中断的响应方式都是抛出InterruptedException等异常,所以在应用层代码中,通常可以通过对InterruptedException等异常进行处理的方式来实现中断响应。

Java标准库中对中断的处理

public void lock() {
    sync.acquire(1);
}
​
public final void acquire(int arg) {
  if (!tryAcquire(arg) &&
  acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  selfInterrupt();
}
复制代码

该方法为ReentrantLock类的lock方法,该方法中没有发现进行判断线程中断标记的代码(没有调用isInterrupted()或interrupted()方法),即该方法不会对线程中断请求作任何响应,也就是说如果目标线程调用了该阻塞方法,如果有线程对该线程发送中断请求,相应线程不能对其作出响应。

ReentrantLock类还提供了一个同类方法:lockInterruptibly()

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
  throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  if (!tryAcquire(arg))
    doAcquireInterruptibly(arg);
}
复制代码

可以看到该方法在进行阻塞操作之前,会判断是否收到了中断请求,如果收到了中断请求便抛出InterruptedException异常,并且在抛出InterruptedException异常之前,会将中断标记置为false。

wait方法、sleep方法、join方法等对中断的响应也都是抛出InterruptedException异常,并且在抛出异常之前,会将中断标记置为false。

java-多线程-wait方法对中断响应方式

打断暂停

如果当发起线程向目标线程发送中断请求的那一刻目标线程由于调用阻塞方法而被暂停,生命周期状态为WAITING或BLOCKED,那么此时Java虚拟机可能会设置线程中断标记并将该线程唤醒,从而使得该目标线程被唤醒后继续执行的代码再次得到响应中断的机会,所以这种情况下响应中断的阻塞方法依然可以抛出InterruptedException异常,并在此之前将线程中断标记为空。

响应中断

前面说到,Java标准库中大部分阻塞方法/操作都是通过抛出InterruptedException等异常,那么在应用层代码通常可以通过对这些异常来进行响应中断请求。

对InterruptedException异常的处理方式一般包括以下几种:

  • 不捕获异常,将异常继续抛给上层代码。
  • 捕获InterruptedException并做一些中间处理,然后再将异常抛给上层代码。
  • 捕获InterruptedException并在捕获异常后中断当前线程。

总结:对InterruptedException的处理,如果当前代码不知道如何处理终端,要么将异常继续抛给上层代码由上层代码处理,要么继续保留中断标志为true,由其他代码来处理。

​​当线程在捕获到InterruptedException后就可以终止的情况下才可以吞没异常,其他情况切记不能吞没异常,即在捕获到interruptedException后既不重新抛出也不保留中断标志。则可能导致目标线程无法被终止。​