菜单

Administrator
发布于 2026-05-18 / 2 阅读
0
0

可重入锁和 Synchronized 的异同

深入对比:Java 可重入锁(ReentrantLock)与 synchronized 的异同

一、前言

在 Java 并发编程中,synchronizedReentrantLock 是最常用的两种锁机制。synchronized 是 JVM 层面的关键字,而 ReentrantLock 是 JDK 提供的 API 层面的可重入锁。本文将从多个维度深入对比二者的异同,帮助读者在实际开发中做出合适的选择。


二、相同点

1. 可重入性(Reentrancy)

两者都支持可重入:同一个线程在持有锁的情况下,可以再次获取同一把锁而不被阻塞。

synchronized 可重入示例:

public class SynchronizedReentrantDemo {

    public synchronized void outer() {
        System.out.println("进入 outer 方法");
        inner(); // 同一个线程再次获取锁
    }

    public synchronized void inner() {
        System.out.println("进入 inner 方法");
    }

    public static void main(String[] args) {
        new SynchronizedReentrantDemo().outer();
    }
}

ReentrantLock 可重入示例:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockReentrantDemo {

    private final ReentrantLock lock = new ReentrantLock();

    public void outer() {
        lock.lock();
        try {
            System.out.println("进入 outer 方法");
            inner(); // 同一个线程再次获取锁
        } finally {
            lock.unlock();
        }
    }

    public void inner() {
        lock.lock();
        try {
            System.out.println("进入 inner 方法");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        new ReentrantLockReentrantDemo().outer();
    }
}

2. 互斥性(Mutual Exclusion)

两者都保证互斥访问:同一时刻最多只有一个线程可以持有锁,其他尝试获取锁的线程会被阻塞或等待。


三、不同点

1. 锁的获取与释放方式

特性 synchronized ReentrantLock
获取锁 隐式获取,由 JVM 自动管理 显式调用 lock()tryLock()
释放锁 隐式释放,退出同步块/方法时自动释放 必须显式调用 unlock()通常放在 finally 块中

ReentrantLock 必须手动释放锁:

// ✅ 正确用法
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区代码
} finally {
    lock.unlock(); // 确保释放锁
}

// ❌ 错误用法 —— 忘记释放锁可能导致死锁
lock.lock();
// 临界区代码
// 忘记调用 lock.unlock()  —— 其他线程将永远无法获取锁

2. 公平性(Fairness)

特性 synchronized ReentrantLock
公平策略 不支持公平锁,是非公平的 支持公平锁和非公平锁(默认非公平)

ReentrantLock 公平锁示例:

// 公平锁:按照线程请求锁的先后顺序分配锁
ReentrantLock fairLock = new ReentrantLock(true);

// 非公平锁(默认):允许插队,可能导致线程饥饿
ReentrantLock unfairLock = new ReentrantLock(false);
ReentrantLock defaultLock = new ReentrantLock(); // 等价于 false

注意:公平锁会牺牲一定的吞吐量来保证公平性。在大多数场景下,非公平锁的性能优于公平锁。

3. Condition 条件变量

特性 synchronized ReentrantLock
等待/通知机制 只有一个隐式的条件队列(wait() / notify() / notifyAll() 支持多个 Condition 对象,可以精细控制线程的等待和唤醒

synchronized 的 wait/notify:

public class SynchronizedConditionDemo {

    private final Object lock = new Object();
    private boolean ready = false;

    public void await() throws InterruptedException {
        synchronized (lock) {
            while (!ready) {
                lock.wait(); // 等待
            }
            System.out.println("条件满足,继续执行");
        }
    }

    public void signal() {
        synchronized (lock) {
            ready = true;
            lock.notifyAll(); // 唤醒所有等待线程
        }
    }
}

ReentrantLock 的 Condition(支持多个条件队列):

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockConditionDemo {

    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull  = lock.newCondition();  // 队列未满条件
    private final Condition notEmpty = lock.newCondition();  // 队列非空条件
    private final Object[] buffer = new Object[10];
    private int putIndex, takeIndex, count;

    public void put(Object item) throws InterruptedException {
        lock.lock();
        try {
            while (count == buffer.length) {
                notFull.await(); // 队列已满,等待
            }
            buffer[putIndex] = item;
            if (++putIndex == buffer.length) putIndex = 0;
            count++;
            notEmpty.signal(); // 唤醒等待的消费者
        } finally {
            lock.unlock();
        }
    }

    @SuppressWarnings("unchecked")
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await(); // 队列为空,等待
            }
            Object item = buffer[takeIndex];
            if (++takeIndex == buffer.length) takeIndex = 0;
            count--;
            notFull.signal(); // 唤醒等待的生产者
            return item;
        } finally {
            lock.unlock();
        }
    }
}

Condition 的优势在于:一个锁可以绑定多个条件,避免使用 notifyAll() 盲目唤醒所有等待线程,提高了效率。

4. 可中断性(Interruptibility)

特性 synchronized ReentrantLock
响应中断 获取锁时不可中断,线程会一直阻塞直到获取锁 支持中断响应,可以通过 lockInterruptibly() 在等待锁时响应中断

ReentrantLock 可中断获取锁:

import java.util.concurrent.locks.ReentrantLock;

public class InterruptibleLockDemo {

    private final ReentrantLock lock = new ReentrantLock();

    public void performTask() {
        try {
            // 可中断地获取锁:如果线程被中断,会抛出 InterruptedException
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName() + " 获取到锁");
                Thread.sleep(5000); // 模拟耗时操作
            } finally {
                lock.unlock();
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " 被中断,放弃等待锁");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        InterruptibleLockDemo demo = new InterruptibleLockDemo();

        Thread t1 = new Thread(demo::performTask, "线程1");
        Thread t2 = new Thread(demo::performTask, "线程2");

        t1.start();
        Thread.sleep(100); // 确保 t1 先拿到锁
        t2.start();
        Thread.sleep(100);

        t2.interrupt(); // 中断 t2,t2 将放弃等待锁
    }
}

synchronized 在等待锁时无法被中断,这可能导致死锁后无法恢复。ReentrantLock 的 lockInterruptibly() 提供了一种"可取消"的锁获取机制。

5. 尝试获取锁(tryLock)

特性 synchronized ReentrantLock
尝试获取 不支持 支持 tryLock()tryLock(long timeout, TimeUnit unit),可设置超时
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TryLockDemo {

    private final ReentrantLock lock = new ReentrantLock();

    public void tryAcquire() {
        // 立即尝试,获取不到就返回 false
        if (lock.tryLock()) {
            try {
                System.out.println(Thread.currentThread().getName() + " 立即获取到锁");
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " 未能获取到锁,做其他事情");
        }
    }

    public void tryAcquireWithTimeout() {
        try {
            // 等待 1 秒,如果 1 秒内获取不到则放弃
            if (lock.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    System.out.println(Thread.currentThread().getName() + " 在超时内获取到锁");
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " 超时未获得到锁");
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " 被中断");
        }
    }
}

6. 锁的实现与性能

特性 synchronized ReentrantLock
实现层级 JVM 层面(基于 monitor 对象) JDK 层面(基于 AbstractQueuedSynchronizer, AQS)
JDK 5 性能 较差(重量级锁) 较好
JDK 6+ 性能 显著优化(锁升级:偏向锁 → 轻量级锁 → 重量级锁) 与 synchronized 差距缩小

JDK 6 之后,synchronized 引入了锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),在无竞争或低竞争场景下性能大幅提升。在大多数现代 JDK 版本中,两者性能相差不大,synchronized 甚至在某些场景下更优。

7. 其他特性对比

特性 synchronized ReentrantLock
底层实现 基于 monitor 对象,借助操作系统 mutex 基于 AQS(AbstractQueuedSynchronizer)
锁类型 非公平锁 公平锁 / 非公平锁
锁状态查询 不支持 支持 isLocked()getQueueLength()hasQueuedThreads()
代码写法 简洁,关键字修饰 需手动加锁/解锁,代码稍复杂
异常处理 锁自动释放,不易出现死锁 忘记 unlock() 可能导致死锁
调试友好度 栈信息清晰,JVM 有优化 栈信息稍复杂

四、完整对比总结表

对比维度 synchronized ReentrantLock
可重入性 ✅ 支持 ✅ 支持
互斥性 ✅ 保证 ✅ 保证
加锁/解锁方式 隐式(JVM 自动管理) 显式(需在 finally 中手动释放)
公平性 ❌ 仅非公平 ✅ 公平 + 非公平(可选)
Condition ❌ 仅一个隐式条件队列 ✅ 支持多个 Condition 对象
可中断 ❌ 不可中断 ✅ 支持 lockInterruptibly()
tryLock ❌ 不支持 ✅ 支持超时尝试
性能 (JDK 5) 较差 较好
性能 (JDK 8+) 优秀(经锁升级优化) 优秀,与 synchronized 接近
锁状态查询 ❌ 不支持 ✅ 支持
代码简洁性 ⭐⭐⭐⭐⭐ ⭐⭐⭐
灵活度 ⭐⭐⭐ ⭐⭐⭐⭐⭐

五、如何选择?

优先使用 synchronized 的场景

  1. 代码简洁优先 —— 不需要手动释放锁,不易出错
  2. 低竞争场景 —— JDK 6+ 经锁升级优化后很高效
  3. 不需要额外特性 —— 不需要公平锁、Condition、可中断等高级功能
  4. 团队规范 —— 大多数团队默认优先使用 synchronized

优先使用 ReentrantLock 的场景

  1. 需要公平锁 —— 如某些对公平性有要求的调度场景
  2. 需要多个 Condition —— 如生产者-消费者模式(多条件等待/唤醒)
  3. 需要可中断的锁获取 —— 避免死锁后无法恢复
  4. 需要尝试获取锁(超时) —— 避免无限期等待
  5. 需要查询锁状态 —— 调试或监控需求

六、经典面试题

Q1:synchronized 和 ReentrantLock 的性能谁更好?

:在 JDK 6 之前,synchronized 性能较差,ReentrantLock 有明显优势。JDK 6 引入了锁升级机制后,synchronized 在无竞争时近乎无开销,两者性能差距大幅缩小。在 JDK 8+ 中,两者性能基本相当,synchronized 在某些场景下甚至更优。选择时不应以性能为主要考量,而应根据功能需求决定。

Q2:ReentrantLock 如何实现公平锁?

:ReentrantLock 基于 AQS(AbstractQueuedSynchronizer)实现。公平锁模式下,线程获取锁时会先检查同步队列中是否有等待时间更长的线程;如果有,则当前线程加入队列尾部,不尝试抢占。非公平锁模式下,线程会先尝试 CAS 抢锁,抢不到再入队。

Q3:synchronized 底层是如何实现的?

:synchronized 基于 Monitor 对象实现,编译后会在同步代码块前后生成 monitorentermonitorexit 字节码指令。JDK 6 后引入了锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),通过对象头中的 Mark Word 标记锁状态,尽量减少操作系统级别的线程阻塞。


七、总结

结论 说明
功能相似 两者都提供可重入的互斥锁,保证线程安全
synchronized 更简洁 隐式加解锁,代码更少,不易出错
ReentrantLock 更灵活 支持公平锁、Condition、可中断、tryLock 等高级特性
性能差距已缩小 JDK 6+ 两者性能接近,synchronized 在某些场景反而更优
推荐原则 能用 synchronized 就用 synchronized;需要高级特性时用 ReentrantLock

最后的建议:在日常开发中,如果 synchronized 能满足需求,优先使用 synchronized;只有当你确实需要公平锁、多个 Condition、可中断获取、超时尝试等高级功能时,再考虑使用 ReentrantLock。


评论