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

Как семафор реализует ожидание потоком завершения работы других потоков

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
// ...

Ключевые моменты

  1. Семафор = счётчик разрешений
  2. acquire() — уменьшает счётчик, блокирует если 0
  3. release() — увеличивает счётчик
  4. Начальное значение 0 — полезно для ожидания события
  5. CountDownLatch — более удобный для ожидания завершения
  6. Не путать с Mutex — мьютекс (Semaphore(1)) для эксклюзивного доступа

Семафоры — один из основных инструментов многопоточного программирования в Java!

Как семафор реализует ожидание потоком завершения работы других потоков | PrepBro