synchronized 与 ReentrantLock 全方位对比 — Java 并发锁的那些坑

🧑‍💻 作者:没用逆称
💡 前言:学并发编程的时候,最头疼的就是各种锁。synchronized 用了很久,后来发现 ReentrantLock 功能更强,但两者到底有什么区别?什么时候用哪个?面试问到也说不清楚。整理了一遍,写一篇笔记给自己总结一下。


一、为什么需要锁?先搞清楚问题

先看一个没有任何锁的情况:

public class UnsafeCounter {
    private int count = 0;

    public void increment() {
        count++;  // 这不是原子操作!
    }

    public int getCount() {
        return count;
    }
}

count++ 其实分三步:

  1. 读取 count 的值
  2. 加 1
  3. 写回 count

两个线程同时执行,count 的最终值可能小于预期。这就是竞态条件(Race Condition)

线程1:读取 count=0 → 计算 0+1=1 → [被挂起]
线程2:读取 count=0 → 计算 0+1=1 → 写入 count=1
线程1:[恢复] → 写入 count=1  ← 覆盖了线程2的结果!

结论:多个线程操作共享变量,必须加锁


二、synchronized — Java 内置锁

2.1 三种使用方式

// 方式1:修饰实例方法 → 锁的是当前对象(this)
public synchronized void method1() {
    // 同一时刻只有一个线程能进入这个方法
}

// 方式2:修饰静态方法 → 锁的是 Class 对象(全局锁)
public static synchronized void method2() {
    // 所有实例共享同一把锁
}

// 方式3:修饰代码块 → 锁的是括号里的对象(最灵活)
public void method3() {
    synchronized (this) {       // 锁当前对象
        // 临界区代码
    }
    synchronized (MyClass.class) { // 锁 Class 对象
        // 临界区代码
    }
    Object lock = new Object();
    synchronized (lock) {         // 锁自定义对象
        // 临界区代码
    }
}

2.2 基本特性

特性 说明
原子性 保证临界区代码原子执行
可见性 unlock 前把工作内存写回主内存
有序性 禁止指令重排(同步块内的代码)
可重入 同一线程可以多次获取同一把锁
不可中断 等待锁的线程无法被中断

2.3 可重入性演示

public class ReentrantDemo {
    public synchronized void methodA() {
        System.out.println("进入 methodA");
        methodB();  // 同一线程,再次获取同一把锁 → 可重入
    }

    public synchronized void methodB() {
        System.out.println("进入 methodB");
    }
}

如果锁不可重入,上面的代码会死锁(自己等自己释放锁)。


三、ReentrantLock — 功能更强的显式锁

ReentrantLockjava.util.concurrent.locks 包下的显式锁,需要手动加锁和释放锁

3.1 基本用法

import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();  // 加锁
        try {
            count++;
        } finally {
            lock.unlock();  // 必须在 finally 里释放!否则死锁
        }
    }
}

⚠️ 最重要的一点:unlock() 必须放在 finally 块里,否则如果临界区抛异常,锁永远不释放,整个系统卡死。

3.2 公平锁 vs 非公平锁

ReentrantLock 构造函数可以传一个 fair 参数:

// 非公平锁(默认,性能更好)
ReentrantLock lock = new ReentrantLock();
// 或
ReentrantLock lock = new ReentrantLock(false);

// 公平锁(按等待顺序获取锁,吞吐量低)
ReentrantLock lock = new ReentrantLock(true);
模式 行为 优点 缺点
非公平锁(默认) 新来的线程可以先抢锁 吞吐量高,减少线程切换 可能导致线程饥饿
公平锁 严格按等待队列顺序获取 每个线程都能获得锁 吞吐量低,频繁线程切换

💡 为什么默认用非公平锁? 非公平锁允许"插队",减少了线程挂起和唤醒的开销。在竞争激烈时,吞吐量通常比公平锁高很多。


四、ReentrantLock 的 4 大"杀手锏"功能

这是面试的重点:ReentrantLock 比 synchronized 强在哪里

4.1 可中断等待(lockInterruptibly()

synchronized 等待锁时,线程无法被中断,只能一直等。

ReentrantLock 可以响应中断:

public void method() throws InterruptedException {
    lock.lockInterruptibly();  // 可以被中断的获取锁
    try {
        // 临界区
    } finally {
        lock.unlock();
    }
}

// 其他线程可以调用 thread.interrupt() 中断等待

使用场景:等待锁的时间太长,想超时放弃。

4.2 尝试获取锁(tryLock()

public void method() {
    // 立即返回,获取不到锁不等待
    if (lock.tryLock()) {
        try {
            // 获取到了锁
        } finally {
            lock.unlock();
        }
    } else {
        // 没获取到锁,做其他事情
        System.out.println("获取锁失败,不等待");
    }
}

// 带超时的版本
public void method2() throws InterruptedException {
    if (lock.tryLock(2, TimeUnit.SECONDS)) {
        try {
            // 2秒内获取到了锁
        } finally {
            lock.unlock();
        }
    } else {
        // 2秒后还没获取到,放弃
        System.out.println("等待超时,放弃");
    }
}

synchronized 做不到这一点,它要么一直等,要么不进同步块,没有中间状态。

4.3 公平锁

上面已经讲过了,synchronized非公平锁,且不能改为公平模式。ReentrantLock 可以自由选择。

4.4 Condition — 比 wait()/notify() 更灵活

synchronized 配合 wait()/notify()/notifyAll() 实现线程间通信,但有个限制:只能有一个等待队列

ReentrantLock 配合 Condition,可以创建多个等待队列

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

public class BoundedQueue<T> {
    private final Object[] items;
    private int count = 0;
    private int takeIndex = 0;
    private int putIndex = 0;

    private final ReentrantLock lock = new ReentrantLock();
    // 两个条件队列
    private final Condition notEmpty = lock.newCondition();  // "队列非空" 条件
    private final Condition notFull = lock.newCondition();   // "队列未满" 条件

    public BoundedQueue(int capacity) {
        this.items = new Object[capacity];
    }

    // 入队 — 队列满时等待
    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            // 队列满了,等待"队列未满"条件
            while (count == items.length) {
                notFull.await();  // 释放锁,等待被唤醒
            }
            items[putIndex] = item;
            putIndex = (putIndex + 1) % items.length;
            count++;
            notEmpty.signal();  // 唤醒等待"队列非空"的线程
        } finally {
            lock.unlock();
        }
    }

    // 出队 — 队列空时等待
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();  // 释放锁,等待被唤醒
            }
            @SuppressWarnings("unchecked")
            T item = (T) items[takeIndex];
            items[takeIndex] = null;
            takeIndex = (takeIndex + 1) % items.length;
            count--;
            notFull.signal();  // 唤醒等待"队列未满"的线程
            return item;
        } finally {
            lock.unlock();
        }
    }
}

Condition 的优势:可以精确唤醒特定类型的等待线程,而不是 notifyAll() 唤醒所有人(减少无效竞争)。


五、synchronized vs ReentrantLock 全面对比

维度 synchronized ReentrantLock
锁的获取 不可中断,一直等 可中断(lockInterruptibly())、可超时(tryLock(timeout))、立即返回(tryLock()
公平锁 不支持(仅非公平) 支持(构造函数传入 true
释放锁 自动释放(代码块结束/JVM 保证) 必须手动 unlock()(通常在 finally
Condition 只有一个等待队列 可以创建多个 Condition,精确唤醒
性能 JDK 6 之后大幅优化,与 ReentrantLock 接近 功能更多,低竞争时略逊于 synchronized
使用难度 简单,不容易出错 复杂,忘记 unlock 会死锁
可重入 ✅ 支持 ✅ 支持
底层实现 JVM 层面(monitorenter/monitorexit 指令) JDK 层面(AQS — AbstractQueuedSynchronizer)

六、底层原理简要了解

面试可能会问两者的实现原理,这里简单说一下。

6.1 synchronized 的实现 — 监视器锁(Monitor)

synchronized 的底层是 ObjectMonitor,每个 Java 对象都有一个 Monitor 与之关联。

Java 对象头(Mark Word)
    │
    ├── 无锁状态
    ├── 偏向锁(Biased Locking)
    ├── 轻量级锁(Thin Lock)
    └── 重量级锁(Heavyweight Lock)← 真正的 Monitor

锁升级过程(JDK 6 优化):

无锁
 ↓ (第一次加锁,且只有一个线程)
偏向锁(在对象头记录线程ID,几乎零开销)
 ↓ (有第二个线程竞争)
轻量级锁(CAS 自旋,避免系统调用)
 ↓ (自旋超过一定次数,或的竞争加剧)
重量级锁(OS 级别的互斥锁,开销最大)

💡 这就是为什么 JDK 6 之后 synchronized 性能大幅提升,不再比 ReentrantLock 差多少。

6.2 ReentrantLock 的实现 — AQS

ReentrantLock 内部使用 AQS(AbstractQueuedSynchronizer) 实现。

ReentrantLock
    │
    ├── Sync(抽象内部类,继承 AQS)
    │       │
    │       ├── NonfairSync(非公平锁)
    │       └── FairSync(公平锁)
    │
    └── state(AQS 的同步状态)
            │
            ├── state = 0 → 锁空闲
            └── state = N → 被同一个线程重入了 N 次

AQS 的核心思想

  • 用一个 volatile int state 表示锁状态
  • 获取锁:CAS(state, 0, 1),失败则进入等待队列
  • 释放锁:state--,state=0 时唤醒等待队列中的下一个线程

七、生产环境怎么选?

优先用 synchronized,以下情况才考虑 ReentrantLock:

需要选择锁的实现时
    │
    ├── 需要可中断等待?          → ReentrantLock
    ├── 需要尝试获取锁(不阻塞)? → ReentrantLock
    ├── 需要公平锁?              → ReentrantLock
    ├── 需要多个等待条件?        → ReentrantLock + Condition
    │
    └── 都不需要?
            → **用 synchronized**(简单、不容易出错)

具体建议:

场景 推荐
简单的同步块,竞争不激烈 synchronized
需要超时获取锁 ReentrantLock.tryLock(timeout)
需要精确唤醒特定线程 ReentrantLock + Condition
读多写少(高性能) ReentrantReadWriteLockStampedLock(比前两者更强)

💡 延伸:读多写少的场景,synchronizedReentrantLock 都是排他锁(读写互斥),性能不好。应该用读写锁 ReentrantReadWriteLock,后面有时间单独写一篇。


八、常见面试题整理

Q1:synchronized 和 ReentrantLock 的区别?

答:从以下角度回答:锁获取方式(可中断/tryLock)、公平锁支持、Condition、释放方式、底层实现、性能。参考上面的对比表。

Q2:synchronized 是公平锁吗?

答:不是synchronized 只支持非公平锁。当一个线程释放锁时,任何等待的线程都有机会抢到锁,不按等待顺序。

Q3:synchronized 和 Lock 性能哪个好?

答:JDK 5 及之前 ReentrantLock 性能明显更好。JDK 6 引入偏向锁、轻量级锁后,synchronized 性能大幅提升,两者在低竞争场景下差距不大。高竞争场景下 ReentrantLock 略好,但差距有限。

实际选择时更多看功能需求,而不是性能。

Q4:ReentrantLock 忘记 unlock() 会怎样?

答:死锁。这个线程持有的锁永远不会释放,其他线程永远等待。所以 unlock() 必须放在 finally 块里

// ✅ 正确写法
lock.lock();
try {
    // ...
} finally {
    lock.unlock();
}

Q5:什么是 AQS?

答:AQS(AbstractQueuedSynchronizer)是 JUC 包的核心基础框架,ReentrantLock、CountDownLatch、Semaphore 等都是基于 AQS 实现的。核心是用一个 volatile int state 表示同步状态,配合 CAS 和等待队列实现线程同步。

Q6:偏向锁、轻量级锁、重量级锁分别是什么?

锁状态 适用场景 开销
偏向锁 只有一个线程访问同步块 几乎零开销(只记录线程 ID)
轻量级锁 多线程交替执行,竞争不激烈 CAS 自旋,避免系统调用
重量级锁 多线程同时竞争 OS 互斥锁,涉及用户态/内核态切换,开销最大

锁只能升级,不能降级(偏向 → 轻量 → 重量)。


九、总结

synchronizedReentrantLock 的核心区别总结成一句话:

synchronized 简单够用,ReentrantLock 功能强大但需要手动管理。

选择建议:

能用 synchronized 就用 synchronized
    │
    └── 需要以下高级功能才换 ReentrantLock:
            - 可中断获取锁
            - 超时获取锁(tryLock with timeout)
            - 公平锁
            - 多个等待条件(Condition)

参考资料

  • 《Java 并发编程实战》(Brian Goetz)
  • 《深入理解 Java 虚拟机》(周志明)— 锁优化章节
  • Java 官方文档 — ReentrantLock
  • 美团技术博客相关文章

并发编程这块坑真的很多,我也是一边学一边踩。如果有写错的地方,欢迎评论区指正!如果觉得有帮助,点个赞呗 😊

更多推荐