并发工具类:CountDownLatch有哪些用处?

在这里插入图片描述

CountDownLatch常见用法

CountDownLatch是jdk1.5之后提供的并发流程控制的工具类,它主要有如下两个方面的作用

  1. 一个线程等多个线程执行完毕,再继续自己的工作
  2. 多个线程等待一个线程的信号,然后同时开始执行

一个线程等多个线程执行完毕,再继续自己的工作

public class CountDownLatchUseCase1 {

    public static void main(String[] args) throws InterruptedException{
        CountDownLatch latch = new CountDownLatch(5);
        ExecutorService service = Executors.newFixedThreadPool(5);
        Random random = new Random();
        for (int i = 0; i < 5; i++) {
            int num = i;
            service.submit(() -> {
                try {
                    TimeUnit.SECONDS.sleep(random.nextInt(5));
                    System.out.println(num + " 号运动员完成比赛");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            });
        }
        System.out.println("等待运动员都跑完");
        latch.await();
        System.out.println("运动员都跑完,裁判宣布比赛结束");
    }

}

一种可能的结果为

等待运动员都跑完
4 号运动员完成比赛
1 号运动员完成比赛
0 号运动员完成比赛
3 号运动员完成比赛
2 号运动员完成比赛
运动员都跑完,裁判宣布比赛结束

这里写图片描述

多个线程等待一个线程的信号,然后同时开始执行

public class CountDownLatchUseCase2 {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("5秒后比赛正式开始");
        CountDownLatch latch = new CountDownLatch(1);
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            int num = i;
            service.submit(() -> {
                System.out.println(num + " 号运动员准备完毕,等待开赛");
                try {
                    latch.await();
                    System.out.println(num + " 号运动员开始跑步");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        TimeUnit.SECONDS.sleep(5);
        System.out.println("5秒准备时间已经过去,比赛开始!");
        latch.countDown();
    }
}

1种可能的结果为

5秒后比赛正式开始
1 号运动员准备完毕,等待开赛
0 号运动员准备完毕,等待开赛
2 号运动员准备完毕,等待开赛
3 号运动员准备完毕,等待开赛
4 号运动员准备完毕,等待开赛
5秒准备时间已经过去,比赛开始!
0 号运动员开始跑步
4 号运动员开始跑步
3 号运动员开始跑步
2 号运动员开始跑步
1 号运动员开始跑步

共享锁的应用

在这里插入图片描述

CountDownLatch的源码非常少,主要是通过操作AbstractQueuedSynchronizer中共享锁的api来实现的,我们在AQS文章中其实基本上把CountDownLatch的源码盘了个遍,这篇文章再接着谢谢,加深记忆

独占锁和共享锁的异同

相同点:获取锁失败的节点会被放入队列

不同点:独占锁只有一个线程能获取锁,当释放锁时需要唤醒header节点后面的一个节点。而共享锁多个线程都可以获取锁,当释放锁时需要唤醒header后面的所有有效节点

public class CountDownLatch {

	// 用来阻塞线程,直到state变为0
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

	// 将state-1
    public void countDown() {
        sync.releaseShared(1);
    }
}

从源码中可以看到CountDownLatch最主要的2个方式是调用AbstractQueuedSynchronizer中的如下方法来实现的

acquireSharedInterruptibly:获取响应中断的共享锁
releaseShared:释放共享锁

获取共享锁(await)

当我们使用CountDownLatch时,构造函数只有一个,且必须传入一个整数,这个整数值会被赋给AQS中的成员变量state,表示共享锁的数量

// CountDownLatch#await
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
// AbstractQueuedSynchronizer#acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 加锁失败
    if (tryAcquireShared(arg) < 0)
        // 入队并阻塞
        doAcquireSharedInterruptibly(arg);
}
// CountDownLatch.Sync#tryAcquireShared
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

因为CountDownLatch的使用场景一般都是调用await,然后调用countDown将阻塞的线程唤醒,所以一般情况下tryAcquireShared返回的都是-1,线程被阻塞

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

当 r >=0 时,表示当前线程获得了锁,然后把同步队列中的线程也都唤醒,这样其他线程也就能获得锁了。

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

doReleaseShared唤醒同步队列中的所有阻塞线程,后面释放锁的时候会再次分析

释放共享锁(countDown)

// CountDownLatch#countDown
public void countDown() {
    sync.releaseShared(1);
}
// AbstractQueuedSynchronizer#releaseShared
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

state - 1,当state = 0时,唤醒阻塞的线程

// CountDownLatch.Sync#tryReleaseShared
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        // 为0不需要释放,因为构造函数中state=0时,线程也没有阻塞
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

唤醒同步队列中所有的阻塞线程

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 头节点的后继节点需要被唤醒
            if (ws == Node.SIGNAL) {
            	// 这里用cas,避免多次执行unpark,setHeadAndPropagate和releaseShared这2个方法都会调用到这里
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 唤醒后继节点
                unparkSuccessor(h);
            }
            // 后继节点暂时不需要唤醒,状态更新为PROPAGATE,确保后续可以传递给后继节点
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 头节点没有更改,退出循环,当前节点是队列中唯一节点
        // 头节点发生变化,其他线程获取了共享锁,继续循环
        if (h == head)                   // loop if head changed
            break;
    }
}

总结

await

共享锁获取成功(计数器为0),从第一个节点一次唤醒后继节点,实现共享状态传播
共享锁获取失败(计数器不为0),阻塞线程

countDown

将state-1,为0,则唤醒所有线程,不为0,则什么都不做

参考博客

[1]http://www.ideabuffer.cn/2017/03/19/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3AbstractQueuedSynchronizer%EF%BC%88%E4%BA%8C%EF%BC%89/#more
[2]https://www.cnblogs.com/windrui/p/9033319.html
PROPAGATE状态的作用
[3]https://jishuin.proginn.com/p/763bfbd33115

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
CyclicBarrier和CountDownLatchJava中的两个并发工具类,它们有以下区别: 1. 作用对象不同:CyclicBarrier作用于线程,要等待固定数量的线程都到达栅栏位置才能继续执行;而CountDownLatch作用于事件,只需等待数字倒数到0。 2. 执行动作不同:CyclicBarrier可以设置执行动作barrierAction,当所有线程都到达栅栏位置时,会执行这个动作;而CountDownLatch没有这个功能。 下面是一个使用CyclicBarrier和CountDownLatch的示例: ```java import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; public class BarrierExample { public static void main(String[] args) { int threadCount = 3; // 使用CyclicBarrier CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount, () -> { System.out.println("所有线程都到达栅栏位置,开始执行barrierAction"); }); for (int i = 0; i < threadCount; i++) { new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " 到达栅栏位置"); cyclicBarrier.await(); System.out.println(Thread.currentThread().getName() + " 继续执行"); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } }).start(); } // 使用CountDownLatch CountDownLatch countDownLatch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " 执行任务"); countDownLatch.countDown(); countDownLatch.await(); System.out.println(Thread.currentThread().getName() + " 继续执行"); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java识堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值