Java 锁机制全景指南
要理解锁,必须先理解 JMM(Java Memory Model)。锁的本质是解决多线程之间的**可见性**和**有序性**问题。 每个线程有自己的工作内存(CPU 缓存 + 寄存器),变量存储在主内存中。线程操作变量时,先从主内存拷贝到工
一、锁的分类总览
Java 并发控制体系
├── 按语法层面
│ ├── synchronized 关键字(JVM 层面实现,C++ 底层)
│ └── Lock 接口实现(JDK 代码层面实现)
│ ├── ReentrantLock
│ ├── ReentrantReadWriteLock
│ └── StampedLock
│
├── 按锁的状态(synchronized 特有)
│ ├── 偏向锁(JDK 15+ 默认废弃)
│ ├── 轻量级锁
│ └── 重量级锁
│ (这是一个单向升级过程,不是三种独立的锁)
│
├── 按获取策略
│ ├── 悲观锁(认为会发生竞争,直接加锁)
│ │ ├── synchronized
│ │ ├── ReentrantLock
│ │ └── ReadWriteLock 的写锁
│ └── 乐观锁(认为不会竞争,CAS 尝试)
│ ├── AtomicInteger 等 Atomic 类
│ ├── StampedLock 的乐观读
│ ├── volatile(广义上的轻量级同步)
│ └── LongAdder(分段 CAS)
│
├── 按是否可重入
│ ├── 可重入锁(同一线程可多次获取)
│ │ ├── synchronized
│ │ ├── ReentrantLock
│ │ └── ReentrantReadWriteLock
│ └── 不可重入锁(StampedLock)
│
├── 按公平性
│ ├── 公平锁(按等待队列顺序获取)
│ └── 非公平锁(新线程可"插队"尝试获取)
│
├── 按共享性
│ ├── 排他锁(独占锁,一次只有一个线程持有)
│ └── 共享锁(多个线程可同时持有,如读锁)
│
└── 无锁并发
├── volatile + CAS
├── ThreadLocal(线程封闭)
├── 不可变对象(Immutable)
└── CopyOnWrite(写时复制)
二、Java 内存模型(JMM)—— 锁的底层基础
要理解锁,必须先理解 JMM(Java Memory Model)。锁的本质是解决多线程之间的可见性和有序性问题。
2.1 主内存与工作内存
线程A ── 工作内存(本地缓存) ──┐
├── 主内存(共享内存,堆中的变量)
线程B ── 工作内存(本地缓存) ──┘
每个线程有自己的工作内存(CPU 缓存 + 寄存器),变量存储在主内存中。线程操作变量时,先从主内存拷贝到工作内存,修改后再写回主内存。
这导致两个问题:
- 可见性:线程 A 修改了变量,线程 B 不一定看得到(缓存不一致)
- 有序性:编译器和 CPU 会对指令重排序,导致执行顺序和代码顺序不一致
2.2 happens-before 原则
JMM 通过 happens-before 原则定义了哪些操作对其他线程是可见的:
- 程序次序规则:同一线程内,前面的操作 happens-before 后面的操作
- 锁规则:unlock 操作 happens-before 后续的 lock 操作
- volatile 规则:对 volatile 变量的写 happens-before 后续对该变量的读
- 传递性:A happens-before B,B happens-before C,则 A happens-before C
- 线程启动规则:Thread.start() happens-before 该线程内的任何操作
- 线程终止规则:线程的所有操作 happens-before Thread.join() 返回
核心记忆:synchronized 和 volatile 都保证了 happens-before 关系,从而保证了可见性。
2.3 内存屏障(Memory Barrier)
CPU 层面的有序性保障。JVM 通过内存屏障禁止指令重排序:
| 屏障类型 | 作用 |
|---|---|
| LoadLoad Barrier | 确保 load1 的数据先于 load2 被读取 |
| StoreStore Barrier | 确保 store1 的数据先于 store2 写入内存 |
| LoadStore Barrier | 确保 load1 的数据先于 store2 被写入 |
| StoreLoad Barrier | 确保 store1 的数据先于 load2 被读取(最昂贵的屏障) |
volatile 的写操作前后会插入 StoreStore 和 StoreLoad 屏障;读操作前后会插入 LoadLoad 和 LoadStore 屏障。
三、volatile 关键字
volatile 是最轻量级的同步机制,和锁密切相关。
3.1 两个核心保证
- 可见性:一个线程修改了 volatile 变量,其他线程立即可见(通过内存屏障刷新到主内存)
- 有序性:禁止指令重排序(通过内存屏障)
不保证原子性:volatile int i; i++ 不是原子操作(读-改-写三步)。
3.2 使用场景
// 场景一:状态标志位(典型用法)
volatile boolean running = true;
public void stop() {
running = false; // 其他线程立即可见
}
public void run() {
while (running) {
// do work
}
}
// 场景二:双重检查锁定(DCL)单例
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(不加锁)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(加锁后)
instance = new Singleton();
}
}
}
return instance;
}
}
为什么 DCL 需要 volatile?
instance = new Singleton() 不是原子操作,分三步:
- 分配内存空间
- 初始化对象
- 将引用指向分配的内存地址
如果发生指令重排(2 和 3 交换),线程 A 执行到第 3 步但第 2 步还没完成,线程 B 进入第一次检查发现 instance != null,直接返回了一个未初始化完成的对象。volatile 禁止了这种重排序。
3.3 volatile vs synchronized
| 维度 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ | ✅ |
| 有序性 | ✅(内存屏障) | ✅ |
| 原子性 | ❌ | ✅ |
| 线程阻塞 | ❌ 不阻塞 | ✅ 会阻塞 |
| 适用场景 | 状态标志、DCL 单例 | 复合操作、临界区 |
四、synchronized 关键字
4.1 使用方式
// 修饰实例方法 —— 锁的是当前实例对象 (this)
public synchronized void method() { ... }
// 修饰静态方法 —— 锁的是 Class 对象
public static synchronized void method() { ... }
// 修饰代码块 —— 锁的是括号里指定的对象
public void method() {
synchronized (someObject) { ... }
}
4.2 底层原理 —— Monitor 对象
synchronized 的底层依赖的是 对象头中的 Mark Word 和 Monitor 对象。
每个 Java 对象在内存中的布局:
对象 = 对象头(Mark Word + Klass Pointer) + 实例数据 + 对齐填充
Mark Word 是核心。它只有 64 位(8 字节),根据锁的状态不同,这 64 位的含义会动态变化:
无锁状态: [unused:25][hashCode:31][分代年龄:4][偏向锁标志=0][锁标志位=01]
偏向锁: [线程ID:54][epoch:2][分代年龄:4][偏向锁标志=1][锁标志位=01]
轻量级锁: [指向锁记录的指针(62位)][锁标志位=00]
重量级锁: [指向 Monitor 的指针(62位)][锁标志位=10]
GC 标记: [锁标志位=11]
锁标志位是最后两位:01=无锁/偏向,00=轻量级,10=重量级,11=GC标记。
Monitor 是什么?
Monitor 是一个 C++ 对象(ObjectMonitor),定义在 HotSpot 源码的 objectMonitor.hpp 中。内部核心字段:
ObjectMonitor {
_owner // 指向当前持有锁的线程
_WaitSet // 调用了 wait() 的线程等待队列
_EntryList // 等待获取锁的线程等待队列
_count // 记录该线程获取锁的累计次数
_recursions // 锁的重入次数
_cxq // 竞争队列(新来的线程先进这里)
}
当多个线程竞争锁时,没抢到的线程进入 _EntryList 或 _cxq 队列阻塞等待。持有锁的线程调用 wait() 会释放锁并进入 _WaitSet。
Monitor 的获取和释放依赖操作系统级别的 Mutex Lock,涉及用户态 ↔ 内核态切换,这是重量级锁性能开销的根源。
4.3 锁升级过程
JDK 6 之前 synchronized 是重量级锁,性能差;JDK 6 之后引入锁优化,有了锁升级的概念。
stateDiagram-v2
[*] --> 无锁状态: 对象创建
无锁状态 --> 偏向锁: 第一个线程访问同步块
偏向锁 --> 轻量级锁: 出现竞争(第二个线程来)
轻量级锁 --> 重量级锁: 自旋超过阈值 / CAS 持续失败
note right of 无锁状态
没有同步需求
Mark Word 存 hashCode、分代年龄等
end note
note right of 偏向锁
只有一个线程访问
Mark Word 中记录线程 ID
后续进入只需比对 ID,无需 CAS
最轻量的同步状态
JDK 15+ 默认废弃
end note
note right of 轻量级锁
多线程交替执行,无激烈竞争
用 CAS + 自旋尝试获取
线程栈帧中存 Lock Record
不阻塞,消耗 CPU 忙等待
end note
note right of 重量级锁
竞争激烈,多个线程同时抢
进入系统内核态,线程阻塞挂起
依赖 OS 的 Mutex Lock
开销最大
end note
偏向锁
当只有一个线程访问同步资源时,JVM 会在对象头的 Mark Word 里"偏向"这个线程,记录其线程 ID。后续这个线程再次进入时,只需检查 Mark Word 中的线程 ID 是否匹配,不需要任何 CAS 或原子操作。
偏向锁的撤销:当其他线程尝试获取偏向锁时,需要在全局安全点(SafePoint)撤销偏向:
- 暂停持有偏向锁的线程
- 检查该线程是否还活着、是否还在同步块内
- 如果已退出同步块,将 Mark Word 恢复为无锁状态
- 如果还在同步块内,升级为轻量级锁
JDK 15 之后,偏向锁默认被废弃(Deprecated)。原因是现代高并发场景下,偏向锁的撤销成本(SafePoint 停顿)往往高于它带来的收益。
轻量级锁
当第二个线程来竞争时,偏向锁升级为轻量级锁。核心机制:
- 线程在自己的栈帧里创建一个 Lock Record
- 把对象的 Mark Word 拷贝到 Lock Record 中
- 通过 CAS 尝试把 Mark Word 替换为指向 Lock Record 的指针
- CAS 成功 → 获得锁;CAS 失败 → 自旋(循环重试)
自旋不是无限的,JDK 6 默认 10 次,JDK 7 后改为自适应自旋 —— 根据历史表现动态调整自旋次数。超过自旋阈值后,升级为重量级锁。
重量级锁
竞争激烈的最终状态。没抢到锁的线程进入 _EntryList 队列 阻塞挂起(不消耗 CPU)。线程调度依赖操作系统的 Mutex Lock,涉及用户态 ↔ 内核态切换。
锁能降级吗?
不能。 锁升级是单向的:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
五、LockSupport —— 线程阻塞/唤醒的底层原语
LockSupport 是 JDK 并发包的最底层线程阻塞工具,AQS 内部的线程挂起和唤醒就是基于它实现的。
// 阻塞当前线程
LockSupport.park();
// 唤醒指定线程
LockSupport.unpark(thread);
核心特点:
- 基于 Permit(许可证)机制:每个线程有一个 Permit(0 或 1)。
park()消耗 Permit(如果为 0 就阻塞),unpark()发放 Permit(最多 1 个,不累加) - 不需要先获取再释放:不像
wait()/notify()必须在 synchronized 块中使用,park()/unpark()可以在任何地方调用 - 不会抛出 InterruptedException:响应中断但不抛异常,通过
Thread.interrupted()判断
wait/notify 和 park/unpark 的区别:
| 维度 | wait/notify | park/unpark |
|---|---|---|
| 所属类 | Object | LockSupport |
| 使用前提 | 必须在 synchronized 块中 | 无限制 |
| 唤醒方式 | notify(随机唤醒一个)/ notifyAll(全部) | unpark(精确指定线程) |
| 顺序依赖 | 必须先 wait 后 notify,否则信号丢失 | 先 unpark 再 park 也不会阻塞(Permit 已存在) |
AQS 中的线程挂起最终调用的就是 LockSupport.park(),释放锁时通过 LockSupport.unpark() 唤醒后继线程。
六、ReentrantLock —— JDK 层面的锁
6.1 与 synchronized 的对比
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 层面(C++ 实现) | JDK 代码层面(Java 实现) |
| 公平性 | 只能非公平 | 可选公平/非公平 |
| 获取/释放 | 自动(进入/退出代码块) | 手动(lock/unlock),必须在 finally 中释放 |
| 中断响应 | 不可中断 | 可中断(lockInterruptibly()) |
| 超时获取 | 不支持 | 支持(tryLock(timeout)) |
| 条件变量 | 一个内置 wait/notify | 支持多个 Condition,更灵活 |
| 性能 | JDK 6 后差距不大 | 激烈竞争时略优 |
6.2 使用示例
ReentrantLock lock = new ReentrantLock();
// ReentrantLock lock = new ReentrantLock(true); // 公平锁
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须在 finally 中释放,防止死锁
}
6.3 AQS 深度解析
AQS(AbstractQueuedSynchronizer)是 Doug Lea 写的,是整个 JUC 并发包的基石。ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock 都基于它。
核心就两样东西:
1. 一个 state 变量(volatile int)
表示同步状态。ReentrantLock 中:
state = 0→ 无锁state > 0→ 有线程持有锁,值就是重入次数
2. 一个 CLH 双向队列(FIFO)
存放等待获取锁的线程节点:
CLH 队列结构:
[head(dummy)] <--> [Node1: thread-A] <--> [Node2: thread-B] <--> [Node3: thread-C] <--> [tail]
每个 Node 包含:
thread:封装的线程waitStatus:节点状态CANCELLED = 1:线程被取消/超时/中断SIGNAL = -1:后继线程需要被唤醒CONDITION = -2:线程在 Condition 队列中等待PROPAGATE = -3:共享模式下传播释放信号
prev/next:前驱/后继指针
获取锁的完整流程(非公平模式)
public void lock() {
sync.lock(); // NonfairSync.lock()
}
// NonfairSync.lock()
final void lock() {
// 第一步:直接尝试 CAS 获取锁("插队")
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 第二步:CAS 失败,进入 AQS 的 acquire 流程
acquire(1);
}
// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 再次尝试获取
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 失败则入队
selfInterrupt();
}
acquireQueued 自旋逻辑:
final boolean acquireQueued(final Node node, int arg) {
for (;;) {
final Node p = node.predecessor(); // 获取前驱节点
if (p == head && tryAcquire(arg)) { // 前驱是头节点,尝试获取
setHead(node); // 获取成功,自己成为头节点
p.next = null; // 帮助 GC
return false;
}
if (shouldParkAfterFailedAcquire(p, node)) // 检查是否需要阻塞
parkAndCheckInterrupt(); // LockSupport.park() 阻塞
}
}
核心要点:
- 线程入队后不会立即阻塞,而是先自旋检查前驱状态
- 只有前驱节点的
waitStatus是SIGNAL(-1)时,当前线程才会被 park - 被唤醒后,继续自旋尝试获取锁
释放锁的流程
public void unlock() {
sync.release(1);
}
// AbstractQueuedSynchronizer.release()
public final boolean release(int arg) {
if (tryRelease(arg)) { // state 减 1,如果 state=0 返回 true
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}
unparkSuccessor 会从 _EntryList 中找到第一个有效的后继节点,调用 LockSupport.unpark() 唤醒它。
公平锁 vs 非公平锁
// 非公平锁:直接 CAS 尝试获取("插队")
protected final boolean tryAcquire(int acquires) {
if (compareAndSetState(0, acquires)) { ... }
}
// 公平锁:先检查队列,有等待线程就排队
protected final boolean tryAcquire(int acquires) {
if (hasQueuedPredecessors()) return false; // 队列中有人就排队
if (compareAndSetState(0, acquires)) { ... }
}
公平锁的性能通常比非公平锁差,因为每次获取锁都要检查队列,增加了线程上下文切换。
可重入的实现
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 当前线程已经持有锁,重入!
int nextc = c + acquires;
setState(nextc); // 不需要 CAS,只有持有锁的线程能改
return true;
}
return false;
}
关键点:只有持有锁的线程才能修改 state(重入时),所以重入时不需要 CAS,直接 setState() 即可。
七、Condition 条件变量
ReentrantLock 支持多 Condition,比 synchronized 的单一 wait/notify 灵活得多。
7.1 为什么需要多个 Condition?
synchronized 的 wait/notify 只能有一个等待集合,所有等待条件混在一起。用 notifyAll() 唤醒所有线程,但只有满足条件的线程才能继续执行,其他的再次等待 —— 这就是信号丢失和虚假唤醒问题。
Condition 允许创建多个等待队列,每个条件一个队列,精确唤醒:
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 队列未满的条件
Condition notEmpty = lock.newCondition(); // 队列非空的条件
// 生产者
lock.lock();
try {
while (queue.size() == MAX_SIZE) {
notFull.await(); // 在 notFull 队列中等待
}
queue.add(item);
notEmpty.signal(); // 精确唤醒在 notEmpty 队列中等待的消费者
} finally {
lock.unlock();
}
// 消费者
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 在 notEmpty 队列中等待
}
item = queue.remove();
notFull.signal(); // 精确唤醒在 notFull 队列中等待的生产者
} finally {
lock.unlock();
}
7.2 Condition 底层实现
每个 Condition 内部维护一个条件队列(单向链表),和 AQS 的同步队列是两个不同的队列。
ConditionObject {
firstWaiter --> [Node: thread-A] --> [Node: thread-B] --> null
lastWaiter
}
调用 await() 时:
- 将当前线程封装为 Node,加入条件队列尾部
- 完全释放锁(state 重置为 0)
LockSupport.park()阻塞
调用 signal() 时:
- 将条件队列中的第一个节点转移到 AQS 同步队列尾部
- 线程会在后续被 unpark 唤醒
- 唤醒后重新获取锁(从
await()方法返回)
八、ReadWriteLock / ReentrantReadWriteLock
读写分离锁,核心思想:读读不互斥,读写互斥,写写互斥。
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReadWriteLock.ReadLock readLock = rwLock.readLock();
ReadWriteLock.WriteLock writeLock = rwLock.writeLock();
// 读操作
readLock.lock();
try { ... } finally { readLock.unlock(); }
// 写操作
writeLock.lock();
try { ... } finally { writeLock.unlock(); }
底层原理:
AQS 的 state 被一分为二:
- 高 16 位:记录读锁的获取次数
- 低 16 位:记录写锁的重入次数
state: [读锁计数(16位)][写锁计数(16位)]
读锁通过 sharedCount() 取高 16 位,写锁通过 exclusiveCount() 取低 16 位。
适用场景:读多写少。典型场景是缓存系统,大量读操作少量更新。
如果写操作频繁,读锁频繁让位给写锁,性能反而不如直接用排他锁。
锁降级:ReentrantReadWriteLock 支持写锁降级为读锁(先获取写锁,再获取读锁,然后释放写锁),但不支持读锁升级为写锁(会导致死锁)。
九、StampedLock(JDK 8 引入)
ReadWriteLock 的升级版,性能更好,但 API 更复杂。
三种模式:
- 写锁(Writing Lock):排他锁,类似 ReentrantLock
- 读锁(Reading Lock):共享锁,类似 ReadLock
- 乐观读锁(Optimistic Read Lock):不上锁,读完后用
validate()检查期间有没有写操作
StampedLock lock = new StampedLock();
// 写锁
long stamp = lock.writeLock();
try { ... } finally { lock.unlockWrite(stamp); }
// 悲观读锁
long stamp = lock.readLock();
try { ... } finally { lock.unlockRead(stamp); }
// 乐观读锁(最常用,性能最好)
long stamp = lock.tryOptimisticRead();
// 读取数据
if (lock.validate(stamp)) {
// 数据有效,读取期间没有写操作
} else {
// 验证失败,升级为悲观读锁重试
stamp = lock.readLock();
try {
// 重新读取数据
} finally {
lock.unlockRead(stamp);
}
}
为什么性能更好?
乐观读锁不需要 CAS,不需要阻塞线程,只是读取一个 stamp 值(volatile long)。如果读取期间没有写操作,就完成了"无锁化"的并发读取。只有在验证失败时才需要升级。
注意:StampedLock 不可重入。同一个线程获取写锁后再次获取,会死锁。
十、乐观锁 —— CAS 机制
10.1 CAS(Compare And Swap)
三个操作数:内存值 V、预期值 A、新值 B。
如果 V == A,则 V = B,返回 true
如果 V != A,则什么都不做,返回 false
底层依赖 CPU 的原子指令:x86 → cmpxchg,ARM → ldrex/strex。
10.2 CAS 的问题
ABA 问题:
线程1 读取值 A
线程2 把 A 改为 B
线程3 把 B 改回 A
线程1 CAS 检查,发现值还是 A,成功 —— 但实际上中间发生过变化
解决:AtomicStampedReference,带上版本号。
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(1, 0);
ref.compareAndSet(1, 2, 0, 1); // 需要同时匹配值和版本号
自旋开销:CAS 失败后会自旋重试,如果竞争激烈,大量线程空转消耗 CPU。
只能保证一个变量的原子操作:多个变量需要原子操作时,CAS 搞不定。
十·五、死锁、活锁与饥饿
十·五·一、死锁(Deadlock)
什么是死锁?
两个或多个线程互相等待对方持有的锁,形成环形依赖,所有线程都永远阻塞。
// 经典死锁场景
Object lockA = new Object();
Object lockB = new Object();
// 线程1:先拿 A,再拿 B
new Thread(() -> {
synchronized (lockA) {
sleep(100); // 让线程2先拿到 lockB
synchronized (lockB) {
System.out.println("Thread1 got both locks");
}
}
}, "Thread-1").start();
// 线程2:先拿 B,再拿 A
new Thread(() -> {
synchronized (lockB) {
sleep(100);
synchronized (lockA) {
System.out.println("Thread2 got both locks");
}
}
}, "Thread-2").start();
// 结果:两个线程互相等待,永久阻塞
// Thread-1 持有 lockA,等待 lockB
// Thread-2 持有 lockB,等待 lockA
死锁的四个必要条件(Coffman 条件)
四个条件同时满足才会死锁,破坏其中任意一个即可预防死锁:
| 条件 | 含义 | 破坏方式 |
|---|---|---|
| 互斥条件 | 锁是排他的,一个资源只能被一个线程持有 | 无法破坏(锁的本质) |
| 占有并等待 | 线程持有锁的同时请求其他锁 | 一次性获取所有需要的锁 |
| 不可抢占 | 线程持有的锁不能被其他线程强行剥夺 | 使用 tryLock(timeout) 超时释放 |
| 循环等待 | 线程之间形成环形等待链 | 统一加锁顺序 |
Java 中 JVM 对死锁的态度
synchronized:JVM 不自动检测死锁(运行时不会主动打破)。死锁的线程会永久阻塞,只能通过外部工具发现。
ReentrantLock:提供了打破死锁的手段:
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
// 方式一:tryLock + 超时 —— 破坏"不可抢占"条件
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
// 获取了两把锁,执行操作
} finally {
lockB.unlock();
}
} else {
// 获取 lockB 超时,放弃 lockA,稍后重试
}
} finally {
lockA.unlock();
}
}
// 方式二:lockInterruptibly —— 响应中断
lockA.lockInterruptibly();
try {
lockB.lockInterruptibly();
try {
// 执行操作
} finally {
lockB.unlock();
}
} finally {
lockA.unlock();
}
// 外部线程可以调用 thread.interrupt() 中断等待
死锁检测 —— 如何排查
方式一:jstack(最常用)
jstack <pid>
输出中会明确标记死锁信息:
Found one Java-level deadlock:
=============================
"Thread-2":
waiting to lock monitor 0x00007f... (object 0x00000007..., java.lang.Object),
which is held by "Thread-1"
"Thread-1":
waiting to lock monitor 0x00007f... (object 0x00000007..., java.lang.Object),
which is held by "Thread-2"
方式二:JConsole / VisualVM
图形化工具的"线程"面板中,死锁线程会标红显示。
方式三:编程方式检测(Java 1.6+)
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
System.out.println("检测到死锁!涉及线程数:" + deadlockedThreads.length);
for (long tid : deadlockedThreads) {
ThreadInfo info = threadBean.getThreadInfo(tid);
System.out.println("死锁线程:" + info.getThreadName());
}
}
死锁预防的最佳实践
- 统一加锁顺序(最常用,破坏"循环等待"条件):
// 不好的做法:加锁顺序不一致
void transfer(Account from, Account to, int amount) {
synchronized (from) { synchronized (to) { ... } }
}
// 线程A: transfer(a, b) → 先锁 a 再锁 b
// 线程B: transfer(b, a) → 先锁 b 再锁 a → 死锁!
// 好的做法:按固定顺序加锁(比如按 ID 大小)
void transfer(Account from, Account to, int amount) {
Account first = from.id < to.id ? from : to;
Account second = from.id < to.id ? to : from;
synchronized (first) { synchronized (second) { ... } }
}
// 无论调用顺序如何,加锁顺序始终是 a → b,不会死锁
- 减少锁的粒度:用细粒度锁代替粗粒度锁,降低同时持有多个锁的概率
- 使用 tryLock 超时:获取锁失败时回退,稍后重试
- 缩短持有锁的时间:临界区尽量短,不要在持有锁的时候做 I/O 操作
十·五·二、活锁(Livelock)
什么是活锁?
线程没有阻塞,一直在运行,但一直在重试、回退、让出资源,导致无法取得进展。
// 活锁场景:两个线程互相谦让
while (!task.done()) {
if (otherThread.isWorking()) {
Thread.sleep(10); // 让给对方
// 对方也在做同样的事,双方都在 sleep → 永远执行不完
} else {
task.doWork();
}
}
和死锁的区别:死锁是线程不干活(永久阻塞),活锁是线程干不完活(一直在忙但没进展)。
解决:引入随机退避时间,打破"对称行为"。
// 加随机等待,打破对称性
if (otherThread.isWorking()) {
Thread.sleep(new Random().nextInt(100)); // 随机退避
}
十·五·三、饥饿(Starvation)
什么是饥饿?
某个线程长期得不到执行,因为资源总是被其他线程抢占。
常见原因:
- 低优先级线程被高优先级线程一直压制
- 不公平锁下,某个线程总是被"插队"
- 某个线程等待一个永远不释放的锁(本质上是活锁/死锁的变种)
解决:使用公平锁,或者调整线程优先级。
十·五·四、死锁 vs 活锁 vs 饥饿 对比
| 维度 | 死锁 | 活锁 | 饥饿 |
|---|---|---|---|
| 线程状态 | 阻塞(不消耗 CPU) | 运行中(消耗 CPU) | 等待中(基本不消耗 CPU) |
| 表现 | 永久卡住 | 一直在忙但没进展 | 长期得不到执行 |
| 根因 | 环形等待 | 对称退让策略 | 资源分配不均 |
| 解决 | 统一加锁顺序、超时释放 | 随机退避 | 公平调度 |
| JVM 是否自动检测 | ❌ | ❌ | ❌ |
十一、LongAdder vs AtomicLong —— 高并发计数优化
11.1 AtomicLong 的问题
AtomicLong 在高并发场景下,大量线程竞争同一个变量,CAS 反复失败、重试,性能急剧下降。
AtomicLong counter = new AtomicLong(0);
// 1000 个线程同时 incrementAndGet()
// 大量 CAS 失败,自旋重试,CPU 空转
11.2 LongAdder 的设计
LongAdder 采用分段 CAS(类似 ConcurrentHashMap 的分段锁思想):
LongAdder {
base: long
cells: Cell[] // 多个 Cell 数组,每个 Cell 有一个 value
}
Cell {
@sun.misc.Contended // 缓存行对齐,防止伪共享
volatile long value;
}
当多个线程并发累加时:
- 先尝试 CAS 更新
base - 如果失败(有竞争),将当前线程 hash 到一个 Cell 上,更新该 Cell 的 value
- 最终结果 =
base + sum(cells[i].value)
LongAdder adder = new LongAdder();
// 多线程并发
adder.increment(); // 分散到不同 Cell,减少竞争
// 获取结果
long total = adder.sum();
性能对比:高并发场景下 LongAdder 的性能远超 AtomicLong(可达 3-5 倍)。代价是 sum() 不是实时精确的(并发更新时可能有不一致),适合计数统计场景。
11.3 伪共享(False Sharing)与 @Contended
LongAdder 的 Cell 使用了 @sun.misc.Contended 注解,这是为了解决伪共享问题。
什么是伪共享?
CPU 缓存不是按字节加载的,而是按缓存行(Cache Line,通常 64 字节)加载。如果两个独立变量在同一个缓存行中,一个线程修改了变量 A,会导致整个缓存行失效,其他 CPU 核心必须重新从内存加载 —— 即使它们只关心变量 B。
缓存行(64 字节)
[变量 A: 8字节][变量 B: 8字节][填充: 48字节]
↑ 线程1 修改 A → 整个缓存行失效 → 线程2 读 B 也要重新加载
@Contended 通过在变量前后填充 128 字节,确保该变量独占一个缓存行:
[填充: 128字节][变量 A: 8字节][填充: 128字节]
↑ 线程1 修改 A,不影响其他变量
JDK 8 起需要加 JVM 参数
-XX:-RestrictContended才能生效。
十二、AQS 的其他应用
12.1 CountDownLatch —— 倒计时门闩
CountDownLatch latch = new CountDownLatch(3);
// 主线程等待
latch.await(); // 阻塞,直到 count 为 0
// 子线程完成工作后
latch.countDown(); // count 减 1
底层:AQS 的 state 初始化为 3(或其他值)。countDown() 通过 CAS 将 state 减 1,当 state 变为 0 时,唤醒所有在 await() 上等待的线程。CountDownLatch 不可重用,state 到 0 后不能再重置。
典型场景:主线程等待多个子线程完成初始化后再继续执行。
12.2 CyclicBarrier —— 循环屏障
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程已到位,开始执行");
});
// 每个线程到达屏障点
barrier.await(); // 阻塞,直到 3 个线程都到达
// 3 个线程同时被唤醒,继续执行
底层:基于 ReentrantLock + Condition 实现。所有线程到达后触发 signalAll()。CyclicBarrier 可重用,一轮完成后可以开始下一轮。
与 CountDownLatch 的区别:
| 维度 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 可重用 | ❌ 不可重用 | ✅ 可循环使用 |
| 等待方 | 一个线程等待多个线程完成 | 所有线程互相等待 |
| 底层 | AQS(共享模式) | ReentrantLock + Condition |
| 典型场景 | 主线程等子线程 | 多线程分阶段协作 |
12.3 Semaphore —— 信号量
Semaphore semaphore = new Semaphore(5); // 最多允许 5 个线程同时执行
semaphore.acquire(); // 获取一个 permit,没有则阻塞
try {
// 临界区(最多 5 个线程并发)
} finally {
semaphore.release(); // 释放一个 permit
}
底层:AQS 的共享模式。state 初始化为 permits 数量。acquire() 将 state 减 1(state < 0 时阻塞),release() 将 state 加 1。
十三、并发集合的锁机制
13.1 ConcurrentHashMap 的演进
JDK 7:分段锁(Segment)
ConcurrentHashMap {
Segment[] segments; // 16 个段,每个段是一把锁
}
Segment extends ReentrantLock {
HashEntry[] table;
}
将数据分成 16 个 Segment,每个 Segment 独立加锁。最多支持 16 个线程并发访问不同 Segment。缺点是并发度固定为 16,无法动态调整。
JDK 8:CAS + synchronized
ConcurrentHashMap {
Node[] table; // 和 HashMap 一样的数组
}
每个桶(bucket)独立加锁:
- put 时,如果桶为空,CAS 插入
- 如果桶不为空(有冲突),synchronized 锁住桶的头节点(Record Lock)
关键变化:
- 去掉了 Segment,直接用
synchronized (f)锁住桶的头节点 - 锁粒度从 Segment(16 个)细化到每个桶(N 个),并发度大幅提升
- 利用 JVM 对 synchronized 的优化(锁升级),性能更好
// JDK 8 ConcurrentHashMap.putVal() 简化版
final V putVal(K key, V value, boolean onlyIfAbsent) {
Node<K,V> f; int n, i, fh;
// 第一步:数组未初始化,先初始化
// 第二步:桶为空,CAS 插入头节点
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
// 第三步:桶不为空,锁住头节点
synchronized (f) {
// 链表插入或红黑树插入
}
}
13.2 CopyOnWriteArrayList —— 无锁读
核心思想:写时复制(Copy-On-Write)。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 写操作加锁
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制一份
newElements[len] = e;
setArray(newElements); // 引用指向新数组
return true;
} finally {
lock.unlock();
}
}
public E get(int index) {
return get(getArray(), index); // 读操作不加锁,直接读旧数组
}
写操作加锁并复制整个数组,读操作不加锁读取旧数组的快照。
适用场景:读多写极少,比如配置列表、白名单。如果写操作频繁,每次都要复制整个数组,性能极差且内存占用大。
13.3 阻塞队列的锁实现
ArrayBlockingQueue:一把锁 + 两个 Condition
final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
public void put(E e) throws InterruptedException {
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await(); // 队列满,等待
enqueue(e);
notEmpty.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
LinkedBlockingQueue:两把锁(takeLock + putLock),读和写分离
private final ReentrantLock takeLock = new ReentrantLock(); // 取数据的锁
private final ReentrantLock putLock = new ReentrantLock(); // 放数据的锁
put 和 take 分别用独立的锁,允许同时进行 put 和 take 操作,比 ArrayBlockingQueue 的并发度更高。
十四、线程封闭 —— 另一种无锁并发思路
14.1 ThreadLocal
每个线程有自己独立的变量副本,不存在共享,自然就不需要锁。
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
// 每个线程用自己的 SimpleDateFormat,线程安全
String formatted = DATE_FORMAT.get().format(new Date());
底层原理:
每个 Thread 对象内部有一个 ThreadLocalMap:
Thread {
ThreadLocalMap threadLocals;
}
ThreadLocalMap {
Entry[] table; // key 是 ThreadLocal 的弱引用,value 是对象
}
ThreadLocal.get() 实际上是从当前线程的 threadLocals 中以自己的 ThreadLocal 对象为 key 取值。
内存泄漏问题:
ThreadLocalMap 的 key 是弱引用,GC 时可能被回收。但 value 是强引用,如果 ThreadLocal 被 GC 回收了,key 变为 null,但 value 还在 —— 如果线程是线程池中的核心线程(长期存活),value 就永远不会被回收。
解决:用完后必须手动 remove()。
try {
DATE_FORMAT.get().format(...);
} finally {
DATE_FORMAT.remove(); // 防止内存泄漏
}
十五、JVM 的锁优化
JIT 编译器在运行时会自动做一些锁优化,开发者无需手动处理。
15.1 锁消除(Lock Elimination)
JIT 发现某个同步块只被单线程访问,直接去掉锁。
public void method() {
StringBuilder sb = new StringBuilder(); // 局部变量,线程私有
synchronized (sb) { ... } // JIT 发现 sb 不会被其他线程访问,消除锁
}
JIT 通过逃逸分析(Escape Analysis)判断对象是否会被其他线程访问。
15.2 锁粗化(Lock Coarsening)
频繁对同一对象加锁解锁,JIT 会把多次加锁合并为一次。
// 原始代码:循环内反复加锁
for (int i = 0; i < 100; i++) {
synchronized (obj) { ... }
}
// JIT 优化后:锁提到循环外面
synchronized (obj) {
for (int i = 0; i < 100; i++) { ... }
}
15.3 自旋锁与自适应自旋
JDK 6:固定自旋次数(默认 10 次)。
JDK 7 及以后:自适应自旋 —— 根据历史表现动态调整。
十六、分布式锁(Java 生态常见实现)
16.1 基于 Redis
// 核心思路:SET key value NX PX milliseconds
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
// 获取成功
}
// 释放锁:用 Lua 脚本保证原子性
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then "
+ "return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(requestId));
问题:
- 锁过期但业务没执行完 → 其他线程获取了锁,并发安全问题
- 主从切换导致锁丢失
解决:Redisson 框架的看门狗(WatchDog)机制自动续期。
16.2 基于 Zookeeper
创建临时顺序节点:/locks/order_0000000001
节点创建成功 → 获取锁
节点已存在 → Watch 上一个节点,等待释放
优势:强一致性,不会丢失锁。劣势:性能不如 Redis。
十七、常见面试追问
Q1:synchronized 和 ReentrantLock 你选哪个?
看场景。大多数场景 synchronized 够用且更安全。需要公平锁、超时获取、可中断、多 Condition 时选 ReentrantLock。
Q2:AQS 为什么用 CLH 队列?
CLH 被改造为双向阻塞队列,每个节点只关注前驱节点的状态,轮询效率高。双向链表方便节点的前后驱操作和取消操作。
Q3:重量级锁和轻量级锁的线程调度有什么区别?
轻量级锁是自旋(忙等待,消耗 CPU 但不阻塞)。重量级锁是阻塞挂起(不消耗 CPU,但涉及用户态/内核态切换)。
Q4:为什么 ReentrantLock 必须在 finally 中释放?
如果临界区抛出异常,unlock() 不会执行,锁永远不会被释放,后续线程全部阻塞。synchronized 由 JVM 自动释放,即使异常也不会泄漏锁。
Q5:锁升级过程中,Mark Word 怎么变的?
无锁: [hashCode:31][分代年龄:4][偏向=0][锁标志=01]
偏向锁: [线程ID:54][epoch:2][分代年龄:4][偏向=1][锁标志=01]
轻量级: [指向 Lock Record 的指针(62位)][锁标志=00]
重量级: [指向 Monitor 的指针(62位)][锁标志=10]
Q6:CAS 的 ABA 问题怎么解决?
AtomicStampedReference,带上版本号。
Q7:volatile 能保证原子性吗?DCL 单例为什么需要 volatile?
不能。volatile i++ 不是原子操作。DCL 中 instance = new Singleton() 不是原子的(分配内存 → 初始化 → 赋值),volatile 禁止了指令重排序,防止其他线程拿到未初始化完成的对象。
Q8:LockSupport.park() 和 Object.wait() 有什么区别?
park() 不需要在 synchronized 块中使用,可以精确唤醒指定线程,先 unpark 再 park 也不会丢失信号。wait() 必须在 synchronized 块中,notify 是随机唤醒,必须先 wait 后 notify。
Q9:LongAdder 为什么比 AtomicLong 快?
分段 CAS:将竞争分散到多个 Cell 上,每个线程 hash 到一个 Cell 独立更新,减少 CAS 冲突。代价是 sum() 不是实时精确的。
Q10:ConcurrentHashMap JDK 7 和 JDK 8 的区别?
JDK 7 用 Segment 分段锁(16 个段,每段一把 ReentrantLock)。JDK 8 去掉了 Segment,用 CAS + synchronized 锁每个桶(头节点),锁粒度更细,并发度更高。
Q11:CountDownLatch 和 CyclicBarrier 的区别?
CountDownLatch 不可重用,一个线程等待多个线程完成。CyclicBarrier 可循环使用,所有线程互相等待。
十八、速查表
| 锁/机制 | 底层实现 | 可重入 | 公平性 | 适用场景 |
|---|---|---|---|---|
| synchronized | Monitor(C++) | 是 | 非公平 | 大多数场景,简单安全 |
| ReentrantLock | AQS(Java) | 是 | 可选 | 需要公平/超时/中断/多条件 |
| ReadWriteLock | AQS(Java) | 是 | 可选 | 读多写少 |
| StampedLock | 自定义队列 | 否 | 非公平 | 高性能读,乐观读场景 |
| volatile | 内存屏障 | N/A | N/A | 状态标志、DCL 单例 |
| CAS/Atomic | CPU 原子指令 | N/A | N/A | 单变量原子操作 |
| LongAdder | 分段 CAS + @Contended | N/A | N/A | 高并发计数统计 |
| ThreadLocal | 线程隔离 Map | N/A | N/A | 线程私有数据,无锁 |
| CopyOnWrite | 写时复制 + ReentrantLock | N/A | N/A | 读多写极少的列表 |
| Redis 分布式锁 | Redis SET NX PX | N/A | N/A | 跨 JVM 互斥 |
| Zookeeper 锁 | 临时顺序节点 | N/A | 公平 | 强一致性分布式锁 |
AQS 应用汇总
| 工具 | AQS 模式 | state 含义 |
|---|---|---|
| ReentrantLock | 独占(Exclusive) | 锁的重入次数 |
| ReadWriteLock | 独占 + 共享 | 高16位读计数,低16位写计数 |
| CountDownLatch | 共享(Shared) | 倒计数剩余值 |
| Semaphore | 共享(Shared) | 剩余 permit 数量 |
| CyclicBarrier | ReentrantLock + Condition | 基于 Lock 实现,非直接 AQS |