文章目录
- 1、volatile可见性案例
- 2、线程工作内存与主内存之间的原子操作
- 3、volatile变量不具有原子性案例
- 4、无原子性的原因分析:i++
- 5、volatile变量小总结
- 6、重排序
- 7、volatile变量禁重排的案例
- 8、日常使用场景
- 9、总结
volatile变量的特点:
- 可见性
- 禁重排
- 无原子性
1、volatile可见性案例
volatile的可见性,即保证不同线程对某一个变量一旦完成更改,其他线程立即可见,因为会从线程的工作内存立马刷到主内存。Demo程序:
public class VolatileDemo1 {
static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "come in ...");
while (flag) {
}
System.out.println("flag被设置为false,线程任务执行结束");
}, "t1").start();
TimeUnit.SECONDS.sleep(2);
flag = false;
System.out.println(Thread.currentThread().getName() + "线程已将flag改为false");
}
}
flag已被main线程改为false,但t1线程没有收到通知而一直在循环:
变量改为volatile变量:
static volatile boolean flag = true;
重新运行:
t1程序可正常停止了,原因就是加了volatile后,flag变量有了可见性,main线程改完后t1可以知道这个变更。
线程t1中为何看不到被主线程main修改为false的flag的值?
可能原因有:
- 主线程修改了flag之后没有将其刷新到主内存,所以t1线程看不到
- 主线程将flag刷新到了主内存,但是t1一直自娱自乐,读取的是自己工作内存中fag的值,没有去主内存中更新获取flag最新的值
想解决这个问题需要:
- 线程中修改了自己工作内存中的副本之后,立即将其刷新到主内存
- 工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。
使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:
- 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
- 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
2、线程工作内存与主内存之间的原子操作
-
read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
-
load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
-
use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时就会执行该操作
-
assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时就会执行该操作
-
store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存
-
write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量
-
lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程
-
unlock:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
以上面的flag变量为例,说明volatile变量的读写过程:
- step1:先从主内存中读到,然后load加载到线程t1自己的工作内存(read、load成对出现),开始use到while循环,然后一直循环
- step2:main线程同样的操作从主内存load到自己的工作内存,但它配合CPU完成了对flag的赋值(assign),并存储(store)到自己的工作内存
- step3:接下来main线程准备要把这个变更写回主内存了,此时必须加锁lock,写完后解锁unlock
- step4:上一步的加锁后会清空其他线程工作内存这个变量的值,在使用变量前必须重新load或者assign,因此t1线程可以获取到最新的变量值
3、volatile变量不具有原子性案例
volatile变量的复合操作不具有原子性,比如number++
先看不用volatile的:
class MyNumber{
int number;
public synchronized void addPlus(){
number++;
}
}
同时开十个线程,每个线程调用1000次addPlus方法:
public class VolatileDemo1 {
public static void main(String[] args) throws InterruptedException {
MyNumber myNumber = new MyNumber();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myNumber.addPlus();
}
},String.valueOf(i)).start();
}
//给上面线程的计算时间
Thread.sleep(2000);
System.out.println(myNumber.number);
}
}
正常输出10000,修改:不用synchronized,变量改为带volatile关键字的:
运行,接近10000,但不会等于10000:
4、无原子性的原因分析:i++
在没有加锁的控制时,就没有原子性的保证(synchronized依靠monitor来保证同一时间只能有一个线程来操作):当线程1对主内存对象发起read操作到write操作第一套流程的时间里,线程2随时都有可能对这个主内存对象发起第二套换作,如下图,虚线表示线程2的读取的可能时机:
从源代码来看,number++只有一行,但对应到底层则是:
而线程自己的工作内存里,数据加载、计算和赋值这三步,不是原子操作,是可能被分开的。
对于volatile变量的可见性,JVM只是保证从主内存加载到线程的工作内存的值是最新的,即数据加载这一步是最新的,对比上面的案例:主内存中,volatile修饰的变量number=5,此时线程A和线程B都能读,线程A要进行+1,线程B也要进行+1,但线程B在CPU的调度下一口气走完了数据加载、计算和赋值这三步,并刷回主内存,此时主内存number=6,而线程B比较慢,刚做完+1的计算,但由于volatile的可见性,主内存中已经等于6了,线程B的值作废,去主内存重读,number = 6,然后线程B一路number+1=7并刷到主内存,一看,两个线程,做了三次+1的操作,结果number只是从5变到7,这就是上面结果接近10000但小于10000的原因
由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步,加锁,同一时间,最多只能有一个线程进来,每次+1,都能走完写的整体流程,因为其他线程进不来,没有上面那种刚完成数据加载,然后数据就被别的线程改了并刷到主内存,导致自己刚加载的作废的情况。再从i++的字节码来看:
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
很明显,i++是个复合操作,分了三步,不具备原子性。如果第二个线程在第一个线程读取旧值和新值写回期间读取了i的值,就会出现两个线程同时对一个值做加1的情况。比如i=6时,线程1完成了6+1,在写回新值前,线程2读取了数据,继续6+1,不管是线程1和线程2谁先写回主内存,(哪怕都写回主内存也是个7,何况volatile下,慢的那一个线程会去主内存重读),都是6+1做了两次,但最后等于7,相当于加了1次
5、volatile变量小总结
volatile变量不适合参与到依赖当前值的运算,比如i = i+1 ; i++之类的
依靠volatile变量的可见性,其适合用于保存某个状态的Boolean值
6、重排序
重排序是编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,但前提是不能改变原语义,或者说指令不能存在数据依赖关系,数据依赖关系即:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖证。
若存在数据依赖关系,禁止重排序===> 重排序发生,会导致程序运行结果不同。
7、volatile变量禁重排的案例
Demo:
public class VolatileDemo2 {
int i = 0;
volatile boolean flag = false;
public void write(){
i = 2;
flag = true;
}
public void read(){
if(flag){
System.out.println("---i = " + i);
}
}
}
在每个volatile写操作后面插入写读屏障:
在每个volatile读操作后面插入读写屏障,
如此,程序原来的的语义就得到的保证
8、日常使用场景
Case1:单一赋值可以,但是含有复合运算赋值(如i++)不适用
volatile int a = 10;
Case2:在高并发里面,如果是靠变量来通知其他线程来改变后续动作的,那可利用volatile变量的可见性,做状态标志位,判断业务是否结束
volatile boolean flag = false;
做为一个布尔状态标志,判断业务是否该结束了
Case3:开销较低的读写锁策略
get和increment方法都加synchronized,安全性是保证了,但太重,性能下降太多:
public class Counter{
private int vlaue;
public synchronized int getValue(){ //读也得先拿对象锁
return value;
}
public synchronized int incerment(){
return value++;
}
}
考虑synchronized结合volatile,此时,也可以每次都读到最新的数据,即使没加锁:
public class Counter{
private volatile int vlaue; //volatile
public int getValue(){ //利用volatile可见性保证并发下也能读取到最新值
return value;
}
public synchronized int incerment(){ //利用synchronized保证复合操作的原子性
return value++;
}
}
Case4:DCL双端检查锁的禁重排
参考经典文章:这篇循序渐进,都讲明白了:
- 【单例模式下的DCL】
大概贴下代码:双端检查锁的普通代码:
问题:
重排序后,先给变量指向了分配的内存地址,在初始化对象前,多线程下,其他线程获取,判断对象是否为null,很明显,对象内存地址不为null了,但其实对象还没new,后面用它就会空指针:
需要给这个变量加volatile关键字来禁止指令重排:
9、总结
凭什么java写了一个volatile关键字,就可以让系统底层加入内存屏障?两者关系怎么勾搭上的?
什么是内存屏障?
内存屏障是一种 屏障指令,它使得 CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束 。 也叫内存栅栏或栅栏指令。
内存屏障能干嘛?
- 阻止屏障两边的指令重排序
- 写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
- 读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据
内存屏障的四大指令?
- 在每一个volatile写操作前面插入一个StoreStore屏障
- 在每一个volatile写操作后面插入一个StoreLoad屏障
- 在每一个volatile读操作后面插入一个LoadLoad屏障
- 在每一个volatile读操作后面插入一个LoadStore屏障
总之:volatile即可见性、禁重排、以及无原子性
- volatile 写之前的操作,都禁止重排序到 volatile 之后
- volatile 读之后的操作,都禁止重排序到 volatile 之前
- volatile 写之后 volatile 读,禁止重排序