认识NIO
- 1 概述
- 2 宏观对比 BIO vs NIO
- 3 NIO核心组件
- 3.1 Buffer缓冲区详解
- 3.2 Channel通道
- 3.3 Selector选择器
- 4 实战代码
- 4.1 场景一:NIO实现文件拷贝
- 4.2 场景二:网络IO处理(示例暂缺)
- 5 为什么NIO更快?
- 5.1 Direct Memory(直接内存)
- 5.2 Zero Copy(零拷贝)
- 6 总结
- 6.1 何时用BIO何时用NIO
- 6.2 关于原生NIO
大家好,我是欧阳方超,公众号同名。

1 概述
NIO在Java1.4引入,它的出现是为了解决传统IO在高并发、大数据量传输时的性能瓶颈,注意,NIO中的N既有new之意也有non-blocking之意。
2 宏观对比 BIO vs NIO
面向缓冲区:BIO,传统IO以流为中心一次读写一个字节/字符,NIO以缓冲区为中心,可灵活处理缓冲区内的数据。
可非阻塞:特别适用于网络编程,线程可以同时处理多个连接,轮询多个连接,哪个数据就绪就处理哪个。注意,NIO支持阻塞和非阻塞两种模式,通道(如SocketChannel、ServerSocketChannel)都可以通过设置切换为阻塞或非阻塞模式。另外需要注意,FileChannel是阻塞的,无法设置为非阻塞的。
3 NIO核心组件
NIO的体系完全建立在三个概念之上:Buffer(缓冲区)、Channel(通道)、Selector(选择器)。
|
组件 |
概念总结 |
理解方式 |
|
Buffer |
缓冲区,实际数据的容器 |
就像一块内存数组,有capacity、limit、position等指针控制 |
|
Channel |
通道,数据与缓冲区之间的桥梁 |
类似流,但可以双向读写,可结合Buffer进行操作 |
|
Selector |
选择器,用于多路复用,多路监听多个Channel的IO状态 |
类似电话总机,一个线程管理多个连接,处理read/write事件 |
3.1 Buffer缓冲区详解
Buffer是数据的缓冲区或载体,在NIO中,不会直接把数据写入通道,也不会直接从通道读取数据,所有数据都必须经过Buffer。Buffer本质上是一个数组,外加一套复杂的指针变量来管理数据的读写位置。这三个指针变量是capacity(容量)、position(位置)、limit(限制),capacity表明数组的大小,position表明从哪个位置读写数据,limit表明还有多少数据可以读写。
Buffer缓冲区有个重大的动作:翻转,使用flip()方法实现该动作。
写模式:往Buffer里数据时,position会增加。
切换:当写完数据后,要把数据从Buffer读出来发给通道,必须调用flip()。
读模式:flip()会把limit设为刚才的position,把position归零。
3.2 Channel通道
Channel是数据的通道,并且是双向的,常用的Channel有如下类型:
- FileChannel:处理文件。
- SocketChannel/ServerSocketChannel:处理TCP网络
- DatagramChannel:处理UDP网络。
- Pipe:线程间单向管道。
3.3 Selector选择器
Selector是选择器,也叫多路复用器,它是NIO处理高并发的灵活,一个线程监听多个连接,适合高并发网络场景。
以餐馆服务员服务顾客为例,在传统IO模式下,一个服务员(线程)只盯着一张桌子,客人看菜单半小时,服务员就傻站半小时,如果有1000张桌子,老板就需要雇1000个服务员,系统直接崩溃。NIO模式下,一个服务员(Selector)站在大堂,谁点菜就举手示意服务员,服务员看到哪桌客人举手了就去处理哪一桌,这样一个线程就能管理成千上万个连接。
Selector实现了I/O多路复用(I/O Multiplexing),底层一般依赖操作系统的 epoll (Linux) 或 kqueue (Mac/BSD) 机制,效率极高。
4 实战代码
4.1 场景一:NIO实现文件拷贝
下面的示例实现了文件拷贝功能,展示了Channel与Buffer的结合使用。
package com.example.demo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOTtest {
public static void main(String[] args) {
try{
//1.获取通道(即使是NIO,源头往往还是流)
//FileChannel无法直接通过new创建,需要从流或RandomAccessFile中获取
FileInputStream fis = new FileInputStream("/Users/mac/data.txt");
File file = new File("/Users/mac/dest.txt");
if (!file.exists()) {
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file);
FileChannel inChannel = fis.getChannel();
FileChannel outchannel = fos.getChannel();
//2.分配缓冲区(列如直接分配1KB的非直接内存)
ByteBuffer buffer = ByteBuffer.allocate(1024);
//3.循环读写
while (true) {
//clear()超级重大!
//它重置position=0,limit=capacity
//如果不清理,buffer满了之后,channel.read就读不进新数据,导致死循环
buffer.clear();
//从通道读数据到缓冲区
int bytesReadx = inChannel.read(buffer);
if (bytesReadx == -1) {
break; //读到末尾
}
//4.模式切换:准备把缓冲区的数据写出去
buffer.flip();
//5.将缓冲区数据写入输出通道
outchannel.write(buffer);
}
//关闭资源
inChannel.close();
outchannel.close();
fis.close();
fos.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
注意,上面的示例中,在while循环中有个对缓冲区执行clear()的操作,这个操作必定要执行,它重置position=0,limit=capacity,如果不执行的话position可能会与limit相等,此时再往缓冲区读内容的话,可能会视为buffer中没有空间了,channel.read(buffer)会返回0,导致程序进入死循环。
4.2 场景二:网络IO处理(示例暂缺)
这里应该有一个基于nio实现网络io的示例,但是代码量会略大,为了不过多占用本文的篇幅,决定另写一篇文章专门介绍如何使用Selector实现一个线程监听多个连接(高并发网络场景)。
5 为什么NIO更快?
实则IO应该从文件IO和网络IO这两个方面进行分类(虽然在操作系统层面IO都是一样的),而NIO的快也应该从网络IO和文件IO两个方面进行解读,对于网络IO而言,Selector是提高效率的关键,但是对于文件IO而言,Selector是不适用的,但即便如此NIO中也确的确 实对文件IO的操作提速不少,主要体目前两个方面:Direct Memory(直接内存)、Zero Copy(零拷贝)。
5.1 Direct Memory(直接内存)
传统Java读取文件的流程,Java读取磁盘文件——>操作系统内核缓冲区——>复制——>Java堆内存(heap)。这里有一次CPU拷贝,发生在操作系统内核缓冲区与Java堆内存之间,且受GC影响。
NIO方式(ByteBuffer.allocateDirect)的流程,Java直接在堆外分配内存,这块内存直接映射到操作的IO缓冲区。少了一次拷贝,且不受GC搬运影响。
5.2 Zero Copy(零拷贝)
如果要把一个文件发送给网络,传统的Java IO是这样处理数据的,磁盘——>内核读缓冲区——>Java堆内存——>内核网络缓冲区——>网卡,其中内核读缓冲区——>Java堆内存涉及到一次CPU拷贝,Java堆内存——>内核网络缓冲区也涉及到一次CPU拷贝。
而NIO的FileChannel.transferTo()方法可以做到这样的数据传输路径,磁盘——>内核读缓冲区——>内核网络缓冲区——>网卡。看到没,数据根本不经过Java应用程序,CPU无需参与数据的搬运,全靠DMA(Direct Memory Access)硬件完成。
6 总结
6.1 何时用BIO何时用NIO
BIO,适用于连接数少、并发低、传输数据量大的场景(如简单的文件上传后台)。代码简单,调试容易。
NIO,适用于连接数多、连接时间短(轻操作)、高并发的场景(如聊天服务器、Web服务器、网关)。
6.2 关于原生NIO
NIO的原生API略微复杂,容易出现Bug,如著名的JDK Epoll空轮询Bug,导致CPU100%。所以更推荐的方案是这样的,如果是做文件操作,直接用JDK的NIO(FileChannel、Path、Files),如果是做网络编程,强烈不提议直接写原生NIO代码,请使用Netty框架,Netty封装了NIO的复杂性,解决了底层Bug,是目前Java网络编程的实际标准。
我是欧阳方超,把事情做好了自然就有兴趣了,如果你喜爱我的文章,欢迎点赞、转发、评论加关注。我们下次见。
















暂无评论内容