JAVA NIO学习笔记基础强化学习总结

基本概念

通过前文的学习,我们用java.net包下的API实现了同步阻塞IO模型,我们用java.nio包下的API实现了同步非阻塞IO和IO多路复用模型。

Java NIO(New I/O 或者是 none-block IO)是 Java 1.4 引入的非阻塞 I/O 模型,旨在解决传统阻塞 I/O(BIO)在高并发场景下的性能瓶颈。这些API的底层在JVM层面包装的是对一些系统函数的调用,供给开发者使用的API。

包含了三大组件:通道 (Channel) 缓冲区 (Buffer) 选择器(Selector)


Channel

Channel是数据传输的双向通道,与流(InputStream/OutputStream)不同,Channel同时支持读写操作。

ServerSocketChannel和SocketChannel是网络读写的Channel:

ServerSocketChannel底层封装了一个服务端 的Socket,SocketChannel则封装了一个客户端Socket。

FileChannel则代表了文件读写

ServerSocketChannel:服务器连接监听

核心功能

  • 绑定本地端口,监听客户端连接请求
  • 通过accept()方法来接受客户端连接,返回SocketChannel用于后续通信
  • 可以配置非阻塞模式,与Selector配合实现多路复用

用于服务端接收客户端连接使用,相关API如下:

java 复制代码
初始化
工厂模式创建ServerSocketChannel,增强扩展性
ServerSocketChannel serverChannel = ServerSocketChannel.open();
绑定本地端口
serverChannel.bind(new InetSocketAddress(8080));
配置非阻塞模式,默认是阻塞模式
serverChannel.configureBlocking(false);
=================================================================
接收连接
阻塞模式:若无连接请求,线程会一直阻塞直到有连接到达
SocketChannel clientChannel = serverChannel.accept();
非阻塞模式:立即返回,没有连接请求返回null
SocketChannel clientChannel = serverChannel.accpet();
if(clientChannel != null){
    // 通常也将客户端设置为非阻塞模式
    clientChannel.configureBlocking(false);
}
=================================================================
关闭通道 释放资源
serverChannel.close();

SocketChannel:客户端与服务端数据通信

核心功能:

  • 客户端通过connect()连接服务端
  • 服务端通过ServerSocketChannel.accept()返回的SocketChannel与客户端通信
  • 支持双向数据读写
  • 支持非阻塞模式,可以与Selector配合实现异步IO

主要是用于双向通信,相关API如下

java 复制代码
创建与连接
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
boolean connected = clientChannel.connect(new InetSocketAddress("localhost",8080));
// 非阻塞模式下,连接可能未立即完成,需检查finishConnect()
if (!connected) {
    while (!clientChannel.finishConnect()) {
        // 等待连接完成(可在此处理其他任务)
    }
}
===========================================================================
数据读写
写入数据(返回写入的字节数)
ByteBuffer buffer = ByteBuffer.wrap("Hello, Server!".getBytes());
int bytesWritten = clientChannel.write(buffer);
 
读取数据(返回读取的字节数,可能为0)
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(readBuffer);
if (bytesRead > 0) {
    readBuffer.flip();
    byte[] data = new byte[bytesRead];
    readBuffer.get(data);
    System.out.println("Received: " + new String(data));
}
===========================================================================
关闭通道 释放资源
clientChannel.close()
组件 核心功能 关键API
ServerSocketChannel 服务端监听端口;接受客户端连接 open(), bind(), accept(), configureBlocking(false)
SocketChannel 客户端连接服务端;双向数据通信 open(), connect(), read(), write(), finishConnect(), configureBlocking(false)

Buffer

数据缓冲区,也是一个数据容器,在JAVA NIO中数据都是用Buffer存储的。Channel读取接收到的数据先写到Buffer中,应用程序再从Buffer中读取,Channel写数据也是先写到Buffer中。

为什么要使用缓冲区?

为了通过 Buffer 的批量操作,将 多次小数据量系统调用 合并为 单次大数据量系统调用,减少内核态与用户态切换次数,显著提高网络通信的性能。

ByteBuffer是里面最常用的一个类型,因为字节类型最通用传输最高效。

ByteBuffer

创建ByteBuffer

allocate(int capacity)方法

创建的是一个HeapByteBuffer实例,数据是存储在堆区按容量开辟空间的字节数组中。

  • GC 管理:无需手动释放内存,但 GC 可能带来停顿(STW)。
  • 访问速度:Java 对象直接访问,速度快(但受 JVM 堆限制)。
  • 适用场景:大多数常规 I/O 操作,尤其是数据量较小或需要频繁 GC 的场景。

allocateDirect(int capacity)方法

创建的是一个DirectByteBuffer实例,数据是存储在操作系统直接内存中按容量开辟空间的字节数组中。

  • 手动管理 :虽然 DirectByteBuffer 对象本身由 GC 管理,但堆外内存需通过 Cleaner 机制在 GC 时释放(或显式调用 sun.misc.Unsafe.freeMemory())。
  • 访问速度
    • 写入:比堆缓冲区慢(需通过 JNI 调用本地方法写入堆外内存)。
    • 读取:比堆缓冲区快(尤其是涉及 I/O 操作时,可避免数据拷贝)。
  • 适用场景:高频、大容量的 I/O 操作(如网络传输、文件读写),尤其是需要零拷贝的场景。
堆缓冲区(HeapByteBuffer) 直接缓冲区(DirectByteBuffer)
内存分配 JVM 堆内存(GC 管理) 操作系统本地内存(需手动释放)
分配/释放速度 快(纯 Java 操作) 慢(需调用本地方法 malloc/free
I/O 性能 低(额外拷贝) 高(零拷贝优化)
访问速度 快(Java 对象直接访问) 慢(需通过 JNI 访问堆外内存)
内存限制 受 JVM 堆大小限制(-Xmx) 受操作系统可用内存限制(可分配 GB 级)
适用场景 小数据量、频繁 GC 的场景 大数据量、高频 I/O、零拷贝场景
操作ByteBuffer

Buffer类中定义了四个索引,这些索引记录了底层数据元素的状态。

capacity:缓冲区可以容纳的数据元素的最大数量

limit:停止读写的索引

position:当前要读或写的索引,读写操作是从position开始,到limit停止

mark:用于记录position的位置,默认值为-1

flip():由写模式切换为读模式 通过limit = position

compact():由读模式切换为写模式

remaining():计算可读/写大小

操作 触发方法 索引调整 模式变化方向
写模式→读模式 flip() limit = position; position=0 写 → 读(单向)
读模式→写模式 clear() position=0; limit=capacity 读 → 写(完全重置)
读模式→写模式 compact() 移动未读数据;调整position/limit 读 → 写(保留未读数据)
  1. ByteBuffer 中写数据:核心方法是put
  • put(byte[] src)
  • put(byte[] src, int offset, int length)
  • put(ByteBuffer src)
  1. ByteBuffer中读数据:核心方法是 get
  • get(byte[] dst)
  • get(byte[] dst, int offset, int length)

Selector选择器

允许单个线程高效管理多个通道(Channel),实现I/O多路复用,从而显著提升网络应用的性能和可扩展性。

selector底层封装的是IO多路复用模型 epoll ,主要功能是将Channel注册到Selector上并指定要监听的事件,selector会不断轮询注册在其上的Channel,检测其事件是否就绪;Selector每次返回已就绪Channel对应的SelectionKey,通过SelectionKey可获取事件的详细信息,然后进行后续的操作。(单线程通过事件驱动监控多个通道,仅当通道发生指定事件(如可读,可写,新连接)时才处理,避免空轮询和线程切换)

Selector支持四种事件类型,通过SelectionKey的常量定义:

  • OP_ACCEPT:服务端接收新连接
  • OP_CONNECT:客户端完成连接
  • OP_READ:通道数据可读
  • OP_WRITE:通道可写(谨慎使用,避免频繁触发)

使用Selector线程循环需要以下步骤:

  1. 调用 select()阻塞当前线程 等待事件发生
  2. 遍历就绪事件(selectedKeys),分发处理
  3. 避免线程阻塞在 I/O 操作上面,充分利用空闲事件处理其他通道

相关API 如下:

Selector类:

java 复制代码
创建Selector实例,底层调用系统默认的SelectorProvider(如Linux的EPollSelectorProvider)。
Selector.open()
阻塞等待事件,超时返回。返回值为就绪通道数量。
select(long timeout)
返回就绪事件的SelectionKey集合,需手动遍历处理。
selectedKeys()
唤醒阻塞的select(),用于优雅退出事件循环。
wakeup()

SelectionKey类:

java 复制代码
设置通道感兴趣的事件(如OP_READ | OP_WRITE)。
interestOps(int ops)
获取当前就绪的事件(如OP_READ)。
readyOps()
绑定用户对象(如Socket实例),便于事件处理时获取上下文。
attach(Object obj)
检查键是否有效(如通道关闭后键自动失效)。
isValid()

NIO完整案例

根据上述学的三大组件写一个完整的Java NIO 案例

java 复制代码
/**
 * @author XJunFy
 */
public class NioServer {
    public static void main(String[] args) {
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(9999));
            serverSocketChannel.configureBlocking(false);

            Selector selector = Selector.open();
            SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while(true){
                int select = selector.select();
                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                while (keyIterator.hasNext()){
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove();
                    processingKey(key,selector);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }

    private static void processingKey(SelectionKey key, Selector selector) throws IOException {
        if (key.isValid()){
            if (key.isAcceptable()){
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                socketChannel.register(selector,SelectionKey.OP_READ);
                return;
            }
            if (key.isReadable()){
                SocketChannel socketChannel = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int read = socketChannel.read(readBuffer);
                if (read > 0){
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String msg = new String(bytes, Charset.defaultCharset());
                    System.out.println("服务端收到来自客户端的数据:" + msg);
                    ByteBuffer sendBuffer = ByteBuffer.allocate(256);
                    sendBuffer.put("hello nio client,i am nio server\n".getBytes(StandardCharsets.UTF_8));
                    sendBuffer.flip();
                    socketChannel.write(sendBuffer);
                }
                return;
            }
        }

    }
}
相关推荐
lingggggaaaa4 小时前
小迪安全v2023学习笔记(七十九讲)—— 中间件安全&IIS&Apache&Tomcat&Nginx&CVE
笔记·学习·安全·web安全·网络安全·中间件·apache
拾忆,想起4 小时前
Redis复制延迟全解析:从毫秒到秒级的优化实战指南
java·开发语言·数据库·redis·后端·缓存·性能优化
我登哥MVP4 小时前
Java File 类学习笔记
java·笔记·学习
掘根4 小时前
【CMake】缓存变量
java·后端·spring
西京刀客4 小时前
macos安装openjdk17
java·macos·java17
Java中文社群5 小时前
面试官:如何实现动态线程池的任务编排?
java·后端·面试
mysla5 小时前
嵌入式学习day44-硬件—ARM体系架构
学习
lozhyf5 小时前
能发弹幕的简单视频网站
java·spring boot·后端
微露清风5 小时前
系统性学习数据结构-第三讲-栈和队列
java·数据结构·学习