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,它必须通知应用层另一端几经终止了那个方向的数据传送。
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个目的:
- 这样做的目的是ACK是可以合并的,也就是指如果连续收到两个TCP包,并不一定需要ACK两次,只要回复最终的ACK就可以了,可以降低网络流量。
- 如果接收方有数据要发送,那么就会在发送数据的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算法
快重传:3次相同的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
CLOSE_WAIT过多的解决方法
一、解决:
原因是因为调用 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
短连接,并行连接,持久连接与长连接
短连接
并行连接
一般机器上面并行连接的条数 4 - 6 条。
持久连接
- 避免了每个事务都会打开/关闭一条新的连接,造成时间和带宽的耗费;
- 避免了 TCP 慢启动特性的存在导致的每条新连接的性能降低;
- 可打开的并行连接数量实际上是有限的,持久连接则可以减少建立的连接的数量;
长连接
- 监控系统:后台硬件热插拔、LED、温度、电压发生变化等;
- IM 应用:收发消息的操作;
- 即时报价系统:例如股市行情 push 等;
- 推送服务:各种 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秒之内到过本机;
1.初始序列号比TW老连接的末序列号大
2.如果使能了时间戳,那么新到来的连接的时间戳比老连接的时间戳大
BBR 算法
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 异常
试图与一个不存在的端口建立连接
试图与一个不存在的主机上面的某端口建立连接
Server进程被阻塞
我们杀死server
Server进程所在的主机关机
实际上这种情况不会带来什么更坏的后果。在系统关闭时,init进程会给所有进程发送SIGTERM信号,等待一段时间(5~20秒),然后再给所有仍在运行的进程发送SIGKILL信号。当服务器进程死掉时,会关闭所有文件描述符。带来的影响和上面杀死server相同。
Server进程所在的主机宕机
服务器进程所在的主机宕机后重启
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 的数据包时,立即将该分段上交到对应的应用程序。即有如下作用:
- 通知发送方立即发送数据。
- 接收方立即将数据推送到应用程序。
端口号
标准的端口号由 Internet 号码分配机构(IANA)分配。这组数字被划分为特定范围,包括
QQ,微信
参考 :
- https://blog.csdn.net/zhangskd/article/details/44177475
- https://juejin.im/post/6869734247465402382#heading-0
- https://www.jianshu.com/p/d759788ab83f
- https://halfrost.com/advance_tcp/
- https://juejin.im/post/6844903881781018632
篇四:求浆得酒,能有好运,也该为大家做些什么
牛油:@方圆想当图灵
一、写在前头
有一肚子的话想说出来,到现在又不知该如何表达了,如果屏幕能传递感情,就好了
我也经历了秋招和春招,把积累的一些心得和知识分享出来,趁着春招还没结束,应该还能给大家一些帮助,在牛客上潜水索取了这么久,也是时候回馈了,这篇帖子开始写于河工大图书馆,也用来纪念为期不多的大学时光
特此感谢@大明宫巴菲特 (之前不是瑶瑶公主吗?哈哈哈),大佬整理的面经我看的七七八八,给我了不少帮助,非常感谢了,大家可以关注一下!!!
下面的一些经验不一定全对也不一定全部有用,也仅仅是把我知道的一些技巧性的东西分享出来罢了,如果能对大家产生一点点帮助,都是我无比荣幸的事情
二、春招历程(截至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字精选面经:牛客精选全网优质面经,专业面试问题、学习路径一网打尽