美文网首页Java 杂谈java 知识点
最详细分析Java 内存模型

最详细分析Java 内存模型

作者: Tim在路上 | 来源:发表于2019-07-30 10:41 被阅读18次

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

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

    内存模型的抽象

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

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

    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,非阻塞数据结构和原子变量类

    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 内存模型

        本文链接:https://www.haomeiwen.com/subject/lqsgrctx.html