如何将线程池运用于具体工作场景?
线程池简介
-
什么是线程池?
- 线程池,就是管理 n 个线程的池子。池子中的线程可以用来处理任务,并且线程池中的线程可以复用。这样相对于每次执行一个任务就需要开启一个线程,节省了很大一部分的 CPU、内存、磁盘 的开销。
-
为什么要用线程池?
- 降低资源开销。通过对线程池中的线程进行复用,节省线程的创建、销毁等产生的消耗。
- 提高任务执行效率。执行任务,直接使用线程池中的线程,减少创建线程的时间。
- 方便管理线程。对线程进行管理,避免创建过多线程导致内存溢出或者内存泄漏。
线程池有哪些?
- 固定线程数的线程池(
FixedThreadPool
)
- 适用于 负载稳定 的场景,线程数固定表示不会经常创建和销毁线程,执行效率较高。
//线程数固定为10的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
- 只有一个线程的线程池(
SingleThreadExecutor
)
- 适用于 需要顺序执行任务 的场景,所有的任务都在同一个线程中执行。
//线程数只有一个的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
- 可调整线程数的线程池(
CachedThreadPool
)
- 适用于 任务数动态变化 的场景,线程池中的线程数量可根据需要创建新线程,也会复用已经创建的线程。
//线程数可以动态变化的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- 定时或周期性执行的线程池(
ScheduledThreadPool
)
- 适用于 执行定时或者周期性任务 的场景,可以指定任务在指定的时间之后执行,或者定期执行。
//线程数为10的线程池,可定时或周期执行任务
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
以下是使用 FixedThreadPool
计算 100 次,1 到 100 的累加和的使用案例,固定 10 个线程执行。
public static final ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
System.out.println("开始使用:fixedThreadPool!");
//使用线程池 fixedThreadPool 计算 100 次,1 到 100 的累加和。
for (int i = 0; i < 100; i++) {
fixedThreadPool.execute(()->{
calculateSum(100);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
}
public static void calculateSum(int n){
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
System.out.println("CurrentThreadName:" + Thread.currentThread().getName() + "。Sum of 1 to 100: " + sum);
}
但是,不推荐使用这四种方法去创建线程池,建议使用 ThreadPoolExecutor 构造函数的方式创建线程池,原因如下:
FixedThreadPool
和SingleThreadExecutor
等待队列为:LinkedBlockingQueue
,最大长度等于Integer.MAX_VALUE
,如果大量任务堆积,会造成 OOM。CachedThreadPool
等待队列为:SynchronousQueue
,并且允许创建线程数等于Integer.MAX_VALUE
, 如果等待任务很多,不断创建线程,最终会导致 OOM。ScheduledThreadPool
等待队列为:DelayedWorkQueue
,最大长度为 Integer.MAX_VALUE,如果大量任务堆积,会造成 OOM。
使用 ThreadPoolExecutor 自定义线程池
JDK 源码中,详细描述了如何使用 ThreadPoolExecutor
自定义线程池。如下图:
corePoolSize
:核心线程数,即使线程是空闲的,线程池也会保持存活的线程数,除非调用了allowCoreThreadTimeOut
方法,并设置为 true,相当于设置核心线程的存活时间为:keepAliveTime
。maximumPoolSize
:线程池中允许的最大线程数。keepAliveTime
:当线程数超过核心线程数时,多余的空闲线程的存活时间。unit
:keepAliveTime
的时间单位。workQueue
:工作队列,用于存放待执行的任务。threadFactory
:线程工厂,用于创建新的线程。handler
:拒绝策略,当线程池和工作队列都满了,如何处理新加入的任务。
线程池拒绝策略:
AbortPolicy
:抛出RejectedExecutionException
异常,拒绝新来的任务。CallerRunsPolicy
:使用调用线程执行线程池中的线程,主线程。DiscardPolicy
:不处理,直接丢弃。DiscardOldestPolicy
:丢弃等待队列中的最老任务。
以下是使用 ThreadPoolExecutor
计算 100 次,1 到 100 的累加和的使用案例。
corePoolSize
(核心线程):10。maximumPoolSize
(最大线程):20。keepAliveTime
(存活时间):300。unit
(存活时间单位):秒。workQueue
(工作队列):60。handler
(拒绝策略):CallerRunsPolicy
。
public static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 300, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(60), new ThreadPoolExecutor.CallerRunsPolicy());
public static void main(String[] args) {
System.out.println("开始使用:ThreadPoolExecutor!");
//使用线程池 ThreadPoolExecutor 计算 100 次,1 到 100 的累加和。
for (int i = 0; i < 100; i++) {
threadPoolExecutor.execute(()->{
calculateSum(100);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
}
/**
* 计算 1 到 n 的累加和
*/
public static void calculateSum(int n){
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
System.out.println("CurrentThreadName:" + Thread.currentThread().getName() + "。Sum of 1 to 100: " + sum);
}
推荐,最大线程数为核心线程数的 2 倍,等待队列是最大线程数的 2-3 倍。
线程池如何执行
此处引用美团技术博客中,线程池的执行流程图。
- 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
- 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
- 如果任务队列已经满了,但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
- 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用
RejectedExecutionHandler.rejectedExecution()
方法,也就是线程池拒绝策略。
线程池使用场景
- CPU 密集型场景:把核心线程数设置为 CPU 核心 + 1。(《Java 并发编程实战》给出的原因是:即使当计算(CPU)密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保 CPU 的时钟周期不会被浪费。 )但是也要考虑到,如果服务器中部署多个应用,得考虑其他应用的线程池使用情况。
- IO 密集型场景:
- 《Java 并发编程实战》书中给出的计算方式,如下图:
核心线程数 = CPU核数 * CPU 目标使用率 * (1 + 线程等待耗时 / 线程计算耗时)
- 但是,线程等待耗时和线程计算耗时,在实际开发中难以确定。而且 Java 线程池可以利用线程切换的方式,最大程度利用 CPU 核数,这样计算出来的核心线程数,十分偏离实际业务场景。为了解决这些问题,我们可以基于不同时间的业务流量,周期性/定时动态修改线程池的参数。
动态修改线程池参数
- 此处引用美团技术博客中,设置核心线程数(
setCorePoolSize
)流程图。
- 在运行期线程池使用方调用此方法设置
corePoolSize
之后,线程池会直接覆盖原来的corePoolSize
值,并且基于当前值和原始值的比较结果采取不同的处理策略。 - 对于当前值小于当前工作线程数的情况,说明有多余的
worker
线程,此时会向当前idle
(空闲)的worker
线程发起中断请求以实现回收,多余的worker
在下次idel
的时候也会被回收; - 对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的
worker
线程来执行队列任务。
- 注意:如果修改了核心线程数,但是没有对应修改最大线程数,可能造成最终的活跃线程数还是之前的最大线程数。
动态修改核心线程数、最大线程数、存活时间大小、线程工厂、拒绝策略,如下所示。
public static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 300, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(60), new ThreadPoolExecutor.CallerRunsPolicy());
public static void main(String[] args) {
System.out.println("开始使用:fixedThreadPool!");
//使用线程池 fixedThreadPool 计算 100 次,1 到 100 的累加和。
for (int i = 0; i < 100; i++) {
threadPoolExecutor.execute(()->{
calculateSum(100);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
threadPoolExecutor.setCorePoolSize(20);
threadPoolExecutor.setMaximumPoolSize(30);
threadPoolExecutor.setKeepAliveTime(600, TimeUnit.SECONDS);
threadPoolExecutor.setThreadFactory(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread.currentThread().setName("test");
return Thread.currentThread();
}
});
threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
}
}
由于 LinkedBlockingDeque
的容量是 final 常量,如下图。所以,如果想要动态修改工作队列大小,需要自己创建一个队列,设置容量为 private int capacity
。
以上就是全部内容了,包含了什么是线程池、为什么要用线程池、线程池有哪些、线程池如何执行、线程池的使用场景、如何修改线程池参数。