线程饿死
一个或多个线程因为无法获得执行所需的资源(如CPU时间、锁、或其他同步控制)而被长时间阻塞或延迟执行的情况。尽管这些线程可能处于可执行状态并且已经准备好运行,但由于资源分配的不均衡或调度策略的问题,它们无法获得执行的机会。
例子
你去 ATM 机取钱,在你后面还排了很对人。你进去后,门锁上了,但你发现 ATM 机里面没钱了,于是你出去了。
按理来说,你出来之后,应该轮到排在你后面的人进去了,可你刚把锁打开,前脚刚迈出去,却心想“里面会不会又有钱了?”,于是你又进去了,把门又锁上了,可你进去之后发现还是没钱。
于是你就这样反反复复、进进出出、开锁上锁。虽然你的行为没有造成任何死锁,但你后面的人却做不了任何事(其他线程无法执行任何逻辑),这就叫“线程饿死
”
- 这属于一个概率性问题,和调度器具体的策略直接相关
- 针对上述问题,同样可以使用
wait / notify
来解决
- 让你在拿到锁的时候进行判定,判定当前是否执行“取钱”操作,如果能执行,就正常执行;如果不能执行,就需要主动释放锁,并且“阻塞等待”(通过调用
wait
),此时这个线程就不会在后续参与锁的竞争了- 一直阻塞到“取钱”的条件具备了,此时再由其他线程通过通知机制(
notify
)唤醒这个线程
wait
因为线程在操作系统上的调度是随机的,而我们不喜欢随机,喜欢确定,所以增加了一些手段来让调度变得确定
- 多个线程,需要控制线程之间执行某个逻辑的先后顺序,就可以让后执行的逻辑使用
wait
,先执行的线程完成某些逻辑后,通过notify
唤醒对应的wait
- 另外通过
wait
和notify
也是为了解决“线程饿死”问题
当我们尝试使用 wait:
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println("wait 之前");
obj.wait();
System.out.println("wait 之后");
}
}
运行后,报错了
Monitor
:此处指的是 synchronized 这里的锁- 合起来为:非法的锁状态异常(加锁状态/未加锁状态)
- 之所以会出现这种情况,是因为
wait
中会进行一个操作,就是针对 obj 对象,先进行解锁。所以,使用wait
的时候,务必要放到synchronized
代码块里面(必须得先加上锁,才能谈“解锁”)- 你进入
ATM
机后,发现没钱,就要“阻塞等待”,此时一定是你先解锁,再开门出去。就是先释放锁,再等待。如果你抱着锁等待,就也没把几回让给别人,因为别人也无法进去
- 并且释放锁和加上锁这两个操作是通过
wait
同时来进行(打包成原子),若不是同时执行,那就可能发生线程切换- 比如在释放锁之后,插进来了一个通知正在等待代的线程继续执行操作的线程,可是前面那个才刚刚释放锁,还没开始进行执行等待的操作,最终这个线程由于错过了通知,将持续等待下去
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println("wait 之前");
synchronized(obj){
obj.wait();
}
System.out.println("wait 之后");
}
}
//打印结果为:wait 之前
- 因为代码阻塞在中间了,所以后面的逻辑就无法完成,此时线程的状态变成了
WAITING
(没有超时时间的等待,有超时时间的事TIMED_WAITING
)- 并且由于代码中没有 notify,所以 wait 将一直持续等待下去
综上
wait
使调用的线程进入阻塞wait
做三件事- 释放锁
- 进入阻塞状态,准备接受通知
- 收到通知后,唤醒,并且重新尝试获取锁
注意
wait
默认是“死等”,但它还提供了一个带参数的版本,指定超时时间。若wait
达到了最大时间,还没等到notify
,就不会继续等待了,而是继续执行wait(1000)
和sleep(1000)
还是有本质区别的- 使用
wait
的本质目的是为了提前唤醒,而sleep
就是固定时间的阻塞,不涉及唤醒(虽然sleep
可以被Interrupt
唤醒,但这是一个终止线程的操作,而不是唤醒) wait
必须要搭配synchronized
使用,并且wait
会先释放锁,同时进行等待sleep
和锁无关,如果不加锁,sleep
可以正常使用;如果加了锁,sleep
操作不会释放锁,会“抱着锁”,一起睡,其他线程无法拿到锁
- 使用
notify唤醒wait的操作
public class Demo5 {
private static Object locker = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (locker){
System.out.println("t1 wait之前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} System.out.println("t1 wait之后");
}
});
Thread t2 = new Thread(() -> {
System.out.println("t2 notify之前");
Scanner scanner = new Scanner(System.in);
scanner.next(); //此处是通过这个next构造一个“阻塞”的状态
synchronized (locker) {
locker.notify();
}
System.out.println("t2 notify之后");
});
t1.start();
t2.start();
}
}
//运行结果:
t1 wait之前
t2 notify之前
9
t2 notify之后
t1 wait之后
- 要保证加锁的对象和调用
wait
的对象是一样的,如果不是同一个对象,那么无法使用,因为锁的状态是不对的 - 要确保调用
wait
和notify
的对象是一样的才能唤醒 wait
和notify
调用前都要加上锁- 因为在多线程中,一个线程加锁,一个不加,是无意义的,不会有任何的阻塞效果。此处希望
t2
执行notify
的时候,t1
是未持有锁的状态,如果 t1 正持有锁,肯定就不是在wait
,这个时候去notify
也没有意义。所以要确保notify
拿到锁,再去notify
唤醒wait
- 因为在多线程中,一个线程加锁,一个不加,是无意义的,不会有任何的阻塞效果。此处希望
- 一定要确保持有锁才能谈释放
-
- 假设是多个现场,如果多个线程都在同一个对象上
wait
,notify
只会随机唤醒其中一个
- 假设是多个现场,如果多个线程都在同一个对象上
notifyAll
和 notify
相对,还有一个 notifyAll
将上面的notify换成notifyAll之后
运行结果为:
---
t1 wait之前
t3 wait之前
t2 wait之前
9
t4 notifyAll之前
t4 notifyAll之后
t1 wait之后
t2 wait之后
t3 wait之后
-
大部分情况下,都是使用
notify
。若要唤醒多个,就一个一个地唤醒,整个程序执行过程是比较有序的,如果一下全唤醒,这些被唤醒的线程就会无序的竞争锁(会很混乱,可能带来未知的风险) -
notify
和notifyAll
通知的时候,如果没有线程在wait
,不会有任何副作用
练习
创建三个线程,使用 wait
和 notify
控制先后打印 A、B、C 三个字母
public class Demo6 {
private static Object locker = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("A");
//Thread.sleep(1000);
synchronized (locker) {
locker.notify(); //这个notify用来唤醒t2中的wait,因为是locker锁
}
});
Thread t2 = new Thread(() -> {
synchronized (locker) {
try {
locker.wait(); //被t1的notify唤醒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("B");
synchronized (locker2){
locker2.notify(); //这个notify用来唤醒t3中的wait,因为是locker2锁
}
}
});
Thread t3 = new Thread(() -> {
synchronized (locker2) {
try {
locker2.wait(); //被t2的notify唤醒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("C");
});
t1.start();
t2.start();
t3.start();
}
}
- 这个代码中,若是先执行
t2
的wait
,后执行t1
的notify
,代码逻辑就一切顺利(大概率就是这样,因为t1
的打印需要不少时间) - 但存在这样的可能:
t1
限制性了打印和notify
,然后t2
才执行wait
,意味着通知来早了,t2
错过了通知,t2
的wait
就无人唤醒了 - 为了解决这样的情况,我们只需要在
t1
里面加一个sleep
就可以了 (只要在锁的前面就行),让t1
线程等一会,其他线程先执行