八股文集合

计算机网络

介绍一下TCP/IP模型和OSI模型的区别

OSI和TCP/IP都是一个计算机进行通信的一种体系,而OSI模型师国际组织制定的一个标准体系,TCP/IP是实际网络通信中的实际的体系结构。

OSI自底向上分为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

TCP/IP分四层,每个层负责特定的网络功能。

自底向上是网络接口层,这层对应OSI的数据链路层和物理层,,这层负责物理传输媒介的定义和管理,比如有线的以太网传输,无线的WIFI传输,此外,网络接口层还管理硬件地址(MAC地址)的管理。

然后是网络层,这层对应OSI的网络层,主要协议是IP,他负责数据包的路由和转发,选择一个最佳路径来将数据包从源主机传送到目标主机。IP协议使用IP地址来进行逻辑地址寻址。

传输层对应OSI的传输层,这层负责应用与应用间的数据传输。主要的传输协议是TCP和UDP。TCP提供可靠的数据传输,UDP提供不可靠的实时传输。

应用层对应OSI的应用层、表示层和会话层,他用于网络上的各种应用提供服务,比如网页浏览,文件传输等。

以上就是osi和tcp/ip的区别了。

从输入URL到页面展示到底发生了什么

个人版:

首先是用户输入url当按下回车键的时候浏览器会先解析url

把url分成

1、协议:http还是https

2、域名:类似于www.baidu.com

3、端口:如果是http的话默认是80端口https默认是443端口

4、路径:/index.html类似这种访问域名服务器下的哪个路径

5、还有一些请求参数等类似于?id=1这种

当解析完url之后再进行DNS解析,就是将域名转换成对应的ip地址就称为DNS解析了,转换的流程是先从浏览器中查看是否有之前的浏览缓存记录,如果浏览器中没有就从操作系统的host文件中查找,如果hosts文件也没有就从ISP的DNS服务器中查找域名对应的IP地址,就是中国移动那种运营商的DNS服务器中查找,如果还没有就从根DNS服务器中查找,直到找到对应的ip地址。找到对应的IP地址后客户端和服务器会开始建立三次握手根服务器建立tcp连接,客户端向服务端发送同步报文的信息,类似于跟服务器说我想跟你建立连接了,然后服务器向客户端发送确认收到连接信息的响应信息,然后客户端再向服务器发送我收到连接建立成功的信息了,

  1. 第一次握手:客户端发送 SYN=1,随机生成初始序号 ISN=x
  2. 第二次握手:服务器确认 SYN=1,并回复 SYN=1, ACK=1,同时生成自己的 ISN=y;
  3. 第三次握手:客户端发送 ACK=1,确认服务器的 ISN=y+1。

如果是https连接的话在建立完tcp连接后还需要

  • 开始进行 TLS/SSL 握手,包括:
    1. 客户端发送 ClientHello(支持的加密套件、协议版本等)
    2. 服务器回应 ServerHello + 证书(包含公钥)
    3. 双方协商密钥、验证身份、完成加密通道建立
    4. 加密通道建立成功后,才开始发送加密的 HTTP 请求

然后这样tcp连接就建立成功了,tcp建立成功后,客户端会向服务器发送请求,包含请求头,请求体等信息,然后服务端就会响应信息,包含响应头,响应体,还有cookie等信息,然后连接保持的字段有个keep-alive这个字段来确认保持连接的状态,燃弧响应后浏览器会解析响应信息,会先解析HTML文件然后形成DOM树,然后再解析CSS文件生成CSSOM,最后将两个图层合并就形成一个好看的页面了,之后再解析JS文件,解析JS文件的过程中会导致页面的重排和重绘,也会有一些外部的资源文件的加载,异步的请求等。

然后会连接可能保持也可能断开,如果数据传输完成了,断开的是四次挥手,客户端向服务器发送我要断开连接的消息,然后服务端向客户端发送我收到断开连接申请的信息,然后再发送我已经断开连接的信息,然后客户端向服务端发送我收到已经断开连接的信息了,以上就是所有。

  • 在 HTTP 请求结束后,如果设置了 Connection: close,则立即断开;
  • 如果是 keep-alive,则 TCP 连接暂时保留,等待下一次复用;
  • 如果一段时间没有新请求(例如服务器设置 idle=60s),连接会被关闭。

HTTP请求报文和响应报文是怎样的,有哪些常见的字段?

HTTP报文分为请求报文和响应报文。

1、请求报文主要由请求行、请求头、空行和请求体构成

请求行包括:

方法:指定要执行的操作,如GET、POST、PUT、DELETE等

资源路径:请求的资源的URI(统一资源标识符)

HTTP版本:使用的HTTP协议版本如HTTP/1.1或HTTP/2.0

请求头字段比较多,通常包含:

Host:请求的服务器域名

Accept: 客户端能够处理的媒体类型

Accept-Encoding:客户端能够解码的内容编码

Authorization:用于认证的凭证信息,比如token数据

Content-Length:请求体长度

Content-Type:请求体的媒体类型

Cookie: 存储在客户端的cookie数据

If-None-Match:资源的ETag值,用于缓存控制

Connection:管理连接的选项比如keep-alive

空行是请求头和请求体之间的空行,主要用于分隔请求头和请求体。请求体通常用于POST和PUT请求,包含发送给服务器的JSON数据。

2、响应报文,HTTP响应报文是服务器向客户端返回的数据格式用于传达服务器对客户端请求的处理结果以及相关数据。通常包含状态行、响应头、空行、响应体。

状态行:HTTP版本、状态码和状态消息比如HTTP/1.1 200 OK

响应头也是以键值对的形式提供信息,一些常见的相应头包括

Content-Type:指定相应主题的媒体类型

Content-Length:指定响应主题的长度(字节数)。

Server:指定服务器的信息

Expires:响应的过期时间,之后内容被认为是过时的。

Etag:响应体的实体标签,用于缓存和条件请求。

Last-Modified:资源最后被修改的日期和时间。

Location:在重定向时指定新的资源位置

Set-Cookie:在响应中设置Cookie

Access-Control-Allow-Origin:夸资源共享CORS策略,指定哪些域可以访问资源。

空行是在响应头和响应体之间表示响应头结束。而响应体式服务端实际传输的数据,可以是文本、HTML页面、图片、视频等,也可能是空。

HTTP有哪些请求方式

  1. GET:请求指定的资源。
  2. POST:向指定资源提交数据进行处理请求(例如表单提交)。
  3. PUT:更新指定资源。
  4. DELETE:删除指定资源。
  5. HEAD:获取报文首部,不返回报文主体。
  6. OPTIONS:查询服务器支持的请求方法。
  7. PATCH:对资源进行部分更新。

GET请求和POST请求的区别

  1. 用途:GET请求通常用于获取数据,POST请求用于提交数据。
  2. 数据传输:GET请求将参数附加在URL之后,POST请求将数据放在请求体中。
  3. 安全性:GET请求由于参数暴露在URL中,安全性较低;POST请求参数不会暴露在URL中,相对更安全。
  4. 数据大小:GET请求受到URL长度限制,数据量有限;POST请求理论上没有大小限制。
  5. 幂等性:GET请求是幂等的,即多次执行相同的GET请求,资源的状态不会改变;POST请求不是幂等的,因为每次提交都可能改变资源状态。
  6. 缓存:GET请求可以被缓存,POST请求默认不会被缓存。

HTTP中常见的状态码有哪些

1xx:表示加载信息很少见

200 :表示客户端请求成功

201:创建了新资源

204:无内容,服务器成功处理请求,但未返回任何内容

301:永久重定向

302:临时重定向

304:请求的内容没有修改过,所以服务器返回响应时,不会返回网页内容,而是使用缓存。

401:请求需要身份验证

403:请求的对应资源禁止被访问

404:服务器无法找到对应的资源

500:服务器内部错误

503:服务不可用

什么是强缓存和协商缓存

强缓存和协商缓存是HTTP缓存机制的两种类型,他们用于减少服务器的负担和提高网页加载速度。

1、强缓存:客户端在没用向服务发送请求的情况下,直接从本地缓存中获取资源。

Expires强缓存:这个是用于设置强缓存时间,此时间范围内,从内存中读取缓存并返回。但是因为Expires判断强缓存过期的机制是获取本地时间戳,与之前拿到的资源文件中的Expires字段的时间做比较来判断是否需要对服务器发起请求。这里有一个巨大的漏洞,如果我本地时间不准怎么办?所以目前已经被废弃了。

Cachee-Control强缓存:目前使用的强缓存是通过HTTP响应头中的Cache-Control字段实现,通过max-age来告诉浏览器在指定时间内可以直接使用缓存数据,无需再次请求。

2、协商缓存:当强缓存失效时,浏览器会发送请求到服务器,通过ETag或Last-Modified等HTTP响应头与服务器进行验证,以确定资源是否被修改。如果资源未修改,服务器服务器返回304 Not Modified状态码,告知浏览器使用本地缓存。如果资源已经修改,则返回新的资源,浏览器更新本地缓存。这种凡是需要与服务器通信,但可以确保用户总是获取最新的内容。

如果是基于缓存的资源获取的话一个是基于Last-Modified一个是基于ETag。

  1. 如果是基于Last-Modified的协商缓存,Last-Modified是资源的最后修改时间,服务器在响应头部中返回。当客户端读取到Last-modified的时候,会在下次的请求标头中携带一个字段If-Modified-Since,而这个请求头中的If-Modified-Since就是服务器第一次修改时候给他的时间服务器比较请求中的If-Modified-Since值与当前资源的Last-Modified值,如果比对的结果是没有变化,表示资源未发生变化,返回状态码304 Not Modified,如果比对的结果说资源已经更新了,就会给浏览器正常资源,返回200状态。

但是这种协商缓存有两个缺点:

  • 因为是更改文件修改时间来判断的,所以在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样有可能文件内容明明没修改但是缓存依然失效了。
  • 当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改事件记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样即使文件内容修改了,依然不会返回新的文件。
  1. 基于ETag的协商缓存:将原先协商缓存的比较时间戳的形式修改成了比较文件指纹(根据文件内容计算出的唯一哈希值)
  • ETag是服务器为资源生成的唯一标识符(文件指纹),可以是根据文件内容计算出的哈希值,服务端将其和资源一起放回给客户端。
  • 客户端在请求头部的If-None-Match字段中携带上次响应的ETag值。
  • 服务器比较请求中的If-None-Match值与当前资源的ETag值,如果匹配,表示资源未发生变化,返回状态码 304 Not Modified。如果两个文件指纹不吻合,则说明文件被更改,那么将新的文件指纹重新存储到响应头的ETag中并返回给客户端

image-20250620095824176

HTTPS和HTTP有哪些区别

两者的主要区别在于安全性和数据加密:

  1. 加密层HTTPSHTTP 的基础上增加了SSL/TLS 协议作为加密层,确保数据传输的安全性。而HTTP 数据传输是明文的,容易受到攻击。
  2. HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
  3. 端口HTTPS 通常使用端口443 ,而HTTP 使用端口80。
  4. HTTPS 协议需要向 CA 申请数字证书,来保证服务器的身份是可信的。

HTTPS的工作原理(HTTPS建立连接的过程)

1、客户端向服务端发送HTTPS请求Client Hello发送过去,这里包含了客户端支持的TLS版本、加密套件以及生成了第一个随机数给服务端

2、服务端打招呼,发送了Server Hello发送给客户端(这里包含确认TLS版本、选择使用的加密套件,以及生成第2个随机数),然后还把证书和公钥发送给客户端

3、这时候客户端生成了第三个随机数,这里称为预主密钥,并用之前接受到的公钥进行加密发送给服务器

4、这时候服务端接收到公钥加密后的字符串之后,利用自己的密钥进行解密,这样客户端和服务端都知道了预主密钥了。

5、这时候客户端和服务端利用第一个随机数和第二个随机数和预主密钥通过加密算法计算得出会话密钥。后面传输的数据都是用会话密钥进行加密。后面的是对称加密的,前面是非对称加密的,也就是说知道会话密钥之后就能自动解密会话密钥加密的字符串了。

https真的安全吗?

假设服务器的网址是http://www.baidu.com那么如果用户输入了baidu.com的话这个是http请求然后会返回302再重定向到https://www.baidu.com那么这时候就会有问题了,假设有一个第三方的人,在302重定向的时候拦截你的请求然后返回给你一个很像的网站,这样就使用不了https了,那这种一般会给浏览器一个警告,警告用户你访问的不是一个https的网站以此来把这个责任推给用户,这个问题还解决不了。此外国际组织还设置了一个新的状态码307状态码,之前重定向是302,现在这个307状态码限制用户访问的location不能改变就比如访问baidu.com那么后面的网址不能更改,只能往前面加https://www.baidu.com来限制重定向到别的网站,但是还是解决不了之前那个拦截请求的问题。最终的解决方案是HSTS 预加载列表,类似于在浏览器设置白名单如果输入baidu.com,浏览器也会自动跳转到 HTTPS,不会发出明文请求

HTTPS 主要基于SSL/TLS 协议,确保了数据传输的安全性和完整性, 其建立连接并传输数据的过程如下:

  1. 密钥交换:客户端发起HTTPS请求后,服务器会发送其公钥证书给客户端。
  2. 证书验证:客户端会验证服务器的证书是否由受信任的证书颁发机构(CA )签发,并检查证书的有效性。
  3. 加密通信:一旦证书验证通过,客户端会生成一个随机的对称加密密钥,并使用服务器的公钥加密这个密钥,然后发送给服务器。
  4. 建立安全连接:服务器使用自己的私钥解密得到对称加密密钥,此时客户端和服务器都有了相同的密钥,可以进行加密和解密操作。
  5. 数据传输:使用对称加密密钥对所有传输的数据进行加密,确保数据在传输过程中的安全性。
  6. 完整性校验:SSL/TLS协议还包括消息完整性校验机制,如消息认证码,确保数据在传输过程中未被篡改。
  7. 结束连接:数据传输完成后,通信双方会进行会话密钥的销毁,以确保不会留下安全隐患。

TCP和UDP的区别

  1. TCP是面向连接的协议,需要在数据传输前建立连接;UDP是无连接的,不需要建立连接。
  2. TCP提供可靠的数据传输,保证数据包的顺序和完整性;UDP不保证数据包的顺序或完整性。
  3. TCP具有拥塞控制机制,可以根据网络状况调整数据传输速率;UDP没有拥塞控制,发送速率通常固定。
  4. TCP通过滑动窗口机制进行流量控制,避免接收方处理不过来;UDP没有流量控制。
  5. TCP能够检测并重传丢失或损坏的数据包;UDP不提供错误恢复机制。
  6. TCP有复杂的报文头部,包含序列号、确认号等信息;UDP的报文头部相对简单。
  7. 由于TCP的连接建立、数据校验和重传机制,其性能开销通常比UDP大;UDP由于简单,性能开销小。
  8. 适用场景:TCP适用于需要可靠传输的应用,如网页浏览、文件传输等;UDP适用于对实时性要求高的应用,如语音通话、视频会议等。

TCP连接如何确保可靠性

TCP通过差错控制(序列号、确认应答、数据校验)、超时重传、流量控制、拥塞控制等机制,确保了数据传输的可靠性和效率。

  1. 序列号:每个TCP段都有一个序列号,确保数据包的顺序正确。
  2. 数据校验:TCP使用校验和来检测数据在传输过程中是否出现错误,如果检测到错误,接收方会丢弃该数据包,并等待重传。
  3. 确认应答:接收方发送ACK确认收到的数据,如果发送方在一定时间内没有收到确认,会重新发送数据。
  4. 超时重传:发送方设置一个定时器,如果在定时器超时之前没有收到确认,发送方会重传数据。
  5. 流量控制:TCP通过滑动窗口机制进行流量控制,确保接收方能够处理发送方的数据量。
  6. 拥塞控制:TCP通过算法如慢启动、拥塞避免、快重传和快恢复等,来控制数据的发送速率,防止网络拥塞。

既然提到了拥塞控制,那你能说说说拥塞控制是怎么实现的嘛

TCP拥塞控制可以在网络出现拥塞时动态地调整数据传输的速率,以防止网络过载。TCP拥塞控制的主要机制包括以下几个方面:

  1. 慢启动(Slow Start): 初始阶段,TCP发送方会以较小的发送窗口开始传输数据。随着每次成功收到确认的数据,发送方逐渐增加发送窗口的大小,实现指数级的增长,这称为慢启动。这有助于在网络刚开始传输时谨慎地逐步增加速率,以避免引发拥塞。
  2. 拥塞避免(Congestion Avoidance): 一旦达到一定的阈值(通常是慢启动阈值),TCP发送方就会进入拥塞避免阶段。在拥塞避免阶段,发送方以线性增加的方式增加发送窗口的大小,而不再是指数级的增长。这有助于控制发送速率,以避免引起网络拥塞。
  3. 快速重传(Fast Retransmit): 如果发送方连续收到相同的确认,它会认为发生了数据包的丢失,并会快速重传未确认的数据包,而不必等待超时。这有助于更快地恢复由于拥塞引起的数据包丢失。
  4. 快速恢复(Fast Recovery): 在发生快速重传后,TCP进入快速恢复阶段。在这个阶段,发送方不会回到慢启动阶段,而是将慢启动阈值设置为当前窗口的一半,并将拥塞窗口大小设置为慢启动阈值加上已确认但未被快速重传的数据块的数量。这有助于更快地从拥塞中恢复。

TCP流量控制是怎么实现的?

流量控制就是让发送方发送速率不要过快,让接收方来得及接收。利用滑动窗口机制就可以实施流量控制,主要方法就是动态调整发送方和接收方之间数据传输速率。

  • 滑动窗口大小: 在TCP通信中,每个TCP报文段都包含一个窗口字段,该字段指示发送方可以发送多少字节的数据而不等待确认。这个窗口大小是动态调整的。
  • 接收方窗口大小: 接收方通过TCP报文中的窗口字段告诉发送方自己当前的可接收窗口大小。这是接收方缓冲区中还有多少可用空间。
  • 流量控制的目标: 流量控制的目标是确保发送方不要发送超过接收方缓冲区容量的数据。如果接收方的缓冲区快满了,它会减小窗口大小,通知发送方暂停发送,以防止溢出。
  • 动态调整: 发送方会根据接收方的窗口大小动态调整发送数据的速率。如果接收方的窗口大小增加,发送方可以加速发送数据。如果窗口大小减小,发送方将减缓发送数据的速率。
  • 确认机制: 接收方会定期发送确认(ACK)报文,告知发送方已成功接收数据。这也与流量控制密切相关,因为接收方可以通过ACK报文中的窗口字段来通知发送方它的当前窗口大小。

UDP怎么实现可靠传输

UDP它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。关键在于两点,从应用层角度考虑:

(1)提供超时重传,能避免数据报丢失。

(2)提供确认序列号,可以对数据报进行确认和排序。

本端:首先在UDP数据报定义一个首部,首部包含确认序列号和时间戳,时间戳是用来计算RTT(数据报传输的往返时间),计算出合适的RTO(重传的超时时间)。然后以等-停的方式发送数据报,即收到对端的确认之后才发送下一个的数据报。当时间超时,本端重传数据报,同时RTO扩大为原来的两倍,重新开始计时。

对端:接受到一个数据报之后取下该数据报首部的时间戳和确认序列号,并添加本端的确认数据报首部之后发送给对段。根据此序列号对已收到的数据报进行排序并丢弃重复的数据报。

为什么是三次握手?

  1. 三次握手的过程
  • 第一次握手:客户端向服务端发送一个SYN(同步序列号)报文,请求建立连接,客户端进入SYN_SENT状态
  • 第二次握手:服务器收到SYN报文后,如果同意建立连接,则会发送一个SYN-ACK(同步确认)报文作为相应,同时进入SYN_RCVD状态
  • 第三次握手:客户端收到服务器的SYN-ACK报文后,会发送一个ACK(确认)报文作为最终相应,之后客户端和服务器都进入ESTABLISHED状态,连接建立成功。
  1. 为什么需要三次握手

通过三次握手,客户端和服务端都能够确认对方的接收和发送能力。第一次握手确认了客户端到服务器的通道是开放的;

第二次握手确认了服务器到客户端的通道是开放的;

第三次握手则确认了客户端接收到服务器的确认,从而确保了双方的通道都是可用的。

如果是两次握手的话,现在客户端向服务端发送SYN报文(同步序列号),这时候如果网络不好导致SYN报文没发送过去,就会进行重传,这时候新的SYN包发过去了,如果同意连接服务端就会发送客户端SYN-ACK包进行同步确认,然后客户端接收到SYN-ACK报文后会发送ACK确认报文给服务端,这时候对于客户端来说认为是建立了一个TCP连接,然后这时候之前旧的SYN报文网络好了,又发送给了服务端,这时候服务端又发送ACK给客户端,但是客户端由于没有发送SYN报文就把这个ACK过滤掉了,但是对于服务端来说就认为建立了两个TCP连接,客户端和服务端的连接个数就不匹配了。所以两次握手不行。

如果是多次握手的话,会造成资源的浪费最终可以优化成三次握手。

为什么是四次挥手?

  1. 四次挥手的过程
  • 第一次挥手:客户端发送一个FIN报文给服务端,表示自己要断开数据传送,报文中会制定一个序列号(seq=x)。然后,客户端进入FIN-WAIT-1状态。
  • 第二次挥手:服务端收到FIN报文后,回复ACK报文给客户端,且把客户端的序列号值+1,作为ACK+1报文的序列号(seq=x+1)。然后,服务端进入CLOSE-WAIT``(seq=x+1)状态,客户端进入FIN-WAIT-2状态。
  • 第三次挥手:服务端也要断开连接时,发送FIN报文给客户端,且指定一个序列号(seq=y+1),随后服务端进入LAST-ACK状态。
  • 第四次挥手:客户端收到FIN报文后,发出ACK报文进行应答,并把服务端的序列号值+1作为ACK报文序列号(seq=y+2)。此时客户端进入TIME-WAIT状态。服务端在收到客户端的ACK报文后进入CLOSE状态。如果客户端等待2MSL没有收到回复,也就是等待一段时间后,才关闭连接。(这是由于如果一段时间后服务端没有收到客户端的ACK包就会重新发送FIN包,然后重新发送ACK包,刷新超时时间,确保服务端收到最后的ACK包)
  1. 为什么需要四次挥手

TCP是全双工通信,可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。 当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后才会完全关闭TCP 连接。因此两次挥手可以释放一端到另一端的TCP连接,完全释放连接一共需要四次挥手。

只有通过四次挥手,才可以确保双方都能接收到对方的最后一个数据段的确认,主动关闭方在发送完最后一个ACK后进入TIME-WAIT 状态,这是为了确保被动关闭方接收到最终的ACK ,如果被动关闭方没有接收到,它可以重发FIN 报文,主动关闭方可以再次发送ACK

而如果使用三次挥手,被动关闭方可能在发送最后一个数据段后立即关闭连接,而主动关闭方可能还没有接收到这个数据段的确认。

HTTP的Keep-Alive是什么?TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是一个东西吗?

1、HTTP的Keep-Alive是叫HTTP长连接,因为由于TCP连接一次只能发送一次请求和响应,每次请求和响应都会重新连接,这种称为HTTP短连接,就会很耗费资源,这时候HTTP1.1之后就实现了HTTP的Keep-Alive实现了一使用同一个TCP连接来发送和接受多个HTTP的请求和响应,避免了多次连接建立和释放的开销,这是HTTP的长连接。通过设置HTTP头Connection:keep-alive来实现。

那这个长连接怎么关闭?是在接收端会设置一个定时器,如果超过了这个时间没再次发送请求,就会在HTTP头中设置Connection:close来关闭长连接。

2、而TCP的Keepalive是由TCP层(内核态)实现的,称为TCP的保活机制,是在TCP连接的时候,有一段时间没发送连接和请求了,通过TCP的保活机制来确认另一端是否还存活,是否有效。这个机制会发送一个小的探测包来检查连接是否仍然有效。

这里的TCP的keepalive不只是支持http,还支持ftp和smtp的,他是一个能力,类似于gc(内存管理机制,自动回收不再使用的内存空间)。

DNS的查询过程

DNS 用来将主机名和域名转换为IP地址, 其查询过程一般通过以下步骤:

  1. 本地DNS缓存检查:首先查询本地DNS缓存,如果缓存中有对应的IP地址,则直接返回结果。
  2. 如果本地缓存中没有,则会向本地的DNS服务器(通常由你的互联网服务提供商(ISP)提供, 比如中国移动)发送一个DNS查询请求。
  3. 如果本地DNS解析器有该域名的ip地址,就会直接返回,如果没有缓存该域名的解析记录,它会向根DNS服务器发出查询请求。根DNS服务器并不负责解析域名,但它能告诉本地DNS解析器应该向哪个顶级域(.com/.net/.org)的DNS服务器继续查询。
  4. 本地DNS解析器接着向指定的顶级域名DNS服务器发出查询请求。顶级域DNS服务器也不负责具体的域名解析,但它能告诉本地DNS解析器应该前往哪个权威DNS服务器查询下一步的信息。
  5. 本地DNS解析器最后向权威DNS服务器发送查询请求。 权威DNS服务器是负责存储特定域名和IP地址映射的服务器。当权威DNS服务器收到查询请求时,它会查找"example.com"域名对应的IP地址,并将结果返回给本地DNS解析器。
  6. 本地DNS解析器将收到的IP地址返回给浏览器,并且还会将域名解析结果缓存在本地,以便下次访问时更快地响应。
  7. 浏览器发起连接: 本地DNS解析器已经将IP地址返回给您的计算机,您的浏览器可以使用该IP地址与目标服务器建立连接,开始获取网页内容。

image-20250626101743663

CDN是什么,有什么作用?

CDN是一种分布式网络服务,通过将内容存储在分布式的服务器上,使用户可以从距离较近的服务器获取所需的内容,从而加速互联网上的内容传输。

  • 就近访问:CDN 在全球范围内部署了多个服务器节点,用户的请求会被路由到距离最近的 CDN 节点,提供快速的内容访问。
  • 内容缓存:CDN 节点会缓存静态资源,如图片、样式表、脚本等。当用户请求访问这些资源时,CDN 会首先检查是否已经缓存了该资源。如果有缓存,CDN 节点会直接返回缓存的资源,如果没有缓存所需资源,它会从源服务器(原始服务器)回源获取资源,并将资源缓存到节点中,以便以后的请求。通过缓存内容,减少了对原始服务器的请求,减轻了源站的负载。
  • 可用性:即使某些节点出现问题,用户请求可以被重定向到其他健康的节点。

Cookie和Session是什么?区别是什么?

CookieSession都用于管理用户的状态和身份, Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。

  1. Cookie
  • 通常,服务器会将一个或多个Cookie 发送到用户浏览器,然后浏览器将这些 Cookie 存储在本地。
  • 服务器在接收到来自客户端浏览器的请求之后,就能够通过分析存放于请求头的Cookie得到客户端特有的信息,从而动态生成与该客户端相对应的内容。
  1. Session

客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。Session 主要用于维护用户登录状态、存储用户的临时数据和上下文信息等。服务器为每个用户分配一个唯一的Session ID,通常存储在Cookie中。

由于HTTP是无状态的所以需要Cokkie和Session来记录用户的状态信息,比如账号、用户昵称等。

(2) Cookie和Session的区别?

  • 存储位置:Cookie 数据存储在用户的浏览器中,而 Session 数据存储在服务器上。
  • 数据容量:Cookie 存储容量较小,一般为几 KB。Session 存储容量较大,通常没有固定限制,取决于服务器的配置和资源。
  • 安全性:由于 Cookie 存储在用户浏览器中,因此可以被用户读取和篡改。相比之下,Session 数据存储在服务器上,更难被用户访问和修改。
  • 生命周期:Cookie可以设置过期时间,Session 依赖于会话的持续时间或用户活动。
  • 传输方式:Cookie 在每次 HTTP 请求中都会被自动发送到服务器,而 Session ID 通常通过Cookie 或 URL 参数传递。

操作系统

进程和线程之间有什么区别

进程是资源分配和调度的基本单位。

线程是程序执行的最小单位,线程是进程的子任务,是进程内的执行单元。 一个进程至少有一个线程,一个进程可以运行多个线程,这些线程共享同一块内存。

资源开销:

  • 进程:由于每个进程都有独立的内存空间,创建和销毁进程的开销较大。进程间切换需要保存和恢复整个进程的状态,因此上下文切换的开销较高。
  • 线程:线程共享相同的内存空间,创建和销毁线程的开销较小。线程间切换只需要保存和恢复少量的线程上下文,因此上下文切换的开销较小。

通信与同步:

  • 进程:由于进程间相互隔离,进程之间的通信需要使用一些特殊机制,如管道、消息队列、共享内存等。
  • 线程:由于线程共享相同的内存空间,它们之间可以直接访问共享数据,线程间通信更加方便。

安全性:

  • 进程:由于进程间相互隔离,一个进程的崩溃不会直接影响其他进程的稳定性。
  • 线程:由于线程共享相同的内存空间,一个线程的错误可能会影响整个进程的稳定性。

并行和并发有什么区别

  • 并行是在同一时刻执行多个任务。
  • 并发是在相同的时间段内执行多个任务,任务可能交替执行,通过调度实现。

并行是指在同一时刻执行多个任务,这些任务可以同时进行,每个任务都在不同的处理单元(如多个CPU核心)上执行。在并行系统中,多个处理单元可以同时处理独立的子任务,从而加速整体任务的完成。

并发是指在相同的时间段内执行多个任务,这些任务可能不是同时发生的,而是交替执行,通过时间片轮转或者事件驱动的方式。并发通常与任务之间的交替执行和任务调度有关。

解释一下用户态和核心态,什么场景下,会发生内核态和用户态的切换?

  1. 用户态和内核态的区别

用户态和内核态是操作系统为了保护系统资源和实现权限控制而设计的两种不同的CPU运行级别,可以控制进程或程序对计算机硬件资源的访问权限和操作范围。

  • 用户态:在用户态下,进程或程序只能访问受限的资源和执行受限的指令集,不能直接访问操作系统的核心部分,也不能直接访问硬件资源。
  • 核心态:核心态是操作系统的特权级别,允许进程或程序执行特权指令和访问操作系统的核心部分。在核心态下,进程可以直接访问硬件资源,执行系统调用,管理内存、文件系统等操作。
  1. 在什么场景下,会发生内核态和用户态的切换
  • 系统调用:当用户程序需要请求操作系统提供的服务时,会通过系统调用进入内核态。
  • 异常:当程序执行过程中出现错误或异常情况时,CPU会自动切换到内核态,以便操作系统能够处理这些异常。
  • 中断:外部设备(如键盘、鼠标、磁盘等)产生的中断信号会使CPU从用户态切换到内核态。操作系统会处理这些中断,执行相应的中断处理程序,然后再将CPU切换回用户态。

什么是死锁,如何避免?

死锁是当系统中有两个或者多个进程在进程中,因为争夺资源而造成的情况,当每个进程都持有一定的资源并且等待其他进程释放他们所需要的资源,如果这个资源被其他进程占有并且不释放,就会导致死锁。

死锁只有同时满足下面四个条件才会发生:

  • 互斥条件:一个进程占用了某个资源时,其他进程无法同时占用该资源。
  • 请求保持条件:一个进程因为请求资源而阻塞的时候,不会释放自己的资源。
  • 不可剥夺条件:资源不能被强制性地从一个进程中剥夺,只能由持有者自愿释放。
  • 循环等待条件:多个进程之间形成一个循环等待资源的链,每个进程都在等待下一个进程所占有的资源。

避免死锁:通过破坏死锁的四个必要条件之一来预防死锁。比如破坏循环等待条件,让所有进程按照相同的顺序请求资源。 检测死锁:通过检测系统中的资源分配情况来判断是否存在死锁。例如,可以使用资源分配图或银行家算法进行检测。 解除死锁:一旦检测到死锁存在,可以采取一些措施来解除死锁。例如,可以通过抢占资源、终止某些进程或进行资源回收等方式来解除死锁。

介绍一下几种经典的锁

  • 互斥锁:互斥锁是一种最常见的锁类型,用于实现互斥访问共享资源。在任何时刻,只有一个线程可以持有互斥锁,其他线程必须等待直到锁被释放。这确保了同一时间只有一个线程能够访问被保护的资源。
  • 自旋锁:自旋锁是一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询,直到锁被释放。

其他的锁都是基于这两个锁的

  • 读写锁:允许多个线程同时读共享资源,只允许一个线程进行写操作。分为读(共享)和写(排他)两种状态。
  • 悲观锁:认为多线程同时修改共享资源的概率比较高,所以访问共享资源时候要上锁
  • 乐观锁:先不管,修改了共享资源再说,如果出现同时修改的情况,再放弃本次操作。

讲一讲我理解的虚拟内存

虚拟内存是指在每一个进程创建加载的过程中,会分配一个连续虚拟地址空间,它不是真实存在的,\而是*通过映射与实际物理地址空间*对应,这样就可以使每个进程看起来都有自己独立的连续地址空间,并允许程序访问比物理内存RAM更大的地址空间, 每个程序都可以认为它拥有足够的内存来运行。

需要虚拟内存的原因:

  • 内存扩展: 虚拟内存使得每个程序都可以使用比实际可用内存更多的内存,从而允许运行更大的程序或处理更多的数据。
  • 内存隔离:虚拟内存还提供了进程之间的内存隔离。每个进程都有自己的虚拟地址空间,因此一个进程无法直接访问另一个进程的内存。
  • 物理内存管理:虚拟内存允许操作系统动态地将数据和程序的部分加载到物理内存中,以满足当前正在运行的进程的需求。当物理内存不足时,操作系统可以将不常用的数据或程序暂时移到硬盘上,从而释放内存,以便其他进程使用。
  • 页面交换:当物理内存不足时,操作系统可以将一部分数据从物理内存写入到硬盘的虚拟内存中,这个过程被称为页面交换。当需要时,数据可以再次从虚拟内存中加载到物理内存中。这样可以保证系统可以继续运行,尽管物理内存有限。
  • 内存映射文件:虚拟内存还可以用于将文件映射到内存中,这使得文件的读取和写入可以像访问内存一样高效。

你知道的线程同步的方式有哪些

线程同步机制是指在多线程编程中,为了保证线程之间的互不干扰,而采用的一种机制。常见的线程同步机制有以下几种:

  1. 互斥锁:互斥锁是最常见的线程同步机制。它允许只有一个线程同时访问被保护的临界区(共享资源)
  2. 条件变量:条件变量用于线程间通信,允许一个线程等待某个条件满足,而其他线程可以发出信号通知等待线程。通常与互斥锁一起使用。
  3. 读写锁: 读写锁允许多个线程同时读取共享资源,但只允许一个线程写入资源。
  4. 信号量:用于控制多个线程对共享资源进行访问的工具。

有哪些页面置换算法

页面置换算法(Page Replacement Algorithm)是操作系统用于管理内存的一种关键技术,特别是在虚拟内存系统中。当程序运行时,其所需的数据和指令可能不会全部同时驻留在物理内存中,而是根据需要在磁盘和内存之间进行交换。当一个进程访问的页面当前不在内存中(即发生缺页中断),操作系统需要从磁盘加载所需的页面到内存中。如果此时内存已经满了,则必须选择一个或多个已经在内存中的页面将其移出以腾出空间,这个过程就是页面置换。

当一个进程访问的数据不在内存中,需要从磁盘加载进来,但如果内存已满,就必须先替换掉一个内存中的页面,这个替换过程就叫做页面置换。

常见页面置换算法有最佳置换算法(OPT)、先进先出(FIFO)、最近最久未使用算法(LRU)、时钟算法(Clock) 等。

  1. 最近最久未使用算法LRU :LRU算法基于页面的使用历史,通过选择最长时间未被使用的页面进行置换。
  2. 先进先出FIFO算法:也就是最先进入内存的页面最先被置换出去
  3. 最不经常使用LFU :淘汰访问次数最少的页面,考虑页面的访问频率。
  4. 时钟算法CLOCK:Clock算法的核心思想是通过使用一个指针(称为时钟指针)在环形链表上遍历,检查页面是否被访问过, 当需要进行页面置换时,Clock算法从时钟指针的位置开始遍历环形链表。 如果当前页面的访问位为0,表示该页面最久未被访问,可以选择进行置换。将访问位设置为1,继续遍历下一个页面。 如果当前页面的访问位为1,表示该页面最近被访问过,它仍然处于活跃状态。将访问位设置为0,并继续遍历下一个页面如果遍历过程中找到一个访问位为0的页面,那么选择该页面进行置换。
  5. 最佳置换算法: 该算法根据未来的页面访问情况,选择最长时间内不会被访问到的页面进行置换。那么就有一个问题了,未来要访问什么页面,操作系统怎么知道的呢?操作系统当然不会知道,所以这种算法只是一种理想情况下的置换算法,通常是无法实现的。

熟悉哪些Linux命令

1、文件操作:

  • ls:列出目录内容。
  • cd:进入指定目录
  • pwd:显示当前工作目录
  • cp:复制文件或目录
  • mv:移动或重命名文件
  • rm:删除文件或目录
  • touch:创建空文件或更新文件时间戳

2、文件内容查看

  • cat:查看文件内容
  • head:查看文件的前几行
  • tail:查看文件的后几行,常用于查看日志文件

3、文件编辑

  • vi或vim:强大的文本编辑器

4、权限管理

  • chmod:更改文件或目录的访问权限
  • chown:更改文件或目录的所有者或所属组

5、磁盘管理

  • df:查看磁盘空间使用情况。

6、网络管理

  • ifconfig或ip addr :查看和配置网络接口
  • ping:查看网络状态和统计信息
  • netstat:查看网络状态和统计信息。
  • ssh:安全远程登录

7、进程管理

  • ps:查看当前运行的进程。
  • kill:杀掉某进程。

8、软件包管理(根据Linux发行版不同,命令可能有所不同)

  • apt-get(Ubuntu):安装、更新和删除软件包
  • npm install(CentOS的)

Linux中如何查看一个进程,如何杀死一个进程,如何查看某个端口有没有被占用

  1. 查看进程:用ps命令查看当前运行的进程,比如ps aux可以列出所有进程及其详细信息。
  2. 杀死进程:首先用ps或top命令找到进程的pid(进程ID)。然后用kill命令加上进程ID来结束进程,例如kill -9 PID. -9是强制杀死进程的信号。
  3. 查看端口占用:使用Isof -i: 端口号 可以查看占用特定端口的进程。或者用netstat -tulnp | grep 端口号,这会显示监听在该端口的服务及其进程ID。

说一下select\poll\epoll

什么是I/O多路复用?

简单来说,就是:

一个线程同时监听多个网络连接(文件描述符),一旦某个连接有数据可读或可写,就通知程序去处理它。

这在服务器开发中非常有用,比如一个 Web 服务器要同时处理成千上万个客户端的连接请求。

  • select:这个是一个最古老的I/O多路复用机制,他可以监视多个文件描述符的可读、可写和错误状态。然而,但是它的效率可能随着监视的文件描述符的数量的增加而降低。
  • poll:poll是select的一种改进,他使用轮询方式来检查多个文件描述符的状态,避免了select中文件描述符数量有限的问题。但对于大量的文件描述符,poll的性能也可能变得不足够高校。
  • epoll:epoll是Linux特有的I/O多路复用机制,相较于select和poll,它在处理大量文件描述符时更加高校。epoll使用时间通知的方式,只有在文件描述符就绪时才会通知应用程序,而不需要应用程序的轮询。

总结select是最早的 I/O 多路复用技术,但受到文件描述符数量和效率方面的限制。poll克服了文件描述符数量的限制,但仍然存在一定的效率问题。epoll是一种高效的I/O多路复用技术,尤其适用于高并发场景,但它仅在 Linux 平台上可用。一般来说,epoll 的效率是要比 selectpoll 高的,但是对于活动连接较多的时候,由于回调函数触发的很频繁,其效率不一定比 selectpoll 高。所以 epoll 在连接数量很多,但活动连接较小的情况性能体现的比较明显。

技术 类比
Blocking I/O 每个快递员都直接送到你家
select/poll 你每天去快递柜一个一个找有没有你的包裹
epoll 包裹到了,快递柜自动发短信提醒你取件

MySQL

一条SQL查询语句是如何执行的?

  1. 连接器:连接器负责跟客户端建立连接、获取权限、维持和管理连接。
  2. 查询缓存:MYSQL拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以key-value对的形式,被直接缓存在内存中。
  3. 分析器:你输入的是由多个字符串和空格组成的一条SQL语句,MYSQL需要识别出里面的字符串分别是什么,代表什么。
  4. 优化器:优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。
  5. 执行器:MYSQL通过分析器知道了你要作什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。

事务的四大特性 有哪些?

事务的四大特性通常被称为ACID特性

  1. 原子性:确保事务的所有操作要么全部执行成功,要么全部失败回滚 ,不存在部分策划给你共的情况。
  2. 一致性:事务在执行前后,数据库从一个一致性状态转变到另一个一致性状态。(例子:银行转账,A=100,B=100,那么一致性就是A=50,B=150,而不是中间态A=50,B=100)
  3. 隔离性:多个事务并发执行时,每个事务都应该被隔离开来,一个事务的执行不应该影响其他事务的执行。
  4. 持久性:一旦事务被提交,它对数据库的改变就是永久性的,即使在系统故障或崩溃后也能够保持。

数据库的事务隔离级别有哪些?

  1. 读未提交:

    • 允许一个事务读取另一个事务尚未提交的数据修改。
    • 最低的隔离级别,存在脏读,不可重复读和幻读的问题。
  2. 读已提交

    • 一个事务只能读取已经提交的数据。其他事物的修改在该事务提交之后才可见。
    • 解决了脏读问题,但仍可能出现不可重复读和幻读。
  3. 可重复读

    • 事务执行期间,多次读取同一数据会得到相同的结果,即在事务开始和结束之间,其他事务对数据的修改不可见。
    • 解决了不可重复读问题,但仍可能出现幻读
  4. 序列化

    • 最高的隔离级别,确保事物之间的并发执行效果与串行执行的效果相同,即不会出现脏读,不可重复读和幻读。

    这里顺便讲一下

    脏读,不可重复读和幻读是什么

    脏读就是读到了脏数据,举个例子A和B转账,A一开始查询有100块然后B给A转账转了50,然后B转账失败了,但是这时候A读取数据读到了150,B回滚了,所以其实A并没有150还是100,所以读到了脏数据。

    不可重复读还是刚刚的例子,A在一个事务里面查询两次,第一次查询有100然后B给A转账成功并且提交了事务,这时候A的事务还没有提交,A再次查询发现有150了,这就是不可重复读,因为B的事务影响到了A的事务,所以违反了原子性。

    幻读是针对数据个数的,A在一个事务中有两个查询,第一次查询商品个数有50个商品,然后B在数据库中增加了一个商品,并且B提交了事务,然后A的事务中再次查询商品个数,发现有51条数据,这就叫幻读。

    不可重复读针对的是update,幻读针对的是insert或者delete.

MySQL的 执行引擎有哪些

MySQL的执行引擎主要负责查询的执行和数据的存储, 其执行引擎主要有MyISAMInnoDBMemory 等。

  • InnoDB引擎提供了对事务ACID的支持,还提供了行级锁和外键的约束,是目前MySQL的默认存储引擎,适用于需要事务和高并发的应用。
  • MyISAM引擎是早期的默认存储引擎,支持全文索引,但是不支持事务,也不支持行级锁和外键约束,适用于快速读取且数据量不大的场景。
  • Memory就是将数据放在内存中,访问速度快,但数据在数据库服务器重启后会丢失。

行级锁的工作原理

当一个事务需要修改某一行数据时,InnoDB会在这行记录上加上锁。这意味着:

  • 其他事务可以同时访问和修改同一张表中的不同行,因为它们不会相互干扰。
  • 只有当两个或更多的事务试图访问或修改相同的行时,这些事务之间才会产生竞争,导致其中一个事务必须等待另一个事务完成并释放锁之后才能继续执行。

行级锁的好处

  1. 提高并发性:由于锁定的是单个行而非整个表,因此多个事务可以同时对不同的行进行读写操作,大大提高了系统的并发处理能力。
  2. 减少冲突:在高并发环境下,如果使用表级锁,那么只要有一个事务正在修改表中的任何一行,其他所有尝试访问该表的操作都将被阻塞。而行级锁只会影响特定行上的操作,减少了不必要的等待时间。
  3. 增强隔离性:行级锁有助于实现更高级别的事务隔离,比如可重复读(Repeatable Read),这有助于防止脏读、不可重复读等问题。

InnoDB中的行级锁类

  • 共享锁(Shared Locks, S锁):允许事务读取一行数据,但阻止其他事务对该行进行修改。多个事务可以同时获得同一行的共享锁。
  • 排他锁(Exclusive Locks, X锁):允许事务更新或删除一行数据,并且阻止其他任何事务获取该行的任何类型的锁(包括共享锁)。也就是说,一旦一行被某个事务加上了排他锁,其他的事务就不能再对该行加任何锁。

MySQL为什么使用B+树来作索引

B+树是一个B树的变种,提供了高效的数据检索、插入、删除和范围查询性能。

  • 单点查询:B 树进行单个索引查询时,时间代价为O(logn)。从平均时间代价来看,会比 B+ 树稍快一些。但是 B 树的查询波动会比较大,因为每个节点既存索引又存记录,所以有时候访问到了非叶子节点就可以找到索引,而有时需要访问到叶子节点才能找到索引。B+树的非叶子节点不存放实际的记录数据,仅存放索引,所以数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
  • 插入和删除效率:B+ 树有大量的冗余节点,删除一个节点的时候,可以直接从叶子节点中删除,甚至可以不动非叶子节点,删除非常快。B+ 树的插入也是一样,有冗余节点,插入可能存在节点的分裂(如果节点饱和),但是最多只涉及树的一条路径。B 树没有冗余节点,删除节点的时候非常复杂,可能涉及复杂的树的变形。
  • 范围查询:B+ 树所有叶子节点间有一个链表进行连接,而 B 树没有将所有叶子节点用链表串联起来的结构,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。存在大量范围检索的场景,适合使用 B+树,比如数据库。而对于大量的单个索引查询的场景,可以考虑 B 树,比如nosql的MongoDB。

说一下索引失效的场景

索引失效意味着查询操作不能有效利用索引进行数据检索,从而导致性能下降,下面一些场景会发生索引失效。

  1. 使用OR条件:当使用OR连接多个条件,并且每个条件用到不同的索引列时,索引可能不会被使用。
  2. 使用非等值查询:当使用!=<>操作符时,索引可能不会被使用,特别是当非等值条件在WHERE子句的开始部分时。
  3. 对列进行类型转换: 如果在查询中对列进行类型转换,例如将字符列转换为数字或日期,索引可能会失效。
  4. 使用LIKE语句:以通配符%开头的LIKE查询会导致索引失效。
  5. 函数或表达式:在列上使用函数或表达式作为查询条件,通常会导致索引失效。
  6. 表连接中的列类型不匹配: 如果在连接操作中涉及的两个表的列类型不匹配,索引可能会失效。例如,一个表的列是整数,另一个表的列是字符,连接时可能会导致索引失效。

补充知识:

索引(Index)在数据库管理系统(DBMS)中是一种用于加速数据检索的数据结构。它就像是书本末尾的索引部分,可以帮助你快速找到所需的信息,而不需要逐页阅读整本书。同样地,在数据库中,索引允许数据库系统更快地定位到特定的数据行,而不必扫描整个表。

索引的基本概念

  • 目的:提高查询效率,减少查找所需的时间。
  • 实现方式:通过创建一个指向数据库表中数据记录的结构化参考,使得数据库引擎可以迅速定位到所需的数据行。
  • 代价:虽然索引能加快读取操作的速度,但它们也会占用额外的存储空间,并且在进行插入、更新或删除操作时需要维护索引结构,这可能会稍微降低这些操作的速度。

索引的工作原理

当你为一个表中的某个列(或多个列组合)创建索引时,数据库会为该列的所有值构建一种特殊的数据结构(如B树、哈希等)。当执行查询时,数据库首先使用这个索引来确定哪些行满足查询条件,然后直接访问那些具体的行,而不是遍历整个表。

示例

假设有一个包含数百万条记录的employees表,其中有一列是last_name。如果你经常根据姓氏搜索员工信息,那么在这个列上创建一个索引将会非常有用。

1
CREATE INDEX idx_last_name ON employees(last_name);

有了这个索引后,当你执行如下查询:

1
SELECT * FROM employees WHERE last_name = 'Smith';

数据库就可以利用idx_last_name索引来快速定位所有姓氏为’Smith’的记录,而不需要检查每一行。

索引类型

  1. 单列索引:基于单个字段创建的索引。

  2. 复合索引(多列索引):基于两个或更多字段创建的索引。例如,可以在first_namelast_name两列上创建一个复合索引。

  3. 唯一索引:确保索引列中的所有值都是唯一的。这对于主键来说特别重要。

  4. 全文索引:用于全文搜索,适用于文本字段,能够支持复杂的文本匹配查询。

  5. 聚集索引与非聚集索引

    • 聚集索引:决定了表中数据的实际物理存储顺序。每个表只能有一个聚集索引。
    • 非聚集索引:不改变数据的物理存储顺序,而是创建一个独立的索引结构来指向实际的数据位置。一个表可以有多个非聚集索引。

使用索引的好处

  • 加速查询:显著提高SELECT语句的性能。
  • 优化排序和分组:对于ORDER BYGROUP BY子句也有帮助。
  • 约束支持:比如唯一性约束可以通过唯一索引来实现。

使用索引的注意事项

尽管索引有很多优点,但也有一些潜在的问题需要注意:

  • 增加存储开销:每个索引都需要额外的磁盘空间。
  • 影响写入性能:每次插入、更新或删除数据时,相关的索引也需要被更新,这会带来一定的性能损耗。
  • 过度索引的风险:并非所有的查询都需要索引,过多的索引不仅浪费资源,还可能降低整体性能。

因此,在设计数据库时,应该根据具体的业务需求和查询模式来合理选择和设计索引。

MySQL和Redis的区别是什么

  • Redis基于键值对,支持多种数据结构,而MySQL是一种关系型数据库,使用表来组织数据。
  • Redis将数据存在内存中,通过持久化机制将数据写入磁盘,MySQL通常将数据存储在磁盘上。
  • Redis不使用SQL,而是使用自己的命令集,MySQL使用SQL来进行数据查询和操作。
  • Redis以高性能和低延迟为目标,适用于读多写少的应用场景,MySQL适用于需要支持复杂查询、事务处理、拥有大规模数据集的场景。

Redis更适合处理高速、高并发的数据访问,以及需要复杂数据结构和功能的场景,在实际应用中,很多系统会同时使用MySQL和Redis。

Redis有什么优缺点?为什么用Redis查询会比较快

1、Redis有什么优缺点?

Redis是一个基于内存的数据库,读写速度非常快,通常被用作,缓存,消息队列,分布式锁,和键值对存储数据库。它支持多种数据结构,如字符串、哈希表、列表、集合、有序集合等,Redis还提供了分布式特性,可以将数据分布在多个节点上,以提高可扩展性和可用性。但是Redis受限于物理内存的大小,不适合存储超大量数据,并且需要大量内存,相比磁盘存储成本更高。

2、为什么Redis查询快

  • 基于内存操作:传统的磁盘文件操作相比减少了IO,提高了操作的速度。
  • 高效的数据结构:Redis专门设计了STRING、LIST、HASH等高效的数据结构,依赖各种数据结构提升了读写的效率。
  • 单线程:单线程操作省去了上下文切换带来的开销和CPU的消耗,同时不存在资源竞争,避免了死锁现象的发生。
  • I/O多路复用:采用I/O多路复用机制同时监听多个Socket,根据Socket上的事件来选择对应的时间处理器进行处理。

Redis的数据类型有哪些?

edis 常见的五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及 Zset(sorted set:有序集合)

  1. 字符串STRING:存储字符串数据,最基本的数据类型。
  2. 哈希表HASH:存储字段和值的映射,用于存储对象。
  3. 列表LIST:存储有序的字符串元素列表。
  4. 集合SET:存储唯一的字符串元素,无序。
  5. 有序集合ZSET:类似于集合,但每个元素都关联一个分数,可以按分数进行排序。

Redis版本更新,又增加了几种数据类型,

  • BitMap: 存储位的数据结构,可以用于处理一些位运算操作。
  • HyperLogLog:用于基数估算的数据结构,用于统计元素的唯一数量。
  • GEO: 存储地理位置信息的数据结构。
  • Stream:专门为消息队列设计的数据类型。

Redis是单线程还是多线程的?为什么?

Redis在其传统的实现中是单线程的(网络请求模块使用单线程进行处理,其他模块仍用多个线程),这意味着它使用单个线程来处理所有的客户端请求。这样的设计选择有几个关键原因:

  1. 简化模型:单线程模型简化了并发控制,避免了复杂的多线程同步问题。
  2. 性能优化:由于大多数操作是内存中的,单线程避免了线程间切换和锁竞争的开销。
  3. 原子性保证:单线程执行确保了操作的原子性,简化了事务和持久化的实现。
  4. 顺序执行:单线程保证了请求的顺序执行。

但是Redis的单线程模型并不意味着它在处理客户端请求时不高效。实际上,由于其操作主要在内存中进行,Redis能够提供极高的吞吐量和低延迟的响应。

此外,Redis 6.0 引入了多线程的功能,用来处理网络I/O这部分,充分利用CPU资源,减少网络I/O阻塞带来的性能损耗。

Redis的持久化机制有哪些?

  • AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;
  • RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
  • 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点;

介绍一下Redis缓存雪崩和缓存穿透,如何解决这些问题?

  1. 缓存雪崩是指在某个时间点,大量缓存同时失效,导致请求直接访问数据库或其他后端系统,增加了系统负载。

对于缓存雪崩,可以通过合理设置缓存的过期时间,分散缓存失效时间点,或者采用永不过期的策略,再结合定期更新缓存。

  1. 缓存击穿是指一个缓存中不存在但是数据库中存在的数据,当有大量并发请求查询这个缓存不存在的数据时,导致请求直接访问数据库,增加数据库的负载。典型的场景是当一个缓存中的数据过期或被清理,而此时有大量请求访问这个缓存中不存在的数据,导致大量请求直接访问底层存储系统。

对于缓存击穿,可以采用互斥锁(例如分布式锁)或者在查询数据库前先检查缓存是否存在,如果不存在再允许查询数据库,并将查询结果写入缓存。

  1. 缓存穿透是指查询一个在缓存和数据库都不存在的数据,这个数据始终无法被缓存,导致每次请求都直接访问数据库,增加数据库的负载。典型的情况是攻击者可能通过构造不存在的 key 大量访问缓存,导致对数据库的频繁查询。

对于缓存穿透,可以采用布隆过滤器等手段来过滤掉恶意请求,或者在查询数据库前先进行参数的合法性校验。

如何保证数据库和缓存的一致性

Cache Aside

  • 原理:先从缓存中读取数据,如果没有就再去数据库里面读数据,然后把数据放回缓存中,如果缓存中可以找到数据就直接返回数据;更新数据的时候先把数据持久化到数据库,然后再让缓存失效。
  • 问题:假如有两个操作一个更新一个查询,第一个操作先更新数据库,还没来及删除缓存,查询操作可能拿到的就是旧的数据;更新操作马上让缓存失效了,所以后续的查询可以保证数据的一致性;还有的问题就是有一个是读操作没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,也会造成脏数据。
  • 可行性:出现上述问题的概率其实非常低,需要同时达成读缓存时缓存失效并且有并发写的操作。数据库读写要比缓存慢得多,所以读操作在写操作之前进入数据库,并且在写操作之后更新,概率比较低。

Read/Write Through

  • 原理:Read/Write Through原理是把更新数据库(Repository)的操作由缓存代理,应用认为后端是一个单一的存储,而存储自己维护自己的缓存。
  • Read Through:就是在查询操作中更新缓存,也就是说,当缓存失效的时候,Cache Aside策略是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对调用方是透明的。
  • Write Through:当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由缓存自己更新数据库(这是一个同步操作)。

Write Behind

  • 原理:在更新数据的时候,只更新缓存,不更新数据库,而缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作非常快,带来的问题是,数据不是强一致性的,而且可能会丢。
  • 第二步失效问题:这种可能性极小,缓存删除只是标记一下无效的软删除,可以看作不耗时间。如果会出问题,一般程序在写数据库那里就没有完成:故意在写完数据库后,休眠很长时间再来删除缓存。

一、回顾三种常见缓存更新策略

  1. Cache Aside(旁路缓存)

原理:

查询:先查缓存,命中则返回;未命中则查数据库,并写入缓存。

更新:先更新数据库,再删除缓存。

存在的问题:

并发读写问题

  • A 读取缓存未命中 → 查询数据库(旧数据)
  • B 更新数据库 + 删除缓存
  • A 写入缓存 → 缓存中是旧数据,导致不一致

解决方案建议:

  • 使用 分布式锁延迟双删
  • 设置一个合理的缓存过期时间,容忍短时间不一致

Read/Write Through(穿透式缓存)

原理

  • 应用只和缓存交互,缓存负责和数据库打交道。
  • Read Through:缓存失效时自动从 DB 加载
  • Write Through:更新缓存后,缓存同步更新 DB

优点:

  • 对应用层透明,简化逻辑
  • 更容易实现一致性

缺点

  • 实现复杂,需要中间件支持(如 Caffeine、Ehcache、Redis Modules 等)

Write Behind(异步回写)

原理:

  • 只更新缓存,缓存异步批量写入数据库
  • 极大提高性能,但牺牲一致性

优点:

  • 高性能,适用于日志、计数器等场景

缺点

  • 数据可能丢失(如果缓存宕机)
  • 不适合金融、订单等强一致性场景

二、你提到的“第二步失效问题”详细解释

❓“这种可能性极小,缓存删除只是标记一下无效的软删除,可以看作不耗时间。如果会出问题,一般程序在写数据库那里就没有完成:故意在写完数据库后,休眠很长时间再来删除缓存。解释一下再”

问题背景

你描述的是这样一种场景:

1
2
3
4
5
6
7
线程 A:
1. 修改数据库
2. 【故意 sleep 很久】
3. 删除缓存

线程 B:
在 A 执行第 2 步期间访问缓存 → 缓存未删 → 返回旧数据

这就是典型的 “延迟双删” 场景下可能出现的缓存脏读问题。


如何解决这个问题?

方法一:延迟双删(Delay Double Delete)

1
2
3
4
5
6
7
8
9
10
11
// 第一次删除缓存
deleteCache(key);

// 更新数据库
updateDatabase(data);

// 强制休眠一段时间(比如 50ms),等待其他请求刷新缓存
Thread.sleep(50);

// 第二次删除缓存
deleteCache(key);

作用

  • 防止在更新数据库和删除缓存之间有请求读到旧缓存
  • 第二次删除确保缓存一定被清空,下次读操作重新加载最新数据

方法二:使用分布式锁

1
2
3
4
5
6
7
try {
lock.acquire(); // 获取锁
updateDatabase(); // 先更新数据库
deleteCache(); // 再删除缓存
} finally {
lock.release();
}

作用

  • 保证更新数据库和删除缓存的操作是原子的
  • 避免并发请求拿到脏数据

方法三:基于 Binlog 的异步更新

通过监听数据库的 binlog 日志,在数据变更后触发缓存更新或删除。

优点

  • 完全解耦业务逻辑和缓存维护
  • 可以保证最终一致性
  • 避免因网络、代码错误等导致缓存不一致

缺点

  • 架构复杂,需引入 Canal、Debezium 等组件
  • 成本较高,适合大型系统

三、不同场景下的推荐策

场景 推荐策略 是否强一致性 说明
读多写少,容忍短时不一致 Cache Aside + 延迟双删 最终一致 最常用,简单有效
对一致性要求高 Cache Aside + 分布式锁 / Write Through 强一致 性能略差,但安全
高吞吐量,可容忍短暂丢失 Write Behind 最终一致 适合非关键数据
大型分布式系统 基于 Binlog 异步更新 最终一致 架构复杂但稳定

四、总结一句话

没有完美的缓存一致性解决方案,只有根据业务场景选择最合适的策略。

如果你的应用是电商、金融类系统,建议优先考虑 Cache Aside + 延迟双删 + 分布式锁; 如果是日志、统计类系统,可以用 Write Behind 提高性能。

Java

String\StringBuffer\StringBuilder的区别

String, StringBuilder, 和 StringBuffer 都是 Java 中用于处理字符串的类,但它们之间有一些重要的区别:

  1. 不可变性
  • String 内部的字符数组使用 final 修饰,为不可变的字符串类,每当我们对 String 对象进行改变时,实际上都会创建一个新的 String 对象,旧的 String 对象会被 JVM 回收, 容易触发 **gc**,引起系统内存抖动。
  • StringBuilderStringBuffer是可变的。即它们都允许修改字符串,而不会创建新的对象
  1. 线程安全
  • String:由于 String 是不可变的,所以是线程安全的。
  • StringBuffer 中的方法均使用 **synchronized** 关键字修饰,线程安全。
  • StringBuilder 线程不安全。
  1. 性能

对于复杂的字符串操作(例如多次的拼接,插入,删除),StringBuilderStringBuffer 效率高于 String,因为它们是可变的,不需要创建新的对象。

  1. 使用场景
  • String: 字符串不经常变化的场景中可以使用 String 类,例如常量的声明、少量的变量运算。
  • StringBuilder: 在频繁进行字符串运算(如拼接、替换、和删除等),并且运行在单线程的环境中,则可以考虑使用,如SQL语句的拼装、JSON封装等。
  • StringBuffer: 在频繁进行字符串运算(如拼接、替换、删除等),并且运行在多线程环境中,则可以考虑使用 StringBuffer,例如 XML 解析、HTTP 参数解析和封装。

接口和抽象类的区别

  1. 从定义上来说
  • 接口是一种抽象类型,它定义了一组方法,但没有实现任何方法的具体代码。接口中的方法默认是抽象的,且接口中只能包含常量(static final 变量)和抽象方法,不能包含成员变量。
  • 抽象类是一个类,可以包含抽象方法和具体方法,也可以包含成员变量和常量。抽象类中的抽象方法是没有实现的方法,而具体方法则包含实现代码。抽象类不能直接实例化,通常需要子类继承并实现其中的抽象方法。
  1. 继承
  • 接口支持多继承,一个类可以实现多个接口。
  • Java 中不支持多继承,一个类只能继承一个抽象类。如果一个类已经继承了一个抽象类,就不能再继承其他类。
  1. 构造器
  • 接口不能包含构造器,因为接口不能被实例化。类实现接口时,必须实现接口中定义的所有方法。
  • 抽象类可以包含构造器,用于初始化抽象类的子类实例。
  1. 访问修饰符
  • 接口中的方法默认是public abstract的。接口中的变量默认是public static final的。
  • 抽象类中的抽象方法默认是protected的,具体方法的访问修饰符可以是publicprotectedprivate
  1. 实现限制
  • 类可以同时实现多个接口,接口中的方法默认为抽象方法,不包含方法体。实现接口时必须要实现这些方法。
  • 一个类只能继承一个抽象类,继承抽象类的子类必须提供抽象类中定义的所有抽象方法的实现。
  1. 设计目的
    • 接口用于定义规范,强调“行为”或“能力”。
    • 抽象类用于代码复用,提供通用的实现或基础功能,并且可以包含方法的具体实现。

java常见的异常类有哪些

  • Java 的异常都是派生于 Throwable 类的一个实例,所有的异常都是由 Throwable 继承而来的。
  • 异常又分为 RuntimeException 和其他异常:
    • 由程序错误导致的异常属于 RuntimeException
    • 而程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于其他异常。
  • 运行时异常 RuntimeException

顾名思义,运行时才可能抛出的异常,编译器不会处理此类异常。比如数组索引越界 ArrayIndexOutOfBoundsException、使用的对象为空 NullPointException、强制类型转换错误 ClassCastException、除 0 等等。出现了运行时异常,一般是程序的逻辑有问题,是程序自身的问题而非外部因素。

  • 其他异常:

Exception 中除了运行时异常之外的,都属于其他异常。也可以称之为编译时异常,这部分异常编译器要求必须处置。这部分异常常常是因为外部运行环境导致,因为程序可能运行在各种环境中,如打开一个不存在的文件,此时抛出 FileNotFoundException。编译器要求Java程序必须捕获或声明所有的编译时异常,强制要求程序为可能出现的异常做准备工作。

说一说Java面向对象的三大特性

Java面向对象编程的三大特性是封装、继承和多态:

  1. 封装:封装是将对象的数据(属性)和行为(方法)结合在一起,并隐藏内部的实现细节,只暴露出一个可以被外界访问的接口。通常使用关键字 privateprotectedpublic 等来定义访问权限,以实现封装。
  2. 继承:允许一个类(子类)继承另一个类(父类)的属性和方法的机制。子类可以重用父类的代码,并且可以通过添加新的方法或修改(重写)已有的方法来扩展或改进功能,提高了代码的可重用性和可扩展性。Java支持单继承,一个类只能直接继承一个父类。
  3. 多态:多态是指允许不同类的对象对同一消息做出响应,但具体的行为会根据对象的实际类型而有所不同。这通常通过方法重载和重写实现。

说一说对java多态的理解

  1. 当把一个子类对象直接赋给父类引用变量,而运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。
  2. 多态有两种形式:编译时多态(静态多态)和运行时多态(动态多态)。
  • 编译时多态:指在编译阶段,编译器就能够确定调用哪个方法,这是通过方法的重载来实现的。编译器在编译时根据方法的参数数量、类型或顺序来选择调用合适的方法。
  • 运行时多态:在程序运行时,根据实际对象的类型来确定调用的方法,这是通过方法的重写来实现的。运行时多态主要依赖于对象的实际类型,而不是引用类型。

Java重写和重载的区别

Java中的重载和重写是实现多态的两种不同方式。

方法的重载是编译时多态,指的是在同一个类中,可以有多个方法具有相同的名称,但是它们的参数列表不同(参数的类型、个数、顺序),可以有不同的返回类型和访问修饰符,通过静态绑定(编译时决定)实现。

方法的重写是运行时多态,指的是在子类中重新定义父类中已经定义的方法,方法名、参数列表和返回类型都必须相同。重写的方法的访问级别不能低于被重写的父类方法,虚拟机在运行时根据对象的实际类型来确定调用哪个方法。

总结来说,重载关注的是方法的多样性,允许同一个类中存在多个同名方法;而重写关注的是方法的一致性,允许子类提供特定于其自己的行为实现。

final关键字有什么用

final 就是不可变的意思,可以修饰变量、方法和类。

  1. 修饰类: final 修饰的类不可被继承,是最终类.
  2. 修饰方法: 明确禁止该方法在子类中被覆盖的情况下才将方法设置为 final
  3. 修饰变量:
  • final 修饰 基本数据类型的变量,其数值一旦在初始化之后便不能更改, 称为常量;
  • final 修饰 引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。虽然不能再指向其他对象,但是它指向的对象的内容是可变的。

==和equals的区别

在Java中,==equals方法用来比较对象,但它们在语义和使用上仍有一定的差别:

  1. ==运算符:对于原始数据类型,==比较的是值是否相等,对于引用类型,==比较的是两个引用是否指向内存中的同一位置,即它们是否是同一个对象。
  2. equals是一个方法,定义在Object类中,默认情况下,equals() 方法比较的是对象的引用,与 == 类似。但在子类中通常被重写,比如 String、 Integer 等,已经重写了 equals() 方法以便比较对象的内容是否相等。
  3. 一般来说,是使用 == 比较对象的引用(内存地址),用 equals() 比较对象的内容。
  4. 需要注意的是,在重写equals方法时,应同时重写hashCode方法,以保持equalshashCode的一致性。

Java的集合类有哪些,哪些是线程安全的,哪些是线程不安全的

Java中的集合类主要由Collection和Map这两个接口派生而出,其中Collection 接口又派生出三个子接口,分别是Set、List、Queue。所有的Java集合类,都是Set、List、Queue、Map

这四个接口的实现类

  • List接口 有序集合,允许重复元素。常见的实现类有ArrayListLinkedList等。
  • Set接口:不允许重复元素的集合。常见的实现类有HashSetLinkedHashSetTreeSet等。
  • Queue接口: 用于表示队列的数据结构。 常见的实现类有LinkedList、PriorityQueue等。
  • Map接口: 表示键值对的集合。常见的实现类有HashMap、LinkedHashMap、TreeMap 等。
  1. 线程不安全的集合类
  • ArrayListLinkedListHashSetHashMap:这些集合类是非线程安全的。在多线程环境中,如果没有适当的同步措施,对这些集合的并发操作可能导致不确定的结果。
  • TreeMapTreeSet: 虽然 TreeMap 和、 TreeSet 是有序的集合,但它们也是非线程安全的。
  1. 线程安全的集合类
  • Vector:类似于ArrayList, 它的方法都是同步的,因此是线程安全的。然而,它相对较重,不够灵活,现在通常建议使用 ArrayList
  • HashTable:类似于HashMap,但它是线程安全的,通过同步整个对象实现。但它的使用已经不太推荐,通常建议使用 HashMap
  • ConcurrentHashMap:提供更好的并发性能,通过锁分离技术实现线程安全。
  • Collections.synchronizedListCollections.synchronizedSetCollections.synchronizedMap: 这些方法可以将非线程安全的集合包装成线程安全的集合。

ArrayList 和 Array 有什么区别?ArrayList 和 LinkedList 的区别是什么?

  1. ArrayList vs Array
    • ArrayList是动态数组的实现,而Array 是固定长度的数组。
    • ArrayList 提供了更多的功能,比如自动扩容、增加和删除元素等,Array则没有这些功能。
    • Array 可以直接存储基本类型数据,也可以存储对象。 ArrayList 中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)
    • 在随机访问时,Array由于其连续内存存储,性能通常优于ArrayList
  2. ArrayList vs LinkedList
    • ArrayList基于动态数组,LinkedList 基于双向链表。
    • 随机访问:ArrayList在随机访问时性能更好,而LinkedList访问元素时效率较低,因为需要从头开始或从尾开始通过链接遍历,时间复杂度为 O(n)
    • 删除/添加元素:在ArrayList末尾添加元素通常很快,但在ArrayList中间或开始插入或删除元素时,可能需要移动后续元素,时间复杂度为 O(n)。而LinkedList添加和删除元素时性能更佳, 只需改变节点的引用。
    • 扩容:当容量不足以容纳更多元素时,ArrayList 会扩容,这个过程涉及创建新数组和复制旧数组的内容,有一定的开销。
    • 使用场景:选择 ArrayList 或者 LinkedList 通常取决于你的 Java 应用是否需要频繁的随机访问操作,还是更多的插入和删除操作。

总结来说,ArrayList和Array的主要区别在于动态大小和功能,而ArrayList和LinkedList的主要区别在于底层数据结构和它们对元素操作的性能特点。选择使用哪一个取决于具体的应用场景和性能需求。

ArrayList的扩容机制

  1. ArrayList扩容的本质就是计算出新的扩容数组的size 后实例化,并将原有数组内容复制到新数组中去。(不是原数组,而是新数组然后给予数组对象地址)。
  2. 当创建一个ArrayList对象时,它会分配一定的初始容量,通常为10。
  3. 当ArrayList中的元素数量达到当前容量时,ArrayList会自动增加其容量。ArrayList 扩容的计算是在一个grow() 方法里面, grow 方法先尝试将数组扩大为原数组的1.5倍。(默新容量=旧容量右移一位(相当于除于2)在加上旧容量)
  4. 若新的容量满足需求,会调用一个Arrays.copyof 方法, 将所有的元素从旧数组复制到新数组中,这个方法是真正实现扩容的步骤。如果扩容后的新容量还是不满足需求,那新容量大小为当前所需的容量加1。

HashMap的底层实现是什么?

在JDK 1.8之前,HashMap数组和链表组成,当发生哈希冲突时,多个元素会以链表的形式存储在同一个数组位置。JDK 1.8开始引入了红黑树,当链表长度超过一定阈值(TREEIFY_THRESHOLD,默认为8)时,链表会转换成红黑树,以提高搜索效率。

  1. 为什么链表大小超过 8 会自动转为红黑树,小于 6 时重新变成链表

根据泊松分布 ,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分 之一,所以将7作为一个分水岭, 等于 7 的时候不转换,大于等于 8 的时候才转换成红黑树,小于等于 6 的时候转化为链表。

  1. 为什么要引入红黑树,而不是其他树?

    是因为红黑树具有以下几点性质

    • 不追求绝对的平衡,插入或删除节点时,允许有一定的局部不平衡,相较于AVL树的绝对自平衡,减少了很多性能开销;
    • 红黑树是一种自平衡的二叉搜索树,因此插入和删除操作的时间复杂度都是O(log n)
  2. HashMap 读和写的时间复杂度是多少?

  • 读:
    • 在最佳情况下:读操作的时间复杂度是 O(1)
    • 最坏情况下:发生哈希冲突,链表为O(n), 红黑树为O(log n)。
  • 写:
  • 理想情况:与读操作类似,如果哈希函数分布均匀,写操作的时间复杂度也是 O(1)。
  • 处理哈希冲突:如果发生哈希冲突,需要在链表尾部添加新元素或将链表转换为红黑树。在这种情况下,写操作的时间复杂度可能达到 O(n),其中 n 是链表的长度。

解决Hash冲突的方法有哪些?HashMap 是如何解决 hash 冲突的

解决哈希冲突的方法主要有以下两种:

  1. 链地址法:在数组的每个位置维护一个链表。当发生冲突时,新的元素会被添加到链表的尾部。
  2. 开放寻址法:当发生冲突时,根据某种探测算法在哈希表中寻找下一个空闲位置来存储元素。

Java中的 HashMap使用链地址法解决hash冲突。

HashMap和put方法流程

  1. 判断键值对数组是否为空或为null,否则执行resize()进行扩容;
  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向步骤6,如果table[i]不为空,转向步骤3;
  3. 判断数组的首个元素是否和key一样,如果相同直接覆盖value,否则转向4,这里的相同指的是hashCode以及equals;
  4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5;
  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量,如果超过,进行扩容。

HashMap 的扩容机制

  1. Java1.7 扩容机制
  • 生成新数组;
  • 遍历老数组中的每个位置上的链表上的每个元素;
  • 获取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标;
  • 将元素添加到新数组中去;
  • 所有元素转移完之后,将新数组赋值给HashMap对象的table属性。
  1. JDK1.8版本扩容
  • 生成新数组;
  • 遍历老数组中的每个位置上的链表或红黑树;
  • 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去;
  • 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置;
    • 统计每个下标位置的元素个数;
    • 如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点添加到新数组的对应位置;
    • 如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置;
  • 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性。

HashMap为什么是线程不安全的?如何实现线程安全

(1) 为什么是线程不安全的

主要原因是它的操作不是原子的,即在多个线程同时进行读写操作时,可能会导致数据不一致性或抛出异常.

  1. 并发修改:当一个线程进行写操作(插入、删除等)时,另一个线程进行读操作,可能会导致读取到不一致的数据,甚至抛出 ConcurrentModificationException 异常。
  2. 非原子性操作:HashMap 的一些操作不是原子的,例如,检查是否存在某个键、获取某个键对应的值等,这样在多线程环境中可能发生竞态条件。

(2)如何实现线程安全

为了实现线程安全的 HashMap,有以下几种方式:

  • 使用Collections.synchronizedMap()方法:可以通过 Collections.synchronizedMap() 方法创建一个线程安全的 HashMap,该方法返回一个同步的 Map 包装器,使得所有对 Map 的操作都是同步的。
  • 使用ConcurrentHashMapConcurrentHashMap 是专门设计用于多线程环境的哈希表实现。它使用分段锁机制,允许多个线程同时进行读操作,提高并发性能。
  • 使用锁机制:可以在自定义的 HashMap 操作中使用显式的锁(例如 ReentrantLock)来保证线程安全。

concurrentHashMap 如何保证线程安全

  1. ConcurrentHashMap 在JDK 1.7中使用的数组 加 链表的结构,其中数组分为两类,大树组 Segment 和 小数组 HashEntryConcurrentHashMap 的线程安全是建立在 SegmentReentrantLock 重入锁来保证
  2. ConcurrentHashMap 在JDK1.8中使用的是数组 加 链表 加 红黑树的方式实现,它是通过 CAS 或者 synchronized 来保证线程安全的,并且缩小了锁的粒度,查询性能也更高。

HashMap和ConcurrentHashMap的区别

  1. 线程安全性:
  • HashMap 不是线程安全的。在多线程环境中,如果同时进行读写操作,可能会导致数据不一致或抛出异常。
  • ConcurrentHashMap 是线程安全的,它使用了分段锁(Segment Locking)的机制,将整个数据结构分成多个段(Segment),每个段都有自己的锁。这样,不同的线程可以同时访问不同的段,提高并发性能。
  1. 同步机制:
  • HashMap 在实现上没有明确的同步机制,需要在外部进行同步,例如通过使用 Collections.synchronizedMap() 方法。
  • ConcurrentHashMap 内部使用了一种更细粒度的锁机制,因此在多线程环境中具有更好的性能。
  1. 迭代时是否需要加锁:
  • HashMap 中,如果在迭代过程中有其他线程对其进行修改,可能抛出 ConcurrentModificationException 异常。
  • ConcurrentHashMap 允许在迭代时进行并发的插入和删除操作,而不会抛出异常。但是,它并不保证迭代器的顺序,因为不同的段可能会以不同的顺序完成操作。
  1. 初始化容量和负载因子:
  • HashMap 可以通过构造方法设置初始容量和负载因子。
  • ConcurrentHashMap 在Java 8及之后版本中引入了ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)构造方法,允许设置初始容量、负载因子和并发级别。
  1. 性能:
  • 在低并发情况下,HashMap 的性能可能会比 ConcurrentHashMap 稍好,因为 ConcurrentHashMap 需要维护额外的并发控制。
  • 在高并发情况下,ConcurrentHashMap 的性能通常更好,因为它能够更有效地支持并发访问。

总的来说,如果需要在多线程环境中使用哈希表,而且需要高性能的并发访问,通常会选择使用 ConcurrentHashMap。如果在单线程环境中使用,或者能够手动进行外部同步管理,那么 HashMap 可能是更简单的选择。

HashSet和HashMap的区别

HashMap 适用于需要存储键值对的情况,而 HashSet 适用于只关心元素唯一性的情况。在某些情况下,可以使用 HashMap 来模拟 HashSet 的行为,只使用键而将值设为固定的常量。

  1. 使用
    • HashMap 用于存储键值对,其中每个键都唯一,每个键关联一个值。
    • HashSet 用于存储唯一的元素,不允许重复。
  1. 内部实现:
    • HashMap 使用键值对的方式存储数据,通过哈希表实现。
    • HashSet 实际上是基于 HashMap 实现的,它只使用了 HashMap 的键部分,将值部分设置为一个固定的常量。
  1. 元素类型:
    • HashMap 存储键值对,可以通过键获取对应的值。
    • HashSet 存储单一元素,只能通过元素本身进行操作。
  1. 允许 null:
    • HashMap 允许键和值都为 null。
    • HashSet 允许存储一个 null 元素。
  1. 迭代方式:
    • HashMap 的迭代是通过迭代器或增强型 for 循环遍历键值对。
    • HashSet 的迭代是通过迭代器或增强型 for 循环遍历元素。
  1. 关联关系:
    • HashMap 中的键与值是一一对应的关系。
    • HashSet 中的元素没有关联的值,只有元素本身。
  1. 性能影响:
    • HashMap 的性能受到键的哈希分布和哈希冲突的影响。
    • HashSet 的性能也受到元素的哈希分布和哈希冲突的影响,但由于它只存储键,通常比 HashMap 的性能稍好。

HashMap和HashTable的区别

  1. 同步

Hashtable 是同步的,即它的方法是线程安全的。这是通过在每个方法上添加同步关键字来实现的,但这也可能导致性能下降。

HashMap 不是同步的,因此它不保证在多线程环境中的线程安全性。如果需要同步,可以使用 Collections.synchronizedMap() 方法来创建一个同步的 HashMap

  1. 性能
  • 由于 Hashtable 是同步的,它在多线程环境中的性能可能较差。
  • HashMap 在单线程环境中可能比 Hashtable 更快,因为它没有同步开销。
  1. 空值
  • Hashtable 不允许键或值为 null
  • HashMap 允许键和值都为 null
  1. 继承关系

HashtableDictionary 类的子类,而 HashMapAbstractMap 类的子类,实现了 Map 接口。

  1. 迭代器
  • Hashtable 的迭代器是通过 Enumerator 实现的。
  • HashMap 的迭代器是通过 Iterator 实现的。
  1. 初始容量和加载因子
  • Hashtable 的初始容量和加载因子是固定的。
  • HashMap 允许通过构造方法设置初始容量和加载因子,以便更好地调整性能。

Java创建线程有哪几种方式

Java 中,创建线程有四种方式,分别是 继承Thread类实现Runnable接口使用Callable和Future, 使用线程池.

  1. 继承Thread类: 通过创建Thread类的子类,并重写其run方法来定义线程执行的任务。
  2. 实现Runnable接口: 创建一个实现了Runnable接口的类,并实现其run方法。然后创建该类的实例,并将其作为参数传递给Thread 对象。
  3. 使用Callable和Future接口:创建一个实现了Callable接口的类,并实现其call方法,该方法可以返回结果并抛出异常。使用ExecutorService来管理线程池,并提交Callable任务获取Future对象,以便在未来某个时刻获取Callable任务的计算结果。
  4. 使用线程池:通过使用Executors类创建线程池,并通过线程池来管理线程的创建和复用。

线程start和run 的区别

在Java多线程中,run 方法和 start 方法的区别在于:

  1. run 方法是线程的执行体,包含线程要执行的代码,当直接调用 run 方法时,它会在当前线程的上下文中执行,而不会创建新的线程。
  2. start 方法用于启动一个新的线程,并在新线程中执行 run 方法的代码。调用 start 方法会为线程分配系统资源,并将线程置于就绪状态,当调度器选择该线程时,会执行 run 方法中的代码。

因此,虽然可以直接调用 run 方法,但这并不会创建一个新的线程,而是在当前线程中执行 run 方法的代码。如果需要实现多线程执行,则应该调用 start 方法来启动新线程。

Java中有哪些锁

  1. 公平锁/非公平锁:公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优 先获取锁。有可能,会造成优先级反转或者饥饿现象。 对于 Java ReentrantLock 而言,默认是非公平锁,对于 Synchronized 而言,也是一种非公平锁。
  2. 可重入锁(递归锁):在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。对于 Java ReentrantLock 而言,是可重入锁,对于 Synchronized 而言,也是一个可重入锁。
  3. 独享锁/共享锁:独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。对于 Java ReentrantLock 而言,其是独享锁。但是对于Lock的另一个实现类 ReadWriteLock,其读锁 是共享锁,其写锁是独享锁。 对于 Synchronized 而言,当然是独享锁。
  4. 互斥锁/读写锁:上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在Java中的具体实现就是 ReentrantLock。读写锁在Java中的具体实现就是 ReadWriteLock
  5. 乐观锁/悲观锁:乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加 锁会带来大量的性能提升。
  6. 分段锁:分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就 是通过分段锁的形式来实现高效的并发操作。
  7. 偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对 Synchronized。在Java 5通过引入锁升级的机制来实现高效 Synchronized 。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
  8. 自选锁:在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好 处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

说说对synchronized的理解

synchronized是Java中的一个关键字,用于实现同步和线程安全。

  • 当一个方法或代码块被 synchronized 修饰时,它将成为一个临界区,同一时刻只能由一个线程访问。其他线程必须等待当前线程退出临界区才能进入。确保多个线程在访问共享资源时不会产生冲突
  • synchronized 可以应用于方法或代码块。当它应用于方法时,整个方法被锁定;当它应用于代码块时,只有该代码块被锁定。这样做的好处是,可以选择性地锁定对象的一部分,而不是整个方法。
  • synchronized 实现的机理依赖于软件层面上的JVM,因此其性能会随着Java版本的不断升级而提高。 到了 Java1.6synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.71.8 中,均对该关键字的实现机理做了优化。 需要说明的是,当线程通过 synchronized 等待锁时是不能被 Thread.interrupt() 中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁的尴尬境地。
  • 最后,尽管 Java 实现的锁机制有很多种,并且有些锁机制性能也比 synchronized 高,但还是强烈推荐在 多线程应用程序中使用该关键字,因为实现方便,后续工作由 JVM 来完成,可靠性高。只有在确定锁机 制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如 ReentrantLock 等。

synchronized和lock的区别是什么

  1. synchronizedLock都是Java中用于实现线程同步的手段,synchronized是Java的关键字,基于JVM的内置锁实现,可以用于修饰方法或代码块,使用起来比较简单直接。而Lock是一个接口,是Java提供的显式锁机制,需要手动获取和释放锁,通过实现类(如ReentrantLock)来创建锁对象,然后主动调用锁的获取和释放方法。

  2. 特性

    • synchronized:灵活性相对较低,只能用于方法或代码块。而且synchronized方法一旦开始执行,即使线程被阻塞,也不能中断。没有超时机制,一旦获取不到锁就会一直等待,也没有公平性的概念,线程调度由JVM控制。
    • lock:提供了更多的灵活性,例如可以尝试获取锁,如果锁已被其他线程持有,可以选择等待或者中断等待。提供了超时获取锁的能力,可以在指定时间内尝试获取锁,也可以设置为公平锁,按照请求锁的顺序来获取锁。
  3. 等待与通知:

    • synchronized:与 wait()notify()/notifyAll() 方法一起使用,用于线程的等待和通知。
    • lock:可以与 Condition 接口结合,实现更细粒度的线程等待和通知机制。
  4. 使用场景:

    总结来说,synchronized使用简单,适合锁的粒度较小、竞争不激烈、实现简单的场景。而Lock提供了更多的灵活性和控制能力,适用于需要更复杂同步控制的场景。

synchronized和ReentrantLock的区别是什么

  1. synchronizedReentrantLock都是Java中用于实现线程同步的手段,synchronized是Java的关键字,基于JVM的内置锁实现,可以用于修饰方法或代码块,使用起来比较简单直接。而ReentrantLockjava.util.concurrent.locks包中的一个锁实现,需要显式创建,并通过调用lock()unlock()方法来管理锁的获取和释放。

  2. 特性

    • synchronized:灵活性相对较低,只能用于方法或代码块。而且synchronized方法一旦开始执行,即使线程被阻塞,也不能中断。没有超时机制,一旦获取不到锁就会一直等待,也没有公平性的概念,线程调度由JVM控制。
    • ReentrantLock:支持中断操作,可以在等待锁的过程中响应中断, 提供了尝试获取锁的超时机制,可以通过tryLock()方法设置超时时间。可以设置为公平锁,按照请求的顺序来获取锁,提供了isLocked()isFair()等方法,可以检查锁的状态。
  3. 条件变量:

    • synchronized可以通过wait()notify()notifyAll()与对象的监视器方法配合使用来实现条件变量。
    • ReentrantLock可以通过Condition新API实现更灵活的条件变量控制。
  4. 锁绑定多个条件:

    • synchronized与单个条件关联,需要使用多个方法调用来实现复杂的条件判断。
    • ReentrantLock可以与多个Condition对象关联,每个对象可以有不同的等待和唤醒逻辑。
  5. 使用场景:

    总结来说,synchronized适合简单的同步需求,而ReentrantLock提供了更丰富的控制能力和灵活性,适用于需要复杂同步控制的场景。

为什么要有线程池

  • 资源管理: 在多线程应用中,每个线程都需要占用内存和CPU资源,如果不加限制地创建线程,会导致系统资源耗尽,可能引发系统崩溃。线程池通过限制并控制线程的数量,帮助避免这个问题。
  • 提高性能:通过重用已存在的线程,线程池可以减少创建和销毁线程的开销。
  • 任务排队:线程池通过任务队列和工作线程的配合,合理分配任务,确保任务按照一定的顺序执行,避免线程竞争和冲突
  • 统一管理:线程池提供了统一的线程管理方式,可以对线程进行监控、调度和管理。

总结:采用多线程编程的时候如果线程过多会造成系统资源的大量占用,降低系统效率。如果有些线程存活的时间很短但是又不得不创建很多这种线程也会造成资源的浪费。线程池的作用就是创造并且管理一部分线程,当系统需要处理任务时直接将任务添加到线程池的任务队列中,由线程池决定由哪个空闲且存活线程来处理,当线程池中线程不够时会适当创建一部分线程,线程冗余时会销毁一部分线程。这样提高线程的利用率,降低系统资源的消耗。

说一说线程池有哪些常用参数

  • corePoolSize核心线程数:线程池中长期存活的线程数。
  • maximumPoolSize最大线程数:线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数。
  • keepAliveTime空闲线程存活时间:当线程数大于corePoolSize时,多余的空闲线程能等待新任务的最长时间。
  • TimeUnit: 与keepAliveTime一起使用,指定keepAliveTime的时间单位,如秒、分钟等。
  • workQueue线程池任务队列: 线程池存放任务的队列,用来存储线程池的所有待执行任务。
  • ThreadFactory:创建线程的工厂: 线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。
  • RejectedExecutionHandler拒绝策略: 当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。

BIO、NIO、AIO 的区别

BIO、AIO和NIO是Java中不同的I/O模型,它们在处理输入输出操作时有不同的特点。

  • BIO: 阻塞式的I/O模型。当一个线程执行I/O操作时,如果数据还没准备好,这个线程会被阻塞,直到数据到达。适合连接数较少且固定的场景,但扩展性较差。
  • NIO: 非阻塞的I/O模型。NIO使用缓冲区和通道来处理数据,提高了I/O操作的效率。支持面向缓冲区的读写操作,可以处理大量并发的连接。
  • AIO: 异步I/O模型,从Java 7开始引入。在AIO中,I/O操作被发起后,线程可以继续执行其他任务,一旦I/O操作完成,操作系统会通知线程。适合需要处理大量并发I/O操作,且希望避免I/O操作阻塞线程的场景。
  • 使用场景:
    • BIO适合低并发、连接数较少的应用。
    • NIO适合高并发、需要处理大量连接的应用。
    • AIO适合需要高性能、异步处理I/O操作的场景。

Java内存区域有哪些划分

【java】jvm内存模型全面解析_哔哩哔哩_bilibili

Java的内存区域主要分为以下几个部分:

  1. 程序计数器:程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。在多线程环境下,每个线程都有自己独立的程序计数器。当线程执行Java方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址。
  2. Java虚拟机栈:每个Java线程都有一个私有的Java虚拟机栈,与线程同时创建。每个方法在执行时都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。栈帧在方法调用时入栈,方法返回时出栈。
  3. 本地方法栈: 本地方法栈与Java虚拟机栈类似,但它为本地方法服务。本地方法是用其他编程语言(如C/C++)编写的,通过JNI与Java代码进行交互。
  4. :Java堆是Java虚拟机中最大的一块内存区域,用于存储对象实例。所有的对象实例和数组都在堆上分配内存。堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆可以分为新生代和老年代等不同的区域,其中新生代又包括Eden空间、Survivor空间(From和To)。
  5. 方法区: 方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在HotSpot虚拟机中,方法区也被称为永久代,但在较新的JVM版本中,永久代被元空间所取代。
  6. 运行时常量池:是方法区的一部分,用于存储编译期生成的类、方法和常量等信息。
  7. 字符串常量池: 字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
  8. 直接内存:不是Java虚拟机运行时数据区的一部分,但Java可以通过NIO操作直接内存,提高IO性能。

介绍一下什么是强引用、软引用、弱引用、虚引用

这四种引用决定了对象的生命周期以及垃圾收集器如何收集垃圾。

  1. 强引用:最常见的引用类型。如果一个对象具有强引用,那么垃圾收集器绝不会回收它。
  2. 软引用:软引用用于描述一些还有用但非必需的对象。如果一个对象只有软引用指向它,那么在系统内存不足时,垃圾回收器会尝试回收这些对象。软引用通常用于实现内存敏感的缓存,可以在内存不足时释放缓存中的对象。
  3. 弱引用:弱引用比软引用的生命周期更短暂。如果一个对象只有弱引用指向它,在进行下一次垃圾回收时,不论系统内存是否充足,这些对象都会被回收。弱引用通常用于实现对象缓存,但不希望缓存的对象影响垃圾回收的情况。
  4. 虚引用:虚引用是Java中最弱的引用类型。如果一个对象只有虚引用指向它,那么无论何时都可能被垃圾回收器回收,但在对象被回收之前,虚引用会被放入一个队列中,供程序员进行处理。虚引用主要用于跟踪对象被垃圾回收的时机,进行一些必要的清理或记录。

有哪些垃圾回收算法

  1. 标记-清除算法

标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。 在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引 用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。 适用场合:

  • 存活对象较多的情况下比较高效
  • 适用于年老代(即旧生代)
  1. 复制算法

从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存上去,之后将原来的那一块儿内存全部回收掉 现在的商业虚拟机都采用这种收集算法来回收新生代。 适用场合:

  • 存活对象较少的情况下比较高效
  • 扫描了整个空间一次(标记存活对象并复制移动)
  • 适用于年轻代(即新生代):基本上98%的对象是”朝生夕死”的,存活下来的会很少

缺点:

  • 需要一块儿空的内存空间
  • 需要复制移动对象
  • 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
  1. 标记整理

标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。 首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

  1. 分代收集算法

分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于新年代的问题,将内存分 为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。 在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率搞,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。

有哪些垃圾回收器

  1. 新生代垃圾收集器
  • Serial 收集器(复制算法)是新生代单线程收集器,优点是简单高效,算是最基本、发展历史最悠久的收集器。它在进 行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。
  • ParNew 收集器(复制算法)是新生代并行收集器,其实就是 Serial 收集器的多线程版本。
  • Parallel Scavenge 收集器是新生代并行收集器,追求高吞吐量,高效利用 CPU。
  1. 老年代垃圾收集器
  • Serial Old是Serial收集器的老年代版本,它同样是一个单线程(串行)收集器,使用标记整理算法。这个收 集器的主要意义也是在于给Client模式下的虚拟机使用

  • Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在 1.6中才开始提供。

  • CMS 收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速 度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求

    CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对前面几种收集器来说更复杂一些,整个过 程分为4个步骤:

    • 初始标记
    • 并发标记
    • 重新标记
    • 并发清除
  1. 新生代和老年代垃圾收集器
  • G1收集器-标记整理算法 :JDK1.7后全新的回收器, 用于取代CMS收集器。

    G1 收集器的优势:

    • 独特的分代垃圾回收器,分代GC: 分代收集器, 同时兼顾年轻代和老年代
    • 使用分区算法, 不要求eden, 年轻代或老年代的空间都连续
    • 并行性: 回收期间, 可由多个线程同时工作, 有效利用多核cpu资源
    • 空间整理: 回收过程中, 会进行适当对象移动, 减少空间碎片
    • 可预⻅性: G1可选取部分区域进行回收, 可以缩小回收范围, 减少全局停顿

G1收集器的阶段分以下几个步骤:

  • 初始标记(它标记了从GC Root开始直接可达的对象)
  • 并发标记(从GC Roots开始对堆中对象进行可达性分析,找出存活对象)
  • 最终标记(标记那些在并发标记阶段发生变化的对象,将被回收)
  • 筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计 划,回收一部分Region)

类加载机制介绍一下

类加载机制是Java虚拟机运行Java程序时负责将类加载到内存中的过程。它包括以下几个步骤:

  1. 加载: 在此阶段,类加载器负责查找类的字节码文件,并将其加载到内存中。字节码可以来自文件系统、网络等位置。加载阶段不会执行类中的静态初始化代码。
  2. 连接:连接阶段包括三个子阶段:
    • 验证:确保加载的类文件格式正确,并且不包含不安全的构造。
    • 准备:在内存中为类的静态变量分配内存空间,并设置默认初始值。这些变量在此阶段被初始化为默认值,比如数值类型为0,引用类型为null。
    • 解析:将类、接口、字段和方法的符号引用解析为直接引用,即内存地址。这一步骤可能包括将常量池中的符号引用解析为直接引用。
  3. 初始化:在此阶段,执行类的静态初始化代码,包括静态字段的赋值和静态代码块的执行。静态初始化在类的首次使用时进行,可以是创建实例、访问静态字段或调用静态方法。

介绍一下双亲委派机制

双亲委派机制是Java类加载器中的一种设计模式,用于确定类的加载方式和顺序。这个机制确保了Java核心库的安全性和一致性。该机制的核心思想是:如果一个类加载器收到了类加载请求,默认先将该请求委托给其父类加载器处理。只有当父级加载器无法加载该类时,才会尝试自行加载。

双亲委派机制能够提高安全性,防止核心库的类被篡改。因为所有的类最终都会通过顶层的启动类加载器进行加载。另外由于类加载器直接从父类加载器那里加载类,也避免了类的重复加载。

说一说你对Spring AOP的了解

面向切面编程,可以说是面向对象编程的补充和完善。OOP 引入封装、继承、多态等概念来建立一种对象层次结构。不过 OOP允许开发者定义纵向的关系,但并不适合定义横向的 关系,例如日志功能。 AOP 技术恰恰相反,它利用一种称为”横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的 公共行为封装到一个可重用模块,并将其命名为切面。所谓”切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的 耦合度,并有利于未来的可操作性和可维护性。

SpringAOP 代理由 Spring 的IOC 容器负责生成、管理,其依赖关系也由 IOC 容器负责管理。因此, AOP 代

理可以直接使用容器中的其它 bean 实例作为目标,这种关系可由 IOC 容器的依赖注入提供。Spring 创建代理的规则为:

  • 默认使用 JDK 动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了
  • 当需要代理类,而不是代理接口的时候,Spring 会切换为使用 CGLIB代理 ,也可强制使用 CGLIB

AOP 编程其实是很简单的事情,纵观 AOP 编程,程序员只需要参与三个部分:

  • 定义普通业务组件
  • 定义切入点,一个切入点可能横切多个业务组件
  • 定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作

所以进行 AOP 编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP 框架将自动生成 AOP 代理,即: 代理对象的方法=增强处理+被代理对象的方法。

说一说你对 Spring中IOC的理解

  1. 什么是 IOC

Spring的IOC,也就是控制反转,它的核心思想是让对象的创建和依赖关系由容器来控制,不是我们自己new出来的,这样各个组件之间就能保持松散的耦合。

这里的容器实际上就是个Map, Map 中存放的是各种对象。通过DI依赖注入,Spring容器可以在运行时动态地将依赖注入到需要它们的对象中,而不是对象自己去寻找或创建依赖。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。举例来说,在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了。

  1. 如何配置

Spring 时代我们一般通过 XML 文件来配置,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。使用配置可以告诉Spring容器如何创建对象、如何管理对象的生命周期。

总结来说,Spring的IOC容器是一个中央化的、负责管理应用中所有对象生命周期的强大工具

Bean的作用域

Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 Bean。而 Bean 的作用域定义了在应用程序中创建的 Bean 实例的生命周期和可见范围,主要有以下几种。

  1. 单例:这是默认的作用域,当一个 Bean 的作用域为 Singleton,那么 Spring IoC 容器中只会存在一个共享的 Bean 实例,并且所有对 Bean 的请求,只要 id 与该 bean 定义相匹配,则只会返回 bean 的同一实例。
  2. 原型:当一个 bean 的作用域为 prototype,表示一个 bean 定义对应多个对象实例。prototype 作用域的 bean 会导致在每次对该 bean 请求时都会创建一个新的 bean 实例。因此,每次请求都会得到一个新的 Bean 实例。
  3. 请求:一个HTTP请求对应一个Bean实例,每个请求都有自己的Bean实例,且该Bean仅在请求期间有效。
  4. 会话:一个HTTP会话对应一个Bean实例,Bean的生命周期与用户的会话周期相同。
  5. 应用程序:对于定义在ServletContext中的Bean,整个Web应用程序共享一个Bean实例。
  6. Websocket: WebSocket生命周期内,每个WebSocket会话拥有一个Bean实例。

Bean的生命周期

Spring Bean的生命周期,其实就是Spring容器从创建Bean到销毁Bean的整个过程。这里面有几个关键步骤:

  1. 实例化Bean: Spring容器通过构造器或工厂方法创建Bean实例。
  2. 设置属性:容器会注入Bean的属性,这些属性可能是其他Bean的引用,也可能是简单的配置值。
  3. 检查Aware接口并设置相关依赖:如果Bean实现了BeanNameAwareBeanFactoryAware接口,容器会调用相应的setBeanNamesetBeanFactory方法。
  4. BeanPostProcessor:在Bean的属性设置之后,Spring会调用所有注册的BeanPostProcessorpostProcessBeforeInitialization方法。
  5. 初始化Bean: 如果Bean实现了InitializingBean接口,容器会调用其afterPropertiesSet方法。同时,如果Bean定义了init-method,容器也会调用这个方法。
  6. BeanPostProcessor的第二次调用**:容器会再次调用所有注册的BeanPostProcessorpostProcessAfterInitialization方法,这次是在Bean初始化之后。
  7. 使用Bean:此时,Bean已经准备好了,可以被应用程序使用了。
  8. 处理DisposableBean和destroy-method:当容器关闭时,如果Bean实现了DisposableBean接口,容器会调用其destroy方法。如果Bean定义了destroy-method,容器也会调用这个方法。
  9. Bean销毁:最后,Bean被Spring容器销毁,结束了它的生命周期。

Spring循环依赖是怎么解决的

  1. 什么是循环依赖

两个或者两个以上的 bean 互相持有对方,最终形成闭环。比如 Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,形成了一个循环依赖关系。这种情况下,如果不处理,会导致 Spring 容器无法完成 Bean 的初始化,从而抛出循环依赖异常。

  1. 怎么检测是否存在循环依赖

检测循环依赖相对比较容易,Bean在创建的时候可以给该Bean打标,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。

  1. 如何解决
  • 构造器循环依赖:Spring容器在创建Bean时,如果遇到循环依赖,通常是无法处理的,因为这会导致无限递归创建Bean实例。所以,构造器注入是不支持循环依赖的。

  • 字段注入或Setter注入:使用了

    三级缓存

    来解决循环依赖问题。

    • 首先,Spring容器会创建一个Bean的原始实例,但此时Bean的属性尚未设置,这个实例被存放在一级缓存中。
    • 当Bean的属性被设置时,如果属性值是其他Bean的引用,Spring会去检查二级缓存,看是否已经有该Bean的引用存在。
    • 如果二级缓存中没有,Spring会尝试创建这个被引用的Bean,并将其放入三级缓存。
    • 最后,当Bean的属性设置完成后,原始的Bean实例会被放入二级缓存,供其他Bean引用
  • 使用@Lazy注解:通过@Lazy注解,可以延迟Bean的加载,直到它被实际使用时才创建,这可以避免一些循环依赖的问题。

Spring 中用到了那些设计模式

  • 工厂设计模式 : Spring 使用工厂模式通过 BeanFactoryApplicationContext 创建 bean 对象。
  • 代理设计模式 : Spring AOP 功能的实现。
  • 单例设计模式 :Spring 中的 Bean 默认都是单例的。
  • 模板方法模式 : SpringjdbcTemplatehibernateTemplate 等以 Template 结尾的对数据库操 作的类,它们就使用到了模板模式。
  • 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访 问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
  • 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
  • 适配器模式 :Spring AOP 的增强或通知 (Advice) 使用到了适配器模式、Spring MVC 中也是用到了 适配器模式适配 Controller

描述一下SpringMVC的执行流程

  1. 用户发送请求至前端控制器 DispatcherServlet
  2. DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器。
  3. 处理器映射器找到具体的处理器(可以根据 xml 配置、注解进行查找),生成处理器对象及处理器拦截 器(如果有则生成)一并返回给 DispatcherServlet
  4. DispatcherServlet 调用 HandlerAdapter 处理器适配器。
  5. HandlerAdapter 经过适配调用具体的处理器(Controller,也叫后端控制器)。
  6. Controller 执行完成返回 ModelAndView
  7. HandlerAdaptercontroller 执行结果 ModelAndView 返回给 DispatcherServlet
  8. DispatcherServletModelAndView 传给 ViewReslover 视图解析器。
  9. ViewReslover 解析后返回具体 View
  10. DispatcherServlet 根据 View 进行渲染视图(即将模型数据填充至视图中)
  11. DispatcherServlet响应用户。

SpringBoot Starter有什么用

Spring Boot Starter 的作用是简化和加速项目的配置和依赖管理。

  • Spring Boot Starter 可以理解为一种预配置的模块,它封装了特定功能的依赖项和配置, ,开发者只需引入相关的 Starter 依赖,无需手动配置大量的参数和依赖项。常用的启动器包括 spring-boot-starter-web(用于Web应用)、spring-boot-starter-data-jpa(用于数据库访问)等。 引入这些启动器后,Spring Boot 会自动配置所需的组件和 Bean,无需开发者手动添加大量配置。
  • Starter还管理了相关功能的依赖项,包括其他Starter和第三方库,确保它们能够良好地协同工作,避免版本冲突和依赖问题。
  • Spring Boot Starter 的设计使得应用可以通过引入不同的Starter来实现模块化的开发。每个Starter都关注一个特定的功能领域,如数据库访问、消息队列、Web开发等。
  • 开发者可以创建自定义的 Starter,以便在项目中共享和重用特定功能的配置和依赖项。

SpringBoot的常用注解

  1. @SpringBootApplication: 用于标识主应用程序类,通常位于项目的顶级包中。这个注解包含了 @Configuration@EnableAutoConfiguration@ComponentScan
  2. @Controller: 用于标识类作为 Spring MVCController
  3. @RestController: 类似于 @Controller,但它是专门用于 RESTful Web 服务的。它包含了 @Controller@ResponseBody
  4. @RequestMapping: 用于将HTTP请求映射到 Controller 的处理方法。可以用在类级别和方法级别。
  5. @Autowired: 用于自动注入 Spring 容器中的 Bean,可以用在构造方法、字段、Setter 方法上。
  6. @Service: 用于标识类作为服务层的 Bean
  7. @Repository: 用于标识类作为数据访问层的 Bean,通常用于与数据库交互。
  8. @Component: 通用的组件注解,用于标识任何 Spring 托管的 Bean
  9. @Configuration: 用于定义配置类,类中可能包含一些 @Bean 注解用于定义 Bean
  10. @EnableAutoConfiguration: 用于启用 Spring Boot 的自动配置机制,根据项目的依赖和配置自动配置 Spring 应用程序。
  11. @Value: 用于从属性文件或配置中读取值,将值注入到成员变量中。
  12. @Qualifier: 与 @Autowired 一起使用,指定注入时使用的 Bean 名称。
  13. @ConfigurationProperties: 用于将配置文件中的属性映射到 Java Bean
  14. @Profile: 用于定义不同环境下的配置,可以标识在类或方法上。
  15. @Async: 用于将方法标记为异步执行。