BT

深入理解Java内存模型(四)——volatile

作者 发布于 2013年2月5日 | ArchSummit全球架构师峰会(北京站)2016年12月02-03日举办,了解更多详情!

volatile的特性

当我们声明共享变量为volatile后,对这个变量的读/写将会很特别。理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步。下面我们通过具体的示例来说明,请看下面的示例代码:

class VolatileFeaturesExample {
    volatile long vl = 0L;  //使用volatile声明64位的long型变量

    public void set(long l) {
        vl = l;   //单个volatile变量的写
    }

    public void getAndIncrement () {
        vl++;    //复合(多个)volatile变量的读/写
    }


    public long get() {
        return vl;   //单个volatile变量的读
    }
}

假设有多个线程分别调用上面程序的三个方法,这个程序在语意上和下面程序等价:

class VolatileFeaturesExample {
    long vl = 0L;               // 64位的long型普通变量

    public synchronized void set(long l) {     //对单个的普通 变量的写用同一个监视器同步
        vl = l;
    }

    public void getAndIncrement () { //普通方法调用
        long temp = get();           //调用已同步的读方法
        temp += 1L;                  //普通写操作
        set(temp);                   //调用已同步的写方法
    }
    public synchronized long get() { 
    //对单个的普通变量的读用同一个监视器同步
        return vl;
    }
}

如上面示例程序所示,对一个volatile变量的单个读/写操作,与对一个普通变量的读/写操作使用同一个监视器锁来同步,它们之间的执行效果相同。

监视器锁的happens-before规则保证释放监视器和获取监视器的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

监视器锁的语义决定了临界区代码的执行具有原子性。这意味着即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读写就将具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。

简而言之,volatile变量自身具有下列特性:

  • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

volatile写-读建立的happens before关系

上面讲的是volatile变量自身的特性,对程序员来说,volatile对线程的内存可见性的影响比volatile自身的特性更为重要,也更需要我们去关注。

从JSR-133开始,volatile变量的写-读可以实现线程之间的通信。

从内存语义的角度来说,volatile与监视器锁有相同的效果:volatile写和监视器的释放有相同的内存语义;volatile读与监视器的获取有相同的内存语义。

请看下面使用volatile变量的示例代码:

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;                   //1
        flag = true;               //2
    }

    public void reader() {
        if (flag) {                //3
            int i =  a;           //4
            ……
        }
    }
}

假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens before规则,这个过程建立的happens before 关系可以分为两类:

  1. 根据程序次序规则,1 happens before 2; 3 happens before 4。
  2. 根据volatile规则,2 happens before 3。
  3. 根据happens before 的传递性规则,1 happens before 4。

上述happens before 关系的图形化表现形式如下:

在上图中,每一个箭头链接的两个节点,代表了一个happens before 关系。黑色箭头表示程序顺序规则;橙色箭头表示volatile规则;蓝色箭头表示组合这些规则后提供的happens before保证。

这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。

volatile写-读的内存语义

volatile写的内存语义如下:

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

以上面示例程序VolatileExample为例,假设线程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读的内存语义做个总结:

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

volatile内存语义的实现

下面,让我们来看看JMM如何实现volatile写/读的内存语义。

前文我们提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。下面是JMM针对编译器制定的volatile重排序规则表:

是否能重排序 第二个操作
第一个操作 普通读/写 volatile读 volatile写
普通读/写     NO
volatile读 NO NO NO
volatile写   NO NO

举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。

从上表我们可以看出:

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

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

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。

下面是保守策略下,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在实现上的一个特点:首先确保正确性,然后再去追求执行效率。

下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:

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

上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变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屏障。

上面的优化是针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以x86处理器为例,上图中除最后的StoreLoad屏障外,其它的屏障都会被省略。

前面保守策略下的volatile读和写,在 x86处理器平台可以优化成:

前文提到过,x86处理器仅会对写-读操作做重排序。X86不会对读-读,读-写和写-写操作做重排序,因此在x86处理器中会省略掉这三种操作类型对应的内存屏障。在x86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在x86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。

JSR-133为什么要增强volatile的内存语义

在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量之间重排序。在旧的内存模型中,VolatileExample示例程序可能被重排序成下列时序来执行:

在旧的内存模型中,当1和2之间没有数据依赖关系时,1和2之间就可能被重排序(3和4类似)。其结果就是:读线程B执行4时,不一定能看到写线程A在执行1时对共享变量的修改。

因此在旧的内存模型中 ,volatile的写-读没有监视器的释放-获所具有的内存语义。为了提供一种比监视器锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和监视器的释放-获取一样,具有相同的内存语义。从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语意,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。

由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而监视器锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,监视器锁比volatile更强大;在可伸缩性和执行性能上,volatile更有优势。如果读者想在程序中用volatile代替监视器锁,请一定谨慎。

参考文献

  1. Concurrent Programming in Java™: Design Principles and Pattern
  2. JSR 133 (Java Memory Model) FAQ
  3. JSR-133: Java Memory Model and Thread Specification
  4. The JSR-133 Cookbook for Compiler Writers
  5. Java 理论与实践: 正确使用 Volatile 变量
  6. Java theory and practice: Fixing the Java Memory Model, Part 2

作者简介

程晓明,Java软件工程师,国家认证的系统分析师、信息项目管理师。专注于并发编程,就职于富士通南大。个人邮箱:asst2003@163.com


感谢张龙对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

这段代码的设计本身就是很失败的,getAndIncrement方法的调用,两次this锁,作者是想故意放大volatile带来的性能消耗么? by 汪 文君

class VolatileFeaturesExample {
long vl = 0L; // 64位的long型普通变量

public synchronized void set(long l) { //对单个的普通 变量的写用同一个监视器同步
vl = l;
}

public void getAndIncrement () { //普通方法调用
long temp = get(); //调用已同步的读方法
temp += 1L; //普通写操作
set(temp); //调用已同步的写方法
}
public synchronized long get() {
//对单个的普通变量的读用同一个监视器同步
return vl;
}
}

Re: 这段代码的设计本身就是很失败的,getAndIncrement方法的调用,两次this锁,作者是想故意放大volatile带来的性能消耗么? by 程 晓明

“volatile的特性”这一章节是为了说明volatile变量所具有的内存可见性和原子性。

我在文章中已经说了,上面的VolatileFeaturesExample程序在语义上和下面的VolatileFeaturesExample程序等价。

下面这个程序是为了说明volatile所具有的特性。并不是说我们要这样写程序,而是想说明:当我们声明一个变量为volatile后,多个线程对这个变量的读/写将能保证内存可见性和原子性;但是,volatile++这种复合操作整体上不具有原子性。下面的这个VolatileFeaturesExample程序可以很清楚的展示这些特性。

深入理解Java内存模型(四)——volatile by zou chun

public void writer() {
a = 1; //1
flag = true; //2
}

public void reader() {
if (flag) { //3
int i = a; //4
……
}
}

flag是volatile,对于这段代码,public void writer() {
a = 1; //1
flag = true; //2
},cpu的并发特性会不会重排序为flag = true; a = 1;??还是说falg是volatile,就相当于已经有一个内存屏障,必须这样执行,即a = 1; flag = true;

Re: 深入理解Java内存模型(四)——volatile by 程 晓明

你的后一个说法“比较”正确。
-----------------------------------------------------------------------------------------------
writer() 方法有两个操作,它们在程序中的执行顺序如下:
1:普通写操作
2:volatile写操作
由于重排序分为编译器重排序和处理器重排序(见本文第一章),所以我们需要分别从这两个方面来分析这个问题。
-----------------------------------------------------------------------------------------------
查看本文中的volatile的重排序规则表,我们会发现JMM禁止编译器对(普通写-volatile写)这种情况做重排序。
所以,编译器在这里不会重排序这两个操作。
-----------------------------------------------------------------------------------------------
对于处理器的重排序,要看java是在那种处理器平台上执行。
对于那些会对写-写操作做重排序的处理器平台,JMM会插入一个StoreStore屏障,通过这个屏障来禁止写-写重排序。
反之,对于那些不会对写-写操作做重排序的处理器平台,JMM会省略StoreStore屏障。

具体来说,JMM在sparc-TSO平台和x86平台上,会省略StoreStore屏障;
而JMM在ia64平台和PowerPC平台上,会插入StoreStore屏障。
之所以这样做的原因,请参考第一章的“处理器重排序与内存屏障指令”一节。

有个迷惑向作者请教一下 by 杨 亮

比如现在有A、B两个线程,A线程读取i对象的x属性(valutile x)并更新x的值,但再更新x的值之前B线程已经将x的值读走了,这时怎么保证B线程得到的值是最新的呢?
第二个问题类似:如果A、B线程都读取了x属性的值,同时更新x的值,这时是如何处理的呢?

Re: 有个迷惑向作者请教一下 by 程 晓明

大家在这里互相交流,谈不上请教的。
*********************************************************
第一个问题
A线程写一个变量,B线程读这个变量。这种情况会存在数据竞争。
要想保证B线程总是读取到变量的最新值,需要做同步处理。
可以用锁来同步,或者声明共享变量为volatile。
让我们以volatile为例,来分析两种可能的执行时序:
-----------------------------------------------------------------
执行时序一:
时间t1:线程A写一个volatile变量
时间t2:线程B读这个volatile变量
以这种时序执行,线程B一定能读到volatile变量(在时间t2这个时间点所具有)的最新值。
这个值是线程A写入后的最新值。
------------------------------------------------------------------------
执行时序二:
时间t1:线程B读一个volatile变量
时间t2:线程A写这个volatile变量
以这种时序执行,线程B一能读到volatile变量(在时间t1这个时间点所具有)的最新值。
但在这里,线程B读取到的是线程A写入之前的值。
这是程序语义层面需要处理的问题,与内存模型的内存可见性问题无关
**********************************************************************
第二个问题
你是不是想问,如何避免出现下面这种情况:
A、B线程并发执行:
1:都同时读取了x值;
2:然后都做一些逻辑判断;
3:最后A、B线程同时更新x的值
要避免出现这个问题,只要能保证这3个步骤能够原子性执行即可。

现代的处理器(1991年以后的处理器),都会提供保证对数据的读取-比对-修改操作能够原子执行的机器指令。
这些机器级别的原子指令分为两类:CAS 和 LL/SC。
Intel,AMD 和 SPARC 的多处理器系统支持“比较并交换”(compare-and-swap,CAS指令)。
IBM PowerPC,Alpha AXP,MISP 和 ARM 的多处理器系统支持“加载链接 / 存储条件”(load-linked/store-conditional,LL/SC指令)。

从JDK5开始提供的java.util.concurrent.atomic包中,提供了能够对变量做原子条件更新操作的类。
这些原子类,对上面这些机器级别的原子指令做了封装。
这些原子类在其compareAndSet()方法中,会使用上面这些机器级别的原子指令来原子性的执行读取-比对-修改操作。

Re: 有个迷惑向作者请教一下 by 杨 亮

关于第一个问题的执行时序二,在一个其他资料上这样写:任何呗volatile修饰的变量,都不拷贝副本到工作内存,任何修改都及时写在主存。按照这样说的话,按照执行时序二也不会有问题,线程B在去使用变量时就是主存中最新值。
关于第二个问题,我想问的就是你说的这种情况。
我这样理解对不对: 其实没有真正意义上的A、B线程同时更新X的值,比如A线程去更新x的值,这时会获得一个锁,B线程再去更新时就会读取-比对-修改。
我的这种理解有问题吗?我在多看看相关的资料。

Re: 有个迷惑向作者请教一下 by 程 晓明

你这段关于volatile特性的描述的原始出处来自于:
《The JavaTM Virtual Machine Specification Second Edition》
在线版本在:docs.oracle.com/javase/specs/jvms/se5.0/html/Th...
“8.7 Rules for volatile Variables”这一节的最后一句话,就是原始出处。
请结合上下文(8.7节,最好是整个第8章),来理解《JVM规范第二版》对volatile在java旧内存模型中所具有特性的描述。

Brian Goetz在《JSR 133 (Java Memory Model) FAQ》中,对volatile做了简洁,清晰的描述:
www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-f...

另外,Brian Goetz写过一篇很棒的文章。
这篇文章说明了应该在什么情况下,怎么样使用volatile才是正确的:
www.ibm.com/developerworks/cn/java/j-jtp06197.html
**************************************************************************
第二个问题
对变量的同时更新的问题,要看你是指某个时间点,还是某个时间段。

我们先考虑时间点。
在任意时间点,不可能有两个处理器能同时访问内存。
因为处理器总线会同步多个处理器对内存的并发访问(请参阅我在这个系列的第三篇中,对处理器总线工作机制的描述)。
因此在任意时间点,不可能有两个线程能同时更新X的值。

再考虑时间段。
在某个时间段,两个线程有可能会同时去读/写同一个共享变量。
比如有一个long型的共享变量l被A,B线程并发访问,同时程序没有做任何同步(指广义上的同步)。
在一些32位的处理器中,程序有可能按下列时序执行:
时间点1:首先,A写l的高32位;
时间点2:然后,B写l的高32位;
时间点3:接着,B写l的低32位;
时间点4:最后,A写l的低32位。
在1到4这个时间段,A,B线程在同时在写变量l;但在1-4中的任意一个时间点,只有一个线程能访问变量l。
以这个时序来执行后,l将变成一个“四不像”(高32位是B线程写的,低32位是A线程写的)。
要避免出现这种问题,只需要对这个多线程程序做正确同步(指广义上的同步)即可。
对于正确同步的多线程程序,java内存模型确保不会出现上述问题。

有一点内容应该是写错了 by Wang Deo

“在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。”应该是“在每个volatile读操作的前面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。”吧?

作者写的很好啊,把多年没搞清楚的volatile对指令重排序的影响搞清楚了。

Re: 有一点内容应该是写错了 by Wang Deo

唔,是我自己搞错了。。sorry。。。

Re: 深入理解Java内存模型(四)——volatile by Wang Deo

你的后一个说法“比较”正确。
-----------------------------------------------------------------------------------------------
writer() 方法有两个操作,它们在程序中的执行顺序如下:
1:普通写操作
2:volatile写操作
由于重排序分为编译器重排序和处理器重排序(见本文第一章),所以我们需要分别从这两个方面来分析这个问题。
-----------------------------------------------------------------------------------------------
查看本文中的volatile的重排序规则表,我们会发现JMM禁止编译器对(普通写-volatile写)这种情况做重排序。
所以,编译器在这里不会重排序这两个操作。
-----------------------------------------------------------------------------------------------
对于处理器的重排序,要看java是在那种处理器平台上执行。
对于那些会对写-写操作做重排序的处理器平台,JMM会插入一个StoreStore屏障,通过这个屏障来禁止写-写重排序。
反之,对于那些不会对写-写操作做重排序的处理器平台,JMM会省略StoreStore屏障。

具体来说,JMM在sparc-TSO平台和x86平台上,会省略StoreStore屏障;
而JMM在ia64平台和PowerPC平台上,会插入StoreStore屏障。
之所以这样做的原因,请参考第一章的“处理器重排序与内存屏障指令”一节。


那也就是说"1 happens before 2"这个结论不是由happens-before规则推导来的而是由volatile的语义推导来的?那么我们应该怎么理解happens before里面的规则:“Each action in a thread happens before every action in that thread that comes later in the program's order.”?

Re: 深入理解Java内存模型(四)——volatile by 程 晓明

--"1 happens before 2"这个结论是由happens-before的程序顺序规则推导出来的。因为在程序顺序中1排在2的前面。
*********************************************************
--怎么理解happens before里面的程序顺序规则
程序顺序规则有两个作用:
1:java内存模型通过这个规则,可以向程序员保证:单线程内不会出现内存可见性问题。因为java内存模型保证,一个线程中的每个操作,都happens-before于程序顺序排在它后面的操作。

2:程序顺序规则与其他规则组合后,可以提供“额外”的保证。
比如在本文的第三章中的“volatile的写-读建立的happens before关系”这一节中,程序顺序规则与volatile规则及传递性规则三者组合后,可以保证:一个线程在写volatile变量之前可见的所有共享变量,在接下来另一个线程读同一个volatile变量后,将立即变得对这个线程可见。
*************************************************
读者在最初的提问中,问的是volatile内存语义的具体实现的问题,所以我回答的是volatile内存语义是如何实现的。

注意,happens-before规则和volatile内存语义的实现不能混淆。
happens-before是java内存模型向我们提供的内存可见性保证;
而volatile内存语义的实现(包括volatile的编译器重排序规则和volatile的内存屏障插入策略),
可以理解为java内存模型如何去实现这些happens-before。

比如在那个程序中,根据happens-before的程序顺序规则:1 happens-before 2 ;3 happens-before 4.
根据happens-before的volatile规则:2 happens-before 3.
因此,有如下happens-before关系:1->2->3->4
volatile的编译器重排序规则和volatile的内存屏障插入策略,可以确保上述happens-before顺序。

Re: 深入理解Java内存模型(四)——volatile by Wang Deo

非常谢谢作者的耐心解答,很nice!!

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

楼主,针对“当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效”,我设想了一个场景,希望您能帮忙解答下:
设线程A和B共享变量x,线程B和C共享变量y,x和y是非volatile的,A和B线程之间共享volatile变量v,那么当B读取v的时候,B线程的本地内存里面的x被设为无效了,这点我理解,问题是,y是否也会被设为无效从而需要到主存中重新读取?

谢谢了先。

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

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

其实:本地内存,主内存,设置本地内存为无效,从主内存中去读取值。这些都是为了让读者更形象生动的理解java内存模型而虚构出来的,并不真实存在。

对于你的问题,可以从volatile的编译器重排序规则和volatile的处理器内存屏障插入策略中找到答案。比如下面的程序代码:
int i = volatile; //1,volatile读
int j = a; //2,普通读(假设a为普通共享变量)
在这里,不管a是在哪些线程之间共享,volatile的编译器重排序规则和volatile的处理器内存屏障插入策略都会禁止2重排序到1的前面。

请问:1和2,5和6之间会重排序吗?谢谢! by Jim Alan

class VolatileExample {
int a = 0;
int b = 0;
volatile boolean flag = false;

public void writer() {
a = 1; //1
b = 2 //2
flag = true; //3
}

public void reader() {
if (flag) { //4
int i = a; //5
int j = b; //6
……
}
}
}
还是说要看不同的CPU?谢谢!

整理了一下刚刚的代码,关键是以下两个问题,谢谢! by Jim Alan

文章中只说了当第二个操作为volatile写时,不会对第一个普通写操作与这个volatile写操作进行重排序,那如果这个第一个操作之前还有一个另外一个普通变量的写操作呢?如同刚刚的代码,现在知道操作3和2是不会进行重排序的;
现在问题是1:编译器是否会对2与3重排序;2:处理器会不会对2与3重排序,请分别解答下,谢谢!!!
另外还有5与6之间会不会进行重排序

volatile by Sean Xj

“这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。”
我对这句话不是很理解, 为什么哪些共享变量在B读之后就变得对B可见了呢? 是不是我的理解错了

Re: 整理了一下刚刚的代码,关键是以下两个问题,谢谢! by 程 晓明

编译器是否会对2与3重排序
--我估计你应该是想问:编译器是否会对1与2重排序
编译器可能会对1和2重排序。
因为1和2是两个普通变量,且没有数据依赖关系。

处理器会不会对2与3重排序
--我估计你应该是想问:处理器是否会对1与2重排序
由于这里的1与2之间没有数据依赖关系,是否被处理器重排序取决于具体的处理器平台。
不会对写-写操作重排序的处理器,不会对这两个操作做重排序;
反之,允许写-写操作重排序的处理器,可能会对这两个操作重排序。
处理器重排序的具体细节,请参考第一章的“处理器重排序与内存屏障指令”。

编译器是否会对5与6重排序
--编译器可能会对5和6做重排序。
因为5和6是两个普通变量,且没有数据依赖关系。

处理器会不会对5与6重排序
--由于这里的5与6之间没有数据依赖关系,是否被处理器重排序取决于具体的处理器平台。
不会对读-读操作重排序的处理器,不会对这两个操作重排序;
反之,允许对读-读操作重排序的处理器,可能会对这两个操作重排序。

Re: 整理了一下刚刚的代码,关键是以下两个问题,谢谢! by Jim Alan

嗯,是1与2,写错了,对你造成的困扰表示道歉,对不起!另外:谢谢您耐心的解答!

Re: volatile by 程 晓明

这句话是对volatile内存语义的“浓缩”,以下面的VolatileExample程序为例:
class VolatileExample {
int a = 0;
volatile boolean flag = false;

public void writer() { //线程A执行
…… //0
a = 1; //1
flag = true; //2
}

public void reader() { //线程B执行
if (flag) { //3
int i = a; //4
…… //5
}
}
}
假设线程A执行writer()方法之后,线程B执行reader()方法。
根据程序顺序规则,1 happens-before 2;3 happens-before 4
根据volatile规则,2 happens-before 3
根据传递性规则,1 happens-before 3
这里我们重点关注这个“1 happens-before 3”。
这个happens-before关系意味着:线程A在2这个时点(写volatile变量)可见的所有的共享变量(包括0和1,以及所有程序顺序排在2前面的对共享变量的修改操作),在线程B在3这个时点(读这个volatile变量)后,都将都将确保对线程B可见了。
因为1 happens-before 3,程序顺序排在1前面的操作必然会happens-before 3(比如根据程序顺序规则和传递性可以推导出0 happens-before 3)。
根据程序顺序规则,按程序顺序排在3后面的4以及更后面的5等等,将确保能看到线程A在2这个时点可见的所有的对共享变量的修改(比如根据程序顺序规则和传递性推导出3 happens-before 4和3 happens-before 5)。
综合来看,0和1 happens-before 4和5。换句话来说,线程A在2这个时点可见的所有共享变量,在线程B在3这个时点开始都将确保对线程B可见。

volatile修饰对象引用和数组 by sc lv

在“深入理解Java内存模型(一)——基础”中提到“共享变量”这个术语代指实例域,静态域和数组元素。
1. www.ibm.com/developerworks/cn/java/j-jtp06197.h... volatile 类型,volatile 类型的引用可以确保对象的发布形式的可见性(我理解的是获取一个完全构造的对象了),除了这个可见性,对象引用声明为volatile还有其它作用么?例如java.io.FilterInputStream中的“protected volatile InputStream in;”,后面又说“但是如果对象的状态在发布后将发生更改,那么就需要额外的同步“。
2.看到有地方这样说"volatile的数组只针对数组的引用具有volatile的语义,而不是它的元素",例如,java.io.BufferedInputStream中的”protected volatile byte buf[];”,加在数组的引用上有什么作用?
目前,对这两方面不是很清楚,希望得到您的回答。

Re: volatile修饰对象引用和数组 by sc lv

1中的链接和文字重合了,修正一下www.ibm.com/developerworks/cn/java/j-jtp06197.html
一次性安全发布”,将对象引用定义为volatile 类型。。。

Re: volatile修饰对象引用和数组 by 程 晓明

问题1
--------------------------
除了这个可见性,对象引用声明为volatile还有其它作用么?
--其他作用和普通volatile变量类似。
比如,任意线程都可以看到这个对象引用的最新值,以及对这个对象引用的写-读可以实现线程之间的通信。
---------------------------
“但是如果对象的状态在发布后将发生更改,那么就需要额外的同步“
--volatile引用可以保证任意线程都可以看到这个对象引用的最新值,但不保证能看到被引用对象的成员域的最新值。
虽然volatile对象引用可以保证对象的安全发布,但是无法保证对象安全发布以后,某个线程对这个对象的状态(指对象的成员域)的更改,能够被其他线程看到。
比如,假设程序按如下时序来执行:
时间点t1:线程A执行initInBackground()方法
时间点t2:线程B修改theFlooble对象引用所引用的Flooble对象的状态(修改Flooble对象的成员域)
时间点t3:线程C执行doWork()方法
这里,由于对象引用theFlooble是volatile,将保证能安全发布对象。也就是说线程C至少能看到线程A对theFlooble所引用对象的写入。
但如果线程B和线程C没有适当的同步的话,线程B对Flooble对象的状态的修改,线程C不一定能看的到。
*******************************
问题2
加在数组的引用上有什么作用?
--加在数组引用上的volatile可以保证任意线程都能看到这个数组引用的最新值(但不保证数组元素的可见性)。
比如,假设一个程序按如下时序来执行:
时间点t1:线程A设置数组引用指向一个数组
时间点t2:线程B修改数组引用指向另一个数组
时间点t3:线程C读这个数组引用
这里,如果这个数组引用为volatile,那么线程C必定能看到线程B对数组引用的修改。
但如果数组引用没有用volatile来声明,就没有这个保证了。

Re: volatile by he haibo

1 请教下 要是线程B先执行render()方法,线程A再执writer方法() ,执行的顺序和上面说的是一样的吗?

2 要是将示例改成成如下代码
public class VolatileExampleExt {
int a=0;
volatile boolean flag=false;

public void render(){//线程B
if(flag){//3
int i=a;//4
...//5
}
}
public void writer(){//线程A
..../0
a=1; //1
flag=true; //2
}
}
然后还是线程A开始执行writer()方法,线程B再执行render()方法?结果还是一样的吗?
源代码到字节码的生成再到内存指令的生成,和源代码的顺序有关系吗?

每次读取volatile变量时,每次都会从内存中读取数据吗 by zhang fei

“当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。”

Re: volatile by Zhefu Zhou

根据volatile规则,2 happens-before 3
————————————————————
这句话不能理解。volatile能够有这样的规则?我举个例子:

线程A (0ms处开始执行)
int a = 1; //1:耗时4ms
flag = true; //2:耗时1ms
线程B (1ms处开始执行)
if (flag) { //3:耗时1ms
int i = a; //4:耗时1ms
}

由于1+2并不是同步操作,当1还剩3ms的时候,3开始执行,接着执行4,3+4都执行完了,1还要过1ms才能完成,并接着执行2。这种情形下除非发生2-1重排,2 happens-before 3是不正确的,也没有什么volatile规则可以帮上忙,因为1+2和3+4并不是volatile的。

Re: volatile by 程 晓明

回复晚了,不好意思。

1:如果线程B先执行reader()方法,然后线程A再执行writer()方法,执行顺序不能保证会和上面一样。
因为,根据happens-before的程序顺序规则,可以建立下面的happens-before关系:
0->1->2
3->4->5
由于在你的这个假设条件中,2和3之间不存在happens-before关系,也就无法保证(0,1,2)->(3,4,5)了

2:在当前讨论的上下文中(线程A执行write()方法之后,线程B执行reader()方法),结果是一样的。

与源代码的顺序是否有关系,要看具体的上下文。
比如,我们假设线程A执行write()方法之后,线程B执行reader()方法。在这种假设条件下,与这两个方法在源代码中的顺序无关。
再比如,假如我们讨论的是write()方法中的三个操作(0,1和2)时,此时就会与源代码顺序相关了。

Re: 每次读取volatile变量时,每次都会从内存中读取数据吗 by 程 晓明

如果从抽象的角度来理解volatile的话,可以这样理解。

下面,我们再从具体实现的角度来理解volatile。
《Java Concurrency in Practice》在“3.1.4. Volatile Variables”中,对volatile有如下描述:
Volatile variables are not cached in registers or in caches where they are hidden from other processors, so a read of a volatile variable always returns the most recent write by any thread.
上面这段话的大意是说:
volatile变量不会被“缓存”在寄存器或“缓存”在对其他处理器不可见的地方,
因此当前线程对一个volatile变量的读,总是能读取到任意线程对这个volatile变量最后的写入。

Re: volatile by 程 晓明

volatile有这个happens-before规则。
在《JSR-133: Java Memory Model and Thread Specification》的“3 Informal Semantics”,
和《The Java Language Specification Third Edition》的“17.4.5 Happens-before Order”中,都定义了下面的volatile规则:
A write to a volatile field happens-before every subsequent read of that volatile.
上面这句话的大意是说:
对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
注意,volatile规则需要一个前提条件:(一个线程)写一个volatile之后,(任意线程)读这个volatile变量。

我在上面的回复中,给出了下面的前提条件:
假设线程A执行writer()方法之后,线程B执行reader()方法。
这个前提条件可以保证:A线程(在操作2)写volatile变量之后,B线程(在操作3)读这个volatile变量。
参照JMM定义的volatile规则,我们可以得出:2 happens-before 3

方法调用会被重排吗? by Li Gui

关于指令重排一直有个疑问:方法调用会不会被重排?
假设 v 是 volatile的,doSomething()不涉及 v 的操作。
那么
doSomething();//1
v=true;//2

1,2会不会被重排呢?
我觉得应该是不会的,
但是能找到的关于重排的讨论,都局限于讨论变量赋值。
我一直没看到关于方法调用的讨论。

Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性 by Zhang Gavin

“Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。” 见 www.ibm.com/developerworks/cn/java/j-jtp06197.h... 跟本文观点不一样啊

Re: Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性 by 程 晓明

谢谢您的关注。
请您阅读英文原文,以及这段话的前后上下文来理解Brian Goetz想表达的意思。
这段话对应的英文原文是:Volatile variables share the visibility features of synchronized, but none of the atomicity features.
翻译成中文应该是:Volatile 具有synchronized的可见性特性,但不具有synchronized的原子性。
synchronized本质上是为了保证整个临界区执行的原子性,这个特性Volatile肯定是没有的。

再看我在文章中说的原话:对任意单个volatile变量的读/写具有原子性。
在《The Java Language Specification Java SE 7 Edition》的17.7章,有如下描述:
a single write to a non-volatile long or double value is treated as two separate writes.
Writes and reads of volatile long and double values are always atomic.
也就是说,java语言规范不保证对long或double的写入具有原子性。
但当我们把 long或double声明为volatile后,对这个变量的写将具有原子性了。

关于 VolatileExample 的一个问题 by 王 伟

class VolatileExample {
int a = 0;
volatile boolean flag = false;

public void writer() {

flag = true; //2 (交换1,2顺序)
a = 1; //1

}

public void reader() {
if (flag) { //3
int i = a; //4
……
}
}
}

就writer()方法而言,书写代码的时候无意或者故意交换1,2的顺序(因为就这个例子而言,writer()方法1,2顺序不存在逻辑性)
我认为reader()得到的结果不确定,是这样的吧。

volatile写 by Zhao Yu

根据您的文章,我看见volatile写之后会插入StoreLoad屏障,但是为什么这个表中,volatile写和普通读/写会重排序呢?StoreLoad不能阻止前后指令重排序吗?

是否能重排序 第二个操作
第一个操作 普通读/写 volatile读 volatile写
普通读/写 NO
volatile读 NO NO NO
volatile写 NO NO

volatile写 by Zhao Yu

根据您的文章,我看见volatile写之后会插入StoreLoad屏障,但是为什么这个表中,volatile写和普通读/写会重排序呢?StoreLoad不能阻止前后指令重排序吗?

是否能重排序 第二个操作
第一个操作 普通读/写 volatile读 volatile写
普通读/写 NO
volatile读 NO NO NO
volatile写 NO NO

关于volatile例子的2点疑问 by 王 伟

class VolatileExample {
volatile int a = 0;
boolean flag = false;

public void writer() {
a = 1; //1
flag = true; //2
}

public void reader() {
if (flag) { //3
int i = a; //4
……
}
}
}
1.将变量a使用vilatile修饰,我想这时候在3 if(flag)线程B可能就看不见线程A对Flag的修改
2.关于happen-before volatile变量的写入happen-before volatile变量的读取。这句话具体应该怎么理解,反正过来,volatile变量的读取 happen-before volatile变量的写入,是否也是正确。期待作者的回复。

Re: 关于 VolatileExample 的一个问题 by 程 晓明

不好意思,回复晚了。
结论是对的,reader()在4不一定能读到其它线程在1的写入操作。

但前提条件的描述有点小问题。
这里的writer()方法的操作1和2,虽然从技术的角度来说没有数据依赖性,但这两个操作之间存在业务逻辑的依赖性。
因为从业务逻辑的角度来看,我们只有在写入了a变量之后,才能设置flag标志位为true--它标志着当前写线程的写入操作(写入变量a)已经完成了。也就是说,从业务逻辑的角度来看,操作1必须要放在2的前面。

Re: volatile写 by 程 晓明

StoreLoad屏障禁止的是处理器的重排序,而这个表格是JMM针对编译器制定的重排序规则表。它们针对不同的目标。
在第一章中,提到过重排序分为编译器的重排序和处理器的重排序。所以,JMM针对编译器的重排序会制定重排序规则表;JMM针对处理器的重排序会插入内存屏障。

Re: 关于volatile例子的2点疑问 by 程 晓明

问题1
是的,线程B在3不一定能看到线程A对flag的修改。

问题2
这个happen-before规则实质上是说,volatile可以实现线程之间的内存可见性通信。
volatile不仅仅可以保证自身的可见性,它还有一个更为关键的特性:线程A在写volatile变量之前的所有的写入操作,在线程B读这个volatile变量开始都将对B线程可见。

上面的描述感觉比较抽象,下面举一个例子:
假设A线程的程序顺:
操作1 //A线程写普通变量a
操作2 //A线程写volatile变量

假设B线程的程序顺序:
操作3 //B线程读同一个volatile变量
操作4 //B线程读普通变量a

根据程序顺序规则(因为编译器和处理器要遵守as-if-serial语义),1 -> 2;3 -> 4
但仅仅有这两个内存可见性保证,还无法保证两个线程能正确的并发执行。因为线程A的1和2,与线程B的3和4之间缺少一个联系的桥梁(线程A在1的写入操作之后,线程B接下来在4不一定能看的到这个写入操作)。happen-before的volatile规则刚好就起到了桥梁的作用。由于happen-before的传递性,volatile规则和上面的两个程序顺序规则组合后:
1 -> 2 -> 3 -> 4
此时,线程A的1和2的写入,线程B在3和4一定能读的到。也就是说,volatile能实现线程之间的内存可见性通信。

Re: 关于volatile例子的2点疑问 by 程 晓明

反正过来,volatile变量的读取 happen-before volatile变量的写入,是否也是正确。
--不正确,JMM没有这个保证。
具体原因,可以参考volatile的编译器重排序规则表和volatile的内存屏障插入策略。
volatile的编译器重排序规则表和volatile的内存屏障插入策略都是针对 “volatile变量的写入happen-before volatile变量的读取”来设计的。JMM专家组把它们设计成这样,就是为了让volatile的写-读可以实现线程之间的内存可见性通信。

volatile内存语义的实现的两个疑问。 by jeon xu

问题1
volatile变量的读取 happen-before volatile变量的写入,是否也是正确。
>>您的回答是--不正确,JMM没有这个保证。
但在(volatile重排序规则表)提到:
第一个操作是volatile读,第二个操作volatile写,不能重排序,这个不能保证happen-before吗?

问题2
两个不同域的volatile依次被读时,
volatile int v1 = 1;
volatile int v2 = 2;

void readAndWrite() {
int i = v1; //第一个volatile读
int j = v2; // 第二个volatile读
根据(volatile重排序规则表)是不会比重排序的,这能保证happen-before吗?
这个官方happen-before中并没有这样的规则。

在《Java并发编程实践》中,有这样一段话,
When two threads synchronize on different locks, we can’t say anything about the ordering of actions between them—there is no happens-before relation between the actions in the two threads.
--volatile读/MonitorEnter,volatile写/MonitorExit的语义相同,
MonitorExit -> MonitorEnter的happens-before关系,是指之能在同一这个Lock吗?

困惑中。
期待作者的解答,谢谢。

Re: volatile内存语义的实现的两个疑问。 by 程 晓明

回复晚了,不好意思。

问题1:
第一个操作是volatile读,第二个操作volatile写,不能重排序,这个不能保证happen-before吗?
--这个不能保证happen-before
这个只能保证两个volatile操作之间不能重排序。
我们可以通过一个简单的例子来推演“volatile变量的读是否能 happen-before volatile变量的写?”:
假设A线程的方法a的程序顺序:
普通变量写 //操作1
volatile变量的读 //操作2

假设B线程的方法b的程序顺序:
volatile变量的写 //操作3
普通变量的读 //操作4

假设A线程先执行方法a之后,B线程执行方法b。
如果“volatile变量的读能够 happen-before volatile变量的写”的话,我们将可以得到如下happen-before :
1 happen-before 2 //根据程序顺序规则
2 happen-before 3 //根据我们在这里假设的这个 happen-before 规则
3 happen-before 4 //根据程序顺序规则
1 happen-before 4 //根据传递性
也就是说,线程A在操作1的写入,线程B在操作4一定看的到。

但是,根据volatile的编译器重排序规则表:
操作1和操作2之间可以重排序;
操作3和操作4之间可以重排序。
即使我们假设2在3之前执行,但线程B的操作4仍可能无法看到线程A在操作1的写入。

简而言之,volatile规则(背后靠volatile的编译器重排序规则和volatile的内存屏障插入策略支撑),保证的是“一个线程的volatile写--另一个线程的volatile读”为程序员提供跨线程的内存可见性保证,而不仅仅是volatile变量自身的可见性!

------------------------

问题2和问题1类似,请参考问题1,以及上面对其他网友问题的回答。

------------------------------

MonitorExit -> MonitorEnter的happens-before关系,是指之能在同一这个Lock吗?
--是的。
对同一个锁的释放--获取才有happens-before关系。

关于 volatile读与监视器的获取有相同的内存语义 的一个问题, by 王 伟

hi,我根据更具你上面那个列子,写了个小小的实验程序,但是结果有一点我不理解,或者是否我程序写

有错误,希望作者能回答下,


public class Test {

public static void main(String args[]) {

Counter counter = new Counter();

new writerThread("writeThread", counter).start();

new ReaderThread("readThread", counter).start();
}
}

class Counter {

private volatile boolean flag = false;

private int a = 0 ;


public void write(int i){




a = i;

System.err.println(Thread.currentThread().getName()+" write :"+a);

flag = true;




}

public void read(){

if(flag){

System.err.println(Thread.currentThread().getName()+" read :"+a);
}

}
}

class writerThread extends Thread{

private Counter counter;

Random random = new Random();

public writerThread(String name,Counter counter){

super(name);

this.counter = counter;
}


public void run(){

for(int i = 0;true;i++){

counter.write(i);

try {
Thread.sleep(random.nextInt(5));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}


class ReaderThread extends Thread{

private Counter counter;

Random random = new Random();

public ReaderThread(String name,Counter counter){

super(name);

this.counter = counter;
}

public void run(){

while(true){

counter.read();

try {
Thread.sleep(3);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}

运行结果片段:
readThread read :27
writeThread write :28
writeThread write :29
writeThread write :30
readThread read :31 ------------------------- read 31
writeThread write :31 ------------------------- write 31
readThread read :31
writeThread write :32

为什么会有一个read 31出现在 write 31之前,谢谢

Re: 关于 volatile读与监视器的获取有相同的内存语义 的一个问题, by 王 伟

我又想了下,不知道对不,我觉得是write()方法里面的

a = i;

System.err.println(Thread.currentThread().getName()+" write :"+a);

这个2句代码被重排序了,不知道我的想法是否正确

JMM如何实现“把该线程对应的本地内存置为无效”? by 袁 先虎

“当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。”
问题:JMM是如何实现“设置线程对应的本地内存无效的”?这是否是内存屏障实现的功能?我看文中描述内存屏障时都只提到了屏障禁止重排序的功能。

关于volatile内存语义实现的一点点疑惑 by 王 伟

1.第一个操作为volatile写
2.第二个操作的普通的读/写
更具表,是可以重排序这个2个操作的,这里有个疑问

example: 第一个操作为volatile写,第二个操作为普通的写


public void writer() {

volatile flag = true; //1
a = 1; //2

}

public void reader() {
if ( volatile flag) { //3
int i = a; //4
……
}
}

根据之前你的讲解,这种情况下,当另外一个线程通过reader()方法读a值,这个时候a的值是不确定的,

现在有个疑问,如果在 writer()方法 指令重排序了,

public void writer() {

a = 1; //2
volatile flag = true; //1
}


这时另外一个线程,在通过reader()方法读取a的时候,这时a的值是可以确定的,请问这种情况a的值到底能否确定,或者能否这样理解,正是因为

public void writer() {

volatile flag = true; //1
a = 1; //2
}

第一个操作为volatile 写,第二个操作为普通的读/写时候,可以重排序,所以导致了另外一个线程通过

reader()方法对变量a的读取时,没有线程同步,谢谢~!

Re: 关于 volatile读与监视器的获取有相同的内存语义 的一个问题, by 程 晓明

为什么会有一个read 31出现在 write 31之前?
--因为在您的程序中,flag标记在read()的末尾没有重置为false,导致第一次迭代之后,读线程读取到的flag的值恒为true。
下面是您的程序:
public void write(int i){//写线程执行
a = i;//1
System.err.println(Thread.currentThread().getName()+" write :"+a);//2
flag = true;//3
}

public void read(){//读线程执行
if(flag){//4
System.err.println(Thread.currentThread().getName()+" read :"+a);5
}
}
即使没有发生重排序,如果读线程和写线程按如下时序执行,就可以产生这种结果:
时间t1:写线程执行1
时间t2:读线程执行4(在第一次迭代之后,flag恒为true)
时间t3:读线程执行5
时间t4:写线程执行2
时间t5:写线程执行3

Re: 关于volatile内存语义实现的一点点疑惑 by 程 晓明

这种情况a的值到底能否确定?
--这种情况下,a的值不确定。
也就是:读线程可能读到写线程写入a之后的值;也可能读到写入之前的值。

Re: JMM如何实现“把该线程对应的本地内存置为无效”? by 程 晓明

JMM是如何实现“设置线程对应的本地内存无效的”?
--本地内存和主内存是为了让读者更容易理解内存模型而虚构出来的。
事实上,在JSR-133内存模型规范中,根本就没有主内存和本地内存的概念。

JSR-133内存模型规范使用happen-before来描述内存可见性。JSR-133内存模型规范中指定的那些happen-before规则,背后是由JSR-133内存模型的编译器重排序规则和处理器内存屏障插入策略来实现的。

Re: 关于 volatile读与监视器的获取有相同的内存语义 的一个问题, by 王 伟

感谢作者的回复,这个疑问解决了,但是这里有个新的疑问,根据您之前的文章,
根据happen-before 和 as-serial语义,
由于 flag 是一个voliate变量,应该能推导出,1 happen-before 3,3 hanppen-before 4,
4 hanppen before 5 从而推到出 3 happen-before 4,5,何来这里 ,为什么这里反而出现-了,4,5 happen-before 3的情况,感谢作者的回复

Re: 关于 volatile读与监视器的获取有相同的内存语义 的一个问题, by 王 伟

感谢作者的回复,这个疑问解决了,但是这里有个新的疑问,根据您之前的文章,
根据happen-before 和 as-serial语义,
由于 flag 是一个voliate变量,应该能推导出,1 happen-before 3,3 hanppen-before 4,
4 hanppen before 5 从而推到出 3 happen-before 4,5,何来这里 ,为什么这里反而出现-了,4,5 happen-before 3的情况,感谢作者的回复

Re: 关于 volatile读与监视器的获取有相同的内存语义 的一个问题, by 程 晓明

不好意思,回复晚了。

由于程序在第一次迭代之后flag恒为true,第一次迭代之后,程序的 read()实质上变成了:
public void read(){//读线程执行
if(true){//4
System.err.println(Thread.currentThread().getName()+" read :"+a);//5
}
}
上述操作4原本是为了判定在flag为true的情况下才能执行操作5.
但现在操作4变成了一个无意义的操作。
现在read()蜕变为了任意时刻调用read()方法,就直接执行操作5:
public void read(){//读线程执行
System.err.println(Thread.currentThread().getName()+" read :"+a);//5
}
假设A线程执行writer()方法,B线程执行read()方法。
当两个线程并发执行的时候,B线程的操作5可以混杂在A线程的操作1,2和3之间的任意位置执行。

在这个系列文章中,当我们谈到多线程之间的happen-before关系时,都有一个隐含的前提:
首先,这个多线程程序在语义是正确的。
在这个前提之下,我们然后才能根据volatile等同步原语提供的happen-before关系来保证多线程之间的内存可见性。

Re: 关于 volatile读与监视器的获取有相同的内存语义 的一个问题, by 刘 佩

我把flag标记在read()方法的末尾重置为false,还是会出现上述问题,为什么呢

不同volatile变量或者不同lock by Jing Xu

感谢您的文章,非常好,学到了很多东西。
有一个疑问,如果是不同的volatile或者不同的lock,我们还能那么我们还能保证多到最新的值吗?比如:
T1 T2
l1.lock
a.write
l1.unlock

l2.lock
a.read
l2.unlock

T2中可以肯定读到T1中a新写的值吗?

Re: volatile写 by 朱 码农

我理解是:JMM禁止编译器重排序最终通过内存屏障指令保证,既然JMM不禁止volatile写与普通读、写的重排序,为什么会插入这个指令呢?或者这么说或许能解释的通,插入storeload指令只是保证volatitle写与后面volatile读重排序。
不知道我的理解是否合理。

顺便请教个问题:volatile写与后面的普通写编译器为什么会允许重排序呢?
若普通写被重排到volatile写之前的话,普通写操作的数据就提前对另一个线程可见了。请帮忙解答

Re: 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。 by 朱 码农

能简单介绍下处理器根据什么把本地内存的某些变量标记为无效?
仅仅通过内存屏障如何把y设置为无效

Re: 关于volatile内存语义实现的一点点疑惑 by 朱 码农

王伟,你好。
关于重排序是否会影响可见性结果,你是怎么理解的
我不太理解,帮忙解释下

线程中的普通变量 何时 刷新至 主内存中 ? by wang zheng

你好,请教一个问题。

假设A线程的程序顺:
操作1 //A线程写普通变量a
操作2 //A线程写volatile变量

假设B线程的程序顺序:
操作3 //B线程读同一个volatile变量
操作4 //B线程读普通变量a

通过您本系列的文章,我可以理解,若是按 1 2 3 4 这个执行顺序进行执行的话,那么,线程B在操作4处,肯定是可以读取到 线程A 在 操作1 处写入的结果的。

我的问题是:
如果执行顺序是 1 3 4 2 这种顺序来执行的话,那么,线程B 在操作4 处能否看到 线程A 在操作1 处写入的结果?是肯定看不见,还是有可能会看见?

如果是肯定看不见的话,那是不是说,在volatile变量写之前的 普通变量的写操作,必须要等到 volatile变量写之后,才会被刷新到 主内存中?

如果是可能会看见,那是不是说,虽然还没有执行过 volatile写操作,但是普通变量的写操作的结果也是可能会被刷新至主内存中的?也就是说 普通变量的值 在什么时候被刷新到主内存中去,并不取决于 volatile 变量的写操作。只不过,在发生 volatile变量写操作的时候,肯定会将 volatile变量写之前的那些 普通变量的写操作的结果刷新到主内存中去而已。但并不能说明,在没有发生 volatile变量 写操作时,就不会将 普通变量的写操作刷到主内存中去。

以上,请作者帮忙解答一下,十分谢谢。

对“JMM针对编译器制定的volatile重排序规则表”的 疑问 by wang zheng

表中有如下两条规则 :
① 第一个操作是 :普通变量读/写 ,第二个操作是 volatile变量写
② 第一个操作是 :volatile变量写 ,第二个操作是 普通变量读/写

其中,① 是不允许进行重排序的,② 是允许进行重排序的。

对于②可以进行重排序有点疑问: 如果②可以进行重排序,那么重排序之后,就变成了① 这种情况,而①又不允许进行重排序了,也就是说,若②被重排序了,那么重排序之后就回不到②这种状态了,只能停留在①这种状态了。这种情况是正常的么?
或者说,重排序只是考虑重排序之前的顺序,至于重排序之后的情况如何,是不被考虑的。

以上,请作者帮忙解答一下,谢谢。

程先生,你好.看了您的第一个示例代码,有点疑问,疑问在下面的内容中. by bob Jiao

class VolatileFeaturesExample {
volatile long vl = 0L; //使用volatile声明64位的long型变量

public void set(long l) {
vl = l; //单个volatile变量的写
}

public void getAndIncrement () {
vl++; //复合(多个)volatile变量的读/写
}


public long get() {
return vl; //单个volatile变量的读
}
}
如果本例中不使用volatile修饰,那么单就set和get方法会有线程不安全的问题吗?但是好像有说long和double类型的赋值操作会有问题,那换成非long和非double并且不用volatile,那么set和get方法会有线程不安全的问题吗?

Re: JMM如何实现“把该线程对应的本地内存置为无效”? by 黄 文海

是内存屏障起的作用。内存屏障中的读屏障(Load barrier)能够根据invalidate queue中的内容将相应处理器的cache中的一些条目标志为无效(invalid)。这样,这个处理器上运行的线程后续读取这些被标记为无效的的条目中的变量时需要从其它处理器或者RAM中加载变量值,这就起到了读取其它线程更新过后的值的效果。
例如,读取volatile变量x,
i n t t = x ; / / v o l a t i l e l o a d
编译器会插入下面两个内存屏障:
[ L o a d L o a d ]
[ L o a d S t o r e]
以x86处理器为例,上面的LoadLoad屏障相当于前面提到的读屏障,它起到刷新当前处理器高速缓存(即问题中的所谓“本地内存”,其实我不建议使用这个称呼,因为它容易被误解)的作用。由于x86并不会对“读-写”操作进行重排序,因此上面的LoadStore相当于一个空操作(no-op)。

Re: volatile by 王 定

我的理解是这样的,也举个例子:
int a = 0;
boolean flag = false;
线程A (0ms处开始执行)
a = 1; //1:耗时4ms
//do some thing :耗时100ms
flag = true; //2:耗时1ms
线程B (1ms处开始执行)
if (flag) { //3:耗时1ms
int i = a; //4:耗时1ms
}
flag 没有被volatile修饰的话,线程A和B运行,线程B运行完,i的值可能为0,因为A线程可能第一句代码执行的是flag=true,flag=true和a=1没有逻辑依赖,可以重排序。如果flag被volatile修饰,就不会出现这种情况,因为线程B在读取flag为true时,线程A做的所有写入操作对于线程B都必须是可见的。

在每个volatile读操作的后面插入一个LoadLoad屏障。 这个应该读操作之前吧 by chen David

在每个volatile读操作的后面插入一个LoadLoad屏障。 这个应该读操作之前吧

Re: 关于 volatile读与监视器的获取有相同的内存语义 的一个问题, by 田 庆俊

我也测试了一下这段代码,不但会出现read在write之前看到31的情况,也会出现已经write了32, 还是read到31的情况。

我的理解,这段代码的问题出在语句3, 5,

System.err.println(Thread.currentThread().getName()+" write :"+a);
System.err.println(Thread.currentThread().getName()+" read :"+a);

这里不是一个原子操作。如果改成
int _a = a;
System.err.println(Thread.currentThread().getName()+" write :"+ _a);

int _a = a;
System.err.println(Thread.currentThread().getName()+" read :"+ _a);

就不会发生问题了。

请批评指正。

Re: 关于 volatile读与监视器的获取有相同的内存语义 的一个问题, by 田 庆俊

sorry, 我弄错了。问题不是在这。

问题在于你不能仅仅在read结束的时候做
flag = false;
如果你在write的开始也加上
flag = false;

就没有问题了。

想想你的write线程有可能跑了两个cycle, read才跑一个,那样的话,第一个write已经把flag设置成true,read线程的print语句很有可能跑在write线程的下面两个state中间
a = i;
(这里跑了read线程)
System.out.println();

而我说的问题,即write线程已经写到了32,read还读出31,确实是因为
System.err.println(Thread.currentThread().getName() + " read :" + a);
不是原子操作引起的。

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

66 讨论
提供反馈
错误报告
商务合作
内容合作
Marketing
InfoQ.com及所有内容,版权所有 © 2006-2016 C4Media Inc. InfoQ.com 服务器由 Contegix提供, 我们最信赖的ISP伙伴。
北京创新网媒广告有限公司 京ICP备09022563号-7 隐私政策
BT

我们发现您在使用ad blocker。

我们理解您使用ad blocker的初衷,但为了保证InfoQ能够继续以免费方式为您服务,我们需要您的支持。InfoQ绝不会在未经您许可的情况下将您的数据提供给第三方。我们仅将其用于向读者发送相关广告内容。请您将InfoQ添加至白名单,感谢您的理解与支持。