并发(十)--CountDownLatch、CyclicBarrier和Semaphore

并发(十)–CountDownLatch、CyclicBarrier和Semaphore

一、CountDownLatch:

CountDownLatch所描述的是”在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待“。在API中是这样描述的:

用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。

在这里插入图片描述

CountDownLatch是通过一个计数器来实现的当我们在new 一个CountDownLatch对象的时候需要带入该计数器值,该值就表示了线程的数量。每当一个线程完成自己的任务后,计数器的值就会减1。当计数器的值变为0时,就表示所有的线程均已经完成了任务,然后就可以恢复等待的线程继续执行了。

要实现主线程等待其他线程完成sheet的解析操作,最简答的做法就是使用join()

当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入join线程执行完为止。

join用于让当前线程等待join线程执行结束。其实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远等待。其中wait(0)表示永远等待下去。

CountDownLatch结构如下:

在这里插入图片描述
通过上面的结构图我们可以看到,CountDownLatch内部依赖Sync实现,而Sync继承AQS。CountDownLatch仅提供了一个构造方法:

CountDownLatch(int count): 构造一个用给定计数初始化的 CountDownLatch

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

sync为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) {
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

通过这个内部类Sync我们可以清楚地看到CountDownLatch是采用共享锁来实现的。

await():

CountDownLatch提供await()方法来使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断,定义如下:

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

await其内部使用AQS的acquireSharedInterruptibly(int arg):

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

在内部类Sync中重写了tryAcquireShared(int arg)方法:

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

getState()获取同步状态,其值等于计数器的值,从这里我们可以看到如果计数器值不等于0,则会调用doAcquireSharedInterruptibly(int arg),该方法为一个自旋方法会尝试一直去获取同步状态:

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) {
                    /**
                     * 对于CountDownLatch而言,如果计数器值不等于0,那么r 会一直小于0
                     */
                    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);
        }
    }

countDown():

CountDownLatch提供countDown() 方法递减锁存器的计数,如果计数到达零,则释放所有等待的线程。

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

内部调用AQS的releaseShared(int arg)方法来释放共享锁同步状态:

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

tryReleaseShared(int arg)方法被CountDownLatch的内部类Sync重写:

 protected boolean tryReleaseShared(int releases) {
        for (;;) {
            //获取锁状态
            int c = getState();
            //c == 0 直接返回,释放锁成功
            if (c == 0)
                return false;
            //计算新“锁计数器”
            int nextc = c-1;
            //更新锁状态(计数器)
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }

总结:

CountDownLatch内部通过共享锁实现。在创建CountDownLatch实例时,需要传递一个int型的参数:count,该参数为计数器的初始值,也可以理解为该共享锁可以获取的总次数。当某个线程调用await()方法,程序首先判断count的值是否为0,如果不会0的话则会一直等待直到为0为止。当其他线程调用countDown()方法时,则执行释放共享锁状态,使count值 - 1。当在创建CountDownLatch时初始化的count参数,必须要有count线程调用countDown方法才会使计数器count等于0,锁才会释放,前面等待的线程才会继续运行。注意CountDownLatch不能回滚重置

使用场景:

  • 开始执行前等待n个线程完成
  • 各自任务
  • 死锁检测

应用示例:

示例仍然使用开会案例。老板进入会议室等待5个人全部到达会议室才会开会。所以这里有两个线程老板等待开会线程、员工到达会议室:

public class CountDownLatchTest {
    private static CountDownLatch countDownLatch = new CountDownLatch(5);

    /**
     * Boss线程,等待员工到达开会
     */
    static class BossThread extends Thread{
        @Override
        public void run() {
            System.out.println("Boss在会议室等待,总共有" + countDownLatch.getCount() + "个人开会...");
            try {
                //Boss等待
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("所有人都已经到齐了,开会吧...");
        }
    }

    //员工到达会议室
    static class EmpleoyeeThread  extends Thread{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ",到达会议室....");
            //员工到达会议室 count - 1
            countDownLatch.countDown();
        }
    }

    public static void main(String[] args){
        //Boss线程启动
        new BossThread().start();

        for(int i = 0 ; i < countDownLatch.getCount() ; i++){
            new EmpleoyeeThread().start();
        }
    }
}

运行结果:

在这里插入图片描述

二、CyclicBarrier:

CyclicBarrier的里面意思是:可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程达到一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

通俗点讲就是:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

实现分析:

CyclicBarrier的结构如下:

在这里插入图片描述
通过上图我们可以看到CyclicBarrier的内部是使用重入锁ReentrantLock和Condition。它有两个构造函数:

CyclicBarrier(int parties):创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。每个线程调用await方法告诉我已经到达了屏障,然后当前线程被阻塞

CyclicBarrier(int parties, Runnable barrierAction) :创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景

public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }

    public CyclicBarrier(int parties) {
        this(parties, null);
    }

CyclicBarrier中最重要的方法莫过于await()方法,在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。如下:

public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);//不超时等待
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

await()方法内部调用dowait(boolean timed, long nanos)方法:

private int dowait(boolean timed, long nanos)
            throws InterruptedException, BrokenBarrierException,
            TimeoutException {
        //获取锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //分代
            final Generation g = generation;

            //当前generation“已损坏”,抛出BrokenBarrierException异常
            //抛出该异常一般都是某个线程在等待某个处于“断开”状态的CyclicBarrie
            if (g.broken)
                //当某个线程试图等待处于断开状态的 barrier 时,或者 barrier 进入断开状态而线程处于等待状态时,抛出该异常
                throw new BrokenBarrierException();

            //如果线程中断,终止CyclicBarrier
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

            //进来一个线程 count - 1
            int index = --count;
            //count == 0 表示所有线程均已到位,触发Runnable任务
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    //触发任务
                    if (command != null)
                        command.run();
                    ranAction = true;
                    //唤醒所有等待线程,并更新generation
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }


            for (;;) {
                try {
                    //如果不是超时等待,则调用Condition.await()方法等待
                    if (!timed)
                        trip.await();
                    else if (nanos > 0L)
                        //超时等待,调用Condition.awaitNanos()方法等待
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)
                    throw new BrokenBarrierException();

                //generation已经更新,返回index
                if (g != generation)
                    return index;

                //“超时等待”,并且时间已到,终止CyclicBarrier,并抛出异常
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            //释放锁
            lock.unlock();
        }
    }

其实await()的处理逻辑还是比较简单的:如果该线程不是到达的最后一个线程,则他会一直处于等待状态除非发生以下情况

  • 最后一个线程到达,即index == 0
  • 超出了指定时间(超时等待)
  • 其他的某个线程中断当前线程
  • 其他的某个线程中断另一个等待的线程
  • 其他的某个线程在等待barrier超时
  • 其他的某个线程在此barrier调用reset()方法。reset()方法用于将屏障重置为初始状态。

当barrier损坏了或者有一个线程中断了,则通过breakBarrier()来终止所有的线程

private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }

在breakBarrier()中除了将broken设置为true,还会调用signalAll将在CyclicBarrier处于等待状态的线程全部唤醒。

当所有线程都已经到达barrier处(index == 0),则会通过nextGeneration()进行更新换地操作,在这个步骤中,做了三件事:唤醒所有线程,重置count,generation。

 private void nextGeneration() {
        trip.signalAll();
        count = parties;
        generation = new Generation();
    }

CyclicBarrier同时也提供了await(long timeout, TimeUnit unit) 方法来做超时控制,内部还是通过调用doawait()实现的。

应用场景:

CyclicBarrier试用与多线程结果合并的操作,用于多线程计算数据,最后合并计算结果的应用场景。比如我们需要统计多个Excel中的数据,然后等到一个总结果。我们可以通过多线程处理每一个Excel,执行完成后得到相应的结果,最后通过barrierAction来计算这些线程的计算结果,得到所有Excel的总和。

应用示例:

比如我们开会只有等所有的人到齐了才会开会,如下:

public class CyclicBarrierTest {
    private static CyclicBarrier cyclicBarrier;

    static class CyclicBarrierThread extends Thread{
        public void run() {
            System.out.println(Thread.currentThread().getName() + "到了");
            //等待
            try {
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args){
        cyclicBarrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("人到齐了,开会吧....");
            }
        });

        for(int i = 0 ; i < 5 ; i++){
            new CyclicBarrierThread().start();
        }
    }
}

运行结果:

在这里插入图片描述

CyclicBarrier与CountDownLatch的区别:

CyclicBarrier所描述的是“允许一组线程互相等待,直到到达某个公共屏障点,才会进行后续任务", 而CountDownLatch所描述的是”在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待“

虽然,CountDownlatch与CyclicBarrier有那么点相似,但是他们还是存在一些区别的:

CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待

CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier

三、Semaphore:

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

信号量Semaphore是一个控制访问多个共享资源的计数器,和CountDownLatch一样,其本质上是一个“共享锁”

Semaphore可以代替synchronized和lock(比如说,只有一个停车位,有30个车来抢,这时Semaphore就相当于synchronized)

Semaphore,在API是这么介绍的:

一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。

Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目

下面我们就一个停车场的简单例子来阐述Semaphore:

为了简单起见我们假设停车场仅有5个停车位,一开始停车场没有车辆所有车位全部空着,然后先后到来三辆车,停车场车位够,安排进去停车,然后又来三辆,这个时候由于只有两个停车位,所有只能停两辆,其余一辆必须在外面候着,直到停车场有空车位,当然以后每来一辆都需要在外面候着。当停车场有车开出去,里面有空位了,则安排一辆车进去(至于是哪辆 要看选择的机制是公平还是非公平)。

从程序角度看,停车场就相当于信号量Semaphore,其中许可数为5,车辆就相对线程。当来一辆车时,许可数就会减 1 ,当停车场没有车位了(许可书 == 0 ),其他来的车辆需要在外面等候着。如果有一辆车开出停车场,许可数 + 1,然后放进来一辆车。

信号量Semaphore是一个非负整数(>=1)当一个线程想要访问某个共享资源时,它必须要先获取Semaphore,当Semaphore >0时,获取该资源并使Semaphore – 1。如果Semaphore值 = 0,则表示全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源。当线程释放资源时,Semaphore则+1。

Semaphore是具有伸缩性的。用完变成0,变成0以后也可以让剩下的线程去抢。

注意:Semaphore仅仅是对资源的并发访问的任务数进行监控,而不会保证线程的安全,因此,哎访问的时候,要自己控制线程的安全访问。

实现分析:

Semaphore结构如下:

在这里插入图片描述
从上图可以看出Semaphore内部包含公平锁(FairSync)和非公平锁(NonfairSync),继承内部类Sync,其中Sync继承AQS(再一次阐述AQS的重要性)。

Semaphore提供了两个构造函数:

  • Semaphore(int permits) :创建具有给定的许可数和非公平的公平设置的 Semaphore。
  • Semaphore(int permits, boolean fair) :创建具有给定的许可数和给定的公平设置的 Semaphore。

实现如下:

public Semaphore(int permits) {
         sync = new NonfairSync(permits);
     }      
  public Semaphore(int permits, boolean fair) {
         sync = fair ? new FairSync(permits) : new NonfairSync(permits); 
    }

Semaphore默认选择非公平锁。

Semaphore semaphore = new Semaphore(5,true);//创建信号量
//true表示公平锁    false表示非公平锁

当信号量Semaphore = 1 时,它可以当作互斥锁使用。其中0、1就相当于它的状态,当=1时表示其他线程可以获取,当=0时,排他,即其他线程必须要等待。

信号量获取:

Semaphore提供了acquire()方法来获取一个许可。

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

内部调用AQS的acquireSharedInterruptibly(int arg),该方法以共享模式获取同步状态:

public final void acquireSharedInterruptibly(int arg)  throws InterruptedException {
         if (Thread.interrupted  ()) throw new InterruptedException(); 
         if (tryAcquireShared(arg) < 0) 
            doAcquireSharedInterruptibly(arg);  
   }

在acquireSharedInterruptibly(int arg)中,tryAcquireShared(int arg)由子类来实现,对于Semaphore而言,如果我们选择非公平模式,则调用NonfairSync的tryAcquireShared(int arg)方法,否则调用FairSync的tryAcquireShared(int arg)方法。

公平:

protected int tryAcquireShared(int acquires) {
        for (;;) {
            //判断该线程是否位于CLH队列的列头
            if (hasQueuedPredecessors())
                return -1;
            //获取当前的信号量许可
            int available = getState();
 
            //设置“获得acquires个信号量许可之后,剩余的信号量许可数”
            int remaining = available - acquires;
 
            //CAS设置信号量
            if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                return remaining;
        }
    }

非公平:

对于非公平而言,因为它不需要判断当前线程是否位于CLH同步队列列头,所以相对而言会简单些。

protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
 
		final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;

信号量释放:

获取了许可,当用完之后就需要释放,Semaphore提供release()来释放许可

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

内部调用AQS的releaseShared(int arg):

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

releaseShared(int arg)调用Semaphore内部类Sync的tryReleaseShared(int arg):

protected final boolean tryReleaseShared(int releases) {
        for (;;) {
            int current = getState();
            //信号量的许可数 = 当前信号许可数 + 待释放的信号许可数
            int next = current + releases;
            if (next < current) // overflow
                throw new Error("Maximum permit count exceeded");
            //设置可获取的信号许可数为next
            if (compareAndSetState(current, next))
                return true;
        }
    }

应用示例:

我们已停车为示例:

public class SemaphoreTest {
 
    static class Parking{
        //信号量
        private Semaphore semaphore;
 
        Parking(int count){
            semaphore = new Semaphore(count);
        }
 
        public void park(){
            try {
                //获取信号量
                semaphore.acquire();
                long time = (long) (Math.random() * 10);
                System.out.println(Thread.currentThread().getName() + "进入停车场,停车" + time + "秒..." );
                Thread.sleep(time);
                System.out.println(Thread.currentThread().getName() + "开出停车场...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
            }
        }
    }
 
 
    static class Car extends Thread {
        Parking parking ;
 
        Car(Parking parking){
            this.parking = parking;
        }
 
        @Override
        public void run() {
            parking.park();     //进入停车场
        }
    }
 
    public static void main(String[] args){
        Parking parking = new Parking(3);
 
        for(int i = 0 ; i < 5 ; i++){
            new Car(parking).start();
        }
    }}

运行结果如下:

在这里插入图片描述

感谢并参考:

https://www.javazhiyin.com/14681.html(【死磕Java并发】—–J.U.C之并发工具类:CountDownLatch)

https://www.javazhiyin.com/14737.html(【死磕Java并发】—- J.U.C之并发工具类:CyclicBarrier)

https://www.javazhiyin.com/14648.html(【死磕Java并发】—– J.U.C之并发工具类:Semaphore)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值