NIO基础入门(一)之缓冲区Buffer

[TOC]

1.概念

NIO(none blocking IO ) 非阻塞IO,多路复用(JDK1.4)

对I/O不足的补全,提供了高速.面向模块的I/O

将I/O操作最耗时的填充和提取缓冲区操作转移回操作系统,大大提高效率

2.场景

短数据,长连接

如Websocket,dubbo等,多种框架都有所集成

3.组成

  1. 缓冲区(Buffers)
  2. 通道(Channels)
  3. 文件锁定和内存映射文件(File locking and memory-mapped files)
  4. 套接字(socket)
  5. 选择器(selectors)
  6. 正则表达式(Regular expressions)
  7. 字符集(Character sets)

4.缓冲区

1.简介

I/O的基础,所谓的输入输出,无非是把数据转移或转出缓冲区

  1. 写(排干净)
  2. 读(塞满)

缓冲区: 固定数量的数据容器,作用是存储器分段运输区

2.分类

  • 直接缓冲区
  • 间接缓冲区
  • 视图缓冲区

3.工作流程

进程调用read()方法,向系统发出调用,要求填满(填入)缓冲区(控制权移交给内核,也称为陷阱)

内核向磁盘控制器发出命令,要求从磁盘读取数据

磁盘控制器读取数据到内核空间的缓冲区中(如果数据不在内核空间,进程将被挂起,直到数据读入内核空间)

通过read()方法,将内核空间缓冲区中的数据复制,填入到用户空间的缓冲区中

用户空间:如JVM等常规进程所在区域,非特权区域,无法直接访问硬件设备

内核空间:如操作系统所在区域,直接与硬件设备可见,I/O都是直接或间接通过内和空间操作

分以上两个空间,主要是考虑进程请求的大小非对齐数据块等问题,添加中间过度进行重新组合转换,内核充当中间人的角色

4.发散和汇聚

许多操作系统把组装/分解过程进行得更加高效.根据发散/汇聚的概念,进程只需要将一个系统调动,就能把一连串缓冲区地址传给操作系统,然后内核可以根据顺序的多个缓冲区地址进行填充排干

  1. 读的时候把数据发散到多个用户空间缓冲区
  2. 写的时候吧多个用户空间缓冲区数据汇聚排出

减少系统调用次数,提高性能

5.虚拟内存

虚拟地址取代物理内存地址

分为两类特性:

  1. 一个以上的虚拟地址可以指向同一个物理内存地址
  2. 虚拟内存空间可大于实际可用的硬件内存

6.内存空间多重映射

设备控制器不能通过DMA直接存储到用户空间,

但是,如上第一类特性所说,把内核空间用户空间的虚拟地址映射到同一个物理地址,

这样DMA硬件(只能访问物理内存地址)就可以填充对内核与用户空间同时可见的缓存区

7.内存页

虚拟内存的第二类特性(寻址空间大于物理内存),

虚拟内存分页(经常称为交换,真正的交换实在进程层完成,而非页面层)

本质上说,物理内存充当分页的高速缓存,实际数据被存储到外部虚拟内存中,

在使用数据时,数据将会被再次交换到物理内存中

所有磁盘I/O都是在页面层完成

8.MMU内存管理单元

CPU的子系统,内存管理单元子系统,逻辑上位于CPU和物理内存之间,

包含虚拟地址向物理地址转换时所需的映射信息,

CPU可以快速的通过MMU进行映射查找,如果查询不到,则MMU向CPU提交页错误,

页错误会随机产生一个系统调用(陷阱),把控制权转移给内核

9.文件I/O

文件I/O时文件系统,与磁盘是两个概念

磁盘:把数据存储在扇区上,属于硬件设备,对文件内容一无所知,只提供数据存取窗口,与内存相似,都是存储器

文件系统:更高级的抽象,是安排/解释磁盘数据的独特方式,代码中的交互是与文件系统打交道,而非磁盘,如:

文件名/路径/大小/文件属性等抽象概念

10.内存映射文件

传统文件I/O是通过发布read()/write()系统调用来传输数据,需要进行copy

内存映射I/O可以屏蔽copy,将内核系统页与用户空间的缓冲映射到同一片物理内存地址直接使用,减少copy的过程

文件锁定

分为独占锁和共享锁,一个线程读取该文件时,限制其他线程的操作

11.缓冲区家族

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

https://colobu.com/2014/10/20/java-buffer-basic/

12.属性

容量(Capacity)

创建时设定,不能修改

上界(Limit)

缓冲区中,存在元素的计数

位置(Position)

下一个要被读或写的元素索引,位置自动由相应的get()和put()更新

标记(Mark)

备忘位置标记,

mark()设定mark=postion

reset()设定position=mark

未读/写数量remaining()

返回position与limit的差

重置reset()

将position的值设置为mark的值,这个方法不会改变mark的值,也不会丢弃mark的值

清空clear()

清空缓冲区,position设置为0,limit设置为capacity,mark设置被丢弃

翻转flip()

将limit设置为position的值,将position设置为0,如果设置了mark会被丢弃,一般在填充完毕后读写数据调用

回退rewind()

position设置为0,limit不变,与翻转类似

array() 返回底层数组实现

底层如果不是数组实现,或者是只读,可能抛出异常

arrayOffset ()

Buffer第一个元素在数组中的偏移量,position+arrayOffset()的值

hasArray()

判断Buffer是否有底层数组实现

hasRemaining

判断position和limit之前是否还有元素

isReadOnly()是否只读

isDirect

buffer判断是否为直接缓冲区

创建缓冲区allocate/allocateDirect/warp

创建HeapByteBuffer/DirectByteBuffer/HeapByteBuffer类

以下不等式在任何情况下都成立

0 <= mark <= position <= limit <= capacity

13.注意:

容量虽然是固定的,但其他三个属性可以再使用缓冲区时改变

buffer允许级联调用:buffer.mark().position(5).reset();

14.示例

A.间接缓冲区

获取缓冲池内容

1
2
3
4
5
String temp="打破玉笼飞彩凤,顿开金锁走蛟龙";
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(temp.getBytes());
byte[] array = byteBuffer.array();
System.out.println(new String(array));

比较缓冲池中的内容

1
2
3
4
5
6
7
8
String temp1="打破玉笼飞彩凤,顿开金锁走蛟龙";
String temp2="打破玉笼飞彩凤,顿开金锁走蛟龙";
ByteBuffer byteBuffer1 = ByteBuffer.allocate(1024);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);
byteBuffer1.put(temp1.getBytes());
byteBuffer2.put(temp2.getBytes());
System.out.println(byteBuffer1.equals(byteBuffer2)); //true
System.out.println(byteBuffer1.compareTo(byteBuffer2)); //0 相等

相等条件:

  1. 两个对象类型相同
  2. 两个对象都剩余同样数量的元素
  3. 在每个缓冲区中应被Get()函数返回的剩余数据元素序列必须一致

如果不满足任意一条,都会返回false

批量移动

1
2
3
4
String temp="飞雪连天射白鹿,笑书神侠倚碧鸳";
char[] chars = temp.toCharArray();
CharBuffer charBuffer = CharBuffer.wrap(chars,0,chars.length);//相当于allocate,底层HeapCharBuffer
System.out.println(charBuffer.array());
B.直接字节缓冲区

allocateDirect();

直接使用本地I/O操作,减少一层copy动作,是JVM中可用的最高效I/O机制

非直接字节缓冲区可以被传递给通道,但是这样消耗性能,如果向一个通道传递一个非直接字节缓冲对象,可能隐含如下操作:

  1. 创建一个临时的直接ByteBuffer对象
  2. 将非直接缓冲区内容复制到临时缓冲中
  3. 使用临时缓冲区执行低层次的I/O操作
  4. 临时缓冲区对象离开作用于,最终成为被回收的无用数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String temp = "This is new message";
byte[] bytes = temp.getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
byteBuffer.put(bytes);

byte[] resultByte = new byte[2];//中文会被切坏,此处仅演示功能,不能直接使用
int count=0;
for (int i = 0; i<byteBuffer.position(); i++) {
if (byteBuffer.position()>=resultByte.length){
System.out.print(new String(resultByte));
count=0;
}
resultByte[count]=byteBuffer.get(i);
count++;
}