【并发编程】CountDownLatch 源码分析

前言

Github:https://github.com/yihonglei/jdk-source-code-reading(java-concurrent)

一 CountDownLatch 概述

1、介绍

CountDownLatch 允许一个或多个线程等待其他线程完成操作后才继续执行。

使用 CountDownLatch 时,需要指定一个整数值 N,此值是线程将要等待的操作数。

当线程 M 为了要等待操作 A 完成时,线程 M 需要调用 await() 方法。await() 方法让

线程 M 进入等待状态直到所有操作 A 完成为止,M 才被唤醒继续执行。当操作 A 执行

完成(每一个处理),调用 countDown() 方法来减少 CountDownLatch 类的内部计数器,

N 每次减少 1。当内部计数器递减为 0 时,CountDownLatch 会唤醒所有调用 await() 方法

挂起的线程,即会唤醒 M,从而实现 M 等待操作 A  执行完成后再继续执行 M 操作的效果。

2、原理

CountDownLatch 的构造函数接收一个 int 类型的参数作为计数器构造参数,如果你想等待

N 个点完成,这里就传入 N。当我们调用 CountDownLatch 的 countDown() 方法时,N 就会

减 1,CountDownLatch 的 await() 方法会阻塞当前线程,直到 N 变成零被唤醒继续执行。

由于 countDown() 方法可以用在任何地方,所以这里说的 N 个点,可以是 N 个线程,

也可以是 1 个线程里的 N 个执行步骤。用在多个线程时,只需要把这个 CountDownLatch

的引用传递到线程里即可。

3、核心方法

countDown():用于减少计数器次数,每调用一次就默认会减少 1,当锁释放完时,

将等待线程唤醒。

await():负责线程的阻塞,当 CountDownLatch 计数的值为 0 时,获取到锁,

才返回主线程执行。

4、典型场景

CountDownLatch 使用场景主要用于控制主线程等待所有子线程全部执行完成

然后恢复主线程执行。

二 CountDownLatch 实例

1、实例场景

我们需要批量的从数据库查询出数据进行处理。一般会想到用多线程去处理,但是,

有一个问题就是我们如何保证每一次查询的数据不是正在处理的数据?方法有很多种,

可以在每一批数据处理完之后再去数据库取下一批数据,每一批数据采取多线程处理的方式。

我们也可以采用别的方案,这里只针对使用 CountDownLatch 来实现批量处理。

CountDownLatch 控制主线程必须等待线程池子线程执行完才恢复执行主线程。

2、实例代码

package com.jpeony.concurrent.countdownlatch;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 多线程+CountDownLatch演示
 *
 * @author yihonglei
 */
public class CountDownLatchTest {
    // 线程池
    private static ExecutorService executorService = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        int counterBatch = 1;
        try {
            // 数据循环处理
            while (true) {
                // 模拟数据库查询出的List
                List<String> list = new ArrayList<>();
                for (int i = 0; i < 10; i++) {
                    list.add("user" + i);
                }
                // 计数器大小定义为集合大小,避免处理不一致导致主线程无限等待
                CountDownLatch countDownLatch = new CountDownLatch(list.size());
                // 循环处理List
                list.parallelStream().forEach(userId -> {
                    // 任务提交线程池
                    CompletableFuture.supplyAsync(() -> {
                        try {
                            // 处理用户数据
                            dealUser(userId);
                        } finally {
                            countDownLatch.countDown();
                        }
                        return 1;
                    }, executorService);
                });
                // 主线程等待所有子线程都执行完成时,恢复执行主线程
                countDownLatch.await();
                System.out.println("========================恢复主线程执行==========================");
                // 数据批次计数器
                counterBatch++;
                // 模拟执行5批
                if (counterBatch > 5) {
                    break;
                }
            }
            System.out.println("循环退出,程序执行完成,counterBatch=" + counterBatch);
            // 关闭线程池
            executorService.shutdown();
        } catch (Exception e) {
            System.out.println("异常日志");
        }
    }

    /**
     * 模拟根据用户Id处理用户数据的逻辑
     */
    public static void dealUser(String userId) {
        System.out.println("ThreadName:" + Thread.currentThread().getName() + ", userId:" + userId + " 处理完成!");
    }

}

运行结果:

ThreadName:pool-1-thread-3, userId:user4 处理完成!
ThreadName:pool-1-thread-7, userId:user9 处理完成!
ThreadName:pool-1-thread-2, userId:user7 处理完成!
ThreadName:pool-1-thread-9, userId:user3 处理完成!
ThreadName:pool-1-thread-5, userId:user2 处理完成!
ThreadName:pool-1-thread-4, userId:user8 处理完成!
ThreadName:pool-1-thread-1, userId:user0 处理完成!
ThreadName:pool-1-thread-8, userId:user6 处理完成!
ThreadName:pool-1-thread-10, userId:user5 处理完成!
ThreadName:pool-1-thread-6, userId:user1 处理完成!
========================恢复主线程执行==========================
ThreadName:pool-1-thread-3, userId:user8 处理完成!
ThreadName:pool-1-thread-7, userId:user1 处理完成!
ThreadName:pool-1-thread-9, userId:user9 处理完成!
ThreadName:pool-1-thread-2, userId:user4 处理完成!
ThreadName:pool-1-thread-7, userId:user6 处理完成!
ThreadName:pool-1-thread-9, userId:user0 处理完成!
ThreadName:pool-1-thread-4, userId:user3 处理完成!
ThreadName:pool-1-thread-5, userId:user2 处理完成!
ThreadName:pool-1-thread-3, userId:user7 处理完成!
ThreadName:pool-1-thread-1, userId:user5 处理完成!
========================恢复主线程执行==========================
ThreadName:pool-1-thread-2, userId:user5 处理完成!
ThreadName:pool-1-thread-7, userId:user8 处理完成!
ThreadName:pool-1-thread-10, userId:user0 处理完成!
ThreadName:pool-1-thread-4, userId:user1 处理完成!
ThreadName:pool-1-thread-8, userId:user2 处理完成!
ThreadName:pool-1-thread-5, userId:user7 处理完成!
ThreadName:pool-1-thread-9, userId:user6 处理完成!
ThreadName:pool-1-thread-6, userId:user4 处理完成!
ThreadName:pool-1-thread-2, userId:user9 处理完成!
ThreadName:pool-1-thread-7, userId:user3 处理完成!
========================恢复主线程执行==========================
ThreadName:pool-1-thread-3, userId:user1 处理完成!
ThreadName:pool-1-thread-1, userId:user8 处理完成!
ThreadName:pool-1-thread-8, userId:user2 处理完成!
ThreadName:pool-1-thread-9, userId:user6 处理完成!
ThreadName:pool-1-thread-2, userId:user3 处理完成!
ThreadName:pool-1-thread-1, userId:user4 处理完成!
ThreadName:pool-1-thread-5, userId:user0 处理完成!
ThreadName:pool-1-thread-10, userId:user5 处理完成!
ThreadName:pool-1-thread-3, userId:user9 处理完成!
ThreadName:pool-1-thread-4, userId:user7 处理完成!
========================恢复主线程执行==========================
ThreadName:pool-1-thread-6, userId:user0 处理完成!
ThreadName:pool-1-thread-7, userId:user8 处理完成!
ThreadName:pool-1-thread-2, userId:user3 处理完成!
ThreadName:pool-1-thread-5, userId:user5 处理完成!
ThreadName:pool-1-thread-8, userId:user1 处理完成!
ThreadName:pool-1-thread-10, userId:user6 处理完成!
ThreadName:pool-1-thread-1, userId:user7 处理完成!
ThreadName:pool-1-thread-7, userId:user2 处理完成!
ThreadName:pool-1-thread-6, userId:user4 处理完成!
ThreadName:pool-1-thread-9, userId:user9 处理完成!
========================恢复主线程执行==========================
循环退出,程序执行完成,counterBatch=6

程序分析:

1)模拟从数据库每一次取出一批数据,每批数据为 10 条;

2)CountDownLatch 计数器大小设定与数据条数相同,这里就为 10;

3)然后循环 List,每一条数据创建一个线程,然后提交线程池,每一个线程处理完要调

countDown(),每次减 1。

4)主线程也就是这里的 main 线程,调用了 await() 方法,await() 方法表示等待线程池的线程

执行完成,恢复主线程执行,即 CountDownLatch 计数器为 0 时恢复主线程,进行下一次的

循环取批数据处理。从而我们可以实现每一批数据取出后,交由线程池多线程处理,

并且主线程会等待子线程都执行完成,然后才恢复执行,进行下一次的循环取批处理,

就不会出现取批次时取到正在处理的数据。

三 CountDownLatch 源码分析(jdk8)

1、CountDownLatch(int count) 构造函数

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

先从构造函数看起,传入 int 的 count,对 count 进行校验,然后 new Sync(count)。

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

Sync 为 AQS 的子类,在构造函数里面,通过 setState 设置 state 的值为 count,state 为 volatile

变量,保证多线程可见性。

2、CountDownLatch#await()

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

调用 CountDownLatch 内部类 Sync 父类 AbstractQueuedSynchronizer 的模板方法

acquireSharedInterruptibly() 尝试获取共享锁。

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    // 判断线程是否中断
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获取共享锁
    if (tryAcquireShared(arg) < 0)
        // 添加到阻塞队列,挂起线程,等待唤醒获取锁
        doAcquireSharedInterruptibly(arg);
}

第1步:Thread.interrupted() 判断线程是否中断,中断则抛出线程中断异常;

第2步:tryAcquireShared(arg) 方法尝试获取共享锁,当 state 为 0 时,返回 1 才能获取锁,

主线程会继续执行,否则返回 -1,获取锁失败,则调用 await 的线程(主线程)通过

doAcquireSharedInterruptibly(arg)方 法进行阻塞操作

这里可以结合实例理解为 main 主线程被阻塞,那么主线程在哪里被唤醒的?

在 countDown() 方法里进行主线程唤醒。

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

第3步:doAcquireSharedInterruptibly(arg) 如何阻塞主线程?

/**
 * Acquires in shared interruptible mode.
 * @param arg the acquire argument
 */
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);
    }
}

addWaiter(Node mode):初始化队里,并基于当前线程构建节点添加到队列尾部。

private Node addWaiter(Node mode) {
    // 基于当前线程构建共享模式 Node
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // 先尝试通过 compareAndSetTail 快速添加队列节点,不行再通过 enq 入队。
    Node pred = tail;
    // 添加第一个队列节点时,尾节点是空的,不会走快速添加,之后才会走CAS快速添加
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 第一次添加节点,走这里,完成队列的初始化和元素的添加
    enq(node);
    return node;
}

enq(final Node node):初始化队列并添加当前线程构建的节点到队尾。

private Node enq(final Node node) {
    for (;;) {
        // 获取尾节点
        Node t = tail;
        // 第一次循环,t是null,会进入if判断,compareAndSetHead设置new Node()到队列,
        // 这个时候队列头节点和尾节点是一个节点,都是空的
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {// 节点插入队尾
            // 第二次循环时,当前节点的前驱节点
            node.prev = t;
            // 节点添加到队尾
            if (compareAndSetTail(t, node)) {
                // t的下一个节点指向node,跟头结点建立引用,形成链表
                t.next = node;
                // 返回t(这个时候队列的头结点是new Node(),尾节点是我们传进来的node,队列里只有两个节点)
                return t;
            }
        }
    }
}

head 永远是空的节点,AQS 的双向链表是基于“哨兵节点”或者叫做“哑节点”方式实现的。

shouldParkAfterFailedAcquire(Node pred, Node node):设置节点的状态为等待唤醒状态。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

boolean parkAndCheckInterrupt():调用 LockSupport.park 暂停当前线程,

并返回线程是否中断的状态。

private final boolean parkAndCheckInterrupt() {
    // 暂停当前的线程
    LockSupport.park(this);
    // 获取线程是否中断的状态
    return Thread.interrupted();
}

调用 await() 方法的现在在这里被暂停的,后期通过 countDown() 里面的逻辑进行唤醒。 

3、CountDownLatch#countDown()

调用 countDown() 方法,每调用一次 state 就会减1。

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

调用 CountDownLatch 内部类 Sync 的 releaseShared() 方法,arg 传值为 1。

public final boolean releaseShared(int arg) {
    // 1
    if (tryReleaseShared(arg)) {
        // 2
        doReleaseShared();
        return true;
    }
    return false;
}

第1步:执行tryReleaseShared(arg)方法,返回 true 或 false,尝试去释放共享锁。

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {// 自旋减1
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;// nextc减到为0时,返回true
    }
}

即当最后一次进行 countDown() 操作时 state 为 1,即 c 为 1,则 nextc 为 0,进行 CAS 操作后,

state 变为 0,返回 true,则执行 doReleaseShared() 方法。

第2步:执行doReleaseShared():方法释放共享锁,唤醒调用 await() 等待线程

private void doReleaseShared() {
    /*
     * Ensure that a release propagates, even if there are other
     * in-progress acquires/releases.  This proceeds in the usual
     * way of trying to unparkSuccessor of head if it needs
     * signal. But if it does not, status is set to PROPAGATE to
     * ensure that upon release, propagation continues.
     * Additionally, we must loop in case a new node is added
     * while we are doing this. Also, unlike other uses of
     * unparkSuccessor, we need to know if CAS to reset status
     * fails, if so rechecking.
     */
    for (;;) {
        // 获取头节点
        Node h = head;
        // 判断头节点不为空,并且不是尾节点,即排除哨兵节点链表中还有封装线程的节点,则尝试进行线程唤醒
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {// 头结点的状态为Node.SIGNAL
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 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;
    }
}

unparkSuccessor(Node node):唤醒后续节点线程。

/**
 * Wakes up node's successor, if one exists.
 *
 * @param node the node
 */
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    // node 是外层传入的头节点,即哨兵节点,s 为头节点的后继节点,即封装线程的节点
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        // 唤醒线程
        LockSupport.unpark(s.thread);
}

主线程一开始被构建在 Node 节点中作为成员变量,被 LockSupport.park 暂停了,

这里当 state 为 0 时获取锁到锁,通过 LockSupport.unpark 唤醒主线程,当线程唤醒后,

调用 await() 的线程会继续执行,去获取到锁,继续执行代码。

四 CountDownLatch 总结

1、CountDownLatch 主要用于主线程等待子线程执行完成,恢复主线程继续执行的场景。

2、A 操作调用 countDown() 减少计数器数值,M 调用 await() 一直等待,直到 countDown()

将 state 减为 0 时恢复 M 主线程继续执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值