java没有提供任何机制来安全地终止线程,但提供了中断,这是一种协作机制。
7.1 任务取消
用一个volatile类型的域保存取消状态,这应该是最基本的做法了
@ThreadSafe
public class PrimeGenerator implements Runnable {
private static ExecutorService exec = Executors.newCachedThreadPool();
@GuardedBy("this") private final List primes
= new ArrayList();
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 get() {
return new ArrayList(primes);
}
static List aSecondOfPrimes() throws InterruptedException {
PrimeGenerator generator = new PrimeGenerator();
exec.execute(generator);
try {
SECONDS.sleep(1);
} finally {
generator.cancel();
}
return generator.get();
}
}
取消策略
- How:如何取消任务。
- When:何时检查是否请求了取消。
- What:响应取消请求时应该执行哪些操作。
7.1.1 中断
上述机制存在问题:如果调用了一个阻塞方法,那么可能永远不会检查取消标志。
class BrokenPrimeProducer extends Thread {
private final BlockingQueue queue;
private volatile boolean cancelled = false;
BrokenPrimeProducer(BlockingQueue queue) {
this.queue = queue;
}
public void run() {
try {
BigInteger p = BigInteger.ONE;
while (!cancelled)
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException consumed) {
}
}
public void cancel() {
cancelled = true;
}
}
如果queue.put因为队列满阻塞了,而消费者这时取消了任务就悲剧了。
所以,来看看中断,Thread中有一些中断方法,例如Interrupt(当然不是立刻停止目标线程,而是传递了请求中断的信息),and阻塞库方法会检查线程何时中断,所以,BrokenPrimeProducer的问题其实很好解决。
public class PrimeProducer extends Thread {
private final BlockingQueue queue;
PrimeProducer(BlockingQueue queue) {
this.queue = queue;
}
public void run() {
try {
BigInteger p = BigInteger.ONE;
while (!Thread.currentThread().isInterrupted())
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException consumed) {
/* Allow thread to exit */
}
}
public void cancel() {
interrupt();
}
}
感觉其实是Thread和类库方法自己做了检查状态的操作,你在外边做不到。
7.1.2 中断策略
这一节有点绕,大概就是强调了中断策略的重要性,以及中断操作应该由谁来做(调用方)。
7.1.3 响应中断
- 传递异常,不捕获直接throws就好了
- 恢复中断状态,Thread.currentThread().interrupt()
7.1.4 示例:计时运行
一种错误的做法:
private static final ScheduledExecutorService cancel Exec = ...;
public static void timeRun(Runnable r, long timeout, TimeUnit unit) {
final Thread taskThread = Thread.currentThread();
cancelExec.shedule(new Runnable() {
publick void run() { taskThread.interrupt(); }
}, timeout, unit);
r.run();
}
它破坏了以下规则:在中断线称之前,应该了解它的中断策略。
r.run()怎么运行?何时结束?interrupt发生在run返回之后会怎样?如果run不响应中断呢?
一种解决办法:join
public class TimedRun2 {
private static final ScheduledExecutorService cancelExec = newScheduledThreadPool(1);
public static void timedRun(final Runnable r,
long timeout, TimeUnit unit)
throws InterruptedException {
class RethrowableTask implements Runnable {
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);
taskThread.join(unit.toMillis(timeout));
task.rethrow();
}
}
不过join也有一点问题:无法知道线程是正常退出还是join超时(这个例子里好像看不出有什么影响?)
7.1.5 通过Future来实现取消
public class TimedRun {
private static final ExecutorService taskExec = Executors.newCachedThreadPool();
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) {
// task will be cancelled below
} catch (ExecutionException e) {
// exception thrown in task; rethrow
throw launderThrowable(e.getCause());
} finally {
// Harmless if task already completed
task.cancel(true); // interrupt if running
}
}
}
注意cancel的参数,一个boolean类型的mayInterruptIfRunning,如果为true并且任务在线程中运行,那么中断。false意味着如果任务还没启动,就不要运行它。
7.1.6 处理不可中断的阻塞
OK,到目前为止,我们处理的都是可以中断的操作(while(!cancel)),如果不可中断呢?例如:
- Socket IO,关闭socket,抛出SocketException。
- 同步IO,用InterruptibleChannel。
- Selector的异步IO,close或wakeup,抛出ClosedSelectorException。
- 获取某个锁,lockInterruptibly。
拿Sokcket举例:
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) { /* Allow thread to exit */
}
}
public void processBuffer(byte[] buf, int count) {
}
}
7.1.7 采用newTaskFor来封装非标准的取消
进一步优化ReaderThread,有点复杂,先记下来吧。
public abstract class SocketUsingTask implements CancellableTask {
@GuardedBy("this") private Socket socket;
protected synchronized void setSocket(Socket s) {
socket = s;
}
public synchronized void cancel() {
try {
if (socket != null)
socket.close();
} catch (IOException ignored) {
}
}
public RunnableFuture newTask() {
return new FutureTask(this) {
public boolean cancel(boolean mayInterruptIfRunning) {
try {
SocketUsingTask.this.cancel();
} finally {
return super.cancel(mayInterruptIfRunning);
}
}
};
}
}
interface CancellableTask extends Callable {
void cancel();
RunnableFuture newTask();
}
@ThreadSafe
class CancellingExecutor extends ThreadPoolExecutor {
public CancellingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public CancellingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public CancellingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public CancellingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
protected RunnableFuture newTaskFor(Callable callable) {
if (callable instanceof CancellableTask)
return ((CancellableTask) callable).newTask();
else
return super.newTaskFor(callable);
}
}
7.2 停止基于线程的服务
7.2.1 示例:日志服务
public class LogWriter {
private final BlockingQueue queue;
private final LoggerThread logger;
private static final int CAPACITY = 1000;
public LogWriter(Writer writer) {
this.queue = new LinkedBlockingQueue(CAPACITY);
this.logger = new LoggerThread(writer);
}
public void start() {
logger.start();
}
public void log(String msg) throws InterruptedException {
queue.put(msg);
}
private class LoggerThread extends Thread {
private final PrintWriter writer;
public LoggerThread(Writer writer) {
this.writer = new PrintWriter(writer, true); // autoflush
}
public void run() {
try {
while (true)
writer.println(queue.take());
} catch (InterruptedException ignored) {
} finally {
writer.close();
}
}
}
}
一个使用阻塞队列的简单示例。缺少完备的关闭机制。
补全:
public class LogService {
private final BlockingQueue queue;
private final LoggerThread loggerThread;
private final PrintWriter writer;
@GuardedBy("this") private boolean isShutdown;
@GuardedBy("this") private int reservations;
public LogService(Writer writer) {
this.queue = new LinkedBlockingQueue();
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) { /* retry */
}
}
} finally {
writer.close();
}
}
}
}
7.2.2 关闭ExecutorService
将管理线程的工作委托给一个ExecutorService
7.2.3 “毒丸”对象
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 queue;
private final FileFilter fileFilter;
private final File root;
public IndexingService(File root, final FileFilter fileFilter) {
this.root = root;
this.queue = new LinkedBlockingQueue(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();
}
}
只有在生产者和消费者的数量都已知的情况下,才可以使用“毒丸”对象。
只有在无界队列中,“毒丸”对象才能可靠的工作。
7.2.4 示例:只执行一次的服务
public class CheckForMail {
public boolean checkMail(Set hosts, long timeout, TimeUnit unit)
throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
final AtomicBoolean hasNewMail = new AtomicBoolean(false);
try {
for (final String host : hosts)
exec.execute(new Runnable() {
public void run() {
if (checkMail(host))
hasNewMail.set(true);
}
});
} finally {
exec.shutdown();
exec.awaitTermination(timeout, unit);
}
return hasNewMail.get();
}
7.2.5 shutdownNow的局限性
shudownNow会尝试取消正在执行的任务,并返回所有已提交但尚未开始的任务。问题是,我们无法知道哪些被取消执行的任务。通过封装ExecutorService并使得execute记录哪些任务是在关闭后取消的。
public class TrackingExecutor extends AbstractExecutorService {
private final ExecutorService exec;
private final Set tasksCancelledAtShutdown =
Collections.synchronizedSet(new HashSet());
public TrackingExecutor(ExecutorService exec) {
this.exec = exec;
}
public void shutdown() {
exec.shutdown();
}
public List shutdownNow() {
return exec.shutdownNow();
}
public boolean isShutdown() {
return exec.isShutdown();
}
public boolean isTerminated() {
return exec.isTerminated();
}
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
return exec.awaitTermination(timeout, unit);
}
public List getCancelledTasks() {
if (!exec.isTerminated())
throw new IllegalStateException(/*...*/);
return new ArrayList(tasksCancelledAtShutdown);
}
public void execute(final Runnable runnable) {
exec.execute(new Runnable() {
public void run() {
try {
runnable.run();
} finally {
if (isShutdown()
&& Thread.currentThread().isInterrupted())
tasksCancelledAtShutdown.add(runnable);
}
}
});
}
}
使用TrackingExecutor的一个例子
public abstract class WebCrawler {
private volatile TrackingExecutor exec;
@GuardedBy("this") private final Set urlsToCrawl = new HashSet();
private final ConcurrentMap seen = new ConcurrentHashMap();
private static final long TIMEOUT = 500;
private static final TimeUnit UNIT = MILLISECONDS;
public WebCrawler(URL startUrl) {
urlsToCrawl.add(startUrl);
}
public synchronized void start() {
exec = new TrackingExecutor(Executors.newCachedThreadPool());
for (URL url : urlsToCrawl) submitCrawlTask(url);
urlsToCrawl.clear();
}
public synchronized void stop() throws InterruptedException {
try {
saveUncrawled(exec.shutdownNow());
if (exec.awaitTermination(TIMEOUT, UNIT))
saveUncrawled(exec.getCancelledTasks());
} finally {
exec = null;
}
}
protected abstract List processPage(URL url);
private void saveUncrawled(List uncrawled) {
for (Runnable task : uncrawled)
urlsToCrawl.add(((CrawlTask) task).getPage());
}
private void submitCrawlTask(URL u) {
exec.execute(new CrawlTask(u));
}
private class CrawlTask implements Runnable {
private final URL url;
CrawlTask(URL url) {
this.url = url;
}
private int count = 1;
boolean alreadyCrawled() {
return seen.putIfAbsent(url, true) != null;
}
void markUncrawled() {
seen.remove(url);
System.out.printf("marking %s uncrawled%n", url);
}
public void run() {
for (URL link : processPage(url)) {
if (Thread.currentThread().isInterrupted())
return;
submitCrawlTask(link);
}
}
public URL getPage() {
return url;
}
}
}
TrackingExecutor存在一个不可避免的竞态条件:一些被任务取消的任务实际上已经执行完成。原因:在任务执行最后一条指令以及线程池将任务记录为“结束”两个时刻之间,线程池关闭。如果任务是幂等的那就无所谓,否则要考虑。
7.3 处理非正常的线程终止
ThreadPoolExecutor和Swing使用的技术:直接捕获Throwable,确保行为糟糕的任务不会影响到后续任务的执行。
未捕获异常的处理:
当一个线程由于未捕获异常而退出时,JVM会把这个事件报告给应用程序提供的UNcaughtExceptionHandler。默认就是输出到System.err