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 乐观读 高并发读多写少

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

相关推荐
William Dawson2 小时前
【一文吃透 Spring Boot 面向切面编程(AOP):实例\+实现\+注意事项】
java·spring boot
fengxin_rou2 小时前
JVM 核心笔记:对象创建、生命周期与类加载器详解
java·jvm·笔记
one_love_zfl2 小时前
java面试-JVM篇
java·jvm·面试
skiy2 小时前
Spring之DataSource配置
java·后端·spring
石榴树下的七彩鱼2 小时前
医疗票据OCR识别API实战:从医保结算单到结构化数据提取(附Python/Java示例)
java·人工智能·python·ocr·api·ocr识别·医疗票据识别
Cat_Rocky2 小时前
k8s-单Master集群部署(简练理解)
java·容器·kubernetes
C雨后彩虹2 小时前
投篮大赛问题
java·数据结构·算法·华为·面试
Hello eveybody2 小时前
介绍最大公因数和最小公约数(C++)
java·开发语言·c++
ckhcxy2 小时前
抽象类和接口
java·开发语言