Java volatile防止重新排序范围

时间:2018-08-02 06:58:02

标签: java multithreading concurrency volatile

将数据写入和写入易失性字段可防止分别在易失性字段之前和之后对读/写进行重新排序。在对volatile变量进行写之前,变量的读/写不能重新排序,而在对volatile变量进行读之后,不能对其进行重新排序。但是,这一禁令的范围是什么?据我了解,volatile变量只能在使用它的块内阻止重新排序,对吗?

为了清楚起见,让我举一个具体的例子。假设我们有这样的代码:

int i,j,k;
volatile int l;
boolean flag = true;

void someMethod() {
    int i = 1;
    if (flag) {
        j = 2;
    }
    if (flag) {
        k = 3;
        l = 4;
    }
}

显然,对l的写操作将阻止对k的写操作进行重新排序,但将阻止对{{1}的对ij的写操作进行重新排序}?换句话说,在写入l之后是否可以写入ij

更新1

感谢大家抽出宝贵时间回答我的问题-非常感谢。问题是您在回答错误的问题。我的问题是关于范围,而不是基本概念。问题基本上是代码在多大程度上保证了与可变字段之间的“先发生”关系。 显然,编译器可以保证在同一代码块内,但是封装块和对等块又如何-这就是我的问题。 @Stephen C说,易失性保证发生在整个方法体内的行为发生之前,甚至在封闭的块中,但是我找不到对此的任何确认。他说的对吗,某处有确认信吗?

让我再举一个有关范围界定的具体示例:

l

在这种情况下,编译器会禁止对setVolatile() { l = 5; } callTheSet() { i = 6; setVolatile(); } 写的重新排序吗?或者,也许编译器不能/没有编程来跟踪在易失性情况下其他方法中会发生什么,并且i写可以重新排序以发生在i之前吗?还是编译器根本不重新排序方法调用?

我的意思是,编译器将无法跟踪是否在某些易失性字段写入之前发生某些代码,这是必须要指出的。否则,一个易失字段的读/写可能会影响程序一半的顺序,如果不是更多的话。这是一种罕见的情况,但是有可能。

此外,请看此报价

  

在新的内存模型下,可变变量不能相互重新排序仍然是正确的。区别在于,现在对它们周围的常规字段访问进行重新排序不再那么容易。

“在他们周围”。这个短语暗示着,在一定范围内,可变字段可以防止重新排序。

4 个答案:

答案 0 :(得分:4)

  

很明显,对l的写操作将阻止对k的写操作进行重新排序,但会阻止对i和j的写操作进行重新排序吗?

不清楚重新排序的含义;请参阅上面的评论。

但是,在Java 5+内存模型中,我们可以说在对i进行写操作之前发生的对jl的写操作对之后的另一个线程可见。已读取l ...,前提是写入i后没有写入jl

这确实具有约束写入ij的指令的任何重新排序的效果。具体来说,在写入l之后,不能将它们移动到内存写入障碍之后,因为这可能导致它们对第二个线程不可见。

  

但是这个禁令的范围是什么?

本身没有禁令

您需要了解,指令,重新排序和内存壁垒只是实现Java内存模型的特定方式的细节。根据保证在任何“格式正确的执行”中可见的内容,实际上定义了该模型。

  

据我了解,volatile会阻止在使用它的块内重新排序,对吗?

实际上,不。块不考虑在内。重要的是方法中语句的(程序源代码)顺序。


  

@Stephen C说,易失性保证发生在整个方法体内的行为发生之前,甚至在封闭的块中,但我对此没有任何确认。

确认为JLS 17.4.3。它指出以下内容:

  

在每个线程t执行的所有线程间操作中,t的程序顺序是一个总顺序,反映了根据t的线程内语义执行这些操作的顺序。

     

如果所有动作均以与程序顺序一致的总顺序(执行顺序)发生,则一组动作是顺序一致的,此外,变量v的每个读取r都看到将w写入v的值这样:

     
      
  • w按执行顺序排在r之前,

  •   
  • 没有其他写操作w',使得w按执行顺序排在w'之前,而w'在r之前。

  •   
     

顺序一致性是对程序执行中的可见性和顺序的非常有力的保证。在顺序一致的执行中,所有单独动作(例如读写)的总顺序与程序的顺序一致,并且每个单独动作都是原子性的,每个线程都可以立即看到。

     

如果一个程序没有数据竞争,那么该程序的所有执行将看起来是顺序一致的。

请注意,此定义中未提及块或作用域。

答案 1 :(得分:3)

编辑2

volatile的唯一受雇者the happens-before relation

为什么要在单线程中重新排序

考虑到我们有两个字段:

int i = 0;
int j = 0;

我们有写它们的方法

void write() {
  i = 1;
  j = 2;
}

如您所知,编译器可能会对其重新排序。那是因为编译器认为先访问哪个无关紧要。因为在单线程中,它们“在一起”。

为什么不能在多线程中重新排序

但是现在我们有了另一种方法可以在另一个线程中读取它们:

void read() {
  if(j==2) {
    assert i==1;
  }
}

如果编译器仍对其重新排序,则该断言可能会失败。这意味着j已经是2,但是i并不是1i=1似乎发生在assert i==1之后。

volatile的行为

volatile的担保人the happens-before relation

现在,我们添加volatile

volatile int j = 0;

当我们观察到j==2为真时,表示j=2发生了,而i=2在它之前发生了。因此断言现在永远不会失败。

“重新排序”只是编译器提供保证的一种方法。

结论

您现在唯一需要做的就是happens-before。请参考以下Java规范链接。 reordering or not只是此保证书的side effect

回答您的问题

由于lvolatile,因此在访问i中的j之前,总是要访问lsomeMethod。事实是,l=4行之前的每件事都会在它之前发生。

编辑1

由于帖子已被编辑。这是进一步的说明。

  

在随后每次对该字段进行读取之前,都会对易失字段(第8.3.1.4节)进行写操作。

happens-before的意思是:

  

如果一个动作发生在另一个动作之前,则第一个动作对第二个动作可见并且在第二个动作之前排好序。

因此,对ij的访问发生在对l的访问之前。

参考:https://docs.oracle.com/javase/specs/jls/se10/html/jls-17.html#jls-17.4.5

原始答案

不,volatile仅能保护自己,尽管很难对volatile附近的现场访问进行重新排序。

  

在新的内存模型下,可变变量不能相互重新排序仍然是正确的。 区别在于,现在不再很容易对它们周围的普通字段访问进行重新排序。写入volatile字段具有与监视器释放相同的存储效果,而从volatile字段读取具有相同的存储效果监控器获得的记忆效应。实际上,由于新的内存模型对易失性字段访问与其他易失性字段访问的重新排序施加了更严格的约束,因此,线程A写入易失性字段f时对线程A可见的任何内容在读取f时对线程B可见。

volatile关键字仅保证:

  

在每次后续读取相同的volatile之前,都会写入volatile字段。

参考:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile

答案 2 :(得分:1)

  

我很想知道volatile变量如何影响其他字段

易变变量确实会影响其他字段。如果JIT编译器认为重新排序不会对执行输出产生任何影响,则可以对其重新排序。因此,如果您有6个独立变量存储,JIT可以对指令进行重新排序。

但是,如果您使变量为volatile,即在您的情况下为变量 l ,则JIT不会在易失性存储之后重新排序任何变量STORES。而且我认为这是有道理的,因为在多线程程序中,如果将变量 l 的值设为4,则应该将 i 设为1,因为在程序中 > i 写在 l 之前,它最终是程序顺序语义学(如果我没有记错的话)。

Note that volatile variables does two things:
  1. 在易失性存储之后,编译器不会对任何存储进行重新排序/在易失性读取之前,不会对任何读取进行重新排序。
  2. 刷新加载/存储缓冲区,以便所有处理器都可以看到更改。

编辑:

这里的博客不错:http://jpbempel.blogspot.com/2013/05/volatile-and-memory-barriers.html

答案 3 :(得分:-2)

也许我知道你在做“真实范围”。

两种重新排序是导致指令结果未排序的主要原因: 1.编译器优化 2. Cpu处理器记录(由于高速缓存和主内存同步引起的邮件冲突)

volatile关键字首先需要确认volatile变量的刷新,与此同时,其他变量也被刷新到主内存中,但是由于编译器重新排序,在volatile易变变量之前的一些可写指令可能会在volatile变量之后重新排序,读者可能会困惑于不能实时读取程序顺序中易失变量之前的其他变量值,因此制定了“在易失变量之前强制执行易变变量之前执行变量写指令”的规则。由Java编译器或JIT完成。

重点是指令中编译器的优化,例如查找无效代码,指令重新排序操作,指令代码范围始终是“基本块”(除了其他一些常数传播优化等)。 基本块是一组内部没有jmp指令的指令,因此这是一个基本块。因此,我认为重新排序操作固定在范围基本块中。 源代码中的基本块始终是块或方法的主体。

并且由于Java没有内联函数,所以动态调用方法指令使用该方法调用,因此重新排序操作不应跨越两个方法。

因此,范围不会大于“方法主体”,或者可能仅是“用于”主体的区域,这是基本块范围。

这是我的全部想法,我不确定是否正确,有人可以帮助使其更加准确。