关于NIO这部分,除了《Java编程思想》中的介绍还有两份资料我觉得很好:一是《深入Java Web技术内幕》第2章的部分,二是并发编程网上Jakob JenkovNIO系列教程翻译,读完之后受益匪浅。
非阻塞IO是NIO的一大特点,那它是怎么实现的呢,Selector和它的名字所反映的一样起到选择和调度的作用,它所调度的就是Channel(通道),Channel包括:FileChannel,SocketChannel,ServerSocketChannel,DatagramChannel,它们负责对指定的资源进行访问读写,其中除了FileChannel都具有非阻塞的功能,ServerSocketChannel相当于一个服务器程序,它有accept方法监听和接受客户端的请求,而SocketChannel则对应于一个具体的Socket连接,它可以通过ServerSocketChannel的accept方法来得到,我们可以通过read和write对该连接信道进行读写,DatagramChannel是UDP数据报通信方式。在传统的IO中,accept,read,write方法都是阻塞的方式进行的,也就是说accept方法负责接受一个客户端请求,在未接受到一个请求之前线程就会阻塞在此处不能进行,因此要同时打开多个ServerSocketChannel必须要有多个线程支持,在非阻塞模式下,accept,read,write可以在直接返回,让其他任务可以执行,这样我们可以在一个线程中同时处理多个Channel,那你可能会问,accept直接返回了,请求来的时候如何在去调用accept接受呢,这就需要Selector来调度了,Selector的select()方法是阻塞的,它可以同时监听对Channel的操作请求,将请求转发到对应的channel中,从总体上看,把每个Channel中阻塞等待的行为统一移到了Selector,从而我们可以在单线程中同时处理多个信道的读写任务。Buffer缓存则是我们对Channel进行读写的工具,它还提供了Char,Int等多种不同的视图让我们可以以不同的方式读写数据,也提供了Heap和Direct直接内存两种缓存存储方式。
下面我们对这3个关键的“部件”进行详细的分析,当然我们应当明白不同的技术有不同的使用场景,这里为了突出NIO的特点我们集中于单线程(或少量线程)非阻塞的方式,它使用与高并发数据量处理少而简单的场景。
结合上面的论述,可以看到Selector起到了代替多个Channel监听感兴趣事件发生的作用,这让我很容易想起一个设计模式——观察者模式,在这里Selector是Obserable,Channel是Observer,Channel要向Selector注册自己对哪些事情感兴趣,当事件发生时,Selector通知对应的Channel。
这里注册有个两个部分:哪个channel和指定的事件,SelectionKey包含了注册的要素:
操作事件:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
因为SelectionKey包含5个部分:
(1)interest集合和ready集合一样包含有一些方便判断的方法,可以看api或源码;
(2)ready集合;
(3)channel引用;
(4)Selector引用;
(5)attach附加对象(可选);
(1)select方法:包括select(),selectNow(非阻塞),select(long timeout)返回int,有多个个ready的Channel;
(2)selectedKeys方法:返回ready的Channel的selectionKey集合,遍历它们,根据readyOps集合处理对应事件;
(3)wakeUp方法:从select阻塞中唤醒;
(4)close方法:是所用selectionKey无效,也就释放了对Channel们的引用不影响垃圾回收啦;
public class SelectorSample { private List<SelectableChannel> channels; private boolean isListening = true; public SelectorSample(List<SelectableChannel> channels) { this.channels = channels; } public void doHandle() { try(Selector selector = Selector.open()) { for(SelectableChannel channel : channels) { channel.configureBlocking(false); //非阻塞 channel.register(selector, SelectionKey.OP_ACCEPT | SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE); } while(isListening) { int ready = selector.select(); if(ready == 0) continue; Set<SelectionKey> selectionKeys = selector.keys(); for(Iterator<SelectionKey> iterator = selectionKeys.iterator(); iterator.hasNext();) { SelectionKey key = iterator.next(); if(key.isAcceptable()) { System.out.println("doSomething when be acceptable"); } else if(key.isConnectable()) { System.out.println("doSomething when be able to connect"); } else if(key.isReadable()) { System.out.println("doSomething when be readable"); } else if(key.isWritable()) { System.out.println("doSomething when be writable"); } iterator.remove(); //注意要从就绪集合中删除,下次就绪有selector添加 } } } catch (IOException e) { e.printStackTrace(); } } }
Channel的体系中有:SelectableChannel和InterruptiableChannel,前者继承自后者,之前说过FileChannel是不可以非阻塞的它属于InterruptiableChannel,而其他3种进一步属于SelectableChannel。
线程安全;
public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel它的主要功能就是监听的某个地址和端口上的套接字请求,并打开SocketChannel;
public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannelByteChannel实现了WritableChannel和ReadableChannel,因此它是可读写的;
Scatter/Gatther分别实现了将一个channel的内容读到多个buffer(一个Buffer满了才能读到下一个)和多个Buffer写到一个Channel的功能;
NetworkChannel:绑定到地址/端口的能力;
我们可以通过它来进行一个端到端的,有连接的套接字通信;
线程安全的;
public abstract class DatagramChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel与SocketChannel接口上的不同在与MulticastChannel,它是NetworkChannel子类;增加了多播的功能,使得我们可以使用基于UDP套接字的多播功能;
管道一般可以在两个线程中进行单向的数据传输,它有两个嵌套类:SinkChannel和SourceChannel,分别负责在一个线程(发送者)写入和在另一个线程中读取:
public static abstract class SinkChannel extends AbstractSelectableChannel implements WritableByteChannel, GatheringByteChannel
public static abstract class SourceChannel extends AbstractSelectableChannel implements ReadableByteChannel, ScatteringByteChannel基于上面的论述,相信已经可以很清除的明白它们各自有什么功能了:都可以非阻塞,一个负责写,并且可以将多个Buffer一起写入,一个负责读,可以从Channel中将数据读入多个Buffer;
这个Channel是阻塞的,是操作磁盘文件一种方式,它和之前几个Channel大不相同,所以我要将它和下面要介绍的DirectByteBuffer(MappedByteBuffer)一起讨论。
总的来说,具体工作中使用Buffer的次数要远远多与Selector和Channel,我们通过它对Channel进行具体的读写操作。
之前说过NIO的特点之一就是面向缓存,我们在使用Buffer时都是基于一块分配指定的大小的固定内存进行操作的,只有两种分配方式:Heap和Direct,它们的区别下面会详细说明。无论我们进行视图转换(CharBuffer/IntBuffer等等),还是compact压缩,还是duplicate复制、slice切片,都是最初的allocate分配那一块内存。
capacity:容量;
limit:可操作的限制位置;
position:下一个操作位置;
mark:标记;
address:使用direct内存时的内存地址;
capacity,limit,position这3个属性是我们进行操作最关键和常用的,结合操作方法我们来看看它们的使用细节:
flip:limit=position,position=0;这个方法通常在从通道中读取数据后使用,这样我们可以再从Buffer中读取数据;或者在Buffer写入数据后调用,让通道写入;
rewind:position=0,mark=-1;
clear:position=0,limit=capacity,mark=-1;通常在重新从
remaining:limit-position;用于检查是否还有数据;
mark和reset:mark标记,mark=position;reset复位,position=mark(mark>0时),注意它并不会修改mark值;
compact:将原来(limit-position)未处理完的数据复制到开头,再将position移到数据的下一个位置,limit=capacity,这个方法是进行压缩,去掉已处理过的数据,主要 为了接下来将数据写入Buffer,注意,它只是在原数组上进行复制的,没有新分配空间;
另一个你可能需要注意的是equals方法和compareTo方法,它们比较的是limit-position之间的大小;
来看看Buffer的体系:
Channel都是基于字节的,我们一般也从ByteBuffer开始;
ByteBuffer有两个分配方法(它们返回HeapByteBuffer和DirectByteBuffer都是default包权限,我们无法直接使用它们):
allocate:HeapByteBuffer,从JVM堆中分配,收到JVM垃圾回收处理机制管理,实际上就是为了一个固定的byte[];
allocateDirect:DirectByteBuffer,使用JNI在native内存中分配,那怎么回收直接内存呢,DirectBuffer(DirectByteBuffer的接口),可以返回Cleaner,通过它我们可以释放直接内存,否则你就只能等待Full GC的发生来释放它了;
在类图中我们可以看到有CharBuffer,IntBuffer,另外还有FloatBuffer,DoubleBuffer,ShortBuffer,LongBuffer以及MappedBuffer(特别的,内存映射);
它们实际上都是由ByteBuffer而产生,操作同一块内存,只是读取的方式不一样,以HeapByteBuffer和CharBuffer为例我们来看看它们是怎么完成“视图”的使命的。
转换方法:
ByteBuffer.asCharBuffer();
HeapByteBuffer中是这样实现的:
public CharBuffer asCharBuffer() { int size = this.remaining() >> 1; int off = offset + position(); return (bigEndian ? (CharBuffer)(new ByteBufferAsCharBufferB(this, -1, 0, size, size, off)) : (CharBuffer)(new ByteBufferAsCharBufferL(this, -1, 0, size, size, off))); }考虑到字顺这里有两个实现,新建一个ByteBufferAsCharBufferB还是操作那一块内存,只是我们换了一组capacity,limit,mark,position来操作;
而在具体的get/put方法中:
public char get() { return Bits.getCharB(bb, ix(nextGetIndex())); }是通过Bits这个工具类来进行不同基本类型的读取和操作。 整个事情就是这样,基于字节ByteBuffer,考虑字顺用不同的方式去读写同一块内存;
PS:对于CharBuffer和ByteBuffer之间的转换,涉及到编解码,Charset有ByteBuffer = encode(CharBuffer)和CharBuffer = decode(ByteBuffer);
public final FileChannel getChannel() { synchronized (this) { if (channel == null) { channel = FileChannelImpl.open(fd, path, true, rw, this); } return channel; } }FileOutputStream:
public FileChannel getChannel() { synchronized (this) { if (channel == null) { channel = FileChannelImpl.open(fd, path, false, true, append, this); } return channel; } }FileInputStream:
public FileChannel getChannel() { synchronized (this) { if (channel == null) { channel = FileChannelImpl.open(fd, path, true, false, this); } return channel; } }正如RandomAccessFile(可以seek方式前后读写文件),FileInputStream和FileOutputStream本身的差异一样,它们也具有不同的特点;
static MappedByteBuffer newMappedByteBuffer(int var0, long var1, FileDescriptor var3, Runnable var4) { if(directByteBufferConstructor == null) { initDBBConstructor(); } try { MappedByteBuffer var5 = (MappedByteBuffer)directByteBufferConstructor.newInstance(new Object[]{new Integer(var0), new Long(var1), var3, var4}); return var5; } catch (IllegalAccessException | InvocationTargetException | InstantiationException var7) { throw new InternalError(var7); } }这里可以看到显然是通过直接内存的方式,但是这段代码是在FileChannel的map方法中调用的,它和allocateDirect是有区别的;要理解直接内存和内存映射中的原理需要一些重要的基础知识,才能真正弄清楚HeapByteBuffer,DirectByteBuffer,和map之间的区别。
public class DirectMemoryTest { private static final String TEST_FILE = "/home/yjh/test.file"; public static void testNormal(String TEST_FILE) { System.out.print("Normal "); try(FileInputStream inputStream = new FileInputStream(TEST_FILE); FileChannel fileChannel = inputStream.getChannel()) { ByteBuffer buffer = ByteBuffer.allocate(1024); while(fileChannel.read(buffer) != -1) { buffer.flip(); buffer.clear(); } } catch (IOException e) { e.printStackTrace(); } } public static void testDirect(String TEST_FILE) { System.out.print("Direct "); try(FileInputStream inputStream = new FileInputStream(TEST_FILE); FileChannel fileChannel = inputStream.getChannel()) { ByteBuffer buffer = ByteBuffer.allocateDirect(1024); while(fileChannel.read(buffer) != -1) { buffer.flip(); buffer.clear(); } } catch (IOException e) { e.printStackTrace(); } } public static void testMapped(String TEST_FILE) { System.out.print("Mapped "); try(FileInputStream inputStream = new FileInputStream(TEST_FILE); FileChannel fileChannel = inputStream.getChannel()) { MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); byte[] b = new byte[1024]; while(buffer.get(b).position() < buffer.limit()) { } } catch (IOException e) { e.printStackTrace(); } } public static void test(Consumer<String> consumer) { long startTime = System.currentTimeMillis(); consumer.accept(TEST_FILE); long endTime = System.currentTimeMillis(); System.out.println("Time consume: " + (endTime - startTime)); } public static void main(String[] args) { test(DirectMemoryTest::testNormal); test(DirectMemoryTest::testDirect); test(DirectMemoryTest::testMapped); } }我对三种方式分别读取整个test文件的内容,该文件大小为500M,运行结果(单位:ms):