1. 线程饥饿
线程饥饿是指一个或多个线程因长期无法获取所需资源(如锁,CPU时间等)而持续处于等待状态,导致其任务无法推进的现象。
典型场景
-
优先级抢占:
-
在支持线程优先级的系统中,高优先级线程可能持续抢占CPU资源
-
导致低优先级线程长期无法获得CPU时间片
-
-
不公平锁竞争:
-
某些线程频繁获取锁,其他线程长期等待
-
典型场景:某个线程释放锁后立即重新竞争并获得锁
-
导致其他线程始终处于BLOCKED状态而无法执行
-
-
资源分配不均:
-
某些线程占用大量I/O或内存资源
-
其他线程因资源不足而无法执行
-
-
线程池配置不当:
-
固定大小线程池中,长任务占用所有线程
-
短任务无法得到执行机会
-
关键问题点
-
锁获取模式问题:
-
活跃线程释放锁后立即重新请求锁
-
处于RUNNABLE状态的线程比BLOCKED状态的线程有更快的响应速度
-
操作系统唤醒BLOCKED线程需要上下文切换,造成竞争劣势
-
-
系统调度机制:
-
默认调度策略可能不利于公平性
-
缺乏有效的防饥饿机制
-
-
线程状态转换开销:
-
BLOCKED→RUNNABLE状态转换需要系统介入
-
这种转换比保持RUNNABLE状态有更高的延迟
-
2. wait 和 notify
wait/notify 的本质作用
- 应用层协作工具:
wait()
和notify()
是 Java 提供的应用层线程协作机制,用于控制线程对共享资源的访问顺序,而非直接干预操作系统的线程调度策略。 - 不改变调度规则:操作系统内核仍按自身调度算法(如轮转法、优先级调度)决定线程何时获得 CPU 时间,
wait/notify
无法强制指定某个线程优先执行。
1. wait()⽅法
wait(); 内部做的三件事:
1. 立即释放锁,无需等待同步块结束
2. 线程状态变化:
RUNNING
→WAITING
3. 线程被唤醒后需重新获取锁,获取成功后从
wait()
调用处继续执行。WAITING
→BLOCKED
(被唤醒后重新竞争锁)→RUNNING
。
线程状态变化后,其他线程就有机会获取锁。
wait() 方法的三种重载形式
方法 | 说明 |
---|---|
wait() | 使当前线程无限期等待,直到另一个线程调用 notify() 或 notifyAll() 方法 |
wait(long timeout) | 指定一个超时时间,线程将在超时后自动被唤醒。线程也可以在超时前被 notify() 或 notifyAll() 方法唤醒。 |
wait(long timeout, int nanos) | 提供更高精度的超时设置,总超时时间(以纳秒为单位)计算为 1_000_000*timeout + nanos |
在 Java 中,调用 wait
方法的对象必须和锁对象一致,这是因为 wait
方法的行为是基于对象的监视器(锁)来实现的。以下是具体解释:
- 原理:当一个线程调用某个对象的
wait
方法时,该线程会释放它所持有的该对象的锁,并进入等待状态,直到其他线程调用同一个对象的notify
或notifyAll
方法来唤醒它。如果调用wait
方法的对象与获取锁的对象不一致,那么线程在等待时就无法正确地与该锁关联,也就无法按照预期被唤醒,并且可能会导致程序出现逻辑错误。
2. notify()⽅法
方法 | 说明 |
---|---|
notify() | 唤醒等待该对象监视器的一个随机线程。选择唤醒哪个线程是非确定性的,取决于“随机调度”算法 |
notifyAll() | 唤醒所有等待该对象监视器的线程。被唤醒的线程会和其他试图获取该对象锁的线程一起竞争锁 |
调用
notify()
或notifyAll()
的对象必须与调用wait()
的对象相同,并且它们必须与synchronized
使用的锁对象一致,否则会抛出IllegalMonitorStateException
。
wait()
、notify()
、notifyAll()
必须由同一个对象调用。必须在
synchronized
块中使用,并且锁对象必须与调用wait()
/notify()
的对象一致。每个 Java 对象都有一个监视器(monitor),也可以理解为锁。当一个线程进入
synchronized
代码块时,它会获取该代码块所关联对象的锁。wait
方法会让当前线程释放这个锁,并进入等待状态,直到其他线程调用同一个对象的notify
或notifyAll
方法来唤醒它。而notify
和notifyAll
方法也需要在获取相同对象的锁之后才能调用,这样它们才能准确地唤醒在该对象上等待的线程。如果这三个对象不一致,就会破坏这种线程同步机制,导致程序出现不可预测的结果,例如线程无法被唤醒、死锁等问题。
wait()
、notify()
和notifyAll()
方法必须在synchronized
修饰的代码块或方法中调用,否则会抛出IllegalMonitorStateException
。1. 锁与等待队列的绑定
每个 Java 对象都有两个核心属性:
- 监视器锁(Monitor):用于实现同步。
- 等待队列(Wait Set):用于存储调用
wait()
的线程。
wait()
和notify()
的操作对象是对象的等待队列,而等待队列的状态由锁来保护。因此,必须先获取锁才能操作等待队列。2. 原子性与可见性保障
public class SynchronizedDomo8 {public static void main(String[] args) throws InterruptedException {Object object = new Object();// 作为同步锁和 wait/notify 的监视器对象Thread t1 = new Thread(()->{synchronized (object){// 获取 object 的锁System.out.println("t1 线程之前");// ①try {//必须使用同一个对象调用object.wait();// ② 释放锁,进入WAITING状态} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t1 线程之后");// ⑦ 被唤醒后重新获取锁,继续执行}});Thread t2 = new Thread(()->{try {Thread.sleep(2000);// ③ 休眠 2 秒,确保 t1 先执行并进入 wait()} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (object){// 获取 object 的锁System.out.println("t2 线程之前");// ④//必须使用同一个对象调用object.notify(); // ⑤ 唤醒t1,但t2仍持有锁System.out.println("t2 线程之后");// ⑥ 同步块结束后释放锁}});t1.start();t2.start();}
}
执行步骤:
-
t1
进入synchronized
块,获取object
的锁。 -
打印
"t1 线程之前"
。 -
调用
object.wait()
:-
释放
object
的锁,t1
进入等待状态。
-
-
t2
先休眠 2 秒,确保t1
先执行并进入wait()
状态。 -
t2
进入synchronized
块,获取object
的锁。 -
打印
"t2 线程之前"
。 -
调用
object.notify()
:-
唤醒
t1(WAITING -> BLOCKED)
,但t1
不会立即执行,因为t2
仍持有锁。
-
-
打印
"t2 线程之后"
,退出synchronized
块,释放锁。 -
t1
重新获取锁,继续执行"t1 线程之后"
。
3. wait()、join()、sleep()方法的区别
方法 | sleep() | join() | wait() |
---|---|---|---|
所属类 | Thread类 | Thread类 | Object类 |
释放锁 | ❌ | ❌ | ✅ |
唤醒条件 | 时间到期 | 目标线程结束或超时 | notify()/notifyAll()或超时 |
使用限制 | 可以直接调用 | 可以直接调用 | 必须在同步块中使用 |
抛出InterruptedException | ✅ | ✅ | ✅ |
精度控制 | 毫秒(实际精度依赖操作系统) | 毫秒+纳秒 | 毫秒+纳秒 |
线程状态变化 | RUNNING → WAITING | RUNNING → TIMED_WAITING | RUNNING → WAITING/TIMED_WAITING |