服务器利用线程技术响应客户请求已经很常见,这的确提高了服务器的响应处理能力,但通常服务器端并不会为每个客户端请求分配一个新的线程对象,在大量的客户端的请求下,这种处理方式将会创建大量的线程来处理请求,这对内存和CPU都是很不利的,于是需要一种更合理的线程技术,这就是线程池技术。在日常开发中,也或多或少会接触到线程池,比较Servlet容器处理浏览器请求的线程池,数据库连接池等,本文将理解一番JDK本身的线程池(ThreadPool)实现,与其他线程池相比,也会大同小异。
-
什么是线程池?
线程池,从字面含义来看,是指管理一组同构工作线程的资源池。其由工作线程(Worker Thread),任务队列(Task Queue),任务(Task)等核心组件组成,如下图所示:
-
为什么使用线程池?
合理利用线程池能够带来三个好处:
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
-
线程池定义
JDK中通过ThreadPoolExecutor来抽象通用线程池,可看其继承树:
-
Executor
Executor接口定义了提交任务(Submit Task)的功能,任务(Task)是一个Runnable,Executor需要实现使用何种策略执行任务,可以通过Runnable.run()同步执行,或者通过new Thread(runnbale).start()异步执行,又或者通过某些调度算法Runnable等,并且Executor也保证了一定的内存语义:提交Runnable之前的动作Happens-Before该Runnable的开始执行。
-
ExecutorService
ExecutorService对Executor进行了一些扩展,增加了一些管理任务执行的功能,如终止,跟踪任务等:
ExecutorService中出现两个新的接口,Callable和Future。Callable类似于Runnable,但其可以获取执行的结果。Future代表了一种异步执行的结果,可以对执行做取消,检查,获取结果等操作。
-
AbstractExecutorService
AbstractExecutorService作为ExecutorService的默认实现,实现了submit,invokeAny,invokeAll等方法,并且提供了构造任务的方法newTaskFor,最终通过FutureTask来抽象线程池中的任务。
-
线程池实现
-
ThreadPoolExecutor
ThreadPoolExecutor作为基础的线程池实现,通过线程池中的某个线程来执行任务。其不但提升了在执行大量异步任务时的性能,还提供了管理资源的手段。ThreadPoolExecutor中有几个很关键的属性,用于调优线程池行为:
属性名 |
描述 |
corePoolSize
|
线程池基本大小。当提交一个新任务到线程池时,只要线程池当前工作线程数小于corePoolSize,线程池就会创建新的工作线程来执行该任务(即使有其他空闲的工作线程能够执行该任务)。默认情况下,线程池只会在有任务提交的情况才会创建工作线程,不过可以通过prestartCoreThread()或prestartAllCoreThreads()事先创建好基本的工作线程。
|
maximumPoolSize
|
线程池最大大小。当线程池的任务队列是有界的时,如果任务队列满了,并且已创建的工作线程数小于maximumPoolSize,则线程池会再创建新的工作线程执行新任务。对于无界的任务队列,该参数无效。
|
keepAliveTime
|
空闲工作线程存活时间。如果线程池当前的工作线程数超过corePoolSize,超出的这些工作线程若处于空闲的时间大于keepAliveTime,这些线程将被销毁,以释放不必要的资源。另外,如果设置了allowCoreThreadTimeOut为true,那么基本大小内的工作线程在空闲时间大于keepAliveTime后,也会被销毁。
|
workQueue
|
任务队列。用于保存等待执行的任务的阻塞队列,通常有几种常用的阻塞队列:
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)排序元素。
LinkedBlockingQueue:是一个基于链表结构的的无界阻塞队列,此队列按FIFO(先进先出)排序元素。
SynchronousQueue:一个不存储元素的阻塞队列每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。
PriorityBlockingQueue:一个具有优先级的无界阻塞队列。
|
ThreadFactory
|
用于设置创建线程的工厂:可以通过线程工厂给每个创建出来的线程设置更有意义的名字,这样有利于开发人员进行必要时排查跟踪。
|
RejectedExecutionHandler
|
拒绝任务的策略处理器:当线程池中的工作线程数大于maximumPoolSize且任务队列已满时,线程池将不再继续执行新提交的任务,而是交由RejectedExecutionHandler处理。ThreadPoolExecutor中提供了4种简单的策略:
AbortPolicy:直接抛出异常。
CallerRunsPolicy:使用调用者所在线程来运行任务。
DiscardOldestPolicy:丢弃队列里最旧的一个任务,并执行当前任务。
DiscardPolicy:直接丢弃掉提交的任务。
当然,开发人员可以自己定制RejectedExecutionHandler来处理。
|
-
ThreadPoolExecutor状态
ThreadPoolExecutor中有几个重要的状态,如下图:
比较巧妙的是,ThreadPoolExecutor的实现中,通过一个int字段来保存了workerCount和线程池状态:
ThreadPoolExecutor中其它一些重要属性:
ThreadPoolExecutor实现了execute()方法,完成任务的提交逻辑:
在ThreadPoolExecutor中,将工作者线程抽象为Worker。Worker实现了Runnable,即将作为线程启动,同时也继承了AbstractQueuedSynchronizer,
AbstractQueuedSynchronizer
是一个基于队列的同步器,其作为Java并发的核心基础类,这里先简单知道Worker是一个同步器,具有非重入的互斥锁的功能:
看看Worker如何被创建:
当创建好Worker后,即启动Worker,Worker本质是一个Runnable,其运行逻辑由自己实现:
再看看Worker如何去获取一个任务:
看看Worker是如何被回收的:
-
线程池关闭
开发人员可以通过shutdown()或shutdownNow()来关闭线程池:
以上则是线程池基本的工作原理,主要包括任务提交,启动工作线程,回收工作线程,线程池状态维护等。
-
线程池使用
-
Executors
日常开发中使用线程池时,常常使用Executors来创建自己的线程池,Executors提供了几个方便的工厂方法:
线程池类型 |
描述 |
newFixedThreadPool
|
线程数固定大小,无空闲线程回收,无界的阻塞队列。
|
newSingleThreadExecutor
|
线程数大小为1,无空闲线程回收,无界的阻塞队列。
|
newCachedThreadPool
|
线程数大小不限,空闲线程超时时间为60s,空元素阻塞队列。
|
newScheduledThreadPool
|
具有调度或周期执行的线程池。
|
-
配置线程池大小
在使用线程池时,还有一个比较关键的地方是配置合理的线程池大小,通常会有一些基本原则:
CPU密集型任务。CPU密集型任务配置应该配置尽可能小的线程,如
Runtime.getRuntime()
.availableProcessors() + 1。
I/O密集型任务。IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如Runtime.getRuntime()
.availableProcessors() * 2。
混合型的任务。如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。
利特尔原则。利特尔法则(Little’s law)是说,一个系统请求数等于请求的到达率与平均每个单独请求花费的时间之乘积,可以帮助开发人员调优比较合理的线程数。
当然,上面只是一些原则性问题,实际生产是应该作一些测试来进行更精确地验证得。
以上则是Java线程池相关的部分实现。明白了实现细节,不仅有利于对线程池运行机制有所理解,也会其他线程池的工作原理能触类旁通。
-
参考文献
JDK7
Java并发编程第六章
Java线程池
Java线程池使用
Java线程池调优