并发三-共享模型之内存
11. 共享模型之内存
11.1 Java内存模型
- JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。
- JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
11.1.1 可见性
退不出的循环
-
package com.sunyang.concurrentstudy; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; /** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-05 20:34 **/ @Slf4j(topic = "c.Demo") public class KeJianDemo { static boolean run = true; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (run){ } log.debug("t 停止"); }).start(); TimeUnit.SECONDS.sleep(1); log.debug("停止"); run = false; } } // 输出 停止
-
初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
-
因为t线程要频繁的从内存(物理硬件内存)中读取run的值,JIT即时编译器就会将run的值缓存至自己工作内存(高速缓存)中的告诉缓存中,减少对主存中run的访问,提高效率
-
1秒之后,主线程修改了run的值,并同步到主存,但是因为t线程一直在读自己工作内存中的run值,结果永远是旧值,因为他不知道主存中的run值已经被改变了。
解决办法1(Volatile-易变关键字)
-
package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-05 20:34 **/@Slf4j(topic = "c.Demo")public class KeJianDemo { volatile static boolean run = true; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (run){ } log.debug("t 停止"); }).start(); TimeUnit.SECONDS.sleep(1); log.debug("停止"); run = false; }}// 输出 20:50:54 [main] c.Demo - 停止// 20:50:54 [Thread-0] c.Demo - t 停止
-
它可以用来修饰成员变量和静态成员变量
-
若CPU1将变量通过缓存回写到主存中,需要先锁住缓存行,此时状态切换为(M),向总线发消息告诉其他在嗅探的CPU该变量已经被CPU1改变并回写到主存中。接收到消息的其他CPU会将共享变量状态从(S)改成无效状态(I),缓存行失效。若其他CPU需要再次操作共享变量则需要重新从内存读取。
-
详细见博客:https://blog.csdn.net/qq_35015148/article/details/110210926
解决办法1(synchronized)
-
package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-05 20:34 **/@Slf4j(topic = "c.Demo")public class KeJianDemo { static boolean run = true; static final Object obj = new Object(); public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (run) { synchronized (obj) { if (!run) { break; } } } log.debug("t 停止"); }).start(); TimeUnit.SECONDS.sleep(1); log.debug("停止"); synchronized (obj) { run = false; } }}// 22:01:41 [main] c.Demo - 停止// 22:01:41 [Thread-0] c.Demo - t 停止
比较
volatile更轻量,可见性推荐volatile,但是不保证原子性
11.1.2 可见性VS原子性
-
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可 见, 不能保证原子性。
-
getstatic run // 线程 t 获取 run truegetstatic run // 线程 t 获取 run truegetstatic run // 线程 t 获取 run truegetstatic run // 线程 t 获取 run trueputstatic run // 线程 main 修改 run 为 false, 仅此一次getstatic run // 线程 t 获取 run false
-
比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错
-
// 假设i的初始值为0getstatic i // 线程2-获取静态变量i的值 线程内i=0getstatic i // 线程1-获取静态变量i的值 线程内i=0iconst_1 // 线程1-准备常量1iadd // 线程1-自增 线程内i=1putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1iconst_1 // 线程2-准备常量1isub // 线程2-自减 线程内i=-1putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
-
注意 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低
-
System.out.println() 底层加了synchronized 关键字
12. 指令集并行原理
12.1 名词解释
- Clock Cycle Time
- 主频的概念大家接触的比较多,而 CPU 的 Clock Cycle Time(时钟周期时间),等于主频的倒数,意思是 CPU 能 够识别的最小时间单位,比如说 4G 主频的 CPU 的 Clock Cycle Time 就是 0.25 ns,作为对比,我们墙上挂钟的 Cycle Time 是 1s 例如,运行一条加法指令一般需要一个时钟周期时间
- CPI
- 有的指令需要更多的时钟周期时间,所以引出了 CPI (Cycles Per Instruction)指令平均时钟周期数
- IPC
- IPC(Instruction Per Clock Cycle) 即 CPI 的倒数,表示每个时钟周期能够运行的指令数
- CPU 执行时间
- 程序的 CPU 执行时间,即我们前面提到的 user + system 时间,可以用下面的公式来表示
程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time
12.2 鱼罐头的故事
加工一条鱼需要 50 分钟,只能一条鱼、一条鱼顺序加工
可以将每个鱼罐头的加工流程细分为 5 个步骤:
- 去鳞清洗 10分钟
- 蒸煮沥水 10分钟
- 加注汤料 10分钟
- 杀菌出锅 10分钟
- 真空封罐 10分钟
即使只有一个工人,最理想的情况是:他能够在 10 分钟内同时做好这 5 件事,因为对第一条鱼的真空装罐,不会 影响对第二条鱼的杀菌出锅…
12.3 指令重排序优化
-
事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令 还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据 写回 这 5 个阶段
-
术语参考:
- instruction fetch (IF)
- instruction decode (ID)
- execute (EX)
- memory access (MEM)
- register write back (WB)
-
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在80年代中叶到九十年代占据了计算机架构的重要地位
-
分阶段,分工是提升效率的关键!
-
指令重排的前提是,重排指令不能影响结果。
12.4 支持流水线的处理器
- 现代CPU支持多级指令流水线,例如支持同时执行 : 取指令-指令译码-执行指令-内存访问-数据写回的处理器,
- 就可以称之为五级指令流水线,这时CPU可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC =1 ,本质上,流水线技术并不能缩短单条指令的执行时间,但是可以缩短多条组合指令的执行时间,他变相的提高了指令的吞吐率。
- 奔腾四(Pentium 4)支持高达 35 级流水线,但由于功耗太高被废弃
12.5 SuperScalar 处理器
-
大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单 元等,这样可以把多条指令也可以做到并行获取、译码等,CPU 可以在一个时钟周期内,执行多于一条指令,IPC > 1
13. Volatile
13.1 如何保证可见性及有序性
-
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存中去。
-
num =2;ready = true; // ready 是volatile赋值带写屏障// 写屏障
-
而读屏障(lfence)保证在该屏障之后的,对共享变量的读取,加载的是主存中最新的数据
-
// 读屏障// ready 是volatile读取值带读屏障 if (ready) { r1 = num; }
-
详细自行博客。
-
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后,
-
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前、
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VzVXGi2b-1631354230951)(C:/Users/Administrator/Desktop/整理/并发8-5(1)].assets/image-20210807101348961.png)
14. DCL问题
14.1 自己的一些问题理解
-
synchronized只是保证临界区也就是synchronized代码块中的代码和外面的代码不会发生指令重排序,但是内部在不影响结果的前提下一样会指令重排序,而volatile可以保证被他修饰的变量,在进行操作时,不会被指令重排序。
-
synchronized的有序性是建立在原子性的基础上,与volatile实现的有序性不同,因为他能保证原子性,所以即使synchronized中的代码被指令重排序了,也不会有问题,因为别的线程在没获得到锁的时候是读取不到这个变量的,是获取不到这个变量的中间状态的,所以他间接的实现了有序性,而并是volatile实现有序性的方式,二者有区别,synchronized的有序性,并不是指的指令重排序的有序性,而是保证同一时刻只有一个线程可以对其进行操作。禁止synchronized外面的指令和我代码块里面的指令进行重排序。是指多线程执行的有序性,
-
这里出现的问题是因为,instance变量脱离了synchronized的管理,类似于逃逸分析、。
14.2 代码示例
-
package com.sunyang.concurrentstudy;import java.util.concurrent.TimeUnit;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-07 10:23 **/public final class Singleton { private Singleton() {} private static volatile Singleton INSTANCE = null; public static Singleton getInstance() throws InterruptedException { if (INSTANCE == null) { synchronized (Singleton.class) { if (INSTANCE == null) { INSTANCE = new Singleton(); } } } return INSTANCE; }}
-
很关键的一点:第一个if使用了INSTANCE变量,是在同步块之外
14.3 字节码分析
-
public final class com.sunyang.concurrentstudy.Singleton minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPERConstant pool: #1 = Methodref #5.#24 // java/lang/Object."<init>":()V #2 = Fieldref #3.#25 // com/sunyang/concurrentstudy/Singleton.INSTANCE:Lcom/sunyang/concurrentstudy/Singleton; #3 = Class #26 // com/sunyang/concurrentstudy/Singleton #4 = Methodref #3.#24 // com/sunyang/concurrentstudy/Singleton."<init>":()V #5 = Class #27 // java/lang/Object #6 = Utf8 INSTANCE #7 = Utf8 Lcom/sunyang/concurrentstudy/Singleton; #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 LocalVariableTable #13 = Utf8 this #14 = Utf8 getInstance #15 = Utf8 ()Lcom/sunyang/concurrentstudy/Singleton; #16 = Utf8 StackMapTable #17 = Class #27 // java/lang/Object #18 = Class #28 // java/lang/Throwable #19 = Utf8 Exceptions #20 = Class #29 // java/lang/InterruptedException #21 = Utf8 <clinit> #22 = Utf8 SourceFile #23 = Utf8 Singleton.java #24 = NameAndType #8:#9 // "<init>":()V #25 = NameAndType #6:#7 // INSTANCE:Lcom/sunyang/concurrentstudy/Singleton; #26 = Utf8 com/sunyang/concurrentstudy/Singleton #27 = Utf8 java/lang/Object #28 = Utf8 java/lang/Throwable #29 = Utf8 java/lang/InterruptedException{ public static com.sunyang.concurrentstudy.Singleton getInstance() throws java.lang.InterruptedException; descriptor: ()Lcom/sunyang/concurrentstudy/Singleton; flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=0 0: getstatic #2 // Field INSTANCE:Lcom/sunyang/concurrentstudy/Singleton; 3: ifnonnull 37 6: ldc #3 // class com/sunyang/concurrentstudy/Singleton 8: dup 9: astore_0 10: monitorenter 11: getstatic #2 // Field INSTANCE:Lcom/sunyang/concurrentstudy/Singleton; 14: ifnonnull 27 17: new #3 // class com/sunyang/concurrentstudy/Singleton 20: dup 21: invokespecial #4 // Method "<init>":()V 24: putstatic #2 // Field INSTANCE:Lcom/sunyang/concurrentstudy/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:Lcom/sunyang/concurrentstudy/Singleton; 40: areturn Exception table: from to target type 11 29 32 any 32 35 32 any LineNumberTable: line 15: 0 line 16: 6 line 17: 11 line 18: 17 line 20: 27 line 22: 37 StackMapTable: number_of_entries = 3 frame_type = 252 /* append */ offset_delta = 27 locals = [ class java/lang/Object ] frame_type = 68 /* same_locals_1_stack_item */ stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 Exceptions: throws java.lang.InterruptedException static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: aconst_null 1: putstatic #2 // Field INSTANCE:Lcom/sunyang/concurrentstudy/Singleton; 4: return LineNumberTable: line 13: 0}SourceFile: "Singleton.java"
-
17表示申请一块内存空间,将对象的引用地址加载到从操作数栈,
-
20 表示赋值一份对象的地址引用,一个在构造方法,一个用在赋值给静态变量
-
21表示利用一个对象引用地址,调用构造方法,
-
24表示利用一个对象引用地址,赋值给static instance。
这时就会出现问题,因为有时JVM会对指令进行优化,也就是指令重排序,先执行24,在执行21
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gqToyw06-1631354230952)(C:/Users/Administrator/Desktop/整理/并发8-5(1)].assets/image-20210807132626170.png)
-
关键在于0:getstatic这行指令在monitior控制之外,他就像一个不守规矩的人,可以越过monitor读取INTSTANCE变量的值,然后又因为同步代码块中发生了指令重排序,让其他线程读取到了未初始化完成的instance变量值的中间状态,两者结合导致了线程不安全的问题。
-
这时t1还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么t2拿到的将是一个未初始化完成的实例。
14.4 解决方案
-
加volatile关键字,上述的代码已经添加,
-
字节码上看不见加的volatile关键字的效果。
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k2OPnfom-1631354230952)(C:/Users/Administrator/Desktop/整理/并发8-5(1)].assets/image-20210807133536908.png)
-
可见性
- 写屏障保证在该屏障之前对t1共享变量的改动,都同步到内存当中。
- 而读屏障保证在该屏障之后t2对共享变量的读取,加载的是主存中的最新数据。
-
有序性
- 写屏障会保证指令重排序时,不会将写屏障之前的代码,重排序到写屏障之后,
- 读屏障会保证指令重排序时,不会讲读屏障之前的代码,重排序到读屏障之前。
-
更底层是读写变量时使用lock指令来管理多核CPU之间的可见性和有序性。
-
单核CPU就没必要了,因为单核cpu共用一个缓存,不用保证缓存一致性问题,因为单核cpu之会发生上下文切换,上下文切换,他们读取到的缓存为同一缓存中的数据,所以单核CPU不会存在可见性问题。个人理解 有待验证,因不太懂得硬件底层原理,所以暂不确定是否正确。
15. happens-before
时间上的顺序发生能推出逻辑上一样的先后发生就是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();
-
-
变量都是指成员变量或静态成员变量
16. 线程安全单例
-
单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用 getInstance)时的线程安全,并思考注释中的问题
-
饿汉式:类加载就会导致该单实例对象被创建
-
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
-
-
实现一:
-
// 问题1:为什么加 final // 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例 public final class Singleton implements Serializable { // 问题3:为什么设置为私有? 是否能防止反射创建新的实例? private Singleton() {} // 防止其他类创建他的对象。不能防止反射创建新的实例。 // 问题4:这样初始化是否能保证单例对象创建时的线程安全? private static final Singleton INSTANCE = new Singleton(); // 可以,因为静态成员类变量的初始化操作是在类加载器中完成的,类加载阶段是由JVM保证线程安全问题。 // 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由 public static Singleton getInstance() { return INSTANCE; } public Object readResolve() { // 因为实现了序列化接口,当被反序列化时,会重新创建一个对象,和这个不同,如果我们重写了这个方法,那么他在调用readResolve时就会调用我们自己重写的方法。返回这个实例。防止反序列化破坏单例。 return INSTANCE; } }
-
-
实现二
-
// 问题1:枚举单例是如何限制实例个数的 // 本质静态成员变量 // 问题2:枚举单例在创建时是否有并发问题 // 问题3:枚举单例能否被反射破坏单例 // 不能 因为在反射时会判断是否是枚举类,如果是会抛异常。 // 问题4:枚举单例能否被反序列化破坏单例 // 因为ENUM父类中的反序列化是通过valueOf实现的,不是通过反射 // 问题5:枚举单例属于懒汉式还是饿汉式 // 饿汉式 // 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做 enum Singleton { INSTANCE; }
-
-
实现三 DCL
-
public final class Singleton { private Singleton() { } // 问题1:解释为什么要加 volatile ? private static volatile Singleton INSTANCE = null; // 问题2:对比实现3, 说出这样做的意义 public static Singleton getInstance() { if (INSTANCE != null) { return INSTANCE; } synchronized (Singleton.class) { // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗 if (INSTANCE != null) { // t2 return INSTANCE; } INSTANCE = new Singleton(); return INSTANCE; } } }
-
-
实现四:
-
静态内部类在使用的时候完成类加载,不会因为外部类的加载而加载,这是他和静态代码块和静态变量的区别。加载内部类时,也不会加载外部类。
-
public final class Singleton { private Singleton() { } // 问题1:属于懒汉式还是饿汉式 private static class LazyHolder { static final Singleton INSTANCE = new Singleton(); } // 问题2:在创建时是否有并发问题 public static Singleton getInstance() { return LazyHolder.INSTANCE; } }
-
载,这是他和静态代码块和静态变量的区别。加载内部类时,也不会加载外部类。
-
public final class Singleton { private Singleton() { } // 问题1:属于懒汉式还是饿汉式 private static class LazyHolder { static final Singleton INSTANCE = new Singleton(); } // 问题2:在创建时是否有并发问题 public static Singleton getInstance() { return LazyHolder.INSTANCE; } }