Когда используется Semaphore в Java
Что проверяют
Понимаете ли вы, что семафор ограничивает число одновременных участников, и чем он отличается от блокировки. Следом обычно спрашивают, кто может вызвать release() и что будет, если вызвать его лишний раз.
Как отвечать
java.util.concurrent.Semaphore хранит счётчик разрешений. acquire() забирает разрешение или блокирует поток, пока оно не появится, release() возвращает. Типовые задачи:
- ограничение параллелизма: не больше десяти одновременных запросов к внешнему API или к медленному сервису;
- виртуальные потоки: с Java 21 их не складывают в пул ради лимита, для ограничения конкурентности JEP 444 прямо советует семафор;
- ограниченный ресурс: самописный пул соединений, лицензий, слотов;
- bulkhead: изоляция одной зависимости от остальных, так устроен семафорный bulkhead в Resilience4j.
Полезные детали API: tryAcquire(timeout, unit) не ждёт бесконечно и позволяет вернуть ошибку вместо зависания, конструктор Semaphore(permits, true) включает честную очередь, acquire(n) берёт несколько разрешений сразу.
Главное отличие от ReentrantLock: у семафора нет владельца, отпустить разрешение может любой поток. Повторного входа тоже нет: поток, который второй раз просит единственное разрешение, заблокирует сам себя.
Правильный шаблон: acquire() до блока try, release() в finally.
Где спотыкаются
Вызывают acquire() внутри try. Если поток прервут во время ожидания, finally выполнит release() без взятого разрешения, и лимит незаметно вырастет на единицу.
Путают ограничение одновременности с ограничением частоты. Семафор на 10 не значит «10 запросов в секунду»: при коротких вызовах их пройдёт сколько угодно. Для частоты нужен rate limiter.
Путают с CountDownLatch. Защёлка одноразовая и нужна, чтобы дождаться N событий, а семафор многократно раздаёт и принимает обратно разрешения.