1 什么是可见性?
通过 volatile 修饰的变量被 a 线程修改 b 线程能立即读取到修改后的值, 不会出现'脏读'
2 可见性原理
volatile 修饰后 hsdis 多了个 Lock 汇编指令, Lock 汇编指令是一种控制指令, 作用是在多线程环境中, 可以基于总线锁或缓存锁的机制来达到共享变量在线程间的可见性
3 硬件层面
CPU > 内存 > IO 硬件方面存在很大的处理速度的差异, 木桶原理最 --- 最短板决定整体性能
所以硬件方面的性能优化要从两方面着手:
1提高短板(基本不可实现)
2最大化利用[性能过剩组件(CPU)]
3.1 最大化利用 CPU 方法
image.PNG
CPU 增加高速缓存, CPU 绝大多数的业务处理中都会依赖内存或者 IO 进行运算或数据存储
CPU 告诉缓存通过降低内存 / IO 读取频率来实现提高整体处理性能
CPU 高速缓存分为: L1>L2>L3 三种, 性能依次下降
image.PNG
L1d:L1 数据缓存
L1i:L1 指令缓存
CPU 高速缓存提高了 CPU 处理过程中频繁与主内存交互的性能
CPU 高速缓存也带来了缓存 (数据) 一致性的问题
3.2 缓存 (数据) 一致性解决方案:
总线锁
通过在总线添加锁的方式来保证缓存 (数据) 一致性, 当 cup0 通过总线操作数据时, 其它 cpu1 将无法获取总线的使用权限, 对性能影响很大
缓存锁
相对于总线锁缓存锁的范围更加精确, 降低看控制粒度, 通过缓存一致性协议实现
缓存一致性协议 MESI
不同的 CPU 架构里缓存一致性协议有着各自不同的实现方式, X86 架构中是基于 MESI 协议
image.PNG
image.PNG
image.PNG
M>Modified 修改状态
E>Exclusive 独享状态
S>Shared 共享状态: 表示数据可被多个缓存对象进行缓存, 且数据值与主内存一致
I>Invlid 失效状态
失效状态缓存不可被使用, 将从主内存中进行读取
3.3 MESI 的局限性
当某个 CPU 修改缓存中的数据时, 首先通知其 cup 缓存中的相同数据, 其它相同缓存置为失效
其它 CPU 缓存失效完成后再通知要修改的 CPU, 该过程中 CUP 处于阻塞中, 浪费了 CPU 性能
image.PNG
3.4 EMSI 改进
为了减少缓存被修改过程中的阻塞时长, 通知修改时采用异步操作, 不进行阻塞
将修改请求缓存到 storebuffer 中
image.PNG
storebuffer 带来的问题
- value =3;
- void cup0{
- value = 10;// 通过 storebuffer 异步通知其他 CPU 缓存, 将缓存 value 变为 I: 失效状态
- isFinish = true; //E 独占状态
- }
- void cup1{
- // 由于 cup0 中 storebuffer 是异步操作
- // 所以理论上村 isFinish=true 而 value=3 这种情况
- if(isFinish){//true
- assert value == 10;//false
- }
- }
storebuffer 可能会导致 cup 的乱序执行既 "指令重排序", 重排序将带来可见性问题
硬件层面的优化, 总是会带来其他问题, 无法真正解决可见性问题, 所以 CPU 层面提供指令 -- 内存屏障供软件方面调用
3.5 内存屏障
- value =3;
- void cup0{
- value = 10;// 通过 storebuffer 异步通知其他 CPU 缓存, 将缓存 value 变为 I: 失效状态
加入内存屏障
- isFinish = true; //E 独占状态
- }
- void cup1{
- // 由于 cup0 中 storebuffer 是异步操作
- // 所以理论上村 isFinish=true 而 value=3 这种情况
- if(isFinish){//true
读取内存屏障 // 由于 cup0 在 'sFinish = true; //E 独占状态' 前加入内存屏障
- // 所以下面代码中 value 值将, 直接从主内存中进行获取
- assert value == 10;//false
- }
- }
cup 层面提供了 3 中内存屏障
读屏障 store barrier
写屏障 load barrier
全屏障 full barrier
X86 架构中 volatile 关键字的实现依赖: volatile--->Lock 指令 (缓存锁)---> 内存屏障
内存屏障 / 指令重排序 等和平台一级硬件有关, 不同硬件是不同的实现. java 是跨平台语言, 不需要在业务点中考虑硬件的差异性的是依托于 JMM 内存模型的存在
4 JMM 虚拟内存模型
image.PNG
语言基本的抽象内存模型, 本与 CPU 内存模型相类似
线程通过操作工作内存来修改数据, 工作内存负责和主内存进行通信和数据同步
JMM 虚拟内存模型为作为一种标准, 不同的硬件设备有着各自的实现(指令). 通过 JMM 业务代码开发人员不需要关系硬件差异化, 从而实现语言的跨平台
4.1 重排序
代码重排序顺序: 源代码 ->编译器重排序 ->CPU 层面重排序 (指令级, 内存)-> 最终执行的指令
通过重排序可以提高代码效率, 但不是所用情况都会进行重排序, 是否重排序取决于[数据依赖规则]
- /**
- * 无数据依赖
- * 1&2 行代码间无相互依赖
- * 可进行从排序
- */
- int a = 1;
- int b = 2;
- /**
- * 部分数据依赖
- * 1&2 行代码间无数据依赖
- * 1&3 行代码间存在数据依赖
- * 2&3 行代码间存在数据依赖
- * 1&2 行可进行重排序 1&3 2&3 行不可重排序
- */
- int a=1;
- int b = 2;
- int c = a+b;
数据依赖规则: as-if-serial
无论代码以何种方案进行重排序, 对于单个线程执行代码的结果不可变
Happens-Befor
代码 A 代码的执行结果对于 B 代码必须是可见, 就成为 A Happens-Befor B
那些场景会触发 Happens-Before 规则?
1 [程序的顺序规则]
- /**
- * 单线程调用该方法时, A Happens-Befor B
- **/
- function X(){
- a =1;// A
- b =2;// A
- }
2 [volatile 规则]
被 volatile 修饰的变量写操作一定对读操作可见, 即 "写" Happens-Befor "读"
3[传递性规则]
如果 :A Happens-Befor B & B Happens-Befor C
那么: A Happens-Befor C
4 [start 规则]
主线程里的 start()方法 Happens-Befo 该线程 run 方法内任意代码
- /**
- * B Happens-Befor C (start 规则)
- * A Happens-Befor B (顺序规则)
- * A Happens-Befor C (传递性规则)
- **/
- public class A{
- static x=0;
- public static void main(String []args){
- Thread t1=new Thread(()->{
- //C .....
- });
- x=10;//A
- t1.start();//B
- }
- }
5[Join 规则]
线程 run 方法内代码 Happens-Befor join()后的代码
- public class Demo {
- static int a = 0;
- /**
- * A Happens-Befor B
- **/
- public static void main(String[] args) throws Exception {
- Thread t1 = new Thread(()->{
- a = 99;//A
- });
- t1.start();
- t1.join();
- System.out.println(a);//B
- }
- }
6 [synchronized 监视器锁规则]
synchronized 的占用顺序决定线程代码顺序
- public class Demo {
- public void xx(){
- synchronized (this){
- //A...
- }
- }
- public static void main(String[] args) {
- /**
- *t1 线程 t1 代码 A Happens-Befor 线程 t2 代码 A
- *
- **/
- Demo demo = new Demo();
- Thread t1 = new Thread(()-> demo.xx());
- Thread t2 = new Thread(()-> demo.xx());
- t1.start();
- t1.join();// 保证 t1 先于 t2
- t2.start();
- }
- }
无数据依赖情况下禁止重排序
当代码间不存在数据依赖, 但在多线程调用的场景下可能会导致执行结果错误, 此时需要人工干预重排序 ---JMM 内存屏障
- value =3;
- void cup0{
- value = 10;// 通过 storebuffer 异步通知其他 CPU 缓存, 将缓存 value 变为 I: 失效状态
- isFinish = true; //E 独占状态
- }
- void cup1{
- if(isFinish){//true
- assert value == 10;//false
- }
- }
JMM 内存屏障: 编译器级别内存屏障, CPU 级别内存屏障
4.2 JMM 解决有序性, 可见性方案
volatile
可解决可见性. 通过内存屏障实现
synchronized
可解决可见性, 有序性, 原子性. 通过对线程阻塞实现单线程调用来实现
final
遍历不可变, 避免了可见性, 原子性等问题
happens-before
5 线程的顺序执行
使用 join
阻塞主线程, 直到调用 join()方法的线程执行完毕; 或者说调用 join()线程的执行结果对主线程可见, 底层通过 wait/notify 实现
- /**
- * 只有添加 join 后线程才会 123 依次执行
- **/
- Thread t1 = new Thread(()->{
- //doSomething1
- });
- Thread t2 = new Thread(()->{
- //doSomething2
- });
- Thread t3 = new Thread(()->{
- //doSomething3
- });
- t1.start();
- t1.join();
- t2.start();
- t2.join();
- t3.start();
来源: http://www.jianshu.com/p/4df71dfd2b4e