BIO NIO AIO
在学习Java I/O类库时,容易混淆NIO、BIO、AIO这几个概念,同时对于阻塞和非阻塞、同步和异步的理解也较为晦涩,这篇文章是对这几个概念的一些区分以及个人的一些见解。
BIO 是同步阻塞通讯。
NIO 是同步非阻塞通信。
AIO 是彻底的异步通信。
有一个经典的举例。烧开水。
假设有这么一个场景,有一排水壶(客户)在烧水。
AIO的做法是,每个水壶上装一个开关,当水开了以后会提醒对应的线程去处理。
NIO的做法是,叫一个线程不停的循环观察每一个水壶,根据每个水壶当前的状态去处理。
BIO的做法是,叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。
一、同步阻塞I/O(BIO):
Blocking(同步阻塞)I/O
- 服务器实现模式为一个连接一个线程。
- 即客户端有连接请求时服务器就需要启动一个线程进行处理。
- 如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。
- BIO是传统的Java io编程,其相关的类和接口在java.io 包下
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务端资源要求比较高,并发局限于应用中,在jdk1.4以前是唯一的IO实现。
- 程序直观简单易理解。
BIO编程流程
- 服务器端启动一个SeverSocket
- 客户端启动Socket对服务器端发起通信,默认情况下服务器端需为每个客户端创建一个线程与之通讯
- 客户端发起请求后,先咨询服务器端是否有线程响应,如果没有则会等待或被拒绝
- 如果有线程响应,客户端线程会等待请求结束后,再继续执行
二、同步非阻塞I/O(NIO):
Non-blocking I/O,在Java领域,也称为New I/O
-
服务器实现模式为一个请求一个线程。
-
客户端发送的连接请求都会注册到多路复用器 (Selector选择器)上,多路复用器(Selector选择器)轮询到连接有IO请求时才启动一个线程进行处理。
-
NIO的相关类都放在java.nio包或其子包下,并对原先java.io包中许多类进行了改写。
-
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中。
-
编程比较复杂,jdk1,4开始支持。
NIO三大核心
缓冲区(Buffer),通道(Channel),选择器(Selector)
缓冲区(Buffer)
-
NIO是面向缓冲区, 或者说是面向块编程的。在NIO的IO传输中,数据会先读入到缓冲区,当需要时再从缓冲区写出,这样减少了直接读写磁盘的次数,提高了IO传输的效率。
-
缓冲区(buffer)本质上是一个可以读写数据的内存块,即在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入和输出的数据,这部分预留的存储空间就叫缓冲区。
-
在NIO程序中,通道channel虽然负责数据的传输,但是输入和输出的数据都必须经过缓冲区buffer。
-
在java中,缓冲区的相关类都在java.nio包下,其最顶层的类是 Buffer,它是一个抽象类。
Buffer类的4个重要属性:
- mark:标记
- position:位置,下一个要被读或写的元素的索引,每次读写缓冲区都会改变该值,为下次读写做准备
- limit:表示缓冲区的终点,不能对缓冲区中超过极限的位置进行读写操作,且极限是可修改的
- capacity:容量,即缓冲区的最多可容纳的数据量,该值在创建缓冲区时被设立,且不可修改
通道(Channel)
在NIO程序中服务器端和客户端之间的数据读写不是通过流,而是通过通道来读写的。
通道类似于流,都是用来读写数据的,但它们之间也是有区别的:
-
通道是双向的,即可以读也可以写,而流是单向的,只能读或写
-
通道可以实现异步读写数据
-
通道可以从缓冲区读数据,也可以把数据写入缓冲区
-
java中channel的相关类在java.nio.channel包下。Channel是一个接口。
-
常用的实现类如下:
-
FileChannel:用于文件的数据读写,其真正的实现类为FileChannelImpl
-
DatagramChannel:用于UDP的数据读写,其真正的实现类为DatagramChannelImpl
-
ServerSocketChannel:用于监听TCP连接,每当有客户端连接时都会创建一个SocketChannel,功能类似ServerSocket,其真正的实现类为ServerSocketChannelImpl
-
SocketChannel:用于TCP的数据读写,功能类似节点流+Socket,其真正的实现类为SocketChannelImpl
-
选择器(Selector)
-
在NIO程序中,可以用选择器Selector实现一个选择器处理多个通道,即一个线程处理多个连接。
-
只要把通道注册到Selector上,就可以通过Selector来监测通道,如果通道有事件发生,便获取事件通道然后针对每个事件进行相应的处理。这样,只有在通道(连接)有真正的读/写事件发生时,才会进行读写操作,大大减少了系统开销,并且不必为每个连接创建单独线程,就不用去维护过多的线程。
-
选择器的相关类在java.nio.channels包和其子包下,顶层类是zSelector,它是一个抽象类。
每个注册到选择器的通道都需定义需进行的操作事件类型,通过查看SelectionKey类的属性可以知道操作事件的类型有4种:
public static final int OP_READ = 1 << 0; //读操作
public static final int OP_WRITE = 1 << 2; //写操作
public static final int OP_CONNECT = 1 << 3; //连接操作
public static final int OP_ACCEPT = 1 << 4; //接收操作
选择器的检查
我们可以通过选择器的检查方法,如select()来得知发生事件的通道数量,当该数量大于为0时,即至少有一个通道发生了事件,就可以使用selectedKeys()方法来获取所有发生事件的通道对应的SelectionKey,通过SelectionKey中的方法来判断对应通道中需处理的事件类型是什么,在根据事件做出相应的处理。
public final boolean isReadable() { //判断是否是读操作
return (readyOps() & OP_READ) != 0;
}
public final boolean isWritable() { //判断是否是写操作
return (readyOps() & OP_WRITE) != 0;
}
public final boolean isConnectable() { //判断是否是连接操作
return (readyOps() & OP_CONNECT) != 0;
}
public final boolean isAcceptable() { //判断是否是接收操作
return (readyOps() & OP_ACCEPT) != 0;
}
三、异步非阻塞I/O(AIO):
Asynchronization I/O 异步非阻塞I/O
一般来说,服务器端的I/O主要有两种情况:一是来自网络的I/O;二是对文件(设备)的I/O。
Windows的异步I/O模型能很好的适用于这两种情况。
Linux针对前者提供了epoll模型,针对后者提供了AIO模型(关于是否把两者统一起来争论了很久)。
-
Linux Native AIO 服务器实现模式为一个有效请求一个线程,
-
AIO是真正的异步IO操作:允许进程发起很多I/O操作,而不用阻塞或等待任何操作完成。
-
AIO的基本思想:
- 允许进程发起很多I/O操作,而不用阻塞或等待任何操作完成,稍后或在接收到I/O操作完成通知时,进程可以检索I/O操作结果
- 在异步非阻塞I/O中,我们可以同时发起多个传输操作,这需要每个传输操作都有唯一的上下文,这样我们才能在他们完成时区分到底是哪个传输操作完成了,这个工作可以通过aiocb结构体进行区分。
客户端的IO请求都是由操作系统先完成了再通知服务器用其启动线程进行处理。
由于操作系统的Read和Write操作是异步的,所以AIO操作是真正异步的。
AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作。
编程比较复杂,jdk1.7开始支持。
四、IO与NIO区别:
IO面向流,NIO面向缓冲区
IO的各种流是阻塞的,NIO是非阻塞模式
Java NIO的选择允许一个单独的线程来监视多个输入通道,可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入或选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道
五、同步与异步的区别:
同步:发送一个请求,等待返回,再发送下一个请求,同步可以避免出现死锁,脏读的发生。
异步:发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发。
同步异步关注点在于消息通信机制,
阻塞与非阻塞关注的是程序在等待调用结果时(消息、返回值)的状态:
-
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
-
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
不同层次:
CPU层次:操作系统进行IO或任务调度层次,现代操作系统通常使用异步非阻塞方式进行IO(有少部分IO可能会使用同步非阻塞),即发出IO请求后,并不等待IO操作完成,而是继续执行接下来的指令(非阻塞),IO操作和CPU指令互不干扰(异步),最后通过中断的方式通知IO操作的完成结果。
线程层次:操作系统调度单元的层次,操作系统为了减轻程序员的思考负担,将底层的异步非阻塞的IO方式进行封装,把相关系统调用(如read和write)以同步的方式展现出来,然而同步阻塞IO会使线程挂起,同步非阻塞IO会消耗CPU资源在轮询上,3个解决方法;
多线程(同步阻塞)
IO多路复用(select、poll、epoll)
直接暴露出异步的IO接口,kernel-aio和IOCP(异步非阻塞)
Linux IO模型:
阻塞/非阻塞:等待I/O完成的方式,阻塞要求用户程序停止执行,直到IO完成,而非阻塞在IO完成之前还可以继续执行。
同步/异步:获知IO完成的方式,同步需要时刻关心IO是否完成,异步无需主动关心,在IO完成时它会收到通知。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!