繁体   English   中英

同步块后读取的可见性

[英]Visibility of a read after synchronized block

JMM 是否保证在synchronized块之后对在另一个线程中读取的变量进行synchronized写入的可见性? 这就是我的意思:

public class SynchronizedWriteRead {

    private int a;
    private int b;

    public void writer() {
        synchronized (this) {
            a = 5;
            b = 7;
        }
    }

    public void reader() {
        synchronized (this) {
            int r1 = a; /* 5 */
        }
        int r2 = b; /* ? */
    }
}

JMM 保证监视器上的解锁发生在该监视器上的每个后续锁定之前。 但我不确定它是否仅与synchronized块体有关。

最近我在 Java 中遇到了 Aleksey Shipilëv 的这篇文章 - 安全发布和安全初始化 它说:

请注意,在Unsafe DCL存储中进行synchronized是如何无济于事的,这与外行人的看法相反,它以某种方式神奇地“刷新了缓存”或诸如此类的东西。 在读取受保护的 state 时,如果没有配对锁,则无法保证在锁保护写入之前看到写入。

所以这就是我问自己这个问题的原因。 我在JLS中找不到答案。

让我们换一种说法。 有时你会捎带一个volatile发生之前的保证,如下所示:

public class VolatileHappensBefore {

    private int a; /* specifically non-volatile */
    private volatile int b;

    public void writer() {
        a = 5;
        b = 7;
    }

    public void reader() {
        int r1 = b; /* 7 */
        int r2 = a; /* 5 */
    }
}

因为同一线程中的顺序操作由happens-before 支持,并且happens-before 本身是可传递的,所以您可以保证看到这两种写入。

我可以以相同的方式使用synchronized发生之前的保证吗? 甚至可能像这样(我已经放置了sync变量以禁止编译器/JVM删除其他空的synchronized块):

    public void writer() {
        a = 5;
        b = 7;
        synchronized (this) {
            sync = 1;
        }
    }

    public void reader() {
        synchronized (this) {
            int r = sync;
        }
        int r1 = a; /* ? */
        int r2 = b; /* ? */
    }

你现在已经得到了答案,公平地说,这里的每个人都是正确的,我只想添加一个重要的规则。 它被称为“在一致性之前发生”,它是这样的:

  • 读取要么看到发生在发生前顺序中的最新写入,要么看到任何其他未按照发生前发生顺序写入的写入,因此是数据竞争。

因此,虽然公认的答案确实是正确的,但应该提到,为了在(3)(4)之间创建发生前的边缘, (4)必须观察(3)所做的写入。

在您的示例中:

public void reader() {
   synchronized (this) {
      int r = sync;
   }
   int r1 = a; /* ? */
   int r2 = b; /* ? */

}

这意味着int r = sync; 不正确,您需要断言您实际上看到sync1 (您已观察到写入)。 例如,这将创建所需的边缘:

if (sync == 1) {
    // guaranteed that r1=5 and r2=7
    int r1 = a;
    int r2 = b;
}

JMM 是否保证在synchronized块之后对在另一个线程中读取的变量进行synchronized写入的可见性?

我可以以相同的方式使用同步发生之前的保证吗?

是的,是的。

synchronizedvolatile提供了相同的可见性保证。

事实上,一个volatile变量的行为就好像这个变量的每次读取和写入都发生在它自己的小synchronized块中。

JLS条款中:

  • 可见性由happens-before关系保证:

    非正式地,如果没有发生之前的命令以防止读取,则允许读取r查看写入w的结果。

  • volatilesynchronized是建立前发生关系的一些方法:
    • 监视器上的解锁发生在该监视器上的每个后续锁定之前
    • 对 volatile 字段(第 8.3.1.4 节)的写入发生在对该字段的每次后续读取之前
    • ...

Java 中安全发布和安全初始化的引用描述了以下情况:

  • object 在一个线程的synchronized块中初始化
  • object 并且在另一个线程中没有synchronized块的情况下读取。

在这种情况下,阅读器线程可能会在初始化过程中看到 object。

这里需要注意的重要一点是, happens-before是一个传递关系。 因此,如果A发生在BB发生在C之前,我们可以安全地得出A发生在C之前的结论。

现在让我们看看有问题的代码(为了清楚起见,我添加了注释):

public void writer() {
    a = 5; //1
    b = 7; //2
    synchronized (this) { 
        sync = 1;
    } //3
}

public void reader() {
    synchronized (this) { //4
        int r = sync;
    }
    int r1 = a; //5 
    int r2 = b; //6
}

我们知道1发生在2之前和2发生在3之前,因为它们是由同一个线程执行的:

如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)。

我们也知道3发生在4之前:

监视器 m 上的解锁操作与 m 上的所有后续锁定操作同步(其中“后续”根据同步顺序定义)。

如果动作 x 与后续动作 y 同步,那么我们也有 hb(x, y)。

最后,我们知道4发生在5之前和5发生在6之前,因为它们在同一个线程中执行。

如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)。

所以我们最终得到了从 1 到 6 的一系列发生之前的关系。因此, 1发生在6之前。

JMM 是否保证在同步块之后对在另一个线程中读取的变量进行同步写入的可见性?

是的,由于其他答案中给出的原因。

但是有一个问题!

让我们假设reader调用跟在writer调用之后,因此在writer退出其同步块和 reader 进入其块之间之前发生了一个事件......并传递它退出块并读取b

问题是当reader退出其块时,不再保证b上的互斥。 所以假设另一个线程立即获取互斥锁并修改b 由于没有 HB 链写入b与 read in reader连接起来,因此无法保证reader代码实际看到更新...或writer之前写入的值。

简而言之,在推理并发算法时,需要考虑的不仅仅是HB 关系。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM