Erlo

使用 volatile 关键字保证变量可见性和禁止指令重排序

时间:2020-10-17   阅读:7次   来源:博客园
页面报错
点赞


volatile 概述

volatile 是 Java 提供的一种轻量级的同步机制。相比于传统的 synchronize,虽然 volatile 能实现的同步性要差一些,但开销更低,因为它不会引起频繁的线程上下文切换和调度。


为了更好的理解 volatile 的作用,首先要了解一下 Java 内存模型与并发编程三要素


Java 内存模型

Java 虚拟机规范中定义了 Java 内存模型(Java Memory Model,JMM),用于屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的并发效果。

JMM 规定了 Java 虚拟机与计算机内存如何协同工作:一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。注意这里的变量是指实例字段,静态字段,构成数组对象的元素,不包括局部变量和方法参数(因为这是线程私有的),可以简单理解为主内存是 Java 虚拟机内存区域中的堆,局部变量和方法参数是在虚拟机栈中定义的。如果堆中的变量在多线程中都被使用,就涉及到了堆和不同虚拟机栈中变量的值的一致性问题了。

Java 内存模型中涉及到的概念有:

Java 虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。


并发编程三要素

在并发编程中,以下三要素是我们经常需要考虑的:


volatile 保证变量可见性

假如有 A、B 两个线程,主内存有变量 i = 0,A 线程将主内存中的 i 拷贝一份到自己的工作内存,并修改为 i = 1,但并没有立即写回到主内存,什么时候写回主存是不确定的。此时 B 线程也将主内存中的 i 拷贝一份到自己的工作内存,而主内存中的 i 还是 0,并不是预想中的 1,这就可能导致一些问题。

volatile 的一个重要作用就是实现了变量可见性。当一个共享变量被 volatile 修饰,它会保证修改的值会立即更新到主存,当其他线程需要读取时,它会去内存中读取新值。


volatile 不保证原子性

假如有 A、B 两个线程,同时对初始值为 0 的变量 i 做加 1 操作,我们希望最终的结果是 i = 2,但有可能并非如此,假设:

出现上述问题的原因是 i++ 并不是一个原子性的操作,Java 内存模型只保证了基本读取和赋值是原子性操作。不同线程之间的操作交互执行,可能会出现漏洞。所以使用 volatile 必须具备以下两个条件:

上述两个条件其实就是要保证操作是原子性的。如果希望实现更大范围操作的原子性,可以通过 synchronized 和 Lock 来实现。synchronized 和 Lock 能保证任一时刻只有一个线程执行该代码块,自然就不存在原子性问题。

volatile 禁止指令重排序

所谓指令重排序,是指计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排。指令重排必须保证最终执行结果和代码顺序执行结果一致。

public void mySort() {
	int x = 11;	// 1
	int y = 12; // 2
	x = x + 5;  // 3
	y = x * x;  // 4
}

正常的执行顺序是 1、2、3、4,如果发生指令重排,就有可能会是 2、1、3、4,或者是 1、3、2、4 等等,但不会出现 4、3、2、1 这样的情况,因为处理器在进行重排时,必须考虑到指令之间的数据依赖性。

在单线程下指令重排是没有问题的,但如果是多线程就不一定了,假设主存中有 a,b,x,y 四个变量(保证了可见性),初始值都是 0,有 A、B 两个线程,它们各自顺序执行时操作如下:

无论两个线程之间的操作如何交错,最终结果都是 x = 0,y = 0(不考虑线程 A 走完再到线程 B 的情况,因为这样就和单线程没有差异了)。可如果发生了指令重排,此时它们各自的操作执行顺序可能变为:

这样造成的结果就是 x = 2,y = 1,和上面的不一致了。因此为了防止这种情况,volatile 规定禁止指令重排,从而保证数据的一致性。


相关推荐

提交留言

评论留言

还没有评论留言,赶紧来抢楼吧~~

吐槽小黑屋()

* 这里是“吐槽小黑屋”,所有人可看,只保留当天信息。

  • Erlo吐槽

    Erlo.vip2020-10-22 00:14:38Hello、欢迎使用吐槽小黑屋,这就是个吐槽的地方。
  • 返回顶部