线程池原理

  1. Hotspot JVM的线程模型中,Java线程被一对一映射为内核线程
    • Java使用线程执行程序时,需要创建一个内核线程,当该Java线程被终止时,这个内核线程也会被回收
    • Java线程的创建和销毁将会消耗一定的计算机资源,从而增加系统的性能开销
    • 大量创建线程也会给系统带来性能问题,线程会抢占内存和CPU资源,可能会发生内存溢出、CPU超负载等问题
  2. 线程池:即可以提高线程复用,也可以固定最大线程数,防止无限制地创建线程
    • 当程序提交一个任务需要一个线程时,会去线程池查找是否有空闲的线程
    • 如果有,则直接使用线程池中的线程工作,如果没有,则判断当前已创建的线程数是否超过最大线程数
    • 如果未超过,则创建新线程,如果已经超过,则进行排队等待或者直接抛出异常

线程池框架Executor

  1. Java最开始提供了ThreadPool来实现线程池,为了更好地实现用户级的线程调度,Java提供了一套Executor框架
  2. Executor框架包括了ScheduledThreadPoolExecutorThreadPoolExecutor两个核心线程池,核心原理一样
    • ScheduledThreadPoolExecutor用来定时执行任务,ThreadPoolExecutor用来执行被提交的任务

Executors

  1. Executors利用工厂模式实现了4种类型的ThreadPoolExecutor
  2. 不推荐使用Executors,因为会忽略很多线程池的参数设置,容易导致无法调优,产生性能问题或者资源浪费
  3. 推荐使用ThreadPoolExecutor自定义参数配置
类型 特性
newCachedThreadPool 线程池大小不固定,可灵活回收空闲线程,若无可回收,则新建线程
newFixedThreadPool 线程池大小固定,当有新任务提交,线程池中如果有空闲线程,则立即执行,
否则新的任务会被缓存在一个任务队列中,等待线程池释放空闲线程
newScheduledThreadPool 定时线程池,支持定时或者周期性地执行任务
newSingleThreadExecutor 只创建一个线程,保证所有任务按照指定顺序(FIFO/LIFO/优先级)执行

BlockingQueue

  1. ArrayBlockingQueue
    • 基于数组结构实现的有界阻塞队列,按照FIFO原则对元素进行排序
    • 使用ReentrantLockCondition来实现线程安全
  2. LinkedBlockingQueue
    • 基于链表结构实现的阻塞队列,按照FIFO原则对元素进行排序
    • 使用ReentrantLockCondition来实现线程安全
    • 吞吐量通常要高于ArrayBlockingQueue
  3. PriorityBlockingQueue
    • 基于二叉堆结构实现的具有优先级无界阻塞队列
    • 队列没有实现排序,每当有数据变更时,都会将最小最大的数据放在堆最上面的节点上
    • 使用ReentrantLockCondition来实现线程安全
  4. DelayQueue
    • 支持延时获取元素无界阻塞队列,基于PriorityBlockingQueue扩展实现
  5. SynchronousQueue
    • 不存储多个元素的阻塞队列,每次进行放入数据时,必须等待相应的消费者取走数据后,才可以再放入数据
线程池类型 实现队列
newCachedThreadPool SynchronousQueue
newFixedThreadPool LinkedBlockingQueue
newScheduledThreadPool DelayedWorkQueue
newSingleThreadExecutor LinkedBlockingQueue

ThreadPoolExecutor

1
2
3
4
5
6
7
8
9
// 构造函数
public ThreadPoolExecutor(int corePoolSize, // 线程池的核心线程数
int maximumPoolSize, // 线程池的最大线程数
long keepAliveTime, // 当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列,用来存储等待执行的任务
ThreadFactory threadFactory, // 线程工厂,用来创建线程,用默认即可
RejectedExecutionHandler handler // 拒绝策略,当提交的任务过多而不能及时处理时,可以定制拒绝策略
)
  1. 在创建完线程池之后,默认情况下,线程池并没有任何线程,等到有任务来才创建线程去执行任务
    • 如果调用prestartAllCoreThreads或者prestartCoreThread,可以提前创建等于核心线程数的线程数量,称为预热
  2. 当创建的线程数等于corePoolSize,提交的任务会被加入到设置的阻塞队列
  3. 当阻塞队列了,会创建线程执行任务,直到线程池中的数量等于maximumPoolSize
  4. 当线程数等于maximumPoolSize,新提交的任务无法加入到阻塞队列,也无法创建非核心线程直接执行
    • 如果没有为线程池设置拒绝策略,线程池会抛出RejectedExecutionException,拒绝接受该任务
  5. 当线程数超过corePoolSize,在某些线程处理完任务后,如果等待keepAliveTime后仍然空闲,那么该线程将会被回收
    • 回收线程时,不会区分是核心线程还是非核心线程,直到线程池中线程的数量等于corePoolSize,回收过程才会停止
  6. 默认情况下,当线程数小于等于corePoolSize时,是不会触发回收过程的,因此_非核心业务线程池的空闲线程会长期存在_
    • 可以通过allowCoreThreadTimeOut方法设置:包括核心线程在内的所有线程,在空闲keepAliveTime后会被回收

线程池的线程分配流程

计算线程数量

多线程执行的任务类型分为CPU密集型IO密集型

CPU密集型

  1. 该类型任务的消耗主要是CPU资源,可以将线程数设置为N(CPU核心数)+1
  2. +1是为了防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的影响,+1能够更充分地利用CPU的空闲时间

IO密集型

  1. 该类型任务在运行时,系统会用大部分的时间来处理IO交互
  2. 线程在处理IO的时间段内是不会占用CPU来处理,此时可以将CPU交出给其他线程使用,可以先设置为2N

通用场景

1
2
3
4
5
线程数 = N * (1+ WT/ST)

N = CPU核数
WT = 线程等待时间
ST= 线程运行时间

可以根据业务场景,先简单地选择N+1或者2N,然后进行压测,最后依据压测结果进行调整

参考资料

Java性能调优实战