目录
考虑因素
正确使用volatile变量的场合
1. 状态标记
2. 一次性安全发布
3. 独立观察
4. volatile bean
5. 高级模式 - 一种开销较低的读写锁
1. 对变量的操作不要依赖当前值
2. 该变量没有包含在具有其他变量的不变式中
下面举一些使用不规范的例子,方便大家理解
例子1-计数器
public class Counter {
private volatile int num;
public void doSomething() {
num++;
// 省略部分代码
}
public int getNum() {
return num;
}
}
显然计数器每次累加都需要依赖当前变量,而且虽然增量操作看起来类似一个单独操作,实际上是由读取-修改-写入操作序列组成的组合操作。volatile不能提供必须的原子性。
例子2-数值范围类
@NotThreadSafe
public class NumberRange {
private int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
如果初始状态是 (0, 5)
,同一时间内,线程 A 调用 setLower(4)
并且线程 B 调用 setUpper(3)
,显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4, 3)
—— 一个无效值
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
由于 volatile 简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile
在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线 程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)
双重检查锁定问题:
public class Singleton {
private static Singleton instance;
public Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
类的实例化步骤:分配内存空间 -》 初始化Singleton实例 -》 赋值 instance 实例引用。但是很有可能上面这些步骤都是进行过重排序,很有可能先直接返回了对象引用。
解决方案:直接给变量instance添加volatile关键字,可以实现变量初始化完全后再返回引用。
定期 “发布” 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
public class UserManager {
public volatile String lastUser;
public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
}
return valid;
}
}
很多框架为易变数据的持有者(例如 HttpSession
)提供了容器,但是放入这些容器中的对象必须是线程安全的。
@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;
}
}
JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑
如果读操作远远超过写操作,可以结合内部锁和volatile变量来实现并发,用volatile保证结果的可见性,如果更新不频繁,该方式可以实现更好的性能。
@ThreadSafe
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}