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

Які проблеми є у Singleton?

Проблеми Singleton поділяються на три категорії: тестованість (неможливо замокати), архітектура (приховані залежності), інфраструктура (проблеми з масштабуванням).

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

🟢 Junior Level

Проблеми Singleton поділяються на три категорії: тестованість (неможливо замокати), архітектура (приховані залежності), інфраструктура (проблеми з масштабуванням).

  1. Важко тестувати — не можна підмінити на мок
  2. Приховані залежності — не видно, що клас використовує Singleton
  3. Глобальний стан — тести впливають один на одного
  4. Не масштабується — у кластері буде по Singleton на кожну ноду

Приклад проблеми:

// ❌ Singleton заважає тестам
public class OrderService {
    public void create() {
        Database.getInstance().save(...);  // Реальна БД!
    }
}

@Test
void testOrder() {
    // Неможливо підмінити Database на тестову!
    // Тест йде у реальну БД
}

Рішення: Замість ручного Singleton використовуйте DI-контейнер (Spring), який керує єдиністю екземпляра за вас. Spring @Component + singleton scope = той самий Singleton, але тестований.


🟡 Middle Level

1. Проблеми з тестуванням

Flaky Tests — тести, що випадково проходять/падають без зміни коду.

// ❌ Singleton = Flaky Tests
@Test
void test1() {
    Counter.getInstance().increment();
    assertEquals(1, Counter.getInstance().getCount());
}

@Test
void test2() {
    // Лічильник вже = 1 із test1!
    assertEquals(1, Counter.getInstance().getCount()); // FAIL!
}

// ✅ Рішення: DI
public class Counter {
    private int count = 0;
    public void increment() { count++; }
    public int getCount() { return count; }
}

@Test
void test1() {
    Counter counter = new Counter();  // Новий для кожного теста
    counter.increment();
    assertEquals(1, counter.getCount());
}

2. Tight Coupling

// ❌ Залежність прихована
public class OrderService {
    public void process() {
        Logger.getInstance().log("...");       // Прихована залежність
        Config.getInstance().getTimeout();     // Ще одна
        Cache.getInstance().get("key");        // І ще
    }
}
// Дивлячись на конструктор — не зрозуміло, що потрібно для роботи!

// ✅ Явні залежності через конструктор
public class OrderService {
    private final Logger logger;
    private final Config config;
    private final Cache cache;

    public OrderService(Logger logger, Config config, Cache cache) {
        this.logger = logger;
        this.config = config;
        this.cache = cache;
    }
}
// Одразу видно, що потрібно для роботи

3. Конкуренція за блокування

// ❌ Singleton зі станом = bottleneck
public class SessionManager {
    private final Map<String, Session> sessions = new HashMap<>();

    public synchronized void addSession(String id, Session session) {
        sessions.put(id, session);  // Усі потоки чекають!
    }
}

// При 1000 запитів/сек → черга до synchronized методу

4. Memory Leaks

Metaspace — область пам’яті JVM для метаданих класів.

// Singleton живе вічно
public class DataLoader {
    private static DataLoader instance;
    private List<Data> cache = new ArrayList<>();  // Росте!

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

// У Tomcat при redeploy:
// Старий ClassLoader не GC → Metaspace Leak
// → OutOfMemoryError: Metaspace

5. Не працює у кластері

Node 1: Singleton → counter = 1
Node 2: Singleton → counter = 1
Node 3: Singleton → counter = 1

// Думали: один лічильник на всі ноди
// Реальність: по лічильнику на кожну ноду!

Рішення: Dependency Injection

// Spring керує життєвим циклом
@Component
public class UserService {
    private final UserRepository repo;

    public UserService(UserRepository repo) {
        this.repo = repo;  // Явна залежність
    }
}

// У тестах легко підмінити
@Test
void test() {
    UserRepository mockRepo = mock(UserRepository.class);
    UserService service = new UserService(mockRepo);
    // Тестуємо ізольовано
}

🔴 Senior Level

Архітектурна деградація

Порушення SOLID:

Принцип Як Singleton порушує
Single Responsibility Клас керує і логікою, і своїм lifecycle
Open/Closed Неможливо розширити без зміни
Liskov Substitution Не можна підмінити нащадком
Interface Segregation Залежний від конкретної реалізації
Dependency Inversion Залежний від конкретики, не від абстракції

Static Initialization Deadlocks

// ❌ Взаємна залежність у статичних ініціалізаторах
public class A {
    static { B.getInstance(); }  // Чекає B
    private static A instance = new A();
    public static A getInstance() { return instance; }
}

public class B {
    static { A.getInstance(); }  // Чекає A
    private static B instance = new B();
    public static B getInstance() { return instance; }
}

// Thread 1: завантажує A → чекає B
// Thread 2: завантажує B → чекає A
// → DEADLOCK на етапі Class Loading!
// → Дуже важко виявити

Горизонтальне масштабування

// Singleton марний для розподілених ID
public class IdGenerator {
    private static long counter = 0;
    public static synchronized long nextId() { return ++counter; }
}

// У кластері з 5 нод:
// Node 1: 1, 2, 3
// Node 2: 1, 2, 3  ← Дублі!
// Node 3: 1, 2, 3

// ✅ Рішення: розподілений генератор
// Redis INCR, Snowflake ID, UUID

Lifecycle Management

// ❌ Singleton не має shutdown()
public class ConnectionPool {
    private static ConnectionPool instance;
    private List<Connection> connections;

    private ConnectionPool() {
        connections = createConnections();  // Відкрили 10 з'єднань
    }

    // Як закрити при зупинці додатка?
    // Немає стандартного методу!
}

// ✅ Spring: @PreDestroy
@Component
public class ConnectionPool {
    @PreDestroy
    public void shutdown() {
        connections.forEach(Connection::close);
    }
}

Thread Contention у Highload

// ❌ Mutable Singleton = bottleneck
public class MetricsCollector {
    private static MetricsCollector instance;
    private final Map<String, Long> metrics = new ConcurrentHashMap<>();

    public void increment(String name) {
        metrics.merge(name, 1L, Long::sum);  // ConcurrentHashMap, але...
    }

    public Map<String, Long> getMetrics() {
        return new HashMap<>(metrics);  // ...copy кожен раз!
    }
}

// При 10 000 req/sec:
// ConcurrentHashMap знижує contention, але copy у getMetrics() створює 10 000 об'єктів/сек → GC pressure.

// ✅ Рішення: ThreadLocal або розподілені метрики

ClassLoader Memory Leaks Deep Dive

Tomcat redeploy:
1. Старий webapp ClassLoader має бути GC
2. Але Singleton.class має static поле → посилання
3. ClassLoader не може бути GC
4. Усі класи webapp залишаються у пам'яті
5. Metaspace росте → OutOfMemoryError

// Рішення:
// 1. НЕ використовувати static Singletons у webapp
// 2. Використовувати DI контейнер (Spring)
// 3. Spring сам очищає при shutdown

Production Experience

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

  • Rate Limiter як Singleton
  • 5 нод у кластері
  • Очікували: 100 req/min на всі ноди
  • Реальність: 100 req/min НА КОЖНУ ноду (500 total!)
  • Рішення: Redis-based rate limiter

Реальний сценарій #2: Static deadlock у production

  • Два Singleton ініціалізували один одного
  • Deadlock при старті → додаток завис
  • 4 години дебагу → знайшли через thread dump
  • Рішення: прибрали циклічну залежність

Реальний сценарій #3: Memory Leak

  • Metaspace ріс на 50 МБ при кожному deploy
  • Через 10 deploy → OutOfMemoryError
  • Причина: Singleton тримав ClassLoader
  • Рішення: мігрували на Spring DI

Best Practices

  1. НЕ використовуйте ручні Singleton у Spring-додатках
  2. DI Container керує lifecycle краще
  3. Уникайте мутабельного стану у Singleton
  4. Для кластера → розподілені рішення (Redis, ZK)
  5. Моніторте Metaspace при використанні static

ZK (Zookeeper) — розподілена система координації.

  1. @PreDestroy для коректного shutdown
  2. ThreadLocal для зниження contention
  3. Явні залежності через конструктор

Резюме для Senior

  • SOLID violations: Singleton порушує 5 з 6 принципів
  • Static deadlocks: важко виявити, катастрофічно
  • ClassLoader leaks: Metaspace OOM при redeploy
  • Cluster myth: Singleton ≠ distributed uniqueness
  • Thread contention: mutable state = bottleneck
  • Lifecycle: немає стандартного shutdown механізму
  • DI Container вирішує ВСІ ці проблеми
  • Правило: Singleton тільки для stateless утиліт або SDK

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

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

  • Singleton порушує SOLID: SRP (керування lifecycle), DIP (приховані залежності), OCP (неможливо розширити), LSP (не можна підмінити), ISP (залежність від конкретики)
  • Неможливо тестувати: не можна замокати, стан між тестами → flaky tests
  • Static initialization deadlocks — два Singleton ініціалізують один одного → deadlock при старті
  • ClassLoader memory leaks: у Tomcat при redeploy Singleton заважає GC → Metaspace OOM
  • У кластері Singleton марний — кожна нода створює свій екземпляр
  • Мутабельний стан у Singleton = thread contention bottleneck
  • DI Container вирішує ВСІ ці проблеми

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

  • Які принципи SOLID порушує Singleton? — SRP (подвійна відповідальність), DIP (приховані залежності), OCP (не можна розширити), LSP (не можна підмінити)
  • Що таке static initialization deadlock? — Два класи у статичних блоках чекають один одного → deadlock на етапі Class Loading
  • Чому Singleton тече у Tomcat? — Static поле тримає ClassLoader → ClassLoader не GC → Metaspace росте
  • Як вирішити проблему тестованості? — DI: передача залежностей через конструктор замість Singleton.getInstance()

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

  • “Singleton чудово тестується” — неможливо замокати, стан між тестами
  • “Я не використовую DI, Singleton простіший” — приховані залежності = technical debt
  • “У кластері Singleton вирішує проблему координації” — кожна нода створює свій екземпляр
  • “Static deadlocks неможливі” — можливі при циклічній залежності статичних ініціалізаторів

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

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