常⻅的Map 及哪些是线程安全的?
唯品会 Java ⾯试
唯品会开发岗的校招薪资范围⼤概是 17k-21k x 14 ,年薪 24w 〜29w ,不过感觉今年唯品会校招没
有怎么招开发岗的⼈,看到⾯试的⼈不算多,可能也是因为公司业务趋向稳定,没有新的增⻓空
间,就没去扩从开发⼈员。
有位⾯了唯品会后端开发岗的同学被拷打了⼀⼩时,⾯试官真的很nice ,我感觉我都没答出来,说
了这辈 ⼦最多的不好意思,但是⾯试官真的很好很好,每⼀题都会适当引 导我,可惜⾃⼰太菜,
有些时候 get 不到⾯试官的点。
主要是拷打了Java 集合、Java 并发、⽹络、场景、算法这些内容。

常⻅的Map 及哪些是线程安全的?

常⻅的Map 集合(⾮线程安全):
HashMap 是基于哈希表实现的 Map ,它根据键的哈希值来存储和获取键值对,JDK 1.8 中是⽤
数组+链表+红⿊树来实现的。 HashMap 是⾮线程安全的,在多线程环境下,当多个线程同时对
HashMap 进⾏操作时,可能会导致数据不⼀致或出现死循环等问题。⽐如在扩容时,多个线程
可能会同时修改哈希表的结构,从⽽破坏数据的完整性。
LinkedHashMap 继承⾃ HashMap ,它在 HashMap 的基础上,使⽤双向链表维护了键值对的插⼊顺序或访问顺序,使得迭代顺序与插⼊顺序或访问顺序⼀致。由于它继承⾃ HashMap ,在多线
程并发访问时,同样会出现与 HashMap 类似的线程安全问题。
TreeMap 是基于红⿊树实现的 Map ,它可以对键进⾏排序,默认按照⾃然顺序排序,也可以通
过指定的⽐较器进⾏排序。 TreeMap 是⾮线程安全的,在多线程环境下,如果多个线程同时对
TreeMap 进⾏插⼊、删除等操作,可能会破坏红⿊树的结构,导致数据不⼀致或程序出现异
常。
常⻅的Map 集合(线程安全):
Hashtable 是早期 Java 提供的线程安全的 Map 实现,它的实现⽅式与 HashMap 类似,但在⽅
法上使⽤了 synchronized 关键字来保证线程安全。通过在每个可能修改 Hashtable 状态的⽅
法上加上 synchronized 关键字,使得在同⼀时刻,只能有⼀个线程能够访问 Hashtable 的这
些⽅法,从⽽保证了线程安全。
ConcurrentHashMap 在 JDK 1.8 以前采⽤了分段锁等技术来 提⾼并发性能。在
ConcurrentHashMap 中,将数据分成多个段(Segment ),每个段都有⾃⼰的锁。在进⾏插⼊、
删除等操作时,只需要获取相应段的锁,⽽不是整个 Map 的锁,这样可以允许多个线程同时访
问不同的段,提⾼了并发访问的效率。在 JDK 1.8 以后是通过 volatile + CAS 或者 synchronized
来保证线程安全的
如何对map 进⾏快速遍 历?
使⽤for-each 循环和entrySet() ⽅法:这是⼀种较为常⻅和简洁的遍历⽅式,它可以同时获取Map 中的键和值
import java .util .HashMap ;
import java .util .Map ;
public class MapTraversalExample {
public static void main (String [] args ) {
Map <String , Integer > map = new HashMap <>();
map .put ("key1" , 1);
map .put ("key2" , 2);
map .put ("key3" , 3);
// 使用 for-each 循环和 entrySet() 遍历 Map
for (Map .Entry <String , Integer > entry : map .entrySet ()) {
System .out .println ("Key: " + entry .getKey () + ", Value: " + entry .getValue ())
}
}
}
import java.util.HashMap;
import java.util.Map;
public class MapTraversalExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("key1", 1);
map.put("key2", 2);
map.put("key3", 3);
// 使用for-each循环和keySet()遍历Map的键
for (String key : map.keySet()) {
System.out.println("Key: " + key + ", Value: " + map.get(key));
}
}
}
使⽤for-each 循环和keySet() ⽅法:如果只需要遍历 Map 中的键,可以使 ⽤ keySet() ⽅法,这种⽅式相对简单,性能也较好。
使⽤迭代器:通过获取Map 的entrySet() 或keySet() 的迭代器, 也可以实现对Map 的遍历,这种⽅式在需要删除元素等操作时⽐较有⽤。
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
public class MapTraversalExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("key1", 1);
map.put("key2", 2);
map.put("key3", 3);
// 使用迭代器遍历Map
Iterator<Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Entry<String, Integer> entry = iterator.next();
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue())
}
使⽤ Lambda 表达式和forEach() ⽅法:在 Java 8 及以上版本中,可以使 ⽤ Lambda 表达式和
forEach() ⽅法来遍历 Map ,这种⽅式更加简洁和函数式。
import java.util.HashMap;
import java.util.Map;
public class MapTraversalExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("key1", 1);
map.put("key2", 2);
map.put("key3", 3);
// 使用Lambda表达式和forEach()方法遍历Map
map.forEach((key, value) -> System.out.println("Key: " + key + ", Value: " + valu
}
}使⽤Stream API :Java 8 引⼊的 Stream API 也可以⽤于遍历 Map ,可以将 Map 转换为流,然
后进⾏各种操作。
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class MapTraversalExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("key1", 1);
map.put("key2", 2);
map.put("key3", 3);
// 使用Stream API遍历Map
map.entrySet().stream()
.forEach(entry -> System.out.println("Key: " + entry.getKey() + ", Value: " + e
// 还可以进行其他操作,如过滤、映射等
Map<String, Integer> filteredMap = map.entrySet().stream()
System.out.println(filteredMap);
}
}常⻅的List ?有没有线程安全的List ?

常⻅的List 集合(⾮线程安全):
ArrayList 基于动态数组实现,它允许快速的随机访问,即通过索引访问元素的时间复杂度为
O (1) 。在添加和删除元素时,如果操作位 置不是列表末尾,可能需要移动⼤量元素,性能相对较低。适⽤于需要频繁随机访问元素,⽽对插⼊和删除操作性能要求不⾼的场景,如数据的查
询和展⽰等。
LinkedList 基于双向链表实现,在插⼊和删除元素时,只需修改链表的指针,不需要移动⼤量
元素,时间复杂度为 O (1) 。但随机访问元素时,需要从链表头或链表尾开始遍历,时间复杂度
为 O (n) 。适⽤于需要频繁进⾏插⼊和删除操作的场景,如队列、栈等数据结构的实现,以及需要在列表中间频繁插⼊和删除元素的情况。
常⻅的List 集合(线程安全):
Vector 和 ArrayList 类似,也是基于数组实现。 Vector 中的⽅法⼤多是同步的,这使得它
在多线程环境下可以保 证数据的⼀致性,但在单线程环境下,由于同步带来的开销,性能会略
低于 ArrayList 。
CopyOnWriteArrayList 在对列表进⾏修改(如添加、删除元素)时,会创建⼀个新的底层数
组,将修改操作应⽤到新数 组上,⽽读操作仍然在原数组上进⾏,这样可以保 证读操作不会被
写操作阻塞,实现了读写分离,提⾼了并发性能。适⽤于读操作远远 多于写操作的并发场景,
如事件监听列表等,在这种场景下可以避免⼤量的锁竞争,提⾼系统的性能和响 应速度。
list 可以⼀边遍历⼀边修改元素吗?
在 Java 中, List 在遍历过程中是否可 以修改元素取决于遍历⽅式和具体的 List 实现类,以下
是⼏种常⻅情况:
使⽤普通for 循环遍历:可以在遍历过程中修改元素,只要修改的索引不超出 List 的范围即
可。
import java.util.ArrayList;
import java.util.List;
public class ListTraversalAndModification {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 使用普通for循环遍历并修改元素
for (int i = 0; i < list.size(); i++) {
list.set(i, list.get(i) * 2);
}
System.out.println(list);
}
}使⽤foreach 循环遍历:⼀般不建议在 foreach 循环中直接修改正在遍历的 List 元素,因为这
可能会导致意外的结果或 ConcurrentModificationException 异常。在 foreach 循环中修改元
素可能会破坏迭代器的内部状态,因为 foreach 循环底层是基于迭代器实现的,在遍历过程中
修改集合结构,会导致迭代器的预期结构和实际结构不⼀致。
import java.util.ArrayList;
import java.util.List;
public class ListTraversalAndModification {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
// 使用foreach循环遍历并尝试修改元素,会抛出ConcurrentModificationException异常
for (Integer num : list) {
list.set(list.indexOf(num), num * 2);
}
System.out.println(list);
}
}使⽤迭代器遍历:可以使 ⽤迭代器的 remove ⽅法来删除元素,但如果要修改元素的值,需要
通过迭代器的 set ⽅法来进⾏,⽽不是直接通过 List 的 set ⽅法,否则也可能会抛出
ConcurrentModificationException 异常。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListTraversalAndModification {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 使用迭代器遍历并修改元素
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer num = iterator.next();
if (num == 2) {
// 使用迭代器的set方法修改元素
iterator.set(4);
}
}
System.out.println(list);
}
}对于线程安全的 List ,如 CopyOnWriteArrayList ,由于其采⽤了写时复制的机制,在遍历的同
时可以进⾏修改操作,不会抛出 ConcurrentModificationException 异常,但可能会读取到旧的数
据,因为修改操作是在新的副本上进⾏的。
list 如何快速删除某个指定下标的元素?
ArrayList 提供了 remove(int index) ⽅法来删除指定下标的元素,该⽅法在删除元素后,会将
后续元素向前移动,以填补被 删除元素的位置。如果删除的是列表末尾的元素,时间复杂度为 O(1) ;如果删除的是列表中间的元素,时间复杂度为 O (n) ,n 为列表中元素的个数,因为需要移动后续的元素。⽰例代 码如下:
import java.util.ArrayList;
import java.util.List;
public class ArrayListRemoveExample {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 删除下标为1的元素
list.remove(1);
System.out.println(list);
}
}
LinkedList 的 remove(int index) ⽅法也可以⽤来删除指定下标的元素。它需要先遍历到指定下
标位置,然后修改链表的指针来删除元素。时间复杂度为 O (n) ,n 为要删除元素的下标。不过,如果已知要删除的元素是链表的头节点或尾节点,可以直接通过修改头指针或尾指针来实现删
除,时间复杂度为 O (1) 。⽰例代 码如下:
import java.util.LinkedList;
import java.util.List;
public class LinkedListRemoveExample {
public static void main(String[] args) {
List<Integer> list = new LinkedList<>();
list.add(1);
list.add(2);
list.add(3);
// 删除下标为1的元素
System.out.println(list);
}
}opyOnWriteArrayList 的 remove ⽅法同样可以删除指定下标的元素。由于
CopyOnWriteArrayList 在写操作时会创建⼀个新的数组,所以删除操作的时间复杂度取决于数组
的复制速度,通常为 O (n) ,n 为数组的⻓度。但在并发环境下,它的删除操作不会影响读操作,具有较好的并发性能。⽰例代 码如下:
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListRemoveExample {
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 删除下标为1的元素
list.remove(1);
System.out.println(list);
}
}常⻅的队列有哪些及应⽤场景?
顺序队列是利⽤⼀组连续的存储单元依次存放从队头到队尾的元素,同时设置两个 指针,⼀个
指向队头元素的位置,称为队头指针;另⼀个指向队尾元素的下⼀个位置,称为队尾指针。实
现简单,空间利⽤率低,当队尾指针到达数组末尾时,即使前⾯有空闲空间,也可能⽆法继续
插⼊元素,造成假溢出。⼀些简单的、对队列操作不太频繁且数据量相对较⼩的场景,如学校
⻝堂打饭排队系统,可简单模拟学⽣排队打饭的过程,新学⽣在队尾加⼊,打到饭的学⽣从队
头离开。

链式队列是通过链表来实现的队列,每个节点包含数据域和指针域,指针域指向下⼀个节点。
队头指针指向链表的头节点,队尾指针指向链表的尾节点。插⼊和删除操作在链表的两端进
⾏,不需要移动元素,操作效率⾼,可动态分配内存,不存在空间溢出问题,但需要额外的指
针空间来存储节点之间的链接关系。常⽤于处理数据量不确定、需要频繁进⾏插⼊和删除操作
的场景,如操作系统中的进程调度,新进程可以随时在队尾加⼊等待队列,就绪的进程从队头
取出执⾏。

循环队列是把顺序队列的存储空间想象成⼀个⾸尾相接的圆环,当队尾指针到达数组末尾时,
若数组头部还有空闲空间,则将队尾指针重新指向数组头部,继续插⼊元素。充分利 ⽤了数组
的空间,避免了顺序队列中的假溢出问题,提⾼了空间利⽤率,但实现相对复杂⼀些,需要处
理队头和队尾指针在循环时的特殊情况。常⽤于数据缓冲区的管理,如⾳频、视频数据的缓
冲,数据以循环的⽅式存⼊缓冲区,消费端从队头取出数据进⾏处理,保证数据的连续和稳
定。

双端队列是⼀种特殊的队列,它允许在队列的两端进⾏插⼊和删除操作,既有队头指针⼜有队
尾指针,两端都可以作 为队头或队尾进⾏操作。具有很⾼的灵活性,可在两端快速插⼊和删除
元素,⽀持多种操作模式,但实现相对复杂,需要更多的代码来维护两端的操作逻辑。在⼀些
需要频繁在两端进⾏数据操作的场景中⾮常有⽤,如在浏览器的⻚⾯浏览历史记录中,⽤⼾可
以向前和向后 浏览⻚⾯,新访问的⻚⾯可以从队头或队尾插⼊,浏览过的⻚⾯可以从两 端删
除。

优先级队列中的每个元素都有⼀个优先级,在插⼊和删除元素时,会根据元素的优先级来进⾏
操作,优先级⾼的元素先出队。通常使⽤堆等数据结构来 实现,以保 证⾼效的插⼊和删除操
作。能够快速获取优先级最⾼(或最低)的元素并进⾏处理,插⼊和删除操作的时间复杂度通
常为 O (log n) ,其中 n 是队列中的元素个数。在任务调度系统中,不同任务可能具有不同的优先级,优先级⾼的任务需要优先执⾏,如操作系统中的进程调度,实时性要求⾼的进程具有较
⾼的优先级,会优 先被调度执⾏。

juc 包下你常⽤的类?
线程池相关:
ThreadPoolExecutor :最核⼼的线程池类,⽤于创建和管理线程池。通过它可以灵活地配置线
程池的参数,如核⼼线程数、最⼤线程数、任务队列等,以满⾜不同的并发处理需求。
Executors :线程池⼯⼚类,提供了⼀系列静态⽅法来创建不同类型的线程池,如
newFixedThreadPool (创建固定线程数的线程池)、 newCachedThreadPool (创建可缓存线程
池)、 newSingleThreadExecutor (创建单线程线程池)等,⽅便开发者快速创建线程池。
并发集合类:
ConcurrentHashMap :线程安全的哈希映射表,⽤于在多线程环境下⾼效地存储和访问键值
对。它采⽤了分段锁等技术,允许多个线程同时访问不同的段,提⾼了并发性能,在⾼并发场
景下⽐传统的 Hashtable 性能更好。
CopyOnWriteArrayList :线程安全的列表,在对列表进⾏修改操作时,会创建⼀个新的底层数
组,将修改操作应⽤到新数 组上,⽽读操作仍然可以在旧数 组上进⾏,从⽽实现了读写分离,
提⾼了并发读的性能,适⽤于读多写少的场景。
同步⼯具类:
CountDownLatch :允许⼀个或多个线程等待其他⼀组线 程完成操作后再继续执⾏。它通过⼀个
计数器来实现,计数器初始化为线程的数量,每个线程完成任务后调⽤ countDown ⽅法将计数
器减⼀,当计数器为零时,等待的线程可以继续执⾏。常⽤于多个线程完成各⾃任务后,再进
⾏汇总或下⼀步操作的场景。
CyclicBarrier :让⼀组线 程互相等待,直到所有线程都到达某个屏障点后,再⼀起继续执
⾏。与 CountDownLatch 不同的是, CyclicBarrier 可以重复使⽤,当所有线程都通过屏障后,
计数器会重置,可以再次⽤于下 ⼀轮的等待。适⽤于多个线程需要协同⼯作,在某个阶段完成
后再⼀起进⼊下⼀个阶段的场景。
Semaphore :信号量,⽤于控制同时访问某个资源的线程数量。它维护了⼀个许可计数器, 线
程在访问资源前需要获取许可,如果有 可⽤许可,则获取成功并将许可计数器减⼀,否则线程
需要等待,直到有其他线程释放许可。常⽤于控制对有限资源的访问,如数据库连接池、线程
池中的线程数量等。
原⼦类:
AtomicInteger :原⼦整数 类,提供了对整数 类型的原⼦操作,如⾃增、⾃减、⽐较并交换
等。通过硬件级别的原⼦指令来保证操作的原⼦性和线程安全性,避免了使⽤锁带来的性能开
销,在多线程环境下对整数 进⾏计数、状态标记等操作⾮常⽅便。
AtomicReference :原⼦引⽤类,⽤于对对 象引⽤进⾏原⼦操作。可以保 证在多线程环境下,
对对 象的更新操作是原⼦性的,即要么全部成功,要么全部失败,不会出现数据不⼀致的情
况。常⽤于实现⽆锁数据结构或需要对对 象进⾏原⼦更新的场景。
3个线程并发执⾏,1个线程等待这三个 线程全部执⾏完在执⾏,怎么实现?
可以使 ⽤ CountDownLatch 来实现 3 个线程并发执⾏,另⼀个线程等待这三个 线程全部执⾏完再
执⾏的需求。以下是具体的实现步骤:
创建⼀个 CountDownLatch 对象,并将计数器初始化为 3,因为有 3 个线程需要等待。
创建 3 个并发执⾏的线程,在每个线程的任务结束时调⽤ countDown ⽅法将计数器减 1。
创建第 4 个线程,使⽤ await ⽅法等待计数器为 0,即等待其他 3 个线程完成任务。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) {
// 创建一个 CountDownLatch,初始计数为 3
CountDownLatch latch = new CountDownLatch(3);
// 创建并启动 3 个并发线程
for (int i = 0; i < 3; i++) {
final int threadNumber = i + 1;
new Thread(() -> {
try {
System.out.println("Thread " + threadNumber + " is working.");
// 模拟线程执行任务
Thread.sleep((long) (Math.random() * 1000));
System.out.println("Thread " + threadNumber + " has finished.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 任务完成后,计数器减 1
latch.countDown();
}
// 创建并启动第 4 个线程,等待其他 3 个线程完成
new Thread(() -> {
try {
System.out.println("Waiting for other threads to finish.");
// 等待计数器为 0
latch.await();
System.out.println("All threads have finished, this thread starts to work
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}代码解释:
⾸先,创建了⼀个 CountDownLatch 对象 latch ,并将其初始计数设置为 3。
然后,使⽤ for 循环创建并启动 3 个线程。每个线程会执⾏⼀些⼯作(这⾥使⽤
Thread.sleep 模拟),在⼯作完成后,会调⽤ latch.countDown() ⽅法,将 latch 的计数减 1。
最后,创建第 4 个线程。这个线程在开始时调⽤ latch.await() ⽅法,它会阻塞,直到
latch 的计数为 0,即前⾯ 3 个线程都调⽤了 countDown() ⽅法。⼀旦计数为 0,该线程将继续执⾏后续任务。
单例模型既然已经⽤了synchronized ,为什么 还要在加volatile ?
使⽤ synchronized 和 volatile ⼀起,可以创建⼀个既线程安全⼜能正确初始化的单例模式,
避免了多线程环境下的各种潜在问题。这是⼀种⽐较完善的线程安全的单例模式实现⽅式,尤其
适⽤于⾼并发环境。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
}
}
return instance;
}
}synchronized 关键字的作⽤⽤于确保在多线程环境下,只有⼀个线程能够进⼊同步块(这⾥是
synchronized (Singleton.class) )。在创建单例对象时,通过 synchronized 保证了创建过程的线程安全性,避免多个线程同时创建多个单例对象。
volatile 确保了对象引⽤的可⻅性和创建过程的有序性,避免了由于指令重排序⽽导致的错
误。
instance = new Singleton(); 这⾏代码并不是⼀个原⼦操作,它实 际上可以分解为以下⼏个步骤:
分配内存空间。
实例化对象。
将对 象引⽤赋值给 instance 。
由于 Java 内存模型允许编译器和处理器对指令进⾏重排序,在没有 volatile 的情况下,可能会
出现重排序,例如先将对 象引⽤赋值给 instance ,但对象的实例化操作尚未完成。
这样,其他线程在检查 instance == null 时,会认为单例已经创建,从⽽得到⼀个未完全初始化的对象,导致错误。
volatile 可以保 证变量的可⻅性和禁⽌指令重排序。它确保对 instance 的修改对所有线程都
是可⻅的,并且保证了上 述三个 步骤按顺序执⾏,避免了在单例创建过程中因指令重排序⽽导致
的问题。
场景
常⻅的限流算法你知道哪些??
滑动窗⼝限流算法是对固定窗⼝限流算法的改进,有效解决了窗⼝切换时可能会产⽣两倍于阈
值流量请求的问题。
漏桶限流算法能够对流量起到整流的作⽤,让随机不稳定的流量以固定的速率流出,但是不能
解决流量突发的问题。
令牌桶算法作为漏⽃算法的⼀种改进,除了能够起到平滑流量的作⽤,还允许⼀定程度的流量
突发。
固定窗⼝限流算法
固定窗⼝限流算法就是对⼀段固定时间窗⼝内的请求进⾏计数,如果请求数超过了阈值,则舍弃
该请求;如果没有达到设定的阈值,则接受该请求,且计数加1。当时间窗⼝结束时,重置计数器
为0。

固定窗⼝限流优点是实现简单,但是会有“流量吐刺”的问题,假设窗⼝⼤⼩为1s ,限流⼤⼩为
100 ,然后恰好在某个窗⼝的第999ms 来了100 个请求,窗⼝前期没有请求,所以这100 个请求都会
通过。再恰好,下⼀个窗⼝的第1ms 有来 了100 个请求,也全部通 过了,那也就是在2ms 之内通过
了200 个请求,⽽我们设定的阈值是100 ,通过的请求达到了阈值的两倍,这样可能会给系统造成
巨⼤的负载压⼒。

滑动窗⼝限流算法
改进固定窗⼝缺陷的⽅法是采⽤滑动窗⼝限流算法,滑动窗⼝就是将限流窗⼝内部切分 成⼀些更
⼩的时间⽚,然后在时间轴上滑动,每次 滑动,滑过⼀个⼩时间⽚,就形成⼀个新的限流窗⼝,
即滑动窗⼝。然后在这个滑动窗⼝内执⾏固定窗⼝算法即可。
滑动窗⼝可以避免固定窗⼝出现的放过两倍请求的问题,因为⼀个短时间内出现的所有请求必然
在⼀个滑动窗⼝内,所以⼀定会被滑动窗⼝限流。

漏桶限流算法
漏桶限流算法是模拟⽔流过⼀个有漏洞的桶进⽽限流的思路,如图。
⽔⻰头的⽔先流⼊漏桶,再通过漏桶底部的孔流出。如果流⼊的⽔量太⼤,底部的孔来不及流
出,就会导致⽔桶太满溢 出去。
从系统的⻆度来看,我们不知道什么 时候会有请求来,也不 知道请求会以 多⼤的速率来,这就给
系统的安全性埋下了 隐患。但是如果加了⼀层漏⽃算法限流之后,就能够保证请求以恒定的速率
流出。在系统看来,请求永 远是以平滑的传输速率过来,从⽽起到了保护系统的作⽤。
使⽤漏桶限流算法,缺点有两个 :
即使系统资源很空闲,多个请求同时到达时,漏桶也是慢慢 地⼀个接⼀个地去处理请求,这其
实并不符合⼈们的期望 ,因为这样就是在浪费计算资源。
不能解决流量突发的问题,假设漏⽃速率是2个/秒,然后突然来了10 个请求,受限于漏⽃的容
量,只有5个请求被接受,另外5个被拒绝。你可能会说,漏⽃速率是2个/秒,然后瞬间接受了5
个请求,这不就解决了流量突发的问题吗?不,这5个请求只是被接受了,但是没有⻢上被处
理,处理的速度仍然是我们设定的2个/秒,所以没有解决流量突发的问题
令牌桶限流算法
令牌桶是另⼀种桶限流算法,模拟⼀个特定⼤⼩的桶,然后向 桶中以特定的速度放⼊令牌
(token ),请求到达后,必须从桶中取出⼀个令牌才能继续处理。如果桶中已经没有令牌了,那
么当前请求就被限流。如果桶中的令牌放满了,令牌桶也会溢出。
放令牌的动作是持续不断进⾏的,如果桶中令牌数达到上限,则丢弃令牌,因此桶中可能⼀直持
有⼤量的可⽤令牌。此时请求进来可以直接拿 到令牌执⾏。⽐如设置 qps 为 100 ,那么限流器初
始化完成 1 秒后,桶中就已经有 100 个令牌了,如果此前还没有请求过来,这时突然来了 100 个
请求,该限流器可以抵挡瞬时的 100 个请求。由此可⻅,只有桶中没有令牌时,请求才会进⾏等
待,最终表现的效果即为以⼀定的速率执⾏。令牌桶的⽰意图如下:

令牌桶限流算法综合效果⽐较好,能在最⼤程度利⽤系统资源处理请求的基础上,实现限流的⽬
标,建议通常场景中优先使⽤该算法。
⽹络
第四次握⼿为什么 需要等待TIME_WAIT ?

TIME_WAIT 状态的存在是为了 确保⽹络连接的可靠关闭。只有主动发起关闭连接的⼀⽅(即主动
关闭⽅)才会有 TIME_WAIT 状态。
TIME_WAIT 状态的需求主要有两个 原因:
防⽌具有相同「四元组」的「旧」数据包被收到:在⽹络通信中,每个 TCP 连接都由源 IP 地址、源端⼝号、⽬标 IP 地址 和⽬标端⼝号这四个元素唯⼀标识,称为「四元组」。当⼀⽅主动
关闭连接后,进⼊ TIME_WAIT 状态,它仍然可以接收到⼀段时间内来⾃对⽅的延迟数据包。这
是因为⽹络中可能存在被延迟传输的数据包,如果没有 TIME_WAIT 状态的存在,这些延迟数据
包可能会被错误地传递给新的连接,导致数据混乱。通过保持 TIME_WAIT 状态,可以防⽌旧的
数据包⼲扰新的连接。
保证「被动关闭连接」的⼀⽅能被正确关闭:当连接的被动关闭⽅接收到主动关闭⽅的 FIN 报
⽂(表⽰关闭连接),它需要发送⼀个确认 ACK 报⽂给主动关闭⽅,以完成连接的关闭。然
⽽,⽹络是不可靠的,ACK 报⽂可能会在传输过 程中丢 失。如果主动关闭⽅在收到 ACK 报⽂之
前就关闭连接,被动关闭⽅将⽆法正常完成连接的关闭。TIME_WAIT 状态的存在确保了被动关
闭⽅能够接收到最后的 ACK 报⽂,从⽽帮助其正常关闭连接。
io 多路复⽤你的理解?
IO 多路复⽤是⼀种IO 得处理⽅式,指的是复⽤⼀个线程,处理多个socket 中的事件。能够资源复
⽤,防⽌创建过多线程导致的上下 ⽂切换的开销。

我们熟悉的 select/poll/epoll 内核提供给⽤⼾态的多路复⽤系统调⽤,进程可以通过⼀个系统调⽤
函数从内核中获取多个事 件。
select/poll/epoll 是如何获取⽹络事件的呢?在获取事件时,先把所 有连接(⽂件描述符)传给内
核,再由内核返回产⽣了事 件的连接,然后在⽤⼾态中再处理这些连接对应的请求即可。
select/poll/epoll 这是三个 多路复⽤接⼝,都能实现 C10K 吗?接下来,我们分别 说说 它们。
select/poll
select 实现多路复⽤的⽅式是,将已连接的 Socket 都放到⼀个⽂件描述符集合,然后调⽤ select
函数将⽂件描述符集合拷⻉到内核⾥,让内核来检查是否有⽹络事件产⽣,检查的⽅式很 粗暴,
就是通过遍历⽂件描述符集合的⽅式,当检查到有事件产⽣后,将此 Socket 标记为可读或可写,
接着再把整个⽂件描述符集合拷⻉回⽤⼾态⾥,然后⽤⼾态还需要再通过遍历的⽅法找到可读或
可写的 Socket ,然后再对其处理。
所以,对于 select 这种⽅式,需要进⾏ 2 次「遍历」⽂件描述符集合,⼀次是在内核态⾥,⼀个
次是在⽤⼾态⾥ ,⽽且还会发⽣ 2 次「拷⻉」⽂件描述符集合,先从⽤⼾空间传⼊内核空间,由
内核修改后,再传出到 ⽤⼾空间中。
select 使⽤固定⻓度的 BitsMap ,表⽰⽂件描述符集合,⽽且所⽀持的⽂件描述符的个数是有限制
的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最⼤值为 1024 ,只能监听 0~1023 的
⽂件描述符。
poll 不再⽤ BitsMap 来存储所关注的⽂件描述符,取⽽代之⽤动态数组,以链表形式 来组织 ,突
破了 select 的⽂件描述符个数限制,当然还会受到系统⽂件描述符限制。
但是 poll 和 select 并没有太⼤的本质区别,都是使⽤「线性结构」存储进程关注的 Socket 集合,
因此都需要遍历⽂件描述符集合来找到可读或可写的 Socket ,时间复杂度为 O(n) ,⽽且也 需要在⽤⼾态与内核态之间拷⻉⽂件描述符集合,这种⽅式随着并发数上来,性能的损耗会呈指数级增
⻓。
epoll
先复习下 epoll 的⽤法。如下的代码中,先⽤epoll_create 创建⼀个 epol l 对象 epfd ,再通过
epoll_ctl 将需要监视的 socket 添加到 epfd 中,最后调⽤ epoll_wait 等待数据。
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...)
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
while(1) {
int n = epoll_wait(...);
for(接收到数据的socket){
//处理
}
}epoll 通过两个 ⽅⾯,很好解决了 select/poll 的问题。
第⼀点,epoll 在内核⾥使⽤红⿊树来跟踪进程所有待检测的⽂件描述字,把需要监控的 socket
通过 epoll_ctl() 函数加⼊内核中的红⿊树⾥,红⿊树是个⾼效的数据结构,增删改⼀般时间复
杂度是 O(logn) 。⽽ select/poll 内核⾥没有类似 epoll 红⿊树这种保存所有待检测的 socket 的数据结构,所以 select/poll 每次 操作时都传⼊整个 socket 集合给内核,⽽ epoll 因为在内核维
护了红⿊树,可以保 存所有待检测的 socket ,所以只需要传⼊⼀个待检测的 socket ,减少了内
核和⽤⼾空间⼤量的数据拷 ⻉和内存分配。
第⼆点, epoll 使⽤事件驱动的机制,内核⾥维护了⼀个链表来记录就绪事件,当某个 socket
有事件发⽣时,通过回调函数内核会将其加⼊到这个就绪事件列表中,当⽤⼾调⽤ epoll_wait()函数时,只会返回有事件发⽣的⽂件描述符的个数,不需要像 select/poll 那样轮询扫描整个
socket 集合,⼤⼤提⾼了检测的效率。
从下 图你可以看到 epoll 相关的接⼝作⽤:

epoll 的⽅式即使监听的 Socket 数量越多的时候,效率不会⼤幅度 降低,能够同时监听的 Socket
的数⽬也⾮常的多了,上限就为系统定义的进程打开的最⼤⽂件描述符个数。因⽽,epoll 被称为
解决 C10K 问题的利器。
https 为什么 可以保 证数据安全?
加密:HTTPS 在 HTTP 的基础上添加了 SSL/TLS 协议,对传输的数据进⾏加密。它使⽤⾮对称
加密(如 RSA )交换对称加密的密钥,然后使⽤对称加密(如 AES )对实际传输的数据进⾏加
密。这样可以防⽌数据在传输过 程中被窃取或篡改,保证了数据的机密性和完整性。
⾝份验证:通过证书机制,服务器向客⼾端发送数字证书,证书中 包含服务器的公钥和服务器
的⾝份信 息。客⼾端可以验证证 书的有效性,确认服务器的⾝份,防⽌中间⼈攻击。
数字证书也 可以伪 造,怎么办?
客⼾端在校验证书的时候,实际上是有⼀个证书链校验的过程。
根证书是整个信任 链的基础,通常由操作系统或浏览器⼚商内置在其系统中。这些根证书由⾼度
可信的证书颁发机构 (CA )颁发和维护。当客⼾端收到服务器的数字证书时,会沿着证书链向上
追溯,从服务器证书到中间证书(如果有 ),最终到根证书。如果根证书被信任 ,并且整个证书链
上的签名都验证通过,客⼾端才会信任 该服务器证书。

伪造⼀个完整的证书链,使其能够追溯到受信任 的根证书⾮常困难,因为根证书的私钥被严密保
护,只有 CA 机构 ⾃⼰掌握 ,并且 CA 机构 在颁发证书时会进⾏严格的⾝份验证。
