NIO Buffer (死磕2)

【正文】JAVA NIO 死磕2: 

NIO Buffer


1. Java NIO Buffer

Buffer是一个抽象类,位于java.nio包中,主要用作缓冲区。Buffer缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

注意:Buffer是非线程安全类。

1.1. Buffer类型的标记属性

Buffer在内部也是利用byte[]作为内存缓冲区,只不过多提供了一些标记变量属性而已。当多线程访问的时候,可以清楚的知道当前数据的位置。

有三个重要的标记属性:capacity、position、limit。

除此之外,还有一个标记属性:mark,可以临时保持一个特定的position,需要的时候,可以恢复到这个位置。

1.1.1. capacity

作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”。你只能往里写capacity个数据。一旦Buffer满了,就不能再写入。

capacity与缓存的数据类型相关。指的不是内存的字节的数量,而是写入的对象的数量。比如使用的是一个保存double类型的Buffer(DoubleBuffer),写入的数据是double类型, 如果其 capacity 是100,那么我们最多可以写入100个 double 数据.

capacity一旦初始化,就不能不会改变。

原因是什么呢?

扫描二维码关注公众号,回复: 3665401 查看本文章

Buffer对象在初始化时,会按照capacity分配内部的内存。内存分配好后,大小就不能变了。分配内存时,一般使用Buffer的抽象子类ByteBuffer.allocate()方法,实际上是生成ByteArrayBuffer类。

1.1.2. position

position表示当前的位置。position在Buffer的两种模式下的值是不同的。

读模式下的position的值为:

当读取数据时,也是从position位置开始读。当将Buffer从写模式切换到读模式,position会被重置为0。当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

写模式下的position的值为:

在写模式下,当写数据到Buffer中时,position表示当前的写入位置。初始的position值为0,position最大可为capacity – 1。

每当一个数据(byte、long等)写到Buffer后, position会向后移动到下一个可插入数据的可写的位置。

1.1.3. limit

limit表示最大的限制。在Buffer的两种模式下,limit的值是不同的。

读模式下的limit的值为:

读模式下,Buffer的limit表示最多能从Buffer里读多少数据。当Buffer从写切换到读模式时,limit的值,设置成写模式的position 值,也是是写模式下之前写入的数量值。

举一个简单的例子,说明一下读模式下的limit值:

先向Buffer写数据,Buffer在写模式。每写入一个数据,position向后面移动一个位置,值加一。假定写入了5个数,当写入完成后,position的值为5。这时,就可以读取数据了。当开始读取数据时,Buffer切换到读模式。limit的值,先会被设置成写入数据时的position值。这里是5,表示可以读取的最大限制是5个数。

写模式下的limit的值为:

limit表示表示可以写入的数据最大限制。在切换成写模式时,limit的值会被更改,设置成Buffer的capacity,为Buffer的容量。

1.1.4. 总结:

在Buffer的四个属性之间,有一个简单的数量关系,如下:

capacity>=limit>=position>=mark>=0

用一个表格,对着4个属性的进行一下对比:

属性

描述

capacity

容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变

limit

上界,缓冲区中当前数据量

position

位置,下一个要被读或写的元素的索引

mark(位置标记)

调用mark(pos)来设置mark=pos,再调用reset()可以让position恢复到标记的位置即position=mark

1.2. Buffer 类型

在NIO中主要有八种缓冲区类,分别如下:

ByteBuffer

CharBuffer

DoubleBuffer

FloatBuffer

IntBuffer

LongBuffer

ShortBuffer

MappedByteBuffer

wps6DEB.tmp



这些 Buffer 覆盖了能从 IO 中传输的所有的 Java 基本数据类型。其中MappedByteBuffer是专门用于内存映射的一种ByteBuffer)。

1.3. Buffer中的方法

本节结合Buffer的几个方法,做了一个完整的实例,包含了从Buffer实例的获取、写入、读取、重复读、标记和重置等一个系列操作的完整流程。

1.3.1. 获取allocate()方法

为了获取一个 Buffer 对象,我们首先需要分配内存空间。分配内存空间使用allocate()方法。

public static void allocatTest()
{
byteBuffer = IntBuffer.allocate(20);
Logger.info("------------after allocate------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}

byteBuffer = IntBuffer.allocate(20);

这里我们分配了20* sizeof(int)字节的内存空间.

输出的结果如下:

       main |>  分配内存

         allocatTest |>  ------------after allocate------------------

         allocatTest |>  position=0

         allocatTest |>  limit=20

         allocatTest |>  capacity=20

通过结果,可以看到Buffer属性的值。

1.3.2. 写put()方法

调用allocate分配内存后,buffer处于写模式。可以通过buffer的put方法写入数据。put方法有一个要求,需要写入的数据类型与Buffer的类型一致。

接着前面的例子,继续上写入的实例代码:

public static void putTest()
{
for (int i = 0; i < 5; i++)
    {
byteBuffer.put(i);
}
    Logger.info("------------after put------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}

写入5个元素后,输出的结果为:

  main |>  写入

             putTest |>  ------------after putTest------------------

             putTest |>  position=5

             putTest |>  limit=20

             putTest |>  capacity=20

调用了put方法后,buffer处于写模式。写入5个数据后,可以看到,position 变成了5,指向了第6个可以写入的元素位置。

除了在新建的buffer之后,如何将buffer切换成写模式呢?

调用 Buffer.clear() 清空或 Buffer.compact()压缩方法,可以将 Buffer 转换为写模式。

1.3.3. 读切换flip()方法

put方法写入数据之后,可以直接从buffer中读吗?

呵呵,不能。

还需要调用filp()走一个转换的工作。flip()方法是Buffer的一个模式转变的重要方法。简单的说,是写模式翻转成读模式——写转读。

接着前面的例子,继续上flip()方法的例子代码:

public static void flipTest()
{
byteBuffer.flip();
Logger.info("------------after flip ------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}

接着上一步的写入,在调用flip之后,buffer的属性有一些奇妙的变化。

运行上面的程序,输出如下:

 main |>  翻转

            flipTest |>  ------------after flipTest ------------------

            flipTest |>  position=0

            flipTest |>  limit=5

            flipTest |>  capacity=20

注意到没有,position从前一个小节的5,变成了0。而limit的保存了之前的position,从20变成5。

这是为什么呢? 先看其源码,Buffer.flip()方法的源码如下:

public final Buffer flip() {

    limit = position;

    position = 0;

    mark = UNSET_MARK;

    return this;

}

解释一下啊,flip()方法主要是从读模式切换成写模式,调整的规则是:

(1)首先设置可读的长度limit。将写模式下的Buffer中内容的最后位置position值变为读模式下的limit位置值,新的limit值作为读越界位置;

(2)其次设置读的起始位置。将当position值置为0,表示从0位置开始读。转换后重头开始读。

(3)如果之前有mark保存的标记位置,还要消除。因为那是写模式下的mark标记。

1.3.4. 读get() 方法

get()读数据很简单,每次从postion的位置读取一个数据,并且进行相应的buffer属性的调整。

接着前面的例子,继续上读取buffer的例子代码:

public static void getTest()
{
    Logger.info("------------after &getTest 2------------------");
    for (int i = 0; i < 2; i++)
    {
int j = byteBuffer.get();
Logger.info("j = " + j);
}
    Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
Logger.info("------------after &getTest 3------------------");
    for (int i = 0; i < 3; i++)
    {
int j = byteBuffer.get();
Logger.info("j = " + j);
}
    Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}

先读2个,再读3个,输出的Buffer属性值如下:

 main |>  读取 

             getTest |>  ------------after &getTest 2------------------ 

             getTest |>  j = 0 

             getTest |>  j = 1 

             getTest |>  position=2 

             getTest |>  limit=5 

             getTest |>  capacity=20 

             getTest |>  ------------after &getTest 3------------------ 

             getTest |>  j = 2 

             getTest |>  j = 3 

             getTest |>  j = 4 

             getTest |>  position=5 

             getTest |>  limit=5 

             getTest |>  capacity=20 

读完之后,缓存的position 值变成了一个没有数据的元素位置,和limit的值相等,已经不能在读了。

读完之后,是否可以直接写数据呢?

不能。一旦读取了所有的 Buffer 数据,那么我们必须清理 Buffer,让其重新可写,可以调用 Buffer.clear() 或 Buffer.compact()。

1.3.5. 倒带rewind()方法

已经读完的数据,需要再读一遍,可以直接使用get方法吗?

答案是,不能。怎么办呢?

使用rewind() 方法,可以进重复读的设置。rewind()也叫倒带,就像播放磁带一样,倒回去,重新播放。

接着前面的例子,继续上重复读的例子代码:

public static void rewindTest()
{
byteBuffer.rewind();
Logger.info("------------after flipTest ------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}

实例的结果如下:

 main |>  重复读

          rewindTest |>  ------------after flipTest ------------------

          rewindTest |>  position=0

          rewindTest |>  limit=5

          rewindTest |>  capacity=20

flip()方法主要是调整Buffer的 position 属性,调整的规则是:

(1)position设回0,所以你可以重读Buffer中的所有数据;

(2)limit保持不变,数据量还是一样的,仍然表示能从Buffer中读取多少个元素。

Buffer.rewind()方法的源码如下:

public final Buffer rewind() {

position = 0;

mark = -1;

return this;

}

看到了实现的源码应该就会清楚flip()的作用了。rewind()方法与flip()很相似,区别在于rewind()不会影响limit,而flip()会重设limit属性值。

1.3.6. mark( )和reset( )

Buffer.mark()方法将当前的 position 的值保存起来,放在mark属性中,让mark属性记住当前位置,之后可以调用Buffer.reset()方法将 position 的值恢复回来。

Buffer.mark()和Buffer.reset()方法是一一配套使用的。都是需要操作mark属性。

在重复读的实例代码中,读到第3个元素,使用mark()方法,设置一下mark 属性,保存为第3个元素的位置。

下面上实例,演示一下mark和reset的结合使用。

实例继续接着上面的rewind倒带后的buffer 状态,开始reRead重复读,实例代码如下:

public static void reRead()
{
    Logger.info("------------after reRead------------------");
    for (int i = 0; i < 5; i++)
    {
int j = byteBuffer.get();
Logger.info("j = " + j);
        if (i == 2)
        {
byteBuffer.mark();
}
    }
    Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}

然后接着上一段reset()实例代码,如下:

public static void afterReset()
{
    Logger.info("------------after reset------------------");
byteBuffer.reset();
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}

上面我们调用 mark() 方法将当前的 position 保存起来(在读模式,因此保存的是读的 position)。接着使用 reset() 恢复原来的读 position,因此读 position 就为3,可以再次开始从第2个元素读取数据.

输出的结果是:

 afterReset |>  ------------after reset------------------

          afterReset |>  position=3

          afterReset |>  limit=5

          afterReset |>  capacity=20

调用reset之后,position的值为3,表示可以从第三个元素开始读。

Buffer.mark()和Buffer.reset()其实很简答,其源码如下:

public final Buffer mark() {

mark = position;

return this;

}

public final Buffer reset() {

int m = mark;

if (m < 0)

throw new InvalidMarkException();

position = m;

return this;

}
1.3.7. clear()清空

clear()方法的作用有两种:

(1)写模式下,当一个 buffer 已经写满数据时,调用 clear()方法,切换成读模式,可以从头读取 buffer 的数据;

(2)读模式下,调用 clear()方法,将buffer切换为写模式,将postion为清零,limit设置为capacity最大容量值,可以一直写入,直到buffer写满。

接着上面的实例,使用实例代码,演示一下clear方法。

代码如下:

public static void clearDemo()
{
    Logger.info("------------after clear------------------");
byteBuffer.clear();
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}

运行之后,结果如下:

main |>  清空

           clearDemo |>  ------------after clear------------------

           clearDemo |>  position=0

           clearDemo |>  limit=20

           clearDemo |>  capacity=20

在clear()之前,buffer是在读模式下。clear()之后,可以看到,清空了position 的值,设置为起始位置。

clear 方法源码:

public final Buffer clear() {

    position = 0;

    limit = capacity;

    mark = -1;

    return this;

}

根据源码我们可以知道,clear 将 positin 设置为0,将 limit 设置为 capacity。

1.4. Buffer 的使用

1.4.1. 使用的基本步骤

总结一下,使用 NIO Buffer 的步骤如下:

一:将数据写入到 Buffer 中;

二:调用 Buffer.flip()方法,将 NIO Buffer 转换为读模式;

三:从 Buffer 中读取数据;

四:调用 Buffer.clear() 或 Buffer.compact()方法,将 Buffer 转换为写模式。

当我们将数据写入到 Buffer 中时,Buffer 会记录我们已经写了多少的数据;当我们需要从 Buffer 中读取数据时,必须调用 Buffer.flip()将 Buffer 切换为读模式。

1.4.2. 完整的实例代码
package com.crazymakercircle.iodemo.base;

import com.crazymakercircle.util.Logger;

import java.nio.IntBuffer;

public class BufferDemo
{
static IntBuffer byteBuffer = null;

public static void allocatTest()
{
byteBuffer = IntBuffer.allocate(20);

Logger.info("------------after allocate------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}

public static void putTest()
{
for (int i = 0; i < 5; i++)
{
byteBuffer.put(i);

}

Logger.info("------------after putTest------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());

}

public static void flipTest()
{

byteBuffer.flip();
Logger.info("------------after flipTest ------------------");

Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}

public static void rewindTest()
{

byteBuffer.rewind();
Logger.info("------------after flipTest ------------------");

Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}

public static void getTest()
{


Logger.info("------------after &getTest 2------------------");
for (int i = 0; i < 2; i++)
{
int j = byteBuffer.get();
Logger.info("j = " + j);
}


Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());

Logger.info("------------after &getTest 3------------------");

for (int i = 0; i < 3; i++)
{
int j = byteBuffer.get();
Logger.info("j = " + j);
}

Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());

}

public static void reRead()
{


Logger.info("------------after reRead------------------");
for (int i = 0; i < 5; i++)
{
int j = byteBuffer.get();
Logger.info("j = " + j);

if (i == 2)
{
byteBuffer.mark();
}
}


Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());

}

public static void afterReset()
{


Logger.info("------------after reset------------------");

byteBuffer.reset();
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());

}

public static void clearDemo()
{


Logger.info("------------after clear------------------");

byteBuffer.clear();
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());

}

public static void main(String[] args)
{
Logger.info("分配内存");

allocatTest();

Logger.info("写入");
putTest();

Logger.info("翻转");

flipTest();

Logger.info("读取");
getTest();

Logger.info("重复读");
rewindTest();
reRead();

Logger.info("make&reset写读");

afterReset();
Logger.info("清空");

clearDemo();


}
}




源码:


代码工程:  JavaNioDemo.zip

下载地址:在疯狂创客圈QQ群文件共享。


疯狂创客圈:如果说Java是一个武林,这里的聚集一群武痴, 交流编程体验心得
QQ群链接:
疯狂创客圈QQ群


无编程不创客,无案例不学习。 一定记得去跑一跑案例哦


JAVA NIO 死磕全目录


1. JAVA NIO简介
1.1. NIO 和 OIO 的对比
1.2. 阻塞和非阻塞
1.3. Channel
1.4. selector
1.5. Java NIO Buffer
2. Java NIO Buffer
2.1. Buffer类型的标记属性
2.1.1. capacity
2.1.2. position
2.1.3. limit
2.1.4. 总结:
2.2. Buffer 类型
2.3. Buffer中的方法
2.3.1. 获取allocate()方法
2.3.2. 写put()方法
2.3.3. 读切换flip()方法
2.3.4. 读get() 方法
2.3.5. 倒带rewind()方法
2.3.6. mark( )和reset( )
2.3.7. clear()清空
2.4. Buffer 的使用
2.4.1. 使用的基本步骤
2.4.2. 完整的实例代码
3. Java NIO Channel
3.1. Java NIO Channel的特点
3.2. Channel类型
3.3. FileChannel
3.4. SocketChannel
3.4.1. 监听连接
3.4.2. 非阻塞模式
3.5. DatagramChannel
4. NIO Selector
4.1. Selector入门
4.1.1. Selector的和Channel的关系
4.1.2. 可选择通道(SelectableChannel)
4.1.3. Channel注册到Selector
4.1.4. 选择键(SelectionKey)
4.2. Selector的使用流程
4.2.1. 创建Selector
4.2.2. 将Channel注册到Selector
4.2.3. 轮询查询就绪操作
4.3. 一个NIO 编程的简单实例


猜你喜欢

转载自blog.51cto.com/14033611/2307135