Многопоточность в 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.

Отсюда практический вывод, который и есть содержание ответа: передавая объект через конкурентную очередь или через результат задачи, вы получаете видимость всего, что записали до передачи. Синхронизировать поля вручную поверх этого не нужно: порядок уже установлен самой передачей.

Обратная сторона называется так же коротко: гарантия действует только на объект-синхронизатор, через который прошла передача. Конкурентная коллекция упорядочивает то, что через неё положили и забрали; она ничего не говорит про поле, изменённое в стороне от неё.

Стоит учесть и форму, в которой это спрашивают. Инженер, описавший свой опыт таких секций, пишет, что задачу часто дают на чтение готового кода: найти ошибку и исправить. Для такого формата нужно уметь объяснить, что в этом фрагменте не упорядочено.

Виртуальные потоки в этой картине ничего не меняют: они меняют цену потока. Правила видимости и happens-before для них те же, и все пять секций выше применимы без поправок.

Проверьте себя

Сначала проговорите ответ, потом сверьтесь с текстом.

Что делает ключевое слово volatile?

Что проверяют: называете вы эффект или механизм. «Значение не кэшируется» — описание догадки о реализации. Гарантия формулируется иначе: запись в volatile-поле happens-before каждое последующее чтение того же поля.

Частая ошибка: сказать «volatile делает переменную потокобезопасной». Потокобезопасность — свойство операции; следующая секция показывает, где эта формулировка разваливается.

Раздел «Что на самом деле проверяют»
Что такое happens-before?

Что проверяют: держите вы в голове отношение между действиями или подменяете его свойством переменной. Из отношения выводятся ответы про synchronized и volatile; из свойства переменной не выводится ничего.

Частая ошибка: пересказать happens-before как «действия выполняются в порядке написания». Спецификация этого не утверждает: перестановка разрешена, пока результат совпадает с законным исполнением.

Раздел «Правило, из которого всё выводится»
Достаточно ли volatile для счётчика, который увеличивают из нескольких потоков?

Что проверяют: различаете вы видимость и атомарность. Вопрос устроен так, что ответ «да» возможен только при их смешении.

Частая ошибка: ответить «да, ведь значение всегда свежее». Свежее оно и будет: у обоих потоков одинаково свежее, и одно из двух увеличений потеряется.

Раздел «Где volatile перестаёт помогать»
Синхронизированный статический метод и синхронизированный обычный метод одного класса — блокируют ли они друг друга?

Что проверяют: знаете вы, что монитор — это конкретный объект, или считаете synchronized свойством метода. Из первого ответ выводится сразу, из второго не выводится вовсе.

Частая ошибка: ответить «да, оба же synchronized». Мониторы разные: у одного — this, у другого — объект Class, и пересечения между ними нет.

Раздел «Два вопроса, на которых это проверяют на практике»
Зачем нужны конкурентные коллекции, если есть synchronized?

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

Частая ошибка: сказать «они быстрее, потому что не блокируют». Часть из них блокирует; к тому же это снова ответ про цену.

Раздел «Почему java.util.concurrent — то же самое правило»

Проверьте эти темы вслух

В тренажёре вы отвечаете голосом, а разбор показывает, где ответ просел.