BufferedInputStream 源码学习笔记

简介: BufferedInputStream是一个带有内存缓冲的InputStream. BufferedInputStream是继承自FilterInputStream。 FilterInputStream继承自InputStream属于输入流中的链接流,同时引用了InputStream,将In

BufferedInputStream是一个带有内存缓冲的InputStream.


BufferedInputStream是继承自FilterInputStream。

FilterInputStream继承自InputStream属于输入流中的链接流,同时引用了InputStream,将InputStream封装成一个内部变量,同时构造方法上需要传入一个InputStream。这是一个典型的装饰器模式,他的任何子类都可以对一个继承自InputStream的原始流或其他链接流进行装饰,如我们常用的使用BufferedInputStream对FileInputStream进行装饰,使普通的文件输入流具备了内存缓存的功能,通过内存缓冲减少磁盘io次数。

Java代码
  1. protected volatile InputStream in;  
    protected FilterInputStream(InputStream in) {  
        this.in = in;  
    }  


注意:成员变量in使用了volatile关键字修饰,保障了该成员变量多线程情况下的可见性。

2.内存缓冲的实现

概要的了解完BufferedInputStream的继承关系,接下来详细理解BufferedInputStream是如何实现内存缓冲。既是内存缓冲,就涉及到内存的分配,管理以及如何实现缓冲。

通过构造方法可以看到:初始化了一个byte数组作为内存缓冲区,大小可以由构造方法中的参数指定,也可以是默认的大小。

Java代码

  1. protected volatile byte buf[];  
    private static int defaultBufferSize = 8192;  
    public BufferedInputStream(InputStream in, int size) {  
        super(in);  
        if (size <= 0) {  
            throw new IllegalArgumentException("Buffer size <= 0");  
        }  
        buf = new byte[size];  
    }  
    public BufferedInputStream(InputStream in) {  
        this(in, defaultBufferSize);  
    }  


看完构造函数,大概可以了解其实现原理:通过初始化分配一个byte数组,一次性从输入字节流中读取多个字节的数据放入byte数组,程序读取部分字节的时候直接从byte数组中获取,直到内存中的数据用完再重新从流中读取新的字节。那么从api文档中我们可以了解到BufferedStream大概具备如下的功能

从api可以了解到BufferedInputStream除了使用一个byte数组做缓冲外还具备打标记,重置当前位置到标记的位置重新读取数据,忽略掉n个数据。这些功能都涉及到缓冲内存的管理,首先看下相关的几个成员变量:

Java代码

  1. protected int count;  
    protected int pos;  
    protected int markpos = -1;  
    protected int marklimit;  


count表示当前缓冲区内总共有多少有效数据;pos表示当前读取到的位置(即byte数组的当前下标,下次读取从该位置读取);markpos:打上标记的位置;marklimit:最多能mark的字节长度,也就是从mark位置到当前pos的最大长度。

从最简单的read()读取一个字节的方法开始看:

Java代码

  1. public synchronized int read() throws IOException {  
        if (pos >= count) {  
            fill();  
            if (pos >= count)  
            return -1;  
        }  
        return getBufIfOpen()[pos++] & 0xff;  
    }  


当pos>=count的时候也就是表示当前的byte中的数据为空或已经被读完,他调用了一个fill()方法,从字面理解就是填充的意思,实际上是从真正的输入流中读取一些新数据放入缓冲内存中,之后直到缓冲内存中的数据读完前都不会再从真正的流中读取数据。

看源码中的fill()方法有很大一段是关于markpos的处理,其处理过程大致为
a.没有markpos的情况很简单


b.有mark的情况比较复杂:


c.read()方法返回值


以上即为内存缓冲管理的完全过程,再回过头看read()方法,当缓冲byte数组中有数据可以读时,直接从数组中读取一个字节,但最后的read方法返回的却是int,而且还和0xff做了与运算。

Java代码
  1. return getBufIfOpen()[pos++] & 0xff;  

为什么不直接返回一个byte,而是一个与运算后的int。首先宏观的看InputStream和Reader两个输入流的抽象类都定义了read接口而且都返回int,一个是字节流,一个是字符流。我们知道字节用byte表示,字符用char表示。首先看java中基本类型的取值范围:

从取值范围来看int包含了char和byte,这为使用int作为返回值类型提供了可能。

在应用中我们一般用read()接口的返回值是-1则表示已经读到文件尾(EOF)。

char的取值范围本身不包含负数,所有用int的-1表示文件读完没问题,但byte的取值范围-128 ~ 127,包含了-1,读取的有效数据范围就是-128~127,没办法用这个取值范围中的任何一个数字表示异常或者数据已经读完,所以接口如果直接使用byte作为返回值不可行,直接将byte强制类型转换成int也不行,因为如果读到一个byte的-1,转为int了也是-1,会被理解为文件已经读完。所以这里做了一个特殊处理return getBufIfOpen()[pos++] & 0xff。

0xff是int类型,二进制为0000 0000 0000 0000 0000 0000 1111 1111。

上述的与运算实际上读取的byte先被强制转换成了int,例如byte的-1(最高位表示符号位,以补码的形式表示负数为:1111 1111)

转换为int之后的二进制1111 1111 1111 1111 1111 1111 1111 1111

& 0xff之后高位去0

最后返回的结果是0000 0000 0000 0000 0000 0000 1111 1111, 为int值为256

其-128~-1被转为int中128~256的正数表示。

这样解决了可以用-1表示文件已经读完。但关键是数据的值发生了变化,真正要用读取的数据时是否还能拿到原始的byte。还拿上面那个例子来看,当读取返回一个256时,将其强制类型转换为byte,(byte)256得到byte的-1,因为byte只有8位,当int的高位被丢弃后就只剩下1111 1111,在byte中高位的1表示符号位为负数,最终的结果即是byte的-1;同样byte的-128(1000 0000)被转为int的128(0000 0000 0000 0000 0000 0000 1000 0000),强制类型转换后还原byte的1000 0000。

4.线程安全

返回值中还有一个细节是getBufIfOpen()[pos++],直接将pos++来获取下一个未读取的数据,这里涉及到的两个元素:一个内存数组,一个当前读取的数据下标都是全局变量,pos++也不是线程安全。那么BufferedInputStream如何保证对内存缓冲数组的操作线程安全?源码中有操作的public方法除了close方法之外,其他方法上都加上了synchronized关键字,以保障上面描述的整个内存缓存数组的操作是线程安全的。但为什么close方法没有synchronized,我们看这个方法做了些什么事情:

Java代码

  1. byte[] buffer;  
    while ( (buffer = buf) != null) {  
        if (bufUpdater.compareAndSet(this, buffer, null)) {  
        InputStream input = in;  
        in = null;  
        if (input != null)  
            input.close();  
        return;  
        }  
        // Else retry in case a new buf was CASed in fill()  
    }  


简单来看做了两个操作:把内存数组置为null,将引用的inputStream置为null,同时将引用的inputStream.close();

这两个操作的核心都是关闭原始流,释放资源,如果加了synchronized关键字,会导致当前线程正在执行read方法,而且系统消耗很大时,想释放资源无法释放。此时read方法还没执行完,我们知道synchronized的锁是加在整个对象上的,所以close方法就必须等到read结束后才能执行,这样很明显不能满足close的需求,甚至会导致大量的io资源被阻塞不能关闭。

但该方法用一个while循环,而且只有当bufUpdater.compareAndSet(this, buffer, null)成功时,才执行上述的资源释放。

先看bufUpdater这个全局变量:

Java代码

  1. protected volatile byte buf[];  
    private static final   
            AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =   
            AtomicReferenceFieldUpdater.newUpdater  
            (BufferedInputStream.class,  byte[].class, "buf");  


AtomicReferenceFieldUpdater是一个抽象类,但该类的内部已经给出了包访问控制级别的一个实现AtomicReferenceFieldUpdaterImpl,原理是利用反射将一个 被声明成volatile 的属性通过JNI调用,使用cpu指令级的命令将一个变量进行更新,保障该操作是原子的。也就是通过上面定义的bufUpdater将buf这个byte数组的跟新变为原子操作,其作用是保障其原子更新。

BufferedInputStream源代码中总共有两个地方用到了这个bufUpdater,一个是我们上面看到的close方法中,另外一个是再前面说道的fill()方法中。既然BufferedInputStream的所有操作上都用了synchronized来做同步,那为什么这里还需要用这个原子更新器呢?带着问题上面提到过fill()方法中的最后一个步骤:当有mark,而且markLimit的长度又大于初始数组的长度时,需要对内存数组扩容,即创建一个尺寸更大的数组,将原来数组中的数据拷贝到新数组中,再将指向原数组的应用指向新的数组。bufUpdater正是用在了将原数组引用指向新数组的操作上,同样close的方法使用的bufUpdater也是用在对数组引用的改变上,这样看来就比较清晰了,主要是为了防止一个线程在执行close方法时,将buffer赋值为null这个时候另外一个线程正在执行fill()方法的最后一个步骤又将buffer赋值给了一个新的数组,从而导致资源没有释放掉。

5.结束

到这里BufferedInputStream的源码每个细节都已经分析完,看似简单的一些方法,返回值和调用中其实蕴藏着很多不简单的东西,通过阅读一些好的源代码可以学到不少东西。

相关文章
|
6月前
|
SQL 人工智能 关系型数据库
让慢SQL消失在提交前:Qoder × RDS AI助手Skill的实时拦截术
在AI Coding快节奏开发中,SQL质量常成盲区:测试难复现、人工Review低效、问题滞后暴露。RDS AI助手提供实时SQL智能审查,3分钟集成Qoder,覆盖正确性、性能、索引、可维护性等维度,将“事后救火”变为“事前预防”,让高质量SQL成为开发默认标准。
|
4月前
《深耕QClaw协作逻辑,构建无误解的智能体沟通体系》
本文聚焦QClaw多Agent协作中普遍存在的信息误解难题,跳出常规角色配置的浅层思路,从实践沉淀视角剖析智能体间语义歧义、上下文断层、反馈缺失等核心协作痛点。文章深度阐释专属沟通协议的四层构建逻辑,从语义统一、上下文完整传递,到反馈确认、分歧调和,给出可落地的搭建思路与落地准则。结合真实实践对比,印证规范沟通协议能大幅消解认知偏差、减少协作内耗,打破多数人只优化单体能力、忽视交互规则的误区。
308 1
|
4月前
|
人工智能 JavaScript API
(技术贴)别被全网爆火的OpenClaw骗了!实测2小时,真不适合普通人
别被全网爆火的OpenClaw误导!实测2小时发现:部署卡顿、API成本高(日耗几十至千元)、报错难排查,需懂命令行与调试——它本质是开发者框架,非普通人开箱即用工具。现阶段,等待成熟或选择成熟产品更明智。
656 6
|
5月前
|
存储 人工智能 缓存
让 AI Agent 过目不忘:无影 AgentBay 跨会话数据持久化实战
阿里云无影 AgentBay 为 Agent 提供安全、隔离的云沙箱执行环境。针对 Session“阅后即焚”痛点,其核心通用 Context 与 Browser Context 持久化方案,可实现代码项目、配置及浏览器登录态的跨会话无缝延续,解决 Agent“失忆”难题,显著降低重复开发成本。
788 5
|
8月前
|
人工智能 自然语言处理 API
全面认识MCP:大模型连接真实世界的“USB-C接口”
MCP(模型上下文协议)是Anthropic推出的开放标准,被誉为AI时代的“USB-C”。它统一了大模型与工具、数据源的连接方式,简化集成,提升安全与扩展性,推动AI智能体实现复杂任务自动化,正重塑全球AI应用生态。
1379 0
|
人工智能 监控 中间件
深入解析|Cursor编程实践经验分享
本文是近两个月的实践总结,结合在实际工作中的实践聊一聊Cursor的表现。记录在该过程中遇到的问题以及一些解法。问题概览(for 服务端): 不如我写的快?写的不符合预期? Cursor能完成哪些需求?这个需求可以用Cursor,那个需求不能用Cursor? 历史代码分析浅显,不够深入理解? 技术方案设计做的不够好,细节缺失,生成代码的可用性不够满意?
2288 11
深入解析|Cursor编程实践经验分享
|
人工智能 自然语言处理 安全
💻 Codex 来了:OpenAI 推出多任务软件工程 AI 代理,开发者工作方式将被重塑?
Codex 是 OpenAI 推出的一款云端智能开发代理,基于优化后的 Codex-1 模型,能够执行从代码编写、Bug 修复到 PR 提交的完整工程任务。通过 ChatGPT 的界面,用户可向 Codex 分配任务,它将在独立沙盒中运行并返回结果。Codex 支持多任务异步处理,遵循项目规范(AGENTS.md),并生成日志与测试报告以确保透明性。作为“AI 参与式开发”的里程碑,Codex 不仅提升效率,还可能重塑开发者角色,使他们从具体编码转向指导 AI 完成任务,推动软件工程进入意图驱动的新时代。
2263 16
|
Ubuntu 安全 Linux
基于Ubuntu24.04原内核6.8.0升级到6.9.0
通过上述步骤,我们在Ubuntu 24.04系统上成功地将内核从6.8.0升级到了6.9.0。升级内核有助于提高系统的安全性和性能,但也可能带来兼容性问题。因此,在升级前备份重要数据,并确保新内核支持所需的所有硬件和软件。通过合理的验证和测试,可以确保系统在新内核上稳定运行。
2807 6
如何在Java中优雅地处理大文件?
如何在Java中优雅地处理大文件?
|
C语言
【51单片机】用汇编语言实现点灯、闪烁
【51单片机】用汇编语言实现点灯、闪烁
656 1

热门文章

最新文章