目录
一、volatile的内存语义
1. happens-before关系
package com.cmmon.instance;
/**
* @description volatile示例代码
* @author tcm
* @version 1.0.0
* @date 2022/2/28 10:10
**/
public class VolatileExample {
public int a = 0;
public volatile boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
int i = a; // 4
}
}
}
如上代码所示,假设线程A执行writer()方法后,线程B执行reader()方法。根据happens-before规则,有以下关系:
1)根据程序顺序规则:1 happens-before 2;3 happens-before 4;
2)根据volatile变量规则:2 happens-before 3;
3)根据传递性规则:1 happens-before 4。
如下图所示happens-before关系图。线程A写一个volatile变量后,线程B读同一个volatile变量,存在:1 happens-before 4。线程A在写volatile变量之前所有可见的共享变量,在线程B读同一个volatile变量后,将立即变得对线程B可见。
2. volatile写-读内存语义
以上节代码和执行线程为例,两个线程的本地内存中的flag=false和a=0都是初始值或默认值。当线程A写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致,如下图所示。
当线程B读volatile变量时,JMM会把线程B对应的本地内存置为无效。线程B将从主内存中读取共享变量。
类型 | 内存语义 |
volatile写 | 1. 本地内存共享变量刷新到主内存中; 2. 写线程向将要读volatile变量的线程发出消息。 |
volatile读 | 1. 读线程接收了写线程发出的消息; 2. 本地内存共享变量设置为无效; 3. 从主内存中读取共享变量。 |
JMM | 1. 写线程经过主内存向读线程发送消息 (JMM决定一个线程对共享变量的写入何时对另一个线程可见) |
3. volatile内存语义实现
a. 禁止编译器重排序
JMM对源码生成字节码时,对volatile变量做了重排序规则。如下表所示,可以得出结论:
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。确保volatile写之前的操作不会被编译器重排序到volatile写之后。
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。确保volatile读之后的操作不会被编译器重排序到volatile读之前。
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
b. 禁止处理器重排序
JMM编译器生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。如下表所示:
操作类型 | 内存屏障 |
volatile写 | 该写操作前面插入一个StoreStore屏障 |
volatile写 | 该写操作后面插入一个StoreLoad屏障 |
volatile读 | 该读操作后面插入一个LoadLoad屏障 |
volatile读 | 该读操作后面插入一个LoadStore屏障 |
如下图所示,StoreStore屏障将保证上面所有的普通写在volatile写之前刷新到主内存。volatile写后面的StoreLoad屏障,作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile读的前面插入一个StoreLoad屏障。
二、synchronized的内存语义
1. happens-before关系
package com.cmmon.instance;
/**
* @description volatile示例代码
* @author tcm
* @version 1.0.0
* @date 2022/2/28 13:48
**/
public class MonitorExample {
public int a = 0;
public synchronized void writer() { // 1
a++; // 2
} // 3
public synchronized void reader() { // 4
int i = a; // 5
} // 6
}
如上代码所示,假设线程A执行writer()方法后,线程B执行reader()方法。根据happens-before规则,有以下关系:
1)根据程序顺序规则:1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6;
2)根据synchronized锁规则:3 happens-before 4;
3)根据传递性规则:2 happens-before 5。
如下图所示happens-before关系图。线程A释放了锁之后,随后线程B获取同一个锁,存在:2 happens-before 5。因此,线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立刻变得对线程B可见。
2. synchronized锁的释放-获取内存语义
以上节代码和执行线程为例,两个线程的本地内存中的a=0都是初始值或默认值。线程A释放锁时,JMM会把线程A对应的本地内存中的共享变量刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致,如下图所示。
当线程B获取锁时,JMM会把线程B对应的本地内存置为无效。线程B从而使得被Monitor保护的代码区内的共享变量必须从主内存中读取共享变量。
类型 | 内存语义 |
释放锁 (释放Monitor对象) | 1. 本地内存共享变量刷新到主内存中; 2. 释放锁线程向将要获取锁的线程发出消息。 |
获取锁 (获取Monitor对象) | 1. 获取锁线程接收了释放锁线程发出的消息; 2. 本地内存共享变量设置为无效; 3. 从主内存中读取共享变量。 |
JMM | 1. 释放锁线程经过主内存向获取锁线程发送消息 (JMM决定一个线程对共享变量的写入何时对另一个线程可见) |
注意 | 锁释放与volatile写、获取锁与volatile读,两者有相同的内存语义 |
3. synchronized内存语义实现
a. ReentrantLock源码分析
下图是ReentrantLock类图,其依赖于Java同步器框架AbstractQueuedSynchronizer。其使用一个整型的volatile变量(private volatile int state;)来维护同步状态。如下表所示是公平锁和非公平锁的释放锁、获取锁的对比。
锁类型 | 方法调用轨迹 | |
公平锁 (FairSync) | 获取锁 | step1:ReentrantLock:lock() step2:FairSync:lock() step3:AbstractQueuedSynchronizer:acquire(int arg) step4:ReentrantLock:tryAcquire(int acquires) |
释放锁 | step1:ReentrantLock:unlock() step2:AbstractQueuedSynchronizer:release(int arg) step3:Sync:tryRelease(int releases) | |
非公平锁 (NonfairSync) | 获取锁 | step1:ReentrantLock:lock() step2:NonfairSync:lock() step3:AbstractQueuedSynchronizer:compareAndSetState(int expect,int update) |
释放锁 | step1:ReentrantLock:unlock() step2:AbstractQueuedSynchronizer:release(int arg) step3:Sync:tryRelease(int releases) | |
注意 | 1. 公平锁与非公平锁的释放锁是相同的; 2. 非公平锁获取锁是通过unsafe.compareAndSwapInt(this, stateOffset, expect, update),即:CAS操作来获取锁(Lock CMPXCHG); 3. 公平锁和非公平锁释放时,最后都要写一个volatile变量state; |
b. synchronized内存语义实现
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c); // 释放锁的最后,写volatile变量state
return free;
}
以上代码所示是公平锁和非公平锁的释放锁的代码。在释放锁时,最后都要写一个volatile变量state。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前的共享变量,在获取锁的线程读取同一个volatile变量后共享变量则可见。
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
以上代码非公平锁的获取锁的代码。AbstractQueuedSynchronizer:compareAndSetState(int expect,int update) 方法以原子操作的方式更新state变量,即:CAS操作。程序在多CPU处理下,cmpxchg指令加上Lock前缀(Lock CMPXCHG)实现总线锁或缓存锁。CAS操作同时实现volatile读和volatile写的内存语义。
锁释放-获取的内存语义的实现至少有下面两种方式:
- 利用volatile变量的写-读所具有的内存语义;
- 利用CAS所附带的volatile读和volatile写的内存语义。
三、final的内存语义
package com.cmmon.instance;
/**
* @description final示例代码
* @author tcm
* @version 1.0.0
* @date 2022/2/28 17:10
**/
public class FinalExample {
public int i; // 普通变量
public final int j; // final变量
public static FinalExample obj;
public FinalExample () { // 构造函数
i = 1; // 写普通域
j = 2; // 写final域
}
public static void writer () { // 写线程A执行
obj = new FinalExample ();
}
public static void reader () { // 读线程B执行
FinalExample object = obj; // 读对象引用
int a = object.i; // 读普通域
int b = object.j; // 读final域
}
}
· 写final域的重排序规则会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore障屏;读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。如下表总结所示。
类型 | 重排序规则 | |
写final | 编译器重排序 | JMM禁止编译器把final域的写重排序到构造函数之外 |
处理器重排序 | 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障 作用为:禁止处理器把final域的写重排序到构造函数之外 | |
读final | 编译器重排序 | --------- |
处理器重排序 | 编译器会在读final域操作的前面插入一个LoadLoad屏障 作用:线程初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序 | |
注意 | 1. 写final域的重排序规则“限定”在了构造函数之内完成; 2. 少数处理器允许对存在间接依赖关系的操作做重排序,所以读final域操作前面插入LoadLoad屏障 |
只要保证写final预在构造函数中没有“逸出”,那么不需要使用同步(指lock和volatile的使用)就可以保证任意线程都能读final域在构造函数中被初始化之后的值。下面代码示例写final域“逸出”。
package com.cmmon.instance;
/**
* @description 写final域在构造函数内溢出
* @author tcm
* @version 1.0.0
* @date 2022/2/28 17:35
**/
public class FinalReferenceEscapeExample {
public final int i;
public static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample () {
i = 1; // 1写final域
obj = this; // 2 this引用在此"逸出"
}
public static void writer() {
new FinalReferenceEscapeExample ();
}
public static void reader() {
if (obj != null) { // 3
int temp = obj.i; // 4
}
}
}
四、参考资料
Java内存模型<一> _ 基础_爱我所爱0505-CSDN博客
volatile与synchronized实现原理_爱我所爱0505-CSDN博客
Volatile使用原理及作用_huangwei18351的博客-CSDN博客_volatile的作用和原理
详解synchronized与Lock的区别与使用_brickworkers的博客-CSDN博客_synchronized和lock的区别