概念解析
Stream、Channel
1、Stream 不会自动缓冲数据,Channel 利用系统提供的发送缓冲区、接收缓冲区(更底层)
2、Stream 仅支持阻塞 API,Channel 同时支持阻塞、非阻塞 API,网络 Channel 可配合 Selector 实现多路复用
3、二者均为全双工,即读、写可以同时进行,Stream 是单向流动,但它也是全双工
IO 模型
1、当调用一次 Channel.read 或 Stream.read 后,会由用户态切换至操作系统内核态,完成真正数据读取
2、读取分为两个阶段
(1)等待数据阶段
(2)复制数据阶段
阻塞 IO
1、用户线程进行 read 操作时,需要等待操作系统执行实际 read 操作,期间用户线程被阻塞,无法执行其他操作
非阻塞 IO
1、用户线程在一个循环中,一直调用 read 方法,若内核空间中还没有数据可读,立即返回
2、只在等待阶段非阻塞
3、用户线程发现内核空间中有数据后,等待内核空间执行复制数据,待复制结束后返回结果
多路复用
1、Java 通过 Selector 实现多路复用
(1)当没有事件时,调用 select 方法会被阻塞
(2)一旦有一个或多个事件发生后,就会处理对应的事件,从而实现多路复用
2、区分
(1)多线程 + 阻塞 IO:每个 Socket 对应一个线程,占用较多资源
(2)多路复用:一个线程就可以管理多个 Socket,只有当 Socket 真正有事件发生,才会占用资源进行实际操作
(3)非阻塞 IO:通过用户线程不断询问 Socket 状态
(4)多路复用 IO:在内核中轮询每个 Socket 状态
3、事项
(1)多路复用通过轮询方式检测是否有事件发生,并且对到达的事件逐一进行响应,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询
(2)多路复用并不是非阻塞,只是它可以同时处理多个连接,单路是一个处理完才能接入新的连接,多路复用就是可以同时接入多个连接,只要新连接准备好就可以返回,然后会记录所有连接的信息,处理完返回响应
(3)多路复用是使用一个线程来检查多个文件描述符(Socket)就绪状态,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时,得到就绪状态后,操作可以在同一个线程里执行,也可以启动线程执行(如:线程池)
信号驱动
1、当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据
2、信号类似于硬件上使用的中断,只不过信号是软件层面上的,可理解为软件层次上对中断的一种模拟,驱动通过主动向应用程序发送可访问的信号,应用程序获取到信号后,即可从驱动设备中读取或写入数据
异步 IO
1、线程 1 调用方法后立即返回,不会被阻塞,也不需要立即获取结果
2、当方法的运行结果出后,由线程 2 将结果返回给线程 1
五种 IO 模型
零拷贝
1、数据无需拷贝到 JVM 内存中
2、优点
(1)更少的用户态与内核态的切换
(2)不利用 CPU 计算,减少 CPU 缓存伪共享
(3)适合小文件传输
传统 IO 内部工作流
1、Java 本身并不具备 IO 读写能力,需要从 Java 程序的用户态切换至内核态
2、调用 read 方法
(1)调用操作系统(Kernel)读,用户态切换至内核态,将数据读入内核缓冲区
(2)期间用户线程阻塞
(3)操作系统使用 DMA(Direct Memory Access)实现文件读,期间也不会使用 CPU
(4)DMA 可以理解为硬件单元,用来解放 CPU 完成文件 IO
(5)从内核态切换回用户态,将数据从内核缓冲区,读入用户缓冲区,期间 CPU 参与拷贝,无法利用 DMA
3、调用 write 方法
(1)将数据从用户缓冲区,写入 Socket 缓冲区,CPU 参与拷贝
(2)向网卡写数据,从用户态切换至内核态,调用操作系统的写,使用 DMA 将 Socket 缓冲区的数据写入网卡,不会使用 CPU
4、问题
(1)Java 的 IO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的
(2)用户态与内核态的切换发生了 3 次,这个操作比较重量级
(3)数据共拷贝 4 次
NIO 优化
1、通过 DirectByteBuf
(1)ByteBuffer.allocate:底层对应 HeapByteBuffer,使用的还是 Java 内存
(2)ByteBuffer.allocateDirect:底层对应 DirectByteBuffer,使用的是操作系统内存
2、Java 可以使用 DirectByteBuffer,将堆外内存映射到 JVM 内存中来直接访问使用
(1)堆外内存不受 JVM 垃圾回收的影响,因此内存地址固定,有助于 IO 读写
(2)Java 中的 DirectByteBuf 对象仅维护此内存的虚引用
3、内存回收
(1)DirectByteBuffer 对象被垃圾回收,将虚引用加入引用队列
(2)当引用的对象 ByteBuffer 被垃圾回收以后,虚引用对象 Cleaner 就会被放入引用队列中,然后调用 Cleaner 的 clean 方法来释放直接内存
(3)DirectByteBuffer 释放底层,调用 Unsafe 的 freeMemory 方法
(4)通过专门线程访问引用队列,根据虚引用释放堆外内存
4、减少一次数据拷贝,用户态与内核态的切换次数没有减少
零拷贝优化 1
1、底层:Linux 2.1 提供的 sendFile 方法
2、Java 中对应着两个 Channel 调用 transferTo / transferFrom 方法拷贝数据
(1)Java 调用 transferTo 方法后,要从 Java 程序的用户态切换至内核态,使用 DMA 将数据读入内核缓冲区,不会使用 CPU
(2)数据从内核缓冲区传输到 Socket 缓冲区,CPU 参与拷贝
(2)最后使用 DMA,将 Socket 缓冲区的数据写入网卡,不会使用 CPU
3、性能
(1)只发生 1 次用户态与内核态的切换
(2)数据拷贝 3 次
零拷贝优化 2
1、Linux 2.4 对 sendFile 优化
2、过程
(1)Java 调用 transferTo 方法后,从 Java 程序的用户态切换至内核态,使用 DMA 将数据读入内核缓冲区,不会使用 CPU
(2)只会将一些 offset 和 length 信息拷入 Socket 缓冲区,几乎无消耗
(3)使用 DMA 将 内核缓冲区的数据写入网卡,不会使用 CPU
3、性能
(1)只发生 1 次用户态与内核态的切换
(2)数据拷贝 2 次
AIO
1、异步非阻塞
2、解决数据复制阶段的阻塞问题
(1)同步:在进行读写操作时,线程需要等待结果,相当于闲置
(2)异步:在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式,由另外的线程来获得结果
3、异步模型需要底层操作系统(Kernel)提供支持
(1)Windows 系统通过 IOCP 实现真正的异步 IO
(2)Linux 系统异步 IO 在 2.6 版本引入,但底层实现还是使用多路复用模拟异步 IO,性能没有优势
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战