首页 > 代码库 > Java NIO读书笔记
Java NIO读书笔记
简介
NIO的作用就是改进程序的性能。因为有时候程序的性能瓶颈不再是CPU,而是IO。这时候NIO就派上用场了。NIO的原理就是尽量利用系统底层的资源来提高效率,比如利用DMA硬件减小CPU负荷,利用操作系统的epoll机制避免线程频繁切换。通过底层资源提高系统的吞吐量。缓冲区
缓冲区就是一个固定大小的一组数据。缓冲区有四个非常重要的属性:容量,限制,位置,标记。容量就是一个缓冲区最大能容量的元素数量,限制就是对容量进行逻辑上的限制,位置用于跟踪get或者put方法的位置,标记用于reset函数返回上次固定的位置。put()方法用于往缓冲区中存入数据,get()方法用于从缓冲区中读取数据。写入操作具体的API有put(byte)、put(index, byte)、put(byte[])、put(byte[], int start, int length),有单个元素的写入,也有批量的写入。读取操作也一样拥有这四种API。
flip()用于交换未写入的和写入的数据。也就是将limit设为position,将position设为0。一般先存入一组数据之后,经过翻转,再从中读取原本写入的数据。
compact()将已经读过的数据进行压缩,将未读过的数据复制到缓冲区索引号为0的位置。复制之后,原来的数据不会被擦除。
mark()方法用于标记,reset()方法用于返回上次标记的位置。rewind()、clear()、flip()都会重置标记,position()、limit()、看情况,如果小于标记时也会重置标记。
缓冲区之间可以比较。比较一定要相同的类型。比较的依据是缓冲区剩余的内容,与标记、位置、容量、限制等无关。
创建缓冲区可以有两种方法。一种是创建新的缓冲区,调用xxBuffer.allocate,第二是将现有的数组进行封装,缓冲区写入的数据都会写入到原来的数组中。
缓冲区是可以复制的。调用duplicate()。复制出来的缓冲区其实是一个视图。复制出来的缓冲区和原来的缓冲区拥有相同的数据,但是每个缓冲区都有各自的属性,限制、位置、标记都是独立的。复制的时候也可以取缓冲区的一部分,调用slice()。
缓冲区还分为big-endian和little-endian。java.nio.ByteOrder可以获取本机的字节顺序。
还有一种缓冲区称之为直接缓冲区,可以通过xxBuffer.allocateDirect获取。直接缓冲区就是操作性能比普通的缓冲区要高。
ByteBuffer提供了asXXBuffer。比如asShortBuffer、asCharBuffer等。这些缓冲区称之为视图缓冲区。就是将字节缓冲区以另外一种行为提供给其他程序。ByteBuffer还提供了getInt、getLong、getDouble等方法,这些方法称之为视图操作,好像就在操作另外一种类型的缓冲区。写入操作也是一样,也有视图操作。视图缓冲区和视图操作和字节顺序有关,所以在操作之前先设置字节顺序,默认的是BigEndian。
Java不支持无符号的数据类型。但是总是有解决办法的。下面就是一种解决办法。
package com.ronsoft.books.nio.buffers; import java.nio.ByteBuffer; /** * Utility class to get and put unsigned values to a ByteBuffer object. * All methods here are static and take a ByteBuffer argument. * Since java does not provide unsigned primitive types, each unsigned * value read from the buffer is promoted up to the next bigger primitive * data type. getUnsignedByte() returns a short, getUnsignedShort() returns * an int and getUnsignedInt() returns a long. There is no getUnsignedLong() * since there is no primitive type to hold the value returned. If needed, * methods returning BigInteger could be implemented. * Likewise, the put methods take a value larger than the type they will * be assigning. putUnsignedByte takes a short argument, etc. * * @author Ron Hitchens (ron@ronsoft.com) */ public class Unsigned { public static short getUnsignedByte (ByteBuffer bb) { return ((short)(bb.get() & 0xff)); } public static void putUnsignedByte (ByteBuffer bb, int value) { bb.put ((byte)(value & 0xff)); } public static short getUnsignedByte (ByteBuffer bb, int position) { return ((short)(bb.get (position) & (short)0xff)); } public static void putUnsignedByte (ByteBuffer bb, int position, int value) { bb.put (position, (byte)(value & 0xff)); } // --------------------------------------------------------------- public static int getUnsignedShort (ByteBuffer bb) { return (bb.getShort() & 0xffff); } public static void putUnsignedShort (ByteBuffer bb, int value) { bb.putShort ((short)(value & 0xffff)); } public static int getUnsignedShort (ByteBuffer bb, int position) { return (bb.getShort (position) & 0xffff); } public static void putUnsignedShort (ByteBuffer bb, int position, int value) { bb.putShort (position, (short)(value & 0xffff)); } // --------------------------------------------------------------- public static long getUnsignedInt (ByteBuffer bb) { return ((long)bb.getInt() & 0xffffffffL); } public static void putUnsignedInt (ByteBuffer bb, long value) { bb.putInt ((int)(value & 0xffffffffL)); } public static long getUnsignedInt (ByteBuffer bb, int position) { return ((long)bb.getInt (position) & 0xffffffffL); } public static void putUnsignedInt (ByteBuffer bb, int position, long value) { bb.putInt (position, (int)(value & 0xffffffffL)); } }
最后还有一种映射缓冲区,这种缓冲区一定是直接缓冲区,只能由FileChannel创建。
通道
通道和缓冲区不同,每个操作系统都有不同的实现方式,因此通道的代码一般都是接口或者抽象类。通道分为阻塞通道和非阻塞通道。非阻塞通道不能在文件通道上使用。
通道类似于一种连接,所以通道是不能循环使用的。通道可以被关闭。关闭可以通过close方法和中断,对通道发送中断信号通道就会关闭。这种设计初看觉得很别扭,但是这样设计是为了便于在不同的操作系统中实现。
通道还支持批量写入或读取多个缓冲区。一般的操作系统都从底层支持批量写入或读取缓冲区,因此Java会将批量操作翻译成系统底层的API调用,让操作系统来完成批量操作,因此速度非常快。
文件通道只能是阻塞通道。比起FileStream,FileChannel还提供了更多的操作,比如指定在某个位置写入数据。文件通道的创建需要FileStream或者RandomAccessFile,文件通道的状态和创建时传入的参数状态是保持一致的,文件的位置是同步的。文件通道还提供了force操作,将文件的修改立即写入文件。文件通道提供了truncate操作,用于设置文件的大小。
文件系统中有个文件洞(File Hole)的概念,就是文件的大小比占用的空间少。比如在文件的1GB位置写入10K数据,那么文件实际善用的空间是10K,而不是1G。
文件锁一个常见的误区是,每个文件只能有一个文件锁,不是每个文件通道对象有一个文件锁,也不是每个线程有一个文件锁,而是每个文件有一个文件锁。因此,在同一个JVM中,如果对一个文件创建了两个文件通道,在同一个地方都加上互斥锁,是不会阻塞的。也就是说,在JVM内部,文件锁是不起作用的。文件锁要记得释放,最好就是将释放的代码放在finally块中。
文件映射缓冲区。这种缓冲区和普通的缓冲区一样,但是数据的内容是放在磁盘上的。映射缓冲区有三种模式,一种是只读,一种是读写,一种是私有。私有模式下,文件的修改是不会写入到文件的,只是保存到缓冲区中。私有模式下,文件的内容会与其他普通的文件通道同步。但是同步的单位是分页,也就是说,私有模式下是否同步跟操作系统的分页大小有关。如果在私有模式下修改文件,那么对应的分页将不再和其他文件通道同步。
通道之间还可以直接传输,相关的方法是transferTo和transferFrom。有些操作系统内核就支持通道之间的传输,因此性能非常高。
文件映射的load()方法可以将整个文件加载到操作系统的文件缓存中,同时文件的内容和磁盘保持同步。
套接字通道和文件通道不同,支持非阻塞模式。每个套接字通道对应了一个套接字。这种通道不能从现有的套接字中创建。
blockingLock()方法会返回一个Object对象,可以用Java中的synchronized关键字对这个对象进行锁定,防止其他的线程对该对象进行修改。套接字通道分为SocketChannel和ServerSocketChannel。ServerSocketChannel只是提供了非阻塞的accept方法。
数据报通道使用UDP协议进行通信。注意,在接收数据的时候,如果缓冲区的容量不够了,那么多出的数据会被\textbf{丢弃},不会有任何现象。发送数据的时候,如果缓冲区太大,超过了系统的发送队列,那么不会有任何数据会被发送。数据报通道也有connect方法,该方法只是指定发送对象,并不是真正的连接。
管道通道(PipeChannel)和Unix中的管道通信不是同一个概念。NIO中的管道通道只能在一个JVM内部进行通信,而不是进程间的通信。进程间通信可以通过套接字。管道通信在创建的时候通过Pipe.open()即可创建一对通道,SinkChannel和SourceChannel。SinkChannel用于写入,SourceChannel用于读取。通过管道可以实现一个线程只顾写入数据,另外一个线程只顾读取数据,有点类似于Python中的generator对象。管道通道最大的用处就是封装。将一个文件通道或者套接字通道封装成管道通道,提高代码的复用程度。经过实验,发现管道内部存在缓冲,就算另外一边没有读取,写入的一边也可以写入大于1K的数据。
选择器
选择器的具体实现只能是通过操作系统来完成,因此性能比较高。有关选择器部分的有三个类,Selector、SelectionKey、SelectableChannel。
Selector用于管理多个可选通道和一堆SelectionKey。select方法会阻塞,返回的不是已经就绪的通道数量,而是在这次调用中成为就绪状态的通道数量。selectedKeys()返回的其实是一个Set,而Set不支持多线程,所以如果selectedKeys放在另外的线程迭代,那么在迭代的过程中可能会产生ConcurrentModificationException。
Selector中有三种集合:注册集合、选择集合、取消集合。选择集合只会增加不会减少,减少需要通过迭代器手动删除,每处理一次请求就删除对应的SelectionKey。
选择模式有两种一种是select另外一种是epoll。Select是POSIX标准,而Epoll是Linux特有的。Select最多只能监听1024个通道,而Epoll则没有这种限制。Select每次调用时会扫描所有的通道,因此通道越多性能越差,而Epoll中有一个可用队列,这个队列由操作系统内核来维护的,当一个通道可用时,操作系统就会往队列中增加通道,因此性能不会随着通道数量的增加而变差。
Epoll有两种工作模式,一种是Level Trigger水平触发,另一种是Edge Trigger边缘触发。默认是水平触发,这种模式当通道的数据还没有读取完时,下一次选择之后selectionKeys会马上返回没有读完的通道,而边缘触发则不会,边缘触发的性能更高但是程序出错的可能性更大。
SelectionKey就是通道和选择器的对应关系。提供了readyOps()方法,这个方法返回通道已经就绪的操作。也可以通过isWritable()、isReadable()等方法判断通道是否支持某个操作,这两种方法是等价的。选择键还可以带上一个附件,便于通道获取参数。需要注意的是,附件如果不再使用,应该马上清除掉,否则会造成内存泄露。
SelectableChannel就是可选通道,它可以在多个Selector中注册,注册的时候要提供需要监听的事件,比如OP\_READ、OP\_WRITE。validOps()方法返回这个通道能够监听的操作。JDK中定义了4种兴趣:读、写、连接、接受。SocketChannel是不能接受连接的,所以validOps不会返回接受动作。注册通道可以重复注册,但是第二次注册时只会修改兴趣集,并返回同一个SelectionKey。如果第二次注册的时候已经调用了cancel()方法,然而Selector还没有来得及更新,就会发生CancelledKeyException。
关闭通道应该是一个非常快速的操作,没有任何阻塞。这是JavaNIO的设计目标。这样的设计称为异步关闭。
一般编写代码的时候模板如下:
while(true) { selector.select(); Iterator<SelectionKey> keys = selector.selectedKeys(); while(keys.hasNext()) { SelectionKey key = keys.next(); // 处理事件 ... // 处理完毕之后删除,这样就表示 这次事件已经处理过了 keys.remove(); } }
对于多核的计算机而言,只有一个线程在工作是非常低效的,为了在多核计算机上提升性能,必须引入多核线程和多个选择器。每个线程一个选择器,每次接受连接的时候随机分配给一个线程。这是一种方法,另外一种方法是其中一个线程用于接受连接,其余的线程专门负责处理业务。
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。