多线程与高并发——共享模型之内存-JMM&Volatile&DCL
共享模型之内存
Java内存模型-JMM
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
可见性
可见性
一个线程对主存数据修改,对于另外一个线程不可见
问题
- 初始状态,t线程从主存读取run到工作内存
- 因为t要频繁从主内存读取run
- JIT编译器将run的值存储在工作内存中,减少对主存run的访问
- CPU高速缓存
- JIT编译器将run的值存储在工作内存中,减少对主存run的访问
- 主线程修改run的值,同步到主存
- t一直从高速缓存中读
解决
加上volatile修饰:易变
- 只能修饰成员、静态成员变量
- 必须从主存获取值
- 只能修饰成员、静态成员变量
加上synchronize解决
- 在Java内存模型中,synchronized规定,线程在加锁时, 先清空工作内存→在主内存中拷贝最新变量的副本到工作内存 →执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。
可见性vs原子性
不能防止指令交错,只能保证读到最新的值
synchronize保证原子性、可见性,重量级
volatile保证可见性,适合一个写一个读
- print里面有synchronized修饰
设计模式
终止模式之两阶段终止模式
原来用打断标记
- 正常执行被打断
- 设置打断标记为真
- 不会进入catch,继续运行
- 设置打断标记为真
- sleep被打断
- 进入catch,会清除打断标记
- 重新设置打断标记为真
- 进入catch,会清除打断标记
- 正常执行被打断
用volatile改进,boolean是否运行
vo 的使用 场景 就是 boolean 变量 可见性
不希望sleep,直接interrupt
- 不需要重设打断等
同步之Balking(犹豫模式)
保证某个线程同时只能start一次,监控线程
标记:boolean变量,一执行改为true
原子性、可见性问题
用synchronize加锁,存在读写操作
应用
- 实现线程安全的单例
有序性
有序性
指令重排
指令重排原理
指令并行优化
同时执行不同指令
前提,不影响结果
问题
- num后执行
验证
压测验证
千万次测试,存在指令重排序
禁用
- ready加上volatile,禁止重排序
- 防止ready之前的代码重排序
- 写屏障
- 防止ready之前的代码重排序
- volatile保证可见性、有序性
- ready加上volatile,禁止重排序
Volatile原理
volatile原理
内存屏障
- 变量写指令,后加入写屏障
- 变量读指令,前加入读屏障
保证可见性
- 写操作时,volatile及以前的,都同步到主存
- 读操作时,volatile及以后的,对共享变量读取,加载主存
保证有序性
写屏障确保指令重排时,写屏障之前代码,不会重排到写屏障后
读屏障确保指令重排时,读屏障之后代码,不会重排到度屏障前
不能解决指令交错问题
- 只能保证可见性和有序性
- 写,之前更改到主存,不能重排到后面
- 读,之后读到最新,无法重排到前面
- synchronize都能做到,可见、有序、原子
- 只能保证可见性和有序性
dcl
简介
懒汉式,单例用到才创建
- balking
- 只有第一次需要线程保护。后面都是读操作
两次检查上锁
- 只在第一次没创建时上锁(外层校验)
- 首次访问同步
指令重排等问题
- 学了多线程现在代码都不敢写了
问题分析
- synchronized无法禁止指令重排序,但可以保证有序性
- synchronized能保证原子性、可见性和有序性,但不能禁止指令重排
- 指令重排,先给变量复制,还没来得及构造对象
- 其他线程判断变量不为空,拿到未初始化完毕的单例
问题纠正
- think
- synchronized能保证只有一个线程执行临界区,且保证这个线程在临界区的有序性
- synchronize不能阻止重排序,volatile才能
- synchronized的有序性是建立在原子性的基础上,与volatile实现的有序性原理不同
- 因为synchronized修饰的代码块是单线程运行的,因此即使发生重排序也不会影响最终的结果,因此,synchronized是可以保证有序性的,但是保证有序性的方式和volatile不同。
- synchronized不能防止指令重排序,但是重排序是不影响”单线程”的执行结果的,加上synchronized只有一个线程能进入执行指令,所以保证”多线程中的有序性”.
- 当前dcl例子中,对INSTANCE的“读取和写入”并没有在synchronized同步块内,不能保证同时只有一个线程执行,而写入的指令中又出现了重排序(写入和构造方法调用),因此导致了这个问题。
- 共享变量若完全给synchronize,即使有重排序,也不会有有序性问题
- 本例共享变量没有完全被synchronize保护,代码块外面还有使用
- 内部重排 + 共享变量外部引用 导致问题
- think
问题解决
加上volatile
阻止指令重排序
调用构造方法等指令,不能重排到赋值指令之后
- 保证先构造再赋值
其他线程读取指令时带读屏障,读取最新结果
其实就是保证了每个线程遇到被volatile修饰的变量时都会从主存中重新获取该值
我觉得这里解决的是先调构造方法再赋值的指令顺序,而不是图上画的,因为这是2个线程的图,volatile是单线程指令排序,多线程可见性
解决dcl单例
happens-before规则
- 这块和指令重排那块没有书上讲的好,最好看书
- 解锁可见
- volatile可见
- start前写
- 线程结束
- 打断之前对变量的写,可见
- 变量默认值的写
- volatile变量前,其他变量的写,都同步到主存(写屏障)
习题
- balking模式
- volatile情况
- 一个线程写其他读
- dcl 锁外,防止指令重排
- volatile情况
- 线程安全单例1
- final
- 防止子类继承这个单例类 然后重写方法
- 防止反序列化破坏单例
- 对,当用反序列化创建对象时,会调用readResovle(),因此我们直接给它重写,让它返回我们创建的对象就行了。
- 克隆、反射、反序列化都会破坏单例
- 私有
- 防止直接创建,不能防止反射
- 初始化对象
- 安全
- 公共静态方法
- 封装、改造、泛型等
- final
- 线程安全单例
- 枚举单例
- 1、定义时有几个就有几个,静态成员变量
- 2、没有并发问题,类加载完成
- 3、不能反射破坏
- 4、可以防止
- 5、饿汉式
- 6、加到构造方法中
- 懒汉式
- 线程安全:类对象锁
- 效率低
- DCL单例
- 只在创建时加锁,效率提高
- 防止第一并发访问,其他线程进入锁,再次创建
- 防止重排序
- 静态内部类,懒汉式
- 类加载是懒惰的
- 用getinstance时才触发类加载操作,初始化
- 类加载由JVM保证线程安全
- 枚举单例
- balking模式
小结
- JMM
- 可见性
- JVM缓存优化引起
- 有序性
- JVM指令重排优化引起
- happens-before规则
- 写读是否可见
- 可见性
- 原理
- CPU指令并行,重排序问题
- volatile
- 读写屏障
- 模式
- 两阶段终止模式优化
- volatile改进
- 同步模式之balking
- 只执行一次情况
- 监控线程启动
- 单例
- 只执行一次情况
- 两阶段终止模式优化