Программирование JavaМногопоточностьJava-разработчик серверных приложений

Почему вызов LockSupport.unpark до LockSupport.park не теряется?

Почему вызов LockSupport.unpark() до LockSupport.park() не теряется?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

LockSupport связывает с каждым потоком один разрешающий сигнал — permit. Если вызвать unpark() до park(), permit сохраняется, поэтому последующий park() сразу возвращается, не блокируя поток.

Это не счётчик сигналов: несколько вызовов unpark() до одного park() не накапливают несколько permit. Кроме того, park() может вернуться самопроизвольно, поэтому его обычно используют внутри цикла, проверяющего условие.

Исторический контекст

Классический wait/notify требует владения монитором и аккуратного протокола с условием ожидания. Такая модель подходит для многих задач, но неудобна при построении собственных очередей, блокировок и других синхронизаторов.

LockSupport предоставляет более низкоуровневый примитив блокировки и пробуждения потока. На его основе реализуются, в частности, очередные синхронизаторы вроде механизмов, построенных с использованием AbstractQueuedSynchronizer.

Постановка проблемы

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

Если такой сигнал просто теряется, ожидающий поток может заблокироваться навсегда. Поэтому важно, чтобы пробуждение, произошедшее непосредственно перед ожиданием, сохраняло эффект.

Подробное решение

У каждого потока есть связанный с LockSupport permit, которого может быть не больше одного. unpark(thread) выдаёт этот permit, а park() потребляет его и немедленно возвращается. Если permit ещё не выдан, park() блокирует поток до выдачи permit, прерывания или ложного пробуждения.

Порядок вызовов поэтому не обязан быть строго таким: сначала park(), затем unpark(). Допустима и обратная последовательность — при условии, что целевой поток уже создан и доступен для операции.

import java.util.concurrent.locks.LockSupport; class Demo { public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { LockSupport.park(); System.out.println("продолжил работу"); }); worker.start(); LockSupport.unpark(worker); worker.join(); } }

В примере поток может ещё не успеть выполнить park(), когда вызывается unpark(). Permit сохранится, и последующий park() не заблокирует поток.

Однако unpark() не передаёт данные и не заменяет протокол проверки состояния. Код должен сначала изменить или опубликовать состояние, а ожидающий поток — после пробуждения заново проверить условие. Действия до unpark() имеют необходимую гарантию порядка относительно последующего возврата из соответствующего park(), но сам факт возврата из park() не доказывает, что произошло именно нужное пробуждение: возможны interrupt и ложные возвраты.

LockSupport также не является очередью разрешений и не гарантирует справедливость. Если требуется одноразовое ожидание результата, обычно понятнее применить CountDownLatch или CompletableFuture; LockSupport оправдан при реализации собственного синхронизатора, где нужны точный контроль очереди и минимальный уровень абстракции.

Ситуация из практики

Команда реализует собственную очередь ожидания для блокировки. Поток добавляет себя в очередь, проверяет, что захватить ресурс пока нельзя, и вызывает park(). Освобождающий поток снимает ограничение и вызывает unpark() для следующего кандидата.

Рассматривались три варианта. wait/notify требовал общего монитора и усложнял адресное пробуждение конкретного участника. CountDownLatch хорошо решал одноразовое событие, но не подходил для многократных циклов захвата и освобождения. LockSupport позволил адресно будить выбранный поток и не терять сигнал, если пробуждение произошло до фактического входа в park().

Выбрали LockSupport вместе с очередью и проверкой состояния в цикле. Это дало нужный контроль, но потребовало самостоятельно корректно обрабатывать отмену, interrupt, удаление узлов очереди и ложные пробуждения.

Что кандидаты часто упускают

  1. Является ли permit счётчиком, если вызвать unpark() несколько раз подряд?

Нет. Для одного потока хранится максимум один permit. Два вызова unpark() до следующего park() эквивалентны одному с точки зрения блокировки: первый выдаёт permit, второй не создаёт дополнительный запас.

Это отличие от семафора. Если требуется накопить несколько разрешений, нужен другой примитив, например Semaphore, либо собственный счётчик состояния с синхронизацией.

  1. Можно ли после park() безусловно продолжать выполнение, не проверяя условие?

Нельзя. park() может вернуться из-за выданного permit, interrupt или ложного пробуждения. Поэтому ожидающий поток должен возвращаться к проверке логического условия, обычно в цикле.

Сам interrupt не обязан автоматически завершать задачу: park() возвращается, а статус interrupt остаётся установленным. Дальнейшее поведение — завершение, повторное ожидание или восстановление статуса — определяется протоколом конкретного синхронизатора.

  1. Заменяет ли unpark() публикацию общего состояния между потоками?

Он обеспечивает требуемый порядок действий, предшествующих unpark(), относительно последующего возврата из соответствующего park(), но не отменяет необходимость корректно проектировать состояние и цикл ожидания. Нельзя полагаться только на произвольные несинхронизированные чтения и записи вокруг этого примитива.

Обычно состояние публикуют через поля с подходящей синхронизацией, например volatile, атомарные классы или защищённую блокировкой структуру. LockSupport отвечает прежде всего за блокирование и пробуждение, а не за автоматическое поддержание инвариантов пользовательского состояния.