在我的机器上,以下代码无限期运行(Java 1.7.0_07):
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;
}
但是,在synchronized
附近添加一个锁定对象和一个stopRequested
语句(实际上,同步块中没有任何内容),并且终止:
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
Object lock = new Object();
int i = 0;
while (!stopRequested) {
synchronized (lock) {}
i++;
}
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
在原始代码中,变量stopRequested
被“悬挂”,变为:
if (!stopRequested)
while (true)
i++;
然而,在修改后的版本中,似乎没有发生这种优化,为什么呢? (事实上,为什么synchronized
没有完全优化掉?)
答案 0 :(得分:3)
虚拟机无法推断锁定未被其他线程同步,因此无法对其进行优化。
根据每个Java内存模型,所有同步块都是完全有序的,并且此顺序(在同一个锁上)有助于建立先发生关系。这就是为什么VM无法移除同步块的原因;除非VM可以证明只有一个线程在对象上进行同步,否则可以删除所有这些同步块,而不会影响之前发生的关系。
如果锁是本地对象,VM可以进行转义分析以消除锁。我们多年来一直听说过逃逸分析,但正如我的例子所示,并且在不久之前我已经进行了测试,它似乎还没有发挥作用。
可能有理由不进行锁定省略。优化非常适合使用本地Vector
或StringBuffer
等的代码。但这仅适用于旧代码;很久没有人这样做了。
某些代码甚至可能依赖于更强大的pre-java-5模型,其中无法永远消除锁定。可能有很多程序,类似于OP的精心设计的例子,在新模型中是不正确的,但过去已经工作了多年。 Lock elision可能会破坏这些程序。
答案 1 :(得分:2)
虽然这可能看起来像内存可见性问题,但在简单示例中通常是JIT优化问题。这是因为JIT可以检测您是否正在修改该标志,如果不这样,该线程可以内联它。有效地将其变为无限循环。
您可以告诉我的一个方法是,可见性问题是短暂的,通常太短,您无法看到。虽然它们是随机的,但通常是一微秒到一毫秒。即,直到线程上下文切换并且当它再次运行时,它不会保留旧值。事实上,你可以看到它始终变成一个永远“检测”变化的无限循环的例子,这是一个让步。
如果你只是使用Thread.sleep(10)
减慢循环,这可以防止它运行足够长的时间来编译。它必须循环10,000次才能进行优化。这通常会“解决”问题。
添加线程安全代码(例如使用volatile变量或添加synchronized块)可能会阻止优化。