Понимание того, почему небезопасно запускать поток внутри конструктора с точки зрения модели памяти Java

Согласно Параллелизм Java на практике опасно запускать поток в конструкторе класса. Причина в том, что это предоставляет указатель this другому потоку до того, как объект будет полностью построен.

Несмотря на то, что эта тема обсуждалась во многих предыдущих вопросах StackOverflow, мне все еще трудно понять, почему это так важно. В частности, я надеюсь выяснить, может ли запуск потока внутри конструктора привести к проблемам непротиворечивости памяти с точки зрения модели памяти Java.

Позвольте мне привести вам конкретный пример того, что я хочу сделать. (Желаемый результат для этого фрагмента кода — число 20, которое будет напечатано на консоли.)

private static class ValueHolder {

    private int value;
    private Thread thread;

    ValueHolder() {
        this.value = 10;
        thread = new Thread(new DoublingTask(this)); // exposing "this" pointer!!!
        thread.start();   // starting thread inside constructor!!!
    }

    int getValue() {
        return value;
    }

    void awaitTermination() {
        try {
            thread.join();
        } catch (InterruptedException ex) {}
    }

}

private static class DoublingTask implements Runnable {

    private ValueHolder valueHolder;

    DoublingTask(ValueHolder valueHolder) {
        this.valueHolder = valueHolder;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName());
        System.out.println(valueHolder.getValue() * 2);  // I expect to print out 20...
    }

}

public static void main(String[] args) {
    ValueHolder myValueHolder = new ValueHolder();
    myValueHolder.awaitTermination();
}

Да, я знаю, что поток запускается до того, как мы возвращаемся из конструктора. Да, я знаю, что указатель this открыт для потока. И тем не менее, я убежден, что код правильный. Я считаю, что он всегда будет печатать число 20 на консоли.

  • Задание this.value = 10бывает-преждеthread.start(). (Это потому, что this.value = 10 предшествует thread.start() в порядке выполнения программы.)
  • Вызов thread.start() в основном потоке бывает-прежде запуск метода .run() во вновь созданном потоке. (Потому что thread.start() — это синхронизирующее действие.)
  • Начало метода .run()бывает-прежде оператора печати System.out.println(valueHolder.getValue() * 2);. (Опять же, по порядку программы.)

Следовательно, согласно модели памяти Java оператор печати должен считывать правильно инициализированное значение valueHolder.value (а именно, 10). Таким образом, несмотря на то, что я проигнорировал совет Параллелизм Java на практике, я все же написал правильный фрагмент кода.

Я сделал ошибку? Что мне не хватает?


ОБНОВИТЬ: Основываясь на ответах и ​​комментариях, теперь я понимаю, что мой пример кода является функционально верен по указанным мною причинам. Однако писать код таким образом — плохая практика, потому что есть шанс, что другие разработчики в будущем добавят дополнительные операторы инициализации после запуска потока. Одна из ситуаций, когда такие ошибки вероятны, — это реализация подклассов этого класса.

Большие программные проекты требуют большого объема обслуживания. В таком проекте ваш код буду со временем будет обновляться другими разработчиками. Прямо сейчас экземпляр ValueHolder полностью инициализируется при запуске потока, но будущий разработчик может легко непреднамеренно добавить инициализацию после построения потока, тем самым создав утечку ссылки на частично инициализированный ValueHolder.

VGR 14.05.2019 16:25

@AndrewTobilko thread.start() — действие синхронизации.

Andy Turner 14.05.2019 16:26
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
6
2
138
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Предположим, я подкласс вашего класса. Он может не инициализировать свои поля к моменту, когда они потребуются.

class BetterValueHolder extends ValueHolder
{
    private int betterValue;

    BetterValueHolder(final int betterValue)
    {
        // not actually required, it's added implicitly anyway.
        // just to demonstrate where your constructor is called from
        super();

        try
        {
            Thread.sleep(1000); // just to demonstrate the race condition more clearly
        }
        catch (InterruptedException e) {}

        this.betterValue = betterValue;
    }

    @Override
    public int getValue()
    {
        return betterValue;
    }
}

Это напечатает ноль, независимо от того, какое значение задано конструктору BetterValueHolder.

да, но этот ValueHolder явно не лучше...

Andrew Tobilko 14.05.2019 16:19

@AndrewTobilko Это лучший код, который я когда-либо писал. Пожалуйста, не говорите этих обидных вещей.

Michael 14.05.2019 16:19

Большое спасибо! Отличный пример! Можно вопрос вдогонку? Предположим, я решил инициализировать value inline (то есть просто объявить private int value = 10;). Мой код все еще верен по причине, которую я указал? Другими словами, сохраняется ли отношение «происходит до» между инициализацией значения и началом потока?

Kenny Wong 14.05.2019 16:45

@Michael Рефакторинг для класса MuchBetterValueHolder ;-)

Palamino 14.05.2019 16:48

MuchBetterValueHolderFactory....?

Martin James 14.05.2019 20:57

@MartinJames Ты не даже пытаясь!

Michael 15.05.2019 11:02

Другие вопросы по теме