线程等待唤醒的几种实现方式和对比

文章详细介绍了Java中实现线程等待唤醒的三种方法:Object的wait和notify,Condition的await和signal,以及LockSupport的park和unpark。对比了它们的使用场景和注意事项,特别强调了LockSupport的灵活性和对调用顺序的不敏感性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程等待唤醒的三种实现方式

使用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();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值