`

Java NIO工作原理(3)

阅读更多

◆  关于缓冲区的更多内容

概  述

到目前为止,您已经学习了使用缓冲区进行日常工作所需要掌握的大部分内容。我们的例子没怎么超出标准的读/写过程种类,在原来的 I/O 中可以像在 NIO 中一样容易地实现这样的标准读写过程。

本节将讨论使用缓冲区的一些更复杂的方面,比如缓冲区分配、包装和分片。我们还会讨论 NIO 带给 Java 平台的一些新功能。您将学到如何创建不同类型的缓冲区以达到不同的目的,如可保护数据不被修改的 只读 缓冲区,和直接映射到底层操作系统缓冲区的 直接 缓冲区。我们将在本节的最后介绍如何在 NIO 中创建内存映射文件。

缓冲区分配和包装

在能够读和写之前,必须有一个缓冲区。要创建缓冲区,您必须 分配 它。我们使用静态方法 allocate() 来分配缓冲区:

  1. ByteBuffer buffer = ByteBuffer.allocate( 1024 ); 

allocate() 方法分配一个具有指定大小的底层数组,并将它包装到一个缓冲区对象中 ― 在本例中是一个 ByteBuffer。

您还可以将一个现有的数组转换为缓冲区,如下所示:

  1. byte array[] = new byte[1024];  
  2. ByteBuffer buffer = ByteBuffer.wrap( array ); 

本例使用了 wrap() 方法将一个数组包装为缓冲区。必须非常小心地进行这类操作。一旦完成包装,底层数据就可以通过缓冲区或者直接访问。

缓冲区分片

slice() 方法根据现有的缓冲区创建一种 子缓冲区 。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。

使用例子可以最好地说明这点。让我们首先创建一个长度为 10 的 ByteBuffer:

  1. ByteBuffer buffer = ByteBuffer.allocate( 10 ) 

然后使用数据来填充这个缓冲区,在第 n 个槽中放入数字 n:

  1. for (int i=0; i<buffer.capacity(); ++i) {  
  2.      buffer.put( (byte)i );  

现在我们对这个缓冲区 分片 ,以创建一个包含槽 3 到槽 6 的子缓冲区。在某种意义上,子缓冲区就像原来的缓冲区中的一个 窗口 。

窗口的起始和结束位置通过设置 position 和 limit 值来指定,然后调用 Buffer 的 slice() 方法:

  1. buffer.position( 3 );  
  2. buffer.limit( 7 );  
  3. ByteBuffer slice = buffer.slice(); 

片 是缓冲区的 子缓冲区 。不过, 片段 和 缓冲区 共享同一个底层数据数组,我们在下一节将会看到这一点。

缓冲区份片和数据共享

我们已经创建了原缓冲区的子缓冲区,并且我们知道缓冲区和子缓冲区共享同一个底层数据数组。让我们看看这意味着什么。

我们遍历子缓冲区,将每一个元素乘以 11 来改变它。例如,5 会变成 55。

  1. for (int i=0; i<slice.capacity(); ++i) {  
  2.      byte b = slice.get( i );  
  3.      b *= 11;  
  4.      slice.put( i, b );  

最后,再看一下原缓冲区中的内容:

  1. buffer.position( 0 );  
  2. buffer.limit( buffer.capacity() );  
  3.  while (buffer.remaining()>0) {  
  4.      System.out.println( buffer.get() );  

结果表明只有在子缓冲区窗口中的元素被改变了:

$ java SliceBuffer 
0 
1 
2 
33 
44 
55 
66 
7 
8 
9

缓冲区片对于促进抽象非常有帮助。可以编写自己的函数处理整个缓冲区,而且如果想要将这个过程应用于子缓冲区上,您只需取主缓冲区的一个片,并将它传递给您的函数。这比编写自己的函数来取额外的参数以指定要对缓冲区的哪一部分进行操作更容易。

只读缓冲区

只读缓冲区非常简单 ― 您可以读取它们,但是不能向它们写入。可以通过调用缓冲区的 asReadOnlyBuffer() 方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。

只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时,您无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以 保证 该缓冲区不会被修改。

不能将只读的缓冲区转换为可写的缓冲区。

直接和间接缓冲区

另一种有用的 ByteBuffer 是直接缓冲区。 直接缓冲区 是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。

实际上,直接缓冲区的准确定义是与实现相关的。Sun 的文档是这样描述直接缓冲区的:

给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。

您可以在例子程序 FastCopyFile.java 中看到直接缓冲区的实际应用,这个程序是 CopyFile.java 的另一个版本,它使用了直接缓冲区以提高速度。

还可以用内存映射文件创建直接缓冲区。

内存映射文件 I/O

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。

内存映射文件 I/O 是通过使文件中的数据神奇般地出现为内存数组的内容来完成的。这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会送入(或者 映射 )到内存中。

内存映射并不真的神奇或者多么不寻常。现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java 内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。

尽管创建内存映射文件相当简单,但是向它写入可能是危险的。仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

将文件映射到内存

了解内存映射的最好方法是使用例子。在下面的例子中,我们要将一个 FileChannel (它的全部或者部分)映射到内存中。为此我们将使用 FileChannel.map() 方法。下面代码行将文件的前 1024 个字节映射到内存中:

  1. MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,     01024 ); 

map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行行映射。

◆  分散和聚集

概  述

分散/聚集 I/O 是使用多个而不是单个缓冲区来保存数据的读写方法。

一个分散的读取就像一个常规通道读取,只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中。同样地,一个聚集写入是向缓冲区数组而不是向单个缓冲区写入数据。

分散/聚集 I/O 对于将数据流划分为单独的部分很有用,这有助于实现复杂的数据格式。

分散/聚集 I/O

通道可以有选择地实现两个新的接口: ScatteringByteChannel 和 GatheringByteChannel。一个 ScatteringByteChannel 是一个具有两个附加读方法的通道:

• long read( ByteBuffer[] dsts );

• long read( ByteBuffer[] dsts, int offset, int length );

这些 long read() 方法很像标准的 read 方法,只不过它们不是取单个缓冲区而是取一个缓冲区数组。

在 分散读取 中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区数组就像一个大缓冲区。

分散/聚集的应用

分散/聚集 I/O 对于将数据划分为几个部分很有用。例如,您可能在编写一个使用消息对象的网络应用程序,每一个消息被划分为固定长度的头部和固定长度的正文。您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区。当您将它们放入一个数组中并使用分散读取来向它们读入消息时,头部和正文将整齐地划分到这两个缓冲区中。

我们从缓冲区所得到的方便性对于缓冲区数组同样有效。因为每一个缓冲区都跟踪自己还可以接受多少数据,所以分散读取会自动找到有空间接受数据的第一个缓冲区。在这个缓冲区填满后,它就会移动到下一个缓冲区。

聚集写入

聚集写入 类似于分散读取,只不过是用来写入。它也有接受缓冲区数组的方法:

• long write( ByteBuffer[] srcs );

• long write( ByteBuffer[] srcs, int offset, int length );

聚集写对于把一组单独的缓冲区中组成单个数据流很有用。为了与上面的消息例子保持一致,您可以使用聚集写入来自动将网络消息的各个部分组装为单个数据流,以便跨越网络传输消息。

从例子程序 UseScatterGather.java 中可以看到分散读取和聚集写入的实际应用。

◆  文件锁定

概  述

文件锁定初看起来可能让人迷惑。它 似乎 指的是防止程序或者用户访问特定文件。事实上,文件锁就像常规的 Java 对象锁 ― 它们是 劝告式的(advisory) 锁。它们不阻止任何形式的数据访问,相反,它们通过锁的共享和获取赖允许系统的不同部分相互协调。

您可以锁定整个文件或者文件的一部分。如果您获取一个排它锁,那么其他人就不能获得同一个文件或者文件的一部分上的锁。如果您获得一个共享锁,那么其他人可以获得同一个文件或者文件一部分上的共享锁,但是不能获得排它锁。文件锁定并不总是出于保护数据的目的。例如,您可能临时锁定一个文件以保证特定的写操作成为原子的,而不会有其他程序的干扰。

大多数操作系统提供了文件系统锁,但是它们并不都是采用同样的方式。有些实现提供了共享锁,而另一些仅提供了排它锁。事实上,有些实现使得文件的锁定部分不可访问,尽管大多数实现不是这样的。

在本节中,您将学习如何在 NIO 中执行简单的文件锁过程,我们还将探讨一些保证被锁定的文件尽可能可移植的方法。

锁定文件

要获取文件的一部分上的锁,您要调用一个打开的 FileChannel 上的 lock() 方法。注意,如果要获取一个排它锁,您必须以写方式打开文件。

  1. RandomAccessFile raf = new RandomAccessFile( "usefilelocks.txt""rw" );  
  2. FileChannel fc = raf.getChannel();  
  3. FileLock lock = fc.lock( start, end, false ); 

在拥有锁之后,您可以执行需要的任何敏感操作,然后再释放锁:

  1. lock.release(); 

在释放锁后,尝试获得锁的其他任何程序都有机会获得它。

本小节的例子程序 UseFileLocks.java 必须与它自己并行运行。这个程序获取一个文件上的锁,持有三秒钟,然后释放它。如果同时运行这个程序的多个实例,您会看到每个实例依次获得锁。

文件锁定可能是一个复杂的操作,特别是考虑到不同的操作系统是以不同的方式实现锁这一事实。下面的指导原则将帮助您尽可能保持代码的可移植性:

• 只使用排它锁。

• 将所有的锁视为劝告式的(advisory)。

分享到:
评论

相关推荐

    Java NIO原理 图文分析及代码实现

    ### Java NIO原理 图文分析及代码实现 #### 前言 在深入探讨Java NIO之前,我们先简要回顾一下NIO的概念及其引入的原因。随着互联网的发展,越来越多的应用程序需要处理高并发的网络连接请求。传统的阻塞I/O模型在...

    java NIO技巧及原理

    **Java IO原理:** Java IO基于流模型,分为输入流和输出流。流是一维数据序列,可以是从源到目标的单向流动。IO操作是阻塞的,即当一个线程执行读或写操作时,如果数据未准备好,线程会被阻塞,直到数据准备就绪。...

    Java NIO的介绍及工作原理

    ### Java NIO的介绍及工作原理 #### 一、引言 Java NIO(New I/O)是Java 1.4版本引入的一个重要的I/O处理框架,它为Java应用程序提供了处理I/O操作的新方法。与传统的Java IO模型相比,NIO提供了一种更加高效的...

    java NIO原理和使用

    在深入了解 Java NIO 的工作原理及其使用之前,我们首先来了解一下什么是 Java NIO(New I/O)。Java NIO 是 Java SE 1.4 版本引入的一个全新的 I/O API,用来替代传统的 I/O 类型,即 java.io 包下的类。 Java NIO...

    Java NIO原理解析

    3. **选择器(Selector)**:选择器是NIO的核心组件,它允许单个线程监控多个通道的事件,如连接、读就绪、写就绪等。通过注册通道到选择器,并设置感兴趣的事件,选择器会在这些事件发生时通知用户。这样,开发者...

    Java NIO原理分析及代码实例

    理解并掌握NIO的原理和使用,对于提升Java应用的性能和可扩展性至关重要。通过以上介绍的知识点,你可以开始编写基于NIO的应用,例如使用SocketChannel实现一个简单的非阻塞服务器。在实际编码时,参考博文链接中的...

    java NIO实例

    下面将详细介绍Java NIO的主要组件和工作原理,并结合这两个文件名推测它们可能包含的内容。 1. **Selector(选择器)**:选择器是NIO的核心组件,它能够监控多个通道(Channel)的状态变化,当某个通道准备进行...

    一个java NIO的例子

    下面我们将深入探讨Java NIO的关键组件和工作原理。 1. **通道(Channel)**:通道是数据传输的途径,类似于传统的流。Java NIO提供了多种类型的通道,如FileChannel用于文件操作,SocketChannel和...

    Java NIO工作原理的全面分析

    Java NIO(New Input/Output)是自JDK 1.4版本开始引入的一种I/O模型,相较于传统的基于流的I/O(IO),NIO提供了一种面向块的、高效的数据...理解和掌握NIO的工作原理,能够帮助开发者编写出性能更好的Java应用程序。

    Java.NIO资源下载资源下载

    - **Selector 基础**:介绍了 Selector 的基本概念及工作原理。 - **使用 SelectionKey**:解释了如何使用 SelectionKey 来管理 Channel 的选择状态。 - **使用 Selectors**:详细讨论了如何使用 Selector 来监听多...

    java nio示例代码

    这些示例通常会包含简单的读写文件、服务器端与客户端的通信以及多路复用的使用,帮助初学者快速理解NIO的工作原理和实际应用。在学习过程中,你可以逐步深入,从基础的Buffer操作到复杂的Selector机制,掌握Java ...

    java NIO详细教程

    ### Java NIO 详细教程知识点解析 #### 一、Java NIO 概述 Java NIO(New IO)是Java平台提供的一种新的IO操作模式,它首次出现在Java 1.4版本中,并在后续版本中不断完善。Java NIO 的设计目的是为了克服传统Java ...

    Java NIO原理和使用

    Java NIO非堵塞应用通常适用用在I/O读写等方面,我们知道,系统运行的性能瓶颈通常在I/O读写,包括对端口和文件的操作上,过去,在打开一个I/O通道后,read()将一直等待在端口一边读取字节内容,如果没有内容进来,...

    JavaNIO的原理.pdf

    Java NIO,全称为Non-Blocking Input/Output(非阻塞输入/输出),是Java从JDK 1.4版本开始引入的一种I/O模型,它为Java提供了更高效的数据传输能力,尤其适用于高并发的I/O操作场景。与传统的 Blocking I/O 相比,...

    Java NIO学习资料+代码.zip

    NIO的工作原理** 在NIO中,首先创建一个或多个通道,并将这些通道注册到选择器上。然后,主线程通过选择器监听所有通道的事件,如连接就绪、数据可读、写操作就绪等。当某个通道准备就绪时,选择器会返回这个通道...

    java nio聊天室源码

    Java NIO(New IO)是Java 1.4版本引入的一种新的I/O API,它提供了非阻塞I/O操作的能力,极大地提升了Java在...通过分析和学习这个源码,开发者可以深入理解Java NIO的工作原理,并将其应用于实际的网络编程项目中。

    Java NIO——Selector机制解析三(源码分析)

    本文将深入探讨Java NIO中的Selector机制,并通过源码分析来理解其实现原理。 Selector机制是Java NIO中的核心组件,它允许单线程同时监控多个通道(Channels)的状态变化,例如连接就绪、数据可读或可写等。这种...

Global site tag (gtag.js) - Google Analytics