线程池是什么?为什么要使用线程池?
线程作为操作系统宝贵的资源,对它的使用需要进行控制管理,线程池就是采用池化思想(类似连接池、常量池、对象池等)管理线程的工具。
使用线程池可以带来以下好处:
- 降低资源消耗。降低频繁创建、销毁线程带来的额外开销,复用已创建线程
- 降低使用复杂度。将任务的提交和执行进行解耦,我们只需要创建一个线程池,然后往里面提交任务就行,具体执行流程由线程池自己管理,降低使用复杂度
- 提高线程可管理性。能安全有效的管理线程资源,避免不加限制无限申请造成资源耗尽风险
- 提高响应速度。任务到达后,直接复用已创建好的线程执行
Java线程池有哪些?
| 线程池 | 描述 |
|---|---|
| FixedThreadPool | 核心线程数与最大线程数相同 |
| CachedThreadPool | 核心线程为0,最大线程数为Integer. MAX_VALUE |
| SingleThreadExecutor | 一个线程的线程池 |
| ScheduledThreadPool | 指定核心线程数的定时线程池 |
| SingleThreadScheduledExecutor | 一个线程的定时线程池 |
| WorkStealingPool | 核心线程数与最大线程数相同 |
- FixedThreadPool: 创建一个固定大小的线程池,当线程池中的线程都处于活动状态时,新提交的任务将会等待,直到有线程空闲。如果线程池中的线程数量已经达到最大值并且所有线程都处于活动状态,则新提交的任务将被拒绝。创建方法:
Executors.newFixedThreadPool(int nThreads)。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- CachedThreadPool: 创建一个可以缓存线程的线程池,线程池的大小会根据实际情况动态调整,如果线程池中的线程都在执行任务,新提交的任务会在队列中等待,当有线程空闲时,线程池会创建新的线程来执行任务。如果线程闲置超过60秒,则会被回收。创建方法:
Executors.newCachedThreadPool()。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- SingleThreadExecutor: 创建一个只有一个线程的线程池,所有任务在一个线程中按顺序执行,相当于线程同步,保证了所有任务的执行顺序。创建方法:
Executors.newSingleThreadExecutor()。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- ScheduledThreadPool: 创建一个可以定期执行任务或在指定延迟后执行任务的线程池。主要用于执行定时任务和具有固定周期的任务。创建方法:
Executors.newScheduledThreadPool(int corePoolSize)。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
- SingleThreadScheduledExecutor: 在SingleThreadExecutor基础上增加了定时任务和周期任务调度的功能,允许用户安排任务在未来某一时刻执行一次,或者按照固定的延迟或间隔重复执行任务。创建方法:
Executors.newSingleThreadScheduledExecutor()。
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
- WorkStealingPool(Java 8及以上版本): 创建一个工作窃取线程池,线程池内部采用“工作窃取”算法,当一个线程空闲时,会尝试从其他线程那里“偷取”任务来执行,以提高多核处理器的利用率。创建方法:
Executors.newWorkStealingPool(int parallelism)。
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
线程池是怎么实现的?
线程池主要接口和类关系图:
顶级接口Executor提供了一种方式,解耦任务的提交和执行,只定义了一个 execute(Runnable command) 方法用来提交任务,至于具体任务怎么执行则交给他的实现者去自定义实现。
ExecutorService 接口继承 Executor,且扩展了生命周期管理的方法、返回 Futrue 的方法、批量提交任务的方法。
AbstractExecutorService 抽象类继承 ExecutorService 接口,对 ExecutorService 相关方法提供了默认实现,用 RunnableFuture 的实现类 FutureTask 包装 Runnable 任务,交给 execute() 方法执行,然后可以从该 FutureTask 阻塞获取执行结果,并且对批量任务的提交做了编排。
ThreadPoolExecutor 继承 AbstractExecutorService,采用池化思想管理一定数量的线程来调度执行提交的任务,且定义了一套线程池的生命周期状态,用一个 ctl 变量来同时保存当前池状态(高3位)和当前池线程数(低29位)。看过源码的小伙伴会发现,ThreadPoolExecutor 类里的方法大量有同时需要获取或更新池状态和池当前线程数的场景,放一个原子变量里,可以很好的保证数据的一致性以及代码的简洁性。
// 用此变量保存当前池状态(高3位)和当前线程数(低29位)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
// 可以接受新任务提交,也会处理任务队列中的任务
// 结果:111跟29个0:111 00000000000000000000000000000
private static final int RUNNING = -1 << COUNT_BITS;
// 不接受新任务提交,但会处理任务队列中的任务
// 结果:000 00000000000000000000000000000
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 不接受新任务,不执行队列中的任务,且会中断正在执行的任务
// 结果:001 00000000000000000000000000000
private static final int STOP = 1 << COUNT_BITS;
// 任务队列为空,workerCount = 0,线程池的状态在转换为TIDYING状态时,会执行钩子方法terminated()
// 结果:010 00000000000000000000000000000
private static final int TIDYING = 2 << COUNT_BITS;
// 调用terminated()钩子方法后进入TERMINATED状态
// 结果:010 00000000000000000000000000000
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
// 低29位变为0,得到了线程池的状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 高3位变为为0,得到了线程池中的线程数
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
项目中应该怎样使用线程池?
首先,不要使用Executors工具类创建线程池。阿里巴巴 Java 开发规范里明确说明不允许使用 Executors 创建线程池,而是通过 ThreadPoolExecutor 显示指定参数去创建。为什么呢,因为Executors 创建的线程池有发生 OOM 的风险:
-
Executors.newFixedThreadPool 和 Executors.SingleThreadPool 创建的线程池内部使用的是无界(Integer.MAX_VALUE)的 LinkedBlockingQueue 队列,可能会堆积大量请求,导致 OOM。
-
Executors.newCachedThreadPool 和 Executors.scheduledThreadPool 创建的线程池最大线程数是用的Integer.MAX_VALUE,可能会创建大量线程,导致 OOM。
其次,如果在Spring应用中,更推荐使用ThreadPoolTaskExecutor创建线程池。
- ThreadPoolTaskExecutor是 Spring Framework 提供的一种对
ThreadPoolExecutor的封装与增强,它基于ThreadPoolExecutor构建,并提供了 Spring 风格的配置、生命周期管理和与 Spring 容器更好的集成。开发者可以利用 Spring 的基础设施来监控线程池状态、调整线程池配置,以及在 Spring 容器启动和关闭时自动管理线程池的生命周期。
线程池的核心参数都有哪些?
包含核心线程数(corePoolSize)、最大线程数(maximumPoolSize),空闲线程超时时间(keepAliveTime)、时间单位(unit)、阻塞队列(workQueue)、拒绝策略(handler)、线程工厂(ThreadFactory)这7个参数。
这些参数在线程池的执行流程中(即execute() 方法执行流程)起作用。
public void execute(Runnable command) {
if (command == null) {
throw new NullPointerException();
} else {
int c = this.ctl.get();
if (workerCountOf(c) < this.corePoolSize) {
if (this.addWorker(command, true)) {
return;
}
c = this.ctl.get();
}
if (isRunning(c) && this.workQueue.offer(command)) {
int recheck = this.ctl.get();
if (!isRunning(recheck) && this.remove(command)) {
this.reject(command);
} else if (workerCountOf(recheck) == 0) {
this.addWorker((Runnable)null, false);
}
} else if (!this.addWorker(command, false)) {
this.reject(command);
}
}
}
总结下任务执行流程:
-
判断线程池的状态,如果不是RUNNING状态,直接执行拒绝策略
-
如果当前线程数 < 核心线程池,则新建一个线程来处理提交的任务
-
如果当前线程数 > 核心线程数且任务队列没满,则将任务放入阻塞队列等待执行
-
如果 核心线程池 < 当前线程池数 < 最大线程数,且任务队列已满,则创建新的线程执行提交的任务
-
如果当前线程数 > 最大线程数,且队列已满,则执行拒绝策略拒绝该任务
线程池的参数应该怎么设置?
1. 确定任务类型
- CPU密集型任务:这类任务主要消耗CPU资源,如数学计算、图像处理等。线程池大小通常设置为CPU核心数的1.5至2倍,以充分利用多核处理器。过多的线程可能导致上下文切换频繁,反而降低效率。
- I/O密集型任务:这类任务主要涉及网络通信、磁盘读写等,线程在等待I/O操作时通常会释放CPU。因此,线程池大小可以设置得比CPU核心数大得多,一般建议根据I/O等待时间和预期并发量来设置,但需注意避免过度增加线程导致系统资源紧张。
2. 评估系统资源
- CPU核心数:了解服务器的CPU核心数,这是设置核心线程数和最大线程数的基础。
- 内存大小:如果任务占用大量内存,或者使用有界任务队列时(任务堆积可能导致内存溢出),要考虑内存容量对线程数和队列大小的影响,防止内存溢出。
- 系统负载:观察系统的平均和峰值负载情况,确保线程池配置不会导致系统过载。
3. 设置核心线程数(Core Pool Size)
- 根据任务类型和系统资源:判断任务是CPU密集型还是I/O密集型任务,结合系统CPU核心数设置核心线程数。
- 稳定性要求:如果希望线程池始终保持一定的活跃线程以提供即时响应,可以设置一个较高的核心线程数,即使在空闲时也不回收。
4. 设置最大线程数(Maximum Pool Size)
- 防止资源耗尽:设置一个合理的上限,防止在高负载下线程数无限增长,导致系统资源耗尽。
- 应对突发流量:最大线程数应足以应对预期的峰值任务负载,允许线程池在短时间内扩容以处理突发任务。
5. 设置空闲线程存活时间(Keep Alive Time)
- 资源回收与创建成本权衡:对于瞬息万变的任务负载,设置较短的存活时间可以快速回收空闲线程,减少资源浪费。反之,如果任务负载相对稳定,较长的存活时间可以减少线程创建和销毁的开销。
6. 选择任务队列(Work Queue)
- 无界队列(如
LinkedBlockingQueue) :适用于任务产生速度与消费速度大致匹配且内存资源充足的场景,可以避免任务被拒绝,但可能导致内存持续增长。 - 有界队列(如
ArrayBlockingQueue或PriorityBlockingQueue) :有助于防止资源过度消耗,特别是内存有限时。队列满时,线程池会根据拒绝策略处理新来的任务。有界队列还能通过调整队列大小控制任务堆积程度和线程池的动态伸缩。 - 同步移交队列(如
SynchronousQueue) :每个提交的任务必须立刻被一个线程接手,否则任务生产者会被阻塞。这种队列可以强制线程池在任务到来时立即创建新线程(如果核心线程数已满且最大线程数未达上限),适用于任务执行非常快速且希望立即执行的任务场景。
7. 指定线程工厂(Thread Factory)
- 自定义线程属性:如线程名称、优先级、是否守护线程等,以便于管理和监控。
8. 选择拒绝策略(RejectedExecutionHandler)
- AbortPolicy(默认):抛出
RejectedExecutionException,阻止系统继续接收新任务。 - CallerRunsPolicy:提交任务的线程自己执行被拒绝的任务,降低系统处理速度,避免任务丢失。
- DiscardPolicy:默默丢弃无法执行的任务,可能会导致数据丢失或业务逻辑错误。
- DiscardOldestPolicy:抛弃队列中最旧的任务,尝试重新提交当前任务。可能牺牲旧任务,但能尝试处理新任务。
什么是阻塞队列?阻塞队列有哪些?
阻塞队列 BlockingQueue 继承 Queue,是我们熟悉的基本数据结构队列的一种特殊类型。 当从阻塞队列中获取数据时,如果队列为空,则等待直到队列有元素存入。当向阻塞队列中存入元素时,如果队列已满,则等待直到队列中有元素被移除。提供 offer()、put()、take()、poll() 等常用方法。
JDK 提供的阻塞队列的实现有以下前 7 种:
-
ArrayBlockingQueue:由数组实现的有界阻塞队列,该队列按照 FIFO 对元素进行排序。维护两个整形变量,标识队列头尾在数组中的位置,在生产者放入和消费者获取数据共用一个锁对象,意味着两者无法真正的并行运行,性能较低。
-
LinkedBlockingQueue:由链表组成的有界阻塞队列,如果不指定大小,默认使用 Integer.MAX_VALUE 作为队列大小,该队列按照 FIFO 对元素进行排序,对生产者和消费者分别维护了独立的锁来控制数据同步,意味着该队列有着更高的并发性能。
-
SynchronousQueue:不存储元素的阻塞队列,无容量,可以设置公平或非公平模式,插入操作必须等待获取操作移除元素,反之亦然。
-
PriorityBlockingQueue:支持优先级排序的无界阻塞队列,默认情况下根据自然序排序,也可以指定 Comparator。
-
DelayQueue:支持延时获取元素的无界阻塞队列,创建元素时可以指定多久之后才能从队列中获取元素,常用于缓存系统或定时任务调度系统。
-
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,与LinkedBlockingQueue相比多了transfer和tryTranfer方法,该方法在有消费者等待接收元素时会立即将元素传递给消费者。
-
LinkedBlockingDeque:一个由链表结构组成的双端阻塞队列,可以从队列的两端插入和删除元素。