【线程基础】wait/notify/sleep/join等重要用法以及注意事项

邓敏 2021年12月08日 90次浏览

使用方法

wait

方法作用
wait()将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程。
wait(long timeout)该方法与wait()方法类似,唯一的区别就是在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒。
wait(long timeout,long nanos)相对于wait(long timeout)来说,拥有更精确的控制调度时间。

notify

方法作用
notify()唤醒一个被wait挂起的线程
notifyAll()唤醒所有被wait挂起的线程

sleep

方法作用
sleep(long timeout)让当前线程暂停指定的时间(毫秒)

yield

方法作用
yield()暂停当前线程,以便其他线程有机会执行。将线程的Running状态转变为Runnable状态

join

方法作用
join()让父线程等待子线程执行完成后再执行
join(long millis)content2
join(long millis, int nanos)等待millis 毫秒终止线程,假如这段时间内该线程还没执行完,将取消等待。
join(long millis, int nanos)相对于join(long millis)来说,拥有更精确的控制调度时间。

注意事项

wait 必须在 synchronized 保护的同步代码中使用

首先我们先看看wait方法源码里面是怎么写的
wait method should always be used in a loop:

 synchronized (obj) {

     while (condition does not hold)

         obj.wait();

     ... // Perform action appropriate to condition

}

This method should only be called by a thread that is the owner of this object's monitor.

这里的意思是说,在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁,也就是通常所说的 synchronized 锁。

我可以先看一下下面这段代码

public class WaitsMain {

    private static String a = "";

    public static void main(String[] args) {
        Thread thread01 = new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" 抢到了锁");
                try {
                    System.out.println(Thread.currentThread().getName()+" 释放锁");
                    a.wait();
                    System.out.println(Thread.currentThread().getName()+" 执行结束了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

        });

        Thread thread02 = new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" 抢到了锁");
                a.notify();
                System.out.println(Thread.currentThread().getName()+" 唤醒了锁");
        });

        thread01.start();
        thread02.start();

    }
}

在不加锁的情况下,这段代码如果执行,会出现如下情况

Thread-0 抢到了锁
Thread-0 释放锁
Thread-1 抢到了锁
Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
	at java.lang.Object.notify(Native Method)
	at src.com.thread.waits.WaitsMain.lambda$main$1(WaitsMain.java:22)
	at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at src.com.thread.waits.WaitsMain.lambda$main$0(WaitsMain.java:12)
	at java.lang.Thread.run(Thread.java:748)

可以看到报错信息出现了IllegalMonitorStateException这个报错,这个报错会在三种情况下抛出:

  • 当前线程不含有当前对象的锁资源的时候,调用obj.wait()方法;
  • 当前线程不含有当前对象的锁资源的时候,调用obj.notify()方法。
  • 当前线程不含有当前对象的锁资源的时候,调用obj.notifyAll()方法。

很明显上段代码中就是因为Thread-1线程没有获取到当前对象锁资源然后去调用notify方法导致出现异常,所以如果在不加锁的情况下,贸然的去使用wait和notify可能会导致整个流程的操作没有在一个原子性的环境下去完成。接下来我们去改进一下上面这段代码

public class WaitsMain {

    private static String a = "";

    public static void main(String[] args) {
        Thread thread01 = new Thread(()->{

            synchronized (a){
                System.out.println(Thread.currentThread().getName()+" 抢到了锁");
                try {
                    System.out.println(Thread.currentThread().getName()+" 释放锁");
                    a.wait();
                    System.out.println(Thread.currentThread().getName()+" 执行结束了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread thread02 = new Thread(()->{
            synchronized (a){
                System.out.println(Thread.currentThread().getName()+" 抢到了锁");
                a.notify();
                System.out.println(Thread.currentThread().getName()+" 唤醒了锁");
            }
        });

        thread01.start();
        thread02.start();

    }
}

控制台输出:

Thread-0 抢到了锁
Thread-0 释放锁
Thread-1 抢到了锁
Thread-1 唤醒了锁
Thread-0 执行结束了

在我们改进后的代码之后,会发现加了锁的代码,wait方法在释放monitor锁的时候,就必须要先进入到synchronized内持有这把锁,这样才能够提高线程的安全性。

wait/notify 和 sleep 方法的异同

相同点

  • 他们都可以让线程阻塞
  • 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。

不同点

  • wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
  • 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
  • sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
  • wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中

  • 因为 Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。
  • 因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。

yield()注意事项

  • yield是一个静态的本地方法(native)
  • 调用yield后,yield告诉当前线程把运行机会交给线程池中有相同优先级的线程。
  • yield不能保证,当前线程迅速从运行状态切换到就绪状态。
  • yield只能是将当前线程从运行状态转换到就绪状态,而不能是等待或者阻塞状态。

join()的实现原理

join()方法实现是通过wait()。 当main线程调用t.join时候,main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(),直到该对象唤醒main线程 ,比如退出后。这就意味着main 线程调用t.join时,必须能够拿到线程t对象的锁。
详细可以看一下join的源码
image.png

join()和yield的区别

yield()做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

join()方法则是可以使得一个线程在另一个线程结束后再执行。如果join()方法在一个线程实例上调用,当前运行着的线程将阻塞直到这个线程实例完成了执行。