线程池解决了"怎么管理线程"的问题,但线程池本身并不保证线程安全——它只是把任务交给线程执行。真正保证多线程正确协作的,是 volatile、synchronized、CAS 和 AQS 这套并发原语。本文从 JMM 内存模型讲起,把 Java 并发的"内功心法"彻底拆开。
一、JMM 内存模型:并发问题的根源
Java 多线程程序中出现的各种诡异 Bug——值没变、结果不对、指令乱序——归根结底都源于 JMM(Java Memory Model)。JMM 不是真实存在的硬件结构,而是一组规范,定义了线程如何通过主内存交互。
1.1 三大特性:可见性、原子性、有序性
| 特性 | 含义 | 违反时的现象 | 保证手段 |
|---|---|---|---|
| 可见性 | 一个线程修改了共享变量,其他线程能立即看到最新值 | 线程 A 改了 flag=true,线程 B 还在读到 false | volatile / synchronized / final |
| 原子性 | 一个操作不可分割,要么全部完成,要么全不做 | i++ 看起来是一行,实际是 读-改-写 三步 | synchronized / Lock / Atomic |
| 有序性 | 程序按照代码顺序执行(在本线程内观察) | 指令重排导致对象未初始化就被引用 | volatile / happens-before |
// 经典的可见性问题示例
public class VisibilityDemo {
private static boolean flag = false; // 没有 volatile!
public static void main(String[] args) throws Exception {
new Thread(() -> {
while (!flag) { } // 可能永远看不到 flag 变为 true
System.out.println("Thread B sees flag = true");
}).start();
Thread.sleep(100);
flag = true; // 主线程修改了,但子线程可能永远看不到
System.out.println("Main set flag = true");
}
}
上述代码在
-server模式下极大概率会死循环——因为 JIT 编译器可能将while(!flag)优化为无限循环,不再从主内存重新读取 flag。
1.2 happens-before 规则
JMM 通过 happens-before 原则来界定操作之间的可见性顺序。如果 A happens-before B,那么 A 的结果对 B 可见:
| 规则 | 说明 |
|---|---|
| 程序顺序规则 | 同一线程中,前面的操作 happens-before 后面的操作 |
| 监视器锁规则 | unlock 操作 happens-before 后续对同一把锁的 lock 操作 |
| volatile 变量规则 | 写操作 happens-before 后续对同一变量的读操作 |
| 线程启动规则 | Thread.start() happens-before 该线程的每一个操作 |
| 线程终止规则 | 线程中的所有操作 happens-before 其他线程检测到该线程结束 |
| 线程中断规则 | interrupt() 调用 happens-before 被中断线程检测到中断事件 |
| 对象终结规则 | 构造函数返回 happens-before finalize() 方法 |
| 传递性 | 若 A hb B 且 B hb C,则 A hb C |
1.3 指令重排序与内存屏障
编译器和 CPU 为了提升性能,会对指令进行重排序,分为三种:
int a = 1; // ①
int b = 2; // ②
int c = a + b; // ③
// ①和②可以互换顺序,但③必须在①②之后 —— 这就是重排序的边界
内存屏障(Memory Barrier) 是阻止重排序的"硬指令",JVM 插入四种屏障:
| 屏障类型 | 作用 |
|---|---|
| LoadLoad | 确保 Load1 数据装载先于 Load2 及后续装载 |
| StoreStore | 确保 Store1 数据刷新先于 Store2 及后续存储 |
| LoadStore | 确保 Load1 数据装载先于 Store2 及后续存储 |
| StoreLoad | 确保 Store1 数据刷新先于 Load2 及后续装载(开销最大) |
二、volatile 关键字深度解析
2.1 volatile 能保证什么(不能保证什么)
一句话总结:volatile 保证可见性和有序性(禁止指令重排),但不保证原子性。
| 能力 | 是否保证 | 说明 |
|---|---|---|
| 可见性 | ✅ | 写操作强制刷回主存,读操作强制从主存读取 |
| 有序性 | ✅ | 插入内存屏障,禁止特定类型的指令重排 |
| 原子性 | ❌ | volatile int i; i++ 仍然不是原子操作 |
// volatile 不保证原子性的经典反例
private static volatile int count = 0;
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) count++;
}).start();
}
Thread.sleep(1000);
System.out.println("count = " + count); // 几乎一定 < 100000
}
即使加了 volatile,
count++仍然是 读-改-写 三步操作,多线程交叉执行就会丢数据。要保证原子性必须用 synchronized 或 Atomic 类。
2.2 volatile 实现原理
volatile 的底层依赖两套机制协同工作:
(1)内存语义层面 —— 内存屏障
volatile boolean v;
v = true; // 写操作:插入 StoreStore + StoreLoad 屏障
boolean b = v; // 读操作:插入 LoadLoad + LoadStore 屏障
(2)硬件层面 —— MESI 缓存一致性协议
现代 CPU 使用 MESI 协议维护缓存行状态:
| 状态 | 含义 |
|---|---|
| M(Modified) | 已修改,与主存不一致,仅在本缓存有效 |
| E(Exclusive) | 独占,与主存一致,仅在本缓存有效 |
| S(Shared) | 共享,与主存一致,多个缓存都有副本 |
| I(Invalid) | 无效,缓存行已失效 |
当 CPU 修改一个 volatile 变量时,会触发 Bus Locking 或 MESI Invalidate,使其他 CPU 中对应的缓存行失效,从而保证可见性。
2.3 volatile 典型使用场景
场景一:状态标志位
// 最经典的 volatile 用法 —— 停止标志
private volatile boolean running = true;
public void shutdown() { running = false; }
public void doWork() {
while (running) {
// 处理业务逻辑...
}
}
场景二:DCL 双重检查锁定(单例模式)
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(); // ⚠️ 这里不是原子操作!
}
}
}
return instance;
}
}
为什么 DCL 中 必须 volatile?因为 分三步:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
步骤 2 和 3 可能被重排序为 1→3→2。如果线程 A 执行完 3 但还没执行 2,此时线程 B 进入第一次检查发现 ,直接返回了一个未初始化完成的对象!volatile 的禁止重排恰好解决这个问题。
场景三:volatile 数组/引用的局限性
private volatile int[] arr = new int[10];
arr[0] = 1; // ❌ 这不是 volatile 写!只保证了 arr 引用的可见性
// arr[0] 的修改对其他线程不一定可见
volatile 只保证引用本身的可见性,不保证数组元素或对象内部字段的可见性。需要细粒度控制时请用 Atomic*Array 或加锁。
三、synchronized 锁机制与锁升级
3.1 synchronized 的三种用法
// 1. 修饰实例方法 —— 锁的是当前实例对象(this)
public synchronized void method() { ... }
// 2. 修饰静态方法 —— 锁的是当前类的 Class 对象
public static synchronized void staticMethod() { ... }
// 3. 修饰代码块 —— 锁的是指定对象
synchronized (lockObject) { ... }
3.2 对象头与 Mark Word
synchronized 的锁信息存储在对象头(Object Header)的 Mark Word 中。在 64 位 JVM 中,Mark Word 的结构如下:
| 锁状态 | 25 bit | 31 bit | 1 bit | 4 bit | 1 bit | 2 bit |
|---|---|---|---|---|---|---|
| 无锁 | hashcode | age | 0 | 01 | unused | 无锁(01) |
| 偏向锁 | thread ID | epoch | 1 | 01 | unused | 偏向(01) |
| 轻量级锁 | 指向栈中 Lock Record 的指针 | — | 00 | 轻量锁(00) | ||
| 重量级锁 | 指向互斥量(Monitor)的指针 | — | 10 | 重量锁(10) | ||
| GC 标记 | 空 | — | 11 | GC(11) |
3.3 锁升级过程
JDK 6 之后,synchronized 引入了偏向锁和轻量级锁,大多数情况下不需要直接进入重量级锁(操作系统层面的互斥锁)。升级路径如下:
无锁 ──(第一次获取)──▶ 偏向锁 ──(竞争出现)──▶ 轻量级锁 ──(自旋失败)──▶ 重量级锁
│ │ │ │
│ CAS 设置 ThreadID │ CAS 替换 Mark Word │ 自旋+CAS │ OS mutex
│ 无竞争时零开销 │ 少量竞争 │ 短时间竞争 │ 上下文切换
偏向锁(Biased Locking)
当一个线程反复进入同一个同步块时,JVM 认为这个锁"偏向"该线程:
- 通过 CAS 将 Mark Word 中的 ThreadID 设为当前线程 ID
- 后续该线程进入/退出同步块无需任何 CAS 操作
- 只有当其他线程尝试获取这把锁时,才撤销偏向锁
生产建议:如果锁竞争确实激烈(如 Web 服务器的请求处理),偏向锁反而增加撤销开销。JDK 15 默认关闭偏向锁(
-XX:-UseBiasedLocking),JDK 18 正式移除。
轻量级锁(Lightweight Locking)
偏向锁撤销后,或两个线程交替获取锁时,升级为轻量级锁:
- 在当前线程的栈帧中创建 Lock Record(锁记录)
- 将对象的 Mark Word CAS 替换为指向 Lock Record 的指针
- 解锁时再将 Lock Record 中的旧 Mark Word CAS 回去
- 如果 CAS 失败(说明有竞争),自旋重试若干次
重量级锁(Heavyweight Locking)
自旋超过阈值(默认 10 次,可通过 -XX:PreBlockSpin 调整)后,升级为重量级锁:
- 向操作系统申请互斥量(mutex)
- 未获取到锁的线程阻塞(BLOCKED 状态),涉及用户态/内核态切换
- 被唤醒的线程需要重新竞争锁(非公平)
3.4 锁消除与锁粗化(JIT 编译器优化)
JIT 编译器在运行时还会对 synchronized 做两种智能优化:
// 锁消除:锁的对象是局部变量,不可能被外部线程访问
public void lockElimination() {
StringBuffer sb = new StringBuffer(); // StringBuffer 方法都是 synchronized 的
sb.append("a").append("b"); // JIT 发现 sb 是局部的 → 直接去掉所有锁
}
// 锁粗化:循环体内频繁加锁解锁 → 合并为一次
for (int i = 0; i < 1000; i++) {
synchronized (lock) { ... } // JIT 可能合并为 synchronized(lock) { for(...) {...} }
}
3.5 synchronized vs volatile 对比
| 维度 | synchronized | volatile |
|---|---|---|
| 可见性 | ✅ | ✅ |
| 原子性 | ✅(复合操作也保证) | ❌ |
| 有序性 | ✅(互斥即有序) | ✅(内存屏障) |
| 阻塞 | 会阻塞(重量级锁时) | 不会阻塞 |
| 性能开销 | 较高(尤其重量级锁) | 很低 |
| 适用场景 | 复合原子操作、临界区保护 | 状态标志、单次读/写 |
四、CAS 与原子操作
4.1 什么是 CAS(Compare And Swap)
CAS 是一条 CPU 原子指令,其语义等价于以下伪代码:
// CAS(V, Expected, NewValue)
// 如果内存值 V == 期望值 Expected,则将 V 更新为 NewValue,否则什么都不做
boolean CAS(V, Expected, NewValue) {
if (V == Expected) {
V = NewValue;
return true; // 成功
}
return false; // 失败,V 已被其他线程修改
}
CAS 是乐观锁的核心思想:不先加锁,而是假设没有冲突,更新时再检验。冲突了就重试。
4.2 CAS 底层实现
Java 中 CAS 通过 sun.misc.Unsafe 类调用本地方法,最终映射为 x86 的 cmpxchg 指令:
// AtomicInteger.getAndIncrement() 的核心调用:
// unsafe.compareAndSwapInt(this, valueOffset, expect, update)
//
// 最终汇编:
// lock cmpxchg [addr], reg ← lock 前缀保证多核原子性
| 平台 | CAS 指令 |
|---|---|
| x86/x64 | lock cmpxchg |
| ARM | LDREX / STREX |
| RISC-V | LR / SC |
4.3 ABA 问题及解决方案
CAS 检查的是"值有没有变",但如果值从 A → B → A 变回来,CAS 会误以为没变过:
线程1 读取值 A(准备 CAS 为 C)
线程2 把 A 改成 B
线程3 又把 B 改回 A
线程1 执行 CAS(A, C) → 成功!但实际上中间已经变了两次
解决方案:加版本号
// AtomicStampedReference —— 带版本号的引用
AtomicStampedReference<String> asr = new AtomicStampedReference<>("A", 0);
// CAS 时同时比较值和版本号
int[] stampHolder = new int[1];
String ref = asr.get(stampHolder); // ref="A", stamp=0
boolean ok = asr.compareAndSet(ref, "C", stampHolder[0], stampHolder[0] + 1);
// 期望引用 新引用 期望版本 新版本
// AtomicMarkableReference —— 带布尔标记的引用(简化版,只关心"变没变过")
AtomicMarkableReference<String> amr = new AtomicMarkableReference<>("A", false);
amr.compareAndSet("A", "B", false, true); // 同时比较引用和标记
4.4 CAS 自旋与性能问题
CAS 失败后会自旋重试,在高竞争下可能导致:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| CPU 空转 | 大量线程自旋消耗 CPU | LongAdder(分段 CAS)、限制自旋次数 |
| ABA 问题 | 值被改回原样导致误判 | AtomicStampedReference |
| 只能保证单个变量 | 无法对多个变量做原子 CAS | 用 synchronized 或封装成对象 |
// LongAdder —— 高并发下的计数器替代方案(JDK 8+)
LongAdder counter = new LongAdder();
counter.increment(); // 内部分散到多个 Cell,减少 CAS 竞争
long sum = counter.sum(); // 汇总所有 Cell
五、Atomic 原子类体系
JUC 提供了一套基于 CAS 实现的原子类,位于 java.util.concurrent.atomic 包下。
5.1 体系总览
java.util.concurrent.atomic
├── 基本类型
│ ├── AtomicInteger
│ ├── AtomicLong
│ └── AtomicBoolean
├── 数组类型
│ ├── AtomicIntegerArray
│ ├── AtomicLongArray
│ └── AtomicReferenceArray<E>
├── 引用类型
│ ├── AtomicReference<V>
│ ├── AtomicStampedReference<V>
│ └── AtomicMarkableReference<V>
├── 字段更新器
│ ├── AtomicIntegerFieldUpdater<T>
│ ├── AtomicLongFieldUpdater<T>
│ └── AtomicReferenceFieldUpdater<T,V>
└── 累加器(JDK 8+)
├── LongAdder
├── DoubleAdder
├── LongAccumulator
└── DoubleAccumulator
5.2 AtomicInteger 核心源码解析
以最常用的 getAndIncrement() (即 i++ 的原子版)为例:
public final int getAndIncrement() {
return getAndAdd(1);
}
public final int getAndAdd(int delta) {
// U = sun.misc.Unsafe 实例
// valueOffset = 字段 value 在对象内存中的偏移量
return U.getAndAddInt(this, VALUE, delta);
}
// Unsafe.getAndAddInt —— CAS 自旋循环
public final int getAndAddInt(Object o, long offset, int delta) {
int prev;
int next;
do {
prev = getIntVolatile(o, offset); // 读取当前值(volatile 读)
next = prev + delta; // 计算新值
} while (!compareAndSwapInt(o, offset, prev, next)); // CAS 更新,失败则重试
return prev; // 返回旧值
}
这个
do-while循环就是自旋锁的本质——不断尝试直到成功。在低竞争时几乎无开销;高竞争时会浪费 CPU 周期。
5.3 字段更新器:给现有类"打补丁"
// 场景:想对一个第三方类的某个 volatile 字段做原子操作,
// 但不能修改它的源码
class Student {
volatile int score;
}
AtomicIntegerFieldUpdater<Student> updater =
AtomicIntegerFieldUpdater.newUpdater(Student.class, "score");
Student s = new Student();
updater.set(s, 80); // 相当于 s.score = 80(原子写)
int old = updater.getAndIncrement(s); // 原子的 s.score++
字段更新器要求目标字段必须是 volatile 的,且反射能访问到。它本质上是对指定偏移量做 CAS 操作,是一种"侵入性更低"的原子操作方式。
六、AQS(AbstractQueuedSynchronizer)框架
6.1 AQS 设计思想
AQS 是 JUC 并发包的基石——ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock 全部基于 AQS 构建。
Doug Lea(JUC 作者)的设计哲学:将同步状态的管理与线程的排队机制分离。
AQS 核心架构:
┌─────────────────────────────────────┐
│ AbstractQueuedSynchronizer │
│ │
│ ┌──────────┐ ┌───────────────┐ │
│ │ state │ │ CLH 队列 │ │
│ │ (int, │ │ (FIFO 双向链表)│ │
│ │ volatile)│ │ Node:thread │ │
│ └──────────┘ │ + waitStatus │ │
│ └───────────────┘ │
│ │
│ tryAcquire() / tryRelease() ← 子类实现 │
│ tryAcquireShared() / tryReleaseShared() │
└─────────────────────────────────────┘
6.2 核心数据结构
state —— 同步状态
// AQS 内部就是一个 volatile int
private volatile int state;
// 提供三个方法操作(都是 CAS 原子操作)
protected final int getState() { return state; }
protected final void setState(int newState) { state = newNode; }
protected final boolean compareAndSetState(int expect, int update) {
// 底层调用 Unsafe.compareAndSwapInt
}
不同同步器对 state 的含义不同:
| 同步器 | state 含义 |
|---|---|
| ReentrantLock | 0=未锁,>0=重入次数 |
| CountDownLatch | 剩余计数 |
| Semaphore | 可用许可数 |
| ReentrantReadWriteLock | 高16位=读锁持有数,低16位=写锁重入数 |
CLH 队列 —— 等待线程队列
AQS 内部维护一个 FIFO 双向链表(CLH 变体),每个节点是一个 Node:
static final class Node {
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 等待的线程
int waitStatus; // 等待状态
// waitStatus 取值:
// 0: 初始状态
// SIGNAL(-1): 后继节点需要被唤醒
// CANCELLED(1): 节点已取消
// CONDITION(-2): 节点在 Condition 队列中
// PROPAGATE(-3): 共享模式下传播释放信号
}
6.3 独占模式 vs 共享模式
AQS 支持两种资源获取模式:
| 模式 | 方法 | 特点 | 代表实现 |
|---|---|---|---|
| 独占(Exclusive) | acquire/release | 同一时刻只有一个线程持有 | ReentrantLock |
| 共享(Shared) | acquireShared/releaseShared | 多个线程可同时持有 | CountDownLatch, Semaphore |
6.4 AQS 获取锁的完整流程(独占模式)
acquire(arg)
│
├─ tryAcquire(arg) ──成功──▶ 直接返回(无需入队)
│ ──失败──▶ addWaiter(Node.EXCLUSIVE) // 尾插法加入队列
│ │
│ ▼
│ acquireQueued(node, arg)
│ │
│ ├─ 前驱是 head → 再 tryAcquire 一次(抢锁机会)
│ │ └─ 成功 → 设置自己为 head,返回
│ │ └─ 失败 → shouldParkAfterFailedAcquire()
│ │ └─ 检查前驱 waitStatus
│ │ └─ SIGNAL → park(挂起,等待 unpark)
│ │ ── CANCELLED → 跳过前驱,重试
│ │
│ └─ 被唤醒后 → 再次循环 tryAcquire
│
└─ 如果中断 → selfInterrupt() 补上中断标记
6.5 ReentrantLock 如何基于 AQS 实现
ReentrantLock 内部有两个 AQS 子类:
// NonfairSync —— 非公平锁
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1)) // 先直接 CAS 抢锁!
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 抢不到再走 AQS 流程
}
protected final boolean tryAcquire(int acquires) {
// 非公平:不管队列里有没有等待的线程,直接抢
...
}
}
// FairSync —— 公平锁
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
// 公平:检查是否有前驱节点,有则放弃抢锁
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // ★ 关键:看队列里有没有人
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
...
}
}
非公平锁 vs 公平锁的性能差异:非公平锁允许"插队"(新来的线程可以直接 CAS 抢锁,不用排队),减少了线程上下文切换,吞吐量更高。公平锁严格按照 FIFO 顺序获取锁,避免了饥饿,但吞吐量低 10%~20%。默认是非公平锁——因为大多数场景更看重吞吐量而非绝对公平。
可重入实现
ReentrantLock 的"可重入"体现在 tryAcquire 中判断当前线程是否已经是锁的持有者:
// 简化的可重入逻辑
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { ... return true; }
}
else if (current == getExclusiveOwnerThread()) { // 已经是持有者 → 重入
int nextc = c + acquires;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc); // 直接设置 state,无需 CAS(因为只有持有者才能到这里)
return true;
}
return false;
}
6.6 AQS Condition(条件变量)
Condition 是 AQS 提供的条件等待/通知机制,替代了 Object.wait()/notify():
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 队列不满条件
Condition notEmpty = lock.newCondition(); // 队列不空条件
// 生产者
lock.lock();
try {
while (count == bufferSize) notFull.await(); // 队列满 → 等待
enqueue(item);
notEmpty.signal(); // 通知消费者
} finally { lock.unlock(); }
// 消费者
lock.lock();
try {
while (count == 0) notEmpty.await(); // 队列空 → 等待
item = dequeue();
notFull.signal(); // 通知生产者
} finally { lock.unlock(); }
Condition 的优势:一个 Lock 可以创建多个 Condition,实现精准唤醒(只唤醒某一类等待线程),而 Object.notify() 是随机唤醒一个。
七、JUC 并发工具类
7.1 CountDownLatch —— 倒计时门闩
// 场景:主线程等待 N 个子线程全部完成后汇总
CountDownLatch latch = new CountDownLatch(3); // 初始计数 = 3
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
// 执行任务...
} finally {
latch.countDown(); // 计数 -1
}
}).start();
}
latch.await(); // 阻塞等待,直到计数归零
System.out.println("所有任务完成!");
原理:内部基于 AQS 共享模式,countDown() 就是 releaseShared(1)(state - 1),await() 就是 acquireSharedInterruptibly(1)(state != 0 则阻塞)。
7.2 Semaphore —— 信号量
// 场景:限流 —— 控制同时访问某资源的线程数不超过 N
Semaphore semaphore = new Semaphore(5); // 5 个许可
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可(阻塞式)
// 访问受限资源...
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 归还许可
}
}).start();
}
原理:同样基于 AQS 共享模式,state 表示剩余许可数。acquire() 就是 tryAcquireShared(state > 0 则 CAS 减 1),release() 就是 releaseShared(state + 1 并唤醒等待线程)。
7.3 CyclicBarrier —— 循环栅栏
// 场景:N 个线程互相等待,全部到达后一起继续(可重复使用)
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有选手到达终点,开始颁奖!");
});
for (int i = 0; i < 3; i++) {
final int no = i + 1;
new Thread(() -> {
System.out.println("选手 " + no + " 出发...");
try {
Thread.sleep((long)(Math.random() * 3000));
System.out.println("选手 " + no + " 到达终点");
barrier.await(); // 等待其他线程
System.out.println("选手 " + no + " 继续下一轮比赛");
} catch (Exception e) { e.printStackTrace(); }
}).start();
}
与 CountDownLatch 的关键区别:CyclicBarrier 可以复用(reset() 后重新计数),且支持 barrierAction(所有线程到达后执行的回调)。底层用的是 ReentrantLock + Condition,不是 AQS。
7.4 三大工具类对比
| 维度 | CountDownLatch | Semaphore | CyclicBarrier |
|---|---|---|---|
| 计数方向 | 递减(归零后不可重置) | 可增可减 | 递增(到达后自动重置) |
| 是否可复用 | ❌ 一次性 | ✅ | ✅ 循环使用 |
| 底层实现 | AQS 共享模式 | AQS 共享模式 | ReentrantLock + Condition |
| 等待方 | 一个/多个线程等待 | 获取许可的线程 | 所有参与线程互相等待 |
| 典型场景 | 等待 N 个任务完成 | 限流、资源池 | 多阶段并行计算 |
八、并发容器
8.1 ConcurrentHashMap(JDK 7 vs JDK 8)
这是面试最高频的并发容器,JDK 7 和 JDK 8 的实现完全不同:
| 维度 | JDK 7 | JDK 8 |
|---|---|---|
| 数据结构 | Segment + HashEntry(分段锁) | Node 数组 + CAS + synchronized |
| 锁粒度 | 每个 Segment 一把锁(默认 16 段) | 每个桶头节点一把锁(更细) |
| 锁实现 | ReentrantLock | synchronized + CAS |
| size 计算 | 累加各 Segment 的 count(不加锁可能不准) | baseCount + CounterCell[] 分散计数 |
| 扩容 | Segment 独立扩容 | 多线程协助扩容(transfer) |
JDK 8 ConcurrentHashMap 核心源码
// put 操作简化流程
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ... hash 计算 ...
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // ① 表空 → 初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node(...))) // ② 桶空 → CAS 插入
break; // 无锁成功
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // ③ 正在扩容 → 协助迁移
else {
synchronized (f) { // ④ 桶非空 → 加锁(锁住头节点)
// 链表遍历 or 红黑树操作...
}
}
}
// ... addCount() → 可能触发扩容 ...
}
JDK 8 的设计非常精妙:大部分情况用无锁 CAS,只在哈希冲突时才降级为 synchronized 锁住单个桶。而且 synchronized 锁的是桶的头节点(Node 对象),不是整个 Segment,粒度比 JDK 7 更细。
8.2 CopyOnWriteArrayList
// 写时复制列表 —— 读完全无锁,写时复制整个底层数组
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 读操作:直接读数组,无锁(适合读多写少)
String s = list.get(0);
// 写操作:复制新数组,修改后替换引用
list.add("hello"); // 内部:Arrays.copyOf() → 新数组添加 → 替换
| 优点 | 缺点 |
|---|---|
| 读操作性能极高(无锁) | 写操作需要复制整个数组,O(n) 开销 |
| 读写完全不互斥 | 数据一致性最终一致(不是强一致) |
| 适合配置列表、事件监听器等读多写少场景 | 不适合写频繁的场景 |
8.3 BlockingQueue 家族
| 实现类 | 数据结构 | 有界? | 特点 |
|---|---|---|---|
| ArrayBlockingQueue | 数组 | ✅ 有界 | 有界容量,必须指定大小 |
| LinkedBlockingQueue | 链表 | 可选有界 | 默认 Integer.MAX_VALUE(近似无界) |
| PriorityBlockingQueue | 堆 | 无界 | 按优先级出队 |
| SynchronousQueue | 无缓冲 | 容量为 0 | 直接传递,不存储元素 |
| DelayQueue | 堆 | 无界 | 元素只有到期后才能取走 |
| LinkedTransferQueue | 链表 | 无界 | transfer() 方法可直接交给消费者 |
// 生产者-消费者经典模式
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(128);
// 生产者
queue.put(task); // 队列满时阻塞
// 消费者
Task task = queue.take(); // 队列空时阻塞
8.4 并发容器选择指南
需要 Map?
├── 读多写少 → ConcurrentHashMap(首选)
├── 要求强一致 → Collections.synchronizedMap()(性能差但简单)
└── 需要排序 → ConcurrentSkipListMap(跳表,有序)
需要 List?
├── 读多写少 → CopyOnWriteArrayList
└── 读写均衡 → Collections.synchronizedList() 或手动加锁
需要 Queue?
├── 有界队列 → ArrayBlockingQueue
├── 无界队列 → LinkedBlockingQueue
├── 直接传递 → SynchronousQueue
└── 延迟任务 → DelayQueue
九、实战避坑清单
以下每一条都是线上血泪教训的总结:
| # | 坑点 | 正确做法 |
|---|---|---|
| 1 | 用 synchronized 包裹耗时 IO 操作 |
缩小同步范围,或用读写锁分离 |
| 2 | 在 synchronized 块中调用外部方法(可能死锁) |
同步块内只操作自己的对象 |
| 3 | volatile 当锁用(以为能保证 i++ 原子性) |
复合操作用 Atomic 或 synchronized |
| 4 | 忘记在 finally 中 unlock() |
始终用 try-finally 或 try-with-resources |
| 5 | CountDownLatch 记得调用 countDown() |
放在 finally 块中,防止异常导致死等 |
| 6 | 创建线程池用 Executors 工厂方法 |
手动创建 ThreadPoolExecutor,指定合理参数 |
| 7 | 共享可变对象作为 HashMap 的 Key | Key 必须是不可变的(immutable) |
| 8 | 在构造函数中启动新线程(this 引用逃逸) | 不要在构造函数中暴露 this 给其他线程 |
| 9 | 忽略中断异常(catch 后吞掉) | 至少恢复中断状态:Thread.currentThread().interrupt() |
| 10 | 用 wait()/notify() 而不用 Condition |
Condition 支持多条件队列、精确唤醒,功能更强 |
总结
本文从 JMM 内存模型出发,梳理了 Java 并发编程的完整知识链路:
JMM(可见性/原子性/有序性)
↓
volatile(内存屏障 + MESI)←→ synchronized(锁升级:偏向→轻量→重量)
↓
CAS(CPU 原子指令)+ ABA 问题
↓
Atomic 原子类(基于 CAS + 自旋)
↓
AQS 框架(state + CLH 队列 + 独占/共享模式)
↓
├── ReentrantLock(可重入 + 公平/非公平 + Condition)
├── CountDownLatch(一次性倒计时)
├── Semaphore(信号量限流)
└── CyclicBarrier(循环栅栏)
↓
并发容器(ConcurrentHashMap / CopyOnWriteArrayList / BlockingQueue)
掌握这套体系,你就拥有了分析和解决绝大多数 Java 并发问题的能力。下一步建议结合 JConsole / VisualVM / Arthas 观察线程状态,在实践中加深理解。