一、什么是线程池
线程池是一种池化技术,是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。这样实现线程的复用,避免重复创建与销毁线程的开销和大量线程上下文切换,提高系统效率和并发度。
使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
二、线程池类ThreadPoolExecutor
我们可以通过ThreadPoolExecutor类构造函数来创建一个线程池,然后调用threadPoolExecutor.execute(Runnable command)方法提交任务执行。
1. ThreadPoolExecutor构造函数参数
最多七个参数
public ThreadPoolExecutor(int corePoolSize,//最大核心线程数
int maximumPoolSize,//最大线程数
long keepAliveTime,//线程最大空闲时间
TimeUnit timeUnit,//keepAliveTime的单位
BlockingQueue<Runnable> workQueue,//任务等待队列
ThreadFactory threadFactory,//线程创建工厂
RejectedExecutionHandler handler)//拒绝策略
(1)corePoolSize
最大核心线程数。默认情况下,核心线程会一直存活,但是当将allowCoreThreadTimeout设置为true时,核心线程超时也会回收。
(2)maximumPoolSize
线程池所允许的最大线程数。当等待队列满,就会创建非核心线程来处理,核心线程数+非核心线程数最大为maximumPoolSize。
(3)keepAliveTime
线程闲置超时时长。如果线程闲置时间超过该时长,非核心线程就会被回收。如果将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。
(4)timeUnit
keepAliveTime的单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
(5)workQueue
等待线程执行的任务阻塞队列。任务提交执行的流程如下
任务出队执行的规则依赖于具体的实现类,任务队列的常用实现类有:
- ArrayBlockingQueue :一个数组实现的有界阻塞队列,此队列按照FIFO的原则对元素进行排序,支持公平访问队列。
- LinkedBlockingQueue :一个由链表结构组成的可选有界阻塞队列,如果不指定大小,则使用Integer.MAX_VALUE作为队列大小,按照FIFO的原则对元素进行排序。
- PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列,默认情况下采用自然顺序排列,也可以指定Comparator。
- DelayQueue:一个支持延时获取元素的无界阻塞队列,创建元素时可以指定多久以后才能从队列中获取当前元素,常用于缓存系统设计与定时任务调度等。
- SynchronousQueue:一个不存储元素的阻塞队列。存入操作必须等待获取操作,反之亦然。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,与LinkedBlockingQueue相比多了transfer和tryTranfer方法,该方法在有消费者等待接收元素时会立即将元素传递给消费者。
- LinkedBlockingDeque:一个由链表结构组成的双端阻塞队列,可以从队列的两端插入和删除元素。
(6)threadFactory
线程创建工厂。用于指定为线程池创建新线程的方式,threadFactory可以设置线程名称、线程组、优先级等参数。可以使用Executors工具类中的方法获取,例如Executors.defaultThreadFactory()
(7)handler
当达到最大线程数且队列任务已满时需要执行的拒绝策略,常见的拒绝策略如下:
- ThreadPoolExecutor.AbortPolicy:默认策略,当任务队列满时抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃掉不能执行的新任务,不抛任何异常。
- ThreadPoolExecutor.CallerRunsPolicy:当任务队列满时使用调用者的线程直接执行该任务。
- ThreadPoolExecutor.DiscardOldestPolicy:当任务队列满时丢弃阻塞队列头部的任务(即最老的任务),然后添加当前任务。
2. ThreadPoolExecutor线程池状态
ThreadPoolExecutor有一个AtomicInteger类型成员变量ctl,高3位存储线程池状态,低29位存储线程数量。
(1) 线程池的状态说明
线程池有如下五种状态
- RUNNING:运行状态,线程池可以接收新的任务和执行已入队的任务。
- SHUTDOWN:线程池处不接收新任务,但不影响正在执行的任务,且能处理已入队的任务。全部处理完毕线程全部回收后进入TIDYING状态。
- STOP:线程池处不接收新任务,移除已经入队的任务且不处理,同时会中断正在执行的任务。线程全部回收后进入TIDYING状态。
- TIDYING:线程池中所有的任务已终止,线程数为0;线程池变为TIDYING状态时,会执行钩子函数terminated(),默认实现为空。
- TERMINATED:钩子函数terminated()被执行完成。
(2)关闭线程池的方法
线程池一旦被创建,就处于RUNNING状态,并且线程池中的初始线程数为0。可调用如下方法关闭线程池
-
shutdown():RUNNING -> SHUTDOWN
-
shutdownNow():直接进入STOP状态。
3. ThreadPoolExecutor线程复用原理
ThreadPoolExecutor对象的成员变量workers保存了当前所有工作线程,每个工作线程都是封装为了一个内部类Worker的对象
private final HashSet<Worker> workers = new HashSet<Worker>();
当调用execute()方法提交任务时,若线程池还没满,则调用addWorker()方法添加一个线程并启动线程执行Worker的run()方法
run()方法中执行了runWorker()方法,在执行完firstTask后,会循环通过getTask()方法从等待队列workQueue中取任务执行,以此实现了线程复用。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
...
try {
while (task != null || (task = getTask()) != null) {
...
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
}
...
三、Executors工具类
Executors工具类中有一些用来创建预定义线程池的方法
- FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- SingleThreadExecutor: 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
- ScheduledThreadPool :该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。
但在《阿里巴巴 Java 开发手册》中并不推荐使用,原因有两点:
- 这些预定义的线程池允许的任务队列长度或最大线程数为Integer.MAX_VALUE,可能会导致请求堆积或创建大量线程,从而导致OOM
- 通过 ThreadPoolExecutor 的方式创建可以更加明确线程池的运行规则,规避资源耗尽的风险。
四、线程池的线程数设置
线程数是线程池中的重要参数,如果设置小了则不能充分利用CPU,并且会导致大量任务阻塞等待,如果设置大了则会导致大量的线程上下文切换影响整体效率。因此,我们需要根据这个线程池要负责的任务类型来合理设置最大线程数。任务可分为以下两种
1. CPU 密集型任务(N+1)
这种任务消耗的主要是 CPU 资源,几乎不需要阻塞等待获取其他资源,能够充分利用CPU,因此可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
2. I/O 密集型任务
这种任务运行时需要进行大量IO操作,而线程在内核处理 I/O 的时间段内不会占用 CPU,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程。具体数量根据任务执行期间平均等待时间来定,需要的IO操作越多等待时间越长,最大线程数就可以设置得越多,以充分利用CPU资源。
例如在Tomcat中处理HTTP请求的线程池默认最大线程数为200,因为HTTP请求处理时通常真正用于计算的时间可能很少,大多数时间可能在阻塞,如等待数据库返回数据、等待硬盘读写数据等。
线程数更严谨的计算的方法应该是:最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间)),其中 WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)。
参考文章:
- Java Guide
- https://www.zhihu.com/question/336683528/answer/2518487120
- https://blog.csdn.net/BUPTZhanggg/article/details/128214568