Java高并发系列——等待唤醒
疑问:
Q:Condition能够支持超时时间的设置,而Object不支持。Object不是有wait(long timeout)超时时间设置么?
这个指的是Object没有方法可以使当前线程释放锁并进入等待状态到将来某个时间。
Q:在Condition.await()方法被调用时,当前线程会释放这个锁,并且当前线程会进行等待(处于阻塞状态)?是阻塞状态不是等待状态?是因为这事api语言级别的等待/通知机制么?
不是处于阻塞状态,而是进入等待队列。
JUC中的LockSupport工具类——无需加锁及考虑等待通知顺序
使用Object类中的方法实现线程等待和唤醒
关于Object类中的用户线程等待和唤醒的方法,总结一下:
- wait()/notify()/notifyAll()方法都必须放在同步代码(必须在synchronized内部执行)中执行,需要先获取锁(否则抛出了
IllegalMonitorStateException
异常 ) - 线程唤醒的方法(notify、notifyAll)需要在等待的方法(wait)之后执行,等待中的线程才可能会被唤醒,否则无法唤醒
使用Condition实现线程的等待和唤醒
关于Condition中方法使用总结:
- 使用Condtion中的线程等待和唤醒方法之前,需要先获取锁。否者会报 IllegalMonitorStateException异常
- signal()方法先于await()方法之前调用,线程无法被唤醒
Object和Condition的局限性
关于Object和Condtion中线程等待和唤醒的局限性,有以下几点:
- 两种方式中的让线程等待和唤醒的方法能够执行的先决条件是:线程需要先获取锁
- 唤醒方法需要在等待方法之后调用,线程才能够被唤醒
关于这2点,LockSupport都不需要,就能实现线程的等待和唤醒。
LockSupport类介绍
LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程。主要是通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作的。(注意park方法等待不释放锁)
每个线程都有一个许可(permit),permit只有两个值1和0,默认是0。
- 当调用unpark(thread)方法,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)。
- 当调用park()方法,如果当前线程的permit是1,那么将permit设置为0,并立即返回。如果当前线程的permit是0,那么当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0,并返回。
注意:因为permit默认是0,所以一开始调用park()方法,线程必定会被阻塞。调用unpark(thread)方法后,会自动唤醒thread线程,即park方法立即返回。
LockSupport中常用的方法
阻塞线程
- void park():阻塞当前线程,如果调用unpark方法或者当前线程被中断,从能从park()方法中返回
- void park(Object blocker):功能同方法1,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查
- void parkNanos(long nanos):阻塞当前线程,最长不超过nanos纳秒,增加了超时返回的特性
- void parkNanos(Object blocker, long nanos):功能同方法3,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查
- void parkUntil(long deadline):阻塞当前线程,直到deadline,deadline是一个绝对时间,表示某个时间的毫秒格式
- void parkUntil(Object blocker, long deadline):功能同方法5,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
唤醒线程
- void unpark(Thread thread):唤醒处于阻塞状态的指定线程
1、LockSupport调用park、unpark方法执行唤醒等待无需加锁。
2、LockSupport中,唤醒的方法不管是在等待之前还是在等待之后调用,线程都能够被唤醒。 唤醒方法在等待方法之前执行,线程也能够被唤醒,这点是另外两种方法无法做到的。而Object和Condition中的唤醒必须在等待之后调用,线程才能被唤醒。
3、park方法可以相应线程中断。
LockSupport.park方法让线程等待之后,唤醒方式有2种:
- 调用LockSupport.unpark方法
- 调用等待线程的 interrupt()方法,给等待的线程发送中断信号,可以唤醒线程
线程t1和t2的不同点是,t2中调用park方法传入了一个BlockerDemo对象,从上面的线程堆栈信息中,发现t2线程的堆栈信息中多了一行 -parking to waitfor<0x00000007180bfeb0>(a com.itsoku.chat10.Demo10$BlockerDemo)
,刚好是传入的BlockerDemo对象,park传入的这个参数可以让我们在线程堆栈信息中方便排查问题,其他暂无他用。
线程等待和唤醒的3种方式做个对比
- 方式1:Object中的wait、notify、notifyAll方法
- 方式2:juc中Condition接口提供的await、signal、signalAll方法
- 方式3:juc中的LockSupport提供的park、unpark方法
3种方式对比:
Object | Condtion | LockSupport | |
---|---|---|---|
前置条件 | 需要在synchronized中运行 | 需要先获取Lock的锁 | 无 |
无限等待 | 支持 | 支持 | 支持 |
超时等待 | 支持 | 支持 | 支持 |
等待到将来某个时间返回 | 不支持 | 支持 | 支持 |
等待状态中释放锁 | 会释放 | 会释放 | 不会释放 |
唤醒方法先于等待方法执行,能否唤醒线程 | 否 | 否 | 可以 |
是否能响应线程中断 | 是 | 是 | 是 |
线程中断是否会清除中断标志 | 是 | 是 | 否 |
是否支持等待状态中不响应中断 | 不支持 | 支持 | 不支持 |
实例:
public class LockSupportTest {
/**
* 输出:
* create a thread start
* 主线程执行完毕!
* thread 被唤醒
*/
public static void main1(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("create a thread start");
LockSupport.park();
System.out.println("thread 被唤醒");
});
t.start();
TimeUnit.SECONDS.sleep(3);
//唤醒处于阻塞状态的指定线程
LockSupport.unpark(t);
//响应线程中断
//t.interrupt();
System.out.println("主线程执行完毕!");
}
/**
* 唤醒方法在等待方法之前执行,线程也能够被唤醒
*输出:
* create a thread start
* 主线程执行完毕!
* thread 被唤醒
*/
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("create a thread start");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park();
System.out.println("thread 被唤醒");
});
t.start();
TimeUnit.SECONDS.sleep(1);
//唤醒处于阻塞状态的指定线程
LockSupport.unpark(t);
//响应线程中断
//t.interrupt();
System.out.println("主线程执行完毕!");
}
}
疑问:
Q:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程。主要是通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作的。(注意park方法等待不释放锁)不释放锁的等待唤醒是在什么场景下使用?为什么要这样?
在我看来是因为LockSupport是不需要锁就能进行等待唤醒的,因此如果有锁是话也是其他资源方面的锁,跟LockSupport无关。
JUC中的Semaphore(信号量)——多把锁控制,用于限流
synchronized和重入锁ReentrantLock,这2种锁一次都只能允许一个线程访问一个资源,而信号量可以控制有多少个线程可以访问特定的资源。
Semaphore常用场景:限流
举个例子:
比如有个停车场(临界值,共享资源),有5个空位,门口有个门卫,手中5把钥匙分别对应5个车位上面的锁,来一辆车,门卫会给司机一把钥匙,然后进去找到对应的车位停下来,出去的时候司机将钥匙归还给门卫。停车场生意比较好,同时来了100两车,门卫手中只有5把钥匙,同时只能放5辆车进入,其他车只能等待,等有人将钥匙归还给门卫之后,才能让其他车辆进入。
上面的例子中门卫就相当于Semaphore,车钥匙就相当于许可证,车就相当于线程。
Semaphore主要方法
Semaphore(int permits):构造方法,参数表示许可证数量,用来创建信号量
Semaphore(int permits,boolean fair):构造方法,当fair等于true时,创建具有给定许可数的计数信号量并设置为公平信号量
void acquire() throws InterruptedException:从此信号量获取1个许可前线程将一直阻塞,相当于一辆车占了一个车位,此方法会响应线程中断,表示调用线程的interrupt方法,会使该方法抛出InterruptedException异常
void acquire(int permits) throws InterruptedException :和acquire()方法类似,参数表示需要获取许可的数量;比如一个大卡车要入停车场,由于车比较大,需要申请3个车位才可以停放
void acquireUninterruptibly(int permits) :和acquire(int permits) 方法类似,只是不会响应线程中断
boolean tryAcquire():尝试获取1个许可,不管是否能够获取成功,都立即返回,true表示获取成功,false表示获取失败
boolean tryAcquire(int permits):和tryAcquire(),表示尝试获取permits个许可
boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException:尝试在指定的时间内获取1个许可,获取成功返回true,指定的时间过后还是无法获取许可,返回false
boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException:和tryAcquire(long timeout, TimeUnit unit)类似,多了一个permits参数,表示尝试获取permits个许可
void release():释放一个许可,将其返回给信号量,相当于车从停车场出去时将钥匙归还给门卫
void release(int n):释放n个许可
int availablePermits():当前可用的许可数
获取许可之后不释放
取许可后,没有释放许可的代码,最终导致,可用许可数量为0,其他线程无法获取许可,会在 semaphore.acquire();
处等待,导致程序无法结束。
没有获取到许可却执行释放(没有获取到许可却在finally中直接执行release方法)
如果获取锁的过程中发生异常,导致获取锁失败,最后finally里面也释放了许可,最终会导致许可数量凭空增长了。
释放许可正确的姿势
程序中增加了一个变量 acquireSuccess
用来标记获取许可是否成功,在finally中根据这个变量是否为true,来确定是否释放许可。
在规定的时间内希望获取许可
Semaphore内部2个方法可以提供超时获取许可的功能:
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedExceptionpublic boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException
在指定的时间内去尝试获取许可,如果能够获取到,返回true,获取不到返回false。
其他一些使用说明
- Semaphore默认创建的是非公平的信号量,什么意思呢?这个涉及到公平与非公平。让新来的去排队就表示公平,直接去插队争抢第一个,就表示不公平。对于停车场,排队肯定更好一些。不过对于信号量来说不公平的效率更高一些,所以默认是不公平的。
- 方法中带有
throwsInterruptedException
声明的,表示这个方法会响应线程中断信号,什么意思?表示调用线程的interrupt()
方法后,会让这些方法触发InterruptedException
异常,即使这些方法处于阻塞状态,也会立即返回,并抛出InterruptedException
异常,线程中断信号也会被清除。
示例:
public class SemaphoreTest {
private static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
Thread t = new T1("T" + i);
t.start();
if (i > 2) {
TimeUnit.SECONDS.sleep(1);
t.interrupt();
}
}
}
public static void main2(String[] args) {
for (int i = 0; i < 5; i++) {
Thread t = new T2("T" + i);
t.start();
}
}
/**
* 在指定的时间内去尝试获取许可,如果能够获取到,返回true,获取不到返回false。
*/
public static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
Boolean hasTicket = false;
Thread thread = Thread.currentThread();
try {
//semaphore.acquire();
hasTicket = semaphore.tryAcquire(1, TimeUnit.SECONDS);
if (hasTicket) {
System.out.println(thread + "获取到停车位!");
} else {
System.out.println(thread + "获取不到停车位!走了");
}
//hasTicket = true;
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (hasTicket) {
semaphore.release();
//没有获取到许可却在finally中直接执行release方法
//Thread[T6,5,main]离开停车位!当前空余停车位数量10
System.out.println(thread + "离开停车位!当前空余停车位数量" + semaphore.availablePermits());
}
}
}
}
/**
* 正确的释放锁的方式
*/
public static class T2 extends Thread {
public T2(String name) {
super(name);
}
@Override
public void run() {
Boolean hasTicket = false;
Thread thread = Thread.currentThread();
try {
semaphore.acquire();
hasTicket = true;
System.out.println(thread + "获取到停车位!");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (hasTicket) {
semaphore.release();
System.out.println(thread + "离开停车位!当前空余停车位数量" + semaphore.availablePermits());
}
}
}
}
}
输出:
test1
Thread[T0,5,main]获取到停车位!
Thread[T1,5,main]获取到停车位!
Thread[T2,5,main]获取不到停车位!走了
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedNanos(AbstractQueuedSynchronizer.java:1039)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.tryAcquireSharedNanos(AbstractQueuedSynchronizer.java:1328)
at java.util.concurrent.Semaphore.tryAcquire(Semaphore.java:409)
at com.self.current.SemaphoreTest$T1.run(SemaphoreTest.java:52)
java.lang.InterruptedException: sleep interrupted
Thread[T1,5,main]离开停车位!当前空余停车位数量1
at java.lang.Thread.sleep(Native Method)
Thread[T4,5,main]获取到停车位!
at java.lang.Thread.sleep(Thread.java:340)
Thread[T0,5,main]离开停车位!当前空余停车位数量1
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
Thread[T4,5,main]离开停车位!当前空余停车位数量2
at com.self.current.SemaphoreTest$T1.run(SemaphoreTest.java:59)
test2
Thread[T0,5,main]获取到停车位!
Thread[T1,5,main]获取到停车位!
Thread[T1,5,main]离开停车位!当前空余停车位数量2
Thread[T2,5,main]获取到停车位!
Thread[T0,5,main]离开停车位!当前空余停车位数量2
Thread[T3,5,main]获取到停车位!
Thread[T2,5,main]离开停车位!当前空余停车位数量1
Thread[T4,5,main]获取到停车位!
Thread[T3,5,main]离开停车位!当前空余停车位数量1
Thread[T4,5,main]离开停车位!当前空余停车位数量2
示例2:
通过判断byteBuffer != null得出是否获取到了许可。
semaphore = new Semaphore(maxBufferCount);
byteBuffer = allocator.allocate();// semaphore.acquire();
finally {
if (byteBuffer != null) {
byteBuffer.clear();
allocator.release(byteBuffer);
}
}
疑问:
Q:Semaphore内部排队等待资源的队列是怎么实现的,公平信号量与非公平的队列类型都是哪种的?
JUC中等待多线程完成的工具类CountDownLatch(闭锁 )——等待多线程完成后执行操作或者实现最大的并发线程数同时执行
CountDownLatch介绍
CountDownLatch称之为闭锁,它可以使一个或一批线程在闭锁上等待,等到其他线程执行完相应操作后,闭锁打开,这些等待的线程才可以继续执行。确切的说,闭锁在内部维护了一个倒计数器。通过该计数器的值来决定闭锁的状态,从而决定是否允许等待的线程继续执行。
一批线程等待闭锁一般用于同步并发,如跑步比赛时作为发令枪作用;
一个线程等待闭锁一般用于等待并发线程或资源的获取满足,用于执行收尾工作,如多任务执行完后合并结果等。
常用方法:
public CountDownLatch(int count):构造方法,count表示计数器的值,不能小于0,否者会报异常。
public void await() throws InterruptedException:调用await()会让当前线程等待,直到计数器为0的时候,方法才会返回,此方法会响应线程中断操作。
public boolean await(long timeout, TimeUnit unit) throws InterruptedException:限时等待,在超时之前,计数器变为了0,方法返回true,否者直到超时,返回false,此方法会响应线程中断操作。
public void countDown():让计数器减1
CountDownLatch(作为一个参数,锁传递到方法中)使用步骤:
- 创建CountDownLatch对象
- 调用其实例方法
await()
,让当前线程等待 - 调用
countDown()
方法,让计数器减1 - 当计数器变为0的时候,
await()
方法会返回
假如有这样一个需求,当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要统计解析总耗时。分析一下:解析每个sheet耗时可能不一样,总耗时就是最长耗时的那个操作。
方法一:使用join实现。
此方法会让当前线程等待被调用的线程完成之后才能继续。可以看一下join的源码,内部其实是在synchronized方法中调用了线程的wait方法,最后被调用的线程执行完毕之后,由jvm自动调用其notifyAll()方法,唤醒所有等待中的线程。这个notifyAll()方法是由jvm内部自动调用的,jdk源码中是看不到的,需要看jvm源码,有兴趣的同学可以去查一下。所以JDK不推荐在线程上调用wait、notify、notifyAll方法。
方法二:使用CountDownLatch实现。
示例:
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(2);
T1 t1 = new T1("sheet1", 3, latch);
T1 t2 = new T1("sheet2", 5, latch);
t1.start();
t2.start();
//调用await()会让当前线程等待,直到计数器为0的时候,方法才会返回,此方法会响应线程中断操作。
//latch.await();
//限时等待,在超时之前,计数器变为了0,方法返回true,否者直到超时,返回false,此方法会响应线程中断操作。
boolean result = latch.await(4, TimeUnit.SECONDS);
long end = System.currentTimeMillis();
//System.out.println("主线程结束,耗时" + (end - start));
System.out.println("主线程结束,耗时" + (end - start)+"是否返回结果:"+result);
}
public static class T1 extends Thread {
private int workTime;
private CountDownLatch countDownLatch;
public T1(String name, int workTime, CountDownLatch countDownLatch) {
super(name);
this.workTime = workTime;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
long start = System.currentTimeMillis();
Thread t = Thread.currentThread();
System.out.println(t.getName() + "线程开始执行!");
try {
TimeUnit.SECONDS.sleep(workTime);
long end = System.currentTimeMillis();
System.out.println(t.getName() + "结束,耗时" + (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
}
}
输出:
sheet1线程开始执行!
sheet2线程开始执行!
sheet1结束,耗时3000
主线程结束,耗时4004是否返回结果:false
sheet2结束,耗时5001
主线程中调用 countDownLatch.await();会让主线程等待,t1、t2线程中模拟执行耗时操作,最终在finally中调用了 countDownLatch.countDown();,此方法每调用一次,CountDownLatch内部计数器会减1,当计数器变为0的时候,主线程中的await()会返回,然后继续执行。注意:上面的 countDown()这个是必须要执行的方法,所以放在finally中执行。
2个CountDown结合使用的示例——跑步比赛耗时统计
有3个人参见跑步比赛,需要先等指令员发指令枪后才能开跑,所有人都跑完之后,指令员喊一声,大家跑完了,计算耗时。
示例:
public class CountDownLatchImplRacingTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch fireGun = new CountDownLatch(1);
CountDownLatch latch = new CountDownLatch(3);
new T1("刘翔", 3, fireGun, latch).start();
new T1("拼嘻嘻", 5, fireGun, latch).start();
new T1("蜡笔小新", 7, fireGun, latch).start();
System.out.println("比赛 wait for ready!");
////主线程休眠3秒,模拟指令员准备发枪耗时操作
TimeUnit.SECONDS.sleep(3);
long start = System.currentTimeMillis();
System.out.println("发令枪响,比赛开始!");
fireGun.countDown();
latch.await();
long end = System.currentTimeMillis();
System.out.println("比赛结束,总耗时" + (end - start));
}
public static class T1 extends Thread {
private int workTime;
private CountDownLatch fireGun;
private CountDownLatch countDownLatch;
public T1(String name, int workTime, CountDownLatch fireGun, CountDownLatch countDownLatch) {
super(name);
this.workTime = workTime;
this.fireGun = fireGun;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
fireGun.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long start = System.currentTimeMillis();
Thread t = Thread.currentThread();
System.out.println(t.getName() + "运动员开始赛跑!");
try {
//模拟耗时操作,休眠workTime秒
TimeUnit.SECONDS.sleep(workTime);
long end = System.currentTimeMillis();
System.out.println(t.getName() + "跑完全程,耗时" + (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
}
}
输出:
比赛 wait for ready!
发令枪响,比赛开始!
刘翔运动员开始赛跑!
蜡笔小新运动员开始赛跑!
拼嘻嘻运动员开始赛跑!
刘翔跑完全程,耗时3000
拼嘻嘻跑完全程,耗时5002
蜡笔小新跑完全程,耗时7001
比赛结束,总耗时7001
手写一个并行处理任务的工具类
示例:
public class TaskDisposeUtils {
private static final Integer POOL_SIZE = Integer.max(Runtime.getRuntime().availableProcessors(), 5);
public static void main(String[] args) {
List list = Stream.iterate(1, a -> a + 1).limit(10).collect(Collectors.toList());
try {
TaskDisposeUtils.dispose(list, item -> {
long start = System.currentTimeMillis();
//模拟耗时操作,休眠workTime秒
try {
TimeUnit.SECONDS.sleep(item);
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(item + "任务完成,耗时" + (end - start));
});
} catch (InterruptedException e) {
e.printStackTrace();
}
//上面所有任务处理完毕完毕之后,程序才能继续
System.out.println(list + "任务全部执行完毕!");
}
public static void dispose(List taskList, Consumer consumer) throws InterruptedException {
dispose(true, POOL_SIZE, taskList, consumer);
}
private static void dispose(boolean moreThread, Integer poolSize, List taskList, Consumer consumer) throws InterruptedException {
if (CollectionUtils.isEmpty(taskList)) {
return;
}
if (moreThread && poolSize > 1) {
poolSize = Math.min(poolSize, taskList.size());
ExecutorService executorService = null;
try {
executorService = Executors.newFixedThreadPool(poolSize);
CountDownLatch latch = new CountDownLatch(taskList.size());
for (T t : taskList) {
executorService.execute(() -> {
try {
consumer.accept(t);
} finally {
latch.countDown();
}
});
}
latch.await();
} finally {
if (executorService != null) {
executorService.shutdown();
}
}
} else {
for (T t : taskList) {
consumer.accept(t);
}
}
}
}
输出:
1任务完成,耗时1376
2任务完成,耗时2014
3任务完成,耗时3179
4任务完成,耗时4449
5任务完成,耗时5000
6任务完成,耗时6001
7任务完成,耗时7000
8任务完成,耗时8000
9任务完成,耗时9000
10任务完成,耗时10001
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]任务全部执行完毕!
在实时系统中的使用场景
- 实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数为1的CountDownLatch,并让所有线程都在这个锁上等待,那么我们可以很轻松地完成测试。我们只需调用 一次countDown()方法就可以让所有的等待线程同时恢复执行。
- 开始执行前等待n个线程完成各自任务:例如应用程序启动类要确保在处理用户请求前,所有N个外部系统已经启动和运行了。
- 死锁检测:一个非常方便的使用场景是,你可以使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁
疑问:
Q:这个notifyAll()方法是由jvm内部自动调用的,jdk源码中是看不到的,需要看jvm源码,有兴趣的同学可以去查一下。所以JDK不推荐在线程上调用wait、notify、notifyAll方法。 是因为没有源码所以才不推荐使用wait、notify、notifyAll方法么?还有其他缺点么?
JUC中的循环栅栏CyclicBarrier的6种使用场景
CyclicBarrier简介
CyclicBarrier通常称为循环屏障。它和CountDownLatch很相似,都可以使线程先等待然后再执行。不过CountDownLatch是使一批(一个)线程等待另一批(一个)线程执行完后再执行;而CyclicBarrier只是使等待的线程达到一定数目后再让它们继续执行。故而CyclicBarrier内部也有一个计数器,计数器的初始值在创建对象时通过构造参数指定,如下所示:
public CyclicBarrier(int parties) {
this(parties, null);
}
每调用一次await()方法都将使阻塞的线程数+1,只有阻塞的线程数达到设定值时屏障才会打开,允许阻塞的所有线程继续执行。除此之外,CyclicBarrier还有几点需要注意的地方:
- CyclicBarrier的计数器可以重置而CountDownLatch不行,这意味着CyclicBarrier实例可以被重复使用而CountDownLatch只能被使用一次。而这也是循环屏障循环二字的语义所在。
- CyclicBarrier允许用户自定义barrierAction操作,这是个可选操作,可以在创建CyclicBarrier对象时指定
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
一旦用户在创建CyclicBarrier对象时设置了barrierAction参数,则在阻塞线程数达到设定值屏障打开前,会调用barrierAction的run()方法完成用户自定义的操作。
CyclicBarrier内部相当于有个计数器(构造方法传入的),每次调用
await();
后,计数器会减1,并且await()方法会让当前线程阻塞,等待计数器减为0的时候,所有在await()上等待的线程被唤醒,然后继续向下执行,此时计数器又会被还原为创建时的值,然后可以继续再次使用。
CountDownLatch和CyclicBarrier的区别
CountDownLatch示例
主管相当于 CountDownLatch,干活的小弟相当于做事情的线程。
老板交给主管了一个任务,让主管搞完之后立即上报给老板。主管下面有10个小弟,接到任务之后将任务划分为10个小任务分给每个小弟去干,主管一直处于等待状态(主管会调用await()
方法,此方法会阻塞当前线程),让每个小弟干完之后通知一下主管(调用countDown()
方法通知主管,此方法会立即返回),主管等到所有的小弟都做完了,会被唤醒,从await()方法上苏醒,然后将结果反馈给老板。期间主管会等待,会等待所有小弟将结果汇报给自己。
而CyclicBarrier是一批线程让自己等待,等待所有的线程都准备好了,所有的线程才能继续。
CountDownLatch: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBrrier: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
重复使用CyclicBarrier、自定义一个所有线程到齐后的处理动作实例:
public class CyclicBarrierTest {
/**
* 可以自定义一个所有线程到齐后的处理动作,再唤醒所有线程工作
*/
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6,()->{
System.out.println("人都到齐了,大家high起来!");
});
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
new T1("驴友"+i, i).start();
}
}
public static class T1 extends Thread {
private int workTime;
public T1(String name, int workTime) {
super(name);
this.workTime = workTime;
}
@Override
public void run() {
//等待人齐吃饭
eat();
//等待人齐上车下一站旅游
travel();
}
private void eat(){
Thread t = Thread.currentThread();
//System.out.println(t.getName() + "号旅客开始准备吃饭!");
try {
TimeUnit.SECONDS.sleep(workTime);
long start = System.currentTimeMillis();
cyclicBarrier.await();
long end = System.currentTimeMillis();
System.out.println(t.getName() + "号旅客吃饭了,sleep:"+workTime+",等待耗时" + (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e1) {
e1.printStackTrace();
}
}
private void travel(){
Thread t = Thread.currentThread();
//System.out.println(t.getName() + "号旅客开始准备吃饭!");
try {
TimeUnit.SECONDS.sleep(workTime);
long start = System.currentTimeMillis();
cyclicBarrier.await();
long end = System.currentTimeMillis();
System.out.println(t.getName() + "号旅客上车了,sleep:"+workTime+",等待耗时" + (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e1) {
e1.printStackTrace();
}
}
}
}
输出:
人都到齐了,大家high起来!
驴友5号旅客吃饭了,sleep:5,等待耗时0
驴友0号旅客吃饭了,sleep:0,等待耗时5002
驴友1号旅客吃饭了,sleep:1,等待耗时4000
驴友4号旅客吃饭了,sleep:4,等待耗时1000
驴友3号旅客吃饭了,sleep:3,等待耗时2000
驴友2号旅客吃饭了,sleep:2,等待耗时3000
人都到齐了,大家high起来!
驴友5号旅客上车了,sleep:5,等待耗时0
驴友4号旅客上车了,sleep:4,等待耗时999
驴友3号旅客上车了,sleep:3,等待耗时2000
驴友2号旅客上车了,sleep:2,等待耗时3000
驴友1号旅客上车了,sleep:1,等待耗时3999
驴友0号旅客上车了,sleep:0,等待耗时5000
其中一个线程被interrupt()打断实例:
public class CyclicBarrierBreakTest {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6);
public static class T1 extends Thread {
private int workTime;
public T1(String name, int workTime) {
super(name);
this.workTime = workTime;
}
@Override
public void run() {
//等待人齐吃饭
long start = 0, end = 0;
Thread t = Thread.currentThread();
try {
TimeUnit.SECONDS.sleep(workTime);
start = System.currentTimeMillis();
System.out.println(t.getName() + "号旅客开始准备吃饭!");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
end = System.currentTimeMillis();
System.out.println(t.getName() + "号旅客吃饭了,sleep:" + workTime + ",等待耗时" + (end - start));
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 6; i++) {
int sleep = 0;
//如果线程只是在睡眠过程时,中断的就不是cyclicBarrier.await();触发的,而是 TimeUnit.SECONDS.sleep(workTime);这时候就达不到效果
T1 t = new T1("驴友" + i, sleep);
t.start();
if (i == 3) {
TimeUnit.SECONDS.sleep(1);
System.out.println(t.getName() + ",有点急事,我先吃了!");
t.interrupt();
TimeUnit.SECONDS.sleep(2);
}
}
}
}
输出:
驴友2号旅客开始准备吃饭!
驴友1号旅客开始准备吃饭!
驴友3号旅客开始准备吃饭!
驴友3,有点急事,我先吃了!
驴友3号旅客吃饭了,sleep:0,等待耗时1003
驴友2号旅客吃饭了,sleep:0,等待耗时1004
驴友1号旅客吃饭了,sleep:0,等待耗时1004
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(A
java.util.concurrent.BrokenBarrierException
驴友4号旅客开始准备吃饭!
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
驴友5号旅客开始准备吃饭!
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
驴友6号旅客开始准备吃饭!
at com.self.current.CyclicBarrierBreakTest$T1.run(CyclicBarrierBreakTest.java:38)
驴友4号旅客吃饭了,sleep:0,等待耗时0
java.util.concurrent.BrokenBarrierException
驴友6号旅客吃饭了,sleep:0,等待耗时0
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
驴友5号旅客吃饭了,sleep:0,等待耗时1
java.util.concurrent.BrokenBarrierException
结论:
- 内部有一个人把规则破坏了(接收到中断信号),其他人都不按规则来了,不会等待了
- 接收到中断信号的线程,await方法会触发InterruptedException异常,然后被唤醒向下运行
- 其他等待中 或者后面到达的线程,会在await()方法上触发
BrokenBarrierException
异常,然后继续执行
其中一个线程执行cyclicBarrier.await(2, TimeUnit.SECONDS);只执行超时等待2秒:
结论:
-
等待超时的方法
public int await(long timeout, TimeUnit unit) throws InterruptedException,BrokenBarrierException,TimeoutException
内部有一个人把规则破坏了(等待超时),其他人都不按规则来了,不会等待了
等待超时的线程,await方法会触发TimeoutException异常,然后被唤醒向下运行
其他等待中或者后面到达的线程,会在await()方法上触发
BrokenBarrierException
异常,然后继续执行
重建规则示例:
第一次规则被打乱了,过了一会导游重建了规则(cyclicBarrier.reset();
),接着又重来来了一次模拟等待吃饭的操作,正常了。
public class CyclicBarrierResetTest {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6);
private static boolean onOrder = false;
public static class T1 extends Thread {
private int workTime;
public T1(String name, int workTime) {
super(name);
this.workTime = workTime;
}
@Override
public void run() {
//等待人齐吃饭
long start = 0, end = 0;
Thread t = Thread.currentThread();
try {
TimeUnit.SECONDS.sleep(workTime);
start = System.currentTimeMillis();
System.out.println(t.getName() + "号旅客开始准备吃饭!");
if (!onOrder) {
if (this.getName().equals("驴友1")) {
cyclicBarrier.await(2, TimeUnit.SECONDS);
} else {
cyclicBarrier.await();
}
} else {
cyclicBarrier.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (TimeoutException e){
e.printStackTrace();
}
end = System.currentTimeMillis();
System.out.println(t.getName() + "号旅客吃饭了,sleep:" + workTime + ",等待耗时" + (end - start));
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 6; i++) {
T1 t = new T1("驴友" + i, i);
t.start();
}
//等待7秒之后,重置,重建规则
TimeUnit.SECONDS.sleep(7);
cyclicBarrier.reset();
onOrder = true;
System.out.println("---------------重新按按规则来,不遵守规则的没饭吃!------------------");
//再来一次
for (int i = 1; i <= 6; i++) {
T1 t = new T1("驴友" + i, i);
t.start();
}
}
}
输出:
驴友1号旅客开始准备吃饭!
驴友2号旅客开始准备吃饭!
驴友3号旅客开始准备吃饭!
java.util.concurrent.BrokenBarrierException
驴友3号旅客吃饭了,sleep:3,等待耗时3
java.util.concurrent.TimeoutException
驴友1号旅客吃饭了,sleep:1,等待耗时2005
java.util.concurrent.BrokenBarrierException
驴友2号旅客吃饭了,sleep:2,等待耗时1006
java.util.concurrent.BrokenBarrierException
驴友4号旅客开始准备吃饭!
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
驴友4号旅客吃饭了,sleep:4,等待耗时0
驴友5号旅客开始准备吃饭!
驴友5号旅客吃饭了,sleep:5,等待耗时0
java.util.concurrent.BrokenBarrierException
java.util.concurrent.BrokenBarrierException
驴友6号旅客开始准备吃饭!
驴友6号旅客吃饭了,sleep:6,等待耗时0
---------------重新按按规则来,不遵守规则的没饭吃!------------------
驴友1号旅客开始准备吃饭!
驴友2号旅客开始准备吃饭!
驴友3号旅客开始准备吃饭!
驴友4号旅客开始准备吃饭!
驴友5号旅客开始准备吃饭!
驴友6号旅客开始准备吃饭!
驴友6号旅客吃饭了,sleep:6,等待耗时0
驴友5号旅客吃饭了,sleep:5,等待耗时1000
驴友4号旅客吃饭了,sleep:4,等待耗时2000
驴友3号旅客吃饭了,sleep:3,等待耗时3000
驴友2号旅客吃饭了,sleep:2,等待耗时3999
驴友1号旅客吃饭了,sleep:1,等待耗时5000