IO流知识总结(二)

当程序阻塞时,会降低程序的效率,于是人们就希望能引入非阻塞的操作方法,也就有了同步非阻塞,但是对于同步状态中是需要等到有返回结果才能继续执行下一步,又希望在等待IO操作的时候还能去做别的事情,等IO操作完通知它,所以又引入了异步,也就有了各种IO模型。

一、IO模型

对于I/O,可以分成阻塞I/O与非阻塞I/O两大类型。阻塞I/O在做I/O读写操作时会使当前线程进入阻塞状态,而非阻塞I/O则不进入阻塞状态。对于线程,单线程情况下由一条线程负责所有客户端连接的I/O操作,而多线程情况下则由若干线程共同处理所有客户端连接的I/O操作。

一个IO读过程是文件数据从磁盘→内核缓冲区→用户内存的过程。同步与异步的区别主要在于数据从内核缓冲区→用户内存这个过程需不需要用户进程等待,即实际的IO读写是否阻塞请求进程(网络IO把磁盘换做网卡即可)。

阻塞和非租塞:阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候阻塞:往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否則一直等待在那里。当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。

同步:所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作,同步就是必须一件一件事做,等前一件做完了才能做下一件事。例如:B/S模式中的表单提交,具体过程是:客户端提交请求->等待服务器处理->处理完毕返回,在这个过程中客户端不能做其他事。

异步:当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。通知调用者,监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低;当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能;与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。例如:B/S模式中的ajax请求,具体过程是:客户端发出ajax请求->服务端处理->处理完毕执行客户端回调,在客户端(浏览器)发出请求后,仍然可以做其他的事。

同步和异步的区别:请求发出后,是否需要等待结果,才能继续执行其他操作。在IO操作中,同步是应用程序要直接参与IO读写的操作。同步方式在处理IO事件的时候,必须阻塞在某个方法上靣等待我们的IO事件完成。异步是所有的IO读写交给操作系统去处理,应用程序只需要等待通知。并不要去完成真正的IO搡作,当搡作系统完成IO后,会给我们的应用程序一个通知。

同步和异步说的是消息的通知机制,阻塞非阻塞说的是线程的状态

1、阻塞IO模型

同步阻塞IO模型是最简单的IO模型,用户线程在内核进行IO操作时被阻塞。用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。进程阻塞挂起不消耗CPU资源,及时响应每个操作;实现难度低、开发应用较容易;适用并发量小的网络应用开发。不适用并发量大的应用:因为一个请求IO会阻塞进程,所以得为每请求分配一个处理进程(线程)以及时响应,系统开销大。典型应用:阻塞socket、Java BIO。

2、非阻塞IO模型

同步非阻塞IO是在同步阻塞IO的基础上。这样做用户线程可以在发起IO请求后可以立即返回。由于是非阻塞的方式,因此用户线程发起IO请求时立即返回。对于上面的阻塞IO模型来说,内核数据没准备好需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞。实现难度低、开发应用相对阻塞IO模式较难;适用并发量较小、且不需要及时响应的网络应用开发;虽然未读取到任何数据不会阻塞线程,但用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断的轮询、重复请求,消耗了大量的CPU的资源。典型应用:socket是非阻塞的方式。

3、IO多路复用模型

多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select, select会监听所有注册进来的IO;如果select没有监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞;而当任一IO在内核缓冲区中有可数据时,select调用就会返回;而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO,读取内核中准备好的数据。可以看到,多个进程注册IO后,只有另一个select调用进程被阻塞。专一进程解决多个进程IO的阻塞问题,性能好;Reactor模式;实现、开发应用难度较大;适用高并发服务应用开发:一个进程(线程)响应多个请求;

4、信号驱动的IO模型

当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。

5、异步IO

当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据。不阻塞,数据一步到位;Proactor模式;需要操作系统的底层支持,实现、开发应用难度大;非常适合高性能高并发应用;典型应用:AIO、高性能服务器应用。

IO流知识总结(二)_第1张图片

总结:

阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动的IO模型者为同步IO模型,只有异步IO模型是异步IO。一般说异步都是非阻塞的,同步才有阻塞和非阻塞之分。
NIO是同步非阻塞的,AIO是异步非阻塞的。对于那些读写过程时间长的,NIO就不太适合,AIO的读写过程完成后才被通知,所以AIO能够胜任那些重量级,读写过程长的任务。

 

二、NIO操作

Java NIO由以下几个核心部分组成,Channel,Buffer 和 Selector 构成了核心的API。其它组件,如Pipe和FileLock,只不过是与三个核心组件共同使用的工具类。NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。

1、Channel组件

Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。

1.1 channel类型

FileChannel从文件中读写数据,FileChannel不能直接打开,需要通过一个与之关联的FileInputStream、FileOutputStream或者RandomAccessFile来获得FilChannel。

RandomAccessFile randomAccessFile = new RandomAccessFile("xxx", "rw");
FileChannel fileChannel = randomAccessFile.getChannel();

DatagramChannel能通过UDP读写网络中的数据,用法和下面的SocketChannel一样。

DatagramChannel channel =  DatagramChannel.open();

SocketChannel能通过TCP读写网络中的数据,ServerSocketChannel可以监听新进来的TCP连接。

SocketChannel socketChannel = SocketChannel.open(); 
socketChannel.connect(new InetSocketAddress(“localhost”, 80));
//默认的工作模式时阻塞的
SocketChannel.configureBlocking(false);

这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO

1.2 分散和聚集

分散(scatter):从Channel中读取是指在读操作时将读取的数据写入多个buffer中,按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写,在移动下一个buffer前,必须填满当前的buffer。
聚集(gather):写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入
scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体

1.3 channel方法

transferFrom()方法:将数据从源通道传输到FileChannel中,字节从给定的可读取字节通道传输到此通道的文件中,输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数
transferTo()方法:将数据从FileChannel传输到其他的channel中

 

2、Buffer组件

Buffer的数据结构是一个保存了原始数据的数组,在Java语言里面封装成为一 个带引用的对象。Buffer一般称为缓冲区,该缓冲区的优点在于它虽然是一个简单数组,但是它封装了很多数据常量以及单个对象的相关属性。跟前面讲的缓冲区是一样的。

2.1 Buffer类型

ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer。

//ByteBuffer通过一定编码转CharBuffer
ByteBuffer buffer = ByteBuffer.allocate(10);
Charset charset = Charset.forName("UTF-8");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer cbuff = decoder.decode(buffer);

2.2 Buffer原理

capacity:作为一个内存块,Buffer有一个固定的大小值,一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据
position:当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置
limit:在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。

2.3 Buffer方法

flip()方法:flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值

rewind()方法:将position设回0,所以你可以重读Buffer中的所有数据,limit保持不变

clear()方法:position将被设回0,limit被设置成 capacity的值

compact()方法:将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面,limit属性设置成capacity,Buffer准备好写数据了,但是不会覆盖未读的数据

mark()方法:可以标记Buffer中的一个特定position

reset()方法:恢复到这个position

clear()方法会清空整个缓冲区。

compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面

remaining()方法返回limit和position之间相对位置差

2.4 Buffer用法

Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer。

分配空间:

ByteBuffer buf = ByteBuffer.allocate(1024);

向Buffer中写数据:从Channel写到Buffer (fileChannel.read(buf)),或者通过Buffer的put()方法 (buf.put(…))

从Buffer中读取数据:从Buffer读取到Channel (channel.write(buf)),或者使用get()方法从Buffer中读取数据 (buf.get())

写入数据到Buffer,调用flip()方法,从Buffer中读取数据,调用clear()方法或者compact()方法。
当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。

当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

综上所述,buffer缓冲区,实际上是一个容器,一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer。流程如下:

 IO流知识总结(二)_第2张图片

3、Selector组件

Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便要使用Selector,得向Selector注册Channel(class.forname注册),然后调用它的select()方法。能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件

3.1 Selector事件

SelectionKey.OP_CONNECT,SelectionKey.OP_ACCEPT,SelectionKey.OP_READ,SelectionKey.OP_WRITE

通道触发了一个事件意思是该事件已经就绪。所以,某个channel成功连接到另一个服务器称为“连接就绪”。一个server socket channel准备好接收新进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”(如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来)。客户端A发送数据,会触发read事件,这样下次轮询调用select方法时,就能通过socketChannel读取数据,同时在selector上注册该socketChannel的OP_WRITE事件,实现服务器往客户端写数据。

3.2 SelectionKey对象

interest集合:是你所选择的感兴趣的事件集合

int interestSet=selectionKey.interestOps();
boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;


ready 集合:是通道已经准备就绪的操作的集合

//int readSet=selectionKey.readOps();
selectionKey.isAcceptable();//等价于selectionKey.readyOps()&SelectionKey.OP_ACCEPT
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel和Selector:从SelectionKey访问Channel和Selector

Channel channel =selectionKey.channel();
Selector selector=selectionKey.selector();

附加的对象:可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道

selectionKey.attach(theObject);//绑定

selectionKey.attachment();     //取出

3.3 通过Selector选择通道

一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法,返回的int值表示有多少通道已经就绪,前面被记录的后面不会再被记录。

int select():阻塞到至少有一个通道在你注册的事件上就绪了。 
int select(long timeout):和select()一样,但最长阻塞时间为timeout毫秒。 
int selectNow():非阻塞,只要有通道就绪就立刻返回

selectedKeys(),访问“已选择键集(selected key set)”中的就绪通道,可以通过SelectionKey的selectedKeySet()方法访问这些对象。对于选中通道有以下步骤:

首先检查已取消键集合,也就是通过cancle()取消的键。如果该集合不为空,则清空该集合里的键,同时该集合中每个取消的键也将从已注册键集合和已选择键集合中移除。(一个键被取消时,并不会立刻从集合中移除,而是将该键“拷贝”至已取消键集合中,这种取消策略就是我们常提到的“延迟取消”。)

再次检查已注册键集合(准确说是该集合中每个键的interest集合)。系统底层会依次询问每个已经注册的通道是否准备好选择器所感兴趣的某种操作,一旦发现某个通道已经就绪了,则会首先判断该通道是否已经存在在已选择键集合当中,如果已经存在,则更新该通道在已注册键集合中对应的键的ready集合,如果不存在,则首先清空该通道的对应的键的ready集合,然后重设ready集合,最后将该键存至已注册键集合中。这里需要明白,当更新ready集合时,在上次select()中已经就绪的操作不会被删除,也就是ready集合中的元素是累积的,比如在第一次的selector对某个通道的read和write操作感兴趣,在第一次执行select()时,该通道的read操作就绪,此时该通道对应的键中的ready集合存有read元素,在第二次执行select()时,该通道的write操作也就绪了,此时该通道对应的ready集合中将同时有read和write元素

wakeUp()方法:某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。如果有其它线程调用了wakeup()方法,
但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来
close()方法:用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭

3.4 Selector用法

通过调用Selector.open()方法创建一个Selector

Selector selector = Selector.open();

为了将Channel和Selector配合使用,必须将channel注册到selector上。通过SelectableChannel.register()方法来实现。注:FileChannel 是不能够使用选择器的, 因为 FileChannel 都是阻塞的。因为 channel 是非阻塞的,因此当没有数据的时候会理解返回,因此 实际上 Selector 是不断的在轮询其注册的 channel 是否有数据就绪

//非阻塞模式
channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

如果 select()方法返回值表示有多个 Channel 准备好了, 那么我们可以通过 Selected key set 访问这个 Channel。注意, 在每次迭代时, 我们都调用 “keyIterator.remove()” 将这个 key 从迭代器中删除, 因为 select() 方法仅仅是简单地将就绪的 IO 操作放到 selectedKeys 集合中, 因此如果我们从 selectedKeys 获取到一个 key, 但是没有将它删除, 那么下一次 select 时, 这个 key 所对应的 IO 事件还在 selectedKeys 中

Set selectedKeys = selector.selectedKeys();

Iterator keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {
    
    SelectionKey key = keyIterator.next();
	keyIterator.remove();
	
	//可能有多个注册事件就绪
	
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } 
    if (key.isConnectable()) {
        // a connection was established with a remote server.

    }
    if (key.isReadable()) {
        // a channel is ready for reading

    } 
    if (key.isWritable()) {
        // a channel is ready for writing
    }

    
}

3.5 Selector原理

SocketChannel、ServerSocketChannel和Selector的实例初始化都通过SelectorProvider类实现,Selector初始化时,会实例化PollWrapper、SelectionKeyImpl数组和Pipe,pollWrapper用Unsafe类申请一块物理内存pollfd,存放socket句柄fdVal和events,pollWrapper提供了fdVal和event数据的相应操作,如添加操作通过Unsafe的putInt和putShort实现。
如果该channel和selector已经注册过,则直接添加事件和附件,否则通过selector实现注册过程,new一个SelectionKeyImpl实体类,完成后执行addKey方法。pollWrapper.addEntry将把selectionKeyImpl中的socket句柄添加到对应的pollfd,k.interestOps(ops)方法最终也会把event添加到对应的pollfd,在selector注册的事件,最终都保存在pollArray中。
对于selector获取多个有事件发生的channel,通过doSelect方法实现,其中 subSelector.poll() 是select的核心,由native函数poll0实现,readFds、writeFds 和exceptFds数组用来保存底层select的结果,数组的第一个位置都是存放发生事件的socket的总数,其余位置存放发生事件的socket句柄fd。执行 selector.select()时,poll0函数把指向socket句柄和事件的内存地址传给底层函数,如果之前没有发生事件,程序就阻塞在select处,当然不会一直阻塞,因为epoll在timeout时间内如果没有事件,也会返回,
一旦有对应的事件发生,poll0方法就会返回,processDeregisterQueue方法会清理那些已经cancelled的SelectionKey,updateSelectedKeys方法统计有事件发生的SelectionKey数量,并把符合条件发生事件的SelectionKey添加到selectedKeys哈希表中,提供给后续使用。

epoll是Linux下的一种IO多路复用技术,原先Selector基于select/poll模型实现,后优化了Selctor的实现,底层使用epoll。epoll可以非常高效的处理数以百万计的socket句柄,epoll初始化时,会向内核注册一个文件系统,用于存储被监控的句柄文件,还会再建立一个list链表,用于存储准备就绪的事件,会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后,就把socket插入到就绪链表里,所以epoll只要观察就绪链表里有没有数据,如果有数据就返回,否则就sleep,超时就立刻返回。

 

三、多路复用Reactor

整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,大部分采用的是多路复用。IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。

通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events(更细小的线程,任务更加单一)事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。

 

四、异步IO

“真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。

异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核,然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步IO

相比于IO多路复用模型,异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。况且目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式(IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中)。

 

五、总结

一开始对于IO操作,CPU是全程参与的,由于CPU的高速性和I/O设备的低速性,致使CPU的绝大部分时间都处于等待I/O设备完成数据I/O的循环测试中,造成了 CPU资源的极大浪费。

然后IO采用中断方式,也就是说读取数据开始和结束的时候才会需要CPU参与,那为什么线程还会阻塞呢,因为是同步的,必须等到IO数据读取完才能执行到下一步,而读取数据又不在需CPU参与,CPU还在这线程就会浪费时间,所以该线程就会阻塞,所以进行IO操作线程会阻塞。对于大部分BIO是满足各大需求的,实现又简单。但是对于客户端与线程数1:1,并且进行读写操作时会阻塞的,当你有成千上万的客户端进行连接,就导致服务器不断的建立新的线程,最后导致资源不足,后面的客户端不能连接服务器,并且连接入的客户端并不是总是在于服务器进行交互,很可能就只是占用着资源而已(虽然线程阻塞到运行状态的改变也耗资源,但不是主要因素)。
而后我们可以通过线程池去控制线程的创建和销毁,但是当有一个客户端的读取信息非常慢时,比如都在下大型文件,服务器对其的写操作时会很慢,甚至会阻塞很长时间,因为线程池中的线程是有限的,当有一个客户端只需要下载一个小型文件几KB的样子,在去分配线程时,就会导致新任务在队列中一直等待阻塞的客户端释放线程。当任务队列已经满时,就会有大量的用户发生连接超时。


所以就出现非阻塞IO。对于非阻塞IO,在碰到IO操作的时候会直接返回,不会阻塞线程,但需要该线程不断去循环判断数据准备好了没有,也会浪费CPU资源。
后又采用多路分离函数select来监听是否有数据到来,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。虽然可以使用单线程就能处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长,有数据来的时候还要循环遍历是哪个通道有数据,对于管理很多个通道的话也会有很多不必要的判断循环(有些通道没有数据也要循环遍历),也会耗时间。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。

于是出现了多路复用模型,可以将用户线程轮询IO操作状态的工作统一交给Reactor进行处理。有数据就给相应事件的进程读取数据。如果对于多个线程多进行IO操作的话,那么这些线程全都要阻塞,而现在是单独开一个线程去监听各线程的IO操作数据有没有到来,如果都没来只需阻塞这一个线程,这样就其他线程都不用阻塞可以去做其他事情,有数据了就通知相应线程。这样也相当于实现了异步阻塞。

最后异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。

你可能感兴趣的:(java)