BIO、Select、Poll、Epoll 四大IO模型详解笔记
BIO、Select、Poll、Epoll 四大IO模型详解笔记(通俗比喻+原理+对比+实战场景)
一、前置基础概念
1. 核心名词解释
- IO(输入/输出):网络读写、文件读写等数据交互操作,网络IO就是客户端与服务端的数据通信。
- Socket(套接字):网络连接的载体,一条客户端与服务端的长连接/短连接对应一个Socket。
- 线程:操作系统最小执行单元,创建、切换线程会消耗CPU资源,大量线程会引发上下文切换,导致性能暴跌。
- 长连接:连接建立后持续保持,服务端可主动推送数据(直播、聊天、游戏、弹幕必备)。
- 短连接:一次请求+一次响应后立即断开,普通网页、接口请求常用。
- IO多路复用:一个线程/少量线程,同时监控大量Socket连接,是高并发网络服务的核心技术,Linux下主流实现为
select、poll、epoll。 - 上下文切换:CPU在线程之间来回切换执行任务,频繁切换会产生巨大性能开销。
- 轮询:循环逐个检查每个连接是否有数据请求,无效轮询会浪费大量CPU。
- 事件回调:连接有数据/事件时主动通知程序,无需主动轮询,是Epoll高效的核心。
2. 场景引入
小型项目(几十在线用户)用简单IO模型即可;数万长连接高并发场景(直播、IM、游戏网关),必须使用IO多路复用,否则服务器会被大量连接拖垮。下文用饭店服务统一比喻,方便理解。
二、四大IO模型逐类解析
(一)BIO 阻塞IO(传统单连接单线程)
1. 工作原理
每建立一条客户端连接,服务端就单独创建一个线程专门处理该连接。线程全程阻塞,连接无数据时也不会释放,一直等待客户端请求。
2. 饭店比喻
饭店每来一桌客人(一条连接),就专门分配一名服务员(一个线程)全程陪同。客人不点菜、不说话,服务员也必须原地等待,不能服务其他桌。
3. 优缺点
- 优点:实现简单,逻辑直白,适合并发量极小(几十人以内)的单体项目。
- 缺点:
- 连接数 = 线程数,上万连接就会创建上万线程,CPU频繁上下文切换,资源耗尽;
- 大量空闲连接会占用线程资源,资源利用率极低。
4. 实战案例
早期小型管理系统、单机简易接口服务,在线用户少,并发压力小,使用BIO完全够用。
致命问题:一旦用于直播、IM等上万长连接场景,服务器直接卡死。
(二)Select(第一代IO多路复用)
1. 工作原理
使用单个线程监控所有Socket连接。线程会循环遍历全部连接,逐个检查是否有读写事件(有无请求),有则处理,无则继续轮询。
2. 饭店比喻
整个饭店只雇一名总服务员,拿着所有桌号清单,从第一桌走到最后一桌,挨个询问“是否需要服务”。哪怕99%客人都在挂机休息,也必须走完完整轮询。
3. 核心限制&缺点
- 连接数量有上限:Linux系统下默认最大监听连接数有限,无法支撑超大并发;
- 全量轮询,效率低下:连接越多,轮询耗时越长,空闲连接越多,无效CPU消耗越大;
- 仅告知“有事件”,不定位具体哪条连接,轮询无法避免。
4. 适用场景
中等并发连接,对性能要求不高的服务,目前基本被淘汰。
(三)Poll(第二代IO多路复用,Select升级版)
1. 工作原理
整体轮询逻辑和Select一致,仅解除连接数量上限,依然是单线程全量遍历所有连接。
2. 饭店比喻
还是一名总服务员轮询所有餐桌,不再限制餐桌总数,但依旧需要一桌一桌挨个询问,没有改变“无效轮询”的本质。
3. 优缺点
- 优点:突破Select的连接数限制,可监听更多连接;
- 缺点:保留轮询短板,连接越多、CPU无效消耗越严重,高并发下性能依旧拉胯。
4. 现状
部分老旧系统沿用,新项目基本不使用。
(四)Epoll(Linux第三代IO多路复用,工业级高并发首选)
1. 核心工作原理
彻底抛弃主动轮询,采用事件通知+回调机制。
内核为每个连接绑定“事件监听”,当连接有读写、数据到达等事件时,主动通知应用线程,线程只处理有事件的连接,空闲连接完全不占用CPU。
2. 饭店比喻(经典易懂)
给每一桌客人安装呼叫铃:
- 服务员平时休息待命,不用来回走动轮询;
- 客人需要服务(有IO事件),按下呼叫铃;
- 服务员收到通知,精准前往对应桌号处理业务;
- 处理完毕继续休息,全程无无效跑动。
3. 核心三大优势
- 无连接数上限:理论可支持百万级长连接,适配互联网高并发场景;
- 无无效轮询:仅处理活跃连接,百万空闲连接几乎不消耗CPU;
- 事件精准通知:内核直接告知具体哪个连接、发生什么事件,定位效率拉满。
4. 补充说明
- 平台限制:Epoll是Linux系统专属;Windows系统对应技术为
IOCP(完成端口); - 主流底层依赖:Redis、Nginx、Node.js、Netty、IM网关、直播弹幕服务器等高并发中间件/框架,底层均基于Epoll实现。
5. 实战案例
- 直播弹幕服务器:数十万用户同时建立长连接,绝大部分时间处于空闲状态,Epoll用少量线程管理全部连接,仅在用户发弹幕时触发处理;
- Redis服务:海量客户端连接,依靠Epoll实现高并发网络读写;
- 微信/QQ IM网关:支撑亿级长连接,核心IO模型为Epoll。
三、四大模型横向对比表
| IO模型 | 核心模式 | 线程模型 | 连接上限 | CPU消耗 | 适用场景 |
|---|---|---|---|---|---|
| BIO | 一连接一线程 | 多线程 | 极低(线程限制) | 高(上下文切换) | 小型项目、低并发短连接 |
| Select | 单线程全量轮询 | 单线程多路复用 | 有上限 | 中高(无效轮询) | 老旧中等并发系统 |
| Poll | 单线程全量轮询 | 单线程多路复用 | 无上限 | 中高(无效轮询) | 过渡型系统,逐步淘汰 |
| Epoll | 事件回调、按需处理 | 单/少量线程 | 百万级无上限 | 极低(仅处理活跃连接) | 高并发长连接:直播、IM、网关、中间件 |
四、关键问题解答(面试高频)
1. 为什么BIO无法支撑上万长连接?
上万连接会创建上万线程,操作系统线程资源有限,且大量线程切换会耗尽CPU,大部分线程还处于空闲等待状态,资源严重浪费。
2. Select/Poll 和 Epoll 最本质区别?
Select/Poll:主动轮询,挨个检查所有连接;
Epoll:被动事件通知,有事件才处理,空闲连接完全无视。
3. Epoll 为什么是高并发长连接的标配?
长连接场景下绝大多数连接长期空闲,Epoll 规避无效轮询和多线程开销,用极低资源承载海量连接。
4. Windows 可以使用 Epoll 吗?
不可以,Epoll 仅Linux;Windows 平台使用 IOCP(IO完成端口),功能与Epoll对等。
五、业务选型指南
- 小型CRUD项目、低并发短连接:普通BIO即可,开发简单,无需复杂优化;
- 中等并发、老旧系统:历史项目维持Select/Poll,新项目不推荐;
- 高并发、海量长连接(直播、聊天、游戏、网关、中间件):Linux环境优先Epoll,Windows使用IOCP;
- Java开发建议:使用Netty框架(底层封装Epoll,屏蔽底层差异,开箱即用)。
六、思维拓展(原文引申)
- BIO 对应工作状态:机械分工,一人盯一件事,大量时间空耗,资源利用率低;
- Select/Poll 对应工作状态:看似不停忙碌,实则大量无效工作,效率低下;
- Epoll 对应工作状态:聚焦核心事件,无事休整、有事精准出击,拒绝无效内卷,是高效做事的思路。
七、整体总结
- 从BIO到Epoll,演进核心是用更少线程,管理更多连接,消灭无效CPU消耗;
- IO多路复用是高并发网络编程的基石,Epoll 凭借事件驱动成为Linux下的最优解;
- 技术选型贴合业务:短连接低并发不用过度设计,海量长连接必须选用Epoll这类高性能模型。
补充:Java NIO、Netty 底层 Epoll 实战代码示例
前置说明:
- Java NIO 对应操作系统 IO 多路复用:Windows 用 Select,Linux 默认自动适配 Epoll;
- JDK NIO 原生
Selector在 Linux 底层封装 epoll; - Netty 默认自动检测 Linux 环境,启用 Epoll 原生传输(
EpollEventLoopGroup),性能优于 JDK 普通 NIO Selector; - BIO =
java.net.ServerSocket;NIO =java.nio.channels;Netty 封装 NIO/Epoll。
一、基础名词补充
- Channel:NIO 里的 Socket 连接,对应 Epoll 监听的文件描述符 fd;
- Selector:多路复用器,底层 Linux 封装 epoll_create/epoll_wait;
- SelectionKey:注册事件(可读、可写、连接就绪),对应 epoll 事件回调;
- EpollEventLoopGroup:Netty Linux 专属,直接调用系统 epoll 原生 API,无 JDK 层封装损耗。
二、原生 JDK NIO(底层 Epoll)极简服务端代码
运行环境:Linux,JDK8+,底层自动使用 epoll。
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 NioEpollServer {
public static void main(String[] args) throws IOException {
// 1. 创建多路复用器Selector,Linux底层调用epoll_create
Selector selector = Selector.open();
// 2. 开启服务通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8888));
// 设置非阻塞(Epoll必须非阻塞IO)
serverChannel.configureBlocking(false);
// 3. 将服务通道注册到selector,监听【连接就绪事件】
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO服务启动,端口8888,底层使用Epoll");
// 4. 循环事件轮询(底层调用epoll_wait,阻塞等待事件)
while (true) {
// 阻塞等待有事件到达,无事件则休眠,不消耗CPU(Epoll核心优势)
selector.select();
// 获取所有产生事件的连接
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); // 必须手动移除,避免重复处理
if (key.isAcceptable()) {
// 事件1:有新客户端连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
// 客户端通道注册到selector,监听读事件
client.register(selector, SelectionKey.OP_READ);
System.out.println("新客户端接入:" + client.getRemoteAddress());
} else if (key.isReadable()) {
// 事件2:客户端有数据可读(epoll触发EPOLLIN事件)
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = client.read(buf);
if (len <= 0) {
// 客户端断开,关闭通道
key.cancel();
client.close();
continue;
}
buf.flip();
String msg = new String(buf.array(), 0, len);
System.out.println("收到客户端消息:" + msg);
// 回写数据
client.write(ByteBuffer.wrap("服务端已收到消息\n".getBytes()));
}
}
}
}
}
代码对应 Epoll 底层逻辑
Selector.open()→ 系统调用epoll_create创建 epoll 实例;channel.register()→epoll_ctl把 socket fd 添加到 epoll 监听列表;selector.select()→ 阻塞调用epoll_wait,无IO事件时线程休眠;- 客户端发数据 → 内核主动触发事件,
select()立刻返回,只遍历有事件连接,无无效轮询。
局限
JDK NIO 的 Selector 是跨平台封装,Linux 虽底层是 epoll,但存在一层 Java 堆/内核缓冲区拷贝,性能不如 Netty 原生 Epoll。
三、Netty 原生 Epoll 高并发服务端代码(Linux专属最优方案)
Netty 区分两套实现:
- 跨平台 NIO:
NioEventLoopGroup(Windows/Linux通用,底层Linux走epoll) - Linux 高性能专属:
EpollEventLoopGroup直接调用 epoll 原生系统API,零多余封装,IM、网关、直播推荐使用。
Maven依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version>
</dependency>
Netty Epoll 服务端完整示例
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
public class NettyEpollServer {
public static void main(String[] args) throws InterruptedException {
// 1. 使用Epoll专属线程池(仅Linux可用,底层原生epoll)
// bossGroup:处理连接建立;workerGroup:处理读写事件
EpollEventLoopGroup bossGroup = new EpollEventLoopGroup(1);
EpollEventLoopGroup workerGroup = new EpollEventLoopGroup(4);
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
// 指定Epoll服务通道,直接绑定系统epoll
.channel(EpollServerSocketChannel.class)
.childHandler(new SimpleServerHandler());
// 绑定8888端口启动服务
ChannelFuture future = bootstrap.bind(8888).sync();
System.out.println("Netty Epoll服务启动成功,端口8888");
// 阻塞等待服务关闭
future.channel().closeFuture().sync();
} finally {
// 优雅关闭线程池
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
// 自定义业务处理器
static class SimpleServerHandler extends ChannelInboundHandlerAdapter {
// 客户端发送数据触发(对应epoll可读事件)
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] data = new byte[buf.readableBytes()];
buf.readBytes(data);
String content = new String(data);
System.out.println("Netty收到消息:" + content);
// 回复客户端
ctx.writeAndFlush(ctx.alloc().buffer().writeBytes("Epoll服务响应成功\n".getBytes()));
super.channelRead(ctx, msg);
}
// 客户端断开连接
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端连接断开");
super.channelInactive(ctx);
}
}
}
关键说明
EpollEventLoopGroup+EpollServerSocketChannel= Linux 原生 epoll,跳过 JDK NIO 中间层,性能更高;- 少量工作线程(示例4个)即可管理上万/百万长连接,完全依托 epoll 事件驱动;
- 空闲长连接不会占用CPU,只有发送消息时触发回调处理。
四、三种IO模型代码效果对比
1. BIO 代码缺陷对比(一连接一线程)
// BIO 伪代码,不推荐长连接场景
ServerSocket server = new ServerSocket(8888);
while(true){
// 每来一个连接新建线程
Socket socket = server.accept();
new Thread(() -> {
while(true){
socket.getInputStream().read(); // 线程阻塞卡死,占用资源
}
}).start();
}
上万长连接会创建上万线程,CPU上下文切换爆满,和Epoll形成鲜明差距。
2. JDK NIO Selector vs Netty Epoll
| 实现 | 底层 | 性能 | 适用场景 |
|---|---|---|---|
| BIO | 阻塞单连接线程 | 极差,并发受限 | 本地小型工具 |
| JDK NIO Selector | Linux epoll(封装层) | 中等 | 跨平台简单服务 |
| Netty EpollEventLoop | Linux原生epoll API | 最优 | IM网关、直播、百万长连接、中间件 |
五、线上生产落地案例结合代码
案例1:直播弹幕网关(Netty Epoll)
直播间十万用户长连接,99%时间空闲,仅发弹幕时有IO事件:
- 使用
EpollEventLoopGroup,4个工作线程承载10万连接; - 无弹幕时 epoll 阻塞休眠,CPU占用低于5%;
- 用户发送弹幕触发可读事件,线程批量处理推送至直播间;
若改用BIO,10万连接需要10万线程,服务器直接OOM卡死。
案例2:Redis底层epoll逻辑
Redis C语言直接调用epoll API,思路和Netty Epoll一致:
- 注册所有客户端fd到epoll;
- 无命令时阻塞等待事件;
- 客户端发送指令触发事件,单线程处理读写,支撑十万客户端连接。
六、核心总结
- Linux环境下,JDK NIO Selector底层封装epoll,实现多路复用,替代BIO多线程;
- Netty 提供专属
EpollEventLoopGroup,直接调用系统epoll,是Java高并发长连接最优方案; - 代码核心特征:全部通道设置非阻塞,依靠事件通知,放弃循环轮询,海量空闲连接几乎零CPU消耗;
- 开发规范:Linux生产环境网关、IM、消息推送服务强制使用 Netty Epoll 模式。

浙公网安备 33010602011771号