Java性能 -- 线程池大小
线程池原理
- 在Hotspot JVM的线程模型中,Java线程被一对一映射为内核线程
- Java使用线程执行程序时,需要创建一个内核线程,当该Java线程被终止时,这个内核线程也会被回收
- Java线程的创建和销毁将会消耗一定的计算机资源,从而增加系统的性能开销
- 大量创建线程也会给系统带来性能问题,线程会抢占内存和CPU资源,可能会发生内存溢出、CPU超负载等问题
- 线程池:即可以提高线程复用,也可以固定最大线程数,防止无限制地创建线程
- 当程序提交一个任务需要一个线程时,会去线程池查找是否有空闲的线程
- 如果有,则直接使用线程池中的线程工作,如果没有,则判断当前已创建的线程数是否超过最大线程数
- 如果未超过,则创建新线程,如果已经超过,则进行排队等待或者直接抛出异常
线程池框架Executor
- Java最开始提供了ThreadPool来实现线程池,为了更好地实现用户级的线程调度,Java提供了一套Executor框架
- Executor框架包括了ScheduledThreadPoolExecutor和ThreadPoolExecutor两个核心线程池,核心原理一样
- ScheduledThreadPoolExecutor用来定时执行任务,ThreadPoolExecutor用来执行被提交的任务
Executors
- Executors利用工厂模式实现了4种类型的ThreadPoolExecutor
- 不推荐使用Executors,因为会忽略很多线程池的参数设置,容易导致无法调优,产生性能问题或者资源浪费
- 推荐使用ThreadPoolExecutor自定义参数配置
类型 | 特性 |
---|---|
newCachedThreadPool | 线程池大小不固定,可灵活回收空闲线程,若无可回收,则新建线程 |
newFixedThreadPool | 线程池大小固定,当有新任务提交,线程池中如果有空闲线程,则立即执行, 否则新的任务会被缓存在一个任务队列中,等待线程池释放空闲线程 |
newScheduledThreadPool | 定时线程池,支持定时或者周期性地执行任务 |
newSingleThreadExecutor | 只创建一个线程,保证所有任务按照指定顺序(FIFO/LIFO/优先级)执行 |
BlockingQueue
- ArrayBlockingQueue
- 基于数组结构实现的有界阻塞队列,按照FIFO原则对元素进行排序
- 使用ReentrantLock、Condition来实现线程安全
- LinkedBlockingQueue
- 基于链表结构实现的阻塞队列,按照FIFO原则对元素进行排序
- 使用ReentrantLock、Condition来实现线程安全
- 吞吐量通常要高于ArrayBlockingQueue
- PriorityBlockingQueue
- 基于二叉堆结构实现的具有优先级的无界阻塞队列
- 队列没有实现排序,每当有数据变更时,都会将最小或最大的数据放在堆最上面的节点上
- 使用ReentrantLock、Condition来实现线程安全
- DelayQueue
- 支持延时获取元素的无界阻塞队列,基于PriorityBlockingQueue扩展实现
- SynchronousQueue
- 不存储多个元素的阻塞队列,每次进行放入数据时,必须等待相应的消费者取走数据后,才可以再放入数据
线程池类型 | 实现队列 |
---|---|
newCachedThreadPool | SynchronousQueue |
newFixedThreadPool | LinkedBlockingQueue |
newScheduledThreadPool | DelayedWorkQueue |
newSingleThreadExecutor | LinkedBlockingQueue |
ThreadPoolExecutor
1 | // 构造函数 |
- 在创建完线程池之后,默认情况下,线程池并没有任何线程,等到有任务来才创建线程去执行任务
- 如果调用prestartAllCoreThreads或者prestartCoreThread,可以提前创建等于核心线程数的线程数量,称为预热
- 当创建的线程数等于corePoolSize,提交的任务会被加入到设置的阻塞队列中
- 当阻塞队列满了,会创建线程执行任务,直到线程池中的数量等于maximumPoolSize
- 当线程数等于maximumPoolSize,新提交的任务无法加入到阻塞队列,也无法创建非核心线程直接执行
- 如果没有为线程池设置拒绝策略,线程池会抛出RejectedExecutionException,拒绝接受该任务
- 当线程数超过corePoolSize,在某些线程处理完任务后,如果等待keepAliveTime后仍然空闲,那么该线程将会被回收
- 回收线程时,不会区分是核心线程还是非核心线程,直到线程池中线程的数量等于corePoolSize,回收过程才会停止
- 默认情况下,当线程数小于等于corePoolSize时,是不会触发回收过程的,因此_非核心业务线程池的空闲线程会长期存在_
- 可以通过allowCoreThreadTimeOut方法设置:包括核心线程在内的所有线程,在空闲keepAliveTime后会被回收
线程池的线程分配流程
计算线程数量
多线程执行的任务类型分为CPU密集型和IO密集型
CPU密集型
- 该类型任务的消耗主要是CPU资源,可以将线程数设置为N(CPU核心数)+1
- +1是为了防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的影响,+1能够更充分地利用CPU的空闲时间
IO密集型
- 该类型任务在运行时,系统会用大部分的时间来处理IO交互
- 线程在处理IO的时间段内是不会占用CPU来处理,此时可以将CPU交出给其他线程使用,可以先设置为2N
通用场景
1 | 线程数 = N * (1+ WT/ST) |
可以根据业务场景,先简单地选择N+1或者2N,然后进行压测,最后依据压测结果进行调整
参考资料
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.