I/O同异步阻塞
在学习 Java I/O 类库时,容易混淆 NIO、BIO、AIO 这几个概念。同时也难以区分阻塞和非阻塞、同步和异步,这篇文章主要区分这几个概念的以及分享一些个人见解。
前言
各位,下面三张脑图清楚的向大家展示了 IO。
到这里,我们先来思考一个问题:我们经常所说的“IO”的全称到底是什么?
可能很多人看到这个问题和我一样一脸懵逼,IO 的全称其实是:Input/Output的缩写。
下面就让我们来进入正题。
BIO、NIO、AIO 对比
这里只考虑两个实体(客户端、服务端)和一个事件(客户端向服务端请求数据)
同步、异步描述的是:客户端在请求数据的过程中,能否做其他事情。
阻塞、非阻塞描述的是:客户端与服务端是否从头到尾始终都有一个持续连接,以至于占用了通道,不让其他客户端成功连接。
那么 BIO、NIO、AIO 就可以简单的理解为:
- BIO(同步阻塞):客户端在请求数据的过程中,保持一个连接,不能做其他事情。
- NIO(同步非阻塞):客户端在请求数据的过程中,不用保持一个连接,不能做其他事情。(不用保持一个连接,而是用许多个小连接,也就是轮询)
- AIO(异步非阻塞):客户端在请求数据的过程中,不用保持一个连接,可以做其他事情。(客户端做其他事情,数据来了等服务端来通知。)
是不是逻辑清楚了?结论下完了,接下来我们说说同步与阻塞的理解。
同步和异步
3.1 常见的误区:
假设有一个展示用户详情的需求,分两步,先调用一个 HTTP 接口拿到详情数据,然后使用适合的视图展示详情数据。
如果网速很慢,代码发起一个 HTTP 请求后,就卡住不动了,直到十几秒后才拿到 HTTP 响应,然后继续往下执行。
这个时候你问别人,刚刚代码发起的这个请求是不是一个同步请求,对方一定回答是。这是对的,它确实是。
但你要问它为什么是呢?对方一定是这样回答的,“因为发起请求后,代码就卡住不动了,直到拿到响应后才可以继续往下执行”。
我相信很多人也都是这样认为的,其实这是不对的,是把因果关系搞反了:不是因为代码卡住不动了才叫同步请求,而是因为它是同步请求所以代码才卡住不动了。
至于为什么能卡住不动,这是由操作系统和 CPU 决定的:因为内核空间里的对应函数会卡住不动,造成用户空间发起的系统调用卡住不动,继而使程序里的用户代码卡住不动了。因此卡住不动了只是同步请求的一个副作用,并不能用它来定义同步请求,那该如何定义呢?
3.2 同步
所谓同步,指的是协同步调。既然叫协同,所以至少要有 2 个以上的事物存在。协同的结果就是:多个事物不能同时进行,必须一个一个的来,上一个事物结束后,下一个事物才开始。那当一个事物正在进行时,其它事物都在干嘛呢?严格来讲这个并没有要求,但一般都是处于一种“等待”的状态,因为通常后面事物的正常进行都需要依赖前面事物的结果或前面事物正在使用的资源。
因此,可以认为,同步更希望关注的是从宏观整体来看,多个事物是一种逐个逐个的串行化关系,绝对不会出现交叉的情况。所以,自然也不太会去关注某个瞬间某个具体事物是处于一个什么状态。把这个理论应用的出神入化的非“排队”莫属。凡是在资源少需求多的场景下都会用到排队。
比如排队买火车票这件事:其实售票大厅更在意的是旅客一个一个的到窗口去买票,因为一次只能卖一张票。即使大家一窝蜂的都围上去,还是一次只能卖一张票,何必呢?挤在一起又不安全。只是有些人素质太差,非要往上挤,售票大厅迫不得已,采用排队这种形式来达到自己的目的,即一个一个的买票。至于每个旅客排队时的状态,是看手机呀还是说话呀,根本不用去在意。
除了这种由于资源导致的同步外,还存在一种由于逻辑上的先后顺序导致的同步。比如,先更新代码,然后再编译,接着再打包。这些操作由于后一步要使用上一步的结果,所以只能按照这种顺序一个一个的执行。
关于同步还需知道两个小的点:
- 范围,并不需要在全局范围内都去同步,只需要在某些关键的点执行同步即可。比如食堂只有一个卖饭窗口,肯定是同步的,一个人买完,下一个人再买。但吃饭的时候也是一个人吃完,下一个人才开始吃吗?当然不是啦。
- 粒度,并不是只有大粒度的事物才有同步,小粒度的事物也有同步。只不过小粒度的事物同步通常是天然支持的,而大粒度的事物同步往往需要手工处理。比如两个线程的同步就需要手工处理,但一个线程里的两个语句天然就是同步的。
3.3 异步
所谓异步,就是步调各异。既然是各异,那就是都不相同。所以结果就是:多个事物可以你进行你的、我进行我的,谁都不用管谁,所有的事物都在同时进行中。
一言以蔽之,同步就是多个事物不能同时开工,异步就是多个事物可以同时开工。
注:一定要去体会“多个事物”,多个线程是多个事物,多个方法是多个事物,多个语句是多个事物,多个 CPU 指令是多个事物。等等等等。
阻塞和非阻塞
所谓阻塞,指的是阻碍堵塞。它的本意可以理解为由于遇到了障碍而造成的动弹不得。所谓非阻塞,自然是和阻塞相对,可以理解为由于没有遇到障碍而继续畅通无阻。
对这两个词最好的诠释就是,当今中国一大交通难题,堵车。
汽车可以正常通行时,就是非阻塞。一旦堵上了,全部趴窝,一动不动,就是阻塞。因此阻塞关注的是不能动,非阻塞关注的是可以动。不能动的结果就是只能等待,可以动的结果就是继续前行。
所以和阻塞搭配的词一定是等待,和非阻塞搭配的词一定是进行。
回到程序里,阻塞同样意味着停下来等待,非阻塞表明可以继续向下执行。从 CPU 角度来看就是这样的:
- 阻塞与非阻塞主要是从 CPU 的消耗上来说的;
- 阻塞就是 CPU 停下来,等待一个慢的操作完成之后才接着完成其它的事;
- 非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作;
- 虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。
同步,异步和阻塞,非阻塞
同步/异步,关注的是能不能同时开工。阻塞/非阻塞,关注的是能不能动。通过推理进行组合:
- 同步阻塞,不能同时开工,也不能动。只有一条小道,一次只能过一辆车,可悲的是还堵上了。
- 同步非阻塞,不能同时开工,但可以动。只有一条小道,一次只能过一辆车,幸运的是可以正常通行。
- 异步阻塞,可以同时开工,但不可以动。有多条路,每条路都可以跑车,可气的是全都堵上了。
- 异步非阻塞,可以工时开工,也可以动。有多条路,每条路都可以跑车,很爽的是全都可以正常通行。
是不是很容易理解啊。其实它们的关注点是不同的,只要搞明白了这点,组合起来也不是事儿。
回到程序里,把它们和线程关联起来:
- 同步阻塞,相当于一个线程在等待。
- 同步非阻塞,相当于一个线程在正常运行。
- 异步阻塞,相当于多个线程都在等待。
- 异步非阻塞,相当于多个线程都在正常运行。
性能分析
组合方式 | 性能分析 |
---|---|
同步阻塞 | 最常用的一种用法,使用也是最简单的,但是 I/O 性能一般很差,CPU 大部分在空闲状态。 |
同步非阻塞 | 提升 I/O 性能的常用手段,就是将 I/O 的阻塞改成非阻塞方式,尤其在网络 I/O 是长连接,同时传输数据也不是很多的情况下,提升性能非常有效。这种方式通常能提升 I/O 性能,但是会增加 CPU 消耗,要考虑增加的 I/O 性能能不能补偿 CPU 的消耗,也就是系统的瓶颈是在 I/O 还是在 CPU 上。 |
异步阻塞 | 这种方式在分布式数据库中经常用到,例如在往一个分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,而还有两至三份是备份记录会写到其它机器上,这些备份记录通常都是采用异步阻塞的方式写 I/O。异步阻塞对网络 I/O 能够提升效率,尤其像上面这种同时写多份相同数据的情况。 |
异步非阻塞 | 这种组合方式用起来比较复杂,只有在一些非常复杂的分布式情况下使用,像集群之间的消息同步机制一般用这种 I/O 组合方式。如 Cassandra 的 Gossip 通信机制就是采用异步非阻塞的方式。它适合同时要传多份相同的数据到集群中不同的机器,同时数据的传输量虽然不大,但是却非常频繁。这种网络 I/O 用这个方式性能能达到最高。 |