NIO学习笔记之缓冲区

缓冲区基础

一个Buffer对象是固定数量的数据的容器。其作用是一个存储器,或者分段运输区,在这里数据可被存储并在之后用于检索。缓冲区可以被写满和释放。对于每个非布尔原始数据类型都有一个缓冲区类。尽管缓冲区作用于它们存的原始数据类型,但缓冲区十分倾向于处理字节。非字节缓冲区可以在后台执行从字节或到字节的转换,这取决于缓冲区是如何创建的。

缓冲区的工作与通道紧密联系。通道是I/O传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标。对于离开缓冲区的传输,您想传递出去的数据被置于一个缓冲区,被传送到通道。对于传回缓冲区的传输,一个通道将数据放置在您所提供的缓冲区中。这种在协同对象(通常是您所写的对象以及一到多个 Channel 对象)之间进行的缓冲区数据传递是高效数据处理的关键。

以下是 Buffer 的类层次图。在顶部是通用Buffer类。Buffer定义所有缓冲区类型共有的操作,无论是它们所包含的数据类型还是可能具有的特定行为。这一共同点将会成为我们的出发点。

属性

所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息。它们是
- 容量(Capacity)缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
- 上界(Limit)缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
- 位置(Position)下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。
- 标记(Mark)
一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position =
mark。标记在设定前是未定义的( undefined) 。这四个属性之间总是遵循以下关系:
0 <= mark <= position <= limit <= capacity

缓冲区 API

让我们来看一下可以如何使用一个缓冲区。以下是 Buffer 类的方法签名:

package java.nio;

import java.util.Spliterator;

public abstract class Buffer {

    // 内部属性: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

    long address;

    public final int capacity( )
    public final int position( )
    public final Buffer position (int newPositio
    public final int limit( )
    public final Buffer limit (int newLimit)
    public final Buffer mark( )
    public final Buffer reset( )
    public final Buffer clear( )
    public final Buffer flip( )
    public final Buffer rewind( )
    public final int remaining( )
    public final boolean hasRemaining( )
    public abstract boolean isReadOnly( );
}
关于这个 API 有一点要注意的是,像clear()这类函数,您通常应当返回void,而不是Buffer引用。这些函数将引用返回到它们在(this)上被引用的对象。这是一个允许级联调用的类设计方法。级联调用允许这种类型的代码:
buffer.mark( );
buffer.position(5);
buffer.reset( );
被简写为:
buffer.mark().position(5).reset( );
java.nio 中的类被特意地设计为支持级联调用。您可能已经在 StringBuffer 类中看 到了级联调用的使用。

如果使用级联调用,就能产生简洁,优美,易读的代码。但如果滥用,就会使代码不知所云。当级联调用可以增加可读性并使让您的目标更加明确时使用它。如果使用级联调用会使代码作用不够清晰,那么请不要使用它。请时刻保证您的代码易于他人阅读。

对于API还要注意的一点是 isReadOnly()函数。所有的缓冲区都是可读的,但并非所有都可写。每个具体的缓冲区类都通过执行isReadOnly()来标示其是否允许该缓存区的内容被修改。一些类型的缓冲区类可能未使其数据元素存储在一个数组中。例如MappedByteBuffer的内容可能实际是一个只读文件。您也可以明确地创建一个只读视图缓冲区,来防止对内容的意外修改。对只读的缓冲区的修改尝试将会导致ReadOnlyBufferException抛出。但是我们要提前做好准备。

存取

缓冲区管理着固定数目的数据元素。但在任何特定的时刻,我们可能只对缓冲区中的一部分元素感兴趣。换句话说,在我们想清空缓冲区之前,我们可能只使用了缓冲区的一部分。这时,我们需要能够追踪添加到缓冲区内的数据元素的数量,放入下一个元素的位置等等的方法。位置属性做到了这一点。它在调用put()时指出了下一个数据元素应该被插入的位置,或者当get()被调用时指出下一个元素应从何处检索。聪明的读者会注意到上文所列出的的Buffer API并没有包括get()或put()函数。每一个Buffer类都有这两个函数,但它们所采用的参数类型,以及它们返回的数据类型对每个子类来说都是唯一的,所以它们不能在顶层Buffer类中被抽象地声明。它们的定义必须被特定类型的子类所遵从。对于这一讨论,我们将假设使用具有这里所给出的函数的ByteBuffer类:

扫描二维码关注公众号,回复: 2601103 查看本文章
package java.nio;

import java.util.Spliterator;

public abstract class Buffer {

    public abstract byte get( );
    public abstract byte get (int index);
    public abstract ByteBuffer put (byte b);
    public abstract ByteBuffer put (int index, byte b);
}

get和put可以是相对的或者是绝对的。在前面的程序列表中,相对方案是不带有索引参数的函数。当相对函数被调用时,位置在返回时前进一。如果位置前进过多,相对运算就会抛出异常。对于put(),如果运算会导致位置超出上界,就会抛出BufferOverflowException 异常。对于get(),如果位置不小于上界,就会抛出BufferUnderflowException异常。绝对存取不会影响缓冲区的位置属性,但是如果您所提供的索引超出范围(负数或不小于上界),也将抛出 IndexOutOfBoundsException 异常。

填充

让我们看一个例子。我们将代表“Hello”字符串的 ASCII 码载入一个名为 buffer 的ByteBuffer 对象中。当在图 1 所新建的缓冲区上执行以下代码后,缓冲区的结果状态如下图:
这里写图片描述

图 1
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');

因为我们存放的是字节而不是字符。记住在 java 中,字符在内部以 Unicode 码表示,每个 Unicode 字符占 16 位。本章节的例子使用包含 ascii字符集数值的字节。通过将char强制转换为byte,我们删除了前八位来建立一个八位字节值。这通常只适合于拉丁字符而不能适合所有可能的 Unicode 字符。为了让事情简化,我们暂时故意忽略字符集的映射问题。

既然我们已经在 buffer 中存放了一些数据,如果我们想在不丢失位置的情况下进行一些更改该怎么办呢?put()的绝对方案可以达到这样的目的。假设我们想将缓冲区中的内容从“Hello”的 ASCII 码更改为“Mellow”。我们可以这样实现:

buffer.put(0,(byte)'M').put((byte)'w');

这里通过进行一次绝对方案的 put 将 0位置的字节代替为十六进制数值0x4d,将0x77放入当前位置(当前位置不会受到绝对 put()的影响)的字节,并将位置属性加一。结果如下图2:
这里写图片描述

图 2

翻转

我们已经写满了缓冲区,现在我们必须准备将其清空。我们想把这个缓冲区传递给一个通道,以使内容能被全部写出。但如果通道现在在缓冲区上执行 get(),那么它将从我们刚刚插入的有用数据之外取出未定义数据。如果我们将位置值重新设为 0,通道就会从正确位置开始获取,但是它是怎样知道何时到达我们所插入数据末端的呢?这就是上界属性被引入的目的。上界属性指明了缓冲区有效内容的末端。我们需要将上界属性设置为当前位置,然后将位置重置为 0。我们可以人工用下面的代码实现:

buffer.limit(buffer.position()).position(0);

但这种从填充到释放状态的缓冲区翻转是 API 设计者预先设计好的,他们为我们提供了一个非常便利的函数:

Buffer.flip();

flip()函数将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素的释放状态。rewind()函数与 flip()相似,但不影响上界属性。它只是将位置值设回 0。您可以使用 rewind()后退,重读已经被翻转的缓冲区中的数据。

如果将缓冲区翻转两次会怎样呢?它实际上会大小变为0。按照相同步骤对缓冲区进行操作;把上界设为位置的值,并把位置设为 0。上界和位置都变成 0。尝试对缓冲区上位置和上界都为 0 的 get()操作会导致 BufferUnderflowException 异常。而 put()则会导致 BufferOverflowException 异常。

释放

同样地,如果您接收到一个在别处被填满的缓冲区,您可能需要在检索内容之前将其翻转。例如,如果一个通道的 read()操作完成,而您想要查看被通道放入缓冲区内的数据,那么您需要在调用 get()之前翻转缓冲区。通道对象在缓冲区上调用 put()增加数据;put 和read 可以随意混合使用。
布尔函数 hasRemaining()会在释放缓冲区时告诉您是否已经达到缓冲区的上界。以下是一种将数据元素从缓冲区释放到一个数组的方法。

for (int i = 0; buffer.hasRemaining( ), i++) {
    myByteArray [i] = buffer.get( );
}

作为选择,remaining()函数将告知您从当前位置到上界还剩余的元素数目。您也可以通过下面的循环来释放缓冲区。

int count = buffer.remaining( );
for (int i = 0; i < count, i++) {
    myByteArray [i] = buffer.get( );
}

如果您对缓冲区有专门的控制,这种方法会更高效,因为上界不会在每次循环重复时都被检查(这要求调用一个buffer样例程序)。上文中的第一个例子允许多线程同时从缓冲区释放元素。

缓冲区并不是多线程安全的。如果您想以多线程同时存取特定 的缓冲区,您需要在存取缓冲区之前进行同步(例如对缓冲区 对象进行跟踪)。

一旦缓冲区对象完成填充并释放,它就可以被重新使用了。Clear()函数将缓冲区重置为空状态。它并不改变缓冲区中的任何数据元素,而是仅仅将上界设为容量的值,并把位置设回 0

压缩

public abstract class ByteBuffer
extends Buffer implements Comparable
{
   // This is a partial API listing
   public abstract ByteBuffer compact( );
}

有时,您可能只想从缓冲区中释放一部分数据,而不是全部,然后重新填充。为了实现这一点,未读的数据元素需要下移以使第一个元素索引为 0。尽管重复这样做会效率低下,但这有时非常必要,而API对此为您提供了一个compact()函数。这一缓冲区工具在复制数据时要比您使用 get()和 put()函数高效得多。所以当您需要时,请使用 compact()。

标记

这本章节的开头,我们已经涉及了缓冲区四种属性中的三种。第四种,标记,使缓冲区能够记住一个位置并在之后将其返回。缓冲区的标记在 mark( )函数被调用之前是未定义的,调用时标记被设为当前位置的值。reset( )函数将位置设为当前的标记值。如果标记值未定义,调用 reset( )将导致 InvalidMarkException 异常。一些缓冲区函数会抛弃已经设定的标记(rewind( ),clear( ),以及 flip( )总是抛弃标记)。如果新设定的值比当前的标记小,调用limit( )或 position( )带有索引参数的版本会抛弃标记。

创建缓冲区

创建一个缓冲区的关键函数,对所有的缓冲区类通用

public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable
{
    // This is a partial API listing
    public static CharBuffer allocate (int capacity)
    public static CharBuffer wrap (char [] array)
    public static CharBuffer wrap (char [] array, int offset,
    int length)
    public final boolean hasArray( )
    public final char [] array( )
    public final int arrayOffset( )
}

新的缓冲区是由分配或包装操作创建的。分配操作创建一个缓冲区对象并分配一个私有的空间来储存容量大小的数据元素。包装操作创建一个缓冲区对象但是不分配任何空间来储存数据元素。它使用您所提供的数组作为存储空间来储存缓冲区中的数据元素。如果您想提供您自己的数组用做缓冲区的备份存储器,请调用 wrap()函数:

char [] myArray = new char [100];
CharBuffer charbuffer = CharBuffer.wrap (myArray);

这段代码构造了一个新的缓冲区对象,但数据元素会存在于数组中。这意味着通过调用
put()函数造成的对缓冲区的改动会直接影响这个数组,而且对这个数组的任何改动也会对这个缓冲区对象可见。带有 offset 和 length 作为参数的 wrap()函数版本则会构造一个按照您提供的 offset 和 length 参数值初始化位置和上界的缓冲区。这样做:

CharBuffer charbuffer = CharBuffer.wrap (myArray, 12, 42);

创建了一个 position 值为 12,limit 值为 54,容量为 myArray.length 的缓冲区。

复制缓冲区

如我们刚刚所讨论的那样,可以创建描述从外部存储到数组中的数据元素的缓冲区对象。但是缓冲区不限于管理数组中的外部数据。它们也能管理 其他缓冲区中的外部数据。当一个管理其他缓冲器所包含的数据元素的缓冲器被创建时,这个缓冲器被称为视图缓冲器。大多数的视图缓冲器都是ByteBuffer的视图。在继续前往字节缓冲器的细节之前,我们先将注意力放在所有存储器类型的共同视图上。
视图存储器总是通过调用已存在的存储器实例中的函数来创建。使用已存在的存储器实例中的工厂方法意味着视图对象为原始存储器的内部实现细节私有。数据元素可以直接存取,无论它们是存储在数组中还是以一些其他的方式,而不需经过原始缓冲区对象的 get()/put() API。如果原始缓冲区是直接缓冲区,该缓冲区的视图会具有同样的效率优势。复制缓冲区常用api如下:

public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable
{
    // This is a partial API listing
    public abstract CharBuffer duplicate( );
    public abstract CharBuffer asReadOnlyBuffer( );
    public abstract CharBuffer slice( );
}

字节缓冲区

目前,字节几乎被广泛认为是八个比特位。但这并非一直是实情。在过去的时代,每个字节可以是3到12之间任何个数或者更多个的比特位,最常见的是6到9位。八位的字节来自于市场力量和实践的结合。它之所以实用是因为8位足以表达可用的字符集(至少是英文字符),8是2的三次乘方(这简化了硬件设计),八恰好容纳两个十六进制数字,而且8的倍数提供了足够的组合位来存储有效的数值。

字节顺序

非字节类型的基本类型,除了布尔型都是由组合在一起的几个字节组成的。这些数据类型及其大小总结在下表:

数据类型 大小(以字节表示)
byte 1
char 2
short 2
int 4
long 8
float 4
double 8

每个基本数据类型都是以连续字节序列的形式存储在内存中。例如,32 位的 int 值0x037fb4c7(十进制的 58,700,999)可能会如图 3所显示的那样被塞入内存字节中(内存地址从左往右增加)。注意前一个句子中的“可能”一词。尽管字节大小已经被确定,但字节顺序问题一直没有被广泛认同。表示一个整型值的字节可能在内存中仅仅如图 4 所示的那样被简单地排列。

大端字节顺序

这里写图片描述

图 3

小端字节顺序

这里写图片描述

图 4

多字节数值被存储在内存中的方式一般被称为 endian-ness(字节顺序)。如果数字数
值的最高字节——big end(大端),位于低位地址,那么系统就是大端字节顺序(如图 3 所示)。如果最低字节最先保存在内存中,那么小端字节顺序(如图 4 所示)。字节顺序很少由软件设计者决定;它通常取决于硬件设计。字节顺序的两种类型有时被称为字节性别,在当今被广泛使用。两种方式都具有自身的优势。Intel 处理器使用小端字节顺序涉及。摩托罗拉的 CPU 系列、SUN 的 Sparc 工作站,以及 PowerPC 的 CPU 架构都采用
大端字节顺序。

字节顺序的问题甚至胜过CPU硬件设计。当Internet的设计者为互联各种类型的计算机
而设计网际协议(IP)时,他们意识到了在具有不同内部字节顺序的系统间传递数值数据的
问题。因此,IP协议规定了使用大端的网络字节顺序概念 4。所有在IP分组报文的协议部分
中使用的多字节数值必须先在本地主机字节顺序和通用的网络字节顺序之间进行转换。

在 java.nio 中,字节顺序由 ByteOrder 类封装。

package java.nio;
public final class ByteOrder
{
    public static final ByteOrder BIG_ENDIAN
    public static final ByteOrder LITTLE_ENDIAN
    // JVM 运行的硬件平台的固有字节顺序
    public static ByteOrder nativeOrder( )
    public String toString( )
}

ByteOrder 类定义了决定从缓冲区中存储或检索多字节数值时使用哪一字节顺序的常
量。这个类的作用就像一个类型安全的枚举。它定义了以其本身实例预初始化的两个 public
区域。只有这两个 ByteOrder 实例总是存在于 JVM 中,因此它们可以通过使用–操作符进
行比较。如果您需要知道 JVM 运行的硬件平台的固有字节顺序,请调用静态类函数
nativeOrder()。它将返回两个已确定常量中的一个。调用 toString()将返回一个包含两
个文字字符串 BIG_ENDIAN 或者 LITTLE_ENDIAN 之一的 String。

public abstract class CharBuffer extends Buffer
implements Comparable, CharSequence
{
    // This is a partial API listing
    public final ByteOrder order( )
}

这个函数从 ByteOrder 返回两个常量之一。对于除了 ByteOrder 之外的其他缓冲区类,字节顺序是一个只读属性,并且可能根据缓冲区的建立方式而采用不同的值。除了ByteBuffer,其他通过分配或包装一个数组所创建的缓冲区将从 order()返回与ByteOrder.nativeOrder()相同的数值。这使因为包含在缓冲区中的元素在 JVM 中将会被作为基本数据直接存取。

ByteBuffer 类有所不同:默认字节顺序总是 ByteBuffer.BIG_ENDIAN,无论系统的固有字节顺序是什么。Java 的默认字节顺序是大端字节顺序,这允许类文件等以及串行化的对象可以在任何 JVM 中工作。如果固有硬件字节顺序是小端,这会有性能隐患。在使用固有硬件字节顺序时,将 ByteBuffer 的内容当作其他数据类型存取(很快就会讨论到)很可能高效得多。

很可能您会对为什么 ByteBuffer 类需要一个字节顺序设定这一问题感到困惑。字节就
是字节,对吗?ByteBuffer 对象像其他基本数据类型一样,具有大量便利的函数用于获取和存放缓冲区内容。这些函数对字节进行编码或解码的方式取决于 ByteBuffer 当前字节顺序的设定。ByteBuffer 的字符顺序设定可以随时通过调用以 ByteOrder.BIG_ENDIAN 或ByteOrder.LITTL_ENDIAN 为参数的 order()函数来改变。

public abstract class ByteBuffer extends Buffer
implements Comparable
{
    // This is a partial API listing
    public final ByteOrder order( )
    public final ByteBuffer order (ByteOrder bo)
}

如果一个缓冲区被创建为一个 ByteBuffer 对象的视图(参见 2.4.3 节),那么order()返回的数值就是视图被创建时其创建源头的 ByteBuffer 的字节顺序设定。视图的字节顺序设定在创建后不能被改变,而且如果原始的字节缓冲区的字节顺序在之后被改变,它也不会受到影响。

直接缓冲区

直接缓冲区被用于与通道和固有I/O例程交互。它们通过使用固有代码来告知操作系统直接释放或填充内存区域,对用于通道直接或原始存取的内存区域中的字节元素的存储尽了最大的努力。直接字节缓冲区通常是I/O操作最好的选择。在设计方面,它们支持JVM可用的最高效I/O机制。非直接字节缓冲区可以被传递给通道,但是这样可能导致性能损耗。通常非直接缓冲不可能成为一个本地 I/O 操作的目标。

直接缓冲区时 I/O 的最佳选择,但可能比创建非直接缓冲区要花费更高的成本。直接缓冲区使用的内存是通过调用本地操作系统方面的代码分配的,绕过了标准 JVM 堆栈。建立和销毁直接缓冲区会明显比具有堆栈的缓冲区更加破费,这取决于主操作系统以及 JVM 实现。
直接缓冲区的内存区域不受无用存储单元收集支配,因为它们位于标准 JVM 堆栈之外。

直接 ByteBuffer 是通过调用具有所需容量的 ByteBuffer.allocateDirect()函数
产生的,就像我们之前所涉及的 allocate()函数一样。注意用一个 wrap()函数所创建的被
包装的缓冲区总是非直接的。

public abstract class ByteBuffer
extends Buffer implements Comparable
{
    // This is a partial API listing
    public static ByteBuffer allocate (int capacity)
    public static ByteBuffer allocateDirect (int capacity)
    public abstract boolean isDirect( );
}

视图缓冲区

就像我们已经讨论的那样,I/O 基本上可以归结成组字节数据的四处传递。在进行大数据量的 I/O 操作时,很又可能您会使用各种 ByteBuffer 类去读取文件内容,接收来自网络连接的数据,等等。一旦数据到达了您的 ByteBuffer,您就需要查看它以决定怎么做或者在将它发送出去之前对它进行一些操作。ByteBuffer 类提供了丰富的 API 来创建视图缓冲区。

视图缓冲区通过已存在的缓冲区对象实例的工厂方法来创建。这种视图对象维护它自己的属性,容量,位置,上界和标记,但是和原来的缓冲区共享数据元素。我们已经在前面见过了这样的简单例子,在例子中一个缓冲区被复制和切分。但是 ByteBuffer 类允许创建视图来将 byte 型缓冲区字节数据映射为其它的原始数据类型。例如,asLongBuffer()函数创建一个将八个字节型数据当成一个 long 型数据来存取的视图缓冲区。下面列出的每一个工厂方法都在原有的 ByteBuffer 对象上创建一个视图缓冲区。

public abstract class ByteBuffer
extends Buffer implements Comparable
{
    // This is a partial API listing
    public abstract CharBuffer asCharBuffer( );
    public abstract ShortBuffer asShortBuffer( );
    public abstract IntBuffer asIntBuffer( );
    public abstract LongBuffer asLongBuffer( );
    public abstract FloatBuffer asFloatBuffer( );
    public abstract DoubleBuffer asDoubleBuffer( );
}

数据元素视图

ByteBuffer 类提供了一个不太重要的机制来以多字节数据类型的形式存取 byte 数据组。ByteBuffer 类为每一种原始数据类型提供了存取的和转化的方法:

public abstract class ByteBuffer
extends Buffer implements Comparable
{
public abstract char getChar( );
public abstract char getChar (int index);
public abstract short getShort( );
public abstract short getShort (int index);
public abstract int getInt( );
public abstract int getInt (int index);
public abstract long getLong( );
public abstract long getLong (int index);
public abstract float getFloat( );
public abstract float getFloat (int index);
public abstract double getDouble( );
public abstract double getDouble (int index);
public abstract ByteBuffer putChar (char value);
public abstract ByteBuffer putChar (int index, char value);
public abstract ByteBuffer putShort (short value);
public abstract ByteBuffer putShort (int index, short value);
public abstract ByteBuffer putInt (int value);
public abstract ByteBuffer putInt (int index, int value);
public abstract ByteBuffer putLong (long value);
public abstract ByteBuffer putLong (int index, long value);
public abstract ByteBuffer putFloat (float value);
public abstract ByteBuffer putFloat (int index, float value);
public abstract ByteBuffer putDouble (double value);
public abstract ByteBuffer putDouble (int index, double value);
}

这些函数从当前位置开始存取 ByteBuffer 的字节数据,就好像一个数据元素被存储在那里一样。根据这个缓冲区的当前的有效的字节顺序,这些字节数据会被排列或打乱成需要的原始数据类型。比如说,如果 getInt()函数被调用,从当前的位置开始的四个字节会被包装成一个 int 类型的变量然后作为函数的返回值返回。

总结

介绍了 java.nio 包中的各种缓冲区。缓冲区对象使接下来几章要介绍的高吞吐率 I/O 得以实现

缓冲区属性所有缓冲区共有的属性在前面的分析涉及。这些属性描述了缓冲区的当前状态,影响了缓冲区的表现。在这一节,我们也学习了怎样改变缓冲区的状态,以及如何增加及去除数据元素。

缓冲区有许多类型。创建缓冲区的方式由缓冲区的使用方式和使用地点决定。

字节缓冲区虽然缓冲区能够创建来缓冲除了布尔类型的原始数据类型数据,字节缓冲区却具有其他缓冲区类型没有的特征。只有字节缓冲区能够与通道共同使用。

猜你喜欢

转载自blog.csdn.net/Pengjx2014/article/details/78986288
今日推荐