• volatile 和锁的内存语义


    一、volatile 的内存语义

    1. volatile 的特性

    volatile变量自身具有以下特性:

    可见性 :对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

    原子性 :对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

    2. volatile 写-读建立的happens-before关系

    从JSR-133开始(即从JDK5开始),volatile变量的写-读可以实现线程之间的通信。

    从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果:volatile写和锁的释放有相同的内存语义;volatile读与锁的获取有相同的内存语义。

    3. volatile 写-读的内存语义

    volatile写的内存语义如下:

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

    volatile读的内存语义如下:

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

    下面对volatile写和volatile读的内存语义做个总结:

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

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

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

    4. volatile 内存语义的实现

    当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。

    当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。

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

    为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

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

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

    • 在每个volatile写操作的后面插入一个StoreLoad屏障。

    • 在每个volatile读操作的后面插入一个LoadLoad屏障。

    • 在每个volatile读操作的后面插入一个LoadStore屏障。

    前文提到过,X86处理器仅会对写-读操作做重排序。X86不会对读-读、读-写和写-写操作做重排序,因此在X86处理器中会省略掉这3种操作类型对应的内存屏障。在X86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。


    二、锁的内存语义

    1、锁的释放-获取建立的 happens-before 关系

    锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

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

    假设线程A执行writer()方法,随后线程B执行reader()方法。根据happens-before规则,这个过程包含的happens-before关系可以分为3类:

    • 1)根据程序次序规则,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6。
    • 2)根据监视器锁规则,3 happens-before 4。
    • 3)根据happens-before的传递性,2 happens-before 5。

    2、锁的释放和获取的内存语义

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

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

    对比锁释放-获取的内存语义与volatile写-读的内存语义可以看出:锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。

    下面对锁释放和锁获取的内存语义做个总结:

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

    3、锁内存语义的实现

    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 的实现依赖于Java同步器框架AbstractQueuedSynchronizer(本文简称之为AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态,这个volatile变量是ReentrantLock内存语义实现的关键。

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

    加锁调用轨迹如下:

    ReentrantLock#lock() -> FairSync#lock() -> 
    AbstractQueuedSynchronizer#acquire(int args) -> FairSync#tryAcquire()
    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()调用轨迹如下:

    ReentrantLock#unlock() -> AbstractQueuedSynchronizer#release(int args) -> Sync#tryRelease(int releases)
    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。

    对于非公平锁,释放和公平锁完全一样,加锁则使用CAS来更新state变量:

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

    sun.misc.Unsafe类的compareAndSwapInt()方法是一个本地方法调用,具体的C++实现就是:

    程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(Lock Cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

    4、concurrent 包的实现

    由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信现在有了下面4种方式:

    • 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包的源代码实现,会发现一个通用化的实现模式:

    首先,声明共享变量为volatile。

    然后,使用CAS的原子条件更新来实现线程之间的同步。

    同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

    AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。

    从整体来看,concurrent包的实现示意图如3-28所示:

    image

    本文来自对《Java并发编程的艺术》一书总结。

  • 相关阅读:
    html图片链接不显示图片
    Mybatis总结
    IllegalArgumentException: Could not resolve resource location pattern [classpath .xml]: class path resource cannot be resolved to URL because it does not exist
    java.lang.ClassNotFoundException: com.radiadesign.catalina.session.RedisSessionHandlerValve
    sqlserver2008客户端设置主键自增
    判断手机还是电脑访问
    SSM与jsp传递实体类
    ssm打印sql语句
    SqlSession 同步为注册,因为同步未激活
    for循环取出每个i的值
  • 原文地址:https://www.cnblogs.com/lucare/p/9312606.html
Copyright © 2020-2023  润新知