java并发-volatile的内存语义
3.4volatile的内存语义
当声明共享变量为volatile后,对这个变量的读/写将会很特别。
3.4.1 volatile的特性
理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这 些单个读/写操作做了同步。下面通过具体的示例来说明,示例代码如下。
class VolatileFeaturesExample {
volatile long vl = 0L; //使用volatile声明64位的long型变量
public void set(long l) {
vl = l; //单个volatile变量的写
}
public void getAndIncrement () {
vl++; // 复合(多个)volatile变量的读/写
}
public long get() {
return vl; // 单个volatile变量的读
}
}
假设有多个线程分别调用上面程序的3个方法,这个程序在语义上和下面程序等价。
class VolatileFeaturesExample {
long vl = 0L; // 64位的long型普通变量
public synchronized void set(long l) { // 对单个的普通变量的写用同一个锁同步
vl = l;
}
public void getAndIncrement () { //普通方法调用
long temp = get(); // 调用已同步的读方法
temp += 1L; //普通写操作
set(temp); //调用已同步的写方法
}
public synchronized long get() { // 对单个的普通变量的读用同一个锁同步
return vl;
}
}
锁的语义决定了临界区代码的执行具有原子性。这意味着,即使是64位的long型和double 型变量,只要它是volatile变量,对该变量的读/写就具有原子性。如果是多个volatile操作或类 似于volatile++这种复合操作,这些操作整体上不具有原子性。
简而言之,volatile变量自身具有下列特性。
- 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写 入。
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不 具有原子性。
3.4.2 volatile写-读建立的happens-before关系
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
int i = a; // 4
}
}
假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-before规则,这个 过程建立的happens-before关系可以分为3类:
- 1)根据程序次序规则,1 happens-before 2;3 happens-before 4。
- 2)根据volatile规则,2 happens-before 3。
- 3)根据happens-before的传递性规则,1 happens-before 4。
3.4.3 volatile写-读的内存语义
volatile写的内存语义如下。 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内 存。
volatile读的内存语义如下。 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主 内存中读取共享变量。
下面对volatile写和volatile读的内存语义做个总结。
- 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程 发出了(其对共享变量所做修改的)消息。
- 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile 变量之前对共享变量所做修改的)消息。
- 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过 主内存向线程B发送消息。
3.4.4 volatile内存语义的实现
能否重排序:
第一个操作 | 第二个操作 普通读/写 | 第二个操作 volatile读 | 第二个操作 volatile写 |
---|---|---|---|
普通读/写 | 能 | 能 | 否 |
volatile读 | 否 | 否 | 否 |
volatile写 | 能 | 否 | 否 |
举例来说,第2行最后一个单元格的意思是:在程序中,当第一个操作为普通变量的读或 写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
从表中可以看出:
-
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile写之前的操作不会被编译器重排序到volatile写之后。
-
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile读之后的操作不会被编译器重排序到volatile读之前。
-
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来 禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总 数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能 得到正确的volatile内存语义。