深入理解 synchronized:锁升级全解析
深入理解 synchronized:锁升级全解析
信不信我花几分钟时间就能让你彻底搞懂 synchronized
?很多人一听面试官问 synchronized
,绞尽脑汁也只记得几个词:什么无锁、偏向锁、轻量级锁、重量级锁,却根本说不清为什么要做锁升级,为什么要有偏向锁、轻量级锁,甚至连“锁监视器”是什么都不清楚。
简历上写着“精通 Java、高并发编程”,但若不能清晰讲出这些概念,那就是纸上谈兵。那么想讲清楚 synchronized
,你必须从 计算机体系结构的发展史 说起。
一、为什么要有 synchronized?
计算机发展过程中,CPU 的速度越来越快,但内存的速度相对较慢。为了缓解 CPU 和内存速度不一致的问题,产生了 多级缓存(L1、L2、L3):
- L1 和 L2 缓存是 CPU 核心私有的;
- L3 缓存是多个核心共享的。
那么问题来了:
在 多线程并发环境下,线程 A 运行在某个 CPU 核心上,把内存的数据读到缓存中并修改,但还没同步到主存;而线程 B 从主存读数据,就会读到旧值。这就是著名的 可见性问题。
此外,还有 指令重排序问题:为了性能,CPU 或编译器可能会重排代码执行顺序,导致实际运行和代码书写顺序不一致。这是 有序性问题。
当然,还有最常见的 原子性问题:多个线程同时修改同一数据,数据就可能错乱。
二、synchronized 是什么?能解决什么问题?
synchronized
是 Java 中的一种 内置锁机制,本质上就是一把 互斥锁(Monitor),它能解决以下三个问题:
- 原子性:通过互斥保证同一时刻只有一个线程能执行被 synchronized 修饰的代码;
- 可见性:加锁时(
monitorenter
)会使用 读屏障 强制从主存读取数据,保证数据是最新的;解锁时(monitorexit
)会使用 写屏障,强制将 CPU 缓存刷新到主存; - 有序性:通过内存屏障防止指令重排序。
三、synchronized 的使用方式
- 修饰普通方法:锁的是当前对象
this
- 修饰静态方法:锁的是
类.class
- 修饰代码块:锁的是括号中指定的对象
四、为什么要做锁优化?锁升级?
synchronized
最初的问题就是 慢,原因是加锁过程涉及到 操作系统原语 Mutex(互斥锁),它本质上会触发 用户态和内核态的切换,线程阻塞和唤醒开销非常大。
Java 线程模型是一对一的:每一个 Java 线程都直接对应一个 OS 内核级线程。
为了避免频繁的线程阻塞、唤醒,JDK1.6 进行了锁优化,引入了 锁升级机制:
1 | 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 |
为什么要这样做?因为大多数情况下锁竞争并不激烈,比如凌晨、系统空闲时间,大多数只有一个线程在执行。锁竞争激烈往往发生在固定高峰时段,比如中午、周末等。
所以为了解决低并发下获取锁代价高的问题,JVM 引入了不同状态的锁,根据实际竞争情况动态升级。
五、锁升级机制详解
1. 无锁状态
对象最开始处于无锁状态,MarkWord 中没有加锁标志。
2. 偏向锁
- 第一个线程进入同步块时,JVM 将对象头的 MarkWord 标记为偏向锁状态,并记录线程 ID;
- 之后该线程进入同步块时无需额外操作,性能极高。
偏向锁适用于“只有一个线程访问同步块”的场景。
3. 轻量级锁
- 当第二个线程尝试获取锁时,偏向锁会升级为轻量级锁;
- 线程通过 自旋(CAS) 的方式不断尝试获取锁,而不是立刻阻塞。
适用于锁竞争不激烈,线程持锁时间短的情况。
4. 重量级锁
- 如果自旋一定次数后仍无法获取锁,或有大量线程竞争,则升级为重量级锁;
- 此时线程会被阻塞,由操作系统管理锁状态。
重量级锁是性能最差的锁,但能应对激烈竞争。
六、为什么要有锁监视器?
当锁升级为重量级锁时,对象头中的 MarkWord 会指向一个 Monitor(锁监视器),这是 JVM 管理锁的核心结构。
Monitor 结构中包含:
owner
:记录当前持锁线程;recursion count
:记录锁的重入次数(可重入锁实现基础);EntryList(锁池)
:等待获取锁的线程集合(阻塞状态,Blocking);WaitSet(等待池)
:调用wait()
后进入等待状态的线程集合(Waiting)。
锁池用于处理互斥问题,等待池用于线程通信问题。两者目标不同,管理方式自然也不同。
七、synchronized 的锁升级过程总结
锁类型 | 特点 | 适用场景 |
---|---|---|
偏向锁 | 记录线程 ID,无竞争快速获取 | 单线程访问 |
轻量级锁 | CAS 自旋,避免阻塞 | 低并发,短时间竞争 |
重量级锁 | 阻塞线程,性能低 | 高并发激烈竞争 |