Питання 3 · Розділ 2

Що таке Singleton?

Singleton може реалізувати інтерфейс (тестованість через моки), static class — ні. Singleton підтримує ледачу ініціалізацію, static class ініціалізується при завантаженні класу.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Singleton — патерн, який гарантує, що у класу буде лише один екземпляр.

Навіщо: коли ресурс спільний (лог-файл, підключення до БД, конфігурація) і кілька копій призведуть до конфліктів або неконсистентного стану.

Проста аналогія: У країні може бути лише один президент. Якщо ви запитаєте “хто президент?”, вам завжди назвуть одну й ту саму людину.

Як працює:

  1. Приватний конструктор (не можна створити через new)
  2. Статичне поле з єдиним екземпляром
  3. Статичний метод для отримання цього екземпляра

Приклад:

public class Logger {
    // 1. Єдиний екземпляр
    private static Logger instance = new Logger();

    // 2. Приватний конструктор
    private Logger() {}

    // 3. Метод для отримання екземпляра
    public static Logger getInstance() {
        return instance;
    }

    public void log(String message) {
        System.out.println(message);
    }
}

// Використання — завжди один і той самий об'єкт
Logger logger1 = Logger.getInstance();
Logger logger2 = Logger.getInstance();
System.out.println(logger1 == logger2); // true (один об'єкт!)

Коли використовувати:

  • Логер (один на весь додаток)
  • Конфігурація (один файл налаштувань)
  • Підключення до БД (пул з’єднань)

Singleton vs static class

Singleton може реалізувати інтерфейс (тестованість через моки), static class — ні. Singleton підтримує ледачу ініціалізацію, static class ініціалізується при завантаженні класу.


🟡 Middle Level

Реалізації Singleton

1. Early Initialization (проста):

public class Singleton {
    private static Singleton instance = new Singleton(); // При завантаженні класу
    private Singleton() {}
    public static Singleton getInstance() { return instance; }
}
// ✅ Потокобезпечна
// ❌ Не ледача (створюється навіть якщо не потрібна)

2. Lazy Initialization:

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
// ✅ Ледача
// ❌ Не потокобезпечна!

3. Thread-Safe (synchronized):

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
// ✅ Потокобезпечна
// ❌ Повільна: synchronized додає ~10-50ns на кожен виклик, що критично в hot-path з мільйонами викликів.

4. Enum Singleton (найкраща):

public enum Singleton {
    INSTANCE;

    public void doWork() {
        System.out.println("Working...");
    }
}

// Використання
Singleton.INSTANCE.doWork();
// ✅ Потокобезпечна
// ✅ Захист від рефлексії
// ✅ Захист від серіалізації

Як можна “зламати” Singleton

1. Reflection:

// ❌ Звичайний Singleton можна зламати
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton fake = constructor.newInstance();  // Другий екземпляр!

// ✅ Enum захищений
Constructor<SingletonEnum> constructor = SingletonEnum.class.getDeclaredConstructor();
// → IllegalArgumentException: Cannot reflectively create enum objects

2. Serialization:

// ❌ При десеріалізації створюється новий об'єкт
ObjectOutputStream out = ...;
out.writeObject(Singleton.getInstance());

ObjectInputStream in = ...;
Singleton another = (Singleton) in.readObject();  // Новий об'єкт!

// ✅ Рішення: метод readResolve()
private Object readResolve() {
    return getInstance();  // Повернути наявний
}

3. ClassLoader:

// Якщо кілька ClassLoader → кілька Singleton!
// Один у Tomcat, інший в OSGi

Singleton у Spring

// Spring керує Singleton сам
@Component  // Singleton scope за замовчуванням!
public class UserService { }

// Завжди один і той самий бін
@Autowired UserService service1;
@Autowired UserService service2;
// service1 == service2 → true

Типові помилки

  1. Singleton замість DI
    // ❌ Прихована залежність
    public class OrderService {
        public void create() {
            Logger.getInstance().log("...");  // Прихована залежність!
        }
    }
    
    // ✅ DI
    public class OrderService {
        private final Logger logger;
        public OrderService(Logger logger) { this.logger = logger; }
    }
    
  2. Стан у Singleton
    // ❌ Мутабельний Singleton = проблеми у багатопотоковості
    public class Counter {
        private int count = 0;  // Race condition!
        public void increment() { count++; }
    }
    

🔴 Senior Level

Контекстуальність Singleton

“Єдиний екземпляр” — це відносно:

Контекст Єдиність
JVM Один екземпляр на JVM
ClassLoader Один на ClassLoader (може бути кілька!)
Spring Context Один на ApplicationContext
Розподілена система НЕ ІСНУЄ (потрібен розподілений блокувальник)

У кластері:

Node 1: Singleton екземпляр A
Node 2: Singleton екземпляр B
Node 3: Singleton екземпляр C
→ 3 екземпляри! Потрібен Redis/Zookeeper для координації

Java Memory Model та Singleton

JMM (Java Memory Model) — специфікація, що визначає, як потоки бачать пам’ять один одного. Bill Pugh Singleton — названий на честь дослідника, що запропонував рішення через static inner class. SRP (Single Responsibility Principle) — принцип єдиної відповідальності.

Проблема visibility без volatile:

// ❌ Небезпечний код
public class UnsafeSingleton {
    private static UnsafeSingleton instance;

    public static UnsafeSingleton getInstance() {
        if (instance == null) {
            instance = new UnsafeSingleton();
        }
        return instance;
    }
}

// Потік 1: виділив пам'ять → опублікував посилання → викликав конструктор
// Потік 2: бачить instance != null → отримує НЕІНІЦІАЛІЗОВАНИЙ об'єкт!

// ✅ Рішення: volatile + double-checked locking
private static volatile UnsafeSingleton instance;

public static UnsafeSingleton getInstance() {
    UnsafeSingleton local = instance;
    if (local == null) {
        synchronized (UnsafeSingleton.class) {
            local = instance;
            if (local == null) {
                instance = local = new UnsafeSingleton();
            }
        }
    }
    return local;
}

Чому volatile критичний:

  • Без volatile: інструкції reorder (виділення пам’яті → публікація → конструктор)
  • З volatile: happens-before гарантія → конструктор ЗАВЕРШЕНИЙ до публікації

Singleton як Anti-pattern

Проблеми:

  1. Tight Coupling
    // Залежність не видна у сигнатурі
    public class OrderService {
        public void process() {
            Database.getInstance().save(...);  // Прихована залежність!
        }
    }
    
  2. Тестованість
    // ❌ Неможливо замокати
    @Test
    void testOrder() {
        // Singleton зберігає стан між тестами!
        // Неможливо підмінити Database.getInstance()
    }
    
  3. Порушення SRP
    // Клас відповідає за:
    // 1. Бізнес-логіку
    // 2. Керування своїм життєвим циклом
    // 3. Конкурентний доступ
    
  4. Memory Leak
    // Singleton живе вічно (статичне поле)
    // У Tomcat при redeploy → старий ClassLoader не GC
    // → Metaspace Leak
    

Bill Pugh Singleton (Static Holder)

public class BillPughSingleton {
    private BillPughSingleton() {
        if (INSTANCE_HOLDER.INSTANCE != null) {
            throw new IllegalStateException("Already initialized");
        }
    }

    private static class INSTANCE_HOLDER {
        static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return INSTANCE_HOLDER.INSTANCE;
    }
}

// JVM гарантує:
// 1. Ледачість (клас завантажиться тільки при першому виклику)
// 2. Потокобезпечність (ініціалізація класу thread-safe)
// 3. Без synchronized overhead

GC і Singleton

// Singleton НЕ буде видалений GC, доки:
// 1. Живий ClassLoader
// 2. Є посилання на статичне поле

// У звичайному Java: ClassLoader живий до кінця процесу
// → Singleton живе "вічно"

// У Tomcat/OSGi: ClassLoader може бути вивантажений
// → Але Singleton заважає GC → Memory Leak

Production Experience

Реальний сценарій #1: Singleton убив тестованість

  • 500 тестів, усі використовують Config.getInstance()
  • Проблема: тести залежать один від одного (спільний стан)
  • Рішення: мігрували на Spring DI
  • Результат: тести ізольовані, паралельний запуск

Реальний сценарій #2: Singleton у кластері

  • ID генератор як Singleton
  • Проблема: у кластері 5 нод → 5 генераторів → дублі ID!
  • Рішення: Redis INCR для генерації ID
  • Урок: Singleton ≠ розподілена єдиність

Best Practices

  1. НЕ пишіть Singleton вручну у Spring-додатках
  2. Використовуйте DI (@Component = Singleton scope)
  3. Enum — найбезпечніший для бібліотек/SDK
  4. Bill Pugh — для ледачої ініціалізації
  5. Уникайте мутабельного стану у Singleton
  6. readResolve() для захисту від серіалізації
  7. Перевірка у конструкторі для захисту від рефлексії
  8. У кластері → Redis/Zookeeper для координації

Резюме для Senior

  • Singleton ≠ розподілена єдиність — тільки в межах JVM
  • Enum — єдиний захист від reflection/serialization атак
  • DI-контейнер замінює ручні Singleton у 99% випадків
  • Memory Model: volatile критичний для DCL
  • Тестованість: Singleton = ворог №1 unit-тестів
  • GC: Singleton живе вічно → обережно з ресурсами
  • Bill Pugh = ледачість + потокобезпечність без synchronized
  • Приховані залежності = tight coupling = technical debt

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Singleton гарантує єдиний екземпляр класу через приватний конструктор + статичний метод доступу
  • Enum Singleton — найбезпечніший: захист від reflection і серіалізації “з коробки”
  • Bill Pugh (Static Holder) — ледача ініціалізація без synchronized, через static inner class
  • У Spring @Component = Singleton scope за замовчуванням, НЕ пишіть Singleton вручну
  • Singleton не працює у кластері — кожна нода створює свій екземпляр
  • Без volatile у DCL можливий reordering: потік отримає неініціалізований об’єкт
  • Singleton вважається антипатерном: порушує SRP, DIP, тестованість

Часті уточнювальні запитання:

  • Як “зламати” Singleton? — Reflection (створити новий екземпляр), серіалізація (десеріалізація створює новий об’єкт), ClassLoader (кілька завантажувачів)
  • Чому Enum кращий за звичайний Singleton? — JVM забороняє reflectively створювати enum, серіалізація коректна
  • Коли Singleton допустимий? — Логери, конфігурація, кеші — stateless або іммутабельні об’єкти
  • Що таке Bill Pugh Singleton? — Ледача ініціалізація через static inner class, JVM гарантує потокобезпечність

Червоні прапорці (НЕ говорити):

  • “Я використовую Singleton для зберігання стану в Spring” — Spring керує цим через DI
  • “Singleton працює у кластері” — кожна нода створює свій екземпляр
  • “Мій Singleton потокобезпечний без volatile” — DCL без volatile = undefined behavior
  • “Singleton — це найкращий патерн” — це один із найбільш критикованих патернів

Пов’язані теми:

  • [[4. Як реалізувати потокобезпечний Singleton]] — способи реалізації
  • [[5. Що таке double-checked locking]] — оптимізація для Singleton
  • [[6. Які проблеми є у Singleton]] — антипатерни і проблеми
  • [[2. Які категорії патернів існують]] — Singleton як Creational патерн
  • [[16. Які антипатерни ви знаєте]] — Singleton як джерело проблем