Приведенный ниже код (Java Concurrency in Practice, листинг 16.3) не является поточно-ориентированным по понятным причинам:
public class UnsafeLazyInitialization {
private static Resource resource;
public static Resource getInstance() {
if (resource == null)
resource = new Resource(); // unsafe publication
return resource;
}
}
Однако несколько страниц спустя, в разделе 16.3, они заявляют:
UnsafeLazyInitializationфактически безопасен, еслиResourceнеизменен.
Я не понимаю это утверждение:
- Если
Resourceявляется неизменным, любой поток, наблюдающий за переменнойresourceбудет видеть ее как нулевой или полностью сконструированный (благодаря строгим гарантиям конечных полей, предоставляемых моделью памяти Java) - Однако ничто не мешает переупорядочению команд: в частности, два чтения
resourceмогут быть переупорядочены (есть одно чтение вifи одно вreturn). Таким образом, поток мог видеть ненулевойresourceвifно возвращать нулевую ссылку (*).
Я думаю, что UnsafeLazyInitialization.getInstance() может вернуть UnsafeLazyInitialization.getInstance() даже если Resource является неизменным. Это так и почему (или почему нет)?
(*) Чтобы лучше понять мою мысль о переупорядочении, этот пост в блоге Джереми Мэнсона, который является одним из авторов главы 17 JLS о параллелизме, объясняет, как хеш-код String безопасно публикуется через добросовестную гонку данных и как удалить использование локальной переменной может привести к тому, что хеш-код неверно вернет 0, из-за возможного переупорядочения, очень похожего на то, что я описал выше:
Здесь я добавил еще одно чтение: второе чтение хэша перед возвращением. Как бы странно это ни звучало и как бы маловероятно ни было, первое чтение может вернуть правильно вычисленное хеш-значение, а второе чтение может вернуть 0! Это разрешено в рамках модели памяти, потому что модель допускает обширное переупорядочение операций. Второе чтение фактически может быть перемещено в вашем коде, так что ваш процессор сделает это раньше первого!