1. 为什么我们需要synchronized锁
在Java并发编程的世界里,synchronized就像交通信号灯一样重要。想象一下,如果没有红绿灯,十字路口的车辆会乱成什么样子?多个线程同时访问共享资源时,如果没有同步机制,程序就会出现数据竞争、内存不一致等问题。
我曾在电商项目中遇到过典型的并发问题:库存超卖。当多个用户同时抢购同一商品时,如果不加锁,库存扣减就会出现错误。比如商品只剩1件,但两个线程同时读取库存为1,都认为可以扣减,最终导致库存变为-1。这就是典型的竞态条件(Race Condition)。
注意:竞态条件是指多个线程对共享数据执行操作时,最终结果取决于线程执行的精确时序。就像两个人在同一条跑道上跑步,谁先到达终点取决于起跑时间。
synchronized关键字提供了三种基本用法:
- 实例方法同步:锁是当前对象实例
- 静态方法同步:锁是当前类的Class对象
- 同步代码块:可以指定任意对象作为锁
java复制// 实例方法同步
public synchronized void method() {
// 临界区代码
}
// 静态方法同步
public static synchronized void staticMethod() {
// 临界区代码
}
// 同步代码块
public void blockMethod() {
synchronized(this) { // 可以使用任意对象作为锁
// 临界区代码
}
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. synchronized的底层实现原理
2.1 Java对象头与Monitor机制
每个Java对象在内存中都有对象头,其中包含Mark Word用于存储对象的运行时数据。当对象作为锁时,Mark Word会存储指向Monitor对象的指针。
Monitor(监视器)是synchronized的底层实现机制,它包含以下关键字段:
- _owner:指向持有锁的线程
- _EntryList:存放等待锁的线程
- _WaitSet:存放调用wait()的线程
java复制// HotSpot虚拟机源码中的Monitor定义(简化版)
class ObjectMonitor {
void* _owner; // 当前持有锁的线程
ObjectWaiter* _EntryList; // 等待锁的线程队列
ObjectWaiter* _WaitSet; // 调用wait()的线程队列
// 其他字段...
}
2.2 锁升级过程:从偏向锁到重量级锁
JVM为了优化synchronized性能,设计了锁升级机制:
- 无锁状态:对象刚创建时
- 偏向锁:只有一个线程访问时,通过CAS在Mark Word中记录线程ID
- 轻量级锁:当有竞争时,撤销偏向锁,通过CAS自旋尝试获取锁
- 重量级锁:自旋失败后,升级为操作系统层面的互斥锁
提示:在Java 6之后,JVM默认开启偏向锁延迟(4秒)。这是因为统计显示大多数对象的生命周期很短,立即启用偏向锁反而会降低性能。
3. synchronized的内存语义与happens-before关系
3.1 内存可见性保证
synchronized不仅提供互斥访问,还确保内存可见性。当线程退出同步块时,会强制将工作内存中的修改刷新到主内存;当线程进入同步块时,会清空工作内存,从主内存重新加载变量。
java复制class VisibilityDemo {
private boolean flag = false;
public synchronized void writer() {
flag = true; // 1. 修改flag
} // 2. 释放锁,强制刷新到主内存
public synchronized void reader() {
if(flag) { // 3. 获取锁,从主内存重新加载
// do something
}
}
}
3.2 happens-before规则
根据Java内存模型,synchronized建立以下happens-before关系:
- 解锁操作happens-before于后续对同一锁的加锁操作
- 线程A的所有操作happens-before于线程A释放锁前的最后操作
4. 性能优化与最佳实践
4.1 锁粒度控制
我曾在日志系统中犯过锁粒度过大的错误:对整个日志类加锁,导致性能瓶颈。后来改为对每个日志文件单独加锁,吞吐量提升了3倍。
优化建议:
- 尽量减小同步代码块的范围
- 对不同资源使用不同的锁对象
- 避免在同步块中执行耗时操作(如IO)
4.2 锁消除与锁粗化
JVM会进行两种锁优化:
- 锁消除:通过逃逸分析,发现对象不会逃逸出当前线程,则消除锁
- 锁粗化:将连续的多个锁操作合并为一个更大的锁操作
java复制// 锁消除示例
public String concat(String s1, String s2) {
StringBuffer sb = new StringBuffer(); // 不会逃逸出方法
sb.append(s1);
sb.append(s2);
return sb.toString(); // JVM会消除StringBuffer的内部锁
}
4.3 替代方案对比
虽然synchronized简单易用,但在高并发场景下,可以考虑以下替代方案:
| 特性 | synchronized | ReentrantLock | StampedLock |
|---|---|---|---|
| 公平锁 | 不支持 | 支持 | 不支持 |
| 可中断 | 不支持 | 支持 | 支持 |
| 尝试获取锁 | 不支持 | 支持 | 支持 |
| 读写分离 | 不支持 | 支持(ReadWriteLock) | 支持 |
| 乐观读 | 不支持 | 不支持 | 支持 |
5. 常见问题排查与调试技巧
5.1 死锁检测与分析
死锁是synchronized使用中最常见的问题之一。我曾遇到过一个典型死锁场景:
java复制// 线程1
synchronized(lockA) {
synchronized(lockB) { ... }
}
// 线程2
synchronized(lockB) {
synchronized(lockA) { ... }
}
排查工具:
jstack <pid>:查看线程堆栈和锁持有情况- JConsole/VisualVM:图形化监控线程状态
- 第三方工具如Arthas
5.2 锁竞争监控
在高并发系统中,锁竞争会成为性能瓶颈。可以通过以下方式监控:
- JMX:
java.lang.management.ThreadMXBean提供锁信息 - JFR:Java Flight Recorder记录锁事件
- APM工具:如SkyWalking、Pinpoint等
bash复制# 使用jstack查看锁信息示例
jstack -l <pid> > thread_dump.txt
# 然后搜索"locked"和"waiting to lock"
5.3 避免锁的误用
常见错误用法包括:
- 锁对象被修改(应使用final修饰)
- 锁不同的String对象(由于字符串驻留)
- 在锁内调用外部方法(可能导致死锁)
java复制// 错误示例:锁String对象
String lock = "lock";
synchronized(lock) {
lock = lock.intern(); // 修改了锁引用
// 其他线程可能使用不同的锁对象
}
6. JVM层实现细节
6.1 字节码层面的实现
synchronized在字节码中通过monitorenter和monitorexit指令实现:
java复制public void syncMethod();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter // 进入同步块
4: aload_1
5: monitorexit // 正常退出
6: goto 14
9: astore_2
10: aload_1
11: monitorexit // 异常退出
12: aload_2
13: athrow
14: return
6.2 HotSpot实现细节
在HotSpot虚拟机中,锁的实现经历了多次优化:
- Java 6:引入偏向锁和轻量级锁
- Java 15:逐步废弃偏向锁(JEP 374)
- Java 18:默认禁用偏向锁(JEP 429)
cpp复制// HotSpot中对象头Mark Word的布局(64位系统)
// 无锁状态:
// unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2
// 偏向锁:
// thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2
// 轻量级锁:
// ptr_to_lock_record:62 | lock:2
// 重量级锁:
// ptr_to_monitor:62 | lock:2
7. 现代JVM中的锁优化趋势
随着硬件发展,锁的实现也在不断演进。我在处理一个高并发系统时发现,在JDK 17上使用synchronized的性能已经接近ReentrantLock,这得益于以下优化:
- 自适应自旋:JVM根据历史数据动态调整自旋次数
- 锁消除:对线程局部的对象消除锁操作
- 锁粗化:合并相邻的同步块
- 偏向锁撤销优化:批量重偏向和批量撤销
在实际项目中,我发现一个有趣的现象:对于短期存活的、线程封闭的对象,禁用偏向锁反而能提升性能。这促使我深入研究了JVM的锁优化策略。
