介绍下JUC的 通信工具类 Semaphore, CountDownLatch,CyclicBarrier 三剑客。
一、 Semaphore
Semaphore 信号量,用来控制同一时间,资源可被多少个线程访问。
先看构造
//可传入一个 boolean 值,控制抢锁是否是公平的。
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
//默认是非公平的
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
ReentrantLock 也有公平和非公平,构造方式一样,后续发文将,以及 AQS
什么是公平和非公平锁呢?
非公平锁: 就是争抢票,谁抢到票,票就是谁的。
公平锁: 来了个正义大侠守着, 一个线程进来了,若是看到前面有人排队,就老老实实排在队伍后面。(线程会先去检查队列有没有等待的线程,如果有就进入等待队列排队)
案例:
/**
*
*@author Saiuna
*@date 2020/7/9 10:40
*/
public class SemaphoreDemo2 {
public static void main(String[] args) {
int count = 10;
//允许n 个线程同时执行, 公平锁
Semaphore s = new Semaphore(2, true);
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < count; i++) {
executorService.execute(()->{
try {
//获得许可
s.acquire();
System.out.println(Thread.currentThread().getName() + " running...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放许可
s.release();
}
});
}
executorService.shutdown();
}
}
输出:观测输出,是两两线程间隔打印的,也就是同时只放行了两个线程跑,改变Semaphore(2, true);的参数看输出结果, 三个则会三个人一起跑。
pool-1-thread-1 running...
pool-1-thread-2 running...
pool-1-thread-3 running...
pool-1-thread-4 running...
pool-1-thread-6 running...
pool-1-thread-5 running...
pool-1-thread-7 running...
pool-1-thread-8 running...
pool-1-thread-9 running...
pool-1-thread-10 running...
使用场景: 限制流量
二 、CountDownLatch
CountDownLatch 源码的官方介绍。
一个同步辅助器,允许一个或多个线程一直等待,直到一组在其他线程执行的操作全部完成。
这是一个一次性的现象 - 计数无法重置。 如果需要重置计数的版本,请考虑使用
CyclicBarrier
。
先来看看它的构造方法
//传入一个 count 值,用于计数。
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
常用方法
//递减锁存器的计数,释放所有等待的线程,如果计数到达零
//If the current count equals zero then nothing happens.
public void countDown() {
sync.releaseShared(1);
}
//当一个线程调用await方法时,就会阻塞当前线程。每当有线程调用一次 countDown 方法时,计数就会减 1。
//当 count 的值等于 0 的时候,被阻塞的线程才会继续运行。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
案例
玩游戏时都会等待一些资源的加载,等前置资源都加载好了方能开始游戏~
难道 这就是你跟妈咪说网络慢衣服还没加载出来 理由吗哈哈
/**
*
*@author Saiuna
*@date 2020/7/9 9:24
*/
public class CountDownLatchDemo {
// 定义前置任务线程
static class PreTaskThread implements Runnable{
private String task;
private CountDownLatch countDownLatch;
public PreTaskThread(String task, CountDownLatch countDownLatch) {
this.task = task;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
Random random = new Random();
Thread.sleep(random.nextInt(1000));
System.out.println("\r\n" + task + " - 任务完成");
countDownLatch.countDown();
if (countDownLatch.getCount() != 0) {
System.out.println(String.format("剩下%d个前置任务", countDownLatch.getCount()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
}
}
}
public static void main(String[] args) {
// 假设有三个模块需要加载
CountDownLatch countDownLatch = new CountDownLatch(3);
new Thread(() -> {
try {
System.out.println("等待数据加载...");
System.out.println(String.format("还有%d个前置任务", countDownLatch.getCount()));
countDownLatch.await();
System.out.println("数据加载完成,正式开始游戏!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 前置任务
new Thread(new PreTaskThread("加载地图数据", countDownLatch)).start();
new Thread(new PreTaskThread("加载人物模型", countDownLatch)).start();
new Thread(new PreTaskThread("加载背景音乐", countDownLatch)).start();
}
}
输出:
等待数据加载...
还有3个前置任务
加载背景音乐 - 任务完成
剩下2个前置任务
加载人物模型 - 任务完成
剩下1个前置任务
加载地图数据 - 任务完成
数据加载完成,正式开始游戏!
这边等到前置任务都完成了,顺利进入游戏。
三、 CyclicBarrier
CyclicBarrirer从名字上来理解是“循环的屏障”的意思。前面提到了CountDownLatch一旦计数值count
被降为0后,就不能再重新设置了,它只能起一次“屏障”的作用。而CyclicBarrier拥有CountDownLatch的所有功能,还可以使用reset()
方法重置屏障。
先看它 的构造方法
// 构造方法
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
// 具体实现
}
案例,同上述的游戏案例:
/**
*
*@author Saiuna
*@date 2020/7/9 18:02
*/
public class CyclicBarrierDemo {
static class PreTaskThread implements Runnable {
private String task;
private CyclicBarrier cyclicBarrier;
public PreTaskThread(String task, CyclicBarrier cyclicBarrier) {
this.task = task;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
// 假设总共三个关卡
for (int i = 1; i < 4; i++) {
try {
Thread.sleep(new Random().nextInt(1000));
System.out.println(String.format("关卡%d的任务%s完成", i, task));
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
cyclicBarrier.reset(); // 重置屏障
}
}
}
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
System.out.println("本关卡所有前置任务完成,开始游戏...");
});
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new PreTaskThread("加载地图数据", cyclicBarrier));
executorService.execute(new PreTaskThread("加载地图数据", cyclicBarrier));
executorService.execute(new PreTaskThread("加载背景音乐", cyclicBarrier));
}
}
输出
关卡1的任务加载地图数据完成
关卡1的任务加载背景音乐完成
关卡1的任务加载人物模型完成
本关卡所有前置任务完成,开始游戏...
关卡2的任务加载地图数据完成
关卡2的任务加载背景音乐完成
关卡2的任务加载人物模型完成
本关卡所有前置任务完成,开始游戏...
关卡3的任务加载人物模型完成
关卡3的任务加载地图数据完成
关卡3的任务加载背景音乐完成
本关卡所有前置任务完成,开始游戏...
这里跟CountDownLatch的代码有一些不同。CyclicBarrier没有分为await()和countDown(),而是只有单独的一个await()方法。
一旦调用await()方法的线程数量等于构造方法中传入的任务总量(这里是3),就代表达到屏障了。CyclicBarrier允许我们在达到屏障的时候可以执行一个任务,可以在构造方法传入一个Runnable类型的对象。上述案例就是在达到屏障时,输出“本关卡所有前置任务完成,开始游戏…”。
总结
- CountDownLatch是一个计数器,线程完成一个记录一个,计数器递减 直到 0,只能只用一次.
- CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供reset功能,可以多次使用
- CyclicBarrier 可以在最后一个线程达到屏障之前,选择先执行一个操作。
- Semaphore ,需要拿到许可才能执行,并可以选择公平和非公平模式