Многопоточность
Потоки, синхронизация, взаимоблокировки, volatile и примитивы координации wait/notify.
13 вопросов
JuniorТеорияОчень частоЧто такое состояние гонки в Java и как его исправить?
Что такое состояние гонки в Java и как его исправить?
Состояние гонки — это когда корректность программы зависит от непредсказуемого порядка обращения двух или более потоков к разделяемому изменяемому состоянию одновременно. Классический случай: два потока читают, увеличивают и записывают один счётчик, и чередующиеся шаги теряют обновление. Лечат сериализацией доступа через synchronized, блокировку или атомарные операции.
Типичные ошибки
- ✗Считать состояние гонки проблемой производительности, а не корректности
- ✗Думать, что одноядерные машины защищены от состояний гонки
- ✗Забывать, что чтение-изменение-запись разделяемого состояния не атомарны без синхронизации
Уточняющие вопросы
- →Почему
counter++над разделяемым полем не атомарен и что из-за этого может пойти не так? - →Как атомарные классы вроде
AtomicIntegerубирают гонку без блокаsynchronized?
MiddleТеорияОчень частоЧто именно делает ключевое слово synchronized в Java?
Что именно делает ключевое слово synchronized в Java?
synchronized-метод или блок захватывает монитор объекта перед входом, поэтому только один поток одновременно может выполнять критическую секцию, защищённую тем же монитором. Это сериализует доступ к разделяемому изменяемому состоянию, предотвращая гонки данных, где чередующиеся чтения и записи портят данные. Освобождение монитора при выходе также сбрасывает изменения, чтобы следующий поток их увидел.
Типичные ошибки
- ✗Думать, что
synchronizedраспараллеливает тело, а не сериализует доступ к нему - ✗Считать, что оно блокирует всю JVM, а не монитор одного конкретного объекта
- ✗Синхронизироваться на разных объектах и ждать, что они исключают друг друга
Уточняющие вопросы
- →На каком объекте захватывает монитор
synchronizedметод экземпляра против статического? - →Как освобождение монитора устанавливает отношение happens-before для видимости?
SeniorТеорияОчень частоЧто такое взаимоблокировка в Java и как её предотвратить?
Что такое взаимоблокировка в Java и как её предотвратить?
Взаимоблокировка — это когда два или более потока удерживают каждый блокировку, нужную другому, и ждут вечно, так что никто не может продолжить. Классический случай: поток A держит блокировку 1 и хочет блокировку 2, а поток B держит блокировку 2 и хочет блокировку 1. Предотвращают её единым глобальным порядком захвата, чтобы все потоки брали блокировки в одной последовательности, через tryLock с таймаутом для отступа или удержанием меньшего числа блокировок.
Типичные ошибки
- ✗Считать deadlock проблемой скорости или приоритета, а не циклического ожидания блокировок
- ✗Думать, что случайный порядок блокировок спасает, тогда как лечит единый порядок
- ✗Игнорировать, что удержание нескольких блокировок сразу и создаёт циклическую зависимость
Уточняющие вопросы
- →Как
tryLockс таймаутом разрывает потенциальный deadlock, который обычная блокировка нет? - →Какие четыре условия Коффмана должны выполняться одновременно для возникновения deadlock?
JuniorТеорияЧастоКакими способами в Java можно создать поток, и какой предпочтительнее?
Какими способами в Java можно создать поток, и какой предпочтительнее?
Два базовых способа: наследовать Thread и переопределить run(), либо реализовать Runnable и передать его в Thread. Реализация Runnable предпочтительнее, так как оставляет свободным единственный слот наследования класса и отделяет задачу от исполнителя. Для задач, возвращающих результат или бросающих проверяемое исключение, реализуют Callable и отправляют его в ExecutorService, который возвращает Future.
Типичные ошибки
- ✗Вызывать
run()напрямую вместоstart(), из-за чего код выполняется на текущем потоке - ✗Наследовать
Threadпо умолчанию, расходуя единственный слот наследования - ✗Думать, что
Runnableможет вернуть значение, тогда как толькоCallableдаёт результат черезFuture
Уточняющие вопросы
- →Что произойдёт, если вызвать
run()напрямую вместоstart()уThread? - →Чем
Callable, возвращающийFuture, отличается отRunnable, отправленного в executor?
MiddleДебаггингЧастоИсправьте цикл, который запускает потоки и завершается
Исправьте цикл, который запускает потоки и завершается
Две ошибки. Первая: лямбда захватывает i, но переменная цикла не является effectively final, поэтому код не компилируется — скопируйте её в финальную локальную (final int n = i;) и печатайте n. Вторая: System.exit(0) немедленно завершает JVM, убивая спящие потоки до печати — уберите его (или сначала join каждый поток), чтобы программа дождалась их вывода.
Типичные ошибки
- ✗Думать, что переменная цикла
foreffectively final и её можно захватить напрямую - ✗Упускать, что
System.exit(0)убивает JVM и ещё работающие потоки - ✗Путать исправление с добавлением
volatile, которое решает видимость, а не захват или завершение
Уточняющие вопросы
- →Почему переменная цикла
forне effectively final, а переменная foreach может быть? - →Как
joinкаждого потока изменит поведение против удаленияSystem.exit?
MiddleТеорияЧастоЧто гарантирует ключевое слово volatile, а что нет?
Что гарантирует ключевое слово volatile, а что нет?
volatile гарантирует видимость: каждое чтение и запись поля идут прямо в главную память, а не в локальный кеш потока, поэтому другие потоки всегда видят последнее записанное значение, а компилятор не переупорядочивает обращения вокруг него. Оно НЕ обеспечивает атомарность составных операций вроде i++, которая является read-modify-write и которую два потока всё ещё могут чередовать, теряя обновления.
Типичные ошибки
- ✗Использовать
volatileдля счётчика, ожидая, чтоi++атомарен между потоками - ✗Путать гарантию видимости со взаимным исключением, которое даёт
synchronized - ✗Думать, что
volatileкеширует значение локально, а не заставляет идти в главную память
Уточняющие вопросы
- →Когда одного
volatileboolean-флага достаточно как корректного средства синхронизации? - →Почему
AtomicIntegerсправляется с атомарным инкрементом там, гдеvolatile intнет?
MiddleТеорияЧастоВ чём разница между методами wait() и sleep() в Java?
В чём разница между методами wait() и sleep() в Java?
wait() — это метод Object, который ОСВОБОЖДАЕТ монитор и паркует поток, пока другой поток не вызовет notify()/notifyAll() на том же объекте; его нужно вызывать, уже удерживая этот монитор, обычно внутри synchronized-блока. sleep() — это статический метод Thread, который просто приостанавливает текущий поток на заданное время и УДЕРЖИВАЕТ все блокировки, которыми он владеет.
Типичные ошибки
- ✗Вызывать
wait()внеsynchronized-блока, вызываяIllegalMonitorStateException - ✗Считать, что
sleep()освобождает удерживаемые блокировки, какwait() - ✗Забывать, что
wait()нужен внешнийnotify()для возобновления, а не только таймер
Уточняющие вопросы
- →Почему
wait()нужно вызывать внутриsynchronized-блока, удерживающего монитор объекта? - →Почему
wait()следует вызывать в цикле, перепроверяющем условие, а не вif?
SeniorТеорияЧастоВ чём разница между состоянием гонки, взаимоблокировкой и живой блокировкой?
В чём разница между состоянием гонки, взаимоблокировкой и живой блокировкой?
Состояние гонки — это ошибка времени, когда потоки чередуют доступ к разделяемому состоянию и портят его; лечат сериализацией доступа блокировками или атомарными операциями. Взаимоблокировка — это потоки, удерживающие каждый нужную другому блокировку и ждущие вечно; предотвращают единым порядком захвата или таймаутами tryLock. Живая блокировка — это потоки, постоянно реагирующие друг на друга и меняющие состояние, но не продвигающиеся; помогает случайная задержка.
Типичные ошибки
- ✗Смешивать все три — считать взаимоблокировку и живую блокировку одним и тем же застреванием
- ✗Называть живую блокировку взаимоблокировкой, хотя её потоки продолжают работать и менять состояние
- ✗Думать, что один механизм вроде
synchronizedсам по себе предотвращает все три проблемы
Уточняющие вопросы
- →Чем живая блокировка отличается от голодания потока, когда один поток никогда не планируется?
- →Почему случайная задержка разрывает живую блокировку, а детерминированный повтор нет?
JuniorТеорияИногдаВ чём разница между процессом и потоком в Java и как они связаны?
В чём разница между процессом и потоком в Java и как они связаны?
Процесс — это независимая запущенная программа со своим изолированным пространством памяти. Поток — это легковесная единица выполнения внутри процесса; все потоки одного процесса разделяют его heap и загруженные объекты, но каждый имеет свой стек. Потоки дешевле создавать, чем процессы, и они напрямую разделяют данные, что быстро, но требует синхронизации.
Типичные ошибки
- ✗Утверждать, что у потоков изолированная память как у процессов и они не могут напрямую разделять объекты
- ✗Считать, что потоки и процессы одинаково дороги в создании и переключении
- ✗Забывать, что разделяемый доступ к heap между потоками требует синхронизации для корректности
Уточняющие вопросы
- →Почему обмен данными между потоками требует синхронизации, а между процессами часто нет?
- →Какая часть потока приватна для него, а какая разделяется с соседними потоками?
MiddleТеорияИногдаЧто модель памяти Java гарантирует про видимость и порядок операций между потоками?
Что модель памяти Java гарантирует про видимость и порядок операций между потоками?
Модель памяти определяет отношение happens-before: если действие A happens-before B, всё записанное в A видно в B и не может быть переупорядочено за него. Освобождение монитора, запись volatile-поля и старт потока создают такое ребро. Без него чтения переупорядочиваются, и поток может бесконечно видеть устаревшее значение.
Типичные ошибки
- ✗Рассуждать так, будто любая запись сразу глобальна, вместо того чтобы требовать ребро happens-before
- ✗Считать
synchronizedтолько взаимным исключением, забывая, что он ещё и публикует записи - ✗Верить, что
Thread.sleepили задержка подольше рано или поздно сделают обычную запись видимой
Уточняющие вопросы
- →Какие действия, кроме записи
volatile, устанавливают ребро happens-before? - →Почему цикл по обычному boolean-флагу может крутиться вечно после того, как другой поток его выставил?
MiddleТеорияИногдаЧто ThreadLocal даёт каждому потоку и чем от него отличается InheritableThreadLocal?
Что ThreadLocal даёт каждому потоку и чем от него отличается InheritableThreadLocal?
ThreadLocal даёт каждому потоку собственную независимую копию значения, которая лежит в карте, привязанной к объекту Thread, поэтому синхронизация для чтения и записи не нужна. InheritableThreadLocal вдобавок копирует значение родителя в дочерний поток в момент его создания. Значения нужно удалять явно: поток из пула переживает задачу и держит запись живой.
Типичные ошибки
- ✗Пропускать
remove()в пуле, из-за чего следующая задача наследует устаревшее значение или запись течёт - ✗Считать, что любой стартованный поток наследует значение
ThreadLocalродителя - ✗Воспринимать
ThreadLocalкак примитив синхронизации, а не как хранилище на поток
Уточняющие вопросы
- →Почему
ThreadLocalв пуле потоков течёт, еслиremove()никогда не вызывается? - →Почему
InheritableThreadLocalне пробрасывает значение черезExecutorService?
SeniorТеорияИногдаВ чём разница между notify() и notifyAll() в Java?
В чём разница между notify() и notifyAll() в Java?
notify() будит один произвольный поток, ожидающий на мониторе объекта; выбрать какой именно нельзя. notifyAll() будит все ожидающие потоки, и они затем заново соперничают за блокировку, каждый перепроверяя своё условие перед продолжением. Предпочитайте notifyAll(), когда потоки ждут разных условий, ведь notify() может разбудить не тот поток и оставить готовый к работе поток ждать навсегда.
Типичные ошибки
- ✗Считать, что
notify()будит дольше всех ждавший поток, тогда как выбор произволен - ✗Думать, что разбуженные потоки запускаются сразу, а не соперничают за монитор заново
- ✗Использовать
notify()при разных условиях ожидания, оставляя готовый поток навсегда
Уточняющие вопросы
- →В какой ограниченной ситуации
notify()безопасен и эффективнееnotifyAll()? - →Почему разбуженный поток должен перепроверять условие в цикле после возврата из
wait()?
SeniorДебаггингИногдаОпределите, почему два рабочих потока перестали продвигаться, по этому thread dump
Определите, почему два рабочих потока перестали продвигаться, по этому thread dump
Оба потока BLOCKED на мониторе, и адреса мониторов перекрещены: worker-1 держит 0x…41180 и ждёт 0x…3f2c8, а worker-2 держит 0x…3f2c8 и ждёт 0x…41180. Это циклическое ожидание на двух мониторах Account — deadlock: transfer захватывает счета в порядке аргументов вызывающего, поэтому два встречных перевода берут их в противоположном порядке. Лечится единым глобальным порядком захвата (например, по id счёта) или tryLock с таймаутом и отступом.
Типичные ошибки
- ✗Читать
BLOCKEDкак нехватку CPU или задержку планировщика, а не ожидание монитора - ✗Игнорировать адреса мониторов, которые и доказывают цикличность ожидания
- ✗Предлагать больший пул или высокий приоритет, что лишь добавит застрявших потоков
Уточняющие вопросы
- →Какая строка
jstackподтвердила бы диагноз автоматически, без чтения кадров? - →Как переход с мониторов
AccountнаtryLockс таймаутом изменил бы этот дамп?