一、被volatile修饰的变量有两大特点
- 特点
-
- 可见性
- 有序性---排序要求----有时需要禁止重排
- 不保证原子性
- 内存语义
-
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
- 当读一个volatile时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量
- 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取
二、内存屏障(面试重点)
生活Case:
- 没有管控,顺序难保
- 设定规则,禁止乱序
- 再说volatile两大特性
-
- 可见:写完后立即刷新回主内存并及时发出通知,大家可以去主内存拿最新版,前面的修改对后面线程可见
- 有序:存在数据依赖关系,禁止重排序
1.内存屏障是什么
内存屏障(也称内存珊栏,屏障指令,是一类同步屏障指令,是CPU和编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序,内存屏障其实就是一种JVM指令,Java内存模型的重排序规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型的可见性和有序性(禁重排),但volatile无法保证原子性 。
- 内存屏障之前 的所有写操作都要写回到主内存
- 内存屏障之后 的所有 读操作 都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
- 因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话:对一个volatile变量的写,先写发生于对这个volatile变量的读,也叫写后读。
2.分类
a.粗分
- 读屏障(Load Barrier)****
-
- 在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存获取数据
- 写屏障(Store Barrier)
-
- 在写指令之后插入写屏障,强制把写缓冲区的数据刷回主内存
b.细分
| 屏障类型 | 指令实例 | 说明 |
|---|---|---|
| LoadLoad | Load1;LoadLoad;Load2 | 保证load1的读取操作在load2及后续操作之前执行 |
| StoreStore | Store1;StoreStore;Store2 | 在store2及其后的写操作执行前,保证store1的写操作已经被刷新回主内存 |
| LoadStore | Load1;LoadStore;Store2 | 在store2及其后的写操作执行前,保证load1的读操作已经读取结束 |
| StoreLoad | Store1;StoreLoad;Load2 | 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行 |
3.细讲
a.什么叫保证有序性
禁重排:
-
通过内存屏障禁重排
-
b.happens-before 之 volatile 变量规则
| 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前 |
|---|
| 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile之后 |
| 当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。 |
c .四种内存屏障的插入策略
- 读屏障
-
- 在每个volatile读操作的后面插入一个LoadLoad屏障
- 在每个volatile读操作的后面插入一个LoadStore屏障
| 在每个volatile读操作的后面插入一个LoadLoad屏障 | 禁止处理器把上面的volatile读与下面的普通读重排序 |
|---|---|
| 在每个volatile读操作的后面插入一个LoadStore屏障 | 禁止处理器把上面的volatile读与下面的普通写排序 |
- 对比图
- 写屏障
| 在每个volatile写操作的前面 插入一个StoreStore屏障 | 可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中 |
|---|---|
| 在每个volatile写操作的后面插入一个StoreLoad屏障 | 作用是避免volatile写与后面可能有的volatile读/写操作重排序 |
三、volatile特性
一、保证可见性
保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见。
volatile变量的读写过程
Java内存模型中定义的8种每个线程自己的工作内存与主物理内存之间的原子操作:
read(读取)-->load(加载)--->use(使用)--->assign(赋值)--->store(存储)--->write(写入)---->lock(加锁)--->unlock(解锁)
二、没有原子性
public class VolatileNoAtomicDemo {
private volatile int number;
public void addPlusPlus(){
number++;
}
public static void main(String[] args) throws InterruptedException {
VolatileNoAtomicDemo volatileNoAtomicDemo = new VolatileNoAtomicDemo();
for (int i=1;i<=10;i++){
new Thread(()->{
for (int j=1;j<=1000;j++){
volatileNoAtomicDemo.addPlusPlus();
}
},String.valueOf(i)).start();
}
TimeUnit.SECONDS.sleep(2);
System.out.println(volatileNoAtomicDemo.number);
}
}
volatile变量的复合操作不具有原子性,比如number++
- 读取赋值一个普通变量的情况
- 不保证原子性
-
- 从 表面上看是i++ 一个操作 ,但在 源代码层面是三个操作
- 对于volatile变量具备可见性,JVM只是保证从主内存加载到工作内存的值是最新的,也仅是数据加载时是最新的。但是多线程环境下,"数据计算"和"数据赋值"操作可能多次出现,若数据在加载之后,若主内存volatile修饰变量修改之后,线程工作内存中的操作将会作废去读主内存最新值,操作出现丢失问题
- 即各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。
- 从i++的字节码角度说明
-
- 原子性指的是一个操作是“不可中断”的,即是是在多线程环境下,一个操作一旦开始就不会被其他线程影响
- 结论:volatile变量不适合参与到依赖当前值的运算
-
- 通常volatile用来保存某个状态的boolean值 or int值。
- 由于volatile变量只能保证可见性,在不符合一下两条规则的运算场景中,仍要公共加锁保证原子性
-
-
- 运算结果并不依赖变量的当前值 ,或者能够 确保只有单一的线程修改变量的值
- 变量不需要与 其他的状态变量 共同参与不变约束。
-
三、指令禁重排
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时会改变程序语句的先后顺序。 存在数据依赖关系,禁止重排序。
注意: 重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
数据依赖性: 若两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两操作间就存在数据依赖性。
code例子:
volatile写之前的操作,都禁止重排序到volatile之后
public void write(){
i=2;
flag=true;
}
public void read(){
if(flag){
System.out.println("----i = "+i)
}
}
四、如何正确使用volatile
- 单一赋值可以,but含复合运算赋值不可以(i++之类)
-
- volatile int a=10
- volatile boolean flag = false
- 状态标志,判断业务是否结束
- 开销较低的读,写锁策略
- DCL双端锁的发布
-
- 传统的单例写法
- 单线程环境下(或者说正常情况下),在问题代码处,会执行如下操作,保证能获取到已完成初始化的实例
- 传统的单例写法
-
-
- singleton = new SageDoubleCheckSingleton()分三步
- memory= allocate 分配对象的内存空间
- ctorInstance(memory) 初始化对象
- instance = memory 设置instance指向刚分配的内存地址
-
-
- 隐患
五、总结
- volatile可见性
- volatile没有原子性
- volatile禁重排
- 凭什么我们Java写了一个volatile关键字系统底层加入内存屏障,两种关系怎么勾搭上的?
-
- 字节码层面
- 字节码层面
- 内存屏障
- 内存屏障能干啥
-
- 阻止屏障两边的指令重排序
- 写数据时加入屏障,强制将线程私有工作内存的数据刷新回主物理内存
- 读数据时加入屏障,线程私有工作内存的数据失效,重新回到主物理内存中获取