Java的volatile和MESI协议
使用过Java多线程的都知道,volatile
可以确保多线程永远都可以读取到最新的变量。但是却无法保证一个操作的原子性
关于这部分可以先从硬件部分说起:
硬件
单核CPU
在现代计算机中,CPU的速度是远远快于内存的,如果不加以任何处理,此时CPU就会一直在等待内存的IO,从而导致一些资源的浪费。所以就有了高速缓冲区(cache),但是在Cache里面肯定是不会将内存的所有数据都拷贝一份,因为cacahe只有几十kb的大小。
这就造成了一个现象,也就是说每一个核心都会有一个共享的内存区域和一个自己Cache区域。
多核CPU
随着现代技术的发展,单核CPU已经被淘汰了,取而代之的是多核CPU,而且现代的CPU已经都是多级缓存了,大部分是三级缓存。在多核心的CPU中,每一个核心都会有自己的Cache,如果某一个核心修改了自己缓存区的数据,那么就会造成自己的缓存区和内存中数据不一致 所以这时候就会有一个MESI
协议(缓存一致性)
MESI协议
MESI
协议简单来讲,就是为了确保每一个核心的缓存数据都是一致的。当有一个核心对某一个变量做了修改之后,就会通知到其他的核心,然后使其失效。
Java
volatile
在了解volatile
之前,首先需要了解Java的内存模型JMM
,在Java内存模型中,堆是所有线程共享的
图中即是JMM模型对硬件的简单映射,在Java里面,对象都是在堆中,而在线程的私有栈空间,每一个线程只能对变量进行修改或者赋值操作。
volatile
首先多线程的本质其实是多个线程轮流执行,在时间片轮转调度算法
里面,每一个线程都会获取到一个执行时间,执行时间结束,就会轮到其他的线程执行。
可见性
考虑一下场景,如果内存中有一个值x=1,线程一
和线程二
都会对x进行一个x+=1
的操作。那么无论哪一个线程率先执行完毕,都会将最新的结果集写入到内存中,这样其他线程在读取的时候都会读取到最新的数据。这样就保证了任意一个线程在读取这个数据的时候都会一个最新的值。
如果不加
volatile
限制,则可能每一个核心操作完成之后只是将值写入到自己Cache区,而不刷新内存。什么时候刷新完全是随机的
非原子性
但是如果出现一下情况:
- 线程一读取x=1
- 线程一正在执行+1的操作
- 线程一的执行时间结束,此时线程二开始执行
- 线程二在执行的时候读取内存数据也是x=1,
- 于是线程二将x+1的结果写入内存(由于volatile的限制,CPU在执行完毕的时候必须强制写入内存)
- 线程一将x+1的结果写入内存
所以volatile虽然具有可见性
,但是却无法保证一个操作的原子性,如下上测试代码:
新建一个增长类:
1 | public class AInstance { |
然后新建一个测试线程:
1 | public class AThread extends Thread { |
最后启动类:
1 | public class ATest { |
如果使用Volatile
修饰的变量可以保证原子性的话,那么a一定会是100000
,但是测试结果一直是随机数字
1 | 31600或者其他的 |
MESI协议和volatile
虽然MESI协议可以保证缓存一致性,但是如果有一个线程在正要进行+1
的时候被挂起了,而另一个线程则正好执行完了x+=1
的操作,此时回到第一个线程继续执行,这样就会导致一个错误的数据。如下:
volatile
所以虽然有MESI
保证缓存的一致性,但是在赋值操作之前已经读取了,所以此时并不会再次读取内存
这就是volatile
只能保证内存的可见性,但是无法保证原子性的问题
单例模式
正是由于volatile的这个特性,所以在多线程中的单例模式都会在获取实例的方法上加上一个synchronized
关键字,以确保只会生成一个对象。
总结
在多线程的场景下,使用volatile需要注意的是原子性操作的问题,否则就会造成程序的数据错误
Java的volatile和MESI协议
https://somersames.github.io/2019/01/05/Java的volatile和MESI协议/