news 2026/4/15 10:53:22

【synchronized 与 ReentrantLock + Condition 的深度对比 Plus版】

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【synchronized 与 ReentrantLock + Condition 的深度对比 Plus版】

Java并发编程:synchronized 与 ReentrantLock + Condition 的深度对比——从Monitor队列到惊群效应与精确唤醒

  • 前言
  • 正文
    • 一、每个Java对象天生都能当锁?Monitor的底层结构
      • 形象比喻:锁对象就像一个会议室。
      • 关键区别:
    • 二、synchronized的线程状态流转全过程
      • 完整过程演绎
        • 1. 抢锁阶段:
        • 2. 条件不满足,主动等待:
        • 3. 被通知唤醒:
      • 为什么被唤醒后还要再抢锁?
      • 惊群效应的根源:
    • 三、ReentrantLock + Condition的高明之处:按等待性质分类的多个队列
    • 四、生产者-消费者完整代码对比
    • 五、为什么JDK的高并发容器都用Condition?
  • 总结:从synchronized到Condition的进化

前言

最近和很多读者深入交流了Java多线程中两个核心的等待/通知机制:synchronized + wait/notify 和 ReentrantLock + Condition。很多同学在学习时都会疑惑:为什么Condition更高级?为什么高并发容器都用Condition?为什么synchronized容易出现“惊群效应”?

今天我们把这些问题全部串起来,用最通俗易懂的语言,从底层实现讲起,一步步把Monitor的两个队列、线程状态流转、惊群效应、Condition的精确唤醒全部讲清楚。读完这篇,你会对Java并发等待/通知机制有彻底的理解!

正文

一、每个Java对象天生都能当锁?Monitor的底层结构

先来一个基础但超级重要的知识点:Java中每个对象都可以作为锁对象,因为它们都继承了Object类的wait()、notify()、notifyAll()方法。
当一个对象第一次被用作锁(比如进入synchronized(obj)块)时,JVM会给它分配一个叫Monitor(对象监视器)的结构。Monitor就像这个对象的“管家”,里面主要有三样东西:

  • Owner:当前持有锁的线程(锁的“主人”)。只有Owner才能执行临界区代码。
  • Entry List(入口等待队列):还没抢到锁的线程被park(阻塞)在这里,等着锁释放后一起竞争。
  • Wait Set(等待队列):已经抢到锁,但业务条件不满足,主动调用wait()的线程,会被放到这里深度睡眠。

形象比喻:锁对象就像一个会议室。

  • Owner:正在会议室里开会的人。
  • Entry List:门口排队的想进来开会的人(抢不到门票)。
  • Wait Set:已经进过会议室,但发现“现在没内容可讨论”,主动出去休息室睡觉的人。

关键区别:

  1. Entry List里的线程:是因为抢不到锁而阻塞。
  2. Wait Set里的线程:是抢到了锁,但条件不满足,主动释放锁去睡觉。

二、synchronized的线程状态流转全过程

我们用生产者-消费者场景来一步步看线程是怎么流动的。

完整过程演绎

1. 抢锁阶段:

线程A先进入synchronized(obj),成功抢到锁 → 成为Owner,进入临界区。

其他线程(B、C、D)也想进来,发现锁被占了 → 被park到Entry List,在门口排队。

2. 条件不满足,主动等待:

线程A在临界区发现“缓冲区满了,没地方放东西”。

调用obj.wait():

立即释放锁(Owner变为null,会议室开门)。

自己走进Wait Set(休息室)睡觉,完全不参与抢锁。

锁释放后,Entry List的线程(B、C、D)被唤醒,开始竞争锁。

3. 被通知唤醒:

假设线程B抢到锁,消费后调用obj.notify()或notifyAll()。

notify():从Wait Set随机选一个线程唤醒。

notifyAll():把Wait Set里所有线程都唤醒。

超级重要!被唤醒的线程不会立刻拿到锁!

它们会从Wait Set转移到Entry List。

然后和Entry List里其他线程一起重新竞争锁。

只有抢到锁后,才能继续执行wait()后面的代码。

为什么被唤醒后还要再抢锁?

  • wait()必须在持有锁的情况下调用,从wait()恢复也必须重新持有锁,确保条件检查和修改是原子的。
    这也是为什么要用while检查条件(防止虚假唤醒),而不是if。

惊群效应的根源:

  1. synchronized只有一个Wait Set,所有等待原因的线程(生产者因为“满”、消费者因为“空”)都挤在这里。
  2. 一次notifyAll() → 把所有人转移到Entry List → 所有人抢锁 → 大部分醒来发现条件不满足,又wait()回去 → 大范围惊群,大量无谓的上下文切换,性能很差。

三、ReentrantLock + Condition的高明之处:按等待性质分类的多个队列

  1. Java为了解决synchronized的痛点,引入了java.util.concurrent.locks包。ReentrantLock基于AQS(AbstractQueuedSynchronizer),底层更灵活。

  2. AQS有一个同步队列(类似Entry List,用来排队等锁)。
    lock.newCondition()可以创建多个Condition对象,每个Condition都有自己独立的等待队列(类似Wait Set,但可以有多个)。

JavaLock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 生产者专用:缓冲区满时等待
Condition notEmpty = lock.newCondition(); // 消费者专用:缓冲区空时等待

关键点:不是每个线程一个队列,而是按“等待性质”分类

一个Condition队列里可以有多个线程,但这些线程的等待原因是相同的(同一种性质)。
比如:notFull队列里可能有50个生产者在等(都因为缓冲区满)。
notEmpty队列里可能有30个消费者在等(都因为缓冲区空)。

await/signal的工作原理:

await():释放锁 → 进入这个Condition的专属队列睡觉。

signal():从这个队列头部取一个线程 → 转移到AQS同步队列 → 去抢锁(和synchronized唤醒后去Entry List一样)。

signal()默认只唤醒一个(先进先出),signalAll()唤醒整个队列(但只限同类线程)。

惊群范围大幅缩小:

  1. 消费者消费后,只调用notEmpty.signal() → 只唤醒notEmpty队列里的消费者。
    生产者的50个线程完全不受影响,继续睡觉。
  2. 即使signalAll(),也只影响同类线程,远比synchronized的“唤醒所有人”好太多。
    加上signal()默认只醒一个,在大多数场景几乎杜绝了惊群。

“遥控器”比喻:Condition对象就像一个遥控器,你用哪个遥控器,就操作哪个队列,完全隔离。

四、生产者-消费者完整代码对比

synchronized版本(容易大范围惊群)

JavapublicclassBuffer{privatefinal Object lock=newObject();privatefinal Queue<Integer>queue=newLinkedList<>();privatefinal int capacity=10;publicvoidproduce(int item)throws InterruptedException{synchronized(lock){while(queue.size()==capacity){lock.wait();// 所有生产者进入同一个Wait Set}queue.offer(item);lock.notifyAll();// 唤醒所有人:生产者+消费者 → 大惊群}}publicintconsume()throws InterruptedException{synchronized(lock){while(queue.isEmpty()){lock.wait();// 所有消费者也进同一个Wait Set}int item=queue.poll();lock.notifyAll();// 同上returnitem;}}}

ReentrantLock + Condition版本(按性质分类,精确唤醒)

JavapublicclassBuffer{privatefinal Lock lock=newReentrantLock();privatefinal Condition notFull=lock.newCondition();// 生产者队列privatefinal Condition notEmpty=lock.newCondition();// 消费者队列privatefinal Queue<Integer>queue=newLinkedList<>();privatefinal int capacity=10;publicvoidproduce(int item)throws InterruptedException{lock.lock();try{while(queue.size()==capacity){notFull.await();// 只进生产者专属队列}queue.offer(item);notEmpty.signal();// 只唤醒一个消费者(推荐)}finally{lock.unlock();}}publicintconsume()throws InterruptedException{lock.lock();try{while(queue.isEmpty()){notEmpty.await();// 只进消费者专属队列}int item=queue.poll();notFull.signal();// 只唤醒一个生产者}finally{lock.unlock();}}}

新版本代码更清晰、性能更高。

五、为什么JDK的高并发容器都用Condition?

打开JDK源码:

ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentHashMap(部分)等底层几乎全是用ReentrantLock + Condition。

原因:

多个独立队列 + 精确唤醒
支持超时等待(awaitNanos)、可中断、公平锁等高级特性
在高并发下性能远超synchronized

总结:从synchronized到Condition的进化

  1. synchronized + wait/notify:简单易用,但只有一个Wait Set,所有线程混在一起。wait()释放锁进Wait Set,notify后转移到Entry List再抢锁,notifyAll()容易造成大范围惊群。
  2. ReentrantLock + Condition:按等待性质分类多个队列,一个队列里多个同类线程。await/signal操作独立,signal()默认只醒一个,惊群范围大幅缩小,几乎杜绝。

推荐:简单场景用synchronized,复杂等待条件(如生产者-消费者、读写锁)强烈建议用Lock + Condition。

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c5721d2340f843d7bddc55fcf6369142.jpeg#pic_center

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/15 1:41:03

基于单片机的智能电子秤设计

一、设计背景与核心需求 传统电子秤功能单一&#xff0c;多仅能实现重量测量&#xff0c;难以满足现代生活中对食材营养分析、物品分类统计、数据追溯等多样化需求。基于单片机的智能电子秤&#xff0c;融合高精度称重、数据处理与智能交互功能&#xff0c;可广泛应用于家庭厨房…

作者头像 李华
网站建设 2026/4/15 9:17:08

Comsol实现激光熔覆的凝固相场树枝晶生长探索

comsol实现激光熔覆的凝固相场树枝晶生长 考虑溶质、 相场 温度场耦合 提供资料 全套的模型文件和参考文献以及讲解视频 利用凝固组织的建模和验证可以减少获得所需组织的迭代成本。 结合Marangoni对流的基于流体体积的数值方法可以准确地预测熔池的几何形状和温度分布&#xf…

作者头像 李华
网站建设 2026/4/15 9:17:04

探索元启发式算法:从建模到改进与实现

元启发式算法建模设计&#xff0c;智能优化算法编程实现&#xff0c;改进算法设计&#xff0c;基于matlab建模在当今的技术领域&#xff0c;元启发式算法与智能优化算法备受瞩目&#xff0c;它们如同神奇的钥匙&#xff0c;打开解决复杂问题的大门。今天咱就深入探讨一下元启发…

作者头像 李华
网站建设 2026/4/15 10:07:37

收藏备用!6种AI Agent核心模式详解,大模型入门必看

对于刚接触大模型开发的程序员和AI小白来说&#xff0c;"AI Agent&#xff08;智能体&#xff09;"无疑是绕不开的核心概念。随着大语言模型&#xff08;LLM&#xff09;驱动的系统越来越复杂&#xff0c;单一智能体早已无法满足实际开发需求&#xff0c;多智能体协作…

作者头像 李华