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语句过程中的行为。 …...

wordpress后台更新后 前端没变化的解决方法
使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...

STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...

Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...

从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(九)
设备树移植 和uboot设备树修改的内容同步到kernel将设备树stm32mp157d-stm32mp157daa1-mx.dts复制到内核源码目录下 源码修改及编译 修改arch/arm/boot/dts/st/Makefile,新增设备树编译 stm32mp157f-ev1-m4-examples.dtb \stm32mp157d-stm32mp157daa1-mx.dtb修改…...

自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...

Springboot社区养老保险系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,社区养老保险系统小程序被用户普遍使用,为方…...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...

[ACTF2020 新生赛]Include 1(php://filter伪协议)
题目 做法 启动靶机,点进去 点进去 查看URL,有 ?fileflag.php说明存在文件包含,原理是php://filter 协议 当它与包含函数结合时,php://filter流会被当作php文件执行。 用php://filter加编码,能让PHP把文件内容…...

Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...