NIO与IO多线程相比,为什么使用NIO?
多线程socket IO
传统的socket IO中,server一直监听服务端口,当一个请求连接发送到server端时,server端获取当前的socket,然后为socket分配一个线程(通过new方式或者线程池获取一个可用的线程),让该线程独占这个socket,执行相关任务、直到任务执行完成后,释放socket和任务线程,相当于一个线程为一个socket服务,直到这个socket生命周期结束;当并发的连接数量非常巨大且是长连接(socket长连接,不释放)时,由于socket长时间占用线程,线程池无法有效回收,也会保留大量任务线程,导致所占用的栈内存和CPU线程切换的开销将非常巨大。
NIO reactor模型(任务线程池)
使用NIO reactor模型时,server也需要一个独立的线程监听服务端口,当一个请求连接发送到server端时,server端也会获取到当前连接的socket;但server不会立即将其分配一个任务线程,而是用一个channel(可以理解为内存中的一个对象)和socket绑定,并注册感兴趣的事件标签(读、写等),由底层硬件监听相关事件消息(具体实现原理不在此详细说明);例如当该socket对应的读缓冲区有数据时,server就会收到可读事件(之前channel注册事件标签),此时server会为socket分配一个任务线程(通过new方式或者线程池获取一个可用的线程),任务线程能够直接获取到缓存区的数据并根据业务进行处理,当任务执行完成(socket缓存区的数据都处理完毕,当前无可读数据),任务线程被线程池回收,若当前socket还未断开(长连接,一个socket连接可能会下发多个任务,比如一个连接串行下发多个http请求,任务中间有时间间隔),可通过channel重新注册相关事件标签。通过上述描述可知,NIO采用IO多路复用方式,不再需要为每个socket连接创建单独的线程,可以用一个含有限数量线程的线程池,甚至一个线程来为任意数量的连接服务,当任务执行完成可及时回收,用于下一个已就绪的socket连接。达到线程数量小于连接数量的效果,有效解决了传统多线程IO的问题;在此说明一下,若socket的缓存区只包含一部分数据(还有数据在传输中,比如一个http请求中body数据过大,分多个tcp包传输),此时任务线程只能处理当前数据,然后阻塞等待剩余数据(通过wait方式不会占用cpu时间),直到任务完整执行完成。
下面用一个例子,形象说明一下上述流程(例子转载)
小量的线程如何同时为大量连接服务呢,答案就是就绪选择。这就好比到餐厅吃饭,每来一桌客人,都有一个服务员专门为你服务,从你到餐厅到结帐走人,这样方式的好处是服务质量好,一对一的服务VIP,可是缺点也很明显,成本高,如果餐厅生意好,同时来100桌客人,就需要100个服务员,那老板发工资的时候得心痛死了,这就是传统的一个连接一个线程的方式。
老板很聪明。这老板就开始捉摸怎么能用10个服务员同时为100桌客人服务呢,老板就发现,服务员在为客人服务的过程中并不是一直都忙着,客人点完菜,上完菜,吃着的这段时间,服务员就闲下来了,可是这个服务员还是被这桌客人占用着,不能为别的客人服务,就是工作不饱满。那怎么把这段闲着的时间利用起来呢。这餐厅老板就想了一个办法,让一个服务员(前台)专门负责收集客人的需求,登记下来(空间换时间原理),比如有客人进来了、客人点菜了,客人要结帐了,都先记录下来按顺序排好。每个服务员到这里领一个需求,比如点菜,就拿着菜单帮客人点菜去了。点好菜以后,服务员马上回来,领取下一个需求,继续为别人客人服务去了。这种方式服务质量就不如一对一的服务了,当客人数据很多的时候可能需要等待。但好处也很明显,由于在客人正吃饭着的时候服务员不用闲着了,服务员这个时间内可以为其他客人服务了,原来10个服务员最多同时为10桌客人服务,现在可能为50桌,60客人服务了。
这种服务方式跟传统的区别有两个:
1、增加了一个角色,要有一个专门负责收集客人需求的人。NIO里对应的就是Selector。
2、由阻塞服务方式改为非阻塞服务了,客人吃着的时候服务员不用一直侯在客人旁边了。传统的IO操作,比如read(),当没有数据可读的时候,线程一直阻塞被占用,直到数据到来。NIO中没有数据可读时,read()会立即返回0,线程不会阻塞。
NIO中,客户端创建一个连接后,先要将连接注册到Selector,相当于客人进入餐厅后,告诉前台你要用餐,前台会告诉你你的桌号是几号,然后你就可能到那张桌子坐下了,SelectionKey就是桌号。当某一桌需要服务时,前台就记录哪一桌需要什么服务,比如1号桌要点菜,2号桌要结帐,服务员从前台取一条记录,根据记录提供服务,完了再来取下一条。这样服务的时间就被最有效的利用起来了。
tips
通过上面的举例,是不是感觉NIO完胜阻塞IO呢?其实上面的例子也说明了,传统阻塞IO属于VIP服务,如果客户比较重要(非常重要、付费的服务连接),可使用传统阻塞IO独占线程,为客户提供优质服务。
nio机制不适用于磁盘io,因为磁盘文件总是可读的,而 IOCP(aio) 就是一步到位,直接送货上门,连下楼取的动作都不需要(相当于直接把数据写入到用户空间,通知用户直接使用)。整个过程完全是非阻塞的。
CPU密集型
一个计算为主的程序(专业一点称为CPU密集型程序)。多线程跑的时候,可以充分利用起所有的cpu核心,比如说4个核心的cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。
但是如果线程远远超出cpu核心数量 反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。
因此对于cpu密集型的任务来说,线程数等于cpu数是最好的了。
IO密集型
如果是一个磁盘或网络为主的程序(IO密集型)。一个线程处在IO等待的时候,另一个线程还可以在CPU里面跑,有时候CPU闲着没事干,所有的线程都在等着IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道IO的速度比起CPU来是慢到令人发指的。所以开多线程,比方说多线程网络传输,多线程往不同的目录写文件,等等。此时 线程数等于IO任务数是最佳的。但是考虑另一方面,磁盘是系统性能的瓶颈之一。如果系统硬盘读写速度比较慢,线程数量不宜过多,否则会造成系统瓶颈更加明显。
不同的任务对线程数量的需求也不同。例如,如果任务是大量的磁盘读取操作(读取大文件),线程数量过多反而会增加磁盘的寻址时间和带宽负担,影响磁盘读取效率。如果任务是网络请求,则线程数量可以适当增加,但也不能无限制增加。
总之,IO密集型任务线程数量的设置应该根据实际情况进行调整,一般来说,线程数量应该不少于CPU核心数量,但也不宜过多。通过实验和性能测试,找到一个合适的线程数量,可以最大限度地发挥系统的性能。
参考链接
Java NIO系列教程(一) Java NIO 概述 ——https://www.cnblogs.com/duanxz/p/6759689.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本