ReentrantLock 和 synchronized 到底选哪个?源码级对比与选型

ReentrantLock 和 synchronized 到底选哪个?源码级对比与选型

面试常考:公平/非公平锁、可中断、超时获取、Condition、锁选型

一、从一道面试题开始

"ReentrantLock 和 synchronized 有什么区别?什么时候用哪个?"------这是 Java 面试中几乎必问的并发问题。大多数人能说出"ReentrantLock 可以公平、可以中断、可以超时",但能说清楚底层实现差异和选型逻辑的不多。本文从源码层面做深度对比。

二、核心特性对比

特性 synchronized ReentrantLock
实现层面 JVM 内置 (Monitor) Java 层 (AQS)
锁状态 对象头 Mark Word volatile int state
锁释放 自动 (出作用域) 手动 (finally unlock)
可中断 ✅ lockInterruptibly
超时获取 ✅ tryLock(timeout)
公平锁 ❌ 只有非公平 ✅ 可选公平/非公平
条件变量 1个 (wait/notify) 多个 Condition
锁绑定 对象 Lock 对象
可重入
锁消除/粗化 ✅ JIT优化
内存开销 低 (对象头) 高 (AQS Node + 队列)

三、加锁流程对比

synchronized 加锁(JVM 层)

javascript 复制代码
synchronized(obj) {
    // code
}

JVM 字节码:
monitorenter      ← 进入 Monitor
// code
monitorexit       ← 退出 Monitor (正常)
...
monitorexit       ← 退出 Monitor (异常)

锁升级链路:
  偏向锁 → 轻量级锁 →  重量级锁
  (对象头 Mark Word 驱动)

ReentrantLock 加锁(AQS 层)

java 复制代码
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // code
} finally {
    lock.unlock();  // 必须 finally 中释放
}
非公平锁 lock 源码
java 复制代码
// NonfairSync
final void lock() {
    if (compareAndSetState(0, 1))          // 直接 CAS 抢锁
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);                         // 抢不到走 AQS acquire
}

// AQS.acquire
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&               // 再次尝试 CAS / 重入
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

// tryAcquire
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {  // CAS
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;  // 重入
        setState(nextc);
        return true;
    }
    return false;
}
公平锁 lock 源码
java 复制代码
// FairSync
final void lock() {
    acquire(1);  // 不直接 CAS,先走 acquire
}

final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 关键区别:检查队列中是否有前驱在等待
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        setState(nextc);
        return true;
    }
    return false;
}

加锁流程对比图

scss 复制代码
         synchronized                        ReentrantLock (非公平)
┌──────────────────────┐              ┌──────────────────────┐
│  monitorenter        │              │  lock.lock()         │
└──────────┬───────────┘              └──────────┬───────────┘
           │                                     │
    ┌──────▼──────┐                      ┌──────▼──────┐
    │ 偏向锁?     │                      │ CAS抢state  │
    │ Mark Word   │                      │ state 0→1   │
    └──┬──────┬───┘                      └──┬──────┬───┘
    成功│      │失败                    成功│      │失败
    ┌──▼┐  ┌──▼────────┐               ┌──▼┐  ┌──▼─────────┐
    │进入│  │轻量级锁   │               │进入│  │acquire()   │
    └───┘  │CAS LockRec│               └───┘  │tryAcquire  │
           └──┬─────┬──┘                      └──┬──────┬───┘
          成功│     │失败                   成功│      │失败
          ┌──▼┐  ┌──▼──────┐                ┌──▼┐  ┌──▼────────┐
          │进入│  │自旋等待 │                │进入│  │addWaiter  │
          └───┘  └──┬──────┘                └───┘  │入CLH队列  │
                    │                              └──┬────────┘
               ┌────▼────┐                     ┌──────▼──────┐
               │重量级锁 │                     │park阻塞     │
               │Monitor  │                     │等unpark唤醒 │
               │内核阻塞 │                     └─────────────┘
               └─────────┘

四、独有特性深度拆解

1. 可中断锁

java 复制代码
// synchronized 不可响应中断
synchronized (lock) {
    // 如果在这里被阻塞,interrupt 无法打断
}

// ReentrantLock 可中断
ReentrantLock lock = new ReentrantLock();
try {
    lock.lockInterruptibly();  // 等待锁的过程中可被中断
    // 持有锁
} catch (InterruptedException e) {
    // 被中断,未获取到锁
} finally {
    if (lock.isHeldByCurrentThread())
        lock.unlock();
}

源码:

java 复制代码
public void lockInterruptibly() throws InterruptedException {
    acquireInterruptibly(1);
}

public final void acquireInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);  // park 时检查中断,抛异常
}

private void doAcquireInterruptibly(int arg) throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 关键:被中断时抛异常而非设置标志
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

2. 超时获取锁

java 复制代码
// 尝试在 3 秒内获取锁
if (lock.tryLock(3, TimeUnit.SECONDS)) {
    try {
        // 持有锁
    } finally {
        lock.unlock();
    }
} else {
    // 超时未获取到
}

源码核心:

java 复制代码
public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    long lastTime = System.nanoTime();
    final Node node = addWaiter(Node.EXCLUSIVE);
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null;
                return true;
            }
            if (nanosTimeout <= 0) {      // 超时
                cancelAcquire(node);
                return false;
            }
            // 超过 1ms 才 park(否则自旋更高效)
            if (nanosTimeout > spinForTimeoutThreshold &&
                shouldParkAfterFailedAcquire(p, node))
                LockSupport.parkNanos(this, nanosTimeout);  // 限时阻塞
            long now = System.nanoTime();
            if (Thread.interrupted())
                throw new InterruptedException();
            nanosTimeout -= now - lastTime;  // 扣减剩余时间
            lastTime = now;
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

3. Condition 多条件变量

java 复制代码
public class TaskQueue {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition taskAvailable = lock.newCondition();
    private final Condition spaceAvailable = lock.newCondition();
    private final Queue<Runnable> queue = new LinkedList<>();
    private final int capacity = 10;

    public void submit(Runnable task) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity)
                spaceAvailable.await();  // 等待空间
            queue.offer(task);
            taskAvailable.signal();      // 通知消费者
        } finally {
            lock.unlock();
        }
    }

    public Runnable take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty())
                taskAvailable.await();   // 等待任务
            Runnable task = queue.poll();
            spaceAvailable.signal();     // 通知生产者
            return task;
        } finally {
            lock.unlock();
        }
    }
}

五、公平锁 vs 非公平锁:性能差异

java 复制代码
public class FairLockBenchmark {
    private static final int THREADS = 10;
    private static final int LOOPS = 100000;

    static void benchmark(boolean fair) throws Exception {
        ReentrantLock lock = new ReentrantLock(fair);
        AtomicLong counter = new AtomicLong();
        ExecutorService pool = Executors.newFixedThreadPool(THREADS);
        long start = System.nanoTime();

        for (int i = 0; i < THREADS; i++) {
            pool.submit(() -> {
                for (int j = 0; j < LOOPS; j++) {
                    lock.lock();
                    try {
                        counter.incrementAndGet();
                    } finally {
                        lock.unlock();
                    }
                }
            });
        }
        pool.shutdown();
        pool.awaitTermination(60, TimeUnit.SECONDS);
        long elapsed = System.nanoTime() - start;
        System.out.printf("%s: %dms, count=%d%n",
            fair ? "公平锁" : "非公平锁",
            elapsed / 1_000_000, counter.get());
    }

    public static void main(String[] args) throws Exception {
        benchmark(false);  // 非公平: ~150ms
        benchmark(true);   // 公平: ~800ms
    }
}

为什么非公平锁快?

css 复制代码
公平锁: 必须排队,每次锁释放 → 线程切换(park/unpark)
  T1释放 → 唤醒T2(线程切换) → T2获取 → T2释放 → 唤醒T3...

非公平锁: 新线程直接插队,可能不用阻塞
  T1释放 → T3恰好在此时请求 → T3直接CAS获取(无切换)
  T1释放 → T3在阻塞 → 唤醒T3(有切换,但不是每次都要)

六、synchronized 的独特优势

1. 锁消除和锁粗化

java 复制代码
// JIT 逃逸分析 → 锁消除
public String concat(String a, String b) {
    StringBuffer sb = new StringBuffer();  // 局部变量不逃逸
    sb.append(a);  // synchronized 被消除
    sb.append(b);
    return sb.toString();
}

// 锁粗化
public void batchOps(Object lock) {
    synchronized (lock) { op1(); }
    synchronized (lock) { op2(); }  // 合并为
    synchronized (lock) { op3(); }  // synchronized(lock){op1();op2();op3();}
}

2. 使用简洁,不会忘记释放

java 复制代码
// synchronized: 超出作用域自动释放
synchronized (lock) {
    doWork();
}  // 自动释放

// ReentrantLock: 忘记 unlock → 死锁
lock.lock();
try {
    doWork();
} finally {
    lock.unlock();  // 必须有
}

3. 内存开销低

yaml 复制代码
synchronized: 锁信息在对象头 Mark Word (0 额外对象)
ReentrantLock: AQS Node 对象 + ConditionObject + CLH 队列

100万个锁对象:
  synchronized: ~16MB (对象头16字节 × 100万)
  ReentrantLock: ~40MB (额外对象开销)

七、选型决策

scss 复制代码
                  ┌──────────────────────┐
                  │  需要加锁吗?        │
                  └──────────┬───────────┘
                             │
                    ┌────────▼────────┐
                    │ 需要以下特性?   │
                    │ • 公平锁        │
                    │ • 可中断        │──Yes──▶ ReentrantLock
                    │ • 超时获取      │
                    │ • 多个Condition │
                    └────────┬────────┘
                             │ No
                    ┌────────▼────────┐
                    │ 锁持有时间长?   │──Yes──▶ ReentrantLock
                    │ (长任务)        │        (避免自旋浪费)
                    └────────┬────────┘
                             │ No
                    ┌────────▼────────┐
                    │ synchronized    │
                    │ (简单场景首选)   │
                    └─────────────────┘

选型建议总结

场景 推荐 原因
简单同步块 synchronized 简洁、自动释放、无内存开销
需要公平排队 ReentrantLock(fair) synchronized 不支持公平
需要中断等待 ReentrantLock lockInterruptibly()
需要超时获取 ReentrantLock tryLock(timeout)
需要多个条件队列 ReentrantLock 多 Condition
锁持有极短 synchronized JVM 优化好
读写分离场景 ReentrantReadWriteLock 读读不互斥
高并发读+少量写 StampedLock 乐观读

八、面试高频问题速答

Q1: 为什么 ReentrantLock 非公平锁默认开启?

非公平锁吞吐量更高。新来的线程直接 CAS 抢锁,如果恰好锁刚释放,可以直接获取而不用 park/unpark,省去了线程切换开销。极端情况下可能饿死排队线程,但实际竞争场景中 starvation 概率极低。

Q2: ReentrantLock 怎么实现重入?

通过 state 计数。tryAcquire 时如果 state==0 直接 CAS 获取,如果 state>0 且当前线程是持有线程,则 state+1 表示重入。释放时 state-1,减到 0 才真正释放锁。

Q3: synchronized 会被 ReentrantLock 完全替代吗?

不会。synchronized 有 JVM 层面的锁消除、锁粗化优化,内存开销低,使用简洁不会忘记释放。JDK 6+ 锁升级优化后性能也很好。大多场景 synchronized 是够用的,ReentrantLock 只在需要高级特性时使用。

Q4: tryLock() 和 tryLock(0, TimeUnit.NANOSECONDS) 有什么区别?

tryLock() 是无参版本,如果锁空闲就获取,不等待。tryLock(0, NANOSECONDS) 即使传 0 也会走 doAcquireNanos 路程,有入队和取消的开销。但语义不同:无参 tryLock() 不响应中断,有参版本响应中断。

Q5: Condition 的 signal 和 signalAll 区别?

signal() 只唤醒等待队列第一个线程,signalAll() 唤醒所有。signalAll 更安全但效率低,因为被唤醒的线程只有一个能获取锁,其余继续 park。推荐用 signal + while 循环条件检查。

九、总结

ReentrantLock 和 synchronized 不是互斥选择,而是互补关系:

  • synchronized:JVM 内置锁,锁升级优化,适合简单同步场景,无内存开销,由 JVM 自动管理释放
  • ReentrantLock:基于 AQS 的 Java 层锁,提供公平锁、可中断、超时获取、多 Condition 等高级特性,适合复杂并发控制需求

选型核心逻辑:简单场景用 synchronized,需要高级特性时才用 ReentrantLock。不要为了"看起来更专业"而滥用 ReentrantLock------它带来的内存开销和必须手动释放的风险,在简单场景下是不必要的设计负债。

相关推荐
计算机毕设定制辅导-无忧学长1 小时前
基于Spring Boot的玄幻小说个性化推荐平台设计与实现
java·vue.js·spring boot·后端·mysql·推荐算法
码路漫漫1 小时前
LRU 真的能解决日志按业务 Key 分文件的落地瓶颈吗?
java·后端
带刺的坐椅1 小时前
不只是 Token 流:Solon AI 4.1 的语义化聊天事件
java·stream·event·solon-ai
延凡科技2 小时前
建筑节能新视角:智慧冷站的数字化实践
java·人工智能·能源·暖通
www_aiyuanma_vip2 小时前
Java个人博客系统
java·开发语言
秋名RG2 小时前
Java 集合框架完全指南:从核心接口到 JDK 21 有序集合新特性
java·windows
蜡台2 小时前
Kotlin 五大作用域函数详解|let/run/apply/also/with 选型指南+实战避坑
android·java·kotlin
AC赳赳老秦3 小时前
农产品公开数据应用:OpenClaw 抓取农产品价格、产销公开数据,实现农产品行情动态监测
java·c语言·javascript·python·php·deepseek·openclaw
1001101_QIA3 小时前
从 CPU 到 GPU:高速缓存、指令并行与 CUDA 执行原理入门
java·后端·spring