面试官:为什么有了sleep还需要wait?

面试官:为什么有了sleep还需要wait?_第1张图片

1. 能不能调整线程先后顺序?

对于线程执行最大的问题就是随机调度,抢占式执行,对于程序猿来讲,是不喜欢这种随机性的,程序猿喜欢确定的东西,于是就有了一些方法,可以控制线程之间的执行顺序,虽然线程在内核里调度是随机的,但我们可以通过一些 api 让线程主动阻塞等待,主动放弃 CPU 给其他线程让路呀!

就比如说,在地铁上,张三看到一位老人上地铁了,主动让座,老人坐了一会,起身对小伙说,我还有一站就到了,你来坐着吧,我站一会就下车了。

这是不是就像线程1正在占用 CPU 资源了,突然线程2开始工作了,于是线程1就让线程2先去工作,等线程2工作差不多了,在通知线程1可以工作了。

在实际开发中,很多时候线程之间是需要相互配合的。

比如篮球哥喜欢打篮球,篮球里,一个队伍五个人,小前锋,大前锋,中锋,后卫,分位,这 5 个角色就像 5 个线程,如果这 5 个人都争这一个球,那这个的队伍就没有配合性,必定会输球。

如果这 5 个人打好配合,先谁持球,然后接着执行什么战术,有合理的战术安排,此时球就能很好的在这 5 个人的手里运作起来,进球的概率也就大大提升。

再比如,球员a 先持球过半场,传球给球员b,球员b接球就投,球进了!

此时是不是就需要 a 先拿球过半场啊,等 a 过了半场,在传球给 b ,在 a 没有传球之前 b 是不能拿到球的!

也就是线程1没有执行到一定阶段,线程2是不能工作的!

对于完成上述的配合操作,主要涉及到三个方法:

  • wait() / wait(long timeout)

  • notify / notiryAll()

此处的方法都是 Object 类中的方法,Object 类是所有类的父类,所以所有对象都有上述方法。

后续的内容也是围绕上述方法进行展开。


2. wait 方法

当某个对象调用 wait 方法时,wait 会做如下三件事:

  • wait 使当前执行代码的线程进行等待(把线程放到阻塞队列中)

  • 释放当前的锁

  • 满足一定条件时被唤醒,重新尝试获取这个锁

由于 wait 执行时会释放当前的锁,所以调用 wait 的时候需要先获取到锁,即 wait 需要搭配 synchronized 使用。

wait 的结束条件(满足一个即可):

  • 其他线程调用该对象的 notify 方法

  • wait 超时等待(wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间)

  • 其他线程调用该线程的 interrupted 方法,导致 wait 抛出 InterruptedException 异常

public static void main(String[] args) {
    Object object = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (object) {
            System.out.println("开始等待!!!");
            try {
                object.wait();
                System.out.println("等待结束!!!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t1.start();
}

显然上述这个代码是一个 "死等",因为没有触发上述 wait 结束条件的任意一个,所以 t1 线程会无止境的等待下去:

面试官:为什么有了sleep还需要wait?_第2张图片

通过 jconsole 工具也能发现,t1 线程始终处于 WAITING 状态!

如何让 wait 结束,那么只要满足上述所说的三个 wait 结束条件即可。


3. notify 方法

notify 的作用是唤醒等待的线程

  • notify 这个方法也要在同步代码块或同步方法中执行(被synchronized 修饰),notify 用于通知哪些可能等待该对象锁的其他线程,并使他们重新获取该对象的锁。

  • 如果有多个线程等待该对象的锁, 则由线程调度器随机挑选出一个呈 wait 状态的线程,并不会采取先来后到的机制。

  • notify 方法后,当前线程不会马上释放该对象锁,要等到执行完 notify 所处被 synchronized 修饰的代码块执行结束,才能释放对象锁!

注意,通过指定对象调用 wait() 进入 WAITING 状态的线程,只有指定对象调用 notify 唤醒后(特殊情况除外),该线程才能尝试获取锁,接着往下执行!

notify 就好像一个妈妈(指定对象),妈妈手上拿着一块小蛋糕,有三个小朋友在桌子旁边坐着等(妈妈.wait),妈妈随机喊了一个小朋友,让他来吃蛋糕(妈妈.notify),但是妈妈并没有把蛋糕放下(没有结束对应代码块,也就是还未释放锁),当妈妈把蛋糕放在桌子上(锁被释放),这个小朋友才能去吃蛋糕(获取到锁)。

面试官:为什么有了sleep还需要wait?_第3张图片

此时有了上述知识,我们就可以实现下上述图中吃蛋糕的场景了(为了代码简洁,我们只设定两个线程来等待被唤醒):

public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (object) {
            System.out.println("张三进入 WAITING 状态");
            try {
                object.wait();
                System.out.println("张三吃到蛋糕了!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    Thread t2 = new Thread(() -> {
        synchronized (object) {
            System.out.println("李四进入 WAITING 状态");
            try {
                object.wait();
                System.out.println("李四吃到蛋糕了!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    t1.start();
    t2.start();
    Thread.sleep(10); // 保证两个线程都进入到 WAITING 状态
    synchronized (object) {
        object.notify();
    }
}
面试官:为什么有了sleep还需要wait?_第4张图片

可能大家多次测试上述代码后,发现一直都是张三吃到了蛋糕啊,但是其实这个是随机的,因为 CPU 就是随机调度的,这个咱们就没必要钻牛角尖了,实在要钻,可以创建线程池(后续讲),搞一堆线程进行测试即可。

此时问题来了,当释放锁了之后,也就是妈妈把蛋糕放在桌子上了,此时被唤醒的线程是可以去拿蛋糕的,但是有没有可能释放锁的瞬间,被其他处在 RUNNABLE 状态的线程给劫持了呢?(其他线程也来竞争这把锁) 也就是突然冲进来了一条小狗,把蛋糕给抢到了,其实是有这种情况的:

public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (object) {
            System.out.println("张三进入 WAITING 状态");
            try {
                object.wait();
                System.out.println("张三吃到蛋糕了!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    Thread t2 = new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (object) {
            System.out.println("小狗把蛋糕抢走了!");
            while (true) {} // 吃蛋糕
        }
    });

    t1.start();
    t2.start();
    Thread.sleep(1000); // 保证两个线程都进入到 WAITING 状态
    synchronized (object) {
        object.notify();
        Thread.sleep(1000); // 唤醒 t1 但并没有立即释放锁, 休眠 1s 再释放
    }
}

上述代码 main 线程等待 1000 毫秒后唤醒 t1 线程,此时 t1 被唤醒,就会重新尝试获取 object 对象锁,但是 t2 休眠了 1000 毫秒后,也想获取 object 对象锁。

唤醒 t1 之后,过了 1000 毫秒,也就意味着锁被释放,此时 t1 和 t2 都想获取到 object 对象锁,那究竟谁能获取到呢?这完全是随机的!比如下面的测试结果:

面试官:为什么有了sleep还需要wait?_第5张图片

所以是有可能别半路截胡的,罪魁祸首还是因为随机调度,抢占式执行呀!所以以后在写多线程代码的时候一定要多多注意,要让每种执行顺序得到的结果都是一样的,这才是好的代码!

关于 notifyAll :

notifyAll 和 notify 非常相似,假设 5 个线程等待 object 对象唤醒,然后 object.notifyAll(),就会将这 5 个线程全部唤醒,然后这 5 个线程竞争 object 对象锁,没竞争到的,就继续进入 WAITING 状态。


4. 使用 wait 和 notify 注意点

一定要弄清楚是谁在等被谁唤醒!

如果 t1 里面调用 o1.wait(),那么只有其他线程调用了 o1.notify() 才能唤醒 t1,如果是其他线程调用 o2.notify(),是不能唤醒 t1 的!因为 t1 线程是在等 o1 唤醒!

而 o1 也只能唤醒在等他的线程,比如 t3 在等 student 唤醒,那调用 o1.notify() 是不能唤醒 t3 的,只能调用 studnet.notify() 才能唤醒 t3。

归根到底,我们一定要弄清楚,线程在等谁,也要弄清楚,这个对象,有哪些线程在等他的唤醒!

这里举两个例子来演示一下:

public static void main(String[] args) throws InterruptedException {
    Object o1 = new Object();
    Object o2 = new Object();
    Thread t = new Thread(() -> {
        synchronized (o1) {
            try {
                o1.wait();
                System.out.println("t 线程被 main 线程唤醒!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
    Thread.sleep(10); // 保证 t 线程进入 WAITING 状态
    synchronized (o2) {
        o2.notify();
        System.out.println("执行完 o2.notify!");
    }
}

这段代码,t 线程在等待 o1 对象唤醒,所以 main 线程中 o2.notify() 是在唤醒等待 o2 的线程,显然没有线程在等待 o2 唤醒,所以空打一枪,然而 t 线程仍然处在 WAITING 状态。

面试官:为什么有了sleep还需要wait?_第6张图片

如果对应对象 notify 的时候,没有线程在等待这个对象唤醒呢?那么就是无效唤醒,也没有什么副作用,所以我们以后写代码的时候还是要尽量保证先执行 wait 在执行 notify 才是有意义的,也就是在 notify 的时候,有线程在等待这个对象唤醒!


5. wait 带参数和 sleep 的区别

wait 的带参数版本,指定了最大的等待时间,看起来和 sleep 有点像,但是还是有本质区别的。

  • notify 唤醒 wait 的时候,是不会有任何异常的(正常的业务逻辑)

  • interrupt 唤醒 sleep 的时候,则是会抛出一个异常(表示逻辑出现了问题)

其实从理论上,wait 和 sleep 是没得比的,wait 是线程之间的通信,互相配合,而 sleep 是单纯让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃 CPU 的调度一段时间而已。

  • wait 是需要搭配 synchronized 使用的,sleep 则不需要

  • wait 是 Object 的方法,而 sleep 是 Thread 的静态方法


下期预告:【多线程】单例模式

你可能感兴趣的:(多线程从入门到精通(暂时限免),jvm,wait,notify)