文章目录
本章介绍了,如何在线程执行完正常工作之前,提前结束
java没有提供任何机制安全的终止线程.但它提供了中断,这是一种协作机制,能使一个线程终止另一个线程的当前工作.
1.任务取消
如果外部代码能够在某个操作完成之前将其置入"完成"状态,那么这个操作就称为可取消的,取消操作的原因很多:
- 用户请求取消
- 有时间限制的操作
- 应用程序事件:多个任务执行搜索,一旦某个任务搜索完成,取消其它任务
- 错误
- 关闭:程序或服务关闭
一种协作机制是设置某个"已取消请求"的标志,任务将定期查看该标志,如果为true,那么任务将提前结束
@ThreadSafe
public class PrimeGenerator implements Runnable{
@GuardedBy("this")
private final List<BigInteger> primes = new ArrayList<>();
//使用volatile类型的域来保存这个状态
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<>(primes);
}
}
下面是调用方法:
List<BigInteger> aSecondOfPrimes() throws InterruptedException {
PrimeGenerator primeGenerator = new PrimeGenerator();
new Thread(primeGenerator).start();
try {
TimeUnit.SECONDS.sleep(1);
} finally {
primeGenerator.cancel();
}
return primeGenerator.get();
}
1.1.中断
上面的例子可能产生一个严重的问题,例如调用一个阻塞方法
public class BrokenPrimeProducer extends Thread{
private final BlockingQueue<BigInteger> queue;
private volatile boolean cancelled;
public BrokenPrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
@Override
public void run() {
BigInteger p = BigInteger.ONE;
while (!cancelled) {
try {
//放到队列中,这一步可能发生阻塞
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException e) {
}
}
}
public void cancel() {
cancelled = true;
}
void consumePrimes() throws InterruptedException {
BlockingQueue<BigInteger> primes = new ArrayBlockingQueue<>(18);
BrokenPrimeProducer producer = new BrokenPrimeProducer(primes);
producer.start();
try {
while (needMorePrimes()) {
consume(primes.take())
}
} finally {
producer.cancel();
}
}
}
如果生产者速度大于消费者速度,队列将被填满,put方法将被阻塞,如果这时消费者希望取消生产者任务,此时生产者永远无法识别cancelled状态,因为它无法从阻塞状态恢复过来(因为此时消费者已经停止从队列中取数,所以put方法将一直阻塞)
Thread类提供了一些用于查询线程中断状态和中断线程的方法
Thread.isInterrupted方法能返回目标线程的中断状态,静态的interrupted方法将清除当前线程的中断状态,并返回它之前的值.
阻塞库方法,例如Thread.sleep()和Object.wait等,都会检查线程何时中断
当线程在非阻塞状态下中断时,它的中断状态将被设置,然后根据将被取消的操作来检查中断状态以判断发生了中断,通过这种方法,中断操作将变得有"黏性"–如果不触发InterruptedException,那么中断状态将被一直保持,直到明确的清除中断状态
调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息.
中断操作不会真正的中断一个正在运行的线程,而是发出中断请求,然后由线程在下一个合适的时刻中断自己
通常,中断是实现取消的最合理的方式
class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
PrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
@Override
public void run() {
BigInteger p = BigInteger.ONE;
while (!Thread.currentThread().isInterrupted()) {
try {
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException e) {
//允许线程退出
}
}
}
}
1.2 中断策略
线程应该包含中断策略,规定线程如何解释某个中断请求–当发现中断请求时,应该做哪些工作,哪些工作单元对中断来说是原子操作,以及以多块的速度来响应中断.
最合理的中断策略是某种形式的线程级取消操作或服务级取消操作:尽快退出,必要时进行清理,通知某个所有者线程已经退出.
还可以建立其它的中断策略,例如暂停服务或重新开始服务.
区分任务和线程对中断的反应是很重要的
当检查到中断请求时,任务并不需要放弃所有操作–它可以推迟处理中断请求,并直到某个更合适的时刻.
由于每个线程拥有各自的中断策略,因此除非你直到中断对该线程的含义,否则就不应该中断这个线程
1.3 响应中断
当调用可终端的阻塞函数时,如Thread.sleep()或BlockingQueue.put等,由两种实用策略可用于处理InterruptedException:
- 传递异常(throws 异常),从而使你的方法也成为可中断的阻塞方法.
- 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理
如果不想传递异常,那么可以通过再次调用interrupt方法来恢复中断状态.
只有实现了线程中断策略的代码才可以屏蔽中断请求.在常规的任务和库代码中都不应该屏蔽中断请求
可以使用Thread的join方法,将任务通过Thread.join(long millis)执行.
如果线程被其它线程中断,join方法会抛出异常,供调用者处理
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
join的不足:无法知道执行控制是因为线程正常退出而返回还是因为join超时而返回.
1.4 通过Future实现取消
private static final ExecutorService executor = Executors.newFixedThreadPool(100);
public static void timedRun( Runnable r,long timeout,TimeUnit unit) throws InterruptedException {
Future<?> task = executor.submit(r);
try {
task.get(timeout,unit);
} catch (ExecutionException e) {
//如果在任务中抛出了异常,那么重新抛出该异常
throw e;
} catch (TimeoutException e) {
//如果超时,接下来的任务将被取消,这里简化到全部在finally中执行
} finally {
//如果任务已经结束,那么执行取消操作也不会有任何影响
//如果任务正在运行,那么将被中断
task.cancel(true);
}
当Future.get抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.calcel来取消任务.
1.5 处理不可中断的阻塞
很多阻断方法都是通过提前响应或是抛出InterruptedException来响应中断请求,但并非所有的可阻塞方法或者阻塞机制都能响应中断
如果一个线程由于执行同步外的Socket I/O 或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有任何其他作用.
可以使用类似于中断的手段来停止这些线程,但要求必须知道线程阻塞的原因.
- Java.io包中的同步Socket I/O.
- Java.io包中的同步 I/O.
- Selector的异步I/O.
- 获取某个锁:如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁所以不会理会中断请求.但在Lock类中提供了lockInterruptibly方法,该方法在等待一个锁的同时仍能响应中断.
public class ReaderThread extends Thread{
private final Socket socket;
private final InputStream in;
public ReaderThread(Socket socket) throws IOException {
this.socket = socket;
this.in = socket.getInputStream();
}
/**
* 重写了interrupt,使其既能处理标准的中断,又能关闭底层套接字
*/
@Override
public void interrupt() {
try {
socket.close();
} catch (IOException e) {}
finally {
super.interrupt();
}
}
@Override
public void run() {
try {
byte[] buf = new byte[30];
while (true) {
int count = in.read(buf);
if (count < 0) {
break;
} else if (count > 0) {
//do something
}
}
} catch (IOException e) {
//允许线程退出
}
}
}
2.停止基于线程的服务
线程的所有权是不可逆转的:应用程序拥有服务,服务拥有工作线程,但应用程序并不能拥有工作线程,因此应用程序不能直接停止工作线程.
服务应该提供生命周期方法来关闭它自己以及它拥有的线程
对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法
2.1 示例:日志服务
日志服务应该用一条单独的线程,可调用日志服务,将日志提交给BlockingQueue,通过BlockingQueue将消息提交给日志线程,并由日志线程写入.
但是当队列满时,生产者线程会阻塞,这些阻塞的线程无法解除阻塞状态.当取消一个生产者-消费者操作时,需要同时取消生产者和消费者.
关闭LogWriter的方法是,设置某个"已请求关闭"标志,以避免进一步提交日志
为了避免竞态条件问题,可以使日志消息的提交操作成为原子操作.:通过原子方式来检查关闭请求,并且有条件的递增一个计数器来"保持"提交消息的权利.
public class LogService {
private final BlockingQueue<String> 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<>(30);
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 {
@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) {
/* retry */
}
}
} finally {
writer.close();
}
}
}
}
ExecutorService提供了shutdown和shutdownNow方法,一般推荐使用shutdown,避免风险
通过关闭公共的ExecutorService,也可以达到中断线程的目的
public class LogService {
private final ExecutorService exec = Executors.newSingleThreadExecutor();
private final PrintWriter writer;
public LogService(PrintWriter writer) {
this.writer = writer;
}
public void stop() {
try {
exec.shutdown();
exec.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
}finally {
writer.close();
}
}
}
2.2 "毒丸"对象
含义:当得到这个对象时,立即停止.
即FIFO队列中,"毒丸"对象将确保消费者在关闭之前首先完成队列中的所有工作,在提交"毒丸"之前提交的所有工作都会背处理,而生产者提交了"毒丸"对象后,将不会再提交任何工作.
只有当生产者和消费者数量都已知的情况下,才可以使用"毒丸"对象
多生产者时,只需每个生产者都像队列中添加一个"毒丸",消费者在接收到指定数量"毒丸"后停止消费即可.反之亦然
当生产者-消费者数量过大时,这种方法将难以适用
2.3 示例:只执行一次的服务
如果某个方法需要处理一批任务,并且当所有任务都处理完成后才返回,那么可以通过一个私有的Executor来简化服务的生命周期管理.
public class CrawlerThread extends Thread{
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(() -> {
if (checkMail(host)) {
hasNewMail.set(true);
}
});
}
}finally {
exec.shutdown();
exec.awaitTermination(timeout,unit);
}
return hasNewMail.get();
}
private boolean checkMail(String host) {
//do something
return false;
}
}
2.4 shutdownNow局限性
通过shutdownNow强行关闭ExecutorService时,它会尝试取消正在执行的任务,并返回所有已提交但尚未开始的任务,从而将这些任务写入日志或者保存起来以便之后进行处理.
public class TrackingExecutor extends AbstractExecutorService {
private final ExecutorService exec;
private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet<>());
public TrackingExecutor(ExecutorService exec) {
this.exec = exec;
}
public List<Runnable> getCancelledTasks() {
if (!exec.isTerminated()) {
throw new IllegalStateException();
}
return new ArrayList<>(tasksCancelledAtShutdown);
}
@Override
public void execute(final Runnable command) {
exec.execute(() -> {
try {
command.run();
} finally {
if (isShutdown() && Thread.currentThread().isInterrupted()) {
tasksCancelledAtShutdown.add(command);
}
}
});
}
@Override
public void shutdown() {
exec.shutdown();
}
@Override
public List<Runnable> shutdownNow() {
return exec.shutdownNow();
}
@Override
public boolean isShutdown() {
return exec.isShutdown();
}
@Override
public boolean isTerminated() {
return exec.isTerminated();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return exec.awaitTermination(timeout,unit);
}
}