Java深入解析篇九之NIO详解

Java深入解析篇九之NIO详解

一、NIO概述

1.1 什么是NIO

NIO(New I/O)是Java在JDK 1.4中引入的一套新的I/O API,位于java.nio包下。它提供了面向缓冲区(Buffer)的、非阻塞的I/O操作方式,是对传统BIO(Blocking I/O)的重大改进。JDK 7对NIO进行了进一步完善,引入了NIO.2(java.nio.filejava.nio.channels的增强)。

1.2 NIO与BIO的核心区别

特性 BIO NIO
面向 流(Stream) 缓冲区(Buffer)
阻塞性 阻塞 非阻塞
多路复用 不支持 Selector支持
数据操作 单向读取 可双向读写
线程模型 一连接一线程 一线程多连接

1.3 NIO的设计目标

  • 高并发:通过Selector实现一个线程管理多个连接
  • 高性能:通过零拷贝、内存映射减少数据复制次数
  • 灵活性:Buffer可双向操作,支持Scatter/Gather

二、NIO三大核心组件

2.1 核心架构

复制代码
┌─────────────────────────────────────────────┐
│                  Selector                     │
│  ┌───────────┐  ┌───────────┐  ┌─────────┐ │
│  │  Channel  │  │  Channel  │  │ Channel │ │
│  │     ↕     │  │     ↕     │  │    ↕    │ │
│  │  Buffer   │  │  Buffer   │  │ Buffer  │ │
│  └───────────┘  └───────────┘  └─────────┘ │
└─────────────────────────────────────────────┘
  • Channel(通道):数据传输的通道,类似BIO中的Stream,但Channel是双向的
  • Buffer(缓冲区):数据的容器,Channel读写数据都通过Buffer
  • Selector(选择器):多路复用器,一个Selector可以监听多个Channel的事件

2.2 三者协作流程

java 复制代码
// 1. 打开Channel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);

// 2. 创建Selector并注册Channel
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

// 3. 通过Selector监听事件,使用Buffer读写数据
while (true) {
    selector.select(); // 阻塞等待就绪事件
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> iter = keys.iterator();
    while (iter.hasNext()) {
        SelectionKey key = iter.next();
        if (key.isAcceptable()) {
            // 处理连接,使用Buffer读写
        }
        iter.remove();
    }
}

三、Buffer详解

3.1 Buffer核心属性

Buffer有四个核心属性:

属性 含义 说明
capacity 容量 Buffer最大可存储的数据量,创建后不可变
limit 界限 当前可读写数据的边界
position 位置 下一个读/写操作的索引位置
mark 标记 记录position的临时位置,可reset恢复

不变式关系:0 <= mark <= position <= limit <= capacity

3.2 Buffer状态转换

复制代码
写模式:position从0递增到写入数据量,limit = capacity
    ↓ flip()
读模式:limit = 原position,position = 0
    ↓ clear()/compact()
写模式:position = 0,limit = capacity

3.3 核心操作方法

java 复制代码
import java.nio.ByteBuffer;

public class BufferDemo {
    public static void main(String[] args) {
        // 创建容量为10的ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(10);
        System.out.println("初始状态: " + buffer);
        // position=0, limit=10, capacity=10

        // 写入数据
        buffer.put((byte) 1);
        buffer.put((byte) 2);
        buffer.put((byte) 3);
        System.out.println("写入3字节后: " + buffer);
        // position=3, limit=10, capacity=10

        // flip():切换为读模式
        buffer.flip();
        System.out.println("flip后: " + buffer);
        // position=0, limit=3, capacity=10

        // 读取数据
        byte b1 = buffer.get();
        byte b2 = buffer.get();
        System.out.println("读取: " + b1 + ", " + b2);
        // position=2, limit=3

        // mark()和reset()
        buffer.mark();        // mark = 2
        byte b3 = buffer.get(); // position=3
        buffer.reset();       // position恢复到2
        System.out.println("reset后position: " + buffer.position());

        // rewind():position归零,limit不变
        buffer.rewind();
        System.out.println("rewind后: " + buffer);
        // position=0, limit=3

        // clear():清空(逻辑清空,数据仍在)
        buffer.clear();
        System.out.println("clear后: " + buffer);
        // position=0, limit=10, capacity=10

        // compact():压缩(将未读数据移到开头)
        buffer.put((byte) 10);
        buffer.put((byte) 20);
        buffer.put((byte) 30);
        buffer.flip();
        buffer.get(); // 读取一个
        buffer.compact(); // 未读的20,30移到开头
        System.out.println("compact后: " + buffer);
        // position=2, limit=10, capacity=10
    }
}

3.4 Buffer类型体系

复制代码
Buffer(抽象基类)
├── ByteBuffer
│   ├── HeapByteBuffer(堆内)
│   └── DirectByteBuffer(堆外)
├── CharBuffer
├── ShortBuffer
├── IntBuffer
├── LongBuffer
├── FloatBuffer
└── DoubleBuffer

3.5 只读Buffer与切片

java 复制代码
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.putInt(100).putInt(200).putInt(300);
buffer.flip();

// 只读Buffer
ByteBuffer readOnly = buffer.asReadOnlyBuffer();
// readOnly.put((byte)1); // 抛出ReadOnlyBufferException

// 切片(共享底层数据)
buffer.position(4);
buffer.limit(12);
ByteBuffer slice = buffer.slice();
// slice与buffer共享position 4~11的数据

// 复制(独立数据)
ByteBuffer duplicate = buffer.duplicate();

四、ByteBuffer:HeapBuffer vs DirectBuffer

4.1 HeapByteBuffer(堆内缓冲区)

java 复制代码
// 分配在JVM堆内存中
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);

// 特点:
// - 分配和回收速度快(受GC管理)
// - 进行I/O操作时需要复制到堆外(JNI调用)
// - 适合小数据量、频繁创建销毁的场景

4.2 DirectByteBuffer(堆外缓冲区)

java 复制代码
// 分配在操作系统本地内存中
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

// 特点:
// - 分配和回收速度慢(系统调用malloc)
// - I/O操作无需额外复制(直接传递给OS)
// - 不受GC直接管理,通过Cleaner机制回收
// - 适合大数据量、长期使用的I/O场景

4.3 性能对比与选择

java 复制代码
public class BufferBenchmark {
    public static void main(String[] args) throws Exception {
        int size = 1024 * 1024; // 1MB
        int iterations = 1000;

        // HeapBuffer测试
        ByteBuffer heap = ByteBuffer.allocate(size);
        long start = System.nanoTime();
        for (int i = 0; i < iterations; i++) {
            heap.clear();
            // 模拟写入
            for (int j = 0; j < size / 8; j++) {
                heap.putLong(j * 8, j);
            }
        }
        System.out.println("HeapBuffer: " + (System.nanoTime() - start) / 1_000_000 + "ms");

        // DirectBuffer测试
        ByteBuffer direct = ByteBuffer.allocateDirect(size);
        start = System.nanoTime();
        for (int i = 0; i < iterations; i++) {
            direct.clear();
            for (int j = 0; j < size / 8; j++) {
                direct.putLong(j * 8, j);
            }
        }
        System.out.println("DirectBuffer: " + (System.nanoTime() - start) / 1_000_000 + "ms");
    }
}

4.4 DirectBuffer的内存管理

java 复制代码
// DirectBuffer通过sun.misc.Cleaner(JDK8)或jdk.internal.ref.Cleaner(JDK9+)回收
// 如果DirectBuffer对象被GC回收,其对应的本地内存也会被释放

// 手动释放(不推荐,依赖内部API):
// ((sun.nio.ch.DirectBuffer) directBuffer).cleaner().clean();

// JVM参数控制直接内存大小:
// -XX:MaxDirectMemorySize=256m

// 最佳实践:复用DirectBuffer,避免频繁创建

五、Channel体系

5.1 Channel概述

Channel是NIO中数据传输的通道,与Stream的区别:

  • Channel是双向的(可读可写),Stream是单向的
  • Channel可以异步读写
  • Channel读写必须通过Buffer

5.2 FileChannel

java 复制代码
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelDemo {
    public static void main(String[] args) throws Exception {
        // 写入文件
        try (FileChannel writeChannel = new RandomAccessFile("output.txt", "rw").getChannel()) {
            ByteBuffer buffer = ByteBuffer.allocate(256);
            buffer.put("Hello, Java NIO!".getBytes());
            buffer.flip();
            while (buffer.hasRemaining()) {
                writeChannel.write(buffer);
            }
        }

        // 读取文件
        try (FileChannel readChannel = new RandomAccessFile("output.txt", "r").getChannel()) {
            ByteBuffer buffer = ByteBuffer.allocate(256);
            int bytesRead;
            StringBuilder sb = new StringBuilder();
            while ((bytesRead = readChannel.read(buffer)) != -1) {
                buffer.flip();
                while (buffer.hasRemaining()) {
                    sb.append((char) buffer.get());
                }
                buffer.clear();
            }
            System.out.println("文件内容: " + sb);
        }
    }
}

5.3 SocketChannel(TCP客户端)

java 复制代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NioClient {
    public static void main(String[] args) throws Exception {
        SocketChannel channel = SocketChannel.open();
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress("localhost", 8080));

        // 非阻塞连接需要等待连接完成
        while (!channel.finishConnect()) {
            // 可以做其他事情
            Thread.sleep(10);
        }
        System.out.println("连接成功");

        // 发送数据
        ByteBuffer buffer = ByteBuffer.allocate(256);
        buffer.put("Hello Server".getBytes());
        buffer.flip();
        while (buffer.hasRemaining()) {
            channel.write(buffer);
        }

        // 接收数据
        buffer.clear();
        int bytesRead;
        while ((bytesRead = channel.read(buffer)) > 0) {
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
            buffer.clear();
        }

        channel.close();
    }
}

5.4 ServerSocketChannel(TCP服务端)

java 复制代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class NioServer {
    public static void main(String[] args) throws Exception {
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8080));
        serverChannel.configureBlocking(false);
        System.out.println("服务器启动,端口8080");

        while (true) {
            // 非阻塞accept,无连接时返回null
            SocketChannel clientChannel = serverChannel.accept();
            if (clientChannel != null) {
                System.out.println("新连接: " + clientChannel.getRemoteAddress());
                handleClient(clientChannel);
            }
            Thread.sleep(100); // 避免忙等
        }
    }

    private static void handleClient(SocketChannel channel) throws Exception {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead = channel.read(buffer);
        if (bytesRead > 0) {
            buffer.flip();
            byte[] data = new byte[bytesRead];
            buffer.get(data);
            System.out.println("收到: " + new String(data));

            // 回写
            buffer.clear();
            buffer.put(("Echo: " + new String(data)).getBytes());
            buffer.flip();
            channel.write(buffer);
        }
        channel.close();
    }
}

5.5 DatagramChannel(UDP)

java 复制代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;

public class UdpDemo {
    public static void main(String[] args) throws Exception {
        // UDP服务端
        DatagramChannel server = DatagramChannel.open();
        server.bind(new InetSocketAddress(9090));
        server.configureBlocking(false);

        // UDP客户端
        DatagramChannel client = DatagramChannel.open();
        ByteBuffer buffer = ByteBuffer.allocate(256);
        buffer.put("UDP Message".getBytes());
        buffer.flip();
        client.send(buffer, new InetSocketAddress("localhost", 9090));

        // 服务端接收
        buffer.clear();
        java.net.SocketAddress sender = server.receive(buffer);
        if (sender != null) {
            buffer.flip();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            System.out.println("收到来自 " + sender + ": " + new String(data));
        }

        client.close();
        server.close();
    }
}

六、Selector多路复用器

6.1 多路复用原理

Selector底层依赖操作系统的I/O多路复用机制:

机制 平台 特点
select 所有 fd数量限制1024,每次需遍历所有fd,O(n)
poll Linux/Unix 无fd数量限制,仍需遍历,O(n)
epoll Linux 事件驱动,O(1)就绪通知,支持ET/LT模式
kqueue macOS/BSD 类似epoll的事件通知机制

Java NIO在Linux上默认使用epoll(JDK 1.5+),Windows上使用select。

6.2 Selector基本用法

java 复制代码
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class SelectorDemo {
    public static void main(String[] args) throws Exception {
        Selector selector = Selector.open();

        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new java.net.InetSocketAddress(8080));
        serverChannel.configureBlocking(false);

        // 注册Channel到Selector
        SelectionKey key = serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            // select():阻塞直到有事件就绪
            // select(timeout):阻塞最多timeout毫秒
            // selectNow():非阻塞,立即返回
            int readyCount = selector.select();
            if (readyCount == 0) continue;

            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectedKeys.iterator();

            while (iterator.hasNext()) {
                SelectionKey selectedKey = iterator.next();

                if (selectedKey.isAcceptable()) {
                    handleAccept(selectedKey);
                } else if (selectedKey.isReadable()) {
                    handleRead(selectedKey);
                } else if (selectedKey.isWritable()) {
                    handleWrite(selectedKey);
                }

                // 必须手动移除,否则下次select还会返回
                iterator.remove();
            }
        }
    }

    private static void handleAccept(SelectionKey key) throws Exception {
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        SocketChannel client = server.accept();
        client.configureBlocking(false);
        client.register(key.selector(), SelectionKey.OP_READ);
    }

    private static void handleRead(SelectionKey key) throws Exception {
        SocketChannel channel = (SocketChannel) key.channel();
        java.nio.ByteBuffer buffer = java.nio.ByteBuffer.allocate(1024);
        int bytesRead = channel.read(buffer);
        if (bytesRead == -1) {
            channel.close();
            return;
        }
        buffer.flip();
        // 处理数据...
        key.interestOps(SelectionKey.OP_WRITE); // 切换为写事件
    }

    private static void handleWrite(SelectionKey key) throws Exception {
        SocketChannel channel = (SocketChannel) key.channel();
        java.nio.ByteBuffer buffer = java.nio.ByteBuffer.wrap("Response".getBytes());
        channel.write(buffer);
        key.interestOps(SelectionKey.OP_READ); // 切回读事件
    }
}

6.3 Selector的wakeup机制

java 复制代码
// 当一个线程阻塞在select()上时,另一个线程可以调用wakeup()使其立即返回
Selector selector = Selector.open();

// 线程A:阻塞等待
new Thread(() -> {
    try {
        selector.select(); // 阻塞
        System.out.println("select返回");
    } catch (Exception e) {
        e.printStackTrace();
    }
}).start();

// 线程B:唤醒
new Thread(() -> {
    try {
        Thread.sleep(3000);
        selector.wakeup(); // 使select()立即返回
    } catch (Exception e) {
        e.printStackTrace();
    }
}).start();

七、SelectionKey与事件注册

7.1 SelectionKey核心概念

SelectionKey表示Channel与Selector之间的注册关系,包含:

java 复制代码
// 四种事件类型
SelectionKey.OP_ACCEPT  = 1 << 0;  // 1  服务端接受连接
SelectionKey.OP_CONNECT = 1 << 1;  // 2  客户端连接完成
SelectionKey.OP_READ    = 1 << 2;  // 4  通道可读
SelectionKey.OP_WRITE   = 1 << 3;  // 8  通道可写

// SelectionKey的核心方法
key.channel();        // 获取关联的Channel
key.selector();       // 获取关联的Selector
key.interestOps();    // 获取感兴趣的事件集合
key.interestOps(int); // 修改感兴趣的事件
key.readyOps();       // 获取已就绪的事件
key.isValid();        // 是否有效
key.cancel();         // 取消注册
key.attach(Object);   // 附加对象(如Buffer、业务上下文)
key.attachment();     // 获取附加对象

7.2 使用attachment传递上下文

java 复制代码
// 注册时附加Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, buffer);

// 事件处理时获取
public void handleRead(SelectionKey key) throws Exception {
    ByteBuffer buffer = (ByteBuffer) key.attachment();
    SocketChannel channel = (SocketChannel) key.channel();
    buffer.clear();
    int bytesRead = channel.read(buffer);
    if (bytesRead > 0) {
        buffer.flip();
        // 处理数据
    }
}

7.3 事件组合与切换

java 复制代码
// 注册多个事件
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);

// 动态切换事件
if (writeBuffer.hasRemaining()) {
    key.interestOps(SelectionKey.OP_WRITE); // 数据未写完,关注写事件
} else {
    key.interestOps(SelectionKey.OP_READ);  // 写完了,切回读事件
}

// 判断就绪事件
if (key.isReadable()) { /* ... */ }
if (key.isWritable()) { /* ... */ }
if (key.isAcceptable()) { /* ... */ }
if (key.isConnectable()) { /* ... */ }

八、NIO网络编程(完整非阻塞示例)

8.1 非阻塞NIO服务器

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NioReactorServer {
    private Selector selector;
    private ServerSocketChannel serverChannel;

    public NioReactorServer(int port) throws IOException {
        selector = Selector.open();
        serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(port));
        serverChannel.configureBlocking(false);
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO Server started on port " + port);
    }

    public void start() throws IOException {
        while (true) {
            int readyCount = selector.select(1000);
            if (readyCount == 0) continue;

            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iter = keys.iterator();

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

                try {
                    if (!key.isValid()) continue;

                    if (key.isAcceptable()) {
                        doAccept(key);
                    } else if (key.isReadable()) {
                        doRead(key);
                    } else if (key.isWritable()) {
                        doWrite(key);
                    }
                } catch (IOException e) {
                    key.cancel();
                    key.channel().close();
                }
            }
        }
    }

    private void doAccept(SelectionKey key) throws IOException {
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        SocketChannel client = server.accept();
        if (client == null) return;

        client.configureBlocking(false);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        client.register(selector, SelectionKey.OP_READ, buffer);
        System.out.println("Client connected: " + client.getRemoteAddress());
    }

    private void doRead(SelectionKey key) throws IOException {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.clear();

        int bytesRead = client.read(buffer);
        if (bytesRead == -1) {
            System.out.println("Client disconnected: " + client.getRemoteAddress());
            client.close();
            key.cancel();
            return;
        }

        if (bytesRead > 0) {
            buffer.flip();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            String message = new String(data).trim();
            System.out.println("Received: " + message);

            // 准备回写数据
            String response = "Server Echo: " + message + "\n";
            ByteBuffer writeBuffer = ByteBuffer.wrap(response.getBytes());
            key.attach(writeBuffer);
            key.interestOps(SelectionKey.OP_WRITE);
        }
    }

    private void doWrite(SelectionKey key) throws IOException {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        while (buffer.hasRemaining()) {
            if (client.write(buffer) == 0) break;
        }

        if (!buffer.hasRemaining()) {
            // 写完了,切回读事件,重新附加读Buffer
            key.attach(ByteBuffer.allocate(1024));
            key.interestOps(SelectionKey.OP_READ);
        }
    }

    public static void main(String[] args) throws IOException {
        new NioReactorServer(8080).start();
    }
}

8.2 非阻塞NIO客户端

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;

public class NioReactorClient {
    private Selector selector;
    private SocketChannel channel;

    public NioReactorClient(String host, int port) throws IOException {
        selector = Selector.open();
        channel = SocketChannel.open();
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress(host, port));
        channel.register(selector, SelectionKey.OP_CONNECT);
    }

    public void start() throws IOException {
        // 输入线程
        new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String msg = scanner.nextLine();
                try {
                    ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                    while (buffer.hasRemaining()) {
                        channel.write(buffer);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 事件循环
        while (true) {
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iter = keys.iterator();

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

                if (key.isConnectable()) {
                    SocketChannel ch = (SocketChannel) key.channel();
                    if (ch.finishConnect()) {
                        System.out.println("Connected to server");
                        key.interestOps(SelectionKey.OP_READ);
                    }
                } else if (key.isReadable()) {
                    SocketChannel ch = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int bytesRead = ch.read(buffer);
                    if (bytesRead > 0) {
                        buffer.flip();
                        System.out.println("Server: " + new String(buffer.array(), 0, bytesRead));
                    } else if (bytesRead == -1) {
                        ch.close();
                        return;
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        new NioReactorClient("localhost", 8080).start();
    }
}

九、零拷贝技术

9.1 传统I/O的数据拷贝

传统文件传输(read + write)需要4次拷贝 + 4次上下文切换:

复制代码
磁盘 → 内核缓冲区 → 用户空间Buffer → Socket缓冲区 → 网卡
 (DMA)    (CPU)         (CPU)          (DMA)

9.2 transferTo(sendfile)

java 复制代码
import java.io.RandomAccessFile;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;

public class ZeroCopySend {
    public static void main(String[] args) throws Exception {
        // 使用transferTo实现零拷贝文件传输
        try (FileChannel fileChannel = new RandomAccessFile("large-file.dat", "r").getChannel();
             SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080))) {

            long fileSize = fileChannel.size();
            long transferred = 0;

            // 数据直接从文件描述符传输到Socket,不经过用户空间
            while (transferred < fileSize) {
                transferred += fileChannel.transferTo(transferred, fileSize - transferred, socketChannel);
            }

            System.out.println("传输完成: " + transferred + " bytes");
        }
    }
}

9.3 transferFrom

java 复制代码
import java.io.RandomAccessFile;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;

public class ZeroCopyReceive {
    public static void main(String[] args) throws Exception {
        try (FileChannel fileChannel = new RandomAccessFile("received.dat", "rw").getChannel();
             SocketChannel socketChannel = SocketChannel.open()) {

            socketChannel.bind(new InetSocketAddress(8080));
            // 实际场景中通过ServerSocketChannel.accept()获取

            long position = 0;
            long count = 1024 * 1024; // 1MB
            long transferred;

            // 从Socket直接写入文件,不经过用户空间
            while ((transferred = fileChannel.transferFrom(socketChannel, position, count)) > 0) {
                position += transferred;
            }

            System.out.println("接收完成: " + position + " bytes");
        }
    }
}

9.4 mmap内存映射(MappedByteBuffer)

java 复制代码
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MmapDemo {
    public static void main(String[] args) throws Exception {
        try (RandomAccessFile raf = new RandomAccessFile("mmap-test.dat", "rw");
             FileChannel channel = raf.getChannel()) {

            long fileSize = 1024 * 1024; // 1MB
            raf.setLength(fileSize);

            // 创建内存映射
            MappedByteBuffer mappedBuffer = channel.map(
                FileChannel.MapMode.READ_WRITE, 0, fileSize);

            // 像操作内存一样操作文件
            mappedBuffer.putInt(0, 42);
            mappedBuffer.putLong(4, System.currentTimeMillis());

            // 读取
            int value = mappedBuffer.getInt(0);
            long timestamp = mappedBuffer.getLong(4);
            System.out.println("Value: " + value + ", Time: " + timestamp);

            // 强制刷盘
            mappedBuffer.force();

            // 注意:MappedByteBuffer没有显式close方法
            // 依赖GC回收,或通过反射调用Cleaner
        }
    }
}

9.5 零拷贝对比

方式 拷贝次数 上下文切换 适用场景
传统read+write 4次 4次 通用
transferTo (sendfile) 2次(DMA) 2次 文件→网络
transferTo + DMA gather 0次CPU拷贝 2次 文件→网络(Linux 2.4+)
mmap 1次(DMA) 4次 文件读写、随机访问

十、MappedByteBuffer内存映射文件

10.1 三种映射模式

java 复制代码
FileChannel.MapMode.READ_ONLY   // 只读,尝试写入抛NonWritableChannelException
FileChannel.MapMode.READ_WRITE  // 读写,修改会写回文件
FileChannel.MapMode.PRIVATE     // 写时复制,修改不影响原文件

10.2 大文件处理

java 复制代码
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class LargeFileProcessor {
    private static final long CHUNK_SIZE = Integer.MAX_VALUE; // 单次映射最大约2GB

    public static void processLargeFile(String filePath) throws Exception {
        try (RandomAccessFile raf = new RandomAccessFile(filePath, "r");
             FileChannel channel = raf.getChannel()) {

            long fileSize = channel.size();
            long position = 0;

            while (position < fileSize) {
                long remaining = fileSize - position;
                long mapSize = Math.min(remaining, CHUNK_SIZE);

                MappedByteBuffer buffer = channel.map(
                    FileChannel.MapMode.READ_ONLY, position, mapSize);

                // 处理当前块
                processChunk(buffer);

                position += mapSize;
                // buffer由GC回收释放映射
            }
        }
    }

    private static void processChunk(MappedByteBuffer buffer) {
        // 处理数据块
        while (buffer.hasRemaining()) {
            byte b = buffer.get();
            // 业务逻辑...
        }
    }
}

10.3 MappedByteBuffer的注意事项

java 复制代码
// 1. 映射后文件被删除/截断,访问会抛IOException
// 2. 没有显式unmap方法,依赖GC(可能导致内存泄漏)
// 3. JDK9+可使用sun.misc.Unsafe或Cleaner手动释放:

// JDK9+ 释放方式(不推荐生产使用):
// sun.misc.Unsafe unsafe = ...;
// unsafe.invokeCleaner(mappedByteBuffer);

// 4. 映射区域大小受限于Integer.MAX_VALUE(约2GB)
// 5. 频繁创建小映射性能差,应复用或批量映射

十一、Scatter/Gather操作

11.1 Scatter(分散读取)

将Channel中的数据分散到多个Buffer中:

java 复制代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class ScatterDemo {
    public static void main(String[] args) throws Exception {
        ServerSocketChannel server = ServerSocketChannel.open();
        server.bind(new InetSocketAddress(8080));
        SocketChannel client = server.accept();

        // 定义协议:前4字节为消息长度,接下来为消息头,最后为消息体
        ByteBuffer headerBuffer = ByteBuffer.allocate(4);
        ByteBuffer bodyBuffer = ByteBuffer.allocate(1024);

        ByteBuffer[] buffers = {headerBuffer, bodyBuffer};

        // Scatter读取:先填满headerBuffer,再填bodyBuffer
        long bytesRead = client.read(buffers);

        headerBuffer.flip();
        int msgLength = headerBuffer.getInt();
        System.out.println("消息长度: " + msgLength);

        bodyBuffer.flip();
        byte[] body = new byte[bodyBuffer.remaining()];
        bodyBuffer.get(body);
        System.out.println("消息体: " + new String(body));

        client.close();
        server.close();
    }
}

11.2 Gather(聚集写入)

将多个Buffer中的数据聚集写入Channel:

java 复制代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class GatherDemo {
    public static void main(String[] args) throws Exception {
        SocketChannel channel = SocketChannel.open(new InetSocketAddress("localhost", 8080));

        // 构造响应:状态码 + 消息头 + 消息体
        ByteBuffer statusBuffer = ByteBuffer.allocate(4);
        statusBuffer.putInt(200);
        statusBuffer.flip();

        ByteBuffer headerBuffer = ByteBuffer.wrap("Content-Type: text/plain\r\n".getBytes());
        ByteBuffer bodyBuffer = ByteBuffer.wrap("Hello, Scatter/Gather!".getBytes());

        ByteBuffer[] buffers = {statusBuffer, headerBuffer, bodyBuffer};

        // Gather写入:按顺序将多个Buffer写入Channel
        long bytesWritten = channel.write(buffers);
        System.out.println("写入字节数: " + bytesWritten);

        channel.close();
    }
}

11.3 带偏移量的Scatter/Gather

java 复制代码
// 只操作Buffer数组的一部分
ByteBuffer[] buffers = new ByteBuffer[5];
// ... 初始化各buffer

// 从第2个buffer开始,操作3个buffer
long bytesRead = channel.read(buffers, 1, 3);
long bytesWritten = channel.write(buffers, 1, 3);

十二、FileChannel文件锁

12.1 文件锁概述

文件锁用于多进程/多线程间对文件的并发访问控制:

  • 排他锁(Exclusive Lock):写锁,其他进程不能读写
  • 共享锁(Shared Lock):读锁,其他进程可以读但不能写

12.2 文件锁使用示例

java 复制代码
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class FileLockDemo {
    public static void main(String[] args) throws Exception {
        // 排他锁(写锁)
        try (RandomAccessFile raf = new RandomAccessFile("locked-file.txt", "rw");
             FileChannel channel = raf.getChannel()) {

            // 获取整个文件的排他锁(阻塞)
            FileLock lock = channel.lock();
            try {
                System.out.println("获得排他锁,开始写入...");
                channel.write(java.nio.ByteBuffer.wrap("Locked data".getBytes()));
                Thread.sleep(5000); // 模拟长时间操作
            } finally {
                lock.release();
                System.out.println("释放排他锁");
            }
        }

        // 共享锁(读锁)
        try (RandomAccessFile raf = new RandomAccessFile("locked-file.txt", "r");
             FileChannel channel = raf.getChannel()) {

            // 获取共享锁
            FileLock sharedLock = channel.lock(0, Long.MAX_VALUE, true);
            try {
                System.out.println("获得共享锁,开始读取...");
                java.nio.ByteBuffer buffer = java.nio.ByteBuffer.allocate(256);
                channel.read(buffer);
                buffer.flip();
                // 读取数据...
            } finally {
                sharedLock.release();
            }
        }
    }
}

12.3 非阻塞锁与区域锁

java 复制代码
// 非阻塞尝试获取锁
FileLock lock = channel.tryLock();
if (lock == null) {
    System.out.println("无法获取锁,其他进程正在使用");
} else {
    try {
        // 操作文件
    } finally {
        lock.release();
    }
}

// 区域锁:只锁定文件的一部分
// 参数:position, size, shared
FileLock regionLock = channel.lock(0, 1024, false); // 锁定前1024字节

// 检查锁是否有效
boolean valid = lock.isValid();
// 检查是否为共享锁
boolean isShared = lock.isShared();

12.4 文件锁注意事项

  • 文件锁是进程级别的(JVM级别),同一JVM内多线程不互斥
  • 在Windows上,文件被锁定时不能删除
  • 锁的粒度是文件区域,不同区域可以分别加锁
  • FileLock实现了AutoCloseable,可用try-with-resources(JDK 7+)
  • 网络文件系统(NFS)上文件锁可能不可靠

十三、Reactor模式

13.1 单Reactor单线程模型

复制代码
┌────────────────────────────────────┐
│           Reactor线程               │
│  ┌──────────┐    ┌──────────────┐  │
│  │ Selector │───→│ 事件分发器    │  │
│  │ (select) │    │ (dispatch)   │  │
│  └──────────┘    └──────┬───────┘  │
│                         │          │
│              ┌──────────┼────────┐ │
│              ↓          ↓        ↓ │
│         [Accept]    [Read]   [Write]│
│         Handler    Handler  Handler│
└────────────────────────────────────┘

所有I/O操作和业务处理在同一线程,适合低并发场景。

13.2 单Reactor多线程模型

复制代码
┌──────────────────────────────────────────┐
│              Reactor线程                   │
│  Selector → Dispatch → Handler           │
└──────────────────┬───────────────────────┘
                   │ 业务处理交给线程池
                   ↓
┌──────────────────────────────────────────┐
│          Worker Thread Pool               │
│  [Thread-1] [Thread-2] [Thread-3] ...    │
└──────────────────────────────────────────┘

13.3 主从Reactor多线程模型(Netty采用)

复制代码
┌─────────────────────┐
│   MainReactor       │  ← 只负责Accept
│   (Boss Group)      │
│   Selector          │
└─────────┬───────────┘
          │ 新连接分发
          ↓
┌─────────────────────────────────────────┐
│         SubReactor Pool                  │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐   │
│  │SubReactor│ │SubReactor│ │SubReactor│  │ ← 负责Read/Write
│  │(Worker-1)│ │(Worker-2)│ │(Worker-3)│  │
│  └────┬────┘ └────┬────┘ └────┬────┘   │
└───────┼────────────┼────────────┼────────┘
        ↓            ↓            ↓
┌─────────────────────────────────────────┐
│         Business Thread Pool             │  ← 业务逻辑处理
└─────────────────────────────────────────┘

13.4 主从Reactor代码实现

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainSubReactorServer {
    private final int port;
    private final ExecutorService bossPool = Executors.newSingleThreadExecutor();
    private final ExecutorService workerPool = Executors.newFixedThreadPool(
        Runtime.getRuntime().availableProcessors());

    public MainSubReactorServer(int port) {
        this.port = port;
    }

    public void start() throws IOException {
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(port));
        serverChannel.configureBlocking(false);

        Selector bossSelector = Selector.open();
        serverChannel.register(bossSelector, SelectionKey.OP_ACCEPT);
        System.out.println("Server started on port " + port);

        // Boss线程:只处理Accept
        bossPool.submit(() -> {
            try {
                while (true) {
                    bossSelector.select();
                    Set<SelectionKey> keys = bossSelector.selectedKeys();
                    Iterator<SelectionKey> iter = keys.iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        if (key.isAcceptable()) {
                            SocketChannel client = serverChannel.accept();
                            if (client != null) {
                                client.configureBlocking(false);
                                // 将新连接分配给Worker
                                workerPool.submit(new WorkerHandler(client));
                            }
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

    // Worker:处理已建立连接的I/O
    static class WorkerHandler implements Runnable {
        private final SocketChannel channel;
        private final Selector selector;

        WorkerHandler(SocketChannel channel) throws IOException {
            this.channel = channel;
            this.selector = Selector.open();
            channel.register(selector, SelectionKey.OP_READ);
        }

        @Override
        public void run() {
            try {
                while (true) {
                    selector.select();
                    Set<SelectionKey> keys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = keys.iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        if (key.isReadable()) {
                            handleRead(key);
                        }
                    }
                }
            } catch (IOException e) {
                try { channel.close(); } catch (IOException ignored) {}
            }
        }

        private void handleRead(SelectionKey key) throws IOException {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int bytesRead = channel.read(buffer);
            if (bytesRead == -1) {
                channel.close();
                return;
            }
            buffer.flip();
            // 处理业务逻辑并回写
            channel.write(buffer);
        }
    }

    public static void main(String[] args) throws IOException {
        new MainSubReactorServer(8080).start();
    }
}

十四、NIO vs BIO性能对比

14.1 并发连接数对比

java 复制代码
// BIO服务器:每个连接一个线程
public class BioServer {
    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(8080);
        ExecutorService pool = Executors.newFixedThreadPool(200); // 线程数受限

        while (true) {
            Socket socket = server.accept(); // 阻塞
            pool.submit(() -> {
                try {
                    InputStream in = socket.getInputStream();
                    OutputStream out = socket.getOutputStream();
                    byte[] buf = new byte[1024];
                    int len;
                    while ((len = in.read(buf)) != -1) { // 阻塞读
                        out.write(buf, 0, len);
                        out.flush();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

14.2 性能指标对比

指标 BIO NIO
1000并发连接 需1000线程,内存约1GB 1-2个线程即可
线程切换开销 高(上下文切换频繁) 极低
连接空闲时 线程阻塞等待,浪费资源 Selector统一管理
吞吐量 受线程数限制 受CPU和带宽限制
编程复杂度 简单直观 较复杂(状态管理)
适用场景 连接数少、数据量大 连接数多、短请求

14.3 选择建议

复制代码
连接数 < 1000 且 请求数据量大 → BIO(简单可靠)
连接数 > 1000 且 请求频繁短小 → NIO(高并发)
需要文件I/O高性能           → NIO(零拷贝、mmap)
需要跨平台简单I/O          → BIO(兼容性好)

十五、Netty对NIO的封装简介

15.1 Netty解决的问题

原生NIO的痛点:

  • API复杂,开发效率低
  • 粘包/拆包需要自行处理
  • 断线重连、心跳检测需手动实现
  • Selector空轮询Bug(epoll bug导致CPU 100%)
  • 内存管理复杂(DirectBuffer泄漏)

15.2 Netty架构概览

复制代码
┌─────────────────────────────────────────┐
│              Netty Application           │
├─────────────────────────────────────────┤
│  ChannelHandler (业务逻辑)               │
├─────────────────────────────────────────┤
│  ChannelPipeline (责任链)                │
├─────────────────────────────────────────┤
│  Codec (编解码:粘包/拆包/协议)          │
├─────────────────────────────────────────┤
│  EventLoop (线程模型:主从Reactor)       │
├─────────────────────────────────────────┤
│  Bootstrap (启动引导)                    │
├─────────────────────────────────────────┤
│  Java NIO / Epoll / KQueue              │
└─────────────────────────────────────────┘

15.3 Netty服务端示例

java 复制代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class NettyServer {
    public static void main(String[] args) throws Exception {
        // Boss线程组:处理Accept
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // Worker线程组:处理I/O
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 128)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childOption(ChannelOption.TCP_NODELAY, true)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new StringEncoder());
                        pipeline.addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                                System.out.println("Received: " + msg);
                                ctx.writeAndFlush("Echo: " + msg + "\n");
                            }

                            @Override
                            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
                                cause.printStackTrace();
                                ctx.close();
                            }
                        });
                    }
                });

            ChannelFuture future = bootstrap.bind(8080).sync();
            System.out.println("Netty Server started on port 8080");
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

15.4 Netty核心组件映射

Netty组件 对应NIO概念 增强
EventLoopGroup Selector + Thread 线程池管理
Channel SocketChannel 统一抽象
ChannelPipeline - 责任链处理
ByteBuf ByteBuffer 读写分离、池化
ChannelFuture - 异步回调
Bootstrap - 流式配置

十六、最佳实践与常见陷阱

16.1 Buffer使用最佳实践

java 复制代码
// 1. 始终检查write返回值(可能写入0字节)
int written = channel.write(buffer);
if (written == 0) {
    // Socket发送缓冲区满,注册OP_WRITE等待可写
    key.interestOps(SelectionKey.OP_WRITE);
}

// 2. 使用Buffer池避免频繁分配
public class BufferPool {
    private static final ThreadLocal<ByteBuffer> BUFFER_POOL =
        ThreadLocal.withInitial(() -> ByteBuffer.allocateDirect(4096));

    public static ByteBuffer getBuffer() {
        ByteBuffer buffer = BUFFER_POOL.get();
        buffer.clear();
        return buffer;
    }
}

// 3. 批量写入减少系统调用
ByteBuffer[] buffers = {header, body};
while (true) {
    long written = channel.write(buffers);
    if (written == 0) break;
    if (!hasRemaining(buffers)) break;
}

16.2 Selector使用陷阱

java 复制代码
// 陷阱1:必须手动remove已处理的SelectionKey
// 否则下次select()还会返回相同的key
while (iter.hasNext()) {
    SelectionKey key = iter.next();
    iter.remove(); // 必须!
    // 处理key...
}

// 陷阱2:Selector空轮询Bug(JDK epoll bug)
// 现象:select()在没有就绪事件时立即返回,导致CPU 100%
// 解决方案(Netty的做法):
int emptySelectCount = 0;
while (true) {
    long beforeSelect = System.nanoTime();
    int count = selector.select(1000);
    long selectTime = System.nanoTime() - beforeSelect;

    if (count == 0 && selectTime < 500_000) { // 小于0.5ms就返回
        emptySelectCount++;
        if (emptySelectCount >= 512) {
            // 重建Selector
            Selector newSelector = Selector.open();
            for (SelectionKey key : selector.keys()) {
                key.channel().register(newSelector, key.interestOps(), key.attachment());
            }
            selector.close();
            selector = newSelector;
            emptySelectCount = 0;
        }
    } else {
        emptySelectCount = 0;
    }
}

// 陷阱3:在select()之前注册新Channel
// 正确做法:使用wakeup() + 队列
private final Queue<Runnable> pendingRegistrations = new ConcurrentLinkedQueue<>();

public void register(SocketChannel channel, int ops) {
    pendingRegistrations.add(() -> {
        try {
            channel.register(selector, ops);
        } catch (ClosedChannelException e) {
            e.printStackTrace();
        }
    });
    selector.wakeup();
}

// 在事件循环中处理
while (true) {
    selector.select(1000);
    Runnable task;
    while ((task = pendingRegistrations.poll()) != null) {
        task.run();
    }
    // 处理就绪事件...
}

16.3 Channel使用注意事项

java 复制代码
// 1. 非阻塞模式下read可能返回0(无数据可读)
int bytesRead = channel.read(buffer);
if (bytesRead == 0) {
    // 正常情况,不是错误
    return;
}
if (bytesRead == -1) {
    // 对端关闭连接
    channel.close();
}

// 2. 非阻塞connect需要finishConnect
channel.configureBlocking(false);
channel.connect(address);
// 不能立即读写,需要等待连接完成
while (!channel.finishConnect()) {
    // 等待或做其他事
}

// 3. 关闭Channel时注意顺序
// 先取消SelectionKey,再关闭Channel
key.cancel();
channel.close();
selector.wakeup(); // 确保cancel生效

// 4. FileChannel是阻塞的(不能设为非阻塞)
// 但可以通过线程池实现异步文件I/O

16.4 内存管理最佳实践

java 复制代码
// 1. DirectBuffer要复用,不要频繁创建
// 错误示范:
void handleRequest() {
    ByteBuffer buf = ByteBuffer.allocateDirect(4096); // 每次请求都分配!
    // ...
}

// 正确示范:使用池化
// Netty的PooledByteBufAllocator就是解决方案

// 2. 设置合理的MaxDirectMemorySize
// -XX:MaxDirectMemorySize=512m
// 默认等于-Xmx,但容器环境可能不准确

// 3. 监控直接内存使用
// JMX: java.nio:type=BufferPool,name=direct
// 或 -XX:NativeMemoryTracking=detail + jcmd

// 4. 避免MappedByteBuffer泄漏
// 映射后不要持有引用过久
// 大文件分块映射处理

16.5 常见异常与解决

异常 原因 解决方案
BufferOverflowException 写入超过limit 检查capacity,扩容或分批写
BufferUnderflowException 读取超过limit 检查remaining()
ReadOnlyBufferException 写入只读Buffer 使用duplicate()或asReadOnlyBuffer()前检查
ClosedChannelException Channel已关闭 检查isOpen(),处理并发关闭
CancelledKeyException SelectionKey已取消 检查isValid()
NonWritableChannelException 以只读模式打开 检查打开模式
OutOfMemoryError: Direct buffer memory 直接内存不足 增大MaxDirectMemorySize或修复泄漏

16.6 生产环境检查清单

复制代码
□ 所有Channel和Selector在finally/try-with-resources中关闭
□ SelectionKey在迭代器中手动remove
□ 处理read返回0和-1的情况
□ 非阻塞connect后调用finishConnect
□ write返回0时注册OP_WRITE而非忙等
□ DirectBuffer复用或使用池化
□ 设置合理的Socket缓冲区(SO_RCVBUF/SO_SNDBUF)
□ 处理Selector空轮询Bug(重建Selector)
□ 新Channel注册通过wakeup+队列,避免并发问题
□ 设置TCP_NODELAY减少延迟(小消息场景)
□ 设置SO_KEEPALIVE检测死连接
□ 业务逻辑不在I/O线程执行(交给业务线程池)
□ 设置读写超时(通过ScheduledExecutor定时检测)
□ 监控Selector管理的连接数和就绪事件频率

十七、NIO.2补充(JDK 7+)

17.1 Path与Files工具类

java 复制代码
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class Nio2Demo {
    public static void main(String[] args) throws Exception {
        // Path操作
        Path path = Paths.get("E:/data", "test.txt");
        System.out.println("文件名: " + path.getFileName());
        System.out.println("父目录: " + path.getParent());
        System.out.println("绝对路径: " + path.toAbsolutePath());

        // Files工具类
        // 读取所有行
        List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);

        // 写入
        Files.write(path, "Hello NIO.2".getBytes(), StandardOpenOption.CREATE);

        // 复制
        Files.copy(Paths.get("source.txt"), Paths.get("target.txt"),
            StandardCopyOption.REPLACE_EXISTING);

        // 遍历目录
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get("."), "*.java")) {
            for (Path entry : stream) {
                System.out.println(entry);
            }
        }

        // WatchService文件监控
        WatchService watcher = FileSystems.getDefault().newWatchService();
        Paths.get(".").register(watcher,
            StandardWatchEventKinds.ENTRY_CREATE,
            StandardWatchEventKinds.ENTRY_MODIFY,
            StandardWatchEventKinds.ENTRY_DELETE);

        WatchKey key = watcher.take(); // 阻塞等待事件
        for (WatchEvent<?> event : key.pollEvents()) {
            System.out.println(event.kind() + ": " + event.context());
        }
        key.reset();
    }
}

17.2 AsynchronousFileChannel(异步文件I/O)

java 复制代码
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.*;
import java.util.concurrent.Future;

public class AsyncFileDemo {
    public static void main(String[] args) throws Exception {
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(
            Paths.get("async-test.txt"),
            StandardOpenOption.READ, StandardOpenOption.WRITE,
            StandardOpenOption.CREATE);

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        // 方式1:Future
        Future<Integer> writeFuture = channel.write(
            ByteBuffer.wrap("Async write".getBytes()), 0);
        int written = writeFuture.get(); // 阻塞等待完成
        System.out.println("写入: " + written + " bytes");

        // 方式2:CompletionHandler回调
        buffer.clear();
        channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                attachment.flip();
                byte[] data = new byte[attachment.remaining()];
                attachment.get(data);
                System.out.println("异步读取: " + new String(data));
            }

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

        Thread.sleep(1000); // 等待回调完成
        channel.close();
    }
}

十八、总结

18.1 NIO核心知识体系

复制代码
Java NIO
├── Buffer(数据容器)
│   ├── 四大属性:capacity/limit/position/mark
│   ├── 状态切换:flip/clear/compact/rewind
│   └── 类型:Heap/Direct/ReadOnly/Slice
├── Channel(传输通道)
│   ├── FileChannel(文件)
│   ├── SocketChannel(TCP客户端)
│   ├── ServerSocketChannel(TCP服务端)
│   ├── DatagramChannel(UDP)
│   └── AsynchronousChannel(NIO.2异步)
├── Selector(多路复用)
│   ├── 底层:select/poll/epoll/kqueue
│   ├── SelectionKey事件管理
│   └── wakeup/注册队列
├── 高级特性
│   ├── 零拷贝:transferTo/transferFrom/mmap
│   ├── Scatter/Gather
│   ├── FileLock文件锁
│   └── MappedByteBuffer内存映射
├── 设计模式
│   └── Reactor(单Reactor/主从Reactor)
└── 框架封装
    └── Netty(EventLoop/Pipeline/ByteBuf)

18.2 面试高频问题

  1. NIO为什么比BIO快? ------ 非阻塞+多路复用减少线程数和上下文切换
  2. epoll比select好在哪? ------ O(1)事件通知、无fd数量限制、支持ET模式
  3. DirectBuffer什么时候用? ------ 大I/O、长生命周期、减少JNI拷贝
  4. 零拷贝原理? ------ 避免用户态和内核态之间的数据复制
  5. Reactor模式核心思想? ------ I/O事件驱动 + 事件分发 + 非阻塞处理
  6. Netty解决了NIO哪些问题? ------ 空轮询Bug、粘包拆包、内存池化、线程模型
  7. Selector线程安全吗? ------ 不是,注册/取消需要在Selector线程或通过wakeup协调
相关推荐
The Chosen One9851 小时前
高进度算法模板速记(待完善)
java·前端·算法
泡沫冰@2 小时前
ECS 的介绍和使用
linux·服务器·网络
笨鸟先飞,勤能补拙2 小时前
AI 赋能网络安全领域深度剖析
网络·人工智能·windows·安全·web安全·网络安全·github
极光代码工作室4 小时前
基于SpringBoot的课程预约系统
java·springboot·web开发·后端开发
Leighteen4 小时前
`try-finally` 里的 `return`:为什么 `finally` 会悄悄改掉返回值、吞掉异常
java·开发语言
名字还没想好☜5 小时前
Go 的 time.After 在 select 循环里内存泄漏:定时器堆积原理与 timer.Reset 正确姿势
java·数据库·golang·go·goroutine
中微极客5 小时前
多智能体编排实战:CrewAI vs AutoGen(2026版)
大数据·网络·人工智能
小王C语言5 小时前
【3. 基于 Vibe Coding 的 OJ 平台】. 构建仓库、环境准备、需求梳理、安装依赖
网络·c++
圆山猫5 小时前
[Virtualization](三):RISC-V H-extension 与 Guest 执行模式
android·java·risc-v