IO模型与系统调用
阻塞IO:read()、write() 用户进程发起IO请求后,如果数据未准备好,进程会阻塞,直到数据准备好并完成IO操作 非阻塞IO:fcntl() 设置非阻塞模式,然后使用read()、write() 用户进程发起IO请求后,如果数据未准备好,进程不会阻塞,而是立即返回,并设置一个标志位,表示数据未准备好 IO多路复用:select()、poll()、epoll(),用于监控多个文件描述符的IO事件 用户进程发起IO请求后,会注册一个或多个文件描述符的IO事件,然后等待这些事件的发生,当有事件发生时,内核会通知用户进程,用户进程可以继续进行IO操作 信号驱动IO:sigaction() 用于注册信号处理函数 用户进程发起IO请求后,会向操作系统注册一个信号处理函数,当数据准备好时,内核会发送一个信号给用户进程,用户进程可以继续进行IO操作 异步IO:aio_read()、aio_write(),用于异步读写文件描述符 用户进程发起IO请求后,会立即返回,内核会在数据准备好后,自动完成IO操作,并通知用户进程
select()、poll()、epoll()比较
select
实现方式:轮询所有文件描述符(线性遍历)
最大fd数量:受限于 FD_SETSIZE(默认 1024)
时间复杂度:O(n)(每次遍历所有 fd)
触发模式:仅支持水平触发(LT)
内核/用户数据拷贝:每次调用需拷贝全部 fd 集合
poll
实现方式:轮询所有文件描述符(链表结构)
最大fd数量:理论无限制(基于链表)
时间复杂度:O(n)
触发模式:仅支持水平触发(LT)
内核/用户数据拷贝:每次调用需拷贝全部 fd 集合
epoll(linux特有)
实现方式:事件驱动(仅处理活跃的 fd)
最大fd数量:仅受系统内存限制
时间复杂度:O(1) 或 O(活跃 fd 数)
触发模式:支持水平触发(LT)和边缘触发(ET)
内核/用户数据拷贝:仅注册一次,无需重复拷贝
水平触发与边缘触发
水平触发(LT):只要 fd 就绪(如缓冲区有数据未读),会持续通知。
边缘触发(ET):仅在 fd 状态变化时触发一次通知(如新数据到达)。
Java NIO的Selector 是基于水平触发(Level-Triggered,LT) 的事件通知机制。
Java NIO 在 Linux 上默认使用epoll,但强制使用水平触发模式(即使epoll支持 ET)。
水平触发的典型问题与优化 问题场景 未及时处理数据:若每次仅读取部分数据,Selector 会持续触发 OP_READ,导致无效循环。 频繁可写事件:若通道始终可写(如 TCP 发送缓冲区未满),OP_WRITE 会不断触发,浪费 CPU。 优化建议 读/写操作彻底:在 OP_READ 事件中循环读取到无数据,在 OP_WRITE 事件中注册/注销写事件。
Java线程状态
RUNNABLE状态 定义:线程正在执行或准备执行任务,可能正在运行或等待操作系统资源(如CPU时间片或I/O操作完成) 阻塞I/O时的表现: 当线程调用阻塞I/O方法(如InputStream.read()、Selector.select())时,线程会被操作系统挂起,直到数据就绪。 在Java的线程状态模型中,此时线程仍显示为RUNNABLE(从JVM角度看线程仍然处于可运行状态,只是在等待底层OS完成IO操作),但底层操作系统的线程状态可能是sleep。 因为JVM仅跟踪线程是否在等待JVM内部的资源(如锁),而不感知操作系统层面的I/O阻塞。 BLOCKED状态 定义:线程因等待进入同步块(synchronized)或获取对象锁而阻塞。 与I/O无关:BLOCKED状态仅与同步机制相关 WAITING和TIMED_WAITING 状态 触发条件:线程因调用Object.wait()、Thread.join()、LockSupport.park()等方法主动进入等待状态。 与I/O无关:这些状态是由线程主动发起的等待,而非I/O操作导致。 为什么阻塞I/O不显示为BLOCKED? JVM与操作系统的分工: JVM的线程状态模型仅反映线程在JVM内部的行为(如锁竞争、主动等待等)。 阻塞I/O由操作系统内核处理,JVM无法直接感知其阻塞细节,因此统一标记为RUNNABLE。
// 线程状态为RUNNABLE,即使I/O未就绪
InputStream in = new FileInputStream("file.txt");
int data = in.read(); // 阻塞直到数据可用