Шаблон одиночки

Шаблон Одиночка — один из простейших паттернов проектирования в Java. Этот тип паттернов проектирования относится к порождающим паттернам, и он предоставляет оптимальный способ создания объектов.

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

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

Примечание:

  • 1. У класса-одиночки может быть только один экземпляр.
  • 2. Класс-одиночка должен создавать свой собственный уникальный экземпляр.
  • 3. Класс-одиночка должен предоставлять этот экземпляр всем другим объектам.

summary

Шаблон одиночки

назначение

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

В основном решает

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

Когда использовать

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

Как решить

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

Ключевой код

Конструктор является приватным.

Пример приложения

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

Преимущества

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

Недостатки

  • Нет интерфейса, нельзя наследовать.
  • Это противоречит принципу единственной ответственности; класс должен заботиться только о своей внутренней логике, а не о том, как он инстанцируется.

Сценарий использования

  • Генерировать уникальные серийные номера.
  • Счётчик в WEB-разработке, позволяющий избежать увеличения значения счётчика в базе данных при каждом обновлении; вместо этого он сначала кэшируется.
  • Создание объектов, потребляющих слишком много ресурсов, таких как I/O и подключения к базе данных.

Примечания

  • Потокобезопасность:getInstance()В методе должны использоваться блокировки синхронизации.synchronized (Singleton.class)Чтобы предотвратить одновременный вход нескольких потоков, из-за которого экземпляр может создаваться несколько раз.
  • Ленивая инициализация: экземпляр создается при первом вызовеgetInstance()метода.
  • Сериализация и десериализация.: ПереопределитеreadResolveМетод, чтобы гарантировать, что при десериализации не создается новый экземпляр.
  • Атака через рефлексию: Добавьте защитный код в конструктор, чтобы предотвратить создание новых экземпляров через рефлексию.
  • Проблема загрузчика классов: Учитывайте проблемы с множественными экземплярами, которые могут быть вызваны сложными средами загрузки классов.

Структура

Шаблон одиночка включает следующие основные роли:

  • Класс-одиночка: класс, который содержит экземпляр-одиночку и обычно объявляет конструктор приватным.
  • Статическая переменная-член: статическая переменная-член, используемая для хранения экземпляра-одиночки.
  • Метод получения экземпляра: статический метод, используемый для получения экземпляра-одиночки.
  • Закрытый конструктор: предотвращает прямое создание экземпляра класса-одиночки внешним кодом.
  • Обработка потокобезопасности: гарантирует, что создание экземпляра одиночки безопасно в многопоточной среде.

Реализация

Мы создадим классSingleObject.SingleObjectКласс имеет свой закрытый конструктор и собственный статический экземпляр.

SingleObjectКласс предоставляет статический метод, с помощью которого внешний код может получить его статический экземпляр.SingletonPatternDemoИспользование классаSingleObjectкласса, чтобы получитьSingleObjectОбъект.

单例模式的 UML 图

Шаг 1

Создайте класс-одиночку.

SingleObject.java

public class SingleObject { //Создайте объект SingleObject private static SingleObject instance = new SingleObject(); //Сделайте конструктор закрытым, чтобы класс нельзя было создать private SingleObject(){} //Получите единственный доступный объект public static SingleObject getInstance(){ return instance; } public void showMessage(){ System.out.println("Hello World!"); } }

Шаг 2

Получите уникальный объект из класса-одиночки.

SingletonPatternDemo.java

public class SingletonPatternDemo { public static void main(String[] args) { //Недопустимый конструктор //Ошибка времени компиляции: конструктор SingleObject() не виден //SingleObject object = new SingleObject(); //Получите единственный доступный объект SingleObject object = SingleObject.getInstance(); //Показать сообщение object.showMessage(); } }

Шаг 3

выполнение программы, вывод результата:

Hello World!

Несколько подходов к реализации шаблона одиночка

Существует несколько способов реализации шаблона одиночка, как показано ниже:

1. Ленивый режим, небезопасный для потоков

Ленивая инициализация:Да

Потокобезопасно:нет

Сложность реализации:низкая

Описание:Это самая базовая реализация. Главная проблема этой реализации в том, что она не поддерживает многопоточность. Поскольку здесь нет synchronized-блокировки, строго говоря, это не паттерн «одиночка».
Этот подход имеет очевидную ленивую загрузку, не требует потокобезопасности и не может корректно работать в многопоточной среде.

Пример

public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

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

2. Ленивый режим, потокобезопасный

Ленивая инициализация:Да

Является ли потокобезопасным:Да

Сложность реализации:низкая

Описание:Этот подход обеспечивает хорошую ленивую загрузку и может хорошо работать при многопоточности, но его эффективность очень низкая, и синхронизация не нужна в 99% случаев.
Преимущества: инициализируется только при первом вызове, что позволяет избежать напрасного расхода памяти.
Недостатки: нужно добавить synchronized, чтобы обеспечить поведение синглтона, но блокировка влияет на эффективность.
Производительность getInstance() не критична для приложения (этот метод используется нечасто).

Пример

public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

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

Является ли инициализация ленивой:нет

Является ли потокобезопасным:Да

Сложность реализации:низкая

Описание:Этот подход довольно распространён, но он может легко создавать мусорные объекты.
Преимущества: отсутствие блокировок, повышается эффективность выполнения.
Недостатки: инициализируется во время загрузки класса, расходуется память.
Он позволяет избежать проблем синхронизации многопоточности благодаря механизму загрузчика классов. Однако экземпляр создаётся при загрузке класса. Хотя существует много причин, вызывающих загрузку класса, в шаблоне singleton большинство из них — вызовы метода getInstance, но нельзя подтвердить, что нет других способов (или других статических методов), вызывающих загрузку класса. В таком случае инициализация экземпляра, очевидно, не достигает эффекта ленивой загрузки.

Пример

public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }

4. Блокировка с двойной проверкой / double-checked locking (DCL, то есть блокировка с двойной проверкой)

Версия JDK:Начиная с JDK 1.5

Ленивая инициализация:Да

Является ли потокобезопасным:Да

Сложность реализации:Более сложная

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

Пример

public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }

5. Реестр/Статический внутренний класс

Ленивая инициализация:Да

Является ли потокобезопасным:Да

Сложность реализации:Средняя

Описание:Этот подход позволяет достичь того же эффекта, что и блокировка с двойной проверкой, но его проще реализовать. Для ленивой инициализации статических полей следует использовать этот подход вместо блокировки с двойной проверкой. Этот подход применим только к статическим полям; блокировку с двойной проверкой можно использовать, когда поля экземпляра требуют ленивой инициализации.
Этот подход также использует механизм загрузчика классов, чтобы гарантировать, что только один поток инициализирует экземпляр. Он отличается от подхода 3 тем, что: в подходе 3, как только класс Singleton загружается, экземпляр будет создан (без достижения ленивой загрузки), тогда как в этом подходе после загрузки класса Singleton экземпляр не обязательно инициализируется. Поскольку класс SingletonHolder активно не используется, только когда метод getInstance явно вызывается, класс SingletonHolder явно загружается, и тем самым создаётся экземпляр. Представьте, что если создание экземпляра потребляет много ресурсов, вы можете захотеть, чтобы он загружался лениво. С другой стороны, вы можете не захотеть, чтобы он создавался при загрузке класса Singleton, потому что нет гарантии, что класс Singleton не будет активно использоваться и загружаться где-то ещё. В этом случае создание экземпляра в тот момент, очевидно, неуместно. В этом плане этот подход гораздо разумнее, чем подход 3.

Пример

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

6. Перечисление

Версия JDK:Начиная с JDK 1.5

Является ли инициализация ленивой:нет

Является ли потокобезопасным:Да

Сложность реализации:низкая

Описание:Этот подход к реализации не получил широкого распространения, но он является лучшим способом реализации шаблона одиночка. Он более лаконичен, автоматически поддерживает механизм сериализации и абсолютно предотвращает многократное создание экземпляров.
Этот подход отстаивает Джош Блох, автор Effective Java. Он не только позволяет избежать проблем синхронизации при многопоточности, но и автоматически поддерживает механизм сериализации, предотвращает повторное создание новых объектов при десериализации и абсолютно исключает множественное создание экземпляров. Однако, поскольку функция enum была добавлена только после JDK 1.5, такой способ написания неизбежно кажется непривычным, и на практике он используется редко.
С его помощью нельзя вызвать приватный конструктор через атаку с использованием рефлексии.

Пример

public enum Singleton { INSTANCE; public void whateverMethod() { } }

Из опыта:В общем случае не рекомендуется использовать 1-й и 2-й ленивые подходы; рекомендуется 3-й нетерпеливый подход. Только когда необходимо явно реализовать ленивую загрузку, следует использовать 5-й подход с регистрацией. Если при создании объектов задействована десериализация, можно попробовать 6-й подход с enum. Если есть другие особые требования, можно рассмотреть 4-й подход с двойной проверкой блокировки.

Другие расширения