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

Чому незмінні об'єкти є потокобезпечними?

Незмінні об'єкти потокобезпечні, тому що race condition потребує як мінімум одного запису та одного читання. Якщо записів немає після створення — race condition фізично неможливий.

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

Junior Level

Незмінні об’єкти потокобезпечні, тому що race condition потребує як мінімум одного запису та одного читання. Якщо записів немає після створення — race condition фізично неможливий.

public final class Config {
    private final String url;
    private final int timeout;

    public Config(String url, int timeout) {
        this.url = url;
        this.timeout = timeout;
    }

    public String getUrl() { return url; }
    public int getTimeout() { return timeout; }
    // Немає сетерів — ніхто не може змінити стан
}

Цей об’єкт можна безпечно передавати між потоками — не потрібні synchronized або Lock.

Важливо: незмінність ≠ thread-safe для посилань

Незмінний об’єкт безпечний для читання з багатьох потоків. Але якщо його поле посилається на змінний об’єкт — той об’єкт потрібно захищати окремо.


Middle Level

Відсутність Race Condition

Race Condition виникає, коли потоки одночасно читають і записують одні дані. Незмінний об’єкт не дозволяє писати — конфлікти на запис фізично неможливі.

Гарантії Java Memory Model для final полів

Навіть якщо об’єкт не змінюється, існує ризик побачити його в частково ініціалізованому стані. Ключове слово final запобігає цьому:

  • В кінці конструктора відбувається “заморозка” (freeze) усіх final полів
  • Будь-який потік, що отримав посилання на об’єкт після завершення конструктора, побачить усі final поля ініціалізованими
  • Процесор і компілятор не можуть переупорядкувати інструкції так, щоб посилання стало доступним раніше встановлення final полів

Копіювання при “зміні”

public final class Counter {
    private final int value;

    public Counter(int value) { this.value = value; }

    public Counter increment() {
        return new Counter(this.value + 1); // новий об'єкт
    }
}

Senior Level

Семантика Freeze та Memory Barriers

Специфікація JLS (Java Language Specification) §17.5 гарантує: після завершення конструктора усі final поля可见 любому потоку. Це забезпечується memory barrier в кінці конструктора, який:

  1. Забороняє reorder інструкцій за межі бар’єру
  2. Гарантує видимість усіх final полів
  3. Створює safe publication без volatile або синхронізації

Об’єкт vs Посилання

Важливо розрізняти незмінність самого об’єкта та незмінність посилання на нього:

public class Service {
    private ImmutableObject data = new ImmutableObject("v1"); // Посилання змінне!

    public void update(String val) {
        this.data = new ImmutableObject(val); // Не потокобезпечно без volatile/final
    }
}

Сам ImmutableObject потокобезпечний, але посилання data — ні. Інші потоки можуть бачити старе посилання зі свого кешу.

Патерни використання

  • Copy-On-WriteCopyOnWriteArrayList покладається на незмінність масивів
  • Value Objects в DDD — незмінні об’єкти для передачі даних між шарами
  • Actors / Event Sourcing — кожна подія незмінна, стан перераховується

Резюме для Senior

  • Потокобезпечність = відсутність мутації + гарантії JMM для final полів
  • Незмінність позбавляє від накладних витрат на синхронізацію — lock-free доступ усуває contention між потоками, який при високій конкуренції може знизити throughput на 50-90%.
  • Завжди використовуйте final для публікації незмінних об’єктів
  • Для змінних посилань на незмінні об’єкти використовуйте volatile або AtomicReference

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

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

  • Race condition неможливий — немає запису після створення об’єкта
  • final поля забезпечують safe publication — JLS §17.5, freeze в кінці конструктора
  • Memory barrier в кінці конструктора забороняє reorder інструкцій
  • Незмінність ≠ thread-safe для посилань: посилання на об’єкт може бути змінним
  • Copy-On-Write патерн — CopyOnWriteArrayList покладається на незмінність масивів
  • Для змінних посилань: volatile або AtomicReference

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

  • Чому не потрібен synchronized? — Немає запису = немає конкуренції за дані
  • Що таке freeze в JMM? — Бар’єр пам’яті в кінці конструктора, що гарантує видимість final полів
  • Чи може потік побачити частково створений об’єкт? — Без final полів — так; з final — ні (safe publication)
  • Що якщо посилання на незмінний об’єкт змінне? — Потрібен volatile або AtomicReference для безпечної заміни

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

  • «Усі об’єкти без сетерів потокобезпечні» — змінні поля всередині все ще небезпечні
  • «synchronized робить об’єкт незмінним» — це про синхронізацію, а не про незмінність
  • «final поля можна змінювати через reflection» — в Java 16+ це заблоковано для java.base
  • «Незмінний об’єкт = потокобезпечне посилання» — саме посилання може бути змінним

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

  • [[1. Що таке незмінний (ім’ютабельний) об’єкт]]
  • [[2. Які переваги дає використання незмінних об’єктів]]
  • [[7. Що таке ключове слово final і як воно допомагає у створенні незмінних класів]]
  • [[23. Як ім’ютабельність впливає на продуктивність]]