在 Java 并发与Spring事务管理结合时,synchronized 和 @Transactional 同时使用确实会踩坑。
🧩 一、机制差异
| 特性 | synchronized | @Transactional |
|---|---|---|
| 作用层面 | JVM 内部的线程锁(单进程级别) | Spring 管理的数据库事务(数据库级别) |
| 控制对象 | 同一个 JVM 内的线程访问代码块 | 同一个数据库连接中的 SQL 操作 |
| 生命周期 | 锁持有期间 | 方法执行期间(代理层包裹) |
| 异常处理 | 不会主动回滚数据库 | 抛出 RuntimeException 时触发回滚 |
⚠️ 二、常见问题与坑点
1️⃣ @Transactional 失效(被 synchronized 影响)
问题场景:
@Transactional
public synchronized void doSomething() {
// DB操作
}
Spring 的事务是通过 AOP 代理 实现的(即代理调用目标方法时开启事务)。
当方法被 synchronized 修饰时,它在进入代理前就被锁了,但事务的开启时机在锁之后。
⚠️ 如果此方法被内部调用(this.doSomething()) ,Spring AOP不会生效 → 事务失效。
✅ 建议:
- 不要在
@Transactional方法上直接加synchronized; - 如果确实需要同步,放在业务层外层或使用 Redis/Zookeeper 分布式锁控制。 2️⃣ 锁与事务交叉导致性能或死锁问题 例如:
public synchronized void updateAccount() {
doUpdate(); // 调用了@Transactional方法
}
这里的 synchronized 锁住了整个方法;
但事务中的数据库锁(例如行锁)又会持有连接,
→ 导致多个线程都在等待锁释放,容易出现:
- JVM 级别锁等待;
- DB 级别锁等待;
- 二者叠加 → 死锁或长时间阻塞。
✅ 建议:
- 让事务尽量“短”;
- 锁只保护临界区逻辑;
- 不要把
synchronized和数据库操作长时间混用。
3️⃣ 多节点集群中 synchronized 无效
@Transactional 事务作用于数据库(集群共享),
但 synchronized 只在单 JVM 实例中有效。
所以在分布式部署下:
// 2个服务节点调用同一个方法
synchronized void transfer() { ... }
→ 各节点各锁自己的内存,不会互斥!
导致并发问题仍会发生。
✅ 建议:
使用:
- Redis分布式锁(如 Redisson)
- Zookeeper分布式锁
- 数据库唯一约束 或 悲观锁
4️⃣ @Transactional 异常回滚 + 锁未释放
如果你在锁中抛出异常导致事务回滚:
public synchronized void saveData() {
throw new RuntimeException("fail"); // 事务回滚
}
锁是 JVM 层面的,异常不会影响锁释放(JVM自动释放 monitor),
但如果你手动实现了锁(如 ReentrantLock)且忘记 finally unlock(),可能造成锁泄露。
✅ 一定要:
lock.lock();
try {
// transactional logic
} finally {
lock.unlock();
}
✅ 三、最佳实践建议总结
| 场景 | 推荐方式 |
|---|---|
| 单机同步 + 事务 | 使用 synchronized 或 ReentrantLock,但锁区域要小,避免嵌套数据库操作。 |
| 分布式系统 | 用 Redis/Zookeeper 分布式锁 控制同步,事务控制在数据库中进行。 |
| 内部方法调用需要事务 | 不要 this.method() 调用,用 @Autowired 注入自身代理(AopContext.currentProxy())。 |
避免 synchronized + @Transactional 直接叠加 | 用外层锁或分布式锁控制,事务放在内层逻辑中。 |