20260409
问题
1.互斥锁
互斥锁: 同一时间只有一个线程能拿到锁,其他线程必须等着。保证同一时间,只有一个线程执行某段代码。
作用:保证线程安全,让并发修改变成排队执行。
原理:
- 锁只有一把
- 线程 A 拿到锁 → 进去执行代码
- 线程 B 来拿锁 → 拿不到,阻塞等待
- 线程 A 执行完 → 释放锁
- 线程 B 才能拿到锁,进去执行
2.熔断和降级的区别
熔断是下游服务异常时,调用方主动切断调用,避免故障扩散、防止服务雪崩。
降级是系统压力过大时,主动关闭非核心功能,保证核心服务稳定运行。
两者目的都是高可用,但熔断是因为别人坏了,降级是因为自己扛不住了。
3.lock和synchronized 区别
synchronized 是 JVM 实现的隐式锁,自动加锁、自动释放,使用简单,不能中断、不支持公平锁、不支持超时获取锁。
Lock 是 JDK 实现的显式锁,需要手动 lock/unlock,支持可重入、可中断、可超时、公平锁、多 Condition,功能更强大灵活。
简单场景用 synchronized,复杂高并发场景用 Lock。
4.线程池的执行过程
- 任务提交过来,先判断核心线程数是否已满
- 没满:直接创建核心线程执行任务
- 满了:进入下一步
- 判断工作队列是否已满
- 没满:把任务放进队列排队
- 满了:进入下一步
- 判断最大线程数是否已满
- 没满:创建非核心线程执行任务
- 满了:进入下一步
- 执行拒绝策略
- 比如抛异常、丢弃任务、丢弃最老任务、调用者线程执行等
5.类加载器
- 启动类加载器(Bootstrap ClassLoader) C++ 实现,JVM 自带 加载 JAVA_HOME/lib 下的核心包,比如 rt.jar
- 扩展类加载器(Extension ClassLoader) Java 实现 加载 JAVA_HOME/lib/ext 下的扩展 jar
- 应用类加载器(App ClassLoader / 系统类加载器) 加载我们项目 classpath 下的类 平时自己写的代码、引入的第三方 jar 都由它加载
- 自定义类加载器 继承 ClassLoader 自己实现 用于热部署、加密加载、模块化加载等场景(比如 Tomcat、SPI)
6.索引失效
1.对索引字段做函数运算、数学运算
比如:age + 1 = 20、substring(name,1,2)='xx'→ 索引直接失效
2.隐式类型转换
比如字段是 varchar,查询用数字:phone = 13800138000
字符串和数字比较会触发隐式转换,索引失效
3.like 以 % 开头
like '%张三' 会失效
like '张三%' 可以走索引
4.使用!= 、 <> 、 not in 、 not exists
这些大概率导致索引失效
5.or 连接的条件有一边没有索引
只要有一个字段没索引,整个索引可能失效
6.违反最左匹配原则
联合索引 (a,b,c)直接查 b 或 c,跳过 a → 索引失效
7.线程池满了服务挂了
为什么:
-
任务无限提交,核心线程 + 最大线程都跑满
-
任务队列也被塞满
-
触发拒绝策略,但如果拒绝策略不合理
比如直接抛异常,大量请求失败
或者用 CallerRuns 让主线程执行业务,导致主线程被堵死
-
最终表现:接口大量超时、服务不响应、甚至整个应用假死
怎么排查:
-
看日志:有没有 RejectedExecutionException
-
用 jstack 看线程:
是不是所有线程都在 RUNNABLE
是不是大量线程卡在数据库、第三方接口
-
看队列:是不是队列设置太大,OOM 了
-
看业务:是不是下游响应慢,导致线程长时间占着不释放
怎么解决:
降级限流
- 入口加限流,不让任务无限进来
优化线程池参数
- 合理设置核心线程、最大线程、队列长度
- 队列不要设置无界,否则会 OOM
异步任务超时控制
- 线程执行必须加超时,避免线程一直被占住
合理拒绝策略
- 不能无脑抛异常
- 重要任务用降级、缓存、兜底
异步解耦
- 把线程池替换成 MQ 削峰填谷
- 线程池只处理轻量任务,不处理长耗时任务
监控告警
- 监控队列长度、活跃线程数、拒绝任务数
8.hashmap底层数据结构
jdk1.8:
数组 + 链表 + 红黑树
主体是:哈希表(Entry 数组,也叫桶)
哈希冲突时:拉链法 —— 链表
当链表长度 ≥ 8 且数组长度 ≥ 64 时:转为红黑树
当树节点 ≤ 6 时:退化成链表
jdk1.7:
数组 + 链表
只有数组 + 链表,没有红黑树
冲突时采用头插法,高并发下容易成环死循环
核心流程:
- 根据 key 计算
hashcode,再做二次哈希 - 通过
(n - 1) & hash定位数组下标 - 没有冲突就直接放数组
- 有冲突就挂链表,太长就转红黑树
9.hashmap线程安全问题
- 为什么不安全? 没有加锁 扩容、插入、删除都不是原子操作 多线程竞争直接导致数据不一致
- 怎么解决?(必说) 用 ConcurrentHashMap(推荐) 或者用 Collections.synchronizedMap(new HashMap<>()) 高并发业务优先选择 ConcurrentHashMap
10.ConcurrentHashMap怎么保证线程安全
-
jdk1.7
结构:Segment 分段数组 + HashEntry 数组 + 链表 采用 分段锁(ReentrantLock) 把整个 Map 分成 16 个 Segment,每个 Segment 独立加锁 锁粒度:Segment 级别 优点:并发度高,多线程可以同时操作不同 Segment 缺点:锁层次深,结构复杂
-
JDK 1.8(重点,现在必考)
结构:数组 + 链表 + 红黑树(和 HashMap 结构一致) 放弃分段锁,改用: CAS + synchronized 保证原子性 锁只锁链表头节点 / 红黑树根节点 锁粒度:更细,只锁当前桶(Node) 读操作无锁,通过 volatile 保证可见性 写操作: 不存在节点:用 CAS 尝试插入 已存在节点:对头节点加 synchronized 锁
11.arraylist和linkedlist区别
-
底层数据结构:
ArrayList:动态数组
LinkedList:双向链表
-
访问效率:
ArrayList:支持随机访问,查询快
LinkedList:不支持随机访问,查询慢
-
增删效率:
ArrayList:中间插入、删除要移动元素,慢
LinkedList:指定节点增删只改引用,快
-
内存占用:
ArrayList:内存连续,占用相对少
LinkedList:每个节点存数据 + 前后指针,开销更大
-
使用场景:
频繁查询、遍历多 → 用 ArrayList
频繁头尾 / 中间增删 → 用 LinkedList
12.spring aop
- 是什么 AOP 叫面向切面编程,就是在不修改原有业务代码的前提下,对方法进行统一增强。
- 底层实现 JDK 动态代理(目标类实现接口) CGLIB 代理(目标类没有接口)
- 核心概念 切面(Aspect):要做的增强功能类(如日志、事务) 通知(Advice):增强的具体逻辑 切点(Pointcut):匹配哪些方法需要被增强 连接点(JoinPoint):可以被拦截的方法 目标对象(Target):被代理的原始对象
- 五种通知类型 @Before:前置通知 @After:后置通知(无论是否异常都执行) @AfterReturning:正常返回后执行 @AfterThrowing:抛出异常后执行 @Around:环绕通知(最强大,前后都能控制)
- 典型应用场景 统一日志记录 接口权限校验 性能监控 声明式事务(Spring 事务就是 AOP) 全局异常处理
13.springboot自动配置
- 核心原理: @SpringBootApplication 启动注解
- 流程简化版: 启动 → 开启自动配置 扫描自动配置类 根据条件判断是否生效 自动创建 Bean 放入 Spring 容器 我们直接用,无需手动配置
14.springboot写一个功能的流程是什么
-
建表 / 定义实体类(Entity / POJO)
对应数据库表,字段、主键、约束。
-
写 Mapper / Dao 层
写数据库操作:增删改查
使用 MyBatis-Plus 或原生 MyBatis
-
写 Service 层(核心业务)
接口 + 实现类
写业务逻辑、参数校验、事务控制、调用 mapper
-
写 Controller 层
定义接口地址、请求方式(GET/POST)
接收参数、调用 service、统一返回结果
15.jvm调优
- 调优目的 减少 FullGC 频率,避免长时间 STW 避免 OOM 降低 CPU 占用,提升系统响应速度
- 常用查看命令 jps:查看 Java 进程 jstat -gc:查看 GC 情况 jmap:导出堆内存快照 jstack:导出线程栈,查死锁、CPU 高 jhat / MAT:分析堆 dump 文件
- 常见 JVM 问题 频繁 FullGC 内存泄漏(对象一直引用不释放) 堆内存不足 OOM 元空间溢出 死锁、线程阻塞
- 调优思路(核心) 合理设置堆大小:-Xms 和 -Xmx 设为相同,避免扩容 合理设置新生代比例,让短命对象在 YoungGC 就回收 调整 survivor 区大小,避免对象过早进入老年代 设置元空间大小,防止类过多溢出 选择合适垃圾收集器 低延迟用 G1 高吞吐用 ParallelGC 低延迟要求极高用 ZGC 代码层面避免内存泄漏 关闭连接、流 避免大对象常驻内存 线程池、缓存合理使用
16.分布式限流
- 核心定义 分布式限流,是在分布式系统中,为了保护系统核心资源(如接口、数据库、MQ)不被高流量击垮,而在所有节点 / 服务全局统一控制请求并发量的机制。
- 为什么要用?(解决什么问题) 保护核心资源:防止秒杀、突发流量把 DB 或 Redis 打挂。 全局统一控制:单个节点限流没用,要全链路统一。 削峰填谷:把高峰期流量排队,让系统平稳运行。
- 常见限流算法(面试必讲)
| 算法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 计数器法 | 单位时间内计数,超过阈值就拒绝 | 简单易实现 | 有临界问题(1 秒末和 2 秒初冲量) |
| 滑动窗口 | 时间窗口拆分,动态统计,解决临界 | 精准 | 计算稍复杂 |
| 漏桶算法 | 请求进桶,以固定速率出水,溢出丢弃 | 严格控制出口速率 | 无法应对突发流量 |
| 令牌桶算法 | 系统以固定速率放令牌,请求需拿令牌才能过 | 支持突发流量(推荐) | 实现稍复杂 |
面试一句话推荐: 实际项目中优先用 令牌桶算法,因为它能应对突发流量,且算法成熟。 4. 分布式限流实现方案(核心得分点) 分布式下不能靠本地内存,必须用中间件来共享计数。 方案一:Redis 实现(最常用、最通用) 原理:利用 Redis 的 单线程 + INCR 命令 + 过期时间(TTL)。 代码逻辑: 请求进来,用 INCR 计数。 若计数为 1,设置 EX 过期时间(例如 1 秒)。 判断计数是否超过阈值,超过则拒绝。 优点:分布式、性能高、实现简单。 缺点:存在非原子性风险(可以用 Lua 脚本保证原子性)。 方案二:使用 Redisson 框架(懒人首选) Redisson 封装了 RRateLimiter 分布式限流器,底层就是 Redis + Lua 脚本。 一行代码搞定:
▼java复制代码Java 运行 RRateLimiter limiter = redisson.getRateLimiter("myLimiter"); limiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS); if (limiter.tryAcquire()) { // 处理业务 }
方案三:网关层限流(入口第一道关) 在 Gateway / Nginx 层直接限流。 优点:请求还没进入业务代码,最快拦截。 实现:Nginx 的 limit_req_zone 或 Gateway 的自定义过滤器。 5. 限流粒度与降级策略 限流粒度:按 IP、按用户 ID、按接口路径、按全局服务 QPS。 降级策略(限流拒绝后怎么办?): 返回默认值(兜底数据)。 排队等待(延时处理)。 快速失败(返回友好提示 / JSON)。 降级为缓存(比如查 Redis 而不是查 DB)。
17.Linux常用命令
一、最常用命令(必会)
查看进程:ps -ef | grep xxx、jps 实时进程:top(看 CPU、内存) 查看端口:netstat -tlnp、ss -tlnp 查看日志:tail -f xxx.log、less 查文件大小:du -sh *、df -h 查找文件:find / -name xxx.jar 解压:unzip、tar -zxvf 权限:chmod、chown 网络:ping、curl、telnet
二、线上问题排查(面试重点)
CPU 占用过高 top 找到高 CPU 进程 top -Hp 进程ID 找到高耗 CPU 线程 jstack 进程ID 导出栈,定位代码 内存溢出 OOM free -h 看内存 jmap -dump 导出堆快照 用 MAT 分析泄漏点 磁盘满了 df -h 看磁盘使用率 du -sh * 定位大文件 清理日志、大文件 端口被占用 netstat -tlnp | grep 端口 找到进程 ID,杀掉或改端口 服务假死、不响应 看日志是否报错、死循环 jstack 看是否死锁、大量阻塞
18.cpu突然100%
先用 top 找到高 CPU 进程,再用 top -Hp 定位线程,转 16 进制后通过 jstack 定位具体代码
19.线程生命周期
-
新建(NEW)
new 了线程对象,但还没调用 start()。
-
可运行(RUNNABLE)
调用 start() 之后进入;
包含正在运行、以及等待 CPU 调度的线程。
-
阻塞(BLOCKED)
等待获取 synchronized 锁,才能继续执行。
-
等待(WAITING)
无限期等待,需要其他线程唤醒:
Object.wait()
Thread.join()
LockSupport.park()
-
超时等待(TIMED_WAITING)
带时间的等待,时间到自动唤醒:
Thread.sleep(ms)
Object.wait(ms)
Thread.join(ms)
-
终止(TERMINATED)
线程执行完毕、或异常退出。
20.wait和sleep区别
-
所属类不同
wait() 是 Object 类的方法
sleep() 是 Thread 类的静态方法
-
锁的处理不同【最重要区别】
wait() 会释放锁
sleep() 不释放锁,抱着锁休眠
-
使用环境不同
wait() 必须在 synchronized 同步代码块里使用
sleep() 任何地方都能用
-
唤醒方式不同
wait() 需要 notify() / notifyAll() 唤醒
sleep() 时间到自动醒来
21.线程和进程区别
-
本质区别
进程是操作系统资源分配的最小单位
线程是CPU 调度执行的最小单位
-
资源共享
进程之间独立,不共享资源
同一进程内的线程共享堆、方法区、文件句柄等资源
-
独有资源
线程有自己独立的栈、程序计数器、局部变量
-
开销
进程创建、销毁、切换开销大
线程创建、切换开销小
-
稳定性
一个进程崩溃,一般不影响其他进程
一个线程崩溃,整个进程都会挂掉
