news 2026/4/16 14:33:48

从synchronized到Condition:FooBar交替打印的进阶之路(1115.交替打印)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从synchronized到Condition:FooBar交替打印的进阶之路(1115.交替打印)

目录

从synchronized到Condition:FooBar交替打印的进阶之路

一、基础解法:能用但不够好的synchronized版本

1.1 基础版代码实现

1.2 基础版的核心痛点

二、进阶解法:ReentrantLock + Condition精准控制

2.1 进阶版代码实现(工业级标准解法)

2.2 核心知识点拆解(从陌生到熟悉)

维度1:ReentrantLock——更灵活的显式锁

维度2:Condition——精准唤醒的“条件队列”

维度3:while循环——延续的“虚假唤醒”防御

三、执行流程推演(理解交替的本质)

四、学习感悟:多线程的“进阶思维”

五、扩展:这些知识点能解决哪些问题?

六、总结


从synchronized到Condition:FooBar交替打印的进阶之路

作为多线程编程的初学者,我最近在啃LeetCode 1115「交替打印FooBar」这个经典问题。一开始用自己熟悉的synchronized + wait + while实现了基础版本,但总觉得执行不够高效。直到接触了ReentrantLock + Condition的组合,才发现多线程同步原来能如此精准灵活。这篇文章就记录下我的学习过程,从基础解法的痛点出发,带你一步步理解这个进阶方案的核心逻辑。

在作答1115题的时候,用最基础的synchronized+while+wait+notifyAll能正确回答问题,但是学习编程的都知道,正确不代表性能,用基础的知识做出了的速度实在太慢,所以就去回忆了之前学的ReentrantLock。

一、基础解法:能用但不够好的synchronized版本

在学习ReentrantLock之前,我对多线程同步的认知停留在synchronized关键字上。针对FooBar问题,基础思路很明确:用一个布尔变量做轮次标记,配合wait()notifyAll()实现线程通信,再用while循环防止虚假唤醒。

1.1 基础版代码实现

​ class FooBar { private int n; private boolean isFooTurn = true; // true为foo轮次,false为bar轮次 private final Object lock = new Object(); ​ public FooBar(int n) { this.n = n; } ​ public void foo(Runnable printFoo) throws InterruptedException { for (int i = 0; i < n; i++) { synchronized (lock) { // 不是foo轮次就等待 while (!isFooTurn) { lock.wait(); } printFoo.run(); isFooTurn = false; // 切换轮次 lock.notifyAll(); // 唤醒所有等待线程 } } } ​ public void bar(Runnable printBar) throws InterruptedException { for (int i = 0; i < n; i++) { synchronized (lock) { // 不是bar轮次就等待 while (isFooTurn) { lock.wait(); } printBar.run(); isFooTurn = true; // 切换轮次 lock.notifyAll(); // 唤醒所有等待线程 } } } }

1.2 基础版的核心痛点

这个版本能满足“交替打印”的基本需求,但在实际运行中会发现明显瓶颈——notifyAll()方法是“无差别唤醒”。比如foo执行完后,只需要唤醒等待的bar线程,但notifyAll()会把所有等待lock的线程都唤醒,包括可能存在的其他线程(虽然这个问题里只有两个线程)。

被唤醒的线程会重新竞争锁,没抢到的线程只能再次阻塞,这就产生了“无意义的锁竞争开销”。当n很大(比如100万次交替)时,这种开销会被无限放大,执行效率大幅下降。

二、进阶解法:ReentrantLock + Condition精准控制

为了解决“无差别唤醒”的问题,我接触到了JUC(java.util.concurrent)包中的ReentrantLockCondition。这对组合的核心优势是“精准唤醒”——可以只唤醒需要执行的目标线程,彻底消除冗余的锁竞争。

2.1 进阶版代码实现(工业级标准解法)

​ import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; ​ class FooBar { private int n; // 轮次标记:false→foo轮次,true→bar轮次 private boolean flag = false; // 可重入锁(替代synchronized) private final ReentrantLock lock = new ReentrantLock(); // 专属条件队列:分别管理等待的foo和bar线程 private final Condition fooCond = lock.newCondition(); private final Condition barCond = lock.newCondition(); ​ public FooBar(int n) { this.n = n; } ​ public void foo(Runnable printFoo) throws InterruptedException { for (int i = 0; i < n; i++) { lock.lock(); // 加锁(显式操作) try { // 不是foo轮次→进入foo专属队列等待 while (flag == true) { fooCond.await(); // 释放锁,仅foo线程等待 } printFoo.run(); // 执行核心逻辑 flag = true; // 切换为bar轮次 barCond.signal(); // 精准唤醒bar线程 } finally { lock.unlock(); // 必须在finally释放锁,防止死锁 } } } ​ public void bar(Runnable printBar) throws InterruptedException { for (int i = 0; i < n; i++) { lock.lock(); try { // 不是bar轮次→进入bar专属队列等待 while (flag == false) { barCond.await(); } printBar.run(); flag = false; // 切换为foo轮次 fooCond.signal(); // 精准唤醒foo线程 } finally { lock.unlock(); } } } }

2.2 核心知识点拆解(从陌生到熟悉)

作为初学者,我一开始对ReentrantLockCondition充满陌生感,但把它们和熟悉的synchronized对比后,很快就理解了核心逻辑。下面从三个关键维度拆解:

维度1:ReentrantLock——更灵活的显式锁

ReentrantLock翻译为“可重入锁”,是synchronized的增强版,核心是“显式操作”:

  • 加锁解锁:用lock.lock()加锁、lock.unlock()解锁,替代synchronized的隐式加锁;

  • 必须在finally释放:显式锁不会像synchronized那样自动释放,放在finally中能保证即使发生异常,锁也能正常释放,避免死锁;

  • 可重入特性:同一线程可以多次获取同一把锁,不会自己阻塞自己(比如递归调用加锁方法)。

对我来说,最直观的感受是“控制权变多了”——可以自主决定加锁和解锁的时机,而不是依赖代码块的作用域。

维度2:Condition——精准唤醒的“条件队列”

这是进阶版的核心,也是解决notifyAll()痛点的关键。Condition可以理解为“绑定在锁上的专属等待队列”,每个Condition对应一类需要等待的线程。

  • 创建方式:通过lock.newCondition()创建,一个锁可以绑定多个Condition

  • 核心方法

    • await():替代Object.wait(),让当前线程释放锁并进入该Condition的等待队列;

      • signal():替代Object.notify(),只唤醒该Condition队列中的一个线程;

  • 精准性体现:foo执行完后调用barCond.signal(),只会唤醒等待的bar线程,不会打扰其他线程(即使有)。

维度3:while循环——延续的“虚假唤醒”防御

虽然用了新的API,但“防止虚假唤醒”的核心逻辑没有变,依然需要用while循环检查轮次标记,而不是if

所谓“虚假唤醒”,是指线程可能在没有被signal()唤醒的情况下,突然从await()中返回(JVM底层机制导致)。如果用if判断,虚假唤醒后会直接执行打印逻辑,导致顺序混乱;而while会循环检查轮次,确保只有满足条件时才继续执行。

三、执行流程推演(理解交替的本质)

为了彻底搞懂代码逻辑,我手动推演了n=2时的执行流程,这对理解线程交互非常有帮助:

  1. 初始状态flag=false(foo轮次),foo和bar线程启动后争抢lock

  2. 第一次foo执行

    • foo抢到锁,while(flag==true)不成立,执行printFoo.run()

    • 设置flag=true,调用barCond.signal()唤醒bar线程;

    • finally中释放锁,foo线程退出同步块,准备下一轮循环。

  3. 第一次bar执行

    • 被唤醒的bar抢到锁,while(flag==false)不成立,执行printBar.run()

    • 设置flag=false,调用fooCond.signal()唤醒foo线程;

    • finally中释放锁,bar线程退出同步块。

  4. 第二次循环:重复步骤2-3,直到foo和bar都完成n次执行,最终输出foo bar foo bar

四、学习感悟:多线程的“进阶思维”

synchronizedReentrantLock + Condition,我不仅学会了一个问题的更优解法,更体会到多线程编程的核心思维转变:

  1. 从“能用”到“好用”:基础解法能满足功能,但工业级开发更关注效率和健壮性。Condition的精准唤醒就是从“能用”到“好用”的关键;

  2. 理解“锁”的本质:锁不仅是“互斥”的工具,更是“线程通信”的桥梁。ReentrantLock通过绑定Condition,让线程通信更精准;

  3. API是工具,逻辑是核心:不管是wait()还是await(),核心都是“释放锁等待-被唤醒抢锁”的循环,while防虚假唤醒的逻辑永远适用。

五、扩展:这些知识点能解决哪些问题?

这个解法的核心思路(锁+条件队列+状态标记)不是只针对FooBar问题,而是多线程交替执行的通用方案,能解决很多类似问题:

  • LeetCode 1116「打印零与奇偶数」:用多个Condition分别管理打印0、奇数、偶数的线程;

  • 交替打印ABC:创建3个Condition,A执行完唤醒B,B执行完唤醒C,C执行完唤醒A;

  • 生产者-消费者问题:用两个Condition分别管理生产者和消费者线程,实现供需平衡。

六、总结

作为多线程初学者,FooBar问题的进阶解法让我打开了JUC并发编程的大门。ReentrantLock的显式控制和Condition的精准唤醒,看似复杂,实则是对synchronized机制的优化和延伸。

核心知识点回顾:

1. 显式锁:ReentrantLock的lock()/unlock(),必须在finally释放; 2. 条件队列:Condition的await()/signal(),实现精准线程通信; 3. 健壮性:while循环防御虚假唤醒,保证执行顺序稳定; 4. 核心逻辑:状态标记控制轮次,锁保证原子性和可见性。

如果你也刚学完synchronized,建议从这个问题入手,手动推演执行流程,相信你会和我一样,对多线程同步有更深刻的理解~

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

音效素材网下载总要翻遍页面?这几位选手的体验流畅得像阵风

你是否也有过这样的经历&#xff1a;为了找到一个合适的开门声或环境音效&#xff0c;不得不在素材网站里来回翻页&#xff0c;复杂的导航和隐蔽的下载入口让人精疲力尽&#xff1f;根据《2025年数字创意产业白皮书》的调研&#xff0c;超过63%的创作者将“素材版权风险”和“低…

作者头像 李华
网站建设 2026/4/16 13:33:41

专业做合肥广告门头招牌的厂家

专业做合肥广告门头招牌的厂家在合肥&#xff0c;商业竞争日益激烈&#xff0c;一个独特且高质量的广告门头招牌对于企业来说至关重要。它不仅是企业的门面&#xff0c;更是吸引顾客、提升品牌形象的重要工具。而贰师兄广告&#xff0c;就是合肥地区专注于门头招牌发光字设计、…

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

【dz-945】基于单片机的矿下气体安全检测系统的设计与实现

基于单片机的矿下气体安全检测系统的设计与实现 摘要 在矿下作业环境中&#xff0c;温度异常及甲烷、CO₂等气体浓度超标是引发安全事故的重要隐患&#xff0c;可能导致爆炸、中毒等严重后果&#xff0c;威胁矿工生命安全和矿山生产安全。因此&#xff0c;研发一种能实时、准确…

作者头像 李华
网站建设 2026/4/16 12:03:30

soular全面介绍(8) - 集成企业微信,使用企业微信扫码登录soular

集成企业微信用户功能划入社区版本&#xff0c;本篇文章将全面介绍如何在soular中集成企业微信用户并实现企业微信用户登录soular。1、配置企业微信进入系统设置->用户->用户目录&#xff0c;点击企业微信后的配置按钮&#xff0c;填写企业微信的配置信息。参数说明企业I…

作者头像 李华
网站建设 2026/4/15 14:00:02

【dz-950】基于单片机的音乐播报器设计

基于STM32的音乐播放器的设计与实现 摘要&#xff1a;随着数字音频技术的发展&#xff0c;便携式音乐播放器在人们的日常生活中扮演着重要角色。基于STM32单片机的音乐播放器设计旨在实现多功能、便捷化的音频播放体验。 该播放器以STM32F103C8T6单片机为核心&#xff0c;具备多…

作者头像 李华
网站建设 2026/4/16 11:58:18

TikTok Studio创作者工具打不开怎么办?

TikTok在全球拥有数亿活跃用户&#xff0c;是许多内容创作者实现变现的重要阵地&#xff0c;为了帮助用户更好的使用&#xff0c;TikTok官方推出了TikTok Studio创作者工具&#xff0c;但是在实际使用过程中&#xff0c;有人却遇到了无法使用的问题&#xff0c;本文就将围绕这个…

作者头像 李华