В этом кратком руководстве мы рассмотрим два наиболее популярных способа реализации синглтонов в Java.

2. Синглтон на основе класса

Наиболее распространённым подходом является создание синглтона путём создания обычного класса и проверки того, что он имеет:

  • приватный конструктор;

  • статическое поле, содержащее его единственный экземпляр;

  • статический фабричный метод для получения экземпляра.

Мы также добавим свойство info для последующего использования. Таким образом, наша реализация будет выглядеть следующим образом:

public final class ClassSingleton {

    private static ClassSingleton INSTANCE;
    private String info = "Initial info class";
    
    private ClassSingleton() {        
    }
    
    public static ClassSingleton getInstance() {
        if(INSTANCE == null) {
            INSTANCE = new ClassSingleton();
        }
        
        return INSTANCE;
    }

    // getters and setters
}

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

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

3. Enum Singleton

Двигаясь дальше, давайте обсудим ещё один интересный подход, который заключается в использовании перечислений:

public enum EnumSingleton {
    
    INSTANCE("Initial class info"); 
 
    private String info;
 
    private EnumSingleton(String info) {
        this.info = info;
    }
 
    public EnumSingleton getInstance() {
        return INSTANCE;
    }
    
    // getters and setters
}

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

4. Использование

Чтобы использовать ClassSingleton, нам просто нужно получить экземпляр статически:

ClassSingleton classSingleton1 = ClassSingleton.getInstance();

System.out.println(classSingleton1.getInfo()); //Initial class info

ClassSingleton classSingleton2 = ClassSingleton.getInstance();
classSingleton2.setInfo("New class info");

System.out.println(classSingleton1.getInfo()); //New class info
System.out.println(classSingleton2.getInfo()); //New class info

Что касается EnumSingleton, то его можно использовать как любой другой Enum:

EnumSingleton enumSingleton1 = EnumSingleton.INSTANCE.getInstance();

System.out.println(enumSingleton1.getInfo()); //Initial enum info

EnumSingleton enumSingleton2 = EnumSingleton.INSTANCE.getInstance();
enumSingleton2.setInfo("New enum info");

System.out.println(enumSingleton1.getInfo()); // New enum info
System.out.println(enumSingleton2.getInfo()); // New enum info

5. Подводные камни

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

Можно выделить два типа проблем с синглтонами:

  • экзистенциальные (нужен ли нам синглтон?)

  • реализационные (правильно ли мы его реализовали?)

5.1. Экзистенциальные проблемы

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

Мы не говорим, что не стоит использовать синглтоны; мы полагаем, что могут быть более эффективные способы организации кода.

Если реализация метода зависит от объекта-синглтона, почему бы не передать его в качестве параметра? В этом случае мы явно показываем, от чего зависит метод. В результате мы можем легко имитировать эти зависимости (если необходимо) при тестировании.

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

Поэтому при запуске тестов в продакшен базу данных попадают тестовые данные, что недопустимо.

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

5.2. Проблемы реализации

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

Синхронизация

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

public synchronized static ClassSingleton getInstance() {
    if (INSTANCE == null) {
        INSTANCE = new ClassSingleton();
    }
    return INSTANCE;
}

Обратите внимание на ключевое слово synchronized в объявлении метода. Тело метода содержит несколько операций (сравнение, инстанцирование и возврат).

При отсутствии синхронизации есть вероятность того, что два потока чередуют выполнения таким образом, что выражение INSTANCE == null оценивается как true для обоих потоков, и в результате создаётся два экземпляра ClassSingleton.

Синхронизация может существенно повлиять на производительность. Если этот код вызывается часто, следует ускорить его, используя различные техники, такие как ленивая инициализация или блокировка с двойной проверкой (имейте в виду, что это может работать не так, как ожидается, из-за оптимизаций компилятора). Подробнее об этом можно прочитать в статье "Double-Checked Locking with Singleton".

Множественные экземпляры

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

  1. Предполагается, что синглтон должен быть уникальным для каждой JVM. Это может быть проблемой для распределённых систем или систем, внутреннее устройство которых основано на распределённых технологиях.

  2. Каждый загрузчик классов может загружать свою версию синглтона.

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

6. Заключение

В этой небольшой статье мы рассмотрели, как реализовать паттерн Singleton с использованием только Java. Мы узнали, как обеспечить его согласованность и как использовать эти реализации.

Полную реализацию этих примеров можно найти на GitHub.

Материал переведён в преддверии старта онлайн-курса "Java QA Engineer. Basic".

Комментарии (25)


  1. n43jl
    25.03.2024 11:53
    +2

    Имхо использование enum для синглетон - это костыль и анти-паттерн, хотя работает отлично.

    Также мне нравится концепция Spring, где класс отвечает за то, что он должен делать, а не то как он инстанцируется. Singleton или нет - это вынесено: Spring bean scopes.


  1. TerraV
    25.03.2024 11:53

    Ох не соврать бы, вроде enum Singleton были "нормальными" в java 1.4 или 1.5. И я сейчас даже не могу припомнить почему. В 2024 году сам шаблон Singleton основательно потускнел, ну или по крайней мере его смысл поменялся (говорим спасибо облакам, кластерам, докеру). Как верно написал @n43jl сейчас мы мыслим категориями scope (контекст). Контекст, а в 99% случаях это Spring Context, сам определяет кого, сколько и в каких случаях инстанцировать. Даже зная что скоуп по умолчанию в спринге Singleton, при проектировании приложений нам необходимо закладывать множество инстансов приложения запущенных параллельно.


    1. n43jl
      25.03.2024 11:53
      +1

      С моей колокольни Singleton был очень популярен 15-20 лет назад, потому что очень часто не нужно больше одного инстанса и все писали свои велосипеды: Logger, DB connection pool, Caching, Any State Management, File System Management, etc. И к сожалению написать правильно синглетон, который будет работать в многопоточке - не так уж и просто, от туда и пошли ужасные костыли вида Double-Checked-Locking Singleton о которых меня даже спрашивали на собеседовании в районе 2010. Enum - решает все вопросы многопоточки, где single-instance гарантируется JVM. Более того использовать Enum для синглетона - это рекомендация из книги Effective Java (имхо анти-рекомендация).

      Но потом года с 2010 начали говорить про зло Global state, про Testability, и Spring начал становиться довольно популярным - Singleton стал анти-паттерном.


      1. aleksandy
        25.03.2024 11:53
        +1

        Более того использовать Enum для синглетона - это рекомендация из книги Effective Java (имхо анти-рекомендация).

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

        Spring начал становиться довольно популярным

        И теперь его, не думая, пихают везде где нужно и где не нужно.

        Singleton стал анти-паттерном

        И именно поэтому его использует вышеупомянутый Spring? Синглтон - это просто объект-одиночка в рамках, как правило, приложения. Вне зависимости от механизма реализации данного поведения, будь-то spring, CDI, micronaut, etc.


        1. n43jl
          25.03.2024 11:53
          +1

          В том что вы предлагаете также несколько других проблем:

          • Сломанная семантика: то что мы делаем синглетоном, очень часто никакого отношения к enumeration не имеет

          • Double responsibility - мы привязываем сервис и его жизненный цикл в один coupled кусок

          И теперь его, не думая, пихают везде где нужно и где не нужно.

          Это совершенно другой топик

          Синглтон - это просто объект-одиночка в рамках, как правило, приложения. Вне зависимости от механизма реализации

          Это понятно. Имхо анти-паттерн - это не class-lifecycle-management, а анти-паттерн связывание бизнес-логики (сервиса) и class-lifecycle-management вместе. Поэтому как GoF его преподносит, и как Effective Java рекомендует делать синглетоны - это имхо антипаттерн. А вот именно Spring class-lifecycle-management - это то, что позволяет нормально тестировать бизнес-логику не привязываясь сколько инстансов реально будет в проде.


          1. aleksandy
            25.03.2024 11:53

            очень часто никакого отношения к enumeration не имеет

            Именно поэтому enum приватный и напрямую наружу не торчит, потому что его использование обусловлено лишь гарантией единственности экземпляра со стороны самой jvm.
            Например в гугловой гуаве до появления лямбд через подобный механизм реализованы некоторые функции (Functions.toStringFunction(), Functions.identity(), ...)

            GoF его преподносит

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

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

            как Effective Java рекомендует делать синглетоны

            Но это единственно рабочий "изкоробочный" способ сделать синглтон без использования сторонних библиотек/фреймворков с минимальными усилиями.


            1. tuxi
              25.03.2024 11:53

              public class SingletonImpl {
              
                private static class Holder {
                  private static final SingletonImpl HOLDER_INSTANCE = new SingletonImpl();
                }
              
                public static SingletonImpl getInstance() {
                  return Holder.HOLDER_INSTANCE;
                }
              
                private SingletonImpl() {}
                
                public void doSomething() {}
                
              }

              а этот вариант через класс холдер чем плох?