一、synchronized概述 #
synchronized
是Java语言中用于控制并发访问的关键字,它是一种排他锁(独占锁)和可重入锁。作为Java内置的同步机制,它能够确保在同一时刻只有一个线程可以访问被保护的代码块或方法。
主要特性: #
- 排他性:同一时间只允许一个线程持有锁
- 可重入性:已经持有锁的线程可以重复获取同一把锁
- 原子性:确保被保护的代码块作为一个不可分割的单元执行
- 可见性:保证锁释放前对共享变量的修改对其他线程可见
二、为什么需要synchronized #
随着计算机硬件的发展,多核处理器成为标配,操作系统也支持多线程编程。Java作为一门现代编程语言,自然需要提供简单有效的手段来协调多线程间的协作。
典型问题:非原子操作导致的竞态条件 #
考虑以下场景:两个线程同时对共享变量N执行1000次递增操作,理论上最终结果应该是2000,但实际运行结果往往小于这个值。
public class TestSum1000 {
public static volatile Integer N = 0;
public static void main(String[] args) throws InterruptedException {
Runnable run = new Runnable() {
public void run() {
for (int i = 0; i < 1000; i++) {
N++; // 非原子操作
}
}
};
new Thread(run).start();
new Thread(run).start();
Thread.sleep(2000);
System.out.println(N); // 结果往往小于2000
}
}
问题分析 #
N++
看似简单的操作,实际上包含三个步骤:
- 读取N的当前值
- 将值加1
- 将新值写回N
当两个线程同时执行时,可能出现以下情况:
- 线程A和线程B同时读取N=10
- 两者都计算得到11
- 最终N只被增加了一次而非两次
解决方案:强制串行化 #
最简单的解决方案是使用synchronized
强制将并行操作改为串行执行,确保同一时间只有一个线程能够执行关键代码段。
生活类比:想象一个多人共用的厕所,为了保证隐私和卫生,厕所配备了门锁。使用厕所的人进去后会锁门,其他人需要在外面等待。Java的synchronized
机制就类似于这个门锁系统。
三、synchronized的核心原理 #
synchronized
本质上是一个线程队列管理器。在没有竞争的情况下,它的开销几乎可以忽略不计;当出现竞争时,它会管理线程的排队、休眠和唤醒。
JDK 1.6之前的实现 #
在早期版本中,synchronized
的实现较为简单粗暴:
- 当线程获取锁失败时,直接进入等待队列
- 线程状态从用户态切换到内核态(较为重量级的操作)
- 锁释放时唤醒等待线程
这种实现方式在以下场景中存在性能问题:
- 低竞争场景:大多数情况下只有一个线程访问同步块
- 短时持有锁:同步块执行时间极短(毫秒级)
因此在JDK 1.6之前,ReentrantLock
(基于CAS实现)往往比synchronized
性能更好。
四、JDK 1.6的优化:锁升级机制 #
针对上述问题,JDK 1.6引入了偏向锁和轻量级锁的概念,形成了锁升级机制。
1. 偏向锁(Biased Locking) #
解决场景:绝大多数情况下锁只被单个线程访问
实现原理:
- 在对象头的Mark Word中记录线程ID
- 同一线程再次获取锁时只需简单检查,无需同步操作
生活类比:想象一个人独占厕所,他可以在里面做各种事情(小解、大解、补妆等)而无需反复开关门锁。
2. 轻量级锁(Lightweight Locking) #
解决场景:锁竞争不激烈,持有时间短
实现原理:
- 使用CAS操作尝试获取锁
- 失败后短暂自旋(忙等待)而不是立即阻塞
- 自旋成功则继续,失败则升级为重量级锁
五、synchronized锁升级详解 #
锁升级是synchronized优化的核心机制,它根据竞争情况动态调整锁的级别。
5.1 锁的四种状态 #
锁状态 | 标志位 | 特点 |
---|---|---|
无锁 | 001 | 新创建对象的状态 |
偏向锁 | 101 | 记录线程ID,优化单线程重复获取场景 |
轻量级锁 | 00 | 使用CAS和自旋,适合短时间低竞争场景 |
重量级锁 | 10 | 真正的互斥锁,涉及线程阻塞和唤醒,开销较大 |
5.2 锁升级流程 #
5.3 实验验证锁状态变化 #
我们可以使用JOL(Java Object Layout)工具来观察对象头的实际变化:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
[!note]
VM启动参数得加上 取消 延迟启动偏向锁-XX:BiasedLockingStartupDelay=0
JDK15之后得加上-XX:+UseBiasedLocking
基础测试:单线程获取锁 #
final Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable()); // 初始状态
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable()); // 获取锁后状态
}
输出分析:
- 初始状态:
101
(偏向锁,但无线程ID)匿名偏向锁 - 获取锁后:
101
(偏向锁,含线程ID) 偏向锁
多线程竞争测试 #
public class LockUpgradeTest {
public static void main(String[] args) throws InterruptedException {
// 禁用偏向锁延迟
System.setProperty("java.vm.version", "15+");
// -XX:BiasedLockingStartupDelay=0
final Object lock = new Object();
// 线程1:首次获取偏向锁
new Thread(() -> {
synchronized (lock) {
System.out.println("Thread1 holding lock:");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}).start();
Thread.sleep(100);
// 线程2:尝试获取锁,触发锁升级
new Thread(() -> {
synchronized (lock) {
System.out.println("Thread2 holding lock:");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}).start();
}
}
实验结果:
- 第一个线程获取锁时,对象处于偏向锁状态(101)
- 第二个线程尝试获取时,大多数情况下会升级为轻量级锁(00)
- 极少数情况下可能保持偏向锁状态(与JVM实现和安全点有关)
5.4 锁降级问题 #
关于锁是否能降级,经过实验验证:
- 锁确实可以降级,但需要满足特定条件(如到达安全点)
- 在同步块执行完毕后,锁会回归无锁状态(001)
- 实际使用中应尽量缩小同步块范围,便于JVM优化
六、偏向锁的争议与未来 #
JDK 15开始默认禁用了偏向锁,主要原因包括:
- 实现复杂度高:偏向锁的撤销需要进入安全点(Stop-The-World)
- 性能收益有限:现代CPU的CAS操作已经高度优化
- 哈希码冲突:偏向锁会占用对象头中存储哈希码的空间
- 实际场景有限:大多数应用更适合轻量级锁或重量级锁
七、总结与最佳实践 #
- 实践验证:对于技术问题,不应仅依赖文档,而应通过实验验证
- 锁范围最小化:尽量缩小同步块的范围,提高并发性能
- 了解机制:理解锁升级过程有助于编写更高效的并发代码
- 与时俱进:关注JDK版本变化,如JDK 15+中偏向锁的默认禁用
synchronized
作为Java并发编程的基石,其设计演变反映了Java语言对性能的不懈追求。理解其内部机制,能帮助开发者编写出更高效、更可靠的并发程序。
附录:参考资料 #
https://developer.huawei.com/consumer/cn/forum/topic/0203924188150280601
https://github.com/HenryChenV/my-notes/issues/3
https://github.com/HenryChenV/my-notes/issues/3