Java方向优秀面经合集(下)

篇三:老铁意向+ 很多tcp的总结

作者:@Yasuo


刚拿到了铁手意向书了,效率真快,一周全完事,hr面后一天就给意向了。

秋招以来好多TCP 的问题都不会,之后复盘详细看了相关资料,写了不少tcp的问题,比较基础的握手挥手就没记下来


tcp 大礼包

连接到底是什么?

所谓的连接其实只是双方都维护了一个状态,通过每一次通信来维护状态的变更,使得看起来好像有一条线关联了对方。

TCP 协议头

l  序号:用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。

l  确认号:期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。

l  数据偏移:指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。

l  控制位:八位从左到右分别是 CWR,ECE,URG,ACK,PSH,RST,SYN,FIN。CWR:CWR 标志与后面的 ECE 标志都用于 IP 首部的 ECN 字段,ECE 标志为 1 时,则通知对方已将拥塞窗口缩小;

l  ECE:若其值为 1 则会通知对方,从对方到这边的网络有阻塞。在收到数据包的 IP 首部中 ECN 为 1 时将 TCP 首部中的 ECE 设为 1;

l  URG:该位设为 1,表示包中有需要紧急处理的数据,对于需要紧急处理的数据,与后面的紧急指针有关;

l  ACK:该位设为 1,确认应答的字段有效,TCP规定除了最初建立连接时的 SYN 包之外该位必须设为 1;

l  PSH:该位设为 1,表示需要将收到的数据立刻传给上层应用协议,若设为 0,则先将数据进行缓存;

l  RST:该位设为 1,表示 TCP 连接出现异常必须强制断开连接;

l  SYN:用于建立连接,该位设为 1,表示希望建立连接,并在其序列号的字段进行序列号初值设定;

l  FIN:该位设为 1,表示今后不再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位置为 1 的 TCP 段。

l  每个主机又对对方的 FIN 包进行确认应答之后可以断开连接。不过,主机收到 FIN 设置为 1 的 TCP 段之后不必马上回复一个 FIN 包,而是可以等到缓冲区中的所有数据都因为已成功发送而被自动删除之后再发 FIN 包;

l  窗口:窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。

TCP/UDP伪首部的理解

其目的是让UDP两次检查数据是否已经正确到达目的地,具体是那两次呢?我们注意伪首部字段:32位源IP地址、32位目的IP地址、8位协议、16位UDP长度。

由此可知,第一次,通过伪首部的IP地址检验,UDP可以确认该数据报是不是发送给本机IP地址的;第二,通过伪首部的协议字段检验,UDP可以确认IP有没有把不应该传给UDP而应该传给别的高层的数据报传给了UDP。从这一点上,伪首部的作用其实很大。

SYN 超时了怎么处理?

也就是 client 发送 SYN 至 server 然后就挂了,此时 server 发送 SYN+ACK 就一直得不到回复,慢慢重试,阶梯性重试, 在 Linux 中就是默认重试 5 次,并且就是阶梯性的重试,间隔就是1s、2s、4s、8s、16s,再第五次发出之后还得等 32s 才能知道这次重试的结果,所以说总共等63s 才能断开连接

SYN Flood 攻击

可以开启 tcp_syncookies,那就用不到 SYN 队列了。

SYN 队列满了之后 TCP 根据自己的 ip、端口、然后对方的 ip、端口,对方 SYN 的序号,时间戳等一波操作生成一个特殊的序号(即 cookie)发回去,如果对方是正常的 client 会把这个序号发回来,然后 server 根据这个序号建连。

或者调整 tcp_synack_retries 减少重试的次数,设置 tcp_max_syn_backlog 增加 SYN 队列数,设置 tcp_abort_on_overflow SYN 队列满了直接拒绝连接。

IP 、UDP和TCP,每一种格式的首部中均包含一个检验和。对每种分组,说明检验和包括IP数据报中的哪些部分,以及该检验和是强制的还是可选的

除了UDP的检验和,其他都是必需的。IP检验和只覆盖了IP首部,而其他字段都紧接着IP首部开始。

为什么所有Internet协议收到有检验和错的分组都仅作丢弃处理?

源IP地址、源端口号或者协议字段可能被破坏了。

为什么会发生 TCP 粘包、拆包?

l  要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。

l  待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。

l  要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。

l  接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

粘包、拆包解决办法

由于 TCP 本身是面向字节流的,无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:

l  消息定长:发送端将每个数据包封装为固定长度(不够的可以通过补 0 填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

l  设置消息边界:服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如 FTP 协议。

l  将消息分为消息头和消息体:消息头中包含表示消息总长度(或者消息体长度)的字段。

l  更复杂的应用层协议比如 Netty 中实现的一些协议都对粘包、拆包做了很好的处理。

TCP 提供了一种字节流服务,而收发双方都不保持记录的边界。应用程序如何提供它们自己的记录标识?

很多Internet应用使用一个回车和换行来标记每个应用记录的结束。这是NVT ASCII采用的编码。另外一种技术是在每个记录之前加上一个记录的字节计数,DNS和Sun RPC采用了这种技术。

为什么在 TCP 首部的开始便是源和目的的端口号?

一个ICMP差错报文必须至少返回引起差错的IP数据报中除了IP首部的前8个字节。当TCP收到一个ICMP差错报文时,它需要检查两个端口号以决定差错对应于哪个连接。因此,端口号必须包含在TCP首部的前8个字节里。

为什么TCP 首部有一个首部长度字段而UDP首部中却没有?

TCP首部的最后有一些选项,但UDP首部中没有选项。

SYN 初始值 ISN 规律

当一端为建立连接而发送它的S Y N时,它为连接选择一个初始序号。ISN随时间而变化,因此每个连接都将具有不同的I S N。RFC 793 指出ISN可看作是一个 32 比特的计数器,每4 ms加1。

这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它作错误的解释。所以 ISN 变成一个递增值,真实的实现还需要加一些随机值在里面,防止被不法份子猜到 ISN。

全双工的体现

既然一个T C P连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向连接。当一端收到一个FIN,它必须通知应用层另一端几经终止了那个方向的数据传送。

发送FIN通常是应用层进行关闭的结果。收到一个FIN只意味着在这一方向上没有数据流动。一个TCP连接在收到一个FIN后仍能发送数据。而这对利用半关闭的应用来说是可能的,尽管在实际应用中只有很少的T C P应用程序这样做。

MSS 确定(商议)的?

它并不是任何条件下都可协商。当建立一个连接时,每一方都有用于通告它期望接收的MSS选项(MSS选项只能出现在SYN报文段中)。如果一方不接收来自另一方的MSS值,则MSS就定为默认值536字节(这个默认值允许20字节的I P首部和20字节的TCP首部以适合576字节I P数据报)。

TCP 最大段大小

最大段大小是指 TCP 协议所允许的从对方接收到的最大报文段,因此这也是通信对方在发送数据时能够使用的最大报文段。根据 [RFCO879],最大段大小只记录 TCP 数据的字节数而不包括其他相关的 TCP 与 IP 头部。

当建立一条 TCP 连接时,通信的每一方都要在 SYN 报文段的 MSS 选项中说明自已允许的最大段大小。这 16 位的选项能够说明最大段大小的数值。在没有事先指明的情况下,最大段大小的默认数值为 536 字节。

任何主机都应该能够处理至少 576 字节的 IPv4 数据报。如果接照最小的 IPv4 与 TCP 头部计算, TCP 协议要求在每次发送时的最大段大小为 536 字节,这样就正好能够组成一个 576 (20+20+536=576)字节的 IPv4 数据报。最大段大小的数值为 1460。 这是 IPv4 协议中的典型值,因此 IPv4 数据报的大小也相应增加 40 个字节(总共 1500 字节,以太网中最大传输单元与互联网路径最大传输单元的典型数值): 20 字节的 TCP 头部加 20 字节的 IP 头部。

当使用 IPv6 协议时,最大段大小通常为 1440 字节。由于 IPv6 的头部比 IPv4 多 20 个字节,因此最大段大小的数值相 应减少 20 字节。在 [RFC2675] 中 65535 是一个特殊数值,与 IPv6 超长数据报一起用来指定一个表示无限大的有效最大段大小值。在这种情况下,发送方的最大段大小等于路径 MTU 的数值减去 60 字节(40 字节用于 IPv6 头部, 20 字节用于 TCP 头部)。值得注意的是,最大段大小并不是 TCP 通信双方的协商结果,而是一个限定的数值。当通信的一方将自已的最大段大小选项发送给对方时,它已表明自已不愿意在整个连接过程中接收任何大于该尺寸的报文段。

time_wait 的坏处

当 TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在Time_wait状态停留的时间为2倍的M S L。这样可让TCP再次发送最后的A C K以防这个ACK丢失(另一端超时并重发最后的F I N)。

这种2 MSL等待的另一个结果是这个TCP连接在2 MSL等待期间,定义这个连接的插口(客户的I P地址和端口号,服务器的I P地址和端口号)不能再被使用。这个连接只能在2 MSL结束后才能再被使用。服务器通常执行被动关闭,不会进入TIME_WAIT状态。这暗示如果我们终止一个客户程序,并立即重新启动这个客户程序,则这个新客户程序将不能重用相同的本地端口。

这不会带来什么问题,因为客户使用本地端口,而并不关心这个端口号是什么。然而,对于服务器,情况就有所不同,因为服务器使用熟知端口。如果我们终止一个已经建立连接的服务器程序,并试图立即重新启动这个服务器程序,服务器程序将不能把它的这个熟知端口赋值给它的端点,因为那个端口是处于2 MSL连接的一部分。在重新启动服务器程序前,它需要在1 ~ 4分钟。

RST 复位什么时候发出

一般说来,无论何时一个报文段发往基准的连接( referenced connection)出现错误, TCP都会发出一个复位报文段(这里提到的“基准的连接”是指由目的 IP 地址和目的端口号以及源 IP 地址和源端口号指明的连接。这就是为什么RFC 793称之为插口)。


产生复位的一种常见情况是当连接请求到达时,目的端口没有进程正在听。


发送一个复位报文段而不是F I N来中途释放一个连接。有时称这为异常释放(abortive release)。异常终止一个连接对应用程序来说有两个优点:

(1)丢弃任何待发数据并立即发送复位报文段;

(2)R S T的接收方会区分另一端执行的是异常关闭还是正常关闭。应用程序使用的A P I必须提供产生异常关闭而不是正常关闭的手段,R S T报文段中包含一个序号和确认序号。需要注意的是R S T报文段不会导致另一端产生任何响应,另一端根本不进行确认。收到R S T的一方将终止该连接,并通知应用层连接复位。

TCP 选项有什么

1.   窗口扩大选项使 TCP 的窗口定义从16 bit增加为32 bit。这并不是通过修改TCP首部来实现的, T C P首部仍然使用16 bit ,而是通过定义一个选项实现对16 bit 的扩大操作 来完成的。于是T C P在内部将实际的窗口大小维持为32 bit的值。

2.   时间戳选项使发送方在每个报文段中放置一个时间戳值。接收方在确认中返回这个数值,从而允许发送方为每一个收到的A C K计算RT T(我们必须说“每一个收到的A C K”而不是“每一个报文段”,是因为T C P通常用一个A C K来确认多个报文段)。我们提到过目前许多实现为每一个窗口只计算一个RT T,对于包含8个报文段的窗口而言这是正确的。然而,较大的窗口大小则需要进行更好的RT T计算。

3.   最大报文传输段(Maximum Segment Size ---MSS)

4.   选择确认选项(Selective Acknowledgements --SACK)

半打开连接和半关闭连接的区别是什么?

在一个半关闭的连接上,一个端点已经发送了一个FIN,正等待另一端的数据或者一个FIN。

一个半打开的连接是当一个端点崩溃了,而另一端还不知道的情况。

未连接队列:在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到 SYN包,并向客户发出确认,正在等待客户的确认包。这些条目所标识的连接在服务器处于Syn_RCVD状态,当服务器收到客户的确认包时,删除该条目, 服务器进入ESTABLISHED状态。

ACK延迟确认机制

接收方在收到数据后,并不会立即回复ACK,而是延迟一定时间。一般ACK延迟发送时间为200ms,但是这个200ms并非收到数据后需要延迟的时间。系统有一个固定的定时器每隔200ms会来检查是否需要发送ACK包。这样做有2个目的:

  1. 这样做的目的是ACK是可以合并的,也就是指如果连续收到两个TCP包,并不一定需要ACK两次,只要回复最终的ACK就可以了,可以降低网络流量。
  2. 如果接收方有数据要发送,那么就会在发送数据的TCP数据包里,带上ACK信息。这样做,可以避免大量的ACK以一个单独的TCP包发送,减少了网络流量。

SACK(Selective Acknowledgment)

SACK是一个TCP的选项,来允许TCP单独确认非连续的片段,用于告知真正丢失的包,只重传丢失的片段。要使用SACK,2个设备必须同时支持SACK才可以,建立连接的时候需要使用SACK Permitted的option,如果允许,后续的传输过程中TCP segment中的可以携带SACK option,这个option内容包含一系列的非连续的没有确认的数据的seq range


TCP收到乱序数据后会将其放到乱序序列中,然后发送重复ACK给对端。对端如果收到多个重复的ACK,认为发生丢包,TCP会重传最后确认的包开始的后续包。这样原先已经正确传输的包,可能会重复发送,降低了TCP性能。为改善这种情况,发展出SACK技术,使用SACK选项可以告知发包方收到了哪些数据,发包方收到这些信息后就会知道哪些数据丢失,然后立即重传丢失的部分。

SACK 重传

1.   未启用 SACK 时,TCP 重复 ACK 定义为收到连续相同的 ACK seq。[RFC5681]

2.   启用 SACK 时,携带 SACK 的 ACK 也被认为重复 ACK。[RFC6675]

SACK option格式 Kind 5 Length 剩下的都是没有确认的segment的range了 比如说segment 501-600 没有被确认,那么Left Edge of 1st Block = 501,Right Edge of 1st Block = 600,TCP的选项不能超过40个字节,所以边界不能超过4组。

Nagle算法

在局域网上,小分组(被称为微小分组)通常不会引起麻烦,因为局域网一般不会出现拥塞。但在广域网上,这些小分组则会增加拥塞出现的可能。

该算法要求一个TCP连接上最多只能有一个未被确认的未完成的小分组,在该分组的确认到达之前不能发送其他的小分组。相反, TCP收集这些少量的分组,并在确认到来时以一个分组的方式发出去。该算法的优越之处在于它是自适应的:确认到达得越快,数据也就发送得越快。而在希望减少微小分组数目的低速广域网上,则会发送更少的分组。插口API用户可以使用 TCP_NODELAY 选项来关闭Nagle算法。

Karn算法

当一个超时和重传发生时,在重传数据的确认最后到达之前,不能更新RT T估计器,因为我们并不知道A C K对应哪次传输(也许第一次传输被延迟而并没有被丢弃,也有可能第一次传输的A C K被延迟)并且,由于数据被重传, RTO已经得到了一个指数退避,我们在下一次传输时使用这个退避后的RTO。对一个没有被重传的报文段而言,除非收到了一个确认,否则不要计算新的RTO。

Karn 算法在分组丢失时可以不测量 RTT 就能解决重传的二义性问题。

快重传:3次相同的ack后会进入慢启动吗?

No,在这种情况下没有执行慢启动的原因是由于收到重复的ACK不仅仅告诉我们一个分组丢失了。由于接收方只有在收到另一个报文段时才会产生重复的ACK,而该报文段已经离开了网络并进入了接收方的缓存。也就是说,在收发两端之间仍然有流动的数据,而我们不想执行慢启动来突然减少数据流。

流程:

1) 当收到第3个重复的ACK 时,将ssthresh 设置为当前拥塞窗口 cwnd的一半。重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小。

2) 每次收到另一个重复的ACK时, cwnd增加1个报文段大小并发送1个分组(如果新的 cwnd允许发送)。

3) 当下一个确认新数据的ACK到达时,设置cwnd为ssthresh(在第1步中设置的值)。这个 ACK应该是在进行重传后的一个往返时间内对步骤1中重传的确认。另外,这个ACK也应该是对丢失的分组和收到的第1个重复的A C K之间的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。

保活计时器的作用?

TCP的Keepalive,目的在于看看对方有没有发生异常,如果有异常就及时关闭连接。当传输双方不主动关闭连接时,就算双方没有交换任何数据,连接也是一直有效的。保活定时器每隔一段时间会超时,超时后会检查连接是否空闲太久了,如果空闲的时间超过了设置时间,就会发送探测报文。然后通过对端是否响应、响应是否符合预期,来判断对端是否正常,如果不正常,就主动关闭连接,而不用等待HTTP层的关闭了。

SYN Cookies

在最常见的SYN Flood攻击中,攻击者在短时间内发送大量的TCP SYN包给受害者。受害者(服务器)为每个TCP SYN包分配一个特定的数据区,只要这些SYN包具有不同的源地址(攻击者很容易伪造)。这将给TCP服务器造成很大的系统负担,最终导致系统不能正常工作。

SYN Cookie是对TCP服务器端的三次握手做一些修改,专门用来防范SYN Flood攻击的一种手段。它的原理是,在TCP服务器接收到TCP SYN包并返回TCP SYN + ACK包时,不分配一个专门的数据区,而是根据这个SYN包计算出一个cookie值。这个cookie作为将要返回的SYN ACK包的初始序列号。当客户端返回一个ACK包时,根据包头信息计算cookie,与返回的确认序列号(初始序列号 + 1)进行对比,如果相同,则是一个正常连接,然后,分配资源,建立连接。

cookie的计算:服务器收到一个SYN包,计算一个消息摘要mac。

CLOSE_WAIT过多的解决方法

系统产生大量“Too many open files”

原因分析:在服务器与客户端通信过程中,因服务器发生了 socket 未关导致的 closed_wait 发生,致使监听 port 打开的句柄数到了 1024 个,且均处于 close_wait 的状态,最终造成配置的port被占满出现 “Too many open files”,无法再进行通信。

解决办法:有两种措施可行

一、解决:
原因是因为调用 ServerSocket 类的 accept() 方法和 Socket 输入流的 read() 方法时会引起线程阻塞,所以应该用 setSoTimeout() 方法设置超时(缺省的设置是0,即超时永远不会发生);超时的判断是累计式的,一次设置后,每次调用引起的阻塞时间都从该值中扣除,直至另一次超时设置或有超时异常抛出。比如,某种服务需要三次调用 read(),超时设置为1分钟,那么如果某次服务三次 read()调用的总时间超过 1 分钟就会有异常抛出,如果要在同一个 Socket 上反复进行这种服务,就要在每次服务之前设置一次超时。

二、规避:
调整系统参数,包括句柄相关参数和TCP/IP的参数;

1.   open files 参数值 加大

2.   当客户端因为某种原因先于服务端发出了 FIN 信号,就会导致服务端被动关闭,若服务端不主动关闭 socket 发 FIN 给 Client,此时服务端 Socket 会处于 CLOSE_WAIT 状态(而不是 LAST_ACK 状态)。通常来说,一个 CLOSE_WAIT 会维持至少 2 个小时的时间(系统默认超时时间的是 7200 秒,也就是 2 小时)。如果服务端程序因某个原因导致系统造成一堆 CLOSE_WAIT 消耗资源,那么通常是等不到释放那一刻,系统就已崩溃。因此,解决这个问题的方法还可以通过修改 TCP/IP 的参数来缩短这个时间,于是修改 tcp_keepalive_*系列参数: /proc/sys/net/ipv4/tcp_keepalive_time , /proc/sys/net/ipv4/tcp_keepalive_probes ,/proc/sys/net/ipv4/tcp_keepalive_intvl

短连接,并行连接,持久连接与长连接

短连接

短连接多用于操作频繁,点对点的通讯,而且连接数不能太多的情况。每个 TCP 连接的建立都需要三次握手,每个 TCP 连接的断开要四次挥手。适用于并发量大,但是每个用户又不需频繁操作的情况。

但是在用户需要频繁操作的业务场景下(如新用户注册,网购提交订单等),频繁的使用短连接则会使性能时延产生叠加。

用户登录这些不频繁的操作可以考虑用短连接。

并行连接

针对短连接,人们想出了优化的办法,连接多条,形成并行连接。

并行连接允许客户端打开多条连接,并行地执行多个事务,每个事务都有自己的TCP连接。这样可以克服单条连接的空载时间和带宽限制,时延可以重叠起来,而且如果单条连接没有充分利用客户端的网络带宽,可以将未用带宽分配来装载其他对象。

在PC时代,利用并行连接来充分利用现代浏览器的多线程并发下载能力的场景非常广泛。

但是并行连接也会产生一定的问题,首先并行连接不一定更快,因为带宽资源有限,每个连接都会去竞争这有限的带宽,这样带来的性能提升就很小,甚至没什么提升。

一般机器上面并行连接的条数 4 - 6 条

持久连接

HTTP1.0 版本以后,允许 HTTP 设备在事务处理结束之后将 TCP 连接保持在打开状态,以便为未来的 HTTP 请求重用现存的连接。在事务处理结束之后仍然保持在打开状态的 TCP 连接被称为持久连接。

持久连接的时间参数,通常由服务器设定,比如 nginx 的 keepalivetimeout,keepalive timout 时间值意味着:一个 http 产生的 tcp 连接在传送完最后一个响应后,还需要 hold 住 keepalive_timeout 秒后,才开始关闭这个连接;

在 HTTP 1.1 中 所有的连接默认都是持续连接,除非特殊声明不支持。HTTP 持久连接不使用独立的 keepalive 信息,而是仅仅允许多个请求使用单个连接。然而,Apache 2.0 httpd 的默认连接过期时间是仅仅 15 秒,对于 Apache 2.2 只有 5 秒。短的过期时间的优点是能够快速的传输多个 web 页组件,而不会绑定多个服务器进程或线程太长时间。

持久连接与并行连接相比,带来的优势如下:
  1. 避免了每个事务都会打开/关闭一条新的连接,造成时间和带宽的耗费;
  2. 避免了 TCP 慢启动特性的存在导致的每条新连接的性能降低;
  3. 可打开的并行连接数量实际上是有限的,持久连接则可以减少建立的连接的数量;

长连接

长连接与持久连接本质上非常的相似,持久连接侧重于 HTTP 应用层,特指一次请求结束之后,服务器会在自己设置的 keepalivetimeout 时间到期后才关闭已经建立的连接。长连接则是 client 方与 server 方先建立连接,连接建立后不断开,然后再进行报文发送和接收,直到有一方主动关闭连接为止。

长连接的适用场景也非常的广泛:
  1. 监控系统:后台硬件热插拔、LED、温度、电压发生变化等;
  2. IM 应用:收发消息的操作;
  3. 即时报价系统:例如股市行情 push 等;
  4. 推送服务:各种 App 内置的 push 提醒服务;

TIME_WAIT快速回收与重用

https://blog.csdn.net/dog250/article/details/13760985

Linux实现了一个TIME_WAIT状态快速回收的机制,即无需等待两倍的MSL这么久的时间,而是等待一个Retrans时间即释放,也就是等待一个重传时间(一般超级短,以至于你都来不及能在netstat -ant中看到TIME_WAIT状态)随即释放。释放了之后,一个连接的tuple元素信息就都没有了。在快速释放掉TIME_WAIT连接之后,peer依然保留着。丢失的仅仅是端口信息。不过有了peer的IP地址信息以及TCP最后一次触摸它的时间戳就足够了,TCP规范给出一个优化,即一个新的连接除了同时触犯了以下几点,其它的均可以快速接入,即使它本应该处在TIME_WAIT状态(但是被即快速回收了):
1.来自同一台机器的TCP连接携带时间戳;
2.之前同一台peer机器(仅仅识别IP地址,因为连接被快速释放了,没了端口信息)的某个TCP数据在MSL秒之内到过本机;

3.新连接的时间戳小于peer机器上次TCP到来时的时间戳,且差值大于重放窗口戳。

建议:如果前端部署了三/四层NAT设备,尽量关闭快速回收,以免发生NAT背后真实机器由于时间戳混乱导致的SYN拒绝问题。

TW重用 有相关的规范,即:如果能保证以下任意一点,一个TW状态的四元组(即一个socket连接)可以重新被新到来的SYN连接使用:


1.初始序列号比TW老连接的末序列号大
2.如果使能了时间戳,那么新到来的连接的时间戳比老连接的时间戳大

BBR 算法

BBR 全称 bottleneck bandwidth and round-trip propagation time。基于包丢失检测的 Reno、NewReno 或者 cubic 为代表,其主要问题有 Buffer bloat 和长肥管道两种。和这些算法不同,bbr 算法会时间窗口内的最大带宽 max_bw 和最小 RTT min_rtt,并以此计算发送速率和拥塞窗口。

当没有足够的数据来填满管道时,RTprop 决定了流的行为;当有足够的数据填满时,那就变成了 BtlBw 来决定。这两条约束交汇在点 inflight =BtlBw*RTprop,也就是管道的 BDP(带宽与时延的乘积)。当管道被填满时,那些超过的部分(inflight-BDP)就会在瓶颈链路中制造了一个队列,从而导致了 RTT 的增大。当数据继续增加直到填满了缓存时,多余的报文就会被丢弃了。拥塞就是发生在 BDP 点的右边,而拥塞控制算法就是来控制流的平均工作点离 BDP 点有多远。

TCP 参数

  • Backlog参数:表示未连接队列的最大容纳数目。
  • SYN-ACK 重传次数: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超 过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同。
  • 半连接存活时间:是指半连接队列的条目存活的最长时间,也即服务从收到SYN包到确认这个报文无效的最长时间,该时间值是所有重传请求包的最长等待时间总和。有时我们也称半连接存活时间为Timeout时间、SYN_RECV存活时间。
  • 开启SYN Cookies : net.ipv4.tcp_syncookies = 1
  • 开启timewait重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0 :net.ipv4.tcp_tw_reuse = 1
  • 开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭;net.ipv4.tcp_tw_recycle = 1
  • tw 时间 : net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
  • 当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为20分钟(20*60s)
    net.ipv4.tcp_keepalive_time = 1200

tcp 异常

试图与一个不存在的端口建立连接

这符合触发发送RST分节的条件,目的为某端口的SYN分节到达,而端口没有监听,那么内核会立即响应一个RST,表示出错。客户端TCP收到这个RST之后则放弃这次连接的建立,并且返回给应用程序一个错误。

正如上面所说的,建立连接的过程对应用程序来说是不可见的,这是操作系统帮我们来完成的,所以即使进程没有启动,也可以响应客户端。

试图与一个不存在的主机上面的某端口建立连接

这也是一种比较常见的情况,当某台服务器主机宕机了,而客户端并不知道,仍然尝试去与其建立连接。根据上面的经验,这次主机已经处于未启动状态,操作系统也帮不上忙了,那么也就是连RST也不能响应给客户端,此时服务器端是一种完全没有响应的状态。

那么此时客户端的TCP会怎么办呢?据书上介绍,如果客户端TCP没有得到任何响应,那么等待6s之后再发一个SYN,若无响应则等待24s再发一个,若总共等待了75s后仍未收到响应就会返回ETIMEDOUT错误。这是TCP建立连接自己的一个保护机制,但是我们要等待75s才能知道这个连接无法建立,对于我们所有服务来说都太长了。更好的做法是在代码中给connect设置一个超时时间,使它变成我们可控的,让等待时间在毫秒级还是可以接收的。

Server进程被阻塞

由于某些情况,服务器端进程无法响应任何请求,比如所在主机的硬盘满了,导致进程处于完全阻塞,通常我们测试时会用gdb模拟这种情况。上面提到过,建立连接的过程对应用程序是不可见的,那么,这时连接可以正常建立。

当然,客户端进程也可以通过这个连接给服务器端发送请求,服务器端TCP会应答ACK表示已经收到这个分节(这里的收到指的是数据已经在内核的缓冲区里准备好,由于进程被阻塞,无法将数据从内核的缓冲区复制到应用程序的缓冲区),但永远不会返回结果。

我们杀死server

这是线上最常见的操作,当一个模块上线时,OP同学总是会先把旧的进程杀死,然后再启动新的进程。那么在这个过程中TCP连接发生了什么呢。在进程正常退出时会自动调用close函数来关闭它所打开的文件描述符,这相当于服务器端来主动关闭连接——会发送一个FIN分节给客户端TCP;客户端要做的就是配合对端关闭连接,TCP会自动响应一个ACK,然后再由客户端应用程序调用close函数,也就是我们上面所描述的关闭连接的4次挥手过程。接下来,客户端还需要定时去重连,以便当服务器端进程重新启动好时客户端能够继续与之通信。

当然,我们要保证客户端随时都可以响应服务器端的断开连接请求,就必须不能让客户端进程再任何时刻阻塞在任何其他的输入上面。比如,书上给的例子是客户端进程会阻塞在标准输入上面,这时如果服务器端主动断开连接,显然客户端不能立刻响应,因为它还在识图从标准输入读一段文本……当然这在实际中很少遇到,如果有多输入源这种情况的话开通通常会用类似select功能的函数来处理,可以同时监控多个输入源是否准备就绪,可以避免上述所说的不能立即响应对端关闭连接的情况。

Server进程所在的主机关机

实际上这种情况不会带来什么更坏的后果。在系统关闭时,init进程会给所有进程发送SIGTERM信号,等待一段时间(5~20秒),然后再给所有仍在运行的进程发送SIGKILL信号。当服务器进程死掉时,会关闭所有文件描述符。带来的影响和上面杀死server相同。

Server进程所在的主机宕机

这是我们线上另一种比较常见的状况。即使宕机是一个小概率事件,线上几千台服务器动不动一两台挂掉也是常有的事。主机崩溃不会像关机那样会预先杀死上面的进程,而是突然性的。那么此时我们的客户端准备给服务器端发送一个请求,它由write写入内核,由TCP作为一个分节发出,随后客户阻塞于read的调用(等待接收结果)。对端TCP显然不会响应这个分节,因为主机已经挂掉,于是客户端TCP持续重传分节,试图从服务器上接收一个ACK,然而服务器始终不能应答,重传数次之后,大约4~10分钟才停止,之后返回一个ETIMEDOUT错误。

这样尽管最后还是知道对方不可达,但是很多时候我们希望比等待4~10分钟更快的知道这个结果。可以为read设置一个超时时间,就得到了一个较好的解决方法。但是这样还是需要等待一个超时时间,事实上TCP为我们提供了更好的方法,用SO_KEEPALIVE的套接字选项——相当于心跳包,每隔一段时间给对方发送一个心跳包,当对方没有响应时会一更短的时间间隔发送,一段时间后仍然无响应的话就断开这个连接。

服务器进程所在的主机宕机后重启

在客户端发出请求前,服务器端主机经历了宕机——重启的过程。当客户端TCP把分节发送到服务器端所在的主机,服务器端所在主机的TCP丢失了崩溃前所有连接信息,即TCP收到了一个根本不存在连接上的分节,所以会响应一个RST分节。如果开发的代码足够健壮的话会试图重新建立连接,或者把这个请求转发给其他服务器。

当TCP连接的进程在忘记关闭Socket而退出、程序崩溃、或非正常方式结束进程的情况下(Windows客户端),会导致TCP连接的对端进程产生“104: Connection reset by peer”(Linux下)或“10054: An existing connection was forcibly closed by the remote host”(Windows下)错误

当TCP连接的进程机器发生死机、系统突然重启、网线松动或网络不通等情况下,连接的对端进程可能检测不到任何异常,并最后等待“超时”才断开TCP连接

当TCP连接的进程正常关闭Socket时,对端进程在检查到TCP关闭事件之前仍然向TCP发送消息,则在Send消息时会产生“32: Broken pipe”(Linux下)或“10053: An established connection was aborted by the software in your host machine”(Windows下)错误

当TCP连接的对端进程已经关闭了Socket的情况下,本端进程再发送数据时,第一包可以发送成功(但会导致对端发送一个RST包过来):

之后如果再继续发送数据会失败,错误码为“10053: An established connection was aborted by the software in your host machine”(Windows下)或“32: Broken pipe,同时收到SIGPIPE信号”(Linux下)错误;

之后如果接收数据,则Windows下会报10053的错误,而Linux下则收到正常关闭消息

TCP连接的本端接收缓冲区中还有未接收数据的情况下close了Socket,则本端TCP会向对端发送RST包,而不是正常的FIN包,这就会导致对端进程提前(RST包比正常数据包先被收到)收到“10054: An existing connection was forcibly closed by the remote host”(Windows下)或“104: Connection reset by peer”(Linux下)错误

PSH和URG

PSH 标志的作用就在这里,当PSH 被置为1 时, 会被立即推出,不会等待其他数据进入缓冲区。当接受端收到 PSH 被置 1 的数据包时,立即将该分段上交到对应的应用程序。即有如下作用:

  • 通知发送方立即发送数据。
  • 接收方立即将数据推送到应用程序。
这个标志是在TCP层清空发送缓存,并将报文段交给IP层的时候设置的(相当于表示一次TCP层的发送操作)。(还有一点需要注意:大多数的API没有向应用层提供通知TCP层设置PUSH标志的方法,据说是因为很多实现程序认为PUSH标志已经过时,而一个好的TCP实现能够自行决定何时设置这个标志。

URG 标志用于通知接收方,数据段内某些数据是需要紧急处理的,应该被优先考虑。接收方收到 URH标志有效的数据报时,回去检测 TCP 报头中的16 位字段 紧急指针。 该字段指示从第一个字节计数的段中的数据时多少紧急处理的。

端口号

标准的端口号由 Internet 号码分配机构(IANA)分配。这组数字被划分为特定范围,包括

熟知端口号(0 - 1023)、注册端口号(1024 - 49151)和动态/私有端口号(49152 - 65535)。

如果我们测试这些标准服务和其他 TCP/IP 服务(Telnet、 FTP、 SMTP等) 使用的端口号,会发现它们大多数是奇数。这是有历史原困的,这些端口号从 NCP 端口号派生而来(NCP 是网络控制协议,在 TCP 之前作为 ARPANET 的传输层协议)。NCP 虽然简单,但不是全双工的,困此每个应用需要两个连接,并为每个应用保留奇偶成对的端口号。当 TCP 和 UDP 成为标准的传输层协议时,每个应用只需要一个端口号,因此来自 NCP 的奇数端口号被使用。


QQ,微信

早期的时候,QQ 还是主要使用 TCP 协议,而后来就转向了采用 UDP 的方式来保持在线,TCP 的方式来上传和下载数据。现在,UDP 是 QQ 的默认工作方式,表现良好。相信这个也被沿用到了微信上。

简单的考证:登录 PC 版 QQ,关闭多余的 QQ 窗口只留下主窗口,并将其最小化。几分钟过后,查看系统网络连接,会发现 QQ 进程已不保有任何 TCP 连接,但有 UDP 网络活动。这时在发送聊天信息,或者打开其他窗口和功能,将发现 QQ 进程会启用 TCP 连接。

登陆成功之后,QQ 有一个 TCP 连接来保持在线状态。这个 TCP 连接的远程端口一般是80,采用 UDP 方式登陆的时候,端口是 8000。

QQ 客户端之间的消息传送采用了 UDP,因为国内的网络环境非常复杂,而且很多用户采用的方式是通过代理服务器共享一条线路上网的方式,在这些复杂的情况下,客户端之间能彼此建立起来 TCP 连接的概率较小,严重影响传送信息的效率。而 UDP 包能够穿透大部分的代理服务器,因此 QQ 选择了 UDP 作为客户之间的主要通信协议。

腾讯采用了上层协议来保证可靠传输:如果客户端使用 UDP 协议发出消息后,服务器收到该包,需要使用 UDP 协议发回一个应答包。如此来保证消息可以无遗漏传输。之所以会发生在客户端明明看到“消息发送失败”但对方又收到了这个消息的情况,就是因为客户端发出的消息服务器已经收到并转发成功,但客户端由于网络原因没有收到服务器的应答包引起的。


参考 :


篇四:求浆得酒,能有好运,也该为大家做些什么

牛油:@方圆想当图灵


一、写在前头

有一肚子的话想说出来,到现在又不知该如何表达了,如果屏幕能传递感情,就好了

我也经历了秋招和春招,把积累的一些心得和知识分享出来,趁着春招还没结束,应该还能给大家一些帮助,在牛客上潜水索取了这么久,也是时候回馈了,这篇帖子开始写于河工大图书馆,也用来纪念为期不多的大学时光

特此感谢@大明宫巴菲特 (之前不是瑶瑶公主吗?哈哈哈),大佬整理的面经我看的七七八八,给我了不少帮助,非常感谢了,大家可以关注一下!!!

下面的一些经验不一定全对也不一定全部有用,也仅仅是把我知道的一些技巧性的东西分享出来罢了,如果能对大家产生一点点帮助,都是我无比荣幸的事情


二、春招历程(截至2021年3月31日)

1.  字节跳动 大力教育后端开发 北京:2月17日牛客内推投递
2月23日一面 被面试官发现非科班,基本上问的全是计算机网络和操作系统相关 

2.  京东物流 Java开发工程师 北京:2月17日官网内推码投递
3月3日 一面 一个多小时,口干舌燥也畅快琳琳 
3月4日 二面 四十多分钟,问的比较发散,不局限于八股文,更关注具体场景业务分析 
3月10日 HR面 大概只有六七分钟
3月12日 查询状态变为HR面完成,Offer灯亮起
3月15日 收到正式Offer

3.  美团 支付部门后端开发 北京:3月4日牛客内推投递
3月13日 笔试,2AC,剩下3道题每题只骗了18%
3月19日 一面,体验非常好的一次面试,面试官很和气,
3月22日 二面,时间蛮长的,一个多小时,
3月31日 三面(HR面),十七分钟,薛定谔的美团面试结果

4.  笔试过没回应的:携程,VIVO,跟谁学
投了没有回应的:华为,OPPO,陌陌,作业帮,小米,搜狗
给了笔试没笔的:顺丰科技,猿辅导,好未来,滴滴,便利蜂


三、整理出来的面经

3.1 Java相关

3.1.1 ArrayList

  • 使用场景:ArrayList的底层是一个数组,适合快速匹配,不适合频繁的增删
  • 允许add null 值,会自动扩容,其中size(),isEmpty(),get(),add()方法的复杂度为O(1)
  • 使用Collentions.synchronizedList(),实现线程安全或者Vector也可(Vector在方法上加的synchronized锁)
  • 调用无参构造函数的时候,在JDK1.8默认为空数组(DEFAULT_EMPTY_ELEMENTDATA = {}),数字大小为10是我们第一次调用add方法是进行扩容的数组大小
    若我们在执行构造函数传入的数组大小为0时,它使用的不是DEFAULT_EMPTY_ELEMENTDATA,而是另一个空数组EMPTY_ELEMENTDATA = {}(这个知识点面试没说过)
  • add方法的过程
    先确定数组大小是否足够,如果我们创建ArrayList的时候指定了大小,那么则以给定的大小创建一个数组,否则默认大小为10;容量够大的情况,直接赋值;如果容量不够大,则进行扩容方法grow(),扩容的大小为原来大小的1.5倍(newCapicity = oldCapicity + oldCapicity >> 1,其中>>1,右移一位除以2),如果扩容后的大小还不够的话,则会将数组大小直接设置为我们需要的大小,扩容的最大值为Integer.MAX_VALUE,之后会调用Arrays.copyOf()方法将原数组中的数组复制过来
    其中Arrays.copyOf()底层调用的是System.arrayCopy(),大家可以去简单了解下
  • remove方法
    该方将被删除位置后的元素向前复制,底层调用的也是System.arrayCopy()方法,复制完成后,将数组元素的最后一个设置为null(因为向前复制一个位置,所以最后位置的元素是重复的),这样就解决了复制重复元素的问题
  • 迭代器和增强for是一样的(这是一个Java语法糖,我后边还会再写语法糖相关的),过程中会判断modCount的值是否符合循环过程中的期望,如果不符合的话则会抛出并发修改异常,比较常见的情况就是在增强for中进行删除操作

3.1.2 LinkedList

  • 使用场景:适合增删,不适合快速匹配
  • 底层数据结构是双向链表,每一个节点为Node,有pre和next属性
  • 提供从头添加和从尾添加的方法,节点删除也提供了从头删除和从尾删除的方法

3.1.3 HashMap

  • 底层数据结构:数组 + 链表 + 红黑树
  • 允许put null 值,HashMap在调用hash算法时,如果key为null,那么hash值为0,这一点区别于HashTable和ConcurrentHashmap
    (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  • loadFactor:负载因子默认为0.75,是均衡了时间和空间损耗计算出来的,较高的值会减少空间的开销,扩容减小,数组大小增加速度变慢,但是增加了查找的成本,hash冲突增加,链表变长
  • 如果有很多需要储存到HashMap中的数据,要在一开始把它的容量设置为足够大,防止出现不断扩容
  • 通过Collections.synchronizedMap()来实现线程安全或者使用ConcurrentHashmap
  • 需要记住的字段如下

1

2

3

4

5

6

DEFAULT_INITIAL_CAPICITY = 1 << 4; 默认大小为16

MAXIMUM_CAPACITY = 1 << 30; 最大容量

DEFAULT_LOAD_FACTOR = 0.75f; 默认负载因子

TREEIFY_THRESHOLD = 8; UNTREEIFY_THRESHOLD = 6; 树化和退化为链表的阈值

MIN_TREEIFY_CAPACITY = 64; 链表转化为红黑树时需要的数组大小

threshold 表示扩用的阈值,大小为 数组大小*负载因子

  • put过程
    首先会判断数组有没有进行初始化,没有的话,先执行初始化操作,resize()方法
    (n - 1) & hash用来定位到数组中具体的位置,如果数组中的该位置为空,直接在该位置添加值
    如果数组当前位置有值的话,如果是链表,采用的是尾插发,并且当链表长度大于等于8时,会进行树化操作;如果是红黑树的话,则会调用红黑树的插入值的方法;添加完成后,会判断size是否大于threshold,是否需要扩容,若扩容的话,数组大小为之前的2倍大小,扩容完成后,将原数组上的节点移动到新数组上。
    一篇我觉得写得不错的博客儿:HashMap扩容时的rehash方法中(e.hash & oldCap) == 0算法推导
  • 为什么树化操作的阈值是8?
    链表的查询时间复杂度为O(n),红黑树的查询时间复杂度为O(logn),在数据量不多的时候,使用链表比较快,只有当数据量比较大的时候,才会转化为红黑树,但是红黑树占用的空间大小是链表的2倍,考虑到时间和空间上的损耗,所以要设置边界值(其实链表长度为8的概率很低,在HashMap注释中写了,出现的概率不择千万分之一,红黑树只是为了在极端情况下来保证性能)
  • 为什么还要有一个阈值是6?(去年面试快手的时候问过)
    避免频繁的进行树退化为链表的操作,因为退化也是有开销的,当我们移除一个红黑树上的值的时候,如果只有阈值8的话,那么它会直接退化,我们若再添加一个值,它有可能又需要变为红黑树了,添加阈值6相当于添加了一个缓冲
  • hash算法
    (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),右移16位的操作使得hash值更加分散
  • 为什么数组大小始终为2的n次幂?
    因为在确定某个值在数组位置的下标时,采用的是(数组大小 - 1)位与上hash值,而数组大小减一之后,用2进制表示最后几位都是1,这样每位在位与运算之后,不是0就是1,如果我们hash值是均匀分布的话,那么我们得到的数组下表也是均匀分布的,而如果我们的数组容量不是2的n次幂,那么就没有这个特性了
  • 数组大小为什么默认是16?
    16是一个经验值,2,4,8有些小,会频繁的扩容,32有些大,这样就多占用了空间
  • 为什么JDK1.8采用了尾插法?
    JDK1.7时采用的是头插法,它在扩容后rehash,会使得链表的顺序颠倒,引用关系发生了改变,那么在多线程的情况下,会出现链表成环而死循环的问题,而尾插法就不会有这样的问题,rehash后链表顺序不变,引用关系也不会发生改变,也就不会发生链表成环的问题
  • 红黑树的5个特点
    根节点是黑色;
    所有叶子节点是黑色;
    其他节点是红色或黑色;
    从每个叶子节点到根节点所有路径上不能有两个连续的红色节点;
    从任一节点到每个叶子节点的所有简单路径上包含相同数量的黑色节点
  • HashMap和Hashtable的区别
    实现方式不同:Hashtable:继承了Dictionary类,而HashMap继承的是AbstractMap类
    初始容量不同:HashMap的初始容量为16,Hashtable为11,负载因子都是0.75
    扩容机制不同:HashMap是翻2倍,Hashtable是翻两倍+1

3.1.4 HashSet、TreeMap、TreeSet、LinkedHashMap、LinkedHashSet

  • HashSet底层基于HashMap实现,若想实现线程安全,需要使用Collections.synchronizedSet();
    它在底层组合的HashMap,并没有继承关系,其中Value值使用的都是被声明为Object的PRESENT对象
    private static final Object PRESENT = new Object();
  • TreeMap的底层数据结构是红黑树,会对key进行排序,维护key的大小关系
    我们可以传入比较器Comparator或者让作为key对象的类实现Comparable接口重写compareTo方法
    禁止添加null值
  • LinkedHashMap 本身继承了HashMap,拥有HashMap的所有特性,在此基础上添加了两个新的特性:
    能按照插入的顺序进行访问(不过它仅仅提供了单向访问,即按照插入的顺序从头到尾访问);
    能实现访问最少最先删除的功能(LRU算法)
  • LinkedHashSet 底层基于LinkedHashMap实现

3.1.5 ConcurrentHashMap(JDK1.8)

  • 底层基于CAS + synchronized实现,所有操作都是线程安全的,允许多个线程同时进行put、remove等操作
  • 底层数据结构:数组、链表和红黑树的基础上还添加了一个转移节点,在扩容时应用
  • table数组被volatile修饰
  • 其中有一个比较重要的字段,sizeCtl
    = -1 时代表table正在初始化
    table未初始化时,代表需要初始化的大小
    table初始化完成,表示table的容量,默认为0.75table大小
  • put过程
    key和value都是不能为空的,否则会产生空指针异常,之后会进入自旋(for循环自旋),如果当前数组为空,那么进行初始化操作,初始化完成后,计算出数组的位置,如果该位置没有值,采用CAS操作进行添加;如果当前位置是转移节点,那么会调用helptransfer方法协助扩容;如果当前位置有值,那么用synchronized加锁,锁住该位置,如果是链表的话,采用的是尾插发,如果是红黑树,则采用红黑树新增的方法,新增完成后需要判断是否需要扩容,大于sizeCtl的话,那么执行扩容操作
  • 初始化过程
    在进行初始化操作的时候,会将sizeCtl利用CAS操作设置为-1,CAS成功之后,还会判断数组是否完成初始化,有一个双重检测的过程
    过程:进入自旋,如果sizeCtl < 0, 线程礼让(Thread.yield())等待初始化;否则CAS操作将sizeCtl设置为-1,再次检测是否完成了初始化,若没有则执行初始化操作
  • 在JDK1.7采用的是Segment分段锁,默认并发度为16

3.1.6 CopyOnWriteArrayList

  • 线程安全的,通过锁 + 数组拷贝 + volatile 保证线程安全(底层数组被volatile修饰)
  • 每次进行数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作之后再赋值回去
  • 对数组的操作,一般分为四步
    1 加锁
    2 从原数组中拷贝出新数组
    3 在新数组上进行操作,并把新数组赋值给原引用
    4 解锁
  • 已经加锁了,为什么还需要拷贝新数组?
    因为在原数组上进行修改,没有办法触发volatile的可见性,需要修改内存地址,即将新拷贝的数组赋值给原引用
  • 在进行写操作的时候,是能读的,但是读的数据是老数组的,能保证数组最终的一致性,不能保证实时一致性;
  • 存在内存占用问题,写时复制比较影响性能

3.1.7 String

  • 不变性:类被final修饰,不可被继承;String中保存的是一个字符数组,被final修饰,这样它的内存地址是不能改变的,另外它的访问权限是private,外部无法访问,也没有公开出对其直接修改的API,所以能保持不变
  • equals方法得看一看

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public boolean equals(Object anO

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

互联网校招大礼包 文章被收录于专栏

本专刊由牛客官方团队打造,互联网技术方向主流岗位简历特辑与面试干货。 保姆级简历教程:手把手教你打造网申优秀简历 主流岗位简历模板:学习大牛简历样本,使用模板增加面试邀请 20W字精选面经:牛客精选全网优质面经,专业面试问题、学习路径一网打尽

全部评论

相关推荐

我知道自己这个念头不好,但是真的很羡慕😭大家的父母长辈都能帮到自己吗?
大飞的诡术妖姬:父母都是普通打工人,身体也不好,能供我读到本科毕业很不容易,毕业以后帮衬心里会有罪恶感。虽然可能会错过很多风景,但还是想活的心安理得。
点赞 评论 收藏
分享
白火同学:能。我当初应届沟通了1200,收简历50,面试10左右吧,加油投吧
点赞 评论 收藏
分享
自由水:笑死了,敢这么面试不敢让别人说
点赞 评论 收藏
分享
评论
点赞
2
分享

创作者周榜

更多
牛客网
牛客企业服务