В сценарии, где поток должен отказаться от ожидания блокировки по interrupt, почему ReentrantLock подходит лучше synchronized?
ReentrantLock позволяет ожидать блокировку в прерываемом режиме через lockInterruptibly(): при получении сигнала прерывания поток прекращает ожидание и получает InterruptedException. При входе в монитор через synchronized такой возможности нет: прерывание устанавливает флаг потока, но само ожидание входа в монитор не отменяет.
Мониторы, используемые конструкцией synchronized, дают простой и безопасный способ взаимного исключения: блокировка автоматически освобождается при выходе из синхронизированного блока или метода. Однако у такого механизма ограничены возможности управления ожиданием.
В Java 5 появился пакет java.util.concurrent и интерфейс Lock, чтобы предоставить более гибкие сценарии синхронизации: прерываемое и ограниченное по времени ожидание, несколько условий ожидания и настраиваемую политику справедливости. ReentrantLock сохраняет взаимное исключение, но явно предоставляет операции захвата и освобождения блокировки.
Поток может надолго зависнуть при попытке войти в занятый монитор. Если приложение завершается, задача отменяется или поток должен реагировать на остановку, обычный synchronized не позволяет корректно отказаться именно от ожидания входа в монитор.
Игнорирование этой особенности приводит к зависшим рабочим потокам, задержке завершения сервиса и невозможности своевременно отменить задачу. Простая замена synchronized на ReentrantLock сама по себе проблему не решает: нужно использовать именно прерываемый способ захвата и гарантировать освобождение блокировки.
ReentrantLock.lockInterruptibly() проверяет состояние прерывания во время ожидания. Если поток был прерван до захвата блокировки или во время ожидания, метод выбрасывает InterruptedException, а блокировка ему не достаётся.
Минимальный пример:
Если блокировка уже получена, последующее прерывание не освобождает её автоматически. Код должен сам завершить работу и выйти из try, после чего finally вызовет unlock().
У ReentrantLock есть и другие режимы: lock() ведёт себя как обычное неограниченное ожидание, а tryLock() позволяет проверить доступность блокировки сразу или ждать ограниченное время. Для отменяемых задач обычно выбирают lockInterruptibly(), потому что он связывает ожидание ресурса с протоколом остановки потока.
У synchronized прерывание не прерывает ожидание входа в монитор. При этом методы wait() внутри монитора реагируют на прерывание, но это уже ожидание условия после захвата монитора, а не ожидание самого монитора. Нельзя использовать это различие как замену lockInterruptibly().
Явная блокировка требует дисциплины: unlock() должен находиться в finally, а владение блокировкой нельзя передавать между потоками без чёткого протокола. Поэтому для простого взаимного исключения предпочтительнее synchronized, а ReentrantLock оправдан, когда нужны его дополнительные режимы ожидания или управления.
В сервере задача обрабатывает запрос и перед чтением общего ресурса ожидает блокировку. При остановке сервиса диспетчер прерывает рабочие задачи. Вариант с synchronized оставляет поток в ожидании монитора: он может не завершиться, пока владелец блокировки не освободит её.
Вариант с ReentrantLock.lock() сохраняет ту же проблему, поскольку это также непрерываемое ожидание. Вариант с периодическим tryLock() позволяет проверять отмену, но требует циклов, тайм-аутов и настройки интервала; слишком большой интервал замедляет остановку, а слишком маленький создаёт лишние пробуждения.
Выбран lockInterruptibly() в сочетании с обработкой InterruptedException на уровне задачи. Поток немедленно прекращает ожидание при отмене, а уже захваченная блокировка освобождается через finally. Это уменьшает время остановки сервиса без искусственного опроса состояния в цикле.
Освобождает ли прерывание уже захваченную ReentrantLock?
Нет. Прерывание только сообщает потоку о необходимости остановиться и может прервать ожидание внутри lockInterruptibly(). Если поток уже владеет блокировкой, он обязан сам завершить критическую секцию и вызвать unlock().
Чем tryLock(timeout) принципиально отличается от lockInterruptibly()?
tryLock(timeout) ограничивает максимальное время ожидания и после его истечения возвращает false. lockInterruptibly() ожидает без заданного тайм-аута, но прекращает ожидание при прерывании и сообщает об этом через InterruptedException.
Выбор зависит от протокола задачи: тайм-аут подходит, когда есть допустимый предел ожидания ресурса, а прерываемая блокировка — когда внешний механизм отмены должен управлять жизненным циклом задачи.
Можно ли считать ReentrantLock безопасной заменой synchronized без изменения остального кода?
Нет. У synchronized захват и освобождение связаны со структурой блока языка, поэтому освобождение происходит автоматически при выходе из него. У ReentrantLock программист обязан соблюдать парность lock() и unlock(), обычно помещая unlock() в finally.
Ошибка в этом протоколе может привести к постоянной блокировке других потоков. Кроме того, нужно учитывать, какой именно метод захвата выбран: использование lock() вместо lockInterruptibly() снова сделает ожидание нечувствительным к прерыванию.