并发三-共享模型之内存

5 篇文章 0 订阅
5 篇文章 0 订阅

并发三-共享模型之内存

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 的值到工作内存。

  • image-20210805204421737

  • 因为t线程要频繁的从内存(物理硬件内存)中读取run的值,JIT即时编译器就会将run的值缓存至自己工作内存(高速缓存)中的告诉缓存中,减少对主存中run的访问,提高效率

  • image-20210805204643349

  • 1秒之后,主线程修改了run的值,并同步到主存,但是因为t线程一直在读自己工作内存中的run值,结果永远是旧值,因为他不知道主存中的run值已经被改变了。

  • image-20210805204855343

解决办法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 分钟,只能一条鱼、一条鱼顺序加工

image-20210806143852750

可以将每个鱼罐头的加工流程细分为 5 个步骤:

  • 去鳞清洗 10分钟
  • 蒸煮沥水 10分钟
  • 加注汤料 10分钟
  • 杀菌出锅 10分钟
  • 真空封罐 10分钟

image-20210806143925678

即使只有一个工人,最理想的情况是:他能够在 10 分钟内同时做好这 5 件事,因为对第一条鱼的真空装罐,不会 影响对第二条鱼的杀菌出锅…

12.3 指令重排序优化

  • 事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令 还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据 写回 这 5 个阶段

  • image-20210806144243171

  • 术语参考:

    • 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 级流水线,但由于功耗太高被废弃
  • image-20210806145143354

12.5 SuperScalar 处理器

  • 大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单 元等,这样可以把多条指令也可以做到并行获取、译码等,CPU 可以在一个时钟周期内,执行多于一条指令,IPC > 1

  • image-20210806162829459

  • image-20210806162839547

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;
     }
    }
    
    

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值