Всем привет!)
Я работаю Senior Java Developer в одном из банков, и за последние годы мне пришлось пройти не одно собеседование, услышать десятки каверзных вопросов и потратить уйму времени на подготовку. И вот что я понял: многопоточность — это одна из самых сложных и любимых тем на Java-собеседованиях, независимо от уровня кандидата.
Поэтому в этой статье я хочу помочь вам уверенно подготовиться к секции по concurrency: разберём ключевые термины, посмотрим, как это работает на практике, и дам несколько советов, которые реально помогают на собесах. Поехали!
Жизненный цикл потока
В языке Java многопоточность определяется основной концепцией Thread. В течение своего жизненного цикла потоки проходят через различные состояния:
NEW - только что созданный поток, который еще не начал выполнение
RUNNABLE - либо запущен, либо готов к выполнению, но ожидает выделения ресурсов
BLOCKED - ожидание получения блокировки монитора для входа или повторного входа в синхронизированный блок/метод
WAITING - ожидание выполнения определенного действия другим потоком без ограничения времени
TIMED_WAITING - ожидание выполнения определенного действия каким-либо другим потоком в течение определенного периода времени
TERMINATED - завершил выполнение.

Runnable vs Extending a Thread
Проще говоря, мы предпочитаем использовать Runnable, а не Thread:
Расширяя класс Thread, мы не переопределяем ни один из его методов. Вместо этого мы переопределяем метод Runnable (который, как оказалось, реализует Thread). Это является явным нарушением принципа IS-A Thread
Создание реализации Runnable и передача ее классу Thread использует композицию, а не наследование, что является более гибким.
Начиная с Java 8, Runnables могут быть представлены в виде лямбда-выражений.
Runnable vs. Callable
Оба интерфейса предназначены для представления задачи, которая может выполняться несколькими потоками. Запускать задачи Runnable можно с помощью класса Thread или ExecutorService, а Callables - только с помощью последнего.
wait(), notify(), notifyAll()
wait()— поток ждёт и отпускает монитор.notify()— будит один поток, ждущий на этом объекте.notifyAll()— будит всех.
Synchronized
Мы можем использовать ключевое слово synchronized на разных уровнях:
Методы экземпляра
Статические методы
Блоки кода
Когда мы используем синхронизированный блок, Java внутренне использует монитор, также известный как блокировка монитора или внутренняя блокировка, для обеспечения синхронизации. Эти мониторы привязываются к объекту, поэтому все синхронизированные блоки одного и того же объекта могут одновременно выполняться только одним потоком.
Статические методы синхронизируются так же, как и методы экземпляра.
Эти методы синхронизируются на объекте Class, связанном с классом. Поскольку в JVM для каждого класса существует только один объект Class, то внутри статического синхронизированного метода для одного класса может выполняться только один поток, независимо от количества его экземпляров.
Иногда мы не хотим синхронизировать весь метод, а только некоторые инструкции внутри него. Этого можно добиться, применив synchronized к блоку:
public void exampleSyncBlock() {
synchronized(this) {
setCount(getCount() + 1)
}
}
Обратите внимание, что в синхронизированный блок мы передали параметр this. Это объект монитора. Код внутри блока синхронизируется на объекте монитора. Проще говоря, внутри этого блока кода может выполняться только один поток на объект монитора.
Если бы метод был статическим, то вместо ссылки на объект мы бы передали имя класса, и класс стал бы монитором для синхронизации блока.
Volatile
Мы можем использовать volatile для решения проблем с согласованностью кэша.
Для того чтобы обновления переменных предсказуемо передавались другим потокам, необходимо применить к ним модификатор volatile.
Таким образом, мы можем взаимодействовать с временем выполнения и процессором, чтобы не переупорядочивать инструкции, связанные с переменной volatile. Кроме того, процессоры понимают, что они должны немедленно стирать любые обновления этих переменных.
volatile - довольно полезное ключевое слово, поскольку с его помощью можно обеспечить видимость изменений данных, не обеспечивая при этом взаимного исключения. Таким образом, оно полезно в тех случаях, когда мы не против параллельного выполнения блока кода несколькими потоками, но нам необходимо обеспечить свойство видимости.
Технически любая запись в волатильное поле происходит до каждого последующего чтения этого же поля. Это правило переменной volatile модели памяти Java (JMM).
Happens-before
Happens-before — это базовое правило модели памяти Java, которое определяет, какие изменения одного потока гарантированно видит другой поток.
Если коротко:
Если действие A happens-before действия B, то все эффекты A (запись в память) гарантированно видны потоку, выполняющему B.
Никаких гонок, никаких “плавающих значений” — видимость и порядок действий гарантированы.
Memory Visibility
В многопоточной среде каждый поток может работать со своими копиями данных, лежащими в кэше ядра CPU.
Когда один поток обновляет переменные, нет гарантии, что другой поток сразу увидит эти изменения в основной памяти.
Из-за оптимизаций процессора и компилятора поток-чтения может:
увидеть старое значение,
увидеть новое, но с задержкой,
увидеть значения в другом порядке, чем они были записаны в коде.
Именно поэтому обычные переменные без синхронизации (synchronized, volatile, locks) не дают гарантий видимости.
Locks
Виды Locks
synchronized блок: это ключевое слово в Java, которое может использоваться для ограничения доступа к коду только одним потоком. Этот блок может быть использован для синхронизации методов или блоков кода.
ReentrantLock: это класс, реализующий интерфейс Lock, который предоставляет более гибкие возможности блокировки. Он позволяет использовать более сложные сценарии блокировки, такие как блокировка с ожиданием определенного времени, блокировка с попыткой прервать ожидание и другие.
ReadWriteLock: это интерфейс, предоставляющий две блокировки - одну для чтения и одну для записи. Эта схема позволяет нескольким потокам выполнять операции чтения одновременно, но блокирует доступ на запись во время обновления данных.
StampedLock: lock с поддержкой write/read и ultra-fast optimistic read, который ускоряет чтение и снижает contention в многопоточности.
Deadlock vs Livelock
Deadlock (взаимная блокировка)
Потоки навсегда блокируют друг друга, каждый ждёт ресурс, удерживаемый другим.
Итог: система стоит, прогресса нет.
Пример:
Поток A держит ресурс 1 и ждёт ресурс 2.
Поток B держит ресурс 2 и ждёт ресурс 1.
Как избегать:
Всегда блокировать ресурсы в одном порядке.
Использовать таймауты при захвате блокировок (
tryLock(timeout)).Минимизировать количество одновременно захватываемых блокировок.
Livelock (ожившая блокировка)
Потоки не заблокированы, но бесполезно двигаются, постоянно пытаясь избежать конфликта и мешая друг другу.
Итог: система работает, но прогресса тоже нет.
Пример:
Два потока уступают друг другу ресурс, отказываются и пытаются снова, но синхронно, и бесконечно.
Как избегать:
Добавлять рандомные задержки или экспоненциальный бэкофф при повторных попытках.
Использовать явные таймауты и прекращать попытки через определённое время.
Пересматривать алгоритм кооперации, чтобы не блокировать друг друга непрерывно.
Atomics
Атомики в Java работают как операции, которые гарантируют атомарность и видимость изменений в разделяемых переменных для многопоточных программ. Атомики в Java реализованы через классы из пакета java.util.concurrent.atomic
Атомарные операции гарантируют, что операция будет выполнена полностью и никакой другой поток не сможет изменить значение переменной до завершения операции. Это обеспечивается блокировкой переменной
CountDownLatch
CountDownLatch (появился в JDK 5) - это утилитарный класс, который блокирует набор потоков до завершения некоторой операции.
CountDownLatch инициализируется счетчиком (тип Integer), который уменьшается по мере завершения выполнения зависимых потоков. Но как только счетчик достигает нуля, другие потоки освобождаются.
CyclicBarrier
CyclicBarrier работает практически так же, как и CountDownLatch, за исключением того, что мы можем использовать его повторно. В отличие от CountDownLatch, он позволяет нескольким потокам ожидать друг друга с помощью метода await() (известного как барьерное условие) перед вызовом конечной задачи.
Semaphore
Семафор используется для блокирования доступа на уровне потоков к некоторой части физического или логического ресурса. Семафор содержит набор разрешений; всякий раз, когда поток пытается войти в критическую секцию, он должен проверить семафор на наличие или отсутствие разрешения.
Phaser
Phaser является более гибким решением, чем CyclicBarrier и CountDownLatch, - он используется в качестве многоразового барьера, на котором динамическое число потоков должно подождать перед продолжением выполнения. Мы можем координировать несколько фаз выполнения, повторно используя экземпляр Phaser для каждой фазы программы.
Concurrent Collections
Это коллекции из java.util.concurrent, безопасные для многопоточного доступа без внешней синхронизации.
Примеры:
ConcurrentHashMap— потокобезопасная карта, поддерживает быстрые операции чтения и записи.CopyOnWriteArrayList— потокобезопасный список, который копирует внутренний массив при модификации.
ConcurrentHashMap часто используется для согласованного кэша: несколько потоков могут читать и обновлять данные одновременно, при этом карта остаётся в корректном состоянии. Это особенно удобно для хранения промежуточных или вычисленных значений, к которым часто обращаются несколько потоков.
Executor
Это интерфейс, представляющий объект, выполняющий поставленные задачи.
От конкретной реализации зависит, будет ли задача выполняться в новом или текущем потоке. Таким образом, используя этот интерфейс, мы можем отделить поток выполнения задачи от собственно механизма ее выполнения.
Следует отметить, что Executor не требует, чтобы выполнение задачи было асинхронным. В простейшем случае исполнитель может вызывать представленную задачу мгновенно в вызывающем потоке. Если Executor исполнитель не сможет принять задачу к исполнению, то он выбросит сообщение RejectedExecutionException
ExecutorService
ExecutorService представляет собой комплексное решение для асинхронной обработки. Он управляет очередью in-memory и планирует выполнение заданий в зависимости от доступности потоков:
ExecutorService excecutor = Executors.newFixedThreadPool(10);
excecutor.submit(() -> {
//задача excecutor
})
Future
Future представляет будущий результат асинхронного вычисления. Этот результат в конечном итоге появится в Future после завершения обработки.
Более того, API cancel(boolean mayInterruptIfRunning) отменяет операцию и освобождает выполняющий ее поток. Если значение параметра mayInterruptIfRunning равно true, то поток, выполняющий задачу, будет немедленно завершен.
CompletableFuture
Это расширение класса Future, которое предоставляет возможность более гибкой работы с асинхронными операциями. CompletableFuture позволяет явно управлять завершением операции, комбинировать несколько операций в цепочку и выполнять дополнительные действия по ее завершению.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Выполнение асинхронной операции
return "Результат операции";
});
Ключевые моменты:
thenApply — применяет функцию к результату предыдущей задачи, возвращает новый CompletableFuture.
cf.thenApply(result -> result * 2);
thenCompose — разворачивает вложенный CompletableFuture, используется для последовательного асинхронного выполнения.
cf.thenCompose(result -> asyncTask(result));
allOf / anyOf — комбинируют несколько CompletableFuture:
allOf — ждет завершения всех задач.
anyOf — возвращает результат первой завершившейся задачи.
CompletableFuture.allOf(cf1, cf2).join();
CompletableFuture.anyOf(cf1, cf2).thenAccept(System.out::println);
Асинхронное выполнение — можно запускать задачи в отдельном пуле потоков:
CompletableFuture.supplyAsync(() -> compute(), executor);
ForkJoinPool
Центральная часть фреймворка fork/join (Java 7). Позволяет эффективно выполнять рекурсивные задачи, не создавая отдельный поток для каждой подзадачи. Задачи можно fork (разделить на подзадачи) и join (ждать их завершения). За счёт work-stealing алгоритма потоки перераспределяют работу между собой, что повышает эффективность.
Рекомендации
Перед собеседованием попросите чата гпт или поищите в интернете задачи на ревью кода по многопоточности, часто на собеседованиях задачи именно на просмотр текущего кода, поиска ошибок и исправлений.
Попрактикуйтесь писать маленькие примеры с
ExecutorService,CompletableFuture,CountDownLatchиSemaphore. На собеседовании часто просят объяснить, как работает код, а практика с реальными примерами помогает быстро ориентироваться.
Итог
Сегодня мы разобрали ключевые темы по многопоточности, которые чаще всего встречаются на собеседованиях по Java. Конечно, не обязательно, что спросят всё из этого списка, но с ним вы точно не останетесь «не в теме». Я собирал его на основе собственного опыта прохождения собеседований — всё это реально спрашивали и продолжают спрашивать.
В будущем я планирую продолжать разбирать другие популярные темы на собеседованиях и делиться с вами советами и шпаргалками по ним, чтобы подготовка стала ещё проще и эффективнее.
Всем спасибо за внимание, удачных собесов и хорошего дня!)
Комментарии (5)

cpud47
16.11.2025 16:19Технически любая запись в волатильное поле происходит до каждого последующего чтения этого же поля
Нет, вообще не гарантируется. Гарантируется SeqCst, что позволяет в среднем притвориться, что эффектов кеша для данной переменной не существует. А вот каких либо гарантий на то, через какое время остальные потоки увидят данное изменение — таких гарантий нигде в jvm получить нельзя.

VirtualVoid
16.11.2025 16:19Стоит не забывать ещё такой полезный инструмент как Java Concurrent Animated.
Может быть чрезвычайно полезен при изучении многопоточки.
Elinkis
Жду вторую часть! Спасибо!