从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用

一、ReentrantLock 是什么?

ReentrantLock 是 Java.util.concurrent.locks 包下的可重入独占锁,从字面意思拆解:

  • Reentrant(可重入) :同一个线程可以多次获取同一把锁,不会因为自己持有锁而阻塞(比如递归调用中加锁不会死锁)。
  • Lock(锁) :它是对 synchronized 关键字的补充和增强,通过编程式的方式实现锁的获取与释放。

1. 核心特性

  • 可重入:和 synchronized 一样,支持同一线程重复加锁,锁会记录 "加锁次数",释放次数需和加锁次数一致才会真正释放。

  • 显式锁 :必须手动调用 lock() 获取锁,手动调用 unlock() 释放锁(通常放在 finally 块中,避免异常导致锁无法释放)。

  • 公平 / 非公平锁

    • 非公平锁(默认):线程获取锁时不按等待顺序,可能 "插队",性能更高(和 synchronized 一致)。
    • 公平锁:线程按等待队列的顺序获取锁,避免 "饥饿",但性能略低(通过构造函数 new ReentrantLock(true) 开启)。
  • 可中断 :支持通过 lockInterruptibly() 响应线程中断,避免线程无限等待锁。

  • 超时获取 :支持 tryLock(long time, TimeUnit unit),在指定时间内获取不到锁则返回 false,不会永久阻塞。

2. 基础使用示例

csharp 复制代码
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    // 创建非公平锁(默认),公平锁:new ReentrantLock(true)
    private final ReentrantLock lock = new ReentrantLock();

    public void doSomething() {
        // 1. 获取锁
        lock.lock();
        try {
            // 业务逻辑:临界区代码
            System.out.println(Thread.currentThread().getName() + " 执行临界区代码");
            // 可重入:同一线程再次获取锁
            doInnerThing();
        } finally {
            // 2. 释放锁(必须放finally,避免异常导致锁泄漏)
            lock.unlock();
        }
    }

    private void doInnerThing() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 执行内部临界区代码");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        new Thread(demo::doSomething, "线程1").start();
        new Thread(demo::doSomething, "线程2").start();
    }
}

执行结果(非公平)

复制代码
线程1 执行临界区代码
线程1 执行内部临界区代码
线程2 执行临界区代码
线程2 执行内部临界区代码

二、ReentrantLock 与 synchronized 的核心区别

我整理了一张核心维度的对比表:

对比维度 ReentrantLock synchronized
锁的类型 显式锁(JDK层面)(手动获取 / 释放) 隐式锁(JVM 自动获取 / 释放)
可重入性 支持(可重入锁) 支持(可重入锁)
公平 / 非公平锁 支持(默认非公平,可手动指定公平) 仅非公平锁
中断性 支持(lockInterruptibly ()) 不支持(等待锁的线程无法被中断)
超时获取锁 支持(tryLock (long time, TimeUnit)) 不支持(要么获取锁,要么一直阻塞)
条件变量(Condition) 支持多个 Condition,可精准唤醒线程 仅支持一个,只能随机 / 全部唤醒线程
锁状态查询 支持(isLocked ()、getHoldCount () 等) 不支持(无法主动查询锁状态)
性能 高并发下性能更优(JDK1.6 后差距缩小) 低并发下更简洁,JVM 优化充分
异常处理 必须手动释放(finally 块),否则锁泄漏 异常时自动释放锁,无需手动处理

关键区别详解

1. 显式 vs 隐式
  • synchronized:写在方法 / 代码块上,JVM 在进入代码块时自动加锁,退出(正常 / 异常)时自动释放锁,无需手动操作。
  • ReentrantLock:必须手动调用 lock() 加锁,且必须在 finally 中调用 unlock() 释放(否则线程异常会导致锁永远无法释放,即 "锁泄漏")。
2. 公平锁支持
  • synchronized:永远是非公平的,线程获取锁时不按等待顺序,可能后到的线程先拿到锁。
  • ReentrantLock:默认非公平,但可以通过构造函数 new ReentrantLock(true) 开启公平锁,保证等待最久的线程先获取锁(适合对顺序要求高的场景)。
3. 中断与超时获取

这是 ReentrantLock 最核心的优势之一:

csharp 复制代码
// 示例:超时获取锁
public void tryLockWithTimeout() {
    try {
        // 尝试在1秒内获取锁,获取到返回true,否则返回false
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                System.out.println(Thread.currentThread().getName() + " 获取到锁");
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " 1秒内未获取到锁,放弃等待");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // 恢复中断状态
        System.out.println(Thread.currentThread().getName() + " 获取锁时被中断");
    }
}

synchronized 无法实现这种 "超时放弃" 的逻辑,只能一直阻塞。

4. 条件变量(Condition)

ReentrantLock 可以通过 newCondition() 创建多个 Condition,实现精准唤醒线程:

java 复制代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {
    private final ReentrantLock lock = new ReentrantLock();
    // 创建两个条件变量:等待读、等待写
    private final Condition readCondition = lock.newCondition();
    private final Condition writeCondition = lock.newCondition();

    public void waitForRead() throws InterruptedException {
        lock.lock();
        try {
            readCondition.await(); // 读线程等待
            System.out.println("读线程被唤醒");
        } finally {
            lock.unlock();
        }
    }

    public void signalRead() {
        lock.lock();
        try {
            readCondition.signal(); // 精准唤醒读线程
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConditionDemo demo = new ConditionDemo();
        new Thread(demo::waitForRead, "读线程1").start();
        Thread.sleep(1000);
        demo.signalRead(); // 唤醒读线程
    }
}

而 synchronized 只能通过 wait()/notify()/notifyAll() 唤醒,notify() 是随机唤醒一个等待线程,notifyAll() 是唤醒所有,无法精准唤醒。

三、使用场景选择

  • 优先用 synchronized:低并发场景、简单的临界区控制,代码更简洁,JVM 优化(偏向锁、轻量级锁)更充分,不易出错。

  • 选择 ReentrantLock

    1. 需要公平锁、可中断锁、超时获取锁的场景;
    2. 需要精准唤醒线程(多 Condition);
    3. 高并发场景下需要更灵活的锁控制(如手动查询锁状态)。

总结

  1. ReentrantLock 是可重入的显式锁,支持公平 / 非公平、中断、超时获取、多 Condition 等高级特性,需手动加锁 / 释放(务必放 finally);
  2. synchronized 是隐式锁,JVM 自动管理,简洁易用,低并发下更友好,但功能单一;
  3. 核心区别在于灵活性和可控性:ReentrantLock 可控性更强,synchronized 更简洁、容错率更高,实际开发中需根据场景选择。

相关推荐
皮皮林55120 分钟前
开源实力派,给本地 AI 装上深度调研能力,一张 3090 跑到 95.7 分!
后端
努力的小雨1 小时前
把 Windows 桌面图标做成一个会自己运转的小世界
后端
触底反弹1 小时前
深入理解大模型采样:Temperature、Top-K、Top-P 的原理与实战
人工智能·算法·面试
武子康1 小时前
Search Console Platform Properties 扩大 SEO 资产边界:从 Page Ranking 到 Topic Coverage(5 类误读边界 + 3 表数据层设计)
前端·人工智能·后端
腾渊信息科技公司2 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
沉默王二3 小时前
腾讯一面,我霸气反问:“你说你们在做Agent项目,说说 SubAgent、Plan 模式、Skill 调用这些你们都是怎么做的?”面试官一直在擦汗。。
面试·agent·ai编程
HLC++3 小时前
Linux的进程间通信
android·linux·服务器
IT_陈寒3 小时前
Vue这个特性差点让我加班到凌晨,谁懂啊
前端·人工智能·后端
段一凡-华北理工大学3 小时前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
码事漫谈4 小时前
1999年的电商前夜和2026年的AI前夜 历史押韵但从不重复
后端