• Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例


    概要

    前面对"独占锁"和"共享锁"有了个大致的了解;本章,我们对CountDownLatch进行学习。和ReadWriteLock.ReadLock一样,CountDownLatch的本质也是一个"共享锁"。

    CountDownLatch简介

    CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

    CountDownLatch和CyclicBarrier的区别
    (01) CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待。
    (02) CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier。
    关于CyclicBarrier的原理,后面一章再来学习。


    CountDownLatch函数列表

    复制代码
    CountDownLatch(int count)
    构造一个用给定计数初始化的 CountDownLatch。
    
    // 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。
    void await()
    // 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
    boolean await(long timeout, TimeUnit unit)
    // 递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
    void countDown()
    // 返回当前计数。
    long getCount()
    // 返回标识此锁存器及其状态的字符串。
    String toString()
    复制代码

    CountDownLatch数据结构

    CountDownLatch的UML类图如下:

    CountDownLatch的数据结构很简单,它是通过"共享锁"实现的。它包含了sync对象,sync是Sync类型。Sync是实例类,它继承于AQS。

    CountDownLatch源码分析(基于JDK1.7.0_40)

    CountDownLatch完整源码(基于JDK1.7.0_40)

     View Code

    CountDownLatch是通过“共享锁”实现的。下面,我们分析CountDownLatch中3个核心函数: CountDownLatch(int count), await(), countDown()。

    1. CountDownLatch(int count)

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

    说明:该函数是创建一个Sync对象,而Sync是继承于AQS类。Sync构造函数如下:

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

    setState()在AQS中实现,源码如下:

    protected final void setState(long newState) {
        state = newState;
    }

    说明:在AQS中,state是一个private volatile long类型的对象。对于CountDownLatch而言,state表示的”锁计数器“。CountDownLatch中的getCount()最终是调用AQS中的getState(),返回的state对象,即”锁计数器“。

    2. await()

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

    说明:该函数实际上是调用的AQS的acquireSharedInterruptibly(1);

    AQS中的acquireSharedInterruptibly()的源码如下:

    复制代码
    public final void acquireSharedInterruptibly(long arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
    复制代码

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

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

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

    说明:tryAcquireShared()的作用是尝试获取共享锁。
    如果"锁计数器=0",即锁是可获取状态,则返回1;否则,锁是不可获取状态,则返回-1。

    复制代码
    private void doAcquireSharedInterruptibly(long arg)
        throws InterruptedException {
        // 创建"当前线程"的Node节点,且Node中记录的锁是"共享锁"类型;并将该节点添加到CLH队列末尾。
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                // 获取上一个节点。
                // 如果上一节点是CLH队列的表头,则"尝试获取共享锁"。
                final Node p = node.predecessor();
                if (p == head) {
                    long r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                // (上一节点不是CLH队列的表头) 当前线程一直等待,直到获取到共享锁。
                // 如果线程在等待过程中被中断过,则再次中断该线程(还原之前的中断状态)。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    复制代码

    说明
    (01) addWaiter(Node.SHARED)的作用是,创建”当前线程“的Node节点,且Node中记录的锁的类型是”共享锁“(Node.SHARED);并将该节点添加到CLH队列末尾。关于Node和CLH在"Java多线程系列--“JUC锁”03之 公平锁(一)"已经详细介绍过,这里就不再重复说明了。
    (02) node.predecessor()的作用是,获取上一个节点。如果上一节点是CLH队列的表头,则”尝试获取共享锁“。
    (03) shouldParkAfterFailedAcquire()的作用和它的名称一样,如果在尝试获取锁失败之后,线程应该等待,则返回true;否则,返回false。
    (04) 当shouldParkAfterFailedAcquire()返回ture时,则调用parkAndCheckInterrupt(),当前线程会进入等待状态,直到获取到共享锁才继续运行。
    doAcquireSharedInterruptibly()中的shouldParkAfterFailedAcquire(), parkAndCheckInterrupt等函数在"Java多线程系列--“JUC锁”03之 公平锁(一)"中介绍过,这里也就不再详细说明了。

    3. countDown()

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

    说明:该函数实际上调用releaseShared(1)释放共享锁。

    releaseShared()在AQS中实现,源码如下:

    复制代码
    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;
            // “锁计数器”-1
            int nextc = c-1;
            // 通过CAS函数进行赋值。
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
    复制代码

    说明:tryReleaseShared()的作用是释放共享锁,将“锁计数器”的值-1。

    总结:CountDownLatch是通过“共享锁”实现的。在创建CountDownLatch中时,会传递一个int类型参数count,该参数是“锁计数器”的初始状态,表示该“共享锁”最多能被count给线程同时获取。当某线程调用该CountDownLatch对象的await()方法时,该线程会等待“共享锁”可用时,才能获取“共享锁”进而继续运行。而“共享锁”可用的条件,就是“锁计数器”的值为0!而“锁计数器”的初始值为count,每当一个线程调用该CountDownLatch对象的countDown()方法时,才将“锁计数器”-1;通过这种方式,必须有count个线程调用countDown()之后,“锁计数器”才为0,而前面提到的等待线程才能继续运行!

    以上,就是CountDownLatch的实现原理。

    CountDownLatch的使用示例

    下面通过CountDownLatch实现:"主线程"等待"5个子线程"全部都完成"指定的工作(休眠1000ms)"之后,再继续运行。

    复制代码
     1 import java.util.concurrent.CountDownLatch;
     2 import java.util.concurrent.CyclicBarrier;
     3 
     4 public class CountDownLatchTest1 {
     5 
     6     private static int LATCH_SIZE = 5;
     7     private static CountDownLatch doneSignal;
     8     public static void main(String[] args) {
     9 
    10         try {
    11             doneSignal = new CountDownLatch(LATCH_SIZE);
    12 
    13             // 新建5个任务
    14             for(int i=0; i<LATCH_SIZE; i++)
    15                 new InnerThread().start();
    16 
    17             System.out.println("main await begin.");
    18             // "主线程"等待线程池中5个任务的完成
    19             doneSignal.await();
    20 
    21             System.out.println("main await finished.");
    22         } catch (InterruptedException e) {
    23             e.printStackTrace();
    24         }
    25     }
    26 
    27     static class InnerThread extends Thread{
    28         public void run() {
    29             try {
    30                 Thread.sleep(1000);
    31                 System.out.println(Thread.currentThread().getName() + " sleep 1000ms.");
    32                 // 将CountDownLatch的数值减1
    33                 doneSignal.countDown();
    34             } catch (InterruptedException e) {
    35                 e.printStackTrace();
    36             }
    37         }
    38     }
    39 }
    复制代码

    运行结果

    复制代码
    main await begin.
    Thread-0 sleep 1000ms.
    Thread-2 sleep 1000ms.
    Thread-1 sleep 1000ms.
    Thread-4 sleep 1000ms.
    Thread-3 sleep 1000ms.
    main await finished.
    复制代码

    结果说明:主线程通过doneSignal.await()等待其它线程将doneSignal递减至0。其它的5个InnerThread线程,每一个都通过doneSignal.countDown()将doneSignal的值减1;当doneSignal为0时,main被唤醒后继续执行。

  • 相关阅读:
    简单编码解码学习
    如何把SQLServer数据库从高版本降级到低版本?
    快速读取csv平面文件,并导入数据库,简单小工具
    数据流处理数据
    配置文件的几种读取方式
    常用webservice接口地址
    路径转换
    Tornado与JS交互工作
    测试技术发展之我见
    移动测试人员的未来:测试开发技术的融合
  • 原文地址:https://www.cnblogs.com/duanxz/p/6063706.html
Copyright © 2020-2023  润新知