volatile的总结

volatile

volatile是java中热门关键字,也是面试中的高频问点,程序员也要不断的给自己充电,下面就让我们一起聊一聊volatile。
1.什么是内存可见性?volatile怎么保证内存可见性?
2.volatile能保证原子性么?为什么?
3.什么是重排序?怎么实现的?

一、线程与线程之间是怎么通信的?
在了解什么是可见性前我们先看一张图,了解在多线程的条件下,线程与线程之间是怎么通信的,
【】共享变量存储在主内存中,每个线程都可以访问
【】每个线程都有自己的私有工作空间及工作内存—》主内存的副本
【】线程不能直接操作主内存,只有操作了工作内存之后才能写入主内存。
volatile的总结
这里的工作内存类似于缓存,并非实际存在的,因为缓存的读取和写入的速度远大于主内存,这样就大大提高了cpu与数据交互的性能。

所有的共享变量都是直接存储于主内存中,工作内存保存线程在使用主内存共享变量的副本,当操作完工作内存的变量,会写入主内存,完成对共享变量的读取和写入。

在单线程时代,不存在数据一致性的的问题,线程都是排队的顺序执行,到了多核多线程的时代,虽然提升了cpu的执行效率,但是却出现了缓存一致性的问题,
线程1修改了副本x的值并刷回主内存,由于cpu的运行时非常快的,线程2并不知道主内存的值被修改了还是操作了工作内存的缓存值,这样就导致了缓存一致性问题。
为了解决数据的一致性问题,通常主流的解决方法有如下两种:
【1】通过总线加锁的方式。
【2】通过缓存一致性协议。
第一种方式常见于早期CPU当中,是一种悲观的方式,CPU和其他组件的通信方式都是通过总线来进行的,如果采用总线加锁的方式则会阻塞其他CPU的组件的访问,同一时间的只有获取到总线锁的CPU才有访问变量内存的权力,这种方式效率极低。
所以就有了第二种方式,通过缓存一致性协议来解决缓存一致性。缓存一致性的大致思想是:
(1)读操作时不做任何处理。
(2)写操作时,发出信号通知其他CPU将变量的Cache line 置为失效。其他CPU在进行对变量的操作不得不到主内存中再次获取。
二、为什么volatile具有可见性?
使用volatile修饰后可产生内存屏障,这是一条CPU指令,可以影响数据的可见性。当变量用volatile修饰时,将会在写操作的后面加一条屏障指令,在读操作的前面加一条屏障指令。这样的话,一旦你写入完成,可以保证其他线程读到最新值,也就保证了可见性。

三、不保证原子性
原子性,就是说一个操作不可被分割或加塞,要么全部执行,要么全不执行。

如i = 1,但是像j = i或者i++都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。

所以假如一个volatile的integer自增(i++),其实要分成3步:

第一步:读取主内存中volatile变量值到工作内存;
第二步:在工作内存中增加变量的值;
第三步:把工作内存的值写主内存。
假如有两个线程都要执行i变量的自增操作,当线程1执行i++;语句时,先是读入i在主内存的值为0并且对变量做+1操作,此时线程2来了线程1被挂起的执行时间被让出。
线程2获得执行,线程2会重新从主内存中,读入i的值还是0,因为线程1对i的赋值操作还未写会主内存,然后线程2执行+1操作,最后把i=1刷新到主内存中;
线程2执行完后,线程1又开始执行,但之前已经读取的i的值0,因为前面的读取原子操作已经结束了,所以它还是在0的基础上执行+1操作,也就是还是等于1,并刷新到主内存中。所以最终的结果是i变量的值为1.从而导致了对变量进行了两次+1操作却和预期的结果不同。
volatile的使用场景:
【1】一写多读
【2】对变量的写操作不依赖于当前值。
【3】该变量没有包含在具有其他变量的不变式中。
四、指令重排序
编译器重排序就是在不改变单线程的语义的前提下,可以重新排列语句的执行顺序。从而提升程序编译及运行的效率。但是要保证重排序前后的结果一致。
如果数据不存在相互依赖关系,处理器可以改变机器指令的执行顺序,从而提高程序的执行效率,但是在多线程中假如两行的代码存在数据依赖,将会被禁止重排序。
public void find(){
int a = 3;//1
int b = 5;//2
int c = a+b;//3
}
这里a=0,b=1两句可以随便排序,不影响程序逻辑结果,但c=a+b这句必须在前两句的后面执行。因为c的赋值依赖于a和b。
在JDK5开始,为了保证程序的有序性,便提出了happen-before原则,假如两个操作符合该原则,那么这两个操作可以随意的进行重排序,并不会影响结果的正确性。
happen-before原则
1.对一个锁的解锁,happens-before于随后对这个锁的加锁。
2.传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
3.如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
4.对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
5.对一个对象的gc动作一定发生在对这个对象初始化之后。
6.对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
五、 volatile与synchronized区别
volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。
volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。
volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。