Poison

volatile

通常,为了确保共享变量一致且可靠地更新,线程应该通过获取锁来确保它独占使用这些变量,该锁通常对这些共享变量强制互斥。Java 编程语言提供了第二种机制,即 volatile 字段,在某些场景下,它比使用锁更方便。一个字段可以被声明为 volatile,在这种情况下,Java 内存模型确保所有线程看到变量的一致值。

比如下面的例子,一个线程不停调用方法 one,另一个线程不停调用方法 two

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
package me.tianshuang;

public class VolatileTest {

static int i = 0, j = 0;

static void one() {
i++;
j++;
}

static void two() {
System.out.println("i=" + i + " j=" + j);
}

public static void main(String[] args) {
new Thread(() -> {
while (true) {
one();
}
}).start();

while (true) {
two();
}
}

}

咋眼一看可能以为输出结果只可能为 j 大于等于 i,因为方法 two 获取两个变量中间的时隙可能会有方法 one 执行,但是实际上因为以上代码没有引入同步机制,且因为指令重排序,可能导致在 one 所处的线程中 j++ 先于 i++ 执行。在我的电脑上,产生了如下的输出:

1
2
3
4
5
6
7
i=1276138785 j=1276138793
i=1276139531 j=1276139531
i=1276140256 j=1276140256
i=1276140977 j=1276140976
i=1276141707 j=1276141715
i=1276142444 j=1276142444
i=1276143125 j=1276143133

通过以上输出可以观察到,打印出的 i 可能等于 j,可能大于 j,也可能小于 j。上面这个例子来自于 Example 8.3.1.4-1. volatile Fields,个人认为这个例子并不是很好,因为同时涉及到了以下问题:

  • 方法 one 执行时线程内部的指令重排序
  • 方法 two 执行时获取两个变量中间时隙可能方法 one 被调度执行
  • 即使变量 ij 在执行方法 one 时被更新,调用方法 two 的线程因为线程本地缓存也可能看不到最新的变量值

关于这个例子的讨论可见 how to understand volatile example in Java Language Specification? - Stack Overflow,感兴趣的可以看看。

可以在两个方法上使用 synchronized 修饰符来规避上面这个例子中的乱序行为,如以下代码:

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
package me.tianshuang;

public class VolatileTest {

static int i = 0, j = 0;

static synchronized void one() {
i++;
j++;
}

static synchronized void two() {
System.out.println("i=" + i + " j=" + j);
}

public static void main(String[] args) {
new Thread(() -> {
while (true) {
one();
}
}).start();

while (true) {
two();
}
}

}

在两个方法上都使用了 synchronized 修饰符后,有了以下的保证:

  • 方法 one 及方法 two 只能串行执行,比如方法 two 在执行过程中方法 one 不可能被调度执行
  • 方法 two 能够看到变量 i 及变量 j 的最新值

因为有了以上保证,ij 的输出值将完全一致。

另一种方式就是使用 volatile 修饰符修饰变量 i 及变量 j,如以下代码:

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
package me.tianshuang;

public class VolatileTest {

static volatile int i = 0, j = 0;

static void one() {
i++;
j++;
}

static void two() {
System.out.println("i=" + i + " j=" + j);
}

public static void main(String[] args) {
new Thread(() -> {
while (true) {
one();
}
}).start();

while (true) {
two();
}
}

}

使用 volatile 修饰符修饰变量 i 和变量 j 后,有了以下保证:

  • 禁止了方法 one 中的两个变量执行的指令重排序
  • 保证了方法 two 获取两个变量时能够看到该变量的最新的值

有了以上的保证,那么方法 one 中的 j++ 不可能先于 i++ 执行,理论上不可能出现变量 j 的值大于变量 i 的值的情况,但是我们在本地执行程序,将会观察到打印出来的值中出现了 j 大于 i,比如如下输出:

1
2
3
4
5
i=305491464 j=305491464
i=305491957 j=305491958
i=305492174 j=305492175
i=305492421 j=305492422
i=305492691 j=305492692

这个很好解释,是因为方法 two 获取变量 i 及变量 j 中间的时隙方法 one 被调度执行了,所以输出值中会有 j 大于 i 的数据,如果仔细观察所有的输出值,不会有 i 大于 j 的数据,这是因为 volatile 禁止掉了方法 one 执行时语句 i++ 与语句 j++ 的指令重排序。

Reference

8.3.1.4. volatile Fields
is volatile required with synchronized? - Stack Overflow
Is the volatile keyword required for fields accessed via a ReentrantLock? - Stack Overflow
Volatile: why prevent compiler reorder code - Stack Overflow
volatile (computer programming) - Wikipedia
Is volatile expensive? - Stack Overflow