同步锁之外的 Java 内存可见性

Java memory visibility outside the synchronized lock

提问人:Anonemous 提问时间:9/25/2023 最后编辑:Anonemous 更新时间:9/25/2023 访问量:101

问:

同步锁是否保证以下代码始终打印“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”?

Java 多线程 同步 Memory-Barrier 内存可见性

评论

1赞 Maurice Perry 9/25/2023
为什么是空块?synchronized
0赞 Anonemous 9/25/2023
@MauricePerry 如果没有空的同步块,代码将始终运行并且永远不会退出。
0赞 Maurice Perry 9/25/2023
它接缝你的目标是保护。所以我你应该把测试放在块内。isynchronized
0赞 Anonemous 9/25/2023
@MauricePerry 我想知道为什么空的使代码退出?synchronized
1赞 Joachim Sauer 9/25/2023
AFAIK 这是一种副作用,不能保证。如果未声明,则无法保证循环内部的观测值会发生变化。在许多情况下(使用当前的 OpenJDK 实现)它会变得可见,但这些情况不是规范所保证的。ivolatilei

答:

4赞 rzwitserloot 9/25/2023 #1

根据 JLS §17.4.5:在顺序之前发生:

如果 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,从而强制可见性 存在 。isynchronizedi1

  • 如果内线程之前运行(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 的主线程永远不会绕过它。这打破了线程的一般要点。 引入了一个保存点,因此它最终会被抢占,但这可能需要相当长的时间。在正确的硬件上,它可能需要远远超过一秒钟的时间。ENDisynchronized

通过在繁忙循环中推入某种或,或使用锁/信号灯/等来轻松修复。sleepwaitj.u.concurrent

但是这个空块不会被优化出来吗?synchronized

不。JLS 几乎决定了必须产生什么的字节。不允许优化空循环。它是热点引擎(即 - 运行时)可以做一些事情,比如“哦,这个循环没有我应该保证的可观察效果,因此,我可以完全优化整个事情”,而 JMM 17.4.5 表明它不能这样做。如果 JVM 实现“优化它”,那将是错误的。javacjava.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){}monitorexitbreakcontinuereturnjavacmonitorexit

结束语

我认为这个问题是本着这样的精神提出的:所以......这里会发生什么?不是本着“这样写好吗”的精神。这是行的 - 除了旋转循环(从来都不是好事),迫使读者进行鹅追逐,确定 HB 实际上是为确保此代码执行您认为它所做的,并且需要有关 HB 规则集的多个详细信息(同步的东西传递性规则以及空同步块没有被优化的知识) - 更不用说明显的一些奇怪的代码(一个空的同步块),尽管如此这里确实有一个功能,这些都不是特别可维护的。完成此特定工作的正确方法很可能是使用简单的锁 from ,或者至少移动块以包含块中的所有内容。此外,锁定你直接控制之外的代码可以获取的东西(并且,微不足道的是,一个全局单例)是一个非常糟糕的主意:唯一不间断的方法是广泛记录你的锁定行为(因此,你现在已经注册无限期地维护该行为,或者如果你改变它,你被迫发布一个主要版本(即向后不兼容)。你几乎总是想锁定你控制的东西——即 - 不可能在您的直接控制之外引用的对象。java.util.concurrentsynchronizedwhileVisibility.classprivate final Object lock = new Object[0];


[1] 从技术上讲,HB 是 JVM 事务,适用于字节码。然而,这个右大括号确实有一个等价的字节码(大多数右大括号没有;那个有):它是显示器的释放(“释放”锁)。这恰恰是 HB 的字节码,相对于同一锁对象的后续排序获取。synchronized

评论

0赞 Joachim Sauer 9/25/2023
一如既往的详细答案。唯一的小问题:我会更明确地说明“答案是否定的”是什么意思,因为这在很大程度上取决于问题的具体措辞,有时问题不清楚他们以哪种方式表达它(或在整个问题中使用不同的措辞)。
0赞 rzwitserloot 9/25/2023
@JoachimSauer,通过将其更改为“是”,我已经对我的一点限定了,你知道,在眼球和大脑神经元之间,问题变成了“这段代码有问题吗?”,但实际问题是:“它能保证打印'END'吗”。对于这个非常具体的代码,我理解 JMM 的方式就是这样,是的,这是一种保证。对于安可,我添加了一个附录,深入研究了块的空性是否是一个问题。事实并非如此。nosynchronized
0赞 Anonemous 9/25/2023
感谢您的精彩回答,还有一个问题,如果我们在空处添加一行,它会变成:,现在 JVM 是否保证它将始终打印“END”?synchronizedsynchronized (Visibility.class){ int x = i;}
0赞 rzwitserloot 9/25/2023
@Anonemous我以一种相当关键的方式编辑了答案 - 它保证始终打印 END。“不”的意思是:“不,这段代码没有 JMM 问题”。