Java NIO学习4(缓冲区2)

来源:互联网 发布:我要学软件 编辑:程序博客网 时间:2024/05/18 03:00
缓冲区分配和包装
在能够读和写之前,必须有一个缓冲区。要创建缓冲区,您必须 分配它。我们使用静态方法 allocate() 来分配缓冲区:
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
allocate() 方法分配一个具有指定大小的底层数组,并将它包装到一个缓冲区对象中 ― 在本例中是一个ByteBuffer。

您还可以将一个现有的数组转换为缓冲区,如下所示:

byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap( array );

本例使用了 wrap() 方法将一个数组包装为缓冲区。必须非常小心地进行这类操作。一旦完成包装,底层数据就可以通过缓冲区或者直接访问。

缓冲区分片
slice() 方法根据现有的缓冲区创建一种 子缓冲区。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。
使用例子可以最好地说明这点。让我们首先创建一个长度为 10 的 ByteBuffer:
ByteBuffer buffer = ByteBuffer.allocate( 10 );
然后使用数据来填充这个缓冲区,在第 n 个槽中放入数字 n:
for (int i=0; i<buffer.capacity(); ++i) {
buffer.put( (byte)i );
}
现在我们对这个缓冲区 分片,以创建一个包含槽 3 到槽 6 的子缓冲区。在某种意义上,子缓冲区就像原来的缓冲区中的一个 窗口。
窗口的起始和结束位置通过设置 position 和 limit 值来指定,然后调用 Buffer 的 slice() 方法:
buffer.position( 3 );
buffer.limit( 7 );
ByteBuffer slice = buffer.slice();
片 是缓冲区的 子缓冲区 。不过, 片段 和 缓冲区 共享同一个底层数据数组

缓冲区份片和数据共享
我们已经创建了原缓冲区的子缓冲区,并且我们知道缓冲区和子缓冲区共享同一个底层数据数组。让我们看看这意味着什么。
我们遍历子缓冲区,将每一个元素乘以 11 来改变它。例如,5 会变成 55。
for (int i=0; i<slice.capacity(); ++i) {
byte b = slice.get( i );
b *= 11;
slice.put( i, b );
}
最后,再看一下原缓冲区中的内容:
buffer.position( 0 );
buffer.limit( buffer.capacity() );
while (buffer.remaining()>0) {
System.out.println( buffer.get() );
}
结果表明只有在子缓冲区窗口中的元素被改变了:
0
1
2
33
44
55
66
7
8
9
缓冲区片对于促进抽象非常有帮助。可以编写自己的函数处理整个缓冲区,而且如果想要将这个过程应用于子缓冲区上,您只需取主缓冲区的一个片,并将它传递给您的函数。这比编写自己的函数来取额外的参数以指定要对缓冲区的哪一部分进行操作更容易。

代码;

//创建一个缓冲区 大小是10ByteBuffer buffer = ByteBuffer.allocate(10);//存入数据 1-10for (int i = 0; i < buffer.capacity(); i++) {buffer.put((byte)i);}//创建一个缓冲区分片  从4-7buffer.position(4);buffer.limit(7);ByteBuffer sub = buffer.slice();//将4,5,6乘以 11for (int i = 0; i < sub.capacity(); i++) {byte b = sub.get(i);sub.put((byte) (b * 11));}buffer.position(0);buffer.limit(buffer.capacity());while (buffer.hasRemaining()) {byte b = buffer.get();System.out.println(b);}

只读缓冲区
只读缓冲区非常简单 ― 您可以读取它们,但是不能向它们写入。可以通过调用缓冲区的 asReadOnlyBuffer() 方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。
只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时,您无法知道这个方法是否会修改缓冲区中的数据。

创建一个只读的缓冲区可以 保证该缓冲区不会被修改。不能将只读的缓冲区转换为可写的缓冲区。

// 创建一个缓冲区 大小是10ByteBuffer buffer = ByteBuffer.allocate(100);// 存入数据 1-9for (int i = 0; i < 10; i++) {buffer.put((byte) i);}//得到readOnly bufferByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();//这句会报错!readOnlyBuffer.put(1, (byte)11);

Exception in thread "main" java.nio.ReadOnlyBufferException
at java.nio.HeapByteBufferR.put(Unknown Source)
at com.anders.javanio.buffer.ReadOnlyBuffer.main(ReadOnlyBuffer.java:15)

直接和间接缓冲区
另一种有用的 ByteBuffer 是直接缓冲区。 直接缓冲区是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。
实际上,直接缓冲区的准确定义是与实现相关的。Sun 的文档是这样描述直接缓冲区的:
给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。
还可以用内存映射文件创建直接缓冲区。

//得到源文件与目标文件的路径String copyPath = Demo1.class.getResource("copy.txt").getPath();String pastePath = Demo1.class.getResource("paste.txt").getPath();FileInputStream ins = new FileInputStream(new File(copyPath));FileOutputStream out = new FileOutputStream(new File(pastePath));//从输入输出流中得到  channelFileChannel fIns = ins.getChannel();FileChannel fOut = out.getChannel();//创建一个直接缓冲区!!!Buffer  大小为1024ByteBuffer buffer = ByteBuffer.allocateDirect(1024);while(true){//清空缓冲区buffer.clear();//读输入到缓冲区int n = fIns.read(buffer);if(n == -1){break;}// flip方法让缓冲区可以将新读入的数据写入另一个通道  buffer.flip();// 从输出通道中将数据写入缓冲区  fOut.write(buffer);}System.out.println("over !");
直接缓冲与间接的区别:

(在数据传输过程中,如果数据量很小,使用同步IO方式会更适合,如果数据量很大,一般以G为单位的时候,使用非阻塞IO方式更快)

创建buffer的两个方式:allocate和allocateDirect

第一种:allocate

产生内存的开销是在JVM中,属于堆分配,可以通过工具查看内存占用。

第二中:allocateDirect

是系统级别的内存分配,产生的开销在Jvm之外,非Java堆,查看不出java heap的内存占用,倒是泄露看不出!

(当java程序接受到外部传来的数据时候,首先是呗系统内存所获取,然后在由系统内存复制到jvm中,java程序才可以使用)

所以第二中方式中,可是省去复制这一步操作,效率上会有所提高,但是系统级别的内存分配比起jvm内存的分配要耗时更多,所以并不是任何时候都allocateDirect的操作效率更好,而且allocateDirect也不是平台独立的

结论:1 档操作数据较少时,两种方式使用时间基本上是相同的,有时allocate更快

             2 档数据量很大时,allocateDirect好于allocate


分散和聚集
概述
分散/聚集 I/O 是使用多个而不是单个缓冲区来保存数据的读写方法。
一个分散的读取就像一个常规通道读取,只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中。同样地,一个聚集写入是向缓冲区数组而不是向单个缓冲区写入数据。
分散/聚集 I/O 对于将数据流划分为单独的部分很有用,这有助于实现复杂的数据格式。

分散/聚集 I/O
通道可以有选择地实现两个新的接口: ScatteringByteChannel 和 GatheringByteChannel。一个
ScatteringByteChannel 是一个具有两个附加读方法的通道:
 long read( ByteBuffer[] dsts );
 long read( ByteBuffer[] dsts, int offset, int length );
这些 long read() 方法很像标准的 read 方法,只不过它们不是取单个缓冲区而是取一个缓冲区数组。
在 分散读取中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区数组就像一个大缓冲区。

分散/聚集的应用
分散/聚集 I/O 对于将数据划分为几个部分很有用。例如,您可能在编写一个使用消息对象的网络应用程序,每一个消息被划分为固定长度的头部和固定长度的正文。您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区。当您将它们放入一个数组中并使用分散读取来向它们读入消息时,头部和正文将整齐地划分到这两个缓冲区中。
我们从缓冲区所得到的方便性对于缓冲区数组同样有效。因为每一个缓冲区都跟踪自己还可以接受多少数据,所以分散读取会自动找到有空间接受数据的第一个缓冲区。在这个缓冲区填满后,它就会移动到下一个缓冲区。

聚集写入
集写入 类似于分散读取,只不过是用来写入。它也有接受缓冲区数组的方法:
 long write( ByteBuffer[] srcs );
 long write( ByteBuffer[] srcs, int offset, int length );
聚集写对于把一组单独的缓冲区中组成单个数据流很有用。为了与上面的消息例子保持一致,您可以使用聚集写入来自动将网络消息的各个部分组装为单个数据流,以便跨越网络传输消息。

public class UseScatterGetter {static private final int HEADER_LENGTH = 20;static private final int BODY_LENGTH = 40;public static void main(String[] args) throws Exception {int port = 8080;ServerSocketChannel ssc = ServerSocketChannel.open();InetSocketAddress address = new InetSocketAddress(port);ssc.socket().bind(address);int messageLength = HEADER_LENGTH + BODY_LENGTH;ByteBuffer buffers[] = new ByteBuffer[2];buffers[0] = ByteBuffer.allocate(HEADER_LENGTH);buffers[1] = ByteBuffer.allocate(BODY_LENGTH);SocketChannel sc = ssc.accept();int byteRead = 0;while (byteRead < messageLength) {long r = sc.read(buffers);byteRead += r;System.out.println(" r : " + r);for (int i = 0; i < buffers.length; i++) {ByteBuffer bb = buffers[i];System.out.println("b" + i + " position : " + bb.position());}}// process message//flip buffersfor (int i = 0; i < buffers.length; i++) {ByteBuffer bb = buffers[i];bb.flip();}//scatter writelong byteWrite = 0;while (byteWrite < messageLength) {long r = sc.write(buffers);byteWrite += r;}//clear buffersfor (int i = 0; i < buffers.length; i++) {ByteBuffer bb = buffers[i];bb.clear();}System.out.println(byteRead + "  " + byteWrite + " " + messageLength);}






原创粉丝点击