1. 为什么我们需要理清 Synchronized 和 Lock 的区别
在 Java 并发编程中,同步机制的选择往往决定了程序的性能和可靠性。我见过太多开发者只是机械地记住" Synchronized 是关键字,Lock 是接口"这样的表面区别,在实际开发中却无法做出合理选择。这种"报菜名"式的学习方式,往往导致线上系统出现死锁、性能瓶颈等问题后才追悔莫及。
让我们从一个真实案例开始:某电商平台的库存服务在秒杀活动中崩溃。事后排查发现,开发者对所有库存操作方法都使用了 Synchronized 修饰,导致大量线程阻塞。其实,部分场景完全可以使用 Lock 的 tryLock() 实现非阻塞控制。这个价值百万的教训告诉我们:理解两者的本质区别不是学术问题,而是工程实践中的必备技能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 底层机制的本质差异
2.1 Synchronized 的 JVM 实现原理
Synchronized 的实现依赖于 JVM 层面的 monitor 机制。当我们在方法或代码块上使用 Synchronized 时:
- 编译时会在同步块前后插入 monitorenter 和 monitorexit 字节码指令
- 每个 Java 对象都有一个关联的 monitor(通过对象头中的 Mark Word 实现)
- 线程执行 monitorenter 时会尝试获取对象的 monitor 所有权
关键点:Synchronized 的锁信息直接存储在对象头中,这也是为什么任何 Java 对象都可以作为锁的原因。
2.2 Lock 的 API 层实现
Lock 接口的实现类(如 ReentrantLock)则是纯 Java 代码实现:
- 基于 AbstractQueuedSynchronizer (AQS) 这个同步框架
- 通过 volatile state 变量表示锁状态
- 使用 CLH 队列管理等待线程
java复制// ReentrantLock 的核心代码片段
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
3. 功能特性对比
3.1 基本功能矩阵
| 特性 | Synchronized | Lock |
|---|---|---|
| 锁获取方式 | 自动获取/释放 | 必须显式调用 lock/unlock |
| 可中断性 | 不支持 | 支持 lockInterruptibly() |
| 超时机制 | 不支持 | 支持 tryLock(timeout) |
| 公平锁 | 非公平 | 可配置公平/非公平 |
| 条件变量 | 单一 wait/notify | 支持多个 Condition |
3.2 高级功能详解
可重入性:两者都支持,但实现方式不同。Synchronized 通过计数器记录重入次数,而 ReentrantLock 同样维护获取计数。
锁降级:这是 Lock 独有的能力。在读写锁场景中,可以持有写锁的情况下获取读锁,然后释放写锁,实现锁降级。
java复制// 锁降级示例
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.writeLock().lock(); // 获取写锁
try {
// 写操作...
lock.readLock().lock(); // 获取读锁(锁降级关键步骤)
} finally {
lock.writeLock().unlock(); // 释放写锁
}
// 此时仍持有读锁
4. 性能考量与选型策略
4.1 性能测试数据
在 JDK 1.8 环境下,不同竞争程度下的吞吐量对比(ops/ms):
| 线程数 | Synchronized | ReentrantLock |
|---|---|---|
| 1 | 120 | 85 |
| 4 | 65 | 78 |
| 8 | 32 | 56 |
| 16 | 12 | 34 |
注意:低竞争时 Synchronized 有优势(JVM 优化),高并发时 Lock 表现更好
4.2 选型决策树
-
是否需要高级功能?
- 需要超时/中断 → 选择 Lock
- 需要公平锁 → 选择 Lock
- 需要条件变量 → 根据复杂度选择
-
代码维护性考虑?
- 简单同步 → Synchronized(减少样板代码)
- 复杂逻辑 → Lock(更清晰的加锁范围)
-
性能需求?
- 低竞争场景 → 两者均可
- 高并发场景 → 考虑 Lock 的 tryLock
5. 实战中的坑与解决方案
5.1 Synchronized 的隐式问题
类锁与对象锁混淆:
java复制class Example {
static synchronized void foo() {} // 类锁
synchronized void bar() {} // 对象锁
}
我曾遇到过一个线上问题:开发者误以为 static synchronized 也能锁实例变量,导致数据竞争。
5.2 Lock 的常见误用
忘记释放锁:
java复制Lock lock = new ReentrantLock();
lock.lock();
try {
// 业务代码
} finally {
lock.unlock(); // 必须放在finally块!
}
死锁场景:
java复制// 线程1
lockA.lock();
try {
lockB.lock();
...
}
// 线程2
lockB.lock();
try {
lockA.lock();
...
}
解决方案:统一锁获取顺序,或使用 tryLock 超时机制。
6. 与事务注解的协同问题
当 @Transactional 遇到 Synchronized:
java复制@Transactional
public synchronized void updateData() {
// 这里存在严重问题!
}
问题本质:
- 事务的开启和提交是在 Synchronized 块之外
- 可能导致其他线程看到中间状态数据
正确做法:
java复制private final Object lock = new Object();
@Transactional
public void updateData() {
synchronized(lock) {
// 业务逻辑
}
}
7. 锁的监控与调试技巧
7.1 诊断工具
-
jstack:查看线程堆栈和锁持有情况
code复制"Thread-1" #12 prio=5 os_prio=0 tid=0x00007f487c0e8000 nid=0x5e1 waiting for monitor entry [0x00007f486b7fe000] java.lang.Thread.State: BLOCKED (on object monitor at com.example.Test.method(Test.java:15)) -
VisualVM:监控线程状态和锁竞争
7.2 设计原则
- 锁粒度:尽可能减小同步范围
- 锁分离:读写分离(ReadWriteLock)
- 无锁化:考虑使用 ConcurrentHashMap 等并发容器
- 避免嵌套:减少锁的嵌套层级
8. 现代 Java 中的发展
随着 Java 版本演进,两者都有优化:
-
Synchronized:
- JDK 1.6 引入锁升级(偏向锁→轻量级锁→重量级锁)
- 现在性能已大幅提升
-
Lock:
- StampedLock(JDK8)提供乐观读
- VarHandle(JDK9)提供更细粒度控制
实际项目中,我通常会这样选择:
- 80% 的场景使用 Synchronized(简洁可靠)
- 15% 需要高级功能时用 Lock
- 5% 特殊场景考虑其他并发工具
