常见的IO模型:
1、同步阻塞IO(Blocking IO): 在Java中,默认创建的socket都是阻塞。同步IO,是一种用户空间与内核空间的IO发起方式。同步IO是指用户空间的线程是主动发起IO请求的一方,内核空间是被动接受方。异步IO则反过来,是指系统内核是主动发起IO请求的一方,用户空间的线程是被动接受。
- 阻塞IO的特点是:在内核进行IO执行的两个阶段,用户线程都被阻塞。
- 阻塞IO的优点:应用的程序开发非常简单;在阻塞等待数据期间,用户线程挂起。在阻塞期间,用户线程基本不会占用CPU。
- 阻塞IO的缺点:一般情况下,会为每个连接配备一个独立的线程;在高并发的应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。因此,基本上阻塞IO模型在高并发应用场景下是不可。
2、同步非阻塞IO(Non-blocking IO): 非阻塞IO要求socket被设置为NONBLOCK。使用非阻塞模式的IO读写,叫作同步非阻塞IO(None Blocking IO),简称为NIO,在NIO模型中,应用程序一旦开始IO系统调用,会出现以下两种情况:
(1)在内核缓冲区中没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息。
(2)在内核缓冲区中有数据的情况下,是阻塞的,直到数据从内核缓冲复制到用户进程缓冲。复制完成后,系统调用返回成功,应用进程开始处理用户空间的缓存。
- 同步非阻塞IO的特点:应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用。
- 同步非阻塞IO的优点:每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。
- 同步非阻塞IO的缺点:不断地轮询内核,这将占用大量的CPU时间,效率低下。
3、IO多路复用(IO Multiplexing):IO多路复用能够避免同步非阻塞IO模型中轮询等待的问题。经典的Reactor反应器设计模式,有时也称为异步阻塞IO,Java中的Selector选择器和Linux中的epoll都是这种模型。在Linux系统中,对应的系统调用为select/epoll系统调用。通过该系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核能够将就绪的状态返回给应用程序。随后,应用程序根据就绪的状态,进行相应的IO系统。在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程,可以不断地轮询成百上千的socket连接,当某个或者某些socket网络连接有IO就绪的状态,就返回对应的可以执行的读写操作。
- IO多路复用模型的特点:IO多路复用模型的IO涉及两种系统调用(System Call),一种是select/epoll(就绪查询),一种是IO操作。IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll。和NIO模型相似,多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,查找出达到IO操作就绪的socket连接。对于注册在选择器上的每一个可以查询的socket连接,一般都设置成为同步非阻塞模型。
- IO多路复用模型的优点:与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接(Connection)。系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。Java语言的NIO(New IO)技术,使用的就是IO多路复用模型。在Linux系统上,使用的是epoll系统调用。
- IO多路复用模型的缺点:本质上,select/epoll系统调用是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个读写过程是阻塞的。
4、异步IO(Asynchronous IO,简称为AIO): 指的是用户空间与内核空间的调用方式反过来。用户空间的线程变成被动接受者,而内核空间成了主动调用者。
- 基本流程:用户线程通过系统调用,向内核注册某个IO操作。内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。
- 异步IO模型的特点:在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。正因为如此,异步IO有的时候也被称为信号驱动IO。
异步IO异步模型的缺点:应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供。
5、Java NIO由以下三个核心组件组成:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。
(1)Buffer类:
- Buffer类是一个非线程安全。MappedByteBuffer是专门用于内存映射的一种ByteBuffer类型。
- Buffer类在其内部,有一个byte[]数组内存块,作为内存缓冲区。为了记录读写的状态和位置,Buffer类提供了一些重要的属性。其中,有四个重要的成员属性:capacity(容量)、position(读写位置)、limit(读写的限制)、mark(标记)。
- Buffer类的capacity属性,表示内部容量的大小;Buffer类的capacity属性一旦初始化,就不能再改变。
- capacity容量不是指内存块byte[]数组的字节的数量。capacity容量指的是写入的数据对象的数量。
- 在读模式下,limit的值含义为最多能从缓冲区中读取到多少数据。
- 调用mark()方法设置mark=position,在调用reset()可以让position恢复到mark标记的位置即position=mark。
- 新建一个缓冲区时,缓冲区处于写入模式,这时是可以写数据的。数据写入后,如果要从缓冲区读取数据,这就要进行模式的切换,可以使用(即调用)flip翻转方法,将缓冲区变成读取模式。在这个flip翻转过程中,position会进行非常巨大的调整,具体的规则是:position由原来的写入位置,变成新的可读位置,也就是0,表示可以从头开始读。flip翻转的另外一半工作,就是要调整limit属性。
- 在读取完成后,可以调用Buffer.clear()清空或者Buffer.compact()压缩方法,再一次将缓冲区切换成写入模式。
(2)NIO Channel通道类:
大部分应用场景,从通道读取数据都会调用通道的int read(ByteBufferbuf)方法,它从通道读取到数据写入到ByteBuffer缓冲区,并且返回读取到的数据量;写入数据到通道。都会调用通道的int write(ByteBufferbuf)方法,参数—ByteBuffer缓冲区,是数据的来源。write方法的作用,是从ByteBuffer缓冲区中读取数据,然后写入到通道自身,而返回值是写入成功的字节数。
- File Channel文件通道:FileChannel为阻塞模式,不能设置为非阻塞模式。
- 新建的ByteBuffer,默认是写入模式,可以作为inChannel.read(ByteBuffer)的参数。inChannel.read方法将从通道inChannel读到的数据写入到ByteBuffer。
- 在NIO中,涉及网络连接的通道有两个,一个是SocketChannel负责连接传输,另一个是ServerSocketChannel负责连接的监听。ServerSocketChannel应用于服务器端,而SocketChannel同时处于服务器端和客户端。无论是ServerSocketChannel,还是SocketChannel,都支持阻塞和非阻塞两种。
(3)NIO Selector:
选择器的使命是完成IO的多路复用。一个通道代表一条连接通路,通过选择器可以同时监控多个通道的IO(输入输出)状况。选择器和通道的关系,是监控和被监控的关系。调用通道的Channel.register(Selector sel,int ops)方法,可以将通道实例注册到一个选择器中。register方法有两个参数:第一个参数,指定通道注册到的选择器实例;第二个参数,指定选择器要监控的IO事件类型。
(1)可读:SelectionKey.OP_READ
(2)可写:SelectionKey.OP_WRITE
(3)连接:SelectionKey.OP_CONNECT
(4)接收:SelectionKey.OP_ACCEPT
事件类型的定义在SelectionKey类中。如果选择器要监控通道的多种事件,可以用“按位或”运算符来实现。
- 判断一个通道能否被选择器监控或选择,有一个前提:判断它是否继承了抽象类SelectableChannel(可选择通道)。如果继承了SelectableChannel,则可以被选择,否则不能。一条通道若能被选择,必须继承SelectableChannel。
- 一旦在通道中发生了某些IO事件(就绪状态达成),并且是在选择器中注册过的IO事件,就会被选择器选中,并放入SelectionKey选择键的集合中。通过SelectionKey选择键,不仅仅可以获得通道的IO事件类型,如SelectionKey.OP_READ;还可以获得发生IO事件所在的通道;另外,也可以获得选出选择键的选择器实例。
- 使用选择器,主要有以下三步:
(1)获取选择器实例;(2)将通道注册到选择器中;(3)轮询感兴趣的IO就绪事件(选择键集合)。
注册到选择器的通道,必须处于非阻塞模式下,否则将抛出IllegalBlockingModeException异常。一个通道,并不一定要支持所有的四种IO事件。例如服务器监听通道ServerSocketChannel,仅仅支持Accept(接收到新连接)IO事件;而SocketChannel传输通道,则不支持Accept(接收到新连接)IO事件。可以在注册之前,可以通过通道的validOps()方法,来获取该通道所有支持的IO事件。
- SelectionKey集合不能添加元素,如果试图向SelectionKey选择键集合中添加元素,则将抛出java.lang. Unsupported-OperationException异常。
select()方法返回的整数值(int整数类型),表示发生了IO事件的通道数量。更准确地说,是从上一次select到这一次select之间,有多少通道发生了IO事件。
6、NIO和OIO的区别:
(1)OIO是面向流(Stream Oriented)的,NIO是面向缓冲区(Buffer Oriented);
(2)OIO的操作是阻塞的,而NIO的操作是非阻塞,NIO使用了通道和通道的多路复用;
(3)OIO没有选择器(Selector)概念,而NIO有选择器的概念。
7、在NIO中,同一个网络连接使用一个通道表示,所有的NIO的IO操作都是从通道开始的。一个通道类似于OIO中的两个流的结合体,既可以从通道读取,也可以向通道写入。通道的读取,就是将数据从通道读取到缓冲区中;通道的写入,就是将数据从缓冲区中写入到通道。