【java并发工具类-协作】CountDownLatch和CyclicBarrier


上面的几篇文章主要介绍Java SDK中提供互斥的工具类:管程synchronized,Lock,ReadWriteLock,StampedLock,LockSupport。下面我们将分几篇文章介绍Java SDK中提供线程协作的工具类,今天讲解CountDownLatch和CyclicBarrier两个工具类。

下面介绍一个场景,全文围绕该场景介绍这两个工具类

业务介绍

在这里插入图片描述

1.创建线程优化业务

while(存在未对账订单){
  // 查询未对账订单
  Thread T1 = new Thread(()->{	pos = getPOrders();	});
  T1.start();
  // 查询派送单
  Thread T2 = new Thread(()->{ dos = getDOrders();	});
  T2.start();
  // 等待T1、T2结束
  T1.join();
  T2.join();
  // 执行对账操作
  diff = check(pos, dos);
  // 差异写入差异库
  save(diff);
} 

main主线程调用线程T1,T2的join方法等待线程执行结束退出,再执行对账和写入库的操作。

2. java的线程和操作系统的线程之间的关系:

操作系统实现的线程是稳定极好的,所以java的线程调度交给操作系统了,而线程的创建、销毁、调度和维护,都是靠操作系统(准确的说是内核)来实现,程序员只需要使用线程即可,而不需要自己设计线程的调度算法和线程对CPU资源的抢占使用。

3.线程池方式实现业务

上面我们知道线程的创建和销毁是重量级的,所以我们开发中经常使用线程池,重复使用线程,避免线程的创建和销毁。
下面我们用线程池方式实现上面的业务:

// 创建2个线程的线程池
Executor executor = Executors.newFixedThreadPool(2);
while(存在未对账订单){
  // 查询未对账订单
  executor.execute(()-> { pos = getPOrders(); });
  // 查询派送单
  executor.execute(()-> {	dos = getDOrders();});
  
  /* ??如何实现等待??*/
  
  // 执行对账操作
  diff = check(pos, dos);
  // 差异写入差异库
  save(diff);
}   

因为线程池的线程是不会退出的,所以Thread的join方法失效了,怎么办?使用Java SDK工具包的CountDownLatch。

4 CountDownLatch

4.1 CountDownLatch适用场景

适用场景:CountDownLatch 主要用来解决一个线程等待多个线程的场景。

4.2 用 CountDownLatch 实现线程等待

// 创建2个线程的线程池
Executor executor = Executors.newFixedThreadPool(2);
while(存在未对账订单){
  // 计数器初始化为2
  CountDownLatch latch = new CountDownLatch(2);
  // 查询未对账订单
  executor.execute(()-> { 	pos = getPOrders();		latch.countDown();		});
  // 查询派送单
  executor.execute(()-> {	dos = getDOrders();		latch.countDown();		});
 
  latch.await(); // 等待两个查询操作结束,实现对计数器=0的等待。
  diff = check(pos, dos); // 执行对账操作
  save(diff); // 差异写入差异库
}

首先我们创建了一个CountDownLatch,计数器初始值是2,当执行完getPOrders方法,调用latch.countDown(),计数器-1,等到计数器=0时,会唤醒执行latch.await()继续向下执行。

5.进一步优化性能

上面我们通过开启两个线程分别执行查询未对账订单和查询派送单,然后等待查询结束再执行对账操作。

其实还有优化地步,就是在执行对账操作的时候,可以同时再去查询未对账订单和查询派送单。
在这里插入图片描述
上面这幅图可以表达我们的意图:当线程T1和线程T2都生产完一条数据(两者需要相互等待),才能继续往下走(查询订单和拍送单),并且执行线程3执行对账。

其实就是生产者消费者模式,线程T1,T2都生产出一条数据,放入队列中,消费者T3消费。

实现难点有两个:

  1. 线程T1,T2都生产了一条数据,才能执行下一轮的查询。
  2. 另一方面要通知到T3.
    因为CountDownLatch在=0之后不会重置计数器,所以下面就使用CyclicBarrier来实现该操作。

6.CyclicBarrier

6.1 CyclicBarrier适用场景

适用场景:CyclicBarrier 是一组线程之间互相等待。

除此之外 CountDownLatch 的计数器是不能循环利用的,也就是说一旦计数器减到 0,再有线程调用 await(),该线程会直接通过。但 CyclicBarrier 的计数器是可以循环利用的,当计数器=0时,调用回调函数,重置计数器的值。

6.2 用 CyclicBarrier 实现线程同步(协作)

线程同步:就是控制线程之间协作。下面使用CyclicBarrier 实现上面的优化。

// 订单队列
Vector<P> pos;
// 派送单队列
Vector<D> dos;
// 执行回调的线程池 
Executor executor = Executors.newFixedThreadPool(1);//注意这里线程池中只有一个线程
final CyclicBarrier barrier =  new CyclicBarrier(2, ()->{
	executor.execute(()->check());	}); //注意这里使用的是线程池的方式。 
void check(){
  P p = pos.remove(0);
  D d = dos.remove(0);
  // 执行对账操作
  diff = check(p, d);
  // 差异写入差异库
  save(diff);
}
  
void checkAll(){
  // 循环查询订单库
  Thread T1 = new Thread(()->{
    while(存在未对账订单){  
      pos.add(getPOrders());  // 查询订单库
      barrier.await(); // 等待
    }
  });
  T1.start();  
  // 循环查询运单库
  Thread T2 = new Thread(()->{
    while(存在未对账订单){
           dos.add(getDOrders()); // 查询运单库
      barrier.await(); // 等待
    }
  });
  T2.start();
}
  1. 创建CyclicBarrier,计数器初始值为2,=0时执行回调函数check()方法。
  2. 当执行checkall()方法,开启两个线程分别查询订单和派送单,查询结束,计数器-1。
  3. 当计数器=0时,使用线程池中的线程执行check()方法,同时重置计数器,开启下一轮的查询订单和派送单。

三个问题:

  • CyclicBarrier的回调函数在哪个线程执行啊?
    CyclicBarrier的回调函数执行在一个回合中最后执行barrier.await()的线程上,而且同步执行check()方法,调用完check()方法,才能开启下一回合。所以check()如果开启另外一个线程异步执行,就起不到性能优化的作用了。
  • 为啥要用线程池,而不是在回调函数中直接调用?
    相信你通过上一个问题也差不多明白了,对就是为了异步。
  • 线程池为啥要使用单线程的?
    线程数量固定为1,防止了多线程并发导致的数据不一致,因为订单和派送单是两个队列,只有单线程去两个队列中取消息才不会出现消息不匹配的问题。(其实可以把每一回合的两个数据封装成一个数据放到一个队列中,多线程取就不会有这种问题了)

参考:极客时间
更多:邓新

发布了34 篇原创文章 · 获赞 0 · 访问量 1089

猜你喜欢

转载自blog.csdn.net/qq_42634696/article/details/105135835
今日推荐