Java NIO三件套之Buffer实现原理解析

capacity10 , position0 , limit4

调用get() :

capacity10 , position4 , limit4

调用clear() :

capacity10 , position0 , limit10

下面我们来对上面代码的执行结果进行图解分析(围绕position、limit、capacity三个字段值):

// 分配一个容量为10的缓冲区(本质上就是一个容量为10的byte数组)

ByteBuffer buffer = ByteBuffer.allocate(10);

// 从管道将数据读取到buffer容器中

channel.read(buffer);

output(“调用read()”, buffer);

首先从通道中读取一些数据到缓冲区中(注意从通道读取数据,相当于往缓冲区写入数据)。如果读取4个字节的数据,则此时 position的值为4,即下一个将要被写入的字节索引为4,而limit仍然是10,如下图所示。

// 准备操作之前,先锁定操作范围:

buffer.flip();

output(“调用flip()”, buffer);

下一步把读取的数据写入输出通道,相当于从缓冲区中读取数据,在此之前,必须调用flip()方法。该方法将会完成以下两件事情:

  • 一是把limit设置为当前的position值。

  • 二是把position设置为 0。

由于position被设置为0,所以可以保证在下一步输出时读取的是缓冲区的第一个字节,而limit被设置为当前的position,可以保证读取的数据正好是之前写入缓冲区的数据,如下图所示。

// 判断有没有可读数据

while (buffer.remaining() > 0){

byte b = buffer.get();

}

output(“调用get()”, buffer);

调用get()方法从缓冲区中读取数据写入输出通道,这会导致 position的增加而limit保持不变,但position不会超过limit的值, 所以在读取之前写入缓冲区的4字节之后,position和limit的值都为 4,如下图所示。

// 可以理解为解锁

buffer.clear();

output(“调用clear()”, buffer);

// 最后把管道关闭

fileInputStream.close();

在从缓冲区中读取数据完毕后,limit的值仍然保持在调用flip()方法时的值,调用clear()方法能够把所有的状态变化设置为初始化时的值,最后关闭流,如下图所示。

通过上述案例,更能突出Buffer是一个特殊的数组容器,与普通数组区别就在于其内置三个 “指针变量”:position、limit、capacity 用于跟踪和记录缓冲区的状态变化情况!

4、allocate方法初始化一个指定容量大小的缓冲区

在创建一个缓冲区对象时,会调用静态方法allocate()来指定缓冲区的容量,其实调用allocate()方法相当于创建了一个指定大小的数组,并把它包装为缓冲区对象。

allocate()源码如下:

// 位于ByteBuffer下

public static ByteBuffer allocate(int capacity) {

if (capacity < 0)

throw new IllegalArgumentException();

// 新建一个ByteBuffer数组对象,容量为:capacity,limit参数值为:capacity

return new HeapByteBuffer(capacity, capacity);

}

// 位于HeapByteBuffer下,父类为:ByteBuffer

HeapByteBuffer(int cap, int lim) {

super(-1, 0, lim, cap, new byte[cap], 0);// 调用ByteBuffer的有参构造函数

}

// 位于ByteBuffer下,父类为:Buffer

ByteBuffer(int mark, int pos, int lim, int cap,

byte[] hb, int offset){

super(mark, pos, lim, cap);// 调用 Buffer构造函数

this.hb = hb;// final byte[] hb; 不可变的byte数组

this.offset = offset;// 偏移量

}

// Buffer构造函数

Buffer(int mark, int pos, int lim, int cap) {

if (cap < 0)

throw new IllegalArgumentException("Negative capacity: " + cap);

this.capacity = cap;// 数组容量

limit(lim);// 数组的了limit

position(pos);// 数组的positio

if (mark >= 0) {

if (mark > pos)

throw new IllegalArgumentException(“mark > position: (”

  • mark + " > " + pos + “)”);

this.mark = mark;

}

}

本质上等同于如下代码:

// 初始化一个byte数组

byte[] bytes = new byte[10];

// 将该数组包装给ByteBuffer

ByteBuffer buffer = ByteBuffer.wrap(bytes);

5、slice方法缓冲区分片

Java NIO中,可以根据先用的缓冲区Buffer对象创建一个子缓冲区。即,在现有缓冲区上切出一片作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的。

示例代码如下所示:

/**

  • @author csp

  • @date 2021-11-28 6:20 下午

*/

public class BufferSlice {

public static void main(String[] args) {

ByteBuffer buffer = ByteBuffer.allocate(10);

// 向缓冲区中put数据: 0~9

for (int i = 0; i < buffer.capacity(); i++) {

buffer.put((byte) i);

}

// 创建子缓冲区:即从数组下标为3的位置到下标为7的位置

buffer.position(3);

buffer.limit(7);

ByteBuffer slice = buffer.slice();

// 改变子缓冲区的内容

for (int i = 0; i < slice.capacity(); i++) {

byte b = slice.get(i);

b *= 10;

slice.put(i, b);

}

// position和limit恢复到初始位置:

buffer.position(0);

buffer.limit(buffer.capacity());

// 输出buffer容器中的内容:

while (buffer.hasRemaining()) {

System.out.println(buffer.get());

}

}

}

在该示例中,分配了一个容量大小为10的缓冲区,并在其中放入 了数据0~9,而在该缓冲区基础上又创建了一个子缓冲区,并改变子缓冲区中的内容,从最后输出的结果来看,只有子缓冲区“可见的” 那部分数据发生了变化,并且说明子缓冲区与原缓冲区是数据共享 的,输出结果如下所示:

0

1

2

30

40

50

60

7

8

9

6、只读缓冲区

只读缓冲区,顾名思义就是只可以从缓冲区中读取数据,而不可以向其中写入数据。

将现有缓冲区让其调用asReadOnlyBuffer()方法,使其转换成只读缓冲区。这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。如果原缓冲区的 内容发生了变化,只读缓冲区的内容也随之发生变化

示例代码如下所示:

/**

  • @author csp

  • @date 2021-11-28 6:33 下午

*/

public class ReadOnlyBuffer {

public static void main(String[] args) {

// 初始化一个容量为10的缓冲区

ByteBuffer buffer = ByteBuffer.allocate(10);

// 向缓冲区中put数据: 0~9

for (int i = 0; i < buffer.capacity(); i++) {

buffer.put((byte) i);

}

// 将该缓冲区转变为只读缓冲区

ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();

// 由于buffer和readOnlyBuffer本质上共享一个byte[]数组对象,

// 所以,改变buffer缓冲区的内容时,会导致只读缓冲区readOnlyBuffer的内容也随着改变。

for (int i = 0; i < buffer.capacity(); i++) {

byte b = buffer.get(i);

b *= 10;

buffer.put(i, b);

}

// position和limit恢复到初始位置:

readOnlyBuffer.position(0);

readOnlyBuffer.limit(buffer.capacity());

// 输出readOnlyBuffer容器中的内容:

while (readOnlyBuffer.hasRemaining()) {

System.out.println(readOnlyBuffer.get());

}

}

}

输出结果如下:

0

10

20

30

40

50

60

70

80

90

如果尝试修改只读缓冲区的内容,则会报 ReadOnlyBufferException异常。只可以把常规缓冲区转换为只读缓冲区,而不能将只读缓冲区转换为 可写的缓冲区。

7、直接缓冲区

对于直接缓冲区的定义,《深入理解Java虚拟机》这本书是这样介绍的:

  • Java NIO字节缓冲区(ByteBuffer)要么是直接的,要么是非直接的。如果为直接字节缓冲区,则java虚拟机会尽最大努力直接在此缓冲区上执行本机的IO操作,也就是说,在每次调用基础操作系统的一个本机IO操作前后,虚拟机都会尽量避免将内核缓冲区内容复制到用户进程缓冲区中,或者反过来,尽量避免从用户进程缓冲区复制到内核缓冲区中。

  • 直接缓冲区可以通过调用该缓冲区类的allocateDirect(int capacity) 方法创建,此方法返回的缓冲区进行分配和取消分配所需的成本要高于非直接缓冲区。直接缓冲区的内容驻留在垃圾回收堆之外,因此他们对应用程序内存(JVM内存)需求不大。所以建议直接缓冲区要分配给那些大型,持久(就是缓冲区的数据会被重复利用)的缓冲区,一般情况下,最好仅在直接缓冲区能在程序性能带来非常明显的好处时才分配它们。

  • 直接缓冲区还可以通过FileCHannel的map()方法将文件区域映射到内存中来创建,该方法返回MappedByteBuffer。Java平台的实现有助于通过JNI本地代码创建直接字节缓冲区,如果以上这些缓冲区中某个缓冲区实例指向的是不可访问的内存区域,则试图方法该区域不会更改缓冲区的内容,并且会在访问期间或者稍后的某个时间导致报出不确定性异常。

  • 字节缓冲区是直接缓冲区还是非直接缓冲区可以通过调用其isDIrect()方法来判断。

案例代码:

/**

  • @author csp

  • @date 2021-11-28 7:07 下午

*/

public class DirectBuffer {

public static void main(String[] args) throws IOException {

// 从磁盘中读取test.txt文件内容

FileInputStream fileInputStream = new FileInputStream(“/Users/csp/IdeaProjects/netty-study/test.txt”);

// 创建文件的操作管道

FileChannel inputStreamChannel = fileInputStream.getChannel();

// 把读取的内容写入到新的文件中

FileOutputStream fileOutputStream = new FileOutputStream(“/Users/csp/IdeaProjects/netty-study/test2.txt”);

FileChannel outputStreamChannel = fileOutputStream.getChannel();

// 创建直接缓冲区

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);

while (true){

byteBuffer.clear();

int read = inputStreamChannel.read(byteBuffer);

if (read == -1){

break;

}

byteBuffer.flip();

outputStreamChannel.write(byteBuffer);

}

}

}

要分配 直接缓冲区,需要调用allocateDirect()方法,而不是allocate()方 法,使用方式与普通缓冲区并无区别。

8、内存映射

内存映射是一种读和写文件数据的方法,可以比常规的基于流或者基于通道的I/O快得多。内存映射文件I/O通过使文件中的数据表现为内存数组的内容来完成,这初听起来似乎不过就是将整个文件读到内存中,但事实上并不是这样的。一般来说,只有文件中实际读取或 写入的部分才会映射到内存中。来看下面的示例代码:

/**

  • @author csp

  • @date 2021-11-28 7:16 下午

*/

public class MapperBuffer {

static private final int start = 0;

static private final int size = 10;

public static void main(String[] args) throws IOException {

RandomAccessFile randomAccessFile = new RandomAccessFile(“/Users/csp/IdeaProjects/netty-study/test.txt”, “rw”);

FileChannel channel = randomAccessFile.getChannel();

// 把缓冲区跟文件系统进行一个映射关联,只要操作缓冲区里面的内容,文件内容也会随之改变

MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, start, size);

mappedByteBuffer.put(4, (byte) 97);// a

mappedByteBuffer.put(5, (byte) 122);// z

randomAccessFile.close();

}

}

原来test.txt文件内容为:

Java
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

ActiveMQ消息中间件面试专题

  • 什么是ActiveMQ?
  • ActiveMQ服务器宕机怎么办?
  • 丢消息怎么办?
  • 持久化消息非常慢怎么办?
  • 消息的不均匀消费怎么办?
  • 死信队列怎么办?
  • ActiveMQ中的消息重发时间间隔和重发次数吗?

ActiveMQ消息中间件面试专题解析拓展:

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM


redis面试专题及答案

  • 支持一致性哈希的客户端有哪些?
  • Redis与其他key-value存储有什么不同?
  • Redis的内存占用情况怎么样?
  • 都有哪些办法可以降低Redis的内存使用情况呢?
  • 查看Redis使用情况及状态信息用什么命令?
  • Redis的内存用完了会发生什么?
  • Redis是单线程的,如何提高多核CPU的利用率?

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM


Spring面试专题及答案

  • 谈谈你对 Spring 的理解
  • Spring 有哪些优点?
  • Spring 中的设计模式
  • 怎样开启注解装配以及常用注解
  • 简单介绍下 Spring bean 的生命周期

Spring面试答案解析拓展

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM


高并发多线程面试专题

  • 现在有线程 T1、T2 和 T3。你如何确保 T2 线程在 T1 之后执行,并且 T3 线程在 T2 之后执行?
  • Java 中新的 Lock 接口相对于同步代码块(synchronized block)有什么优势?如果让你实现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。
  • Java 中 wait 和 sleep 方法有什么区别?
  • 如何在 Java 中实现一个阻塞队列?
  • 如何在 Java 中编写代码解决生产者消费者问题?
  • 写一段死锁代码。你在 Java 中如何解决死锁?

高并发多线程面试解析与拓展

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM


jvm面试专题与解析

  • JVM 由哪些部分组成?
  • JVM 内存划分?
  • Java 的内存模型?
  • 引用的分类?
  • GC什么时候开始?

JVM面试专题解析与拓展!

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
开启注解装配以及常用注解

  • 简单介绍下 Spring bean 的生命周期

Spring面试答案解析拓展

[外链图片转存中…(img-gtgUu9bV-1713390217509)]


高并发多线程面试专题

  • 现在有线程 T1、T2 和 T3。你如何确保 T2 线程在 T1 之后执行,并且 T3 线程在 T2 之后执行?
  • Java 中新的 Lock 接口相对于同步代码块(synchronized block)有什么优势?如果让你实现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。
  • Java 中 wait 和 sleep 方法有什么区别?
  • 如何在 Java 中实现一个阻塞队列?
  • 如何在 Java 中编写代码解决生产者消费者问题?
  • 写一段死锁代码。你在 Java 中如何解决死锁?

高并发多线程面试解析与拓展

[外链图片转存中…(img-0iZ2G7uy-1713390217509)]


jvm面试专题与解析

  • JVM 由哪些部分组成?
  • JVM 内存划分?
  • Java 的内存模型?
  • 引用的分类?
  • GC什么时候开始?

JVM面试专题解析与拓展!

[外链图片转存中…(img-5yXfN3iJ-1713390217510)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 16
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值