Java编程并发实战
当我们点击某个杀毒软件的取消按钮来停止查杀病毒时,当我们在控制台敲入quit命令以结束某个后台服务时……都需要通过一个线程去取消另一个线程正在执行的任务。Java没有提供一种安全直接的方法来停止某个线程,但是Java提供了中断机制。
一、中断的基本概念
Java的中断是一种协作机制,也就是说通过中断并不能直接中断另外一个线程,而需要被中断的线程自己处理中断。
在Java的中断模型中,每个线程都有一个boolean标识,代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为true,然后线程2可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。
java.lang.Thread类提供了几个方法来操作这个中断状态,这些方法包括:
方法
描述
public static boolean interrupted
测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。此方法内部调用的死私有的Thread.currentThread().isInterrupted(true); 带true参数的isInterrupted方法意思是返回当前线程的中断状态,然后reset当前线程的中断状态
public boolean isInterrupted()
测试线程是否已经中断。线程的中断状态不受该方法的影响。内部调用的私有的参数为false的isInterrupted(false)方法
public void interrupt()
中断线程,将中断状态设为true
java类库中的有些类的方法也可能会调用中断,如FutureTask中的cancel方法,如果传入的参数为true,它将会在正在运行异步任务的线程上调用interrupt方法,如果正在执行的异步任务中的代码没有对中断做出响应,那么cancel方法中的参数将不会起到什么效果;又如ThreadPoolExecutor中的shutdownNow方法会遍历线程池中的工作线程并调用线程的interrupt方法来中断线程,所以如果工作线程中正在执行的任务没有对中断做出响应,任务将一直执行直到正常结束。
通常,中断是实现取消的最合理方式。
二、如何发出中断
在java中,中断一个线程的唯一方式是调用interrupt方法。
三、如何接收中断
如果是自己编写的方法,想要处理中断,那么只能通过Thread.currentThread().isInterrupted()方法 手动检测,通常放在一个while循环当中。
如果是调用了可中断的阻塞函数时,可以通过try{}catch{InterruptedException e} 来捕捉中断异常,即接收中断。
四、如何处理接收到的中断
如果是自己编写的方法,那么视情况自己处理即可。
如果是接收到了可中断的阻塞函数抛出的中断异常,那么有两种实用策略来处理异常:
传递异常(可能在执行某个特定于任务的清除操作之后),从而使你的方法也成为可中断的阻塞方法
恢复中断状态,从而使调用栈中的上层代码能够对其进行处理。
例如java中一些特殊的阻塞库的方法支持中断,使用的第一种处理异常的方法,例如Thread.sleep和Object.wait等,都会检查线程何时中断,并且在发现中断时提前返回。他们在响应中断时执行的操作包括:
清除中断状态(将中断状态置为false)
抛出InterruptedException,表示阻塞操作由于中断而提前结束。
有时,你可能不想传递InterruptedException或无法传递(或许通过Runnable传递任务),那么需要寻找另一种方式来保存中断请求。一种标准的方法就是通过再次调用interrupt方法来恢复中断状态(因为java中可响应中断的阻塞方法在向上抛出异常的同时均清除了中断状态)。
五、如何处理不可中断的阻塞
在java库中,许多可阻塞的方法都是通过提前返回或者抛出InterruptedException来响应中断请求的,比如BlockingQueue.put、BlockingQueue.take、Object.wait、Thread.sleep等,从而使开发人员更容易构建出能响应取消请求的任务。然而,并非所有的可阻塞方法或者阻塞机制都能响应中断;如果一个线程由于执行同步的Socket I/O或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有任何其他任何作用。对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但要求我们必须知道线程阻塞的原因。
java.io包中的同步Socket I/O 。在服务器应用程序中,最常见的阻塞I/O形式就是对套接字进行读取和写入。虽然InputStream和OutputStream中的read和write等方法都不会响应中断,但通过关闭底层的套接字,可以使得由于执行read或write等方法而被阻塞的线程抛出一个SocketException。
java.io包中的同步I/O。当中断一个正在InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptedException并关闭链路(这还会使得其他在这条链路上阻塞的线程同样抛出ClosedByInterruptedException)。当关闭一个InterruptibleChannel时,将导致所有在链路操作上阻塞的线程都抛出AsynchronousCloseException。大多数标准的Channel都实现了InterruptibleChannel。
Selector的异步I/O。如果一个线程在调用Selector.select方法(在java.nio.channels中)时阻塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前退出。
获取某个锁。如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求。但是在Lock类中提供了lockInterruptible方法,该方法允许在等待一个锁的同时仍能响应中断。
六、中断的使用
通常,中断的使用场景有以下几个:
点击某个桌面应用中的取消按钮时
某个操作超过了一定的执行时间限制需要中止时
多个线程做相同的事情,只要一个线程成功其它线程都可以取消时
一组线程中的一个或多个出现错误导致整组都无法继续时
当一个应用或服务需要停止时。