Согласно Параллелизм 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 на практике, я все же написал правильный фрагмент кода.
Я сделал ошибку? Что мне не хватает?
ОБНОВИТЬ: Основываясь на ответах и комментариях, теперь я понимаю, что мой пример кода является функционально верен по указанным мною причинам. Однако писать код таким образом — плохая практика, потому что есть шанс, что другие разработчики в будущем добавят дополнительные операторы инициализации после запуска потока. Одна из ситуаций, когда такие ошибки вероятны, — это реализация подклассов этого класса.
@AndrewTobilko thread.start() — действие синхронизации.




Предположим, я подкласс вашего класса. Он может не инициализировать свои поля к моменту, когда они потребуются.
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 явно не лучше...
@AndrewTobilko Это лучший код, который я когда-либо писал. Пожалуйста, не говорите этих обидных вещей.
Большое спасибо! Отличный пример! Можно вопрос вдогонку? Предположим, я решил инициализировать value inline (то есть просто объявить private int value = 10;). Мой код все еще верен по причине, которую я указал? Другими словами, сохраняется ли отношение «происходит до» между инициализацией значения и началом потока?
@Michael Рефакторинг для класса MuchBetterValueHolder ;-)
MuchBetterValueHolderFactory....?
@MartinJames Ты не даже пытаясь!
Большие программные проекты требуют большого объема обслуживания. В таком проекте ваш код буду со временем будет обновляться другими разработчиками. Прямо сейчас экземпляр ValueHolder полностью инициализируется при запуске потока, но будущий разработчик может легко непреднамеренно добавить инициализацию после построения потока, тем самым создав утечку ссылки на частично инициализированный ValueHolder.