通过上篇文章我们得知程序在CPU中是以指令的形式执行的。
本篇文章有序性问题
也称cpu指令重排序
在CPU缓存优化过程中引入了StoreBuffer,虽说优化了性能,但也出现了新的问题,先看一段代码
static int x = 0, y = 0; static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { int i = 0; for (; ; ) { i++; x = 0;y = 0;a = 0;b = 0; Thread t1 = new Thread(() -> { a = 1; x = b; }); Thread t2 = new Thread(() -> { b = 1; y = a; }); t1.start();t2.start(); t1.join();t2.join(); if (x == 0 && y == 0) { System.out.println("第" + i + "次:x=" + x + ",y=" + y); break; } } } //我电脑上执行 第161581次:x=0,y=0
仔细看上诉代码,正常来说只有三个结果:[10],[01],[11],但是为什么会出现[00]呢?
这就是典型的指令重排序了,等于执行时变成了 x = b;a = 1; y = a;b = 1;
了
//例如这段代码 int a = 0; function(){ a = 1; b = a+1; assert(b == 2); //false } //指令重排序 b = a+1; a = 1;
多线程情况下步骤讲解:
a = 1
,发现并没有加载a,a在共享状态下(CPU1和CPU2下共享),需要把其他CPU的缓存读取过来并置为失效状态,最终完成后也就到了第二步a=0/E
,此时a在其他CPU处于失效状态,所以在CPU1下是独占状态。b=a+1
了,此时b没有被加载,所以b=0/E
是独占状态,接下来第4步,此时a还在异步等待,b就变成b=0+1 -> b=1/M
修改状态a=1
,但此时b=a+1
已经执行完毕,所以就导致了指令重排序问题
进行再次优化,引入了 invalidate queue
失效队列,但由于失效队列是异步处理的,还是会有此问题存在,此问题CPU层面已无法解决,于是提供内存屏障指令,由开发者根据需求使用
加入内存屏障其实也就是#Lock指令,它既能实现缓存锁/总线锁也能实现内存屏障
为什么需要开发者实现?因为CPU层面不知道什么时候允许优化,什么时候不允许优化
在liunx上分别对应方法
接下来看这个代码
int a = 0; function(){ a = 1; //读屏障 b=a+1 必须要在a=1之后执行 smp_rmb(); b = a+1; assert(b == 2); //false }
为此定义了一种抽象模型,即JMM模型
JAVA线程去访问内存的一个规范,它是一种抽象模型,解决有序性可见性问题(关键字)
不同的CPU架构不同的汇编指令,这个就是对不同操作操作系统添加内存屏障的封装,提供以下方法,具体源码在hotspot中的orderAccess_操作系统
中实现
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Load1;LoadLoad;Load2 | 确保Load1数据装载优先于Load2及所有后续的装载指令 |
StoreStore Barriers | Store1;LoadLoad;Store2 | 确保Store1数据刷新到内存优先于Store2及所有后续的存储指令 |
LoadStore Barriers | Load1;LoadLoad;Store2 | 确保Load1数据装载优先于Store2刷新到内存指令及所有后续的存储指令 |
StoreLoad Barriers | Store1;LoadLoad;Load2 | 确保Store1数据刷新到内存优先于Load2及所有后续的装载指令 |
可见性导致的问题
使用synchronized volatile finanl关键字加锁保证可见性。
提供内存屏障指令,保证程序不会出现可见性,有序性问题
以上就是本章的全部内容了。
上一篇:线程安全性之可见性、缓存一致性(MESI)以及伪共享问题分析
下一篇:J.U.C ReentrantLock可重入锁使用以及源码分析
云想衣裳花想容,春风拂槛露华浓