点赞关注,不再迷路,你的支持对我意义重大!
Hi,我是丑丑。本文 「Java 路线」| 导读 —— 他山之石,可以攻玉 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)
前言
- 线程池 是 Java 并发编程中非常重要的概念,同时也是面试重点考察的知识点之一「敲黑板」;
- 在这篇文章里,我将重点分析 线程池工作机制 & 注意事项。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
目录
1. 前置知识
这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~
阻塞队列: 「Java 路线」| 阻塞队列
线程协作机制:【点赞催更】
2. 线程池概述
2.1 为什么要使用线程池?
1、降低资源消耗: 线程是稀缺资源,如果无限制 / 重复创建,会消耗系统资源;
2、提高响应速度: 通过复用线程来执行任务,可以缩短创建和销毁线程的时间;
3、提高线程的可管理性: 使用线程池可以对线程进行统一分配、调优和监控。
2.2 线程池如何实现线程复用?
线程执行完任务之后,调用阻塞队列BlockingQueue#take()
出队操作,当阻塞队列非空时,则继续执行任务;当阻塞队列为空时,则当前队列阻塞。
2.3 提交任务
向线程池提交任务可以使用 execute() & submit()
,区别如下:
execute(): 用于不需要返回值的任务,无法感知任务执行完成;
submit(): 用于需要返回值的任务,通过返回值 Future 可以获得返回值,如果任务未执行完成,调用
Futrue#get(...)
会阻塞当前线程。
2.4 关闭线程池
shutdown() / shutdownNow()
线程中断协作机制
2.5 阿里巴巴编程规范
根据《阿里巴巴 Java 开发手册》,线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
3. 线程池相关类
Executor
ExecutorService
AbstractExecutorService
ThreadPoolExecutor
ScheduledExecutorService
ScheduledThreadPoolExecutor
4. 线程池参数
ThreadPoolExecutor.java
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
参数 | 描述 |
---|---|
1、int corePoolSize | 核心线程数 |
2、int maximunPoolSize | 最大线程数 |
3、long keepAliveTime | 线程空闲最大存活时间 |
4、BlockingQueue workQueue | 阻塞队列 |
5、ThreadFactory threadFactory | 线程工厂 |
6、RejectedExecutionHandler handler | 拒绝策略 |
4.1 int corePoolSize(核心线程数)
如果当前线程数等于 corePoolSize,继续提交任务将进入阻塞队列中等待。调用 prestartAllCoreThreads()
可以提前启动所有核心线程;
4.2 int maximunPoolSize(最大线程数)
如果阻塞队列满,继续提交任务将创建新线程,最多可以存在 maximunPoolSize 个线程;
4.3 long keepAliveTime(线程空闲最大存活时间)
在线程池空闲时线程继续存活的时间。注意:keepAliveTime 只有线程数大于 corePoolSize 才有效;
4.4 BlockingQueue(阻塞队列)
如果当前线程数等于 corePoolSize,继续提交任务将进入阻塞队列中等待。线程池中的阻塞队列应尽量使用有界队列,使用无界队列会导致影响线程池的工作机制,原因:
- 1、使用无界队列,意味着阻塞队列永远不会占满,maximunPoolSize 和 keepAliveTime 是无效的;
- 2、无界队列有可能造成系统资源耗尽,同时即使使用有界队列,也要尽量控制在合理范围内。
4.5 ThreadFactory threadFactory(线程工厂)
用于获取 Thread 对象的实例,Executors 中默认的线程工厂的线程命名规则为:pool-「线程池计数」-thread-「线程计数」
Executors.java
namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
4.6 RejectedExecutionHandler(拒绝策略 )
如果阻塞队列满,且线程数到达最大值 maximunPoolSize,继续提交任务则会触发拒绝策略 RejectedExecutionHandler#rejectedExecution(...)
:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
线程池提供了以下 4 种拒绝策略:
拒绝策略 | 描述 |
---|---|
AbortPolicy | 抛出 RejectedExecutionException 异常(默认) |
CallerRunsPolicy | 直接在调用线程执行 |
DiscardOldestPolicy | 丢弃阻塞队列中队首的任务 |
DiscardPolicy | 直接丢弃当前任务 |
5. 线程池工作机制
线程池的工作机制指的是向线程池提交任务时,线程池内部对任务的调度流程。这部分内容是线程池最核心的内容,也是面试重点。
- 1、如果当前运行的线程少于 corePoolSize,则「创建新的线程」来执行任务(注意,执行这一步骤需要获取全局锁);
- 2、如果运行的线程等于或多于 corePoolSize,则将任务「加入 BlockingQueue」;
- 3、如果 BlockingQueue 已满,则「创建新的线程」来处理任务;
- 4、如果创建新线程将使当前运行的线程超出 maximumPoolSize,将「触发拒绝策略」并调用RejectedExecutionHandler#rejectedExecution()方法。
为什么先将任务加入阻塞队列,而不是线程池满了再加入阻塞队列?
Editting...
6. 如何合理配置线程池?
线程池的配置需要根据「任务特性」选择不同的任务配置,因地制宜。主要从以下角度分析:
6.1 性质
任务的性质分为:CPU 密集型、IO 密集型和混合型。
对于 CPU 密集型任务,CPU 负载已经非常高了,应配置尽可能小的线程,经验值为 Ncpu + 1(调用 Runtime.getRuntime().availableProcessors() 获得可用的核心数。);
对于 IO 密集型任务(如 磁盘 / 网络 IO),磁盘或网络的读取速度是远远小于 CPU 执行速度的,为了避免 CPU 出现空闲,应配置较多的线程,经验值为 Ncpu * 2;
对于混合型任务,则将其拆分为一个 CPU 密集型任务和 IO 密集型任务,分别到上述两种线程池执行。需要注意的是,如果两种拆分的两个任务执行时间相差太大,则应该视为一种非混合型任务。
为什么 CPU 密集型线程池经验值为 Ncpu + 1?
在操作系统中,会将磁盘的一部分空间划分为虚拟内存(读写速度慢),当 CPU 需要访问的数据在虚拟内存上时,当前线程就进入了 “页缺失” 状态,需要等待数据从磁盘调度到真实内存才会唤醒。为了防止出现 “页缺失” 时,CPU 空闲出来,保证任意时刻 CPU 都不会空闲,可以选择 + 1;
6.2 优先级
需要区分任务优先级,则使用 PriorityBlockingQueue;
6.3 执行耗时
执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行;
6.4 建议使用有界队列
无界队列没有限制队列元素个数,有可能有造成资源耗尽。
7. 总结
创作不易,你的「三连」是丑丑最大的动力,我们下次见!