concurrency_volatile

第三十一章 关键字volatile

1 概述

在Java多线程环境中,使用关键字synchronized锁Lock的目的是为了保护共享资源。但是,使用这两种方法的代价很高,不适用于运算速度快,性能要求高的应用场景。因此,Java语言引入了关键字volatile。使用volatile的目的也是为了保护共享变量,使得共享变量能在多个线程中安全可靠的访问和更新。因为volatile关键字的实现机制处于更加底层的位置,可以在硬件上实现,也可以在底层软件中实现。所以,使用volatile的代价很小。因为volatile的实现多种多样,本章并不会把重点放在volatile的实现细节上;而是会着重讲解volatile的原理。

volatile的实现并不依赖于Java语言提供的synchronized机制或者锁机制,使用volatile的效率比synchronized和锁高很多。但是,使用volatile的场景有限,使用上没有synchronized和锁机制灵活。

2 问题

我们曾在第二十三章介绍过多线程程序的复杂性的四个问题。volatile关键字的出现是为了帮助开发人员解决其中两个重要问题:原子性(Atomicity)和数据可见性(Data Visibility)。从实现的角度看,编译器不会改变使用了volatile关键字代码的执行顺序。所以,实际上,关键字volatile也影响着编译器优化算法。

2.1 原子性(Atomicity)问题

目前,在Java语言中,读或者写一个32位数据的操作都是原子操作。例如,写一个int基本数据类型的数据是原子操作。但是,读或者写一个64位数据的操作需要两个原子操作完成。当Java虚拟机写一个64位的数据时(例如:long或者double),Java虚拟机实际上是先写高32位数据,然后再写低32位数据,或者反过来,先写低32位数据,再写高32位数据。那么,如果在这两个写操作之间发生了线程切换,那么,该64位数据是处于中间状态的。如果,在此时,另一个线程访问该数据的话,该数据的值是错误的,程序的结果将变得不可预测。类似的情况也可能发生在Java虚拟机读取一个64位的数据时发生。

因此,为了正确的读或者写一个64位数据,Java程序需要使用一种线程同步方法来保护这个共享的64位数据。可是,如果在此时使用synchronized关键字或者使用Lock的话,效率非常低,因为获得锁的操作本身就是一个效率不高的操作(对于底层软件而言)。因此,为了提高效率,Java语言引入了关键字volatile。Java语言保证被声明为volatile的64位数据的读和写操作都是原子操作,是不可分割的操作。所以,线程之间的切换要么发生在读操作/写操作之前,要么发生在读操作/写操作之后,Java语言保证64位对象不会处于上述的中间状态。

public class VolatileExample {
    public static void main() {
        long i = 0;
        volatile long j = 0;
        i = 1; // 这不是原子操作
        j = 1; // 这个是原子操作,因为j声明为volatile long类型
    }
}

2.2 数据可见性(Data Visibility)问题

第二个问题是数据可见性问题。在第二十三章我们曾介绍过,为了进一步加快软件运行的速度,部分数据会存放在高速缓存中。CPU的每个核心(Core)都有各自独立的且仅能被该核心访问的L1 Cache;L2 Cache是所有核心共享的高速缓存。当一个线程写一个数据时,新的数值可能会暂时缓存在运行该线程的核心的L1 Cache中。若此时,运行在另一个核心上的线程访问该数据的话,该线程是无法得到最新的数值的,因为,最新的数值在另一个核心的L1 Cache中。我们称这个问题为数据可见性问题。

使用关键字volatile能有效的解决这个问题。Java语言明确指出,当一个变量声明为volatile后,所有线程访问该变量得到的值是一致的。

A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable.  -- Java 13 标准文档

因此,对于volatile变量,线程不能从L1 Cache中读取该变量或者写入将其写入L1 Cache中。在下图展示的CPU体系结构中,Java虚拟机可将volatile变量存放于内存(Main Memory)中。

图一 volatile变量的读与写

图一 volatile变量的读与写

2.3 什么问题可以由关键字volatile解决

关键字volatile能有效解决上述的原子性和数据可见性问题,但是,能使用volatile的场景任然十分有限。总的来说,当多个线程同时访问一个volatile的共享变量时,如果只有一个线程会改变该变量的值,而其余的线程只是读取该变量的值的话,这种方式的多线程访问是安全的。但凡有两个或者多个线程能同时修改一个volatile的共享变量时,这时的写操作是不安全的,因为一个新值可能被另一个新值覆盖。

当一个写操作和一个读操作同时发生时,会如何呢?因为对volatile变量的读和写都是原子操作,所以,它们都是不可分割的操作。当在写操作完毕之前,如果读操作已发生,则读取的应该是旧的值。如果读操作在写操作完毕之后发生,则读取的是新的值,即使读和写在不同的线程中。因为,在写操作完毕之后,volatile变量的新值对所有线程都是立即可见的。

Java语言还要求,在使用volatile变量的语句禁止使用重排序(Re-ordering Optimization)优化,这也是为了确保修改数据的操作在正确的地方发生,并且数据能对所有线程立即可见。

3 例子

最后,我们通过一个用例来展示关键字volatile的使用方法。下面的代码描述的是一种传统的停止线程的方式。在VolatileThreadExample类中定义了一个私有的,volatile的变量isRunnable,用来标记该线程是否需要继续运行。在run()方法中,该线程不断地检查isRunnable的值,一旦该值被置为false,则循环立即终止,线程结束。方法readyToStop()则为其他线程提供了终止该线程的接口。

因为isRunnable被声明为volatile,所以,当isRunnable被修改后,所有线程都能立即访问到新值,因此,VolatileThreadExample所代表的线程能够立即退出。如果isRunnable未被声明为volatile的话,有可能在isRunnable被设置为false之后,VolatileThreadExample线程仍然会继续运行一段时间,直到isRunnable的新值变为可见为止。

public class VolatileThreadExample implements Runnable {
    // 定义为volatile成员变量
    private volatile boolean isRunnable = true;

    @Override 
    public void run() {
        try {
            while (isRunnable) {
                System.out.println("I am running.");
                Thread.sleep(100);
            }
            System.out.println("Exiting.");
        } catch (InterruptedException ex) {}
    }

    public void readyToStop() {
        isRunnable = false;
    }

    public static void main(String[] args) {
        try {
            VolatileThreadExample example = new VolatileThreadExample();
            Thread thread = new Thread(example);
            thread.start();

            Thread.sleep(300);
            example.readyToStop();
            thread.join();
        } catch (InterruptedException ex) {}
    }
}

程序运行结果:

> java VolatileThreadExample
I am running.
I am running.
I am running.
Exiting.

4 总结

本小节讲解了关键字volatile的用法和其解决的问题。volatile工作于底层,所以,使用volatile的速度比使用synchronized和锁的速度要快很多。但是,能使用volatile的场景非常有限,synchronized和锁为开发人员提供了更加灵活的线程同步机制。

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.