Приведенный ниже код (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! Это разрешено в рамках модели памяти, потому что модель допускает обширное переупорядочение операций. Второе чтение фактически может быть перемещено в вашем коде, так что ваш процессор сделает это раньше первого!