最详细分析Java 内存模型

并发编程中, 线程之间如何通信及线程之间如何同步, 通信是指线程之间以何种机制来交换
信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。

Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

内存模型的抽象

java中,所有的实例域,静态域和数组元素存储在堆内存中. 局部变量,方法定义参数和异常处理参数定义栈内存中,他们不会有内存可见性问题,不受内存模型影响.

共享内存,只的是共享变量存储在主内存中,但是每一个线程都有一个私有的本地内存.
本地内存中存储了该线程 读/写 共享变量的副本.

最详细分析Java 内存模型_第1张图片
javashare.png

A B 两个线程进行通信需要, A 更新本地缓存, A将共享变量刷新到主存中,
B 去主存中拉去 A 已经更新后的共享变量.

JMM (Java内存模型) 提供内存可见性保证.

源代码的重排序

执行程序时为提高性能,编译器会对指令做重排序

  1. 编译器优化重排序 2.指令级并行重排序 3. 内存系统重排序

2,3 属于处理器重排序,处理器重排序,JMM会要求Jav编译器在生成指令序列的时候,插入特定类型的内存屏障指令, 通过内存屏障指令来禁止特定类型的处理器重排序.

happens-before

从 JDK5 开始,java 使用新的 JSR -133 内存模型(本文除非特别说明,针对的都
是 JSR- 133 内存模型)。JSR-133 使用 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。

volatile

我们把对 volatile 变量的单个读/写,看成是使用同一个锁对这些单
个读/写操作做了同步

class VolatileFeaturesExample {
    
    volatile long vl = 0L;
    //使用 volatile 声明 64 位的 long 型变量
    public void set(long l) {
    vl = l;
    //单个 volatile 变量的写
    }
    public void getAndIncrement () {
    vl++;
    //复合(多个)volatile 变量的读/写
    }
    public long get() {
    return vl;
    //单个 volatile 变量的读
    }
}

假设有多个线程分别调用上面程序的三个方法,实际变为:

class VolatileFeaturesExample {
    long vl = 0L;
    public synchronized void set(long l) {
    // 64 位的 long 型普通变量
    //对单个的普通变量的写用同一个
    锁同步
    vl = l;
    }
    public void getAndIncrement () {
    long temp = get();
    //普通方法调用
    //调用已同步的读方法
    temp += 1L; //普通写操作
    set(temp); //调用已同步的写方法
    }
    public synchronized long get() {
    //对单个的普通变量的读用同一个
    锁同步
    return vl;
    }
}
volatile重排序
  • 当第二个操作是 volatile写时, 不管第一个操作是什么,都不能重排序.

  • 第一个操作是volatile读时,不管第二个操作是什么都不能重排序

  • 第一个操作是volatile写,第二个操作是volatile读时,不能重排序

在每个 volatile 写操作的前面和后面插入一个 StoreStore 屏障。

主要防止: 禁止上面的普通写和下面的volatile 写重排序,防止上面的
volatile 写与下面可能有的 volatile读/写重排序。

class Monitor Example {
    int a = 0;
    public synchronized void writer() {
    a++;
    }
    //1
    //2
    //3
    public synchronized void reader() {
    int i = a;
    //4
    //5
    ......
    }
    //6
}

锁释放时: JMM 会把该线程对应的本地内存中的共享变量刷新到主内存
中。

获取锁时: JMM 会把该线程对应的本地内存置为无效。从而使得被监视器
保护的临界区代码必须要从主内存中去读取共享变量。

class ReentrantLockExample {
    int a = 0;
    ReentrantLock lock = new ReentrantLock();
    public void writer() {
    lock.lock();
    //获取锁
    try {
    a++;
    } finally {
    lock.unlock(); //释放锁
    }
    }
    public void reader () {
    lock.lock();
    //获取锁
    try {
    int i = a;
    ......
    } finally {
    lock.unlock(); //释放锁
    }
    }
}

在 ReentrantLock 中,调用 lock()方法获取锁;调用 unlock()方法释放锁。

ReentrantLock 的实现依赖于 java 同步器框架 AbstractQueuedSynchronizer
(本文简称之为 AQS)。AQS 使用一个整型的 volatile 变量(命名为 state)来维
护同步状态,马上我们会看到,这个 volatile 变量是 ReentrantLock 内存语义实现
的关键。

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

加锁

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread. currentThread ();
    int c = getState();
    //获取锁的开始,首先读 volatile 变量 state
    if (c == 0) {
    if (isFirst(current) &&
    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;
}

释放锁

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);
    //释放锁的最后,写volatile变量state
    return free;
}

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

核心实现

protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

compareAndSet() 方法调用简称为
CAS。JDK 文档对该方法的说明如下: 如果当前状态值等于预期值,则以原子方式将同步
状态设置为给定的更新值。

ReentrantLock 的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种方
式:

  1. 利用 volatile 变量的写-读所具有的内存语义。

  2. 利用 CAS 所附带的 volatile 读和 volatile 写的内存语义。

concurrent 包的实现

Java 线程之间的通信现在有了下面四种方式:

  1. A 线程写 volatile 变量,随后 B 线程读这个 volatile 变量。
  2. A 线程写 volatile 变量,随后 B 线程用 CAS 更新这个 volatile 变量。
  3. A 线程用 CAS 更新一个 volatile 变量,随后 B 线程用 CAS 更新这个 volatile变量。
  4. A 线程用 CAS 更新一个 volatile 变量,随后 B 线程读这个 volatile 变量。

把这些特性整合在一起,就形成了整个 concurrent 包得以实现的基石。

分析 concurrent 包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为 volatile;
  2. 然后,使用 CAS 的原子条件更新来实现线程之间的同步;
  3. 同时,配合以 volatile 的

AQS,非阻塞数据结构和原子变量类

最详细分析Java 内存模型_第2张图片
concur.png

finall 实现

对于 final 域,编译器和处理器要遵守两个重排序规则:

  1. 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一
    个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操
    作之间不能重排序。
public class FinalExample {
    int i; //普通变量
    final int j; //final 变量
    static FinalExample obj;
    public void FinalExample () {
    //构造函数
    i = 1; //写普通域
    j = 2; //写 final 域
    }
    public static void writer () {
    //写线程 A 执行
    obj = new FinalExample ();
    }
    public static void reader () {
    //读线程 B 执行FinalExample object = obj;
    //读对象引用
    int a = object.i; //读普通域
    int b = object.j; //读 final 域
    }
}

你可能感兴趣的:(最详细分析Java 内存模型)