大多数操作系统都可以利用虚拟内存实现来将一个文件或者文件的一部分“映射”到内存中。然后这个文件就可以当作是 内存数组一样地访问,这比传统的文件操作要快的多。
java.nio 使内存映射变得十分简单:
首先,从文件中获得一个通道(channel),通道是用于磁盘文件的一种抽象,它使我们可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。
FileChannel channel = FileChannel.open(path,options);
然后通过调用FileChannel 类的map 方法从这个通道中获得一个ByteBuffer。你可以指定想要映射的文件区域与映射模式,支持的模式有三种:
- FileChannel.MapMode.READ_ONLY: 所产生的缓冲区是只读的,任何对该缓冲区写入的尝试都会导致 ReadOnlyBufferException 异常。
- FileChannel.MapMode.READ_WRITE:所产生的缓冲区是可写的,任何修改都会在某个时刻写回到文件中。注意,其他映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的确切行为是依赖于操作系统的。
FileChannel.MapMode.PRIVATE: 所产生的缓冲区是可写的,但是任何修改对这个缓冲区来说都是私有的,不会传播到文件中。
一旦有了缓冲区,就可以使用ByteBuffer类和Buffer超类的方法读写数据了。
缓冲区支持顺序和随机访问,它有一个可以通过get和put 操作来移动的位置。例如,可以像下面这样顺序遍历缓冲区中的所有字节:
while(buffer.hasRemaining()){
byte b = buffer.get();
...
}
或者,像下面这样进行随机访问:
for(int i=0;i<buffer.limit();i++){
byte b = buffer.get(i);
...
}
可以用下面的方法来读写字节数组:
get(byte[] bytes)
get(byte[] bytes,int offset,int length)
最后还有下面的方法:
getInt getLong getShort getChar getFloat getDouble
用来读入在文件中存储为二进制的基本类型值。
Java对二进制数据使用高位在前的排序机制,但是,如果需要以低位在前的排序方式处理包含二进制数字的文件,那么只需调用:
buffer.order(ByteOrder.LITTLE_ENDIAN);
要查询缓冲区内当前的字节顺序,可以调用:
ByteOrder order = buffer.order();
要向缓冲区写数字,可以使用下列的方法:
putInt putLong putShort putChar putFloat putDouble
在恰当的时机,以及当通道关闭时,会将这些修改写回到文件中。
缓冲区数据结构
缓冲区是由具有相同类型的数值构成的数组,Buffer 是一个抽象类,它有众多具体的子类,包括ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。
每个缓冲区都具有:
- 一个容量,它永远不能改变。
- 一个读写位置,下一个值将在此进行读写。
- 一个界限,超过它进行读写是没有意义的。
这些值满足下面的条件: 0<= 标记 <= 位置 <=界限 <= 容量
使用缓冲区的主要目的是执行“写,然后读入”循环。假设我们有一个缓冲区,在一开始,它的位置为0,界限等于容量。我们不断地调用 put 将值添加到这个缓冲区中,当我们耗尽所有的数据或者写出的数据量达到容量大小时,就该切换到读入操作了。
这时调用 flip 方法将界限设置到当前位置,并把位置复位到0.现在在 remaining 方法返回正数时(它返回的值是“界限-位置”),不断地调用 get。在我们将缓冲区中所有的值都读入之后,调用clear 为下一次写循环做好准备。clear 方法将位置复位到0,并将界限位置复位到容量。
要想重读缓冲区,可以使用 rewind 或mark / reset 方法 ,
要获取缓冲区,可以调用诸如 ByteBuffer.allocate 或 ByteBuffer.wrap 这样的静态方法。
然后,可以用来自某个通道的数据填充缓冲区,或者将缓冲区的内容写出通道中。例如:
ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
channel.read(buffer);
channel.position(newpos);
buffer.flip();
channel.write(buffer);
实例如下:
import java.nio.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.nio.channels.*;
public class BufferTest{
public static void main(String[] args) throws Exception{
Path path = Paths.get("G:\\BufferTest.txt");
FileChannel channel = FileChannel.open(path,StandardOpenOption.WRITE,StandardOpenOption.READ);
int length = (int)channel.size();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE,0,length);
//为了获取文件中的中文,使用Charset
CharBuffer cbuffer = CharBuffer.allocate(128);
Charset charset = Charset.forName("GB2312");
//解码器
CharsetDecoder decoder = charset.newDecoder();
ByteBuffer bu = ByteBuffer.allocate(128);
int i = channel.read(bu);
while(i!=-1){
//为读做好准备
bu.flip();
decoder.decode(bu,cbuffer,false);
//为读做好准备
cbuffer.flip();
//循环读
while(cbuffer.hasRemaining()){
char b = cbuffer.get();
System.out.print(b);
}
//为写循环做好准备
cbuffer.clear();
bu.clear();
i = channel.read(bu);
}
//打印Java对二进制数据使用的排序机制
ByteOrder order = buffer.order();
System.out.println(order.toString());
channel.close();
}
}
/*
*运行结果:你好,世界BIG_ENDIAN
*分析:你好,世界是文件内容,采用GB2312编码;BIG_ENDIAN是采用高位在前的排序机制
*/
文件加锁机制
考虑一下多个同时执行的程序需要修改同一个文件的情形,很明显,这些程序需要以某种方式进行通信,不然这个文件很容易被损坏。文件锁可以解决这个问题,它可以控制对文件或文件中某个范围的字节的访问。
假设你的应用程序将用户的偏好存储在一个配置文件中,当用户调用这个应用的两个实例时,这两个实例就有可能会同时希望写这个配置文件。在这种情况下,第一个实例应该锁定这个文件,当第二个实例发现这个文件是被锁定时,它必须决策是等待直至这个文件解锁,还是直接跳过这个写操作的过程。
要锁定一个文件,可以调用FileChannel 类的lock 或 tryLock方法:
FileChannel channel = FileChannel.open(path);
FileLock lock = channel.lock(); //阻塞直至可获得锁
// FileLock lock = channel.tryLock(); 立即返回,要么返回锁,要么在锁不可获得的情况下返回null。
//锁定文件的一部分
FileLock lock(long start,long size,boolean shared)
FileLock tryLock(long start,long size,boolean shared)
/*如果shared 标志为false ,则锁定文件的目的是读写,如果为true ,则这时一个共享锁,它允许多个进程从文件中读入,并阻止任何进程获得独占的锁(依赖于操作系统)。
*/
这个文件将保持锁定状态,直至这个通道关闭,或者在锁上调用了 release 方法。
要确保在操作完成时释放锁,最好在一个 try 语句中执行释放锁的操作:
try(FileLock lock = channel.lock())
{
...
}
文件加锁机制是依赖于操作系统的。
以上内容和部分代码来源于Java 核心技术卷Ⅱ (原书第九版)