Java
⼩鹏汽⻋ Java ⾯试
现在新能源汽⻋的竞争已经开始进⼊下半场了,各家公司都开始进⾏组织 架构 调整,决战决赛
圈,如果不能在决赛圈胜出,可能企业很难存活下来,市场就是这么残酷,赢家通吃,输家出
局。
之前发过理想 、蔚来 、⽐亚迪 、极越 等⻋企公司的薪资情况和⾯试真题,这次来补上⼩
鹏汽⻋的!
⼩鹏汽⻋ 2025 年计划招聘 6000 ⼈,即使之前从⼩鹏离职过的,也可以重新加⼊⼩鹏。
之所以今年⼤⼒开始招聘,也是因为⼩鹏汽⻋穿过了暴⻛⾬,在 2024 年度 销量有突破性的增⻓,
现在⼩鹏汽⻋⽉销 4w+ 台是常态,⽽且应该会很快 上升到 5w+ 台,销量上去了,⾃然要扩⼤员
⼯,在新的⼀年,将继续推出更多新型号的汽⻋。
这次来看看 25 届⼩鹏汽⻋校招的薪资,⽬前还没看到 Java 岗的薪资,所以列了⼀些其他岗位的
薪资情况,也可以做个参考:
软件测试:18.5k x 15 = 27.7w ,办公地点⼴州,同学背景硕⼠ 985
嵌⼊式开 发:20k * 15 = 30w ,办公地点上海,同学背景硕⼠211
产品经理:18.5 x 15 = 27.7w ,办公地点⼴州,同学背景硕⼠其他
影像开发:25k x 15 = 37.5w ,办公地点⼴州,同学背景硕⼠ 985⼩鹏汽⻋的薪资对⽐互联⽹公司⼤⼚的话,是会少⼀些,⼤概是互联⽹中⼚的薪资⽔平,如果同
学⼿上有互联⽹公司选择的话,⼩鹏汽⻋的竞争⼒就会弱⼀些了 。新能源汽⻋⾥开的薪资能对标
互联⽹⼤⼚的,⽬前来看是理想汽⻋了。
肯定也有同学好奇 ,⼩鹏汽⻋的⾯试难度如何?
这次来看看 ⼩鹏汽⻋Java 岗位的校招⾯经,主要考察计算机基、Java 、JVM 、⽹络、算法这些内
容,难度的话,算中等,算法还是会考察,⼤部分新能源汽⻋都会考察,不管⾯互联⽹公司,还
是新能源⻋企,算法⼤家都需要准备。

计算机基础
ARM 有了解嘛?
了解不过,⼤概知道是⼀个处理器, 嵌⼊式领域⽤的多。
⼆进制怎么转16 机制?
可以采⽤分组转换法。
分组转换法基于⼆进制和⼗六进制之间的位权关系。因为2^4=16 ,这意味着 4 位⼆进制数能够表
⽰的状态数刚好与⼗六进制的⼀位所能表⽰的状态数相同。所以可以将⼆进制数按每 4 位⼀组进
⾏划分 ,每⼀组都能唯⼀地对应⼀个⼗六进制数字。
下⾯我给出了⼆进制的数,我们将它转换为⼗六进制,例如:0101101 ,我们将这个数按4个⼀组
来划分 ,变成 0010 1101 (这⾥本来 是010 1101 前⾯不够4位我们就凑⼀个0),可以得到 0010 =2 、
1101=D 所以转换成⼗六进制就是2D 。byte 类型的-1 怎么表⽰?
byte 类型是有符号的 8 位整数 ,取值范围是 -128 到 127 。 -1 在 byte 类型中的⼆进制表⽰
是补码形式 ,正数的补码与原码相同,负数的补码是在反码的基础上加 1,这是因为计算机中采⽤
补码来进⾏减法运算,可以将减法转换为加法,⽅便硬件实现,计算过程如下:
先写 出 1 的原码: 00000001 。
然后得到 -1 的原码: 10000001 。
接着求 -1 的反码: 11111110 。
最后求 -1 的补码: 11111111 。
所以,在 Java 的 byte 类型中, -1 ⽤⼆进制补码表⽰为 11111111 。当进⾏运算或存储时,计
算机使⽤这个补码来处理 -1 相关的操作。例如,在进⾏加法运算时, -1 + 1 的计算过程如下:
- 1 的补码是 11111111 , 1 的补码是 00000001 。
相加得到: 11111111 + 00000001 = 100000000 (9 位,超出 byte 范围)。由于 byte 类型是 8 位,会发⽣截断,得到 00000000 ,也就是 0 ,这符合数学运算结果。
Java
两个 ⽅法都被synchronized 修饰,其中⼀个调⽤另⼀个可以成功嘛?
synchronized 修饰⽅法锁的那⼀部分?
如果两个 ⽅法都被 synchronized 修饰,⼀个⽅法内部调⽤另⼀个⽅法是可以成功的。这是因为
synchronized ⽅法默认是对当前对象( this )加锁。当⼀个线程进⼊了⼀个 synchronized ⽅
法,它已经获得了该对象的锁,在这个⽅法内部调⽤另⼀个 synchronized ⽅法时,由于是同⼀个
对象的锁,所以线程可以继续执⾏被调⽤的 synchronized ⽅法,不会出现锁竞争导致⽆法调⽤的
情况。
例如下⾯的代码, method1 调⽤ method2 时,因为它们都是同⼀个对象 example 的
synchronized ⽅法,所以可以正常执⾏。
public class SynchronizedExample {
public synchronized void method1() {
System.out.println("Method 1 started");
method2();
System.out.println("Method 1 ended");
}
public synchronized void method2() {
System.out.println("Method 2 is running");
}
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
example.method1();
}
}synchronized 修饰⽅法锁的对象:
对于⾮静态⽅法:当 synchronized 修饰⼀个⾮静态⽅法时,锁的是当前对象( this )。这意
味着同⼀时刻,对于同⼀个对象实例,只有⼀个线程能够执⾏这个对象的 synchronized ⾮静态
⽅法。不同的对象实例之间的 synchronized ⾮静态⽅法可以被不同的线程同时执⾏,因为它们
的锁对象( this )是不同的。
对于静态⽅法:当 synchronized 修饰⼀个静态⽅法时,锁的是这个类的 Class 对象。因为静
态⽅法是属于类的,⽽不是属于某个具体的对象实例。所以同⼀时刻,对于⼀个类的所有实
例,只有⼀个线程能够执⾏这个类的 synchronized 静态⽅法。例如,下⾯的例⼦,
staticMethod1 和 staticMethod2 都是静态的 synchronized ⽅法,它们共享同⼀个类的
Class 对象作为锁。所以当 thread1 和 thread2 同时启动时,其中⼀个⽅法会先获得类的
Class 对象锁,另⼀个⽅法需要等待锁释放后才能执⾏。
public class SynchronizedStaticExample {
public static synchronized void staticMethod1() {
System.out.println("Static Method 1 started");
}
public static synchronized void staticMethod2() {
System.out.println("Static Method 2 started");
}
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
SynchronizedStaticExample.staticMethod1();
});
Thread thread2 = new Thread(() -> {
SynchronizedStaticExample.staticMethod2();
});
thread1.start();
thread2.start();
}
}静态内部类和匿名内部类有什么 区别吗?
静态内部类是定义在另⼀个类内部的类,并且使⽤ static 关键字修饰。它就像是类的⼀个静态成
员,不依赖于外部类的实例,就像下⾯的例⼦中, StaticInnerClass 可以直接访问 OuterClass
的 outerStaticVar 静态变量。
class OuterClass {
private static int outerStaticVar = 10;
static class StaticInnerClass {
public void printOuterStaticVar() {
System.out.println(outerStaticVar);
}
}
}静态内部类不能直接访问外部类的⾮静态成员,因为⾮静态成员是依赖于外部类的实例存在的。
如果要访问外部类的⾮静态成员,需要通过外部类的实例来访问。
静态内部类的⽣命周 期与外部类的静态成员相似。它在外部类加载时不会⾃动加 载,只有在第⼀
次被使⽤(例如,通过 new 关键字创建实例或者访问静态成员)时才会加载。加载后,只要类加
载器没有卸载这 个类,它就⼀直存在于内存中。
实例化静态内部类时,不需要外部类的实例。可以直接通过 外部类名 .静态内部类名 的⽅式来创建
实例,例如 OuterClass.StaticInnerClass innerObj = new OuterClass.StaticInnerClass(); 。当⼀个类只与另⼀个类有⽐较紧密的关联,并且主 要是为了 辅助外部类完成某些功能,同时⼜不
依赖于外部类的实例时,适合使⽤静态内部类。例如,⼀个⼯具类中的⼀些⼯具⽅法可以组织 成
静态内部类,这些⽅法可能会共享⼀些外部类的静态资源。静态内部类还可以⽤于实现单例模
式。通过将单例对象的实例化放在静态内部类中,可以保 证在第⼀次访问单例对象时才进⾏实例
化,并且保证了线程安全。
匿名内部类是⼀种没有名字的内部类。它是在创建对象的同时定义类的⼀种⽅式,通常⽤于只需
要使⽤⼀次的类,并且是作为某个接⼝或者抽象类的实现(或者某个类的⼦类)出现。例如,在
下⾯实现接⼝的例⼦中,匿名内部类是在 main ⽅法内部定义的,它的⾏为可能会受到 main ⽅法
中的其他变量或者外部类的状态的影响。
interface MyInterface {
void myMethod();
}
class Main {
public static void main(String[] args) {
MyInterface anonymousClass = new MyInterface() {
@Override
public void myMethod() {
System.out.println("This is an anonymous class implementing MyInterface")
}
};
anonymousClass.myMethod();
}
}匿名内部类可以访问外部类的成员变量和⽅法,包括静态和⾮静态的。如果访问外部类的局部变
量,这些局部变量必须是 final (在 Java 8 之后,实际上是隐式 final )的,这是为了 保证在匿
名内部类的⽣命周 期内,这些变量的值不会被改变。
匿名内部类的⽣命周 期取决于它的使⽤场景。如果它是在⼀个⽅法内部定义的,那么当⽅法执⾏
结束后,只要没有其他引⽤指向这个匿名内部类的对象,它就会被垃圾 回收。如果它是作为⼀个
类的成员变量定义的,那么它的⽣命周 期会和这个类的对象⽣命周 期相关。匿名内部类在定义的
同时就会被实例化,并且只能创建⼀个实例。因为它没有类名,所以不能像普通类⼀样通过 new
关键字在其他地⽅再次创建实例。
当只需要临时实现⼀个接⼝或者继承⼀个抽象类来提供特定的功能,并且这个实现类只使⽤⼀次
时,匿名内部类是⼀个很好的选择。它避免了为 ⼀个简单的功能定义⼀个完整的类,从⽽简化了
代码结构。
匿名内部内可以使 ⽤外部类的引⽤吗?静态的呢?
HashMap 和HashTable 区别?
HashMap 线程不安全,效率⾼⼀点,可以存储null 的key 和value ,null 的key 只能有⼀个,null 的value 可以有多个。默认初始容量为16 ,每次 扩充变为原来2倍。创建时如果给定了初始容量,
则扩充为2的幂次⽅⼤⼩。底层数据结构为数组+链表,插⼊元素后如果链表⻓度⼤于阈值(默
认为8),先判断数 组⻓度是否⼩于64 ,如果⼩于,则扩充数组,反之将链表转化为红⿊树,以
减少搜索时间。
HashTable 线程安全,效率低⼀点,其内 部⽅法基本都经过synchronized 修饰,不可以有null 的
key 和value 。默认初始容量为11 ,每次 扩容变为原来的2n+1 。创建时给定了初始容量,会直接
⽤给定的⼤⼩。底层数据结构为数组+链表。它基本被淘汰了,要保证线程安全可以⽤
ConcurrentHashMap 。
ConcurrentHashMap 是Java 中的⼀个线程安全的哈希表实现,它可以在多线程环境下并发地进
⾏读写操作,⽽不需要像传统的HashTable 那样在读写时加锁。ConcurrentHashMap 的实现原理
主要基于分段锁和CAS 操作。它将整个哈希表分成了多Segment (段),每个Segment 都类似于
⼀个⼩的HashMap ,它拥有⾃⼰的数组和⼀个独⽴的锁。在ConcurrentHashMap 中,读操作不
需要锁,可以直接对Segment 进⾏读取,⽽写操作则只需要锁定对应的Segment ,⽽不是整个
哈希表,这样可以⼤⼤提⾼并发性能。
讲⼀下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) 的时间复杂度。JVM
类加载过 程?
类从被加载到虚拟机内存开始,到卸载出内存为⽌,它的整个⽣命周 期包括以下 7 个阶段:

加载:通过类的全限定名(包名 + 类名),获取到该类的.class ⽂件的⼆进制字节流,将⼆进制字节流所代表的静态存储结构,转化为⽅法区运⾏时的数据结构,在内存中⽣成⼀个代表该类
的java.lang.Class 对象,作为⽅法区这个类的各种数据的访问⼊⼝
连接:验证、准备、解析 3 个阶段统称为连接。
验证:确保class ⽂件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class 类的正确性,不会危害到虚拟机的安全。验证阶段⼤致会完成以下四个阶段的检验动
作:⽂件格式校验、元数据验证、字节码验证、符号引⽤验证
准备:为类中的静态字段分配内存,并设置默认的初始值,⽐如int 类型初始值是0。被final
修饰的static 字段不会设置,因为final 在编译的时候就分配了
解析:解析阶段是虚拟机将常量池的「符号引⽤」直接替换为「直接引⽤」的过程。符号引
⽤是以⼀组符号来描述所引⽤的⽬标,符号可 以是任何 形式 的字⾯量,只要使⽤的时候可以
⽆歧义地定位到⽬标即可。直接引⽤可以是直接指 向⽬标的指针、相对偏移量或是⼀个能间
接定位到⽬标的句柄,直接引⽤是和虚拟机实现的内存布局相关的。如果有 了直接引⽤, 那
引⽤的⽬标必定已经存在在 内存中了 。
初始化:初始化是整个类加载过 程的最后⼀个阶段,初始化阶段简单来说就是执⾏类的构造器
⽅法,要注意的是这⾥的构造器⽅法() 并不是开发者写的,⽽是编译器⾃动⽣成的。使⽤:使⽤类或者创建对象
卸载:如果有 下⾯的情况,类就会被卸载:1. 该类所有的实例都已经被回收,也就是java 堆中不存在该类的任何 实例。2. 加载该类的ClassLoader 已经被回收。 3. 类对应的java.lang.Class 对
象没有任何 地⽅被引⽤,⽆法在任何 地⽅通过反射访问该类的⽅法。
双亲委派机制为什么 叫双亲?有什么 好处 ?
双亲委派模型,简单说就是当类加载器( Class-Loader )试图加载某个类型的时候,除⾮⽗加载器
找不到相应类型,否则尽量将这个任务代理给当前加 载器的⽗加载器去做。使⽤委派模型的⽬的
是避免重复加载 Java 类型。
“双亲” 并不是指有两个 ⽗⺟⼀样的⻆⾊。实际上,这⾥的 “双亲” 是⼀种形象的⽐喻,它是指除了
最顶层的启动类加载器( Bootstrap ClassLoader )外,每个类加载器都有⼀个⽗类加载器。当⼀个类加载器需要加载类时,它会先委托给它的⽗类加载器去尝试加载,这个过程就好像孩⼦(⼦加
载器) 先请求⽗⺟(⽗加载器) 帮忙做事⼀样,所以称为 “双亲委派”。
双亲委派机制好处 主要是:
防⽌核⼼ API 被篡改:Java 的核⼼类库(如 java.lang 包中的类)是由启动类加载器(Bootstrap ClassLoader )加载的。因为双亲委派机制的存在,⾃定义的类加载器在加载类
时,⾸先会将加载请求委托给⽗加载器。这就保证了像 java.lang.Object 这样的核⼼类不会被
⾃定义的同名 类随意替换。例如,如果没有双亲委派机制,恶意代码可能会定义⼀个⾃⼰的
类,并且通过⾃定义的类加载器加载,从⽽破坏 Java 程序的基本运⾏规则。
避免类的重复加载:由于类加载请求是由下向上委托,然后再从上 向下尝试加载。如果⽗加载
器已经成功加 载了某个类,⼦加载器就不会再重复加载该类,从⽽避免了因多次加载同⼀类⽽
可能导致的类型不⼀致等问题。例如,系统中有多个不 同的类加载器都可能需要加载
类,通过双亲委派机制,只有启动类加载器会加载这 个类,其他类加载
器会直接使⽤已经加载好的类。
** 保证类的⼀致性:** 在 Java 的运⾏环境中,对于同样全限定名的类,应该只有⼀份字节码被
加载并使⽤。双亲委派机制确保了在整个类加载体系中,类的加载是有层次和顺序的。例如,
在⼀个复杂的 Java 应⽤系统中,可能存在多个模块都依赖于同⼀个第三⽅库中的类。通过双亲
委派机制,这些模块所使⽤的该类是由同⼀个类加载器加载的,保证了在整个系统中该类的⼀
致性,使得不同模块之间可以正确地交互 和共享对象。
## class ⽂件和字节码⽂件的区别?概念上的区别:
Class ⽂件:在 Java 中, .class ⽂件是 Java 编译器( javac )将 .java 源⽂件编译后⽣成的⽂件格式。它是⼀种⼆进制⽂件,存储了 Java 程序的字节码指令、常量池、访问标志、类
名、⽅法名、字段名等各种信息。可以把 .class ⽂件看作是字节码的⼀种物理存储形式 ,是字节码的载体。
字节码(Byte - code ):字节码是⼀种中间形式 的机器语⾔,它是 Java 程序经过编译后产⽣的
指令集。字节码是⼀种⾼度抽象的、与具体机器硬件⽆关的指令代 码,它可以在任何 安装了
Java 虚拟机(JVM )的平台上执⾏。字节码指令是 JVM 能够理解和执⾏的基本单位,这些指令
类似于汇编语⾔指令,但更加抽象和⾼级。
Class ⽂件⽤途
存储和分发: .class ⽂件是 Java 程序的⼀种可存储和可 分发的形式 。当开 发⼀个 Java 项⽬
时,编译器会⽣成⼀系列的 .class ⽂件,这些⽂件可以被打包成 .jar ⽂件或者部署到服务器等环境中,供其他程序使⽤或者在运⾏时被加载。
跨平台基础: .class ⽂件的存在是 Java 实现 “⼀次编写,到处运⾏” 特性的基础之⼀。因为不
同的操作系统有不同的机器指令集,Java 编译器将 .java 源⽂件编译成与平台⽆关的 .class
⽂件,然后由各个平台上的 JVM 对 .class ⽂件进⾏解释执⾏或者编译成机器码执⾏。字节码⽤途
JVM 执⾏的指令集:字节码是 JVM 执⾏ Java 程序的实际指令。当 JVM 加载 .class ⽂件时,
它会解析 .class ⽂件中的字节码指令,并按照字节码指令的顺序执⾏操作。例如,当调⽤⼀个 Java ⽅法时,JVM 会读取⽅法表中的字节码指令,逐条执⾏这些指令来完成⽅法的功能。
动态加载和执⾏:字节码的动态特性使得 Java 可以实现⼀些⾼级的功能,如动态代理、字节码
增强等。通过在运⾏时动态⽣成字节码或者修改已有的字节码,可以实现诸如 AOP 等编程技
术,为 Java 程序提供了更⼤的灵活性。
弱引 ⽤和软引⽤的区别?
软引⽤是⼀种相对较强的引⽤类型。它所引⽤的对象在内存⾜够的情况下,不会被垃圾 回收器
回收;只有在内存不⾜时,才会被回收。这使得软引⽤适合⽤来缓存⼀些可能会被频繁使⽤,
但⼜不是必须⼀直存在的数据,例如缓存图⽚等资源。
弱引 ⽤是⼀种⽐较弱的引⽤类型。被弱引 ⽤关联的对象,只要垃圾 回收器运⾏,⽆论当前内存
是否充⾜,都会被回收。它主要⽤于解决⼀些对象的⽣命周 期管理问题,例如在哈希表中,如
果键是弱引 ⽤,当对象没有其他强引 ⽤时,就可以⾃动被回收,避免内 存泄漏。
⽹络
计⽹分层结构说⼀下?
OSI 七层模型
为了 使得多种设备能通过⽹络相互通信,和为了 解决各种不同设备在⽹络互联中的兼容性问题,
国际标准化组织 制定了开放式系统互联通信参考模型(Open System Interconnection Reference Model ),也就是 OSI ⽹络模型,该模型主要有 7 层,分别 是应⽤层、表⽰层、会话层、传输层、
⽹络层、数据链路层以及物理层。

每⼀层负责 的职能都不同,如下:
应⽤层,负责 给应⽤程序提供统⼀的接⼝;
表⽰层,负责 把数据转换成兼容另⼀个系统能识别的格式;
会话层,负责 建⽴、管理和终⽌表⽰层实体之间的通信会 话;
传输层,负责 端到端的数据传输;
⽹络层,负责 数据的路由、转发、分⽚;
数据链路层,负责 数据的封帧和差错检测,以及 MAC 寻址;
物理层,负责 在物理⽹络中传输数据帧;
由于 OSI 模型实在太复 杂,提出的也只是概念理论上的分层,并没有提供具体的实现⽅案。
事实上,我们⽐较常⻅,也⽐较实⽤的是四层模型,即 TCP/IP ⽹络模型,Linux 系统正是按照这
套⽹络模型来实现⽹络协议栈的。
TCP/IP 模型
TCP/IP 协议被组织 成四个概念层,其中有三层对 应于ISO 参考模型中的相应层。ICP/IP 协议族并不
包含物理层和数据链路层,因此它不能独⽴完成整个计算机⽹络系统的功能,必须与许多其他的
协议协同⼯作。TCP/IP ⽹络通常是由上到下分成 4 层,分别 是应⽤层,传输层,⽹络层和⽹络接
⼝层。

应⽤层 ⽀持 HTTP 、SMTP 等最终⽤⼾进程
传输层 处理主机到主机的通信(TCP 、UDP )
⽹络层 寻址和路由数据包(IP 协议)
链路层 通过⽹络的物理电线、电缆或⽆线信道移动⽐特
TCP 为什么 要三次握⼿?
三次握⼿的原因:
三次握⼿才可以阻⽌重复历史连接的初始化(主要原因)
三次握⼿才可以同步双⽅的初始序列号
三次握⼿才可以避免资源浪费
原因⼀:避免历史连接
我们来看看 RFC 793 指出的 TCP 连接使⽤三次握⼿的⾸要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
简单来说,三次握⼿的⾸要原因是为了 防⽌旧的重复连接初始化造成混乱。
我们考虑⼀个场景,客⼾端先发送了 SYN (seq = 90 )报⽂,然后客⼾端宕机了,⽽且这个 SYN报⽂还被⽹络阻塞了,服务并没有收到,接着客⼾端重启后 ,⼜重新向服务端建⽴连接,发送了
SYN (seq = 100 )报⽂(注意!不是重传 SYN
,重传的 SYN
的序列号是⼀样的)。
看看 三次握⼿是如何阻⽌历史连接的:

客⼾端连续发送多次 SYN (都是同⼀个四元组)建⽴连接的报⽂,在⽹络拥堵情况下:
⼀个「旧 SYN 报⽂」⽐「最新的 SYN 」 报⽂早到达了服务端,那么此时服务端就会回⼀个
SYN + ACK 报⽂给客⼾端,此报⽂中的确认号是 91 (90+1 )。
客⼾端收到后,发现⾃⼰期望 收到的确认号应该是 100 + 1 ,⽽不是 90 + 1 ,于是就会回 RST
报⽂。
服务端收到 RST 报⽂后,就会释放连接。
后续最新的 SYN 抵达了服务端后,客⼾端与服务端就可以正常的完成三次握⼿了。
上述中的「旧 SYN 报⽂」称为历史连接,TCP 使⽤三次握⼿建⽴连接的最主要原因就是防⽌「历
史连接」初始化了连接。
如果是两次握⼿连接,就⽆法阻⽌历史连接,那为什么 TCP 两次握⼿为什么 ⽆法阻⽌历史连接
呢?
我先直接说结论,主要是因为在两次握⼿的情况下,服务端没有中间状态给客⼾端来阻⽌历史连
接,导致服务端可能建⽴⼀个历史连接,造成资源浪费。
你想想 ,在两次握⼿的情况下,服务端在收到 SYN 报⽂后,就进⼊ ESTABLISHED 状态,意味着这
时可以给对⽅发送数据,但是客⼾端此时还没有进⼊ ESTABLISHED 状态,假设这次是历史连接,
客⼾端判断到此次 连接为历史连接,那么就会回 RST 报⽂来断开连接,⽽服务端在第⼀次握⼿的
时候就进⼊ ESTABLISHED 状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只
有在收到 RST 报⽂后,才会断开连接。

可以看到,如果采⽤两次握⼿建⽴ TCP 连接的场景下,服务端在向客⼾端发送数据前,并没有阻
⽌掉历史连接,导致服务端建⽴了⼀个历史连接,⼜⽩⽩发送了数据,妥妥 地浪费了服务端的资
源。
因此,要解 决这种现象,最好就是在服务端发送数据前,也就是建⽴连接之前,要阻⽌掉历史连
接,这样就不会造成资源浪费,⽽要实现这个功能,就需要三次握⼿。
所以,TCP 使⽤三次握⼿建⽴连接的最主要原因是防⽌「历史连接」初始化了连接。
原因⼆:同步双⽅初始序列号
TCP 协议的通信双⽅, 都必须维护⼀个「序列号」, 序列号是可靠传输的⼀个关键因素,它的作
⽤:
接收⽅可以去除重复的数据;
接收⽅可以根据数据包的序列号按序接收;
可以标识发送出去的数据包中, 哪些是已经被对⽅收到的(通过 ACK 报⽂中的序列号知道);
可⻅,序列号在 TCP 连接中占据着⾮常重要的作⽤,所以当客⼾端发送携带「初始序列号」的
SYN 报⽂的时候,需要服务端回⼀个 ACK 应答报⽂,表⽰客⼾端的 SYN 报⽂已被服务端成功接
收,那当服务端发送「初始序列号」给客⼾端的时候,依然也要得到客⼾端的应答回应,这样⼀
来⼀回,才能确保双⽅的初始序列号能被可靠的同步。

四次握⼿其实也能够可靠的同步双⽅的初始化序号,但由于第⼆步和第三步可以优 化成⼀步,所
以就成了「三次握⼿」。
⽽两次握⼿只保证了⼀⽅的初始序列号能被对⽅成功接收,没办法保证双⽅的初始序列号都能被
确认接收。
原因三:避免资源浪费
如果只有「两次握⼿」,当客⼾端发⽣的 SYN 报⽂在⽹络中阻塞,客⼾端没有接收到 ACK 报⽂,
就会重新发送 SYN ,由于没有第三次握⼿,服务端不清楚客⼾端是否收到了⾃⼰回复的 ACK 报
⽂,所以服务端每收到⼀个 SYN 就只能先主动建⽴⼀个连接,这会造成什么 情况呢?
如果客⼾端发送的 SYN 报⽂在⽹络中阻塞了,重复发送多次 SYN 报⽂,那么服务端在收到请求后
就会建⽴多个冗余的⽆效链接,造成不必要的资源浪费。

即两次握⼿会造成消息滞留情况下,服务端重复接受⽆⽤的连接请求 SYN 报⽂,⽽造成重复分配
资源
算法
反转链表
通过迭代遍历链表,在遍历过程中改变链表节点指针的指向,将当前节点的 next 指针指向前⼀个
节点,从⽽实现链表的反转。需要使⽤三个 指针来辅助操作,分别 指向当前节点、前⼀个节点和后⼀个节点。
class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
public class ReverseLinkedList {
public static ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr!= null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
}在上述代码中:
prev 初始化为 null ,代表反转后链表的末尾(也就是原链表的头节点反转后的前⼀个节点)。
curr 初始化为原链表的头节点 head ,然后在循环中,先保存当前节点的下⼀个节点到
nextTemp ,接着将当前节点的 next 指针指向前⼀个节点 prev ,再更新 prev 和 curr
的值,继续下⼀轮循环,直到遍历完整个链表,最后返回 prev ,它就是反转后链表的头节
点。
