Java八股-计网篇

基础篇

OSI七层模型和TCP/IP网络模型

三分恶面渣逆袭:三种网络体系结构

每一层对应协议

各层网络对应的网络协议

输入网址到网页,发生了什么

  1. 浏览器对输入的URL进行解析

  2. 浏览器根据输入域名进行域名解析,获取到对应的IP地址

    浏览器首先查询自身缓存中是否有这个域名对应的IP地址,如果有直接返回;没有则查询本地缓存中是否有该域名记录,如果有则直接返回;没有则查询hosts文件,有则返回;没有则查询本地DNS服务器(由网络接入服务器商提供,比如中国移动),有则返回;没有则本地DNS服务器向根域名服务器请求,根域名服务器可以直出接下来要向谁查询并告诉本地DNS,最终本地DNS查询到域名对应的IP地址,然后返回给浏览器

  3. 浏览器获取到请求的域名对应的IP后首先进行TCP连接,通过三次握手建立连接,之后发送HTTP请求,HTTP请求有请求行、请求头、空行、请求数据组成;如果使用的HTTP协议是HTTP1.1及以后将开启HTTP长连接,不需要每次发送HTTP请求都要从新建立连接;如果使用了HTTPS协议,则需要进行非对称加密来传递服务器公钥并根据一定算法生成此次连接要使用的密钥,随后使用生成的回话密钥进行对称加密。

  4. 服务器接收到客户端发送的请求后对其做出相应,将对应的请求资源发送给浏览器

  5. 浏览器根据接收到的响应报文渲染页面

  6. 连接结束:浏览器进行四次挥手释放TCP连接

HTTP

HTTP状态码

6-五大类HTTP状态码

301表示永久移动,请求的资源已被永久移动到新位置。服务器返回此响应时,会返回新的资源地址

302表示临时性移动,服务器从另外的地址响应资源,但是客户端还应该使用这个地址

304表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,也就是告诉客户端可以继续使用缓存资源,用于缓存控制

HTTP常见字段

  • Host

    客户端发送请求时,用来指定服务器的域名

  • Content-Length

    服务器返回数据时,会有该字段,表示本次响应数据的长度。由于是基于TCP进行通信,所以存在粘包问题,HTTP协议通过设置回车符、换行符作为HTTP Header的边界,通过Content-Length字段作为Http Body的边界

  • Connection

    客户端要求服务器使用HTTP 长连接机制

    Connection: Keep-Alive
  • Content-Type

    服务器响应时返回给客户端,表示本次数据是什么格式

    Content-Type: text/html; Charset=utf-8
  • Accept

    客户端告诉服务器自己能接受哪些数据格式

    Accept: */*
  • Content-Encoding

    说明数据的压缩方式,表示服务器返回的数据使用了什么压缩格式

    Content-Encoding: gzip
  • Accept-Encoding

    客户端告诉服务器自己可以接受哪写压缩方式

    Accept-Encoding: gzip, deflate

GET和POST

  • GET方法将请求信息放在URL中,而POST将请求信息放在请求体中;由于浏览器对URL长度有限制,所以使用GTE的请求的URL长度有限制;GET将请求信息直接暴露在URL中不安全
  • 由于GET从定义上是从服务器上获取资源,所以每次请求都不会影响到服务器而且每次的结果都是相同的即是幂等的。而POST定义是新增或提交数据,会修改服务器上的资源,是不安全的,同时每次提交数据都会创建多个资源所以不是幂等的。
  • GET请求能够被缓存

HTTP缓存技术

对于一些具有重复性的HTTP请求,比如每次请求得到的数据都是一样的,就可以将数据缓存在本地。HTTP缓存有两种实现方式:强制缓存协商缓存

强制缓存

强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在浏览器。强缓存利用以下两个HTTP响应头字段实现,表示资源在客户端缓存的有效期:

  • Cache-Control:相对时间
  • Expires:绝对时间

如果同时包含,Cache-Control优先级高于Expires。

  1. 当浏览器第一次请求服务器资源时,服务器在返回该资源时会在响应报文头部加上Cache-Control,Cache-Control中设置了过期时间
  2. 浏览器再次请求访问服务器中的该资源时,会通过请求资源的时间和过期时间判断资源是否过期,如果没有则使用该缓存,否则重新请求
  3. 服务器再次收到请求后,更新响应头中的Cache-Control

协商缓存

当一些请求的响应码为304时,表示服务器告诉浏览器可以使用本地缓存资源,通常这种通过服务端告知客户端是否可以使用缓存的方式被称为协商缓存。

使用ETag字段实现协商缓存:

  1. 当浏览器第一次请求访问服务器资源时,服务器在返回这个资源的同时在响应头中加上ETag唯一标识(由当前请求的资源生成)
  2. 当浏览器再次请求该资源时,首先检查强制缓存是否过期,没有过期则直接使用本地缓存,否则在请求头上加If-None-Match字段,值为ETag唯一标识
  3. 服务器再次接收到请求后,会根据请求中的If-None-Match值与当前请求资源生成的唯一标识进行比较,相等则返回304,不相等则返回资源并在响应头上加上新的ETag
  4. 如果收到304请求响应状态码,直接本地加载资源,否则更新资源
http缓存

HTTP报文结构

请求报文

  • 请求行

    请求方法、请求URL和HTTP协议版本

    GET /index.html HTTP/1.1
  • 请求头

    包含请求的附加信息,如客户端想要接收的内容类型、浏览器类型等

  • 空行

    请求头部和消息正文之间有一个空行,表示请求头部结束

  • 请求体

    请求的具体内容,如 POST 请求中的表单数据;GET 请求中没有消息正文

响应报文

  • 状态行

    HTTP协议版本、状态码、状态消息

    HTTP/1.0 200 OK
  • 响应头

    包含响应的附加信息,如服务器类型、内容类型、内容长度等

  • 空行

    表示响应头部结束

  • 响应体

    响应的具体内容,如 HTML 页面。不是所有的响应都有消息正文,如 204 No Content 状态码的响应

HTTP和HTTPS

  • HTTP是超文本超文本传输协议,信息是明文传输,存在安全风险问题。HTTPS解决HTTP不安全的缺陷,在TCP和HTTP网络层之间加入了SSL/TLS安全协议,使得报文能够加密传输
  • HTTP连接建立简单,在TCP三次握手之后便可以进行HTTP报文传输;而HTTPS在TCP三次握手后还需要进行SSL/TLS的握手过程才可以进行加密报文传输
  • HTTPS协议需要向CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的

HTTP三大风险

  • 窃听风险

    混合加密实现信息的机密性,解决窃听风险

  • 篡改风险

    摘要算法实现完整性,根据数据生成唯一的指纹,该指纹用于校验数据的完整性,解决了篡改风险

  • 冒充风险

    将服务器公钥放在数字证书中,解决了冒充的风险

混合加密

使用非对称加密和对称加密实现混合加密。

  1. 在通信建立前采用非对称加密方式交换会话密钥,后续使用对称加密
  2. 在通信过程中全部使用非对称加密得到的会话密钥进行对称加密,对明文数据进行加密
摘要算法+数字签名

为了保证传输的内容不被篡改,针对传输的内容计算出一个指纹,然后一起传输给对方。对方收到报文后首先对内容也计算出一个指纹,然后将其与发送方的指纹进行对比,如果相同则说明没有被篡改,不同则被篡改了。在计算机里会用摘要算法(哈希函数)来计算出内容的哈希值,也就是内容的「指纹」,这个哈希值是唯一的,且无法通过哈希值推导出内容

但是这样并不能保证内容和哈希值不会都被替换,所以使用非对称加密对内容的哈希值进行加密形成数字签名,将内容和数字签名一同发送给服务器,服务器对内容计算哈希值后将其与公钥解密的数字签名比较

数字证书

虽然上述可以实现对内容的可靠加密,但是不能保证服务器公钥的合法性(使用伪造的公钥和私钥)。

于是存在一个CA机构,将服务器的个人信息+公钥等用自己的私钥打包成一个数字证书,服务器不仅会使用私钥对内容进行签名还会将数字证书发送给接收方。客户端接收到数字证书后首先使用CA的公钥对数字证书进行解密验证合法性,判断合法后客户端就获得了服务器的公钥,之后就可以进行非对称加密来传输生成会话密钥的准备数据。

HTTPS 建立连接

  • 客户端向服务器索要并验证服务器公钥
  • 双方协商生成会话密钥
  • 双方采用会话密钥进行加密通信
  1. 客户端向服务器发起加密通信请求(客户端支持TLS协议版本、客户端生成的随机数A、客户端支持的密码套件)

  2. 服务器收到客户端请求后对客户端发起响应(确认TLS协议版本、服务器生成的随机数B、确认密码套件、服务器的数字证书)

    数字证书为由CA用他自己的私钥对服务器公钥进行非对称加密得到的

  3. 客户端收到服务器回应后,使用浏览器或者系统中的CA公钥对服务器发送过来的数字证书进行解密确认证书的合法性和真实性。如果证书没有问题,此时客户端就获取到了服务器的公钥,随后使用它加密报文,并且向服务器发送消息(随机数C、加密通信算法改变通知,表示随后信息都将会用会话密钥加密通信、客户端握手结束通知,同时将之前内容做摘要供服务器校验)

    服务器和客户端都有了三个随机数,接着使用协商好的加密算法各自生成本次通信的会话密钥

  4. 服务器计算出会话密钥并向客户端发送最后信息(加密算法改变通知、服务器握手结束通知,同时将之前内容做摘要供客户端校验)

中间人伪造数字证书并作为中间人转发消息
https中间人.drawio

伪造的证书会被浏览器认定是非法的,此时用户不接受该证书就不会发生安全问题,接受了则存在安全风险。

HTTP1.0,1.1,2.0,3.0

HTTP1.0

HTTP1.0是无状态的,每个请求之间相互独立,服务器不保存任何请求的状态信息;默认状态下每个HTTP请求/响应之后连接都会被关闭,属于短连接,但是可以设置Connection: keep-alive强制开启长连接

HTTP1.1

HTTP1.1默认开启长连接,可以在一个TCP连接上发送多个HTTP请求和响应;同时支持客户端在前一个请求的相应到达前发送下一个请求,提高传输效率

HTTP2.0

  • 二进制协议:使用二进制而不是文本格式来传输数据,解析更加高效

  • 头部压缩:如果同时发送多个请求,他们的请求头是一样的或者相似的,协议会减少冗余字段。

    HPACK 算法:在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号

  • 并发传输(多路复用):HTTP1.x存在队头阻塞问题,提出Stream概念,多个Stream复用在一条TCP连接上,每个Stream由一个唯一的ID,不同Stream之间的数据可以乱序发送,因此可以并发的发送不同的Stream,实现并行交错发送请求和响应

  • 服务器推送:服务器可以主动向客户端推送资源,而不需要客户端明确请求

HTTP3.0

将HTTP下层的TCP协议改成了UDP,设计QUIC协议实现类似TCP的可靠性传输

QUIC特点:

  • 无队头阻塞:类似HTTP2.0的Stream与多路复用概念,但是不同流之间相互独立,当某个流发生丢包只会阻塞这个流,其他流不会受影响,因此不存在队头阻塞问题
  • 更快的连接建立: // TODO
  • 连接迁移:根据连接ID可以快速建立连接 // TODO

Cookie和Session

​ Cookie 是保存在客户端的一小块文本串的数据。客户端向服务器发起请求时,服务端会向客户端发送一个 Cookie,客户端就把 Cookie 保存起来。在客户端下次向同一服务器再发起请求时,Cookie 被携带发送到服务器。服务端可以根据这个 Cookie 判断用户的身份和状态

​ Session 指的就是服务器和客户端一次会话的过程。它是另一种记录客户状态的机制。不同的是 cookie 保存在客户端浏览器中,而 session 保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是 session。客户端浏览器再次访问时只需要从该 session 中查找用户的状态

  • Cookie在客户端,Session在服务器端
  • Cookie大小不超过4k,Session存储数据高于Cookie
  • Session安全性高于Cookie
  • Cookie存活时间高于Session

TCP

三次握手

  • 第一次握手:客户端发送TCP报文段到服务器,该TCP报文中SYN被设置为1,表示这是一个连接请求,同时客户端会随机生成一个序列号x发送给服务器,客户端进入SYN_SENT状态

  • 第二次握手:服务器收到客户端的连接请求后如果同意建立连接则会发送应答TCP报文给客户端,该报文中SYNACK都被设置为1,同时服务器也会生成一个随机的序列号y,并且将客户端的序列号加一(x+1)作为确认号发送给客户端,此时服务器进入SYN_RCVD状态

  • 第三次握手:客户端接收到服务器的应答后需要给服务器发送一个确认报文,该报文ACK被设置为1,同时确认号设置为服务器序列号加一(y+1),这条消息的序列号设置为x+1,此时客户端进入ESTABLISHED状态,服务器收到该报文后也将进入ESTABLISHED状态

    注:第三次握手是可以携带数据的,前两次握手不行

为什么是三次,两次行不行

为什么是三次:

  • 避免历史连接

    客户端首先发送了一个序列号为x的连接请求(第一次握手),结果发生了网络阻塞并且客户端宕机了,重启后再次发送连接请求,序列号为y。结果旧的连接请求x在网络恢复正常后先到达服务器,此时分别考虑三次握手和两次握手:

    • 三次握手

      服务器收到历史连接请求x后就会发送确认报文给客户端,并设置设置确认号为x+1,此时客户端对比自己期望的确认号y+1和回传的确认号x+1,发现不是,则直接发送RST报文终止连接,此时服务器直接从SYN_RCVD状态进入CLOSE状态,等待真正的请求报文y到达后再次建立连接。该过程中即使历史连接先一步到达,也不会占用服务器资源导致资源浪费。

    • 两次握手

      服务器收到历史连接请求x后就会发送确认报文给客户端,并设置设置确认号为x+1,由于是两次握手,服务器发送发确认报文直接进入ESTABLISHED状态并向客户端发送数据,客户端此时通过与期望确认号对比发现不是想要的便发送RST报文终止连接,服务器收到后断开连接。但是服务器已经建立了一次连接而且发送了数据,白白浪费了资源

  • 同步双方序列号

    序列号可以过滤掉重复消息并且保证消息的有序性,同时确保消息有没有被接收到,方便消息重发等。如果只有两次握手只能保证客户端的初始序列号被服务器接收到并不能保证服务器的初始序列号被成功接收。

  • 避免资源浪费

    如果只有两次握手,服务器无法知道自己ACK报文是否被成功接收,所以每接收到一个SYN报文就建立连接,导致建立过多的冗余连接造成了不必要的资源浪费。

为什么每次建立TCP连接时,初始序列号都要求不一样

  • 防止历史报文被下一个相同四元组(源ip,源端口,目的ip,目的端口)的连接接收

    假设每次建立连接初始序列号都是从0开始的,由一个客户端发送的数据报因为网络阻塞迟迟没有到达服务器,然而此时连接异常中断,再次建立连接后,该历史数据包刚好到达服务器并且序列号刚好在服务器接收窗口范围内,此时就导致了历史数据报被新的连接接收。

  • 防止黑客伪造的相同序列号的 TCP 报文被对方接收

握手报文丢失

第一次握手丢失

第一次握手丢失服务器将接收不到SYN报文,此时客户端等待一段时间后没有收到预期的服务器恢复的SYN+ACK报文,就会重新发送SYN报文至服务器端,如果仍然没有回应则重复发送,直到发送次数达到最大重传次数,然后返回连接建立失败。

第二次握手丢失

第二次握手丢失即客户端无法接收到服务器端回复的SYN+ACK报文,此时客户端会继续发送SYN报文直到接收到第二次握手报文或者超过最大重发次数,而此时服务器端会一直阻塞在**accet()**处,等待客户端发送ACK报文,同时服务器端也会重发SYN+ACK报文,直至达到最大重发次数

第三次握手丢失

第三次握手丢失即服务器端无法接收到客户端发送的ACK报文,服务器端同样会采用超时重传机制,如果重发次数超过限制则**accept()**调用返回-1,服务器端建立连接失败;而此时客户端认为已经建立成功,开始向服务器端发送数据,但是服务器端的accept()系统调用已返回-1,不在监听状态,因此服务器接收到客户端发送的数据会发送RST报文给客户端,断开客户端单方面建立的连接状态。

四次挥手

  • 第一次挥手:客户端准备关闭连接,发送FIN报文给服务器端,此时客户端进入FIN_WAIT_1状态
  • 第二次挥手:服务器接收到FIN报文后向客户端发送ACK报文并进入CLOSE_WAIT状态;客户端接收到ACK报文后进入FIN_WAIT_2状态
  • 第三次挥手:等待服务器处理完数据后会给客户端发送FIN报文,之后服务器进入LAST_ACK状态
  • 第四次挥手:客户端接收到服务器发送的ACK报文后回复一个ACK报文,然后进入TIME_WAIT状态;服务器端收到ACK报文后进入CLOSE状态,客户端结果2MSL后自动进入CLOSE状态

主动关闭连接的才有TIME_WAIT状态

为什么挥手需要四次

  • 关闭连接时,客户端向服务器发送FIN时只代表客户端不在发送数据了,但是仍然能接收数据
  • 服务器收到FIN报文后,先回复一个ACK报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接

挥手报文丢失

第一次挥手报文丢失

客户端发送FIN报文后如果没有在一定时间内接收到ACK报文,则会触发重传机制,在超过最大重传次数后客户端直接进入CLOSE状态

第二次挥手报文丢失

客户端发送FIN报文后服务器会回复ACK报文,此时服务器进入CLOSE_WAIT状态。由于ACK报文是不会重传的,所以当第二次挥手发送的ACK报文丢失,客户端会触发重传机制重发FIN报文,直到收到第二次挥手,或者达到最大重传次数后直接进入CLOSE状态

第三次挥手报文丢失

当第三次挥手FIN报文丢失后,服务器端因为长时间没有接收到第四次挥手ACK报文,就会触发超时重传机制直至到达最大重传次数后服务器直接断开连接。客户端由于是通过close函数关闭连接的,所以FIN_WAIT_2状态有时间限制,如果长时间未接收到服务器打第三次挥手报文则会直接断开连接。

第四次挥手报文丢失

当客户端就收到服务器发送的第三次挥手报文FIN后会发送ACK报文即第四次挥手,此时客户端进入TIME_WAIT状态,2MSL后进入关闭状态。服务器端如果没有接收到ACK报文则会一直处于LAST_ACK状态,同时会重发FIN报文,当超过最大重传次数后还没有接收到客户端的ACK报文则会直接断开连接;客户端在收到第三次挥手后就会进入TIME_WAIT状态,如果在2MSL内再次接收到FIN报文就会重置定时器,当等待2MSL后直接断开连接。

为什么TIME_WAIT等待的时间是2MSL

MSL是报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间的报文将被丢弃。2MSL即保证报文一来一回的时间,从第四次挥手发送ACK报文开始计时,如果服务器端没有接收到该报文则会重新发送FIN报文,在极端情况下ACK报文刚好到达服务器端,此时服务器端刚好重发FIN报文,等到重发FIN报文到达客户端时刚好是2MSL,所以需要2MSL保证FIN报文重发。

为什么需要TIME_WAIT状态

  • 防止历史连接中的数据被后面相同四元组的连接错误的接收

    设置为2MSL能够保证两个方向的数据包都将被丢弃,使得历史连接的数据包在网络中自然消失,再次出现的数据包一定是新建立的连接产生的

  • 保证被动关闭连接的一方能被正确的关闭

    保证第四次挥手ACK报文能够让被动关闭方接收从而正常关闭。如果TIME_WAIT时间过短或者没有就会导致重发后客户端已经进入CLOSE状态,此时就会回复RST报文,对服务器不友好

TIME_WAIT过多的危害

如果服务器有处于TIME_WAIT状态的TCP,说明是由服务器方主动发起的断开请求。过多的TIME_WAIT状态主要危害如下:

  • 内存资源占用
  • 端口资源占用,一个TCP连接至少消耗一个本地端口

如何解决TIME_WAIT状态过多

  • 服务器设置SO_REUSEADDR套接字来通知内核,如果端口被占用,但是TCP连接处于TIME_WAIT状态时可以重用端口
  • 使用长连接减少TCP的连接和断开,在长连接业务中往往不需要考虑TIME_WAIT状态

TCP报文头部格式

三分恶面渣逆袭:TCP 报文头部的格式

TCP如何保证可靠性的

  • 连接管理:三次握手、四次挥手
  • 校验和
  • 序列号/确认应答
  • 流量控制
  • 最大消息长度
  • 超时重传
  • 拥塞控制

TCP重传机制

超时重传

当数据包或者确认应答丢失时会触发超时重传

image-20240527152029107

RTT:往返时延,即数据发送时刻到接收到确认的时刻的差值(报文一来一回的时间)

RTO:超时重传时间,超时重传时间应该略大于往返时延

快速重传

不以时间为驱动,而是以数据驱动重传,基于接受端的反馈信息来引发重传。当连续接收到三个相同的ACK报文后直接进行重传。快速重传只解决了超时时间问题,不能解决是重传之前的一个还是所有报文。

带选择确认的重传(SACK)

在快速重传的基础上,接收方返回最近收到报文段的序列号范围,这样发送方就知道接收方哪写数据包是没收到的,这样就很清楚应该重传哪些数据包

重复SACK

在SACK基础上做了扩展,告诉发送方有哪些数据包自己重复接收了

滑动窗口

TCP发送一个数据,如果需要收到确认应答才会发送下一个数据。这样就会导致效率比较低。TCP引入窗口概念,有了窗口就可以指定窗口大小,窗口大小就是指无需等待确认应答而可以继续发送数据的最大值。接收方每次收到数据包在发送确认报文时同时告诉对方自己缓冲区还有多少空余空间,发送方根据这个剩余窗口大小值决定下次发送多少数据。

Nagle算法和延迟确认

当我们 TCP 报⽂的承载的数据⾮常⼩的时候,例如⼏个字节,那么整个⽹络的效率是很低的,因为每个 TCP 报⽂中都会有 20 个字节的 TCP 头部,也会有 20 个字节的 IP 头部,⽽数据只有⼏个字节,所以在整个报⽂中有效数据占有的比例就会⾮常低。

  • Nagle算法:

    任意时刻,最多只能有一个未被确认的小段。所谓 “小段”,指的是小于 MSS 尺寸的数据块,所谓 “未被确认”,是指一个数据块发送出去后,没有收到对方发送的 ACK 确认该数据已收到。必须满足以下两个条件:1)没有已发送未确认报文时,立刻发送数据;2)存在未确认报文时,直到【没有已发送未确认报文】或【数据长度达到MSS大小】时再发送数据

  • 延迟确认

    ACK报文如果没有携带数据则网络效率很低,TCP 延迟确认的策略:

    • 当有响应数据要发送时,ACK 会随着响应数据⼀起⽴刻发送给对⽅
    • 当没有响应数据要发送时,ACK 将会延迟⼀段时间,以等待是否有响应数据可以⼀起发送
    • 如果在延迟等待发送 ACK 期间,对⽅的第⼆个数据报⽂⼜到达了,这时就会⽴刻发送 ACK

拥塞控制

当网络出现拥堵时,如果继续发送数据包可能会导致数据包时延、丢失等,这是TCP就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大,于是拥塞控制的目的就是避免发送方的数据填满整个网络。

慢启动

当发送方每收到一个ACK,拥塞窗口cwnd的大小就会加一,呈指数性增长。当窗口大小小于慢启动阈值时使用慢启动算法,当大于等于慢启动阈值时使用拥塞避免算法

拥塞避免算法

进入拥塞避免算法后,每接收到一个ACK,cwnd增加1/cwnd,cwnd呈现线性增长,如果触发了重传机制就会进入拥塞发生算法

拥塞发生

当网络发生拥塞,也就是发生数据包重传,重传机制主要有两种:

  • 超时重传

    当发生超时重传,此时慢启动阈值变为cwnd/2,cwnd设置为初始值

  • 快速重传

    当发生快速重传,cwnd变成原来的一半,慢启动阈值变为当前cwnd

快速恢复

快速恢复和快速重传一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。从快速重传进入快速恢复算法后cwnd等于慢启动阈值+3,表示三个ACK数据包被接收了,然后重传丢失的数据包,如果再收到重复的ACK那么cwnd加一,如果收到新的ACK,把cwnd设置为慢启动阈值,表示恢复过程已经结束,可以恢复之前的状态了,即再次进入拥塞避免状态。

TCP半连接队列和全连接队列

在三次握手过程中,Linux内核会维护两个队列:半连接队列(SYN队列)和全连接队列(accept队列)

服务器端收到客户端发起的SYN请求后,内核会把该连接存储到半连接队列,并向客户端响应SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。

image-20240527164156218

当服务端并发处理大量请求时,如果 TCP 全连接队列过小,就容易溢出。发生 TCP 全连接队溢出的时候,后续的请求就会被丢弃,这样就会出现服务端请求数量上不去的现象。

TCP粘包和半包

为什么会出现粘包和半包

  • 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包
  • 接受数据端的应用层没有及时读取接受缓冲区中的数据,将发生粘包
  • 要发送的数据大于TCP发送缓冲区剩余空间大小将会发生半包
  • 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包

如何解决

  • 固定长度消息
  • 特殊字符作为边界
  • 自定义消息结构

四次挥手中收到乱序的FIN包会如何处理

​ 如果FIN报文比数据报先抵达客户端,此时FIN报文其实是一个乱序的报文,此时客户端的TCP连接不会从FIN_WAIT_2状态转移到TIME_WAIT状态。在FIN_WAIT_2状态时,如果收到乱序的FIN报文,那么就会被加入到乱序队列,并不会进入到TIME_WAIT状态。等再次收到前面被网络延迟的数据包时,会判断乱序队列有没有数据,然后会检测乱序队列中是否有可用的数据,如果能在乱序队列中找到与当前报文的序列号保持的顺序的报文,就会看该报文是否有 FIN 标志,如果发现有 FIN 标志,这时才会进入 TIME_WAIT 状态。

TCP一端断电和进程崩溃

前提:没有开启TCP keepalive

主机崩溃

客户端主机崩溃服务器是无法感知的,再加上服务器没有开启TCP keepalive,又没有数据交互的情况下,服务器的TCP连接将会一直处于ESTABLISHED连接状态,直到服务器重启进程。即在没有TCP保活机制且双方不传输数据的情况下,一方的TCP连接处在ESTABLISHED状态,并不代表另一方的连接还一定正常。

进程崩溃

TCP的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有TCP连接资源,于是内核会发送第一次挥手FIN报文,后续的挥手过程也都在内核完成并不需要进程的参与,所以及时服务器的进程退出了,还是能和客户端完成TCP四次挥手的过程。

拔掉网线后原本TCP连接还在吗

客户端拔掉网线后,并不会影响TCP连接状态。所以,拔掉网线后TCP连接是否还会存在关键要看拔掉网线后又没有数据传输。

有数据传输

  • 客户端拔掉网线后如果服务器端发送了报文,那么在服务器重传次数没有达到最大值之前,客户端插回了网线,那么双方原本的TCP连接还能够正常存在
  • 客户端拔掉网线后如果服务器端发送了报文,那么在服务器重传次数到达了最大值,服务器会断开TCP连接,等到客户端插回网线并发送数据,此时会收到服务器发送的RST报文而断开TCP连接

无数据传输

  • 如果没有开启TCP keepalive机制,那么客户端拔掉网线后,即使客户端一直不插回网线TCP连接也不会断开
  • 如果开启了TCP keepalive机制,客户端拔掉网线后,TCP keepalive机制会探测到对方TCP连接没有存活就会断开TCP连接

IP

IP协议相关技术

DNS域名解析、ARP和RARP协议、DHCP动态获取IP地址、NAT网络地址转换、ICMP网络控制报文协议、IGMP网络组管理协议

DNS域名协议

浏览器首先看一下自己的缓存里有没有,如果没有就向操作系统的缓存要,还没有就检查本机域名解析文件 hosts,如果还是没有,就会 DNS 服务器进行查询,查询的过程如下:

  1. 客户端首先会发出一个 DNS 请求,问 www.server.com 的 IP 是啥,并发给本地 DNS 服务器(也就是客户端的 TCP/IP 设置中填写的 DNS 服务器地址)。
  2. 本地域名服务器收到客户端的请求后,如果缓存里的表格能找到 www.server.com,则它直接返回 IP 地址。如果没有,本地 DNS 会去问它的根域名服务器:“老大, 能告诉我 www.server.com 的 IP 地址吗?” 根域名服务器是最高层次的,它不直接用于域名解析,但能指明一条道路。
  3. 根 DNS 收到来自本地 DNS 的请求后,发现后置是 .com,说:“www.server.com 这个域名归 .com 区域管理”,我给你 .com 顶级域名服务器地址给你,你去问问它吧。”
  4. 本地 DNS 收到顶级域名服务器的地址后,发起请求问“老二, 你能告诉我 www.server.com 的 IP 地址吗?”
  5. 顶级域名服务器说:“我给你负责 www.server.com 区域的权威 DNS 服务器的地址,你去问它应该能问到”。
  6. 本地 DNS 于是转向问权威 DNS 服务器:“老三,www.server.com对应的IP是啥呀?” server.com 的权威 DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。
  7. 权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。
  8. 本地 DNS 再将 IP 地址返回客户端,客户端和目标建立连接
image-20240531170032839

ARP

ARP(Address Resolution Protocol,地址解析协议)是网络通信中的一种协议,主要目的是将网络层的 IP 地址解析为链路层的 MAC 地址。

三分恶面渣逆袭:ARP 协议作用

  1. ARP 请求

​ 当主机 A 要发送数据给主机 B 时,首先会在自己的 ARP 缓存中查找主机 B 的 MAC 地址。如果没有找到,主机 A 会向网络中广播一个 ARP 请求数据包,请求网络中的所有主机告诉它们的 MAC 地址;这个请求包含了请求设备和目标设备的 IP 和 MAC 地址。

  1. ARP 应答

​ 网络中的所有主机都会收到这个 ARP 请求,但只有主机 B 会回复 ARP 应答,告诉主机 A 自己的 MAC 地址。并且主机 B 会将主机 A 的 IP 和 MAC 地址映射关系缓存到自己的 ARP 缓存中,以便下次通信时直接使用。

  1. 更新 ARP 缓存

​ 主机 A 收到主机 B 的 ARP 应答后,也会将主机 B 的 IP 和 MAC 地址映射关系缓存到自己的 ARP 缓存中。

ICMP

ICMP 主要的功能包括:确认 IP 包是否成功送达目标地址、报告发送过程中 IP 包被废弃的原因和改善网络设置等。

ping原理

ping基于ICMP实现:

  1. 当执行 Ping 命令,如ping www.baidu.com,Ping 首先解析域名获取 IP 地址,然后向目标 IP 发送一个 ICMP Echo Request 消息。

  2. 当目标 IP 收到 ICMP Echo Request 消息后,它会生成一个 ICMP Echo Reply 消息并返回,即 Ping 响应消息。

  3. 发起 Ping 命令的设备接收到 ICMP Echo Reply 消息后,计算并显示从发送 Echo Request 到接收到 Echo Reply 的时间(通常称为往返时间 RTT,Round-Trip Time),以及可能的丢包情况。

Java八股-并发编程篇

基础

并行和并发区别

并行是指多个处理器同时执行多个任务,同一时间多个任务同时进行

并发是指在单处理器上一个时间段内多个任务同时执行,但是某一时刻只有一个任务执行

进程和线程

  • 进程是CPU资源分配的最小单位,线程是CPU调度的最小单位
  • 线程执行在进程中,一个进程可以拥有多个线程
  • 进程之间的切换是耗时的,同一个进程间的线程进行切换很快
  • 同一个进程内的线程共享进程中的资源
  • 不同进程间的资源相互独立

线程创建方式

  • 继承Thread类

Java不支持多继承,如果已经继承了其他类则不能使用该方法

class ThreadExam extends Thread {
    public void run() {
        System.out.println("继承Thread创建线程...");
    }
    
    public static void main(String[] args) {
        ThreadExam exam = new ThreadExam();
        exam.start();
    }
}
  • 实现Runnable接口
class RunnableExam implements Runnable {
    public void run() {
        System.out.println("实现Runnable接口创建线程...");
    }

    public static void main(String[] args) {
        RunnableExam exam = new RunnableExam();
        Thread thread = new Thread(exam);
        thread.start();
    }
}
  • 实现Callable接口

可以结合FutureTask,通过get方法获取执行结果

class CallableExam implements Callable<String> {
    public String call() {
        return "实现Callable接口创建线程..."";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableExam exam = new CallableExam();
        FutureTask<String> futureTask = new FutureTask<>(exam);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}

为什么不直接调用线程的run()方法而去执行start()

通过调用 start() 方法来告知JVM需要创建新的线程并准备好需要的资源,然后在新建线程中执行 run() 方法,直接调用 run() 方法相当于只是调用了Thread中的一个普通方法,执行还是在主线程中。

线程常用调度方法

  • wait()

    A线程调用该方法将被阻塞挂起直到其他线程调用notify(notifyAll)或调用A线程的interrupt方法。其使用前提必须是在同步代码块中,并且当前线程拥有该对象的锁

    synchronized (obj) {
    	while (<condition does not hold>)
    		obj.wait();
    		... // Perform action appropriate to condition
    }
  • wait(long timeout)

    相比于wait方法,在指定时间超时后会自动返回

  • notify()

    A线程调用该方法将随机唤醒阻塞在当前加锁变量上的一个线程,直到当前线程放弃当前锁,唤醒的线程可以正常地争抢该锁。使用前提也必须是在同步代码块中,并且由加锁对象执行调用。

    synchronized (obj) {
    	while (<condition does not hold>)
    		obj.notify();
    		... // Perform action appropriate to condition
    }
  • notifyAll()

    相比于notify,该方法将唤醒所有阻塞在当前锁上的对象,使用条件相同

  • sleep(long millis)

    暂时让出执行时间的执行权,但是获取的锁仍然保持,等到时间到了会继续获取CPU资源,然后正常运行。

  • yield()

    Thread类的静态方法,一个线程调用yield后,表示当前线程请求让出CPU (注意:不会释放锁,只是从运行状态转移到就绪状态)

  • join()

    A、B线程,B线程调用A.join(),此时B线程会进入阻塞队列,直到线程A运行结束或线程B中断。join方法会释放锁

线程状态

状态 说明
NEW 初始状态:线程被创建,但还没有调用 start()方法
RUNNABLE 运行状态:Java 线程将操作系统中的就绪和运行两种状态笼统的称作“运行”
BLOCKED 阻塞状态:表示线程阻塞于锁
WAITING 等待状态:表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
TIME_WAITING 超时等待状态:该状态不同于 WAITIND,它是可以在指定的时间自行返回的
TERMINATED 终止状态:表示当前线程已经执行完毕
Java线程状态变化

线程间通信方式

  • volatile和synchronized
  • 等待/通知机制(wait、notify等)
  • 管道输入/输出流
  • Thread.join()
  • ThreadLocal

ThreadLocal

实现原理

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ....
}

set方法

每个线程都有一个ThreadLocalMap对象,在使用set方法时首先通过Thread.currentThread()获取当前线程,然后调用内部方法获取到当前线程的ThreadLocalMap对象,判断是否为空,为空则调用createMap方法直接创建并赋值;不为空则以当前ThreadLocal对象为key存储。

public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
	return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

get方法

同样首先获取当前线程,然后获得线程中的ThreadLocalMap对象,判断是否为空,为空直接调用setInitialValue方法(与set方法相同,仅将value修改为null);不为空则通过当前ThreadLocal对象获取value并返回。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

remove方法

通过getMap方法获取当前线程的ThreadLocalMap对象,非空则执行ThreadLocalMap内部remove方法

public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

ThreadLocalMap

ThreadLocalMap是ThreadLocal类的静态内部类,它是一个定制的哈希表,专门用于保存每个线程中的线程局部变量。

Entry

Entry继承了弱引用WeakReference<ThreadLocal<?>>,value用于存储与特定ThreadLocal对象关联的值。因为Entry的key为弱引用,所以当ThreadLocal外部的强引用被置为null,则根据可达性分析,ThreadLocal将会在下次GC中被回收,此时ThreadLocalMap就会出现key为null的Entry,如果线程不结束则key为null的value会一直存在一条强引用链,导致无法回收造成内存泄漏。Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

ThreadLocal各引用间的关系

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

set方法

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 获取当前key的hash值
    int i = key.threadLocalHashCode & (len-1);
	// 防止地址冲突
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        // 获取当前hash对应的Entry中的ThreadLocal对象
        ThreadLocal<?> k = e.get();
		// 为当前key则直接更新
        if (k == key) {
            e.value = value;
            return;
        }
		// 为空则替换掉当前槽位的key,防止空值key导致的内存泄漏
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	// 当前位置未被初始化过
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

扩容

ThreadLocalMap默认大小为16,阈值为2/3,超过阈值则准备扩容,首先会将key为null的entry的value设置为null便于垃圾回收,然后判断当前长度是否大于阈值的3/4,如果大于则进行扩容。

private static final int INITIAL_CAPACITY = 16;

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

private void setThreshold(int len) {
    threshold = len * 2 / 3;
}
private void rehash() {
    expungeStaleEntries();

    // 清理完key为null的Entry后长度仍大于阈值的3/4则进行扩容
    if (size >= threshold - threshold / 4)
        resize();
}

remove方法

首先获取当前ThreadLocal对应的key,然后执行clear方法,然后向后清理脏Entry数据

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         // 哈希碰撞
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            // 从该hash值对应索引向后开始查询,因为与当前索引发生碰撞后只会向后赋值
            // 清除掉当前key后向后查询已有的key是否是通过rehash得到的,判断获取到的ThreadLocal是否为null,如果是则清除,
            // 不是则重新进行hash计算并赋值,直到下一个槽位为null
            expungeStaleEntry(i);
            return;
        }
    }
}

// 发生哈希碰撞,线性向后查询
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
private final int threadLocalHashCode = nextHashCode();
// 初始为0
private static AtomicInteger nextHashCode = new AtomicInteger();
// 增长步长
private static final int HASH_INCREMENT = 0x61c88647;
// 获取下一个hash值
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

Java内存模型

线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了共享变量的副本,用来进行线程内部的读写操作。

  • 当一个线程更改了本地内存中共享变量的副本后,它需要将这些更改刷新到主内存中,以确保其他线程可以看到这些更改
  • 当一个线程需要读取共享变量时,它可能首先从本地内存中读取。如果本地内存中的副本是过时的,线程将从主内存中重新加载共享变量的最新值到本地内存中
深入浅出 Java 多线程:Java内存模型

原子性、可见性、有序性

JMM通过内存屏障来实现内存的可见性以及禁止重排序

指令重排序规则:

  • happens-before

  • as-if-serial

synchronized

synchronized可以修饰普通方法(相当于对当前对象加锁)、静态方法(相当于对当前类加锁)、代码块(显式的指定对谁加锁)

synchronized特性

  • 互斥

  • 刷新内存(和volatile类似)

  • 可重入

  • 非公平锁

synchronized原理

1、synchronized修饰代码块时,JVM采用monitorentermonitorexit两个指令来实现同步,monitorenter指令指向同步代码块的开始位置,monitorexit指令指向同步代码块的结束位置。

2、synchronized 修饰同步方法时,JVM 采用ACC_SYNCHRONIZED标记符来实现同步,这个标识指明了该方法是一个同步方法。

monitorenter、monitorexit 或者 ACC_SYNCHRONIZED 都是基于 Monitor 实现的。在 Java 虚拟机(HotSpot)中,Monitor 是由ObjectMonitor 实现的,可以叫做内部锁,或者 Monitor 锁。

ObjectMonitor:

  • 两个队列_WaitSet、_EntryList,分别用来保存wait状态和block状态线程
  • _owner,获取到Monitor对象的线程进入_owner区时,_count+1,线程调用wait方法则会释放Monitor对象,_owner 恢复为空, _count - 1。同时该等待线程进入 _WaitSet 中,等待被唤醒。同时每进入一次_count都会加一,从而实现了synchronized的可重入特性。

锁级别

优点 缺点 使用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗 CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗 CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行时间较长。

偏向锁

线程第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID。当下次该线程进入这个同步块时,会去检查锁的 Mark Word 里面是不是放的自己的线程 ID。

如果是,表明该线程已经获得锁了,以后该线程在进入和退出同步代码块时不需要花费CAS操作来加锁和解锁

如果不是,就代表有另一个线程来竞争这个偏向锁。这个时候会尝试使用 CAS 来替换 Mark Word 里面的线程 ID 为新线程的 ID。

  • 成功:表示之前的线程不存在了, Mark Word 里面的线程 ID 为新线程的 ID,锁不会升级,仍然为偏向锁;
  • 失败:表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为 0,并设置锁标志位为 00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。

轻量级锁

多个线程在不同时间段获取同一把锁,不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM 采用轻量级锁来避免线程的阻塞与唤醒。

重量级锁

重量级锁依赖于操作系统的互斥锁(mutex,用于保证任何给定时间内,只有一个线程可以执行某一段特定的代码段) 实现,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。

锁升级过程

  • 检查MarkWord中存放的是不是自己的ThreadId,如果是则表示当前线程处于偏向锁状态
  • 如果不是自己的ThreadId,锁升级,此时通过CAS来执行切换,新的线程根据MarkWord里现有的ThreadId,通知之前线程暂停,之前线程将MarkWord中的内容置空
  • 两个线程都把锁对象的 HashCode 复制到自己新建的用于存储锁的记录空间,接着开始通过 CAS 操作,把锁对象的 MarkWord 的内容修改为自己新建的记录空间的地址的方式竞争 MarkWord
  • 成功执行 CAS 的获得资源,失败的则进入自旋
  • 自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败
  • 进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己

注:一个对象在调用原生hashCode方法后(来自Object的,未被重写过的),该对象将无法进入偏向锁状态,起步就会是轻量级锁。若hashCode方法的调用是在对象已经处于偏向锁]状态时调用,它的偏向状态会被立即撤销,并且锁会升级为重量级锁。

synchronized锁对象

synchronized锁住的是对象,如果锁住的这个对象在多线程中相同,那么这些线程访问synchronized修饰的代码块时总是互斥的。但是如果锁住的这个对象在多线程中是不同的,那么这些线程访问synchronized修饰的代码块时不会互斥的。

  • 对象锁

    如果是同一个实例,就会按照顺序访问,如果是不同实例,就可以同时访问

    • 锁非静态变量
    • 锁this对象
    • 锁非静态方法
  • 类锁

    所有实例按照顺序访问

    • 锁静态变量
    • 锁类的class
    • 锁静态方法

volatile

volatile可以保证可见性但是不保证原子性

  • 当写一个volatile变量时,JMM会把该线程在本地内存中的变量强制刷新到主内存中
  • 这个写操作会导致其他线程中的volatile变量缓存无效

volatile会禁止指令重排,当使用volatile修饰变量时,JMM会插入内存屏障来确保以下两点:

  • 写屏障(Write Barrier):当一个 volatile 变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存
  • 读屏障(Read Barrier):当读取一个 volatile 变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取

ReentrantLock

ReentrantLock是可重入的独占锁,只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞。

ReentrantLock加锁和解锁过程:

// 创建非公平锁(默认创建的是非公平锁,传入参数true创建公平锁)
ReentrantLock lock = new ReentrantLock();
// 获取锁操作
lock.lock();
try {
    // 执行代码逻辑
} catch (Exception ex) {
    // ...
} finally {
    // 解锁操作
    lock.unlock();
}

公平锁和非公平锁

  • 公平锁意味着在多个线程竞争锁时,获取锁的顺序与线程请求的顺序相同,即先来先服务(FIFO)。虽然能保证锁的顺序,但是需要额外维护一个有序队列
  • 非公平锁不保证线程获取锁的顺序,当锁被释放时,任何请求锁的线程都有机会获取锁,而不是按照请求的顺序。

synchronized和ReentrantLock

synchronized是一个关键字,而Lock属于一个接口

三分恶面渣逆袭:synchronized和ReentrantLock的区别

  • 使用方式不同

    synchronized可以直接在方法上加锁,也可以在代码块上加锁(不需要手动释放锁,锁会自动释放),而ReentrantLock必须手动声明来加锁和释放锁

  • 功能特点不同

    如果需要更细粒度的控制(如可中断的锁操作、尝试非阻塞获取锁、超时获取锁或者使用公平锁等),可以使用 Lock

AQS

AQS,全称是 AbstractQueuedSynchronizer,中文意思是抽象队列同步器。AQS 的思想是,如果被请求的共享资源空闲,则当前线程能够成功获取资源;否则,它将进入一个等待队列,当有其他线程释放资源时,系统会挑选等待队列中的一个线程,赋予其资源。

三分恶面渣逆袭:AQS抽象队列同步器
  • 同步状态state由volatile修饰,保证多线程之间的可见性

    private volatile int state;
  • 同步队列时通过内部定义的Node类来实现的,每个Node包含等待状态、前后节点、线程的引用等

    static final class Node {
        // 表示该结点(对应的线程)已被取消
        static final int CANCELLED =  1;
        // 表示后继结点(对应的线程)需要被唤醒
        static final int SIGNAL    = -1;
        // 表示该结点(对应的线程)在等待某一条件
        static final int CONDITION = -2;
        // 表示有资源可用,新head结点需要继续唤醒后继结点
        // (共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;
        // 设置新的head结点时,会继续唤醒其后继结点)
        static final int PROPAGATE = -3;
    
        volatile Node prev;
    
        volatile Node next;
    
        volatile Thread thread;
    }
  • 两种同步方式

    • 独占模式(Exclusive):资源是独占的,一次只能有一个线程获取,如ReentrantLock。
    • 共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定,如Semaphore、CountDownLatch

preview

加锁

ReentrantLock.lock()

public void lock() {
    sync.lock();
}
abstract static class Sync extends AbstractQueuedSynchronizer

sync是一个静态内部类,继承AQS,有两个实现NofairSync(非公平锁),FailSync(公平锁)

NonfairSync.lock

final void lock() {
    if (compareAndSetState(0, 1)) // 通过cas操作来修改state状态,表示争抢锁的操作
      setExclusiveOwnerThread(Thread.currentThread());// 设置当前获得锁状态的线程
    else
      acquire(1); //尝试去获取锁
}

acquire

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

由于是非公平锁,所以调用lock方法时,先通过cas去抢占锁,如果抢占锁成功则保存获得锁成功的当前线程,否则调用acquire来走锁竞争逻辑。通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false,如果tryAcquire失败则会通过addWaiter方法将当前线程封装成Node添加到AQS队列尾部;acquireQueued将Node作为参数,通过自旋去尝试获取锁

acquire流程

释放锁

ReentrantLock.unlock

首先释放锁然后唤醒park的线程

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

CAS

CAS(Compare-and-Swap)是一种乐观锁的实现方式,全称为“比较并交换”,是一种无锁的原子操作。

在CAS中有三个值:V(要更新的变量)、E(预期值)、N(新值),比较并交换的过程如下:判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其他线程更新了V,于是当前线程放弃更新,什么也不做。

当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

CAS三大问题

  • ABA问题

    一个值原来是A,变成了B,又变回了A,这个时候CAS检查不出来。(栈顶元素判断)

    追加版本号或时间戳

  • 长时间自旋

    CAS长时间自旋不成功会占用大量CPU资源

  • 多个共享变量的原子操作

    当对一个共享变量执行操作时,CAS 能够保证该变量的原子性。但是对于多个共享变量,CAS 就无法保证操作的原子性,这时通常有两种做法:

    1. 使用AtomicReference类保证对象之间的原子性,把多个变量放到一个对象里面进行 CAS 操作;
    2. 使用锁。锁内的临界区代码可以保证只有当前线程能操作。

线程死锁

  • 互斥条件
  • 请求并持有
  • 不可剥夺条件
  • 循环等待条件

线程同步

  • 互斥量
  • 读写锁
  • 条件变量
  • 自旋锁
  • 信号量

线程池

线程池其实是一种池化的技术实现,池化技术的核心思想就是实现资源的复用,避免资源的重复创建和销毁带来的性能开销。线程池可以管理一堆线程,让线程执行完任务之后不进行销毁,而是继续去处理其它线程已经提交的任务。

线程池参数

  • corePoolSize:核心线程数

    线程池中核心线程数量,即使这些线程处于空闲状态也不会被回收。

  • maximumPoolSize:最大线程数

    线程池允许的最大线程数量。当工作队列满了之后,线程池会创建新线程来处理任务,直到线程数达到这个最大值。

  • keepAliveTime:非核心线程存活时间

    非核心线程的空闲存活时间。如果线程池中的线程数量超过了 corePoolSize,那么这些多余的线程在空闲时间超过 keepAliveTime 时会被终止。

  • unit:非核心线程存活时间单位

  • workQueue:等待队列

    用于存放待处理任务的阻塞队列。当所有核心线程都忙时,新任务会被放在这个队列里等待执行。

    • ArrayBlockingQueue:有界的先进先出的阻塞队列,底层是数组,适合固定大小的线程池
    • LinkedBlockingQueue:底层数据结构时链表,不指定大小默认是Integer.MAX_VALUE,相当于一个无界队列
    • PriorityBlockingQueue:支持优先队列排序的无界阻塞队列。任务按照其自然顺序或通过构造器给定的Comparator来排序
    • DelayQueue:由二叉堆实现的无界优先级阻塞队列
    • SynchronousQueue:实际上它不是一个真正的队列,因为没有容量。每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都必须等待另一个线程的插入操作
  • threadFactory:创建线程使用工厂

  • handler:饱和拒绝策略

    定义了当线程池和工作队列都满了之后对新提交的任务的处理策略。

    • AbortPolicy:默认拒绝策略。抛出异常
    • CallerRunsPolicy:不抛出异常,会让提交任务的线程自己来执行任务
    • DiscardOldestPolicy:丢弃队列中最老的一个任务(在队列中等待最久的任务)
    • DiscardPolicy:直接丢弃该任务

常见线程池

  • newFixedThreadPool (固定数目线程的线程池)

    核心线程数和最大线程数相同,阻塞队列为无界队列LinkedBlockingQueue,容易造成OOM

  • newCachedThreadPool (可缓存线程的线程池)

    核心线程数为0,最大线程数为Integer.MAX_VALUE,可能无限创建线程导致OOM,阻塞队列为SynchronousQueue,非核心线程空闲存活时间为60秒

  • newSingleThreadExecutor (单线程的线程池)

    核心线程数和最大线程数都为1,阻塞队列为无界队列LinkedBlockingQueue,可能导致OOM

  • newScheduledThreadPool (定时及周期执行的线程池)

    最大线程数为Integer.MAX_VALUE,有OOM风险,阻塞队列为DelayedWorkQueue

动态调节参数

  • 利用配置中心配置线程池参数,监听修改后通过对应的set方法重新配置线程池
  • 自己实现线程池,监听参数变化,根据实际业务需求更改对应参数

并发容器

ConcurrentHashMap

在JDK7时采用分段锁机制(Segment Locking),整个Map被分为若干段,每个段都可以独立地加锁,因此不同线程可以同时操作不同的段从而实现并发访问。

初念初恋:JDK 7 ConcurrentHashMap

在 JDK 8 及以上版本中,ConcurrentHashMap 的实现进行了优化,不再使用分段锁,而是使用了一种更加精细化的锁——桶锁,以及 CAS 无锁算法。每个桶(Node 数组的每个元素)都可以独立地加锁,从而实现更高级别的并发访问。

初念初恋:JDK 8 ConcurrentHashMap

同时,对于读操作,通常不需要加锁,可以直接读取,因为 ConcurrentHashMap 内部使用了 volatile 变量来保证内存可见性。

对于写操作,ConcurrentHashMap 使用 CAS 操作来实现无锁的更新,这是一种乐观锁的实现,因为它假设没有冲突发生,在实际更新数据时才检查是否有其他线程在尝试修改数据,如果有,采用悲观的锁策略,如 synchronized 代码块来保证数据的一致性。

Hashtable

Hashtable 在任何时刻只允许一个线程访问整个 Map,通过对整个 Map 加锁来实现线程安全。而 ConcurrentHashMap(尤其是在 JDK 8 及之后版本)通过锁分离和 CAS 操作实现更细粒度的锁定策略,允许更高的并发。

CopyOnWriteArrayList

CopyOnWriteArrayList 是一个线程安全的 ArrayList,它遵循写时复制(Copy-On-Write)的原则,即在写操作时,会先复制一个新的数组,然后在新的数组上进行写操作,写完之后再将原数组引用指向新数组。

Java八股-MySQL篇

MySQL优化

如何定位慢查询

聚合查询、多表查询、表数据量过大查询、深度分页查询

  • 开源工具
    • 调试工具:Arthas
    • 运维工具:Prometheus、Skywalking
  • MySQL自带慢查询日志(slow_query_log、long_query_time)

如何分析慢查询

explain或者desc获取MySQL如何执行select语句的信息

  • possible_keys:当前sql可能用到的索引
  • key:当前sql实际命中的索引
  • key_len:索引占用的大小
  • type:sql连接的类型,NULL、system、const、eq_ref、ref、range、index、all

什么是索引

索引是帮助MySQL高效获取数据的数据结构(有序)

采用B+树作为数据结构存储索引

  • 阶数更多。路径更短
  • 磁盘读写代价B+树更低,非叶子结点只存储指针,叶子结点存储数据
  • B+树便于扫库和区间查询,叶子结点是一个双向链表

聚簇索引?非聚簇索引?会回表查询?覆盖索引?

  • 聚簇索引:数据和索引放一块,B+树的叶子结点保存整行数据,有且只有一个
  • 非聚簇索引(二级索引):数据和索引分开存储,B+树的叶子结点保存对应的主键,可以有多个
  • 回表查询:通过二级索引找到对应的主键值,到聚簇索引中查找整行数据
  • 覆盖索引:查询使用了索引,并且需要返回的列在该索引中已经全部能够找到

MySQL超大分页处理

在数据量比较大时,如果进行limit分页查询,在查询时越往后分页查询效率越低

优化思路:一般分页查询时,通过创建覆盖索引能够比较好地提升性能,可以通过覆盖索引加子查询形式优化。

超大分页一般都是在数据量比较大时,我们使用limit分页查询,并且需要对数据进行排序,这个时候效率就很低,我们可以采用覆盖索引+子查询来解决。

先分页查询数据id字段,确定了id之后又,再用子查询来过滤,只查询这个id列表中的数据就可以了,因为查询id的时候,走的覆盖索引,所以效率可以提升很多。

select * 
from User_table a,
	(select id from User_table order by id limit 1000000,10) b 
where a.id = b.id

索引创建原则

  • 针对数据量较大,且查询比较频繁的表建立索引(10w)
  • 针对常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引
  • 选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高
  • 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引
  • 尽量使用联合索引,减少单列索引,查询时联合索引很多时候可以使用覆盖索引,节省存储空间,避免回表,提高查询效率
  • 控制索引数量,影响增删改效率
  • 如果索引列不能存储NULL,创建表时使用NOT NULL约束,优化器知道每列是否包含NULL值时,可以更好地确定哪个索引更有效地用于查询

索引失效

联合索引:(a,b,c)

  • 违反最左前缀法则

    a=A,c=C时会只使用a索引

  • 范围查询右边的列不能使用索引

    a=A,b>B时用不到c索引

  • 索引列上进行运算操作

    类似substring(a, 0, 2),索引失效

  • 字符串不加单引号

    查询时,没有对字符串加单引号,MySQL的查询优化器会自动进行类型转换造成索引失效

  • 模糊查询

    %like会失效,like%不会

sql优化

image-20240521161345554

事务

事务特性

ACID;原子性、一致性、隔离性、持久性

并发事务带来了哪些问题

脏读:一个事务读到另一个事务还没提交的数据

不可重复度:一个事务先后读取同一条数据,但是两次读取数据不同

幻读:一个事务按照条件查询数据时,没有对应的数据行,但是插入数据时又发现这行数据已经存在

隔离级别:

隔离级别 脏读 不可重复读 幻读
Read uncommitted 读未提交
Read committed 读已提交 ×
Repeatable Read 可重复度 × ×
Serializable 串行化 × × ×

undo log和redo log

  • 缓冲池(buffer pool):主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,增删改查时先操作缓冲池中的数据(没有就从磁盘上加载并缓存),以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度
  • 数据页(page):是InnoDB存储引擎磁盘管理的最小单元,每个页大小默认为16KB,页中存储的是行数据

redo log

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性

该文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存在该日志文件中,用于在刷新脏页到磁盘发生错误时进行数据恢复使用。

undo log

回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚MVCC。undo log和redo log记录物理日志不一样,它是逻辑日志

  • delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然
  • update一条数据时,记录一条相反的update记录

undo log可以实现事务的一致性和原子性

redo log和undo log区别

  • redo log记录的是数据页的物理变化,服务器宕机可以用来同步数据
  • undo log记录的是逻辑日志,当事务回滚时通过逆操作恢复原来数据
  • redo log保证事务持久性,undo log保证事务原子性和一致性

MVCC(多版本并发控制)

实现原理

  • 记录中的隐藏字段

    隐藏字段 含义
    DB_TRX_ID 最近一次修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID
    DB_ROLL_PTR 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上个版本
    DB_ROW_ID 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段
  • undo log

    当insert时产生的undo log日志只在回滚时需要,在事务提交后可被立即删除

    update、delete的时候产生的undo log日志不仅回滚时需要,mvcc版本访问时也需要,不会立即删除

  • undo log版本链

    不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的记录

  • readview

    ReadView(视图读)是快照读SQL执行时MVCC提取数据的依赖,记录并维护系统当前活跃的事务(未提交)id

    字段 含义
    m_ids 当前活跃的事务ID集合
    min_trx_id 最小活跃事务ID
    max_trx_id 预分配事务ID,当前最大事务ID+1
    creator_trx_id ReadView创建者的事务ID

    RC隔离级别下,在事务中每一次执行快照读时生成ReadView

    RR隔离级别下,仅在事务第一次执行快照读时生成ReadView,后续复用该ReadView

image-20240521174453239

MySQL主从同步

二进制日志(binlog)记录了所有的DDL(数据定义语言 create drop等)语句和DML(数据操纵语言 update insert等),但不包括数据查询(select show)语句

  • Master主库在事务提交时会把数据变更记录在二进制日志文件Binlog中
  • 从库读取主库的binlog文件,写入到从库的中继日志relay log
  • 从库重做中继日志中的事件,实现主从同步

分库分表

垂直分库

以表为依据,根据业务将不同表拆分到不同库中

特点:

  • 按业务对数据分级管理、维护、监控、扩展
  • 在高并发下,提高磁盘IO和数据量连接数

垂直分表

以字段为依据,根据字段属性将不同字段拆分到不同表中;不常用的字段单独放一张表,把text、blob等大字段拆分出来放在附表中

特点:

  • 冷热数据分离
  • 减少IO过度争抢,两表互不影响

水平分库

将一个库的数据拆分到多个库中

路由规则:

  • 根据id取模
  • 按照id范围路由(1-100w)(100w-200w

特点:

  • 解决了单库大数量、高并发的性能瓶颈问题
  • 提高系统稳定性和可用性

水平分表

将一个表的数据拆分到多个表中(可以在同一个库中)

特点:

  • 优化单表数据过大产生的性能问题
  • 避免IO争抢并减少锁表的几率

Java八股-Redis篇

使用场景

缓存

缓存穿透

查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,每次请求都会查数据库

解决方案:

  • 缓存空数据,查询返回的数据为空,仍然把空结果进行缓存(简单、消耗内存,存在不一致问题)
  • 布隆过滤器(缓存预热时初始化)
    通过多个hash函数获取hash,将hash结果对应数组位置改为1

缓存击穿

当某一个key设置了过期时间,当key过期时,恰好有大量请求访问这个key,并发请求压垮数据库

解决方案:

  • 互斥锁
    保证数据强一致性,性能差
  • 逻辑过期
    热点key不设置过期时间。发现逻辑过期,获取互斥锁,重开线程进行缓存重建并更新过期时间。高可用、性能优,不保证数据绝对一致

缓存雪崩

同一个时间段内缓存key同时失效或者redis宕机,导致大量请求到达数据库

解决方案:

  • 给不同的key的过期时间添加随机值(同时过期)
  • 利用redis集群提高服务可用性
  • 给缓存业务添加降级限流策略(保底策略;防止缓存穿透、击穿、雪崩)
  • 给业务添加多级缓存

mysql数据如何和redis进行同步(双写一致性)

保证强一致性,先删除缓存还是数据库

延迟双删(数据库主从同步时间,延时时间不好把控且延时过程中会出现脏数据)、redis读写锁

  • 先删除缓存再删除数据库

    image-20240517205825138

  • 先删除数据库再删除缓存

    image-20240517205950034

保证弱一致性

异步通知,保证最终一致性(消息队列,Canal中间件)

redis持久化

  • RDB(文件小,恢复速度快,数据丢失多)

把内存中的所有数据都记录到磁盘中,redis故障重启后,从磁盘读取快照文件并恢复数据

bgsave执行原理:
通过fork创建子进程复制页表信息,可以访问到磁盘数据。同时采用copy-on-write 技术,对磁盘数据进行read-only 设置,主进程写操作时拷贝数据副本进行改写,同时读取时也读取副本内容。

  • AOF(追加文件,文件大小大,恢复速度慢,数据丢失少)

记录每次执行命令

结合两者使用:首先使用RDB进行数据恢复,在使用AOF的增量数据进行数据恢复

redis数据过期策略

  • 惰性删除(CPU友好,内存占用大)
    设置key过期时间后不去管他,需要使用时检查是否过期,过期立马删除,否则返回

  • 定期删除
    每隔一段时间对一些key进行检查,删除里面过期的key

redis删除策略:惰性删除+定期删除

redis数据淘汰策略

内存不够时,继续添加新的key,redis会按照某种规则将内存中的数据删掉

  • noeviction(默认策略,不淘汰,禁止写入)
  • volatile-ttl(ttl越小越先淘汰)
  • allkeys-random(随机淘汰)
  • volatile-random(对设置了ttl的的key,随机淘汰)
  • allkeys-lru(所有key LRU)
  • volatile-lru(设置了ttl的key LRU)
  • allkeys-lfu(所有key LFU)
  • volatile-lfu(设置了ttl的key LFU)

分布式锁

分布式环境下synchronized锁无法实现加锁功能

setnx

redisson(lua脚本 原子性)

加锁(成功),操作redis,另起一个线程(watch dog)每隔一段时间(默认10秒)做一次过期时间的续期
加锁(失败),watch dog执行while循环不断获取锁直到达到等待时间

redisson加锁可重入,根据线程id,利用hash结构记录线程id和重入次数key-(<threadId, count>);不能解决主从数据一致性问题

其他面试题

redis主从同步

全量同步

  • 从节点发送数据同步请求并附带replid和offset
  • 主节点判断replid是否一致,不一致表示第一次同步,返回自身replid和offset至从节点;一致则直接将repl_backlog发送给从节点,从节点根据对比offset进行数据同步
  • replid不一致,从节点保存主节点回传的replid和offset,同时主节点执行bgsave生成RDB文件并发送,从节点清空本地数据加载RDB,最后执行主节点发送的repl_backlog文件中的命令

增量同步(slave重启或后期数据变化)

从节点发送请求,主节点判断是否第一次请求,不是则从repl_backlog中获取offset后的数据发送给从节点,从节点执行命令

哨兵模式

image-20240517222059017

image-20240517222128710

image-20240517222145269

分片集群

image-20240517222522412

Redis单线程为什么这么快

  • Redis是纯内存操作,执行速度快
  • 采用单线程,避免了不必要的上下文切换,多线程还要考虑线程安全问题
  • 使用I/O多路复用模型,非阻塞IO

​ Redis性能瓶颈是网络延迟

JVM七连问

关于Object o = new Object()

1、请解释一下对象的创建过程(半初始化)

申请空间 - 初始化 - 引用

JVM首先检查当前类是否被加载解析和初始化过,如果没有就先执行对应的类加载器;如果加载了就为新对象分配内存空间,并且将分配的内存空间初始化为零值(成员变量,数值类型是 0,布尔类型是 false,对象类型是 null),接下来设置对象头,最后JVM执行构造方法(<init> ),将成员变量赋值为预期的值。

2、DCL(double check lock)要不要加volatile问题(指令重排)

synchronized 内部可以重排序,锁的外部可以访问到中间状态

必须要加 volatile(保证线程之间的可见性和禁止指令重排序(内存屏障))

public class SingleObject {
    private static volatile SingleObject INSTANCE;

    private SingleObject() {
    }

    public static SingleObject getInstance() {
        if (INSTANCE == null) {
            synchronized (SingleObject.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new SingleObject();
                }
            }
        }
        return INSTANCE;
    }
}

3、对象在内存中的存储布局(对象与数据的存储不同)

  • Markword(64位机器8字节 32位机器4字节)
  • 类型指针(4字节)
  • 实例数据(String类型4字节)
  • 对齐(被8整除)

4、对象头具体包括什么

markwordclass pointer

markword

  • 锁信息
  • GC信息
  • hashcode

5、对象怎么定位

直接间接

  • 直接(默认)
    直接指针,仅包含实例数据,实例数据中包含类型数据指针指向方法区中的T.class

    速度快

  • 间接
    句柄方式,包含实例数据指针和类型数据指针

    对象在内存中移动位置,地址不用变

6、对象怎么分配

栈上 - 线程本地 - Eden - Old

  • 尝试分配到栈上(逃逸分析、标量替换)
  • 足够大直接分配到老年代
  • TLAB(给每个线程在伊甸区的一个独立空间,线程同步)
  • 分配到伊甸区

7、Object o = new Object()在内存中占多少字节

16字节

24-04-18番茄小说一面

问题:

  • 拷打项目
  • 项目中的分布式id生成器:id产生浪费、超过id段情况(超过直接返回null)、数据库交互
  • 其他的id生成器(本地的那种):UUID、雪花算法
  • 数据库分库
  • 主从数据库一致性怎么保证(binlog)
  • redis用过哪写数据结构(场景)string底层实现
  • mysql多表设计(关联关系、结构设计等)
  • 场景题:电影售票系统(怎么设计表,表的字段)
  • volatilesynchronizedReentrantLock 那个熟,说前两个,让讲一下volatile
  • 讲一下RockerMQ(原理)
  • Http和Https区别
  • https怎么加密,加密过程
  • mysql慢查询,怎么识别慢查询(explain

算法:

image-20240421212628862

24-04-15美团前端&移动端面2

上次面了一次后估计是被刷了,简历又被捞起来了重新拷打!

问题:

  • 线程和进程的区别

  • TCP三次握手

  • 为什么要三次握手、两次不行吗

  • getpost 请求区别

  • StringStringBuilderStringBuffer区别

  • String为什么要设计成不可变的

  • Java泛型

  • HashMap 实现原理

  • 扩容算法

  • ConCurrentHashMap

  • synchronizedvolatile 区别

  • synchronized 修饰哪写

  • 保证线程安全有哪些方式

  • notifynotifyAll

  • 使用场景(使用位置)要求

  • 垃圾回收机制

  • 怎么判断对象是否要被回收(可达性分析、引用计数)

  • 有哪些可以被当做 GCRoots

    • JVM栈中的引用(方法参数、局部变量等)

    • 本地方法栈中的JNI引用

    • 类静态变量

    • 运行时常量池中的常量(String、Class类型)

  • 强引用、软引用、弱引用、虚引用区别

  • 说一下哪写技能会在职业领域反复使用(比较开放,技术、沟通、协作…)

算法:

image-20240421210237894

image-20240421210438785

24-04-10美团前端&移动端面

问题:

  • 安卓四大组件(不会直接问Java了)

  • 说一下StringBuilder和StringBuffer区别

    StringBuilder与StringBuffer功能类似,但是StringBuffer是线程安全的,方法都添加了synchronized关键字。

  • 为什么不直接使用String而要用StringBuilder或者StringBuffer

    String类的对象是不可变的,一旦String对象被创建,所包含的字符串内容是不可改变的。每次对String对象进行修改操作都会生成一个新的String对象,导致大量的内存开销。而StringBuffer和StringBuilder的字符串修改操作都是直接在原有字符串对象的底层数组上进行的,而不会生成新的String对象。

  • String内部成员变量、.length()方法时间复杂度多少

    以JDK8为例:包含final修饰的 char数组hash

    image-20240410185212778

    其中 .length() 方法调用的是 value 数组的 length 属性

    image-20240410185423398

  • 如何将String设计成动态容量的

    问的是C++中string是可变的,能不能Java的String也实现动态长度?

  • Map底层数据结构???如何保证key唯一

    回答的HashMap,说Map类似?不懂啥意思

  • Java与C++区别

  • JVM、垃圾回收

    JVM原理最全、清晰、通俗讲解,五天40小时吐血整理_jvm原理讲解教程最全清晰通俗讲解-CSDN博客

  • C++源文件如何到机器码?还是字节码如何到机器码

  • 网页输入网址到显示内容发生了什么

    1、DNS解析:发送DNS请求(浏览器缓存、本地缓存、hosts文件),发送给本地DNS服务器(缓存有直接返回),没有询问根域名服务器,根据根域名的指示找到对应的DNS服务器得到IP地址

    2、TCP连接 三次握手

    3、发送HTTP请求给服务器

    4、服务器处理请求并返回响应报文

    5、浏览器解析渲染页面

    6、结束连接 四次挥手

  • 不同局域网可能包含相同ip,顶端DNS如何将查询到的信息返还给请求主机

    不懂

  • 线程池、如何设计

    核心线程数、最大线程数、非核心线程存活时间、阻塞队列、拒绝策略、线程工厂

  • 其他忘了。。。

算法:

  • 树的最近祖先

    e3698b209adb05022f00c94a63c9485b

  • 最长合法括号序列

    5eb3f230d9be3dcbdd9a43ede7b36c8b

原文地址:Lightweight Remote Sensing Change Detection With Progressive Feature Aggregation and Supervised Attention

摘要

问题

尽管之前的基于CNN的方法能够取得不错的结果,但是模型的内存和计算成本太高,很难在实际中应用

工作

提出一种新颖的轻量级网络,它通过渐进式特征聚合和监督注意力,根据移动网络提取的特征识别变化

  • 设计了一个neighbor aggregation module(NAM)来融合主干网络邻近阶段的特征,用来增强时间特征的表示能力
  • 提出progressive change identifying module(PCI)从双时相特征中提取时间差异信息
  • 设计了supervised attention模块(SAM)来对特征重新加权,以有效地从高层到低层聚合多级特征

引言

先前方法缺点

参数量和计算成本太大,不利于模型在真实世界的部署实现

解决方法

  1. 使用轻量的特征提取网络(MobileNetV2),同时为了弥补特征表示能力的不足,构建NAM来增强特征表示能力
  2. 设计一个具有轻量化模块的decoder,PCI、SAM

主要贡献

  1. 提出轻量化模型A2Net(3.78 M parameters and 6.02 G FLOPs),并且取得了sota
  2. 提出NAM增强backbone特征表示能力
  3. 提出PCI逐步找到不同特征级别的时间变化信息以准确识别变化对象,并且使用SAM来重新加权特征以实现渐进的特征聚合

方法

网络结构

首先通过骨干网络提取多尺度特征,然后经过NAM对特征进行增强,最后获得差分特征。后面先将差分特征输入PCIM中进行差异特征增强然后经过SAM对特征进行重新加权,输出预测图。

image-20230224103752830

NAM

由于backbone采用的是移动网络MobileNetV2,所以提取到的特征表征能力较弱,于是提出NAM对特征进行增强。具体操作为:所提取特征上下阶段特征分别进行最大池化和上采样后经过卷积与当前阶段特征进行concat,而后当前特征通过1×1卷积构成一个残差网络,保持当前阶段特征的主要信息。

image-20230224113134285

PCIM

PCIM主要是为了成分挖掘差异特征中的变化信息,输入特征从大感受野到小感受野逐步挖掘变化信息。

image-20230224113608045

SAM

在上一步通过PCIM已经充分挖掘了变化信息,但是由于高级特征缺少上下文信息的指导,容易造成噪声等无关信息。所以SAM旨在对特征进行重新加权,确保变化特征能够得到更多的关注,忽略无关特征。该模块首先经过1×1卷积然后通过激活层。最后得到变化图(值为0-1)和非变化图(用全1矩阵减去变化图),将两者进行concat,通过1×1卷积后作为权重map与原特征进行点乘,最后得到重加权的特征。

image-20230224114240434

实验

对比实验

该模型相比于其他的方法具有更好的效果。如图7第三行所示,相同的建筑物在不同的时间显示不同的颜色,从而导致一些与真实建筑物变化相反的无关变化,该方法能够较好地检测出变化区域;同时图8第六行,许多方法无法区分包含与建筑物外观相似的道路的伪变化。相反,所提出的方法可以很好地识别建筑物变化的边界和主体。

image-20230224114942640

image-20230224114957987

image-20230224115022493

image-20230224115046949

消融实验

image-20230224115119249

总结

  1. NAM对于特征有较好的增强效果,特征交互类似思路可行
  2. PCIM中利用不同感受野挖掘特征潜在信息也能够较好地运行,在对于较小分辨率特征是否可行?
  3. SAM类似于自注意力机制,主要是通过变化图和非变化图生成特征权重图。

论文地址:Remote Sensing | Free Full-Text | RACDNet: Resolution- and Alignment-Aware Change Detection Network for Optical Remote Sensing Imagery

摘要

动机

变化检测任务是基于等效分辨率的共同配准多时相图像。由于传感器成像条件和重访周期的限制,很难获得所需的图像,特别是在紧急情况下。此外,准确的多时相图像配准在很大程度上受到大量对象变化和匹配算法的限制。

主要思路

  • 首先,提出了一种基于WDSR的轻量级超分辨率网络。为了更好地恢复高频细节信息,计算梯度权重并将其分配给不同的区域,从而迫使网络集中在难以重建的区域。
  • 为了减轻过度平滑的影响,进一步引入了对抗损失和感知损失,以提高重建图像的视觉感知质量
  • 在孪生U-Net中为了对齐双时态深度特征,可变形卷积单元 (DCU) 通过使用学习到的偏移量扭曲特征图来使用。
  • 解码器中为了弥合编码器和解码器之间的语义鸿沟,嵌入了一个有效的注意单元(AU)。

主要工作

  • 提出了一种新颖的超分辨率网络,它在恢复 RS 图像中的高频细节方面简单而有效。
  • 提出了一种对齐感知 CD 网络,其中可以通过使用 DCU、ACU 和注意机制进一步引入双时态深度特征来显式对齐以提高 CD 性能

网络结构

整体网络结构分为两部分,第一部分为将图像转为高分辨率图像来凸显高频部分,使得特征中高频细节信息更加丰富。第二部分为特征提取和变化信息生成部分,主要采用孪生U-Net网络结构进行特征提取,并且在双时相图像融合的过程中采用DCU模块进行图像特征配准,解决图像配准问题

image-20221125120756581

image-20221125120956911

image-20221125121009521

DCU

首先将双时相特征进行concat然后通过卷积层输出各个坐标的位移矩阵(即当前像素位置应当如何调整,分为x、y两个方向),之后将位移矩阵与对应特征相乘便得到了配准之后的特征,后续就可以做对应操作了。

image-20221125121232605

AU

因为深层特征包含丰富的语义信息,能够很好地识别背景和前景,所以通过通道注意力得到深层特征的通道级别注意力来重新校准低级特征,不仅减轻了语义鸿沟而且还能很好地融合高级特征的语义特征和低级特征的细节信息。

image-20221125121726955

总结

图像高频信息在后续实验中可以着重思考,并且特征对齐、配准等也有实验价值。