系统相关
首页 > 系统相关> > 线程安全性之有序性和内存屏障

线程安全性之有序性和内存屏障

作者:互联网

有序性问题

通过上篇文章我们得知程序在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;

多线程情况下步骤讲解:

  1. CPU0执行a = 1,发现并没有加载a,a在共享状态下(CPU1和CPU2下共享),需要把其他CPU的缓存读取过来并置为失效状态,最终完成后也就到了第二步a=0/E,此时a在其他CPU处于失效状态,所以在CPU1下是独占状态。
  2. 由于是store buffer同步到cache是必须要等待到其他CPU都同步完成才会继续,可能存在的情况是先执行到b=a+1了,此时b没有被加载,所以b=0/E是独占状态,接下来第4步,此时a还在异步等待,b就变成b=0+1 -> b=1/M修改状态
  3. 最后第5步a终于执行完毕再设置a=1,但此时b=a+1已经执行完毕,所以就导致了指令重排序问题

3.CPU性能优化博弈图

CPU性能优化图
进行再次优化,引入了 invalidate queue 失效队列,但由于失效队列是异步处理的,还是会有此问题存在,此问题CPU层面已无法解决,于是提供内存屏障指令,由开发者根据需求使用

5.怎么解决指令重排序

加入内存屏障其实也就是#Lock指令,它既能实现缓存锁/总线锁也能实现内存屏障

内存屏障

1.什么是内存屏障

为什么需要开发者实现?因为CPU层面不知道什么时候允许优化,什么时候不允许优化

在liunx上分别对应方法

接下来看这个代码

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 BarriersLoad1;LoadLoad;Load2确保Load1数据装载优先于Load2及所有后续的装载指令
StoreStore BarriersStore1;LoadLoad;Store2确保Store1数据刷新到内存优先于Store2及所有后续的存储指令
LoadStore BarriersLoad1;LoadLoad;Store2确保Load1数据装载优先于Store2刷新到内存指令及所有后续的存储指令
StoreLoad BarriersStore1;LoadLoad;Load2确保Store1数据刷新到内存优先于Load2及所有后续的装载指令

3.happends-before规则

  1. 程序顺序型规则 ,单线程执行结果一定不会发生变化
  2. 传递性规则,a happends before b,b happends before c,a happends before c
  3. volatile规则
  4. 监视器规则,锁的释放一直在执行结果之后
  5. start规则,线程启动之前的数值,在线程执行后一定是新的数值,不存在可见性问题
  6. join规则,线程执行结果一定在这个之前
    happends-before规则就是为了描述可见性规则

线程安全性中的可见性和有序性总结

可见性导致的问题

使用synchronized volatile finanl关键字加锁保证可见性。
提供内存屏障指令,保证程序不会出现可见性,有序性问题

以上就是本章的全部内容了。

上一篇:线程安全性之可见性、缓存一致性(MESI)以及伪共享问题分析
下一篇:J.U.C ReentrantLock可重入锁使用以及源码分析

云想衣裳花想容,春风拂槛露华浓

标签:屏障,线程,指令,有序性,排序,CPU,内存
来源: https://blog.csdn.net/qq_35551875/article/details/121655685