【JUC】volatile和JMM

news2024/11/16 4:54:19

【JUC】volatile和JMM

文章目录

  • 【JUC】volatile和JMM
    • 1. volatile
      • 1.1 特点
      • 1.2 内存语义
    • 2. 内存屏障
      • 2.1 分类
      • 2.2 什么叫保证有序性?
      • 2.3 内存屏障的4种插入策略
    • 3. volatile特性
      • 3.1 保证可见性
      • 3.2 volatile读写过程
      • 3.3 没有原子性
      • 3.4 指令禁重排(有序性)
    • 4. 正确使用volatile
      • 4.1 单一赋值
      • 4.2 状态标志
      • 4.3 开销较低的读、写锁策略
      • 4.4 DCL双端锁
    • 5. 总结

1. volatile

1.1 特点

被volatile关键字修饰的变量有2大特点:

  1. 可见性
  2. 有序性(禁重排)

1.2 内存语义

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量。
  • 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。

2. 内存屏障

内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。

内存屏障之前的所有写操作都要回写到主内存,

内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。

  • 写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。也就是说当看到Store屏障指令,就必须把该指令之前的所有写入指令执行完毕才能继续往下执行。
  • 读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后整免得读取数据指令一定能够读取到最新的数据。

image-20230414213303110

因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。

一句话:对一个 volatile 域的写, happens-before 于任意后续对这个 volatile 域的读,也叫写后读。


2.1 分类

内存屏障可粗分为2种:

  1. 写屏障(Store Memory Barrier)
  2. 读屏障(Load Memory Barrier)

还可细分为以下4种:

屏障类型指令示例说明
LoadLoadLoad1;LoadLoad;Load2保证Load1的读取操作在Load2及后续读取操作之前执行
StoreStoreStore1;StoreStore;Store2在Store2及其后的写操作执行前,保证Store1的写操作已刷新到主内存
LoadStoreLoad1;LoadStore;Store2在Store2及其后的写操作执行前,保证Load1的读操作已读取结束
StoreLoadStore1;StoreLoad;Load2保证Store1的写操作已刷新到主内存之后,Load2及其后的读操作才能执行

2.2 什么叫保证有序性?

禁重排,通过内存屏障禁重排。

  1. 重排序有可能影响程序的执行和实现,因此,我们有时候希望告诉JVM不要重排序。
  2. 对于编译器的重排序,JMM会根据重排序的规则,禁止特定类型的编译器重排序。
  3. 对于处理器的重排序,Java编译器在生成指令序列的适当位置,插入内存屏障指令,来禁止特定类型的处理器排序。

happens-before之volatile变量规则

第一个操作第二个操作:普通写第二个操作:volatile读第二个操作:volatile写
普通读写可重排可重排不可重排
volatile读不可重排不可重排不可重排
volatile写可重排不可重排不可重排
  • 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。
  • 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。
  • 当地一个操作为volatile写时,第二个操作为volatile读时,不能重排。

2.3 内存屏障的4种插入策略

读屏障

  1. 在每个volatile读操作的后面插入一个 LoadLoad 屏障
  2. 在每个volatile读操作的后面插入一个 LoadStore 屏障
  • 在每个volatile读操作的后面插入一个 LoadLoad 屏障,禁止处理器把上面的volatile读与下面的普通读重排序。
  • 在每个volatile读操作的后面插入一个 LoadStore 屏障,禁止处理器把上面的volatile读与下面的普通写重排序。

image-20230414223509294

写屏障

  1. 在每个volatile写操作的前面插入一个 StoreStore 屏障
  2. 在每个volatile写操作的后面插入一个 SotreLoad 屏障
  • 在每个volatile写操作的前面插入一个 StoreStore 屏障,可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
  • 在每个volatile写操作的后面插入一个 StoreLoad 屏障,作用是避免volatile写与后面可能有的volatile读/写操作重排序。

image-20230414224333060


3. volatile特性

3.1 保证可见性

说明:保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可见。

示例:

不加volatile,没有可见性,程序无法停止

static boolean flag = true;

public static void main(String[] args)
{
    new Thread(() -> {
        System.out.println(Thread.currentThread().getName()+"\t -----come in");
        while(flag)
        {

        }
        System.out.println(Thread.currentThread().getName()+"\t -----flag被设置为false,程序停止");
    },"t1").start();

    //暂停几秒钟线程
    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

    flag = false;

    System.out.println(Thread.currentThread().getName()+"\t 修改完成flag: "+flag);


}

运行结果:

image-20230416234536328

加了volatile,保证可见性,程序可以停止

static volatile boolean flag = true;

public static void main(String[] args)
{
    new Thread(() -> {
        System.out.println(Thread.currentThread().getName()+"\t -----come in");
        while(flag)
        {

        }
        System.out.println(Thread.currentThread().getName()+"\t -----flag被设置为false,程序停止");
    },"t1").start();

    //暂停几秒钟线程
    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

    flag = false;

    System.out.println(Thread.currentThread().getName()+"\t 修改完成flag: "+flag);


}

运行结果:

image-20230416234614461


volatile特点:

  1. 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
  2. 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存

3.2 volatile读写过程

Java内存模型中定义的8种工作内存与主内存之间的原子操作

read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)

image-20230416234921940

read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存

load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载

use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作

assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作

store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存

write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量

由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:

lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。

unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用


3.3 没有原子性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。

volatile变量的复合操作(如i++)不具有原子性

class MyNumber
{
    volatile int number;

    public void addPlusPlus()
    {
        number++;
    }
}

public class VolatileNoAtomicDemo
{
    public static void main(String[] args){
        MyNumber myNumber = new MyNumber();

        for (int i = 1; i <=10; i++) {
            new Thread(() -> {
                for (int j = 1; j <=1000; j++) {
                    myNumber.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println(myNumber.number);

    }
}

运行结果如下:

image-20230417000958257

事实上 i++ 由三条指令组成,分别是获取i的值、计算i的值、为i赋值,所以 i++ 不具备原子性。

如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于add方法必须使用synchronized修饰,以便保证线程安全.

image-20230417001324842

多线程环境下,"数据计算"和"数据赋值"操作可能多次出现,即操作非原子。若数据在加载之后,若主内存count变量发生修改之后,由于线程工作内存中的值在此前已经加载,从而不会对变更操作做出相应变化,即私有内存和公共内存中变量不同步,进而导致数据不一致。

对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的。

由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改共享变量的场景必须使用加锁同步


3.4 指令禁重排(有序性)

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序.

不存在数据依赖关系,可以重排序;存在数据依赖关系,禁止重排序

重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!

重排序的分类和执行流程如下:

image-20230418000505447

  • 编译器优化的重排序: 编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序
  • 指令级并行的重排序: 处理器使用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
  • 内存系统的重排序: 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行

数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。

示例:

public class VolatileTest {
    int i = 0;
    volatile boolean flag = false;
    public void write(){
        i = 2;
        flag = true;
    }
    public void read(){
        if(flag){
            System.out.println("---i = " + i);
        }
    }
}

分析:

  • write() 方法中有一个普通写和一个volatile写。所以在volatile写之前插入一个 StoreStore 屏障,之后插入一个 StoreLoad 屏障。

    image-20230418000941631

  • read() 方法中有一个volatile读和一个普通读。所以在volatile读之后插入一个 LoadLoad 屏障和 LoadStore 屏障。

    image-20230418001150382


4. 正确使用volatile

使用volatile的几种场景:

  • 单一赋值,不包括符合运算符(如i++之类)
  • 状态标志,判断业务是否结束
  • 开销较低的读、写锁策略
  • DCL双端锁的发布

4.1 单一赋值

示例:

volatile int a = 10;
volatile boolean flag = false

4.2 状态标志

示例:

private volatile static boolean flag = true;

    public static void main(String[] args)
    {
        new Thread(() -> {
            while(flag) {
                //do something......
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            flag = false;
        },"t2").start();
    }

4.3 开销较低的读、写锁策略

示例:

public class UseVolatileDemo
{
    /**
     * 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
     * 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
     */
    public class Counter
    {
        private volatile int value;

        public int getValue(){
            return value;   //利用volatile保证读取操作的可见性
        }
        
        public synchronized int increment(){
            return value++; //利用synchronized保证复合操作的原子性
        }
    }
}

4.4 DCL双端锁

public class SafeDoubleCheckSingleton
{
    //通过volatile声明,实现线程安全的延迟初始化。
    private volatile static SafeDoubleCheckSingleton singleton;
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取

                                      //原理:利用volatile,禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}

创建对象分为三步:

  1. 分配对象的内存空间
  2. 初始化对象
  3. 设置变量指向刚分配的内存地址

不加volatile关键字的话重排序之后可能变成1->3->2的顺序。


5. 总结

  1. volatile可见性
  2. volatile没有原子性
  3. volatile禁重排
  4. 内存屏障是什么
    • 是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。
  5. 内存屏障能干嘛
    • 阻止屏障两边的指令重排序
    • 写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
    • 读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据
  6. 内存屏障四大指令
    • 在每一个volatile写操作前面插入一个StoreStore屏障
    • 在每一个volatile写操作后面插入一个StoreLoad屏障
    • 在每一个volatile读操作后面插入一个LoadLoad屏障
    • 在每一个volatile读操作后面插入一个LoadStore屏障
  7. 三句话总结
    • volatile写之前的操作,都禁止重排序到volatile之后
    • volatile读之后的操作,都禁止重排序到volatile之前
    • volatile写之后volatile读,禁止重排序

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/442735.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python标识符概念及规范

在python中 能取名字的东西非常非常多 例如 我们之前学的变量 以及后面要接触的 函数 类&#xff0c;等等&#xff0c;等等 而我们给这些取的名字 被统称为 标识符 而 python中 标识符的命名也是有限制的 主要有三种 1 内容限定 2 大小写铭感 3 不能使用关键字 内容限定来讲…

leetcode6_N字形变换

如有错误&#xff0c;感谢不吝赐教、交流 leetcode6 题目描述 将一个给定字符串 s 根据给定的行数 numRows &#xff0c;以从上往下、从左到右进行 Z 字形排列。 比如输入字符串为 “PAYPALISHIRING” 行数为 3 时&#xff0c;排列如下&#xff1a; P A H N A P L S I…

HTB-SecNotes

HTB-SecNotes 信息收集8808端口80端口通过CSRF获取通过二次注入 立足tyler -> administrator 信息收集 8808端口 Windows IIS 10.0 可以从官方文档查看10.0版本可能的操作系统。 80端口 通过CSRF获取 目录扫描发现需要登陆后继续进一步操作啊。 对其进行简单的SQL注入测…

数据库基础篇 《7.单行函数》

目录 1. 函数的理解 1.1 什么是函数 1.2 不同DBMS函数的差异 ​编辑1.3 MySQL的内置函数及分类 ​编辑 2. 数值函数 2.1 基本函数 ​编辑 2.2 角度与弧度互换函数 2.3 三角函数 ​编辑 2.4 指数与对数 ​编辑 2.5 进制间的转换 ​编辑3. 字符串函数 ​编辑…

SAM(segment anything model)分割一切 Demo测试及API调用

SAM 分割一切 一&#xff0c;SAM介绍1.1 介绍1.2 项目链接 二&#xff0c;Demo-Test&#xff1a;2.1 Demo功能介绍2.1.1&#xff0c;首页就是这个SAM&#xff0c;点击try demo&#xff0c;可以选择它的自带图片&#xff0c;也可以自己添加。2.1.2 , 自己上传图片测试&#xff1…

[java基础]面向对象(五)

访问控制修饰符&#xff1a;--------------保护数据的安全(隐藏数据、暴露行为)&#xff0c;实现封装 public&#xff1a;公开的&#xff0c;任何类 private&#xff1a;私有的&#xff0c;本类 protected&#xff1a;受保护的&#xff0c;本类、派生类、同包类 默认的&…

learn_C_deep_3 (最名不符实的关键字 - static、static关键字总结、基本数据类型、最冤枉的关键字 - sizeof)

目录 最名不符实的关键字 - static stati修饰全局变量和函数 static修饰局部变量 static关键字总结 几个问题 1.c语言要设置全局变量和函数可以跨文件使用的原因 2.C程序地址空间是什么样的&#xff1f; 3.局部变量为什么具有临时性 4.全局变量为什么具有全局性 5.为…

vue-cli版本号始终是2.9.6,且无法删除,安装更新无效的问题。

参考博客 目录 1.问题出现原因2.我的解决办法&#xff1a;删除原脚手架&删除原vuevue.cmd 1.问题出现原因 从各种博客我得知&#xff0c;这种问题出现在2处&#xff1a; 没有卸载原来的脚手架原来的vue和vue.cmd没删除干净 2.我的解决办法&#xff1a;删除原脚手架&…

[oeasy]python0135_命名惯用法_name_convention

命名惯用法 回忆上次内容 上次 了解了isidentifier的细节 关于 关键字关于 下划线 如何查询 变量所指向的地址&#xff1f; id 如何查询 已有的各种变量&#xff1f; locals 如果 用一个变量a的值 给另一个变量b 赋值是什么样的过程 呢&#xff1f;&#xff1f;&#x1f914;…

当,Kotlin Flow与Channel相逢

前言 之前的文章已经分析了Flow的相关原理与简单使用&#xff0c;Flow之所以用起来香&#xff0c;Flow便捷的操作符功不可没&#xff0c;而想要熟练使用更复杂的操作符&#xff0c;那么需要厘清Flow和Channel的关系。 本篇文章构成&#xff1a; 1. Flow与Channel 对比 1.1 Fl…

AVL树(C++实现)

文章目录 AVL树的概念AVL树结点定义AVL树的插入AVL树的旋转左单旋右单旋左右单旋右左双旋 AVL树的验证AVL树的性能AVL树及测试完整代码 AVL树的概念 二叉搜索树虽然可以缩短查找的效率,但如果数据有序或接近有序,那么二叉搜索树将退化为单支树,查找元素则相当于在顺序表中搜索…

从零手写Resnet50实战——利用 torch 识别出了虎猫和萨摩耶

大家好啊&#xff0c;我是董董灿。 自从前几天手写了一个慢速卷积之后&#xff08;从零手写Resnet50实战—手写龟速卷积&#xff09;&#xff0c;我便一口气将 Resnet50 中剩下的算法都写完了。 然后&#xff0c;暴力的&#xff0c;按照 Resnet50 的结构&#xff0c;将手写的…

【Flowable】Flowable基础表结构

1.表结构讲解 表结构创建文件&#xff1a;flowable-engine-6.3.0.jar!\org\flowable\db\create\flowable.mysql.create.engine.sql 工作流程的相关操作都是操作存储在对应的表结构中&#xff0c;为了能更好的弄清楚Flowable的实现原理和细节&#xff0c;我们有必要先弄清楚Fl…

Python边缘检测之prewitt, sobel, laplace算子

文章目录 滤波算子简介具体实现测试 滤波算子简介 ndimage中提供了卷积算法&#xff0c;并且建立在卷积之上&#xff0c;提供了三种边缘检测的滤波方案&#xff1a;prewitt, sobel以及laplace。 在convolve中列举了一个用于边缘检测的滤波算子&#xff0c;统一维度后&#xf…

es6 const的使用

1.const用来定义常量&#xff0c;赋值知乎不能再赋值&#xff0c;再次赋值会报错。 <script>//1.定义常量&#xff0c;赋值后不能再赋值&#xff0c;在赋值报错const count 1// count 2</script> ​ 2.const不能只声明不赋值&#xff0c;会报错。 <script>…

智能学习 | MATLAB实现CS-BP多变量时间序列预测(布谷鸟搜索算法优化BP神经网络)

智能学习 | MATLAB实现CS-BP多变量时间序列预测(布谷鸟搜索算法优化BP神经网络) 目录 智能学习 | MATLAB实现CS-BP多变量时间序列预测(布谷鸟搜索算法优化BP神经网络)预测效果基本介绍程序设计参考资料预测效果 基本介绍 MATLAB实现CS-BP多变量时间序列预测(布谷鸟搜索算法…

chatGPT衣食住行10种场景系列教程(01)chatGPT热点事件+开发利器

导读 时隔5个多月&#xff0c;chatGPT可谓是一日千里&#xff0c;越演越火&#xff0c;携带着AIGC行业一起飞了起来&#xff0c;那么在短短5个月当中有那些值得我们关注的事件&#xff1f;有那些好玩的场景&#xff1f;以及有那些chatGPT好用的工具&#xff1f;本文都将一一告…

大数据时代必备技能,学好数据可视化

互联网时代&#xff0c;都在强调数据分析的重要性&#xff0c;但是干巴巴的数据没人爱看&#xff0c;老板们对“简单直观地看数据”的需求愈发强烈。随着大数据建设的如火如荼&#xff0c;别讲底层技术和算法牛逼&#xff0c;最终的效率提升、业绩提升要通过数据展示出来&#…

vba:消息框基础,msgbox

常量常量值说明vbOKOnly0只显示“确定”按钮&#xff08;缺省值&#xff09;VbOKCancel1显示“确定”和“取消”按钮VbAbortRetryIgnore2显示“终止”、“重试”和“忽略” 按钮VbYesNoCancel3显示“是”、“否”和“取消”按钮VbYesNo4显示“是”和“否”按钮VbRetryCancel5显…