【问题标题】:Why can't Java constructors be synchronized?为什么 Java 构造函数不能同步?
【发布时间】:2011-06-20 07:01:05
【问题描述】:

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

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

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

我的问题是:Java 是否有理由明确禁止构造函数上的同步修饰符?也许我上面的例子有缺陷,或者真的没有理由,这是一个任意的设计决定。无论哪种情况,我都很好奇,很想知道答案。

【问题讨论】:

  • 顺便说一句,强烈建议在构造函数完成之前不要让“this”引用转义。
  • @Mike Q- 我以前听说过这个,但不完全明白为什么。有什么特别的原因吗?如果您在完成初始化对象之前给出对 this 的引用,我可能会看到坏事发生,但如果这是您在构造函数中做的最后一件事呢?
  • 这确实是另一个问题的主题,但即使它是你在构造函数中做的最后一件事,如果对象被子类化,那么子类还没有完成构造。如果这个类是最终的,并且您没有链接构造函数(调用 this(...))并且在链调用之后执行其他操作并且这是您做的最后一件事,那很好。当然,这些决定中的任何一个都可能改变(您可以稍后添加第二个构造函数)。
  • 如果你引用 JLS 的实际说法会更清楚:“没有实际需要同步构造函数,因为它会锁定正在构建的对象,这通常在对象的所有构造函数完成其工作之前,其他线程不会使用。"

标签: java constructor language-design synchronized


【解决方案1】:

因为synchronized 保证对同一对象的操作不会由多个线程执行。当构造函数被调用时,你仍然没有对象。两个线程访问同一个对象的构造函数在逻辑上是不可能的。

在您的示例中,即使新线程调用了一个方法,它也不再与构造函数有关 - 它与目标方法是否同步有关。

【讨论】:

  • 两个线程不能都调用构造函数是正确的,但是如果一个线程调用构造函数,然后触发另一个线程尝试调用该类的不同同步方法呢?在这种情况下,拥有一个同步构造函数将阻止第二个线程调用任何同步方法,直到构造函数终止。
  • 这不再是关于构造函数,而是关于有问题的方法。
  • 您可以在构造函数中执行 synchronized(this) {}。对构造函数本身进行同步是一派胡言。在开始使对象存在的过程之前,如何获取对象的互斥锁?
  • 创建对象的 JVM 代码分两步 - 分配所有字段默认初始化的对象,然后调用构造函数。在那个中间步骤中,设置对象的监视器似乎是完全合理的,这可以允许构造函数被标记为同步。
  • (我不是反对者。) templatetypedef 的解释是有道理的:如果您不希望您的类(或任何子类或超类)的同步方法之前由另一个线程执行你的构造函数已经完成,甚至超类构造函数也发布了你的对象,你已经丢失了——即使是构造函数中的同步块(就像我的回答一样)也无济于事,因为它不适用于超类构造函数。
【解决方案2】:

在您的示例中,构造函数实际上只从一个线程调用一次。

是的,可以获得对不完全构造的对象的引用(一些关于双重​​检查锁定的讨论以及它被破坏的原因揭示了这个问题),但是,不能通过第二次调用构造函数。

在构造函数上同步会阻止两个线程同时在同一个对象上调用构造函数,这是不可能的,因为永远不可能两次在对象实例上调用构造函数。

【讨论】:

  • 我没有投反对票,但一个可能的原因:构造函数上的同步不仅可以避免两个线程调用同一个线程的构造函数(这无论如何都是不可能的),而且还可以避免调用一个同步的方法这个对象(或以这个对象作为参数进入一个同步块)在另一个线程中。
【解决方案3】:

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

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.");
        }
    }

}

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

因此,我们从中了解到:如果您的类不是最终类,则永远不要像我在超类构造函数中所做的那样 - 并且不要从您的构造函数中调用您自己类的任何非最终方法。

【讨论】:

  • @Paulo,您的示例非常做作。首先,从构造函数调用抽象方法是一种不好的做法,但更重要的是,如果子类上没有线程,则会完全遇到同样的问题。所以syncronized会添加什么?支持极不可能的情况,同时导致子类无论如何都必须以一种对这两种情况都适用的方式解决更常见的情况。
  • @Yishai:你说得对,例子不太现实。但是我见过在构造函数中将自己的对象发布到其他线程的类(不一定在同一个类中,也不一定在睡眠中),然后您可能会遇到同步构造函数可以提供帮助的此类问题。当然,我的例子中的这个方法应该保护自己不被超类构造函数调用。
  • 我也不相信这是一个很好的例子,主要是因为在构造函数中启动线程是不好的做法。
  • @MandeepRajpal 感谢编辑提议,但第一个示例不需要abstract。不过,第二个确实需要它——我在这里添加了它。
【解决方案4】:

我认为没有理由禁止构造函数同步。它在多线程应用程序的许多场景中都很有用。如果我正确理解了 Java 内存模型(我阅读了 http://jeremymanson.blogspot.se/2008/11/what-volatile-means-in-java.html),那么下面的简单类可能会受益于同步构造函数。

public class A {
    private int myInt;

    public /*synchronized*/ A() {
        myInt = 3;
    }

    public synchronized void print() {
        System.out.println(myInt);
    }
}

理论上,我相信对print() 的调用可以打印“0”。如果 A 的实例由线程 1 创建,对实例的引用与线程 2 共享,并且线程 2 调用 print(),则可能会发生这种情况。如果 Thread 1 的 write myInt = 3 和 Thread 2 对同一字段的 read 之间没有特殊的同步,则不能保证 Thread 2 看到写入。

同步的构造函数可以解决这个问题。我说得对吗?

【讨论】:

  • (i) 您描述的问题是 A 的实例未正确发布,在这种情况下,其观察到的状态可能确实是任何东西 (ii) 您可以在构造函数中编写 synchronized (this) { myInt = 3; } 来解决问题,或将myInt 标记为最终问题。
  • 你也可以用volatile关键字标记myInt变量。
  • 不一定。如果对象的引用在完全构造之前泄露给其他线程,那么其他线程也可以调用该对象的非同步方法,在这种情况下,同步构造函数将无济于事。避免后一种情况意味着不应该进行泄漏,此后同步构造函数就变得不必要了。
  • 没有。在您的情况下,线程 1 在其构造函数完成之前不会有 A 的实例,因此它将无法与线程 2 共享它。即使有可能,您也可以简单地将构造函数代码包装在 synchronized 中阻止并获得相同的效果。
  • 这与在构造函数中泄漏 this 无关,更多与 JIT/优化器所做的有关。 - 允许重新排序东西,但不能超出同步操作。因此,如果没有构造函数同步,构造函数可以返回,但 myInt 字段仍然存在于寄存器中,而不是内存中。所以另一个线程准备了一个0。
【解决方案5】:

以下代码可以实现同步构造函数的预期结果。

public class SynchronisedConstructor implements Runnable {

    private int myInt;

    /*synchronized*/ static {
        System.out.println("Within static block");
    }

    public SynchronisedConstructor(){
        super();
        synchronized(this){
            System.out.println("Within sync block in constructor");
            myInt = 3;
        }
    }

    @Override
    public void run() {
        print();
    }

    public synchronized void print() {
        System.out.println(Thread.currentThread().getName());
        System.out.println(myInt);
    }

    public static void main(String[] args) {

        SynchronisedConstructor sc = new SynchronisedConstructor();

        Thread t1 = new Thread(sc);
        t1.setName("t1");
        Thread t2 = new Thread(sc);
        t2.setName("t2");

        t1.start();
        t2.start();
    }
}

【讨论】:

    【解决方案6】:

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

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

    【讨论】:

    • 我不明白,甚至讨论语言是否应该允许它有什么意义,特别是当其他人说它没有意义时,因为“当调用构造函数时你仍然不有对象(要同步)”。所以我觉得它根本不可能用语言来实现。还是我缺少什么?
    • @anir 想象一个static MyObject o; - 线程1 执行o = new MyObject();,而线程2 调用o.getSomething();。存在竞争条件,线程 2 可以观察到部分构造的对象。如果构造函数和getSomething 都是同步的,那么您可以保证看到对象处于一致状态。因此,上面引用中的注释“客户端代码不应该使用竞争来传达引用,所以它应该无关紧要。”。
    【解决方案7】:

    JLS 中的Constructor Modifiers section 明确表示

    There is no practical need for a constructor to be synchronized, because it would
    lock the object under construction, which is normally not made available to other
    threads until all constructors for the object have completed their work.
    

    所以构造函数不需要同步。

    另外,不建议在创建对象之前给出对象引用(this)。一种可能的模棱两可的情况是在创建子类对象时给出对象引用是超类构造函数。

    【讨论】:

    • 你可以同步其他东西。
    【解决方案8】:

    这样的同步在极少数情况下可能有意义,但我想,这不值得:

    • 您始终可以改用同步块
    • 它会以一种非常奇怪的方式支持编码
    • 它应该同步什么? 构造函数是一种静态方法,它在对象上工作,但在没有它的情况下被调用。所以在类上同步也有(一些)意义!

    如有疑问,请忽略它。

    【讨论】:

      【解决方案9】:

      请注意,构造函数无法同步 - 将 synchronized 关键字与构造函数一起使用是语法错误。同步构造函数没有意义,因为只有创建对象的线程才能在构造对象时访问它。

      【讨论】:

        猜你喜欢
        • 2016-02-17
        • 2012-09-18
        • 2012-01-29
        • 1970-01-01
        • 2011-11-05
        • 1970-01-01
        • 2017-04-11
        相关资源
        最近更新 更多