Java
⻉壳 Java ⾯试
⼤家好,我是⼩林。
这⼀周明显 感觉开奖的公司越来越多了,有位同学跟我反馈,他拿了⻉壳校招开发岗的 sp 薪资,
跟⼀线⼤⼚的开发岗位薪资⼀样了。
我也根据其他同学爆料的薪资,整理了⻉壳 25 届校招的开发岗位的薪资情况,供⼤家参考参考,
本科和硕⼠的薪资有⼀点差别。

本科:
普通档:20k 〜21k x 16 = 32w 〜33.6w 总包
sp 档:23k x 16 = 36.8w 总包硕⼠:
普通档:23k x 16 = 36.8w 总包sp 档:25k x 16 = 40w 总包
既然⻉壳薪资跟 ⼤⼚差不多,是不是意味着⾯试难度跟⼤⼚差不多?
还真是呢,我翻了⼀些⻉壳同学的⾯经,⾯试难度真跟⼤⼚差不太多 ,⼋股也问的蛮多的,算法
也是需要⼿撕的。
这次就分享⼀位⻉壳校招Java 后端开发的⾯经,这个是⼀⾯⾯经,主要以拷打⼋股为主了 。

考察的知识点,我也给⼤家罗列⼀下:
Java :HashMap 、ConcurrentHashMap 、synchronized 、类加载
MySQL :索引字段、索引失效
⽹络:tcp 三次握⼿和四次挥⼿、time_wait 状态、拥塞控制、流量控制
操作系统:进程和线程、进程间通信
算法:⼆分查找
Java
HashMap 底层的数据结构是怎么样的?
在 JDK 1.7 版本之前, HashMap 数据结构是数组和链表,HashMap 通过哈希算法将元素的键

(Key )映射到数组中的槽位(Bucket )。如果多个键映射到同⼀个槽位,它们会以 链表的形式 存储在同⼀个槽位上,因为链表的查询时间是O(n) ,所以冲突很严重,⼀个索引上的链表⾮常⻓,效率就很低了。所以在 JDK 1.8 版本的时候做 了优化,当⼀个链表的⻓度超过8的时候就转换数据结构,不再使⽤
链表存储,⽽是使⽤红⿊树,查找时使⽤红⿊树,时间复杂度O(log n ),可以提⾼查询性能,但
是在数量较少时,即数量⼩于6时,会将红⿊树转换回链表。

ConcurrentHashMap 是怎么实现线程安全和并发的?
JDK 1.7 ConcurrentHashMap
在 JDK 1.7 中它使⽤的是数组加链表的形式 实现的,⽽数组⼜分为:⼤数组 Segment 和⼩数组
HashEntry 。 Segment 是⼀种可重⼊锁(ReentrantLock ),在 ConcurrentHashMap ⾥扮演锁的⻆⾊;HashEntry 则⽤于存储键值对数据。⼀个 ConcurrentHashMap ⾥包含⼀个 Segment 数组,⼀个 Segment ⾥包含⼀个 HashEntry 数组,每个 HashEntry 是⼀个链表结构的元素。

JDK 1.7 ConcurrentHashMap 分段锁技术将数据分成⼀段⼀段的存储,然后给每⼀段数据配⼀把
锁,当⼀个线程占⽤锁访问其中⼀个段数据的时候,其他段的数据也能被其他线程访问,能够实
现真正的并发访问。
JDK 1.8 ConcurrentHashMap
在 JDK 1.7 中,ConcurrentHashMap 虽然是线程安全的,但因为它的底层实现是数组 + 链表的形式,所以在数据⽐较多的情况下访问是很慢的,因为要遍历整个链表,⽽ JDK 1.8 则使⽤了数组 +
链表/红⿊树的⽅式优化了 ConcurrentHashMap 的实现,具体实现结构如下:

JDK 1.8 ConcurrentHashMap JDK 1.8 ConcurrentHashMap 主要通过 volatile + CAS 或者
synchronized 来实现的线程安全的。添加元素时⾸先会判断容器是否为空:
如果为空则使⽤ volatile 加 CAS 来初始化
如果容器不为 空,则根据存储的元素计算该位置是否为空。
如果根据存储的元素计算结果为空,则利 ⽤ CAS 设置该节点;
如果根据存储的元素计算结果不为 空,则使⽤ synchronized ,然后,遍历桶中的数据,并替
换或新增节点到桶中,最后再判断是否需要转为红⿊树,这样就能保证并发访问时的线程安
全了。
如果把上⾯的执⾏⽤⼀句话归纳的话,就相当于是ConcurrentHashMap 通过对头结点加锁来保证
线程安全的,锁的粒度相⽐ Segment 来说更⼩了,发⽣冲突和加锁的频率降低了,并发操作的性
能就提⾼了。
⽽且 JDK 1.8 使⽤的是红⿊树优化了之 前的固定链表,那么当数据量⽐较⼤的时候,查询性能也得
到了很⼤的提升,从之 前的 O(n) 优化到了 O(logn) 的时间复杂度。ConcurrentHashMap ⽀持并发写, ConcurrentHashMap 实现⼤⼩获取的
## size() 函数是怎么实现的
JDK1.8 的 size() 代码:
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}
sumCount() 的代码如下::
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
分析⼀下 sumCount() 代码。ConcurrentHashMap 提供了 baseCount 、counterCells 两个 辅助变
量和⼀个 CounterCell 辅助内部类。 sumCount() 就是迭代 counterCells 来统计 sum 的过程。 put
操作时,肯定会影响 size() ,在 put() ⽅法最后会调⽤ addCount() ⽅法。
size() 使⽤sumCount() ⽅法计算map 的size 。
对于size 的计算,在扩容和addCount() ⽅法已经有所处理了;JDK1.8 ConCurrentHashMap 的⼤⼩ size 通过 baseCount 和 counterCells 两个 变量维护:
在没有并发的情况下,使⽤⼀个volatile 修饰的 baseCount 变量即可;
当有并发时,CAS 修改 baseCount 失败后,会使 ⽤ CounterCell 类,即 创建⼀个CounterCell 对
象,设置其volatile 修饰的 value 属性为 1,并将其放在ConterCells 数组的随机位置;
最终在sumCount() ⽅法中通过累加 baseCount 和CounterCells 数组⾥每个CounterCell 的值 得出Map 的总⼤⼩Size 。
然⽽ 返回的值是⼀个估计值;如果有 并发插⼊或者删除操作,和实际的数量可能有所不同。
另外size() ⽅法的最⼤值是 Integer 类型的最⼤值,⽽ Map 的 size 有可能超过
Integer.MAX_VALUE ,所以 JDK8 建议使⽤ mappingCount() ,⽽不是 size() ,因为这个⽅法
的返回值是 long 类型,不会因为 size ⽅法是 int 类型限制最⼤值。线程池ThreadPoolExecutor 的核⼼参数以及在它的⽣命周 期中这些核⼼参
数的作⽤是什么 , 能描述下吗?
线程池的构造函数有7个参数:

corePoolSize :线程池核⼼线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize ,那么即使这些线程处于空闲状态,那也不 会被销毁。maximumPoolSize :限制了线程池能创建的最⼤线程总数(包括核⼼线程和⾮核⼼线程),当
corePoolSize 已满 并且 尝试将新任务加 ⼊阻塞队列失败(即队列已满)并且 当前线程数 <maximumPoolSize ,就会创建新线程执⾏此任务,但是当 corePoolSize 满 并且 队列满 并且
线程数已达 maximumPoolSize 并且 ⼜有新任务提交时,就会触发拒绝策略。
keepAliveTime :当线程池中线程的数量⼤于corePoolSize ,并且某个线程的空闲时间超过了
keepAliveTime ,那么这个线程就会被销毁。
unit :就是keepAliveTime 时间的单位。
workQueue :⼯作队列。当没有空闲的线程执⾏新任务时,该任务就会被放⼊⼯作队列中,等
待执⾏。
threadFactory :线程⼯⼚。可以⽤来给线 程取名字等等
handler :拒绝策略。当⼀个新任务交给线 程池,如果此时线程池中有空闲的线程,就会直接执
⾏,如果没有空闲的线程,就会将该任务加 ⼊到阻塞队列中,如果阻塞队列满了,就会创建⼀
个新线程,从阻塞队列头部取出⼀个任务来执⾏,并将新任务加 ⼊到阻塞队列末尾。如果当前
线程池中线程的数量等于maximumPoolSize ,就不会创建新线程,就会去执⾏拒绝策略
说⼀下synchronized 和ReentrantLock 的区别 synchronized 和
ReentrantLock 都是 Java 中提供的可重⼊锁:
⽤法不同:synchronized 可⽤来修饰普通⽅法、静态⽅法和代码块,⽽ ReentrantLock 只能⽤
在代码块上。
获取锁和释放锁⽅式不同:synchronized 会⾃动加 锁和释放锁,当进⼊ synchronized 修饰的代
码块之后会⾃动加 锁,当离开 synchronized 的代码段之后会⾃动释放锁。⽽ ReentrantLock 需
要⼿动加 锁和释放锁
锁类型不同:synchronized 属于⾮公平锁,⽽ ReentrantLock 既可以是公平锁也可以是⾮公平
锁。
响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,⽽ synchronized 不能响应中
断。
底层实现不同:synchronized 是 JVM 层⾯通过监视器实现的,⽽ ReentrantLock 是基于 AQS
实现的。
synchronized 底层原理了解过吗
synchronized 是Java 提供的原⼦性内置锁,这种内置的并且使⽤者看不到的锁也被称为监视器锁,
使⽤synchronized 之后,会在编译之后在同步的代码块前后加上monitorenter 和monitorexit 字节码
指令,他依赖操作系统底层互斥锁实现。他的作⽤主要就是实现原⼦性操作和解决共 享变量的内
存可⻅性问题。
执⾏monitorenter 指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器
+1 。此时其他竞争锁的线程则会进⼊等待队列中。执⾏monitorexit 指令时则会把计数器-1 ,当计
数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。
synchronized 是排它锁,当⼀个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,
⽽且由于Java 中的线程和操作系统原⽣线程是⼀⼀对应的,线程被阻塞或者唤醒时时 会从⽤⼾态切
换到内核态,这种转换⾮常消耗性能。
从内存语义来说,加锁的过程会清除⼯作内存中的共享变量,再从主 内存读取,⽽释放锁的过程
则是将⼯作内存中的共享变量写回主内存。
实际上⼤部分时候我认为说到monitorenter 就⾏了,但是为了 更清楚的描述,还是再具体⼀点。
如果再深⼊到源码来说,synchronized 实际上有两个 队列waitSet 和entryList 。
当多个线程进⼊同步代码块时,⾸先进⼊entryList
有⼀个线程获取到monitor 锁后,就赋值给当前线程,并且计数器+1
如果线程调⽤wait ⽅法,将释放锁,当前线程置为null ,计数器-1 ,同时进⼊waitSet 等待被唤醒,调⽤notify 或者notifyAll 之后⼜会进⼊entryList 竞争锁
如果线程执⾏完毕,同样释放锁,计数器-1 ,当前线程置为null

image-20250903140439134
JAVA ⾥的类加载机制了解吗

我们把 Java 的类加载过 程分为三个主 要步骤:加载、链接、初始化。
⾸先是加载阶段(Loading ),它是 Java 将字节码数据从不 同的数据源读取到 JVM 中,并映射为
JVM 认可的数据结构(Class 对象),这⾥的数据源可能是各种各样的形态,如 jar ⽂件、class ⽂件,甚⾄是⽹络数据源等;如果输⼊数据不是 ClassFile 的结构,则会抛出 ClassFormatError 。
加载阶段是⽤⼾参与的阶段,我们可以⾃定义类加载器, 去实现⾃⼰的类加载过 程。
第⼆阶段是链接(Linking ),这是核⼼的步骤,简单说是把原始的类定义信息平滑地转化⼊ JVM
运⾏的过程中。这⾥可进⼀步细分为三个 步骤:
验证(Verification ),这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机
规范的,否则就被认为是 VerifyError ,这样就防⽌了恶意信息或者不合规的信息危害 JVM 的运
⾏,验证阶段有可能触发更多 class 的加载。准备(Preparation ),创建类或接⼝中的静态变量,并初始化静态变量的初始值。但这⾥的“初
始化”和下⾯的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执⾏更
进⼀步的 JVM 指令。
解析(Resolution ),在这⼀步会将常量池中的符号引⽤(symbolic reference )替换为直接引
⽤。
最后是初始化阶段(initialization ),这⼀步真正去执⾏类初始化的代码逻辑,包括静态字段赋值的
动作,以及执⾏类定义中 的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理
好,⽗类型的初始化逻辑优先于当前类型的逻辑。
类加载时, 类加载器的双亲委派机制了解吗
双亲委派机制,简单说就是当类加载器( Class-Loader )试图加载某个类型的时候,除⾮⽗加载器
找不到相应类型,否则尽量将这个任务代理给当前加 载器的⽗加载器去做。使⽤委派模型的⽬的
是避免重复加载 Java 类型。
双亲委派模型的作⽤:
保证类的唯⼀性:通过委托机制,确保了所有加载请求都会传 递到启动类加载器, 避免了不 同
类加载器重复加载相同类的情况,保证了Java 核⼼类库的统⼀性,也防⽌了⽤⼾⾃定义类覆盖
核⼼类库的可能。
保证安全性:由于Java 核⼼库被启动类加载器加载,⽽启动类加载器只加载信任 的类路径中的
类,这样可以防⽌不可信的类假冒核⼼类,增强了系统的安全性。例如,恶意代码⽆法⾃定义
⼀个java.lang.System 类并加载到JVM 中,因为这个请求会被委托给启动类加载器, ⽽启动类加
载器只会加载标准的Java 库中的类。
⽀持隔离和层次划分 :双亲委派模型⽀持不同层次的类加载器服务于不 同的类加载需求,如应
⽤程序类加载器加载⽤⼾代码,扩展类加载器加载扩展框架,启动类加载器加载核⼼库。这种
层次化的划分 有助于实现沙箱安全机制,保证了各个层级类加载器的职责清晰,也便于维护和
扩展。
简化了加载流程:通过委派,⼤部分类能够被正确的类加载器加载,减少了每个加载器需要处
理的类的数量,简化了类的加载过 程,提⾼了加载效率。
MySQL
你⼀般通过什么 来判断⼀个字段需不需要建索引?
什么 时候适⽤索引?
字段有唯⼀性限制的,⽐如商品编码;
经常⽤于 WHERE 查询条件的字段,这样能够提⾼整个表的查询速度,如果查 询条件不是⼀个
字段,可以建⽴联合索引。
经常⽤于 GROUP BY 和 ORDER BY 的字段,这样在查询的时候就不需要再去做⼀次排序了,因
为我们都已经知道了建⽴索引之后在 B+Tree 中的记录都是排序好的。
什么 时候不需要创建索引?
WHERE 条件, GROUP BY , ORDER BY ⾥⽤不到的字段,索引的价值是快速定位,如果起不到
定位的字段通常是不需要创建索引的,因为索引是会占⽤物理空间的。
字段中存在⼤量重 复数据,不需要创建索引,⽐如性别字段,只有男⼥,如果数据库表中,男
⼥的记录分布均匀,那么⽆论搜索哪个值都可能得到⼀半的数据。在这些情况下,还不如不要
索引,因为 MySQL 还有⼀个查询优化器, 查询优化器发现某个值出现在表的数据⾏中的百 分⽐
很⾼的时候,它⼀般会忽略索引,进⾏全表扫描。
表数据太少的时候,不需要创建索引;
经常更新的字段不⽤创建索引,⽐如不要对电商项⽬的⽤⼾余额建⽴索引,因为索引字段频繁
修改,由于需要维护索引的有序性,有额外的维护成 本。
索引失效的场景有哪些?
6 种会发⽣索引失效的情况:
当我们使 ⽤左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种⽅式都会造成索
引失效;
当我们在查询条件中对索引列使⽤函数,就会导致索引失效。
当我们在查询条件中对索引列进⾏表达式计算,也是⽆法⾛索引的。
MySQL 在遇到字符串和数字⽐较的时候,会⾃动把字符串转为数字,然后再进⾏⽐较。如果字
符串是索引列,⽽条件语句中的输⼊参数是数字的话,那么索引列会发⽣隐式类型转换,由于
隐式类型转换是通过 CAST 函数实现的,等同于对索引列使⽤了函数,所以就会导致索引失
效。
联合索引要能正确使⽤需要遵循最左匹配原则,也就是按照最左优先的⽅式进⾏索引的匹配,
否则就会导致索引失效。
在 WHERE ⼦句中,如果在 OR 前的条件列是索引列,⽽在 OR 后的条件列不是索引列,那么索
引会失效。
⽹络
说⼀下TCP 的三次握⼿和四次挥⼿
三次握⼿
TCP 是⾯向连接的协议,所以使 ⽤ TCP 前必须先建⽴连接,⽽建⽴连接是通过三次握⼿来进⾏
的。三次握⼿的过程如下图:

⼀开始,客⼾端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端⼝,处于 LISTEN 状
态

客⼾端会随机初始化序号(client_isn ),将此序号置于 TCP ⾸部的「序号」字段中,同时把
SYN 标志位置为 1,表⽰ SYN 报⽂。接着把第⼀个 SYN 报⽂发送给服务端,表⽰向服务端发起
连接,该报⽂不包含应⽤层数据,之后客⼾端处于 SYN-SENT 状态。

服务端收到客⼾端的 SYN 报⽂后,⾸先服务端也随机初始化⾃⼰的序号(server_isn ),将此序
号填⼊ TCP ⾸部的「序号」字段中,其次把 TCP ⾸部的「确认应答号」字段填⼊ client_isn + 1,接着把 SYN 和 ACK 标志位置为 1。最后把该报⽂发给客⼾端,该报⽂也不 包含应⽤层数据,之
后服务端处于 SYN-RCVD 状态。

客⼾端收到服务端报⽂后,还要向服务端回应最后⼀个应答报⽂,⾸先该应答报⽂ TCP ⾸部
ACK 标志位置为 1 ,其次「确认应答号」字段填⼊ server_isn + 1 ,最后把报 ⽂发送给服务
端,这次报⽂可以携带客⼾到服务端的数据,之后客⼾端处于 ESTABLISHED 状态。
服务端收到客⼾端的应答报⽂后,也进⼊ ESTABLISHED 状态。
从上 ⾯的过程可以发现第三次握⼿是可以携带数据的,前两次握⼿是不可以携带数据的,这也是
⾯试常问的题。
⼀旦完成三次握⼿,双⽅都处于 ESTABLISHED 状态,此时连接就已建⽴完成,客⼾端和服务端就
可以相互发送数据了。
四次挥⼿

具体过程:
客⼾端主动调⽤关闭连接的函数,于是就会发送 FIN 报⽂,这个 FIN 报⽂代表客⼾端不会再发
送数据了,进⼊ FIN_WAIT_1 状态;服务端收到了 FIN 报⽂,然后⻢上回复⼀个 ACK 确认报⽂,此时服务端进⼊ CLOSE_WAIT 状
态。在收到 FIN 报⽂的时候,TCP 协议栈会为 FIN 包插⼊⼀个⽂件结束符 EOF 到接收缓冲区
中,服务端应⽤程序可以通过 read 调⽤来感知这个 FIN 包,这个 EOF 会被放在已排队等候的
其他已接收的数据之后,所以必须要得继续 read 接收缓冲区已接收的数据;
接着,当服务端在 read 数据的时候,最后⾃然就会读到 EOF ,接着 read() 就会返回 0,这时服务端应⽤程序如果有 数据要发送的话,就发完数据后才调⽤关闭连接的函数,如果服 务端应⽤
程序没有数据要发送的话,可以直接调⽤关闭连接的函数,这时服务端就会发⼀个 FIN 包,这
个 FIN 报⽂代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;
客⼾端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客⼾端将进⼊ TIME_WAIT
状态;
服务端收到 ACK 确认包后,就进⼊了最后的 CLOSE 状态;
客⼾端经过 2MSL 时间之后,也进⼊ CLOSE 状态;
第 3 次挥⼿后,主动关闭的⼀⽅会有⼀个TIME-WAIT 的状态对吧, 了解吗?
了解的,TIME-WAIT 会等待 2MSL 的时间,随后才会释放连接。
TIME_WAIT 状态的存在是为了 确保⽹络连接的可靠关闭。只有主动发起关闭连接的⼀⽅(即主动
关闭⽅)才会有 TIME_WAIT 状态。
TIME_WAIT 状态的需求主要有两个 原因:
防⽌具有相同「四元组」的「旧」数据包被收到:在⽹络通信中,每个 TCP 连接都由源 IP 地址、源端⼝号、⽬标 IP 地址 和⽬标端⼝号这四个元素唯⼀标识,称为「四元组」。当⼀⽅主动
关闭连接后,进⼊ TIME_WAIT 状态,它仍然可以接收到⼀段时间内来⾃对⽅的延迟数据包。这
是因为⽹络中可能存在被延迟传输的数据包,如果没有 TIME_WAIT 状态的存在,这些延迟数据
包可能会被错误地传递给新的连接,导致数据混乱。通过保持 TIME_WAIT 状态,可以防⽌旧的
数据包⼲扰新的连接。
保证「被动关闭连接」的⼀⽅能被正确关闭:当连接的被动关闭⽅接收到主动关闭⽅的 FIN 报
⽂(表⽰关闭连接),它需要发送⼀个确认 ACK 报⽂给主动关闭⽅,以完成连接的关闭。然
⽽,⽹络是不可靠的,ACK 报⽂可能会在传输过 程中丢 失。如果主动关闭⽅在收到 ACK 报⽂之
前就关闭连接,被动关闭⽅将⽆法正常完成连接的关闭。TIME_WAIT 状态的存在确保了被动关
闭⽅能够接收到最后的 ACK 报⽂,从⽽帮助其正常关闭连接。
被动关闭连接的⼀⽅⽆法正常关闭会有什么 问题吗?
会导致被动关闭连接⼀⽅堆积⼤量处于CLOSE_WAIT 状态的TCP 连接。
通常出现⼤量CLOSE_WAIT 状态连接是因为代码的问题,⽐如没有调⽤ close 来关闭连接,或者是
程序在调⽤ close 发⽣了 fullgc 、死锁、死循环的问题,这时候排查的⽅向是排查代码为什么 没有
调⽤ close 。
TCP 这块有拥塞控制和流量控制, 这⼀块你了解吗?
流量控制的主要⽬的是防⽌发送⽅发送数据过快,导致接收⽅来不及处理,从⽽造成数据丢失。
TCP 通过控制窗⼝(TCP Window )来实现流量控制。窗⼝的⼤⼩是接收⽅根据⾃⾝的缓冲区⼤⼩
动态调整的:
接收窗⼝(Receive Window ):接收⽅告诉发送⽅它可以接收多少数据。这个窗⼝的⼤⼩由接
收⽅计算,并在TCP 报⽂头中传递给发送⽅。
滑动窗⼝(Sliding Window ):TCP 使⽤滑动窗⼝机制,使得在发送确认之前,可以连续发送多
个数据包,从⽽提⾼效率。
拥塞控制则 是为了 避免过多的数据被发送到⽹络中,以⾄于⽹络拥堵,导致数据包丢失和延迟。
TCP 的拥塞控制机制主要包括以下⼏个算法:
慢启动(Slow Start ):初始时,拥塞窗⼝设置为⼀个⼩值(通常是⼀个MSS ),然后每收到⼀个
确认(ACK ),拥塞窗⼝就加倍,直到达到⼀个阈值(ssthresh )。
拥塞避免(Congestion Avoidance ):当拥塞窗⼝达到阈值后,进⼊拥塞避免阶段,窗⼝增⼤速
度减缓,通常是线性增⼤(每经过⼀个RTT (往返时间),增加1)。
快重传(Fast Retransmit ):当发送⽅连续接收到三个 重复的ACK 时,认为丢 包发 ⽣,⽴即重发
丢失的数据包,⽽不是等待超时。
快恢复(Fast Recovery ):在快重传后,不会将拥塞窗⼝减少到初 始值,⽽是设置为ssthresh ,
并进⼊拥塞避免阶段。
如果接收⽅接收能⼒不够, 导致TCP ⾸部⾥标识的滑动窗⼝⼤⼩不断减⼩, 如
果窗⼝减⼩到了0, 那怎么重新开始呢?
TCP 通过让接收⽅指明希望从发送⽅接收的数据⼤⼩(窗⼝⼤⼩)来进⾏流量控制。
如果窗⼝⼤⼩为 0 时,就会阻⽌发送⽅给接收⽅传递数据,直到窗⼝变为⾮ 0 为⽌,这就是窗⼝
关闭。
接收⽅向发送⽅通告窗⼝⼤⼩时,是通过 ACK 报⽂来通告的。
那么,当发⽣窗⼝关闭时,接收⽅处理完数据后,会向发送⽅通告⼀个窗⼝⾮ 0 的 ACK 报⽂,如
果这个通告窗⼝的 ACK 报⽂在⽹络中丢 失了,那⿇烦就⼤了。

这会导致发送⽅⼀直等待接收⽅的⾮ 0 窗⼝通知,接收⽅也⼀直等待发送⽅的数据,如不采取措
施,这种相互等待的过程,会造成了死锁的现象。
为了 解决这个问题,TCP 为每个连接设有⼀个持续定时器, 只要 TCP 连接⼀⽅收到对⽅的零窗⼝
通知,就启动持续计时器。
如果持续计时器超时,就会发送窗⼝探测 ( Window probe ) 报⽂,⽽对⽅在确认这个探测报⽂时,给出⾃⼰现在的接收窗⼝⼤⼩。

如果接收窗⼝仍然为 0,那么收到这个报⽂的⼀⽅就会重新启动持续计时器;
如果接收窗⼝不是 0,那么死锁的局⾯就可以被打破了。
窗⼝探测的次数⼀般为 3 次,每次 ⼤约 30-60 秒(不同的实现可能会不⼀样)。如果 3 次过后接收
窗⼝还是 0 的话,有的 TCP 实现就会发 RST 报⽂来中断连接。
操作系统
简单说下进程和线程的区别

本质区别:进程是操作系统资源分配的基本单位,⽽线程是任务调度和执⾏的基本单位
在开销⽅⾯:每个进程都有独⽴的代码和数据空间(程序上下 ⽂),程序之间的切换会有较⼤的
开销;线程可以看做轻量级的进程,同⼀类线程共享代码和数据空间,每个线程都有⾃⼰独⽴
的运⾏栈和程序计数器( PC ),线程之间切换的开销⼩
稳定性⽅⾯:进程中某个线程如果崩溃了,可能会导致整个进程都崩溃。⽽进程中的⼦进程崩
溃,并不会影响其他进程。
内存分配⽅⾯:系统在运⾏的时候会为每个进程分配不同的内存空间;⽽对线程⽽⾔,除了
CPU 外,系统不会为线程分配内存(线程所使⽤的资源来⾃其所属进程的资源),线程组之间只
能共享资源
包含关系:没有线程的进程可以看做是单线程的,如果⼀个进程内有多个线程,则执⾏过程不
是⼀条线的,⽽是多条线
进程间有哪些通信机制
Linux 内核提供了不 少进程间通信的⽅式:
管道
消息队列
共享内存
信号
信号量
socket

Linux 内核提供了不 少进程间通信的⽅式,其中最简单的⽅式就是管道,管道分为「匿名管道」和
「命名 管道」。
匿名管道顾名思义,它没有名字标识,匿名管道是特殊⽂件只存在于内存,没有存在于⽂件系统
中,shell 命令中的「|」竖线就是匿名管道,通信的数据是⽆格式的流并且⼤⼩受限,通信的⽅式
是单向的,数据只能在⼀个⽅向上流动,如果要双向通信,需要创建两个 管道,再来匿名管道是
只能⽤于存在⽗⼦关系的进程间通信,匿名管道的⽣命周 期随着进程创建⽽建⽴,随着进程终⽌
⽽消失。
命名 管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使⽤命名 管道的前提,需要在
⽂件系统创建⼀个类型为 p 的设备⽂件,那么毫⽆关系的进程就可以通过这 个设备⽂件进⾏通
信。另外,不管是匿名管道还是命名 管道,进程写⼊的数据都是缓存在内核中,另⼀个进程读取
数据时候⾃然也是从内核中获取,同时通信数据都遵循先进先出原则,不⽀持 lseek 之类的⽂件定
位操作。
消息队列克服了管道通 信的数据是⽆格式的字节流的问题,消息队列实际上是保存在内核的「消
息链表」,消息队列的消息体是可以⽤⼾⾃定义的数据类型,发送数据时,会被分成⼀个⼀个独⽴
的消息体,当然接收数 据时,也要与发送⽅发送的消息体的数据类型保持⼀致,这样才能保证读
取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次 数据的写⼊和读取都需要经过
⽤⼾态与内核态之间的拷⻉过程。
共享内存可以解决消息队列通信中⽤⼾态与内核态之间数据拷 ⻉过程带来的开销,它直接分配⼀
个共享空间,每个进程都可以直接访问,就像访问进程⾃⼰的空间⼀样快捷⽅便,不需要陷⼊内
核态或者系统调⽤,⼤⼤提⾼了通信的速度,享有最快的进程间通信⽅式之名。但是便捷⾼效的
共享内存通信,带来新的问题,多进程竞 争同个共享资源会造成数据的错乱。
那么,就需要信号量来保护共享资源,以确保任何 时刻只能有⼀个进程访问共享资源,这种⽅式
就是互斥访问。信号量不仅 可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是⼀
个计数器, 表⽰的是资源个数,其值可以通过两个 原⼦操作来控制,分别 是 P 操作和 V 操作。
与信号量名字很相似的叫信号,它俩名字虽然相似,但功能⼀点⼉都不⼀样。信号是异步通信机
制,信号可 以在应⽤进程和内核之间直接交互 ,内核也可以利⽤信号来通知⽤⼾空间的进程发⽣
了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),
⼀旦有信号发⽣,进程有三种⽅式响应信号 1. 执⾏默认操作、2. 捕捉 信号、3. 忽略信号。有两个
信号是应⽤进程⽆法捕捉 和忽略的,即 SIGKILL 和 SIGSTOP ,这是为了 ⽅便我们能在任何 时候结束或停⽌某个进程。
前⾯说到的通信机制,都是⼯作于同⼀台主机,如果要与不 同主机的进程间通信,那么就需要
Socket 通信了。Socket 实际上不仅 ⽤于不 同的主机进程间通信,还可以⽤于本地主机进程间通
信,可根据创建 Socket 的类型不同,分为三 种常⻅的通信⽅式,⼀个是基于 TCP 协议的通信⽅
式,⼀个是基于 UDP 协议的通信⽅式,⼀个是本地进程间通信⽅式。
算法
NC105 ⼆分查找-II
