文章目录
- 1.wait 和 notify
- 1.1 wait()方法
- 1.2 notify()方法
- 1.3 notifyAll()方法
1.wait 和 notify
线程最大的问题是抢占式指向,随机调度。而写代码的时候,确定的东西会比较好。
于是就有程序猿发明了一些办法,来控制线程之间的执行顺序。
虽然线程在内核里的调度是随机的,但是可以通过一些 API 然线程主动阻塞,主动放弃 CPU。(给别的线程让路)
比如,t1 t2 两个线程,希望 t1 先干活,干的差不多的时候,再让 t2 来干。
就可以让 t2 wait 。(阻塞,主动放弃 CPU)
上面的场景,使用 join 和 sleep 无法做到。
使用 jion 则必须要 t1 彻底执行完毕,t2 才可以运行。
如果是希望 t1 先干50%的活,就让 t2 开始行动,join 无能为力。
使用 sleep 指定一个休眠时间,但是 t1 执行的这些活,到底花了多少的时间,不好估计。
于是就可以使用 wait 和 notify。
wait、notify、notifyAll 这几个类,都是,Object类中的方法。
1.1 wait()方法
wait 的作用是进行阻塞。
当某个线程调用 wait 方法时,就会进入阻塞(无论是通过哪个对象 wait 的)。
此时的状态就处在 WAITING
先来看一段代码。
package thread;
public class ThreadDemo18 {
public static void main(String[] args) throws InterruptedException{
Object object = new Object();
object.wait();
}
}
}
throws InterruptedException
有很多带阻塞功能的方法都有这个异常。
这些方法都是可以被 interrupt 方法通过这个异常唤醒。
wait 不加任何参数就会一直等待,直到有其他的线程唤醒它。
上面的代码会抛一个异常。
这是一个非法的锁状态异常。
锁的状态:被加锁的状态和解锁的状态。
wait 的操作分为:
- 先释放锁。
- 进行阻塞等待
- 收到通知之后,重新尝试获取锁,并且在获取锁之后继续往下执行。
这里的锁的状态异常,就是没加锁,就要要释放锁了。
解决办法就是搭配 synchronized 来使用。
package thread;
public class ThreadDemo18 {
public static void main(String[] args) throws InterruptedException{
Object object = new Object();
synchronized (object) {
System.out.println("wait之前");
object.wait();
System.out.println("wait之后");
}
}
}
虽然这里的 wait 是阻塞在 synchronized 代码块里了,
但是实际上,这里的阻塞是释放了锁的。
此时其他线程是可以获取到 object 这个对象的锁的。
此时这里的阻塞就处于 WAITING
1.2 notify()方法
notify 方法是唤醒等待的线程。
来看一段代码
package thread;
public class ThreadDemo19 {
public static void main(String[] args) {
Object object = new Object();
Thread t1 = new Thread(() -> {
//线程1负责进行等待
System.out.println("t1 wait之前");
try {
synchronized (object) {
object.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 wait之后");
});
Thread t2 = new Thread(() -> {
//线程2负责唤醒
System.out.println("t2 notify之前");
synchronized (object) {
object.notify();
}
System.out.println("t2 notify之后");
});
t1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
此处,先执行了 wait 很明显 操作阻塞了,没有看到 wait 之后的打印。
接下来执行到了 t2 ,t2 进行了 notify 的时候,才会把 t1 的 wait 唤醒,t1 才能继续执行。
此处的通知和 wait 配对。
如果 wait 使用的对象和 notify 使用的对象不同。
此时的 notify 不会有任何效果。(notify 只能唤醒在同一个对象上等待的线程)
如果的代码这里写作:t1.start 和 t2.start
由于线程不确定性,此时不能保证是先执行 wait,后执行 notify。
如果调用 notify 此时没有 wait 。此处的 wait 是无法被唤醒的。
因此此处的代码要求保证先执行 wait 后执行 notify,这样才是有意的。
wait 和 sleep 的区别
wait 的带有时间的版本看起来就和 sleep 有点像。
但是其实还是有区别的,虽然都是可以指定等待的时间,也都能被提前唤醒,
(wait 是被 nottify 唤醒,sleep 是被 interrupt 唤醒)
但是这里表示的含义截然不同。
- notify 唤醒 wait 是不会有任何异常的。(正常的业务逻辑)
- interr 唤醒 sleep 则是会出现异常。(表示一个出问题的逻辑)
下面看一道练习:
有三个线程,输出字母 ABC,控制三个线程分别按照 ABC 的顺序打印出来。
package thread;
public class ThreadDemo20 {
//有三个线程,控制三个线程分别按照 ABC 的顺序打印出来
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println('A');
});
Thread t2 = new Thread(() -> {
System.out.println('B');
});
Thread t3 = new Thread(() -> {
System.out.println('C');
});
t1.start();
t2.start();
t3.start();
}
}
因为它的结果是随机调度的因此不能确定先输出哪个,结果可能是 ABC,也可能是ACB,或者是其他的结果。
我们需要通过 wait 和 notify 告知 线程1 在打印完A之后通知线程2可以打印B了
要保证线程2要阻塞到A打印结束,才会开始打印B。
线程3也是同理,直到按照顺序打印结束是会出现 ABC。
下面是优化过的版本。
package thread;
public class ThreadDemo20 {
//有三个线程,控制三个线程分别按照 ABC 的顺序打印出来
public static void main(String[] args) throws InterruptedException{
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
System.out.println('A');
synchronized (locker1) {
//告知线程2B可以打印了
locker1.notify();
}
});
Thread t2 = new Thread(() -> {
//等待线程1打印完A再打印B
synchronized (locker1) {
try {
locker1.wait();//等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println('B');
synchronized (locker2) {
//告知线程3C可以打印了
locker2.notify();
}
});
Thread t3 = new Thread(() -> {
synchronized (locker2) {
try {
locker2.wait();//等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println('C');
});
t1.start();
t2.start();
t3.start();
}
}
通过 wait 和 notify 来实现三个线程之间相互等待和通知的作用。
c此时就是预期好的顺序了。
但是这样还是会有一些小问题:
如果是先执行线程2的 notify 后执行线程2的 wait ,此时程序就会僵住
解决办法就是 保证线程1的启动速度要慢于线程2和线程3
关键代码如下:
t2.start();
t3.start();
Thread.sleep(1000);
t1.start();
调整完线程1的顺序后,再加一个睡眠时间 1000 ms。
1.3 notifyAll()方法
notify 只能唤醒某一个等待线程,而 notifyAll 可以唤醒所有的等待线程。
注意:
虽然是同时唤醒多个线程, 但是这多个线程都需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行。
这是 notify 只能唤醒一个。
这是 notifyAll 可以换线多个线程,但是多个线程需要竞争一把锁。