← Назад к вопросам
Как семафор реализует ожидание потоком завершения работы других потоков
2.0 Middle🔥 201 комментариев
#Многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Семафоры в Java: синхронизация потоков
Что такое семафор?
Семафор (Semaphore) — это синхронизационный примитив, который управляет доступом к ресурсу через счётчик. Он позволяет одновременно работать N потокам с общим ресурсом.
Семафор имеет:
- Счётчик — количество доступных разрешений
- acquire() — попытаться получить разрешение (счётчик --)
- release() — вернуть разрешение (счётчик ++)
Как работает семафор
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
// Семафор с 2 разрешениями (максимум 2 потока одновременно)
static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) throws InterruptedException {
// Создаём 5 потоков
for (int i = 1; i <= 5; i++) {
new Thread(new Worker(i)).start();
}
}
static class Worker implements Runnable {
int id;
Worker(int id) { this.id = id; }
@Override
public void run() {
try {
System.out.println("Thread " + id + " waiting...");
semaphore.acquire(); // Получить разрешение
System.out.println("Thread " + id + " acquired resource");
Thread.sleep(2000); // Работаем с ресурсом 2 сек
System.out.println("Thread " + id + " releasing resource");
semaphore.release(); // Вернуть разрешение
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// Вывод:
// Thread 1 waiting...
// Thread 2 waiting...
// Thread 3 waiting...
// Thread 4 waiting...
// Thread 5 waiting...
// Thread 1 acquired resource
// Thread 2 acquired resource
// (Thread 3, 4, 5 ждут)
// Thread 1 releasing resource
// Thread 3 acquired resource
// (Thread 4, 5 ждут)
// ...
Здесь:**
- Семафор имеет 2 разрешения
- Максимум 2 потока могут одновременно работать
- Остальные ждут своей очереди
Структура семафора внутри
Когда поток вызывает acquire():
┌─────────────────────────────────┐
│ Semaphore(permits=2) │
├─────────────────────────────────┤
│ Permits: 2 (начально) │
│ │
│ Thread 1: acquire() │
│ → Permits: 1 (разрешение дано)│
│ │
│ Thread 2: acquire() │
│ → Permits: 0 (разрешение дано)│
│ │
│ Thread 3: acquire() │
│ → Permits: 0, заблокирован! │
│ (ждёт в очереди) │
│ │
│ Thread 1: release() │
│ → Permits: 1 (разрешение │
│ вернулось, разблокирован T3) │
└─────────────────────────────────┘
Три способа ждать завершения
1. Семафор для ожидания завершения
Semaphore semaphore = new Semaphore(0); // 0 разрешений
// Главный поток
public static void main(String[] args) throws InterruptedException {
// Создаём рабочие потоки
Thread worker1 = new Thread(() -> {
System.out.println("Worker 1 started");
// ... работа ...
System.out.println("Worker 1 finished");
semaphore.release(); // Освобождаем разрешение
});
Thread worker2 = new Thread(() -> {
System.out.println("Worker 2 started");
// ... работа ...
System.out.println("Worker 2 finished");
semaphore.release();
});
worker1.start();
worker2.start();
// Ждём завершения обоих потоков
semaphore.acquire(); // Ждём первого release()
semaphore.acquire(); // Ждём второго release()
System.out.println("All workers finished!");
}
// Вывод:
// Worker 1 started
// Worker 2 started
// Worker 1 finished
// Worker 2 finished
// All workers finished!
2. CountDownLatch (более удобно для ожидания)
import java.util.concurrent.CountDownLatch;
CountDownLatch latch = new CountDownLatch(2); // Ждём 2 события
public static void main(String[] args) throws InterruptedException {
Thread worker1 = new Thread(() -> {
System.out.println("Worker 1 started");
// ... работа ...
System.out.println("Worker 1 finished");
latch.countDown(); // Уменьшить счётчик
});
Thread worker2 = new Thread(() -> {
System.out.println("Worker 2 started");
// ... работа ...
System.out.println("Worker 2 finished");
latch.countDown();
});
worker1.start();
worker2.start();
latch.await(); // Ждёмпока счётчик не станет 0
System.out.println("All workers finished!");
}
3. CyclicBarrier (для синхронизации нескольких потоков)
import java.util.concurrent.CyclicBarrier;
CyclicBarrier barrier = new CyclicBarrier(3); // Ждём 3 потока
public static void main(String[] args) throws Exception {
Thread[] workers = new Thread[3];
for (int i = 0; i < 3; i++) {
final int id = i + 1;
workers[i] = new Thread(() -> {
try {
System.out.println("Thread " + id + " ready");
barrier.await(); // Ждём остальные потоки
System.out.println("Thread " + id + " go!");
} catch (Exception e) {
e.printStackTrace();
}
});
workers[i].start();
}
}
// Вывод:
// Thread 1 ready
// Thread 2 ready
// Thread 3 ready
// Thread 1 go!
// Thread 2 go!
// Thread 3 go!
// (все начинают одновременно)
Различие между синхронизационными примитивами
| Примитив | Назначение | Счётчик |
|---|---|---|
| Semaphore | Контроль доступа к ресурсам | Начальное > 0 |
| CountDownLatch | Ожидание события N раз | Начальное > 0, только вниз |
| CyclicBarrier | Синхронизация N потоков | Может использоваться повторно |
| ReentrantLock | Взаимное исключение | Логический флаг |
Практический пример: ограничение одновременного доступа
import java.util.concurrent.Semaphore;
public class ConnectionPool {
static class DatabaseConnection {
int id;
DatabaseConnection(int id) { this.id = id; }
}
private static final Semaphore semaphore = new Semaphore(3); // 3 соединения
private static final DatabaseConnection[] connections = new DatabaseConnection[3];
static {
for (int i = 0; i < 3; i++) {
connections[i] = new DatabaseConnection(i + 1);
}
}
public static DatabaseConnection getConnection() throws InterruptedException {
semaphore.acquire(); // Получить разрешение
synchronized (connections) {
for (DatabaseConnection conn : connections) {
if (conn != null) {
DatabaseConnection temp = conn;
conn = null; // Пометить как занято
return temp;
}
}
}
return null; // Никогда не случится
}
public static void releaseConnection(DatabaseConnection conn) {
synchronized (connections) {
for (int i = 0; i < connections.length; i++) {
if (connections[i] == null) {
connections[i] = conn; // Пометить как свободно
break;
}
}
}
semaphore.release(); // Вернуть разрешение
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
DatabaseConnection conn = getConnection();
System.out.println("Thread using connection " + conn.id);
Thread.sleep(2000);
releaseConnection(conn);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
// Вывод:
// Thread using connection 1
// Thread using connection 2
// Thread using connection 3
// (ждут)
// (после 2 сек первый поток освобождает соединение)
// Thread using connection 1
// ...
Ключевые моменты
- Семафор = счётчик разрешений
- acquire() — уменьшает счётчик, блокирует если 0
- release() — увеличивает счётчик
- Начальное значение 0 — полезно для ожидания события
- CountDownLatch — более удобный для ожидания завершения
- Не путать с Mutex — мьютекс (Semaphore(1)) для эксклюзивного доступа
Семафоры — один из основных инструментов многопоточного программирования в Java!