一、BIO(阻塞 I/O)的底层原理

BIO 基于操作系统的阻塞式 I/O 模型,其核心特点是线程会被内核阻塞直到 I/O 操作完成。

1. 底层流程(以读操作为例)
  • 用户态发起读请求:Java 调用 InputStream.read() 时,会触发系统调用(如 Linux 的 read()),从用户态切换到内核态。
  • 内核等待数据:内核需要等待数据从外部设备(如网卡、磁盘)加载到内核缓冲区(Kernel Buffer),这个过程中,用户线程会被挂起(阻塞),释放 CPU 资源。
  • 数据拷贝:当数据到达内核缓冲区后,内核将数据从内核缓冲区拷贝到用户缓冲区(User Buffer)。
  • 唤醒线程:拷贝完成后,内核唤醒阻塞的用户线程,线程从内核态切换回用户态,继续执行后续逻辑。
2. 线程阻塞的本质
  • 阻塞过程中,线程处于 WAITING 状态,不消耗 CPU,但会占用系统资源(如线程栈内存)。
  • 每个连接对应一个线程,当并发量高时,线程数量激增,会导致:
    • 线程上下文切换开销增大(CPU 频繁切换线程状态)。
    • 内存占用过高(每个线程默认栈大小为 1MB 左右)。、

BIO 服务器端示例代码

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class BioServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("BIO 服务器启动,监听端口 8080...");
        
        while (true) {
            // 阻塞等待客户端连接
            Socket socket = serverSocket.accept();
            System.out.println("新客户端连接:" + socket.getInetAddress());
            
            // 为每个连接创建新线程处理
            new Thread(() -> {
                try {
                    handleClient(socket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    
    private static void handleClient(Socket socket) throws IOException {
        // 读取客户端数据(阻塞操作)
        byte[] buffer = new byte[1024];
        int len = socket.getInputStream().read(buffer);
        if (len > 0) {
            System.out.println("收到客户端数据:" + new String(buffer, 0, len));
        }
        
        // 向客户端写数据(阻塞操作)
        socket.getOutputStream().write("已收到数据".getBytes());
        socket.close();
    }
}

二、NIO(非阻塞 I/O)的底层原理

NIO 基于操作系统的非阻塞 I/O 模型多路复用 I/O 模型,核心是通过Selector(选择器) 实现单线程管理多个 I/O 通道。

1. 非阻塞 I/O 的核心机制
  • 非阻塞读 / 写:当调用 SocketChannel.read() 时,如果内核缓冲区没有数据,会立即返回 0(而非阻塞),线程可以继续处理其他任务。
  • 缓冲区(Buffer):数据必须通过 Buffer 交互,本质是一块用户态内存,减少用户态与内核态的切换次数(通过一次拷贝批量处理数据)。
2. 多路复用(Selector)的底层原理

Selector 是 NIO 的核心,其底层依赖操作系统的多路复用器(如 Linux 的 epoll、Windows 的 IOCP、macOS 的 kqueue)。以 Linux 的 epoll 为例:

  • 注册事件:Java 将 Channel(如 SocketChannel)注册到 Selector 时,底层会调用 epoll_ctl 向内核的 epoll 实例注册事件(如连接、读、写)。
  • 等待事件就绪:调用 selector.select() 时,底层会调用 epoll_wait阻塞等待内核通知就绪的 I/O 事件(这是 NIO 中唯一可能阻塞的地方,但只阻塞一个线程)。
  • 事件分发:当有 I/O 事件就绪(如数据到达),内核会将就绪事件列表返回给用户态,Selector 遍历事件列表,唤醒对应的 Channel 进行处理。
3. 流程对比(与 BIO 对比)
  • BIO:一个线程处理一个连接,全程阻塞等待 I/O。
  • NIO:一个线程通过 Selector 管理所有连接,仅在 select() 时阻塞,数据就绪后才处理,大幅减少线程数量。

NIO 服务器端示例代码

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.Iterator;
import java.util.Set;

public class NioServer {
    public static void main(String[] args) throws IOException {
        // 创建选择器
        Selector selector = Selector.open();
        
        // 创建服务器通道并绑定端口
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 注册到选择器,关注连接事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        System.out.println("NIO 服务器启动,监听端口 8080...");
        
        while (true) {
            // 阻塞等待就绪的通道(可以设置超时时间)
            int readyChannels = selector.select();
            if (readyChannels == 0) {
                continue;
            }
            
            // 获取就绪通道的 SelectionKey 集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                
                // 处理连接事件
                if (key.isAcceptable()) {
                    ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                    // 接受客户端连接(非阻塞)
                    SocketChannel socketChannel = serverChannel.accept();
                    socketChannel.configureBlocking(false);
                    // 注册到选择器,关注读事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("新客户端连接:" + socketChannel.getRemoteAddress());
                }
                // 处理读事件
                else if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    // 读取数据(非阻塞)
                    int len = socketChannel.read(buffer);
                    if (len > 0) {
                        buffer.flip();
                        System.out.println("收到客户端数据:" + new String(buffer.array(), 0, len));
                        
                        // 响应客户端
                        buffer.clear();
                        buffer.put("已收到数据".getBytes());
                        buffer.flip();
                        socketChannel.write(buffer);
                    } else if (len == -1) {
                        // 客户端断开连接
                        socketChannel.close();
                        System.out.println("客户端断开连接");
                    }
                }
                
                // 移除已处理的 SelectionKey
                iterator.remove();
            }
        }
    }
}

三、BIO 与 NIO 底层原理对比

维度 BIO 底层原理 NIO 底层原理
I/O 模型 阻塞式 I/O 非阻塞 I/O + 多路复用 I/O
线程与连接关系 一对一(一个线程绑定一个连接) 一对多(一个线程管理多个连接)
阻塞点 read()/write()/accept() 均阻塞 仅 selector.select() 可能阻塞
系统调用 频繁的 read()/write() 系统调用 少量 epoll_ctl/epoll_wait 调用
内核交互 每次 I/O 都需用户态 - 内核态切换 通过 Buffer 批量处理,减少切换次数
适用场景 连接数少、数据处理快的场景 高并发、连接数多的场景(如服务器)

四、关键底层组件解析

  1. Channel(通道)

    • 对应操作系统的文件描述符(File Descriptor),是 I/O 操作的抽象(如 SocketChannel 对应网络套接字)。
    • 支持双向操作(读写),而 BIO 的 Stream 是单向的。
  2. Buffer(缓冲区)

    • 本质是一块连续的内存块,封装了数据读写的位置(positionlimitcapacity)。
    • 减少用户态与内核态的数据拷贝次数(通过 flip()clear() 等方法高效复用缓冲区)。
  3. Selector(选择器)

    • 底层映射到操作系统的多路复用器(如 epoll),负责监听 Channel 上的事件(连接、读、写)。
    • 通过 SelectionKey 关联 Channel 与事件,实现事件的分发与处理。

总结

  • BIO 是简单直观的阻塞模型,依赖线程阻塞等待 I/O,适合低并发场景。
  • NIO 基于非阻塞和多路复用,通过 Selector 实现单线程管理多连接,大幅降低资源消耗,适合高并发场景。
  • 两者的本质区别在于对操作系统 I/O 模型的封装方式:BIO 直接映射阻塞式 I/O,而 NIO 则利用了内核的多路复用机制,是更高效的 I/O 模型。

更多推荐