一、Mark Word与对象头结构
理解锁的本质,要从HotSpot对象头开始。64位JVM的对象头由Mark Word(8字节)和Klass Pointer(4字节开启压缩时)组成:
// 64位JVM Mark Word 结构(8字节 = 64bit)
// 不同锁状态下,同一内存位置存储不同含义
// -------------------------------------------
// 无锁状态(hashcode未计算):
// [25bit: identity_hashcode | 2bit: age | 4bit: biased_lock | 1bit: 0]
// age=0000 (未启用分代年龄)
// 无锁状态(hashcode已计算):
// [31bit: hashcode | 2bit: unused | 4bit: age | 1bit: 0]
// hashcode是延迟计算的,首次调用System.identityHashCode时写入
//
// 偏向锁状态:
// [23bit: thread_id | 2bit: epoch | 2bit: age | 4bit: biased_lock | 1bit: 1]
// thread_id: 持有偏向锁的线程ID
// epoch: 偏向时间戳,用于批量重偏向
//
// 轻量锁状态:
// [30bit: 指向栈中锁记录的指针 | 2bit: 00]
//
// 重量锁状态:
// [30bit: 指向重量锁Monitor的指针 | 2bit: 10]
//
// GC标记状态:
// [62bit: 无意义 | 2bit: 11]
// JOL 查看对象头
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
// [B@6bc168e5] object internals:
// OFFSET SIZE TYPE DESCRIPTION
// 0 4 (object header: mark) = 0x0000000000000005 (non-biasable; age=0)
// 4 4 (object header: class) = 0x08000000
// 8 4 (object header: array) (object boundary)
// 12 4 (loss due to next object alignment)
// Instance size: 16 bytes
二、锁的四种状态与升级过程
锁只能升级不能降级(synchronized):无锁 → 偏向锁 → 轻量锁 → 重量锁。这是JVM为了减少不必要的重量级锁开销而设计的"锁膨胀"机制。
2.1 偏向锁(Biased Lock)
偏向锁的核心假设:**大多数同步块只被一个线程持有**,在第一次进入时记录线程ID,后续该线程进入时无需任何同步操作:
// 偏向锁开启(Java 6默认开启,Java 15开始废弃)
// -XX:+UseBiasedLocking
// 偏向锁延迟:JVM启动后4秒才开启偏向锁
// -XX:BiasedLockingStartupDelay=4000
// 偏向锁获取流程:
// 1. 线程A第一次进入synchronized,读取Mark Word
// 2. 判断biased_lock=0(可偏向)且epoch有效
// 3. CAS将Mark Word中的thread_id设置为A的线程ID
// 4. 设置biased_lock=1,写入epoch
// 5. 后续A线程进入synchronized只需检查thread_id,无任何CAS操作
// 偏向锁撤销:
// 场景1:另一个线程B尝试获取锁(safepoint检查)
// 场景2:显式调用wait()/notify()(重量锁才支持,偏向锁需升级)
// 撤销开销:需要等待safepoint(全局安全点,所有线程停顿)
// 批量重偏向(Bulk Rebiasing)
// epoch机制:每个class有一个epoch字段
// 当一个类的偏向锁撤销次数达到阈值(-XX:BiasedLockingBulkRebiasThreshold=20)
// JVM将该class的epoch+1,并批量将已分配的偏向锁升级为无锁
// 后续新分配的偏向锁使用新的epoch值
// 批量锁定(Bulk Revoking)
// 当撤销次数达到 -XX:BiasedLockingBulkRevokeThreshold=40
// JVM禁止该class使用偏向锁,所有新实例直接无锁
2.2 轻量锁(Lightweight Lock)
轻量锁通过CAS替代互斥量(mutex),避免用户态和内核态切换的开销。适用于**短时间轻量竞争**的场景:
// 轻量锁获取过程:
// 线程A进入synchronized(发现有竞争或偏向锁不可用)
// 1. 在自己的线程栈中分配Lock Record(锁记录)
// 2. Lock Record包含:Displaced Mark Word(原Mark Word备份)+ 对象指针
// 3. CAS将对象的Mark Word(无锁状态)替换为指向Lock Record的指针
// 4. CAS成功 → 线程A持有轻量锁
// 5. CAS失败 → 存在竞争,升级为重量锁
// 轻量锁的CAS(乐观锁)
// 线程B尝试获取轻量锁时:
// mark = object.header
// if (mark == 无锁状态) {
// if (CAS(&object.header, mark, 线程B的LockRecord指针)) {
// // 成功,线程B持有轻量锁
// } else {
// // 失败,升级重量锁
// }
// } else {
// // 已偏向或已轻量,升级重量锁
// }
// 轻量锁释放:
// 使用CAS将栈中的Displaced Mark Word写回对象头
// if (CAS(&object.header, 当前LockRecord指针, Displaced Mark Word)) {
// // 成功,无竞争
// } else {
// // 失败,说明有竞争,升级重量锁,由重量锁来释放
// }
// ⚠️ 轻量锁的ABA问题:
// 如果Mark Word被替换了两次(被其他线程持有后又释放),CAS可能误判成功
// 解决方案:Mark Word中的Lock Record指针包含线程ID,防止ABA
2.3 重量锁(Heavyweight Lock / ObjectMonitor)
重量锁依赖操作系统Mutex,线程进入时陷入内核态,等待调度器唤醒,是最"重"的锁:
// ObjectMonitor 关键数据结构
// ObjectMonitor() {
// _header = NULL;
// _count = 0; // 等待线程计数
// _waiters = 0; // 等待线程数(不含在_entryList中的)
// _owner = NULL; // 持有锁的线程
// _WaitSet = NULL; // 调用wait()的线程队列(CircularLinkList)
// _WaitSetTop = NULL;
// _entryList = NULL; // 阻塞队列(Contention List的入口)
// _next = NULL;
// }
// 进入重量锁的流程(从_EntryList排队)
// om_lock():
// // 原子操作:尝试将owner设为当前线程
// if (CAS(&_owner, NULL, current_thread)) {
// return; // 成功,当前线程持锁
// }
// // 失败,进入_EntryList自旋
// for (;;) {
// if (自旋次数 < 自旋上限 && 能够获取到锁) {
// return; // 成功
// }
// park(); // park当前线程,陷入内核,等待被唤醒
// }
// wait() 实现
// void wait() {
// _owner = NULL; // 释放锁
// _Waiters++; // 进入_WaitSet队列
// park(); // 线程阻塞
// }
// notify() 实现
// void notify() {
// if (_Waiters > 0) {
// // 从_WaitSet中唤醒一个线程
// // 唤醒的线程重新进入_EntryList竞争锁
// unpark(_WaitSetTop);
// }
// }
// ⚠️ synchronized vs ReentrantLock:
// - synchronized:JVM内置,偏向锁/轻量锁优化,编译期确定锁范围
// - ReentrantLock:API层实现,通过AQS,完全基于CAS+park,JDK层面可控(可中断、公平/非公平)
// 性能差异:Java 15后,两者差距已极小(JVM对synchronized做了大量优化)
三、锁消除与锁粗化
3.1 锁消除(Lock Elision)
JIT编译器通过逃逸分析(Escape Analysis)判断一个对象不会逃逸出方法或线程边界,对其上的同步操作进行消除:
// JIT逃逸分析 - 查看汇编证据
// 添加参数:-XX:+PrintAssembly
// -XX:CompileCommand=print,*ObjectHashCode
// 锁消除示例
public String concat(String a, String b) {
// sb是方法内部局部变量,不会逃逸出方法
// JIT可以证明:不可能有两个线程同时访问同一个sb对象
// 因此JIT会将StringBuilder的append()上的锁消除
StringBuilder sb = new StringBuilder();
sb.append(a); // JIT可能消除synchronized
sb.append(b);
return sb.toString();
}
// 逃逸分析的判断条件:
// 1. 对象作为返回值
// 2. 对象写入静态变量
// 3. 对象写入其他逃逸对象(传播性)
// 4. 对象传入Thread.start()
// JIT参数:
// -XX:+DoEscapeAnalysis 开启逃逸分析(默认)
// -XX:+EliminateLocks 开启锁消除(默认)
3.2 锁粗化(Lock Coarsening)
JIT将多次相邻的synchronized块合并为一次,以减少锁获取/释放的开销:
// 锁粗化示例
StringBuilder sb = new StringBuilder();
synchronized(this) {
sb.append(a);
} // ❌ 释放锁
synchronized(this) {
sb.append(b);
} // ❌ 释放锁
synchronized(this) {
sb.append(c);
} // ❌ 释放锁
// JIT优化后:
synchronized(this) {
sb.append(a);
sb.append(b);
sb.append(c);
} // ✅ 只获取/释放一次锁
// JIT阈值(-XX:+UnlockExperimentalVMOptions):
// -XX:LoopUnrollThreshold=N // 循环展开阈值
四、生产环境锁优化策略
// 策略1:用ConcurrentHashMap替代synchronized包裹HashMap
// ❌ 低效:整个Map加锁,高并发下串行化
Map map = new HashMap<>();
synchronized(map) {
map.put(key, value);
map.get(key);
}
// ✅ 高效:分段锁(Java 7及之前)
ConcurrentHashMap cmap = new ConcurrentHashMap<>();
// Java 8+:CAS+synchronized(Node的next指针用synchronized保护)
// 策略2:减少锁粒度
// ❌ 粗锁:对整个聚合对象加锁
synchronized(this) { updateCount(); updateTotal(); }
// ✅ 细锁:分别对子对象加锁
synchronized(countLock) { updateCount(); }
synchronized(totalLock) { updateTotal(); }
// 策略3:读写锁分离
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock(); // 多个读线程可并发
try { return cache.get(key); }
finally { rwLock.readLock().unlock(); }
rwLock.writeLock().lock(); // 写线程独占
try { cache.put(key, value); }
finally { rwLock.writeLock().unlock(); }
// 策略4:LongAdder替代AtomicLong(高并发写竞争)
AtomicLong counter = new AtomicLong();
// 高并发下所有线程竞争同一内存地址,CAS失败率极高
LongAdder adder = new LongAdder();
// LongAdder内部维护Cell数组(类似分段锁),
// 写操作分散到不同Cell,最后求和
adder.increment();
// 策略5:StampedLock(乐观读,比ReadWriteLock性能更好)
StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead(); // 获取乐观读戳
int value = cache.get(key);
if (!sl.validate(stamp)) { // 验证期间是否有写操作
stamp = sl.readLock(); // 降级为悲观读
try { value = cache.get(key); }
finally { sl.unlockRead(stamp); }
}