Java多线程(3) Volatile的实现原理

Volatile变量

程序设计中,尤为是在C语言C++C#Java语言中,使用volatile关键字声明的变量对象一般拥有和优化和(或)多线程相关的特殊属性。一般,volatile关键字用来阻止(伪)编译器对某些其认为没法“被代码自己”改变的代码(变量/对象)进行优化。如在C语言中,volatile关键字能够用来提醒编译器它后面所定义的变量随时有可能改变,所以编译后的程序每次须要存储或读取这个变量的时候,都会直接从变量地址中读取数据。若是没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,若是这个变量由别的程序更新了的话,将出现不一致的现象。 在C环境中,volatile关键字的真实定义和适用范围常常被误解。虽然C++、C#和Java都从C中神秘地“继承”了volatile,在这些编程语言中volatile的用法和语义却截然不同。【维基百科】java

 

通俗点讲解上面这句话,意思就是,编译器为了快速读写,会将数据放到寄存器缓存中,而使用了volatile修饰之后,告诉编译器,不要对该变量进行优化,每次读取都从内存中去读取。编程

 

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只容许一个线程持有某个特定的锁,所以可以使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程可以使用该共享数据。可见性要更加复杂一些,它必须确保释放锁以前对共享数据作出的更改对于随后得到该锁的另外一个线程是可见的 —— 若是没有同步机制提供的这种可见性保证,线程看到的共享变量多是修改前的值或不一致的值,这将引起许多严重问题。缓存

 

Volatile 变量具备 synchronized 的可见性特性,可是不具有原子特性。这就是说线程可以自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,可是只能应用于很是有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。所以,单独使用 volatile 还不足以实现计数器、互斥锁或任何具备与多个变量相关的不变式(Invariants)的类安全

 

 

1.1 正确使用 volatile 变量的条件

您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时知足下面两个条件:多线程

  • 对变量的写操做不依赖于当前值。
  • 该变量没有包含在具备其余变量的不变式中。

第一个条件的限制使 volatile 变量不能用做线程安全计数器。虽然增量操做(x++)看上去相似一个单独操做,实际上它是一个由读取-修改-写入操做序列组成的组合操做,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操做须要使 x 的值在操做期间保持不变,而 volatile 变量没法实现这点。(然而,若是将值调整为只从单个线程写入,那么能够忽略第一个条件。)架构

清单 1. 非线程安全的数值范围类
@NotThreadSafe 
public class NumberRange {
    private int lower, upper;

    public int getLower() { return lower; }
    public int getUpper() { return upper; }

    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException(...);
        lower = value;
    }

    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException(...);
        upper = value;
    }
}

这种方式限制了范围的状态变量,所以将 lower 和 upper 字段定义为 volatile 类型不可以充分实现类的线程安全;从而仍然须要使用同步。不然,若是凑巧两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态。例如,若是初始状态是(0, 5),同一时间内,线程 A 调用 setLower(4) 而且线程 B 调用 setUpper(3),显然这两个操做交叉存入的值是不符合条件的,那么两个线程都会经过用于保护不变式的检查,使得最后的范围值是 (4, 3) —— 一个无效值。至于针对范围的其余操做,咱们须要使 setLower()和 setUpper() 操做原子化 —— 而将字段定义为 volatile 类型是没法实现这一目的的。  并发

   

1.2  性能考虑

很难作出准确、全面的评价,例如 “X 老是比 Y 快”,尤为是对 JVM 内在的操做而言。(例如,某些状况下 VM 也许可以彻底删除锁机制,这使得咱们难以抽象地比较 volatile 和 synchronized 的开销。)就是说,在目前大多数的处理器架构上,volatile 读操做开销很是低 —— 几乎和非 volatile 读操做同样。而 volatile 写操做的开销要比非 volatile 写操做多不少,由于要保证可见性须要实现内存界定(Memory Fence),即使如此,volatile 的总开销仍然要比锁获取低。编程语言

 

不少并发性专家事实上每每引导用户远离 volatile 变量,由于使用它们要比使用锁更加容易出错。然而,若是谨慎地遵循一些良好定义的模式,就可以在不少场合内安全地使用 volatile 变量。要始终牢记使用 volatile 的限制 —— 只有在状态真正独立于程序内其余内容时才能使用 volatile —— 这条规则可以避免将这些模式扩展到不安全的用例。性能

 

1.3 正确使用 volatile 的模式

清单 2. 将 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)。此外,还须要某些原子状态转换机制,例如原子变量。

 

 

与锁相比,Volatile 变量是一种很是简单但同时又很是脆弱的同步机制,它在某些状况下将提供优于锁的性能和伸缩性。若是严格遵循 volatile 的使用条件 —— 即变量真正独立于其余变量和本身之前的值 —— 在某些状况下可使用 volatile 代替 synchronized 来简化代码。然而,使用 volatile 的代码每每比使用锁的代码更加容易出错。本文介绍的模式涵盖了可使用 volatile 代替 synchronized 的最多见的一些用例。遵循这些模式(注意使用时不要超过各自的限制)能够帮助您安全地实现大多数用例,使用 volatile 变量得到更佳性能。

相关文章
相关标签/搜索