在多线程程序设计中,经常会遇到一个线程等待一个或多个线程的场景,遇到这样的场景应该如何解决?
如果是一个线程等待一个线程,则可以通过await()和notify()来实现;
如果是一个线程等待多个线程,则就可以使用CountDownLatch和CyclicBarrier来实现比较好的控制。
下面来详细描述下CountDownLatch的应用场景:
例如:百米赛跑:8名运动员同时起跑,由于速度的快慢,肯定有会出现先到终点和晚到终点的情况,而终点有个统计成绩的仪器,当所有选手到达终点时,它会统计所有人的成绩并进行排序,然后把结果发送到汇报成绩的系统。
其实这就是一个CountDownLatch的应用场景:一个线程或多个线程等待其他线程运行达到某一目标后进行自己的下一步工作,而被等待的“其他线程”达到这个目标后继续自己下面的任务。
这个场景中:
1. 被等待的“其他线程”------>8名运动员
2. 等待“其他线程”的这个线程------>终点统计成绩的仪器
那么,如何来通过CountDownLatch来实现上述场景的线程控制和调度呢?
jdk中CountDownLatch类有一个常用的构造方法:CountDownLatch(int count);
两个常用的方法:await()和countdown()
其 中count是一个计数器中的初始化数字,比如初始化的数字是2,当一个线程里调用了countdown(),则这个计数器就减一,当线程调用了 await(),则这个线程就等待这个计数器变为0,当这个计数器变为0时,这个线程继续自己下面的工作。下面是上述CountDownLatch场景的 实现:
Work类(运动员):
import java.util.concurrent.CountDownLatch; public class Work implements Runnable { private int id; private CountDownLatch beginSignal; private CountDownLatch endSignal; public Work(int id, CountDownLatch begin, CountDownLatch end) { this.id = id; this.beginSignal = begin; this.endSignal = end; } @Override public void run() { try { System.out.println("准备完毕.."+id); beginSignal.await(); System.out.println("起跑..."+id); System.out.println("work" + id + "到达终点"); endSignal.countDown(); System.out.println("work" + id + "继续干其他事情"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public static void main(String[] args) { CountDownLatch begSignal = new CountDownLatch(1); CountDownLatch endSignal = new CountDownLatch(8); for (int i = 0; i < 8; i++) { new Thread(new Work(i, begSignal, endSignal)).start(); } try { Thread.currentThread().sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-------------------------------"); try { begSignal.countDown(); //统一起跑 endSignal.await(); //等待运动员到达终点 System.out.println("结果发送到汇报成绩的系统"); } catch (InterruptedException e) { e.printStackTrace(); } } }
输出结果:
准备完毕..4
准备完毕..5
准备完毕..1
准备完毕..2
准备完毕..6
准备完毕..0
准备完毕..3
准备完毕..7
-------------------------------
起跑...4
起跑...2
起跑...6
work6到达终点
起跑...1
work1到达终点
起跑...5
work5到达终点
work1继续干其他事情
work6继续干其他事情
起跑...7
work7到达终点
work7继续干其他事情
起跑...3
work3到达终点
work3继续干其他事情
起跑...0
work2到达终点
work2继续干其他事情
work4到达终点
work4继续干其他事情
work0到达终点
work0继续干其他事情
work5继续干其他事情
结果发送到汇报成绩的系统
下面详细描述下CyclicBarrier的应用场景:
有四个游戏玩家玩游戏,游戏有三个关卡,每个关卡必须要所有玩家都到达后才能允许通关。
其 实这个场景里的玩家中如果有玩家A先到了关卡1,他必须等待其他所有玩家都到达关卡1时才能通过,也就是说线程之间需要互相等待,这和 CountDownLatch的应用场景有区别,CountDownLatch里的线程是到了运行的目标后继续干自己的其他事情,而这里的线程需要等待其 他线程后才能继续完成下面的工作。
jdk中CyclicBarrier类有两个常用的构造方法:
1. CyclicBarrier(int parties)
这里的parties也是一个计数器,例如,初始化时parties里的计数是3,于是拥有该CyclicBarrier对象的线程当parties的计数为3时就唤醒,注:这里parties里的计数在运行时当调用CyclicBarrier:await()时,计数就加1,一直加到初始的值
2. CyclicBarrier(int parties, Runnable barrierAction)
这里的parties与上一个构造方法的解释是一样的,这里需要解释的是第二个入参(Runnable barrierAction),这个参数是一个实现Runnable接口的类的对象,也就是说当parties加到初始值时就出发barrierAction的内容。
下面来实现上述的应用场景:
Player类(玩家类)
java.util.concurrent.BrokenBarrierException java.util.concurrent.CyclicBarrier public class Player implements Runnable{ private CyclicBarrier cyclicBarrier; private int id; public Player(int id, CyclicBarrier cyclicBarrier) { this.cyclicBarrier = cyclicBarrier; this.id = id; } @Override public void run() { try { System.out.println("玩家" + id + "正在玩第一关..."); cyclicBarrier.await(); System.out.println("玩家" + id + "进入第二关..."); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(4, new Runnable() { @Override public void run() { System.out.println("所有玩家进入第二关!"); } }); for (int i = 0; i < 4; i++) { new Thread(new Player(i, cyclicBarrier)).start(); } } }
输出结果:
玩家1正在玩第一关...
玩家2正在玩第一关...
玩家3正在玩第一关...
玩家0正在玩第一关...
所有玩家进入第二关!
玩家0进入第二关...
玩家1进入第二关...
玩家2进入第二关...
玩家3进入第二关...