Перейти к основному содержимому

Синглтоны в Java

· 5 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Введение

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

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

Самый популярный подход — реализовать Singleton, создав обычный класс и убедившись, что он имеет:

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

Мы также добавим свойство 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. Одноэлементное перечисление

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

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

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

Несколько экземпляров

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

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

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

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

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