0
点赞
收藏
分享

微信扫一扫

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)


📒博客首页:热爱编程的大李子 📒

🌞文章目的:共享对象之管程 🌞

🍄参考视频:​​深入学习Java并发编程​​🍄

🙏博主在学习阶段,如若发现问题,请告知,非常感谢🙏

💙同时也非常感谢各位小伙伴们的支持💙

🌈每日一语:未经一番寒彻骨,哪得梅花扑鼻香!🌈

💗感谢: 我只是站在巨人们的肩膀上整理本篇文章,感谢走在前路的大佬们💗

🌟最后,祝大家每天进步亿点点! 欢迎大家点赞👍➕收藏⭐️➕评论💬支持博主🤞!🌟

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_并发编程

文章目录

  • ​​3.8 Monitor 概念​​
  • ​​3.8.1 Java 对象头​​
  • ​​3.8.2 原理之 Monitor(锁)​​
  • ​​3.8.3 原理之 synchronized​​
  • ​​3.8.4 小故事​​
  • ​​3.8.5 原理之 synchronized 进阶​​
  • ​​3.9 wait notify​​
  • ​​3.9.1 **小故事 - 为什么需要 wait**​​
  • ​​3.9.2 原理之 wait / notify​​
  • ​​3.9.3 API 介绍​​
  • ​​3.9.4 API源码查看​​
  • ​​3.10 wait notify 的正确姿势​​
  • ​​3.10.0 **sleep(long n) 和 wait(long n) 的区别**​​
  • ​​3.10.1 问题描述​​
  • ​​3.10.2 step 1​​
  • ​​3.10.3 step 2​​
  • ​​3.10.4 step 3-4​​
  • ​​3.10.5 step 5​​
  • ​​3.10.6 使用wait和notify的正确姿势总结​​
  • ​​3.11 Park & Unpark​​
  • ​​3.11.1 基本使用​​
  • ​​3.11.2 特点​​
  • ​​**3.11.3 原理之 park & unpark**​​
  • ​​3.12 重新理解线程状态转换​​
  • ​​**情况 1: `NEW --> RUNNABLE`**​​
  • ​​**情况 2:` RUNNABLE <--> WAITING`**​​
  • ​​情况 3: `RUNNABLE <--> WAITING`​​
  • ​​情况 4 `RUNNABLE <--> WAITING`​​
  • ​​情况 5 `RUNNABLE <--> TIMED_WAITING`​​
  • ​​情况 6 `RUNNABLE <--> TIMED_WAITING`​​
  • ​​情况 7 RUNNABLE <--> TIMED_WAITING​​
  • ​​情况 8 `RUNNABLE <--> TIMED_WAITING`​​
  • ​​情况 9 `RUNNABLE <--> BLOCKED`​​
  • ​​情况 10 `RUNNABLE <--> TERMINATED`​​
  • ​​3.13 多把锁​​
  • ​​3.14 活跃性​​
  • ​​3.14.1 死锁​​
  • ​​3.14.2 定位死锁​​
  • ​​3.14.3 哲学家就餐问题​​
  • ​​3.14.4 活锁​​
  • ​​3.14.5 饥饿​​
  • ​​3.15 ReentrantLock​​
  • ​​3.15.1 可重入​​
  • ​​3.15.2 可打断​​
  • ​​3.15.3 锁超时介绍以及应用​​
  • ​​3.15.4 公平锁​​
  • ​​3.15.5 条件变量​​
  • ​​3.15.5 同步模式之顺序控制​​
  • ​​本章小结​​

3.8 Monitor 概念

3.8.1 Java 对象头

以 32 位虚拟机为例

普通对象

|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|

  • klass word:指向类型元数据的指针。比如 student类型的对象,指向Student类型

数组对象

|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|

其中 Mark Word 结构为

|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|

  • hashcode:每个对象都有一个hashcode值
  • age:垃圾回收时的分代年龄
  • biased_lock:是否偏向锁
  • 加锁状态
  • ptr_to_heavyweight_monitor:指向操作系统中monitor的指针
  • ptr_to_lock_record:锁记录地址

以Integer对象为例,占 ​​8 + 4​​ 个字节(对象头 + value),基本类型int占 4个字节

3.8.2 原理之 Monitor(锁)

3.8.3 原理之 synchronized

3.8.4 小故事

  • 故事角色
  • 老王 - JVM
  • 小南 - 线程
  • 小女 - 线程
  • 房间 - 对象
  • 房间门上 - 防盗锁 - Monitor
  • 房间门上 - 小南书包 - 轻量级锁
  • 房间门上 - 刻上小南大名 - 偏向锁
  • 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
  • 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向

小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁(Monitor),当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。— 重量级锁

但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?— 挂书包(轻量级锁

小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式(存在竞争时,锁升级轻量级 --> 重量级)。

后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。

于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,(偏向锁)下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。(存在竞争时,锁升级偏向锁 --> 轻量级锁

同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王(JVM)觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字(批量重偏向)

后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包(不可偏向)

3.8.5 原理之 synchronized 进阶

3.9 wait notify

3.9.1 小故事 - 为什么需要 wait

  • 由于条件不满足,小南不能继续进行计算
  • 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
  • 《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_并发编程_02

  • 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,其它人可以由老王随机安排进屋
  • 直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)
  • 《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_对象锁_03

  • 小南于是可以离开休息室,重新进入竞争锁的队列
  • 《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_Java_04

3.9.2 原理之 wait / notify

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_对象锁_05

Owner 线程获取了锁,但是发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态

WAITING线程和BLOCKED线程的区别?

  • WAITING:已经获得了锁,但是条件不满足,释放锁后进入WaitSet队列;
  • BLOCKED:没有获得锁,进入EntryList中等待,状态是BLOCKED

==相同点:==都处于阻塞状态,不占用CPU时间片,将来调度时候也不会考虑。

WAITING线程和BLOCKED线程的唤醒条件?

  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒

3.9.3 API 介绍

方法名称

描述

​obj.wait() ​

让进入 object 监视器的线程到 waitSet 等待

​obj.notify()​

在 object 上正在 waitSet 等待的线程中挑一个唤醒

​obj.notifyAll()​

让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法

  • wait:先成为Owner,才有资格进入WaitSet等待。
  • Notify:必须成为Owner,才能叫醒隔壁的WaitSet中的线程。

**案例一:**当没有获得锁,就使用wait/notify/notifyAll方法时,报​​IllegalMonitorStateException​

@Slf4j(topic = "c.Test18")
public class Test18 {
static final Object lock = new Object();
public static void main(String[] args) {

// synchronized (lock) {
try {
lock.wait();//IllegalMonitorStateException
} catch (InterruptedException e) {
e.printStackTrace();
}
// }
}
}

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_Java_06

**案例二:**演示Notify和NotifyAll区别

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object obj = new Object();

public static void main(String[] args) {

new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();

new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();

// 主线程两秒后执行
sleep(2);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
// obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}

notify 的一种结果

15:38:17.518 c.TestWaitNotify [t1] - 执行....
15:38:17.520 c.TestWaitNotify [t2] - 执行....
15:38:19.519 c.TestWaitNotify [main] - 唤醒 obj 上其它线程
15:38:19.519 c.TestWaitNotify [t1] - 其它代码....

notifyAll 的结果

15:37:19.908 c.TestWaitNotify [t1] - 执行....
15:37:19.910 c.TestWaitNotify [t2] - 执行....
15:37:21.908 c.TestWaitNotify [main] - 唤醒 obj 上其它线程
15:37:21.908 c.TestWaitNotify [t2] - 其它代码....
15:37:21.908 c.TestWaitNotify [t1] - 其它代码....

3.9.4 API源码查看

wait() 和wait(long n) 的区别

  • wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止
  • wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_Java_07

public final native void notify();

public final native void notifyAll();

public final void wait() throws InterruptedException {
wait(0);
}

public final native void wait(long timeout) throws InterruptedException; //本地方法

public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
//可以看出wait(long timeout, int nanos)的nacos参数是无用的
if (nanos > 0) {
timeout++;
}

wait(timeout);//依旧调用的是wait(long timeout)
}

3.10 wait notify 的正确姿势

3.10.0 sleep(long n) 和 wait(long n) 的区别

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  4. 它们状态 都是​​TIMED_WAITING ​​(相同点)
  5. 当不满足条件等待,最好使用wait.因为sleep不会释放锁。sleep一般是让CPU休息…:

/**
* 演示sleep和wait睡眠后,是否释放锁
*/
@Slf4j(topic = "c.Test19")
public class Test19 {

static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock) {
log.debug("获得锁");
try {
// Thread.sleep(20000);
lock.wait(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();

Sleeper.sleep(1);
synchronized (lock) {
log.debug("获得锁");
}
}
}

3.10.1 问题描述

小南只有叼有烟的时候才干活,其他人只要有时间片就干活,用代码模拟这个过程

3.10.2 step 1

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep1 {
static final Object room = new Object();//建议:引用用final修饰,保证锁的对象就不可变。
static boolean hasCigarette = false; // 有没有烟
static boolean hasTakeout = false;

public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
sleep(2);
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();

for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}

sleep(1);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
//synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
// }
}, "送烟的").start();
}

}

输出1:

17:17:31.927 c.TestCorrectPosture [小南] - 有烟没?[false]
17:17:31.935 c.TestCorrectPosture [小南] - 没烟,先歇会!
17:17:32.929 c.TestCorrectPosture [送烟的] - 烟到了噢!
17:17:33.940 c.TestCorrectPosture [小南] - 有烟没?[true]
17:17:33.940 c.TestCorrectPosture [小南] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] - 可以开始干活了
17:17:33.940 c.TestCorrectPosture [其它人] -

输出2:(当加上​​synchronized (room)​​)

17:25:44.996 c.TestCorrectPosture [小南] - 有烟没?[false]
17:25:45.004 c.TestCorrectPosture [小南] - 没烟,先歇会!
17:25:47.007 c.TestCorrectPosture [小南] - 有烟没?[false]
17:25:47.007 c.TestCorrectPosture [送烟的] - 烟到了噢!
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
17:25:47.007 c.TestCorrectPosture [其它人] - 可以开始干活了
17:25:47.007 c.TestCorrectPosture [其它人] -

分析:

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

3.10.3 step 2

使用wait-notify改进上面代码

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep2 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();

for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}

sleep(1);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
room.notify();
}
}, "送烟的").start();
}

}

输出:

17:42:24.719 c.TestCorrectPosture [小南] - 有烟没?[false]
17:42:24.727 c.TestCorrectPosture [小南] - 没烟,先歇会!
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:24.727 c.TestCorrectPosture [其它人] - 可以开始干活了
17:42:25.719 c.TestCorrectPosture [送烟的] - 烟到了噢!
17:42:25.719 c.TestCorrectPosture [小南] - 有烟没?[true]
17:42:25.719 c.TestCorrectPosture [小南] -

分析:

  • 此改进可以让其他线程同时运行,不会占用锁,并发效率大大提升
  • 思考:如果有其他线程也在等待,那么会不会错误的叫醒了其他线程?

3.10.4 step 3-4

加入另外一个线程 小女线程时,当外卖送到,小女可开始工作。思考并分析运行结果

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep3 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

// 虚假唤醒
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();

new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();

sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notify();
// room.notifyAll();
}
}, "送外卖的").start();


}

}

输出1:(使用notify

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_sed_08

  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线
    程,称之为【虚假唤醒
  • 解决方法,改为 notifyAll

输出2:(使用notifyAll

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_并发编程_09

  • 用 notifyAll 仅解决某个线程的唤醒问题,但使用​​if + wait ​​​判断仅有一次机会,一旦条件不成立,就没有重新
    判断的机会了(比如小南被错误唤醒后,就不能重新判断了)
  • 解决方法,用 while + wait,当条件不成立,再次 wait

3.10.5 step 5

将 ​if ​​改为 ​​while​​ ,防止虚假唤醒

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep5 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) {


new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();

new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();

sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();

}

}

输出:

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_sed_10

**结论:**此方法满足需求。即提高了并发效率问题,又不会出现虚假唤醒问题。

思考:while中使用wait()相比wait(long num)会不会浪费CPU呢?

只有线程被唤醒才会接着while,不唤醒就是wait,所以不会有cpu空转

3.10.6 使用wait和notify的正确姿势总结

synchronized(lock) {
while(条件不成立) {//while防止虚假唤醒
lock.wait();
}
// 干活
}

//另一个线程
synchronized(lock) {
lock.notifyAll();//notifyAll唤醒所有等待序列中国的线程
}

3.11 Park & Unpark

3.11.1 基本使用

它们是 LockSupport 类中的方法

// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

先 park 再 unpark

@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
Sleeper.sleep(1);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");

t1.start();
Sleeper.sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_Java_11

先 unpark 再 park:交换main和t1线程sleep的时间

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_Java_12

3.11.2 特点

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor(锁) 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify (先notify的代码会被忽略)

3.11.3 原理之 park & unpark

3.12 重新理解线程状态转换

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_并发编程_13

  • NEW:初始状态;创建了Java线程对象,还没有与操作系统的线程相关联。

情况 1: ​​NEW --> RUNNABLE​

  • 当调用​​ t.start()​​​ 方法时,由​​NEW --> RUNNABLE​

情况 2:​​ RUNNABLE <--> WAITING​

t 线程用 ​​synchronized(obj)​​ 获取了对象锁后

  • 调用​​obj.wait()​​​ 方法时,t 线程从​​RUNNABLE --> WAITING​
  • 调用​​obj.notify()​​​ ,​​obj.notifyAll() ​​​,​​ t.interrupt()​​ 时
  • 竞争锁成功,t 线程从​​WAITTING --> RUNNABLE​
  • 竞争锁失败,t 线程依旧是​​WAITTING --> BLOCKED​

**注意:**第二步中,调用 ​​obj.notify()​​​ , ​​obj.notifyAll() ​​​,​​ t.interrupt()​​​ 但未释放锁,t 线程会先进入EntryList 等待竞争锁,此时为​​BLOCKED​​状态。持锁线程释放锁后 EntryList中的线程会进行竞争。然后 根据竞争结果,t 线程会处于不同的状态。此过程可在后续Debug分析中清晰看到…

测试代码

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object obj = new Object();

public static void main(String[] args) {

new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();

new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();

// 主线程两秒后执行
sleep(2);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
// obj.notify(); // 唤醒obj上一个线程
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}

Debug分析:

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_sed_14

情况 3: ​​RUNNABLE <--> WAITING​

  • 当前线程调用​​t.join()​​​ 方法时,当前线程从​​RUNNABLE --> WAITING​
  • 注意是当前线程t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的​​interrupt()​​ 时,当前线程从​​ WAITING --> RUNNABLE​

情况 4 ​​RUNNABLE <--> WAITING​

  • 当前线程调用​​LockSupport.park()​​​ 方法会让当前线程从​​RUNNABLE --> WAITING​
  • 调用​​LockSupport.unpark(目标线程)​​​ 或调用了线程 的​​interrupt() ​​​,会让目标线程从​​WAITING --> RUNNABLE ​

情况 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​

情况 6 ​​RUNNABLE <--> TIMED_WAITING​

  • 当前线程调用​​t.join(long n)​​ 方法时,当前线程从​​RUNNABLE --> TIMED_WAITING​
  • 注意是当前线程在t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的​​interrupt() ​​时,当前线程
    ​​​TIMED_WAITING --> RUNNABLE​

情况 7 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用​​Thread.sleep(long n)​​​ ,当前线程从​​ RUNNABLE --> TIMED_WAITING​
  • 当前线程等待时间超过了 n 毫秒,当前线程从​​ TIMED_WAITING --> RUNNABLE​

情况 8 ​​RUNNABLE <--> TIMED_WAITING​

  • 当前线程调用​​LockSupport.parkNanos(long nanos)​​​ 或​​LockSupport.parkUntil(long millis) ​​时,当前线
    从​​RUNNABLE --> TIMED_WAITING​
  • 调用​​LockSupport.unpark(目标线程) ​​​或调用了线程 的​​interrupt()​​​,或是等待超时,会让目标线程从
    ​​​TIMED_WAITING--> RUNNABLE​

情况 9 ​​RUNNABLE <--> BLOCKED​

  • t 线程用​​synchronized(obj)​​​ 获取了对象锁时如果竞争失败,从​​ RUNNABLE --> BLOCKED​
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有​​BLOCKED​​ 的线程重新竞争,如果其中t 线程竞争
    成功,从​​​BLOCKED --> RUNNABLE​​​,其它失败的线程仍然​​BLOCKED​

情况 10 ​​RUNNABLE <--> TERMINATED​

当前线程所有代码运行完毕,进入​​TERMINATED​

3.13 多把锁

多把不相干的锁

一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低

解决方法是准备多个房间(多个对象锁)

例如

public class TestMultiLock {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.study();
},"小南").start();
new Thread(() -> {
bigRoom.sleep();
},"小女").start();
}
}

@Slf4j(topic = "c.BigRoom")
class BigRoom {

public void sleep() {
synchronized (this) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}

public void study() {
synchronized (this) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}

}

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_Java_15

改进:

@Slf4j(topic = "c.BigRoom")
class BigRoom {
//使用多把锁,相当于把房间分成了两块,允许两个没有关联的动作同时进行
private final Object studyRoom = new Object();

private final Object bedRoom = new Object();

public void sleep() {
synchronized (bedRoom) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}

public void study() {
synchronized (studyRoom) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}

}

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_并发编程_16

将锁的粒度细分

  • 好处,是可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

3.14 活跃性

3.14.1 死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
​​​t1 线程 ​​​获得​​A对象​​​锁,接下来想获取 ​​B对象​​​的锁 。​​t2 线程 ​​​获得 ​​B对象 ​​​锁,接下来想获取 ​​A对象​​的锁。

代码演示:

@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
test1();
}

private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");

Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}

结果:

15:13:19.339 c.TestDeadLock [t2] - lock B
15:13:19.339 c.TestDeadLock [t1] - lock A

3.14.2 定位死锁

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:

  • 使用jps+jstack定位死锁

F:\自学课程\底层课程\Java并发编程\concurrent>jps //查看正在运行的Java进程
11188 Launcher
17828 Jps
15192 TestDeadLock
19404
4684 RemoteMavenServer36

F:\自学课程\底层课程\Java并发编程\concurrent>jstack 15192//查看进程的详细信息

Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.192-b12 mixed mode):

"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000002e13800 nid=0x428c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
//以下是t2线程的详细信息
"t2" #12 prio=5 os_prio=0 tid=0x000000001ed35800 nid=0x290 waiting for monitor entry [0x000000001f37f000]
java.lang.Thread.State: BLOCKED (on object monitor)//阻塞状态
at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$1(TestDeadLock.java:32)
- waiting to lock <0x000000076ec5d160> (a java.lang.Object)//正在等待的锁...
- locked <0x000000076ec5d170> (a java.lang.Object)//已经获得的锁
at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$2/1792845110.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
//以下是t1线程的详细信息
"t1" #11 prio=5 os_prio=0 tid=0x000000001ed35000 nid=0xcd4 waiting for monitor entry [0x000000001f27f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$0(TestDeadLock.java:21)
- waiting to lock <0x000000076ec5d170> (a java.lang.Object)
- locked <0x000000076ec5d160> (a java.lang.Object)
at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$1/897913732.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)

//省略部分输出

Found one Java-level deadlock: //发现一个Java级别的死锁
=============================
"t2":
waiting to lock monitor 0x0000000002f0c0c8 (object 0x000000076ec5d160, a java.lang.Object),
which is held by "t1"
"t1":
waiting to lock monitor 0x0000000002f0b9e8 (object 0x000000076ec5d170, a java.lang.Object),
which is held by "t2"

Java stack information for the threads listed above:
===================================================
"t2"://出现死锁的行号信息
at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$1(TestDeadLock.java:32)
- waiting to lock <0x000000076ec5d160> (a java.lang.Object)
- locked <0x000000076ec5d170> (a java.lang.Object)
at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$2/1792845110.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1":
at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$0(TestDeadLock.java:21)
- waiting to lock <0x000000076ec5d170> (a java.lang.Object)
- locked <0x000000076ec5d160> (a java.lang.Object)
at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$1/897913732.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

  • 使用Jconsole工具

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_死锁_17

  • 避免死锁要注意加锁顺序
  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待。对于这种情况 linux 下可以通过 jps+jstack方式进行排查。

3.14.3 哲学家就餐问题

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_并发编程_18

有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
  • 如果筷子被身边的人拿着,自己就得等待

//就餐测试类
public class TestDeadLock {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c1, c5).start();
}
}

//哲学家类
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;

public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);//线程名称
this.left = left;
this.right = right;
}

@Override
public void run() {
while (true) {
// 尝试获得左手筷子
synchronized (left) {
// 尝试获得右手筷子
synchronized (right) {
eat();
}
}
}
}

Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}

//筷子类
class Chopstick {
String name;

public Chopstick(String name) {
this.name = name;
}

@Override
public String toString() {
return "筷子{" + name + '}';
}
}

执行不多会,就执行不下去了

12:33:15.575 [苏格拉底] c.Philosopher - eating...
12:33:15.575 [亚里士多德] c.Philosopher - eating...
12:33:16.580 [阿基米德] c.Philosopher - eating...
12:33:17.580 [阿基米德] c.Philosopher - eating...
// 卡在这里, 不向下运行

使用 jconsole 检测死锁,发现

-------------------------------------------------------------------------
名称: 阿基米德
状态: cn.itcast.Chopstick@1540e19d (筷子1) 上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 2, 总等待数: 1

堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@6d6f6e28 (筷子5)
-------------------------------------------------------------------------
名称: 苏格拉底
状态: cn.itcast.Chopstick@677327b6 (筷子2) 上的BLOCKED, 拥有者: 柏拉图
总阻止数: 2, 总等待数: 1

堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@1540e19d (筷子1)
-------------------------------------------------------------------------
名称: 柏拉图
状态: cn.itcast.Chopstick@14ae5a5 (筷子3) 上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0

堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@677327b6 (筷子2)
-------------------------------------------------------------------------
名称: 亚里士多德
状态: cn.itcast.Chopstick@7f31245a (筷子4) 上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 1, 总等待数: 1

堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@14ae5a5 (筷子3)
-------------------------------------------------------------------------
名称: 赫拉克利特
状态: cn.itcast.Chopstick@6d6f6e28 (筷子5) 上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0

堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@7f31245a (筷子4)

这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况

3.14.4 活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如

@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();

public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}

**解决办法:**让两个线程的执行时间交错,具体方案是通过增加随机睡眠时间

3.14.5 饥饿

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题。

下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_Java_19

顺序加锁的解决方案(线程1,2获取锁的顺序是相同的)

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_Java_20

案例演示:

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_Java_21

3.15 ReentrantLock

相对于 ​​synchronized​​ 它具备如下特点

  • 可中断(使用其他线程或者方法取消锁)
  • 可以设置超时时间(一段时间内未获取到锁,放弃去争抢锁,执行一些其他逻辑操作)
  • 可以设置为公平锁(先进先出,防止出现锁饥饿现象)
  • 支持多个条件变量(允许有多个WaitSet,不满足条件1时,去waitSet1中等待,不满足2时,去waitSet2中等待。当然唤醒时,也可以根据条件进行唤醒)

这里的中断是指,别的线程可以破坏你的blocking状态,而不是指自己中断阻塞状态

**相同点:**与 synchronized 一样,都支持可重入(同一线程对对象可以重复加锁)

基本语法

// 获取锁
reentrantLock.lock();

try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}

3.15.1 可重入

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

@Slf4j(topic = "c.Test17")
public class Test17 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}

public static void method2() {
lock.lock();
try {
log.debug("execute method2");
method3();
} finally {
lock.unlock();
}
}

public static void method3() {
lock.lock();
try {
log.debug("execute method3");
} finally {
lock.unlock();
}
}
}

输出:

15:33:20.791 c.Test17 [main] - execute method1
15:33:20.794 c.Test17 [main] - execute method2
15:33:20.794 c.Test17 [main] -

3.15.2 可打断

/**
* @author lxy
* @version 1.0
* @Description ReentrantLock可打断特性示例
* @date 2022/7/3 17:29
* 使用lock.lockInterruptibly()的优点:可以防止死锁的产生,避免长时间的等待。
*/
@Slf4j(topic = "c.Test18")
public class Test18 {
private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
//如果没有竞争那么此方法就会获取lock对象锁
//如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 方法
log.debug("尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("没有获得锁,返回");
return;
}

try {
log.debug("获取到锁");
} finally {//为啥俩try不能被合并:因为被打断的话,不能unlock,只有获取锁了才能继续往下走,往下走又必须来一个finally来保证锁一定被释放
lock.unlock();//释放锁
}
}, "t1");
//1.当只有一个t1线程时
//t1.start();
//2.当有t1和main线程时
// lock.lock();
// t1.start();
//3.当t1被interrupt打断时
// lock.lock();
// t1.start();
// Sleeper.sleep(2);
// log.debug("打断t1");
// t1.interrupt();
}
}

输出:

  • 当只有一个t1线程时

17:55:18.341 c.Test18 [t1] - 尝试获取锁
17:55:18.343 c.Test18 [t1] -

  • 当有t1和main线程时

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_死锁_22

  • 当t1被interrupt打断时

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_sed_23

  • 如果将上锁代码替换成​​lock.lock();​

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_死锁_24

==可打断锁的意义:==可以防止死锁的产生,避免长时间的等待。

3.15.3 锁超时介绍以及应用

可打断和锁超时的区别:可打断是线程t1调用interrupt方法进行打断阻塞状态,是被动的;而锁超时是超过一定时间就放弃获得,是主动的。

  • 使用方式一:​​tryLock​

@Slf4j(topic = "c.Test19")
public class Test19 {
private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("尝试获得锁");
if (!lock.tryLock()) {
log.debug("获取不到锁");
return;
}
try {
log.debug("获得到锁");
} finally {
lock.unlock();
}
}, "t1");

lock.lock();
log.debug("获得到锁");
t1.start();
}
}

输出:

19:27:15.757 c.Test19 [main] - 获得到锁
19:27:15.759 c.Test19 [t1] - 尝试获得锁
19:27:15.759 c.Test19 [t1] -

  • 使用方式2:使用​​trylock(long timeout,TimeUnit unit)​​ 且超时
  • 《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_并发编程_25

输出:超时后放弃获取锁

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_Java_26

  • 使用方式三:使用​​trylock(long timeout,TimeUnit unit)​​ 且未超时

@Slf4j(topic = "c.Test19")
public class Test19 {
private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("尝试获得锁");
try {
if(!lock.tryLock(2, TimeUnit.SECONDS)) {
log.debug("获取不到锁");
return;
}
}catch (InterruptedException e){
e.printStackTrace();
}

try {
log.debug("获得到锁");
} finally {
lock.unlock();
}
}, "t1");

lock.lock();
log.debug("获得到锁");
t1.start();
Sleeper.sleep(1);
lock.unlock();
log.debug("释放了锁");
}
}

输出:

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_对象锁_27

锁超时的应用-解决哲学家就餐问题

//测试死锁
public class TestDeadLock {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}

//哲学家类
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;

public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);//线程名称
this.left = left;
this.right = right;
}

@Override
public void run() {
while (true) {
// 尝试获得左手筷子
if(left.tryLock()){
try {
// 尝试获得右手筷子
if(right.tryLock()){
try {
eat();
}finally {//为了保证获取右手锁后可以成功释放,所以需要加一个 try...catch块
right.unlock();
}
}
}finally {//为了保证获取左手锁后可以成功释放,所以需要加一个try...catch
left.unlock();
}

}
}
}

Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}

//筷子类
class Chopstick extends ReentrantLock {//让筷子锁对象有重入锁的一些特征
String name;

public Chopstick(String name) {
this.name = name;
}

@Override
public String toString() {
return "筷子{" + name + '}';
}
}

输出:

14:20:25.184 c.Philosopher [亚里士多德] - eating...
14:20:25.199 c.Philosopher [苏格拉底] - eating...
14:20:25.705 c.Philosopher [柏拉图] - eating...
14:20:25.705 c.Philosopher [赫拉克利特] - eating...
14:20:26.205 c.Philosopher [苏格拉底] - eating...//哲学家们可以正常的进餐

3.15.4 公平锁

ReentrantLock 默认是不公平的,但是可以通过构造方法来设置是否是公平锁。

公平的举例:

  • 不公平的锁,比如​​synchronized​​;每次当有线程A占用锁对象,其他线程会进入阻塞队列进行等待,当A使用完后,释放锁,其他线程就会进行竞争锁,抢到了就可以使用。所以这个过程时不公平的。
  • 公平的锁,比如​​ReentrantLock ​​ ,按照线程进入阻塞队列的顺序来获得锁,先来先得。

公平锁主要来解决线程饥饿问题。前面我们使用tryLock()也可以进行解决。所以公平锁一般没有必要,会降低并发度,后面分析原理时会讲解

**注:**关于公平锁的示例代码,这里不再演示。后面源码剖析时详细介绍。

3.15.5 条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的(休息室),这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:

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

演示代码

@Slf4j(topic = "c.Test20")
public class Test20 {
static boolean hasCigarette = false;
static boolean hasTakeout = false;
static ReentrantLock ROOM = new ReentrantLock();

//等待烟的休息室
static Condition waitCigaretteSet = ROOM.newCondition();

static Condition waitTakeoutSet = ROOM.newCondition();

public static void main(String[] args) {
new Thread(()->{
ROOM.lock();
try {
log.debug("烟送到没?[{}]",hasCigarette);
while (!hasCigarette){
log.debug("没烟,先歇会!");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
}finally {
ROOM.unlock();
}
},"小南").start();

new Thread(()->{
ROOM.lock();
try {
log.debug("外卖送到没?[{}]",hasTakeout);
while (!hasTakeout){
log.debug("没外卖,先歇会!");
try {
waitTakeoutSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
}finally {
ROOM.unlock();
}
},"小女").start();

Sleeper.sleep(1);
new Thread(()->{
ROOM.lock();
try {
hasTakeout = true;
log.debug("外卖送到了");
waitTakeoutSet.signal();
}finally {
ROOM.unlock();
}
},"送外卖的").start();

Sleeper.sleep(1);
new Thread(()->{
ROOM.lock();
try {
hasCigarette = true;
log.debug("烟送到了!");
waitCigaretteSet.signal();
}finally {
ROOM.unlock();
}
},"送烟的").start();
}
}

输出:

《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)_sed_28

3.15.5 同步模式之顺序控制

本章小结

本章我们需要重点掌握的是

  • 分析多线程访问共享资源时,哪些代码片段属于临界区
  • 使用 synchronized 互斥解决临界区的线程安全问题
  • 掌握 synchronized 锁对象语法
  • 掌握 synchronzied 加载成员方法和静态方法语法
  • 掌握 wait/notify 同步方法
  • 使用 lock 互斥解决临界区的线程安全问题
  • 掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
  • 学会分析变量的线程安全性、掌握常见线程安全类的使用
  • 了解线程活跃性问题:死锁、活锁、饥饿
  • 应用方面
  • 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
  • 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
  • 原理方面
  • monitor(管程)、synchronized 、wait/notify 原理
  • synchronized 进阶原理
  • park & unpark 原理
  • 模式方面
  • 同步模式之保护性暂停
  • 异步模式之生产者消费者
  • 同步模式之顺序控制

注意:

  • synchronized的互斥是为了临界区的代码不上下文切换,产生指令交错,从而保证临界区代码的原子性。synchronized的同步是为了解决当条件不满足时线程等待,条件满足时继续运行。ReentrantLock也可以实现互斥和同步。
  • monitor的源码是用C++写的,Java也实现了Monitor锁,即ReentrantLock


举报

相关推荐

0 条评论