【Java IO 总述】核心是NIO(两篇BIO + 三篇IO+一篇Reactor彻底搞懂)

Java IO,面试中遇到,基本上说原理层面的了,使用层面没什么好说的
NIO-Reactor-netty-RPC(Dubbo Thrift) 核心是NIO 面试中都是问NIO的,毕竟netty使用NIO实现,没人问BIO AIO,这两个点一下就好
Netty:直接对Java NIO编程非常不容易,想写出高并发的、健壮的程序很难,不仅要求超一流的编程技巧,还需要几个复杂领域(网络编程、多线程处理和并发)的专业知识。Netty 优雅地处理了这些领域的知识,使得即使是网络编程新手也能使用。
Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端, 它对Java NIO做了封装,把那些复杂的底层细节都屏蔽了。
1、首先它是个框架,是个“半成品”,不能开箱即用,你必须得拿过来做点定制,利用它开发出自己的应用程序,然后才能运行(就像使用Spring那样)。 如果你想开发一个自己的高性能的RPC框架, RPC的调用协议,数据的格式和次序也是自己定义的,现有的HTTP根本玩不转,那使用Netty就是绝佳的选择。一个更加知名的例子就是阿里巴巴的Dubbo了,这个RPC框架的底层用的就是Netty。
2、高性能,高可靠,有了Netty,自己就不要基于NIO进行编程了。

JavaIO三篇+Reactor总算搞懂一篇:

【Java IO】使用层面:BIO的使用,第一篇(非面试重点,面试都用NIO)

【Java IO】使用层面:BIO的使用,第二篇(非面试重点,面试都用NIO)

JavaIO 001 Linux五种IO模型

JavaIO 002 Java三种IO模型

JavaIO 003 Reactor三种模式

Reactor总算搞懂

**1、Java 三种IO是基于Linux底层五种IO实现的(相关博客好了:两篇博客:Linux五种IO+Java三种IO)
2、Reactor三种模式是基于Java NIO实现的,是对Nio的封装(相关博客好了:一篇博客:从NIO到Reactor三种模式)
3、Netty是基于Reactor实现的(Nio+socket 变为了 网络框架)(两篇博客:netty源码解析,解释netty是如何基于Reactor实现的)
4、RPC可以使用netty + spring + zookeeper可以实现一个RPC框架(一篇博客:实践类:使用netty + spring + zookeeper可以实现一个RPC框架)
4、很多RPC框架使用netty作为通信:阿里Dubbo RPC框架、淘宝的RocketMQ 消息中间件、游戏领域、大数据领域Hadoop **

序列化和RPC的关系:RPC存在网络传输,所以需要一种对Java Bean的序列化技术

Linux五种io模型

Java三个IO和Linux五种IO及其对应关系
1、在Java中,主要有三种IO模型,分别是阻塞IO(BIO)、非阻塞IO(NIO)和 异步IO(AIO)。
2、在Linux(UNIX)操作系统中,共有五种IO模型,分别是:阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动IO模型以及异步IO模型。
3、Java中提供的IO有关的API,在文件处理的时候,其实依赖操作系统层面的IO操作实现的

Java BIO 对应 Linux 同步非阻塞IO
Java NIO 对应 Linux 信号驱动IO
Java AIO 对应 Linux 异步IO

金手指:五种方式一句话总结,好背诵
1、同步阻塞IO 全程阻塞(等待数据期间,recvfrom系统调用);
2、同步非阻塞IO 等待数据轮询(等待数据期间,不断发出recvfrom系统调用),复制数据阻塞
3、同步信号驱动IO 等待数据不阻塞(建立用来接收sigio信号的信号处理程序,发出sigaction系统调用得到返回,信号处理程序收到内核空间的sigio信号的时候,发出recvfrom系统调用),复制数据阻塞
4、异步IO 等待数据和复制数据都不阻塞(发出aio_read系统调用得到返回,信号处理程序收到内核空间的递交的aio_read中的指定信号,表示复制完成,使用程序处理数据报)
5、 IO复用 全程阻塞 (发出select系统调用,收到可读指令后,发出recvfrom系统调用)
金手指:io复用和信号驱动io记忆差不多,发送的信号不大一样,但是不是那么一回事,只是可以这样记忆

Java BIO NIO AIO

面试语言组织:
推荐:对比、理论语言
不推荐:图片、源代码、举例子

金手指:一句话小结关于BIO NIO AIO:
服务端采用同步阻塞的BIO
服务端采用同步阻塞的线程池的BIO
JDK4之后服务端采用同步非阻塞的NIO
JDK7之后服务端采用异步非阻塞的AIO

Java BIO 对应 Linux 同步非阻塞IO
Java NIO 对应 Linux 信号驱动IO
Java AIO 对应 Linux 异步IO

金手指:同步与异步、阻塞与非阻塞
同步与异步

同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。

异步:
异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。

同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。

阻塞和非阻塞

阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。

非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

BIO:
1、BIO也是accept read write三个操作,NIO也是accept read write三个操作,AIO也是accept read write三个操作,任何io都是accept read write三个操作;
2、Bio中是accept read write都是同步阻塞操作,这是无法改变,所有要改善bio,必须使用线程池,但是线程的创建和销毁的代价比较大,特别是对于linux这种,一个线程就是一个进程的,但是没办法,只有这样一种方式。
Bio中是accept read write都是同步阻塞操作:同步阻塞I/O模式,一个请求一个应答,数据的读取写入必须阻塞在一个线程内等待其完成。采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在 while(true) 循环中服务端会调用 accept() 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接。
多线程,就是伪异步IO:如果要让 BIO 通信模型 能够同时处理多个客户端请求,就必须使用多线程(主要原因是 socket.accept()、 socket.read()、 socket.write() 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通信模型 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过 线程池机制 改善,线程池还可以让线程的创建和回收成本相对较低。使用FixedThreadPool 可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M),下面一节"伪异步 BIO"中会详细介绍到。
多线程,伪异步IO的代价大,但是没办法:在 Java 虚拟机中,线程是宝贵的资源,线程的创建和销毁成本很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。

伪异步IO:
线程池:为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
伪异步IO:采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口,变成一个可以多线程的类)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
伪异步IO还是BIO:伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层任然是同步阻塞的BIO模型,因此无法从根本上解决问题。

NIO:
NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。
NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和
ServerSocketChannel
两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式(使用的时候,服务端channel在启动五步中就设置为非阻塞,accept连接之后得到的客户端channel设置为非阻塞,其中,服务端只有启动的时候有一个channel,每收到一个客户端连接,就有一个客户端channel,n个客户端连接就有n个客户端channel,一个客户端断开连接会销毁这个客户端channel,取消在select中的selectionKey)。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用
NIO 的非阻塞模式来开发

面试官:NIO与IO区别?
核心:先从从 NIO 流是同步非阻塞,而 IO 流是同步阻塞 IO 说起。然后,从 NIO 的3个核心组件/特性为 NIO带来的一些改进来分析。
第一,IO流是阻塞的,NIO流是不阻塞的
1.1 Java IO的各种流是阻塞的。这意味着,当一个线程调用 accept() read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
1.2 Java NIO使我们可以进行非阻塞IO操作。
非阻塞读read:
单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。
非阻塞写write:
一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
第二,NIO三要素Buffer(缓冲区) IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)
Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。
在NIO厍中,所有数据都是用缓冲区buffer处理的。
在读取数据时,它是直接读到缓冲区中的;
在写入数据时,写入到缓冲区中。
任何时候访问NIO中的数据,都是通过缓冲区进行操作。
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。

第三,NIO三要素Channel (通道)NIO 通过Channel(通道) 进行读写
通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道channel 只能和Buffer交互。因为 Buffer,通道可以异步地读写。

第四,NIO三要素Selectors(选择器) NIO有选择器,而IO没有
选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。
因此,为了提高系统效率选择器是有用的。
channel注册到selector中,
在这里插入图片描述

记住,NIO中的所有IO都是从 Channel(通道) 开始的。

从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。

从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。

数据读取和写入操作图示:

在这里插入图片描述

从BIO到NIO
ServerSocket 变为 ServerSocketChannel
NIO三要素:Buffer Channel Selector
buffer表示满了才读,这就是linux五种IO中的同步非阻塞IO recvfrom 轮询
channel ServerSocket 变为 ServerSocketChannel Socket变为SocketChannel
Selector 用来选择

NIO三个东西:Buffer 对于accept接收请求的线程没用,只对执行IO操作的线程有用(read/write)

注意:这里将所有发生的事件都交给单个线程去处理,如果性能不够,可以开个线程池去处理事件。
比如,Reactor三种模式:单线程、线程池、主从线程池
金手指:select模型,redis单线程连接多个,good
while (selector.select()>0){ //如果没有准备好的通道,这里会阻塞住,减少CPU消耗,有准备好的通过,马上使用

为什么大家都不愿意用 JDK 原生 NIO 进行开发呢?从上面的代码中大家都可以看出来,是真的难用!除了编程复杂、编程模型难之外,它还有以下让人诟病的问题:
JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%
Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题,看一下后面的Reactor三种模式就知道,完成封装好了NIO的操作。

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。

从NIO到Reactor三种模式

面试语言组织:(背下来,理解后背诵很简单,理解和防范面试官的问题)
三个要素
Reactor模型是基于事件驱动的线程模型,可以分为Reactor单线程模型、Reactor多线程模型、主从Reactor多线程模型,通常基于在I/O多路复用实现。不同的角色职责有:Dispatcher负责事件分发、Acceptor负责处理客户端连接、Handler处理非连接事件(例如:读写事件)。
一、Reactor单线程模型
1、原理图示
在Reactor单线程模型中,操作在同一个Reactor线程中完成。根据事件的不同类型,由Dispatcher将事件转发到不同的角色中处理。连接事件转发到Acceptor处理、读写事件转发到不同的Handler处理。
在这里插入图片描述
2、实现图示
NIO实现中,可以将Accept事件注册到select选择器中,轮询是否有“接受就绪”事件。如果为“连接就绪”分发给Acceptor角色处理;“写就绪”事件分发给负责写的Handler角色处理;“读就绪”事件分发给负责读的Handler角色处理。这是事情都在一个线程中处理。
在这里插入图片描述
二、Reactor多线程模型
1、原理图示
在Reactor多线程模型中。根据事件的不同类型,由Dispatcher将事件转发到不同的角色中处理。连接事件转发到Acceptor单线程处理、读写事件转发到不同的Handler由线程池处理。
在这里插入图片描述
2、实现图示
NIO实现中,可以将Accept事件注册到select选择器中,轮询是否有“接受就绪”事件。如果为“连接就绪”分发给Acceptor角色处理,此处处理“连接就绪”为一个线程;“写就绪”事件分发给负责写的Handler角色由线程池处理;“读就绪”事件分发给负责读的Handler角色由线程池处理。
在这里插入图片描述
三、主从Reactor多线程模型
1、原理图示
Reactor多线程模型,由Acceptor接受客户端连接请求后,创建SocketChannel注册到Main-Reactor线程池中某个线程的Select中;具体处理读写事件还是使用线程池处理(Sub-Reactor线程池)。
在这里插入图片描述
2、实现图示
将Accept事件注册到select选择器中,轮询是否有“接受就绪”事件;“连接就绪”分发给Acceptor角色处理,创建新的SocketChannel转发给Main-Reactor线程池中的某个线程处理;在指定的Main-Reactor某个线程中,将SocketChannel注册读写事件;当“写就绪/读就绪”事件分别由线程池(Sub-Reactor线程池)处理。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_36963950/article/details/108020214