Java之sleep和wait

Java之sleep和wait

sleep和wait方法都是native关键字修饰的方法,这说明这两个方法是原生函数,也就是由C/C++实现的,那么我们就暂时不关心它的具体实现了。

sleep方法是Thread类中的方法,而wait方法是Object中的方法,那么我们首先看看wait方法。

Object#wait()

从Object源码中,我们可以发现,wait有三个重载方法,分别是无参的wait方法,带有long和int类型参数的的wait方法,以及带有long类型参数的方法。其实前两个方法最终都是调用了wait(long)方法,而wait(long)方法是native修饰的方法,它底层就是由C++实现的,这里暂且不讨论,我们来看看这个方法的注释。

image-20210512110048274

/*
wait方法会导致当前线程等待,直到其他线程调用notify和notifyAll方法,或者达到了指定的等待时间
使用wait方法的前提是当前线程拥有该对象的监视器也就是锁

该方法会导致当前线程T(调用wait方法的线程)将自己放到该对象的等待集合中,然后会放弃此对象上的所有同步声明,也就是会放弃对象的锁
该等待线程不会被调度并且处于休眠状态,直到以下四种情况之一发生:
1、其他线程调用等待对象notify方法,并且当前线程T被随机选为要唤醒的方法时,线程将会退出休眠状态
2、其他线程调用等待对象的notifyAll方法
3、其他线程中断当前线程T
4、超过指定的等待时间。如果等待时间为0的话,时间因素将不会被考虑,那线程将等待直到被通知唤醒
当发生上述四种情况时,线程T将会从该对象的等待集合中移除,并且可以重新被调度。然后它以通常的方式与其他线程竞争对象上的同步锁,一旦它获得了对对象的控制权,它对对象的所有同步声明将会恢复到原来的状态,也就是说,恢复到调用wait方法时的状态。然后线程T将会从调用wait方法的方法中返回。因此,从wait方法返回时,对象和线程T的同步状态与调用wait方法时的状态完全相同。

如果当前线程在等待之前或者等待时被中断,会抛出InterruptedException异常。

注意,等待方法将当前线程防止到该对象的等待集合时,只解锁此对象;在线程等待时,当前线程同步的其他任何对象都将保持锁定状态。

wait方法仅能被持有对象监视器的线程调用(对象监视器就相当于对象的锁)
通过如下方法可以获得对象的监视器:
1、通过执行该对象的同步方法(也就是synchronized关键字修饰的方法)
2、通过执行该对象的同步代码块(synchronized(Object) {})
3、通过执行类的同步静态代码块(也就是synchronized关键字修饰的静态方法)
*/
public final native void wait(long timeout) throws InterruptedException;

通过wait方法的注释,我们可以发现,wait方法有如下作用:

  • 使线程进入休眠状态,不被调度,直到被notify方法选中或者notifyAll方法的执行,才会被唤醒
  • 线程会释放调用wait方法的对象的锁(但是不会释放线程持有的其他对象的锁),这样其他线程可以竞争该对象的锁
  • 从wait方法中退出后,线程会回到调用该方法时的状态

既然要notify和notifyAll方法才能唤醒调用wait方法陷入等待的线程,那么我们看看这两个方法的注释:

/*
唤醒一个等待对象锁的线程
如果有多个线程在等待该对象,会随机唤醒一个线程

在当前线程放弃该对象的锁之前,唤醒的线程将无法继续执行
唤醒的线程将以通常的方式与其他线程竞争,这些线程会公平地在这个对象上进行同步竞争。例如,被唤醒的线程在竞争对象的锁时没有特权或者缺点

该方法仅在线程持有对象的监视器时才能被调用,获取对象的监视器有如下方法:也就是synchronized关键字修饰的方法、静态方法以及代码块等
*/
public final native void notify();

/*
唤醒所有等待该对象监视器的线程
在当前持有对象锁的线程放弃对象锁之前,被唤醒的线程无法执行
*/
public final native void notifyAll();

通过方法的注释来看,这两个方法就是用于唤醒等待该对象的线程,notify随机唤醒一个线程,notifyAll会唤醒全部线程,这些被唤醒的线程处于就绪态,它们会和正在运行的线程一起抢占对象的锁,得到对象的锁之后才能继续执行。

通过JDK源码的注释,我们对wait和notify方法有了更进一步的了解,那么接着看看sleep方法,才能知道wait和sleep的区别

Thread#sleep()

Thread类中的sleep方法也有两个重载方法,其中sleep(long)是底层的实现。

image-20210512141821809

来看看它们的注释:

/*
根据系统计时器和调度程序的精度和准确性,使当前执行的线程休眠(暂时停止执行)指定的毫秒数
线程不会释放持有的锁
该方法响应中断,当遇到中断时会抛出InterruptedException异常,并且会清除当前线程的中断状态
*/
public static native void sleep(long millis) throws InterruptedException;
/*
使当前执行的线程休眠(临时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统计时器和调度程序的精度和准确性
线程不会释放持有的锁
nanos的范围:0-999999
*/
public static void sleep(long millis, int nanos)
    throws InterruptedException {
    	// 判断millis和nanos是否符合条件
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
		// 调用原生sleep方法
        sleep(millis);
    }

wait()和sleep()方法区别

通过源码的注释我们可以发现wait()方法和sleep()方法的一些区别:

  • 最容易发现的就是wait方法属于Object类,sleep方法属于Thread类
  • wait方法被调用后会释放持有的对象上的锁,而sleep方法不会释放对应的锁
  • wait方法和sleep方法都响应中断,但是两者响应中断之后的操作略有不同,wait方法响应中断抛出异常之后不会清除线程的中断状态,而sleep方法响应中断抛出异常之后会清除线程的中断状态
  • wait、notify、notifyAll方法只能在同步方法或者同步代码块中使用(wait和notify方法必须在synchronized修饰的方法或代码块中调用,如果在非同步的代码块中调用,会产生IllegalMonitorStateException异常),而sleep在任何地方使用
  • sleep方法必须捕获异常,而wait方法不需要捕获异常
  • 调用wait方法后,必须调用notify或者notifyAll方法唤醒等待的线程,如果线程持有对象的锁,那么会释放该对象的锁,但是不会释放持有的其他锁
  • 一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。

Thread#yield()和Thread#join()方法

yield方法就是当前线程主动让处CPU,让其他同等优先级的线程获得执行机会。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

/*
对调度程序的提示,表示当前线程愿意放弃当前对处理器的使用,调度程序可以忽略该提示。
yield是一种启发式尝试,旨在改善线程之间的执行方式,避免某个线程过度使用CPU
它的使用应该与详细的分析和基准测试相结合,以确保它实际具有预期的效果。
使用这种方法很少合适。它可能对调试或测试有用,因为它可能有助于再现由于竞争条件而产生的bug。
*/
public static native void yield();

从源码的注释来看,yield方法在我们的实际开发中,使用的场景可能不是很多,因为即使使用了也可能无法达到想要的效果,所以,我们需要谨慎使用

接下来看看join方法,join方法用于在某一个线程执行过程中调用另一个线程执行,等到被调用的线程执行结束后,会继续执行当前线程,也就是等待某个线程执行结束后会再执行当前线程

那么这是怎么实现的呢?我们可以发现,在调用join方法之后,join方法会判断当前线程是否存活,如果存活的话,会调用wait(0)方法,让线程一直等待。当线程结束的时候,会调用notifyAll方法,让等待的线程被唤醒,等待被调度。这是在JVM层面实现,JVM中在线程执行结束exit后,会调用notifyAll方法,这也是为什么注释中建议应用程序不要在线程实例上使用wait、notify和notifyAll方法

/*
等待此线程死亡的时间最多为毫秒,0意味着永远等待
此实现使用以this.isAlive()为条件的this.wait的while循环,当线程终止时,会调用notifyAll方法
建议应用程序不要在线程实例上使用wait、notify和notifyAll方法

该方法会响应中断,并且将在抛出异常后会清除线程的中断状态
*/
public final synchronized void join(long millis)
throws InterruptedException {
    // 获取系统当前时间戳
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        // isAlive判断当前线程是否还存活,如果存活的话就调用wait方法
        while (isAlive()) {
            wait(0);
        }
    } else {
        // 如果当前线程还处于存活状态
        while (isAlive()) {
            // 计算实际的等待时间,调用wait方法
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

总结

通过jdk源码的注释,我们可以学习到好多东西!

  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值