Java NIO

在整理NIO文档前先介绍下5个I/O模型。在将I/O模型之前还得介绍几个概念:

I/O操作流程

  • 内核空间和用户空间

数据从磁盘移动到用户进程的内存区域时,要涉及到内核空间和用户空间。
用户空间就是常规进程(如JVM)所在区域,_用户空间_是非特权区域,如不能直接访问硬件设备内核空间操作系统所在区域,那肯定是有特权啦,如能与设备控制器通讯,控制用户区域的进程运行状态。进程执行I/O操作时,它执行一个系统调用把控制权交由内核。

  • 缓冲区操作

缓冲区操作是所有I/O的基础,进程执行I/O操作,归结起来就是向操作系统发出请求,让它要么把缓冲区里的数据排干(写),要么把缓冲区填满(读)

5种I/O模型

IO请求分为两个阶段:1、等待数据就绪。2、从内核缓冲区拷贝数据到进程缓冲区
按照请求是否阻塞分为:同步IO和异步IO。
Unix存在5种IO模型:
(1)阻塞IO:最常用的模型,缺省情况文件操作都是阻塞的。
阻塞I/O
(2)非阻塞IO:用户需要不断主动询问kernel数据是否准备好了,准备好之后通知用户将数据从内核空间copy到用户空间(用户得到通知之后,亲自去内核空间中将数据复制到用户空间)。
这里的非阻塞是指等待数据准备好的这段时间不会阻塞,需要说明的是等待就绪的阻塞是不使用CPU的,是在空等而真正的读写操作的阻塞是使用CPU的,真正在”干活”,而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。。
非阻塞I/O
(3)IO复用:也叫作event driven IO
I/O复用
(4)信号驱动IO:内核通知我们何时可以开始一个I/O操作
(5)异步IO:内核通知我们I/O操作何时已经完成
异步I/O

疑问–Java NIO对应哪种I/O模型?

wiki上说”Non-blocking I/O (usually called NIO, and sometimes called “New I/O”)”,而又有很多blog说NIO就是IO复用,经常说NIO的select是IO复用,在I/O模型中非阻塞IO和IO复用是两种不同的模型,这个是怎么回事???Java NIO与Non-blocking I/O、I/O复用的关系是什么????是一种同步非阻塞的I/O模型,也是I/O多路复用的基础

NIO的优势

NIO较于传统IO的优势在于NIO是基于的,而且有通道(Channel)和缓冲区(buffer)的概念,更加接近操作系统执行IO的方式。

优势如下:

  • 事件驱动模型(Reactor设计模式–Reactor是并发编程中的一种基于事件驱动的设计模式。)
  • 避免多线程(Reactor模式中_单线程轮询_来进行事件分发)
  • 非阻塞I/O,I/O读写不再阻塞,而是返回0
  • 基于block的传输,通常比基于流的传输更高效
  • 更高级的IO函数,zero-copy
  • IO多路复用大大提高了Java网络应用的可伸缩性和实用性

概念

  • 缓冲区

在缓冲区中,最重要的属性有下面三个,它们一起合作完成对缓冲区内部状态的变化跟踪:
position:指定了下一个将要被写入或者读取的元素索引,它的值由get()/put()方法自动更新,在新创建一个Buffer对象时,position被初始化为0。
limit:指定当前buffer中读取数据时的末尾index 例如当前buffer的capacity是10,但只写入内容只占到4,则此时读取buffer时,limit为4,读取的内容到limit为止,也可以说limit记录了当前buffer已用的容量。也就是说指定当前buffer有多少数据需要取出(在从缓冲区写入通道时),或者有多少空间可以放入数据(在从通道读入缓冲区时)。
capacity:指定了可以存储在缓冲区中的最大数据容量,实际上,它指定了底层数组的大小,或者至少是指定了准许我们使用的底层数组的容量。

读取buffer中的数据时,调用flip(),此方法将会完成两件事情:
1、把limit设置为当前的position值
2、把position设置为0

以上三个属性值之间相对大小的关系:0 <= position <= limit <= capacity。如果我们创建一个新的容量大小为10的ByteBuffer对象,在初始化的时候(调用clear()方法将buffer重置为初始化),position设置为0,limit和 capacity被设置为10,在以后使用ByteBuffer对象过程中,capacity的值不会再发生变化,而其它两个个将会随着使用而变化。

  • 通道

通道主要用来传输数据,与缓冲区进行交互,与Selector关联起来使用的,实际上是通过SelectableChannel中的register()方法进行注册的(如果在阻塞模式下注册一个通道,系统会抛出IllegalBlockingModeException异常。 )register()方法返回一个SelectionKeySelectionKey是Channel和Selector之间的关联对象,该对象可以通过channel()selector()方法得到该SelectionKey关联的channel和selector。SelectionKey维护着两个Set集合,一个是感兴趣的事件集合interestOps(),另一个是准备好了的,可以进行IO操作的集合readyOps()。SelectionKey类中定义了4种可选择操作:read、write、 connect和accept,每个具体通道有不同的有效的可选择操作集 合,比如ServerSocketChannel的有效操作集合是accept,而SocketChannel的有效操作集合是read、write和 connect。

  • 选择器

选择器可谓NIO中的重头戏,I/O复用的核心,其中维护着两个键的集合:已注册的键的集合keys()已选择的键的集合selectedKeys(),已选择的键的集合是已注册的键的集合的子集。除此之外,其实选择器内部还维护着一个已取消的键的集合,这个集合包含了cancel()方法被调用过的键。

当Selector和特定的SelectableChannel关联好后就开始工作了,工作时只需要调用select()方法。select()阻塞调用线程,直到有某个Channel的某个感兴趣的Op准备好了。select()只返回本次执行select()时从_未准备好到准备好状态的channel数(在执行select()之前已经准备好的channel不进行计数)_,如果不为0,将调用selectedKeys()方法得到一个已选择的键的集合set。

代码

下面是一个服务器端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
int port = 30;

// ServerSocketChannel没有bind()方法,
//因此需要取出对等的Socket对象并使用它来绑定到某一端口以开始监听连接。
ServerSocketChannel serverChannel = ServerSocketChannel.open( );
ServerSocket serverSocket = serverChannel.socket( );
serverSocket.bind (new InetSocketAddress (port));

//通常Selector是由静态工厂方法open()实例化的
Selector selector = Selector.open( );

//如果在阻塞模式下注册一个通道,系统会抛出IllegalBlockingModeException异常。
serverChannel.configureBlocking (false);
serverChannel.register (selector, SelectionKey.OP_ACCEPT);

// Dispatch Loop
while (true) {

//select() 阻塞调用线程,直到有某个Channel的某个感兴趣的Op准备好了
//返回本次执行select时从未准备好到准备好状态的channel数,
//不为0,则调用selector.selectedKeys()
int n = selector.select( );
if (n == 0) {
continue; // nothing to do
}
Iterator it = selector.selectedKeys().iterator( );
while (it.hasNext( )) {
//得到SelectionKey
SelectionKey key = (SelectionKey) it.next( );
if (key.isAcceptable( )) {
//通过SelectionKey的channel()得到该感兴趣事件的channel
ServerSocketChannel server =
(ServerSocketChannel) key.channel( );
//在非阻塞模式下,当没有传入连接在等待时,
//其accept()方法会立即返回null。
SocketChannel channel = server.accept( );
if (channel == null) {
//handle code, could happen
}
channel.configureBlocking (false);
channel.register (selector, SelectionKey.OP_READ);
}
if (key.isReadable( )) {
readDataFromSocket (key);
}
//这行代码是必要的,将该key放入cancel集合中,在下次select()时删除
it.remove( );
}
}

参考

https://yq.aliyun.com/articles/2371
http://www.molotang.com/articles/906.html
http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf