工作中一个博士同事,代码写的一坨屎,需求不了解就直接开搞,埋了很多坑。明明做的效果不怎么样,但是领导还是很看重。无解了
刚看到个吐槽:自己部门有个博士同事,代码写得一团糟,需求也没搞清就上手,坑到处都是,效果一般,结果领导还特别看重他,发帖人一个字:无解。
我觉得这事吧,先承认一点:学历确实很重要,但它只解决“进门”的问题,不解决“好不好用”的问题。博士头上那俩字,就是标签,就是光环,领导天然会多给几分耐心和信任,这就是现实。
但换个角度想:领导看重的也未必只是代码,他可能看中的是博士身份带来的“可包装性”、背后的资源、人设形象,这些都是职场里的“隐性价值”。
网友们要么骂博士水,要么骂领导瞎,我反倒更关心吐槽的那位:与其盯着别人不配,不如想想——怎么让自己变成那个“虽然不完美但不得不用的人”。技术扎实一点,会汇报一点,会让领导省心一点,慢慢也就有自己的光环了。
面试题:按序打印
按序打印这个题啊,其实挺有意思的,我第一次见还是在公司楼下喝咖啡,旁边我们组那个小李一脸愁容跟我说:“哥,我线程那题又写炸了,面试官让写个按序打印,我直接自闭。”
结果他给我看了一眼题目,哎,其实就那意思:
有三个线程,分别调用 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 走到 3notifyAll()是唤醒所有在这个锁上等待的线程,因为你不知道现在是谁在等
这个写法的缺点就是:代码看起来比 CountDownLatch 复杂一点,容易写错,尤其是刚接触并发的同学。
那这两种方案,实战里我一般怎么选?
面试题、写 LeetCode: CountDownLatch那版又短又直观,优先老项目里已经大量用 wait/notify了,那就按原来的风格来,别整太花的
再多说一句,真正业务里很少会有这么“死板”的 3 步固定顺序,大多都是:
N 个步骤 某些可以并发执行 某些必须等前面几步都完成
这个时候你可以把刚才这套思路扩展一下,比如:
多搞几个 CountDownLatch或者用 CyclicBarrier,等一批线程都准备好了再一起冲
但是啊,你先把这个最基础的“按序打印”弄明白,面试题基本就稳住了。
行了,差不多就这样,我去泡杯咖啡,你可以自己把 Foo 跑起来试试,把三个线程顺序打乱启动,看输出还是不是 first second third。如果不是,那就说明哪儿又手抖了😄
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html