由BIO到NIO:NIO解决了BIO的哪些问题

背景:网络通信三个socket的阻塞

在这里插入图片描述

server端服务器:
第一个socket:
ServerSocket serverSocket = new ……
第二个socket(阻塞1):
Socket socket = serverSocket.accept()
阻塞2:
socket.read()
在这里插入图片描述

BIO常用的解决方案是:在accept之后另起一个线程处理,这就会遇到C10K内存爆掉的问题:如果发起10K链接,且大多数链接是不活跃的,就会大量占用系统资源。
稍微优化版本的是:使用线程池进行管理,但是内存池一般是由线程个数限制,比如500个,那么就意味着只能起500个线程。且这500个线程也会面临非活跃线程占用的问题,即对方只是连接但是不发送数据,此时会造成线程池无线程可用。

client端:
Socket socket = new Socket()
在这里插入图片描述

NIO核心设计思路:解决2个阻塞问题

首先,启用了一个list来管理已经连接的socket,通过调用Linux底层的epoll函数来对这些socket进行管理。
解决连接时的阻塞:一直等待连接过程中导致该线程无法处理已经连接的socket发送的数据。
解决读取数据时的阻塞:读取某个已经连接的socket发送的数据导致该线程无法处理连接。
在这里插入图片描述
直接在BIO的基础上写NIO实现代码,思路如上图:

  1. list管理各个socket
  2. 连接时设置为非阻塞
  3. 读取时设置为非阻塞

这就是NIO相对于BIO的改良核心
socket在NIO中为channel,上述的list为List< channel>即为selector。还提供了buffer用来解决BIO中没有的缓存读写功能。

实际上当前redis一个单线程能够处理多线程并发的机制设计思路也是上述。

性能优化

继续深入:
大量启用线程造成的问题:存在大量不活跃的线程占用系统资源;大量不活跃的线程造成管理时的时间空间浪费,需要轮询大量不活跃线程。
socket大量的连接中即存在上述类似的问题,大量连接可能根本就没有起到作用。Java自己管理这些连接效率非常低下。

不使用Java语言进行处理而使用内核态的线程进行处理的原因如下:由网络通信模型决定。
下图中数据发送流程:
某应用程序(浏览器) -》操作系统 -》 网卡 -》 (通过协议)-》服务器的网卡 -》 操作系统轮询网卡得知有数据到来 -》 Java应用程序通过轮询获取系统状态,得知数据ready后,读取内核cache中的数据,并解阻塞(如果用BIO的话,应用程序read的时候,放弃了CPU,此时应用程序一直调用操作系统的函数轮序操作系统缓存中是否已经拿到了所需要的数据)
java是无法直接获取到网卡数据的,只有通过操作系统才能从硬件上获取数据,Java只是调用了操作系统的函数去get数据。
一个优化点:省去Java轮询操作系统这件事情,直接让操作系统去处理。即将上述核心代码中的for循环给省去了,在NIO中直接变成了native方法,交给操作系统函数去做轮询并查找到以及通知对应的socket去做后续操作。
NIO的read、accept方法等都交给操作系统去进行处理不再由程序员写Java代码处理,通过内核级别的线程去做进行提速。
在Linux系统中,调用的内核函数是epoll函数。
再延伸,redis的底层实现是用C写的,IO模型用的也是多路复用IO,底层使用Linux的epoll函数。redis没有win版本的,原因即在于没有对win版本做匹配,win版本又没有epoll,因此没有这个版本。
在这里插入图片描述

总结

NIO核心是为了解决2个阻塞问题
NIO为了性能的优化底层使用内核函数(Linux系统为epoll,win下为select)对连接进行管理,避免处理部分ready线程时要将所有线程进行遍历与处理。

猜你喜欢

转载自blog.csdn.net/weixin_38370441/article/details/113888155