Java 并发编程核心:volatile、synchronized、CAS 与 AQS 原理解析

线程池解决了”怎么管理线程”的问题,但线程池本身并不保证线程安全——它只是把任务交给线程执行。真正保证多线程正确协作的,是 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 经典的可见性问题示例
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 为了提升性能,会对指令进行重排序,分为三种:

1
2
3
4
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++ 仍然不是原子操作
1
2
3
4
5
6
7
8
9
10
11
12
// 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)内存语义层面 —— 内存屏障

1
2
3
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 LockingMESI Invalidate,使其他 CPU 中对应的缓存行失效,从而保证可见性。

2.3 volatile 典型使用场景

场景一:状态标志位

1
2
3
4
5
6
7
8
9
10
// 最经典的 volatile 用法 —— 停止标志
private volatile boolean running = true;

public void shutdown() { running = false; }

public void doWork() {
while (running) {
// 处理业务逻辑...
}
}

场景二:DCL 双重检查锁定(单例模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 中 instance 必须 volatile?因为 new Singleton() 分三步:

  1. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存地址

步骤 2 和 3 可能被重排序为 1→3→2。如果线程 A 执行完 3 但还没执行 2,此时线程 B 进入第一次检查发现 instance != null,直接返回了一个未初始化完成的对象!volatile 的禁止重排恰好解决这个问题。

场景三:volatile 数组/引用的局限性

1
2
3
private volatile int[] arr = new int[10];
arr[0] = 1; // ❌ 这不是 volatile 写!只保证了 arr 引用的可见性
// arr[0] 的修改对其他线程不一定可见

volatile 只保证引用本身的可见性,不保证数组元素或对象内部字段的可见性。需要细粒度控制时请用 Atomic*Array 或加锁。


三、synchronized 锁机制与锁升级

3.1 synchronized 的三种用法

1
2
3
4
5
6
7
8
// 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 引入了偏向锁轻量级锁,大多数情况下不需要直接进入重量级锁(操作系统层面的互斥锁)。升级路径如下:

1
2
3
4
无锁 ──(第一次获取)──▶ 偏向锁 ──(竞争出现)──▶ 轻量级锁 ──(自旋失败)──▶ 重量级锁
│ │ │ │
│ 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 做两种智能优化:

1
2
3
4
5
6
7
8
9
10
// 锁消除:锁的对象是局部变量,不可能被外部线程访问
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 原子指令,其语义等价于以下伪代码:

1
2
3
4
5
6
7
8
9
// 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 指令:

1
2
3
4
5
// 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
2
3
4
线程1 读取值 A(准备 CAS 为 C)
线程2 把 A 改成 B
线程3 又把 B 改回 A
线程1 执行 CAS(A, C) → 成功!但实际上中间已经变了两次

解决方案:加版本号

1
2
3
4
5
6
7
8
9
10
11
12
// 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 或封装成对象
1
2
3
4
// 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 体系总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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++ 的原子版)为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 字段更新器:给现有类”打补丁”

1
2
3
4
5
6
7
8
9
10
11
12
// 场景:想对一个第三方类的某个 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 作者)的设计哲学:将同步状态的管理与线程的排队机制分离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
AQS 核心架构:
┌─────────────────────────────────────┐
│ AbstractQueuedSynchronizer │
│ │
│ ┌──────────┐ ┌───────────────┐ │
│ │ state │ │ CLH 队列 │ │
│ │ (int, │ │ (FIFO 双向链表)│ │
│ │ volatile)│ │ Node:thread │ │
│ └──────────┘ │ + waitStatus │ │
│ └───────────────┘ │
│ │
│ tryAcquire() / tryRelease() ← 子类实现 │
│ tryAcquireShared() / tryReleaseShared() │
└─────────────────────────────────────┘

6.2 核心数据结构

state —— 同步状态

1
2
3
4
5
6
7
8
9
// 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:

1
2
3
4
5
6
7
8
9
10
11
12
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 获取锁的完整流程(独占模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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 中判断当前线程是否已经是锁的持有者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 简化的可重入逻辑
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():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 —— 倒计时门闩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 场景:主线程等待 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 —— 信号量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 场景:限流 —— 控制同时访问某资源的线程数不超过 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 —— 循环栅栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 场景: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 核心源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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

1
2
3
4
5
6
7
8
// 写时复制列表 —— 读完全无锁,写时复制整个底层数组
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() 方法可直接交给消费者
1
2
3
4
5
6
7
8
// 生产者-消费者经典模式
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(128);

// 生产者
queue.put(task); // 队列满时阻塞

// 消费者
Task task = queue.take(); // 队列空时阻塞

8.4 并发容器选择指南

1
2
3
4
5
6
7
8
9
10
11
12
13
14
需要 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-finallytry-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 并发编程的完整知识链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JMM(可见性/原子性/有序性)

volatile(内存屏障 + MESI)←→ synchronized(锁升级:偏向→轻量→重量)

CAS(CPU 原子指令)+ ABA 问题

Atomic 原子类(基于 CAS + 自旋)

AQS 框架(state + CLH 队列 + 独占/共享模式)

├── ReentrantLock(可重入 + 公平/非公平 + Condition)
├── CountDownLatch(一次性倒计时)
├── Semaphore(信号量限流)
└── CyclicBarrier(循环栅栏)

并发容器(ConcurrentHashMap / CopyOnWriteArrayList / BlockingQueue)

掌握这套体系,你就拥有了分析和解决绝大多数 Java 并发问题的能力。下一步建议结合 JConsole / VisualVM / Arthas 观察线程状态,在实践中加深理解。