前言

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

特性

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

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

  • 可见性: 当一个线程更新了 volatile 修饰的共享变量,那么任意其他线程都能知道这个变量最后修改的值。简单的说,就是多线程运行时,一个线程修改 volatile 共享变量后,其他线程获取值时,一定都是这个修改后的值。
  • 有序性: 一个线程中的操作,相对于自身,都是有序的,Java 内存模型会限制编译器重排序和处理器重排序。意思就会说 volatile 内存语义单个线程中是串行的语义。
  • 原子性: 多线程操作中,非复合操作单个 volatile 的读写是具有原子性的。

可见性

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

无 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,在某些解决问题上能获取更佳的性能。

个人博客: https://ytao.top

关注公众号 【ytao】,更多原创好文

volatile 手摸手带你解析的更多相关文章

  1. 【转】手摸手,带你用vue撸后台 系列三(实战篇)

    前言 在前面两篇文章中已经把基础工作环境构建完成,也已经把后台核心的登录和权限完成了,现在手摸手,一起进入实操. Element 去年十月份开始用vue做管理后台的时候毫不犹豫的就选择了Elemen, ...

  2. 手摸手带你理解Vue的Watch原理

    前言 watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用.在面试时,也是必问知识点,一般会用作和 computed 进行比较. 那么本文就来带大家从源码理解 ...

  3. 【手摸手,带你搭建前后端分离商城系统】03 整合Spring Security token 实现方案,完成主业务登录

    [手摸手,带你搭建前后端分离商城系统]03 整合Spring Security token 实现方案,完成主业务登录 上节里面,我们已经将基本的前端 VUE + Element UI 整合到了一起.并 ...

  4. 【转】手摸手,带你用vue撸后台 系列二(登录权限篇)

    前言 拖更有点严重,过了半个月才写了第二篇教程.无奈自己是一个业务猿,每天被我司的产品虐的死去活来,之前又病了一下休息了几天,大家见谅. 进入正题,做后台项目区别于做其它的项目,权限验证与安全性是非常 ...

  5. 【转】手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板)

    前言 做这个 vueAdmin-template 的主要原因是: vue-element-admin 这个项目的初衷是一个vue的管理后台集成方案,把平时用到的一些组件或者经验分享给大家,同时它也在不 ...

  6. 【转】手摸手,带你用vue撸后台 系列一

    前言 说好的教程终于来了,第一篇文章主要来说一说在开始写业务代码前的一些准备工作吧,但这里不会教你webpack的基础配置,热更新怎么做,webpack速度优化等等,有需求的请自行google. 目录 ...

  7. 原创 | 手摸手带您学会 Elasticsearch 单机、集群、插件安装(图文教程)

    欢迎关注笔者的公众号: 小哈学Java, 每日推送 Java 领域干货文章,关注即免费无套路附送 100G 海量学习.面试资源哟!! 个人网站: https://www.exception.site/ ...

  8. 浅谈Java中的Condition条件队列,手摸手带你实现一个阻塞队列!

    条件队列是什么?可能很多人和我一样答不出来,不过今天终于搞清楚了! 什么是条件队列 条件队列:当某个线程调用了wait方法,或者通过Condition对象调用了await相关方法,线程就会进入阻塞状态 ...

  9. 手摸手带你理解Vue的Computed原理

    前言 computed 在 Vue 中是很常用的属性配置,它能够随着依赖属性的变化而变化,为我们带来很大便利.那么本文就来带大家全面理解 computed 的内部原理以及工作流程. 在这之前,希望你能 ...

  10. YApi——手摸手,带你在Win10环境下安装YApi可视化接口管理平台

    手摸手,带你在Win10环境下安装YApi可视化接口管理平台 YApi YApi 是高效.易用.功能强大的 api 管理平台,旨在为开发.产品.测试人员提供更优雅的接口管理服务.可以帮助开发者轻松创建 ...

随机推荐

  1. CHI 2015大会:着眼于更加个性化的人机交互

    2015大会:着眼于更加个性化的人机交互" title="CHI 2015大会:着眼于更加个性化的人机交互"> 本周,人机交互领域的顶级盛会--2015年ACM C ...

  2. ZooKeeper源码阅读——client(二)

    原创技术文章,转载请注明:转自http://newliferen.github.io/ 如何连接ZooKeeper集群   要想了解ZooKeeper客户端实现原理,首先需要关注一下客户端的使用方式, ...

  3. Ionic3学习笔记(八)使iOS端、Android端 Navbar 透明化

    本文为原创文章,转载请标明出处 若要使iOS端 Navbar 透明,只需要给 ion-navbar 添加 transparent 属性,但是Android端依旧会有一条border在,所以再给 ion ...

  4. HTTP 协议的基本知识,包括请求流程、请求方法等

    一.什么是http协议? HTTP是Hyper Text Transfer Protocol(超文本传输协议)的缩写.它的发展是万维网协会(World Wide Web Consortium)和Int ...

  5. 2020 将至,Tester 你过得还好么?

    "昏天黑地地执行用例.跟踪 bug.与开发和产品争吵.工作被压在产品发布的最后阶段,因而要背负整个团队的压力,在 retro meeting 时承受着疯狂 diss......" ...

  6. 【Hardware】i386、x86和x64的故事

    (1)x86的由来 x86架构首度出现在1978年推出的Intel 8086中央处理器,它是从Intel 8008处理器中发展而来的,而8008则是发展自Intel 4004的.在8086之后,Int ...

  7. Simpo

    Time: 2017-01-16 - Download Github: https://github.com/KeliCheng/Simpo一款快速发布文字和图片到社交网站的macOS菜单栏App,目 ...

  8. django之学习前的准备

    一.配置环境 Windows 10操作系统 Python安装配置教程参考:https://www.cnblogs.com/huangbiquan/p/7784533.html 安装Python虚拟环境 ...

  9. 项目页面集成ckeditor富文本编辑器

    步骤一.引入ckeditor.js (注:本实例以ThinkPHP3.2框架为载体,不熟悉ThinkPHP的朋友请自行补习,ckeditor文件代码内容也请去ckeditor官网自行下载) 作为程序员 ...

  10. php -v 找不到命令

    [root@localhost htdocs]# php -v -bash: php: command not found export PATH=$PATH:/usr/local/php7/bin ...