volatile原理
由上文的Demo效果,我们可以知道volatile解决了可见性问题,再加上可见性的原理分析和解决方法的分析,也就可以推出下图
volatile关键字其实就是在汇编层面加了一个lock指令,然后lock指令最后是采用缓存锁还是总线锁来解决缓存一致性问题是由CPU自己决定的,我们不用管。
在输出结果中按ctrl+f进行搜索
通俗一点:Volatile在这里保证了可见性:修改一个volatile修饰变量之后,会立即将修改同步到主内存,使用一个volatile修饰的变量之前或者期间,会立即从主内存中刷新数据。保证读取的数据都是最新的,之前的修改都是可见的。
指令重排序
除了缓存一致性问题还有最后一个就是指令重排序的问题。即CPU,JVM层面优化指令的执行顺序。
案例
a = 3;
b = 2;
a = a + 1;
//不重排序的话
load a
set to 3
store 3
load b
set to 2
store b
load a
set to 4
store a
//经过重排序处理后
load a
set to 3
set to 4
store a
load b
set to 2
store b
- 可以看到指令重排序后少了两个指令,优化了性能!
- 但这种优化在多线程环境下有时候也会产生问题比如单例双重检查不写Volatile时产生的DCL问题(即创建对象的重排序)见我这篇博客
Volatile除了保证可见性,还可以保证有序性(禁止指令重排序优化):有volatile修饰的变量,赋值后多了一个“内存屏障“( 指令重排序时不能把后面的指令重排序到内存屏障之前的位置)
Happens-Before规则(哪些地方不会出现可见性问题)
即:前一个操作的结果可以被后续的操作获取。
- 程序的顺序性规则(as-if-serial):在一个线程内一段代码的执行结果是有序的。虽然还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
- volatile规则: 就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
- 传递性规则:happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。
- 管程锁定规则:无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
- 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
- 线程终止规则: 在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
- 线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
- 对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
内存屏障
- CPU在性能优化道路上导致的重排序问题,靠CPU自己是不能解决的,原因是CPU只是一个工具,它只接收指令并且执行指令,并不清楚当前执行的整个逻辑中是否存在不能优化的问题
- 所以解决重排序的问题只能靠我们开发人员,我们觉得哪里会出现重排序问题就去加上Volatile关键字,而具体底层其实是靠三种指令来解决的。
这三种指令分别是SFENCE、LFENCE、MFENCE指令
- sfence:也就是save fence,写屏障指令。在sfence指令前的写操作必须在sfence指令后的写操作
前完成。 - lfence:也就是load fence,读屏障指令。在lfence指令前的读操作必须在lfence指令后的读操作
前完成。 - mfence:也就是modify/mix,混合屏障指令,在mfence前得读写操作必须在mfence指令后的读
写操作前完成。
Volatile是靠mfence解决的(也就是lock指令除了加缓存锁或者总线锁,还会加内存屏障的指令)
CPU优化之路的总结
我们就是在不断压榨CPU的利用率,但每次压榨的方式都会造成一些新的问题,我们又需要去解决新的问题,就这样提出了很多东西,下图完美展示