Чому незмінні об'єкти є потокобезпечними?
Незмінні об'єкти потокобезпечні, тому що race condition потребує як мінімум одного запису та одного читання. Якщо записів немає після створення — race condition фізично неможливий.
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 в кінці конструктора, який:
- Забороняє reorder інструкцій за межі бар’єру
- Гарантує видимість усіх
finalполів - Створює 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-Write —
CopyOnWriteArrayListпокладається на незмінність масивів - 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. Як ім’ютабельність впливає на продуктивність]]