我觉得吧, 没必要因为不懂而感到羞耻, 不懂就去学呗 一天学不会就再来一天喽, 总有一天会明白的,所以大胆的承认自己的无知吧, 然后去追寻答案
锁的基础核心实现(两大核心)
CAS(compare and swap)
首先记住CAS 是 乐观锁 (乐观策略) 的核心实现。
阻塞与唤醒
这是悲观锁 (悲观策略)的核心实现。
上一篇章我们学习了锁的分类, 要知道所有分类的锁 , 底层最终只依赖这两种核心的实现手段, 这是根基, 不管什么维度的锁, 本质上都是这两种手段的单独使用或组合使用, 掌握它能一通百通。 也是面试的核心。 这一篇章, 我们就只聚焦这两个核心他们的以下三个知识点
- 熟悉核心流程
- 优缺点
- 存在的核心问题以及解决方案
CAS
核心流程
1 . 首先核心3参数, 内存地址V, 预期旧值 A, 目标新值 B。
-
线程读取内存地址V 存储的 当前值 是否与 预期旧值 A 一致, 如果一致 , 则用 CAS原子命令 将 内存地址V存储的值 更新 为目标新值, 返回成功 。
-
不一致 ,则更新失败, 直接失败 或者 自旋重试(业务侧更新预期旧值 以及 重新计算 目标新值B 再重新尝试CAS流程)
优点
无上下文切换开销, 当并发冲突少时, 性能极高。
基于CPU原子指令 实现, 无需依赖OS 即 操作系统, 属于用户态的操作, 响应快。
缺点
当并发冲突高时, 自旋会持续占用CPU资源, 导致CPU飙升。
仅能保证单个变量操作的原子性, 多个变量就没法支持了。
核心问题 + 解决方案
- 问题1 : ABA问题(内存地址V存的值从1 -》 2 -》 1 , 对于执行CAS操作 预期旧值是1 的线程, 误判为没变)
- 解决方案 : 版本号机制, 比对值+ 版本号
- 问题2 : 高竞争下自旋浪费CPU
- 解决方案 : 自适应自旋 , 就是 通过一些指标判断当前环境竞争大与否, 如果大 , 同一时间的自旋次数少, 反之, 自旋次数多。
- 问题3: 单变量原子性局限
- 解决方案: 结合锁(如 synchronized ) , 拆分业务逻辑, 或把 多个变量封装成一个对象 使用AtomicReference.
阻塞唤醒
核心流程
- 线程竞争失败
- 放弃CPU执行权, 进入阻塞状态(waiting / timed_waiting),移出CPU调度队列
- 持有锁 线程 释放锁时, 唤醒阻塞队列中 等待线程
- 被唤醒的线程 重新进入 就绪队列
- 等待CPU调度后 再次竞争锁
优点
并发冲突高时, 线程由于直接阻塞, 所以就不会占用CPU资源, 也就不会浪费CPU资源。
支持多变量, 复杂逻辑的线程安全, 使用场景更广。
缺点
线程阻塞 / 唤醒 , 涉及 用户态 -》 内核态(OS) -》 用户态 切换,主要是在这过程中上下文切换开销大。 且线程被唤醒后,存在CPU调度的延迟, 响应不如 CAS。
核心问题 + 解决方案
- 问题1 : 阻塞唤醒开销大
- 解决方案: 前置CAS 优化, 比如synchronized 先 自旋CAS , 失败再则色, 来减少阻塞的概率。
- 问题2 : 线程唤醒后 ,竞争锁 ,仍可能失败(惊群效应)
- 解决方案 : 这种有两种方案 一种用公平锁, 确保拿锁线程的顺序, 另一种方案 则是 重试一定次数之后, 直接失败处理, 不重试了。
- 问题3 : 死锁风险
- 解决方案: 避免锁的循环依赖, 按照固定顺序 加锁, 设置锁超时。
补充一下-死锁的四个必要的条件
- 一个锁只能被一个线程占用
- 请求与保持, 请求新的锁, 保持对已有的锁的占用
- 不可剥夺, 线程占用的锁不能被强行剥夺。
- 循环等待, 多个线程构成了一个循环链。