易失性变量,并刷新至主存储器/从主存储器读取

时间:2018-08-05 10:26:27

标签: java multithreading jvm synchronization volatile

官方注释说

  

写到volatile字段具有与监视器释放相同的存储效果,而从volatile字段读取具有与监视器获取相同的存储效果。

  

有效地,volatile的语义已得到实质性增强,几乎达到了同步的水平。出于可见性目的,对易失性字段的每次读取或写入都像“半”同步一样。

来自here

这是否意味着对volatile变量的任何写入都会使正在执行的线程将其缓存刷新到主内存中,而从volatile字段中进行的每次读取都会使线程从主内存中重新读取其变量?

我问是因为相同的文本包含了这一说法

  

重要说明:注意,两个线程访问相同的volatile变量以正确设置事前发生关系很重要。在线程A写入易失性字段f时,线程A读取易失性字段g之后对线程B可见的所有情况并非如此。发布和获取必须“匹配”(即在相同的volatile字段上执行)以具有正确的语义。

这句话让我很困惑。我肯定知道用同步语句进行常规锁获取和释放不是正确的-如果某个线程释放了任何监视器,则它对所有其他线程所做的所有更改都将变为对其他所有线程可见(更新:实际上不是真的-观看最佳答案)。甚至还有question about it的stackoverflow。但据指出,无论出于何种原因,对于可变字段而言,情况并非如此。我无法想象任何发生的事前保证,都不会使更改对其他线程(不读取相同的volatile变量的线程)可见。至少可以想象一个实现,它与前两个引号没有矛盾。

此外,在发布此问题之前,我进行了一些研究,例如this article,其中包含此句子

  

执行了这些指令之后,所有其他写操作都可以通过缓存子系统或主内存对所有其他线程可见。

提到的指令是在写入易失性字段时发生的指令。

那重要的提示应该是什么意思?还是我想念什么?还是那条纸条是完全错误的?

答案?

进行了更多研究之后,我只能在官方文档中找到有关易失性字段及其对非易失性字段变化的影响的声明:

  

使用易失性变量可以降低内存一致性错误的风险,因为对易失性变量的任何写入都会与该变量的后续读取建立先发生后关联的关系。这意味着对volatile变量的更改始终对其他线程可见。 此外,这还意味着当线程读取一个volatile变量时,它不仅会看到volatile的最新更改,还会看到导致更改的代码的副作用。

来自here

我不知道这是否足以得出结论,只有在读取相同volatile的线程中才能保证事前发生关系。因此,现在我只能总结一下结果尚无定论。

但是在实践中,我建议考虑线程A所做的更改在写入易失字段时,只有在线程B读取时,才保证对线程B可见相同的易失性字段。以上来自官方消息的引用强烈暗示了这一点。

2 个答案:

答案 0 :(得分:3)

您正在从完全错误的角度看待这个问题。首先,您要引用JLS,而不是谈论 flush ,这将是该规范的实现细节。您唯一需要依赖的就是JLS ,其他任何不难理解的事情可能都是,但并不能证明任何形式或形式的规范是对还是错。

您错了的根本原因是:

  

我确定对于常规锁获取而言,这不是真的...

实际上,您在x86上可能是对的,但是JLS and the official oracle tutorial mandates that

  

当线程释放固有锁时,该动作与该锁的任何后续获取 之间将建立先发生关系。

后续动作建立了

发生前(如果需要的话,请阅读两个动作,如果您更简单)。一个线程释放该锁,另一个线程获取它-这些是后续的(release-acquire semantics)。

volatile发生了同样的事情-一些线程对此进行写操作,而当其他某个线程通过后续读取观察到该写操作时,则发生在-before之前成立。

答案 1 :(得分:2)

  

这是否意味着对volatile变量的任何写入都会使执行   线程将其缓存刷新到主内存中,并从易失性中每次读取   字段使线程从主内存中重新读取其变量?

不,并不意味着那样。那样思考是一个常见的错误。这意味着Java内存模型中指定的内容。

在intel CPU上,有刷新缓存行的指令:clflushclflushopt,并且在发生易失性写操作时,对整个缓存行进行这种刷新是非常低效的。 / p>

作为一个例子,让我们看看

public static volatile long a = 0;

public static void main(String[] args){
    Thread t1 = new Thread(() -> {
        while(true){
            //to avoid DCE
            if(String.valueOf(String.valueOf(a).hashCode()).equals(String.valueOf(System.nanoTime()))){
                System.out.print(a);
            }
        }
    });

    Thread t2 = new Thread(() -> {
        while(true){
            inc();
        }
    });

    t1.start();
    t2.start();
}

public static void inc(){
    a++;
}

为我的Haswell。让我们来写一个简单的例子:

java -server -XX:-TieredCompilation -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*Volatile.inc -jar target/test-0.0.1.jar

我禁用了分层编译,并使用C2编译器如下运行:

  # {method} {0x00007f87d87c6620} 'inc' '()V' in 'com/test/Volatlee'
  #           [sp+0x20]  (sp of caller)
  0x00007f87d1085860: sub     $0x18,%rsp
  0x00007f87d1085867: mov     %rbp,0x10(%rsp)   ;*synchronization entry
                                                ; - com.test.Volatlee::inc@-1 (line 26)

  0x00007f87d108586c: movabs  $0x7191fab68,%r10  ;   {oop(a 'java/lang/Class' = 'com/test/Volatlee')}
  0x00007f87d1085876: mov     0x68(%r10),%r11
  0x00007f87d108587a: add     $0x1,%r11
  0x00007f87d108587e: mov     %r11,0x68(%r10)
  0x00007f87d1085882: lock addl $0x0,(%rsp)     ;*putstatic a
                                                ; - com.test.Volatlee::inc@5 (line 26)

  0x00007f87d1085887: add     $0x10,%rsp
  0x00007f87d108588b: pop     %rbp
  0x00007f87d108588c: test    %eax,0xca8376e(%rip)  ;   {poll_return}
  0x00007f87d1085892: retq
  ;tons of hlt ommited

输出如下:

volatile

因此,在这个简单的示例中,lock编译为一条exclusive ed指令,要求高速缓存行具有要执行的api.get_user(...)状态(如果没有,则可能向其他内核发送读取无效信号)。