Многопоточность в Java
Многопоточность в Java — это выполнение нескольких потоков внутри одного процесса. Все потоки делят кучу процесса и загруженные классы, но у каждого свой стек. Это делает обмен данными быстрым — копировать ничего не нужно, — но опасным: два потока могут одновременно читать и писать один объект. Порядок их шагов планировщик не гарантирует, поэтому корректность нельзя строить на «повезло с таймингом».
Фундамент — Java Memory Model (JMM) и отношение happens-before. Без явной синхронизации у одного потока нет гарантии увидеть запись другого: значение может застрять в кэше ядра или регистре. Монитор (synchronized), volatile и классы из java.util.concurrent устанавливают happens-before — границу, после которой изменения гарантированно видны. При этом важно не путать две разные гарантии: видимость (увидит ли поток свежее значение) и атомарность (не разорвётся ли составная операция на полпути). Их смешение — источник большинства ошибок ниже. Полная карта — в слоях.
Карта темы
- Процесс против потока — процесс изолирован своей памятью, поток живёт внутри процесса, делит его кучу, но держит собственный стек.
- Создание потоков — наследовать
Threadпротив реализоватьRunnable, пулExecutorServiceи результат черезFuture. - Синхронизация —
synchronizedзахватывает монитор объекта и сериализует критическую секцию, устанавливая happens-before. - Ключевое слово volatile — гарантирует видимость и запрет переупорядочивания, но НЕ атомарность составных операций.
- wait против sleep —
wait()отпускает монитор и ждётnotify(),sleep()удерживает все блокировки. - Ошибки конкурентности — гонка данных, взаимоблокировка, живая блокировка и голодание: чем отличаются и чем лечатся.
- Захват effectively-final — лямбды и анонимные классы захватывают только effectively-final локальные переменные.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Вызвать run() вместо start() | Код выполняется в текущем потоке — нового потока не появляется |
Считать volatile атомарным для count++ | read-modify-write чередуется, и обновления теряются |
Вызвать wait() вне synchronized | IllegalMonitorStateException во время выполнения |
Думать, что sleep() отпускает монитор | Спящий поток держит блокировку — остальные простаивают |
Захватить переменную цикла for в лямбде | Не компилируется: переменная не effectively final |
| Брать две блокировки в разном порядке | Циклическое ожидание → deadlock |
notify() при разных условиях ожидания | Разбужен не тот поток, а готовый к работе ждёт навсегда |
Значение для собеседований
Конкурентность — один из главных фильтров на middle/senior-интервью по Java. Проверяют не знание ключевых слов, а модель разделяемого состояния: понимаете ли вы, что монитор даёт и взаимное исключение, и видимость, а volatile — только видимость; что гонка данных — проблема корректности, а не скорости; что deadlock лечится единым порядком захвата, а не приоритетами.
Что обычно проверяют:
- Разницу процесса и потока и что именно потоки делят (куча), а что приватно (стек).
- Способы создания потока и почему
Runnableпредпочтительнее наследованияThread. - Что делает
synchronized— сериализует секцию на мониторе объекта и устанавливает happens-before. - Что
volatileгарантирует видимость, но не атомарностьi++. - Разницу
wait()иsleep()по отношению к удерживаемому монитору. - Гонку, deadlock и livelock — как отличить и как каждый лечить.
Типичный неверный ответ: «volatile делает счётчик потокобезопасным». Это открывает разговор о том, что count++ — это read-modify-write из трёх шагов, которые два потока чередуют, теряя обновления; volatile синхронизирует лишь видимость одиночного чтения и записи, а атомарный инкремент даёт synchronized или AtomicInteger.