← Назад к вопросам

Для чего нужен Semaphore?

1.0 Junior🔥 161 комментариев
#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Для чего нужен Semaphore?

Semaphore — это синхронизационный примитив в Java, который контролирует доступ к ограниченному количеству ресурсов. Это инструмент для управления количеством потоков, имеющих доступ к определенному ресурсу.

Определение

Semaphore содержит счетчик (permit count), который показывает сколько потоков могут одновременно использовать ресурс.

import java.util.concurrent.Semaphore;

// Создаем семафор с 3 разрешениями
Semaphore semaphore = new Semaphore(3);

// Один поток занимает разрешение
semaphore.acquire();   // count: 3 → 2

// Другой поток занимает разрешение
semaphore.acquire();   // count: 2 → 1

// Третий поток занимает разрешение
semaphore.acquire();   // count: 1 → 0

// Четвёртый поток ЖДЁТ (разрешений нет!)
semaphore.acquire();   // БЛОКИРУЕТСЯ (count = 0)

// Первый поток освобождает разрешение
semaphore.release();   // count: 0 → 1

// Четвёртый поток может продолжить
// semaphore.acquire() завершается

Типы Semaphore

1. Binary Semaphore (0 или 1)

Равносильно Mutex — только один поток может использовать ресурс:

Semaphore binaryLock = new Semaphore(1);

Thread t1 = new Thread(() -> {
    try {
        binaryLock.acquire();      // Занимает
        System.out.println("T1 работает");
        Thread.sleep(2000);
        binaryLock.release();       // Освобождает
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

Thread t2 = new Thread(() -> {
    try {
        binaryLock.acquire();      // ЖДЁТ, пока T1 освободит
        System.out.println("T2 работает");
        binaryLock.release();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

t1.start();
t2.start();

// Вывод:
// T1 работает (T2 ждет 2 сек)
// T2 работает

2. Counting Semaphore (N > 1)

Дозволяет N потокам одновременно использовать ресурс:

// Пул из 3 потоков
Semaphore poolSize = new Semaphore(3);

// 3 потока работают одновременно
// 4-й ждёт пока один из первых трёх освободится

Практический пример: Пул ресурсов

Представь, что у тебя есть 3 принтера и 10 сотрудников:

public class PrinterPool {
    private final Semaphore availablePrinters = new Semaphore(3);
    
    public void print(String document) throws InterruptedException {
        availablePrinters.acquire();  // Занимаешь принтер
        try {
            System.out.println(Thread.currentThread().getName() + " печатает " + document);
            Thread.sleep(2000);        // Печать (2 сек)
            System.out.println(Thread.currentThread().getName() + " закончил печать");
        } finally {
            availablePrinters.release();  // Освобождаешь принтер
        }
    }
}

// Использование
PrinterPool pool = new PrinterPool();

for (int i = 0; i < 10; i++) {
    final int docNumber = i;
    new Thread(() -> {
        try {
            pool.print("Document " + docNumber);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }, "Employee-" + i).start();
}

// Вывод (примерный):
// Employee-0 печатает Document 0
// Employee-1 печатает Document 1
// Employee-2 печатает Document 2
// Employee-3 ЖДЁТ (нет свободных принтеров)
// Employee-4 ЖДЁТ
// ...
// Employee-0 закончил печать
// Employee-3 печатает Document 3  ← теперь может использовать принтер

Пример: Ограничение подключений к БД

public class DatabaseConnectionPool {
    private final Semaphore semaphore;
    private final Queue<Connection> availableConnections = new ConcurrentLinkedQueue<>();
    
    public DatabaseConnectionPool(int maxConnections) throws SQLException {
        semaphore = new Semaphore(maxConnections);
        
        // Инициализируем пул
        for (int i = 0; i < maxConnections; i++) {
            availableConnections.add(createConnection());
        }
    }
    
    public Connection getConnection() throws InterruptedException {
        semaphore.acquire();  // Ждем свободное соединение
        
        Connection conn = availableConnections.poll();
        if (conn == null) {
            // Это не должно случиться, но на всякий случай
            semaphore.release();
            throw new RuntimeException("Нет соединения в пуле");
        }
        
        return conn;
    }
    
    public void releaseConnection(Connection conn) {
        availableConnections.offer(conn);
        semaphore.release();  // Освобождаем разрешение
    }
    
    private Connection createConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:mysql://localhost:3306/db");
    }
}

// Использование
public void queryDatabase() {
    DatabaseConnectionPool pool = new DatabaseConnectionPool(5);
    
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            try {
                Connection conn = pool.getConnection();
                try {
                    // Выполняем запрос
                    System.out.println("Выполняю запрос...");
                    Thread.sleep(1000);
                } finally {
                    pool.releaseConnection(conn);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
    }
}

// Максимум 5 потоков работают одновременно
// Остальные 15 ждут

Semaphore vs ReentrantLock

ReentrantLock:

ReentrantLock lock = new ReentrantLock();

lock.lock();      // Один поток
try {
    // критическая секция
} finally {
    lock.unlock();
}
// Только 1 поток одновременно (бинарный семафор)

Semaphore (3 потока):

Semaphore sem = new Semaphore(3);

sem.acquire();    // До 3 потоков
try {
    // критическая секция
} finally {
    sem.release();
}
// До 3 потоков одновременно

Пример: Rate Limiting (ограничение частоты)

public class RateLimiter {
    private final Semaphore semaphore;
    private final long refillInterval;
    private final int tokensPerInterval;
    
    public RateLimiter(int tokensPerSecond) {
        this.semaphore = new Semaphore(tokensPerSecond);
        this.tokensPerInterval = tokensPerSecond;
        this.refillInterval = 1000;  // 1 секунда
        
        // Поток, который пополняет токены каждую секунду
        new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(refillInterval);
                    semaphore.release(tokensPerInterval);
                } catch (InterruptedException e) {
                    break;
                }
            }
        }).setDaemon(true);
    }
    
    public void acquire() throws InterruptedException {
        semaphore.acquire();  // Занимаем один токен
    }
}

// Использование
RateLimiter limiter = new RateLimiter(5);  // 5 запросов в секунду

for (int i = 0; i < 20; i++) {
    limiter.acquire();
    System.out.println("Запрос " + i);
}
// Первые 5 запросов выполнятся сразу
// Следующие 5 дождутся следующей секунды
// Итого: 20 запросов за 4 секунды

fairness параметр

Визубережень порядок получения разрешений:

// Несправедливый (неопределённый порядок)
Semaphore unfair = new Semaphore(3);

// Справедливый (FIFO)
Semaphore fair = new Semaphore(3, true);

// С fair=true потоки получают разрешения в порядке очереди
// С fair=false быстрее, но может быть "голодание" потока

Методы Semaphore

Semaphore sem = new Semaphore(5);

// Основные
sem.acquire();           // Занимает 1 разрешение (блокирует)
sem.release();           // Освобождает 1 разрешение
sem.tryAcquire();        // Пытается занять, возвращает boolean
sem.tryAcquire(2);       // Занимает 2 разрешения

// С timeout
sem.tryAcquire(1, TimeUnit.SECONDS);

// Информация
int available = sem.availablePermits();  // Сколько свободных
int queue = sem.getQueueLength();        // Сколько потоков ждёт

// Batch операции
sem.acquire(3);          // Занимает 3 разрешения
sem.release(3);          // Освобождает 3 разрешения

Потенциальные проблемы

1. Забыл release()

// ПЛОХО - разрешения никогда не освобождаются
for (int i = 0; i < 10; i++) {
    semaphore.acquire();
    // Забыли semaphore.release()!
}
// Все разрешения закончились

// ХОРОШО - используй try-finally
semaphore.acquire();
try {
    // работа
} finally {
    semaphore.release();
}

2. Дедлок

// Может привести к дедлоку
Semaphore sem1 = new Semaphore(1);
Semaphore sem2 = new Semaphore(1);

Thread t1 = new Thread(() -> {
    sem1.acquire();  // T1 захватил sem1
    sem2.acquire();  // T1 ждет sem2 (T2 её держит)
});

Thread t2 = new Thread(() -> {
    sem2.acquire();  // T2 захватил sem2
    sem1.acquire();  // T2 ждет sem1 (T1 её держит)
    // ДЕДЛОК!
});

3. Исключение прерывает работу

// ПЛОХО - исключение может произойти до release()
semaphore.acquire();
int result = riskyOperation();  // Может выбросить исключение
semaphore.release();  // Не выполнится!

// ХОРОШО
semaphore.acquire();
try {
    int result = riskyOperation();
} finally {
    semaphore.release();
}

Альтернативы

// Вместо Semaphore можно использовать:

// 1. CountDownLatch - для ожидания события
CountDownLatch latch = new CountDownLatch(3);
latch.await();   // Ждать пока счетчик не станет 0

// 2. CyclicBarrier - для синхронизации группы потоков
CyclicBarrier barrier = new CyclicBarrier(3);
barrier.await(); // Все 3 потока ждут друг друга

// 3. BlockingQueue - для ограничения размера очереди
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(10);
queue.put(task);  // Блокируется если очередь полная

// 4. ThreadPoolExecutor - встроенный пул с управлением
ExecutorService executor = Executors.newFixedThreadPool(3);

Заключение

Semaphore нужен для:

  • Ограничения количества потоков, использующих ресурс
  • Управления пулами ресурсов (соединения, потоки)
  • Rate limiting (ограничение частоты)
  • Синхронизации доступа к ограниченным ресурсам

Особенности:

  • Counting Semaphore (N потоков) vs Binary Semaphore (1 поток)
  • Всегда используй try-finally для release()
  • Помни про InterruptedException
  • Для rate limiting создавай поток, который пополняет токены
  • В современном коде часто предпочитают BlockingQueue или pools
Для чего нужен Semaphore? | PrepBro