自学内容网 自学内容网

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 基于字节流或字符流(如 FileInputStreamBufferedReader 等)进行文件读写,以及使用 SocketServerSocket 进行网络传输。传统 IO 采用阻塞式模型,对于每个连接,都需要创建一个独立的线程来处理读写操作。当一个线程在等待 I/O 操作时,无法执行其他任务,这会导致大量线程的创建和销毁,以及上下文切换,降低了系统性能。

1.2 NIO(Non-blocking IO)

NIO 使用通道(Channel)和缓冲区(Buffer)进行文件操作,以及使用 SocketChannelServerSocketChannel 进行网络传输。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_ACCEPTOP_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 使用基于字节流或字符流的类(如 FileInputStreamBufferedReader 等)进行文件读写。NIO 使用通道(Channel)和缓冲区(Buffer)进行文件操作,NIO 在性能上的优势并不大。
  • 传统 I/O 使用 SocketServerSocket 进行网络传输,存在阻塞问题。NIO 提供了 SocketChannel ServerSocketChannel,支持非阻塞网络传输,提高了并发处理能力。

理解 NIO 和传统 IO 的差异及其适用场景,有助于在实际开发中选择合适的 IO 机制,以提高程序的性能和可扩展性。

5 思维导图

在这里插入图片描述

6 参考链接

Java NIO 比传统 IO 强在哪里?


原文地址:https://blog.csdn.net/gaosw0521/article/details/143788256

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!