牛客面经八股题目----包含题解版
温馨提示:下面题目是我结合网络以及自己所学总结,如果有意见可以在评论区留言,并提出你的见解
线程池的作用是什么?如何创建线程池?线程池常见参数有哪些?
作用
1.减少线程创建销毁的开销,提高资源利用率
2.统一管理线程资源
3.提高响应速率,不用等待线程的创建
如何创建线程池
使用 ThreadPoolExecutor 构造
通过构造函数精细控制参数,避免资源耗尽风险;
创建线程池七个要素:
1.核心线程数,
线程池长期保持的线程数量(即使线程空闲也不会销毁)
2.最大线程数,
线程池允许创建的最大线程数(核心线程数 + 非核心线程数)。当任务队列满后,若当前线程数小于最大线程数,会创建非核心线程执行任务。
3.等待队列,
用于存放待执行任务的阻塞队列
4.空闲超时时间
非核心线程的空闲存活时间。若线程空闲时间超过此值,非核心线程会被销毁,以节省资源
5.时间单位,
空闲超时时间的时间单位,如TimeUnit.SECONDS(秒)
6.线程工厂,
用于创建线程的工厂,可自定义线程名称、优先级
7.饱和策略
当任务队列满且线程数达到最大线程数时,对新提交任务的处理策略
什么是进程和线程?进程和线程的区别?
定义
进程是指在内存中运行的应用程序
线程是指进程中的一个执行任务流,负责进程的程序执行
区别
1.进程是系统资源分配的最小单位,线程是系统资源调度的最小单位
2.每一个进程有自己独立的内存空间,多个线程共享同一进程的内存地址和内存空间
3.线程的创建销毁成本小于进程
请你说说HashMap底层原理和扩容机制。
底层原理
HashMap的底层是数组+链表+红黑树实现的
首先对键的hashcode进行高低位异或,再进行桶索引的计算,这里采用哈希值&(桶数组长度-1)来计算,然后进行插入操作,通过桶索引找到要插入的位置,如果桶为空,直接插入,如果当前桶不为空,则分情况进行判断,如果桶中是红黑树的话,则按红黑树的插入方式进行插入,如果是链表的话,jdk1.7版本进行的是头插法,而jdkl1.8之后用的是尾插法,目的是避免多线程扩容死循环。如果插入之后链表长度>=8,则将链表变为红黑树存储,再说查询,首先对要查询键获取桶索引,如果是红黑树,则按照红黑树的查询规则,如果是链表,则遍历链表进行查询。
扩容机制
扩容机制:当桶内元素总数超过阈值(容量*负载因子),则进行扩容,将桶数组长度变为原来的二倍,并重新计算所有值的索引,但在jdk1.8之后,通过(哈希值& 旧桶数组长度)判断节点是否需要迁移到新桶,减少重哈希开销
TCP 与 UDP 的核心区别是什么?
1. 连接方式
-
TCP:面向连接,需通过三次握手建立连接,传输结束后通过四次挥手释放连接。
-
UDP:无连接,直接发送数据,无需预先建立连接。
2. 可靠性
-
TCP:可靠传输,通过确认应答(ACK)、超时重传、丢包重发等机制保证数据完整有序。
-
UDP:不可靠传输,不保证数据是否到达或顺序正确。
3.数据顺序
-
TCP:通过序列号和确认机制保证数据按发送顺序到达。
-
UDP:不保证顺序,即使数据乱序到达也不会重新排序。
4. 流量控制
-
TCP:通过滑动窗口机制来控制流量,防止丢包
-
UDP:无流量控制,可能因发送过快导致丢包。
5. 拥塞控制
-
TCP:通过慢启动、拥塞避免等算法避免网络拥堵。
-
UDP:无拥塞控制,可能加剧网络拥堵。
6. 传输效率
-
TCP:因连接管理、重传等机制,头部开销大(20字节以上),传输效率较低。
-
UDP:头部仅8字节,无额外控制机制,传输效率高。
7. 多播/广播支持
-
TCP:仅支持单播(一对一通信)。
-
UDP:支持单播、多播(一对多)和广播(一对所有)。
8.传输数据
-
TCP:传输字节流
-
UDP:传输数据报
Redis如何与数据库保持双写一致性?
保证最终一致性:
先更新数据库再删除缓存,可以避免多线程同时写而导致的不一致
强一致性
采用分布式锁+mysql事务:
更新数据前先获取分布式锁,确保只有一个线程能访问数据,然后更新mysql再更新缓存,最后释放分布式锁,这样就可以保证强一致性了,即使mysql更新的时候出现异常,也可以通过重试机制+异步补偿机制确保一致性
详细的说说Redis的数据类型
Redis中常用的五种数据结构:string、list、set、zset、hash
String(字符串)
底层结构:
int--->短字符串-->长字符串(根据元素的长度变化)
应用:缓存热点数据,分布式锁,计数器
Hash(哈希)[key field value]
底层结构:
ziplist--->hashtable(根据元素个数和元素大小变化)应用:存储对象数据,购物车
List(列表)
底层结构:
压缩列表-->双向列表(后面引入了quickList,将链表分为多个zipList片段,每个片段之间用Linklisted连接)
应用:消息队列
Set(集合)
底层结构:
intset(整数集合)-->HashTable(根据是否全是整形或者长度是否超过某一阈值变化)应用:去重,社交场景 - 共同好友 / 共同关注,抖音推荐
ZSet(有序集合)
底层结构:
ziplist->skipList(当元素个数或者元素大小超过阈值时变化)应用:排行榜,延迟任务
请你说说ArrayList和LinkedList的区别。
- ArrayList底层是数组实现的,读取快,插入删除慢;
- LinkedList底层基于双向链表,读取慢,插入删除快;
说说JVM的垃圾回收算法
1. 标记清除
这种策略,就是直接把垃圾对象的内存释放,但是这个方式的缺点就是会产生内存碎片.

2.复制算法

将堆内存分为两块,一块使用,一块用来放置将来存活的对象,这样会造成空间浪费,且如果存活的对象太多,则复制成本就有点大了
3. 标记整理

效率不高:如果要搬运的空间比较大,此时开销也很大
4.分代回收

刚new 出来的,年龄是0的对象,放到新生代。熬过一轮GC,对象就要被放到幸存区了
新生代到幸存区,采用的是复制算法.
幸存区之后,也要周期性的接受GC的考验.
如果变成垃圾,就要被释放.如果不是垃圾,拷贝到另外一个幸存区(这俩幸存区同一时刻只用一个),在两者之间来回拷贝(复制算法),由于幸存区体积不大, 此处的空间浪费也能接受.如果这个对象已经再两个幸存区中来回拷贝很多次了这个时候就要进入老年代了·
老年代都是年纪大的对象.生命周期普遍更长.针对老年代,也要周期性GC扫描,但是频率更低了
如果老年代的对象是垃圾了,使用标记整理的方式进行释放.
说说缓存穿透、击穿、雪崩的区别
1.缓存穿透:大量客户端访问不存在的数据,使得请求直达数据库,导致负载过大,直至宕机
解决方法:
- 数据库未命中后,返回空值存入缓存层,客户端再次访问时,缓存层直接返回空值
- 将数据存入布隆过滤器,访问缓存之前经过滤器,若请求的数据不存在则直接返回空值。
2.缓存雪崩:大量key过期或者redis宏机,导致大量请求直达数据库,导致负载过大,直至宏机
解决方法:
- 设置key永不过期
- 设置key过期时间的时候添加一个随机因子,让key过期时间不均匀
- 采用redis集群,一个宕机,另外的还能用
3.缓存击穿:热点key过期,导致大量请求访问数据库,负载过大导致宏机
解决方法:
- 设置热点key永不过期
- 加互斥锁,当一个线程访问该数据时,另一个线程只能等待,这个线程访问之后,缓存中的数据将被重建,届时其他线程就可以从缓存中取值。
说说Redis的持久化策略
redis的持久化策略有三种
1.RDB持久化
将当前进程中的数据已生成快照的方式保存到硬盘中,是redis默认的持久化机制。
分为自动触发和手动触发俩种
- 手动触发:
save:会阻塞 Redis 服务器bgsave:异步执行,Redis 会 fork 一个子进程负责生成 RDB,主进程继续处理命令,是更常用的方式。
原理
bgsave触发时,主进程通过fork()创建子进程;- 子进程遍历内存数据,生成 RDB 文件并写入磁盘;
- 子进程完成后,通知主进程,旧 RDB 文件被新文件替换。
优点
- 文件体积小:二进制压缩格式
- 恢复速度快:加载 RDB 文件时直接解析二进制数据,比 AOF 高效。
- 对性能影响小:
bgsave异步执行,主进程几乎无阻塞。
缺点
- 数据安全性低:快照是周期性生成的,若在两次快照之间发生故障,这段时间的数据会丢失。
fork()开销:当数据量大时,fork()子进程可能导致短暂阻塞
2.AOF持久化
以独立日志的方式记录每次写入的命令,重启时执行AOF中的命令即可恢复数据。默认文件名是 appendonly.aof
命令写入与同步策略
- 写命令先放入内存缓冲区,再根据配置的同步策略写入磁盘:
appendfsync always:每次写命令都同步到磁盘,安全性最高,但 IO 开销大,性能最差。appendfsync everysec(默认):每秒同步一次,平衡安全性和性能,最多丢失 1 秒数据。appendfsync no:由操作系统决定何时同步,性能最好,但安全性最低(可能丢失大量数据)。
AOF 重写(Rewrite)
AOF 文件会随命令增多而膨胀,Redis 提供重写机制压缩文件:
- 原理:通过重新分析内存数据,生成等效的最小命令集,替换旧 AOF 文件。
优点
- 数据安全性高:可配置每秒同步,数据丢失风险低(最多 1 秒)。
- 日志可读性强:文本格式
缺点
- 文件体积大:相同数据下,AOF 文件通常比 RDB 大。
- 恢复速度慢:加载时需重新执行所有命令,耗时较长。
3.RDB + AOF 混合持久化
AOF 重写时,先以 RDB 格式写入内存数据的快照,再追加后续的写命令日志。
优势:
- 恢复速度接近 RDB(开头是 RDB 快照)。
- 数据安全性接近 AOF
事务隔离级别有哪些?
读未提交,读已提交,可重复读,串行化
脏读,幻读,不可重复读是什么?什么原因导致的?解决方法是什么?
脏读(Dirty Read)
- 定义:一个事务读取到了另一个未提交事务修改的数据。
- 原因:当事务 A 正在修改某条数据(尚未提交),此时事务 B 读取了该条被修改后的数据。若事务 A 后续因异常回滚
- 解决方案:1.通过 “读已提交” 及以上隔离级别2. 写锁
不可重复读(Non-Repeatable Read)
- 定义:同一事务内,多次读取同一数据时,结果不一致(后一次读取到了其他事务已提交的修改)。
- 产生原因:事务 A 第一次读取数据后,事务 B 对该数据进行了修改并提交,事务 A 再次读取时,发现数据发生了变化。
- 解决方案: MVCC(只读取当前版本及以前的数据)
幻读(Phantom Read)
- 定义:同一事务内,多次执行相同的查询条件时,返回的结果集行数不一致
- 产生原因:事务 A 按条件查询数据(例如 “年龄> 20 的用户”),事务 B 此时插入 / 删除了符合该条件的新数据并提交
- 解决方案:通过 “串行化” 隔离级别,彻底禁止并发
HTTP和HTTPS 有什么区别
HTTP是明文传输不安全,HTTPS加了一层协议对消息传输加密,先检查ca和签名,再用非对称加密交换对称加密密钥,后续用对称加密传输消息
说说 TCP 的三次握手
1.服务端处于监听状态,客户端向服务端发送第一次连接请求,会生成SYN报文,表示发起连接(第一次握手); 2.服务端收到SYN报文后,也生成一个SYN报文,以及一个ACK报文,将报文全部发给客户端(第二次握手); 3.客户端收到服务端的报文后,然后再次发送ACK报文给客户端,客户端处于已建立状态,这次握手可以携带数据到服务端(第三次握手); 4.服务端收到应答报文后,也处于已建立状态;
如何利用Redis实现一个分布式锁?
setnx(不存在就设置,存在就失败,结束之后del)+expire(防止节点挂了之后一直占有锁)+校验id(防止其他任意服务器都可以del key)+lua脚本(实现原子操作:get校验id和del)+watch dog(看门狗,防止业务逻辑没执行完就释放掉锁)+Redlock算法(引入多组redis节点,加锁和解锁的时候对这些redis也进行加锁,防止单节点的情况下主节点宏机导致的从节点未来得及同步分布式锁的key,导致多key的情况)

SQL 调优常见方法有哪些?
合理使用索引,优化查询语句,优化表结构
讲一下乐观锁和悲观锁
悲观锁认为在并发条件下冲突总是会发生,因此在对数据进行操作之前需要加锁,主要实现有synchronized,由于加锁,解锁和线程上下文切换,性能上有一定的开销。适用于写多读少的场景。
乐观锁认为在并发条件下冲突很少发生,因此直接对数据进行操作,提交时再检查是否冲突。主要实现有cas,版本号等机制。在冲突较少的情况下性能极高,适用于读多写少的场景。
消息队列如何保证消息不丢?
1.生产者到RabbitMQ
通过发布确认机制保证生产者到RabbitMQ的消息不丢失,其中confirm模式保证消息从生产者到exchange不丢失,return模式保证exchange到queue不丢失
2.确保消息在 MQ 中可靠保存
开启持久化机制,包括交换机持久性,队列持久性,消息持久性,即使服务器宏机之后,也可以将消息恢复
3.消费端:确保消息被成功处理
消息确认机制保证消息被成功处理
手动确认保证消费者的可靠性,这样如果我们业务有异常或者有网络问题,我们可以不进行确认,我们可以选择重试,或者直接把这条消息放到死信队列,我们可以进行人工处理
MySQL 中的 MVCC 是什么?Read View 在 MVCC 中如何工作?如果没有 MVCC 会怎么样
MVCC 是 mysql为了解决并发环境下读写冲突而实现的一种多版本并发控制机制。核心思想是采用不同版本快照的方式去避免事务并发过程中的各种问题,通过 Read View 决定当前事务能看到哪个版本,从而实现“读不加锁、写也不阻塞读”,显著提升并发性能。没有 MVCC 的话,读操作必须加锁,会与写事务互相等待,导致系统吞吐量大幅下降。
设计一个支持万人同时抢购商品的秒杀系统,如何解决超卖、库存扣减和高并发请求问题?
高并发:
流量削峰:
- 前端限流:增加验证码
- 网关限流:通过网关gateWay进行限流
- 使用RabbitMQ进行削峰
防止超卖:
- Redis 原子操作:使用 Redis 的原子操作(如 DECR 或 Lua 脚本)扣减库存,确保库存不会被超扣。
- 分布式锁:在 Redis上加锁,确保同一时间只有一个线程能扣减库存。
- 乐观锁:在数据库层面使用版本号或cas机制,防止并发更新
库存扣减:
- 秒杀前将商品库存同步到 Redis(如
seckill:stock:1001 = 100),请求到达时先通过 Redis 的decr命令预扣减库存:- 若
decr后结果 ≥ 0:预扣减成功,进入下一步处理。 - 若
decr后结果 < 0:库存不足,直接返回失败(此步骤可过滤 90% 以上无效请求)。
- 若
ConcurrentHashMap 线程安全的具体实现方式
说说volatile的用法及原理
用法1
volatile 是 Java 中用于保证内存可见性和禁止指令重排序的关键字
保证内存可见性:当一个变量被 volatile 修饰后,任何线程对该变量的修改会立即同步到主内存,其他线程读取时会强制从主内存加载最新值
底层实现原理1
JVM底层是通过内存屏障来实现,当对一个变量进行读取时,插入读屏障,表示先从主内存读取,当对一个变量进行写入后,插入写屏障,表示立即同步到主内存,这样就解决了可见性问题
用法2
禁止指令重排序:volatile 会通过内存屏障阻止 JVM 和 CPU 对指令的乱序执行,保证代码执行顺序与预期一致。
底层实现原理2
在 volatile 变量的读写前后插入特定内存屏障,限制指令的重排范围
说说线程的创建方式
线程的创建方式常用的有四种:
1)继承Thread类,重写run()方法
2)实现Runnable接口,重写该接口的run方法。
3)实现Callable接口,重写该接口的call方法。
4)从线程池中取出。
Redis中的Zset怎么实现的?
1.数据量较小的时候使用压缩表ziplist,数据和score按照score顺序成对存储
2.数据量较大的时候使用跳表skiplist+哈希表,skiplist按照score顺序存储数据,实现数据有序以及范围查询等功能,哈希表存储数据和score的对应关系,实现数据唯一以及快速查询单个数据的功能
TCP 协议如何保证可靠传输?
通过确认应答为核心,借助其他机制辅助最终完成可靠传输,例如:三次握手,四次挥手,消息重传,拥塞机制等辅助机制来保证消息可靠传输
说说你对AOP的理解
aop是一种面向切面编程的编程思想; 有切面,连接点,通知,切点。 aop是基于动态代理的技术,在程序运行的时候,动态地修改class字节码生成新的class文件进行运行的技术,可以对某个类的某个方法、多个类的多个方法,通过配置切点表达式进行匹配,然后改造被匹配到的类的某一些方法,然后运行; 常见的aop使用场景,日志统一管理,事务管理,权限校验。AOP的实现方式有四种:1.@基于Aspect注解和切点表达式2.基于⾃定义注解和@annotation3.基于springAPI4.基于代理模式
说说类加载机制
类加载的完整流程
加载
- 核心动作:通过类的全限定名,找到对应的
.class字节码文件,将其读取到内存中
验证
- 核心动作:校验字节码的合法性(如格式是否正确、是否符合 Java 语法规范、是否有安全隐患)
准备
- 核心动作:为类的静态变量(static)分配内存,并设置默认初始值
解析
- 核心动作:将类中的符号引用(如代码中引用的类名、方法名,以字符串形式存在)转换为直接引用(内存地址或指针)
初始化
- 核心动作:执行类的初始化代码(静态变量赋值、静态代码块
static {})
GET 与 POST 有什么区别?
GET和POST从本质上来说,没有什么区别;GET的场景,使用POST也OK,PSOT的场景,使用GET也没问题
但是从使用习惯上有区别
1)从语义上来说,GET表示“获取数据”,POST表示“提交数据”
2)在传递数据的时候,GET通常使用query string,POST通常使用body
3)服务器对于请求的设计,GET请求经常设计成“幂等”,而POST不要求“幂等”
HTTP 常见状态码有哪些?
HTTP 状态码分五大类,核心按请求结果划分: 2xx 成功、3xx 重定向、4xx 客户端错、5xx 服务端错,1xx 很少用。高频的比如 200(请求成功带数据)、204(成功无数据),404 资源不存在; 500 服务端 bug,301 永久重定向(域名迁移)
I/O 多路复用(select/poll/epoll)是什么?通常适用哪些场景?
io多路复用指通过单线程管理多个 IO 连接,避免线程切换开销,同时监听多个连接的读写事件,大幅提升 IO 效率。
IO模型主流包括 select、poll、epoll
select、poll、epoll 的底层原理与区别
三者均是操作系统提供的 “IO 事件监听接口”,核心目标相同,但实现方式不同,导致性能差异巨大,epoll 是当前 Linux 系统的最优选择。

适用场景:用于高并发的网络服务器,如Web服务器,聊天室,游戏服务器等,特别是当连接很多,但大部分连接是不活跃的长连接时,这种做法更有效。
HTTPS 的「秘钥交换 + 证书校验」全流程
①、客户端向服务器发起请求。
②、服务器接收到请求后,返回自己的数字证书,包含了公钥、颁发机构等信息。
③、客户端收到服务器的证书后,验证证书的合法性,如果合法则生成一个会话密钥,然后用服务器的公钥加密,发送给服务器。
④、服务器收到被公钥加密的密文,用私钥解密,得到会话密钥。
⑤、客户端和服务器通过会话密钥对通信内容进行加密,然后传输。
如果通信内容被截取,但由于没有会话密钥,所以无法解密。当通信结束后,连接会被关闭,会话密钥也会被销毁,下次通信会重新生成一个会话密钥。
请你说一下抽象类和接口的区别
相同点:
1、两者都不能实例化;
2、可以拥有抽象方法。
区别:
1、抽象类定义的关键字是abstract class,接口定义的关键字是interface;
.2、属性上,抽象类可以有静态变量、常量和成员变量,接口只能有常量;
3、抽象方法可以有普通方法,而接口只能有抽像方法(JDK8之后可以使用默认方法)
4、抽象方法可以有构造方法,接口不可以有构造方法。
5、一个类只能单继承一个父类,而一个接口可以继承多个父接口
说说Redis的缓存淘汰策略

Redis 如何处理过期键
惰性删除
惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key
定期删除
定期删除:每隔一段时间,我们就对一些key进行检查,删除里面过期的key
说说Spring事务管理
spring支持编程式事务管理和声明式事务管理两种方式:
①编程式事务管理使用TransactionTemplate。
②声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。 声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。

