flowchart TB %% ================= Java代码层 ================= subgraph Java代码 Lock --> AQS AQS --> synchronized AQS --> volatile AQS --> UnsafeCAS[Unsafe.compareAndSwap] end %% ================= Java字节码 ================= subgraph Java字节码 ACC_SYNCHRONIZED ACC_VOLATILE end synchronized --> ACC_SYNCHRONIZED volatile --> ACC_VOLATILE UnsafeCAS --> cmpxchg3 %% ================= JVM 层 ================= subgraph JVM monitorenter_exit[monitorenter / monitorexit] end ACC_SYNCHRONIZED --> monitorenter_exit %% ================= JVM代码 ================= subgraph JVM代码 ObjectMonitor stroeload["stroeload()"] end monitorenter_exit --> ObjectMonitor ACC_VOLATILE --> stroeload %% ================= C++ ================= subgraph C++ Mutex end %% ================= 操作系统 ================= subgraph 操作系统 cmpxchg1[cmpxchg] cmpxchg2[cmpxchg] cmpxchg3[cmpxchg] cmpxchg4[cmpxchg] end ObjectMonitor --> cmpxchg1 stroeload --> cmpxchg2 Mutex --> cmpxchg4 %% ================= 汇编 ================= subgraph 汇编 lock1[lock指令] lock2[lock指令] lock3[lock指令] lock4[lock指令] end cmpxchg1 --> lock1 cmpxchg2 --> lock2 cmpxchg3 --> lock3 cmpxchg4 --> lock4 %% ================= 硬件 ================= subgraph 硬件 MESI1[MESI协议: 广播和失效] MESI2[MESI协议: 广播和失效] MESI3[MESI协议: 广播和失效] MESI4[MESI协议: 广播和失效] end lock1 --> MESI1 lock2 --> MESI2 lock3 --> MESI3 lock4 --> MESI4
sychronized
字节码层面
public class Main {
public synchronized void syncMethod() {
System.out.println("syncMethod");
}
public void syncBlock() {
synchronized (this) {
System.out.println("syncBlock");
}
}
public static void main(String[] args) {
}
}
使用 javap -verbose <class文件>
可以查看到字节码信息
synchronized 方法的 ACC_SYNCHRONIZED 标志和 synchronized 代码块的monitorenter/monitorexit
指令,最终在JVM执行时,都会转化为对同一个Monitor底层对象的获取和释放操作
public class Main
{
public synchronized void syncMethod();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
public void syncBlock();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #21 // String syncBlock
9: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
同步代码块:使用monitorenter
和monitorexit
指令显式地在字节码中标记出临界区的开始和结束
public void method() {
synchronized (obj) { // 编译后:monitorenter指令
// 临界区代码
} // 编译后:monitorexit指令
}
同步方法:使用ACC_SYNCHRONIZED
标志隐式地声明整个方法都是同步的
public synchronized void method() {
// 整个方法都是临界区
}
ACC_SYNCHRONIZED
是一个方法访问标志。当JVM的方法调用引擎遇到这个标志时,它会在方法的实际执行前后,自动插入管程(Monitor)的获取和释放操作
这个过程可以理解为JVM在运行时为你做了如下事情:
- 在调用同步方法之前:JVM会隐式地尝试获取该方法所属对象实例(对于实例方法)或所属类(对于静态方法)的管程(Monitor)锁
- 如果获取成功:当前线程成为Monitor的所有者,然后开始执行方法体
- 如果获取失败:当前线程被阻塞,进入该Monitor的入口集中等待,直到锁被释放
- 在方法执行完成(正常return)或异常抛出后:JVM会隐式地释放该管程(Monitor)锁,无论方法是如何结束的
对比:代码块 vs 方法
特性 | synchronized 代码块 (monitorenter /monitorexit ) |
synchronized 方法 (ACC_SYNCHRONIZED ) |
---|---|---|
字节码表示 | 显式的指令 | 隐式的标志 |
控制粒度 | 细粒度,可以精确控制需要同步的代码范围 | 粗粒度,整个方法都是同步的 |
锁对象 | 非常灵活,可以是任何对象(synchronized(obj) ) |
固定:实例方法是this ,静态方法是Class 对象 |
异常处理 | 编译器必须生成额外的monitorexit 指令以确保在异常路径上也能释放锁 |
JVM保证无论方法正常返回还是异常抛出,锁都会被释放 |
实现本质 | 在字节码层面显式指明了锁操作 | 依赖JVM的方法调用机制在运行时自动添加锁操作 |
monitorenter
/monitorexit
:是“手动挡”,由编译器在编译时为你生成具体的操作指令ACC_SYNCHRONIZED
:是“自动挡”,它告诉JVM:“这个方法需要同步,麻烦你在调用和返回时自动帮我处理好加锁和解锁的事情”
最终,两者都会在JVM的执行引擎中,转化为对同一个底层管程(Monitor)的竞争和管理。 这种设计分离了字节码的表示和JVM的具体实现,使得JVM可以自由地优化同步机制(如我们之前讨论的锁升级),而无需修改已编译的类文件
JVM 层面
monitorenter 底层是由 JVM 的代码 ObjectMonitor 实现
ObjectMonitor() {
// 多线程竞争锁进入时的单向链表
ObjectWaiter * volatile _cxq;
//处于等待锁block状态的线程,会被加入到该列表
ObjectWaiter * volatile _EntryList;
// _header是一个markOop类型,markOop就是对象头中的Mark Word
volatile markOop _header;
// 抢占该锁的线程数,约等于WaitSet.size + EntryList.size
volatile intptr_t _count;
// 等待线程数
volatile intptr_t _waiters;
// 锁的重入次数
volatile intptr_ _recursions;
// 监视器锁寄生的对象,锁是寄托存储于对象中
void* volatile _object;
// 指向持有ObjectMonitor对象的线程
void* volatile _owner;
// 处于wait状态的线程,会被加入到_WaitSet
ObjectWaiter * volatile _WaitSet;
// 操作WaitSet链表的锁
volatile int _WaitSetLock;
// 嵌套加锁次数,最外层锁的_recursions属性为0
volatile intptr_t _recursions;
}
enter 方法
无锁、偏向锁、轻量级锁、重量级锁,核心方法是 Atomic::cmpxchg_ptr,CAS 操作
锁 | 方法 | 描述 |
---|---|---|
偏向锁 | Atomic::cmpxchg_ptr | 将 owner 替换为当前线程,成功则获取到锁 |
轻量级锁 | TrySpin->Atomic::cmpxchg_ptr | 不断自旋将 owner 替换为当前线程,成功则获取到锁 |
重量级锁 | EnterI>Atomic::cmpxchg_ptr | park 然后将 owner 替换为当前线程,成功则获取到锁 |
cmpxchg_ptr
cmpxchg
是一条CPU指令**:x86/x86-64架构处理器提供的一条**硬件原子指令。它不是操作系统特有的函数,而是CPU硬件直接支持的能力lock
前缀:这是一个指令前缀,用于修饰像cmpxchg
这样的内存操作指令。当一条指令被lock
修饰后,它会确保该指令对内存的读-改-写操作在 multiprocessor(多处理器系统,即多核系统)上是原子的。它通过锁定总线或缓存锁来实现这一点
JVM在不同操作系统上利用C++内联汇编来调用CPU的硬件指令
特性 | Linux / GCC 实现 (如OpenJDK) | Windows / VC++ 实现 |
---|---|---|
实现方式 | GCC扩展内联汇编 | 编译器内置函数(Intrinsics) |
代码示例 | __asm__ __volatile__(... LOCK_IF_MP ... "cmpxchgq" ...) |
_InterlockedCompareExchange64(...) |
核心逻辑 | 手动判断是否需要lock 前缀,并手动嵌入cmpxchg 指令 |
调用编译器提供的函数,由编译器负责生成最优指令 |
共同终点 | x86 lock cmpxchgq 机器指令 |
x86 lock cmpxchgq 机器指令 |
volatile
字节码层面
flags: ACC_STATIC, ACC_VOLATILE
JVM 层面
OrderAccess::storeload();
, storeload 屏障
进入 OrderAccess 源码可以看到,直接执行了一段汇编指令,并且有 lock 前缀
inline void OrderAccess::storeload() { fence(); }
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
Lock指令前缀和MESI
多核处理器系统不使用共享内存,有两种方法
- 锁内存总线,也就是说执行这条指令的时候,其他的核心都不能在访问内存了
- 锁缓存行,现在 CPU 本身是有多级缓存的,而这些缓存是如何保持一致的,由 MESI 来支持,MESI 协议可以保证其他核心不使用内存,或者换一种说法,可以使用,但被修改的内容会失效
MESI 协议下,缓存行(cache line)有四种状态来保证缓存的一致性
- 已修改Modified (M) 缓存行是脏的,与主存的值不同。如果别的 CPU 内核要读主存这块数据,该缓存行必须回写到主存,状态变为共享(S)
- 独占Exclusive (E) 缓存行只在当前缓存中,但是干净的(clean)–缓存数据同于主存数据。当别的缓存读取它时,状态变为共享;当前写数据时,变为已修改状态
- 共享Shared (S) 缓存行也存在于其它缓存中且是干净的。缓存行可以在任意时刻抛弃
- 无效Invalid (I) 缓存行是无效的,需要从主内存中读取最新值
每次要修改缓存,如果缓存行状态为 S 的话都要先发一个 invalidate 的广播,再等其他 CPU 将缓存行设置为无效后返回 invalidate ack 才能写到 Cache 中,因为这样才能保证缓存的一致性
但是如果 CPU 频繁地修改数据,就会不断地发送广播消息,CPU 只能被动同步地等待其他 CPU 的消息,显然会对执行效率产生影响
为了解决此问题,工程师在 CPU 和 cache 之间又加了一个 store buffer,同时在 cache 和总线之间添加了 Invalidate Queue
这个 buffer 可以让广播和收广播的处理异步化,效率当然会变高,但强一致性变为了最终一致性
lock 指令是 CPU 硬件工程师给程序员留的一个口子,把对 MESI 协议的优化(store buffer, invalidate queue)禁用,暂时以同步方式工作,使得对于该关键字的 MESI 协议退回强一致性状态
这种一致性的“降级”主要体现在一个核心的写操作无法被其他核心立即观察到
通过一个具体的例子来对比说明,并解释其背后的原因
1. 理想的强一致性(无Store Buffer/Invalidate Queue)
假设我们有两个CPU核心:Core 0 和 Core 1。它们都缓存了共享变量 data = 0
,状态为 Shared (S)
步骤 | Core 0 的操作 | Core 0 的 Cache 状态 | 总线活动 | Core 1 的 Cache 状态 | Core 1 看到的 data 值 |
---|---|---|---|---|---|
1 | - | data=0 (S) |
- | data=0 (S) |
0 |
2 | data = 42 |
需要写! | 1. 发出 Invalidate 广播 | data=0 (S) |
0 |
2. 等待 所有ACK (包括Core1的) | → 收到广播,将缓存行置为 I,发回ACK | ||||
3. 收到所有ACK | data=? (I) |
||||
3 | 写入Cache | data=42 (M) |
- | data=? (I) |
必须从内存重新读取 |
4 | - | data=42 (M) |
- | 读 data ,导致Core 0写回内存 |
从内存/总线读到 42 |
强一致性体现: 在步骤2,Core 0必须同步等待 Core 1的Invalidate Acknowledge(无效化确认)返回后,才能进行步骤3的实际写入。这意味着,在Core 0的写入操作完成(步骤3结束)的那一刻,全球所有其他核心的缓存中关于这个数据的副本都已经被强制无效化了。因此,Core 1在步骤4读取时,必然会发现自己的缓存已失效,从而去获取最新的值(42)。Core 1绝对不可能读到旧的值0
核心是:写操作会阻塞,直到全局的缓存一致性得到保证后,写操作才完成
2. 实际的最终一致性(有Store Buffer/Invalidate Queue)
现在引入Store Buffer和Invalidate Queue
- Store Buffer:一个核心要写数据时,如果缓存行是S或I状态,它不再傻等ACK,而是把要写的值
(data=42)
先塞进自己的Store Buffer,然后发出Invalidate广播后就继续执行后面的指令了(仿佛已经写完了一样)。 - Invalidate Queue:其他核心收到Invalidate广播后,不再同步地处理它(立即将缓存行置为I),而是把这个无效化请求塞进自己的Invalidate Queue,然后就立即返回ACK。它承诺“我稍后会处理这个无效化请求”。
现在让我们看看同样的操作流程:
步骤 | Core 0 的操作 | Core 0 的 Store Buffer | Core 0 Cache | 总线活动 | Core 1 的 Invalidate Queue | Core 1 Cache | Core 1 看到的 data 值 |
---|---|---|---|---|---|---|---|
1 | - | - | data=0 (S) |
- | - | data=0 (S) |
0 |
2 | data = 42 |
data=42 |
data=0 (S) |
发出 Invalidate 广播 | [Invalidate for data] |
data=0 (S) |
0 |
→继续执行! | → 立即返回ACK | ||||||
3 | 读 data |
||||||
(重要!) | |||||||
4 | - | data=42 |
data=0 (S) |
- | [Invalidate for data] |
data=0 (S) |
0 (旧值!) |
最终一致性体现:
- 写操作“提前完成”:Core 0在步骤2发出广播后立刻收到ACK(因为Core 1只是把请求放队列里就回复了),然后它就直接继续向下执行步骤3了,此时
data=42
这个新值还躺在它的Store Buffer里,并没有真正写入缓存 - 读操作的风险:
- Core 0 自己读:在步骤3,如果Core 0紧接着要读
data
,硬件会优先从它的Store Buffer里查找,从而读到新值42。这保证了写操作的“自见性”。 - Core 1 读:问题就在这里! 在步骤4,Core 1尝试读
data
。它的缓存行状态仍然是 S(有效!),因为那个Invalidate请求还在它的Invalidate Queue里排队,还没被真正处理。所以,它直接就从自己的缓存里读到了旧的值0,它完全不知道数据已经失效了!
- Core 0 自己读:在步骤3,如果Core 0紧接着要读
- “最终”的含义:这个“不一致”的状态不会一直持续。最终,两个异步组件会工作:
- Core 0 的 Store Buffer 会迟早将数据
42
写入它的缓存(状态变为M) - Core 1 的 CPU 迟早会处理它的 Invalidate Queue,将对应的缓存行状态置为 I
- 一旦这两个动作都完成,下次 Core 1 再读
data
时,就会发现缓存失效(I),从而从总线获取最新值42
- Core 0 的 Store Buffer 会迟早将数据
从Core 0写入完成,到Core 1真正感知到数据失效的这段时间窗口内,系统处于不一致状态。 这就是“最终一致性”——它保证如果不再有新的写操作,经过一段“传播时间”后,所有核心最终都会看到同一个值,但不能保证在写操作后的每一个瞬间都立即一致
特性 | 强一致性 (理想MESI) | 最终一致性 (带Buffer/Queue) |
---|---|---|
写操作延迟 | 高(必须同步等待所有ACK) | 低(写操作立即进入Buffer,继续执行) |
性能 | 低 | 高 |
可见性 | 即时全局可见:写操作完成时,所有核心的缓存副本立即失效。 | 延迟全局可见:存在一个时间窗口,其他核心可能读到旧值。 |
核心保证 | 线性一致性(Linearizability) | 顺序一致性(Sequential Consistency)被破坏 |
为了解决这个由“最终一致性”带来的问题(即代码执行顺序可能与预期不符),程序员需要在可能发生这种问题的代码位置手动插入内存屏障(Memory Barrier) 指令
- 写屏障:告诉CPU,必须等到Store Buffer中的所有写操作都真正刷入缓存后,才能执行后面的写操作。这保证了写操作的顺序
- 读屏障:告诉CPU,必须等到Invalidate Queue中的所有无效化请求都处理完后,才能执行后面的读操作。这保证了能读到其他核心最新的写操作
正是因为这个架构层面的优化,Java等高级语言中volatile
关键字和atomic
操作才变得至关重要,因为它们编译后的指令会包含必要的内存屏障,强制在特定位置恢复“强一致性”的内存视图,从而保证线程安全