Semaphore和CountDownLatch
目录
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;}@Overridepublic 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