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

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

Описание метода

Метод Semaphore.release() из пакета java.util.concurrent освобождает одно разрешение (permit) семафора, увеличивая число доступных разрешений. Существует также перегрузка release(int permits), которая добавляет указанное количество разрешений. Оба метода возвращают void и не блокируют вызывающий поток.

Типичные сценарии использования: управление количеством одновременно работающих потоков (пул ограниченного размера), реализация пула ресурсов, синхронизация доступа к ограниченным внешним ресурсам.

Поведение и важные детали:

  • Если вызов release() выполняется без предварительного acquire() тем же потоком, число доступных разрешений увеличится - это законное поведение, исключений не генерируется.
  • Методы не привязаны к владельцу потока; семафор не отслеживает, кто брал разрешение.
  • Перегрузка release(int permits) добавляет указанное количество разрешений; при передаче отрицательного числа обычно генерируется IllegalArgumentException (некорректное значение).
  • Семафор может быть создан с флагом справедливости (fair): в справедливом режиме ожидающие потоки получают разрешения в порядке FIFO; в несчастедливом режиме порядок не гарантируется.
  • Связанные методы: acquire(), tryAcquire(), availablePermits(), drainPermits(). Метод release() не возвращает значения.

Аргументы и возвращаемые значения:

  • release() - без аргументов, возвращает void.
  • release(int permits) - аргумент permits типа int, ожидается неотрицательное число; метод возвращает void.

Замечание о корректности: семафор не поддерживает «максимум» разрешений по умолчанию: число доступных разрешений может расти произвольно при последовательных вызовах release(), если явно не ограничивать это поведение дополнительной логикой.

Короткие примеры использования

Примеры демонстрируют типовые варианты применения методов release() и release(int).

1) Пример: простое acquire/release.

import java.util.concurrent.*;

public class SimpleRelease {
  public static void main(String[] args) throws Exception {
    Semaphore s = new Semaphore(1);
    s.acquire();
    System.out.println("acquired, available=" + s.availablePermits());
    s.release();
    System.out.println("released, available=" + s.availablePermits());
  }
}
acquired, available=0
released, available=1

2) Пример: release(int) добавляет несколько разрешений.

import java.util.concurrent.*;

public class ReleaseMultiple {
  public static void main(String[] args) {
    Semaphore s = new Semaphore(0);
    s.release(3);
    System.out.println("available=" + s.availablePermits());
  }
}
available=3

3) Пример: вызов release без предварительного acquire увеличивает доступные разрешения.

import java.util.concurrent.*;

public class ReleaseWithoutAcquire {
  public static void main(String[] args) {
    Semaphore s = new Semaphore(1);
    s.release();
    System.out.println("available=" + s.availablePermits());
  }
}
available=2

4) Пример: неправильный аргумент для release(int) (пример генерации исключения).

import java.util.concurrent.*;

public class ReleaseInvalid {
  public static void main(String[] args) {
    Semaphore s = new Semaphore(1);
    s.release(-1); // некорректное значение
  }
}
Exception in thread "main" java.lang.IllegalArgumentException: permits < 0
    at java.base/java.util.concurrent.Semaphore.release(Semaphore.java:...)
    ...

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

Вместо семафора иногда применяются другие примитивы синхронизации в зависимости от задачи:

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

Выбор между этими средствами зависит от требований: нужна ли повторяемость (CyclicBarrier), одноразовость (CountDownLatch), управление количеством параллельных исполнений (Semaphore или BlockingQueue) или тонкая логика ожидания (ReentrantLock).

Соответствующие конструкции в других языках

Краткое сравнение и примеры для популярных языков.

  • Python (threading)
    import threading
    s = threading.Semaphore(1)
    s.acquire()
    print('acquired')
    s.release()
    print('released')
    acquired
    released
    Отличие: метод release() также не проверяет владельца потока; есть BoundedSemaphore, ограничивающий максимум.
  • Python (asyncio)
    import asyncio
    
    async def worker(sem):
        await sem.acquire()
        print('acquired')
        sem.release()
        print('released')
    
    asyncio.run(worker(asyncio.Semaphore(1)))
    acquired
    released
    Отличие: методы используются в асинхронном контексте; acquire() - корутина, release() - обычный метод.
  • C#
    using System;
    using System.Threading;
    
    class Program {
      static void Main() {
        var s = new SemaphoreSlim(1);
        s.Wait();
        Console.WriteLine("acquired");
        s.Release();
        Console.WriteLine("released");
      }
    }
    acquired
    released
    Примечание: есть Semaphore (системный) и SemaphoreSlim (легковесный).
  • Go
    package main
    
    import "fmt"
    
    func main() {
      sem := make(chan struct{}, 1)
      // acquire
      sem <- struct{}{}
      fmt.Println("acquired")
      // release
      <-sem
      fmt.Println("released")
    }
    acquired
    released
    В Go часто используются буферизированные каналы как семафор; нет встроенного типа Semaphore в stdlib.
  • Kotlin (корутины)
    import kotlinx.coroutines.*
    import kotlinx.coroutines.sync.Semaphore
    import kotlinx.coroutines.sync.withPermit
    
    fun main() = runBlocking {
      val s = Semaphore(1)
      s.acquire()
      println("acquired")
      s.release()
      println("released")
    }
    acquired
    released
    Отличие: у kotlinx.coroutines.sync.Semaphore методы являются suspend-функциями для acquire; release - обычный метод.
  • PHP
    // Требуется расширение sysvsem
    $sem = sem_get(ftok(__FILE__, 'a'), 1);
    sem_acquire($sem);
    echo "acquired\n";
    sem_release($sem);
    echo "released\n";
    acquired
    released
    Замечание: доступ к системным семафорам зависит от платформы и конфигурации.
  • Lua
    -- Простой пример с флагом и очередью (псевдокод)
    -- Реальная реализация основана на библиотеках для потоков
    
    (зависит от используемой библиотеки)
    Часто используются сторонние библиотеки или механизмы очередей.
  • SQL
    -- В SQL нет встроенного семафора; используются блокировки таблиц или записей
    SELECT pg_try_advisory_lock(1);
    (true или false в PostgreSQL)
    В реляционных СУБД применяются advisory locks или транзакционные блокировки вместо семафора.

Типичные ошибки и примеры

Частые просчеты при использовании release() и их проявления.

  • Ожидание совпадения acquire/release: предположение, что release() должен вызывать тот же поток, который сделал acquire(). Это неверно; семафор не отслеживает владельцев. Следствие: неверные проверки логики при попытке отследить «кто» освободил ресурс.
  • Отрицательные аргументы: вызов release(-n) приводит к исключению. Пример:
import java.util.concurrent.*;

public class BadReleaseArg {
  public static void main(String[] args) {
    Semaphore s = new Semaphore(1);
    s.release(-2);
  }
}
Exception in thread "main" java.lang.IllegalArgumentException: permits < 0
    at java.base/java.util.concurrent.Semaphore.release(Semaphore.java:...)
    ...
  • Потеря вызова release(): забытый вызов в блоке finally приводит к утечке «захваченных» разрешений, из-за чего другие потоки блокируются.
  • Неправильное ожидание порядка: в несчастливом (non-fair) семафоре порядок предоставления разрешений может отличаться от FIFO; для строгого порядка требуется флаг справедливости при создании.
  • Переполнение логики: без использования ограничений число доступных разрешений может вырасти больше, чем планировалось (последовательный вызов release() без соответствующих acquire()).

Рекомендация по поводу ошибок: вызывать release() в блоке finally после успешного acquire(), проверять входные аргументы для release(int) и при необходимости использовать BoundedSemaphore (аналогично) либо дополнительную логику для ограничения максимума.

Изменения в API и версиях

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

Для корутинных библиотек (например, Kotlin с kotlinx.coroutines) семафоры реализованы отдельно и имеют особенности интеграции с suspend-функциями.

Расширенные и редкие сценарии применения

Несколько продвинутых примеров использования семафора в реальных или нестандартных сценариях.

1) Реализация пула соединений с ограничением одновременных подключений.

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

class Connection {}

public class ConnectionPool {
  private final Semaphore sem;
  private final Queue pool = new ArrayDeque<>();

  public ConnectionPool(int max) {
    sem = new Semaphore(max);
    for (int i = 0; i < max; i++) pool.add(new Connection());
  }

  public Connection acquire() throws InterruptedException {
    sem.acquire();
    synchronized (pool) { return pool.poll(); }
  }

  public void release(Connection c) {
    synchronized (pool) { pool.add(c); }
    sem.release();
  }

  public static void main(String[] args) throws Exception {
    ConnectionPool cp = new ConnectionPool(2);
    Connection c1 = cp.acquire();
    Connection c2 = cp.acquire();
    System.out.println("two acquired");
    // третий вызов будет ждать, пока не освободят
    new Thread(() -> {
      try { Connection c3 = cp.acquire(); System.out.println("third acquired"); cp.release(c3); }
      catch (InterruptedException e) { }
    }).start();
    Thread.sleep(500);
    cp.release(c1);
    System.out.println("one released");
  }
}
two acquired
one released
third acquired

Пояснение: семафор гарантирует, что одновременно используются не более N ресурсов; очередь хранения ресурсов синхронизуется отдельно.

2) Реализация простого rate limiter с помощью семафора и периодического пополнения.

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

public class RateLimiter {
  private final Semaphore sem;
  private final ScheduledExecutorService sched = Executors.newSingleThreadScheduledExecutor();

  public RateLimiter(int permitsPerSecond) {
    sem = new Semaphore(permitsPerSecond);
    sched.scheduleAtFixedRate(() -> {
      // пополнение до permitsPerSecond
      int toAdd = permitsPerSecond - sem.availablePermits();
      if (toAdd > 0) sem.release(toAdd);
    }, 1, 1, TimeUnit.SECONDS);
  }

  public boolean tryAcquire() { return sem.tryAcquire(); }
}

// Использование: проверять tryAcquire() перед выполнением операции
(вывод зависит от использования; лимитирует количество действий в секунду)

Пояснение: release(int) используется для массового добавления разрешений; важно учитывать гонки и корректно рассчитывать необходимое пополнение.

3) Справедливость: тест, демонстрирующий порядок в fair и non-fair семафорах.

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

public class FairnessDemo {
  public static void demo(boolean fair) throws InterruptedException {
    Semaphore s = new Semaphore(1, fair);
    Runnable r = () -> {
      try {
        long id = Thread.currentThread().getId();
        System.out.println("thread " + id + " waiting");
        s.acquire();
        System.out.println("thread " + id + " acquired");
        Thread.sleep(100);
        s.release();
      } catch (InterruptedException e) {}
    };
    for (int i = 0; i < 5; i++) new Thread(r).start();
    Thread.sleep(1500);
  }
  public static void main(String[] args) throws Exception {
    System.out.println("non-fair:");
    demo(false);
    System.out.println("fair:");
    demo(true);
  }
}
non-fair:
thread 12 waiting
thread 13 waiting
thread 14 waiting
thread 15 waiting
thread 16 waiting
thread 13 acquired
thread 12 acquired
...
fair:
thread 12 waiting
thread 13 waiting
thread 14 waiting
thread 15 waiting
thread 16 waiting
thread 12 acquired
thread 13 acquired
...

Пояснение: в non-fair режиме порядок может меняться, в fair режиме ожидающие потоки обслуживаются по очереди.

4) Редкий сценарий: повышение разрешений для обработки burst-запросов. При необходимости разрешения можно временно увеличить с помощью release(n), затем вручную уменьшить через drainPermits() или другую логику.

Пример java
// Псевдокод: увеличить допустимый параллелизм на время всплеска
sem.release(5); // временно дать больше слотов
// обработать всплеск
// затем уменьшить: sem.reducePermits(5); // protected, требует расширения класса
(результат зависит от реализации; важно учитывать thread-safety)

Замечание: метод reducePermits в стандартной реализации является защитным и применяется в специальных подклассах или при расширении класса.

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

En
Semaphore.release() Освобождает разрешение (увеличивает счетчик)