Java 易失性循环

Java volatile loop

我正在处理某人的代码并遇到了类似的代码:

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

其中someVolatileMember定义如下:

private volatile int someVolatileMember;

如果某个线程 A 是 运行 for 循环而另一个线程 B 写入 someVolatileMember 那么我假设当线程 A 是 运行 不是很好的循环。我想这会解决它:

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

我的问题是:

您的理解是正确的:

  1. 根据 Java Language Specificationvolatile 字段的语义确保在不同线程之间完成更新后看到的值之间的一致性:

    The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.

    A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable (§17.4).

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

  2. 一旦分配了 final 变量,它的值就永远不会改变,因此循环计数也不会改变。

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

关于性能,代码如下:

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

从上面删除 volatile 关键字将导致输出 500000000 took 275ms.

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

这个循环有点废话,它的编写方式和假设有一些方法实际上可能会改变 someVolatileMember;之所以如此,是因为您可能永远不知道 if 何时结束,或者根本不知道。由于具有非易失性字段,这甚至可能是一个更昂贵的循环,因为 volatile 意味着在 CPU 级别使缓存和耗尽缓冲区比通常的变量更频繁。

您的解决方案首先 读取 一个 volatile 并使用它实际上是一个非常常见的模式;它也产生了一个非常常见的反模式:"check then act"...你将它读入一个局部变量,因为如果它以后发生变化,你不在乎 - 你正在使用你拥有的最新副本此时此刻。所以是的,您在本地复制它的解决方案很好。