阻塞与同步
1)阻塞(Block)和非租塞(NonBlock):
阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候阻塞:往往需要等待缞冲区中的数据准备好过后才处理其他的事情,否則一直等待在那里。
非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回
2)同步(Synchronization)和异步(Async)的方式:
同步:应用程序要直接参与IO读写的操作。
异步:所有的IO读写交给搡作系统去处理,应用程序只需要等待通知。
同步方式在处理IO事件的时候,必须阻塞在某个方法上靣等待我们的IO事件完成(阻塞IO事件或者通过轮询IO事件的方式).对于异步来说,所有的IO读写都交给了搡作系统。这个时候,我们可以去做其他的事情,并不拓要去完成真正的IO搡作,当搡作完成IO后.会给我们的应用程序一个通知。
Java中的IO模型
- Java BIO:同步并阻塞,服务器实现模式为一个连接一个线程,既客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
- JavaNIO:同步非阻塞,服务器实现的模型为一个线程处理多个请求,既客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理
- JavaAIO:异步非阻塞,AIO引入异步通道的概念,采用Proactor模式,简化了程序编写,有效的请,求才启动线程,它的特点是由操作系统完成后才通知服务端启动线程去处理,一般使用于连接较多且连接时间较长的应用
BIO、NIO、AIO适用场景
- BIO方式适用于连接数目比较小并且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用种
- NIO方式适用于连接数多并且连接比较短(轻操作)的架构中,比如聊天服务器,弹幕系统,服务器见通讯等
- AIO适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发
BIO
java1.4以前的IO模型,一连接对一个线程。
原始的IO是面向流的,不存在缓存的概念。Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
Java IO的各种流是阻塞的,这意味着当一个线程调用read或 write方法时,该线程被阻塞,直到有一些数据被读取,或数据完全写入,该线程在此期间不能再干任何事情了。
什么是NIO
java.nio全称java non-blocking IO(实际上是 new io),是指JDK 1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。
HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。
NIO是面向缓冲区的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性。
Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
NIO的核心实现
NIO有三大核心部分:Channel,Buffer,Selector
通道Channel
NIO的通道类似于流,但有些区别如下:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲区读数据,也可以写数据到缓冲区
NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件,因此使用单个线程就可以监听多个客户端通道
缓冲区Buffer
缓冲区本质上是一个可以写入数据的内存块,然后可以再次读取,该对象提供了一组方法,可以更轻松地使用内存块,该对象提供了一组方法,可以更加轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区状态变化情况。Channel提供从文件、网络读取数据的渠道,但是读取和写入的数据都必须经由Buffer
Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其包含的数据元素的信息
- capacity 容量,表示可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
- limit:表示缓冲区的当前终点,不能对缓冲区超过限制的位置进行读写操作,这个限制时可以修改的
- position:位置,下一个要被读或写的元素的索引,每次读写缓冲区时都会改变该值,为下次读写做准备
- mark:标记,记录当前position的索引位置,为reset方法提供支持
使用缓冲区读取和写入数据通常遵循以下四个步骤:
- 写数据到缓冲区;
- 调用buffer.flip()方法;
- 从缓冲区中读取数据;
- 调用buffer.clear()或buffer.compat()方法;
当向Buffer写入数据时,buffer会记录下写了多少数据,一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式,在读模式下可以读取之前写入到buffer的所有数据,一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。
Selector
一个组件,可以检测多个NIO channel,看看读或者写事件是否就绪。
多个Channel以事件的方式可以注册到同一个Selector,从而达到用一个线程处理多个请求成为可能。
Java的NIO,用非阻塞IO的方式,可以用一个线程,处理多个客户端连接,就会用到selector
- selector能够检测多个注册的通道上是否有事件发生(多个channel以事件的方式可以注册到同一个selector),如果有事件发生,获取事件然后针对每个事件进行相应的处理。
- 只有在channel真正有读写事件发生时,才会进行读写,大大减少了系统的开销,并且不必为每个连接都创建一个线程
Selector、Channel、Buffer的关系图
- 每个Channel对应一个Buffer
- Selector对应多个Channel
- 一个线程对应一个Selector
- 程序切换到哪个Channel是由事件决定的,Event是个很重要的概念
- Selector会根据不同的事件,在各个Channel上切换
- Buffer就是一个内存块,底层是有一个数组实现
- 数据的读取和写入都是通过Buffer,NIO的Buffer既可以读也可以写,通过flip方法切换方向
- Channel是双向的,可以返回底层操作系统的情况
NIO与零拷贝
零拷贝描述的是CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于通过网络传输一个文件时以减少CPU周期和内存带宽。
零拷贝的好处:
- 减少甚至完全避免不必要的CPU拷贝,从而让CPU解脱出来去执行其他的任务
- 减少内存带宽的占用
- 通常零拷贝技术还能够减少用户空间和操作系统内核空间之间的上下文切换
传统I/O
在Java中,我们可以通过InputStream从源数据中读取数据流到一个缓冲区里,然后通过OutputStream输出到另一个数据源。我们知道,这种IO方式传输效率是比较低的。那么,当使用上面的代码时操作系统会发生什么情况:
- JVM发出read() 系统调用。
- OS上下文切换到内核模式(第一次上下文切换)并将数据读取到内核空间缓冲区。(第一次拷贝:hardware ----> kernel buffer)
- OS内核然后将数据复制到用户空间缓冲区(第二次拷贝: kernel buffer ——> user buffer),然后read系统调用返回。而系统调用的返回又会导致一次内核空间到用户空间的上下文切换(第二次上下文切换)。
- JVM处理代码逻辑并发送write()系统调用。
- OS上下文切换到内核模式(第三次上下文切换)并从用户空间缓冲区复制数据到内核空间缓冲区(第三次拷贝: user buffer ——> kernel buffer)。
- write系统调用返回,导致内核空间到用户空间的再次上下文切换(第四次上下文切换)。将内核空间缓冲区中的数据写到hardware(第四次拷贝: kernel buffer ——> hardware)。
总的来说,传统的I/O操作进行了4次用户空间与内核空间的上下文切换,以及4次数据拷贝。显然在这个用例中,从内核空间到用户空间内存的复制是完全不必要的,因为除了将数据转储到不同的Buffer之外,我们没有做任何其他的事情。所以,我们能不能直接从Hardware读取数据到Kernel Buffer后,再从Kernel Buffer写到目标地点不就好了。为了解决这种不必要的数据复制,操作系统出现了零拷贝的概念。
带有DMA收集拷贝功能的sendfile实现的I/O
- 发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
- 没有数据拷贝到socket缓冲区。取而代之的是只有相应的描述符信息会被拷贝到相应的socket缓冲区当中。该描述符包含了两方面的信息:a)kernel buffer的内存地址;b)kernel buffer的偏移量。
- sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。DMA gather copy根据socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(第二次拷贝: kernel buffer ——> protocol engine),这样就避免了最后一次CPU数据拷贝。
- 带有DMA收集拷贝功能的sendfile实现的I/O只使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝。这样一来我们就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换。
IO模型
传统阻塞IO服务模型
- 采用阻塞IO模式获取输入的数据
- 每个连接都需要独立的线程完成数据的输入、业务处理、数据返回
问题分析
- 当并发数很大,就会创建大量的线程,占用大量的系统资源
- 连接创建后,如果当前线程暂时没有数据读写,该线程被阻塞,降低了系统资源的利用率
Reactor模型
- 基于IO复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,程序从阻塞状态返回,开始进行业务处理
- 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程池进行处理,一个线程可以处理多个连接的业务
Reactor模式中核心组成
- Reactor:在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理器程序来对IO事件做出反应。
- Handlers:事件处理其,由Reactor分派调度执行
Reactor模式分类:
- 单Reactor单线程
- 单Reactor多线程
- 主从Reactor多线程
单Reactor
- Reactor对象通过Select监控客户端请求事件,收到事件后通过Dispatch进行分发
- 如果是建立连接请求,则由Acceptor通过Accept处理连接请求,然后创建一个Handler对象处理连接完成后的后续业务处理
- 如果不是连接连接事件,则Reactor会分发调用连接对应的Handler来响应
- Handler会完成读写等完成业务流程
- 服务端用一个线程通过多路复用搞定所有的IO操作,编码简单,清晰明了,但是如果客户端连接数量较多,将无法支撑
优点:
缺点:
- 不能利用多核CPU;
- 一个线程需要执行处理所有的accept、read、decode、process、encode、send事件,处理成百上千的链路时性能上无法支撑;
单reactor多线程
- Reactor对象通过select监控客户端请求事件,收到事件后,通过dispatch进行分发
- 如果是建立连接请求,则由Acceptor通过accept处理连接请求,然后创建一个Handler对象处理完成连接后的各种事件
- 如果不是连接请求,则由reactor分发调用连接对应的handler处理
- handler只负责响应事件,不做具体的业务处理,通过read读取数据后,会分发到worker线程池的某个线程处理业务
- worker线程池会分配独立线程完成真正的业务,worker线程处理后通过handler对象回发
优点:
缺点:
- 多线程数据共享和访问比较复杂,reactor处理所有事件的监听和响应,运行在单线程,在高并发场景容易出现性能瓶颈
主从Reactor多线程
- reactor主线程mainReactor对象通过select监听连接事件,收到事件后,通过acceptor处理连接事件
- 当accpetor处理连接事件后,mainReactor将连接分配给subReactor
- subReactor将连接加入到连接队列进行监听,并创建handler进行各种事件处理
- 当由新事件发生时,subReactor就会调用对应的handler处理
- handler通过read读取数据,分发给worker线程池分配线程处理
- worker线程池分配独立的worker线程进行业务处理,worker线程处理后通过handler对象回发
优点
- 父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理
- 父线程与子线程的数据交互简单,reactor主线程只需要把新连接传给子线程,子线程无需返回数据
缺点
Netty模型
-
Netty抽象出两组线程池 BossGroup和WorkerGroup,其中BossGroup专门负责接收客户端的连接,WorkerGroup专门负责网络的读写
-
BossGroup和WorkerGroup的类型都是NioEventLoopGroup
-
NioEventLoopGroup相当于一个事件循环组,该组中含有多个事件循环,而每一个事件循环都是NioEventLoop
-
NioEventLoop表示一个不断循环的处理任务的线程,每个NioEventLoop都有一个Selector,用于监听绑定在其上的网络事件
-
在BossGroup下的每个NioEventLoop循环执行三个步骤
-
- 轮询accept事件
- 处理accept事件,与client建立连接,生成NioSocketChannel,并将它注册到某个WorkerGroup的NioEventLoop上的Selector
- 处理任务队列中的剩余任务,既runAllTasks
-
WorkerGroup下的每个NIOEventLoop循环执行三个步骤
-
- 轮询读写事件
- 处理读写事件,在对应的NIOSocketChannel中处理
- 处理任务队列中的剩余任务,既runAllTasks
-
WorkerGroup下的NIOEventLoop处理业务时,会使用pipeline(管道),pipeline中包含了channel,既通过pipeline可以获取到对应的通道,管道中维护了很多的处理器