Java并发编程(线程池)
- 线程频繁的创建和销毁带来性能开销
- 线程的数量过多,上下文切换,消耗CPU资源
池化技术
连接池、对象池、内存池、线程池
池化计数的核心:复用
线程池
提前创建一系列的线程,保存在这个线程池中。
有任务要执行时,从线程池取出线程,任务执行完后归还线程。
Java线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
-
线程池参数:
- corePoolSize:核心线程数
- maximumPoolSize:允许创建的最大线程数量
- keepAliveTime:空闲线程的存活时间
- unit:keepAliveTime 的时间单位
- workQueue:工作队列,存放待执行任务的队列。
- threadFactory:创建线程的工厂,可以自定义线程名等属性
- handler:拒绝策略
- AbortPolicy:抛出RejectedExecutionException异常。(默认的拒绝策略)
- DiscardPolicy:丢弃该任务,不会有任何异常抛出。
- DiscardOldestPolicy:丢弃队列中最前面的一个任务,并尝试再次提交被拒绝的任务。
- CallerRunsPolicy:由提交任务的线程来执行该任务。
-
线程池运行过程:
- 当提交一个新任务时,如果当前线程数小于corePoolSize,那么会创建新的线程执行任务。
- 当运行的任务数达到corePoolSize后,后续提交的任务会存放在工作队列,任务调度时再从队列中取出任务执行。
- 当提交一个新任务,如果当前线程数大于等于corePoolSize,小于maximumPoolSize,且工作队列已满时,会创建新的线程执行任务。
- 当线程池线程数达到maximumPoolSize,且工作队列达到上限,提交的任务使用拒绝策略处理。
-
空闲线程的回收:
- 当线程数大于核心线程数时,空闲时间超过keepAliveTime的线程会被回收。
- 如果设置了allowCoreThreadTimeout=true,当线程数小于等于核心线程数时,也会回收空闲的线程。
Java中提供的线程池
- newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
线程池的设计
线程池的线程数量设置
- 线程池线程数量公式
-
CPU密集性
- CPU的利用率高,尽可能避免发生线程上下文的切换
- W很小,C很大
- 线程数设定参考:CPU核数 + 1
-
IO密集型
- CPU的利用率不高,等待IO时间
- W很大,C很小
- 线程数设定参考:根据公式计算(关注W/C的比值),或者2*CPU核数
-
公式只是理论值,实际环境中服务器上有很多其他线程在运行,都会占用资源。要靠压测来验证。
-
压测方法:
- 尽可能排除其他线程的干扰
- 设定目标:
- 期望的CPU利用率
- 期望的GC指标:GC频率/吞吐量/停顿时间
- 期望的TPS/QPS,RT平均响应时间
- 不断调整线程数来压测,满足自己的目标期望。
-
压测后得到多个线程数的数值和对应的性能指标
- 对于核心应用,业务调用频繁,可以设置核心线程数=最大线程数
- 对于非核心应用,可以设置核心线程数 < 最大线程数
考虑资源瓶颈
- 真实的调用场景往往会很复杂,关于优化性能,要考虑到整个调用链路上的所有节点,哪些是资源瓶颈,优先针对资源瓶颈优化。
- 比如说做一个文件上传功能,带宽是2M,上传的文件是1M,那么每秒最多能上传2个文件,限制了整体的吞吐量,设置过多的线程处理也无法提高性能。