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中构造函数同步的进展,以及这样的代码是否会有问题。我最近看到过这样的代码,它们这样做而不是使用静态工厂方法,我想知道这在多线程系统中有多危险(或安全)。

某处有人告诉我,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溶液是一种更好的模式。

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

"某处某人"信息严重错误。构造函数未同步。证明:

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

此代码在wait()调用中抛出java.lang.IllegalMonitorStateException。如果同步生效,它就不会。

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

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

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

您被误导了。您所描述的内容实际上被称为不适当的发布,并在《Java并发实践》一书中进行了详细讨论。

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

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

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

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

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

这是不安全的。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
}
}
}

最新更新