并发编程工具类—— CountdownLatch(计数器)

最近学了并发编程,想谈谈自己的一些理解。一来是为了加深自己的印象,二来也希望能和大家公共学习。不对的地方请斧正,谢谢!

之前说了TheadLocal,现在来讲一下CountdownLatch。说起CountdownLatch就不得不说AQS,不了解AQS的同学可以去看看我之前写得关于AQS的文章,因为CountdownLatch是基于AQS来实现的。所以,如果想更好的了解CountdownLatch,那么了解AQS的工作机制和原理是并不可少的。

这里顺便说一下CAS操作:

CAS(compareAndSwap)也就是原子操作的底层,要么都成功,那么都失败。

CAS(V,expect,update)

V表示当前内存中的值,而expect表示当前读取到的值,只有当当前读取到的值和内存中的值一致时,我们会把内存中的时更新为最新的值,即update;若不等,则说明该值被其他线程修改了;

(自旋情况)每次CAS操作时,一定有一个线程会成功更新变量,而其他失败的线程不会挂起,而是允许继续重试,直到成功为止。

这里注意一下,CAS是乐观锁:先执行了再说,执行完了再保证数据的安全

CAS 是怎么实现线程的安全呢?语言层面不做处理,我们将其交给硬件— CPU 和内存,利用 CPU 的多处理能力,实现硬件层面的阻塞,再加上 volatile 变量的特性即可实现基于原子操作的线程安全。
下面是CAS循环:

ok,接下来让我们来认识下countdownLatch

countdownLatch其实我们可以称它为计数器,用与等待某些任务执行完成再去执行下面的任务,如下图:在我们设置的数扣完之前,我们无法执行await()后面的工作。当扣减为0时,我们就可以执行红线后面的工作了,否则将在红线处等待。

下面我们就来详细说一下它对应的源码:

先来看看它其中的所有方法

好了,我们来看看这个sync。它实际上是继承了AQS的,然后重写了tryAcquireShared()和tryReleaseShared()方法。

我们先看看CountdownLatch这个方法。

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

首先,传递的count只不能小于0,实际上你如果传递了0,其实也没有任何意义了。

然后我们看到了sync的有参构造方法,其中调用了一个很熟悉的方法,setState()。之前我们在AQS中说了,state就相当于一个计数器,你设置多少,就有多少个。当state为0时,说明没有任何线程获得同步状态,大于0则说明当前是有线程获得了同步状态的。如果不了解这个,你可以把他当成一个计数器,设置为3,就说明有三个线程已经获取了同步状态。而我们在countdownlatch中所作的操作就是从3开始减,然后让它为0时进行自己的操作。

Sync(int count) {
    setState(count);
}

接着,让我们来看看countdown()方法,它调用了sync的releaseShared()方法(释放锁,就相当于减去state)。

public void countDown() {
    sync.releaseShared(1);
}

点击进入该方法,我们进去了AQS类中

首先我们先来看看这个tryReleaseShared方法,而这个方法,就是我们在CountdownLatch类中,sync继承了AQS时重写的方法。

    protected boolean tryReleaseShared(int releases) {
        // Decrement count; signal when transition to zero
        //自旋操作 —— 就是为了如果CAS操作不成功让他一直去执行
        for (;;) {
            //得到当前state的值
            int c = getState();
            //如果为0,返回false
            if (c == 0)
                return false;
            //不为0,进行-1操作
            int nextc = c-1;
            //接着,通过CAS设置最新的state的值。
            if (compareAndSetState(c, nextc))
                //如果设置成功,并且state当前为0,那么满足这个判断,返回true,否则返回false
                return nextc == 0;
        }
    }
}

然后返回为true(代表state已经等于0了),这时候调用doReleaseShared()方法:

private void doReleaseShared() {
    //依然是自旋操作
    for (;;) {
        //得到同步队列的头节点 —— 如果不懂请一定去看看之前的那篇AQS的文章
        Node h = head;
        //如果头节点不为空,并且头节点不是尾结点  
        if (h != null && h != tail) {
            //获取当前头节点的同步状态 
            int ws = h.waitStatus;
            //如果为-1(我都准备好了,随时都可以获取同步状态)
            if (ws == Node.SIGNAL) {
                 //那么进行CAS操作,修改当前节点的状态为0
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    //不成功,那么再一次循环
                    continue;            // loop to recheck cases
                //成功了,唤醒离头节点最新的节点(这个方法这里我就不讲了,不懂得去看下AQS就ok)
                unparkSuccessor(h);
            }
        //如果当前状态为初始化状态且当前状态利用CAS操作修改初始化状态为-3失败(PROPAGATE,只有在线程处于SHARED模式才会被使用该状态)
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                  //再一次循环判断
                  continue;                // loop on failed CAS
        }
        //如果是仅仅是头节点,那么跳出循环
        if (h == head)                   // loop if head changed
            break;
    }
}

这里说明一下,返回true是代表countdown()操作成功。false则是操作失败,至于什么时候操作失败,则是在state为0时,这时候已经能再去-1了。

最后来看看await()方法,它依然调用了sync的获取中断方法

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

点进去,依然后跳到了AQS类中。这里说明一下,由于Sync继承了AQS,所以countdownLatch是直接使用了它的父类方法。而使用AQS统一方式都是这样,你只需要区分到底是独占锁还是共享锁,进而选择重写AQS给你提供的方法。即可完成你所需要的同步锁(把这个记成一个固定模式即可,AQS说白了就是一个框架,一个工具)

首先该方法中先判断了线程是否被中断,如果没有中断。那么调用tryAcquireShared()方法:

protected int tryAcquireShared(int acquires) {
    //判断state是否为0,如果是返回1.不是返回-1
    return (getState() == 0) ? 1 : -1;
}

如果上图所示,如果返回-1。表示state不为0,则进入下面的方法doAcquireSharedInterruptibly();(这里这个arg变量已经固定为1了,在我们调用方法时,countdownlatch类已经写死为1了)

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    //创建一个Node节点
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        //自旋
        for (;;) {
            //得到该节点的上一个节点
            final Node p = node.predecessor();
            //如果上一个节点为头节点
            if (p == head) {
                //执行获取锁操作(之前说的重写方法)
                int r = tryAcquireShared(arg);
                //如果大于0,说明为1.这时候state已经等于0了
                if (r >= 0) {
                    //设置该节点为头节点
                    setHeadAndPropagate(node, r);
                    //将上一个节点的指向为null 
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            //如果不是头节点,那么进行阻塞等操作。等待被唤醒 
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

现在大家应该大致可以理解,为什么await能够等待计数器变成0的时候,才会执行后面的代码了吧。从原理上来讲,await()方法就是一个获取锁的方法,但是前提是计数器为0时。而countdown()方法又是释放锁的方法,每成功一次就-1,直到state为0。每个线程进来执行完了自己的方法就调用一次countdown,当这些线程都完成了自己方法await()后面的方法才会执行。就像我们刚刚那张图一样

 

下面总结一下:

countdownlatch是并发的一种工具,通常用在多线程统计之类的的操作上(生成Excel之后打包zip等)。但是,一定要设置好计数器的state个数,有几个线程就设置为多少,然后每个线程执行完成之后去调用countdown()方法,代表本线程的任务已经完成了。等待所有线程的任务都完成了,我们再在设置好await()的位置去执行主线程的方法。

最后提供一个简单的demo供大家参考,一定要记住的是:设置的state数量一定要和我们另起的线程数量相同

public class UseCountDownLatch {

    static CountDownLatch latch = new CountDownLatch(6);

    /*初始化线程*/
    private static class InitThread implements Runnable{

        public void run() {
            System.out.println("Thread_"+Thread.currentThread().getId()
                    +" ready init work......");
            latch.countDown();
            for(int i =0;i<2;i++) {
                System.out.println("Thread_"+Thread.currentThread().getId()
                        +" ........continue do its work");
            }
        }
    }

    /*业务线程等待latch的计数器为0完成*/
    private static class BusiThread implements Runnable{

        public void run() {
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i =0;i<3;i++) {
                System.out.println("BusiThread_"+Thread.currentThread().getId()
                        +" do business-----");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            public void run() {
                SleepTools.ms(1);
                System.out.println("Thread_"+Thread.currentThread().getId()
                        +" ready init work step 1st......");
                latch.countDown();
                System.out.println("begin step 2nd.......");
                SleepTools.ms(1);
                System.out.println("Thread_"+Thread.currentThread().getId()
                        +" ready init work step 2nd......");
                latch.countDown();
            }
        }).start();
        new Thread(new BusiThread()).start();
        for(int i=0;i<=3;i++){
            Thread thread = new Thread(new InitThread());
            thread.start();
        }

        latch.await();
        System.out.println("Main do ites work........");
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值