为什么Java NIO就是比BIO高效呢?【内含对比代码】

为什么 Java NIO 比 Java BIO 高效?

  1. 非阻塞 I/O

    • BIO:每个 I/O 操作都会阻塞线程,导致大量线程处于空闲状态,增加了线程管理和上下文切换的开销。
    • NIO:I/O 操作是非阻塞的,线程可以在等待 I/O 完成的同时执行其他任务,减少了线程阻塞的时间,提高了资源利用率。
  2. 多路复用(Multiplexing)

    • BIO:每个连接对应一个线程,随着连接数的增加,线程数量也线性增长,资源消耗大。
    • NIO:使用 Selector 可以同时监听多个通道,单个线程可以管理多个连接,减少了线程数量,降低了资源消耗。
  3. 缓冲区(Buffer)

    • BIO:数据以字节流形式逐字节或逐块传输,频繁的系统调用导致性能下降。
    • NIO:数据存储在缓冲区中,可以通过一次系统调用来批量读取或写入数据,减少了系统调用次数,提高了 I/O 效率。
  4. 直接缓冲区(Direct Buffer)

    • BIO:数据需要在 JVM 堆内存和操作系统内核空间之间多次复制,增加了数据复制的开销。
    • NIO:直接缓冲区直接在 JVM 堆外内存中分配,避免了数据在 JVM 堆内存和操作系统内核空间之间的复制,减少了数据复制的开销,提高了 I/O 操作的速度。
  5. 通道(Channel)

    • BIO:数据通过 InputStream 和 OutputStream 流进行传输,每次操作只能处理一部分数据,代码复杂且效率低下。
    • NIOChannel 是双向的,既可以读取数据也可以写入数据,简化了数据处理流程,减少了代码复杂度。

对比案例

BIO 示例:简单 TCP 服务器

  1. 服务器端代码
package com.example.bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

publicclass BIOServer {

    public static void main(String[] args) throws IOException {
        // 创建一个 ServerSocket 监听 8080 端口
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("BIO Server started on port 8080");

        while (true) {
            // 阻塞等待客户端连接
            Socket clientSocket = serverSocket.accept();
            System.out.println("New client connected: " + clientSocket.getInetAddress());
            // 为每个客户端连接创建一个新的线程来处理
            new Thread(new ClientHandler(clientSocket)).start();
        }
    }

    staticclass ClientHandler implements Runnable {
        privatefinal Socket clientSocket;

        public ClientHandler(Socket socket) {
            this.clientSocket = socket;
        }

        @Override
        public void run() {
            try (
                // 创建输入流读取客户端发送的数据
                BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                // 创建输出流向客户端发送数据
                PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)
            ) {
                String inputLine;
                // 循环读取客户端发送的数据
                while ((inputLine = in.readLine()) != null) {
                    System.out.println("Received from client: " + inputLine);
                    // 将接收到的数据回显给客户端
                    out.println("Echo: " + inputLine);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    // 关闭客户端连接
                    clientSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  1. 客户端代码
package com.example.bio;

import java.io.*;
import java.net.Socket;

publicclass BIOClient {

    public static void main(String[] args) throws IOException {
        // 连接到服务器的 8080 端口
        Socket socket = new Socket("localhost"8080);
        System.out.println("Connected to server");

        // 创建标准输入流读取用户输入
        BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in));
        // 创建输入流读取服务器发送的数据
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        // 创建输出流向服务器发送数据
        PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

        String userInputLine;
        // 循环读取用户输入并发送到服务器
        while ((userInputLine = userInput.readLine()) != null) {
            out.println(userInputLine);
            // 读取服务器的响应并打印
            System.out.println("Server response: " + in.readLine());
        }

        // 关闭连接
        socket.close();
    }
}

NIO 示例:简单 TCP 服务器

  1. 服务器端代码
package com.example.nio;

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;

publicclass NIOServer {

    public static void main(String[] args) throws IOException {
        // 创建一个 Selector 来管理多个 Channel
        Selector selector = Selector.open();
        // 创建一个 ServerSocketChannel 监听 8080 端口
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        // 设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 注册 OP_ACCEPT 事件,表示关注新的连接请求
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("NIO Server started on port 8080");

        while (true) {
            // 阻塞直到至少一个事件发生
            selector.select();
            // 获取所有准备就绪的 SelectionKey
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();

                if (key.isAcceptable()) {
                    // 处理新的连接请求
                    handleAccept(key, selector);
                } elseif (key.isReadable()) {
                    // 处理可读事件
                    handleRead(key);
                }

                // 移除已经处理过的 SelectionKey
                keyIterator.remove();
            }
        }
    }

    private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
        // 获取 ServerSocketChannel
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        // 接受新的客户端连接
        SocketChannel clientSocketChannel = serverSocketChannel.accept();
        // 设置客户端通道为非阻塞模式
        clientSocketChannel.configureBlocking(false);
        // 注册 OP_READ 事件,表示关注该通道的可读事件
        clientSocketChannel.register(selector, SelectionKey.OP_READ);
        System.out.println("New client connected: " + clientSocketChannel.getRemoteAddress());
    }

    private static void handleRead(SelectionKey key) throws IOException {
        // 获取 SocketChannel
        SocketChannel clientSocketChannel = (SocketChannel) key.channel();
        // 创建直接缓冲区
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        // 从通道读取数据到缓冲区
        int bytesRead = clientSocketChannel.read(buffer);

        if (bytesRead == -1) {
            // 如果没有读取到数据,关闭连接
            clientSocketChannel.close();
            return;
        }

        // 切换缓冲区模式为读模式
        buffer.flip();
        byte[] bytes = newbyte[buffer.remaining()];
        buffer.get(bytes);
        String message = new String(bytes, "UTF-8");
        System.out.println("Received from client: " + message);

        // 回显消息给客户端
        ByteBuffer responseBuffer = ByteBuffer.allocateDirect(("Echo: " + message).getBytes("UTF-8").length);
        responseBuffer.put(("Echo: " + message).getBytes("UTF-8"));
        responseBuffer.flip();
        clientSocketChannel.write(responseBuffer);
    }
}
  1. 客户端代码
package com.example.nio;

import java.io.*;
import java.net.Socket;

publicclass NIOClient {

    public static void main(String[] args) throws IOException {
        // 连接到服务器的 8080 端口
        Socket socket = new Socket("localhost"8080);
        System.out.println("Connected to server");

        // 创建标准输入流读取用户输入
        BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in));
        // 创建输入流读取服务器发送的数据
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        // 创建输出流向服务器发送数据
        PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

        String userInputLine;
        // 循环读取用户输入并发送到服务器
        while ((userInputLine = userInput.readLine()) != null) {
            out.println(userInputLine);
            // 读取服务器的响应并打印
            System.out.println("Server response: " + in.readLine());
        }

        // 关闭连接
        socket.close();
    }
}

性能对比

  1. 线程数量

    • 即便在高并发情况下,也能保持较低的线程数量,节省资源。
    • 高并发时,线程数量急剧增加,可能导致资源耗尽。
    • BIO: 每个连接一个线程,线程数量与连接数量成正比。
    • NIO: 单个线程管理多个连接,线程数量固定,不受连接数量影响。
  2. 资源消耗

    • 由于使用单个线程管理多个连接,减少了线程切换的开销,提高了资源利用率。
    • 每个线程都需要占用一定的内存和 CPU 时间片,尤其是在高并发场景下。
    • BIO: 线程切换和上下文切换开销大,资源消耗高。
    • NIO: 资源消耗较低,适合高并发场景。
  3. 吞吐量

    • 通过多路复用技术,单个线程可以高效地处理多个连接,提高了系统的吞吐量。
    • 每个线程只能处理一个连接,随着连接数的增加,吞吐量迅速下降。
    • BIO: 吞吐量受限于线程数量,高并发时性能下降。
    • NIO: 吞吐量较高,能够处理更多的并发连接。
  4. 数据处理效率

    • 减少了系统调用次数,提高了 I/O 效率。
    • 频繁的系统调用导致性能下降。
    • BIO: 数据以字节流形式逐字节或逐块传输,缺乏缓冲机制。
    • NIO: 数据存储在缓冲区中,可以通过一次系统调用来批量读取或写入数据。
  5. 直接缓冲区

    • 减少了数据复制的开销,提高了 I/O 操作的速度。
    • 增加了额外的数据复制开销,降低了 I/O 性能。
    • BIO: 数据需要在 JVM 堆内存和操作系统内核空间之间多次复制。
    • NIO: 直接缓冲区直接在 JVM 堆外内存中分配,避免了数据在 JVM 堆内存和操作系统内核空间之间的复制。

总结

通过上述代码示例和分析,可以看出 Java NIO 相较于 Java BIO 在以下几个方面具有显著的优势:

  1. 非阻塞 I/O:减少了线程阻塞的时间,提高了资源利用率。
  2. 多路复用:通过 Selector 可以高效地管理多个连接,降低了线程的数量。
  3. 缓冲区:批量处理数据减少了系统调用次数,提高了 I/O 效率。
  4. 直接缓冲区:减少数据复制的开销,提高了 I/O 操作的速度。
  5. 通道:双向通信简化了数据处理流程,减少了代码复杂度。
  6. 异步 I/O(NIO.2):进一步提升了系统的吞吐量和响应速度。

这些特性使得 NIO 成为处理高并发、大规模 I/O 操作的理想选择,尤其适用于现代互联网应用和服务端开发。

关注我!Java从此不迷路!


为什么Java NIO就是比BIO高效呢?【内含对比代码】

为什么Java NIO就是比BIO高效呢?【内含对比代码】





原文始发于微信公众号(Java知识日历):为什么Java NIO就是比BIO高效呢?【内含对比代码】

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/310665.html

(0)
服务端技术精选的头像服务端技术精选

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!