一、volatile的特性
当声明共享变量为volatile后,对这个变量的读/写将会很特别。理解volatile特性的一个好方法是把对volatile变量的单个读/写(不是类似val++的复合操作),看成是使用同一个锁对这些单个读/写操作做了同步。下面通过具体的示例来说明。
class VolatileFeaturesExample { volatile long vl = 0L; // 使用volatile定义一个64位的long型的共享变量v1 public void set(long l) { vl = l; // 单个volatile变量的写 } public void getAndIncrement() { vl++; // 复合(多个)volatile变量的读/写,存在竞态条件,线程不安全,需要加锁 } public long get() { return vl; // 单个volatile变量的读 } }假设有多个线程分别调用上面程序的3个方法,这个程序在语义上和下面程序等价。
class VolatileFeaturesExample1 { long vl = 0L; // 定义一个64位的long型普通变量 public synchronized void set(long l) {// 对单个的普通变量的写用同一个锁同步,锁对象为this vl = l; } public void getAndIncrement() { // 普通方法调用 long temp = get(); // 调用已同步的读方法 temp += 1L; // 普通写操作,存在竞态条件,线程不安全,需要加锁 set(temp); // 调用已同步的写方法 } public synchronized long get() { // 对单个的普通变量的读用同一个锁同步,锁对象为this return vl; } }如上面示例程序所示,一个volatile变量的单个读/写操作,与一个普通变量的读/写操作都是使用同一个锁来同步,它们之间的执行效果相同。
注意:上面的getAndIncrement()方法是非线程安全的,因为其中存在竞态条件(value++实际上包含三个独立的操作:1 读取value;2 将 value加1;3 将计算结果写入value)。
锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。锁的语义决定了临界区代码的执行具有原子性。这意味着,即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性(单个volatile变量)。如果是多个volatile操作或类似于volatile++这种复合操作整体上不具有原子性。
volatile变量自身具有下列特性
可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
二、volatile写-读建立的happens-before关系
从内存语义的角度来说,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 System.out.println("i=" + i); } } }假设线程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。
三、volatile写-读的内存语义
volatile写的内存语义如下,当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
以上面示例程序VolatileExample为例,假设线程A首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。图3-17是线程A执行volatile写后,共享变量的状态示意图。
如图所示线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。
四、volatile读的内存语义如下
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
如图所示,在读flag变量后,本地内存B包含的值已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值变成一致。
如果我们把volatile写和volatile读两个步骤综合起来看的话,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。
下面对volatile写和volatile读的内存语义做个总结。(重点理解下面的总结)
>线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
>线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
>线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。