前言
volatile 这个关键字可能很多朋友都听说过,它有两个重要的特性: 保证可见性 和 禁止指令重排序 。但是对于 volatile 的使用以及背后的原理我们一无所知,所以本文将带你好好了解一番。
由于 volatile 关键字是与 Java的内存模型有关的,因此在讲述 volatile 关键之前,我们先来了解一下Java 内存模型,然后介绍 volatile 关键字的使用,最后详解 volatile 关键字的原理。废话不多说,我们直接进入正文。
volatile的使用
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的 。
- 禁止进行指令重排序 。
volatile保证可见性
先看一段代码,假如线程A先执行,线程B后执行:
public class VolatitleTest { private static boolean stopRequested = false; public static void main(String[] args) throws InterruptedException { int n = 0; Thread thread1 = new Thread(() -> { int i = 0; while (!stopRequested) { i++; } },"A"); Thread thread2 = new Thread(() -> { stopRequested = true; },"B"); thread1.start(); TimeUnit.SECONDS.sleep(1); //为了演示死循环,特意sleep一秒 thread2.start(); } }
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。
下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程A在运行的时候,会将 stopRequested 变量的值拷贝一份放在自己的工作内存当中。
那么当线程B更改了 stopRequested 变量的值之后,但是还没来得及写入主存当中,线程B转去做其他事情了,那么线程A由于不知道线程B对 stopRequested 变量的更改,因此还会一直循环下去。
上述代码将 stopRequested 定义为 volatile,就变成了典型的 状态标记量 案例。
当一个变量被定义成 volatile 之后,它将具备以下特性: 保证此变量对所有线程的可见性,这里的“ 可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。具体而言就是说,volatile 关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。
普通变量与 volatile 变量的区别是:volatile 的特殊规则保证了新值能立即同步到主内存,以及每个线程在每次使用 volatile 变量前都立即从主内存刷新。因此我们可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
在本例中,线程B更改了 stopRequested 变量的值之后,新值会被立即回写到主存中,线程A再次读取 stopRequested 变量时要去主存读取。
关于 volatile 变量的可见性,经常会被开发人员误解,他们会误以为下面的描述是正确的:“ volatile 变量对所有线程是立即可见的,对 volatile 变量所有的写操作都能立刻反映到其他线程之中。换句话说,volatile 变量在各个线程中是一致的,所以基于 volatile 变量的运算在并发下是线程安全的”。这句话的论据部分并没有错,但是由其论据并不能得出“ 基于 volatile 变量的运算在并发下是线程安全的”这样的结论。Java 里面的运算操作符并非原子操作,这导致 volatile 变量的运算在并发下一样是不安全的。
volatile无法保证原子性
在JMM 一文中提到 volatile 不能保证原子性,接下来我们通过案例进行分析。
public class VolatileAddNum { static volatile int count = 0; public static void main(String[] args) { VolatileAddNum obj = new VolatileAddNum(); Thread t1 = new Thread(() -> { obj.add(); },"A"); Thread t2 =new Thread(() -> { obj.add(); },"B"); t1.start(); t2.start(); try { t1.join(); t2.join(); System.out.println("main线程输入结果为==>" + count); } catch (InterruptedException e) { e.printStackTrace(); } } public void add() { for (int i = 0; i < 100000; i++) { count++; } } }
上面这段代码做的事情很简单,开了 2 个线程对同一个共享整型变量分别执行十万次加1操作,我们期望最后打印出来 count 的值为200000,但事与