JDK源码阅读-ByteBuffer
本文转载自JDK源码阅读-ByteBuffer
导语
Buffer是Java NIO中对于缓冲区的封装。在Java BIO中,所有的读写API,都是直接使用byte数组作为缓冲区的,简单直接。但是在Java NIO中,缓冲区这一概念变得复杂,可能是对应Java堆中的一块内存,也可能是对应本地内存中的一块内存。而byte数组只能用来指定Java堆中的一块内存,所以Java NIO中设计了一个新的缓冲区抽象,涵盖了不同类型缓冲区,这个抽象就是Buffer。
Buffer
Buffer是Java NIO中对于缓冲区的抽象。是一个用于存储特定基本数据类型的容器。Buffer是特定基本数据类型的线性有限序列。
Java有8中基本类型:byte,short,int,long,float,double,char,boolean,除了boolean类型外,其他的类型都有对应的Buffer具体实现:
Buffer抽象类定义了所有类型的Buffer都有的属性和操作,属性如下:
capacity
:缓冲区的容量,在缓冲区建立后就不能改变limit
:表示第一个不能读写的元素位置,limit不会大于capacityposition
:表示下一个要读写的元素位置,position不会大于limitmark
:用于暂存一个元素位置,和书签一样,用于后续操作
所有的Buffer操作都围绕这些属性进行。这些属性满足一个不变式:0<=mark<=position<=limit<=capacity
。
新建的Buffer这些属性的取值为:
- position=0
- limit=capacity=用户设置的容量
- mark=-1
直接看定义比较抽象,可以看一下示意图,下图是一个容量为10的Buffer:
ByteBuffer的具体实现
所有Buffer实现中,最重要的实现是ByteBuffer,因为操作系统中所有的IO操作都是对字节的操作。当我们需要从字节缓冲区中读取别的数据类型才需要使用其他具体类型的Buffer实现。
ByteBuffer也是一个抽象类,具体的实现有HeapByteBuffer和DirectByteBuffer。分别对应Java堆缓冲区与堆外内存缓冲区。Java堆缓冲区本质上就是byte数组,所以实现会比较简单。而堆外内存涉及到JNI代码实现,较为复杂,本次我们以HeapByteBuffer为例来分析Buffer的相关操作,后续专门分析DirectByteBuffer。
ByteBuffer的类图如下:
读写Buffer
Buffer作为缓冲区,最主要的作用是用于传递数据。Buffer提供了一系列的读取与写入操作。因为不同类型的Buffer读写的类型不同,所以具体的方法定义是定义在Buffer实现类中的。与读写相关的API如下:
byte get()
byte get(int index)
ByteBuffer get(byte[] dst, int offset, int length)
ByteBuffer get(byte[] dst)
ByteBuffer put(byte b)
ByteBuffer put(int index, byte b)
ByteBuffer put(ByteBuffer src)
ByteBuffer put(byte[] src, int offset, int length)
Buffer的读写操作可以按照两种维度分类:
- 单个/批量:
- 单个:一次读写一个字节
- 批量:一次读写多个字节
- 相对/绝对:
- 相对:从Buffer维护的position位置开始读写,读写时position会随之变化
- 绝对:直接指定读写的位置。指定index的API就是绝对API
接着我们来看看这些函数在HeapByteBuffer中是如何实现的:
final byte[] hb; // 作为缓冲区的byte数组
final int offset; // 指定缓冲区的起始位置
public byte get() {
// get操作就是直接从数组中获取数据
return hb[ix(nextGetIndex())];
}
public byte get(int i) {
// 从指定位置获取数据,是绝对操作,只需检查下标是否合法
return hb[ix(checkIndex(i))];
}
// 获取下一个要读取的元素的下标
// position的定义就是下一个要读写的元素位置,
// 所以这里是返回position的当前值,然后再对position进行加一操作
final int nextGetIndex() { // package-private
if (position >= limit)
throw new BufferUnderflowException();
return position++;
}
// 因为支持偏移量,所以算出来的下标还需要加上偏移量
protected int ix(int i) {
return i + offset;
}
单字节put与get逻辑一样。看一下批量get是如何实现的:
public ByteBuffer get(byte[] dst) {
return get(dst, 0, dst.length);
}
public ByteBuffer get(byte[] dst, int offset, int length) {
// 检查参数是否越界
checkBounds(offset, length, dst.length);
// 检查要获取的长度是否大于Buffer中剩余的数据长度
if (length > remaining())
throw new BufferUnderflowException();
// 调用System.arraycopy进行数组内容拷贝
System.arraycopy(hb, ix(position()), dst, offset, length);
// 更新position
position(position() + length);
return this;
}
可以看出,HeapByteBuffer是封装了对byte数组的简单操作。对缓冲区的写入和读取本质上是对数组的写入和读取。使用HeapByteBuffer的好处是我们不用做各种参数校验,也不需要另外维护数组当前读写位置的变量了。
同时我们可以看到,Buffer中对于position的操作没有使用锁进行保护,所以Buffer不是线程安全的。
Buffer的模式
虽然JDK的Java Doc并没有提到Buffer有模式,但是Buffer提供了flip等操作用于切换Buffer的工作模式。在正确使用Buffer时,一定要注意Buffer的当前工作模式。否则会导致数据读写不符合你的预期。
Buffer有两种工作模式,一种是接收数据模式,一种是输出数据模式。
新建的Buffer处于接收数据的模式,可以向Buffer放入数据,放入一个对应基本类型的数据后,position加一,如果position已经等于limit了还进行put操作,则会抛出BufferOverflowException异常。
这种模式的Buffer可以用于Channel的read操作缓冲区,或者是用于相对put操作。
比如向一个接受数据模式的Buffer put5个byte后的示例图:
因为Buffer的设计是读写的位置变量都使用position这个变量,所以如果要从Buffer中读取数据,要切换Buffer到输出数据模式。Buffer提供了flip方法用于这种切换。
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
切换后的效果图:
然后就可以从Buffer中读取数据了。每次读取一个元素,position就会加一,如果position已经等于limit还进行读取,会抛出BufferUnderflowException异常。
可以看出Buffer本身没有一个用于存储模式的变量,模式的切换只是position和limit的变换而已。
flip方法只会把Buffer从接收模式切换到输出模式,如果要从输出模式切换到接收模式,可以使用compact
或者clear
方法,如果数据已经读取完毕或者数据不要了,使用clear
方法,如果已读的数据需要保留,同时需要切换到接收数据模式,使用compat
方法。
// 压缩Buffer,去掉已经被读取的数据
// 压缩后的Buffer处于接收数据模式
public ByteBuffer compact() {
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
position(remaining());
limit(capacity());
discardMark();
return this;
}
// 清空Buffer,去掉所有数据(没有做清理工作,是指修改位置变量)
// 清空后的Buffer处于接收数据模式
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
总结
- Buffer是Java NIO对缓冲区的抽象
- 除了boolean类型,其他的基本类型都有对应的Buffer实现
- 最常用的Buffer实现是ByteBuffer,具体的实现有HeapByteBuffer和DirectByteBuffer,分别对应Java堆缓冲区与对外内存缓冲区
- HeapByteBuffer是对byte数组的封装,方便使用
- Buffer不是线程安全的
- Buffer有两种模式一种是接收数据模式,一种是输出数据模式。新建的Buffer处于接收数据模式,使用
flip
方法可以切换Buffer到输出数据模式。使用compact
或者clear
方法可以切换到接收数据模式。
参考资料
JDK源码阅读-ByteBuffer的更多相关文章
- JDK源码阅读-DirectByteBuffer
本文转载自JDK源码阅读-DirectByteBuffer 导语 在文章JDK源码阅读-ByteBuffer中,我们学习了ByteBuffer的设计.但是他是一个抽象类,真正的实现分为两类:HeapB ...
- JDK源码阅读(三):ArraryList源码解析
今天来看一下ArrayList的源码 目录 介绍 继承结构 属性 构造方法 add方法 remove方法 修改方法 获取元素 size()方法 isEmpty方法 clear方法 循环数组 1.介绍 ...
- JDK源码阅读(一):Object源码分析
最近经过某大佬的建议准备阅读一下JDK的源码来提升一下自己 所以开始写JDK源码分析的文章 阅读JDK版本为1.8 目录 Object结构图 构造器 equals 方法 getClass 方法 has ...
- 利用IDEA搭建JDK源码阅读环境
利用IDEA搭建JDK源码阅读环境 首先新建一个java基础项目 基础目录 source 源码 test 测试源码和入口 准备JDK源码 下图框起来的路径就是jdk的储存位置 打开jdk目录,找到sr ...
- JDK源码阅读-FileOutputStream
本文转载自JDK源码阅读-FileOutputStream 导语 FileOutputStream用户打开文件并获取输出流. 打开文件 public FileOutputStream(File fil ...
- JDK源码阅读-FileInputStream
本文转载自JDK源码阅读-FileInputStream 导语 FileIntputStream用于打开一个文件并获取输入流. 打开文件 我们来看看FileIntputStream打开文件时,做了什么 ...
- JDK源码阅读-RandomAccessFile
本文转载自JDK源码阅读-RandomAccessFile 导语 FileInputStream只能用于读取文件,FileOutputStream只能用于写入文件,而对于同时读取文件,并且需要随意移动 ...
- JDK源码阅读-FileDescriptor
本文转载自JDK源码阅读-FileDescriptor 导语 操作系统使用文件描述符来指代一个打开的文件,对文件的读写操作,都需要文件描述符作为参数.Java虽然在设计上使用了抽象程度更高的流来作为文 ...
- JDK源码阅读-Reference
本文转载自JDK源码阅读-Reference 导语 Java最初只有普通的强引用,只有对象存在引用,则对象就不会被回收,即使内存不足,也是如此,JVM会爆出OOME,也不会去回收存在引用的对象. 如果 ...
随机推荐
- 使用 html5 svg 绘制图形
有一次看一个项目的时候,看到图片的格式为svg,作为萌新的我瞬间有点小懵,这可是之前从没有见到过的格式,于是就开始上某度进行学习,发现某博主的优秀文章,进行转载方便自己学习,感谢原博主的优秀文章. · ...
- 码一次前后台post请求交互,以及接口的使用,json数据格式的传递
近几天,公司疯狂加班,然后补做了很多功能,很多东西虽然是自己熟悉的,但是却不会上手,动手实践能力仍需加强,对此对一些代码记录,留待学习和总结. 简单描述功能 具体实现 前台JSP.JS.后台actio ...
- pytest测试框架+jenkins结合pytest+jenkins邮件通知配置
刚刚做完一个项目,由于这是一个方案项目,而不是产品,所以各种准备很不充分,很多公司的能力不能复用,整个团队又都是新员工,而且有部分实习生,匆忙上马,今天对我的自动化框架做一个回溯 自动化测试框架的选择 ...
- dedecms织梦后台栏目显示文档数不为0,但点进去之后什么都没有
曾经通过sql语句直接删除过dede_addonarticle或者dede_archives或者dede_arctiny中的记录,这三个表是有关联的,如果要通过sql语句删除内容,一定要同时将这三个表 ...
- Git实现1个项目2个地址1次推送
Git实现1个项目2个地址1次推送 考虑到不需要pull操作,因此本方法适用于个人项目分别在两个平台或地址进行部署 给origin 增加一个可以push的地址 git remote set-url - ...
- HDU - 6761 Minimum Index (字符串,Lyndon分解)
Minimum Index 题意 求字符串所有前缀的所有后缀表示中字典序最小的位置集合,最终转换为1112进制表示.比如aab,有三个前缀分别为a,aa,aab.其中a的后缀只有一个a,位置下标1:a ...
- 【poj 3090】Visible Lattice Points(数论--欧拉函数 找规律求前缀和)
题意:问从(0,0)到(x,y)(0≤x, y≤N)的线段没有与其他整数点相交的点数. 解法:只有 gcd(x,y)=1 时才满足条件,问 N 以前所有的合法点的和,就发现和上一题-- [poj 24 ...
- codeforces 949B :A Leapfrog in the Array 找规律
题意: 现在给你一个n,表示有2*n-1个方格,第奇数方格上会有一个数字 1-n按顺序放.第偶数个方格上是没有数字的.变动规则是排在最后一个位置的数字,移动到它前边最近的空位 . 直到数字之间没有空位 ...
- hutool学习总结
1. 为什么要学习Hutool的使用 Hutool官网 中文写的已经很清楚了 Hutool是一款强力的工具类.封装了工作开发中一些常见的功能操作.避免重复造轮子,使用它大大提高的开发效率. 2. Hu ...
- 国产网络测试仪MiniSMB - 如何3秒内创建出16,000条源/目标MAC地址号递增流
国产网络测试仪MiniSMB(www.minismb.com)是复刻smartbits的IP网络性能测试工具,是一款专门用于测试智能路由器,网络交换机的性能和稳定性的软硬件相结合的工具.可以通过此以太 ...