volatile 关键字(轻量级同步机制)
更多内容,前往 IT-BLOG
volatile 表示 “不稳定” 的意思。用于修饰共享可变变量,即没有使用 final(不可变变量) 关键字修饰的实例变量或静态变量,相应的变量就被称为 volatile 变量,如下:
volatile 关键字修饰的变量容易变化(多线程下会被其它线程修改),因而不稳定。volatile 变量的不稳定性意味着对这种变量的读和写操作都必须从高速缓存或者主内存(也是通过高速缓存读取),以读取变量的最新值。因此,volatile 变量不会被编译器分配到寄存器进行存储(相当于备份),对 volatile 变量的读写操作都是内存访问(访问高速缓存相当于主内存)操作。
Volatile 被称为轻量级锁,其作用与锁的作用有相同的地方:保证可见性和有序性。所不同的是,在原子性方面它仅能保障写 volatile 变量操作的原子性,但没有锁的排他性;volatile 关键字的使用不会引起上下文切换(这是 volatile 被冠以 “轻量级” 的原因)。
一、volatile 的作用
volatile 关键字的作用包括:保障可见性、有序性和 long/double 型变量读写操作的原子性。一般而言,对 volatile 变量的赋值操作,其右边表达式中只要涉及共享变量(包括被赋值的 volatile 变量本身),那么这个赋值操作就不是原子操作。要保障这样操作的原子我们仍需要借助锁。
volatile Map map = new HashMap(); 可以分解为如下伪代码所示的几个子操作:
【1】objRef = allocate(HashMap.class); //子操作①:分配对象所需的存储空间。
【2】invokeConstructor(objRef); //子操作②:初始化 objRef 引用的对象。
【3】map = objRef; //子操作③:将对象引用写入变量 map。
虽然 volatile 关键字仅保障其中的子操作③是一个原子操作,但是由于子操作①和子操作②仅涉及局部变量而未涉及共享变量,因此对变量 map 的赋值操作仍然是一个原子操作。
写线程对 volatile 变量的写操作会产生类似于释放锁的效果。读线程对 volatile 变量的读操作会产生类型于获得锁的效果。因此,volatile 具有保障有序性和可见性的作用。
对于 volatile 变量的写操作,Java 虚拟机会在该操作之前插入一个释放屏障[禁止 volatile 写操作与该操作之前的任何读、写操作进行重排序,从而保证了 volatile 写操作之前的任何读、写操作会先于 volatile 写操作被提交],并在该操作之后插入一个存储屏障[具有冲刷处理器缓存的作用,因此在 volatile 变量写操作之后插入的一个存储屏障就使得该存储屏障前所有操作的结果(包括 volatile 变量写操作及该操作之前的任何操作)对其他处理器来说是可同步的],如下图:
二、volatile 变量的开销
volatile 变量的开销包括读变量和写变量两个方面。volatile 变量的读、写操作都不会导致上下文切换,因此 volatile 的开销比锁要小。写一个 volatile 变量会使该操作以及该操作之前的任何写操作的结果对其他处理器是可同步的,因此 volatile 变量写操作的成本介于普通变量的写操作和在临界区进行的写操作之间。读取 volatile 变量的成本也比在临界区中读取变量要低(没有锁的申请与释放以及上下文切换的开销),但是其成本可能比读取普通变量要高一些。这是因为 volatile 变量的值每次都需要从高速缓存或者主内存中读取,而无法被暂存在寄存器中,从而无法发挥访问的高效性。
三、volatile 的典型应用场景
【场景一】:使用 volatile 变量作为状态标志。在该场景中,应用程序的某个状态由一个线程设置,其他线程会读取该状态并以该状态作为其计算的依据(或者仅仅读取并输出这个状态值)。此时使用 volatile 变量作为同步机制的好处是一个线程能够“通知”另外一个线程某种事件(例如,网络连接断连之后重新连上)的发生,而这些线程又无须使用锁,从而避免了锁的开销以及相关问题。
【场景二】:使用 volatile 保障可见性。在该场景中,多个线程共享一个可变状态变量,其中一个线程更新了该变量之后,其他线程在无须加锁的情况下也能够看到该更新。
【场景三】:使用 volatile 变量代替锁。volatile 关键字并非锁的代替品,但是在一定的条件下它比锁更合适(性能开销小,代码简单)。多个线程共享一组可变状态变量的时候,通常我们需要使用锁来保障对这些变量的更新操作的原子性,以避免产生不一致问题。利用 volatile 变量写操作具有的原子性,我们可以把这一组可变状态变量封装成一个对象,那么对这些状态变量的更新操作就可以通过创建一个新的对象并将该对象引用赋值给相应的引用型变量来实现。在这个过程中,volatile 保障了原子性和可见性,从而避免了锁的使用。
volatile 关键字并非锁的替代品,volatile 关键字和锁各有其使用条件。前者更适合与多个线程共享一个状态变量(对象),而后者更适合于多个线程共享一组状态变量。某些情形下,我们可以将多个线程共享的一组状态变量合并成一个对象,用一个 volatile 变量来引用该对象,从而使我们不必要使用锁。
【场景四】:使用 volatile 实现简易版读写锁。在该场景中,读写锁是通过混合使用锁和 volatile 变量而实现的,其中锁用于保障共享变量写操作的原子性,volatile 变量用于保障共享变量的可见性。因此,与 ReentrantReadWriteLock 所实现的读写锁不同的是,这种简易版读写锁仅涉及一个共享变量并且允许一个线程读取这个共享变量时其他线程可以更新该变量(这是因为读线程并没有加锁)。因此,这种读写锁允许读线程可以读取到共享变量的非最新值。该场景的一个典型例子是实现一个计数器:
四、volatile 实现可见性的原理
在 Java 并发编程中,一定绕不开 volatile、synchronize、lock 几个关键字,其中 volatile 关键字是用来解决共享变量(类成员变量、类的静态成员变量等)的可见性问题,非共享成员变量(局部变量)是分配在 JVM 虚拟机的栈中,是线程私有的,不涉及可见性问题。
【可见性】:在 JAVA规范中是这样定义的:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。通俗的讲就是如果有一个共享变量N,当有两个线程T1、T2同时获取了N的值,T1修改N的值,而T2读取N的值。那么可见性规范要求T2读取到的必须是T1修改后的值,而不能在T2读取旧值后T1修改为新值。volatile关键字修饰的共享变量可以提供这种可见性规范,也叫做读写可见。那么底层实现是通过机制保证 volatile变量读写可见的?Volatile的实现机制:在说这个问题之前,我们先看看CPU是如何执行 Java 代码的。