一张图看懂 synchronized 锁升级和 AQS 排队

一张图看懂 synchronized 锁升级和 AQS 排队 前几篇把各种锁的原理和对比讲了一遍但是原理是懂了但脑袋里没画面。锁升级到底是怎么一步步变重的AQS 那个队列到底怎么排队怎么唤醒的光看文字确实容易晕。所以这篇我试着用画面感来写把整个过程变成你能在脑子里看到的场景。synchronized 的锁升级从认人到打架synchronized 不会一上来就用最重的锁。它是看情况来的——竞争的人少就轻着用抢的人多了才变重。这个过程叫锁升级。网上很多人说是打怪升级我觉得挺贴切。第一阶段无锁最开始没有人来抢这把锁。它就是个普通对象对象头里的 Mark Word 存的是哈希码之类的东西。这个阶段没什么好说的——没人抢当然最省事。第二阶段偏向锁认人模式来了第一个线程它要进入同步块了。这时候 JVM 想“就你一个人那我把你的 ID 记在对象头上下次你再来就不用检查了直接进。”这就是偏向锁。它的样子大概是门上贴了一个铭牌写着第一个线程的名字。这个线程每次来看一眼铭牌是自己的推门就进。不需要任何多余的操作。速度最快——因为没有同步开销。就是一个最普通的 if 判断比一比就过去了。但如果第二个线程来了呢铭牌上写的不是自己的名字它就只能等着不它会尝试把铭牌上的名字改成自己的。这时候就有竞争了。偏向锁觉得不对劲开始撤退。第三阶段轻量级锁自旋模式第二个线程来抢锁偏向锁就升级成了轻量级锁。这个阶段我自己的理解是两个人抢一把旋转门的钥匙两个人都站在门口手里拿了个令牌。谁都想去替换门上的锁标记——换成自己的。没抢到的人也不去睡觉就站在门口原地转圈一秒钟问十次好了没好了没。这叫自旋。里面的人一出来外面转圈的人立刻抢上去。自旋的好处是不用把线程挂起再唤醒——挂起和唤醒是要操作系统帮忙的开销很大。坏处是如果里面的人一直不出来外面的人一直在转CPU 就被白白烧掉了。所以轻量级锁只适合代码执行特别快竞争也轻微的场景。比如就执行一个 count几纳秒就完事了自旋一小会儿完全等得起。第四阶段重量级锁阻塞模式如果不止两个人抢了三个、四个、一堆人都来了一圈人在那里自旋CPU 要被烧冒烟了。JVM 这时候说“算了别转了升级吧。”重量级锁上来之后操作方式完全变了没抢到锁的人直接被安排到等候室睡觉线程阻塞让出 CPU。里面的人出来的时候由操作系统保安去叫醒下一个人。这一来一回从用户态切到内核态再切回来开销不小。但没办法线程太多了自旋更亏。锁升级的完整路径画出来就是这样JDK 15 之后[无锁] │ 第一个线程来了 ▼ [轻量级锁] ─────── 出现多个线程同时竞争 ────── [重量级锁] 自旋模式 阻塞模式 CAS 抢锁 Mutex Lock比原来少了偏向锁那一环。ReentrantLock 的 AQS 排队把它想象成银行柜台ReentrantLock 跟 synchronized 的工作方式完全不同。它不搞锁升级那一套它的核心是AQSAbstractQueuedSynchronizer。我第一次理解 AQS 的时候是靠一个画面一个银行网点只有一个柜台在办业务。AQS 内部就维护了这么几个东西state 变量0 表示柜台空着1 表示有人在办业务大于 1 表示同一个人反复办了好几个业务可重入等待队列CLH 双向链表排队的人一个接一个串起来画成图就是柜台正在办业务线程A 排队的人 线程B ←→ 线程C ←→ 线程D非公平锁怎么工作的默认模式默认情况下的 ReentrantLock 是非公平锁。非公平锁的意思是允许插队。流程是这样的1. 插队线程来了不管队列里有没有人在排队它先看一眼柜台——如果柜台空着state 0它直接冲上去把 state 改成 1开始办业务。线程E 冲进来 → 看到柜台空着 → 一屁股坐下开始办业务 排队中的 B、C、D这就是非公平锁。2. 判断是不是自己如果柜台已经有人在办业务它再看一眼——柜台坐着的会不会是自己如果是自己那 state 加 1重入。比如自己连着办了两次业务state 就是 2。3. 乖乖去排队如果柜台是别人或者它没抢过别人那就只能排队了。它把自己打包成一个 Node挂在队列末尾。4. 睡觉排上了之后它发现前面还有人。这时候它调用LockSupport.park()把自己休眠了让出 CPU。等前面的人办完了会来叫醒它。公平锁的区别不许插队公平锁的区别只有一句话到了柜台前先回头看一眼——后面有没有人在排队。如果有人排队即使柜台空着你也不能直接坐上去。你得乖乖去队尾站着。这个检查在 AQS 里对应的方法是hasQueuedPredecessors()——队列里有没有排在我前面的人奶茶店的例子我跟朋友聊这个的时候用奶茶店的例子解释。对方一下就懂了假设你在奶茶店买奶茶前面的人刚买完走人柜台空了。非公平锁你刚好站在柜台前不管后面有没有人在排队你直接对店员说给我来一杯柠檬茶。如果店员理你了你就买到了——后面排队的人只能看着。公平锁你走到柜台前先回头看一眼。如果取号机那儿还有人在等你即使站在空柜台前也不能买必须去队尾重新排队等叫号。在性能上非公平锁通常比公平锁快——因为线程刚释放锁下一个线程刚好上来抢这个时间窗口里大概率抢得到省了唤醒其他线程的开销。公平锁虽然公平但吞吐量低一些。解锁的流程不管公平还是不公平解锁是一样的先确认——是不是自己坐在柜台里不是的话抛异常state 减 1——如果减完还不是 0说明还有重入没退出比如 state 从 2 减到 1流程结束如果 state 变成了 0——说明彻底空出来了。AQS 会找到队列里的第一个人头节点的下一个调用LockSupport.unpark()把它唤醒被唤醒的人醒过来再试着去抢锁两个锁的工作方式一句话概括synchronized升级制。人少就轻着来人多了再变重——“看人下菜碟”。ReentrantLock排队制。来了就抢抢不到就排队等叫号——“银行柜台”。这两种思路没有绝对的好和不好。它们只是选择了不同的策略来解决同一个问题多个人抢同一个资源的时候怎么保证不乱套。而且从结果来看不管是哪种锁最后落到代码层面你写的都是同一件事——把需要保护的那几行代码包起来不让多个线程同时碰。