IO:阻塞和非阻塞、同步和异步

简介: 当数据还没准备好时,调用了阻塞的方法,则线程会被挂起,会让出CPU时间片,此时是无法处理过来的请求,需要等待其他线程来进行唤醒,该线程才能进行后续操作或者处理其他请求。

阻塞和非阻塞

阻塞的时候线程会被挂起

阻塞:
当数据还没准备好时,调用了阻塞的方法,则线程会被挂起,会让出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队列+线程池处理请求的方式来优化资源

相关文章
|
1月前
|
并行计算 数据处理 Python
Python并发编程迷雾:IO密集型为何偏爱异步?CPU密集型又该如何应对?
在Python的并发编程世界中,没有万能的解决方案,只有最适合特定场景的方法。希望本文能够为你拨开迷雾,找到那条通往高效并发编程的光明大道。
38 2
|
2月前
|
开发框架 并行计算 算法
揭秘Python并发神器:IO密集型与CPU密集型任务的异步革命,你竟还傻傻分不清?
揭秘Python并发神器:IO密集型与CPU密集型任务的异步革命,你竟还傻傻分不清?
39 4
|
27天前
|
存储 缓存 算法
如何优化阻塞IO的性能?
【10月更文挑战第6天】如何优化阻塞IO的性能?
31 5
|
28天前
|
数据库
同步IO模型是一种常见的编程模型
【10月更文挑战第5天】同步IO模型是一种常见的编程模型
17 2
|
2月前
|
算法 Java 程序员
解锁Python高效之道:并发与异步在IO与CPU密集型任务中的精准打击策略!
在数据驱动时代,高效处理大规模数据和高并发请求至关重要。Python凭借其优雅的语法和强大的库支持,成为开发者首选。本文将介绍Python中的并发与异步编程,涵盖并发与异步的基本概念、IO密集型任务的并发策略、CPU密集型任务的并发策略以及异步IO的应用。通过具体示例,展示如何使用`concurrent.futures`、`asyncio`和`multiprocessing`等库提升程序性能,帮助开发者构建高效、可扩展的应用程序。
76 0
|
3月前
|
C# 开发者 设计模式
WPF开发者必读:命令模式应用秘籍,轻松简化UI与业务逻辑交互,让你的代码更上一层楼!
【8月更文挑战第31天】在WPF应用开发中,命令模式是简化UI与业务逻辑交互的关键技术,通过将请求封装为对象,实现UI操作与业务逻辑分离,便于代码维护与扩展。本文介绍命令模式的概念及实现方法,包括使用`ICommand`接口、`RelayCommand`类及自定义命令等方式,并提供示例代码展示如何在项目中应用命令模式。
46 0
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
4月前
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
2月前
|
Java 大数据 API
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
|
3月前
|
Java 数据处理
Java IO 接口(Input)究竟隐藏着怎样的神秘用法?快来一探究竟,解锁高效编程新境界!
【8月更文挑战第22天】Java的输入输出(IO)操作至关重要,它支持从多种来源读取数据,如文件、网络等。常用输入流包括`FileInputStream`,适用于按字节读取文件;结合`BufferedInputStream`可提升读取效率。此外,通过`Socket`和相关输入流,还能实现网络数据读取。合理选用这些流能有效支持程序的数据处理需求。
42 2