大家好,我是考100分的小小码 ,祝大家学习进步,加薪顺利呀。今天说一说Juc:锁升级、偏向轻重锁、wait notify notifyall「终于解决」,希望您对编程的造诣更进一步.
进程和线程
锁升级
Java 中存在了三种类型得锁,分别是偏向锁、轻量锁和重量锁。
我们可以用一个门口带有小黑板的房间举例:
- 偏向锁:
- 加锁:你进入一个门口带有小黑板的房间。小黑板上是空白一片的,你进入房间之前先把自己的名字写上
- 释放锁:你直接离开
- 再次加锁:你进入房间前,看见小黑板上依旧是写着你的名字,你推门就进
- 轻量锁:
- 加锁:你进入一个门口带有小黑板的房间。小黑板上是空白一片的。你进入房间之前先把自己的名字写上
- 释放锁:你把门口小黑板的名字擦掉然后离开
- 再次加锁:你进入房间前,先看小黑板上是否写着你的名字。如果空白一片则进入,就在进来之前先把你的名字写了
- 重量锁:
- 加锁:你进入一个门口带有小黑板的房间,先上去晃动一下门看看有没有被锁起来,没有就进去上锁;有就在门口等待
- 释放锁:你把门锁打开,出去。
- 再次加锁:再去看看门有没有被锁起来,没有就去门口等待。
偏向锁
轻量级锁
锁对象中存在这自己的锁记录Mark Word,其中记录了hash、age、状态码 01 等其他信息
-
上锁:
-
企图上锁的线程在自己的栈中创建栈帧。栈帧中包含了锁记录Mark Word 和锁对象地址。Mark Word里记录了线程自己的地址和状态码 00。锁对象地址指向锁对象。
-
线程指向锁对象,尝试用 cas 替换将锁记录自己的 Mark Word 和锁对象的 Mark Word 进行替换
-
获取成功
-
检查锁对象状态为是否为 01,如果为 01 则代表可以替换。替换成功,锁对象的 Mark Word 的状态将从 01 转换为 00
-
-
获取失败
-
如果不为 01,则说明已上锁。这样就代表了资源竞争,会导致锁膨胀。
-
不为 01,但是发现锁对象的 Mark Word 指向自己的时候,代表锁重入。这时候线程会创建一个新的栈帧。新栈帧和第一次获取锁的栈帧不同,它的 Mark Word 是 null,但是锁对象地址依旧指向锁对象
-
-
-
-
解锁:
-
对线程的栈进行 pull 操作,发现有 Mark Word 为 null 的栈帧时,代表出现了锁重入现象。只需要把对应的栈帧清除即可。
-
当发现 Mark Word 不为 null 的时候,会采用 cas 将对应栈帧 Mark Word 和锁对象的 Mark Word 进行交换。
- 交换成功:锁释放
- 交换失败:说明在上锁期间进入了锁膨胀或许锁已升级为重量级锁,进入重量级锁解锁流程
-
重量级锁
-
Monitor
Monitor 就是锁,也是重量级锁里重要的一环。他的 WaitSet 是用来存放调用 Wait 方法的线程;Owner 是用来存在当前上锁的线程;EntryList 用来存放等待释放锁的线程。
每一个 Java 对象都可以关联到一个 Monitor 对象。如果一个锁对象是重量级锁,那么他的 Mark Word 就会指向 Monitor 对象的指针。接下来就通过 线程、锁对象、Monitor 来讲解一下重量级锁的上锁和解锁过程。
- 上锁:
- 线程 A 向锁对象进行查询,判断锁对象的 Monitor 对象是否存在
- 不存在,就为锁对象关联对应的 Monitor 对象,并且在 Monitor 对象中的 Owner 指向自己的地址,即上锁
- 存在,则判断 Monitor 对象的 Owner 是否存在
- 不存在,则将 Owner 指向自己的地址,即上锁
- 存在,则将自己放入 Monitor 对象的 EntryList 当中。
- 线程 A 向锁对象进行查询,判断锁对象的 Monitor 对象是否存在
- 解锁:
- 线程 A 解锁后,让 Owner 指向 null
- 通知 Monitor 的 EntryList,让 EntryList 的线程进行争夺,让其成为最后的 Owner
锁膨胀
当我们的偏向锁或者轻量级加锁过程中,cas 操作无法成功,这个时候说明了该资源有竞争。这时候需要进行锁膨胀,将锁向上提升一个量级。我们先用轻量级锁膨胀到重量级锁为例。
线程B 向锁对象加锁,但是锁对象的 Mark Word 的状态已经变成了 00,并且也指向了线程A。这个时候就是加锁操作失败,进入锁膨胀流程
- 为锁对象申请 Monitor 对象,让锁对象的 Mark Word 指向 Monitor 地址,状态变为 10。
- Monitor 对象的 Owner 指向现在加锁的线程A
- 线程B 放入 Monitor 的 EntryList 中等待。
- 线程 A 解锁后,让 Owner 指向 null
- 通知 Monitor 的 EntryList,让 EntryList 的线程进行争夺,让其成为最后的 Owner
wait、notify、notifyAll
api
举例子
- 为什么调用wait、notify、notifyAll
- 有的线程进行下去的时候必须要有一些条件去满足。当出现 Owner 线程的条件不满足的时候,可以让该线程调用一个 wait 线程,让其释放锁。当线程满足之后再将其唤醒,让其继续进行未完成的事情。
我们就用一个房间来举例子
- wait
- 有一个独立的房间名字叫 Owner 。Owner 用来自习,每次只能进去一个人
- 小明先在房间的门口那条名字叫 EntryList 的走廊里等待,发现没人他就直接拿进 Owner学习了。
- 小明进 Owner 本来希望复习【语数英课本】但是发现 Owner 里没有【语数英课本】。这样子他即没办法学习也占用了 Owner 。
- 小明主动调用了 wait() 方法,申请要出门到旁边的一个叫 WaitSet 的等待室等待,直到有【语数英课本】才能继续自己的学习
- 这样子这件 Owner 就空闲了,就可以有其他的人进去使用。
- notify
- 房间空出来之后,在 EntryList 里排队的小红进入了 Owner。
- 他是小明的马仔,专门是来为小明放书的。他把【语数英课本】放在 Owner 里,然后调用了 notify() 方法,紧接着他就离开了
- notify() 方法调用之后,就会在 WaitSet 里随机唤醒一个幸运儿让他去 EntryList 里排队。如果抽中小明,当小明进入 Owner 的时候发现【语数英课本】都在,就能继续复习了。如果发现没有【语数英课本】,小明就会回到 WaitSet 当中
- notifyAll
- 有的时候 WaitSet 当中可能是有多个人在等待。
- 小红就不可以调用 notify() 方法去随机唤醒幸运儿了。为了确保小明能够从 WaitSet 中出来到 EntryList 排队到 Owner 里看到自己放的【语数英课本】,他就可以调用 notifyAll() 方法。
- notifyAll() 方法调用之后,WaitSet 当中的所有人都会被唤醒到 EntryList 中排队。有的人发现 Owner 中满足了自己的条件就回去完成自己的事情。有的人发现依旧没满足就会继续去 WaitSet 中等待。小明就发现 Owner 满足了自己的条件,就会继续自己的学习了。
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变成 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁的时候唤醒
- WAITING 线程会在 Owner 调用 notify() 或 notifyAll() 时唤醒,但是唤醒后并不意味着立刻获得锁,仍需要进入 EntryList 重新竞争
虚假唤醒
notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线 程,称之为【虚假唤醒】
虚假唤醒的解决方法:notifyAll()
每个需要调用 wait 的线程格式:
synchronized(lock){
....
while(!条件是否满足){
lock.wait();
}
....
}
唤醒的线程:
synchronized(lock){
....
lock.notifyAll();
....
}
保护性暂停
保护性暂停模式就是提供了一种线程间通信能力的模式。
如果有一个线程的执行结果需要传递给另一个线程,就需要使用保护性暂停模式将两条线程关联起来。
JDK 中 join 方法和 Future 就是使用了此模式实现的。
-
创建一个类来保存执行结果,里面包含了一个线程安全的存入数据方法和获取方法
-
存入数据方法
/** * 传入数据 * * @param date 数据 */ public void pushData(Object date) { synchronized (this) { this.data = date; this.notifyAll(); } }
-
获取数据方法
/** * 获取数据 * * @param timeout 最长等待时间 * @return 数据 */ public Object pullData(long timeout) { synchronized (this) { // 开始时间 long begin = System.currentTimeMillis(); // 已经使用的时间 long passTime = 0; while (data == null) { // 剩余可等待时间 long waitTime = timeout - passTime; if (waitTime <= 0) { break; } try { this.wait(waitTime); } catch (InterruptedException e) { e.printStackTrace(); } passTime = System.currentTimeMillis() - begin; } return data; } }
我们就来重点解释一下这个获取方法。可以看到获取方法当中是使用了时间和 while 循环,这是为了避免出现虚假唤醒和过长等待而设置的。
- 方法先获取传入的参数最长等待时间 timeout
- 记录我们的开始时间 begin 和我们已经使用的时间 passTime
- 然后进入防止虚假唤醒的训话当中。在每次循环开始之前先计算出剩余可等待时间 waitTime
- 如果 waitTime <= 0 代表无剩余可等待时间了,就退出循环返回结果
- 如果还有剩余可等待时间,就进入等待,并且传入可剩余等待时间
- 如果被虚假唤醒,则重新计算 passTime
-
-
创建多线程环境,一个线程负责存入数据,一个线程负责获取数据
public static void main(String[] args) { GuardedObject guardedObject = new GuardedObject(); // 获取数据线程 new Thread(() -> { log.debug("开始等待获取"); Object data = guardedObject.pullData(2000); log.debug("获取结束:{}", data); }, "pull").start(); // 存入数据线程 new Thread(() -> { log.debug("开始存入数据"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // 测试传入空值,pull线程是否会等待 guardedObject.pushData(null); // 传入数据,pull 线程是否会提前结束 // guardedObject.pushData("data"); log.debug("存入数据"); }, "push").start(); }
所有代码:
import lombok.extern.slf4j.Slf4j;
import java.security.PrivateKey;
import java.util.concurrent.TimeUnit;
/** * @author HGD * @date 2022/12/14 23:21 */
@Slf4j
public class Test7 {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
// 获取数据线程
new Thread(() -> {
log.debug("开始等待获取");
Object data = guardedObject.pullData(2000);
log.debug("获取结束:{}", data);
}, "pull").start();
// 存入数据线程
new Thread(() -> {
log.debug("开始存入数据");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 测试传入空值,pull线程是否会等待
guardedObject.pushData(null);
// 传入数据,pull 线程是否会提前结束
// guardedObject.pushData("data");
log.debug("存入数据");
}, "push").start();
}
}
/** * 增加超时效果 */
class GuardedObject {
/** * 需要传递的传递的数据 */
private Object data;
/** * 获取数据 * * @param timeout 最长等待时间 * @return 数据 */
public Object pullData(long timeout) {
synchronized (this) {
// 开始时间
long begin = System.currentTimeMillis();
// 循环经历时间
long passTime = 0;
while (data == null) {
// 剩余可等待时间
long waitTime = timeout - passTime;
if (waitTime <= 0) {
break;
}
try {
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
passTime = System.currentTimeMillis() - begin;
}
return data;
}
}
/** * 传入数据 * * @param date 数据 */
public void pushData(Object date) {
synchronized (this) {
this.data = date;
this.notifyAll();
}
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13298.html