文章目录
- 被 volatile 修饰的变量有两大特点
- volatile 的内存语义
- volatile 凭什么可以保证有序性和可见性?
- 内存屏障(面试重点)
- 解读 volatile 的两大特性
- 内存屏障
- volatile 特性
- 两大类
- 读屏障(Load Barrier)
- 写屏障(Store Barrier)
- 四小类
- C++源码分析
- 四类屏障特点
- 如何保证有序性?
- happens-before 之 volatile 变量规则
- JMM 就内存屏障插入策略分为 4 中规则
- 如何保证可见性?
- volatile 变量的读写过程
- 无法保证原子性
- 关于上述现象的解释
- 结论
- 面试回答
- 指令禁止重排序
- 案例说明
- volatile 底层实现通过内存屏障
- 代码案例分析
- volatile 的使用场景
- 单一赋值可用,存在复合运算赋值不可用(i++等)
- 状态标志,判断业务是否结束
- 开销较低的读,写锁策略
- DCL 双端锁的发布
- 问题描述
- 解决方案
- Summary
被 volatile 修饰的变量有两大特点
- 可见性,某个线程对该变量的修改对其它线程可见
- 有序性,允许指令重排序,有时也需要禁止指令重排序
volatile 的内存语义
- 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量立即刷新回主内存中
- 当读一个 volatile 变量时间,JMM 会把本地内存置为无效,重新回到主内存中读取最新共享变量
- 因此 volatile 的写内存语义是直接刷新到主内存中,写内存语义是直接从主内存中读取
volatile 凭什么可以保证有序性和可见性?
- 基于内存屏障(Memory Barrier)
内存屏障(面试重点)
解读 volatile 的两大特性
- 可见性
- 一个线程完成对变量的修改之后,立即写回主内存,并及时通知其它线程
- 其他线程读取时,直接丢弃本地副本,读取主内存变量
- 有序性(支持禁止重排序)
- 指令重排序是编译器和处理器为了优化程序性能,在不改变程序执行效果的前提在对指令进行重新排序的一种手段
- 不存在数据依赖时,可以重排序,存在数据依赖时,禁止重排序
- 重排序指令不看更改原有串行语义
内存屏障
内存屏障(也称为内存栅栏,屏障指令等)
- 是一类同步屏障指令,
- 是 CPU 或编译器在对内存随机访问的操作中的一个同步点,
- 使得此点之前所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重排序
- 本质是一种 JVM 指令,
- Java 内存模型的重排规则会要求 Java 编译器在生成 JVM 指令时,插入特定的内存屏障指令,通过这些内存屏障指令,volatile 实现了 Java 内存模型中的可见性和有序性(禁重排),但是 volatile 无法保证原子性
- 内存屏障之前的所有写操作都要写回主内存
- 内存屏障之后的所有读操作都能获得内存屏障之前所有写操作的最新结果(实现了可见性)
- 写屏障(Store Memory Barrier)
- 告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。
- 当看到 store 屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行
- 读屏障(Load Memory Barrier)
- 处理器在读屏障之后的读操作,均在读屏障之后执行
- 在 Load 屏障指令之后就能保证后面读取数据指令一定能够读取到最新的数据
- 因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前
- 对一个 volatile 变量的写,先行发生于任何一个后续的对这个 volatile 变量的读,亦称为写后读
- 对一个 volatile 变量的写,先行发生于任何一个后续的对这个 volatile 变量的读,亦称为写后读
volatile 特性
先行发生原则,happens-before,本质是一个理论,一套规范,内存屏障基于该规范进行实现
两大类
读屏障(Load Barrier)
- 在读指令之前插入读屏障,让工作内存或 CPU 高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据
写屏障(Store Barrier)
- 在写指令之后插入写屏障,强制把缓冲区的数据刷回主内存中
四小类
C++源码分析
Unsafe.class,JDK 中的屏障接口
//....code segment...
public native void loadFence();public native void storeFence();public native void fullFence();
Unsafe.java
/*** Ensures lack of reordering of loads before the fence* with loads or stores after the fence.* @since 1.8*/public native void loadFence();/*** Ensures lack of reordering of stores before the fence* with loads or stores after the fence.* @since 1.8*/public native void storeFence();/*** Ensures lack of reordering of loads or stores before the fence* with loads or stores after the fence.* @since 1.8*/public native void fullFence();
Unsafe.cpp
UNSAFE_ENTRY(void, Unsafe_LoadFence(JNIEnv *env, jobject unsafe))UnsafeWrapper("Unsafe_LoadFence");OrderAccess::acquire();
UNSAFE_ENDUNSAFE_ENTRY(void, Unsafe_StoreFence(JNIEnv *env, jobject unsafe))UnsafeWrapper("Unsafe_StoreFence");OrderAccess::release();
UNSAFE_ENDUNSAFE_ENTRY(void, Unsafe_FullFence(JNIEnv *env, jobject unsafe))UnsafeWrapper("Unsafe_FullFence");OrderAccess::fence();
UNSAFE_END
OrderAccess.hpp
//声明四类屏障类型,读读,写写,读写,写读
class OrderAccess : AllStatic {public:static void loadload();static void storestore();static void loadstore();static void storeload();//....more code.....
}
orderAccess_linux_ x86.inline.hpp
#include "runtime/atomic.inline.hpp"
#include "runtime/orderAccess.hpp"
#include "runtime/os.hpp"
#include "vm_version_x86.hpp"// Implementation of class OrderAccess.inline void OrderAccess::loadload() { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore() { acquire(); }
inline void OrderAccess::storeload() { fence(); }inline void OrderAccess::acquire() {volatile intptr_t local_dummy;
#ifdef AMD64__asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else__asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64
}inline void OrderAccess::release() {// Avoid hitting the same cache-line from// different threads.volatile jint local_dummy = 0;
}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}
}
四类屏障特点
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad | Load1;LoadLoad;Load2; | 保证 load1 的读操作在 load2 及后续读操作之前执行 |
StoreStore | Store1;StoreStore;Store2; | 在 store2 及其后的写操作执行前,保证 store1 的写操作已经刷新到主内存中 |
LoadStore | Load1;LoadStore;Store2; | 在 store2 及其后的写操作执行前,保证 load1 的读操作已读取结束 |
StoreLoad | Store1;StoreLoad;Load2 | 保证 store1 的写操作已刷新到主内存之后,load2 以及其后的读操作才能执行 |
如何保证有序性?
核心思想==>禁止指令重排序
实现方式==>内存屏障
- 重排序有可能影响程序的执行和实现
- 对编译器的重排序,JMM 会根据重排序规则,禁止特定类型的编译器重排序
- 对处理器的重排序,Java 编译器在生成指令序列的适当位置,插入内存品质指令,用于禁止特定类型的处理器重排序
happens-before 之 volatile 变量规则
- 当第一个操作为 volatile 读时
- 无论第二个操作是什么,都禁止重排序
保证了 volatile 读之后的操作不会被重排到 volatile 读之前
- 当第一个操作为 volatile 写时,第二个操作为 volatile 读时,不能重排(写后读)
- 当第二个操作为 volatile 写时,
- 无论第一个操作是什么,都禁止重排序
保证了 volatile 写之前的操作不会被重排序到 volatile 写之后
操作 1 | 操作 2-普通读写 | 操作 2-volatile 读 | 操作 2-volatile 写 |
---|---|---|---|
普通读写 | 可以重排 | 可以重排 | 不可重排 |
volatile 读 | 不可重排 | 不可重排 | 不可重排 |
volatile 写 | 可以重排 | 不可重排 | 不可重排 |
JMM 就内存屏障插入策略分为 4 中规则
- 在每个 volatile 读操作之后插入一个 LoadLoad/LoadStore 屏障
- 禁止处理器把上面的 volatile 读与下面的普通读/普通写重排序
- 在每个 volatile 写操作的前面插入一个 StoreStore/StoreLoad 屏障
StoreStore
可以保证在 volatile 写之前,前面的所有普通写操作都刷新至主内存中StoreLoad
避免 volatile 写与其后的 volatile 读/写重排序
如何保证可见性?
什么是 volatile 的可见性?
- 保证不同线程对某个共享变量完成操作后,结果及时对其它线程可见
- caseDemo
static boolean flag = true;public static void main(String[] args) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + "\t -----come in");while (flag) {}System.out.println(Thread.currentThread().getName() + "\t -----flag被设置为false,程序停止");}, "t1").start();//暂停几秒钟线程try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}flag = false;System.out.println(Thread.currentThread().getName() + "\t 修改完成flag: " + flag);}
- flag 为普通变量时,程序没有停止,flag 已经修改为 false,但是对其他线程(t1)不可见
- flag 为 volatile 变量时,
static volatile boolean flag = true;
- 小结
- 不加 volatile,flag 的修改对其他线程不可见,t1 持续死循环
- 加了 volatile 对其他线程可见,t1 结束死循环
- 案例解释
- 线程 t1 中为何看不到被主线程 main 修改为 false 的 flag?
- 主线程 main 修改了 flag 之后没有刷新到主内存中
- 主线程将最新值刷新到主内存之后,t1 线程一直读取本地工作内存中的 flag,没有将其置为无效再去访问主内存中的最新值
- 场景诉求
- 写操作,对变量进行操作后立即刷新回主内存
- 读操作,舍弃本地变量,从主内存中进行拷贝至工作内存,再读取
- volatile 读写特性与诉求一致
volatile 变量的读写过程
- JMM 中定义了 8 种每个线程自己的工作内存与主物理内存之间的原子操作
- read,作用于主内存,将变量值从主内存中读取到工作内存
- load,作用于工作内存,将 read 从主内存读取的变量值存入工作内存中
- use,作用于工作内存,将变量值交有 CPU 中的 JVM 进行执行
- assgin,作用于工作内存,JVM 执行赋值操作
- store,作用于工作内存,CPU 在 JVM 执行完赋值操作后将更新值写回主内存
- write,作用于主内存,更新主内存中由工作内存 store 过来的变量
上述 6 条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大量加锁,因此,JVM 提供了另外两条原子指令
- lock,作用于主内存,将一个变量标记为线程独占状态,只在写的时候加锁,锁住写变量的过程
- unlock,作用于主内存,将一个处于锁定状态的变量释放
无法保证原子性
- 场景描述
资源类中,一个普通的 int 变量,synchronized 修饰的自增方法
10 个线程并发修改
- 资源类
class MyNumber {int number;public synchronized void addPlusPlus() {number++;}
}
- 测试代码
public static void main(String[] args) {MyNumber myNumber = new MyNumber();for (int i = 1; i <= 10; i++) {new Thread(() -> {for (int j = 1; j <= 1000; j++) {myNumber.addPlusPlus();}}, String.valueOf(i)).start();}//暂停几秒钟线程try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(myNumber.number);}
- 测试结果,正常得到期望值 1000
- 修改资源类
//volatile变量的复合操作不具备原子性volatile int number; public void addPlusPlus() {number++;}
关于上述现象的解释
读取赋值一个普通变量的情况
- 当线程 1 对主内存对象发起 read 操作到 write 操作的过程中,线程 2 随时可能对主内存中的对象发起一轮新的操作
不保证原子性
从 i++的字节码角度说明
- 原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其它线程所影响
- 上述代码中
n++;
不具备原子性,若第二个线程在第一个线程读取旧值和写回新值期间读取 n 的域值,则两个线程均会对相同的 n 执行+1 操作,其结果就是+2 的操作 - 对于 add 方法必须使用悲观锁保证线程安全
结论
volatile 变量不适合参与到依赖当前值的运算(i=i+1;/i++;等)
- 通常使用 volatile 保存某个状态的 boolean 值或者 int 值
- 《深入理解 Java 虚拟机》中提到
由于 volatile 变量只能保证可见性,在不符合以下两条规定的运算场景中,仍需要使用 synchronized 或是 java.util.concurrent 中的锁或者原子类,来保证原子性
- 运算结果不依赖变量的当前值,或者能够确保只有单一线程修改变量的值
- 变量不需要与其他状态变量共同参与不变约束
面试回答
JVM 字节码角度,n++底层指令角度,间隙期不同步非原子操作(n++)
- 对于 volatile 变量具备可见性,JVM 只是保证从主内存加载到线程工作空间的值是最新的(数据加载是最新的)
- 若第二个线程在第一个线程读取旧值和写回新值期间读取 i 的域值,则会有线程安全问题
指令禁止重排序
案例说明
详见上文《Java 内存模型 JMM》JMM 有序性
volatile 底层实现通过内存屏障
- volatile 禁止指令重排序的情况
- 详见上文,happens-before 之 volatile 变量规则
- 四大屏障的插入情况
- 详见上文四类屏障特点
- 详见上文四类屏障特点
代码案例分析
volatile 的使用场景
单一赋值可用,存在复合运算赋值不可用(i++等)
volatile int a = 0;
volatile boolean flag = false;
状态标志,判断业务是否结束
- 详见上述,
3.4 如何保证可见性?
案例
开销较低的读,写锁策略
/*** 使用 : 当读多于写,结合使用内部锁和volatile变量来减少同步的开销* 理由 : 利用volatile保证读取操作的可见性,利用synchronized保证复合操作的原子性*/private volatile int value;public int getValue() {return value; //利用volatile保证读取操作的可见性}public synchronized int increment() {return this.value++; //利用synchronized保证复合操作的原子性}
DCL 双端锁的发布
问题描述
public class SafeDoubleCheckSingleton {private static SafeDoubleCheckSingleton safeDoubleCheckSingleton;/*** 私有构造方法*/private SafeDoubleCheckSingleton() {}/*** 双重锁设计*/public static SafeDoubleCheckSingleton getInstance(){if (safeDoubleCheckSingleton==null) {synchronized (SafeDoubleCheckSingleton.class){if (safeDoubleCheckSingleton==null){//存在隐患==>多线程环境下,由于重排序,该对象可能未完成初始化,就被其它线程所读取safeDoubleCheckSingleton = new SafeDoubleCheckSingleton();}}}//对象不为空,已存在或是创建完毕,执行getInstance不需要获取锁,直接返回对象return safeDoubleCheckSingleton;}
}
- 单线程情况下,隐患处的代码细分为三条指令
memory = allcate(); //1.分配对象内存空间
ctorInstance(memory); //2.初始化对象
instance = memory; //3.设置instance指向所分配的内存地址
- 单线程情况上述代码可以指向;由于存在指令重排序,多线程情况下存在问题
- 指令重排序将 2,3 倒序,造成的后果就是其他线程得到的是 null 并非完成初始化的对象
- 重排序后的指令顺序
memory = allcate(); //1.分配对象内存空间
instance = memory; //3.设置instance指向所分配的内存地址//此时对象还未进行初始化
ctorInstance(memory); //2.初始化对象
解决方案
添加 volatile 修饰
//通过volatile 声明实现线程安全的延迟初始化private volatile static SafeDoubleCheckSingleton safeDoubleCheckSingleton;/*** 私有构造方法*/private SafeDoubleCheckSingleton() {}/*** 双重锁设计*/public static SafeDoubleCheckSingleton getInstance(){//首次检查if (safeDoubleCheckSingleton==null) {synchronized (SafeDoubleCheckSingleton.class){//第二次检查if (safeDoubleCheckSingleton==null){//存在隐患==>多线程环境下,由于重排序,该对象可能未完成初始化,就被其它线程所读取safeDoubleCheckSingleton = new SafeDoubleCheckSingleton();//解决隐患的原理: 利用volatile,禁止"初始化对象"(2)和"设置singleton指向内存空间"(3)的重排序}}}//对象不为空,已存在或是创建完毕,执行getInstance不需要获取锁,直接返回对象return safeDoubleCheckSingleton;}
Summary
- volatile 可见性,基于读写特性
- 写操作,变量值会立即刷新回主内存(不保证原子性,存在写丢失)
- 读操作,总是能够读取到这个变量的最新值
- 当某个线程收到通知,去读取 volatile 修饰的变量时,线程工作内存中的数据就会失效,重新回主内存中读取最新的数据
- volatile 没有原子性,在多线程进行写操作必须加锁
- volatile 禁止指令重排序,基于读操作与写操作的内存屏障
- volatile 写操作,前置 StoreStore 屏障,后置 StoreLoad
- volatile 读操作,后置 LoadLoad,LoadStore
- volatile 与内存屏障的关联,基于 JVM 字节码字段特性
- 内存屏障是什么
内存屏障 : 是一种屏障指令,使得 CPU 或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束,也称为内存栅栏或栅栏指令
- 内存屏障能干吗
- 阻止屏障两边的指令重排序
- 写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
- 读数据时加入屏障,线程私有工作内存的数据失败,重新到主物理内存中获取最新的数据
- 内存屏障四大指令
- 写屏障的 StoreStore/StoreLoad
- 读屏障的 LoadLoad/LoadStore
- 小总结
- volatile 写之前的操作禁止重排序到 volatile 之后
- volatile 读之后的操作禁止重排序到 volatile 之前
- volatile 写之后 volatile 读禁止重排序