Fork me on GitHub

Java并发编程实战———取消与关闭

要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事。Java没有提供任何机制来安全地终止线程。但它提供了中断(Interruption),这是一种协作机制,能够使一个线程终止另一个线程的当前工作。

任务取消

如果外部代码能在某个操作正常完成之前将其置入“完成”状态,那么这个操作就称为可取消的。在Java中没有一种安全的抢占式方法来停止线程,因此也就没有安全的抢占方式来停止任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。

其中一种协作机制能设置某个“已请求取消”标志,而任务将定期地查看该标志。如果设置了这个标记,那么任务将提前结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//使用volatile类型的域来保存取消状态
public class PrimeGenerator implements Runnable {
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);
}
}
//一个仅允许一秒钟的素数生成器
List<BigInteger> aSecondOfPrimes() throws InterruptionException {
PrimeGenrator generator = new PrimeGenerator();
new Thread(generator).start();
try {
SECONDS.sleep(1);
} finally {
generator.cancel();
}
return generator.get();
}

中断

1
2
3
4
5
6
//Thread中的中断方法
public class Thread {
public void interrupt() { ... }
public boolean isInterrupted() { ... }
public static boolean interrupted() { ... }
}

Thread中包含了中断线程以及查询线程中断状态的方法:interrupt方法能中断目标线程,而isInterrupted方法能返回目标线程的中断状态,静态的interrupted方法将清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方法。

调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。

对中断操作的正确理解是:它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。(这些时刻也被称为取消点)

在使用静态的interrupted时应该小心,因为它会清除当前线程的中断状态。如果在调用interrupted时返回了true,那么除非你想屏蔽这个中断,否则必须对它进行处理–可以抛出InterruptedException,或者通过再次调用interrupt来恢复中断状态。

通常中断是实现取消的最合理方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//通过中断来取消
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();
}
}

中断策略

任务代码不应该对其执行所在的线程的中断策略做出假设,执行取消操作的代码也不应该对线程的中断策略做出假设。线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中。

由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。

响应中断

当调用可中断的阻塞函数时,例如Thread.sleepBlockingQueue.put等,有两种实用策略可用于处理InterruptedException

  • 传递异常,从而使你的方法也成为可中断的阻塞方法。
  • 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理。
1
2
3
4
5
6
//将InterruptedException传递给调用者
BlockingQueue<Task> queue;
...
public Task getNextTask() throws InterruptedException {
return queue.take();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//不可取消的任务在退出前恢复中断
public Task getNextTask(BlockingQueue<Task> queue) {
boolean interrupted = false;
try {
while(true) {
try {
return queue.take();
} catch (InterruptedException e) {
interrupted = true;
}
}
} finally {
if(interrupted) {
Thread.currentThread().interrupt();
}
}
}

通过Future来实现取消

Future拥有一个cancel方法,该方法带有一个boolean类型的参数mayInterruptIfRunning,表示取消操作是否成功。(这只是表示任务是否能够接收中断,而不是表示任务是否能够检测并处理中断。) 如果mayInterruptIfRunningtrue并且任务当前正在某个线程中运行,那么这个线程能被中断。如果这个参数为false,那么意味着“若任务还没有启动,就不要运行它”,这种方式应该用于那些不处理中断的任务中。

如果任务在标准的Executor中运行,并通过它们的Future来取消任务,那么可以设置mayInterruptIfRunning。当尝试取消某个任务时,不宜直接中断线程池,因为你并不知道当中断请求到达时正在运行什么任务–只能通过任务的Future来实现取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//通过Future来取消任务
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抛出InterruptedExceptionTimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务。

停止基于线程的服务

正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控。在线程API中,并没有对线程所有权给出正式的定义:线程由Thread对象表示,并且像其他对象一样可以被自由共享。然而,线程有一个相应的所有者,即创建该线程的类。因此线程池是其工作者线程的所有者,如果要中断这些线程,那么应该使用线程池。
与其他封装对象一样,线程的所有权是不可传递的:应用程序可以拥有服务,服务也可以拥有工作者线程,但应用程序并不能拥有工作者线程,因此应用程序不能直接停止工作者线程。相反,服务应该提供生命周期方法来关闭它自己以及它所拥有的线程。这样,当应用程序关闭该服务时,服务就可以关闭所有的线程了。

对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法。

关闭ExecutorService

ExecutorService提供了两种关闭方法:使用shutdown正常关闭,以及使用shutdownNow强行关闭。在进行强行关闭时,shutdownNow首先关闭当前正在执行的任务,然后返回所有尚未启动的任务清单。

shutdownNow的局限性

当通过shutdownNow来强行关闭ExecutorService时,它会尝试取消正在执行的任务,并返回所有已提交但尚未开始的任务。然而,我们无法通过常规方法来找出哪些任务已经开始但尚未结束。

JVM关闭

JVM既可以正常关闭,也可以强行关闭。正常关闭的触发方式有多种,包括:当最后一个非守护线程结束时,或者当调用了System.exit时,或者通过其他特定于平台的方法关闭时。虽然可以通过这些标准方法来正常关闭JVM,但也可以通过调用Runtime.halt或者在操作系统中杀死JVM进程来强行关闭JVM

关闭钩子

关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程。在正常关闭中,JVM首先调用所有已注册的关闭钩子。JVM并不能保证关闭钩子的调用顺序。在关闭应用程序线程时,如果有线程仍然在运行,那么这些线程接下来将与关闭进程并发执行。JVM并不会停止或中断任何在关闭时仍然运行的应用程序线程。当JVM最终结束时,这些线程将被强行结束。如果关闭钩子或终结器没有执行完成,那么正常关闭进程挂起并且JVM必须强行关闭。当被强行关闭时,只是关闭JVM,而不会运行关闭钩子。

守护线程

线程分为两种:普通线程和守护线程。在JVM启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程。当创建一个新线程时,新线程将继承创建它的线程的守护状态,因此在默认情况下,主线程创建的所有线程都是普通线程。
普通线程与守护线程之间的差异仅在于当线程退出时发生的操作。当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作。当JVM停止时,所有仍然存在的守护线程都将被抛弃–既不会执行finally代码块,也不会执行回卷栈,而JVM只是直接退出。
应尽可能少地使用守护线程,守护线程通常不能用来替代应用程序管理程序中各个服务的生命周期。

终结器

垃圾回收器对那些定义了finalize方法的对象会进行特殊处理:在回收器释放它们后,调用它们的finalize方法,从而保证一些持久化的资源释放。
在大多数情况下,通过使用finally代码块和显式的close方法,能够比使用终结器更好地管理资源。

避免使用终结器。

求鼓励,求支持!