1. 为什么需要wait
Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态。
(Waiting是获得了锁,条件不满足又放弃了锁,而blocked是在等待锁。)
BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片。
BLOCKED 线程会在 Owner 线程释放锁时唤醒。
WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争。
【waitTest.java】
需要上锁的对象最好加 final
,保证锁着的都是同一个对象,引用是没有变的。
2. API
- obj.wait() 让进入 object 监视器的线程到 waitSet 等待。
- obj.notify() 在 object 上正在 waitSet 等待的线程中随机挑一个唤醒。
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒。
- 它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法。
3. Sleep VS. Wait
- sleep 是 Thread 的静态方法,而 wait 是 Object 的方法。(API)
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用(要先上锁了,才能等待。)
- sleep 在睡眠的同时,如果加了锁不会释放对象锁的,虽然他也放弃了CPU的使用权,但别的线程还是只能在EntryList里阻塞着,但 wait 在等待的时候会释放对象锁, 它们状态 TIMED_WAITING。
3. Wait正确使用例子
- Step 1
其它干活的线程,都要一直阻塞,效率太低。
小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来。
加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加
synchronized 就好像 main 线程是翻窗户进来的。
解决方法,使用 wait - notify 机制。
- Step 2
解决了其它干活的线程阻塞的问题。
但如果有其它线程也在等待条件呢? notify可能会错误的叫醒其他线程。
(注意:烟到了也要加syn,因为有notify(),想象成必须要进房间了才好去通知。)
- Step 3
notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】,上面提出的问题。
解决方法,改为 notifyAll(4)
- Step 4
用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了。
解决方法,用 while + wait,当条件不成立,再次 wait(5)
- Step 5
![[uTools_1688371234799.png]]
虽然都被叫醒了,但小南从room.wait()
代码块被唤醒后,往后走,因为变成while了,还在循环里,判断不是送烟的,又去wait了,是送烟的,就往下运行。
3. 1 总结代码套路:
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
4. Park & Unpark
它们是 LockSupport 类中的方法
// 暂停当前线程, 线程进入wait状态
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
4.1 与 Object 的 wait & notify 相比 △
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必。
- park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】。
- park & unpark 可以先 unpark(4.2图三),而 wait & notify 不能先 notify。
4.2 Park & Unpark原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter
,_cond
和 _mutex
。
打个比喻:
线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。
_counter
就好比背包中的备用干粮(0 为耗尽,1 为充足)。
调用 park 就是要看需不需要停下来歇息:
如果备用干粮耗尽,那么钻进帐篷歇息。
如果备用干粮充足,那么不需停留,继续前进。
调用 unpark,就好比令干粮充足:
如果这时线程还在帐篷,就唤醒让他继续前进。
如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进。
因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮。
5. 总结线程方法和状态
5. 1 NEW --> RUNNABLE
当调用
t.start()
方法时,由 NEW --> RUNNABLE。
5. 2 RUNNABLE <–> WAITING
t 线程用
synchronized(obj)
获取了对象锁后,
调用obj.wait()
方法时,t 线程从 RUNNABLE --> WAITING。
调用 obj.notify() , obj.notifyAll() , t.interrupt() 时:
- 竞争锁成功,t 线程从 WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 WAITING --> BLOCKED
5. 3 RUNNABLE <–> WAITING
当前线程调用
t.join()
方法时,当前线程从 RUNNABLE --> WAITING,注意是当前线程在t 线程对象的监视器上等待。
t 线程运行结束,或调用了当前线程的interrupt()
时,当前线程从 WAITING --> RUNNABLE。
5. 4 RUNNABLE <–> WAITING
当前线程调用
LockSupport.park()
方法会让当前线程从 RUNNABLE --> WAITING
调用LockSupport.unpark(目标线程)
或调用了线程 的interrupt()
,会让目标线程从 WAITING --> RUNNABLE。
5. 5 RUNNABLE <–> TIMED_WAITING
t 线程用
synchronized(obj)
获取了对象锁后,
调用obj.wait(long n)
方法时,t 线程从 RUNNABLE --> TIMED_WAITING。
t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时:
- 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
5. 6 RUNNABLE <–> TIMED_WAITING
当前线程调用
t.join(long n)
方法时,当前线程从 RUNNABLE --> TIMED_WAITING,注意是当前线程在t 线程对象的监视器上等待。
当前线程等待时间超过了 n 毫秒,或t线程运行结束,或调用了当前线程的interrupt()
时,当前线程从 TIMED_WAITING --> RUNNABLE。
5. 7 RUNNABLE <–> TIMED_WAITING
当前线程调用
Thread.sleep(long n)
,当前线程从 RUNNABLE --> TIMED_WAITING。
当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE。
5. 8 RUNNABLE <–> TIMED_WAITING
当前线程调用
LockSupport.parkNanos(long nanos)
或LockSupport.parkUntil(long millis)
时,当前线程从 RUNNABLE --> TIMED_WAITING。
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING–> RUNNABLE。
5. 9 RUNNABLE <–> BLOCKED
t 线程用
synchronized(obj)
获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED。
持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED。
5. 10 RUNNABLE <–> TERMINATED
当前线程所有代码运行完毕,进入 TERMINATED
6. 多把锁
将锁的粒度细分:
好处,是可以增强并发度。
坏处,如果一个线程需要同时获得多把锁,就容易发生死锁。
6.1 死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁。
比如:
t1 线程 获得 A对象锁,接下来想获取 B对象 的锁 t2 线程 获得 B对象锁,接下来想获取 A对象的锁。
检测死锁:
可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁。
避免死锁要注意加锁顺序。
另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查。
哲学家问题
【deadPhil.java】
6.2 活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
7. ReentrantLock
独占锁,相对于 synchronized 它具备如下特点:
- 可中断。
lock.lockInterruptibly();
- 可以设置超时时间。 【deadPhil.java】
- 默认不公平(强行插入,有机会在中间输出),可以设置为公平锁。
- 支持多个条件变量。
与 synchronized 一样的是都支持可重入。
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
7. 1 条件变量
synchronized 中也有条件变量,就是我们讲原理时那个waitSet 休息室,当条件不满足时进入 waitSet 等待。
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
synchronized 是那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒。
7.1.1 使用要点:
await 前需要获得锁。
await 执行后,会释放锁,进入 conditionObject 等待。
await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁。
竞争 lock 锁成功后,从 await 后继续执行。