1. 为什么当初要这样设计?(设计初衷)
Java 诞生于 20 世纪 90 年代中期,当时多线程编程(并发)正在兴起。Java 的设计者高斯林(James Gosling)希望 Java 成为一门简单易用的多线程语言。
简化并发编程
如果对象没有内置锁,你每次想给一段代码加锁,都必须显式地创建一个 Lock 对象:
// 如果没有内置锁,你得这么写:
Lock myLock = new ReentrantLock();
void doSomething() {
myLock.lock();
try {
// 业务逻辑
} finally {
myLock.unlock();
}
}
而 Java 设计者引入了 synchronized 关键字,直接让对象充当锁,把复杂的同步简化成了:
// 极简的同步写法
synchronized(this) {
// 业务逻辑
}
“万物皆对象,万物皆可为锁”的设计,让开发者不需要管理一堆乱七八糟的锁对象,只要拿到目标对象,就能直接进行同步控制。
2. 这样不浪费内存吗?JVM 是怎么“偷懒”的?
你担心的内存浪费,JVM 工程师早就想到了。他们绝对不会傻到给每个刚 new 出来的对象都分配一个沉重的操作系统级别的锁(Monitor)。
JVM 解决这个问题的核心思路是:按需分配,动态升级。
秘密武器:对象头(Object Header)
在 Java 中,每个对象的内存结构里都有一个叫 Mark Word(标记字) 的区域(在 64 位虚拟机上占 8 个字节 / 64 位)。
这 8 个字节是并发的核心。它并不直接存储一个复杂的锁对象,而是像一个“多功能瑞士军刀”,根据锁的状态,复用这 64 位的存储空间:
| 锁状态 | Mark Word 存储的内容 | 解释 |
|---|---|---|
| 无锁状态 (001) | 对象的 HashCode、分代年龄等 | 绝大多数 Java 对象的一生都是这个状态。 根本没有锁,完全不浪费额外内存。 |
| 偏向锁 (101) | 持有锁的线程 ID、分代年龄 | JVM 发现只有“你”这一个线程老来拿锁,干脆直接把你的名字写在对象头上。几乎没有性能和内存开销。 |
| 轻量级锁 (00) | 指向当前线程栈中锁记录的指针 | 出现轻微竞争时,利用 CAS(自旋)把指针指向线程自己的栈,依然不需要向操作系统申请重锁。 |
| 重量级锁 (10) | 指向 Monitor 对象(管程)的指针 | 只有当激烈竞争发生时,JVM 才会真正创建一个重量级的 Monitor 锁对象,并把指针存到这里。 |
💡 总结来说: > 只要你不用
synchronized,或者没有多线程激烈的抢夺,这个对象头里存的就只是 HashCode 和垃圾回收数据。锁的属性只是这 64 位数据在特定状态下的“临时兼职”,并没有为锁单独开辟额外的内存空间。
3. 现代 Java 的反思:这种设计完美吗?
虽然 JVM 优化到了极致,但“万物皆可为锁”的设计在今天看来,确实带有时代的局限性。
- 语义模糊: 一个用来存数据的
User对象,同时还能用来当锁,这违反了“单一职责原则”。 - 现代 Java 的转向: 自 Java 5 引入
java.util.concurrent(如ReentrantLock)以来,官方更推荐显式锁。而在现代高并发开发中,大家已经很少直接在普通业务对象上用synchronized了。