Как реализовать неблокирующий доступ к критической секции в Java
Что проверяют
Понимаете ли вы разницу между «без synchronized» и «без блокировок» в строгом смысле и знаете ли, на чём держится lock-free код в Java. Следом почти всегда просят написать CAS-цикл руками или объяснить, что такое проблема ABA.
Как отвечать
Начните с определения: неблокирующий доступ значит, что поток не засыпает в ожидании монитора, а при конфликте повторяет попытку. Основа всего это операция compare-and-swap (CAS), которую процессор выполняет атомарно.
- атомики:
AtomicInteger,AtomicLong,AtomicReferenceдаютincrementAndGet(),compareAndSet(),updateAndGet(). Секцию над одним значением заменяет CAS-цикл: прочитали старое, посчитали новое,compareAndSet(old, next), при неудаче повторили; - несколько полей сразу: собрать их в неизменяемый объект и менять ссылку через
AtomicReference, тогда состояние переключается одним CAS. От ABA защищаетAtomicStampedReference; LongAdder: счётчик под сильной конкуренцией, раскладывает инкременты по ячейкам и выигрывает уAtomicLong, когда пишут много потоков;VarHandle: CAS над обычным полем без обёртки-атомика;- готовые структуры:
ConcurrentLinkedQueue,ConcurrentSkipListMap,ConcurrentHashMap(чтение без блокировок, запись блокирует только одну корзину); tryLock(): уReentrantLockне ждёт, а сразу возвращаетfalse, если секция занята. Это не lock-free, но поток не засыпает.
Хорошо упомянуть StampedLock.tryOptimisticRead(): читатель не берёт блокировку, а после чтения проверяет штамп через validate(). Запись там при этом эксклюзивная и блокирующая.
Где спотыкаются
Называют StampedLock или ReadWriteLock неблокирующими. Оптимистичное чтение действительно идёт без блокировки, но писатель ждёт, а при неудачной валидации читатель обычно переходит на readLock().
Делают get() и set() атомика двумя отдельными шагами и считают код потокобезопасным. Между чтением и записью влезает другой поток, нужен именно compareAndSet или updateAndGet.
Кладут в лямбду updateAndGet побочные эффекты. При конфликте функция выполнится повторно, и запись в лог или сетевой вызов случатся дважды.