Synchronized 与 ReentrantLock 深度对比

前言

在Java并发编程中,锁机制是保证线程安全的核心手段。synchronizedReentrantLock 是两种最常用的锁实现,面试中经常被要求对比它们的区别。

本文将深入分析两者的底层原理、功能特性、性能差异以及各自的适用场景。


一、快速概览

维度 synchronized ReentrantLock
类型 关键字(JVM实现) API类(Java实现)
锁获取/释放 自动,JVM保证释放 手动,必须在finally中unlock
灵活性 较低 高,支持tryLock、超时、中断
公平性 非公平锁 默认非公平,支持公平锁
条件变量 单一(wait/notify) 多个Condition
锁升级 JDK 6后支持
底层实现 对象头Mark Word + 操作系统Mutex AQS + CAS

二、Synchronized 详解

2.1 使用方式

java 复制代码
// 1. 修饰实例方法:锁当前实例对象
public synchronized void method1() {
    // 业务逻辑
}

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

// 3. 修饰代码块:锁指定对象
public void method3() {
    synchronized (this) {
        // 业务逻辑
    }
}

2.2 底层原理

synchronized 的锁信息存储在对象头的 Mark Word 中。

JDK 6 之前的实现

基于操作系统的**互斥量(Mutex)**实现,每次加锁/解锁都需要从用户态切换到内核态,开销较大,被称为"重量级锁"。

JDK 6 之后的锁升级机制

为了减少重量级锁的开销,JVM引入了锁升级机制,锁状态从低到高逐步升级(不可降级):

复制代码
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
锁状态 适用场景 原理
偏向锁 只有一个线程重复获取锁 记录线程ID,无需CAS
轻量级锁 少量线程竞争 CAS自旋尝试获取,不自旋过度
重量级锁 多线程激烈竞争 阻塞等待,操作系统Mutex

锁升级的好处:在低竞争场景下,避免了操作系统层面的线程阻塞,大幅提升了性能。

2.3 锁的释放

synchronized 的锁释放是自动的

  • 方法执行完毕自动释放
  • 代码块执行完毕自动释放
  • 抛出异常时JVM自动释放

优点:不会因忘记释放锁而导致死锁。


三、ReentrantLock 详解

3.1 使用方式

java 复制代码
public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void doSomething() {
        lock.lock();  // 获取锁
        try {
            // 业务逻辑
        } finally {
            lock.unlock();  // 必须手动释放
        }
    }
}

3.2 核心特性

① 可重入性

同一个线程可以多次获取同一把锁,每获取一次计数器+1,释放一次计数器-1,计数器归零时锁才真正释放。

java 复制代码
lock.lock();
lock.lock();  // 可重入
try {
    // 业务逻辑
} finally {
    lock.unlock();
    lock.unlock();  // 需要释放两次
}
② 公平锁与非公平锁
java 复制代码
// 非公平锁(默认)
ReentrantLock nonFairLock = new ReentrantLock();

// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);

公平锁 :线程按照请求顺序获取锁,保证FIFO。
非公平锁:允许插队,吞吐量更高(减少线程挂起/唤醒开销)。

③ 尝试获取锁(tryLock)
java 复制代码
// 非阻塞获取
if (lock.tryLock()) {
    try {
        // 获取成功
    } finally {
        lock.unlock();
    }
} else {
    // 获取失败,执行其他逻辑
}

// 带超时获取
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    // ...
}
④ 可中断获取锁
java 复制代码
lock.lockInterruptibly();  // 响应中断
⑤ 条件变量(Condition)
java 复制代码
class BoundedBuffer {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    
    public void put(Object item) throws InterruptedException {
        lock.lock();
        try {
            while (isFull()) {
                notFull.await();  // 等待不满
            }
            // 插入元素
            notEmpty.signal();   // 唤醒等待非空的线程
        } finally {
            lock.unlock();
        }
    }
    
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (isEmpty()) {
                notEmpty.await();  // 等待非空
            }
            // 取出元素
            notFull.signal();      // 唤醒等待不满的线程
            return item;
        } finally {
            lock.unlock();
        }
    }
}

优势 :一个 ReentrantLock 可以创建多个 Condition,实现精确唤醒,而 synchronized 的 wait/notify 只能随机唤醒一个或全部。

3.3 底层原理:AQS

ReentrantLock 基于 AQS(AbstractQueuedSynchronizer) 实现。

AQS 维护了一个 volatile int state (同步状态)和一个 FIFO 等待队列

  • state = 0:锁未被占用
  • state > 0:锁被占用,且可重入计数

非公平锁的获取流程

  1. CAS 尝试将 state 从 0 改为 1,成功则获取锁
  2. 如果当前线程已持有锁,state + 1(可重入)
  3. 如果 CAS 失败,进入等待队列排队

四、深度对比

4.1 功能特性对比

功能 synchronized ReentrantLock
可重入
支持中断 ✅ (lockInterruptibly)
超时获取 ✅ (tryLock)
公平锁
多条件变量
尝试非阻塞获取

4.2 性能对比

JDK 6 引入锁升级机制后,synchronized 在低竞争场景下性能大幅提升,甚至优于 ReentrantLock。

  • 低竞争场景:两者性能相当,synchronized 略有优势
  • 高竞争场景:ReentrantLock 的灵活性(如 tryLock)可以避免不必要的阻塞,表现更好

4.3 使用场景选择

优先使用 synchronized

  • 简单的同步需求
  • 方法级别或简单代码块
  • 希望代码简洁、不易出错

选择 ReentrantLock

  • 需要尝试获取锁(tryLock)
  • 需要超时获取锁
  • 需要响应中断
  • 需要公平锁
  • 需要多条件变量(Condition)

五、常见面试追问

Q1:什么是可重入锁?为什么需要可重入?

:可重入锁指同一个线程可以多次获取同一把锁,不会造成死锁。这在递归调用、嵌套同步场景中非常必要。

Q2:公平锁和非公平锁的区别?

  • 公平锁:按请求顺序获取锁,吞吐量较低,避免饥饿
  • 非公平锁:允许插队,吞吐量更高,但可能导致线程饥饿

Q3:synchronized 锁升级的原理?

:JVM 根据锁的竞争程度,将锁从偏向锁 → 轻量级锁 → 重量级锁逐步升级,减少在低竞争场景下操作系统的上下文切换开销。


六、总结

维度 结论
简单场景 用 synchronized,代码简洁、安全
高级功能需求 用 ReentrantLock,灵活可控
性能 JDK 6 后两者差距不大,根据场景选择

💡 面试建议:回答时可以先用表格快速对比,然后结合锁升级和AQS展示深度,最后给出使用建议,体现工程思维。

相关推荐
C++chaofan1 天前
RPC框架负载均衡机制深度解析
java·开发语言·负载均衡·juc·synchronized·
Thomas.Sir2 天前
深入剖析 Redis 经典面试题
redis·分布式·高并发·
我真会写代码2 天前
深度解析ConcurrentHashMap:从底层原理到生产实战,搞定并发安全映射(含面试避坑)
java·并发编程
曼彻斯特的海边3 天前
synchronized优化原理
jvm·juc·synchronized
我真会写代码4 天前
深度解析并发编程锁升级:从偏向锁到重量级锁,底层原理+面试考点全拆解
java·并发编程·
一只大袋鼠8 天前
并发编程(二十三):单例模式(二):静态/非静态方法:单例内存优化关键
java·单例模式·并发编程
一只大袋鼠8 天前
并发编程(二十四):单例模式(三):构造方法私有:单例模式的 “第一道防线”
java·单例模式·并发编程
一只大袋鼠9 天前
并发编程(二十二):单例模式:从基础实现到 Spring Web 实战
java·spring·单例模式·并发编程
予枫的编程笔记10 天前
【面试专栏|Java并发编程】ConcurrentHashMap并发原理详解:JDK7 vs JDK8 核心对比
java·并发编程·hashmap·java面试·集合框架·jdk8·jdk7