妙啊,阻塞到底是个啥?黄袍加身,亦能谈古说今

1,879 阅读8分钟

不羡鸳鸯不羡仙,一行代码调半天。原创:小姐姐味道(ID:xjjdog),欢迎分享,转载请保留出处。

现在,请记住你的身份!从进入本篇文章开始,你就是皇帝!三宫六院七十二妃,任君品尝。

人有亲疏远近,事有轻重缓急。作为万岁,你的时间非常宝贵。整个王朝都在你手中运算,方能国泰民安。

为了讨论方便,我们把场景界限在单核CPU上。你就是CPU,当然是仅仅是一颗单核的CPU。

为了让你更好的安排自己的时间,我将你的时间切割成了八九七十二份,每一份都弥足珍贵。

就凭我画的这些密密麻麻的小方块,你就应该给xjjdog点下赞。

现实的CPU,时间片分的会更细,但作为人类你是理解不了那么小的时间间隔的:你可能每天都要花很多时间在吃喝拉撒上,但后宫里总有大部分希望得到你宠幸的妃子,你一点时间片都不留给她。

实在是忙不过来呀!需要一个太监!

1. 中断就是从中断掉

不是让太监来帮你干活的,他没有那个能力。太监是用来给你调度工作的。

比如,有反叛的军队攻到了城外,太监慌慌张张来报告,你就不得不暂停后宫的活动,提着裤子处理首要的问题;再比如,有刚来的妃子频频抛媚眼,但你还有一大堆公文要批,心有余而力不足。

这种处理问题的方式,就是中断(从中断掉就是太监)。中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的 CPU 暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。

我们来看下底层的中断处理程序。

request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)

可以看到,太监只需要给皇帝要做的事情,都编码备案,并固定下处理流程,调整好优先级,皇帝的时间片就可以有效的轮转起来。不至于江山都丢了,还在后宫里风花雪月。

拿网络传输来说,当有了网络数据包,就需要及时处理,否则客户端会超时。这个时候,网卡会立马发出中断请求,CPU就会通过网卡的中断程序去处理这些缓冲区。这都是非常重要的工作。

中断又有硬中断和软中断之分。硬中断是由硬件产生的,比如,像磁盘,网卡,键盘,时钟等。软中断是由当前正在运行的进程所产生的,通常优先级比硬中断低一些。

2. 阻塞会占用CPU么?

代入了皇帝这个身份,我们就可以解释一些平常遇到的,令人疑惑的问题。

我们都见过在Concurent包下面,有一个叫做LinkedBlockingQeque的类。从它的名字就可以看出,这是一个阻塞队列。实际上,它也并不是挂着羊头卖狗肉。

如下面的代码,我们通常把它放在循环中。我对while(true)这种东西是有心理阴影的,因为它有可能会跑满你的CPU。

while(true){
   Object o =  linkedBlockingQeque.poll();
}

但实际上,并不会。因为人家都说了,这是个阻塞队列。

相似的,还有NIO中的select。把逻辑放在while循环里,不怕得报应么?

 while (!stop) {
 int num = selector.select();
    if (num == 0) {
        continue;
    }
    Iterator<SelectionKey> events = selector.selectedKeys().iterator();
 }

这还真不怕。因为阻塞并不会占用任何资源。

比如,小太监上报了一个折子,是关于吕嫔妃的舅舅的贪污问题处理。但是这个问题,需要等待司法调查的结果,还需要听听爱妃的意见,就先可以把它搁置在一旁。

把问题记录在一个其他的小册子里,等这些依赖的事办的差不多了,同时你又有龙时,那就可以继续处理。

可以看到,这种阻塞性的问题,虽然是个任务,但并不会占用你的任何时间,这在计算机中是一样的。

我们来看一下常见的Java阻塞方式。

sleep和wait

睡和等。用词很巧妙,到底妙在哪呢?因为它是现实中的场景。

sleep

sleep函数会让线程在一定的时间内进入阻塞状态,不能得到cpu时间,但不会释放锁资源。指定的时间一过,线程重新进入可执行状态。

注意我们这里说的是线程,并不是CPU本身。线程不活动了,并不代表CPU不能干其他事情。

比如,今天是接见大臣的黄道吉日,王天师得到了接见的机会,其他的大臣们就得在外面等着被传唤。结果王天师的谈话又臭又长,勾不起你的任何兴趣。正好小太监急匆匆跑来,在你耳边悄悄说: 李贵妃生了个儿子!

这是让人振奋的事情,因为其他儿子都在宫斗中被KO了。于是你装模作样的对王天师说: 我现在有点头痛,需要小憩一会儿。” 其实你已经偷偷去探望李贵妃了。

注意,这个时候,王天师只能唯唯诺诺的等着。对于“接见”这个主线来说,其他的大臣也只能在外面等着被传唤。它们都没有拿到“接见”这把锁,王天师也一直占用着这把锁,直到你看完了儿子归来。

这就是sleep不释放锁的意思,因为sleep后,在sleep那一瞬间的任何东西都没有改变。

wait

wait( ) 使线程进入阻塞状态,同时释放自己占有的锁资源,和notify( )搭配使用。

对于wait来说,就完全不一样了。 如图,每个监视器(Monitor)在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”。而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”,而在 “Wait Set”中等待的线程状态是 “in Object.wait()”。

术语难以理解,还是以皇帝的身份来潇洒一下。

这个时候,你还打算接见大臣。不过,现在不想再one by one了,因为这太低效太枯燥了。某个大臣在你的书房里待得长了些,就有可能有大臣怀疑你在搞gay,这种副作用让人心里不悦。

p2p不行,那就聚在一块谈谈心吧。

正在和你谈话的是王天师,因为这货话比较多,你也比较喜欢他。

王天师说: 小太子出生在三伏天,就叫史三伏吧!

你这才想起自己姓史。作为熟读文章的皇帝,你对此嗤之以鼻,听着这不入流的名字,还隐隐有点生气。

王爱卿,你还是先wait一下吧,听听别人意见。

这个时候,一大堆等着拍马屁的大臣开始举手,跃跃欲试。刘道长抢到了 谈话主线 这把锁。

刘道长: 天地长久,人有终时,北冥有鱼,其名为鲲,可活亿年。我看,就叫史鲲吧。

你听后微微颔首,果然仙人嘴下口水香,但总感觉有点怪异。

注意注意。等着发言的这群大臣,就叫做Entry Set,谁举手举得快,就可以回答这个问题。

像王天师这种被喊停的大臣,就属于Wait Set,只有你重新让他说话,他才有机会。

这整个过程,谈话是可以继续的,并不因为王天师被禁言了谈话就无法进行下去。我们就可以说,wait操作是释放了对象锁的。

计算机中各种所谓的阻塞,都是通过划分不同的队列资源进行处理。比如epoll就是围绕着工作队列和等待队列进行编程的。虽然底层的数据结构有些不同,但思想都是一样的。

线程如何获取时间片?

这个不容易回答,因为你需要知道一个事实:Java中的线程,在Linux上本质是一个轻量级进程,它的调度都是操作系统来完成的。

可以看一下我们最上面那一副让人容易产生密集恐惧症的图片。我们的CPU时间,就划分为多个CPU时间片。你的程序虽然在执行while(true),但不代表它总能够得到CPU资源,所以其他的进程也有机会去执行。

JVM采用抢占式调度模型,指的是让优先级高的线程占用比较多的CPU,如果线程优先级相同,那么就随机选择一个线程,使其占用CPU。

注意“随机”这两个字,就非常的有魔性。它可以让你每天都中100万的彩票,也可能每天喝水都被呛着。

可怜的计算机系统,也参与到大千世界让人无奈的随机命运而来。

但有一种很霸道的任务,对CPU一抢一个准,那就是我们上面提到的硬中断--那些不得不优先处理的事情。

下辈子投胎,就当个硬中断吧(囧)。

快来点赞累加你的幸运值吧 :)。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的GZH。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。