Buffer数据结构和new IO的Memory-mapped files
2015-10-17 21:32 宏愿。 阅读(982) 评论(0) 编辑 收藏 举报
一、Buffer类
java.nio.Buffer这个类是用来干什么的?有怎样的结构?
"Core Java"中是这样定义的“A buffer is array of values of the same type”。所以,我们可以感性的认识到:buffer就像数组一样,存放的是相同类型的数据。还有一个重要的事情就是:Buffer是一种随机存储类型的数据结构,就像普通数组一样(用下标的方式)能够用索引号定位到buffer中的任何一个位置的数据上)。
Buffer类是一个抽象类,其子类有(注意:StringBuffer类和这里的buffer没有什么联系):
Buffer类中的属性和方法用于管理和控制buffer的状态,其子类则有get或put方法用于“读出”或“写入”不同类型的数据;我们还应该注意到:ByteBuffer、CharBuffer等依然是一个抽象类。所以,我们是不能够通过new的方式得到一个ByteBuffer或CharBuffer。现在,万一我们想得到一个CharBuffer怎么办呢(ByteBuffer可以用同样的方式得到)?
答案:可以利用CharBuffer的allocate(int capacity)方法,或者是对现有的char[] array进行包装的wrap方法,如下:
在我们实际的编程中使用最多的是ByteBuffer和CharBuffer,如下图所示,Buffer具有以下4个属性:
①、capacity,一个Buffer建立以后,它就固定不变了;
②、position,它指示了下一个要读或写数据的位置;
③、limit,超过limit位置的数据是没有意义的;
④、mark,它标记某个重要的位置,用于后面能够返回到该位置进行重新读或写;
它们四个的关系是:
0≤mark≤position≤limit≤capacity
Buffer最重要的一个作用就是:循环的用于“先写,后读”。下面是一个先写后读的过程:
①、最开始的时候,position=0 && limit=capacity;
②、调用put方法向buffer中写数据,当数据写完了或者是position到达了capacity的位置,下面就开始从buffer中读取数据;
③、从“写”状态向“读”状态转变,需要调用flip方法。其作用就是:先让limit等于当前的position,然后将position设置为0;
④、读取数据的时候,只要remaining方法(limit-position)返回一个正数,那么我们就可以持续的调用get方法从buffer中读取数据;
⑤、当读取数据过程完毕了,我们可以调用clear方法,将buffer从“读”状态转换为“写”状态,进入下一个“先读,后写”循环;
⑥、clear方法时设置position=0 && limit=capacity;
⑦、如果想重新读取buffer,可以调用其rewind或者是mark/reset方法,API中有详细介绍;
API:java.nio.Buffer
①、Buffer clear()
设置buffer进入到写状态,设置position=0 && limit=capacity;
②、Buffer flip()
设置buffer进入到读状态,设置limit等于当前的position && position=0;
③、Buffer rewind() //rewind可以翻译为“倒回”,“倒带”等
准备重新读取buffer中相同的数据,设置position=0 && limit保持不变;
④、Buffer mark()
将当前的position设置为mark,其可以配合reset()方法实现buffer的重读/写;
⑤、Buffer reset()
设置position=mark,从而可以从mark位置重新开始读或写;
⑥、int remaining()
返回buffer中“可读”数据的个数,或者是还可以“写入”多少个新的数据,返回limit-position;
⑦、int position() 返回当前position的值;
⑧、int capacity() 返回buffer的capacity的值;
二、nio的文件映射
下面我们看java.nio包相对于旧的IO而言有哪些增强的新特性:
nio主要是支持一下四个增强的特性:
①、字符集的编码和解码;
②、非阻塞IO(nonblocking I/O);
③、Memmory-mapped files;
④、文件锁;
对字符集的编码和解码可以单独拿出来讲。非阻塞IO主要用在网络通信中。文件锁是一个复杂却不怎么靠得住的东西(依赖于具体操作系统对锁的支持),在并发的情况下,通常可以借助于数据库的锁机制,将文件存入数据库中即可实现文件的同步。
这里主要总结Memmory-mapped files。
大多数操作系统可以利用虚拟内存(virtual memory)的优势,将整个文件或者是文件的一部分映射到内存中。然后,我们就可以像内存数组一样访问映射文件了(主要是其随机访问特性),这样会比传统的文件操作(RandomAccessFile)要快很多。
【讨论:对文件的操作有大体的三种方式File流、RandomAccessFile、Memmory-mapped files,但是各有特色:①文件流和缓冲流结合起来会很快,但是不具有随机访问特性;②、RandomAccessFile有随机访问特性,但是它效率十分低下;③、Memmory-mapped files具有随机访问特性,其效率甚至要比缓冲流还高,它主要是用于对“大文件”的操作上】
文件的映射操作比较简单,依据下面的步骤即可:
①、从文件中获取到一个channel。其中channel是磁盘文件的一个抽象,通过它能够获取到操作系统的一些特性,比如:内存映射、文件锁、文件间的快速数据传递。在jak1.4中已经重写了FileInputStream、FileOutputStream和RandomAccessFile类,为它们添加了getChannel方法。所以,我们可以通过调用getChannel方法获取到磁盘文件的channel。如下:
FileInputStream in = new FileInputStream(...); FileChannel channel = in.getChannel();
②、从channel中获取到MappedByteBuffer。我们可以通过调用Channel类的map方法进行文件的映射,此方法会返回一个MappedByteBuffer对象。在map方法中我们可以指定文件映射的范围(全部或者是部分),还可以指定映射的模式,共支持三种模式:
----FileChannel.MapMode.READ_ONLY:只能从buffer中读取数据,不能像buffer中写入数据。当调用写方法的时候会抛出一个ReadOnlyBufferException异常。
----FileChannel.MapMode.READ_WRITE:buffer是可写的,同时buffer中改变的数据会在某个时候写回到文件中。注意,其它也映射了该文件的程序并不能马上感知到这一改变(所以,有了文件同步锁机制)。
----FileChannel.MapMode.PRIVATE:buffer是可写的,但是任何改变都不会写回到文件中去。
③、一旦我们得到了一个buffer,那么我们就可以调用Buffer或者是其子类的方法对buffer进行数据的访问。注意,Buffer同时支持顺序访问和随机访问两种方式。比如,下面的两个例子:
//使用顺序访问buffer while(buffer.hasRemaining()){ byte b = buffer.get(); ... } //使用随机访问buffer for(int i = 0; i < buffer.limit(); i++){ byte b = buffer.get(i); //Buffer这个抽象类没有get和put方法,它只负责管理和控制buffer的状态 ... }
buffer.order(ByteOrder.LITTLE_ENDIAN); 指定小端存储
ByteOrder b = buffer.order(); 找出当前buffer中存放byte的模式
下面的一个例子中,分别使用了FileInputStream,BufferedInputStream,RandomAccessFile和MappedFile来读取rt.jar文件(59.8MB),并计算器CRC32值。我们可以对比四种处理方式的效率,得到一个感性的认识:
package nio; import java.io.*; import java.nio.*; import java.nio.channels.FileChannel; import java.util.Scanner; import java.util.zip.CRC32; public class NIOtest { public static long checksumInputStream(String filename) throws Exception{ CRC32 crc32; InputStream in = null; try { crc32 = new CRC32(); in = new FileInputStream(filename); int c; while((c = in.read()) != -1){ crc32.update(c); } } finally{ if(in != null) in.close(); } return crc32.getValue(); } public static long checksumBufferedInputStream(String filename) throws Exception{ CRC32 crc32 = new CRC32(); InputStream in = null; try { in = new BufferedInputStream(new FileInputStream(filename)); int b; while((b = in.read()) != -1){ crc32.update(b); } } finally{ if(in != null) in.close(); } return crc32.getValue(); } public static long checksumRandomAccessFile(String filename) throws Exception{ RandomAccessFile file = null; CRC32 crc = new CRC32(); try { file = new RandomAccessFile(filename, "r");//只读模式 long length = file.length(); for(int i = 0; i < length; i++){ file.seek(i); int b = file.read(); crc.update(b); } } finally{ if(file != null) file.close(); } return crc.getValue(); } public static long checksumMappedFile(String filename) throws Exception{ CRC32 crc = new CRC32(); FileInputStream in = null; FileChannel channel = null; try { in = new FileInputStream(filename); channel = in.getChannel(); int size = (int) channel.size(); MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, size); for(int p = 0; p < size; p++){ int b = buffer.get(p); crc.update(b); } } finally{ if(in != null) in.close(); } return crc.getValue(); } public static void main(String[] args) throws Exception { System.out.println("输入测试文件路径:"); InputStream in = System.in; Scanner scanner = new Scanner(in); String filename = scanner.nextLine(); System.out.println("开始计算..."); System.out.println(); long start = System.currentTimeMillis(); long crcValue = checksumInputStream(filename); long end = System.currentTimeMillis(); System.out.println("---InputStream----> " + (end - start ) + " 毫秒。 crc32 值: " + Long.toHexString(crcValue)); start = System.currentTimeMillis(); crcValue = checksumBufferedInputStream(filename); end = System.currentTimeMillis(); System.out.println("---BufferedInputStream----> " + (end - start ) + " 毫秒。 crc32 值: " + Long.toHexString(crcValue)); start = System.currentTimeMillis(); crcValue = checksumRandomAccessFile(filename); end = System.currentTimeMillis(); System.out.println("---RandomAccessFile----> " + (end - start ) + " 毫秒。 crc32 值: " + Long.toHexString(crcValue)); start = System.currentTimeMillis(); crcValue = checksumMappedFile(filename); end = System.currentTimeMillis(); System.out.println("---MappedFile----> " + (end - start ) + " 毫秒。 crc32 值: " + Long.toHexString(crcValue)); if(scanner != null) scanner.close(); } }
执行结果:
输入测试文件路径: E:\Java\jdk1.8.0_25\jre\lib\rt.jar 开始计算... ---InputStream----> 222721 毫秒。 crc32 值: 2a57ac2 ---BufferedInputStream----> 3020 毫秒。 crc32 值: 2a57ac2 ---RandomAccessFile----> 377263 毫秒。 crc32 值: 2a57ac2 ---MappedFile----> 4323 毫秒。 crc32 值: 2a57ac2
三、关于FileChannel
这个类,也是一个抽象类。
它有基于byte的读写方法。不过,都是基于ByteBuffer对象的,和Channel直接进行通信的Buffer类型只有ByteBuffer类型。从而,我么可以看出,Channel是一个非常底层的类,更确切的说这样做时为了让大多数的操作系统都能支持这种有效的文件映射。
所以,凡是想通过FileChannel读写数据到文件中,必须要通过ByteBuffer或者是ByteBuffer[]。我们知道,ByteBuffer也同样是一个抽象类,是不可以通过new方法获得的。在ByteBuffer类中为我们提供了静态的方法:loacate(int capacity)和wrap方法。通过这些静态方法可以获得一个ByteBuffer对象。像下面这样通过Channel来对文件进行读写操作:
package nio; import java.io.*; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class ChannelTest { public static void main(String[] args) throws Exception { //通过Channel写文件 FileChannel fc = new FileOutputStream("data.txt").getChannel(); fc.write(ByteBuffer.wrap("Some text".getBytes())); fc.close(); //将内容添加到文件的末尾 fc = new RandomAccessFile("data.txt", "rw").getChannel(); fc.position(fc.size()); //跳到文件的末尾 fc.write(ByteBuffer.wrap(" Some more".getBytes())); fc.close(); //通过Channel读取文件 fc = new FileInputStream("data.txt").getChannel(); ByteBuffer buffer = ByteBuffer.allocate((int) fc.size()); fc.read(buffer); //从channel中读取数据到buffer中 buffer.flip(); //准备从buffer中读取数据给我们自己用 while(buffer.remaining() > 0){ System.out.print((char)buffer.get()); } fc.close(); } }