我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Java中的volatile

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Java中的volatile

文章目录

1、volatile的内存语义

内存可见性

​ volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级,相比使用synchronized所带来的庞大开销,倘若能恰当的合理的使用volatile,自然是美事一桩。

为了能比较清晰彻底的理解volatile,我们一步一步来分析。首先来看看如下代码

public class volatileDemo1 {    static boolean flag = true;    public static void main(String[] args) {        new Thread(()->{            System.out.println(Thread.currentThread().getName()+"\t -----come in");            try {                TimeUnit.SECONDS.sleep(2);            } catch (InterruptedException e) {                e.printStackTrace();            }            flag = false;        },"t1").start();        new Thread(()->{            System.out.println(Thread.currentThread().getName()+"\t -----come in");            while (flag) {            }            System.out.println(Thread.currentThread().getName()+"\t -----flag被设置为false,程序停止");        },"t2").start();    }}

上面这个例子,模拟在多线程环境里,t1线程对flag共享变量修改的值能否被t2可见,即是否输出 “-----flag被设置为false,程序停止” 这句话?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vzNZ5vt5-1656404046754)(JUC并发编程.assets/image-20220628160812458.png)]

答案是:NO! 输出结果如下~
在这里插入图片描述

​ 这个结论会让人有些疑惑,可以理解。因为倘若在单线程模型里,因为先行发生原则之happens-before,自然是可以正确保证输出的;但是在多线程模型中,是没法做这种保证的。因为对于共享变量flag来说,线程t1的修改,对于线程t2来讲,是"不可见"的。也就是说,线程t2此时可能无法观测到flage已被修改为false。那么什么是可见性呢?

所谓可见性,是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更,JMM规定了所有的变量都存储在主内存。很显然,上述的例子中是没有办法做到内存可见性的。

volatile的内存语义

  • 一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
  • 一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量

所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取,从而保证了可见性。

volatile变量有2大特点,分别是:

  • 可见性

  • 有序性:禁重排!

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

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

    但重排后的指令绝对不能改变原有串行语义!

那么volatile凭什么可以保证可见性和有序性呢??

  • 内存屏障Memory Barrier~

2、内存屏障

​ 内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。

​ Java中的内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性。

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

粗分主要是以下两种屏障:

  • 读屏障(Load Memory Barrier) :在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据。
  • 写屏障(Store Memory Barrier) :在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中。

因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读。

让我们来看看源码:sun.misc.Unsafe.java
在这里插入图片描述
主要包括以上三个方法,接着在对应的Unsafe.cpp 源码中查看:
在这里插入图片描述
在底层C++代码中发现其底层调用的是OrderAccess类中的方法~
在这里插入图片描述
我们发现其又细分了四种屏障,四大屏障分别是什么意思呢?

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

接下来结合底层linux_86代码来分析~
在这里插入图片描述

2、happens-before 之 volatile 变量规则

在这里插入图片描述

给大家讲解一下上表,主要有以下三种情况不允许重拍~

  1. 蓝色:当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。

  2. 红色:当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会重排序到volatile写之后。

  3. 绿色:当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。

其他情况都允许被重排。

4、Demo

可见性案例

public class volatileDemo1 {    static volatile boolean flag = true;    public static void main(String[] args) {        new Thread(()->{            System.out.println(Thread.currentThread().getName()+"\t -----come in");            try {                TimeUnit.SECONDS.sleep(2);            } catch (InterruptedException e) {                e.printStackTrace();            }            flag = false;        },"t1").start();        new Thread(()->{            System.out.println(Thread.currentThread().getName()+"\t -----come in");            while (flag) {            }            System.out.println(Thread.currentThread().getName()+"\t -----flag被设置为false,程序停止");        },"t2").start();    }}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DVv5C5mZ-1656423701210)(JUC并发编程.assets/image-20220628161643842.png)]

若不加volatile修饰为何t2 看不到被 t1线程修改为 false的flag的值?

  1. t1线程修改了flag之后没有将其刷新回住内存,所以t2线程获取不到。
  2. 主线程将flag刷新到了主内存,但是t2一直读取的是自己工作内存中flag的值,没有去主内存中更新获取flag最新的值。

使用volatile修饰共享变量后,被volatile修饰的变量有以下特点:

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

无原子性案例

首先我们先编写一个用 synchronized 修饰的案例:

class MyNumber {    int number;    public synchronized void addPlusPlus() {        number++;    }}

在我们的main方法中开启一个线程执行 number++的方法,然后等待2秒,大家的预期值是不是1000呢?

public class volatileDemo2 {    public static void main(String[] args) {        MyNumber myNumber = new MyNumber();        for (int i = 0; i < 10; i++) {            new Thread(()->{                for (int i1 = 0; i1 < 1000; i1++) {                    myNumber.addPlusPlus();                }            }).start();        }        try {            TimeUnit.SECONDS.sleep(1);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println(myNumber.number);    }}

结果是一致的,因为使用了 synchronized 修饰了number++方法,从而保证了原子性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RhetVkfU-1656423701211)(JUC并发编程.assets/image-20220628163421068.png)]

接下来 使用 volatile修饰number~

class MyNumber {    volatile int number;    public void addPlusPlus() {        number++;    }}

在这里插入图片描述

那为什么会出现不预期的结果呢?

​ 对于volatile变量具备可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的,也仅是数据加载时是最新的。但是在多线程环境下,“数据计算” 和 “数据赋值” 操作可能多次出现,若数据在加载之后,若主内存volatile修饰变量发生修改之后,线程工作内存中的操作将会作废去读主内存的最新值,操作出现丢失问题。即 各线程工作内存和主内存中变量不同步,进而导致数据不一致。由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步。

禁重排

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

在本案例中 变量i 和 flag 语句的执行顺序如果被重排的话就会影响结果,存在数据依赖关系,禁止重排序。

说了这么多,那么在什么时候使用 volatile 呢?

  1. 单一赋值可以,但含复合运算赋值不可以
  2. 状态标志,判断业务是否结束
  3. 开销较低的读,写锁策略
  4. DCL双端锁的发布

这里要提提 DCL双端锁,小编最近面试有被问到~在接下来的博客里给大家谈谈单例模式

来源地址:https://blog.csdn.net/m0_49183244/article/details/125493673

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Java中的volatile

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

java中的volatile怎么应用

这篇文章主要介绍了java中的volatile怎么应用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇java中的volatile怎么应用文章都会有所收获,下面我们一起来看看吧。在某些情况下,volatile关键
2023-06-30

怎么在java中使用volatile

本篇文章为大家展示了怎么在java中使用volatile,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。Java是什么Java是一门面向对象编程语言,可以编写桌面应用程序、Web应用程序、分布式系统和
2023-06-14

深入浅析java 中volatile与lock的原理

深入浅析java 中volatile与lock的原理?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。java 中volatile和lock原理分析volatile和lock是
2023-05-31

java中volatile变量的原理是什么

这篇文章给大家介绍java中volatile变量的原理是什么,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。Java可以用来干什么Java主要应用于:1. web开发;2. Android开发;3. 客户端开发;4. 网
2023-06-14

Java中的volatile关键字有什么用

本篇内容主要讲解“Java中的volatile关键字有什么用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java中的volatile关键字有什么用”吧!一、volatile作用可以保证多线程环
2023-06-30

Java中static和volatile关键字的区别

Java中static和volatile关键字的区别有:1. 作用范围不同;2. 变量类型不同;3. 内存模型不同;4. 线程安全不同;5. 性能不同。作用不同指的是,static关键字用于声明类级别的变量或方法,所有类的实例共享同一个static变量的副本。volatile关键字用于确保一个变量在多线程环境中的可见性,使所有线程都能看到最新的变量值。
Java中static和volatile关键字的区别
2023-10-29

java中volatile和synchronized的区别与联系

java中volatile和synchronized的区别与联系这个可能是最好的对比volatile和synchronized作用的文章了。volatile是一个变量修饰符,而synchronized是一个方法或块的修饰符。所以我们使用这两
2023-05-31

java中的volatile关键字怎么使用

本篇内容介绍了“java中的volatile关键字怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!1.volatile实现可见性的原理
2023-06-25

Java中Volatile变量有什么用

这篇文章将为大家详细讲解有关Java中Volatile变量有什么用,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。Volatile关键字是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步
2023-06-25

Java中不可或缺的关键字volatile详析

volatile是Java提供的一种轻量级的同步机制,下面这篇文章主要给大家介绍了关于Java中不可或缺的关键字volatile的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
2022-12-27

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录