Введение
Шаблон Одиночка на Java является одним из шаблонов проектирования Банды Четырех и относится к категории Шаблонов Создания. Судя по определению, он кажется простым шаблоном проектирования, но при реализации возникает множество вопросов.
В этой статье мы узнаем о принципах шаблона проектирования Одиночка, рассмотрим различные способы его реализации и некоторые лучшие практики его использования.
Принципы шаблона Одиночка
- Шаблон Одиночка ограничивает создание экземпляра класса и гарантирует, что виртуальная машина Java содержит только один экземпляр класса.
- Класс Одиночка должен предоставить глобальную точку доступа для получения экземпляра класса.
- Шаблон Одиночка используется для журналирования, объектов драйверов, кэширования и пула потоков.
- Одиночный шаблон проектирования также используется в других шаблонах проектирования, таких как Абстрактная фабрика, Строитель, Прототип, Фасад и т.д.
- Одиночный шаблон проектирования также используется в основных классах Java (например,
java.lang.Runtime
,java.awt.Desktop
).
Реализация шаблона Singleton в Java
Для реализации шаблона Singleton существует разные подходы, но все они имеют следующие общие концепции.
- Приватный конструктор для ограничения создания экземпляра класса из других классов.
- Приватная статическая переменная того же класса, которая является единственным экземпляром класса.
- Публичный статический метод, возвращающий экземпляр класса – это глобальная точка доступа для внешнего мира для получения экземпляра класса Singleton.
В последующих разделах мы рассмотрим различные подходы к реализации шаблона Singleton и проблемы проектирования при его реализации.
1. Стремительная инициализация
При стремительной инициализации экземпляр синглтона создается во время загрузки класса. Недостаток стремительной инициализации заключается в том, что метод создается даже если клиентское приложение может его не использовать. Вот реализация синглтона с использованием статической инициализации:
package com.journaldev.singleton;
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
// частный конструктор, чтобы избежать использования клиентскими приложениями конструктора
private EagerInitializedSingleton(){}
public static EagerInitializedSingleton getInstance() {
return instance;
}
}
Если ваш класс синглтона не использует много ресурсов, это подход, который следует использовать. Но в большинстве случаев классы синглтона создаются для ресурсов, таких как файловая система, соединения с базой данных и т. д. Мы должны избегать создания экземпляра до тех пор, пока клиент не вызовет метод getInstance
. Кроме того, этот метод не предоставляет никаких вариантов для обработки исключений.
2. Статическая блочная инициализация
Инициализация статического блока реализуется аналогично жадной инициализации, за исключением того, что экземпляр класса создается в статическом блоке, что предоставляет возможность для обработки исключений.
package com.journaldev.singleton;
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton(){}
// инициализация статического блока для обработки исключений
static {
try {
instance = new StaticBlockSingleton();
} catch (Exception e) {
throw new RuntimeException("Exception occurred in creating singleton instance");
}
}
public static StaticBlockSingleton getInstance() {
return instance;
}
}
И жадная инициализация, и инициализация статического блока создают экземпляр даже до его использования, и это не является лучшей практикой использования.
3. Ленивая инициализация
Метод ленивой инициализации для реализации паттерна Singleton создает экземпляр в методе глобального доступа. Вот пример кода для создания класса Singleton с использованием этого подхода:
package com.journaldev.singleton;
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton(){}
public static LazyInitializedSingleton getInstance() {
if (instance == null) {
instance = new LazyInitializedSingleton();
}
return instance;
}
}
Предыдущая реализация работает хорошо в случае однопоточной среды, но когда речь идет о многопоточных системах, это может вызвать проблемы, если несколько потоков находятся внутри условия if
одновременно. Это нарушит паттерн Singleton, и оба потока получат разные экземпляры класса Singleton. В следующем разделе мы рассмотрим различные способы создания потокобезопасного класса Singleton.
4. Однопоточный Синглтон
A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:
package com.journaldev.singleton;
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton(){}
public static synchronized ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
}
Предыдущая реализация работает нормально и обеспечивает потокобезопасность, но снижает производительность из-за затрат, связанных с синхронизированным методом, хотя нам это нужно только для первых нескольких потоков, которые могут создавать отдельные экземпляры. Чтобы избежать этого дополнительного накладного расхода каждый раз, используется принцип двойной проверки блокировки. В этом подходе синхронизированный блок используется внутри условия if
с дополнительной проверкой, чтобы гарантировать создание только одного экземпляра класса синглтона. Приведенный ниже фрагмент кода предоставляет реализацию двойной проверки блокировки:
public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
Продолжайте обучение с Классом Синглтона с Потокобезопасностью.
5. Реализация Синглтона по Биллу Пью
Перед Java 5 у модели памяти Java было много проблем, и предыдущие подходы терпели неудачу в определенных сценариях, когда слишком много потоков одновременно пытались получить экземпляр класса Singleton. Так что Bill Pugh предложил другой подход для создания класса Singleton с использованием внутреннего статического вспомогательного класса. Вот пример реализации Singleton от Bill Pugh:
package com.journaldev.singleton;
public class BillPughSingleton {
private BillPughSingleton(){}
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
Обратите внимание на частный внутренний статический класс, который содержит экземпляр класса Singleton. Когда класс Singleton загружается, класс SingletonHelper
не загружается в память, и только когда кто-то вызывает метод getInstance()
, этот класс загружается и создает экземпляр класса Singleton. Это самый распространенный подход к реализации класса Singleton, поскольку он не требует синхронизации.
6. Использование Reflection для разрушения шаблона Singleton
Reflection можно использовать для разрушения всех предыдущих подходов к реализации Singleton. Вот пример класса:
package com.journaldev.singleton;
import java.lang.reflect.Constructor;
public class ReflectionSingletonTest {
public static void main(String[] args) {
EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton instanceTwo = null;
try {
Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
// Этот код разрушит шаблон Singleton
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
Когда вы запустите предыдущий тестовый класс, вы заметите, что hashCode
обоих экземпляров не совпадает, что разрушает паттерн Singleton. Рефлексия очень мощный инструмент и используется во многих фреймворках, таких как Spring и Hibernate. Продолжайте изучать с Учебник по рефлексии в Java.
7. Перечисление Singleton
Чтобы преодолеть эту ситуацию с помощью рефлексии, Джошуа Блох предлагает использовать enum
для реализации паттерна проектирования Singleton, поскольку в Java гарантируется, что любое значение enum
инстанциируется только один раз в программе на Java. Поскольку значения Enum в Java доступны глобально, таков и Singleton. Недостаток заключается в том, что тип enum
относительно не гибок (например, он не позволяет ленивую инициализацию).
package com.journaldev.singleton;
public enum EnumSingleton {
INSTANCE;
public static void doSomething() {
// сделать что-то
}
}
8. Сериализация и Singleton
Иногда в распределенных системах нам нужно реализовать интерфейс Serializable
в классе-одиночке, чтобы мы могли сохранять его состояние в файловой системе и затем извлекать его в более поздний момент времени. Вот небольшой класс-одиночка, который также реализует интерфейс Serializable
:
package com.journaldev.singleton;
import java.io.Serializable;
public class SerializedSingleton implements Serializable {
private static final long serialVersionUID = -7604766932017737115L;
private SerializedSingleton(){}
private static class SingletonHelper {
private static final SerializedSingleton instance = new SerializedSingleton();
}
public static SerializedSingleton getInstance() {
return SingletonHelper.instance;
}
}
Проблема с сериализованным классом-одиночкой заключается в том, что при его десериализации будет создан новый экземпляр класса. Вот пример:
package com.journaldev.singleton;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class SingletonSerializedTest {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
SerializedSingleton instanceOne = SerializedSingleton.getInstance();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
"filename.ser"));
out.writeObject(instanceOne);
out.close();
// десериализация из файла в объект
ObjectInput in = new ObjectInputStream(new FileInputStream(
"filename.ser"));
SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
in.close();
System.out.println("instanceOne hashCode="+instanceOne.hashCode());
System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());
}
}
Этот код приводит к следующему результату:
OutputinstanceOne hashCode=2011117821
instanceTwo hashCode=109647522
Таким образом, это нарушает шаблон одиночки. Чтобы преодолеть этот сценарий, все, что нам нужно сделать, – предоставить реализацию метода readResolve()
.
protected Object readResolve() {
return getInstance();
}
После этого вы заметите, что hashCode
обоих экземпляров одинаков в тестовой программе.
Прочитайте о Сериализации в Java и Десериализации в Java.
Заключение
В этой статье рассмотрен шаблон проектирования одиночка.
Продолжайте свое обучение с дополнительными учебниками по Java.