阻塞I/O
非阻塞IO
同步
用户进程自己去查询数据是否就绪
异步
不用自己,内核把数据拷贝到了buffer,通知用户程序去读取
多路复用
多个IO事件会注册到select上,select监听多个IO,当有就绪的IO事件,select返回IO的状态,程序调用IO会阻塞
select的实现方式有如下几种:
select
select底层,内核存储fd使用的是数组,默认1024,不能扩容,select底层没有开辟空间存储fd,所以每次用户程序都要将fd传输给select
poll
poll过程和select一样,内核存储fd使用的是链表
epoll
epoll底层开辟了空间使用红黑树会存储fd,所以用户态不需要拷贝fd到内核态
epoll相比select,poll的优势
1.select/poll把fd的监听列表放在用户空间,由用户空间管理,导致在用户空间和内核空间之间频繁重复拷贝大量fd;epoll在内核建立fd监听列表(实际是红黑树),每次通过epoll_ctl增删改即可
2.select/poll每当有fd内核事件时,都唤醒当前进程,然后遍历监听列表全部fd,检查所有就绪fd并返回;epoll在有fd内核事件时,通过回调把该fd放到就绪队列中,只需返回该就绪队列即可,不需要每次遍历全部监听fd
信号驱动
首先允许套接字使用信号驱动I/O.在这种模式下,系统调用将会立即返回,然后程序可以处理其它的工作。当数据准备就绪的时候,系统会向程序进程发送一个SIGIO信号。我们随后就可以在信号处理函数中调用read读取数据报,并通知主循环数据已经准备好待处理,也可以立即通知主循环,让它读取数据报。
异步I/O
相对于同步IO,异步IO不是顺序执行的,用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,用户进程就可以做其他的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的
BIO模式
BIO(同步阻塞)模式流程
流程:
1.服务器端的Server是一个线程,线程中执行一个死循环来阻塞的监听客户端的连接请求和通信。
2.当客户端向服务器端发送一个连接请求后,服务器端的Server会接受客户端的请求,ServerSocket.accept()从阻塞中返回,得到一个与客户端连接相对于的Socket。
3.构建一个handler,将Socket传入该handler。创建一个线程并启动该线程,在线程中执行handler,这样与客户端的所有的通信以及数据处理都在该线程中执行。当该客户端和服务器端完成通信关闭连接后,线程就会被销毁。
4.然后Server继续执行accept()操作等待新的连接请求。
BIO模式优点
1.使用简单,容易编程
2.在多核系统下,能够充分利用了多核CPU的资源。即,当I/O阻塞系统,但CPU空闲的时候,可以利用多线程使用CPU资源
BIO模式缺点
1.该模式的本质问题在于严重依赖线程,但线程Java虚拟机非常宝贵的资源。随着客户端并发访问量的急剧增加,线程数量的不断膨胀将服务器端的性能将急剧下降。
2.线程生命周期的开销非常高。线程的创建与销毁并不是没有代价的。在Linux这样的操作系统中,线程本质上就是一个进程,创建和销毁都是重量级的系统函数。
3.资源消耗。内存:大量空闲的线程会占用许多内存,给垃圾回收器带来压力。;CPU:如果你已经拥有足够多的线程使所有CPU保持忙碌状态,那么再创建更过的线程反而会降低性能。
4.稳定性。在可创建线程的数量上存在一个限制。这个限制值将随着平台的不同而不同,并且受多个因素制约:a)JVM的启动参数、b)Threa的构造函数中请求的栈大小、c)底层操作系统对线程的限制 等。如果破坏了这些限制,那么很可能抛出OutOfMemoryError异常。
5.线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,不仅会带来许多无用的上下文切换,还可能导致执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统负载偏高、CPU sy(系统CPU)使用率特别高,导致系统几乎陷入不可用的状态。
6.容易造成锯齿状的系统负载。一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。
7.若是长连接的情况下并且客户端与服务器端交互并不频繁的,那么客户端和服务器端的连接会一直保留着,对应的线程也就一直存在在,但因为不频繁的通信,导致大量线程在大量时间内都处于空置状态。
BIO模式适用场景
如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合
Rector模式
基于I/O多路复用模型,多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
单Reactor单线程
步骤
Reactor是一个线程对象,该线程会启动事件循环,并使用Selector来实现IO的多路复用,注册一个Acceptor事件处理器到Reactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样Reactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。
1.Reactor对象通过Select监听客户端Accept事件,收到事件后通过Dispatch进行分发给对应的Acceptor进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将该连接所关注的READ事件以及对应的READ事件处理器注册到Reactor中。
2.当Reactor监听到读写事件时,则Reactor会分发对应的读写Handler处理。
3.每当处理完所有就绪的感兴趣的I/O事件后,Reactor线程会再次执行select()阻塞等待新的事件就绪并将其分派给对应处理器进行处理
注意,Reactor的单线程模式的单线程主要是针对于I/O操作而言,也就是所以的I/O的accept()、read()、write()以及connect()操作都在一个线程上完成的。
优点
模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
缺点
1.性能问题,只有一个线程,无法完全发挥多核 CPU 的性能
2.可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用
单Reactor多线程
相比于单Reactor单线程模式,单Reactor多线程增加了线程池来处理非I/O操作(业务处理),这样能够提高Reactor线程的I/O响应,不至于因为一些耗时的业务逻辑而延迟对后面I/O请求的处理
优点
可以充分的利用多核cpu 的处理能力
缺点
多线程数据共享和访问比较复杂,Reactor处理所有的事件的监听和响应,在单线程运行, 在高并发场景容易出现性能瓶颈
主从Reactor多线程
步骤
1.注册一个Acceptor事件(处理accept事件)处理器到MainReactor中,启动MainReactor的事件循环
2.客户端向服务端发起一个连接请求,MainReactor监听到该Accept事件并将该Acceptor事件派发给Acceptor处理器进行处理。Acceptor处理器通过accept()方法得到该连接的SocketChannel,然后将SocketChannel通过dispatch派发给subReactor
3.SubReactor线程池分配一个SubReactor线程给这个SocketChannel,将这个SocketChannel的R/W事件到SubReactor的selector中。
4.当有I/O事件就绪时,相关的SubReactor就将事件派发给相应的Handler。这里的subReactor线程只负责完成I/O的read()操作,业务处理是在线程池中完成的。
注意,所以的I/O操作(包括,I/O的accept()、read()、write()以及connect()操作)依旧还是在Reactor线程(mainReactor线程或subReactor线程)中完成的。Thread Pool(线程池)仅用来处理非I/O操作的逻辑
优点
1.父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。
2.父线程与子线程的数据交互简单,Reactor主线程只需要把新连接传给子线程,子线程无需返回数据。