尴尬了!外包同事参加部门聚餐,刚到包间门口,就听主管说:都自己人哈,以后有啥脏活累活都给外包,招他们本就是服务咱的,不要不好意思
刚看到个贴子,说一位外包同事去参加部门聚餐,还没进包间,就听见主管在里面说“以后脏活累活都给外包,招他们本来就是服务咱的”。尴尬拉满。
我觉得这事吧,难堪不只在于那句“脏活累活”,而是“自己人”和“外人”被划了粗粗一道杠。岗位可以有区别,对待人不该有鄙视链。网友们的回复我看了看,有人说“现实就是这样,认命吧”,也有人觉得“外包本来就是廉价劳动力”。说实话,我对这种“理直气壮地不尊重人”的态度挺无语的。
换个角度想,外包也是在给公司创造价值,把人当一次性工具,用工成本可能省了点,团队氛围、口碑和长远效率都要还回去。说到底还是管理者的格局问题:你怎么对待所谓“外人”,正式员工都看在眼里。
面试题:交替打印 FooBar
昨天晚上十一点多,我在公司楼下便利店买咖啡,准备回去改个接口,结果我们组那个小李在群里丢了句:“哥,LeetCode 那个交替打印 FooBar 怎么写啊,用 Java 的,多线程那个,我脑袋已经嗡了。”我当时整个人都是困的,但是一听多线程,精神又来了,毕竟之前排查线程 Bug 的时候,比这个痛苦一百倍的都见过
先把题目说清楚哈,不整啥高大上:
有一个类 FooBar,里面有个整数 n,会启动两条线程:
一个线程负责打印 "foo"另一个线程负责打印 "bar"
要求最终输出是:foobarfoobarfoobar... 连续打印 n 次,中间不能乱序,更不能出现 foofoo 或者 barbar 这种串台的。说白了,就是两个线程“你一句我一句”排队说话,谁也别抢话筒。
我当时跟小李说,你别一上来就想啥“线程池”“并发容器”,这题就考你一个点:怎么在多线程里控制“先后顺序”。
我就边走边跟他说了两个写法,一个偏“底层味”,一个偏“并发工具类味”,你可以挑自己顺手的。
我先从最接地气的 synchronized + wait/notify 这个讲起。这个组合就像两个人抢一把麦克风,谁拿到锁谁说话,另外一个只能干等。
大概结构是这样:
publicclassFooBar{
privateint n;
privateboolean fooTurn = true; // 轮到谁说话
publicFooBar(int n){
this.n = n;
}
publicvoidfoo(Runnable printFoo)throws InterruptedException {
for (int i = 0; i < n; i++) {
synchronized (this) {
while (!fooTurn) {
this.wait(); // 现在不是我,说啥也白搭,只能等
}
printFoo.run(); // 打印 "foo"
fooTurn = false; // 轮到 bar 了
this.notifyAll(); // 叫醒对面
}
}
}
publicvoidbar(Runnable printBar)throws InterruptedException {
for (int i = 0; i < n; i++) {
synchronized (this) {
while (fooTurn) {
this.wait(); // 轮到 foo 的时候,我就闭嘴等
}
printBar.run(); // 打印 "bar"
fooTurn = true; // 又轮回到 foo
this.notifyAll();
}
}
}
}
用的时候两条线程这么跑:
publicstaticvoidmain(String[] args){
FooBar fooBar = new FooBar(3);
Thread t1 = new Thread(() -> {
try {
fooBar.foo(() -> System.out.print("foo"));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread t2 = new Thread(() -> {
try {
fooBar.bar(() -> System.out.print("bar"));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
t1.start();
t2.start();
}
这里面有几个小坑,工作里也经常有人踩:
一个是,很多人喜欢用 if 去判断是否轮到自己,比如:
if (!fooTurn) {
this.wait();
}
这个在面试题里大概率也能过,但在真实环境里容易翻车,因为存在虚假唤醒,线程被莫名其妙唤醒一次,如果你只用 if,它就直接往下执行了,顺序就乱了,所以老老实实用 while 去“二次确认”,这算是多线程里一个小常识。
第二个是,notify() vs notifyAll()。这题里面只有两个线程,其实 notify() 也够用,但很多人写多了以后到业务里去,别的线程也在等同一个锁,这时候随手一个 notify() 叫醒了“错误的人”,整个逻辑就乱成一锅粥。所以我自己习惯这里直接用 notifyAll(),简单粗暴一点,谁该继续谁就再去抢锁。
不过说实话,要是你平时工作里不用 wait/notify,看着会有点烦,脑子里要同时记:谁在等、谁在叫醒、锁在谁手上。那还有一种写法就清爽很多:直接上信号量 Semaphore。
就好像门口排队取号:
fooSem初始 1,说明一开始允许foo先说barSem初始 0,说明bar一开始没资格说
代码是这样:
import java.util.concurrent.Semaphore;
publicclassFooBar{
privateint n;
private Semaphore fooSem = new Semaphore(1);
private Semaphore barSem = new Semaphore(0);
publicFooBar(int n){
this.n = n;
}
publicvoidfoo(Runnable printFoo)throws InterruptedException {
for (int i = 0; i < n; i++) {
fooSem.acquire(); // 拿不到号就等着
printFoo.run(); // 打印 "foo"
barSem.release(); // 轮到 bar
}
}
publicvoidbar(Runnable printBar)throws InterruptedException {
for (int i = 0; i < n; i++) {
barSem.acquire(); // 等 foo 把号发给我
printBar.run(); // 打印 "bar"
fooSem.release(); // 再把号还给 foo
}
}
}
这个写法的好处就是:不需要再关心锁对象是谁,也不用自己 wait/notify,把“轮到谁”的信息都塞进信号量里了。 你只要记住一个节奏:
foo干完活,就给bar放一个许可bar干完活,就给foo放一个许可
说起来就像两个人过桥,每次最多一个人在桥上,过完拉对面一把。
为啥我挺喜欢拿这种题说事儿呢?因为现实工作里,这种“交替打印”的场景不是只有打印日志这么简单。 比如:
一个线程从 MQ 里拉消息,另一个线程负责把消息异步落到 DB,你要控制“拉一批、处理一批”,避免拉太多堆内存炸掉; 又或者一个线程负责把原始报文写到磁盘,另一个线程负责做解析,你想让写入和解析像齿轮一样咬合,不要互相拖后腿。
这些地方本质上都在玩一个东西:有顺序的协作。
LeetCode 这道交替打印的题,就是把问题缩小到最小颗粒,让你把“顺序”和“并发”这个混在一起的东西拆开想:
顺序怎么保证?用状态( fooTurn)或者用许可数量(Semaphore)并发怎么安全?要么自己玩锁 + wait/notify,要么用现成的并发工具类
等你哪天线上碰到线程乱序、死锁、卡死那种诡异问题,再回头看这种小算发题,会突然觉得:哎,好像也不是纯刷题,它至少帮你提前把脑子拧到“并发模式”上。
行了不唠了,我这会儿得去看下我们测试环境那个线程池配置是不是又被谁改小了,不然等会又有人喊接口卡死了。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html