Java 锁机制深度解析(系列一):总览与核心概念
Java 锁机制深度解析(系列一):总览与核心概念
前言
在并发编程中,锁是协调多线程对共享资源访问的核心手段。Java 提供了丰富的锁机制,从最底层的synchronized到 JUC(java.util.concurrent)包中的各类显式锁,形成了一个层次分明的锁体系。
一、锁的分类体系
从不同维度来看,Java 中的锁可以划分为多个类别:
按粒度
| 维度 | 粗粒度锁 | 细粒度锁 |
|---|---|---|
| 锁范围 | 大(整个对象/类) | 小(分段/行) |
| 并发度 | 低 | 高 |
| 实现复杂度 | 简单 | 复杂 |
| 典型代表 | synchronized(类.class) | ConcurrentHashMap 分段锁 |
| 适用场景 | 低频访问、简单同步 | 高频并发、高性能需求 |
按性质
| 维度 | 隐式锁(synchronized) | 显式锁(Lock) |
|---|---|---|
| 管理方式 | JVM 自动加锁/解锁 | 开发者手动 lock() / unlock() |
| 灵活性 | 低,固定语法 | 高,支持超时、中断、尝试 |
| 锁类型 | 内置互斥锁 | 多种实现(互斥、读写、乐观读) |
| 性能 | 经过持续优化(偏向锁等) | 取决于具体实现 |
| 适用场景 | 简单同步需求 | 复杂并发控制 |
按竞争策略
公平锁 vs 非公平锁
| 维度 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取策略 | FIFO 先来后到 | 允许插队 |
| 饥饿 | 无饥饿 ✅ | 可能导致饥饿 ❌ |
| 吞吐量 | 较低 | 更高 ✅ |
| 典型实现 | ReentrantLock(true) | synchronized、ReentrantLock(false) |
可重入锁 vs 不可重入锁
| 维度 | 可重入锁 | 不可重入锁 |
|---|---|---|
| 同一线程重复获取 | 允许 ✅ | 不允许 ❌(死锁) |
| 使用复杂度 | 简单 | 复杂,需自行避免重入 |
| 典型实现 | ReentrantLock、synchronized | 自定义简单锁 |
| 实际应用 | 几乎全部使用 | 很少使用 |
按状态(JVM 内部)
这是理解synchronized性能的关键:
| 锁状态 | 标志位 | 适用场景 | Mark Word 存储 |
|---|---|---|---|
| 无锁 | 01 | 无竞争 | hashCode、分代年龄 |
| 偏向锁 | 01 | 单一线程反复获取 | 线程 ID |
| 轻量级锁 | 00 | 少量竞争,CAS 自旋 | Lock Record 指针 |
| 重量级锁 | 10 | 多线程激烈竞争 | Monitor 指针 |
二、核心术语速览
| 术语 | 说明 | 示例 |
|---|---|---|
| CAS | Compare-And-Swap,无锁原子操作 | AtomicInteger.incrementAndGet() |
| 自旋 | 线程不挂起,循环尝试获取锁 | 轻量级锁的获取方式 |
| 锁消除 | JIT 编译时发现锁对象不会逃逸,移除锁 | StringBuffer在方法内使用时 |
| 锁粗化 | JIT 将相邻的加锁/解锁合并 | 循环内反复加锁的场景 |
| 管程(Monitor) | 操作系统中管理线程同步的机制,Java 中每个对象关联一个 Monitor | synchronized的底层实现 |
| AQS | AbstractQueuedSynchronizer,JUC 锁的基石 | ReentrantLock、CountDownLatch 等 |
三、如何选择锁?
实际开发中的选择原则:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 方法级简单同步 | synchronized | 代码简洁,JVM 自动优化 |
| 需要超时/中断 | ReentrantLock | 提供tryLock(timeout)、lockInterruptibly() |
| 读多写少 | ReadWriteLock或StampedLock | 读读不互斥,大幅提升并发度 |
| 极致读性能 | StampedLock乐观读 | 乐观读完全不加锁,仅事后校验 |
| 单个变量更新 | Atomic*类 | CAS 无锁,性能优于锁 |
| 集合类并发访问 | ConcurrentHashMap、CopyOnWriteArrayList | 内部已实现高效分段锁或写时复制 |
四、性能对比(概览)
以下为 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自旋失败 (需在安全点) (线程挂起/阻塞)