JAVA特性

volatile关键字

2017-03-14  本文已影响278人  一位不愿透露姓名的李小姐

参考链接:http://www.infoq.com/cn/articles/java-memory-model-4/
http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html
http://blog.csdn.net/feier7501/article/details/20001083

在Java的内存模式下,线程就把变量保存到本地的内存中,不直接和主存进行读写。这样就可能会出现有一个线程在贮存中修改了某变量,但是另一个线程读取的是它之前在本地内存里的拷贝值。这样就会造成数据不一致了。

volatile写-读的内存语义#

volatile写的内存语义如下:

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。

假设线程A首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。下图是线程A执行volatile写后,共享变量的状态示意图:

线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。

volatile读的内存语义如下:

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
下面是线程B读同一个volatile变量后,共享变量的状态示意图:

如上图所示,在读flag变量后,本地内存B已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值也变成一致的了。

如果我们把volatile写和volatile读这两个步骤综合起来看的话,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。

我以前看了这些之后就一直以为volatile是原子性的,可以实现线程同步的,事实上并不是这样的!!##

Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。

正确使用 volatile 变量的条件#

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

1)对变量的写操作不依赖于当前值。
2)该变量没有包含在具有其他变量的不变式中。

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

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

大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized 那样普遍适用于实现线程安全。

性能考虑#

使用 volatile 变量的主要原因是其简易性:
在某些情形下,使用 volatile 变量要比使用相应的锁简单得多。使用 volatile 变量次要原因是其性能:某些情况下,volatile 变量同步机制的性能要优于锁。

很难做出准确、全面的评价,例如 “X 总是比 Y 快”,尤其是对 JVM 内在的操作而言。(例如,某些情况下 VM 也许能够完全删除锁机制,这使得我们难以抽象地比较 volatile 和 synchronized 的开销。)

就是说,在目前大多数的处理器架构上,volatile 读操作开销非常低 —— 几乎和非 volatile 读操作一样。而 volatile 写操作的开销要比非 volatile 写操作多很多,因为要保证可见性需要实现内存界定(Memory Fence),即便如此,volatile 的总开销仍然要比锁获取低。

volatile 操作不会像锁一样造成阻塞,因此,在能够安全使用 volatile 的情况下,volatile 可以提供一些优于锁的可伸缩特性。

如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。

原文地址是:http://www.ibm.com/developerworks/cn/java/j-jtp06197.html
上链接原文里提到了很多模式(而我还没有明白的看懂:-O)

关于volatile的重排列#

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的前面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。

就是这样:

StoreStore屏障在volatile写之前,这样前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。

这里比较有意思的是volatile写后面的StoreLoad屏障。这个屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面,是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确实现volatile的内存语义,JMM在这里采取了保守策略:在每个volatile写的后面或在每个volatile读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里我们可以看到JMM在实现上的一个特点:首先确保正确性,然后再去追求执行效率。

上图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

这里有一段示例代码:

class VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;           //第一个volatile读
        int j = v2;           // 第二个volatile读
        a = i + j;            //普通写
        v1 = i + 1;          // 第一个volatile写
        v2 = j * 2;          //第二个 volatile写
    }

    …                    //其他方法
}

针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化:

注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器常常会在这里插入一个StoreLoad屏障。

以上大多数内容都来自http://www.infoq.com/cn/articles/java-memory-model-4/#anch95647
博主写的尤为精彩,尤其是评论里的交流也值得一看。

在评论 区里看到一个 解答了自己的困惑:

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。#

问题:设线程A和B共享变量x,线程B和C共享变量y,x和y是非volatile的,A和B线程之间共享volatile变量v,那么当B读取v的时候,B线程的本地内存里面的x被设为无效了,这点我理解,问题是,y是否也会被设为无效从而需要到主存中重新读取?

以下为博主的回答:

y也会被设为无效,从而需要到主存中重新读取.

其实:本地内存,主内存,设置本地内存为无效,从主内存中去读取值。这些都是为了让读者更形象生动的理解java内存模型而虚构出来的,并不真实存在。
对于你的问题,可以从volatile的编译器重排序规则和volatile的处理器内存屏障插入策略中找到答案。比如下面的程序代码:

int i = volatile; //1,volatile读
int j = a; //2,普通读(假设a为普通共享变量)

在这里,不管a是在哪些线程之间共享,volatile的编译器重排序规则和volatile的处理器内存屏障插入策略都会禁止2重排序到1的前面。

关于好多JMM,甚至volatile本身我也没有百分百看懂 。此篇仅为学习记录,存在大量知识是我摘抄的~

上一篇下一篇

猜你喜欢

热点阅读