计算机网络
TCP/IP网络模型/ OSI模型 OSI模型, 是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系,将计算机网络通信划分为七个不同的层级,每个层级都负责特定的功能。每个层级都构建在其下方的层级之上,并为上方的层级提供服务。七层从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。虽然OSI模型在理论上更全面,但在实际网络通信中,TCP/IP模型更为实用。 TCP/IP模型分为四个层级,每个层级负责特定的网络功能。 1 应用层:该层与OSI模型的应用层和表示层以及会话层类似,提供直接与用户应用程序交互的接口。它为网络上的各种应用程序提供服务,如电子邮件(SMTP)、网页浏览(HTTP)、文件传输(FTP)等。 2 传输层:该层对应OSI模型的传输层。它负责端到端的数据传输,提供可靠的、无连接的数据传输服务。主要的传输层协议有TCP和UDP。TCP提供可靠的数据传输,确保数据的正确性和完整性;而UDP则是无连接的,适用于不要求可靠性的传输,如实时音频和视频流。 3 网际层:该层对应OSI模型的网络层。主要协议是IP,它负责数据包的路由和转发,选择最佳路径将数据从源主机传输到目标主机。IP协议使用IP地址来标识主机和网络,并进行逻辑地址寻址。 4 网络接口层:该层对应OSI模型的数据链路层和物理层。它负责物理传输媒介的传输,例如以太网、Wi-Fi等,并提供错误检测和纠正的功能。此外,网络接口层还包含硬件地址(MAC地址)的管理。
HTTP有哪些请求方式?GET请求和POST请求的区别 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缓存机制的两种类型,它们用于减少服务器的负担和提高网页加载速度。 1 强缓存 :客户端在没有向服务器发送请求的情况下,直接从本地缓存中获取资源。强缓存通过HTTP响应头中的 Cache-Control 字段实现,如 max-age ,告诉浏览器在指定时间内可以直接使用缓存数据,无需再次请求。 2 协商缓存 :当强缓存失效时,浏览器会发送请求到服务器,通过 ETag 或 Last-Modified 等HTTP响应头与服务器进行验证,以确定资源是否被修改。如果资源未修改,服务器返回 304 Not Modified 状态码,告知浏览器使用本地缓存;如果资源已修改,则返回新的资源,浏览器更新本地缓存。这种方式需要与服务器通信,但可以确保用户总是获取最新的内容。 强缓存和协商缓存是HTTP缓存机制的两种类型,它们用于减少服务器的负担和提高网页加载速度。 1 强缓存 :客户端在没有向服务器发送请求的情况下,直接从本地缓存中获取资源。 ● Expires强缓存 :设置一个强缓存时间,此时间范围内,从内存中读取缓存并返回。但是因为 Expires 判断强缓存过期的机制是获取本地时间戳,与之前拿到的资源文件中的 Expires 字段的时间做比较来判断是否需要对服务器发起请求。这里有一个巨大的漏洞:“如果我本地时间不准咋办?”所以目前已经被废弃了。 ● Cache-Control强缓存 :目前使用的强缓存是通过HTTP响应头中的 Cache-Control 字段实现,通过 max-age 来告诉浏览器在指定时间内可以直接使用缓存数据,无需再次请求。 1 协商缓存 :当强缓存失效时,浏览器会发送请求到服务器,通过 ETag 或 Last-Modified 等HTTP响应头与服务器进行验证,以确定资源是否被修改。如果资源未修改,服务器返回 304 Not Modified 状态码,告知浏览器使用本地缓存;如果资源已修改,则返回新的资源,浏览器更新本地缓存。这种方式需要与服务器通信,但可以确保用户总是获取最新的内容。 ● 基于 Last-Modified 的协商缓存 ○ Last-Modified 是资源的最后修改时间,服务器在响应头部中返回。 ○ 当客户端读取到 Last-modified 的时候,会在下次的请求标头中携带一个字段: If-Modified-Since ,而这个请求头中的 If-Modified-Since 就是服务器第一次修改时候给他的时间。 ○ 服务器比较请求中的 If-Modified-Since 值与当前资源的 Last-Modified 值,如果比对的结果是没有变化,表示资源未发生变化,返回状态码 304 Not Modified 。如果比对的结果说资源已经更新了,就会给浏览器正常返回资源,返回200状态。 但是这样的协商缓存有两个缺点: ○ 因为是更改文件修改时间来判断的,所以在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。 ○ 当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会返回新的文件。 ● 基于ETag的协商缓存:将原先协商缓存的比较时间戳的形式修改成了比较文件指纹(根据文件内容计算出的唯一哈希值)。 ○ ETag 是服务器为资源生成的唯一标识符(文件指纹),可以是根据文件内容计算出的哈希值,服务端将其和资源一起放回给客户端。 ○ 客户端在请求头部的 If-None-Match 字段中携带上次响应的 ETag 值。 ○ 服务器比较请求中的 If-None-Match 值与当前资源的 ETag 值,如果匹配,表示资源未发生变化,返回状态码 304 Not Modified 。如果两个文件指纹不吻合,则说明文件被更改,那么将新的文件指纹重新存储到响应头的ETag中并返回给客户端
HTTP1.0和HTTP1.1的区别 1 持久连接 : HTTP/1.1 默认支持持久连接,允许在一个TCP连接上发送多个HTTP请求和响应,减少了连接建立和关闭的开销。而 HTTP/1.0 默认为短连接,每次请求都需要建立一个TCP连接,并通过 Connection: keep-alive 头来实现持久连接。 2 管道化 : HTTP/1.1 支持管道化,允许客户端在第一个请求的响应到达之前发送多个请求,这可以减少等待时间,提高效率。HTTP/1.0不支持管道化。 3 缓存控制 : HTTP1.0 主要使用 If-Modified-Since/Expires 来做为缓存判断的标准,而 HTTP1.1 则引入了更多的缓存控制策略例如 Etag / If-None-Match 等更多可供选择的缓存头来控制缓存策略。 4 错误处理: HTTP/1.1 增加了一些新的HTTP状态码,如 100 Continue ,用于增强错误处理和请求的中间响应。 5 Host 头: HTTP/1.1 引入了 Host 头,允许客户端指定请求的主机名,这使得在同一台服务器上托管多个域名成为可能。HTTP/1.0没有这个头字段。 6 带宽优化 : HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能, 而 HTTP1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content)
HTTP2.0与HTTP1.1的区别? 1 二进制协议 : HTTP/2.0 采用二进制格式传输数据,而非 HTTP/1.1 的文本格式,使得解析更高效,减少了解析时间。 2 多路复用 : HTTP/2.0 支持多路复用,允许在单个TCP连接上并行交错发送多个请求和响应,解决了 HTTP/1.1 中的 队头阻塞 问题。 3 头部压缩 : HTTP/2.0 引入了 HPACK 压缩算法,对请求和响应的头部信息进行压缩,减少了冗余头部信息的传输,提高了传输效率。 4 服务器推送 : HTTP/2.0 允许服务器主动推送资源给客户端,而不需要客户端明确请求,这可以减少页面加载时间。 5 优先级和依赖 : HTTP/2.0 允许客户端为请求设置优先级,并表达请求之间的依赖关系,资源加载更加有序。
HTTPS和HTTP有哪些区别 两者的主要区别在于安全性和数据加密: 1 加密层 : HTTPS 在 HTTP 的基础上增加了 SSL/TLS 协议作为加密层,确保数据传输的安全性。 2 数据安全 : HTTPS 通过加密,保护数据在传输过程中不被窃听或篡改,而 HTTP 数据传输是明文的,容易受到攻击。 3 端口 : HTTPS 通常使用端口 443 ,而 HTTP 使用端口80。
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适用于对实时性要求高的应用,如语音通话、视频会议等。
三次握手的过程,为什么是三次 (1) 三次握手的过程 1 第一次握手 :客户端向服务器发送一个 SYN (同步序列编号)报文,请求建立连接,客户端进入 SYN_SENT 状态。 2 第二次握手 :服务器收到 SYN 报文后,如果同意建立连接,则会发送一个 SYN-ACK (同步确认)报文作为响应,同时进入 SYN_RCVD 状态。 3 第三次握手 :客户端收到服务器的 SYN-ACK 报文后,会发送一个 ACK (确认)报文作为最终响应,之后客户端和服务器都进入 ESTABLISHED 状态,连接建立成功。 (2)为什么需要三次握手 通过三次握手,客户端和服务器都能够确认对方的接收和发送能力。第一次握手确认了客户端到服务器的通道是开放的;第二次握手确认了服务器到客户端的通道是开放的;第三次握手则确认了客户端接收到服务器的确认,从而确保了双方的通道都是可用的。 而如果仅使用两次握手,服务器可能无法确定客户端的接收能力是否正常,比如客户端可能已经关闭了连接,但之前发送的连接请求报文在网络上延迟到达了服务器,服务器就会主动去建立一个连接,但是客户端接收不到,导致资源的浪费。而四次握手可以优化为三次。
四次挥手的过程,为什么是四次 (1)四次挥手的过程 1 第一次挥手:客户端发送一个 FIN 报文给服务端,表示自己要断开数据传送,报文中会指定一个序列号 (seq=x) 。然后,客户端进入 FIN-WAIT-1 状态。 2 第二次挥手:服务端收到 FIN 报文后,回复 ACK 报文给客户端,且把客户端的序列号值 +1 ,作为ACK +1 报文的序列号 (seq=x+1) 。然后,服务端进入 CLOSE-WAIT``(seq=x+1) 状态,客户端进入 FIN-WAIT-2 状态。 3 第三次挥手:服务端也要断开连接时,发送 FIN 报文给客户端,且指定一个序列号 (seq=y+1) ,随后服务端进入 LAST-ACK 状态。 4 第四次挥手:客户端收到 FIN 报文后,发出 ACK 报文进行应答,并把服务端的序列号值 +1 作为 ACK 报文序列号 (seq=y+2) 。此时客户端进入 TIME-WAIT 状态。服务端在收到客户端的 ACK 报文后进入 CLOSE 状态。如果客户端等待 2MSL 没有收到回复,才关闭连接。 (2)为什么需要四次挥手 TCP 是全双工通信,可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。 当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后才会完全关闭 TCP 连接。因此两次握手可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次握手。 只有通过四次挥手,才可以确保双方都能接收到对方的最后一个数据段的确认,主动关闭方在发送完最后一个 ACK 后进入 TIME-WAIT 状态,这是为了确保被动关闭方接收到最终的 ACK ,如果被动关闭方没有接收到,它可以重发 FIN 报文,主动关闭方可以再次发送 ACK 。 而如果使用三次挥手,被动关闭方可能在发送最后一个数据段后立即关闭连接,而主动关闭方可能还没有接收到这个数据段的确认。
TCP连接如何确保可靠性 TCP通过序列号、确认应答、超时重传、数据校验、流量控制、拥塞控制等机制,确保了数据传输的可靠性和效率。 1 序列号 :每个TCP段都有一个序列号,确保数据包的顺序正确。 2 确认应答 :接收方发送ACK确认收到的数据,如果发送方在一定时间内没有收到确认,会重新发送数据。 3 超时重传 :发送方设置一个定时器,如果在定时器超时之前没有收到确认,发送方会重传数据。 4 数据校验 :TCP使用校验和来检测数据在传输过程中是否出现错误,如果检测到错误,接收方会丢弃该数据包,并等待重传。 5 流量控制 :TCP通过滑动窗口机制进行流量控制,确保接收方能够处理发送方的数据量。 6 拥塞控制 :TCP通过算法如慢启动、拥塞避免、快重传和快恢复等,来控制数据的发送速率,防止网络拥塞。
拥塞控制的实现机制 1 慢启动: 开始时以较低的速率发送数据。随着每次成功收到确认的数据,发送方逐渐增加发送窗口的大小,实现指数级的增长,这称为慢启动。 2 拥塞避免: 一旦达到一定的阈值(通常是慢启动阈值),TCP发送方就会进入拥塞避免阶段。在拥塞避免阶段,发送方以线性增加的方式增加发送窗口的大小,而不再是指数级的增长。 3 快速重传: 如果发送方连续收到相同的确认,它会认为发生了数据包的丢失,并会快速重传未确认的数据包,而不必等待超时。 4 快速恢复: 在发生快速重传后,TCP进入快速恢复阶段。在这个阶段,发送方不会回到慢启动阶段,而是将慢启动阈值设置为当前窗口的一半,并将拥塞窗口大小设置为慢启动阈值加上已确认但未被快速重传的数据块的数量。
HTTP的Keep-Alive是什么?TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是一个东西吗? 1 HTTP 的 Keep-Alive ,是由应用层实现的,称为 HTTP 长连接 每次请求都要经历这样的过程:建立 TCP 连接 -> HTTP 请求资源 -> 响应资源 -> 释放连接,这就是HTTP短连接,但是这样每次建立连接都只能请求一次资源,所以 HTTP 的 Keep-Alive 实现了使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,就就是 HTTP 长连接 。通过设置HTTP头 Connection: keep-alive 来实现。 1 TCP 的 Keepalive ,是由 TCP 层(内核态)实现的,称为 TCP 保活机制,是一种用于在 TCP 连接上检测空闲连接状态的机制 当 TCP 连接建立后,如果一段时间内没有任何数据传输, TCP Keepalive 会发送探测包来检查连接是否仍然有效。
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地址与目标服务器建立连接,开始获取网页内容。
CDN是什么 CDN是一种分布式网络服务,通过将内容存储在分布式的服务器上,使用户可以从距离较近的服务器获取所需的内容,从而加速互联网上的内容传输。 ● 就近访问 :CDN 在全球范围内部署了多个服务器节点,用户的请求会被路由到距离最近的 CDN 节点,提供快速的内容访问。 ● 内容缓存 :CDN 节点会缓存静态资源,如图片、样式表、脚本等。当用户请求访问这些资源时,CDN 会首先检查是否已经缓存了该资源。如果有缓存,CDN 节点会直接返回缓存的资源,如果没有缓存所需资源,它会从源服务器(原始服务器)回源获取资源,并将资源缓存到节点中,以便以后的请求。通过缓存内容,减少了对原始服务器的请求,减轻了源站的负载。 ● 可用性:即使某些节点出现问题,用户请求可以被重定向到其他健康的节点。
Cookie和Session是什么?有什么区别? (1) Cookie和Session是什么? Cookie 和 Session 都用于管理用户的状态和身份, Cookie 通过在客户端记录信息确定用户身份, Session 通过在服务器端记录信息确定用户身份。 1 Cookie ● 通常,服务器会将一个或多个 Cookie 发送到用户浏览器,然后浏览器将这些 Cookie 存储在本地。 ● 服务器在接收到来自客户端浏览器的请求之后,就能够通过分析存放于请求头的 Cookie 得到客户端特有的信息,从而动态生成与该客户端相对应的内容。 1 Session 客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是 Session 。Session 主要用于维护用户登录状态、存储用户的临时数据和上下文信息等。服务器为每个用户分配一个唯一的 Session ID ,通常存储在 Cookie 中。 (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切换回用户态。
进程调度算法你了解多少 ● 先来先服务:按照请求的顺序进行调度。 这种调度方式简单,但是能导致较长作业阻塞较短作业。 ● 最短作业优先:非抢占式的调度算法,按估计运行时间最短的顺序进行调度。 但是如果一直有短作业到来,那么长作业永远得不到调度,造成长作业“饥饿”现象。 ● 最短剩余时间优先:基于最短作业优先改进,按剩余运行时间的顺序进行调度。当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。 ● 优先级调度:为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。 ● 时间片轮转:为每个进程分配一个时间片,进程轮流执行,时间片用完后切换到下一个进程。 ● 多级队列:时间片轮转调度算法和优先级调度算法的结合。 将进程分为不同的优先级队列,每个队列有自己的调度算法。
进程间有哪些通信方式 1 管道: 是一种 半双工 的通信方式,数据只能单向流动而且只能在具有父子进程关系的进程间使用。 2 命名管道: 类似管道,也是半双工的通信方式,但是它允许在不相关的进程间通信。 3 消息队列: 允许进程发送和接收消息,而消息队列是消息的链表,可以设置消息优先级。 4 信号 :用于发送通知到进程,告知其发生了某种事件或条件。 5 信号量 :是一个计数器,可以用来控制多个进程对共享资源的访问,常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此主要作为进程间以及同一进程内不同线程之间的同步手段。 6 共享内存 :就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的进程通信方式, 7 Socket 套接字:是支持TCP/IP 的网络通信的基本操作单元,主要用于在客户端和服务器之间通过网络进行通信。 8 互斥锁:一种信号量,用于保护共享数据结构,防止多个进程同时访问。 9 条件变量:与互斥锁配合使用,用于进程间的同步,等待某些条件成立。
解释一下进程同步和互斥,以及如何实现进程同步和互斥 进程同步是指 多个并发执行的进程之间协调和管理它们的执行顺序,以确保它们按照一定的顺序或时间间隔执行。 互斥指的是在某一时刻只允许一个进程访问某个共享资源。当一个进程正在使用共享资源时,其他进程不能同时访问该资源。 解决进程同步和互斥的问题有很多种方法,其中一种常见的方法是使用 信号量和 PV 操作 。信号量是一种特殊的变量,它表示系统中某种资源的数量或者状态。PV 操作是一种对信号量进行增加或者减少的操作,它们可以用来控制进程之间的同步或者互斥。 ● P操作 :相当于“检查”信号量,如果资源可用,就减少计数,然后使用资源。 ● V操作 :相当于“归还”资源,增加信号量的计数,并可能唤醒等待的进程。 除此之外,下面的方法也可以解决进程同步和互斥问题: ● 临界区 :将可能引发互斥问题的代码段称为临界区,里面包含了需要互斥访问的资源。进入这个区域前需要先获取锁,退出临界区后释放该锁。这确保同一时间只有一个进程可以进入临界区。 ● 互斥锁(Mutex) :互斥锁是一种同步机制,用于实现互斥。每个共享资源都关联一个互斥锁,进程在访问该资源前需要先获取互斥锁,使用完后释放锁。只有获得锁的进程才能访问共享资源。 ● 条件变量 :条件变量用于在进程之间传递信息,以便它们在特定条件下等待或唤醒。通常与互斥锁一起使用,以确保等待和唤醒的操作在正确的时机执行。
什么是死锁,如何预防死锁? 死锁是系统中两个或多个进程在执行过程中,因争夺资源而造成的一种僵局。当每个进程都持有一定的资源并等待其他进程释放它们所需的资源时,如果这些资源都被其他进程占有且不释放,就导致了死锁。 死锁只有同时满足以下四个条件才会发生: ● 互斥条件:一个进程占用了某个资源时,其他进程无法同时占用该资源。 ● 请求保持条件:一个线程因为请求资源而阻塞的时候,不会释放自己的资源。 ● 不可剥夺条件:资源不能被强制性地从一个进程中剥夺,只能由持有者自愿释放。 ● 循环等待条件:多个进程之间形成一个循环等待资源的链,每个进程都在等待下一个进程所占有的资源。 避免死锁: 通过破坏死锁的四个必要条件之一来预防死锁。比如破坏循环等待条件,让所有进程按照相同的顺序请求资源。 检测死锁:通过检测系统中的资源分配情况来判断是否存在死锁。例如,可以使用资源分配图或银行家算法进行检测。 解除死锁:一旦检测到死锁存在,可以采取一些措施来解除死锁。例如,可以通过抢占资源、终止某些进程或进行资源回收等方式来解除死锁。
讲一讲你理解的虚拟内存 虚拟内存是指在每一个进程创建加载的过程中,会分配一个连续虚拟地址空间, 它不是真实存在的, 而是 通过映射与实际物理地址空间 对应,这样就可以使每个进程看起来都有自己独立的连续地址空间,并允许程序访问比物理内存 RAM 更大的地址空间, 每个程序都可以认为它拥有足够的内存来运行。 需要虚拟内存的原因: ● 内存扩展: 虚拟内存使得每个程序都可以使用比实际可用内存更多的内存,从而允许运行更大的程序或处理更多的数据。 ● 内存隔离:虚拟内存还提供了进程之间的内存隔离。每个进程都有自己的虚拟地址空间,因此一个进程无法直接访问另一个进程的内存。 ● 物理内存管理:虚拟内存允许操作系统动态地将数据和程序的部分加载到物理内存中,以满足当前正在运行的进程的需求。当物理内存不足时,操作系统可以将不常用的数据或程序暂时移到硬盘上,从而释放内存,以便其他进程使用。 ● 页面交换:当物理内存不足时,操作系统可以将一部分数据从物理内存写入到硬盘的虚拟内存中,这个过程被称为页面交换。当需要时,数据可以再次从虚拟内存中加载到物理内存中。这样可以保证系统可以继续运行,尽管物理内存有限。 ● 内存映射文件:虚拟内存还可以用于将文件映射到内存中,这使得文件的读取和写入可以像访问内存一样高效。
你知道的线程同步的方式有哪些? 线程同步机制是指在多线程编程中,为了保证线程之间的互不干扰,而采用的一种机制。常见的线程同步机制有以下几种: 1 互斥锁:互斥锁是最常见的线程同步机制。它允许只有一个线程同时访问被保护的临界区(共享资源) 2 条件变量:条件变量用于线程间通信,允许一个线程等待某个条件满足,而其他线程可以发出信号通知等待线程。通常与互斥锁一起使用。 3 读写锁: 读写锁允许多个线程同时读取共享资源,但只允许一个线程写入资源。 4 信号量:用于控制多个线程对共享资源进行访问的工具。
介绍一下几种典型的锁 ● 互斥锁 :互斥锁是一种最常见的锁类型,用于实现互斥访问共享资源。在任何时刻,只有一个线程可以持有互斥锁,其他线程必须等待直到锁被释放。这确保了同一时间只有一个线程能够访问被保护的资源。 ● 自旋锁:自旋锁是一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询,直到锁被释放。 其他的锁都是基于这两个锁的 ● 读写锁:允许多个线程同时读共享资源,只允许一个线程进行写操作。分为读(共享)和写(排他)两种状态。 ● 悲观锁:认为多线程同时修改共享资源的概率比较高,所以访问共享资源时候要上锁 ● 乐观锁:先不管,修改了共享资源再说,如果出现同时修改的情况,再放弃本次操作。
有哪些页面置换算法 常见页面置换算法有最佳置换算法(OPT)、先进先出(FIFO)、最近最久未使用算法(LRU)、时钟算法(Clock) 等。 1 最近最久未使用算法 LRU :LRU算法 基于页面的使用历史,通过选择最长时间未被使用的页面进行置换。 2 先进先出 FIFO 算法:也就是 最先进入内存的页面最先被置换出去 。 3 最不经常使用 LFU :淘汰访问次数最少的页面,考虑页面的访问频率。 4 时钟算法 CLOCK :Clock算法的核心思想是通过使用一个指针(称为时钟指针)在环形链表上遍历,检查页面是否被访问过, 当需要进行页面置换时,Clock算法从时钟指针的位置开始遍历环形链表。 如果当前页面的访问位为0,表示该页面最久未被访问,可以选择进行置换。将访问位设置为1,继续遍历下一个页面。 如果当前页面的访问位为1,表示该页面最近被访问过,它仍然处于活跃状态。将访问位设置为0,并继续遍历下一个页面如果遍历过程中找到一个访问位为0的页面,那么选择该页面进行置换。 5 最佳置换算法: 该 算法根据未来的页面访问情况,选择最长时间内不会被访问到的页面进行置换 。那么就有一个问题了,未来要访问什么页面,操作系统怎么知道的呢?操作系统当然不会知道,所以这种算法只是一种理想情况下的置换算法,通常是无法实现的。
select、poll、epoll的区别 I/O多路复用通常通过select、poll、epoll等系统调用来实现。 ● select: select是一个最古老的I/O多路复用机制,它可以监视多个文件描述符的可读、可写和错误状态。然而,但是它的效率可能随着监视的文件描述符数量的增加而降低。 ● poll: poll是select的一种改进,它使用 轮询方式 来检查多个文件描述符的状态,避免了select中文件描述符数量有限的问题。但对于大量的文件描述符,poll的性能也可能变得不足够高效。 ● epoll: epoll是Linux特有的I/O多路复用机制,相较于select和poll,它在处理大量文件描述符时更加高效。epoll使用事件通知的方式,只有在文件描述符就绪时才会通知应用程序,而不需要应用程序轮询。 I/O多路复用允许在一个线程中处理多个I/O操作,避免了创建多个线程或进程的开销,允许在一个线程中处理多个I/O操作,避免了创建多个线程或进程的开销。
数据库
一条SQL查询语句是如何执行的? 1 连接器:连接器负责跟客户端建立连接、获取权限、维持和管理连接。 2 查询缓存: MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。 3 分析器:你输入的是由多个字符串和空格组成的一条 SQL 语句, MySQL 需要识别出里面的字符串分别是什么,代表什么。 4 优化器:优化器是在表里面有多个索引的时候,决定使用哪个索引; 或者在一个语句有多表关联( join )的时候,决定各个表的连接顺序。 5 执行器: MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。
数据库的事务隔离级别有哪些? 1 读未提交(Read Uncommitted): ○ 允许一个事务读取另一个事务尚未提交的数据修改。 ○ 最低的隔离级别,存在脏读、不可重复读和幻读的问题。 2 读已提交(Read Committed): ○ 一个事务只能读取已经提交的数据。其他事务的修改在该事务提交之后才可见。 ○ 解决了脏读问题,但仍可能出现不可重复读和幻读。 3 可重复读(Repeatable Read): ○ 事务执行期间,多次读取同一数据会得到相同的结果,即在事务开始和结束之间,其他事务对数据的修改不可见。 ○ 解决了不可重复读问题,但仍可能出现幻读。 4 序列化(Serializable): ○ 最高的隔离级别,确保事务之间的并发执行效果与串行执行的效果相同,即不会出现脏读、不可重复读和幻读。
事务的四大特性有哪些? 事务的四大特性通常被称为 ACID 特性 1 原子性:确保事务的所有操作要么全部执行成功,要么全部失败回滚,不存在部分成功的情况。 2 一致性:事务在执行前后,数据库从一个一致性状态转变到另一个一致性状态。 3 隔离性:多个事务并发执行时,每个事务都应该被隔离开来,一个事务的执行不应该影响其他事务的执行。 4 持久性:一旦事务被提交,它对数据库的改变就是永久性的,即使在系统故障或崩溃后也能够保持。
MySQL的执行引擎有哪些? MySQL的执行引擎主要负责查询的执行和数据的存储, 其执行引擎主要有 MyISAM 、 InnoDB 、 Memery 等。 ● InnoDB 引擎提供了对事务ACID的支持,还提供了行级锁和外键的约束,是目前MySQL的默认存储引擎,适用于需要事务和高并发的应用。 ● MyISAM 引擎是早期的默认存储引擎,支持全文索引,但是不支持事务,也不支持行级锁和外键约束,适用于快速读取且数据量不大的场景。 ● Memery 就是将数据放在内存中,访问速度快,但数据在数据库服务器重启后会丢失。
MySQL为什么使用B+树来作索引 B+树是一个B树的变种,提供了高效的数据检索、插入、删除和范围查询性能。 ● 单点查询:B 树进行单个索引查询时,最快可以在 O(1) 的时间代价内就查到。从平均时间代价来看,会比 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 表连接中的列类型不匹配: 如果在连接操作中涉及的两个表的列类型不匹配,索引可能会失效。例如,一个表的列是整数,另一个表的列是字符,连接时可能会导致索引失效。
undo log、redo log、binlog 有什么用? ● undo log 是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC 。 ● redo log 是物理日志,记录了某个数据页做了什么修改,每当执行一个事务就会产生一条或者多条物理日志。 ● binlog (归档日志)是 Server 层生成的日志,主要用于数据备份和主从复制。
什么是慢查询?原因是什么?可以怎么优化? 数据库查询的执行时间超过指定的超时时间时,就被称为慢查询。 原因: ● 查询语句比较复杂:查询涉及多个表,包含复杂的连接和子查询,可能导致执行时间较长。 ● 查询数据量大:当查询的数据量庞大时,即使查询本身并不复杂,也可能导致较长的执行时间。 ● 缺少索引:如果查询的表没有合适的索引,需要遍历整张表才能找到结果,查询速度较慢。 ● 数据库设计不合理:数据库表设计庞大,查询时可能需要较多时间。 ● 并发冲突:当多个查询同时访问相同的资源时,可能发生并发冲突,导致查询变慢。 ● 硬件资源不足:如果MySQL服务器上同时运行了太多的查询,会导致服务器负载过高,从而导致查询变慢 优化: 1 运行语句,找到慢查询的sql 2 查询区分度最高的字段 3 explain:显示mysql如何使用索引来处理select语句以及连接表,可以帮助选择更好的索引、写出更优化的查询语句 4 order by limit 形式的sql语句,让排序的表优先查 5 考虑建立索引原则
Redis有什么优缺点?为什么用Redis查询会比较快 (1) Redis有什么优缺点? Redis 是一个基于内存的数据库, 读写速度非常快,通常被用作缓存 、消息队列、分布式锁和键值存储数据库。它支持多种数据结构,如字符串、哈希表、列表、集合、有序集合等, Redis 还提供了 分布式 特性,可以将数据分布在多个节点上,以提高可扩展性和可用性。但是 Redis 受限于物理内存的大小,不适合存储超大量数据,并且需要大量内存,相比磁盘存储成本更高。 (2) 为什么Redis查询快 ● 基于内存操作 : 传统的磁盘文件操作相比减少了IO,提高了操作的速度。 ● 高效的数据结构:Redis专门设计了STRING、LIST、HASH等高效的数据结构,依赖各种数据结构提升了读写的效率。 ● 单线程:单线程操作省去了上下文切换带来的开销和CPU的消耗,同时不存在资源竞争,避免了死锁现象的发生。 ● I/O多路复用:采用I/O多路复用机制同时监听多个Socket,根据Socket上的事件来选择对应的事件处理器进行处理。
Redis的数据类型有那些? Redis 常见的五种数据类型: 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 的优点;
缓存雪崩、击穿、穿透和解决办法 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操作非常快,带来的问题是,数据不是强一致性的,而且可能会丢。 ● 第二步失效问题 :这种可能性极小,缓存删除只是标记一下无效的软删除,可以看作不耗时间。如果会出问题,一般程序在写数据库那里就没有完成:故意在写完数据库后,休眠很长时间再来删除缓存。
Java
String、StringBuffer、StringBuilder的区别 String , StringBuilder , 和 StringBuffer 都是 Java 中用于处理字符串的类,但它们之间有一些重要的区别: 1 不可变性 ● String 内部的字符数组使用 final 修饰,为不可变的字符串类,每当我们对 String 对象进行改变时,实际上都会创建一个新的 String 对象,旧的 String 对象会被 JVM 回收, 容易触发 **gc** ,引起系统内存抖动。 ● StringBuilder 和 StringBuffer 是可变的。即它们都允许修改字符串,而不会创建新的对象 1 线程安全 ● String :由于 String 是不可变的,所以是线程安全的。 ● StringBuffer 中的方法均使用 synchronized 关键字修饰, 线程 安全。 ● 而 StringBuilder 线程不安全。 1 性能 对于复杂的字符串操作(例如多次的拼接,插入,删除), StringBuilder 和 StringBuffer 效率高于 String ,因为它们是可变的,不需要创建新的对象。 1 使用场景 ● String : 字符串不经常变化的场景中可以使用 String 类,例如常量的声明、少量的变量运算。 ● StringBuilder : 在频繁进行字符串运算(如拼接、替换、和删除等),并且运行在单线程的环境中,则可以考虑使用,如SQL语句的拼装、JSON封装等。 ● StringBuffer : 在频繁进行字符串运算(如拼接、替换、删除等),并且运行在多线程环境中,则可以考虑使用 StringBuffer ,例如 XML 解析、 HTTP 参数解析和封装。
接口和抽象类的区别 1 从定义上来说 ● 接口是一种抽象类型,它定义了一组方法(方法签名, 包括方法名、参数列表和返回类型)但没有实现任何方法的具体代码。接口中的方法默认是抽象的,且接口中只能包含常量( static final 变量)和抽象方法,不能包含成员变量。 ● 抽象类是一个类,可以包含抽象方法和具体方法,也可以包含成员变量和常量。抽象类中的抽象方法是没有实现的方法,而具体方法则包含实现代码。抽象类不能直接实例化,通常需要子类继承并实现其中的抽象方法。 1 继承 ● 接口支持多继承,一个类可以实现多个接口。 ● Java 中不支持多继承,一个类只能继承一个抽象类。如果一个类已经继承了一个抽象类,就不能再继承其他类。 1 构造器 ● 接口不能包含构造器,因为接口不能被实例化。类实现接口时,必须实现接口中定义的所有方法。 ● 抽象类可以包含构造器,用于初始化抽象类的实例。当子类实例化时,会调用父类的构造器。 1 访问修饰符 ● 接口中的方法默认是 public abstract 的。接口中的变量默认是 public static final 的。 ● 抽象类中的抽象方法默认是 protected 的,具体方法的访问修饰符可以是 public 、 protected 或 private 。实现限制 ● 类可以同时实现多个接口,接口中的方法默认为抽象方法,不包含方法体。实现接口时必须要实现这些方法。 ● 一个类只能继承一个抽象类,继承抽象类的子类必须提供抽象类中定义的所有抽象方法的实现。 1 设计目的 ○ 接口用于定义规范,强调“行为”或“能力”。 ● 抽象类用于代码复用,提供通用的实现或基础功能,并且可以包含方法的具体实现。
Java常见的异常类有哪些 ● Java 的异常都是派生于 Throwable 类的一个实例,所有的异常都是由 Throwable 继承而来的。 ● 异常又分为 RuntimeException 和其他异常: ○ 由程序错误导致的异常属于 RuntimeException ○ 而程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于其他异常。 ● 运行时异常 RuntimeException : 顾名思义,运行时才可能抛出的异常,编译器不会处理此类异常。比如数组索引越界 ArrayIndexOutOfBoundsException 、使用的对象为空 NullPointException 、强制类型转换错误 NullPointException 、除 0 等等。出现了运行时异常,一般是程序的逻辑有问题,是程序自身的问题而非外部因素。 ● 其他异常: Exception 中除了运行时异常之外的,都属于其他异常。也可以称之为 编译时异常 ,这部分异常编译器要求必须处置。这部分异常常常是因为外部运行环境导致,因为程序可能运行在各种环境中,如打开一个不存在的文件,此时抛出 FileNotFoundException 。编译器要求Java程序必须捕获或声明所有的编译时异常,强制要求程序为可能出现的异常做准备工作。
说一说Java面向对象三大特性 Java面向对象编程的三大特性是封装、继承和多态: 1 封装 :封装是将对象的数据(属性)和行为(方法)结合在一起,并隐藏内部的实现细节,只暴露出一个可以被外界访问的接口。通常使用关键字 private 、 protected 、 public 等来定义访问权限,以实现封装。 2 继承 :允许一个类(子类)继承另一个类(父类)的属性和方法的机制。子类可以重用父类的代码,并且可以通过添加新的方法或修改(重写)已有的方法来扩展或改进功能,提高了代码的可重用性和可扩展性。 Java 支持单继承,一个类只能直接继承一个父类。 3 多态 :多态是指允许不同类的对象对同一消息做出响应,但具体的行为会根据对象的实际类型而有所不同。这通常通过方法重载和重写实现。
说一说你对Java多态的理解 1 当把一个子类对象直接赋给父类引用变量,而运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。 2 多态有两种形式:编译时多态(静态多态)和运行时多态(动态多态)。 ● 编译时多态:指在编译阶段,编译器就能够确定调用哪个方法,这是通过方法的重载来实现的。编译器在编译时根据方法的参数数量、类型或顺序来选择调用合适的方法。 ● 运行时多态:在程序运行时,根据实际对象的类型来确定调用的方法,这是通过方法的重写来实现的。运行时多态主要依赖于对象的实际类型,而不是引用类型。
重写和重载的区别 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 方法,以保持 equals 和 hashCode 的一致性。
Java的集合类有哪些,那些是线程安全的,那些是线程不安全的 1 Java 中的集合类主要由 Collection 和 Map 这两个接口派生而出,其中 Collection 接口又派生出三个子接口,分别是 Set 、 List 、 Queue 。所有的 Java 集合类,都是 Set 、 List 、 Queue 、 Map 这四个接口的实现类 ○ List接口 : 有序集合,允许重复元素。常见的实现类有 ArrayList 、 LinkedList 等。 ○ Set接口 :不允许重复元素的集合。常见的实现类有 HashSet 、 LinkedHashSet 、 TreeSet 等。 ○ Queue接口 : 用于表示队列的数据结构。 常见的实现类有LinkedList、PriorityQueue等。 ○ Map接口 : 表示键值对的集合。常见的实现类有HashMap、LinkedHashMap、TreeMap 等。 2 线程不安全的集合类 ● ArrayList 、 LinkedList 、 HashSet 、 HashMap :这些集合类是非线程安全的。在多线程环境中,如果没有适当的同步措施,对这些集合的并发操作可能导致不确定的结果。 ● TreeMap 、 TreeSet : 虽然 TreeMap 和、 TreeSet 是有序的集合,但它们也是非线程安全的。 1 线程安全的集合类 ● Vector :类似于 ArrayList , 它的方法都是同步的,因此是线程安全的。然而,它相对较重,不够灵活,现在通常建议使用 ArrayList 。 ● HashTable :类似于 HashMap ,但它是线程安全的,通过同步整个对象实现。但它的使用已经不太推荐,通常建议使用 HashMap 。 ● ConcurrentHashMap :提供更好的并发性能,通过锁分离技术实现线程安全。 ● Collections.synchronizedList 、 Collections.synchronizedSet 、 Collections.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。这是为了节省内存,因为并不是所有的ArrayList都需要大量的空间。 3 当ArrayList中的元素数量达到当前容量时,ArrayList会自动增加其容量。 ArrayList 扩容的计算是在一个 grow() 方法里面, grow 方法先尝试将数组扩大为原数组的1.5倍。(默新容量=旧容量右移一位(相当于除于2)在加上旧容量) 4 若新的容量满足需求,会调用一个 Arrays.copyof 方法, 将所有的元素从旧数组复制到新数组中,这个方法是真正实现扩容的步骤。如果扩容后的新容量还是不满足需求,那新容量大小为当前所需的容量加 1。
Java中的HashMap了解吗?HashMap 的底层实现是什么? 1 底层实现 在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(n); ○ 最坏情况下:发生哈希冲突,链表为O(n), 红黑树为O(log n)。 ● 写: O(n),但是如果所有元素都在一个桶内,则每次插入需要O(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 的操作都是同步的。 ● 使用 ConcurrentHashMap : ConcurrentHashMap 是专门设计用于多线程环境的哈希表实现。它使用分段锁机制,允许多个线程同时进行读操作,提高并发性能。 ● 使用锁机制:可以在自定义的 HashMap 操作中使用显式的锁(例如 ReentrantLock )来保证线程安全。
concurrentHashMap 如何保证线程安全 1 ConcurrentHashMap 在JDK 1.7中使用的数组 加 链表的结构,其中数组分为两类,大树组 Segment 和 小数组 HashEntry , ConcurrentHashMap 的线程安全是建立在 Segment 加 ReentrantLock 重入锁来保证 2 ConcurrentHashMap 在JDK1.8中使用的是数组 加 链表 加 红黑树的方式实现,它是通过 CAS 或者 synchronized 来保证线程安全的,并且缩小了锁的粒度,查询性能也更高。
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 继承关系 Hashtable 是 Dictionary 类的子类,而 HashMap 是 AbstractMap 类的子类,实现了 Map 接口。 1 迭代器 ● Hashtable 的迭代器是通过 Enumerator 实现的。 ● HashMap 的迭代器是通过 Iterator 实现的。 1 初始容量和加载因子 ● Hashtable 的初始容量和加载因子是固定的。 ● HashMap 允许通过构造方法设置初始容量和加载因子,以便更好地调整性能。
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 可能是更简单的选择。
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 乐观锁/悲观锁 :乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加 锁会带来大量的性能提升。 悲观锁在Java中的使用,就是利用各种锁。 乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋 实现原子操作的更新。 6 分段锁:分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就 是通过分段锁的形式来实现高效的并发操作。 7 偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对 Synchronized 。在Java 5通过引入锁升级的机制来实现高效 Synchronized 。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 8 自选锁:在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好 处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
说说你对 synchronized的理解 synchronized 是Java中的一个关键字,用于实现同步和线程安全。 ● 当一个方法或代码块被 synchronized 修饰时,它将成为一个临界区,同一时刻只能由一个线程访问。其他线程必须等待当前线程退出临界区才能进入。确保多个线程在访问共享资源时不会产生冲突 ● synchronized 可以应用于方法或代码块。当它应用于方法时,整个方法被锁定;当它应用于代码块时,只有该代码块被锁定。这样做的好处是,可以选择性地锁定对象的一部分,而不是整个方法。 ● synchronized 实现的机理依赖于软件层面上的 JVM ,因此其性能会随着Java版本的不断升级而提高。 到了 Java1.6 , synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。 需要说明的是,当线程通过 synchronized 等待锁时是不能被 Thread.interrupt() 中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁的尴尬境地。 ● 最后,尽管 Java 实现的锁机制有很多种,并且有些锁机制性能也比 synchronized 高,但还是强烈推荐在 多线程应用程序中使用该关键字,因为实现方便,后续工作由 JVM 来完成,可靠性高。只有在确定锁机 制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如 ReentrantLock 等。
synchronized和Lock的区别是什么 1 synchronized 和 Lock 都是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 synchronized 和 ReentrantLock 都是Java中用于实现线程同步的手段, synchronized 是Java的关键字,基于JVM的内置锁实现,可以用于修饰方法或代码块,使用起来比较简单直接。而 ReentrantLock 是 java.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 提供了更丰富的控制能力和灵活性,适用于需要复杂同步控制的场景。
volatile 关键字的作用有那些? 1 volatile 通常被比喻成"轻量级的 synchronized ",它不需要获取和释放锁,是Java并发编程中比较重要的一个关键字。 和 synchronized 不同, volatile 是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。 2 volatile 关键字在Java中主要用于保证变量的内存可见性和禁止指令重排。 ● 保证可见性 : 确保当一个线程修改了一个 volatile 变量时,其他线程能够立即看到这个改变。 ○ 当对非 volatile 变量进行读写的时候,每个线程先从主内存拷贝变量到 CPU 缓存中,如果计算机有多个 CPU ,每个线程可能在不同的 CPU 上 被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。 ○ volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过 CPU cache 这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。 ● 禁止指令重排 : volatile 变量的写操作在JVM执行时不会发生指令重排,确保写入操作在读取操作之前完成。 ○ 指令重排序是 JVM 为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度, 包括编译器重排序和运行时重排序; ○ volatile 变量禁止指令重排序。针对 volatile 修饰的变量,在读写操作指令前后会插入内存屏障,指令重排序时不能把后面的指令重排序到内存屏障. 1 虽然 volatile 可以确保可见性,但它不保证复合操作的原子性。
volatile 与synchronized 的对比 volatile 和 synchronized 都是Java中用于多线程同步的工具,在用途、原子性、互斥性、性能和使用场景上有一定的区别。 1 机制和用途 ● synchronized :用于提供线程间的同步机制。当一个线程进入一个由 synchronized 修饰的代码块或方法时,它会获取一个监视器锁,这保证了同一时间只有一个线程可以执行这段代码。其主要用途是确保数据的一致性和线程安全性。 ● volatile :用于修饰变量。 volatile 的主要作用是确保变量的可见性,即当一个线程修改了一个 volatile 变量的值,其他线程能够立即看到这个修改。此外,它还可以防止指令重排序。但是, volatile 并不能保证复合操作的原子性。 ● 总结: volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。 1 原子性 ● synchronized :可以保证被其修饰的代码块的原子性,即这段代码在执行过程中不会被其他线程打断。 ● volatile :只能保证单个读写操作的原子性,对于复合操作(如自增、自减等)则无法保证原子性。 1 性能 volatile 通常比 synchronized 更轻量级,所以 volatile 性能肯定比 synchronized 关键字要好; 因为它不涉及锁的获取和释放。但是,这也意味着它提供的同步级别较低。 1 互斥性: ● synchronized :提供了互斥性,即同一时间只有一个线程可以执行被其修饰的代码块或方法。 ● volatile :不提供互斥性,只是确保变量的可见性。 1 使用场景 ● volatile 适用于简单的内存可见性要求 ● 而 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的内存区域主要分为以下几个部分: 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 技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的 公共行为封装到一个可重用模块,并将其命名为切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的 耦合度,并有利于未来的可操作性和可维护性。 Spring 中 AOP 代理由 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实现了 BeanNameAware 或 BeanFactoryAware 接口,容器会调用相应的 setBeanName 或 setBeanFactory 方法。 4 BeanPostProcessor:在Bean的属性设置之后,Spring会调用所有注册的 BeanPostProcessor 的 postProcessBeforeInitialization 方法。 5 初始化 Bean : 如果Bean实现了 InitializingBean 接口,容器会调用其 afterPropertiesSet 方法。同时,如果Bean定义了 init-method ,容器也会调用这个方法。 6 BeanPostProcessor的第二次调用**:容器会再次调用所有注册的 BeanPostProcessor 的 postProcessAfterInitialization 方法,这次是在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 使用工厂模式通过 BeanFactory 、 ApplicationContext 创建 bean 对象。 ● 代理设计模式 : Spring AOP 功能的实现。 ● 单例设计模式 : Spring 中的 Bean 默认都是单例的。 ● 模板方法模式 : Spring 中 jdbcTemplate 、 hibernateTemplate 等以 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 HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet 。 8 DispatcherServlet 将 ModelAndView 传给 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 MVC 的 Controller 。 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 名称。 1 @ConfigurationProperties : 用于将配置文件中的属性映射到 Java Bean 。 2 @Profile : 用于定义不同环境下的配置,可以标识在类或方法上。 3 @Async : 用于将方法标记为异步执行。
JDK8新特性 1 Lambda 表达式:允许以更简洁的语法编写匿名函数。 2 Stream API : Stream API 提供了一种声明式的方式来对集合进行操作。它支持各种操作,如过滤、映射、排序、归约等。 3 函数式接口: Java 8 提供了一系列函数式接口,如 Consumer 、 Predicate 、 Function 等。 4 新的日期和时间 API: Java 8 引入了 java.time 包,提供了全新的日期和时间 API 。它解决了旧的 java.util.Date 和 java.util.Calendar 的诸多问题,并提供了更加清晰和易用的日期和时间处理方式。 5 方法引用:方法引用允许通过方法的名称来引用一个方法,而不是执行它。它们提供了一种更简洁的方式来传递方法作为参数,如 System.out::println 。
6/35696字