一、Java NIO 基础概念
Java NIO(New Input/Output)是从 Java 1.4 版本开始引入的新的 IO API,它提供了与标准 IO 不同的工作方式。主要特点包括:
- 面向缓冲区:数据读取到一个稍后处理的缓冲区,需要时可在缓冲区中前后移动,增加了处理过程中的灵活性。
- 非阻塞 IO:允许线程在等待数据时执行其他任务,提高了系统的吞吐量和响应性。
- 选择器:一个选择器可以监听多个通道的事件(如连接打开、数据到达等),从而让一个线程可以处理多个通道。
二、核心组件详解
1. 缓冲区(Buffer)
缓冲区是一个用于存储特定基本数据类型的容器,所有缓冲区都具有以下属性:
- capacity:容量,即缓冲区能够容纳的数据元素的最大数量,在创建时确定且不能更改。
- position:位置,下一个要读取或写入的数据元素的索引。
- limit:限制,缓冲区中可以读取或写入的最大位置,位于 limit 后的数据不可读写。
- mark:标记,一个备忘位置,调用 mark () 方法将 mark 设为当前的 position 值,之后可通过 reset () 方法将 position 恢复到标记的位置。
常用方法:
allocate(int capacity)
:创建一个指定容量的缓冲区put(data)
:向缓冲区写入数据flip()
:切换到读模式(将 limit 设为 position,position 设为 0)get()
:从缓冲区读取数据clear()
:清空缓冲区(将 position 设为 0,limit 设为 capacity)rewind()
:重置缓冲区(将 position 设为 0,mark 被丢弃)
示例代码:
import java.nio.IntBuffer;
public class BufferExample {
public static void main(String[] args) {
// 创建一个容量为10的IntBuffer
IntBuffer buffer = IntBuffer.allocate(10);
// 向缓冲区写入数据
for (int i = 0; i < 5; i++) {
buffer.put(i);
}
// 切换到读模式
buffer.flip();
// 从缓冲区读取数据
while (buffer.hasRemaining()) {
System.out.print(buffer.get() + " ");
}
System.out.println();
// 重置缓冲区,以便再次读取
buffer.rewind();
// 再次读取数据
System.out.println("Rewind后再次读取:");
while (buffer.hasRemaining()) {
System.out.print(buffer.get() + " ");
}
// 清空缓冲区
buffer.clear();
System.out.println("\nclear后position: " + buffer.position());
System.out.println("clear后limit: " + buffer.limit());
}
}
2. 通道(Channel)
通道是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。与流不同的是,通道是双向的,支持异步读写操作,且通道始终与缓冲区交互。
常用通道实现:
FileChannel
:用于文件读写SocketChannel
:用于 TCP 网络通信的客户端ServerSocketChannel
:用于 TCP 网络通信的服务器端DatagramChannel
:用于 UDP 网络通信
示例代码:使用 FileChannel 复制文件
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("source.txt");
FileOutputStream fos = new FileOutputStream("destination.txt");
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel()) {
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 从输入通道读取数据到缓冲区
while (inChannel.read(buffer) != -1) {
// 切换到读模式
buffer.flip();
// 将缓冲区的数据写入输出通道
outChannel.write(buffer);
// 清空缓冲区,准备下一次读取
buffer.clear();
}
System.out.println("文件复制完成");
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 选择器(Selector)
选择器是 Java NIO 中实现非阻塞 IO 的核心组件,它允许一个线程处理多个通道的事件。通过使用选择器,线程可以监听多个通道的连接、数据到达等事件,从而高效地管理多个通道。
使用步骤:
- 创建 Selector:
Selector selector = Selector.open();
- 将通道注册到选择器:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
- 轮询选择器:
selector.select();
- 获取就绪的事件:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
- 处理事件:遍历 selectedKeys,根据 key 的类型处理相应的事件
示例代码:使用 Selector 实现简单的非阻塞服务器
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 NioServerExample {
private static final int PORT = 8080;
public static void main(String[] args) {
try (Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
// 配置服务器套接字通道
serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
serverSocketChannel.configureBlocking(false);
// 将服务器套接字通道注册到选择器,监听接受连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动,监听端口: " + PORT);
// 轮询选择器
while (true) {
// 阻塞等待就绪的通道
selector.select();
// 获取就绪的事件集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
// 处理每个就绪的事件
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 处理接受连接事件
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = ssc.accept();
System.out.println("接受新连接: " + socketChannel);
// 配置客户端通道为非阻塞模式,并注册到选择器,监听读取事件
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
// 处理读取事件
else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取客户端数据
int bytesRead = socketChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data, "UTF-8");
System.out.println("收到客户端消息: " + message);
// 回写响应给客户端
String response = "服务器已收到消息: " + message;
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
socketChannel.write(responseBuffer);
} else if (bytesRead == -1) {
// 客户端关闭连接
System.out.println("客户端关闭连接: " + socketChannel);
socketChannel.close();
}
}
// 移除已处理的事件
keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、非阻塞 IO 编程实践
1. 非阻塞客户端实现
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class NioClientExample {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8080;
public static void main(String[] args) {
try (SocketChannel socketChannel = SocketChannel.open();
Scanner scanner = new Scanner(System.in)) {
// 连接服务器
socketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));
System.out.println("已连接到服务器: " + socketChannel);
// 配置为非阻塞模式
socketChannel.configureBlocking(false);
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取用户输入并发送给服务器
while (true) {
System.out.print("请输入要发送的消息(输入exit退出): ");
String message = scanner.nextLine();
if ("exit".equalsIgnoreCase(message)) {
break;
}
// 发送消息给服务器
buffer.clear();
buffer.put(message.getBytes("UTF-8"));
buffer.flip();
socketChannel.write(buffer);
// 接收服务器响应
buffer.clear();
int bytesRead = socketChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String response = new String(data, "UTF-8");
System.out.println("收到服务器响应: " + response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. 基于选择器的多客户端处理
上面的 NioServerExample 已经展示了基于选择器的服务器实现,它可以同时处理多个客户端连接。关键点在于:
- 服务器通道注册 OP_ACCEPT 事件以接受新连接
- 客户端通道注册 OP_READ 事件以读取客户端数据
- 单个线程通过 Selector 轮询所有注册的通道,处理就绪的事件
四、NIO 高级应用
1. 文件锁定
Java NIO 提供了文件锁定机制,可以锁定文件的一部分或全部,防止其他程序访问。
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
public class FileLockingExample {
public static void main(String[] args) {
try (RandomAccessFile file = new RandomAccessFile("test.txt", "rw");
FileChannel channel = file.getChannel()) {
// 获取文件锁(锁定整个文件)
FileLock lock = channel.lock();
System.out.println("文件已锁定");
// 执行文件操作
// ...
// 释放锁
lock.release();
System.out.println("文件锁已释放");
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. 内存映射文件
内存映射文件允许将文件的一部分或全部映射到内存中,这样可以像访问内存一样访问文件,提高 IO 效率。
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class MemoryMappedFileExample {
private static final int SIZE = 1024 * 1024; // 1MB
public static void main(String[] args) {
try (RandomAccessFile file = new RandomAccessFile("mapped_file.dat", "rw");
FileChannel channel = file.getChannel()) {
// 创建内存映射文件
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, SIZE);
// 写入数据
for (int i = 0; i < SIZE; i++) {
buffer.put((byte) 'A');
}
// 读取数据
buffer.position(0);
for (int i = 0; i < 10; i++) {
System.out.print((char) buffer.get());
}
System.out.println();
System.out.println("内存映射文件操作完成");
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 异步 IO(AIO)
Java 7 引入了 NIO.2,提供了真正的异步 IO 支持,通过 CompletionHandler 回调或 Future 对象获取操作结果。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.Future;
public class AioServerExample {
private static final int PORT = 8090;
public static void main(String[] args) throws IOException, InterruptedException {
// 创建异步服务器套接字通道
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()
.bind(new InetSocketAddress(PORT));
System.out.println("异步服务器启动,监听端口: " + PORT);
// 接受客户端连接
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
// 继续接受下一个连接
serverChannel.accept(null, this);
// 处理当前连接
handleClient(clientChannel);
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
// 保持主线程运行
Thread.sleep(Long.MAX_VALUE);
}
private static void handleClient(AsynchronousSocketChannel clientChannel) {
try {
System.out.println("接受新连接: " + clientChannel.getRemoteAddress());
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 异步读取数据
clientChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer bytesRead, ByteBuffer buffer) {
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data);
System.out.println("收到客户端消息: " + message);
// 回写响应
String response = "服务器已收到: " + message;
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
// 异步写入响应
Future<Integer> writeResult = clientChannel.write(responseBuffer);
try {
writeResult.get(); // 等待写入完成
System.out.println("响应已发送给客户端");
} catch (Exception e) {
e.printStackTrace();
}
} else if (bytesRead == -1) {
// 客户端关闭连接
try {
System.out.println("客户端关闭连接: " + clientChannel.getRemoteAddress());
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
try {
System.out.println("读取失败,关闭连接: " + clientChannel.getRemoteAddress());
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
五、NIO 性能优化建议
-
合理设置缓冲区大小:过小的缓冲区会增加 IO 操作次数,过大的缓冲区会浪费内存。
-
避免不必要的上下文切换:非阻塞 IO 的优势在于一个线程可以处理多个通道,避免创建过多线程导致上下文切换开销。
-
使用直接缓冲区:对于需要频繁进行 IO 操作的数据,使用直接缓冲区(
ByteBuffer.allocateDirect()
)可以减少内存拷贝。 -
优化选择器配置:避免在选择器上注册过多通道,可根据系统资源和负载情况进行合理划分。
-
合理处理并发:在多线程环境下,注意对共享资源的同步访问,避免数据竞争。