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 线程会无止境的等待下去:
通过 jconsole 工具也能发现,t1 线程始终处于 WAITING 状态!
如何让 wait 结束,那么只要满足上述所说的三个 wait 结束条件即可。
3. notify 方法
notify 的作用是唤醒等待的线程
-
notify 这个方法也要在同步代码块或同步方法中执行(被synchronized 修饰),notify 用于通知哪些可能等待该对象锁的其他线程,并使他们重新获取该对象的锁。
-
如果有多个线程等待该对象的锁, 则由线程调度器随机挑选出一个呈 wait 状态的线程,并不会采取先来后到的机制。
-
notify 方法后,当前线程不会马上释放该对象锁,要等到执行完 notify 所处被 synchronized 修饰的代码块执行结束,才能释放对象锁!
注意,通过指定对象调用 wait() 进入 WAITING 状态的线程,只有指定对象调用 notify 唤醒后(特殊情况除外),该线程才能尝试获取锁,接着往下执行!
notify 就好像一个妈妈(指定对象),妈妈手上拿着一块小蛋糕,有三个小朋友在桌子旁边坐着等(妈妈.wait),妈妈随机喊了一个小朋友,让他来吃蛋糕(妈妈.notify),但是妈妈并没有把蛋糕放下(没有结束对应代码块,也就是还未释放锁),当妈妈把蛋糕放在桌子上(锁被释放),这个小朋友才能去吃蛋糕(获取到锁)。
此时有了上述知识,我们就可以实现下上述图中吃蛋糕的场景了(为了代码简洁,我们只设定两个线程来等待被唤醒):
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();
}
}
可能大家多次测试上述代码后,发现一直都是张三吃到了蛋糕啊,但是其实这个是随机的,因为 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 对象锁,那究竟谁能获取到呢?这完全是随机的!比如下面的测试结果:
所以是有可能别半路截胡的,罪魁祸首还是因为随机调度,抢占式执行呀!所以以后在写多线程代码的时候一定要多多注意,要让每种执行顺序得到的结果都是一样的,这才是好的代码!
关于 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 状态。
如果对应对象 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 的静态方法文章来源:https://www.toymoban.com/news/detail-449096.html
下期预告:【多线程】单例模式文章来源地址https://www.toymoban.com/news/detail-449096.html
到了这里,关于面试官:为什么有了sleep还需要wait?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!