java.util.concurrent
Явные блокировки и реентерабельность, объекты Condition, CAS и атомики, синхронизаторы и конкурентные коллекции, включая ConcurrentHashMap.
9 вопросов
MiddleТеорияОчень частоЧто такое сравнение-с-обменом CAS и как AtomicInteger обходится с ним без блокировок?
Что такое сравнение-с-обменом CAS и как AtomicInteger обходится с ним без блокировок?
CAS — одна атомарная аппаратная инструкция: она записывает новое значение, только если в памяти всё ещё лежит ожидаемое старое, иначе терпит неудачу. AtomicInteger.incrementAndGet() читает значение, вычисляет old + 1 и повторяет CAS в цикле, пока не выиграет. Ни один поток не блокируется — проигравший просто перечитывает и пробует снова, так что нет ни блокировки, ни переключения контекста.
Типичные ошибки
- ✗Думать, что одного
volatileдостаточно, чтобыcount++стал атомарным - ✗Считать, что неудачный
CASблокирует вызывающего, а не сообщает о провале для повтора в цикле - ✗Полагать, что атомики всегда быстрее блокировки, даже при конкуренции, копящей повторы
Уточняющие вопросы
- →Почему
AtomicIntegerможет проиграть обычной блокировке при очень высокой конкуренции? - →Какой класс
Atomicподходит счётчику, в который пишут гораздо чаще, чем читают?
MiddleТеорияОчень частоЧто ReentrantLock даёт такого, чего не может synchronized?
Что ReentrantLock даёт такого, чего не может synchronized?
Оба — реентерабельные блокировки взаимного исключения, но ReentrantLock — явный объект, поэтому он добавляет то, чего блок дать не может: tryLock() с таймаутом, прерываемый захват, необязательную честную очередь и несколько объектов Condition. Плата — ручная дисциплина: unlock() обязан стоять в блоке finally, тогда как synchronized освобождается сам на выходе из блока.
Типичные ошибки
- ✗Забывать
unlock()в блокеfinally, из-за чего брошенное исключение навсегда теряет блокировку - ✗Считать, что
synchronizedподдерживает захват с таймаутом или с прерыванием - ✗Полагать, что
ReentrantLockчестен по умолчанию — он барджит, если не создан сtrue
Уточняющие вопросы
- →Что произойдёт, если исключение пропустит вызов
unlock()у захваченногоReentrantLock? - →Чего стоит честный
ReentrantLockпо сравнению с барджащим по умолчанию?
JuniorТеорияЧастоЧто такое реентерабельность блокировки и почему synchronized и ReentrantLock её поддерживают?
Что такое реентерабельность блокировки и почему synchronized и ReentrantLock её поддерживают?
Реентерабельность означает, что поток, уже владеющий блокировкой, может захватить её снова, а не блокировать сам себя. Блокировка хранит поток-владелец и счётчик захватов: каждый повторный захват увеличивает его, а каждое освобождение уменьшает, и блокировка снимается лишь когда счётчик возвращается к нулю. И synchronized, и ReentrantLock реентерабельны, поэтому один synchronized-метод может безопасно вызвать другой на том же объекте.
Типичные ошибки
- ✗Считать, что поток блокирует сам себя, повторно входя в блокировку, которой уже владеет
- ✗Забывать, что блокировка снимается лишь когда каждый захват уравновешен освобождением
- ✗Думать, что
ReentrantLockреентерабелен, а встроенные мониторыsynchronized— нет
Уточняющие вопросы
- →Что произойдёт, если поток освободит
ReentrantLockменьше раз, чем захватил? - →Почему нереентерабельная блокировка вызвала бы взаимоблокировку рекурсивного synchronized-вызова?
JuniorТеорияЧастоЧем Collections.synchronizedList() отличается от CopyOnWriteArrayList?
Чем Collections.synchronizedList() отличается от CopyOnWriteArrayList?
Collections.synchronizedList() оборачивает список так, что одна блокировка защищает каждый метод: чтения блокируют друг друга, а итерацию всё равно приходится синхронизировать вручную. CopyOnWriteArrayList копирует внутренний массив на каждой записи, поэтому чтения не берут блокировку вовсе, а каждый итератор видит неизменяемый снимок.
Типичные ошибки
- ✗Думать, что обёртка synchronized делает итерацию безопасной без внешней блокировки вокруг цикла
- ✗Выбирать
CopyOnWriteArrayListдля частых записей, где каждая запись копирует весь массив - ✗Ждать, что итератор
CopyOnWriteArrayListувидит записи, сделанные после его создания
Уточняющие вопросы
- →Почему обход
Collections.synchronizedList()всё равно требует внешней блокировки? - →При какой частоте записей
CopyOnWriteArrayListперестаёт быть разумным выбором?
MiddleТеорияЧастоКак ConcurrentHashMap избегает блокировки всей карты при записи?
Как ConcurrentHashMap избегает блокировки всей карты при записи?
Чтения не берут блокировку: таблица и её узлы объявлены volatile, поэтому get() просто проходит по корзине. Запись же блокирует лишь головной узел той единственной корзины, в которую попал хеш, поэтому писатели из разных корзин идут параллельно, а пустая корзина заполняется через compare-and-swap. Java 8 заменила фиксированный массив сегментов Java 7 на такую поэлементную блокировку.
Типичные ошибки
- ✗Думать, что
ConcurrentHashMapв Java 8 всё ещё блокирует фиксированный сегмент, а не одну корзину - ✗Считать, что
get()берёт блокировку или блокируется против одновременного писателя - ✗Полагать, что пара «проверил-положил» атомарна без
computeIfAbsentилиputIfAbsent
Уточняющие вопросы
- →Почему
computeIfAbsentатомарен, а параget()и затемput()— нет? - →Что делает
ConcurrentHashMap, когда одна корзина вырастает до дерева?
MiddleТеорияЧастоЧем объекты Condition улучшают связку wait/notify?
Чем объекты Condition улучшают связку wait/notify?
Встроенный монитор даёт объекту ровно одну очередь ожидания, поэтому производители и потребители стоят в ней вместе, и notifyAll() вынужден будить всех подряд. ReentrantLock умеет создавать несколько объектов Condition, у каждого своя очередь, поэтому notFull.signal() разбудит только производителей. В остальном await()/signal() повторяют wait()/notify() и требуют удерживать блокировку.
Типичные ошибки
- ✗Звать
await()илиsignal(), не удерживая блокировку, что бросаетIllegalMonitorStateException - ✗Ждать через
if, а не в циклеwhile, перепроверяющем предикат после пробуждения - ✗Считать, что у одного объекта уже есть несколько встроенных очередей ожидания
Уточняющие вопросы
- →Почему
await()обязан стоять внутри циклаwhile, а не подif? - →Когда
signalAll()всё ещё нужен даже при раздельных объектахCondition?
MiddleТеорияЧастоЧем CountDownLatch отличается от CyclicBarrier?
Чем CountDownLatch отличается от CyclicBarrier?
CountDownLatch одноразов и однонаправлен: ждущие стоят в await(), пока другие потоки не сведут счётчик к нулю вызовами countDown(), и сбросить его нельзя никогда. CyclicBarrier — это встреча N участников, ждущих друг друга: когда приходит последний, освобождаются все, выполняется необязательное действие барьера, и барьер сбрасывается к следующему кругу.
Типичные ошибки
- ✗Пытаться переиспользовать
CountDownLatchпосле того, как его счётчик уже дошёл до нуля - ✗Думать, что
countDown()блокирует вызывающего, тогда как блокирует толькоawait() - ✗Забывать, что
CyclicBarrierломается для всех, когда одного участника прервали или он истёк по таймауту
Уточняющие вопросы
- →Что станет с остальными участниками, если один поток на
CyclicBarrierпрервали? - →Какой синхронизатор подходит для стартового сигнала, отпускающего много потоков разом?
JuniorТеорияИногдаЧто пакет java.util.concurrent даёт сверх synchronized и wait/notify?
Что пакет java.util.concurrent даёт сверх synchronized и wait/notify?
Он заменяет самописный код на мониторе готовыми проверенными блоками: явные блокировки, атомики, конкурентные коллекции, пулы executor'ов и синхронизаторы. Вы собираете решение из примитивов, а не пишете циклы wait/notify вручную.
Типичные ошибки
- ✗Считать
java.util.concurrentлишь сахаром надsynchronizedбез новых механизмов - ✗Хвататься за сырые
wait/notify, когда подходит готовый синхронизатор или executor - ✗Думать, что пакет убирает настоящие потоки ОС, а не строится поверх них
Уточняющие вопросы
- →Какой класс из
java.util.concurrentограничит число одновременных задач? - →Почему
ExecutorServiceобычно предпочтительнее создания сырыхThread?
SeniorТеорияИногдаЧто такое проблема ABA в цикле сравнения-с-обменом CAS и как её смягчают?
Что такое проблема ABA в цикле сравнения-с-обменом CAS и как её смягчают?
Поток читает A, застревает, пока другой меняет значение на B и обратно на A, и его CAS проходит, хотя состояние, о котором он рассуждал, уже исчезло: в lock-free стеке снятый узел могут переиспользовать и переподвязать, испортив список. Лечение — версионировать ссылку: AtomicStampedReference соединяет её со счётчиком, поэтому CAS сравнивает обе части и проваливается после любого промежуточного изменения.
Типичные ошибки
- ✗Думать, что успешный
CASдоказывает, будто в промежутке ничего не менялось - ✗Считать, что
volatileили более длинный цикл повторов предотвращаетABA - ✗Полагать, что
ABAбезобиден, раз наблюдаемое значение в итоге то же самое
Уточняющие вопросы
- →Почему
ABAпочти безобиден для счётчика, но опасен для lock-free стека? - →Чем
AtomicMarkableReferenceотличается отAtomicStampedReference?