synchronized 和 ReentrantLock的区别

120 阅读3分钟

synchronized 和 ReentrantLock 都是 Java 中用于实现线程同步的机制,用于解决多线程并发访问共享资源时的线程安全问题。它们有相似之处,但也存在显著差异,以下是详细对比:

1. 基本概念

  • synchronized

    • Java 内置的关键字,属于 JVM 层面的锁机制。
    • 基于 JVM 隐式实现,无需手动释放锁,由编译器自动处理加锁和释放(如方法结束或异常时自动释放)。
    • 可修饰方法(实例方法、静态方法)或代码块。
  • ReentrantLock

    • Java 1.5 引入的 java.util.concurrent.locks 包中的类,属于 API 层面的锁机制。
    • 需要显式通过 lock() 加锁和 unlock() 释放锁,通常配合 try-finally 确保锁释放。
    • 实现了 Lock 接口,支持更多灵活的功能。

2. 核心区别

特性synchronizedReentrantLock
锁的实现JVM 内置关键字(C++ 实现)API 层面的类(Java 代码实现)
锁的释放自动释放(方法结束 / 异常)必须手动调用 unlock()(通常在 finally 中)
可中断性不可中断,一旦阻塞无法被中断可中断(lockInterruptibly() 方法)
公平锁支持非公平锁(无法设置为公平锁)支持公平锁和非公平锁(构造函数参数指定)
尝试获取锁不支持(获取不到则阻塞)支持 tryLock() 尝试获取锁(可超时)
条件变量不支持(仅通过 wait()/notify()支持 Condition,可创建多个条件变量
锁的状态查询无法直接查询可通过 isLocked() 等方法查询
性能JDK 1.6 后优化(偏向锁、轻量级锁),性能接近 ReentrantLock早期性能优于 synchronized,现在差异不大

3. 关键特性详解

(1)可中断性

  • synchronized:一旦线程进入阻塞状态(等待锁),无法被中断,只能一直等待。

  • ReentrantLock:提供 lockInterruptibly() 方法,允许线程在等待锁时响应中断,避免无限期阻塞。

    java

    运行

    // ReentrantLock 可中断示例
    ReentrantLock lock = new ReentrantLock();
    try {
        lock.lockInterruptibly(); // 可被中断的加锁
        // 临界区代码
    } catch (InterruptedException e) {
        // 处理中断
    } finally {
        lock.unlock();
    }
    

(2)公平锁

  • 公平锁:线程获取锁的顺序与请求顺序一致,避免饥饿(长期等待的线程有机会获取锁)。

  • 非公平锁:允许 “插队”,可能导致某些线程长期无法获取锁,但性能通常更好。

    • synchronized:仅支持非公平锁。
    • ReentrantLock:默认非公平锁,可通过构造函数 new ReentrantLock(true) 实现公平锁。

(3)尝试获取锁

  • ReentrantLock 的 tryLock() 方法可尝试获取锁,成功返回 true,失败返回 false,无需阻塞:

    java

    运行

    if (lock.tryLock(1, TimeUnit.SECONDS)) { // 超时等待1秒
        try {
            // 临界区
        } finally {
            lock.unlock();
        }
    } else {
        // 获取锁失败的处理逻辑
    }
    
  • synchronized 无此功能,获取不到锁时会直接阻塞。

(4)条件变量(Condition)

  • ReentrantLock 可通过 newCondition() 创建多个 Condition 对象,实现更灵活的线程通信:

    java

    运行

    ReentrantLock lock = new ReentrantLock();
    Condition notEmpty = lock.newCondition();
    Condition notFull = lock.newCondition();
    
    // 线程1等待条件
    lock.lock();
    try {
        while (queue.isEmpty()) {
            notEmpty.await(); // 等待“非空”信号
        }
    } finally {
        lock.unlock();
    }
    
    // 线程2发送信号
    lock.lock();
    try {
        queue.add(element);
        notEmpty.signal(); // 通知“非空”条件已满足
    } finally {
        lock.unlock();
    }
    
  • synchronized 仅通过对象的 wait()/notify()/notifyAll() 实现通信,且一个对象只能有一组条件。

4. 选择建议

  • 优先使用 synchronized

    • 代码更简洁,无需手动管理锁的释放,降低出错风险。
    • 适合简单的同步场景(如单条件的线程通信)。
    • JVM 对其优化持续升级(如偏向锁、轻量级锁)。
  • 使用 ReentrantLock 场景

    • 需要中断等待锁的线程(lockInterruptibly())。
    • 需要公平锁机制。
    • 需要尝试获取锁(tryLock())或超时等待。
    • 需要多个条件变量(Condition)。
    • 需要查询锁的状态(如监控锁的持有情况)。

总结

synchronized 是 Java 原生的简单同步机制,适合大多数基础场景;ReentrantLock 提供更丰富的功能,灵活性更高,适合复杂的同步需求。实际开发中需根据具体场景选择,两者的核心目的都是保证线程安全,但特性和使用方式差异较大。