回答
BIO
阻塞式 I/O,NIO
同步非阻塞 I/O,AIO
异步非阻塞 I/O。它们三者之间有如下几个区别。
- I/O 模型
BIO
:同步且阻塞 I/O 模型。服务端模型为一个连接一个线程。NIO
:同步非阻塞 I/O 模型。服务端模型为一个线程处理多个连接。AIO
:异步非阻塞 I/O 模型。服务端模型为一个有效请求一个线程。
- 阻塞与否
BIO
:阻塞,即在读写操作完成之前,线程会一直等待,直到操作完成。NIO
:非阻塞,即线程在请求读写操作时会立即返回,可以进行其他任务。AIO
:非阻塞且异步,当操作完成时,会被动地通知或回调线程进行后续操作。
- 线程模型
BIO
:一个连接一个线程,对于每个新的客户端连接都需要创建一个新的线程。NIO
:多路复用,使用单个或几个线程来管理多个连接,通过 Selector 监控客户端的连接和请求。AIO
:异步任务完成后,系统会主动回调,使用线程池来处理这些回调,从而实现高效的并发处理。
- 应用场景
BIO
:适用于连接数比较少的场景,实现较为简单,入门级。NIO
:适用于连接数较多且连接较短的场景(例如聊天),实现稍微复杂,需要理解 Buffer、Channel、Selector 等 Java NIO 的概念。AIO
:适合于高负载、高并发的场景,但是实现比较复杂,需要理解异步编程模型。
详解
BIO
BIO
(Blocking I/O),即阻塞式IO,是传统的Java I/O编程模型。在BIO中
,当一个线程调用读写数据的方法时,该线程会被阻塞,直到有一些数据被读取或数据完全被写入。这期间,该线程无法进行其他任务,只有当数据完全传输完成后,线程才能继续执行。
BIO
的服务端模型为一个连接一个线程。在 BIO
模式中,服务端通常由一个主线程负责监听客户端的连接请求,每当接收到一个客户端连接请求时,主线程都会为这个请求创建一个新的线程来进行处理。图例如下:
示例如下:
public class BIOServer {
public static void main(String[] args) {
// 创建服务器端的ServerSocket,监听8080端口
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
// 等待客户端连接,此处会阻塞
Socket clientSocket = serverSocket.accept();
System.out.println("客户端连接成功:" + clientSocket.getInetAddress().getHostAddress());
// 为每个客户端创建一个新的线程进行处理
new Thread(() -> {
try {
// 读取客户端发送的数据
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String clientData = reader.readLine();
System.out.println("接收到客户端的数据:" + clientData);
// 处理业务
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
由于该模式每个连接都需要新建一个线程,当连接较多时,就会创建大量线程,严重消耗系统资源,限制了服务的扩展能力,所以它只适用于连接较少的应用场景。同时,由于它是同步阻塞的,所以整体性能不是很高。它的优点嘛,就是模型简单,实现起来没有什么困难之处。
NIO
NIO
,即同步非阻塞 IO。NIO
主要解决了 BIO
因阻塞而导致的效率低下问题。在 NIO 模型中,一个服务端线程可以管理多个连接。它的服务端模型为一个线程处理多个连接,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理,图例如下:
NIO
引入 Channel
、Buffer
、Selector
三个核心概念,实现了数据的异步处理和通道的多路复用,大大提高了IO操作的效率,表现了超高的性能和扩展性。
Channel
:通道,可以看作是连接数据源的开放链接,如文件、网络套接字等等。与传统 IO 的流不同, Channel 是双向的,既可以用来进行读操作,也可以用来进行写操作。Buffer
:字节缓冲区,它是数据的容器。Selector
:选择器。Selector
是 NIO 实现单独线程管理多个链接的关键机制,它会不断轮询注册到其上的 Channel,当某个 Channel 上的 IO 操作准备就绪时,能够高效地进行处理。
NIO 使用单一线程来管理多个连接,采用多路复用机制,使用较少的线程来处理连接,减少了线程资源的消耗,同时也减少了上下文的切换,提高了系统的处理能力,适用于连接数多且连接比较短(轻操作)的架构,但是它的实现稍微会复杂写。
由于 NIO 的高性能和高可扩展性,使得它是目前开发高性能网络服务器使用最为广泛的模型。
下面是一个简单的代码:
public class NIOServer {
public static void main(String[] args) throws Exception {
// 打开服务器套接字通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
// 绑定监听端口
serverSocketChannel.bind(new InetSocketAddress(8080));
// 打开选择器
Selector selector = Selector.open();
// 将服务器套接字通道注册到选择器上,设置为接受连接操作
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动,监听端口:8080");
while (true) {
// 选择一组键,其相应的通道已为I/O操作准备就绪
selector.select();
// 获取选择键集
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 处理接受连接就绪事件
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverChannel.accept();
socketChannel.configureBlocking(false);
// 将新连接的通道注册到选择器上,设置为读就绪操作
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接成功");
}
// 处理读就绪事件
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int length = socketChannel.read(buffer);
if (length > 0) {
buffer.flip();
System.out.println("接收到客户端的数据:" + new String(buffer.array(), 0, length));
buffer.clear();
}
}
iterator.remove();
}
}
}
}
AIO
AIO
,即异步非阻塞 IO,AIO 提供了一种完全异步的概念,在该模型下,IO 操作是完全异步的,即我们执行一个 IO 操作后我们不需要等待它完成,可以直接进入其他任务,当 IO 操作完成时,它会触发一个回调函数来处理结果。理解该模型我们需要理解如下几个概念。
- **异步通道:**Java 提供了
AsynchronousSocketChannel
和AsynchronousServerSocketChannel
异步通道,这两个 Channel 提供了对异步非阻塞 IO 的支持,它允许我们进行异步读写操作,当操作完成时,可以接收到通知。 - CompletionHandler:
CompletionHandler
接口是AIO
中处理异步操作结果的回调接口。它包含了completed()
和failed()
两个方法,分别在操作成功完成和失败时被调用。
AIO
的服务端模型为一个有效请求一个线程,其工作原理如下:
- 执行一个异步 IO 操作后,Java 程序可以立刻返回去执行其他任务。
- 实际的 AIO 操作由操作系统异步完成。
- 当异步 IO 操作完成后,会回调相应的
CompletionHandler
回调函数。
下面是一个简单的 AIO 服务端示例代码:
public class SimpleAIOServer {
public static void main(String[] args) throws Exception {
// 打开异步服务器套接字通道
final AsynchronousServerSocketChannel listener =
AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));
// 接受客户端连接(异步)
listener.accept(null, new CompletionHandler<AsynchronousSocketChannel,Void>() {
public void completed(AsynchronousSocketChannel ch, Void att) {
// 接受下一个连接
listener.accept(null, this);
// 处理当前连接
handleClient(ch);
}
public void failed(Throwable exc, Void att) {
// 处理错误
}
});
}
static void handleClient(AsynchronousSocketChannel ch) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 异步读取数据
ch.read(buffer, null, new CompletionHandler<Integer,Void>() {
public void completed(Integer result, Void attachment) {
// 处理
}
@Override
public void failed(Throwable exc, Void attachment) {
// 处理错误
}
});
}
}
AIO 模型非常适合高负载、高并发的场景。但是它的实现会比较复杂。
更多阅读