java Volatile和锁的实现原理与内存语义

Volatile的实现原理与内存语义

Volatile这个单词的意思是易变的,易挥发的。用Volatile关键字修饰的共享变量处理器会保证其“可见性”。即当一个线程修改一个共享变量时,另外一个线程能读到这个修改后的值。

Volatile的成本比Synchroinzed更低,它不会引起线程上下文的切换和调度,有Volatile变量修饰的共享变量进行写操作时,编译出的汇编指令会带有写前和写后内存屏障,内存屏障能保证CPU重排序不会破环多线程内存读写“可观察到”的顺序。
比如,对于代码

private volatile Student student = new Student("SA001", "zhq", 18, Gender.MALE);

通过JDK 21 的 C1 编译器生成的汇编指令是

110: dmb  ish                  ; 写前内存屏障
114: ldr  x0, [sp, #0x48]       ; 获取构造完成的 Student 引用
118: lsr  x8, x0, #3            ; 压缩对象引用
11c: str  w8, [x1, #0xc]        ; 将引用写入 this.student
120: dmb  ish                  ; 写后内存屏障

其中110和120行就是内存屏障,保证114,118,11c不管怎么CPU重排序,其他线程看到的都是初始化完成的student对象。

线程 A 初始化 Student
↓
线程 A 写入 volatile student
↓ happens-before
线程 B 读取 volatile student,获得该引用
↓
线程 B 能看到发布前完成的初始化写入


另一方面,对声明了volatile的变量进行写操作,CPU在自己内部缓存(L1, L2)中完成写操作之后,会主动回写到主内存,但其他CPU内部缓存里的值仍然是旧的。为了解决这个问题,又引入了缓存一致性协议:每个处理器通过嗅探在主内存里的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改后,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。 

这个处理器和总线之间的缓存一致性协议的实现,让我想到了“微服务架构中配置中心的配置变化动态发现与推送”,“分布式系统中多个partition之间保证数据最终一致性”等场景的实现方式。小到一个处理器内部,大到全球分布的系统,计算机技术对某些问题的处理方式是相通的。

 

Volatile的内存语义

Volaile变量在多个线程之间的写-读通过共享内存实现线程间通信,从内存语义的角度来说,volatile的写与锁的释放有相同的内存语义;volatile的读与锁的获取有相同的内存语义。

线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量作出修改)的消息。

线程B读一个volatile变量,实质上是线程B接收了以前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。

线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送信息。

 

 

锁的实现原理与内存语义

Java中的每个对象都可以作为锁:对于普通同步方法,锁是当前实例对象;对于静态同步方法,锁是当前类的Class对象;对于同步方法块,锁的是Synchonized括号里配置的对象。

JVM基于进入和退出Monitor对象来实现代码块同步和方法同步。Synchronized用的锁是存在Java对象头里的。对象头里存储的有对象的HashCode、分代年龄、是不是偏向锁和锁标记位。

当同一个线程多次访问同一个同步对象或同步块,会在对象头he栈帧中的锁记录里存储锁偏向的线程ID,以后该先吃在进入和退出同步块时不需要进行CAS操作来加锁和解锁,从而让同线程获取锁的代价更低,这就是偏向锁。JDK15之后不再支持偏向锁。

在线程执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的跟锁相关的标记复制到栈帧中的锁记录空间。然后线程尝试使用CAS将对象头中的跟锁相关的标记替换为指向栈帧中锁记录空间的指针,如果成功则当前线程获取锁,如果失败,表示其他线程在竞争锁,当前线程便尝试使用自旋来获取锁,这就是轻量级锁。

 

锁的内存语义

通过ReentrantLock分析锁的内存语义。ReentrantLock的实现依赖Java同步器框架AbstractQueuedSynchronizer(AQS)。AQS使用一个整型的volatile int state变量来维护同步状态。

ReentrantLock分为公平锁和非公平锁,对于公平锁,释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。对于非公平锁,也是释放锁的最后写volatile变量state,非公平锁获取锁时,首先会用CAS更新volatile变量state,这个操作同时具有volatile读和volatile写的内存语义。

所以,使用非公平锁时,排队的线程上来就用CAS写state,会有插队现象。而使用公平锁时,都先读state,按队列顺序不会插队。 

JMM Happens-Before规则

在 Java 内存模型中,Happens-before 关系用于描述两次操作之间的内存可见性。
  • 如果操作 x happens-before 操作 y(记作 x \to y),那么 x 的执行结果对 y 是可见的,且 x 的执行顺序排在 y 之前。
  • 如果两个操作之间存在 happens-before 关系,并不意味着 JVM 必须按照这个顺序去执行硬件指令;只要重排序后的执行结果与 happens-before 规定的结果一致,JVM 怎么优化都可以。
官方规范中定义的几个关键 Happens-before 规则:
1.程序次序规则(Program Order Rule)
在一个线程内,按照代码的编写顺序(依照控制流顺序),书写在前面的操作 happens-before 书写在后面的操作。
 
2.监视器锁规则(Monitor Lock Rule)
对一个锁的解锁(unlock),happens-before 于随后对这个锁的加锁(lock)。
 
3.volatile 变量规则(Volatile Variable Rule)
对一个 volatile 变量的写操作,happens-before 于后续对这个 volatile 变量的读操作。
 
4.线程启动规则(Thread Start Rule)
主线程调用 Thread.start() 方法,happens-before 于该启动线程中的任意操作。

5.线程终止规则(Thread Termination Rule)
线程中的所有操作都 happens-before 于其他线程检测到该线程已经终止(例如通过 Thread.join() 成功返回,或 isAlive() 返回 false)。
 
6.线程中断规则(Thread Interruption Rule)
对线程 interrupt() 方法的调用,happens-before 于被中断线程的代码检测到中断事件的发生(通过 Thread.interrupted() 或 isInterrupted())。
 
7.对象终结规则(Finalizer Rule)
一个对象的构造函数结束(构造函数执行完毕)happens-before 于该对象的 finalize() 方法的开始。
 
8.传递性(Transitivity)
如果操作 A happens-before 操作 B,且操作 B happens-before 操作 C,那么可以得出操作 A happens-before 操作 C。




posted @ 2026-11-01 22:37  软件心理学工程师  Views(3)  Comments(0)    收藏  举报