«1. Введение

В этой быстрой статье мы обсудим два наиболее популярных способа реализации синглетонов в простой 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
}

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

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

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 оценивается как истинное для обоих потоков, и в результате создаются два экземпляра ClassSingleton.

«Синхронизация может существенно повлиять на производительность. Если этот код вызывается часто, мы должны ускорить его, используя различные методы, такие как ленивая инициализация или блокировка с двойной проверкой (имейте в виду, что это может работать не так, как ожидалось, из-за оптимизации компилятора). Мы можем увидеть более подробную информацию в нашем руководстве «Блокировка с двойной проверкой с помощью Singleton».

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

  1. A singleton is supposed to be unique per JVM. This might be a problem for distributed systems or systems whose internals are based on distributed technologies.
  2. Every class loader might load its version of the singleton.
  3. A singleton might be garbage-collected once no one holds a reference to it. This issue does not lead to the presence of multiple singleton instances at a time, but when recreated, the instance might differ from its previous version.

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

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

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