目录
从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)包中的ReentrantLock和Condition。这对组合的核心优势是“精准唤醒”——可以只唤醒需要执行的目标线程,彻底消除冗余的锁竞争。
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 核心知识点拆解(从陌生到熟悉)
作为初学者,我一开始对ReentrantLock和Condition充满陌生感,但把它们和熟悉的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时的执行流程,这对理解线程交互非常有帮助:
初始状态:
flag=false(foo轮次),foo和bar线程启动后争抢lock;第一次foo执行:
foo抢到锁,
while(flag==true)不成立,执行printFoo.run();设置
flag=true,调用barCond.signal()唤醒bar线程;finally中释放锁,foo线程退出同步块,准备下一轮循环。
第一次bar执行:
被唤醒的bar抢到锁,
while(flag==false)不成立,执行printBar.run();设置
flag=false,调用fooCond.signal()唤醒foo线程;finally中释放锁,bar线程退出同步块。
第二次循环:重复步骤2-3,直到foo和bar都完成n次执行,最终输出
foo bar foo bar。
四、学习感悟:多线程的“进阶思维”
从synchronized到ReentrantLock + Condition,我不仅学会了一个问题的更优解法,更体会到多线程编程的核心思维转变:
从“能用”到“好用”:基础解法能满足功能,但工业级开发更关注效率和健壮性。
Condition的精准唤醒就是从“能用”到“好用”的关键;理解“锁”的本质:锁不仅是“互斥”的工具,更是“线程通信”的桥梁。
ReentrantLock通过绑定Condition,让线程通信更精准;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,建议从这个问题入手,手动推演执行流程,相信你会和我一样,对多线程同步有更深刻的理解~