[Java] volatile的使用场景

volatile的使用场景

volatile用处

volatile是java中较轻量的同步原语,用volatile声明的变量保证了该变量的可见性, 使用volatile声明的变量被其他线程更改时,该变量会立即被写入内存,同时 CPU会锁住总线,将缓存同步指令传递到其他处理器,其他处理器会从主内存中获取该变量的最新值更新到自己的缓存中。

如下面的代码,如果这个counter变量没有声明成volatile,那么不同线程的访问会存在可见性的问题,如多个线程会访问各自处理器的缓存,而不是主内存中的数据。

public class SharedObject {

    public int counter = 0;

}

下图所示,thread 1将该变量的值改成了7,但是并没有同步到主内存,而thread 2仍然使用的之前的值(0),并没有进行更新。

所以在这种场景下存在一个可见性问题,需要将counter声明成volatile的。

public class SharedObject {

    public volatile int counter = 0;

}

volatile使用场景

状态标志

实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

volatile boolean shutdownRequested;
 
...
 
// 其他线程调用,通知需要结束,并改变状态
public void shutdown() { shutdownRequested = true; }
 
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

很可能会从循环外部调用 shutdown() 方法 —— 即在另一个线程中 —— 因此,需要执行某种同步来确保正确实现 shutdownRequested 变量的可见性。(可能会从 JMX 侦听程序、GUI 事件线程中的操作侦听程序、通过 RMI 、通过一个 Web 服务等调用)。然而,使用 synchronized 块编写循环要比使用清单 2 所示的 volatile 状态标志编写麻烦很多。由于 volatile 简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile

这种类型的状态标记的一个公共特性是:通常只有一种状态转换;shutdownRequested 标志从 false 转换为 true,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察觉的情况下才能扩展(从 false 到 true,再转换到 false)。此外,还需要某些原子状态转换机制,例如原子变量。

一次性安全发布(DCL单例)

缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原语值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)。

实现安全发布对象的一种技术就是将对象引用定义为 volatile 类型。

public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;
 
    public void initInBackground() {
        // do lots of stuff
        theFlooble = new Flooble();  // this is the only write to theFlooble
    }
}
 
public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // do some stuff...
            // use the Flooble, but only if it is ready
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}

该模式的一个必要条件是:被发布的对象必须是线程安全的,或者是有效的不可变对象(有效不可变意味着对象的状态在发布之后永远不会被修改)。volatile 类型的引用可以确保对象的发布形式的可见性,但是如果对象的状态在发布后将发生更改,那么就需要额外的同步。

错误的使用场景

volatile可以表示变量在读的时候获得可见性保护,但是如果变量需要读写操作,如i++那么则必须需要锁来保证同步性。

下面演示一个错误的使用。

public class VolatileTest2 implements Runnable {


    private static volatile boolean flag = true;

    public static void main(String[] args) {
        for (int j = 0; j < 10000; j++) {
            Thread t = new Thread(new VolatileTest2());
            t.start();
        }
    }

    public void run() {
        // 这里设置一个Flag,如果有线程执行,那么更改flag阻止其他线程打印。
        if (flag) {
            flag = false;
            System.out.println("我成功了!");
        }
    }
}

但是结果可能出现这种情况:

我成功了!
我成功了!

有多个线程打印了数值。根本原因是获取变量的值和更改变量的值是两个操作,中间可能会并发别的指令,比如判断完成,还没有将flag更新时,另一线程进入,导致逻辑错误。所以需要一个原子操作,或者加锁保证我们的同步。

下面使用AtomicBoolean来修复这个问题

public class VolatileTest2 implements Runnable {



    private static AtomicBoolean mAtomicFlag = new AtomicBoolean(true);

    public static void main(String[] args) {
        for (int j = 0; j < 10000; j++) {
            Thread t = new Thread(new VolatileTest2());
            t.start();
        }
    }

    public void run() {
        if (mAtomicFlag.compareAndSet(false,true)) {
            System.out.println("我成功了!");
        }
    }
}

此时变量的读和写是原子操作,满足我们的业务逻辑,结果无多次打印的情况。

我成功了!

参考:
https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
http://tutorials.jenkov.com/java-concurrency/volatile.html

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页