`
laughingchs
  • 浏览: 67926 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

nio学习杂记

    博客分类:
  • nio
 
阅读更多

缓冲区(Buffers)

新的 Buffer 类是常规 Java 类和通道之间的纽带。缓冲区提供了一个会合点:通道既可提取放在缓冲区中的数据(写),也可向缓冲区存入数据供读取(读)。


通道(Channels)
Channel 对象模拟了通信连接,管道既可以是单向的(进或出),也可以是双向的(进和出)。可以把通道想象成连接缓冲区和 I/O 服务的捷径。
多数通道可工作在非块模式(即非阻塞模式)下,这意味着更好的可伸缩性,尤其是与选择器一同使用的时候。

内存映射文件
将文件映射到内存,这样在您看来,磁盘上的文件数据就像是在内存中一样。这利用了操作系统的虚拟内存功能,无需在内存中实际保留一份文件的拷贝,就可实现文件内容的动态高速缓存。

套接字(Sockets)
套接字通道可工作于非块模式,并可与选择器一同使用。因此,多个套接字可实现多路传输,管理效率也比 java.net 提供的传统套接字更高。

选择器(Selectors)
选择器可实现就绪性选择。Selector 类提供了确定一或多个通道当前状态的机制。使用选择器,借助单一线程,就可对数量庞大的活动 I/O 通道实施监控和维护。

操作系统与 Java 基于流的 I/O模型有些不匹配。操作系统要移动的是大块数据(缓冲区),这往往是在硬件直接存储器存取
(DMA)的协助下完成的。而 JVM 的 I/O 类喜欢操作小块数据——单个字节、几行文本。结果,操作系统送来整缓冲区的数据,java.io 的流数据类再花大量时间把它们拆成小块,往往拷贝一个小块就要往返于几层对象。操作系统喜欢整卡车地运来数据,java.io 类则喜欢一铲子一铲子地加工数据。有了 NIO,就可以轻松地把一卡车数据备份到您能直接使用的地方(ByteBuffer 对象)。


图 1-1. I/O 缓冲区操作简图
图 1-1 简单描述了数据从外部磁盘向运行中的进程的内存区域移动的过程。进程使用 read( )系统调用,要求其缓冲区被填满。内核随即向磁盘控制硬件发出命令,要求其从磁盘读取数据。磁盘控制器把数据直接写入内核内存缓冲区,这一步通过 DMA 完成,无需主 CPU 协助。一旦磁盘控制器把缓冲区装满,内核即把数据从内核空间的临时缓冲区拷贝到进程执行 read( )调用时指定的缓冲区。

注意图中用户空间和内核空间的概念。用户空间是常规进程所在区域。JVM 就是常规进程,驻守于用户空间。用户空间是非特权区域:比如,在该区域执行的代码就不能直接访问硬件设备。内核空间是操作系统所在区域。内核代码有特别的权力:它能与设备控制器通讯,控制着用户区域进程的运行状态,等等。最重要的是,所有 I/O 都直接(如这里所述)或间接(见 1.4.2 小节)通
过内核空间。

当进程请求 I/O 操作的时候,它执行一个系统调用(有时称为陷阱)将控制权移交给内核。C/C++程序员所熟知的底层函数 open( )、read( )、write( )和 close( )要做的无非就是建立和执行适当的系统调用。当内核以这种方式被调用,它随即采取任何必要步骤,找到进程所需数据,并把数据传送到用户空间内的指定缓冲区。内核试图对数据进行高速缓存或预读取,因此进程所需数据可能已经在内核空间里了。如果是这样,该数据只需简单地拷贝出来即可。如果数据不在内核空间,则进程被挂起,内核着手把数据读进内存。

看了图 1-1,您可能会觉得,把数据从内核空间拷贝到用户空间似乎有些多余。为什么不直接让磁盘控制器把数据送到用户空间的缓冲区呢?这样做有几个问题。首先,硬件通常不能直接访问用户空间 1。其次,像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责数据的分解、再组合工作,因此充当着中间人的角色。


1.4.1.1 发散/汇聚
许多操作系统能把组装/分解过程进行得更加高效。根据发散/汇聚的概念,进程只需一个系
统调用,就能把一连串缓冲区地址传递给操作系统。然后,内核就可以顺序填充或排干多个缓冲
区,读的时候就把数据发散到多个用户空间缓冲区,写的时候再从多个缓冲区把数据汇聚起来(图
1-2)。
图 1-2. 三个缓冲区的发散读操作
这样用户进程就不必多次执行系统调用(那样做可能代价不菲),内核也可以优化数据的处理过程,因为它已掌握待传输数据的全部信息。如果系统配有多个 CPU,甚至可以同时填充或排干多个缓冲区。


1.4.2 虚拟内存
所有现代操作系统都使用虚拟内存。虚拟内存意为使用虚假(或虚拟)地址取代物理(硬件RAM)内存地址。这样做好处颇多,总结起来可分为两大类:
1. 一个以上的虚拟地址可指向同一个物理内存地址。
2. 虚拟内存空间可大于实际可用的硬件内存。
前一节提到,设备控制器不能通过 DMA 直接存储到用户空间,但通过利用上面提到的第一项,则可以达到相同效果。把内核空间地址与用户空间的虚拟地址映射到同一个物理地址,这样,DMA 硬件(只能访问物理内存地址)就可以填充对内核与用户空间进程同时可见的缓冲区(见图1-3)。
图 1-3. 内存空间多重映射
这样真是太好了,省去了内核与用户空间的往来拷贝,但前提条件是,内核与用户缓冲区必须使用相同的页对齐,缓冲区的大小还必须是磁盘控制器块大小(通常为 512 字节磁盘扇区)的倍数。操作系统把内存地址空间划分为页,即固定大小的字节组。内存页的大小总是磁盘块大小的倍数,通常为 2 次幂(这样可简化寻址操作)。典型的内存页为 1,024、2,048 和 4,096 字节。虚拟和物理内存页的大小总是相同的。图 1-4 显示了来自多个虚拟地址的虚拟内存页是如何映射到物理内存的。
图 1-4. 内存页
1.4.3 内存页面调度
为了支持虚拟内存的第二个特性(寻址空间大于物理内存),就必须进行虚拟内存分页(经常称为交换,虽然真正的交换是在进程层面完成,而非页层面)。依照该方案,虚拟内存空间的页面能够继续存在于外部磁盘存储,这样就为物理内存中的其他虚拟页面腾出了空间。从本质上说,物理内存充当了分页区的高速缓存;而所谓分页区,即从物理内存置换出来,转而存储于磁盘上的内
存页面。
图 1-5 显示了分属于四个进程的虚拟页面,其中每个进程都有属于自己的虚拟内存空间。进程A 有五个页面,其中两个装入内存,其余存储于磁盘。
图 1-5. 用于分页区高速缓存的物理内存
把内存页大小设定为磁盘块大小的倍数,这样内核就可直接向磁盘控制硬件发布命令,把内存页写入磁盘,在需要时再重新装入。结果是,所有磁盘 I/O 都在页层面完成。对于采用分页技术的现代操作系统而言,这也是数据在磁盘与物理内存之间往来的唯一方式。

现代 CPU 包含一个称为内存管理单元(MMU)的子系统,逻辑上位于 CPU 与物理内存之间。该设备包含虚拟地址向物理内存地址转换时所需映射信息。当 CPU 引用某内存地址时,MMU负责确定该地址所在页(往往通过对地址值进行移位或屏蔽位操作实现),并将虚拟页号转换为物理页号(这一步由硬件完成,速度极快)。如果当前不存在与该虚拟页形成有效映射的物理内存
页,MMU 会向 CPU 提交一个页错误。

页错误随即产生一个陷阱(类似于系统调用),把控制权移交给内核,附带导致错误的虚拟地址信息,然后内核采取步骤验证页的有效性。内核会安排页面调入操作,把缺失的页内容读回物理内存。这往往导致别的页被移出物理内存,好给新来的页让地方.在这种情况下,如果待移出的页已经被碰过了(自创建或上次页面调入以来,内容已发生改变),还必须首先执行页面调出,把页
内容拷贝到磁盘上的分页区。
如果所要求的地址不是有效的虚拟内存地址(不属于正在执行的进程的任何一个内存段),则该页不能通过验证,段错误随即产生。于是,控制权转交给内核的另一部分,通常导致的结果就是进程被强令关闭。

一旦出错的页通过了验证,MMU 随即更新,建立新的虚拟到物理的映射(如有必要,中断被移出页的映射),用户进程得以继续。造成页错误的用户进程对此不会有丝毫察觉,一切都在不知不觉中进行。


1.4.4 文件I/O——————见《Java NIO》一书的P21-------P22

1.4.4.1 内存映射文件
传统的文件 I/O 是通过用户进程发布 read( )和 write( )系统调用来传输数据的。为了在内核空间的文件系统页与用户空间的内存区之间移动数据,一次以上的拷贝操作几乎总是免不了的。这是因为,在文件系统页与用户缓冲区之间往往没有一一对应关系。但是,还有一种大多数操作系统都支持的特殊类型的 I/O 操作,允许用户进程最大限度地利用面向页的系统 I/O 特性,并完全摒弃缓冲区拷贝。这就是内存映射 I/O,如图 1-6 所示。

图 1-6. 用户内存到文件系统页的映射
内存映射 I/O 使用文件系统建立从用户空间直到可用文件系统页的虚拟内存映射。这样做有几个好处:
• 用户进程把文件数据当作内存,所以无需发布 read( )或 write( )系统调用。
• 当用户进程碰触到映射内存空间,页错误会自动产生,从而将文件数据从磁盘读进内存。如果用户修改了映射内存空间,相关页会自动标记为脏,随后刷新到磁盘,文件得到更新。
• 操作系统的虚拟内存子系统会对页进行智能高速缓存,自动根据系统负载进行内存管理。
•  数据总是按页对齐的,无需执行缓冲区拷贝。
• 大型文件使用映射,无需耗费大量内存,即可进行数据拷贝。
虚拟内存和磁盘 I/O 是紧密关联的,从很多方面看来,它们只是同一件事物的两面。在处理大量数据时,尤其要记得这一点。如果数据缓冲区是按页对齐的,且大小是内建页大小的倍数,那么,对大多数操作系统而言,其处理效率会大幅提升。


1.4.5 流I/O
并非所有 I/O 都像前几节讲的是面向块的,也有流 I/O,其原理模仿了通道。I/O 字节流必须顺序存取,常见的例子有 TTY(控制台)设备、打印机端口和网络连接。
流的传输一般(也不必然如此)比块设备慢,经常用于间歇性输入。多数操作系统允许把流置于非块模式,这样,进程可以查看流上是否有输入,即便当时没有也不影响它干别的。这样一种能力使得进程可以在有输入的时候进行处理,输入流闲置的时候执行其他功能。
比非块模式再进一步,就是就绪性选择。就绪性选择与非块模式类似(常常就是建立在非块模式之上),但是把查看流是否就绪的任务交给了操作系统。操作系统受命查看一系列流,并提醒进程哪些流已经就绪。这样,仅仅凭借操作系统返回的就绪信息,进程就可以使用相同代码和单一线程,实现多活动流的多路传输。这一技术广泛用于网络服务器领域,用来处理数量庞大的网络连接。就绪性选择在大容量缩放方面是必不可少的。

块模式文件
内容被缓冲的文件。对这种文件的所有读写操作均通过缓冲区进行,这样在下层硬件上就可以允许异步写入和读取。这使得系统在缓冲区中已经存在数据时就不必访问磁盘。

字符模式文件
内容未被缓冲的文件。当其同物理设备相关联后,对这样的设备所有的输入/输出均立即执行。某些特殊的字符设备由操作系统创建(/dev/zero、/dev/null 及其它等)。它们对应于数据流。


尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。非字节缓冲区可以在后台执行从字节或到字节的转换,这取决于缓冲区是如何创建的 .

缓冲区的工作与通道紧密联系。通道是 I/O 传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标。对于离开缓冲区的传输,您想传递出去的数据被置于一个缓冲区,被传送到通道。对于传回缓冲区的传输,一个通道将数据放置在您所提供的缓冲区中。这种在协同对象(通常是您所写的对象以及一到多个 Channel 对象)之间进行的缓冲区数据传递是高效数据处理的关键。

概念上,缓冲区是包在一个对象内的基本数据元素数组。Buffer 类相比一个简单数组的优点是它将关于数据的数据内容和信息包含在一个单一的对象中。

2. 1 缓冲区基础
属性:
容量(Capacity)
       缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
上界(Limit)
       缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
位置(Position)
       下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。它在调用 put()时指出了下一个数据元素应该被插入的位置,或者当 get()被调用时指出下一个元素应从何处检索。

标记(Mark)
        一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position =mark。标记在设定前是未定义的(undefined)。

这四个属性之间总是遵循以下关系:
0 <= mark <= position <= limit <= capacity


缓冲区 API
对于 API 还要注意的一点是 isReadOnly()函数。所有的缓冲区都是可读的,但并非所有都可写。每个具体的缓冲区类都通过执行 isReadOnly()来标示其是否允许该缓存区的内容被修改。一些类型的缓冲区类可能未使其数据元素存储在一个数组中。例如MappedByteBuffer 的内容可能实际是一个只读文件。您也可以明确地创建一个只读视图缓冲区,来防止对内容的意外修改。对只读的缓冲区的修改尝试将会导致ReadOnlyBufferException 抛出。但是我们要提前做好准备。
public abstract byte get()
public abstract byte get(int index)
public abstract ByteBuffer put(byte b);
public abstract ByteBuffer put(int index,byte b);
Get 和 put 可以是相对的或者是绝对的。在前面的程序列表中,相对方案是不带有索引参数的函数。当相对函数被调用时,位置在返回时前进一。绝对存取不会影响缓冲区的位置属性.

如果将缓冲区翻转两次会怎样呢?它实际上会大小变为 0。——————即位置和上界都变成0,即使这时容量没变


批量移动
public abstract class CharBuffer extends Buffer implements CharSequence, Comparable
{
          // This is a partial API listing
          public CharBuffer get (char [] dst)
          public CharBuffer get (char [] dst, int offset, int length)
          public final CharBuffer put (char[] src)
          public CharBuffer put (char [] src, int offset, int length)
          public CharBuffer put (CharBuffer src)
          public final CharBuffer put (String src)
          public CharBuffer put (String src, int start, int end)
}
对于get相关的方法,如果您所要求的数量的数据不能被传送,那么不会有数据被传递,缓冲区的状态保持不变,同时抛出 BufferUnderflowException 异常。因此当您传入一个数组并且没有指定长度,您就相当于要求整个数组被填充。如果缓冲区中的数据不够完全填满数组,您会得到一个异常。这意味着如果您想将一个小型缓冲区传入一个大型数组,您需要明确地指定缓冲区中剩余的数据长度。上面的第一个例子不会如您第一眼所推出的结论那样,将缓冲区内剩余的数据元素复制到数组的底部。要将一个缓冲区释放到一个大数组中,要这样做:
char [] bigArray = new char [1000];

// Get count of chars remaining in the buffer
int length = buffer.remaining( );

// Buffer is known to contain < 1,000 chars
buffer.get (bigArrray, 0, length);

// Do something useful with the data
processData (bigArray, length);
记住在调用 get( )之前必须查询缓冲区中的元素数量(因为我们需要告知 processData( )被放置在 bigArray 中的字符个数)。调用 get( )会向前移动缓冲区的位置属性,所以之后调用remaining( )会返回 0。

对于put相关的方法:如 果 缓 冲 区 有 足 够 的 空 间 接 受 数 组 中 的 数 据(buffer.remaining()>myArray.length),数据将会被复制到从当前位置开始的缓冲区,并且缓冲区位置会被提前所增加数据元素的数量。如果缓冲区中没有足够的空间,那么不会有数据被传递,同时抛出一个 BufferOverflowException 异常。


创建缓冲区
public abstract class CharBuffer extends Buffer implements CharSequence, Comparable
{
        // This is a partial API listing
        public static CharBuffer allocate (int capacity)
        public static CharBuffer wrap (char [] array)
        public static CharBuffer wrap (char [] array, int offset,
        int length)
        public final boolean hasArray( )
        public final char [] array( )
        public final int arrayOffset( )
}
新的缓冲区是由分配或包装操作创建的。分配操作创建一个缓冲区对象并分配一个私有的空间来储存容量大小的数据元素。包装操作创建一个缓冲区对象但是不分配任何空间来储存数据元素。它使用您所提供的数组作为存储空间来储存缓冲区中的数据元素。

char [] myArray = new char [100];
CharBuffer charbuffer = CharBuffer.wrap (myArray, 12, 42);
创建了一个 position 值为 12,limit 值为 54,容量为 myArray.length 的缓冲区。这个函数并不像您可能认为的那样,创建了一个只占用了一个数组子集的缓冲区。这个缓冲区可以存取这个数组的全部范围;offset 和 length 参数只是设置了初始的状态。调用使用上面代码中的方法创建的缓冲区中的 clear()函数,然后对其进行填充,直到超过上界值,这将会重写数组中的所有元素。Slice()函数(2.3 节将会讨论)可以提供一个只占用备份数组一部分的缓冲区。

通过 allocate()或者 wrap()函数创建的缓冲区通常都是间接的(直接缓冲区会在2.4.2 节讨论)。间接的缓冲区使用备份数组,像我们之前讨论的,您可以通过上面列出的API 函数获得对这些数组的存取权。Boolean 型函数 hasArray()告诉您这个缓冲区是否有一个可存取的备份数组。如果这个函数的返回 true,array()函数会返回这个缓冲区对象所使用的数组存储空间的引用。

到现在为止,这一节所进行的讨论已经针对了所有的缓冲区类型。我们用来做例子的CharBuffer 提供了一对其它缓冲区类没有的有用的便捷的函数:
public abstract class CharBuffer extends Buffer implements CharSequence, Comparable
{
        // This is a partial API listing
       public static CharBuffer wrap (CharSequence csq)
       public static CharBuffer wrap (CharSequence csq, int start,int end)
}
Wrap()函数创建一个只读的备份存储区是 CharSequence 接口或者其实现的的缓冲区对象。(CharSequence 对象将在第五章讨论)。Charsequence 描述了一个可读的字符流 。 像 JDK1.4 中 , 三 个 标 准 的 实 现 了 Charsequence 接 口 的 类 : String ,StringBuffer,和 CharBuffer。Wrap()函数可以很实用地“缓冲”一个已有的字符数据,通过缓冲区的 API 来存取其中的内容。对于字符集解码(第六章)和正则表达式处理(第五章)这将是非常方便的。

复制缓冲区
如我们刚刚所讨论的那样,可以创建描述从外部存储到数组中的数据元素的缓冲区对象。但是缓冲区不限于管理数组中的外部数据。它们也能管理其他缓冲区中的外部数据。当一个管理其他缓冲器所包含的数据元素的缓冲器被创建时,这个缓冲器被称为视图缓冲器。
视图存储器总是通过调用已存在的存储器实例中的函数来创建。使用已存在的存储器实例中的工厂方法意味着视图对象为原始存储器的内部实现细节私有。数据元素可以直接存取,无论它们是存储在数组中还是以一些其他的方式,而不需经过原始缓冲区对象的 get()/put()API。如果原始缓冲区是直接缓冲区,该缓冲区的视图会具有同样的效率优势。
public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable
{
// This is a partial API listing
public abstract CharBuffer duplicate( );
public abstract CharBuffer asReadOnlyBuffer( );
public abstract CharBuffer slice( );
}
Duplicate()函数创建了一个与原始缓冲区相似的新缓冲区。两个缓冲区共享数据元素,拥有同样的容量,但每个缓冲区拥有各自的位置,上界和标记属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。这一副本缓冲区具有与原始缓冲区同样的数据视图。如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。

您 可 以 使 用 asReadOnlyBuffer() 函 数 来 生 成 一 个 只 读 的 缓 冲 区 视 图 。 这 与duplicate()相同,除了这个新的缓冲区不允许使用 put(),并且其 isReadOnly()函数将 会 返 回 true 。 对 这 一 只 读 缓 冲 区 的 put() 函 数 的 调 用 尝 试 会 导 致 抛 出ReadOnlyBufferException 异常。

分割缓冲区与复制相似,但 slice()创建一个从原始缓冲区的当前位置开始的新缓冲区,并且其容量是原始缓冲区的剩余元素数量(limit-position)。这个新缓冲区与原始缓冲区共享一段数据元素子序列。分割出来的缓冲区也会继承只读和直接属性。

























 

多数情况下,通道与操作系统的文件描述符(File Descriptor)和文件句柄(File Handle)有着一对一的关系。虽然通道比文件描述符更广义,但您将经常使用到的多数通道都是连接到开放的文件描述符的。Channel 类提供维持平台独立性所需的抽象过程,不过仍然会模拟现代操作系统本身的 I/O 性能。


 

通道只能在字节缓冲区上操作。层次结构表明其他数据类型的通道也可以从 Channel 接口引申而来。这是一种很好的类设计,不过非字节实现是不可能的,因为操作系统都是以字节的形式实现底层 I/O 接口的。

 

 

通道可以以多种方式创建。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( );


通道会连接一个特定 I/O 服务且通道实例(channel instance)的性能受它所连接的 I/O 服务的特征限制,记住这很重要。
我们知道,一个文件可以在不同的时候以不同的权限打开。从 FileInputStream 对象的getChannel( )方法获取的 FileChannel 对象是只读的,不过从接口声明的角度来看却是双向的,因为FileChannel 实现 ByteChannel 接口。在这样一个通道上调用 write( )方法将抛出未经检查的NonWritableChannelException 异常,因为 FileInputStream 对象总是以 read-only 的权限打开文件。


ByteChannel 的 read( ) 和 write( )方法使用 ByteBuffer 对象作为参数。两种方法均返回已传输的字节数,可能比缓冲区的字节数少甚至可能为零。缓冲区的位置也会发生与已传输字节相同数量的前移。如果只进行了部分传输,缓冲区可以被重新提交给通道并从上次中断的地方继续传输。该过程重复进行直到缓冲区的 hasRemaining( )方法返回 false 值。

通道可以以阻塞(blocking)或非阻塞(nonblocking)模式运行。非阻塞模式的通道永远不会让调用的线程休眠。请求的操作要么立即完成,要么返回一个结果表明未进行任何操作。只有面向流的(stream-oriented)的通道,如 sockets 和 pipes 才能使用非阻塞模式。

与缓冲区不同,通道不能被重复使用。一个打开的通道即代表与一个特定 I/O 服务的特定连接并封装该连接的状态。当通道关闭时,那个连接会丢失,然后通道将不再连接任何东西。调用通道的close( )方法时,可能会导致在通道关闭底层I/O服务的过程中线程暂时阻塞 ,哪怕该通道处于非阻塞模式。通道关闭时的阻塞行为(如果有的话)是高度取决于操作系统或者文件系统的。在一个通道上多次调用close( )方法是没有坏处的,但是如果第一个线程在close( )方法中阻塞,那么在它完成关闭通道之前,任何其他调用close( )方法都会阻塞。后续在该已关闭的通道上调用close( )不会产生任何操作,只会立即返回。


通道引入了一些与关闭和中断有关的新行为。如果一个通道实现 InterruptibleChannel 接口,它的行为以下述语义为准:如果一个线程在一个通道上被阻塞并且同时被中断(由调用该被阻塞线程的 interrupt( )方法的另一个线程中断),那么该通道将被关闭,该被阻塞线程也会产生一个 ClosedByInterruptException 异常。
此外,假如一个线程的 interrupt status 被设置并且该线程试图访问一个通道,那么这个通道将立即被关闭,同时将抛出相同的 ClosedByInterruptException 异常。线程的 interrupt status 在线程的interrupt( )方法被调用时会被设置。我们可以使用 isInterrupted( )来测试某个线程当前的 interruptstatus。当前线程的 interrupt status 可以通过调用静态的 Thread.interrupted( )方法清除。

可中断的通道也是可以异步关闭的。实现 InterruptibleChannel 接口的通道可以在任何时候被关闭,即使有另一个被阻塞的线程在等待该通道上的一个 I/O 操作完成。当一个通道被关闭时,休眠在该通道上的所有线程都将被唤醒并接收到一个 AsynchronousCloseException 异常。接着通道就被关闭并将不再可用。

不实现 InterruptibleChannel 接口的通道一般都是不进行底层本地代码实现的有特殊用途的通道。这些也许是永远不会阻塞的特殊用途通道,如旧系统数据流的封装包或不能实现可中断语义的writer 类等。


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

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









 


 



 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics