基础 | NIO - [Buffer]

@

§1 概述

  • 缓冲区是一块内存空间,可以将它直观的理解为一个数组
  • 这块内存空间直接和 Channel 相连接
  • 可以向这块内存空间中写或读取数据

常用实现

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

常规使用流程

  • Buffer 中写入数据
  • 调用 flip() 方法,将 Buffer 从写方向切换为读方向
  • Buffer 中读取数据
  • 调用 clear()compact() 方法清空 Buffer
    • clear() 清空整个 Buffer
    • compact() 清空 Buffer 中读过的部分

常规使用流程示例

public void read(String path, String file, int buffer) {
    try (
            RandomAccessFile raf = new RandomAccessFile(path + file,"rw");
            // 获取 FileChannel
            FileChannel channel = raf.getChannel()
    ){
        // 创建读取时的 缓冲区
        ByteBuffer buf = ByteBuffer.allocate(buffer);
        // 读取 channel.read(buf)
        for(int count = channel.read(buf); count != -1; count = channel.read(buf)){
            buf.flip();

            //读取后的动作
            for(;buf.hasRemaining();)
                System.out.print((char) buf.get());

            buf.clear();
        }
    } catch (Exception e){ /* 异常处理 */ }
}

§2 重要成员

§2.1 property

mark

  • 标记
  • mark 可以设置一个标记,以供 buffer 在读取时重新回到这个标记

capacity

  • 容量,即 Buffer 针对当前类型的大小
    • 每个 Buffer 中有一个对应属性的数组
    • 这个数组就是 Buffer 存放数据的容器
    • capacity 就是这个数组的 length
  • 当容量写满,就必须在清空后才能继续写入

position

  • 位置,表示即将读写数据的当前位置
  • 从 0 开始,到 capacity-1 为止
  • 每次从 position 读写后, position 会移动到下一个可读写的位置

limit

  • 写模式时,表示最多写入的数据量,即 capacity
  • 读模式时,表示最多读取的数据量,即 position

§2.2 method

Buffer 分配空间
allocate(capacity)

Buffer 写入数据

  • 直接写入,buffer.put(content)
  • 通过 ChannelBuffer 读取,channel.read(buffer)

切换 Buffer 为读模式
flip()
flip() 会将 limit 置为 position 的位置,并将 limit position 置为 0

Buffer 读取数据
get()

重读
rewind()
rewind()position 置为 0,但 limit 不变
因此可以使用此方法重读已经读过的数据

清空
clear()
会清除整个缓冲区的数据,若缓冲区中有未读取的数据,这些数据会被丢弃

压缩
compact()
会清除缓冲区中已经读取过的数据,以便缓冲区可以继续写入
- 复制未读取过的数据到缓冲区开头
- limit 指向缓冲区末尾,即 capacity
- position 指向最后一个未读数据的后面

标记 & 重设标记

  • mark()
    标记一个 Buffer 中的 position
  • reset()
    回到之前标记的 position

缓冲区分片
slice()

  • slice() 可以在当前缓冲区范围内创建一个子缓冲区
  • 子缓冲区相当于当前缓冲区的窗口
  • 子缓冲区范围是 positionlimit 之间

只读缓冲区
asReadOnlyBuffer()

  • asReadOnlyBuffer() 会返回当前缓冲区的只读版本
  • 只读缓冲区在读取前需要先进行一次 rewind()
  • 只读缓冲区与当前缓冲区共享数据,当前缓冲区数据变化时,只读缓冲区同步变化

直接缓冲区
allocateDirect()

  • allocateDirect() 会在直接内存中分配缓冲区
  • 直接缓冲区避免了操作系统读取数据后,将数据写入 JVM 的开销,因此更快
  • 直接缓冲区依赖 Full GC 进行垃圾回收,因此一直不触发 Full GC 可能导致 OOM: direct buffer

内存映射缓冲区

  • 速度远高于常规流、通道
  • 通过映射文件中数据为内存中数组实现
    常规读写是将整个文件载入内存,此方式下只有实际读写内容映射到内存

§3 示例

缓冲区分片

public void subbuffer(){
    IntBuffer buffer = IntBuffer.allocate(16);
    buffer.put(new int[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});

    buffer.position(8);

	// 分片
    IntBuffer sub = buffer.slice();
    for(int i=0;i<sub.capacity();i++){
        sub.put(i,sub.get(i)*10);
    }

    buffer.rewind();
    while(buffer.hasRemaining()){
        System.out.println(buffer.get());
    }
}

只读缓冲区

public void readonly(){
    IntBuffer buffer = IntBuffer.allocate(16);
    buffer.put(new int[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});

	// 只读
    IntBuffer ro = buffer.asReadOnlyBuffer();

    ro.rewind();
    while(ro.hasRemaining()){
        System.out.print(ro.get());
    }
    System.out.println();
    // 重读缓冲区,观察原缓冲区修改后,只读缓冲区同步修改
    buffer.rewind();
    ro.rewind();
    for(int i=0; i<buffer.capacity();i++){
        buffer.put(i,buffer.get(i)*10);
    }
    while(ro.hasRemaining()){
        System.out.print(ro.get());
    }
}

直接缓冲区

public void directBuffer(String srcFilePath,String descFilePath){
    try (
            RandomAccessFile src = new RandomAccessFile(srcFilePath,"r");
            RandomAccessFile desc = new RandomAccessFile(descFilePath,"rw");
            FileChannel read = src.getChannel();
            FileChannel write = desc.getChannel();
    ){
    	// 直接
        ByteBuffer buf = ByteBuffer.allocateDirect(1024);
        for(int length=read.read(buf);;buf.clear(),length=read.read(buf)){
            if(-1==length)
                break;

            buf.flip();
            while(buf.hasRemaining()){
                write.write(buf);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

内存映射缓冲区复制文件

public void mappedByteBuffer(String srcFilePath,String descFilePath){
    try (
            RandomAccessFile src = new RandomAccessFile(srcFilePath,"r");
            RandomAccessFile desc = new RandomAccessFile(descFilePath,"rw");
            FileChannel read = src.getChannel();
            FileChannel write = desc.getChannel();
    ){
        long pos = 0; // 每轮读起始位置
        long max = read.size(); // 文件长度
        long size = Math.min(1024,max); // 缓冲区大小,又可能文件本身还没缓冲区大
        for(;pos<max;pos += size){
        	// 只读内存映射缓冲区用来读,下面是个读写内存映射缓冲区用来写
        	// 每一轮起始位置是上一轮起始位置+上一轮缓冲区大小
        	// 每一轮映射大小是 缓冲区大小 和 文件长度-当前起始位置 的较小值,又可能最后一次映射不足一个缓冲区
            MappedByteBuffer ib = read.map(FileChannel.MapMode.READ_ONLY,pos,Math.min(size,max-pos));
            MappedByteBuffer ob = write.map(FileChannel.MapMode.READ_WRITE,pos,Math.min(size,max-pos));
            // 真正的复制就这一句
            ob.put(ib);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
posted @ 2025-05-20 14:51  问仙长何方蓬莱  阅读(12)  评论(0)    收藏  举报