通道(Channel)是java.nio的第二个主要创新。它们既不是一个扩展也不是一项增强,而是全新、极好的Java I/O示例,提供与I/O服务的直接连接。Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。
多数情况下,通道与操作系统的文件描述符(File Descriptor)和文件句柄(File Handle)有着一对一的关系。虽然通道比文件描述符更广义,但您将经常使用到的多数通道都是连接到开放的文件描述符的。Channel类提供维持平台独立性所需的抽象过程,不过仍然会模拟现代操作系统本身的I/O性能。
通道是一种途径,借助该途径,可以用最小的总开销来访问操作系统本身的I/O服务。缓冲区则是通道内部用来发送和接收数据的端点。
类继承图:
1、channel基础概念
下面是Channel接口的完整源码:
package java.nio.channels;
public interface Channel {
public boolean isOpen( );
public void close( ) throws IOException;
}
与缓冲区不同,通道API主要由接口指定。不同的操作系统上通道实现(Channel Implementation)会有根本性的差异,所以通道API仅仅描述了可以做什么。因此很自然地,通道实现经常使用操作系统的本地代码。通道接口允许您以一种受控且可移植的方式来访问底层的I/O服务。
您可以从顶层的Channel接口看到,对所有通道来说只有两种共同的操作:检查一个通道是否打开(IsOpen())和关闭一个打开的通道(close())。图3-2显示,所有有趣的东西都是那些实现Channel接口以及它的子接口的类。
InterruptibleChannel是一个标记接口,当被通道使用时可以标示该通道是可以中断的(Interruptible)。如果连接可中断通道的线程被中断,那么该通道会以特别的方式工作。大多数但非全部的通道都是可以中断的。
从Channel接口引申出的其他接口都是面向字节的子接口,包括Writable ByteChannel 和 ReadableByteChannel。这也正好支持了我们之前所学的:通道只能在字节缓冲区上操作。层次结构表明其他数据类型的通道也可以从Channel接口引申而来。这是一种很好的类设计,不过非字节实现是不可能的,因为操作系统都是以字节的形式实现底层I/O接口的。
java.nio.channels.spi。这两个类是AbstractInterruptibleChannel和AbstractSelectableChannel,它们分别为可中断的(interruptible)和可选择的(selectable)的通道实现提供所需的常用方法。尽管描述通道行为的接口都是在java.nio.channels包中定义的,不过具体的通道实现却都是从java.nio.channels.spi中的类引申来的。这使得他们可以访问受保护的方法,而这些方法普通的通道用户永远都不会调用。
作为通道的一个使用者,您可以放心地忽视SPI包中包含的中间类。这种有点费解的继承层次只会让那些使用新通道的用户感兴趣。SPI包允许新通道实现以一种受控且模块化的方式被植入到Java虚拟机上。这意味着可以使用专为某种操作系统、文件系统或应用程序而优化的通道来使性能最大化。
1.1、打开通道
通道是访问I/O服务的导管。正如我们在第一章中所讨论的,I/O可以分为广义的两大类别:File I/O和Stream I/O。那么相应地有两种类型的通道也就不足为怪了,它们是文件(file)通道和套接字(socket)通道。您就会发现有一个FileChannel类和三个socket通道类:SocketChannel、ServerSocketChannel和DatagramChannel。
通道可以以多种方式创建。Socket通道有可以直接创建新socket通道的工厂方法。但是一个FileChannel对象却只能通过在一个打开的RandomAccessFile、FileInputStream或FileOutputStream对象上调用getChannel( )方法来获取。您不能直接创建一个FileChannel对象。
代码示例:
SocketChannel sc = SocketChannel.open( );
sc.connect (new InetSocketAddress ("somehost", someport));
ServerSocketChannel ssc = ServerSocketChannel.open( );
ssc.socket( ).bind (new InetSocketAddress (somelocalport));
DatagramChannel dc = DatagramChannel.open( );
RandomAccessFile raf = new RandomAccessFile ("somefile", "r");
FileChannel fc = raf.getChannel( );
java.net的socket类也有新的getChannel( )方法。这些方法虽然能返回一个相应的socket通道对象,但它们却并非新通道的来源,RandomAccessFile.getChannel( )方法才是。只有在已经有通道存在的时候,它们才返回与一个socket关联的通道;它们永远不会创建新通道。
1.2、使用通道
通道将数据传输给ByteBuffer对象或者从ByteBuffer对象获取数据进行传输。
public interface ReadableByteChannel extends Channel {
public int read (ByteBuffer dst) throws IOException;
}
public interface WritableByteChannel extends Channel {
public int write (ByteBuffer src) throws IOException;
}
public interface ByteChannel extends ReadableByteChannel, WritableByteChannel {
}
继承UML图:
通道可以是单向(unidirectional)或者双向的(bidirectional)。一个channel类可能实现定义read( )方法的ReadableByteChannel接口,而另一个channel类也许实现WritableByteChannel接口以提供write( )方法。实现这两种接口其中之一的类都是单向的,只能在一个方向上传输数据。如果一个类同时实现这两个接口,那么它是双向的,可以双向传输数据。
ByteChannel接口,该接口引申出了ReadableByteChannel 和WritableByteChannel两个接口。ByteChannel接口本身并不定义新的API方法,它是一种用来聚集它自己以一个新名称继承的多个接口的便捷接口。根据定义,实现ByteChannel接口的通道会同时实现ReadableByteChannel 和WritableByteChannel两个接口,所以此类通道是双向的。这是简化类定义的语法糖(syntactic sugar),它使得用操作器(operator)实例来测试通道对象变得更加简单。
通道会连接一个特定I/O服务且通道实例(channel instance)的性能受它所连接的I/O服务的特征限制,记住这很重要。一个连接到只读文件的Channel实例不能进行写操作,即使该实例所属的类可能有write( )方法。基于此,程序员需要知道通道是如何打开的,避免试图尝试一个底层I/O服务不允许的操作。
// A ByteBuffer named buffer contains data to be written
FileInputStream input = new FileInputStream (fileName);
FileChannel channel = input.getChannel( );
// This will compile but will throw an IOException
// because the underlying file is read-only
channel.write (buffer);
ByteChannel的read( ) 和write( )方法使用ByteBuffer对象作为参数。两种方法均返回已传输的字节数,可能比缓冲区的字节数少甚至可能为零。缓冲区的位置也会发生与已传输字节相同数量的前移。如果只进行了部分传输,缓冲区可以被重新提交给通道并从上次中断的地方继续传输。该过程重复进行直到缓冲区的hasRemaining( )方法返回false值。
通道可以以阻塞(blocking)或非阻塞(nonblocking)模式运行。非阻塞模式的通道永远不会让调用的线程休眠。请求的操作要么立即完成,要么返回一个结果表明未进行任何操作。只有面向流的(stream-oriented)的通道,如sockets和pipes才能使用非阻塞模式。
socket通道类从SelectableChannel引申而来。从SelectableChannel引申而来的类可以和支持有条件的选择(readiness selectio)的选择器(Selectors)一起使用。将非阻塞I/O和选择器组合起来可以使您的程序利用多路复用I/O(multiplexed I/O)。
1.3、 关闭通道
与缓冲区不同,通道不能被重复使用。一个打开的通道即代表与一个特定I/O服务的特定连接并封装该连接的状态。当通道关闭时,那个连接会丢失,然后通道将不再连接任何东西。
调用通道的close( )方法时,可能会导致在通道关闭底层I/O服务的过程中线程暂时阻塞7,哪怕该通道处于非阻塞模式。通道关闭时的阻塞行为(如果有的话)是高度取决于操作系统或者文件系统的。在一个通道上多次调用close( )方法是没有坏处的,但是如果第一个线程在close( )方法中阻塞,那么在它完成关闭通道之前,任何其他调用close( )方法都会阻塞。后续在该已关闭的通道上调用close( )不会产生任何操作,只会立即返回。
可以通过isOpen( )方法来测试通道的开放状态。如果返回true值,那么该通道可以使用。如果返回false值,那么该通道已关闭,不能再被使用。尝试进行任何需要通道处于开放状态作为前提的操作,如读、写等都会导致ClosedChannelException异常。
通道引入了一些与关闭和中断有关的新行为。如果一个通道实现InterruptibleChannel接口(参见图3-2),它的行为以下述语义为准:如果一个线程在一个通道上被阻塞并且同时被中断(由调用该被阻塞线程的interrupt( )方法的另一个线程中断),那么该通道将被关闭,该被阻塞线程也会产生一个ClosedByInterruptException异常。
此外,假如一个线程的interrupt status被设置并且该线程试图访问一个通道,那么这个通道将立即被关闭,同时将抛出相同的ClosedByInterruptException异常。线程的interrupt status在线程的interrupt( )方法被调用时会被设置。我们可以使用isInterrupted( )来测试某个线程当前的interrupt status。当前线程的interrupt status可以通过调用静态的Thread.interrupted( )方法清除。
“在全部平台上提供确定的通道行为”这一需求导致了“当I/O操作被中断时总是关闭通道”这一设计选择。这个选择被认为是可接受的,因为大部分时候一个线程被中断就是希望以此来关闭通道。java.nio包中强制使用此行为来避免因操作系统独特性而导致的困境,因为该困境对I/O区域而言是极其危险的。这也是为增强健壮性(robustness)而采用的一种经典的权衡。
可中断的通道也是可以异步关闭的。实现InterruptibleChannel接口的通道可以在任何时候被关闭,即使有另一个被阻塞的线程在等待该通道上的一个I/O操作完成。当一个通道被关闭时,休眠在该通道上的所有线程都将被唤醒并接收到一个AsynchronousCloseException异常。接着通道就被关闭并将不再可用。
不实现InterruptibleChannel接口的通道一般都是不进行底层本地代码实现的有特殊用途的通道。这些也许是永远不会阻塞的特殊用途通道,如旧系统数据流的封装包或不能实现可中断语义的writer类等。
2、Scatter/Gather
通道提供了一种被称为Scatter/Gather的重要新功能(有时也被称为矢量I/O)。Scatter/Gather是一个简单却强大的概念,它是指在多个缓冲区上实现一个简单的I/O操作。对于一个write操作而言,数据是从几个缓冲区按顺序抽取(称为gather)并沿着通道发送的。缓冲区本身并不需要具备这种gather的能力(通常它们也没有此能力)。该gather过程的效果就好比全部缓冲区的内容被连结起来,并在发送数据前存放到一个大的缓冲区中。对于read操作而言,从通道读取的数据会按顺序被散布(称为scatter)到多个缓冲区,将每个缓冲区填满直至通道中的数据或者缓冲区的最大空间被消耗完。
大多数现代操作系统都支持本地矢量I/O(native vectored I/O)。当您在一个通道上请求一个Scatter/Gather操作时,该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区。这是一个很大的进步,因为减少或避免了缓冲区拷贝和系统调用。Scatter/Gather应该使用直接的ByteBuffers以从本地I/O获取最大性能优势。
public interface ScatteringByteChannel extends ReadableByteChannel {
public long read (ByteBuffer [] dsts) throws IOException;
public long read (ByteBuffer [] dsts, int offset, int length) throws IOException;
}
public interface GatheringByteChannel extends WritableByteChannel {
public long write(ByteBuffer[] srcs) throws IOException;
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException;
}
从上图您可以看到,这两个接口都添加了两种以缓冲区阵列作为参数的新方法。另外,每种方法都提供了一种带offset和length参数的形式。
ByteBuffer header = ByteBuffer.allocateDirect (10);
ByteBuffer body = ByteBuffer.allocateDirect (80);
ByteBuffer [] buffers = { header, body };
int bytesRead = channel.read (buffers);
使用得当的话,Scatter/Gather会是一个极其强大的工具。它允许您委托操作系统来完成辛苦活:将读取到的数据分开存放到多个存储桶(bucket)或者将不同的数据区块合并成一个整体。这是一个巨大的成就,因为操作系统已经被高度优化来完成此类工作了。它节省了您来回移动数据的工作,也就避免了缓冲区拷贝和减少了您需要编写、调试的代码数量。既然您基本上通过提供数据容器引用来组合数据,那么按照不同的组合构建多个缓冲区阵列引用,各种数据区块就可以以不同的方式来组合了。
3、文件通道
FileChannel类可以实现常用的read,write以及scatter/gather操作,同时它也提供了很多专用于文件的新方法。这些方法中的许多都是我们所熟悉的文件操作,不过其他的您可能之前并未接触过。现在我们将在此对它们全部予以讨论。
文件通道总是阻塞式的,因此不能被置于非阻塞模式。现代操作系统都有复杂的缓存和预取机制,使得本地磁盘I/O操作延迟很少。网络文件系统一般而言延迟会多些,不过却也因该优化而受益。面向流的I/O的非阻塞范例对于面向文件的操作并无多大意义,这是由文件I/O本质上的不同性质造成的。对于文件I/O,最强大之处在于异步I/O(asynchronous I/O),它允许一个进程可以从操作系统请求一个或多个I/O操作而不必等待这些操作的完成。发起请求的进程之后会收到它请求的I/O操作已完成的通知。
FileChannel对象不能直接创建。一个FileChannel实例只能通过在一个打开的file对象(RandomAccessFile、FileInputStream或FileOutputStream)上调用getChannel( )方法获取8。调用getChannel( )方法会返回一个连接到相同文件的FileChannel对象且该FileChannel对象具有与file对象相同的访问权限,然后您就可以使用该通道对象来利用强大的FileChannel API了:
package java.nio.channels;
public abstract class FileChannel
extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {
// This is a partial API listing
// All methods listed here can throw java.io.IOException
public abstract int read (ByteBuffer dst, long position)
public abstract int write (ByteBuffer src, long position)
public abstract long size( ) public abstract long position( )
public abstract void position (long newPosition)
public abstract void truncate (long size)
public abstract void force (boolean metaData)
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)
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
}
public abstract long transferTo (long position, long count, WritableByteChannel target)
public abstract long transferFrom (ReadableByteChannel src, long position, long count)
}
同大多数通道一样,只要有可能,FileChannel都会尝试使用本地I/O服务。FileChannel类本身是抽象的,您从getChannel( )方法获取的实际对象是一个具体子类(subclass)的一个实例(instance),该子类可能使用本地代码来实现以上API方法中的一些或全部。
FileChannel对象是线程安全(thread-safe)的。多个进程可以在同一个实例上并发调用方法而不会引起任何问题,不过并非所有的操作都是多线程的(multithreaded)。影响通道位置或者影响文件大小的操作都是单线程的(single-threaded)。如果有一个线程已经在执行会影响通道位置或文件大小的操作,那么其他尝试进行此类操作之一的线程必须等待。并发行为也会受到底层的操作系统或文件系统影响。
同大多数I/O相关的类一样,FileChannel是一个反映Java虚拟机外部一个具体对象的抽象。FileChannel类保证同一个Java虚拟机上的所有实例看到的某个文件的视图均是一致的,但是Java虚拟机却不能对超出它控制范围的因素提供担保。通过一个FileChannel实例看到的某个文件的视图同通过一个外部的非Java进程看到的该文件的视图可能一致,也可能不一致。多个进程发起的并发文件访问的语义高度取决于底层的操作系统和(或)文件系统。一般而言,由运行在不同Java虚拟机上的FileChannel对象发起的对某个文件的并发访问和由非Java进程发起的对该文件的并发访问是一致的。
3.1、访问文件
每个FileChannel对象都同一个文件描述符(file descriptor)有一对一的关系,所以上面列出的API方法与在您最喜欢的POSIX(可移植操作系统接口)兼容的操作系统上的常用文件I/O系统调用紧密对应也就不足为怪了。名称也许不尽相同,不过常见的suspect(“可疑分子”)都被集中起来了。您可能也注意到了上面列出的API方法同java.io包中RandomAccessFile类的方法的相似之处了。本质上讲,RandomAccessFile类提供的是同样的抽象内容。在通道出现之前,底层的文件操作都是通过RandomAccessFile类的方法来实现的。FileChannel模拟同样的I/O服务,因此它的API自然也是很相似的。
让我们来进一步看下基本的文件访问方法:
public abstract class FileChannel
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来访问文件数据。
有两种形式的position( )方法:
第一种,不带参数的,返回当前文件的position值。返回值是一个长整型(long),表示文件中的当前字节位置;
第二种形式的position( )方法带一个long(长整型)参数并将通道的position设置为指定值。如果尝试将通道position设置为一个负值会导致java.lang.IllegalArgumentException异常,不过可以把position设置到超出文件尾,这样做会把position设置为指定值而不改变文件大小。假如在将position设置为超出当前文件大小时实现了一个read( )方法,那么会返回一个文件尾(end-of-file)条件;倘若此时实现的是一个write( )方法则会引起文件增长以容纳写入的字节,具体行为类似于实现一个绝对write( )并可能导致出现一个文件空洞;
FileChannel位置(position)是从底层的文件描述符获得的,该position同时被作为通道引用获取来源的文件对象共享。这也就意味着一个对象对该position的更新可以被另一个对象看到。
类似于缓冲区的get( ) 和put( )方法,当字节被read( )或write( )方法传输时,文件position会自动更新。如果position值达到了文件大小的值(文件大小的值可以通过size( )方法返回),read( )方法会返回一个文件尾条件值(-1)。可是,不同于缓冲区的是,如果实现write( )方法时position前进到超过文件大小的值,该文件会扩展以容纳新写入的字节。
同样类似于缓冲区,也有带position参数的绝对形式的read( )和write( )方法。这种绝对形式的方法在返回值时不会改变当前的文件position。由于通道的状态无需更新,因此绝对的读和写可能会更加有效率,操作请求可以直接传到本地代码。更妙的是,多个线程可以并发访问同一个文件而不会相互产生干扰。这是因为每次调用都是原子性的(atomic),并不依靠调用之间系统所记住的状态。
尝试在文件末尾之外的position进行一个绝对读操作,size( )方法会返回一个end-of-file。在超出文件大小的position上做一个绝对write( )会导致文件增加以容纳正在被写入的新字节。文件中位于之前end-of-file位置和新添加的字节起始位置之间区域的字节的值不是由FileChannel类指定,而是在大多数情况下反映底层文件系统的语义。取决于操作系统和(或)文件系统类型,这可能会导致在文件中出现一个空洞。
当需要减少一个文件的size时,truncate( )方法会砍掉您所指定的新size值之外的所有数据。如果当前size大于新size,超出新size的所有字节都会被悄悄地丢弃。如果提供的新size值大于或等于当前的文件size值,该文件不会被修改。这两种情况下,truncate( )都会产生副作用:文件的position会被设置为所提供的新size值。
force()如果文件位于一个本地文件系统,那么一旦force( )方法返回,即可保证从通道被创建(或上次调用force( ))时起的对文件所做的全部修改已经被写入到磁盘。对于关键操作如事务(transaction)处理来说,这一点是非常重要的,可以保证数据完整性和可靠的恢复。然而,如果文件位于一个远程的文件系统,如NFS上,那么不能保证待定修改一定能同步到永久存储器(permanent storage)上,因Java虚拟机不能做操作系统或文件系统不能实现的承诺。如果您的程序在面临系统崩溃时必须维持数据完整性,先去验证一下您在使用的操作系统和(或)文件系统在同步修改方面是可以依赖的。该方法告诉通道强制将全部待定的修改都应用到磁盘的文件上。所有的现代文件系统都会缓存数据和延迟磁盘文件更新以提高性能。调用force( )方法要求文件的所有待定修改立即同步到磁盘。
3.2、文件锁定
绝大多数现代操作系统早就有了文件锁定功能,而直到JDK 1.4版本发布时Java编程人员才可以使用文件锁(file lock)。在集成许多其他非Java程序时,文件锁定显得尤其重要。此外,它在判优(判断多个访问请求的优先级别)一个大系统的多个Java组件发起的访问时也很有价值。
锁(lock)可以是共享的(shared)或独占的(exclusive)。本节中描述的文件锁定特性在很大程度上依赖本地的操作系统实现。并非所有的操作系统和文件系统都支持共享文件锁。对于那些不支持的,对一个共享锁的请求会被自动提升为对独占锁的请求。这可以保证准确性却可能严重影响性能。
有关FileChannel实现的文件锁定模型的一个重要注意项是:锁的对象是文件而不是通道或线程,这意味着文件锁不适用于判优同一台Java虚拟机上的多个线程发起的访问。
文件锁旨在在进程级别上判优文件访问,比如在主要的程序组件之间或者在集成其他供应商的组件时。如果您需要控制多个Java线程的并发访问,您可能需要实施您自己的、轻量级的锁定方案。那种情形下,内存映射文件(本章后面会进行详述)可能是一个合适的选择。
FileLock类封装一个锁定的文件区域。FileLock对象由FileChannel创建并且总是关联到那个特定的通道实例。您可以通过调用channel( )方法来查询一个lock对象以判断它是由哪个通道创建的。
一个FileLock对象创建之后即有效,直到它的release( )方法被调用或它所关联的通道被关闭或Java虚拟机关闭时才会失效。我们可以通过调用isValid( )布尔方法来测试一个锁的有效性。一个锁的有效性可能会随着时间而改变,不过它的其他属性——位置(position)、范围大小(size)和独占性(exclusivity)——在创建时即被确定,不会随着时间而改变。
尽管一个FileLock对象是与某个特定的FileChannel实例关联的,它所代表的锁却是与一个底层文件关联的,而不是与通道关联。因此,如果您在使用完一个锁后而不释放它的话,可能会导致冲突或者死锁。请小心管理文件锁以避免出现此问题。一旦您成功地获取了一个文件锁,如果随后在通道上出现错误的话,请务必释放这个锁。
4、内存映射
新的FileChannel类提供了一个名为map( )的方法,该方法可以在一个打开的文件和一个特殊类型的ByteBuffer之间建立一个虚拟内存映射。在FileChannel上调用map( )方法会创建一个由磁盘文件支持的虚拟内存映射(virtual memory mapping)并在那块虚拟内存空间外部封装一个MappedByteBuffer对象。
由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
}
}
与文件锁的范围机制不一样,映射文件的范围不应超过文件的实际大小。如果您请求一个超出文件大小的映射,文件会被增大以匹配映射的大小。假如您给size参数传递的值是Integer.MAX_VALUE,文件大小的值会膨胀到超过2.1GB。即使您请求的是一个只读映射,map( )方法也会尝试这样做并且大多数情况下都会抛出一个IOException异常,因为底层的文件不能被修改。
FileChannel类定义了代表映射模式的常量,且是使用一个类型安全的枚举而非数字值来定义这些常量。这些常量是FileChannel内部定义的一个内部类(inner class)的静态字段,它们可以在编译时被检查类型,不过您可以像使用一个数值型常量那样使用它们。
您应该注意到了没有unmap( )方法。也就是说,一个映射一旦建立之后将保持有效,直到MappedByteBuffer对象被施以垃圾收集动作为止。同锁不一样的是,映射缓冲区没有绑定到创建它们的通道上。关闭相关联的FileChannel不会破坏映射,只有丢弃缓冲区对象本身才会破坏该映射。NIO设计师们之所以做这样的决定是因为当关闭通道时破坏映射会引起安全问题,而解决该安全问题又会导致性能问题。如果您确实需要知道一个映射是什么时候被破坏的,他们建议使用虚引用(phantom references,参见java.lang.ref.PhantomReference)和一个cleanup线程。不过有此需要的概率是微乎其微的。
MemoryMappedBuffer直接反映它所关联的磁盘文件。如果映射有效时文件被在结构上修改,就会产生奇怪的行为(当然具体的行为是取决于操作系统和文件系统的)。MemoryMappedBuffer有固定的大小,不过它所映射的文件却是弹性的。具体来说,如果映射有效时文件大小变化了,那么缓冲区的部分或全部内容都可能无法访问,并将返回未定义的数据或者抛出未检查的异常。
所有的MappedByteBuffer对象都是直接的,这意味着它们占用的内存空间位于Java虚拟机内存堆之外。
因为MappedByteBuffers也是ByteBuffers,所以能够被传递SocketChannel之类通道的read( )或write( )以有效传输数据给被映射的文件或从被映射的文件读取数据。如能再结合scatter/gather,那么从内存缓冲区和被映射文件内容中组织数据就变得很容易了。
load( )方法会加载整个文件以使它常驻内存。正如我们在第一章所讨论的,一个内存映射缓冲区会建立与某个文件的虚拟内存映射。此映射使得操作系统的底层虚拟内存子系统可以根据需要将文件中相应区块的数据读进内存。已经在内存中或通过验证的页会占用实际内存空间,并且在它们被读进RAM时会挤出最近较少使用的其他内存页。
对于大多数程序,特别是交互性的或其他事件驱动(event-driven)的程序而言,为提前加载文件消耗资源是不划算的。在实际访问时分摊页调入开销才是更好的选择。让操作系统根据需要来调入页意味着不访问的页永远不需要被加载。同预加载整个被映射的文件相比,这很容易减少I/O活动总次数。
5、Socket通道
新的socket通道类可以运行非阻塞模式并且是可选择的。这两个性能可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。借助新的NIO类,一个或几个线程就可以管理成百上千的活动socket连接了并且只有很少甚至可能没有性能损失。
全部socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)都是由位于java.nio.channels.spi包中的AbstractSelectableChannel引申而来。这意味着我们可以用一个Selector对象来执行socket通道的有条件的选择(readiness selection)。
DatagramChannel和SocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,它本身从不传输数据。
全部socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)在被实例化时都会创建一个对等socket对象。这些是我们所熟悉的来自java.net的类(Socket、ServerSocket和DatagramSocket),它们已经被更新以识别通道。对等socket可以通过调用socket( )方法从一个通道上获取。此外,这三个java.net类现在都有getChannel( )方法。
5.1、非阻塞模式
非阻塞I/O是许多复杂的、高性能的程序构建的基础。
要把一个socket通道置于非阻塞模式,我们要依靠所有socket通道类的公有超级类:SelectableChannel。下面的方法就是关于通道的阻塞模式的:
public abstract class SelectableChannel
extends AbstractChannel implements Channel {
// This is a partial API listing
public abstract void configureBlocking (boolean block) throws IOException;
public abstract boolean isBlocking( );
public abstract Object blockingLock( );
}
有条件的选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻塞I/O和可选择性是紧密相连的,那也正是管理阻塞模式的API代码要在SelectableChannel超级类中定义的原因。
设置或重新设置一个通道的阻塞模式是很简单的,只要调用configureBlocking( )方法即可,传递参数值为true则设为阻塞模式,参数值为false值设为非阻塞模式。
5.2、ServerSocketChannel
以下是ServerSocketChannel的完整API:
public abstract class ServerSocketChannel
extends AbstractSelectableChannel {
public static ServerSocketChannel open( ) throws IOException
public abstract ServerSocket socket( );
public abstract ServerSocket accept( ) throws IOException;
public final int validOps( )
}
ServerSocketChannel是一个基于通道的socket监听器。它同我们所熟悉的java.net.ServerSocket执行相同的基本任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。
用静态的open( )工厂方法创建一个新的ServerSocketChannel对象,将会返回同一个未绑定的java.net.ServerSocket关联的通道。该对等ServerSocket可以通过在返回的ServerSocketChannel上调用socket( )方法来获取。作为ServerSocketChannel的对等体被创建的ServerSocket对象依赖通道实现。这些socket关联的SocketImpl能识别通道。通道不能被封装在随意的socket对象外面。
由于ServerSocketChannel没有bind( )方法,因此有必要取出对等的socket并使用它来绑定到一个端口以开始监听连接。我们也是使用对等ServerSocket的API来根据需要设置其他的socket选项。
同它的对等体java.net.ServerSocket一样,ServerSocketChannel也有accept( )方法。一旦您创建了一个ServerSocketChannel并用对等socket绑定了它,然后您就可以在其中一个上调用accept( )。如果您选择在ServerSocket上调用accept( )方法,那么它会同任何其他的ServerSocket表现一样的行为:总是阻塞并返回一个java.net.Socket对象。如果您选择在ServerSocketChannel上调用accept( )方法则会返回SocketChannel类型的对象,返回的对象能够在非阻塞模式下运行。假设系统已经有一个安全管理器(security manager),两种形式的方法调用都执行相同的安全检查。
5.3、SocketChannel
public abstract class SocketChannel
extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel {
// This is a partial API listing
public static SocketChannel open( ) throws IOException
public static SocketChannel open (InetSocketAddress remote) throws IOException
public abstract Socket socket( );
public abstract boolean connect (SocketAddress remote) throws IOException;
public abstract boolean isConnectionPending( );
public abstract boolean finishConnect( ) throws IOException;
public abstract boolean isConnected( ); public final int validOps( )
}
Socket和SocketChannel类封装点对点、有序的网络连接,类似于我们所熟知并喜爱的TCP/IP网络连接。SocketChannel扮演客户端发起同一个监听服务器的连接。直到连接成功,它才能收到数据并且只会从连接到的地址接收。
每个SocketChannel对象创建时都是同一个对等的java.net.Socket对象串联的。静态的open( )方法可以创建一个新的SocketChannel对象,而在新创建的SocketChannel上调用socket( )方法能返回它对等的Socket对象;在该Socket上调用getChannel( )方法则能返回最初的那个SocketChannel。
我们可以通过在通道上直接调用connect( )方法或在通道关联的Socket对象上调用connect( )来将该socket通道连接。一旦一个socket通道被连接,它将保持连接状态直到被关闭。您可以通过调用布尔型的isConnected( )方法来测试某个SocketChannel当前是否已连接。
在SocketChannel上并没有一种connect( )方法可以让您指定超时(timeout)值,当connect( )方法在非阻塞模式下被调用时SocketChannel提供并发连接:它发起对请求地址的连接并且立即返回值。如果返回值是true,说明连接立即建立了(这可能是本地环回连接);如果连接不能立即建立,connect( )方法会返回false且并发地继续连接建立过程。
面向流的的socket建立连接状态需要一定的时间,因为两个待连接系统之间必须进行包对话以建立维护流socket所需的状态信息。跨越开放互联网连接到远程系统会特别耗时。假如某个SocketChannel上当前正由一个并发连接,isConnectPending( )方法就会返回true值。
Socket通道是线程安全的。并发访问时无需特别措施来保护发起访问的多个线程,不过任何时候都只有一个读操作和一个写操作在进行中。请记住,sockets是面向流的而非包导向的。它们可以保证发送的字节会按照顺序到达但无法承诺维持字节分组。某个发送器可能给一个socket写入了20个字节而接收器调用read( )方法时却只收到了其中的3个字节。剩下的17个字节还是传输中。由于这个原因,让多个不配合的线程共享某个流socket的同一侧绝非一个好的设计选择。
6、管道
java.nio.channels包中含有一个名为Pipe(管道)的类。广义上讲,管道就是一个用来在两个实体之间单向传输数据的导管。Pipe类实现一个管道范例,不过它所创建的管道是进程内(在Java虚拟机进程内部)而非进程间使用的。
Pipe类创建一对提供环回机制的Channel对象。这两个通道的远端是连接起来的,以便任何写在SinkChannel对象上的数据都能出现在SourceChannel对象上。
package java.nio.channels;
public abstract class Pipe {
public static Pipe open( ) throws IOException
public abstract SourceChannel source( );
public abstract SinkChannel sink( );
public static abstract class SourceChannel
extends AbstractSelectableChannel implements ReadableByteChannel, ScatteringByteChannel
public static abstract class SinkChannel
extends AbstractSelectableChannel implements WritableByteChannel, GatheringByteChannel
}
Pipe实例是通过调用不带参数的Pipe.open( )工厂方法来创建的。Pipe类定义了两个嵌套的通道类来实现管路。这两个类是Pipe.SourceChannel(管道负责读的一端)和Pipe.SinkChannel(管道负责写的一端)。这两个通道实例是在Pipe对象创建的同时被创建的,可以通过在Pipe对象上分别调用source( )和sink( )方法来取回。
您不能使用Pipe在操作系统级的进程间建立一个类Unix管道(您可以使用SocketChannel来建立)。Pipe的source通道和sink通道提供类似java.io.PipedInputStream和java.io.PipedOutputStream所提供的功能,不过它们可以执行全部的通道语义。请注意,SinkChannel和SourceChannel都由AbstractSelectableChannel引申而来(所以也是从SelectableChannel引申而来),这意味着pipe通道可以同选择器一起使用。
Pipes的另一个有用之处是可以用来辅助测试。一个单元测试框架可以将某个待测试的类连接到管道的“写”端并检查管道的“读”端出来的数据。它也可以将被测试的类置于通道的“读”端并将受控的测试数据写进其中。
管路所能承载的数据量是依赖实现的(implementation-dependent)。唯一可保证的是写到SinkChannel中的字节都能按照同样的顺序在SourceChannel上重现。
7、通道工具类
一个工具类(java.nio.channels.Channels的一个稍微重复的名称)定义了几种静态的工厂方法以使通道可以更加容易地同流和读写器互联。
相关阅读:
Reactor和Preactor模型 【//www.greatytc.com/p/b4de9b85c79d】
Selector【//www.greatytc.com/p/65157b97cc6e】
Buffer【//www.greatytc.com/p/49d20a7547f6】
IO相关基本概念【//www.greatytc.com/p/177021e33428】