Какие способы написания потокобезопасных классов существуют
Что проверяют
Понимаете ли вы, что потокобезопасность начинается с проектирования состояния, а не с расстановки synchronized. Следом обычно просят объяснить разницу между видимостью и атомарностью или спрашивают, почему counter++ на volatile поле теряет значения.
Как отвечать
Идите от самого дешёвого способа к самому дорогому:
- нет общего состояния: класс без изменяемых полей потокобезопасен даром. Сюда же ограничение потоком, когда данные живут в локальных переменных или в
ThreadLocal; - неизменяемость: поля
final, классfinal, изменяемые коллекции копируются на входе черезList.copyOf. Поляfinalпо модели памяти Java видны другим потокам корректно инициализированными, еслиthisне утёк из конструктора; - атомарные классы:
AtomicInteger,AtomicReference,LongAdderдля счётчиков под высокой конкуренцией. Работают через CAS без блокировок; - конкурентные структуры:
ConcurrentHashMapс атомарнымиcomputeиmerge,CopyOnWriteArrayListдля частого чтения и редкой записи,BlockingQueueдля передачи задач; - блокировки:
synchronizedилиReentrantLock, когда инвариант охватывает несколько полей. УReentrantLockестьtryLockс таймаутом и прерываемое ожидание, уReadWriteLockиStampedLockраздельные режимы чтения и записи; volatile: только видимость и порядок, подходит для флага остановки или однократно публикуемой ссылки.
Хороший штрих про виртуальные потоки: в Java 21 блокировка внутри synchronized закрепляла виртуальный поток за несущим, в JDK 24 это исправили, но ReentrantLock всё равно гибче.
Где спотыкаются
Считают, что volatile делает операцию атомарной. counter++ это чтение, сложение и запись, и два потока теряют инкременты. Нужен AtomicInteger или блокировка.
Собирают класс из потокобезопасных частей и считают его потокобезопасным. Связка containsKey и put на ConcurrentHashMap всё равно гонка, правильно putIfAbsent или computeIfAbsent.
Синхронизируют запись, а читают поле без блокировки. Без общего монитора или volatile читающий поток может так и не увидеть новое значение.