一、任务执行

任务通常是一些抽象且离散的工作单元,通过把应用程序的工作分解到多个任务中,简化程序的组织结构,提供一种自然的事务边界来优化错误恢复过程,提供一种自然的并行工作结构来提升并发性。

1.在线程中执行任务

当需要设计应用程序结构时,需要找出清晰的任务边界和明确的任务执行策略。

  • 串行的执行任务
    在单个线程中串行的执行各项任务是最简单的策略,但存在很多阻塞问题导致服务器不可用。只有当服务器只为单个用户提供服务,任务量少并且执行时间长时才适用。
  • 显式的为任务创建线程
    为每个请求创建一个新的线程来提供服务,实现更高的响应性,任务处理从主线程分离出来,任务可以并行处理,任务处理代码必须是线程安全的。
  • 无限制创建线程的不足
    上述显式创建线程的方案存在一些缺陷,尤其是大量创建时,线程生命周期的开销非常高,活跃的线程会消耗系统资源,超过一些参数限制时会抛出OutOfMemoryError。

2.Executor框架

任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。在Java类库中,任务执行的主要抽象不是Thread,而是Executor,虽然它只是一个简单的接口,但它却为灵活且强大的异步任务执行框架提供了基础,该框架能支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程与执行过程解耦开来。

将任务的提交和任务的执行策略分离开来有助于在部署阶段选择与可用硬件资源最匹配的执行策略。

Executor框架中的线程池对比为每个人物分配一个线程来说,节省了线程创建和销毁过程产生的巨大开销,提高了响应性,有效防止了资源竞争引起失败。

Executor的实现通常会创建线程来执行任务,但JVM只有在线程全部终止后才会退出,如果无法正确的关闭Executor,那么JVM将无法结束。在关闭应用程序时,一般采用平缓的关闭形式,不再接受新的任务,等待已经提交的任务执行完成,包括还未开始执行的任务。


二、取消与关闭

使任务和线程能够安全、快速、可靠地停止下来,Java提供了中断的协作机制,能使一个线程终止另一个线程的当前工作。

一个良好的软件能够很完善的处理失败、关闭和取消等过程,本部分将给出各种实现取消和中断的机制,以及如何编写任务和服务,使它们能够对取消请求做出响应。

Java并没有提供某种抢占式的机制来取消操作或者终结线程,它提供了一种协作式的中断机制来实现取消操作,通过使用FutureTask和Executor框架,可以帮助我们构建可取消的任务和服务。

1.任务取消

让线程死亡的方式有两种:正常完成和未捕获的异常,Java中如果想要取消一个任务,只能使用中断。

在Java中没有一种安全的抢占式方法来停止线程,因此也没有安全的抢占式方法来停止任务。只有中断这种协作机制,使请求取消的任务和代码都遵循一种协商好的协议。一个可取消的任务必须拥有取消策略,这个策略必须详细地定义取消操作的How、When、What。

中断

中断并没有和任何取消语义关联起来,但实际上,在取消之外的其他操作中使用中断都是不合适的,很难支撑起更大的应用。通常,中断是实现取消的最合理方式,每一个线程都有中断线程和查询线程中断状态的方法。

中断并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。

中断策略

线程应该包含中断策略,这一策略规定线程如何解释某个中断请求,即当发现中断请求时,线程应该做哪些工作,哪些工作单元对于中断来说是原子操作,以及以多快的速度来响应中断。

每个线程都拥有各自的中断策略,除非你知道中断对该线程的含义,否则就不应该中断这个线程。只有在执行前会考察中断状态的任务才被视为是一个可取消的任务。

响应中断

只有实现了线程中断策略的代码才可以屏蔽中断请求,在常规的任务和库代码中都不应该屏蔽中断请求。

当尝试取消某个任务时,不宜直接中断线程池,因为你并不知道当中断请求到达是正在运行什么任务。

2.停止基于线程的服务

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

3.处理非正常的线程终止

导致线程提前终止的原因是RuntimeException。在运行时间较长的应用程序中,通常会为所有线程的未捕获异常指定同一个异常处理器,并且该处理器至少会将异常信息记录到日志中。

4.JVM关闭

JVM正常关闭的方式有:当最后一个“正常(非守护)”线程结束时,或者当调用了System.exit时,或者其他特定于平台的方法关闭时(例如发送了SIGINT信号或键入Ctrl+C)。

守护线程

如果希望一个线程来执行一些辅助工作,但又不希望这个线程阻碍JVM的关闭,这种情况下需要使用守护线程。在JVM启动时创建的所有线程中,除了主线程以外其他的线程都是守护线程,并且新线程创建时将继承创建它的线程的守护状态,所以主线程创建的所有线程都是普通线程。

当JVM停止时所有仍然存在的守护线程将被抛弃,JVM直接退出,因此守护线程最好用于执行“内部”任务,要支持能够在不进行清理的情况下被安全地抛弃。


三、线程池的使用

线程池的配置与调优,使用任务执行框架的注意事宜。

1.在任务与执行策略之间的隐形耦合

只有当任务都是同类型的并且相互独立时,线程池的性能才能达到最佳。如果运行时间差距较大的任务混合在一起,除非线程池很大,否则将可能造成“拥塞”。

如果某个任务依赖于其他任务,那么会要求线程池足够大来确保它依赖的任务不会被放入等待队列或被拒绝,采用线程封闭机制的任务需要串行执行。

限定任务的等待资源时间,缓解执行时间较长任务造成的影响,这样无论任务的最终结果是否成功,这种办法总能确保任务继续执行下去。

2.设置线程池大小

如果线程池过大,大量的线程将在相对很少的CPU和内存资源上竞争,导致更高的内存使用量或耗尽资源;如果设置过小,导致许多空闲处理器无法执行工作,降低吞吐率。

线程池的理想大小取决于被提交任务的类型以及所部署系统的特性,需要分析计算环境、资源预算和任务特性。

拥有N个处理器的系统上,线程池大小设置成N+1时,通常能实现最优的利用率。对于包含I/O操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模要设置更大。

3.配置ThreadPoolExecutor

ThreadPoolExecutor是一个灵活的、稳定的线程池,允许进行各种定制。以下是其最常见的构造函数形式

1
2
3
4
5
6
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)

线程的创建与销毁

线程池的基本大小(Core Pool Size)、最大大小(Maximum Pool Size)以及存活时间等因素共同负责线程的创建与销毁。

基本大小时线程池的目标大小,到达目标大小并且工作队列满了才会创建超出这个数量的线程,最大大小表示可同时活动的线程数量的上限。如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程将被终止。

管理队列任务

优先的线程池会限制可并发执行的任务数量。

线程池对比无限创建线程来说,解决了无限创建的不稳定性,但是线程池在高负载下,应用程序仍可能耗尽资源,例如新请求到达的速率超过了线程池的处理速率,这也有可能会耗尽资源。

ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务,基本的任务排队方法有3种:无界队列、有界队列和同步移交(Synchronous Handoff)。

无界队列:工作线程忙碌时,如果任务持续快速的到达,那么队列将无限制的增加。

有界队列:有助于避免资源耗尽的情况发生,队列的大小和线程池的大小必须一起调节。

SynchronousQueue:他并不是一个队列,都是一种在线程之间进行移交的机制。将一个元素放入其中必须有另一个线程正在等待接受这个元素,如果没有线程正在等待并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建一个新的线程,否则任务将被拒绝。

只有任务相互独立时,设置线程池或工作队列界限才是合理的,如果任务间存在依赖,有界会导致出现线程“饥饿”问题,这时应该使用无界的线程池。

饱和策略

有界队列被填满后,饱和策略开始发挥作用。“中止”策略是默认的饱和策略,即抛出未检查的RejectedExecutionException。

线程工厂

每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。默认的线程工厂方法将创建一个新的、非守护的线程,并且不包含特殊的配置信息,然而,在许多情况下都需要使用定制的线程工厂方法。

在调用构造函数后再定制ThreadPoolExecutor

构造完成后,仍然可以通过设置函数(Setter)来修改大多数传递给它的构造函数的参数,例如线程池的基本大小、最大大小、存活时间、线程工厂和拒绝执行处理器。

4.扩展ThreadPoolExecutor

ThreadPoolExecutor是可扩展的,它提供了几个可以在子类化中改写的方法:beforeExecute、afterExecutor和terminated。前两个主要用来添加日志、计时、监视或统计信息收集的功能。无论正常返回还是抛出一个异常,afterExecutor都会被调用,如果beforeExecutor抛出RuntimeException,任务将不会被执行,并且afterExecute也不会被调用。


四、图形化用户界面应用程序

所有的GUI框架基本上都实现为单线程的子系统,其中所有与表现相关的代码都作为任务在事件线程中运行。由于只有一个事件线程,因此运行时间较长的任务会降低GUI程序的响应性,所以应该放在后台线程中运行。在一些辅助类中提供了对取消、进度指示以及完成指示的支持,因此对于执行时间较长的任务来说,无论在任务中包含了GUI组件还是非GUI组件,在开发时都可以得到简化。

详细略


Ref

Briangoetz. Java并发编程实战[M]. 机械工业出版社, 2012.