线程安全性之有序性和内存屏障
作者:互联网
有序性问题
通过上篇文章我们得知程序在CPU中是以指令的形式执行的。
本篇文章有序性问题
也称cpu指令重排序
1.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;
了
2.怎么导致重排序的
//例如这段代码
int a = 0;
function(){
a = 1;
b = a+1;
assert(b == 2); //false
}
//指令重排序
b = a+1;
a = 1;
- 再看一张图讲解
多线程情况下步骤讲解:
- CPU0执行
a = 1
,发现并没有加载a,a在共享状态下(CPU1和CPU2下共享),需要把其他CPU的缓存读取过来并置为失效状态,最终完成后也就到了第二步a=0/E
,此时a在其他CPU处于失效状态,所以在CPU1下是独占状态。 - 由于是store buffer同步到cache是必须要等待到其他CPU都同步完成才会继续,可能存在的情况是先执行到
b=a+1
了,此时b没有被加载,所以b=0/E
是独占状态,接下来第4步,此时a还在异步等待,b就变成b=0+1 -> b=1/M
修改状态 - 最后第5步a终于执行完毕再设置
a=1
,但此时b=a+1
已经执行完毕,所以就导致了指令重排序问题
3.CPU性能优化博弈图
进行再次优化,引入了 invalidate queue
失效队列,但由于失效队列是异步处理的,还是会有此问题存在,此问题CPU层面已无法解决,于是提供内存屏障指令,由开发者根据需求使用
5.怎么解决指令重排序
加入内存屏障其实也就是#Lock指令,它既能实现缓存锁/总线锁也能实现内存屏障
内存屏障
1.什么是内存屏障
为什么需要开发者实现?因为CPU层面不知道什么时候允许优化,什么时候不允许优化
- 读屏障(lfence) load 读操作必须在写操作之前完成
- 写屏障(sfence)
- 全屏障(mfence)
在liunx上分别对应方法
- smp_rmb
- smp_wmb
- smp_mb
接下来看这个代码
int a = 0;
function(){
a = 1;
//读屏障 b=a+1 必须要在a=1之后执行
smp_rmb();
b = a+1;
assert(b == 2); //false
}
为此定义了一种抽象模型,即JMM模型
2.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及所有后续的装载指令 |
3.happends-before规则
- 程序顺序型规则 ,单线程执行结果一定不会发生变化
- 传递性规则,a happends before b,b happends before c,a happends before c
- volatile规则
- 监视器规则,锁的释放一直在执行结果之后
- start规则,线程启动之前的数值,在线程执行后一定是新的数值,不存在可见性问题
- join规则,线程执行结果一定在这个之前
happends-before规则就是为了描述可见性规则
线程安全性中的可见性和有序性总结
可见性导致的问题
- CPU高速缓存
- 指令重排序
使用synchronized volatile finanl关键字加锁保证可见性。
提供内存屏障指令,保证程序不会出现可见性,有序性问题
以上就是本章的全部内容了。
上一篇:线程安全性之可见性、缓存一致性(MESI)以及伪共享问题分析
下一篇:J.U.C ReentrantLock可重入锁使用以及源码分析
云想衣裳花想容,春风拂槛露华浓
标签:屏障,线程,指令,有序性,排序,CPU,内存 来源: https://blog.csdn.net/qq_35551875/article/details/121655685