在 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 参考链接
Java NIO 比传统 IO 强在哪里?