История вопроса
До Java 5 координация потоков полагалась на примитивные методы, такие как Thread.suspend (устаревший из-за inherent deadlock рисков) или Object.wait/notify, которые требовали строгого владения мониторами и страдали от потерь пробуждений, если уведомление произошло до ожидания. С введением java.util.concurrent в Java 5 (JSR 166) LockSupport был разработан как низкоуровневый примитив для разблокировки, чтобы обеспечить создание высокопроизводительных синхронизаторов, таких как AbstractQueuedSynchronizer, без лишних затрат, связанных с внутренними блокировками.
Проблема
В конкурентном программировании классическое состояние гонки происходит, когда сигнализирующий поток вызывает механизм unpark до того, как целевой поток фактически паркуется. С традиционными условными переменными этот сигнал будет потерян, что приведет к тому, что целевой поток будет спать бесконечно. Наивное решение может использовать подсчетный семафор для накопления разрешений, но это вносит ненужную сложность и потенциальные утечки ресурсов, если производитель опережает потребителя.
Решение
LockSupport использует неаккумулирующее, однобитное разрешение, связанное с каждым потоком. Это разрешение действует как одноразовый, локальный пропуск:
Поскольку разрешение не накапливается (оно насыщается до 1), это предотвращает утечки памяти от чрезмерного разблокирования и гарантирует, что одно unpark, выданное перед park, будет запомнено, тем самым устраняя проблему потери пробуждений через отношение happens-before.
import java.util.concurrent.locks.LockSupport; public class PermitExample { public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { System.out.println("Worker: Initial work..."); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("Worker: Attempting to park..."); LockSupport.park(); System.out.println("Worker: Unparked successfully!"); }); worker.start(); // Сигнал до того, как работник фактически паркуется Thread.sleep(50); System.out.println("Main: Calling unpark before worker parks"); LockSupport.unpark(worker); worker.join(); } }
Описание проблемы
При проектировании системы ордеров для высокочастотной торговли нам нужен был механизм сдерживания, при котором потоки-потребители могли бы приостанавливать обработку, когда входная очередь достигала емкости, не удерживая блокировки, которые бы помешали производителям проверять состояние очереди. Стандартный ReentrantLock с Condition создавал конкуренцию за блокировку очереди во время сигнализации, а Object.wait/notify страдал от риска потерянных пробуждений во время высокоуровневых гонок.
Рассмотренные различные решения
1. Object.wait/notifyAll
Этот подход использовал внутреннюю блокировку очереди. Плюсы: Просто реализовать, используя стандартные мониторы. Минусы: Требовал от производителя захвата монитора для вызова notify, создавая бутылочное горлышко сериализации. Худшее, если производитель вызывал notify в кратком промежутке времени между проверкой потребителя размера очереди и вызовом wait, сигнал терялся, вызывая постоянное состояние взаимной блокировки потребителя.
2. ReentrantLock с несколькими условиями
Мы пытались использовать отдельные условия для состояний "полной" и "пустой". Плюсы: Более гибкие, чем внутренние блокировки, позволяющие выборочное пробуждение. Минусы: Все равно требовали захвата блокировки для сигнализации (signalAll), и сложность правильной передачи потоков между очередями условий вводила накладные расходы на обслуживание, не решая основную накладную стоимость блокировки.
3. LockSupport с явным атомарным состоянием
Выбранное решение использовало AtomicBoolean для представления "разрешения на продолжение" и LockSupport для блокировок. Когда очередь заполнялась, потребитель атомарно устанавливал флаг "needsParking" и затем паркировался. Производители, удаляя элемент, проверяли флаг и вызывали unpark, если он установлен. Плюсы: Сигнализация не требовала никаких блокировок, устраняя конкуренцию во время пробуждений. Модель с однобитным разрешением гарантировала, что даже если производитель вызывал unpark нано-секунды до того, как потребитель вызывал park (из-за планирования CPU), пробуждение не терялось.
Выбранное решение и результат
Мы выбрали подход с LockSupport. Разъединив механизм сигнализации от структурной блокировки очереди, мы сократили задержку производителя на 40% при высокой нагрузке и устранили сценарии потерянного пробуждения, наблюдаемые во время стресс-тестирования. Явное управление состоянием (двойная проверка условия после unpark) обеспечивало правильность, несмотря на контракт ложных пробуждений метода park().
Освобождает ли LockSupport.park владение мониторами, удерживаемыми потоком?
Нет. Это критическое различие по сравнению с Object.wait(). Когда поток вызывает LockSupport.park, он входит в состояние ожидания, но сохраняет владение всеми мониторами, которые он в настоящее время удерживает. Если другой поток пытается войти в один из этих мониторов (например, синхронизированный блок на том же объекте), он будет заблокирован, что потенциально может вызвать взаимную блокировку, если единственный поток, который может его освободить, – это припаркованный поток. Кандидаты часто ошибочно полагают, что park подобен wait и освобождает блокировки; это исключительно примитив планировщика, локальный для потока.
Каково поведение LockSupport.park при вызове на потоке, статус прерывания которого установлен?
Метод возвращает немедленно без блокировки и не очищает статус прерывания. Это принципиально отличает его от Object.wait(), который очищает статус прерывания и выбрасывает InterruptedException. С помощью LockSupport поток должен явно проверить и очистить статус прерывания (через Thread.interrupted()), если он хочет соблюдать соглашения прерывания. Этот дизайн позволяет использовать park в непрерываемых контекстах или там, где прерывание обрабатывается как отдельная проблема от разрешения парковки.
Как LockSupport обрабатывает ложные пробуждения, и как это влияет на кодовые паттерны?
LockSupport.park документируется как возвращающий "по какой-либо причине" (ложное пробуждение), хотя на практике это редко происходит на современных JVM. В отличие от пробуждения на основе разрешения (unpark), ложные пробуждения не расходуют разрешение. Поэтому вызывающий всегда должен повторно проверить условие, которое вызвало парковку в цикле:
while (!canProceed()) { LockSupport.park(); }
Кандидаты часто упускают, что простая проверка условия один раз после park недостаточна; поток может проснуться ложным образом (или из-за случайного прерывания) без вызова unpark, требуя переоценки состояния условия. Разрешение гарантирует, что действительный unpark не будет потерян, но оно не предотвращает ложные возвраты.