正文
内存可见性:
在并发的程序中,我们时不时会用到共享的变量,用这些变量控制我们整体业务逻辑的行为。
运行下面的程序:
public class ShareThread { private static boolean isRun = true; public static class MyThread extends Thread { @Override public void run() { while(isRun) { // do other things } } } public static class StopThread extends Thread { @Override public void run() { try { // 自己休眠2s 执行权限让给其他人 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // 结束 MyThread 的运行 isRun=false; // do other things } } public static void main(String[] args) throws Exception{ // 创建线程 MyThread myThread = new MyThread(); // 创建结束线程 StopThread stopThread = new StopThread(); // 启动线程 myThread.start(); stopThread.start(); // 主线程 等待线程执行完 myThread.join(); stopThread.join(); } }
执行上面的线程,得到的结果是:2s后,线程还没有结束执行。
StopThread这个线程中明明isRun=false;应当结束啊,为什么呢?
刚才问题的思考?
就在上面,定义了2个线程:
1、一个线程执行业务逻辑;
2、另一个线程负责控制第一个业务的执行。
但是,感觉没有任何的疑问啊,为啥没有停止呢?
有可能大家都是到 我们加一个关键字就好啦:volatile,这样就实现最终的效果。
计算机实现可见性
我们编写代码都是使用的高级语言,就像我们上面写的程序,它实质的执行过程是:编译-> 加载-> 验证->准备->解析->初始化->使用->卸载。这一套整体的过程。
真正执行的是机器码在计算机中执行。
实现内存的可见性,首先就要了解计算机的运行,如何实现缓存一致性的。
缓存一致性协议
首先了解一下计算机的体系结构:
计算机在执行计算的所使用的数据不是直接从内存中取出来的,而是先校验高速缓存中是否存在这样的数据,如果有,直接返回;如果没有,那么就会读取内存中的数据。
CPU为什么需要高速缓存?
首先,肯定是提高工作效率,其次是缓解CPU的执行速度与内存读取时间不匹配的问题,是一个时间的问题。
局部性原理:
时间局部性:如果一个信息项正在被访问,那么在近期它很可能还会被再次访问,因为程序中存在着循环、递归、方法的反复调用等。
空间局部性:如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。因为程序中顺序执行的代码、连续创建的两个对象、数组等。
这一介绍一个多核CPU的缓存一致性协议MESI:
由于我们的计算机体系结构是:
CPU<->高速缓存<->控制总线、数据总线<->内存<->硬盘
在高速缓存中,每一个cacheline都会有一个状态的标志,占用2bit,MESI就是这些状态的英文单词首字母的缩写。
M(Modified):该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。
E(Exclusive):该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。
S(Shared):该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。
I(Invalid): 该Cache line无效。
在多核CPU中,每一个CPU都会拥有自己的高速缓存以及相应的其他的硬件。
根据缓存一致性的协议:
当某一个CPU读取后,会将这cachekine标志变为E,
当其他的CPU读取相同的数据的时候,这个时候会将这个状态变成:S状态。
某一个CPU修改的时候,修改的这个cache变成M,其他引用的cache数据变成I状态。
同步数据:其他的CPU读取当前CUP修改的数据,会通知当前的CPU将数据写会在主存,当前的状态变成E,之后,其他的CPU会读取当前的数据,读取完成之后,会将所有的状态变成S状态。
经过这样的步骤,实现了不同的CUP的数据的可见性。
缓存一致性的问题:想必大家看到上面的过程有可能会感觉步骤这么多。对的,这个一致性是非常消耗时间的。
Java代码中实现内存可见性
在开头,我们的程序加上:volatile关键字就可以保证程序的正确执行,这是因为volatile关键字可以实现内存的可见性以及禁止指令的重排序。
实现内存的可见性关键技术:内存屏障以及先发生原则(Happen-Before) 。
内存屏障:
内存屏障分为:读屏障、写屏障。
先发生原则:
程序顺序原则:一个线程内保证语义的串行性
volatile规则:volatile 变量的写,先发生于读,这保证了volatile变量的可见性
锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
传递性:A先于B,B先于C,那么A必然先于C
线程的start()方法先于它的每一个动作
线程的所有操作先于线程的终结(Thread.join())
线程的中断(interrupt())先于被中断线程的代码
对象的构造函数执行,结束先于finalize() 方法
这样的约束,实现了volatile关键字可以实现内存的可见性以及禁止指令的重排序,保证了程序的正常运行。