内卷老员工之java内存模型的happens-before原则

432 阅读4分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

java内存模型的happens-before原则

前言

  • happens-before原则是指线程本地内存与主内存的同步关系,只有满足happens-before原则的情况下,线程内存发生变化时,才会将数据同步到主内存,使得其他线程对该数据变化可见。

指令重排

指令重排介绍

  • 只要执行的语义不改变,java vm和cpu就允许指令进行并行执行
a = b + c
x = y + z
  • 如上两条语句在语义上不会互相依赖,因此可以存在并行的情况。
a = x + y
d = a + z
  • 如上两条语句互相依赖,需要第一条执行完毕后才能执行第二条,因此这种情况下则不能进行指令重排。

多cpu的指令重排问题

  • 首先来看下面这样一段代码
    private volatile boolean a = false;
    private int b = 0;
    private int c = 0;

    // 线程处理操作a
    public void methodA(){
        this.b++;
        this.c++;
        this.a = true;
    }

    // 线程处理操作b
    public void methodB(){
        while(this.a){
            // 具体逻辑操作
        }
    }
  • 如图所示为两个线程执行的两段代码。可以看到methodA方法中的三行代码实际上并不是互相依赖的,那么可以产生指令重排现象,如下图所以。当出现这种重排序时,可想而知执行时可能会产生异常报错问题,在某些场景甚至会带来毁灭性灾难。
public void methodA(){
        this.a = true;
        this.b++;
        this.c++;
    }

happens-before八种原则

  • 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
  • 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
  • volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
  • happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
  • 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
  • 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
  • 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
  • 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。

voletile的指令重排

  • 首先看下面一段代码
private volatile boolean a = false;
private int b = 0;
private int c = 0;

// 线程处理操作a
public void methodA(){
    this.b++;
    this.c++;
    this.a = true;
}

// 线程处理操作b
public void methodB(){
    while(this.a){
        // 具体逻辑操作
    }
}
  • 使用了volatile关键字保证了变量在操作时会同步到主内存中。当a的变量被同步时,b、c变量同时会被同步到主内存中。倘若指令发生了重排,a变量修改为true时,b、c变量值还未发生变化,则此时被其他线程读取到,会产生异常问题。因此volatile关键字对所修饰的变量前后同样进行了禁指令重排的限制。
public void methodA(){
        this.a = true;
        this.b++;
        this.c++;
    }

synchronised关键字的指令重排

  • synchronised关键字会保证同步块被同步到主内存中,同样的先来看如下一段代码:
private int a = 0;
private int b = 0;
private int c = 0;

// 线程处理操作a读取变量值
public void methodA(){
    synchronised(this){
        int a1 = this.a;
    }
    int b1 = this.b;
    int c1 = this.c;
}

// 线程处理操作b写入变量值
public void methodB(){
    this.b = b1;
    this.c = c1;
    synchronised(this){
        this.a = a1;
    }
}
  • 使用synchronised关键字读取会将主内存中所有数据读入到本线程中,而写入时会将所有本线程数据写入到主内存中。因此如果发生了指令重排,可能出现在同步代码块写入之后进行其他变量写入或读取之前进行其他变量读取,会导致部分变量无法从主内存读取到当前线程副本中引发异常数据的问题。因此synchronised同样保证不会进行指令重排。
    int c1 = this.c;
    synchronised(this){
        int a1 = this.a;
    }
    int b1 = this.b;
    this.c = c1;
    synchronised(this){
        this.a = a1;
    }
    this.b = b1;

尾记

  • 千古兴亡多少事?悠悠。不尽长江滚滚流。