并发编程学习4—深入浅出线程状态

1. 为什么需要wait

在这里插入图片描述
在这里插入图片描述

Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态。
Waiting是获得了锁,条件不满足又放弃了锁,而blocked是在等待锁。

BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片。
BLOCKED 线程会在 Owner 线程释放锁时唤醒。
WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争。
【waitTest.java】
需要上锁的对象最好加 final,保证锁着的都是同一个对象,引用是没有变的。

2. API

  1. obj.wait() 让进入 object 监视器的线程到 waitSet 等待。
  2. obj.notify() 在 object 上正在 waitSet 等待的线程中随机挑一个唤醒。
  3. obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒。
  4. 它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法。

3. Sleep VS. Wait

  1. sleep 是 Thread 的静态方法,而 wait 是 Object 的方法。(API)
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用(要先上锁了,才能等待。)
  3. sleep 在睡眠的同时,如果加了锁不会释放对象锁的,虽然他也放弃了CPU的使用权,但别的线程还是只能在EntryList里阻塞着,但 wait 在等待的时候会释放对象锁, 它们状态 TIMED_WAITING。

3. Wait正确使用例子

  1. Step 1
    在这里插入图片描述
    在这里插入图片描述

其它干活的线程,都要一直阻塞,效率太低。
小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来。
加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加
synchronized 就好像 main 线程是翻窗户进来的。
解决方法,使用 wait - notify 机制。

  1. Step 2
    在这里插入图片描述

解决了其它干活的线程阻塞的问题。
但如果有其它线程也在等待条件呢? notify可能会错误的叫醒其他线程。
(注意:烟到了也要加syn,因为有notify(),想象成必须要进房间了才好去通知。)

  1. Step 3
    notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】,上面提出的问题。

解决方法,改为 notifyAll(4)

  1. Step 4
    用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了。

解决方法,用 while + wait,当条件不成立,再次 wait(5)

  1. 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 相比 △

  1. wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必。
  2. park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】。
  3. 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() 时:

  1. 竞争锁成功,t 线程从 WAITING --> RUNNABLE
  2. 竞争锁失败,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() 时:

  1. 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
  2. 竞争锁失败,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 它具备如下特点:

  1. 可中断。lock.lockInterruptibly();
  2. 可以设置超时时间。 【deadPhil.java】
  3. 默认不公平(强行插入,有机会在中间输出),可以设置为公平锁。
  4. 支持多个条件变量。

与 synchronized 一样的是都支持可重入。
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

7. 1 条件变量

synchronized 中也有条件变量,就是我们讲原理时那个waitSet 休息室,当条件不满足时进入 waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
synchronized 是那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

7.1.1 使用要点:

await 前需要获得锁。
await 执行后,会释放锁,进入 conditionObject 等待。
await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁。
竞争 lock 锁成功后,从 await 后继续执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值