46-锁的内存语义
引言
前几篇我们分别讲了volatile和final的内存语义。volatile用内存屏障保证可见性和有序性,final用StoreStore/LoadLoad屏障保证初始化安全。但Java并发中还有一类最常用的同步手段——锁。
锁不只是"互斥"工具,它同样承担内存同步职责。无论是内置的synchronized,还是java.util.concurrent.locks.ReentrantLock,甚至是基于CAS的原子类,它们都有明确的内存语义:加锁时刷新工作内存,解锁时写回主内存。理解锁的内存语义,才能明白为什么"synchronized块内的写对下一个获取锁的线程可见",以及为什么ReentrantLock能替代synchronized。
本篇从synchronized的内存语义切入,扩展到ReentrantLock(AQS的volatile state)、CAS的内存语义,最后用happens-before关系对比三种同步方式。
synchronized的内存语义
synchronized是Java最基础的锁机制,底层由JVM的monitorenter/monitorexit指令和对象头的Mark Word实现。但它除了互斥,还有明确的内存同步语义。
加锁与解锁的语义
JMM对synchronized的内存语义定义如下:
- 加锁(monitorenter / 进入synchronized块):清空当前线程的工作内存(本地缓存副本),相当于强制后续读取从主内存重新加载
- 解锁(monitorexit / 退出synchronized块):把工作内存中所有修改过的值写回主内存,相当于执行StoreStore + StoreLoad屏障
更直观的理解:
线程A(持锁并修改数据):
┌──────────────────────────┐
│ synchronized(lock) { │ ← 加锁:清空工作内存,后续读从主内存取
│ read sharedData │ (获取锁时的最新值)
│ sharedData = newValue │ (在工作内存修改)
│ } │ ← 解锁:写回主内存(StoreStore + StoreLoad)
└──────────────────────────┘
线程B(随后获取同一把锁):
┌──────────────────────────┐
│ synchronized(lock) { │ ← 加锁:清空工作内存
│ read sharedData │ (从主内存读到线程A写入的newValue)
│ } │
└──────────────────────────┘
这和volatile的"写刷新主内存、读强制重载"机制本质相同,只是作用范围更大——volatile作用于单个字段,synchronized作用于整个临界区内的所有共享变量。
为什么是"清空工作内存"
synchronized加锁时"清空工作内存"的设计,目的是保证临界区内的读取拿到最新值。如果不清空,线程可能用工作内存中的旧副本,导致读到过期数据。
具体来说,JMM规定:
这样,synchronized块就成了一个**“内存屏障块”**——进入时强制与主内存同步,退出时强制把修改同步回去。
代码示例
// 适用 JDK 8/11/17
public class SynchronizedMemoryDemo {
private int data = 0;
private boolean flag = false;
private final Object lock = new Object();
// 线程A:加锁修改
public void writer() {
synchronized (lock) { // 加锁:清空工作内存
data = 42; // 修改1
flag = true; // 修改2
} // 解锁:写回主内存(data=42, flag=true)
}
// 线程B:加锁读取
public void reader() {
synchronized (lock) { // 加锁:清空工作内存,重新从主内存读
// 因为A的解锁 hb B的加锁(监视器锁规则)
// 且A解锁时把data和flag写回了主内存
// 所以这里必然读到 data=42, flag=true
System.out.println("data=" + data + ", flag=" + flag);
}
}
}
注意:即使data和flag不是volatile,只要它们在synchronized块内被读写,且所有访问都通过同一把锁,可见性就有保证。但如果某线程在不加锁的情况下读data,就不保证可见性——synchronized的内存语义只覆盖"同一把锁"的加锁-解锁序列。
synchronized的happens-before关系
结合上一篇的happens-before规则,synchronized的内存语义可以用一条规则概括:监视器锁规则——对同一把锁的unlock操作happens-before后续的lock操作。
推演:
线程A: [临界区内写] → unlock
│
│ happens-before(监视器锁规则)
↓
线程B: lock → [临界区内读]
传递性扩展:线程A临界区内的所有写操作(程序顺序规则:写 hb unlock),通过监视器锁规则(unlock hb lock),传递到线程B临界区内的所有读操作(程序顺序规则:lock hb 读)。所以线程A临界区内的所有写,对线程B临界区内的所有读可见。
ReentrantLock的内存语义
ReentrantLock是java.util.concurrent.locks包提供的显式锁,功能比synchronized更灵活(可中断、可超时、公平/非公平)。它在内存语义上与synchronized等价——加锁和 unlocks具有相同的可见性保证。实现机制则完全不同。
AQS与volatile state
ReentrantLock基于**AQS(AbstractQueuedSynchronizer)**实现。AQS的核心是一个volatile int state字段,表示同步状态:
// AQS核心字段(简化展示)
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // volatile!
protected final int getState() { return state; }
protected final void setState(int newState) { state = newState; }
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
}
state是volatile,这是ReentrantLock内存语义的锚点:
- 加锁:通过CAS把state从0改为1(非公平)或排队后CAS(公平)。CAS成功即获取锁
- 解锁:把state从1改为0(或递减重入计数)
因为state是volatile,加锁时的CAS(volatile写)和解锁时的setState(volatile写)都遵循volatile的内存语义。配合happens-before规则,这就建立了与synchronized等价的可见性保证。
lock/unlock的happens-before链
ReentrantLock的可见性保证,可以通过happens-before规则推导:
// 适用 JDK 8/11/17
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockMemoryDemo {
private int data = 0;
private final ReentrantLock lock = new ReentrantLock();
// 线程A:加锁修改
public void writer() {
lock.lock(); // 操作1:CAS state 0→1(volatile写)
try {
data = 42; // 操作2:普通写
} finally {
lock.unlock(); // 操作3:setState(0)(volatile写)
}
}
// 线程B:加锁读取
public void reader() {
lock.lock(); // 操作4:CAS state(volatile读+写)
try {
// 推理:
// 程序顺序:操作2 hb 操作3(同一线程内)
// volatile规则:操作3(写state) hb 操作4(读state)
// —— 因为unlock写state,lock读state,state是volatile
// 程序顺序:操作4 hb 读data
// 传递性:操作2 hb 读data → data=42必然可见
System.out.println(data);
} finally {
lock.unlock();
}
}
}
关键链路:
所以线程B必然读到data=42。这条推理链和synchronized完全对称——只是synchronized靠监视器锁规则,ReentrantLock靠volatile规则。
AQS的内存语义设计
AQS巧妙地用volatile state作为"内存同步锚点",这是一个经典设计:
- state的volatile写(unlock时)把当前线程在临界区内的所有修改刷新到主内存
- state的volatile读(lock时)强制后续读取看到最新值
这等价于:unlock具有"StoreStore + StoreLoad"的语义(volatile写后屏障),lock具有"LoadLoad + LoadStore"的语义(volatile读后屏障)。与synchronized的"解锁写回、加锁清空"语义一致。
AQS的其他同步器(Semaphore、CountDownLatch、CyclicBarrier)也都基于volatile state,因此具有相同的内存语义——释放/获取操作之间有happens-before关系。
CAS的内存语义
CAS(Compare-And-Swap)是java.util.concurrent.atomic包原子类的基础操作。它不仅保证原子性,还有明确的内存语义。
CAS的volatile读写语义
JMM规定:CAS操作同时具有volatile读和volatile写的内存语义。
具体来说,AtomicInteger.compareAndSet(int expect, int update):
- 读当前值(类似volatile读)
- 比较是否等于expect
- 如果相等,写入update(类似volatile写)
- 整个过程原子
这个"volatile读写"语义意味着:
- CAS之前的普通读写,不会重排到CAS之后(volatile写的StoreStore效果)
- CAS之后的普通读写,不会重排到CAS之前(volatile读的LoadLoad/LoadStore效果)
- CAS的结果对后续操作立即可见(volatile写的StoreLoad效果)
底层实现:Unsafe + lock cmpxchg
CAS在x86上通过lock cmpxchg指令实现。lock前缀的作用上一篇讲过——触发缓存一致性协议,保证原子性和可见性。cmpxchg是"比较并交换"指令,配合lock前缀成为原子操作。
// AtomicInteger.compareAndSet 的底层(简化)
public final boolean compareAndSet(int expect, int update) {
// 调用 Unsafe.compareAndSwapInt
// 最终生成 lock cmpxchg 指令
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
lock cmpxchg同时完成"读-比较-写"三步,且是原子的,同时带有lock前缀的内存屏障效果。所以CAS的内存语义是volatile读写的超集——既原子又可见。
代码示例
// 适用 JDK 8/11/17
import java.util.concurrent.atomic.AtomicInteger;
public class CASMemoryDemo {
private AtomicInteger atomicValue = new AtomicInteger(0);
private int normalData = 0;
// 线程A:先写普通变量,再CAS
public void writer() {
normalData = 42; // 操作1:普通写
boolean success = atomicValue.compareAndSet(0, 1); // 操作2:CAS
// CAS具有volatile写的语义
// 程序顺序:操作1 hb 操作2(CAS前的写不能重排到CAS后)
}
// 线程B:先CAS(看到A的CAS结果),再读普通变量
public void reader() {
int val = atomicValue.get(); // 操作3:volatile读
if (val == 1) {
// CAS(volatile写) hb volatile读 → 操作2 hb 操作3
// 传递性:操作1 hb 操作3 hb 操作4
System.out.println(normalData); // 操作4:读到42
}
}
}
推理链:
所以线程B读到val==1时,normalData必然是42。这就是CAS的内存语义——它和volatile一样,可以作为"发布锚点"。
AtomicInteger.get()的语义
注意上面用了atomicValue.get()而非另一个CAS。AtomicInteger.get()就是读取volatile字段——AtomicInteger内部value是volatile:
public class AtomicInteger {
private volatile int value; // volatile!
public final int get() {
return value; // volatile读
}
}
所以AtomicInteger的所有操作(get、set、compareAndSet、getAndIncrement)都基于volatile value,具有volatile的内存语义。这让原子类既能保证原子性,又能保证可见性。
三种同步方式的内存语义对比
synchronized、ReentrantLock、CAS(原子类)是Java并发的三大同步手段。它们的内存语义对比如下:
对比表
| 互斥性 | 互斥(同一时刻一个线程) | 互斥 | 无互斥(乐观策略) |
| 原子性 | 临界区整体原子 | 临界区整体原子 | 单个CAS操作原子 |
| 可见性 | 加锁清空、解锁写回 | volatile state(等价) | volatile读写语义 |
| 有序性 | 临界区整体有序 | volatile屏障(等价) | CAS前后屏障 |
| happens-before | 监视器锁规则(unlock hb lock) | volatile规则(写state hb 读state) | volatile规则(CAS hb 后续读) |
| 底层指令 | monitorenter/monitorexit | CAS + volatile state | lock cmpxchg |
| 阻塞 | 竞争时阻塞(锁升级) | 竞争时阻塞(park/unpark) | 不阻塞(自旋重试) |
| 适用场景 | 临界区保护、复合操作 | 可中断/超时/公平锁 | 单变量原子操作 |
| 性能(低竞争) | 偏向锁几乎免费 | 略慢于偏向锁 | 接近volatile读写 |
| 性能(高竞争) | 重量级锁开销大 | 与synchronized相当 | 自旋消耗CPU |
内存语义的等价性
三种方式的可见性保证是等价的——都建立了"前一个操作的结果对后续操作可见"的happens-before关系。区别在于:
- synchronized:通过JVM内置的monitorenter/monitorexit实现,加锁时清空工作内存、解锁时写回。语义最直观
- ReentrantLock:通过AQS的volatile state实现,lock/unlock操作state,借用volatile的内存语义。语义等价但实现不同
- CAS:通过lock cmpxchg指令,单次操作同时完成"读-比较-写"并保证可见性。粒度最细
选择策略
// 适用 JDK 8/11/17
// 场景1:单变量原子操作 → 用原子类
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet(); // CAS,无锁,高并发友好
// 场景2:多步复合操作 → 用synchronized或ReentrantLock
synchronized (lock) {
if (balance >= amount) {
balance -= amount; // 多步操作需要整体原子
transfer(amount);
}
}
// 场景3:需要可中断/超时/公平 → 用ReentrantLock
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 临界区
} finally {
lock.unlock();
}
}
// 场景4:简单互斥,不需要高级功能 → 优先synchronized
// 代码简洁,JVM优化充分(偏向锁/轻量锁/重量锁自动升级)
选择原则:
锁释放-获取的happens-before关系
无论哪种锁,"释放-获取"的happens-before关系都是内存语义的核心:
线程A(持锁修改):
[临界区内的所有写] → 释放锁
│
│ happens-before(锁释放-获取)
↓
线程B(获取锁读取):
获取锁 → [临界区内的所有读能看到A的写]
- synchronized:unlock hb lock(监视器锁规则)
- ReentrantLock:unlock(写volatile state)hb lock(读volatile state)(volatile规则)
- AQS同步器(Semaphore/CountDownLatch):release(写volatile state)hb acquire(读volatile state)
这条"锁释放-获取"的happens-before关系,是所有锁机制共享的内存语义基石。理解了它,就能推导出任何锁的可见性保证。
实践要点
synchronized块内的修改对下一个获取锁的线程可见。不需要额外用volatile修饰临界区内的变量——锁的内存语义已覆盖。但如果某变量在不加锁时也被访问,那它需要自己的可见性保证(volatile或锁)。
ReentrantLock的lock/unlock必须配对。unlock一定要在finally块中,否则异常时锁不释放,且内存同步(写回主内存)也不会执行——后续获取锁的线程可能看不到正确状态。
原子类不是锁的替代品。CAS适合单变量的原子操作,但不能保护多步复合操作。AtomicInteger能保证incrementAndGet原子,但"检查余额+扣款+转账"这种多步操作仍需锁。
公平锁和非公平锁的内存语义相同。ReentrantLock的公平/非公平只影响获取锁的顺序(是否允许插队),不影响内存语义——两者都基于volatile state,可见性保证一致。
锁的粒度影响内存同步开销。synchronized块越大,清空/写回的工作内存范围越大。但这通常不是瓶颈——锁竞争本身的开销远大于内存同步。优化应聚焦减少锁竞争(减小粒度、分段锁、无锁化)。
synchronized和ReentrantLock不要混用保护同一资源。两者是不同的锁对象(synchronized用对象头,ReentrantLock用AQS),混用不构成"同一把锁",监视器锁规则不成立,可见性无保证。选择一种并统一使用。
CAS自旋在高竞争下消耗CPU。原子类在低竞争下性能优异,但高竞争时CAS频繁失败重试,浪费CPU周期。这种场景用LongAdder(JDK 8+,分散热点)或改用锁更优。
AQS同步器(Semaphore/CountDownLatch)也有内存语义。它们的release/acquire基于volatile state,具有与锁相同的happens-before关系。CountDownLatch.countDown() hb await()返回,Semaphore.release() hb acquire()。善用这些"免费"的可见性保证。
小结
- synchronized的内存语义:加锁时清空工作内存(强制从主内存读),解锁时写回主内存(StoreStore + StoreLoad)。等价于在临界区边界插了全功能内存屏障
- ReentrantLock的内存语义:基于AQS的volatile state实现。lock是CAS(volatile读+写),unlock是setState(volatile写)。通过volatile规则建立与synchronized等价的happens-before关系
- CAS的内存语义:同时具有volatile读和volatile写的语义。lock cmpxchg指令保证原子性+可见性,可作为"发布锚点"建立happens-before链
- 三种方式等价性:synchronized(监视器锁规则)、ReentrantLock(volatile规则)、CAS(volatile规则)的可见性保证等价,都是"释放-获取"的happens-before关系。区别在于互斥性、阻塞行为、功能特性
- 选择策略:单变量原子用CAS原子类,简单互斥用synchronized,需要高级特性用ReentrantLock。不确定时优先synchronized——简洁且优化充分
- 锁释放-获取是happens-before核心:所有锁机制(包括AQS同步器)的内存语义,都归结为"释放操作happens-before后续获取操作"。理解这一条,就能推导任何锁的可见性保证
网硕互联帮助中心




评论前必须登录!
注册