一、为什么需要再谈“锁”
并发是 Java 的灵魂,而锁是并发的节拍器。
当多线程同时读写共享变量,当微服务跨节点竞争资源,当 CPU 核心数突破三位数,锁的形态、粒度、语义都在飞速演化。
本文尝试用近四千字,把 Java 生态中的各类锁——从语言级到 JVM 级,再到框架级与分布式级——串成一条思维链,帮助你下一次排查“线程饥饿”或“死锁”时,能一眼定位症结所在。
二、锁的初心:临界区与可见性
并发编程的底层矛盾是“原子性”与“可见性”。
临界区是“必须一次性完成的动作”;可见性是“一个线程的修改何时被另一个线程感知”。
锁用互斥保证原子,用内存屏障保证可见,两者缺一不可。
理解这一点,再复杂的锁都只是“临界区+可见性”的不同实现。
三、语言级锁:synchronized 的三次进化
1. 早期重量级
早期 synchronized 直接映射为 OS 互斥量,线程阻塞与唤醒需要内核介入,性能犹如“高速公路上的收费站”。
2. 偏向锁
JVM 检测到只有一个线程反复进入临界区,把锁标记“偏向”该线程,后续进入无需 CAS,减少无竞争场景开销。
3. 轻量级锁
当第二个线程出现时,偏向锁升级为轻量级锁,通过 CAS 操作在对象头里“抢锁”,失败后再膨胀为重量级锁。
4. 自旋与适应性自旋
短暂阻塞时让线程忙等若干循环,避免上下文切换;JVM 会根据历史成功率动态调整自旋次数。
一句话:synchronized 现在是一条“可伸缩的锁链”,从无竞争到高竞争,自动选择最轻的形态。
四、显式锁:Lock 接口的万花筒
- ReentrantLock:可重入、可中断、可限时、可公平/非公平。
- ReadWriteLock:读共享、写独占,在读多写少场景提升并发度。
- StampedLock:乐观读、悲观读、写锁三种模式,通过版本戳避免写饥饿。
- Condition:精准唤醒,实现“生产者-消费者”的精细化协作。
显式锁把“锁策略”交回开发者手中,代价是需要手动释放,稍有疏忽便可能“忘记解锁”。
五、原子变量:锁的极致轻量
AtomicInteger、AtomicLong、AtomicReference 利用 CPU 的 CAS 指令,把锁粒度压缩到单个变量。
ABA 问题由版本戳(AtomicStampedReference)解决;
高并发下的 CAS 失败重试可能导致 CPU 空转,于是有了 LongAdder 的“分段累加”思想。
原子变量证明了:当临界区足够小时,锁可以“消失”。
六、JVM 级锁优化:从对象头到内存屏障
1. 对象头 Mark Word
记录锁状态、哈希码、GC 分代年龄,一条 64 位指令完成状态切换。
2. 内存屏障
LoadLoad、StoreStore、LoadStore、StoreLoad 四种屏障确保指令重排不会破坏可见性。
3. 逃逸分析
若对象只在单线程内使用,JVM 会“消除锁”,把同步块优化成普通代码。
4. 锁粗化与锁消除
连续加锁解锁会被合并;不可能共享的对象会被去掉锁。
七、并发容器:锁的“隐身术”
ConcurrentHashMap 在 Java 8 之前用分段锁,之后改为 CAS + synchronized 链表/红黑树头节点,锁粒度降到单个桶。
CopyOnWriteArrayList 用写时复制,读操作完全无锁,适合读多写少的广播场景。
BlockingQueue 家族用“双锁+条件变量”实现生产-消费解耦,避免全局锁。
八、框架级锁:读写世界的再抽象
- StampedLock 的三态模型:乐观读 → 悲观读 → 写锁,通过 stamp 版本号避免写饥饿。
- Guava Striped 锁:把对象哈希到 N 条锁带,降低锁竞争。
- Netty 轻量级锁:利用 FastThreadLocal 减少同步开销。
这些框架把 JDK 原语包装成更易用的 DSL,但仍需理解底层语义。
九、分布式锁:跨进程的闸门
1. 基于数据库
用唯一索引或行级锁实现,简单却存在单点故障。
2. 基于缓存
通过 SET NX + 过期时间实现,支持高并发,但需处理“锁续期”与“时钟漂移”。
3. 基于共识
利用分布式一致性算法(Raft、ZAB)选举锁持有者,保证强一致,但吞吐受限。
4. RedLock 争议
多节点多数派加锁,临界场景仍需权衡一致性与可用性。
分布式锁的核心矛盾:网络延迟与单点故障之间的永恒拉锯。
十、锁的可见性问题:volatile 与 happens-before
锁不仅排他,还建立 happens-before 关系:
- 解锁前的写操作对后续加锁线程可见。
- volatile 变量写之后的读操作保证可见性,但不保证原子性。
理解“锁-内存屏障-volatile”三角关系,才能写出正确并发程序。
十一、死锁、活锁、饥饿:锁的副作用
- 死锁:互相等待对方释放锁,形成循环。
预防法:一次性申请全部资源;或按全局顺序加锁。
- 活锁:线程不断重试但始终失败。
解决法:退避策略 + 随机等待。
- 饥饿:某些线程长期拿不到锁。
解决法:公平锁或队列锁。
线上排查三板斧:线程 dump、锁分析工具、业务日志。
十二、性能调优:锁争用与 CPU 亲和
- 锁争用检测:查看阻塞线程栈、锁持有时间、竞争次数。
- 锁分离:把一把大锁拆成多把小锁。
- CPU 亲和:让线程固定在某个核心,减少缓存失效。
- 读写分离:读操作无锁,写操作串行。
- 自旋时间:根据 CPU 核心数与业务延迟动态调整。
十三、未来趋势:锁的消亡与演进
- 无锁算法:CAS、原子累加、Disruptor 环形缓冲区,把同步开销降到 CPU 指令级。
- 协程与虚拟线程:把阻塞代价从内核态降到用户态。
- 硬件事务内存(HTM):CPU 原生支持“事务性”内存操作,失败即回滚。
- 分布式共识升级:Raft 与多副本缓存结合,兼顾性能与一致。
十四、结语:锁的宇宙观
从最轻的原子变量到最重的分布式锁,
从单核时代的 synchronized 到多核时代的无锁算法,
锁的形态不断演化,核心矛盾却始终围绕“互斥、可见、性能”三重张力。
理解锁,不仅是掌握 API,更是理解并发世界的底层节拍。
当你在深夜排查“线程卡死”时,请记住:
锁不是敌人,而是并发秩序的第一道闸门。