在同步方法中读取值时的安全发布

Posted

技术标签:

【中文标题】在同步方法中读取值时的安全发布【英文标题】:Safe publication when values are read in synchronized methods 【发布时间】:2011-05-01 06:34:07 【问题描述】:

我的问题是关于 Java 中字段值的安全发布(在此处讨论 Java multi-threading & Safe Publication)。

据我了解,如果:

读取和写入在同一监视器上同步 字段是最终字段 字段不稳定

如果我的理解是正确的,那么下面的类应该不是线程安全的,因为初始值是在没有这些特征的情况下编写的。但是,我很难相信我需要创建 first volatile,即使它只能通过 synchronized 方法访问。

public class Foo 

    private boolean needsGreeting = true;

    public synchronized void greet() 
        if (needsGreeting) 
            System.out.println("hello");
            needsGreeting = false;
        
    

我错过了什么吗?上面的代码是否正确,如果正确,为什么?或者在这种情况下是否有必要创建first volatile 或使用final AtomicBoolean 或类似的东西除了synchronized 方法访问它。

(澄清一下,我知道,如果初始值是用synchronized 方法编写的,即使没有volatile 关键字,它也是线程安全的。)

【问题讨论】:

【参考方案1】:

构造函数的结束和方法调用之间没有发生之前的关系,因此一个线程可以开始构造实例并使引用可用,而另一个线程可以获取该引用并开始调用部分构造对象上的 greet() 方法。 greet() 中的同步并没有真正解决这个问题。

如果您通过著名的双重检查锁定模式发布实例,则更容易了解如何操作。如果有这种happens-before关系,即使使用DCLP也应该是安全的。

public class Foo 
    private boolean needsGreeting = true;

    public synchronized void greet() 
        if (needsGreeting) 
            System.out.println("Hello.");
            needsGreeting = false;
        
    


class FooUser 
    private static Foo foo;

    public static Foo getFoo() 
        if (foo == null) 
            synchronized (FooUser.class) 
                if (foo == null) 
                    foo = new Foo();
                
            
        
        return foo;
    

如果多个线程同时调用FooUser.getFoo().greet(),一个线程可能正在构造Foo实例,但是另一个线程可能过早地找到了一个非空的Foo引用,调用greet()并找到needsGreeting 仍然是错误的。

Java Concurrency in Practice (3.5) 中提到了一个例子。

【讨论】:

所以这是真的,即使对foo 的分配是在 new Foo() 被完全评估之后完成的?你有参考证实这一点? 对 foo 的赋值是在执行 new Foo() 的线程中 完成后完成的。从其他线程的角度来看,它可能不是(或可能不是)。只要在执行代码的线程中保持一致,编译器和处理器就可以重新排序执行而不刷新更改。毕竟,这就是可见性的全部意义所在。这就是 DCLP 被破坏的原因:可见性。语言规范的 Java 内存模型部分将是一个很好的参考。正确的可见性只有在适当的发生之前的关系中才能提供。 这也可能有用:cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 令我沮丧的是,您似乎完全正确。我已删除自己的答案并对此表示赞同。【参考方案2】:

严格来说,当调用greet 时,我无法确定将needsGreeting 设置为true。

要做到这一点,在初始写入(在构造对象时发生)和第一次读取(在greet-方法中)之间必须有一个发生之前的关系。然而,JLS 中的 Chapter 17 Threads and Locks 声明了以下关于发生前 (hb) 约束的内容:

17.4.5 下单前发生 两个动作可以通过happens-before关系排序。如果一个动作发生在另一个动作之前,那么第一个动作对第二个动作可见并在第二个动作之前排序。

如果我们有两个动作 x 和 y,我们写 hb(x, y) 来表示 x 发生在 y 之前。

如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则 hb(x, y)。 从对象的构造函数的末尾到该对象的终结器(第 12.6 节)的开头之间存在发生前边缘。 如果动作 x 与后续动作 y 同步,那么我们也有 hb(x, y)。 如果 hb(x, y)hb(y, z),则 hb(x, z)

此外,引入同步关系,即同步顺序的唯一方法是执行以下操作:

同步动作导致动作上的同步关系,定义如下:

监视器 m 上的解锁操作与 m 上的所有后续锁定操作同步(其中后续根据同步顺序定义)。 对 volatile 变量(第 8.3.1.4 节)v 的写入与任何线程对 v 的所有后续读取同步(其中后续根据同步顺序定义)。 启动线程的操作与其启动的线程中的第一个操作同步。 将默认值(零、假或空)写入每个变量与每个线程中的第一个操作同步。虽然在分配包含变量的对象之前将默认值写入变量似乎有点奇怪,但从概念上讲,每个对象都是在程序开始时使用其默认初始化值创建的。 线程 T1 中的最终操作与另一个线程 T2 中检测到 T1 已终止的任何操作同步。 T2 可以通过调用 T1.isAlive() 或 T1.join() 来完成此操作。 如果线程 T1 中断线程 T2,则 T1 的中断与任何其他线程(包括 T2)确定 T2 已中断的任何点同步(通过引发 InterruptedException 或通过调用 Thread.interrupted 或 Thread.isInterrupted) .

它没有说“对象的构造发生在对象上的任何方法调用之前。然而,发生之前的关系表明有一个发生之前的边缘对象的构造函数的结尾到该对象的终结器(第 12.6 节)的开头。,这可能暗示存在没有 从对象的构造函数末尾到任意方法的开头的发生前边缘!

【讨论】:

“[...] 这有点暗示从对象的构造函数的末尾到任意方法的开头没有发生前的边缘!”。这种“暗示”是不正确的。 哦,当然,这不是一个正式的暗示。然而,事实上他们声明构造函数的结束发生在终结器方法的开始之前,我们应该说“提示”这对于任意方法可能不是真的。 @aioobe 不,这仅适用于以下代码:new SomeObject(),即调用构造函数但不存储引用。然后可以立即对对象进行垃圾回收并立即调用终结器。您所指的句子只是确保构造函数在终结器运行之前仍然完成。 为什么投反对票? aioobe 说 JLS 中似乎没有提到,这表示代码是正确的。如果他错了,为什么不参考给我们这个保证的段落(或指出其他资源)? happens-before 关系已明确说明,除非另有说明,否则假定没有happens-before。如果构造函数调用的结束与方法调用具有发生前的关系,则双重检查锁定模式将是安全的。

以上是关于在同步方法中读取值时的安全发布的主要内容,如果未能解决你的问题,请参考以下文章

synchronized同步方法

从另一个同步方法调用同步方法是不是安全?

将变量与 UserDefaults 同步的正确方法

java中多线程安全性和同步的常用方法

39集合线程安全问题

如果不修改静态类变量,非同步静态方法是不是线程安全?