news 2026/3/18 22:50:15

从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/3/17 9:37:47

如何实现照片扫码即看?图片转二维码技巧

在日常分享、物料印刷、信息存档等场景中&#xff0c;将照片转化为二维码是一种高效便捷的展示方式。扫码即可直接查看高清图片&#xff0c;无需手动传输或下载&#xff0c;无论是个人分享旅行照片、企业展示产品图册&#xff0c;还是活动现场陈列作品&#xff0c;都能大幅提升…

作者头像 李华
网站建设 2026/3/15 17:44:31

编辑器工具--直接将精灵拖进场景并自动创建物体然后赋值给Image组件

我们平时是自己再创景里的Canvas上创建一个物体 物体上有Image组件 然后把美术给的Sprite拖进去 现在这个编辑器工具 直接批量选中 一键再场景生成好物体 上面挂载了相应精灵 以下脚本赋值以后 放到 Editor文件夹下 再工具栏的tools下面就有了 using UnityEditor; using UnityE…

作者头像 李华
网站建设 2026/3/13 22:30:22

如何高效批量制作静态网址码?批量静态网址码生成技巧

在日常工作中&#xff0c;当需要将数十甚至上百个静态网址转化为二维码时&#xff0c;逐个制作不仅耗时费力&#xff0c;还容易出现重复或错误。批量生成二维码能大幅提升效率&#xff0c;尤其适用于企业推广、活动宣传、文档配套等场景。下面分享一套实用的批量静态网址码在线…

作者头像 李华
网站建设 2026/3/15 16:15:32

kanass全面介绍(14) - 如何管理项目集

kanass是一款国产开源免费、简洁易用的项目管理工具&#xff0c;包含项目管理、项目集管理、事项管理、版本管理、迭代管理、计划管理等相关模块。工具功能完善&#xff0c;用户界面友好&#xff0c;操作流畅。本文主要介绍项目集管理。1、添加项目集1.1 添加项目集点击项目集-…

作者头像 李华
网站建设 2026/3/13 11:04:58

【dz-949】矿井安全通风系统设计

矿井安全通风系统设计 摘要 在矿井生产环境中&#xff0c;温湿度异常、烟雾聚集及瓦斯泄漏等问题直接威胁作业人员的生命安全。温度过高可能引发设备故障或火灾&#xff0c;湿度过大影响作业环境&#xff0c;烟雾和瓦斯浓度超标则易导致爆炸或中毒事故&#xff0c;这些隐患若不…

作者头像 李华