#java #concurrency #lazy-loading #volatile #vavr
Вопрос:
Я столкнулся со следующим кодом при чтении Lazy
исходного кода Vavr:
private transient volatile Supplier<? extends T> supplier;
private T value; // will behave as a volatile in reality, because a supplier volatile read will update all fields (see https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile)
public T get() {
return (supplier == null) ? value : computeValue();
}
private synchronized T computeValue() {
final Supplier<? extends T> s = supplier;
if (s != null) {
value = s.get();
supplier = null;
}
return value;
}
Это известный шаблон под названием «Блокировка с двойной проверкой», но для меня он выглядит сломанным. Предположим, мы внедрим этот код в многопоточную среду. Если get()
метод вызывается первым потоком и поставщик создает новый объект, (для меня) у другого потока есть возможность увидеть полу-построенный объект из-за изменения порядка следующего кода:
private synchronized T computeValue() {
final Supplier<? extends T> s = supplier;
if (s != null) {
// value = s.get(); suppose supplier = () -> new AnyClass(x, y , z)
temp = calloc(sizeof(SomeClass));
temp.<init>(x, y, z);
value = temp; //this can be reordered with line above
supplier = null;
}
return value;
}
К сожалению, рядом с value
полем есть комментарий:
private transient volatile Supplier<? extends T> supplier;
private T value; // will behave as a volatile in reality, because a supplier volatile read will update all fields (see https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile)
Насколько я знаю, изменчивое чтение «обновит» значения переменных, которые считываются после изменчивого чтения. Другими словами — это истощает очередь аннулирования кэша (если мы говорим о протоколе когерентности MESI). Кроме того, это предотвращает загрузку/чтение, которые происходят после того, как изменчивое чтение будет переупорядочено перед ним. Тем не менее, это не дает никакой гарантии, что инструкции по созданию объекта (вызов get()
метода поставщика) не будут переупорядочены.
Мой вопрос в том, почему этот код считается потокобезопасным?
Очевидно, что я не нашел ничего интересного в источнике, размещенном в комментарии.
Ответ №1:
Не говорите о кэшах, когда речь заходит о модели памяти Java. Что имеет значение, так это то, что формальные отношения случаются до отношений.
Обратите внимание, что computeValue()
заявлен synchronized
, так что для потоков, выполняющихся метод переупорядочивания в методе не имеют никакого отношения, так как они могут только войти в метод, когда любой поток, который выполняется метод раньше, вышел метод, а там происходит-до отношения между методом выхода из предыдущей темы и следующий поток ввода метода.
Действительно интересный метод заключается в
public T get() {
return (supplier == null) ? value : computeValue();
}
который не использует synchronized
, но полагается на volatile
чтение supplier
. Это, очевидно, предполагает, что начальное состояние supplier
не является null
, например, назначенным в конструкторе, и окружающий код гарантировал, что get
метод не может быть выполнен до того, как это назначение произошло.
Затем, когда supplier
считывается как null
, это может быть только результатом записи, выполненной первым выполняемым потоком computeValue()
, что устанавливает связь «происходит до» между записями, выполненными потоком до назначения null
supplier
, и чтениями, выполняемыми этим потоком после чтения null
supplier
. Таким образом, он будет воспринимать полностью инициализированное состояние объекта, на который ссылается value
.
Таким образом, вы правы в том , что события, происходящие в конструкторе значения, могут быть переупорядочены с присвоением value
ссылки, но они не могут быть переупорядочены с последующей записью supplier
, на которую get
полагается метод.