Semaphore和CountDownLatch

发布于:2025-09-14 ⋅ 阅读:(23) ⋅ 点赞:(0)

目录

1. Semaphore (信号量)

1.1 核心概念

1.2 实战中主要应用场景(设计思想)

停车场管理系统(数据库连接池类似):

2. CountDownLatch (倒计时门闩shuān‌)

2.1 核心概念

2.2 实战中主要应用场景(设计思想)

3. 核心区别总结

3.1 如何选择?


前言:

在 Java 并发编程的世界里,AbstractQueuedSynchronizer(简称 AQS)是 java.util.concurrent.locks 包下的一个核心抽象类。

           AQS,从名字看就知道是个跟队列、同步相关的抽象神器。简单来说,它为 Java 里实现锁和其他同步组件打造了一个超实用的基础框架。有了它,开发者能轻松定制各种复杂的同步需求,像是 ReentrantLock(可重入锁)、Semaphore(信号量)、CountDownLatch(倒计时器)这些大名鼎鼎的并发工具,底层都离不开 AQS 的强力支撑。

           和传统的 synchronized 关键字相比,AQS 的优势可不少。synchronized 用起来简单直接,编译器编译后会在同步块前后生成 monitorenter 和 monitorexit 字节码指令,靠对象头里的标记来控制锁的获取和释放。但它灵活性欠佳,像一些复杂的同步场景就有点应付不来。AQS 则不同,它把同步状态、线程排队这些底层逻辑都封装得妥妥当当,开发者可以根据需求定制同步规则,实现更精细的并发控制,而且在性能优化上也有更多的施展空间,能轻松应对高并发挑战。

       今天重点讲述Semaphore和CountDownLatch,具体如下:

1. Semaphore (信号量)

1.1 核心概念

Semaphore 用来控制同时访问特定资源的线程数量,它通过维护一组“许可证”(permits)来实现。你可以把它想象成一个售票厅,只有固定数量的票(许可证)。拿到票的线程可以进入“场馆”(访问资源),用完后归还票,其他线程才能获取。

  • 主要方法

    • acquire(): 获取一个许可证。如果无法获取(许可证为0),则线程阻塞,直到有许可证可用或被中断。

    • release(): 释放一个许可证,将其返还给信号量,从而允许一个等待的线程获取它。

    • 还有其他方法如 tryAcquire()(尝试获取,不阻塞)、acquire(int permits)(获取多个)等。

1.2 实战中主要应用场景(设计思想)

它适用于流量控制,特别是那种资源有限(如数据库连接、线程池、带宽),需要限制同时使用资源的线程数量的场景。

停车场管理系统(数据库连接池类似):
package onlyqi.daydayupgo06.juc;

import java.util.concurrent.Semaphore;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

// 停车场类
class ParkingLot {
    private final Semaphore semaphore;
    private final int capacity;
    private AtomicInteger atomicIntegerAvailableSpots = new AtomicInteger();

    public ParkingLot(int capacity) {
        this.capacity = capacity;
        this.semaphore = new Semaphore(capacity);
        atomicIntegerAvailableSpots.set(capacity);
    }

    // 车辆进入停车场
    public void enter(int carId) throws InterruptedException {
        // 获取一个车位,如果没有则等待
        semaphore.acquire();

        // 同步更新可用车位计数
            System.out.printf("车辆 %d 进入停车场,可用车位: %d/%d%n",
                    carId, atomicIntegerAvailableSpots.decrementAndGet(), capacity);

    }

    // 车辆离开停车场
    public void exit(int carId) {
        // 同步更新可用车位计数

            System.out.printf("车辆 %d 离开停车场,可用车位: %d/%d%n",
                    carId, atomicIntegerAvailableSpots.incrementAndGet(), capacity);
        // 释放一个车位
        semaphore.release();
    }
}

// 车辆线程类
class Car extends Thread {
    private final int carId;
    private final ParkingLot parkingLot;
    private final Random random = new Random();

    public Car(int carId, ParkingLot parkingLot) {
        this.carId = carId;
        this.parkingLot = parkingLot;
    }

    @Override
    public void run() {
        try {
            System.out.printf("车辆 %d 到达停车场%n", carId);

            // 尝试进入停车场
            parkingLot.enter(carId);

            // 在停车场内停留随机时间(1-3秒)
            int stayTime = random.nextInt(2000) + 1000; // 1000-3000毫秒
            Thread.sleep(stayTime);

            // 离开停车场
            parkingLot.exit(carId);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.printf("车辆 %d 操作被中断%n", carId);
        }
    }
}

// 主类
public class ParkingLotSimulation {
    public static void main(String[] args) {
        // 创建一个容量为5的停车场
        ParkingLot parkingLot = new ParkingLot(5);
        // 模拟10辆汽车
        int carCount = 10;
        Random random = new Random();

        for (int i = 1; i <= carCount; i++) {
            Car car = new Car(i, parkingLot);
            car.start();

            // 车辆到达时间间隔随机(100-500毫秒)
            try {
                Thread.sleep(random.nextInt(400) + 100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
}
    

2. CountDownLatch (倒计时门闩shuān‌)

2.1 核心概念

CountDownLatch 允许一个或多个线程等待其他线程完成操作。它像一个倒计时的计数器:初始化一个数值,线程通过 countDown() 方法将计数器减1,await() 方法会阻塞当前线程,直到计数器减到0。

  • 主要方法

    • await(): 使当前线程等待,直到计数器减到0。

    • countDown(): 将计数器减1。

    • await(long timeout, TimeUnit unit): 带超时的等待。

2.2 实战中主要应用场景(设计思想)

它适用于“主从”协作模式,一个或多个主线程需要等待所有准备工作(由其他从线程完成)就绪后,才能继续执行。或者用于让多个线程在同一时刻同时开始执行(类似于赛跑发令枪)。

并行计算,汇总结果(应用程序启动前的准备工作类似:
主线程需要等待所有必要的服务(如数据库、缓存、网络连接)都初始化完成后,才能对外提供服务。

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ParallelSum {
    public static void main(String[] args) throws InterruptedException {
        int taskCount = 5;
        CountDownLatch latch = new CountDownLatch(taskCount);
        List<Integer> results = new ArrayList<>();
        ExecutorService executor = Executors.newCachedThreadPool();
        Random random = new Random();

        // 提交并行任务
        for (int i = 0; i < taskCount; i++) {
            final int taskId = i;
            executor.execute(() -> {
                try {
                    int val = random.nextInt(100); // 模拟计算结果
                    Thread.sleep(random.nextInt(1000)); // 模拟计算耗时
                    synchronized (results) { results.add(val); }
                    System.out.printf("任务%d完成: %d%n", taskId, val);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
        }

        // 等待所有任务完成并汇总结果
        latch.await();
        int sum = results.stream().mapToInt(Integer::intValue).sum();
        System.out.printf("汇总结果: 总和=%d, 平均值=%.1f%n", 
                         sum, sum / (double) taskCount);
        
        executor.shutdown();
    }
}
    

3. 核心区别总结

特性 Semaphore CountDownLatch
目的 控制资源访问的线程数量 (流量控制) 等待一个或多个事件完成 (线程协作)
计数器 可增减acquire() 减1,release() 加1 只减不增countDown() 减1,无法重置
重用性 可以。许可证可以被获取和释放,循环使用。 不可以。计数器到0后,门闩打开,无法重置(除非用新的实例)。
核心方法 acquire(), release() await(), countDown()
典型比喻 票闸机(有票才能进,出来还票) 发令枪(所有人准备好,等一声枪响)或 终点线(等所有人都跑完)
3.1 如何选择?
  • 当你需要限制同时访问某个资源的线程数时,用 Semaphore

  • 当你需要一个或多个线程等待其他一系列操作完成后才能继续时,用 CountDownLatch