跳至主要內容

Java NIO学习总结

fangzhipeng约 2679 字大约 9 分钟

简介

Java NIO(New I/O)是 Java 在 JDK 1.4 版本中引入的新的 I/O API。它提供了一种基于缓冲区、通道和非阻塞 I/O 模型的开发方式,相对于传统的 Java IO来说,Java NIO 更加灵活和高效。

Java NIO 的主要特点和组成部分包括:

  • 缓冲区(Buffer):Java NIO 的数据读写以缓冲区为中心,Buffer 类提供了读写数据的方法,来支持高效的 I/O 操作。常见的Buffer覆盖了能通过IO发送的基本数据类型:byte, short, int, long, float, double 和 char。如下:

    • ByteBuffer
    • CharBuffer
    • DoubleBuffer
    • FloatBuffer
    • IntBuffer
    • LongBuffer
    • ShortBuffer
  • 通道(Channel):通道是一个用于读写数据的对象,可以非阻塞地进行读写操作,提供了更高效的数据传输能力。在 Java NIO 中,大部分 I/O 操作都是基于通道进行的。JAVA NIO中的一些主要Channel的实现,如下:

    • FileChannel:从文件中读写数据
    • DatagramChannel:能通过UDP读写网络中的数据
    • SocketChannel:能通过TCP读写网络中的数据
    • ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样,对每一个新进来的连接都会创建一个SocketChannel。
  • 选择器(Selector):选择器是 Java NIO 提供的一种多路复用的机制,允许单个线程处理多个通道(如网络连接或文件),从而提高了系统的吞吐量和响应速度。

  • 非阻塞模式:Java NIO 支持非阻塞(非同步)I/O 操作,可以在等待 I/O 操作完成时同时执行其他任务,提高了程序的性能和响应能力。

  • 文件操作:Java NIO 提供了对文件的更强大和灵活的支持,包括文件读写、文件锁定、文件映射等功能。

Java NIO 可以用于构建高并发、高性能的网络服务器,特别是在面对大量连接的场景下。它提供了更多的控制权和灵活性,使开发者能够更好地利用系统资源,提高 I/O 操作的效率。同时,Java NIO 还被广泛应用于大数据处理、分布式系统和高性能计算等领域。

Channel

在 Java NIO 中,Channel(通道)是连接数据源和目标的管道,用于对数据进行读取和写入操作。它是 Java NIO 进行 I/O 操作的核心组件之一。

Channel 的特点和功能包括:

  1. 支持双向操作:Channel 可以用于读取和写入数据,因此它可以在一个方向上读取数据,同时在另一个方向上写入数据。
  2. 高效的数据传输:Channel 实现了高效的数据传输,可以直接将数据从缓冲区读取到通道,或者从通道写入到缓冲区,提高了数据传输的效率。
  3. 非阻塞模式:Channel 可以以非阻塞的方式进行读写操作,即在没有数据可读或可写时并不会阻塞线程,可以同时处理多个通道的 I/O 操作。

以下是一些常见的 Channel 类型:

  • FileChannel:用于对文件进行读写操作。
  • SocketChannel:用于通过 TCP 协议进行网络连接,进行网络数据的读写操作。
  • ServerSocketChannel:用于监听和接收客户端的连接请求。
  • DatagramChannel:用于通过 UDP 协议进行网络数据的读写操作。
  • Pipe.SinkChannel 和 Pipe.SourceChannel:用于在同一程序中进行线程间通信。

FileChannel

以下是一个使用 FileChannel 进行文件读写操作的基本示例:

import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelExample {
    public static void main(String[] args) {
        try {
            // 打开一个 RandomAccessFile,以读写模式
            RandomAccessFile file = new RandomAccessFile("test.txt", "rw");
            FileChannel channel = file.getChannel();

            // 从文件中读取数据到缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int bytesRead = channel.read(buffer);
            while (bytesRead != -1) {
                buffer.flip(); // 切换缓冲区为读取模式
                while (buffer.hasRemaining()) {
                    System.out.print((char) buffer.get()); // 逐个字节读取并输出
                }
                buffer.clear(); // 清空缓冲区
                bytesRead = channel.read(buffer);
            }

            // 将数据写入文件
            String data = "Hello, FileChannel!";
            buffer.clear();
            buffer.put(data.getBytes());
            buffer.flip(); // 切换缓冲区为写入模式
            channel.write(buffer);

            // 关闭通道和文件
            channel.close();
            file.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

以上示例演示了如何使用 FileChannel 从文件中读取数据,并将数据写入到文件中。

  • 首先通过 RandomAccessFile 创建一个文件对象,并通过 getChannel() 获取文件对应的 FileChannel。
  • 然后创建一个 ByteBuffer 作为缓冲区,通过 read() 方法从文件通道中读取数据到缓冲区,再通过 flip() 切换为读取模式逐个字节输出。
  • 我们将字符串数据写入到缓冲区,并通过 write() 方法将数据写入文件通道。
  • 最后,关闭通道和文件。

Buffer

Buffer 是 Java NIO 中的一个关键组件,用于高效地进行数据读写操作。它是一个对象数组,可以存储不同类型的数据,并提供了一系列方法来管理数据的读写。

Buffer 的主要属性和方法包括:

  1. 容量(Capacity):Buffer 的容量是在创建时预先确定的,它表示可以存储的最大数据量。创建 Buffer 时会指定容量,并且无法更改。

  2. 位置(Position):位置表示下一个要读取或写入的元素索引,初始位置为 0。对于写入操作,每次写入后位置会自动递增;对于读取操作,每次读取后位置也会递增。

  3. 上界(Limit):上界表示缓冲区中已经存储的元素数量。在写入模式下,上界等于缓冲区的容量;在读取模式下,上界等于最后一个写入元素的索引加一。

  4. 标记(Mark):可以通过 mark() 方法将当前位置设置为标记位置,并通过 reset() 方法将位置重置为标记位置。

  5. 读写模式切换:flip() 方法将写入模式切换为读取模式,同时将位置设置为 0,并将上界设置为当前位置。clear() 方法将读取模式切换为写入模式,同时将位置设置为 0,并将上界设置为容量。

2.png
2.png

基本的 Buffer 类型包括 ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer 和 DoubleBuffer,它们分别用于读写不同类型的数据。

Buffer 的常用操作包括:

  • put():向缓冲区写入数据。
  • get():从缓冲区读取数据。
  • flip():切换为读取模式,重置位置和上界,准备读取缓冲区中的数据。
  • clear():切换为写入模式,重置位置和上界,准备写入数据到缓冲区。
  • rewind():重置位置为 0,保持上界不变,准备重新读取缓冲区中的数据。
  • compact():在写入模式下,将所有未读完的数据移到缓冲区的开头,重置位置和上界,准备继续写入数据。

Buffer 在进行数据读写时,会通过位置来确定当前要操作的数据元素,通过上界来限制读写的范围。通过适当地使用这些方法,可以有效地管理缓冲区的状态,实现高效的数据处理。

下面是一个使用 Buffer 进行基本操作的示例代码:

import java.nio.Buffer;
import java.nio.ByteBuffer;

public class BufferExample {
    public static void main(String[] args) {
        // 创建一个 ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(10);

        // 写入数据到缓冲区
        buffer.put((byte) 'H');
        buffer.put((byte) 'e');
        buffer.put((byte) 'l');
        buffer.put((byte) 'l');
        buffer.put((byte) 'o');

        // 切换为读取模式
        buffer.flip();

        // 从缓冲区读取数据
        while (buffer.hasRemaining()) {
            System.out.print((char) buffer.get());
        }
        System.out.println();

        // 清空缓冲区
        buffer.clear();

        // 再次写入数据
        buffer.put((byte) 'W');
        buffer.put((byte) 'o');
        buffer.put((byte) 'r');
        buffer.put((byte) 'l');
        buffer.put((byte) 'd');

        // 切换为读取模式
        buffer.flip();

        // 从缓冲区读取部分数据
        System.out.println("First: " + (char) buffer.get());
        System.out.println("Second: " + (char) buffer.get());

        // compact() 方法演示
        buffer.compact();

        // 从缓冲区读取剩余数据
        while (buffer.hasRemaining()) {
            System.out.print((char) buffer.get());
        }
        System.out.println();

        // rewind() 方法演示
        buffer.rewind();

        // 从缓冲区重新读取数据
        while (buffer.hasRemaining()) {
            System.out.print((char) buffer.get());
        }
        System.out.println();
    }
}

以上示例演示了 ByteBuffer 的常用操作。首先创建一个容量为 10 的 ByteBuffer,然后通过 put() 方法写入数据到缓冲区。接着,通过 flip() 方法切换为读取模式,并使用 get() 方法逐个字节读取数据并输出。然后,通过 clear() 方法清空缓冲区,并再次使用 put() 方法写入数据。再次切换为读取模式,并使用 get() 方法读取部分数据。随后演示了 compact() 方法的使用,可以将缓冲区中未读完的数据移到缓冲区的开头。最后,通过 rewind() 方法将缓冲区的位置重置为 0,并重新读取缓冲区中的数据。

Selector

Selector 是一种多路复用的机制,允许单个线程同时监听多个通道的事件。通过使用 Selector,可以实现在单个线程中处理多个通道的 I/O 操作,提高系统的吞吐量和响应性能。

nio-selector
nio-selector

Selector 的工作原理如下:

  1. 创建 Selector 对象:通过调用 Selector.open() 方法创建一个 Selector 实例。

  2. 注册通道:将需要监听的通道注册到 Selector 上,并指定感兴趣的事件,如读(SelectionKey.OP_READ)、写(SelectionKey.OP_WRITE)、连接(SelectionKey.OP_CONNECT)或接受(SelectionKey.OP_ACCEPT)等。

  3. 轮询就绪事件:通过调用 Selector 的 select() 方法来轮询监听注册的通道,当有通道有感兴趣的事件发生时,该方法将返回。

  4. 处理就绪事件:通过 selectedKeys() 方法获取到 Selector 所管理的所有就绪的 SelectionKey,然后遍历这些 SelectionKey,根据就绪的事件类型执行相应的处理逻辑。

  5. 取消注册或更新事件:如果某个通道的事件处理完成后,需要取消注册或更新感兴趣的事件,可以通过调用 SelectionKey 对象的相应方法进行操作。

Selector 的优势在于可以使用较少的线程来处理多个通道的 I/O 操作,避免了为每个通道创建一个线程的开销。这使得它非常适用于需要同时处理大量连接的网络编程场景。通过 Selector,开发者可以实现高并发、高性能的网络服务器,提高了系统的资源利用率和响应速度。

需要注意的是,Selector 是非阻塞模式下的特性,因此它和非阻塞的通道(如 SocketChannel 和 ServerSocketChannel)结合使用,以实现高效的 I/O 操作。

总结

本文主要介绍了NIO的一些基本组件,主要包括通道Channel、缓冲区Buffer、选择器Selecotor组件。后面的文章将以案例的形式介绍SocketChannel、ServerSocketChannel的网络编程。