大家好,我是程序员强子。
又来刷英雄熟练度咯~今天专攻 Java 并发工具~
之前聊并发关键字时,很多实战中高频使用的并发工具只是简单带过,没深挖它们的底层关联和实战技巧,今天就来系统突破,逐个拆解透彻!
我们来看一下,今晚我们准备练习哪些内容:
- LockSupport,AQS 的核心机制、数据结构、模式实现及自定义同步器要点
- ReentrantLock与 ReentrantReadWriteLock 的特性、实现原理及锁相关机制
- 同步工具(CountDownLatch、CyclicBarrier、Semaphore)与 BlockingQueue的核心特性及用法
- 线程池的核心参数、工作流程、常见实现、拒绝策略及关闭机制
今天的内容干而多,系好安全带,我们发车啦~
LockSupport
在 Java 里,要让线程 暂停 或 恢复,最先想到的可能是Object.wait()/notify()
但 LockSupport 才是并发工具的 底层依赖 , AQS、ReentrantLock 等都靠它实现线程阻塞唤醒
作用
精准控制单个线程的阻塞(park)和唤醒(unpark)
它就像给线程发了一张 许可, 有许可就能继续跑,没许可就阻塞
对比
和Object.wait()/notify()比,它解决了很多痛点:
| 对比维度 | LockSupport | Object.wait()/notify() |
|---|---|---|
| 调用前提 | 无需持有锁 | 必须在synchronized块内 |
| 唤醒目标 | 精准唤醒指定线程 | 随机唤醒一个 等待线程(notify ()) 或唤醒全部(notifyAll ()) |
| 阻塞状态解除 | 1. unpark () 唤醒 2. 线程中断 3. 超时 | 1. notify ()/notifyAll () 2. 线程中断 3. 超时 |
| 灵活性 | 支持 先唤醒后阻塞 | 必须先 wait () 再 notify (),否则唤醒无效 |
案例
public class LockSupportDemo {
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
System.out.println("线程A:我要阻塞等许可了");
LockSupport.park(); // 没有许可,阻塞
System.out.println("线程A:拿到许可,继续执行");
}, "ThreadA");
threadA.start();
Thread.sleep(1000); // 等A先阻塞
Thread threadB = new Thread(() -> {
System.out.println("线程B:我给A发许可");
LockSupport.unpark(threadA); // 给A发许可
}, "ThreadB");
threadB.start();
}
}
// 输出:
// 线程A:我要阻塞等许可了
// 线程B:我给A发许可
// 线程A:拿到许可,继续执行
如果换成wait()/notify(),必须套synchronized,还没法精准唤醒 A,只能唤醒随机线程
底层实现
LockSupport 的底层依赖 JVM 的Unsafe类(封装了操作系统的线程操作)
核心是 许可机制:
-
每个线程都有一个 许可槽
- 槽里只有两种状态,槽里只有两种状态:0(无许可)、1(有许可),不能累加。
- 类似数据库表的字段,这个字段只有 0 或者 1两种值,更新的时候是set 字段,所以就算10次set为1,结果还是1,不会累加
-
park()方法:先检查许可槽
- 如果有许可(1),就消耗掉许可(变 0),继续执行;
- 如果没许可(0),就阻塞当前线程,直到拿到许可
-
unpark(Thread t)方法:给指定线程的许可槽 有条件的set值=1
- 如果槽里是 0,就变成 1;
- 如果已经是 1,啥也不做
为什么用许可机制?
因为它能解决 唤醒丢失问题
比如先调用 unpark,再调用 park,线程也能正常执行,许可预先发放
这是wait()/notify()做不到的,先 notify 再 wait 会永远阻塞
park () 被中断后会抛异常吗?为什么?
不会抛异常,但会立即返回。
因为park()的定位是 线程阻塞工具,中断只是 解除阻塞的一个原因,而非 错误
如果抛异常,会强制我们处理 try/catch,反而增加代码复杂度
public class LockSupportInterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
System.out.println("线程A:开始park");
LockSupport.park(); // 被中断后返回,不抛异常
// 检查是否被中断
if (Thread.interrupted()) {
System.out.println("线程A:被中断了,退出");
return;
}
System.out.println("线程A:正常执行完");
}, "ThreadA");
threadA.start();
Thread.sleep(500);
threadA.interrupt(); // 中断线程A
}
}
// 输出:
// 线程A:开始park
// 线程A:被中断了,退出
AQS
定义
有了 LockSupport,我们就能控制线程的阻塞唤醒
但 什么时候阻塞、什么时候唤醒、如何管理等待线程” 这些逻辑,需要一个统一的框架来封装 , 这就是AQS(AbstractQueuedSynchronizer,抽象队列同步器)
AQS 的核心思想是 模板方法模式
它定义了一套同步逻辑的骨架(比如线程入队、阻塞、唤醒),把具体的 锁获取 / 释放 逻辑留给子类实现(比如 ReentrantLock、CountDownLatch)
AQS核心作用
- 封装 线程排队 和 阻塞唤醒 的通用逻辑,让子类不用重复写这些底层代码;
- 提供state变量(同步状态),让子类通过修改state来控制锁的获取 / 释放。
AQS底层数据结构
-
state 变量
- volatile 修饰的 int 值,是 AQS 的 状态核心
- 子类通过它定义同步规则(比如:state=0 表示无锁,state>0 表示有锁且记录重入次数);
-
CLH 同步队列
- 双向链表结构,用来存放 获取锁失败的线程(每个节点对应一个等待线程),
- 链表的头节点是 当前持有锁的线程,后续节点是等待线程。
state变量在子类中有什么用?CLH的数据结构是怎么样的呢?接着往下看~
state变量
state 是 AQS 的 灵魂,它的含义由子类定义
-
ReentrantLock
- 锁的重入次数
- state=0(无锁),state=2(当前线程重入 2 次)
-
CountDownLatch
- 倒计时计数器
- state=3(需要 3 个线程调用 countDown ())
-
ReentrantReadWriteLock
- 读写锁状态(高 16 位读计数,低 16 位写计数)
- state=0x00010000(1 个读锁),state=0x00000002(2 次写锁重入)
-
...
state 的修改必须是原子操作,AQS 提供了getState()、setState()、compareAndSetState()(CAS)方法来保证线程安全
CLH数据结构
CLH 队列是 AQS 管理等待线程的 容器,结构是双向链表,每个节点是Node类的实例,包含以下核心属性:
-
thread:当前等待的线程;
-
prev/next:前驱 / 后继节点,构成双向链表;
-
waitStatus:节点状态,决定节点的行为(比如是否要被唤醒、是否已取消)
- CANCELLED(值为 1):线程已取消,无效状态
- SIGNAL(值为 - 1): 唤醒下一个,只唤醒一个
- CONDITION(值为 - 2):「我在条件队列里等,别在同步队列找我」
- PROPAGATE(值为 - 3):共享模式下,唤醒要一传十,唤醒一个接一个,唤醒一批
- 0:初始状态
核心方法流程
独占模式下 acquire 流程
-
尝试获取锁(tryAcquire (arg)):
- 调用子类重写的tryAcquire()方法(比如 ReentrantLock 会判断 state 是否为 0,并用 CAS 修改 state);
- 如果成功(返回 true),直接返回,当前线程持有锁;
- 如果失败(返回 false),进入下一步。
-
入队(addWaiter(Node.EXCLUSIVE)):
- 创建一个 独占模式 的 Node 节点,把当前线程封装进去;
- 通过CAS把节点加入 CLH 队列的尾部;
-
阻塞线程(acquireQueued(node, arg)):
- 节点入队后,检查前驱节点是否是头节点(如果是,可能还有机会获取锁);
- 如果不是头节点,或者再次尝试获取锁失败,就调用LockSupport.park()阻塞当前线程;
- 线程被唤醒后(unpark 或中断),会再次尝试获取锁,直到成功。
一句话总结:先抢锁,抢不到就排队,排好队就睡觉,被叫醒了再抢,直到抢到为止。
共享模式下 releaseShared流程
-
尝试释放锁(tryReleaseShared (arg)):
- 调用子类重写的tryReleaseShared()方法(比如 CountDownLatch 会用 CAS 把 state 减 1,直到 state=0);
- 如果释放成功(返回 true,比如 CountDownLatch 的 state 减到 0),进入下一步;
- 如果失败(返回 false),直接返回。
-
唤醒并传播(doReleaseShared ()):
- 唤醒 CLH 队列的头节点的后继节点(用 LockSupport.unpark ());
- 被唤醒的节点获取锁后,会继续调用doReleaseShared() ,唤醒它的后继节点 —— 直到所有等待节点都被唤醒(这就是 传播)。
核心钩子方法
-
独占
-
tryAcquire(int arg)
- 尝试获取独占锁
- true = 成功,false = 失败
-
tryRelease(int arg)
- 尝试释放独占锁
- true = 成功(state 已恢复到可竞争状态),false = 失败
-
-
共享
-
tryAcquireShared(int arg)
- 尝试获取共享锁
- 负数 = 失败;0 = 成功,但后续线程不能获取;
- 正数 = 成功,且后续线程也能获取(需传播)
-
tryReleaseShared(int arg)
- 尝试释放共享锁
- true = 释放成功(state 已到最终状态,需唤醒后续),false = 释放未完成
-
基于 AQS 实现自定义同步器
AQS 是抽象类,子类只需重写 获取 / 释放锁 的核心逻辑(模板方法的 钩子),不用关心队列和阻塞细节。
核心步骤
- 定义同步器类,继承AbstractQueuedSynchronizer;
- 根据需求(独占 / 共享)重写对应的方法;
- 对外提供 API,调用 AQS 的模板方法(比如 acquire、release)。
需重写的方法(按需选择):
-
独占模式
- tryAcquire(int arg):尝试获取独占锁,返回 true/false
- tryRelease(int arg):尝试释放独占锁,返回 true/false
- isHeldExclusively():判断当前线程是否持有独占锁(可选)
-
共享模式
- tryAcquireShared(int arg):尝试获取共享锁,返回负数(失败)/ 非负数(成功)
- tryReleaseShared(int arg):尝试释放共享锁,返回 true/false
ReentrantLock
ReentrantLock 是 AQS 独占模式的 经典实现,支持可重入、公平 / 非公平锁、条件等待等特性
可重入
什么是可重入?
同一个线程可以多次获取同一把锁,不会自己阻塞自己
ReentrantLock 的可重入实现原理:
-
state 变量计数
- 每次lock()时,若当前线程已持有锁,就把 state+1;
- 每次unlock()时,state-1,直到 state=0 才真正释放锁;
-
exclusiveOwnerThread 记录线程
- AQS 的exclusiveOwnerThread字段记录当前持有锁的线程
- tryAcquire()时会判断 当前线程是否是持有锁的线程,是则允许重入。
synchronized 的可重入区别有哪些区别?
| 对比维度 | ReentrantLock | synchronized |
|---|---|---|
| 实现层面 | AQS 层面(Java 代码) | JVM 层面(C++ 实现) |
| 重入计数方式 | state 变量显式计数 | 线程栈帧的 锁记录 隐式计数 |
| 灵活性 | 可通过代码获取重入次数 getHoldCount () | 无法直接获取重入次数 |
| 释放方式 | 必须手动 unlock () | 自动释放(代码块结束或异常) |
如何判断当前线程是否持有锁?
ReentrantLock 提供了isHeldByCurrentThread()方法
底层就是检查 AQS 的exclusiveOwnerThread是否等于当前线程
公平与非公平
ReentrantLock 的公平锁和非公平锁区别?性能差异原因?
核心区别:tryAcquire时是否 遵守排队顺序
-
公平锁:
- 获取锁前,先检查 CLH 队列;
- 查看是否有 前驱节点,如果有,就不抢锁,直接入队;
-
非公平锁:获取锁前,不检查队列,直接尝试 CAS 抢锁(即使队列有等待线程),抢不到再入队。
非公平锁性能更高,核心原因是减少线程切换
Condition
概念
ReentrantLock 的 Condition 如何实现等待 / 通知?
Condition 是 ReentrantLock 提供的 条件等待 工具,相当于给锁增加了 多个等待队列,解决了Object.wait()只有一个等待队列的痛点。
Condition 不能通过 new 关键字直接实例化,必须通过 Lock.newCondition() 方法创建。Condition 操作必须在 Lock 保护下进行;
可以根据业务新建不同的condition来满足需求
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition(); // Condition 由 Lock 创建
Lock lock = new ReentrantLock();
Condition condition2 = lock.newCondition(); // 多个条件,根据需求
核心原理
Condition 的核心是条件队列, 独立于 CLH 同步队列的单向链表,用于存放 因特定条件不满足 而等待的线程
- 每个 Condition 对应一个 条件队列(也是 Node 链表,节点状态为 CONDITION);
- Condition.await():当前线程释放锁,进入条件队列并阻塞;
- Condition.signal():从条件队列中取出一个节点,移到 CLH 同步队列,等待获取锁;
- Condition.signalAll():把条件队列的所有节点移到同步队列。
和 Object.wait/notify 的区别?
| 对比维度 | ReentrantLock.Condition | Object.wait()/notify() |
|---|---|---|
| 等待队列数量 | 一个锁可创建多个条件队列 | 一个对象只有一个等待队列 |
| 唤醒目标 | 可唤醒指定条件队列的线程 | 随机唤醒或全部唤醒 |
| 调用前提 | 必须持有 ReentrantLock 锁 | 必须持有对象的 synchronized 锁 |
| 灵活性 | 支持超时、中断、可中断等待 | 支持超时和中断,但功能单一 |
ReentrantReadWriteLock
概念
包含两个锁:读锁(共享锁) 和写锁(独占锁) ,能根据操作类型(读 / 写)灵活控制线程访问
- 读锁:多个线程可以同时持有(共享),适合查询操作;
- 写锁:同一时间只能有一个线程持有(独占),适合修改操作;
- 互斥关系:读锁和写锁不能同时持有(读的时候不能写,写的时候不能读)
为什么读多写少场景,读写锁比 ReentrantLock 快
核心原因是减少了锁竞争:
- ReentrantLock 不管读还是写,所有线程都要排队抢锁
- 读写锁中,读操作不需要抢锁(共享),只有写操作需要抢锁(独占)
底层实现
读写锁和 ReentrantLock 一样,依赖 AQS 的state变量,但它把state(32 位 int)分成两个:
- 低 16 位:记录写锁的重入次数(和 ReentrantLock 的 state 作用类似)
- 高 16 位:记录读锁的持有次数(多个线程持有读锁时,总次数累加)
怎么通过位运算拆分?
AQS 里定义了两个常量:
// 写锁掩码:低16位全为1,用于提取写锁计数
static final int EXCLUSIVE_MASK = (1 << 16) - 1; // 0x0000FFFF
// 读锁移位:高16位需要左移16位
static final int SHARED_SHIFT = 16;
- 计算写锁计数:state & EXCLUSIVE_MASK(取低 16 位);
- 计算读锁计数:state >>> SHARED_SHIFT(右移 16 位,取高 16 位)。
写锁可重入性实现
读写锁的读锁和写锁都支持可重入,但实现方式不同。
写锁的可重入性(和 ReentrantLock 类似) :
-
线程获取写锁时,先检查低 16 位(写计数)
- 若写计数为 0(无锁),则 CAS 抢占写锁,成功后记录当前线程为写锁持有者
- 若写计数 > 0,且当前线程就是写锁持有者,则写计数 + 1(重入)
读锁可重入性实现
AQS 通过ThreadLocal+HoldCounter实现。
读锁是共享锁,多个线程可以同时持有,所以不能简单用高 16 位总计数记录单个线程的重入次数
- ThreadLocal:每个线程关联一个计数器(HoldCounter),记录该线程持有读锁的次数;
- 高 16 位的读计数:记录所有线程持有读锁的总次数(每次有线程获取读锁 + 1,释放 - 1)。
锁升级降级
- 锁升级:线程先持有读锁,再尝试获取写锁(读→写);
- 锁降级:线程先持有写锁,再尝试获取读锁,最后释放写锁(写→读)
锁降级
允许且常用,保证数据可见性
线程先持有写锁,再尝试获取读锁,最后释放写锁(写→读)
写锁是独占的,获取读锁时不会有其他线程干扰,降级后能安全读取自己修改的数据,同时允许其他线程读(共享)
锁升级
不允许,会导致死锁
线程先持有读锁,再尝试获取写锁(读→写);
假设两个线程都持有读锁,然后同时尝试升级为写锁:
- 线程 1:持有读锁,尝试获取写锁(需要等待所有读锁释放);
- 线程 2:持有读锁,尝试获取写锁(也需要等待所有读锁释放);
- 结果:两个线程互相等待对方释放读锁,导致死锁。尝试锁升级会阻塞
同步工具
CountDownLatch
核心作用
让一个线程等待其他N 个线程完成某个操作后再继续
比如主线程等待所有子线程初始化完成。
基于 AQS 的共享模式实现,核心逻辑:
- 初始化时,state设为 N(需要等待的线程数)
- 每个子线程完成后调用countDown():通过 CAS 将state减 1(直到 0)
- 等待的线程调用await():如果state>0,则进入 AQS 共享队列阻塞;当state=0时,唤醒所有等待线程
计数一旦到 0 就不可重置,只能使用一次。
典型适用场景
-
初始化前置任务等待: 例如程序启动时,需要先完成多个初始化任务(加载配置、初始化缓存、连接数据库等),主线程必须等待所有初始化完成后才能对外提供服务。
-
多线程任务汇总
- 例如统计多个子线程的计算结果(如分批次计算数据,主线程等待所有子线程计算完后汇总)
示例
// 假设有3个初始化任务
CountDownLatch latch = new CountDownLatch(3);
// 启动3个线程执行初始化
new Thread(() -> { loadConfig(); latch.countDown(); }, "配置加载").start();
new Thread(() -> { initCache(); latch.countDown(); }, "缓存初始化").start();
new Thread(() -> { connectDB(); latch.countDown(); }, "数据库连接").start();
// 主线程等待所有初始化完成
latch.await();
System.out.println("所有初始化完成,程序启动成功");
CyclicBarrier
核心作用
- 让 N 个线程互相等待,直到所有线程都到达「同一个屏障点」后,再一起继续执行(屏障点后可以执行一个共同的任务);
- 计数从 N 开始,每个线程到达屏障点时调用 await() ,当所有 N 个线程都到达后,计数重置(可重复使用)。
典型使用场景
-
分阶段协同任务
- 多线程并行处理一个大任务,任务分为多个阶段,每个阶段都需要所有线程完成当前阶段后,才能一起进入下一个阶段
-
多线程数据准备
- 例如多个线程分别准备不同的数据源,只有当所有数据源都准备好后,才能一起开始合并数据
-
重复执行的同步场景
- 定期执行的任务(每小时执行一次),每次执行前都需要多个线程准备就绪,此时 CyclicBarrier 可重复使用(计数重置)
示例
// 3个线程到达屏障后,一起执行合并任务
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有数据准备完成,开始合并..."); // 屏障后的共同任务
});
// 线程1:准备数据源A
new Thread(() -> { prepareDataA(); barrier.await(); }, "数据A").start();
// 线程2:准备数据源B
new Thread(() -> { prepareDataB(); barrier.await(); }, "数据B").start();
// 线程3:准备数据源C
new Thread(() -> { prepareDataC(); barrier.await(); }, "数据C").start();
Semaphore
核心作用
- 维护一个「许可集」,通过控制许可数量限制同时访问某个资源的线程数
- 线程需要访问资源时,调用 acquire() 获取许可(许可不足则阻塞),访问完成后调用 release() 释放许可(许可可被其他线程获取)
- 许可数量可动态调整(通过 release(n) 增加许可)
典型适用场景
-
限流控制
- 限制同时访问数据库的连接数(避免连接池耗尽),或限制并发请求接口的线程数(防止系统过载)
-
实现资源池
- 线程池、连接池的底层实现(通过 Semaphore 控制池内资源的并发获取,不过实际池化技术更多用 AQS 直接实现,但思想一致)
示例
// 限制最多3个线程同时访问数据库
Semaphore semaphore = new Semaphore(3);
// 10个线程竞争访问
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可(最多3个线程同时进入)
accessDB(); // 访问数据库
} finally {
semaphore.release(); // 释放许可
}
}).start();
}
ps:后续更多的示例(比如多个线程交替答应数字,打印奇偶数等),会在编程篇 加上~
BlockingQueue
阻塞队列是一种特殊的队列,它最大的特点是 线程安全 且支持阻塞操作
当队列满时,写入线程会阻塞;当队列空时,读取线程会阻塞
核心特性
- 线程安全:内部通过锁(ReentrantLock)或 CAS 保证多线程读写安全;
- 阻塞操作:提供put()(写入,队列满则阻塞)和take()(读取,队列空则阻塞)方法;
- 边界特性:分为 有界(容量固定,如 ArrayBlockingQueue)和 无界(容量可动态增长,如 LinkedBlockingQueue 默认无界)。
put () 和 take () 的阻塞机制
阻塞队列的核心价值在于put()和take()的阻塞逻辑,以最常用的 ArrayBlockingQueue 为例,底层用ReentrantLock+Condition实现
-
队列里有两个 Condition:notEmpty(等待非空,供 take () 使用)和notFull(等待未满,供 put () 使用)
-
put(E e)
- 加锁,检查队列是否满;
- 若满,调用notFull.await()阻塞,释放锁(等待其他线程 take () 后唤醒);
- 若不满,把元素加入队列,调用notEmpty.signal()唤醒一个等待 take () 的线程;
- 解锁
-
take()
- 加锁,检查队列是否空;
- 若空,调用notEmpty.await()阻塞,释放锁(等待其他线程 put () 后唤醒);
- 若不空,取出元素,调用notFull.signal()唤醒一个等待 put () 的线程;
- 解锁
ArrayBlockingQueue vs LinkedBlockingQueue
| 对比维度 | ArrayBlockingQueue | LinkedBlockingQueue |
|---|---|---|
| 数据结构 | 数组(连续内存) | 链表(节点离散) |
| 容量 | 必须指定(有界) | 可选(默认 Integer.MAX_VALUE,几乎无界) |
| 锁设计 | 单锁(ReentrantLock),读写互斥 | 双锁(takeLock+putLock),读写可并行 |
| 内存占用 | 初始化时分配固定大小,内存紧凑 | 动态创建节点,内存碎片化可能更多 |
| 迭代器一致性 | 弱一致性(迭代时不阻塞,可能看到旧数据) | 弱一致性 |
| 性能特点 | 读写竞争激烈时性能下降 | 读写分离,高并发下性能更稳定 |
线程池
创建线程是有成本的(操作系统内核态切换、内存分配等),如果每次任务都创建新线程,高并发下会导致系统资源耗尽
线程池通过 复用线程 和 控制并发数 解决了这个问题
线程池的核心参数
线程池的核心类是ThreadPoolExecutor,它的构造函数有 7 个参数,其中 5 个是核心
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲超时时间
TimeUnit unit, // 超时时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂(可选)
RejectedExecutionHandler handler // 拒绝策略(可选)
)
- corePoolSize:线程池长期保持的线程数(核心线程),即使空闲也不会销毁(除非设置allowCoreThreadTimeOut);
- maximumPoolSize:线程池允许创建的最大线程数(核心 + 非核心线程);
- workQueue:用于存放等待执行的任务的阻塞队列(核心线程满时,新任务入队);
- rejectedExecutionHandler:当线程数达最大值且队列满时,新任务的处理策略(拒绝策略)
新任务线程池的处理流程
当调用execute(Runnable task)提交任务时,线程池按以下步骤处理:
- 核心线程处理:若当前线程数 < corePoolSize,创建新线程(核心线程)执行任务;
- 队列缓存:若核心线程满,且 workQueue 未满,将任务放入队列等待;
- 非核心线程处理:若队列满,且当前线程数 < maximumPoolSize,创建非核心线程执行任务;
- 拒绝任务:若队列满且线程数达 maximumPoolSize,执行拒绝策略。
拒绝策略
当线程池无法处理新任务(线程满 + 队列满)时,拒绝策略决定如何处理任务
| 拒绝策略类型 | 处理逻辑 | 适用场景 |
|---|---|---|
| AbortPolicy(默认) | 直接抛出 RejectedExecutionException | 要求严格处理的场景(任务不能丢,需感知失败) |
| CallerRunsPolicy | 让提交任务的线程自己执行该任务 | 高并发场景(回压机制,减缓任务提交速度) |
| DiscardPolicy | 默默丢弃任务,不抛异常 | 任务可丢失的场景(如日志收集,允许部分丢失) |
| DiscardOldestPolicy | 丢弃队列中最旧的任务,再尝试提交当前任务 | 需处理最新任务的场景(如实时数据处理) |
高并发场景推荐 CallerRunsPolicy
当线程池满时,让提交任务的线程(比如 Tomcat 的工作线程)亲自执行任务
这会阻塞提交线程,间接减缓任务提交速度(“回压”),给线程池留出处理时间,避免任务大量丢失
应用中存在多个线程池合理吗?
合理场景。
多个线程池的存在往往是为了 隔离任务风险 和 优化资源分配。
不过 避免盲目创建大量线程池,一个中型应用的线程池数量建议控制在10 个以内
CompletableFuture 修改线程池依赖
CompletableFuture 的异步方法(如 supplyAsync(Supplier)、runAsync(Runnable))有两个重载版本
- 不指定线程池:默认使用 ForkJoinPool.commonPool()(一个共享的 ForkJoinPool 实例);
- 指定线程池:通过 supplyAsync(Supplier, Executor)、runAsync(Runnable, Executor) 传入自定义 Executor。
而 ThreadPoolExecutor 实现了 Executor 接口,因此完全可以作为参数传入,替代默认的 ForkJoinPool。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CompletableFutureWithThreadPool {
public static void main(String[] args) {
// 1. 创建自定义 ThreadPoolExecutor(IO 密集型,线程数设为 CPU 核心数 * 2)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8, // 核心线程数
16, // 最大线程数
60, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(100), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 2. CompletableFuture 使用自定义 ThreadPoolExecutor
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 模拟 IO 任务(如查询数据库)
try { TimeUnit.MILLISECONDS.sleep(100); }
catch (InterruptedException e) { e.printStackTrace(); }
return "查询结果:" + Thread.currentThread().getName();
}, executor); // 传入自定义线程池
// 3. 处理结果
future.thenAccept(result -> System.out.println("处理结果:" + result));
// 4. 关闭线程池(实际业务中注意在程序退出时关闭)
executor.shutdown();
}
}
总结
今天把并发进阶工具学习透了!
- LockSupport 的使用逻辑、
- AQS 的核心机制 + 数据结构 + 模式实现及自定义同步器要点,
- ReentrantLock 与 ReentrantReadWriteLock 的特性和锁实现原理
- CountDownLatch/CyclicBarrier/Semaphore 这些同步工具的核心特性,
- 还有 BlockingQueue 的用法套路
- 再到线程池的核心参数、工作流程、常见实现、拒绝策略及关闭机制
下一场该冲字节码与增强技术专场了!
- 字节码的本质、作用、JIT 编译、文件结构与调用指令,
- 字节码增强的基础概念、工具、与反射的区别及应用和潜在风险
- 还有 Arthas 底层的代码增强原理
- ...
这些可是底层调试与性能优化的关键,必须吃透逻辑,练出实战能力。
熟练度刷不停,知识点吃透稳,下期接着练~