java nio的学习-6

来源:互联网 发布:cname 域名跳转 编辑:程序博客网 时间:2024/05/16 06:57

FileChannel  文件通道--------------------->>>>>>>>>>>>>>>>>>>(文件通道总是阻塞式的,因此不能被置于非阻塞模式)


FileChannel 类可以实现常用的 read ,write以及scatter/gather 操作,同时它也提供了很多专用于文件的新方法。

现代操作系统都有复杂的缓存和预取机制,使得本地磁盘 I/O 操作延迟很少。网络文件系统一般而言延迟会多些,不过却也因该优化而受益。面向流的 I/O 的非阻塞范例对于面向文件的操作并无多大意义,这是由文件 I/O 本质上的不同性质造成的。对于文件 I/O,最强大之处在于异步 I/O(asynchronous I/O),它允许一个进程可以从操作系统请求一个或多个 I/O 操作而不必等待这些操作的完成。发起请求的进程之后会收到它请求的I/O 操作已完成的通知。异步 I/O 是一种高级性能,当前的很多操作系统都还不具备。以后的NIO 增强也会把异步 I/O 纳入考虑范围。

同大多数通道一样,只要有可能,FileChannel 都会尝试使用本地 I/O 服务。FileChannel 类本身是抽象的,您从 getChannel( )方法获取的实际对象是一个具体子类(subclass)的一个实例(instance),该子类可能使用本地代码来实现以上 API 方法中的一些或全部。

File channels are safe for use by multiple concurrent threads.

FileChannel 对象是线程安全(thread -safe )的。多个进程可以在同一个实例上并发调用方法而不会引起任何问题,不过并非所有的操作都是多线程的(multithreaded)。影响通道位置或者影响文件大小的操作都是单线程的(single-threaded )。如果有一个线程已经在执行会影响通道位置或文件大小的操作,那么其他尝试进行此类操作之一的线程必须等待。并发行为也会受到底层的操作系统或文件系统影响。

同大多数 I/O 相关的类一样,FileChannel 是一个反映 Java 虚拟机外部一个具体对象的抽象。FileChannel 类保证同一个 Java 虚拟机上的所有实例看到的某个文件的视图均是一致的,但是 Java虚拟机却不能对超出它控制范围的因素提供担保。通过一个 FileChannel 实例看到的某个文件的视图同通过一个外部的非 Java 进程看到的该文件的视图可能一致,也可能不一致。多个进程发起的并发文件访问的语义高度取决于底层的操作系统和(或)文件系统。一般而言,由运行在不同 Java虚拟机上的 FileChannel 对象发起的对某个文件的并发访问和由非 Java 进程发起的对该文件的并发访问是一致的


访问文件:每个FileChannel 对象都同一个文件描述符(file descriptor )有一对一的关系。您可能也注意到了FileChannel的 API 方法同 java.io包中 RandomAccessFile 类的方法的相似之处了本质上讲,RandomAccessFile 类提供的是同样的抽象内容。在通道出现之前,底层的文件操作都是通过 RandomAccessFile 类的方法来实现FileChannel 模拟同样的 I/O 服务,因此它的API 自然也是很相似的


public abstract class FileChannel
extends AbstractInterruptibleChannel
implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
public abstract class FileCh annel extends AbstractChannel  implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // This is a partial API listing public abstract long position() public abstract void position (long newPosition)  public abstract int read (ByteBuffer dst) public abstract int read (ByteBuffer dst, long position) public abstract int write (ByteBuffer src) public abstract int write (ByteBuffer src, long position) public abstract long size()  public abstract void truncate (long size) public abstract void force (boolean metaData)  }

同底层的文件描述符一样,每个 FileChannel 都有一个叫“file position” 的概念。这个 position值决定文件中哪一处的数据接下来将被读或者写。从这个方面看,FileChannel 类同缓冲区很类似,并且MappedByteBuffer类使得我们可以通过 ByteBuffer API 来访问文件数据。


(一个有符号长整型(signed   long)值能代表多达9,223,372,036,854,775,80 7 字节的文件大小。这个数据量差不多是840 万TB,可以在您本地电脑上填满大约9000万个100-GB的磁盘驱动器。)


文件空洞究竟是什么? 
 
当磁盘上一个文件的分配空间小于它的文件大小时会出现“文件空洞”。对于内容稀疏的文件,大多数现代文件系统只为实际写入的数据分配磁盘空间(更准确地说,只为那些写入数据的文件系统页分配空间)。假如数据被写入到文件中非连续的位置上,这将导致文件出现在逻辑上不包含数据的区域(即“空洞”)。例如,下面的代码可能产生一个如下图所示的文件: 

package com.ronsoft.books.nio.channels;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;import java.io.File;import java.io.RandomAccessFile;import java.io.IOException;/** * Create a file with holes in it *  * @author Ron Hitchens (ron@ronsoft.com) */public class FileHole {public static void main(String[] argv) throws IOException {// Create a temp file, open for writing, and get// a FileChannelFile temp = File.createTempFile("holy", null);RandomAccessFile file = new RandomAccessFile(temp, "rw");FileChannel channel = file.getChannel();// Create a working bufferByteBuffer byteBuffer = ByteBuffer.allocateDirect(100);putData(0, byteBuffer, channel);putData(5000000, byteBuffer, channel);putData(50000, byteBuffer, channel);// Size will report the largest position written, but// there are two holes in this file. This file will// not consume 5 MB on disk (unless the filesystem is// extremely brain -damaged)System.out.println("Wrote temp file '" + temp.getPath() + "', size="+ channel.size());channel.close();file.close();}private static void putData(int position, ByteBuffer buffer,FileChannel channel) throws IOException {String string = "*<-- location " + position;buffer.clear();buffer.put(string.getBytes("US-ASCII"));buffer.flip();channel.position(position);channel.write(buffer);}}
输出结果:
Wrote temp file 'C:\DOCUME~1\wuli\LOCALS~1\Temp\holy7438794624816759725.tmp', size=5000021

如果该文件被顺序读取的话,所有空洞都会被“0”填充但不占用磁盘空间。读取该文件的进程会看到 5,000,021 个字节,大部分字节都以“0”表示。试试在该文件上运行 strings 命令,看看您会得到什么。再试试将文件大小的值提高到 50或100MB,看看您的全部磁盘空间消耗以及顺序扫描该文件所需时间会发生何种变化(前者不会改变,但是后者将有非常大的增加)。


从这里,我大概理解了file position的意思了,应该是类似于写入或读入点的意思。。。。。。。。。。。。。。。。。


FileChannel 位置(position)是从底层的文件描述符获得的,该 position同时被作为通道引用获取来源的文件对象共享。这也就意味着一个对象对该position的更新可以被另一个对象看到: 

RandomAccessFile randomAccessFile = new RandomAccessFile("filename","r");// Set the file positionrandomAccessFile.seek(1000);// Create a channel from the fileFileChannel fileChannel = randomAccessFile.getChannel();// This will print "1000"System.out.println("file pos: " + fileChannel.position());// Change the position using the RandomAccessFile objectrandomAccessFile.seek(500);// This will print "500"System.out.println("file pos: " + fileChannel.position());// Change the position using the FileChannel objectfileChannel.position(200);// This will print "200"System.out.println("file pos: " + randomAccessFile.getFilePointer());

类似于缓冲区的 get( )  和put( )方法,当字节被 read( ) 或write( ) 方法传输时,文件 position会自动更新。如果 position值达到了文件大小的值(文件大小的值可以通过 size( )方法返回),read( )方法会返回一个文件尾条件值(- 1)。可是,不同于缓冲区的是,如果实现 write( ) 方法时 position前进到超过文件大小的值,该文件会扩展以容纳新写入的字节

read(ByteBuffer dst, long position)
          Reads a sequence of bytes from this channel into the given buffer, starting at the given file position.

write(ByteBuffer src, long position)
          Writes a sequence of bytes to this channel from the given buffer, starting at the given file position.


同样类似于缓冲区,也有带 position参数的绝对形式的 read( ) 和  write( ) 方法。这种绝对形式的方法在返回值时不会改变当前的文件 position。由于通道的状态无需更新,因此绝对的读和写可能会更加有效率,操作请求可以直接传到本地代码。更妙的是,多个线程可以并发访问同一个文件而不会相互产生干扰。这是因为每次调用都是原子性的(atomic ),并不依靠调用之间系统所记住的状态。


尝试在文件末尾之外的 position进行一个绝对读操作,size( ) 方法会返回一个 end-of -file 。在超出文件大小的 position上做一个绝对 write( ) 会导致文件增加以容纳正在被写入的新字节。文件中位于之前 end-of -file 位置和新添加的字节起始位置之间区域的字节的值不是由 FileChannel 类指定,而是在大多数情况下反映底层文件系统的语义。取决于操作系统和(或)文件系统类型,这可能会导致在文件中出现一个空洞


trunca te( ) 都会产生副作用:文件的position会被设置为所提供的新 size 值。

truncate

public abstract FileChannel truncate(long size)                              throws IOException
Truncates this channel's file to the given size.

If the given size is less than the file's current size then the file is truncated, discarding any bytes beyond the new end of the file. If the given size is greater than or equal to the file's current size then the file is not modified. In either case, if this channel's file position is greater than the given size then it is set to that size. 

force

public abstract void force(boolean metaData)                    throws IOException
Forces any updates to this channel's file to be written to the storage device that contains it. 
所有的现代文件系统都会缓存数据和延迟磁盘文件更新以提高性能。调用 force( ) 方法要求文件的所有待定修改立即同步到磁盘。 

如果文件位于一个本地文件系统,那么一旦 force( ) 方法返回,即可保证从通道被创建(或上次调用 force( ) )时起的对文件所做的全部修改已经被写入到磁盘。对于关键操作如事务(transaction )处理来说,这一点是非常重要的,可以保证数据完整性和可靠的恢复。然而,如果文件位于一个远程的文件系统,如 NFS 上,那么不能保证待定修改一定能同步到永久存储器(permanent storage)上,因 Java 虚拟机不能做操作系统或文件系统不能实现的承诺。如果您的程序在面临系统崩溃时必须维持数据完整性,先去验证一下您在使用的操作系统和(或)文件系统在同步修改方面是可以依赖的。force( ) 方法的布尔型参数表示在方法返回值前文件的元数据(metadata)是否也要被同步更新磁盘。元数据指文件所有者、访问权限、最后一次修改时间等信息。大多数情形下,该信息对数据恢复而言是不重要的。给 force( ) 方法传递 false 值表示在方法返回前只需要同步文件数据的更改。大多数情形下,同步元数据要求操作系统进行至少一次额外的底层 I/O 操作。一些大数量事务处理程序可能通过在每次调用 force( ) 方法时不要求元数据更新来获取较高的性能提升,同时也不会牺牲数据完整性。 

------------->>>>>>>>>>对于需要特别保证数据完整性的应用程序,请务必验证一下您计划部署该程序的操作环境的性能。


文件锁定:在JDK 1.4 版本之前,Java I/O 模型都未能提供文件锁定(file locking),缺少这一特性让人们很头疼。绝大多数现代操作系统早就有了文件锁定功能,而直到 JDK 1.4 版本发布时 Java 编程人员才可以使用文件锁(file lock)。在集成许多其他非 Java 程序时,文件锁定显得尤其重要。此外,它在判优(判断多个访问请求的优先级别)一个大系统的多个 Java 组件发起的访问时也很有价值。锁(lock )可以是共享的(shared )或独占的(exclusive )。本节中描述的文件锁定特性在很大程度上依赖本地的操作系统实现。并非所有的操作系统和文件系统都支持共享文件锁。对于那些不支持的,对一个共享锁的请求会被自动提升为对独占锁的请求。这可以保证准确性却可能严重影响性能。如果您计划部署程序,请确保您了解所用操作系统和文件系统的文件锁定行为,因为这将严重影响您的设计选择。另外,并非所有平台都以同一个方式来实现基本的文件锁定。在不同的操作系统上,甚至在同一个操作系统的不同文件系统上,文件锁定的语义都会有所差异。一些操作系统仅提供劝告锁定(advisory locking ),一些仅提供独占锁(exclusive locks),而有些操作系统可能两种锁都提供。您应该总是按照劝告锁的假定来管理文件锁,因为这是最安全的。但是如能了解底层操作系统如何执行锁定也是非常好的。例如,如果所有的锁都是强制性的(mandatory)而您不及时释放您获得的锁的话,运行在同一操作系统上的其他程序可能会受到影响。 


---------------》》》》有关FileChannel 实现的文件锁定模型的一个重要注意项是:锁的对象是文件而不是通道或线程,这意味着文件锁不适用于判优同一台 Java 虚拟机上的多个线程发起的访问。


如果一个线程在某个文件上获得了一个独占锁,然后第二个线程利用一个单独打开的通道来请求该文件的独占锁,那么第二个线程的请求会被批准。但如果这两个线程运行在不同的 Java 虚拟机上,那么第二个线程会阻塞,因为锁最终是由操作系统或文件系统来判优的并且几乎总是在进程级而非线程级上判优。锁都是与一个文件关联的,而不是与单个的文件句柄或通道关联。 

锁与文件关联,而不是与通道关联。我们使用锁来判优外部进程,而不是判优同一个 Java 虚拟机上的线程。


如果您需要控制多个 Java 线程的并发访问,您可能需要实施您自己的、轻量级的锁定方案。那种情形下,内存映射文件可能是一个合适的选择


public abstract class FileChannel  extends AbstractChannel  implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // This is a partial API listing  public final FileLock lock() public abstract FileLock lock (long position, long size, boolean shared) public final FileLock tryLock() public abstract FileLock tryLock (long position, long size, boolean shared) }


tryLock()
          Attempts to acquire an exclusive lock on this channel's file.

tryLock(long position, long size, boolean shared)
          Attempts to acquire a lock on the given region of this channel's file.

lock()
          Acquires an exclusive lock on this channel's file.

lock(long position, long size, boolean shared)
          Acquires a lock on the given region of this channel's file.
锁是在文件内部区域上获得的。调用带参数的 Lock( )方法会指定文件内部锁定区域的开始 position 以及锁定区域的 size。第三个参数  shared 表示您想获取的锁是共享的(参数值为 true)还是独占的(参数值为 false )。要获得一个共享锁,您必须先以只读权限打开文件,而请求独占锁时则需要写权限。另外,您提供的 position和size 参数的值不能是负数。

锁定区域的范围不一定要限制在文件的 size 值以内,锁可以扩展从而超出文件尾。因此,我们可以提前把待写入数据的区域锁定,我们也可以锁定一个不包含任何文件内容的区域,比如文件最后一个字节以外的区域。如果之后文件增长到达那块区域,那么您的文件锁就可以保护该区域的文件内容了。相反地,如果您锁定了文件的某一块区域,然后文件增长超出了那块区域,那么新增加的文件内容将不会受到您的文件锁的保护。


不带参数的简单形式的 lock( ) 方法是一种在整个文件上请求独占锁的便捷方法,锁定区域等于它能达到的最大范围。该方法等价于: 

fileChannel.lock (0L, Long.MAX_VALUE, false);

如果您正请求的锁定范围是有效的,那么 lock( ) 方法会阻塞,它必须等待前面的锁被释放。假如您的线程在此情形下被暂停,该线程的行为受中断语义控制。如果通道被另外一个线程关闭,该暂停线程将恢复并产生一个 AsynchronousCloseException 异常。假如该暂停线程被直接中断(通过调用它的 interrupt( ) 方法),它将醒来并产生一个FileLockInterruptionException 异常。如果在调用 lock( ) 方法时线程的 interrupt status 已经被设置,也会产生 FileLockInterruptionException 异常。

public final FileLock tryLock()                       throws IOException
Attempts to acquire an exclusive lock on this channel's file.

An invocation of this method of the form fc.tryLock() behaves in exactly the same way as the invocation

     fc.tryLock(0L, Long.MAX_VALUE, false) 
FileLock (线程安全的)类封装一个锁定的文件区域。Fi leLock 对象由 FileChannel 创建并且总是关联到那个特定的通道实例。您可以通过调用 channel( )方法来查询一个 lock 对象以判断它是由哪个通道创建的。

一个FileLock 对象创建之后即有效,直到它的 release( ) 方法被调用或它所关联的通道被关闭或Java 虚拟机关闭时才会失效。我们可以通过调用 isValid( ) 布尔方法来测试一个锁的有效性。一个锁的有效性可能会随着时间而改变,不过它的其他属性——位置(position)、范围大小(size )和独占性(exclusivity)——在创建时即被确定,不会随着时间而改变

要是需要保证一定能在期望的区域上获得一个锁,但是 Java 虚拟机上的其他地方或者外部进程可能已经在该期望区域上有一个或多个锁了。您最好使用 tryLock( ) 方法确认一下。 

尽管一个 FileLock 对象是与某个特定的 FileChannel 实例关联的,它所代表的锁却是与一个底层文件关联的,而不是与通道关联。因此,如果您在使用完一个锁后而不释放它的话,可能会导致冲突或者死锁。请小心管理文件锁以避免出现此问题。一旦您成功地获取了一个文件锁,如果随后在通道上出现错误的话,请务必释放这个锁。推荐使用类似下面的代码形式:

FileLock lock = fileChannel.lock() try { <perform read/write/whatever on channel> } catch (IOException) [  <handle unexpected exception> } finally { lock.release() }

下面的例子是共享锁同独占锁交互 :(以下代码直接忽略了之前说给的用 try/catch/finally来释放锁的建议,在您自己所写的实际代码中请不要这么懒。)

package com.ronsoft.books.nio.channels;import java.nio.ByteBuffer;import java.nio.IntBuffer;import java.nio.channels.FileChannel;import java.nio.channels.FileLock;import java.io.RandomAccessFile;import java.util.Random;/** * Test locking with FileChannel. Run one copy of this code with arguments * "-w /tmp/locktest.dat" and one or more copies with "-r /tmp/locktest.dat" to * see the interactions of exclusive and shared locks. Note how too many readers * can starve out the writer. Note: The filename you provide will be * overwritten. Substitute an appropriate temp filename for your favorite OS. *  * Created April, 2002 *  * @author Ron Hitchens (ron@ronsoft.com) */public class LockTest {private static final int SIZEOF_INT = 4;private static final int INDEX_START = 0;private static final int INDEX_COUNT = 10;private static final int INDEX_SIZE = INDEX_COUNT * SIZEOF_INT;private ByteBuffer buffer = ByteBuffer.allocate(INDEX_SIZE);private IntBuffer indexBuffer = buffer.asIntBuffer();private Random rand = new Random();public static void main(String[] argv) throws Exception{boolean writer = false;String filename;if (argv.length != 2) {System.out.println("Usage: [ -r | -w ] filename");return;}writer = argv[0].equals("-w");filename = argv[1];RandomAccessFile raf = new RandomAccessFile(filename, (writer) ? "rw": "r");FileChannel fc = raf.getChannel();LockTest lockTest = new LockTest();if (writer) {lockTest.doUpdates(fc);} else {lockTest.doQueries(fc);}}// ----------------------------------------------------------------// Simulate a series of read -only queries while// holding a shared lock on the index areavoid doQueries(FileChannel fc) throws Exception {while (true) {println("trying for shared lock...");FileLock lock = fc.lock(INDEX_START, INDEX_SIZE, true);int reps = rand.nextInt(60) + 20;for (int i = 0; i < reps; i++) {int n = rand.nextInt(INDEX_COUNT);int position = INDEX_START + (n * SIZEOF_INT);buffer.clear();fc.read(buffer, position);int value = indexBuffer.get(n);println("Index entry " + n + "=" + value);// Pretend to be doing some workThread.sleep(100);}lock.release();println("<sleeping>");Thread.sleep(rand.nextInt(3000) + 500);}}// Simulate a series of updates to the index area// while holding an exclusive lockvoid doUpdates(FileChannel fc) throws Exception {while (true) {println("trying for exclusive lock...");FileLock lock = fc.lock(INDEX_START, INDEX_SIZE, false);updateIndex(fc);lock.release();println("<sleeping>");Thread.sleep(rand.nextInt(2000) + 500);}}// Write new values to the index slotsprivate int idxval = 1;private void updateIndex(FileChannel fc) throws Exception {// "indexBuffer" is an int view of "buffer"indexBuffer.clear();for (int i = 0; i < INDEX_COUNT; i++) {idxval++;println("Updating index " + i + "=" + idxval);indexBuffer.put(idxval);// Pretend that this is really hard workThread.sleep(500);}// leaves position and limit correct for whole bufferbuffer.clear();fc.write(buffer, INDEX_START);}// ----------------------------------------------------------------private int lastLineLen = 0;// Specialized println that repaints the current lineprivate void println(String msg) {System.out.print("\r ");System.out.print(msg);for (int i = msg.length(); i < lastLineLen; i++) {System.out.print(" ");}System.out.print("\r");System.out.flush();lastLineLen = msg.length();}}


内存映射文件:FileChannel 类提供了一个名为 map( )的方法,该方法可以在一个打开的文件和一个特殊类型的 ByteBuffer之间建立一个虚拟内存映射。

在 FileChannel 上调用 map( )方法会创建一个由磁盘文件支持的虚拟内存映射(virtual memory mapp ing)并在那块虚拟内存空间外部封装一个 MappedByteBuffer对象。

public abstract MappedByteBuffer map(FileChannel.MapMode mode,                                     long position,                                     long size)                              throws IOException
Maps a region of this channel's file directly into memory.
由map( )方法返回的 MappedByteBuffer对象的行为在多数方面类似一个基于内存的缓冲区,只不过该对象的数据元素存储在磁盘上的一个文件中。调用 get( )方法会从磁盘文件中获取数据,此数据反映该文件的当前内容,即使在映射建立之后文件已经被一个外部进程做了修改。通过文件映射看到的数据同您用常规方法读取文件看到的内容是完全一样的。相似地,对映射的缓冲区实现一个put( )会更新磁盘上的那个文件(假设对该文件您有写的权限),并且您做的修改对于该文件的其他阅读者也是可见的。

通过内存映射机制来访问一个文件会比使用常规方法读写高效得多,甚至比使用通道的效率都高。因为不需要做明确的系统调用,那会很消耗时间。更重要的是,操作系统的虚拟内存可以自动缓存内存页(memory page)。这些页是用系统内存来缓存的,所以不会消耗 Java 虚拟机内存堆(memory heap)。


一旦一个内存页已经生效(从磁盘上缓存进来),它就能以完全的硬件速度再次被访问而不需要再次调用系统命令来获取数据。那些包含索引以及其他需频繁引用或更新的内容的巨大而结构化文件能因内存映射机制受益非常多如果同时结合文件锁定来保护关键区域和控制事务原子性,那您将能了解到内存映射缓冲区如何可以被很好地利用

下面让我们来看一下如何使用内存映射:

public abstract class FileChannel  extends AbstractChannel  implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // This is a partial API listing public abstract MappedByteBuffer map (MapMode mode, long position,long size) public static class MapMode { public static final MapMode READ_ONLY public static final MapMode READ_WRITE  public static final MapMode PRIVATE } } 
例如,要映射 100 到199(包含199)位置的字节,可以使用下面的代码: 
buffer = fileChannel.map (FileChannel.MapMode.READ_ONLY, 100, 200); 
如果要映射整个文件则使用:

buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
映射文件的范围不应超过文件的实际大小。如果您请求一个超出文件大小的映射,文件会被增大以匹配映射的大小。假如您给 size 参数传递的值是Integer.MAX_VALUE,文件大小的值会膨胀到超过 2.1GB 。即使您请求的是一个只读映射,map( )方法也会尝试这样做并且大多数情况下都会抛出一个 IOException异常,因为底层的文件不能被修改。该行为同之前讨论的文件“空洞”的行为是一致的。

请求的映射模式将受被调用 map( )方法的 FileChannel 对象的访问权限所限制。如果通道是以只读的权限打开的而您却请求 MapMode.READ_WRITE 模式,那么map( )方法会抛出一个 NonWritableChannelException 异常;如果您在一个没有读权限的通道上请求MapMode.READ_ONLY映射模式,那么将产生 NonReadableChannelException 异常。不过在以read/write权限打开的通道上请求一个 MapMode.READ_ONLY映射却是允许的。MappedByteBuffer对象的可变性可以通过对它调用 isReadOnly( ) 方法来检查。
第三种模式 MapMode.PRIVATE 表示您想要一个写时拷贝(copy -on-write)的映射。这意味着您通过 put( )方法所做的任何修改都会导致产生一个私有的数据拷贝并且该拷贝中的数据只有MappedByteBuffer实例可以看到。该过程不会对底层文件做任何修改,而且一旦缓冲区被施以垃圾收集动作(garbage collected ),那些修改都会丢失。尽管写时拷贝的映射可以防止底层文件被修改,您也必须以read/write权限来打开文件以建立 MapMode.PRIVATE 映射。只有这样,返回的MappedByteBuffer 对象才能允许使用 put( )方法。


写时拷贝这一技术经常被操作系统使用!在处理同一文件的多个映射时也有相同的优势(当然,这需要底层操作系统的支持)。假设一个文件被多个 MappedByteBuffer对象映射并且每个映射都是 MapMode.PRIVATE 模式,那么这份文件的大部分内容都可以被所有映射共享。


映射缓冲区没有绑定到创建它们的通道上。关闭相关联的 FileChannel 不会破坏映射,只有丢弃缓冲区对象本身才会破坏该映射。NIO 设计师们之所以做这样的决定是因为当关闭通道时破坏映射会引起安全问题,而解决该安全问题又会导致性能问题。如果您确实需要知道一个映射是什么时候被破坏的,他们建议使用虚引用(phantom references ,参见 java.lang.ref.PhantomReference )和一个 cleanup线程。不过有此需要的概率是微乎其微的。


因为MappedByteBuffers 也是 ByteBuffers ,所以能够被传递 SocketChannel 之类通道的 read( ) 或write( ) 以有效传输数据给被映射的文件或从被映射的文件读取数据。如能再结合 scatter/gather,那么从内存缓冲区和被映射文件内容中组织数据就变得很容易了。下例 就是以此方式写 HTTP 回应的。

package com.ronsoft.books.nio.channels;import java.nio.ByteBuffer;import java.nio.MappedByteBuffer;import java.nio.channels.FileChannel;import java.nio.channels.FileChannel.MapMode;import java.nio.channels.GatheringByteChannel;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.net.URLConnection;/** * 诠释了内存映射缓冲区如何同 scatter/gather 结合使用  * Dummy HTTP server using MappedByteBuffers. * Given a filename on the command line, pretend to be a web server and generate * an HTTP response containing the file content preceded by appropriate headers. * The data is sent with a gathering write. *  * @autho r Ron Hitchens (ron@ronsoft.com) */public class MappedHttp {private static final String OUTPUT_FILE = "MappedHttp.out";private static final String LINE_SEP = "\r\n";private static final String SERVER_ID = "Server: Ronsoft Dummy Server";private static final String HTTP_HDR = "HTTP/1.0 200 OK" + LINE_SEP+ SERVER_ID + LINE_SEP;private static final String HTTP_404_HDR = "HTTP/1.0 404 Not Found"+ LINE_SEP + SERVER_ID + LINE_SEP;private static final String MSG_404 = "Could not open file: ";public static void main(String[] argv) throws Exception {if (argv.length < 1) {System.err.println("Usage: filename");return;}String file = argv[0];ByteBuffer header = ByteBuffer.wrap(bytes(HTTP_HDR));ByteBuffer dynhdrs = ByteBuffer.allocate(128);ByteBuffer[] gather = { header, dynhdrs, null };String contentType = "unknown/unknown";long contentLength = -1;try {FileInputStream fis = new FileInputStream(file);FileChannel fc = fis.getChannel();MappedByteBuffer filedata = fc.map(MapMode.READ_ONLY, 0, fc.size());gather[2] = filedata;contentLength = fc.size();contentType = URLConnection.guessContentTypeFromName(file);} catch (IOException e) {// file could not be opened; report problemByteBuffer buf = ByteBuffer.allocate(128);String msg = MSG_404 + e + LINE_SEP;buf.put(bytes(msg));buf.flip();// Use the HTTP error responsegather[0] = ByteBuffer.wrap(bytes(HTTP_404_HDR));gather[2] = buf;contentLength = msg.length();contentType = "text/plain";}StringBuffer sb = new StringBuffer();sb.append("Content-Length: " + contentLength);sb.append(LINE_SEP);sb.append("Content-Type: ").append(contentType);sb.append(LINE_SEP).append(LINE_SEP);dynhdrs.put(bytes(sb.toString()));dynhdrs.flip();FileOutputStream fos = new FileOutputStream(OUTPUT_FILE);FileChannel out = fos.getChannel();// All the buffers have been prepared; write 'em outwhile (out.write(gather) > 0) {// Empty body; loop until all buffers are empty}out.close();System.out.println("output written to " + OUTPUT_FILE);}// Convert a string to its constituent bytes// from the ASCII character setprivate static byte[] bytes(String string) throws Exception {return (string.getBytes("US -ASCII"));}}

FileInputStream fis = new FileInputStream(file);FileChannel fc = fis.getChannel();MappedByteBuffer filedata = fc.map(MapMode.READ_ONLY, 0, fc.size());

到现在为止,我们已经讨论完了映射缓冲区同其他缓冲区相同的特性,这些也是您会用得最多的。不过 MappedByteBuffer还定义了几个它独有的方法: 
public abstract class MappedByteBuffer  extends ByteBuffer  { // This is a partial API listing public final MappedByteBuffer load() public final boolean isLoaded() public final MappedByteBuffer force()  }

当我们为一个文件建立虚拟内存映射之后,文件数据通常不会因此被从磁盘读取到内存(这取决于操作系统)。该过程类似打开一个文件:文件先被定位,然后一个文件句柄会被创建,当您准备好之后就可以通过这个句柄来访问文件数据。对于映射缓冲区,虚拟内存系统将根据您的需要来把文件中相应区块的数据读进来。这个页验证或防错过程需要一定的时间,因为将文件数据读取到内存需要一次或多次的磁盘访问。某些场景下,您可能想先把所有的页都读进内存以实现最小的缓冲区访问延迟。如果文件的所有页都是常驻内存的,那么它的访问速度就和访问一个基于内存的缓冲区一样了。

load( ) 方法会加载整个文件以使它常驻内存。在一个映射缓冲区上调用 load( ) 方法会是一个代价高的操作,因为它会导致大量的页调入(page -in ),具体数量取决于文件中被映射区域的实际大小。然而,load( ) 方法返回并不能保证文件就会完全常驻内存,这是由于请求页面调入(demand paging)是动态的。请小心使用 load( ) 方法,它可能会导致您不希望出现的结果。该方法的主要作用是为提前加载文件埋单,以便后续的访问速度可以尽可能的快。

对于那些要求近乎实时访问(near -realtime access)的程序,解决方案就是预加载。但是请记住,不能保证全部页都会常驻内存,不管怎样,之后可能还会有页调入发生。

对于大多数程序,特别是交互性的或其他事件驱动(event-driven )的程序而言,为提前加载文件消耗资源是不划算的。在实际访问时分摊页调入开销才是更好的选择。让操作系统根据需要来调入页意味着不访问的页永远不需要被加载。同预加载整个被映射的文件相比,这很容易减少 I/O 活动总次数。操作系统已经有一个复杂的内存管理系统了,就让它来替您完成此工作吧!


isLoaded( ) 方法的返回值只是一个暗示,由于垃圾收集的异步性质、底层操作系统以及运行系统的动态性等因素,想要在任意时刻准确判断全部映射页的状态是不可能的。

当用 MappedByteBuffer对象来更新一个文件,您应该总是使用 MappedByteBuffer.force( )而非FileChannel.force( ),因为通道对象可能不清楚通过映射缓冲区做出的文件的全部更改。


三种类型的内存映射缓冲区举例:具体来说,例中代码诠释了写时拷贝是如何页导向(page -oriented)的。当在使用 MAP_MODE.PRIVATE 模式创建的 MappedByteBuffer 对象上调用put( )方法而引发更改时,就会生成一个受影响页的拷贝。这份私有的拷贝不仅反映本地更改,而且使缓冲区免受来自外部对原来页更改的影响。然而,对于被映射文件其他区域的更改还是可以看到的。

package com.ronsoft.books.nio.channels;import java.nio.ByteBuffer;import java.nio.MappedByteBuffer;import java.nio.channels.FileChannel;import java.io.File;import java.io.RandomAccessFile;/** * Test behavior of Memory mapped buffer types. Create a file, write some data * to it, then create three different types of mappings to it. Observe the * effects of changes through the buffer APIs and updating the file directly. * The data spans page boundaries to illustrate the page-oriented nature of Copy * -On-Write mappings. *  * @author Ron Hitchens (ron@ronsoft.com) */public class MapFile {public static void main(String[] argv) throws Exception {// Create a temp file and get a channel connected to itFile tempFile = File.createTempFile("mmaptest", null);RandomAccessFile file = new RandomAccessFile(tempFile, "rw");FileChannel channel = file.getChannel();ByteBuffer temp = ByteBuffer.allocate(100);// Put something in the file, starting at location 0temp.put("This is the file content".getBytes());temp.flip();channel.write(temp, 0);// Put something else in the file, starting at location 8192.// 8192 is 8 KB, almost certainly a different memory/FS page.// This may cause a file hole, depending on the// filesystem page size.temp.clear();temp.put("This is more file content".getBytes());temp.flip();channel.write(temp, 8192);// Create three types of mappings to the same fileMappedByteBuffer ro = channel.map(FileChannel.MapMode.READ_ONLY, 0,channel.size());MappedByteBuffer rw = channel.map(FileChannel.MapMode.READ_WRITE, 0,channel.size());MappedByteBuffer cow = channel.map(FileChannel.MapMode.PRIVATE, 0,channel.size());// the buffer states before any modificationsSystem.out.println("Begin");showBuffers(ro, rw, cow);// Modify the copy -on-write buffercow.position(8);cow.put("COW".getBytes());System.out.println("Change to COW buffer");showBuffers(ro, rw, cow);// Modify the read/write bufferrw.position(9);rw.put(" R/W ".getBytes());rw.position(8194);rw.put(" R/W ".getBytes());rw.force();System.out.println("Change to R/W buffer");showBuffers(ro, rw, cow);// Write to the file through the channel; hit both pagestemp.clear();temp.put("Channel write ".getBytes());temp.flip();channel.write(temp, 0);temp.rewind();channel.write(temp, 8202);System.out.println("Write on channel");showBuffers(ro, rw, cow);// Modify the copy -on-write buffer againcow.position(8207);cow.put(" COW2 ".getBytes());System.out.println("Second change to COW buffer");showBuffers(ro, rw, cow);// Modify the read/write bufferrw.position(0);rw.put(" R/W2 ".getBytes());rw.position(8210);rw.put(" R/W2 ".getBytes());rw.force();System.out.println("Second change to R/W buffer");showBuffers(ro, rw, cow);// cleanupchannel.close();file.close();tempFile.delete();}// Show the current content of the three bufferspublic static void showBuffers(ByteBuffer ro, ByteBuffer rw, ByteBuffer cow)throws Exception{dumpBuffer("R/O", ro);dumpBuffer("R/W", rw);dumpBuffer("COW", cow);System.out.println("");}// Dump buffer content, counting and skipping nullspublic static void dumpBuffer(String prefix, ByteBuffer buffer)throws Exception {System.out.print(prefix + ": '");int nulls = 0;int limit = buffer.limit();for (int i = 0; i < limit; i++) {char c = (char) buffer.get(i);if (c == '\u0000') {nulls++;continue;}if (nulls != 0) {System.out.print("|[" + nulls + " nulls]|");nulls = 0;}System.out.print(c);}System.out.println("'");}}

结果:
BeginR/O: 'This is the file content|[8168 nulls]|This is more file content'R/W: 'This is the file content|[8168 nulls]|This is more file content'COW: 'This is the file content|[8168 nulls]|This is more file content'Change to COW bufferR/O: 'This is the file content|[8168 nulls]|This is more file content'R/W: 'This is the file content|[8168 nulls]|This is more file content'COW: 'This is COW file content|[8168 nulls]|This is more file content'Change to R/W bufferR/O: 'This is t R/W le content|[8168 nulls]|Th R/W  more file content'R/W: 'This is t R/W le content|[8168 nulls]|Th R/W  more file content'COW: 'This is COW file content|[8168 nulls]|Th R/W  more file content'Write on channelR/O: 'Channel write le content|[8168 nulls]|Th R/W  moChannel write t'R/W: 'Channel write le content|[8168 nulls]|Th R/W  moChannel write t'COW: 'This is COW file content|[8168 nulls]|Th R/W  moChannel write t'Second change to COW bufferR/O: 'Channel write le content|[8168 nulls]|Th R/W  moChannel write t'R/W: 'Channel write le content|[8168 nulls]|Th R/W  moChannel write t'COW: 'This is COW file content|[8168 nulls]|Th R/W  moChann COW2 te t'Second change to R/W bufferR/O: ' R/W2 l write le content|[8168 nulls]|Th R/W  moChannel  R/W2 t'R/W: ' R/W2 l write le content|[8168 nulls]|Th R/W  moChannel  R/W2 t'COW: 'This is COW file content|[8168 nulls]|Th R/W  moChann COW2 te t'

Channel -to -Channel 传输:由于经常需要从一个位置将文件数据批量传输到另一个位置,FileChannel 类添加了一些优化方法来提高该传输过程的效率:

public abstract class FileChannel  extends AbstractChannel  implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // This is a partial API listing public abstract long transferTo (long position, long count, WritableByteChannel target) public abstract long transferFrom (ReadableByteChannel src,  long position, long count) }
transferTo( )和transferFrom( )方法允许将一个通道交叉连接到另一个通道,而不需要通过一个中间缓冲区来传递数据只有 FileChannel 类有这两个方法因此 channel-to -channel传输中通道之一必须是 FileChannel您不能在 socket 通道之间直接传输数据,不过 socket 通道实现WritableByteChannel 和ReadableByteChannel 接口,因此文件的内容可以用 transferTo( )方法传输给一个socket 通道,或者也可以用 transferFrom( )方法将数据从一个 socket 通道直接读取到一个文件中。

直接的通道传输不会更新与某个 FileChannel 关联的 position值。请求的数据传输将从position 参数指定的位置开始,传输的字节数不超过 count 参数的值。实际传输的字节数会由方法返回,可能少于您请求的字节数。


对于传输数据来源是一个文件的 transferTo( )方法,如果 position + count 的值大于文件的size 值,传输会在文件尾的位置终止。假如传输的目的地是一个非阻塞模式的 socket 通道,那么当发送队列(send queue )满了之后传输就可能终止,并且如果输出队列(output queue)已满的话可能不会发送任何数据。类似地,对于 transferFrom( )方法:如果来源 src 是另外一个 FileChannel并且已经到达文件尾,那么传输将提早终止;如果来源 src 是一个非阻塞 socket 通道,只有当前处于队列中的数据才会被传输(可能没有数据)。由于网络数据传输的非确定性,阻塞模式的socket 也可能会执行部分传输,这取决于操作系统。许多通道实现都是提供它们当前队列中已有的数据而不是等待您请求的全部数据都准备好

此外,请记住:如果传输过程中出现问题,这些方法也可能抛出 java.io.IOException异常。


Channel-to -channel传输是可以极其快速的,特别是在底层操作系统提供本地支持的时候。某些操作系统可以不必通过用户空间传递数据而进行直接的数据传输。对于大量的数据传输,这会是一个巨大的帮助。

看下例:使用通道传输进行文件连结

package com.ronsoft.books.nio.channels;import java.nio.channels.FileChannel;import java.nio.channels.WritableByteChannel;import java.nio.channels.Channels;import java.io.FileInputStream;/** * Test channel transfer. This is a very simplistic concatenation program. It * takes a list of file names as arguments, opens each in turn and transfers * (copies) their content to the given WritableByteChannel (in this case, * stdout). *  * Created April 2002 *  * @author Ron Hitchens (ron@ronsoft.com) */public class ChannelTransfer {public static void main(String[] argv) throws Exception {if (argv.length == 0) {System.err.println("Usage: filename ...");return;}catFiles(Channels.newChannel(System.out), argv);}// Concatenate the content of each of the named files to// the given channel. A very dumb version of 'cat'.private static void catFiles(WritableByteChannel target, String[] files)throws Exception {for (int i = 0; i < files.length; i++) {FileInputStream fis = new FileInputStream(files[i]);FileChannel channel = fis.getChannel();channel.transferTo(0, channel.size(), target);channel.close();fis.close();}}}













































































































































原创粉丝点击