7

volatile 作为 Java 语言的一个关键字,被看作是轻量级的 synchronized(锁)。虽然 volatile 只具有synchronized 的部分功能,但是一般使用 volatile 会比使用 synchronized 更有效率。在编写多线程程序的时候,volatile 修饰的变量能够:

  1. 保证内存 可见性
  2. 防止指令 重排序
  3. 保证对 64 位变量 读写的原子性

一. 保证内存可见性

JVM 中,每个线程都拥有自己栈内存,用来保存当前线程运行过程中的变量数据;然后多个线程之间共享堆内存(也称主存)。当线程需要访问一个变量时,首先将其从堆内存中复制到自己的栈内存作为副本,然后线程每次对该变量的操作,都将是对栈中的副本进行操作 —— 在某些时刻(比如退出 synchronized 块或线程结束),线程会将栈中副本的值写回到主存,此时主存中的变量才会被替换为副本的值。这样自然就带来一个问题,即如果两个线程共享一个变量,线程A 改变了变量的值,但是 线程B 可能无法立即发现。比如下面这个经典的例子:

public class ConcurrentTest {

    private static boolean running = true;

    public static class AnotherThread extends Thread {

        @Override
        public void run() {
            System.out.println("AnotherThread is running");

            while (running) { }

            System.out.println("AnotherThread is stoped");
        }

    }

    public static void main(String[] args) throws Exception {
        new AnotherThread ().start();

        Thread.sleep(1000);
        running = false;  // 1 秒之后想停止 AnotherThread 
    }
}

上面这段代码一般情况下都会死锁,就是因为在 main 方法(主线程)中对 running 做的修改,并不能立马对 AnotherThread 可见。

如果将 running 加上修饰符 volatile,那么便可以获取实际希望的结果,因为此时主线程中设置 runningfalse 之后,AnotherThread 可以立马发现 running 的值发生了改变:
实际希望的结果

对于 volatile 修饰的变量,JVM 可以保证:

  1. 每次对该变量的写操作,都将立即同步到主存;
  2. 每次对该变量的读操作,都将从主存读取,而不是线程栈

二. 防止指令重排序

如果一个操作不是原子操作,那么 JVM 便可能会对该操作涉及的指令进行 重排序。重排序即在不改变程序语义的前提下,通过调整指令的执行顺序,尽可能达到提高运行效率的目的。

对于单例模式,为了达到延时初始化,并且可以在多线程环境下使用,我们可以直接使用 synchronized 关键字:

public class Singleton {

    public static Singleton instance = null;

    private Singleton() { }

    public synchronized static Singleton getSingleton() {
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

这样做的缺陷也很明显,那就是 instance 初始化完毕之后,以后每次获取 instance 仍然需要进行加锁操作,是个很大的效率浪费。

于是出现了一种经典写法叫 “双重检测锁”:

public class Singleton {

    public static Singleton instance = null;

    private Singleton() { }

    public static Singleton getSingleton() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}

但是这样的写法同样会存在问题,因为 instance = new Singleton() 并非原子操作,其大概可以等同于执行:

  1. 分配一个 Singleton 对应的内存
  2. 初始化这个 Singleton 对应的内存
  3. instance 指向对应的内存的地址

其中,2 依赖于 1,但是 3 并不依赖于 2 —— 所以,存在 JVM 将这三条语句重排序为 1->3->2 的可能,即变为:

a. 分配一个 Singleton 对应的内存
b.instance 指向对应的内存的地址
c. 初始化这个 Singleton 对应的内存

此时如果 线程A 执行完 b,那么此时的 instance 指向的内存并不为 null,然而这块内存却还没有被初始化。当 线程B 此时判断第一个 if (instance == null) 时发现 instance 并不为 null,便会将此时的 instance 返回 —— 但 Singleton 的初始化可能并未完成,此时 线程B 使用 instance 便可能会出现错误。

在 JDK 1.5 之后,增强了 volatile 的语义,严格限制 JVM (编译器、处理器)不能对 volatile 修饰的变量涉及的操作指令进行重排序。

所以为了避免对 instance 变量涉及的操作进行重排序,保证 “双重检测锁” 的正确性,我们可以将 instance 使用 volatile 修饰:

public class Singleton {

    /* 使用 volatile 修饰 */
    public static volatile Singleton instance = null;

    private Singleton() { }

    public static Singleton getSingleton() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}

三. 保证对 64 位变量读写的原子性

JVM 可以保证对 32位 数据读写的原子性,但是对于 longdouble 这样 64位 的数据的读写,会将其分为 高32位 和 低32位 分两次读写。所以对于longdouble 的读写并不是原子性的,这样在并发程序中共享 longdouble 变量就可能会出现问题,于是 JVM 提供了 volatile 关键字来解决这个问题:

使用 volatile 修饰的 longdouble 变量,JVM 可以保证对其读写的原子性。

但值得注意的是,此处的 “写” 仅指对 64位 的变量进行直接赋值。而对于 i++ 这个语句,事实上涉及了 读取-修改-写入 三个操作:

  1. 读取变量到栈中某个位置
  2. 对栈中该位置的值进行自增
  3. 将自增后的值写回到变量对应的存储位置

因此哪怕变量 i 使用 volatile 修饰,也并不能使涉及上面三个操作的 i++ 具有原子性。所以多线程条件下使用 volatile 关键字的前提是:对变量的写操作不依赖于变量的当前值,而赋值操作很明显满足这一前提。


在多线程环境下,正确使用 volatile 关键字可以比直接使用 synchronized 更加高效而且代码简洁,但是使用 volatile 关键字也更容易出错。所以,除非十分清楚 volatile 的使用场景,否则还是应该选择更加具有保障性的 synchronized

Brian Goetz 大大写过一篇 “volatile 变量使用指南”,有兴趣的读者可以参阅:Java 理论与实践: 正确使用 Volatile 变量

volatile 变量的底层实现原理,有兴趣的读者可以参阅:

  1. http://www.infoq.com/cn/artic...
  2. http://www.cnblogs.com/paddix...

mizhoux
3k 声望336 粉丝

Java Follower