IO:阻塞和非阻塞、同步和异步
阻塞和非阻塞
阻塞的时候线程会被挂起
阻塞:
当数据还没准备好时,调用了阻塞的方法,则线程会被挂起,会让出 CPU 时间片,此时是无法处理过来的请求,需要等待其他线程来进行唤醒,该线程才能进行后续操作或者处理其他请求。
非阻塞:
意味着,当数据还没准备好的时候,即便我调用了阻塞方法,该线程也不会被挂起,后续的请求也能够被处理。
同步
同步和异步跟串行和并行非常形似。
假设在一个场景下:完成一个大任务需要 4 个小任务。
同步的做法:需要依次 4 个步骤,注意这里是依次,也就是说完成这个步骤,需要先完成前置步骤,也就是说下一个步骤是要看上一个步骤的执行结果。
异步的做法:可以同时进行 4 个步骤,无需等待其他步骤的执行结果。
阻塞和同步的最本质差别在于:
即便是同步,在等待的过程中,线程是不会被挂起,也不需要让出 CPU 时间片的,
在 IO 中的体现
网络编程的基本模型是:Client/Server 模型
两个进程之间要相互通信,其中服务端需要提供位置信息,让客户端找到自己。服务端提供 IP 地址和监听的端口。
客户端拿着这些信息去向服务端发起建立连接请求,通过三次握手成功建立连接后,客户端就可以通过socket
向服务器发送和接受消息。
BIO
BIO 通信模型采用的是典型的:一请求一应答通信模型
采用 BIO 通信模型的服务端,通常会由一个独立的Acceptor
线程负责监听客户端的连接。
他不负责处理请求,他只是起到一个委派工作的作用,当他接收到请求之后,会为每个客户端创建一个新的线程进行链路处理。
处理完之后,通过输出流,返回应答给客户端,然后线程被销毁,资源被回收。
该模型的最大问题就是缺乏弹性伸缩能力,服务端的线程个数和客户端的并发访问数是**1:1
**的关系。
由于线程是 Java 虚拟机非常宝贵的资源,当线程书膨胀之后,系统的性能会随着并发量增加呈正比的趋势下降。
而且会有OOM
的风险,当没有内存空间创建线程时,就无法处理客户端请求,最终导致进程宕机或卡死,无法对外提供服务。
最大的问题就是:每当有一个客户端请求接入时,就会创建一个线程来处理请求。
为了改进这个一线程一连接模型,后面又演进出通过:
线程池
消息队列
来实现 1 个或者多个线程处理 N 个客户端的模型。
在这里,无论是线程池和消息队列,都是解决内存空间,线程的问题,并没有实质性地改变同步阻塞通信本质问题
所以这种优化版本的 BIO 也被称为是伪异步。
伪异步 IO
采用线程池和任务队列可以实现一种:伪异步的 IO 通信
将客户端的请求封装成一个Task
(该任务实现 java.lang.Runnable 接口),投递到消息队列中。
如果通过线程池维护一堆处理线程,去消费队列中的消息。
处理完毕之后,再去通过客户端就可以了,他的资源是可控的,无论客户端的请求量是多少,也不会发生变化,同样这也是他的缺点之一。
建立连接的accpet
方法、读取数据的read
方法都是阻塞。
这就意味着,如果有一方处理请求或者发出请求的比较慢,或者是网络传输比较慢,那么都会影响对方。
当调用 OutputStream 的write
方法写输出流的时候,它将会被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。
在 TCP/IP 中,当消息的接收方处理缓慢的时候,由于消息滑动窗口的存在,那么它的接收窗口就会变小,就是那个TCP window size
。
如果这里采用同步阻塞 IO,并且write
操作被阻塞很久,直到TCP window size
大于 0 或者发生 IO 异常了。
那么通信对方返回应答时间过长会引起的级联故障:
线程问题:假如所有的可用线程都被故障服务器阻塞,那么后续所有的 IO 消息都将被队列中排队。
队列问题:如果队列采用的是
有界队列
,队列满了之后那么就会无法后续处理请求;如果采用的是无界队列
,那么会有 OOM 风险。
NIO
NIO,官方叫法是
new IO
,因为它相对于之前出的 java.io 包是新增的但是之前老的 IO 库都是阻塞的,New IO 类库目标就是为了让 Java 支持非阻塞 IO,所有更多的人称为
Non-Block IO
缓冲区 Buffer
Buffer 是一个对象,通常是 ByteBuffer 类型
任何时候操作 NIO 中的数据,都需要经过缓冲区。
在NIO
库里,所有数据操作是用缓冲区处理的。
读取数据时,是直接读到缓冲区中(这里并没有直接读到某个地方,而是都放到缓冲区中)
写入数据时,写入到缓冲区
缓冲区实质上是一个数组,通常是一个字节数组ByteBuffer
,自身还需要维护读写位置,可以用指针或者偏移量来实现。
除了 ByteBuffer 还有其他基本类型缓冲区:
CharBuffer
:字符缓冲区ShortBuffer
:短整型缓冲区IntBuffer
:整形缓冲区LongBuffer
:长整型缓冲区DoubleBuffer
:双精度缓冲区
通常是用 ByteBuffer
通道 Channel
网络数据通过 Channel 读取和写入
Channel 通道和 Stream 流最大的区别在于:
Channel
的数据流向是双向的Stream
的数据流向是单向的
这就意味着:使用 Channel,可以同时进行读和写,他是全双工模型。(可以联想到HTTP1.1
HTTP2.0
HTTP3.0 ``websocket
)
多路复用器 Selector
Selector 是 NIO 编程的基础
Selector
会不断轮询注册在其上的Channel
。
如果某个 Channel 发生读写事件,就代表这个 Channel 是就绪状态,会被 Selector 轮询出来。
然后根据SelectionKey
可以获取就绪 Channel 的集合,进行后续 IO 操作。
一个 Selector 可以轮询多个 Channel,JDK 是基于 epoll 代替传统的 select,所以不受句柄 fd 的限制。
意味着,一个线程负责 Selector 的轮询千万个客户端,
AIO
NIO2.0
引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现
通过 java.util.concurrent.
Future
类来表示异步操作的结果。在执行异步操作的时候传入一个 java.nio.channels
CompletionHandler 接口的实现类作为操作完成的回调
NIO2.0
的异步 socket 通道是真正的异步非阻塞 IO。
同步 socket channel:
SocketServerChannel
异步 socket channel:
AsynchronousServerSocketChannel
它不需要通过多路复用器(selector
)对注册到里面的通过进行轮询操作,就可以实现异步读写。
AIO 和 NIO 最大的区别在于:异步 Socket Channel 是被动执行对象
NIO 需要我们把 channel 注册到 selector 上进行顺序扫描、轮询
AIO 则是通过
Future
类,实现回调方法:completed、failed
4 种 IO 对比
IO 模型主要是探讨 2 个维度:
同步/异步
阻塞/非阻塞
同步/异步的判断标准主要是:Channel
的问题
阻塞/非阻塞的判断标准主要是:selector
的问题
阻塞的关键点在于:建立连接和数据传输
BIO(阻塞)意味着在完成建立连接(accpet
)动作之后,才能进行后续操作
NIO(非阻塞)在处理客户端的连接时,可以将对应的 channel 注册到 Selector 上,此时我不管他好了没有,我有Selecotr
来帮我去扫就绪态的 channel,所以他是非阻塞的
异步非阻塞 IO
异步非阻塞 IO:
AIO
有的人也叫JDK1.4
推出的 NIO 为异步非阻塞 IO
但是严格来说,它只能被称为是非阻塞 IO,并不是真正意义上的异步
前期selector
的底层是通过 select/poll 来实现的,虽然是用 epoll 替代了 select/poll,上层的 API 没有变化,只是一次 NIO 的性能优化,仍旧没有改变 IO 的模型
在JDK1.7
提供的NIO2.0
新增了:异步套接字通道,他才是真正的异步 IO。
多路复用器 Selector
Selector 的核心功能:就是用来轮询注册在它上面的 Channel
当发现某个就绪态的 Channel,就会找出他的SelectionKey
,然后进行后续的 IO 操作。
前期的时候 JDK1.4,selector 底层是基于 select/poll 技术实现
后面优化,使用 epoll 来代替
伪异步 IO
只是在线程层面上进行了一次优化,IO 模型并没有改变
通过处理任务 Task 队列+线程池处理请求的方式来优化资源
解决了 BIO 的线程和请求:1 对 1 的关系
评论