概述

JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过多时 Lock 锁的频繁操作。这三种辅助类为:

  • CountDownLatch: 减少计数
  • CyclicBarrier: 循环栅栏
  • Semaphore: 信号量

减少计数 CountDownLatch

概念

CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行 减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法 之后的语句。

  • CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这 些线程会阻塞
  • 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程 不会阻塞)
  • 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行

场景

现在有这样一个场景,假设一个自习室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的

代码示例

这个时候就用到了CountDownLatch,计数器了。我们一共创建6个线程,然后计数器的值也设置成6

  1. // 计数器 CountDownLatch countDownLatch = new CountDownLatch(6);

然后每次学生线程执行完,就让计数器的值减1

  1. for (int i = 0; i <= 6; i++) {
  2. new Thread(() -> {
  3. System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
  4. countDownLatch.countDown();
  5. }, String.valueOf(i)).start();
  6. }

最后我们需要通过CountDownLatch的await方法来控制班长主线程的执行,这里 countDownLatch.await()可以想成是一道墙,只有当计数器的值为0的时候,墙才会消失,主线程才能继续往下执行

  1. countDownLatch.await();
  2. System.out.println(Thread.currentThread().getName() + "\t 班长最后关门");

如若不加CountDownLatch的执行结果,我们发现main线程提前已经执行完成了

1 上完自习,离开教室 0 上完自习,离开教室 main 班长最后关门 2 上完自习,离开教室 3 上完自习,离开教室 4 上完自习,离开教室 5 上完自习,离开教室 6 上完自习,离开教室

引入CountDownLatch后的执行结果,我们能够控制住main方法的执行,这样能够保证前提任务的执行

2 上完自习,离开教室 5 上完自习,离开教室 4 上完自习,离开教室 1 上完自习,离开教室 3 上完自习,离开教室 6 上完自习,离开教室 main 班长锁门走人了

完整代码

  1. import java.util.concurrent.CountDownLatch;
  2. /**
  3. * CountDownLatch
  4. **/
  5. public class CountDownLatchDemo {
  6. //6个同学陆续离开教室之后,班长锁门
  7. public static void main(String[] args) throws InterruptedException {
  8. //创建CountDownLatch对象,设置初始值
  9. CountDownLatch countDownLatch = new CountDownLatch(6);
  10. //6个同学陆续离开教室之后
  11. for (int i = 1; i <=6; i++) {
  12. new Thread(()->{
  13. System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
  14. //计数 -1
  15. countDownLatch.countDown();
  16. },String.valueOf(i)).start();
  17. }
  18. //等待
  19. countDownLatch.await();
  20. System.out.println(Thread.currentThread().getName() + "\t 班长锁门走人了");
  21. }
  22. }

循环栅栏 CyclicBarrier

概念

和CountDownLatch相反,需要集齐七颗龙珠,召唤神龙。也就是做加法,开始是0,加到某个值的时候就执行 CyclicBarrier的字面意思就是可循环(cyclic)使用的屏障(Barrier)。它要求做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await方法

场景

集齐 7 颗龙珠就可以召唤神龙

代码示例

首先创建CyclicBarrier

  1. /**
  2. * 定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法
  3. */
  4. CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
  5. System.out.println("召唤神龙");
  6. });

然后同时编写七个线程,进行龙珠收集,但一个线程收集到了的时候,我们需要让他执行await方法,等待到7个线程全部执行完毕后,我们就执行原来定义好的方法

  1. for (int i = 0; i < 7; i++) {
  2. final Integer tempInt = i;
  3. new Thread(() -> {
  4. System.out.println(Thread.currentThread().getName() + "\t 收集到 第" + tempInt + "颗龙珠");
  5. try {
  6. // 先到的被阻塞,等全部线程完成后,才能执行方法
  7. cyclicBarrier.await();
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. } catch (BrokenBarrierException e) {
  11. e.printStackTrace();
  12. }
  13. }, String.valueOf(i)).start();
  14. }

完整代码

  1. /**
  2. * CyclicBarrier
  3. **/
  4. public class CyclicBarrierDemo {
  5. public static void main(String[] args) {
  6. /**
  7. * 定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法
  8. */
  9. CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
  10. System.out.println("召唤神龙");
  11. });
  12. for (int i = 0; i < 7; i++) {
  13. final Integer tempInt = i;
  14. new Thread(() -> {
  15. System.out.println(Thread.currentThread().getName() + "\t 收集到 第" + tempInt + "颗龙珠");
  16. try {
  17. // 先到的被阻塞,等全部线程完成后,才能执行方法
  18. cyclicBarrier.await();
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. } catch (BrokenBarrierException e) {
  22. e.printStackTrace();
  23. }
  24. }, String.valueOf(i)).start();
  25. }
  26. }
  27. }

运行结果

2 收集到 第2颗龙珠 4 收集到 第4颗龙珠 1 收集到 第1颗龙珠 0 收集到 第0颗龙珠 3 收集到 第3颗龙珠 5 收集到 第5颗龙珠 6 收集到 第6颗龙珠 召唤神龙

信号量 Semaphore

概念

信号量主要用于两个目的

  • 一个是用于共享资源的互斥使用
  • 另一个用于并发线程数的控制

场景

抢车位, 6 辆汽车 3 个停车位

代码示例

那么我们首先需要定义信号量为3,也就是3个停车位

  1. /**
  2. * 初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位
  3. */
  4. Semaphore semaphore = new Semaphore(3, false);

然后我们模拟6辆车同时并发抢占停车位,但第一个车辆抢占到停车位后,信号量需要减1

  1. // 代表一辆车,已经占用了该车位
  2. semaphore.acquire(); // 抢占

同时车辆假设需要等待3秒后,释放信号量

  1. // 每个车停3秒
  2. try {
  3. TimeUnit.SECONDS.sleep(3);
  4. } catch (InterruptedException e) {
  5. e.printStackTrace();
  6. }

最后车辆离开,释放信号量

  1. // 释放停车位
  2. semaphore.release();

完整代码

  1. import java.util.concurrent.Semaphore;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * Semaphore
  5. **/
  6. public class SemaphoreDemo {
  7. public static void main(String[] args) {
  8. /**
  9. * 初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位
  10. */
  11. Semaphore semaphore = new Semaphore(3, false);
  12. // 模拟6部车
  13. for (int i = 0; i < 6; i++) {
  14. new Thread(() -> {
  15. try {
  16. // 代表一辆车,已经占用了该车位
  17. semaphore.acquire(); // 抢占
  18. System.out.println(Thread.currentThread().getName() + "\t 抢到车位");
  19. // 每个车停3秒
  20. try {
  21. TimeUnit.SECONDS.sleep(3);
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. System.out.println(Thread.currentThread().getName() + "\t 离开车位");
  26. } catch (Interr uptedException e) {
  27. e.printStackTrace();
  28. } finally {
  29. // 释放停车位
  30. semaphore.release();
  31. }
  32. }, String.valueOf(i)).start();
  33. }
  34. }
  35. }

运行结果

0 抢到车位 4 抢到车位 1 抢到车位 0 离开车位 4 离开车位 1 离开车位 5 抢到车位 3 抢到车位 2 抢到车位 5 离开车位 2 离开车位 3 离开车位

结论

看运行结果能够发现,0 4 1 车辆首先抢占到了停车位,然后等待3秒后,离开,然后后面 5 3 2 又抢到了车位