synchronized 和 ReentrantLock 的区别是什么?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 锁机制的全面认知:面试官不仅仅是想让你列几条不同点,更是想知道你是否理解两种锁的设计哲学——synchronized 是 JVM 层面的 "内建锁",ReentrantLock 是 API 层面的 "显式锁"。这两条路线的差异,决定了它们在功能、性能和使用方式上的根本不同。

  2. 实战选型能力:考察你在实际项目中能否根据业务场景选择合适的锁。比如需要公平锁、需要尝试获取锁、需要多个条件变量时,用 synchronized 就搞不定。

  3. 底层原理深度:如果你能说出 synchronized 的锁升级过程(偏向锁 → 轻量级锁 → 重量级锁),以及 ReentrantLock 的 AQS 原理,面试官会认为你是真正理解了,而不是在背八股。

核心答案

先甩一张总览表:

对比维度synchronizedReentrantLock
实现层面JVM 关键字(内建锁)JDK API(java.util.concurrent.locks
加锁/解锁方式自动(进入同步块加锁,退出自动释放)手动(lock() / unlock(),必须在 finally 中释放)
可中断性不可中断(等待锁时无法响应中断)可中断(lockInterruptibly()
公平性只有非公平锁支持公平锁和非公平锁(构造参数控制)
条件变量只有 wait() / notify(),一个等待队列支持 Condition,多个等待队列
尝试获取锁不支持(拿不到就一直阻塞等)支持(tryLock(),拿不到立刻返回 false)
超时获取锁不支持支持(tryLock(timeout, unit)
锁绑定条件只有一个条件多个 Condition,精确唤醒
可重入性✅ 可重入✅ 可重入
性能JDK 6 之后经过锁升级优化,性能不差高并发下略优,功能更灵活

简单记:synchronized 是 "自动挡",简单省心;ReentrantLock 是 "手动挡",灵活但需要自己控制

深度解析

一、基本用法对比

// ============ synchronized 用法 ============
public class SyncDemo {
    // 修饰实例方法(锁是当前实例对象)
    public synchronized void method1() {
        // 业务逻辑
    }

    // 修饰静态方法(锁是当前类的 Class 对象)
    public static synchronized void method2() {
        // 业务逻辑
    }

    // 修饰代码块(锁是括号里的对象)
    public void method3() {
        synchronized (this) {
            // 业务逻辑
        }
    }
}
// ============ ReentrantLock 用法 ============
public class LockDemo {
    private final ReentrantLock lock = new ReentrantLock(); // 默认非公平锁

    public void method() {
        lock.lock();   // 手动加锁
        try {
            // 业务逻辑
        } finally {
            lock.unlock();  // 必须在 finally 中释放!忘了就死锁了
        }
    }
}

注意看区别:synchronized 不需要手动释放锁,JVM 帮你搞定;ReentrantLock 必须在 finallyunlock()忘记释放锁是一个常见的生产事故来源。这点在面试中提到,会显得你很有实战意识。

二、ReentrantLock 的四大高级能力

synchronized 搞不定的场景,才是 ReentrantLock 的主战场。

1. 尝试获取锁——避免死锁

ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();

// tryLock() 拿不到锁立刻返回 false,不会阻塞等待
if (lock1.tryLock()) {
    try {
        if (lock2.tryLock()) {
            try {
                // 两把锁都拿到了,执行业务逻辑
            } finally {
                lock2.unlock();
            }
        }
    } finally {
        lock1.unlock();
    }
}
// 拿不到就做别的事,不会死等,有效避免死锁

这种 "拿不到就走" 的模式,用 synchronized 根本做不到。synchronized 一旦进入等待,就只能阻塞,什么都干不了。

2. 超时获取锁——防止无限等待

// 等待 3 秒,拿不到就放弃
if (lock.tryLock(3, TimeUnit.SECONDS)) {
    try {
        // 业务逻辑
    } finally {
        lock.unlock();
    }
} else {
    // 超时未获取到锁,执行降级逻辑
    log.warn("获取锁超时,执行降级方案");
}

这个在防止系统雪崩时特别有用——如果大量线程都在等同一把锁,超时机制能快速失败,不至于把线程池打满。

3. 可中断获取锁——优雅响应中断

// lockInterruptibly() 在等待锁的过程中可以响应中断
try {
    lock.lockInterruptibly();
    try {
        // 业务逻辑
    } finally {
        lock.unlock();
    }
} catch (InterruptedException e) {
    // 被中断了,做清理工作
    Thread.currentThread().interrupt();
}

synchronized 在等待锁时是无法响应中断的。如果你需要优雅地停止一个正在等锁的线程,只能用 ReentrantLock

4. Condition 精确唤醒——多个等待队列

上图对比了两种锁的唤醒机制,核心区别在于:

  • synchronized 只有一个等待队列,调用 notify() 时随机唤醒一个线程,调用 notifyAll() 时全部唤醒。问题是——如果你有两种不同条件的等待线程混在一个队列里,根本没法精确唤醒 "等条件 A" 的线程。你只能 notifyAll(),把所有人都叫醒,不符合条件的再接着等。这就是所谓的 "惊群效应"。

  • ReentrantLock 支持多个 Condition,每个 Condition 是独立的等待队列。你可以精确地只唤醒 "等某个条件" 的线程。生产者-消费者模型中,这特别好用。

// 经典的生产者-消费者模型(Condition 精确唤醒)
public class BoundedBuffer {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull  = lock.newCondition();  // 条件 1:不满
    private final Condition notEmpty = lock.newCondition();  // 条件 2:不空
    private final Object[] items = new Object[100];
    private int putIdx, takeIdx, count;

    // 生产者:往队列里放元素
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();       // 队列满了,等待 "不满" 条件
            items[putIdx] = x;
            if (++putIdx == items.length) putIdx = 0;
            count++;
            notEmpty.signal();          // 精确唤醒等待 "不空" 条件的消费者
        } finally {
            lock.unlock();
        }
    }

    // 消费者:从队列里取元素
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();       // 队列空了,等待 "不空" 条件
            Object x = items[takeIdx];
            if (++takeIdx == items.length) takeIdx = 0;
            count--;
            notFull.signal();            // 精确唤醒等待 "不满" 条件的生产者
            return x;
        } finally {
            lock.unlock();
        }
    }
}

三、底层实现原理

两者底层走的是完全不同的路线:

维度synchronizedReentrantLock
实现JVM 层面(字节码指令 monitorenter / monitorexitAPI 层面(基于 AQS)
核心对象头中的 Mark Word 和 MonitorAQS 的 state 变量 + CLH 队列
优化锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁CAS + LockSupport.park()/unpark()

synchronized 在 JDK 6 之前是纯重量级锁(依赖操作系统的 Mutex),性能确实差。但 JDK 6 引入了锁升级机制后,大部分场景下性能已经和 ReentrantLock 差不多了。所以 "ReentrantLock 性能更好" 这个说法已经过时了,面试中别踩这个坑。

面试高频追问

  1. 追问一:synchronized 的锁升级过程了解吗?

    JDK 6 之后,synchronized 引入了锁升级机制:无锁 → 偏向锁(只有一个线程访问时,直接把线程 ID 记录在对象头的 Mark Word 中)→ 轻量级锁(多个线程交替访问时,用 CAS 自旋获取锁)→ 重量级锁(真正竞争激烈时,膨胀为操作系统的 Mutex 锁,线程阻塞)。这个升级是单向的,不可降级(实际上重量级锁在特定条件下可以降级,但 STW 场景才发生,可以忽略)。

  2. 追问二:什么是公平锁和非公平锁?ReentrantLock 默认是哪种?

    公平锁按线程等待的先后顺序获取锁(先到先得),非公平锁允许插队。ReentrantLock 默认是 非公平锁new ReentrantLock() 等价于 new ReentrantLock(false))。非公平锁性能更好,因为减少了线程上下文切换,但可能导致线程 "饥饿"。实际开发中绝大部分场景用非公平锁就够了。

  3. 追问三:AQS 是什么?

    AQS(AbstractQueuedSynchronizer)是 java.util.concurrent 的核心框架。它内部维护一个 volatile int state 变量表示锁状态,以及一个 CLH 双向队列管理等待线程。ReentrantLockSemaphoreCountDownLatch 等都是基于 AQS 实现的。

常见面试变体

  • "什么场景下用 ReentrantLock 比 synchronized 更合适?"
  • "synchronized 底层是怎么实现的?"
  • "什么是可重入锁?synchronized 是可重入锁吗?"
  • "公平锁和非公平锁有什么区别?"

记忆口诀

选锁口诀:简单同步用 synchronized(自动挡、省心),需要高级功能用 ReentrantLock(手动挡、灵活)。高级功能指:尝试获取、超时获取、可中断、多条件队列。

总结

synchronizedReentrantLock 不是谁替代谁的关系,而是互补的。简单场景用 synchronized(代码简洁、不会忘记释放锁),需要尝试获取、超时获取、可中断、多个条件队列时用 ReentrantLock。面试中把 "自动挡 vs 手动挡" 的比喻说出来,再配合 AQS 和锁升级的原理,基本稳了。