JUC 并发编程
从「为什么会有并发问题」讲到「线上线程池打满怎么查」。02 篇的多线程只讲到「怎么创建线程」,这篇补的是内存模型、锁、AQS、线程池源码、并发容器这一层。
1. 并发问题的三个根源
所有并发 bug 都能归到三条里:
| 问题 | 含义 | 典型现象 |
|---|---|---|
| 原子性 | 一个或多个操作要么全做、要么全不做 | i++ 丢更新、先查后改的竞态 |
| 可见性 | 一个线程改了共享变量,另一个线程要能立刻看到 | 死循环读不到 stop 标志位 |
| 有序性 | 编译器 / CPU 的指令重排不能改变单线程语义,但会打乱多线程下的执行顺序 | 半初始化对象被别的线程看到 |
为什么会有可见性问题:CPU 有自己的 L1/L2 缓存,写操作先落到缓存,靠缓存一致性协议(MESI)同步;同时为了压榨性能,写缓冲区和无效化队列会让「写入」和「其他核看到」之间存在延迟。Java 不去直接管硬件,而是抽象出一套 JMM(Java 内存模型) 来规定「什么时候必须可见」。
线程 A 主内存 线程 B
| | |
工作内存 A <--read--- 共享变量 x=0 ---read--> 工作内存 B
| | |
写 x=1 -> 先写本地缓存,何时刷回主内存?—— 由 JMM 的 happens-before 决定线程 A 主内存 线程 B
| | |
工作内存 A <--read--- 共享变量 x=0 ---read--> 工作内存 B
| | |
写 x=1 -> 先写本地缓存,何时刷回主内存?—— 由 JMM 的 happens-before 决定2. JMM 与 happens-before
2.1 happens-before 是什么
JMM 用 happens-before 定义可见性:如果 A happens-before B,那么 A 的结果对 B 可见,且 A 在 B 之前执行(对程序员可见的顺序,不代表 CPU 真的按这个顺序跑)。
8 条规则(记住前 6 条够用):
- 程序顺序:单线程内,前面的操作 hb 后面的操作。
- 监视器锁:对同一个锁的 unlock hb 后续的 lock。
- volatile:对 volatile 变量的写 hb 后续对这个变量的读。
- 线程启动:
Thread.start()hb 该线程里的任何操作。 - 线程终止:线程里的所有操作 hb 其他线程检测到它终止(
join()返回)。 - 中断:对线程
interrupt()hb 被中断线程检测到中断。 - 对象终结:构造函数结束 hb
finalize()开始。 - 传递性:A hb B、B hb C,则 A hb C。
2.2 as-if-serial 与指令重排
单线程里,只要最终结果不变,编译器、JIT、CPU 都可以重排。所以这段代码里 a 和 b 的赋值顺序不保证:
int a = 1; // 可能后执行
int b = 2; // 可能先执行int a = 1; // 可能后执行
int b = 2; // 可能先执行但 x = 1; y = x + 1; 不会重排成先算 y —— 这就是 as-if-serial 的边界:有数据依赖的不能重排。
2.3 内存屏障
JMM 把屏障分成四类,是 volatile 和锁的底层实现手段:
| 屏障 | 作用 |
|---|---|
| LoadLoad | 前面的读先于后面的读 |
| StoreStore | 前面的写先于后面的写 |
| LoadStore | 前面的读先于后面的写 |
| StoreLoad | 前面的写先于后面的读(开销最大,volatile 写后必插) |
x86 上只有 StoreLoad 需要显式指令(lock 前缀 / mfence),其余三种靠 x86 本身较强的内存模型天然满足 —— 所以「volatile 在 x86 上几乎无额外开销」这句话只对读成立。
3. volatile:只管可见性和有序性
3.1 三个语义
- 可见性:写 volatile 变量时,会把该线程工作内存中的值刷回主内存;读时会从主内存重新读。
- 禁止重排:写前插 StoreStore,写后插 StoreLoad;读后插 LoadLoad / LoadStore。
- 不保证原子性:
volatile int i; i++依然是「读-改-写」三步,照样丢更新。
3.2 为什么 DCL 单例必须加 volatile
public class Singleton {
private static volatile Singleton instance; // 少了 volatile 就可能拿到半初始化对象
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 1.分配内存 2.初始化 3.引用指向内存
}
}
}
return instance;
}
}public class Singleton {
private static volatile Singleton instance; // 少了 volatile 就可能拿到半初始化对象
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 1.分配内存 2.初始化 3.引用指向内存
}
}
}
return instance;
}
}new 不是原子的。2 和 3 可能重排:别的线程看到 instance != null 就直接返回,但此时对象字段还是默认值。
3.3 什么时候用 volatile
- 状态标志位(
volatile boolean running) - 一次性安全发布(DCL 的 instance)
- 读多写少、写不依赖当前值(比如配置项、计数器只用于统计可容忍误差)
不要用它做 i++、复合判断、需要互斥的临界区。
3.4 volatile 与 synchronized 的区别
| volatile | synchronized | |
|---|---|---|
| 作用范围 | 变量 | 代码块 / 方法 |
| 原子性 | 不保证(单次读写除外,long/double 的 64 位读写靠它保证) | 保证 |
| 可见性 / 有序性 | 保证 | 保证 |
| 阻塞 | 不阻塞 | 会阻塞 |
| 底层 | 内存屏障 | monitor(monitorenter / monitorexit) |
4. synchronized 与锁升级
4.1 字节码层面
synchronized (lock) { ... }synchronized (lock) { ... }编译后是 monitorenter / monitorexit(正常退出一个、异常退出一个,所以必须成对)。方法是 ACC_SYNCHRONIZED 标志位。二者最终都落到 monitor 对象(HotSpot 里是 ObjectMonitor,有 _owner、_EntryList、_WaitSet、_recursions)。
4.2 锁状态存在哪:对象头 Mark Word
64 位 JVM 的 Mark Word(8 字节):
| 锁状态 | 62 位内容 |
|---|---|
| 无锁 | unused:25 + hash:31 + unused:1 + age:4 + 0 + 01 |
| 偏向锁 | 线程 ID:54 + epoch:2 + unused:1 + age:4 + 1 + 01 |
| 轻量级锁 | 指向栈中锁记录的指针:62 + 00 |
| 重量级锁 | 指向 ObjectMonitor 的指针:62 + 10 |
| GC 标记 | 空 + 11 |
4.3 升级路径
无锁 --(第一个线程 CAS 记 ID)--> 偏向锁 --(第二个线程竞争)--> 轻量级锁 --(自旋失败/竞争加剧)--> 重量级锁无锁 --(第一个线程 CAS 记 ID)--> 偏向锁 --(第二个线程竞争)--> 轻量级锁 --(自旋失败/竞争加剧)--> 重量级锁- 偏向锁:只有一个线程反复进同一个锁,把线程 ID 写在 Mark Word 里,之后进来连 CAS 都省了。JDK 15 起默认关闭、JDK 18 起彻底废弃(撤销成本高于收益),新项目直接按「无锁 → 轻量 → 重量」理解即可,但面试仍会问,答的时候补一句「JDK 15+ 已默认禁用偏向锁」会加分。
- 轻量级锁:在当前线程栈帧里建 Lock Record,CAS 把 Mark Word 指向它;失败就自旋,自旋到阈值(自适应自旋)还不行就膨胀。
- 重量级锁:竞争线程进
_EntryList排队,由操作系统 mutex 挂起 —— 涉及用户态/内核态切换,所以「synchronized 慢」指的是这一层。
4.4 锁优化
- 锁消除:逃逸分析发现锁对象不会逃出方法,直接去掉(如方法内
new Object()加锁)。 - 锁粗化:循环里反复加解锁会被合并成一次。
- 自适应自旋:根据上次自旋成功率决定这次自旋多久。
4.5 synchronized vs ReentrantLock
| synchronized | ReentrantLock | |
|---|---|---|
| 实现 | JVM 关键字,monitor | JDK 类,基于 AQS |
| 释放 | 自动(异常也释放) | 必须 finally { unlock(); } |
| 可中断 | 不支持 | lockInterruptibly() |
| 超时 | 不支持 | tryLock(timeout) |
| 公平锁 | 只有非公平 | 可选公平 / 非公平 |
| 条件队列 | 一个 wait/notify | 多个 Condition,可精准唤醒 |
| 性能 | JDK6 后优化得差不多 | 高竞争下更可控 |
结论口径:能不用显式锁就不用;需要超时、可中断、多条件队列时才上 ReentrantLock。
5. CAS 与原子类
5.1 CAS 是什么
// 伪代码:只有当 value 等于 expected 时才更新为 newValue
boolean cas(int expected, int newValue)// 伪代码:只有当 value 等于 expected 时才更新为 newValue
boolean cas(int expected, int newValue)对应 CPU 的 cmpxchg 指令,Java 里通过 Unsafe.compareAndSwapInt(JDK9+ 是 VarHandle)调用。它是乐观锁:先假设没人改,改的时候校验,失败就重试(自旋)。
5.2 三个问题
- ABA:值从 A→B→A,CAS 察觉不到。解决:
AtomicStampedReference带版本号。 - 自旋开销:高竞争下大量空转。解决:
LongAdder分段、或者直接上锁。 - 只能保证一个变量:多个变量要么合并成一个对象用
AtomicReference,要么加锁。
5.3 AtomicInteger 与 LongAdder
AtomicInteger ai = new AtomicInteger(0);
ai.incrementAndGet(); // 底层就是 CAS 自旋
LongAdder adder = new LongAdder(); // 高并发计数首选
adder.increment();
long sum = adder.sum(); // 汇总所有 CellAtomicInteger ai = new AtomicInteger(0);
ai.incrementAndGet(); // 底层就是 CAS 自旋
LongAdder adder = new LongAdder(); // 高并发计数首选
adder.increment();
long sum = adder.sum(); // 汇总所有 CellLongAdder 把热点值拆成 base + Cell[],不同线程 CAS 不同的 Cell,读的时候再累加。写多读少用它,读多写少用 AtomicLong。
6. AQS:整个 JUC 锁的地基
6.1 核心结构
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 同步状态,语义由子类定义
private transient volatile Node head; // 同步队列头
private transient volatile Node tail; // 同步队列尾
}public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 同步状态,语义由子类定义
private transient volatile Node head; // 同步队列头
private transient volatile Node tail; // 同步队列尾
}state:ReentrantLock里是重入次数,Semaphore里是可用许可数,CountDownLatch里是剩余计数。- 同步队列:CLH 变体,双向链表,抢不到锁的线程包装成
Node排队,LockSupport.park()挂起。 - 条件队列:
Condition.await()把线程从同步队列移到条件队列,signal()再移回来重新排队抢锁。
6.2 独占与共享
子类只需实现 tryAcquire / tryRelease(独占)或 tryAcquireShared / tryReleaseShared(共享),AQS 负责排队、阻塞、唤醒。这就是模板方法模式。
// 以 ReentrantLock 非公平锁为例(简化)
final boolean nonfairTryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 直接抢,不排队 —— 这就是"非公平"
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
setState(c + acquires); // 重入,直接加 state
return true;
}
return false;
}// 以 ReentrantLock 非公平锁为例(简化)
final boolean nonfairTryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 直接抢,不排队 —— 这就是"非公平"
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
setState(c + acquires); // 重入,直接加 state
return true;
}
return false;
}公平 vs 非公平:公平锁在 tryAcquire 里先 hasQueuedPredecessors() 判断有没有人在排队,有就不抢;非公平锁上来就 CAS。非公平吞吐更高,代价是可能饿死排队线程。
6.3 基于 AQS 的四个常用工具
| 工具 | state 语义 | 模式 | 典型用法 |
|---|---|---|---|
| ReentrantLock | 重入次数 | 独占 | 替代 synchronized,需要超时/中断 |
| ReentrantReadWriteLock | 高 16 位读、低 16 位写 | 共享 + 独占 | 读多写少;注意写锁可降级、读锁不能升级 |
| Semaphore | 剩余许可数 | 共享 | 限流、连接池 |
| CountDownLatch | 剩余计数(不可重置) | 共享 | 一个线程等 N 个任务完成 |
// CountDownLatch:主线程等 3 个初始化完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> { init(); latch.countDown(); }).start();
}
latch.await(); // 归零前一直阻塞// CountDownLatch:主线程等 3 个初始化完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> { init(); latch.countDown(); }).start();
}
latch.await(); // 归零前一直阻塞CyclicBarrier 不是 AQS 实现(用 ReentrantLock + Condition),区别是可重用,且是「线程互相等」。
7. ThreadLocal:线程私有的上下文
7.1 结构
Thread --持有--> ThreadLocalMap(每个线程独一份)
└── Entry[] table,key 是 ThreadLocal 的弱引用,value 是强引用Thread --持有--> ThreadLocalMap(每个线程独一份)
└── Entry[] table,key 是 ThreadLocal 的弱引用,value 是强引用ThreadLocal<User> holder = new ThreadLocal<>();
holder.set(user); // 存到当前线程的 map 里
User u = holder.get();
holder.remove(); // 一定要 removeThreadLocal<User> holder = new ThreadLocal<>();
holder.set(user); // 存到当前线程的 map 里
User u = holder.get();
holder.remove(); // 一定要 removeThreadLocalMap 用开放寻址(线性探测)解决冲突,不用链表 —— 因为 entry 少。
7.2 内存泄漏链路
Thread → ThreadLocalMap → Entry(key 弱引用, value 强引用) → value 对象Thread → ThreadLocalMap → Entry(key 弱引用, value 强引用) → value 对象ThreadLocal 对象被回收后,key 变 null,但 value 还被 Entry 强引用,只要线程不死(尤其是线程池里的核心线程),value 就一直泄漏。所以:
用完必须
remove()。线程池里不 remove,下一个任务可能读到上一个用户的身份信息 —— 这既是内存泄漏,也是越权 bug。
7.3 InheritableThreadLocal 与 TTL
InheritableThreadLocal:在new Thread()时把父线程的 map 浅拷贝一份给子线程 —— 只在线程创建时生效,线程池复用线程时失效。TransmittableThreadLocal(阿里 TTL):配合TtlExecutors包装线程池,在任务提交和执行的瞬间做上下文快照/回放,解决线程池传递问题。
8. 线程池:最容易被追问的一节
8.1 七个参数
new ThreadPoolExecutor(
corePoolSize, // 核心线程数,常驻(allowCoreThreadTimeOut 可回收)
maximumPoolSize, // 最大线程数
keepAliveTime, // 非核心线程空闲存活时间
TimeUnit.SECONDS,
workQueue, // 等待队列
threadFactory, // 线程工厂,务必自定义名字,排查靠它
handler // 拒绝策略
);new ThreadPoolExecutor(
corePoolSize, // 核心线程数,常驻(allowCoreThreadTimeOut 可回收)
maximumPoolSize, // 最大线程数
keepAliveTime, // 非核心线程空闲存活时间
TimeUnit.SECONDS,
workQueue, // 等待队列
threadFactory, // 线程工厂,务必自定义名字,排查靠它
handler // 拒绝策略
);8.2 执行流程(背下来)
提交任务
├─ 当前线程数 < core → 新建核心线程执行
├─ 核心已满 → 尝试入队
│ ├─ 入队成功 → 等核心线程来取
│ └─ 队列满 → 线程数 < max → 新建非核心线程执行
└─ 队列满且线程数 = max → 触发拒绝策略提交任务
├─ 当前线程数 < core → 新建核心线程执行
├─ 核心已满 → 尝试入队
│ ├─ 入队成功 → 等核心线程来取
│ └─ 队列满 → 线程数 < max → 新建非核心线程执行
└─ 队列满且线程数 = max → 触发拒绝策略为什么先入队而不是先开满线程:创建线程成本高(栈内存 + 内核调度),队列能缓冲突发流量;只有在队列也满了才说明真的扛不住。
8.3 四种拒绝策略
| 策略 | 行为 | 适合 |
|---|---|---|
| AbortPolicy(默认) | 抛 RejectedExecutionException | 必须让上游感知失败 |
| CallerRunsPolicy | 提交任务的线程自己跑 | 削峰,天然限速 |
| DiscardPolicy | 静默丢弃 | 日志、埋点等可丢数据 |
| DiscardOldestPolicy | 丢队头最老的,再重试提交 | 只要最新数据 |
8.4 为什么禁用 Executors 的默认工厂
newFixedThreadPool/newSingleThreadExecutor:队列是LinkedBlockingQueue,无界,任务堆积到 OOM。newCachedThreadPool:最大线程数Integer.MAX_VALUE,无限创建线程,直接 OOM。
面试口径:必须手写
ThreadPoolExecutor,队列要有界,线程数要按业务算,并且给线程起名。
8.5 线程数怎么定
- CPU 密集(计算、序列化):
N + 1,N 是核数。 - IO 密集(RPC、DB、文件):
N × (1 + 等待时间 / 计算时间),实测 2N ~ 4N 起步再压测。 - 混合型按瓶颈拆分两个池,别用一个池扛所有。
8.6 几个坑
// 坑 1:submit 吞异常 —— 异常被封在 Future 里
executor.submit(() -> { throw new RuntimeException("boom"); }); // 主线程毫无感知
Future<?> f = executor.submit(task);
f.get(); // 这样才抛出来
// 或者 execute():异常会走到 UncaughtExceptionHandler
// 坑 2:线程池里 ThreadLocal 脏读 —— 任务结束必须 remove
// 坑 3:关闭要用 shutdown() 而不是 shutdownNow()(后者会中断正在跑的任务)// 坑 1:submit 吞异常 —— 异常被封在 Future 里
executor.submit(() -> { throw new RuntimeException("boom"); }); // 主线程毫无感知
Future<?> f = executor.submit(task);
f.get(); // 这样才抛出来
// 或者 execute():异常会走到 UncaughtExceptionHandler
// 坑 2:线程池里 ThreadLocal 脏读 —— 任务结束必须 remove
// 坑 3:关闭要用 shutdown() 而不是 shutdownNow()(后者会中断正在跑的任务)8.7 监控
ThreadPoolExecutor pool = ...;
pool.getActiveCount(); // 活跃线程
pool.getQueue().size(); // 积压任务数 ← 最该告警的指标
pool.getCompletedTaskCount();
pool.getPoolSize();ThreadPoolExecutor pool = ...;
pool.getActiveCount(); // 活跃线程
pool.getQueue().size(); // 积压任务数 ← 最该告警的指标
pool.getCompletedTaskCount();
pool.getPoolSize();把 queue.size / queue.remainingCapacity 打到 Prometheus,超过 70% 就告警 —— 这是线上最有效的一道防线。
9. 并发容器
9.1 ConcurrentHashMap(JDK 1.8)
| 版本 | 并发控制 |
|---|---|
| 1.7 | Segment 分段锁,默认 16 段 |
| 1.8 | CAS 初始化 + synchronized 锁桶头节点 + 红黑树 |
关键点:
- 锁粒度到「链表/树的头节点」,不是整表。
size()用baseCount + CounterCell[]分散计数(和 LongAdder 同思路)。get()不加锁,靠volatile读 Node 的 val 和 next。- 不允许 null key / null value —— 因为并发下无法区分「不存在」和「值是 null」。
9.2 CopyOnWriteArrayList
写时复制整个数组,读完全无锁。只适合读多写极少(白名单、监听器列表),写多了会疯狂 GC。
9.3 BlockingQueue 家族
| 实现 | 特点 | 场景 |
|---|---|---|
| ArrayBlockingQueue | 有界数组,单锁两 Condition | 线程池队列(推荐) |
| LinkedBlockingQueue | 链表,默认无界(坑) | 一定传容量 |
| SynchronousQueue | 不存储,直接交接 | newCachedThreadPool |
| PriorityBlockingQueue | 无界优先队列 | 按优先级消费 |
| DelayQueue | 延迟到期才能取 | 本地延迟任务 |
| LinkedTransferQueue | 无锁,transfer 语义 | 高吞吐生产者消费者 |
10. CompletableFuture:异步编排
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.get(id), pool);
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> orderService.list(id), pool);
// 两个并行、都完成后再合并
CompletableFuture<UserDetail> detail = userFuture
.thenCombine(orderFuture, (u, orders) -> new UserDetail(u, orders))
.exceptionally(e -> UserDetail.empty()) // 兜底
.completeOnTimeout(UserDetail.empty(), 500, TimeUnit.MILLISECONDS); // 超时兜底CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.get(id), pool);
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> orderService.list(id), pool);
// 两个并行、都完成后再合并
CompletableFuture<UserDetail> detail = userFuture
.thenCombine(orderFuture, (u, orders) -> new UserDetail(u, orders))
.exceptionally(e -> UserDetail.empty()) // 兜底
.completeOnTimeout(UserDetail.empty(), 500, TimeUnit.MILLISECONDS); // 超时兜底记住三组:
| 需求 | 方法 |
|---|---|
| 串行 | thenApply(有返回值)/ thenAccept(无)/ thenRun(不关心结果) |
| 并行合并 | thenCombine(两个)/ allOf(多个,无返回值)/ anyOf |
| 异常与超时 | exceptionally / handle / whenComplete / completeOnTimeout |
必须传自定义线程池,否则用 ForkJoinPool.commonPool(),核心数 - 1 个线程,被 IO 任务占满会拖垮全站。
10.1 allOf 最大的坑:它不会抛异常
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.get(); // ❌ 某个子任务异常时,这里可能感知不到
// ✅ 正确姿势:allOf 只负责「都结束」,异常要自己收
CompletableFuture.allOf(f1, f2, f3)
.exceptionally(e -> { log.error("有任务失败", e); return null; })
.join();
// 或者逐个 join,让异常冒出来
f1.join(); f2.join(); f3.join();CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.get(); // ❌ 某个子任务异常时,这里可能感知不到
// ✅ 正确姿势:allOf 只负责「都结束」,异常要自己收
CompletableFuture.allOf(f1, f2, f3)
.exceptionally(e -> { log.error("有任务失败", e); return null; })
.join();
// 或者逐个 join,让异常冒出来
f1.join(); f2.join(); f3.join();原因:allOf 返回的是「组合后的 future」,子任务的异常被包在各自的 future 里,不 join 就不会抛。
10.2 带 Async 后缀和不带的区别
| 写法 | 回调在哪个线程跑 |
|---|---|
thenApply(fn) | 当前完成的那个线程(可能是任意线程) |
thenApplyAsync(fn) | 提交到线程池(默认 commonPool) |
thenApplyAsync(fn, pool) | 提交到指定线程池 |
坑:thenApply 如果跟在 supplyAsync(ctx) 后面,回调可能跑在业务线程池的线程里 —— 如果在回调里做阻塞 IO,就会占满那个池。需要隔离时一律显式传池。
10.3 不要在 CompletableFuture 里忘记兜底
// ❌ 没有超时、没有兜底:下游卡住 → 整条链路一起卡
CompletableFuture.supplyAsync(() -> remote.call(), pool).join();
// ✅ 超时 + 兜底 + 异常日志
CompletableFuture.supplyAsync(() -> remote.call(), pool)
.completeOnTimeout(Default.INSTANCE, 300, TimeUnit.MILLISECONDS)
.exceptionally(e -> { log.warn("降级", e); return Default.INSTANCE; })
.join();// ❌ 没有超时、没有兜底:下游卡住 → 整条链路一起卡
CompletableFuture.supplyAsync(() -> remote.call(), pool).join();
// ✅ 超时 + 兜底 + 异常日志
CompletableFuture.supplyAsync(() -> remote.call(), pool)
.completeOnTimeout(Default.INSTANCE, 300, TimeUnit.MILLISECONDS)
.exceptionally(e -> { log.warn("降级", e); return Default.INSTANCE; })
.join();11. 三个高频并发模式
11.1 生产者消费者
// 生产环境直接用 BlockingQueue,别手写 wait/notify
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(1000);
// 生产
queue.put(task); // 满了阻塞;offer 是非阻塞版
// 消费
Task t = queue.take(); // 空了阻塞;poll 是非阻塞版// 生产环境直接用 BlockingQueue,别手写 wait/notify
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(1000);
// 生产
queue.put(task); // 满了阻塞;offer 是非阻塞版
// 消费
Task t = queue.take(); // 空了阻塞;poll 是非阻塞版手写版(面试让写就用这个):while 判断条件 + wait() + notifyAll(),三个细节缺一不可。
11.2 单例的并发正确姿势
// 首选:静态内部类(懒加载 + 线程安全,靠类加载机制保证)
public class Singleton {
private Singleton() {}
private static class Holder { static final Singleton INSTANCE = new Singleton(); }
public static Singleton getInstance() { return Holder.INSTANCE; }
}
// 次选:枚举(防反射、防反序列化破坏)
public enum SingletonEnum {
INSTANCE;
public void doSomething() {}
}// 首选:静态内部类(懒加载 + 线程安全,靠类加载机制保证)
public class Singleton {
private Singleton() {}
private static class Holder { static final Singleton INSTANCE = new Singleton(); }
public static Singleton getInstance() { return Holder.INSTANCE; }
}
// 次选:枚举(防反射、防反序列化破坏)
public enum SingletonEnum {
INSTANCE;
public void doSomething() {}
}11.3 并发限流(Semaphore 版)
Semaphore permits = new Semaphore(10); // 最多 10 个并发
if (permits.tryAcquire(200, TimeUnit.MILLISECONDS)) {
try { doWork(); } finally { permits.release(); }
} else {
// 排队超时,走降级
}Semaphore permits = new Semaphore(10); // 最多 10 个并发
if (permits.tryAcquire(200, TimeUnit.MILLISECONDS)) {
try { doWork(); } finally { permits.release(); }
} else {
// 排队超时,走降级
}12. 线上并发问题排查
12.1 死锁
jstack <pid> | grep -A 20 "Found one Java-level deadlock"jstack <pid> | grep -A 20 "Found one Java-level deadlock"输出里会直接给出互相等待的两个线程和持有的锁。代码层面:所有线程按同一顺序加锁、用 tryLock(timeout) 代替 lock()。
12.2 CPU 100%
top -Hp <pid> # 找到最耗 CPU 的线程 tid
printf '%x\n' <tid> # 转 16 进制
jstack <pid> | grep -A 30 <hex-tid>top -Hp <pid> # 找到最耗 CPU 的线程 tid
printf '%x\n' <tid> # 转 16 进制
jstack <pid> | grep -A 30 <hex-tid>看栈顶是业务死循环、GC 线程(说明内存有问题)、还是正则回溯。
12.3 线程池打满
现象:接口 RT 飙升但 CPU 不高、getActiveCount() 顶到 max、队列持续增长。排查顺序:
- 队列大小和拒绝计数(有没有在丢任务)
- 任务里有没有阻塞调用(DB 慢查询、同步 HTTP、锁等待)
- 线程栈里都在等什么:
jstack | grep -c "waiting on condition"
13. 虚拟线程与并行流
13.1 虚拟线程(JDK 21)
是什么:由 JVM 调度的轻量线程,大量虚拟线程跑在少量「载体线程」上(M:N)。创建成本约几百字节,可以几十万个。
解决什么问题:平台线程(1:1 映射内核线程)很贵 —— 几千个就吃满内存,而且一旦阻塞就白占一个内核线程。虚拟线程遇到阻塞 IO 时会自动从载体线程上卸载,载体线程立刻去跑别的虚拟线程。
// 推荐:每个任务一个虚拟线程(不要池化)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> httpClient.send(request, BodyHandlers.ofString())));
}// 推荐:每个任务一个虚拟线程(不要池化)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> httpClient.send(request, BodyHandlers.ofString())));
}| 平台线程 | 虚拟线程 | |
|---|---|---|
| 数量级 | 几千 | 几十万 |
| 调度者 | 操作系统 | JVM |
| 阻塞代价 | 占住内核线程 | 卸载,载体线程继续干活 |
| 适合 | CPU 密集 | IO 密集、每请求一线程 |
三个坑:
- 不要池化虚拟线程(池化就失去了「便宜」的意义,直接 per-task 创建);
synchronized里长时间阻塞会 pin 住载体线程(JDK 21 的限制,JDK 24 已大幅改善)—— 高并发场景改用ReentrantLock;ThreadLocal慎用:几十万虚拟线程各存一份会爆内存,改用 ScopedValue 或显式传参。
13.2 ForkJoinPool 与 parallelStream
工作窃取(work-stealing):每个 worker 有自己的双端队列,任务被 fork 成子任务压入自己的队列;空闲的 worker 从别人队列的尾部偷任务。适合「能递归拆分的纯计算」。
// parallelStream 用的就是 ForkJoinPool.commonPool()
list.parallelStream().map(this::heavyCompute).collect(toList());// parallelStream 用的就是 ForkJoinPool.commonPool()
list.parallelStream().map(this::heavyCompute).collect(toList());并行流的四个坑:
| 坑 | 说明 |
|---|---|
| 在流里做阻塞 IO | 会占满 commonPool(并行度 = 核数 - 1),拖垮全站其他并行流 |
| 共用 commonPool | 所有 parallelStream 抢同一个池,互相影响 |
| 有状态/顺序敏感 | forEach 不保证顺序,共享可变状态会竞态(要用 collect) |
| 数据量小反而更慢 | 拆分与合并有开销,几个元素的列表用 for 循环 |
结论:CPU 密集 + 数据量大 → 可以用;涉及 IO → 用自定义线程池或虚拟线程。
13.3 StampedLock(读多写少的利器)
StampedLock lock = new StampedLock();
// 乐观读:完全不加锁,读完校验版本
long stamp = lock.tryOptimisticRead();
int x = state.x;
if (!lock.validate(stamp)) { // 期间有人写过 → 退化成悲观读
stamp = lock.readLock();
try { x = state.x; } finally { lock.unlockRead(stamp); }
}StampedLock lock = new StampedLock();
// 乐观读:完全不加锁,读完校验版本
long stamp = lock.tryOptimisticRead();
int x = state.x;
if (!lock.validate(stamp)) { // 期间有人写过 → 退化成悲观读
stamp = lock.readLock();
try { x = state.x; } finally { lock.unlockRead(stamp); }
}| ReentrantReadWriteLock | StampedLock | |
|---|---|---|
| 读模式 | 悲观读(要加锁) | 悲观读 + 乐观读(不加锁) |
| 可重入 | ✅ | ❌ 不可重入 |
| 支持条件变量 | ✅ | ❌ |
| 适用 | 通用 | 读远多于写、且能接受不可重入 |
14. 速查表
| 问题 | 一句话答案 |
|---|---|
| volatile 能保证原子性吗 | 不能,只能保证可见性和有序性 |
| synchronized 锁的是什么 | 对象头 Mark Word + monitor;锁代码块锁的是括号里的对象 |
| 锁能降级吗 | 不能,只能升级 |
| AQS 的 state 是什么 | 由子类定义的同步状态:重入次数 / 许可数 / 剩余计数 |
| 线程池先入队还是先扩容 | 先入队,队列满了才扩到 max |
| 为什么不用 Executors | 队列无界或线程无上限,都会 OOM |
| ThreadLocal 为什么会泄漏 | key 弱引用 value 强引用,线程不死 value 不释放 |
| CountDownLatch 和 CyclicBarrier 区别 | 前者一次性、一个等多个;后者可重用、互相等 |
| CompletableFuture 用哪个线程池 | 必须传自定义的,默认 commonPool 只有 N-1 个线程 |
| CAS 的 ABA 怎么解决 | 加版本号,AtomicStampedReference |
Cao Note