正确使用volatile变量

 

目录

考虑因素

正确使用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变量的场合

1. 状态标记

volatile boolean shutdownRequested;
 
...
 
public void shutdown() { shutdownRequested = true; }
 
public void doWork() {
    while (!shutdownRequested) {
        // do stuff
    }
}

由于 volatile 简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile 

2. 一次性安全发布

在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线 程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(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关键字,可以实现变量初始化完全后再返回引用。

3. 独立观察

定期 “发布” 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 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;
    }
}

4. volatile bean

很多框架为易变数据的持有者(例如 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 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑 

5. 高级模式 - 一种开销较低的读写锁

如果读操作远远超过写操作,可以结合内部锁和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++;
    }
}

你可能感兴趣的:(java并发编程)