【问题标题】:Java Thread Safety of Initialized Objects已初始化对象的 Java 线程安全
【发布时间】:2013-12-28 01:56:27
【问题描述】:

考虑以下类:

public class MyClass
{
    private MyObject obj;

    public MyClass()
    {
        obj = new MyObject();
    }

    public void methodCalledByOtherThreads()
    {
        obj.doStuff();
    }
}

既然 obj 是在一个线程上创建并从另一个线程访问的,那么在调用 methodCalledByOtherThread 时 obj 可以为 null 吗?如果是这样,将 obj 声明为 volatile 是解决此问题的最佳方法吗?将 obj 声明为 final 会有什么不同吗?

编辑: 为了清楚起见,我认为我的主要问题是: 其他线程是否可以看到 obj 已被某个主线程初始化,或者 obj 是否已过时(null)?

【问题讨论】:

  • 那么您是在问是否有可能让另一个线程获得对MyClass 类对象的引用在其构造函数完成之前?好问题!不过试试看。尝试在另一个线程上编写代码。 :)
  • 使用当前代码,不,因为this 引用不会转义构造函数。
  • @SotiriosDelimanolis 您可能应该将其作为答案而不是评论。我认为这个问题非常好,值得一个可以接受的答案。只是一个想法,你可能不同意.....
  • @RayToal 不完全是。我相当肯定这是不可能的。我想知道 methodCalledByOtherThreads 是否可能读取 obj 的陈旧值。

标签: java concurrency thread-safety


【解决方案1】:

为了使methodCalledByOtherThreads 被另一个线程调用并导致问题,该线程必须获得对MyClass 对象的引用,该对象的obj 字段未初始化,即。构造函数尚未返回的地方。

如果您从构造函数中泄露了this 引用,这将是可能的。例如

public MyClass()
{
    SomeClass.leak(this); 
    obj = new MyObject();
}

如果SomeClass.leak() 方法启动一个单独的线程,该线程在this 引用上调用methodCalledByOtherThreads(),那么您就会遇到问题,但不管volatile 是什么,都是如此。

由于您没有我上面描述的内容,因此您的代码很好。

【讨论】:

  • 你能解释一下stackoverflow.com/questions/17728710/…stackoverflow.com/questions/6457109/…的答案吗?他们好像说代码不行。谢谢
  • @Xor 你最好分别问回答他们的人。
  • 也许我措辞错误。你不需要解释别人的答案。无论如何,发布的第二个链接似乎与您的答案相矛盾:它表示构造函数中分配的值不一定是调用 methodCalledByOtherThreads() 看到的线程。
  • @Xor 在您当前的代码中,methodCalledByOtherThreads 只能在构造函数完成后被另一个线程调用。到那时,另一个Thread 将看到它的原样。在问题本身中,请参阅引用 If an object is properly constructed (which means that references to it do not escape during construction)
【解决方案2】:

这取决于引用是否“不安全”地发布。引用通过写入共享变量来“发布”;另一个线程读取变量以获取引用。如果不存在happens-before(write, read) 的关系,则该发布称为不安全。不安全发布的一个示例是通过非易失性静态字段。

@chrylis 对“不安全发布”的解释并不准确。在构造函数退出之前泄漏this 与不安全发布的概念是正交的。

通过不安全的发布,另一个线程可能会观察到处于不确定状态的对象(因此得名);在您的情况下,字段 obj 对于另一个线程可能看起来为空。除非objfinal,否则即使宿主对象发布不安全,它也不会显示为空。

这太技术性了,需要进一步阅读才能理解。好消息是,您不需要掌握“不安全发布”,因为无论如何这是一种不鼓励的做法。最好的做法很简单:永远不要做不安全的发布; 从不进行数据竞赛; 始终通过正确的同步读取/写入共享数据,使用synchronized, volatilejava.util.concurrent

如果我们总是避免不安全的发布,我们是否仍然需要 final 字段?答案是不。那么为什么某些对象(例如String)通过使用最终字段设计为“线程安全不可变”?因为假设它们可用于通过故意不安全发布来试图创建不确定状态的恶意代码。我认为这是一个过分的担忧。这在服务器环境中没有多大意义——如果应用程序嵌入了恶意代码,服务器就会受到威胁。在 JVM 运行来自未知来源的不受信任的代码的 Applet 环境中,这可能有点意义——即便如此,这也是一个不太可能的攻击向量;这种攻击没有先例;显然,还有很多其他更容易被利用的安全漏洞。

【讨论】:

    【解决方案3】:

    这段代码很好,因为在构造函数返回之前,任何其他线程都无法看到对 MyClass 实例的引用。

    具体来说,happens-before relation 要求动作的可见效果按照它们在程序代码中列出的顺序发生,因此在构造MyClass 的线程中,obj 必须是明确的在构造函数返回之前赋值,并且实例化线程直接从没有对MyClass对象的引用状态进入对完全构造的MyClass对象的引用。

    然后该线程可以将对该对象的引用传递给另一个线程,但是所有的构造都将在第二个线程可以调用它的任何方法之前传递地发生。这可能通过构造线程启动第二个线程、synchronized 方法、volatile 字段或其他并发机制发生,但所有这些都将确保在实例化线程中发生的所有操作都完成在内存屏障通过之前。

    请注意,如果对 this 的引用从构造函数内部的某个类中传递出去,则该引用可能会四处浮动并在构造函数完成之前被使用。这就是所谓的对象的不安全发布,但是像你这样的代码不会从构造函数调用非final 方法(或直接传递对this 的引用)是可以的。

    【讨论】:

      【解决方案4】:

      您的其他线程可能看到一个空对象。 volatile 对象可能会有所帮助,但显式锁定机制(或 Builder)可能是更好的解决方案。

      看看Java Concurrency in Practice - Sample 14.12

      【讨论】:

      • 我相信这个答案是正确的,但你能解释一下为什么显式锁或生成器会更好。将 obj 声明为 final 的 volatile 似乎要简单得多,但同样有效。
      • 构建器肯定不会在对象准备好之前留下对对象的引用(并使该变量成为最终变量!)。
      • Volatile 不是你想要的(你不想要空指针对吗?)而且现在它的速度慢得惊人。不惜一切代价避免。 Final 更好,但如果另一个线程尝试读取变量,则不会保护您。
      • 在这种特殊情况下,Sotirios 是正确的。在对象完成之前,指针不能滑出。
      • 不,另一个线程看不到它。在构造函数返回之前不存在对 MyClass 对象的引用,并且此时肯定会分配 obj。在这种情况发生之前,其他线程无法获得对 MyClass 的引用。
      【解决方案5】:

      此类(如果按原样使用)不是线程安全的。简而言之:java (Instruction reordering & happens-before relationship in java) 中的指令重新排序,当您在代码中实例化 MyClass 时,在某些情况下您可能会得到以下指令集:

        • 为 MyClass 的新实例分配内存;
        • 返回此内存块的链接;
        • 链接到这个未完全初始化的 MyClass 可用于其他线程,他们可以调用“methodCalledByOtherThreads()”并得到 NullPointerException;
        • 初始化 MyClass 的内部。

        为了防止这种情况并使您的 MyClass 真正线程安全 - 您必须将“final”或“volatile”添加到“obj”字段。在这种情况下,Java 的内存模型(从 Java 5 开始)将保证在 MyClass 的初始化过程中,只有在所有内部都初始化时才会返回对分配给它的内存块的引用。

        有关更多详细信息,我强烈建议您阅读好书“Java 并发实践”。第 50-51 页(第 3.5.1 节)准确描述了您的案例。我什至会说——您无需阅读那本书就可以编写正确的多线程代码! :)

        【讨论】:

          【解决方案6】:

          @Sotirios Delimanolis 最初选择的答案是错误的。 @ZhongYu 的回答是正确的。

          这里存在关注的可见性问题。因此,如果 MyClass 不安全地发布,任何事情都可能发生。

          评论中有人要求提供证据——可以查看Java Concurrency in Practice一书中的清单 3.15:

          public class Holder { 
              private int n;
              
              // Initialize in thread A
              public Holder(int n) { this.n = n; }
              
              // Called in thread B
              public void assertSanity() { 
                  if (n != n) throw new AssertionError("This statement is false."); 
              }
          
          }
          

          有人想出了一个例子来验证这段代码:

          coding a proof for potential concurrency issue

          至于本帖的具体例子:

          public class MyClass{
              private MyObject obj;
              
              // Initialize in thread A
              public MyClass(){
                  obj = new MyObject();
              }
              
              // Called in thread B
              public void methodCalledByOtherThreads(){
                  obj.doStuff();
              }
          }
          

          如果 MyClass 在线程 A 中初始化,则无法保证线程 B 会看到此初始化(因为更改可能会保留在线程 A 运行的 CPU 的缓存中,并且尚未传播到主内存中)。

          正如@ZhongYu 所指出的,因为读写发生在两个独立的线程上,所以没有happens-before(write, read) 关系。

          为了解决这个问题,正如原作者所说,我们可以将私有 MyObject obj 声明为 volatile,这将确保引用本身对其他线程及时可见 (https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/volatile-ref-object.html)。

          【讨论】:

          • 如果您想讨论其他答案,请添加评论而不是发布新答案。此外,请添加证据。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2010-10-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-02-26
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多