Java BIO NIO AIO 模型介绍和使用样例

本文详细解释了计算机中的IO操作,包括阻塞与非阻塞、同步与异步等概念,并介绍了同步阻塞IO、同步非阻塞IO、IO多路复用及异步IO模型。此外,还提供了Java中BIO、NIO和AIO的具体使用样例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     在计算机的世界中,IO操作是不可避免的一个话题,IO操作涉及到的阻塞非阻塞同步异步这些概念常常让我感到混乱,为此,专门抽出时间对这些概念做了一下简单的研究,记录如下。希望可以帮助还在这些概念中挣扎的同学。

1.阻塞,非阻塞,同步和异步

     IO操作实际上可以分为两步:发起IO请求实际的IO操作。如果在第一步发起IO请求时发生阻塞,那么这个IO操作就可以说阻塞的,否则是非阻塞的。如果在第二步实际IO操作时发生阻塞,那么这个IO操作就是同步的,否则就是异步的。
     换种说法,阻塞和非阻塞是指在用户程序查询IO就绪状态时(比如查询IO是否有数据),用户程序对IO不同的就绪状态所表现出来的不同形式。以读取数据为例,当IO没有数据可供读取时,如果是阻塞IO,程序会一直阻塞直至IO有数据,如果是非阻塞IO,程序会直接返回错误码说当前没有数据,请稍后再来查询。同步和异步是由在进行实际的IO操作时,用户程序是否等待数据操作完成来决定。还是以读取数据为例,如果是同步IO,用户程序会等待读取数据完成,在此期间这个线程什么也不做,如果是异步IO,用户程序可以去作别的事情,内核在完成数据读取后,会以回调的方式通知用户程序。

2.同步阻塞IO,同步非阻塞IO,IO多路复用和异步IO模型

     在Unix中,共有五种IO模型,阻塞IO,非阻塞IO,IO多路复用和信号驱动IO以及异步IO,Java中实现了除信号驱动IO外的其他四种。
(1) 同步阻塞IO
     同步阻塞IO模型相对比较简单,操作流程如下,从用户发起read请求,到数据从IO读取至用户buffer,整个过程中用户线程始终处于阻塞状态,无法做别的事情。

(2) 同步非阻塞IO
     同步非阻塞IO模型的操作流程如下,用户发起read请求,这时候如果IO数据没有准备好,那么read函数立即返回,用户线程需要不停的去轮询是否有数据到来,当有数据到来时,用户程序再次发起读取数据操作,这个时候用户线程会发生阻塞,直至数据从IO拷贝至buffer操作完成,用户线程继续处理读取到的数据,这也是同步的意义所在。

(3) IO多路复用
     IO多路复用本质上还是属于同步非阻塞IO模型,不同点在于它通过selector实现在一个线程中监听多路通道数据。IO多路复用的操作流程如下,用户线程首先需要把需要监听的socket注册至selector,然后selector负责去轮询各个socket通道是否有数据到来,一旦有数据到来,select返回,最后用户程序读取IO数据,这个过程仍然是用户线程阻塞直至数据读取完成。需要注意的是,这里的socket通道必须配置为非阻塞,这样select才能去依次轮询所有注册在selector中的socket通道。 

      IO多路复用采用Reactor设计模式,操作流程如下。用户线程需要首先在Reactor中注册一个事件处理器,然后Reactor(相当于上文提到的selector)负责轮询各个通道是否有新的数据到来,当有新的数据到来时,Reactor通过先前注册的事件处理器通知用户线程有数据可读,此时用户线程向内核发起读取IO数据的请求,用户线程阻塞直至数据读取完成。 


(4) 异步IO
     最后是异步IO。异步IO采用Proactor设计模式,操作流程如下,跟Reactor模式一样,用户线程首先也需要向Proactor注册一个事件处理器,然后告诉内核要读取IO数据,这个时候用户线程就去做别的事情了,剩下监听IO数据以及IO数据的读取都由内核来完成,完成之后内核通过用户线程注册的事件处理器通知用户线程,“数据已经读取完成,你可以对这些数据做你自己的处理了”,最后用户线程拿到数据开始做自己的处理。

         上面分析了阻塞,非阻塞,同步和异步的区别以及各种IO模型的操作流程,下面我们用实际的例子来说明在Java中各个IO模型的使用。

3.Java中BIO,NIO和AIO使用样例
     下面我们主要介绍Java中BIO,NIO和AIO三种IO模型如何使用。需要注意的是,本文中所提到的所有样例都是在一个server对应一个client的情况下工作的,如果你想扩展为一个server服务多个client,那么代码需要做相应的修改才能使用。另外,本文只会讲解server端如何处理,客户端的操作流程可以仿照服务端进行编程,大同小异。

(1) BIO(Blocking I/O)
     在Java中,BIO是基于流的,这个流包括字节流或者字符流,但是细心的同学可能会发现基本上所有的流都是单向的,要么只读,要么只写。在实际上编程时,在对IO操作之前,要先获取输入流或输出流,然后对输入流读或对输出流写即完成实际的IO读写操作。 首先需要新建一个ServerSocket对象监听特定端口,然后当有客户端的连接请求到来时,在服务器端获取一个Socket对象,用来进行实际的通信。
ServerSocket serverSocket = new ServerSocket(PORT);
Socket socket = serverSocket.accept();
获取到Socket对象后,通过这个Socket对象拿到输入流和输出流就可以进行相应的读写操作了。
DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
由于BIO的编程的模型比较简单,这里就写这么多。

(2) NIO(New I/O, or Nonblocking I/O)
     BIO的编程模型简单易行,但是缺点也很明显。由于采用的是同步阻塞IO的模式,所以server端要为每一个连接创建一个线程,一方面,线程之间在进行上下文切换的时候会造成比较大的开销,另一方面,当连接数过多时,可能会造成服务器崩溃的现象产生。
     为了解决这个问题,在JDK 1.4的时候,引入了NIO(New IO)的概念。NIO主要由三个部分组成,即Channel,Buffer和Selector。Channel可以跟BIO中的Stream类比,不同的是Channel是可读可写的。当和Channel进行交互的时候需要Buffer的支持,数据可以从Buffer写到Channel中,也可以从Channel中读到Buffer中,他们的关系如下图。
       channel&buffer 以SocketChannel为例,Channel和Buffer交互的例子如下。ByteBuffer是Buffer的一种实现,在使用ByteBuffer之前,需要为其分配空间,然后调用Channel的read方法将数据写入Buffer中,在完成后,在使用Buffer中的数据之前需要调用Buffer的flip方法。Buffer中有个position常量,记录当前操作数据的位置,当向Buffer中写数据时,position会记录当前写的位置,当写操作完成后,flip会把position至为0,这样读取Buffer中的数据时,就会从0开始了。另外需要注意的是处理完Buffer中的数据后需要调用clear方法将Buffer清空。向Channel中写数据的操作比较简单,这里不再赘述。
// Read data from channel to buffer
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();  
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);  
while (socketChannel.read(byteBuffer) > 0) {  
    byteBuffer.flip();
    while(byteBuffer.hasRemaining()){
        System.out.print((char) byteBuffer.get());
    }
    byteBuffer.clear();}

// Write data to channel from buffer
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
       NIO中另一个重要的组件是Selector,Selector可以用来检查一个或多个Channel是否有新的数据到来,这种方式可以实现在一个线程中管理多个Channel的目的,示意图如下。


 selector 在使用selector之前,一定要注意把对应的Channel配置为非阻塞。否则在注册的时候会抛异常。
serverSocketChannel.configureBlocking(false);
然后调用select函数,select是个阻塞函数,它会阻塞直到某一个操作被激活。这个时候可以获取一系列的SelectionKey,通过这个SelectionKey可以判断其对应的Channel可进行的操作(可读,可写或者可接受连接),然后进行相应的操作即可。这里还要注意一个问题就是在判断完可执行的操作后,需要将这个SelectionKey从集合中移除。
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
    SelectionKey selectionKey = iterator.next();
    if (!selectionKey.isValid())
        continue;
    if (selectionKey.isAcceptable()) {
        // ready for accepting
    } else if (selectionKey.isReadable()) {
        // ready for reading
    } else if (selectionKey.isWritable()) {
        // ready for writing
    }
    iterator.remove();
}
NIO这里最后一个问题是,什么时候Channel可写,这个问题困扰了我很久,经过从网上查资料最后得出的结论是,只要这个Channel处于空闲状态,都是可写的。这个我也从实际的程序中论证了。

(3) AIO(Asynchronous I/O)
     在JDK 1.7时,Java引入了AIO的概念,AIO还是基于Channel和Buffer的,不同的是它是异步的。用户线程把实际的IO操作以及数据拷贝全部委托给内核来做,用户只要传递给内核一个用于存储数据的地址空间即可。内核处理的结果通过两种方式返回给用户线程。一是通过Future对象,另外一种是通过回调函数的方式,回调函数需要实现CompletionHandler接口。这里只给出通过回调方式处理数据的样例,其中关键的步骤已经在程序中添加了注释。
// 创建AsynchronousServerSocketChannel监听特定端口,并设置回调AcceptCompletionHandler
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));  
serverSocketChannel.accept(serverSocketChannel, new AcceptCompletionHandler());

// 监听回调,当用连接时会触发该回调private static class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel> {  
    @Override
    public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 注册read请求以及回调ReadCompletionHandler
        result.read(byteBuffer, result, new ReadCompletionHandler(byteBuffer, "client"));
        // 递归监听
        attachment.accept(attachment, this);
    }
    @Override
    public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
        // 递归监听
        attachment.accept(attachment, this);
    }}

// 读取数据回调,当有数据可读时触发该回调public class ReadCompletionHandler  implements CompletionHandler<Integer, AsynchronousSocketChannel> {  
    private ByteBuffer byteBuffer;
    private String remoteName;
    public ReadCompletionHandler(ByteBuffer byteBuffer, String remoteName) {
        this.byteBuffer = byteBuffer;
        this.remoteName = remoteName;
    }
    @Override
    public void completed(Integer result, AsynchronousSocketChannel attachment) {
        if (result <= 0)
            return;

        byteBuffer.flip();
        System.out.println("[" + this.remoteName + "] " + new String(byteBuffer.array()));

        byteBuffer.clear();
        // 递归监听数据
        attachment.read(byteBuffer, attachment, this);
    }

    @Override
    public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
        byteBuffer.clear();
        // 递归监听数据
        attachment.read(byteBuffer, attachment, this);
    }}
     上面给出了BIO,NIO以及AIO在Java中的使用的部分程序,并且分析了其中关键步骤的使用及其需要注意的事项。

### JavaBIONIO AIO 的区别 #### 同步阻塞 I/O (BIO) 同步阻塞 I/O 是最传统的 I/O 模型。在这种模式下,当应用程序发起一个 I/O 请求时,调用线程会被挂起直到请求完成。这意味着如果当前线程正在处理某个耗时的 I/O 操作,则该线程无法继续执行其他任务。 对于服务器端开发而言,通常需要为每一个客户端连接创建一个新的线程来处理通信过程中的读写操作。这种方式虽然简单直观,但在高并发场景下的资源消耗非常大,因为每个活跃连接都需要占用独立的工作线程[^5]。 ```java ServerSocket server = new ServerSocket(port); while(true){ Socket client = server.accept(); // 此处会发生阻塞 } ``` #### 非阻塞 I/O (NIO) 非阻塞 I/O 则允许程序在发出 I/O 调用时不被阻塞,而是立即返回给调用方。具体来说,在 NIO 下可以通过 `Selector` 对象管理多个 Channel 实的状态变化事件(如可读、可写)。一旦有就绪状态发生改变,就会触发相应的回调逻辑去处理实际的数据交换工作。这种机制大大提高了单个线程所能承载的最大并发量,并且降低了上下文切换带来的开销[^3]。 ```java // 创建 Selector 并注册感兴趣的事件类型 Selector selector = Selector.open(); serverChannel.configureBlocking(false); serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 处理已准备好的 channel int readyChannels = selector.select(); if (readyChannels == 0) continue; Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()){ SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // handle accept event... } else if (key.isReadable()) { // handle read event... } ... } ``` #### 异步非阻塞 I/O (AIO/NIO.2) 自JDK 7引入以来,AIO 提供了一个更加高级别的抽象层用于构建高效的网络服务端应用。不同于之前的两种方式,这里所有的I/O操作均采用完全异步的方式运行——既不会阻塞也不会轮询查询结果;相反地,它们会在后台由操作系统负责调度直至完成后通知用户态的应用代码。开发者只需定义好完成处理器(`CompletionHandler`)即可轻松捕获到最终的结果反馈信息[^1]. ```java AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open(); serverChannel.bind(new InetSocketAddress(port)); serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() { @Override public void completed(AsynchronousSocketChannel result, Void attachment) { System.out.println("New connection accepted"); ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); result.read(buffer, buffer, new ReadCompletionHandler(result)); } @Override public void failed(Throwable exc, Void attachment) { logger.log(Level.SEVERE,"Failed to accept a connection",exc); } }); ``` ### 应用场景分析 - **低并发环境**: 如果业务需求较为单一且预计访问压力不大,那么可以选择简单的 BIO 方案快速搭建原型系统; - **中等规模并发负载**: 当面对一定数量级的同时在线用户数时,建议考虑基于 NIO 构建的服务架构,利用多路复用技术提升吞吐率; - **大规模分布式集群部署**: 在追求极致性能表现的情况下,尤其是涉及到海量短链接频繁交互或是长时间保持长连接通讯的任务场景,应该优先评估是否适合迁移到支持全异步语义的新一代 API —— 即 AIO 上面。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员学习圈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值