NIO 与传统 IO:深入理解与应用场景
在 Java 编程中,IO(输入/输出)操作是不可或缺的一部分。Java 提供了两种主要的 IO 机制:传统的阻塞式 IO(Blocking IO)和非阻塞式 IO(Non-blocking IO),后者通常被称为 NIO(New IO)。本文将深入探讨这两种 IO 模型的差异,特别是在文件操作和网络传输中的应用场景。
1 传统 IO 与 NIO 的对比
1.1 传统 IO(Blocking IO)
传统 IO 基于字节流或字符流(如 FileInputStream
、BufferedReader
等)进行文件读写,以及使用 Socket
和 ServerSocket
进行网络传输。传统 IO 采用阻塞式模型,对于每个连接,都需要创建一个独立的线程来处理读写操作。当一个线程在等待 I/O 操作时,无法执行其他任务,这会导致大量线程的创建和销毁,以及上下文切换,降低了系统性能。
1.2 NIO(Non-blocking IO)
NIO 使用通道(Channel)和缓冲区(Buffer)进行文件操作,以及使用 SocketChannel
和 ServerSocketChannel
进行网络传输。NIO 采用非阻塞模型,允许线程在等待 I/O 时执行其他任务。这种模式通过使用选择器(Selector)来监控多个通道(Channel)上的 I/O 事件,实现了更高的性能和可伸缩性。
2 文件操作中的 NIO 与传统 IO
2.1 性能测试
为了比较 NIO 和传统 IO 在文件操作中的性能,我们编写了一个简单的文件复制程序,分别使用传统 IO 和 NIO 进行文件复制。
public class SimpleFileTransferTest {
// 使用传统的 I/O 方法传输文件
private long transferFile(File source, File des) throws IOException {
long startTime = System.currentTimeMillis();
if (!des.exists())
des.createNewFile();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(source));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(des));
byte[] bytes = new byte[1024 * 1024];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
// 使用 NIO 方法传输文件
private long transferFileWithNIO(File source, File des) throws IOException {
long startTime = System.currentTimeMillis();
if (!des.exists())
des.createNewFile();
RandomAccessFile read = new RandomAccessFile(source, "rw");
RandomAccessFile write = new RandomAccessFile(des, "rw");
FileChannel readChannel = read.getChannel();
FileChannel writeChannel = write.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);
while (readChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
writeChannel.write(byteBuffer);
byteBuffer.clear();
}
writeChannel.close();
readChannel.close();
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
public static void main(String[] args) throws IOException {
SimpleFileTransferTest simpleFileTransferTest = new SimpleFileTransferTest();
File sourse = new File("[电影天堂www.dygod.cn]猜火车-cd1.rmvb");
File des = new File("io.avi");
File nio = new File("nio.avi");
long time = simpleFileTransferTest.transferFile(sourse, des);
System.out.println(time + ":普通字节流时间");
long timeNio = simpleFileTransferTest.transferFileWithNIO(sourse, nio);
System.out.println(timeNio + ":NIO时间");
}
}
测试结果:在文件较大的情况下,传统 IO 的速度竟然比 NIO 更快。这可能是因为文件操作本身不涉及大量并发,NIO 的非阻塞特性在文件操作中并没有明显优势。
3. 网络传输中的 NIO 与传统 IO
在 Java 中,传统 IO 和 NIO 在服务器端实现上有显著的差异。传统 IO 使用阻塞式模型,而 NIO 使用非阻塞式模型,通过 Selector 实现 I/O 多路复用。下面我们将详细对比这两种模型的实现。
3.1 服务器端代码对比
传统 IO 服务器:传统 IO 服务器使用 ServerSocket 和 Socket 类来实现阻塞式 I/O。每个连接都需要一个单独的线程来处理,这在大规模并发连接的情况下会导致性能问题。
public class IOServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept();
InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream();
byte[] buffer = new byte[1024];
int bytesRead = in.read(buffer);
out.write(buffer, 0, bytesRead);
in.close();
out.close();
client.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
关键点:
阻塞式 I/O:
serverSocket.accept()
和in.read(buffer)
都是阻塞的,直到有新的连接或数据到达。
线程模型:每个连接都需要一个单独的线程来处理,这在大规模并发连接的情况下会导致性能问题。
NIO 服务器:NIO 服务器使用 ServerSocketChannel 和 Selector 类来实现非阻塞式 I/O 和 I/O 多路复用。单个线程可以处理多个连接,从而提高性能。
public class NIOServer {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8081));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer);
buffer.flip();
client.write(buffer);
client.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
关键点:
非阻塞式 I/O:
serverSocketChannel.configureBlocking(false)
和client.configureBlocking(false)
设置为非阻塞模式。
I/O 多路复用:使用Selector
监控多个SocketChannel
,单个线程可以处理多个连接。
事件驱动:通过SelectionKey
处理不同的事件(如OP_ACCEPT
和OP_READ
)。
客户端测试用例:为了比较传统 IO 和 NIO 服务器的性能,我们编写了一个简单的客户端测试用例,分别测试处理 10000 个客户端请求所需的时间。
public class TestClient {
public static void main(String[] args) throws InterruptedException {
int clientCount = 10000;
ExecutorService executorServiceIO = Executors.newFixedThreadPool(10);
ExecutorService executorServiceNIO = Executors.newFixedThreadPool(10);
Runnable ioClient = () -> {
try {
Socket socket = new Socket("localhost", 8080);
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
out.write("Hello, 沉默王二 IO!".getBytes());
byte[] buffer = new byte[1024];
in.read(buffer);
in.close();
out.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
};
Runnable nioClient = () -> {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8081));
ByteBuffer buffer = ByteBuffer.wrap("Hello, 沉默王二 NIO!".getBytes());
socketChannel.write(buffer);
buffer.clear();
socketChannel.read(buffer);
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
};
long startTime, endTime;
startTime = System.currentTimeMillis();
for (int i = 0; i < clientCount; i++) {
executorServiceIO.execute(ioClient);
}
executorServiceIO.shutdown();
executorServiceIO.awaitTermination(1, TimeUnit.MINUTES);
endTime = System.currentTimeMillis();
System.out.println("传统 IO 服务器处理 " + clientCount + " 个客户端耗时: " + (endTime - startTime) + "ms");
startTime = System.currentTimeMillis();
for (int i = 0; i < clientCount; i++) {
executorServiceNIO.execute(nioClient);
}
executorServiceNIO.shutdown();
executorServiceNIO.awaitTermination(1, TimeUnit.MINUTES);
endTime = System.currentTimeMillis();
System.out.println("NIO 服务器处理 " + clientCount + " 个客户端耗时: " + (endTime - startTime) + "ms");
}
}
测试结果:NIO 服务器处理 10000 个客户端请求的时间明显优于传统 IO 服务器,NIO 在网络传输中的性能优势显著。
4. 总结
- 文件操作:传统 IO 和 NIO 在文件操作中的性能差异不大,NIO 的非阻塞特性在文件操作中没有明显优势。
- 网络传输:NIO 在网络传输中的性能显著优于传统 IO,特别是在高并发场景下。NIO 的非阻塞模型和 I/O 多路复用机制使得单个线程可以高效地管理多个并发连接,从而提高系统性能。
- 传统 I/O 采用阻塞式模型,线程在 I/O 操作期间无法执行其他任务。NIO 使用非阻塞模型,允许线程在等待 I/O 时执行其他任务,通过选择器(
Selector
)监控多个通道(Channel
)上的 I/O 事件,提高性能和可伸缩性。 - 传统 I/O 使用基于字节流或字符流的类(如
FileInputStream
、BufferedReader
等)进行文件读写。NIO 使用通道(Channel
)和缓冲区(Buffer
)进行文件操作,NIO 在性能上的优势并不大。 - 传统 I/O 使用
Socket
和ServerSocket
进行网络传输,存在阻塞问题。NIO 提供了SocketChannel
和ServerSocketChannel
,支持非阻塞网络传输,提高了并发处理能力。
理解 NIO 和传统 IO 的差异及其适用场景,有助于在实际开发中选择合适的 IO 机制,以提高程序的性能和可扩展性。
5 思维导图
6 参考链接
原文地址:https://blog.csdn.net/gaosw0521/article/details/143788256
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!