【并发编程】闭锁CountDownLatch从入门到源码精通

什么是CountDownLatch

  • CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。
  • CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值(count)由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回。count不会被重置!
  • 如果你需要一个重置count的版本,那么请考虑使用CyclicBarrier。

CountDownLatch的使用场景

  • CountDownLatch一般用作多线程倒计时计数器,强制它们等待其他一组(CountDownLatch的初始化决定)任务执行完成。
  • 场景1:让多个线程等待单个线程。
  • 场景2:让单个线程等待多个线程。

CountDownLatch的多个线程等待的场景验证。

import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest1 {
	 
    public static void main(String[] args) throws InterruptedException {
    	// 定义闭锁
        CountDownLatch countDownLatch = new CountDownLatch(1);
        // 定义5个线程
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                	System.out.println(System.currentTimeMillis() + "【" + Thread.currentThread().getName() + "】" + "准备完毕!");
                    //准备完毕……等待号令
                    countDownLatch.await();
                    System.out.println(System.currentTimeMillis() + "【" + Thread.currentThread().getName() + "】" + "开始执行……");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

    	System.out.println(System.currentTimeMillis() + "【" + Thread.currentThread().getName() + "】" + "开始准备!");
        Thread.sleep(2000);// 裁判准备发令
        System.out.println(System.currentTimeMillis() + "【" + Thread.currentThread().getName() + "】" + "准备完毕!");
        countDownLatch.countDown();// 执行发令
    }
}
  • 执行结果:main线程执行完,等待的线程才会执行!
    多个线程等待的结果.png

CountDownLatch的单个线程等待的场景验证。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;

public class CountDownLatchTest1 {
	public static void main(String[] args) throws Exception {

		CountDownLatch countDownLatch = new CountDownLatch(6);
		for (int i = 0; i < 10; i++) {
			final int index = i;
			new Thread(() -> {
				try {
					Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(1000));
					System.out.println(Thread.currentThread().getName() + " 正常执行第" + index + "个");

					countDownLatch.countDown();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}).start();
		}

		// 主线程在阻塞,当计数器==0,就唤醒主线程往下执行。
		countDownLatch.await();
		System.out.println("主线程被唤醒!");

	}
}

  • 执行结果:倒计时减为0,main线程才会执行!
    单个线程等待的结果.png

CountDownLatch的常用方法。

// 调用 await() 方法的线程会被挂起,它会等待直到 count 值为 0 才继续执行
public void await() throws InterruptedException { };  
// 和 await() 类似,若等待 timeout 时长后,count 值还是没有变为 0,不再等待,继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
// 会将 count 减 1,直至为 0
public void countDown() { };

CountDownLatch的实现原理

  • 底层基于 AbstractQueuedSynchronizer 实现,CountDownLatch 构造函数中指定的count直接赋给AQS的state;
  • 每次countDown()则都是release(1)减1,最后减到0时unpark阻塞线程;
  • 最后一个执行countdown方法的线程去unpark阻塞线程。

CountDownLatch与Thread.join的区别

  • CountDownLatch的作用就是允许一个或多个线程等待其他线程完成操作,看起来有点类似join() 方法,但其提供了比 join() 更加灵活的API。
  • CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作。而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。

CountDownLatch与CyclicBarrier的区别

  • CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次
  • CyclicBarrier还提供getNumberWaiting(可以获得CyclicBarrier阻塞的线程数量)、isBroken(用来知道阻塞的线程是否被中断)等方法。
  • CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
  • CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同。
  • CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,再执行。CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行。
  • CyclicBarrier 还可以提供一个 barrierAction,合并多线程计算结果。
  • CyclicBarrier是通过ReentrantLock的"独占锁"和Conditon来实现一组线程的阻塞唤醒的,而CountDownLatch则是通过AQS的“共享锁”实现

CountDownLatch构造方法源码分析

/**
 * CountDownLatch的构造方法
 */
public CountDownLatch(int count) {
    // 传入的数量小于0,抛异常
    if (count < 0) throw new IllegalArgumentException("count < 0");
    // 调用Sync的构造方法
    this.sync = new Sync(count);
}

/**
 * Sync的构造方法
 */
Sync(int count) {
    // 设置状态数量为count
    setState(count);
}

CountDownLatch的阻塞方法:await()

/**
 * 无参的await方法
 */
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

/**
 * 有时间参数的await方法
 */
public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

/**
 * 有时间校验的参数获取共享锁
 */
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    // 如果线程被中断,抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获取锁,大于等于0说明没有锁了。没有锁的时候尝试获取共享锁或者中断
    return tryAcquireShared(arg) >= 0 ||
        doAcquireSharedNanos(arg, nanosTimeout);
}

/**
 * 尝试获取共享锁或者中断,多了时间的校验
 */
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    // 纳秒的时间小于0,直接返回false。时间上不需要锁了
    if (nanosTimeout <= 0L)
        return false;
    // 得到最后期限的世界
    final long deadline = System.nanoTime() + nanosTimeout;
    // 添加到等待队列
    final Node node = addWaiter(Node.SHARED);
    // 定义失败标志为true
    boolean failed = true;
    try {
        for (;;) {
            // 得到当前节点的前驱节点
            final Node p = node.predecessor();
            // 如果前置节点是头结点
            if (p == head) {
                // 尝试去获取锁
                int r = tryAcquireShared(arg);
                // 大于等于0,说明state值为0
                if (r >= 0) {
                    // 设置头结点和链表
                    setHeadAndPropagate(node, r);
                    // 取消节点的引用,方便GC去回收
                    p.next = null; // help GC
                    // 将失败标记改为false
                    failed = false;
                    // 跳出方法!
                    return true;
                }
            }
            // 计算最后期限与当前时间的差值
            nanosTimeout = deadline - System.nanoTime();
            // 时间小于0,直接释放,可以获取锁!
            if (nanosTimeout <= 0L)
                return false;
            // 代码执行到这里,说明尝试获取锁,但是获取锁失败了。
            // 阻塞前的准备工作操作成功(状态是-1的时候成功)
            // 状态是-1的时候,并且这个时间大于默认值spinForTimeoutThreshold:1000L的时候,才会进行尝试阻塞略及
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                // 准备去阻塞线程
                LockSupport.parkNanos(this, nanosTimeout);
            // 如果线程被中断,抛出异常
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        // 上面代码抛出异常的时候,会执行这里的逻辑
        if (failed)
            // 取消获取锁的逻辑
            cancelAcquire(node);
    }
}

/**
 * 阻塞逻辑,多了时间的校验
 */
public static void parkNanos(Object blocker, long nanos) {
    if (nanos > 0) {
        // 得到当前线程
        Thread t = Thread.currentThread();
        // 设置被谁阻塞
        setBlocker(t, blocker);
        // 阻塞当前线程
        UNSAFE.park(false, nanos);
        // 唤醒后,清空blocker信息
        setBlocker(t, null);
    }
}

/**
 * 获取可中断的共享资源
 */
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    // 如果线程被中断,抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获取锁,这里不满足直接抛出异常
    if (tryAcquireShared(arg) < 0)
        // 尝试获取共享锁或者中断。CountDownLatch进入这里的时候state肯定不为0
        doAcquireSharedInterruptibly(arg);
}

/**
 * 尝试获取锁
 */
protected int tryAcquireShared(int acquires) {
    // 只有当前状态为0的时候返回正数!其他都是负数
    return (getState() == 0) ? 1 : -1;
}

/**
 * 尝试获取共享锁或者中断
 */
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 添加到等待队列
    final Node node = addWaiter(Node.SHARED);
    // 定义失败标记为true
    boolean failed = true;
    try {
        for (;;) {
            // 得到当前节点的前驱节点
            final Node p = node.predecessor();
            // 如果前置节点是头结点
            if (p == head) {
                 // 尝试去获取锁:公平锁与非公平锁实现不一致!
                 int r = tryAcquireShared(arg);
                 // 获取的资源数大于0(获取到了资源,Semaphore)
                 if (r >= 0) {
                     // 设置头结点和链表,准备唤醒后继节点
                     setHeadAndPropagate(node, r);
                     // 取消节点的引用,方便GC去回收
                     p.next = null; // help GC
                     // 将失败标记改为false
                     failed = false;
                     // 跳出方法!
                     return;
                 }
            }
            // 代码执行到这里,说明尝试获取锁,但是获取锁失败了。
            // 阻塞前的准备工作操作成功(状态是-1的时候成功)
            // 将线程阻塞,等待他去唤醒。唤醒后返回线程的中断状态!
            if (shouldParkAfterFailedAcquire(p, node) &&
                 parkAndCheckInterrupt())
                 throw new InterruptedException();
        }
    } finally {
        // 上面代码抛出异常的时候,会执行这里的逻辑
        if (failed)
            // 取消获取锁的逻辑
            cancelAcquire(node);
    }
}

/**
 * 添加线程到同步队列
 */
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:尝试enq的快速路径;故障时备份到完整enq
    // 得到之前的尾节点
    Node pred = tail;
    // 尾节点不为空,说明队列存在
    if (pred != null) {
        // 设置当前线程节点的上一个节点是之前的尾节点
        node.prev = pred;
        // cas尝试将尾节点设置为当前线程的节点
        if (compareAndSetTail(pred, node)) {
            // 设置之前尾节点,现在的倒数第二个节点的下一个节点是当前线程节点
            pred.next = node;
            // 返回当前节点
            return node;
        }
    }
    // CAS失败或者节点没有创建,会执行这入队的操作。详细请看下面的代码
    enq(node);
    // 入队成功后,返回当前节点
    return node;
}

/**
 * 设计精髓:100%创建队列或者100%入队
 */
private Node enq(final Node node) {
    for (;;) {
        // 得到之前的尾节点
        Node t = tail;
        // 之前的尾节点为空,需要进行初始化队列
        if (t == null) { // Must initialize
            // 通过CAS的方式将头节点设置为当前节点
            if (compareAndSetHead(new Node()))
                // 头结点设置成功后,复制给尾节点。只有一个节点的状态
                tail = head;
        } else {
            // 设置当前线程节点的上一个节点是之前的尾节点
            node.prev = t;
            // cas尝试将尾节点设置为当前线程的节点
            if (compareAndSetTail(t, node)) {
                // 设置之前尾节点,现在的倒数第二个节点的下一个节点是当前线程节点
                t.next = node;
                // 返回当前节点!注意:这里是这个方法唯一返回的地方!也就是说初始化后还会继续循环一次来设置上一个下一个节点,然后进行返回。
                return t;
            }
        }
    }
}

/**
 * 获取锁失败后的准备逻辑,阻塞前的准备逻辑
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取前驱当前节点的等待状态
    int ws = pred.waitStatus;
    // 状态为-1的时候:当前节点的后继节点包含的线程需要运行,也就是unpark;
    if (ws == Node.SIGNAL)
        /*
        * This node has already set status asking a release
        * to signal it, so it can safely park.
        */
        // 前驱当前节点状态已经设置为SIGNAL,可以进行安全的阻塞
        return true;
    // 大于0(CANCELLED状态):表示当前的线程被取消;
    if (ws > 0) {
        /*
        * Predecessor was cancelled. Skip over predecessors and
        * indicate retry.
        */
        // 前驱节点已经因为超时或响应了中断,需要跳过这些状态大于0的节点,直到找到一个状态不是大于0的。
        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.
        */
        // 针对于ReentrantLock,到这里的状态只能为0或者PROPAGATE(-3)
        // 通过CAS将前置节点的状态设置为SIGNAL(-1)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

/**
 * 状态为SIGNAL(-1)成功。他需要排队,所以直接调用park方法进行阻塞
 */
private final boolean parkAndCheckInterrupt() {
    // 阻塞
    LockSupport.park(this);
    // unpark之后,返回当前的中断状态,并清除中断标志位
    return Thread.interrupted();
}

/**
 * 设置头结点和链表
 */
private void setHeadAndPropagate(Node node, int propagate) {
    // 得到旧的头
    Node h = head; // Record old head for check below
    // 将当前节点设置为头节点,所属线程设置为null,前驱节点设置为null
    setHead(node);
    // propagate > 0:说明还有剩余共享锁可以获取,那么短路后面条件。
    // h == null与(h = head) == null是一个防止控制住的标准写法,因为经过了addWaiter方法,肯定会有一个节点!
    // h.waitStatus < 0:状态小于0。在调用doReleaseShared的时候回cas状态到0!说明有其他线程调用了doReleaseShared()。第一个是旧的头结点,第二个是新的头结点(当前节点)!
    // (h = head) == null:除了防止空指针,这里会将当前节点复制到h变量上
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        // 得到当前节点的下一个节点
        Node s = node.next;
        // 下一个节点为空(node是队尾)或者下一个节点是共享模式
        if (s == null || s.isShared())
            // 唤醒后继的节点并且保证继续传播
            doReleaseShared();
    }
}

/**
 * 唤醒后继的节点并且保证继续传播
 */
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;
            // 头结点状态为-1:当前节点的后继节点包含的线程需要运行
            if (ws == Node.SIGNAL) {
                // cas将当前头结点状态设置为0(当前节点在sync队列中,等待着获取锁)失败。进入下次循环!CAS成功继续执行!
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 执行唤醒锁的流程
                unparkSuccessor(h);
            }
            // 头结点的状态为0并且不能变为cas状态到-3(后续的acquireShared能够得以执行),继续执行!否则进入下次循环
            else if (ws == 0 &&
                    !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 如果头部改变,则继续循环。没有改变就跳出循环!
        if (h == head)                   // loop if head changed
            break;
    }
}

/**
 * 取消获取锁的逻辑
 */
private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    // 忽略节点不存在的时候
    if (node == null)
        return;

    // 设置当前节点的线程为null
    node.thread = null;

    // Skip cancelled predecessors:有前驱节点被取消,跳过所有被取消的
    // 得到前驱节点
    Node pred = node.prev;
    // 前驱节点的状态大于0,被取消
    while (pred.waitStatus > 0)
        // 将前驱结点的前驱结点设置为当前节点的前驱结点。简单理解就是将当前节点的前驱节点设置为第一个找到的正常状态(<=0)的前驱节点
        node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    // 获取当前节点的下一个节点
    Node predNext = pred.next;

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    // 将当前节点状态设置为1(取消状态)。这里不用CAS的原因是这个执行完其他线程会跳过取消状态,这个执行前无其他线程在执行!
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
    // 如果当前节点是尾节点,将尾节点设置为上一个节点。简单理解就是移除当前节点。
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // 进入else说明node不是队尾(或者是队尾但是cas队尾失败(其实结果也不是队尾,因为被别的线程抢先了))
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        // 定义一个状态标识
        int ws;
        // 筛选后的前驱节点不是头结点
        // 并且当前节点状态为-1(等待唤醒)或者(当前节点不在运行或者不被取消(<= 0)并且可以将当前节点CAS到-1状态(等待唤醒))
        // 并且前驱节点有线程持有!
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            // 得到当前节点的下一个节点
            Node next = node.next;
            // 下一个节点不为空,并且下一个节点没有被取消
            if (next != null && next.waitStatus <= 0)
                // CAS将前一个节点与下一个节点连接。简单理解就是跳过(取消)当前节点!
                compareAndSetNext(pred, predNext, next);
        } else {
            // 唤醒下一个不被取消的节点!
            unparkSuccessor(node);
        }
        // 当前节点的下一个节点取消指向
        node.next = node; // help GC
    }
}

CountDownLatch的倒计时countDown()方法

/**
 * 倒计时方法
 */
public void countDown() {
    // 直接调用释放共享锁的逻辑
    sync.releaseShared(1);
}

/**
 * 释放共享锁的逻辑
 */
public final boolean releaseShared(int arg) {
    // 尝试去释放共享锁。只有state大于0并且获取成功才会返回true
    if (tryReleaseShared(arg)) {
        // 唤醒后继的节点并且保证继续传播
        doReleaseShared();
        // 整体返回true,表示释放共享锁成功
        return true;
    }
    // 锁的数量大于0或者在释放锁的时候已经为0的时候为false
    return false;
}

/**
 * 尝试去释放共享锁
 */
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        // 获取状态:锁的数量,构造方法传入的数量
        int current = getState();
        // 得到如果释放够的总数量
        int next = current + releases;
        // 大于int的最大值,next变为负数!溢出了,抛异常!
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        // CAS将持有的状态(剩余资源数)设置为新的值
        if (compareAndSetState(current, next))
            return true;
    }
}

/**
 * 尝试去释放共享锁
 */
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    // 递减计数;转换到零时发出信号
    for (;;) {
        // 获取状态:锁的数量,构造方法传入的数量
        int c = getState();
        // 锁的数量已经为0的时候,返回false
        if (c == 0)
            return false;
        // 共享说数量减一
        int nextc = c-1;
        // cas尝试将减一后锁的数量设置到state上
        if (compareAndSetState(c, nextc))
            // 返回锁的状态是否为0。为0返回true,负责返回false
            return nextc == 0;
    }
}

结束语

  • 获取更多有价值的文章,让我们一起成为架构师!
  • 关注公众号,可以让你对MySQL有非常深入的了解
  • 关注公众号,每天持续高效的了解并发编程!
  • 关注公众号,后续持续高效的了解spring源码!
  • 这个公众号,无广告!!!每日更新!!!
    作者公众号.jpg
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值