Многопоточность: вопросы с ответами

80 разобранных вопросов по теме «Многопоточность». Каждый — с правильным ответом и пояснением.

  • 4 условия возникновения deadlock?

    (1) Mutual exclusion — ресурс не разделяемый. (2) Hold and wait — поток держит один ресурс, ждёт другой. (3) No preemption — отнять ресурс нельзя. (4) Circular wait — цикл ожидания. Чтобы deadlock был — нужны все четыре одновременно, поэтому для предотвращения достаточно нарушить любое одно из них.

  • Что такое Atomic-классы?

    AtomicInteger, AtomicLong, AtomicReference. CAS (Compare-And-Swap) — атомарная инструкция CPU. Lock-free. Методы: get, set, compareAndSet, incrementAndGet. Для счётчиков с высоким contention — LongAdder (лучше масштабируется).

  • Atomic-классы — как работают без блокировок?

    Через CAS (compare-and-swap) — атомарную инструкцию CPU. Lock-free. Классы: AtomicInteger, AtomicLong, AtomicReference.

  • Что нужно знать про CompletableFuture?

    thenApply: T → R. thenCompose: T → CF<R> (flatMap). thenCombine: объединение двух CF. exceptionally: обработка ошибки. Async-варианты выполняются в ForkJoinPool.commonPool() или указанном Executor.

  • В чём разница: CompletableFuture: thenApply vs thenCompose vs thenCombine?

    thenApply — преобразование результата. thenCompose — flatMap для CompletableFuture (нужен, когда лямбда возвращает CompletableFuture). thenCombine — объединение двух.

  • В чём разница: CountDownLatch vs CyclicBarrier vs Semaphore?

    CountDownLatch: одноразовый счётчик, потоки ждут обнуления. CyclicBarrier: переиспользуемый, все потоки ждут друг друга. Semaphore: ограничение числа одновременных потоков (пул ресурсов).

  • Что такое Deadlock?

    Два+ потока ждут друг друга. Условия Коффмана: mutual exclusion, hold and wait, no preemption, circular wait. Избегать: упорядочить захват мониторов (lock A → lock B), tryLock с таймаутом. Обнаружение: jstack, VisualVM.

  • Deadlock, livelock, starvation?

    Deadlock — два потока ждут друг друга. Livelock — активны, но не прогрессируют (оба уступают). Starvation — поток не получает CPU/ресурс.

  • Deadlock, livelock, starvation — в чём разница?

    Deadlock: два потока ждут друг друга. Livelock: потоки активны, но не прогрессируют. Starvation: поток не получает ресурс.

  • Deadlock — как возникает?

    Два+ потока ждут друг друга. Условия Коффмана. Избежать: упорядочить захват мониторов, tryLock с таймаутом. Обнаружение: jstack, VisualVM, Thread.getState().

  • Что такое happens-before?

    Ключевое отношение JMM: гарантия видимости и упорядочивания эффектов (JVM вправе переупорядочивать, пока наблюдаемый порядок соблюдён). Если A happens-before B, то записи в A (в том числе все обычные записи до volatile-записи) видны в B. Примеры: unlock → lock того же монитора; volatile write → read; Thread.start() → первая инструкция потока; последняя инструкция → Thread.join().

  • happens-before: какие пары существуют?

    volatile write → read. synchronized release → acquire того же монитора. Thread.start() → первая инструкция потока. Последняя инструкция потока → возврат из Thread.join(). final-поля видны после конструктора, если this не утёк наружу.

  • Race condition: как воспроизвести и как бороться?

    Check-then-act без атомарности: if (map.containsKey(k)) map.get(k). Между проверкой и получением другой поток может изменить состояние. Решение: единая атомарная операция computeIfAbsent. Воспроизвести: запустить много потоков на общей мапе (гонка проявляется даже на одном ядре из-за вытеснения планировщиком).

  • Structured Concurrency — что это?

    StructuredTaskScope (preview в Java 21): управление временем жизни задач. Все подзадачи привязаны к scope — при закрытии scope все незавершённые отменяются. ShutdownOnFailure: при первой ошибке все остальные отменяются. Подзадачи выполняются параллельно.

  • Что такое synchronized?

    Захватывает монитор объекта. Instance-метод — монитор this. Static — монитор Class. Блок — указанный объект. Только один поток. Reentrant: поток может повторно захватить свой монитор (счётчик). Даёт happens-before, поэтому отдельный volatile не нужен.

  • synchronized на двух методах одного объекта — будут ли работать параллельно?

    Нет. Оба используют один монитор — this. Пока один поток в methodA(), другой не может войти в methodB(). Решение: synchronized(privateLockA) и synchronized(privateLockB) — разные мониторы.

  • ThreadLocal — где пригодится и где опасен?

    Для контекста (например, SecurityContext, транзакционный контекст). Опасен с пулами потоков — значение протекает между задачами, нужен remove() в finally.

  • ThreadLocal — где пригождается и где опасен?

    Удобен для контекста (например, SecurityContext). Опасен при использовании с пулами потоков — значение «протекает» между задачами. Обязательно remove() в finally.

  • ThreadLocal — опасность?

    С пулами потоков: поток переиспользуется, ThreadLocal остаётся от предыдущей задачи (утечка данных/памяти). Обязательно: try { ... } finally { threadLocal.remove(); }. В Reactor/WebFlux — не работает, нужен Context.

  • Что такое ThreadPoolExecutor?

    corePoolSize, maxPoolSize, keepAliveTime, workQueue, threadFactory, rejectedExecutionHandler. newCachedThreadPool опасен — OOM.

  • ThreadPoolExecutor — параметры?

    corePoolSize, maxPoolSize, keepAliveTime, workQueue (LinkedBlockingQueue/ArrayBlockingQueue/SynchronousQueue), threadFactory, rejectedExecutionHandler (AbortPolicy/CallerRunsPolicy/DiscardPolicy/DiscardOldestPolicy).

  • Virtual Threads (Java 21)?

    Лёгкие потоки, управляемые JVM (не ОС). Thread.ofVirtual().start(). Миллионы потоков без проблем. НЕ подходят для CPU-bound задач (нет preemption). Идеальны для I/O-bound (сетевые запросы, БД). Executors.newVirtualThreadPerTaskExecutor().

  • Virtual Threads (Java 21): pinning problem?

    Virtual Thread на synchronized блоке → pinning (привязка к platform thread, теряется лёгкость). Причина: synchronized работает через монитор объекта, JVM не может «припарковать» виртуальный поток, и carrier простаивает. Решение — заменить synchronized на ReentrantLock.

  • Что такое Virtual Threads и synchronized?

    Virtual Thread на synchronized → pinning (привязка к platform thread). Решение: ReentrantLock вместо synchronized. Это частый вопрос для Java 21.

  • Virtual Threads — чем отличаются от обычных?

    Управляются JVM, а не ОС. Лёгкие (несколько KB против ~1 MB у platform). Можно писать блокирующий код, который масштабируется как асинхронный.

  • Что такое volatile?

    Видимость между потоками (запрет кэширования в регистрах/L1-кэше) + запрет reordering вокруг volatile. НЕ гарантирует атомарность i++ (read-increment-write = три операции). Для атомарных — AtomicInteger.

  • volatile: что гарантирует и почему недостаточно для i++?

    Видимость (запрет кэширования), happens-before (volatile write → read), запрет reordering. i++ = read+increment+write — три операции. Между ними другой поток может прочитать старое значение. Решения: AtomicInteger (CAS) или synchronized.

  • wait / notify / notifyAll — правила?

    Только внутри synchronized на том же объекте. wait() освобождает монитор и засыпает. notify будит один, notifyAll — все.

  • wait / notify / notifyAll — правила использования?

    Вызывать только внутри synchronized на том же объекте. wait() освобождает монитор и засыпает. notify будит один поток, notifyAll — все.

  • wait/notify — почему while?

    Spurious wakeup: JVM/ОС может разбудить поток без notify. while (!condition) { wait(); }. Важно: вызывать ТОЛЬКО внутри synchronized на том же объекте, иначе — IllegalMonitorStateException. notify() будит один поток, а при notifyAll просыпаются несколько — условие нужно перепроверить, поэтому if недостаточно, только while.

  • В чём разница start() и run() у Thread?

    start() — создаёт новый поток ОС и в нём вызывает run(). run() напрямую — это обычный метод, выполнится В ТЕКУЩЕМ потоке. Это любимая ловушка на собесах. Запомни: «start запускает новый, run просто исполняет в этом же».

  • В чём разница процесса и потока?

    Процесс — изолированный экземпляр программы: у каждого своё адресное пространство, файловые дескрипторы, переменные окружения, и процессы не могут просто так читать память друг друга. Потоки же внутри одного процесса разделяют его память и ресурсы, имея лишь свой стек и регистры, поэтому они легковеснее процессов.

  • Что такое Виды пулов в Executors?

    newFixedThreadPool, newCachedThreadPool, newSingleThreadExecutor, newScheduledThreadPool. В проде часто создают ThreadPoolExecutor вручную, чтобы контролировать очередь и политику отказа.

  • Что такое Виды пулов потоков в Executors?

    newFixedThreadPool, newCachedThreadPool, newSingleThreadExecutor, newScheduledThreadPool, newWorkStealingPool. Для прода обычно создают ThreadPoolExecutor вручную, чтобы контролировать очередь.

  • Где можно вызывать wait и notify?

    Только внутри synchronized-блока на том же объекте (иначе IllegalMonitorStateException). wait() отпускает монитор, поток засыпает. notify() / notifyAll() — будит. После notify поток должен снова получить монитор, чтобы продолжить.

  • Для чего нужен ThreadLocal?

    Хранение значения, уникального для каждого потока. Например, traceId, MDC для логов, контекст транзакции. В рамках одного потока — данные доступны без передачи через параметры. В пулах нужно вызывать remove(), иначе возможны утечки.

  • Если в обработчике (сервлете) сделать нестатическое поле — какие проблемы возникнут?

    Сервлет один на JVM (по умолчанию), обрабатывает много запросов параллельно разными потоками. Нестатическое поле — общее для всех запросов. Получится shared mutable state без синхронизации = race condition, гонка данных.

  • Что такое Жизненный цикл потока?

    NEW → RUNNABLE → (BLOCKED / WAITING / TIMED_WAITING) → TERMINATED.

  • Зачем volatile?

    Гарантирует видимость изменений между потоками (запрет кэширования в регистрах/локальном кэше CPU) и запрет reordering. НЕ гарантирует атомарность составных операций (i++).

  • Зачем нужен volatile?

    Гарантирует видимость изменений переменной между потоками (без кэширования в регистрах). НЕ гарантирует атомарность операций типа i++.

  • Зачем нужны пулы потоков?

    Создание потока — дорогая операция (~1 МБ памяти + системный вызов). Пул переиспользует потоки. Также даёт контроль над количеством (не создаст тысячу) и очередь задач.

  • Как java.util.Random становится thread-safe?

    java.util.Random формально thread-safe — это написано в Javadoc. Реализация потокобезопасности — через атомарный CAS на одном поле AtomicLong seed: метод next() в цикле читает старое значение и через compareAndSet атомарно записывает новое, повторяя попытку при конфликте. Это неблокирующий подход без synchronized.

  • Как избежать deadlock?

    Самое простое — захватывать ресурсы в едином порядке (например, по ID). Тогда циклический wait невозможен. Альтернатива — tryLock с таймаутом (ReentrantLock).

  • Как исправить?

    Два рабочих варианта. Первый — создать свой ForkJoinPool под конкретную задачу и передать параллельный стрим внутрь его submit(...).get(), чтобы тяжёлая/блокирующая работа не занимала общий commonPool. Второй — вообще не использовать parallelStream для блокирующих операций.

  • Как работает synchronized?

    Захватывает монитор объекта. На методе — монитор this (на static — Class-объект). На блоке — указанного объекта. Только один поток одновременно.

  • Как работает synchronized на методе?

    Для обычного метода блокировка идёт на this (на сам объект). Для static-метода — на Class-объект (ClassName.class). На уровне байткода — это инструкции monitorenter / monitorexit. Если уже владеешь монитором, повторный вход не блокирует: мониторы в Java реентерабельны, поэтому deadlock не возникает.

  • Как устроен ThreadLocalRandom?

    ThreadLocalRandom использует отдельный seed для каждого потока — хранится в полях класса Thread (это специальная JVM-оптимизация, не обычный ThreadLocal). Padding-поля снижают false sharing, обновление seed идёт без CAS и без синхронизации, так как разделяемого состояния нет.

  • Какие способы создать поток в Java?

    (1) Унаследоваться от Thread и переопределить run(). Не лучший способ — лимит одного наследования. (2) Реализовать Runnable и передать в Thread: new Thread(() -> {...}).start(). Запускать поток нужно через start(), а не run() — вызов run() напрямую исполнит код в текущем потоке.

  • Какие типы пулов есть в Executors?

    newFixedThreadPool(n) — фиксированный размер с неограниченной очередью. newCachedThreadPool() — растёт по необходимости, потоки умирают через 60 сек простоя. newSingleThreadExecutor() — один поток, гарантирующий последовательное выполнение задач.

  • Когда использовать что — Random, SecureRandom, ThreadLocalRandom?

    Random — для однопоточного кода с обычной случайностью (тесты, моки, простой код). ThreadLocalRandom — для многопоточного кода с обычной случайностью (concurrent workloads, parallel stream), без contention на общий seed. SecureRandom — для криптографии (токены, пароли, соль), но он медленнее.

  • Когда что выбирать — свой ForkJoinPool или virtual threads?

    ForkJoinPool — для CPU-bound задач: математика, парсинг, трансформации без IO. Размер пула = число ядер. Wow-эффекта не даст, но не блокирует common pool. Virtual threads — для IO-bound: блокирующий JDBC, HTTP-вызовы, где поток при блокировке дёшево отпускает несущий поток.

  • Почему i++ не атомарен?

    Это три операции: прочитать значение → прибавить 1 → записать обратно. Два потока могут одновременно прочитать одно значение, оба прибавить, оба записать — будет +1 вместо +2 (lost update). Атомарность даёт AtomicInteger или synchronized; volatile обеспечивает только видимость.

  • Почему .parallel() — проблема?

    parallel() и parallelStream() под капотом используют ForkJoinPool.commonPool() — один общий пул на всё JVM-приложение с размером по числу ядер. Поэтому одна блокирующая или тяжёлая задача душит все остальные параллельные стримы, деля с ними этот единственный пул.

  • Почему wait() нужно проверять в while, а не if?

    Из-за spurious wakeup — поток может проснуться сам по себе. После пробуждения условие может быть уже невалидным.

  • Почему wait() проверяют в while, а не if?

    Spurious wakeup — поток может проснуться сам по себе. После пробуждения условие может быть уже невалидным, нужна повторная проверка.

  • Почему опасно использовать Executors.newCachedThreadPool() в проде?

    Unbounded очередь и неограниченное количество потоков — при всплеске нагрузки можно положить JVM OutOfMemoryError.

  • Что такое Пулы потоков?

    newFixedThreadPool(n) — фикс. число потоков, неограниченная LinkedBlockingQueue (при наплыве задачи копятся в очереди). newCachedThreadPool — неограниченно плодит потоки (ОПАСНО — OOM). newScheduledThreadPool — периодические задачи через scheduleAtFixedRate. В проде: ThreadPoolExecutor вручную с corePoolSize, задающим базовое число потоков.

  • Что такое Разница между synchronized-методом и synchronized-блоком?

    Метод захватывает this (для static — Class-объект). Блок — любой указанный объект. Блок обычно эффективнее.

  • Что такое Реализуй ограниченную BlockingQueue?

    synchronized + wait/notify. while(!condition) wait() — не if! (spurious wakeups). put() ждёт если полная, take() ждёт если пустая. notifyAll() после каждой операции.

  • Что такое Способы создать поток?

    extends Thread, implements Runnable/Callable, через ExecutorService, CompletableFuture. Предпочтительнее — пулы потоков через Executors.

  • Что такое Способы создать поток в Java?

    extends Thread, implements Runnable, через Callable + ExecutorService, через CompletableFuture. На Java 21+ — Virtual Threads.

  • Чем Atomic лучше volatile?

    AtomicInteger / AtomicLong / AtomicReference дают и видимость, и атомарность через CAS (compare-and-swap). CAS — операция процессора «прочитать, проверить, заменить» одной инструкцией без блокировок. compareAndSet выполняет составную операцию атомарно, чего volatile (только видимость) не умеет для i++.

  • Чем Runnable отличается от Callable?

    Runnable — run() возвращает void, не может бросать checked-исключения. Подходит для «просто запусти». Callable<V> — call() возвращает V, может бросать Exception. Используется через ExecutorService.submit() и Future<V>.

  • Чем synchronized(this) отличается от synchronized(privateLock)?

    this доступен извне — любой может случайно или намеренно synchronized(obj) на том же объекте. privateLock (private final Object lock = new Object()) — полный контроль, никто извне не может захватить.

  • Чем опасен Executors.newCachedThreadPool() в проде?

    Неограниченное количество потоков — при всплеске нагрузки может положить JVM через OutOfMemoryError (unable to create new native thread).

  • Чем отличается synchronized на методе и в блоке?

    synchronized на методе — блокировка на весь метод (неявно берётся монитор this, а у статического — Class). synchronized(obj) {...} — на конкретный объект-монитор и конкретный кусок кода. Блок даёт больше контроля: блокировку можно держать только на нужный участок, выбрав произвольный объект и сузив критическую секцию.

  • Чем процесс отличается от потока?

    Процесс — изолированная единица ОС со своей памятью. Поток — единица исполнения внутри процесса, память общая.

  • Чем это грозит в проде?

    Сценарий: микросервис обрабатывает 1000 RPS обычных запросов. Каждый из них где-то использует parallelStream (агрегация, фильтр коллекции). Все parallelStream делят один общий ForkJoinPool.commonPool() размером примерно (ядра-1), поэтому задачи выстраиваются в очередь, растёт конкуренция за потоки и латентность взлетает, а блокирующие операции внутри стрима могут застопорить весь пул.

  • Что гарантирует volatile?

    Видимость записи между потоками — запись одного потока становится сразу видна другим. Запрет переупорядочивания инструкций вокруг volatile. НО volatile НЕ даёт атомарности составных операций. i++ — это read-modify-write, поэтому остаётся гонкой; для атомарного инкремента нужны AtomicInteger или synchronized.

  • Что делает Thread.sleep() и Thread.yield()?

    sleep — приостанавливает поток на указанное время (не освобождает мониторы). yield — намекает планировщику «дай поработать другим», но это лишь подсказка.

  • Что отпускает wait, а что — sleep?

    wait() отпускает монитор объекта. sleep() — НЕ отпускает, поток продолжает держать все свои блокировки, просто не выполняется.

  • Что такое ABA-проблема?

    Значение сменилось с A на B и обратно на A. CAS это не видит. Решение: AtomicStampedReference с версией.

  • Что такое deadlock?

    Взаимная блокировка двух (или более) потоков. Поток A держит ресурс X, ждёт Y. Поток B держит Y, ждёт X. Никто не сдвинется.

  • Что такое happens-before?

    Отношение в JMM, гарантирующее видимость. Примеры: synchronized release → acquire, volatile write → read, Thread.start() → действия потока, final-поля после конструктора.

  • Что такое race condition?

    Гонка — ситуация, когда результат зависит от порядка выполнения потоков. Классика: i++ из двух потоков. Может быть +1, может быть +2 — зависит от планировщика. Решение — синхронизация (synchronized, locks, atomic) или immutable.

  • Что такое synchronized?

    Ключевое слово для захвата монитора объекта. Гарантирует, что в блок одновременно войдёт только один поток. Можно на методе или на блоке кода.

  • Что такое монитор?

    У каждого Java-объекта есть неявный монитор — встроенный механизм блокировки. Только один поток в один момент может «владеть» монитором объекта. Это используется в synchronized.

  • Что такое монитор объекта? Как работает synchronized?

    У каждого объекта есть монитор. synchronized захватывает его при входе, освобождает при выходе (включая исключения).

  • На code review найден shared java.util.Random внутри parallelStream. Какой комментарий наиболее полезен?

    Random формально thread-safe, но общий seed создаёт contention; лучше ThreadLocalRandom или RandomGenerator.

  • Почему volatile не делает операцию i++ потокобезопасной?

    volatile даёт видимость и порядок вокруг volatile-доступа, но i++ остаётся последовательностью read-increment-write.

новые гайды и свежие вопросы с собесов — первыми в Telegram Смотреть гайды Подписаться