Java内存模型
JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU 寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面
- 原子性-保证指令不会受到线程上下文切换的影响
- 可见性-保证指令不会受cpu 缓存的影响
- 有序性-保证指令不会受 cpu 指令并行优化的影响
主存:所有线程都共享的数据,包括静态成员变量,成员变量
工作内存:每个线程私有的,比如对应的局部变量
可见性
先看一个小例子:
为什么会出现以上问题呢?我们分析一下:
1.初始状态,run值为true,t线程将run值从主存读取到了工作内存。
2.因为t线程需要频繁地去主存中读取run值,所以JIT编译器会把run值缓存到自己的工作内存中的高速缓存中,减少主存中run的访问,提高效率
3.1秒之后,main线程将run修改成false,并且将其同步到主存中,但是t中仍然是从自己工作内存中的高速缓存里去读取的run值,结果还是旧值
解决方案
在共享变量前添加volatile关键字,能够保证线程不会去缓存中获取run值,每次都去主存中寻找这个值,虽然性能有所损失,但是保证了共享变量在多个线程中的可见性
注意:这里除了使用volatile,使用synchronized也可以保证共享变量的可见性
可见性VS原子性
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见,不能保证原子性,适用于一个写线程,多个读线程的情况:
如下图所示,两个线程一个i++ 一个--,只能保证看到最新值,这时使用volatile就不能解决指令交错得问题,原子性没有得到保障
注意: synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是
svnchronized是属于重量级操作,性能相对更低
两阶段终止模式
经过volatile改良过得两阶段终止模式如下
犹豫模式
有序性
指令重排
事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的CPU指令。为什么这么做呢?可以想到指令可以再划分为一个个更小的阶段,例如每条指令可以划分为:取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这五个阶段
现代的CPU支持多级指令流水线,比如支持同时执行取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这五个阶段的处理器,就可以称之为五级处理器,这时CPU可以再一个时钟周期内同时执行指令的五个不同阶段(相当于一条执行时间最长的复杂指令),IPC=1,本质上,流水线的技术不能缩短单挑指令的执行时间,但是变相的提高了指令的吞吐率。
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在80's 中叶到 90's 中叶占据了计算架构的重要地位。
多线程中指令重排的问题
禁止重排序
在ready上加上volatile注解就可以禁止使用重排序,可以保证给ready赋值语句之前的代码禁止使用重排序
Volatile原理
保证可见性
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动都会保存到主存中,代码如下
写屏障会自动生成在被volatile修饰的变量赋值后的位置,注意这里的普通变量num也会被顺带同步到主存
- 读屏障(Ifence)保证在该屏障之后的,对共享变量的读取,加载的都是主存中的最新数据,代码如下
读屏障会自动生成在被volatile修饰的变量读取前的位置,注意这包括num读取的也是主存中的数据
保证有序性
- 写屏障除了保证可见性之外,还会保证写屏障前的代码不会重排到写屏障之后,代码如下图所示
- 读屏障也会保证读屏障之后的代码不会重排到读屏障之前,代码如下图所示
double-checked locking问题
以上代码使用双重检查的方法来优化单例懒加载的性能
首次使用getInstance方法的时候才使用synchronized加锁,后续使用的时候无需进行加锁
但是上面示例其实是有缺陷的,第一个If使用了INSTANCE变量,是在同步代码块之外,所以在多线程的情况下,getInstance的字节码为:
这里因为0:getstatic这行代码在monitor控制之外,所以它可以不守规矩,无需获取锁就可以去判断instance是否为空,然后将对象返回
这时如果t1还没有完全构造方法执行完毕,那么t2拿到的将会是一个未初始化完毕的单例
注意:synchronized里代码块的指令仍然是可以被重排序的,volatile才能阻止重排序,但是如果共享变量是完全被synchronized保护的,那么这个共享变量在使用的过程中是不会有原子性和可见性以及有序性问题的。
为什么在双重检查的单例模式中,既然已经加了synchronized为什么还需要用volatile去修饰变量呢?
因为synchronized能禁止指令重排,而synchronized能保证一个有序性是因为它本质是让多个线程在调用synchronized修饰的方法时,有并行转换成串行调用,所以这里有一个前提要求就是共享变量是完全被synchronized保护的。
而在双重检查的单例模式中,为了减少synchronized的范围,所以新增了一层检查,所以这重检查外是不被synchronized保护的,所以有可能会发生重排序造成对象先被赋值后再调用构造方法。
happens-before
happens-before规定了对共享变量的写操作对其他线程的读操作可见,它是可见性和有序性的一套规则总结,跑开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其他线程对该共享变量的读可见。