Java IO二月 21, 2026IO 模型与概念框架# - IO 模型与概念框架 - 四个基本概念 - 阻塞 vs 非阻塞 - 定义:一次调用在数据未就绪时是否立即返回 - 阻塞:调用线程进入等待,直到条件满足才返回,例如:InputStream.read(),数据没到就卡住 - 非阻塞:调用立即返回,不等数据 - 读:没数据时返回 0/-1 或抛特定异常 - 写:写不完就写一部分,返回已写字节数 - JavaNIO - SocketChannel.configureBlocking(false) 之后,read() 不会阻塞,可能返回 0 - 阻塞非阻塞指的就是线程是否在这次调用上被卡主 - 同步 vs 异步 - 定义:结果是由谁来完成并通知 - 同步:调用负责发起+等待/轮询+拿结果,即使是非阻塞轮询,仍然是同步,因为结果获取的责任方在调用方 - 异步:调用方发起后立即返回,后续由系统/框架在操作完成时回调/事件通知取结果,调用方不需要自己轮询等待结果 - JavaNIO - AsynchronousSocketChannel + CompletionHandler 是典型的 AIO 异步模型:完成时回调你 - 四象限组合 - 同步阻塞:BIO(经典 read() 卡住直到有数据) - 同步非阻塞:NIO 非阻塞 read() + 你自己轮询/配合 Selector 处理就绪事件,本质仍然是调用方驱动流程 - 异步非阻塞:AIO(回调通知你“读完了/写完了“) - 异步阻塞:理论上存在,比如发起异步后又 get() 阻塞等待,但是工程上不这么用 - BIO/NIO/AIO 的准确定义 - BIO(Blocking IO) - API 代表:ServerSocket/Socket、InputStream/OutputStream - 特征:accept()/read()/write() 默认阻塞 - 典型服务端模型:1 连接 1 线程(或线程池+阻塞读写) - 痛点: - 连接数上来后线程数膨胀:上下文切换、栈内存、调度开销 - 大量线程处于阻塞态,浪费资源 - NIO(Non-blocking I/O + Multiplexing) - API 代表:Channel/Buffer/Selector - 关键点:非阻塞 Channel + Selector 多路复用 - 本质:少量线程监听大量连接的就绪事件,再去执行非阻塞读写 - 价值: - 连接多时不需要等比例增加线程 - 事件驱动,提升并发连接处理能力 - NIO 三件套 - Channel:数据容器,可读可写 - Buffer:数据容器 - Selector:事件分发器 - AIO(Asynchronous I/O) - API 代表 AsynchronousServerSocketChannel/AsynchronousSocketChannel - 特征:发起读写后立即返回,完成后通过 CompletionHandler 回调通知结果 - 工程事实:Java AIO 在不同 OS 上底层支持与效果不同;很多高性能网络主流仍然是 Netty(基于 NIO 的 Reactor + 工程化) - 多路复用(IO Multiplexing) - 核心:一个线程可以等待多个 fd 的事件 - 没有多路复用:要等待某个 socket 可读,只能在这个 socket 上阻塞 read - 有多路复用:线程阻塞在 selector/poll/epoll_wait 上,内核告诉你那些 fd 可读/可写,再去处理 - Java NIO 的 Selector.select()=在做等事件,底层映射到 OS 的 select/poll/epoll/kqueue - Reactor 模式(NIO 常见线程模型) - Reactor:事件循环(event loop) + 分发(dispatch) - 流程 - selector.select() 事件 - 拿到 selected keys - 根据事件类型调用对于 handler - 单 Reactor 单线程:简单,适合低负载 - 主从 Reactor(boss/worker):boss 负责 accept,worker 负责读写 Java BIO# - Java BIO - IO 流体系 - 两大抽象 - 字节流:InputStream/OutputStream - 面向原始字节 - 字符流:Reader/Writer - 面向字符,内部会涉及字符集解码/编码 - 涉及文本语义/字符集,就优先用 Reader/Writer 或者用 InputStream + 明确 Charset 解码;处理二进制就用 InputStream/OutputStream - 节点流 vs 处理流 - 节点流:直接连接数据源/目的地 - FileInputStream、FileOutputStream、Socket.getInputStream - 处理流:包装节点流,增强能力(缓冲、转码、数据类型、对象序列化、压缩、校验) - BufferedInputStream、InputStreamReader、DataInputStream、ObjectInputStream - 装饰器模式:不改原类,通过包装叠加功能 - Buffered 缓冲为什么快 - 不加缓冲为什么慢 - 许多 read()/write() 最终都会调用 OS 层的 read(2)/write(2) 系统调用,系统开销不小 - 不缓冲:每读 1KB 就要系统调用一次,次数巨大 - 加缓冲:一次系统调用读 8KB/64KB 放到用户态缓冲区,后续小读都在内存中完成,降低系统调用次数 - BufferedInputStream/BufferedOutputStream - BufferedInputStream:内部有 byte[] buffer(默认 8KB),先批量从底层读,再按需给你 - BufferedOutputStream:先写入 buffer,满了再 flush 到底层,减少了 write 的系统调用次数 - flush 是什么 - 把用户缓冲区里的数据推下去 - flush() != 一定落盘 - 字符集与桥接流 - InputStreamReader/OutputStreamWriter - InputStreamReader:字节 -> 字符(解码),需要 Charset - OutputStreamWriter:字符 -> 字节(编码),需要 Charset - 典型用法 - 读文本 - BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(f), UTF_8)) - 写文本 - BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f), UTF_8)) - 为什么不能随便 new String(byte[]) - 不指定 charset,会走平台默认编码,导致乱码 - 常见功能处理流 - DataInputStream/DataOutputStream(理解二进制协议) - 能按固定类型读写:writeInt/readInt,writeLong/readLong - 用途:自定义二进制协议、文件格式 - DataInputStream 读写的是 Java 规定格式,跨语言要慎重,更常见的跨语言协议是 protobuf、msgpack 等 - ObjectInputStream/ObjectOutputStream - Java 原生序列化:实现 Serializable - 问题:性能一般、版本兼容差、安全风险 - 不建议用于 RPC/外部输入,内部常见简单可用,生产用 protobuf/kryo/json 等 - PrintWriter/PrintStream(输出遍历但注意编码/flush) - System.out 是 PrintStream - PrintWriter 可选 autoFlush(遇到 println/printf/format 自动 flush) - PrintWriter 默认吞异常 - 资源管理与异常 - try-with-resources - 自动 close,且 close 顺序与声明顺序相反 - close 过程中异常会作为 suppressed 异常附加到主屏幕 - close vs flush - close vs flush - close() 会隐式 flush(),不能依赖进程崩溃前的 close - 网络场景:写完重要数据要主动 flush 或者使用协议层 ack。 - BIO 与网络 IO:Socket 阻塞读写的关键点 - 阻塞读到集中返回情况 - InputStream.read(byte[]) - 返回 >0:读到的字节数 - 返回 -1:对端关闭输出 - 可能一直阻塞:对端不发数据也不关 - 半包/粘包在 BIO 也存在 - BIO 只是阻塞,TCP 仍然是字节流,read() 不保证一条业务消息完整到达 NIO 基础 Buffer/Channel# - NIO 基础 - NIO 的核心心智模型:Buffer+Channel - 在 NIO 中,数据永远在 Buffer 里转 - 读:Channel.read(ByteBuffer) 把数据写进 buffer - 写:Channel.write(ByteBuffer) 从 buffer 读出数据写到 channel - Channel 是管道,Buffer 是货箱,NIO 必须管理 buffer 状态 - ByteBuffer 属性与状态 - 关键属性 - capacity:容量,创建后固定 - position:当前位置(下次读/写从这里开始) - limit:边界(读/写不能超过它) - mark:标记位置(可选) - remaing():limit - position - 两种模式 - 写模式:准备往 buffer 里放数据(例如从 channel read 进来) - position 向后走,limit 通常等于 capacity - 读模式:准备从 buffer 里取数据(例如写到 channel 或解析协议) - position 向后走,limit 表示可读数据的末尾 - 三个最重要的方法 - flip:写模式 -> 读模式 - 把 limit 设置为当前 position,再把 position 设置为 0 - 我写完了,现在从头读刚写的数据 - clear:读完后重置为写模式(不清数据,只重置指针) - position = 0; limit = capacity; mark = -1 - 不关心旧数据了,直接当成空 buffer 继续写 - compact:读了一部分,保留剩余未读数据,并切换为写模式 - 把未读数据 [position,limit) 移动到开头 - position=remaining;limit=capacity - 我还剩一点没处理,先挪到前面,下次继续在后面写新数据 - HeapBuffer vs DirectBuffer - Heap ByteBuffer(堆内) - ByteBuffer.allocate(n) - 好处:分配快,受 JVM GC 管理,排查简单 - 坏处:做网络 IO 时需要把数据从堆复制到内核缓冲区 - Direct ByteBuffer(堆外) - ByteBuffer.allocate(n) - 好处:减少一次拷贝,适合大块网络 IO,频繁 IO。 - 坏处 - 分配/释放更贵 - 受 MaxDirectMemorySize 限制 - 回收依赖 Cleaner/GC 时机,不是=null 就立刻释放 - Channel:FileChannel 与 SocketChannel - FileChannel - 来源 - FileInputStream.getChannel() - FileOutputStream.getChannel() - 常用能力 - read(ByteBuffer)/write(ByteBuffer) - position()/position(long):随机读写位置 - size() 文件大小 - force(boolean metaData):把修改刷到存储设备 - transferTo/transferFrom:为后续零拷贝铺垫 - SocketChannel - 支持 configureBlocking(false) NIO 非阻塞基石 - NIO 文件读写的标准流程 - FileChannel 读取 - buffer.clear() 进入写模式 - int n = channel.read(buffer) 把数据写进 buffer - buffer.flip() 进入读模式 - 从 buffer 读取处理 - 循环直到 n == -1 NIO 进阶 Selector/多路复用# - NIO 进阶 Selector/多路复用 - Selector/SelectionKey/Channel:三者关系与生命周期 - 三件套职责 - Selector:事件复用器,阻塞在 select() 等待就绪事件,把就绪的 key 放进 selectedKeys 集合 - SelectableChannel:可注册到 Selector 的通道 - SelectionKey:一次注册的凭证/句柄,包含 - channel():对于 channel - selector():归属 selector - interestOps():你关心那些事件 - readyOps():当前就绪了哪些事件 - attachment():绑定业务状态 - 注册与事件的基本规则 - channel.register(selector, OP_READ, attachment):把 channel 注册给 selector,并声明兴趣事件 - interestOps 是订阅,可以动态改 - readyOps 是本轮 select 的结果,不能直接改 - key 的取消与资源释放 - key.cancel():从 selector 的 key 集合中移除 - 移动要同时 close channel:channel.close(),否则 fd 泄露 - 被 cancel 的 key 不会立刻从所有集合消失,通常在下一次 select 时清理 - 多路复用:就绪事件语义 - 就绪不是完成 - OP_READ 就绪:表示现在读不阻塞,不保证读到一条完整的业务消息 - OP_WRITE 就绪:表示现在写更可能成功,不保证一次 write 写完你要写的所有数据 - 各个事件含义 - OP_ACCEPT:ServerSocketChannel 有新连接可以 accept - OP_CONNECT:客户端连接建立过程完成(非阻塞 connect) - OP_READ:SocketChannel 可读 - OP_WRITE:SocketChannel 可写(注意:很多时候“总是可写”) - Reactor 模式:从单线程到主从 Reactor - 单线程 Reactor - 一个线程做三件事 - selector.select() 等待事件 - 遍历 selectedKeys - 按事件类型分发处理:accept/read/write - 优点:简单 - 缺点:业务处理慢会阻塞整个事件循环 - 主从 Reactor(boss/worker) - boss:只负责 accept,把新 SocketChannel 分发到 worker 的 selector - worker:负责读写与协议解析、业务派发 Netty# - Netty - 解决什么问题 - 原生 NIO 的痛点 - 需要自己管理:Selector 轮询、Key 生命周期、半包/粘包、写不完与 OP_WRITE 开关 - Buffer 管理复杂:flip/compact、输入输出缓冲复用、DirectBuffer 回收时机 - 性能与稳定性:Selector 空轮询、内存膨胀、背压、线程隔离 - Netty 提供的工程化能力 - 线程模型(EventLoopGroup + Reactor)成熟 - Pipeline/Handler 体系:解码、业务、编码职责分离 - ByteBuf:读写指针友好、池化、零拷贝视图、引用计数 - 编解码器、背压、写队列、水位线、内存泄露检测等工具链 - 线程模型 - EventLoop - EventLoop = 一个线程 + 一个 Selector + 一组 Channel - EventLoop 负责 - 处理 IO 事件 - 执行任务队列 - 同一个 Channel 的所有 Handler 回调默认都在通一个 EventLoop 线程执行,一致性,减少锁 - EventLoopGroup - bossGroup:负责 accept(ServerSocketChannel) - workerGroup:负责已建立连接的读写(SocketChannel) - 为什么少量线程能抗大量连接 - 事件驱动:线程主要阻塞在 epoll/kqueue 上,事件就绪才处理 - 连接多不会导致线程线性增长 - Channel、Future、Promise:异步编程语义 - ChannelFuture - Netty 大量 API 返回 ChannelFuture,比如 bind、connect、writeAndFlush,表示操作已发起,完成会通知 - future.addListener(...):完成回调(推荐) - future.sync():阻塞等待 - Promise - 可写的 future,用于自己控制完成/失败 - Pipeline/Handler:职责分离 - Pipeline 是什么 - 每个 Channel 都有一条 ChannelPipeline - Inbound(入站):数据从 socket 进来 -> 解码 -> 业务处理 - Outbound(出站):业务写出 -> 编码 -> flush 到 socket - Handler 分类 - ChannelInboundHandler:处理入站事件(channelRead、channelActive…) - ChannelOutboundHandler:处理出站事件(write、flush…) - ChannelDuplexHandler:双向 - SimpleChannelInboundHandler<T>:自动释放入站消息(配合 ByteBuf 引用计数要理解) - ByteBuf - 为什么 ByteBuf 比 ByteBuffer 友好 - 两个指针:readerIndex/writerIndex - 不需要 flip:写完读直接通过读指针移动 - 支持切片/复制语义清晰 - 三种常见 ByteBuf - Heap:堆内 - Direct:堆外 - CompositeByteBuf:多个 buffer 组合成一个逻辑 buffer(减少拷贝) select/poll/epoll# - select/poll/epoll - 简介 - 都是操作系统提供的 I/O 多路复用机制 - 让一个线程同时等待很多文件描述符的可读/可写/异常事件,然后再去对就绪的 fd 做 read/write - 术语 - fd:内核里对打开的文件/套接字等资源的编号。网络 socket 也是 fd。 - 就绪:表示现在读/写大概率不会阻塞,不等于你一次就能读到完整业务包/一次写完所有数据 - select:最老牌的多路复用 - 工作方式:把三组 fd(读/写/异常) 集合传给内核,内核扫描那些就绪,然后返回就绪集合。 - 特点: - fd 数量上限:select 受到 FD_SETSIZE 限制,常见 1024 - 每次调用要拷贝与扫描:用户态要把位图/集合传入内核,内核要线性扫描 - 集合会被修改:返回时就绪集合是被改过的,下次调用要重新准备集合 - 适用: - fd 少,跨平台,简单场景。 - poll:对 select 的数组版改良 - 工作方式:把 fd 列表放在一个 pollfd[] 数组传递给内核,内核扫描并在数组里标记那些就绪。 - 相比 select 改进 - 不收 FD_SETSIZE 位图限制 - 表达更灵活 - 缺点 - 仍然是 O(n) 扫描 - 每次调用仍然需要把数组从用户态传给内核 - epoll:为海量连接而生 - epoll 是 Linux 的高性能事件通知机制,核心思想是:把关注列表留在内核里,避免每次把大集合传来传去,返回就绪列表而不是让自己全量扫描 - 关键点 - 关注列表在内核维护:避免每轮 select/poll 大集合拷贝+扫描 - epoll_wait 返回的是就绪事件列表,只处理这些 fd,更接近 O(就绪数) - LT vs ET 触发模式 - LT (Level-Triggered 水平触发,默认) - 只要还可读/可写,就会持续通知你 - 编程更简单:读不完下次还会通知你 - ET (Edge-Triggered,边缘触发) - 只有从不可读 -> 可读的边缘变化才通知一次 - 必须把数据一次性读到不能再读,否则可能丢通知导致卡住 - 性能更高,但对实现要求更严格(非阻塞+循环读/写)