从对象结构到锁机制:Java 对象锁与类锁深度解析
Java 的并发控制中,synchronized 是最经典的内置锁机制。它基于 JVM 的对象结构实现,分为对象锁(实例锁)和类锁(静态锁)。下面从对象内存布局入手,逐步拆解锁的原理、实现和使用,帮助你建立从底层到实践的完整认知。
目标:看完后,你能自信地说“我懂 Java 锁的本质”,并能优化/调试 90% 的多线程代码问题(基于 JDK 8~21,HotSpot JVM)。
第一步:Java 对象内存结构(锁的物理基础)
Java 对象在堆内存中的布局是锁机制的基石。每个对象都有一个对象头(Object Header),其中Mark Word 存储锁信息。
| 部分 | 描述 | 大小(64位 JVM) | 锁相关作用 |
|---|---|---|---|
| Mark Word | 存储哈希码、GC 信息、锁状态 | 8 字节 | 记录锁类型(无锁/偏向/轻量/重量)、线程 ID、指针等 |
| Class Pointer | 指向 Class 对象(类型信息) | 4~8 字节 | 类锁使用 Class 对象作为锁标识 |
| 实例数据 | 字段值(如 int、引用) | 动态 | — |
| 对齐填充 | 确保 8 字节对齐 | 0~7 字节 | — |
Mark Word 的锁状态演变(synchronized 的核心):
- 无锁:正常状态,Mark Word 存哈希码。
- 偏向锁:低竞争时,偏向第一个线程(存线程 ID),无开销。
- 轻量级锁:轻微竞争,用 CAS 替换 Mark Word 为栈帧锁记录指针。
- 重量级锁:高竞争,膨胀为 Monitor 对象(互斥锁),线程阻塞/唤醒。
JVM 通过 锁升级(无锁 → 偏向 → 轻量 → 重量)优化性能。类锁类似,但锁对象是 Class(全局唯一)。
第二步:synchronized 锁机制核心(Monitor 的工作原理)
synchronized 基于 Monitor(监视器)实现:
- 进入 Monitor:获取锁(lock),失败则阻塞。
- 退出 Monitor:释放锁(unlock),唤醒等待线程。
- JVM 字节码:
monitorenter和monitorexit指令。
对象锁 vs 类锁的本质区别:
- 对象锁:锁住实例(this),每个对象独立。多实例并发不互斥。
- 类锁:锁住 Class 对象(全局单例),所有实例共享。静态方法/代码块互斥。
第三步:对象锁与类锁的详细对比
| 维度 | 对象锁(实例锁) | 类锁(静态锁) |
|---|---|---|
| 语法 | synchronized 非静态方法 / 代码块(synchronized(this) 或 synchronized(instance)) | synchronized 静态方法 / 代码块(synchronized(ClassName.class)) |
| 锁对象 | 对象实例(每个 new 出的对象独立) | Class 对象(类加载时唯一) |
| 作用范围 | 实例级:不同实例不互斥 | 类级:所有实例共享互斥 |
| 典型场景 | 保护实例变量(如账户余额) | 保护静态变量(如全局计数器) |
| 性能影响 | 竞争小:偏向/轻量锁高效 | 竞争大:全局锁,易成瓶颈 |
| 升级路径 | 同(基于 Mark Word) | 同,但 Class 的 Mark Word 全局共享 |
注意:JDK 6+ 引入偏向锁(-XX:+UseBiasedLocking,默认开),但高并发场景建议关闭(-XX:-UseBiasedLocking)以避免升级开销。
第四步:真实代码示例(强烈建议运行一遍)
public class LockDemo {
private int instanceCount = 0; // 实例变量
private static int staticCount = 0; // 静态变量
// 对象锁:非静态 synchronized 方法
public synchronized void incrementInstance() {
instanceCount++;
System.out.println(Thread.currentThread().getName() + " instance: " + instanceCount);
}
// 对象锁:synchronized 代码块
public void incrementInstanceBlock() {
synchronized (this) {
instanceCount++;
System.out.println(Thread.currentThread().getName() + " instance: " + instanceCount);
}
}
// 类锁:静态 synchronized 方法
public static synchronized void incrementStatic() {
staticCount++;
System.out.println(Thread.currentThread().getName() + " static: " + staticCount);
}
// 类锁:synchronized 代码块
public static void incrementStaticBlock() {
synchronized (LockDemo.class) {
staticCount++;
System.out.println(Thread.currentThread().getName() + " static: " + staticCount);
}
}
public static void main(String[] args) {
LockDemo demo1 = new LockDemo();
LockDemo demo2 = new LockDemo();
// 测试对象锁:demo1 和 demo2 不互斥
new Thread(() -> demo1.incrementInstance()).start();
new Thread(() -> demo2.incrementInstance()).start(); // 可能同时执行
// 测试类锁:所有线程互斥
new Thread(LockDemo::incrementStatic).start();
new Thread(LockDemo::incrementStatic).start(); // 顺序执行
}
}
输出示例(对象锁不互斥,类锁互斥):
Thread-1 instance: 1
Thread-0 instance: 1
Thread-2 static: 1
Thread-3 static: 2
第五步:最容易踩的 8 个坑与优化(企业级经验)
- 误用对象锁保护静态变量:静态变量用对象锁无效(不同实例不共享锁)。
- String 常量池锁:synchronized(“abc”) 用字符串常量作为锁,可能全局互斥(常量池共享)。
- Integer 缓存锁:synchronized(Integer.valueOf(100)) 用缓存对象,可能意外共享。
- 锁粗化/消除:JVM 优化(如循环内 synchronized 合并),但别依赖——手动优化代码。
- 死锁:A 持对象锁等类锁,B 持类锁等对象锁 → 用 jstack 诊断。
- 高并发瓶颈:类锁易成单点,用 ConcurrentHashMap / Atomic 等代替。
- ReentrantLock vs synchronized:前者更灵活(可中断、公平锁),但 synchronized 更简单/高效(JDK 优化后)。
- JDK 版本差异:JDK 21+ 虚拟线程(Project Loom)下,synchronized 更高效,但类锁仍全局。
优化建议:优先用 java.util.concurrent 包(如 ReentrantLock、Semaphore),synchronized 适合简单场景。
第六步:快速自测清单(验证掌握度)
- 对象头中哪个部分存储锁信息?(Mark Word)
- synchronized 静态方法锁的是什么?(Class 对象)
- 两个不同实例调用同一 synchronized 非静态方法,会互斥吗?(不会)
- 锁升级的顺序是什么?(无锁 → 偏向 → 轻量 → 重量)
- 如何用代码块实现类锁?(synchronized(ClassName.class))
- 为什么 String 常量不适合做锁?(常量池共享)
- synchronized 是可重入锁吗?(是,同一线程可多次获取)
- 如何查看线程锁信息?(jstack -l pid)
答案自己验证(可查 OpenJDK 源码或《Java 并发编程实战》)。如果你把代码跑通、坑避开,恭喜——Java 锁机制你已深度掌握。
有哪部分模糊(如锁升级源码、ReentrantLock 对比、虚拟线程影响、CAS 底层)?直接告诉我,我再给你针对性展开更多例子和代码。