开发需了解的知识:JAVA IO 系统结构

294 阅读15分钟

一、Java IO 的体系结构

Java IO 提供了对文件、网络、控制台等多种输入输出设备的支持,主要包括以下两大类:

  1. 字节流:处理二进制数据。

    • 输入流基类:InputStream
    • 输出流基类:OutputStream
    • 示例:FileInputStreamFileOutputStreamBufferedInputStream 等。
  2. 字符流:处理文本数据。

    • 输入流基类:Reader
    • 输出流基类:Writer
    • 示例:FileReaderFileWriterBufferedReader 等。

Java IO 的基本模型

流的方向:
输入流 (InputStream/Reader)  -> 从外部资源读取数据到内存。
输出流 (OutputStream/Writer) -> 从内存写数据到外部资源。

装饰模式实现增强:
普通流 -> 缓冲流 (BufferedXXX) -> 格式化流 (PrintXXX)

常见设计模式

1. 装饰器模式 (Decorator Pattern)

作用:增强流的功能,如缓冲、格式化等。

示例

import java.io.*;

public class DecoratorPatternExample {
    public static void main(String[] args) throws IOException {
        // 创建文件输入流
        FileInputStream fileInputStream = new FileInputStream("example.txt");
        // 为输入流添加缓冲功能
        BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

        int data;
        while ((data = bufferedInputStream.read()) != -1) {
            System.out.print((char) data);
        }

        bufferedInputStream.close();
        fileInputStream.close();
    }
}

解释

  • FileInputStream 是基础流,用于从文件读取数据。
  • BufferedInputStream 是装饰流,增强了读取性能,避免频繁的磁盘操作。

2. 工厂模式 (Factory Pattern)

作用:创建不同类型的流对象,简化代码结构。

示例

import java.io.*;

public class IOFactory {
    public static InputStream getFileInputStream(String path) throws FileNotFoundException {
        return new FileInputStream(path);
    }

    public static OutputStream getFileOutputStream(String path) throws FileNotFoundException {
        return new FileOutputStream(path);
    }
}

class FactoryPatternExample {
    public static void main(String[] args) throws IOException {
        InputStream inputStream = IOFactory.getFileInputStream("example.txt");
        OutputStream outputStream = IOFactory.getFileOutputStream("output.txt");

        int data;
        while ((data = inputStream.read()) != -1) {
            outputStream.write(data);
        }

        inputStream.close();
        outputStream.close();
    }
}

Java NIO中也体现了多种设计模式:

1. 单例模式

  • 应用:NIO 中的 SelectorProvider 使用单例模式提供多路复用器的默认实现。
  • 优势:保证资源唯一性。

2. 观察者模式

  • 应用:NIO 的 Selector 使用观察者模式监控多个通道的事件(如读、写、连接等)。
  • 优势:提高线程利用率。

示例

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class ObserverPatternNIO {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8080));
        serverChannel.configureBlocking(false);

        Selector selector = Selector.open();
        serverChannel.register(selector, serverChannel.validOps());

        while (true) {
            selector.select(); // 阻塞等待事件
            selector.selectedKeys().forEach(key -> {
                // 处理事件逻辑
            });
        }
    }
}

二、Java NIO(New IO)

Java NIO 的核心是基于通道(Channel)和缓冲区(Buffer)实现的数据流管理,并通过选择器(Selector)实现非阻塞 IO。

1. 通道(Channel)

  • 类似于传统 IO 中的流,但通道是双向的,可以进行读写操作。

  • 通道与流的区别:

    • 流是单向的(读或写)。
    • 通道是双向的(可以同时读和写)。
  • 常见通道:

    • FileChannel:用于文件的数据传输。
    • SocketChannel:用于 TCP 的网络通信。
    • DatagramChannel:用于 UDP 的网络通信。

2. 缓冲区(Buffer)

  • 数据读写操作的核心部分。所有数据都需要通过缓冲区进行存储和传递。

  • 常见缓冲区类型:

    • ByteBuffer:处理字节数据(最常用)。
    • CharBuffer:处理字符数据。
    • IntBufferFloatBuffer 等:处理基本类型数据。
  • 缓冲区的关键属性:

    • capacity:缓冲区的容量,固定大小,不能超过它存储数据。

    • position:下一个读或写操作的位置。

    • limit:读写操作的界限,不能超过此值。

    • 常见方法:

      buffer.put(data); // 写入数据到缓冲区
      buffer.flip();    // 切换为读取模式
      buffer.get();     // 从缓冲区读取数据
      buffer.clear();   // 清空缓冲区,准备再次写入
      

3. 选择器(Selector)

  • 多路复用器,用于管理多个通道的 IO 操作。

  • 允许单个线程监控多个通道的事件(如读、写、连接等),实现高效的非阻塞 IO。

  • 常见使用场景:

    • 网络服务器:高并发的客户端请求处理。

4. NIO多路复用机制

java NIO 的多路复用机制是 NIO 的核心特性之一,依赖于 Selector 实现多个通道的高效管理。

核心组件

4.1. Selector

  • NIO 中的多路复用核心接口,负责管理多个通道的事件。
  • Selector 本身并不直接处理 IO,而是充当调度器的角色,基于底层操作系统提供的多路复用机制。

4.2. SelectionKey

  • 描述一个通道和 Selector 的注册关系。
  • 记录通道的事件类型(如读、写、连接等)和附加的上下文信息。

底层实现机制

4.1. 操作系统支持的多路复用模型

不同操作系统提供了多种 IO 多路复用机制,Java NIO 会根据操作系统选择相应的实现:

  1. Linux:epoll

    • 默认实现(从 Java 1.6 开始)。

    • 高性能,支持大规模连接。

    • 事件触发模式:支持水平触发(Level Triggered, LT)和边沿触发(Edge Triggered, ET)。

    • 主要接口:

      • epoll_create():创建一个 epoll 实例。
      • epoll_ctl():向 epoll 实例注册、修改或删除文件描述符。
      • epoll_wait():等待文件描述符上事件发生。
  2. Windows:select

    • Java NIO 会基于 Windows 提供的 select 实现。
    • 文件描述符数量受限(一般 1024 或 2048),性能不如 epoll

4.2. Selector 的工作流程

(1) Selector 初始化

  • 调用 Selector.open() 创建 Selector。

  • Java NIO 会根据平台调用相应的底层接口初始化多路复用实例:

    • Linux 上调用 epoll_create()
    • macOS 上调用 kqueue()

(2) 注册通道到 Selector

  • 调用 channel.register(selector, ops) 将通道注册到 Selector。
  • 注册时,Java 会调用底层的 epoll_ctl()kqueue 等接口,将通道的文件描述符添加到多路复用实例中。

(3) 事件监听

  • 调用 selector.select()
  • 此时,Java 会进入本地代码层,调用操作系统的接口(如 epoll_wait)阻塞等待事件。
  • 如果有事件发生,select() 方法返回,Java NIO 会将就绪的通道封装为 SelectionKey 返回给用户。

(4) 事件处理

  • 通过 selector.selectedKeys() 获取就绪通道集合。
  • 遍历集合,对每个通道进行处理。

4.3. 底层实现分析(以 Linux epoll 为例)

  1. epoll_create 创建实例

    • Selector.open() 时,底层调用 epoll_create() 创建一个 epoll 文件描述符。
  2. epoll_ctl 注册通道

    • 当调用 channel.register() 时,底层调用 epoll_ctl() 将文件描述符注册到 epoll 中。

    • 示例:

      epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
      
  3. epoll_wait 等待事件

    • 调用 selector.select() 时,底层调用 epoll_wait() 阻塞等待事件。

    • 示例:

      int nfds = epoll_wait(epfd, events, maxevents, timeout);
      
  4. 就绪事件返回

    • 当有通道的事件就绪时,epoll_wait 返回就绪的事件列表,Java NIO 将其封装为 SelectionKey

三、Java AIO(Asynchronous IO)

特点

  • 异步非阻塞模型:线程提交读写操作后立即返回,操作完成后通过回调机制处理结果。
  • 面向事件驱动:数据读写的完成由操作系统通知。
  • 适合高并发场景:线程数较少,避免了线程阻塞。

核心类

  1. AsynchronousServerSocketChannel:异步服务器套接字通道。
  2. AsynchronousSocketChannel:异步客户端套接字通道。
  3. CompletionHandler:回调接口,处理异步操作的完成通知。

AIO 网络操作示例

服务端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.ExecutionException;

public class AIOServerExample {
    public static void main(String[] args) throws IOException {
        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8080));

        System.out.println("Server is listening...");

        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
                ByteBuffer buffer = ByteBuffer.allocate(256);
                try {
                    clientChannel.read(buffer).get();
                    buffer.flip();
                    System.out.println("Received: " + new String(buffer.array()).trim());
                } catch (Exception e) {
                    e.printStackTrace();
                }
                serverChannel.accept(null, this); // 接收下一个连接
            }

            @Override
            public void failed(Throwable exc, Void attachment) {
                exc.printStackTrace();
            }
        });

        // 阻塞主线程(仅示例用)
        while (true) {}
    }
}

客户端

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.ExecutionException;

public class AIOClientExample {
    public static void main(String[] args) throws Exception {
        AsynchronousSocketChannel clientChannel = AsynchronousSocketChannel.open();
        clientChannel.connect(new InetSocketAddress("127.0.0.1", 8080)).get();

        ByteBuffer buffer = ByteBuffer.wrap("Hello AIO Server".getBytes());
        clientChannel.write(buffer).get();
        System.out.println("Message sent to server.");
        clientChannel.close();
    }
}

对比总结

特性IO(阻塞)NIO(非阻塞)AIO(异步非阻塞)
线程模型单线程单独连接单线程处理多个连接单线程异步处理多连接任务
编程复杂度简单较复杂较复杂
数据处理方式基于流基于缓冲区基于回调
性能低,线程开销大中,需管理多路复用高,异步回调
使用场景小规模、低并发中等并发,实时性要求高高并发,高性能的实时系统

扩展1:Reactor模型

NIO 是实现 Reactor 模式的技术基础,NIO 提供了非阻塞的 I/O 操作以及多路复用的能力,而 Reactor 模式则利用这些能力实现了高效的事件驱动网络架构。

  1. NIO 提供的核心支持

    • Selector:NIO 的多路复用器,允许一个线程监听多个通道的 I/O 事件。
    • Channel:非阻塞通道,支持 Socket 的异步 I/O 操作。
    • Buffer:用于数据的读写操作,是 NIO 数据处理的核心。
    • 非阻塞 I/O:通过通道与选择器结合,避免传统阻塞 I/O 的性能问题。
  2. Reactor 模型对 NIO 的封装

    • Reactor 模型通过 Selector 监听事件,将事件分发给对应的处理器(Handler)。
    • NIO 的底层机制(如 epoll)为 Reactor 模型提供了高效的事件监听与分发能力。
    • Reactor 的抽象使得开发者可以更专注于事件的处理逻辑,而不用直接管理底层的 I/O 操作。

Reactor 模型的核心内容

Reactor 模型是一种事件驱动的设计模式,其核心在于如何高效地监听和处理多个事件。以下是模型的主要内容:


一、Reactor 模型的组成

  1. Reactor

    • 职责:事件管理中心,负责监听事件(如连接请求、读写事件)并分发给相应的处理器。
    • 实现:利用 Selectorselect() 方法监听多个通道的事件。
    • 特点:通常是单线程或有限的多线程,处理事件的监听与分发。
  2. Handler(处理器)

    • 职责:负责具体的事件处理逻辑,如接收数据、业务处理、发送响应等。
    • 实现:注册到 Reactor 中,当事件触发时被调用。
    • 特点:每种事件类型对应一个 Handler,实现解耦。
  3. Channel(通道)

    • 职责:表示与客户端通信的通道,支持非阻塞 I/O。
    • 实现:使用 NIO 中的 SocketChannelServerSocketChannel
    • 特点:与 Selector 配合,实现事件的异步处理。
  4. Selector(选择器)

    • 职责:多路复用器,监听多个通道上的事件(如读、写、连接)。
    • 实现:底层依赖操作系统的多路复用机制,如 epollpollselect
    • 特点:是实现高并发的核心组件。

二、Reactor 模型的流程

  1. 通道注册

    • 服务器端的 ServerSocketChannel 注册到 Selector 上,用于监听连接事件。
    • 客户端的 SocketChannel 注册到 Selector 上,用于监听读写事件。
  2. 事件轮询

    • Reactor 调用 Selector.select() 方法,阻塞等待通道上的事件触发。
    • 如果有事件触发,Selector 返回所有就绪的通道。
  3. 事件分发

    • 根据事件类型(如连接、读、写)将事件分发给对应的 Handler
  4. 事件处理

    • Handler 完成具体的业务逻辑处理(如读取数据、处理请求、发送响应)。
    • 在需要时,将通道重新注册到 Selector,监听后续事件。

三、Reactor 模型的关键实现

  1. 事件监听与分发

    • 使用 Selectorselect() 方法监听多个通道。
    • 通过 SelectionKey 获取事件类型并分发给对应的处理器。
  2. 非阻塞 I/O

    • 通道设置为非阻塞模式(channel.configureBlocking(false))。
    • 通过 channel.read()channel.write() 进行非阻塞的读写操作。
  3. 并发处理

    • 可以使用线程池来处理复杂的业务逻辑,以避免阻塞 Reactor 线程。

四、Reactor 模型的变种

  1. 单线程模型

    • 单个线程负责所有的事件监听与处理。
    • 适用于低并发场景,但难以扩展。
  2. 多线程模型

    • Reactor 负责事件监听,具体事件由线程池中的线程处理。
    • 能够充分利用多核 CPU,适用于中高并发场景。
  3. 主从 Reactor 模型

    • 主 Reactor 线程负责监听连接事件,并将新连接交给从 Reactor。
    • 从 Reactor 线程负责处理读写事件,适用于高并发场景。

五、Reactor 模型的优点

  1. 高效

    • 使用 NIO 的多路复用技术,一个线程可以同时管理多个连接,减少了线程切换的开销。
  2. 解耦

    • 将事件监听与处理分开,便于扩展与维护。
  3. 适应高并发

    • 能够处理成千上万的连接请求,适合 Web 服务器、即时通讯系统等场景。
  4. 灵活

    • 支持单线程、多线程或主从模型,能够根据需求进行调整。

扩展2:零拷贝技术

在传统的 I/O 模型中,数据通常需要经过多次拷贝(如从磁盘到内核缓冲区、再到用户缓冲区),这些拷贝操作会占用大量的 CPU 资源并导致性能下降。Java 的 NIO(New I/O) 提供了一些机制和方法,支持零拷贝的高效数据传输。

一、传统 I/O 的拷贝过程

在传统的阻塞式 I/O 中,数据从文件到网络的传输通常会涉及以下几个步骤:

  1. 磁盘读取到内核缓冲区:数据从磁盘读取到操作系统内核缓冲区。
  2. 内核缓冲区到用户缓冲区:数据从内核缓冲区拷贝到用户缓冲区。
  3. 用户缓冲区到内核缓冲区(Socket 缓冲区) :数据从用户缓冲区再次拷贝到内核缓冲区。
  4. 内核缓冲区发送到网络:数据从 Socket 缓冲区通过网络发送。

在上述过程中,数据在内存中的拷贝次数较多(至少两次:内核缓冲区到用户缓冲区,以及用户缓冲区回到内核缓冲区),而且涉及用户态与内核态的切换,导致性能瓶颈。


二、NIO 零拷贝技术的实现

NIO 提供的零拷贝机制,利用操作系统支持的一些优化方法,减少甚至避免数据在内存中的拷贝操作,直接实现高效传输。主要依赖以下几种技术:

1. FileChanneltransferTo()transferFrom()

  • 原理

    • transferTo() 方法可以将数据从文件直接传输到目标通道(例如 SocketChannel)。
    • transferFrom() 方法允许将数据从源通道(例如 SocketChannel)传输到文件。
  • 实现机制

    • 底层通过操作系统的 sendfile 系统调用来实现零拷贝。
    • 数据从文件直接传输到网络设备的缓冲区,无需进入用户态或用户缓冲区。

过程

  1. 文件的数据被 DMA(Direct Memory Access,直接内存访问)从磁盘传输到内核缓冲区。
  2. 数据直接从内核缓冲区传输到 Socket 缓冲区。
  3. 最终由网络设备将数据发送出去。

特点

  • 避免了用户态和内核态之间的拷贝。
  • 减少了 CPU 的负担(数据传输主要由 DMA 完成)。
  • 提升了大文件传输的性能。

2. MappedByteBuffer 和内存映射文件

  • 原理

    • 使用 FileChannelmap() 方法将文件内容映射到内存中,生成一个 MappedByteBuffer
    • 操作系统会将文件直接加载到内存映射区域,而无需手动拷贝数据。
  • 实现机制

    • 内存映射文件使用操作系统的 mmap 系统调用,将文件的内容与进程的地址空间关联。
    • 数据读取和写入通过内存地址操作,而不是通过显式的 I/O 方法。

过程

  1. 文件被映射到内存区域。
  2. 应用程序直接通过 MappedByteBuffer 访问内存中的文件内容,无需额外拷贝。

特点

  • 高效读取和写入文件内容,适用于随机访问文件数据。
  • 缺点是需要注意映射的内存大小,可能导致内存不足问题。

3. 零拷贝的网络传输

  • 在网络 I/O 中,Java NIO 的 SocketChannel 与操作系统的 sendfile 技术结合,实现零拷贝。
  • 例如,在 FileChannel.transferTo() 中,数据从文件直接传输到 SocketChannel,而不经过用户态的缓冲区。

三、零拷贝的底层实现(操作系统支持)

  1. DMA(Direct Memory Access)

    • 通过 DMA 技术,数据可以在设备(如磁盘)与内存之间直接传输,无需 CPU 的参与。
    • Java NIO 零拷贝技术依赖 DMA 来减少 CPU 负担。
  2. mmap 系统调用

    • 将文件内容映射到内存地址空间,避免了额外的 I/O 拷贝操作。
    • 常用于 MappedByteBuffer 实现零拷贝。
  3. sendfile 系统调用

    • sendfile 是操作系统提供的零拷贝技术,用于高效地将数据从文件传输到网络。
    • 在 Java 的 FileChannel.transferTo()transferFrom() 中得以应用。

四、NIO 零拷贝的优缺点

优点

  1. 高性能

    • 减少内存拷贝次数,提升 I/O 性能。
    • 避免用户态与内核态之间的频繁切换。
  2. 低 CPU 占用

    • 使用 DMA 技术,大部分数据传输由硬件完成,降低 CPU 负担。
  3. 适合大文件传输

    • 特别适合需要处理大文件的网络应用场景,如文件服务器和视频流媒体服务。

缺点

  1. 兼容性

    • 零拷贝的效果依赖操作系统支持,不同操作系统实现可能有所差异。
    • 某些老旧的操作系统可能不支持 sendfilemmap
  2. 内存管理复杂

    • 使用 MappedByteBuffer 时,内存映射文件需要注意内存使用问题,可能导致内存不足。
  3. 数据处理能力有限

    • 零拷贝适用于直接传输数据,但对数据需要处理(如加密、压缩)时,效果有限。

五、使用场景

  1. 大文件传输

    • 文件服务器或下载服务(如 HTTP 文件传输)。
  2. 流媒体传输

    • 视频流和音频流服务(如 YouTube)。
  3. 高性能网络应用

    • 高并发的网络服务(如 Web 服务器、聊天服务器)。
  4. 日志系统

    • 日志文件的高效写入和读取。

六、示例代码

1. FileChannel.transferTo() 示例

import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;

public class ZeroCopyExample {
    public static void main(String[] args) throws Exception {
        RandomAccessFile file = new RandomAccessFile("example.txt", "r");
        FileChannel fileChannel = file.getChannel();

        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
        long fileSize = fileChannel.size();

        fileChannel.transferTo(0, fileSize, socketChannel);

        fileChannel.close();
        socketChannel.close();
    }
}

2. 使用 MappedByteBuffer 示例

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MappedByteBufferExample {
    public static void main(String[] args) throws Exception {
        RandomAccessFile file = new RandomAccessFile("example.txt", "rw");
        FileChannel channel = file.getChannel();

        // 将文件映射到内存
        MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());
        buffer.put(0, (byte) 'H');  // 修改内存中的文件内容

        channel.close();
        file.close();
    }
}