如果在多个线程中使用了instanced,我们是否应该将私有字段声明为volatile
?
在Effective Java中,有一个例子,代码在没有volatile的情况下不起作用:
import java.util.concurrent.TimeUnit;
// Broken! - How long would you expect this program to run?
public class StopThread {
private static boolean stopRequested; // works, if volatile is here
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested)
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
解释说
while(!stopRequested)
i++;
针对以下内容进行了优化:
if(!stopRequested)
while(true)
i++;
所以后台线程看不到stopRequested
的进一步修改,所以它永远循环。 (顺便说一句,该代码在JRE7上没有volatile
终止。)
现在考虑这个课程:
public class Bean {
private boolean field = true;
public boolean getField() {
return field;
}
public void setField(boolean value) {
field = value;
}
}
和一个如下的线程:
public class Worker implements Runnable {
private Bean b;
public Worker(Bean b) {
this.b = b;
}
@Override
public void run() {
while(b.getField()) {
System.err.println("Waiting...");
try { Thread.sleep(1000); }
catch(InterruptedException ie) { return; }
}
}
}
上面的代码按预期工作,不使用挥发物:
public class VolatileTest {
public static void main(String [] args) throws Exception {
Bean b = new Bean();
Thread t = new Thread(new Worker(b));
t.start();
Thread.sleep(3000);
b.setField(false); // stops the child thread
System.err.println("Waiting the child thread to quit");
t.join();
// if the code gets, here the child thread is stopped
// and it really gets, with JRE7, 6 with -server, -client
}
}
我认为由于公共setter,编译器/ JVM永远不应该优化调用getField()
的代码,但是this article表示存在一些“Volatile Bean”模式(模式#4),应该应用于创建可变的线程安全类。 更新:也许该文章仅适用于IBM JVM?
问题是:JLS的哪一部分明确地或隐含地说具有公共getter / setter的私有原始字段必须声明为volatile
(或者他们不必)?
很抱歉,我试图详细解释这个问题。如果有什么不清楚,请告诉我。感谢。
答案 0 :(得分:9)
问题是:JLS的哪一部分明确地或隐含地说具有公共getter / setter的私有原始字段必须声明为volatile(或者它们不必)?
JLS内存模型不关心getter / setter。从内存模型的角度来看,它们是无操作的 - 你也可以访问公共领域。将布尔值包装在方法调用后面不会影响其内存可见性。你的后一个例子纯粹是运气。
如果在多个线程中使用了instanced,我们是否应该将私有字段声明为volatile?
如果要在多线程环境中使用类(bean),则必须以某种方式将其考虑在内。创建私有字段volatile
是一种方法:它确保每个线程都能保证看到该字段的最新值,而不是任何缓存/优化过时的过时值。但它并没有解决atomicity的问题。
The article you linked to适用于任何遵守JVM规范(JLS倾向于此)的JVM。您将获得各种结果,具体取决于JVM供应商,版本,标志,计算机和操作系统,运行程序的次数(HotSpot优化经常在第10000次运行后启动)等,因此您必须了解规范并仔细遵守遵守规则以创建可靠的计划。在这种情况下进行试验是一种很难找到工作原理的方法,因为JVM可以以任何方式运行,只要它符合规范,并且大多数JVM确实包含所有类型的动态优化。
答案 1 :(得分:4)
不,该代码同样不正确。 JLS中没有任何内容表示字段必须声明为volatile。但是,如果您希望代码在多线程环境中正常工作,则必须遵守可见性规则。 volatile和synchronized是两个正确使数据在线程间可见的主要工具。
至于你的例子,编写多线程代码的难点在于许多形式的错误代码在测试中都能正常工作。仅仅因为多线程测试在测试中“成功”并不意味着它是正确的代码。
有关特定的JLS参考,请参阅Happens Before部分(以及页面的其余部分)。
注意,作为一般的经验法则,如果你认为你已经想出了一种巧妙的新方法来绕过“标准”的线程安全习语,那么你很可能是错的。
答案 2 :(得分:4)
在我回答你的问题之前,我想解决
顺便说一句,该代码在JRE7
上没有volatile就终止了
如果您使用不同的运行时参数部署相同的应用程序,则可能会发生这种情况。提升不一定是JVM的默认实现,因此它可以在一个而不是另一个中工作。
要回答你的问题,没有什么能阻止Java编译器执行你的后一个例子
@Override
public void run() {
if(b.getField()){
while(true) {
System.err.println("Waiting...");
try { Thread.sleep(1000); }
catch(InterruptedException ie) { return; }
}
}
}
它仍然是顺序一致,因此维护了Java的保证 - 您可以专门阅读17.4.3:
在每个线程t执行的所有线程间动作中, 程序顺序t是反映顺序的总顺序 这些动作将根据内线程执行 语义学。
如果所有操作都发生在a中,则一组操作将按顺序一致 与程序一致的总订单(执行订单) 此外,变量v的每个读数r都看到该值 写作w写给v:
换句话说 - 只要一个线程看到以相同顺序读取和写入字段,而不管编译器/内存的重新排序,它就会被认为是顺序一致的。