Java 锁机制深度解析(系列一):总览与核心概念

Java 锁机制深度解析(系列一):总览与核心概念

前言

在并发编程中,是协调多线程对共享资源访问的核心手段。Java 提供了丰富的锁机制,从最底层的synchronized到 JUC(java.util.concurrent)包中的各类显式锁,形成了一个层次分明的锁体系。


一、锁的分类体系

从不同维度来看,Java 中的锁可以划分为多个类别:

按粒度

维度粗粒度锁细粒度锁
锁范围大(整个对象/类)小(分段/行)
并发度
实现复杂度简单复杂
典型代表synchronized(类.class)ConcurrentHashMap 分段锁
适用场景低频访问、简单同步高频并发、高性能需求

按性质

维度隐式锁(synchronized)显式锁(Lock)
管理方式JVM 自动加锁/解锁开发者手动 lock() / unlock()
灵活性低,固定语法高,支持超时、中断、尝试
锁类型内置互斥锁多种实现(互斥、读写、乐观读)
性能经过持续优化(偏向锁等)取决于具体实现
适用场景简单同步需求复杂并发控制

按竞争策略

公平锁 vs 非公平锁

维度公平锁非公平锁
获取策略FIFO 先来后到允许插队
饥饿无饥饿 ✅可能导致饥饿 ❌
吞吐量较低更高 ✅
典型实现ReentrantLock(true)synchronizedReentrantLock(false)

可重入锁 vs 不可重入锁

维度可重入锁不可重入锁
同一线程重复获取允许 ✅不允许 ❌(死锁)
使用复杂度简单复杂,需自行避免重入
典型实现ReentrantLocksynchronized自定义简单锁
实际应用几乎全部使用很少使用

按状态(JVM 内部)

这是理解synchronized性能的关键:

锁状态标志位适用场景Mark Word 存储
无锁01无竞争hashCode、分代年龄
偏向锁01单一线程反复获取线程 ID
轻量级锁00少量竞争,CAS 自旋Lock Record 指针
重量级锁10多线程激烈竞争Monitor 指针

二、核心术语速览

术语说明示例
CASCompare-And-Swap,无锁原子操作AtomicInteger.incrementAndGet()
自旋线程不挂起,循环尝试获取锁轻量级锁的获取方式
锁消除JIT 编译时发现锁对象不会逃逸,移除锁StringBuffer在方法内使用时
锁粗化JIT 将相邻的加锁/解锁合并循环内反复加锁的场景
管程(Monitor)操作系统中管理线程同步的机制,Java 中每个对象关联一个 Monitorsynchronized的底层实现
AQSAbstractQueuedSynchronizer,JUC 锁的基石ReentrantLock、CountDownLatch 等

三、如何选择锁?

实际开发中的选择原则:

场景推荐方案原因
方法级简单同步synchronized代码简洁,JVM 自动优化
需要超时/中断ReentrantLock提供tryLock(timeout)lockInterruptibly()
读多写少ReadWriteLockStampedLock读读不互斥,大幅提升并发度
极致读性能StampedLock乐观读乐观读完全不加锁,仅事后校验
单个变量更新Atomic*CAS 无锁,性能优于锁
集合类并发访问ConcurrentHashMapCopyOnWriteArrayList内部已实现高效分段锁或写时复制

四、性能对比(概览)

以下为 JDK 8+ 典型场景下的相对性能(数值越大越好,仅供参考):

说明:JDK 6 对synchronized做了大量优化(偏向锁、轻量级锁、锁消除、锁粗化),现代 JDK 中synchronized在低竞争场景下性能已不逊于ReentrantLock


五、一个直观的例子:从无锁到有锁

// 场景:10000 个线程对共享变量自增// 方案1:无锁(线程不安全)staticintcount1=0;// 结果不可预测// 方案2:synchronizedstaticintcount2=0;staticsynchronizedvoidincrement2(){count2++;}// 方案3:ReentrantLockstaticintcount3=0;staticLocklock=newReentrantLock();staticvoidincrement3(){lock.lock();try{count3++;}finally{lock.unlock();}}// 方案4:AtomicInteger(无锁 CAS)staticAtomicIntegercount4=newAtomicInteger(0);// count4.incrementAndGet();

性能表现(粗略,取决于线程数和竞争程度):

低竞争: CAS ≈ synchronized ≈ ReentrantLock 高竞争: synchronized(重量级) < ReentrantLock < CAS(但CAS可能导致自旋开销) 读多写少: ReadWriteLock > ReentrantLock > synchronized

六、底层基石:对象头与 Mark Word

理解 Java 锁必须理解对象的内存布局。一个 Java 对象在堆内存中包含:

+------------------+ ← 对象起始 | Mark Word | 8 字节(32位JVM)/ 8 字节(64位JVM 压缩后) +------------------+ | Klass Pointer | 4/8 字节(指向类元数据) +------------------+ | 实例数据 | (按需) +------------------+ | 对齐填充 | +------------------+

Mark Word是锁状态的核心存储区,它的结构随锁状态变化:

64位JVM Mark Word 布局: 无锁 (Normal): | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | 偏向锁 (Biased): | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | 轻量级锁 (Lightweight Locked): | ptr_to_lock_record:62 | 00 | 重量级锁 (Heavyweight Locked): | ptr_to_monitor:62 | 10 | GC 标记 (Marked for GC): | 11 |

关键观察:Mark Word 在无锁时存储对象的 identity hashcode 和 GC 分代年龄;一旦加锁,这些信息会被覆盖并暂存到其他地方。这就是为什么重写了hashCode()的对象无法使用偏向锁——因为偏向锁需要利用 Mark Word 中原本存储 hashcode 的位置来存储线程 ID。


七、锁升级路径(synchronized 的核心)

synchronized在 JDK 6 之后采用了锁升级机制,这是其性能大幅提升的根本原因:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁 (只升不降,但GC后可能恢复到无锁)
轻量级锁失败 无锁 ──[单线程]──→ 偏向锁 ──[多线程交替]──→ 轻量级锁 ──[多线程竞争]──→ 重量级锁 ↓ ↓ 撤销偏向锁 CAS自旋失败 (需在安全点) (线程挂起/阻塞)