栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JUC 中的多线程协作工具类:CountDownLatch 和 CyclicBarrier

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JUC 中的多线程协作工具类:CountDownLatch 和 CyclicBarrier

最近在学习 RocketMQ ,在发送异步消息的时候遇到了一些问题,我直接复现场景,异步消息生产者代码如下:

public class AsySendProducer {

    public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException {

 DefaultMQProducer producer = new DefaultMQProducer("test-producer-group");
 // 设置nameserver 地址
 producer.setNamesrvAddr("xxxxxxx:9876");
 //启动生产者
 producer.start();
 System.out.println("-------------启动服务--------------");
 // 设置异步发送失败重试次数,默认为2
 producer.setRetryTimesWhenSendAsyncFailed(0);
 for (int i = 0; i < 10; i++) {//
     final int index = i;
     // ID110:业务数据的ID,比如用户ID、订单编号等等
     Message msg = new Message("TopicTest",
      "TagA",
      "ID110",
      ("Hello RocketMQ " +
i).getBytes(RemotingHelper.DEFAULT_CHARSET) 
     );
     producer.send(msg, new SendCallback() {
  
  @Override
  public void onSuccess(SendResult sendResult) {
      System.out.printf("%-10d OK %s%n", index,sendResult.getMsgId());
  }
  
  @Override
  public void onException(Throwable e) {
      System.out.printf("%-10d Exception %s %n", index, e);
  }
     });
 }
 //关闭生产者连接
 producer.shutdown();
 System.out.println("--------------关闭服务器-----------");
    }
}

如果您没有学过 MQ ,看不懂这段代码,没关系,我简单的讲一下,就是启动一个生产者,异步(多线程发送,不需要等待结果,直接返回,结果会回调我们的方法)发送 10 条消息,然后关闭生产者。运行 main 方法。结果如下:

结果并不是我们想要的,从结果中,我们可以获取到一些信息,生产者启动之后,紧接着生产者就被关闭了,导致后续发送消息时,生产者不可用,连接 RocketMQ 服务器失败。

很明显我们需要解决生产者关闭时机的问题,需要保证生产者在消息都发送完成之后再关闭,那么我们可以如何保证?

有一个常用的思路,简单直接,就是定义一个计数器,每发送完一条消息,计数器减一,当计数器为 0 时,说明消息全部发送完毕,这时候再把生产者关闭。这确实是一个不错的思路,但是我们并不需要自己实现一个计数器,因为在 JDK 中已经提供了。在 JUC 下有一个 CountDownLatch 类提供了类似的功能。

CountDownLatch 是什么? CountDownLatch 提供的功能是让一个或者多个线程一直等待,直到一组在其他线程中执行的操作完成。这不就是我们需要的吗?我们需要让生产者等消息都发送完成后再关闭。

CountDownLatch 类实现了 AbstractQueuedSynchronizer 同步器,利用 AQS 中的 state 字段来判断其他线程是否完成。从源码入手了解 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;
     }
 }
    }

CountDownLatch 中实现 AQS 的内部类,实现的方法没有 Semaphore 类中的多,提供的功能也比 Semaphore 中的少。在 CountDownLatch 类中,主要提供了以下几个方法:

// 构造函数,count 为计数器
public CountDownLatch(int count);

// 等待方法,会一直等下去
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

// 带超时的等待方法,超时之后,count还没有到 0 ,则自动唤醒等待线下
public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

// 计数器减一,重要方法
public void countDown() { sync.releaseShared(1);}

CountDownLatch 类比较简单,一共就 300 行代码,有兴趣的同学可以详细研究研究。我们将 CountDownLatch 应用到 RockerMQ 的异步消息发送中,改造起来也比较简单,无论消息发送成功 OR 失败,在回调方法中 调用CountDownLatch 的 countDown() 方法,在生产者关闭前调用 CountDownLatch 的 await() 方法,使生产者关闭线程处于阻塞状态。具体代码如下:

public class AsySendProducer {

    public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException {

 DefaultMQProducer producer = new DefaultMQProducer("test-producer-group");
 // 设置nameserver 地址
 producer.setNamesrvAddr("xxxxxx:9876");
 //启动生产者
 producer.start();
 System.out.println("-------------启动服务--------------");
 // 设置异步发送失败重试次数,默认为2
 producer.setRetryTimesWhenSendAsyncFailed(0);

 // 实例化一个 countDownLatch 实例,我们需要发送 10条消息,所以计数器为 10.
 CountDownLatch countDownLatch = new CountDownLatch(10);

 for (int i = 0; i < 10; i++) {//
     final int index = i;
     // ID110:业务数据的ID,比如用户ID、订单编号等等
     Message msg = new Message("TopicTest",
      "TagA",
      "ID110",
      ("Hello RocketMQ " +
i).getBytes(RemotingHelper.DEFAULT_CHARSET) 
     );
     // 不管消息发送成功还是失败,CountDownLatch 中的计数器都需要减一
     producer.send(msg, new SendCallback() {
  
  @Override
  public void onSuccess(SendResult sendResult) {
      System.out.printf(Thread.currentThread().getName() + " %-10d OK %s%n", index, sendResult.getMsgId());
      // 计数器减一
      countDownLatch.countDown();
  }

  
  @Override
  public void onException(Throwable e) {
      System.out.printf(Thread.currentThread().getName() + " %-10d Exception %s %n", index, e);
      // 计数器减一
      countDownLatch.countDown();
  }
     });

 }
 // 等待 countDownLatch 计数器为0,如果指定时间内计数器没有为0,则不等待了,直接唤醒等待线程。
 countDownLatch.await(5, TimeUnit.SECONDS);
 //关闭生产者连接
 producer.shutdown();
 System.out.println("--------------关闭服务器-----------");
    }
}

运行改造后的 producer 方法,结果如下:

引入 CountDownLatch 之后,我们的消息就发送正常了。

提到 CountDownLatch,那么肯定会联想到 CyclicBarrier 类

CyclicBarrier 类称作循环屏障,跟 CountDownLatch 的功能很相似,但是又不太一样,主要有以下几种区别:

  • 工作原理不一样,CountDownLatch 是使一批线程等待另一批线程执行完之后再执行,而 CyclicBarrier 是当等待的线程达到一定数目后,就开始继续工作。
  • CyclicBarrier 是可以循环使用的,而 CountDownLatch 类不一样。
  • 服务对象不一样,CountDownLatch 是等待后,其他线程继续执行,而 CyclicBarrier 是等待线程数达到一定屏障后,自己继续执行。就好比吃饭,等人到齐后,才开始吃,等的人是你,吃饭的人还是你。
  • 实现不一样,CountDownLatch 是基于 AQS 实现的,CyclicBarrier 是基于 ReentrantLock 实现的。

CyclicBarrier 类的源代码也不是太多,就不贴了,有兴趣的可以研究研究。直接实战一把。在原有的异步消息中,添加一个功能,当所有的消息发送完成后,每个消息上报自己消息发送的情况。代码如下:

public class AsySendProducer {

    public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException {

 DefaultMQProducer producer = new DefaultMQProducer("test-producer-group");
 // 设置nameserver 地址
 producer.setNamesrvAddr("xxxxx:9876");
 //启动生产者
 producer.start();
 System.out.println("-------------启动服务--------------");
 // 设置异步发送失败重试次数,默认为2
 producer.setRetryTimesWhenSendAsyncFailed(0);

 // 实例化一个 countDownLatch 实例,我们需要发送 10条消息,所以计数器为 10.
 CountDownLatch countDownLatch = new CountDownLatch(5);
		// 实例化 CyclicBarrier 
 CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

 for (int i = 0; i < 5; i++) {//
     final int index = i;
     // ID110:业务数据的ID,比如用户ID、订单编号等等
     Message msg = new Message("TopicTest",
      "TagA",
      "ID110",
      ("Hello RocketMQ " +
i).getBytes(RemotingHelper.DEFAULT_CHARSET) 
     );
     // 不管消息发送成功还是失败,CountDownLatch 中的计数器都需要减一
     producer.send(msg, new SendCallback() {
  
  @Override
  public void onSuccess(SendResult sendResult) {
      System.out.printf(Thread.currentThread().getName() + " %-10d OK %s%n", index, sendResult.getMsgId());
      countDownLatch.countDown();
      try {
	      // 等待所有线程执行完
   cyclicBarrier.await();
   System.out.println(Thread.currentThread().getName()+":我执行成功!");
      } catch (InterruptedException e) {
   e.printStackTrace();
      } catch (BrokenBarrierException e) {
   e.printStackTrace();
      }
  }

  
  @Override
  public void onException(Throwable e) {
      System.out.printf(Thread.currentThread().getName() + " %-10d Exception %s %n", index, e);
      countDownLatch.countDown();
      try {
	      // 等待所有线程执行完
   cyclicBarrier.await();
   System.out.println(Thread.currentThread().getName()+":我执行失败!");
      } catch (InterruptedException ex) {
   ex.printStackTrace();
      } catch (BrokenBarrierException ex) {
   ex.printStackTrace();
      }

  }
     });

 }
 //关闭生产者连接
 countDownLatch.await();
 producer.shutdown();
 System.out.println("--------------关闭服务器-----------");

    }
}

执行 main 方法,结果如下:

可以看出消息发送情况是在所有消息发送完成后才执行的。不信你可以试着将 CyclicBarrier 相关的代码注释,会得到不一样的结果。

当然这只是 CyclicBarrier 类的简单使用,您还可以试试 CyclicBarrier 的其他方法,CyclicBarrier 的个数设置的跟线程数不一致,看看会出现什么情况。编程嘛,多动手试,什么都知道了

CountDownLatch 和 CyclicBarrier 的介绍到这里就结束了,感谢您的阅读,希望这篇文章对您的学习或者工作有一点帮助。有收获的话,也可以帮忙推荐给其他的小伙伴,让更多的人受益,万分感谢

最后

目前互联网上很多大佬都有 JUC 相关文章,如有雷同,请多多包涵了。原创不易,码字不易,还希望大家多多支持。若文中有所错误之处,还望提出,谢谢。

作者:平头哥

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号