Java NIO到底是个什么东西?

张开发
2026/4/15 4:24:22 15 分钟阅读

分享文章

Java NIO到底是个什么东西?
NIO就是Java处理网络IO的另一种方式。就这么简单。但很多人学NIO学不明白不是因为它难是因为教程都在讲错重点。随便打开一个NIO教程上来就是Channel、Buffer、Selector三大组件。然后画个图告诉你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万个Channelwhile (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(); // 阻塞直到有事件 SetSelectionKey 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(); ByteBuffer buffer ByteBuffer.allocate(1024); int n client.read(buffer); if (n 0) { // 处理数据 } } } keys.clear(); // 必须手动清空否则下次还会处理 }这就是NIO的完整流程创建Selector把ServerSocketChannel注册到Selector上关注ACCEPT事件新连接循环调用selector.select()等待事件有新连接了accept()拿到SocketChannel设为非阻塞注册到Selector上关注READ事件某个Channel有数据了read()读取数据处理处理完继续循环一个线程几十行代码轻松处理几万个连接。但问题是——敢把这代码上线吗我当时就上了然后炸了。半夜被oncall电话叫醒服务器CPU打满处理量暴跌。排查了一夜才发现某个客户端发了半截数据就断了我代码里没处理半包Buffer一直在等完整数据这个Channel就一直占着key。还有一次某个客户端发了个100MB的数据包我代码里buffer只有1KB读了几个小时还没读完其他正常请求全被饿死。NIO代码看起来简单生产级的NIO服务器要处理的东西太多半包、粘包异常断线、心跳检测读写缓冲区管理流量控制、背压优雅关闭、资源清理自己手撸NIO99%要出问题。四、学完NIO能自己写服务器吗别想了很多人学完NIO以为自己能写服务器了。醒醒。Netty是基于NIO的框架它帮你解决了上面说的所有坑零拷贝、池化ByteBuf、内存泄漏检测、Reactor线程模型、编解码器链、粘包半包处理、背压流控、优雅关闭。哪一样都不简单。我们2013年从自己手撸的NIO改成Netty代码从2000行变成200行。稳定性以前三天两头出问题改完之后半年没出过事故。学NIO不是让你自己写服务器是让你看懂Netty底层在干什么。Netty的EventLoop就是在跑selector.select()循环ChannelHandler就是在处理SelectionKey的回调。理解了NIONetty的源码就不神秘了。五、NIO性能一定比BIO好不一定看场景。短连接、请求立刻响应的比如HTTP APIBIO可能更合适。每个请求处理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用NIODubbo的通信层用NettyNetty用NIO。你调一个RPC接口底层几万行NIO代码在跑。但自己写NIO不需要了。虚拟线程已经把高并发问题解决得够好。七、Buffer的那些反人类设计ByteBuffer有四个指针position、limit、capacity、mark。每次读写完要flip()、clear()、compact()。我第一次用的时候对着文档看了一下午还是搞不懂什么时候该用哪个。最经典的坑buffer.put(data); // 写入数据 channel.write(buffer); // 发现什么都没发出去写完数据position在最后了。你直接writeChannel从position读到limit读到的是空的。必须先flip()buffer.put(data); buffer.flip(); // position0, limit原来的position channel.write(buffer);还有clear()和compact()的区别。clear()是清空整个Buffercompact()是把未读的数据移到开头。半包处理的时候用错了直接丢数据客户端收到的都是乱码。2011年有次线上事故就是这个原因。某个socket半包了我代码里用了clear()把前半截数据清掉了。客户端一直等后半截等了5分钟超时投诉打到老板那。所以生产环境别直接用ByteBuffer。要么用Netty的ByteBufread/write指针分开怎么用都不会错要么现在直接上虚拟线程连Buffer都不用碰。八、AIO是个什么东西没用Java 7加了AIO异步IOChannel.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用的是NettyNetty用的是NIO。学NIO不是为了自己写服务器是为了看懂这些框架是怎么实现的。理解Selector为什么能让一个线程管一万个连接理解为什么Netty的EventLoop要用单线程理解为什么Dubbo能在一台机器上保持几万个RPC连接。至于Buffer怎么flip、compact怎么用——别纠结。你用虚拟线程写业务代码根本碰不到Buffer。你看框架源码看到Buffer就跳过先理解Selector这个核心概念。最后一句话2026年写Java业务代码用虚拟线程想看框架源码再学NIO。

更多文章