单任务顺序执行与每个任务一个线程
单线程顺序执行任务,效率过低,没有发挥多核的优势。
为每个任务分配一个线程去执行,速度有所提升,但是有很大的缺陷。
无限制创建线程的不足
生产环境中,每一个任务分配一个线程的方法存在一些缺陷,尤其是需要创建大量的线程时:
- 线程生命周期的开销非常高。线程的创建与销毁是有代价的,延迟处理的请求,并且需要JVM和操作系统提供一些辅助操作。如果请求的到达率非常高且请求的处理过程是轻量级的,例如大多数服务器应用程序就是这种情况,那么为每个请求创建一个新线程将消耗大量的计算资源。
- 资源消耗。活跃的线程会消耗系统资源,尤其是内存。如果可运行的线程数量多于柯用处理器的数量,那么有些线程将闲置。大量闲置的线程会占用许多内存,给垃圾回收器带来压力,并且大量线程在竞争CPU资源时还将产生其他的性能开销。如果已经拥有足够多的线程使所有CPU保持忙碌状态,那么再创建更多的线程反而会降低性能。
- 稳定性。可创建线程的数量存在一个限制,不是无限的,跟平台有关,受多个因素制约。比如JVM启动参数、Thread构造函数中请求的栈大小以及底层操作系统对线程的限制等。破坏了限制会跑出OutOfMemory异常,很难恢复。
Executor框架
提供一种标准的方法将任务的提交过程与执行过程解耦,并用Runnable来表示任务。Executor的实现还提供了对生命周期的支持以及统计信息收集、应用程序管理机制和性能监视等机制。
Executor是基于生产者-消费者模式
的,提交任务的操作相当于生产者,执行任务的线程则相当于消费者。
执行策略
在执行策略中定义了任务执行的“What、Where、When、How”等方面:
- 在什么(What)线程中执行任务?
- 任务按照什么(What)顺序执行(FIFO、LIFO、优先级)?
- 有多少个(How Many)任务能够并发执行?
- 在队列中有多少个(How Many)任务在等待执行?
- 如果系统由于过载需要拒绝一个任务,那么应该选择哪一个(Which)任务?如何通知(How)应用程序有任务被拒绝?
- 在执行一个任务之前或之后,应该进行哪些(What)动作?
执行策略可以看做一种资源管理工具,最佳策略取决于可用的计算资源以及对服务质量的需求。
Executor的生命周期
Executor是异步执行的,所以提交的任务不是立即可见的。有些任务可能已经完成,有些可能正在运行,其他任务可能在队列中等待执行。
Executor扩展了ExecutorService接口,添加了用于管理生命周期的方法。
- shutdown(): 执行平缓的关闭过程,不再接受新的任务,同时等待已经提交的任务执行完成,包括那些未开始执行的任务。
- shutdownNow(): 执行粗暴的关闭过程,尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。
- isShutdown()
- isTerminated(): 轮询ExecutorService是否已经终止
- awaitTermination(long timeout, TimeUnit unit): 等待ExecutorService到达终止状态。通常调用awaitTermination之后会立即调用shutdown()
延时任务与周期任务
Timer在执行所有定时任务时只会创建一个线程,如果某个任务的执行时间过长,将会破坏其他TimerTask的定时精确性。此外Timer线程不捕获异常,因此当TimerTask跑出未检查异常时将终止定时线程。这种情况下,Timer也不会恢复线程的执行,而是会错误的认为整个Timer都被取消了。因此,已经被调度但尚未执行的TimerTask将不会再执行,新的任务也不能被调度。(这个问题被称为“线程泄露”)。
调度服务可以使用延迟队列,底层实现用BlockingQueue,为ScheduledThreadPoolExecutor提供调度功能。每个Delayer对象都有一个相应的延迟时间:在DelayQueue中,只有某个元素逾期后才能从DelayQueue中执行take操作。从DelayQueue中返回的对象是根据它们的延迟时间进行排序的。
线程中断
对中断操作的正确理解是:它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己(这些时刻也被称为取消点)。
通常,中断是实现取消的最合理方式。
中断策略
中断策略规定线程如何解释某个中断请求—当发现中断请求时,应该做哪些工作,哪些工作单元对于中断来说是原子操作,以及多快的速度响应中断。
最合理的中断策略:某种形式的线程级取消操作或服务级取消操作:尽快退出,在必要时进行清理,通知某个所有者线程已经退出。
其他中断策略:暂停服务或重新开始服务,适用于包含非标准中断策略的线程或线程池。
当检查到中断请求时,任务并不需要放弃所有操作,它可以推迟处理中断请求,并直到某个更何时的时刻。因此需要记住中断请求,并在完成当前任务后抛出InterruptedException火表示已经收到中断请求。这么做能够确保在更新过程中发生中断时,数据结构不会被破坏。
线程应该只能由其所有者中断,任务代码将任务提交给线程去执行,不应该对其执行所在的线程的中断策略做出假设。由于每个线程拥有各自的中断策略,除非明确知道中断对该线程的含义,否则就不应该中断这个线程。
响应中断
只有实现了线程中断策略的代码才可以屏蔽中断请求,在常规的任务和库代码中都不应该屏蔽中断请求。