IO多路复用学习随笔

Java层面的IO 分为BIO,NIO和AIO.

BIO: 中的B就是blocking,阻塞的意思.顾名思义就是 同步并阻塞的IO操作.
他的缺点就是: 一般我们开发的就是作为服务端,如果作为服务端的话,我们使用ServerSocket绑定完端口号之后,我们就会监听这个端口,然后等待accept事件,当我们接收到accept之后,就会阻塞当前主线程,程序会那倒也一个客户端与当前服务端连接的socket,针对这个socket,我们进行读写操作,但是socket的读写操作是会阻塞当前线程的.这也就是BIO最大的缺点,一般来说,我们在为C/S服务端, 交互都是多线程的,如果使用BIO,我们就很难做到C10K(单机10K个并发连接问题).也就是说有10k个服务端我们就需要10k个线程去支持连接.如果真正达到了10k个线程的话,光线程的上下文切换就会直接把机器的负载拉飞了.

然后就有了 NIO , NIO 就是同步不阻塞的IO .他是如何解决多客户端问题的呢?
在Java层面,为了实现NIO,Java给我们提供了一套完整的接口,这样我们就不用再去为每一个C/S的长连接去维持一个独立的线程了.BIO之所以会保留独立的线程就是因为他是阻塞的.所以Java NIO API 它就具备了非阻塞的特性. 就可以使用一个线程去监听N个socket.Java的NIO API为我们提供了一个selector,我们需要把我们需要检查的socket注册到这个selector中,然后主线程阻塞在selector的select方法中,当selector发现某一个socket处于就绪状态,那么就会唤醒主线程去获取到这个socket,然后执行相关操作.

这里我们也不难发现,NIO其实也有部分是阻塞的,那么AIO是什么呢? AIO 就是异步不阻塞的IO,是JDK7 引入的.
一般来说,异步编程都是基于回调的,回调函数简单来说就是预定义的方法,在发生特定情况执行特定的方法.在java编程中典型的类就是xxxListener的模式.
在Java的AIO中,对于Java进程来说,它需要的IO操作就是注册完成后的逻辑处理和发出异步请求,具体的事情由操作系统去完成,操作系统完成了操作后会通知给Java进程,进而执行预定义的回调函数。
AIO和同步IO(BIO和NIO)不同在于,IO操作全部委托给了操作系统,在BIO和NIO中,不管是使用阻塞流还是使用selector,用户进程下一步操作都是依赖操作系统的IO操作结果的,也就是需要同步的.而在AIO中,我们直接让操作系统去执行IO操作,然后我们继续其他操作,当IO操作完成后,操作系统会通知Java进程,然后Java进程再去执行回调,获取IO操作结果.

当然,在Java层面谈IO是没有多少意义的,因为所有的IO操作都是交给操作系统去完成的,都需要调用SystemCall 的kernel 来实现的.
在最老的版本当中,我们调用的事 kernel的select函数, 而Java每次调用这个函数,其实都涉及到 用户态和内核态的切换,而这个切换是耗费性能的.我们调用select函数,还需要传递 需要检查的socket集合,其实就是文件的描述符ID集合.调用函数之后,需要先去检查内存中的socket套接字状态,检查完一遍之后,如果有就绪状态的socket,那么就直接返回,不会阻塞进程,而如果没有就绪状态的socket,那么就会阻塞到某一socket就绪为止.然后又返回到Java层面,又是一次内核态和用户态的切换.
select函数最多是只能监视1024个socket,这是由于bitmap的长度是1024,而修改这个bitmap的操作是非常复杂且困难的.

为了解决这个问题,就出现了poll函数. poll函数和 select函数的区别就是 参数不同,poll使用的数组结构,这样就没有长度限制是1024的问题,就可以监听更多的线程.

当然 select和poll函数都有两个非常明显的缺点:
1.我们调用这两个函数都需要提供所有的需要检查的socket的文件描述符结合,而且主线程是死循环调用select/poll函数的, 这里面就涉及到了 用户空间数据到内核空间拷贝的过程,这就是比较耗费性能的…但是每次我们实际 只有 很少的socket会发生改变.这样就很影响效率
2.select/poll函数的返回值是int 整型,只能告诉我们有几个就绪或者错误,而不能告诉我们是哪个.这就导致了我们的程序被唤醒之后,再进行检查具体是哪一个socket处于就绪或者错误.这就涉及到了多次的内核态和用户态的切换,导致性能的损耗.

后面针对这两个缺点,又出现了epoll…

以上…后续学习会继续整理…

猜你喜欢

转载自blog.csdn.net/shiliu_baba/article/details/107775774