要使任务和线程能安全地停下来,并不是一件容易的事。如果某个任务、线程、服务立即停止,会使共享的数据结构处于不一致的状态。当需要停止时,它们首先会清除当前正在执行的工作,然后结束。
1. 任务取消
如果外部代码能在某个操作正常完成之前将其置入"完成"状态,那么这个操作就称为可以取消的。
取消某个操作的原因:
- 用户请求取消
- 有时间限制的操作:计时器超时
- 应用程序事件
- 错误:运行期错误
- 关闭:当程序或者服务关闭时
设置某个“已请求取消”标志,而任务定期查看这个标志,如果取消,那么任务将提前结束。
package chapter6;
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;
public void run(){
BigInteger p = BigInteger.ONE;
System.out.println(Thread.currentThread().getName());
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);
}
public static void main(String[] args) throws InterruptedException{
PrimeGenerator primeGenerator = new PrimeGenerator();
Thread thread = new Thread(primeGenerator);
System.out.println(Thread.currentThread().getName());
thread.start();
Thread.sleep(3000);
primeGenerator.cancel();
System.out.println(primeGenerator.get());
}
}
一个可取消的任务必须拥有取消策略:即其它代码如何(How)请求取消该任务,任务在何时(When)检查是否已经请求了取消,以及在响应取消请求时应该执行那些(what)操作。
中断
上面的代码有个问题:如果调用了一个阻塞方法(BlockingQueue.put),那么任务可能永远不会检查取消标志,因此永远不会结束。
import java.math.BigInteger;
import java.util.concurrent.BlockingQueue;
public class BrokenPrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
private volatile boolean cancelled = false;
public BrokenPrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
@Override
public void run() {
try{
BigInteger p = BigInteger.ONE;
while(!cancelled){
queue.put(p = p.nextProbablePrime());
}
} catch (InterruptedException e){
}
}
public void cancel(){
this.cancelled = true;
}
void consumePrimes() throws InterruptedException{
BlockingQueue<BigInteger> primes =;
BrokenPrimeProducer producer = new BrokenPrimeProducer(primes);
producer.start();
try{
while(needMorePrimes())
consume(primes.take());
}finally {
producer.cancel();
}
}
}
Thread中的中断方法:
public class Thread{ /*中断目标线程*/ public void interrupt(){...} /*f返回目标线程的中断状态*/ public boolean isInterrupted(){...} /*清除当前线程的中断状态*/ public static boolean interrupted(){...} ... } Thread.sleep()和Object.wait()等方法都会检查合适中断,并且发现中断时返回。 他们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException异常, 表示阻塞操作由于中断提前结束。 try{}catch(InterruptedException e){}
中断策略
最合理的中断策略是某种形式的线程级取消操作或服务级取消操作: 尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。
区分任务和线程对中断的反应很重要。一个中断请求可以有一个或者多个接收者---中断线程池中的某个工作者线程,同时意味着“取消当前任务"和”关闭工作者线程“。大多数可阻塞的库函数都只是抛出InterruptedException作为中断响应。由于它们实现了最合理的取消策略:尽快退出执行流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作。
由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则不应该中断这个线程。
响应中断
调用可中断的阻塞函数时(Thread.sleep,BlockingQueue.put等),有两种策略处理InterruptedException:
- 传递异常,从而使你的方法也成为可中断的方法。
- 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理。
// 传递InterruptedException与将InterruptedException添加到throws子句一样简单
BlockingQueue<Task> queue;
...
public Task getNextTask() throws InterruptedException {
return queue.take();
}
如果不想或无法传递InterruptedException,那么需要寻找另一种方式来保存中断请求。一种标准的方式就是通过再次调用interrupt来恢复中断状态【不能屏蔽InterruptedException】。只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和代码中都不应该屏蔽中断请求。
package chapter7;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
/**
* 〈一句话功能简述〉<br>
* 〈〉
*
* @author 我们
* @create 2021/1/22
* @since 1.0.0
*/
public class InterruptedExceptionDemo {
public static Integer getNextTask(BlockingQueue<Integer> queue){
boolean interrupted = false;
try{
while(true){
try{
return queue.take();
} catch (InterruptedException e){
interrupted = true;
// 重新尝试
}
}
} finally {
if (interrupted){
Thread.currentThread().interrupt(); // 再次调用interrupt()恢复中断
}
}
}
public static void main(String[] args) throws InterruptedExcepiton{
BlockingQueue<Integer> queue = new LinkedBlockingDeque<>();
queue.add(1);
queue.add(2);
queue.add(3);
queue.add(4);
System.out.println(getNextTask(queue));
System.out.println(getNextTask(queue));
System.out.println(getNextTask(queue));
}
}
示例:计时运行
private static final SchedulExecutorService cancelExec = ...;
// 如果任务在超时之前完成,那么中断timedRun所在线程的取消任务将在timedRun返回到调用者之后启动,
// 如果任务不响应中断,那么timedRun会在任务结束时返回,此时已经超过了指定的时限
public static void timedRun (Runnable r, long timeout, TimeUnit unit){
final Thread taskThread = Thread.currentThread();
cancelExec.schedule(new Runnable(){
@Override
public void run{
// 在中断线程之前,应该了解它的中断策略。由于timeRun()可以被任意一个线程调用,因此无法
// 知道这个调用线程的中断策略(处理InterruptedException的方法)
taskThread.interrupt();
}, timeout, unit);
r.run();
}
它依赖一个限时的join,因此存在join不足:无法知道执行控制是因为线程正常退出而返回还是因为join超时而返回。
public static void timedRun(final Runnable r, long timeout, TimeUnit unit)
throws InterruptedException{
// 用RethrowableTask来捕获r执行过程中的异常
class RethrowableTask implements Runnable{
// 由于Throwable将在两个线程之间共享,所以声明为volatile,
// 确保任务线程InterruptedException发布到timedRun中
private volatile Throwable t;
public void run(){
try{ r.run(); }
catch (Throwable t) { this.t = t; }
}
void rethrow(){
if (t != null){
throw launderThrowable(t);
}
}
RethrowableTask task = new RethrowableTask();
final Thread taskThread = new Thread(task);
taskThread.start(); // 执行任务
cancelExec.schedule(new Runnable(){
public void run() { taskThread.interrupt(); }
}, timeout, unit);
// 在当前线程中调用另外一个线程的join方法:则当前线程转为阻塞状态,等待调用join线程结束
taskThread.join(unit.toMillis(timeout)); // taskThread等待Runnable任务结束
task.rethrow(); // 抛出异常
}
通过Future来实现取消
我们已经使用了一种抽象的机制来管理任务的生命周期,处理异常,以及实现取消,即Future。
Future future = ExecutorService.submit(); boolean cancel(boolean mayInterruptIfRunning);
mayInterruptIfRunning:表示取消操作是否成功。 1)true且任务正在某个线程中运行,那么这个线程能够被中断。 2)false:若任务还有没有启动,就不要运行它。 什么情况下调用cancel可以将参数指定为true? 由标准的Executor创建的,它实现了一种中断策略使得任务可以通过中断被取消,所以如果任务在标准Executor中运行,并通过它们的Future来取消任务,那么可以设置true,当尝试取消某个任务时, 不宜直接中断线程池。
public static void timedRun(Runnable r, long timeout, TimeUnit unit)
throws InterruptedException{
Future<?> task = taskExec.submit(r); // 用线程池执行任务
try{
task.get(timeout, unit); // 设置超时时间
} catch (TimeoutException e){
// 接下来任务将被取消
} catch (ExecutionException e){
// 如果在任务中抛出异常,那么重新抛出该异常
throw launderThrowable(e.getCause());
} finally {
// !!!如果任务已经结束,那么执行取消操作也不会带来任何影响!!!
task.cancel(true); // 如果任务正在运行,那么将被中断
}
}
当Future.get()抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel()来取消任务。