深入浅出Java多线程(四)之阻塞与唤醒

807 阅读4分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

初始wait()、notify()和notifyAll()

关于wait()、notify()、notifyAll()方法,虽然是对于线程的操作方法,但是却不属于Thread特有的方法,而是Object中的方法,也就是说在JDK中的每个类都拥有这三个方法,那么这三个方法到底有什么神奇之处可以使线程阻塞又可以唤醒线程呢?我们先来说说wait()方法。

下面是wait()的三个重载方法

public final void wait() throws InterruptedException

public final void wait(long timeout) throws InterruptedException

public final void wait(long timeout , int nanos) throws InterruptedException

  • wait()方法的这三个重载方法都将调用wait(long timeout)这个方法,上述所提到的wait() 方法等价于 wait(0),0代表着永不超时。
  • Object的wait(long timeout) 方法会导致当前线程进入阻塞状态,直到有其他的线程调用了Object的notify() 或者 notifyAll() 方法才能将其唤醒,或者阻塞时间到达了timeout时间而自动唤醒。
  • wait()方法必须拥有该对象的monitor(资源/锁),也就是wait()方法必须在同步方法(即Synchronized修饰的方法)中使用。
  • 当前线程执行了该对象的wait()方法之后,将会放弃对该monitor的所有权并且进入与该对象关联的wait set中,也就是说一旦线程执行了某个object的wait()方法之后,它就会释放对该对象monitor的所有权,其他线程也有机会继续争抢该monitor的所有权。

关于notify()

private final native void notify();

  • 唤醒单个正在执行的该对象wait()方法的线程
  • 如果有某个线程由于执行该对象的wait()方法而进入阻塞则会被唤醒,如果没有则忽略。
  • 被唤醒的线程需要重新获取对该对象所关联monitor的lock才能继续执行。

notify()和wait()的注意事项

  • wait()方法是可中断方法,这也就意味着,当前线程一旦调用了wait()方法进入阻塞状态,其他线程是可以使用interrupt方法将其打断的;可中断方法被打断后会受到InterruptedException,同时interrupt标识也会被擦除。
  • 线程执行了某个对象的wait()方法之后,会加入与之对应的wait set中,每一个对象的monitor都有一个与之关联的wait set。
  • 当线程进入 wait set之后,notify方法可以将其唤醒,也就是从wait set中弹出,同时中断wait中的线程也会将其唤醒。
  • 必须在同步(synchronized)方法中使用wait()和notify()方法,因为执行wait()和notify的前提条件是必须持有同步方法的monitor的所有权。
  • 同步代码的monitor必须与执行wait()、notify()方法的对象一致,简单来说就是用哪个对象的monitor必须同步,就只能用哪个对象进行wait()和notify()操作

实例

  1. new 一个Object 作为monitor
  2. Wait类实现run方法,在同步代码块内输出获取锁,并执行wait()方法,释放Object(monitor)
  3. Notify类获取到锁,输出抢到资源,并唤醒被阻塞的线程,继续执行当前线程直至结束(重点)
  4. Wait类被唤醒,继续执行后续输出
 private static Object object = new Object();

    static class Wait implements Runnable{

        @Override
        public void run() {
            synchronized (object){
                System.out.println("当前线程"+ Thread.currentThread().getName()+"获取到了锁");
                //释放
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("重新获取到了锁,线程名为"+Thread.currentThread().getName());
            }
        }
    }

    static class Notify implements Runnable{

        @Override
        public void run() {
            synchronized (object){
                System.out.println("有人释放了锁,我抢到了资源"+Thread.currentThread().getName());
                //唤醒
                object.notify();
                System.out.println("结束了");
            }
        }
    }


    public static void main(String[] args) {
        Thread thread1 = new Thread(new Wait());
        Thread thread2 = new Thread(new Notify());
        thread1.start();
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
    }

结果

当前线程Thread-0获取到了锁 有人释放了锁,我抢到了资源Thread-1 结束了 重新获取到了锁,线程名为Thread-0

wait和sleep

从表面上看,wait和sleep方法都可以使当前线程进入阻塞状态,但是两者之间存在着本质的区别。

  • wait和sleep都可以使线程进入阻塞状态
  • wait和sleep方法均是可中断方法,被中断后都会收到中断异常。
  • wait是Object的方法,sleep是Thred的特有方法
  • wait方法的执行必须在同步执行,而sleep不需要
  • 线程在执行sleep方法时,并不会释放monitor(锁),而wait会释放
  • sleep方法在设定时间休眠后会主动退出阻塞,而wait方法(没有指定时间,默认为0,永久阻塞)则需要被其他线程中断后才能退出阻塞。