20260409

问题

1.互斥锁

互斥锁: 同一时间只有一个线程能拿到锁,其他线程必须等着。保证同一时间,只有一个线程执行某段代码。

作用:保证线程安全,让并发修改变成排队执行。

原理:

  • 锁只有一把
  • 线程 A 拿到锁 → 进去执行代码
  • 线程 B 来拿锁 → 拿不到,阻塞等待
  • 线程 A 执行完 → 释放锁
  • 线程 B 才能拿到锁,进去执行

2.熔断和降级的区别

熔断是下游服务异常时,调用方主动切断调用,避免故障扩散、防止服务雪崩。

降级是系统压力过大时,主动关闭非核心功能,保证核心服务稳定运行。

两者目的都是高可用,但熔断是因为别人坏了,降级是因为自己扛不住了。

3.lock和synchronized 区别

synchronized 是 JVM 实现的隐式锁,自动加锁、自动释放,使用简单,不能中断、不支持公平锁、不支持超时获取锁。

Lock 是 JDK 实现的显式锁,需要手动 lock/unlock,支持可重入、可中断、可超时、公平锁、多 Condition,功能更强大灵活。

简单场景用 synchronized,复杂高并发场景用 Lock。

4.线程池的执行过程

  1. 任务提交过来,先判断核心线程数是否已满
  2. 没满:直接创建核心线程执行任务
  3. 满了:进入下一步
  4. 判断工作队列是否已满
  5. 没满:把任务放进队列排队
  6. 满了:进入下一步
  7. 判断最大线程数是否已满
  8. 没满:创建非核心线程执行任务
  9. 满了:进入下一步
  10. 执行拒绝策略
  11. 比如抛异常、丢弃任务、丢弃最老任务、调用者线程执行等

5.类加载器

  • 启动类加载器(Bootstrap ClassLoader) C++ 实现,JVM 自带 加载 JAVA_HOME/lib 下的核心包,比如 rt.jar
  1. 扩展类加载器(Extension ClassLoader) Java 实现 加载 JAVA_HOME/lib/ext 下的扩展 jar
  2. 应用类加载器(App ClassLoader / 系统类加载器) 加载我们项目 classpath 下的类 平时自己写的代码、引入的第三方 jar 都由它加载
  3. 自定义类加载器 继承 ClassLoader 自己实现 用于热部署、加密加载、模块化加载等场景(比如 Tomcat、SPI)

6.索引失效

1.对索引字段做函数运算、数学运算

比如:age + 1 = 20substring(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.线程池满了服务挂了

为什么:

  1. 任务无限提交,核心线程 + 最大线程都跑满

  2. 任务队列也被塞满

  3. 触发拒绝策略,但如果拒绝策略不合理

    比如直接抛异常,大量请求失败

    或者用 CallerRuns 让主线程执行业务,导致主线程被堵死

  4. 最终表现:接口大量超时、服务不响应、甚至整个应用假死

怎么排查:

  1. 看日志:有没有 RejectedExecutionException

  2. 用 jstack 看线程:

    ​ 是不是所有线程都在 RUNNABLE

    ​ 是不是大量线程卡在数据库、第三方接口

  3. 看队列:是不是队列设置太大,OOM 了

  4. 看业务:是不是下游响应慢,导致线程长时间占着不释放

怎么解决:

降级限流

  • 入口加限流,不让任务无限进来

优化线程池参数

  • 合理设置核心线程、最大线程、队列长度
  • 队列不要设置无界,否则会 OOM

异步任务超时控制

  • 线程执行必须加超时,避免线程一直被占住

合理拒绝策略

  • 不能无脑抛异常
  • 重要任务用降级、缓存、兜底

异步解耦

  • 把线程池替换成 MQ 削峰填谷
  • 线程池只处理轻量任务,不处理长耗时任务

监控告警

  • 监控队列长度、活跃线程数、拒绝任务数

8.hashmap底层数据结构

jdk1.8:

​ 数组 + 链表 + 红黑树

​ 主体是:哈希表(Entry 数组,也叫桶)

​ 哈希冲突时:拉链法 —— 链表

​ 当链表长度 ≥ 8 且数组长度 ≥ 64 时:转为红黑树

​ 当树节点 ≤ 6 时:退化成链表

jdk1.7:

​ 数组 + 链表

​ 只有数组 + 链表,没有红黑树

​ 冲突时采用头插法,高并发下容易成环死循环

核心流程:

  1. 根据 key 计算 hashcode,再做二次哈希
  2. 通过 (n - 1) & hash 定位数组下标
  3. 没有冲突就直接放数组
  4. 有冲突就挂链表,太长就转红黑树

9.hashmap线程安全问题

  1. 为什么不安全? 没有加锁 扩容、插入、删除都不是原子操作 多线程竞争直接导致数据不一致
  2. 怎么解决?(必说) 用 ConcurrentHashMap(推荐) 或者用 Collections.synchronizedMap(new HashMap<>()) 高并发业务优先选择 ConcurrentHashMap

10.ConcurrentHashMap怎么保证线程安全

  1. jdk1.7

    结构:Segment 分段数组 + HashEntry 数组 + 链表 采用 分段锁(ReentrantLock) 把整个 Map 分成 16 个 Segment,每个 Segment 独立加锁 锁粒度:Segment 级别 优点:并发度高,多线程可以同时操作不同 Segment 缺点:锁层次深,结构复杂

  2. JDK 1.8(重点,现在必考)

    结构:数组 + 链表 + 红黑树(和 HashMap 结构一致) 放弃分段锁,改用: CAS + synchronized 保证原子性 锁只锁链表头节点 / 红黑树根节点 锁粒度:更细,只锁当前桶(Node) 读操作无锁,通过 volatile 保证可见性 写操作: 不存在节点:用 CAS 尝试插入 已存在节点:对头节点加 synchronized 锁

11.arraylist和linkedlist区别

  1. 底层数据结构:

    ArrayList:动态数组

    LinkedList:双向链表

  2. 访问效率:

    ArrayList:支持随机访问,查询快

    LinkedList:不支持随机访问,查询慢

  3. 增删效率:

    ArrayList:中间插入、删除要移动元素,慢

    LinkedList:指定节点增删只改引用,快

  4. 内存占用:

    ArrayList:内存连续,占用相对少

    LinkedList:每个节点存数据 + 前后指针,开销更大

  5. 使用场景:

    频繁查询、遍历多 → 用 ArrayList

    频繁头尾 / 中间增删 → 用 LinkedList

12.spring aop

  1. 是什么 AOP 叫面向切面编程,就是在不修改原有业务代码的前提下,对方法进行统一增强。
  2. 底层实现 JDK 动态代理(目标类实现接口) CGLIB 代理(目标类没有接口)
  3. 核心概念 切面(Aspect):要做的增强功能类(如日志、事务) 通知(Advice):增强的具体逻辑 切点(Pointcut):匹配哪些方法需要被增强 连接点(JoinPoint):可以被拦截的方法 目标对象(Target):被代理的原始对象
  4. 五种通知类型 @Before:前置通知 @After:后置通知(无论是否异常都执行) @AfterReturning:正常返回后执行 @AfterThrowing:抛出异常后执行 @Around:环绕通知(最强大,前后都能控制)
  5. 典型应用场景 统一日志记录 接口权限校验 性能监控 声明式事务(Spring 事务就是 AOP) 全局异常处理

13.springboot自动配置

  1. 核心原理: @SpringBootApplication 启动注解
  2. 流程简化版: 启动 → 开启自动配置 扫描自动配置类 根据条件判断是否生效 自动创建 Bean 放入 Spring 容器 我们直接用,无需手动配置

14.springboot写一个功能的流程是什么

  1. 建表 / 定义实体类(Entity / POJO)

    对应数据库表,字段、主键、约束。

  2. 写 Mapper / Dao 层

    写数据库操作:增删改查

    使用 MyBatis-Plus 或原生 MyBatis

  3. 写 Service 层(核心业务)

    接口 + 实现类

    写业务逻辑、参数校验、事务控制、调用 mapper

  4. 写 Controller 层

    定义接口地址、请求方式(GET/POST)

    接收参数、调用 service、统一返回结果

15.jvm调优

  1. 调优目的 减少 FullGC 频率,避免长时间 STW 避免 OOM 降低 CPU 占用,提升系统响应速度
  2. 常用查看命令 jps:查看 Java 进程 jstat -gc:查看 GC 情况 jmap:导出堆内存快照 jstack:导出线程栈,查死锁、CPU 高 jhat / MAT:分析堆 dump 文件
  3. 常见 JVM 问题 频繁 FullGC 内存泄漏(对象一直引用不释放) 堆内存不足 OOM 元空间溢出 死锁、线程阻塞
  4. 调优思路(核心) 合理设置堆大小:-Xms 和 -Xmx 设为相同,避免扩容 合理设置新生代比例,让短命对象在 YoungGC 就回收 调整 survivor 区大小,避免对象过早进入老年代 设置元空间大小,防止类过多溢出 选择合适垃圾收集器 低延迟用 G1 高吞吐用 ParallelGC 低延迟要求极高用 ZGC 代码层面避免内存泄漏 关闭连接、流 避免大对象常驻内存 线程池、缓存合理使用

16.分布式限流

  1. 核心定义 分布式限流,是在分布式系统中,为了保护系统核心资源(如接口、数据库、MQ)不被高流量击垮,而在所有节点 / 服务全局统一控制请求并发量的机制。
  2. 为什么要用?(解决什么问题) 保护核心资源:防止秒杀、突发流量把 DB 或 Redis 打挂。 全局统一控制:单个节点限流没用,要全链路统一。 削峰填谷:把高峰期流量排队,让系统平稳运行。
  3. 常见限流算法(面试必讲)
算法原理优点缺点
计数器法单位时间内计数,超过阈值就拒绝简单易实现有临界问题(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.线程生命周期

  1. 新建(NEW)

    new 了线程对象,但还没调用 start()。

  2. 可运行(RUNNABLE)

    调用 start() 之后进入;

    包含正在运行、以及等待 CPU 调度的线程。

  3. 阻塞(BLOCKED)

    等待获取 synchronized 锁,才能继续执行。

  4. 等待(WAITING)

    无限期等待,需要其他线程唤醒:

    Object.wait()

    Thread.join()

    LockSupport.park()

  5. 超时等待(TIMED_WAITING)

    带时间的等待,时间到自动唤醒:

    Thread.sleep(ms)

    Object.wait(ms)

    Thread.join(ms)

  6. 终止(TERMINATED)

    线程执行完毕、或异常退出。

20.wait和sleep区别

  1. 所属类不同

    wait() 是 Object 类的方法

    sleep() 是 Thread 类的静态方法

  2. 锁的处理不同【最重要区别】

    wait() 会释放锁

    sleep() 不释放锁,抱着锁休眠

  3. 使用环境不同

    wait() 必须在 synchronized 同步代码块里使用

    sleep() 任何地方都能用

  4. 唤醒方式不同

    wait() 需要 notify() / notifyAll() 唤醒

    sleep() 时间到自动醒来

21.线程和进程区别

  1. 本质区别

    进程是操作系统资源分配的最小单位

    线程是CPU 调度执行的最小单位

  2. 资源共享

    进程之间独立,不共享资源

    同一进程内的线程共享堆、方法区、文件句柄等资源

  3. 独有资源

    线程有自己独立的栈、程序计数器、局部变量

  4. 开销

    进程创建、销毁、切换开销大

    线程创建、切换开销小

  5. 稳定性

    一个进程崩溃,一般不影响其他进程

    一个线程崩溃,整个进程都会挂掉

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
9716
下载 APP