共享模型之内存 逃离我推掉我的手 2022-12-25 01:00 121阅读 0赞 ### 共享模型之内存 ### * 一、Java 内存模型 * 二、可见性 * * 例子引入 * 解决方法 * 可见性 vs 原子性 * 三、有序性 * * 诡异的结果 * 解决方法 * 原理之 volatile * * 如何保证可见性 * 如何保证有序性 * double-checked locking 问题 * double-checked locking 解决 * happens-before Monitor主要关注的是访问共享变量时,保证临界区代码的原子性 现在我们进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题 # 一、Java 内存模型 # JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。 JMM 体现在以下几个方面 * 原子性 - 保证指令不会受到线程上下文切换的影响 * 可见性 - 保证指令不会受 cpu 缓存的影响 * 有序性 - 保证指令不会受 cpu 指令并行优化的影响 # 二、可见性 # ## 例子引入 ## 先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止 static boolean run = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(()->{ while(run){ // .... } }); t.start(); sleep(1); run = false; // 线程t不会如预想的停下来 } 为什么呢?分析一下: 1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存 ![2.][] 2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYyNDQxMA_size_16_color_FFFFFF_t_70] 3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYyNDQxMA_size_16_color_FFFFFF_t_70 1] ## 解决方法 ## volatile(易变关键字) 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存 ## 可见性 vs 原子性 ## 前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况,而 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低 # 三、有序性 # JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码 static int i; static int j; // 在某个线程内执行如下赋值操作 i = ...; j = ...; 可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是先执行 i ,也可以是先执行 j 。 这种特性称之为『指令重排』,但是多线程下『指令重排』会影响正确性。 ## 诡异的结果 ## int num = 0; boolean ready = false; // 线程1 执行此方法 public void actor1(I_Result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } // 线程2 执行此方法 public void actor2(I_Result r) { num = 2; ready = true; } I\_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种? * 情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1 * 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1 * 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了) 其实还有一种情况是可能是 0! * 情况4:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2 这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化 ## 解决方法 ## volatile 修饰的变量,可以禁用指令重排 ## 原理之 volatile ## volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence) * 对 volatile 变量的写指令后会加入写屏障 * 对 volatile 变量的读指令前会加入读屏障 ### 如何保证可见性 ### 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中 public void actor2(I_Result r) { num = 2; ready = true; // ready 是 volatile 赋值带写屏障 // 写屏障 } 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据 public void actor1(I_Result r) { // 读屏障 // ready 是 volatile 读取值带读屏障 if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } ### 如何保证有序性 ### 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 public void actor2(I_Result r) { num = 2; ready = true; // ready 是 volatile 赋值带写屏障 // 写屏障 } 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前 public void actor1(I_Result r) { // 读屏障 // ready 是 volatile 读取值带读屏障 if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } 原子性和有序性图示: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYyNDQxMA_size_16_color_FFFFFF_t_70 2] 注意: volatile 不能解决指令交错: * 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去 * 而有序性的保证也只是保证了本线程内相关代码不被重排序 ### double-checked locking 问题 ### 以著名的 double-checked locking 单例模式为例 public final class Singleton { private Singleton() { } private static Singleton INSTANCE = null; public static Singleton getInstance() { if(INSTANCE == null) { // t2 // 首次访问会同步,而之后的使用没有 synchronized synchronized(Singleton.class) { if (INSTANCE == null) { // t1 INSTANCE = new Singleton(); } } } return INSTANCE; } } 以上的实现特点是: * 懒惰实例化 * 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁 * 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外 但在多线程环境下,上面的代码是有问题的 我们从字节码的角度看一看 0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 3: ifnonnull 37 6: ldc #3 // class cn/itcast/n5/Singleton 8: dup 9: astore_0 10: monitorenter 11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 14: ifnonnull 27 17: new #3 // class cn/itcast/n5/Singleton 20: dup 21: invokespecial #4 // Method "<init>":()V 24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 27: aload_0 28: monitorexit 29: goto 37 32: astore_1 33: aload_0 34: monitorexit 35: aload_1 36: athrow 37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 40: areturn 其中 * 17 表示创建对象,将对象引用入栈 // new Singleton * 20 表示复制一份对象引用 // 引用地址 * 21 表示利用一个对象引用,调用构造方法 * 24 表示利用一个对象引用,赋值给 static INSTANCE 也许 jvm 会优化为:先执行 24,再执行 21。 如果两个线程 t1,t2 按如下时间序列执行: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYyNDQxMA_size_16_color_FFFFFF_t_70 3] 这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的将是一个**未初始化完毕的单例** ### double-checked locking 解决 ### 可以对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,把 INSTANCE 声明改成 `private static volatile Singleton INSTANCE = null;` 现在读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点: * 可见性 * 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中 * 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据 * 有序性 * 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 * 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYyNDQxMA_size_16_color_FFFFFF_t_70 4] ## happens-before ## happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见 * 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见 static int x; static Object m = new Object(); new Thread(()->{ synchronized(m) { x = 10; } },"t1").start(); new Thread(()->{ synchronized(m) { System.out.println(x); } },"t2").start(); * 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见 volatile static int x; new Thread(()->{ x = 10; },"t1").start(); new Thread(()->{ System.out.println(x); },"t2").start(); * 线程 start 前对变量的写,对该线程开始后对该变量的读可见 static int x; x = 10; new Thread(()->{ System.out.println(x); },"t2").start(); * 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束) static int x; Thread t1 = new Thread(()->{ x = 10; },"t1"); t1.start(); t1.join(); System.out.println(x); * 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted) static int x; public static void main(String[] args) { Thread t2 = new Thread(()->{ while(true) { if(Thread.currentThread().isInterrupted()) { System.out.println(x); break; } } },"t2"); t2.start(); new Thread(()->{ sleep(1); x = 10; t2.interrupt(); },"t1").start(); while(!t2.isInterrupted()) { Thread.yield(); } System.out.println(x); } * 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见 * 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排 volatile static int x; static int y; new Thread(()->{ y = 10; x = 20; },"t1").start(); new Thread(()->{ // x=20 对 t2 可见, 同时 y=10 也对 t2 可见 System.out.println(x); },"t2").start(); [2.]: /images/20221120/18e38cf7b2a444838ed083a14236f3db.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYyNDQxMA_size_16_color_FFFFFF_t_70]: /images/20221120/4faf3e4f21fe44d9af25f5552c1b7b7b.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYyNDQxMA_size_16_color_FFFFFF_t_70 1]: /images/20221120/4e17998f5aa14400bf1f6bc4beec004e.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYyNDQxMA_size_16_color_FFFFFF_t_70 2]: /images/20221120/3b79d37afa6a482ea0ac12786cf8ad43.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYyNDQxMA_size_16_color_FFFFFF_t_70 3]: /images/20221120/b453fc6c049f4708b78b738c210295c4.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYyNDQxMA_size_16_color_FFFFFF_t_70 4]: https://img-blog.csdnimg.cn/20201130094534161.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYyNDQxMA==,size_16,color_FFFFFF,t_70
相关 并发编程——4.共享模型之内存 目录 4.共享模型之内存 4.1.Java 内存模型 4.2.可见性 4.2.1.退不出的循环 ゞ 浴缸里的玫瑰/ 2023年10月07日 22:28/ 0 赞/ 58 阅读
相关 计算机基础之内存 目录 1.什么是内存 2.内存的物理结构 3.内存的使用 4.数据在内存中为什么用二进制表示 5.二进制的补码 1.什么是内存 内存 快来打我*/ 2023年01月13日 04:22/ 0 赞/ 131 阅读
相关 共享模型之内存 共享模型之内存 一、Java 内存模型 二、可见性 例子引入 解决方法 可见性 vs 原子性 三、有序性 逃离我推掉我的手/ 2022年12月25日 01:00/ 0 赞/ 122 阅读
相关 共享内存 1.共享内存 a.操作步骤 (1)创建共享内存 (2)映射共享内存 (3)分离共享内存 (4)控制、删除共享内存 b.相关函数 1.shmget 作用:在 男娘i/ 2022年07月15日 13:23/ 0 赞/ 213 阅读
相关 共享内存 共享内存 是被多个进程共享的一部分物理内存,共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以理科看到其 小灰灰/ 2022年07月15日 01:38/ 0 赞/ 280 阅读
相关 共享内存 共享内存 是被多个进程共享的一部分物理内存,共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以理科看到其 淡淡的烟草味﹌/ 2022年07月15日 01:38/ 0 赞/ 258 阅读
相关 共享内存 之前我们已经了解了管道和消息队列:[http://blog.csdn.net/qq\_34021920/article/details/79596262][http_blog. 蔚落/ 2022年06月14日 03:52/ 0 赞/ 273 阅读
相关 linux 共享内存 共享内存允许两个或多个进程进程共享同一块内存(这块内存会映射到各个进程自己独立的地址空间)从而使得这些进程可以相互通信。在GNU/Linux中所有的进程都有 唯一的虚 电玩女神/ 2022年03月20日 08:56/ 0 赞/ 332 阅读
相关 共享内存 共享内存头文件为shm.h 共享内存可以视为进程间通信速度最快的方式 共享内存实现函数非常简单, shmctl shmget shmat shmdt 这四个函数是共... 我就是我/ 2021年05月17日 14:09/ 0 赞/ 526 阅读
还没有评论,来说两句吧...