middle
Как создать потокобезопасный Singleton?
Существует несколько способов создания потокобезопасного Singleton в Java, каждый со своими компромиссами:
1. Static field (Eager initialization)
Пример
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
}
Экземпляр создаётся при загрузке класса. Класс-загрузчик гарантирует потокобезопасность инициализации.
- Плюс: простота, гарантированная потокобезопасность.
- Минус: экземпляр создаётся, даже если он никогда не будет использован (eager).
2. Enum
Пример
public enum Singleton {
INSTANCE;
public void doSomething() { /* ... */ }
}
- Плюс: самый лаконичный способ, защита от десериализации и рефлексии «из коробки» (JVM гарантирует уникальность enum-констант).
- Минус: невозможно наследование; enum загружается eager; не все считают использование enum для Singleton идиоматичным.
- Рекомендация Джошуа Блоха (Effective Java): «enum — лучший способ реализации Singleton».
3. Synchronized Accessor
Пример
public class Singleton {
private static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- Плюс: ленивая инициализация.
- Минус: каждый вызов
getInstance()проходит черезsynchronized— избыточная блокировка после первого создания.
4. Double Checked Locking + volatile
Пример
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
Singleton localInstance = instance;
if (localInstance == null) {
synchronized (Singleton.class) {
localInstance = instance;
if (localInstance == null) {
instance = localInstance = new Singleton();
}
}
}
return localInstance;
}
}
- Плюс: ленивая инициализация, блокировка только при первом создании.
- Минус: сложность кода, необходимость
volatile.
5. On Demand Holder Idiom (Initialization-on-demand holder)
Пример
public class Singleton {
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
- Плюс: ленивая инициализация (вложенный класс загружается только при первом вызове
getInstance()), потокобезопасность гарантирована JLS (спецификация загрузки классов), никакой синхронизации при доступе. - Минус: не защищён от рефлексии и десериализации (в отличие от enum).
Сравнительная таблица:
| Способ | Ленивость | Защита от рефлексии | Защита от десериализации | Сложность |
|---|---|---|---|---|
| Static field | Нет | Нет | Нет | Минимальная |
| Enum | Нет | Да | Да | Минимальная |
| Synchronized | Да | Нет | Нет | Низкая |
| DCL + volatile | Да | Нет | Нет | Средняя |
| Holder Idiom | Да | Нет | Нет | Низкая |
На собеседовании часто спрашивают: «Какой способ лучше?» Ответ зависит от контекста. Для большинства случаев — Enum (если не нужно наследование) или Holder Idiom (если нужна ленивость и привычный класс). DCL уместен, если нужно передать параметры в конструктор при первом вызове.