W3docs

Паттерн Singleton в Java

Реализация паттерна Singleton в Java: eager, lazy и enum-подходы с учётом потокобезопасности.

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

Сразу предостережение. «Singleton» печально известен тем, что его легко применять там, где не надо — каждый синглтон, по сути, является глобальной переменной, а глобальные переменные делают код сложнее для тестирования и понимания. Большинство современных Java-приложений предпочитают внедрение зависимостей: фреймворк сам создаёт единственный экземпляр и передаёт его нужным компонентам, и никому из них не приходится вызывать Foo.getInstance(). Прибегайте к явному синглтону лишь тогда, когда DI недоступен или действительно избыточен.

Что нужно каждому синглтону

Любая реализация синглтона включает три элемента:

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

Различия между реализациями в основном касаются момента создания экземпляра и способа обеспечения потокобезопасности доступа.

Ранняя инициализация

Простейшая форма создаёт экземпляр при загрузке класса:

public final class Eager {
  private static final Eager INSTANCE = new Eager();
  private Eager() {}
  public static Eager getInstance() { return INSTANCE; }
}

Инициализация класса в JVM гарантированно потокобезопасна и выполняется ровно один раз, поэтому INSTANCE устанавливается безопасно, без блокировок. Используйте этот подход, когда:

  • Создание объекта дешёво, или вы уверены, что экземпляр всегда понадобится.
  • Вас устраивает оплата этой стоимости в момент загрузки класса.

Ленивая инициализация с двойной проверкой блокировки

Если создание объекта дорогостоящее и экземпляр может не понадобиться, его инициализацию можно отложить. Наивная ленивая версия не является потокобезопасной; правильная использует двойную проверку блокировки с volatile:

public final class Lazy {
  private static volatile Lazy instance;
  private Lazy() {}

  public static Lazy getInstance() {
    Lazy local = instance;       // local read avoids re-reading the volatile field
    if (local == null) {
      synchronized (Lazy.class) {
        local = instance;
        if (local == null) {
          local = new Lazy();
          instance = local;
        }
      }
    }
    return local;
  }
}

volatile необходим — без него другой поток может увидеть поле установленным в ненулевую ссылку, хотя конструктор ещё не завершился. Громоздко, но корректно.

Holder-идиома (инициализация по требованию)

Самая чистая ленивая форма использует приватный вложенный класс. JVM загружает holder только при первом вызове getInstance(), поэтому работа откладывается — а гарантии JVM по инициализации класса обеспечивают потокобезопасность:

public final class Holder {
  private Holder() {}
  private static class H {
    private static final Holder INSTANCE = new Holder();
  }
  public static Holder getInstance() { return H.INSTANCE; }
}

Никакого synchronized, никакого volatile, никакой двойной проверки блокировки — и при этом ленивая инициализация. Именно эту ленивую форму стоит использовать в большинстве случаев.

Синглтон на основе enum

Самый короткий корректный синглтон в Java — это enum с одной константой:

public enum Config {
  INSTANCE;
  public String get(String key) { /* ... */ }
}

Config.INSTANCE и есть синглтон. Рекомендация Джошуа Блоха (Effective Java, Item 3) состоит в том, что это лучшая реализация синглтона, поскольку JVM гарантирует:

  • Ровно один экземпляр. Enum-ы создаются ровно один раз на JVM.
  • Потокобезопасное создание. Те же гарантии инициализации класса, что и в holder-паттерне.
  • Защита от Reflection. Reflection не может вызвать конструктор enum; обычные синглтоны можно обойти через Constructor.setAccessible(true).
  • Защита от сериализации. Десериализация обычного синглтона может незаметно породить второй экземпляр, если не обработать readResolve. Enum-ы от этого защищены.

Единственное, чего нельзя сделать с enum, — наследовать другой класс, так как enum-ы неявно расширяют java.lang.Enum. Реализовывать интерфейсы по-прежнему можно.

Что может сломать наивные синглтоны

Обратите внимание на следующие ситуации — именно поэтому «просто используй статическое поле» не всегда достаточно:

  • Несколько загрузчиков классов. Синглтон существует в одном экземпляре на загрузчик классов, а не на JVM. В контейнерах, изолирующих приложения с помощью собственных загрузчиков, у одного и того же класса может быть несколько «единственных» экземпляров.
  • Reflection. setAccessible(true) вместе с Constructor.newInstance() позволяют создать второй экземпляр любого синглтона, не основанного на enum. Если это реальная проблема, защитите конструктор с помощью if (INSTANCE != null) throw ....
  • Сериализация. Сериализуемому синглтону нужен метод private Object readResolve() { return INSTANCE; }, чтобы избежать создания второй копии при каждой десериализации.
  • Тестирование. Синглтоны печально трудно подменять или сбрасывать. Предпочитайте внедрение зависимостей в коде, который планируете покрывать юнит-тестами.

Работающий пример

java— editable, runs on the server

Что дальше

На этом завершается часть 6 и весь обзор объектно-ориентированного Java — от классов, наследования и полиморфизма до интерфейсов, enum-ов, records, запечатанных иерархий и методов, которые наследует каждый объект. В следующей части рассматривается организация Java-кода: пространства имён, структура файловой системы, которая их отражает, и механизм import, позволяющий использовать типы из других мест. Продолжайте с пакетами Java.

Практика

Практика
Почему синглтон на основе enum (`enum X { INSTANCE; ... }`) обычно считается лучшей реализацией синглтона в Java?
Почему синглтон на основе enum (`enum X { INSTANCE; ... }`) обычно считается лучшей реализацией синглтона в Java?
Was this page helpful?