JMM(有序性,原子性,可见性)
JMM可以看作为JVM的运行时区域
JMM的主内存(堆和方法区)
1. 存储java实例对象
2.存储常量,成员变量,静态变量等
3.是线程的共享区域,多线程操作时会有线程安全问题
JMM的工作内存(方法栈和程序计数器)
1. 存储当前方法的所有本地变量信息,存储主内存变变量副本的拷贝
2. 字节码行号指示器,native方法信息
3. 是线程私有区域
JMM解决可见性问题
为了提高性能处理器和编译器会处理重排序,但并不是随意的进行重排序,满足条件如下
- 在单线程环境下程序运行结果不会发生改变
- 存在数据依赖关系的不可以重排序
以上可总结为无法通过happens-before原则 推导出来的,才能进行指令的重排序
什么是数据依赖关系?
比如有如下连个两个操作
c=1+2
z=c+2
此时这两个操作不能交换顺序,由于第一个操作运算的结果是第二个操作需要的参数,必须把第一个运算结果计算出来才可以执行第二个操作。此时这两个操作就是数据依赖关系。,
当A操作的结果需要对操作可见,则A和B就有happens-before关系
happens-before的概念:如果两个操作不满足任意一个happens-before原则。就说明这两个操作没有顺序保障,可以i女性重排序
happens-before八大原则
volatile:JVM提供的轻量级同步机制
volatile保证可线程对volatile修饰的共享变量对所有线程是可见的(就是线程A修改了一个变量,其他线程也会知道这个变量被修改了),并且禁止指令重排序优化le
volatile会有线程安全性问题
由于value++没有原子性,此时会有线程安全性问题。
J.U.C的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选
volatile变量是怎么实现对线程的立即可见?
当需要写volatile变量时,JMM会把该线程对应的工作内存的共享变量值刷到内存中
当需要读取volatile变量时,JMM会把该线程对应的工作内存为无效
volatile如何禁止重排优化
1. 通过插入内存屏障指令禁止在内存屏障前后的指令禁止在内存屏障前后的指令执行重排序游优化
2. 强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本
内存屏障:保证特定操作的顺序
保证某些变量的内存可见性
单例的双重检测实现,一下代码在多线程情况下依然会有问题
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
instance = new Singleton();可以分为三步
1. memory = allocate(); //分配对向内存空间
2. instance(memory);//初始化对象
3.instance = memory//instance指向分配的内存地址
此时这三个指令的顺序可以重排序为
1. memory = allocate(); //分配对向内存空间
3.instance = memory//instance指向分配的内存地址
2. instance(memory);//初始化对象
此时的instance仍然为null,若此时有另一个线程执行第一个if语句时,仍然为null.此时会出现重复创建单例的情况
可以用volatile来禁止指令重排序问题
volatile和synchronized的区别
- volatile 本质时让当前变量需要从主存中获取,在工作内存的是不可靠的
- volatile修饰变量,synchronized可以修饰变量,类,方法
- volatile只保证可见性,不保证原子性。synchronized两者都可保证
- volatile不会造成线程阻塞,synchronzied会造成阻塞
- volatile不允许重排序,synchronizaed允许
CAS
CAS(一种高效的实现线程安全性的方法):
支持原子更新操作,适用于计数器,序列发生器等
属于乐观锁机制
操作失败时有开发者确定是否继续
思想:包含三个出操作数:预期原址(B),新值(A)和内存位置(V),如果内存位置的值与预期原值相相等(若不相等则证明已经被修改,不符合原子性),那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作
当一个线程需要修该一个共享变量时,就把变量赋值为A,经过对A操作后,就会生成新值B,需要修改内存中V的值时,就需要调用CAS
缺点:1. 若循环时间长,则开销较大,
2. 只能保证一个共享变量的原子操作
3. ABA问题:如果初值和最后的值相等,CAS会认为发生改变,这样是错的(可用AtomicStampedReference解决)