虽然作为Java开发程序员,很多都听过IO、NIO这些,但是很多人都没深入去了解这些内容。
同步并阻塞模式,调用方在发起IO操作时会被阻塞,直到操作完成才能继续执行,适用于连接数较少的场景。
例如:服务端通过ServerSocket监听端口,accept()阻塞等待客户端连接。
优缺点:
适用于低并发、短连接的场景,如传统的HTTP服务

同步非阻塞模型,客户端发送的连接请求都会注册到Selector多路复用器上,服务器端通过Selector管理多个通道Channel,Selector会轮询这些连接,当轮询到连接上有IO活动就进行处理。
NIO基于 Channel 和 Buffer 进行操作,数据总是从通道读取到缓冲区或者从缓冲区写入到通道。Selector 用于监听多个通道上的事件(比如收到连接请求、数据达到等等),因此使用单个线程就可以监听多个客户端通道。
IO多路复用:一个线程可对应多个连接,不用为每个连接都创建一个线程

核心组件:
优缺点:
使用于高并发、长连接的场景,如即时通讯场景
异步非阻塞模型,基于事件回调或Future机制
AsynchronousSocketChannel是AIO的代表类,通过回调函数处理读写操作完成后的结果优缺点:
适用于高吞吐、低延迟的场景,如日志批量写入
说起Java的IO模型,绕不开的就是Netty框架了,那什么是Netty,为什么Netty的性能这么高呢?
很多中间件的底层通信框架用的都是它,比如:RocketMQ、Dubbo、Elasticsearch
核心特点:
高性能的核心原因:
Netty的零拷贝体现在操作数据时, 不需要将数据 buffer从 一个内存区域拷贝到另一个内存区域。少了一次内存的拷贝,CPU 效率就得到的提升。

Netty 的 Zero-copy 完全是在用户态(Java 应用层)的, 更多的偏向于优化数据操作。而在 OS 层面上的 Zero-copy 通常指避免在用户态(User-space)与内核态(Kernel-space)之间来回拷贝数据

Reactor可以分为单Reactor单线程模式、单Reactor多线程模型,主从Reactor多线程模型

该模式简单,所有操作都由1个IO线程处理,缺点是存在性能瓶颈,只有1个线程工作,无法发挥多核CPU的性能。

充分发挥了多核CPU的处理能力,缺点是用一个线程接收事件和响应,高并发时仍然会有性能瓶颈

该模式优点是主从线程分工明确,能应对更高的并发。缺点是编程复杂度较高。
应用该模式的中间件有:Dubbo、RocketMQ、Zookeeper等
Reactor模式的核心在于用一个或少量线程来监听多个连接上的事件,根据事件类型分发调用相应处理逻辑,从而避免为每个连接都分配一个线程

简单理解:Boss线程是老板,Worker线程是员工,老板负责接收处理的事件请求,Worker负责工作,处理请求的I/O事件,并交给对应的Handler处理
本质是将线程连接和具体的业务处理分开
这三种都是操作系统中的多路复用I/O机制
轮询机制:select使用一个固定大小的位图来表示文件描述符集,将文件描述符的状态(如可读、可写)存储在一个数组中,调用select时,每次需将完整的位图从用户空间拷贝到内核空间,内核遍历所有描述符,检查就绪状态
局限:
poll使用了动态数组来替代位图,使用pollfd结构数组存储文件描述符和事件,无数量限制
工作机制:每次调用时仍然需要遍历所有描述符,即使只有少量描述符修改了,仍然要检查整个数组,时间复杂度为O(N)
1)事件驱动模型:epoll使用红黑树来存储和管理注册的文件描述符,使用就绪事件链表来存储触发的事件。当某个文件描述符上的事件就绪时,epoll会将该文件描述符添加到就绪链表中。
2)触发模式:支持水平触发(LT)和边缘触发(ET),ET模式下事件仅通知一次
3)工作流程:
epoll_create创建实例:分配相应数据结构,并返回一个epoll文件描述符。内核分配一棵红黑树管理文件描述符,以及一个就绪事件的链表epoll_ctl注册、修改、删除事件:epoll_ctl是用于管理文件描述符与事件关系的接口epoll_wait等待事件:epoll会检查就绪事件链表,将链表中所有就绪的文件描述符返回给用户空间。epoll_wait高效体现在它返回的是已经发生事件的文件描述符,而不是遍历所有注册的文件描述符优点是时间复杂度O(1),仅处理活跃连接,性能和连接数无关
4)零拷贝机制:
总结:epoll每次只传递发生的事件,不需要传递所有文件描述符,所以提高了效率
Java NIO在Linux系统下默认是epoll机制,理论上无客户端连接时Selector.select()方法是会阻塞的。
发生空轮询bug表现时,即时select轮询事件返回数量是0,Select.select()方法也不会被阻塞,NIO就会一直处于while死循环中,不断向CPU申请资源导致CPU 100%
底层原因:
Netty并没有解决这个bug,而是绕开了这个错误,具体如下:
1)统计空轮询次数:通过selectCnt计数器来统计连续空轮询的次数,每次执行Selector.select()方法后,如果发现没有IO事件,selectCnt就会递增
2)设置阈值:定义了一个阈值,默认为512,当空轮询达到这个阈值时,Netty就会触发重建Selector的操作
3)重建Selector:Netty新建一个Selector,并将所有注册的Channel从旧的Selector转移到新的Selector上,过程涉及取消旧Selector上的注册,以及新Selector上重新注册
4)关闭旧的Selector:重建Selector并将Channel重新注册后,Netty关闭旧的Selector
总结:通过SelectCnt统计没有IO事件的次数,来判断当前是否发生了空轮询,如果发生了,就重建一个Selector来替换之前出问题的Selector
核心代码如下:
long time = System.nanoTime();
//调用select方法,阻塞时间为上面算出的最近一个将要超时的定时任务时间
int selectedKeys = selector.select(timeoutMillis);
//计数器加1
++selectCnt;
if (selectedKeys != 0 || oldWakenUp || this.wakenUp.get() || this.hasTasks() || this.hasScheduledTasks()) {
//进入这个分支,表示正常场景
//selectedKeys != 0: selectedKeys个数不为0, 有io事件发生
//oldWakenUp:表示进来时,已经有其他地方对selector进行了唤醒操作
//wakenUp.get():也表示selector被唤醒
//hasTasks() || hasScheduledTasks():表示有任务或定时任务要执行
//发生以上几种情况任一种则直接返回
break;
}
//此处的逻辑就是: 当前时间 - 循环开始时间 >= 定时select的时间timeoutMillis,说明已经执行过一次阻塞select(), 有效的select
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
//进入这个分支,表示超时,属于正常的场景
//说明发生过一次阻塞式轮询, 并且超时
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
//进入这个分支,表示没有超时,同时 selectedKeys==0
//属于异常场景
//表示启用了select bug修复机制,
//即配置的io.netty.selectorAutoRebuildThreshold
//参数大于3,且上面select方法提前返回次数已经大于
//配置的阈值,则会触发selector重建
//进行selector重建
//重建完之后,尝试调用非阻塞版本select一次,并直接返回
selector = this.selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
currentTimeNanos = time;原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。