# NIO

作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)


Java NIO是Java 1.4引入的一套全新的IO API,和传统的阻塞IO(BIO)相比,NIO支持非阻塞IO操作,极大提升了高并发场景下的性能和资源利用率

# 一、NIO的设计理念与优势

  • 非阻塞IO 线程可以发起IO请求后,不必等待操作完成就继续执行,避免线程被阻塞。
  • 多路复用 通过Selector,一个线程可以同时监听多个通道(Channel),高效管理多个连接。
  • 基于缓冲区(Buffer) 数据的读写是围绕Buffer展开的,应用程序直接操作缓冲区,降低数据拷贝成本。
  • 面向通道(Channel) 代替传统的流,支持读写数据,支持非阻塞模式。

# 二、Java NIO核心组件详解

# Channel

作用

Channel 是Java NIO中数据传输的核心接口,类似于传统IO中的流,但区别在于它是双向的、可读写的,并且支持阻塞和非阻塞两种模式。它负责连接实体和数据的传输。

设计

  • Channel是一个接口,位于java.nio.channels包。
  • 继承自Closeable接口,支持关闭操作。

源码接口定义(简化版):

public interface Channel extends Closeable {
    boolean isOpen();
    void close() throws IOException;
}
1
2
3
4

主要实现类

类名 说明
FileChannel 处理文件数据的通道
SocketChannel TCP客户端Socket通道,支持连接远程服务器
ServerSocketChannel TCP服务器端Socket通道,监听连接请求
DatagramChannel UDP数据报通道

关键方法示例(以SocketChannel为例)

// 打开SocketChannel
SocketChannel sc = SocketChannel.open();
// 设置非阻塞模式
sc.configureBlocking(false);
// 连接远程服务器
sc.connect(new InetSocketAddress("localhost", 8080));
// 读写操作通过ByteBuffer进行
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = sc.read(buf);
1
2
3
4
5
6
7
8
9

内部设计细节

  • 其底层依赖操作系统的Socket、File等系统资源,通过FileDescriptor与本地系统交互。
  • 通过SelectableChannel抽象类支持注册到Selector,实现多路复用。

SocketChannel继承链(简化):

java.lang.Object
 └─ java.nio.channels.spi.AbstractSelectableChannel
      └─ java.nio.channels.SocketChannel
1
2
3

# Buffer

# 作用:

Buffer是数据的容器,是NIO读写数据的中介。所有NIO数据读写都是基于Buffer进行的。它为数据操作提供统一的API,并负责维护读写位置、容量等状态。

# 设计:

  • 抽象类java.nio.Buffer是所有具体Buffer的父类。
  • 常用实现包括ByteBuffer, CharBuffer, IntBuffer等,分别针对不同数据类型。

核心属性:

属性 说明
capacity 容量,Buffer最大存储量
position 当前读写位置
limit 读写的边界,不能越界

典型Buffer状态转换方法:

  • flip():切换读写模式,limit=position, position=0
  • clear():清空Buffer,准备写入
  • rewind():回到Buffer起始位置,重新读写

# 代码示例:

ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello".getBytes());
buffer.flip();  // 准备读
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
System.out.println(new String(bytes));  // 输出Hello
1
2
3
4
5
6

# 源码核心方法(以ByteBuffer为例):

ByteBufferBuffer的子类,重点管理字节数据。

  • put(byte b) 写入一个字节,并将position向前移动
  • get() 读取当前位置的字节,并将position向前移动

源码中有大量方法重载来支持不同读写模式和内存管理(堆内存 vs 直接内存)。

# Selector

# 作用:

Selector是Java NIO实现非阻塞IO和多路复用的关键。它允许一个线程监听多个SelectableChannel的事件(如连接就绪、数据可读等),极大提高服务器的并发处理能力。

# 设计:

Selector是一个抽象类,位于java.nio.channels包。

public abstract class Selector implements Closeable {
    public static Selector open() throws IOException {...}
    public abstract boolean isOpen();
    public abstract void close() throws IOException;
    public abstract int select() throws IOException;
    public abstract int select(long timeout) throws IOException;
    public abstract Set<SelectionKey> selectedKeys();
    public abstract Set<SelectionKey> keys();
    public abstract void wakeup();
}
1
2
3
4
5
6
7
8
9
10

# 工作流程:

  • 通过select()方法阻塞等待事件发生。
  • 事件发生后,通过selectedKeys()获取就绪事件的集合。
  • 对每个SelectionKey处理对应的事件(连接、读写等)。
  • 通过wakeup()可中断阻塞等待。

# 源码设计亮点:

  • Selector的具体实现依赖于操作系统的IO多路复用机制(Linux使用epoll,Windows使用IOCPselect)。
  • JDK 11中SelectorProvider提供不同平台的Selector实现。

# SelectionKey

# 作用:

SelectionKey代表SelectableChannel注册到Selector后与该Selector之间的关联关系,表示该通道当前的状态和感兴趣的事件。

# 设计:

  • 作为事件注册和事件状态的载体。
  • 包含4类事件操作标志:
标志 说明
OP_ACCEPT 服务器Socket监听连接事件
OP_CONNECT 客户端Socket连接完成事件
OP_READ 通道数据可读事件
OP_WRITE 通道可写事件
  • 通过interestOps()设置关注事件,通过readyOps()获取就绪事件。

源码简化接口:

public abstract class SelectionKey {
    public abstract SelectableChannel channel();
    public abstract Selector selector();
    public abstract int interestOps();
    public abstract SelectionKey interestOps(int ops);
    public abstract int readyOps();
}
1
2
3
4
5
6
7

# 三、总结

组件 核心作用 设计亮点和源码相关
Channel 数据传输通道,支持非阻塞,基于操作系统底层资源 抽象接口,具体实现依赖SelectableChannel和操作系统FD
Buffer 数据缓冲区,维护数据读写状态,提供高效操作接口 抽象类Buffer+具体实现如ByteBuffer,支持堆和直接内存
Selector 事件多路复用器,监听多个通道的IO事件 抽象类,具体实现依赖操作系统的多路复用API(epoll、kqueue等)
SelectionKey 通道与选择器的绑定信息,携带关注事件和就绪事件状态 包含事件操作标志,管理事件注册与触发

# 四、NIO工作流程

  1. 打开通道 创建通道对象,如ServerSocketChannel.open()
  2. 配置非阻塞模式 通过channel.configureBlocking(false)将通道设置为非阻塞。
  3. 绑定端口,监听请求 服务器通道绑定端口,准备接收连接。
  4. 创建Selector 选择器用于监听通道事件。
  5. 通道注册到Selector 将通道注册感兴趣的事件(如OP_ACCEPTOP_READ等)。
  6. 轮询Selector,处理事件 调用selector.select()阻塞等待事件发生,遍历selectedKeys处理事件。

# 五、代码示例

# 服务器端(非阻塞NIO)

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 1. 打开服务器通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8080));
        serverChannel.configureBlocking(false); // 非阻塞

        // 2. 打开选择器
        Selector selector = Selector.open();

        // 3. 注册服务器通道到选择器,监听连接事件
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("Server started on port 8080...");

        while (true) {
            // 4. 选择器阻塞等待就绪事件
            int readyChannels = selector.select();
            if (readyChannels == 0) continue;

            // 5. 获取就绪事件集合
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                keyIterator.remove();

                if (key.isAcceptable()) {
                    // 6. 处理连接请求
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = ssc.accept();
                    clientChannel.configureBlocking(false);
                    clientChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("Accepted connection from " + clientChannel);
                } else if (key.isReadable()) {
                    // 7. 处理读事件
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int bytesRead = clientChannel.read(buffer);
                    if (bytesRead > 0) {
                        buffer.flip();
                        byte[] data = new byte[buffer.limit()];
                        buffer.get(data);
                        System.out.println("Received: " + new String(data));
                        // 简单回写
                        buffer.rewind();
                        clientChannel.write(buffer);
                    } else if (bytesRead == -1) {
                        // 客户端关闭连接
                        clientChannel.close();
                    }
                }
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# 客户端示例

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

public class NIOClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(true);  // 阻塞模式也可

        socketChannel.connect(new InetSocketAddress("localhost", 8080));

        String message = "Hello NIO Server!";
        ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());

        socketChannel.write(buffer);

        // 读取回显
        buffer.clear();
        int bytesRead = socketChannel.read(buffer);
        if (bytesRead > 0) {
            buffer.flip();
            byte[] data = new byte[buffer.limit()];
            buffer.get(data);
            System.out.println("Echo from server: " + new String(data));
        }

        socketChannel.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30