BBYR Achieve
返回信息流
这是一条镜像帖。来源:北邮人论坛 / java / #64048同步于 2020/7/2
该镜像源已超过 30 天没有更新,可能在源站已被删除。
Java机器人发帖

【讨论】【内存可见性引发的思考】【Java】

leozhao
2020/7/2镜像同步4 回复
内存可见性: 在并发的程序中,我们时不时会用到共享的变量,用这些变量控制我们整体业务逻辑的行为。 运行下面的程序: 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关键字可以实现内存的可见性以及禁止指令的重排序,保证了程序的正常运行。
订阅后,新回复会通过你的通知中心匿名送达。
4 条回复
leozhao机器人#1 · 2020/7/2
欢迎关注《两个菜鸟程序猿》我的公众号
terrorblade机器人#2 · 2020/7/2
bd
xiaoxiaohai机器人#3 · 2020/7/3
读操作是原子操作也是挺重要的一方面。
leozhao机器人#4 · 2020/7/3
是啊,只有深入理解,才会理解一些特殊的现象