选择器与I/O多路复用
Selector选择器是NIO技术中的核心组件,可以将通道注册进选择器中,其主要作用就是使用1个线程来对多个通道中的已就绪通道进行选择,然后就可以对选择的通道进行数据处理,属于一对多的关系,也就是使用1个线程来操作多个通道,这种机制在NIO技术中称为“I/O 多路复用”。它的优势是可以节省CPU资源,因为只有1个线程,CPU不需要在不同的线程间进行上下文切换。线程的上下文切换是一个非常耗时的动作,减少切换对设计高性能服务器具有很重要的意义。
不适用I/O多路复用
使用I/O多路复用
使用了I/O多路复用后,只需要使用1个线程就可以操作多个通道,属于一对多的关系。它和“不使用I/O多路复用”相比最大的优势就是内存占用率下降.了,因为线程对象的数量大幅减少,还有CPU不需要过多的上下文切换,这对高并发高频段处理的业务环境有非常重要的优势。
线程数会随着通道的多少而动态地增减以进行适配,在内部其实并不永远是一个线程, 多路复用的核心目的就是使用最少的线程去操作更多的通道。在JDK的源代码中,创建线程的个数是根据通道的数量来决定的,每注册1023个通道就创建1个新的线程,这些线程执行Windows中的select(方法来监测系统socket的事件,如果发生事件则通知应用层中的main线程终止阻塞,继续向下运行,处理事件。可以在CMD中使用jps和jstack来查看创建线程的数量。
注意:
在使用I/O多路复用时,这个线程不是以for循环的方式来判断每个通道是否有数据要进行处理,而是以操作系统底层作为“通知器”,来“通知JVM中的线程”哪个通道中的数据需要进行处理,这点一定要注意。当不使用for循环的方式来进行判断,而是使用通知的方式时,这就大大提高了程序运行的效率,不会出现无限期的for循环迭代空运行了。
核心类Selector、SelectionKey和SelectableChannel的关系
在使用选择器技术时,主要由3个对象以合作的方式来实现线程选择某个通道进行业务处理,这3个对象.分别是Selector、SelectionKey 和SelectableChannel。
Selector:
Selctor类是抽象类,它是SelectableChannel对象的多路复用器。这句话的含义是只有SelectableChannel通道对象才能被Selector.java选择器所复用
,那么为什么必须是SelectableChannel通道对象才能被Selector.java 选择器所复用呢?因为只有SelectableChannel 类才具有register(Selector sel,int ops)
方法,该方法的作用是将当前的SelectableChannel 通道注册到指定的选择器中,参数sel也说明了这个问题。
由选择器来决定对哪个通道中的数据进行处理,这些能被选择器所处理的通道的父类就是SelectableChannel,它是抽象类.
SelectableChannel
SelectableChannel类和FileChannel类是平级关系,都继承自父类AbstractlnterruptibleChannel。抽象类SelectableChannel有很多子类,如图下图所示,其中的3个通道子类是在选择器技术中使用最为广泛的:
SelectionKey:
SelectionKey类的作用是一个标识,这个标识代表SelectableChannel类已经向Selector类注册了。
通道类AbstractInterruptibleChannel与接口InterruptibleChannel的介绍
AbstractInterruptibleChannel
类实现了InterruptibleChannel 接口,该接口的主要作用,是使通道能以异步的方式进行关闭与中断。
如果通道实现了asynchronously 和closeable特性,那么,当一个线程在一个能被中断的通道上出现了阻塞状态,其他线程调用这个通道的close()方法时,这个呈阻塞状态的线程将接收到AsynchronousCloseException异常。
如果通道实现了asynchronously和closeable,并且还实现了interruptible 特性,那么,当一个线程在一个能被中断的通道上出现了阻塞状态,其他线程调用这个阻塞线程的interrupt()方法时,通道将被关闭,这个阻塞的线程将接收到ClosedByInterruptException异常,这个阻塞线程的状态一直是中断状态。
AbstractInterruptibleChannel
类是抽象类,其内部的API结构比较简单,只有两个方法。
AbstractInterruptibleChannel类的主要作用是提供了一个可以被中断的通道基本实现类。
此类封装了能使通道实现异步关闭和中断所需要的最低级别的机制。在调用有可能无限期阻塞的I/O操作的之前和之后,通道类必须分别调用begin()和end()方法。为了确保始终能够调用end()方法,应该在try … finally块中使用begin和end()方法:
boolean completed = false;
try {
begin();
completed = ....;//执行blocking I/O操作
return ...;
} finally {
end(completed);
}
end()方法的completed参数用于告知I/O操作实际是否已完成。例如,在读取字节的操作中,只有确实将某些字节传输到目标缓冲区,此参数才应该为true,代表完成的结果是成功的。
具体的通道类还必须实现implCloseChannel()方法,其方式为:如果在调用此方法的同时,另一个线程阻塞在该通道上的本机I/O操作中,则该操作将立即返回,要么抛出异常,要么正常返回。如果某个线程被中断,或者异步地关闭了阻塞线程所处的通道,则该通道的end()方法会抛出相应的异常。
通道类SelectableChannel的介绍
从继承关系的结构信息来看,抽象类SelectableChannel并没有实现其他的新接口,只是单纯地从父类AbstractlnterruptibleChannel进行继承,进行扩展,它的API结构如图:
通过register方法,可将SelectableChannel注册到选择器对象上。
SelectableChannel类可以通过选择器实现多路复用。
在与选择器结合使用的时候,需要先调用SelectableChannel对象的register()
方法在选择器对象里注册SelectableChannel, register() 方法返回一个新的SelectionKey对象,SelectionKey表示该通道已向选择器注册了。
当SelectableChannel在选择器里注册后,通道在注销之前将一直保持注册状态。需要注意的是,不能直接注销通道,而是通过调用SelectionKey类的cancel()
方法显式地取消,这将在选择器的下一次选择select()操作期间去注销通道。无论是通过调用通道的close()方法,还是中断阻塞于该通道上I/O操作中的线程来关闭该通道,都会隐式地取消该通道的所有SelectionKey。
如果选择器本身已关闭,则将注销该通道,并且表示其注册的SelectionKey将立即无效。
一个通道至多只能在任意特定选择器上注册一次。可以通过调用isRegistered()
方法来确定是否已经向一个或多个选择器注册了某个通道。
SelectableChannel在多线程并发环境下是安全的。
SelectableChannel要么处于阻塞模式,要么处于非阻塞模式。
- 在阻塞模式中,每一个I/O操作完成之前都会阻塞在其通道上调用的其他I/O操作。
- 在非阻塞模式中,永远不会阻塞I/O操作,并且传输的字节可能少于请求的数量,或者可能根本不传输字节。可通过调用SelectableChannel的
isBlocking()
方法来确定其是否为阻塞模式。
新创建的SelectableChannel总是处于阻塞模式。
在结合使用基于选择器的多路复用时,非阻塞模式是最有用的。向选择器注册某个通道前,必须将该通道置于非阻塞模式,并且在注销之前可能无法返回到阻塞模式。
通道类AbstractSelectableChannel
从继承关系的结构信息来看,抽象类AbstractSelectableChannel并没有实现其他的新接口,只是单纯地从父类SelectableChannel进行继承扩展:
抽象类AbstractSelectableChannel是可选择通道的基本实现类。此类定义了处理通道注册、注销和关闭机制的各种方法。它会维持此通道的当前阻塞模式及其当前的选择键集SelectionKey。它执行实现SelectableChannel规范所需的所有同步。此类中所定义的抽象保护方法的实现不必与同一操作中使用的其他线程同步。
通道类ServerSocketChannel与接口NetworkChannel
ServerSocketChannel类是针对面向流的侦听套接字的可选择通道。
ServerSocketChannel不是侦听网络套接字的完整抽象,必须通过调用socket()方法所获得的关联ServerSocket对象来完成对套接字选项的绑定和操作。不可能为任意的已有ServerSocket创建通道,也不可能指定与ServerSocketChannel关联的ServerSocket所使用的SocketImpl对象。
通过调用此类的open()方法创建ServerSocketChannel。新创建的ServerSocketChannel已打开,但尚未绑定。试图调用未绑定的ServerSocketChannel的accept()方法会导致抛出NotYetBoundException。可通过调用相关ServerSocket的某个bind()方法来绑定ServerSocketChannel。
多个并发线程可安全地使用服务器套接字通道ServerSocketChannel。
实现此接口的通道就是网络套接字通道。bind()方法用于将套接字绑定到本地地址,getLocalAddress()方法返回套接字绑定到的地址,setOption()和getOption()方法分别用于设置和查询套接字选项。
ServerSocketChannel类可供调用的API如下: