【深入理解Java IO流0x09】解读Java NIO核心知识(下篇)

1. NIO简介

在开始前,让我们再简单回顾一下NIO。
在传统的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式进行的。也就是说,当一个线程执行一个 I/O 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。
为了解决这个问题,在 Java1.4 版本引入了一种新的 I/O 模型 — NIO (New IO,也称为 Non-blocking IO) 。NIO 弥补了同步阻塞 I/O 的不足,它在标准 Java 代码中提供了非阻塞、面向缓冲、基于通道的 I/O,可以使用少量的线程来处理多个连接,大大提高了 I/O 效率和并发。
image.png
⚠️需要注意:使用 NIO 并不一定意味着高性能,它的性能优势主要体现在高并发和高延迟的网络环境下。当连接数较少、并发程度较低或者网络传输速度较快时,NIO 的性能并不一定优于传统的 BIO 。

2. NIO核心组件

NIO 主要包括以下三个核心组件:

  • Buffer(缓冲区):NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
  • Channel(通道):Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。
  • Selector(选择器):允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。

三者的关系:
image.png
我们都知道,BIO是以流的方式来处理数据的,而NIO是以Buffer缓冲器和Channel通道配合来处理数据的。简单来说就是,不妨把buffer类比为火车,那么channel就是铁路,NIO就是通过channel通道运输着存储数据的buffer来实现数据处理。buffer和channel各司其职,channel不与数据打交道,只负责运输。
BIO中,流的单向的。但是对于NIO,基于channel的概念,我们的读写都是双向的
下面我们一个一个来看NIO的组件。

3. 缓冲区Buffer

Buffer是缓冲器的抽象类:
image.png
Buffer的实现类中,我们用的最多的就是ByteBuffer了,它可以用来存储和操作字节数据。
作为一个缓冲区,最重要的功能就是写数据进去和从里面拿数据,也就是put方法和get方法。在具体看之前,我们先看一下Buffer类中定义的四个成员变量,具体含义见注释:

// 大小关系: 0 <= mark <= position <= limit <= capacity

// Buffer允许将位置直接定位到该标记处,这是一个可选属性
private int mark = -1;
// 下一个可以被读写的数据的位置(索引)。
// 从写操作模式到读操作模式切换的时候(flip),position归零,这样就可以从头读写
private int position = 0;
// Buffer 中可以读/写数据的边界。
// 写模式下,limit代表最多能写入的数据,一般等于capacity(可以通过limit(int newLimit)方法设置);
// 读模式下,limit 等于Buffer中实际写入的数据大小。
private int limit;
// Buffer可以存储的最大数据量,Buffer创建时设置且不可改变.
private int capacity;

Buffer 有读模式写模式这两种模式,分别用于从Buffer中读取数据或者向Buffer中写入数据。Buffer被创建之后默认是写模式,调用flip()可以切换到读模式。如果要再次切换回写模式,可以调用clear()或者compact()方法。
image.png
image.png
image.png
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 的值。

来看个实战:

import java.nio.*;

public class CharBufferDemo {
    public static void main(String[] args) {
        // 分配一个容量为8的CharBuffer
        CharBuffer buffer = CharBuffer.allocate(8);
        System.out.println("初始状态:");
        printState(buffer);

        // 向buffer写入3个字符
        buffer.put('a').put('b').put('c');
        System.out.println("写入3个字符后的状态:");
        printState(buffer);

        // 调用flip()方法,准备读取buffer中的数据,将 position 置 0,limit 的置 3
        buffer.flip();
        System.out.println("调用flip()方法后的状态:");
        printState(buffer);

        // 读取字符
        while (buffer.hasRemaining()) {
            System.out.print(buffer.get());
        }

        // 调用clear()方法,清空缓冲区,将 position 的值置为 0,将 limit 的值置为 capacity 的值
        buffer.clear();
        System.out.println("调用clear()方法后的状态:");
        printState(buffer);

    }

    // 打印buffer的capacity、limit、position、mark的位置
    private static void printState(CharBuffer buffer) {
        System.out.print("capacity: " + buffer.capacity());
        System.out.print(", limit: " + buffer.limit());
        System.out.print(", position: " + buffer.position());
        System.out.print(", mark 开始读取的字符: " + buffer.mark());
        System.out.println("\n");
    }
}

---------------------------------------------------------------
output:
初始状态:
capacity: 8, limit: 8, position: 0

写入3个字符后的状态:
capacity: 8, limit: 8, position: 3

准备读取buffer中的数据!

调用flip()方法后的状态:
capacity: 8, limit: 3, position: 0

读取到的数据:abc

调用clear()方法后的状态:
capacity: 8, limit: 8, position: 0

画个图方便大家理解:
image.png

4. 通道Channel

Channel 通道只负责传输数据、不直接操作数据。操作数据都是通过 Buffer 缓冲区来进行操作!通常,通道可以分为两大类:文件通道和套接字通道。

  • FileChannel:用于文件 I/O 的通道,支持文件的读、写和追加操作。FileChannel 允许在文件的任意位置进行数据传输,支持文件锁定以及内存映射文件等高级功能。FileChannel 无法设置为非阻塞模式,因此它只适用于阻塞式文件操作。
  • SocketChannel:用于 TCP 套接字 I/O 的通道。SocketChannel 支持非阻塞模式,可以与 Selector(下文会讲)一起使用,实现高效的网络通信。SocketChannel 允许连接到远程主机,进行数据传输。
  • ServerSocketChannel:用于监听 TCP 套接字连接的通道。与 SocketChannel 类似,ServerSocketChannel 也支持非阻塞模式,并可以与 Selector 一起使用。ServerSocketChannel 负责监听新的连接请求,接收到连接请求后,可以创建一个新的 SocketChannel 以处理数据传输。
  • DatagramChannel:用于 UDP 套接字 I/O 的通道。DatagramChannel 支持非阻塞模式,可以发送和接收数据报包,适用于无连接的、不可靠的网络通信。

因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。特别是在 UNIX 网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。
Channel 最核心的两个方法:

  1. read :读取数据并写入到 Buffer 中。
  2. write :将 Buffer 中的数据写入到 Channel 中。

这里我们以 FileChannel 为例演示一下如何复制文件:

@Test
public void test007() throws IOException{
    FileChannel readChannel = FileChannel.open(Paths.get("test.txt"),StandardOpenOption.READ);
    FileChannel writeChannel = FileChannel.open(Paths.get("test_nio.txt"),StandardOpenOption.WRITE,
            StandardOpenOption.CREATE);

    ByteBuffer buffer = ByteBuffer.allocate(1024);
    while(readChannel.read(buffer)!=-1){
        buffer.flip();
        writeChannel.write(buffer);
        buffer.clear();
    }
    readChannel.close();
    writeChannel.close();
}

5. 选择器Selector

Selector(选择器)是 NIO 中的一个关键组件,它允许一个线程处理多个 Channel。Selector 是基于事件驱动的 I/O 多路复用模型,主要运作原理是:通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。当事件发生时,比如:某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来。Selector 会将相关的 Channel 加入到就绪集合中。通过 SelectionKey 可以获取就绪 Channel 的集合,然后对这些就绪的 Channel 进行相应的 I/O 操作。
image.png
一个多路复用器 Selector 可以同时轮询多个 Channel,由于 JDK 使用了epoll()代替传统的 select 实现,所以它并没有最大连接句柄 1024/2048 的限制。这也就意味着只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。
Selector 可以监听以下四种事件类型:

  1. SelectionKey.OP_ACCEPT:表示通道接受连接的事件,这通常用于 ServerSocketChannel
  2. SelectionKey.OP_CONNECT:表示通道完成连接的事件,这通常用于 SocketChannel
  3. SelectionKey.OP_READ:表示通道准备好进行读取的事件,即有数据可读
  4. SelectionKey.OP_WRITE:表示通道准备好进行写入的事件,即可以写入数据。

Selector是抽象类,可以通过调用此类的open()静态方法来创建Selector实例。Selector可以同时监控多个SelectableChannel的 IO 状况,是非阻塞 IO 的核心。
一个Selector实例有三个SelectionKey集合:

  1. 所有的SelectionKey集合:代表了注册在该Selector上的Channel,这个集合可以通过keys()方法返回。
  2. 被选择的SelectionKey集合:代表了所有可通过select()方法获取的、需要进行 IO 处理的 Channel,这个集合可以通过selectedKeys()返回。
  3. 被取消的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() 方法立刻返回。
  • ……

来看一个使用Selector实现网络读写的简单demo:

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();
    }
  }
}

在示例中,我们创建了一个简单的服务器,监听 8080 端口,使用 Selector 处理连接、读取和写入事件。当接收到客户端的数据时,服务器将读取数据并将其打印到控制台,然后向客户端回复 “Hello, Client!”。

6. NIO零拷贝

零拷贝是提升 IO 操作性能的一个常用手段,像 ActiveMQ、Kafka 、RocketMQ等消息队列都用到了零拷贝。
零拷贝(Zero-Copy)是一种优化数据传输性能的技术,它最大限度地减少了在数据传输过程中的 CPU 和内存开销。在传统的数据传输过程中,数据通常需要在用户空间和内核空间之间进行多次拷贝,这会导致额外的 CPU 和内存开销。零拷贝技术通过避免这些多余的拷贝操作,实现了更高效的数据传输。
下图为零拷贝技术对比图:

CPU拷贝DMA拷贝系统调用上下文切换
传统方法22read+write4
mmap+write12mmap+write4
sendfile12sendfile2
sendfile+DMA gather copy02sendfile2

可以看出,无论是传统的 I/O 方式,还是引入了零拷贝之后,2 次 DMA(Direct Memory Access) 拷贝是都少不了的。因为两次 DMA 都是依赖硬件完成的。零拷贝主要是减少了 CPU 拷贝及上下文的切换。
Java对零拷贝的支持:

  • MappedByteBuffer是 NIO 基于内存映射(mmap)这种零拷贝方式的提供的⼀种实现,底层实际是调用了 Linux 内核的 mmap 系统调用。它可以将一个文件或者文件的一部分映射到内存中,形成一个虚拟内存文件,这样就可以直接操作内存中的数据,而不需要通过系统调用来读写文件。
  • FileChanneltransferTo()/transferFrom()是 NIO 基于发送文件(sendfile)这种零拷贝方式的提供的一种实现,底层实际是调用了 Linux 内核的 sendfile系统调用。它可以直接将文件数据从磁盘发送到网络,而不需要经过用户空间的缓冲区。

7. 总结

这篇文章我们主要介绍了 NIO 的核心知识点,包括 NIO 的核心组件和零拷贝。如果我们需要使用 NIO 构建网络程序的话,不建议直接使用原生 NIO,编程复杂且功能性太弱,推荐使用一些成熟的基于 NIO 的网络编程框架比如 Netty。Netty 在 NIO 的基础上进行了一些优化和扩展比如支持多种协议、支持 SSL/TLS 等等。

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

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

相关文章

第⑫讲:Ceph集群OSD扩缩容中Reblanceing数据的重分布

文章目录 1.Reblanceing数据重分布的概念2.验证Reblanceing触发的过程3.Reblanceing细节4.临时关闭Reblanceing机制 1.Reblanceing数据重分布的概念 当集群中OSD进行扩缩容操作后&#xff0c;会触发一个Reblanceing数据重分布的机制&#xff0c;简单的理解就是将扩缩容前后OSD…

服务调用-微服务小白入门(4)

背景 各个服务应用&#xff0c;有很多restful api&#xff0c;不论是用哪种方式发布&#xff0c;部署&#xff0c;注册&#xff0c;发现&#xff0c;有很多场景需要各个微服务之间进行服务的调用&#xff0c;大多时候返回的json格式响应数据多&#xff0c;如果是前端直接调用倒…

AI python

AI python 软件方面程序上的人工智能&#xff0c;和物理那种能跑机器人没关系

超聚变服务器快速收集硬件故障日志方法(iBMC)

1、使用网线直接连接服务器的Mgmt口&#xff0c;另外一端连接电脑 2、电脑随便配置一个192.168.2.101段的IP&#xff0c;除100外 3、使用以下默认信息连接IBMC&#xff0c;即可成功登录 默认连接地址&#xff1a;192.168.2.100 默认账号&#xff1a;Administrator 默认密码&am…

【Vit】Vision Transformer 入门与理解

在学习VIT之前&#xff0c;建议先把 Transformer 搞明白了&#xff1a;【transformer】入门与理解 做了那些改进&#xff1f; 看图就比较明白了&#xff0c;VIT只用了Encoder的部分&#xff0c;把每一个图片裁剪成若干子图&#xff0c;然后把一个子图flatten一下&#xff0c;…

[大模型]Langchain-Chatchat安装和使用

项目地址&#xff1a; https://github.com/chatchat-space/Langchain-Chatchat 快速上手 1. 环境配置 首先&#xff0c;确保你的机器安装了 Python 3.8 - 3.11 (我们强烈推荐使用 Python3.11)。 $ python --version Python 3.11.7接着&#xff0c;创建一个虚拟环境&#xff…

力扣HOT100 - 240. 搜索二维矩阵 II

解题思路&#xff1a; 从左下角开始&#xff0c;根据条件删除行和列。 class Solution {public boolean searchMatrix(int[][] matrix, int target) {int row matrix.length - 1;int col matrix[0].length - 1;int l 0;while (row > 0 && l < col) {if (targ…

宝宝洗衣机怎么选?四款畅销卓越婴儿洗衣机深度剖析!

近几年科技高速发展&#xff0c;我们的生活也因此变得更加便捷、健康高效。尤其是在家庭生活中&#xff0c;各种新兴家电的出现让我们的生活变得更加健康卫生。婴儿洗衣机也为现代家庭提供了极大的便捷。由于婴儿刚出生免疫力比较弱&#xff0c;所以建议婴儿的衣物尽量和大人的…

【面试八股总结】排序算法(一)

参考资料 &#xff1a;阿秀 一、冒泡排序 冒泡排序就是把小的元素往前交换或者把大的元素往后交换&#xff0c;比较相邻的两个元素&#xff0c;交换也发生在这两个元素之间。具体步骤&#xff1a; 比较相邻的元素。如果第一个比第二个大&#xff0c;就交换他们两个。对每一对…

细胞活性和细胞增殖检测试剂盒--CCK8试剂盒

Cell Counting Kit-8又称CCK-8试剂盒或者CCK8试剂盒。CCK8试剂盒基于WST-8法检测细胞的细胞活性和细胞增殖。 WST-8是MTT的升级产品&#xff0c;试剂盒的工作原理是在电子耦合试剂存在的情况下&#xff0c;可以被线粒体内的脱氢酶还原生成高度 水溶性的橙黄色的甲臜产物&#…

域权限维持—黄金票据和白金票据

黄金票据和白金票据 前言 某老哥的一次面试里问到了这个问题&#xff0c;故来做一番了解 该攻击方式在BlackHat 2014被提出&#xff0c;演讲者为Alva Duckwall & Benjamin Delpy&#xff08;gentilkiwi)进行了演示&#xff0c;该演讲提出了Kerberos协议实现过程中的设计…

Python数据分析案例42——基于Attention-BiGRU的时间序列数据预测

承接上一篇的学术缝合&#xff0c;排列组合模型&#xff0c;本次继续缝合模型演示。 Python数据分析案例41——基于CNN-BiLSTM的沪深300收盘价预测-CSDN博客 案例背景 虽然我自己基于各种循环神经网络做时间序列的预测已经做烂了.....但是还是会有很多刚读研究生或者是别的领…

2024-4-11-arm作业

汇编实现三个灯的闪烁 源代码&#xff1a; .text .global _start _start: 时钟使能LDR r0,0x50000A28ldr r1,[r0]orr r1,r1,#(0x3<<4)str r1,[r0]设置PE10输出LDR r0,0x50006000ldr r1,[r0]bic r1,r1,#(0x3<<20)orr r1,r1,#(0x1<<20)str r1,[r0]设置PE1…

TikTok如何矩阵养号?TK防关联引流系统助力TK账号安全运营

TK是 TikTok旗下的短视频社交媒体&#xff0c;平台目前是全球最火的短视频平台&#xff0c;目前全球活跃用户已经超过8亿。其中 TikTok的用户已经达到8亿。TK这款短视频社交媒体平台在海外的发展潜力非常大&#xff0c;也是国内很多人的创业目标&#xff0c;很多人都想从 TK这个…

Lua脚本使用手册(Redis篇)

Lua脚本 **简介&#xff1a;**Lua是一种功能强大的&#xff0c;高效&#xff0c;轻量级&#xff0c;可嵌入的脚本语言。它是动态类型语言&#xff0c;通过使用基于寄存器的虚拟机解释字节码运行&#xff0c;并具有增量垃圾收集的自动内存管理&#xff0c;是配置&#xff0c;脚…

【源码】2024最新海外刷单抢单平台源码/自带利息宝/理财活动/带搭建教程

源码描述&#xff1a; 前台是单语言 全开源 可二开的版本 CD&#xff1a;获取方式联系小编 微信&#xff1a;uucodes 公众号&#xff1a;资源猿

房贷还款(C语言)

一、运行结果&#xff1b; 二、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h> # include <math.h>int main() {//初始化变量值&#xff1b;double m, r 0.01;float d 300000;float p 6000;//运算还款所需月份&#xff1b;m log10…

婴儿洗衣机哪个牌子好?四款超值婴儿洗衣机汇总安利

婴儿洗衣机的优点很多&#xff0c;一是省时省力&#xff0c;二是安全卫生&#xff0c;虽说我们无法为孩子营造一个无菌的成长环境&#xff0c;但哪个宝妈宝爸不希望自己的孩子随时都能保持自己的清洁卫生呢&#xff1f;随着市场的不断增长&#xff0c;婴儿洗衣机的品牌也在不断…

llama-factory SFT系列教程 (一),大模型 API 部署与使用

文章目录 背景简介难点 前置条件1. 大模型 api 部署下一步阅读 背景 本来今天没有计划学 llama-factory&#xff0c;逐步跟着github的文档走&#xff0c;发现这框架确实挺方便&#xff0c;逐渐掌握了一些。 最近想使用 SFT 微调大模型&#xff0c;llama-factory 是使用非常广泛…

2024年MathorCup数学应用挑战赛C题思路分析(妈妈杯)

2024年第十四届MathorCup数学应用挑战赛C题解析 文章目录 题目概览第一问&#xff1a;货量预测第二问&#xff1a;运输线路变化的预测第三问&#xff1a;单目标优化第四问&#xff1a;排班计划的优化 MATLAB代码框架货量预测人员排班 2024年的MathorCup数学应用挑战赛再次为我…