Почему конструкторы 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 специально запрещает синхронизированный модификатор в конструкторе? Возможно, мой приведенный выше пример ошибочен, или, возможно, действительно нет причин, и это произвольное дизайнерское решение. В любом случае, мне очень интересно и хотелось бы знать ответ.

8 58

8 ответов:

Если вам действительно нужна синхронизация остальной части конструктора по сравнению с любыми потоками, которые так или иначе получают ссылку на ваш еще не полностью построенный объект, вы можете использовать synchronized-block:

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

обычно считается плохим стилем "выдавать" ваш еще не построенный объект, поэтому синхронизированный конструктор не нужен.


в некоторых угловых случаях будет полезен синхронизированный конструктор. Вот более реалистичный пример, из обсуждения ответа Божо:

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() метод вызывается только после завершения построения нашего объекта подкласса, например, мы хотим только вывод "все ОК". Но в этом случае, когда вы можете редактировать только свой подкласс, у вас нет шансов достичь этого. Если бы конструктор можно было синхронизировать, это решило бы проблему.

Итак, что мы узнаем об этом: никогда не делайте что-то, как я сделал здесь в конструкторе суперкласса, если ваш класс не является окончательным-и не вызывайте никакие неокончательные методы вашего собственного класса из вашего конструктора.

вопрос был поднят в списке обсуждений, используемом авторами Java concurrent API и модели памяти Java. Было дано несколько ответов, в частности Ханс Бем ответил::

некоторые из нас (включая IIRC) фактически утверждали во время обсуждения модели памяти Java, что синхронизированные конструкторы должны быть разрешены. Теперь я мог пойти в любом случае. Клиентский код не должен использовать гонки для передачи ссылки, поэтому он не должен вопрос. Но если вы не доверяете клиентам [вашего класса], я думаю, что синхронизированные конструкторы могут быть полезны. И это было главным аргументом в пользу окончательной полевой семантики. [...] Как сказал Дэвид, вы можете использовать синхронизированные блоки.

, потому что synchronized гарантирует, что действия над одними и теми же объектами не будут выполняться несколькими потоками. И когда вызывается конструктор, у вас все еще нет объекта. Логически невозможно для двух потоков получить доступ к конструктору одного и того же объекта.

в вашем примере, даже если метод вызывается новым потоком, это уже не о конструкторе - это о целевом методе, который синхронизируется или нет.

в вашем примере конструктор вызывается только один раз из одного потока.

да, можно получить ссылку на неполно построенный объект (некоторые обсуждения вокруг блокировки двойной проверки и почему она нарушена, показывают эту проблему), однако, не вызывая конструктор во второй раз.

Syncronized на конструкторе предотвратил бы два потока от вызова конструктора на том же объекте одновременно, и это невозможно, так как это никогда не возможно вызвать конструктор на экземпляре объекта дважды, точка.

конструктор модификаторы разделе в JLS четко сказано

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) перед созданием объекта. Одной из возможных неоднозначных ситуаций было бы выдать ссылку на объекты-конструктор суперкласса при создании объекта subsclass.

Я не вижу причин запрещать синхронизацию конструкторов. Было бы полезно во многих сценариях в многопоточных приложениях. Если я правильно понимаю модель памяти 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(). Если нет специальной синхронизации между записями myInt = 3 потока 1 и чтение того же поля потоком 2, поток 2 не гарантируется, чтобы увидеть запись.

синхронизированный конструктор исправит эту проблему. Я прав насчет этого?

следующий код может достичь ожидаемого результата для синхронизированного конструктора.

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

такая синхронизация может иметь смысл в некоторых очень редких случаях, но я думаю, это просто не стоит:

  • вы всегда можете использовать синхронизированный блок вместо
  • он будет поддерживать кодирование довольно странным образом
  • на что он должен синхронизироваться? конструктор-это своего рода статический метод, он работает с объектом, но вызывается без него. Так что синхронизация на классе также имеет (некоторый) смысл!

когда сомневаешься, оставь это.