Java面试案例题

wen java案例 2

本文目录导读:

Java面试案例题

  1. 案例一:字符串与常量池
  2. 案例二:HashMap原理与扩容
  3. 案例三:线程池参数实践
  4. 案例四:锁的升级与死锁排查
  5. 案例五:Spring Bean 生命周期
  6. 案例六:SQL 索引失效(典型场景)
  7. 使用建议

我为你整理了一套Java面试案例题,包含代码案例常见面试追问解析,覆盖核心知识点,这套题既适合模拟面试,也适合自查基础。


字符串与常量池

public class StringTest {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "abc";
        String s3 = new String("abc");
        String s4 = new String("abc");
        System.out.println(s1 == s2);
        System.out.println(s1 == s3);
        System.out.println(s3 == s4);
        System.out.println(s1 == s3.intern());
    }
}

问题:

  1. 请说出以上代码的输出结果。
  2. 解释一下JVM字符串常量池的作用。
  3. intern() 方法的作用是什么?
  4. 在JDK 1.6和JDK 1.7+中,字符串常量池的位置有什么区别?

✅ 参考答案

true    // s1与s2都指向常量池中的同一个对象
false   // s3是堆中新建的对象,s1是常量池中的对象
false   // new两次是不同对象
true    // s3.intern()返回常量池中的引用,即s1

解析:

  • s1s2 在编译期就确定,直接放入常量池,所以指向同一地址。
  • new String("abc") 会在中创建新对象(即使常量池已有"abc"),s3 != s1
  • 连续 new 两次,堆中对象引用不同,s3 != s4
  • intern() 首先检查字符串常量池中是否有该字符串,若存在则返回常量池引用,若不存在则将该字符串放入常量池并返回引用。s3.intern() 返回的是 s1 的引用,所以为 true

追问加分点:

  • JDK 1.6 中常量池在永久代(PermGen)intern()复制一份放入永久代;
  • JDK 1.7+ 中常量池移到堆(Heap)intern() 若池中不存在,则将该对象的引用放入池中(不会再复制一份),不同版本的 intern() 行为略有差异。

HashMap原理与扩容

你有一个 HashMap,初始容量是 16,加载因子默认是 0.75,请问:

  1. 当元素个数达到多少时会发生第一次扩容?
  2. 扩容后容量是多少?
  3. 简述 JDK 1.8 的 HashMap 底层数据结构。
  4. 在多线程环境下,HashMap 会出现什么问题?如何解决?

✅ 参考答案

  1. 阈值 = 容量 × 加载因子 = 16 × 0.75 = 12,当插入第 13 个元素(size > 12)时触发扩容。
  2. 扩容后容量变为 32(每次翻倍,即 cap << 1),但源码中 不是直接乘以2,而是调用 resize() 方法,计算新容量为 oldCap << 1
  3. JDK 1.8 HashMap 底层 = 数组 + 链表 + 红黑树
    • 数组默认长度 16;
    • 当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树;
    • 当树节点 ≤ 6 时,红黑树退化为链表。
  4. 多线程环境下:
    • JDK 1.7:并发 put 可能导致链表成环(形成死循环),引起 CPU 100%问题。
    • JDK 1.8:不会产生链表成环,但仍然有数据覆盖丢失size 统计不准确)。
    • 解决方案:使用 Collections.synchronizedMap()ConcurrentHashMap(推荐,锁粒度更细,使用 CAS + synchronized 锁住桶节点)。

线程池参数实践

你是一家电商公司的后端开发,系统突然面临高并发流量,经排查发现核心业务线程池配置如下:

ExecutorService pool = new ThreadPoolExecutor(
    2,          // corePoolSize
    10,         // maximumPoolSize
    60L,        // keepAliveTime
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(5)  // 队列容量为5
);

问题:

  1. 请描述该线程池的任务处理流程。
  2. 线程池支持的最大并发任务数(队列+最大线程)是多少?
  3. 如果此时来了 20 个任务,会有什么现象?
  4. 什么是拒绝策略?默认策略是什么?如果让你选,在电商下单场景你会选哪种?

✅ 参考答案

任务处理流程:

  • 核心线程数为 2,任务数 ≤ 2 时,直接由核心线程执行;
  • 任务数 > 2 时,新任务进入阻塞队列
  • 队列满(容量 5)时,继续提交任务会创建新线程(非核心线程),直到线程数达到 maximumPoolSize(10);
  • 线程数达到最大值且队列已满,再有新任务则触发拒绝策略

最大并发任务数 = 队列容量 5 + 最大线程数 10 = 15,这意味着最多同时存 15 个任务(10个在执行,5个等待)。

来 20 个任务:

  • 前 2 个 → 核心线程执行;
  • 第 3~7 个 → 进入队列(5个);
  • 第 8~17 个 → 创建新线程执行(扩充至最大线程10,即额外的8个新线程);
  • 第 18~20 个 → 触发拒绝策略。

拒绝策略:

  • 4种内置策略:
    • AbortPolicy(默认):直接抛异常 RejectedExecutionException
    • CallerRunsPolicy:提交任务的线程自己执行该任务;
    • DiscardPolicy:直接丢弃任务,无任何提示;
    • DiscardOldestPolicy:丢弃队列最老的任务,重试提交当前任务。
  • 电商下单场景推荐 CallerRunsPolicy:退化为同步执行,保证不丢订单,同时天然限流(不会让下游压力过大)。

锁的升级与死锁排查

public class DeadLockDemo {
    private static Object resourceA = new Object();
    private static Object resourceB = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resourceA) {
                System.out.println("线程1拿到A锁");
                try { Thread.sleep(100); } catch (Exception e) {}
                synchronized (resourceB) {
                    System.out.println("线程1拿到B锁");
                }
            }
        }).start();
        new Thread(() -> {
            synchronized (resourceB) {
                System.out.println("线程2拿到B锁");
                try { Thread.sleep(100); } catch (Exception e) {}
                synchronized (resourceA) {
                    System.out.println("线程2拿到A锁");
                }
            }
        }).start();
    }
}

问题:

  1. 这段代码会发生什么?为什么?
  2. 如何定位死锁问题?(请给出命令行排查步骤)
  3. 如何避免死锁?

✅ 参考答案

会发生死锁。

  • 线程1持有 resourceA,等待 resourceB
  • 线程2持有 resourceB,等待 resourceA
  • 两个线程互相等待对方的锁,形成循环等待,程序永远不会结束。

排查步骤:

# 步骤1:查看Java进程ID
jps -l
# 步骤2:输出线程快照(关键!)
jstack <PID>

jstack 输出中搜索 "Found one Java-level deadlock",即可看到死锁的线程和锁信息。

也可以使用图形化工具:

jconsole <PID>

在“线程”标签页中,死锁线程会以红色标记。

避免死锁的方法:

  • 加锁顺序一致:所有线程按照相同顺序获取锁(比如都先拿A再拿B);
  • 使用 tryLock 超时:使用 ReentrantLock.tryLock(timeout),拿不到锁则放弃;
  • 使用 ConcurrentHashMap/Atomic 等无锁并发容器替代加锁;
  • 尽量缩小同步代码块范围,减少持锁时间。

Spring Bean 生命周期

在Spring中,一个普通Bean从创建到销毁的完整生命周期中,会经过哪些主要阶段?

追问:

  1. @Autowired 依赖注入发生在哪个阶段?
  2. @PostConstructInitializingBean.afterPropertiesSet() 的执行顺序是什么?
  3. Spring 的循环依赖是如何解决的?(三级缓存)

✅ 参考答案

  • 完整生命周期简版:
    实例化(new) → 属性填充(依赖注入) → Aware接口回调
    → BeanPostProcessor前置处理 → @PostConstruct/InitializingBean
    → BeanPostProcessor后置处理(AOP代理在这里产生)
    → 使用Bean → 容器关闭 → @PreDestroy/DisposableBean

依赖注入阶段: @Autowired属性填充(Populate Bean) 阶段完成,属于实例化之后、初始化之前。

执行顺序:

@PostConstruct(JSR-250标准)
↓
InitializingBean.afterPropertiesSet()(Spring接口)
↓
自定义init-method(XML注解)

Spring循环依赖(三级缓存):

  • 一级缓存:singletonObjects(完整的单例Bean);
  • 二级缓存:earlySingletonObjects(早期暴露的Bean,还没完成属性填充);
  • 三级缓存:singletonFactories(单例工厂,用于生成代理对象)。

解决流程(A→B→A循环):

  1. 创建A → 实例化A → 提前暴露A的三级缓存
  2. 填充A的属性时发现需要B → 创建B;
  3. B填充属性时需要A → 从三级缓存中找到A的早期引用 → 赋值给B;
  4. B创建完成 → 返回给A → A正常完成初始化;
  5. 最终A放到一级缓存,三级缓存清除。

注意:单例模式非构造器注入的循环依赖能解决;构造器注入的循环依赖无法解决,会直接报错。


SQL 索引失效(典型场景)

假设有一张订单表 t_order,包含字段 order_no(VARCHAR)user_id(BIGINT)status(TINYINT)create_time(DATETIME)

你为这张表创建了联合索引:idx_user_status (user_id, status) 和普通索引 idx_create_time (create_time)

以下 SQL 中哪些会走索引,哪些会索引失效

-- ①
SELECT * FROM t_order WHERE user_id = 100 AND status = 1;
-- ②
SELECT * FROM t_order WHERE status = 1;
-- ③
SELECT * FROM t_order WHERE user_id = 100 ORDER BY create_time;
-- ④
SELECT * FROM t_order WHERE create_time BETWEEN '2024-01-01' AND '2024-01-31';

✅ 参考答案

  • ① 走索引:联合索引最左前缀原则,user_idstatus 顺序符合,完美命中 idx_user_status
  • ② 索引失效:只查 status,跳过了最左列 user_id最左前缀法则失效,不会走联合索引,可能全表扫描。
  • ③ 索引失效ORDER BY create_time 时,由于 create_time 不在联合索引 idx_user_status 中,无法利用索引排序;需要走文件排序(filesort)
  • ④ 走索引create_time 独立索引,范围查询可以正常走 idx_create_time 索引。

追问: status 字段只有 0/1/2 三种值(区分度极低),索引效果如何?

回答: 区分度太低,索引基本没效果,MySQL优化器可能放弃索引直接走全表扫描,即使强行走索引,回表代价远大于全表,建议去掉 status 上的单列索引。


使用建议

6个案例覆盖了:

模块 案例
JVM/字符串 案例一
集合/并发 案例二
线程池/高并发 案例三
锁/死锁 案例四
Spring核心 案例五
MySQL索引 案例六

你可以针对薄弱环节单独拆开来练习,需要我针对某个案例再出一套更深入的连环追问或者面试官视角的评分标准,随时告诉我。

抱歉,评论功能暂时关闭!