Singleton

Назначение

Singleton обеспечивает наличие в системе только одного экземпляра заданного класса, позволяя другим классам получать доступ к этому экземпляру.

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

Описание

Допустим, имеется некоторая центральная функциональность, которая должна быть разделена на всё приложение. Такой функциональностью может быть, например, сбор статистики (истории) или управление единственным ресурсом. Эта функциональность должна быть реализована классом, единственный экземпляр которого используется всеми клиентами внутри приложения. Для избежания несогласованного состояния недопустимо позволять клиентам самостоятельно создавать экземпляры этого класса. С другой стороны, каждому клиенту необходимо предоставить доступ к единственному объекту.

Реализация

Во-первых, необходимо запретить создавать объекты класса. Для этого надо определить в классе только private конструктор(ы). Это также ведёт к потере возможности наследования такого класса. Для получения единственного экземпляра класса добавляется public static метод getInstance(), возвращающий единственный объект singleton класса. Ссылка на этот объект хранится как private static поле класса. Как правило, создание singleton объекта происходит при инициализации класса, но возможно и “lazy” инстанциирование, когда создание экземпляра происходит в момент первого вызова getInstance().

Опасности:

  • все конструкторы обязательно должны быть объявлены private и причем должен быть как минимум один конструктор, иначе Java добавит конструктор по-умолчанию;
  • класс не должен допускать альтернативных возможностей создания своих экземпляров: реализовывать интерфейсы Cloneable или Serializable;
  • внимательно надо относиться к реализации Singleton в условиях, когда программа ссылается на Singleton только через динамически загружаемые классы: апплеты, сервлеты, мидлеты. Если программа не использует классы, то они могут быть удалены сборщиком мусора. Когда Singleton будет загружен снова, то информация в старом экземпляре будет потеряна;
  • потокобезопасность.
  • потокобезопасность является проблемой только для ленивой инициализации; для инициализации при загрузке класса и варианта реализации через enum потокобезопасность достигается из коробки;
  • для ленивой инициализации при одновременном доступе нескольких потоков возможно создание нескольких экземпляров Singleton. Решения:
  • на потокобезопасность надо обращать внимание не только в контексте инициализации Singleton, но и в контексте вызова других методов, так как если существует единственный объект, то все потоки используют только этот объект.

Singleton
Runtime
Класс, объект которого существует в единственном экземпляре.
instance
private static final Runtime currentRuntime
private static final поле типа Singleton, ссылка на тот самый единственный экземпляр, который создается при инициализации класса (как правило)
Singleton()
private Runtime() {}
privateконструктор (часто пустой) для защиты от создания новых объектов
getInstance()
public static Runtime getRuntime()
public staticметод для доступа к единственному экземпляру класса, т.е. возвращает ссылку на instance

Примеры

  • Класс java.lang.Runtime имеет единственный private конструктор с комментарием /** Don't let anyone else instantiate this class */ и статический метод getRuntime(), который возвращает private static final экземпляр currentRuntime.
  • Может использоваться например для реализации Null Object. Так как Null Object обычно всё равно не содержит никакой информации о состоянии, то для него подойдет реализация в виде Singleton.

Варианты

  1. “Канонический”, не ленивый: создание экземпляра при инициализации класса:
    public class Singleton {
     private static final Singleton instance = new Singleton();
    
     private Singleton(){}
    
     public static Singleton getInstance() {
         return instance;
     }
    }
    
  2. Ленивый (потоконебезопасный) Singleton: instance создается не при инициализации класса, а лениво, во время вызова getInstance():
    // ...
    public static Singleton getInstance() {
     if (instance == null) {
         instance = new Singleton();
     }
     return instance;
    }
    
  3. Ленивый (потокобезопасный) Singleton через synchronized. Добавлен модификатор synchronized к методу getInstance().
  4. Ленивый (потокобезопасный) Singleton через идиому Double Checked Locking & volatile
    public class Singleton {
     private static volatile Singleton instance;
    
     public static Singleton getInstance() {
         Singleton localInstance = instance;
         if (localInstance == null) {
             synchronized (Singleton.class) {
                 localInstance = instance;
                 if (localInstance == null) {
                     instance = localInstance = new Singleton();
                 }
             }
         }
         return localInstance;
     }
    }
    
  5. Ленивый (потокобезопасный) Singleton через идиому On Demand Holder idiom
    public class Singleton {
     private static class SingletonHolder {
         public static final Singleton HOLDER_INSTANCE = new Singleton();
     }
    
     public static Singleton getInstance() {
         return SingletonHolder.HOLDER_INSTANCE;
     }
    }
    

    Идиома основана на том, что класс инициализируется только при первом активном использовании (см. справку). То есть, инициализация класса Singleton выполняется тривиально (в нем нет static переменных или static блоков инициализации) и не вызывает инициализацию SingletonHolder. Инициализация SingletonHolder выполняется в момент использования статического поля HOLDER_INSTANCE внутри метода getInstance(). JLS гарантирует последовательную, не многопоточную инициализацию класса.

  6. Реализация через enum, не ленивый (предпочтительна по Effective Java by Joshua Bloch):
    public enum Singleton {
     INSTANCE;
     public void execute(String arg){
         // perform operation here
     }
    }
    
  7. Допустимо несколько экземпляров Singleton. Метод getInstance() управляет распределением экземпляров и иногда может создавать новые. Обобщается до шаблона ObjectPool.
  8. Метод getInstance() может возвращать подклассы Singleton.

Справка

Следует различать инициализацию объектов класса, инициализацию классов и загрузку классов.

Загрузка класса - чтение его бинарного представления в JVM. Момент загрузки класса явно не специфицирован, известно лишь что она происходит до инициализации класса. Загрузка может быть ленивой или нет в зависимости от JVM.

Инициализация класса - установка начальных значений static переменных и выполнение кода в static блоках. Момент инициализации класса - первое активное использование класса:

  • создается экземпляр класса (в том числе через reflection, клонирование, десериализацию)
  • вызывается статический метод класса
  • присваивается значение статической переменной, объявленной в классе
  • используется статическое поле, если это не константа
  • выполняется выражение assert, вложенное лексически в класс верхнего уровня

Ссылки

https://java-design-patterns.com/patterns/singleton/

https://github.com/iluwatar/java-design-patterns/tree/master/singleton

StackOverflow: Examples of singleton classes in the Java APIs

Хабр: Правильный Singleton в Java

Wikipedia: Initialization-on-demand holder idiom

https://refactoring.guru/design-patterns/singleton

Baeldung - Singletons in Java