Java NIO 缓冲区简介

        NIO 是New IO 的简称,从JDK1.4开始添加支持,用来解决传统IO的问题,比如并发,高效传输等。NIO作为传统IO的一个补充及优化,在解决一些传统IO无法应对的场景非常有效,而且对于NIO的掌握也对于开发人员是一种技能水平的提升,所以就同我一起来学习一下NIO的相关知识吧。

        通过前文我们对于Java I/O的结构已经有了一个比较清晰的认知,Java引入了一个新包java.nio。以下是java.nio的包目录结构:
        java.nio 包定义了缓冲区类Buffer及相关子类,这些类适用于所有 NIO API;
        java.nio.charset 包中定义了字符集相关 API;
        java.nio.channels 包中定义了信道和选择器相关 API;
        java.nio.charsetspi java.nio.channels.spi 子包的内容可用于扩展平台的默认实现或构造替代实现,说白了就是只有那些需要定义新的选择器提供者的开发人员才应直接使用spi包;

        包结构如下:

java.nio 
  |--java.nio.channels 
        |--java.nio.channels.spi 
  |--java.nio.charset 
        |--java.nio.charset.spi 

        在NIO中存在着一些与传统I/O迥异的设计思想及概念,NIO中存在着缓存区(Buffer),字符集(charset),通道(Channel),选择器(Selector)等概念,所以学习NIO前需要理解他们,并且清楚他们的作用。

        NIO中的相关概念:

        1)缓冲区:缓冲区可以理解为数据容器,用于承载数据; 
        2)字符集、解码器编码器:利用它们在字节和 Unicode 字符之间进行转换;
        3)通道:用于连接执行 IO 操作的实体;
        4)择器、选择键:它们与可选择通道(已注册通道)一起定义了多路的、无阻塞的I/O 模式。

        本文从缓冲区开始逐一对NIO中的概念及实现进行学习。 

 
        1.缓冲区简介
        缓冲区是一个固定大小且指定基本类型的数据容器,缓冲区是特定基本类型元素的线性有限序列。除内容外,缓冲区的基本属性还包括容量、限制和位置:

        容量(capacity):缓冲区所包含的元素的数量,缓冲区的容量不能为负并且不能更改。

        限制(limit):缓冲区中有效位置的数目,也就是限制缓冲区的可使用大小。缓冲区的限制不能为负,并且不能大于其容量。

        位置(position):是下一个要读取或写入的元素的索引(index)。缓冲区的位置不能为负,并且不能大于其限制。 通俗理解就是从哪个位置开始写入或读取。

        缓冲区的设计实现类为Buffer,Buffer类是一个抽象类,其中包含缓冲区一些的基本属性和方法,上述三种概念的对应实现为:

capacity(容量)
public final int capacity()
返回:此缓冲区的容量

position(位置)
public final int position()
返回:此缓冲区的位置

limit(限制)
public final int limit() 
返回:此缓冲区的限制

        2.缓冲区种类

        缓冲区的最上层设计实现为Buffer类,在Buffer类下层还包含了ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer七个子类:


        这七个子类分别是除 boolean 基本类型外几种Java基本类型所对应的缓冲区实现:

Byte --> ByteBuffer
Char --> CharBuffer
Double --> DoubleBuffer
Float --> FloatBuffer
Int --> IntBuffer
Long --> LongBuffer
Short --> ShortBuffer

         缓冲区按基本类型分成了七种,分别用于对应存储不同类型的数据,其中ByteBuffer 比较特殊,ByteBuffer 可以转换成其他6种缓冲区类型,而其他6种类型间无法转换。

        3.数据操作
        每个缓冲区子类都定义了两种(get)和放置(put)操作,被称为“相对”和“绝对”,其实也就是是否指定index索引位置,如:
        get():相对获取,会读取此缓冲区当前位置(position)的数据,然后该位置(position)递增。
        get(int index):从指定索引处获取数据,获取数据不会改变位置(position)值。

        “相对”操作读取或写入一个或多个元素时,会从从当前位置(position)开始,然后将位置增加所传输的元素数。如果请求的传输超出限制,则相对获取(get)操作将抛出 BufferUnderflowException,相对放置(put)操作将抛出BufferOverflowException;这两种情况下,都讲没有数据被传输。

        “绝对”操作采用指定元素索引方式调用,该操作不会影响位置(position)。如果索引参数超出限制,绝对获取(get)操作和放置(put)操作将抛出 IndexOutOfBoundsException。


        4.标记和重置
        缓冲区的标记(mark)是一个索引,在调用 reset 方法时会将缓冲区的位置重置为该索引。我们不需要初始化定义标记,但在一旦定义了标记时,不能将其定义为负数,并且不能让它大于位置。如果定义了标记,则在将位置或限制调整为小于该记的值时,该标记将被丢弃。如果未定义标记,那么调用 reset 方法将导致抛出 InvalidMarkException。


        5.不变式
        不变式可以理解为缓冲区各属性间永恒关系,标记、位置、限制和容量值遵守以下不变式:

0 <= 标记 <= 位置 <= 限制 <= 容量


        新创建的缓冲区总有一个数值为0 的位置和一个未定义的标记。初始限制可以为 0,也可以为其他值,这取决于缓冲区类型及其构建方式。一般情况下,缓冲区的初始内容是未定义的。


        6.只读缓冲区
        每个缓冲区都是可读取的,但并非每个缓冲区都是可写入的。可以调用其 isReadOnly 方法确定缓冲区是否为只读。每个缓冲区类的转变方法都被指定为可选操作,当对只读缓冲区调用时,将抛出 ReadOnlyBufferException。只读缓冲区不允许更改其内容,但其标记、位置和限制值是可变的。
        通过asReadOnlyBuffer()方法可以轻松的将缓冲区转换成只读缓冲区,对于保护数据很有用。在将缓冲区传递给某个对象的方法时,您无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以保证该缓冲区不会被修改。
        注意:无法将只读的缓冲区再次转换为可写的缓冲区。

        7.“直接”与“非直接”缓冲区

        缓冲区要么是直接的,要么是非直接的。直接缓冲区是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。创建ByteBuffer 缓冲区时可以通过方法allocateDirect(int capacity)来创建直接缓冲区,如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
        直接字节缓冲区(ByteBuffer)通过调用ByteBuffer 的allocateDirect(int capacity) 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
        直接字节缓冲区(ByteBuffer)还可以通过 mapping 将文件区域直接映射到内存中来创建。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
        除ByteBuffer 外其他缓冲区实现通过 wrap 方法创建的 long 缓冲区都是非直接的。当且仅当字节缓冲区本身为直接时,作为字节缓冲区的视图创建的其他缓冲区才是直接的。通过调用 isDirect 方法可以确定该缓冲区是否为直接的。 相关实例会在后面展现。

       其他6种类型缓冲区并不含有allocateDirect(int capacity) 工厂方法,所以他们无法通过此方法直接来直接创建缓冲区,想要创建这几种类型的直接缓冲区就需要通过ByteBuffer 类的as****Buffer方法转换:

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);   
System.out.println(byteBuffer.isDirect());   
CharBuffer charBuffer = byteBuffer.asCharBuffer();   
System.out.println(charBuffer.isDirect());   
//打印结果:   
true  
true

        关于直接、非直接缓冲区后面的文章还会深入讲解。


        8.清除、反转和重绕
        除了访问位置、限制、容量值的方法以及做标记和重置的方法外,还定义了以下可对缓冲区进行的操作:

        clear():使缓冲区为一系列新的通道读取或相对放置(put)操作做好准备:它将限制设置为容量大小,将位置设置为 0。

        flip():使缓冲区为一系列新的通道写入或相对获取(get)操作做好准备:它将限制设置为当前位置,然后将位置设置为 0。

        rewind():使缓冲区为重新读取已包含的数据做好准备:它使限制保持不变,将位置设置为 0。


        9.线程安全
        多个当前线程使用缓冲区是不安全的。如果一个缓冲区由不止一个线程使用,则应该通过适当的同步来控制对该缓冲区的访问。

        10.调用链
        指定此类中的方法返回调用它们的缓冲区(否则它们不会返回任何值)。此操作允许将方法调用组成一个链;例如,语句序列:

b.flip();
b.position(23);
b.limit(42);

        可以由以下更紧凑的一个语句代替:

b.flip().position(23).limit(42);

        调用链这种形式在开发中较为常见,我这里就不多说了。

        以上就是对于NIO中缓冲区的一些概念性了解,接下来的几篇文章会更深入的去学习更多的知识及原理。

猜你喜欢

转载自286.iteye.com/blog/2212184
今日推荐