Двойная проверка блокировки в JAVA
Читая о DCL в Википедии, я задавался вопросом о проблеме в DCL и предлагаемом решении, или другими словами, почему volatile
ключевое слово нужно? Короче говоря, проблема: использование DCL может в некоторых случаях приводить к тому, что частично сконструированный объект используется потоком. Предлагаемое решение (для JAVA 5+):
// Works with acquire/release semantics for volatile
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
private volatile Helper helper;
public Helper getHelper() {
Helper result = helper;
if (result == null) {
synchronized(this) {
result = helper;
if (result == null) {
helper = result = new Helper();
}
}
}
return result;
}
// other functions and members...
}
Теперь мой вопрос: почему бы не бросить volatile
от помощника? поправьте меня, если я ошибаюсь, но если вы просто нарушите эту строку: helper = result = new Helper();
чтобы:
result = new Helper();
helper = result;
В этом сценарии helper
никогда не получит ссылку, пока объект не будет завершен. Не правда ли? Как volatile
дела лучше?
РЕДАКТИРОВАТЬ: принять этот код:
class Foo {
private volatile Helper helper;
public Helper getHelper() {
Helper result = helper;
if (result == null) {
synchronized(this) {
result = helper;
if (result == null) {
result = new Helper();
result.someMethod();
helper = result;
}
}
}
return result;
}
}
Если следующая строка после инициализации не гарантирует полностью инициализированный объект, я не могу вызвать методы для него. Могу я?
1 ответ
Если вы опустите volatile
, компилятор может оптимизировать result
полностью. Эта дополнительная локальная переменная мало что меняет. Это экономит дополнительную нагрузку, но это volatile
ключевые слова, которые фиксируют состояние гонки.
Предположим, у нас есть следующий фрагмент кода:
public volatile Object helper;
public synchronized void initHelper() {
Object result = new Object();
helper = result;
}
Он будет скомпилирован в следующую псевдо-сборку:
public synchronized void initHelper() {
Object result = Object.new();
result.<init>();
// result is now fully initialized
// but helper is still null for all threads
this.helper = result;
// now all threads can access fully initialized helper
}
Если вы удалите volatile
компилятору разрешено предполагать, что никакой другой поток не использует helper
и переставить код, чтобы оптимизировать его. Может быть решено удалить ненужную локальную переменную и выдать следующий вывод:
public synchronized void initHelper() {
this.helper = Object.new();
// from now on other threads can access helper
this.helper.<init>();
// but it's fully initialized only after this line
}
Если вы добавите вызов функции в initHelper
, Компилятор никогда не вызовет этот вызов до инициализации. Это изменило бы смысл программы даже в одном потоке. Но разрешено проводить оптимизации, которые нарушат многопоточный код, обращающийся к этому полю.