Как разрешить декларацию с двойной проверкой блокировки в Java?

Я хочу реализовать ленивую инициализацию для многопоточности в 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...
}

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

Ответ 1

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

Если вам нужно использовать ленивую инициализацию для производительности на поле экземпляра, используйте идиому двойной проверки. Эта идиома позволяет избежать затрат на блокировку при доступе к полю после его инициализации (пункт 67). Идея, лежащая в основе этой идиомы, состоит в том, чтобы проверять значение поля дважды (отсюда и название дважды): один раз без блокировки, а затем, если поле кажется неинициализированным, второй раз с блокировкой. Только если вторая проверка указывает, что поле не инициализировано, вызов инициализирует поле. Поскольку нет блокировки, если поле уже инициализировано, очень важно, чтобы поле было объявлено как 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 идиома двойной проверки не работала надежно, потому что семантика летучего модификатора была недостаточно сильной, чтобы его поддерживать [Pugh01]. Модель памяти, представленная в выпуске 1.5, исправила эту проблему [JLS, 17, Goetz06 16]. Сегодня идиома двойной проверки - это метод выбора для ленивой инициализации поля экземпляра. В то время как вы можете применить идиому двойной проверки и к статическим полям, нет никаких оснований для этого: идиома класса ленивого держателя инициализации - лучший выбор.

Ссылка

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

Ответ 2

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

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

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

Ответ 4

DCL с помощью ThreadLocal Брайан Гетц @JavaWorld

что сломано в DCL?

DCL полагается на несинхронизированное использование поля ресурсов. Это кажется безвредным, но это не так. Чтобы понять, почему, представьте, что поток A находится внутри синхронизированного блока, выполняя оператор resource = new Resource(); в то время как поток B просто вводит getResource(). Рассмотрим влияние на память этой инициализации. Будет выделена память для нового ресурса; вызывается конструктор 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 для достижения явной цели idlom 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, имеют явные машинные инструкции для выполнения барьеров памяти.) Когда поток выходит из синхронизированного блока, он выполняет барьер записи - он должен вымыть любые переменные, измененные в этом блоке, в основную память перед выпуском замок. Аналогично, при входе в синхронизированный блок он выполняет барьер чтения - это как если бы локальная память была недействительной и она должна извлекать любые переменные, на которые будут ссылаться в блоке из основной памяти.

Ответ 5

Определите переменную, которая должна быть дважды проверена с помощью 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;
    }
}

Ответ 6

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

Двойная проверка заблокирована. проверьте википедию:

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;
   }

Ответ 7

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

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

Ответ 8

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

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

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

Пояснение:

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

Ответ 9

Если я не ошибаюсь, есть и другое решение, если мы не хотим использовать ключевое слово 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, что позволяет избежать создания частично созданного объекта.