前言
线程并发系列文章:
Java 线程基础
Java 线程状态
Java “优雅”地中断线程-实践篇
Java “优雅”地中断线程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)
线程池必懂系列
上篇文章分析了Semaphore/CountDownLatch/CyclicBarrier 实现原理,这次从应用的角度探索三者适用场合及其使用方式。
通过本篇文章,你将了解到:
1、场景引入
2、Semaphore 适用场景及使用方式
3、CountDownLatch 适用场景及使用方式
4、CyclicBarrier 适用场景及使用方式
5、三者适用场景总结
1、场景引入
网上很多文章在讲解这几个类的时候,分别举例讲解不同场景下的应用,这些例子本身没有紧密关联,不好横向对比。
因此,此处我们先预设整体场景。
参与主体
5位顾客到餐馆吃火锅,餐馆里有3位服务员。
桌上最多只能容下4盘菜。
活动内容
1、服务员上菜,顾客吃菜,服务员回收空盘子。
2、服务员涮1盘牛肚,需要烫5秒后顾客才能吃。
3、顾客自己上菜,等到每个人都上了一份菜之后才开吃。
2、Semaphore 适用场景及使用方式
场景说明
服务员开始上菜,当上了4盘菜之后,就不能再上了。需要将菜倒进锅里,然后将空盘子回收后才可以继续上菜。
这其实是典型共享有限资源场景,此处的资源即是:桌子上能放的菜盘子数。
Semaphore 使用
用Semaphore 模拟这种场景:
public class TestThread {
//最大许可数量
Semaphore semaphore = new Semaphore(4);
//竞争共享资源的线程数
final int threadCount = 5;
class SemaphoreThread extends Thread{
private Semaphore semaphore;
public SemaphoreThread(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 准备上菜 桌子上还可以放 " +
semaphore.availablePermits() + " 盘菜");
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 已经上菜了");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " 回收空盘子 桌子上还可以放 " +
semaphore.availablePermits() + " 盘菜");
}
}
}
private void testSemaphore() {
for (int i = 0 ;i < threadCount; i++) {
Thread thread = new SemaphoreThread(semaphore);
thread.setName("服务员" + (i + 1));
thread.start();
}
}
public static void main(String args[]) {
TestThread testThread = new TestThread();
testThread.testSemaphore();
}
}
将每个服务员当作线程,将桌子上可放的盘子数量当作许可。
服务员上了菜之后,将空盘子回收。
打印如下:
这结果可能不是那么明显,是因为服务员太少了,上菜速度不够快,因此增加服务员数量,设置:
final int threadCount = 10;
再来看结果:
可以看出中途桌子上可放的盘子数量为0,说明有的服务员需要等待其他服务员回收空盘子。
Semaphore 许可的数量和线程数不存在必然的联系。
3、CountDownLatch 适用场景及使用方式
场景说明
服务员给顾客们涮牛肚,为了保证口味,该牛肚只需要烫5秒钟即可,在没烫好之前,顾客只能干巴巴等待,服务员烫好后会告知顾客可以吃了。
CountDownLatch 使用
Demo1
用CountDownLatch 模拟这种场景:
public class TestThread {
//设置倒数计数
CountDownLatch countDownLatch = new CountDownLatch(5);
//等待计数的线程个数
final int threadWaitCount = 5;
//执行倒数计数的线程个数
final int threadCLCount = 1;
class WaitThread extends Thread{
private CountDownLatch countDownLatch;
public WaitThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 等着吃牛肚 当前倒数计数: " + countDownLatch.getCount());
//等待倒数结束
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + " 开始吃牛肚");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class CountDownThread extends Thread {
private CountDownLatch countDownLatch;
public CountDownThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
while (countDownLatch.getCount() > 0) {
//倒数计数
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() + " 在倒数,当前计数:" + countDownLatch.getCount());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private void testCountDownLatch() {
for (int i = 0 ;i < threadWaitCount; i++) {
Thread thread = new WaitThread(countDownLatch);
thread.setName("顾客" + (i + 1));
thread.start();
}
for (int i = 0; i < threadCLCount; i++) {
Thread thread = new CountDownThread(countDownLatch);
thread.setName("服务员" + (i + 1));
thread.start();
}
}
public static void main(String args[]) {
TestThread testThread = new TestThread();
testThread.testCountDownLatch();
}
}
结果如下:
注:现在只是一个服务员在倒数计数,可以多个服务员倒数计数。
可以看出:
CountDownLatch 倒数计数和线程数不存在必然的联系。
Demo2
Demo1 针对的是一个或者多个线程倒数计数,然而一些时候我们需要每个线程只计数一次。
如:每个服务员只负责上一个菜,顾客需要等每个服务员都上了菜之后,才开始吃。
public class TestThread {
//等待计数的线程个数
final int threadWaitCount = 2;
//执行倒数计数的线程个数
final int threadCLCount = 3;
//设置倒数计数
CountDownLatch countDownLatch = new CountDownLatch(threadCLCount);
class WaitThread extends Thread{
private CountDownLatch countDownLatch;
public WaitThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 等待上菜 还差 " + countDownLatch.getCount() + " 个菜没上");
//等待倒数结束
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + " 菜上齐了,开吃");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class CountDownThread extends Thread {
private CountDownLatch countDownLatch;
public CountDownThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始上菜");
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() + " 完成上菜,还差 " + countDownLatch.getCount() + " 个菜没上");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void testCountDownLatch() {
for (int i = 0 ;i < threadWaitCount; i++) {
Thread thread = new WaitThread(countDownLatch);
thread.setName("顾客" + (i + 1));
thread.start();
}
for (int i = 0; i < threadCLCount; i++) {
Thread thread = new CountDownThread(countDownLatch);
thread.setName("服务员" + (i + 1));
thread.start();
}
}
public static void main(String args[]) {
TestThread testThread = new TestThread();
testThread.testCountDownLatch();
}
}
CountDownLatch 倒数计数设置为服务员(线程)人数,每个服务员上菜之后调用CountDownLatch.countDown()进行倒数计数。
结果如下:
4、CyclicBarrier 适用场景及使用方式
场景说明
还是以上菜为例,若是服务员太忙,那只能顾客自己上了,每位顾客上菜的时候看看菜齐了没(菜齐的标准是每位顾客都有一盘菜),若是发现菜上齐了就告知大伙儿可以开饭了。
CyclicBarrier 使用
用CyclicBarrier 模拟这种场景:
public class TestThread {
//于栅栏处等待其它参与者
final int threadWaitCount = 5;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("最后一个菜上了,上齐了");
nextGeneration();
}
};
//初始值为线程个数
CyclicBarrier cyclicBarrier = new CyclicBarrier(threadWaitCount, runnable);
class WaitThread extends Thread{
private CyclicBarrier cyclicBarrier;
public WaitThread(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 准备上菜,当前上了 " + cyclicBarrier.getNumberWaiting()
+ " 个菜");
try {
cyclicBarrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 菜上齐了,开吃");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void testCyclicBarrier() {
for (int i = 0 ;i < threadWaitCount; i++) {
Thread thread = new WaitThread(cyclicBarrier);
thread.setName("顾客" + (i + 1));
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void nextGeneration() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("吃完了,开始下一轮上菜");
testCyclicBarrier();
}
}).start();
}
public static void main(String args[]) {
TestThread testThread = new TestThread();
testThread.testCyclicBarrier();
}
}
结果如下:
可以看出:
1、CyclicBarrier 初始的参与者个数就是线程的个数。
2、CyclicBarrier 可以重复利用。
5、三者适用场景总结
下篇将总结线程并发涉及到的锁的知识点,也是对并发系列文章的一次简短回顾。
本文基于jdk1.8。