Java 易失性循环

Java volatile loop

提问人:asimes 提问时间:10/10/2018 更新时间:10/11/2018 访问量:782

问:

我正在处理某人的代码,并遇到了与此相当的内容:

for (int i = 0; i < someVolatileMember; i++) {
    // Removed for SO
}

其中定义如下:someVolatileMember

private volatile int someVolatileMember;

如果某个线程 A 正在运行 for 循环,而另一个线程 B 正在写入,那么我假设当线程 A 运行循环时要执行的迭代次数会发生变化,这不是很好。我认为这将解决它:someVolatileMember

final int someLocalVar = someVolatileMember;
for (int i = 0; i < someLocalVar; i++) {
    // Removed for SO
}

我的问题是

  • 只是为了确认线程 A 所做的迭代次数可以 如果线程 B 修改,则当 for 循环处于活动状态时更改someVolatileMember
  • 本地非易失性副本足以确保当 线程 A 运行循环,线程 B 无法更改 迭 代
爪哇岛 挥发性的

评论

4赞 brso05 10/10/2018
是的,是的......
1赞 asimes 10/10/2018
谢谢,只是习惯了C++而不是Java
0赞 brso05 10/10/2018
没问题,我只需要在末尾添加点,因为评论不够长......

答:

5赞 M A 10/10/2018 #1

你的理解是正确的:

  1. 根据 Java 语言规范,字段的语义可确保在不同线程之间完成更新后看到的值之间的一致性:volatile

    Java 编程语言提供了第二种机制,即字段,它在某些目的上比锁定更方便。volatile

    可以声明一个字段,在这种情况下,Java 内存模型确保所有线程都看到变量的一致值 (§17.4)。volatile

    请注意,即使没有修饰符,循环计数也可能根据许多因素而变化。volatile

  2. 分配变量后,其值永远不会更改,因此循环计数不会更改。final

评论

0赞 asimes 10/10/2018
您能否解释一下,“请注意,即使没有易失性修饰符,循环计数也可能根据许多因素而变化”
1赞 M A 10/10/2018
@asimes 对于 a ,如果线程 B 发生变化,线程 A 将在某个时间点看到更新的值。 只是确保它会立即被看到。private int someMembersomeMembervolatile
0赞 Eugene 10/11/2018
@manouti理论上,它可能永远不会看到这个更新的值,并且立即是牵强附会的,一旦写入了易失性,包含它的缓存就会失效,这也需要时间;因此,JLS表示“易失性写入发生在随后的易失性读取之前”
0赞 diginoise 10/11/2018 #2

此外,还会对性能产生影响,因为 的值永远不会从最本地的缓存中获取,但 CPU 会采取其他步骤来确保修改得到传播(可能是缓存一致性协议、延迟读取到 L3 缓存或从 RAM 读取)。使用变量的范围内的其他变量也有影响(这些变量也与主内存同步,但我在这里不演示)。volatilevolatile

关于性能,代码如下:

private static volatile int limit = 1_000_000_000;

public static void main(String[] args) {
    long start = System.nanoTime();
    for (int i = 0; i < limit; i++ ) {
        limit--;   //modifying and reading, otherwise compiler will optimise volatile out 
    }
    System.out.println(limit + " took " + (System.nanoTime() - start) / 1_000_000 + "ms");
}

...指纹500000000 took 4384ms

从上面删除关键字将导致输出。volatile500000000 took 275ms

评论

0赞 Eugene 10/11/2018
首先,很少从主内存中“获取”这样的内存 - 有缓存一致性协议;其次,您正在执行易失性读取易失性写入;与 OP 示例不同
0赞 diginoise 10/11/2018
@Eugene - 感谢您指出缓存一致性。我会更新答案。无论 CPU 架构如何,使用缓存的平台的存在都意味着需要采取额外的步骤来满足 .此外,我正在写信给那个易失性,以展示最坏的情况,如果只读取值,编译器无法通过删除来优化代码。volatilevolatilevolatile
0赞 Eugene 10/11/2018 #3

好吧,首先,该字段是私有的(除非您省略了一些实际上可能会更改它的方法)......

这个循环有点无意义,它的编写方式和假设存在实际上可能会改变的方法;之所以如此,是因为您可能永远不知道何时完成,或者是否完成。这甚至可能是一个成本更高的循环,因为具有非易失性字段,因为这意味着在 CPU 级别使缓存失效和耗尽缓冲区比通常的变量要频繁得多。someVolatileMembervolatile

您的解决方案首先读取易失性并使用它实际上是一种非常常见的模式;这也催生了一种非常常见的反模式:“先检查后行动”......你把它读入一个局部变量,因为如果它以后发生了变化,你不在乎 - 你正在使用你目前拥有的最新副本。所以是的,您在本地复制它的解决方案很好。

评论

1赞 asimes 10/11/2018
Private 与线程无关,单个类可以有两个线程。在我问题的一开始,我就写道,代码不是我写的
0赞 Eugene 10/11/2018
@asimes 我不想批评你的代码,而是说一般来说(编辑以消除混淆),如果该字段是私有的 - 两个线程将如何改变它?除非通过某些方法?
0赞 Eugene 10/11/2018
@asimes,您可能真正的意思是:具有一些不变量(您的易失性属性)的单个实例可以通过两个线程访问。如果是这样,是的 - 你是对的,但现在这个字段是私有的 - 这两个线程将如何写入它?