Java NIO 缓冲区

JDK 1.4 中引入的新输入输出 (NIO) 库提供了高速的、面向块的 I/O。Java中原有的面向流的 IO 已经以 NIO 为基础重新进行了实现,因此现在它也可以利用 NIO 的一些特性。NIO提供了对多路复用的支持,主要优势在于一个线程可以处理多个连接,减少线程切换带来的开销。

通道和缓冲区是 NIO 中的核心对象,几乎在每一个 I/O 操作中都要使用它们。


缓冲区

在面向流的 I/O 中,数据直接写入或者读取到流中。在 NIO 中,所有数据都是在缓冲区中处理。

所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息。它们是:

  • 容量(Capacity) 缓冲区能够容纳的数据元素的最大数量,在缓冲区创建时设定,不能改变
  • 上界(Limit) 缓冲区中现存元素的个数
  • 位置(Position) 下一个要被读或写的元素的索引,位置会自动由 get( ) 和 put( ) 方法更新
  • 标记(Mark) 标记某个位置,使得之后可以返回这个位置

四个属性之间总是遵循以下关系: 0 <= mark <= position <= limit <= capacity

缓冲区实质上是一个数组,提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。缓冲区类型有如下几种,对应了基本数据类型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
它们的方法都是类似的,但只有字节缓冲区(ByteBuffer)能够与通道共同使用。

创建缓冲区

以 CharBuffer 为例进行说明。
首先我们需要创建一个 CharBuffer。主要有如下两种方法来创建:

  • CharBuffer allocate(int capacity)
  • CharBuffer wrap(char[] array)
第一种方法从堆空间中分配了一个 char 型数组作为存储器,第二种方法用指定的数组当做缓冲区的存储器。

直接写入和读取缓冲区中的数据主要通过 put() 与 get() 两种方法,get()的几种重载方法如下:

  • char get()
  • char get(int index)
  • CharBuffer get(char[] dst)
  • CharBuffer get(char[] dst, int offset, int length)

put() 的重载方法与之类似。

看下面一个程序,猜一下它的输出结果:

public class CharBuffer_Demo {
    public static void main(String[] args) {
        CharBuffer buffer = CharBuffer.allocate(10);
        buffer.put('h');
        char c = buffer.get();
        System.out.println(c);
    }
}

事实上,输出的是空字符,原因在于使用put之后position自动加1,如下图所示:

为了让 position 重新回到起始位置,有三种方法:
  • public final Buffer flip()
  • public final Buffer clear()
  • public final Buffer rewind()

它们的区别从源码中看的很清楚:

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

flip() 方法可以理解为将写模式转换为读模式,clear() 方法可以理解为重新开始写入,rewind() 方法只是将 position 置0。注意,这三种方法都会清除标记。

对上面的代码进行修改后输出:

public class CharBuffer_Demo {
    public static void main(String[] args) {
        CharBuffer buffer = CharBuffer.allocate(10);
        buffer.put('h');
        buffer.flip();
        char c = buffer.get();
        System.out.println(c);
    }
}

输出结果:

h

缓冲区视图

复制缓冲区主要有以下几种方法:

  • duplicate() 方法:创建一个新缓冲区,两个缓冲区共享数据元素,拥有同样的容量,但每个缓冲区拥有各自的位置、上界和标记属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。如果原始缓冲区为只读或者为直接缓冲区,新的缓冲区会继承这些属性。
  • asReadOnlyBuffer() 方法:生成一个只读的缓冲区视图。
  • slice() 方法:创建一个从原始缓冲区的当前位置开始的新缓冲区,容量与上界是原始缓冲区的剩余元素数量。新缓冲区与原始缓冲区共享一段数据元素子序列,并继承只读和直接属性

slice() 方法使用示例:

public class SliceDemo {
    public static void main(String[] args) {
        CharBuffer buffer = CharBuffer.allocate(10);
        buffer.put("0123456789");
        buffer.position(4).limit(6);
        CharBuffer slice = buffer.slice();
        System.out.println(slice);
    }
}

输出结果:

45

直接缓冲区

直接缓冲区被用于与通道和固有I/O例程交互。它们通过使用固有代码来告知操作系统直接释放或填充内存区域,对用于通道直接或原始存取的内存区域中的字节元素的存储尽了最大的努力。

上面通过 allocate() 或 wrap() 方法建立的缓冲区都属于间接缓冲区,直接缓冲区的建立需要通过 allocateDirect() 方法(只有ByteBuffer具有这个方法)。

直接缓冲区使用的内存由操作系统分配,不直接受GC管理,建立和销毁直接缓冲区所需要的开销更大。

虽然通过 allocateDirect() 方法创建直接缓冲区可以在一定程度上提高效率,但这种方式并不是平台独立的,会因JVM、操作系统、代码实现等因素产生巨大差异,因此需要谨慎使用 allocateDirect() 方法。

猜你喜欢

转载自blog.csdn.net/weixin_43320847/article/details/83043889