一、synchronized的底层实现基础
在JVM中,synchronized的实现依赖于"对象头"和"监视器锁(Monitor)"。要理解synchronized的底层逻辑,首先需要明确两个核心概念:
-
对象头(Object Header):Java中每个对象都有一个对象头,用于存储对象的元数据信息(如哈希码、GC分代年龄、锁状态等)。synchronized的锁状态就存储在对象头的"Mark Word"部分(Mark Word是对象头的核心组成部分,占4字节或8字节,取决于JVM位数)。 Mark Word的结构会随锁状态变化而变化,核心状态包括:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
-
监视器锁(Monitor):Monitor是JVM内部的一个同步工具,也称为"管程"。每个对象在JVM中都对应一个Monitor对象,当线程尝试获取synchronized锁时,本质上是在获取该对象对应的Monitor的所有权。 Monitor的核心结构包括:
-
持有线程(Owner):当前持有Monitor的线程,初始为null;
-
等待队列(WaitSet):调用wait()方法后释放锁的线程会进入该队列;
-
阻塞队列(EntryList):未获取到锁的线程会进入该队列,等待Owner释放锁后竞争。
二、synchronized的锁升级过程(JDK 6+优化)
在JDK 6之前,synchronized是一种"重量级锁",每次获取和释放锁都需要调用操作系统的内核态指令,性能开销较大。JDK 6对synchronized进行了优化,引入了"偏向锁"和"轻量级锁",实现了"锁升级"的机制------锁会根据线程竞争的激烈程度,从无锁状态逐步升级为偏向锁、轻量级锁、重量级锁,避免了不必要的性能开销。
-
1. 无锁状态:对象刚创建时,Mark Word处于无锁状态,存储对象的哈希码、GC分代年龄等信息,此时无线程竞争。
-
2. 偏向锁(单线程竞争):当只有一个线程多次获取同一个锁时,JVM会将锁升级为偏向锁。核心逻辑是:线程第一次获取锁时,会在Mark Word中记录当前线程的ID(偏向线程ID),之后该线程再次获取锁时,无需进行CAS操作或内核态调用,直接通过判断Mark Word中的偏向线程ID是否为当前线程即可,几乎无性能开销。 触发偏向锁撤销的场景:当有其他线程尝试获取该锁时,偏向锁会被撤销,升级为轻量级锁。撤销过程需要暂停持有偏向锁的线程,开销较小。
-
3. 轻量级锁(少量线程竞争,无阻塞):当有两个或多个线程交替获取锁(竞争不激烈,无线程阻塞)时,偏向锁撤销后升级为轻量级锁。核心逻辑是:
-
线程获取锁时,会在自己的工作内存中创建一个"锁记录(Lock Record)",存储锁对象的Mark Word副本;
-
通过CAS操作将锁对象的Mark Word更新为指向当前线程的Lock Record的指针;
-
若CAS操作成功,线程获取轻量级锁;若失败(说明有其他线程竞争),则自旋重试(通过循环尝试CAS,避免阻塞)。 触发轻量级锁升级的场景:当自旋重试次数达到阈值(默认10次),或有线程在自旋过程中被阻塞时,轻量级锁会升级为重量级锁。
- 4. 重量级锁(激烈竞争,线程阻塞):当多个线程同时竞争锁,且竞争激烈(出现线程阻塞)时,轻量级锁升级为重量级锁。此时,锁的实现依赖于Monitor,线程获取锁失败后会进入Monitor的阻塞队列(EntryList),并放弃CPU执行权(阻塞状态),等待持有锁的线程释放锁后被唤醒竞争。 重量级锁的特点:性能开销大(涉及内核态调用),但能保证线程安全,适用于激烈竞争的场景。
三、synchronized的原子性实现原理
synchronized保证原子性的核心是"Monitor的独占性":当线程获取到Monitor(即获取锁)后,会独占该锁,其他线程无法获取,直到持有锁的线程执行完同步代码块并释放锁。具体过程如下:
-
线程执行同步代码前,必须先获取锁(通过CAS操作或Monitor竞争);
-
获取锁成功后,线程进入同步代码块执行逻辑,此时其他线程尝试获取锁会被阻塞(轻量级锁自旋,重量级锁进入阻塞队列);
-
线程执行完同步代码后,释放锁(更新Mark Word状态,唤醒阻塞队列中的线程)。
通过这种"独占式"的执行机制,synchronized确保了同步代码块中的所有操作会作为一个原子操作执行,不会被其他线程打断。
四、synchronized与volatile的区别
synchronized和volatile都是Java中解决并发问题的关键字,但两者的作用和实现原理差异较大,核心区别如下:
| 对比维度 | synchronized | volatile |
|---|---|---|
| 核心作用 | 保证原子性、可见性、有序性 | 仅保证可见性、有序性,不保证原子性 |
| 实现原理 | 基于对象头和Monitor,锁升级机制 | 基于内存屏障(Memory Barrier),禁止指令重排序 |
| 适用场景 | 多线程修改共享资源、复合操作 | 多线程读取共享资源、避免指令重排序(如单例模式) |
| 性能开销 | 有锁升级优化,轻竞争下开销小,激烈竞争下开销大 | 开销极小(仅内存屏障),无锁机制 |
五、底层指令层面的实现
当我们编译synchronized修饰的代码时,JVM会在字节码层面插入两个核心指令:monitorenter和monitorexit,分别对应锁的获取和释放。
-
monitorenter:线程执行到monitorenter指令时,会尝试获取当前对象对应的Monitor所有权。若Monitor的Owner为null,则当前线程成为Owner;若当前线程已为Owner,则计数器加1(可重入锁的实现基础);若Owner为其他线程,则当前线程进入阻塞队列。
-
monitorexit:线程执行到monitorexit指令时,会将Monitor的计数器减1。当计数器减为0时,释放Monitor(Owner设为null),并唤醒阻塞队列中的线程竞争锁。
需要注意的是,JVM会确保每个monitorenter指令都有对应的monitorexit指令(即使代码抛出异常,也会通过异常处理机制插入monitorexit),保证锁一定会被释放,避免死锁。