Java内存可见性volatile
概述
JMM规范指出,每一个线程都有自己的工作内存(working memory),当变量的值发生变化时,先更新自己的工作内存,然后再拷贝到主存(main memory),这样其他线程就能读取到更新后的值了。
注意:工作内存和主存是JMM规范里抽象的概念,在JVM的内存模型下,可以将CPU缓存对应作线程工作内存,将JVM堆内存对应主存。

写线程更新后的值何时拷贝到主存?读线程何时从主存中获取变量的最新值?hotspotJVM中引入volatile关键字来解决这些问题,当某个变量被volatile关键字修饰后,多线程对该变量的操作都将直接在主存中进行。在CPU时钟顺序上,某个写操作执行完成后,后续的读操作一定读取的都是最新的值。
内存可见性带来的问题
如下代码片段,写线程每隔1秒递增共享变量counter,读线程是个死循环,如果读线程始终能读取到counter的最新值,那么最终的输出应该是 12345。
public class App {
// 共享变量
static int counter = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
int temp = 0;
while (true) {
if (temp != counter) {
temp = counter;
// 打印counter的值,期望打印 12345
System.out.print(counter);
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
counter++;
// 等待1秒,给读线程足够的时间读取变量counter的最新值
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 退出程序
System.exit(0);
});
thread1.start();
thread2.start();
}
}
在没有volatile的情况下,实际的输出结构如下:
1
Process finished with exit code 0
通过volatile解决问题
将共享变量用volatile关键字修饰即可,如下:
// 共享变量
static volatile int counter = 0;
再次执行程序,输出结果如下:
12345
Process finished with exit code 0
综上,volatile关键字使得各个线程对共享变量的操作变得一致。在非volatile字段上做更新操作时,无法保证其修改后的值何时从工作内存(CPU缓存)刷新到主存。对于非volatile字段的读操作也是如此,无法保证线程何时从主存中读取最新的值。
volatile无法保证线程安全性
如下代码片段,多个线程同时递增一个计数器:
public class App {
// 共享变量
static volatile int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter++;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter++;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("总和:" + counter);
}
输入结果:
总和:12374
如果volatile能保证线程安全,那么输出结果应该是20000,但上面的代码输出12374,所以说,volatile不能解决线程安全(thread)的问题。
所以,还是要通过其他手段来解决多线程安全的问题,比如synchronized。
volatile和synchronized的区别
在上述的代码示例中,我们并没有涉及到多线程竞态(race condition)的问题,核心点是“多线程情况下,对共享变量的写入如何被其他线程及时读取到”。
synchronized关键字是Java中最常用的锁机制,保证临界区(critical section)中的代码在同一个时间只能有一个线程执行,临界区中使用的变量都将直接从主存中读取,对变量的更新也会直接刷新到主存中。所以利用synchronized也能解决内存可见性问题。
代码如下:
public class App {
// 共享变量
static int counter = 0;
public static void main(String[] args) {
// 读取变量的线程
Thread readThread = new Thread(() -> {
int temp = 0;
while (true) {
synchronized (App.class) {
if (temp != counter) {
temp = counter;
// 打印counter的值,期望打印 12345
System.out.print(counter);
}
}
}
});
// 修改变量的线程
Thread writeThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
synchronized (App.class) {
counter++;
}
// 等待1秒,给读线程足够的时间读取变量counter的最新值
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.exit(0);
});
readThread.start();
writeThread.start();
}
}
运行,输入结果:
12345
Process finished with exit code 0
虽然通过synchronized也能解决内存可见性的问题,但是这个解决方案也带来了其他问题,比如性能会比较差。
总结
多线程可以提升程序的运行速度,充分利用多核CPU的算力,但多线程也是“恶魔”,会给程序员带来很多问题,比如本文中的内存可见性问题。volatile可以使变量的更新及时刷新到主存,变量的读取也是直接从主存中获取,保证了数据的内存一致性。但是volatile不是用来解决线程安全问题的,无法替代锁机制。
参考:
[1] Java Memory Model - Visibility problem, fixing with volatile variable
[2] Guide to the Volatile Keyword in Java
[3] Managing volatility
[4] Java Volatile Keyword
[5] Thread and Locks
Java内存可见性volatile的更多相关文章
- 从一个小例子引发的Java内存可见性的简单思考和猜想以及DCL单例模式中的volatile的核心作用
环境 OS Win10 CPU 4核8线程 IDE IntelliJ IDEA 2019.3 JDK 1.8 -server模式 场景 最初的代码 一个线程A根据flag的值执行死循环,另一个线程B只 ...
- 一个Java内存可见性问题的分析
如果熟悉Java并发编程的话,应该知道在多线程共享变量的情况下,存在“内存可见性问题”: 在一个线程中对某个变量进行赋值,然后在另外一个线程中读取该变量的值,读取到的可能仍然是以前的值: 这里并非说的 ...
- java内存模型-volatile
volatile 的特性 当我们声明共享变量为 volatile 后,对这个变量的读/写将会很特别.理解 volatile 特性的一个好方法是:把对 volatile 变量的单个读/写,看成是使用同一 ...
- 深入理解Java内存模型 - volatile
volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这 ...
- 深入理解Java内存模型——volatile
volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会非常特别. 理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁 ...
- Java内存可见性
如果一个线程对共享变量的修改,能够被其它线程看到,那么就能说明共享变量在线程之间是可见的.如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量.Java内存模型(Java ...
- 并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则
前言 楼主这个标题其实有一种作死的味道,为什么呢,这三个东西其实可以分开为三篇文章来写,但是,楼主认为这三个东西又都是高度相关的,应当在一个知识点中.在一次学习中去理解这些东西.才能更好的理解 Jav ...
- Java内存模型-volatile的内存语义
一 引言 听说在Java 5之前volatile关键字备受争议,所以本文也不讨论1.5版本之前的volatile.本文主要针对1.5后即JSR-133针对volatile做了强化后的了解. 二 vol ...
- java 内存可见性
java线程 -> 线程工作内存 -> 主物理内存 线程工作内存的原理是栈内是连续的小空间,寻址速度比堆快得多,将变量拷贝到栈内生成副本再操作 什么是重排序 代码指令可能并不是严格按照代码 ...
随机推荐
- Fabric 源码学习:如何实现批量管理远程服务器?
前不久,我写了一篇<Fabric教程>,简单来说,它是一个用 Python 开发的轻量级的远程系统管理工具,在远程登录服务器.执行 Shell 命令.批量管理服务器.远程部署等场景中,十分 ...
- css3 scale 缩放出现 1px 问题
问题描述 先来一段html代码 <div class="container"> <div class="parent"> <div ...
- C++ 动态链接库 dll的加载
//首先生成一个my.dll项目,在cpp中添加如下代码 //导出函数 _declspec(dllexport) int test(int a, int b) { return a + b; } // ...
- [置顶] Django-rest framework框架
出师表 先帝创业未半而中道崩殂,今天下三分,益州疲弊此诚危急存亡之秋也.然侍卫之臣不懈于内忠志之士忘身于外者盖追先帝之殊遇,欲报之于陛下也.诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻 ...
- 测试必知必会系列- Linux常用命令 - history
21篇测试必备的Linux常用命令,每天敲一篇,每次敲三遍,每月一循环,全都可记住!! https://www.cnblogs.com/poloyy/category/1672457.html 查看历 ...
- 练习div出现的小问题
一.出现图片不显示状况1.class中设定的名字不对2.在高单位后面出现了 “:“ 所以也不显示3.上一个div盒子没有写结束标签 4.在添加背景图,把height的值改成auto后不显示,填上具体数 ...
- Cinemachine中噪音的应用
两种默认产生噪音的方式 Nosie阶段的Component Component在流水线中主要通过MuteCameraState来处理对State的计算. 对于Noise类型的Component ...
- NLP(二十七)开放领域的三元组抽取的一次尝试
当我写下这篇文章的时候,我的内心是激动的,这是因为,自从去年6月份写了文章利用关系抽取构建知识图谱的一次尝试 后,我就一直在试图寻找一种在开放领域能够进行三元组抽取的办法,也有很多读者问过我这方面 ...
- Python 之装饰器
Python 的装饰器可谓是提高开发效率的一大利器.然而初学装饰器的时候感觉很难理解,因为除了 Python 之外没听说哪个语言有这种东西. 而且网上看的很多解释看似容易理解,但只能很快理解了装饰器能 ...
- hdu4107Gangster 线段树
题目链接:http://icpc.njust.edu.cn/Problem/Hdu/4107/ 题目给定一个初始值都是零的序列,操作只有一种,就是给一个区间加上一个数,但是当一个数大于等于给定的P的时 ...