被Volatile修饰的变量有两大特点
可见性
有序性(禁重排)
如何保证的?内存屏障
Volatile的内存语义
当写一个Volatile变量的时候,JMM会把该线程对应的本地内存共享变量值立即刷新回主内存。
当读一个Volatile变量的时候,JMM会把本地内存置为无效,重新回到主内存中读取最新共享变量。
内存屏障
内存屏障前的所有写操作都要写回主内存
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
看到写屏障:把这个指令之前的全部写回主内存
看到读屏障:保证了读取到的数据都是最新的
写屏障(Store Barrier)
写指令之后插入
读屏障(Load Barrier)
读指令之前插入
源码分析
度屏障
写屏障
读写混合
Unsafe.class 一定会对应 Unsafe.java
C++代码
细分4种
禁重排
其他情况随便编译器怎么重排序都行
Volatile插入读屏障示意图
Volatile插入写屏障示意图
保证可见性
案例1
结果
程序无法结束
原因:
案例2
将变量声明为volatile
结果
程序正常结束,灯灭了
volatile变量的读写过程
t1线程:
1.先从主内存中读取到自己的工作内存
2.然后use,使用这个变量,当前就一直在while循环里
这里切回main线程看:
1.先从主内存中读取到自己的工作内存
2.然后use,使用这个变量
3.传给cpu,把变量修改为false后,赋值写回工作内存
4.main线程准备把修改写回主内存了,这步要加锁,因为其他线程可能也在往回写,会发生线程安全问题,不就乱了。所以这步一定会加锁的
5.加锁后会清空工作内存中的值,也就是说其他线程手中持有的那份会作废掉,请他们再重新从主内存中重读一份或者重新赋值。
6.此时main线程写完了,立刻解锁,
这里切回t1线程看:
此时工作内存中的变量已经作废了,重新从主内存中拉取最新值为false
t1线程while循环退出
程序结束。
没有原子性
案例1
正常情况:
10个线程 操作1000次 应该是1w
结果:
案例2
此时将变量修改为volatile,同时去除Synchronized关键字
结果:
volatile不具备原子性 不等于1w
读取和赋值一个普通变量的情况
不保证原子性
number++这个操作在java代码看只有一行,但底层其实有3步
1.数据加载
2.数据计算
3.数据赋值
虽然读取到的数据都输最新的,但是写操作可能会出现丢失问题,无法保证原子性。
加了Synchronized关键字的情况
线程A加锁把5改成6,此时线程B原先读的5作废,重新读到6,+1改成7,结果正确。
不加Synchronized关键字的情况
线程B的操作直接作废,比如读到了5然后+1,但还没来得及写,线程A先一步完成,直接把线程B的操作全部作废,产生了写丢失,线程提前结束。
number++字节码分析
原子性:是指一个操作不能被中断,就算在多线程的环境下,也不应该受到其他线程的影响。
建议
指令禁重排
案例1
这里需要加入volatile禁止指令重排序
否则在多线程环境下,i=2和flag=true没有数据依赖性有可能交换顺序,read方法的结果就不可控了
volatile日常使用场景
DCL双端锁单例写法
隐患位置代码:
单线程环境下:
多线程环境下:
不加volatile