提问人:Anonemous 提问时间:9/25/2023 最后编辑:Anonemous 更新时间:9/25/2023 访问量:101
同步锁之外的 Java 内存可见性
Java memory visibility outside the synchronized lock
问:
同步锁是否保证以下代码始终打印“END”?
public class Visibility {
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread_1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(i);
while (true) {
if (i == 1) {
System.out.println("END");
return;
}
synchronized (Visibility.class){}
}
}
});
thread_1.start();
Thread.sleep(1000);
synchronized (Visibility.class) {
i = 1;
}
}
}
我在我的笔记本电脑上运行它,它总是打印“END”,但我想知道 JVM 是否保证此代码将始终打印“END”?
此外,如果我们在空块内添加一行,它就变成了:synchronized
public class Visibility {
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread_1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(i);
while (true) {
if (i == 1) {
System.out.println("END");
return;
}
synchronized (Visibility.class){
int x = i; // Added this line.
}
}
}
});
thread_1.start();
Thread.sleep(1000);
synchronized (Visibility.class) {
i = 1;
}
}
}
现在,JVM是否保证此代码将始终打印“END”?
答:
如果 hb(x, y) 和 hb(y, z),则 hb(x, z)。
换句话说,HB(Happens-before)是传递的。HB 是可观测性的主要参与者:如果 - 而这正是您要做的(或者更确切地说,防止发生),则不可能像执行 X 行之前那样观察 Y 行中的状态: 您感兴趣的是行 Y () 是否可以观察行 X 之前的状态 (, 在代码段底部的同步块中)。hb(x, y)
if (i == 1)
i = 1
鉴于传递性规则,特定代码段的答案是“是” - 您可以保证它打印 .在将一个特定片段的分析外推到更一般的情况时,请始终小心 - 这些东西不容易简化,您必须每次都应用 happens-before 分析(或者,更常见的是,通过字段写入尽可能多地避免线程之间的交互):END
hb(exiting a synchronized block, entering it)
是一回事。获取监视器的不同线程是有序的,并且它们之间存在 HB 关系。因此,线程中的倒数第二个(退出块)1 和零语句块之间存在 hb 关系。}
synchronized
如果内部线程之后以某种方式运行(很奇怪,因为这意味着它甚至需要 1 秒以上的时间才能启动,但从技术上讲,JVM 不做时序保证,所以理论上是可能的),已经是 1。此更改可能尚未“同步”,除非 while 循环命中了该无内容块,然后建立了 hb,从而强制可见性 存在 。
i
synchronized
i
1
如果内线程之前运行(100% 的所有情况,那里有什么,差不多),那么最终适用相同的逻辑。
Thread.sleep(1000)
为了真正将它们加在一起,我们需要添加“自然”hb 规则:字节码 X 和 Y 确定 X 和 Y 是否由同一线程执行,并且 Y 是否按程序顺序在 X 之后。即给定:你不能像以前一样观察 y 存在 - 这是“咄!HB 规则 - 当然,如果 JVM 可以随意在单个线程中重新排序内容,那么 java 作为一门语言将毫无用处。那+传递性规则就足够了。
hb(x, y)
y = 5; System.out.println(y)
y = 5;
这里有一个技术问题
您启动的线程永远不会自愿放弃,这可能会造成一些破坏(该代码中的任何内容都不会“休眠”CPU)。你永远不应该编写像这样繁忙旋转的代码,JVM 当时对事情是如何工作的不是特别清楚,这会导致 CPU 出现巨大的热量/功率问题!在单核处理器上,JVM 基本上被允许将所有时间都花在繁忙的等待上,永远,这是使 JVM 永远不打印的一种方法:因为设置为 1 的主线程永远不会绕过它。这打破了线程的一般要点。 引入了一个保存点,因此它最终会被抢占,但这可能需要相当长的时间。在正确的硬件上,它可能需要远远超过一秒钟的时间。END
i
synchronized
通过在繁忙循环中推入某种或,或使用锁/信号灯/等来轻松修复。sleep
wait
j.u.concurrent
但是这个空块不会被优化出来吗?synchronized
不。JLS 几乎决定了必须产生什么的字节。不允许优化空循环。它是热点引擎(即 - 运行时)可以做一些事情,比如“哦,这个循环没有我应该保证的可观察效果,因此,我可以完全优化整个事情”,而 JMM 17.4.5 表明它不能这样做。如果 JVM 实现“优化它”,那将是错误的。javac
java.exe
我们可以通过以下方式确认这一点:javap
> cat Test.java
class Test {
void test() {
synchronized(this) {}
}
}
> javac Test.java; javap -c -v Test
[ ... loads of output elided ...]
3: monitorenter
4: aload_1
5: monitorexit
monitorenter
是块中左大括号的字节码,是右大括号的字节码(或任何其他控制流出一个 - 异常抛出和//也使发出字节码。synchronized (x){}
monitorexit
break
continue
return
javac
monitorexit
结束语
我认为这个问题是本着这样的精神提出的:所以......这里会发生什么?不是本着“这样写好吗”的精神。这是不行的 - 除了旋转循环(从来都不是好事),迫使读者进行鹅追逐,确定 HB 实际上是为确保此代码执行您认为它所做的,并且需要有关 HB 规则集的多个详细信息(同步的东西和传递性规则以及空同步块没有被优化的知识) - 更不用说明显的一些奇怪的代码(一个空的同步块),尽管如此这里确实有一个功能,这些都不是特别可维护的。完成此特定工作的正确方法很可能是使用简单的锁 from ,或者至少移动块以包含块中的所有内容。此外,锁定你直接控制之外的代码可以获取的东西(并且,微不足道的是,一个全局单例)是一个非常糟糕的主意:唯一不间断的方法是广泛记录你的锁定行为(因此,你现在已经注册无限期地维护该行为,或者如果你改变它,你被迫发布一个主要版本(即向后不兼容)。你几乎总是想锁定你控制的东西——即 - 不可能在您的直接控制之外引用的对象。java.util.concurrent
synchronized
while
Visibility.class
private final Object lock = new Object[0];
[1] 从技术上讲,HB 是 JVM 事务,适用于字节码。然而,这个右大括号确实有一个等价的字节码(大多数右大括号没有;那个有):它是显示器的释放(“释放”锁)。这恰恰是 HB 的字节码,相对于同一锁对象的后续排序获取。synchronized
评论
no
synchronized
synchronized
synchronized (Visibility.class){ int x = i;}
评论
synchronized
i
synchronized
synchronized
i
volatile
i