
deepseek生成仅供学习分享文章目录Q1:A1:为什么 JVM 要把“空闲态”叫做“无锁”历史包袱Q2:A2:1. 唤醒阻塞线程到底“贵”在哪里用户态 vs 内核态2. 非公平锁是如何“省掉”唤醒开销的关键场景对比3. 为什么公平锁不能让“插队的线程 C”先跑再唤醒 B4. 一张表看清开销差距量化对比5. 既然非公平锁这么好为什么还要有公平锁Q1:从广义上可以把锁分为乐观锁和悲观锁体现了看待线程同步的不同角度。CAS算法是乐观锁的典型实现。又说Java中synchronized关键字和Lock的实现类都是悲观锁。但是从上面“针对synchronized的四种锁的状态”中可以看到无锁、偏向锁、轻量级锁三种状态要么不加锁要么通过CAS和自旋等待这显然更像乐观锁的实现那怎么能说synchronized是悲观锁呢A1:你之所以觉得矛盾是因为你把“宏观语义层面”的锁分类和“JVM微观实现层面”的状态升级混为一谈了。这两者讨论的完全是两码事。我用一句话帮你理清核心差异synchronized在“语义”上永远是悲观锁但它内部的“状态名”借用了“无锁”这个词用来表示“当前没有线程竞争”。下面我分三层彻底帮你解开这个结第一层语义层面synchronized 为什么是悲观锁从使用效果来看无论synchronized处于哪种状态它都绝对不允许两个线程同时进入临界区。一旦有线程没抢到锁JVM 最终在重量级锁时会把线程挂起到操作系统内核态让出 CPU。这种“假设一定有人跟我抢抢不到我就阻塞等待”的策略在并发理论中就被定义为悲观锁。第二层实现层面四种状态到底是什么这四种状态是 JDK 1.6 之后为了优化性能做的“锁升级”记录在 Java 对象的对象头Mark Word里。它们描述的是 “这把锁当前被竞争得有多激烈”而不是“这锁是乐观还是悲观”。无锁Unlocked对象刚 new 出来没有任何线程来抢这把锁标志位是 01。偏向锁Biased Locking只有一个线程来抢锁。JVM 觉得“既然只有你一个人我连 CAS原子比较交换都懒得做了直接把你的线程 ID 写进对象头就行”。轻量级锁Lightweight Locking有第二个线程来抢了但两者交替执行没有同时竞争。此时通过 CAS自旋 来抢锁不挂起线程。重量级锁Heavyweight Locking多个线程同时激烈竞争CAS 自旋太久浪费 CPU于是升级为重量级抢不到的线程被操作系统挂起阻塞。第三层核心澄清——“无锁”状态为什么不是乐观锁这是你最困惑的地方。请记住这个金科玉律synchronized里的“无锁”状态仅仅表示“这把锁目前空闲没有被任何线程持有”它是一个“空闲态Idle”而不是“乐观并发控制Optimistic Concurrency”真正的乐观锁如 AtomicInteger 底层的 CAS是指多个线程可以同时进来读数据修改时如果发现版本变了就重试但绝对不阻塞对方。而synchronized的“无锁”状态一旦有第一个线程进入它立刻就会变成“偏向锁”或“轻量级锁”直接禁止第二个线程进入临界区哪怕第二个线程只是想读数据也不行。它压根不允许并发修改后回滚这种事情发生所以它骨子里就是悲观的。一张图帮你彻底捋清关系锁类型核心策略是否允许多个线程同时进入临界区乐观锁如 AtomicInteger先尝试修改失败则重试自旋重试等待允许大家都来读修改时 CAS 保证原子synchronized任何状态先尝试获取锁失败则自选重试等待或直接阻塞绝对不允许任何时候只允许一个线程进入临界区为什么 JVM 要把“空闲态”叫做“无锁”历史包袱因为在 JVM 对象头的那 2 个比特位Lock Flags里01 表示“未锁定Unlocked”/“偏向未生效”00 表示“轻量级锁”10 表示“重量级锁”11 表示“GC标记”。翻译成中文时大家习惯把 Unlocked 直译为“无锁”。这是一个翻译上的“偷懒”它真正的含义是“未锁定”而不是“无锁编程Lock-Free”中的那个无锁。最后给你一个终极定论如果面试官问你“synchronized 是无锁吗”你可以自信地回答不是。synchronized无论在偏向、轻量还是重量级下都是悲观锁。它对象头里所谓的“无锁”状态只是表示当前没有线程持有该监视器即空闲状态而不是指它采用了乐观并发控制策略。乐观锁的代表是java.util.concurrent(JUC)包下的 CAS 原子类。Q2:为什么说CPU唤醒阻塞线程的开销比非公平锁大A2:要回答这个问题我们先看一个极简的数学公式然后从 CPU 和操作系统层面拆解底层原理。核心结论非公平锁通过“允许插队”尽可能地避免了线程的“挂起Park”和“唤醒Unpark”而公平锁严格排队导致每次锁交接都必须触发一次昂贵的线程唤醒。一次线程唤醒的开销微秒级约等于 500 ~ 1000 次 CAS比较并交换自旋操作纳秒级的耗时。1. 唤醒阻塞线程到底“贵”在哪里用户态 vs 内核态当我们说“唤醒线程”时底层调用的是LockSupport.unpark()或操作系统底层的pthread_cond_signal。这个操作之所以昂贵是因为它涉及用户态与内核态的切换Context Switch系统调用SyscallJava 代码运行在用户态唤醒线程必须调用 JVM 的 native 方法触发系统调用陷入内核态Kernel Mode。内核调度器介入操作系统内核需要修改等待队列中线程的状态从 WAITING - RUNNABLE。CPU 上下文切换CPU 需要保存当前线程的寄存器、程序计数器等状态加载被唤醒线程的上下文。缓存失效Cache Miss被唤醒的线程很可能之前被切换到了别的 CPU 核上导致 L1/L2 缓存变冷数据需要重新从内存加载。这一套组合拳下来通常需要几微秒到几十微秒。在超高并发每秒百万级 QPS场景下如果每次锁交接都做一次唤醒CPU 将会把大量时间耗在操作系统调度上而不是执行业务逻辑。2. 非公平锁是如何“省掉”唤醒开销的关键场景对比我们来看两个具体的执行场景假设锁当前被线程 A 持有线程 B 因为没抢到锁已经被挂起阻塞在等待队列里。场景一公平锁严格 FIFO当线程 A 执行完临界区释放锁unlock()。公平锁会强制检查等待队列发现队列头是线程 B。JVM 必须立即执行unpark(B)系统调用唤醒线程 B。线程 B 被唤醒后需要重新参与 CPU 时间片竞争一段时间后才能获取锁并运行。代价每次锁交接都必须经过一次完整的“用户态-内核态-调度”链路。场景二非公平锁允许插队当线程 A 执行完临界区释放锁unlock()的那一瞬间锁处于短暂的空闲期。此时线程 C当前活跃在 CPU 上的线程恰好执行到lock()方法。非公平锁允许线程 C 立刻执行一次 CAS原子比较交换尝试拿锁。由于线程 C 当前正占用 CPU 时间片CAS 指令极快纳秒级瞬间拿到锁。线程 B 依然安静地阻塞在等待队列里这次压根没有触发 unpark() 系统调用。结论非公平锁把锁的交接变成了 “运行中的线程直接接力”避免了线程 B 的挂起和唤醒。虽然线程 B 多等了一会儿但 CPU 的通路吞吐量Throughput得到了极大提升。3. 为什么公平锁不能让“插队的线程 C”先跑再唤醒 B理论上可以但那会破坏“公平性”的定义。公平锁为了确保“先来后到”它必须在释放锁时立即唤醒等待队列头部的线程。即使此时有活跃线程 C 在 CPU 上飞奔它也必须把锁让给被唤醒的 B或者 B 醒了之后 C 才来。这就导致了一个尴尬的局面CPU 为了让一个被挂起的、缓存失效的线程 B 拿到锁强行打断了当前正在运行的线程 C。这种“打断”本质上就是一次昂贵的上下文切换。4. 一张表看清开销差距量化对比操作类型耗时级别触发场景CAS自旋~ 10 - 50 纳秒非公平锁的“插队”尝试CPU 缓存 Miss~ 100 - 300 纳秒唤醒线程后重建缓存系统调用unpark~ 0.5 - 5 微秒公平锁释放锁时的强制唤醒上下文切换挂起唤醒~ 5 - 20 微秒线程阻塞和恢复的完整过程核心数据一次完整的“公平锁唤醒”约等于 100 ~ 500 次 非公平锁的 CAS 尝试。当并发冲突极其频繁时非公平锁的总吞吐量TPS通常比公平锁高 1~3 个数量级。5. 既然非公平锁这么好为什么还要有公平锁非公平锁虽然省 CPU但有一个致命弱点线程饥饿Starvation。如果线程 C 总是恰好在锁释放的瞬间拿到锁因为它是活跃线程那么等待队列里的线程 B、D、E 可能永远拿不到锁导致长时间阻塞甚至超时。非公平锁适合业务逻辑极短、锁持有时间极短的场景比如纯粹的计数器累加。这个时候“插队”影响不大性能收益巨大synchronized 和 ReentrantLock 默认都是非公平的。公平锁适合业务逻辑较长、锁持有时间较长或要求严格避免饥饿的场景比如银行转账、注册审核。此时线程挂起的开销相比于业务耗时毫秒级可以忽略不计但公平性至关重要。最后回应你之前看到的Unsafe.getAndAddInt你上一轮看的 CAS 自旋do-while正是非公平锁底层的核心。它本身不会主动挂起线程而是让线程在用户态“疯狂试探”。当 synchronized 升级为重量级锁时它本质上会进入一个阻塞队列这时候就涉及到了唤醒开销。这就是为什么 JVM 一定要先尝试偏向锁和轻量级锁自旋——目的就是为了避免进入重量级锁从而避开那笔昂贵的“唤醒税Wake-up Tax”。