Двойная проверка запирания Артикул

Я читал эта статья о «Двойной проверке блокировки» и вне основной темы статьи мне было интересно, почему в какой-то момент статьи автор использует следующую идиому:

Listing 7. Attempting to solve the out-of-order write problem

public static Singleton getInstance()  
{
    if (instance == null)
    {
        synchronized(Singleton.class) {      //1
            Singleton inst = instance;         //2
            if (inst == null)
            {
                synchronized(Singleton.class) {  //3
                    inst = new Singleton();        //4
                }
                instance = inst;                 //5
            }
        }
    }
    return instance;
}

И мой вопрос: Есть ли смысл синхронизировать дважды какой-то код с одной и той же блокировкой? Есть ли у этого какая-то цель?

Спасибо заранее.

Есть связанный вопрос "Лучшая реализация синглтона на Java"

Stephen Denne 04.10.2008 14:49
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
11
1
1 981
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

Ответ принят как подходящий

Точка блокировки дважды заключалась в пытаться, чтобы предотвратить запись не по порядку. Модель памяти определяет, где может произойти переупорядочение, частично с точки зрения блокировок. Блокировка гарантирует, что никакие записи (в том числе в конструкторе синглтона) не произойдут после "instance = inst;" линия.

Однако, чтобы углубиться в тему, я бы порекомендовал Статья Билла Пью. И никогда не пытайтесь это сделать :)

Но вы можете использовать реально работающий вариант Java 5 в конце статьи.

Hans-Peter Störr 25.01.2010 14:33

@hstoerr: Лично я бы не стал, если бы не обнаружил, что это узкое место в каком-то коде. Слишком легко ошибиться. Конечно, всему есть свое место, но я бы редко им пользовался.

Jon Skeet 25.01.2010 15:07

Хорошо, но в статье сказано, что

The code in Listing 7 doesn't work because of the current definition of the memory model. The Java Language Specification (JLS) demands that code within a synchronized block not be moved out of a synchronized block. However, it does not say that code not in a synchronized block cannot be moved into a synchronized block.

И также похоже, что JVM выполняет следующий перевод в «псевдокод» в ASM:

public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {      //1
      Singleton inst = instance;         //2
      if (inst == null)
      {
        synchronized(Singleton.class) {  //3
          //inst = new Singleton();      //4
          instance = new Singleton();               
        }
        //instance = inst;               //5
      }
    }
  }
  return instance;
}

Пока что точка отсутствия записи после "instance = inst" не выполнена?

Прочитаю сейчас статью, спасибо за ссылку.

Джон Скит прав: прочтите статью Билла Пью. Идиома, которую использует Ханс, - это точная форма не будет работать, и ее не следует использовать.

Это небезопасно:

private static Singleton instance;

public static Singleton getInstance() {
  if (instance == null) {
    synchronized(Singleton.class) {
      if (instance == null) {
        instance = new Singleton();
      }
    }
  }
  return instance;
}

Это тоже небезопасно:

public static Singleton getInstance()  
{
    if (instance == null)
    {
        synchronized(Singleton.class) {      //1
            Singleton inst = instance;         //2
            if (inst == null)
            {
                synchronized(Singleton.class) {  //3
                    inst = new Singleton();        //4
                }
                instance = inst;                 //5
            }
        }
    }
    return instance;
}

Никогда не делайте ни того, ни другого.

Вместо этого синхронизируйте весь метод:

    public static synchronized Singleton getInstance() {
      if (instance == null) {
        instance = new Singleton();
      }
      return instance;
    }

Если вы не извлекаете этот объект миллион раз в секунду, снижение производительности в реальном выражении будет незначительным.

Статья относится к модели памяти Java (JMM) до версии 5.0. Согласно этой модели, оставив синхронизированный блок, принудительная запись в основную память. Таким образом, похоже, что это попытка убедиться, что объект Singleton выталкивается до ссылки на него. Однако это не совсем работает, потому что запись в экземпляр может быть перемещена в блок - мотель roach.

Однако модель до 5.0 так и не была реализована правильно. 1.4 должен соответствовать модели 5.0. Классы инициализируются лениво, так что вы можете просто написать

public static final Singleton instance = new Singleton();

Или лучше не используйте синглтоны, потому что они злые.

Начиная с Java 5, вы можете выполнить двойную проверку блокировки, объявив поле volatile.

См. http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html для полного объяснения.

По поводу этой идиомы есть очень полезная и уточняющая статья:

http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html?page=1

С другой стороны, я думаю, что dhighwayman.myopenid означает, почему писатель поместил один синхронизированный блок, относящийся к тому же классу (synchronized (Singleton.class)), в другой синхронизированный блок, относящийся к тому же классу. Это может произойти, если в этом блоке создается новый экземпляр (Singleton inst = instance;), и для обеспечения его потоковой безопасности необходимо написать еще один синхронизированный.

Иначе смысла не вижу.

Следуя рекомендации Джон Скит:

However, to go deeper into the subject I'd recommend Bill Pugh's article. And then never attempt it :)

А вот ключ для второго блока синхронизации:

This code puts construction of the Helper object inside an inner synchronized block. The intuitive idea here is that there should be a memory barrier at the point where synchronization is released, and that should prevent the reordering of the initialization of the Helper object and the assignment to the field helper.

Таким образом, в основном с помощью блока внутренней синхронизации мы пытаемся «обмануть» JMM, создавая экземпляр внутри блока синхронизации, чтобы заставить JMM выполнить это распределение до завершения блока синхронизации. Но проблема здесь в том, что JMM направляет нас вверх и перемещает назначение, которое находится перед блоком синхронизации, внутри блока синхронизации, возвращая нашу проблему к началу.

Это то, что я понял из тех статей, действительно интересно и еще раз спасибо за ответы.

Я расскажу об этом здесь:

http://tech.puredanger.com/2007/06/15/double-checked-locking/

См. Google Tech Talk на Модель памяти Java для действительно хорошего введения в тонкости JMM. Поскольку он отсутствует здесь, я также хотел бы указать на блог Джереми Мэнсона "Java Concurrency" особенно. сообщение на Двойная проверка блокировки (любой, кто хоть что-нибудь в мире Java, похоже, имеет статью об этом :).

Для Java 5 и выше на самом деле существует вариант с двойной проверкой, который может быть лучше, чем синхронизация всего средства доступа. Это также упоминается в Декларация о блокировке с двойной проверкой:

class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null)
                    helper = new Helper();
            }
        }
        return helper;
    }
}

Ключевым отличием здесь является использование летучий в объявлении переменной - в противном случае он не работает, и в любом случае он не работает в Java 1.4 или ниже.

Другие вопросы по теме