2020年五一的倒数第二天假了,其实还是那个想法,如果是个穷人,其实假期对于自己来说,只是一个追赶别人的一个机会。废话不多说,今天聊一下并发编程中的valatile。
volatile,是什么,它是修饰变量的,修饰完了之后有什么变化呢?
两点:1,让该变量在多个线程之间保持可见性 2,在jvm层次可以设置内存屏障防止重排序的事情发生
什么是线程的可见性?或者说什么是线程的不可见性?
不管单核的还是双核的cpu其实都有着自己的缓存,例如上图,在对A变量进行计算动作的时候,cpu会先把A加载到自己的缓存空间之中(这里有cpu的一级,二级,三级的缓存之说,我们就统一叫缓存),然后通过对缓存中的A进行计算,动作完成之后再刷新到内存之中(严格意义上讲应该叫做主存当中),这里便于理解就叫内存,这样就会有一个问题,如果cpu1把A的值修改了,但是cpu2 读的还是之前它自己缓存的A,那么就不对了,这就是线程的不可见问题,那么怎么办?加了volatile之后,如果cpu1 修改了A的值,它会通知cpu2 该变量也就是该缓存行失效了,你再读的时候需要去主存里面读,而不是读自己的缓存。
这就是volatile保持可见性的原因。
听到这里,肯定是又有些不明白,怎么 通知的,为什么可以通知?
要说明上面的两个问题,不得不说一下缓存行,与缓存一致性的概率、
首先什么叫做缓存行?
cpu读取的时候,是通过一行或者说一块来读的,你可以理解为读取的时候是需要用一个数组来读的,而这个数组有一个长度,这么设计是考虑到,大多数相邻的变量一起使用的概率要大一些,所以如果可以一起读到缓存当中,下次取相邻的变量的时候可以直接去自己缓存中拿,提高效率。这样有一个问题呀?那么这个缓存行该定义多大呢? 在64位操作系统中,这个缓存行是64个字节。
64这个数字是怎么来的? 思考一个问题,如果缓存行的容量是1,那么每次读取的速度是非常快的,但是局部利用率是0; 如果缓存行的容量是10000,那么每次读取的速度是很慢的,但是局部利用率是很大的,所以64是综合考虑取得一个折中值。
缓存行,了解了,那么什么是缓存一致性,就是两个cpu都加载了自己的缓存行,刚好两个缓存行都包含一个A变量,那么如果cpu1改变了这个A的值,它自动通知cpu2 让它不要读自己的缓存中的A,去主存中刷新这个A的值,这就是缓存一致性。我们常说的mesi其实只是实现了缓存一致性的一种方式。
可见性的问题是搞清楚了,但是内存屏障和防止重排序是什么呢?
依旧先了解一个常识,就是cpu并不是每次都按照我们写的代码一行一行的,下面一行一定要等到上面一行执行为完毕,才会执行。例如:
x=a;
y=1;
上面这两行代码,如果x=a,比y=1要耗时长,然后他们两个之间也没有相互关系,我是不需要等到x=a执行完了之后才去执行y=1的。或者说我也可以先执行y=1;这个赋值操作。
再例如:
class Object{
c=1;
}
Object o = new Object();
在new的时候,大体有四个动作:1,申请一块内存空间; 2,初始化C设置初始值0 ;3,初始化C设置真实值为1 ;4,将o指向c
其实3和4就可以倒过顺序来执行,这就是cpu的乱序执行,指令的重排序问题。其实它的目的是为了提高cpu的执行效率。
as-if-serial : 看起来是序列化的,说的就是不管cpu如果进行指令的重排序,最后一个线程从上往下执行的最后结果是不变的。
happens-before: 说的是 在jvm层次上,如果要进行重排序需要遵循的规则。load store随机组合的内存屏障实现。
阻止重排序能解决什么问题:最典型的就是DCL(double check lock)模式下的单例,安全问题,只有加了volatile才能保证该单例任何时候才是完整的,这里就不展开说了,上面其实已经自己讲过了 new的四个动作。也是问题的关键所在。
最后说一下,虽然volatile解决了可见性和有序性,但是它并未解决原子性的问题。原子性怎么解决?那就去上一篇synchronize的文章。