程序员老鬼

好多央国企,工资到手才四五千,里面一大堆都是硕士研究生学历,工资明明这么低,为啥央国企还能把这帮高学历的年轻人稳稳留住?

央国企到手四五千,放外面看确实不高,尤其一看名单,硕士一抓一把,985、211也不少。你说他们图啥?真图那几千块工资吗?大概率不是。

很多人图的是稳。不是嘴上说说那种稳,是房贷敢办、家里人放心、对象父母听了不皱眉的稳。外面互联网一年多拿几万,看着香,但今天优化、明天转岗,后天部门没了,心脏不好的真扛不住。

Image

还有些岗位,工资低归低,福利、食堂、补贴、公积金、年假这些加一起,账不能只看到账短信。更别说有些人进去后,压根不是奔着短期发财去的,是想慢慢熬资历、熬编制感、熬个不用天天被KPI追着跑的生活。

所以这事儿离谱吗?有点。但你要说没人懂账,那也不至于。大家都不傻,只是怕的东西不一样。

面试题:睡眠函数

线程明明睡了 100ms,日志里却只隔了 83ms。

这种题别急着写 Thread.sleep(millis)。面试里问“睡眠函数”,通常不是考你会不会调 API,而是想看你知不知道:睡眠不是精确定时器,线程可能被中断,也可能提前醒,系统调度还会把时间拖长。

题目可以简化成一句话:

给一个毫秒数 millis,让当前线程至少等待这么久,再继续往下执行。

我一般不会直接写成这样:

Thread.sleep(millis);

这行代码当然能跑,但它有两个问题。

第一,中断怎么处理没说清楚。

第二,sleep 被中断后会直接抛异常,实际等待时间可能不够。你要的是“至少睡够”,还是“被打断就结束”,这两个语义差很多。

我更愿意把代码写成这种:

import java.util.concurrent.locks.LockSupport;

publicclassQuietSleeper{

publicstaticbooleansleepAtLeast(long millis){
if (millis <= 0) {
returntrue;
        }

long deadline = System.nanoTime() + millis * 1_000_000L;
boolean interrupted = false;

while (true) {
long left = deadline - System.nanoTime();
if (left <= 0) {
break;
            }

            LockSupport.parkNanos(left);

if (Thread.interrupted()) {
                interrupted = true;
            }
        }

if (interrupted) {
            Thread.currentThread().interrupt();
        }

return !interrupted;
    }
}

这里有几个地方别写歪。

我没有用 currentTimeMillis(),因为它是墙上时间,可能被系统时间校准影响。比如机器时间被 NTP 往回拨了一下,你这个睡眠逻辑就很难看。nanoTime() 看的是相对时间,更适合做间隔计算。

还有一个点,parkNanos 不是承诺睡满。它可能提前返回。所以外面必须套一层循环,醒了就重新算还剩多久。这个细节不少人会漏。

测试的时候可以这么跑:

publicclassSleepTest{
publicstaticvoidmain(String[] args){
long start = System.nanoTime();

boolean clean = QuietSleeper.sleepAtLeast(300);

long costMs = (System.nanoTime() - start) / 1_000_000;
        System.out.println("cost=" + costMs + "ms, clean=" + clean);
    }
}

正常输出大概是:

cost=301ms, clean=true

不一定刚好 300ms。这里别较真,Java 线程不是单片机定时器,调度权在操作系统手里。你能保证的是“不少于目标时间”,不能保证“正好这个时间”。

再看中断场景:

Thread t = new Thread(() -> {
boolean ok = QuietSleeper.sleepAtLeast(2000);
    System.out.println("sleep result=" + ok);
    System.out.println("interrupt flag=" + Thread.currentThread().isInterrupted());
});

t.start();

Thread.sleep(200);
t.interrupt();

这段代码里,我没有在被中断后立刻退出,而是继续睡够 2 秒,然后把中断标记补回去。

为什么这么写?

因为这个函数的名字叫 sleepAtLeast,语义就是至少睡够。中断这件事不能吞,吞了后面业务线程池、任务取消、网关超时都会变得很难查。所以最后要 interrupt() 补回去。

如果题目要求“被中断就停止等待”,那代码反而更短:

publicstaticbooleansleepUntilInterrupted(long millis){
if (millis <= 0) {
returntrue;
    }

try {
        Thread.sleep(millis);
returntrue;
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
returnfalse;
    }
}

这两段代码没有谁高级谁低级,关键看题目语义。

手写睡眠函数真正要盯的就三件事:时间源别用错,提前醒要重算,中断别吞掉。

这题看着小,线上很多定时轮询、失败重试、限流退避,最后都栽在这些小地方。