任务&&线程的取消与关闭机制_中断
任务和线程的启动很容易。但要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事。Java没有提供任何机制来安全地终止线程。但它提供了中断(Interruption),这是一种协作机制,能够使一个线程终止另一个线程的当前工作。
这种协作方式是很必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用一种协作的方式:当需要停止时,他们首先会清除当前正在执行的工作,然后在结束。这提供了更好的灵活性。因为任务本身的代码比发出取消请求的代码更清楚如何执行清楚操作。
使用“已请求取消”标志取消关闭任务和线程
看代码演示
package sync;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
/**
*/
public class PrimeGenerator implements Runnable {
private final List<BigInteger> primes = new ArrayList<BigInteger>();
private volatile boolean cancelled;//通过标志位取消线程任务的执行
@Override
public void run() {
BigInteger p = BigInteger.ONE;
while (!cancelled) {
p = p.nextProbablePrime();
synchronized (this) {
primes.add(p);
}
}
}
public void cancel() {
cancelled = true;
}
public synchronized List<BigInteger> get() {
return new ArrayList<BigInteger>(primes); //注意这里要重新new一个对象,防止外部修改
}
public static void main(String args[]) throws InterruptedException {
PrimeGenerator generator = new PrimeGenerator();
new Thread(generator).start();
try {
Thread.sleep(1000);
} finally {
generator.cancel();
}
List<BigInteger> ps = generator.get();
for (BigInteger b : ps) {
System.out.println(b);
}
return;
}
}
PrimeGenerator 采用了一种简单的取消策略:客户代码通过调用cancel来请求取消,PrimeGenerator 在每次搜索素数前首先检查是否存在取消请求,如果存在则退出。
PrimeGenerator 中的取消机制最终会使搜索素数的任务退出,但在退出过程中需要花费一定的时间。然而,如果使用了这种方法的任务调用了一个阻塞方法,例如BlockingQueue.put, 那么可能会产生一个更严重的问题——任务可能永远不会检查取消标志,因此永远不会结束。
通过中断取消关闭任务和线程
线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉他在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。
这好比是家里的父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。
每一个线程都有一个boolean类型的中断状态。当中断线程时,这个线程的中断状态将被设置为true。在Thread中包含了中断线程以及查询线程中断状态的方法。
Java中断模型也是这么简单,每个线程对象里都有一个boolean类型的标识(不一定就要是Thread类的字段,实际上也的确不是,这几个方法最终都是通过native方法来完成的),代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为true,然后线程2可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。
Thread中的中断方法:
public void interrupt():能中断目标线程(interrupt方法仅仅只是将中断状态置为true)
public boolean isInterrupted():返回目标线程的中断状态
public static boolean interrupted():将清除当前线程的中断状态,这也是清除中断状态的唯一方法
interrupt方法是唯一能将中断状态设置为true的方法。静态方法interrupted会将当前线程的中断状态清除。
/**
* Tests whether the current thread has been interrupted. The
* <i>interrupted status</i> of the thread is cleared by this method. In
* other words, if this method were to be called twice in succession, the
* second call would return false (unless the current thread were
* interrupted again, after the first call had cleared its interrupted
* status and before the second call had examined it).
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> if the current thread has been interrupted;
* <code>false</code> otherwise.
* @see #isInterrupted()
* @revised 6.0
*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/**
* Tests whether this thread has been interrupted. The <i>interrupted
* status</i> of the thread is unaffected by this method.
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> if this thread has been interrupted;
* <code>false</code> otherwise.
* @see #interrupted()
* @revised 6.0
*/
public boolean isInterrupted() {
return isInterrupted(false);
}
阻塞库方法,例如Thread.sleep()和Object.wait()等,都会检查线程何时中断,并且在发现中断时提前返回。它们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束。JVM并不能保证阻塞方法检测到中断的速度,但是实际情况下响应的速度还是非常快的。
当线程在非阻塞状态下中断时,他的中断状态将被设置,然后根据将被取消的操作来检查中断状态以判断发生了中断。通过这样的方法,中断操作将变得“有黏性”——如果不触发InterruptException,那么中断状态将一直保持,直到明确地清除中断状态。
调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。
通常,中断是实现取消的最合理方式。使用中断来取消任务和线程的示例
package sync;
import java.math.BigInteger;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
*/
public class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
public PrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
BigInteger p = BigInteger.ONE;
while (!Thread.currentThread().isInterrupted()) {
queue.put(p = p.nextProbablePrime());
}
} catch (InterruptedException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
}
}
// 中断线程,将中断标志位设为 true
public void cancel() {
interrupt();
}
public synchronized BigInteger[] get() {
return queue.toArray(new BigInteger[queue.size()]);
}
public static void main(String args[]) throws InterruptedException {
BlockingQueue q = new ArrayBlockingQueue(500);
PrimeProducer primeProducer = new PrimeProducer(q);
primeProducer.start();
try {
Thread.sleep(2000); //使当前线程等待2秒
} finally {
primeProducer.cancel(); //中断线程
}
if (primeProducer.isInterrupted()) {
BigInteger[] bs = primeProducer.get();
for (BigInteger b : bs) {
System.out.println(b);
}
}
}
}
在每次迭代循环中,有两个位置可以检测出中断:在阻塞的put方法调用中,以及在循环开始处查询中断状态时。由于调用了阻塞的put方法(线程内部在执行时可以检查中断状态这个值,来获知此线程是否应该结束了),因此这里并不一定需要进行显式的检测,但执行检测会使PrimeProducer对中断具有较高的响应性,因为他是在启动寻找素数任务之前检查中断的,而不是在任务完成之后。如果可中断的阻塞方法的调用频率并不高,不足以获得足够的响应性,那么显式的检测中断状态能起到一定的帮助作用。
能够设置中断状态的方法
除了 Thread.interrupt() 方法以外,下列 JDK 中的方法也会设置中断(也是通过调用 Thread.interrupt() 来实现的):
FutureTask.cancel()
ExecutorService.shutdownNow() 这个方法会调用线程池中所有线程的中断方法,不论它们是空闲的还是运行中的
ExecutorService.shutdown() 方法只能中断空闲的线程
上面只是举两个 JDK 中应用到了线程中断的例子,这样的例子还有很多,就不一一列举了。当然,为了能响应中断,在你所写的 Runnable 或 Callable 代码中,必须通过 Thread.isInterrupted()、Thread.interrupted() 方法,或者捕获 InterruptedException 等的中断异常来发现线程中断并处理,否则线程是不会自行提前结束的。
中断的响应和处理
InterruptedException 是最常见的中断表现形式。所以如何处理 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 之后,便可不用在恢复线程中断了(当然,如果有回滚等的需求,还是需要实现的)。因为通过异常的捕获,你已经可以正确终止线程了。
但是,如果不是上述情况,你所写的,捕获 InterruptedException 的方法会被其它的、非线程池类的方法调用。例如有 A, B 两个方法,A 被 B 方法调用,A 中捕获 InterruptedException 后没有恢复线程中断,而 B 方法是一个无限循环,通过检查线程中断来退出,或者在调用 A 之后有个阻塞的方法。那便会造成线程无法按照期望被终止的情况发生。
自己抛出InterruptedException
有时候,你需要“无中生有”地创造出一个 InterruptedException 以表示中断的发生。当然,在这个时候,你也需要使用 Thread.isInterrupted() 或 Thread.interrupted() 来检测中断的发生。其实看了前面的部分我们知道,抛出 InterruptedException 时,线程中断状态需要被清除()。这就是 Thread.interrupted() 的作用。其实,你要是看 JDK 源代码,就会发现,JDK 中并发类也是这么做的。
NOTE: 使用 Thread.interrupt() 和 InterruptedException 中的哪种方法表示中断?
上面提到了一种情况是由于接口的限制而无法抛出 InterruptedException,这时你别无选择,只能用 Thread.interrupt() 恢复中断。除了这种情况,其它的时候推荐使用 InterruptedException 来表示中断。当方法声明抛出 InterruptedException 时,它就是在告诉调用者,我这个方法可能会花费很多的时间,而你可以通过线程中断来终止调用。通过 InterruptedException 来表示中断,含义更清晰,反应也更迅速。
===========END===========