某大厂后端一面

场景题,实践题目居多,考察实际能力。

1.Java基础功底

复制代码
public class Test {
      public static void main(String[] args) {
          Integer a = 127;
          Integer b = 127;
          Integer c = 128;
          Integer d = 128;
          System.out.println(a == b);   // 输出?
          System.out.println(c == d);   // 输出?
      }
  }

请问上面两行输出分别是什么?为什么?

考察对于Integer包装类的理解,Integer 缓存池范围是 -128~127

  • a == btrue:因为 127 在 Integer 缓存池范围内,二者指向同一个对象。

  • c == dfalse:128 超出了缓存池范围,会分别创建两个新对象,地址不同。

深入细节:

Integer a = 127; 实际上会被编译器转换为:

Integer a = Integer.valueOf(127);

所以关键就在于 Integer.valueOf(int i) 的源码实现。

复制代码
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high) {
        return IntegerCache.cache[i + (-IntegerCache.low)];
    }
    return new Integer(i);
}
  • 如果参数在缓存范围内,直接从缓存数组中取同一个对象返回。

  • 如果不在范围内,直接 new Integer(i) 创建一个新对象。

缓存范围源码:

复制代码
private static class IntegerCache {
    static final int low = -128;          // 固定下限
    static final int high;                // 可配置上限
    static final Integer cache[];

    static {
        // 默认上限 = 127
        int h = 127;
        // 可通过 JVM 参数修改:-XX:AutoBoxCacheMax=<size>
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
        }
        high = h;
        // 初始化缓存数组 [-128, 127] 的 Integer 对象
        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++) {
            cache[k] = new Integer(j++);
        }
    }
}

补充:new Integer(127) == Integer.valueOf(127) 结果?

falsenew 一定在堆上创建新对象,valueOf() 从缓存取,地址不同。

2.单例设计模式深入

复制代码
 public class Singleton {
      private static Singleton instance;

      public static Singleton getInstance() {
          if (instance == null) {
              synchronized (Singleton.class) {
                  if (instance == null) {
                      instance = new Singleton();
                  }
              }
          }
          return instance;
      }
  }

这段代码是 DCL(双重检查锁)单例,它有什么问题?

这段代码在没有 volatile 的情况下,可能会因为指令重排序 返回一个未完全初始化好的对象

具体细节:

instance = new Singleton(); 这行代码在 JVM 层面并不是一个原子操作,它大致分为三步:

  1. 分配内存 :在堆上为 Singleton 对象分配内存空间。

  2. 初始化对象 :调用 Singleton 的构造方法,完成内部字段的初始化。

  3. 将引用指向内存 :将 instance 变量指向刚分配的内存地址。

单线程 中,即使 2 和 3 被重排序,也不影响最终结果。但在多线程 下,如果发生 2 和 3 的重排序,就会产生严重问题:

复制代码
重排序后顺序:
① 分配内存
③ 建立引用 (instance 不再为 null)
② 初始化对象

假设有两个线程 AB ,同时调用 getInstance()

时间线 线程 A(首次创建) 线程 B(并发获取)
T1 进入 getInstance(),看到 instance == null 为真。 ---
T2 获取到 synchronized 锁,进入同步块。 ---
T3 再次检查 instance == null 为真,开始执行 instance = new Singleton();恰好发生指令重排序 :先分配内存、将引用赋给 instance但还没有调用构造函数 ---
T4 --- 此时线程 B 也进入 getInstance(),执行第一个 if (instance == null)。 因为 A 已经将 instance 指向了未初始化完毕的内存,instance 此时 != null
T5 --- 线程 B 直接跳过 synchronized ,返回了一个 半成品对象
T6 线程 A 继续执行完构造函数,对象才真正初始化完毕。 线程 B 已经拿着一个未初始化完全的对象开始调用了,可能导致 NPE 或逻辑错误。

这就是典型的对象逸出问题:线程 B 读到了不为 null、但实际上构造还没跑完的"残缺"对象。

volatile 在这里起到两个关键作用:

  1. 禁止指令重排序(最关键)

    JMM 规定,在 volatile 变量的写操作之前,不能将其后的指令(即对象的初始化)与写操作重排序。也就是说,必须严格按照 分配内存 → 初始化 → 建立引用 的顺序执行。这样线程 B 看到 instance != null 时,对象一定已经初始化完毕。

  2. 保证内存可见性
    volatile 变量的写操作会立即刷入主存,读操作会直接从主存中拿,保证所有线程看到的都是最新状态,不会出现从本地线程缓存里读到过期值的情况。

主包再总结一下:如果不加 volatile,第 2 步和第 3 步可能被 重排序,导致线程 A 刚执行完 3(引用已赋值但对象还没初始化),线程 B进来检测到 instance != null,直接返回了一个 半初始化的对象------此时访问成员变量可能读到默认值(0/null),引发奇怪的线上bug

3.线上问题排查

某线上服务突然 CPU 飙升到100%,你没有任何监控平台,只能上机器用命令行排查。请说出你从登录服务器到定位问题的完整排查步骤

1、ssh登录服务器

2、确认整体状况------top

复制代码
top -c
  • 作用:查看系统整体 CPU、内存、负载,并按 CPU 使用率排序,显示完整命令行。

  • 关注:

    • 第一行 load average 如果很高,说明系统压力大。

    • 找到 CPU 或内存占用最高的进程 PID。

    • 通常按 P 可以按 CPU 排序(默认就是)。

假设我们看到 PID 31415 的 Java 进程 CPU 100%。

3、定位到高 CPU 的线程------top -H

复制代码
top -H -p 31415
  • -H:显示线程级别信息。

  • -p:指定进程。

  • P 确保按 CPU 排序,找出 CPU 占用最高的线程 ID(TID ),比如 31416

补充:也可以使用 ps -eLf | grep 31415ps -T -p 31415 查看线程。

4、线程 ID 转十六进制

复制代码
printf "%x\n" 31416

假设输出为 7ab8

  • 原因:jstack 输出的线程栈中,nid 字段是十六进制的,需要转换。

5.用 jstack 打印线程堆栈

复制代码
jstack 31415 > jstack.log
  • 查看该线程的 状态代码调用栈

常见高 CPU 线程状态及对应原因:

状态 可能原因
RUNNABLE 线程正在执行代码,可能是死循环、大量计算、正则回溯等
BLOCKED 等待获取对象锁,说明锁竞争严重
WAITING / TIMED_WAITING 线程在等待其他资源,CPU 高可能是其他线程导致的?但本身不会占 CPU
GC 线程 如果是 VM 线程如 GC task thread#... 占用高 CPU,说明 GC 频繁
  • 如果看到的是 VM ThreadGang worker#...,说明是 GC 线程,需要转向 GC 排查。

6、分析堆栈代码

假设看到的是一个业务线程,处于 RUNNABLE,栈顶指向某个方法:

复制代码
"high-cpu-thread" #34 prio=5 os_prio=0 tid=0x00007f9a0c009800 nid=0x7ab8 runnable [0x00007f9a1f4e5000]
   java.lang.Thread.State: RUNNABLE
	at com.water.service.DataAnalyzer.analyze(DataAnalyzer.java:88)
	at com.water.service.DataAnalyzer.run(DataAnalyzer.java:43)
	...
  • 立即检查对应代码:是否存在死循环、大正则、递归、大量无缓存的字符串操作等。

7、检查 GC 状况------jstat

jstat -gcutil 31415 1000 10

  • 参数:1000 毫秒一次,打印 10 次。

  • 关注 FGC(Full GC 次数)和 FGCT(Full GC 总耗时)。如果 FGC 增长快、耗时大,说明内存不足,GC 线程消耗大量 CPU。

  • 同时看 E(Eden)、O(Old)、M(Metaspace)使用率是否接近 100%。

如果是 GC 频繁导致 CPU 高,需要进一步:

复制代码
jmap -heap 31415    # 查看堆配置和使用详情
jmap -histo 31415 | head -30   # 查看对象实例数量排名,找出可能的内存泄漏对象
jmap -dump:live,format=b,file=heap.bin 31415  # 生成堆转储(慎用,会挂起应用)

4.SQL优化实操

复制代码
有一张订单表 t_order(order_id, user_id, status, create_time, amount),数据量 2000 万行。现在这个查询很慢:

  SELECT * FROM t_order
  WHERE status = 'PAID'
  ORDER BY create_time DESC
  LIMIT 100000, 20;

  请说说为什么慢?你能给出哪些优化方案?(至少说出 2 种)

方案一:使用"覆盖索引 + 延迟关联"【最推荐】

原理:先用一个紧凑的覆盖索引快速定位到主键,再用主键去取完整数据,避免大量回表。

复制代码
SELECT o.*
FROM t_order o
INNER JOIN (
    SELECT order_id
    FROM t_order
    WHERE status = 'PAID'
    ORDER BY create_time DESC
    LIMIT 100000, 20
) AS tmp ON o.order_id = tmp.order_id;

所需索引(status, create_time, order_id)

因为子查询只用到 status, create_time, order_id,这个联合索引完全覆盖了子查询,不需要回表,索引体积小,扫描 100020 行极快。外层再用主键 order_id 精确关联取 20 行完整数据。

方案 2:将 LIMIT 大分页改为"游标分页"(基于上次位置)

原理 :不再使用 LIMIT offset, size 这种逐页跳转,而是记住上一页最后一条的 create_time 和主键,用条件筛选下一页。

复制代码
-- 第1页
SELECT * FROM t_order
WHERE status = 'PAID'
ORDER BY create_time DESC, order_id DESC
LIMIT 20;

-- 记下上一页最后一条的 create_time 和 order_id,比如 '2025-01-01 10:00:00' 和 123456

-- 第2页
SELECT * FROM t_order
WHERE status = 'PAID'
  AND (create_time < '2025-01-01 10:00:00'
       OR (create_time = '2025-01-01 10:00:00' AND order_id < 123456))
ORDER BY create_time DESC, order_id DESC
LIMIT 20;

5.Redis高并发

一个电商系统的商品详情页,QPS 大约是 8000,主要用 Redis 做缓存。某天活动中,一个热门商品 key在失效的瞬间,突然有大量请求同时穿透到数据库,导致数据库连接池被打满。

这是什么问题?

如果不解决,下一次该 key 失效时还会复现吗?为什么?

请给出至少两种解决思路。

这是典型的缓存击穿(Cache Breakdown)

定义

缓存中某个热点 Key(高并发访问的 Key)在过期的瞬间,大量请求同时涌入,绕过缓存直接打到数据库,导致数据库压力骤增,甚至宕机。

与另外两个易混淆概念的区分

  • 缓存雪崩大量 Key 在同一时间段集体过期,或缓存服务宕机,导致请求大面积打到 DB。

  • 缓存穿透 :请求的数据在缓存和数据库中都不存在,导致每次请求都打到 DB(通常用布隆过滤器或空值缓存解决)。

你这里的现象是单个热门商品 Key 过期,完全符合缓存击穿特征。

如果不解决,下一次该 Key 失效时还会复现吗?为什么?

会,一定会复现。

这个热门 Key 仍然会被设置 TTL(比如 10 分钟)。到期后,Key 自动删除,缓存再次为空。

互斥锁(分布式锁)------ 简单粗暴,强一致性

原理

当缓存失效时,不允许多个请求同时去查数据库,而是用一把分布式锁 (如 Redis 的 SETNX),保证只有一个请求能去加载 DB 并回写缓存,其他请求则等待或重试。

流程

  1. 请求发现缓存未命中。

  2. 尝试获取分布式锁(例如 SET lock:product:123 1 NX EX 10)。

  3. 获取锁成功的请求:查询数据库 → 写回缓存 → 释放锁。

  4. 获取锁失败的请求:休眠几十毫秒后重新查缓存(此时缓存可能已被更新),或者返回一个降级数据(如"请稍后重试")。

  5. 回写缓存时,依然要设置合理的过期时间(通常加上随机抖动,避免再次同时过期)。

优点 :实现简单,数据一致性强。
缺点:有短暂的等待时间;锁需要处理好死锁和超时。

逻辑过期(永不过期)------ 性能最优,允许最终一致

原理

Redis 中缓存的 Key 不设置 TTL (物理上永不过期),而是在缓存的 Value 中增加一个 逻辑过期时间(如一个时间戳字段)。所有读请求都能直接拿到缓存值(旧值也没关系),而缓存的异步刷新由一个后台线程或定时任务完成。

流程

  1. 缓存 Value 结构示例:{ "data": {...}, "expireAt": 1715692800000 }

  2. 请求永远能从缓存中读到这个 Value。

  3. 代码中判断:如果当前时间 > expireAt,说明数据已经"逻辑过期"。

  4. 此时返回旧的缓存数据同时 ,触发一个异步任务(或尝试获取互斥锁)去更新缓存:查询数据库,写回缓存(同时更新 expireAt)。

  5. 在异步更新完成前,所有请求依旧返回旧数据,完全不会穿透到 DB。

优点

  • 请求零等待,完全不阻塞。

  • 彻底杜绝了瞬时穿透,性能最好。

缺点

  • 会短暂返回过期数据,适用于对一致性要求不是"绝对实时"的场景(商品详情页完全适用)。

  • 需要一个异步线程池或消息队列来执行刷新任务。

相关推荐
W23035765731 小时前
Linux C++ 基于 timerfd + epoll 实现高性能定时器队列(完整源码 + 超详细解析)
linux·开发语言·c++·线程池
爱笑的源码基地1 小时前
拿来即用:基于Spring Cloud+UniApp的智慧工地源码,架构清晰易扩展
java·云计算·源码·智慧工地·程序·开箱即用·数字工地
WL_Aurora1 小时前
Java技术体系:JDK、JRE、JVM的关系与演进(2026最新版)
java·开发语言·jvm
吃好睡好便好1 小时前
在Matlab中绘制二维等高线图
开发语言·人工智能·学习·算法·matlab
wkj0011 小时前
JavaScript模块化技术进程详解
开发语言·javascript·ecmascript
2zcode1 小时前
基于Matlab元胞自动机模拟(CA)动态再结晶过程
开发语言·matlab·动态再结晶
Gerardisite1 小时前
企业微信怎么玩?用 API 打造智能私域助手
开发语言·python·机器人·企业微信
砚底藏山河1 小时前
股票数据API接口:(沪深A股)如何获取股票当天逐笔交易数据
java·windows·python·maven
buhuizhiyuci1 小时前
【QT-百日筑基篇】打完完怪,开始学炼丹, 前往藏书阁寻找对应材料的信息,并前往去寻找对应材料-QT信号和槽
开发语言·qt