文章目录

  • 1、volatile的内存语义
  • 2、内存屏障
  • 2、happens-before 之 volatile 变量规则
  • 4、Demo

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类中的方法~

我们发现其又细分了四种屏障,四大屏障分别是什么意思呢?

屏障类型 指令示例 说明
LoadLoad Load1;LoadLoad;Load2 保证Load的读取操作在load2及后续读取操作之前执行
StoreStore Store1;StoreStore;Store2 在Store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
LoadStore Load1;LoadLoad;Store2 在Store2及其后的写操作执行前,保证Load1的读操作已读取结束
StoreLoad Store1;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();}
}

若不加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++方法,从而保证了原子性

接下来 使用 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双端锁,小编最近面试有被问到~在接下来的博客里给大家谈谈单例模式

Java中的volatile相关推荐

  1. 面试:说说Java中的 volatile 关键词?

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 作者 | Matrix海子 来源 | https://w ...

  2. 如何理解 JAVA 中的 volatile 关键字

    如何理解 JAVA 中的 volatile 关键字 最近在重新梳理多线程,同步相关的知识点.关于 volatile 关键字阅读了好多博客文章,发现质量高适合小白的不多,最终找到一篇英文的非常通俗易懂. ...

  3. java中的Volatile关键字使用

    文章目录 什么时候使用volatile Happens-Before java中的Volatile关键字使用 在本文中,我们会介绍java中的一个关键字volatile. volatile的中文意思是 ...

  4. Java中的Volatile如何工作? Java中的volatile关键字示例

    如何在Java中使用Volatile关键字 在Java采访中,什么是volatile变量以及何时在Java中使用volatile变量是Java 采访中一个著名的多线程采访问题 . 尽管许多程序员都知道 ...

  5. java中二进制怎么说_面试:说说Java中的 volatile 关键词?

    volatile 这个关键字可能很多朋友都听说过,或许也都用过.在 Java 5 之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 Java 5之后,volatile 关 ...

  6. 深入理解Java中的volatile关键字

    在再有人问你Java内存模型是什么,就把这篇文章发给他中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性.可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized ...

  7. java中的Volatile 变量

    Java 语言中的 volatile 变量可以被看作是一种 "程度较轻的 synchronized":与 synchronized 块相比,volatile 变量所需的编码较少,并 ...

  8. java中的volatile变量

    同步与线程间通信: 通信  通信是指消息在两条线程之间传递.  既然要传递消息,那接收线程 和 发送线程之间必须要有个先后关系,此时就需要用到同步.通信和同步是相辅相成的. 同步  同步是指,控制多条 ...

  9. 自顶向下彻底理解 Java 中的 volatile 关键字

    标题 neta 自<计算机网络自顶向下> 思维导图 volatile 在 Java 中被称为轻量级 synchronized.很多并发专家引导用户远离 volatile 变量,因为使用它们 ...

最新文章

  1. CTF-i春秋网鼎杯第一场misc部分writeup
  2. boost::fusion::filter用法的测试程序
  3. [蓝桥杯]算法提高 vertex cover(dfs)
  4. Linux编程练习 --多线程5--信号量(semaphore)
  5. Controller和RequestMapping
  6. 面向切面编程AspectJ在Android埋点的实践
  7. 01.26 小组功能初步总结
  8. webloigc12服务启动不了_weblogic启动不能锁定AdminServer.lok的故障处理
  9. 单身暴击!程序员用 Python 给女朋友写了个翻译软件
  10. 经典案例 | I-SPY2乳腺癌药物临床试验采用RPPA技术建立药物响应相关分子分型
  11. react 截取视频随意一帧裁剪图片存为base64格式
  12. 数据改版 | CnOpenData中国行政区划数据
  13. java.io.IOException: Server returned HTTP response code: 503 for UR
  14. html中水平线颜色代码,网页设计水平线代码 怎么在dw中修改水平线的颜色
  15. 求出二维数组的最大元素及其所在的坐标
  16. UGUI源代码之Image-Sliced模式
  17. 2019年第十一届蓝桥杯国赛JavaB组第H题——“大胖子走迷宫”题目及解析
  18. 从键盘输入一个三位整数n,分别求出n的个位数字、十位数字和百位数字
  19. cscd期刊是c刊吗_核心期刊和C刊有什么区别
  20. baiduRanking多站点批量百度排名查询

热门文章

  1. 量化交易ctp接口是什么以及有什么用?
  2. ug896-vivado-ip中文文档 | Xilinx
  3. 猎豹MFC--画椭圆圆弧饼图多边形及其他形状
  4. 中国螺旋埋弧焊管市场发展现状及前景规划深度研究报告2022-2028年版
  5. NetworkX创建图
  6. 计算机主机箱背面各,一种计算机主机箱的制作方法
  7. 含有token鉴权的接口项目使用unittest框架设计测试登录,充值的接口
  8. 在vue中使用高德地图的下属省市区查询,实现省市区的联动以及规划范围
  9. 控制台 钢铁雄心2_陆战学说_钢铁雄心2控制台及秘籍一览_3DM单机
  10. 大数据参考架构和关键技术