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

6 ответов

Решение

Кто-то где-то сказал мне, что 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 will force the happens-before of the 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();
    }
}

Этот код выкидывает java.lang.IllegalMonitorStateException на wait() вызов. Если бы была действующая синхронизация, этого бы не произошло.

Это даже не имеет смысла. Нет необходимости их синхронизировать. Конструктор может быть вызван только после new(), и по определению каждый вызов new() возвращает другое значение. Таким образом, существует нулевая вероятность того, что конструктор будет вызван двумя потоками одновременно с одинаковым значением this, Таким образом, нет необходимости в синхронизации конструкторов.

если у меня есть конструктор, который сохраняет объект на карте, а другой поток извлекает его из этой карты до завершения его построения, будет ли этот поток блокироваться до завершения конструктора?

Нет. Зачем это делать? Кто собирается это заблокировать? Пропускать 'this' из конструктора, подобного этому, является плохой практикой: это позволяет другим потокам обращаться к объекту, который все еще находится в стадии разработки.

Вы были дезинформированы. То, что вы описываете, на самом деле называется неправильной публикацией и подробно обсуждается в книге Java Concurrency In Practice.

Так что да, другой поток сможет получить ссылку на ваш объект и начать пытаться использовать его до завершения инициализации. Но подождите, это станет еще хуже, подумайте об этом ответе: /questions/35098820/kak-prodemonstrirovat-usloviya-gonki-vokrug-znachenij-kotoryie-ne-opublikovanyi-dolzhnyim-obrazom/35098829#35098829... в принципе может быть изменение порядка назначения ссылок и завершения конструктора. В приведенном примере один поток может назначить h = new Holder(i) и еще один поток вызова h.assertSanity() на новом экземпляре с синхронизацией как раз правильно, чтобы получить два разных значения для n член, который назначен в Holderконструктор.

Конструкторы, как и другие методы, не требуют дополнительной синхронизации (кроме обработки final поля).

код будет работать, если this опубликован позже

public Test() 
{
    // Some lengthy operation to fully initialize this object

    this.id = atomicIdGenerator.getAndIncrement();
    testsById.put(this.id, this);
}

Хотя на этот вопрос дан ответ, но код, вставленный в вопрос, не соответствует безопасным методам конструирования, поскольку позволяет этой ссылке выйти из конструктора, я хотел бы поделиться прекрасным объяснением, представленным Брайаном Гетцем в статье: "Теория и практика Java: Техника безопасного строительства "на сайте IBM developerWorks.

Это небезопасно. В 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
        }
    }
}
Другие вопросы по тегам