【死磕 NIO】— 深入分析Buffer
大家好,我是大明哥,今天我们来看看 Buffer。

上面几篇文章详细介绍了 IO 相关的一些基本概念,如阻塞、非阻塞、同步、异步的区别,Reactor 模式、Proactor 模式。以下是这几篇文章的链接,有兴趣的同学可以阅读下:
从这篇文章开始,我们将回归 NIO 方面的相关知识,首先从 NIO 的三大核心组件说起。
Buffer
Channel
Selector
首先是 Buffer
Buffer
Buffer 是一个抽象类,主要用作缓冲区,其实质我们可以认为是一个可以写入数据,然后从中读取数据的内存块。这块内存被包装成 NIO Buffer 对象,并提供一系列的方法便于我们访问这块内存。
要理解 Buffer 的工作原理,首先就要理解它的 4 个索引:
capacity:容量
position:位置
limit:界限
mark:标记
capacity 则表示该 Buffer 的容量,而 position 和 limit 的含义取决于 Buffer 处于什么模式(读模式或者写模式),下图描述读写模式下这三种属性的含义

- capacity
capacity 表示容量,Buffer 是一个内存块,其存储数据的最大大小就是 capacity。我们不断地往 Buffer 中写入数据,当 Buffer 被写满后也就是存储的数据达到 capacity 了就需要将其清空,才能继续写入数据。
- position
position 的含义取决于 Buffer 处于写模式还是读模式:
如果是写模式,则写入的地方就是所谓的 position,其初始值是 0,最大值是 capacity - 1,当往 Buffer 中写入一个数据时,position 就会向前移动到下一个待写入的位置。
如果是读模式,则读取数据的地方就是 position。当执行
flip()将 buffer 从写模式切换到读模式时,position 会被重置为 0,随着数据不断的读取,position 不断地向前移,直到 limit。limit
与 position 一样,limit 的含义也取决于 Buffer 处于何种模式:
写模式:当 Buffer 处于写模式时,limit 是指能够往 Buffer 中写入多少数据,其值等于 capacity
读模式:当 Buffer 处于读模式时,limit 表示能够从 Buffer 中最多能够读取多少数据出来,所以当 Buffer 从写模式切换到读模式时,limit 会被设置写模式下的 position 的值
mark
mark 仅仅只是一个标识,可以通过 mark() 方法进行设置,设置值为当前的 position
Buffer 方法
Buffer 提供了一系列的方法用来操作它,比如 clear() 用来清空缓冲区,filp() 用来读切换等等方法,下面将依次演示 Buffer 的主要方法,包含从 Buffer 获取实例、写入数据、读取数据、重置等等一个系列的操作流程,同时将 position、limit 两个参数打印出来,便于我们更好地理解 Buffer。
allocate()
要获取一个 Buffer 对象,首先就要为期分配内存空间,使用 allocate() 方法分配内存空间,如下:
DoubleBuffer buffer = DoubleBuffer.allocate(10);
System.out.println("================= allocate 10 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
这里分配了 10 * sikeof(double) 字节的内存空间。需要注意的是 allocate() 里面参数并不是字节数,而是写入对象的数量,比如上面实例参数是 10 ,表明我们可以写 10 个 double 对象。
结果如下:
================= allocate 10 后 =================
capacity = 10
position = 0
limit = 10
此时,Buffer 的情况如下:

put()
调用 allocate() 分配内存后,得到 DoubleBuffer 实例对象,该对象目前处于写模式,我们可以通过 put() 方法向 Buffer 里面写入数据。
buffer.put(1);
buffer.put(2);
System.out.println("================= put 1、2 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
调用 put() 往 DoubleBuffer 里面存放 2 个元素,此时,各自参数值如下:
================= put 1、2 后 =================
capacity = 10
position = 2
limit = 10
我们看到 position 的值变成了 2 ,指向第三个可以写入元素的位置。这个时候我们再写入 3 个元素:
buffer.put(3);
buffer.put(4);
buffer.put(5);
System.out.println("================= put 3、4、5 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
得到结果如下:
================= put 3、4、5 后 =================
capacity = 10
position = 5
limit = 10
此时,position 的值变成 5 ,指向第 6 个可以写入元素的位置。
该 Buffer 的情况如下:

flip()
调用 put() 方法向 Buffer 中存储数据后,这时 Buffer 仍然处于写模式状态,在写模式状态下我们是不能直接从 Buffer 中读取数据的,需要调用 flip() 方法将 Buffer 从写模式切换为读模式。
buffer.flip();
System.out.println("================= flip 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
得到的结果如下:
================= flip 后 =================
capacity = 10
position = 0
limit = 5
调用 flip() 方法将 Buffer 从写模式切换为读模式后,Buffer 的参数发生了微秒的变化:position = 0,limit = 5。前面说过在读模式下,limit 代表是 Buffer 的可读长度,它等于写模式下的 position,而 position 则是读的位置。
flip() 方法主要是将 Buffer 从写模式切换为读模式,其调整的规则如下:
设置可读的长度 limit。将写模式写的 Buffer 中内容的最后位置 position 值变成读模式下的 limit 位置值,新的 limit 值作为读越界位置
设置读的起始位置。将 position 的值设置为 0 ,表示从 0 位置处开始读
如果之前有 mark 保存的标记位置,也需要消除,因为那是写模式下的 mark 标记
调动 flip() 后,该 Buffer 情况如下:

get()
调用 flip() 将 Buffer 切换为读模式后,就可以调用 get() 方法读取 Buffer 中的数据了,get() 读取数据很简单,每次从 position 的位置读取一个数据,并且将 position 向前移动 1 位。如下:
System.out.println("读取第 1 个位置的数据:" + buffer.get());
System.out.println("读取第 2 个位置的数据:" + buffer.get());
System.out.println("================= get 2 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
连续调用 2 次 get() 方法,输出结果:
读取第 1 个位置的数据:1.0
读取第 2 个位置的数据:2.0
================= get 2 后 =================
capacity = 10
position = 2
limit = 5
position 的值变成了 2 ,表明它向前移动了 2 位,此时,Buffer 如下:

我们知道 limit 表明当前 Buffer 最大可读位置,buffer 也是一边读,position 位置一边往前移动,那如果越界读取呢?
System.out.println("读取第 3 个位置的数据:" + buffer.get());
System.out.println("读取第 4 个位置的数据:" + buffer.get());
System.out.println("读取第 5 个位置的数据:" + buffer.get());
System.out.println("读取第 6 个位置的数据:" + buffer.get());
System.out.println("读取第 7 个位置的数据:" + buffer.get());
limit = 5,6 、7 位置明显越界了,如果越界读取,Buffer 会抛出 BufferUnderflowException,如下:
读取第 3 个位置的数据:3.0
读取第 4 个位置的数据:4.0
读取第 5 个位置的数据:5.0
Exception in thread "main" java.nio.BufferUnderflowException
at java.nio.Buffer.nextGetIndex(Buffer.java:500)
at java.nio.HeapDoubleBuffer.get(HeapDoubleBuffer.java:135)
at com.chenssy.study.nio.BufferTest.main(BufferTest.java:48)
rewind()
position 是随着读取的进度一直往前移动的,那如果我想在读取一遍数据呢?使用 rewind() 方法,可以进行重复读。rewind() 也叫做倒带,就想播放磁带一样,倒回去重新读。
buffer.rewind();
System.out.println("================= rewind 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
运行结果:
================= rewind 后 =================
capacity = 10
position = 0
limit = 5
可以看到,仅仅只是将 position 的值设置为了 0,limit 的值保持不变。
clear() 和 compact()
flip() 方法用于将 Buffer 从写模式切换到读模式,那怎么将 Buffer 从读模式切换至写模式呢?可以调用 clear() 和 compact() 两个方法。
- clear()
buffer.clear();
System.out.println("================= clear 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
运行结果如下:
================= clear 后 =================
capacity = 10
position = 0
limit = 10
调用 clear() 后,我们发现 position 的值变成了 0,limit 值变成了 10,也就是 Buffer 被清空了,回归到最初始状态。但是里面的数据仍然是存在的,只是没有标记哪些数据是已读,哪些为未读。

- compact()
compact() 方法也可以将 Buffer 从读模式切换到写模式,它跟 clear() 有一些区别。
buffer.compact();
System.out.println("================= compact 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
运行结果如下:
================= compact 后 =================
capacity = 10
position = 3
limit = 10
可以看到 position 的值为 3,它与 clear() 区别就在于,它会将所有未读的数据全部复制到 Buffer 的前面(5次put(),两次 get()),将 position 设置到这些数据后面,所以此时是从未读的数据后面开始写入新的数据,Buffer 情况如下:

mark() 和 reset()
调用 mark() 方法可以标志一个指定的位置(即设置 mark 的值),之后调用 reset() 时,position 又会回到之前标记的位置。
通过上面的步骤演示,我想小伙伴基本上已经掌握了 Buffer 的使用方法,这里简要总结下,使用 Buffer 的步骤如下:
将数据写入 Buffer 中
调用
flip()方法,将 Buffer 切换为读模式从 Buffer 中读取数据
调用
clear()或者compact()方法将 Buffer 切换为写模式
Buffer 的类型
在 NIO 中主要有 8 中 Buffer,分别如下:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
MappedByteBuffer
其 UML 类图如下:

这些不同的 Buffer 类型代表了不同的数据类型,使得可以通过 Buffer 直接操作如 char、short 等类型的数据而不是字节数据。这些 Buffer 基本上覆盖了所有能从 IO 中传输的 Java 基本数据类型,其中 MappedByteBuffer 是专门用于内存映射的的一种 ByteBuffer,后续会专门介绍。
到这里 Buffer 也就介绍完毕了,下篇文章将介绍它的协作者 Channel。
【死磕 NIO】— 深入分析Buffer的更多相关文章
- 【死磕NIO】— 跨进程文件锁:FileLock
大家好,我是大明哥,一个专注于[死磕 Java]系列创作的程序员. [死磕 Java ]系列为作者「chenssy」 倾情打造的 Java 系列文章,深入分析 Java 相关技术核心原理及源码 死磕 ...
- 【死磕NIO】— 阻塞、非阻塞、同步、异步,傻傻分不清楚
万事从最基本的开始. 要想完全掌握 NIO,并不是掌握上面文章([死磕NIO]- NIO基础详解)中的三大组件就可以了,我们还需要掌握一些基本概念,如什么是 IO,5 种IO模型的区别,什么是阻塞&a ...
- 【死磕NIO】— 阻塞IO,非阻塞IO,IO复用,信号驱动IO,异步IO,这你真的分的清楚吗?
通过上篇文章([死磕NIO]- 阻塞.非阻塞.同步.异步,傻傻分不清楚),我想你应该能够区分了什么是阻塞.非阻塞.异步.非异步了,这篇文章我们来彻底弄清楚什么是阻塞IO,非阻塞IO,IO复用,信号驱动 ...
- 【死磕 NIO】— Reactor 模式就一定意味着高性能吗?
大家好,我是大明哥,我又来了. 为什么是 Reactor 一般所有的网络服务,一般分为如下几个步骤: 读请求(read request) 读解析(read decode) 处理程序(process s ...
- 【死磕 NIO】— Proactor模式是什么?很牛逼吗?
大家好,我是大明哥. 上篇文章我们分析了高性能 IO模型Reactor模式,了解了什么是Reactor 模式以及它的三种常见的模式,这篇文章,大明再介绍另外一种高性能IO模型: Proactor. 为 ...
- 【死磕NIO】— 探索 SocketChannel 的核心原理
大家好,我是大明哥,一个专注于[死磕 Java]系列创作的程序员. [死磕 Java ]系列为作者「chenssy」 倾情打造的 Java 系列文章,深入分析 Java 相关技术核心原理及源码. 死磕 ...
- 【死磕NIO】— NIO基础详解
Netty 是基于Java NIO 封装的网络通讯框架,只有充分理解了 Java NIO 才能理解好Netty的底层设计.Java NIO 由三个核心组件组件: Buffer Channel Sele ...
- Java NIO Buffer(netty源码死磕1.2)
[基础篇]netty源码死磕1.2: NIO Buffer 1. Java NIO Buffer Buffer是一个抽象类,位于java.nio包中,主要用作缓冲区.Buffer缓冲区本质上是一块可 ...
- JAVA NIO 简介 (netty源码死磕1.1)
[基础篇]netty 源码死磕1.1: JAVA NIO简介 1. JAVA NIO简介 Java 中 New I/O类库 是由 Java 1.4 引进的异步 IO.由于之前老的I/O类库是阻塞I/ ...
随机推荐
- 手动实现 shared_ptr
面试写了一个基础的 scoped_ptr,被面试官要求写 shared_ptr,一时语塞.面试官不断提示我说在现有的基础上实现 shared_ptr 很简单,真的很简单,宛如在不断暗示我 1+1 就是 ...
- Win32对话框模板创建的窗口上响应键消息,Tab焦点切换消息,加速键消息
今天在学习的时候,发现对话框上不响应键盘消息,查了老半天,终于成功了,现分享出来, 1,首先要在消息循环的时候加如下代码. int WINAPI WinMain(_In_ HINSTANCE hIns ...
- 那些我用Windows时必备的软件
那些我用Windows时必备的软件 使用Windows多年来,自己发现了许多不错的小软件,在此做一个备份清单,方便今后装新机. 起源 \(hadow\)ock$ 越过高墙的小飞机 Google Chr ...
- 使用Mybatis的一些基本配置及Mybatis与数据库交互测试验证
1.简介 什么是MyBatis? MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程以及高级映射.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.My ...
- spring提供的可拓展接口
接口:SmartLifecycle(https://www.jianshu.com/p/7b8f2a97c8f5)
- 【Azure 应用服务】App Service For Linux 如何在 Web 应用实例上住抓取网络日志
问题描述 在App Service For Windows的环境中,我们可以通过ArmClient 工具发送POST请求在Web应用的实例中抓取网络日志,但是在App Service For Linu ...
- 如何从一台OPC Server访问多个PLC
项目中遇到如下情况: 1. 整条生产线由多个PLC分别控制,但是所有PLC在同一个局域网内.PLC采用西门子的S7-200 Smart 2. 客户希望在操作工站的电脑(跟PLC在同一个局域网内)上提供 ...
- [敏捷软工团队博客]Beta阶段发布声明
项目 内容 2020春季计算机学院软件工程(罗杰 任健) 博客园班级博客 作业要求 Beta阶段发布声明 我们在这个课程的目标是 在团队合作中锻炼自己 这个作业在哪个具体方面帮助我们实现目标 对Bet ...
- Scrum Meeting 0503
零.说明 日期:2021-5-3 任务:简要汇报两日内已完成任务,计划后两日完成任务 一.进度情况 组员 负责 两日内已完成的任务 后两日计划完成的任务 qsy PM&前端 完成登录.后端管理 ...
- AIApe问答机器人Scrum Meeting 4.25
Scrum Meeting 2 日期:2021年4月25日 会议主要内容概述:前后端针对WebAPI进行协调与统一工作,商量接下来两日计划:敲定部分设计细节. 一.进度情况 组员 负责 两日内已完成的 ...