Как решить декларацию «Двойная проверка заблокирована» в Java? - PullRequest
32 голосов
/ 26 августа 2010

Я хочу реализовать ленивую инициализацию для многопоточности в Java.
У меня есть некоторый код такого рода:

class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            Helper h;
            synchronized(this) {
                h = helper;
                if (h == null) 
                    synchronized (this) {
                        h = new Helper();
                    } // release inner synchronization lock
                helper = h;
            } 
        }    
        return helper;
    }
    // other functions and members...
}

И я получаю объявление «Двойная проверка блокировки заблокирована».
Как я могу решить эту проблему?

Ответы [ 9 ]

71 голосов
/ 27 августа 2010

Вот идиома, рекомендуемая в Item 71: Используйте ленивую инициализацию разумно Эффективной Java:

Если вам нужно использовать ленивую инициализацию для производительности наполе экземпляра, используйте дважды проверить идиому .Эта идиома позволяет избежать затрат на блокировку при доступе к полю после его инициализации (пункт 67).Идея, лежащая в основе этой идиомы, состоит в том, чтобы проверять значение поля дважды (отсюда и название double-check ): один раз без блокировки, а затем, если поле кажется неинициализированным, второй раз с блокировкой.Только если вторая проверка указывает, что поле не инициализировано, вызов инициализирует поле.Поскольку нет блокировки, если поле уже инициализировано, критично , чтобы поле было объявлено volatile (Элемент 66).Вот идиома:

// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if (result != null) // First check (no locking)
        return result;
    synchronized(this) {
        if (field == null) // Second check (with locking)
            field = computeFieldValue();
        return field;
    }
}

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

До выпуска 1.5 идиома двойной проверки не работала надежно, потому что семантика модификатора volatile не быладостаточно сильный, чтобы поддержать его [Pugh01].Модель памяти, представленная в выпуске 1.5, исправила эту проблему [JLS, 17, Goetz06 16].Сегодня идиома двойной проверки - это метод выбора для ленивой инициализации поля экземпляра.Хотя вы можете применить идиому двойной проверки и к статическим полям, нет никаких оснований для этого: идиома класса ленивого держателя инициализации - лучший выбор.

Ссылка

  • Эффективная Java, второе издание
    • Элемент 71: Используйте ленивую инициализацию разумно
12 голосов
/ 26 августа 2010

Вот шаблон для правильной двойной проверки блокировки.

class Foo {

  private volatile HeavyWeight lazy;

  HeavyWeight getLazy() {
    HeavyWeight tmp = lazy; /* Minimize slow accesses to `volatile` member. */
    if (tmp == null) {
      synchronized (this) {
        tmp = lazy;
        if (tmp == null) 
          lazy = tmp = createHeavyWeightObject();
      }
    }
    return tmp;
  }

}

Для синглтона есть гораздо более читаемая идиома для отложенной инициализации.

class Singleton {
  private static class Ref {
    static final Singleton instance = new Singleton();
  }
  public static Singleton get() {
    return Ref.instance;
  }
}
3 голосов
/ 11 декабря 2015

DCL с использованием ThreadLocal By Брайан Гетц @ JavaWorld

что не так в DCL?

DCL полагается нанесинхронизированное использование поля ресурса.Это кажется безвредным, но это не так.Чтобы понять почему, представьте, что поток A находится внутри синхронизированного блока, выполняя инструкцию resource = new Resource ();в то время как поток B только вводит getResource ().Рассмотрим влияние на память этой инициализации.Память для нового объекта Resource будет выделена;будет вызван конструктор для Resource, инициализирующий поля-члены нового объекта;и полевому ресурсу SomeClass будет назначена ссылка на вновь созданный объект.

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

Однако, поскольку поток B не выполняется внутри синхронизированного блока, он может видеть эти операции с памятью в другом порядке, чемодин поток A выполняется.Это может быть случай, когда B видит эти события в следующем порядке (и компилятор также может свободно переупорядочивать такие инструкции): выделять память, назначать ссылку на ресурс, вызывать конструктор.Предположим, что поток B появляется после того, как память была выделена и поле ресурса установлено, но до вызова конструктора.Он видит, что ресурс не равен нулю, пропускает синхронизированный блок и возвращает ссылку на частично созданный ресурс!Излишне говорить, что результат не является ни ожидаемым, ни желаемым.

Может ли ThreadLocal помочь исправить DCL?

Мы можем использовать ThreadLocal для достижения явной цели идиомы DCL - ленивыйинициализация без синхронизации по общему пути кода.Рассмотрим эту (поточно-ориентированную) версию DCL:

Листинг 2. DCL с использованием ThreadLocal

class ThreadLocalDCL {
  private static ThreadLocal initHolder = new ThreadLocal();
  private static Resource resource = null;
  public Resource getResource() {
    if (initHolder.get() == null) {
      synchronized {
        if (resource == null) 
          resource = new Resource();
        initHolder.set(Boolean.TRUE);
      }
    }
    return resource;
  }
}

Я думаю;здесь каждый поток однажды входит в блок SYNC для обновления значения threadLocal;тогда не будет.Таким образом, ThreadLocal DCL обеспечит вход потока только один раз в блок SYNC.

Что на самом деле означает синхронизация?

Java обрабатывает каждый поток так, как если бы он выполнялся на своемсобственный процессор со своей локальной памятью, каждый из которых общается и синхронизируется с общей оперативной памятью.Даже в однопроцессорной системе эта модель имеет смысл из-за влияния кэшей памяти и использования регистров процессора для хранения переменных.Когда поток изменяет местоположение в своей локальной памяти, эта модификация в конечном итоге также должна отображаться в основной памяти, и JMM определяет правила, когда JVM должна передавать данные между локальной и основной памятью.Архитекторы Java поняли, что чрезмерно ограниченная модель памяти серьезно подорвет производительность программы.Они попытались создать модель памяти, которая позволила бы программам хорошо работать на современном компьютерном оборудовании, в то же время предоставив гарантии, которые позволили бы потокам взаимодействовать предсказуемым образом.

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

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

3 голосов
/ 26 августа 2010

Единственный способ сделать правильную двойную проверку блокировки в Java - это использовать «volatile» объявления для рассматриваемой переменной.Хотя это решение является правильным, обратите внимание, что «volatile» означает, что строки кэша сбрасываются при каждом доступе.Так как «синхронизированный» сбрасывает их в конце блока, он может на самом деле не быть более эффективным (или даже менее эффективным).Я бы рекомендовал просто не использовать двойную проверку блокировки, если вы не профилировали свой код и не обнаружили проблему с производительностью в этой области.

2 голосов
/ 17 января 2017

Копирование ниже откуда-то еще, что объясняет, почему использование локальной переменной метода в качестве копии для энергозависимой переменной ускорит процесс.

Заявление, требующее объяснения:

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

Пояснение:

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

2 голосов
/ 27 августа 2010

Как отметили некоторые, вам определенно необходимо ключевое слово volatile, чтобы оно работало правильно, если только все члены объекта не объявлены final, в противном случае не произойдет до безопасной публикации pr, и вы сможете увидетьзначения по умолчанию.

Мы устали от постоянных проблем с людьми, которые неправильно это понимают, поэтому мы закодировали утилиту LazyReference , которая имеет окончательную семантику, профилирована и настроена так быстровозможно.

2 голосов
/ 26 августа 2010

что вы имеете в виду, от кого вы получаете декларацию?

Исправлена ​​двойная проверка блокировки. проверьте википедию:

public class FinalWrapper<T>
{
    public final T value;
    public FinalWrapper(T value) { this.value = value; }
}

public class Foo
{
   private FinalWrapper<Helper> helperWrapper = null;
   public Helper getHelper()
   {
      FinalWrapper<Helper> wrapper = helperWrapper;
      if (wrapper == null)
      {
          synchronized(this)
          {
              if (helperWrapper ==null)
                  helperWrapper = new FinalWrapper<Helper>( new Helper() );
              wrapper = helperWrapper;
          }
      }
      return wrapper.value;
   }
2 голосов
/ 26 августа 2010

Определите переменную, которую нужно перепроверить с помощью volatile midifier

Вам не нужна переменная h.Вот пример из здесь

class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null)
                    helper = new Helper();
            }
        }
        return helper;
    }
}
0 голосов
/ 07 февраля 2018

Если я не ошибаюсь, есть и другое решение, если мы не хотим использовать ключевое слово volatile

например, взяв предыдущий пример

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

тест всегда выполняется для вспомогательной переменной, но построение объекта выполняется незадолго до этого с помощью newHelper, при этом избегается частично построенный объект

...