为什么 Java NIO 比 Java BIO 高效?
-
非阻塞 I/O:
-
BIO:每个 I/O 操作都会阻塞线程,导致大量线程处于空闲状态,增加了线程管理和上下文切换的开销。 -
NIO:I/O 操作是非阻塞的,线程可以在等待 I/O 完成的同时执行其他任务,减少了线程阻塞的时间,提高了资源利用率。 -
多路复用(Multiplexing):
-
BIO:每个连接对应一个线程,随着连接数的增加,线程数量也线性增长,资源消耗大。 -
NIO:使用 Selector
可以同时监听多个通道,单个线程可以管理多个连接,减少了线程数量,降低了资源消耗。 -
缓冲区(Buffer):
-
BIO:数据以字节流形式逐字节或逐块传输,频繁的系统调用导致性能下降。 -
NIO:数据存储在缓冲区中,可以通过一次系统调用来批量读取或写入数据,减少了系统调用次数,提高了 I/O 效率。 -
直接缓冲区(Direct Buffer):
-
BIO:数据需要在 JVM 堆内存和操作系统内核空间之间多次复制,增加了数据复制的开销。 -
NIO:直接缓冲区直接在 JVM 堆外内存中分配,避免了数据在 JVM 堆内存和操作系统内核空间之间的复制,减少了数据复制的开销,提高了 I/O 操作的速度。 -
通道(Channel):
-
BIO:数据通过 InputStream
和OutputStream
流进行传输,每次操作只能处理一部分数据,代码复杂且效率低下。 -
NIO: Channel
是双向的,既可以读取数据也可以写入数据,简化了数据处理流程,减少了代码复杂度。
对比案例
BIO 示例:简单 TCP 服务器
-
服务器端代码:
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();
}
}
}
}
}
-
客户端代码:
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 服务器
-
服务器端代码:
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);
}
}
-
客户端代码:
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();
}
}
性能对比
-
线程数量:
-
即便在高并发情况下,也能保持较低的线程数量,节省资源。 -
高并发时,线程数量急剧增加,可能导致资源耗尽。 -
BIO: 每个连接一个线程,线程数量与连接数量成正比。 -
NIO: 单个线程管理多个连接,线程数量固定,不受连接数量影响。 -
资源消耗:
-
由于使用单个线程管理多个连接,减少了线程切换的开销,提高了资源利用率。 -
每个线程都需要占用一定的内存和 CPU 时间片,尤其是在高并发场景下。 -
BIO: 线程切换和上下文切换开销大,资源消耗高。 -
NIO: 资源消耗较低,适合高并发场景。 -
吞吐量:
-
通过多路复用技术,单个线程可以高效地处理多个连接,提高了系统的吞吐量。 -
每个线程只能处理一个连接,随着连接数的增加,吞吐量迅速下降。 -
BIO: 吞吐量受限于线程数量,高并发时性能下降。 -
NIO: 吞吐量较高,能够处理更多的并发连接。 -
数据处理效率:
-
减少了系统调用次数,提高了 I/O 效率。 -
频繁的系统调用导致性能下降。 -
BIO: 数据以字节流形式逐字节或逐块传输,缺乏缓冲机制。 -
NIO: 数据存储在缓冲区中,可以通过一次系统调用来批量读取或写入数据。 -
直接缓冲区:
-
减少了数据复制的开销,提高了 I/O 操作的速度。 -
增加了额外的数据复制开销,降低了 I/O 性能。 -
BIO: 数据需要在 JVM 堆内存和操作系统内核空间之间多次复制。 -
NIO: 直接缓冲区直接在 JVM 堆外内存中分配,避免了数据在 JVM 堆内存和操作系统内核空间之间的复制。
总结
通过上述代码示例和分析,可以看出 Java NIO 相较于 Java BIO 在以下几个方面具有显著的优势:
-
非阻塞 I/O:减少了线程阻塞的时间,提高了资源利用率。 -
多路复用:通过 Selector
可以高效地管理多个连接,降低了线程的数量。 -
缓冲区:批量处理数据减少了系统调用次数,提高了 I/O 效率。 -
直接缓冲区:减少数据复制的开销,提高了 I/O 操作的速度。 -
通道:双向通信简化了数据处理流程,减少了代码复杂度。 -
异步 I/O(NIO.2):进一步提升了系统的吞吐量和响应速度。
这些特性使得 NIO 成为处理高并发、大规模 I/O 操作的理想选择,尤其适用于现代互联网应用和服务端开发。
关注我!Java从此不迷路!
原文始发于微信公众号(Java知识日历):为什么Java NIO就是比BIO高效呢?【内含对比代码】
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/310665.html