23

有人告诉我Java构造函数是同步的,因此在构造过程中不能同时访问它,我想知道:如果我有一个将对象存储在映射中的构造函数,并且另一个线程在构建之前从该映射中检索它完成后,该线程会阻塞直到构造函数完成吗?

让我用一些代码来演示:

public class Test {
    private static final Map<Integer, Test> testsById =
            Collections.synchronizedMap(new HashMap<>());
    private static final AtomicInteger atomicIdGenerator = new AtomicInteger();
    private final int id;

    public Test() {
        this.id = atomicIdGenerator.getAndIncrement();
        testsById.put(this.id, this);
        // Some lengthy operation to fully initialize this object
    }

    public static Test getTestById(int id) {
        return testsById.get(id);
    }
}

假设 put/get 是地图上唯一的操作,所以我不会通过迭代之类的方法获得 CME,并尝试忽略此处的其他明显缺陷。

我想知道的是,如果另一个线程(显然不是构造对象的线程)尝试使用getTestById并调用对象来访问对象,它会阻塞吗?换句话说:

Test test = getTestById(someId);
test.doSomething(); // Does this line block until the constructor is done?

我只是想澄清构造函数同步在 Java 中的进展程度,以及这样的代码是否会出现问题。我最近看到这样的代码,而不是使用静态工厂方法,我想知道这在多线程系统中有多危险(或安全)。

4

6 回答 6

25

有人告诉我Java构造函数是同步的,因此在构造过程中不能同时访问它

当然不是这样。与构造函数没有隐含的同步。不仅可以同时发生多个构造函数,而且还可以通过例如在构造函数内部分叉一个线程并引用this正在构造的构造函数来解决并发问题。

如果我有一个将对象存储在映射中的构造函数,并且另一个线程在其构造完成之前从该映射中检索它,那么该线程会阻塞直到构造函数完成吗?

不,不会的。

线程应用程序中构造函数的最大问题是编译器有权在 Java 内存模型下对构造函数内部的操作进行重新排序,以便它们发生(所有事情中)对象引用被创建并且构造函数完成之后。 final字段将保证在构造函数完成时完全初始化,而不是其他“正常”字段。

在您的情况下,由于您将您Test放入同步映射然后继续进行初始化,正如@Tim 所提到的,这将允许其他线程以可能的半初始化状态获取对象。一种解决方案是使用一种static方法来创建您的对象:

private Test() {
    this.id = atomicIdGenerator.getAndIncrement();
    // Some lengthy operation to fully initialize this object
}

public static Test createTest() {
    Test test = new Test();
    // this put to a synchronized map forces a happens-before of Test constructor
    testsById.put(test.id, test);
    return test;
}

我的示例代码有效,因为您正在处理一个同步映射,它调用synchronized确保Test构造函数已完成并已实现内存同步。

您的示例中的大问题是“发生之前”保证(构造函数在Test放入映射之前可能无法完成)和内存同步(构造线程和获取线程可能会看到Test实例的不同内存)。如果您移动put构造函数的外部,则两者都由同步映射处理。不管它在什么对象synchronized 上,都可以保证构造函数在放入映射之前已经完成并且内存已经同步。

我相信,如果您testsById.put(this.id, this);在构造函数的最后调用,您实际上可能没问题,但这不是好的形式,至少需要仔细的评论/文档。如果该类是子类并且在super(). 我展示的static解决方案是一个更好的模式。

于 2012-09-26T22:25:52.073 回答
15

有人告诉我Java构造函数是同步的

“某个地方的某个人”被严重误导。构造函数不同步。证明:

public class A
{
    public A() throws InterruptedException
    {
        wait();
    }

    public static void main(String[] args) throws Exception
    {
        A a = new A();
    }
}

java.lang.IllegalMonitorStateException此代码在wait()调用时抛出。如果同步有效,则不会。

这甚至没有意义。它们不需要同步。只能在 a 之后调用构造函数,new(),并且根据定义,每次调用都new()返回不同的值。因此,两个线程同时调用具有相同值的构造函数的可能性为零this。所以不需要构造函数的同步。

如果我有一个将对象存储在映射中的构造函数,并且另一个线程在其构造完成之前从该映射中检索它,那么该线程会阻塞直到构造函数完成吗?

不,它为什么要这样做?谁来阻止它?让'this'从这样的构造函数中逃脱是不好的做法:它允许其他线程访问仍在构建中的对象。

于 2012-09-27T10:28:35.027 回答
13

你被误导了。您所描述的实际上被称为不当发布,并在 Java Concurrency In Practice 一书中进行了详细讨论。

所以是的,另一个线程有可能获得对您的对象的引用并在它完成初始化之前开始尝试使用它。但是等等,考虑这个答案会变得更糟:https ://stackoverflow.com/a/2624784/122207 ...基本上可以重新排序引用分配和构造函数完成。在引用的示例中,一个线程可以分配h = new Holder(i),另一个线程调用h.assertSanity()新实例,时机恰到好处,以便为在的构造函数n中分配的成员获取两个不同的值。Holder

于 2012-09-26T22:26:05.803 回答
2

构造函数就像其他方法一样,没有额外的同步(处理final字段除外)。

this如果稍后发布该代码将起作用

public Test() 
{
    // Some lengthy operation to fully initialize this object

    this.id = atomicIdGenerator.getAndIncrement();
    testsById.put(this.id, this);
}
于 2012-09-26T22:52:38.497 回答
1

虽然这个问题得到了回答,但是粘贴的代码没有遵循安全的构造技术,因为它允许这个引用从构造函数中逃逸,我想分享一下 Brian Goetz 在文章中提出的一个漂亮的解释:“Java 理论与实践:安全构造技术”在 IBM developerWorks 网站上

于 2013-12-07T12:24:49.433 回答
-1

这是不安全的。JVM 中没有额外的同步。你可以这样做:

public class Test {
    private final Object lock = new Object();
    public Test() {
        synchronized (lock) {
            // your improper object reference publication
            // long initialization
        }
    }

    public void doSomething() {
        synchronized (lock) {
            // do something
        }
    }
}
于 2012-09-26T22:32:52.170 回答