Java并发专题(三)深入理解volatile关键字
前言
上一章节简单介绍了线程安全以及最基础的保证线程安全的方法,建议大家手敲代码去体会。这一章会提到volatile关键字,虽然看起来很简单,但是想彻底搞清楚需要具备JMM、CPU缓存模型的知识。不要小看这个关键字,它在整个并发包(concurrent包)使用的非常广泛,掌握volatile关键字是非常重要的。
如果你是一个急性子,请看下面3点就行:
- 保证了多线程读取变量的可见性,一个线程修改volatile修饰的变量,另外一个线程会立即读取到新的值
- 禁止指令重排序
- volatile关键字不会像synchronized关键字一样造成线程阻塞,也就是说无锁
1.1 初识volatile关键字
我先写一个例子,在主线程启动2个线程,一个线程负责写,一个线程负责读,读写的该变量就是共享变量,那么结果是你想的那样吗?
/**
* volatile第一个演示Demo类。
*
* @author GrimMjx
*/
public class VolatileDemo1 { //i的初始值为0
public static int i;
//i的最大值为3
public static int MAX = 3; public static void main(String[] args) {
//读线程
new Thread(() -> {
int index = i;
while (index < MAX) {
if (i != index) {
System.out.println("i = " + i);
index = i;
}
}
}).start(); //写线程
new Thread(() -> {
int index = i;
while (index < MAX) {
System.out.println("new i = " + ++i);
index = i;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
我贴上一份我执行的结果:
new i = 1
i = 1
new i = 2
new i = 3
程序不会停,需要手动停。那么问题来了,为什么明明写入了i了,读线程还是无法读到新的i的值呢?读线程压根没有感知到i的变化!只要我们把变量i的定义改变一下,那么就可以解决这个问题。
public volatile static int i;
改好之后再试一下,确实如我们预料的执行了且读线程也不会死循环。只是一个关键字的差别,会发生很大的不同。那接下来请带着疑问去学习。
new i = 1
i = 1
new i = 2
i = 2
new i = 3
i = 3
1.2 机器CPU
所有的指令都是CPU寄存器完成的,CPU指令的过程中涉及到数据的写入和读取。CPU能访问的所有数据只能是RAM(计算机内存)。虽然CPU频率不断提升,但是RAM访问速度没有很大突破,因此CPU处理速度和内存的访问速度差距巨大,一次主内存的访问通常在几十到几百个甚至上千个时钟周期,一次L1高速缓存的读写需2个左右时钟周期,一次L2高速缓存的读写需要几十个时钟周期。
1.2.1 CPU Cache模型
可以直观看到两边的速度严重不对等,于是有了在CPU和主内存之间增加缓存,最靠近CPU的缓存成为L1高速缓存,其次是L2,L3和主内存。我们先看一张各级缓存之间响应时间差距,以及内存到底有多慢。

接下来我们看一下CPU Cache模型:

1.2.2 CPU缓存一致性
缓存大大提高了访问速度,但是同时也引入了缓存不一致的问题,比如i++;这个操作。具体的过程如下:
- 读取主内存的i到CPU Cache中
- 将i+1
- 将结果写回CPU Cache
- 将数据刷新回主内存
i++在单线程完全不会有问题,但是多线程的时候就会有问题,每个线程都有自己的工作内存(本地内存),如果在2个线程都执行i++;操作,A线程和B线程此时的工作内存中的i都是0,加1之后都变成1。最后经过计算再写入主内存可能结果还是1。这就是缓存不一致问题。如果想要解决这个问题,主流方法是通过缓存一致性协议(MESI协议)。这个协议的大致思想就是如果当CPU在操作Cache中的数据时,其他Cache也存在一份副本,那么会进行如下操作:
- 读取操作,不做任何处理,只是将Cache中的数据读取到寄存器
- 写入操作,发出信号通知其他CPU将其变量中的Cache line置为无效状态,其他CPU在进行该变量的读取时候不得不到主内存中再次获取

1.3 Java内存模型
JMM指定了JVM和计算机RAM如何进行工作的,同时也决定了一个线程对共享变量的写入何时对其他线程可见,有以下几个要点:
- 每个线程都有自己的工作内存,也成为本地内存
- 工作线程只存储线程对共享变量的副本
- 线程不能直接操作主内存,只能操作工作内存
- 工作内存和JMM都是一个抽象的概念,实际并不存在,覆盖了寄存器,编译器优化等等

主内存和工作内存的关系和CPU与CPUCache之间的关系是非常类似的,所以通过图示和讲解,我们发现理解volatile关键字会比synchronized关键字困难很多,需要了解机器CPU还有JMM。volatile在JDK5以后的concurrent包运用非常广泛,所以掌握volatile关键字很重要。
1.4 深入理解volatile关键字
说到并发,有三大特性,原子性,有序性和可见性,那我们从三个方面来介绍
1.4.1 原子性
volatile不具备原子性
原子性的意思就是在一次操作中,所有的操作全部执行或者都不执行,就像名字一样是不可分割的。Java中,对变量的读取和赋值操作都是源自的,但是多个原子性的操作在一起,不一定是个原子操作。JMM只保证了基本的读取和赋值的原子性,其他的均不保证。说回volatile,如果在上一章节的UnsafeAdd的例子,用volatile修饰变量i,是否可以解决多线程并发问题呢,结果是不可以的,可以自己去试试。
就像是i++操作,他其实包含了3步骤
1.从主内存获取i,缓存到工作内存
2.在工作内存中进行+1
3.刷回主内存。
这也就是刚刚说的多个原子性的操作在一起,不一定是个原子操作
Java中想要保证原子性,需要使用synchronized关键字,concurrent包的lock,原子封装类和循环CAS的方式(原子变量是一种更好的volatile,后面会讲到)
1.4.2 可见性
volatile具备可见性
读取:当一个变量被volatile关键字修饰时,当其他线程对此变量进行了修改,则会迫使其他线程的工作内存中的该变量失效,所以必须从主内存重新获取。(使用的是机器指令lock)
写入:当然是先修改工作内存,修改后立即将其刷新到主内存中。
Java中volatile,synchronized关键字和显式锁lock都保证可见性
1.4.3 有序性
volatile具备有序性
首先volatile遵循happens-before原则:对一个变量的写操作要早于这个变量之后的读操作。也就是说,如果一个变量使用volatile关键字修饰,一个线程对这个变量进行写操作,另外一个线程对这个变量进行读操作。那么写操作肯定要先发生于读操作。
volatile对顺序性非常霸道,直接禁止JVM和处理器进行指令重排序,但是对于volatile前后无依赖关系的执行可以随便排序。
Java中volatile,synchronized关键字和显式锁lock都保证有序性
1.5 volatile的正确打开姿势
- 确保它们所引用状态的可见性
- 标识一些重要的程序生命周期事件发生(init,destroy)
- 确保只有一个线程更新变量的值
- 不会用就不要用:)
Java并发专题(三)深入理解volatile关键字的更多相关文章
- Java并发编程(六)volatile关键字解析
由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识. 一.内存模型的相关概念 Java内存模型规定所有的变量都是存在 ...
- 【Java并发编程】6、volatile关键字解析&内存模型&并发编程中三概念
volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以 ...
- Java并发编程:JMM和volatile关键字
转载请标明出处: http://blog.csdn.net/forezp/article/details/77580491 本文出自方志朋的博客 Java内存模型 随着计算机的CPU的飞速发展,CPU ...
- Java并发编程——为什么要用volatile关键字
首发地址 https://blog.leapmie.com/archives/66ba646f/ 日常编程中出现 volatile 关键字的频率并不高,大家可能对 volatile 关键字比较陌生,再 ...
- Java并发机制(3)--volatile关键字与内存模型
Java并发编程:volatile关键字解析及内存模型 个人整理自:博客园-海子-http://www.cnblogs.com/dolphin0520/p/3920373.html 1.线程内存模型: ...
- 【Java并发编程】从CPU缓存模型到JMM来理解volatile关键字
目录 并发编程三大特性 原子性 可见性 有序性 CPU缓存模型是什么 高速缓存为何出现? 缓存一致性问题 如何解决缓存不一致 JMM内存模型是什么 JMM的规定 Java对三大特性的保证 原子性 可见 ...
- Java并发(3)- 聊聊Volatile
引言 谈到volatile关键字,大多数开发者都有一定了解,可以说是开发者非常熟悉,深入之后又非常陌生的一个关键字.相当于轻量的synchronized,也叫轻量级锁,与synchronized相比性 ...
- 深入理解volatile关键字
Java内存模型 想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的. Java内存模型规定了所有的变量都存储在主内存中.每条线程中还有自己的工作内存,线程的工作内存 ...
- 对精致码农大佬的 [理解 volatile 关键字] 文章结论的思考和寻找真相
一:背景 1. 讲故事 昨天在园里的编辑头条看到 精致码农大佬 写的一篇题为:[C#.NET 拾遗补漏]10:理解 volatile 关键字 (https://www.cnblogs.com/will ...
随机推荐
- BZOJ_2393_Cirno的完美算数教室&&BZOJ_1853_[Scoi2010]幸运数字 _深搜+容斥原理
BZOJ_2393_Cirno的完美算数教室&&BZOJ_1853_[Scoi2010]幸运数字 _深搜+容斥原理 题意: ~Cirno发现了一种baka数,这种数呢~只含有2和⑨两种 ...
- BZOJ_3589_动态树_容斥原理+树链剖分
BZOJ_3589_动态树_容斥原理+树链剖分 题意: 维护一棵树,支持1.子树内点权加上一个数 2.给出k条链,求路径上的点权和(重复的计算一次) (k<=5) 分析: 可以用树剖+线段树解 ...
- gitlab-ci-runner安装
前言 什么是CI/CD? CI (Continuous Integration) 持续集成, CD (Continuous Delivery) 持续部署 个人理解 本地开发代码, 提交远程仓库 仓库接 ...
- 我和Python的Py交易》》》》》》 浮点数的身世字谜
什么是浮点数? 在数据类型中写道,浮点数是带小数点的小数,这个概念是不准确的:浮点数是除了无限不循环小数之外的小数,也就是可以用分数表示的带小数点的数. 好了,浮点数就这些内容,讲完了,各回各家,各找 ...
- appium 提示报错“TypeError: 'unicode' object is not callable”的解决方式!
这里提到的这个报错,是小错误且容易经常会犯,有时需要特别注意使用. 目的要求结果:根据某个元素的id值获取到对应id的text值,并且将获取的text值与本身存在的text值做比较,查看text值是否 ...
- 记录Ocelot + SignalR 多服务端测试
前言 分两个项目,一个Gatway,一个SignalR 贴代码 1.Gatway 1.引用Ocelot 2.添加一点点代码 Startup.cs 3.简单配置ocelot ocelot.json { ...
- WebGL展示3D房屋内景
原文地址:WebGL展示3D房屋内景 由于生活和工作上的原因,从年前开始一直到处奔波,没有太多的时间去关注和学习WebGL图形学相关的技术, 不过陆陆续续都有学习使用blender进行3D建模 ...
- shell-输入与输出<echo, read, cat, tee >
1. echo echo命令可以显示文本行或变量,或者吧字符串输入到文件. 用法:echo [option] string #[option]可选:-e ==>解析转移字符,(常用的\n & ...
- 微服务(入门一):netcore安装部署consul
环境准备 vs开发环境:vs2017 consul版本: 1.4.4 netcore版本:2.1 安裝Consul 1.从官网下载consul到本地,选择系统对应的版本进行下载到本地,下载地址:h ...
- <pre>标签:\r\n换行符的保留
mysql数据读库的\r\n换行符处理 这个问题是我在采集数据时发现的,采集网页的数据,大概有6千多条,采集的内容保留了最原始的\r\n和\n换行字符,但在mysql管理工具中(phpmyadmin和 ...