BIO NIO AIO
BIO:采用阻塞式 I/O 模型,线程在执行 I/O 操作时被阻塞,无法处理其他任务,适用于连接数较少的场景。
NIO:采用非阻塞 I/O 模型,线程在等待 I/O 时可执行其他任务,通过 Selector 监控多个 Channel 上的事件,适用于连接数多但连接时间短的场景。
AIO:使用异步 I/O 模型,使得 I/O 操作可以异步进行。这意味着线程发起一个读写操作后不必等待其完成,可以立即进行其他任务,并且当读写操作真正完成时,线程会被异步地通知。
当客户端请求连接到服务端,并请求数据的过程中,其实就是通过 Socket 进行连接。可以把 Socket 理解成插座,就像手机需要充电,而电要从服务端获取。手机的充电口和服务端的插座本质上都是 Socket。
BIO(同步阻塞)
假设客户端连接到了服务端,但服务端从磁盘中查询数据的过程很慢。只要服务端没有查询完,这个客户端就需要一直等着。如果有其他客户端请求连接,也得等着服务端把当前请求执行完,才能继续处理下一个客户端。
如果请求的客户端数量较少还好,但如果有 1万个客户端 呢?

它们只能干等着,就像你去饭店吃饭,在窗口点菜后,一直站在窗口等着饭做好。在饭没做好之前,服务员无法接待其他顾客,大家只能干等着,最终饭店迟早会凉。而这就是 传统的 BIO 模式(同步阻塞)。
来看一下 BIO 的代码:

在服务端,首先创建 ServerSocket 并设置端口号,然后执行 accept()
。这个操作会 阻塞 线程,直到有客户端连接。客户端连接后,服务端获取 Socket,再通过 getInputStream()
获取流,最终读取流中的内容。
BIO 的问题在于 如果获取流或读取流的过程很慢,整个期间服务端无法处理其他客户端请求,导致服务器吞吐量低。
NIO(同步非阻塞)
回到吃饭的例子,如果你在窗口点菜后,服务员给你一个小票,你就可以去旁边等着,其他顾客也能继续点菜,不用一直堵在窗口。等饭做好后,服务员叫你的号码,你再去取餐。这样,效率就大大提升了。这就是 NIO(同步非阻塞)。
NIO 的核心特点是 一个线程可以处理多个 Socket 连接,主要流程如下:
通道(Channel) 当客户端连接到服务端时,会创建一个 通道(Channel),也称为 Socket Channel。
轮询器(Selector) 服务器端有一个 Selector(轮询器),客户端的 Channel 会注册到 Selector 上。如果有多个客户端连接,它们的 Channel 也都会注册到 Selector 上。
轮询机制 服务器端的线程会不断轮询 Selector,检查 Channel 的状态:
如果 Channel 变成可连接状态,服务器就会取出该通道,改为监听 是否可读 的状态,并重新注册到 Selector。
如果 Channel 变成可读状态,服务器就会读取数据。
这样,每个客户端的事件状态变化时,都会被轮询到并执行相应操作,而不是像 BIO 那样 必须等前一个请求执行完,才能轮到下一个。
NIO 的优化:多路复用(Selector/Poll)
NIO 通过 轮询机制 实现了并发处理,但仍然有个问题:
轮询本身是需要不断从用户空间切换到内核空间,以查询通道的状态。每次轮询时,操作系统需要进行 用户态 → 内核态 的切换,这个切换过程是 昂贵 的。
想象一下,你在家里烤串,每次都要起身走到外面看看熟了没有。一个熟了就拿回去吃,然后再起身检查下一个,这样来回跑很麻烦。而更高效的方法是 一次性把所有熟的串都拿走,然后回家慢慢吃。
在计算机系统中,对应的优化方法是:
- 把所有 Channel 的文件描述符(FD)传递给内核,让 内核 自己去遍历这些通道的状态。
- 之后,内核把所有有状态变化的通道信息返回给用户空间。这样,用户线程只需要 一次性获取所有状态变更的通道,而不是反复轮询。
这样,整个过程中 用户态和内核态的切换次数大大减少,一次调用,就可以查询到多个路通道的状态,也就是这些通道被一次调用给复用了,这就叫所谓的多路复用。大幅提升了效率。而实现这个多路复用的机制,就是 Selector / Poll / Epoll。