程序员老鬼

工作中一个博士同事,代码写的一坨屎,需求不了解就直接开搞,埋了很多坑。明明做的效果不怎么样,但是领导还是很看重。无解了

刚看到个吐槽:自己部门有个博士同事,代码写得一团糟,需求也没搞清就上手,坑到处都是,效果一般,结果领导还特别看重他,发帖人一个字:无解。

Image

我觉得这事吧,先承认一点:学历确实很重要,但它只解决“进门”的问题,不解决“好不好用”的问题。博士头上那俩字,就是标签,就是光环,领导天然会多给几分耐心和信任,这就是现实。

但换个角度想:领导看重的也未必只是代码,他可能看中的是博士身份带来的“可包装性”、背后的资源、人设形象,这些都是职场里的“隐性价值”。

网友们要么骂博士水,要么骂领导瞎,我反倒更关心吐槽的那位:与其盯着别人不配,不如想想——怎么让自己变成那个“虽然不完美但不得不用的人”。技术扎实一点,会汇报一点,会让领导省心一点,慢慢也就有自己的光环了。

面试题:按序打印

按序打印这个题啊,其实挺有意思的,我第一次见还是在公司楼下喝咖啡,旁边我们组那个小李一脸愁容跟我说:“哥,我线程那题又写炸了,面试官让写个按序打印,我直接自闭。”

结果他给我看了一眼题目,哎,其实就那意思:

有三个线程,分别调用 first()、second()、third() 三个方法,每个方法里会打印一段东西,要求不管线程启动顺序多乱,最后输出必须是:

first
second
third

就这么点事儿,对吧,看着简单,但一扯到“多线程按顺序”,不少人就开始脑袋打结了。

先把题目翻译成人话一下:

  • 有一个类叫 Foo
  • 里面有三个方法:first、second、third
  • 三个方法会被三个不同的线程调用,谁先启动你控制不了
  • 但你得保证执行顺序一定是:先 first,再 second,最后 third

你可以想象成: 运营同学要发一个活动,先“生成活动内容”,再“计算优惠”,最后“推送给用户”。你要是第三步先跑了,那推的不是垃圾数据嘛。

我先说一个很多人下意识会想到、但不太好的写法:

有的人会想着搞一个共享变量:

volatileint flag = 1;

然后在 first 里把 flag 改成 2,在 second 里改成 3,在 third 里不停 while 等:

while (flag != 1) { } // 忙等

这种写法面试现场一看就不太行:

  • CPU 会一直空转,浪费资源
  • 线上真这么写,风扇先飞起来

所以这种“死循环+flag”的写法,偶尔当个思路还行,别真写到生产里去。

咱们来个正经一点、又好理解的方案:用 CountDownLatch

它是 JUC 里特别顺手的一个同步工具,说白了就是“倒计时门闩”:

  • 初始化的时候给个计数,比如 1
  • 别的线程调用 await() 会在门口等着
  • 某个线程调用 countDown() 把计数减到 0,门就开了,等着的线程一起通过

按序打印刚好可以用两个门:

  • 第二步要等第一步干完
  • 第三步要等第二步干完

直接上代码,你们先感受一下:

import java.util.concurrent.CountDownLatch;

publicclassFoo{

private CountDownLatch secondLatch = new CountDownLatch(1);
private CountDownLatch thirdLatch = new CountDownLatch(1);

publicFoo(){
    }

publicvoidfirst(Runnable printFirst)throws InterruptedException {
// 这里是真正的“first”逻辑
        printFirst.run();
// 通知 second:我跑完了,你可以开始等下一个了
        secondLatch.countDown();
    }

publicvoidsecond(Runnable printSecond)throws InterruptedException {
// 先等 first 完成
        secondLatch.await();
        printSecond.run();
// 通知 third:轮到你了
        thirdLatch.countDown();
    }

publicvoidthird(Runnable printThird)throws InterruptedException {
// 先等 second 完成
        thirdLatch.await();
        printThird.run();
    }
}

这个写法的逻辑特别直:

  • second() 一上来先 await(),卡在门口
  • 只有 first() 把 secondLatch.countDown() 调掉了,门开了,second() 才能继续往下跑
  • third() 也是一样,等第二个门

线程启动顺序再乱,也逃不出这俩门的控制。面试官一般看你写出这版,就知道你对并发有点概念。

有人会问:能不能只用 synchronized 搞定?当然可以,就是写起来啰嗦一点。

来个比较“原始”的版本:wait/notifyAll

publicclassFooSync{

privateint flag = 1; // 1 表示该 first,2 表示该 second,3 表示该 third

publicFooSync(){
    }

publicvoidfirst(Runnable printFirst)throws InterruptedException {
synchronized (this) {
while (flag != 1) {
this.wait();
            }
            printFirst.run();
            flag = 2;
this.notifyAll();
        }
    }

publicvoidsecond(Runnable printSecond)throws InterruptedException {
synchronized (this) {
while (flag != 2) {
this.wait();
            }
            printSecond.run();
            flag = 3;
this.notifyAll();
        }
    }

publicvoidthird(Runnable printThird)throws InterruptedException {
synchronized (this) {
while (flag != 3) {
this.wait();
            }
            printThird.run();
// 到这儿其实已经结束了
        }
    }
}

这里有几个小点你注意一下就行:

  • 为啥用 while 不用 if?因为线程被唤醒不一定是因为条件满足,也可能是“虚假唤醒”,醒来再检查一遍更保险
  • flag 就是一个简单的状态机,从 1 走到 3
  • notifyAll() 是唤醒所有在这个锁上等待的线程,因为你不知道现在是谁在等

这个写法的缺点就是:代码看起来比 CountDownLatch 复杂一点,容易写错,尤其是刚接触并发的同学。

那这两种方案,实战里我一般怎么选?

  • 面试题、写 LeetCode:CountDownLatch 那版又短又直观,优先
  • 老项目里已经大量用 wait/notify 了,那就按原来的风格来,别整太花的

再多说一句,真正业务里很少会有这么“死板”的 3 步固定顺序,大多都是:

  • N 个步骤
  • 某些可以并发执行
  • 某些必须等前面几步都完成

这个时候你可以把刚才这套思路扩展一下,比如:

  • 多搞几个 CountDownLatch
  • 或者用 CyclicBarrier,等一批线程都准备好了再一起冲

但是啊,你先把这个最基础的“按序打印”弄明白,面试题基本就稳住了。

行了,差不多就这样,我去泡杯咖啡,你可以自己把 Foo 跑起来试试,把三个线程顺序打乱启动,看输出还是不是 first second third。如果不是,那就说明哪儿又手抖了😄

-END-

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

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