写点什么

Java Core「3」volatile 关键字

作者:Samson
  • 2022 年 5 月 23 日
  • 本文字数:1919 字

    阅读完需:约 6 分钟

Java Core「3」volatile 关键字

当我们提到 volatile 关键字的作用时,想到的是可见性、原子性、禁止重排序。

01-可见性

可见性问题指一个线程修改了共享变量的值,而另外一个线程却看不到。造成这个问题的原因是线程中存在一个高速缓存区(working memory)。


我们从 Java Memory Model(JMM)和硬件角度分析下可见性出现的原因。



图 1.JMM 与 硬件架构之间的关系(右图来自于 jenkov.com


线程中的 working memory 对应的是计算机硬件中的 CPU cache memory,用来解决内存与 CPU 之间访问速度的差异,提高运算速度。


在图 1.的基础上,我们举例说明为什么会出现可见性问题?假设主存中存在一个变量obj.count,它的当前值为 1。


  • 线程 A 在访问该变量时,会将其载入到自己的 working memory。随后,如果修改该变量的值,也并不会立刻写回到主存中(写回时机由操作系统控制)。

  • 假设线程 A 将obj.count的值修改为 2,而 CPU cache memory 又恰巧尚未写回到主存,那么线程 B 此时从主存中读取的obj.count的值就仍然是 1。


上面这个过程我们可以通过一个程序来验证下:


public class VolatileExamples {    private int a = 1;    private int b = 2;
public void change() { this.a = 3; this.b = a; }
public void lookup() { System.out.printf("a = %s, b= %s%n", a, b); }
public static void main(String[] args) { while (true) { final VolatileExamples example = new VolatileExamples(); new Thread() { @Override public void run() { try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException ignored) {} example.change(); } }.start();
new Thread() { @Override public void run() { try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException ignored) {} example.lookup(); } }.start(); } }}
复制代码


运行足够时间后,输出中会有如下的结果:


...a = 1, b= 3        // 这里a = 3, b= 3a = 1, b= 2a = 1, b= 2...
复制代码


了解到可见性问题产生的原因,我们来看一下 volitale 是如何实现可见性的。volatile 指令实际上是通过 JVM lock指令添加内存屏障实现可见性的[1]。


lock 前缀的指令在多核处理器下会引发两件事情:

  1. 将当前处理器缓存行的数据写回到系统内存。

  2. 写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。


[1] Java Memory Model


[2] Threads and Locks

02-原子性

volatile 是无法保证i++操作的原子性的,因为i++是一个复核操作,包含了:1)读取 i 的值;2)对 i 执行加 1 操作;3)将 i 的值写回内存。


但是对于 double 和 long 类型的变量,是鼓励使用 volatile 修饰的。因为 JLS 中解释:


Writes and reads of volatile long and double values are always atomic.


如果不使用 volatile ,在图 1.中的主存与 working memory 之间的 read/write 和 load/store 操作都是将其当作是两个对立的 32 位变量来对待。


不过,现在 JVM 普遍都将 64 位数据的读写当作是原子操作。一般情况下,不使用 volatile 修饰 long 或 double 变量也不会出错。

03-有序性

JLS 中关于 volatile 变量有一条 happens-before 规则:


A write to a volatile field happens-before every subsequent read of that field.


从前面的章节中了解到,volatile 实现可见性是通过插入内存屏障。内存屏障还有一个其他的作用就是,禁止指令重排序。


04-volatile 的应用场景

在单例模式中,单例对象一般会被 volatile 修饰。例如:


public class Singleton {
public static volatile Singleton singleton;
private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }}
复制代码


这里使用 volatile 的主要原因是防止指令重排序。因为,singleton = new Singleton();是一个复合操作,它包括:


  • 分配内存空间

  • 初始化对象

  • 将对象地址的引用赋值给singleton


禁止指令重排序是为了避免对象在初始化之前被返回。


历史文章

Java Core「2」synchronized 关键字

Java Core「1」JUC- 线程基础

发布于: 刚刚阅读数: 4
用户头像

Samson

关注

还未添加个人签名 2019.07.22 加入

还未添加个人简介

评论

发布
暂无评论
Java Core「3」volatile 关键字_学习笔记_Samson_InfoQ写作社区