这一节介绍并发编程中如何对任务执行取消操作。java中没有提供停止线程的方法,只用通过协作——使代码遵循一定的协议来请求取消。
轮询检查
一种常见的取消任务的办法是轮询检查。线程在执行的过程中通过定期检查一个boolean变量来决定是否终止任务。下面是一个使用轮询检查来终止素数生成的例子:
public class PrimeGenerator implements Runnable {
private static ExecutorService exec = Executors.newCachedThreadPool();
private final List<BigInteger> primes
= new ArrayList<BigInteger>();
private volatile boolean cancelled;
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);
}
}
但是轮询检查也存在问题,比如在循环中调用一个阻塞方法,如BlockingQueue.put,如果该方法阻塞,则意味着在阻塞结束之前都不会进入下一轮循环,也就不会再检查boolean变量了,因而阻塞期间无法终止该任务。
中断
对于上面的问题,更常用的方法是使用中断(interrupt)机制。在java中,每一个线程都有一个中断标识,一个线程t1调用另一个线程t2的interrupt方法,是将t2的中断标识设置成了true,使t2变成中断状态,但对于t2是否终止自己的任务,由t2自己决定。至于线程如何终止当前任务,这与具体场景和业务逻辑相关。
下面是一个对阻塞方法使用中断取消任务的例子:
public class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
PrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
public void run() {
try {
BigInteger p = BigInteger.ONE;
while (!Thread.currentThread().isInterrupted())
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException consumed) {
//允许线程退出
}
}
public void cancel() {
interrupt();
}
}
在这个例子中,在每次执行循环之前都会判断线程是否处于阻塞状态,若是则不会再执行任务。
InterruptException
在上面的例子中,方法会捕获一个InterruptException异常,那么什么时候会引发InterruptException呢?当一个线程处于中断状态(isInterrupted返回true)时调用了导致阻塞的方法(如sleep,join,wait),或者一个线程阻塞期间被interrupt导致中断,就会引发InterruptException,InterruptException被抛出后线程的中断状态会被消除(重置为false)。
对于InterruptException,有两种处理办法,一种是将其传递给调用者(在方法上使用throws);二是将其捕获并且恢复中断状态(Thead.currentThread.interrupt()
),并根据中断策略进行处理。一般来说不应该掩盖InterruptException,就是在捕获之后什么也不做,除非我们知道这么做不会引起任何不良后果。
如果不清楚一个线程的中断策略,就不要调用interrupt方法,因为我们不知道有什么任务在运行、线程在被中断之后又做了什么。
常用的捕获异常的代码:
try {
task.get(timeout, unit);
} catch (TimeoutException e) {
// 之后任务会被取消
} catch (ExecutionException e) {
// 任务执行过程中引发的异常,将其重新抛出
throw launderThrowable(e.getCause());
} finally {
//当任务已经结束可以这么做
task.cancel(true);
}
不可中断阻塞
对于一些不能响应中断的阻塞如socket IO、等待获取内部锁等,一种办法是重写interrupt方法 ,然后在interrupt方法中执行取消操作。下面是一个取消socket IO的例子,通过重写interrupt方法,关闭底层socket:
public class ReaderThread extends Thread {
private static final int BUFSZ = 512;
private final Socket socket;
private final InputStream in;
public ReaderThread(Socket socket) throws IOException {
this.socket = socket;
this.in = socket.getInputStream();
}
public void interrupt() {
try {
socket.close();
} catch (IOException ignored) {
} finally {
super.interrupt();
}
}
public void run() {
try {
byte[] buf = new byte[BUFSZ];
while (true) {
int count = in.read(buf);
if (count < 0)
break;
else if (count > 0)
processBuffer(buf, count);
}
} catch (IOException e) {
/* 允许线程退出 */
}
}
public void processBuffer(byte[] buf, int count) {
}
}
停止基于线程的服务
除非拥有某个线程,否则不应该取消或操纵它。线程的拥有者就是创建线程的类,当使用线程池时,停止线程应该由线程池负责。
在应用程序中,包含多个服务,而服务可能拥有工作线程。应用程序不应该直接关闭线程,正确的做法是应用程序关闭服务,由服务提供生命周期方法来终止线程。
实例:日志服务的关闭
这里实现了一个多生产者-但消费者的日志服务,有多个应用程序(生产者线程)将日志消息提交进队列,由一个消费者线程从队列中取出消息并打印输出。常用的关闭这种服务的做法是设置一个bool变量,当提交消息时检查该变量,若为true则通过抛出异常来终止当前生产者线程的提交操作;而消费者线程会不断处理消息直至队列为空。由于存在判断bool变量这一竞争条件,这就要求操作是原子的,而同时我们又希望不在插入队列的操作上加锁(这样多个生产者可以并发提交消息,不必获得锁),这通过更细粒度的锁来实现,代码:
public class LogService {
private final BlockingQueue<String> queue;
private final LoggerThread loggerThread;
private final PrintWriter writer;
private boolean isShutdown;
private int reservations;
public LogService(Writer writer) {
this.queue = new LinkedBlockingQueue<String>();
this.loggerThread = new LoggerThread();
this.writer = new PrintWriter(writer);
}
public void start() {
loggerThread.start();
}
public void stop() {
synchronized (this) {
isShutdown = true;
}
loggerThread.interrupt();
}
public void log(String msg) throws InterruptedException {
synchronized (this) {
if (isShutdown)
throw new IllegalStateException(/*...*/);
++reservations;
}
queue.put(msg);
}
private class LoggerThread extends Thread {
public void run() {
try {
while (true) {
try {
synchronized (LogService.this) {
if (isShutdown && reservations == 0)
break;
}
String msg = queue.take();
synchronized (LogService.this) {
--reservations;
}
writer.println(msg);
} catch (InterruptedException e) {
/* 重试 */
}
}
} finally {
writer.close();
}
}
}
}
致命药丸(poison pill)
致命药丸是一种关闭线程的机制:它是一个可识别的特殊的对象,置于队列中,工作者线程在获取该对象之后停止工作。当致命药丸应用于生产者-消费者模式时,生产者在提交了致命药丸之后不会再提交任何工作;消费者在获取致命药之后不会继续获取工作。致命药丸只有在生产者、消费者数量已知的情况下才能使用,例如有N个生产者和1个消费者,则消费者再接收了N个药丸时停止;反过来如果有1个生产者和N个消费者,则生产者需要向队列中放置N个药丸来停止任务。致命药丸通常用于无限队列中。下面的例子使用致命药丸来实现桌面文件搜索:
public class IndexingService {
private static final int CAPACITY = 1000;
private static final File POISON = new File("");
private final IndexerThread consumer = new IndexerThread();
private final CrawlerThread producer = new CrawlerThread();
private final BlockingQueue<File> queue;
private final FileFilter fileFilter;
private final File root;
public IndexingService(File root, final FileFilter fileFilter) {
this.root = root;
this.queue = new LinkedBlockingQueue<File>(CAPACITY);
this.fileFilter = new FileFilter() {
public boolean accept(File f) {
return f.isDirectory() || fileFilter.accept(f);
}
};
}
private boolean alreadyIndexed(File f) {
return false;
}
//生产者线程
class CrawlerThread extends Thread {
public void run() {
try {
crawl(root);
} catch (InterruptedException e) { /* fall through */
} finally {
while (true) {
try {
queue.put(POISON);
break;
} catch (InterruptedException e1) { /* retry */
}
}
}
}
private void crawl(File root) throws InterruptedException {
File[] entries = root.listFiles(fileFilter);
if (entries != null) {
for (File entry : entries) {
if (entry.isDirectory())
crawl(entry);
else if (!alreadyIndexed(entry))
queue.put(entry);
}
}
}
}
//消费者线程
class IndexerThread extends Thread {
public void run() {
try {
while (true) {
File file = queue.take();
if (file == POISON)
break;
else
indexFile(file);
}
} catch (InterruptedException consumed) {
}
}
public void indexFile(File file) {
/*...*/
};
}
public void start() {
producer.start();
consumer.start();
}
public void stop() {
producer.interrupt();
}
public void awaitTermination() throws InterruptedException {
consumer.join();
}
}
处理反常的线程终止
线程执行过程中抛出RuntimeException,或者其他未受检异常,导致线程死亡。这时我们需要通知框架,之后由框架根据需求是否创建新线程来取代该线程的工作。例:
public void run(){ Throwable thrown = null;
try{
while(!isInterrupted())
runTask(getTaskFromWorkQueue);
}catch(Throwable e){
thrown = e;
}finally{
threadExited(this, thrown);
}
}
处理未捕获的异常
对于未捕获的异常,通常需要将错误信息写入日志,这通过实现UncaughtExceptionHandler接口来实现。例:
public class UEHLogger implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e);
}
}
参考文献:《Java并发编程实战》