目录
- 阻塞队列
- 线程池
- 周期性线程池
- 如何去查看一个线程池是一个合理的状态去优化线程池。
一、阻塞队列
1. 基本概念
- 队列(Queue) :一种 先进先出(FIFO) 的数据结构,元素从队尾添加(入队),从队首移除(出队)。
- 阻塞(Blocking) :当线程尝试操作队列时,若条件不满足(如队列为空或满),线程会被 挂起(阻塞) ,直到条件满足才唤醒。
2. 阻塞队列是什么?
- 定义:一种支持阻塞操作的 线程安全队列,提供以下特性:
-
- 队列为空时:消费者线程尝试取数据会被阻塞,直到队列非空。
- 队列已满时:生产者线程尝试存数据会被阻塞,直到队列有空位。
- 核心接口:Java 中的
BlockingQueue接口。
3. 为什么线程池中使用阻塞队列,而不是其他的呢?
- 协调速率:当队列满时,生产者线程被阻塞,避免无限制提交任务导致 OOM;当队列空时,消费者线程被阻塞,避免无意义轮询浪费 CPU。
- 线程安全设计:阻塞队列内部已实现同步机制(如
ReentrantLock+Condition),无需手动加锁。
普通队列的(非线程安全)需自行处理线程同步和阻塞逻辑,复杂易出错 。
4.常用的阻塞队列
| 场景 | 推荐队列 | 理由 |
|---|---|---|
| 内存敏感型任务(如低端设备) | ArrayBlockingQueue | 有界队列避免内存溢出,严格限制任务堆积。 |
| 通用任务缓冲(默认推荐) | LinkedBlockingQueue | 无界队列简化管理,适合任务量可控的场景。 |
| 高吞吐量、短任务 | SynchronousQueue | 直接传递任务,避免缓冲开销,最大化线程利用率。 |
| 按优先级处理任务 | PriorityBlockingQueue | 支持自定义优先级,适合需要分级处理的场景(如 VIP 用户请求优先)。 |
| 定时或延迟任务 | DelayQueue | 按延迟时间调度任务,适合心跳检测、倒计时等场景。 |
有界:队列长度有限制,满了以后,就会阻塞,添加不了。
无界:队列长度没限制,添加不会阻塞,但是,当队列为空时,消费者线程会被阻塞。
接下来,我们先看看线程池,然后在看看阻塞队列和线程池的一起使用。
5.举例使用
BlockingQueue queue = new ArrayBlockingQueue<>(10); // 有界队列,容量10
// 生产者线程
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
queue.put(i); // 队列满时阻塞
System.out.println("duilie 生产: " + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// 消费者线程
new Thread(() -> {
try {
while (true) {
Object value = queue.take(); // 队列空时阻塞
System.out.println("duilie 消费: " + value);
Thread.sleep(2000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
二、线程池
线程池是一种线程管理机制,通过预先创建一组线程并复用它们来执行多个任务,避免频繁创建和销毁线程的开销。其核心目标是 提升系统资源利用率 和 降低任务执行延迟。
2.1 目的
- 缩短任务总执行时间:
-
- 减少线程创建/销毁开销:线程的创建和销毁涉及系统调用和资源分配,复用线程可大幅减少这类开销。
- 降低任务调度延迟:任务到达时,线程池中已有可用线程,无需等待新线程创建。
- 线程是稀缺而昂贵的资源:
-
- 内存占用:每个线程需分配栈内存(默认1MB),线程过多易导致内存耗尽。
- 上下文切换开销:线程数超过CPU核心数时,频繁切换线程会降低CPU利用率。
- 系统限制:操作系统对线程数有上限(如Linux默认为1024),超出会导致错误。
2.2 线程池如何使用?
我们直接来个例子:
// 创建自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60, // 空闲线程存活时间(秒)
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10), // 任务队列容量10
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:抛异常
);
try {
// 提交15个任务(测试拒绝策略)
for (int i = 0; i < 15; i++) {
final int taskId = i;
executor.execute(() -> {
try {
Log.d(TAG, "onCreate: "+"完成任务: " + taskId + ",线程: " + Thread.currentThread().getName());
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} finally {
executor.shutdown();
}
执行流程:
- 当调用
executor.execute()提交任务时,任务0和任务1会立即执行,因为我们的核心线程数是2,所以两个任务立马得到执行,而不是放到队列里面在进行阻塞。 - 当核心线程已满,任务会进入队列等待,比如2~11任务就会进入队列阻塞,那么这个时候队列就满了。
- 此时,如果核心线程还在处理任务,并且队列也满了,然后你还在提交任务,那么就会触发创建临时线程(最大线程数4 - 核心线程2 = 2个临时线程),那么此时,就会有4个线程在运行。
- 如果说已满足线程数最大值4,队列已满10个任务,如果还添加任务,那么就会根据我们设置的拒绝策略,就会抛出异常。
- 核心线程
thread-1和thread-2会复用。临时线程thread-3和thread-4在处理完任务后,60秒内可能被复用
2.3 核心组件:
- ThreadPoolExecutor:最常用的线程池实现,支持精细参数配置:
corePoolSize:核心线程数(常驻线程),线程池长期保留的线程数量,即使处于空闲状态。maximumPoolSize:最大线程数(临时线程)keepAliveTime:空闲线程存活时间workQueue:任务队列(如LinkedBlockingQueue)RejectedExecutionHandler:拒绝策略
- 往线程池里面提交任务有两种方法:
execute(): 提交不需要返回值的Runnable任务。submit(): 提交需要返回值的Callable任务。
- 关闭线程池的两种方式
1. shutdown():
-
停止接收新任务。
-
等待已提交的任务(包括队列中的任务)执行完成。
-
不强制中断正在运行的任务。
-
适用场景:希望所有已提交任务正常完成后再关闭。
2. shutdownNow():
-
立即停止接收新任务。
-
尝试中断所有正在执行的任务(通过
Thread.interrupt())。 -
清空任务队列,返回未执行的任务列表。
-
是否真正中断取决于任务是否响应中断。
-
适用场景:需要尽快释放资源,允许部分任务未完成。
executor.execute(() -> {
while (true) {
// 未检查中断状态,即使调用shutdownNow()也无法停止
System.out.println("无法停止的任务");
}
});
需要改写成如下这种
executor.execute(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// 模拟工作(每次循环检查中断)
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("工作中...");
} catch (InterruptedException e) {
// 捕获中断异常后,需再次设置中断标志(或退出循环)
Thread.currentThread().interrupt(); // 重新设置中断标志
System.out.println("任务被中断");
break;
}
}
});
2.4 如何合理配置线程池?
比如我们一个app,可能有很多地方使用到了线程,比如AFragment、BFragment,我们应该如何创建线程池呢?
避免为每个模块单独创建线程池(防止资源浪费)全App共享1~3个全局线程池(如CPU池、IO池)
-
线程数计算公式
- CPU密集型:
线程数 = CPU核心数 + 1
(减少上下文切换,最大化利用CPU) - IO密集型:
线程数 = CPU核心数 * 2
(更多线程应对阻塞等待)
- CPU密集型:
① CPU密集型任务
- 特征:
-
- 线程几乎不阻塞(无网络/文件操作)。
- 典型场景:
-
- 视频转码:大量数学运算压缩视频。
- 科学计算:如物理模拟、数据加密。
- 图像处理:滤镜渲染、3D建模。
- 代码示例:
// 计算斐波那契数列(纯CPU计算)
public class CpuTask {
public static long fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n-1) + fibonacci(n-2);
}
}
② IO密集型任务
- 特征:
-
- CPU 使用率低(通常低于50%)。
- 线程频繁阻塞(等待网络、磁盘、数据库响应)。
- 任务执行时间长(秒级或更长)。
- 典型场景:
-
- Web服务器:处理 HTTP 请求,等待数据库返回结果。
- 文件上传/下载:网络带宽或磁盘速度是瓶颈。
- 消息队列消费:等待消息到达或处理外部API调用。
- 代码示例:
// 从数据库查询用户信息(IO等待)
public class IoTask {
public User getUserById(int id) {
return jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", User.class, id);
}
}
③ 混合型任务
- 特征:
-
- CPU 和 IO 交替占用:先计算再等待IO,或反过来。
- 资源使用波动大:CPU 和 IO 使用率均有峰值。
- 典型场景:
-
- 数据处理流水线:读取文件(IO)→ 清洗数据(CPU)→ 写入数据库(IO)。
- 实时推荐系统:计算用户偏好(CPU)→ 调用推荐算法服务(网络IO)。
- 游戏服务器:物理引擎计算(CPU)→ 保存玩家状态到磁盘(IO)。
- 代码示例:
// 处理订单:验证数据(CPU)→ 调用支付接口(IO)→ 生成报表(CPU)
public class MixedTask {
public void processOrder(Order order) {
validate(order); // CPU计算
callPaymentGateway(order); // 网络IO
generateReport(order); // CPU计算
}
}
2.5如何创建CPU、IO或者混合密集型的线程池呢?
线程池其实都一样的,只不过配置的参数调用的方法不一样。
一、CPU密集型线程池
适用场景:数学计算、图像处理、数据加密等需要大量CPU运算的任务。
配置要点:
- 线程数 ≈ CPU核心数 + 1
- 使用有界队列防止资源耗尽
- 拒绝策略建议
AbortPolicy(直接拒绝新任务)
public class CpuThreadPool {
// 获取CPU核心数(Android设备通常4~8核)
private static final int CPU_CORES = Runtime.getRuntime().availableProcessors();
// CPU密集型线程池
private static final ThreadPoolExecutor cpuExecutor = new ThreadPoolExecutor(
CPU_CORES + 1, // 核心线程数
CPU_CORES + 1, // 最大线程数(与核心线程数相同)
30L, TimeUnit.SECONDS, // 非核心线程空闲存活时间(此处无效)
new ArrayBlockingQueue<>(100), // 有界队列,容量100
new CustomThreadFactory("CPU-Pool"), // 自定义线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
// 获取线程池实例
public static ThreadPoolExecutor getExecutor() {
return cpuExecutor;
}
}
CPU密集型任务为什么要用有界队列?执行时间稳定,几乎不会阻塞。如果使用无界队列(如LinkedBlockingQueue无参构造),当任务提交速度 > 处理速度时,队列会无限增长。使用有界队列(如ArrayBlockingQueue(100)),当队列满时触发拒绝策略,保护系统稳定性。
二、IO密集型线程池
适用场景:网络请求、文件读写、数据库操作等存在阻塞等待的任务。
配置要点:
- 线程数 ≈ CPU核心数 * 2
- 使用无界队列或同步队列
- 拒绝策略建议
CallerRunsPolicy(由调用线程执行)
public class IoThreadPool {
private static final int CPU_CORES = Runtime.getRuntime().availableProcessors();
// IO密集型线程池
private static final ThreadPoolExecutor ioExecutor = new ThreadPoolExecutor(
CPU_CORES * 2, // 核心线程数
CPU_CORES * 2, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(), // 无界队列(默认容量Integer.MAX_VALUE)
new CustomThreadFactory("IO-Pool"),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
public static ThreadPoolExecutor getExecutor() {
return ioExecutor;
}
}
IO密集型任务为什么用无界队列?大部分时间在等待IO响应,线程实际占用CPU时间短。使用无界队列允许更多任务排队,利用线程等待IO的空闲时间处理其他任务。
三、混合型线程池
适用场景:任务中同时包含CPU计算和IO操作(如先下载文件再解析)。
配置要点:
- 核心线程数按CPU密集型设置
- 最大线程数适当扩大
- 使用优先级队列区分任务重要性
public class HybridThreadPool {
private static final int CPU_CORES = Runtime.getRuntime().availableProcessors();
// 混合型线程池(带任务优先级)
private static final ThreadPoolExecutor hybridExecutor = new ThreadPoolExecutor(
CPU_CORES + 1, // 核心线程数
CPU_CORES * 2, // 最大线程数
45L, TimeUnit.SECONDS,
new PriorityBlockingQueue<>(100), // 优先级队列
new CustomThreadFactory("Hybrid-Pool"),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
}
四、线程池类型选择策略
| 场景判断方法 | 选择类型 | 配置示例 |
|---|---|---|
| 任务中90%时间在计算 | CPU密集型 | 线程数=CPU+1,队列容量100 |
| 任务中60%时间在等待网络响应 | IO密集型 | 线程数=CPU*2,无界队列 |
| 计算与IO时间接近(如40% CPU计算) | 混合型 | 核心线程=CPU+1,最大线程=CPU*2,优先级队列 |
| 线程池类型 | 核心线程数建议 | 典型模块 |
|---|---|---|
| 网络请求池 | CPU核心数 * 2 | Retrofit/OkHttp异步请求 |
| 图像处理池 | CPU核心数+1 | Bitmap解码、滤镜处理 |
| 设备CPU核心数 | 总核心线程数范围 | 典型分配方案 |
|---|---|---|
| 4核 | 12 ~ 20 | CPU池(5) + IO池(8) + BG池(2) = 15 |
| 8核 | 24 ~ 40 | CPU池(9) + IO池(16) + BG池(4) = 29 |
三、案例:ScheduledThreadPoolExecutor
Android 智能家居开发,串口读写数据,会涉及到频繁的数据读写,那么我们应该使用IO密集型,还是CPU呢?
-
数据发送:将字节流写入串口缓冲区(由硬件处理实际传输)写入缓冲区后由硬件处理,无需持续占用CPU
-
数据接收:轮询或中断方式读取缓冲区(可能有阻塞等待)线程可能在
read()时挂起等待数据,类似网络IO等待
建议: 优先选择IO密集型线程池,次选混合型(因为也有可能有数据处理(如CRC校验)若校验逻辑复杂(如加密解密),则需要)。
我们读数据,一般是间隔个几秒或者几十秒,就需要读取一次数据,这里我们需要是又有定时执行的线程池,比如ScheduledThreadPoolExecutor
3.1 是什么?
ScheduledThreadPoolExecutor 是 Java 并发包中专门为定时任务和周期性任务设计的线程池,解决了以下传统方案的痛点:
| 传统方案 | 痛点 |
|---|---|
Timer | 单线程执行任务,一个任务阻塞会导致所有后续任务延迟 |
Thread.sleep() | 需手动管理线程生命周期,无法复用线程资源 |
| 普通线程池+循环提交 | 无法精确控制任务触发时间,代码复杂度高 |
3.2 如何使用
(1)创建实例
// 创建核心线程数为3的定时线程池
ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(3);
// (关闭时清理未执行任务)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scheduled.setRemoveOnCancelPolicy(true);
}
scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
(2)固定频率任务
// 初始延迟0秒,之后每10秒执行一次(无视任务耗时)
scheduler.scheduleAtFixedRate(() -> {
System.out.println("固定频率任务开始: " + System.currentTimeMillis());
try {
Thread.sleep(3000); // 模拟任务耗时3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 0, 10, TimeUnit.SECONDS);
(3)固定间隔任务
// 每次任务结束后间隔10秒再执行下一次
scheduler.scheduleWithFixedDelay(() -> {
System.out.println("固定间隔任务开始: " + System.currentTimeMillis());
try {
Thread.sleep(3000); // 模拟任务耗时3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 0, 10, TimeUnit.SECONDS);
-
不要用
scheduleAtFixedRate执行可能超时的任务 -
推荐使用
scheduleWithFixedDelay处理不确定耗时的任务
(4)关闭线程池
// 平缓关闭(等待正在执行的任务完成)
scheduler.shutdown();
// 强制关闭(立即终止所有任务)
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
shutdown:仅允许已开始的周期任务完成当前周期,不会中断,允许任务完成,但会丢弃未到期的延迟任务。而 ThreadPoolExecutor的shutdown是会执行完队列中所有任务。
shutdownNow:适用于紧急终止场景,强制中断所有任务并清空队列,允许任务未完成,但需任务代码配合响应中断。
ScheduledThreadPoolExecutor 默认使用的是 AbortPolicy 拒绝策略。具体行为如下:
-
默认策略:
继承自ThreadPoolExecutor的默认拒绝策略AbortPolicy,当任务无法被接受时抛出RejectedExecutionException。 -
触发条件:
由于ScheduledThreadPoolExecutor使用无界的DelayedWorkQueue(队列容量实际为Integer.MAX_VALUE),在正常情况下队列不会满,因此拒绝策略仅在以下场景触发:- 线程池已关闭(
shutdown或shutdownNow)时提交新任务。 - 系统资源耗尽(如内存不足导致无法创建新线程或入队任务)。
- 线程池已关闭(
四、如何去查看线程池是一个合理的状态
通过以下关键指标判断线程池是否健康,并识别潜在问题:
| 指标 | 获取方法 | 健康状态参考值 |
|---|---|---|
| 活跃线程数 | executor.getActiveCount() | 长期接近最大线程数 → 可能需扩容 |
| 队列大小 | executor.getQueue().size() | 持续超过队列容量80% → 需调整队列容量或拒绝策略 |
| 已完成任务数 | executor.getCompletedTaskCount() | 与提交任务数对比,差值过大 → 可能存在任务堆积或拒绝 |
| 拒绝任务数 | 自定义计数器(如AtomicLong rejectedTasks) | 拒绝次数 > 0 → 需优化任务提交频率或调整线程池容量 |
| 任务平均执行时间 | 任务内记录时间差 → 统计平均值 | 执行时间远大于预期 → 需优化任务逻辑或拆分任务 |
| 最大线程存活时间 | executor.getKeepAliveTime(TimeUnit) | 临时线程存活时间应与任务波动周期匹配 |
4.1、监控方法与工具
1. 内置API监控(代码级)
// 定时打印线程池状态(每30秒)
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleAtFixedRate(() -> {
System.out.println(
"活跃线程: " + executor.getActiveCount() +
" 队列大小: " + executor.getQueue().size() +
" 完成数: " + executor.getCompletedTaskCount() +
" 拒绝数: " + rejectedTasks.get()
);
}, 0, 30, TimeUnit.SECONDS); // 每30秒检查一次
2. 参数配置评估指标
| 参数 | 不合理表现 | 优化方向 |
|---|---|---|
| corePoolSize | 活跃线程数长期 < corePoolSize → 资源浪费 | 减少核心线程数 |
| maximumPoolSize | 频繁创建临时线程 → 线程数常达最大值 | 增大maximumPoolSize 或优化任务逻辑 |
| workQueue | 队列持续满载 → 任务响应延迟 | 增大队列容量 或 使用有界队列+合理拒绝策略 |
| rejectedPolicy | 大量任务被拒绝 → 业务数据丢失 | 改用CallerRunsPolicy 或 增加降级逻辑 |
2. 任务类型与参数调整
| 任务类型 | 推荐配置 |
|---|---|
| CPU密集型 | - 核心线程数 = CPU核心数 + 1 - 使用有界队列(如ArrayBlockingQueue) |
| IO密集型 | - 核心线程数 = CPU核心数 * 2 - 使用无界队列(如LinkedBlockingQueue) |
| 混合型任务 | - 拆分任务到不同线程池 - 核心线程数按主要任务类型配置 |
3. Android Profiler(可视化工具)【AI分享】
通过 Android Studio 的 Profiler 实时分析线程状态:
-
CPU Profiler:
- 查看线程池工作线程的CPU占用率
- 定位高耗时任务(火焰图)
-
Memory Profiler:
- 检测线程泄漏(线程数量持续增长)
-
Network Profiler:
- 结合网络请求分析IO密集型线程池的负载
4. 第三方库辅助
-
BlockCanary:检测UI卡顿,定位线程池任务阻塞主线程
-
implementation 'com.github.markzhai:blockcanary-android:1.5.0' -
LeakCanary:检测线程泄漏
-
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'