1、任务执行
1.1、Executor
1.1.1、线程池与Executor生命周期
Executor基于生产者-消费者模式,我们可以通过Executors的四个静态工厂方法创建线程池:newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool。这四个工厂方法其实都是通过ThreadPoolExecutor来创建的,只是传递的参数不同[主要是工作队列的不同]罢了。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
其中,workQueue是工作队列,工作队列中保存了所有等待执行的任务,工作线程的任务很简单:从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。ThreadFactory用于创建新的线程。
Executor的实现通常是创建线程来执行任务,但JVM只有在所有的(非守护)线程全部终止后才会退出。因此,如果无法正确地关闭Executor,那么JVM将无法结束。
为了解决执行服务的生命周期问题,Executor扩展了ExecutorService接口,添加了一些用于生命周期管理的方法(同时还有一些用于任务提交的便利方法)。
ExecutorService的生命周期有三种状态:运行、关闭和已终止。ExecutorService在初始创建的时候,处于运行状态。shutDown方法将执行平缓的关闭过程:不再接收新的任务,同时等待已经提交的任务执行完成 - 包括那些还未开始执行的任务。shutDownNow方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。
在ExecutorService关闭后提交的任务将由“拒绝执行处理器”来处理,它会抛弃任务,或者使得execute方法抛出一个未检查RejectedException异常。等待所有的任务执行完成,ExecutorService将转入终止状态。可以调用awaitTermination来等待ExecutorService到达终止状态,或者通过调用isTerminated来轮询ExecutorService是否已经终止。
1.1.2、延迟任务与周期任务
Timer在执行所有定时任务时只会创建一个线程,如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时准确性。【Timer支持基于绝对时间而不是相对时间的调度机制,而ScheduledThreadPoolExecutor只支持基于相对时间的调度】。
Timer的另一个问题,如果TimerTask抛出了一个未检查异常,那么Timer将表现糟糕的行为。Timer线程并不捕获异常,因此当TimerTask抛出未检查异常时将终止定时线程。这种情况下,Timer也不会恢复线程的执行,而是会错误的认为整个Timer都被取消了。因此,已经被调度但尚未执行的TimerTask将不会再执行,新的任务也不能被调度。(这个问题被称为“线程泄露”)。
Timer存在一些缺陷,因此应该考虑使用ScheduledThreadPoolExecutor来代替它。在JDK5.0 或者更高的JDK中,将很少使用Timer。
1.1.3、Callable与Future
Executor框架使用Runnable作为其基本的任务表示形式。Runnable是一种有很大局限的抽象,虽然run能写入日志文件或者将结果放入某个共享的数据结构,但它不能返回一个值或抛出一个受检查异常。
许多任务实际上都是存在延迟的计算 - 执行数据库查询,从网络获取一个资源等,对于这些任务,Callable是一种更好的抽象:它认为主入口点(Call)将返回一个值,并可能抛出一个异常。在Executor中包含了一些辅助方法能将其他类型的任务封装成一个Callable,例如Runnable和java.security.PrivilegedAction。
Executor执行的任务有四个生命周期:创建、提交、开始和完成。在Executor框架中,已经提交但尚未开始的任务可以取消,但对于那些已经开始执行的任务,只有当它们中断时,才能取消。取消一个已经完成的任务不会有任何影响。
Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。在Future规范中包含的隐含意义是,任务的生命周期只能前进,不能后退,就像ExecutorService的生命周期一样。当某个任务执行完成后,它就永远停留在“完成”状态。
get方法的行为取决于任务的状态(尚未开始、正在执行、已完成)。如果任务已经完成,get将立即返回或者抛出一个Exception。如果任务没有完成,那么get将阻塞直到任务完成。
ExecutorService中的所有submit方法都将返回一个Future,从而将一个Runnable或Callable提交给Executor。并得到一个Future用来获得任务的执行结果或取消任务。还可以显式地为某个指定的Runnable或Callable实例化一个FutureTask(由于FutureTask实现了Runnable,因此可以将它提交给Executor来执行,或者直接调用它的run方法)。
在将Runnable或Callable提交到Executor的过程中,包含了一个安全发布过程,即将Runnable或Callable从提交线程发布到最终执行任务的线程。类似的,在设置Future结果的过程中也包含了一个安全发布,即将这个结果从计算它的线程发布任何通过通过get获取它的线程。
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
注意:只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升。1.2、CompletionService
需求场景:向Executor提交一组计算任务,并且希望在计算完成之后获得结果。
解决方式:保留与每个任务关联的Future,然后反复使用get方法,同时将参数timeout指定为0,从而通过轮询来判断任务是否完成。
缺点:可行,但是繁琐。
优雅的解决方式:CompletionService。
CompletionService将Executor和BlockingQueue的功能融合在一起。你可以将Callable任务提交给给它执行,然后使用类似队列操作的take和poll等方法来获得已完成的结果,而这些结果在完成时将被封装成Future。ExecutorCompletionService实现了CompletionService并将计算部分委托给一个Executor。
public ExecutorCompletionService(Executor executor)
public ExecutorCompletionService(Executor executor,
BlockingQueue<Future<V>> completionQueue)
public Future<V> submit(Callable<V> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<V> f = newTaskFor(task);
executor.execute(new QueueingFuture(f));
return f;
}
public Future<V> submit(Runnable task, V result) {
if (task == null) throw new NullPointerException();
RunnableFuture<V> f = newTaskFor(task, result);
executor.execute(new QueueingFuture(f));
return f;
}
ExecutorCompletionService的实现很简。在构造函数中创建一个BlockingQueue来保存计算完成的结果。当计算完成时,调用FutureTask中的done方法。当提交某个任务时,该任务将首先包装成一个QueueingFuture,这是一个FutureTask的一个子类。
private class QueueingFuture extends FutureTask<Void> {
QueueingFuture(RunnableFuture<V> task) {
super(task, null);
this.task = task;
}
protected void done() { completionQueue.add(task); }
private final Future<V> task;
}
可以看到,task在被包装时,同时被放到了BlockingQueue中。前面说了,这个done方法会在计算完成时,放到BlockingQueue中。public Future<V> take() throws InterruptedException {
return completionQueue.take();
}
public Future<V> poll() {
return completionQueue.poll();
}
take和poll方法在得到结果之前阻塞。多个ExecutorCompletionService可以共享一个Executor,因此可以创建一个对于特定计算私有,又能共享一个公共Executor的ExecutorCompletionService。1.3、为任务设定时限
在支持时间限制的Future.get中支持这种需求:当结果可用时,它将立即返回。如果在指定的时间内没有计算结果,那么将抛出TimeOutException异常,可以在抛出异常时,取消任务。
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
支持限时的invokeAll,将多个任务提交到一个ExecutorService并获得结果。InvokeAll方法的参数为一组任务,并返回一组Future。invokeAll按照任务集合中迭代器的顺序将所有的Future添加到返回的集合中,从而使调用者能将各个Future与其表示的Callable关联起来。当所有任务都执行完毕时,或者调用线程被中断时,又或者超时指定时限时,invokeAll将返回。当invokeAll返回后,每个任务要么正常的完成。要么被取消,而客户端代码可以调用get或isCancelled来判断究竟是何种情况。----------------------------------------------------
2、取消与关闭
要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事情。Java没有提供任何机制来安全地终止线程【虽然Thread.stop和Thread.suspend等方法提供了这样的机制,但由于存在着严重的缺陷,因此应该避免使用】。但它提供了中断,这是一种协作机制,能够使一个线程终止另一个线程的当前工作。这种协作的方式是有必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用一种协作的方式:当需要停止时,它们首先会清除当前正在执行的工作,然后再结束。这提供了更好的灵活性。因为任务本身的代码比发出取消请求的代码更清除如何执行清除工作。
在Java的Api或语言规范中,并没有将中断与任何取消语义关联起来,但实际上,如果在取消之外的其他操作中使用中断,那么都是不合适的,并且很难支撑起来很大的应用。
每个线程都有一个boolean类型的中断状态。当中断线程时,这个线程的中断状态将设置为true。
interrupt()方法能中断目标线程。【注意:调用该方法并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息】
isInterrupt()方法能返回目标线程的中断状态;
interrupted()将清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方法。
阻塞库方法,例如Thread.sleep和Object.wait等,都会检查线程何时中断,并且在发生中断时提前返回。它们在响应中断时执行的操作包括:清除中断状态,抛出InterruptionException,表示阻塞操作由于中断而提前结束。JVM并不能保证阻塞方法检测到中断的速度,但在实际情况中响应速度还是非常快的。
当线程在非阻塞状态下中断时,中断状态将被设置,然后根据将被取消的操作来检查中断状态以判断发生了状态。
对中断的正确理解:它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。有些方法。例如sleep、wait、join等,将严格的处理这种请求,当它们收到中断请求或者在开始执行时发现某个已被设置好的中断状态时,将抛出一个异常。
在使用静态的interrupted时应该小心,因为它会清除当前现成的中断状态。如果在调用interrupted时返回true,那么除非你想屏蔽这个中断,否则必须对它进行处理-可以抛出interruptionException,或者通过再次调用interrupt来恢复中断状态。
注意:通常,中断时实现取消的最合理方式。
public class BrokenPrimerProduce extends Thread{
private final BlockingDeque<BigInteger> queue;
private volatile boolean canceled = false;
public BrokenPrimerProduce(BlockingDeque<BigInteger> queue){
this,queue = queue;
}
public void run(){
try{
BigInteger p = BigInteger.ONE;
while (!canceled){
queue.put(p = p.nextProbablePrime());
}
}catch (InterruptedException e){
}
}
public void cancel(){
canceled = true;
}
}
上述这段代码有什么问题呢?如果队列已满,则put会阻塞,而此时如果消费者线程取消了任务,调用了cancel方法,那么此时生产者将用于无法检测到cancel标志,因为它无法从被阻塞的put方法中恢复过来。解决方式:
public class BrokenPrimerProduce extends Thread{
private final BlockingDeque<BigInteger> queue;
public BrokenPrimerProduce(BlockingDeque<BigInteger> queue){
this,queue = queue;
}
public void run(){
try{
BigInteger p = BigInteger.ONE;
while (!Thread.currentThread().isInterrupted()){
queue.put(p = p.nextProbablePrime());
}
}catch (InterruptedException e){//在catch里面,允许线程退出
}
}
public void cancel(){
interrupt();//导致阻塞put抛出interruptionException异常
}
}
2.1、响应中断
在调用可中断的阻塞函数时,例如Thread.sleep或BlockingQueue.put等,有两种实用策略可用于处理InterruptException:
a)传递异常(可能在执行某个特定的清除操作之后),从而使你的方法也成为可中断的阻塞方法;
b)恢复中断状态,从而使得调用栈的上层代码能够对其进行处理,一种标准的方法就是通过再次调用interrupt方法来恢复中断状态;
你不能屏蔽InterruptedException,例如在catch块中捕获到异常却不做任何处理,除非在你的代码中实现了线程的中断策略。由于大多数代码并不知道它们将在哪个线程中运行,因此应该保存中断状态。
注意:只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和库代码中都不应该屏蔽中断请求。
2.2、通过Future来实现取消
通常,使用现有库中的类比自行编写更好。ExecutorService.submit将返回一个Future来描述任务。Future拥有一个cancel方法,该方法带有一个boolean类型的参数mayInterruptIfRunning,表示取消操作是否成功。(这只是表示任务是否能够接收中断,而不是表示任务是否能检测并处理中断)。如果mayInterruptIfRunning为true并且任务当前正在某个线程中运行,那么这个线程能被中断。这个参数为false,那么意味着“若任务还没有启动,就不要运行它”,这种方式应该用于那些不处理中断的任务中。
除非你清楚线程的中断策略,否则不要中断线程,那么在什么情况下调用cancel方法可以指定参数为true呢?
执行任务的线程是由标准的Executor创建的,它实现了一种中断策略使得任务可以通过中断被取消,所以如果任务在标准Executor中运行,并通过它们的Future来取消任务,那么可以设置mayInterruptIfRunning。当尝试取消某个任务时,不宜直接中断线程池,因为你并不知道当中断请求到达时正在运行什么任务 - 只能通过任务的Future来实现取消。
【当Future.get抛出InterruptException或者TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务】
2.3、处理不可中断的阻塞
在Java库中,许多可阻塞的方法都是通过提前返回或者抛出InterruptException来响应中断请求的,从而使得开发人员更容易构建出能响应取消请求的任务。然而,并非所有的可阻塞方法或者阻塞机制都能响应中断:如果一个线程由于执行同步Socket I/O或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外,没有其他任何作用。对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似中断的手段来停止这些线程。比如改写interrupt方法等。