线程等待唤醒的三种实现方式
使用Object中的wait和notify方法
public static void waitAndNotify() throws InterruptedException {
Object object=new Object();
new Thread(()->{
synchronized (object){
log.info("进入{}线程",Thread.currentThread().getName());
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("线程{}被唤醒",Thread.currentThread().getName());
}
},"t1").start();
TimeUnit.SECONDS.sleep(2);
new Thread(()->{
synchronized (object){
log.info("进入{}线程,发起通知",Thread.currentThread().getName());
object.notify();
}
},"t2").start();
}
使用Condition中的await和signal方法
public static void awaitAndSignal() throws InterruptedException {
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
new Thread(()->{
try{
lock.lock();
log.info("进入{}线程",Thread.currentThread().getName());
condition.await();
log.info("线程{}被唤醒",Thread.currentThread().getName());
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
},"t1").start();
TimeUnit.SECONDS.sleep(2);
new Thread(()->{
try{
lock.lock();
log.info("进入{}线程,发起通知",Thread.currentThread().getName());
condition.signal();
}finally {
lock.unlock();
}
},"t2").start();
}
使用LockSupport类中的park和unpark方法
public static void parkAndUnPark() throws InterruptedException {
Thread t1=new Thread(()->{
log.info("进入{}线程",Thread.currentThread().getName());
LockSupport.park();
log.info("线程{}被唤醒",Thread.currentThread().getName());
},"t1");
t1.start();
TimeUnit.SECONDS.sleep(2);
new Thread(()->{
log.info("进入{}线程,发起通知",Thread.currentThread().getName());
LockSupport.unpark(t1);
},"t2").start();
}
三种实现方式比较
在使用wait和notify时,需要注意前后的调用顺序,先调用wait后才调用notify,如果顺序不对会导致调用wait方法的线程一直处于阻塞状态,如下面的例子所示
public static void waitAndNotify() throws InterruptedException {
Object object=new Object();
new Thread(()->{
//延迟两秒,让第二个线程先调用notify方法
TimeUnit.SECONDS.sleep(2);
synchronized (object){
log.info("进入{}线程",Thread.currentThread().getName());
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("线程{}被唤醒",Thread.currentThread().getName());
}
},"t1").start();
new Thread(()->{
synchronized (object){
log.info("进入{}线程,发起通知",Thread.currentThread().getName());
object.notify();
}
},"t2").start();
}
并且wait和notify方法的调用需要些在synchronized同步代码块中,否则会抛出监视器状态异常的错误。
在使用await和signal方法时,同样需要注意两者之间的调用顺序,先调用await后才调用signal,否则调用await方法的线程就永远处于阻塞状态。并且,这两个方法的调用都需要放在Lock锁的同步代码块中进行调用,否则也会抛出监视器状态异常的错误。
使用LockSupprot类的park方法和unpark方法时可以完全脱离上述两种等待唤醒机制中所出现的问题,park和unpark的调用不区分先后顺序,并且也不需要再同步代码块中执行,相对来说比上述两种方式要灵活的多。
LockSupport详解
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以 让线程在任意位置阻塞,阻塞之后也有相应的唤醒方法,归根结底,LockSuport调用的是UnSafe中的native代码(底层C语言代码)
park和unpark实现阻塞和解除阻塞的过程
LockSuport和每个使用它的线程都有一个许可证(permit)关联,每个线程都有一个permit,permit**有且只能有一个,调用unpark方法给指定的线程颁发许可证,重复调用unpark方法并不会累加许可证的个数**。当调用park方法时,如果有凭证,则消耗掉这个凭证然后正常退出,如果没有凭证就必须阻塞等待凭证可用
为什么可以突破wait/notify的原有调用顺序
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞,先发放了凭证后续就可以畅通无阻。就好比坐车拿身份证一样,如果你事先准备好了(unpark调用了)你就可以在关卡(park)处直接同行,如果没有,那你就在关卡(park)哪里等一下,找到了(unpark)在同行,一样的道理。
public static void parkAndUnPark() throws InterruptedException {
Thread t1=new Thread(()->{
//线程t1先暂停,让第二个线程先发放通行证
TimeUnit.SECONDS.sleep(2);
log.info("进入{}线程",Thread.currentThread().getName());
LockSupport.park();
log.info("线程{}被唤醒",Thread.currentThread().getName());
},"t1");
t1.start();
new Thread(()->{
log.info("进入{}线程,发起通知",Thread.currentThread().getName());
LockSupport.unpark(t1);
},"t2").start();
}
为什么唤醒两次后阻塞两次,但最终结果还是阻塞线程
因为凭证的数量最多就是1,连续两次调用unpark和调用一次unpark的效果是一样的,只会增加一个凭证,而调用两次park却需要消耗掉两张凭证
public static void continuity() throws InterruptedException {
Thread t1=new Thread(()->{
log.info("进入{}线程",Thread.currentThread().getName());
LockSupport.park();
LockSupport.park();
log.info("线程{}被唤醒",Thread.currentThread().getName());
},"t1");
t1.start();
TimeUnit.SECONDS.sleep(2);
new Thread(()->{
log.info("进入{}线程,发起通知",Thread.currentThread().getName());
LockSupport.unpark(t1);
LockSupport.unpark(t1);
},"t2").start();
}