NIO之Buffer基本原理

在Java NIO中各类Buffer主要用于和NIO Channel进行交互,数据从Channel中读取到Buffer中,从Buffer写入到Channel中。
channel-buffer数据交互

我们可以将Buffer看做内存中的一块区域,我们可以在这块区域上写数据,然后在从中读取。这块内存区域被包装成NIO Buffer对象,提供了一系列的方法使我们操作这块内存变得更简单一些。

Buffer的基本使用

使用Buffer进行读写数据一般会通过下边四个步骤处理:

  • 将数据写到Buffer中
  • 调用buffer.flip()切换为读模式
  • 从Buffer中读取数据
  • 调用buffer.clear()或者buffer.compact()清空或压缩buffer

下边是个简单的Buffer使用的例子

public class FileChannelExam {
    public static void main(String[] args){
        try {

            String path = FileChannelExam.class.getResource("/data/nio-data.txt").getPath();

            // 创建一个文件通道
            RandomAccessFile file = new RandomAccessFile(path, "rw");
            FileChannel channel = file.getChannel();

            // 创建一个字节buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            // 读取数据到buffer
            int len = channel.read(buffer);

            while (len != -1){
                System.out.println("Read " + len);

                // 将写模式转变为读模式,
                // 将写模式下的buffer内容最后位置设为读模式下的limit位置,作为读越界位,同时将读位置设为0
                // 表示转换后重头开始读,同时消除写模式的mark标记
                buffer.flip();

                // 判断当前读取位置是否到达越界位(position < limit)
                while (buffer.hasRemaining()){
                     // 读取当前position的字节(position++)
                    System.out.println(buffer.get());
                }

                // 清空当前buffer内容
                buffer.clear();
                len = channel.read(buffer);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

当我们将数据写入buffer时,buffer会记录我们写入了多少数据,当需要读取数据的时候,需要调用flip()方法将buffer从写模式切换到读模式,在读模式下,buffer允许用户读取已经写入buffer的所有数据。 

Buffer 以下实现

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

想获取一个Buffer对象的话首先要进行buffer对象的分配,每一个Buffer类都有一个allocate的方法。具体用法:

ByteBuffer byteBuffer1 = ByteBuffer.allocate(1024);

观察如上的代码,创建一个具体的Buffer对象都要使用相关的Buffer类的allocate方法。到这里,可能就会有同学有疑问了,为什么不能直接new出来。下面,进入ByteBuffer源码里看一眼。

public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer>
public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }

估计可以看出一些门道来了,ByteBuffer是一个抽象类,allocate是抽象类中的静态方法,通过allocate方法构造的是HeapByteBuffer对象。

接下来,为了理解buffer的工作原理,看下buffer的三个属性

  • capacity【容量】
  • position【位置】
  • limit【界限】

关于这三个属性之间的关系,找了一个比较易懂的图片来解释下

左边的代表写入的时候,右边的代表读取的时候

当写入的时候,capacity的大小代表利用allocate初始化时候的大小,代表这个缓存块的容量,最多只能向这个缓存块中放入capacity个char,long,int,byte等等。当缓存块满了的时候需要将其清空,才能继续往里面写数据

position:代表当前位置,初始化值为0,当一个数据写入buffer中的时候,position会移动到下一个可插入的buffer单元,因此,position的最大值为capacity-1,

limit:在写的模式下面,limit表示你最多可以向buffer里面写入多少个buffer数据,在写模式下面,limit=capacity。

当切换到读模式下面,代表最多能读取到多少数据。因此当切换到读模式下面,limit会被置为写模式下面的position值。因此,你能读取到在写模式下面写入的所有值。

以上的话,就是这三个属性的具体介绍。理解了这三个属性之后,对于理解buffer的一些方法有很大的用处。

Buffer常用方法

1.申请一个Buffer

在使用Buffer之前,你必须为它申请一块内存空间,每个Buffer的实现类都实现了它自己的allocate()方法来完成内存申请的工作,下面的代码展示了如何创建一个Buffer对象。

// 创建一个1024字节的ByteBuffer对象
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

// 创建一个1024字符的CharBuffer对象
CharBuffer charBuffer = CharBuffer.allocate(1024);
// 以上都是堆上分配内存

// 如果在直接内存分配
ByteBuffer byteBuffer = CharBuffer.allocateDirect(1024);

2.写入数据到buffer中

向buffer写入数据有两种方法:

  1. 通过Channel向Buffer中写入数据
  2. 直接写入数据到Buffer
// 通过Channel写入,即将Channel数据读取到buffer中
int len = channel.read(buffer);

// 直接写入,调用put方法
buffer.put(127);

需要注意的是,put()方法有多重实现,你可以使用不同的方式写入数据,例如:写入到特定的位置,写入一个字节数组等。

3.flip()写切换到读

       flip()方法是将buffer由写模式切换到读模式的方法,flip()方法将position重置为0,将limit设置为已经写入的最大位置,也就是position从标记写入位置改变为标记都区位置;源码中flip()方法的实现如下:

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

4.从buffer中读取数据

从buffer中读取数据同样有两种方法:

  1. 通过Channel从Buffer中读取数据
  2. 直接从Buffer中读取数据
// 使用Channel读取数据,即将数据写入Channel
int len = channel.write(buffer);

// 直接读取数据
byte data = buffer.get();

同样get()方法也有很多重载实现,允许我们使用不同的方法读取数据,可以参考Buffer实现类文档查看更多细节。

5.倒回rewind()

rewind()倒回方法只是将position重置为0,limit仍保持原值;一般在读模式下使用可以让我们重复读取buffer中的数据;在写模式下则会导致重新写入数据(类似于置空了buffer)。源码:

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

6.clear()和compact()

一旦完成读操作,我们需要让buffer重新改变为写模式,以便可以重新向buffer写入新的数据,buffer通过clear()和compact()来完成。
当调用clear的时候position会重置为0,limit设置为capacity,虽然buffer中的数据未被擦除,但逻辑上相当于buffer被清空了,因为新写入的数据会覆盖旧数据,如果buffer中还有未被读取的数据,这些数据依然会被覆盖!
clear源码实现,可以和rewind的比较一下,看有什么区别:

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

如果希望保留buffer中还未读取的数据,只是清理已读取的数据来腾出写入空间,则可以通过compact()方法实现;compact()方法会拷贝未读入的数据到buffer内存空间的起始位置,然后将position设置到未读取数据元素的最后位置,limit值仍然为buffer的capacity,现在buffer就有了更多的空间供写入数据。我们可以看一下HeapByteBuffer的源代码实现:

public ByteBuffer compact() {
    //复制数据
    System.arraycopy(hb, ix(position()), hb, ix(0), remaining());  
    // 重置position位置   
    position(remaining());
    // limit设置为capacity      
    limit(capacity());              
    discardMark();
    return this;
}

7.mark()和reset()

mark和reset方法是配合使用的一组方法,你可以通过mark()方法标记buffer中的一个位置,经过读写操作后position位置会改变,然后你就可以使用reset()方法使position位置回到mark()方法标记的位置。

buffer.mark();
...; // 读或写操作
buffer.reset();  // 回到标记位置

8.equals()

可以通过equals和compareTo()方法来比较两个buffer,equals判断条件:

  • 1. 两个buffer是否同一类型;
  • 2. 是否持有相同数量的数据;
  • 3. 持有的数据是否每个元素都相同。

9.Scatter和Gather

Java NIO内置支持分散(Scatter)和聚集(Gather),Scatter和Gather是用于读取和写入Channel的概念。

Scatter是指从一个Channel中分散读取数据到一个或多个Buffer的操作,因此Channel将数据分散到多个Buffer中;
Gather是指将一个或多个Buffer中的数据写入一个Channel的操作,一次Channel可以从多个Buffer中收集数据。
Scatter和Gather在解决传输数据拥有多个部分需要进行分离的场景下有很大的用处;比如,一个消息数据中包含消息头(header)和消息体(body)两部分,我们就可以将消息头和消息体分别读入不同的Buffer保存,使得消息的分离处理更加方便。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值