文章目录
7 取消与关闭
Java没有提供任何机制来安全地终止线程。但是Java提供了中断(interruption)这种协作机制。
7.1 任务取消
取消某个操作的原因:
- 用户请求取消
- 有时间限制的操作
- 应用程序事件。例如,应用程序对某个问题空间进行分解并搜索,当其中一个任务找到了解决方案时,所有其他仍在搜索的任务都将被取消。
- 错误。例如爬虫程序,当一个爬虫任务发生错误时(例如磁盘空间已满),那么所有的搜索任务都会取消,此时可能记录它们的当前状态,以便稍后重新启动。
- 关闭。在平缓的关闭过程中,当前正在执行的任务将继续执行直到完成;在立即关闭的过程中,当前的任务则可能取消。
程序清单 7-1-1 使用volatile类型的域来保存取消状态
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
@ThreadSafe
public class PrimeGenerator implements Runnable {
@GaurdedBy("this")
private final List<BigInteger> primes = new ArrayList<>();
private volatile boolean canceled;
@Override
public void run() {
BigInteger p = BigInteger.ONE;
while (!canceled) {
p = p.nextProbablePrime();
synchronized (this) {
primes.add(p);
}
}
}
public void cancel() {
this.canceled = true;
}
public synchronized List<BigInteger> get() {
return new ArrayList<>(primes);
}
}
程序清单 7-1-2 一个仅运行一秒的素数生成器
List<BigInteger> aSecondOfPrimes() throws InterruptedException {
PrimeGenerator generator = new PrimeGenerator();
new Thread(generator).start();
try {
TimeUnit.SECONDS.sleep(1);
} finally {
generator.cancel();
}
return generator.get();
}
程序清单 7-1-1中的代码是有问题的。如果素数生成器中调用的是阻塞式的方法,任务可能永远不会检查取消标志,因此永远不会结束。
7.1.1 中断
在Java的API或语言规范中,并没有将中断和任何取消语义关联起来,但实际上,如果在取消之外的其他操作中使用中断都是不合适的,并且很难支撑起更大的应用。
调用intterrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。
通常,中断是实现取消的最合理方式。
程序清单 7-1-1-1 通过中断来取消
import java.math.BigInteger;
import java.util.concurrent.BlockingQueue;
public class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
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 consumed) {
/* 允许线程退出 */
}
}
public void cancel() {
interrupt();
}
}
7.1.2 中断策略
由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。
7.1.3 响应中断
只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和库代码中都不应该屏蔽中断请求。
对于一些不支持取消但仍可以调用可中断阻塞方法的操作,它们必须在循环中调用这些方法,并在发现中断后重新尝试。在这种情况下,应该在本地保存中断状态,并在返回前恢复状态而不是在捕获InterruptedException时恢复状态。
程序清单 7-1-3-1 不可取消的任务在退出前恢复中断
public Task getNexeTask(BlockingQueue<Task> queue) {
boolean interrupted = false;
try {
while (true) {
try {
return queue.take();
} catch (InterruptedException e) {
interrupted = true;
// 重新尝试
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
7.1.4 示例:计时运行
private static final ScheduledExecutorService cancelExec = ...;
public static void timedRun(final Runnable r, long timeout, TimeUnit unit) throws InterruptedException {
class RethowableTask implements Runnable {
private volatile Throwable t;
@Override
public void run() {
try {
r.run();
} catch (Throwable t) {
this.t = t;
}
}
void rethrow() {
if (t != null) {
throw launcherThrowable(t);
}
}
}
RethowableTask task = new RethowableTask();
final Thread taskTread = new Thread(task);
taskTread.start();
cancelExec.schedule(new Runnable() {
@Override
public void run() {
taskTread.interrupt();
}
}, timeout, unit);
taskTread.join(unit.toMillis(timeout));
task.rethrow();
}
7.1.5 通过Future来实现取消
private static final ScheduledExecutorService taskExec = ...;
public static void timedRun(final 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 launcherThrowable(e.getCause());
} finally {
// 如果任务已经结束,那么执行取消操作也不会带来任何影响
// 如果任务正在运行,那么将被中断
task.cancel(true);
}
}
当Future.get() 抛出InterruptedException或TimeoutException 时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务。
7.1.6 处理不可中断的阻塞
java.io包中的同步socket I/O: 虽然InputStream和OutputStream中的读写等方法都不会响应中断,但是通过关闭底层套接字,可以使得由于执行读写方法而被阻塞的线程抛出一个SocketException。
java.io包中的同步I/O: 当中断一个正在IntteruptibleChannel上等待的线程时,将抛出ClosedByInterruptException并关闭链路(其他在这条链路上阻塞的线程也会抛出ClosedByInterruptException)。当关闭一个InterruptibleChannel时,将导致所有在链路上阻塞的线程都抛出AsynchronousCloseException。大多数标准的channel都实现了InterruptibleChannel。
Selector的异步I/O: 如果一个线程在调用Selector.select方法的时候阻塞了,那么调用close或者wakeup方法会使线程抛出ClosedSelectorException并提前返回。
获取某个锁: 如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求。但是,Lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁的同时仍能响应中断。
程序清单 7-1-6-1 通过改写interrupt方法将非标准的取消操作封装在Thread中
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
public class ReaderThread extends Thread{
private final Socket socket;
private final InputStream in;
public ReaderThread(Socket socket) throws IOException {
this.socket = socket;
this.in = this.socket.getInputStream();
}
@Override
public void interrupt() {
try {
socket.close();
} catch (IOException ignored) {
} finally {
super.interrupt();
}
}
@Override
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) {
// 允许线程退出
}
}
}
7.1.7 采用newTaskFor来封装非标准的取消
public interface CancellableTask<T> extends Callable<T> {
void cancel();
RunnableFuture<T> newTask();
}
public class CancellingExecutor extends ThreadPoolExecutor {
@Override
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
if (callable instanceof CancellableTask<T>) {
return ((CancellableTask) callable).newTask();
} else {
return super.newTaskFor(callable);
}
}
}
import java.net.Socket;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
public abstract class SocketUsingTask<T> implements CancellableTask<T> {
private Socket socket;
protected synchronized void setSocket(Socket socket) {
this.socket = socket;
}
public synchronized void cancel() {
try {
if (socket != null)
socket.close();
} catch (Exception exception) {
}
}
@Override
public RunnableFuture<T> newTask() {
return new FutureTask<>(this) {
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
try {
SocketUsingTask.this.cancel();
} finally {
return super.cancel(mayInterruptIfRunning);
}
}
};
}
}
7.2 停止基于线程的服务
线程的所有权是不可以传递的:应用程序可以拥有服务,服务也可以拥有工作者线程,但是应用程序并不能拥有工作者线程,因此应用程序不能直接停止工作者线程。
对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法。
7.2.1 示例:日志服务
public class LogWriter {
private final BlockingQueue<String> queue;
private final LoggerThread logger;
public LogWriter(Writer writer) {
this.queue = new LinkedBlockingQueue<>();
this.logger = new LoggerThread((PrintWriter) 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(PrintWriter writer) {
this.writer = writer;
}
@Override
public void run() {
try {
while (true) {
writer.println(queue.take());
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
writer.close();
}
}
}
}
public class LogService {
private final BlockingQueue<String> queue;
private final LoggerThread loggerThread;
private final PrintWriter writer;
private boolean isShutdown;
private int reservations;
public void start() {
loggerThread.start();
}
public void log(String msg) throws InterruptedException {
synchronized (this) {
if (isShutdown)
throw new IllegalStateException();
++reservations;
}
queue.put(msg);
}
private class LoggerThread extends Thread {
@Override
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();
}
}
}
}
7.2.2 关闭ExecutorService
public class LogService {
private final ExecutorService exec = Executors.newSingleThreadExecutor();
public void start() {}
public void stop() throws InterruptedException {
try {
exec.shutdown();
exec.awaitTermination(timeout, unit);
} finally {
writer.close();
}
}
public void log(String msg) {
try {
exec.execute(new WriteTask(msg));
} catch (RejectedExecutionException e) {
}
}
}
7.2.3 毒丸对象
毒丸对象的含义是:当得到这个对象时,立即停止。毒丸对象保证消费者在关闭线程之前完成队列中的所有工作。 生产者提交了毒丸对象之后,将不会再提交任何工作。
只有生产者消费者数量已知的情况下,才可以使用毒丸(Poison Pill)对象。并且只有在无界队列中,毒丸对象才可以可靠的工作。
7.2.4 示例:只执行一次的服务
程序清单 7-2-4-1 使用私有的Executor,并且该Executor的生命周期受限于方法调用
boolean checkMail(Set<String> 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() {
@Override
public void run() {
if (checkMail(host))
hasNewMail.set(true);
}
});
}
} finally {
exec.shutdown();
exec.awaitTermination(timeout, unit);
}
return hasNewMail.get();
}
7.2.5 shutdownNow的局限性
public class TrackingExecutor extends AbstractExecutorService {
private final ExecutorService exec;
private final Set<Runnable> tasksCanceledAtShutdown = Collections.synchronizedSet(new HashSet<>());
public List<Runnable> getCanceledTasks() {
if (!exec.isTerminated())
throw new IllegalStateException();
return new ArrayList<>(tasksCanceledAtShutdown);
}
@Override
public void execute(Runnable command) {
exec.execute(new Runnable() {
@Override
public void run() {
try {
command.run();
} finally {
if (isShutdown() && Thread.currentThread().isInterrupted()) {
tasksCanceledAtShutdown.add(command);
}
}
}
});
}
// 其他方法全部委托给exec
}