作用原理
volatile是Java虚拟机提供的轻量级的同步机制。
两个作用
- 可见性:保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
- 有序性:禁止指令重排序优化
但无法保证原子性。
作用实现
可见性
package com.gx.demo.bingfa; public class VolatileTest { private volatile boolean changeFlag = false; public void save() { this.changeFlag = true; System.out.println("线程:" + Thread.currentThread().getName() + " 修改了主存中的共享变量changeFlag"); } public void load() { while (!changeFlag) { } System.out.println("线程:" + Thread.currentThread().getName() + " 感知到了changeFlag变量的修改"); } public static void main(String[] args) { VolatileTest sample = new VolatileTest(); Thread threadA = new Thread(() -> { sample.save(); },"threadA"); Thread threadB = new Thread(()-> { sample.load(); },"threadB"); threadB.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } threadA.start(); } } 结果: 线程:threadA:修改共享变量changeFlag 线程:threadB 感知到了changeFlag变量的修改
有序性(禁止指令重排)
禁止指令重排优化指的是:避免多线程环境下程序出现乱序执行的现象。
内存屏障
概念:
什么是内存屏障(Memory Barrier)?
内存屏障(memory barrier)是一个CPU指令。
它的作用有两个:
a) 确保一些特定操作执行的顺序;
b) 影响一些数据的可见性(可能是某些指令执行后的结果,保证在内存中可见)。
编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。
例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。
硬件层的内存屏障
Intel硬件提供了一系列的内存屏障,主要有:
- lfence,是一种Load Barrier 读屏障
- sfence, 是一种Store Barrier 写屏障
- mfence, 是一种全能型的屏障,具备ifence和sfence的能力
- Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
JVM提供的四类内存屏障
Java内存屏障主要有Load和Store两类。
对Load Barrier(读)来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据
对Store Barrier(写)来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存
对于Load和Store,在实际使用中,又分为以下四种:
屏障类型 |
指令例子 |
说明用途 |
LoadLoad |
Load1,Loadload,Load2 |
确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入 |
StoreStore |
Store1,StoreStore,Store2 |
确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据) |
LoadStore |
Load1; LoadStore; Store2 |
确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。 |
StoreLoad |
Store1; StoreLoad; Load2 |
确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见 |
volatile的有序性实现
JMM针对编译器制定的volatile重排序规则表。
第一个操作 第二个操作:普通读写 第二个操作:volatile读 第二个操作:volatile写
普通读写 可以重排 可以重排 不可以重排
volatile读 不可以重排 不可以重排 不可以重排
volatile写 可以重排 不可以重排 不可以重排
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。
∙在每个volatile写操作的前面插入一个StoreStore屏障。
∙在每个volatile写操作的后面插入一个StoreLoad屏障。
∙在每个volatile读操作的后面插入一个LoadLoad屏障。
∙在每个volatile读操作的后面插入一个LoadStore屏障。
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到
正确的volatile内存语义。
有序性示例:
单例模式中的DCL(double check lock)。
private static MyTest instance; public static MyTest getInstance(){ if(instance == null){ synchronized (MyTest.class){ if(instance == null){ instance = new MyTest(); } } } return instance; } instance = new MyTest();
在内存中具体分为几个步骤实现
1.给对象分配内存空间
2.初始化对象 init()
3.给变量分配内存地址
步骤2、3可能会发生重排序,所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
解决办法:
private volatile static MyTest instance;//禁止指令的重排
原子性
volatile无法保证原子性。
static volatile int i = 0; public static void caculate(){ i++; }
i++分为:先去读取i的值,然后再+1写入一个新的值,两个步骤完成,本身不具备原子性。
假如在第一步完成之后,第二步执行之前时,有线程在此时读取了i在内存中的值,那么这个线程会和开始那个线程相当于要对i执行一样的操作,i结果都是1。也就造成线程安全失败了。
解决办法:对执行方法添加synchronized,但是synchronized一样具备了可见性,可以不用volatile修饰了。