java多线程系统——(9)CountDownLatch使用与源码

CountDownLatch是一种常用的多线程工具类,允许一个或多个线程在完成一组其他线程中的操作前保持等待状态。本文介绍了CountDownLatch的基本概念、使用示例及源码分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、CountDownLatch简介

CountDownLatch是我们日常开发中经常用到的一个多线程工具类,它在完成一组正在其他线程中执行的操作之前,允许一个或多个线程一直等待,因此它的作用和join()方法类似,但是比join()方法更加的灵活。CountDownLatch在内部提供了一个计数器,每个线程完成任务后将计数器减一,当计数器减为0时所有等待在CountDownLatch的线程才会被唤醒。

2、CountDownLatch示例

CountDownLatch最常见的使用场景就是开启多个线程同时执行某个任务,等到所有任务都执行完再统计汇总结果,比如火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检测。只有等到所有的检查完毕后,引擎才能点火。那么在检测环节当然是多个检测项可以同时进行的。代码实现:

public class CountDownLatchTest {

    static CountDownLatch countDownLatch = new CountDownLatch(3);

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new Engine());
        Thread threadB = new Thread(new Fire());
        Thread threadC = new Thread(new Instrument());
        threadA.start();
        threadB.start();
        threadC.start();
        countDownLatch.await();
        System.out.println("火箭发射");
    }


    static class Engine implements Runnable{

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println("引擎检查完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        }
    }

    static class Fire implements Runnable{

        @Override
        public void run() {
            try {
                Thread.sleep(2000);
                System.out.println("点火设备检查完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        }
    }

    static class Instrument implements Runnable{

        @Override
        public void run() {
            try {
                Thread.sleep(3000);
                System.out.println("仪器检查完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        }
    }
}

运行结果:
在这里插入图片描述

3、CountDownLatch源码分析

CountDownLatch的数据结构很简单,它是通过共享锁实现的,内部拥有一个Sync对象,sync对象继承于AQS。

CountDownLatch的核心方法比较少,如下图:
在这里插入图片描述

3.1 构造函数

CountDownLatch只有一个构造函数:

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

该构造方法中指定了计数器的大小,其本质还是初始化内部sync变量。

3.2 await()

await()方法是让线程等待,直到CountDownLatch计数器减为0才继续运行,代码如下:

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

实际上还是调用了sync对象的acquireSharedInterruptibly(1)方法,进去看一下:

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())	//如果线程被中断了,就抛出异常
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0) //尝试获得共享锁
            doAcquireSharedInterruptibly(arg); 
    }

如果当前线程是中断状态,则抛出异常InterruptedException。否则,调用tryAcquireShared(arg)尝试获取共享锁;尝试成功则返回,否则就调用doAcquireSharedInterruptibly()。doAcquireSharedInterruptibly()会使当前线程一直等待,直到当前线程获取到共享锁(或被中断)才返回。

tryAcquireShared(arg)在CountDownLatch的Sync内被重写:

protected int tryAcquireShared(int acquires) {
	return (getState() == 0) ? 1 : -1;
}

如果当前的计数器为0,说明没有其他线程拥有锁,这时候返回1代表可以获得锁。否则返回-1代表不能获得锁。接着调用 doAcquireSharedInterruptibly(arg);方法继续尝试获得锁:

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

(01) addWaiter(Node.SHARED)的作用是,创建”当前线程“的Node节点,且Node中记录的锁的类型是”共享锁“(Node.SHARED);并将该节点添加到CLH队列末尾。
(02) node.predecessor()的作用是,获取上一个节点。如果上一节点是CLH队列的表头,则”尝试获取共享锁“。
(03) shouldParkAfterFailedAcquire()的作用和它的名称一样,如果在尝试获取锁失败之后,线程应该等待,则返回true;否则,返回false。
(04) 当shouldParkAfterFailedAcquire()返回ture时,则调用parkAndCheckInterrupt(),当前线程会进入等待状态,直到获取到共享锁才继续运行。

有关shouldParkAfterFailedAcquire()和shouldParkAfterFailedAcquire()方法在上一篇ReentrantLock使用与源码解析(下)有介绍,这里不在重复说明。

3.3 countDown()

countDown()方法的作用是使当前计数器减一:

    public void countDown() {
        sync.releaseShared(1);
    }
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

releaseShared()的目的是让当前线程释放它所持有的共享锁。
它首先会通过tryReleaseShared()去尝试释放共享锁。尝试成功通过doReleaseShared()去释放共享锁。

tryReleaseShared()在CountDownLatch.java中被重写,源码如下:

protected boolean tryReleaseShared(int releases) {
	// Decrement count; signal when transition to zero
	for (;;) {
		int c = getState();
			if (c == 0)
				return false;
		int nextc = c-1;
		if (compareAndSetState(c, nextc))
			return nextc == 0;
	}
}

如果当前计数器为0就不能继续释放锁,否则就将state减一,返回减一后的值。如果释放锁成功,就执行 doReleaseShared()来唤醒等待的线程:

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {	//头节点不为空,且有等待的节点
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {	//头节点状态为SIGNAL,说明要唤醒后续的节点
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))	//将当前状态设置为0
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);	//唤醒后驱节点
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

这里分别有这么几种情况:

  1. 头节点不为空,且有等待的节点,并且头节点的状态为SIGNAL,说明此时需要唤醒后驱节点,调用unparkSuccessor(h)唤醒后驱节点。
  2. 头节点不为空,且有等待的节点,但头节点的状态为0,也就是初始化状态,这时候不需要将后续节点唤醒,则把当前节点状态设置为PROPAGATE确保以后可以传递下去

最后如果头结点没有发生变化,表示设置完成,退出循环如果头结点发生变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,必须进行重试。

4、总结

CountDownLatch主要运用于在完成一组正在其他线程中执行的操作之前,允许一个或多个线程一直等待,它的本质是一个共享锁。其使用方法就是每当一个线程执行完任务,就将计数器减一,当计数器减到0时,开始唤醒后续的线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值