标题 neta 自《计算机网络自顶向下》
思维导图
volatile 在 Java 中被称为轻量级 synchronized
。很多并发专家引导用户远离 volatile 变量,因为使用它们要比使用锁更加容易出错。但是如果理解了 volatile 能帮助你写出更好的程序。
为了方便理解 volatile,用代码来表示一下加了 volatile 的效果。
给变量加上 volatile 相当于在 get 和 set 方法中加了锁。
public synchronized int getX() { return x; } public synchronized void setX(int x) { this.x = x; }
注意这里只保证了get 和 set 的原子性,当有其他操作的时候就不是原子性的了。
下面的操作不是原子性的,当个 5 个线程同时执行这个方法 100 次后出现的结果很可能小于 500。
volatile int x; public void inc() { x++; }
原因是这个程序相当于
int x; public synchronized int getX() { return x; } public synchronized void setX(int x) { this.x = x; } public void inc() { int temp = getX(); // 1 temp += 1; // 2 setX(temp); // 3 }
可以看出即使 get 和 set 操作是原子性的,整个操作也不是原子性的。
当两个线程 A , B 同时执行 inc
时,可能会出现
A-1 得到 x = 1
B-1 得到 x = 1
A-2 temp 为 2
B-2 temp 为 2
A-3 x 被设为 2
B-3 x 被设为 2
在执行完毕后 x 的值只增加了 1。
我们在 JMM 中讲解 volatile 的内存语义。可以参照着这篇看。 JVM内存模型、指令重排、内存屏障概念解析
volatile 保证了新的值能立刻同步到主内存中,以及每次使用前都到主内存刷新。
volatile 通过在写入变量的时候,JVM 会向 CPU 发送一个 lock 前缀指令将变量同步入主内存
而当出现了这个命令以后,所有其他线程上的缓存就会被强制设置成无效,当下次要用到这个变量的时候需要去主内存中取。
通过 Lock 指令
每次使用变量之前都必须先从主内存刷新最新的值。
每次修改变量后都必须立即同步回主内存中,保证其他线程可以看到最新的值。
一个比较有用的抽象:把加了 volatile 的变量当作是没有中间的缓存,所有的数据操作都是在主内存上的。
###禁止指令重排
CPU 和编译器为了执行效率,会将指令重排序。如果不知道的可以参照上面那一片博文来对照着读。
volatile 修饰变量不会被指令重排优化,保证代码执行顺序和程序顺序相同。
在几个地方会插入 StoreStore 和 StoreLoad 阻止重排序。
如果不知道这两个指令可以看一下上面的博客。 // TODO 马上写完 (咕咕咕
正确使用 volatile 依赖于
也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,比如游戏结束,将游戏正在进行的 flag 设置为 false,通知绘图线程停止。
volatile boolean flag = false; private void waiting() { while(!flag) { // do something } }
一次性安全分布用于双重检查实现单例模式。
private volatile SingleTest instance; SingleTest getInstance() { if (instance == null) { synchronized (SingleTest.class) { if (instance == null) { instance = new SingleTest(); } } } return instance; }
为什么要用到 volatile 呢?因为新建类分为三步
在这里第二步可能重排序,这时候可能会将没有初始化成功就把对象发布出去了,所以需要 volatile 来阻止指令重排。
安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
使用该模式的另一种应用程序就是收集程序的统计信息。清单 4 展示了身份验证机制如何记忆最近一次登录的用户的名字。将反复使用 lastUser
引用来发布值,以供程序的其他部分使用。
该模式是前面模式的扩展;将某个值发布以在程序内的其他地方使用,但是与一次性事件的发布不同,这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布后不会更改。使用该值的代码需要清楚该值可能随时发生变化。
volatile bean 是线程安全的。在 volatile bean 模式中,JavaBean 被用作一组具有 getter 和/或 setter 方法 的独立属性的容器。
在 volatile bean 模式中,
volatile
@ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }
在 JDK 7 并发包里新增了一个队列集合 LinkedTransferQueue,它在使用 volatile 变量时,用一种追加字节的方式来优化出队和入队的性能。
它将变量追加到了 64 字节来提高性能。
64 位 CPU 在队列中的元素不足 64 个字节时会将多个元素读入一个缓存行中,在多线程当读取一个元素的时候会锁住这个缓存行,进而导致这个元素附近的元素都不能被读取。
如果一个变量为 64 字节,那么每个元素都被读入不同的缓存中,相邻队列元素就能被不同线程同时访问了。
参考文献