countDownLatch是在 java1.5 被引入,跟它一起被引入的工具类还有 CyclicBarrier、Semaphore、concurrentHashMap 和 BlockingQueue,它们存在于java.util.cucurrent包下,这里我们只会说到countDownLatch、CyclicBarrier、Semaphore这三种。
什么是CountDownLatch
意思是 CountDownLatch 是一个同步辅助器,允许一个或多个线程一直等待,直到一组在其他线程执行的操作全部完成。
CountDownLatch方法列表
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
传入一个 count 值,构造一个用给定计数初始化的 CountDownLatch。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
await方法使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
await(long timeout, TimeUnit unit)方法使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间
public void countDown() {
sync.releaseShared(1);
}
countDown方法递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
public long getCount() {
return sync.getCount();
}
getCount方法返回当前计数。
而常用的方法有:
public void countDown() {
sync.releaseShared(1);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
当一个线程调用 await 方法时,就会阻塞当前线程。每当有线程调用一次 countDown 方法时,计数就会减 1。当 count 的值等于 0 的时候,被阻塞的线程才会继续运行。
我们举个例子,比如外面小哥张三接到了两个很紧急的单子,但是如果一个人送单时间会赶不上,这时叫来了另一个外面小哥李四,一人送一个单子,最后两个单子都在规定的时间内完成了。
现在我们来代码模拟一下:
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
Worker worker1 = new Worker("外面小哥张三", 10000, countDownLatch);
Worker worker2 = new Worker("外面小哥李四", 10000, countDownLatch);
worker1.start();
worker2.start();
long startTime = System.currentTimeMillis();
countDownLatch.await();
System.out.println("外卖单子全部送完,结束时间:"+ (System.currentTimeMillis() - startTime));
}
static class Worker extends Thread{
String name;
int serviceTime;
CountDownLatch countDownLatch;
public Worker(String name, int serviceTime, CountDownLatch countDownLatch) {
this.name = name;
this.serviceTime = serviceTime;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println(name+"开始送餐,当前时间:"+sdf.format(new Date()));
try {
Thread.sleep(serviceTime);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(name+"结束送餐,当前时间:"+sdf.format(new Date()));
countDownLatch.countDown();
}
}
外面小哥李四开始送餐,当前时间:2021-12-22 11:31:16
外面小哥张三开始送餐,当前时间:2021-12-22 11:31:16
外面小哥李四结束送餐,当前时间:2021-12-22 11:31:26
外面小哥张三结束送餐,当前时间:2021-12-22 11:31:26
外卖单子全部送完,结束时间:10014
原本需要 20 秒送餐时间,两个人 10 秒就完成了,效率真是大大的提高了。
什么是CyclicBarrier
CyclicBarrier的 arrier 英文是屏障,障碍,栅栏的意思。cyclic 是循环的意思,字面意思是可循环使用(Cyclic)的屏障(Barrier)。他要做的事情是,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有屏障拦截的线程才会继续运行。
CyclicBarrier 方法列表
CyclicBarrier 类有两个常用的构造方法:
public CyclicBarrier(int parties) {
this(parties, null);
}
这里的 parties 指的是需要几个线程一起到达,例如,初始化时 parties 里的计数是 3,于是拥有该 CyclicBarrier 对象的线程当达到 3 时就唤醒
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
这里的parties与上一个构造方法的解释是一样的,这里需要解释的是第二个入参(Runnable barrierAction),用于在所有线程达到屏障时,优先执行 barrierAction。
这里我们模拟一下,现在有一组运动员100米决赛,现在要等待所有运动员准备完成后,等待裁判吹口哨之后才能开跑。
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
@Override
public void run() {
try {
System.out.println("=========等待裁判吹口哨=============");
//这里停顿两秒更便于观察线程执行的先后顺序
Thread.sleep(2000);
System.out.println("=========裁判吹口哨=============");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Runner runner1 = new Runner(cyclicBarrier, "飞人1");
Runner runner2 = new Runner(cyclicBarrier, "飞人2");
Runner runner3 = new Runner(cyclicBarrier, "飞人3");
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.execute(runner1);
executorService.execute(runner2);
executorService.execute(runner3);
executorService.shutdown();
}
}
class Runner implements Runnable{
private CyclicBarrier cyclicBarrier;
private String name;
public Runner(CyclicBarrier barrier, String name) {
this.cyclicBarrier = barrier;
this.name = name;
}
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(5000));
System.out.println(name + ":准备开跑");
cyclicBarrier.await();
System.out.println(name + ":开跑");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e){
e.printStackTrace();
}
}
上面代码定义 Runner 类代表运动员,内部维护了一个共有的 CyclicBarrier,每个人都有准备的时间,准备完成后会调用 await 方法等待其他运动员,当所有运动员都准备完成,等裁判吹口哨就可以开跑了。
测试运行的结果
飞人3:准备开跑
飞人2:准备开跑
飞人1:准备开跑
=========等待裁判吹口哨=============
=========裁判吹口哨=============
飞人1:开跑
飞人3:开跑
飞人2:开跑
那上面提到的循环利用又是怎么实现的呢,我们把上面的代码修改一下:
把屏障值改为 2
增加一个“飞人4” 参与赛跑
飞人3:准备开跑
飞人2:准备开跑
=========等待裁判吹口哨=============
=========裁判吹口哨=============
飞人2:开跑
飞人3:开跑
飞人4:准备开跑
飞人1:准备开跑
=========等待裁判吹口哨=============
=========裁判吹口哨=============
飞人1:开跑
飞人4:开跑
最终结果是第一次先跑两个人,然后第二次再跑两个人。也就实现了屏障的循环使用。
什么是Semaphore
Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
比如:学生去食堂打饭,假如有 3 个窗口可以打饭,同一时刻也只能有 3 名学生打饭。第 4 个学生来了之后就必须在外面等着,只要有打饭的同学好了,就可以去相应的窗口了 。
Semaphore 方法列表
acquire(int permits)
从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。就好比是一个学生占两个窗口。这同时也对应了相应的release方法。
release(int permits)
释放给定数目的许可,将其返回到信号量。这个是对应于上面的方法,一个学生占几个窗口完事之后还要释放多少
availablePermits()
返回此信号量中当前可用的许可数。也就是返回当前还有多少个窗口可用。
reducePermits(int reduction)
根据指定的缩减量减小可用许可的数目。
hasQueuedThreads()
查询是否有线程正在等待获取资源。
getQueueLength()
返回正在等待获取的线程的估计数目。该值仅是估计的数字。
tryAcquire(int permits, long timeout, TimeUnit unit)
如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。
acquireUninterruptibly(int permits)
从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。
更多的请查看 类信号量
这里我们模拟一下上面学生去食堂吃饭,3个窗口有学生在打饭,剩下的学生等着前面打完饭的学生,才能接下来的操作。
private static int count = 10;
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(count);
//指定最多只能有五个线程同时执行
Semaphore semaphore = new Semaphore(3);
Random random = new Random();
for (int i = 0; i < count; i++) {
final int no = i;
executorService.execute(new Runnable() {
@Override
public void run() {
try {
//获得许可
semaphore.acquire();
System.out.println(no+"可以来打饭了");
//模拟车辆通行耗时
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(no+"打好饭了,别的学生可以来打了");
//释放许可
semaphore.release();
}
}
});
}
executorService.shutdown();
}
2可以来打饭了
0可以来打饭了
1可以来打饭了
2打好饭了,别的学生可以来打了
1打好饭了,别的学生可以来打了
0打好饭了,别的学生可以来打了
3可以来打饭了
5可以来打饭了
4可以来打饭了
5打好饭了,别的学生可以来打了
3打好饭了,别的学生可以来打了
4打好饭了,别的学生可以来打了
7可以来打饭了
6可以来打饭了
8可以来打饭了
6打好饭了,别的学生可以来打了
7打好饭了,别的学生可以来打了
8打好饭了,别的学生可以来打了
9可以来打饭了
9打好饭了,别的学生可以来打了
从上面的结果我们可以知道,同一时间只能有3个学生同时打饭,其他学生必须要等待前面的学生打好饭才可以继续打饭。
现在有个问题,所有等待的学生都想先拿到许可,先通行,怎么办?这就需要,用到锁了。就所有人都去抢,谁先抢到,谁就先走呗。
从 Semaphore的构造函数中就会发现,可以传入一个 boolean 值的参数,控制抢锁是否是公平的。
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
默认是非公平,可以传入 true 来使用公平锁。
总结
- CountDownLatch 是一个线程等待其他线程, CyclicBarrier 是多个线程互相等待
- CountDownLatch 的计数是减 1 直到 0,CyclicBarrier 是加 1,直到指定值
- CountDownLatch 是一次性的, CyclicBarrier 可以循环利用
- CyclicBarrier 可以在最后一个线程达到屏障之前,选择先执行一个操作
- Semaphore ,需要拿到许可才能执行,并可以选择公平和非公平模式