Semaphore.acquire(): примеры (JAVA)

Использование acquire в java.util.concurrent.Semaphore
Раздел: Многопоточность, Семафоры
Semaphore.acquire(): void

Описание метода Semaphore.acquire()

Метод Semaphore.acquire() из пакета java.util.concurrent предназначен для получения одного разрешения (permit) из счётчика семафора. Если разрешение доступно, вызов возвращает немедленно и уменьшает число доступных разрешений на единицу. Если разрешений нет, поток блокируется до тех пор, пока разрешение не станет доступно или пока поток не будет прерван.

Сигнатуры основных методов, связанных с получением разрешений:

  • void acquire() throws InterruptedException - получить одно разрешение, блокируясь и реагируя на прерывание.
  • void acquire(int permits) throws InterruptedException - получить указанное число разрешений; при отрицательном аргументе выбрасывается IllegalArgumentException.
  • void acquireUninterruptibly() - получить одно разрешение, игнорируя прерывания (не выбрасывает InterruptedException).

Возвращаемое значение: все перечисленные версии возвращают void. Успешное завершение означает, что разрешение(я) получены. Вариант с прерываемым ожиданием может завершиться исключением InterruptedException, если поток был прерван во время блокировки.

Поведение в режиме справедливости: при создании семафора можно указать параметр fair. В справедливом режиме (fair = true) очередность получения разрешений выполняется в порядке ожидания. В несостоятельном режиме (fair = false) порядок может быть менее предсказуемым, но доступ может быть более производительным.

Связанные методы: release() и release(int) для возвращения разрешений, tryAcquire() и tryAcquire(long, TimeUnit) для неглокирующего или таймаутного получения, availablePermits() для просмотра текущего числа разрешений, getQueueLength() и hasQueuedThreads() для диагностики очереди потоков.

Когда применяется

Метод удобен при ограничении количества одновременно выполняемых задач, при организации пула одинаковых ресурсов (например, соединений) или при реализации простых механизмов синхронизации без явного блокирования на условных переменных.

Примеры базового применения

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

Пример 1. Базовый acquire()

import java.util.concurrent.*;

Semaphore s = new Semaphore(1);
Thread t = new Thread(() -> {
    try {
        s.acquire();
        System.out.println("Занял разрешение");
        Thread.sleep(200);
        s.release();
        System.out.println("Освободил разрешение");
    } catch (InterruptedException e) {
        System.out.println("Прерван");
    }
});
t.start();
Thread.sleep(50);
System.out.println("Главный поток ждёт");
s.acquire();
System.out.println("Главный поток получил разрешение");
s.release();
Занял разрешение
Главный поток ждёт
Освободил разрешение
Главный поток получил разрешение

Пример 2. acquire(int permits)

Semaphore s = new Semaphore(3);

// Потребление двух разрешений
s.acquire(2);
System.out.println("Осталось " + s.availablePermits() + " разрешений");
// Возврат двух разрешений
s.release(2);
System.out.println("После release: " + s.availablePermits());
Осталось 1 разрешений
После release: 3

Пример 3. acquireUninterruptibly()

Semaphore s = new Semaphore(0);
Thread t = new Thread(() -> {
    s.acquireUninterruptibly();
    System.out.println("Поток получил разрешение без реакции на прерывание");
});
t.start();
Thread.sleep(50);
t.interrupt();
// поток всё ещё будет ждать, пока кто-то не вызовет release()
(без вывода до вызова release())

Похожие механизмы в Java

  • ReentrantLock с Condition - более низкоуровневый контроль блокировок и ожидания, удобен для сложных условных схем; предпочтителен при потребности в явном захвате/освобождении локов и проверке состояния.
  • CountDownLatch - одноразовый счётчик ожиданий, полезен для ожидания завершения набора задач; не позволяет вернуть "разрешения" после снижения счётчика.
  • CyclicBarrier - барьер для синхронизации фиксированного числа потоков; применяется для синхронных этапов, не для ограничения параллелизма.
  • BlockingQueue - можно использовать как пул ресурсов: извлечение элемента равно получению разрешения, вставка - возврат; удобен, когда требуется передача объектов и управление доступом одновременно.

Выбор зависит от задачи: для простого ограничения параллелизма семафор чаще предпочтительней; для управляемой передачи и повторного использования ресурсов удобнее использовать очередь.

Аналоги в других языках

Python

# threading
from threading import Semaphore, Thread
s = Semaphore(2)

def worker(n):
    s.acquire()
    print('start', n)
    s.release()

Thread(target=worker, args=(1,)).start()
start 1

В asyncio доступна asyncio.Semaphore, поддерживающая асинхронное ожидание через await sem.acquire().

JavaScript (Node.js и браузер)

В языке нет встроенного семафора. Часто реализуется на промисах или через сторонние пакеты.

// Простой счётный семафор на промисах
class Semaphore {
  constructor(count) { this.count = count; this.queue = []; }
  acquire() {
    if (this.count > 0) { this.count--; return Promise.resolve(); }
    return new Promise(res => this.queue.push(res));
  }
  release() { this.count++; if (this.queue.length) this.queue.shift()(); }
}
(вызов acquire возвращает промис; при release промис резолвится)

PHP

Доступ к системным семафорам возможен через расширение SysV (функции sem_get, sem_acquire). В модуле pthreads доступны примитивы для многопоточности в CLI.

$sem = sem_get(ftok(__FILE__, 'a'), 1);
sem_acquire($sem);
echo "Занято\n";
sem_release($sem);
Занято

C#

// Синхронно
var sem = new System.Threading.SemaphoreSlim(2);
await sem.WaitAsync(); // асинхронный аналог acquire
sem.Release();
(ожидание и освобождение выполняются как в Java)

Различие: SemaphoreSlim легче и имеет асинхронные методы; класс Semaphore работает между процессами через дескриптор ядра.

Go

Шаблон ограничителя параллелизма реализуется через буферные каналы или через пакет golang.org/x/sync/semaphore.

// Буферный канал
sem := make(chan struct{}, 3)
sem <- struct{}{} // acquire
<-sem // release
(пополнение канала блокирует при заполнении, чтение освобождает слот)

Kotlin

Используется Java-реализация Semaphore или корутинные механизмы (Channel, Semaphore из kotlinx.coroutines).

Lua и прочие

В большинстве интерпретируемых языков семафоры реализуются через сторонние библиотеки или через низкоуровневые примитивы ОС. В SQL прямых семафоров нет; в PostgreSQL применяются advisory locks (pg_advisory_lock), обеспечивающие глобальную блокировку по ключу, похожую на семафор с одним разрешением.

-- PostgreSQL
SELECT pg_advisory_lock(42);
-- критическая секция
SELECT pg_advisory_unlock(42);
(заблокирует идентификатор 42 до разблокировки)

Типичные ошибки и их последствия

  • Забыть вызвать release()
  • Если после успешного acquire не вызывается release, ресурсы не возвращаются и возможна блокировка других потоков. Пример:

    Semaphore s = new Semaphore(1);
    try {
        s.acquire();
        // исключение здесь прервет выполнение до release
        throw new RuntimeException("ошибка");
    } finally {
        // отсутствие finally приведёт к утечке разрешения
    }
    Если release не вызван - другие потоки могут навсегда заблокироваться
  • Игнорирование InterruptedException
  • Игнорирование прерываний может привести к некорректной логике завершения потоков. Правильнее обрабатывать прерывание и при необходимости восстанавливать состояние.

  • Передача отрицательного аргумента в acquire(int)
  • Semaphore s = new Semaphore(1);
    s.acquire(-1);
    java.lang.IllegalArgumentException: permits < 0
  • Неправильное сочетание fairness и производительности
  • Указание справедливого режима может устранить голодание, но снизит пропускную способность. Выбор влияет на порядок обслуживания потоков.

  • Чрезмерный выпуск release()
  • Вызов release больше раз, чем было acquire, приведет к увеличению числа разрешений сверх первоначального ожидания; это не вызывает исключения, но нарушает логику ограничения параллелизма.

Изменения и совместимость

Класс java.util.concurrent.Semaphore и его метод acquire() доступны с Java 5 и остаются стабильными в последующих версиях. API не претерпевал значительных изменений в последних релизах, за исключением появления вспомогательных средств в экосистеме (например, расширения асинхронных библиотек в других языках и вспомогательных утилит в сторонних библиотеках). Совместимость обратная: код, использующий Semaphore, продолжит работать в современных версиях JVM.

Расширенные и нетипичные сценарии применения

1. Ограничитель одновременных обращений к внешнему сервису

Пример java
import java.util.concurrent.*;

Semaphore sem = new Semaphore(5); // максимум 5 параллельных запросов
ExecutorService es = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
    final int id = i;
    es.submit(() -> {
        try {
            sem.acquire();
            System.out.println("Запрос " + id + " выполняется");
            Thread.sleep(200); // эмуляция запроса
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            sem.release();
        }
    });
}
es.shutdown();
es.awaitTermination(5, TimeUnit.SECONDS);
(Вывод показывает максимум 5 одновременных "Запрос ... выполняется" в любой момент)

2. Реализация пула подключений через Semaphore и очередь

Пример java
// Пул: семафор контролирует число занятых соединений, очередь хранит свободные объекты
class ConnectionPool {
    private final Semaphore sem;
    private final BlockingQueue pool;
    ConnectionPool(int size) {
        sem = new Semaphore(size);
        pool = new ArrayBlockingQueue<>(size);
        for (int i = 0; i < size; i++) pool.add("conn-" + i);
    }
    String acquireConnection() throws InterruptedException {
        sem.acquire();
        return pool.take();
    }
    void releaseConnection(String c) {
        pool.offer(c);
        sem.release();
    }
}
(Гарантирует, что одновременно выдается не более настроенного числа соединений)

3. tryAcquire с таймаутом для отказа без блокировки

Пример java
Semaphore s = new Semaphore(1);
if (s.tryAcquire(100, TimeUnit.MILLISECONDS)) {
    try { /* работа */ } finally { s.release(); }
} else {
    System.out.println("Не удалось получить разрешение за таймаут");
}
Не удалось получить разрешение за таймаут (если занято другим потоком)

4. Комбинирование с CompletableFuture и ограничение числа асинхронных задач

Пример java
Semaphore sem = new Semaphore(10);
ExecutorService pool = Executors.newFixedThreadPool(20);
List();
for (int i = 0; i < 50; i++) {
    final int id = i;
    sem.acquireUninterruptibly();
    CompletableFuture<String> f = CompletableFuture.supplyAsync(() -> {
        try { Thread.sleep(100); return "res-" + id; }
        catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; }
        finally { sem.release(); }
    }, pool);
    futures.add(f);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
System.out.println("Все задачи завершены");
pool.shutdown();
Все задачи завершены

5. Использование fairness для упорядочивания доступа

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

6. Вариант для микропроцессинга - имитация семафора через AtomicInteger

Пример java
// Нежелательный, но возможный подход без блокировки
import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger permits = new AtomicInteger(3);
while (true) {
    int cur = permits.get();
    if (cur > 0 && permits.compareAndSet(cur, cur - 1)) {
        try {
            // работа
        } finally {
            permits.incrementAndGet();
        }
        break;
    }
    // спин или уступка процессора
}
(Такая реализация сложнее и подвержена ошибкам; предпочтительнее использовать готовый Semaphore)

джава Semaphore.acquire() function comments

En
Semaphore.acquire() Запрашивает разрешение (уменьшает счетчик)