Java NIO 文件通道 FileChannel 用法及原理
FileChannel提供了一种通过通道来访问文件的方式,它可以通过带参数position(int)方法定位到文件的任意位置开始进行操作,还能够将文件映射到直接内存,提高大文件的访问效率。本文将介绍其详细用法和原理。
1.通道获取
FileChannel可以通过FileInputStream,FileOutputStream,RandomAccessFile的对象中的getChannel()方法来获取,也可以同通过静态方法FileChannel.open(Path,OpenOption...)来打开。
1.1从FileInputStream/FileOutputStream中获取
从FileInputStream对象中获取的通道是以读的方式打开文件,从FileOutpuStream对象中获取的通道是以写的方式打开文件。
FileOutputStreamous=newFileOutputStream(newFile("a.txt")); FileChannelout=ous.getChannel();//获取一个只读通道 FileInputStreamins=newFileInputStream(newFile("a.txt")); FileChannelin=ins.getChannel();//获取一个只写通道
1.2从RandomAccessFile中获取
从RandomAccessFaile中获取的通道取决于RandomAccessFaile对象是以什么方式创建的,"r","w","rw"分别对应着读模式,写模式,以及读写模式。
RandomAccessFilefile=newRandomAccessFile("a.txt","rw"); FileChannelchannel=file.getChannel();//获取一个可读写文件通道
1.3通过FileChannel.open()打开
通过静态静态方法FileChannel.open()打开的通道可以指定打开模式,模式通过StandardOpenOption枚举类型指定。
FileChannelchannel=FileChannel.open(Paths.get("a.txt"),StandardOpenOption.READ);//以只读的方式打开一个文件a.txt的通道
2.读取数据
读取数据的read(ByteBufferbuf)方法返回的值表示读取到的字节数,如果读到了文件末尾,返回值为-1。读取数据时,position会往后移动。
2.1将数据读取到单个缓冲区
和一般通道的操作一样,数据也是需要读取到1个缓冲区中,然后从缓冲区取出数据。在调用read方法读取数据的时候,可以传入参数position和length来指定开始读取的位置和长度。
FileChannelchannel=FileChannel.open(Paths.get("a.txt"),StandardOpenOption.READ); ByteBufferbuf=ByteBuffer.allocate(5); while(channel.read(buf)!=-1){ buf.flip(); System.out.print(newString(buf.array())); buf.clear(); } channel.close();
2.2读取到多个缓冲区
文件通道FileChannel实现了ScatteringByteChannel接口,可以将文件通道中的内容同时读取到多个ByteBuffer当中,这在处理包含若干长度固定数据块的文件时很有用。
ScatteringByteChannelchannel=FileChannel.open(Paths.get("a.txt"),StandardOpenOption.READ); ByteBufferkey=ByteBuffer.allocate(5),value=ByteBuffer.allocate(10); ByteBuffer[]buffers=newByteBuffer[]{key,value}; while(channel.read(buffers)!=-1){ key.flip(); value.flip(); System.out.println(newString(key.array())); System.out.println(newString(value.array())); key.clear(); value.clear(); } channel.close();
3.写入数据
3.1从单个缓冲区写入
单个缓冲区操作也非常简单,它返回往通道中写入的字节数。
FileChannelchannel=FileChannel.open(Paths.get("a.txt"),StandardOpenOption.WRITE); ByteBufferbuf=ByteBuffer.allocate(5); byte[]data="Hello,JavaNIO.".getBytes(); for(inti=0;i3.2从多个缓冲区写入
FileChannel实现了GatherringByteChannel接口,与ScatteringByteChannel相呼应。可以一次性将多个缓冲区的数据写入到通道中。
FileChannelchannel=FileChannel.open(Paths.get("a.txt"),StandardOpenOption.WRITE); ByteBufferkey=ByteBuffer.allocate(10),value=ByteBuffer.allocate(10); byte[]data="017Robothy".getBytes(); key.put(data,0,3); value.put(data,4,data.length-4); ByteBuffer[]buffers=newByteBuffer[]{key,value}; key.flip(); value.flip(); channel.write(buffers); channel.force(false);//将数据刷出到磁盘 channel.close();3.3数据刷出
为了减少访问磁盘的次数,通过文件通道对文件进行操作之后可能不会立即刷出到磁盘,此时如果系统崩溃,将导致数据的丢失。为了减少这种风险,在进行了重要数据的操作之后应该调用force()方法强制将数据刷出到磁盘。
无论是否对文件进行过修改操作,即使文件通道是以只读模式打开的,只要调用了force(metaData)方法,就会进行一次I/O操作。参数metaData指定是否将元数据(例如:访问时间)也刷出到磁盘。
channel.force(false);//将数据刷出到磁盘,但不包括元数据4.文件锁
可以通过调用FileChannel的lock()或者tryLock()方法来获得一个文件锁,获取锁的时候可以指定参数起始位置position,锁定大小size,是否共享shared。如果没有指定参数,默认参数为position=0,size=Long.MAX_VALUE,shared=false。
位置position和大小size不需要严格与文件保持一致,position和size均可以超过文件的大小范围。例如:文件大小为100,可以指定位置为200,大小为50;则当文件大小扩展到250时,[200,250)的部分会被锁住。
shared参数指定是排他的还是共享的。要获取共享锁,文件通道必须是可读的;要获取排他锁,文件通道必须是可写的。
由于Java的文件锁直接映射为操作系统的文件锁实现,因此获取文件锁时代表的是整个虚拟机,而非当前线程。若操作系统不支持共享的文件锁,即使指定了文件锁是共享的,也会被转化为排他锁。
FileLocklock=channel.lock(0,Long.MAX_VALUE,false);//排它锁,此时同一操作系统下的其它进程不能访问a.txt System.out.println("Channellockedinexclusivemode."); Thread.sleep(30*1000L);//锁住30s lock.release();//释放锁 lock=channel.lock(0,Long.MAX_VALUE,true);//共享锁,此时文件可以被其它文件访问 System.out.println("Channellockedinsharedmode."); Thread.sleep(30*1000L);//锁住30s lock.release();与lock()相比,tryLock()是非阻塞的,无论是否能够获取到锁,它都会立即返回。若tryLock()请求锁定的区域已经被操作系统内的其它的进程锁住了,则返回null;而lock()会阻塞,直到获取到了锁、通道被关闭或者线程被中断为止。
5.通道转换
普通的读写方式是利用一个ByteBuffer缓冲区,作为数据的容器。但如果是两个通道之间的数据交互,利用缓冲区作为媒介是多余的。文件通道允许从一个ReadableByteChannel中直接输入数据,也允许直接往WritableByteChannel中写入数据。实现这两个操作的分别为transferFrom(ReadableByteChannelsrc,position,count)和transferTo(position,count,WritableChanneltarget)方法。
这进行通道间的数据传输时,这两个方法比使用ByteBuffer作为媒介的效率要高;很多操作系统支持文件系统缓存,两个文件之间实际可能并没有发生复制。
transferFrom或者transferTo在调用之后并不会改变position的位置。
下面示例是一个spring源码中的一个工具方法。
publicstaticvoidcopy(Filesource,Filetarget)throwsIOException{ FileInputStreamsourceOutStream=newFileInputStream(source); FileOutputStreamtargetOutStream=newFileOutputStream(target); FileChannelsourceChannel=sourceOutStream.getChannel(); FileChanneltargetChannel=targetOutStream.getChannel(); sourceChannel.transferTo(0,sourceChannel.size(),targetChannel); sourceChannel.close(); targetChannel.close(); sourceOutStream.close(); targetOutStream.close(); }需要注意的是,调用这两个转换方法之后,某些情况下并不保证数据能够全部完成传输,确切传输了多少字节的数据需要根据返回的值来进行判断。例如:从一个非阻塞模式下的SocketChannel中输入数据就不能够一次性将数据全部传输过来,或者将文件通道的数据传输给一个非阻塞模式下的SocketChannel不能一次性传输过去。
下面给出一个示例,客户端连接到服务端,然后从服务端下载一个叫video.mp4文件,文件在当前目录存在。
错误示例:
/**服务端**/ ServerSocketChannelserverSocketChannel=ServerSocketChannel.open();//打开服务通道 serverSocketChannel.bind(newInetSocketAddress(9090));//绑定端口号 SocketChannelclientChannel=serverSocketChannel.accept();//等待客户端连接,获取SocketChannel FileChannelfileChannel=FileChannel.open(Paths.get("video.mp4"),StandardOpenOption.READ);//打开文件通道 fileChannel.transferTo(0,fileChannel.size(),clientChannel);//【可能出错位置】文件通道数据输出转化到socket通道,输出范围为整个文件。文件太大将导致输出不完整 /**客户端**/ SocketChannelsocketChannel=SocketChannel.open(newInetSocketAddress("localhost",9090));//打卡socket通道并连接到服务端 FileChannelfileChannel=FileChannel.open(Paths.get("video-downloaded.mp4"),StandardOpenOption.TRUNCATE_EXISTING,StandardOpenOption.WRITE,StandardOpenOption.CREATE);//打开文件通道 fileChannel.transferFrom(socketChannel,0,Long.MAX_VALUE);//【非阻塞模式下可能出错】 fileChannel.force(false);//确保数据刷出到磁盘正确的姿势是:transferTo/transferFrom的时候应该用一个循环检查实际输出内容大小是否和期望输出内容大小一致,特别是通道处于非阻塞模式下,极大概率不能够一次传输完成。
所以服务端正确的转换方式是:
longtransfered=0; while(transfered本例中客户端使用的是阻塞模式,服务端通道关闭输出(socketChannel.shutdownOutput())之后transferFrom才退出,服务端正常关闭通道的情况下数据传输不会出错,这里就不处理非正常关闭的情况了。(完整代码)。
6.截取文件
FileChannel.truncate(longsize)可以截取指定的文件,指定大小之后的内容将被丢弃。size的值可以超过文件大小,超过的话不会截取任何内容,也不会增加任何内容。
FileChannelfileChannel=FileChannel.open(Paths.get("a.txt"),StandardOpenOption.WRITE); fileChannel.truncate(1); System.out.println(fileChannel.size());//输出1 fileChannel.write(ByteBuffer.wrap("Hello".getBytes())); System.out.println(fileChannel.size());//输出5 fileChannel.force(true); fileChannel.close();7.映射文件到直接内存
文件通道FileChannel可以将文件的指定范围映射到程序的地址空间中,映射部分使用字节缓冲区的一个子类MappedByteBuffer的对象表示,只要对映射字节缓冲区进行操作就能够达到操作文件的效果。与之相对应的,前面介绍的内容是通过操作文件通道和堆内存中的字节缓冲区HeapByteBuffer来达到操作文件的目的。
通过ByteBuffer.allocate()分配的缓冲区是一个HeapByteBuffer,存在于JVM堆中;而FileChannle.map()将文件映射到直接内存,返回的是一个MappedByteBuffer,存在于堆外的直接内存中;这块内存在MappedByteBuffer对象本身被回收之前有效。
7.1内存映射原理
前面使用堆缓冲区ByteBuffer和文件通道FileChannel对文件的操作使用的是read()/write()系统调用。读取数据时数据从I/O设备读到内核缓存,再从内核缓存复制到用户空间缓存,这里是JVM的堆内存。而映射磁盘文件是使用mmap()系统调用,将文件的指定部分映射到程序地址空间中;数据交互发生在I/O设备于用户空间之间,不需要经过内核空间。
虽然映射磁盘文件减少了一次数据复制,但对于大多数操作系统来说,将文件映射到内存这个操作本身开销较大;如果操作的文件很小,只有数十KB,映射文件所获得的好处将不及其开销。因此,只有在操作大文件的时候才将其映射到直接内存。
7.2映射缓冲区用法
文件通道FileChanle通过成员方法map(MapModemode,longposition,longsize)将文件映射到应用内存。
FileChannelfileChannel=FileChannel.open(Paths.get("a.txt"),StandardOpenOption.READ,StandardOpenOption.WRITE);//以读写的方式打开文件通道 MappedByteBufferbuf=fileChannel.map(FileChannel.MapMode.READ_WRITE,0,fileChannel.size());//将整个文件映射到内存mode表示打开模式,为枚举值,其值可以为READ_ONLY,READ_WRITE,PRIVATE。
+模式为READ_ONLY时,不能对buf进行写操作;
+模式为READ_WRITE时,通道fileChannel必须具有读写文件的权限;对buf进行的写操作将对文件生效,但不保证立即同步到I/O设备;
+模式为PRIVATE时,通道fileChannle必须对文件有读写权限;但是对文件的修改操作不会传播到I/O设备,而是会在内存复制一份数据。此时对文件的修改对其它线程和进程不可见。position指定文件的开始映射到内存的位置;
size指定映射的大小,值为非负int型整数。
调用map()方法之后,返回的MappedByteBuffer就于fileChannel脱离了关系,关闭fileChannel对buf没有影响。同时,如果要确保对buf修改的数据能够同步到文件I/O设备中,需要调用MappedByteBuffer中的无参数的force()方法,而调用FileChannel中的force(metaData)方法无效。
此时可以通过操作缓冲区来操作文件了。不过映射的内容存在于JVM程序的堆外内存中,这部分内存是虚拟内存,意味着buf中的内容不一定都在物理内存中,要让这些内容加载到物理内存,可以调用MappedByteBuffer中的load()方法。另外,还可以调用isLoaded()来判断buf中的内容是否在物理内存中。
FileChannelfileChannel=FileChannel.open(Paths.get("a.txt"),StandardOpenOption.WRITE,StandardOpenOption.READ); MappedByteBufferbuf=fileChannel.map(FileChannel.MapMode.READ_WRITE,0,fileChannel.size()); fileChannel.close();//关于文件通道对buf没有影响 System.out.println(buf.capacity());//输出fileChannel.size() System.out.println(buf.limit());//输出fileChannel.size() System.out.println(buf.position());//输出0 buf.put((byte)'R');//写入内容 buf.compact();//截掉positoin之前的内容 buf.force();//将数据刷出到I/O设备8.小结
1)文件通道FileChannel能够将数据从I/O设备中读入(read)到字节缓冲区中,或者将字节缓冲区中的数据写入(write)到I/O设备中。
2)文件通道能够转换到(transferTo)一个可写通道中,也可以从一个可读通道转换而来(transferFrom)。这种方式使用于通道之间地数据传输,比使用缓冲区更加高效。
3)文件通道能够将文件的部分内容映射(map)到JVM堆外内存中,这种方式适合处理大文件,不适合处理小文件,因为映射过程本身开销很大。
4)在对文件进行重要的操作之后,应该将数据刷出刷出(force)到磁盘,避免操作系统崩溃导致的数据丢失。
到此这篇关于JavaNIO文件通道FileChannel用法的文章就介绍到这了,更多相关JavaNIO文件通道FileChannel内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!