Комментарии (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