1,概念
一个线程池管理了一组工作线程,同时它还包括了一个用于放置等待执行任务的任务队列(阻塞队列)。
默认情况下,在创建了线程池后,线程池中的线程数为0。
1.1 Executor接口
Executor 是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable 类型,从字面意思可以理解,就是用来执行传进去的任务的;
真正的线程池的实现为ThreadPoolExecutor。
1.2 Executors 类
通过工厂类创建了多种ThreadPoolExecutor线程池:
但是一般不这样使用,因为:
- FixedThreadPool和SingleThreadExecutor允许请求队列长度为Integer.MAX_VALUE
可能会堆积大量请求导致OOM - CachedThreadPool和ScheduledThreadPool允许创建线程数量为Integer.MAX_VALUE
可能会创建大量的线程,从而导致OOM。
1>FixedThreadPool(固定型)
- 线程数量固定
- 不回收空闲线程
相当于全部是核心线程,线程池关闭才释放。 - 允许请求队列长度为Integer.MAX_VALUE
场景:适合立刻响应执行的任务。
ExecutorService pool = Executors.newFixedThreadPool(2); //核心线程数为2
//创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
Thread t1 = new MyThread();
t1.setPriority(1);//设置线程优先级
//将线程放入池中进行执行
pool.execute(t1);
2> CachedThreadPool(缓存型)
- 线程数量不定;==》全是最大 核心为0
- 超过60s空闲线程会回收;
- 允许创建线程数量为Integer.MAX_VALUE;
场景:适合执行大量的耗时较少的任务(即生存期较短的异步型任务)。
3> ScheduledThreadPool(调度型)
- 核心线程数量固定;
- 最大线程数:Integer.MAX_VALUE
- 线程可以按schedule依次delay执行,或周期执行。
场景:用于执行定时任务和具有固定周期的重复任务。
4> SingleThreadExecutor(单例型)
- 只有一个核心线程
确保所有的任务都在同一个线程中按顺序执行。 - 请求队列长度为Integer.MAX_VALUE
注意:如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务。
优点:保证所有任务顺序执行。
1.3 ExecutorService 接口
继承了Executor 接口,并声明了一些方法:submit、invokeAll、invokeAny 以及shutDown 等;
1.4 AbstractExecutorService 抽象类
实现了ExecutorService 接口,基本实现了ExecutorService 中声明的所有方法;
1.5 ThreadPoolExecutor 类
继承了类AbstractExecutorService。
2,优点(线程复用和资源控制)
- 重用线程池中的线程
避免因为线程的创建和销毁所带来的性能开销。通过固定线程调用任务的run方法。 - 有效控制线程池的最大并发数
避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。
如果不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存。 - 提高线程的可管理性。
使用线程池可以对线程进行统一的分配和监控,提供定时执行、指定间隔循环执行等功能。 - 提高响应速度。
当任务到达时,任务可以不需要等到线程创建就可以立即执行。
3,参数配置
ThreadPoolExecutor 的构造方法提供了一系列参数来配置线程池。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
0)新任务进入线程池执行策略
- workerCount < corePoolSize
创建线程,执行新任务。 - workerCount >= corePoolSize
&&
阻塞队列未满
将任务添加到阻塞队列中。 - workerCount >= corePoolSize
&&
workerCount < maximumPoolSize&&
线程池内的阻塞队列已满
创建并启动一个线程来执行新提交的任务。 - 如果workerCount >= maximumPoolSize
&&
线程池内的阻塞队列已满
根据拒绝策略来处理该任务, 默认是直接抛异常。
1)corePoolSize(核心线程数)
1> 工作机制
默认核心线程一直存活。
2> 参数调优
根据任务性质:
- CPU密集型(计算密集型):
系统的I/O读写效率高于CPU效率。
大部分的情况是CPU有许多运算需要处理,使用率很高。但I/O执行很快。
方案:CPU核心数+1。==》过多线程增加上下文切换的次数,带来额外开销。 - I/O密集型:
系统的CPU性能比磁盘读写效能要高很多,大多数情况是CPU在等I/O的读写操作,此时CPU的使用率并不高;
方案:2*CPU核心数。==》让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。 - 混合型任务:
既包含CPU密集型又包含I/O密集型。
方案:线程池的核心线程数=*(线程等待时间/线程CPU时间+1)CPU核心数;或者分成IO密集型和CPU密集型2个线程池去执行。
2)maximumPoolSize(最大线程数:用于业务繁忙期
1> 工作机制
当活动线程数达到此值,后续新任务将会被阻塞。
2> 参数调优
所以任务优先放入阻塞队列、而不是直接创建最大线程。
==》
创建新线程要获得全局锁,影响整体效率。使用队列可以避免频繁的创建和销毁线程,同时阻塞队列会释放cpu资源。
最大线程存在的意义:业务繁忙期使用。
方案:
IO密集型: 2N+1 (N为CPU数量,下同)
CPU密集型:最大线程设置为 N+1
3)keepAliveTime 非核心线程闲置时的超时时长。
超过此时长,非核心线程就会被回收。
当ThreadPoolExecutor的allowCoreThreadTimeOut = true
时,可作用于核心线程。
4)unit 用于指定keepAliveTime参数的时间单位
枚举:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
5)workQueue 等待队列
1>参数调优
- 尽量配置有界队列,在一些可能会有爆发性增长的情况下使用无界队列。
- 阻塞队列的选择:
使用非阻塞队列使用CAS 操作替代锁可以获得好的吞吐量。synchronousQueue 吞吐率最高。
如果是阻塞队列:
1)任务超过队列上限:超过的任务阻塞在此等待放入。
2)队列为空,尝试获取任务的线程会被阻塞(take方法挂起)。==》
线程进入wait状态,释放cpu资源。
3)阻塞队列自带阻塞和唤醒的功能,不需要额外处理。
6)threadFactory 线程工厂
为线程池提供创建新线程的功能。
ThreadFactory是一个接口,它只有一个方法:Thread newThread(Runnable r)。
7)RejectedExecutionHandler handler 拒绝策略
1>工作机制
默认直接抛异常。
当线程无法执行新任务时,ThreadPoolExecutor会调用handler的rejectedExecution方法来通知调用者。
2>拒绝策略
拒绝策略 | 说明 | 场景 |
---|---|---|
AbortPolicy(中止策略) | 默认,抛出异常并中止 | |
CallerRunsPolicy(调用者运行策略) | 调用者执行任务。 提供了简单的反馈机制,降低新任务提交的速度。可能会阻塞后续任务的执行,影响效率。 | 不允许失败场景(对性能要求不高、并发量较小) |
DiscardPolicy(丢弃策略) | 直接丢弃任务,无异常。可能丢失数据 | 无关紧要的任务 |
DiscardOldestPolicy(弃老策略) | 丢弃最老任务,尝试执行新任务。可能丢失数据 | 发布消息 |
4,原理
1)线程复用原理
每个线程执行一个循环任务,不停检测是否有任务需要执行。如果有则直接调线程的run方法(不是start方法)。
5,常用方法
ThreadPoolExecutor 类常用方法:
方法 | 说明 | 返回值 | 备注 |
---|---|---|---|
execute() | 启动线程池 | void | |
submit() | 启动线程池 | Future | 通过捕获Future.get捕获内部抛出的异常。 |
shutdown() | 停止接收新任务,原来的任务继续执行,最后关闭线程池 | ||
shutdownNow() | 停止接收新任务,原来的任务停止执行,并关闭线程池 | 取消所有位于阻塞队列中的任务,并将其放入List<Runnable> 容器,作为返回值。 | |
awaitTermination() | 当前线程阻塞并关闭线程池 | 若关闭返回true,否则返回false | 一般情况下会和shutdown方法组合使用。 |
1)优雅关闭线程池
service.shutdown();
while (!service.awaitTermination(1, TimeUnit.SECONDS)) {
System.out.println("线程池没有关闭");
}
但这样也不一定会关闭掉线程池,原因是有些代码里有try-catch,将线程中断作为异常捕获了,导致无法关闭线程池。修改try-catch如下图:
红框里是中断异常,将它捕获并抛出。
第二个catch是正常的try-catch,防止程序崩溃。
6,Demo
通过for循环创建线程,线程池处理线程:
涉及:ThreadPoolExecutor 、CountDownLatch、synchronized的使用。
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class UUIDTest {
@Autowired
private ThreadPoolExecutor threadPoolExecutor;
@Test
public void test(){
//线程安全的list
List<String> rets = new CopyOnWriteArrayList<>();
Integer countDown = 10;
CountDownLatch countDownLatch = new CountDownLatch(countDown.intValue());
try {
Object lock = new Object();
for (; countDown > 0; countDown--) {
Integer finalCountDown = countDown;
threadPoolExecutor.execute(() -> {
String msg = "";
try {
synchronized (lock) {
log.info("finalCountDown is {}", finalCountDown);
//todo: 数据准备
msg = "msg content is " + finalCountDown;
}
//todo:耗时操作
log.info("msg is {}", msg);
rets.add(msg);
} catch (Exception e) {
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
log.info("rets is {}", rets);
}
}
@Configuration
public class ThreadExecutorConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor() {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
4, 10, 1000, TimeUnit.MICROSECONDS,
new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
return threadPoolExecutor;
}
}