当声明共享变量为volatile后,对这个变量的读/写将会很特别。为了揭开volatile的神秘面纱,下面将介绍volatile的内存语义及其实现。
1.volatile的实现
理解volatile特性的一个好方法是吧对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。下面通过示例来进行说明。
class VolatileFeaturesExample {
volatile long v1 = 0L; //使用volatile声明64位的long型变量
public void set(long l) {
v1 = l; //单个volatile变量的写
}
public void getAndIncrement() {
v1++; //复合(多个)volatile变量的读/写
}
public long get() {
return v1; //单个volatile变量的读
}
}
假设有多个线程分别调用上面程序的3个方法,这个程序在语义上和下面的程序等价
class VolatileFeaturesExample {
volatile long v1 = 0L; //64位的long型普通变量
public synchronized void set(long l) {
v1 = l; //对单个普通变量的写用同一个锁同步
}
public void getAndIncrement() { //普通方法调用
long temp = get(); //调用已同步的读方法
temp += 1L; //普通写操作
set(temp) //调用已同步的写方法
}
public synchronized long get() { //对单个普通变量的读用同一个锁同步
return v1;
}
}
如上面程序所示,一个volatile变量的单个读/写操作,与一个普通变量的读/写操作都是使用同一个锁来同步,他们之间的执行效果相同。
锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到对这个volatile变量最后的写入。
锁的语义决定了临界区代码的执行具有原子性。这意味着,即使是64位的long型和double型变量,只要他是volatile变量,对该变量的读/写就具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。
简而言之,volatile变量自身具有以下特性:
1.可见性。对一个volatile变量的读,总是能看到对这个volatile变量最后的写入。
2.原子性。对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
2.volatile写-读建立的happens-before关系
上面讲的是volatile变量自身的特性,对程序员来说,volatile对线程的内存可见性的影响比volatile自身的特性更为重要,也更需要我们关注。
从JDK1.5开始volatile变量的写-读可以实现线程之间的通信。
从内存语义的角度来说,volatile变量的写-读与锁的释放-获取有相同的内存效果:volatile写和锁的释放有相同的内存语义,volatile的读与锁的获取有相同的内存语义。
class VolatileExample {
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.volatile写-读的内存语义
1.volatile写的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存中。
2.volatile读的内存语义
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
4.volatile内存语义的实现
下面来看看JMM如何实现volatile的内存语义
前面我们提到过重排序分为编译器重排序和处理器重排序。为了实现volatile语义,JMM会分别限制这两种类型得重排序类型。
1.当第二个操作时volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到写之后。
2.当第一个操作时volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到读之前。
3.当第一个操作时volatile写,第二个操作是volatile读时,不能重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。
1.在每个volatile写操作的前面插入一个StoreStore屏障。
2.在每个volatile写操作的前面插入一个StoreLoad屏障。
3.在每个volatile读操作的前面插入一个LoadLoad屏障。
4.在每个volatile读操作的前面插入一个LoadStore屏障。
5.JSR-133为什么要增强volatile的内存语义
在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量重排序。因此,在旧的内存模型中,volatile的写-读没有锁的释放-获取所具有的内存语义。为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义: 严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读与锁的释放-获取具有相同的内存语义。从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语义,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。
由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大,在可伸缩性和性能上,volatile更有优势。