BIO
一、重要概念解析
1. 阻塞 IO 与 非阻塞 IO
- 定义:程序级别的概念,描述程序在请求操作系统执行 IO 操作后,若 IO 资源未准备好时的处理方式。
- 阻塞 IO:
- 程序等待,直到 IO 资源准备好并完成操作。
- 示例:调用 read() 时,若数据未就绪,线程暂停。
- 非阻塞 IO:
- 程序不等待,立即返回(通常返回错误码),通过轮询检查 IO 是否完成。
- 示例:线程反复调用 read() 检查数据状态。
- 关键区别:阻塞等待 vs 非阻塞轮询。
2. 同步 IO 与 异步 IO
- 定义:操作系统级别的概念,描述操作系统在收到 IO 请求后,若资源未就绪时的响应方式。
- 同步 IO:
- 操作系统不立即响应,程序需等待资源就绪。
- 示例:accept() 或 read() 阻塞至数据到达。
- 异步 IO:
- 操作系统立即返回标记(如文件描述符),资源就绪后通过事件机制通知程序。
- 示例:aio_read() 返回后,数据准备好时通知。
- 关键区别:同步需等待 vs 异步立即返回并回调。
二、传统 BIO 通信方式简介
- 定义:传统阻塞式 IO(BIO,Blocking IO)通信方式。
- 特点:
- 客户端:发出请求后等待服务器响应,无法执行其他任务。
- 服务器端:单线程处理请求时,其他客户端请求需排队等待。
- 典型场景:
- 客户端 A 请求,服务器处理 A 时,客户端 B 请求被阻塞。
BIO 的问题
- 并发性差:
- 服务器单线程一次只能处理一个客户端请求。
- 高并发下,客户端请求排队严重影响效率。
- 资源限制:
- 单线程无法应对多个同时到达的请求。
三、多线程方式(伪异步方式)
- 改进思路:
- 服务器端:主线程接收请求后,将业务处理交给独立线程,主线程继续接收其他请求。
- 客户端:使用子线程与服务器通信,主线程不受阻塞,子线程通过监听/观察模式获取响应。
- 优点:
- 客户端主线程可继续工作。
- 服务器端业务处理并行化。
多线程的局限性
- 接受请求仍阻塞:
- accept() 仍为单线程顺序执行,无法并行接收多个客户端请求。
- 业务处理虽多线程化,但数据接收仍串行。
- 线程资源限制:
- Linux 下线程数有限(查看:cat /proc/sys/kernel/threads-max)。
- 线程切换开销随数量增加而增大,CPU 用于业务的时间减少。
- 资源消耗:
- JVM 创建线程需分配堆栈空间(默认 128K,可通过 -Xss 调整)。
- 线程池虽缓解创建开销,但任务积压仍消耗内存。
-
长连接问题:
- 长连接下线程不释放,资源占用失控。
-
结论:多线程仅优化业务处理,未解决 accept() 和 read() 的阻塞问题,不是高并发的根本解决方案。
四、BIO 通信方式深入分析
问题根源
- 核心:BIO 的阻塞性源于 accept() 和 read() 的操作系统级同步 IO 实现,而非是否使用多线程。
- 验证方法:
- 模拟 20 个客户端并发请求,观察服务器单线程和多线程的表现。
实验代码分析
- 客户端代码:
- SocketClientDaemon:启动 20 个线程模拟客户端。
- SocketClientRequestThread:使用 CountDownLatch 确保 20 个客户端同时发送请求。
- 行为:连接服务器,发送请求,等待响应。
- 单线程服务器(SocketServer1):
- 使用 serverSocket.accept() 接收请求,顺序处理。
- 日志显示:请求逐一被接收和处理。
- 多线程服务器(SocketServer2):
- 主线程 accept() 接收请求,创建线程处理业务。
- 日志显示:accept() 仍顺序执行,业务处理并行。
实验结果
- 单线程:20 个请求按序处理,效率低。
- 多线程:
- 接收仍串行(accept() 阻塞)。
- 处理并行,但未解决根本阻塞问题。
五、阻塞原因解析
- 操作系统支持:
- accept() 和 read() 的阻塞性依赖操作系统同步 IO 实现。
- 工作原理:
- 服务器调用 accept(),询问操作系统是否有新连接。
- 若无连接,操作系统等待,accept() 阻塞。
- ServerSocket.accept():监听连接并接受,阻塞直到连接建立。
- 操作系统未返回数据,线程挂起等待。
六、总结
- BIO 核心问题:
- accept() 和 read() 的阻塞性限制了并发能力。
- 多线程局限:
- 仅优化业务处理,接收仍串行,资源消耗大。
- 解决方向:
- 异步 IO 或 IO 多路复用(如 NIO)可突破阻塞限制。