多线程之volatile关键字解读

   接触过java的小伙伴们都知道,java有很多很好用的关键字,平常我们自己写代码的时候也会经常用到,但是并没有几个人真正了解其中含义和实现原理,今天我就带大家来详细解读一下volatile关键字。

    在这之前,我们先来看看现在计算机的硬件内存架构图:

早期的计算机并没有缓存这个东西,后来是因为在CPU和内存之间频繁地读取数据效率太慢而增加,其实这里面还隐隐地有二八定理地影子。当CPU核心要取数据地时候先会取命中缓存,如果没有就从内存读取到缓存行。在这种系统架构里面内存与其他处理器交互地场地是在PCI总线上的。缓存处理器在处理各自内容的时候是相互不可见的,所以在多线程条件下做读取和存储操作的时候会出现线程安全问题,这时候操作系统给我们提供了两种解决办法:总线锁和缓存锁,总线锁实际上就是在一个线程读取某个数据的时候强行占据整个总线不让其他CPU处理,很显然这种方式效率是及其低下的。缓存锁的实现主要是靠Lock前缀指令,这个指令会把当前缓存行的内容刷回到主内存,通过缓存一致性协议(MESI)保证数据的一致性。在java的内存模型中,也有类似的模型:

不过这个本地内存(线程私有)是抽象出来的概念,并不真实存在。理解java里面的线程安全问题首先要知道java中的多线程通信方式包含共享变量以及消息传递。共享变量的方式是对程序员透明的,也就是隐式传递消息,所以一旦出现多线程安全问题,不了解其中原理的人很容易不知所措。我们知道堆内存是所有线程共享的,存储在其中的实例对象,静态变量和数组对象都可以作为我们的通信媒介。在JDK1.5之后java使用JSR-133内存呢模型,通过happens-before原则来描述操作之间的内存可见性,通常来讲我们比较有关联的原则有(这里用hb表示):

        1.在单线程里面的任意一个操作hb于后续操作

        2.对一个锁的解锁hb与获取锁的操作

        3.对一个volatile变量的写操作hb于对该volatile变量的读操作

        4.如果A hb B,B hb C,则 A hb C

怎么理解hb,前面说到这个原则是用来描述操作之间的内存可见性的,那么就意味着在单线程里面,前面的操作结果应该对后续结果都是可以看见的,那么对volatile变量而言,所有更改该值的操作结果对读取都是可见的,这就保证了读取时候的一致性。那么这个原则在JMM里面是如何实现的呢?答案就是禁止重排序。首先我们来了解一下为什么会有重排序这个东西。

        为了追求性能,java开发者分别在编译器和处理器上做了优化,它允许编译器在不影响程序具体语义的条件下可以重新安排语句执行顺序,比如说a=0;b=0;:这两个操作的前后操作交换并不影响第二条语句后续操作的理解,这时候可以是b的赋值在前。而处理器重排序的出现是因为现代处理器采用了指令并行来将多条指令重叠执行,所以在不影响数据依赖的条件下可以改变语句对应机器指令的执行顺序。

        前面说到volatile变量是通过禁止重排序完成的,而禁止重排序是使用了内存屏障的指令完成的。JMM把内存屏障指令分成四类:分别是LoadLoad,LoadStore,StoreLoad,StoreStore,就我的理解上来将可能把store看成是存储值,load看成是读取值会好记一些。对于Load在前的指令,都是确保前面的读取指令会先于后续的读取/存储指令,而Store在前的指令则是确保前面的存储指令(刷新到内存)会对后续的存储和读取可见。不过StoreLoad是个特殊指令,在满足前面的条件的同时,它还会等该屏障之前的所有指令都完成之后才开始执行之后的指令。

        JMM为volatile变量提供了一个保守型的内存屏障插入,它在每个volatile写操作前加StoreStore,在写之后加StoreLoad,在读之前加LoadLoad,读之后加LoadStore(真好记)。从它给的指令可以知道,volatile的写操作前的写操作都会对这该操作可见,其他同理。

猜你喜欢

转载自blog.csdn.net/qq_36243399/article/details/79768239
今日推荐