大家熟知的Volatile保证了可见性,禁止指令重排,那么底层到底是如何实现的呢?
1.指令重排,什么是指令重排?
1.1 指令重排的概念
指令重排序是指源码的代码顺序与实际执行的顺序不相同,重排的单位为指令。
重排序一般分为三种类型:
1.编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2.指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3.内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
1.2 为什么要指令重排?
我们知道程序运行时,cpu需要从内存中读取数据来执行,但是cpu的速度是内存的N倍,指令又一般分为取指、译码、访存、执行、写回等等个阶段,当如果有两条指令是这样的话:
1.去内存中读一个数据,然后从内存里面返回一个值。
2.CPU做另外一个操作,对里面的值做一个计算,比如说对寄存器里面某一个值进行加1减1这样的一个操作。
由于CPU的速度要比内存快很多当cpu从内存中加载数据的时候cpu只能等待着完成,显然并没有发挥出cpu效率高的优点,所以在底层会做一些指令重排的操作来尽可能的发挥出cpu的性能,当然在进行指令重排时候也有一定的规则。
2.Volatile如何禁止指令重排?
1.什么叫做内存屏障?
内存屏障其实就是一条指令,可以保证对编译器和处理器的指令重排做出一定的限制,对于CPU层面的内存屏障指令分为:lfence(读屏障),sfence(写屏障),mfence(全屏障)
2.Volatile是怎么做的呢?
volatile有序性是通过内存屏障实现的。JVM和CPU都会对指令做重排优化,所以在指令间插入一个屏障点,就告诉JVM和CPU,不能进行重排优化。
3.如何保证可见性?
说到可见性一定要提的就是JMM和MESI缓存一致性协议,那么JMM是怎么样呢?
1.java内存模型
如上图所示所有线程的共享变量都存储在主内存中,每一个线程都有一个独有的工作内存,每个线程不直接操作在主内存中的变量,而是将主内存上变量的副本放进自己的工作内存中,只操作工作内存中的数据。当修改完毕后,再把修改后的结果放回到主内存中。每个线程都只操作自己工作内存中的变量,无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
这样就会产生一个问题,当多个线程从主存中拿到数据对工作内存中当前的数据进行操作时候并不知道其他的线程是否进行了修改,比如数据一个变量A,线程1将A读取到工作内存中,同时线程2也将A读取到工作内存中,两个线程都对A进行+1然后回写到主存,这时候主存的数据只会+1产生了错误。
2.MESI是什么?
MESI协议是基于Invalidate的高速缓存一致性协议,并且是支持回写高速缓存的最常用协议之一。
MESI通过对标记高速缓存行的数据独占状态,而CPU会对状态来进行监听。
3.Volatile是如何保证的可见性?
volatile保证可见性是基于cpu的lock指令和MESI协议来做到的,上文已经说过MESI,配合lock指令即可做到可见性,lock指令触发将当前缓存行的数据回写到主存中,而此时则会出发MESI缓存一致性协议来保证了可见性。
MESI缓存一致性协议并不是完全可以解决的,当缓存行不可放下数据的时候,缓存一致性协议是无法达到预期的效果的,会触发总线锁来保证。
Volatile并不能保证原子性。原子性还需要synchronized或者其他方式。
另外给大家推荐一个公众号



