初始化对象的Java线程安全性

时间:2013-12-28 01:56:28

标签: java concurrency thread-safety

考虑以下课程:

public class MyClass
{
    private MyObject obj;

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

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

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

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

6 个答案:

答案 0 :(得分:7)

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

如果从构造函数中泄露this引用,则可以执行此操作。例如

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

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

由于您没有我上面描述的内容,因此您的代码就可以了。

答案 1 :(得分:3)

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

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

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

这太技术性了,需要进一步阅读才能理解。好消息是,你不需要掌握“不安全的出版物”,因为无论如何这是一种沮丧的做法。最佳做法是:绝不做不安全的出版物; 永远不会进行数据竞争; 即。始终使用synchronized, volatilejava.util.concurrent通过适当的同步来读取/写入共享数据。

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

答案 2 :(得分:1)

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

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

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

请注意,如果对volatile的引用从某个构造函数内的类中传出,那么该引用可能会在构造函数完成之前浮动并被使用。这就是对象的 unsafe publishing ,但是你的代码不会从构造函数中调用非this方法(或直接传递对final的引用)很好。

答案 3 :(得分:0)

您的其他主题可以查看空对象。易失性对象可能有所帮助,但显式锁机制(或构建器)可能是更好的解决方案。

查看Java Concurrency in Practice - Sample 14.12

答案 4 :(得分:0)

此类(如果按原样)不是线程安全的。用两个词来说:在java(Instruction reordering & happens-before relationship in java)中重新排序指令,在你的代码中你实例化MyClass,在某些情况下你可能会得到以下指令集:

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

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

有关详细信息,我会严格建议您阅读好书“Java Concurrency in Practice”。确切地说,您的案例在第50-51页(第3.5.1节)中有描述。我甚至会说 - 您只需编写正确的多线程代码而无需阅读该书! :)

答案 5 :(得分:-1)

我认为选择的答案 (@Sotirios Delimanolis) 是错误的。 @ZhongYu 的回答是正确的。

如果 MyClass 发布不安全,任何事情都可能发生。