电话:13485538018
关闭
您当前的位置:首页 > 职场资讯 > 面试秘籍

socket网络编程面试:IO模型与同步异步,一文讲透

来源:网络整理 时间:2026-06-30 作者:佚名 浏览量:

今儿咱们在进入Java NIO编程之前, 先来探讨些较为基础的知识, 即I/O模型。本文先开启同步与异步概念的话题开端, 随后阐释阻塞和非阻塞的差异所在, 并介绍阻塞IO与非阻塞IO的不同之处, 再接着说明同步IO和异步IO的区别, 而后介绍5种IO模型, 最后介绍两种跟高性能IO设计有关的设计模式(Reactor和Proactor)。

以下是本文的目录大纲:

一.什么是同步?什么是异步?

二.什么是阻塞?什么是非阻塞?

三.什么是阻塞IO?什么是非阻塞IO?

四.什么是同步IO?什么是异步IO?

五.五种IO模型

六.两种高性能IO设计模式

若有不正之处,请多多谅解并欢迎批评指正。

一.什么是同步?什么是异步?

用于表示同时进行以及不同时进行的相关概念诞生的时间已然过去了很长时间 , 在网络上围绕着表示同时进行以及不同时进行的各种说法同样存在着许多。以下是基于我个人的情形所形成的理解:

同步的情况是这样的: 存在多个任务或者会有多个事件将要发生, 这些任务及事件得逐个依次去开展, 只要有某一个事件或者任一项任务开始执行起来了, 那就会致使整个流程陷入暂时等待的状态当中, 这些事件根本没有办法同时进行执行。

一旦存在多个任务, 或者出现多个事件发生的情况, 那么这些事件能够以并发的方式来执行, 并且其中一个事件或者任务的执行, 不会致使整个流程产生暂时等待的状况, 这便是异步。

呈现出来的这便是同步以及异步, 去列举一个简易的事例, 假设存在一项任务涵盖了两个子任务A与B, 针对同步而言, 就在A开展执行具体情况之际, B仅仅能够予以等待, 一直到A执行完成, B才能够展开执行;然而对于异步来讲就是A和B能够进行并发地执行, B并不需要等待A执行完成之后才开始执行, 如此一来就不会因为A的执行致使整个任务出现暂时的等待情况。

如果还不理解,可以先看下面这2段代码:

void fun1() {

void fun2() {

void function(){

fun1();

fun2()

.....

.....

这段是极为典型的同步模式, 于方法function里, 当fun1处于执行进程里, 其将引发之后fun2无法展开执行, fun2唯有等待fun1执行结束才可开展执行。

接着看下面这段代码:

void fun1() {

void fun2() {

void function(){

new Thread(){

public void run() {

fun1();

}.start();

new Thread(){

public void run() {

fun2();

}.start();

.....

.....

这段代码属于一种典型的异步情况, fun1开始执行, 不会对fun2的执行产生影响, 此时不会致使其后续的执行进程处于暂时的等待状态, fun2开始执行, 不会对其后续的执行进程造成处于短暂停顿的状况, fun1和fun2的执行不会让其后续的执行过程处于暂时性的等待状况。

在实际情况当中, 同步以及异步属于一个极为宽泛的概念, 它们的关键之点就在于当多个任务还有事件出现的时候, 一个事件的出现或者实施期间, 整个流程是否会因为此而出现暂时的等待情况。我个人认为能够把同步和异步跟Java里的synchronized关键字串联着来作类比。当存在多个线程同时对一个变量进行访问时, 每个线程针对该变量的访问都算是一个事件, 就同步而言, 这些线程得逐个地去访问该变量, 在一个线程访问该变量的进程当中, 其他线程是必须要等待的;就异步来讲, 意味着多个线程并非要逐个地去访问该变量, 能够同时开展访问。

因此, 个人感觉同步跟异步呢, 在好多层面都能够体现出来, 然而得记着, 其重点在于, 当多个任务以及事件出现的时候, 一个事件的发生或者执行, 会不会致使整个流程出现短暂的等待。通常情况下, 可以借助多线程的办法去达成异步, 但是一定要记好, 别把多线程跟异步直接等同起来, 异步仅仅是宏观范畴内的一种模式, 运用多线程去实现异步只是一种途径, 而且通过多进程的方式同样能够实现异步。

二.什么是阻塞?什么是非阻塞?

在之前部分介绍了同步与异步二者的差异, 于当前这一章节去瞧一瞧阻塞跟非阻塞之间的不同之处!

阻塞的情况是这样, 在某一桩事件或者任务于执行进程当中时, 它发出一项请求进行操作, 然而因为这个请求操作所需的条件并未得到满足, 所以就会一直在原地等候着, 一直到条件得以满足才会停止。

非阻塞之意为, 于某个事件或者任务开展执行流程之时, 进行一个具有请求性质的操作, 一旦此请求操作依赖的条件未达成, 便即刻回返一则传达此条件未达成的标志信息去, 而不会持续在那里处于等待状态。

这便是阻塞与非阻塞之间的差异所在, 换句话讲, 阻塞和非阻塞的区别重点在于, 当发起一项请求时, 若条件未能达成, 究竟是会持续等待还是返回一则标志信息。

举个简单的例子:

要是我打算去读取一个文件里头所包含的内容, 当这个时候文件之中不存在可供读取的内容, 对于同步这种情况而言就是会一直在那儿持续等待, 一直到文件里出现有内容能够进行读取;然而对于非阻塞这种情形来讲, 就会直接返回一条标志信息来告知文件之内暂时没有内容可以予以读取。

网上存在一些朋友, 把同步跟异步分别同阻塞以及非阻塞划上等号, 实际上, 它们是两组全然不同的概念, 留意, 领会这两组概念的差异, 针对后面IO模型的理解极为重要。

同步以及异步的着重点处于多个任务开展的进程当中, 一个任务的执行究竟会不会致使整个流程出现短暂的等待呢, 是这样的情况。

在发出一个该请求操作之际, 该阻塞与非阻塞的着重点呢, 是这样的要是进行操作的条件并不满足, 那是否会返回一个标志信息去告知条件是不满足的了。

采用同对线程阻塞进行类比的方式, 能够理解阻塞以及非阻塞, 当有一个线程着手去做一个请求的操作之时, 如果条件并不满足, 那么这个线程就会受阻, 也就是在那里静等条件出现满足的情况。

三.什么是阻塞IO?什么是非阻塞IO?

查看一个真实存在的具体的IO操作具体的开展进程, 于明白阻塞IO以及非阻塞IO之前着手。能当标点不算吧? 算吧所以有句号最后就标点符合了。

一般来讲, IO操作涵盖, 针对硬盘的读取与写入, 针对socket的读取与写入, 以及对外设的读取与写入。

当用户线程发起一个以读请求操作为例的IO请求操作时, 内核会去查看要读取的数据是否就绪, 对于阻塞IO而言, 如果数据没有就绪, 就会一直在那等待, 直至数据就绪, 对于非阻塞IO而言, 如果数据没有就绪, 就会返回一个标志信息,告知用户线程当前要读的数据没有就绪, 当数据就绪之后, 便会把数据拷贝到用户线程, 如此才完成了一个完整的IO读请求操作, 换言之, 一个完整的IO读请求操作涵盖两个阶段:

1)查看数据是否就绪;

2)进行数据拷贝(内核将数据拷贝到用户线程)。

那么, 阻塞(blocking IO)与非阻塞(non-blocking IO)的差异所在便是第一个阶段, 当数据尚未就绪时, 于查看数据是否就绪的这个进程里, 究竟是持续等待, 亦或是直接返回一则标志信息。

在Java里, 传统的IO全都是阻塞IO, 举例来讲, 借助socket去读取数据, 在调用read()方法后,要是数据尚未就绪, 那么当前线程就会始终阻塞于read方法调用之处, 一直到有数据才会返回;然而要是是非阻塞IO的状况, 当数据未就绪时, read()方法应当返回一则标志信息, 以此告知当前线程数据未就绪, 而非一直在那儿等候。

四.什么是同步IO?什么是异步IO?

让我们首先去瞧一瞧同步IO与异步IO的定义, 于《Unix网络编程》这本书里头对于同步IO以及异步IO的定义是这般的:

异步输入输出操作致使发出请求的进程被阻止掉, 一直到那个输入输出操作完成才得以继续。并且只有在该项输入输出的工作全部结束之后才可恢复开始运行 , 然后恢复能够去持续接下来工作进度安排的有关请求项目 , 一直到最后完成这个输入输出操作的全部具体过程, 最终顺利达成的整个过程。

从字面意思能够看得出, 同步IO是这样的情况, 要是有一个线程去请求开展IO操作, 在这个IO操作完成以前, 这个线程会处于被阻塞的状态 , 而异步IO则是如此, 要是有一个线程去请求开展IO操作, 这个IO操作不会致使请求线程被阻塞。

实际上, 同步IO以及异步IO模型, 是就用户线程跟内核的交互而言的。

对于同步IO, 在用户发出IO请求操作后, 要是数据未就绪, 就得借助用户线程或者内核持续地去轮询数据是否就绪, 等数据就绪了, 才把数据从内核拷贝到用户线程。

而异步 IO, 其 IO 请求操作的发出, 是借助用户线程来开展的, 不过 IO 操作的两个阶段, 皆由内核自行完成, 之后发送通知, 去告知用户线程 IO 操作已然完成, 换句话讲, 在异步 IO 情形下, 不会给用户线程造成任何阻塞。

这是同步IO与异步IO关键区别所存在之处, 同步IO跟异步IO的关键区别体现于数据拷贝阶段是由用户线程去完成, 还是由内核来完成, 所以讲异步IO必定得有操作系统的底层给予支持。

需留意, 同步IO以及异步IO, 跟阻塞IO还有非阻塞IO, 是不一样的两组概念噢。

阻塞IO与非阻塞IO所反映的情况是, 当用户发起IO操作请求时, 若数据尚未就绪, 存在这样两种情形, 一种是用户线程持续等待数据就绪, 另一种是会收到一标志信息, 就是, 阻塞IO和非阻塞IO所反映的内容是, 在IO操作的首个阶段, 于查看数据是否就绪之际是怎样进行处理的。

五.五种IO模型

《Unix网络编程》这本书里讲了五种IO模型, 其分别为阻塞IO, 非阻塞IO, 多路复用IO, 信号驱动IO, 还有异步IO。

下面就分别来介绍一下这5种IO模型的异同。

1.阻塞IO模型

最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。

用户线程发出 IO 请求之过后, 内核会去查验数据是不是就绪, 要是没有就绪就会在那儿等待数据变为就绪状况呈现相应状态转变, 而此刻用户线程就会处于堵塞状态, 进而用户线程放开 CPU 的占用转移等待被新进程或线程占用使用。待数据就绪之状况出现之之后, 内核会把数据复制到用户线程的相应存储区域当中去, 并将运算或查找等所需结果返回给用户线程, 如此这般用户线程才会解除处于堵塞的那种状态恢复正常运行状态。

典型的阻塞IO模型的例子为:

data = socket.read();

如果数据没有就绪,就会一直阻塞在read方法。

2.非阻塞IO模型

用户线程发起read操作后, 无需等待, 即刻得到一个结果。若结果为error, 它便知晓数据尚未准备好, 于是可再度发送read操作。一旦内核中的数据准备就绪, 且再次收到用户线程的请求, 那么它即刻将数据拷贝至用户线程, 随后返回。

所以, 实际上, 于非阻塞IO模型里, 用户线程得持续询问内核数据是不是就绪, 也就是说非阻塞IO不会交出CPU, 而是会始终占用CPU。

典型的非阻塞IO模型一般如下:

while(true){

data = socket.read();

if(data!= error){

处理数据

break;

但对于非阻塞IO而言, 存在一个极为严重的问题, 在while循环里, 需持续不断地向内核询问数据是否就绪, 如此一来会致使CPU占用率相当高, 所以通常情形下, 很少运用while循环这种形式去读取数据。

3.多路复用IO模型

当下使用较为频繁的模型是多路复用IO模型, Java NIO事实上就是多路复用IO。

在多路复用 IO 模型内里, 有一个线程会持续不断地去轮询多个 socket 的状态, 只有当 socket 切实存在读写事件之际, 才会真正去调用实则的 IO 读写操作。在多路复用 IO?模型里中, 仅是需要运用一个线程便能管理多个 socket, 系统无需去建立崭新的进程或者崭新的线程, 同时也不必去维护这些线程以及进程, 而且只有在切实存在 socket 读写事件正在进行之时,才会动用 IO 资源, 所以它极大地减少了资源占用。

在Java NIO里, 借助selector.select()来查问每个通道有无到达事件, 要是没有事件, 便会持续阻塞在那儿, 所以此种方式会致使用户线程被阻塞。

或许会有朋友讲, 我能够运用多线程加上阻塞IO获取相仿的成效, 然而鉴于处在多线程与阻塞IO里, 每一个socket对应着一个线程, 这般会导致极大的资源消耗, 而且特别是针对长连接而言, 线程的资源始终不会被释放, 要是后续接连有诸多连接, 就会酿成性能上的瓶颈。

然而, 有一种多路复用IO模式, 能借助一个线程做到管理数目不止一个的socket, 并且, 只有当个别的socket实实在在发生了读写种种事件的时候, 此模式才会占用相应资源用以开展实际的读写操作过程。因此, 可以得出这样的结论, 多路复用IO模式比较适合应用于连接数相对较多的那种情况。

另外, 多路复用IO比非阻塞IO模型效率高, 原因在于, 在非阻塞IO里, 不断询问socket状态是由用户线程来进行的, 然而在多路复用IO中, 轮询每个socket状态是内核在做的, 并且这个效率比用户线程要高得多。

然而需要留意的是, 多路复用 IO 模型用以检测是否有事件抵达所采用的方法是轮询, 而且对于抵达的事件会逐个予以响应。所以就多路复用 IO 模型而言, 要是事件响应体长, 那便会使得后续的事件长时间无法得到处理, 同时还会对新的事件轮询造成影响。

4.信号驱动IO模型

于信号驱动IO模型里, 当用户线程发起一项IO请求操作时, 会给对应的socket注册一个信号函数, 之后用户线程会持续执行, 当内核数据就绪时会给用户线程发送一个信号, 用户线程接收到信号后, 会在信号函数中调用IO读写操作去开展实际的IO请求操作。

5.异步IO模型

异步IO模型堪称最为理想的IO模型, 于异步IO模型里, 一旦用户线程发起read操作后, 马上就能着手去做别的事情。而从内核层面来讲, 当它接收到一个asynchronous read之后, 会即刻返回, 表明read请求已然成功发起, 所以不会对用户线程造成任何阻塞。接着, 内核会等着数据准备工作达成, 随后把数据复制到用户线程, 待这全部结束后, 内核会给用户线程发送一个信号, 告知其read操作已完成。也就是说用户线程压根无需知晓具体整体的IO操作是怎样开展的, 只需先发出一个请求, 当收到内核返回的成功信号时便意味着IO操作已完成, 能够直接去运用数据了。

那么就是说, 于异步IO模型里, IO操作的那两个阶段不会有阻塞用户线程的情况, 这两个阶段由内核自行去完成, 之后向外发送一个信号, 以此告知用户线程操作已然完成, 用户线程当中无需再度去调用IO函数来开展具体的读写操作咯。这点跟信号驱动模型存在差异, 在信号驱动模型里, 用户线程接收到数据就绪信号, 接着要用户线程调用IO函数开展实际的读写操作, 而在异步IO模型中, 收到信号意味着IO操作完结就无需再于用户线程中调用IO函数进行实际的读写操作。

留意, 异步IO需操作系统底层予以支持, 于Java 7里, 给出了Asynchronous IO。

前面的四种IO模型, 事实上都归属于同步IO, 仅仅最后那一种才是实实在在的异步IO, 这是为何, 请听好, 不管是多路复用IO也好, 还是信号驱动模型也罢, 在IO操作的第2个阶段, 会导致用户线程出现阻塞的情况, 一言以蔽之那就是, 内核进行数据拷贝的这个过程, 都会使得用户线程被阻塞住。

六.两种高性能IO设计模式

于传统的网络服务设计模式里, 存在着两种颇为经典的模式, 其一为多线程, 其二是线程池。

就多线程模式而言, 也就是说一旦有了client, 那么服务器便会去新建一个线程, 以此来处理该client下的读写事件, 情况如下所示:

这种模式在处理方面呈现出简单便利的特性, 然而, 鉴于服务器针对每个client的连接采用线程进行处理这一情况, 致使其资源占用极多异常庞大。所以, 当连接数量抵达上限之际, 若有其他用户发出请求做出连接等行为, 便会径直引发资源瓶颈状况, 严重的话甚至极有可能直接造成服务器不再正常运行而崩溃。

所以, 为了处理这种因一个线程对应一位客户端模式所引发的困难, 提出运用线程池的办法, 也就是说构建一个具备固定规模的线程池, 每当有一个客户端到来, 便从线程池中选取一个处于空闲状态的线程去开展处理工作, 当客户端完成读写操作之后, 就返还对线程的占用, 所以如此便规避了为每一个客户端都去创建线程所造成的资源损耗, 进而让线程能够实现重复使用。

但是线程池存在着它所具有的弊端, 要是连接多数情况属于长连接, 所以有可能致使在一段时期之内, 线程池当中的线程均被占有应用, 倘若这个时候再有用户发起请求连接, 鉴于没有可用于处理的空闲线程存在, 就会造成客户端连接遭遇失败状况, 进而对用户体验产生影响。所以, 线程池相对而言比较适宜应用于大量的短连接情形。

因为这样, 所以就产生了接下来的两种具备高性能的IO设计模式, 它们分别是Reactor以及Proactor。

于Reactor模式里, 会先针对每个client去注册感兴趣的事件, 接着有一个线程专门用来轮询每个client是否存在事件发生, 当有事件发生之际, 便依照顺序处理每个事件, 当所有事件处理完毕之后, 便再转过去继续进行轮询, 如同下面的图示这般:

这里能瞧出, 上头那五种 IO 模型里的多路复用 IO 采用的是 Reactor 模式, 留意, 上头那绘出的呈现的是依次处理每个事件, 当然为了提升事件处理速率, 可循着多线程或者线程池的途径来处理事件。

在Proactor模式之内,当察觉到有事件发生时刻, 会新开启起一个异步操作之举, 而后交付由内核线程去实施处理, 当内核线程达成IO操作这件事之后,去发送一则通知用以告知操作已然完成, 能够得悉, 异步IO模型所采用的便是Proactor模式。

微信扫一扫分享资讯
相关推荐
暂无相关推荐
客服服务热线
13485538018
24小时服务
微信公众号
手机浏览

CopyrightC 2009-2025 All Rights Reserved 版权所有 芜湖人才网 本站内容仅供参考,不承担因使用信息、外部链接或服务中断导致的任何直接或间接责任,风险自担。如有侵权,请联系删除,联系邮箱:ysznh@foxmail.com 鄂ICP备2025097818号-15

地址: EMAIL:qlwl@foxmail.com

Powered by PHPYun.

用微信扫一扫