Вопросы по многопоточности в Java на собеседовании
Многопоточность в Java любят на собесах именно потому, что здесь нельзя ответить заученным определением. Спросят, что произойдёт с полем без volatile, и сразу видно, читал человек про модель памяти или запомнил слово.
Что спрашивают
- +Модель памяти: видимость и happens-before, зачем volatile, чем он не заменяет synchronized
- +Синхронизация: мониторы, ReentrantLock, состояние гонки и дедлок, атомарные классы
- +Пулы потоков: как устроен ThreadPoolExecutor, очередь задач, CompletableFuture и цепочки
- +Коллекции: почему обычный HashMap ломается в многопоточной среде и что предлагает ConcurrentHashMap
- +Virtual Threads: что изменил Loom, где виртуальные потоки помогают, а где остаются те же проблемы
Из чего состоит тема
Так тема разложена в тренажёре: движок ведёт прогресс по каждой подтеме отдельно и возвращает те, где вы ошибаетесь.
- Синхронизация и volatile20
- Локи и примитивы18
- Потоки: основы16
- Пулы и Future16
- Virtual Threads (Loom)13
Разборы подтем
Конспект по каждой: что это, как отвечать вслух, на чём валятся, плюс вопросы для самопроверки.
- Пулы потоков и Future16 вопросов
- Локи и примитивы синхронизации18 вопросов
- Virtual Threads и Project Loom13 вопросов
- Синхронизация и volatile в Java20 вопросов
- Потоки в Java: основы16 вопросов
Примеры вопросов с разбором
- Зачем использовать ExecutorService вместо new Thread на каждую задачу?A)Переиспользует пул потоков и управляет их жизненным цикломB)Он создаёт по одному новому потоку на каждую задачу, но делает это чуть быстрее ручного new ThreadC)Он выполняет все задачи строго последовательно в одном потоке, обеспечивая отсутствие гонокD)Он автоматически делает весь переданный ему код потокобезопасным без синхронизации со стороны разработчика
показать ответ и разбор
+A)Переиспользует пул потоков и управляет их жизненным циклом// разбор: ExecutorService отделяет ЗАДАЧИ от ПОТОКОВ: держит пул переиспользуемых потоков, ставит задачи в очередь, распределяет их, возвращает Future с результатом и даёт управляемое завершение (shutdown). Это убирает дороговизну создания потока на каждую задачу и хаос ручного управления. Создают через Executors.* или напрямую ThreadPoolExecutor (тонкая настройка). Потокобезопасность самих задач — всё ещё забота разработчика.
- Чем ReentrantLock даёт больше возможностей, чем synchronized?A)tryLock, прерываемость, справедливость, несколько ConditionB)Он работает быстрее synchronized, поэтому synchronized в новом коде устарелC)Он снимает блокировку автоматически при выходе из метода, так что вызывать unlock() вручную не нужноD)Он один обеспечивает видимость изменений между потоками, чего synchronized делать не умеет
показать ответ и разбор
+A)tryLock, прерываемость, справедливость, несколько Condition// разбор: ReentrantLock — явная блокировка с гибкостью, которой нет у synchronized: неблокирующая попытка tryLock() и с таймаутом (профилактика deadlock), прерываемое ожидание lockInterruptibly(), опциональная справедливость (FIFO), несколько Condition на один лок. Плата — надо вручную unlock() в finally, иначе лок утечёт. synchronized проще и достаточно в большинстве случаев; Lock берут, когда нужны эти фичи.
- Что такое виртуальные потоки (virtual threads, Java 21)?A)Лёгкие потоки, планируемые JVM поверх немногих потоков-носителейB)Это обычные потоки ОС, которым JVM просто присвоила пониженный приоритет ради экономии памятиC)Это отдельные процессы операционной системы, которыми управляет не JVM, а планировщик ядра ОСD)Это потоки, выполняющие лишь не-блокирующий код: блокирующий вызов внутри них запрещён
показать ответ и разбор
+A)Лёгкие потоки, планируемые JVM поверх немногих потоков-носителей// разбор: Виртуальные потоки (Project Loom, финализированы в Java 21) — очень лёгкие потоки, которыми управляет JVM, мультиплексируя их на небольшое число платформенных потоков-носителей (carrier). Их можно создавать миллионами. При блокирующей операции виртуальный поток «отмонтируется» от носителя, освобождая его для других — поэтому блокирующий стиль кода снова дёшев. Создаются через Thread.ofVirtual() или Executors.newVirtualThreadPerTaskExecutor().
- Что такое состояние гонки (race condition)?A)Результат зависит от непредсказуемого порядка доступа потоков к общим даннымB)Ситуация, когда два потока стартуют одновременно и соревнуются, кто первым захватит процессорное ядроC)Ошибка, при которой поток бесконечно ждёт монитор, уже захваченный другим потоком навсегдаD)Замедление программы из-за того, что потоки по очереди выполняют один и тот же участок кода
показать ответ и разбор
+A)Результат зависит от непредсказуемого порядка доступа потоков к общим данным// разбор: Race condition — когда корректность зависит от НЕДЕТЕРМИНИРОВАННОГО порядка, в котором потоки читают/пишут общие данные без синхронизации. Классика — два потока делают counter++ (чтение-инкремент-запись), и одно обновление теряется (lost update). Такие баги плавающие и трудно воспроизводимые. Лечат синхронизацией (synchronized, Lock), атомиками или неизменяемостью/изоляцией данных по потокам.
- Чем отличается вызов thread.start() от thread.run()?A)start() запускает новый поток; run() выполняется в текущемB)Start() и run() делают одно и то же — оба создают новый поток и выполняют его тело параллельноC)Run() запускает новый поток, а start() лишь помечает поток готовым к запуску, но не выполняет егоD)Start() выполняет код синхронно в текущем потоке, а run() отдаёт его планировщику ОС в отдельный поток
показать ответ и разбор
+A)start() запускает новый поток; run() выполняется в текущем// разбор: start() просит JVM/ОС создать новый поток исполнения, который затем вызывает run(). Прямой вызов run() — это обычный вызов метода в ТЕКУЩЕМ потоке, никакой параллельности. Частая ошибка новичка: написать run() вместо start() и удивляться, что «многопоточности нет». Повторный start() у одного объекта Thread тоже запрещён (IllegalThreadStateException).
- Чем опасен Executors.newCachedThreadPool под высокой нагрузкой?A)Он держит ровно один поток, поэтому под нагрузкой задачи выстраиваются в бесконечную очередь и висятB)Он создаёт неограниченно много потоков и может исчерпать ресурсыC)Он заранее создаёт фиксированное большое число потоков, занимая всю память ещё до поступления задачD)Он не переиспользует потоки: каждый поток после одной задачи немедленно уничтожается
показать ответ и разбор
+B)Он создаёт неограниченно много потоков и может исчерпать ресурсы// разбор: newCachedThreadPool создаёт потоки по мере поступления задач без верхнего предела (и переиспользует простаивающие ~60 сек). Под резким всплеском задач он может наплодить тысячи потоков и уронить приложение (OutOfMemory, деградация планировщика). Для контролируемой нагрузки берут пул с явной верхней границей и ОГРАНИЧЕННОЙ очередью (ThreadPoolExecutor) и продуманной политикой отказа, а не «резиновый» cached.
- Что делает tryLock() у ReentrantLock?A)Захватывает лок и ждёт его сколько угодно долго, но при этом не допускает deadlockB)Пытается захватить лок и сразу возвращает true/false, не блокируясьC)Захватывает сразу два лока атомарно, что делает вложенные блокировки безопаснымиD)Проверяет, свободен ли лок, но сам его не берёт — захват нужно потом выполнить методом lock()
показать ответ и разбор
+B)Пытается захватить лок и сразу возвращает true/false, не блокируясь// разбор: tryLock() пытается захватить лок и НЕМЕДЛЕННО возвращает результат: true (захватил — не забыть unlock в finally) или false (занят, можно сделать что-то другое). Есть вариант с таймаутом tryLock(t, unit). Это ключевой инструмент против deadlock: вместо вечного ожидания поток может отступить, освободить уже взятые локи и повторить попытку. С synchronized так нельзя — там ожидание всегда блокирующее.
- Чем платформенный поток отличается от виртуального?A)Платформенный поток не получится заблокировать, а виртуальный можно — в остальном они идентичны по ценеB)Платформенный — обёртка над потоком ОС (дорогой); виртуальный — управляется JVM (дешёвый)C)Виртуальный поток быстрее платформенного на вычислениях за счёт компиляции в нативный кодD)Платформенные потоки появились в Java 21, а виртуальные существуют с самых первых версий языка
показать ответ и разбор
+B)Платформенный — обёртка над потоком ОС (дорогой); виртуальный — управляется JVM (дешёвый)// разбор: Платформенный поток — тонкая обёртка над потоком ОС (1:1), дорогой по памяти (стек ~1МБ) и переключению; их держат сотнями. Виртуальный — сущность JVM, которую планировщик Loom мультиплексирует на пул носителей (M:N), дешёвая и многочисленная. Для CPU-задач разницы в скорости нет (упираемся в ядра); выигрыш виртуальных — в МАССОВОЙ конкурентности блокирующих операций (много одновременных запросов, ожидающих I/O).
- Что гарантирует блок synchronized?A)Только взаимное исключение; видимость записей другим потокам он никак не обеспечиваетB)Взаимное исключение и видимость изменений между потокамиC)Только атомарность отдельных операций внутри блока, но два потока могут входить в него одновременноD)Что заблокированный участок выполнится быстрее, так как JVM отдаёт ему всё процессорное время
показать ответ и разбор
+B)Взаимное исключение и видимость изменений между потоками// разбор: synchronized захватывает МОНИТОР объекта: в защищённый участок в каждый момент входит только один поток (взаимное исключение). Плюс он устанавливает отношение happens-before — изменения, сделанные до выхода из монитора, ВИДНЫ потоку, который затем этот монитор захватит. То есть synchronized решает сразу и атомарность критической секции, и видимость. Монитор реентерабельный (один поток может войти повторно).
это 9 из 83
Ещё 74 вопросов по теме — в тренажёре, с движком повторения
Прочитать разбор и ответить самому — разные навыки. В Сеньорчике вопросы идут сессиями, а движок возвращает подтемы, где вы ошибаетесь, пока они не начнут отскакивать. Бесплатно, лимит по энергии.
Частые вопросы
Что спрашивают про многопоточность на уровне джуна?
Обычно основы: чем поток отличается от процесса, зачем synchronized, что такое состояние гонки и дедлок. Модель памяти и пулы обычно всплывают на уровне выше.
Зачем нужен volatile, если есть synchronized?
volatile решает только задачу видимости и упорядочивания, но не даёт атомарности составных операций. На собесе часто просят объяснить именно эту границу.
Спрашивают ли виртуальные потоки?
Всё чаще, особенно там, где уже перешли на свежие версии JDK. Достаточно понимать модель, отличие от платформенных потоков и то, что блокировки никуда не исчезают.