当前位置: 首页 > news >正文

Day58 | Java IO模块概览

今天我们进入一个新的模块,Java中的IO模块。

先来看看IO模块中都有哪些内容:

 

看着多,但是实际上可以分成三块内容:BIO、NIO、AIO。

这三块内容也是Java中IO模式的一个演进路线。

本文我们还是不讲具体的API,我们先理清楚一些基础的概念,看看Java的IO模块的演进历史。

一、什么是I/O

这里的I、O分别代表的是Input和Output,也就是输入和输出。

要搞清楚I/O,我们就要先确定一个中心或者边界。在编程的世界里,这个中心就是内存。

我们的程序、代码、运行时的数据都存放在内存里。

所以I/O实际上就是内存和外部设备之间的数据交换。

输入代表数据从外部流向内存,比如我们把磁盘上的文件读取到内存上、接收网络上的数据到内存或者从键盘获取输入到内存。

输出则意味着数据从内存流向外部,我们把内存里的数据写到文件里、通过网络发送数据出去或者把内容输出到显示器上都是输出。

这里的外部是一个相对的概念,泛指内存之外的一切。比如我们提到的磁盘、网卡、键盘、显示器,甚至是另一台服务器或另一个程序。

 

再看一些关于IO的基础概念。

阻塞和非阻塞IO这两个概念是程序级别的。

阻塞IO就是程序发起IO请求后,如果资源没准备好,发起调用的线程会被挂起(阻塞),直到IO操作完成。

非阻塞IO就是程序发起IO请求后,如果资源没准备好,调用会马上返回一个状态(比如错误码),线程不会被阻塞,可以继续执行。程序需要通过轮询的方式,反复检查IO有没有准备好。

同步IO和非同步IO这两个概念是操作系统级别的。

操作系统在完成整个IO操作(比如数据从内核空间复制到用户空间)之前,不会给应用程序最终的完成信号。应用程序(或其线程)在这期间需要主动等待结果。这就是同步IO。

而非同步IO也叫异步IO,在操作系统收到请求后,会立刻返回一个标记(表示请求收到了),应用程序可以马上处理其他事情。操作系统会在后台独立完成整个IO操作,当数据真正准备好后,再通过事件机制(比如回调函数或通知)来告诉应用程序。

二、I/O是怎么工作的

当我们在代码里写下读写文件的时候,我们的代码并不是直接操作了磁盘。

如果每个应用程序都能直接操作硬盘、网卡这些硬件,那我们所有的信息都相当于在裸奔了。

操作系统为了安全和稳定,设计了一套权限隔离机制,把内存分成了用户空间和内核空间。

我们编写的所有应用程序,包括JVM,都只能访问属于自己的那部分内存,没办法直接操作硬件。

内核空间是操作系统的核心代码访问的,他可以直接跟硬件(CPU、内存、磁盘、网卡)打交道。

如果我们的Java程序需要读取一个文件,他不能直接去磁盘拿数据,他只能跟内核发一个申请,这就是系统调用。

系统调用说白了就是操作系统内核预先定义好的一些函数,他是用户态程序请求内核服务的唯一合法接口。

我们的程序执行read(), write(), connect()这些操作的时候,实际上就是在发起系统调用。

当我们发起了系统调用,内核就要开始工作了,这个时候,CPU需要从执行我们的程序代码,转去执行内核的代码。这个转换过程,就是我们经常说上下文切换。

什么是上下文呢?

可以理解成CPU执行任务时所需要的现场记录,包括程序计数器、寄存器状态、内存映射等。

CPU需要先保存当前程序的现场(用户态上下文),然后加载内核的现场(内核态上下文),执行完内核任务后,再恢复我们的程序现场。

传统的I/O工作流程:

 

三、BIO

BIO是Java中JDK1.4之前唯一的I/O 模型。中文叫同步阻塞I/O。

他是最老最简单,也是最符合我们直觉的模型。最大的特点就是阻塞。

 

还记得上一节传统I/O工作流程图吧。BIO的阻塞主要体现在两个点:

第一个就是发起read()时阻塞,我们的Java程序调用read,发起系统调用之后,Java线程会马上被挂起。

他必须一直等着,直到内核完成了第5步(把数据从内核缓冲区拷贝到用户缓冲区)和第7步(切换回用户态),read() 方法才会返回。

在这整个等待过程中(等待磁盘、等待网络),这个线程干不了其他的。

还有一个点是发起accept()的时候会阻塞,对于服务器端,ServerSocket.accept() 方法会一直阻塞,直到有一个新的客户端连接进来,才会返回。

这个时候我们可能会有疑问。accept()和read()都会阻塞,那服务器是怎么处理成百上千的客户端的请求的?

如果服务器只有一个线程,那么他在accept()一个连接后,去调用read(),这个线程就会被这个客户端占着。他在read()的整个过程中,都没办法返回去调用accept()来接收新的连接。

BIO服务器就采用了一个请求一个线程的模式,这种模式的工作流程大概是这样的:

1.服务器启动一个主线程,这个主线程在一个无限循环里,专门调用ServerSocket.accept()来等待客户端连接。

2.当accept()方法返回一个Socket(代表一个新连接)时,主线程并不会自己去处理这个连接上的I/O(比如read()或write())。

3.主线程会马上创建一个全新的线程,把这个Socket交给新线程去处理。

4.这个新创建的业务线程会负责这个客户端的所有通信。他会调用socket.read(),这个调用是阻塞的,但这没关系,因为他只阻塞了这一个业务线程,不会影响主线程。

5.主线程在创建并启动了新线程后,会马上回到循环的开头,再次调用accept(),准备接收下一个客户端连接。

 

这个模型相当于是甩锅给了新线程,来解决accept()和read()的阻塞冲突。

如果在高并发的场景下,就不适用了。

在程序里,线程本身就是比较昂贵的资源,Java里,每个线程都要占用一些内存作为线程栈。如果线程太多了,上万个,线程栈的耗费对于服务器来说就很多。

就算内存不值钱,堆了足够大的内存,CPU也没办法同时运行这些线程。操作系统必须在这些线程之间快速切换,这个过程就是我们前面提到的上下文切换。

当线程数量(尤其是大量处于休眠和等待I/O的线程)非常多的时候,CPU会花费大量的时间在保存和恢复线程的现场记录上,这些又不是真正的业务代码逻辑,就会直接导致性能下降。

还有就是很多情况下,这些线程其实都是空闲的,他们只是在read()那里阻塞着,等待客户端发数据。这些线程占着资源不放,又不干活,单纯的浪费。

既然那么多问题,大家肯定第一个想到了线程池这个东西,可以很好的复用线程,不至于造成那么多的浪费。

我们确实可以创建一个固定大小的线程池,当accept()获取一个新Socket后,他会把这个Socket封装成一个任务(Task),提交给线程池去执行。

线程被复用了,避免了频繁创建和销毁线程的开销。

线程池的大小限制了服务器同时处理的最大连接数,防止服务器由于线程太多而崩溃。

但是本质上没有解决根本的问题。

线程池只是把瓶颈从无限创建线程导致内存耗尽转移到了线程池大小。

如果线程池大小是200,那服务器只能同时处理200个客户端的read()。

当第201个客户端连接进来的时候,他的任务会被提交到线程池的等待队列里。

这个客户端必须等待,直到前面有一个客户端完成了通信(或者断开连接),释放了一个线程,才能被处理。

所以,线程池模式下的BIO,还是同步阻塞的。他只是用一个队列削峰填谷,提高了服务器的存活率,并没有提高I/O的吞吐量和伸缩性。

BIO模型的根本问题在于他的阻塞特性,导致一个线程在同一时间只能处理一个连接的I/O。这在硬件资源(主要是内存)和软件资源(CPU上下文切换)上都开销巨大。

四、NIO

NIO是从Java1.4开始引入的。

NIO有两种解读:官方全称是New I/O,因为他相对于旧的java.io包是全新的。

但我们更多的时候叫它Non-blocking I/O(非阻塞I/O),因为这是他最核心的特性。

NIO的核心目标就是解决BIO的两个阻塞点,accept()的阻塞和read()/write()的阻塞。

为了解决这个问题,NIO引入了三个核心的组件:Channel(通道)、Buffer(缓冲区) 和Selector(选择器)。

我举两个例子说明下BIO和NIO的区别。

BIO模式下,就好比银行有100个窗口(相当于线程池里的100个线程)。 每个人(客户端连接)来银行,就必须找到一个空闲的窗口坐下。 如果100个窗口都有人,新来的人就只能在排队等着(线程池满了,连接进入等待队列)。 就算坐在窗口的人也只能发呆(连接建立了,但客户端没发数据,read阻塞),这个窗口也被占用了,别人用不了。

NIO模式下,搞了1个综合服务大厅。 所有人来了之后,不去抢窗口,而是都进入这个大厅。 大厅里有一个大堂经理(NIO里的Selector线程),他手里有个小本子。 每个进来的人,大堂经理都会问他:你要办什么业务?是存钱、取钱,还是开户?然后记在小本子上。 大堂经理的任务就是不停地巡视大厅里的所有人(轮询): 谁的钱准备好了要存?” -> A举手 -> 大堂经理把客户A带去一个空闲的柜台处理。 谁要取钱? -> B举手 -> 大堂经理把客户B带去处理。 如果大厅里1000个人都在发呆(没数据交互),大堂经理看了一圈发现没人举手,他就坐那儿歇会儿,而不是像以前那样,非要派1000个工作人员一对一盯着他们。

好,我们回到NIO的三个组件上。

4.1 Channel

在BIO里,我们都是面向流编程,比如InputStream和OutputStream。流是单向的,要么读,要么写。

在NIO里,我们面向Channel编程。通道是双向的,可以从通道读取数据,也可以往通道写数据。 常见的 Channel有SocketChannel、ServerSocketChannel、FileChannel。

4.2 Buffer

Buffer是一个内存块。在BIO里,我们直接从Stream里读字节。在NIO里,所有数据的读写都必须通过Buffer。

读数据的时候,从Channel读取数据 -> 写入Buffer。

写数据的时候,从Buffer读取数据 -> 写入Channel。 Channel有点像铁路,Buffer像火车。数据(货物)不能直接扔在铁轨上,必须装在火车(Buffer)里才能在铁路(Channel)上传输。

最常用的Buffer是ByteBuffer,提供了很多方法来给我们操作这个内存块(put(), get(), flip(), clear() 等)。

4.3 Selector

这是最核心的组件,是NIO实现非阻塞的关键。我们也叫他多路复用器。就是上面我们说的大堂经理。

一个Selector可以同时管理多个Channel,这就叫多路。他会不停的轮询注册在他小本子上的所有Channel。

我们只需要一个线程,就可以管理成千上万个连接。只有当连接真正有事件发生的时候,线程才会去处理他,减少了线程的空转和上下文切换。

我们通过流程图看一下NIO的工作模式:

 

相比起BIO的工作流程,确实NIO要复杂多了。

BIO里一个while(read),逻辑都在一个循环里,非常简单。

NIO把一个简单的I/O操作拆得非常的零碎,用起来还是有很大的心智负担。

五、AIO

在NIO里,我们通过Selector避免了accept()和read()的阻塞,但是我们还是需要主动去调用selector.select()来判断谁准备好了。当Selector告诉我们有谁准备好了,我们的线程需要亲自去执行socketChannel.read(),然后把数据从内核缓冲区搬到用户缓冲区。

这个时候我们就会想,我们连大堂经理都不想当了,只当个甩手掌柜。 我们告诉操作系统:你去帮我从张三那里读数据,读完之后,直接把数据放到我们的Buffer里。全部搞定之后,再来告诉我。

这种模式其实就是AIO,也就是异步非阻塞I/O。

AIO是在JDK1.7里被引入的,也被称为NIO.2(因为他在java.nio包里面提供了新的Asynchronous...开头的API)。

 

AIO是操作系统级别的异步。

我们的发起一个read()操作,传入一个Buffer。

调用会立即返回,我们的线程可以马上去干别的事情。

操作系统(内核)接管一切。他会等待数据到达,然后自动把数据从内核空间拷贝到我们指定的用户空间Buffer里。

当所有操作(包括数据拷贝)都完成之后,操作系统才会通知我们:数据已经放进Buffer了。

 

AIO有两种通知方式,Future和CompletionHandler。

我们发起一个read操作,立刻返回一个Future<Integer>对象。我们的线程就可以去做其他事情了,等我们想起来了,就去调用future.get(),如果操作系统还没处理完,get方法就会阻塞,如果已经出来完了,get方法就能直接拿到结果。这就是Future方式。

而CompletionHandler方式是在我们发起read的时候,注册一个回调函数(CompletionHandler)。然后我们的线程就完全不管了。等操作洗脱处理完,他会自动调我们注册的回调函数。

虽然AIO是非阻塞的,又是异步的,回调模式的API也比NIO要简单很多。

但是AIO在Java服务器端几乎没有被使用。

因为在Windows上,AIO的底层实现是IOCP,这是个真异步I/O模型。

但是在Linux上,AIO的底层实现是用NIO的Selector(epoll)模拟的,所以在Linux上,JDK的AIO只是一个马甲。他在底层创建了一个线程池,然后在池子里跑NIO的Selector。当我们发起一个AIO的read,他只是把这个请求丢给那个NIO线程池去select()和read(),完成之后再回调我们。

而大多数的Java应用都是跑在Linux服务器上的,所以AIO并没有被广泛的使用。

结语

本文从Java I/O的演进路线,大致的分析了从传统的BIO演进到NIO,再后来的AIO。

作为IO模块的开篇,理清这些核心概念和演进路线,对于后续的学习,比如NIO的Buffer、Channel或者Netty都大有帮助。

下一篇预告

待定

如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!

http://www.dtcms.com/a/594400.html

相关文章:

  • 新闻联播(2025年11月10日总第二期)
  • 学校后勤网站建设方案wordpress 优惠卷
  • 合肥义城建设集团有限公司网站四川省住房城乡建设厅网站
  • 青岛网站制作seo建设网站需要服务器
  • 企业级 ERP 安全隐患全景:接口未鉴权、默认配置与远程执行的系统性剖析
  • 做视频的素材网站阿里云 域名申请
  • 自己建设网站容易吗哪个网站做图片外链
  • 分布式专题——50 电商项目仿京东商品搜索服务实战
  • 第三方应用软件提权之symantic pcanywhere提权
  • 科普:LLM领域中的“样本(sample)”、“指令(instruction)”和“提示词(prompt)”
  • 宁波网站运营优化系统推广营销方案
  • 【WIP】大模型运维中GPU机器介绍
  • 在家没事做建什么网站好joomla 网站建设教程
  • explorer.exe源代码分析之热键的注册和处理
  • 免费做网站通栏广告做企业网站哪家好
  • 后端开发CRUD实现
  • 4.忘记密码页测试用例
  • 怎么建设个网站做网站用啥软件
  • 凡科可以做淘宝客网站吗上海企业登记在线电子签名
  • 网站关键词优化代理山东临沂市需要建设网站的公司
  • Hello-Agents task1 智能体与语言模型基础
  • 做宣传手册的网站智慧团建网站登录忘记密码
  • 山西省建设监理协会官方网站外链代发免费
  • 区间|单调栈
  • 基于Springboot的电器商城管理系统
  • 做摄影网站的目的是什么意思wordpress创建角色
  • 公司网站设计开发公司注册域名阿里云
  • 强化学习3 Q-learning
  • 惠州制作公司网站pythone网站开发
  • 企业只有建立自己的网站平台广西麒铭建设有限公司网站