在 Java 并发编程中,synchronizedReentrantLock 都是实现线程同步、保证互斥访问共享资源的关键机制,但它们在设计理念、功能和使用方式上存在显著差异。以下是主要区别:

一. 本质与实现方式

  • synchronized
    • Java 语言内置的关键字。 是 Java 语法的一部分。
    • 隐式锁(监视器锁)。 基于 JVM 底层(对象头中的 Mark Word)的 monitorentermonitorexit 指令实现锁的获取和释放。锁的获取和释放由 JVM 自动管理。
    • 与对象关联。 锁是附加在对象上的(实例对象或 Class 对象)。
  • ReentrantLock
    • java.util.concurrent.locks 包中的一个类。 是 JDK 提供的 API。
    • 显式锁。 开发者需要显式地调用 lock() 方法来获取锁,并(必须)在 finally 块中调用 unlock() 方法来释放锁。
    • 独立的对象。 ReentrantLock 本身就是一个锁对象。

二. 锁的获取与释放

  • synchronized
    • 自动获取与释放。 线程进入 synchronized 修饰的代码块(方法或代码段)时自动获取关联对象的锁,退出该代码块(正常退出或异常退出)时 JVM 会自动释放锁。开发者无需手动管理。
  • ReentrantLock
    • 手动获取与释放。 必须显式调用 lock.lock() 获取锁。强烈要求finally 块中调用 lock.unlock() 来确保锁在任何情况下(包括异常)都能被释放,否则可能导致死锁。

三. 功能特性

  • synchronized
    • 基本互斥与重入。 提供基本的互斥性和可重入性(同一线程可多次进入自己持有的锁保护的代码块)。
    • 等待/通知机制单一。 通过 Object.wait(), Object.notify(), Object.notifyAll() 实现等待/通知,但一个对象的锁只能关联一个等待队列(隐式的条件队列)。
  • ReentrantLock (功能更丰富):
    • 基本互斥与重入。 同样提供互斥性和可重入性。
    • 尝试非阻塞获取锁: tryLock() 方法尝试获取锁,如果锁当前未被其他线程占用则立即获取成功并返回 true,否则立即返回 false,线程不会被阻塞。
    • 可中断的锁获取: lockInterruptibly() 方法允许在等待锁的过程中响应中断(调用 Thread.interrupt())。
    • 超时获取锁: tryLock(long timeout, TimeUnit unit) 方法允许在指定的时间内尝试获取锁,超时后返回 false
    • 公平性选择: 构造函数可指定锁是公平的 (new ReentrantLock(true)) 还是非公平的 (new ReentrantLock()new ReentrantLock(false))。公平锁按线程请求锁的顺序(FIFO)授予锁;非公平锁允许插队,吞吐量通常更高,但可能导致某些线程饥饿。synchronized非公平的。
    • 多个条件队列 (Condition): 可以通过 newCondition() 方法创建多个 Condition 对象。每个 Condition 相当于一个独立的等待队列。这使得线程可以在不同的条件谓词上等待(例如,生产者等待非满,消费者等待非空),并可以精确地唤醒特定条件上的线程(Condition.signal()/signalAll())。这是 synchronizedwait()/notify() 机制无法做到的。

四. 性能

  • 在 Java 5 及以前版本,ReentrantLock 在高竞争场景下的性能通常优于 synchronized
  • 随着 Java 6 对 synchronized 进行了大量优化(如偏向锁、轻量级锁、适应性自旋锁、锁消除、锁粗化等),两者的性能差距在大多数常见场景下已经变得很小,甚至 synchronized 有时更优
  • ReentrantLock超高竞争或者需要利用其高级功能(如 tryLock, 公平性)的场景下可能仍有一定优势。
  • 结论: 性能不应再是选择两者的主要依据。 优先考虑功能需求和代码简洁性。

五. 异常处理

  • synchronized
    • 如果在同步块中抛出异常,JVM 会在退出同步块时自动释放锁。不会因为异常导致锁泄漏。
  • ReentrantLock
    • 如果在调用 lock() 之后、unlock() 之前的代码中抛出异常,并且 unlock() 没有被放在 finally 块中执行,那么锁将永远不会被释放,导致严重死锁。必须使用 try-finally 确保释放。

六. 使用场景

  • 优先考虑 synchronized
    • 需要实现基本的同步互斥。
    • 代码逻辑相对简单。
    • 不需要 ReentrantLock 提供的高级功能(如超时、可中断、多个条件、公平性)。
    • 追求代码简洁性和减少出错可能性(避免忘记 unlock())。
  • 考虑使用 ReentrantLock
    • 需要尝试非阻塞地获取锁 (tryLock())。
    • 需要可中断地等待锁 (lockInterruptibly())。
    • 需要超时获取锁 (tryLock(timeout))。
    • 需要公平锁机制。
    • 需要多个等待条件 (Condition),实现更复杂的线程协作(如经典的生产者-消费者问题)。
    • 在确实存在证据表明 synchronized 成为性能瓶颈(且 ReentrantLock 能解决)的超高竞争场景下。

总结对比表

特性 synchronized ReentrantLock
本质 Java 语言关键字 JDK API (一个类)
锁类型 隐式锁 (JVM 管理) 显式锁 (手动管理)
锁获取/释放 自动 (进入代码块获取,退出释放) 手动 (lock() / unlock()必须在 finally 中释放)
等待/通知 单一条件 (wait()/notify()/notifyAll()) 多个条件 (Condition 对象,await()/signal()/signalAll())
尝试获取锁 不支持 支持 (tryLock())
可中断获取锁 不支持 (等待时阻塞不可中断) 支持 (lockInterruptibly())
超时获取锁 不支持 支持 (tryLock(long timeout, TimeUnit unit))
公平性 仅非公平锁 可选择公平锁或非公平锁 (构造函数参数)
性能 Java 6+ 优化后性能良好,一般场景与 ReentrantLock 相当 Java 5 优势明显,Java 6+ 在超高竞争或需高级功能时有优势
异常时释放锁 自动释放 必须手动在 finally 中释放,否则锁泄漏
代码简洁性 (语法简洁) (需显式加解锁,代码稍繁琐)
适用场景 基本同步需求,简单场景 需要高级功能 (尝试、中断、超时、公平、多条件) 的场景

简单来说:

  • synchronized 简单、安全、内置、非公平。适合大多数基础同步需求。
  • ReentrantLock 灵活、功能强大、显式控制、可选择公平性。当你需要 tryLock、可中断锁等待、超时锁、公平锁或多个条件变量 (Condition) 时使用它。但要牢记手动释放锁的责任。

选择哪个取决于具体的需求和偏好。对于大多数标准互斥场景,synchronized 通常是更简洁、更不易出错的选择。当需要更精细的控制或 synchronized 无法满足的高级功能时,ReentrantLock 是强大的替代方案。