ReentrantLock:AQS家的“锁二代”,但比 synchronized 更会“来事儿”

ReentrantLock:AQS家的"锁二代",但比 synchronized 更会"来事儿"🔓

一个曾在synchronizedReentrantLock之间反复横跳,最终"全都要"的Java端水大师。🍵

朋友们,上回书说到,AQS是那位深藏功与名的"包租公",管着所有锁的排队和调度。那么今天这位主角------ReentrantLock ------就是包租公最出息的"儿子",江湖人称 "锁二代"

但你可别小看这"二代",他虽然姓"Reentrant"(可重入),但本事比他那老古董叔叔 **synchronized**​ 要大得多,也灵活得多。如果说synchronized是Java语言内置的"自动挡老头乐",那ReentrantLock就是可以手动切换赛道、带涡轮增压的"性能小钢炮"。🏎️

一、ReentrantLock 是啥?一个"手工高端锁"!

简单说,ReentrantLock基于AQS实现的一个可重入的互斥锁 。它完全用Java写成,提供了与synchronized相同的基本行为和内存语义,但多了些"高级功能"。

"可重入"是啥?

就是说,同一个线程可以多次进入同一把锁 。不会把自己锁死在外面。比如你在一个synchronized方法里调用另一个synchronized方法,如果是同一把锁,线程可以进入。ReentrantLock也一样,它内部有个计数器,记录锁被同一线程获取了多少次,必须释放同样次数,锁才真正释放。

csharp 复制代码
ReentrantLock lock = new ReentrantLock();

void outer() {
    lock.lock();
    try {
        inner(); // 同一个线程可以再次获取锁!
    } finally {
        lock.unlock();
    }
}
void inner() {
    lock.lock(); // 这里不会死锁!
    try {
        // do something
    } finally {
        lock.unlock();
    }
}

二、为啥不用 synchronized ?因为它"不够秀"!

synchronized是JVM原生支持的,简单粗暴,用起来很香。但它在有些场合显得"力不从心":

  1. 它不能"中途放弃" :线程在等synchronized锁时,会一直傻等,不能被中断。ReentrantLock提供了lockInterruptibly(),等锁时可以被别的线程中断,优雅退出。
  2. 它没有"超时机制" :等锁等多久?等到地老天荒。ReentrantLocktryLock(long time, TimeUnit unit),等一段时间还拿不到,老子不玩了!去做点别的。
  3. 它只能是"非公平"的synchronized的锁策略是非公平的,可能造成线程"饿死"。ReentrantLock可以选择公平模式new ReentrantLock(true)),让等待时间最长的线程优先获取锁,讲究一个先来后到。
  4. 它不能"条件等待" :一个synchronized锁只有一个等待队列(wait/notify)。ReentrantLock可以关联多个Condition对象 ,实现更精细的线程等待/唤醒。比如经典的"生产者-消费者"模型,可以用两个Condition,一个给队列满时等,一个给队列空时等,精准唤醒,不"惊群"。

三、解决了什么?把"锁"的控制权交给你!

ReentrantLock的核心思想是:将锁的获取和释放操作,从JVM的隐式管理,变成程序员显式控制。

  • synchronized :锁的获取和释放由JVM在代码块进入和退出时自动管理。你只管用,丢了不负责。
  • ReentrantLock :锁的获取(lock())和释放(unlock())必须手动配对写在代码里 。通常unlock()要放在finally块里,确保无论如何都会释放锁。

这带来了无与伦比的灵活性,但也带来了责任。 ​ 就像给你一辆手动挡跑车,你可以玩漂移,但也可能熄火。

四、工作中的注意事项:别开翻车了!🚨

  1. "忘写 unlock() 是大忌!"

    这是ReentrantLock最经典的坑。一旦忘记unlock(),锁就永远不释放,其他线程全卡住,系统"静默式死亡"。务必、务必、务必将unlock()放在finally块中!

    csharp 复制代码
    ReentrantLock lock = new ReentrantLock();
    lock.lock(); // 危险!如果这里抛异常,锁永远不释放!
    try {
        // 临界区代码
    } finally {
        lock.unlock(); // 必须放这里!
    }
  2. "tryLock 不是 lock!"

    tryLock()是"尝试获取锁",它不会阻塞! ​ 获取成功返回true,失败返回false。千万别把它当成lock()的替代品。它通常用于避免死锁,或者尝试获取锁失败时执行备用逻辑。

    csharp 复制代码
    if (lock.tryLock()) { // 尝试一下,拿不到就算了
        try {
            // 拿到锁了,干活
        } finally {
            lock.unlock();
        }
    } else {
        // 没拿到锁,我去做点别的不行吗?
        doSomethingElse();
    }
  3. "公平锁性能有代价!"

    公平锁(new ReentrantLock(true))保证了绝对的先来后到,但引入了额外的线程切换和调度开销,吞吐量通常低于非公平锁 。除非你的业务对公平性有严格要求(比如防止线程饿死导致严重不公),否则默认用非公平锁就好

  4. "Condition 用对了是神器,用错了是迷宫"

    Conditionawait()signal()必须放在对应的lock()unlock()之间。并且,signal()一次只唤醒一个等待在该Condition上的线程,比Object.notifyAll()的"全叫醒"高效得多。但要小心,如果用多个Condition,唤醒错了队列,程序就可能永远等下去。

五、怎么合适恰当地用?选"老头乐"还是"小钢炮"?

口诀:默认用 synchronized,不够用了再上 ReentrantLock。

synchronized当你的需求是:

  • 锁的获取和释放非常规律(基于代码块)。
  • 不需要中断、超时、公平锁、多个等待条件这些高级功能。
  • 代码简洁性至上,不想处理try...finally

ReentrantLock当你的需求是:

  1. 需要可中断的锁获取:不想让线程无限期等待。
  2. 需要尝试性获取锁tryLock):拿不到锁时,有备用方案。
  3. 需要公平锁:业务上要求严格的先来后到。
  4. 需要多个等待条件Condition):实现复杂的线程协作模型(如生产-消费者、连接池)。
  5. 需要进行锁的细粒度调试ReentrantLock提供了一些监控方法,如getQueuedThreads(),方便排查问题。

一个经典场景:高竞争下的"锁升级"策略

csharp 复制代码
ReentrantLock lock = new ReentrantLock();
public void accessResource() {
    if (!lock.tryLock(50, TimeUnit.MILLISECONDS)) { // 先快速尝试
        // 快速路径失败,可能锁竞争激烈,走慢速路径
        lock.lock(); // 阻塞等待
    }
    try {
        // 访问共享资源
    } finally {
        lock.unlock();
    }
}

结语

synchronizedReentrantLock不是"谁取代谁"的关系,而是"互补"的武器。JVM一直在优化synchronized,在大多数无竞争或低竞争场景下,它的性能已经不输甚至优于ReentrantLock,而且写法简单安全。

ReentrantLock看作你并发工具箱里的一把"瑞士军刀" ------平时用自带的小刀(synchronized)就够了,但当你需要开瓶器、剪刀、镊子(可中断、超时、公平、多条件)时,它会是你最得力的助手。

记住,能力越大,责任越大 。享受ReentrantLock带来的灵活性时,也请务必担起手动管理锁生命周期的责任。不然,你得到的将不是高性能,而是一串死锁的"惊喜"。😉

(现在,你可以自信地根据场景,在synchronized的"简单"和ReentrantLock的"灵活"之间做出选择了。)

相关推荐
巫山老妖1 小时前
OpenClaw 心跳机制实战:让 AI Agent 24 小时不停自主运行
java·前端
没有bug.的程序员1 小时前
低代码平台后端引擎:元数据驱动架构、插件化内核与 Java 扩展机制
java·低代码·架构·插件化·元数据·扩展机制
武子康1 小时前
大数据-246 离线数仓 - 电商分析 Hive 拉链表实战:初始化、每日增量更新、回滚脚本与错误排查
大数据·后端·apache hive
懈尘1 小时前
【实战分享】智慧养老系统核心模块设计 —— 健康监测与自动紧急呼叫
java·后端·websocket·mysql·springboot·livekit
亚马逊云开发者1 小时前
写 Prompt 让 AI 出代码?Kiro 说你该先写 Spec
java
筱顾大牛1 小时前
点评项目---分布式锁
java·redis·分布式·缓存·idea
想不明白的过度思考者1 小时前
【MyBatis 知识点解析】#{} 与 ${} 的区别及 SQL 注入实战演示
java·数据库·spring boot·sql·mybatis
丶小鱼丶1 小时前
数据结构和算法之【数组】
java·数据结构·算法
惊讶的猫2 小时前
maven介绍_1
java·maven