本文适合人群:学过 synchronized 关键字,但每次面试被问到底层总是一脸懵的同学。
看完本文你将能用人话解释清楚:锁究竟"长"在哪里?为啥会"升级"?Monitor 是个啥"保安"?


一、是什么:synchronized 的本质

很多人以为 synchronized 是 Java 语言层面的一个"功能",但实际上,它只是一个入口

真正干活的,是 JVM 底层对对象内存结构的精心设计。

锁到底"住"在哪里?

当你在 Java 里 new 出一个对象,比如:

java

复制

Object lock = new Object();

这个对象在堆内存里,其实由三部分拼成:

┌─────────────────────────────┐
│       对象头 (Object Header) │  ← synchronized 的灵魂!
├─────────────────────────────┤
│       实例数据 (Instance Data)│  ← 你定义的那些字段
├─────────────────────────────┤
│       对齐填充 (Padding)      │  ← 凑整到 8 字节的倍数(CPU 寻址用)
└─────────────────────────────┘

锁,就住在对象头里。 具体说,是对象头里那 64 bit(8字节)的区域 —— Mark Word(标记字段)

一句话总结synchronized 所谓的"加锁",本质上就是多个线程抢着去修改这个 Mark Word 里的数据


二、怎么用:三种锁状态对应三段代码

在深入原理之前,先来三段代码感受一下锁在不同场景下的"脸色变化"。

场景 1:偏向锁 —— VIP 专属通道

java

复制

public class BiasedLockDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // 只有主线程自己在反复获取锁,压根没竞争
        for (int i = 0; i < 5; i++) {
            synchronized (lock) {
                System.out.println("第 " + i + " 次:只有我一个人,VIP 通道开启");
            }
        }
    }
}
// 运行输出:
第 0 次:只有我一个人,VIP 通道开启
第 1 次:只有我一个人,VIP 通道开启
第 2 次:只有我一个人,VIP 通道开启
第 3 次:只有我一个人,VIP 通道开启
第 4 次:只有我一个人,VIP 通道开启

场景 2:轻量级锁 —— 礼貌的交接班

java

复制

public class LightweightLockDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程 1 拿到锁,执行完毕,礼貌退出");
            }
        });
        t1.start();
        t1.join(); // 等 t1 完全结束,再让 t2 上

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                // 此时偏向锁失效,升级为轻量级锁
                System.out.println("线程 2 拿到锁,平稳交接,无人阻塞");
            }
        });
        t2.start();
    }
}
// 运行输出:
线程 1 拿到锁,执行完毕,礼貌退出
线程 2 拿到锁,平稳交接,无人阻塞

场景 3:重量级锁 —— 真刀真枪的惨烈厮杀

java

复制

public class HeavyweightLockDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // 10 个线程同时冲!
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                synchronized (lock) {
                    try {
                        Thread.sleep(200); // 故意霸占锁,加剧竞争
                        System.out.println(Thread.currentThread().getName() + " 终于抢到了!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "激烈竞争线程-" + i).start();
        }
    }
}
// 运行输出(顺序不固定):
激烈竞争线程-0 终于抢到了!
激烈竞争线程-3 终于抢到了!
激烈竞争线程-7 终于抢到了!
... (依次逐个输出,其余线程老老实实排队阻塞)

三、重点细节:Mark Word 的"七十二变"与锁升级全流程

这才是本文的重头戏。Mark Word 就像一块动态电子公告板,它最低两位是"锁标志位",会根据竞争状态不断改写自己。

我们用一个"奶茶店争位子"的故事来串联全程👇

第 1 阶段:无锁状态(标志位 01,偏向位 0

刚开业的奶茶店,位子没人坐,桌上只贴着餐厅的标准介绍。

对象刚被 new 出来,Mark Word 里存的是 HashCode + 分代年龄(GC 用的),干干净净,没有任何锁信息。


第 2 阶段:偏向锁(标志位 01,偏向位 1

常客小明(线程 1)第一次来,直接把自己的名字贴到桌上:"这桌我的,以后我来不用叫号"。

底层动作:
线程 1 首次进入 synchronized 块,JVM 把线程 1 的 Thread ID 直接"刻"进这个对象的 Mark Word 里。
下次线程 1 再来,扫一眼标签是自己的,直接免检入场,连 CAS 都不做,性能极高。

Mark Word (偏向锁状态):
[ Thread ID (54bit) | epoch (2bit) | 分代年龄 (4bit) | 偏向标记=1 | 锁标志位=01 ]

第 3 阶段:轻量级锁(标志位 00

小红(线程 2)也来了,看到桌上贴着"小明",偏向锁被打破!
两人约定用"抢凳子"游戏决定谁坐 —— 谁先把凳子搬到自己座位旁边(CAS 成功),谁就算赢,输的人原地踏步等一会儿。

底层动作(请结合栈帧知识理解!):

  1. JVM 在当前抢锁线程的栈帧里开辟一块小空间 —— 锁记录(Lock Record),先把 Mark Word 的副本拷进去。
  2. 线程用 CAS(比较并交换) 操作,尝试把对象头里的 Mark Word 整个替换成一个指向自己栈帧 Lock Record 的指针。
  3. 谁 CAS 成功,谁就拿到轻量级锁;没成功的线程就执行 自旋(while 循环空转),等一会儿再抢。
Mark Word (轻量级锁状态):
[ 指向线程栈帧中 Lock Record 的指针 (62bit) | 锁标志位=00 ]

💡 关键点:这整个过程都在用户态完成,没有操作系统介入,没有线程切换,速度依然很快。


第 4 阶段:重量级锁(标志位 10

小红原地踏步转了 10 圈(自旋次数耗尽),还有小李、小王、小赵……一堆人都来了,全程空转 CPU 实在太浪费了。
此时店长出手,呼叫 "保安室"(Monitor),强制让所有抢锁失败的人坐到等候区排队,释放 CPU 资源,有空位再叫号。

触发条件:自旋超过阈值(默认约 10 次,或并发抢锁线程过多)

底层动作
Mark Word 被再次改写,这次变成了一个指向 Monitor 对象的指针

Mark Word (重量级锁状态):
[ 指向 Monitor 对象的指针 (62bit) | 锁标志位=10 ]

四、终极 Boss:Monitor 到底是个什么"保安室"?

Monitor(监视器)是 JVM 底层用 C++ 实现的数据结构,叫做 ObjectMonitor。这才是 synchronized 性能差的根本原因 —— 它需要操作系统介入,触发内核态切换

你可以把 Monitor 想象成一个极其严格的保安室,它有三个核心区域:

┌──────────────────────────────────────────┐
│              Monitor 保安室               │
│                                          │
│  👑 _owner(VIP 包厢)                   │
│     → 当前持有锁的那个线程               │
│                                          │
│  😤 _EntryList(阻塞排队区)             │
│     → 来晚了、CAS 失败的线程             │
│     → 被操作系统强制剥夺 CPU,进入        │
│       BLOCKED 状态,老老实实等叫号        │
│                                          │
│  😴 _WaitSet(等待休息区)               │
│     → 调用了 lock.wait() 的线程          │
│     → 主动放弃锁,进入 WAITING 状态      │
│     → 等别人 notify() 唤醒,再回         │
│       EntryList 重新参与竞争             │
└──────────────────────────────────────────┘

三者与线程状态的对应关系

Monitor 区域 线程状态 如何进入 如何离开
_owner RUNNABLE(运行中) 抢锁成功 执行完毕释放锁
_EntryList BLOCKED(阻塞) 抢锁失败,OS 强制挂起 锁释放后被唤醒,重新竞争
_WaitSet WAITING(等待) 主动调用 lock.wait() 其他线程调用 notify()/notifyAll()

💡 BLOCKED 和 WAITING 的本质区别是什么?
— BLOCKED 是被动的,是操作系统强行踢出去的;
— WAITING 是主动的,是线程自己调用 wait() 躺平的。


五、对比 / 总结:一张图看透锁升级全流程

三种锁性能对比

锁状态 适用场景 是否需要 OS 介入 性能
偏向锁 单线程重复进入 ❌ 不需要 ⚡⚡⚡ 极高
轻量级锁 多线程交替执行,无真实并发 ❌ 不需要 ⚡⚡ 较高
重量级锁 多线程同时激烈竞争 ✅ 需要,内核态切换 ⚡ 较低

最后送你一句话synchronized 不是洪水猛兽,理解了它的升级机制,你才能在代码设计时做出真正明智的选择——什么时候用它,什么时候换 ReentrantLock,心里自然有数。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐