目录
2. CountDownLatch (倒计时门闩shuān)
前言:
在 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