Java-NIO(1)
概述
Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java1.4开始),Java NIO提供了与标准IO不同的IO工作方式。
所以Java NIO是一种新式的IO标准,与之前的普通IO的工作方式不同。标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入通道也类似。
由上面的定义就说明NIO是一种新型的IO,但NIO不仅仅就是等于Non-blocking IO(非阻塞IO),NIO中有实现非阻塞IO的具体类,但不代表NIO就是Non-blocking IO(非阻塞IO)。
Java NIO 由以下几个核心部分组成:
- Buffer
- Channel
- Selector
传统的IO操作面向数据流,意味着每次从流中读一个或多个字节,直至完成,数据没有被缓存在任何地方。NIO操作面向缓冲区,数据从Channel读取到Buffer缓冲区,随后在Buffer中处理数据。
翻译自JDK文档:
Java NIO包概述:
定义缓冲区,缓冲区是数据的容器,并提供其他NIO包的概述。
NIO主要的API是:
Buffers-是数据的容器
Charsets-字符集及其关联的解码器和编码器,在字节和Unicode字符之间转换
- Channels- 代表连接的各种类型的通道,能够执行I / O操作的实体
- Selectors & selection keys-选择器和选择键,以及可选通道定义了多路复用的非阻塞I / O设施。
java.nio包定义了缓冲区类,这些缓冲区类在整个NIO API中都有使用。 字符集API在java.nio.charset包中定义,而通道和选择器API在java.nio.channels包中定义。 这些子包中的每个子包都有其自己的服务提供(SPI)子包,其内容可用于扩展平台的默认实现或构建替代实现。
缓冲区是用于固定数量的特定基本数据类型的容器。 除了其内容外,缓冲区还具有一个位置(即要读取或写入的下一个元素的索引)和一个限制(即不应读取或写入的第一个元素的索引)的位置。 基本的Buffer类定义这些属性以及清除,翻转和倒带,标记当前位置以及将位置重置为先前标记的方法。
每个非布尔基元类型都有一个缓冲区类。 每个类都定义了一系列get和put方法,用于将数据移入和移出缓冲区,压缩,复制和切片缓冲区的方法,以及用于分配新缓冲区以及将现有数组包装到其中的静态方法。 缓冲。
字节缓冲区的区别在于它们可以用作I / O操作的源和目标。 它们还支持其他缓冲区类中未提供的一些功能:
可以将字节缓冲区分配为直接缓冲区,在这种情况下,Java虚拟机将尽最大努力直接在其上执行本机I / O操作
可以通过将文件的区域直接映射到内存中来创建字节缓冲区,在这种情况下,可以使用MappedByteBuffer类中定义的一些其他与文件相关的操作
字节缓冲区以大尾数或小尾数字节顺序提供对任何非布尔基元类型的二进制数据的异构或同质序列的访问
包中类概览:
Class | Description |
---|---|
Buffer | A container for data of a specific primitive type. |
ByteBuffer | A byte buffer. |
ByteOrder | A typesafe enumeration for byte orders. |
CharBuffer | A char buffer. |
DoubleBuffer | A double buffer. |
FloatBuffer | A float buffer. |
IntBuffer | An int buffer. |
LongBuffer | A long buffer. |
MappedByteBuffer | A direct byte buffer whose content is a memory-mapped region of a file. |
ShortBuffer | A short buffer. |
Buffer
直接已知子类:
ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer
概述:
特定基本数据类型的容器。
缓冲区是特定原始类型的元素的线性有限序列。 除了其内容之外,缓冲区的基本属性还包括其容量,限制和位置:
- capacity:缓冲区的容量是它包含的元素数量。 缓冲区的容量永远不会为负,也不会改变。
- limit:缓冲区的限制是不应读取或写入的第一个元素的索引。 缓冲区的限制永远不会为负,也永远不会大于缓冲区的容量。
- position:缓冲区的位置是下一个要读取或写入的元素的索引。 缓冲区的位置永远不会为负,也不会大于其限制。
对于每个非布尔基本类型,此类都有一个子类。
Transferring data
此类的每个子类定义了两种的get和put操作:
Relative operations:相对操作从当前位置开始读取或写入一个或多个元素,然后将该位置增加所传送元素的数量。 如果请求的传输超出限制,则相对的get操作将引发BufferUnderflowException,而相对的put操作将引发BufferOverflowException; 无论哪种情况,都不会传输数据。
Absolute operations :绝对运算采用显式元素索引,并且不影响位置。 如果index参数超出限制,则绝对的get和put操作将引发IndexOutOfBoundsException。
当然,也可以通过始终相对于当前位置的适当通道的I / O操作将数据移入或移出缓冲区。
Marking and resetting
缓冲区的标记是在调用reset方法时将其位置重置到的索引。 标记并非总是定义的,但是定义时,它永远不会为负,也永远不会大于位置。 如果定义了标记,则在将位置或限制调整为小于标记的值时将其丢弃。 如果未定义标记,则调用reset方法将引发InvalidMarkException。
Invariants
对于标记,位置,限制和容量值,以下不变式成立:
0 <= mark <= position <= limit <= capacity
新创建的缓冲区始终具有零position和未定义的mark 。 initial limit可以为零,也可以是其他一些值,具体取决于缓冲区的类型及其构造方式。 新分配的缓冲区的每个元素都初始化为零。
Additional operations
除了访问位置,极限和容量值以及标记和重置的方法之外,此类还定义了以下对缓冲区的操作:
clear():clear()使缓冲区为新的通道读取或相对放置操作序列做好准备:将limit设置为capacity ,并将position为零。
flip():flip()使缓冲区为新的通道写入或相对get操作序列做好准备:它将limit设置为position,然后将position设置为零。
rewind():rewind()使缓冲区准备好重新读取它已经包含的数据:保留limit不变,并将position设置为零。
- slice():slice()创建缓冲区的子序列:它使limit 和position 保持不变。
- duplicate():创建缓冲区的浅表副本:它使limit 和position 保持不变。
Read-only buffers
每个缓冲区都是可读的,但并非每个缓冲区都是可写的。 每个缓冲区类的变异方法都指定为可选操作,当对只读缓冲区调用时,该方法将引发ReadOnlyBufferException。 只读缓冲区不允许更改其内容,但其标记,位置和限制值是可变的。 缓冲区是否为只读可以通过调用isReadOnly方法来确定。
Thread safety
缓冲区不能安全用于多个并发线程。 如果一个缓冲区将由多个线程使用,则应通过适当的同步来控制对该缓冲区的访问。
Invocation chaining
此类中没有返回值的方法被指定为返回在其上调用它们的缓冲区。 这使得方法调用可以链接在一起; 例如,语句序列
b.flip();
b.position(23);
b.limit(42);
可以用一个更紧凑的语句代替
b.flip().position(23).limit(42);
Buffer初始时3个变量的情况如下图:
ByteBuffer
知道的直接子类:
MappedByteBuffer
概述:
此类定义了对字节缓冲区的六种操作类别:
读取和写入单个字节的绝对和相对的get和put方法;
相对批量获取方法,用于将字节的连续序列从此缓冲区传输到数组中;
相对批量放置方法,用于将字节数组或某些其他字节缓冲区中连续的字节序列传输到此缓冲区中;
绝对和相对的get和put方法,用于读取和写入其他基本类型的值,并将它们以特定字节顺序在字节序列之间来回转换;
创建视图缓冲区的方法,该方法允许将字节缓冲区视为包含某些其他原始类型值的缓冲区;
一种压缩字节缓冲区的方法。
字节缓冲区可以通过分配(为缓冲区内容分配空间)来创建,也可以通过将现有的字节数组包装到缓冲区中来创建。
Direct vs. non-direct buffers
字节缓冲区可以是直接的,也可以是非直接的。 给定直接字节缓冲区,Java虚拟机将尽最大努力直接在其上执行本机I / O操作。 也就是说,它将尝试避免在每次调用底层操作系统的本机I / O操作之前(或之后)将缓冲区的内容复制到中间缓冲区(或从中间缓冲区复制)。
可以通过调用此类的allocateDirect工厂方法来创建直接字节缓冲区。 这种方法返回的缓冲区通常比非直接缓冲区具有更高的分配和释放成本。 直接缓冲区的内容可能驻留在普通垃圾回收堆的外部,因此它们对应用程序内存占用的影响可能并不明显。 因此,建议直接缓冲区主要分配给大型,寿命长的缓冲区,这些缓冲区要受基础系统的本机I / O操作的约束。 通常,最好仅在直接缓冲区产生可衡量的程序性能提升时才分配它们。
直接字节缓冲区也可以通过将文件的区域直接映射到内存中来创建。 Java平台的实现可以选择支持通过JNI从本机代码创建直接字节缓冲区。 如果这些缓冲区之一的实例引用了内存的不可访问区域,则访问该区域的尝试将不会更改缓冲区的内容,并且将导致在访问时或稍后发生未指定的异常。 时间。
字节缓冲区是直接还是非直接缓冲区可以通过调用其isDirect方法来确定。 提供此方法是为了可以在性能关键代码中完成显式缓冲区管理。
Access to binary data
此类定义用于读取和写入除布尔值以外的所有其他基本类型的值的方法。 根据缓冲区的当前字节顺序将原始值转换为字节序列(或从字节序列转换为字节序列),可以通过order方法进行检索和修改。 特定的字节顺序由ByteOrder类的实例表示。 字节缓冲区的初始顺序始终为BIG_ENDIAN。
为了访问异构二进制数据(即不同类型的值的序列),此类定义了每种类型的绝对和相对get和put方法的族。 例如,对于32位浮点值,此类定义:
float getFloat()
float getFloat(int index)
void putFloat(float f)
void putFloat(int index, float f)
为char,short,int,long和double类型定义了相应的方法。 绝对get和put方法的索引参数以字节为单位,而不是读取或写入的类型。
为了访问同类的二进制数据(即相同类型的值的序列),此类定义了可以创建给定字节缓冲区的视图的方法。 视图缓冲区只是另一个缓冲区,其内容由字节缓冲区支持。 对字节缓冲区内容的更改将在视图缓冲区中可见,反之亦然; 两个缓冲区的位置,限制和标记值是独立的。 例如,asFloatBuffer方法创建FloatBuffer类的实例,该实例由在其上调用该方法的字节缓冲区支持。 为char,short,int,long和double类型定义了相应的视图创建方法。
与上述类型特定的get和put方法系列相比,视图缓冲区具有三个重要优点:
- 视图缓冲区的索引不是以字节为单位,而是根据其值的特定于类型的大小进行索引。
视图缓冲区提供相对的批量获取和放置方法,这些方法可以在缓冲区与数组或同一类型的其他缓冲区之间传输连续的值序列;
视图缓冲区可能会更有效,因为只有当其后备字节缓冲区是直接的时,视图缓冲区才是直接的。
视图缓冲区的字节顺序固定为创建视图时其字节缓冲区的字节顺序。
CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer说明与上面基本一致
java.nio.channels
包说明:
定义通道,这些通道代表与能够执行I / O操作的实体(例如文件和套接字)的连接; 定义用于多路复用,非阻塞I / O操作的选择器。
channel
通道表示与诸如硬件设备,文件,网络套接字或程序组件之类的实体的开放连接,该实体能够执行一个或多个不同的I / O操作(例如,读取或写入)。 如Channel接口中所指定,通道是打开的还是关闭的,并且它们都是异步可关闭和可中断的。
Channel接口由其他几个接口扩展。
ReadableByteChannel接口指定一种读取方法,该方法将通道中的字节读取到缓冲区中。 类似地,WritableByteChannel接口指定一种写入方法,该方法将字节从缓冲区写入通道。 对于可以同时读取和写入字节的通道的常见情况,ByteChannel接口将这两个接口统一起来。 SeekableByteChannel接口使用查询和修改通道的当前位置及其大小的方法扩展了ByteChannel接口。
ScatteringByteChannel和GatheringByteChannel接口分别扩展了ReadableByteChannel和WritableByteChannel接口,添加了采用一系列缓冲区而不是单个缓冲区的读取和写入方法。
NetworkChannel接口指定用于绑定通道的套接字,获取套接字所绑定的地址的方法,以及用于获取和设置套接字选项的方法。 MulticastChannel接口指定加入Internet协议(IP)多播组的方法。
Channels实用程序类定义了静态方法,这些方法支持java.io包的流类与该包的通道类的互操作。 可以从InputStream或OutputStream构造适当的通道,相反,可以从通道构造InputStream或OutputStream。 可以构造一个使用给定字符集的Reader来解码给定可读字节通道中的字节,相反,可以构造一个Writer来使用给定字符集将字符编码为字节并将它们写入给定可写字节通道。
FileChannel类支持从连接到文件的通道读取字节和向其写入字节的常规操作,以及查询和修改当前文件位置以及将文件截断为特定大小的常规操作。 它定义了获取整个文件或文件特定区域上的锁的方法。 这些方法返回FileLock类的实例。 最后,它定义了以下方法:强制更新文件以写入包含该文件的存储设备,有效地在文件和其他通道之间传输字节,以及将文件区域直接映射到内存中。
通过调用其静态打开方法之一,或者通过调用FileInputStream,FileOutputStream或RandomAccessFile的getChannel方法来创建FileChannel,以返回连接到与java.io类相同的基础文件的文件通道。
selector
selectors,可选通道和选择键提供了多路复用的非阻塞I / O,它比面向线程的阻塞I / O具有更大的可伸缩性。
选择器( selector)是可选通道的多路复用器,而可选通道又是可以置于非阻塞模式的特殊类型的通道。 为了执行多路复用的I / O操作,首先创建一个或多个可选通道,将其置于非阻塞模式,并向选择器注册。 注册通道指定了将由选择器测试是否准备就绪的一组I / O操作,并返回代表注册的选择键。
一旦某些频道已向选择器注册,就可以执行选择操作,以发现哪些频道(如果有的话)已准备好执行先前已声明感兴趣的一项或多项操作。 如果通道已准备好,则注册时返回的键将添加到选择器的选择键集中。 可以检查键集及其中的键,以确定每个通道准备就绪的操作。 一个人可以从每个键中检索相应的通道,以执行所需的任何I / O操作。
选择键指示其通道已准备好进行某些操作是一个提示,但不能保证,这样的操作可以由线程执行而不会导致线程阻塞。 必须编写执行多路I / O的代码,以便在证明不正确时忽略这些提示。
该软件包定义了与java.net软件包中定义的DatagramSocket,ServerSocket和Socket类相对应的selectable-channel类。 为了支持与通道关联的套接字,对这些类进行了较小的更改。 该软件包还定义了一个实现单向管道的简单类。 在所有情况下,都可以通过调用相应类的静态open方法来创建新的可选通道。 如果通道需要关联的套接字,则将创建套接字作为此操作的副作用。
选择器,可选通道和选择键的实现可以通过“插入” java.nio.channels.spi包中定义的SelectorProvider类的替代定义或实例来替换。 预计不会有许多开发人员实际使用此功能。 它主要是为了使高级用户在需要非常高的性能时可以利用特定于操作系统的I / O多路复用机制。
实现复用I / O抽象所需的许多记帐和同步操作都是由java.nio.channels.spi包中的AbstractInterruptibleChannel,AbstractSelectableChannel,AbstractSelectionKey和AbstractSelector类执行的。 在定义自定义选择器提供程序时,应仅直接子类化AbstractSelector和AbstractSelectionKey类。 自定义渠道类应扩展此包中定义的适当的SelectableChannel子类。
Asynchronous channels
异步通道是一种特殊类型的通道,能够执行异步I / O操作。 异步通道是非阻塞的,并定义了用于启动异步操作的方法,并返回表示每个操作的未决结果的Future。 Future可用于轮询或等待操作结果。 异步I / O操作还可以指定CompletionHandler来在操作完成时调用。 完成处理程序是用户提供的代码,执行该代码以消耗I / O操作的结果。
该包定义了异步通道类,这些类连接到面向流的连接或侦听套接字或面向数据报的套接字。 它还定义了AsynchronousFileChannel类,用于异步读取,写入和操作文件。 与FileChannel一样,它支持将文件截断为特定大小,强制更新文件以将其写入存储设备或获取整个文件或文件特定区域上的锁的操作。 与FileChannel不同,它没有定义用于将文件区域直接映射到内存中的方法。 如果需要内存映射的I / O,则可以使用FileChannel。
出于资源共享的目的,异步通道绑定到异步通道组。 组具有关联的ExecutorService,向其提交了处理I / O事件的任务,并将其分派给完成处理程序,这些处理程序使用了在组中通道上执行的异步操作的结果。 创建通道时可以选择指定组,也可以将通道绑定到默认组。 老练的用户可能希望创建自己的异步通道组或配置将用于默认组的ExecutorService。
与选择器一样,可以通过“插入” java.nio.channels.spi包中定义的AsynchronousChannelProvider类的替代定义或实例来替换异步通道的实现。 预计不会有许多开发人员实际使用此功能。 它主要是为了使高级用户在需要非常高的性能时可以利用特定于操作系统的异步I / O机制。
Channel
通道是Java NIO提供的一座桥梁,用于我们的程序和操作系统底层I/O服务进行交互。
通道表示与诸如硬件设备,文件,网络套接字或程序组件之类的实体的开放连接,该实体能够执行一个或多个不同的I / O操作(例如,读取或写入)。
通道是打开的还是关闭的。 通道在创建时打开,一旦关闭,它便保持关闭状态。 通道一旦关闭,任何在其上调用I / O操作的尝试都将引发ClosedChannelException。 可以通过调用isOpen方法来测试通道是否打开。
通常,通道的目的是确保多线程访问的安全,如接口规范以及扩展和实现此接口的类中所述。
通道是一种很基本很抽象的描述,和不同的I/O服务交互,执行不同的I/O操作,实现不一样,因此具体的有FileChannel、SocketChannel等。
Java NIO中最常用的通道实现是如下几个,可以看出跟传统的 I/O 操作类是一一对应的。
- FileChannel:读写文件
- DatagramChannel: UDP协议网络通信
- SocketChannel:TCP协议网络通信
- ServerSocketChannel:监听TCP连接
Selector
Selector(选择器)是一个特殊的组件,用于采集各个通道的状态(或者说事件)。我们先将通道注册到选择器,并设置好关心的事件,然后就可以通过调用select()方法,静静地等待事件发生。
通道有如下4个事件可供我们监听:
- Accept:有可以接受的连接
- Connect:连接成功
- Read:有数据可读
- Write:可以写入数据了
SelectableChannel对象的多路复用器。
可以通过调用此类的open方法来创建选择器,该方法将使用系统的默认选择器提供程序来创建新的选择器。 选择器也可以通过调用自定义选择器提供程序的openSelector方法来创建。 选择器保持打开状态,直到通过其close方法关闭它为止。
可选通道在选择器中的注册由SelectionKey对象表示。 选择器维护三组选择键:
- 键集包含代表此选择器当前通道注册的键。 该集合由keys方法返回。
- 所选键集合是键集合,使得在先前的选择操作期间,每个键的信道被检测为准备好在键的目标集中标识的至少一个操作。 该集合由selectedKeys方法返回。 所选键集始终是键集的子集。
- 取消键集是已取消但其通道尚未注销的键集。 此集不能直接访问。 取消键集始终是键集的子集。
在新创建的选择器中,所有三个集合均为空。
一个键被添加到选择器的键集中,作为通过通道的注册方法注册通道的副作用。 在选择操作期间,已取消的键将从键集中删除。 密钥集本身不能直接修改。
取消键时,无论是通过关闭其通道还是通过调用其cancel方法,都会将一个键添加到其选择器的cancelled键集中。 取消某个键将导致其频道在下一次选择操作期间被注销,这时该键将从所有选择器的键集中删除。
通过选择操作将键添加到所选键集。 通过调用集合的remove方法或调用从集合中获得的迭代器的remove方法,可以直接从所选键集中删除密钥。 决不能以任何其他方式将键从选定键集中删除。 作为选择操作的副作用,尤其不要删除它们。 键可能无法直接添加到所选键集中。
Selection
在每个选择操作期间,可以将键添加到选择器的选定键集中或从中删除,也可以将其从选择键和取消键集中删除。 选择是通过select(),select(long)和selectNow()方法执行的,包括三个步骤:
- 1 已取消键集中的每个键都从其所属的每个键集中删除,并且其通道已注销。 此步骤将取消键设置为空。
2 询问底层操作系统是否有更新,有关每个剩余通道的准备情况,以执行自选择操作开始时由其键的兴趣集标识的任何操作。 对于准备进行至少一项此类操作的通道,将执行以下两个操作之一:
(1)如果通道的键尚未位于所选键集中,则将其添加到该键集中,并修改其就绪操作集以准确标识现在报告通道已准备就绪的那些操作。 先前记录在就绪集中的任何准备信息都将被丢弃。
(2)否则,通道的键已经在所选键集中,因此修改其就绪操作集以识别报告通道已准备就绪的任何新操作。 先前记录在就绪集中的任何准备信息都将保留; 换句话说,底层系统返回的就绪集按位分离到密钥的当前就绪集中。
如果在此步骤开始时键集中的所有键都具有空集,则所选键集和任何键的就绪操作集都不会更新。
- 3 如果在执行步骤(2)时将任何键添加到取消键集中,则将按步骤(1)进行处理。
选择操作是否阻塞等待一个或多个通道准备就绪,如果等待了多长时间,则是这三种选择方法之间的唯一本质区别。
Concurrency
选择器本身可以安全地供多个并发线程使用。 但是,它们的密钥集不是。
选择操作按该顺序在选择器本身,键集和选定键集上同步。 它们还会在上面的步骤(1)和(3)期间同步取消键集。
在进行选择操作时,对选择器的键的兴趣集所做的更改对该操作没有影响; 他们将在下一个选择操作中看到。
可以随时取消键并关闭通道。 因此,一个选择器的一个或多个键集中的键的存在并不表示该键有效或其通道已打开。 如果其他线程有可能取消键或关闭通道,则应用程序代码应谨慎同步并在必要时检查这些条件。
在select()或select(long)方法之一中阻塞的线程可能会以其他三种方式之一被其他某个线程中断:
通过调用选择器的唤醒方法,
通过调用选择器的close方法,
- 通过调用被阻塞线程的中断方法,在这种情况下,将设置其中断状态,并调用选择器的唤醒方法。
close方法以与选择操作相同的顺序在选择器和所有三个键集上同步。
通常,选择器的键集和选定的键集不能安全地供多个并发线程使用。 如果此类线程可以直接修改这些集合之一,则应通过在集合本身上进行同步来控制访问。 这些集合的迭代器方法返回的迭代器是快速失败的:如果在创建迭代器之后修改集合,则可以通过调用迭代器自己的remove方法的任何方式来修改该集合,否则将抛出ConcurrentModificationException。