Ваш код не является потокобезопасным, что может быть легко показано путем удаления всех несущественных частей:
public class Memoized<T> {
private T value;
// irrelevant parts omitted
public T get() {
T tempValue = value;
if (tempValue == null) {
// irrelevant parts omitted
}
return tempValue;
}
}
Таким образом, value
не имеет модификатора volatile
, и вы читаете его в методе get()
без синхронизации, а если он не является null
, продолжайте использовать его без какой-либо синхронизации.
Один только этот путь к коду уже делает код неработоспособным, независимо от того, что вы делаете при назначении value
, поскольку для всех потоковобезопасных конструкций требуется использовать оба конца, стороны чтения и записи, чтобы использовать совместимый механизм синхронизации.
Тот факт, что вы используете эзотерические конструкции, такие как if (_volatile);
, становится неактуальным, поскольку код уже нарушен.
Причина, по которой в примере в Википедии используется оболочка с полем final
, заключается в том, что неизменяемые объекты, использующие только поля final
, защищены от гонок данных и, следовательно, единственной конструкции, которая безопасна при чтении своей ссылки без действия синхронизации. .
Обратите внимание, что, поскольку лямбда-выражения попадают в одну и ту же категорию, вы можете использовать их, чтобы упростить пример для вашего варианта использования:
public class Memoized<T> {
private boolean initialized;
private Supplier<T> supplier;
public Memoized(Supplier<T> supplier) {
this.supplier = () -> {
synchronized(this) {
if(!initialized) {
T value = supplier.get();
this.supplier = () -> value;
initialized = true;
}
}
return this.supplier.get();
};
}
public T get() {
return supplier.get();
}
}
Здесь supplier.get()
в пределах Memoized.get()
может считывать обновленное значение supplier
без действия синхронизации, и в этом случае оно будет читать правильное value
, поскольку оно неявно final
. Если метод считывает устаревшее значение для ссылки supplier
, он заканчивается в блоке synchronized(this)
, который использует флаг initialized
, чтобы определить, необходима ли оценка исходного поставщика.
Поскольку поле initialized
будет доступно только в пределах блока synchronized(this)
, оно всегда будет иметь правильное значение. Этот блок будет выполняться не более одного раза для каждого потока, тогда как только первый будет оценивать get()
для исходного поставщика. После этого каждый поток будет использовать поставщика () -> value
, возвращая значение без каких-либо действий по синхронизации.