@author ixenosjava
前提:内存的访问速度比磁盘高几个数量级,可是基本的IO操做是直接调用native方法得到驱动和磁盘交互的,IO速度限制在磁盘速度上数组
由此,就有了缓存的思想,将磁盘内容预先缓存在内存上,这样当供大于求的时候IO速度基本就是之内存的访问速度为主,例如BufferedInput/OutputStream等缓存
而咱们知道大多数OS均可以利用虚拟内存实现将一个文件或者文件的一部分映射到内存中,而后,这个文件就能够看成是内存数组同样地访问,咱们能够把它当作一种“永久的缓存”安全
内存映射文件:内存映射文件容许咱们建立和修改那些由于太大而不能放入内存的文件,此时就能够假定整个文件都放在内存中,并且能够彻底把它当成很是大的数组来访问(随机访问)网络
如下是四大文件操做对比:数据结构
本图使用思惟导图软件XMind制做多线程
在Core Java II中进行了这么一个实验:在同一台机器上,对JDK的jre/lib目录中的37MB的rt.jar文件分别用以上四种操做来计算CRC32校验和,记录下了以下时间并发
方法 | 时间 |
普通输入流 | 110s |
带缓冲的输入流 | 9.9s |
随机访问文件 | 162s |
内存映射文件 | 7.2s |
这个小实验也验证了内存映射文件这个方法的可行性,因为具备随机访问的功能(映射在内存数组),因此经常使用来替代RandomAccessFile。app
固然,对于中等尺寸文件的顺序读入则没有必要使用内存映射以避免占用本就有限的I/O资源,这时应当使用带缓冲的输入流。dom
java.nio包使得内存映射变得十分简单
一、首先,从文件中得到一个通道(channel)。通道是用于磁盘文件的一种抽象,它使咱们能够访问诸如内存映射、文件加锁机制(下文缓冲区数据结构部分将提到)、文件间快速数据传递等操做系统特性。
1 FileChannel channel = FileChannel.open(path, options);
还能经过在一个打开的 File 对象(RandomAccessFile、FileInputStream 或 FileOutputStream)上调用 getChannel() 方法获取。调用 getChannel() 方法会返回一个链接到相同文件的 FileChannel 对象且该 FileChannel 对象具备与 File 对象相同的访问权限
二、而后,经过调用FileChannel类的map方法进行内存映射,map方法从这个通道中得到一个MappedByteBuffer对象(ByteBuffer的子类)。
你能够指定想要映射的文件区域与映射模式,支持的模式有3种:
1 import java.io.*; 2 import java.nio.*; 3 import java.nio.channels.*; 4 import java.nio.file.*; 5 import java.util.zip.*; 6 7 public class MemoryMapTest 8 { 9 10 public static long checksumMappedFile(Path filename) throws IOException 11 { 12 //直接经过传入的Path打开文件通道 13 try (FileChannel channel = FileChannel.open(filename)) 14 { 15 CRC32 crc = new CRC32(); 16 int length = (int) channel.size(); 17 //经过通道的map方法映射内存 18 MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, length); 19 20 for (int p = 0; p < length; p++) 21 { 22 int c = buffer.get(p); 23 crc.update(c); 24 } 25 return crc.getValue(); 26 } 27 } 28 29 public static void main(String[] args) throws IOException 30 { 31 System.out.println("Mapped File:"); 32 start = System.currentTimeMillis(); 33 crcValue = checksumMappedFile(filename); 34 end = System.currentTimeMillis(); 35 System.out.println(Long.toHexString(crcValue)); 36 System.out.println((end - start) + " milliseconds"); 37 } 38 }
三、一旦有了缓冲区,就可使用ByteBuffer类和Buffer超类的方法来读写数据
缓冲区支持顺序和随机数据访问:
顺序:有一个能够经过get和put操做来移动的位置
1 while(buffer.hasRemaining()){ 2 byte b = buffer.get(); //get当前位置 3 ... 4 }
随机:能够按内存数组索引访问
1 for(int i=0; i<buffer.limit(); i++){ 2 byte b = buffer.get(i); //这个get能指定索引 3 ... 4 }
能够用下面的方法来读写数据到一个字节数组(destination array):
get(byte[] bytes) /get(byte[] bytes, int offset, int length)
The method transfers bytes from this buffer into the given destination array.
还有下列getXxx方法:getInt, getLong, getShort, getChar, getFloat, getDouble 用来读入在文件中存储为二进制值的基本类型值
关于二进制数据排序机制不一样的读取问题:
咱们知道,Java对二进制数据使用高位在前的排序机制(好比 0XA就是 0000 1010,高位在前低位在后),
可是,若是须要低位在前的排序方式(0101 0000)处理二进制数字的文件,需调用:
buffer.order(ByteOrder.LITTLE_ENDIAN);
要查询缓冲区内当前的字节顺序,能够调用:
ByteOrder b = buffer.order();
要向缓冲区写数字,使用对应的putXxx方法,在恰当的时机,以及当通道关闭时,会将这些修改写回到文件中的哦。
在使用内存映射时,咱们既能够建立单一的缓冲区横跨整个文件或者感兴趣的文件区域,也可使用更多的缓冲区来读写大小适度的信息块。
这一小节,就来说讲缓冲区Buffer对象上的基本操做。
缓冲区是具备相同基本类型的数值构成的数组(数组在内存中建立),Buffer类是一个抽象类,有如下具体的子类:ByteBuffer,CharBuffer,DoubleBuffer,IntBuffer,LongBuffer和ShortBuffer。(注意StringBuffer跟这些人不要紧,并且String本质是引用类型)
实践中,最经常使用的是ByteBuffer和CharBuffer。
每一个缓冲区都具备:
一、一个恒定的容量;
二、一个读写位置,下一个值将在此进行读写;
三、一个界限,超过他没法读写;
四、一个可选的标记,用于重复一个读入或写出操做;
0≤标记≤位置≤界限≤容量
一、写:一开始时位置为0,界限等于容量,当咱们不断调用put添值到缓冲区中,直至耗尽全部数据或者写出的数据集量达到容量大小时,就该进行读入操做了;
二、读:这时调用 flip 方法将界限设置到当前位置(至关于trim),并把位置复位到0(为了读操做),如今在remaining方法返回(界限 — 位置)正数时,不断调用get;
三、复位:将缓冲区中全部值读入后,调用clear(位置复位到0,界限复位到容量)使缓冲区为下一次写循环作准备;
四、复读:想复读缓冲区,可调用rewind或mark/reset方法;
缓冲区的得到:
A、内存映射时使用的是MappedByteBuffer,这是ByteBuffer的子类,由FileChannel的map()方法调用
B、饿汉要获取缓冲区,可调用ByteBuffer.allocate或ByteBuffer.wrap这样的静态方法,而后用来自某个通道的数据填充缓冲区,或者将缓冲区的内容写出通道中:
1 ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE); 2 3 //填充缓冲区 4 channel1.read(buffer); 5 //将Channel位置指定到newpos,做为覆盖文件内容的起点 6 channel1.position(newpos); 7 //将Buffer界限设置到当前位置,准备写出,注意区别Buffer和Channel的position,二者是不一样的概念 8 buffer.flip(); 9 //将缓冲区数据写出通道中 10 channel.write(buffer);
这些方法和RandomAccessFile类的方法相似,但性能更高,所以经常使用以代替随机访问文件。
线程安全:咱们知道多线程并发修改共享数据会产生安全问题——竞争条件,为了保证对数据的原子性操做——同步存取,咱们有了synchronized关键字添加隐式锁以及ReentranLock添加显式锁。可是多进程的同步存取又该怎么实现呢?
进程安全:OS有个文件加锁机制,因为通道是对磁盘的一种抽象,FileChannel所以也实现了文件锁,能够调用其lock或tryLock方法进行锁定。
文件锁示例:锁定一个文件
1 FileChannel channel = FileChannel.open(path); 2 3 //调用lock,阻塞 4 FileLock lock = channel.lock(); 5 6 //调用tryLock,当即响应 7 FileLock lock = channel.tryLock();
一、第一个调用 lock() 会阻塞直至可得到锁,而第二个调用 tryLock() 将当即返回,要么得到锁,要么在锁不可得到的状况家返回null;
二、这个文件将保持锁定状态,直至这个通道关闭,或者在锁上调用了release方法;
三、还能够锁定文件的一部分:FileLock lock(long start, long size, boolean shared) 或 FileLock tryLock(long start, long size, boolean shared)
a)若是shared标志位false,则锁定文件的目的是读写,而若是为true,则这是一个共享锁,容许多个进程从文件中读入,并阻止任何进程得到独占的锁。调用FIleLock的isShared可查询当前持有的文件锁类型。
b)若是锁了文件的尾部,但文件长度随后增加超过了锁定部分,那么超过的任然是不锁定的,此时须要使用 Long.MAX_VALUE 来表示尺寸。
四、要确保在操做完成时释放锁,可用 try-with-resources 语句(FileLock实现了AutoCloseable接口)
1 try(FileLock lock = channel.lock()){ 2 ... 3 }
手动释放锁可调用FileLock对象的close()方法
注意点:
一、文件加锁机制是依赖于操做系统的
二、意外的建议锁:在某些系统中文件锁仅仅是建议性的,可能出现一个应用未能获得锁,它仍旧能够向被另外一个进程并发锁定的文件执行写操做;
三、意外的原子性:在某些系统中,不能在锁定一个文件的同时将其映射到内存中,原子性;
四、意外的全释放:在某些系统中,关闭一个通道会释放由JVM持有的底层文件上的全部锁,所以避免在同一个锁定文件上使用多个通道,否则其余通道的锁也可能被释放!
五、不可重入锁:文件锁是由整个JVM持有的,两个由同一VM启动的程序不可能得到在同一个文件上的锁,若是尝试对VM上已加锁的文件再加锁,将抛出OverlappingFileLockException;
(注意:多线程的ReentranLock是可重入的!简称可重入锁,而文件锁是不可重入锁)
六、在网络文件系统上锁定文件是高度依赖于系统的,尽可能避免使用文件锁。