一文看懂 BIO、NIO、AIO

想象一下,你开了一家小餐馆,负责接待客人和上菜。客人就像网络请求,你处理请求的方式,就像程序处理 IO 操作的不同模式。今天就用餐馆经营做比喻,聊聊 BIO、NIO、AIO 这三种 IO 模型的区别。

先搞懂两个关键概念:同步/异步 和 阻塞/非阻塞

在讲三种模型前,得先明白这两组词的意思,它们是区分 IO 模型的核心:

  • 同步 vs 异步:看的是"结果怎么来"

    • 同步:你得主动去问结果(比如点餐后站在柜台等餐)
    • 异步:结果自动来找你(比如外卖到了骑手打电话通知你)
  • 阻塞 vs 非阻塞:看的是"等待时能不能干别的"

    • 阻塞:等待时啥也干不了(比如打电话时对方让等,你只能举着电话发呆)
    • 非阻塞:等待时可以做其他事(比如发微信后不用等回复,先去刷视频)

有了这两个标尺,我们就能给三种 IO 模型"贴标签"了:

  • BIO:同步阻塞
  • NIO:同步非阻塞
  • AIO:异步非阻塞

一、BIO:就像"一个客人配一个服务员"

BIO(阻塞 IO)是最传统的处理方式,就像小餐馆刚开业时的经营模式:

工作场景

你雇了几个服务员(线程),来了一个客人(请求),就派一个服务员全程跟进。服务员带客人入座、点餐、传菜,直到客人离开。如果客人临时去打电话(IO 操作未完成),服务员就只能站在旁边等着,啥也干不了。

代码里的表现

用 Java 早期的 Socket 编程就是典型的 BIO:

  • 服务器用 accept() 方法等客人来,没人来就一直等(阻塞)
  • 来了客人就派一个线程专门服务,线程调用 read() 读数据时,如果对方没发数据,线程就卡在那不动(阻塞)
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOServer {
    public static void main(String[] args) throws IOException {
        // 1. 创建服务器套接字,绑定到8080端口
        // 相当于餐馆租了个店面,门口挂了个牌子"8080号店"
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("BIO服务器启动,监听端口8080...");
        
        // 2. 创建线程池,管理服务员(线程)
        // 避免来了客人再临时雇人,提前准备好一批服务员
        ExecutorService executor = Executors.newCachedThreadPool();
        
        while (true) {
            // 3. 等待客户端连接(阻塞方法)
            // 服务员站在门口等客人,没人来就一直等,啥也干不了
            final Socket clientSocket = serverSocket.accept();
            System.out.println("新客人来了,分配一个服务员...");
            
            // 4. 为每个新连接分配一个线程处理(一客一线程)
            executor.execute(() -> {
                try {
                    // 5. 获取输入流,准备读取数据(客人点餐)
                    InputStream in = clientSocket.getInputStream();
                    byte[] buffer = new byte[1024];
                    
                    while (true) {
                        // 6. 读取数据(阻塞方法)
                        // 如果客人还没点餐(没发数据),服务员就站着等
                        int len = in.read(buffer);
                        if (len == -1) { // 客人说"点完了"
                            break;
                        }
                        // 处理客人的请求
                        System.out.println("收到客人消息:" + new String(buffer, 0, len));
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        // 7. 客人离开,关闭连接
                        clientSocket.close();
                        System.out.println("客人走了,服务员空闲了");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

问题在哪?

如果突然来了 100 个客人(高并发),你就得雇 100 个服务员(线程)。但服务员工资贵啊(线程占用内存),人多了还会互相干扰(CPU 上下文切换)。而且大部分时间服务员都在"站着等"(IO 阻塞),纯属浪费人力。

适合啥情况?

就像社区小餐馆,每天就几桌客人,用这种模式简单好管理。程序里就是并发量特别低的场景,比如内部小工具。

二、NIO:就像"一个服务员管整个餐厅"

NIO(非阻塞 IO)就像餐馆扩大规模后的聪明做法:雇一个全能服务员(单线程),通过"望闻问切"同时处理所有客人。

工作场景

这个服务员超能干:

  1. 先站在门口看有没有新客人(用 Selector 监听连接事件)
  2. 看到新客人就安排入座,然后继续盯着门口和其他桌子
  3. 时不时扫一眼各桌:有客人举手要点餐(读数据)就过去记单,有客人吃完要结账(写数据)就过去处理
  4. 如果某桌客人正在打电话(数据没准备好),服务员不傻等,先去忙别的桌子

代码里的关键角色

NIO 有三个核心"工具":

  • Channel(通道):就像每张餐桌,是和客人交互的场所(替代 BIO 里的 Socket)
  • Buffer(缓冲区):就像服务员的记事本,记着客人点的菜(数据必须先放这里才能处理)
  • Selector(选择器):就像服务员的"火眼金睛",能同时盯着所有桌子的动态(哪个需要服务)
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;

public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 1. 创建服务器通道(相当于餐厅大门)
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 绑定8080端口
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        // 设置为非阻塞模式(关键!服务员不会傻等)
        serverSocketChannel.configureBlocking(false);
        
        // 2. 创建选择器(相当于服务员的"火眼金睛")
        // 能同时盯着所有桌子(通道)的动态
        Selector selector = Selector.open();
        
        // 3. 将服务器通道注册到选择器,关注"新客人到了"事件(OP_ACCEPT)
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO服务器启动,监听端口8080...");
        
        while (true) {
            // 4. 等待事件发生(阻塞方法,但可以设置超时时间)
            // 服务员站在餐厅中央观察,没人需要服务就稍等
            int readyChannels = selector.select();
            if (readyChannels == 0) {
                continue; // 没人需要服务,继续观察
            }
            
            // 5. 获取所有有事件发生的通道(需要服务的桌子)
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
            
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                
                // 6. 处理"新客人到了"事件
                if (key.isAcceptable()) {
                    // 获取服务器通道
                    ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                    // 接受新连接(非阻塞,因为前面已经知道有客人来了)
                    SocketChannel clientChannel = serverChannel.accept();
                    System.out.println("新客人来了,安排入座...");
                    
                    // 客户端通道也要设置为非阻塞
                    clientChannel.configureBlocking(false);
                    // 将客户端通道注册到选择器,关注"客人要点餐"事件(OP_READ)
                    clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                // 7. 处理"客人要点餐"事件(有数据可读)
                else if (key.isReadable()) {
                    // 获取客户端通道
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    // 获取缓冲区(服务员的记事本)
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    
                    // 读取数据(非阻塞,因为前面已经知道有数据了)
                    int bytesRead = clientChannel.read(buffer);
                    if (bytesRead > 0) {
                        // 切换缓冲区为读模式
                        buffer.flip();
                        // 读取数据并处理
                        byte[] data = new byte[buffer.remaining()];
                        buffer.get(data);
                        System.out.println("收到客人消息:" + new String(data));
                        
                        // 清空缓冲区,准备下次使用
                        buffer.clear();
                    } else if (bytesRead == -1) {
                        // 客人离开了,关闭通道
                        clientChannel.close();
                        System.out.println("客人走了");
                    }
                }
                
                // 8. 移除已处理的事件,避免重复处理
                keyIterator.remove();
            }
        }
    }
}

为啥比 BIO 好?

一个服务员就能管几十桌客人(单线程处理多连接),人力成本骤降。服务员大部分时间都在"主动干活",而不是"傻等",效率高多了。就像酒吧的调酒师,同时盯着好几个酒杯,哪个该加料了就过去处理一下。

适合啥情况?

就像生意火爆的连锁餐厅,客人多但 each 客人停留时间不长。程序里就是高并发场景,比如聊天软件服务器、消息队列(Kafka 就用了类似思想)。

三、AIO:就像"全自动智能餐厅"

AIO(异步 IO)是最高级的模式,相当于把餐厅改成了全自动模式:

工作场景

你雇了个前台(线程),客人来了只需在前台登记需求,然后就可以随便逛(线程继续干别的)。餐厅里有各种智能设备:

  • 客人扫码点餐,系统自动通知后厨(IO 操作由系统处理)
  • 菜做好了,传送带自动把菜送到桌前,同时给客人发消息:“您的餐到了”(IO 完成后系统回调通知)
  • 整个过程中,前台不用管任何细节,客人也不用等

代码里的表现

Java 7 后的 AsynchronousSocketChannel 就是 AIO:

  • 发起 IO 操作时,只需要告诉系统:“完事了叫我”(注册回调函数)
  • 线程该干啥干啥,完全不耽误
  • 系统处理完 IO 后,自动调用你写的回调方法
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;

public class AIOServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        System.out.println("AIO服务器启动,监听端口8080...");
        
        // 1. 创建异步服务器通道(全自动餐厅的入口)
        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
        // 绑定8080端口
        serverChannel.bind(new InetSocketAddress(8080));
        
        // 用于等待,防止主线程退出
        CountDownLatch latch = new CountDownLatch(1);
        
        // 2. 接受连接(异步操作)
        // 告诉系统:"有人来的时候叫我,我先去忙别的了"
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            // 3. 连接建立成功后的回调方法(客人到了,系统通知)
            @Override
            public void completed(AsynchronousSocketChannel clientChannel, Object attachment) {
                System.out.println("新客人来了,系统已自动安排...");
                
                // 继续接受其他连接(告诉系统:下一个客人来的时候也叫我)
                serverChannel.accept(null, this);
                
                // 创建缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                
                // 4. 异步读取数据(告诉系统:客人点餐完成后叫我)
                clientChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                    // 5. 读取完成后的回调方法(客人点完餐,系统通知)
                    @Override
                    public void completed(Integer bytesRead, ByteBuffer buffer) {
                        if (bytesRead > 0) {
                            // 切换缓冲区为读模式
                            buffer.flip();
                            // 读取数据并处理
                            byte[] data = new byte[buffer.remaining()];
                            buffer.get(data);
                            System.out.println("收到客人消息:" + new String(data));
                            
                            // 可以在这里处理业务逻辑,然后异步写回响应
                            
                            // 清空缓冲区,准备下次读取
                            buffer.clear();
                            // 继续监听这个客人的新消息
                            clientChannel.read(buffer, buffer, this);
                        } else if (bytesRead == -1) {
                            // 客人离开了
                            try {
                                clientChannel.close();
                                System.out.println("客人走了");
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                    
                    // 读取失败的回调方法
                    @Override
                    public void failed(Throwable exc, ByteBuffer buffer) {
                        exc.printStackTrace();
                        try {
                            clientChannel.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
            
            // 连接建立失败的回调方法
            @Override
            public void failed(Throwable exc, Object attachment) {
                exc.printStackTrace();
                latch.countDown();
            }
        });
        
        // 等待,保持服务器运行
        latch.await();
    }
}

真的完美吗?

理论上效率最高,但现实中有点"水土不服":

  • 智能设备成本高(操作系统对异步 IO 的支持不完善,尤其 Linux 环境)
  • 出了问题不好排查(回调逻
  • 辑复杂,调试麻烦)
  • 就像自动餐厅,偶尔会出现"菜送到了但客人没收到通知"的 bug

适合啥情况?

就像需要长时间烹饪的高端餐厅(IO 操作耗时久),比如大文件传输、数据库慢查询,这时全自动模式才能体现优势。

三者对比:一张表看懂核心差异

对比项 BIO(同步阻塞) NIO(同步非阻塞) AIO(异步非阻塞)
形象比喻 一个客人配一个服务员 一个服务员管所有客人 全自动餐厅,客人自助+系统通知
线程状态 大部分时间在傻等 主动巡视,不等闲 彻底解放,啥也不用等
并发能力 弱(100 人需要 100 个服务员) 强(1 人能管 1000 人) 极强(理论无上限)
编程难度 简单(像写流水账) 中等(要懂"望闻问切") 难(要设计回调逻辑)
典型应用 早期小网站 Netty 框架、Kafka 大文件传输、数据库异步操作
Logo

一座年轻的奋斗人之城,一个温馨的开发者之家。在这里,代码改变人生,开发创造未来!

更多推荐