← Назад к вопросам

Как обойтись без synchronized в многопоточности на Singleton

2.0 Middle🔥 131 комментариев
#SOLID и паттерны проектирования#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Singleton без synchronized в многопоточности

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

1. Ленивая инициализация с Double-Checked Locking

Использует оптимизацию: проверяем дважды, синхронизируем только блок инициализации:

public class Singleton {
    private static volatile Singleton instance; // volatile — важно!
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) { // Первая проверка без блокировки
            synchronized (Singleton.class) {
                if (instance == null) { // Вторая проверка внутри блокировки
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Почему это работает:

  • volatile гарантирует видимость изменений между потоками
  • Первая проверка избегает синхронизации в 99% случаев
  • Вторая проверка предотвращает множественное создание экземпляра
  • Синхронизируем только блок, не весь метод

Недостаток: Не очень читаемо из-за complexity.

2. Eager Initialization (Инициализация при загрузке класса)

Создаём Singleton в статическом инициализаторе — это потокобезопасно по умолчанию:

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

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

  • Простая, понятная реализация
  • Гарантирует потокобезопасность благодаря гарантиям Java для static инициализации
  • Нет лишних проверок
  • Нет synchronized

Недостаток: Singleton создаётся при загрузке класса, даже если никогда не используется.

3. Holder Pattern (Инициализация при первом доступе) ⭐

Использует static inner class для ленивой инициализации без синхронизации:

public class Singleton {
    private Singleton() {}
    
    // Static inner class загружается только при первом обращении к SingletonHolder
    private static class SingletonHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Как это работает:

  • Inner class не загружается до момента, пока не будет обращение к SingletonHolder
  • При обращении Java гарантирует потокобезопасную инициализацию static переменной
  • Нет synchronized, нет volatile
  • Ленивая инициализация
  • Идеально читаемо

Это считается лучшей практикой для многопоточного Singleton.

4. Enum (Лучший способ для Java 5+)

Enum по сути гарантирует потокобезопасность и сериализацию:

public enum Singleton {
    INSTANCE;
    
    private String state = "initial";
    
    public void setState(String state) {
        this.state = state;
    }
    
    public String getState() {
        return state;
    }
}

// Использование
public class App {
    public static void main(String[] args) {
        Singleton singleton = Singleton.INSTANCE;
        singleton.setState("ready");
    }
}

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

  • Гарантирует одинственность экземпляра по спецификации Java
  • Автоматически обрабатывает сериализацию
  • Защищён от рефлексии
  • Просто и элегантно
  • Современный стандарт

Сравнение методов

МетодПотокобезопасностьЛенивая инициализацияЧитаемостьРекомендуется
Synchronized методеНет
Double-Checked Locking⭐⭐Редко
Eager initialization⭐⭐⭐Для простых случаев
Holder Pattern⭐⭐⭐Да для классов
Enum⭐⭐⭐⭐⭐Да (лучший выбор)

Почему вообще избегать synchronized

  1. Производительность — синхронизация имеет оверхэд
  2. Deadlocks — сложнее отлаживать проблемы
  3. Читаемость — код с синхронизацией сложнее понять
  4. Альтернативы — есть более эффективные способы

Вывод

Для современного Java кода используйте Enum — это самый безопасный и элегантный способ. Если нужен класс (не enum), то Holder Pattern — лучший компромисс между производительностью и читаемостью.

Как обойтись без synchronized в многопоточности на Singleton | PrepBro