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;

这四个成员变量的含义如下

  1. 容量(capacity):Buffer可以存储的最大数据量,Buffer创建时设置且不可改变。
  2. 界限(limit):Buffer中可以读、写数据的边界。写模式下,limit 代表最多能写入的数据,一般等于capacity。读模式下,limit等于Buffer中实际写入的数据大小。
  3. 位置(position):下一个可以被读写的数据的位置,从写操作模式到读操作模式切换的时候(flip),position会归零,这样就可以从头开始读写了。
  4. 标记(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最核心的两个方法:

  1. read:读取数据并写入到Buffer中
  2. 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的基础上进行了进一步优化和扩展。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/852176.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在 Kubernetes 上拉取 Harbor 私有仓库镜像并部署服务

上一篇讲解了IntelliJ IDEA和Jib Maven插件配合&#xff0c;镜像一键推送到Harbor私服仓库&#xff0c;今天来讲解下怎么让k8s直接拉取Harbor 私有仓库上面的镜像 创建 Kubernetes Secret 用于拉取镜像 因为 Harbor 仓库是私有的&#xff0c;我们需要创建一个 Kubernetes Sec…

JavaScript面向对象

一、编程思想 面向过程介绍 面向过程就是分析出解决问题所需要的步骤&#xff0c;然后用函数把这些一步一步实现&#xff0c;使用的时候再一个一个依次调用就可以了。 面向过程&#xff0c;就是按照我们分析好了的步骤&#xff0c;按照步骤解决问题。 面向对象编程&#xf…

阻抗控制(Impedance Control)和导纳控制(Admittance Control)例子

阻抗控制(Impedance Control)和导纳控制(Admittance Control) 是两种用于机械臂或机器人交互控制的策略。阻抗控制定义的是机器人端部的力和位置之间的关系,而导纳控制则定义的是外力和运动之间的关系。导纳控制常用于处理机器人与环境交互中的力控制问题。 适用场景对比…

185.二叉树:二叉搜索树的最近公共祖先(力扣)

代码解决 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/class Solution { public:// 函数用于寻找二叉搜索树中节点 p 和 q 的最低…

Honor of Kings 2024.06.13 (2)

【第一局】准确的说&#xff0c;其实对面优势更加明显&#xff0c;可惜黄忠和墨子喜欢杀人&#xff0c;而我又是不喜欢杀人的&#xff0c;打了好几次失误 【第二局】阵容本来很有优势&#xff0c;这个二呆射手跟第一局黄忠一样爱杀人&#xff0c;应该说三路的输出都爱杀人&…

小主机折腾记26

双独立显卡调用问题 前两天将tesla p4从x99大板上拆了下来&#xff0c;将880G5twr上的rx480 4g安装到了x99大板上&#xff0c;预计是dg1输出&#xff0c;rx480做3d运算。安装完驱动后&#xff0c;还想着按照之前tesla p4的设置方法去设置rx480&#xff0c;结果果然&#xff0c…

Serverless 使用OOS将http文件转存到对象存储

目录 背景介绍 系统运维管理OOS 文件转存场景 前提条件 实践步骤 附录 示例模板 背景介绍 系统运维管理OOS 系统运维管理OOS&#xff08;CloudOps Orchestration Service&#xff09;提供了一个高度灵活和强大的解决方案&#xff0c;通过精巧地编排阿里云提供的OpenAPI…

AcWing 477:神经网络 ← 拓扑排序+链式前向星

【题目来源】https://www.acwing.com/problem/content/479/【题目描述】 人工神经网络&#xff08;Artificial Neural Network&#xff09;是一种新兴的具有自我学习能力的计算系统&#xff0c;在模式识别、函数逼近及贷款风险评估等诸多领域有广泛的应用。 对神经网络的研究…

Rust : windows下protobuf和压缩传输方案

此前dbpystream库是用python开发 web api。今天在rust中试用一下protobuf。 本文关键词&#xff1a;编译器、protobuf、proto文件、序列化、zstd压缩&#xff0c;build。 一、 protobuf编译器下载 具体见相关文章。没有编译器&#xff0c;protobuf无法运行。 windows参见&am…

【皇帝的新衣】虚拟小组长的团队管理

团队有时候会需要设立虚拟小组长来分组帮忙管理&#xff0c;那么&#xff0c;虚拟小组的负责人应当怎么做好管理动作&#xff1f; 目前很多大厂追求团队管理上的扁平化&#xff0c;但真正有实职的领导们一般管理30人数&#xff0c;此时需要一个虚拟小组长来分组帮忙管理。 一、…

字节出品SDXL-Lightning:文生图开放模型新突破

生成模型的研究中&#xff0c;文本到图像的生成一直是一个充满挑战的任务。传统的扩散模型虽然在生成质量上取得了显著的成果&#xff0c;但其生成过程往往需要大量的迭代步骤&#xff0c;这不仅导致计算成本高昂&#xff0c;而且生成速度缓慢&#xff0c;难以满足实时或近实时…

【MATLAB源码-第225期】基于matlab的计算器GUI设计仿真,能够实现基础运算,三角函数以及幂运算。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 界面布局 计算器界面的主要元素分为几大部分&#xff1a;显示屏、功能按钮、数字按钮和操作符按钮。 显示屏 显示屏&#xff08;Edit Text&#xff09;&#xff1a;位于界面顶部中央&#xff0c;用于显示用户输入的表达式和…

没有特斯拉的开源专利,就没有中国电动车产业今天的成就?

原文链接&#xff1a;没有特斯拉的开源专利&#xff0c;就没有中国电动车产业今天的成就&#xff1f; 特斯拉的开源专利&#xff0c;对中国电动车产业的影响有多大&#xff1f; 2014年6月12日&#xff08;June 12, 2014&#xff09;&#xff0c;特斯拉&#xff08;TESLA&…

nodejs 某音douyin网页端搜索接口及x_bogus、a_bogus(包含完整源码)(2024-06-13)

前言 x_bogus或a_bogus算法大概是对数据、ua、时间戳、浏览器的几个指纹进行计算&#xff0c;拿到一个110位大数组&#xff0c;然后转字符&#xff0c;在头部再添加十二位随机字符&#xff0c;再进行魔改的base64加密。 问&#xff1a;抖音的x_bogus、a_bogus值有什么用&#x…

网工内推 | 外企、上市公司运维工程师,有软考中高项证书优先

01 优尼派特&#xff08;苏州&#xff09;物流有限公司 &#x1f537;招聘岗位&#xff1a;软件运维测试工程师 &#x1f537;任职要求&#xff1a; 1、负责公司自主研发的软件售后服务工作, 包括软件的安装, 调试, 升级,培训, 参数配置, 需求与Bug的处理; 2、负责数据库升级及…

远程主机强迫关闭了一个现有的连接redis

引言 在使用 Redis 进行开发和运维过程中&#xff0c;我们可能会遇到 Redis 连接被远程主机强制关闭的情况。本文将介绍造成这种情况的原因&#xff0c;并给出一些处理方法和建议。 远程主机强制关闭连接的原因 远程主机强制关闭连接通常是由于网络不稳定、连接超时、Redis 配…

数据质量测试:测试数据有效性和准确性的方法

以下为作者观点&#xff0c;来看看你认同吗&#xff1f; 如果西西弗斯&#xff08;编者注&#xff1a;希腊神话中的人物&#xff09;是一个数据分析师或数据科学家&#xff0c;他在山上滚动的巨石将是他的数据质量保障。即使所有获取、处理和建模的工程流程都无懈可击&#xf…

HarmonyOs修改应用名称和图标方法

最近在开发Harmony应用&#xff0c;发现修改app.json5下的lable:app_name和icon不生效 后来经过查找&#xff0c;原来还需要更改entry下的src/main/module.json5才行&#xff0c;具体操作路径是&#xff1a; 更改后生效&#xff1a;

Vue31-自定义指令:总结

一、自定义函数的陷阱 1-1、自定义函数名 自定义函数名&#xff0c;不能用驼峰式&#xff01;&#xff01;&#xff01; 示例1&#xff1a; 示例2&#xff1a; 1-2、指令回调函数的this 【回顾】&#xff1a; 所有由vue管理的函数&#xff0c;里面的this直接就是vm实例对象。…

Linux发邮件的工具推荐有哪些?如何配置?

Linux发邮件的功能怎么样&#xff1f;Linux系统如何设置服务器&#xff1f; 在Linux操作系统中&#xff0c;有多种工具可供选择用来发送电子邮件&#xff0c;每种工具都有其独特的特点和适用场景。AokSend将介绍几种常用的Linux发邮件工具&#xff0c;并分析它们的优缺点和适用…