Синхронизация конструктора в Java - программирование
Подтвердить что ты не робот

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

4b9b3361

Ответ 1

Кто-то где-то сказал мне, что Java-конструкторы синхронизированы, так что к ним невозможно получить доступ во время построения

Это, конечно, не так. Синхронизация с конструкторами отсутствует. Мало того, что несколько конструкторов могут возникать одновременно, но вы можете получить проблемы concurrency, например, путем форматирования потока внутри конструктора со ссылкой на построенный 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, которое я показал, является лучшим образцом.

Ответ 2

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

Итак, да, будет возможно, что другой поток получит ссылку на ваш объект и начнет пытаться использовать его до завершения инициализации. Но подождите, ухудшается рассмотрение этого ответа: fooobar.com/questions/326363/...... в основном может быть переупорядочение задания задания и завершения конструктора. В приведенном примере один поток может назначить h = new Holder(i) и другой вызов потока h.assertSanity() в новом экземпляре с синхронизацией как раз справа, чтобы получить два разных значения для члена n, назначенного в конструкторе Holder.

Ответ 3

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

Ответ 4

Конструкторы

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

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

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

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

Ответ 5

Несмотря на то, что на этот вопрос дан ответ, но связанный с ним код не соответствует безопасным технологиям строительства, поскольку он позволяет этой ссылке выйти из конструктора, я хотел бы поделиться красивой объяснение, представленное Брайаном Гетцем

Ответ 6

Это небезопасно. Дополнительной синхронизации в 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
        }
    }
}