news 2026/2/17 1:41:12

Java多线程等待唤醒机制:从synchronized到Lock+Condition

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java多线程等待唤醒机制:从synchronized到Lock+Condition

从synchronized到Lock+Condition

  • 前言
  • 1. 先说传统方式:synchronized + wait/notify
  • 2. 重头戏:Lock + Condition(强烈推荐!)
    • 为什么推荐它?
    • 核心组件:
    • 关键规则:
  • 为什么能精确唤醒?
  • 在实际项目中的应用(阻塞队列)
  • 3. 两种方式对比总结
  • 结论:新项目直接用Lock + Condition!老方式只用来理解历史。

前言

今天来聊聊Java多线程里一个超级经典的话题——等待唤醒机制

在多线程编程里,我们经常遇到“线程协作”的场景:比如一个线程生产数据,另一个线程消费数据;或者两个线程需要严格交替执行(像乒乓球一样你一下我一下)。这时候就需要“等待唤醒”:条件不满足时线程自己睡一觉,等条件好了再被叫醒继续干活。

Java提供了两种实现方式:

  • 老派:synchronized + wait()/notify()
  • 新派(推荐):Lock + Condition

今天重点讲Lock + Condition,因为它更灵活、更高效,是现代并发编程的主流(Java并发包里的阻塞队列都是用这个实现的)。我们会用一个简单例子——两个线程交替打印0~9——来一步步讲解。

1. 先说传统方式:synchronized + wait/notify

这是JDK 1.0就有的方式,简单但有局限。

核心规则:

  • 必须在synchronized同步块里调用wait()/notify()
  • wait():当前线程释放锁,进入等待状态(睡大觉)。
  • notify():随机唤醒一个等待线程。
  • notifyAll():唤醒所有等待线程(生产者消费者场景通常用这个,避免“假死”)。

必须用while检查条件(重要!防止伪唤醒)。

简单例子(交替打印):

publicclassSyncWaitNotifyDemo{privatestaticfinal Object lock=newObject();// 共享锁对象privatestaticint num=0;privatestaticboolean isATurn=true;// true: A的回合publicstaticvoidmain(String[]args){newThread(()->{while(num<10){synchronized(lock){while(!isATurn){// 用while防伪唤醒try{lock.wait();// 不是我的回合,释放锁等待}catch(InterruptedException e){e.printStackTrace();}}System.out.println("A打印: "+num++);isATurn=false;lock.notifyAll();// 唤醒所有(安全)}}},"A").start();newThread(()->{while(num<10){synchronized(lock){while(isATurn){try{lock.wait();}catch(InterruptedException e){e.printStackTrace();}}System.out.println("B打印: "+num++);isATurn=true;lock.notifyAll();}}},"B").start();}}

它能工作,但缺点明显:

  • 只有一个等待队列,所有线程混在一起。
    notifyAll()会把所有线程都唤醒(即使不需要),醒来后又发现条件不满足,再wait——浪费性能(叫“惊群效应”)。
    不支持超时、精确唤醒等高级功能。

2. 重头戏:Lock + Condition(强烈推荐!)

从JDK 1.5开始,JUC包(java.util.concurrent)引入了ReentrantLock和Condition,彻底升级了等待唤醒机制。

为什么推荐它?

  • 精确唤醒:一个Lock可以创建多个Condition,每个Condition有独立的等待队列。你可以“只唤醒消费者”或“只唤醒生产者”,不浪费。
  • 功能更强:支持超时等待(awaitNanos)、不可中断等待等。
  • 性能更好:避免惊群,高并发下更快。
  • 灵活:公平锁可选(避免线程饥饿)。

核心组件:

  • ReentrantLock lock = new ReentrantLock();:可重入锁,手动加锁解锁。
  • Condition cond = lock.newCondition();:可以创建多个,每个是一个独立等待队列。
  • cond.await():释放锁,当前线程进入该Condition的等待队列(睡大觉)。
  • cond.signal():只唤醒该队列的一个线程。
  • cond.signalAll():唤醒该队列的所有线程。

关键规则:

  • 必须先lock.lock()获取锁,再操作Condition。
  • unlock()一定要放finally里(防止死锁)。
  • 永远用while检查条件(防伪唤醒)。

完整例子:交替打印0~9(带详细注释)

Javaimport java.util.concurrent.locks.Condition;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;publicclassLockConditionDemo{privatestaticfinal Lock lock=newReentrantLock();// 一把可重入锁,所有线程共享privatestaticfinal Condition condA=lock.newCondition();// A线程专属等待队列(约定)privatestaticfinal Condition condB=lock.newCondition();// B线程专属等待队列(约定)privatestaticint num=0;// 当前数字privatestaticboolean isATurn=true;// true: 轮到A(初始让A先)publicstaticvoidmain(String[]args){// A线程:打印偶数newThread(()->{while(num<10){// 直到打印完9lock.lock();// 1. 先拿锁try{while(!isATurn){// 2. 用while检查:不是我的回合就等condA.await();// 释放锁,当前线程(A)进入condA队列睡觉}// 到这说明轮到我了,且重新拿到了锁System.out.println("A打印: "+num);num++;isATurn=false;// 交给BcondB.signal();// 精确唤醒B(只从condB队列拿一个)}catch(InterruptedException e){e.printStackTrace();}finally{lock.unlock();// 3. 一定释放锁!}}},"A").start();// B线程:打印奇数newThread(()->{while(num<10){lock.lock();try{while(isATurn){// 不是我的回合就等condB.await();// B线程进入condB队列}System.out.println("B打印: "+num);num++;isATurn=true;condA.signal();// 精确唤醒A}catch(InterruptedException e){e.printStackTrace();}finally{lock.unlock();}}},"B").start();}}

运行结果(完美交替):
textA打印: 0
B打印: 1
A打印: 2
B打印: 3

B打印: 9

为什么能精确唤醒?

  • 不是Condition“认人”,而是我们约定:A只在condA等,B只在condB等。
    signal()只去对应队列叫醒人,不会吵醒另一边。

在实际项目中的应用(阻塞队列)

  • Java的ArrayBlockingQueue就是用一个Lock + 两个Condition实现的:

  • notEmpty:所有消费者共享的等待队列(队列空时await)。
    notFull:所有生产者共享的等待队列(队列满时await)。
    生产后notEmpty.signal()(只唤醒一个消费者)。
    消费后notFull.signal()(只唤醒一个生产者)。

多生产者/多消费者时,它们共享同一个Condition队列,signal()只唤醒一个就够了——超级高效!

3. 两种方式对比总结

  • 特性synchronized + wait/notifyLock + Condition等待队列只有一个,所有线程混一起可以多个,独立队列(精确唤醒)唤醒方式notify随机,常用notifyAll(惊群)signal精确,只唤醒需要的功能基础支持超时、中断、公平锁等性能一般高并发下更好使用难度简单(自动加解锁)稍复杂(手动unlock)推荐场景简单同步生产者消费者、阻塞队列、高并发

结论:新项目直接用Lock + Condition!老方式只用来理解历史。

  • 希望这篇文章让你对等待唤醒机制不再迷糊~如果你有疑问,或者想看生产者消费者的完整代码,欢迎留言讨论!
    点赞 + 收藏 + 关注,三连支持一下呗~
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/5 3:00:58

272. Java Stream API - 使用数字专用流,避免装箱开销

文章目录272. Java Stream API - 使用数字专用流&#xff0c;避免装箱开销&#x1f6ab; 问题&#xff1a;普通 Stream 会引发装箱性能问题✅ 解决方案&#xff1a;使用数字专用流&#x1f4ca; IntStream 示例&#xff1a;终端操作更丰富&#x1f9ee; summaryStatistics() 示…

作者头像 李华
网站建设 2026/2/12 18:26:46

紧急规避生产事故:多模态Agent未隔离网络的3个致命风险(必读)

第一章&#xff1a;多模态 Agent 的 Docker 网络隔离概述在构建多模态 Agent 系统时&#xff0c;Docker 容器化技术为不同模态&#xff08;如文本、图像、语音&#xff09;的处理模块提供了轻量级、可移植的运行环境。然而&#xff0c;多个 Agent 模块之间既需要独立运行以保障…

作者头像 李华
网站建设 2026/2/13 19:45:50

Docker容器间Agent服务互相影响?资深运维总结的5级隔离模型曝光

第一章&#xff1a;Docker容器间Agent服务互相影响&#xff1f;资深运维总结的5级隔离模型曝光在微服务架构日益复杂的今天&#xff0c;多个Docker容器中运行的Agent服务&#xff08;如监控、日志采集、安全探针等&#xff09;常因资源争抢或网络冲突导致异常行为。资深运维团队…

作者头像 李华
网站建设 2026/2/16 18:18:47

揭秘Docker Buildx构建日志:5个你必须关注的关键调试信息

第一章&#xff1a;Docker Buildx构建日志的核心价值Docker Buildx 是 Docker 官方提供的 CLI 插件&#xff0c;扩展了原生 docker build 命令的能力&#xff0c;支持跨平台构建、并行输出和高级镜像构建功能。在多架构支持日益重要的今天&#xff0c;构建日志不再仅仅是输出信…

作者头像 李华
网站建设 2026/2/15 1:07:36

为什么顶级AI团队都在用Docker网络隔离保护多模态Agent?真相揭晓

第一章&#xff1a;多模态 Agent 的 Docker 网络隔离在构建多模态 Agent 系统时&#xff0c;Docker 容器化技术为不同功能模块&#xff08;如语音识别、图像处理、自然语言理解&#xff09;提供了轻量级部署方案。然而&#xff0c;多个 Agent 间若共用默认网络环境&#xff0c;…

作者头像 李华
网站建设 2026/2/16 15:18:13

【C语言入门】彻底搞懂一维数组

在编程的世界里&#xff0c;我们经常需要处理大量同类型的数据。比如统计全班50个同学的成绩&#xff0c;或者存储100个随机生成的数字。如果没有数组&#xff0c;你可能需要定义50个变量&#xff1a; score1, score2, ..., score50 。这不仅写起来累死人&#xff0c;计算平均分…

作者头像 李华