共享模型之内存

Java内存模型-JMM

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。

JMM 体现在以下几个方面

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

可见性

  • 可见性

  • 一个线程对主存数据修改,对于另外一个线程不可见

  • 问题

    • 初始状态,t线程从主存读取run到工作内存
    • 因为t要频繁从主内存读取run
      • JIT编译器将run的值存储在工作内存中,减少对主存run的访问
        • CPU高速缓存
    • 主线程修改run的值,同步到主存
      • t一直从高速缓存中读
  • 解决

    • 加上volatile修饰:易变

      image-20220315130021025

      • 只能修饰成员、静态成员变量
        • 必须从主存获取值
    • 加上synchronize解决

      • 在Java内存模型中,synchronized规定,线程在加锁时, 先清空工作内存→在主内存中拷贝最新变量的副本到工作内存 →执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。
  • 可见性vs原子性

    • 不能防止指令交错,只能保证读到最新的值

    • synchronize保证原子性、可见性,重量级

    • volatile保证可见性,适合一个写一个读

      image-20220315130104860

      • print里面有synchronized修饰

设计模式

  • 终止模式之两阶段终止模式

    • 原来用打断标记

      • 正常执行被打断
        • 设置打断标记为真
          • 不会进入catch,继续运行
      • sleep被打断
        • 进入catch,会清除打断标记
          • 重新设置打断标记为真
    • 用volatile改进,boolean是否运行

      • vo 的使用 场景 就是 boolean 变量 可见性

      • 不希望sleep,直接interrupt

        image-20220315130122284

        • 不需要重设打断等
  • 同步之Balking(犹豫模式)

    • 保证某个线程同时只能start一次,监控线程

    • 标记:boolean变量,一执行改为true

    • 原子性、可见性问题

      • 用synchronize加锁,存在读写操作

        image-20220315130128605

    • 应用

      image-20220315130134816

      • 实现线程安全的单例

有序性

  • 有序性

    • 指令重排

    • 指令重排原理

      • 指令并行优化

        image-20220315130152912

        • 同时执行不同指令

        • 前提,不影响结果

    • 问题

      • num后执行
    • 验证

      • 压测验证

        image-20220315130215855

      • 千万次测试,存在指令重排序

    • 禁用

      • ready加上volatile,禁止重排序
        • 防止ready之前的代码重排序
          • 写屏障
      • volatile保证可见性、有序性

Volatile原理

  • volatile原理

    image-20220315130225821

    • 内存屏障

      • 变量写指令,后加入写屏障
      • 变量读指令,前加入读屏障
    • 保证可见性

      image-20220315130549217

      • 写操作时,volatile及以前的,都同步到主存
      • 读操作时,volatile及以后的,对共享变量读取,加载主存
    • 保证有序性

      • 写屏障确保指令重排时,写屏障之前代码,不会重排到写屏障后

      • 读屏障确保指令重排时,读屏障之后代码,不会重排到度屏障前

      • 不能解决指令交错问题

        • 只能保证可见性和有序性
          • 写,之前更改到主存,不能重排到后面
          • 读,之后读到最新,无法重排到前面
        • synchronize都能做到,可见、有序、原子
    • dcl

      • 简介

        image-20220315130257450

        • 懒汉式,单例用到才创建

          • balking
          • 只有第一次需要线程保护。后面都是读操作
        • 两次检查上锁

          • 只在第一次没创建时上锁(外层校验)
          • 首次访问同步
        • 指令重排等问题

          • 学了多线程现在代码都不敢写了
      • 问题分析

        • synchronized无法禁止指令重排序,但可以保证有序性
        • synchronized能保证原子性、可见性和有序性,但不能禁止指令重排
        • 指令重排,先给变量复制,还没来得及构造对象
        • 其他线程判断变量不为空,拿到未初始化完毕的单例
      • 问题纠正

        • think
          • synchronized能保证只有一个线程执行临界区,且保证这个线程在临界区的有序性
          • synchronize不能阻止重排序,volatile才能
          • synchronized的有序性是建立在原子性的基础上,与volatile实现的有序性原理不同
          • 因为synchronized修饰的代码块是单线程运行的,因此即使发生重排序也不会影响最终的结果,因此,synchronized是可以保证有序性的,但是保证有序性的方式和volatile不同。
          • synchronized不能防止指令重排序,但是重排序是不影响”单线程”的执行结果的,加上synchronized只有一个线程能进入执行指令,所以保证”多线程中的有序性”.
          • 当前dcl例子中,对INSTANCE的“读取和写入”并没有在synchronized同步块内,不能保证同时只有一个线程执行,而写入的指令中又出现了重排序(写入和构造方法调用),因此导致了这个问题。
        • 共享变量若完全给synchronize,即使有重排序,也不会有有序性问题
        • 本例共享变量没有完全被synchronize保护,代码块外面还有使用
        • 内部重排 + 共享变量外部引用 导致问题
      • 问题解决

        • 加上volatile

          • 阻止指令重排序

            image-20220315130316484

          • 调用构造方法等指令,不能重排到赋值指令之后

            • 保证先构造再赋值
          • 其他线程读取指令时带读屏障,读取最新结果

            • 其实就是保证了每个线程遇到被volatile修饰的变量时都会从主存中重新获取该值

            • 我觉得这里解决的是先调构造方法再赋值的指令顺序,而不是图上画的,因为这是2个线程的图,volatile是单线程指令排序,多线程可见性

              image-20220315130324230

          • 解决dcl单例

      • happens-before规则

        image-20220315130331914

        • 这块和指令重排那块没有书上讲的好,最好看书
        • 解锁可见
        • volatile可见
        • start前写
        • 线程结束
        • 打断之前对变量的写,可见
        • 变量默认值的写
        • volatile变量前,其他变量的写,都同步到主存(写屏障)
      • 习题

        • balking模式
          • volatile情况
            • 一个线程写其他读
            • dcl 锁外,防止指令重排
        • 线程安全单例1
          • final
            • 防止子类继承这个单例类 然后重写方法
          • 防止反序列化破坏单例
            • 对,当用反序列化创建对象时,会调用readResovle(),因此我们直接给它重写,让它返回我们创建的对象就行了。
            • 克隆、反射、反序列化都会破坏单例
          • 私有
            • 防止直接创建,不能防止反射
          • 初始化对象
            • 安全
          • 公共静态方法
            • 封装、改造、泛型等
        • 线程安全单例
          • 枚举单例
            • 1、定义时有几个就有几个,静态成员变量
            • 2、没有并发问题,类加载完成
            • 3、不能反射破坏
            • 4、可以防止
            • 5、饿汉式
            • 6、加到构造方法中
          • 懒汉式
            • 线程安全:类对象锁
            • 效率低
          • DCL单例
            • 只在创建时加锁,效率提高
            • 防止第一并发访问,其他线程进入锁,再次创建
            • 防止重排序
          • 静态内部类,懒汉式
            • 类加载是懒惰的
            • 用getinstance时才触发类加载操作,初始化
          • 类加载由JVM保证线程安全

小结

  • JMM
    • 可见性
      • JVM缓存优化引起
    • 有序性
      • JVM指令重排优化引起
    • happens-before规则
      • 写读是否可见
  • 原理
    • CPU指令并行,重排序问题
    • volatile
      • 读写屏障
  • 模式
    • 两阶段终止模式优化
      • volatile改进
    • 同步模式之balking
      • 只执行一次情况
        • 监控线程启动
        • 单例