Статьи

Двойная проверка блокировки в Java

В этой статье мы рассмотрим некоторые приемы создания объекта Singleton в RxJava. Самое главное, мы узнаем о двойной проверке блокировки в Java.

Синглтон-паттерн в Java — это творческий паттерн. Со временем появились опасения по поводу использования и реализации шаблона Singleton. Это вытекает из некоторых фундаментальных проблем, связанных с тем, как синглтоны реализуются и используются.

Синглтон шаблон в Java

Шаблон Singleton в Java имеет различные функции, такие как:

  1. Гарантирует, что только один экземпляр класса живет внутри JVM.
  2. Предоставляет глобальный доступ к экземпляру класса.
  3. Закрытый конструктор для предотвращения непосредственного создания экземпляра класса.
  4. Лучше всего использовать для регистрации, пула потоков, кэширования и т. Д.

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

основной

Вот базовая реализация шаблона Singleton в Java.

01
02
03
04
05
06
07
08
09
10
11
class Example{
     
  private Example mExample = null;
   
  public Example getInstance (){
    if (mExample == null)
      mExample = new Example ();
    return mExample;
  }
  // rest of the code...
}

Примечание. Конструктор будет закрытым во всех реализациях.

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

Преимущества:

  • Легко читать.
  • Будет хорошо работать в однопоточном приложении.

Недостатки:

  • Сбой в многопоточном контексте.
  • Несколько потоков могут создавать несколько экземпляров этого класса.
  • Не удалось бы цель синглетонов.

Держите это синхронизированным глупо

Некоторые умные люди придумали элегантное решение создания синглетонов. Мы используем ключевое слово synchronized для предотвращения одновременного доступа потоков к методу getInstance () .

01
02
03
04
05
06
07
08
09
10
11
class Example{
     
  private Example mExample = null;
   
  public synchronized Example getInstance (){
    if (mExample == null)
      mExample = new Example ();
    return mExample;
  }
  // rest of the code...
}

Используя ключевое слово synchronized , мы JVM позволяем только одному полю обращаться к этому методу одновременно. Это решает нашу проблему с многопоточными контекстами.

Но это не идеально!

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

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

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

Преимущества:

  • Очень хорошо справляется с многопоточной средой.
  • Легко понять.

Недостатки:

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

Двойная проверка блокировки

В предыдущем методе мы синхронизировали весь метод для обеспечения безопасности потоков. Но синхронизация работает не только с методами. Мы также можем создавать синхронизированные блоки.

В этом методе мы будем создавать синхронизированный блок вместо всего метода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class Example{
     
  private Example mExample = null;
   
  public Example getInstance (){
    if(mExample == null){
        synchronized(Example.class){
            if (mExample == null)
                mExample = new Example ();
        }
    }
    return mExample;
  }
  // rest of the code...
}

Вот последовательность шагов:

  • Первый поток вызывает метод getInstance ().
  • Он проверяет, является ли экземпляр пустым (для первого потока это так).
  • Затем он приобретает блокировку.
  • Проверяет, является ли поле все еще нулевым?
  • Если это так, он создает новый экземпляр класса и инициализирует поле. Наконец, экземпляр возвращается.
  • Остальным потокам не нужно захватывать блокировку, так как поле уже инициализировано, что снижает количество совпадений синхронизации!

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

Преимущества:

  • Работает в многопоточной среде.
  • Имеет гораздо лучшую производительность, чем синхронизированный метод.
  • Только первый поток должен получить блокировку.
  • Лучший из вышеперечисленных методов.

Недостатки:

  • Двойные нулевые проверки могут сначала сбить с толку.
  • Не работает !!

Подождите, что, это не работает ?!

Да, есть небольшая проблема с вышеуказанным методом. Это не всегда работает.

Проблема в том, что компилятор видит программы совсем иначе, чем человеческий глаз. Согласно нашей логике, сначала необходимо создать экземпляр класса Example, а затем присвоить его полю mExample.

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

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

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

Даг Ли написал подробный пост о переупорядочении на основе компилятора .

Пол Якубик нашел пример использования двойной проверки блокировки, которая не работала правильно.

Итак, что нам теперь делать?

Если все вышеперечисленные методы склонны к сбою, что у нас осталось?

В J2SE 5.0 модель памяти Java сильно изменилась. Ключевое слово volatile теперь решает проблему выше.

Платформа java не позволяет переупорядочивать чтение или запись изменчивого поля с любым предыдущим чтением или записью.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class Example{
     
  private volatile Example mExample = null;
   
  public Example getInstance (){
    if(mExample == null){
        synchronized(Example.class){
            if (mExample == null)
                mExample = new Example ();
        }
    }
    return mExample;
  }
  // rest of the code...
}

Осторожно: это работает только с JDK 5 и выше. Для разработчиков Android вы можете пойти, поскольку Android использует Java 7 и выше.

Вывод

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

Смотрите оригинальную статью здесь: Двойная проверка блокировки в Java

Мнения, высказанные участниками Java Code Geeks, являются их собственными.