深入理解Volatile
public class ThreadDemo1 {
public static boolean stop = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
System.out.println("stop 修改为 true");
}).start();
while (true) {
if (stop) {
System.out.println(" stop 变为 true");
break;
}
}
}
}
当我们执行这段代码的时候,我们的预期是1秒之后会执行 "stop 变为 true",但是我们的输出结果一直是"stop 修改为true", 既然修改为了true, 那么不就会执行while的代码吗?
接着使用volatile修饰了静态变量stop,达到了预期的效果,这就是volatile的作用了。
不信你看
volatile可以使在多处理器环境下保证了共享变量的可见性,什么是可见性,通俗来讲:在一个单线程的环境下,如果向一个变量stop先写入一个值,然后在没有写干涉的情况下读取这个变量stop的值,那么这个时候读取到的这个变量应该是你之前写入的那个值,这是很正常的,但是在多线程下,读和写发生在不同线程的时候,就有可能会出现:读线程不能及时的读取到其他线程写入的最新的值。这就是可见性。这个时候我们必须使用某种机制来实现跨线程写入的内存的可见性,而volatile就是这种机制。
当我们的代码被编译会汇编指令后,我们可以发现在带有volatile修饰的成员变量时,会多一个lock指令,lock指令时一种控制指令,在多处理器的环境下,lock指令可以基于总线锁或者是缓存锁的机制来达到一个可见性的效果。
我们要从计算机的核心组件CPU、内存、I/O讲起,这三者的处理速度差异非常大,CPU处理速度最快,内存次之,最后是IO设备。但是在绝大部分的程序中,一定会存在内存访问或则和IO设备访问,比如磁盘的访问。
为了提升计算性能,CPU从单核升级到多核,但是仅仅提升CPU的性能是不够的,因为如果后面两者的性能没跟上,计算的速度还是取决于最慢的设备。所以为了提升性能,从硬件上做了一些优化:
假设我们现在是有两个CPU:CPU0跟CPU1,当CPU要去读取主内存的数据的时候,通过总线去读,所以为了提升速度,硬件在CPU与主内存中添加了CPU高速内存这种东西。
可以打开任务管理器查看
当我们的CPU0去读取主内存的数据i = 0的时候 ,会将数据缓存到CPU的缓存中,同样 CPU1去读取数据的时候也会缓存一份到缓存中,这样就很好的解决了处理器与内存的速度矛盾。
但是这个时候又出现了问题:当CPU0更改了i的值之后,会同步将i的值到主内存中,但是这个时候CPU1中也缓存了i的值 是0,CPU1还不知道主内存中的i的值已经被CPU0修改了,这个时候就会出现了缓存一致性的问题了。
为了解决上面的一致性问题。CPU就做出了两个解决方法,加锁
因为CPU在与内存拿数据的时候,一定是通过总线去拿的,所以就在总线加了锁,但是锁定了总线之后,其他的处理是无法通过总线去拿数据,又影响到了性能,这个时候就需要通过锁的控制力度来优化,所以又采用了缓存锁。而缓存锁是居于缓存一致性协议来实现的。
为了达到数据访问的一致,各个处理器在访问缓存时就需要遵循一些协议,最常见的就是MESI协议
MESI表示缓存行的四种状态
对于MESI协议,从CPU读写角度来说会遵循以下原则:
CPU读请求:缓存处于M、E、S状态都可以被读取,I状态CPU只能从主内存中读取数据
CPU写请求:缓存处于M、E状态菜可以被写。对于S状态的写,需要将其他的CPU中缓存置为无效才可以写。所以使用了缓存锁机制之后,CPU对于内存的操作基本达到了缓存一致性的效果。
由于CPU高速缓存的出现,使得多个CPU同时缓存了相同的共享数据时,可能存在可见性问题。也就是CPU0修改了自己缓存的值对于CPU1是不可见的,不可见导致了CPU1在后续对该数据进行写入的操作时,使用的是脏数据。使最终的结果错误。
另外还有一个问题就是线程的执行的顺序问题,因为多线程是无法控制哪个线程的某句代码会在另一个先册灰姑娘的某句代码后面执行,所以我们也就只能基于它的原理去了解一个这样存在的事实。
那么是不是有了缓存锁机制就能够达到缓存一致性的要求,那为什么还要加volatile关键字呢?
MESI协议虽然实现了缓存的一致性,但是同时又存在了一些问题:
各个CPU缓存的状态是通过消息传递来进行的。假设CPU0跟CPU1都缓存了 i = 0, 这个时候CPU0要对缓存中的共享变量i进行写入,首先就要发送一个失效的消息给CPU1,告诉CPU1它要开车了,然后还要等到CPU1收到消息之后再确认回执给回CPU0(有点像HTTP的三次握手)。CPU0这个时候都会处于阻塞状态。为了避免阻塞带来的资源浪费。在cpu中又引入了要给store bufferes。
CPU0只需要在写入共享数据时,直接把数据写入到store bufferes中,同时发送invalidate消息,然后继续去处理其他指令。当其他CPU发送了invalidate acknowledge消息时,再将store bufferes中的数据存储至cache line中。最后再从缓存同步到主内存中。
但是这里又会存在另外的问题。。。。
这个时候,,,反正怎么优化都不符合要求,硬件层面就把执行权给到软件了,所以CPU层面提供了内存屏障指令,在软件层面可以决定在适当的地方来使用内存屏障。
那内存屏障如何来加?其实就是volatile关键字,前面说到,volatile会在汇编指令中加入一个lock的指令,这个指令其实相当于实现了一种内存屏障。