0 概述
Channel相关基础知识可以参考Channel的基础。Java NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。FileChannel对象是线程安全的,每个FileChannel对象都同一个文件描述符(file descriptor)有一对一的关系。
FileChannel对象不能直接创建。一个FileChannel实例只能通过在一个打开的file对象(RandomAccessFile、FileInputStream或FileOutputStream)上调用getChannel( )方法获取。
FileChannel位置(position)是从底层的文件描述符获得的,该position同时被作为通道引用获取来源的文件对象共享。这也就意味着一个对象对该position的更新可以被另一个对象看到。
如果position值达到了文件大小的值(文件大小的值可以通过size( )方法返回),read( )方法会返回一个文件尾条件值(-1)。可是,不同于缓冲区的是,如果实现write( )方法时position前进到超过文件大小的值,该文件会扩展以容纳新写入的字节。
当需要减少一个文件的size时,truncate( )方法会砍掉您所指定的新size值之外的所有数据。如果当前size大于新size,超出新size的所有字节都会被悄悄地丢弃。
所有的现代文件系统都会缓存数据和延迟磁盘文件更新以提高性能, force( )方法要求文件的所有待定修改立即同步到磁盘。
public class FileChannelDemo {
public static void main(String[] args) throws IOException {
String fileName = "/Users/hsc/src.mp4";
FileChannel fileChannel = null;
FileInputStream input = null;
try {
input = new FileInputStream(fileName);
fileChannel = input.getChannel();
//文件位置
System.out.println(fileChannel.position());
//文件大小
System.out.println(fileChannel.size()/(1024*1024));
} catch (Exception ex) {
System.out.println(ex.toString());
} finally {
if (fileChannel != null) {
fileChannel.close();
}
if (input != null) {
input.close();
}
}
}
}
文件空洞:当磁盘上一个文件的分配空间小于它的文件大小时会出现“文件空洞”。对于内容稀疏的文件,大多数现代文件系统只为实际写入的数据分配磁盘空间(PS:这个和操作系统的文件系统有很关系)(更准确地说,只为那些写入数据的文件系统页分配空间。假如数据被写入到文件中非连续的位置上,这将导致文件出现在逻辑上不包含数据的区域(即“空洞”)。
1 文件锁定
有关FileChannel实现的文件锁定模型的一个重要注意项是:锁的对象是文件而不是通道或线程,这意味着文件锁不适用于判优同一台Java虚拟机上的多个线程发起的访问。
如果一个线程在某个文件上获得了一个独占锁,然后第二个线程利用一个单独打开的通道来请求该文件的独占锁,那么第二个线程的请求会抛出OverlappingFileLockException异常。但如果这两个线程运行在不同的Java虚拟机上,那么第二个线程会阻塞,因为锁最终是由操作系统或文件系统来判优的并且几乎总是在进程级而非线程级上判优。锁都是与一个文件关联的,而不是与单个的文件句柄或通道关联。
锁是在文件内部区域上获得的。调用带参数的Lock( )方法会指定文件内部锁定区域的开始position以及锁定区域的size。第三个参数shared表示您想获取的锁是共享的(参数值为true)还是独占的(参数值为false)。
lock底层方法实现:
public final FileLock lock() throws IOException {
return lock(0L, Long.MAX_VALUE, false);
}
public class FileLockDemo {
public static void main(String[] args) throws IOException {
FileChannel fileChannel = null;
FileOutputStream fileOutputStream = null;
FileLock fileLock = null;
try {
fileOutputStream = new FileOutputStream("test.txt");
fileChannel = fileOutputStream.getChannel();
fileLock = fileChannel.lock();
System.out.println(fileLock);
} catch (Exception ex) {
System.out.println(ex);
} finally {
if (fileLock != null) {
fileLock.close();
}
if (fileChannel != null) {
fileChannel.close();
}
if (fileOutputStream != null) {
fileOutputStream.close();
}
}
}
}
public class FileLockDemo {
public static void main(String[] args) throws IOException {
FileOutputStream fileOutputStream=new FileOutputStream("test.txt");
final FileChannel fileChannel=fileOutputStream.getChannel();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
FileLock fileLock= fileChannel.lock();
System.out.println(fileLock.isValid());
Thread.sleep(1000);
}catch (Exception ex)
{
System.out.println(ex.getMessage());
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
FileLock fileLock= fileChannel.lock();
System.out.println(fileLock.isValid());
Thread.sleep(1000);
}catch (Exception ex)
{
System.out.println(ex);
}
}
});
thread1.start();
thread2.start();
}
}
##输出:
java.nio.channels.OverlappingFileLockException
true
2.内存映射文件
FileChannel类提供了一个map()的方法,在FileChannel上调用map( )方法会创建一个由磁盘文件支持的虚拟内存映射(virtual memory mapping)并在那块虚拟内存空间外部封装一个MappedByteBuffer对象,调用get()方法会从磁盘文件中获取数据。相似地,对映射的缓冲区实现一个put( )会更新磁盘上的那个文件(假设对该文件您有写的权限),并且您做的修改对于该文件的其他阅读者也是可见的。
通过内存映射机制来访问一个文件会比使用常规方法读写高效得多,甚至比使用通道的效率都高。因为不需要做明确的系统调用,那会很消耗时间。更重要的是,操作系统的虚拟内存可以自动缓存内存页(memory page)。这些页是用系统内存来缓存的,所以不会消耗Java虚拟机内存堆(memory heap)。
API 接口
public abstract class FileChannel extends AbstractInterruptibleChannel
implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {
public abstract MappedByteBuffer map(MapMode mode, long position, long size)
throws IOException;
public static class MapMode {
/**
* Mode for a read-only mapping.
*/
public static final MapMode READ_ONLY = new MapMode("READ_ONLY");
/**
* Mode for a read/write mapping.
*/
public static final MapMode READ_WRITE = new MapMode("READ_WRITE");
/**
* Mode for a private (copy-on-write) mapping.
*/
public static final MapMode PRIVATE = new MapMode("PRIVATE");
private final String name;
private MapMode(String name) {
this.name = name;
}
/**
* Returns a string describing this file-mapping mode.
*
* @return A descriptive string
*/
public String toString() {
return name;
}
}
}
public class FileCopyDemo {
public static void main(String[] args) throws IOException {
String srcFile = "/Users/hsc/12.mp4";
String destFile = "/Users/hsc/dest.mp4";
FileChannel srcChannel = null;
FileChannel destChannel = null;
FileInputStream input = null;
RandomAccessFile randomAccessFile = null;
try {
input = new FileInputStream(srcFile);
randomAccessFile = new RandomAccessFile(destFile, "rw");
srcChannel = input.getChannel();
destChannel = randomAccessFile.getChannel();
copyUseMappedByteBuffer(srcChannel,destChannel);
copy(srcChannel,destChannel);
Long begin = System.currentTimeMillis();
//transferTo方法
srcChannel.transferTo(0, srcChannel.size(), destChannel);
System.out.println("耗时:"+(System.currentTimeMillis() - begin));
} catch (Exception ex) {
System.out.println(ex);
} finally {
if (srcChannel != null) {
srcChannel.close();
}
if (destChannel != null) {
destChannel.close();
}
if (input != null) {
input.close();
}
if (randomAccessFile != null) {
randomAccessFile.close();
}
}
}
private static void copy(FileChannel src, FileChannel dest) throws IOException {
Long begin = System.currentTimeMillis();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (src.read(byteBuffer) != -1) {
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
dest.write(byteBuffer);
}
byteBuffer.clear();
}
System.out.println("耗时:"+(System.currentTimeMillis() - begin));
}
private static void copyUseMappedByteBuffer(FileChannel src, FileChannel dest) throws IOException {
Long begin = System.currentTimeMillis();
//使用内存映射
MappedByteBuffer mappedByteBuffer = src.map(FileChannel.MapMode.READ_ONLY, 0, src.size());
int n = dest.write(mappedByteBuffer);
System.out.println(n==src.size());
System.out.println("耗时:"+(System.currentTimeMillis() - begin));
}
}
输出结果:
true
耗时:1011
耗时:2928
耗时:745
这个所需时间可能会因为文件系统和操作系统的缓存会有点差别(后面的会有缓存),但是可以看出的使用内存映射文件方法进行文件copy是比直接使用ByteBuffer要快很多。