一文搞懂volatile的可见性原理
说volatile之前,了解JMM(Java内存模型)有助于我们理解和描述volatile关键字。JMM是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让Java程序在各种平台下都达到一致的内存访问效果。JMM也可以称之为Java线程内存模型,也描述了Java线程在工作中对数据的操作过程以及描述了线程之间的通信过程。

以上便是JMM的基本逻辑图,Java采用工作内存和主内存进行数据交互的原因可以解释为,工作内存一般为cpu的高速缓存,cpu的高速缓存就是为了解决cpu日益增长的速度与主存不匹配导致浪费计算资源,所以线程的工作内存位于cpu的高速缓存中来提高运算速度。但是多个线程在对主内存中共享变量操作时会有一个可见性问题。具体可看以下代码:
package xyz.ring2.demo.test;
public class VolatileVisibilityTest {
public static boolean flag = false;
public static void changeCondition(){
flag = true;
}
public static void main(String[] args) throws InterruptedException {
System.out.println("working and waiting for change...");
new Thread(new Runnable() {
@Override
public void run() {
while (!flag){
System.out.println("hello");
}
}
}).start();
Thread.sleep(200);
new Thread(new Runnable() {
@Override
public void run() {
changeCondition();
System.out.println("condition has changed.");
}
}).start();
Thread.sleep(200);
System.out.println("work done.");
}
}
在该程序中有一个共享变量flag,第一个线程运行时等待别的线程改变flag的值使其跳出循环,第二个线程是去改变共享变量flag的值。在我们看来,第一个线程只需要等待第二个线程改变了flag的即可跳出循环。以下是程序运行结果:

可以看到当“work done”打印出来时程序还没有停止,此时我们可以得出结论。两个线程对共享变量的操作是互相不可见的。此时我们很自然的想到了通过加synchronizedJava内置锁来解决。
通过在while循环外添加synchronized(this)同步块确实能解决这种问题,但是在这种仅仅只需要保证一个共享变量可见的情况下采用synchronized锁来保证同步代价太大,此时我们应该采用Java所
提供的volatile关键字来保证变量的可见性。使用上通过在flag前加上volatile关键字即可。
public static volatile boolean flag = false;
以下是运行结果:

正常的使程序结束了,线程一成功的感知到了线程二对flag变量的改变。
那么volatile关键字使如何保证多线程下共享变量线程间可见的呢?
首先我们来了解以下JMM中的数据原子操作:
- read(读取):从主内存读取数据
- load(载入):将主内存读取到的数据写入工作内存
- use(使用):从工作内存读取数据来计算
- assign(赋值):将计算好的值从新赋值到工作内存中
- store(存储):将工作内存数据写入到主内存
- write(写入):将store过去的变量值赋值给主内存中的变量
- lock(锁定):将主内存变量加锁,标识为线程独占状态
- unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
JVM通过以上原子操作来处理主内存和工作内存中的数据交互。那么volatile到底是如何保证的呢?
Java中的volatile关键字是通过调用C语言实现的,而在更底层的实现上,即汇编语言的层面上,用volatile关键字修饰后的变量在操作时,最终解析的汇编指令会在指令前加上lock前缀指令
来保证工作内存中读取到的数据是主内存中最新的数据。具体的实现原理是在硬件层面上通过:MESI缓存一致性协议:多个cpu从主内存读取数据到高速缓存中,如果其中一个cpu修改了数据
,会通过总线立即回写到主内存中,其他cpu会通过总线嗅探机制感知到缓存中数据的变化并将工作内存中的数据失效,再去读取主内存中的数据。
IA32架构软件开发者手册对lock前缀指令的解释:
1.会将当前处理器缓存行的数据立即回写到系统内存中,
2.这个写回内存的操作会引起其他cpu里缓存了该内存地址的数据失效(MESI协议)

现在我们知道了volatile可以保证变量的可见性,我们还应该知道volatile不可以保证原子性:
volatile无法保证原子性:如:两个线程同时read主内存中相同的值,load到工作内存中,两个线程的cpu又同时use了count值并进行了计算且assign回工作内存,但其中一个线程通过总线store回主内存的
速度更快,于是由于(总线)MESI缓存一致性协议下的cpu总线嗅探机制就会使得另一个线程工作内存中的变量副本失效,导致之前的操作结果丢失(可以结合图片理解)。
并发编程的三大特性:可见性,原子性,有序性。那么volatile对有序性又是怎样的呢。。。这涉及到happens-before规则,volatile关键字可以体统屏障保护,使得编译器和jvm对变量操作的重排序失效。
可以读取我的另一篇文章:单例模式值双检索 来理解一下重排序所带来的问题。
一文搞懂volatile的可见性原理的更多相关文章
- 一文搞懂一致性hash的原理和实现
在 go-zero 的分布式缓存系统分享里,Kevin 重点讲到过一致性hash的原理和分布式缓存中的实践.本文来详细讲讲一致性hash的原理和在 go-zero 中的实现. 以存储为例,在整个微服务 ...
- 一文搞懂 Prometheus 的直方图
原文链接:一文搞懂 Prometheus 的直方图 Prometheus 中提供了四种指标类型(参考:Prometheus 的指标类型),其中直方图(Histogram)和摘要(Summary)是最复 ...
- Web端即时通讯基础知识补课:一文搞懂跨域的所有问题!
本文原作者: Wizey,作者博客:http://wenshixin.gitee.io,即时通讯网收录时有改动,感谢原作者的无私分享. 1.引言 典型的Web端即时通讯技术应用场景,主要有以下两种形式 ...
- 一文搞懂所有Java集合面试题
Java集合 刚刚经历过秋招,看了大量的面经,顺便将常见的Java集合常考知识点总结了一下,并根据被问到的频率大致做了一个标注.一颗星表示知识点需要了解,被问到的频率不高,面试时起码能说个差不多.两颗 ...
- 一文搞懂指标采集利器 Telegraf
作者| 姜闻名 来源|尔达 Erda 公众号 导读:为了让大家更好的了解 MSP 中 APM 系统的设计实现,我们决定编写一个<详聊微服务观测>系列文章,深入 APM 系统的产品.架构 ...
- 一文搞懂RAM、ROM、SDRAM、DRAM、DDR、flash等存储介质
一文搞懂RAM.ROM.SDRAM.DRAM.DDR.flash等存储介质 存储介质基本分类:ROM和RAM RAM:随机访问存储器(Random Access Memory),易失性.是与CPU直接 ...
- 基础篇|一文搞懂RNN(循环神经网络)
基础篇|一文搞懂RNN(循环神经网络) https://mp.weixin.qq.com/s/va1gmavl2ZESgnM7biORQg 神经网络基础 神经网络可以当做是能够拟合任意函数的黑盒子,只 ...
- 一文搞懂vim复制粘贴
转载自本人独立博客https://liushiming.cn/2020/01/18/copy-and-paste-in-vim/ 概述 复制粘贴是文本编辑最常用的功能,但是在vim中复制粘贴还是有点麻 ...
- 三文搞懂学会Docker容器技术(中)
接着上面一篇:三文搞懂学会Docker容器技术(上) 6,Docker容器 6.1 创建并启动容器 docker run [OPTIONS] IMAGE [COMMAND] [ARG...] --na ...
随机推荐
- Linux操作系统进入单用户模式的方法
单用户模式的作用 在使用Linux的过程中,维护人员经常会碰到一些问题,就是在拥有root账号权限和密码的用户中,总是会出现忘记root密码的情况. 遇到这种情况,一般情况下,维护人员就会通过最常用的 ...
- Codeforces Round #628 (Div. 2) 题解
人闲桂花落,夜静春山空. 月出惊山鸟,时鸣春涧中.--王维 A. EhAb AnD gCd You are given a positive integer x. Find any such 2 po ...
- js 实现动画功能,完整解析插件版 可更改配置参数[animate.js]
前言: 本人纯小白一个,有很多地方理解的没有各位大牛那么透彻,如有错误,请各位大牛指出斧正!小弟感激不尽. 本篇文章为您分析一下原生JS写一个运动插件 基本功能: 补充 ...
- Linux系统应用管理:增加普通用户(密码管理等)
1. 查看当前Linux系统的版本.内核等信息 [root@oldboy ~]# cat /etc/redhat-release CentOS release 6.7 (Final) . # 系统版本 ...
- 【Linux常见命令】tree命令
tree - list contents of directories in a tree-like format. tree命令用于以树状图列出目录的内容. 执行tree指令,它会列出指定目录下的所 ...
- CSS样式2
1.css重用 <style> .cl{ ... } .c2{ ... } .c{ ... } </style> <div class='c c2'></di ...
- ROC-RK3328-CC开源主板运行LibreELEC系统
LibreELEC是运行Kodi媒体中心的轻量级操作系统,基于Linux内核发行,系统为适配Kodi运行环境,做了许多优化和精简,运行速度快,操作简单.是一款很优秀的多功能播放器操作系统. ROC-R ...
- Naigos install pnp4nagios 绘图插件
原文地址:http://www.cnblogs.com/caoguo/p/5022230.html vim /etc/httpd/conf/httpd.conf <Directory " ...
- MySQL JDBC Driver 8.0+设置服务器时区
遇到一个问题,线下环境测试数据的查询完全没有问题,但是线上环境却没法查询出数据,并且从mybatis输出的日志来看,查询参数也没有问题,数据库中数据也是存在的,查询参数类型是java.util.Dat ...
- 数据库——数据库SQL语句大全,最常用的SQL语句
检索数据 检索单个列: SELECT pname FROM product 检索多个列: SELECT pname,market_price,is_hot FROM product 检索所有列: SE ...