Эффект синхронизации Java при блокировке с двойной проверкой?

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

class SomeClass {
  private Resource resource = null;
  public Resource getResource() {
    if (resource == null) {
      synchronized {
        if (resource == null) 
          resource = new Resource();
      }
    }
    return resource;
  }
}

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

  1. Будет выделена память для нового объекта Resource;
  2. будет вызван конструктор для ресурса,
  3. инициализация полей-членов нового объекта;
  4. ресурсу поля SomeClass будет присвоена ссылка на вновь созданный объект

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

Почему поток B может видеть эти операции с памятью в другом порядке, чем выполняется одним потоком A? Я думал, что поток B не будет знать, что объект ресурса создан, пока поток A не сбрасывает свою локальную память в основную память когда он выходит из синхронизированного блока, потому что поток B может читать объект ресурса только из разделяемой основной памяти?

Пожалуйста, поправьте мое понимание .... Спасибо.


person enix0907    schedule 23.08.2012    source источник
comment
Извините за ссылку на эту старую (2001 г.) статью, чтобы запутать себя. Как это исправить в версии Java +5.0? Это потому, что ключевое слово Volatile может гарантировать, что поток B никогда не прочитает частично инициализированный объект ресурса? Другими словами, поток B может читать только объект ресурса, который является либо нулевым, либо полностью инициализированным объектом ресурса?   -  person enix0907    schedule 23.08.2012


Ответы (4)


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

Вот где он ломается. Поскольку поток B обращается к resource без синхронизации, для его операций нет барьера чтения. Поэтому он может увидеть устаревшую кэшированную копию памяти для ячейки resource или (немного позже) ячейки, соответствующей какому-либо полю экземпляра Resource.

Исправление Costi Ciudatu подходит для версий Java> = 5.0. Но для более ранних версий семантика volatile не гарантирует, что все изменения будут сброшены из A в основную память в B.

person Stephen C    schedule 23.08.2012
comment
Спасибо за ваше объяснение, которое для меня имеет больше смысла. Когда вы сказали, что поскольку поток B обращается к ресурсу без синхронизации, для его операций нет барьера чтения. Означает ли это первое if (resource == null) над синхронизированным блоком? Другой находится внутри синхронизированного блока, так что я полагаю, что все в порядке. - person enix0907; 23.08.2012
comment
В принципе, это правильно. До тех пор, пока блок synchronized не будет выполнен, между действием (ями) в потоке A и действием (ями) в потоке B не существует связи «происходит раньше». Изменение объявления на volatile - это один из способов добавить требуемую взаимосвязь «происходит до». - person Stephen C; 24.08.2012
comment
Когда поток B обращается к ресурсу в первом if (resource == null) над синхронизированным блоком, читает ли он ресурс из собственной локальной памяти или из общей основной памяти? Прежде чем поток A выйдет из синхронизированного блока, инициализирующий объект ресурса должен все еще находиться в локальной памяти потока A и не записывать обратно в основную память. Если это правда, как поток B может увидеть устаревшую кэшированную копию памяти для ячейки ресурса? Я думал, что поток B может читать только свою локальную память до барьера чтения. - person enix0907; 24.08.2012
comment
Я думал, что поток B может читать только свою локальную память до барьера чтения. Это не правда. Он мог видеть либо содержимое основной памяти, или ранее кэшированную копию этой области памяти. Барьер чтения приводит к тому, что кэшированная копия становится недействительной, в результате чего ЦП вынужден выполнять чтение из основной памяти. - person Stephen C; 24.08.2012
comment
Когда Tread A инициализирует объект ресурса внутри синхронизированного блока, инициализируется ли этот объект в локальной памяти Thread A? Затем, когда он выйдет из блока, он запишет свою локальную копию обратно в основную память. Если это правда, тем временем, когда поток B читает либо из основной памяти, либо из собственной локальной памяти, он не должен знать эффект, вызванный потоком A, до тех пор, пока поток A не выйдет из блока. Как поток B может по-прежнему видеть изменение инициализации объекта ресурса? - person enix0907; 24.08.2012
comment
Я предполагаю, что это потому, что resource = new Resource () можно разделить на несколько инструкций, включая его метод конструктора, которые находятся вне этого синхронизированного блока ... Спасибо за ваше расширенное объяснение. - person enix0907; 24.08.2012
comment
Ваша теория (позапрошлый комментарий) неверна. Модель памяти Java не ограничивает порядок / образец такой записи. Вместо того, чтобы гадать, как ведут себя потоки, вам следует либо прочитать JLS, либо получить копию Java Concurrency in Practice Брайана Гетца и прочитать главу о модели памяти. - person Stephen C; 25.08.2012
comment
Спасибо за ссылку ... очень полезно :) Значит, порядок выполнения инструкций может быть гарантирован внутри синхронизированного блока? - person enix0907; 25.08.2012
comment
@ enix0907 - только по отношению к потоку, который их выполняет. Найдите / прочтите копию Goetz. Это слишком сложно объяснять в комментариях. - person Stephen C; 26.08.2012

Цитированная вами статья относится к модели памяти Java до Java 5.0.

В Java 5.0+ ваш resource должен быть объявлен volatile, чтобы это работало. Даже если изменения сбрасываются в основную память, нет никакой гарантии (кроме volatile), что поток B прочитает новое значение из основной памяти, а не из своего собственного локального кеша (где значение равно нулю).

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

person Costi Ciudatu    schedule 23.08.2012

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

person Eugene    schedule 23.08.2012

"Двойная проверка блокировки" - один из мемов, который не умрет. ИМХО, использование перечисления намного умнее (как было предложено Джошем Блохом во 2-м издании Effective Java)

enum SomeClass {
    INSTANCE; // thread safe and lazy loaded
}

Ошибка, о которой вы говорите, была исправлена ​​в Java 5.0 в 2004 году.

Короче говоря, а) не используйте его б) используйте версию Java 5.0+ в) не используйте действительно старые неподдерживаемые версии Java и воспринимайте действительно очень старые статьи (2001 г.) с недоверием.

person Peter Lawrey    schedule 23.08.2012
comment
См. Здесь: stackoverflow.com/questions/11925539/ - person JohnB; 23.08.2012