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的基础上进行了进一步优化和扩展。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

龙迅LT6911GX HDMI 2.1转四 PORT MIPI或者LVDS,支持图像处理以及旋转,内置MCU以及LPDDR4

龙迅LT6911GX描述&#xff1a; LT6911GX是一款高性能的HDMI2.1到MIPI或LVDS芯片&#xff0c;用于VR/显示器应用。HDCP RX作为HDCP中继器的上游端&#xff0c;可以与其他芯片的HDCP TX协同工作&#xff0c;实现中继器的功能。对于HDMI2.1输入&#xff0c;LT6911GX可配置为3/4车…

Elasticsearch集群运维,重平衡、分片、宕节点、扩容

个人博客&#xff1a;无奈何杨&#xff08;wnhyang&#xff09; 个人语雀&#xff1a;wnhyang 共享语雀&#xff1a;在线知识共享 Github&#xff1a;wnhyang - Overview 参考 探索集群 Elasticsearch 中文文档 https://www.elastic.co/guide/en/elasticsearch/reference…

linux系统——wget命令

wget命令可以用于下载指定的url地址文件&#xff0c;支持断点续传&#xff0c;支持ftp&#xff0c;http协议下载&#xff0c;在下载普通文件时&#xff0c;即使网络出现故障&#xff0c;依然会不断尝试下载 wget命令直接加url地址 使用-o参数可以将下载文件改名&#xff0c;-c…

windows11 建立批处理bat文件来删除指定目录下的所有隐藏的文件。

今天在导入项目的时候发现之前项目中的文件夹中有很多隐藏的临时文件&#xff0c;这个文件应该是版本控制产生的&#xff0c;导致导入后文件夹上有X&#xff0c;然后里面文件是一个没有错。 我们来建立一个bat来&#xff0c;进行批量删除隐藏文件就可以了&#xff1a; echo o…

【Go】用 DBeaver、db browser 和 SqlCipher 读取 SqlCipher 数据库

本文档主要描述如何用 DBeaver、db browser 和 SqlCipher 上打开加密的 SQLite3 数据库(用 SqlCipher v3 加密) 软件版本 DBeaver&#xff1a;v24.1.0 SQLite-driver: sqlite-jdbc-3.46.0.0.jar dbbrowser-for-sqlite-cipher&#xff1a;3.12.2 SqlCipher cli(ubuntun)&am…

探索未来通信的新边界:AQChat一款融合AI的在线匿名聊天

探索未来通信的新边界&#xff1a;AQChat一款融合AI的在线匿名聊天 在数字时代&#xff0c;即时通讯变得无处不在&#xff0c;但隐私和性能仍旧是许多用户和开发者关注的焦点。今天&#xff0c;我要介绍一个开创性的开源项目 —— AQChat&#xff0c;它不仅重定义了在线匿名聊…

noVNC 小记

1. 怎么查看Ubuntu版本

Vue.js结合ASP.NET Core构建用户登录与权限验证系统

1. 环境准备2. 创建项目3. Vue配置步骤一: 安装包步骤二: 配置文件步骤三: 页面文件 4. 后台配置 在本教程中&#xff0c;我将利用Visual Studio 2022的强大集成开发环境&#xff0c;结合Vue.js前端框架和ASP.NET Core后端框架&#xff0c;从头开始创建一个具备用户登录与权限验…

Ubuntu server 24 (Linux) Zabbix 7.0 LTS 配置mail邮件报警

1 告警--媒介 选择右边默认模板修改 2 用户设置--配置--报警媒介 3 告警--动作--触发器动作 #测试 sudo systemctl stop zabbix-agent 本文使用postfix自建邮件服务器&#xff0c;如有需要请看

使用脚手架创建vue2项目(关闭eslint语法检查 、运行项目时自动打开网址、src文件夹简写方法)

使用脚手架创建vue2项目会默认安装的插件&#xff08;eslint) 这个插件是检查语法的。 假设我们在main.js中定义了一个变量&#xff0c;没有使用 eslint 就会检测出错误 &#xff08;事实是我们并没有写错而是eslint 给我们判断是错的&#xff0c;所以这样会很麻烦&#xff…

【SAP-ABAP】-权限批导-批量给某个角色导入事务码权限

需求&#xff1a;SAP期初上线的时候&#xff0c;业务顾问经常会遇到批量创建角色和分配角色权限的情况 岗位需求&#xff1a;一般是业务顾问定义权限&#xff0c;BASIS进行后期运维&#xff0c;今天讲两个批导功能&#xff0c;方便期初上线 主要函数&#xff1a;PRGN_READ_ROLE…

linux中DNS域名解析服务

DNS系统在网络中的作用就是维护一个地址数据库&#xff0c;其中记录了各种主机域名与IP地址的对应关系&#xff0c;以便为客户程序提供正向或反向的地址查询服务&#xff0c;即正向解析与反向解析。 正向解析&#xff1a;根据域名查IP地址&#xff0c;即将指定的域名解析为相对…

Kafka高频面试题整理

文章目录 1、什么是Kafka?2、kafka基本概念3、工作流程4、Kafka的数据模型与消息存储机制1)索引文件2)数据文件 5、ACKS 机制6、生产者重试机制:7、kafka是pull还是push8、kafka高性能高吞吐的原因1&#xff09;磁盘顺序读写&#xff1a;保证了消息的堆积2&#xff09;零拷贝机…

stm32_HAL_CAN(特点:广播试,检测线路是否空,id有优先级)

UART I2C SPI 介绍 CAN&#xff08;Controller Area Network&#xff09;接口是一种常用于嵌入式系统中的通信接口&#xff0c;尤其在汽车和工业领域。它由Robert Bosch GmbH在1986年开发&#xff0c;旨在提供一种可靠、高效、灵活的车载通信网络。CAN总线的主要 特点包括&am…

Flask基础2-Jinja2模板

目录 1.介绍 2.模板传参 1.变量传参 2.表达式 3.控制语句 4.过滤器 5.自定义过滤器 6.测试器 7.块和继承 flask基础1 1.介绍 Jinja2:是Python的Web项目中被广泛应用的模板引擎,是由Python实现的模板语言,Jinja2 的作者也是 Flask 的作 者。他的设计思想来源于Django的模…

Windows本地使用SSH连接VM虚拟机

WIN10 VM17.5 Ubuntu:20.04 1.网路设置 1)选择编辑->更改设置 配置完成 2.修改了服务器文件&#xff0c;修改sshd配置&#xff0c;在此文件下/etc/ssh/sshd_config&#xff0c;以下为比较重要的配置 PasswordAuthentication yes PermitRootLogin yes PubkeyAuthenticat…

华为昇腾异构计算架构CANN及AI芯片简介

异构计算架构CANN 异构计算架构CANN&#xff08;Compute Architecture for Neural Networks&#xff09;是华为针对AI场景推出的异构计算架构&#xff0c;向上支持多种AI框架&#xff0c;包括MindSpore、PyTorch、TensorFlow等&#xff0c;向下服务AI处理器与编程&#xff0c;…

ISCC2024 winterbegins

首先&#xff0c;用IDA打开程序&#xff0c;看到一大堆while(1)又套着while(1)的结构&#xff0c;肯定是控制流平坦化了&#xff0c;我们可以使用IDA插件d810去掉。 现在程序就好看多了。如果IDA没有显示这堆中文字符串&#xff0c;可以考虑使用IDA8.3&#xff0c;就可以显示…

MyBatis 动态 SQL 的详细内容讲解

1. MyBatis 动态 SQL 的详细内容讲解 文章目录 1. MyBatis 动态 SQL 的详细内容讲解2. 准备工作3. if 标签4. where 标签5. trim 标签6. set 标签7. choose when otherwise 标签8. foreach 标签8.1 批量删除8.2 批量添加 9. SQL 标签与 include 标签10. 总结&#xff1a;11. 最…

【介绍下ERP,什么是ERP?】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…