工具类-计数器 CountDownLatch

也有叫倒计时门闩、倒计时锁、计数器,作用可以 让一个或多个线程阻塞,直到其他线程完成再继续执行

一次性使用,计数器只能递减,不能重置,不能增加

主要方法

// 构造函数(初始化计数器)
public CountDownLatch(int count)

// 等待直到计数器归零(可中断)
public void await()

// 带超时的等待
public boolean await(long timeout, TimeUnit unit)

// 递减计数器(计数-1)
public void countDown()

// 获取当前计数值
public long getCount()

使用示例

基础示例

// 创建计数器(3个子任务)
CountDownLatch latch = new CountDownLatch(3);

// 启动工作线程
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            // 模拟任务执行
            Thread.sleep((long)(Math.random() * 2000));
            System.out.println(Thread.currentThread().getName() + " 完成任务");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            latch.countDown(); // 任务完成,计数器-1
        }
    }).start();
}

// 主线程等待所有任务完成
latch.await();
System.out.println("所有任务已完成,继续主流程");

带超时示例

ExecutorService executor = Executors.newFixedThreadPool(5);
CountDownLatch latch = new CountDownLatch(10);

// 提交10个任务
for (int i = 0; i < 10; i++) {
    executor.execute(() -> {
        try {
            // 执行任务...
        } finally {
            latch.countDown();
        }
    });
}

// 等待所有任务完成(最多等5秒)
if (latch.await(5, TimeUnit.SECONDS)) {
    System.out.println("全部任务按时完成");
} else {
    System.out.println("部分任务超时未完成");
}
executor.shutdown();

多个线程等待

// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(3); // 需要等待3个前置任务

// 工作线程
for (int i = 0; i < 3; i++) {
    int finalI = i+1;
    pool.execute(() -> {
        try {
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(finalI));
            System.out.println("工作线程"+finalI+"完成");
        } finally {
            latch.countDown(); // 每个任务完成时计数器-1
        }
    });
}

// 等待线程
for (int i = 0; i < 2; i++) {
    int finalI = i+1;
    pool.execute(() -> {
        try {
            System.out.println("等待线程"+finalI+"等待...");
            latch.await(); // 等待前置任务完成
            System.out.println("等待线程"+finalI+"完成");
            // 执行后续任务...
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}

pool.shutdown();

初始化

// java.util.concurrent.CountDownLatch#CountDownLatch
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

// java.util.concurrent.CountDownLatch.Sync#Sync
Sync(int count) {
    setState(count);
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#setState
protected final void setState(int newState) { // 底层就是初始时给 AQS 的 state 赋值
    state = newState;
}

await() 原理

字面意思等待,底层原理也是获取锁,获取不到就入队并阻塞。只是这里的获取锁就是看 state 是否是 0

// java.util.concurrent.CountDownLatch#await()
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted()) throw new InterruptedException();
  	
    if (tryAcquireShared(arg) < 0) // 获取锁,CountDownLatch 的处理是:返回 1(获取成功),返回 -1(获取失败)
        doAcquireSharedInterruptibly(arg); // 获取锁失败的处理,线程入队,挂起线程
}

// java.util.concurrent.CountDownLatch.Sync#tryAcquireShared
protected int tryAcquireShared(int acquires) { // 获取锁的做法就是判断 state 是否等于 0,等于0表示获取锁成功,不然返回-1表示获取失败
    return (getState() == 0) ? 1 : -1;
}

doAcquireSharedInterruptibly() 方法用于处理获取锁失败的处理,也就是线程入队、挂起线程,是 AQS 实现的,这里就不展开了

Semaphore 里有介绍过这个方法,AQS 中介绍过 doAcquireShared() 方法,两个方法差不多,区别就是 doAcquireSharedInterruptibly() 能响应中断处理

countDown() 原理

字面意思计数-1,底层原理也是释放锁,就是把 state -1

// java.util.concurrent.CountDownLatch#countDown
public void countDown() {
    sync.releaseShared(1);
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#releaseShared
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // 释放锁
        doReleaseShared(); // 释放成功的处理
        return true;
    }
    return false;
}

// java.util.concurrent.CountDownLatch.Sync#tryReleaseShared
protected boolean tryReleaseShared(int releases) { // 释放锁就是把 state--,如果 state = 0 就表示释放成功,反之释放失败
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

doReleaseShared() 方法用于释放锁成功后的处理,就是处理线程出队和唤醒,因为是共享锁,所以会唤醒多个线程

这是有共性的方法,所以 AQS 已经实现好了,这里不赘述,想详细了解可以查看 AQS 从入门到精通

总结

  • 基于 AQS 实现,当多个线程作为等待线程时,也支持公平和非公平
  • 初始化时维护 state 的值
  • 等待线程调用 await() 方法时,如果 state 不等于 0 就进入队列阻塞( state!=0 表示获取锁失败)
  • 工作线程调用 countDown() 方法时,让 state--
    • 如果减后 state = 0 表示释放锁成功,唤醒等待线程(共享锁,所以可能唤醒多个)
    • 如果减后 state != 0 表示释放锁失败(不唤醒线程)
posted @ 2023-05-24 16:45  CyrusHuang  阅读(50)  评论(0)    收藏  举报