Java NIO到底是个什么东西?

NIO就是Java处理网络IO的另一种方式。就这么简单。

但很多人学NIO学不明白,不是因为它难,是因为教程都在讲错重点。

随便打开一个NIO教程,上来就是:Channel、BufferSelector三大组件。然后画个图,告诉你Buffer怎么flip()、怎么rewind(),Channel怎么读怎么写。你看懂了,然后呢?还是不知道NIO到底解决什么问题。

就像你问"汽车变速箱是干啥的",对方给你讲齿轮比、扭矩转换、行星齿轮组结构。听完了还是一头雾水。

一、NIO解决的核心问题:一个线程怎么处理一万个连接

2005年前后,我司有台服务器,8核16G,跑一个Web服务。高峰期5000个并发连接,服务器就扛不住了。top一看,8000多个线程,CPU全在做上下文切换,真正干活的时间不到20%。

这就是传统BIO(阻塞IO)的问题:一个连接就得占一个线程

你写个Socket服务器,BIO的代码长这样:

复制代码
ServerSocket server = new ServerSocket(8080);
while (true) {
    Socket client = server.accept(); // 阻塞等待连接
    new Thread(() -> {
        // 处理这个客户端的请求
        InputStream in = client.getInputStream();
        byte[] data = new byte[1024];
        int len = in.read(data); // 阻塞等待数据
        // 处理数据...
    }).start();
}

每来一个连接,就new一个Thread。1万个连接就是1万个线程。

问题在哪?

  • 线程本身要占内存(栈空间,默认1MB)
  • 线程切换要消耗CPU(上下文切换、缓存失效)
  • 大部分时间线程都在等(等客户端发数据)

你想想,聊天室服务器,1万个在线用户,99%的时间没人发消息。你给每个用户分配一个线程,这9900个线程都在in.read()那傻等。

阻塞IO的致命问题就是:线程在等IO的时候,啥也干不了

NIO的核心思路:不等。

一个线程管一万个连接。谁有数据处理谁,没数据的别占线程。

二、NIO是怎么做到"不等"的

传统IO是这样的:

复制代码
in.read(data); // 这里会阻塞,直到有数据

你调read(),客户端没发数据的话,线程就卡这了。操作系统把这个线程挂起,等网卡收到数据,再把线程唤醒。

中间线程啥也没干,就在等。

NIO的Channel可以设置成非阻塞:

复制代码
channel.configureBlocking(false);
int n = channel.read(buffer); // 立刻返回,n可能是0

调用read(),如果没数据,立刻返回0,不等。有多少数据读多少,读完了继续干别的。

但这还不够。

如果你写个死循环,一直轮询1万个Channel:

复制代码
while (true) {
    for (Channel ch : channels) {
        int n = ch.read(buffer);
        if (n > 0) {
            // 处理数据
        }
    }
}

这不是更浪费?CPU 100%空转,大部分时间都在查没数据的Channel。

所以Selector出现了。

Selector是操作系统提供的能力(Linux的epoll、Windows的IOCP)。把1万个Channel注册到Selector上,然后调:

复制代码
int n = selector.select(); // 阻塞,直到有Channel准备好

这行代码会阻塞,但任何一个Channel有数据了,立刻返回。你再遍历准备好的Channel,处理它们。

这就是"IO多路复用":一个线程,通过Selector监听1万个Channel,谁准备好了处理谁

三、真实项目里的NIO长什么样

我2010年第一次用NIO写服务器,踩了无数坑。给你看个最简化的代码:

复制代码
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    selector.select(); // 阻塞,直到有事件
    Set<SelectionKey> keys = selector.selectedKeys();
    
    for (SelectionKey key : keys) {
        if (key.isAcceptable()) {
            // 有新连接
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
        } 
        else if (key.isReadable()) {
            // 有数据可读
            SocketChannel client = (SocketChannel) key.channel();
            https://zhida.zhihu.com/search?content_id=768608875&content_type=Answer&match_order=1&q=ByteBuf&zhida_source=entityfer buffer = ByteBuffer.allocate(1024);
            int n = client.read(buffer);
            if (n > 0) {
                // 处理数据
            }
        }
    }
    keys.clear(); // 必须手动清空,否则下次还会处理
}

这就是NIO的完整流程:

  1. 创建Selector
  2. 把ServerSocketChannel注册到Selector上,关注ACCEPT事件(新连接)
  3. 循环调用selector.select(),等待事件
  4. 有新连接了,accept()拿到SocketChannel,设为非阻塞,注册到Selector上,关注READ事件
  5. 某个Channel有数据了,read()读取数据,处理
  6. 处理完继续循环

一个线程,几十行代码,轻松处理几万个连接。

但问题是------敢把这代码上线吗?

我当时就上了,然后炸了。半夜被oncall电话叫醒,服务器CPU打满,处理量暴跌。排查了一夜才发现:某个客户端发了半截数据就断了,我代码里没处理半包,Buffer一直在等完整数据,这个Channel就一直占着key。

还有一次,某个客户端发了个100MB的数据包,我代码里buffer只有1KB,读了几个小时还没读完,其他正常请求全被饿死。

NIO代码看起来简单,生产级的NIO服务器要处理的东西太多:

  • 半包、粘包
  • 异常断线、心跳检测
  • 读写缓冲区管理
  • 流量控制、背压
  • 优雅关闭、资源清理

自己手撸NIO,99%要出问题。

四、学完NIO能自己写服务器吗?别想了

很多人学完NIO,以为自己能写服务器了。醒醒。

Netty是基于NIO的框架,它帮你解决了上面说的所有坑:零拷贝、池化ByteBuf、内存泄漏检测、Reactor线程模型、编解码器链、粘包半包处理、背压流控、优雅关闭。

哪一样都不简单。

我们2013年从自己手撸的NIO改成Netty,代码从2000行变成200行。稳定性?以前三天两头出问题,改完之后半年没出过事故。

学NIO不是让你自己写服务器,是让你看懂Netty底层在干什么。Netty的EventLoop就是在跑selector.select()循环,ChannelHandler就是在处理SelectionKey的回调。理解了NIO,Netty的源码就不神秘了。

五、NIO性能一定比BIO好?不一定

看场景。

短连接、请求立刻响应的(比如HTTP API),BIO可能更合适。每个请求处理10ms,配合线程池(比如Tomcat默认200个线程),够用了。NIO的优势根本发挥不出来。

NIO适合长连接、大量空闲连接的场景:

  • WebSocket服务器(几万个在线用户,大部分时间没在发消息)
  • 游戏服务器(玩家在线但没操作)
  • 消息推送服务(大量连接在等推送)
  • RPC框架(连接池里的连接大部分时间空闲)

这些场景用BIO,光线程就占死了。用NIO,一个线程轻松扛几万连接。

但NIO有代价:代码复杂度高。BIO用同步代码就能写完,NIO要拆成异步回调。可读性、可维护性都差。

所以才会出现各种框架:Netty、Vert.x、Mina,本质都是在NIO上封装一套好用点的API。

六、但2026年了,虚拟线程来了

聊了半天NIO,现在得说个扎心的事:Java 21的虚拟线程,把上面这套理论全颠覆了

去年我们把一个WebSocket服务从Netty改成虚拟线程+BIO,代码量少了一半,性能没降反升。当时都怀疑是不是测错了,反复压测了三天,确认没问题。

虚拟线程是什么?JVM自己实现的协程,让阻塞式代码可以承载百万级并发。

传统线程(平台线程)一个对应一个操作系统线程,创建1万个就炸了。虚拟线程是JVM管的,创建100万个都行。虚拟线程阻塞的时候,JVM自动把它从平台线程上摘下来,换另一个虚拟线程上去跑。

然后你继续写阻塞式代码就行:

复制代码
// Java 21 + 虚拟线程
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    ServerSocket server = new ServerSocket(8080);
    while (true) {
        Socket client = server.accept();
        executor.submit(() -> {
            var in = client.getInputStream();
            var data = in.read(); // 阻塞,但不占平台线程
            // 处理数据...
        });
    }
}

看起来和2005年的BIO代码一模一样,但现在轻松扛住10万个连接。这10万个虚拟线程,底层只用了几个平台线程。

去年双11压测,推送服务20万在线连接,8核16G服务器CPU占用不到30%。以前用Netty的时候,为了优化这30%,代码里全是回调套回调。现在虚拟线程,代码就是最直白的同步写法,性能反而更好。

那NIO是不是没用了?

不是。底层框架(Netty、Dubbo、gRPC)还是基于NIO的。虚拟线程虽然轻量,但调度开销比不过epoll直接通知。极致性能场景(消息队列、网关),NIO还是最优解。

但业务代码------写个微服务接口、搞个WebSocket聊天室、做个实时推送------虚拟线程够了。不用学NIO,不用学Netty,写最简单的阻塞代码就行。

现在给新人培训,我都让他们先用虚拟线程写业务,碰到性能瓶颈再说NIO。实际上99%的业务根本到不了需要NIO的地步。

所以2026年,学NIO还有意义吗?

有,但意义变了。

以前学NIO是为了写高性能服务器,现在是为了理解底层框架怎么实现的。Spring Boot底层的Tomcat用NIO,Dubbo的通信层用Netty,Netty用NIO。你调一个RPC接口,底层几万行NIO代码在跑。

但自己写NIO?不需要了。虚拟线程已经把高并发问题解决得够好。

七、Buffer的那些反人类设计

ByteBuffer有四个指针:position、limit、capacity、mark。每次读写完要flip()、clear()、compact()。我第一次用的时候,对着文档看了一下午,还是搞不懂什么时候该用哪个。

最经典的坑:

复制代码
buffer.put(data); // 写入数据
channel.write(buffer); // 发现什么都没发出去

写完数据,position在最后了。你直接write,Channel从position读到limit,读到的是空的。必须先flip():

复制代码
buffer.put(data);
buffer.flip(); // position=0, limit=原来的position
channel.write(buffer);

还有clear()和compact()的区别。clear()是清空整个Buffer,compact()是把未读的数据移到开头。半包处理的时候,用错了直接丢数据,客户端收到的都是乱码。

2011年有次线上事故,就是这个原因。某个socket半包了,我代码里用了clear(),把前半截数据清掉了。客户端一直等后半截,等了5分钟超时,投诉打到老板那。

所以生产环境别直接用ByteBuffer。要么用Netty的ByteBuf(read/write指针分开,怎么用都不会错),要么现在直接上虚拟线程,连Buffer都不用碰。

八、AIO是个什么东西?没用

Java 7加了AIO(异步IO),Channel.read()不阻塞,回调告诉你数据来了。听起来很美好,实际上------Linux下AIO的实现就是epoll+线程池模拟出来的,性能还不如直接用NIO。Windows的IOCP倒是真异步,但谁在Windows上跑生产服务器?

Netty 5.0曾经准备支持AIO,折腾了一年,发现性能提升不到5%,反而代码复杂度翻倍。后来直接砍了,退回NIO。

所以AIO在Java生态里基本是死的。你看市面上所有的框架,没有一个用AIO的。别被这个名词忽悠了。

九、总结一下

2005年的时候,一个连接一个线程,服务器扛不住几千个并发。NIO出来了,用Selector让一个线程管一万个连接,解决了C10K问题。

2026年了,虚拟线程让阻塞式代码可以承载百万级并发。大部分业务场景,你写最简单的BIO代码就够了。

NIO还有用吗?有,但不是用来写业务代码。

底层框架(Netty、Dubbo、Spring WebFlux)还是基于NIO。你用Spring Boot写接口,底层Tomcat用的是NIO。你调RPC,底层Dubbo用的是Netty,Netty用的是NIO。

学NIO不是为了自己写服务器,是为了看懂这些框架是怎么实现的。理解Selector为什么能让一个线程管一万个连接,理解为什么Netty的EventLoop要用单线程,理解为什么Dubbo能在一台机器上保持几万个RPC连接。

至于Buffer怎么flip、compact怎么用------别纠结。你用虚拟线程写业务代码,根本碰不到Buffer。你看框架源码,看到Buffer就跳过,先理解Selector这个核心概念。

最后一句话:2026年写Java,业务代码用虚拟线程,想看框架源码再学NIO。

相关推荐
lolo大魔王3 小时前
Go语言的异常处理
开发语言·后端·golang
likerhood4 小时前
简单工厂设计模式
java·ide·intellij-idea
chh5634 小时前
C++--模版初阶
c语言·开发语言·c++·学习·算法
灼灼桃花夭5 小时前
js之阳历 → 农历(含时辰)转换函数
开发语言·前端·javascript
派大星酷5 小时前
Java 调用 Kimi API 实战:实现与大模型的简单对话
java·开发语言·ai编程
小李子呢02115 小时前
前端八股性能优化(1)---防抖和节流
开发语言·前端·javascript
henrylin99995 小时前
Hermes Agent 核心运行系统调用流程--源码分析
开发语言·人工智能·python·机器学习·hermesagent
珎珎啊5 小时前
Python3 字符串核心知识点
开发语言·python
会编程的土豆5 小时前
01背包与完全背包详解
开发语言·数据结构·c++·算法