volatile 手摸手带你解析
前言
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++
是属于一个非原子操作的复合操作,所以不能保证其原子性。
使用场景
- volatile 变量最后的运算结果不依赖变量的当前值,也就是前面提到的直接赋值变量的原子操作,比如:保存数据遍历的特定条件的一个值。
- 可以进行状态标记,比如:是否初始化,是否停止等等。
总结
volatile 是一个简单又轻量级的同步机制,但在使用过程中,局限性比较大,要想使用好它,必须了解其原理及本质,所以在使用过程中遇到的问题,相比于其他同步机制来说,更容易出现问题。但使用好 volatile,在某些解决问题上能获取更佳的性能。
个人博客: https://ytao.top
关注公众号 【ytao】,更多原创好文
volatile 手摸手带你解析的更多相关文章
- 【转】手摸手,带你用vue撸后台 系列三(实战篇)
前言 在前面两篇文章中已经把基础工作环境构建完成,也已经把后台核心的登录和权限完成了,现在手摸手,一起进入实操. Element 去年十月份开始用vue做管理后台的时候毫不犹豫的就选择了Elemen, ...
- 手摸手带你理解Vue的Watch原理
前言 watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用.在面试时,也是必问知识点,一般会用作和 computed 进行比较. 那么本文就来带大家从源码理解 ...
- 【手摸手,带你搭建前后端分离商城系统】03 整合Spring Security token 实现方案,完成主业务登录
[手摸手,带你搭建前后端分离商城系统]03 整合Spring Security token 实现方案,完成主业务登录 上节里面,我们已经将基本的前端 VUE + Element UI 整合到了一起.并 ...
- 【转】手摸手,带你用vue撸后台 系列二(登录权限篇)
前言 拖更有点严重,过了半个月才写了第二篇教程.无奈自己是一个业务猿,每天被我司的产品虐的死去活来,之前又病了一下休息了几天,大家见谅. 进入正题,做后台项目区别于做其它的项目,权限验证与安全性是非常 ...
- 【转】手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板)
前言 做这个 vueAdmin-template 的主要原因是: vue-element-admin 这个项目的初衷是一个vue的管理后台集成方案,把平时用到的一些组件或者经验分享给大家,同时它也在不 ...
- 【转】手摸手,带你用vue撸后台 系列一
前言 说好的教程终于来了,第一篇文章主要来说一说在开始写业务代码前的一些准备工作吧,但这里不会教你webpack的基础配置,热更新怎么做,webpack速度优化等等,有需求的请自行google. 目录 ...
- 原创 | 手摸手带您学会 Elasticsearch 单机、集群、插件安装(图文教程)
欢迎关注笔者的公众号: 小哈学Java, 每日推送 Java 领域干货文章,关注即免费无套路附送 100G 海量学习.面试资源哟!! 个人网站: https://www.exception.site/ ...
- 浅谈Java中的Condition条件队列,手摸手带你实现一个阻塞队列!
条件队列是什么?可能很多人和我一样答不出来,不过今天终于搞清楚了! 什么是条件队列 条件队列:当某个线程调用了wait方法,或者通过Condition对象调用了await相关方法,线程就会进入阻塞状态 ...
- 手摸手带你理解Vue的Computed原理
前言 computed 在 Vue 中是很常用的属性配置,它能够随着依赖属性的变化而变化,为我们带来很大便利.那么本文就来带大家全面理解 computed 的内部原理以及工作流程. 在这之前,希望你能 ...
- YApi——手摸手,带你在Win10环境下安装YApi可视化接口管理平台
手摸手,带你在Win10环境下安装YApi可视化接口管理平台 YApi YApi 是高效.易用.功能强大的 api 管理平台,旨在为开发.产品.测试人员提供更优雅的接口管理服务.可以帮助开发者轻松创建 ...
随机推荐
- 对Vue为什么不支持IE8的解释之一
在JavaScript对象中有一个Object.defineProperties(obj, props)方法 该方法主要用来给指定对象添加自定义属性 可以接收两个参数: 第一个参数 要定义或者修改属性 ...
- 修改npm安装的全局路径和配置环境变量
我之前安装npm时全是默认安装,模块全部安装在C盘了,今天心血来潮,把路径改到了D盘,结果改完后模块都不能识别了,都提示XX模块不是内部命令,这其实是环境变量配置的问题,我都是按照网上的教程改的环境变 ...
- box-sizing: border-box
如果在元素上设置了 box-sizing: border-box; 则 padding(内边距) 和 border(边框) 也包含在 width 和 height 中
- 849. Dijkstra求最短路 I
给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值. 请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1. 输入格式 第一行包含整数n和m. 接下来m行每行包 ...
- Docker 运行容器 CentOS7 使用systemctl 启动报错 Failed to get D-Bus connection: Operation not permitted
原系统:Centos 7 Docker 版本:1.12.6 操作:安装并运行 Tomcat 问题:在创建好容器之后,并且进入系统运行启动tomcat [root@cd11558d3a22 /]# sy ...
- Mariadb 修改root密码及跳过授权方式启动数据库
默认情况下,yum方式新安装的 mariadb 的密码为空,在shell终端直接输入 mysql 就能登陆数据库. 如果是刚安装第一次使用,请使用 mysql_secure_installation ...
- 阿里云ECS开放批量创建实例接口,实现弹性资源的创建
摘要: 为了更方便的实现弹性的资源创建,方便用户一次运行多台ECS按量实例来完成应用的开发和部署,阿里云开放了ECS的批量创建实例接口RunInstances,可以单次最多创建100台实例,避免重复调 ...
- 用jQuery怎么做到前后端分离
传统的web开发模式想必大家都知道,不管是jsp.asp.php或者一些魔板引擎开发,其实道理都是一样的,都是服务端渲染,原理是:浏览器发送一个get请求,服务器对应的返回前端一个html页面,由浏览 ...
- Python 绘图 - Bokeh 柱状图小试(Stacked Bar)
背景 在 Bokeh 初探之后,学习使用它来做个图 目标 做一个柱状图,支持多个 y 数据源,即有堆叠效果的柱状图 stacked bar 实现 单数据源 简单的柱状图 参考 Handling Cat ...
- C++冒险攻略(持续更新中。。。)
C++语言程序设计 我的C++冒险之旅 绪论 计算机系统基本概念 计算机硬件 计算机程序语言 计算机解决问题是程序控制的 程序就是操作步骤 程序要使用语言来表达 机器语言 计算机能识别的是机器语言 机 ...