Java synchronized 和 Lock 该怎么选

synchronized 和 Lock 都是 Java 里加锁的手段,一个是关键字一个是接口,功能重叠又各有脾气。这篇讲清它俩的区别:谁自动释放、谁能中断、谁能读写分离,再说说什么场景用哪个,帮你别再纠结到底选哪个。

写 Java 多线程,绕不开加锁。而一说加锁,就有个老问题冒出来:synchronized 和 Lock,到底用哪个?

网上的答案两极分化,有人说 synchronized 简单够用,有人说 Lock 才是高级玩法。

我自己踩下来的体会是:它俩没有绝对的谁好谁坏,是看场景。这篇就把区别和选择讲明白。

先说清楚,标题里那个 synchronized 是 Java 的关键字;Lock 是 java.util.concurrent.locks 包里的一个接口,最常用的实现是 ReentrantLock。

它俩各是什么来头

synchronized 是关键字,Java 语言自带的,JVM 层面帮你实现的锁。

用起来极简单,直接把要保护的代码框起来:

// 锁住这段代码,同一时刻只有一个线程能进
synchronized (lockObj) {
    // 操作共享数据
    balance = balance - 100;
}
​

Lock 是接口,属于 JDK 后来提供的并发工具,得手动 new 一个实现类出来用:

Lock lock = new ReentrantLock();

lock.lock();          // 手动上锁
try {
    balance = balance - 100;
} finally {
    lock.unlock();    // 手动释放,必须放 finally 里
}
​

光看这两段,第一个区别就出来了。

区别一 释放锁谁操心

synchronized 的锁是自动释放的。

代码块执行完,或者中间抛异常了,JVM 都会帮你把锁放掉。你不用管,省心。

Lock 就得自己动手释放。

你 lock() 了,就必须记得 unlock()。而且这个 unlock 一定要放在 finally 里——万一中间抛异常,finally 保证锁能被释放。

忘了写 unlock,或者没放 finally 结果异常把它跳过了,这把锁就永远不释放了,其他线程全卡死在那等着。这是用 Lock 最经典的坑。

所以这一局:synchronized 省心,Lock 灵活但要你自己兜底。

区别二 能不能不死等

这是 Lock 真正的看家本领。

用 synchronized,一个线程拿不到锁,就只能傻等,一直等到拿到为止,中途没法反悔,也问不了“还要等多久”。

Lock 就聪明多了,它给了你好几种“不死等”的选择:

  • tryLock():试一下,拿得到就锁,拿不到马上返回,我不等了,先去干别的
  • tryLock(时间):最多等这么久,超时还没拿到就放弃
  • lockInterruptibly():等锁的过程中,允许被别的线程中断掉

举个例子,抢不到锁就直接走人:

if (lock.tryLock()) {   // 试着抢锁
    try {
        // 抢到了才干活
    } finally {
        lock.unlock();
    }
} else {
    // 没抢到,不干等,去做别的
}
​

这种能力在实际项目里很有用。比如避免死锁、比如做个“抢不到就降级”的兜底逻辑,synchronized 干不了这些。

区别三 读写能不能分开

还有个场景 synchronized 处理得比较糙。

很多数据是读多写少的。读操作之间其实不冲突,几个线程一起读没问题,只有写的时候才需要互斥。

但 synchronized 一刀切,不管读还是写,一次只放一个线程进去。几个线程明明只是想读一下,也得排队,白白浪费并发。

Lock 这边有个 ReentrantReadWriteLock,把锁拆成读锁和写锁:

  • 读锁:多个线程可以同时拿,一起读
  • 写锁:独占,写的时候谁都别进
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

// 读的时候用读锁,允许多个线程并发读
rwLock.readLock().lock();
try {
    // 读共享数据
} finally {
    rwLock.readLock().unlock();
}
​

读多写少的场景,读写锁能把并发度拉上来一大截,这是 synchronized 给不了的。

顺带说说公平不公平

还有个小差别,锁的“公平性”。

  • synchronized 是非公平的,谁抢到算谁的,不排队论资排辈
  • Lock 可以选,new ReentrantLock(true) 就是公平锁,先来的先拿

公平锁看着更“讲道理”,但维护排队顺序有额外开销,吞吐量反而低。

所以多数时候非公平就够了,除非你真的需要严格的先来后到。这个点了解一下即可,不用纠结。

那到底怎么选

绕回最开始那个问题。我的判断很简单:

能用 synchronized 就用 synchronized。

它简单、不容易出错、锁自动释放,JDK 这些年也把它优化得很好,性能早就不是短板了。日常大部分加锁场景,它完全够用。

只有当 synchronized 满足不了时,才换 Lock。 什么时候满足不了?

  • 需要尝试加锁、超时放弃、可中断 → 用 Lock 的 tryLock 那套
  • 读多写少,想让读操作并发 → 用读写锁
  • 需要公平锁 → 用 ReentrantLock 的公平模式

一句话:synchronized 是默认选项,Lock 是“需要高级功能时”的进阶选项。别一上来就为了显得高级硬上 Lock,结果 unlock 忘了写,得不偿失。

小结

把这俩兄弟捋一遍,其实分工挺清楚。

  • 释放:synchronized 自动放,Lock 手动 unlock(记得放 finally)
  • 等待:synchronized 只能死等,Lock 能 tryLock、超时、可中断
  • 读写:synchronized 一刀切,Lock 有读写锁能分离
  • 公平:synchronized 非公平,Lock 可选公平

选择的原则更简单:默认 synchronized,需要高级能力再上 Lock。

没有哪个绝对更好,合适的才是好的。

搞清楚它俩各自的脾气,选起来就不慌了。

更多推荐

章节目录