Skip to content

介绍一下你提到的 synchronized 和 ReentrantLock?该怎么选用?

约 1112 字大约 4 分钟

多线程与并发字节

2025-03-20

⭐ 题目日期:

字节 - 2024/12/25

📝 题解:

在 Java 中,synchronizedReentrantLock 是两种实现线程同步的核心工具,但它们在设计理念、功能和使用方式上有显著区别。以下是它们的对比和选型建议:


1. synchronized(隐式锁)

核心特性

  • 语法简单:通过关键字修饰方法或代码块,JVM 自动管理锁的获取和释放。
  • 可重入性:线程可以重复获取已持有的锁。
  • 非公平锁:默认抢占式获取锁,不保证等待时间长的线程优先获得锁。
  • 异常自动释放:线程执行完同步代码或发生异常时,JVM 自动释放锁。
  • 优化机制:JDK 6 后引入偏向锁、轻量级锁、重量级锁等优化,性能大幅提升。

使用方式

// 修饰代码块
public void method() {
    synchronized (lockObject) {
        // 同步代码
    }
}

// 修饰实例方法(锁对象是当前实例)
public synchronized void method() { ... }

// 修饰静态方法(锁对象是当前类的 Class 对象)
public static synchronized void staticMethod() { ... }

2. ReentrantLock(显式锁)

核心特性

  • 显式控制:需手动调用 lock()unlock(),通常结合 try-finally 确保释放。
  • 可重入性:与 synchronized 相同,支持重复获取锁。
  • 支持公平锁:构造函数可指定是否为公平锁(默认非公平)。
  • 高级功能
    • 可中断锁lockInterruptibly() 允许在等待锁时响应中断。
    • 超时获取锁tryLock(long timeout, TimeUnit unit) 避免无限等待。
    • 条件变量:通过 newCondition() 创建多个条件队列,实现精细的线程等待/唤醒(如生产者-消费者模型)。

使用方式

ReentrantLock lock = new ReentrantLock();

public void method() {
    lock.lock();
    try {
        // 同步代码
    } finally {
        lock.unlock(); // 必须确保释放锁
    }
}

3. 关键区别总结

特性synchronizedReentrantLock
锁的获取与释放JVM 自动管理需手动调用 lock()unlock()
公平性仅支持非公平锁支持公平锁和非公平锁
功能扩展基础功能支持可中断、超时、条件变量等高级功能
性能JDK 6+ 优化后接近显式锁高并发竞争时可能更优
代码复杂度简单需显式管理,易出错(如忘记释放锁)
异常处理自动释放锁需在 finally 块中手动释放

4. 选型建议

优先选择 synchronized 的场景

  1. 简单同步需求:如基本的临界区保护,无需高级功能。
  2. 代码简洁性:避免手动管理锁,减少编码错误。
  3. 性能敏感但低竞争:在低并发或无激烈锁竞争时,synchronized 的 JVM 优化可能更高效。

优先选择 ReentrantLock 的场景

  1. 需要高级功能
    • 可中断锁:避免死锁(如等待锁时响应外部中断)。
    • 超时机制:防止线程长时间阻塞。
    • 公平锁:减少线程饥饿问题。
  2. 复杂同步逻辑
    • 使用条件变量(Condition)实现多条件等待(如 await()/signal())。
    • 需要同时持有多个锁(如通过 tryLock 按顺序获取)。
  3. 高竞争环境:在极端高并发场景下,ReentrantLock 的非公平模式可能提供更高吞吐量。

5. 示例对比

场景:可中断的锁等待

// 使用 ReentrantLock 的可中断特性
ReentrantLock lock = new ReentrantLock();

public void performTask() throws InterruptedException {
    try {
        lock.lockInterruptibly(); // 可中断的锁获取
        // 同步代码
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

// 使用 synchronized 无法直接中断等待锁的线程
public synchronized void performTask() {
    // 同步代码(无法响应中断)
}

场景:条件变量(生产者-消费者模型)

ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

// 生产者
public void produce() {
    lock.lock();
    try {
        while (queue.isFull()) {
            notFull.await(); // 等待队列非满
        }
        queue.add(item);
        notEmpty.signal(); // 唤醒消费者
    } finally {
        lock.unlock();
    }
}

6. 注意事项

  • 避免死锁:无论是 synchronized 还是 ReentrantLock,需确保锁的获取顺序一致。
  • 性能权衡:在大多数场景下,synchronized 已足够高效;仅在需要高级功能或极端性能优化时使用 ReentrantLock
  • 资源泄漏:使用 ReentrantLock 时务必在 finally 块中释放锁。

总结

  • synchronized:简洁、安全,适合大多数基础同步场景。
  • ReentrantLock:灵活、功能强大,适合复杂同步需求或需要精细控制的场景。

根据具体需求选择工具:优先使用 synchronized,在需要高级功能时转向 ReentrantLock