Loading

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

使用方法

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()方法在一个线程实例上调用,当前运行着的线程将阻塞直到这个线程实例完成了执行。

posted @ 2022-01-24 19:40  邓小白  阅读(355)  评论(0编辑  收藏  举报