Java NIO(新 I/O)是 Java 提供的一个更为高效的 I/O 处理框架。Java NIO(New I/O)是对传统 I/O(java.io)模型的改进,它引入了非阻塞 I/O 操作和面向缓冲区的数据读写方式,解决了传统 I/O 模型中的性能瓶颈。NIO 的设计目标是使 I/O 操作更加高效,特别是在大数据量、高并发情况下,能够充分利用操作系统的底层 I/O 多路复用机制。
Java NIO 的核心概念包括:Buffer(缓冲区)、Channel(通道)、Selector(选择器)。
这些组件使得 Java NIO 在处理大量并发连接时,能够减少线程的消耗,提高系统的吞吐量。
区别维度 | BID | NIO |
---|---|---|
阻塞与非阻塞 | 传统的阻塞 I/O 模型,线程在进行 I/O 操作时会被阻塞,直到操作完成。在处理多个连接时,可能会创建大量线程,每个线程处理一个连接。这种方式的缺点是线程的开销较大,且响应速度较慢。线程数过多导致上下文切换和内存消耗大,效率低。 | Java NIO 提供了非阻塞 I/O 模型。在 NIO 中,I/O 操作不会阻塞当前线程,线程可以发起请求并继续执行其他任务。通过选择器(Selector),一个线程可以管理多个通道,从而减少了线程的数量,降低了上下文切换的成本,能够更有效地利用系统资源。 |
I/O 模 | 每个客户端连接都会占用一个线程,线程会被阻塞直到完成 I/O 操作。 | 使用 Channel 和 Buffer 进行数据传输,通过 Selector 实现单线程管理多个连接。 |
实现方式 | 通过 InputStream/OutputStream,每个线程处理一个 I/O 操作。 | 通过 Channel 和 Buffer,数据读写通过缓冲区进行,提供了非阻塞的读写方式。 |
Channel 是 NIO 中的一个接口,表示可以进行 I/O 操作的对象。Channel 可以进行读取和写入操作。
常用的 Channel 类型有:
Channel 是 IO 通讯的通道,类似于 InputStream、OutputStream,但是 Channel 是没有方向性的。
// FileInputStream/FileOutputStream
FileInputStream fis = new FileInputStream("example.txt");
FileChannel fileInputChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream("example.txt");
FileChannel fileOutputChannel = fos.getChannel();
// RandomAccessFile
RandomAccessFile raf = new RandomAccessFile("example.txt", "rw");
FileChannel randomAccessFileChannel = raf.getChannel();
// FileChannel
FileChannel channel = FileChannel.open(Paths.get("example.txt"), StandardOpenOption.READ);
// Socket
Socket socket = new Socket("localhost", 8080);
SocketChannel socketChannel = socket.getChannel();
// ServerSocket
ServerSocket serverSocket = new ServerSocket(8080);
ServerSocketChannel serverSocketChannel = serverSocket.getChannel();
// DatagramSocket
DatagramSocket datagramSocket = new DatagramSocket(8080);
DatagramChannel datagramChannel = DatagramChannel.open(); // 需独立创建
datagramChannel.bind(datagramSocket.getLocalSocketAddress());
NIO 中的数据传输是基于缓冲区(Buffer)的。数据读写必须先存入缓冲区,然后通过 Channel 进行传输。
Channel 读取或者写入的数据,都要写到 Buffer 中,才可以被程序操作。
Buffer 有几种类型:ByteBuffer, MappedByteBuffer, CharBuffer, IntBuffer, DoubleBuffer, LongBuffer 等。
Buffer 的主要方法:
put()
:将数据写入缓冲区。get()
:从缓冲区读取数据。flip()
:将缓冲区从写模式切换到读模式。clear()
:清空缓冲区,其实只是状态的改变,并不会真正清空。compact()
:将缓冲区中的未读数据向前移动,便于后续写操作。因为 Channel 没有方向性,所以 Buffer 为了区分读写,引入了读模式、写模式进行区分。
写模式:新创建的 Buffer 就是写模式、调用 clear 方法清空后。
读模式:调用 flip 方法后。
// 创建指定大小的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 获取字符集,假设文件使用 UTF-8 编码
Charset charset = StandardCharsets.UTF_8;
// 通过字符集获取
ByteBuffer encode = charset.encode("Hello, World!");
Selector 是 NIO 的一个核心组件,允许单线程通过轮询机制来监听多个 Channel 上的事件。Selector 通过轮询的方式检查是否有可用的 I/O 操作,从而避免了每个连接都占用一个线程。
channel.read(buffer);
buffer.flip();
//设置读模式buffer.get();
buffer.clear();
//设置写模式Java NIO 的工作流程可以分为以下几个步骤:
open()
方法打开相应的 Channel
,如 FileChannel
、SocketChannel
等。Buffer
用于读写数据,Buffer
的大小通常是固定的,写入的数据首先会被写入 Buffer
中。Channel
从文件或网络读取数据到 Buffer
中,或者将 Buffer
中的数据写入到 Channel
。Selector
监控多个 Channel
的状态。Selector
会返回一个就绪的 Channel
,应用程序可以对其进行读写操作。Channel
释放资源。Selector
,NIO 允许单个线程管理多个 Channel
,大大减少了线程的开销,提高了并发处理能力。Buffer
直接与内存进行交互,不像传统的 I/O 需要多次数据复制,减少了内存的消耗和处理的延迟。BIO(Blocking I/O)模型下,每次读取文件时,线程会被阻塞,直到完成读取操作。在传统的 BIO 中,我们通过 InputStream
来读取文件内容。下面是一个使用 BIO 方式读取文件的例子:
package fun.xuewei.nio.file;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* BIO 读取文件示例
*
* @author 薛伟
*/
public class BioFileReader {
public static void main(String[] args) throws IOException {
// 记录开始时间
long startTime = System.nanoTime();
// 使用传统的 BIO 读取文件
FileInputStream fis = new FileInputStream("example.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line); // 打印每一行
}
reader.close();
fis.close();
// 记录结束时间
long endTime = System.nanoTime();
System.out.println("\n\n\n");
System.out.println("=============================================================");
System.out.println("BIO 读取文件耗时: " + (endTime - startTime) / 1000000 + " 毫秒");
System.out.println("=============================================================");
}
}
在这段代码中:
BufferedReader
从文件流中逐行读取内容,直到文件结束。System.nanoTime()
来记录开始和结束时间,以便测量执行时间。NIO 文件读取的方式使用 FileChannel
和 ByteBuffer
,能够通过非阻塞的方式高效地处理 I/O 操作。你之前提供的代码就是一个 NIO 读取文件的示例。
package fun.xuewei.nio.file;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* NIO 读取文件示例
*
* @author 薛伟
*/
public class NioFileReader {
public static void main(String[] args) throws IOException {
// 记录开始时间
long startTime = System.nanoTime();
// 打开文件通道
FileInputStream fis = new FileInputStream("example.txt");
FileChannel fileChannel = fis.getChannel();
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 获取字符集,假设文件使用 UTF-8 编码
Charset charset = StandardCharsets.UTF_8;
// 读取文件内容到缓冲区
while (fileChannel.read(buffer) > 0) {
buffer.flip(); // 切换读模式
while (buffer.hasRemaining()) {
// 将字节缓冲区解码为字符并打印
System.out.print(charset.decode(buffer).toString());
}
buffer.clear(); // 清空缓冲区,准备下次读取
}
fileChannel.close();
fis.close();
// 记录结束时间
long endTime = System.nanoTime();
System.out.println("\n\n\n");
System.out.println("=============================================================");
System.out.println("NIO 读取文件耗时: " + (endTime - startTime) / 1000000 + " 毫秒");
System.out.println("=============================================================");
}
}
在这段代码中:
FileChannel
和 ByteBuffer
从文件中读取内容。System.nanoTime()
来记录执行时间。这两段程序的差异在于文件读取的方式:
FileChannel
和 ByteBuffer
,通过缓冲区和通道的组合进行读取,在处理大文件时能够更高效地读取数据。分别运行上面两个程序可以得到下面的结果:
=============================================================
BIO 读取文件耗时: 243 毫秒
=============================================================
=============================================================
NIO 读取文件耗时: 59 毫秒
=============================================================
通过执行时间的比较,可以得出以下结论:
在 Java NIO 中,通过利用 FileChannel
和操作系统提供的 零拷贝
(Zero-Copy)机制,可以实现高效的文件复制。零拷贝指的是在数据传输过程中,不需要将数据复制到用户空间,而是直接在内核空间进行操作,减少了内存拷贝和 CPU 的消耗。
在文件复制的场景中,使用 FileChannel.transferTo()
或 FileChannel.transferFrom()
可以实现零拷贝。这些方法直接将数据从一个通道传输到另一个通道,避免了中间缓冲区的创建和数据复制。
使用 transferTo()
方法进行零拷贝文件复制。FileChannel.transferTo()
方法允许将文件内容从一个 FileChannel
直接传输到另一个 FileChannel
,通常用于文件复制。
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class NioFileCopyZeroCopy {
public static void main(String[] args) {
// 源文件路径和目标文件路径
String sourceFilePath = "source.txt";
String destFilePath = "destination.txt";
try (FileInputStream fis = new FileInputStream(sourceFilePath);
FileOutputStream fos = new FileOutputStream(destFilePath)) {
// 获取源文件和目标文件的 FileChannel
FileChannel sourceChannel = fis.getChannel();
FileChannel destChannel = fos.getChannel();
// 使用 transferTo 实现零拷贝文件复制
long position = 0; // 从文件的起始位置开始复制
long size = sourceChannel.size(); // 获取源文件的大小
long bytesTransferred = 0;
// 执行零拷贝:从源文件到目标文件
while (bytesTransferred < size) {
bytesTransferred += sourceChannel.transferTo(position + bytesTransferred, size - bytesTransferred, destChannel);
}
System.out.println("文件复制完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
FileInputStream
和 FileOutputStream
被用来读取和写入文件。fis.getChannel()
获取源文件的 FileChannel
,通过 fos.getChannel()
获取目标文件的 FileChannel
。transferTo
实现零拷贝复制:
sourceChannel.transferTo(position, size, destChannel)
是 FileChannel
提供的零拷贝复制方法。它会将源文件的内容从指定位置(position
)复制到目标通道 destChannel
,直到复制完指定大小的数据。position
是文件的起始位置,size
是要复制的字节数。try-with-resources
语法自动关闭文件流和通道,确保资源释放。transferTo()
和 transferFrom()
方法,但实现细节可能有所不同。在 BIO 模型下,每个连接都会在一个独立的线程中进行处理,线程会阻塞直到 I/O 操作完成。以下是使用 BIO 实现的客户端与服务器通信的代码。
package fun.xuewei.nio.network;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO 服务器端
*
* @author 薛伟
*/
public class BioServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("BIO 服务器启动,等待连接...");
while (true) {
// 阻塞直到有客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端连接:" + clientSocket.getInetAddress());
// 获取输入流并读取数据
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String message = reader.readLine(); // 阻塞直到收到消息
System.out.println("收到消息:" + message);
// 给客户端发送响应
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
writer.println("消息已收到");
// 关闭连接
clientSocket.close();
}
}
}
package fun.xuewei.nio.network;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* BIO客户端
*
* @author 薛伟
*/
public class BioClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 8080);
System.out.println("连接到服务器...");
// 获取输出流并发送数据
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
writer.println("你好,服务器!");
// 获取输入流并读取服务器响应
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String response = reader.readLine();
System.out.println("服务器回应: " + response);
// 关闭连接
socket.close();
}
}
在 NIO 模型下,我们使用 SocketChannel
和 ServerSocketChannel
来实现客户端与服务器之间的通信,并且可以通过 Selector
来管理多个连接。以下是使用 NIO 实现的客户端与服务器通信的代码。
package fun.xuewei.nio.network;
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.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
/**
* Nio 服务器端
*
* @author 薛伟
*/
public class NioServer {
public static void main(String[] args) throws IOException {
// 创建 Selector 和 ServerSocketChannel
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
// 注册到 Selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO 服务器启动,等待连接...");
// 获取字符集,假设文件使用 UTF-8 编码
Charset charset = StandardCharsets.UTF_8;
while (true) {
// 等待事件发生
selector.select();
// 获取所有已准备就绪的 SelectionKey
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
// 处理接收客户端连接
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
// 将客户端连接注册到 Selector
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接:" + clientChannel.getRemoteAddress());
}
// 处理读取客户端数据
if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(256);
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
clientChannel.close();
System.out.println("客户端断开连接");
} else {
buffer.flip();
// 将字节缓冲区解码为字符并打印
String message = charset.decode(buffer).toString();
System.out.println("收到消息:" + message);
// 给客户端发送响应
buffer.clear();
String response = "消息已收到";
buffer.put(response.getBytes(charset)); // 使用正确的字符集编码
buffer.flip();
clientChannel.write(buffer);
}
}
}
}
}
}
package fun.xuewei.nio.network;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* NIO 客户端
*
* @author 薛伟
*/
public class NioClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
// 获取字符集,假设文件使用 UTF-8 编码
Charset charset = StandardCharsets.UTF_8;
// 发送消息到服务器
ByteBuffer buffer = ByteBuffer.allocate(256);
String message = "你好,NIO 服务器!";
buffer.put(message.getBytes(charset)); // 使用正确的字符集编码
buffer.flip();
socketChannel.write(buffer);
// 接收服务器响应
buffer.clear();
socketChannel.read(buffer);
buffer.flip();
// 将字节缓冲区解码为字符并打印
String response = charset.decode(buffer).toString();
System.out.println("服务器回应: " + response);
// 关闭 SocketChannel
socketChannel.close();
}
}