目录
1.Semaphore(资源调度)
2.CountDownLatch(子线程优先)
3.CyclicBarrier(栅栏)
4.Exchanger(公共交换区)
5.CompletableFuture(异步编程)
由于系统资源并不是无限的,如果多线程的无度索取的话,会对系统造成非常大的负担。在JUC中引入了Semaphore资源调度,通过设置固定的资源量,让线程进行争取。最常见的就是业务办理时候虽然人很多,但是办理窗口是有限的。不是每个人一个窗口。
以下是Semaphore类的常用方法:
方法签名 | 说明 |
---|---|
Semaphore(int permits) | 构造一个具有给定许可数的信号量,并设置为非公平模式 |
Semaphore(int permit, boolean fair) | 构造一个具有给定许可数的信号量,并根据公平性参数指定是否在获取信号量时使用先进先出的排序 |
acquire() | 从信号量获取一个许可,如果没有可用的许可,则阻塞直到有可用的许可 |
availablePermits() | 返回当前可用的许可数 |
release() | 释放一个许可,将其返回给信号量 |
案例代码:证件办理,有10个人需要办理某证件但是只有两个办理业务窗口
package Example2118;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class javaDemo {
public static void main(String[] args) {
// 只有两个办理证件窗口
Semaphore semaphore = new Semaphore(2);
// 十个线程客户
for (int i=0;i<10;i++){
new Thread(()->{
try {
// 排队抢在前面
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到首位");
// 业务办理与延迟时间
System.out.println("开始进行业务办理");
TimeUnit.SECONDS.sleep(2);
// 业务办理结束并释放资源
System.out.println("一轮业务办理结束");
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
},"客户"+i).start();
}
}
}
面试题:你了解过哪些同步器?请分别介绍下。(1)
(1)Semaphore同步器
特征:
经典的信号量,通过计数器控制对共享资源的访问
Semaphore(int count):创建拥有count个许可证的信号量
acquire()/acquire(int num) : 获取1/num个许可证
release/release(int num) : 释放1/num个许可证
在开发中有时候会遇到一个程序的运行,需要先开启完所有的服务才能运行。意思是必须先让所有子线程运行完主线程才会运行。就像是考试一样,需要先复习完所有的学科才进行考试。所以JUC引入了CountDownLatch
以下是CountDownLatch类的常用方法:
方法签名 | 说明 |
---|---|
CountDownLatch(int count) | 构造一个具有给定计数的CountDownLatch对象 |
await() | 导致当前线程等待,直到计数到达零,除非线程被中断 |
countdown() | 递减计数器的计数,如果计数达到零,则释放所有等待的线程 |
getCount() | 返回当前计数 |
案例代码:
在校园中举办了校运会,现在有五位选手进行50米的比赛,当所有选手都准备好的时候裁判才会鸣枪指示。其中有位选手在准备前突然口渴,需要喝完水再准备比赛。
package Example2119;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class javaDemo {
public static void main(String[] args)throws Exception {
// 五位选手入场
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i=1;i<=5;i++){
new Thread(()->{
try {
if (Thread.currentThread().getName().equals("选手3")){
System.out.println("选手3喝水中");
// 突然口渴了去喝个水
TimeUnit.SECONDS.sleep(3);
}
// 当选手就绪时候等待计数器减少1
System.out.println(Thread.currentThread().getName()+"已经就绪");
countDownLatch.countDown();
}catch (Exception e){
e.printStackTrace();
}
},"选手"+i).start();
}
// 触发线程等待
countDownLatch.await();
System.out.println("所有选手都准备好了,准备开始!");
}
}
面试题:你了解过哪些同步器?请分别介绍下。(2)
(2)CountDownLatch同步器
特征:
必须发生指定数量的事件后才可以继续运行(比如赛跑比赛,裁判喊出3,2,1之后大家才同时跑)
CountDownLatch(int count):必须发生count个数量才可以打开锁存器
await:等待锁存器
countDown:触发事件
与CountDownLatch类似,JUC提供了一个CyclicBarrier,其在程序中设置一个公共屏障类把已经到达的线程隔绝在外让其等待,当线程数量足够满足公共屏障的条件时候,则一起运行。注意栅栏并不阻碍主线程,也就意味着主线程和子线程都可以正常运行。
CyclicBarrier类的常用方法:
方法签名 | 说明 |
---|---|
CyclicBarrier(int parties) | 创建一个新的 CyclicBarrier,当指定数量的线程(parties)调用 await() 方法时,它将打开屏障 |
CyclicBarrier(int parties, Runnable barrierAction) | 创建一个新的 CyclicBarrier,当指定数量的线程(parties)调用 await() 方法时,它将执行给定的 barrierAction,然后打开屏障 |
await() | 导致当前线程等待,直到所有参与者线程都达到屏障位置 |
await(timeout) | 导致当前线程等待,直到所有参与者线程都达到屏障位置,或者超过指定的超时时间 |
getNumber() | 返回当前在屏障处等待的参与者数目 |
reset() | 将屏障重置为其初始状态 |
isBroken() | 查询当前屏障是否处于破坏状态 |
getParties() | 返回需要参与屏障的参与者数目 |
案例:
设置多股水流,一旦水流超过界限值,则冲垮大坝。然后过一段时间后重新修复大坝
package Example2120;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
public class javaDemo {
public static void main(String[] args)throws Exception{
// 当出现十股超大型水流汇聚时候大坝就无法承受
CyclicBarrier barrier = new CyclicBarrier(10,()->{
System.out.println("大坝:太重啦我承受不了了,裂开!");
});
for (int i=1;i<=10;i++){
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"汇聚"+",当前已经汇聚"+",正在等待其他水流汇聚");
// 等待暴风雨来临
TimeUnit.SECONDS.sleep(1);
barrier.await();
}catch (Exception e){
e.printStackTrace();
}
},"第"+i+"股水流").start();
}
TimeUnit.SECONDS.sleep(3);
// 大坝修复工作
System.out.println("修复大坝");
barrier.reset();
System.out.println("大坝现在的承受量为"+barrier.getParties()+"股水流");
}
}
面试题:你了解过哪些同步器?请分别介绍下。(3)
(3)CyclicBarrier同步器
特征:
适用于只有多个线程都到达预定点时才可以继续执行(比如斗地主,需要等齐三个人才开始)
CyclicBarrier(int num) :等待线程的数量
CyclicBarrier(int num, Runnable action) :等待线程的数量以及所有线程到达后的操作
await() : 到达临界点后暂停线程
由于消费者-生产者模型中数据交换需要一个独立的空间,所以JUC专门为此设计了一个Exchanger泛型类。并且使用条件一定需要多个线程。
Exchanger
方法名 | 方法描述 |
---|---|
exchange(V x) |
在当前线程中调用此方法并传递数据 x ,然后等待另一个线程也调用 exchange() 方法,在交换时将自己的数据与对方交换,并返回对方线程传递过来的数据 |
exchange(V x, long timeout, TimeUnit unit) |
在当前线程中调用此方法并传递数据 x ,然后等待另一个线程也调用 exchange() 方法,在交换时将自己的数据与对方交换,并返回对方线程传递过来的数据。如果超过指定的超时时间,将抛出 TimeoutException 异常 |
exchange(V x, long timeout, TimeUnit unit, boolean timed) |
在当前线程中调用此方法并传递数据 x ,然后等待另一个线程也调用 exchange() 方法,在交换时将自己的数据与对方交换,并返回对方线程传递过来的数据。如果超过指定的超时时间,将根据 timed 参数决定是否抛出 TimeoutException 异常 |
toString() |
返回此 Exchanger 对象的字符串表示形式 |
案例:展示通过Exchanger实现生产者-消费者模型(自助餐中厨师和客人)
package Example2121;
import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;
public class javaDemo {
public static void main(String[] args) throws Exception{
String dinners[] = new String[]{"番茄炒蛋", "宫保鸡丁", "麻婆豆腐", "红烧肉", "清蒸鲈鱼", "香辣虾", "回锅肉", "鱼香肉丝", "干煸豆角", "糖醋排骨"
};
// 创建交换空间
Exchanger dinner = new Exchanger<>();
// 厨师线程
new Thread(()->{
try {
for (int i =1;i<=10;i++){
// 厨师正在做菜
TimeUnit.SECONDS.sleep(2);
// 做好了饭菜
dinner.exchange(dinners[i]);
System.out.println("厨师做好了"+dinners[i]+"并端上了桌");
}
}catch (Exception e){
e.printStackTrace();
}
},"厨师").start();
// 顾客线程
new Thread(()->{
String data = null;
try {
while (true){
// 顾客拿去交换区的食物
data = dinner.exchange(null);
// 顾客享用饭菜并等待下一道菜
System.out.println("客人正在享用"+data);
TimeUnit.SECONDS.sleep(3);
System.out.println("享用完毕,正在等待下一道菜");
}
}catch (Exception e){
e.printStackTrace();
}
},"客人").start();
}
}
在JDK1.8以后,JUC提供了Completable简化了异步编程的复杂性.
CompletableFuture常用方法:
方法名 | 方法描述 |
---|---|
T get() |
阻塞当前线程,直到结果可用,并返回结果值 |
void complete(T value) |
手动完成计算,并设置计算结果为指定的值 |
案例:在校运会跑步比赛时候,只有当裁判鸣枪时候才能起步冲刺
package Example2122;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class javaDemo {
public static void main(String[] args) {
CompletableFuture future = new CompletableFuture<>();
Random random = new Random();
for (int i=1;i<=3;i++){
new Thread(()->{
System.out.println("选手准备好了,正在等待枪声");
try {
// 开枪啦,跑步
System.out.println("选手听到"+future.get());
TimeUnit.SECONDS.sleep(random.nextInt(5));
System.out.println(Thread.currentThread().getName()+"到达终点");
}catch (Exception e){
}
},"选手-"+i).start();
}
new Thread(()->{
try {
// 倒计时3秒开枪
TimeUnit.SECONDS.sleep(3);
future.complete("开枪啦");
}catch (Exception e){
e.printStackTrace();
}
},"裁判").start();
}
}
面试题:AQS 对资源的共享方式?
(1)Exclusive(独占)
只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的(2)Share(共享)
多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。