Многопоточность в Java на собеседовании: что проверяют
За вопросами про synchronized и volatile проверяют одно — понимаете ли вы видимость. Разбираем happens-before и то, где заученный ответ разваливается.
Вопросов про многопоточность в подборках десятки: volatile, synchronized, разница между wait и sleep, зачем нужен ConcurrentHashMap, что такое CountDownLatch. Выучить полсотни определений тяжело, а главное — ненадёжно: список по своей форме состоит из независимых пунктов и не показывает, что между ними общего. А общее есть — почти все эти вопросы проверяют одно свойство. Ниже — какое именно и что из него следует.
Всё, что сказано о поведении, относится к Java SE 21.
Что на самом деле проверяют
Свойство, вокруг которого построена вся тема, называется видимостью: увидит ли один поток то, что записал другой. Ответ спецификации звучит жёстче, чем ожидают: результат записи гарантированно виден чужому чтению только если между ними установлено отношение happens-before. Нет отношения — нет гарантии, и поток вправе читать старое значение сколько угодно долго.
Отсюда первое следствие, которое переворачивает интуицию: бесконечный цикл по неволатильному флагу — не ошибка реализации, а разрешённое поведение. Ничто не обязывает поток перечитывать поле, если между записью и чтением нет установленного порядка.
Второе следствие важнее первого и звучит неприятно: отсутствие сбоя ничего не доказывает. Спецификация прямо разрешает исполнять действия не в том порядке, в котором они связаны отношением happens-before, — если результат совпадает с каким-то законным исполнением, перестановка законна. Программа с гонкой может годами работать правильно и сломаться от смены процессора или версии JVM. Поэтому «я запустил, всё считается верно» — не аргумент о корректности, а наблюдение об одном запуске.
Правило, из которого всё выводится
Happens-before — это отношение порядка между действиями, а не свойство отдельной переменной. Формулировка спецификации короткая: если одно действие happens-before другое, то первое видимо второму и упорядочено раньше него.
Базовых источников этого отношения немного, и их стоит помнить списком, потому что дальше всё выводится из них:
- каждое действие потока happens-before все его последующие действия — порядок внутри одного потока;
- выход из монитора happens-before каждый последующий вход в тот же монитор;
- запись в
volatile-поле happens-before каждое последующее чтение того же поля; Thread.start()happens-before любое действие запущенного потока;- все действия потока happens-before успешный возврат из
join()на нём.
Дальше работает транзитивность, и это самый полезный пункт: если действие A упорядочено раньше B, а B раньше C, то A упорядочено раньше C. Именно транзитивность объясняет, почему synchronized защищает не только то, что лежит внутри блока: все действия потока до выхода из монитора становятся видны любому потоку, который потом в этот монитор вошёл, — включая записи в обычные, не помеченные ничем поля.
Из того же списка выводится и определение гонки, которое стоит уметь сказать своими словами: гонка — это два конфликтующих доступа к одной переменной, не упорядоченные отношением happens-before, где конфликтующие означает, что хотя бы один из них — запись.
Где volatile перестаёт помогать
Граница проходит между двумя разными свойствами, и заученное определение её не показывает. volatile даёт видимость. Он не даёт атомарности составной операции.
Классический пример — увеличение счётчика. counter++ выглядит как одно действие, а состоит из трёх: прочитать, прибавить, записать. volatile гарантирует, что каждое из трёх работает со свежим значением, и ничего не гарантирует про промежуток между ними. Два потока читают одно и то же, оба прибавляют единицу, оба записывают — счётчик вырос на единицу вместо двух. Отсюда правило: volatile подходит для флага, который один поток пишет, а другие читают, и не подходит для значения, которое вычисляется из самого себя. Для второго случая есть AtomicInteger и synchronized.
Есть и второй разрез, менее известный и потому полезный. Спецификация выделяет 64-битные типы: запись в не-volatile поле типа long или double рассматривается как две отдельные записи по 32 бита. Практическое следствие — поток может увидеть половину значения от одной записи и половину от другой, то есть число, которого никто никогда не записывал. Пометка volatile эту проблему снимает: чтения и записи volatile-полей типа long и double атомарны всегда.
Полезно назвать и границу этого утверждения: на ссылки оно не распространяется — их чтение и запись атомарны независимо от разрядности. Речь именно о значениях, а не об объектах.
Два вопроса, на которых это проверяют на практике
Разбор двухсот сорока семи собеседований, опубликованный в 2026 году, описывает блок многопоточности как три захода подряд: volatile и happens-before, затем synchronized на статическом и на обычном методе, затем потокобезопасный синглтон. Первое разобрано выше; два оставшихся — это то же правило, приложенное к коду.
На каком объекте берётся монитор. Спецификация отвечает буквально одной строкой: обычный synchronized-метод захватывает монитор объекта, на который ссылается this; статический — монитор объекта Class этого класса. Отсюда следствие, ради которого вопрос и задают: это два разных монитора, и они не мешают друг другу. Статический и обычный синхронизированные методы одного класса могут выполняться одновременно, и общее состояние между ними не защищено ничем.
Потокобезопасный синглтон с двойной проверкой. Здесь сходятся оба свойства сразу, и потому этот пример — хорошая проверка того, что правило усвоено, а не заучено. Поле должно быть volatile, и причина выводится из того, что уже сказано: без него между записью ссылки в поле и её чтением другим потоком нет happens-before. А значит, чужой поток вправе увидеть ссылку раньше, чем записи, сделанные конструктором, — то есть получить ссылку на объект, который ещё не достроен. Проверка if (instance != null) при этом пройдёт успешно, и наружу уйдёт полуинициализированный объект.
Из той же логики следует, почему одной внешней проверки без блокировки мало, а одной блокировки без volatile — недостаточно: блокировка даёт взаимное исключение, volatile даёт видимость, и двойная проверка требует обоих.
Почему java.util.concurrent — то же самое правило
Пакет java.util.concurrent кажется набором независимых инструментов: очереди, пулы, защёлки, барьеры. На собеседовании полезнее другая рамка, и она взята прямо из его документации: весь пакет описан как расширение happens-before на высокоуровневые конструкции. Не новый механизм, а то же отношение, доведённое до очередей и исполнителей.
Формулировки документации стоит помнить, потому что из них ответы выводятся, а не вспоминаются:
- действия до помещения объекта в конкурентную коллекцию упорядочены раньше действий после его извлечения другим потоком;
- действия до передачи задачи исполнителю упорядочены раньше начала её выполнения;
- действия внутри асинхронного вычисления упорядочены раньше того, что происходит после получения результата через
Future.get(); - действия до
Lock.unlock,Semaphore.release,CountDownLatch.countDownупорядочены раньше действий после соответствующихlock,acquire,await.
Отсюда практический вывод, который и есть содержание ответа: передавая объект через конкурентную очередь или через результат задачи, вы получаете видимость всего, что записали до передачи, — не только самого объекта. Синхронизировать поля вручную поверх этого не нужно — порядок уже установлен самой передачей.
Обратная сторона называется так же коротко: гарантия действует только на объект-синхронизатор, через который прошла передача. Конкурентная коллекция упорядочивает то, что через неё положили и забрали; она ничего не говорит про поле, изменённое в стороне от неё.
Стоит учесть и форму, в которой это спрашивают: инженеры, описывающие свой опыт этих секций, говорят, что задача нередко даётся на чтение готового кода — найти ошибку и исправить, а не рассказать определение. К такому формату помогает готовность объяснить не «что делает volatile», а «что в этом фрагменте не упорядочено».
Виртуальные потоки в этой картине ничего не меняют: они меняют цену потока, а не правила видимости. Happens-before для них тот же, и все пять секций выше применимы без поправок.

Backend-разработчик Java
Учебный маршрут backend-разработчика на Java: язык и JVM, коллекции и многопоточность, Spring, реляционные данные и JPA, Kafka, безопасность, продакшн-инженерия и архитектура.

Backend Developer Java
Глубокое понимание Java и JVM для backend-разработки. Курс разбирает не синтаксис, а семантику языка и поведение платформы: коллекции и их стоимость, Stream API, устройство JVM — память, сборку мусора, JIT, — модель памяти и многопоточность, внутренности Spring (контейнер, прокси, транзакции), JPA/Hibernate, Kafka и production-практики: тестирование, наблюдаемость, контейнеры, устойчивость. Цель — научиться видеть стоимость абстракций и поведение кода под нагрузкой, а не просто заставлять его работать.
Готовьтесь не вслепую
Возьмите маршрут по своей роли и отрабатывайте ответы на вопросы с AI-разбором.