使用线程池可以降低资源消耗,提高线程的可管理性。好处有以下几点:
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
可以利用 JUC 包(java.util.concurrent)下的 Executors 的静态方法创建不同的线程池
方法 | 说明 |
---|---|
newFixedThreadPool(int nThreads) | 指定工作线程数量的线程池。 |
newCachedThreadPool() | 处理大量短时间工作任务的线程池; 1、试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程; 2、如果线程闲置的时机超过阈值,则会被终止并移除缓存; 3、系统长时间闲置的时候,不会消耗什么资源。 |
newSingleThreadExecutor() | 创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它。 |
newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize) | 定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程。 |
newWorkStealingPool() | 内部会构建 ForkJoinPool,利用 working-stealing 算法,并行地处理任务,不保证处理顺序。JDK1.7 及以上版本 Java 提供了 Fork/Join 框架,Fork/Join 是把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架 。 |
生产环境中不建议使用 Executors 的静态方法创建线程池,建议根据业务自定义 ThreadPoolExecutor ,这里我们封装了一个基于链表的阻塞队列的线程池:
public class LinkedBlockingThreadPool extends ThreadPoolExecutor {
/**
* 线程池 基于链表的阻塞队列
*
* @param corePoolSize 核心线程数
* @param maximumPoolSize 最大线程数 maximumPoolSize >= corePoolSize
* @param keepAliveTime 线程存活时间, 秒
* @param blockingQueueCapacity LinkedBlockingQueue的容量
*/
public LinkedBlockingThreadPool(int corePoolSize, int maximumPoolSize,
long keepAliveTime, int blockingQueueCapacity) {
super(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
// 当任务队列是LinkedBlockingQueue, 会将超过核心线程的任务放在任务队列中排队
new LinkedBlockingQueue<Runnable>(blockingQueueCapacity),
Executors.defaultThreadFactory());
}
}
LinkedBlockingQueue 一定需要指定大小,因为 LinkedBlockingQueue 的默认容量是 Integer.MAX_VALUE,不指定的话极端情况下会造成大量积压,进而引发 OOM,业务不可用。
ThreadPoolExecutor 构造函数的七个参数:
public class ThreadPoolExecutor extends AbstractExecutorService {
// ctl的高3位用来保存线程池的运行信息, 另外的低29位保存线程池内有效线程的数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 线程不够用时能够创建的最大线程数 maximumPoolSize>=corePoolSize
long keepAliveTime, // 核心线程数之外的空闲线程的存活时间, 超时后线程销毁
TimeUnit unit, // 核心线程数之外的空闲线程的存活时间单位 TimeUnit.SECONDS:秒
BlockingQueue<Runnable> workQueue, // 任务等待队列
ThreadFactory threadFactory), // 创建新线程的线程工厂
RejectedExecutionHandler handler) { // 线程池饱和策略
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
// 省略了部分代码
}
public abstract class AbstractExecutorService implements ExecutorService {
// 省略代码
}
public interface ExecutorService extends Executor {
// 省略代码
}
public interface Executor {
void execute(Runnable command);
}
ThreadFactory 参数表示创建新线程的线程工厂,一般使用默认的 Executors.defaultThreadFactory(),用这个线程工厂创建的新线程具有相同的优先级且是非守护线程。
RejectedExecutionHandler 参数表示线程池饱和策略,如果阻塞队列满了,并且没有空闲线程,这时如果继续提交任务,这时就需要采取一种策略处理该任务,线程池提供了四种策略:
- AbortPolicy:直接抛出异常,默认策略;
- CallerRunsPolicy:用调用者所在的线程来执行任务;
- DiscardOldestPolicy:丢弃队列中靠最前的任务,并执行当前任务;
- DiscardPolicy:直接丢弃任务。
除此之外,也可以通过实现 RejectedExecutionHandler 接口的自定义 handler。
ExecutorService 的工作流程:
新任务提交 execute 执行后的判断流程图:
线程池的状态:
- RUNNING:能接受新提交的任务,并且也能处理 workQueue 中的任务;
- SHUTDOWN:terminated() 方法执行完后进入该状态。不再接受新提交的任务,但可以处理存量任务;
- STOP:不再接受新提交的任务,也不处理存量任务;
- TIDYING:所有的任务都已终止;
- TERMINATED:terminated() 方法执行完后进入该状态。
线程池的状态转换图:
JUC 的三个 Executor 接口:
- Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦;
- ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善;
- ScheduledExecutorService:支持 Future 和定期执行任务。
队列
线程池主要用到的阻塞队列是前四个
名称 | 特点 |
---|---|
ArrayBlockingQueue | 用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下 |
LinkedBlockingQueue | 基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,此队列按照先进先出(FIFO)的原则对元素进行排序 |
PriorityBlockingQueu | 是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。可以自定义实现compareTo()方法来指定元素进行排序规则,或者初始化PriorityBlockingQueue 时,指定构造参数 Comparator 来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。 |
DelayQueue | 是一个支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue 来实现 |
SynchronousQueue | 是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。 |
LinkedTransferQueue | 是 一 个 由 链 表 结 构 组 成 的 无 界 阻 塞 TransferQueue 队 列 |
LinkedBlockingDeque | 是一个由链表结构组成的双向阻塞队列 |
线程池在业务中的实践
场景1:快速响应用户请求
描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。
分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。
场景2:快速处理批量任务
描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。
分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。
实际问题及方案思考
线程池使用面临的核心的问题在于:线程池的参数并不好配置。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,这导致业界并没有一些成熟的经验策略帮助开发人员参考。
业务中要使用线程池,而使用不当又会导致故障,那么我们怎样才能更好地使用线程池呢?针对这个问题,我们下面延展几个方向:
1. 能否不用线程池?
回到最初的问题,业务使用线程池是为了获取并发性,对于获取并发性,是否可以有什么其他的方案呢替代?我们尝试进行了一些其他方案的调研:
综合考虑,这些新的方案都能在某种情况下提升并行任务的性能,然而本次重点解决的问题是如何更简易、更安全地获得的并发性。另外,Actor模型的应用实际上甚少,只在Scala中使用广泛,协程框架在Java中维护的也不成熟。这三者现阶段都不是足够的易用,也并不能解决业务上现阶段的问题。
2. 追求参数设置合理性?
有没有一种计算公式,能够让开发同学很简易地计算出某种场景中的线程池应该是什么参数呢?
我们调研了业界的一些线程池参数配置方案:
调研了以上业界方案后,我们并没有得出通用的线程池计算方式。并发任务的执行情况和任务类型相关,IO密集型和CPU密集型的任务运行起来的情况差异非常大,但这种占比是较难合理预估的,这导致很难有一个简单有效的通用公式帮我们直接计算出结果。
3. 动态化线程池
- 待完善
如何设置合理的线程池大小?
首先通过公式预估所需线程池大小:
I/O 密集型应用:
线程数=cpu核数∗(1+线程平均等待时间/线程平均执行时间)
CPU 密集型应用:
线程数=cpu核数+1
这两个公式是前人根据大量的经验得出的较为合理的计算方式,然后再通过压测调优,找到最合适的线程池大小。
创建线程池一般不建议使用 Executors 的静态方法去创建,而是应该通过直接构造 ThreadPoolExecutor 的方式,这样的处理方式可以更加明确线程池的运行规则,规避资源耗尽的风险。
常见问题
如果线上机器突然宕机,线程池的阻塞队列中的请求怎么办?
如果说你要提交一个任务到线程池里去,在提交之前,麻烦你先在数据库里插入这个任务的信息,更新他的状态:未提交、已提交、已完成。提交成功之后,更新他的状态是已提交状态。
线程池配置记录(压测配置结果):
机器配置:8 核 16 G,QPS:500,IO 密集型,业务比较重。
Service 层一级并发线程池配置:
核心线程数 corePoolSize:100
最大线程数 maximumPoolSize:200
存活时间 keepAliveTime:5 秒
任务等待队列 workQueue:1500
Service 层二级并发线程池配置:
核心线程数 corePoolSize:200
最大线程数 maximumPoolSize:300
存活时间 keepAliveTime:10 秒
任务等待队列 workQueue:2000