如何正确的使用 volatile 变量
上面我们聊了这么多 volatile 的原理,下面我们就来谈一谈 volatile 的使用问题。
volatile 通常用来和 synchronized 锁进行比较,虽然它和锁都具有可见性,但是 volatile 不具有原子性,它不是真正意义上具有线程安全性的一种工具。
从程序代码简易性和可伸缩性角度来看,你可能更倾向于使用 volatile 而不是锁,因为 volatile 写起来更方便,并且 volatile 不会像锁那样造成线程阻塞,而且如果程序中的读操作的使用远远大于写操作的话,volatile 相对于锁还更加具有性能优势。
很多并发专家都推荐远离 volatile 变量,因为它们相对于锁更加容易出错,但是如果你谨慎的遵从一些模式,就能够安全的使用 volatile 变量,这里有一个 volatile 使用原则
只有在状态真正独立于程序内其他内容时才能使用 volatile。
下面我们通过几段代码来感受一下这条规则的力量。
状态标志
一种最简单使用 volatile 的方式就是将 volatile 作为状态标志来使用。
volatile boolean shutdownRequested; public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } }
为了能够正确的调用 shutdown() 方法,你需要确保 shutdownRequested 的可见性。这种状态标志的一种特性就是通常只有一种状态转换:shutdownRequested 的标志从 false 转为 true,然后程序停止。这种模式可以相互来回转换。
双重检查锁
使用 volatile 和 synchronized 可以满足双重检查锁的单例模式。
class Singleton{ private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if(instance == null) { synchronized (Singleton.class) { if(instance == null) instance = new Singleton(); } } return instance; } }
这里说下为什么要用两次检查,假如有两个线程,线程一在进入到 synchronized 同步代码块之后,在还没有生成 Singleton 对象前发生线程切换,此时线程二判断 instance == null 为 true,会发生线程切换,切换到线程一,然后退出同步代码块,线程切换,线程二进入同步代码块后,会再判断一下 instance 的值,这就是双重检查锁的必要所在。
读-写锁
这也是 volatile 和 synchronized 一起使用的示例,用于实现开销比较低的读-写锁。
public class ReadWriteLockTest { private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } }
如果只使用 volatile 是不能安全实现计数器的,但是你能够在读操作中使用 volatile 保证可见性。如果你想要实现一种读写锁的话,必须进行外部加锁。