操作系统
OPPO Java ⾯试
最近看到⼀些校招同学反馈 OPPO 开奖了
根据⼤家提供的信息,我这边 也整理了 OPPO 校招开发岗位的薪资情况,⽬前发现主要以下⼏个
档次:
20k x 15 + 1.2k x 12 = 31w
22k x 15 + 1.2k x12 = 34w
24k x 15 + 1.2k x12 = 37w
25k x 15 + 1.2k x12 = 38w
26k x 15 + 1.2k x12 = 40w整体看 OPPO 校招薪资还是很不错的,跟互联⽹公司基本差不多,年包基本在 30w+ ,其中 1.2k x 12 属于房补,今年的薪资情况基本跟去年差 不多。
也有不少同学好奇 OPPO ⾯试会问什么 ?
这次,就来分享⼀位同学的 OPPO 校招后端开发的⾯经,考察了 Java 并发+MySQL+ 操作系统+计
算机⽹络这⼏个⽅⾯的知识,有些地⽅考察的还是⽐较底层和细节。

线程池是什么 ,有什么 作⽤?
线程池是为了 减少频繁的创建线程和销毁线程带来的性能损耗,线程池的⼯作原理如下图:

线程池分为核⼼线程池,线程池的最⼤容量,还有等待任务的队列,提交⼀个任务,如果核⼼线
程没有满,就创建⼀个线程,如果满了,就是会加⼊等待队列,如果等待队列满了,就会增加线
程,如果达到最⼤线程数量,如果都达到最⼤线程数量,就会按照⼀些丢 弃的策略进⾏处理。
线程之间的状态如何变化 ?
源⾃《Java 并发编程艺术》 java.lang.Thread.State 枚举类中定义了 六种线程的状态,可以调⽤线程

Thread 中的getState() ⽅法获取当前线程的状态。Java 中的锁机制,分别 讲讲 ?
Java 中的锁是⽤于管理多线程并发访问共享资源的关键机制。锁可以确保在任意给定时间内只有⼀
个线程可以访问特定的资源,从⽽避免数据竞争和不⼀致性。Java 提供了多种锁机制,可以分为以
下⼏类:
内置锁(synchronized ):Java 中的 synchronized 关键字是内置锁机制的基础,可以⽤于⽅法
或代码块。当⼀个线程进⼊ synchronized 代码块或⽅法时,它会获取关联对象的锁;当线程离
开该代码块或⽅法时,锁会被释放。如果其他线程尝试获取同⼀个对象的锁,它们将被阻塞,
直到锁被释放。其中,syncronized 加锁时有⽆锁、偏向锁、轻量级锁和重量 级锁⼏个级别。偏
向锁⽤于当⼀个线程进⼊同步块时,如果没有任何 其他线程竞 争,就会使 ⽤偏向锁,以减少锁
的开销。轻量级锁使⽤线程栈上的数据结构,避免了操作系统级 别的锁。重量 级锁则涉及操作
系统级 的互斥锁。
ReentrantLock: java.util.concurrent.locks.ReentrantLock 是⼀个显式的锁类,提供了⽐
synchronized
更⾼级的功能,如可中断的锁等待、定时锁等待、公平锁选项等。
ReentrantLock
使⽤ lock() 和 unlock() ⽅法来获取和释放锁。其中,公平锁按照线程请求锁的顺序来分配锁,保证了锁分配的公平性,但可能增加锁的等待时间。⾮公平锁不保证锁分配
的顺序,可以减少锁的竞争,提⾼性能,但可能造成某些线程的饥饿 。
读写锁(ReadWriteLock ): java.util.concurrent.locks.ReadWriteLock 接⼝定义了 ⼀种锁,
允许多个读取者同时访问共享资源,但只允许⼀个写⼊者。读写锁通常⽤于读取远多于写⼊的
情况,以提⾼并发性。
乐观锁和悲观锁:悲观锁(Pessimistic Locking )通常指在访问数据前就锁定资源,假设最坏的
情况,即数据很可能被其他线程修改。 synchronized 和 ReentrantLock 都是悲观锁的例⼦。乐
观锁(Optimistic Locking )通常不锁定资源,⽽是在更新数 据时检查数据是否已被其他线程修
改。乐观锁常使⽤版本号或时间戳来实现。
⾃旋锁:⾃旋锁是⼀种锁机制,线程在等待锁时会持续循环检查锁是否可 ⽤,⽽不是放弃CPU
并阻塞。通常可以使 ⽤CAS 来实现。这在锁等待时间很短的情况下可以提⾼性能,但过度⾃旋
会浪费CPU 资源。
synchronized 和 lock 的实现原理是什么
synchronized 的实现原理:
字节码层⾯:通过 monitorenter 和 monitorexit 指令来实现加锁和解锁。
底层机制:基于 JVM 中的 Monitor 来实现,本质上依赖操作系统的互斥锁来保证共享数据不会
被并发访问。
锁升级:从偏向锁开始,随着锁竞争的不断升级,逐步演化⾄轻量级锁,最终变为重量 级锁。
Lock 的实现原理:
以 ReentrantLock 为例,通过 AQS (AbstractQueuedSynchronizer )来实现。AQS 使⽤⼀个 int
成员变量表⽰同步状态,通过内置的双向链表来完成资源获取线程的排队⼯作。
Lock 可以通过 tryAcquire 和 tryRelease 等⽅法来实现对同步状态的获取和释放。
当 AQS 需要阻塞或唤醒⼀个线程时,会使 ⽤ LockSupport ⼯具类来完成相应的⼯作。
AQS 的原理是什么 ?
AQS (QueuedSynchronizer )是⼀个⽤于构建锁和同 步容器的框架。它使⽤⼀个 int 成员变量表⽰
同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队⼯作。AQS 使⽤ CAS 对该同步状态
进⾏原⼦操作实现对其值的修改。
AQS 内部维护了⼀个 CLH 队列来管理锁。当线程获取同步状态失败(锁)时,AQS 会将当前线程
以及等待状态等信息构造成⼀个节点并将其加⼊同步队列,同时会阻塞当前线程。当同步状态释
放时,则会把节点中的线程唤醒,使其再 次尝试获取同步状态。

AQS 定义了两 种资源共享⽅式:独占锁和共享锁。独占锁在同⼀时刻只能有⼀个线程能获取到
锁,如 ReentrantLock 采⽤独占模式。共享锁允许多个线程同时获取锁,多个线程可同 时执⾏,如
Semaphore 、CountDownLatch 等。
在 AQS 中,节点的状态不再仅仅 是简单的布尔值,⽽是有更 复杂的定义。并且,AQS 中的队列是
双向链表,通过 Head 、Tail 头尾两个 节点来组成队列结构,通过 volatile 修饰保证可⻅性。Head
节点为已获取锁的节点,是⼀个虚拟节点,节点本⾝不持有具体的线程对象。获取不到同步状
态,会将节点进⾏⾃旋获取锁,⾃旋⼀定次数失败后会将线程阻塞,相对于 CLH 队列性能较好。
总结下来,⽤⼤⽩话说就是, AQS 是基于 CLH 队列,使⽤ volatile 修饰共享变量 state ,线程
通过 CAS ⽅式去改变 state 状态值,如果成功则 获取锁成功,失败则进⼊等待队列,等待被唤醒
的线程同步器框架。
Java 申请⼀块内存或者新建⼀个对象,他的地址 是在创建对象的时候就有的
吗?
对象的内存地址 是在创建对象的过程中由 JVM ⾃动分 配的。程序员对对 象的访问是通过引⽤实现
的,⽽不是通过直接操作内存地址 。以下是相关细节:
1、内存分配过程
对象创建:当你使 ⽤ new 关键字创建⼀个对象时,例如 MyClass obj = new MyClass(); ,JVM 会进⾏以下操作:
内存分配:JVM 从堆内存中分配⾜够的空间来存储这个对象的实例,包括对象的字段和元数据
(如对象的头部信息)。
执⾏构造函数:在分配内存后,JVM 会调⽤对象的构造函数来初始化对象。
2、内存地址 的获取
对象地址 :当对象被创建并且内存分配完成后,JVM 会返回⼀个对该对象的引⽤,虽然在 Java
中并没有直接展⽰内存地址 。但这个引⽤实际上包含了指向对象在内存中的实际地址 。
引⽤的表现:对于程序员来说,操作对象时使⽤的是引⽤,⽽不是直接操作内存地址 。这种设计提⾼了安全性和可 移植性,避免了许多常⻅的内存管理错误。
3、对象地址 的时机
在创建时:对象的内存地址 是在调⽤ new 关键字(或类似的内存分配⽅法)时确定的,在对
象创建完成之前,该地址 并不可⽤。
动态分配:内存的地址 是动态分配的,意味着每次 创建对象时,JVM 可能会给不同的对象分配
不同的内存地址 。这个地址在 程序运⾏时是 动态变化 的。
4. 、使⽤ System.identityHashCode()如果你希望通过对象引⽤查看其“地址 ”的哈希值(实际上是对象的哈希代码),可以使 ⽤
System.identityHashCode(obj) 。这个⽅法会返回对象的哈希码,可以⽤作对象在 JVM 中的唯⼀标识(并不等于内存地址 ,但可以作 为识别对象的依据)。
例⼦
public class MyClass {
public int value ;
public MyClass (int value ) {
this .value = value ;
}
public static void main (String [] args ) {
MyClass obj = new MyClass (10 );操作系统
进程之间的地址 空间是否可 以共享?
进程之间的地址 空间通常是不共享的。这是因为每个进程都有⾃⼰的虚拟地址 空间,操作系统为
每个进程分配独⽴的内存区域,以保 护进程之间的内存不受⼲扰。这样设计 可以有效地提⾼系统
的安全性和稳定性,防⽌⼀个进程意外或恶意地影响其他进程的运⾏。
尽管进程之间的地址 空间通常不共享,但有多种机制可以让它们进⾏通信:
共享内存:操作系统提供共享内存机制,允许多个进程访问同⼀块物理内存区域。这样,进程
可以像使⽤常规内存⼀样访问共享内存。这种⽅法⾮常⾼效,因为它避免了数据的复制。然
⽽,使⽤共享内存时,进程之间还是需要⼀些同步机制(如信号量、互斥锁)来避免数据竞争
问题。
管道与消息队列:进程可以通过管道(管道是单向的)和消息队列等机制进⾏数据交换。这些
机制提供了⼀种可靠的⽅式来传递信息,⽽不需要直接共享内存。
套接字:套接字允许不同主机上的进程进⾏通信,也可以在同⼀台机器上的不同进程之间进⾏
IPC 。
⽂件:进程还可以通过读写⽂件的⽅式交换数据。虽然这种⽅法速度较慢,但在⼀定情况下可
以保 证数据的持久性。
线程之间是否可 以共享内存空间?
可以的,线程可以共享全局变量、静态变量以及Heap (堆)中的动态分配的内存。

尽管线程之间可以共享内存空间,但这也引⼊了数据⼀致性的问题:
竞争条件:多个线程同时访问共享数据并试图修改同⼀数据时,会导致数据竞争和不⼀致性。
例如,⼀个线程在读取数据时,另⼀个线程可能正在修改该数据。
同步机制:为了 避免竞争条件,通常需要使⽤同步机制,如互斥锁(mutex )、读写锁
(rwlock )、信号量(semaphore )、条件变量等。这些机制可以确保在某⼀时刻只有⼀个线程
能够访问特定的共享资源,从⽽维护数据的⼀致性。
在设计 多线程程 序时,确保对共享数据的访问是线程安全的⾮常重要。程序员需要采取适当的⽅
法来保护共享资源,以避免潜在的问题。常⽤的⽅法包括:
使⽤互斥锁:将对 共享数据的访问代码块⽤互斥锁包围。
使⽤原⼦操作:某些操作可以使 ⽤寻址机制直接在硬件层⾯实现原⼦化,降低线程间的⼲扰。
线程局部存储(Thread Local Storage ):在某些情形下,可以让每个线程有⾃⼰的数据副本,
以减少共享带来的复杂性。
操作系统如何调度线程?
操作系统可以采⽤不同的调度策略,主要有以下⼏种:
** 先来先服务:** 按照线程进⼊就绪队列的顺序进⾏调度,较为简单,但可能导致“饥饿 ”现象
(即⻓时间⽆法得到CPU 时间的线程)。
** 短作业优先:** 优先调度执⾏时间短的线程,这有助于提⾼系统的吞吐 量,但缺点是可能导致
⻓作业被饿死。
** 轮转 调度:** 为各个线程分配固定的时间⽚(time slice ),如在多任务环境中,每个线程按照
顺序获得CPU 执⾏权,时间⽚耗尽后切换到下⼀个线程。
优先级调度:根据线程的优先级进⾏调度,⾼优先级的线程先执⾏。这种调度可结合时间⽚,
会造成低优 先级线 程可能较⻓时间得不到执⾏。
** 多级反馈队列:** 使⽤多个就绪队列,线程可以根据运⾏情况在不同队列间动态变化 ,以提⾼
系统的灵活性和响 应时间。
当操作系统从⼀个线程切换到另⼀个线程时,会发⽣上下 ⽂切换(Context Switch ),其过程⼀般
包括以下步骤:
保存当前线程的上下 ⽂:在内存中保存当前线程的 CPU 寄存 器状态、程序计数器、堆栈指针以
及其他此线程需要的信息。
选择下⼀个线程:根据调度策略选择下⼀个要执⾏的线程。
加载下⼀个线程的上下 ⽂:从内存中恢复所选线程的 CPU 寄存 器状态、程序计数器、堆栈指针
等信息,使得该线程能够继续执⾏。
转移控制:通过调度程序恢复新的线程运⾏,操作系统进⼊该线程的执⾏状态。
⼀个多核CPU ,跑多个线程,多个线程如何抢夺CPU 资源?
时间⽚轮转 调度:操作系统会将 CPU 的时间划分 为多个时间⽚,每个时间⽚是⼀个很⼩的时间间隔 (例如,⼏⼗毫秒)。当多个线程处于就绪状态时,操作系统会按照⼀定的顺序为每个线程
分配⼀个时间⽚。在这个时间⽚内,线程可以在 CPU 上运⾏,⼀旦时间⽚⽤完,操作系统会暂
停该线程的执⾏,并将 CPU 资源分配给下⼀个就绪的线程。例如,在 Linux 系统中,完全公 平
调度器( CFS )就是⼀种常⻅的调度器, 它试图让每个线程都能公平地获得 CPU 时间,根据线
程的虚拟运⾏时间(vruntime )来决定哪个线程应该被调度,虚拟运⾏时间是线程在 CPU 上运
⾏的时间的⼀种加权表⽰,权重考虑了线程的优先级等因素。
优先级调度:线程可以被赋予不 同的优先级。⾼优先级的线程会⽐低优 先级的线程更优先获得
CPU 资源。在某些情况下,⾼优先级的线程会抢占低优 先级线 程正在使⽤的 CPU 资源。不过,
不同操作系统对优先级的处理⽅式可能不同。例如,在 Windows 操作系统中,优先级从 0 到
31 ,0 为最低优 先级,31 为最⾼优先级。⾼优先级的线程会优 先获得 CPU 资源,并且在⾼优先
级线 程处于就绪状态时,可能会中断低优 先级线 程的执⾏。
⻚⾯的换⼊换出机制是怎么的?
在操作系统中,由于虚拟地址 空间往往 ⼤于实际物理内存⼤⼩,所以会 存在虚拟地址 到物理地址
的映射重叠。
当进程当前访问的内容不在物理内存中时,就需要从磁盘导⼊,这称为内存换⼊;
⽽当分配给进程的内存⻚有限,需要将不活跃的内存换出到 磁盘,以腾出空间给当前需要的⻚
⾯,这称为内存换出。

内存换⼊时,进程根据逻辑地址 访问内存,逻辑地址 转换为虚拟地址 ,再根据虚拟地址 得出⻚号
查⻚表得出物理⻚框号。若⻚表显⽰该⻚号未映射到具体物理⻚框号或⻚内容不在内存中,会产
⽣缺⻚中断,中断处理程序负责 将内容从磁盘中导⼊并更新⻚表。
内存换出的关键在于选择哪⼀⻚,常⻅的⻚⾯置换算法有 FIFO (先进先出)、OPTIMAL (最优算
法)、LRU (最近最少使⽤)等。
中断是什么 ?
CPU 停下当前的⼯作任 务,去处理其他事 情,处理完后回来继续执⾏刚才的任务,这⼀过程便是中
断。
中断分为外部中断和内部中断:
外部中断分为可屏蔽中断和不可屏蔽中断:
可屏蔽中断:通过INTR 线向CPU 请求的中断,主要来⾃外部设备如 硬盘,打印机,⽹卡等。此
类中断并不会影响系统运⾏,可随时处理,甚⾄不处理,所以名为可屏蔽。
不可屏蔽中断:通过NMI 线向CPU 请求的中断,如电源掉电,硬件线路故障等。这⾥不可屏蔽
的意思不是不可以屏蔽,不建议屏蔽,⽽是问题太⼤,屏蔽不了 ,不能屏蔽的意思。注:INTR
和NMI 都是CPU 的引脚
内部中断分为陷阱 、故障、终⽌:
陷阱 :是⼀种有意的,预先安排的异常事件,⼀般是在编写程序时故意设下的陷阱 指令,⽽后
执⾏到陷阱 指令后,CPU 将会调⽤特定程序进⾏相应的处理,处理结束后返回到陷阱 指令的下
⼀条指令。如系统调⽤,程序调试功能等。如printf 函数,最底层的实现中会有⼀条int 0x80 指
令,这就是⼀条陷阱 指令,使⽤0x80 号中断进⾏系统调⽤。
故障:故障是在引起故障的指令被执⾏,但还没有执⾏结束时,CPU 检测到的⼀类的意外事
件。** 出错时交由故障处理程序处理,如果能处理修正这个错误,就将 控制返回到引起故障的
指令即CPU 重新执这条指令。如果不能处理就报错。常⻅的故障为缺⻚,当CPU 引⽤的虚拟地址对应的物理⻚不存在时就会发⽣故障。缺⻚异常是能够修正的,有着专⻔的缺⻚处理程序,
它会将缺失的物理⻚从磁盘中重新调进主存。⽽后再次执⾏引起故障的指令时便能够顺利执⾏
了。
终⽌:执⾏指令的过程中发⽣了致命错误,不可修复,程序⽆法继续运⾏,只能终⽌,通常会
是⼀些硬件的错误。终⽌处理程序不会将控制返回给原程序,⽽是直接终⽌原程序
物理内存不够,为什么 程序可以很⼤?
应⽤程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内
存。
当应⽤程序读写了这块虚拟内存,CPU 就会去访问这个虚拟内存, 这时会发现这个虚拟内存没有
映射到物理内存, CPU 就会产⽣缺⻚中断,进程会从⽤⼾态切换到内核态,并将缺⻚中断交给内
核的 Page Fault Handler (缺⻚中断函数)处理。
缺⻚中断处理函数会看是否有空闲的物理内存:
如果有 ,就直接分配物理内存,并建⽴虚拟内存与物理内存之间的映射关系。
如果没有空闲的物理内存,那么内核就会开始进⾏回收内存的⼯作,如果回收内存⼯作结束
后,空闲的物理内存仍然⽆法满⾜此次 物理内存的申请,那么内核就会放最后的⼤招了触发
OOM (Out of Memory )机制。
32 位操作系统和 64 位操作系统的虚拟地址 空间⼤⼩是不同的,在 Linux 操作系统中,虚拟地址 空
间的内部⼜被分为内核空间和⽤⼾空间两部分,如下所⽰:

通过这 ⾥可以看出:
32 位系统的内核空间占⽤ 1G ,位于最⾼处,剩下的 3G 是⽤⼾空间;因为进程最⼤只能申请 3GB ⼤⼩的虚拟内存,所以直接申请 8G 内存,会申请失败。
64 位系统的内核空间和⽤⼾空间都是 128T ,分别 占据整个内存空间的最⾼和最低处,剩下的
中间部分是未定义的。因为进程最⼤只能申请 128 TB ⼤⼩的虚拟内存,即使物理内存只有
4GB ,申请 8G 内存也是没问题,因为申请的内存是虚拟内存,等这块虚拟内存被访问了,因为
物理空间不够,就会发⽣ OOM 。
缺⻚中断是什么 ,操作系统会⼲什么 ?
当 CPU 访问的⻚⾯不在物理内存时,便会 产⽣⼀个缺⻚中断,请求操作系统将所缺⻚调⼊到物理
内存。那它与⼀般中断的主要区别在于:
缺⻚中断在指令执⾏「期间」产⽣和处理中断信号,⽽⼀般中断在⼀条指令执⾏「完成」后检
查和处理中断信号。
缺⻚中断返回到该指令的开始重新执⾏「该指令」,⽽⼀般中断返回回 到该指令的「下⼀个指
令」执⾏。
我们来看⼀下缺⻚中断的处理流程,如下图:

在 CPU ⾥访问⼀条 Load M 指令,然后 CPU 会去找 M 所对应的⻚表项。
如果该⻚表项的状态位是「有效的」,那 CPU 就可以直接去访问物理内存了,如果状态位是
「⽆效的」,则 CPU 则会发送缺⻚中断请求。
- 操作系统收到了缺⻚中断,则会执⾏缺⻚中断处理函数,先会查找该⻚⾯在磁盘中的⻚⾯的位
置。
- 找到磁盘中对应的⻚⾯后,需要把该⻚⾯换⼊到物理内存中,但是在换⼊前,需要在物理内存
中找空闲⻚,如果找到空闲⻚,就把⻚⾯换⼊到物理内存中。
⻚⾯从磁盘换⼊到物理内存完 成后,则把⻚表项中的状态位修改为「有效的」。
最后,CPU 重新执⾏导致缺⻚异常的指令。
上⾯所说的过程,第 4 步是能在物理内存找到空闲⻚的情况,那如果找不到呢?
找不到空闲⻚的话,就说明此时内存已满了,这时候,就需要「⻚⾯置换算法」选择⼀个物理
⻚,如果该物理⻚有被修改过(脏⻚),则把它换出到 磁盘,然后把该被置换出去的⻚表项的状态
改成「⽆效的」,最后把正在访问的⻚⾯装⼊到这个物理⻚中。
这⾥提⼀下,⻚表项通常有如下图的字段:

那其中:
状态位:⽤于表⽰该⻚是否有效,也就是说是否在物理内存中,供程序访问时参考。
访问字段:⽤于记录该⻚在⼀段时间被访问的次数,供⻚⾯置换算法选择出⻚⾯时参考。
修改位:表⽰该⻚在调⼊内存后是否有被修改过,由于内存中的每⼀⻚都在磁盘上保留⼀份副
本,因此,如果没有修改,在置换该⻚时就不需要将该⻚写回到磁盘上,以减少系统的开销;
如果已经被修改,则将该⻚重写到磁盘上,以保 证磁盘中所保留的始终是最新的副本。
硬盘地址 :⽤于指出该⻚在硬盘上的地址 ,通常是物理块号,供调⼊该⻚时使⽤。
这⾥我整理了虚拟内存的管理整个流程,你可以从下 ⾯这张图看到:

所以,⻚⾯置换算法的功能是,当出现缺⻚异常,需调⼊新⻚⾯⽽内存已满时,选择被置换的物
理⻚⾯,也就是说选择⼀个物理⻚⾯换出到 磁盘,然后把需要访问的⻚⾯换⼊到物理⻚。
MySQL
MySQL 的索引结构是什么 ,为什么 ⽤B+ 树
MySQL InnoDB 引擎是⽤了B+ 树作为了 索引的数据结构,原因有:

B+ 树的⾮叶⼦节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相⽐存储
即存索引⼜存记录的 B 树,B+ 树的⾮叶⼦节点可以存放更多的索引,因此 B+ 树可以⽐ B 树更
「矮胖」,查询底层节点的磁盘 I/O 次数会更少。
B+ 树有⼤量的冗余节点(所有⾮叶⼦节点都是冗余索引),这些冗余索引让 B+ 树在插⼊、删
除的效率都更⾼,⽐如删除根节点的时候,不会像 B 树那样会发⽣复杂的树的变化 ;
B+ 树叶⼦节点之间⽤链表连接了起来,有利于范围查询,⽽ B 树要实现范围查询,因此只能通
过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。
⽹络
TCP 丢包了会怎么样?
重传机制:当TCP 发送数据包时,会启动⼀个定时器来监控该数据包的确认(ACK )是否会收
到。如果在设定的时间内没有收到确认,TCP 会认为数据包已经丢失,并会重新发送该数据包。
流量控制:TCP 通过滑动窗⼝机制来控制数据流的速率。当⽹络丢包造成拥 塞时,TCP 会减⼩窗
⼝⼤⼩,从⽽减缓数据发送速 率,避免进⼀步的丢包。
拥塞控制:TCP 使⽤各种算法(如慢启动、拥塞避免、快重传和快恢复)来检测和处理⽹络拥塞
情况。通过这 些算法,TCP 能够调整其发送速 率以适应⽹络的当前状态,减少丢包的发⽣。
拥塞避免的⽅法有哪些?
TCP 拥塞控制是为了 防⽌⽹络过载 ⽽采取的⼀系列措施,这些措施能够动态调整数 据传输速率以适
应⽹络状态。TCP 的拥塞控制主要包括以下⼏种算法和策略:
慢启动:当 TCP 连接建⽴或发⽣丢包重传时,TCP 从⼀个相对较⼩的拥塞窗⼝(cwnd )开始。
这⼀窗⼝随着每次 成功收到确认(ACK )⽽指数增⻓(每收到⼀个 ACK ,窗⼝⼤⼩增加 1),⽬
的是快速探测可⽤带宽。
拥塞避免:当拥塞窗⼝达到⼀个阈值(ssthresh, slow start threshold )后,TCP 进⼊拥塞避免阶
段。在这个阶段,拥塞窗⼝以线性增⻓⽅式增加(每经过⼀个轮次,增加 1,即每 RTT 增加
1),这样可以减少发⽣拥塞的⻛险。
快速重传(Fast Retransmit ):发送⽅在未收到某个数据包的 ACK 时,收到三个 重复的 ACK
(即接收⽅多次确认相同的数据包)时,发送⽅会⽴即重传缺失的数据包,⽽不是等待重传定
时器超时。这有助于减少由于丢 包导致的延迟。
快速恢复(Fast Recovery ):在快速重传机制之后,TCP 会进⼊快速恢复阶段。此时,ssthresh
被设置为当前的 cwnd 值的⼀半,接着 cwnd 被设置为 ssthresh 加上三个 未确认的 MSS (最⼤
段⼤⼩)。这样,TCP 在丢包后不会完全降低其传输速率,⽽是球⽴⼀个新的⽬标以便 快速返回
正常状态。
⽹络中序号为 101 数据发送失败了,102 ,103 发送成功了,102 , 103 还会
重新发送吗?
序号为 102 和 103 的数据包不会被重传,因为它们已经成功发送并且接收⽅已经接收了它们。但
是,由于序号为 101 的数据包未成功接收,接收⽅会继续期望 接收 101 ,当收到了 102 和 103 数
据的时候,会重复响应 101 数据的ACK ,让发送⽅重传 101 。
虽然 102 和 103 能被接收⽅成功接收,但是应⽤层此时还是⽆法读到这个数据,因为这时候 101
是丢失的,代表这时候的数据并不是连续的,应⽤层只能按序的读取数据,如果数据是乱序的,
就⽆法读取。
四次挥⼿的 timewait 的作⽤是什么
TIME_WAIT 状态的存在是为了 确保⽹络连接的可靠关闭。只有主动发起关闭连接的⼀⽅(即主动
关闭⽅)才会有 TIME_WAIT 状态。
TIME_WAIT 状态的需求主要有两个 原因:
防⽌具有相同「四元组」的「旧」数据包被收到:在⽹络通信中,每个 TCP 连接都由源 IP 地址、源端⼝号、⽬标 IP 地址 和⽬标端⼝号这四个元素唯⼀标识,称为「四元组」。当⼀⽅主动
关闭连接后,进⼊ TIME_WAIT 状态,它仍然可以接收到⼀段时间内来⾃对⽅的延迟数据包。这
是因为⽹络中可能存在被延迟传输的数据包,如果没有 TIME_WAIT 状态的存在,这些延迟数据
包可能会被错误地传递给新的连接,导致数据混乱。通过保持 TIME_WAIT 状态,可以防⽌旧的
数据包⼲扰新的连接。
保证「被动关闭连接」的⼀⽅能被正确关闭:当连接的被动关闭⽅接收到主动关闭⽅的 FIN 报
⽂(表⽰关闭连接),它需要发送⼀个确认 ACK 报⽂给主动关闭⽅,以完成连接的关闭。然
⽽,⽹络是不可靠的,ACK 报⽂可能会在传输过 程中丢 失。如果主动关闭⽅在收到 ACK 报⽂之
前就关闭连接,被动关闭⽅将⽆法正常完成连接的关闭。TIME_WAIT 状态的存在确保了被动关
闭⽅能够接收到最后的 ACK 报⽂,从⽽帮助其正常关闭连接。
场景题
如何设计 ⼀个可重⼊的分布式锁,⽤什么 结构设计 ?
可重⼊锁允许同⼀线程在持有锁的情况下多次获得锁⽽不会产⽣死锁。当线程请求锁时,如果它
已经拥有了该锁,则可以直接获得锁,锁的计数器会增加。在释放锁时,计数器会减少,只有当
计数器为零时,锁才会真正释放。
在分布式系统中,通常会使 ⽤诸如 Redis 、Zookeeper 或 etcd 等组件来实现锁的管理。下⾯以
Redis 为例,简单描述如何设计 ⼀个可重⼊的分布式锁。
设计 要素
锁的标识符(Lock ID ):⽤于标识锁的唯⼀性。
持有者标识(Owner ID ):线程或进程的唯⼀标识符,通常是线程的 ID 或进程的 ID 。
计数器:记录当 前持有锁的次数。
过期时间:为了 避免死锁,锁需要设定⼀个合理的超时时 间。
我们可以在 Redis 中使⽤⼀个哈希表或简单的键值对来存储锁的状态。使⽤⼀个 key (如 lock:
<resource> )来表⽰锁,包含以下字段:owner : 当前持有锁的线程/进程标识符
count : 当前计数器, 表⽰获得锁的次数
expires_at : 锁的到期时间,⽤于防⽌死锁⽰例数据结构
{
"key": "lock:resource_1",
"value": {
"owner": "thread_id_or_process_id",
"count": 3,
"expires_at": "2023-10-01T12:00:00Z"
}
}以下是围绕这个锁结构的基本操作:
1. 获取锁 (Lock Acquisition) :尝试设 置 lock: 的值,如果这个 key 不存在(即没有锁),那么可以
创建该 key ,并设置 owner 为当前线程/进程的唯⼀ID ,count 设为 1,expires_at 设为当前时间加上锁的过期时间。 如果该 key 已存在且 owner 是当前线程/进程的 ID ,则增加 count 并更新
expires_at 。
2. 释放锁 (Lock Release) :检查当前的 owner 是否是当前线程/进程的 ID 。如果是,减少 count 。如果 count 减少到 0,则删 除该 key 。 如果在持有锁的情况下,锁已过期,系统会根据
expires_at 检查锁是否可 释放,避免死锁。
3. 锁续期 (Lock Renewal) :如果当前线程在执⾏过程中需要继续持有锁,可以在逻辑处理中重新
设置 expires_at.- 超时与故障恢复:可以设置锁的⾃动超时时 间,例如,设定⼀个最⼤持锁时间,超时后锁⾃动
释放。这可以在⼀定程度上防⽌死锁的情况。
整体流程⽰例
获取锁:
线程A调⽤获取锁的 API 。如果成功,返回锁的状态。
如果线程B也尝试获取同⼀把锁,则会返回锁已被占⽤。
释放锁:
线程A在完成任务时调⽤释放锁的 API ,递减 count 字段。
如果 count 达到 0,删除锁并释放资源。
续约锁:
线程A在处理过程中可以根据需要续约锁,更新 expires_at 。
