之前聊过了 synchronized 和 ReentrantLock,今天来看看 JUC 里几个好用的工具类。
先说个场景
场景:用户下单,需要干这几件事:
1. 查库存
2. 查优惠
3. 查用户等级
三个查询互不相干,可以同时查,等都查完了再合并结果。
这种事交给 JUC 工具类,几行代码就搞定了。
CountDownLatch:等所有人都到齐
核心思想:计数器为 0 之前一直等。
适用场景
"主线程等子线程全部完成"。
比如:游戏加载资源、等所有玩家准备完成、服务启动时等依赖服务就绪。
用法
java
public class GameDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3); // 3个子任务
// 玩家1加载
new Thread(() -> {
System.out.println("玩家1加载完成");
latch.countDown(); // 计数-1
}).start();
// 玩家2加载
new Thread(() -> {
System.out.println("玩家2加载完成");
latch.countDown();
}).start();
// 玩家3加载
new Thread(() -> {
System.out.println("玩家3加载完成");
latch.countDown();
}).start();
// 主线程等待
latch.await(); // 阻塞,直到计数为0
System.out.println("所有玩家加载完成,游戏开始!");
}
}
输出:
玩家2加载完成
玩家1加载完成
玩家3加载完成
所有玩家加载完成,游戏开始!
常见错误
java
// 错误:计数不对
CountDownLatch latch = new CountDownLatch(3);
latch.countDown();
latch.countDown();
// 只减了2次,await() 会一直等
// 正确:计数和任务数要匹配
CountDownLatch latch = new CountDownLatch(3);
// 3个线程,每个都countDown()
实战:等多服务启动完成
java
public class ServiceStarter {
public void startAll() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
// 启动数据库
new Thread(() -> {
dbService.start();
latch.countDown();
}).start();
// 启动缓存
new Thread(() -> {
cacheService.start();
latch.countDown();
}).start();
// 启动消息队列
new Thread(() -> {
mqService.start();
latch.countDown();
}).start();
// 等所有服务都启动完成
latch.await();
System.out.println("所有服务启动完成,可以开始接受请求了");
}
}
CyclicBarrier:所有人都到了再一起走
核心思想:所有人都到了才放行。
适用场景
"大家约好了一起走,谁先到谁等着"。
比如:多线程计算,最后合并结果;赛车比赛,等所有车都就位再发车。
用法
java
public class RaceDemo {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(3);
// 选手1
new Thread(() -> {
System.out.println("选手1就位");
try {
barrier.await(); // 等待
System.out.println("选手1出发!");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
// 选手2
new Thread(() -> {
System.out.println("选手2就位");
try {
barrier.await();
System.out.println("选手2出发!");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
// 选手3
new Thread(() -> {
System.out.println("选手3就位");
try {
barrier.await();
System.out.println("选手3出发!");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
输出:
选手2就位
选手1就位
选手3就位
选手1出发!
选手2出发!
选手3出发!
和 CountDownLatch 的区别
| CountDownLatch | CyclicBarrier | |
|---|---|---|
| 计数方向 | 减法 | 减法,到0重置 |
| 谁等谁 | 主线程等子线程 | 子线程互相等 |
| 能否重用 | 不能 | 能(cyclic) |
| 典型场景 | 等所有人都完成 | 等所有人都到齐 |
实战:多线程汇总计算
java
public class DataAggregator {
public void aggregate() throws InterruptedException {
int[] results = new int[3];
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
// 所有人都到了,执行汇总
int sum = results[0] + results[1] + results[2];
System.out.println("汇总完成: " + sum);
});
// 计算华南数据
new Thread(() -> {
results[0] = calculateSouth();
try {
barrier.await();
} catch (Exception e) {}
}).start();
// 计算华东数据
new Thread(() -> {
results[1] = calculateEast();
try {
barrier.await();
} catch (Exception e) {}
}).start();
// 计算华北数据
new Thread(() -> {
results[2] = calculateNorth();
try {
barrier.await();
} catch (Exception e) {}
}).start();
}
}
Semaphore:限流神器
核心思想:只有 N 个许可,拿到了才能干活。
适用场景
限流:数据库连接池、线程池限制、接口限流。
用法
java
public class SemaphoreDemo {
public static void main(String[] args) {
// 只能有5个线程同时访问
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 10; i++) {
int id = i;
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
System.out.println("线程" + id + "开始访问");
Thread.sleep(1000); // 模拟访问
System.out.println("线程" + id + "结束访问");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
}).start();
}
}
}
输出:
线程0开始访问
线程1开始访问
线程2开始访问
线程3开始访问
线程4开始访问
线程0结束访问 <- 释放后,线程5才能进来
线程5开始访问
...
实战:连接池限流
java
public class ConnectionPool {
private final Semaphore semaphore;
private final Connection[] connections;
private final AtomicInteger cursor = new AtomicInteger(0);
public ConnectionPool(int poolSize) {
this.semaphore = new Semaphore(poolSize);
this.connections = new Connection[poolSize];
// 初始化连接...
}
public Connection getConnection() throws InterruptedException {
semaphore.acquire(); // 限流
return nextConnection();
}
public void releaseConnection(Connection conn) {
// 归还连接
semaphore.release();
}
}
实战:接口限流
java
public class RateLimiter {
private final Semaphore semaphore = new Semaphore(100); // 每秒100个请求
public boolean tryAcquire() {
return semaphore.tryAcquire(); // 非阻塞,拿不到就返回false
}
public void release() {
semaphore.release();
}
}
// 使用
public String getUser(Long id) {
if (!rateLimiter.tryAcquire()) {
return "请求太频繁,请稍后";
}
try {
return userService.getUser(id);
} finally {
rateLimiter.release();
}
}
ReadWriteLock:读写分离
核心思想:读可以并行,写需要独占。
适用场景
读多写少场景:配置缓存、商品详情页。
用法
java
public class CacheDemo {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Map<String, Object> cache = new HashMap<>();
// 读:多个线程可以同时读
public Object get(String key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
// 写:只能一个线程写,写的时候不能读
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
为什么不直接用 synchronized
java
// synchronized 的问题:读写互斥,读读也互斥
public synchronized Object get(String key) {
return cache.get(key);
}
// ReadWriteLock:读读不互斥,只有写才互斥
public Object get(String key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
性能对比:
- synchronized:所有操作串行
- ReadWriteLock:读并行,写串行(读多写少时性能提升明显)
StampedLock:更快的读写锁
JDK 8 引入,比 ReadWriteLock 更快。
java
public class StampedLockDemo {
private final StampedLock stampedLock = new StampedLock();
private final Map<String, Object> cache = new HashMap<>();
public Object get(String key) {
// 乐观读
long stamp = stampedLock.tryOptimisticRead();
Object value = cache.get(key);
// 检查读锁是否有效(期间是否有写操作)
if (!stampedLock.validate(stamp)) {
// 升级为悲观读
stamp = stampedLock.readLock();
try {
value = cache.get(key);
} finally {
stampedLock.unlockRead(stamp);
}
}
return value;
}
public void put(String key, Object value) {
long stamp = stampedLock.writeLock();
try {
cache.put(key, value);
} finally {
stampedLock.unlockWrite(stamp);
}
}
}
一图总结
┌─────────────────────────────────────────────────────┐
│ JUC 工具类 │
├─────────────────────────────────────────────────────┤
│ │
│ CountDownLatch 等所有人都完成,主线程才继续 │
│ ↓ │
│ CyclicBarrier 等所有人都到齐,一起开始 │
│ ↓ │
│ Semaphore 只有N个许可,限流用的 │
│ ↓ │
│ ReadWriteLock 读写分离,读并行,写独占 │
│ ↓ │
│ StampedLock 更快的读写锁,乐观读优化 │
│ │
└─────────────────────────────────────────────────────┘
总结
| 工具类 | 核心思想 | 典型场景 |
|---|---|---|
| CountDownLatch | 倒数计数 | 等子任务完成、启动等待 |
| CyclicBarrier | 互相等待 | 多线程汇总、赛车发车 |
| Semaphore | 许可控制 | 连接池限流、接口限流 |
| ReadWriteLock | 读写分离 | 缓存读写 |
| StampedLock | 乐观读 | 高并发读多写少 |
都是工具类,关键是想清楚业务场景用哪个合适就行。