Java NIO核心原理与高并发实践:从BIO到Reactor模式
1. 先搞清楚一件事BIO的瓶颈到底在哪儿聊NIO之前得先直面一个老问题为什么传统BIOBlocking I/O在高并发下撑不住很多人一上来就学NIO的APISelector、Channel、Buffer背得滚瓜烂熟但真到排查性能问题的时候说不出个所以然。原因很简单——不理解NIO到底解决了什么痛点代码写得再花哨也是空中楼阁。1.1 阻塞不只是卡住这么简单我先用一个极其生活化的例子解释阻塞。你去银行办业务。传统的BIO模式是什么样每个客户进门银行派一个专属柜员一对一服务。这个柜员从头到尾只伺候你一个人你填单子填了十分钟柜员就在那儿干等着什么都干不了。客户一多银行就得不停招柜员。招到后来大厅站满了柜员但大部分人在等客户填表。Java的BIO服务器本质上就是这个模式一个连接对应一个线程。最典型的写法是ServerSocket serverSocket new ServerSocket(8080); while (true) { Socket socket serverSocket.accept(); // 阻塞等待新连接 new Thread(() - handleRequest(socket)).start(); // 每来一个连接开一个线程 }这段代码看起来没问题但就是它在高并发下会把服务器活活拖死。accept()是阻塞的read()是阻塞的write()也是阻塞的。一个线程从连接到断开绝大多数时间都趴在read()上等数据CPU流转率极低资源利用率惨不忍睹。1.2 线程开销的账得算一笔明白账有人说了阻塞就阻塞呗我上线程池限制并发数不就行了账不是这么算的。假设你的服务器有4核8线程的CPU物理上能同时跑的就8个线程。我们来算三笔账第一笔内存账。每个线程默认栈大小是1MB这是JVM的默认配置。如果你开了5000个线程光是线程栈就要吃掉约5GB内存。机器内存16G的话JVM堆还能剩多少第二笔上下文切换账。一个CPU核心在多个线程之间来回切换每次切换都要保存当前线程的寄存器状态、程序计数器、栈指针加载下一个线程的上下文。这个开销大约是微秒级别。单看一次切换不算什么但高并发下每秒切换几万次CPU时间就全耗在切换上了而不是在真正执行业务代码。第三笔IO等待账。网络请求的特点是大部分时间在等小部分时间在算。一个连接可能1秒内只有10毫秒在真正传输数据剩下的990毫秒都在闲着。可BIO的线程必须在这990毫秒里占着一个线程寸步不离。这三笔账加起来结论非常扎心连接数一旦上千BIO模式基本必死。真正的转折点出现在连接数和线程数的关系上。你不可能为每个连接都准备一个线程因为线程是有成本的但你也不能让少量线程阻塞式地等连接因为这样并发量上不去。这两头都堵就需要一个完全不同的思路。1.3 NIO到底换了个什么思路NIO全称Non-blocking I/O直译就是非阻塞I/O。它换的思路是把一个连接一个线程改成一个线程管所有连接。怎么管核心就是让线程不在某个连接上死等。我去银行办业务银行改成了大堂经理模式大堂经理手里拿一个排队叫号器一次性受理几百个客户的业务诉求。谁的材料准备好了叫号器提示轮到谁了大堂经理就去处理谁其他没准备好的客户大堂经理不用管继续等下一个叫号提示。对应到技术层面就是三个组件各司其职Buffer缓冲区数据搬运的容器一切读写都要经过它。Channel通道连接的另一层抽象相当于客户的业务单据。Selector选择器那个排队叫号器负责监听一堆通道里到底谁有事件发生。一句话概括NIO的核心思想用极少的线程通过事件驱动机制处理大量的并发连接。这个思路不是Java发明的操作系统层面的epoll、kqueue早就是这套逻辑了。NIO做的事情是把这个能力从操作系统的手里通过JDK API暴露给Java开发者。搞懂了这一点后面的代码就都好理解了。2. 三大核心组件把NIO的骨架拆开看很多人学NIO学得痛苦是因为一上来就被一堆抽象概念砸晕了。Buffer、Channel、Selector各自还有一堆方法flip、clear、compact、rewind……全挤在一起不懵才怪。我换个顺序先告诉你每个组件是干什么的再告诉你它们是怎么配合的最后再上代码。2.1 Buffer数据进出的统一容器Buffer本质上就是一个内存块只不过它比数组多了一个游标机制。你可以把它理解成一个带指针的数组这个指针叫position。第一次接触NIO的人最容易被Buffer的四个属性搞晕属性含义生活化类比capacity缓冲区容量创建后不可变杯子的总容积position当前读写的位置勺子当前舀到哪儿了limit可读写的边界杯子上标的水位线mark标记位置可随时reset在书上夹的书签举个最典型的场景写数据。你要往Buffer里写入1024字节写入前看position写完再看position它从0变成了1024。然后你要把数据读出去不能直接读得先调用flip()方法——flip()的作用是把limit设为当前positionposition归零。这样一来Buffer就从写模式切换成了读模式。很多人第一次写NIO代码容易漏掉flip()结果读出来的全是默认值或旧数据。这个坑我当年也踩过查了很久才发现是忘记翻转。读完之后要再写入调用clear()或者compact()。clear()简单粗暴直接把position归零、limit设回capacity相当于把杯子里的水倒掉重来compact()则更温柔它会把未读完的数据挪到头部然后把position挪到数据后面方便你接着往下写数据。这里有个细节值得注意clear()并不会真正清空数据它只是重置了游标。如果你读了旧数据还是能读出来。之所以这么设计是因为清空内存是有开销的而JVM团队在性能上锱铢必较。2.2 Channel连接和文件的新管子同BIO里的InputStream/OutputStream不同NIO的Channel是双向的。一个SocketChannel既能读也能写不用再像BIO那样分开维护两个流对象。Channel还有一个特点它是非阻塞的基石。你可以把Channel设置成非阻塞模式这样对它的read()调用就不会傻等——有数据就返回数据长度没数据就立刻返回0或-1线程不会被卡住。最常见的Channel有这么几种SocketChannelTCP连接的通道对应BIO的Socket。ServerSocketChannel服务端监听通道对应BIO的ServerSocket它负责accept新连接。FileChannel文件读写通道。注意文件Channel没法设置成非阻塞模式这是文件IO的天然属性决定的。Channel和Buffer的关系是Channel负责和底层IO打交道Buffer负责和业务代码打交道。数据从Channel读进Buffer再从Buffer写入Channel。业务代码永远不直接触碰Channel的底层IO接口只操作Buffer这个中间容器。2.3 Selector真正让NIO飞起来的那个轮子Selector是整个NIO的发动机。它做的事情可以用一句话概括帮我盯着这一堆Channel哪个有事件了逐个通知我。你可以往Selector上注册多个Channel然后调用selector.select()。这个方法会阻塞直白点说就是在那里蹲点一旦有任何一个Channel触发了事件它就会返回。返回之后你通过selectedKeys()拿到所有就绪的通道逐个处理。事件类型一共四种OP_ACCEPT有新的连接进来了服务端通道触发。OP_CONNECT客户端连接成功客户端通道触发。OP_READ通道里有数据可读了。OP_WRITE通道可以写数据了。这里有个非常关键但新手容易用错的点OP_WRITE事件不要一直在Selector上注册。如果你注册了OP_WRITE而不立刻写数据这个事件会一直就绪select()几乎不会阻塞你的线程就会在一个死循环里空转CPU瞬间打满。正确的做法是需要写数据时才注册OP_WRITE写完立刻取消注册。这个坑我在本地跑压测时踩过当时一个一万并发的小测试CPU直接99%查了半天才发现一个连接把OP_WRITE写在了Selector上却没做善后清理。3. 非阻塞的本质从专职服务员到事件大堂经理前面说了组件接下来把最核心的机制讲透。这些机制理解了你写出来的代码就不再是抄API而是真正能应对高并发场景的设计。3.1 IO多路复用到底是什么Selector背后依赖的是操作系统的IO多路复用机制。在Linux上具体是select、poll、epoll这三代演进在macOS上则是kqueue。JDK的NIO在Linux上默认走的是epoll只有极老版本的JDK才会退回select。IO多路复用什么意思多路指的是多个网络连接复用指的是复用一个线程。就是说操作系统帮你监控一堆socket任何一个socket有数据到达操作系统就通知你。你不需要对每个socket单独开线程去等。这就好比你作为前台不需要在每个客户旁边站一个服务员。你把所有客户的需求登记在册操作系统大堂广播一喊3号桌客人需要茶水你直接端着茶过去就行。用epoll的系统调用来看整个过程简洁得惊人int epfd epoll_create(1024); epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, ev); // 注册关注的事件 while (1) { int n epoll_wait(epfd, events, 1024, -1); // 等待事件就绪 for (int i 0; i n; i) { handle_event(events[i]); // 逐个处理就绪的事件 } }JDK的Selector就是把这套系统调用封装成了Java API。核心差别在于select和poll每次都要把文件描述符从用户态拷贝到内核态数量多了性能下降明显epoll则用红黑树维护监听集合内核态和用户态之间共享一块mmap内存事件来了直接通过回调机制通知效率完全不在一个量级。3.2 一个简单的Reactor事件循环NIO编程最经典的模型叫Reactor模式网上说是反应堆模式听起来挺玄乎。其实它就是一个事件循环线程不断问Selector有没有事件有事件就分发给对应的处理器。我画一个流水线式的流程while (true) { selector.select(); // 1. 蹲点等事件 IteratorSelectionKey it selector.selectedKeys().iterator(); // 2. 取出就绪的事件 while (it.hasNext()) { SelectionKey key it.next(); it.remove(); // 3. 必须移除否则会重复处理 if (key.isAcceptable()) { // 4. 新连接 acceptConnection(key); } else if (key.isReadable()) { // 5. 可读 readData(key); } else if (key.isWritable()) { // 6. 可写 writeData(key); } } }看到那个it.remove()了吗很多NIO入门者在这里踩坑selectedKeys()返回的是上次select结果的就绪Key集合处理完之后不会自动清除。如果你忘了remove下次select回来这批旧Key还在里面你会拿旧事件反复处理轻则重复读数据重则引发异常。处理完readable事件之后key.channel()是那个触发事件的SocketChannel。从这里拿到数据存入Buffer然后交给业务逻辑处理。3.3 为什么说这不是魔法而是数据结构很多人第一次接触NIO觉得Selector特别玄乎好像一个线程有分身术。其实拆开看底层就是几个数据结构的巧妙配合。在epoll的实现里内核维护了一个红黑树用来存储所有注册的socket fd文件描述符又维护了一个就绪链表用来存储有事件发生的socket。当某个socket有数据到达时内核通过回调函数把它加入就绪链表。epoll_wait()要做的仅仅是检查就绪链表是否为空不为空就把这些事件拷贝出来返回。看到没有内核不是遍历所有连接而是只返回有事件的连接。这就好比你有一个几千人的通讯录你不是挨个打电话问你有事吗而是谁主动打电话给你你就接谁的。连接再多这个机制的时间复杂度都很优秀。这也是为什么NIO能够支持数十万连接——不是靠某个黑魔法而是靠数据结构和操作系统的配合把等待这件事彻底抛给了内核去做。4. 手写一个高并发NIO服务器跑起来再说理论说了不少不上代码总觉得隔着一层。这一节我带着你从头到尾写一个可运行的NIO服务器包含accept连接、read请求、write响应三个核心流程。代码不追求极端性能重点是逻辑清晰、能跑、能观察。4.1 需要准备的基础组件环境就一个JDK8的Java工程不需要任何第三方依赖。我们需要的类都在java.nio包下。整体结构是这样的public class NioHttpServer { private Selector selector; private ServerSocketChannel serverChannel; private ByteBuffer buffer ByteBuffer.allocate(1024); public NioHttpServer(int port) throws IOException { selector Selector.open(); serverChannel ServerSocketChannel.open(); serverChannel.configureBlocking(false); // 非阻塞模式 serverChannel.socket().bind(new InetSocketAddress(port), 1024); serverChannel.register(selector, SelectionKey.OP_ACCEPT); } // 后续方法往下补 }三个关键点configureBlocking(false)必须在注册之前调用。如果通道是阻塞模式注册到Selector是没意义的甚至会报错。bind()的第二个参数是backlog表示系统在拒绝新连接前可以排队的连接数。调大它能扛住瞬时连接爆发。register()方法的返回值是SelectionKey后续所有事件的判断和操作都围绕它展开。4.2 逐步实现从Accept到Read再到Write先写事件循环public void start() throws IOException { while (true) { selector.select(); // 阻塞等待事件 IteratorSelectionKey iterator selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key iterator.next(); iterator.remove(); if (key.isAcceptable()) { handleAccept(key); } else if (key.isReadable()) { handleRead(key); } else if (key.isWritable()) { handleWrite(key); } } } }然后逐个实现。先看handleAcceptprivate void handleAccept(SelectionKey key) throws IOException { ServerSocketChannel server (ServerSocketChannel) key.channel(); SocketChannel client server.accept(); if (client ! null) { client.configureBlocking(false); // 新连接注册读事件不注册写事件 client.register(selector, SelectionKey.OP_READ); System.out.println(新连接: client.getRemoteAddress()); } }这里面有两个容易犯的错错误一accept()之后忘了configureBlocking(false)。新accept出来的是默认阻塞模式直接注册到Selector上会抛出IllegalBlockingModeException。你可以在accept后立刻设置也可以在ServerSocketChannel上设置——注意两个通道需要分别设置ServerSocketChannel的设置不会自动传给SocketChannel。错误二新连接只注册OP_READ不注册OP_WRITE。前面说了OP_WRITE事件会持续就绪如果一开始就注册上你的selector.select()基本不会阻塞CPU直接跑满。写事件应该在确实有数据要发送时临时注册发完就取消。接下来看handleReadprivate void handleRead(SelectionKey key) throws IOException { SocketChannel channel (SocketChannel) key.channel(); buffer.clear(); int bytesRead channel.read(buffer); if (bytesRead -1) { // 连接已关闭关闭通道取消key channel.close(); key.cancel(); return; } if (bytesRead 0) { buffer.flip(); byte[] data new byte[buffer.remaining()]; buffer.get(data); String request new String(data, StandardCharsets.UTF_8); System.out.println(收到请求: request); // 把响应数据准备好注册写事件 String response HTTP/1.1 200 OK\r\n Content-Type: text/plain\r\n Content-Length: 12\r\n\r\n Hello, NIO!; ByteBuffer writeBuffer ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)); channel.register(selector, SelectionKey.OP_WRITE, writeBuffer); } }这里的核心思路是读事件触发了读数据然后不直接写而是把要写的数据挂在attachment上改注册为写事件。等selector再次轮询到这个通道时写事件就绪了我们再执行写操作。接着看handleWriteprivate void handleWrite(SelectionKey key) throws IOException { SocketChannel channel (SocketChannel) key.channel(); ByteBuffer writeBuffer (ByteBuffer) key.attachment(); channel.write(writeBuffer); if (!writeBuffer.hasRemaining()) { // 数据写完了清除写事件改回读事件 channel.register(selector, SelectionKey.OP_READ); System.out.println(响应已发送等待新请求); } else { // 有剩余说明socket发送缓冲区满了下次继续写 System.out.println(数据未写完剩余 writeBuffer.remaining() 字节); } }注意这里的channel.write(writeBuffer)和BIO完全不一样BIO的write必须把所有数据写完才返回NIO的write可能只写了部分就返回了因为底层socket的发送缓冲区塞满了。所以用完hasRemaining()判断如果还有剩余说明这次没写完等下次OP_WRITE事件继续写。4.3 一些值得注意的非阻塞小细节细节一buffer的重复利用。我在类里定义了一个成员变量buffer ByteBuffer.allocate(1024)每次read前调clear()重置游标。如果请求体很大超过1KB这个buffer就不够用需要循环读。更健壮的做法是每次读取时按实际数据量动态分配或者用ByteBuffer.allocateDirect()直接分配堆外内存不过堆外内存的管理更复杂入门阶段先掌握堆内Buffer即可。细节二读事件里可能只读到半包。TCP是流协议没有消息边界。一次read()可能只读到半个请求也可能读到两个完整的请求。写NIO服务器时必须有粘包/半包处理逻辑。我的示例代码简化了这块直接按当前读到字节当一整条消息——实际生产代码中你要么用长度字段要么用分隔符像HTTP就用\r\n\r\n来标识头部结束。细节三业务处理不要放在selector的IO线程里。如果handleRead里直接查数据库、调远程接口一个慢业务会拖住整个selector循环其他所有连接的读写都会卡住。典型做法是IO线程只负责收发消息把解析后的业务请求丢给工作线程池处理处理完再通过队列把响应交回IO线程发送。Netty的EventLoop就是按这个思路设计的。我把完整的类贴出来方便你直接跑import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.Iterator; public class NioHttpServer { private Selector selector; private ServerSocketChannel serverChannel; private final ByteBuffer buffer ByteBuffer.allocate(1024); public NioHttpServer(int port) throws IOException { selector Selector.open(); serverChannel ServerSocketChannel.open(); serverChannel.configureBlocking(false); serverChannel.socket().bind(new InetSocketAddress(port), 1024); serverChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println(NIO服务器启动端口: port); } public void start() throws IOException { while (true) { selector.select(); IteratorSelectionKey iterator selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key iterator.next(); iterator.remove(); try { if (key.isAcceptable()) handleAccept(key); else if (key.isReadable()) handleRead(key); else if (key.isWritable()) handleWrite(key); } catch (IOException e) { System.err.println(处理异常关闭通道: key.channel()); key.channel().close(); key.cancel(); } } } } private void handleAccept(SelectionKey key) throws IOException { SocketChannel client ((ServerSocketChannel) key.channel()).accept(); if (client ! null) { client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); System.out.println(新连接: client.getRemoteAddress()); } } private void handleRead(SelectionKey key) throws IOException { SocketChannel channel (SocketChannel) key.channel(); buffer.clear(); int bytesRead channel.read(buffer); if (bytesRead -1) { System.out.println(客户端断开: channel.getRemoteAddress()); channel.close(); key.cancel(); return; } if (bytesRead 0) { buffer.flip(); byte[] data new byte[buffer.remaining()]; buffer.get(data); String request new String(data, StandardCharsets.UTF_8); System.out.println(收到请求: request.trim()); String response HTTP/1.1 200 OK\r\n Content-Type: text/plain\r\n Content-Length: 12\r\n\r\n Hello, NIO!; ByteBuffer writeBuffer ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)); channel.register(selector, SelectionKey.OP_WRITE, writeBuffer); } } private void handleWrite(SelectionKey key) throws IOException { SocketChannel channel (SocketChannel) key.channel(); ByteBuffer writeBuffer (ByteBuffer) key.attachment(); channel.write(writeBuffer); if (!writeBuffer.hasRemaining()) { channel.register(selector, SelectionKey.OP_READ); System.out.println(响应已发送); } else { System.out.println(写缓冲区已满剩余: writeBuffer.remaining()); } } public static void main(String[] args) throws IOException { new NioHttpServer(8080).start(); } }启动它浏览器访问http://localhost:8080或者用curl发请求你会在控制台看到完整的请求、响应日志。用netstat或者压测工具开几十个并发连接你会发现这个单线程服务器能轻松扛住——这放在BIO时代想都不敢想。5. 实测中发现的问题帮你们提前排雷代码跑通之后才是真正理解NIO的开始。我在实际压测和线上使用中遇到过一堆问题每个都值得单独拎出来讲。5.1 空转循环与性能损耗第一个问题前面提过就是OP_WRITE事件引发的空转。压测的时候我写了个简单的echo服务器注册了OP_READ和OP_WRITE结果一跑起来CPU直接飙到100%select()几乎不等待。排查过程是这样的我先用jstack抓了线程栈发现线程卡在selector.select()上没有阻塞而是一直在返回。然后我一步步屏蔽代码最后定位到是register(selector, OP_WRITE)的问题。修改方式是只在有数据要写时才注册OP_WRITE写完立刻切回OP_READ。还有一个更隐蔽的版本数据没读完就注册了OP_READ。如果Buffer里还有未消费的数据下次read事件触发时底层socket缓冲区没有新数据read()返回0但事件已经触发了。你如果对返回0不做判断代码就会在这个连接上空转。处理方式是在handleRead中判断bytesRead 0的情况直接return不处理。5.2 消息边界问题粘包和半包这是NIO编程绕不过去的坎。TCP是流协议不保证消息边界。客户端连续发两条消息ABC和DEF服务端可能一次性读到ABCDEF也可能先读到ABC再读DEF还可能先读到AB再读CDEF。你无法根据read()的次数来划分消息。我的做法是固定长度法每条消息前四个字节存放消息体长度。服务端先读4字节得到长度再读对应长度的内容如果长度不满足就继续等。核心代码像这样// 假设header已经保存了本次消息总长度 private void parseMessage(SocketChannel channel, ByteBuffer buffer) throws IOException { buffer.flip(); while (buffer.remaining() 4) { int length buffer.getInt(); if (buffer.remaining() length) { byte[] data new byte[length]; buffer.get(data); // 业务处理 } else { // 半包把读指针回退等下次数据来 buffer.position(buffer.position() - 4); break; } } }这种方案简单可靠缺点是字节数开销大每条消息多了4字节。如果是HTTP这类自带分隔符的协议用\r\n\r\n做边界判断也行但需要你在buffer里维护收到的字节而不能每读一个事件就当一条完整消息。5.3 单线程vs多线程Reactor什么场景该升级我上面写的单线程Reactor一个selector线程处理所有accept和IO事件。这个模型的优点是简单、无锁、性能足够高因为IO操作本身不消耗CPU瓶颈通常在业务处理上。但单线程Reactor有个致命弱点如果某个事件的处理逻辑里出现了耗时的非IO操作比如加解密比如大对象序列化整个selector线程就会被卡住所有连接的读写全部停摆。解决思路是引入多线程。业界最成熟的做法是Netty的模型BossGroup一个或多个线程只负责accept连接。WorkerGroup一组线程每个线程对应一个Selector负责处理已建立连接的读写。业务Handler业务逻辑放到单独的业务线程池不阻塞IO线程。从单线程升级到多线程Reactor并不是简单地把代码复制几份。它涉及连接在多个Selector之间分配、线程安全、数据在多线程间的传递复杂度会上一个台阶。这也是为什么实际项目中更多人直接用Netty而不裸写NIO——Netty把这些细节都封装好了而且踩过的坑比我们多得多稳定性有保障。5.4 零拷贝和DirectBuffer先别急着追潮流网上聊NIO必定会提到零拷贝。FileChannel的transferTo()方法可以直接把文件内容发送到socket不需要在用户态和内核态之间倒腾数据这在传输大文件时性能优势明显。但我要提醒一句零拷贝不是万能药别为了用而用。如果你的业务需要处理的是小报文、频繁读写堆外内存DirectBuffer的分配和回收成本反而比堆内Buffer高。JDK的零拷贝主要在文件传输场景下收益最大比如HTTP静态文件服务、FTP服务。我的建议是先用堆内Buffer把功能跑通压测看瓶颈如果确实遇到大量大文件传输的场景再去研究transferTo()。顺序别搞反不然你写出来的代码看起来很高大上实则在填毫无必要的坑。6. NIO学完之后下一步到底该往哪儿走NIO本身是一个完整的编程范式但学完之后你可能会困惑我该直接拿它做生产项目吗还是说只当基础理论从我的经验来看NIO最值得投入的后续方向有两个。第一个方向是直接上Netty。Netty是NIO在生产场景的集大成者核心架构就是多线程Reactor模式但对上层暴露的是更友好的API。你理解了NIO的Buffer、Channel、Selector再看Netty的ByteBuf、ChannelPipeline、EventLoop会发现到处都是熟悉的味道。Netty解决的粘包拆包、断线重连、流量整形、编解码这些问题如果让你用原生NIO从头造一遍轮子工程量极其惊人。Netty社区活跃文档完善性能可靠是生产环境的首选。第二个方向是用NIO加深对IO模型的理解。如果你往后接触Go语言的goroutine、Node.js的事件循环、C语言的epoll编程会发现它们解决问题的思路和NIO如出一辙。NIO是你理解这些现代编程模型的桥梁它的意义不止于Java本身。学习路径上我有个建议先手写一遍NIO服务器用压测工具验证它的高并发能力再读Netty的源码看它如何把NIO封装成优雅的事件驱动框架最后再回到NIO去思考为什么Netty要这么设计。这个循环走完你对高并发IO的理解会进入一个全新的层次。最后说个实际操作中的小技巧排查NIO问题的时候jstack比日志有用得多。线程卡住了jstack可以看到线程到底是阻塞在selector.select()上、阻塞在业务代码里还是空转消耗CPU。很多NIO的坑在日志里是看不出所以然的但在线程栈里一目了然。学会看线程栈是NIO开发的隐形基本功。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →