CountDownLatch和CyclicBarrier

上一篇的线程通信方式我们学习的是Condition,它比较适合于一对一等待。今天勾勾和大家一起学习一对多的线程通信方式CountDownLatch和CyclicBarrier。

目录

CountDownLatch减数计数器

CountDownLatch原理分析

CountDownLatch方法总结

CyclicBarrier循环栅栏

CyclicBarrier循环特点

CyclicBarrier原理分析

CyclicBarrier方法总结


CountDownLatch减数计数器

CountDownLatch是一个倒数的计数器阀门,初始化时阀门关闭,指定计数的数量,当数量倒数减到0时阀门打开,被阻塞线程被唤醒。

我们先熟悉下它的用法:我们使用CountDownLatch实现main线程等待所有的线程存入数据结束后才能执行。

public static void main(String[] args) {
    final int capacity = 5;
    final CountDownLatch countDownLatch = new CountDownLatch(capacity);
    List<Integer> list = new ArrayList<>();
    //线程的数量需与CountDownLatch的计数器相等
    IntStream.range(0,capacity).forEach(i ->
       new Thread(()->{
           try {
               list.add(i);
               System.out.println(Thread.currentThread().getName()+"存入数据成功");
           } finally {
               countDownLatch.countDown();
           }
           System.out.println(Thread.currentThread().getName()+"继续做其他事情");

        }, "put-" + i).start()
    );
    try {
        //等待所有线程都计数,计算器减为0
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName()+"被唤醒");
}

上述代码中我们使用有参构造函数指定了计数器的数量,main线程调用await()方法进入阻塞状态,等待计数器减为0;

put线程调用countDown()方法对计数器减1,直到计数器为0,main线程被唤醒。

我们查看运行的结果:

图片

从结果看出,main线程一直阻塞直到所有的线程执行结束。

put线程调用countDown()方法时并没有阻塞,而是继续执行后面的逻辑。

看过现象之后,我们接下来透过现象看本质,分析CountDownLatch实现的原理。

 

CountDownLatch原理分析

CountDownLatch是AQS的共享模式的实现,其内部也有一个静态内部类Sync继承了AbstractQueuedSynchronizer。

当我们通过构造函数创建CountDownLatch对象时,其实是指定了AQS的同步状态state的值,所以state在CountDownLatch代表的即使计数器的个数。

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
private static final class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 4982264981922014374L;
 //有参构造器,指定state的值
    Sync(int count) {
        setState(count);
    }
 //获取state状态值
    int getCount() {
        return getState();
    }
 //实现了AQS的共享模式的加锁方法
    protected int tryAcquireShared(int acquires) {
        //如果state为0,则返回大于0的数值,不等于0则返回小于0的数值
        return (getState() == 0) ? 1 : -1;
    }
 //通过死循环的方式释放锁
    protected boolean tryReleaseShared(int releases) {       
        for (;;) {
            //获取状态值
            int c = getState();
            //如果此时状态值已经为0了,说明计数器已经减到0了不能再减了
            if (c == 0)
                return false;
            //否则就将计数器减1,并通过CAS的方式赋值给state,因为此时还会有其他线程修改状态
            //这里与ReentrantLock的独占解锁方式不同,独占是直接setState,因为它不会有其他线程竞争
            int nextc = c-1;
            if (compareAndSetState(c, nextc))
                //如果计数器为0,则返回true,唤醒同步队列中的等待线程
                //不等于0则返回false
                return nextc == 0;
        }
    }
}

看过Sync的源码我们可以明白CountDownLatch是共享模式的加锁和解锁方式,await()表示获取操作,countDown()表示释放操作。

state为0则表示可以加锁,不等于0的时候则线程会调用AQS提供的doAcquireSharedInterruptibly加入同步队列。每次解锁都只释放一个同步器状态,如果计数器为0的时候则会唤醒同步队列中的等待线程。

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

 

CountDownLatch方法总结

CountDownLatch(int count):构造函数,需要指定一个不小于0的int数值。

await():当前线程调用该方法会进入阻塞状态,直到同步器状态为0时被其他线程唤醒或者被其他线程中断。也即将计数器减为0返回true的线程负责唤醒阻塞的线程。当计数器为0时,调用await()方法将立即返回。

await(long timeout, TimeUnit unit):该方法与await()作用一样,只是添加了等待的时间,如果超过等待时间还没有被唤醒或者被中断,那么阻塞线程将退出阻塞状态。

countDown():该方法主要是将指定的计数器减1,当计数器已经是0了调用该方法将会被忽略,也就是说计数器的值最小只能是0;

为了保证计数器一定会减1,一般要在finally语句块中执行countDown操作。

CountDownLatch到这里为止都介绍完了,下面我们学习跟CountDownLatch能实现同样效果的工具类CyclicBarrier。

 

CyclicBarrier循环栅栏

CyclicBarrier是一个可循环的屏障,它允许多个线程在执行完相应的操作后彼此等待共同到达一个point,等所有线程都到达后再继续执行。

CyclicBarrier也可以像CountDownLatch一样适用于多个子任务并发执行,当所有子任务都执行完后再继续接下来的工作。

我们把上述CountDownLatch的例子改成CyclicBarrier,代码如下:

public static void main(String[] args) {
        final int capacity = 5;
        final CyclicBarrier cyclicBarrier = new CyclicBarrier(capacity);
        List<Integer> list = new ArrayList<>();
        //线程的数量需与cyclicBarrier的parties数量相等
        IntStream.range(0,capacity).forEach(i ->
                new Thread(()->{
                    try {
                        list.add(i);
                        System.out.println(Thread.currentThread().getName()+"存入数据成功");
                    } finally {
                        try {
                            cyclicBarrier.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (BrokenBarrierException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(Thread.currentThread().getName()+"继续做其他事情");

                }, "put-" + i).start()
        );

    }

上述代码中通过有参构造指定了parties的数量,然后启动同等数量的线程。

线程调用await()方法会在障点阻塞,直到所有线程都到达障点再继续运行。

我们查看运行结果:

图片

通过运行结果我们看到await()方法之后的代码一直没有运行直到所有线程都调用的await()方法之后才开始执行。

如果我们想要所有线程到达障点之后优先执行某个动作,可以在创建CyclicBarrier对象时指定一个Runnable接口。

final CyclicBarrier cyclicBarrier = new CyclicBarrier(capacity, ()->{
    System.out.println("我要优先执行");
});

修改之后的运行结果:

图片

从结果可以看到Runnable接口的逻辑优先执行了。

 

CyclicBarrier循环特点

CyclicBarrier之所以被称为循环栅栏,是因为它还有一个主要的特点,它内部的计数器在为0之后又被重置了还可以再循环利用。

CountDownLatch不能达到这种效果,他的计数器减为0之后就不能再使用了。

我们通过一个例子看下CyclicBarrier的循环利用。

public static void main(String[] args) {
    final int capacity = 5;
    final CyclicBarrier cyclicBarrier = new CyclicBarrier(capacity);
    List<Integer> list = new ArrayList<>();
    Map<Integer, String> map = new HashMap<>(8);
    //线程的数量需与cyclicBarrier的parties相等
    IntStream.range(0,capacity).forEach(i ->
            new Thread(()->{
                try {
                    list.add(i);
                    System.out.println(Thread.currentThread().getName()+ "存入list成功");
                } finally {
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    map.put(i, "value-i");
                    System.out.println(Thread.currentThread().getName()+ "存入map成功");
                } finally {
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName()+"继续做其他事情");

            }, "put-" + i).start()
    );

}

上述代码中我们只创建了一个CyclicBarrier对象,但是每个线程调用了两次的await()方法,重复利用了栅栏的计数器。

我们查看运行结果:

图片

熟悉了CyclicBarrier的用法之后,接下来我们分析其实现原理。

 

CyclicBarrier原理分析

CyclicBarrier内部维护了独占锁ReentrantLock,并且关联了一个Condition。

await()方法主要是判断count的数量来决定线程进入阻塞状态还是唤醒所有的阻塞线程。count是初始化时parties的值,parties的值一经赋值不会改变,count会随着线程到达障点而减到0。

public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}
//阻塞方法
 private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        //获取独占锁
        final ReentrantLock lock = this.lock;
        //加锁,之后的代码都是属于同步代码
        lock.lock();
        try {
            final Generation g = generation;
            //broken默认false,已经broken的barrier不能再次使用了
            if (g.broken)
                throw new BrokenBarrierException();
   //如果线程被打断了,那么将唤醒所有的阻塞线程
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
            //count值减1
            int index = --count;
            //如果index值为0,则表示所有的线程都到达了障点
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    //获取Runnable执行单元,如果不为空则执行逻辑
                    //此处就可以明白为什么Runnable逻辑优先执行了吧
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    //唤醒阻塞的所有线程,重置count
                    //此处可以明白CyclicBarrier为什么可以循环利用了吧
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }
            // 如果index不为0,则表示还有线程没有达到障点
            //死循环一直等待唤醒
            for (;;) {
                try {
                    //如果没有设置超时时间,则调用Condition的await()方法
                    //await方法线程释放锁并加入等待队列
                    //是不是又到了AQS了
                    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 {                        
                        Thread.currentThread().interrupt();
                    }
                }

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

                if (g != generation)
                    return index;

                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            //释放锁
            lock.unlock();
        }
    }

await方法主要代码分析都写在注释里了,我们通过流程图再熟悉下整个过程:

 

图片

在await()方法中,很多个分支调用了breakBarrier方法,此方法主要用于异常分支下的线程唤醒和count重置,但是broken被设置为true的CyclicBarrier已经不能再使用了,必须使用reset方法重置它。

private void breakBarrier() {
    //设置broken为true
    generation.broken = true;
    //重置count
    count = parties;
    //唤醒所有阻塞的线程
    trip.signalAll();
}
//重置CyclicBarrier,break现有的generation,重新生成新的generation
public void reset() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        breakBarrier();   // break the current generation
        nextGeneration(); // start a new generation
    } finally {
        lock.unlock();
    }
}

nextGeneration用于唤醒所有阻塞的线程,并重置count和generation。

private void nextGeneration() {
    // 唤醒所有阻塞的线程
    trip.signalAll();
    // 设置count
    count = parties;
    generation = new Generation();
}

 

CyclicBarrier方法总结

CyclicBarrier(int parties):构造器指定不能小于0的parties,该值不会发生改变。

CyclicBarrier(int parties, Runnable barrierAction):构造器指定parties和一个Runnable 接口,当所有的线程到达障点之后Runnable 接口会被调用。

await():当前线程调用该方法之后会进入阻塞状态直到所有的线程都调用await()方法到达障点才会被唤醒。当CyclicBarrier内部的count为0时,调用await()方法不会进入阻塞状态。

await(long timeout, TimeUnit unit):该方法与await()方法作用一样,只是可以设置阻塞等待的时间,超时没有被唤醒将退出阻塞状态。

isBroken():返回barrier的broken状态,某个线程执行await()方法进入阻塞状态,如果被中断了isBroken()方法将返回true。也即是线程的中断将会导致CyclicBarrier被broken,被broken的CyclicBarrier此时不能再使用必须reset,如果此时线程调用了await()方法将抛出异常BrokenBarrierException。

reset():中断当前barrier,并重新生成Generation。

 

CyclicBarrier和CountDownLatch区别

  • CountDownLatch的await()线程会等待计数器减为0,而执行CyclicBarrier的await()方法会使线程进入阻塞等待其他线程到达障点。

  • CountDownLatch计数器不能重置,CyclicBarrier可以重置循环利用。

  • CountDownLatch是基于AQS的共享模式实现的,CyclicBarrier是基于ReentrantLock和Condition实现的。

  • CountDownLatch不会让子线程进入阻塞,CyclicBarrier会使所有子线程进入阻塞。

我是勾勾,一直在努力的程序媛,感谢您的点赞、转发和关注!

  • 6
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值