两张图理解volatile关键字

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sureSand/article/details/82557274

最近又看了一下volatile关键字,以前觉得花很大功夫才理解的东西发现其实也没自己想的那么难,画个模型图然后再跑跑代码感觉很容易就理解了,而且记的也牢,虽然JDK1.7,JDK1.8的synchronized关键字已经优化的很好了,但也不能synchronized关键字一条道走到黑,哈哈。

  1. 内存模型概念

计算机执行程序时,指令都是由cpu执行的,执行指令必然会涉及到数据的读写,cpu执行的速度远比从内存读写数据快,所以读取内存数据成了cpu执行速度的拦路虎,所以cpu就有了一个高速缓存。
也就是当cpu运行程序时会将程序所需的数据复制一份到cpu的高速缓存中,完成计算后将最新数据刷到主存中,如图

这里写图片描述

2.volatile如何保证可见性?
由上图我们思考如果两个线程同时进行i=i+1操作:

这里写图片描述

那么在不做任何处理的情况下i最终会等于3吗?
答案显然是不一定?

假如线程1,线程2同时把i=1复制到高速缓存,线程1执行完i=i+1后返回数据到主存将i重新赋值为2,然后线程2的高速缓存并不知道主存的i已经变成2,它还是继续将i+1执行结果2重新赋值给主存i,这样经过两次计算i的值还是2!

这就是著名的缓存不一致的问题,解决这个问题一个是通过加锁的办法,即每次保证只有一个cpu读到这个这个主存的i值就不会有两个高速缓存的冲突,但这种方式太低下,于是我们想到有没有一种方法保证线程1的i值改变后立刻告诉线程2?

当我们想到这里时,就离理解volatile关键字的精髓不远了,当我们用volatile修饰一个共享变量时,只要一个线程改变这个变量,它会将最新的值更新的主存,并告诉其他线程改变量高速缓存失效需重新读取,这样就保证了i值的可见性,也保证了i值不会被重新的不正确赋值。

3.volatile为什么不保证原子性?
volatile有个性质就是如果变量没有变化就不会通知其它线程该变量在高速缓存无效!

i=i+1的自增操作我们知道不是原子性的:

第一步:线程先读取了变量i的原始值
第二步:线程进行+1操作,并将值刷新到主存

我们试想线程1先读取了i的值,但此时假如它被阻塞没有将i值变化,线程2的高速缓存的变量i就不会变化,还是将i+1=2,然后将2刷新赋值主存。此时线程1的高速缓存值会变成2,但是之前线程1已经完成了赋值1,还会继续进行计算i+1=2,最后将i=2刷新到主存,结果还是错误的!

4.volatile怎样保证有序性?

一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。当然语句2依赖于语句1,那么语句1无论什么情况都会在语句2前执行,例如:

int i=1;//语句1
i++;//语句2

但是如果是:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
  sleep() 
}
doSomethingwithconfig(context);

  上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
  
 从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
如果我们用voliatle关键字对inited 修饰,那么就会保证执行到inited = true; 的时候context = loadContext(); 已经执行完了。

volatile能禁止指令重排序(所以volatile能在一定程度上保证有序性),但是这里只能保证volatile所修饰的变量之前的程序不会在该变量之后执行,该变量之后的代码不会在变量之前执行。

5.volatile使用场景??
volatile的使用场景在我看来只要保证了原子性操作的情况下都可以使用。
例如最典型的状态变量标记:

volatile boolean flag = false;

while(!flag){
    doSomething();
}

public void setFlag() {
    flag = true;
}


volatile boolean inited = false;
//线程1:
context = loadContext();   
inited = true;             

//线程2:
while(!inited ){
sleep() 
}
doSomethingwithconfig(context);

其实通过volatile我们也了解了要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

猜你喜欢

转载自blog.csdn.net/sureSand/article/details/82557274