alexpdh's blog

理解 JVM:Java 内存模型(二)——volatile

概述

java 内存模型的核心是围绕着在并发过程中如何处理原子性、可见性、有序性这3个特性来展开的,它们是多线程编程的核心。

  • 原子性(Atomicity):是指一个操作是不可中断的,即使是多个线程同时执行的情况下,一个操作一旦开始,就不会被其它线程干扰。对于基本类型的读写操作基本都具有原子性的(在32位操作系统中 long 和 double 类型数据的读写不是原子性的,因为它们有64位)。
  • 可见性(Visibility):是指在多线程环境下,当一个线程修改了某一个共享变量的值,其它线程能够立刻知道这个修改。
  • 有序性(Ordering):是指程序的执行顺序是按照代码的先后顺序执行的;对于这句话如果在单线程中所有的操作都是有序的,但是在多线程环境下,一个线程的操作相对于另外一个线程的操作是无序的。

先行发生原则(happens-before)

先行发生是 Java 内存模型中定义的两个操作之间的偏序关系,这些先行关系无需任何同步器的协助就已经存在,可以在编码中直接使用。Java 内存模型对这些关系作了如下规则:

  • 程序次序规则(Program Order Rule):在一个线程内,程序安装代码顺序执行。即所谓的“线程内表现为串行的语义(Within-Thread As-If-Serial Semantics)”。
  • 管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
  • volatile 变量规则(Volatile Variable Rule):对于一个 volatile 变量的写操作先行发生于此线程的每一个动作。
  • 线程启动规则(Thread Start Rule):Thread 对象的 start() 方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中的所有操作先行发生于对此线程的终止检测。
  • 线程中断规则(Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成先行发生于它的 finalizer() 方法。
  • 传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于 操作 C,可以推断出 操作 A 先行发生于操作 C。

关键字 volatile

volatile 修饰的变量保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。因为当对普通变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。而volatile修饰的变量,JVM保证了每次读变量都从内存中读,跳过CPU cache这一步。volatile修饰的变量禁止进行指令重排序,所以能在一定程度上保证有序性。只能保证该变量所在的语句还是原来的位置,并不能保证该语句之前或之后的语句是否被打乱。

volatile 的特性

  1. 当一个变量被 volatile 修饰之后,能保证此变量对所有线程的可见性,即当一个线程修改了这个变量的值,新值对其它线程是立即可见的。
  2. 被 volatile 修饰的变量通过查询内存屏障来禁止指令重排序,所以能在一定程度上保证有序性。
  3. 对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性。 例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.pdh.test;
/**
* volatile 复合操作测试
*
* @author pengdh
* @date 2017/11/12
*/
public class VolatileDemo {
// 申明 volatile 变量
private static volatile int i = 0;
// 计数
private static final int COUNT = 10;
/**
* 对 volatile 变量复合运算
*/
private static void increase() {
i++;
}
public static void main(String[] args) {
// 启动 10 个线程分别对 i 进行 10000 次计算,正常情况结果为 100000
for (int j = 0; j < COUNT; j++) {
new Thread(() -> {
for (int k = 0; k < 10000; k++) {
increase();
}
}).start();
}
// 等待所有累加线程全部执行结束,这里不同 ide 中线程存活数不一样,
// 该示例代码在 idea 中运行,会多出一个 Monitor Ctrl-Break 线程,故条件是 > 2,
// 如果在 Eclipse 中条件应为 > 1
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}

如上代码正常运行结果应该打印100000,但实际结果基本得不到正确结果。这说明了 volatile 变量的复合运算并不具有原子性,想要得到正确结果,需要对 volatile 变量运算操作加锁或者加上同步块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.pdh.test;
/**
* volatile 复合操作测试
*
* @author pengdh
* @date 2017/11/12
*/
public class VolatileDemo {
// 申明 volatile 变量
private static volatile int i = 0;
// 计数
private static final int COUNT = 10;
/**
* 对 volatile 变量复合运算,使用 synchronized 同步
*/
private static synchronized void increase() {
i++;
}
public static void main(String[] args) {
// 启动 10 个线程分别对 i 进行 10000 次计算,正常情况结果为 100000
for (int j = 0; j < COUNT; j++) {
new Thread(() -> {
for (int k = 0; k < 10000; k++) {
increase();
}
}).start();
}
// 等待所有累加线程全部执行结束,这里不同 ide 中线程存活数不一样,
// 该示例代码在 idea 中运行,会多出一个 Monitor Ctrl-Break 线程,故条件是 > 2,
// 如果在 Eclipse 中条件应为 > 1
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}

volatile 适合场景

volatile适用于不需要保证原子性,但却需要保证可见性的场景。一种典型的使用场景是用它修饰用于停止线程的状态标记,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.pdh.test;
/**
* volatile 复合操作测试
*
* @author pengdh
* @date 2017/11/12
*/
public class VolatileDemo {
// 申明 volatile 变量
private volatile boolean flag = false;
// 计数
private static final int COUNT = 10;
/**
* 使用 volatile 变量作为线程结束标志
*/
private void start() {
new Thread(() -> {
while (!flag) {
System.out.println("Thread is running");
}
}).start();
}
private void shutdown() {
flag = true;
System.out.println("Thread is stop");
}
public static void main(String[] args) throws InterruptedException {
VolatileDemo demo = new VolatileDemo();
demo.start();
Thread.sleep(2000);
demo.shutdown();
}
}

使用 volatile 的意义

在只需保证可见性的情况下,volatile 的同步机制性能要优于锁。


参考文献

  • 深入理解 Java 虚拟机
alexpdh wechat
欢迎扫一扫关注 程序猿pdh 公众号!