并发编程——并发工具类
0.1. 内容概要
- CountDownLatch
- CyclicBarrier
- Semaphore
- Exchanger
- 总结
0.2. 学习目标
- 理解常用并发工具类的概念、工作原理
- 能够根据需求正确使用并发工具类
1. CountDownLatch
1.1. 概念和作用
倒计时门闩;倒计时计数器;发令枪;
一个同步辅助类,它允许一个或多个线程等待,直到在其他线程中执行的一组操作完成为止。
-
特点
无法重置计数
-
使用场景
等待/通知;倒计时计数;
-
基本操作
await():使当前线程等待,直到计数器被扣减为0
countDown():扣减计数器,每次扣减1
1.2. 注意事项
-
countDown之后,子线程依然在执行,并未被阻塞
-
若计数器未能完全释放,则main线程一直等待(可以尝试使用await带超时参数的重载方法)
-
计数器 >= 线程数,一个线程可以倒计时多次
1.3. 扩展:底层实现机制
共享锁:也叫做读锁,
所有加锁的线程只能读取锁,不能写入、修改等操作,
每次获取锁,得到的锁状态都是固定值,
其它线程也可以对该锁对象进行加锁(但是只能加共享锁,而非互斥锁)
加锁过程:LockSupport.park(this) 释放锁过程:LockSupport.unpark(s.thread)
1.4. 示例代码
1.4.1. 需求一:
领队带领10个小伙伴一起出游,模拟实现签到功能
1.4.1. 思路分析:
1.使用main线程模拟领队
2.定义总数为10的计数器,代表10个小伙伴
3.每个人签到时,计数器扣减1
4.所有人都签到完成,即计数器扣减为0时,出发,否则,领队将一直等待
1.4.1. 步骤分析:
1.定义计数器CountDownLatch对象,计数参数为10
2.启动10个线程,模拟10个小伙伴
3.每个小伙伴都执行相同的任务:出发-签到-上车
在执行签到动作时,进行计数器的扣减
1.4.1. 代码实现:
String[] actions = {"打瞌睡", "听歌", "玩游戏", "吃早餐", "闲聊"};
// 1.定义计数器CountDownLatch对象,计数参数为10
CountDownLatch count = new CountDownLatch(10);
Runnable runnable = () -> {
Random random = new Random();
try {
int waitSeconds = random.nextInt(5); // 随机休眠时间
Thread.sleep(waitSeconds * 1000);
String name = Thread.currentThread().getName();
System.out.println("小伙伴" + name + "磨磨蹭蹭了" + waitSeconds + "秒");
// 3.在执行签到动作时,进行计数器的扣减
count.countDown(); // 小伙伴上车了
System.out.println("小伙伴" + name + "已上车,开始" + actions[waitSeconds]);
if (Integer.parseInt(name) == 5) {
count.countDown();
System.err.println("小伙伴" + name + "又签到了一次");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
System.out.println("小伙们已出发。。。开始计时");
long start = System.currentTimeMillis();
// 2.启动10个线程,模拟10个小伙伴
for (int i = 0; i < 9; i++) {
new Thread(runnable, "" + i).start();
}
try {
count.await(); // 领队等待所有小伙伴上车
// count.await(7000, TimeUnit.MILLISECONDS); // 领队等待所有小伙伴上车
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("所有小伙伴都已到达,共耗时:"+ (end - start) / 1000.0 +"秒");
1.4.1. 输出结果:
小伙们已出发。。。开始计时
小伙伴8磨磨蹭蹭了0秒
小伙伴2磨磨蹭蹭了0秒
小伙伴4磨磨蹭蹭了0秒
小伙伴4已上车,开始打瞌睡
小伙伴8已上车,开始打瞌睡
小伙伴2已上车,开始打瞌睡
小伙伴1磨磨蹭蹭了2秒
小伙伴7磨磨蹭蹭了2秒
小伙伴1已上车,开始玩游戏
小伙伴7已上车,开始玩游戏
小伙伴3磨磨蹭蹭了3秒
小伙伴6磨磨蹭蹭了3秒
小伙伴0磨磨蹭蹭了3秒
小伙伴6已上车,开始吃早餐
小伙伴3已上车,开始吃早餐
小伙伴0已上车,开始吃早餐
小伙伴5磨磨蹭蹭了4秒
小伙伴5已上车,开始闲聊
小伙伴5又签到了一次
所有小伙伴都已到达,共耗时:4.031秒
1.4.2. 需求二:
15个线程并发计数后,合并总数,在主线程中打印结果
1.4.2. 思路分析:
1.定义计数器CountDownLatch对象
2.所有子线程启动之后,主线程应该立即进入等待
3.子线程完成任务之后,计数器对象减 1
1.4.2. 代码实现:
public static AtomicInteger number = new AtomicInteger(0);
public static void main(String[] args) {
// 1.定义计数器CountDownLatch对象
CountDownLatch count = new CountDownLatch(15);
Runnable runnable = () -> {
for (int i = 0; i < 1000; i++){
number.incrementAndGet(); // number = ++number
}
System.out.println(Thread.currentThread().getName() + ":" + number.get());
// 3.子线程完成任务之后,计数器对象减 1
count.countDown();
};
for (int i = 0; i < 15; i++) {
new Thread(runnable).start();
}
// 2.所有子线程启动之后,主线程应该立即进入等待
try {
count.await(); // 加强版join()
} catch (InterruptedException e) {
e.printStackTrace();
}
// try {
// Thread.sleep(3000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println("最终结果" + number.get());
}
1.4.2. 输出结果:
Thread-12:4018
Thread-6:7810
Thread-4:14039
Thread-7:9378
Thread-0:3000
Thread-11:10686
Thread-14:5810
Thread-8:11000
Thread-3:6810
Thread-9:15000
Thread-5:14194
Thread-10:8234
Thread-2:2074
Thread-1:1518
Thread-13:14680
最终结果15000
2. CyclicBarrier
2.1. 概念和作用
循环屏障;
一个同步辅助类,它允许一组线程全部互相等待以到达一个公共的障碍点。
-
特点
释放等待线程后可以重新使用
-
使用场景
执行一组固定大小、且彼此偶尔需要相互等待的线程;
-
基本操作
await():使当前线程等待,直到计数器被扣减为0后,会自动唤醒
2.2. 扩展:底层实现机制
-
使用可重入锁ReentrantLock + Condition对象实现同步:
让线程在同一个条件对象上等待:因为没有扣减到0
-
定义计数器count,每当线程执行await方法,计数器减1
-
当计数器减为0时,唤醒所有在此条件对象上等待的线程
唤醒之后,重置屏障,以便下次使用
-
关于超时和异常、线程中断:
若线程超时,未能到达屏障,则重置/销毁屏障
程序发生异常,则重置/销毁屏障
唤醒所有线程
2.3. 示例代码
2.3.1. 需求:
3个小伙伴一起出游。路线:故宫——奥林匹克公园——香山公园。于某日某时某刻在第一站“故宫”集合,然后自由玩耍;出发去下一站之前再次集合,直到游玩结束。
2.3.1. 思路分析:
- 分别用三个线程模拟三个小伙伴;
- 定义集合点,先到达的小伙伴等待;
- 所有小伙伴集合完毕,之后再出发去下一站;
2.3.1. 步骤分析:
-
定义CyclicBarrier对象:代表集合点
约定集合的小伙伴的个数:3
-
在需要的集合的地方,调用await方法进行等待
await()方法会自动计数
计数扣减为0时,会自动唤醒所有线程
2.3.1. 代码实现:
// 三个旅游站点
String[] imperialPalace = {"天安门", "延禧宫", "慈宁宫"};
String[] olympic = {"鸟巢", "水立方", "森林公园"};
String[] fragrantHills = {"香山东门", "香山寺", "见心斋", "碧云寺"};
String[][] places = {imperialPalace, olympic, fragrantHills};
// 1.定义CyclicBarrier对象:代表集合点
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
System.err.println("所有小伙伴集合完成");
});
Runnable play = () -> {
String name = Thread.currentThread().getName();
try {
for (int placeNumber = 0; placeNumber < places.length; placeNumber++) {
// 出发
System.out.println("小伙伴们已出发。。。正在前往第" + (placeNumber + 1) + "个站点:"
+ Arrays.toString(places[placeNumber]));
// 游玩
String[] place = places[placeNumber]; // 景点数组
Random random = new Random();
for (int i = 0; i < place.length; i++) {
int index = random.nextInt(place.length); // 随机游玩的景点的索引
Thread.sleep((index + 1) * 1000); // 线程休眠,在此地游玩时间
System.out.println("小伙伴"+ name +"在"+ place[index] +"玩了" + index + "秒");
}
System.out.println("小伙伴"+ name +"正在前往第"+ (placeNumber + 1) +"个集合点");
// 集合
cyclicBarrier.await(); // 所有线程在此处阻塞
}
} catch (Exception e) {
e.printStackTrace();
}
// 结束
System.out.println("小伙伴"+ name +"游玩结束,今天很开心");
};
// 三个小伙伴
for (int i = 0; i < 3; i ++) {
new Thread(play).start();
}
2.3.1. 输出结果:
小伙伴们已出发。。。正在前往第1个站点:[天安门, 延禧宫, 慈宁宫]
小伙伴们已出发。。。正在前往第1个站点:[天安门, 延禧宫, 慈宁宫]
小伙伴们已出发。。。正在前往第1个站点:[天安门, 延禧宫, 慈宁宫]
小伙伴Thread-1在延禧宫玩了1秒
小伙伴Thread-2在慈宁宫玩了2秒
小伙伴Thread-0在慈宁宫玩了2秒
小伙伴Thread-2在延禧宫玩了1秒
小伙伴Thread-1在慈宁宫玩了2秒
小伙伴Thread-0在慈宁宫玩了2秒
小伙伴Thread-1在延禧宫玩了1秒
小伙伴Thread-1正在前往第1个集合点
小伙伴Thread-2在慈宁宫玩了2秒
小伙伴Thread-2正在前往第1个集合点
小伙伴Thread-0在慈宁宫玩了2秒
小伙伴Thread-0正在前往第1个集合点
所有小伙伴集合完成
小伙伴们已出发。。。正在前往第2个站点:[鸟巢, 水立方, 森林公园]
小伙伴们已出发。。。正在前往第2个站点:[鸟巢, 水立方, 森林公园]
小伙伴们已出发。。。正在前往第2个站点:[鸟巢, 水立方, 森林公园]
小伙伴Thread-0在水立方玩了1秒
小伙伴Thread-2在水立方玩了1秒
小伙伴Thread-1在森林公园玩了2秒
小伙伴Thread-2在鸟巢玩了0秒
小伙伴Thread-1在鸟巢玩了0秒
小伙伴Thread-0在水立方玩了1秒
小伙伴Thread-2在水立方玩了1秒
小伙伴Thread-2正在前往第2个集合点
小伙伴Thread-0在水立方玩了1秒
小伙伴Thread-0正在前往第2个集合点
小伙伴Thread-1在森林公园玩了2秒
小伙伴Thread-1正在前往第2个集合点
所有小伙伴集合完成
小伙伴们已出发。。。正在前往第3个站点:[香山东门, 香山寺, 见心斋, 碧云寺]
小伙伴们已出发。。。正在前往第3个站点:[香山东门, 香山寺, 见心斋, 碧云寺]
小伙伴们已出发。。。正在前往第3个站点:[香山东门, 香山寺, 见心斋, 碧云寺]
小伙伴Thread-2在香山寺玩了1秒
小伙伴Thread-1在香山寺玩了1秒
小伙伴Thread-2在香山东门玩了0秒
小伙伴Thread-0在碧云寺玩了3秒
小伙伴Thread-1在香山寺玩了1秒
小伙伴Thread-0在香山东门玩了0秒
小伙伴Thread-2在香山寺玩了1秒
小伙伴Thread-1在香山寺玩了1秒
小伙伴Thread-2在香山东门玩了0秒
小伙伴Thread-2正在前往第3个集合点
小伙伴Thread-0在香山寺玩了1秒
小伙伴Thread-0在香山东门玩了0秒
小伙伴Thread-0正在前往第3个集合点
小伙伴Thread-1在见心斋玩了2秒
小伙伴Thread-1正在前往第3个集合点
所有小伙伴集合完成
小伙伴Thread-0游玩结束,今天很开心
小伙伴Thread-1游玩结束,今天很开心
小伙伴Thread-2游玩结束,今天很开心
2.4. CountDownLatch和CyclicBarrier的区别
3. Semaphore
3.1. 概念和作用
信号量,信号灯;
它允许线程集等待,直到被允许继续运行为止。一个信号量管理多个许可(permit),线程必须请求许可,否则等待。
-
特点
控制访问多个共享资源的计数器
-
使用场景
通常用于限制访问资源的线程总数;
流量控制;
-
基本操作
acquire():请求许可
release():释放许可
3.2. 注意事项
-
锁是由其它线程释放的,而不是主线程(车场管理员)
-
获取许可和释放许可的不一定是同一个线程
第一个线程释放,第二个线程请求获取
-
线程可以申请多个许可,也可以释放多个许可
-
许可初始化数量为1时,可以当作普通的互斥锁来使用
3.3. 扩展:底层实现机制
公平锁和非公平锁
共享锁
3.4. 示例代码
3.4.1. 需求:
假设停车场有3个车位,且都没有停车。先后到来5辆车,管理员只能允许3辆车停入车位。直到车辆陆陆续续从车场离开,有了空余车位,后来的车才能停入车位。
3.4.1. 思路分析:
- 车场管理员就是一个Semaphore,控制车辆访问车位
- 车辆入场,必须向管理员申请许可(permit)
- 车辆出场,需要向管理员释放许可(permit)
3.4.1. 实现步骤:
-
定义车场管理员对象Semaphore,管理3个车位
Semaphore semaphore = new Semaphore(3);
-
车辆入场,申请许可:
acquire()
-
车辆出场,释放许可:
release()
3.4.1. 代码实现:
// 1.定义车场管理员对象Semaphore,管理3个车位
Semaphore parkingAdmin = new Semaphore(3);
Runnable carRunnable = () -> {
try {
String name = Thread.currentThread().getName();
// 2.车辆入场,申请许可:
parkingAdmin.acquire();
// 停车入位
Random random = new Random();
int seconds = random.nextInt(3) + 1;
Thread.sleep(seconds * 1000);
System.out.println(name + "停车入位,耗时"+ seconds +"秒");
// 3.车辆出场,释放许可
seconds = random.nextInt(15) + 1;
Thread.sleep(seconds * 1000);
parkingAdmin.release();
System.err.println(name + "车辆离场,停车"+ seconds +"秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
for (int i = 0; i < 5; i++) {
new Thread(carRunnable).start();
}
3.4.1. 输出结果:
Thread-0停车入位,耗时1秒
Thread-1停车入位,耗时2秒
Thread-2停车入位,耗时3秒
Thread-0车辆离场,停车8秒
Thread-4停车入位,耗时3秒
Thread-1车辆离场,停车13秒
Thread-3停车入位,耗时2秒
Thread-2车辆离场,停车14秒
Thread-4车辆离场,停车7秒
Thread-3车辆离场,停车10秒
4. Exchanger
4.1. 概念和作用
交换器;
允许两个线程在要交换的对象准备好时交换对象 。
-
特点
在成对出现的线程间交换数据
-
使用场景
一个线程向实例添加数据,
另一个线程从实例清除数据
-
基本操作
exchange(V):尝试交换指定对象。直到配对的线程执行到此处之前,此线程一直等待
4.2. 注意事项
- Exchanger交换器的交换槽,只支持两两交换
4.3. 示例代码
4.3.1. 需求:
两个特务头子约定在某时某地交换身份
4.3.1. 思路分析:
- 使用两个线程来分别模拟两个特务
- 定义交换点(同步点),先到达的特务等待
- 后来的特务与先到的特务交换身份
4.3.1. 实现步骤:
-
创建交换器对象:
Exchanger<V> exchanger = new Exchanger();
-
在指定位置交换身份:
exchange(V)
4.3.1. 代码实现:
// 1.创建交换器对象
Exchanger<UserInfo> exchanger = new Exchanger<>();
Runnable runnable = () -> {
try {
// 前往交换地点
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "正在前往交换地点....");
// 到达交换点,等待
UserInfo user = new UserInfo(18, threadName);
System.out.println(threadName + "已到达交换点");
// 进行交换
user = exchanger.exchange(user); // 在第二个线程到达此处之前,第一个线程一直等待
System.out.println("交易完成,交易前我是"+ threadName +",交换后的信息是:" + user);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(runnable, "特务头子1").start();
new Thread(runnable, "特务头子2").start();
4.3.1. 输出结果:
特务头子2正在前往交换地点....
特务头子1正在前往交换地点....
特务头子2已到达交换点
特务头子1已到达交换点
交易完成,交易前我是特务头子2,交换后的信息是:UserInfo{age=18, name='特务头子1'}
交易完成,交易前我是特务头子1,交换后的信息是:UserInfo{age=18, name='特务头子2'}