Java NIO 学习笔记一

缓冲区操作

进程执行I/O操作,归结起来就是向操作系统发出请求,它要么把缓存区例的数据排干(写),要么用数据把数据区填满(读)。进程使用这一机制处理所有数据进出操作。
m7IfQH.png
进程使用read()系统调用,要求其缓存区被填满。内核随即向磁盘控制器发出命令,要求其从磁盘读取数据。通过DMA技术直接将磁盘中的数据写入内核内存缓存区,一旦磁盘控制器把缓存区填满,内存立即把数据从内核空间的里你是缓冲区拷贝到进程执行read()调用时指定的缓冲区。

发散/汇聚

根据发散/汇聚的概念,进程只需要一个系统调用,就能把一连串的缓冲区地址传递给操作系统。然后,内核就能顺序填充或排干多个缓冲区,读的时候将数据发散到多个用户空间缓冲区,写的时候再从多个缓冲区把数据汇聚起来。
m7oYnA.png

利用虚拟内存避免一些拷贝

前面提到设备控制器不能使用DMA直接存储到用户空间,需要从内核空间拷贝到用户空间。但在使用内存多重映射技术可以避免这种拷贝。
现代操作系统都使用虚拟内存,它有极大优点:

  1. 多个虚拟地址可以映射到同一个物理地址。
  2. 虚拟内存空间可能大于实际可用的硬件内存。

m7T9ud.png
借助虚拟内存的特点,将内核空间中的缓冲区的虚拟地址和用户空间的缓冲区虚拟地址映射到一个物理地址(即内存多重映射技术)。
但这也是有前提的,内核与用户缓冲区必须使用相同的页对齐,缓冲区的大小还必须是磁盘控制块大小的倍数。

采用分页技术的操作系统执行IO操作

  1. 确定请求的数据分布在文件系统的哪些页,这些页不一定都是连续的
  2. 在内核空间种分配足够的页,以容纳文件系统页
  3. 在内存页与磁盘上的文件系统页之间建立映射
  4. 为每个页产生一个缺页异常
  5. 虚拟内存系统俘获缺页异常,调用相应的缺页处理程序,将文件系统页调入主存
  6. 页面调入成功后,文件系统队原始数据进行解析,获取文件内容和属性信息。

文件系统页也会和其它内存页一样被缓存在主存z

内存映射文件

m7sbJ1.png
内存映射IO使用文件系统建立用户空间直到可用文件系统页的虚拟内存映射。
当用户进行触碰到映射内存空间时,会自动产生页错误,从而将文件系统从磁盘读进主存。如果用户修改了映射内存空间时,相应的页会被标记为脏页,随后就会将更改持久化到磁盘。

其优点:

  1. 用户进程直接将文件数据当作内存。
  2. 自动产生页错误,将文件数据从磁盘读入主存
  3. 操作系统的虚拟内存子系统可以对这些页进行智能高速缓存
  4. 数据总是按页对齐的
  5. 大型文件使用映射可以节约内存。

文件锁定机制

文件锁定机制允许一个进程阻止其它仅从存取某文件,或限制其存取方式。
文件锁定的锁定区域可以是整个文件也可ui细致到单个字节。

共享锁和独占锁

多个共享锁可以同时对同一文件区域发生作用;独占锁要求相关区域不能有其它锁定在起作用。

共享锁和独占锁的经典应用 --- 控制读取共享文件的更新
某个进程要读取文件,就要先取得相关区域的共享锁。其它希望读取相同文件区域的进程也会请求共享锁。多个进程得以并行读取,互不影响。如果在此时有其它进行想要更新文件,那么它需要请求独占锁,然后该进行会进入阻滞状态,直到既有锁定(共享的,独占的)全部解除它才能拿到独占锁。一旦该进程拿到了独占锁,其它所有的共享锁读取线程间进入阻滞状态,直到独占锁解除。

流I/O

并非所有的I/O都是面向块的,也有流I/O,其原理模仿了通道。I/O流字节必须顺序读取。流的传输一般比块设备慢,进程用于间歇性输入。

Buffer类

一个Buffer对象是固定容量的数据的容器。在这里数据可以被存储并在之后用于检索。

Buffer类的层次图

m7bFER.png

属性

    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity; 
  1. 容量(capacity)
  2. 上界(limit)
    缓冲区第一个不能被读或写的元素。或者说是现存元素的计数。其指明了缓冲区中有效内容末端的位置。
  3. 标记(mark)
    一个备忘的位置。使用mark()来设定mark=postion.调用reset()设定position=mark
    这四个属性之间遵循的关系为:
    0<= mark <= position <= limit <= capacity

重要方法

put()

put方法就是将元素加入缓冲区,值得注意的是,put方法只改变position,不会改变limit和capacity。
mHPgtU.png

flip()

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

filp()方法将一个能够继续添加数据的填充状态的缓冲区翻转为一个准备读出元素的释放状态。根据代码filp()所作的工作不言而喻。

rewind()

rewind方法和flip方法非常类似,但是它不会影响上界属性,可以利用rewind方法来重新读已经被翻转的缓冲区中的数据。

hasRemaining()

        //切换到读模式
        buffer.flip();
        int count=buffer.remaining();
        System.out.println("当前位置距离上界还有:"+count);
        
        while (buffer.hasRemaining()){
            System.out.print(buffer.get()+" ");
        }

hasRemaining()可以判断当前位置是否已经达到buffer的上界。remaining()可以获取当前位置与上界的距离。

compact()

当需要从buffer中释放一部分已经被读取过的数据时,可以使用compact方法,他会将为读的数据元素下移动使得第一个元素的索引为0.该方法在复制数据的场景下,比使用get方法和put方法更加的高效。

关于标记的一些注意点

在未设置标记之前,mark是等于-1的。此时如果调用reset()会抛出InvalidMarkExceptioin异常。值得注意的是由许多方法都是会将mark重置为-1的。如:rewind(),flip(),clear()等。

缓冲区相等

两个缓冲区相等的条件:

  1. 对象类型相同
  2. 两个对象剩余同样的元素(剩余是指position到limit之间的元素)

被认为相等的两个缓冲区
mbGwtO.png

批量的移动

以CharBuffer为例子

public CharBuffer get(char[] dst)
//offset参数是填充dst的起点位置
public CharBuffer get(char[] dst,int offset,int lenth);

public final CharBuffer put(char[] src)
public CharBuffer put(char[] src,int offset,int length)
public CharBUffer put(CharBuffer src)

可以使用以下方法高效的读取处理数据

    buffer.flip();
    int[] arr=new int[10];
    while (buffer.hasRemaining()){
        int len=Math.min(arr.length,buffer.remaining());
        buffer.get(arr,0,len);
        //处理数据
        processData(arr,len);
    }

缓存区创建的两个关键方法

public static CharBuffer allocate(int capacity)

public static CharBuffer Wrap(char[] array)
//offset参数用来初始化position参数,length参数用来初始化limit参数
public static CharBuffer wrap(char[] array,int offset,int length)

创建新的缓存区有两种方式,分别是分配或包装操作。
allocate方法采用的分配的方式,他会分类一个数组来存储数据。而wrap方法是将一个数组包装为一个缓冲区。这意味着对这个数组的任何改动都会对这个缓冲区可见。

复制缓冲区

public abstract CharBuffer duplicate();
public abstract CharBuffer asReadOnlyBuffer();
public abstract CharBuffer slice();

使用duplicate方法可以创建一个和原缓冲区共享数据元素的副本缓冲区。它们共享数据元素但是拥有各自的位置,上界和标记属性。

使用asReadOnlyBuffer方法可以创建一个只读的副本缓冲区。它所创建的副本是不允许使用put方法的。

使用slice方法可以创建一个position到limit的元素的一个副本。

值得注意的是以上三种方法都不会在堆中重新分配空间用来存储数据。所以它们都是复制缓冲区的方法。

字节缓冲区

字节顺序

字节顺序分为大端存储和小端存储
大端:
高位存放在内存的低地址位
小端:
低位存放在内存的低地址位

使用ByteOrder order()方法可以获得该缓冲区的字节顺序;使用ByteBuffer order(ByteOrder bo) 方法可以修改缓冲区的字节顺序。

直接缓冲区

字节缓冲区的一大特点就是它可以是直接缓冲区。它可以成为通道所执行的I/O的源头或目标。

非直接缓冲区:非直接缓冲区将缓冲区建立在JVM内存在中。
mO5GXF.png

非直接缓冲区:直接将缓冲区建立在物理内存中,可以提高效率。
mO5N79.png

        //分配直接缓冲区
        ByteBuffer bf=ByteBuffer.allocateDirect(10);
        //判断其是否是直接缓冲区,结果是true
        System.out.println(bf.isDirect());

视图缓冲区

ByteBuffer类允许创建视图来将byte型缓冲区字节数据映射位其它的原始数据类型。视图对象维护它自己的容量、位置、上界和标记,但是和原来的缓冲区共享数据元素。

        ByteBuffer bf=ByteBuffer.allocate(10);
        IntBuffer intBuffer=bf.asIntBuffer();
posted @ 2019-09-01 17:44  zofun  阅读(192)  评论(0编辑  收藏  举报