欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 培训 > 【JavaSE】IO模型

【JavaSE】IO模型

2024/10/25 4:21:53 来源:https://blog.csdn.net/qq_58023907/article/details/142440195  浏览:    关键词:【JavaSE】IO模型

        IO,英文全称是 Input/Output,翻译过来就是输入/输出。我们听得挺多,就是磁盘 IO,网络 IO 等。IO 即输入/输出,到底谁是输入?谁是输出?IO 如果脱离了主体,会让人疑惑。

计算机角度的 IO

        我们常说的输入输出,比较直观的意思就是计算机的输入输出,计算机就是主体。计算机分成分为 5 个部分:运算器、控制器、存储器、输入设备、输出设备。输入设备是向计算机输入数据和信息的设备,键盘,鼠标都属于输入设备;输出设备是计算机硬件系统的终端设备,用于接收计算机数据的输出显示,一般显示器、打印机属于输出设备。

操作系统角度的 IO

        我们要将内存中的数据写入到磁盘的话,那么主体就是一个程序。操作系统负责计算机的资源管理和进程的调度,我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写内存的读写等等。

        真正的 IO 是在操作系统执行的。即应用程序的 IO 操作分为两种动作:IO 调用IO 执行。IO 调用是由进程(应用程序的运行态)发起,而 IO 执行是操作系统内核的工作。

        应用程序发起的一次 IO 操作包含两个阶段:

        IO 调用:应用程序进程向操作系统内核发起调用。

        IO 执行:操作系统内核完成 IO 操作。

IO 模型

阻塞 IO

        假设应用程序的进程发起 IO 调用,但是如果内核的数据还没准备好的话,那应用程序进程就一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次 IO 操作,称之为阻塞 IO

        阻塞 IO 比较经典的应用就是阻塞 socketJava BIO。阻塞 IO 的缺点就是:如果内核数据一直没准备好,那用户进程将一直阻塞,浪费性能,可以使用非阻塞 IO 优化。

非阻塞 IO

        如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求,这就是非阻塞 IO。

        非阻塞 IO 的流程如下:

        1.应用进程向操作系统内核,发起 recvfrom( )读取数据。

        2.操作系统内核数据没有准备好,立即返回 EWOULDBLOCK 错误码。

        3. 应用程序进程轮询调用,继续向操作系统内核发起 recvfrom 读取数据。

        4.操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间。

        5.完成调用,返回成功提示。

        recvfrom() 用来接收远程主机经指定的 socket 传来的数据,并把数据传到由参数 buf 指向的内存空间。非阻塞 IO 模型,简称 NIO,Non-Blocking IO。它相对于阻塞 IO,虽然大幅提升了性能,但是它依然存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的 CPU 资源。

IO 多路复用

        既然 NIO 无效的轮询会导致 CPU 资源消耗,我们等到内核数据准备好了,主动通知应用进程再去进行系统调用。

        IO 复用模型核心思路:系统给我们提供一类函数(如 select、poll、epoll),它们可以同时监控多个 fd 的操作,任何一个返回内核数据就绪,应用进程再发起 recvfrom() 系统调用。

        文件描述符 fd(File Descriptor),它是计算机科学中的一个术语,形式上是一个非负整数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符,用来表示相关文件信息。

select

        应用进程通过调用 select 函数,可以同时监控多个 fd,在 select 函数监控的 fd中,只要有任何一个数据状态准备就绪了,select 函数就会返回可读状态,这时应用进程再发起 recvfrom( )请求去读取数据。select 的 IO 多路复用模型,只需要发起一次询问就够了,大大优化了性能。

        但是,select 有几个缺点:监听的 IO 最大连接数有限,在 Linux 系统上一般为 1024kb。select 函数返回后,是通过遍历 fdset,找到就绪的描述符 fd(仅知道有 I/O 事件发生,却不知是哪几个 FD,所以遍历所有 FD)。因为存在大量遍历,所以会有连接数限制,所以后来又提出了 poll。与 select 相比,poll 解决了连接数限制问题。但是,select 和 poll 一样,还是需要通过遍历 FD 来获取已经就绪的 socket。如果同时连接大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。因此经典的多路复用模型 epoll 诞生。

epoll

        为了解决 select/poll 存在的问题,多路复用模型 epoll 诞生,它采用事件驱动来实现。epoll 先通过 epoll_ctl() 来注册一个 fd,一旦基于某个 fd 就绪时,内核会采用回调机制,迅速激活这个 fd,当进程调用 epoll_wait()时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的机制。这就是 epoll的亮点。

select、poll、epoll 的区别

selectpollepoll
底层数据结构数组链表红黑树和双链表
获取就绪的 fd遍历遍历事件回调
事件复杂度O(n)O(n)O(1)
最大连接数1024无限制无限制
fd 数据拷贝每次调用 select,需要将 fd 数据从用户空间拷贝到内核空间每次调用 poll,需要将 fd 数据从用户空间拷贝到内核空间使用内存映射(mmap),不需要从用户空间频繁拷贝 fd 数据到内核空间

异步 IO

        前面讲的 BIO,NIO,在数据从内核复制到应用缓冲的时候,都是阻塞的,因此 AIO 实现了 IO 全流程的非阻塞,就是应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是表示类似提交成功的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程 IO 操作执行完毕。

        异步 IO 的优化思路很简单,只需要向内核发送一次请求,就可以完成数据状态询问和数据拷贝的所有操作,并且不用阻塞等待结果。日常开发中,有类似思想的业务场景:比如发起一笔批量转账,但是批量转账理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。

一个生活中经典例子:

BIO

小明去吃饭,就这样在那里排队,等了一小时,轮到她了,然后才开始吃饭。

package IO;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;public class BIODemo1 {public static void main(String[] args) throws IOException, InterruptedException {ServerSocket serverSocket = new ServerSocket(9999, 1024);System.out.println("服务器启动");while (true) {Socket socket = serverSocket.accept();}}}
package IO;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class BIODemo2 {public static void main(String[] args) throws IOException, InterruptedException {//创建线程池ThreadPoolExecutor executor = new ThreadPoolExecutor(50,50, 200,TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(20),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());ServerSocket serverSocket = new ServerSocket(9999, 1024);System.out.println("服务器启动");while (true) {Socket socket = serverSocket.accept();System.out.println(socket + "连接到服务器");executor.execute(() -> {//执行任务});}}}
package IO;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;/*** BIO模拟NIO*/
public class BIODemo3 {public static void main(String[] args) throws IOException, InterruptedException {List<Socket> list = new ArrayList<>();ServerSocket serverSocket = new ServerSocket(9999, 1024);System.out.println("服务器启动");while (true) {// 设置非阻塞 serverSocket.set不阻塞Socket socket = serverSocket.accept(); // 不管接到接不到继续向下执行// 判断有没有接到if (socket != null) { // 没有人连接到服务器// 虽然没有人连接,但是有可能有人发送消息,此时也需要处理.// 循环集合中的socket} else { // 有人连接到服务器System.out.println(socket + "连接到服务器");list.add(socket);// 循环集合中的socket}}}}

NIO

        小红也去吃饭,她一看要等挺久的,于是去逛会商场,每次逛一下,就跑回来看看,是不是轮到她了。于是最后她既购了物,又吃上饭了。

package IO;import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;public class NIODemo1 {public static void main(String[] args) throws IOException {FileInputStream in = new FileInputStream("E:/source.txt");FileOutputStream out = new FileOutputStream("E:/dest.txt");FileChannel inchannel = in.getChannel();FileChannel outchannel = out.getChannel();ByteBuffer byteBuffer = ByteBuffer.allocate(1024);while (inchannel.read(byteBuffer) != -1) {byteBuffer.flip();outchannel.write(byteBuffer);byteBuffer.clear();}}}
package IO;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;/*NIO完整写法*/
public class NIODemo2 {public static void main(String[] args) throws InterruptedException, IOException {List<SocketChannel> list = new ArrayList<>();ServerSocketChannel serverSogketChannel = ServerSocketChannel.open();//创建服务器serverSogketChannel.bind(new InetSocketAddress(9999), 1024);serverSogketChannel.configureBlocking(false);//设置非阻塞//注册选择器Selector selector = Selector.open();//创建选择器serverSogketChannel.register(selector, SelectionKey.OP_ACCEPT);//向selector注册管道System.out.println("启动服务器");for (; ; ) {selector.select();Set<SelectionKey> selectionKeys = selector.selectedKeys();//返回所有选择器接收到的操作Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey selectionKey = iterator.next();if (selectionKey.isAcceptable()) {//连接ServerSocketChannel serverSockChannel = (ServerSocketChannel) selectionKey.channel();SocketChannel acceptSocketChannel = serverSockChannel.accept();System.out.println(acceptSocketChannel.getRemoteAddress());acceptSocketChannel.configureBlocking(false);acceptSocketChannel.register(selector, SelectionKey.OP_WRITE);}if (selectionKey.isWritable()) {//写SocketChannel socketChannel = (SocketChannel) selectionKey.channel();String resp = "响应";try {Thread.sleep(500);socketChannel.write(ByteBuffer.wrap(resp.getBytes()));} catch (Exception e) {e.printStackTrace();}}if (selectionKey.isReadable()) {//读SocketChannel channel = (SocketChannel) selectionKey.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int length = channel.read(buffer);String msg = "server receive msg:" + new String(buffer.array(), 0, length);System.out.println(msg);}iterator.remove();}}}
}

AIO

        小华一样,去吃饭,由于他是高级会员,所以店长说,你去商场随便逛会吧,等下有位置,我立马打电话给你。于是小华不用干巴巴坐着等,也不用每过一会儿就跑回来看有没有等到,最后也吃上了饭。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com