JUC 常用工具类:CountDownLatch、CyclicBarrier、Semaphore

之前聊过了 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 乐观读 高并发读多写少

都是工具类,关键是想清楚业务场景用哪个合适就行。

相关推荐
丘山一郎11 小时前
Spring 中的IOC控制反转 和DI依赖注入
java·后端·spring
码视野11 小时前
基于 Spring Boot + Vue3 的【大学英语四六级 (CET-4/6) 作文智能评分与句式润色系统】设计与实现(含PRD/三端高保真源码/大屏)
java·前端·人工智能·spring boot·后端·vue3
MetaLite11 小时前
Spring-AOP自调用为什么失效-AopContext真能解决吗
java·后端·spring
大模型码小白11 小时前
AI 对话流性能调优:万级消息的虚拟滚动落地
java·大数据·前端·javascript·人工智能·算法·机器学习
许彰午12 小时前
14-字段级SM4加密与SM2签名
java·低代码·架构
许彰午13 小时前
13-describe元数据输出
java·低代码·架构·状态模式
用户37215742613514 小时前
Java 设置 Word 文档编辑限制并允许指定区域编辑
java
宠友信息14 小时前
社区类源码开发实践中的仿小红书系统技术要点分析
java·spring boot·redis·mysql·uni-app·vue·内容运营
用户37215742613515 小时前
Java 设置 PDF 表单域只读或扁平化
java
luteres16 小时前
Spring学习笔记
java·后端·spring