Java 并发编程之 volatitle

volatile 关键字是 Java 提供的另一种解决可见性和有序性问题的方案。对于原子性,对 volatile 变量的单次读写操作可以保证原子性的,如 long 和 double 类型变量,但是并不能保证 i++ 这种操作的原子性,因为本质上 i++ 是读写两次操作。

可见性

每个线程拥有自己的一个自己的线程工作内存,在多线程下可能会出现一个线程修改了一个变量没有及时更新到主存,其他线程获得的还是一个老的值。修改 volatile 变量时会强制将修改后的值刷新到主内存中,修改 volatile 变量后会导致其他线程工作内存中对应的变量值失效,因此再读取该变量值的时候就需要重新从读取主内存中的值。

有序性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {

public static volatile Singleton singleton;

private Singleton() {};

public static Singleton getInstance() {
if (singleton == null) {
synchronized (singleton) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

上面是一种经典的单例模式的实现,什么要对 singleton 变量加上 volatile 关键字。实例化一个对象其实可以分为三个步骤:

  1. 分配内存空间。
  2. 初始化对象。
  3. 将内存空间的地址赋值给对应的引用。

但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:

  1. 分配内存空间。
  2. 将内存空间的地址赋值给对应的引用。
  3. 初始化对象

如果是上面这个步骤,多线程环境下就可能将一个未初始化的对象引用暴露出来,为了防止重排序需要将变量设置为 volatile 类型的变量。

happans-before

JSR 133 中对 happen-before 的定义如下:

  1. Each action in a thread happens before every subsequent action in that thread.
  2. An unlock on a monitor happens before every subsequent lock on that monitor.
  3. A write to a volatile field happens before every subsequent read of that volatile.
  4. A call to start() on a thread happens before any actions in the started thread.
  5. All actions in a thread happen before any other thread successfully returns from a join() on that thread.
  6. If an action a happens before an action b, and b happens before an action c, then a happens before c.

为了实现 volatile 可见性和 happen-befor 的语义。JVM 底层是通过内存屏障来完成的,内存屏障是一组处理器指令,用于实现对内存操作的顺序限制。

原子性

volatitle 其实不能保证我们大多时候所能理解的原子性,只能保证单次读/写操作的原子性,可以参考一下下面这个例子:

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
public class Demo {

private volatile int i;

private void add(){
i++;
}

public static void main(String[] args) throws Exception {
Demo demo = new Demo();

for (int n = 0; n < 10000; n++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo.add();
}
}).start();
}

Thread.sleep(10000);

System.out.println(demo.i);
}
}

可以多运行几次上面的代码,结果不是 10000,因为 i++ 其实是一个复合操作包括三步骤:

  1. 读取i的值。
  2. 对i加1。
  3. 将i的值写回内存。

上面这个问题可以通过加锁或者利用 AtomicInteger 基于 cas 来解决。