第六章 任务执行
在线程中执行任务
无限制创建线程的不足:
- 线程生命周期开销非常高
- 资源消耗
- 稳定性:在可创建线程的数量上存在限制,这个限制随着平台的不同而不同
Executor框架
虽然Executor是个简单的接口,但它却为灵活且强大的异步任务执行框架提供了基础,该框架能支持多种不同类型的任务执行策略。它提供了一种标准的方法,将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务。Executor的实现还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。
Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。
Executor单纯定义了执行Runable的方法;
ExecutorService定义了执行Callable的方法以及定义了一系列线程管理的方法。
AbstractExecutorService提供了ExecutorService部分方法的实现,对Runable和Callable进行了封装,统一成了FutureTask。
ThreadPoolExecutor是真正的线程池。
ScheduledExecutorService定义了一些定时、延迟执行相关的方法。
ScheduledThreadPoolExecutor提供了基于ThreadPoolExecutor的ScheduledExecutorService的实现。
线程池
线程池,从字面含义来看,是指管理一组同构工作线程的资源池。
线程池比“为每个任务分配一个线程”的优势:通过重用现有的线程,而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。而另一个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。
Executors中静态工厂方法:
newFixedThreadPool。newFixedThreadPool将创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)。newCachedThreadPool。newCachedThreadPool将创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模不存在任何限制。newScheduledThreadExecutor。newScheduledThreadExecutor是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。newScheduledThreadExecutor能确保依照任务在队列中的顺序来串行执行。newScheduledThreadPool。newScheduledThreadPool创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似Timer。
Executor的生命周期
JVM只有在所有(非守护)线程全部终止后才会退出。因此,如果无法正确地关闭Executor,那么JVM将无法结束。
为了解决执行服务的生命周期问题,Executor扩展了ExecutorService接口,添加了一些用于生命周期管理的方法,同时还有一些用于任务提交的便利方法。
public interface Executor {
void execute(Runnable command);
}
public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
throws InterruptedException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
ExecutorService的生命周期有三种状态:运行、关闭和已终止。ExecutorService在初始创建时处于运行状态。shutdown方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交了任务执行完成------包括那些还未开始执行的任务。shutdownNow方法将执行粗暴的关闭过程:它将尝试取消所有类型的任务,并且不再启动队列中尚未开始执行的任务。
在ExecutorService关闭后提交的任务将由“拒绝执行处理器(Rejected Execution Handler)”来处理,它会抛弃任务,或者使得execute的方法抛出一个未检查的RejectedExecutionException。等所有任务都完成后,ExecutorService将转入终止状态。可以调用awaitTermination来等待ExecutorService到达终止状态,或者通过调用isTerminated来轮询ExecutorService是否已经终止。通常在调用shutdown之后会立即调用awaitTermination,从而产生同步的关闭ExecutorService的效果。
找出可利用的并行性
Runnable和Callable描述的都是抽象的计算任务。这些任务通常是有范围的,即都有一个明确的起始点,并且最终会结束。
Executor执行的任务有4个生命周期阶段:创建、提交、开始和完成。
Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务。
public interface Callable<V> {
V call() throws Exception;
}
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
CompletionService将Executor和BlockingQueue的功能融合在一起,你可以将Callable任务提交给它来执行,然后使用类似于队列操作的take和poll等方法来获得已完成的结果,而这些结果会在完成时将被封装为Future。ExecutorCompletionService实现了CompilationService,并将计算部分委托给一个Execotor。
第七章 取消与关闭
Java没有提供任何机制来安全的终止线程。但它提供了中断(Interruption),这是一种协作机制,能够使一个线程终止另一个线程的当前工作。
任务取消
取消某个操作的原因很多:用户取消请求、有时间限制的操作、应用程序事件、错误、关闭。
在Java中没有一种安全的抢占式方法来停止线程,因此也就没有安全的抢占式方法来停止任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。
中断
线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的或者可能的情况下停止当前工作。并转向执行其他工作。
interrupt方法能中断目标线程,isInterrupted方法能返回目标线程的中断状态。静态的interrupted的方法将清除当前线程的中断状态,并返回它之前的值。这也是清除中断状态的唯一方法。
/***
* Interrupts this thread.
* Unless the current thread is interrupting itself, which is always permitted, the checkAccess method of this thread is invoked, which may cause a SecurityException to be thrown.
* If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.
* If this thread is blocked in an I/O operation upon an InterruptibleChannel then the channel will be closed, the thread's interrupt status will be set, and the thread will receive a java.nio.channels.ClosedByInterruptException.
* If this thread is blocked in a java.nio.channels.Selector then the thread's interrupt status will be set and it will return immediately from the selection operation, possibly with a non-zero value, just as if the selector's wakeup method were invoked.
* If none of the previous conditions hold then this thread's interrupt status will be set.
* Interrupting a thread that is not alive need not have any effect.
*/
public void interrupt();
中断策略
最合理的中断策略是在某种形式的线程级(Thread-Level)取消操作或服务级(Service-Level)取消操作:尽快退出,在必要时进行清理,通知某个所有者线程已经退出。此外,还可以建立其他中断策略,例如暂停服务或重新开始服务。但对那些包含非标准中断策略的线程或线程池,只能用于能知道这些策略的任务中。
任务不应该对执行该任务的线程的中断策略做出任何假设,除非该任务被专门设计为在该服务中运行,并且在这些服务中包含特定的中断策略。无论任务把中断视为取消还是其他某个中断响应操作,都应该小心地保存执行线程的中断状态。如果除了将InterruptedException传递给调用者之外还需要执行其他操作,那么应该在捕获InterruptedException之后恢复中断状态:
Thread.currentThread().interrupt();
正如任务代码不应该对其执行所在的线程的中断策略做出假设,执行取消操作的代码也不应该对线程的中断策略做出假设。线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中,例如关闭(shutdown)方法。
响应中断
两种策略:
- 传递异常(可能在执行某个特定于任务的清除操作之后),从而使你的方法也成为可中断的阻塞方法。
- 恢复中断状态,从而使调用栈中的上层代码能对其进行处理。
通过Future来实现取消
Future代表了一个异步计算任务的结果。Future拥有一个cancel方法:
/***
* Attempts to cancel execution of this task. This attempt will fail if the task has already completed, has already been cancelled, or could not be cancelled for some other reason. If successful, and this task has not started when cancel is called, this task should never run. If the task has already started, then the mayInterruptIfRunning parameter determines whether the thread executing this task should be interrupted in an attempt to stop the task.
* After this method returns, subsequent calls to isDone will always return true. Subsequent calls to isCancelled will always return true if this method returned true.
*
* Params:
* mayInterruptIfRunning – true if the thread executing this task should be interrupted; otherwise, in-progress tasks are allowed to complete
* Returns:
* false if the task could not be cancelled, typically because it has already completed normally; true otherwise
*/
boolean cancel(boolean mayInterruptIfRunning);
NOTE:当
Future.get抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务。InterruptedException与TimeoutException是当前线程调用get方法而抛出的,而不是运行任务的线程。
处理不可中断的阻塞
- **Java.io包中的同步Socket I/O。**在服务器应用程序中,最常见的阻塞I/O形式就是对套接字进行读取和写入,虽然
InputStream和OutputStream中的read和write等方法都不会响应中断,但通过关闭底层的套接字,可以使得由于执行read或write等方法而被堵塞的线程抛出一个SocketException。 - **Java.io包中的同步I/O。**当中断一个正在
InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException并关闭链路(这还会使得其他在这条链路上阻塞的线程同样抛出ClosedByInterruptException)。当关闭一个InterruptibleChannel时,将导致所有在链路操作上阻塞的线程都抛出AsynchronousCloseException,大多数标准的channel都实现了InterruptibleChannel。 - **Selector的异步I/O。**如果一个线程在调用
selector.select方法(在java.nio.channels中)时堵塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回。 - **获取某个锁。**如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求。但是,在Lock类中提供了
lockInterruptibly方法,该方法允许在等待一个锁的同时,仍能响应中断。
采用newTaskFor来封装非标准的取消
newTaskFor是Java 6在ThreadPoolExcutor中新增的功能。newTaskFor是一个工厂方法,它将创建Future来代表任务。通过定制表示任务的Future可以改变Future.cancel的行为,例如执行非标准的取消。
停止基于线程的服务
应用程序通常会创建拥有多个线程的服务,例如线程池,并且这些服务的生命周期通常比创建它们的方法的生命周期更长。如果应用程序准备退出,那么这些服务所拥有的线程也需要结束,由于无法通过抢占式的方法来停止线程,因此他们需要自行结束。
正确的封装原则是,除非拥有某个线程,否则不能对该线程进行操控,例如中断线程或修改线程的优先级等。
关闭ExecutorService
ExecutorService提供了两种关闭的方法:使用shutdown的当正常关闭,以及使用shutdownNow强行关闭。在进行强行关闭时,shutdownNow首先关闭当前正在执行的任务,然后返回所有尚未启动的任务清单。
这两种关闭方式的差别在于各自的安全性和响应性:强行关闭的速度更快。但风险也更大,因为任务很可能在执行到一半时被结束;而正常关闭虽然速度慢,但却更安全,因为ExecutorService为一直等到队列中的所有任务都执行完成后才关闭。在其他拥有线程的服务中,也应该考虑提供类似的关闭方法以供选择。
”毒丸“对象
“毒丸(Poison Pill)”是指一个放在队列上的对象,其含义是:“当得到这个对象时,立即停止。”
shutdownNow的局限性
shutdownNow无法知道哪些任务已经开始但未结束。可以通过对ExecutorService进一步封装解决该问题。
处理非正常的线程终止
典型的线程池工作者线程结构,如果任务抛出了一个未检查异常:那么它将使线程终结,但会首先通知框架该线程已经终结。
public void run() {
Throwable thrown = null;
try {
while (!isInterrupted())
runTask(getTaskFromWorkQueue());
} catch (Throwable e) {
thrown = e;
} finally {
threadExited(this, thrown);
}
}
未捕获异常的处理
在Thread API中同样提供了UncaughtExceptionHandler,它能检测出某个线程由于未捕获的异常而终结的情况。
要为线程池中的所有线程设置一个UncaughtExceptionHandler需要为ThreadPoolExecutor的构造函数提供一个ThreadFactory。所有的线程操控一样,只有线程的所有者能够改变现成的UncaughtExceptionHandler。标准线程池允许当发生未捕获异常时结束线程,但由于使用了一个try-finally代码块来接收通知,因此当线程结束时,将有新的线程来代替它。如果没有提供捕获异常处理器或其他的故障通知机制,那么任务会悄悄失败,从而导致极大的混乱。如果你希望在任务由于发生异常而失败时获得通知,并且执行一些特定于任务的恢复操作,那么可以将任务封装在能捕获异常的Runnable或Callable中,或者改写ThreadPoolExecutor的afterExecute方法。
令人困惑的是,只有通过execute提交的任务,才能将它抛出的异常交给为未捕获异常处理器,而通过submit提交的任务,无论是抛出的未检查异常,还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由submit提交的任务由于抛出了异常而结束,那么这个异常将被future.get封装在ExecutionException中重新抛出。
JVM关闭
JVM可以正常关闭,也可以强行关闭,正常关闭的触发方式有多种,包括:当最后一个“正常(非守护)”线程结束时,或者当调用System.exist时,或者通过其他特定于平台的方法关闭时(例如发送了SIGINT信号或键入Ctrl-C)。虽然可以通过这些标准方法来正常关闭JVM,但也可以通过调用Runtime.halt或者在操作系统中杀死JVM进程(例如发送SIGKILL)来强行关闭JVM。
关闭钩子
在正常关闭中,JVM首先调用所有已注册的关闭钩子(Shutdown Hook)。关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程。JVM并不能保证关闭钩子的调用顺序。在关闭应用程序线程时,如果有(守护或非守护)线程仍在运行,那么这些线程接下来将与关闭进程并发执行。当所有的关闭钩子都执行结束时,如果runFinalizersOnExit为true,那么JVM将运行终结器,然后再停止。JVM并不会停止或中断任何在关闭时仍然运行的应用程序线程。当JVM最终结束时,这些线程将被强行结束。如果关闭钩子或终结器没有执行完成。那么正常关闭进程“挂起”,并且JVM必须被强行关闭。当被强行关闭时,只是关闭了JVM而不会运行关闭钩子。
关闭钩子应该是线程安全的。关闭钩子不应该依赖那些可能被应用程序或其他关闭钩子关闭的服务。
守护线程
JVM启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程(例如垃圾回收器以及其他执行辅助工作的线程)。刚创建一个新线程时,新线程将继承创建它的线程的守候状态,因此在默认情况下,主线程创建的所有线程都是普通线程。
普通线程与守护线程之间的差异仅在于当线程退出时发生的操作。当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作,当JVM停止时,所有仍然存在的守护线程都将被抛弃------既不会执行finally代码块,也不会执行回卷栈,而JVM只是直接退出。
我们应该尽量少的使用守护线程。
终结器
对于文件句柄或套接字句柄,当不再需要它们时,必须显示的交还给操作系统,为了实现这个功能,垃圾回收器对那些定义了finalize方法的对象会进行特殊处理,待回收器释放他们后,调用他们的finalize的方法,从而保证一些持久化的资源被释放。
NOTE:避免使用终结器
第八章 线程池的使用
在任务和执行策略之间的隐形耦合
Executor框架可以将任务的提交与任务的执行策略解耦开来。就像许多对复杂过程的解耦操作那样,这种论断多少有些言过其实了。有些类型的任务需要明确地指定执行策略:
- 依赖性任务。显然,依赖其他任务的任务提交到线程池,容易产生活跃性风险。
- 使用线程封闭机制的任务。单线程Executor隐式承诺了线程安全,换了其他Executor可能引发问题。
- 对响应时间敏感的任务。降低了响应性,对GUI之类的程序是不可接受的。
- 使用ThreadLocal的任务。ThreadLocal使每个线程都可以拥有某个变量的一个私有“版本”。然后线程复用的时候可能会引发异常。
线程饥饿死锁
如果线程池中的任务需要无限期等待一些必须由池中其他任务才能提供的资源或条件,那么除非线程池足够大,否则将发生线程饥饿死锁。
运行时间较长的任务
如果任务阻塞的时间过长,那么即使不出现死锁,线程池的响应性也会变得糟糕。
设置线程池的大小
要想正确地设置线程池的大小,必须分析计算环境,资源预算和任务的特性。
配置ThreadPoolExecutor
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize 线程池中保留的线程数量,甚至它们被闲置。除非
* {@code allowCoreThreadTimeOut} 被设置。
* @param maximumPoolSize 被允许的线程池中最多线程数量
* @param keepAliveTime 当线程数量超过了核心数量,这是多余闲置线程允许等待任务的最大时间。
* will wait for new tasks before terminating.
* @param {@code keepAliveTime}的时间单位
* @param workQueue 这个队列用来在任务执行前持有任务。这个队列只持有被{@code execute}方法提交的{@code Runnable}任务
* @param threadFactory executor用来创建线程的工厂
* @param handler 由于线程达到边界并且队列容量达到最大而执行阻塞时的处理器
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
线程的创建和销毁
线程池的基本大小(Core Pool Size)、最大大小(Maximum Pool Size)以及存活时间等因素,共同负责线程的创建与销毁。
管理任务队列
基本的任务排队方法有3种:无界队列、有界队列和同步移交(Synchronous Handoff)。队列的选择与其他的配置参数有关,例如线程池的大小等。
比较特殊的是SynchronousQueue。对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及将任务从生产者移交给工作者线程。SynchronousQueue不是一个真正的队列,而是一种在线程之间进行移交的机制。如果提交任务时没有线程在等待,并且线程数量达到了上限,那么任务将被拒绝。
饱和策略
JDK提供了几种不同的RejectedExecutionHandler实现,每种实现都包含有不同的饱和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。
“终止(Abort)”策略是默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。
“抛弃(Discard)”策略。当提交的任务无法保存到队列中等待执行时,该会悄悄抛弃该任务。
“抛弃最旧的(Discard-Oldest)”策略则会抛弃下一个将被执行的任务,然后尝试重新提交新的任务。如果工作队列是一个优先级队列,那么DiscardOldestPolicy将导致抛弃优先级最高的任务,最好不要将DiscardOldestPolicy和优先级队列放在一起时。
“调用者运行(Caller-Runs)”策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者(提交者线程直接执行任务),从而降低新任务的流量。
线程工厂
每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。通过指定一个线程工厂方法,可以定制线程池的配置信息。
在调用构造函数后再定制ThreadPoolExecutor
在调用完ThreadPoolExecutor的构造函数后,仍然可以修改大多数构造函数中的参数。
Executors中包含一个unconfigurableExecutorService工厂方法,该方法对一个现有的ExecutorService进行包装,使其之后暴露出ExecutorService的方法,因此不能对其进行配置。newSingleThreadExecutor返回按这种方式封装的ExecutorService,因此不能进行二次配置。
扩展ThreadPoolExecutor
ThreadPoolExecutor是可扩展的,它提供几个可以在子类化中改写的方法:beforeExecute、afterExecute和terminated,这些方法可以用于扩展ThreadPoolExecutor的行为。
在线程池完成关闭操作时,调用terminated,也就是在所有任务都已经完成,并且所有工作者线程也已经关闭后。
递归算法并行化
递归算法可以通过拆分成合理的子任务并发执行,提高效率。