Java 锁相关详解【二、synchronized:Java 内置锁的全方位解析】
synchronized:Java 内置锁的全方位解析
在 Java 并发编程领域,synchronized 是最基础、最经典的同步手段。它伴随 Java 从诞生到如今,经历了从“性能低下”到“高度优化”的演变。本文将系统地拆解 synchronized 的语义、实现机制、性能优化与实践问题,帮助读者对这一核心锁机制形成全面、深入的理解。
一、synchronized 的语义与用法
1.1 基本语义
synchronized 关键字实现了 互斥(Mutual Exclusion) 和 可见性(Visibility),是 Java 提供的 内置锁(Intrinsic Lock / Monitor Lock)。
其作用可以归纳为:
- 互斥访问:保证同一时刻只有一个线程进入临界区。
- 内存语义:保证进入和退出临界区的线程在内存可见性上符合 JMM(Java 内存模型)的要求。
1.2 使用方式
-
修饰实例方法:锁住当前实例对象。
public synchronized void increment() { count++; } -
修饰静态方法:锁住当前类的 Class 对象。
public static synchronized void log() { System.out.println("Log..."); } -
修饰代码块:锁住指定对象引用。
synchronized (lock) { list.add("item"); }
注意:锁的真正粒度是 对象引用,而不是代码块本身。每个对象都可以作为一把锁。
二、JVM 层实现原理
2.1 字节码指令
synchronized 通过字节码中的 monitorenter 与 monitorexit 指令实现:
monitorenter:线程请求获取锁。monitorexit:线程释放锁。
例如:
synchronized (this) {
count++;
}
编译后的字节码(简化):
monitorenter
iload_1
iinc
istore_1
monitorexit
JVM 会保证 monitorexit 一定执行,即使方法抛出异常,否则会导致锁泄漏。
2.2 对象头(Mark Word)
在 HotSpot 虚拟机中,每个对象都有一个对象头,其中 Mark Word 用来存储锁相关状态。
| 锁状态 | Mark Word 内容(32位为例) |
|---|---|
| 无锁 | 哈希码、GC 分代年龄等 |
| 偏向锁 | 线程 ID、epoch 时间戳 |
| 轻量级锁 | 指向栈中 Lock Record 的指针 |
| 重量级锁 | 指向 Monitor 对象的指针 |
2.3 Monitor 机制
当锁升级到重量级锁时,JVM 会使用 ObjectMonitor 来管理:
- EntryList:等待获取锁的线程队列。
- WaitSet:执行
wait()后等待唤醒的线程队列。
Monitor 底层依赖操作系统的 互斥量(mutex)和条件变量(condvar),涉及用户态 ↔ 内核态切换,开销较大。
三、锁的升级过程
JDK 1.6 引入了 锁升级(Lock Escalation) 机制,通过多层锁状态减少无谓的开销。
3.1 无锁(Unlocked)
- 初始状态,没有任何竞争。
3.2 偏向锁(Biased Locking)
- 对象第一次被某线程获取时,将 Mark Word 绑定该线程 ID。
- 同一线程再次进入临界区时,无需 CAS 竞争,性能极高。
- 当有其他线程竞争时,偏向锁会撤销,升级为轻量级锁。
3.3 轻量级锁(Lightweight Locking)
- JVM 在栈上创建 Lock Record,尝试用 CAS 把对象头指向它。
- 如果 CAS 成功,获得锁;若失败则自旋尝试。
- 若自旋失败(竞争激烈),升级为重量级锁。
3.4 重量级锁(Heavyweight Locking)
- 通过 ObjectMonitor 管理,线程进入阻塞/唤醒。
- 保证安全,但性能最差。
四、内存语义
synchronized 在 JMM 中具备明确的内存语义:
-
加锁(monitorenter):
- 清空工作内存,强制从主内存中读取共享变量的最新值。
-
解锁(monitorexit):
- 将工作内存中的变量刷新到主内存,保证其他线程可见。
这意味着 synchronized 同时提供:
- 原子性(互斥访问)
- 可见性(写入刷新、读取加载)
- 有序性(禁止指令重排序)
例如,在 DCL(双重检查锁定单例模式)中,若没有 synchronized 的内存语义,可能会出现“指令重排导致未初始化对象被访问”的问题。
五、JVM 优化策略
现代 JDK 已经对 synchronized 做了深度优化,使其性能不再是瓶颈。
-
锁消除(Lock Elimination)
JIT 通过逃逸分析,消除只在当前线程可见的锁。public void foo() { Object o = new Object(); synchronized (o) { // 没有线程间共享,锁会被优化掉 } } -
锁粗化(Lock Coarsening)
多次短小的加锁操作会被合并成一次更大范围的加锁,减少频繁进入/退出锁的开销。 -
自旋锁与自适应自旋
- 在轻量级锁下,CAS 失败时会先自旋几次而不是立刻阻塞。
- JVM 根据历史情况自适应调整自旋次数,避免浪费 CPU。
-
偏向锁延迟
- JVM 在应用启动几秒后再开启偏向锁,以避免冷启动阶段频繁撤销。
六、常见问题与实践
6.1 死锁
synchronized (lockA) {
synchronized (lockB) {
// ...
}
}
若另一个线程反向加锁,会导致死锁。
解决方案:
- 保证获取锁顺序一致。
- 使用可定时锁(
ReentrantLock.tryLock())。
6.2 锁竞争
- 临界区过大或写操作过多,可能严重影响性能。
优化思路:
- 减少锁粒度(例如分段锁)。
- 使用并发容器替代传统集合。
- 使用
ReadWriteLock或StampedLock提升读性能。
6.3 工程建议
- 优先使用
synchronized(JDK 1.6+ 性能足够,语义简洁)。 - 避免锁对象的可变性(推荐用
final Object lock = new Object();)。 - 临界区要尽量短小,避免长时间持锁。
- 对于复杂同步需求(如可中断、公平性),考虑使用 JUC 显式锁。
七、总结
synchronized是 Java 并发的基石,提供 原子性、可见性、有序性 的保障。- 它依赖 字节码指令(monitorenter/monitorexit)+ 对象头 Mark Word + Monitor 实现。
- 通过 偏向锁 → 轻量级锁 → 重量级锁 的升级机制,JVM 尽量减少锁竞争带来的开销。
- 随着 JDK 的优化,
synchronized在现代应用中完全可以胜任高并发场景。
换句话说,
synchronized已经从“性能差”标签中走出,成为 语义清晰 + 高度优化 + 安全可靠 的同步工具。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120849

浙公网安备 33010602011771号