Java NIO学习总结
简介
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 的特点和功能包括:
- 支持双向操作:Channel 可以用于读取和写入数据,因此它可以在一个方向上读取数据,同时在另一个方向上写入数据。
- 高效的数据传输:Channel 实现了高效的数据传输,可以直接将数据从缓冲区读取到通道,或者从通道写入到缓冲区,提高了数据传输的效率。
- 非阻塞模式: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 的主要属性和方法包括:
容量(Capacity):Buffer 的容量是在创建时预先确定的,它表示可以存储的最大数据量。创建 Buffer 时会指定容量,并且无法更改。
位置(Position):位置表示下一个要读取或写入的元素索引,初始位置为 0。对于写入操作,每次写入后位置会自动递增;对于读取操作,每次读取后位置也会递增。
上界(Limit):上界表示缓冲区中已经存储的元素数量。在写入模式下,上界等于缓冲区的容量;在读取模式下,上界等于最后一个写入元素的索引加一。
标记(Mark):可以通过
mark()
方法将当前位置设置为标记位置,并通过reset()
方法将位置重置为标记位置。读写模式切换:
flip()
方法将写入模式切换为读取模式,同时将位置设置为 0,并将上界设置为当前位置。clear()
方法将读取模式切换为写入模式,同时将位置设置为 0,并将上界设置为容量。
基本的 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 操作,提高系统的吞吐量和响应性能。
Selector 的工作原理如下:
创建 Selector 对象:通过调用
Selector.open()
方法创建一个 Selector 实例。注册通道:将需要监听的通道注册到 Selector 上,并指定感兴趣的事件,如读(
SelectionKey.OP_READ
)、写(SelectionKey.OP_WRITE
)、连接(SelectionKey.OP_CONNECT
)或接受(SelectionKey.OP_ACCEPT
)等。轮询就绪事件:通过调用 Selector 的
select()
方法来轮询监听注册的通道,当有通道有感兴趣的事件发生时,该方法将返回。处理就绪事件:通过
selectedKeys()
方法获取到 Selector 所管理的所有就绪的 SelectionKey,然后遍历这些 SelectionKey,根据就绪的事件类型执行相应的处理逻辑。取消注册或更新事件:如果某个通道的事件处理完成后,需要取消注册或更新感兴趣的事件,可以通过调用 SelectionKey 对象的相应方法进行操作。
Selector 的优势在于可以使用较少的线程来处理多个通道的 I/O 操作,避免了为每个通道创建一个线程的开销。这使得它非常适用于需要同时处理大量连接的网络编程场景。通过 Selector,开发者可以实现高并发、高性能的网络服务器,提高了系统的资源利用率和响应速度。
需要注意的是,Selector 是非阻塞模式下的特性,因此它和非阻塞的通道(如 SocketChannel 和 ServerSocketChannel)结合使用,以实现高效的 I/O 操作。
总结
本文主要介绍了NIO的一些基本组件,主要包括通道Channel、缓冲区Buffer、选择器Selecotor组件。后面的文章将以案例的形式介绍SocketChannel、ServerSocketChannel的网络编程。