Подтвердить что ты не робот

Неизменность и переупорядочение

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

4b9b3361

Ответ 1

Смятение, которое, как мне кажется, у вас есть, - это то, что автор подразумевал под безопасной публикацией. Он имел в виду безопасную публикацию ненулевого ресурса, но, похоже, вы это понимаете.

Ваш вопрос интересен - возможно ли вернуть нулевое кэшированное значение ресурса?

Да.

Компилятор может изменить порядок работы, например

public static Resource getInstance(){
   Resource reordered = resource;
   if(resource != null){
       return reordered;
   }
   return (resource = new Resource());
} 

Это не нарушает правила последовательной согласованности, но может возвращать нулевое значение.

Будет ли это лучшая реализация для обсуждения, но нет правил для предотвращения такого типа переупорядочения.

Ответ 2

ОБНОВЛЕНИЕ Feb10

Я убеждаюсь, что мы должны разделить две фазы: компиляция и выполнение.

Я думаю, что фактором принятия решения о том, разрешено ли возвращать null или нет, является , что байт-код. Я сделал 3 примера:

Пример 1:

Оригинальный исходный код, буквально переведенный на байт-код:

if (resource == null)
    resource = new Resource();  // unsafe publication
return resource;

Байт-код:

public static Resource getInstance();
Code:
0:   getstatic       #20; //Field resource:LResource;
3:   ifnonnull       16
6:   new             #22; //class Resource
9:   dup
10:  invokespecial   #24; //Method Resource."<init>":()V
13:  putstatic       #20; //Field resource:LResource;
16:  getstatic       #20; //Field resource:LResource;
19:  areturn

Это самый интересный случай, потому что есть 2 read (строка # 0 и строка # 16), и между ними находится 1 write (строка № 13). Я утверждаю, что невозможно изменить порядок, но рассмотрим его ниже.

Пример 2:

"Оптимизированный для компилятора" код, который можно буквально переконвертировать в java следующим образом:

Resource read = resource;
if (resource==null)
    read = resource = new Resource();
return read;

Байт-код для этого (на самом деле я создал это путем компиляции приведенного выше фрагмента кода):

public static Resource getInstance();
Code:
0:   getstatic       #20; //Field resource:LResource;
3:   astore_0
4:   getstatic       #20; //Field resource:LResource;
7:   ifnonnull       22
10:  new     #22; //class Resource
13:  dup
14:  invokespecial   #24; //Method Resource."<init>":()V
17:  dup
18:  putstatic       #20; //Field resource:LResource;
21:  astore_0
22:  aload_0
23:  areturn

Очевидно, что если компилятор "оптимизирует", и получается байт-код, как указано выше, может произойти нулевое чтение (например, я ссылаюсь на Блог Джереми Мэнсона)

Также интересно посмотреть, как работает a = b = c: ссылка на новый экземпляр (строка № 14) дублируется (строка № 17), и сохраняется та же ссылка, а затем сначала b (ресурс, (Строка # 18)), затем a (read, (строка # 21)).

Пример 3:

Сделайте еще более маленькую модификацию: прочитайте resource только один раз! Если компилятор начинает оптимизировать (и используя регистры, как указывали другие), это лучше оптимизация, чем выше, потому что строка №4 здесь представляет собой "доступ к регистру", а не более дорогой "статический доступ" в примере 2.

Resource read = resource;
if (read == null)   // reading the local variable, not the static field
    read = resource = new Resource();
return read;

Байт-код для примера 3 (также созданный с буквальной компиляцией выше):

public static Resource getInstance();
Code:
0:   getstatic       #20; //Field resource:LResource;
3:   astore_0
4:   aload_0
5:   ifnonnull       20
8:   new     #22; //class Resource
11:  dup
12:  invokespecial   #24; //Method Resource."<init>":()V
15:  dup
16:  putstatic       #20; //Field resource:LResource;
19:  astore_0
20:  aload_0
21:  areturn

Также легко видеть, что невозможно получить null из этого байт-кода, так как он построен так же, как String.hashcode(), имея только 1 чтение статической переменной resource.

Теперь рассмотрим Пример 1:

0:   getstatic       #20; //Field resource:LResource;
3:   ifnonnull       16
6:   new             #22; //class Resource
9:   dup
10:  invokespecial   #24; //Method Resource."<init>":()V
13:  putstatic       #20; //Field resource:LResource;
16:  getstatic       #20; //Field resource:LResource;
19:  areturn

Вы можете видеть, что строка № 16 (чтение variable#20 для возврата) наиболее заметна для записи из строки # 13 (назначение variable#20 из конструктора), поэтому ее нельзя размещать вперед в любом порядке выполнения, где выполняется строка # 13. Таким образом, переупорядочение не возможно.

Для JVM можно построить (и воспользоваться) ветку, которая (используя определенные дополнительные условия) обходит строку № 13 write: условие состоит в том, что чтение из variable#20 не должно быть нулевым.

Таким образом, ни в одном случае для примера 1 нельзя вернуть null.

Вывод:

Увидев приведенные выше примеры, байт-код, показанный в примере 1, НЕ ПРОИЗВОДИТ null. Оптимизированный байт-код, как в примере 2 ПРОВЕРИТЬ null, но есть еще более оптимизированная оптимизация Пример 3, который НЕ ПРОИЗВОДИТ null.

Поскольку мы не можем быть готовы ко всей возможной оптимизации всех компиляторов, можно сказать, что в некоторых случаях это возможно, некоторые другие случаи невозможны для return null, и все это зависит от байтового кода. Кроме того, мы показали, что есть хотя бы один пример для обоих случаев.


Предыдущее рассуждение. Обращаем внимание на пример Assylias: главный вопрос: действительно ли (применительно ко всем спецификациям JMM, JLS), что VM изменит порядок чтения 11 и 14 так, что 14 произойдет до 11?

Если это может произойти, тогда независимый Thread2 может записать ресурс с 23, поэтому 14 может читать null. Я заявляю, что он невозможен.

На самом деле, поскольку существует возможная запись 13, она не будет действительным порядком выполнения. VM может оптимизировать порядок выполнения так, что исключает неработающие ветки (оставшиеся всего 2 чтения, без записи), но для принятия этого решения он должен выполнить первое чтение (11), и он должен читать не -null, поэтому чтение 14 не может предшествовать 11-ти прочитанному. Таким образом, невозможно вернуть null.


Неизменность

Что касается неизменяемости, я думаю, что это утверждение неверно:

UnsafeLazyInitialization действительно безопасна, если ресурс является неизменным.

Однако, если конструктор непредсказуем, могут появиться интересные результаты. Представьте себе такой конструктор:

public class Resource {
    public final double foo;

    public Resource() {
        this.foo = Math.random();
    }
}

Если у нас есть Thread s, это может привести к тому, что 2 потока получат объект с другим поведением. Итак, полное утверждение должно звучать так:

UnsafeLazyInitialization действительно безопасна, если ресурс неизменен и его инициализация согласована.

По соглашению я имею в виду, что вызов конструктора resource дважды мы получим два объекта, которые ведут себя точно так же (вызов тех же методов в том же порядке на обоих даст одинаковые результаты).

Ответ 3

После применения правил JLS к этому примеру я пришел к выводу, что getInstance может определенно вернуть null. В частности, JLS 17.4:

Модель памяти определяет, какие значения могут быть прочитаны в каждой точке программы. Действия каждого потока в изоляции должны вести себя как управляемые семантикой этого потока, , за исключением того, что значения, наблюдаемые каждым чтением, определяются моделью памяти.

Затем ясно, что при отсутствии синхронизации null является законным результатом метода, поскольку каждое из двух чтений может наблюдать что угодно.


Доказательство

Разложение записей и записей

Программа может быть разложена следующим образом (чтобы четко видеть записи и записи):

                              Some Thread
---------------------------------------------------------------------
 10: resource = null; //default value                                  //write
=====================================================================
           Thread 1               |          Thread 2                
----------------------------------+----------------------------------
 11: a = resource;                | 21: x = resource;                  //read
 12: if (a == null)               | 22: if (x == null)               
 13:   resource = new Resource(); | 23:   resource = new Resource();   //write
 14: b = resource;                | 24: y = resource;                  //read
 15: return b;                    | 25: return y;                    

Что говорит JLS

JLS 17.4.5 дает правила чтения для чтения записи:

Мы говорим, что чтению r переменной v разрешено наблюдать запись w в v, если в частичном порядке выполнения:

  • r не упорядочивается до w (т.е. это не тот случай, когда hb (r, w)) и
  • нет промежуточной записи w 'в v (т.е. не писать w' в v, что hb (w, w ') и hb (w', r)).

Применение правила

В нашем примере допустим, что поток 1 видит нуль и правильно инициализирует resource. В потоке 2 недопустимое выполнение было бы для 21, чтобы наблюдать 23 (из-за порядка программы), но любая другая запись (10 и 13) может наблюдаться либо с помощью:

  • 10 происходит - перед всеми действиями, поэтому чтение не выполняется до 10
  • 21 и 24 не имеют отношения hb с 13
  • 13 не происходит - до 23 (нет отношения hb между ними)

Таким образом, как 21, так и 24 (наши 2 чтения) имеют возможность наблюдать либо 10 (нуль), либо 13 (не нуль).

Путь выполнения, который возвращает null

В частности, предполагая, что Thread 1 видит нуль в строке 11 и инициализирует resource в строке 13, Thread 2 может законно выполнить следующее:

  • 24: y = null (читает запись 10)
  • 21: x = non null (читает запись 13)
  • 22: false
  • 25: return y

Примечание. Чтобы прояснить, это не означает, что T2 видит не null, а затем видит null (что нарушит требования причинности) - это означает, что с точки зрения выполнения эти два чтения были переупорядочено, а второе - до первого, но похоже, что более поздняя запись была замечена до более ранней, основанной на исходном заказе программы.

ОБНОВЛЕНИЕ 10 фев.

Возвращаясь к коду, действительное переупорядочение будет:

Resource tmp = resource; // null here
if (resource != null) { // resource not null here
    resource = tmp = new Resource();
}
return tmp; // returns null

И поскольку этот код последовательно согласован (если он выполняется одним потоком, он всегда будет иметь такое же поведение, что и исходный код), он показывает, что требования к причинности удовлетворяются (есть допустимое выполнение, которое приводит к результату).


После публикации в списке интересов concurrency я получил несколько сообщений относительно законности этого переупорядочения, которые подтверждают, что null является юридическим результатом:

  • Трансформация определенно легальна, так как однопоточное выполнение не скажет разницы. [Обратите внимание, что] преобразование не кажется разумным - нет никакой веской причины, чтобы компилятор это сделал. Однако, учитывая большее количество окружающего кода или, возможно, оптимизацию компилятора "ошибка", это может произойти.
  • Утверждение о заказе внутрипотока и порядке программы - вот что заставило меня подвергнуть сомнению обоснованность вещей, но в конечном итоге JMM относится к байт-коду, который выполняется. Преобразование может быть выполнено компилятором javac, и в этом случае значение null будет совершенно корректным. И нет правил для того, как javac должен конвертировать из Java-источника в Java байт-код, поэтому...

Ответ 4

Есть два вопроса, которые вы задаете:

1. Может ли метод getInstance() вернуть null из-за переупорядочения?

(что я думаю, что вы действительно после этого, поэтому я постараюсь ответить на него в первую очередь)

Хотя я считаю, что создание Java для этого абсолютно безумное, кажется, что вы на самом деле правы, что getInstance() может возвращать null.

Пример кода:

if (resource == null)
    resource = new Resource();  // unsafe publication
return resource;

логически на 100% идентичен примеру в блоге, с которым вы связались:

if (hash == 0) {
    // calculate local variable h to be non-zero
    hash = h;
}
return hash;

Затем Джереми Мэнсон описывает, что его код может вернуть 0 из-за переупорядочения. Во-первых, я не верил в это, поскольку я думал, что следующее "случится-до" -логическое должно быть выполнено:

   "if (resource == null)" happens before "resource = new Resource();"
                                   and
     "resource = new Resource();" happens before "return resource;"
                                therefore
"if (resource == null)" happens before "return resource;", preventing null

Но Джереми приводит следующий пример в комментарии к своему сообщению в блоге, как этот код может быть правильно переписан компилятором:

read = resource;
if (resource==null)
    read = resource = new Resource();
return read;

Это в однопоточной среде ведет себя точно идентично исходному коду, но в многопоточной среде может привести к следующему порядку выполнения:

Thread 1                        Thread 2
------------------------------- -------------------------------------------------
read = resource;    // null
                                read = resource;                      // null
                                if (resource==null)                   // true
                                    read = resource = new Resource(); // non-null
                                return read;                          // non-null
if (resource==null) // FALSE!!!
return read;        // NULL!!!

Теперь, с оптимизационной точки зрения, делать это не имеет для меня никакого смысла, поскольку вся суть этих вещей заключалась бы в сокращении нескольких чтений в одном месте, и в этом случае нет никакого смысла в том, что компилятор вместо этого не генерирует if (read==null), предотвращая проблему. Итак, как указывает Джереми в своем блоге, вероятно, вряд ли это произойдет. Но кажется, что, сугубо с точки зрения правил языка, на самом деле это разрешено.

Этот пример действительно рассматривается в JLS:

http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4

Эффект, наблюдаемый между значениями r2, r4 и r5 в Table 17.4. Surprising results caused by forward substitution, эквивалентен тому, что может произойти с read = resource, if (resource==null) и return resource в пример выше.

Кроме того: почему я ссылаюсь на сообщение в блоге как на конечный источник ответа? Потому что парень, который написал это, также является парнем, который написал главу 17 JLS на concurrency! Так что, лучше быть прав!:)

2. Сделал бы Resource неизменным сделать метод getInstance() потокобезопасным?

Учитывая потенциальный результат null, который может произойти независимо от того, является ли Resource изменчивым или нет, немедленный простой ответ на этот вопрос: Нет (не строго)

Если мы проигнорируем этот крайне маловероятный, но возможный сценарий, ответ будет следующим: Зависит.

Очевидная проблема с потоком с кодом заключается в том, что он может привести к следующему порядку выполнения (без какого-либо переупорядочения):

Thread 1                                 Thread 2
---------------------------------------- ----------------------------------------
if (resource==null) // true;  
                                         if (resource==null)          // true
                                             resource=new Resource(); // object 1
                                         return resource;             // object 1
    resource=new Resource(); // object 2
return resource;             // object 2

Таким образом, безопасность, не связанная с потоками, исходит из того факта, что вы можете вернуть из функции два разных объекта (хотя без переупорядочения ни один из них никогда не будет null).

Теперь то, что, вероятно, пыталась сказать книга, следующее:

Неизменяемые объекты Java, такие как строки и целые, стараются не создавать несколько объектов для одного и того же содержимого. Итак, если у вас есть "hello" в одном месте и "hello" в другом месте, Java предоставит вам то же точное описание объекта. Аналогично, если у вас есть new Integer(5) в одном месте и new Integer(5) в другом. Если бы это имело место и с new Resource(), вы получили бы ту же ссылку назад, а object 1 и object 2 в приведенном выше примере были бы тем же самым объектом. Это действительно привело бы к эффективной потокобезопасной функции (игнорируя проблему переупорядочения).

Но если вы реализуете Resource самостоятельно, я не верю, что есть даже способ, чтобы конструктор возвращал ссылку на ранее созданный объект, а не создавал новый. Таким образом, вам не удастся сделать object 1 и object 2 тем же самым объектом. Но, учитывая, что вы вызываете конструктор с теми же аргументами (ни один в обоих случаях), вполне вероятно, что, хотя ваши созданные объекты не являются одним и тем же точным объектом, они будут, по сути, вести себя как если они были, также эффективно делают код безопасным для потоков.

Это не обязательно должно быть так. Представьте себе неизменяемую версию Date, например. Конструктор по умолчанию Date() использует текущее системное время в качестве значения даты. Таким образом, хотя объект неизменен и конструктор вызывается с тем же аргументом, вызов его дважды, вероятно, не приведет к эквивалентному объекту. Поэтому метод getInstance() не является потокобезопасным.

Итак, как общее утверждение, я считаю, что строка, которую вы цитировали из книги, просто неверна (по крайней мере, как выведенная из контекста здесь).

ДОБАВЛЕНИЕ Re: переупорядочение

Я нахожу пример resource==new Resource() слишком упрощенным, чтобы помочь понять, почему WHY разрешает такое переупорядочение Java когда-либо иметь смысл. Поэтому позвольте мне посмотреть, могу ли я придумать что-то, что могло бы помочь в оптимизации:

System.out.println("Found contact:");
System.out.println(firstname + " " + lastname);
if (firstname==null) firstname = "";
if (lastname ==null) lastname  = "";
return firstname + " " + lastname;

Здесь, в наиболее вероятном случае, когда оба ifs дают false, неосуществимо выполнить дорогостоящую конкатенацию String firstname + " " + lastname дважды, один раз для сообщения отладки, один раз для возврата. Таким образом, здесь действительно имеет смысл изменить порядок кода, чтобы сделать следующее:

System.out.println("Found contact:");
String contact = firstname + " " + lastname;
System.out.println(contact);
if ((firstname==null) || (lastname==null)) {
    if (firstname==null) firstname = "";
    if (lastname ==null) lastname  = "";
    contact = firstname + " " + lastname;
}
return contact;

Как примеры становятся более сложными, и когда вы начинаете думать о компиляторе, отслеживая то, что уже загружено/вычислено в регистрах процессора, которое оно использует и интеллектуально пропускает повторный расчет уже существующих результатов, этот эффект может фактически стать более скорее всего, произойдет. Поэтому, хотя я и не думал, что когда-нибудь буду говорить об этом, когда я ложился спать прошлой ночью, думая об этом больше, я действительно считаю, что это, возможно, было необходимым/хорошим решением, чтобы действительно разрешить оптимизацию кода, чтобы сделать все возможное впечатляющая магия. Но это все еще кажется мне довольно опасным, так как я не думаю, что многие люди знают об этом, и даже если это так, довольно сложно обернуть голову тем, как правильно писать код, не синхронизируя все (что потом удалит много раз с любыми преимуществами производительности, полученными в результате более гибкой оптимизации).

Я думаю, если бы вы не допустили такого переупорядочения, любое кэширование и повторное использование промежуточных результатов серии шагов процесса стало бы незаконным, что позволило бы устранить одну из самых мощных оптимизаций компилятора.

Ответ 5

Ничто не устанавливает ссылку на null, если она не является null. Возможно, чтобы поток увидел null после того, как другой поток установил его не-t20, но я не вижу, как возможно обратное.

Я не уверен, что переопределение заказов является фактором здесь, но чередование инструкций двумя потоками. Разветвление if не может быть переупорядочено для выполнения до того, как его условие будет оценено.

Ответ 6

Извините, если я ошибаюсь (потому что я не носитель английского языка), но мне кажется, что упомянутое утверждение:

UnsafeLazyInitialization действительно безопасна, если ресурс является неизменным.

вырывается из контекста. Это утверждение действительно касается использования безопасности инициализации:

Гарантия безопасности инициализации позволяет правильно сконструированные неизменяемые объекты безопасно распределять между потоками без синхронизации

...

Безопасность инициализации гарантирует, что для правильно построенных объектов все потоки будут видеть правильные значения конечных полей которые были заданы конструктором

Ответ 7

После прочтения сообщения, которое вы связали более тщательно, вы правы, пример, который вы опубликовали, может (предположительно, в текущей модели памяти) вернуть null. Соответствующий пример ниже в комментариях сообщения, но эффективно, время выполнения может сделать это:

public class UnsafeLazyInitialization {
    private static Resource resource;

    public static Resource getInstance() {
        Resource tmp = resource;
        if (resource == null)
            tmp = resource = new Resource();  // unsafe publication
        return tmp;
    }
}

Это подчиняется ограничениям для одного потока, но может привести к возврату null, если несколько потоков вызывают метод (первое присваивание tmp получает нулевое значение, блок if видит ненулевое значение, tmp получает значение null).

Чтобы сделать это "безопасным" небезопасным (при условии, что ресурс является неизменным), вы должны явно читать resource только один раз (подобно тому, как вы должны относиться к общей изменчивой переменной:

public class UnsafeLazyInitialization {
    private static Resource resource;

    public static Resource getInstance() {
        Resource cur = resource;
        if (cur == null) {
            cur = new Resource();
            resource = cur;
        }
        return cur;
    }
}

Ответ 8

Теперь это очень длинный назад поток, все еще учитывая этот вопрос, обсуждает многие интересные работы по переупорядочению и concurrency, в которых я участвую здесь, хотя и в последнее время.

На мгновение, если мы не включаем concurrency, действия и допустимые переупорядочения в многопоточной ситуации.
"Может ли JVM использовать операцию записи записи в кеш-память в однопоточном контексте". Я думаю нет. Учитывая, что есть операция записи, если условие может кэшировать, чтобы играть вообще.
Поэтому вернемся к вопросу: неизменность гарантирует, что объект полностью или правильно создан до того, как ссылка будет доступна или опубликована, поэтому определенность помогает. Но здесь есть операция записи после создания объекта. Таким образом, второй считывает кеш значения из предварительной записи в том же потоке или другом. Нет. Один поток может не знать о записи в другом потоке (учитывая, что нет необходимости в непосредственной видимости между потоками). Таким образом, не будет невозможна возможность возврата ложного нуля (т.е. после создания объекта). (Код в вопросе прерывает singleton, но нас здесь не беспокоит)

Ответ 9

Действительно безопасно UnsafeLazyInitialization.resource является неизменным, то есть поле объявляется окончательным:

private static final Resource resource = new Resource();

Он также может считаться потокобезопасным, если сам класс Resource является неизменным и не имеет значения, какой экземпляр вы используете. В этом случае два вызова могут возвращать разные экземпляры Resource без проблем, кроме увеличения потребления памяти, в зависимости от количества потоков, вызывающих getInstance() одновременно).

Это кажется надуманным, хотя и я считаю, что есть опечатка, реальное предложение должно быть

UnsafeLazyInitialization действительно безопасна, если * r * esource является неизменны.

Ответ 10

UnsafeLazyInitialization.getInstance() никогда не может вернуть значение null.

Я буду использовать таблицу @assylias.

                              Some Thread
---------------------------------------------------------------------
 10: resource = null; //default value                                  //write
=====================================================================
           Thread 1               |          Thread 2                
----------------------------------+----------------------------------
 11: a = resource;                | 21: x = resource;                  //read
 12: if (a == null)               | 22: if (x == null)               
 13:   resource = new Resource(); | 23:   resource = new Resource();   //write
 14: b = resource;                | 24: y = resource;                  //read
 15: return b;                    | 25: return y;    

Я буду использовать номера строк для Thread 1. Thread 1 видит запись на 10 перед чтением 11 и чтение строки 11 перед чтением 14. Это внутрипотока происходит раньше и ничего не говорят о потоке 2. В строке 14, прочитанной в строке 14, возвращается значение, определенное JMM. В зависимости от времени это может быть ресурс, созданный в строке 13, или это может быть любое значение, написанное Thread 2. Но эта запись должна произойти - после чтения в строке 11. Существует только одна такая запись, небезопасная публикация в строке 23. Запись в нуль в строке 10 не входит в область видимости, поскольку она произошла до строки 11 из-за внутри -thread ordering.

Не имеет значения, является ли Resource неизменным или нет. Большая часть обсуждений до сих пор была сосредоточена на межпоточных действиях, где неизменяемость была бы релевантной, но переупорядочение, которое позволило бы этому методу вернуть null, запрещено правилами внутри -thread. Соответствующий раздел спецификации JLS 17.4.7.

Для каждого потока t действия, выполняемые t в A, такие же, как будет генерироваться этим потоком в программном порядке изолированно, с каждая запись w записывает значение V (w), учитывая, что каждый прочитанный r видит значение V (W (r)). Значения, просматриваемые каждым чтением, определяются памятью модель. Приведенный заказ программы должен отражать порядок программы, в котором действия будут выполняться в соответствии с семантикой внутри потока от P.

Это в основном означает, что, хотя чтение и запись могут быть переупорядочены, чтение и запись в одну и ту же переменную должны появляться так, как они происходят, чтобы поток, который выполняет чтение и запись.

Там только одна запись null (в строке 10). Либо нить может видеть свою собственную копию ресурса или другой поток, но не может видеть, что более ранняя запись в null после читает либо ресурс.

В качестве побочного примечания инициализация в null происходит в отдельном потоке. В разделе "Безопасная публикация в JCIP" говорится:

Статические инициализаторы выполняются JVM при инициализации класса время; из-за внутренней синхронизации в JVM этот механизм гарантированно безопасно публиковать любые объекты, инициализированные таким образом [JLS 12.4.2].

Возможно, стоит попытаться написать тест, который получает UnsafeLazyInitialization.getInstance(), чтобы возвращать значение null, и получает некоторые из предложенных эквивалентных перезаписей для возврата null. Вы увидите, что они действительно не эквивалентны.

ИЗМЕНИТЬ

Вот пример, который отделяет чтение и запись для ясности. Скажем, есть публичный объект статической переменной.

public static Object object = new Integer(0);

Тема 1 записывает этот объект:

object = new Integer(1);
object = new Integer(2);
object = new Integer(3);

Тема 2 читает этот объект:

System.out.println(object);
System.out.println(object);
System.out.println(object);

Без какой-либо формы синхронизации, обеспечивающей межпоточные связи до отношений, Thread 2 может распечатывать много разных вещей.

1, 2, 3
0, 0, 0
3, 3, 3
1, 1, 3
etc.

Но он не может распечатать уменьшающуюся последовательность, такую ​​как 3, 2, 1. Семантика внутри потока, указанная в 17.4.7, сильно ограничивает переупорядочение здесь. Если вместо использования object три раза мы изменили этот пример, чтобы использовать три отдельные статические переменные, было бы возможно еще много выходов, потому что ограничений на переупорядочение не было бы.