设计思想
本文主要介绍线程池的设计思想及简单使用方式,如有疏漏之处,敬请批评指正。
在介绍线程池之前,首先明确一下什么是线程。百度百科的说法:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
笔者按照自己的理解,以服务大厅(进程)与服务窗口(线程)举例:
-
单线程模式:服务大厅只有一个窗口,所有办理业务的人,排队进行处理。
-
多线程模式:来一个客户办理业务,便找客服人员多开一个窗口,办理完成之后,再关闭窗口。
-
线程池模式:提前预设N个窗口,找一个调度人员给客户进行挂号排队,叫到号的客户去对应窗口进行业务办理。如果客户特别多,动态增加服务窗口,为了保证服务大厅的可用性,最大可增加至M个窗口。当窗口空闲一段时间之后,再请客服人员退场休息。
使用线程池有哪些好处呢?
- 线程池可以减少多线程场景下,不断创建、销毁线程的资源;
- 线程池可以根据业务量、系统承载能力,有效管理最小线程、最大线程,以防止业务量过大、程序不断创建线程导致OOM。
看到这里的同学,可能对线程池已经有了一定概念,接下来一起看一下线程池的工作原理,笔者画了两张简图。
多线程:
线程池:
重点是工作队列。线程池中队列的出现,代表线程池使用的是事件驱动模型,当工作线程资源充足时,系统会将队列中的任务委派至对应线程处理(部分线程池无缓存队列);当没有可用线程时,可以根据配置等待或丢弃。如果当前线程池中的活跃线程数小于最大线程数,当所有线程处于工作状态时,会创建新的线程,直到活跃线程数等于最大线程数。待活跃线程空闲一定时间之后,会被回收。
事件驱动模型可以降低不同模块或系统之间的耦合度。线程池的设计思想,通过工作队列将请求与处理请求的线程解耦。笔者一直信奉一句话:“程序世界中,没有什么问题是加一层中间件解决不了的”。而线程池中的工作队列,也在一定程度上充当中间件的作用。
了解线程池的设计思想之后,我们来看下常见的使用方式。
使用方式
首先,引用一段《阿里巴巴Java开发手册》中的话:
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。——《阿里巴巴Java开发手册》
所以,本文只介绍ThreadPoolExecutor线程池的使用方式。
ThreadPoolExecutor构造函数如下:
public ThreadPoolExecutor(
// 最小连接数
int corePoolSize,
// 最大连接数
int maximumPoolSize,
// 线程空闲等待时间,超过时间之后,空闲线程回收
long keepAliveTime,
// 线程空闲时间单位
TimeUnit unit,
// 工作队列
BlockingQueue<Runnable> workQueue,
// 构建线程的工厂函数
ThreadFactory threadFactory,
// 拒绝策略
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- corePoolSize:最小线程数
- maximumPoolSize:最大线程数
- keepAliveTime、unit:如果线程池中活跃线程数 > corePoolSize,那么当线程空闲时间到达
keepAliveTime & unit
之后,空闲线程会被回收。 - workQueue:工作线程,常用的有SynchronousQueue、ArrayBlockingQueue、LinkedBlockingQueue三种。
- SynchronousQueue:无缓存的阻塞队列。既,队列长度为0,当收到请求时,若无空闲线程,则触发拒绝策略。(此队列一般要求线程池最大线程数为Integer.MAX_VALUE,防止因阻塞导致请求丢失,但也存在OOM的问题。)
- ArrayBlockingQueue:基于数组结构的阻塞队列,线程池创建后,无法修改缓存区(队列)的长度。
- LinkedBlockingQueue:基于联表结构的阻塞队列。
- 其他队列:PriorityBlockingQueue、DelayQueue
- threadFactory:线程构造函数,可以定义线程池中线程的名称等信息,方便排查问题;
- handler:拒绝策略。系统提供了四种拒绝策略,分别是:CallerRunsPolicy、AbortPolicy、DiscardPolicy、DiscardOldestPolicy。
- CallerRunsPolicy:提交任务的线程自己去执行。比如使用main方法创建线程池,当触发拒绝策略时,主线程(main)去执行此任务;
- AbortPolicy:默认拒绝策略,系统抛出异常:RejectedExecutionException;
- DiscardPolicy:直接丢弃(任务丢失,不建议使用);
- DiscardOldestPolicy:丢弃最早进入队列的任务,新任务放入队列中。
- 当然,如果你觉得这些策略都不够优雅,你也可以自定义拒绝策略。只需要实现
RejectedExecutionHandler
接口即可(自定义拒绝策略,在某种程度上,可以实现服务降级。我们可以Mock一个响应,给客户端)。
代码示例
package test;
import cn.hutool.core.thread.ThreadFactoryBuilder;
import java.util.concurrent.*;
public class Test {
public static void main (String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
1, // 初始线程数
10, // 最大线程数
1000, // 空闲线程活跃时间
TimeUnit.MINUTES, // 空闲线程活跃时间单位
//new LinkedBlockingQueue<Runnable>(3),
// new SynchronousQueue<Runnable>(),
new ArrayBlockingQueue<Runnable>(3),
// new DelayQueue<>(),
//new PriorityBlockingQueue<>(),
// 通过ThreadFactory,自定义线程名称,方便排查问题
new ThreadFactoryBuilder().setNamePrefix("test-thread-pool-").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
// 自定义拒绝策略
// new RejectedExecutionHandler() {
//
// @Override
// public void rejectedExecution (Runnable r, ThreadPoolExecutor executor) {
// System.out.println("哈哈哈,不够使了~" + r + executor);
// }
// }
);
for (int i = 0; i < 20; i++) {
int id = i;
threadPoolExecutor.execute(() -> {
System.out.println("测试ID: " + id + ",线程:" + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
threadPoolExecutor.shutdown();
}
}
文以载道,疏漏之处敬请批评指正,愿与诸君共勉。