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

2021/12/6 7:18:55

本文主要是介绍线程安全性之有序性和内存屏障,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

有序性问题

通过上篇文章我们得知程序在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层面不知道什么时候允许优化,什么时候不允许优化

  • 读屏障(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 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规则就是为了描述可见性规则

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

可见性导致的问题

  • CPU高速缓存
  • 指令重排序

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

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

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

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



这篇关于线程安全性之有序性和内存屏障的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程