1227java面经

embedded/2024/12/28 17:35:47/

1,HTTP 请求报文是如何组成的?

HTTP 请求报文主要由以下几个部分组成:

请求行(Request Line)

这是请求报文的起始行,包含了请求方法、请求的 URL(统一资源定位符)以及所遵循的 HTTP 协议版本这三个关键要素,各部分之间通过空格进行分隔。

  • 请求方法

    :用于告知服务器客户端期望执行的操作类型,常见的请求方法有:

    • GET:通常用于获取指定资源,例如从服务器获取一个网页文档、查询某些数据等,它一般不会对服务器端的数据产生实质性修改,并且请求参数通常会附加在 URL 后面传递。
    • POST:常用于向服务器提交数据,比如提交表单数据(如用户注册、登录时填写的信息等),它将数据放在请求体中发送给服务器,相比于 GET 方法,POST 更适合用于传递大量数据或者敏感数据,因为数据不会直接暴露在 URL 中。
    • PUT:一般用于更新服务器上的资源,客户端通过该方法向服务器发送完整的资源数据,以替换服务器上指定资源的原有内容。
    • DELETE:顾名思义,主要用于请求服务器删除指定的资源。
    • 此外,还有像 HEAD(类似于 GET,但只获取头部信息,不返回正文内容)、OPTIONS(用于查询服务器支持的 HTTP 方法等信息)等其他方法,不过使用相对较少。
  • 请求的 URL:明确指出了客户端希望获取或操作的资源在服务器上的位置,例如 http://example.com/index.html,这里的 http://example.com 是服务器的域名或 IP 地址,/index.html 就是具体指向服务器上的某个网页资源路径。

  • HTTP 协议版本:常见的有 HTTP/1.0、HTTP/1.1 以及 HTTP/2 等,它表明客户端所遵循的 HTTP 协议标准,不同版本在功能、性能以及特性方面存在一定差异,比如 HTTP/1.1 相比 HTTP/1.0 在持久连接、缓存处理等方面有改进,HTTP/2 则在性能优化上有更多特性,如多路复用等。

请求头部(Request Headers)

请求头部包含了一系列的键值对,用于向服务器传递额外的信息,比如客户端的一些属性、对响应的期望、数据的格式等内容,每一行表示一个头部字段,格式为 “字段名:字段值”,常见的请求头部字段如下:

  • User-Agent:用于告知服务器客户端使用的软件及版本信息,例如浏览器类型及版本(如 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, dlgflow Chrome/92.0.4515.131 Safari/537.36 表示使用的是 Chrome 浏览器及对应的版本号等情况),服务器可以根据这个信息来针对不同类型的客户端返回适配的内容或者进行统计分析等操作。
  • Accept:指定客户端能够接受的响应数据的类型(通常称为 MIME 类型),例如 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8 表示客户端可以接受多种格式的数据,并且对不同类型的数据有不同的偏好程度(通过 q 值来体现,取值范围是 0 到 1,值越高表示越偏好该类型数据),这样服务器可以根据此选择合适的响应格式进行返回。
  • Accept-Encoding:用于告诉服务器客户端支持的内容编码方式,常见的有 gzipdeflatebr 等,服务器可以对响应数据进行相应的编码压缩后再发送给客户端,以减少网络传输的数据量,提高传输效率,客户端在接收到数据后再进行解压还原操作。
  • Content-Type:如果请求中有数据要发送给服务器(如 POST 请求),该字段用于说明发送的数据的类型,例如 Content-Type: application/json 表示发送的数据是 JSON 格式的数据,服务器可以根据此来正确解析接收到的数据。
  • Cookie:用于在客户端和服务器之间传递会话相关的信息,比如用户登录后的身份标识等,服务器在之前响应中设置的 Cookie 会通过客户端的请求中的这个字段回传给服务器,服务器再根据 Cookie 来识别客户端并提供相应的服务。

请求体(Request Body)

并非所有的请求报文都有请求体,只有当请求方法需要向服务器发送数据时(如 POST、PUT 等方法)才会有这部分内容。请求体就是实际要发送给服务器的数据内容,其格式和类型取决于 Content-Type 头部字段所指定的类型,常见的有以下几种情况:

  • 表单数据格式(application/x-www-form-urlencoded):当表单提交数据时,如果采用这种格式,数据会被编码成键值对形式,类似 name=value&name2=value2 的格式,各个键值对之间通过 & 符号连接,然后放在请求体中发送给服务器,服务器可以按照这种格式解析出对应的表单数据。
  • JSON 格式(application/json):在现代的 Web 开发中应用非常广泛,数据按照 JSON 的语法规则进行组织,例如 {"username": "张三", "password": "123456"},这种格式清晰、易读且方便在不同的编程语言之间进行数据交互,服务器端如果支持解析 JSON 数据,就可以将接收到的数据转换为相应的对象进行处理。
  • 文件上传格式(multipart/form-data):用于向服务器上传文件时,该格式会将文件以及其他相关的表单数据一起进行组织,包含了文件的元数据(如文件名、文件类型等)以及文件的二进制内容等信息,通过特定的边界标识来区分不同的部分,服务器可以根据这些信息来接收并保存上传的文件以及处理相关的表单数据。

总之,HTTP 请求报文通过这样有结构的几个部分,清晰地向服务器传达了客户端的请求意图、相关信息以及要发送的数据等内容,使得服务器能够准确地处理客户端的请求并返回相应的响应。

2,HTTP1.0和HTTP1.1 有哪些区别?

HTTP 1.0 和 HTTP 1.1 是 HTTP 协议的两个重要版本,它们主要有以下区别:

1. 连接方式

  • HTTP 1.0:默认使用非持久连接。这意味着每进行一次 HTTP 操作(如请求一个网页中的多个资源),客户端和服务器之间都要建立一次新的 TCP 连接。例如,当一个网页包含多个元素(如图片、脚本文件、样式表等),浏览器每请求一个元素就需要建立一个新的连接,这会导致频繁的连接建立和断开过程,开销较大。
  • HTTP 1.1:默认采用持久连接(也称为长连接)。在一次 TCP 连接中,客户端可以向服务器发送多个请求,并且在一个请求响应完成后,连接不会立刻关闭,而是可以继续被后续的请求使用。这样,当浏览器请求一个网页及其包含的多个资源时,只需要建立一次 TCP 连接,就可以完成所有的请求和响应过程,大大减少了建立和关闭连接的开销,提高了性能。

2. 带宽优化和性能

  • HTTP 1.0:没有针对带宽优化的相关机制。每个请求和响应的消息头通常包含完整的信息,即使是在请求相同类型资源的情况下,也不会对消息头进行优化处理。例如,每次请求一张图片,消息头中的一些重复的信息(如浏览器标识等)都会完整地发送,没有利用到之前已经发送过的相同信息。
  • HTTP 1.1:引入了一些带宽优化措施。例如,支持请求头(Request Headers)和响应头(Response Headers)的部分字段值的缓存。如果客户端再次发送相似的请求,一些不需要改变的头部字段(如 User - Agent)就可以从缓存中获取,不必重新发送,从而减少了网络传输的数据量。同时,还支持分块传输编码(Chunked Transfer - Coding),允许服务器将内容分成多个小块进行发送,这样在动态生成内容的场景下,可以更灵活地将数据逐步发送给客户端,而不需要等待整个内容全部生成后再发送,提高了响应速度。

3. 缓存处理

  • HTTP 1.0:缓存机制相对简单。主要通过 Expires 头字段来控制缓存,它指定了一个绝对的过期时间。例如,服务器可以在响应头中设置 Expires: Fri, 30 Oct 1998 14:19:41 GMT,表示资源在这个时间之后就过期了,客户端需要重新向服务器请求该资源。这种方式比较僵硬,因为如果服务器时间和客户端时间不一致,可能会导致缓存失效或者过期时间不准确的问题。
  • HTTP 1.1:缓存控制更加灵活和强大。除了保留 Expires 头字段外,还引入了 Cache - Control 头字段,它可以使用相对时间来控制缓存,如 Cache - Control: max - age = 3600,表示资源在 3600 秒内可以使用缓存,不需要重新请求。同时,Cache - Control 还支持更多的指令,如 no - cache(表示每次使用缓存前都需要向服务器验证缓存的有效性)、private(表示缓存只能在客户端本地使用,不能被代理服务器等共享)等,这些指令可以根据不同的场景和需求来精确地控制缓存的行为。

4. 主机头(Host Header)

  • HTTP 1.0:没有 Host 头字段。在 HTTP 1.0 时代,一个 IP 地址通常只对应一个主机(服务器),所以在请求中不需要特别指明请求的主机名。
  • HTTP 1.1:新增了 Host 头字段。这是因为随着虚拟主机技术的发展,一个 IP 地址可以对应多个主机(通过不同的域名来区分)。例如,一个服务器的 IP 地址为 192.168.1.1,它可能同时承载了 www.example1.comwww.example2.com 两个网站。客户端在请求时通过 Host: www.example1.com 这样的头字段来明确告诉服务器它请求的是哪个主机名下的资源,使得服务器能够正确地处理请求并返回对应的内容。

5. 协议状态码

  • HTTP 1.0:定义了一些基本的状态码,如常见的 200 OK(表示请求成功)、404 Not Found(表示请求的资源不存在)等,但状态码的种类和语义相对较少。
  • HTTP 1.1:对状态码进行了扩充和细化。新增了一些状态码来更好地处理复杂的网络情况和请求响应场景。例如,100 Continue 状态码用于客户端在发送请求体之前,先询问服务器是否愿意接收请求(主要用于 POST 等请求可能包含大量数据的情况),如果服务器返回 100 Continue,客户端就可以继续发送请求体;还有 417 Expectation Failed 用于表示服务器无法满足客户端在请求头中包含的某些期望条件(如通过 Expect 头字段指定的特殊要求)等。

HTTP 1.1 在连接方式、带宽优化、缓存处理、主机头以及状态码等方面都进行了改进和扩展,使得网络通信更加高效、灵活,能够更好地适应复杂多样的 Web 应用场景。

3,介绍 HTTP 和 HTTPS

HTTP(超文本传输协议)和 HTTPS(超文本传输安全协议)是用于在网络上传输数据的协议,它们在 Web 开发和网络通信中起着至关重要的作用。

HTTP

  1. 定义和工作原理
    • HTTP 是一种应用层协议,它基于请求 / 响应模型来工作。客户端(如浏览器)向服务器发送一个请求,请求中包含了请求方法(如 GET、POST 等)、请求的资源路径(如网页、图片、脚本文件等的 URL)以及一些请求头信息(如浏览器类型、可接受的数据格式等)。服务器接收到请求后,根据请求的内容查找相应的资源,然后将资源以及响应状态码、响应头信息(如内容类型、缓存信息等)一起返回给客户端。
    • 例如,当你在浏览器中输入一个网址并按下回车键时,浏览器会构建一个 HTTP 请求,发送到对应的服务器。服务器如果找到请求的网页,就会将网页的 HTML 内容以及相关的响应信息返回给浏览器,浏览器再对这些内容进行解析和渲染,展示出网页。
  2. 特点
    • 简单性:HTTP 协议的基本结构比较简单,请求和响应消息的格式易于理解和实现。它使用文本格式进行通信,方便开发人员进行调试和查看通信内容。
    • 无状态性:HTTP 是无状态协议,这意味着服务器不会在不同的请求之间记住客户端的状态。每个请求都是独立的,服务器不会自动关联一个请求和之前的请求。例如,当用户在一个购物网站上添加商品到购物车,然后又进行了其他页面的浏览后,服务器不会自动知道之前添加购物车的操作,需要通过一些额外的机制(如使用 Cookie 或 Session)来跟踪用户状态。
    • 支持多种数据类型:可以传输各种类型的数据,如 HTML 文件、图片(JPEG、PNG 等格式)、脚本文件(JavaScript)、样式表(CSS)等,通过请求头中的 Content - Type 等字段来指定和识别数据类型。
  3. 应用场景
    • 适用于大多数对安全性要求不是特别高的网络应用场景,如普通的网页浏览、信息查询网站、公开的资源分享网站等。例如,一些博客网站、新闻资讯网站等,主要是提供信息展示和查询功能,使用 HTTP 协议可以满足基本需求。

HTTPS

  1. 定义和工作原理
    • HTTPS 是 HTTP 的安全版本,它在 HTTP 协议的基础上加入了 SSL/TLS(安全套接层 / 传输层安全)加密协议。在通信过程中,客户端和服务器之间会首先进行 SSL/TLS 握手,这个过程主要是协商加密算法、交换密钥等操作。一旦握手成功,双方就会使用协商好的加密算法和密钥对传输的数据进行加密和解密。这样,即使数据在传输过程中被第三方截获,由于数据是加密的,第三方也很难获取到原始信息。
    • 例如,在进行网上银行转账操作时,浏览器和银行服务器之间会建立 HTTPS 连接。在连接建立初期,双方会进行复杂的加密协商过程,之后所有的转账信息(如账户号码、转账金额、密码等)在传输过程中都会被加密,确保了信息的安全性。
  2. 特点
    • 安全性高:通过加密机制保护数据的保密性和完整性。防止数据在传输过程中被窃取、篡改。同时,还可以通过数字证书来验证服务器的身份,确保客户端连接到的是真实的、合法的服务器,避免中间人攻击。
    • 信任建立机制:基于数字证书来建立信任关系。服务器需要向权威的证书颁发机构(CA)申请数字证书,证书中包含了服务器的公钥以及服务器的身份信息等内容。客户端在连接服务器时,会验证证书的有效性,只有证书有效且与服务器匹配时,才会建立安全连接。
  3. 应用场景
    • 适用于对安全性和隐私性要求较高的场景,如网上银行、电子商务、电子政务、企业内部的敏感信息系统等。在这些场景中,涉及用户的个人隐私信息(如身份证号码、银行卡号、密码等)、商业机密(如企业的财务数据、订单信息等),使用 HTTPS 可以有效保护这些重要信息的安全。

4,HTTP无状态的特性带来了哪些问题?

HTTP 的无状态特性虽然使得协议简单且易于实现,但也带来了一些问题:

1. 用户体验问题

  • 场景示例
    • 在电商网站中,用户将商品添加到购物车后,若 HTTP 是有状态的,服务器会自然地记住用户购物车的内容。但由于 HTTP 无状态,当用户跳转到其他页面或者进行新的操作后,服务器就 “忘记” 了购物车中的商品信息。
  • 影响
    • 这会导致用户每次查看购物车内容或者进行结算等操作时,可能看到购物车为空或者之前添加的商品信息丢失,极大地影响了用户购物体验的连贯性。

2. 业务逻辑复杂程度增加

  • 场景示例
    • 在一个在线学习平台,用户需要先登录才能访问课程内容。如果没有额外的机制来处理无状态问题,每次用户请求访问课程页面时,服务器都无法识别用户是否已经登录,会把用户当作新的访问者。
  • 影响
    • 为了实现正常的业务逻辑,开发人员需要在每个涉及用户权限的页面请求中添加额外的验证步骤。例如,通过检查请求中是否携带有效的登录凭证(如 Cookie 或 Session 中的用户标识信息)来判断用户是否已经登录,这使得业务逻辑变得复杂,增加了开发和维护的成本。

3. 数据一致性和准确性问题

  • 场景示例
    • 考虑一个多步骤的表单提交场景,如用户在网站上申请贷款。用户需要在多个页面依次填写个人信息、贷款金额、贷款期限等内容。由于 HTTP 无状态,服务器在接收每个页面的表单数据时,无法直接将这些数据关联起来看作一个完整的贷款申请流程。
  • 影响
    • 如果没有合适的机制来管理这些数据,可能会出现数据丢失、数据不匹配或者重复提交等问题。例如,用户在某个步骤由于网络原因重新提交了表单,服务器可能会因为无法识别这是重复提交而错误地处理数据,导致数据一致性被破坏。

4. 性能开销问题

  • 场景示例
    • 为了在无状态的 HTTP 环境下实现用户状态跟踪,通常会采用如 Cookie 或 Session 的机制。每次客户端请求都会携带 Cookie 信息,服务器需要对这些 Cookie 进行解析和验证。
  • 影响
    • 这增加了网络传输的数据量(尤其是当 Cookie 内容较多时)和服务器的处理开销。而且如果使用 Session,服务器还需要在内存或者其他存储介质中维护每个用户的 Session 信息,当用户数量庞大时,会占用大量的资源,影响系统的性能。

5,从键入网址到网页显示,期间发生了什么?

从键入网址到网页显示,期间主要发生了以下过程:

浏览器处理阶段

  1. URL 解析:浏览器首先对输入的网址(URL)进行解析,将其分解为协议、域名、路径、查询参数等部分。例如,对于网址 “https://www.example.com/index.html?param=value”,浏览器会解析出协议为 “https”,域名是 “www.example.com”,路径为 “/index.html”,查询参数为 “param=value”3。
  2. 缓存检查:浏览器会检查自身的缓存,看是否有之前访问过的该网址对应的资源。如果有,且资源未过期,浏览器会直接从缓存中获取并显示,不再向服务器发送请求。浏览器缓存的查找顺序通常是 Service Worker、Memory Cache、Disk Cache、Push Cache2。

DNS 解析阶段

  1. 本地查询:如果浏览器缓存中没有找到对应的 IP 地址,浏览器会检查本地的 Hosts 文件,看是否有域名与 IP 地址的映射关系。如果 Hosts 文件中也没有,就会向本地 DNS 服务器发送查询请求1。
  2. 递归查询:本地 DNS 服务器收到请求后,首先会检查自己的缓存。如果没有找到,它会依次向更高级别的 DNS 服务器发起递归查询,直到找到对应的 IP 地址或者查询到根域名服务器仍无法找到为止23。

建立连接阶段

  1. 获取端口号:在 DNS 解析得到目标服务器的 IP 地址后,浏览器需要确定访问服务器的端口号。如果是 HTTP 协议,默认端口号是 80;如果是 HTTPS 协议,默认端口号是 443。也可以在 URL 中指定其他端口号。
  2. TCP 连接建立:浏览器与服务器之间会进行 TCP 三次握手来建立连接。首先,浏览器向服务器发送一个 SYN 数据包,表示请求建立连接并告知服务器自己将在连接中发送数据的初始序列号;服务器收到后,会向浏览器发送 ACK 确认,并同时发送自己的 SYN 数据包,告知浏览器自己将在连接中发送数据的初始序列号;浏览器收到服务器的 ACK 和 SYN 后,向服务器发送 ACK 确认,至此 TCP 连接建立成功,双方可以开始传输数据24。

数据传输阶段

  1. HTTP 请求发送:TCP 连接建立后,浏览器会根据解析出的 URL 和用户的操作(如点击链接、提交表单等),按照 HTTP 协议的规定,构建 HTTP 请求报文并发送给服务器。请求报文中包含请求方法(如 GET、POST 等)、请求的资源路径、HTTP 版本号以及一些请求头信息(如浏览器类型、可接受的数据格式等)24。
  2. 服务器处理请求:服务器接收到浏览器发送的 HTTP 请求后,会对请求报文进行解析,根据请求的资源路径和其他参数,从服务器的文件系统、数据库或其他后端服务中获取相应的数据。这可能涉及到复杂的业务逻辑处理、数据库查询、文件读取等操作34。
  3. HTTP 响应返回:服务器在获取到请求的资源数据后,会按照 HTTP 协议的规定,构建 HTTP 响应报文并发送给浏览器。响应报文中包含响应状态码(如 200 表示成功、404 表示未找到资源等)、响应头信息(如内容类型、缓存控制信息等)以及实际的资源数据(如 HTML 文件、CSS 样式表、JavaScript 脚本、图片等)34。

页面渲染阶段

  1. 解析 HTML:浏览器收到服务器返回的 HTTP 响应后,首先会解析 HTML 文档,构建出 DOM 树,即文档对象模型树,它是 HTML 文档的树形表示,包含了网页中的所有元素和它们之间的结构关系。
  2. 加载外部资源:在解析 HTML 的过程中,浏览器会发现 HTML 文档中引用的外部资源,如 CSS 样式表、JavaScript 脚本、图片等,然后会根据资源的 URL 再次向服务器发送请求获取这些资源,并进行加载和解析。
  3. 渲染页面:浏览器根据 DOM 树和加载的 CSS 样式表,对网页进行布局和渲染,将各个元素按照其在 DOM 树中的位置和 CSS 样式的定义进行绘制和显示。同时,浏览器会执行 JavaScript 代码,实现网页的交互效果和动态功能,如点击事件、表单验证、数据加载等。

连接关闭阶段

当浏览器完成网页的渲染和所有资源的加载后,如果没有其他需要与服务器交互的操作,浏览器会向服务器发送一个 FIN 数据包,表示请求关闭 TCP 连接。服务器收到后,会向浏览器发送 ACK 确认,并在一段时间后也发送一个 FIN 数据包给浏览器。浏览器收到服务器的 FIN 后,发送 ACK 确认,然后双方关闭 TCP 连接.

6,三次握手四次挥手

“三次握手” 和 “四次挥手” 是 TCP(传输控制协议)中用于建立连接和断开连接的重要机制。

三次握手(建立连接)

  1. 第一次握手(SYN)
    • 客户端行为:客户端想要建立连接时,会向服务器发送一个带有 SYN(同步序列号)标志位的 TCP 报文段。这个报文段中还包含一个初始序列号(Sequence Number,ISN),它是一个随机生成的 32 位数字,用于后续的数据传输顺序编号。例如,假设客户端生成的初始序列号为 x,那么这个报文段可以表示为 SYN = 1,SEQ = x。
    • 目的:客户端通过这个报文段向服务器表明自己想要建立连接的意图,并告知服务器自己将从这个初始序列号开始发送数据。
  2. 第二次握手(SYN + ACK)
    • 服务器行为:服务器收到客户端的 SYN 报文段后,会返回一个 SYN - ACK 报文段。这个报文段中,SYN 标志位仍然为 1,表示服务器也同意建立连接,同时 ACK 标志位为 1,表示这是对客户端 SYN 报文段的确认。服务器也会生成自己的初始序列号(假设为 y),并且确认号(Acknowledgment Number)设置为客户端的初始序列号加 1,即 x + 1。所以这个报文段可以表示为 SYN = 1,ACK = 1,SEQ = y,ACK = x + 1。
    • 目的:服务器向客户端表示同意建立连接,并告知自己的初始序列号,同时确认已经收到客户端的连接请求,确认号的设置是为了告诉客户端下一个期望收到的数据序列号。
  3. 第三次握手(ACK)
    • 客户端行为:客户端收到服务器的 SYN - ACK 报文段后,会发送一个 ACK 报文段进行确认。这个报文段的 ACK 标志位为 1,确认号设置为服务器的初始序列号加 1,即 y + 1,序列号为自己第一次握手时的初始序列号加 1(因为已经发送了一个字节的数据,序列号需要递增),假设为 x + 1。这个报文段可以表示为 ACK = 1,SEQ = x + 1,ACK = y + 1。
    • 目的:客户端通过这个报文段告诉服务器,自己已经收到了服务器的同意建立连接的回复,至此双方完成了三次握手,连接成功建立,可以开始进行数据传输。

四次挥手(断开连接)

  1. 第一次挥手(FIN)
    • 主动关闭方行为(假设是客户端):当客户端想要关闭连接时,会向服务器发送一个带有 FIN(结束)标志位的 TCP 报文段。这个报文段的序列号为客户端当前的序列号(假设为 m),表示客户端已经没有数据要发送了,希望断开连接。可以表示为 FIN = 1,SEQ = m。
    • 目的:客户端主动发起关闭连接的请求,通知服务器自己不再发送数据。
  2. 第二次挥手(ACK)
    • 被动关闭方行为(服务器):服务器收到客户端的 FIN 报文段后,会返回一个 ACK 报文段进行确认。确认号为客户端的序列号加 1,即 m + 1,序列号为服务器当前的序列号(假设为 n)。这个报文段可以表示为 ACK = 1,SEQ = n,ACK = m + 1。
    • 目的:服务器确认收到客户端的关闭连接请求,但此时服务器可能还有数据需要发送给客户端,所以先进行确认,等待自己的数据发送完毕。
  3. 第三次挥手(FIN)
    • 被动关闭方行为(服务器):当服务器发送完剩余的数据后,会向客户端发送一个 FIN 报文段,表示自己也没有数据要发送了,希望断开连接。这个报文段的序列号为服务器当前的序列号(假设为 p),可以表示为 FIN = 1,SEQ = p。
    • 目的:服务器通知客户端自己也准备好关闭连接了。
  4. 第四次挥手(ACK)
    • 主动关闭方行为(客户端):客户端收到服务器的 FIN 报文段后,会发送一个 ACK 报文段进行确认。确认号为服务器的序列号加 1,即 p + 1,序列号为客户端当前的序列号(假设为 q)。这个报文段可以表示为 ACK = 1,SEQ = q,ACK = p + 1。
    • 目的:客户端确认收到服务器的关闭连接请求,双方完成四次挥手,连接正式关闭。

三次握手和四次挥手的过程确保了 TCP 连接的可靠建立和有序关闭,通过序列号和确认号的机制,可以保证数据传输的准确性和完整性,同时也能让双方在连接的建立和关闭过程中有足够的沟通和协调。

7,什么是I/O多路复用?解决了什么问题?使用场景是什么?

  1. I/O 多路复用的定义
    • I/O 多路复用(I/O Multiplexing)是一种 I/O 处理机制,它允许一个进程(或线程)同时监视多个文件描述符(File Descriptor,FD),这些文件描述符可以是套接字(Socket)、管道(Pipe)、终端设备等。当这些文件描述符中的任何一个变为可读、可写或出现异常等事件时,I/O 多路复用机制能够通知程序进行相应的 I/O 操作。
    • 常见的 I/O 多路复用技术实现方式有三种:selectpollepoll(在 Linux 系统下)。它们的基本原理是相似的,但在性能、使用方式等方面存在一些差异。
  2. 解决的问题
    • 资源浪费问题:在传统的阻塞 I/O 模型中,当一个进程(或线程)对一个文件描述符进行 I/O 操作(如读取网络数据)时,如果数据没有准备好,进程会被阻塞,直到数据可读。如果要同时处理多个 I/O 操作,就需要为每个 I/O 操作创建一个进程(或线程)。这样会导致大量的进程(或线程)处于阻塞状态,浪费系统资源(如内存和 CPU 时间)。例如,一个服务器需要同时处理多个客户端的连接请求,若采用传统阻塞 I/O 模型,为每个客户端创建一个线程,当客户端数量众多时,线程切换的开销会很大,并且大量线程处于等待 I/O 的状态,系统资源利用率很低。
    • 效率问题:在非阻塞 I/O 模型中,虽然进程不会被阻塞,但需要不断地轮询(Polling)文件描述符,查看是否有 I/O 事件发生。这种方式会导致 CPU 资源的浪费,因为大部分轮询操作可能都是徒劳的,没有 I/O 事件发生。I/O 多路复用通过让内核负责监视多个文件描述符,当有事件发生时才通知应用程序,避免了无意义的轮询,提高了效率。
  3. 使用场景
    • 网络服务器编程:如 Web 服务器、邮件服务器等。以 Web 服务器为例,服务器需要同时处理大量客户端的连接请求,接收和发送 HTTP 请求和响应。使用 I/O 多路复用技术,服务器可以在一个线程(或少量线程)中同时监视多个客户端连接的套接字,当有客户端发送请求或者接收缓冲区可写时,能够及时进行处理,高效地利用系统资源,提高服务器的并发处理能力。
    • 高性能的网络代理和负载均衡器:这些系统需要同时处理多个后端服务器和大量客户端之间的连接和数据传输。I/O 多路复用可以帮助它们有效地管理众多的连接,根据后端服务器的负载情况和客户端请求的特点,合理地分配 I/O 资源,确保数据的高效转发和负载均衡。
    • 数据库服务器:数据库服务器需要处理来自多个客户端的查询请求、事务处理等操作。通过 I/O 多路复用,可以高效地监视客户端连接和数据库内部的 I/O 操作(如数据文件的读写),提升数据库服务器的整体性能和并发处理能力,减少客户端请求的响应时间。
    • 需要同时处理多种 I/O 设备的系统软件:例如,一个同时与多个外部设备(如传感器、打印机等)通信的监控系统,这些设备通过不同的接口(如串口、USB 等)连接到计算机,每个接口可以看作一个文件描述符。I/O 多路复用可以让系统软件同时监视这些设备的输入输出状态,当有设备发送数据或者需要接收数据时,能够及时进行处理,保证系统的实时性和可靠性。

8,登录鉴权你采用的方案是什么呢?优势和劣势分别是什么呢?你还能想到其他的方案进行鉴权么?

  1. 常见的登录鉴权方案 - 基于 Cookie - Session 的鉴权
    • 方案描述
      • 当用户首次登录时,服务器验证用户的凭据(如用户名和密码)。如果验证通过,服务器会创建一个 Session 对象,这个对象存储在服务器端,并且会生成一个唯一的 Session ID。然后服务器将包含 Session ID 的 Cookie 发送给客户端浏览器。
      • 客户端在后续的请求中会自动带上这个 Cookie。服务器收到请求后,通过读取 Cookie 中的 Session ID,在服务器端查找对应的 Session 对象,以此来确定用户的身份和状态,从而实现鉴权。
    • 优势
      • 简单易用:对于开发者来说,这种方式相对容易理解和实现。许多 Web 框架都提供了内置的支持,如 Java 中的 Spring 框架,它可以方便地处理 Cookie 和 Session 的创建、管理和验证。
      • 状态管理方便:可以在 Session 对象中存储用户相关的各种信息,如用户权限、购物车内容等,方便在不同的页面请求之间共享和管理用户状态。
    • 劣势
      • 可扩展性问题:在分布式系统或者集群环境中,由于 Session 存储在服务器端,需要考虑 Session 的共享和一致性问题。如果多个服务器处理用户请求,可能会出现某个服务器上没有用户对应的 Session 的情况,需要使用额外的技术(如分布式 Session 存储)来解决。
      • 安全性风险:如果 Cookie 被窃取,攻击者可能会冒充用户身份。虽然可以通过设置 Cookie 的属性(如 HttpOnly、Secure)来增强安全性,但仍然存在一定风险。而且 Session ID 如果没有合理的加密和保护措施,也可能被猜出或者篡改。
      • 性能开销:服务器需要维护每个用户的 Session 对象,当用户数量庞大时,会占用大量的内存资源。并且每次请求都需要查询和验证 Session,会增加一定的处理开销。
  2. 其他登录鉴权方案 - 基于 Token 的鉴权(JWT - JSON Web Token)
    • 方案描述
      • 用户登录成功后,服务器会生成一个包含用户信息(如用户 ID、权限等)的 Token,这个 Token 通常是一个经过加密和签名的 JSON 对象。服务器将 Token 返回给客户端,客户端可以将 Token 存储在本地(如 Local Storage 或 Cookie 中,不过存储在 Cookie 中有一些跨域等问题需要注意)。
      • 客户端在后续请求中,将 Token 放在请求头(如 Authorization 头)中发送给服务器。服务器收到请求后,会验证 Token 的合法性(包括签名是否正确、是否过期等),如果 Token 有效,就可以从 Token 中获取用户信息,从而实现鉴权。
    • 优势
      • 无状态性:服务器不需要存储用户的会话状态,减轻了服务器的存储负担,在分布式系统和微服务架构中更容易实现扩展和负载均衡。因为每个请求的鉴权只依赖于 Token 本身,而不是服务器端的会话存储。
      • 跨平台和跨域支持较好:由于 Token 是一种自包含的格式,只要客户端和服务器遵循相同的 Token 验证规则,就可以在不同的平台(如 Web、移动端)和不同的域名之间方便地传递和验证,便于实现单点登录等功能。
    • 劣势
      • 管理复杂:Token 的生成、验证和管理需要一定的安全机制,如加密算法的选择、密钥的管理等。如果处理不当,可能会导致安全漏洞。而且一旦 Token 发放出去,很难进行撤销(虽然有一些复杂的解决方案,但会增加系统的复杂性)。
      • 数据大小限制:如果在 Token 中存储了过多的用户信息,会导致 Token 的体积变大,增加网络传输的负担,并且可能会受到一些系统(如 HTTP 请求头长度限制)的限制。
  3. 基于 OAuth 的鉴权(以 OAuth 2.0 为例)
    • 方案描述
      • 主要用于第三方授权登录场景。例如,用户使用微信账号登录一个第三方应用。第三方应用会引导用户到微信的授权服务器,用户在微信授权服务器上授权第三方应用获取自己的部分信息(如基本用户信息、头像等)。
      • 微信授权服务器会向第三方应用颁发一个访问令牌(Access Token),第三方应用使用这个访问令牌向微信的资源服务器(如获取用户信息的服务器)请求用户的相关资源,以此来实现登录鉴权和获取用户信息。
    • 优势
      • 用户体验好:用户可以使用已有的第三方账号(如微信、QQ、支付宝等)进行登录,不需要在每个应用中单独注册和记忆账号密码,方便快捷。
      • 安全性较高:通过授权码(Authorization Code)等中间步骤,以及严格的令牌管理机制,确保了用户信息的安全性。第三方应用只能获取用户授权的部分信息,并且访问令牌有一定的有效期和权限范围限制。
    • 劣势
      • 实现复杂:对于开发者来说,需要理解和实现 OAuth 协议的多个流程和端点(如授权端点、令牌端点等),涉及到多个角色(用户、客户端应用、授权服务器、资源服务器)之间的交互,开发和维护成本较高。
      • 依赖第三方服务:完全依赖第三方服务提供商的稳定性和安全性。如果第三方服务出现故障或者安全漏洞,可能会影响到用户的登录和数据安全。而且第三方服务提供商可能会对应用的使用进行限制或者收费。

9,JWT的token的数据组成有哪些?

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。一个典型的 JWT 由三部分组成,这三部分通过 “.” 拼接在一起,格式为 “头部(Header). 负载(Payload). 签名(Signature)”。

1. 头部(Header)

  • 功能:主要用于描述关于该 JWT 的元数据,包括令牌的类型(JWT)以及所使用的签名算法,如 HMAC SHA256 或者 RSA 等。
  • 数据结构:是一个 JSON 对象,例如:
{"alg": "HS256","typ": "JWT"
}

其中,alg字段指定了签名算法(在这个例子中是 HMAC SHA256),typ字段表示令牌类型是 JWT。这个 JSON 对象会经过 Base64Url 编码后成为 JWT 的第一部分。

2. 负载(Payload)

  • 功能:也称为声明(Claims)部分,包含了要传递的实际信息,这些信息是关于用户或者其他实体的声明。声明可以分为三种类型:注册声明(Registered Claims)、公共声明(Public Claims)和私有声明(Private Claims)。

  • 数据结构

    :同样是一个 JSON 对象。

    • 注册声明

      :这些是 JWT 标准中预先定义的一些常用声明,包括:

      • iss(Issuer):令牌的发行者,通常是一个 URL,用于标识发布这个 JWT 的实体。
      • sub(Subject):主题,通常是一个用户 ID 或者其他唯一标识符,表示这个 JWT 所涉及的主体。
      • aud(Audience):受众,是一个接收 JWT 的目标受众列表(可以是一个或多个实体),通常也是通过 URL 或者其他标识符来表示。
      • exp(Expiration Time):过期时间,是一个 Unix 时间戳,表示 JWT 在这个时间之后就无效了。这有助于防止令牌被无限期地使用,增强安全性。
      • nbf(Not Before):生效时间,也是一个 Unix 时间戳,表示 JWT 在这个时间之前是无效的。
      • iat(Issued At):发布时间,记录 JWT 发布的时间戳,用于跟踪令牌的生命周期等用途。
    • 公共声明:这些声明是由使用 JWT 的各方共同定义的,不是 JWT 标准强制要求的,但是通常用于在不同系统之间传递一些通用的信息,比如用户的角色(role)、权限级别(permission_level)等。

    • 私有声明:这些是由应用程序开发者自定义的声明,用于在特定的应用场景中传递一些私有信息,比如应用内部的用户部门(department)、用户偏好(user_preferences)等。例如:

{"sub": "1234567890","name": "John Doe","role": "admin","department": "IT"
}

这个负载部分的 JSON 对象也会经过 Base64Url 编码后成为 JWT 的第二部分。

3. 签名(Signature)

  • 功能:签名是为了验证消息在传递过程中没有被篡改,并且确保令牌是由可信的发行者发布的。它是通过对头部和负载进行特定的加密算法计算得到的。

  • 计算方式

    :使用头部中指定的签名算法(

    alg
    

    字段),将编码后的头部、编码后的负载和一个密钥(secret)作为输入进行计算。例如,如果头部指定的算法是 HS256(HMAC SHA256),密钥是

    my_secret_key
    

    ,签名的计算过程大致如下:

    • 首先,将 Base64Url 编码后的头部(header)和 Base64Url 编码后的负载(payload)用 “.” 拼接起来,得到一个字符串encoded_string = header + '.' + payload
    • 然后,使用 HMAC SHA256 算法和密钥my_secret_keyencoded_string进行加密计算,得到签名部分signature
      这个签名部分会与前面的头部和负载拼接在一起,形成完整的 JWT。在接收端验证 JWT 时,会使用相同的算法和密钥重新计算签名,如果计算得到的签名与接收到的签名一致,并且其他验证(如过期时间等)也通过,就可以认为这个 JWT 是有效的。

10,热点Key解决方案有哪些?

  1. 本地缓存 + 分布式缓存双层架构
    • 方案描述
      • 本地缓存(如 Guava Cache):在应用服务器本地设置一层缓存。当有请求查询热点数据时,首先在本地缓存中查找。本地缓存的数据存储在应用服务器的内存中,访问速度极快。
      • 分布式缓存(如 Redis):如果本地缓存未命中,再从分布式缓存中查询。分布式缓存存储在独立的缓存服务器集群中,能存储大量数据,并且数据可以在多个应用服务器之间共享。当分布式缓存中的热点数据被频繁访问时,将其同步一份到本地缓存,这样后续相同的请求就可以直接从本地缓存获取,减少对分布式缓存的压力。
    • 优势
      • 性能提升明显:本地缓存能够快速响应大部分热点数据请求,减少了网络延迟和分布式缓存的访问压力,极大地提高了系统的整体性能。
      • 灵活性高:可以根据应用服务器的内存大小和热点数据的特点灵活调整本地缓存的大小和过期策略等。
    • 劣势
      • 数据一致性维护复杂:当分布式缓存中的热点数据更新时,需要考虑如何及时更新本地缓存中的数据副本,以保证数据的一致性。可能需要采用复杂的缓存更新策略,如基于消息队列的异步更新等。
      • 本地缓存占用内存:如果本地缓存设置过大,可能会占用过多的应用服务器内存,影响服务器上其他应用程序的运行;如果设置过小,又不能充分发挥本地缓存的优势。
  2. 分布式缓存集群的优化策略
    • 方案描述
      • 数据分片(Sharding):将热点数据按照一定的规则(如哈希算法)分散存储到多个缓存节点上。例如,在 Redis 集群中,可以使用一致性哈希算法对热点数据的键(Key)进行分片,使得每个缓存节点负责一部分数据的存储和查询。这样可以避免单个缓存节点因承受过多热点数据请求而成为性能瓶颈。
      • 热点数据分区(Partitioning):将缓存空间划分为热点分区和非热点分区。可以通过对数据的访问频率进行统计分析,把频繁访问的热点数据集中存储在热点分区,并且为热点分区分配更多的资源(如内存、CPU 等)。在访问缓存时,先判断数据是否属于热点分区,然后针对性地进行查询,提高热点数据的访问效率。
    • 优势
      • 可扩展性强:通过数据分片可以方便地扩展缓存集群的规模,增加缓存节点来应对不断增长的热点数据访问量。
      • 高效利用资源:热点数据分区能够确保资源优先分配给热点数据,使得缓存系统能够更高效地利用有限的资源来处理高并发的热点数据请求。
    • 劣势
      • 实现复杂:数据分片和热点数据分区都需要复杂的算法和配置来实现。例如,一致性哈希算法的实现和维护需要一定的技术水平,并且在缓存节点增减时,需要考虑数据的重新分片和迁移问题,否则可能会导致数据丢失或访问不一致的情况。
      • 管理成本高:需要对缓存集群进行精细的管理和监控,包括节点的状态、数据分布情况、访问频率统计等。这增加了系统的管理成本和运维难度。
  3. 使用缓存预热和预加载策略
    • 方案描述
      • 缓存预热(Warm - up):在系统启动或者业务低峰期,提前将可能成为热点的数据加载到缓存中。例如,对于一个电商系统,可以在每天凌晨,将当天即将上架的热门商品信息、促销活动信息等加载到缓存。可以通过定时任务或者数据迁移工具来实现缓存预热。
      • 预加载(Pre - loading):在应用程序运行过程中,根据业务规则和数据访问模式,提前预测并加载可能会成为热点的数据。比如,当用户查看某一类商品的详情页时,系统预测用户可能还会查看该商品的评论和相关推荐商品,于是提前将这些相关数据加载到缓存中。
    • 优势
      • 减少冷启动时间:缓存预热可以确保系统在面对热点数据请求时,不会因为缓存未命中而导致响应延迟,尤其是在系统刚启动或者新功能上线时,能够快速提供服务。
      • 提高用户体验:预加载策略能够提前准备好用户可能需要的数据,减少用户等待时间,提升用户体验。
    • 劣势
      • 资源浪费风险:如果预测的热点数据不准确,可能会导致缓存中加载了大量无用的数据,占用缓存空间和网络资源,反而影响系统性能。
      • 数据更新问题:在预热和预加载后,如果数据发生更新,需要及时更新缓存中的数据,否则可能会向用户提供过时的信息。这增加了数据更新的复杂性和及时性要求。

11,Redis常见的数据结构有哪些?底层描述。

  1. 字符串(String)
    • 功能及应用场景
      • 字符串是 Redis 中最基本的数据结构,它可以存储任何形式的字符串,包括整数、浮点数和文本数据。在实际应用中,常用于存储用户的基本信息(如用户名、密码等)、计数器(如网站访问量、点赞数等)、分布式锁的标识等。
    • 底层数据结构及原理
      • Redis 中的字符串是一个简单动态字符串(Simple Dynamic String,SDS)结构。SDS 是一种类似于 C 语言中的字符串数组,但它在原有基础上进行了优化。它包含了一个 len 字段用于记录字符串的长度,一个 alloc 字段用于记录分配的内存空间大小,以及一个字符数组 buf 用于存储实际的字符串内容。
      • 当对字符串进行修改操作(如追加、截断等)时,SDS 会根据需要自动扩展或收缩内存空间。这种动态分配内存的方式避免了 C 语言中字符串操作可能导致的缓冲区溢出问题。而且,SDS 的 len 字段使得获取字符串长度的操作时间复杂度为 O (1),而不像 C 语言中需要遍历整个字符串来计算长度。
  2. 列表(List)
    • 功能及应用场景
      • 列表是一个有序的字符串列表,可以在头部和尾部进行插入和删除操作。它常用于实现消息队列、文章列表、评论列表等功能。例如,在一个简单的社交平台中,可以用列表存储用户发布的动态消息,新消息可以添加到列表头部,而获取消息列表就是获取整个列表的内容。
    • 底层数据结构及原理
      • Redis 3.2 之前,列表的底层实现是一个双向链表和压缩列表(ziplist)。当列表元素较少且每个元素的长度较小时,使用压缩列表;当元素较多或者元素长度较大时,会转换为双向链表。
      • 双向链表的每个节点包含了指向前一个节点和后一个节点的指针,以及存储元素的数据部分。这种结构使得在头部和尾部进行插入和删除操作非常方便,时间复杂度为 O (1),但查找元素的时间复杂度为 O (n)。
      • 压缩列表是一种为了节省内存而设计的连续内存结构。它将多个元素紧凑地存储在一起,通过不同的编码来表示元素的长度、前一个元素的长度等信息。虽然压缩列表可以节省内存,但在进行一些操作(如插入或删除中间元素)时,可能需要对整个列表进行重新排列,效率相对较低。
      • Redis 3.2 之后,引入了快速链表(quicklist)作为列表的底层实现。快速链表是一个双向链表,每个节点是一个压缩列表。这种结构结合了双向链表的高效插入删除特性和压缩列表的内存节省优势,使得列表在各种场景下都能有较好的性能表现。
  3. 哈希(Hash)
    • 功能及应用场景
      • 哈希用于存储键值对(field - value)的集合,适合存储对象的属性信息。例如,在存储用户信息时,可以将用户的姓名、年龄、性别等属性作为 field - value 对存储在一个哈希中。这样可以方便地对用户的某个属性进行单独修改和查询,而不需要获取整个用户对象。
    • 底层数据结构及原理
      • Redis 中的哈希底层可以是压缩列表(ziplist)或者哈希表(hashtable)。当哈希中的键值对数量较少且每个键值对的长度较小时,使用压缩列表;当键值对数量较多或者长度较大时,会转换为哈希表。
      • 哈希表是一个典型的数组 + 链表的结构。它通过一个哈希函数将键(field)映射到数组的一个索引位置。如果多个键映射到同一个索引位置(即发生哈希冲突),则在该位置形成一个链表,将这些冲突的键值对存储在链表中。在查询时,先通过哈希函数找到索引位置,然后在链表中查找对应的键值对。这种结构在平均情况下,查找、插入和删除操作的时间复杂度为 O (1),但在最坏情况下(所有键都映射到同一个索引位置),时间复杂度会退化为 O (n)。
  4. 集合(Set)
    • 功能及应用场景
      • 集合是一个无序的、不包含重复元素的字符串集合。它常用于实现标签系统、好友关系、共同关注等功能。例如,在一个社交应用中,可以用集合存储用户的标签,或者用户关注的其他用户列表,通过集合的交、并、差等操作可以方便地实现推荐系统等功能。
    • 底层数据结构及原理
      • Redis 中的集合有两种底层实现方式:整数集合(intset)和哈希表(hashtable)。
      • 当集合中的元素都是整数且数量较少时,使用整数集合。整数集合是一个有序的整数数组,它可以根据元素的大小动态调整数组的类型(如从 int16_t 升级到 int32_t)以节省内存。在整数集合中插入和删除元素时,需要保持数组的有序性,时间复杂度为 O (n),但由于元素是有序的,查找操作的时间复杂度可以达到 O (log n)。
      • 当集合中的元素不是整数或者元素数量较多时,会转换为哈希表。哈希表的原理与哈希数据结构中的哈希表类似,通过哈希函数将元素映射到不同的位置,以实现快速的插入、删除和查找操作,时间复杂度在平均情况下为 O (1)。
  5. 有序集合(Sorted Set)
    • 功能及应用场景
      • 有序集合是一个集合,其中的每个元素都关联了一个分数(score),集合中的元素根据分数进行有序排列。它常用于实现排行榜、优先级队列等功能。例如,在一个游戏中,可以用有序集合存储玩家的分数,根据分数高低对玩家进行排名;或者在一个任务调度系统中,根据任务的优先级(作为分数)来安排任务的执行顺序。
    • 底层数据结构及原理
      • Redis 中的有序集合底层是通过跳跃表(skiplist)和哈希表(hashtable)来实现的。
      • 跳跃表是一种多层的有序链表结构,它通过在原始链表的基础上添加多级索引来提高查找效率。在跳跃表中,查找、插入和删除操作的时间复杂度在平均情况下为 O (log n)。
      • 哈希表用于存储元素和分数之间的映射关系,以便快速查找元素对应的分数。通过跳跃表和哈希表的结合,有序集合既能够快速地根据分数对元素进行排序,又能够快速地查找元素对应的分数,提供了高效的操作性能。

12,Bitmap和布隆过滤器的区别是什么?

  1. 数据结构和原理
    • Bitmap(位图)
      • 定义和结构:Bitmap 是一种简单的数据结构,它使用一个二进制位(bit)来表示一个数据元素的状态,通常是 0 或 1。例如,如果要表示一个包含 100 个元素的集合,只需要 100 位(100/8 = 12.5 字节)的空间,每个位对应一个元素。如果位的值为 1,表示该元素存在;为 0,表示不存在。
      • 操作原理:它的基本操作主要是位运算。比如,要检查某个元素是否存在,只需查看对应的位的值;要添加或删除一个元素,就将对应的位设置为 1 或 0。在实际应用中,常用于对大量整数进行快速的存在性检查,比如统计用户的登录天数(每天对应一位,登录为 1,未登录为 0)。
    • 布隆过滤器(Bloom Filter)
      • 定义和结构:布隆过滤器是一种概率型数据结构,它由一个位数组(bit array)和多个哈希函数组成。位数组的初始值全部为 0。当要插入一个元素时,通过多个哈希函数对该元素进行计算,得到多个哈希值,这些哈希值对应位数组中的位置,将这些位置的位设置为 1。
      • 操作原理:在检查一个元素是否存在时,同样使用这些哈希函数对元素进行计算,得到对应的位置。如果这些位置的位全部为 1,则认为该元素可能存在;如果有任何一个位为 0,则该元素一定不存在。但是,由于哈希冲突的存在,可能会出现误判(即一个元素实际上不存在,但检查时认为它可能存在)。
  2. 空间占用和效率
    • 空间占用
      • Bitmap:空间占用取决于元素的范围。如果要表示范围是从 0 到 n - 1 的整数集合,需要 n 位的空间。例如,要表示 0 - 1000 的整数是否存在,需要 1000 位(约 125 字节)。它的空间占用是精确的,与实际存储的元素数量无关,只与元素的范围有关。
      • 布隆过滤器:空间占用主要由位数组的大小和哈希函数的数量决定。位数组越大,哈希函数越多,误判率越低,但空间占用也越大。通常可以根据预期插入的元素数量和可接受的误判率来计算合适的位数组大小和哈希函数数量。在实际应用中,对于相同数量的元素,布隆过滤器的空间占用可能比精确存储(如使用哈希表)要小很多。
    • 检查效率
      • Bitmap:检查一个元素是否存在的时间复杂度是 O (1),因为只需要进行一次位运算。例如,在一个存储用户 ID 是否活跃的 Bitmap 中,检查某个用户 ID 是否活跃,只需要找到对应的位并查看其值,操作非常快速。
      • 布隆过滤器:检查一个元素是否存在的时间复杂度也近似为 O (1)。它需要对元素进行多个(通常是固定数量)哈希函数计算,然后检查对应的位,但由于哈希函数计算和位检查的操作都很快,所以整体效率也很高。不过,由于可能存在误判,需要在一些对准确性要求极高的场景中谨慎使用。
  3. 准确性和应用场景
    • 准确性
      • Bitmap:只要操作正确,Bitmap 对于元素存在性的判断是准确的。如果位为 1,表示元素存在;为 0,表示不存在,不存在误判的情况。
      • 布隆过滤器:布隆过滤器存在一定的误判率。它只能判断一个元素可能存在,而不能确定其一定存在。这种概率性的判断是由其结构和操作原理决定的,因为多个元素可能通过哈希函数映射到相同的位。
    • 应用场景
      • Bitmap:适用于对元素存在性判断要求准确无误的场景,并且元素的范围相对固定且明确。比如,统计用户的签到情况、存储某些范围内的整数是否被使用等。
      • 布隆过滤器:适用于对空间要求比较严格,允许一定概率的误判,并且主要用于快速判断元素不存在的场景。例如,在缓存系统中,用于判断一个请求是否可能在缓存中(避免大量不存在的请求访问底层存储系统);在网络爬虫中,用于判断一个 URL 是否已经被访问过等。

13,你了解过跳表吗?压缩列表?

  1. 跳表(Skip List)
    • 定义和结构
      • 跳表是一种有序的数据结构,它在原有的有序链表基础上增加了多级索引,以提高查找效率。可以将其想象成一个带有 “跳板” 的链表。在最底层是一个完整的有序链表,存储了所有的数据元素。然后在这个链表之上,每隔一定数量的节点就会构建一层索引,索引节点只包含了指向下一层节点的指针和关键字。每一层的节点数量都比下一层少,最顶层的索引节点数量最少。
    • 操作原理
      • 查找操作:当进行查找时,从最顶层的索引开始。如果当前索引节点的关键字小于要查找的关键字,就沿着索引节点的指针向右移动;如果当前索引节点的关键字大于要查找的关键字,就下降到下一层继续查找。通过这种方式,可以快速跳过链表中的许多节点,减少比较次数。例如,在一个存储整数的跳表中查找元素 100,如果最顶层索引节点的值为 50、150,那么首先比较发现 100 大于 50,就向右移动,遇到 150 后发现大于 100,就下降到下一层继续查找,这样就跳过了很多小于 100 的节点。
      • 插入操作:插入一个新元素时,首先要找到合适的插入位置,这个过程和查找类似。找到位置后,将新元素插入到最底层的链表中。同时,有一定的概率(通常是固定的概率,如 0.5)将新元素提升到上一层索引中,这个概率决定了索引的层数和密度。
      • 删除操作:删除操作相对复杂一些。首先要在跳表中找到要删除的元素,然后将其从最底层链表中删除。接着,需要检查每一层索引,看是否需要删除对应的索引节点,以保持跳表的结构完整性。
    • 时间复杂度和空间复杂度
      • 在平均情况下,跳表的查找、插入和删除操作的时间复杂度都是 O (log n),其中 n 是元素的数量。这是因为每一层索引都可以跳过一部分元素,使得搜索路径的长度大致为对数级别。空间复杂度方面,由于有多层索引,跳表的空间复杂度是 O (n log n),不过在实际应用中,可以通过调整索引的层数和节点密度来平衡空间和时间的关系。
    • 应用场景
      • 跳表主要用于需要快速查找的有序数据集合。在 Redis 的有序集合(Sorted Set)中,底层就是使用跳表和哈希表来实现的。它能够在保证元素有序的情况下,快速地进行查找、插入和删除操作,适用于排行榜系统(如游戏分数排名)、优先级队列等场景。
  2. 压缩列表(ziplist)
    • 定义和结构
      • 压缩列表是一种为了节省内存而设计的连续内存结构。它由一系列不同编码的字段组成,这些字段用于存储列表中的元素以及一些元数据。一个典型的压缩列表包含以下几个部分:
      • 表头信息:包括列表长度(即元素个数)和列表末端(用于标记列表结束位置)的偏移量等信息,这些信息用于快速定位和遍历列表。
      • 元素内容:每个元素按照一定的编码规则存储在连续的内存空间中。元素的长度可以是不同的,并且根据元素的类型和长度采用不同的编码方式。例如,对于较小的整数元素,可能会采用更紧凑的编码,以节省空间。
      • 元素之间没有指针连接,而是通过元素的长度和偏移量来确定相邻元素的位置,这使得整个结构更加紧凑。
    • 操作原理
      • 查找操作:由于压缩列表是连续的内存结构,要查找一个元素,需要从表头开始,根据元素的长度和偏移量逐个遍历元素,直到找到目标元素或者遍历完整个列表。这种查找方式在元素数量较多或者列表较长时效率相对较低。
      • 插入和删除操作:插入和删除操作可能会导致整个压缩列表的重新排列。当插入一个元素时,需要移动插入位置之后的所有元素,为新元素腾出空间;删除一个元素时,需要将后面的元素向前移动,填充被删除元素的空间。这两种操作的时间复杂度都可能达到 O (n),其中 n 是元素的数量,尤其是在列表中间进行插入或删除操作时,性能开销较大。
    • 时间复杂度和空间复杂度
      • 查找操作的时间复杂度在最好情况下是 O (1)(如果要查找的元素是表头元素),但在平均和最坏情况下是 O (n)。插入和删除操作在平均和最坏情况下时间复杂度是 O (n)。空间复杂度方面,由于它是紧凑的连续内存结构,空间复杂度是 O (n),其中 n 是元素的数量,并且它在存储小元素和元素数量较少的情况下能够有效地节省内存。
    • 应用场景
      • 压缩列表在 Redis 中有广泛的应用。当 Redis 中的列表(List)、哈希(Hash)等数据结构中的元素较少且每个元素的长度较小时,会使用压缩列表作为底层实现。例如,在存储一些配置信息、小型的键值对集合等场景中,使用压缩列表可以有效地节省内存空间。

14,生产者-消费者如何实现(架构或源码)?

  1. 使用BlockingQueue实现生产者 - 消费者模式(Java)
java">import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;class Producer implements Runnable {private final BlockingQueue<Integer> queue;public Producer(BlockingQueue<Integer> queue) {this.queue = queue;}@Overridepublic void run() {try {for (int i = 0; i < 10; i++) {queue.put(i);System.out.println("生产者生产了数据: " + i);Thread.sleep(1000);}} catch (InterruptedException e) {Thread.currentThread().interrupt();}}
}class Consumer implements Runnable {private final BlockingQueue<Integer> queue;public Consumer(BlockingQueue<Integer> queue) {this.queue = queue;}@Overridepublic void run() {try {while (true) {Integer data = queue.take();System.out.println("消费者消费了数据: " + data);}} catch (InterruptedException e) {Thread.currentThread().interrupt();}}
}public class ProducerConsumerExample {public static void main(String[] args) {BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();Thread producerThread = new Thread(new Producer(queue));Thread consumerThread = new Thread(new Consumer(queue));producerThread.start();consumerThread.start();}
}

在这个 Java 示例中:

  • BlockingQueue介绍
    • BlockingQueue是一个支持阻塞操作的队列接口,在java.util.concurrent包中。它提供了一些方法,如puttake,使得生产者和消费者在队列满或空时能够自动阻塞,避免了复杂的条件判断和等待机制。
    • 在这个例子中,使用了LinkedBlockingQueue作为BlockingQueue的实现类,它是一个基于链表的阻塞队列。
  • 生产者(Producer类)
    • 构造函数接收一个BlockingQueue对象,用于存储生产的数据。
    • run方法中,通过queue.put(i)将数据放入队列。put方法在队列满时会阻塞生产者线程,直到队列有空间。
    • 每次放入数据后,打印生产信息,并通过Thread.sleep(1000)模拟生产数据的时间间隔。
  • 消费者(Consumer类)
    • 同样,构造函数接收BlockingQueue对象。
    • run方法中,使用queue.take()从队列中获取数据。take方法在队列为空时会阻塞消费者线程,直到队列中有数据。
    • 获取数据后,打印消费信息。
  • main函数部分
    • 创建了一个LinkedBlockingQueue对象,用于存储数据。
    • 然后分别创建生产者线程和消费者线程,并将队列对象传递给它们。
    • 最后启动这两个线程,实现了生产者 - 消费者模式。生产者不断生产数据放入队列,消费者不断从队列中获取数据进行消费。

以下是使用 Java 的synchronized关键字、wait方法以及notify方法(或notifyAll方法)手写的生产者 - 消费者模型代码示例。在这个示例中,我们创建一个简单的场景,生产者生产数字并放入共享缓冲区,消费者从缓冲区获取数字并消费。

java">class Buffer {private int value;private boolean isEmpty = true;// 生产者往缓冲区放入数据的方法public synchronized void put(int val) {while (!isEmpty) {try {wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}value = val;isEmpty = false;notifyAll();}// 消费者从缓冲区获取数据的方法public synchronized int get() {while (isEmpty) {try {wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}isEmpty = true;notifyAll();return value;}
}class Producer implements Runnable {private Buffer buffer;public Producer(Buffer buffer) {this.buffer = buffer;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {buffer.put(i);System.out.println("生产者生产了数据: " + i);try {Thread.sleep(1000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}
}class Consumer implements Runnable {private Buffer buffer;public Consumer(Buffer buffer) {this.buffer = buffer;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {int data = buffer.get();System.out.println("消费者消费了数据: " + data);try {Thread.sleep(1500);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}
}public class ProducerConsumerWithWaitNotify {public static void main(String[] args) {Buffer buffer = new Buffer();Thread producerThread = new Thread(new Producer(buffer));Thread consumerThread = new Thread(new Consumer(buffer));producerThread.start();consumerThread.start();}
}

以下是对上述代码的详细解释:

1. Buffer

  • 成员变量
    • private int value;:用于存储生产者放入缓冲区的数据。
    • private boolean isEmpty = true;:用于标记缓冲区当前是否为空,初始化为true表示一开始缓冲区是空的。
  • put方法(生产者放入数据的操作)
    • 首先使用synchronized关键字修饰,确保同一时刻只有一个线程(生产者或消费者)能访问缓冲区的这个方法,保证了数据的一致性。
    • while (!isEmpty)这个循环判断缓冲区是否为空,如果不为空(即已经有数据了,消费者还没取走),则调用wait方法让生产者线程进入等待状态,释放锁,等待消费者取走数据后被唤醒。
    • 当缓冲区为空时,将传入的参数val赋值给value,表示放入了新数据,然后将isEmpty设置为false,表示缓冲区有数据了,接着调用notifyAll方法唤醒所有因为调用wait方法而等待在这个对象上的线程(这里主要是唤醒可能在等待获取数据的消费者线程)。
  • get方法(消费者获取数据的操作)
    • 同样使用synchronized关键字修饰,保证线程安全。
    • while (isEmpty)循环判断缓冲区是否为空,如果为空则调用wait方法让消费者线程进入等待状态,释放锁,等待生产者放入数据后被唤醒。
    • 当缓冲区有数据时,先将isEmpty设置为true,表示取走数据后缓冲区又为空了,然后调用notifyAll方法唤醒可能在等待的生产者线程,最后返回取到的value数据。

2. Producer类(生产者线程类)

  • 实现了Runnable接口,在run方法中,通过循环 10 次来模拟生产 10 个数据。
  • 每次调用buffer.put(i)将数据i放入缓冲区,放入后打印生产信息,并且通过Thread.sleep(1000)模拟生产一个数据的时间间隔。

3. Consumer类(消费者线程类)

  • 同样实现Runnable接口,在run方法中,也是循环 10 次来模拟消费 10 个数据。
  • 每次调用buffer.get()从缓冲区获取数据,获取后打印消费信息,然后通过Thread.sleep(1500)模拟消费一个数据的时间间隔,这里消费者消费的时间间隔设置得比生产者生产的时间间隔长一点,更符合实际场景中不同操作的耗时差异情况。

4. main函数部分

  • 创建了一个Buffer对象,用于作为生产者和消费者之间共享的缓冲区。
  • 接着分别创建生产者线程和消费者线程,并将Buffer对象传递给它们,最后启动这两个线程,实现了基于waitnotify机制的生产者 - 消费者模型,让生产者不断生产数据放入缓冲区,消费者不断从缓冲区获取数据进行消费。

需要注意的是,在实际应用中,这种简单的模型可以根据具体需求进行扩展和优化,比如处理更多复杂的业务逻辑、支持多个生产者和消费者等情况。

15进程是如何通信的?

进程通信(IPC,Inter - Process Communication)是指在不同进程之间交换数据和信息的机制。以下是几种常见的进程通信方式:

1. 管道(Pipe)

  • 匿名管道(Anonymous Pipe)
    • 原理:匿名管道是一种半双工的通信方式,通常用于具有父子关系的进程之间通信。它在内核中创建一个缓冲区,这个缓冲区用于在两个进程之间传递数据。数据像水流一样从管道的一端流入,从另一端流出。例如,在 Linux 系统中,pipe函数用于创建一个匿名管道,它会返回两个文件描述符,一个用于读,一个用于写。
    • 使用场景:常见于命令行中管道操作,如ls -l | grep "txt"ls -l的输出通过管道传递给grep命令作为输入进行过滤。在编程中,父进程可以创建一个管道,然后通过fork函数创建子进程,子进程会继承父进程的文件描述符,从而实现父子进程之间的数据传输,比如父进程将一些配置信息发送给子进程。
  • 命名管道(Named Pipe)
    • 原理:命名管道是一种特殊类型的文件,它在文件系统中有一个名字,多个进程可以通过这个名字来打开管道进行通信。与匿名管道不同,它可以用于没有亲属关系的进程之间通信。当一个进程向命名管道写入数据时,另一个进程可以从管道中读取数据。例如,在 Windows 系统中,可以使用CreateNamedPipeConnectNamedPipe函数来创建和连接命名管道;在 Linux 系统中,使用mkfifo命令或函数来创建命名管道。
    • 使用场景:适用于在不同的应用程序之间进行数据交换。比如,一个服务器进程和多个客户端进程之间的通信,服务器进程创建命名管道,客户端进程通过管道名字连接到服务器进程进行数据交互,如发送请求和接收响应。

2. 消息队列(Message Queue)

  • 原理:消息队列是一个由操作系统内核维护的消息链表,它可以存储多个消息。进程可以向消息队列中发送消息(把消息添加到链表尾部),也可以从消息队列中接收消息(从链表头部获取消息)。消息队列有消息类型的属性,接收进程可以根据消息类型有选择地接收消息。例如,在 Linux 系统中,msgget函数用于创建或获取一个消息队列的标识符,msgsndmsgrcv函数分别用于发送和接收消息。
  • 使用场景:适用于需要在多个进程之间异步传递消息的场景。例如,在一个分布式系统中,不同的服务进程之间可能需要发送各种类型的消息,如任务调度消息、事件通知等,通过消息队列可以实现消息的可靠传递和缓冲,接收进程可以在自己方便的时候处理消息,而不需要实时响应。

3. 共享内存(Shared Memory)

  • 原理:共享内存是一种高效的进程通信方式。它允许多个进程共享同一块物理内存区域,这些进程可以直接读写这块内存中的数据,就好像这些数据是在自己的进程空间中一样。但是,为了避免多个进程同时访问共享内存时出现数据不一致的问题,通常需要配合信号量(Semaphore)等同步机制来进行访问控制。例如,在 Linux 系统中,shmget函数用于创建或获取共享内存段的标识符,shmat函数用于将共享内存段连接到进程的地址空间,这样进程就可以访问共享内存了。
  • 使用场景:在需要频繁地在多个进程之间交换大量数据的场景中非常有用。比如,在一个多媒体处理系统中,多个进程可能需要同时访问和处理同一段音频或视频数据,使用共享内存可以减少数据的复制开销,提高处理效率。

4. 信号量(Semaphore)

  • 原理:信号量是一个整型变量,它主要用于实现进程之间的同步和互斥。它可以被看作是一种资源的计数器,通过P操作(一般是wait操作)和V操作(一般是signal操作)来控制对共享资源的访问。P操作会将信号量的值减 1,如果信号量的值小于 0,则进程会被阻塞;V操作会将信号量的值加 1,如果信号量的值小于等于 0,则会唤醒一个被阻塞的进程。例如,在 Linux 系统中,semget函数用于创建或获取信号量集的标识符,semop函数用于对信号量进行操作。
  • 使用场景:在多个进程需要访问共享资源(如共享内存、打印机等)时,用于控制访问顺序和避免冲突。例如,在一个数据库系统中,多个进程可能需要同时访问数据库文件,通过信号量来控制每次只有一个进程能够进行写操作,从而保证数据的一致性。

5. 套接字(Socket)

  • 原理:套接字是一种用于不同主机之间进程通信的机制,它基于网络协议(如 TCP/IP 协议)。一个套接字由 IP 地址、端口号和协议类型来标识。进程可以通过创建套接字、绑定端口、监听连接等操作,在网络上与其他进程进行通信。例如,在网络编程中,socket函数用于创建一个套接字,bind函数用于将套接字绑定到一个本地地址和端口,listenaccept函数用于监听和接受客户端的连接请求,connect函数用于客户端连接到服务器。
  • 使用场景:广泛应用于网络应用程序开发,如 Web 服务器与浏览器之间的通信、电子邮件客户端与服务器之间的通信、即时通讯软件等。通过套接字,不同主机上的进程可以实现跨网络的通信,实现数据的传输和交互。

16,线程是如何通信的?

线程通信是指在同一个进程中的多个线程之间交换数据和信息的机制。以下是几种常见的线程通信方式:

1. 共享变量

  • 原理:线程共享所属进程的地址空间,因此可以通过访问和修改共享变量来实现通信。例如,在一个多线程的程序中,有一个全局变量count,多个线程可以对这个count进行读取和修改操作。但是,这种方式可能会导致数据竞争(Data Race)问题,即多个线程同时访问和修改共享变量时可能会产生不确定的结果。为了解决这个问题,通常需要配合使用同步机制。

  • 同步机制 - 互斥锁(Mutex)

    • 原理:互斥锁是一种简单的同步原语,用于保护共享变量。它的工作原理是,当一个线程想要访问共享变量时,它首先尝试获取互斥锁。如果锁没有被其他线程占用,该线程成功获取锁并可以访问共享变量;如果锁已经被其他线程占用,该线程会被阻塞,直到锁被释放。例如,在 Java 中,可以使用synchronized关键字或者ReentrantLock类来实现互斥锁。
    • 示例(以 Java 为例)
java">class SharedCounter {private int count = 0;private final Object lock = new Object();public void increment() {synchronized (lock) {count++;}}public int getCount() {synchronized (lock) {return count;}}
}

在这个示例中,incrementgetCount方法都使用了synchronized关键字来获取lock对象的互斥锁,以保证对count变量的安全访问。

  • 同步机制 - 读写锁(Read - Write Lock)

    • 原理:读写锁是一种更复杂的同步机制,它允许同时有多个线程对共享变量进行读取操作,但在有线程进行写入操作时,会阻塞其他线程(包括读线程和写线程)。读写锁适用于共享变量的读取操作频繁,而写入操作相对较少的场景。例如,在 Java 中,ReentrantReadWriteLock类提供了读写锁的实现。
    • 示例(以 Java 为例)
java">import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;class SharedData {private int data;private final ReadWriteLock lock = new ReentrantReadWriteLock();public void readData() {lock.readLock().lock();try {System.out.println("读取数据: " + data);} finally {lock.readLock().unlock();}}public void writeData(int newData) {lock.writeLock().lock();try {data = newData;System.out.println("写入数据: " + data);} finally {lock.writeLock().unlock();}}
}

在这个示例中,readData方法获取读锁来读取数据,writeData方法获取写锁来写入数据,从而实现了对data变量的高效读写控制。

2. 等待 - 通知机制

  • 原理:这种机制基于线程的等待(wait)和通知(notifynotifyAll)操作。在一个对象上,线程可以调用wait方法使自己进入等待状态,释放对象的锁。其他线程可以调用该对象的notify(唤醒一个等待线程)或notifyAll(唤醒所有等待线程)方法来通知等待线程继续执行。通常与共享变量结合使用,当共享变量满足一定条件时,通过通知机制来唤醒等待的线程。
  • 示例(以 Java 为例)
java">class MessageQueue {private String message;private boolean isEmpty = true;public synchronized void putMessage(String msg) {while (!isEmpty) {try {wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}message = msg;isEmpty = false;notifyAll();}public synchronized String getMessage() {while (isEmpty) {try {wait();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}isEmpty = true;notifyAll();return message;}
}

在这个示例中,putMessage方法用于生产消息,getMessage方法用于消费消息。当消息队列空时,消费者线程会等待;当消息队列满时,生产者线程会等待。通过notifyAll方法来唤醒等待的线程,实现了线程之间的通信和同步。

3. 信号量(Semaphore)

  • 原理:信号量用于控制对共享资源的访问,它可以看作是一个计数器。线程在访问共享资源之前,需要先获取信号量。如果信号量的值大于 0,线程可以获取信号量并访问资源,同时信号量的值减 1;如果信号量的值等于 0,线程需要等待,直到信号量的值大于 0。当线程完成对资源的访问后,会释放信号量,使信号量的值加 1。信号量可以用于实现线程之间的互斥和同步。
  • 示例(以 Java 为例)
java">import java.util.concurrent.Semaphore;class ResourceAccess {private final Semaphore semaphore;public ResourceAccess(int permits) {semaphore = new Semaphore(permits);}public void accessResource() throws InterruptedException {semaphore.acquire();try {// 访问共享资源的代码System.out.println("线程正在访问资源");} finally {semaphore.release();}}
}

在这个示例中,Semaphore对象的acquire方法用于获取信号量,release方法用于释放信号量,从而控制了对共享资源的访问,实现了线程之间的通信和协调。

4. 管道(Pipe)

  • 原理:在一些操作系统中,也可以像进程通信一样,通过管道来实现线程通信。管道提供了一种单向的数据流通方式,一个线程向管道写入数据,另一个线程从管道读取数据。不过这种方式在多线程编程中相对较少使用,因为共享变量的方式通常更简单直接。
  • 示例(以 Java 为例,使用PipedInputStreamPipedOutputStream
java">import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;class ThreadPipeCommunication {public static void main(String[] args) throws IOException {PipedOutputStream outputStream = new PipedOutputStream();PipedInputStream inputStream = new PipedInputStream(outputStream);Thread producerThread = new Thread(() -> {try {outputStream.write("Hello from producer".getBytes());outputStream.close();} catch (IOException e) {e.printStackTrace();}});Thread consumerThread = new Thread(() -> {try {byte[] buffer = new byte[1024];int length = inputStream.read(buffer);System.out.println("消费者收到消息: " + new String(buffer, 0, length));inputStream.close();} catch (IOException e) {e.printStackTrace();}});producerThread.start();consumerThread.start();}
}

在这个示例中,producerThread通过PipedOutputStream向管道写入数据,consumerThread通过PipedInputStream从管道读取数据,实现了两个线程之间的通信。

17,进程和线程的关系(从概念得出结论)?

  1. 概念

    • 进程:进程是资源分配的基本单位,它是一个具有独立功能的程序关于某个数据集合的一次运行活动。进程拥有自己独立的地址空间,包括代码段、数据段、堆和栈等。进程在操作系统的管理下运行,操作系统会为每个进程分配所需的资源,如内存、文件句柄、CPU 时间片等。例如,当你打开一个文本编辑器程序,操作系统就会为这个文本编辑器创建一个进程,这个进程有自己的内存空间来存储程序代码、打开文件的内容以及各种运行时的数据。
    • 线程:线程是进程内部的一个执行单元,是程序执行流的最小单元。一个进程可以包含多个线程,这些线程共享进程的资源,如地址空间、文件句柄等。线程有自己独立的栈空间用于存储局部变量和函数调用信息,但它们共享进程的堆空间和代码段。例如,在一个文本编辑器进程中,可能有一个线程负责接收用户的输入,另一个线程负责将用户输入的内容保存到文件中,这些线程共享文本编辑器进程的内存和文件资源。
  2. 关系结论

    • 包含关系

      • 一个进程可以包含多个线程,线程是进程的一部分。进程为线程提供了执行环境和资源支持,线程在进程的资源空间内运行。就好像一个工厂(进程)里有多个工人(线程),工厂为工人提供了工作场所、设备(资源),工人在工厂的范围内进行生产活动。
    • 资源共享与独立

      • 共享方面:线程共享进程的大部分资源,这使得线程之间的通信和数据共享相对容易。比如,多个线程可以直接访问进程中的共享变量,通过共享内存来交换信息,这比进程间通信要简单高效得多。因为进程间通信需要通过操作系统提供的复杂机制(如管道、消息队列、共享内存等,这些机制都需要进行系统调用并涉及到内核的参与)来实现,而线程共享进程的地址空间,所以可以直接读写共享变量(当然,为了保证数据安全,需要适当的同步机制)。
      • 独立方面:每个线程也有自己独立的一些资源。最主要的是线程拥有自己独立的栈,用于存储线程的局部变量和函数调用信息。这使得每个线程可以独立地执行自己的函数调用序列,而不会相互干扰。例如,不同的线程可以同时调用不同的函数,每个函数的局部变量存储在各自线程的栈中,不会混淆。
    • 调度与执行

      • 进程是操作系统进行资源分配的单位,而线程是操作系统调度执行的单位。操作系统在分配 CPU 时间片时,会将时间片分配给线程。也就是说,在多处理器或多核系统中,多个线程可以真正地并行执行,而进程只有在拥有多个线程或者多个进程在多个处理器 / 核上运行时才能实现并行。例如,一个拥有双核心处理器的计算机系统,一个进程中的两个线程可以分别在两个核心上同时执行,提高程序的执行效率;而如果只有一个进程且只有一个线程,那么在同一时刻这个程序只能在一个核心上运行。
    • 生命周期关联

      • 线程的生命周期依赖于进程。当一个进程结束时,它所包含的所有线程也会随之结束。就像工厂倒闭(进程结束),工厂里的工人(线程)自然就失业(结束)了。不过,线程也可以自己主动结束自己的生命周期,例如通过返回函数或者调用线程退出函数等方式,但这种情况下,只要进程还在运行,其他线程可能仍然继续工作。

http://www.ppmy.cn/embedded/149504.html

相关文章

Python软体中简化版MapReduce任务的实现:处理大量日志数据

Python软体中简化版MapReduce任务的实现:处理大量日志数据 引言 在大数据时代,日志数据的处理与分析变得尤为重要。无论是服务器日志、应用程序日志还是用户行为日志,如何高效地处理和分析这些数据是每个开发者和数据科学家面临的挑战。MapReduce是一种编程模型,能够有效…

突发!GitLab将停止对中国区用户提供GitLab.com账号服务

突发!GitLab将停止对中国区用户提供GitLab.com账号服务 近日,被视为全球第二大开源代码托管和项目管理平台的 GitLab 宣布其将对中国区用户停止提供 GitLab.com 账号服务,建议现有用户迁移到极狐。中国 IP 地址现在访问 GitLab.com 页面会弹出下面窗口且直接转到 about.git…

.net core 的字符串处理

Python基础 引言 Python是一种广泛使用的高级编程语言&#xff0c;由Guido van Rossum于1991年首次发布。其设计理念强调代码的可读性和简洁性&#xff0c;使得Python成为初学者和专业开发者的热门选择。Python支持多种编程范式&#xff0c;包括面向对象、过程式和函数式编程…

探索 DC-SDK:强大的 3D 地图开发框架

在现代 Web 开发中&#xff0c;地理信息系统&#xff08;GIS&#xff09;和 3D 地图可视化变得越来越重要。dc-sdk 是一个基于 Cesium 的开源 WebGL 地图开发框架&#xff0c;它提供了丰富的地图可视化功能和简单易用的 API&#xff0c;使开发者能够轻松地在 Web 应用中集成 3D…

ID卡网络读卡器C#小程序开发

ID卡全称为身份识别卡&#xff08;Identification Card&#xff09;&#xff0c;以下是对ID卡的详细介绍&#xff1a; 一、定义与分类 ID卡是一种不可写入的感应卡&#xff0c;含有固定的编号。按照规格和形状&#xff0c;它可以分为ID厚卡、标准卡&#xff08;85.6x54x0.800…

MySQL:SELECT list is not in GROUP BY clause 报错 解决方案

一、前言 一大早上测试环境&#xff0c;发现测试环境的MySQL报错了。 SELECT list is not in GROUP BY clause and contains nonaggregated column二、解决方案 官方文档中提到&#xff1a; 大致意思&#xff1a; 用于GROUP BY的SQL / 92标准要求满足以下条件&#xff1a; SE…

springboot maven 构建 建议使用 --release 21 而不是 -source 21 -target 21,因为它会自动设置系统模块的位置

使用 --release 选项代替 -source 和 -target 是一种更安全、更兼容的方式,特别是在构建使用较新版本 JDK 的项目时。以下是详细解释和建议: 1. 为什么推荐使用 --release 问题点: 使用 -source 和 -target 标志时,仅设置了代码的语言级别和字节码目标版本,但编译器仍可…

金蝶V10中间件的使用

目录 环境准备搭建过程配置修改应用部署 环境准备 Linux内核服务器JDK1.8安装包&#xff1a;AAS-V10.zip程序包&#xff1a;***.war 搭建过程 将安装包上传至服务器opt目录下&#xff0c;官方给定的默认服务主目录为“/opt/AAS-V10/ApusicAS/aas/”&#xff1b;解压安装包(解…