多线程之 Volatile 变量 详解

Java 理论与实践: 正确使用 Volatile 变量

原文:http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

总结:

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

Volatile 变量

Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性(是指对I++这样的符合操作来说,对于某个值的单个操作,例如实例化和为long类型赋值,具有原子特性,即不会将指令重新排序)。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。

正确使用 volatile 变量的条件

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

  • 对变量的写操作不依赖于当前值。  (例如 I++)
  • 该变量没有包含在具有其他变量的不变式中。 (例如 下界总是小于或等于上界)


举例说明:

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

大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized 那样普遍适用于实现线程安全。清单 1 显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。

清单 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 类型是无法实现这一目的的。


正确使用 volatile 的模式

原则:要始终牢记使用 volatile 的限制 —— 只有在状态真正独立于程序内其他内容时才能使用 volatile —— 这条规则能够避免将这些模式扩展到不安全的用例。

正确用例1:(可见性特性)

实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志

volatile boolean shutdownRequested;

...

public void shutdown() { shutdownRequested = true; }

public void doWork() {
    while (!shutdownRequested) {
        // do stuff
    }
}

正确用例2:(不重排序特性)

线程安全的单例模式

  1. public class SingletonKerriganD {
  2. /**
  3. * 单例对象实例
  4. */
  5. private static SingletonKerriganD instance = null;
  6. public static SingletonKerriganD getInstance() {
  7. if (instance == null) {
  8. synchronized (SingletonKerriganD.class) {
  9. if (instance == null) {
  10. instance = new SingletonKerriganD();
  11. }
  12. }
  13. }
  14. return instance;
  15. }
  16. }

其他用例看原文。

正确用例3(高级):开销较低的读写锁策略。(volatile 的开销比synchronized 小,而且volatile 读操作,更小,其不包含所操作,几乎等于非volatile读)

如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。清单 6 中显示的线程安全的计数器使用synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the ‘this‘ lock held
    @GuardedBy("this") private volatile int value;

    public int getValue() { return value; }

    public synchronized int increment() {
        return value++;
    }
}
				
时间: 2024-07-29 06:41:31

多线程之 Volatile 变量 详解的相关文章

多线程之 Final变量 详解

原文: http://www.tuicool.com/articles/2Yjmqy 总结: Final 变量在并发当中,原理是通过禁止cpu的指令集重排序(重排序详解http://ifeve.com/java-memory-model-1/ http://ifeve.com/java-memory-model-2/),来提供现成的课件性,来保证对象的安全发布,防止对象引用被其他线程在对象被完全构造完成前拿到并使用. 与Volatile 有相似作用,不过Final主要用于不可变变量(基本数据类型

小编带您Volatile的详解

volatile关键字修饰的共享变量主要有两个特点:1.保证了不同线程访问的内存可见性 2.禁止重排序在说内存可见性和有序性之前,我们有必要看一下Java的内存模型(注意和JVM内存模型的区分)为什么要有java内存模型?首先我们知道内存访问和CPU指令在执行速度上相差非常大,完全不是一个数量级,为了使得java在各个平台上运行的差距减少,哪些搞处理器的大佬就在CPU上加了各种高速缓存,来减少内存操作和CPU指令的执行速度差距.而Java在java层面又进行了一波抽象,java内存模型将内存分为

mysql show variables系统变量详解

mysql系统变量详解 mysqld服务器维护两种变量.全局变量影响服务器的全局操作.会话变量影响具体客户端连接相关操作. 服务器启动时,将所有全局变量初始化为默认值.可以在选项文件或命令行中指定的选项来更改这些默认值.服务器启动后,通过连接服务器并执行SET GLOBAL var_name语句可以更改动态全局变量.要想更改全局变量,必须具有SUPER权限. 服务器还为每个客户端连接维护会话变量.连接时使用相应全局变量的当前值对客户端会话变量进行初始化.客户可以通过SET SESSION var

PATH环境变量和CLASSPATH环境变量详解

大凡装过JDK的人都知道要安装完成后要设置环境变量,可是为什么要设置环境变量呢?环境变量有什么作用? 1)PATH详解: 计算机安装JDK之后,输入“javac”“java”之类的命令是不能马上被计算机正确执行的,因为计算机不知道到哪里去找这两个命令. 计算机如何查找命令呢?Windows操作系统根据PATH环境变量来查找命令.PATH环境变量的值是一系列路径,Windows操作系统将在这一系列的路径中依次查找命令,如果能找到这个命令,则该命令是可执行的:否则将出现“‘XXX’不是内部命令或外部

java环境变量详解---找不到或无法加载主类

刚学java,配置好环境变量之后,在DOS下却运行java小程序却始终出现"找不到或无法加载主类"然后返回配置环境变量折腾了好久,查看书籍.网上的资料,最终OK了!安装Eclipse始终出现 "could not find Java SE runtime environment"."could not find java dll"这样的问题,按照网上提供的资料却始终解决不了问题,最后将最新版本的Eclipse换了,才可以用. 以下是安装JDK的总

Shell学习之Bash变量详解(二)

Shell学习之Bash变量详解 目录 Bash变量 Bash变量注意点 用户自定义变量 环境变量 位置参数变量 预定义变量 Bash变量 用户自定义变量:在Bash中由用户定义的变量. 环境变量:这种变量中主要保存和系统操作环境相关的数据. 位置参数变量:这种变量主要是用来向脚本当中传递参数或数据的,变量名不能自定义,变量作用是固定的. 预定义变量:是Bash中已经定义好的变量,变量名不能自定义,变量作用也是固定的. Bash变量注意点 1.变量名称可以由字母.数字和下划线组成,但是不能以数字

【转】asp.net core环境变量详解

环境变量详解 Windows操作系统的环境变量在哪设置应该都知道了. Linux(centos版本)的环境变量在/etc/profile里面进行设置.用户级的环境变量在其它文件里面,不多说了,有兴趣的可以网上查下. 当我们的应用程序发布到生产环境时,如果用到了环境变量,就需要对服务器操作系统的环境变量进行设置.这里只是进行提前说明. 设置环境变量 环境变量可以在launchSettings.json文件里面设置,也可以右键项目->属性->调试->环境变量进行设置.这两种方式的内容是同步的

Java中Volatile关键字详解

一.基本概念 先补充一下概念:Java并发中的可见性与原子性 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情.为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的.也就是一个线程修改的结果.另一个线程马上就能看到.比如:用volatile修饰的变量,就会具有可见性.volatile修饰的变量不允许线程

Java多线程:volatile变量、happens-before关系及内存一致性

什么是 Volatile 变量?Volatile 是 Java 中的一个关键字.你不能将它设置为变量或者方法名,句号. 认真点,别开玩笑,什么是 Volatile 变量?我们应该什么时候使用它?哈哈,对不起,没法提供帮助. volatile 关键字的典型使用场景是在多线程环境下,多个线程共享变量,由于这些变量会缓存在 CPU 的缓存中,为了避免出现内存一致性错误而采用 volatile 关键字. 考虑下面这个生产者/消费者的例子,我们每次生成/消费一个元素: 1 2 3 4 5 6 7 8 9