本篇内容主要摘自《Java并发编程的艺术-方腾飞》
- 更多相关文章见笔者博客
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;
}
}
- 如上面示例程序所示,一个 volatile 变量的单个读 / 写操作,与一个普通变量的读 / 写操作都是使用同一个锁来同步,它们之间的执行效果相同
- 锁的 happens-before 规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入
- 锁的语义决定了临界区代码的执行具有原子性。这意味着,即使是 64 位的 long 型和 double 型变量,只要它是 volatile 变量,对该变量的读 / 写就具有原子性。如果是多个volatile 操作或类似于volatile++ 这种复合操作,这些操作整体上不具有原子性
- 简而言之,volatile 变量自身具有下列特性
-
可见性 对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后
的写入 - 原子性 对任意单个 volatile 变量的读 / 写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性
-
可见性 对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后
2. volatile 写 - 读建立的 happens-before 关系
-
上面讲的是 volatile 变量自身的特性,对程序员来说,volatile 对线程的内存可见性的影响比 volatile 自身的特性更为重要,也更需要我们去关注
-
从 JSR-133 开始(即从 JDK5 开始),volatile 变量的写 - 读可以实现线程之间的通信。
-
从内存语义的角度来说,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上述 happens-before 关系的图形化表现形式如下
![](https://img.haomeiwen.com/i4478430/8b1a8f47865369a5.png)
在上图中,每一个箭头链接的两个节点,代表了一个 happens-before 关系。黑色箭头表示程序顺序规则;橙色箭头表示 volatile 规则;蓝色箭头表示组合这些规则后提供的 happens-before保证
这里 A 线程写一个 volatile 变量后,B 线程读同一个 volatile 变量。A 线程在写 volatile 变量之前所有可见的共享变量,在 B 线程读同一个 volatile 变量后,将立即变得对 B 线程可见。
3. volatile 写 - 读的内存语义
volatile 写的内存语义如下
- 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。
以上面示例程序 VolatileExample 为例,假设线程 A 首先执行 writer() 方法,随后线程 B执行 reader() 方法,初始时两个线程的本地内存中的 f lag 和 a 都是初始状态。图 3-17 是线程A 执行 volatile 写后,共享变量的状态示意图
![](https://img.haomeiwen.com/i4478430/c1b6948b73b6a5b6.png)
如上图所示,线程 A 在写 f lag 变量后,本地内存 A 中被线程 A 更新过的两个共享变量的值被刷新到主内存中。此时,本地内存 A 和主内存中的共享变量的值是一致的
-
volatile 读的内存语义如下
当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量
图 3-18 为线程 B 读同一个 volatile 变量后,共享变量的状态示意图。
如图所示,在读 f lag 变量后,本地内存 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 发送消息
![](https://img.haomeiwen.com/i4478430/36b995ca7c32fad1.png)
网友评论