点击上方 好好学java ,选择 星标 公众号

重磅资讯、干货,第一时间送达
今日推荐:终于放弃了单调的swagger-ui了,选择了这款神器—knife4j个人原创100W+访问量博客:点击前往,查看更多

前言

volatile 是 Java 里的一个重要的指令,它是由 Java 虚拟机里提供的一个轻量级的同步机制。一个共享变量声明为 volatile 后,特别是在多线程操作时,正确使用 volatile 变量,就要掌握好其原理。

特性

volatile 具有可见性有序性的特性,同时,对 volatile 修饰的变量进行单个读写操作是具有原子性

这几个特性到底是什么意思呢?

  • 可见性: 当一个线程更新了 volatile 修饰的共享变量,那么任意其他线程都能知道这个变量最后修改的值。简单的说,就是多线程运行时,一个线程修改 volatile 共享变量后,其他线程获取值时,一定都是这个修改后的值。

  • 有序性: 一个线程中的操作,相对于自身,都是有序的,Java 内存模型会限制编译器重排序和处理器重排序。意思就会说 volatile 内存语义单个线程中是串行的语义。

  • 原子性: 多线程操作中,非复合操作单个 volatile 的读写是具有原子性的。

可见性

可见性是在多线程中保证共享变量的数据有效,接下来我们通过有 volatile 修饰的变量和无 volatile 修饰的变量代码的执行结果来做对比分析。

附上我历时三个月总结的 Java面试思维导图,拿去不谢!

下载方式

1. 首先扫描下方二维码

2. 后台回复「思维导图」即可获取

无 volatile 修饰变量

以下是没有 volatile 修饰变量代码,通过创建两个线程,来验证 flag 被其中一个线程修改后的执行情况。

/** * Created by YANGTAO on 2020/3/15 0015. */public class ValatileDemo {static Boolean flag = true;public static void main(String[] args) {// A 线程,判断其他线程修改 flag 之后,数据是否对本线程有效        new Thread(() -> {            while (flag) {}            System.out.printf("********** %s 线程执行结束!**********", Thread.currentThread().getName());        }, "A").start();// B 线程,修改 flag 值        new Thread(() -> {            try {                // 避免 B 线程比 A 线程先运行修改 flag 值                  TimeUnit.SECONDS.sleep(1);                flag = false;                // 如果 flag 值修改后,让 B 线程先打印信息                TimeUnit.SECONDS.sleep(2);System.out.printf("********** %s 线程执行结束!**********", Thread.currentThread().getName());            } catch (InterruptedException e) {                e.printStackTrace();            }        }, "B").start();}}

上面代码中,当 flag 初始值 true,被 B 线程修改为 false。如果修改后的值对 A 线程有效,那么正常情况下 A 线程会先于 B 线程结束。执行结果如下:

执行结果是:当 B 线程执行结束后, flag=false并未对 A 线程生效,A 线程死循环。

volatile 修饰变量

在上述代码中,当我们把 flag 使用 volatile 修饰:

/** * Created by YANGTAO on 2020/3/15 0015. */public class ValatileDemo {static volatile Boolean flag = true;public static void main(String[] args) {// A 线程,判断其他线程修改 flag 之后,数据是否对本线程有效        new Thread(() -> {            while (flag) {}            System.out.printf("********** %s 线程执行结束!**********", Thread.currentThread().getName());        }, "A").start();// B 线程,修改 flag 值        new Thread(() -> {            try {                // 避免 B 线程比 A 线程先运行修改 flag 值                  TimeUnit.SECONDS.sleep(1);                flag = false;                // 如果 flag 值修改后,让 B 线程先打印信息                TimeUnit.SECONDS.sleep(2);System.out.printf("********** %s 线程执行结束!**********", Thread.currentThread().getName());            } catch (InterruptedException e) {                e.printStackTrace();            }        }, "B").start();}}

B 线程修改 flag 值后,对 A 线程数据有效,A 线程跳出循环,执行完成。所以 volatile 修饰的变量,有新值写入后,对其他线程来说,数据是有效的,能被其他线程读到。

主内存和工作内存

上面代码中的变量加了 volatile 修饰,为什么就能被其他线程读取到,这就涉及到 Java 内存模型规定的变量访问规则。

  • 主内存:主内存是机器硬件的内存,主要对应Java 堆中的对象实例数据部分。

  • 工作内存:每个线程都有自己的工作内存,对应虚拟机栈中的部分区域,线程对变量的读/写操作都必须在工作内存中进行,不能直接读写主内存的变量。

上面 无volatile修饰变量部分的代码执行示意图如下:

当 A 线程读取到 flag 的初始值为 true,进行 while 循环操作,B 线程将工作内存 B 里的 flag 更新为 false,然后将值发送到主内存进行更新。随后,由于此时的 A 线程不会主动刷新主内存中的值到工作内存 A 中,所以线程 A 所取得 flag 值一直都是 true,A 线程也就为死循环不会停止下来。

上面 volatile修饰变量部分的代码执行示意图如下:

当 B 线程更新 volatile 修饰的变量时,会向 A 线程通过线程之间的通信发送通知(JDK5 或更高版本),并且将工作内存 B 中更新的值同步到主内存中。A 线程接收到通知后,不会再读取工作内存 A 中的值,会将主内存的变量通过主内存和工作内存之间的交互协议,拷贝到工作内存 A 中,这时读取的值就是线程 A 更新后的值 flag=false。整个变量值得传递过程中,线程之间不能直接访问自身以外的工作内存,必须通过主内存作为中转站传递变量值。在这传递过程中是存在拷贝操作的,但是对象的引用,虚拟机不会整个对象进行拷贝,会存在线程访问的字段拷贝。

有序性

volatile 包含禁止指令重排的语义,Java 内存模型会限制编译器重排序和处理器重排序,简而言之就是单个线程内表现为串行语义。那什么是重排序?重排序的目的是编译器和处理器为了优化程序性能而对指令序列进行重排序,但在单线程和单处理器中,重排序不会改变有数据依赖关系的两个操作顺序。比如:

/** * Created by YANGTAO on 2020/3/15 0015. */public class ReorderDemo {        static int a = 0;static int b = 0;public static void main(String[] args) {        a = 2;        b = 3;    }}
// 重排序后:
public class ReorderDemo {        static int a = 0;static int b = 0;public static void main(String[] args) {        b = 3;  // a 和 b 重排序后,调换了位置        a = 2;    }}

但是如果在单核处理器和单线程中数据之间存在依赖关系则不会进行重排序,比如:

/** * Created by YANGTAO on 2020/3/15 0015. */public class ReorderDemo {static int a = 0;static int b = 0;public static void main(String[] args) {        a = 2;        b = a;    }}
// 由于 a 和 b 存在数据依赖关系,则不会进行重排序

volatile 实现特有的内存语义,Java 内存模型定义以下规则(表格中的 No 代表不可以重排序):

Java 内存模型在指令序列中插入内存屏障来处理 volatile 重排序规则,策略如下:

  • volatile 写操作前插入一个 StoreStore 屏障

  • volatile 写操作后插入一个 StoreLoad 屏障

  • volatile 读操作后插入一个 LoadLoad 屏障

  • volatile 读操作后插入一个 LoadStore 屏障

该四种屏障意义:

  • StoreStore:在该屏障后的写操作执行之前,保证该屏障前的写操作已刷新到主内存。

  • StoreLoad:在该屏障后的读取操作执行之前,保证该屏障前的写操作已刷新到主内存。

  • LoadLoad:在该屏障后的读取操作执行之前,保证该屏障前的读操作已读取完毕。

  • LoadStore:在该屏障后的写操作执行之前,保证该屏障前的读操作已读取完毕。

原子性

前面有提到 volatile 的原子性是相对于单个 volatile 变量的读/写具有,比如下面代码:

/** * Created by YANGTAO on 2020/3/15 0015. */public class AtomicDemo {static volatile int num = 0;public static void main(String[] args) throws InterruptedException {final CountDownLatch latch = new CountDownLatch(10);        for (int i = 0; i < 10; i++) {  // 创建 10 个线程            new Thread(() -> {                for (int j = 0; j < 1000; j++) {    // 每个线程累加 1000                    num ++;                }                latch.countDown();            }, String.valueOf(i+1)).start();        }latch.await();                // 所有线程累加计算的数据        System.out.printf("num: %d", num);    }}

上面代码中,如果 volatile 修饰 num,在 num++ 运算中能持有原子性,那么根据以上数量的累加,最后应该是 num:10000。代码执行结果:

结果与我们预计数据的相差挺多,虽然 volatile 变量在更新值的时候回通知其他线程刷新主内存中最新数据,但这只能保证其基本类型变量读/写的原子操作(如:num = 2)。由于 num++是属于一个非原子操作的复合操作,所以不能保证其原子性。

使用场景

  1. volatile 变量最后的运算结果不依赖变量的当前值,也就是前面提到的直接赋值变量的原子操作,比如:保存数据遍历的特定条件的一个值。

  2. 可以进行状态标记,比如:是否初始化,是否停止等等。

总结

volatile 是一个简单又轻量级的同步机制,但在使用过程中,局限性比较大,要想使用好它,必须了解其原理及本质,所以在使用过程中遇到的问题,相比于其他同步机制来说,更容易出现问题。但使用好 volatile,在某些解决问题上能获取更佳的性能。

最后,再附上我历时三个月总结的 Java 面试 + Java 后端技术学习指南,这是本人这几年及春招的总结,目前,已经拿到了大厂offer,拿去不谢!

下载方式

1. 首先扫描下方二维码

2. 后台回复「Java面试」即可获取

volatile 手摸手带你解析相关推荐

  1. 微服务(三) 【手摸手带你搭建Spring Cloud】 Ribbon 什么是负载均衡?spring cloud如何实现负载均衡?ribbon负载均衡有几种策略?Ribbon是什么?

    在上一章,我介绍了springcloud的eureka搭建.我们做了服务注册.最后我们还介绍了一些续约,失效剔除等参数配置.已经不需要再通过手动输入ip去访问服务,而是通过中心只需要通过服务名就可以获 ...

  2. 每天研究一个产品,阿德老师“手摸手”带你写产品分析报告 |

    作为一个产品经理,要高频地去把玩各种最新产品,所以我们想把那些对世界充满好奇心.勇于探索新鲜事物的产品经理都聚在一起.一起深入研究国内外最新/奇产品,一起发现有趣的事情,并把研究心得都整理成文章沉淀下 ...

  3. 手摸手,带你用vue撸后台 系列一(基础篇) - 掘金

    完整项目地址:vue-element-admin 系列文章: 手摸手,带你用 vue 撸后台 系列一(基础篇) 手摸手,带你用 vue 撸后台 系列二(登录权限篇) 手摸手,带你用 vue 撸后台 系 ...

  4. 《手摸手带你学ClickHouse》之Oracle同步数据到Clickhouse

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 chaodev 即可关注. 文章目录 前文回顾: <手摸手带你学ClickHouse>之安装部署 <手摸手带你学Cl ...

  5. 手摸手带你写项目----秒杀系统(一)

    博客地址: 手摸手带你写项目----秒杀系统(一) 所有文章会第一时间在博客更新! 后面的时间我会手摸手带大家一起写几个实战性的项目.主要希望能应用上之前梳理的那些知识点,同时让没有写过项目的同学对实 ...

  6. 手摸手带你学移动端WEB开发

    HTML常用标签总结 手摸手带你学CSS HTML5与CSS3知识点总结 好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP ...

  7. 《手摸手带你学ClickHouse》之安装部署

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 chaodev 即可关注. 文章目录 1.Clickhouse简介 1.1 简介 1.2 应用场景 1.3 架构 2. ClickHo ...

  8. 手摸手带你理解 进制 字节 ASCII码 Unicode 与 字节编码(UTF-8 /16)等(下)

    手摸手带你理解 进制 字节 ASCII码 Unicode 与 字节编码(UTF-8 /16)等(上) Unicode 先讲讲这个东西的规则 Unicode 通常(不是所有)用两个字节来表示 一个字符 ...

  9. 手摸手带你用实现vue全屏loading插件

    手摸手带你用实现vue全屏loading插件 前言: 由于我们打开网页时,浏览器与服务器交互需要时间,受限于宽带以及服务器性能,导致用户在访问一个网页时,往往需要一个等待期,才能在浏览器中真正完全展示 ...

  10. vue 前端显示图片加token_手摸手,带你用vue撸后台 系列二(登录权限篇)

    完整项目地址:vue-element-admin https://github.com/PanJiaChen/vue-element-admin 前言 拖更有点严重,过了半个月才写了第二篇教程.无奈自 ...

最新文章

  1. RedHat 7.0 Chrome浏览器 安装
  2. 手机拍视频,实时换背景,继马卡龙玩图后,Versa又出了一款更厉害的App
  3. Python 笔试集(4):True + True == ?
  4. c语言蓝桥十进制转十六进制,蓝桥杯 基础练习 十进制转十六进制
  5. Sublime Text
  6. Spring Security学习(二)
  7. CentOS5.6环境安装oracle 10g(完整版)
  8. 怎么注销笔记本icloud_如何在笔记本电脑或台式机的Web浏览器中在线查看Apple iCloud照片
  9. Exynos4412启动过程分析
  10. 算子基本思想_2.2 量子力学基本假设 Part 2
  11. linux下git的使用
  12. echarts x轴像直尺一样设置刻度_MATLAB作图实例:6:指定轴刻度值和标签
  13. Ubuntu 系统进不去 左上角减号
  14. cocos2d-x移植android,Cocos2d-x Xcode 移植到 Android stutio
  15. ASP.NET(C#版) FileUpload控件
  16. android 自动化测试 百度,Android 自动化测试框架-百度cafe
  17. 记一次RATEL脱壳配合Il2CppDumper解密完成的样本分析
  18. z变换判断稳定性和因果性_LTI系统判断因果性稳定性.PPT
  19. 边缘计算的深刻详细解读
  20. 终端连接阿里云服务器出现Permission denied (publickey)解决方法

热门文章

  1. 用Visual C#开发简单的复合控件
  2. C++ - RTTI(RunTime Type Information)运行时类型信息 详解
  3. vim编辑器的常用技巧
  4. 基于Spring Cloud实现微服务前后端系统
  5. 近世代数--有限交换群--存在子群的阶是群阶的因子
  6. [力扣leetcode39]组合总和及回溯法
  7. shell中三种引号的区别
  8. 【Python小程序】必备软件系列之文字识别提取,前台再也不同担心纸质转电子文件啦~
  9. 【Python小游戏】当当当当 万众瞩目得《滑雪大冒险》来啦~(附源码)
  10. 08-Measured Boot Driver (MBD)