NIO——笔记四

通道(Channel)

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

通道是一种途径,借助该途径,可以用最小的总开销来访问操作系统本身的 I/O 服务。缓冲区则是通道内部用来发送和接收数据的端点。
NIO——笔记四_第1张图片
与缓冲区不同,通道 API 主要由接口指定。不同的操作系统上通道实现会有根本性差异,所以通道 API 仅仅描述了可以做什么。因此很自然地,通道实现经常使用操作系统的本地代码。通道接口允许你以一种受控课移植的方式来访问底层的 I/O 服务。

顶层的 Channel 接口看到,对所有的通道来说只有两种共同的操作: 检查一个通道是否打开 和 关闭一个打开的通道

InterruptibleChannel 是一个标记接口,当被通道使用时可以标示该通道是可以中断的。如果连接可中断通道的线程被中断,那么该通道会以特别的方式工作,大多数但并非全部的通道都是可以中断的。

AbstractInterruptibleChannel 和 AbstractSelectableChannel 分别是可中断的 和 可选择的通道实现提供所需的常用方法。尽管描述通道行为的接口都是在 java.io.channels 包中定义的,不过具体的通道实现却都是从 java.io.channels.spi 中的类引申来的。这使得他们可以访问受保护的方法,而这些方法普通的通道用户永远也不会调用

通道可以是单向的也可以是双向的,一个 Channel 类可能实现定义 read() 方法的 ReadableByteChannel 接口,而另一个 chanel 类也许实现 WritableByteChannel 接口以提供 write() 方法。实现这两种接口其中之一的类都是单向的,只能在一个方向上传输数据。如果一个类同时实现这两个接口,那么它是双向的,可以双向传输数据。
对于使用 java.nio.channels 包中标准通道类的程序员来说,这些接口并没有太大的吸引力。假如你看过源码或api,你会发现每一个 file 或 socket 通道都实现全部的三个接口。从类的定义而言,这意味着全部 file 和 socket 通道对象都是双向的。这对于 socket 不是问题,因为它们一直都是双向的,不过对于 files 却是个问题。因为我们都知道,一个文件在不同的时候以不同的权限打开。从 FileInputStream 对象的 getChannel() 方法 获取的 FileChannel 对象是只读的,不过从接口声明的角度来看却是双向的,因为 FileChannel 实现 ByteChannel 接口。在这样一个通道上调用 write() 方法将抛出未经检查的 NonWritableChannelException 异常,因为 FileInputStream 对象总是以 read-only 的权限打开文件

通道会连接一个特定的 I/O 服务且通道实例的性能受它所连接的 I/O 服务的特征限制,记住这很重要。
注意: 根据底层文件句柄的访问模式,通道实例可能不允许使用 read() 或 write() 方法

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

socket 通道类从 SelectableChannel 引申而来。从 SelectableChannel 引申而来的类可以支持有条件的选择的选择器一起使用。将非阻塞 I/O 和选择器组合起来可以使你的程序利用多路复用 I/O

关闭通道
与缓冲区不同,通道不能被重复利用。一个打开的通道即代表一个特定的I/O 服务的特定连接,并封装该连接的状态。当通道关闭时,那个连接会丢失,然后通道不再会连接任何东西。

调用通道的 close() 方法时,可能会导致再通道关闭底层 I/O 服务的过程中线程暂时性阻塞(Socket 通道关闭会花费较长时间,具体耗时取决于操作系统的网络实现。在输出内容被提取时,一些网络协议堆栈可能会阻塞通道的关闭) 哪怕该通道处于非阻塞模式。通道关闭时的阻塞行为(如果有的话)是高度取决于操作系统或文件系统的。在一个通道上多次调用 close() 方法是没有坏处的,但如果第一个线程在close() 方法中阻塞,那么在它完成关闭通道之前,任何其他调用 close() 方法都会阻塞。后续咋爱该已关闭的通道上调用 close() 不会产生任何操作,只会立即返回

通道引入了一些与关闭和中断有关的新行为。如果一个通道实现 InterruptibleChannel 接口,它的行为以以下语义为准:如果一个线程在一个通道上被阻塞并且同时被中断(由调用该被阻塞线程的 interrupt() 方法的另一个线程中断),那么该通道将被关闭,该被阻塞线程也会产生一个 ClosedByInterrupException 异常

注意: 不要在 Channels 上休眠的中断线程同在 Selectors 上休眠的中断线程混淆。前者会关闭通道,而后者则不会。不过,如果你的线程在 Selector 上休眠时被中断,那它的 interrupt status 会被设置。假设那个线程接着又访问一个 Channel,则该通道会被关闭。

仅仅因为休眠在其上的线程被中断就关闭通道,者看起来似乎过于苛刻,不过这却是 NIO 架构师们所作出的明确设计决定。经验表明,想要在所有的操作系统上一致而可靠地处理被中断的 I/O 操作是不可能的。“在全部平台上提供确定的通道行为"这一需求导致了” 当 I/O操作被中断时总是关闭通道"这一设计选择。这个选择被认为是可接受的,因为大部分时候一个线程被中断就是希望以此来关闭通道。 Java.nio包中强制使用此行为来避免因操作系统独特性而导致的困境,因为该困境对 I/O区域而言是及其危险的,这也是为其增强健壮性而采用的一种经典的权衡

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

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

访问文件

每个FileChannel 对象都同一个文件描述符有一对一的关系。本质上来讲,RandomAccessFile 类提供的是同样的抽象内容,在通道出现之前,底层的文件操作都是通过 RandomAccessFile 类的方法来实现的。FileChannel 模拟同样的 I/O 服务,因此它们的 API 自然是很相似的。

同底层的文件描述符一样,每个 FileChannel 都有一个叫"file position" 的概念。这个 position 值决定文件中哪一处的数据姐下来将被读或写。有两种形式的 position() 方法。第一种,不带参数,返回当前文件的 position 值。返回值是一个长整型,表示文件中当前字节位置。第二种形式的 position() 方法带一个 long 参数并将通道的 position 设置为指定值。如果尝试将通道 position 设置为一个负值会导致异常,不过可以把 position 设置到超出文件尾,这样做会把 position 设置为指定值而不改变文件大小。假如在将 position 设置为超出当前文件大小时实现了一个read() 方法,那么会返回一个文件尾条件;倘若此时实现的是一个 write() 方法则会引起文件增长以容纳写入的字节,具体行为类似于实现一个绝对 write() 并可能导致出现一个文件空洞。
文件空洞:当磁盘上一个文件的分配空间小于它的文件大小时会出现“文件空洞”。对于内容稀疏的文件,大多数现代文件系统只为实际写入的数据分配磁盘空间(更准确的说,只为那些写入数据的文件系统页分配空间)。假如数据被写入到文件中非连续的位置上,这将导致文件在逻辑上不包含数据的区域(即“空洞”)。

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

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

当需要减少一个文件的 size 时,truncate() 方法会砍掉您所指定的新 size 值之外的所有数据。如果当前 size 大于新 size,超出新 size 的所有字节都会被悄悄地丢弃。如果提供的新 size 值大于或等于当前文件的 size 值,该文件不会被修改。这两种情况下,truncate()都会产生副作用:文件的 position 会被设置为所提供的新 size 值。

文件锁定

并非所有平台都以同一个方式来实现基本的文件锁定。在不同的操作系统上,甚至在同一个操作系统的不同文件上,文件锁定的语义都会有所差异。

有关 FileChannel 实现的文件锁模型的一个重要的注意事项是: 锁对象是文件而不是通道或线程,这意味着文件锁不适用于判优同一个 JAVA虚拟机上的多个线程发起的访问.文件锁旨在进程级别判优文件访问,比如在主要的程序组件之间或者在集成其他供应商的组件时。如果你需要控制多个java线程的并发访问,你可能需要实施你自己的、轻量级的锁定方案。那种情况下,内存映射文件可能是一个何使的选择。

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

你可能感兴趣的:(Java基础学习)