synchronized和RentrantLock用哪个?
在现代 Java 版本(JDK 8 及以后),synchronized
和 ReentrantLock
在大多数常规场景下的性能已经非常接近,甚至在某些情况下 synchronized
表现更好。
因为 synchronized
在 JDK 6 之后引入了偏向锁、轻量级锁和自适应自旋等优化,才使得它的性能得到了质的飞跃,不再是过去那个“重量级”的代名词。
我们来深入探讨一下:
1. synchronized
的演进与优化
在 JDK 6 之前,synchronized
被认为是重量级锁,因为它依赖于操作系统的互斥量(Mutex),涉及到用户态和内核态的切换,开销较大。
但从 JDK 6 开始,JVM 对 synchronized
进行了大量的优化,引入了锁升级机制:
-
无锁状态: 初始状态,没有竞争。
-
偏向锁 (Biased Locking):
- 思想: 如果一个线程反复进入同一个同步块,那么它就不需要每次都进行加锁和解锁的操作。JVM 会“偏向”这个线程,认为它会一直持有这个锁。
- 原理: 当一个线程第一次获取锁时,JVM 会把锁对象的 Mark Word(对象头的一部分)设置为偏向模式,并记录下这个线程的ID。如果后续还是这个线程来获取锁,它只需要检查Mark Word中的线程ID是否是自己,如果是,就直接进入同步块,无需任何同步操作。
- 适用场景: 几乎没有竞争的场景。
- 效率: 极高,几乎没有开销。
-
轻量级锁 (Lightweight Locking):
- 思想: 如果有少量线程在短时间内交替竞争同一个锁,但没有发生线程阻塞(即没有线程进入等待状态),那么就不需要升级到重量级锁。
- 原理: 当偏向锁失效(有其他线程尝试获取锁)时,JVM 会在当前线程的栈帧中创建锁记录(Lock Record),并将锁对象的Mark Word复制到锁记录中,然后尝试使用 CAS (Compare-And-Swap) 操作将Mark Word更新为指向锁记录的指针。如果成功,则获取锁。如果失败,说明有竞争,但如果竞争不激烈,会尝试自旋。
- 适用场景: 线程交替执行同步块,竞争不激烈。
- 效率: 较高,避免了用户态/内核态切换。
-
自适应自旋 (Adaptive Spinning):
- 思想: 在轻量级锁竞争失败后,线程不会立即阻塞,而是会“自旋”一段时间(空循环),看看持有锁的线程是否很快释放锁。如果自旋成功(锁很快被释放),就避免了线程的上下文切换。自旋的次数不是固定的,而是根据上次自旋的成功率和锁持有者的状态动态调整。
- 适用场景: 竞争不激烈,锁持有时间短。
- 效率: 进一步提升轻量级锁的性能。
-
重量级锁 (Heavyweight Locking):
- 思想: 如果竞争非常激烈,自旋也无法获取到锁,或者持有锁的线程执行时间过长,导致其他线程长时间自旋,那么就会升级为重量级锁。
- 原理: 依赖操作系统的互斥量,线程会进入阻塞状态,涉及到用户态和内核态的切换。
- 适用场景: 竞争激烈,线程需要阻塞等待。
- 效率: 最低,但能保证正确性。
2. ReentrantLock
ReentrantLock
是 java.util.concurrent.locks
包下的一个类,它是基于 AQS (AbstractQueuedSynchronizer) 实现的。
核心思想: “我给你提供更灵活的锁操作,你可以手动控制加锁和解锁,还可以尝试非阻塞地获取锁,或者设置超时时间。”
特点:
- 手动加锁/解锁: 必须手动调用
lock()
和unlock()
方法,容易忘记释放锁导致死锁。 - 可中断锁:
lockInterruptibly()
方法允许在等待锁的过程中被中断。 - 尝试非阻塞获取锁:
tryLock()
方法可以尝试获取锁,如果失败立即返回,不会阻塞。 - 公平锁/非公平锁: 可以选择公平锁(按请求顺序获取锁)或非公平锁(抢占式获取锁,默认非公平,性能更高)。
- 条件变量: 可以通过
newCondition()
创建多个条件变量,实现更复杂的线程间协作。
效率:
ReentrantLock
内部也是通过 CAS 操作来实现锁的获取和释放,避免了用户态/内核态的切换(除非竞争激烈到需要阻塞)。- 在竞争不激烈的情况下,它的性能与
synchronized
的轻量级锁类似。 - 在竞争激烈的情况下,它会通过 AQS 的队列机制来管理等待线程,性能也很好。
3. 性能对比与选择
特性/锁类型 | synchronized | ReentrantLock |
---|---|---|
实现方式 | JVM 原生支持,字节码指令(monitorenter/exit) | Java API,基于 AQS (AbstractQueuedSynchronizer) |
锁升级 | 自动升级/降级(偏向 -> 轻量 -> 重量) | 无锁升级概念,直接使用 CAS 和 AQS 队列 |
加锁/解锁 | 自动(编译器插入) | 手动(lock() / unlock() ),必须在 finally 块中释放 |
灵活性 | 较低,功能固定 | 较高,提供更多高级功能(可中断、尝试锁、公平/非公平、条件变量) |
公平性 | 非公平 | 可选公平/非公平 |
性能 | JDK 8+ 大多数场景下与 ReentrantLock 相当,甚至更好 | 在特定高级功能需求下有优势,常规场景与 synchronized 相当 |
可重入性 | 是 | 是 |
异常处理 | 自动释放锁 | 必须在 finally 块中手动释放,否则可能死锁 |
为什么说 synchronized
在某些情况下可能更好?
- JVM 优化:
synchronized
是 JVM 原生支持的,JVM 可以对其进行更深层次的优化,例如逃逸分析、锁消除、锁粗化等。这些优化是ReentrantLock
无法享受到的。 - 自动管理:
synchronized
的自动加锁和解锁机制,使得代码更简洁,不易出错。而ReentrantLock
需要手动管理,一旦忘记unlock()
,就可能导致死锁。 - 偏向锁的优势: 在几乎没有竞争的场景下,
synchronized
的偏向锁性能是最高的,因为它几乎没有开销。而ReentrantLock
即使在无竞争情况下,也需要执行 CAS 操作。
什么时候选择 ReentrantLock
?
尽管 synchronized
性能已大幅提升,但 ReentrantLock
仍然有其不可替代的优势:
- 需要尝试非阻塞地获取锁:
tryLock()
。 - 需要可中断地获取锁:
lockInterruptibly()
。 - 需要设置获取锁的超时时间:
tryLock(long timeout, TimeUnit unit)
。 - 需要实现公平锁:
new ReentrantLock(true)
。 - 需要多个条件变量进行线程协作:
newCondition()
。
对于大多数简单的同步需求,优先使用 synchronized
。它代码简洁,由 JVM 自动管理,并且在现代 JVM 中性能已经非常优秀。
只有当你需要 ReentrantLock
提供的高级功能(如可中断锁、非阻塞获取锁、公平锁、多条件变量)时,才考虑使用 ReentrantLock
。
这两种锁的演进,体现了计算机科学中一个重要的思想:“针对特定场景进行优化”。
synchronized
的优化,是从“悲观锁”的重量级实现,通过分析实际运行时的竞争模式(无竞争、少量竞争、激烈竞争),逐步引入了“乐观锁”的思想(偏向锁、轻量级锁的 CAS),以及“自适应”的思想(自适应自旋),从而在不改变语义的前提下,大幅提升了性能。它是一种**“由内而外”**的优化,由 JVM 自动完成。ReentrantLock
则是提供了一种**“由外而内”**的控制能力,它把锁的更多细节暴露给开发者,让开发者可以根据业务需求,更精细地控制锁的行为。