作为Java中最常用的同步机制,synchronized背后的实现原理和优化策略值得深入理解。本文将从底层实现到高级特性,全面解析synchronized的锁机制。
文章目录
- [1. synchronized实现原理揭秘](#1. synchronized实现原理揭秘)
-
- [1.1 同步的基本概念](#1.1 同步的基本概念)
- [1.2 Monitor机制核心原理](#1.2 Monitor机制核心原理)
- [1.3 字节码层面分析](#1.3 字节码层面分析)
- [2. 锁的膨胀升级全过程](#2. 锁的膨胀升级全过程)
-
- [2.1 对象头与锁状态](#2.1 对象头与锁状态)
- [2.2 完整的锁升级流程](#2.2 完整的锁升级流程)
- [3. synchronized如何保证三大特性](#3. synchronized如何保证三大特性)
-
- [3.1 原子性(Atomicity)](#3.1 原子性(Atomicity))
- [3.2 可见性(Visibility)](#3.2 可见性(Visibility))
- [3.3 有序性(Ordering)](#3.3 有序性(Ordering))
- [4. synchronized vs ReentrantLock深度对比](#4. synchronized vs ReentrantLock深度对比)
-
- [4.1 特性对比表格](#4.1 特性对比表格)
- [4.2 使用场景对比](#4.2 使用场景对比)
- [5. 重量级锁的价值与代价](#5. 重量级锁的价值与代价)
-
- [5.1 为什么需要重量级锁?](#5.1 为什么需要重量级锁?)
- [5.2 性能权衡分析](#5.2 性能权衡分析)
- [6. 锁优化技术详解](#6. 锁优化技术详解)
-
- [6.1 锁粗化(Lock Coarsening)](#6.1 锁粗化(Lock Coarsening))
- [6.2 锁消除(Lock Elimination)](#6.2 锁消除(Lock Elimination))
- [6.3 自适应自旋(Adaptive Spinning)](#6.3 自适应自旋(Adaptive Spinning))
- [7. 常见问题深度解答](#7. 常见问题深度解答)
-
- [7.1 synchronized的锁升级过程有几次自旋?](#7.1 synchronized的锁升级过程有几次自旋?)
- [7.2 synchronized锁的是什么?](#7.2 synchronized锁的是什么?)
- [7.3 synchronized的锁能降级吗?](#7.3 synchronized的锁能降级吗?)
- [7.4 synchronized是非公平锁吗?如何体现?](#7.4 synchronized是非公平锁吗?如何体现?)
1. synchronized实现原理揭秘
1.1 同步的基本概念
在多线程编程中,当多个线程同时访问共享、可变 的临界资源时,需要采用同步机制来保证线程安全。synchronized作为Java内置的同步机制,通过对临界资源进行序列化访问来解决并发问题。
1.2 Monitor机制核心原理
synchronized基于JVM的Monitor(监视器锁) 实现,每个Java对象都关联一个Monitor对象:
java
// HotSpot虚拟机中ObjectMonitor的核心数据结构(C++实现)
ObjectMonitor() {
_count = 0; // 记录线程进入锁的次数
_owner = NULL; // 指向持有锁的线程
_WaitSet = NULL; // 处于wait状态的线程队列
_EntryList = NULL; // 处于等待锁block状态的线程队列
_recursions = 0; // 锁的重入次数
}
Monitor工作流程:
- 线程进入同步代码时,尝试通过
monitorenter指令获取Monitor所有权 - 获取成功则设置_owner为当前线程,_count计数器加1
- 如果获取失败,线程进入_EntryList等待
- 线程执行完同步代码后,通过
monitorexit指令释放锁,_count减1
1.3 字节码层面分析
同步代码块被编译为字节码后,会生成对应的monitorenter和monitorexit指令:
java
public void syncMethod() {
synchronized(this) {
System.out.println("同步代码块");
}
}
// 对应的字节码:
// monitorenter // 进入同步块
// ... 业务逻辑代码
// monitorexit // 正常退出
// monitorexit // 异常退出 - 保证在异常情况下也能释放锁
而同步方法则通过方法访问标志ACC_SYNCHRONIZED来实现:
java
public synchronized void syncMethod() {
System.out.println("同步方法");
}
// 方法flags中包含ACC_SYNCHRONIZED标志
2. 锁的膨胀升级全过程
2.1 对象头与锁状态
锁状态信息存储在对象的Mark Word中,HotSpot虚拟机的对象内存布局如下:
对象内存布局 对象头 Header 实例数据 Instance Data 对齐填充 Padding Mark Word 类型指针 数组长度 锁状态信息 哈希码 GC分代年龄
32位虚拟机的Mark Word结构:
| 锁状态 | 25bit | 4bit | 1bit | 2bit |
|---|---|---|---|---|
| 无锁态 | 对象的hashCode | 分代年龄 | 0 | 01 |
| 偏向锁 | 线程ID + Epoch | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | ||
| 重量级锁 | 指向Monitor的指针 | 10 | ||
| GC标记 | 空 | 11 |
2.2 完整的锁升级流程
无锁状态 偏向锁 轻量级锁 重量级锁 撤销偏向锁 自旋优化
步骤1:无锁 → 偏向锁
- 当第一个线程访问同步块时,检查对象Mark Word中的偏向锁标识
- 如果支持偏向锁(默认开启),CAS操作将线程ID记录到Mark Word
- 成功则进入偏向模式,后续该线程进入同步块无需同步操作
步骤2:偏向锁 → 轻量级锁
- 当另一个线程尝试获取锁时,检查持有偏向锁的线程是否存活
- 如果原线程已不存活,撤销偏向锁到无锁状态,重新竞争
- 如果原线程仍存活,检查是否还需要继续持有锁
- 发生竞争时,偏向锁升级为轻量级锁
步骤3:轻量级锁 → 重量级锁
- 轻量级锁通过CAS自旋尝试获取锁
- 如果自旋超过一定次数(JDK6之前默认10次,JDK6引入自适应自旋)仍未获取到锁
- 或者有第三个线程参与竞争,轻量级锁升级为重量级锁
3. synchronized如何保证三大特性
3.1 原子性(Atomicity)
synchronized通过Monitor的互斥特性保证原子性:
java
public class AtomicExample {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized(lock) {
count++; // 这个操作在同步块内是原子的
}
}
}
原理分析:
monitorenter和monitorexit指令确保同步块内的所有操作作为一个不可分割的整体执行- 同一时刻只有一个线程能够持有Monitor锁
3.2 可见性(Visibility)
synchronized通过内存屏障和happens-before原则保证可见性:
java
public class VisibilityExample {
private boolean flag = false;
private final Object lock = new Object();
public void writer() {
synchronized(lock) {
flag = true; // 对后续获取同一个锁的线程可见
}
}
public void reader() {
synchronized(lock) {
if(flag) {
// 一定能看到writer线程的修改
System.out.println("Flag is true");
}
}
}
}
实现机制:
- 线程释放锁时,JMM强制将工作内存中的修改刷新到主内存
- 线程获取锁时,JMM使该线程的工作内存无效,从主内存重新加载变量
3.3 有序性(Ordering)
synchronized通过限制指令重排序来保证有序性:
java
public class OrderingExample {
private int a = 0;
private boolean initialized = false;
private final Object lock = new Object();
public void init() {
synchronized(lock) {
a = 1; // 不会重排序到initialized = true之后
initialized = true;
}
}
}
有序性保证:
- 同步块内的指令不会重排序到同步块之外
- 不同线程按照获取锁的顺序执行同步代码,建立执行顺序的约束
4. synchronized vs ReentrantLock深度对比
4.1 特性对比表格
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现机制 | JVM内置,基于Monitor | JDK实现,基于AQS |
| 锁的获取 | 隐式获取释放 | 显式lock()/unlock() |
| 可中断性 | 不支持 | 支持lockInterruptibly() |
| 超时机制 | 不支持 | 支持tryLock(timeout) |
| 公平性 | 非公平锁 | 可选择公平/非公平 |
| 条件变量 | 一个Condition | 多个Condition |
| 性能 | JDK6后优化,与ReentrantLock相当 | 稳定高效 |
| 代码复杂度 | 简单,自动释放锁 | 复杂,需要手动释放 |
4.2 使用场景对比
synchronized适用场景:
java
// 简单的同步需求,代码简洁
public class SimpleCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
ReentrantLock适用场景:
java
// 复杂的同步需求,需要高级特性
public class AdvancedCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
private final Condition notZero = lock.newCondition();
public void increment() {
lock.lock();
try {
count++;
notZero.signalAll(); // 精确唤醒等待条件线程
} finally {
lock.unlock();
}
}
}
5. 重量级锁的价值与代价
5.1 为什么需要重量级锁?
尽管重量级锁性能较低,但在高竞争场景下仍然必要:
轻量级锁的局限性:
- 自旋锁消耗CPU资源,长时间自旋得不偿失
- 多个线程竞争时,CAS操作成功率急剧下降
- 线程数超过CPU核心数时,自旋策略失效
重量级锁的优势:
java
// 高竞争场景下,重量级锁通过线程挂起避免CPU空转
public class HighContentionExample {
private final Object lock = new Object();
public void highContentionMethod() {
synchronized(lock) {
// 在100个线程竞争的场景下
// 重量级锁通过线程排队,避免99个线程空转消耗CPU
doSomething();
}
}
}
5.2 性能权衡分析
低竞争场景性能对比:
- 偏向锁/轻量级锁:CAS操作,用户态完成,性能极高
- 重量级锁:系统调用,用户态/内核态切换,性能较低
高竞争场景性能对比:
- 偏向锁/轻量级锁:大量CAS失败和自旋,CPU资源浪费
- 重量级锁:线程挂起等待,CPU资源有效利用
6. 锁优化技术详解
6.1 锁粗化(Lock Coarsening)
java
// 优化前:多次锁申请释放
public void beforeOptimization() {
for(int i = 0; i < 1000; i++) {
synchronized(lock) {
// 少量操作
}
}
}
// 优化后:一次锁申请释放
public void afterOptimization() {
synchronized(lock) {
for(int i = 0; i < 1000; i++) {
// 合并后的操作
}
}
}
6.2 锁消除(Lock Elimination)
基于逃逸分析的锁优化:
java
// 这个StringBuffer不会被其他线程访问,JVM会消除锁
public String concat(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1); // append是同步方法,但锁会被消除
sb.append(s2);
sb.append(s3);
return sb.toString();
}
开启锁消除参数:-XX:+EliminateLocks
6.3 自适应自旋(Adaptive Spinning)
JDK6引入的自适应自旋优化:
- 根据之前自旋的成功率动态调整自旋次数
- 如果之前自旋很少成功,则减少自旋次数
- 如果之前自旋经常成功,则增加自旋次数
7. 常见问题深度解答
7.1 synchronized的锁升级过程有几次自旋?
在轻量级锁阶段,线程会进行自旋尝试获取锁。JDK6之前自旋次数固定(默认10次),JDK6引入自适应自旋:
- 自旋次数不再固定,由JVM根据监控数据动态决定
- 如果之前自旋成功获取锁,则增加自旋次数
- 如果很少自旋成功,则可能直接升级为重量级锁
7.2 synchronized锁的是什么?
synchronized锁的是对象,而不是代码或引用:
java
public class LockTargetExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized(lock1) { // 锁的是lock1对象
// ...
}
}
public void method2() {
synchronized(lock2) { // 锁的是lock2对象,与method1不互斥
// ...
}
}
}
7.3 synchronized的锁能降级吗?
不能降级。锁升级是单向过程:
- 无锁 → 偏向锁 → 轻量级锁 → 重量级锁
- 设计初衷是为了优化性能,降级带来的收益有限且实现复杂
7.4 synchronized是非公平锁吗?如何体现?
是的,synchronized是非公平锁,体现在:
java
public class FairnessExample {
private final Object lock = new Object();
public void demonstrateUnfairness() {
// 线程A、B、C都在等待锁
// 线程A释放锁后,新来的线程D可能比等待中的B、C先获取到锁
synchronized(lock) {
// 新线程可以"插队"获取锁
}
}
}
非公平锁的优势:
- 减少线程切换开销,提高吞吐量
- 避免线程唤醒的延迟