并发工具一之CountDownLatch

一、CountDownLatch简介

countdownlatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完毕再执行。从命名可以解读到 countdown 是倒数的意思,类似于我们倒计时的概念。

countdownlatch 提供了两个方法,一个是 countDown,一个是 await, countdownlatch 初始化的时候需要传入一个整数,在这个整数倒数到 0 之前,调用了 await 方法的程序都必须要等待直到0才开始执行,然后通过 countDown 来倒数。

二、使用示例

我们定义一个总数为3的计数器,t1线程调用await()方法,其他t2/t3/t4线程调用countDown()方法,会发现打印结果总是最后打印t1线程

public class CountDownLatchDemo {
    // 初始化一个总数为3的计数器
    static CountDownLatch count = new CountDownLatch(3);

    public static void main(String[] args) {
        new Thread(()->{
            try {
                // 调用await()方法,当计数器不为0的时候,阻塞
                count.await();
                System.out.println("线程t1执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1").start();

        new Thread(()->{
            // 调用countDown()方法,计数器-1
            count.countDown();
            System.out.println("线程t2执行");
        },"t2").start();

        new Thread(()->{
            // 调用countDown()方法,计数器-1
            count.countDown();
            System.out.println("线程t3执行");
        },"t3").start();

        new Thread(()->{
            // 调用countDown()方法,计数器-1
            count.countDown();
            System.out.println("线程t4执行");
        },"t4").start();
    }
}

打印执行结果:

线程t2执行
线程t4执行
线程t3执行
线程t1执行

Process finished with exit code 0

由于t1阻塞,必须等到其他三个线程执行到计数器为0的时候才会执行,所以t1总是最后打印

三、CountDownLatch作用

我们可以使用CountDownLatch做并发模拟,假设我们有100个线程,我们希望他同时在某一时刻同时执行,那么我们就可以使用CountDownLatch

定义一个线程,调用await()方法直到计数器为0才执行

public class ThreadDemo extends Thread {

    private CountDownLatch count;

    public ThreadDemo(CountDownLatch count){
        this.count = count;
    }

    @Override
    public void run() {
        try {
            count.await();
            System.out.println("线程"+Thread.currentThread().getName()+"执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试,循环启动线程,然后调用countDown()方法,令计数器为0,然后线程会同时执行

public class CountDownLatchDemo02 {

    static CountDownLatch count = new CountDownLatch(1);

    public static void main(String[] args) {
        for(int i=0;i<10;i++) {
            new ThreadDemo(count).start();
        }
        count.countDown();
    }
}

四、源码分析

1.
1.构造方法CountDownLatch(int count)

public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        // 定义了一个内部变量sync和内部类Sync,把count传值到内部类的构造方法
        this.sync = new Sync(count);     //  (1)
    }

我们看一下内部类Sync的实现,发现内部类Sync继承了AQS,那根据前面对AQS的实现的学习,对此就能比较好了解了

private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
		// (1) 初始化CountDownLatch调用此方法,把count的值赋给了state,我们学习AQS和ReentrantLock实现的时候知道state是记录锁的,state>0表示锁的次数
        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

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

        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;
            }
        }
    }

2.await()方法

public void await() throws InterruptedException {
		// await是调用AQS的acquireSharedInterruptibly方法加锁,从方法名字可以看出,这个实现是一种共享锁
        sync.acquireSharedInterruptibly(1);
    }
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        // 判断是否被中断过,如果中断过就跑出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        //state 如果不等于 0,说明当前线程需要加入到共享锁队列中
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

doAcquireSharedInterruptibly添加锁的过程

  1. addWaiter 设置为 shared 模式。
  2. tryAcquire 和 tryAcquireShared 的返回值不同,因此会多出一个判断过程
  3. 在 判 断 前 驱 节 点 是 头 节 点 后 , 调 用 了setHeadAndPropagate 方法,而不是简单的更新一下头节点。
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) {
                // 如果前一个是head节点,就判断尝试获取锁
                    int r = tryAcquireShared(arg);
                    //r>=0 表示获取到了执行权限,这个时候因为 state!=0,所以不会执行这段代码
                    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);
        }
    }

setHeadAndPropagate方法
这个方法的主要作用是把被唤醒的节点,设置成 head 节点。 然后继续唤醒队列中的其他线程。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

由于线程被 await 方法阻塞了,所以只有等到countdown 方法使得 state=0 的时候才会被唤醒,我们来看看 countdown 做了什么

3.countDown()方法

public void countDown() {
        sync.releaseShared(1);
    }
  1. 只有当 state 减为 0 的时候,tryReleaseShared 才返回 true, 否则只是简单的 state = state - 1
  2. 如果 state=0, 则调用 doReleaseShared唤醒处于 await 状态下的线程
public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
protected boolean tryReleaseShared(int releases) {
            // 用自旋的方法实现 state 减 1
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

AQS. doReleaseShared方法
共享锁的释放和独占锁的释放有一定的差别,前面唤醒锁的逻辑和独占锁是一样,先判断头结点是不是SIGNAL 状态,如果是,则修改为 0,并且唤醒头结点的下一个节点
PROPAGATE : 标识为 PROPAGATE 状态的节点,是共享锁模式下的节点状态,处于这个状态下的节点,会对线程的唤醒进行传播

private void doReleaseShared() {
    
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                // 这个 CAS 失败的场景是:执行到这里的时候,刚好有一个节点入队,入队会将这个 ws 设置为 -1
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // 如果到这里的时候,前面唤醒的线程已经占领了 head,那么再循环
			// 通过检查头节点是否改变了,如果改变了就继续循环
            if (h == head)                   // loop if head changed
                break;
        }
    }

一旦 线程被唤醒,代码又会继续回到doAcquireSharedInterruptibly 中来执行。如果当前 state满足=0 的条件,则会执行 setHeadAndPropagate 方法

五、总结

1.从代码的实现来看,有点类似 join 的功能,但是比 join 更加灵活。CountDownLatch 构造函数会接收一个 int 类型的参数作为计数器的初始值,当调用 CountDownLatch 的countDown 方法时,这个计数器就会减一。通过 await 方法去阻塞主流程
2.CountDownLatch的实现是基于共享锁机制实现的

本文是综合自己的认识和参考各类资料(书本及网上资料)编写,若有侵权请联系作者,所有内容仅代表个人认知观点,如有错误,欢迎校正; 邮箱:[email protected] 博客地址:https://blog.csdn.net/qq_35576976/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章