程序员老鬼

忽然发现同事老婆很漂亮,感觉身边研发同事好多这样,人都不善言辞的,找老婆倒是都很成功。

昨晚十一点多,我在公司楼下跟小李吃泡面,他一脸生无可恋跟我说:“哥,今天面试官让我写个多线程打印 0 和奇偶数,我当场脑袋就空白了……”我一听这题目,你们肯定也见过,就是那个打印 0102030405... 的题,对吧。

先把题目用人话说清楚哈,不整那些晦涩的定义。大概意思是:有三个线程,一个专门打印 0,一个专门打印奇数,一个专门打印偶数,最后控制这三个线程按顺序输出:01020304... 一直到 n。比如 n = 5,那输出就是 0102030405。 难点不是“打印”,而是:怎么让三个线程老老实实排队轮流输出,不抢话,不掉号。

我当时在桌子上随手拿了三根一次性筷子,跟小李比划。 想象一下:

  • 拿黑色筷子的负责打印 0
  • 拿红色筷子的打印奇数
  • 拿蓝色筷子的打印偶数

桌子中间放一块牌子 state,上面写当前轮到谁了:

  • 写 0:代表轮到“打印 0”的线程
  • 写 1:代表轮到“打印奇数”的线程
  • 写 2:代表轮到“打印偶数”的线程

每个线程都盯着这块牌子,不是自己就乖乖等,是自己就干活,然后把牌子改成下一个人,再喊一嗓子“可以了”,别人再起来干活。其实多线程那点事儿,本质就是“轮流说话”加“一个共享状态”。

小李当时第一反应是:“那我三个 while 死循环,不停判断该不该打印,不就完了?” 我说你这个叫“自旋”,CPU 会被你打满,电脑小风扇都要起飞。面试官看到你这么写,多半会跟你说“那今天就先聊到这儿”。

换个稍微正经点的写法,我们用 synchronized + wait/notifyAll,毕竟 Java 里最朴素那套同步工具,上手快,理解成本也低。

我直接把核心代码给你,先看个全貌,再一点点拆:

import java.util.function.IntConsumer;

publicclassZeroEvenOdd{

privatefinalint n;
// 0: 该打印 0 了;1: 该打印奇数;2: 该打印偶数
privateint state = 0;

publicZeroEvenOdd(int n){
this.n = n;
    }

// 打印 0 的线程执行这个方法
publicsynchronizedvoidzero(IntConsumer printNumber)throws InterruptedException {
for (int i = 1; i <= n; i++) {
// 不是自己就等着
while (state != 0) {
                wait();
            }
            printNumber.accept(0);
// 决定下一个该谁来,根据当前数字 i 的奇偶
if ((i & 1) == 1) {
                state = 1;  // 轮到奇数
            } else {
                state = 2;  // 轮到偶数
            }
            notifyAll();
        }
    }

// 打印奇数的线程执行这个方法
publicsynchronizedvoidodd(IntConsumer printNumber)throws InterruptedException {
for (int i = 1; i <= n; i += 2) {
while (state != 1) {
                wait();
            }
            printNumber.accept(i);
// 打完奇数,再轮到 0
            state = 0;
            notifyAll();
        }
    }

// 打印偶数的线程执行这个方法
publicsynchronizedvoideven(IntConsumer printNumber)throws InterruptedException {
for (int i = 2; i <= n; i += 2) {
while (state != 2) {
                wait();
            }
            printNumber.accept(i);
// 打完偶数,再轮到 0
            state = 0;
            notifyAll();
        }
    }

// 简单测一下
publicstaticvoidmain(String[] args){
        ZeroEvenOdd zeroEvenOdd = new ZeroEvenOdd(5);

        IntConsumer printer = x -> System.out.print(x);

        Thread tZero = new Thread(() -> {
try {
                zeroEvenOdd.zero(printer);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread tOdd = new Thread(() -> {
try {
                zeroEvenOdd.odd(printer);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread tEven = new Thread(() -> {
try {
                zeroEvenOdd.even(printer);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        tZero.start();
        tOdd.start();
        tEven.start();
    }
}

你先别急着往下划,脑子里过一遍执行顺序:

  1. 程序一启动,state = 0,所以“打印 0”的那个线程一上来就能干活,另外两个看到 state != 自己,就老老实实 wait() 了。
  2. 打 0 的线程,第一次打印出 0,这时候 i = 1,是奇数,于是把 state 改成 1,意思是“奇数同学你上”,然后 notifyAll() 把大家都叫醒。
  3. 被叫醒后,奇数线程发现 state == 1,开心打印 1,然后把 state 改回 0,让“打印 0”的接着来。
  4. 如此反复,奇数、偶数轮流接到 0 的“发球”,整个顺序就被锁死了。

这里面几个小点,面试的时候非常容易被问到,我当时跟小李强调了几遍:

一个是为什么用 while (state != X) { wait(); },不用 if。 你们知道线程有个东西叫“虚假唤醒”,就是明明没人叫你,你也醒了;或者一起被唤醒,但是状态又被别人改回去了。 如果用 if,你醒来一次就不再检查条件,直接往下打印,就可能乱序。while 每次被唤醒都会重新检查 state,不满足就继续等,这个是标准写法。

还有一个是 notify() 和 notifyAll() 的问题。 理论上说,叫一个人起来(notify)就够了,但是我们这里有三个线程,谁在等你其实不一定。 为了不踩奇怪的边角 bug,用 notifyAll(),把所有等的人都叫醒,真正能继续干活的,还是只有那个 state 对得上的线程,其他人醒来发现状态不对,又继续 wait() 了。

再一个小细节,odd 方法里 for 循环是 i += 2,even 也是 i += 2,这样每个线程只管自己那一拨数字,比在里面自己算“当前该打印几”要清爽得多,也不容易写错范围。

小李当时看着代码,跟我说:“这么一写,好像也没那么可怕哈,就是围着一个 state 转。” 我说是啊,多线程这种题,别上来就想各种锁、各种类,先把“轮流说话”的模型想明白,再翻译成 state + wait/notify,就顺了。

当然这题也有别的写法,比如用 Semaphore 三把信号量排队,或者 Lock + Condition 把三个条件队列分开,整体思路都差不多: 共享一个计数 i,三个线程守着不同的条件,满足就打印,不满足就睡。

不过对大部分同学来说,你只要能把上面这版 synchronized 写顺溜,把几个细节说清楚:

  • 为什么要 while
  • 为什么要 notifyAll
  • state 是怎么在三个线程之间流转的 面试官一般就知道你多线程的基本功还不错了。

行了,零和奇偶数就聊到这儿吧,我手机又在震了,估计是产品在群里喊人,我先去看下他们又改了啥需求……

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

最后给大家分享一份不错的副业资料,点击下方公众号,回复关键字: 副业 领,也可以链接我微信:hls404