Java I/O模型
引言
根据冯.诺依曼结构,计算机结构分为5个部分:运算器、控制器、存储器、输入设备、输出设备。
输入设备和输出设备都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。
从计算机结构的视角来看,I/O描述了计算机系统与外部设备之间通信的过程。
从应用程序的视角来看,我们的应用程序对操作系统的内核发起了IO调用(系统调用),操作系统负责的内核执行具体的IO操作。也就是说,我们的应用程序实际上只是发起了IO操作的调用而已,具体的IO执行是由操作系统的内核来完成
UNIX系统中,IO模型一共有五种:同步阻塞I/O、同步非阻塞I/O、I/O多路复用、信号驱动I/O和异步IO。
Java中3种常见IO模型
BIO(同步阻塞IO模型)
同步阻塞IO模型中,应用程序发起read调用后,会一直阻塞,直到内核把数据拷贝到用户空间。

在客户端连接数量不高的情况下,是没问题的,但是,当面对十万甚至百万级连接的时候,BIO模型是无能为力的。
NIO
在传统的Java I/O模型中,I/O操作是以阻塞的方式进行的,也就是说,当一个线程执行一个I/O操作时,线程会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。
为了解决这个问题,在Java1.4版本引入了一种新的I/O模型——NIO。NIO弥补了同步阻塞IO的不足,他在标准Java代码中提供了非阻塞、面向缓冲(Buffer)、基于通道的IO(Channel),可以使用少量的线程来处理多个连接(Selector),大大提高了IO效率和并发。
需要注意:使用NIO并不一定意味着高性能,他的性能优势主要体现在高并发和高延迟的网络环境下,当连接数较少、并发程度较低或者网络传输速度较快时,NIO的性能并不一定优于传统的BIO。
NIO核心组件
NIO主要包括以下三个核心组件
- Buffer(缓冲区):NIO读写数据都是通过缓冲区进行操作的,读操作的时候将Channel中的数据填充到Buffer中,而写操作时将Buffer中的数据写入到Channel中。
- Channel(通道):Channel是一个双向的、可读可写的数据传输通道,NIO通过Channel来实现数据的输入输出。通道是一个抽象的概念,他可以代表文件、套接字或者其他数据源之间的连接。
- Selector(连接器):允许一个线程处理多个Channel,基于事件驱动的IO多路复用模型,所有的Channel都注册到Selector上,由Selector来分配线程处理事件。
三者关系如下所示

Buffer(缓冲区)
在传统的BIO中,数据的读写是面向流的,分为字节流和字符流。
在NIO中,读取数据时是将数据直接读取到缓冲区中的,写入数据时,也是将数据直接写入到缓冲区中。
Buffer的子类如下所示,其中,最常用的是ByteBuffer,他可以用来存储和操作字节数据。
可以将Buffer理解为一个数组,IntBuffer、FloatBuffer、CharBuffer等分别对应int[]、float[]、char[].
Buffer类中定义的四个关键成员变量
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
这四个成员变量的含义如下
- 容量(capacity):Buffer可以存储的最大数据量,Buffer创建时设置且不可改变。
- 界限(limit):Buffer中可以读、写数据的边界。写模式下,limit 代表最多能写入的数据,一般等于capacity。读模式下,limit等于Buffer中实际写入的数据大小。
- 位置(position):下一个可以被读写的数据的位置,从写操作模式到读操作模式切换的时候(flip),position会归零,这样就可以从头开始读写了。
- 标记(mark):Buffer允许将位置直接定位到该标记处,这是一个可选属性。
并且,上述变量满足如下关系:0<=mark<=position<=limit<=capacity
另外,Buffer有读模式和写模式这两种模式,分别用于从Buffer中读取数据或者向Buffer中写入数据。Buffer被创建之后默认是写模式,调用flip()可以切换到读模式。如果要再次切换回写模式,可以调用clear()或者compact()方法。


Buffer对象不能通过new调用构造方法创建对象,只能通过静态方法实例化Buffer。
以ByteBuffer为例:
// 分配堆内存
public static ByteBuffer allocate(int capacity);
// 分配直接内存
public static ByteBuffer allocateDirect(int capacity);
Buffer最核心的两个方法:
- get:读取缓冲区的数据
- put:向缓冲区写入数据
除了上述方法之外,其他的重要方法
-
flip: 将缓冲区从写模式切换到读模式,他会将limit的值设置为当前的position,将position值设为0。
-
clear:清空缓冲区,将缓冲区从读模式切换到写模式,并将position值设置为0,将limit的值设置为capacity的值。
Channel(通道)
Channel是一个通道,他建立了与数据源(如文件、网络套接字等)之间的连接。我们可以利用它来读取和写入数据,就像打开了一条自来水管,让数据在Channel中自由流动。
BIO中的流是单向的,分为各种InputStream(输入流)和OutputStream(输出流),数据只是在一个方向上传输,通道与流的不同之处在于通道是双向的,它可以用于读、写或者同时用于读写。
Channel与前面的Buffer打交道,读操作的时候将Channel中的数据填充到Buffer中,而写操作时将Buffer中的数据写入到Channel中。

另外,因为Channel是全双工的,所以他可以比流更好的映射底层操作系统的API,特别是在Unix网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。
Channel中,最常用的以下几种通道:
- FileChannel:文件访问通道
- SocketChannel、ServerSocketChannel:TCP通信通道
- DatagramChannel:UDP通信通道
Channel最核心的两个方法:
- read:读取数据并写入到Buffer中
- write:将Buffer中的数据写入到Channel中。
以FileChannel为例:
RandomAccessFile reader = new RandomAccessFile("/Users/guide/Documents/test_read.in", "r"))
FileChannel channel = reader.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
Selector(选择器)
Selector(选择器)是NIO中的一个关键组件,允许一个线程处理多个Channel。Selector是基于事件驱动的I/O多路复用模型,他的工作原理是:
- 通道注册: 通过Selector注册通道的事件,Selector会不断的轮询注册在其上的Channel。
- selector轮询等待事件发生:当事件发生时,比如:某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来。
- 选取就绪Channel进行IO操作:Selector会将相关的Channel加入到就绪集合中,通过selectionKey可以获取就绪Channel的集合。然后对这些就绪的Channel进行相应的IO操作。

一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以他并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责selector的轮询,就可以接入成千上万的客户端。
Selector可以监听以下四种事件类型:
- SelectionKey.OP_ACCEPT: 表示通道接受连接的事件,这通常用于ServerSocketChannel。
- SelectionKey.OP_CONNECT:表示通道完成连接的事件,这通常用于SocketChannel。
- SelectionKey.OP_READ: 表示通道准备好进行读取时间,即有数据可读
- SelectionKey.OP_WRITE:表示通道准备好进行写入的时间,即可以写入数据。
Selector是抽象类,可以通过调用此类的open()方法来创建Selector实例。Selector可以同时监控多个SelectableChannel的I/O状态,是非阻塞IO的核心。
一个selector实例有三个SelectionKey集合:
-
所有的SelectionKey集合:代表了注册在该Selector上的Channel,这个集合可以通过keys()方法返回。
-
被选择的SelectionKey集合:代表了所有可通过select()方法获取的、需要进行IO处理的Channel,这个集合可以通过selectedKeys()返回。
-
被取消的SelectionKey集合:代表了所有被取消注册关系的Channel,下一次执行select()方法时,这些Channel对应的SelectionKey会被彻底删除。
示例演示如何遍历被选择的SelectionKey集合并进行处理:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();if (key != null) {if (key.isAcceptable()) {// ServerSocketChannel 接收了一个新连接} else if (key.isConnectable()) {// 表示一个新连接建立} else if (key.isReadable()) {// Channel 有准备好的数据,可以读取} else if (key.isWritable()) {// Channel 有空闲的 Buffer,可以写入数据}}keyIterator.remove();
}
Selector还提供了一系列和select()相关的方法
-
int select(): 监控所有注册的Channel,当他们中间有需要处理的IO操作时,该方法返回,并将对应的SelectionKey加入被选择的SelectionKey集合,该方法返回这些Channel的数量。
-
int select(long timeout): 可以设置超时时长的select()操作。
-
int selectNow():执行一个立即返回的select()操作,相对于select()方法而言,该方法不会阻塞线程。
-
Selector wakeup(): 使一个还未返回的select()方法立刻返回。
示例程序
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.util.Iterator;
import java.util.Set;public class NioSelectorExample {public static void main(String[] args) {try {ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.socket().bind(new InetSocketAddress(8080));Selector selector = Selector.open();// 将 ServerSocketChannel 注册到 Selector 并监听 OP_ACCEPT 事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while (true) {int readyChannels = selector.select();if (readyChannels == 0) {continue;}Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();if (key.isAcceptable()) {// 处理连接事件ServerSocketChannel server = (ServerSocketChannel) key.channel();SocketChannel client = server.accept();client.configureBlocking(false);// 将客户端通道注册到 Selector 并监听 OP_READ 事件client.register(selector, SelectionKey.OP_READ);} else if (key.isReadable()) {// 处理读事件SocketChannel client = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int bytesRead = client.read(buffer);if (bytesRead > 0) {buffer.flip();System.out.println("收到数据:" +new String(buffer.array(), 0, bytesRead));// 将客户端通道注册到 Selector 并监听 OP_WRITE 事件client.register(selector, SelectionKey.OP_WRITE);} else if (bytesRead < 0) {// 客户端断开连接client.close();}} else if (key.isWritable()) {// 处理写事件SocketChannel client = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.wrap("Hello, Client!".getBytes());client.write(buffer);// 将客户端通道注册到 Selector 并监听 OP_READ 事件client.register(selector, SelectionKey.OP_READ);}keyIterator.remove();}}} catch (IOException e) {e.printStackTrace();}}
}
测试效果

NIO零拷贝
零拷贝是提升IO操作性能的一个常用手段,像ActiveMQ、Kafka、RocketMQ等都用到了零拷贝。
零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及CPU拷贝时间。也就是说,零拷贝主要解决操作系统在处理IO操作时频繁复制数据的问题。
零拷贝常用技术有:mmap+write、sendfile和sendfile+DMA gather copy。
零拷贝技术对比图:

从图中可以看出,无论是传统的IO方式,还是引入了零拷贝之后,2次DMA拷贝都是少不了的,因为两次DMA(将数据从输入设备传输到内存)都是依赖硬件完成的。零拷贝主要减少CPU拷贝以及上下文切换。
Java对零拷贝的支持
- MappedByteBuffer:是NIO基于内存映射提供的一种实现,底层实际上是调用了Linux内核的mmap系统调用,它可以将一个文件或者文件的一部分映射到内存,形成一个虚拟内存文件,这样就可以直接操作内存中的数据,而不需要通过系统调用来读写文件。
- FileChannel:FileChannel的transferTo()/transferFrom()是NIO基于发送文件(sendfile)这种零拷贝方式提供的一种实现,底层调用了linux内核的sendfile系统调用。
他可以直接将文件数据从磁盘发送到网络,而不需要经过用户空间的缓冲区。
代码示例:
private void loadFileIntoMemory(File xmlFile) throws IOException {FileInputStream fis = new FileInputStream(xmlFile);// 创建 FileChannel 对象FileChannel fc = fis.getChannel();// FileChannel.map() 将文件映射到直接内存并返回 MappedByteBuffer 对象MappedByteBuffer mmb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());xmlFileBuffer = new byte[(int)fc.size()];mmb.get(xmlFileBuffer);fis.close();
}
总结
文章主要介绍了NIO的核心组件以及零拷贝。如果需要使用NIO构建网络程序的话,不建议直接使用NIO,编程模型过于复杂,可以使用Netty,Netty在NIO的基础上进行了进一步优化和扩展。
相关文章:
Java I/O模型
引言 根据冯.诺依曼结构,计算机结构分为5个部分:运算器、控制器、存储器、输入设备、输出设备。 输入设备和输出设备都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。 从计算机结构的视角来看,I/O描述了…...
【简单介绍下Sass,什么是Sass?】
🎥博主:程序员不想YY啊 💫CSDN优质创作者,CSDN实力新星,CSDN博客专家 🤗点赞🎈收藏⭐再看💫养成习惯 ✨希望本文对您有所裨益,如有不足之处,欢迎在评论区提出…...
bat脚本—快速修改网络配置
一、bat编写前注意事项 windows桌面用文本文件打开把批命令输入在文本框中,保存采用ANSI编码,后缀用.bat 可参考博客——bat脚本简介学习原理以及具体创建方式 (文件扩展名位置) 语法准确性:严格遵循 BAT 脚本的语…...
node.js漏洞——
一.什么是node.js 简单的说 Node.js 就是运行在服务端的 JavaScript。 Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台。 Node.js 是一个事件驱动 I/O 服务端 JavaScript 环境,基于 Google 的 V8 引擎,V8 引擎执行 Javascript 的速度非常…...
Qt多线程之moveToThread()函数
文章目录 一、moveToThread()执行后,当前代码线程没有改变。二、对象执行moveToThread()后,哪些成员加入了子线程1、创建对象时不指定父对象2、对属性对象使用moveToThread加入子线程作用域3、将属性对象的创建放到子线程中执行 三、C内存模型 在使用“继…...
【WEB前端2024】智体OS:poplang编程控制成本小千元的长续航robot机器人底盘(开源)
【WEB前端2024】智体OS:poplang编程控制成本小千元的长续航robot机器人底盘(开源) 前言:dtns.network是一款主要由JavaScript编写的智体世界引擎(内嵌了three.js编辑器的定制版-支持以第一视角游览3D场馆)…...
动态规划法学习
当然,让我们用更生活化的语言和一个实际的例子来解释动态规划,以及如何在实践中应用它。 动态规划通俗理解 想象一下,你是个水果摊老板,每天要决定订购多少苹果,目标是最大化利润。但苹果的价格每天波动,…...
前端技术回顾系列 10|TS 泛型在类和接口中的应用
在微信中阅读,关注公众号:CodeFit。 创作不易,如果你觉得这篇文章对您有帮助,请不要忘了 点赞、分享 和 关注 我的公众号:CodeFit,为我的持续创作提供动力。 上文回顾:约束泛型(Generic Constraints) 上一篇文章我们回顾了 泛型 在 TypeScript 中的高级用法 —— 泛型…...
【Ardiuno】实验ESP32单片机自动配置Wifi功能(图文)
这里小飞鱼按照ESP32的示例代码,实验一下wifi的自动配置功能。所谓的自动配置,就是不用提前将wifi的名称和密码写到程序里,这样可以保证程序在烧录上传后,可以通过手机端的软件来进行配置,可以避免反复修改代码&#x…...
xml数据解析
XML Pull Parser(使用Android的XmlPullParser) 原理 Pull Parser允许应用程序代码从XML数据中“拉取”事件,而不是像SAX那样通过事件处理程序被“推送”。应用程序代码可以决定何时拉取下一个事件,如开始元素、结束元素或文本内…...
vite工程化搭建vue项目之自动按需导入
背景 当我们在使用vue3组合式开发的时候,大多数情况下我们的代码可能是这样的 <script setup lang"ts"> import { ref, reactive, toRefs, onMounted, computed } from vue; defineProps({}); </script><template><div></di…...
yolo-inference多后端+多任务+多算法+多精度模型 框架开发记录(python版)
先贴出github地址,欢迎大家批评指正:https://github.com/taifyang/yolo-inference 不知不觉LZ已经快工作两年了,由于之前的工作内容主要和模型部署相关,想着利用闲暇时间写一些推理方面的经验总结,于是有了这个工程。其…...
uniapp使用vue3语法构建自定义导航栏,适配小程序胶囊
具体代码 <template><view class"nav-wrapper-container" :style"height:navBarHeight px"><view class"nav-status-container" :style"height:navstatusBarHeight px;" /><view v-if"isCustom" clas…...
wpf、winform 监听USB拔插时触发
C# USB拔插监听 C#查找设备管理器中所有的 USB 设备 wpf、winform 监听USB拔插时触发 监听Windows USB 拔插时触发 private void MainWindow_Loaded(object sender, RoutedEventArgs e){FleckWebSocketConfig.OpenSocketConfig().GetAwaiter(); //websocket 服务开启用于监听W…...
C语言:指针笔试题
// 输入某一年的第几天,计算并输出它是这一年的第几月第几日。 /* 函数功能: 对给定的某一年的第几天,计算它是这一年的第几月第几日。 函数入口参数: 整形变量year,存储年; 整形变量yearDay,存储某一年的第几天&am…...
搜维尔科技:Movella旗下的Xsens在人形机器人开发中得到广泛应用
人形机器人的发展正在全球范围内受到广泛关注。作为机器人领域的重要分支,人形机器人因其具备高度仿真的外观和动作,以及更贴近人类的行为模式,有望逐渐成为人们日常生活和工业生产中的得力助手。在中国,这一领域的发展尤为引人注…...
k8s学习--kubernetes服务自动伸缩之水平伸缩(pod副本伸缩)HPA详细解释与案例应用
文章目录 前言HPA简介简单理解详细解释HPA 的工作原理监控系统负载模式HPA 的优势使用 HPA 的注意事项应用类型 应用环境1.metircs-server部署2.HPA演示示例(1)部署一个服务(2)创建HPA对象(3)执行压测 前言…...
Mock数据
Mock 数据 引入依赖 <dependency><groupId>com.github.jsonzou</groupId><artifactId>jmockdata</artifactId><version>4.3.0</version></dependency>mock 数据 MockConfig mockConfig new MockConfig().sizeRange(1, 1);A.…...
【MySQL】性能分析
https://www.bilibili.com/video/BV1Kr4y1i7ru/?p78 查看执行频次 查看当前数据库的 INSERT, UPDATE, DELETE, SELECT 访问频次: SHOW GLOBAL STATUS LIKE Com_______; 或者 SHOW SESSION STATUS LIKE Com_______; 慢查询日志 慢查询日志记录了所有执行时间超过指…...
MyBatis插件机制
MyBatis插件机制是该框架提供的一种灵活扩展方式,允许开发者在不修改框架源代码的情况下对MyBatis的功能进行定制和增强。这种机制主要通过拦截器(Interceptor)实现,使得开发者可以拦截和修改MyBatis在执行SQL语句过程中的行为。 …...
linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果 {//枚举 0 出现的次数//因…...
AspectJ 在 Android 中的完整使用指南
一、环境配置(Gradle 7.0 适配) 1. 项目级 build.gradle // 注意:沪江插件已停更,推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...
HTML前端开发:JavaScript 获取元素方法详解
作为前端开发者,高效获取 DOM 元素是必备技能。以下是 JS 中核心的获取元素方法,分为两大系列: 一、getElementBy... 系列 传统方法,直接通过 DOM 接口访问,返回动态集合(元素变化会实时更新)。…...
[论文阅读]TrustRAG: Enhancing Robustness and Trustworthiness in RAG
TrustRAG: Enhancing Robustness and Trustworthiness in RAG [2501.00879] TrustRAG: Enhancing Robustness and Trustworthiness in Retrieval-Augmented Generation 代码:HuichiZhou/TrustRAG: Code for "TrustRAG: Enhancing Robustness and Trustworthin…...
水泥厂自动化升级利器:Devicenet转Modbus rtu协议转换网关
在水泥厂的生产流程中,工业自动化网关起着至关重要的作用,尤其是JH-DVN-RTU疆鸿智能Devicenet转Modbus rtu协议转换网关,为水泥厂实现高效生产与精准控制提供了有力支持。 水泥厂设备众多,其中不少设备采用Devicenet协议。Devicen…...
聚六亚甲基单胍盐酸盐市场深度解析:现状、挑战与机遇
根据 QYResearch 发布的市场报告显示,全球市场规模预计在 2031 年达到 9848 万美元,2025 - 2031 年期间年复合增长率(CAGR)为 3.7%。在竞争格局上,市场集中度较高,2024 年全球前十强厂商占据约 74.0% 的市场…...
