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
}
我的问题是:
- 只是确认线程A做的迭代次数可以
如果线程 B 修改,则在 for 循环处于活动状态时更改
someVolatileMember
- 本地非易失性副本足以确保
线程 A 运行循环 线程 B 不能改变
迭代次数
您的理解是正确的:
根据 Java Language Specification,volatile
字段的语义确保在不同线程之间完成更新后看到的值之间的一致性:
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
修饰符,循环计数也可能会根据许多因素发生变化。
一旦分配了 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"...你将它读入一个局部变量,因为如果它以后发生变化,你不在乎 - 你正在使用你拥有的最新副本此时此刻。所以是的,您在本地复制它的解决方案很好。
我正在处理某人的代码并遇到了类似的代码:
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
}
我的问题是:
- 只是确认线程A做的迭代次数可以
如果线程 B 修改,则在 for 循环处于活动状态时更改
someVolatileMember
- 本地非易失性副本足以确保 线程 A 运行循环 线程 B 不能改变 迭代次数
您的理解是正确的:
根据 Java Language Specification,
volatile
字段的语义确保在不同线程之间完成更新后看到的值之间的一致性: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
修饰符,循环计数也可能会根据许多因素发生变化。一旦分配了
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"...你将它读入一个局部变量,因为如果它以后发生变化,你不在乎 - 你正在使用你拥有的最新副本此时此刻。所以是的,您在本地复制它的解决方案很好。