并发编程之Java内存模型volatile的内存语义

1、volatile的特性

理解volatile特性的一个好办法是把对volatile变量的单个读/写,看成是使用同一个锁对单个读/写操作做了同步。

代码示例:

package com.lizba.p1;

/**
 * <p>
 *      volatile示例
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/9 21:34
 */
public class VolatileFeatureExample {

    /** 使用volatile声明64位的long型变量 */
    volatile long v1 = 0l;

    /**
     * 单个volatile写操作
     * @param l
     */
    public void set(long l) {
        v1 = l;
    }

    /**
     * 复合(多个)volatile读&写
     */
    public void getAndIncrement() {
        v1++;
    }

    /**
     * 单个volatile变量的读
     * @return
     */
    public long get() {
        return v1;
    }

}

假设有多个线程分别调用上面程序的3个方法,这个程序在语义上和下面程序等价。

package com.lizba.p1;

/**
 * <p>
 *      synchronized等价示例
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/9 21:46
 */
public class SynFeatureExample {

    /** 定义一个64位长度的普通变量 */
    long v1 = 0L;

    /**
     * 使用同步锁对v1变量进行写操作
     * @param l
     */
    public synchronized void set(long l) {
        v1 = l;
    }

    /**
     *  通过同步读和同步写方法对v1进行+1操作
     */
    public void getAndIncrement() {
        long temp = get();
        // v1加一
        temp += 1L;
        set(temp);
    }

    /**
     * 使用同步锁对v1进行读操作
     * @return
     */
    public synchronized long get() {
        return v1;
    }

}

如上两个程序所示,一个volatile变量的单个读\写操作,与一个普通变量的读\写操作都是使用同一个锁来同步,它们之间的执行效果相同。

上述代码总结:

锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总能看到(任意线程)对这个volatile变量最后的写入。
锁的语义决定了临界区代码的执行具有原子性。这意味着,即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具备原子性。

总结volatile特性:

  • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性。对任意volatile变量的读/写具有原子性,但类似volatile++这种复合操作不具有原子性。

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

  • 对于程序员来说,我们更加需要关注的是volatile对线程内存的可见性。

从JDK1.5(JSR-133)开始,volatile变量的写-读可以实现线程之间的通信。从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果。

  • volatile的写和锁的释放有相同的内存语义
  • volatile的读和锁的获取有相同的内存语义

代码示例:

package com.lizba.p1;

/**
 * <p>
 *
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/9 22:23
 */
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
            System.out.println(i);
        }
    }

}

假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-before规则,

这个过程建立的happens-before关系如下:

  • 根据程序次序规则,1 happens-before 23 happens-before 4
  • 根据volatile规则,2 happens-before 3
  • 根据happens-before的传递性规则,1 happens-before 4

图示上述happens-before关系:

总结:这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即对B线程可见。

3、volatile写-读的内存语义

volatile写的内存语义

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

以上面的VolatileExample为例,假设A线程首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。

A执行volatile写后,共享变量状态示意图。

线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中,此时A的本地内存和主内存中的值是一致的。

volatile读的内存语义

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

B执行volatile读后,共享变量的状态示意图:

在读flag变量后,本地内存B包含的值已经被置为无效。此时,线程B必须从主内存中重新读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值变为一致。

总结volatile的写和volatile读的内存语义

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

4、volatile内存语义实现

程序的重排序分为编译器重排序和处理器重排序(我的前面的博文内容有写哈)。为了实现volatile内存语义,JMM会分别禁止这两种类型的重排序。

volatile重排序规则表:

是否能重排序 第二个操作
第一个操作 普通读/写 volatile读 volatile写
普通读/写 NO
volatile读 NO NO NO
volatile写 NO NO

上图举例:第一行最后一个单元格意思是,在程序中第一个操作为普通读/写时,如果第二个操作为volatile写,则编译器不能重排序。

总结上图:

  • 第二个操作是volatile写时,都不能重排序。确保volatile写之前的操作不会被编译器重排序到volatile之后
  • 第一个操作为volatile读时,都不能重排序。确保volatile读之后的操作不会被编译器重排序到volatile之前
  • 第一个操作为volatile写,第二个操作为volatile读时,不能重排序。

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

JMM采取的是保守策略内存屏障插入策略,如下:

  • 在每个volatile写操作屏障前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

保守策略可以保证在任意处理器平台上,任意程序中都能得到正确的volatile内存语义。

保守策略下,volatile写插入内存屏障后生成的指令序列图:

解释:

StoreStore屏障可以保证在volatile写之前,其前面所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有普通写在volatile写之前刷新到主内存。

保守策略下,volatile读插入内存屏障后生成的指令序列图:

解释:

LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。

代码示例:

package com.lizba.p1;

/**
 * <p>
 *      volatile屏障示例
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/9 23:48
 */
public class VolatileBarrierExample {

    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        // 第一个volatile读
        int i = v1;
        // 第二个volatile读
        int j = v2;
        // 普通写
        a = i + j;
        // 第一个volatile写
        v1 = i + 1;
        // 第二个volatile写
        v2 = j * 2;
    }

    // ... 其他方法

}

针对VolatileBarrierExamplereadAndWrite(),编译器生成字节码时可以做如下优化:

注意:最后的StoreLoad屏障无法省略。因为第二个volatile写之后,程序return。此时编译器无法准确断定后面是否会有volatile读写操作,为了安全起见,编译器通常会在这里插入一个StoreLoad屏障。

上面的优化可以针对任意处理器平台,但是由于不同的处理器有不同的“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。

X86处理器平台优化

X86处理器仅会对写-读操作做重排序。X86不会对读-读、读-写和写-写重排序,因此X86处理器会省略掉这3种操作类型对应的内存屏障。在X86平台中,JMM仅需要在volatile写后插入一个StoreLoad屏障即可正确实现volatile写-读内存语义。同时这样意味着X86处理器中,volatile写的开销会远远大于读的开销。

5、volatile和锁的比较

功能上:

锁比volatile更强大

可伸缩性和执行性能上:

volatile更具有优势

到此这篇关于并发编程之Java内存模型volatile的内存语义的文章就介绍到这了,更多相关Java内存模型volatile的内存语义内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解Java volatile 内存屏障底层原理语义

    目录 一.volatile关键字介绍及底层原理 1.volatile的特性(内存语义) 2.volatile底层原理 二.volatile--可见性 三.volatile--无法保证原子性 四.volatile--禁止指令重排 1.指令重排 2.as-if-serial语义 五.volatile与内存屏障(Memory Barrier) 1.内存屏障(Memory Barrier) 2.volatile的内存语义实现 六.JMM对volatile的特殊规则定义 一.volatile关键字介绍及底

  • Java并发编程之Volatile变量详解分析

    目录 一.volatile变量的特性 1.1.保证可见性,不保证原子性 1.2.禁止指令重排 二.内存屏障 三.happens-before Volatile关键字是Java提供的一种轻量级的同步机制.Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量, 相比synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度. 但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其

  • java 多线程与并发之volatile详解分析

    目录 CPU.内存.缓存的关系 CPU缓存 什么是CPU缓存 为什么要有多级CPU Cache Java内存模型(Java Memory Model,JMM) JMM导致的并发安全问题 可见性 原子性 有序性 volatile volatile特性 volatile 的实现原理 总结 CPU.内存.缓存的关系 要理解JMM,要先从计算机底层开始,下面是一份大佬的研究报告 计算机在做一些我们平时的基本操作时,需要的响应时间是不一样的!如果我们计算一次a+b所需要的的时间: CPU读取内存获得a,1

  • Java关键字volatile知识点总结

    volatile是什么 volatile关键字是Java提供的一种轻量级同步机制.它能够保证可见性和有序性,但是不能保证原子性 可见性 对于volatile的可见性,先看看这段代码的执行 flag默认为true 创建一个线程A去判断flag是否为true,如果为true循环执行i++操作两秒后,创建另一个线程B将flag修改为false 线程A没有感知到flag已经被修改成false了,不能跳出循环 这相当于啥呢?相当于你的女神和你说,你好好努力,年薪百万了就嫁给你,你听了之后,努力赚钱.3年之

  • java高并发的volatile与Java内存模型详解

    public class Demo09 { public static boolean flag = true; public static class T1 extends Thread { public T1(String name) { super(name); } @Override public void run() { System.out.println("线程" + this.getName() + " in"); while (flag) { ;

  • Java中JMM与volatile关键字的学习

    目录 JMM volatile关键字 可见性与原子性测试 哪些地方用到过volatile? 单例模式的安全问题 你知道CAS吗? CAS底层原理 CAS缺点 ABA问题 总结 JMM JMM是指Java内存模型,不是Java内存布局,不是所谓的栈.堆.方法区. 每个Java线程都有自己的工作内存.操作数据,首先从主内存中读,得到一份拷贝,操作完毕后再写回到主内存. JMM可能带来可见性.原子性和有序性问题. 1.可见性:指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改.显然

  • 并发编程之Java内存模型volatile的内存语义

    1.volatile的特性 理解volatile特性的一个好办法是把对volatile变量的单个读/写,看成是使用同一个锁对单个读/写操作做了同步. 代码示例: package com.lizba.p1; /** * <p> * volatile示例 * </p> * * @Author: Liziba * @Date: 2021/6/9 21:34 */ public class VolatileFeatureExample { /** 使用volatile声明64位的long型

  • 并发编程之Java内存模型顺序一致性

    目录 1.数据竞争和顺序一致性 1.1 Java内存模型规范对数据竞争的定义 1.2 JMM对多线程程序的内存一致性做的保证 2.顺序一致性内存模型 2.1 特性 2.2 举例说明顺序一致性模型 2.3 同步程序的顺序一致性效果 2.4 未同步程序的执行特性 3. 64位long型和double型变量写原子性 3.1 CPU.内存和总线简述 3.2 long和double类型的操作 简介: 顺序一致性内存模型是一个理论参考模型,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照

  • 并发编程之Java内存模型锁的内存语义

    目录 1.锁的释放-获取建立的happens-before关系 2.锁释放和获取的内存语义 3.锁内存的语义实现 4.concurrent包的实现 简介: 锁的作用是让临界区互斥执行.本文阐述所得另一个重要知识点--锁的内存语义. 1.锁的释放-获取建立的happens-before关系 锁是Java并发编程中最重要的同步机制.锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息. 锁释放-获取的示例代码: package com.lizba.p1; /** * <p>

  • Java并发编程之Java内存模型

    目录 1.什么是Java的内存模型 2.为什么需要Java内存模型 3.Java内存模型及操作规范 4.Java内存模型规定的原子操作 5.Java内存模型同步协议 6.Java内存模型的HB法则 JMM的HB法则 总结 1.什么是Java的内存模型 Java内存模型简称JMM(Java Memory Model),JMM是和多线程并发相关的一组规范.各个jvm实现都要遵循这个JMM规范.才能保证Java代码在不同虚拟机顺利运行.因此,JMM 与处理器.缓存.并发.编译器有关.它解决了CPU 多

  • 并发编程之Java内存模型

    目录 一.Java内存模型的基础 1.1 并发编程模型的两个关键问题 1.2 Java内存模型的抽象结构 1.3 从源代码到指令重排序 1.4 写缓冲区和内存屏障 1.4.1 写缓冲区 1.4.2 内存屏障 1.5 happens-before 简介 简介: Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,这一系列几篇文章将揭开Java内存模型的神秘面纱. 这一系列的文章大致分4个部分,分别是: Java内存模型基础,主要介绍内存模型相关基本概念 Java内存模型

  • Java内存模型final的内存语义

    目录 1.final域的重排序规则final 2.写final域的重排序规则 3.读final与的重排序规则 4.final域为引用类型 5.为什么final引用不能从构造函数内"逸出" 6.final语义在处理器中的实现 7.JSR-133为什么要增强final的语义 上篇并发编程之Java内存模型volatile的内存语义介绍了volatile的内存语义,本文讲述的是final的内存语义,相比之下,final域的读和写更像是普通变量的访问. 1.final域的重排序规则final

  • 深入分析Java并发编程之CAS

    在Java并发编程的世界里,synchronized 和 Lock 是控制多线程并发环境下对共享资源同步访问的两大手段.其中 Lock 是 JDK 层面的锁机制,是轻量级锁,底层使用大量的自旋+CAS操作实现的. 学习并发推荐<Java并发编程的艺术> 那什么是CAS呢?CAS,compare and swap,即比较并交换,什么是比较并交换呢?在Lock锁的理念中,采用的是一种乐观锁的形式,即多线程去修改共享资源时,不是在修改之前就加锁,而是乐观的认为没有别的线程和自己争锁,就是通过CAS的

  • Java并发编程之ConcurrentLinkedQueue源码详解

    一.ConcurrentLinkedQueue介绍 并编程中,一般需要用到安全的队列,如果要自己实现安全队列,可以使用2种方式: 方式1:加锁,这种实现方式就是我们常说的阻塞队列. 方式2:使用循环CAS算法实现,这种方式实现队列称之为非阻塞队列. 从点到面, 下面我们来看下非阻塞队列经典实现类:ConcurrentLinkedQueue (JDK1.8版) ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全的队列.当我们添加一个元素的时候,它会添加到队列的尾部,当我们

  • 详解Java高并发编程之AtomicReference

    目录 一.AtomicReference 基本使用 1.1.使用 synchronized 保证线程安全性 二.了解 AtomicReference 2.1.使用 AtomicReference 保证线程安全性 2.2.AtomicReference 源码解析 2.2.1.get and set 2.2.2.lazySet 方法 2.2.3.getAndSet 方法 2.2.4.compareAndSet 方法 2.2.5.weakCompareAndSet 方法 一.AtomicReferen

随机推荐