Java - JVM - 监视器锁 与 等待队列
-
概述
- jvm 监视器锁 与 等待队列
- 初版, 目前来看, 还是一个 生硬的总结
- 后续会做调整
-
背景
- 之前讲了 synchronized
- 但是其中的原理, 并没有讲
- 这些是定义在 java 内存模型 里的
1. 回顾: synchronized
-
概述
- 回顾之前的内容
-
格式
-
方法
# 后面简称 同步方法 public static synchronized void method() {} public synchronized void method() {}
-
代码块
# 后面简称 同步代码块 synchronized(obj) {}
-
-
作用
- 通过一个对象, 来限定 synchronized 范围内的代码块
- 获取对象锁的线程, 可以执行代码块内容
- 其他线程, 需要等到 对象锁 被释放, 才有机会执行
- 通过一个对象, 来限定 synchronized 范围内的代码块
-
所以
- 这个锁, 到底是个 什么情况
- 监视器锁
- 这个锁, 到底是个 什么情况
2. 监视器锁
-
概述
- 监视器锁
-
监视器锁
-
概述
- 一种 同步机制
-
机制
-
对象
- 每个对象, 都有一个 监视器锁
-
线程
-
持有
- 线程获取 监视器锁 成功, 则称 线程持有锁
- 同时, 相关的 同步方法/代码块, 进入 加锁状态, 只有 持有锁 的线程, 才可以执行
- 同一时间, 同一个监视器锁, 只能被一个线程持有
- 同一个线程, 可以持有同一个 监视器锁 多次 - 递归
- 同一个线程, 可以持有不同的 监视器锁 多次 - 同步方法/同步代码块 的连环调用
- 持有 监视器锁
- 可以执行执行 依赖该锁 的 同步方法/同步代码块
- 线程获取 监视器锁 成功, 则称 线程持有锁
-
触发
- 线程执行 同步代码块/同步方法 时, 会 触发 对 相应监视器锁 的请求
-
请求
- 未加锁
- 单线程请求
- 线程请求成功, 持有锁
- 相关 同步方法/同步代码块 加锁
- 多线程请求
- 只有一个线程 可以请求成功, 并持有锁
- 相关 同步方法/同步代码块 加锁
- 其他线程, 进入 阻塞状态
- 直到 之前的线程, 不再持有锁, 展开下一次 竞争
- 老实说, 这个状态, 我也不太清楚, 先这么说, 不影响理解
- 单线程请求
- 已加锁
- 持有锁线程
- 再额外持有一次锁
- 其他线程
- 进入 阻塞状态
- 持有锁线程
- 锁对象
- 实例方法 和 普通代码块
- 申请 对象 的监视器锁
- 静态方法
- 申请 类对象 的监视器锁
- 实例方法 和 普通代码块
- 未加锁
-
释放
- 正常
- 情况
- 方法正常执行完
- 结果
- 持有锁 的线程, 归还 一层锁
- 释放的顺序, 类似 栈 - 先持有, 后释放
- 如果 线程持有 0 层锁, 则 同步方法/同步代码块 不再加锁
- 持有锁 的线程, 归还 一层锁
- 情况
- 异常
- 情况
- 出现 未处理异常 时, 会抛出异常 并 归还 所有锁
- 这个在 ref 里有提到, 感兴趣的朋友, 可以看下
- 出现 未处理异常 时, 会抛出异常 并 归还 所有锁
- 情况
- 正常
-
-
-
-
问题
- 现状
-
已经形成一个相对完整的循环
# 正常情况 触发 > 请求 > 执行 > 释放 > 再次请求
-
- 问题
-
线程的切换
- 每个线程, 必须执行完一段 同步方法/同步代码块, 才能切换
- 这样的切换方式, 感觉有些 机械, 无法应对一些场景
-
场景
- 线程A 在 同步代码/同步代码块 中, 需要等待另一个资源就绪
- 按现在的机制来设计代码 - 暂时不考虑 异步...
- 检测资源是否就绪
- 如果就绪了, 就正常执行
- 如果没有就绪, 则直接退出
- 退出之后, 由外面一层方法负责 轮询
- 按现在的机制来设计代码 - 暂时不考虑 异步...
- 线程A 在 同步代码/同步代码块 中, 需要等待另一个资源就绪
-
问题
- 需要一个 循环检测
- 增加了代码的 复杂度
- 如果资源没有就绪, 则 需要重新执行方法
- 方法中 部分代码, 可能会执行多次,
- 需要考虑 幂等性
- 增加了代码的 复杂度
- 锁的影响
- 要么一直 持有锁, 不释放
- 如果别的线程也需要这样的锁, 有概率在成 长时间阻塞
- 要么多次 申请同一个锁
- 如果这个锁竞争激烈, 可能会导致处理效率降低
- 竞争本身, 线程切换, 都会有资源的消耗
- 增加了 系统的额外消耗
- 要么一直 持有锁, 不释放
- 需要一个 循环检测
-
解决
- 让线程之间, 相互协调
-
- 现状
-
注意
- 同一个监视器锁, 可以被持有多次
- 解锁也需要多次
- 同一个线程, 可以持有多个监视器锁
- 持有多个锁的时候, 如果出现异常, 会一次把所有锁 都归还
- 同一个监视器锁, 可以被持有多次
3. 等待队列
-
概述
- 等待队列
-
准备
- 场景
- 线程
- 多个线程
- 同步方法/同步代码块
- 一段
- 线程
- 场景
-
机制
-
等待
- 前提
- 线程 持有锁
- 操作
- 线程执行 等待 操作
- LockObj.wait()
- LockObj.wait(time)
- 线程执行 等待 操作
- 结果
- 线程 放弃锁
- 线程进入 等待队列
- 前提
-
等待队列
- 监视器锁
- 每个 监视器锁 都有一个 等待队列
- 线程
- 进入等待队列的线程, 都是曾经 持有过锁, 并且主动放弃的
- 队列里的线程, 会一直呆在队列里, 不会再对锁去做 申请
- 除非被唤醒
- 监视器锁
-
唤醒
- 前提
- 线程 持有锁
- 操作
- 线程执行 唤醒 操作
- LockObj.notify()
- LockObj.notifyAll()
- 线程执行 唤醒 操作
- 结果
- notify
- 随机一个线程被唤醒
- notifyAll
- 唤醒所有线程
- 被唤醒的线程
- 离开等待队列
- 重新参与 锁的竞争
- 抢到锁之后, 从 wait() 后面开始继续执行
- notify
- 前提
-
-
疑问
-
等待队列里 唤醒的线程, 和 被阻塞 的线程, 优先级有区别吗
- 我目前没发现区别
- 可以理解为, 从 等待队列 里出来, 就重新去外面排队了
- 等待队列的作用, 就是让你 既没有锁, 也不去排队
- 我目前没发现区别
-
为什么 wait 和 notify 都需要 持有锁呢
- 目的
- 防止 notify 被忽略
- 场景
-
线程 t1
-
执行 wait()
-
但是 wait 通常不单独存在, 需要配合 条件判定 来使用
-
所以, 通常是
if (condition) { wait(); }
-
-
线程 t2
- 执行 notifyAll()
-
假设, 都没有同步
-
执行
- t1: 判定通过, 还没来得及执行 wait(), 被切换了
- t2: 执行 notifyAll(), 切回去
- t1: 执行 wait(), 完美错过 notify
-
- 目的
-
-
异常
-
如果 wait 中发生中断, 会抛出异常
- InterruptedException
-
处理
- 机制
- try...catch
- 机制
- 等待队列中的线程t, 接收到 中断请求
- 线程t 响应中断请求, 退出 等待队列, 并修改 自身状态
- 线程重新获得锁时, try...catch...内的处理, 会执行
- Java 语言规范
- 机制
-
4. 后续
-
概述
- 感觉后续还有些问题
-
后续
- InterruptedException 的一些机制
- 触发时机
- 标记位
- 线程停止的其他方法
- sleep
- yeild
- join
- 线程状态
- 目前只有 运行, 阻塞, 等待
- InterruptedException 的一些机制
-
疑问: 锁的特性
- 忽然回想起, 面试时候, 面试官会问一些我完全摸不着头脑的问题
- 你本来就很菜
- 大概是 什么 乐观锁, 悲观锁, 偏向锁, 自旋锁
- 这些到底是什么玩意, 也没见 Java 里有这些锁
- 后来才知道, 这些是 锁的性质
- 但是 好多博文上乱七八糟, 甚至和 锁的实现 混为一谈, 让人懵逼
- 但是看一堆博客, 总觉得这玩意学起来, 心理不踏实, 想找本书, 看看什么书 有讲这些玩意
- ref
- Java多线程中锁的理解与使用
- 原文是 并发编程网 的, 不知道为啥这几天我上不去了
- Java多线程中锁的理解与使用
- 忽然回想起, 面试时候, 面试官会问一些我完全摸不着头脑的问题
ps
-
ref
- Java 语言规范(Se 8)
- chpt17
- 图解 Java 多线程设计模式
- 序章1
- 一个线程执行synchronized同步代码时,再次重入该锁过程中,如果抛出异常,会释放锁吗?
- Java SE 8 源码
- Object.java
- 关于 wait() 方法的同步执行
- 关于 InterruptedException
- Java并发--InterruptedException机制
- 虽然标榜原创, 但是和 下面 2006 年的文章, 十分相似
- 几篇 ref 都值得一看
- 处理 InterruptedException
- Java并发--InterruptedException机制
- Java 语言规范(Se 8)
-
感觉
- 并发的内容, 需要先明确这么些东西
- 哪些线程, 会共用一把锁
- 这些线程, 会有什么样的 公共资源
- 线程之间, 需要怎么样的依赖关系
- 并发的内容, 需要先明确这么些东西
尽量尝试解释清楚; 自己校对能力有限, 如果有错误欢迎指出