Java 中的构造函数同步

有人告诉我,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中走了多远,以及这样的代码是否会有问题。我最近看到像这样的代码,它这样做而不是使用静态工厂方法,我想知道这在多线程系统中有多危险(或安全)。


答案 1

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

事实并非如此。没有与构造函数的隐含同步。不仅多个构造函数可以同时发生,而且您可以通过例如在构造函数内部分叉线程并引用正在构造的构造函数来获得并发问题。this

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

不,它不会。

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

在你的例子中,由于你正在把你的放入同步映射,然后继续执行初始化,正如@Tim提到的,这将允许其他线程在可能的半初始化状态下获得对象。一种解决方案是使用一种方法来创建对象:Teststatic

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;
}

我的示例代码之所以有效,是因为您正在处理一个同步映射,该映射会进行调用,以确保构造函数已完成并已同步内存。synchronizedTest

示例中的最大问题是“发生在之前”保证(构造函数在放入映射之前可能无法完成)和内存同步(构造线程和 get-ing 线程可能会看到实例的不同内存)。如果移动构造函数的外部,则两者都由同步映射处理。无论它位于哪个对象上,都可以保证构造函数在放入映射之前已经完成并且内存已同步。TestTestputsynchronized

我相信,如果你在构造函数的最后调用,你在实践中可能没问题,但这不是一个好的形式,至少需要仔细的注释/文档。如果类被子类化,并且在 之后的子类中完成初始化,则这不会解决问题。我展示的解决方案是一个更好的模式。testsById.put(this.id, this);super()static


答案 2

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

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

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

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

此代码在调用时引发。如果同步有效,则不会。java.lang.IllegalMonitorStateExceptionwait()

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

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

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


推荐