为什么 Java 构造函数不能同步?

根据 Java 语言规范,构造函数不能标记为同步,因为在创建对象的线程完成之前,其他线程无法看到正在创建的对象。这似乎有点奇怪,因为我确实可以在构造对象时有另一个线程查看对象:

public class Test {
    public Test() {
       final Test me = this;
       new Thread() {
           @Override
           public void run() {
               // ... Reference 'me,' the object being constructed
           }
       }.start();
    }
}

我知道这是一个非常人为的例子,但从理论上讲,似乎有人可以提出一个更现实的情况,其中将构造函数标记为同步是合法的,以防止与这样的线程进行比赛。

我的问题是:Java特别禁止在构造函数上使用同步修饰符,这是有原因的吗?也许我上面的例子是有缺陷的,或者也许真的没有理由,这是一个武断的设计决策。无论哪种情况,我都很好奇,很想知道答案。


答案 1

如果您确实需要将构造函数的其余部分与以任何方式获取对尚未完全构造的对象的引用的任何线程进行同步,则可以使用同步块:

public class Test {
    public Test() {
       final Test me = this;
       synchronized(this) {
          new Thread() {
             @Override
             public void run() {
                // ... Reference 'me,' the object being constructed
                synchronized(me) {
                   // do something dangerous with 'me'.
                }
             }
          }.start();
          // do something dangerous with this
       }
    }
}

通常,像这样“提供”尚未构造的对象被认为是不好的样式,因此不需要同步构造函数。


在某些极端情况下,同步构造函数会很有用。这里有一个更现实的例子,来自Bozho答案的讨论:

public abstract class SuperClass {

   public SuperClass() {
       new Thread("evil") { public void run() {
          doSomethingDangerous();
       }}).start();
       try {
          Thread.sleep(5000);
       }
       catch(InterruptedException ex) { /* ignore */ }
   }

   public abstract void doSomethingDangerous();

}

public class SubClass extends SuperClass {
    int number;
    public SubClass () {
        super();
        number = 2;
    }

    public synchronized void doSomethingDangerous() {
        if(number == 2) {
            System.out.println("everything OK");
        }
        else {
            System.out.println("we have a problem.");
        }
    }

}

我们希望只有在SubClass对象的构造完成后才调用该方法,例如,我们只需要“一切正常”的输出。但是在这种情况下,当您只能编辑子类时,您没有机会实现这一目标。如果构造函数可以同步,它将解决问题。doSomethingDangerous()

所以,我们从中学到的是:如果你的类不是最终的,永远不要像我在超类构造函数中所做的那样做一些事情 - 并且不要从你的构造函数中调用你自己类的任何非最终方法。


答案 2

这个问题已经在Java并发API和Java内存模型的作者使用的讨论列表中提出。给出了几个答案,特别是Hans Boehm回答说

我们中的一些人(包括IIRC)实际上在Java内存模型审议期间认为应该允许同步构造函数。现在我可以走任何一条路。客户端代码不应使用竞赛来传达引用,因此这无关紧要。但是,如果您不信任[您的类]的客户端,我认为同步构造函数可能会很有用。这就是最终字段语义背后的大部分推理。[...]正如David所说,你可以使用同步块。