JUC(一)CountDownlatch:如何让一些线程等待另外一些线程执行完再执行

一、案例分析:

1.假如我们现在有一个需求

 (1)全班10个同学出游,司机要等待这10个同学都上车之后才出发
 (2)假如我们要通过50个线程,打印5000以内的偶数,最后计算总共消耗的时间(代码实现附在文末)
 (3)线程a计算公司支出,线程b计算公司盈利,线程c等待ab执行完成后计算公司利润。

2.需求实现:

我们实现需求1吧,其他自行实现,欢迎尝试。 按照正常写法会是这样的:

public class juctest {
    /**
     * author:liganggang
     * 班级出游,司机等全班10个同学全部上车之后再出发
     */
        public static void main(String[] args) throws Exception {
            for (int i = 0; i < 10; i++) {
                new Thread(new Runnable(){
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName()+"同学上车");
                    }
                },String.valueOf(i)+"号").start();
            }
            System.out.println(Thread.currentThread().getName()+"\t*********司机开车走人");
        }
}

但实际执行结果却是如下:
在这里插入图片描述

3.问题:

这时你会发现,司机没有等所有同学都上车就开车走人了,这就gg了

4.改造:

这里我们使用jdk提供的tools工具countDownLatch这个同步辅助类来实现

5.改造的代码:

public class juctest {
    /**
     * author:liganggang
     * 班级出游,司机等全班10个同学全部上车之后再出发
     */
        public static void main(String[] args) throws Exception {
            CountDownLatch countDownLatch = new CountDownLatch(10);
            for (int i = 0; i < 10; i++) {
                new Thread(new Runnable(){
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName()+"同学上车");
                        countDownLatch.countDown();
                    }
                },String.valueOf(i)+"号").start();
            }
            countDownLatch.await();
            System.out.println(Thread.currentThread().getName()+"\t*********司机开车走人");
        }
}

改造后运行结果:
在这里插入图片描述
你会发现,我们改造的代码中只加了3行代码,就实现了main线程在所有线程执行完后再执行,这就是jdk提供的juc包的强大之处,接下来我们了解下countDownLatch是什么及实现。

6.countDownlatch解读:

又名闭锁,是一种同步方法,可以延迟线程的进度直到其终止状态,可以用来确保某些活动直到其他活动都执行玩才解需执行。
它有俩种典型的用法:

让一些线程阻塞直到另一些线程完成一系列操作后再执行;

  • 将 CountDownLatch 的计数器初始化为n :new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1

  • CountDownLatch主要有两个方法:
    (1)当一个或多个线程调用await方法时,调用线程会被阻塞
    (2)其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)

  • 当计数器的值变为0时,因调用await方法被阻塞的线程会被唤醒,继续执行

实现多个线程开始执行任务的最大并行性

注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。

7.源码实现:

以下是countDownLatch的全部实现代码,没错,就是这几个方法,是不是很简单

package java.util.concurrent;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class CountDownLatch {
    
private static final class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 4982264981922014374L;

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

int getCount() {
    return getState();
}

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

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

}


private final Sync sync;
//构造一个给定计数的countDownLatch
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
//使当前线程在锁存器倒计数至0之前一直等待,除非线程被中断
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
//使当前线程在锁存器倒计数至0之前一直等待,除非线程被中断或超出了指定的等待时间
public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
//递减锁存器的计数,如果计数到达0.则释放所有等待的线程
public void countDown() {
    sync.releaseShared(1);
}
//返回当前计数
public long getCount() {
    return sync.getCount();
}
// 返回标识此锁存器及其状态的字符串。
public String toString() {
    return super.toString() + "[Count = " + sync.getCount() + "]";
}

}

1.构造方法:创建一个Sync对象,而Sync继承AQS。所以这里我们完全又必要了解AQS原理,具体请看另外一篇博客。

2.Sync 是CountDownLatch的内部私有类,组合到CountDownLatch里:
在AQS中state是一个private volatile int类型的对象。CountDownLatch使用state来计数,CountDownLatch的getCount最终调用的是AQS的getState(),返回state进行计数。

3.await()方法:调用AQS的acquireSharedInterruptibly方法

public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//1.获取共享锁
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//判断线程是否为中断状态,如果是抛出interruptedException
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取共享锁,尝试成功就返回,否则调用doAcquireSharedInterruptibly方法
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
//2.尝试获取共享锁,重写AQS里面的方法protected int tryAcquireShared(int acquires) {
//锁状态 == 0,表示所没有被任何线程所获取,即是可获取的状态,否则锁是不可获取的状态
return (getState() == 0) ? 1 : -1;
}
//3.doAcquireSharedInterruptibly方法会使得当前线程一直等待,直到当前线程获取到锁(或被中断)才返回
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//创建“当前线程”的Node节点,且node中记录的锁是“共享锁”类型,并将节点添加到CLH队列末尾。
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);
}
}
/*说明:4.shouldParkAfterFailedAcquire 返回当前线程是否应该阻塞
(01) 关于waitStatus请参考下表(中扩号内为waitStatus的值),更多关于waitStatus的内容,可以参考前面的Node类的介绍。

CANCELLED[1] -- 当前线程已被取消
SIGNAL[-1] -- “当前线程的后继线程需要被unpark(唤醒)”。一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被release或cancel掉,因此需要唤醒当前线程的后继线程。
CONDITION[-2] -- 当前线程(处在Condition休眠状态)在等待Condition唤醒
PROPAGATE[-3] -- (共享锁)其它线程获取到“共享锁”
[0] -- 当前线程不属于上面的任何一种状态。
(02) shouldParkAfterFailedAcquire()通过以下规则,判断“当前线程”是否需要被阻塞。

规则1:如果前继节点状态为SIGNAL,表明当前节点需要被unpark(唤醒),此时则返回true。
规则2:如果前继节点状态为CANCELLED(ws>0),说明前继节点已经被取消,则通过先前回溯找到一个有效(非CANCELLED状态)的节点,并返回false。
规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,并返回false。
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 前驱节点的状态
int ws = pred.waitStatus;
// 如果前驱节点是SIGNAL状态,则意味着当前线程需要unpark唤醒,此时返回true
if (ws == Node.SIGNAL)
return true;
// 如果前继节点是取消的状态,则设置当前节点的“当前前继节点为”原节点的前继节点
if (ws > 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. compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
  1. countDown()源码 :
//1.该方法其实调用AQS中的releaseShared(1)释放共享锁方法。
public void countDown() {
sync.releaseShared(1);
}
//2.目的是让当前线程释放它所持有的共享锁,它首先会通过tryReleaseShared()去尝试释放共享锁。尝试成功,则直接返回;尝试失败,则通过doReleaseShared()去释放共享锁。public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
//3.tryReleaseShared()在CountDownLatch.java中被重写,释放共享锁,将锁计数器-1protected 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;
}
}

附录:

需求2代码实现

public class LatchDemo implements Runnable {
    private CountDownLatch latch;
    public LatchDemo(CountDownLatch latch) {
        this.latch = latch;
    }
    @Override
    public void run() {
        synchronized (this) {
            try {
                for (int i = 0; i < 5000; i++) {
                    if (i % 2 == 0) {
                        System.out.println(i);
                    }
                }
            } finally {
                latch.countDown();
            }
        }
    }}
public class CountDownLatchTest {
    public static void main(String[] args) {
        final CountDownLatch latch = new CountDownLatch(50);
        LatchDemo ld = new LatchDemo(latch);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 50; i++) {
            new Thread(ld).start();
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("耗费时间为:" + (end - start));
    }}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值