## java内存模型

2022-10-25 17:16:31 浏览数 (2)

java内存模型

happens-before

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之

内,也可以是在不同线程之间。

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作

可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。

重排序

as-if-serial语义

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

重排序对多线程的影响
代码语言:javascript复制
public class ReorderExample {
    int a = 0;
    volatile boolean flag = false;
    public void writer() {
        a = 2; // 1
    }
    public void reader() {
        if (flag) { // 3
            int i = a * a; // 4
            System.out.println(i);
        }
    }

}

假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入呢?

答案是:不一定能看到。

操作1和操作2做了重排序。程序执行时,线程A首先写标记变量flag,随后线程B读这个变量。由于条件判断为真,线程B将读取变量a。此时,变量a还没有被线程A写入,在这里多线程程序的语义被重排序破坏了!

操作3和操作4存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲(Reorder Buffer,ROB)的硬件缓存中。当操作3的条件判断为真时,就把该计算结果写入变量i中。

对操作3和4做了重排序。重排序在这里破坏了多线程程序的语义!

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果

volatile

volatile写-读建立的happens-before关系
代码语言:javascript复制
public class VolatileExample {
    int a = 0;
    volatile boolean flag = false;
    public void writer() {
        a = 1; // 1
        flag = true; // 2
    }
    public void reader() {
        if (flag) { // 3
            int i = a; // 4
        }
    }
}

假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-before规则,这个过程建立的happens-before关系可以分为3类: 1)根据程序次序规则,1 happens-before 2;3 happens-before 4。 2)根据volatile规则,2 happens-before 3。 3)根据happens-before的传递性规则,1 happens-before 4。

volatile写-读的内存语义

volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

volatile读的内存语义如下:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。

线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。

线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。
代码语言:javascript复制
public class VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;
    void readAndWrite() {
        int i = v1; // 第一个volatile读
        int j = v2; // 第二个volatile读
        a = i   j; // 普通写
        v1 = i   1; // 第一个volatile写
        v2 = j * 2; // 第二个 volatile写
    }
}

线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。

线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

ReentrantLock分为公平锁和非公平锁,我们首先分析公平锁

使用公平锁时,加锁方法lock()调用轨迹如下。

1)ReentrantLock:lock()。

2)FairSync:lock()。

3)AbstractQueuedSynchronizer:acquire(int arg)。

4)ReentrantLock:tryAcquire(int acquires)。

代码语言:javascript复制
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c   acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

从上面源代码中我们可以看出,加锁方法首先读volatile变量state。

在使用公平锁时,解锁方法unlock()调用轨迹如下。

1)ReentrantLock:unlock()。

2)AbstractQueuedSynchronizer:release(int arg)。

3)Sync:tryRelease(int releases)。

代码语言:javascript复制
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

从上面的源代码可以看出,在释放锁的最后写volatile变量state。

公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共

享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。

0 人点赞