Java中常见的 IO 方式

        冯·诺依曼结构中计算机结构被分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备,输入设备向计算机输入数据,输出设备接收计算机输出的数据。从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。 网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。

        所谓 IO 即 输入输出,是操作系统为了保证操作系统的稳定性和安全性,将一个进程的地址空间划分为 用户空间(User space  内核空间(Kernel space  。我们运行的应用程序都是运行在用户空间,而只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等。我们想要进行 IO 操作,一定是要依赖内核空间的能力。并且,用户空间的程序不能直接访问内核空间。当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间,而我们常见的 IO主要是磁盘 IO(读写文件)  网络 IO(网络请求和响应)。

        从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),而具体的IO 操作则是系统的内核来执行。 当应用程序发起 I/O 调用后,会经历两个步骤:

  • 内核等待 I/O 设备准备好数据
  • 内核将数据从内核空间拷贝到用户空间。

1、IO 的分类

        IO读写操作实际上 Read 系统调用是将数据从内核空间复制到用户空间;write系统调用是将数据从用户空间复制到内核空间;底层都是由内核来完成的。UNIX 系统下的 IO 模型一共有同步阻塞 I/O、同步非阻塞 I/OI/O 多路复用、信号驱动 I/O 和异步 I/O 5 种而信号驱动 I/O 使用中并不多见,这里不多做介绍了

        java中的 IO 在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。在 Java 7 中时,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。

a、同步阻塞 I/O

        当用户程序执行 read ,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read 才会返回。阻塞等待的是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程,在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

工作原理

  • 进程发起 I/O 请求后,会一直等待,直到数据传输完成。
  • 在数据传输期间,进程被挂起,无法执行其他任务。

  • 应用进程向内核发起 read 读取数据
  • 等待内核缓冲区数据(阻塞)
  • 内核缓冲区数据准备好后,复制数据到用户缓冲区(阻塞)
  • 复制完成,返回成功

java中的实现

ServerSocket serverSocket = new ServerSocket(portNumber);
Socket clientSocket = serverSocket.accept(); // 阻塞直到有连接
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) { // 阻塞直到数据可读
    // 处理数据
}
b、同步非阻塞 NI/O 

        非阻塞的 read 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read 调用才可以获取到结果。

工作原理

  • 进程发起 I/O 请求后,如果数据未准备好,请求会立即返回一个错误码。
  • 进程需要不断轮询来检查数据是否准备好。

  • 应用进程向内核发起 read 读取数据
  • 数据没有准备好,直接返回 EWOULDBLOCK 错误(非阻塞)
  • 应用进程不断发起 read 读取数据
  • 内核缓冲区数据准备好后,将数据复制到用户缓冲区(阻塞)
  • 复制完成,返回成功

java中的实现

SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); // 设置为非阻塞模式
socketChannel.connect(new InetSocketAddress("example.com", 80));
while (!socketChannel.finishConnect()) {
    // 非阻塞模式下,连接尚未完成,可以做其他事情
}
// 连接建立后,进行数据读写
 c、I/O 多路复用

        使用 select、poll、epoll 等系统调用,进程可以监视多个文件描述符,一旦某个文件描述符就绪(读写操作不会阻塞),则能够通知进程进行相应的读写操作。JAVA 中的 NIO 是使用的 IO 多路复用,Linux环境下底层使用的是 epoll。该模型适用于连接数多,连接时间短的场景,select阻塞,等待所有socket中的任意一个变成可读

工作原理

  • 使用单个线程来监控多个文件描述符的 I/O 状态。
  • 当某个文件描述符就绪时,线程会被唤醒来执行 I/O 操作。

  • 进程受阻于select调用,等待多个 socket 中的任意一个变为可读(阻塞)
  • 当socket变成可读时(说明内核缓冲区数据已经准备好),用户进程发起read读取数据,将数据复制到用户缓冲区(阻塞)
  • 复制完成,返回成功

java中的实现

Selector selector = Selector.open();
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress(portNumber));
serverSocket.configureBlocking(false);
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
    int readyChannels = selector.select();
    if (readyChannels == 0) continue;
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isAcceptable()) {
            // 处理接受连接
        } else if (key.isReadable()) {
            // 处理读
        }
        keyIterator.remove();
    }
}
d、异步 I/O

        进程发起异步 I/O 操作后,可以立即返回继续执行其他任务。当整个 I/O 操作(包括数据准备和传输)完成时,进程会收到一个信号或者其他形式的通知。

工作原理

  • 进程发起异步 I/O 操作后,可以立即返回继续执行其他任务。
  • 当整个 I/O 操作完成时,进程会收到通知。

java中的实现

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(Paths.get("data.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
fileChannel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        // 读取完成时的处理
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        // 读取失败时的处理
    }
});
// 继续执行其他任务,不需要等待 I/O 完成

       异步 I/O 模型虽然提供了非阻塞操作和高性能的潜力,但由于其编程复杂性、平台依赖性、调试难度以及对现有同步系统的重构成本,它并没有被广泛使用和推广。然而,在特定的高并发和 I/O 密集型应用场景中,异步 I/O 仍然是一个重要且有价值的工具

2、总结对比

优点缺点
同步阻塞 I/O

- 编程模型简单,易于理解和实现

- 适合连接数较少且持续时间短的应用

- I/O 操作会阻塞线程,效率低下,不适合高并发场景

- 线程资源消耗大,每个连接都需要一个线程来处理

同步非阻塞 I/O

- 允许单线程管理多个连接,提高资源利用率

- 线程可以在没有数据可读写时执行其他任务

- 编程模型复杂,需要处理轮询逻辑。

- 高负载下,CPU 轮询消耗可能会增加

I/O 多路复用

- 单线程或少量线程就可以处理多个客户端连接,适合高并发场景。

- 通过减少线程数量,降低了资源消耗。

- 编程模型相对复杂,需要维护多个文件描述符和事件。

- 在某些实现(如 select)中,有文件描述符数量限制

异步 I/O

- 真正的异步,应用程序无需等待 I/O 完成,可以立即处理其他任务。

- 提高了程序的吞吐量和响应性

- 编程模型最为复杂,需要处理回调或者 Future 对象。

- 系统实现复杂,依赖操作系统的支持。

        同步阻塞 I/O 适合简单的应用场景,而 I/O 多路复用和异步 I/O 更适合高并发和高吞吐量的场景。同步非阻塞 I/O  和信号驱动 I/O 在实际应用中较少见,通常被 I/O 多路复用和异步 I/O 所取代。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值