Java 基础 & Web 面试大全

6 阅读32分钟

Java 基础 & Web 面试大全


一、Java 基础

1. JVM 由几部分构成?作用是什么?

JVM(Java 虚拟机)主要由以下几部分组成:

组成部分作用
类加载器(ClassLoader)负责 .class 字节码文件加载到 JVM 中,包括启动类加载器、扩展类加载器、应用类加载器
运行时数据区JVM 内存区域划分,包含:方法区、堆、虚拟机栈、本地方法栈、程序计数器
执行引擎解释执行或编译执行字节码(JIT 即时编译器),负责执行指令
本地接口(JNI)与本地库交互,调用 C/C++ 编写的 native 方法

运行时数据区详解:

  • 程序计数器:记录当前线程执行的字节码行号,线程私有
  • 虚拟机栈:存储栈帧(局部变量表、操作数栈等),每个方法调用创建一个栈帧,线程私有
  • 本地方法栈:为 Native 方法服务,线程私有
  • 堆(Heap):存放对象实例和数组,所有线程共享,GC 主要管理区域
  • 方法区:存储类信息、常量池、静态变量、编译后的代码,所有线程共享

2. 接口和抽象类的区别

对比维度接口(interface)抽象类(abstract class)
关键字interfaceabstract class
继承关系多实现(implements),一个类可实现多个接口单继承(extends),一个类只能继承一个抽象类
构造方法不能有构造方法可以有构造方法(供子类初始化使用)
成员变量默认 public static final(常量)可以是各种类型(普通变量、静态变量、常量)
成员方法JDK8 之前只能是抽象方法;J8 支持 default/static 方法;J9 支持 private 方法可以是抽象方法,也可以是非抽象方法(具体实现)
访问修饰符默认 public可用任意修饰符
设计层面是一种"行为规范"的契约(can do)是一种模板设计、"is-a" 关系
适用场景定义能力/行为规范,如 Comparable、Serializable相关类共享代码和属性,如模板方法模式

选择建议:

  • 如果是一种 IS-A 关系且需要代码复用 → 抽象类
  • 如果是一种 CAN-DO 关系 → 接口

3. == 和 equals 区别

对比项==equals()
基本数据类型比较值是否相等不适用
引用类型比较内存地址(引用是否指向同一对象)默认比较地址(Object 类),可重写后比较内容
String s1 = new String("abc");
String s2 = new String("abc");

s1 == s2;        // false(不同对象,地址不同)
s1.equals(s2);   // true(String 重写了 equals,比较字符序列)

// 字符串常量池
String s3 = "abc";
String s4 = "abc";
s3 == s4;        // true(指向常量池同一位置)

重写 equals 的原则:

  1. 自反性:x.equals(x) 返回 true
  2. 对称性:x.equals(y)y.equals(x)
  3. 传递性:x.equals(y)y.equals(z)x.equals(z)
  4. 一致性:多次调用结果一致
  5. 非空性:x.equals(null) 返回 false
  6. 重写 equals 时必须重写 hashCode

4. 重载(Overload)和重写(Override)的区别

对比项重载(Overloading)重写(Overriding)
定义同一个类中多个同名方法,参数列表不同子类重新定义父类的方法
发生范围同一个类中子类与父类之间
方法签名方法名相同,参数列表不同(个数、类型、顺序)方法名、参数列表、返回值都相同
返回值可以不同必须相同或是其子类型(协变返回)
访问修饰符无特殊要求子类不能缩小父类的访问权限
异常无特殊要求子类不能抛出新的或更广的检查异常
多态类型编译时多态(静态绑定)运行时多态(动态绑定)
// 重载示例
public void print(int a) {}
public void print(String s) {}       // 参数类型不同
public void print(int a, int b) {}   // 参数个数不同

// 重写示例
class Parent { public void show() {} }
class Child extends Parent {
    @Override
    public void show() {}  // 重写父类方法
}

5. sleep() 和 wait() 的区别

对比项sleep()wait()
所属类Thread.sleep()Object.wait()
释放锁不释放锁(持有锁继续睡)释放锁(进入等待队列)
唤醒方式时间到自动唤醒需要 notify() / notifyAll() 或时间到
使用范围任何地方调用必须在 synchronized 块内调用
作用范围当前线程当前对象的等待线程
用途线程暂停执行一段时间线程间通信/协调
// sleep 示例 - 不释放锁
synchronized (lock) {
    Thread.sleep(1000);   // 睡眠期间其他线程无法获取 lock
}

// wait 示例 - 释放锁
synchronized (lock) {
    lock.wait();          // 释放 lock,进入等待状态
}

6. StringBuilder 和 StringBuffer 的区别

对比项StringBuilderStringBuffer
线程安全非线程安全(不同步)线程安全synchronized 同步)
性能较高(无同步开销)较低(有同步开销)
适用场景单线程环境字符串拼接多线程环境字符串操作
JDK版本JDK 1.5 引入JDK 1.0 就存在

为什么不用 String 拼接? String 不可变,每次拼接都会创建新对象,循环中性能极差。


7. String 常用的方法有哪些

分类方法说明
判断equals() / equalsIgnoreCase()比较内容是否相等
contains() / startsWith() / endsWith()包含/前缀/后缀判断
isEmpty()长度是否为0
获取length() / charAt()长度/指定位置字符
indexOf() / lastIndexOf()第一次/最后一次出现的位置
substring(int begin, int end)截取子串 [begin, end)
toCharArray() / getBytes()转字符/字节数组
转换toUpperCase() / toLowerCase()大小写转换
trim()去除首尾空格
replace() / replaceAll() / split()替换/正则替换/拆分
valueOf()各种类型转 String(静态方法)
比较compareTo() / matches()字典序比较/正则匹配
join(CharSequence delimiter, ...)拼接字符串(JDK8+)

8. Synchronized 和 Lock 有什么区别

对比项synchronizedLock(ReentrantLock 等)
实现层面JVM 层面(monitorenter/monitorexit 指令)API 层面(java.util.concurrent.locks 包)
释放锁自动释放(代码块结束或异常时)必须手动在 finally 中 unlock()
中断响应不支持(不可被中断)支持响应中断 lockInterruptibly()
公平性非公平锁(不能保证获取顺序)可选公平/非公平(构造参数设置)
条件变量只能有一个 waitSet(单一条件)支持多个 Condition(精细控制)
超时机制不支持支持 tryLock(timeout) 尝试获取锁
读写锁不支持提供 ReentrantReadWriteLock
可重入性可重入可重入(ReentrantLock)

使用建议: 一般情况优先使用 synchronized;需要公平锁、可中断、超时尝试、多条件变量等高级功能时使用 Lock


9. 谈一谈你对多态的理解

多态是指同一个行为具有多种不同的表现形式。

三个必要条件:

  1. 继承(或实现接口)
  2. 重写(子类重新父类方法)
  3. 父类引用指向子类对象(向上转型)
Animal a = new Cat();    // 向上转型
a.makeSound();           // 调用的是 Cat 的 makeSound(动态绑定)

两种多态类型:

  • 编译时多态(重载):编译阶段确定调用哪个方法
  • 运行时多态(重写):运行时根据实际对象类型确定

好处:解耦 + 扩展性好 + 统一处理(开闭原则)


10. 泛型的理解,泛型存在于编译期还是运行期?

泛型本质是参数化类型,将类型作为参数传递,使代码更通用、类型安全。

核心问题答案:泛型存在于编译期,运行期会被擦除(Type Erasure)。

  • 编译后的字节码中没有泛型信息,全部变为原始类型(raw type)
  • <T> 变成 Object 或指定的边界类型
  • 泛型只是给编译器看的,用于编译期类型检查
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
list1.getClass() == list2.getClass();  // true!运行时类型相同(都变成了 List)

通配符:

  • ? — 无限定通配符
  • ? extends T — 上界通配符(只能读,不能写)
  • ? super T — 下界通配符(只能写,不能读)
  • PECS 原则:Producer Extends Consumer Super

11. Java 八大基本数据类型及字节数

数据类型关键字字节数取值范围
整数型byte1-128 ~ 127
short2±32,767
int4约 ±21亿
long8约 ±9.2×10¹⁸
浮点型float4±3.4×10³⁸
double8±1.7×10³⁰⁸
字符型char20 ~ 65,535(Unicode)
布尔型boolean1true / false

12. 哈希冲突怎么解决的?

哈希冲突:两个不同的 key 经过 hash 计算得到相同的哈希值。四种解决方式:

  1. 链地址法(拉链法)⭐ — HashMap 采用的方式,冲突元素追加到链表/红黑树
  2. 开放定址法 — 冲突时按规则寻找下一个空位置(ThreadLocalMap 用此策略)
  3. 再哈希法 — 使用多个哈希函数
  4. 公共溢出区 — 冲突元素放到专门的溢出表中

HashMap(JDK8)做法:数组 + 链表 + 红黑树

  • 链表长度 > 8 且数组长度 ≥ 64 时转为红黑树

13. HashMap 怎么判断键值的唯一性?

两步判断:

  1. Hash 值是否相等:先比较 hashCode()
  2. equals 判断:hashCode 相同再调用 equals() 比较
// HashMap 内部逻辑
if (p.hash == hash && 
    ((k = p.key) == key || (key != null && key.equals(k))))
    return p.value;  // 键已存在

因此:重写 equals 时必须重写 hashCode! 否则两个"相等"的对象会存到不同的桶里,导致 containsKey/find 失败。


14. HashMap 底层原理

底层结构(JDK8):数组 + 链表 + 红黑树

┌─────────────────────────────────────┐
│           Node[] table               │
│  ┌─────┬─────┬─────┬─────┬─────┐   │
│  │  0123  │ ... │   │  ← 数组(默认容量16)
│  └──┬──┴──┬──┴──┬──┴──┬──┴─────┘   │
│     │     │     │     │            │
│   null   ●────●     ●              │
│         ↓     ↓     ↓              │
│      node1  node2  node3           │  ← 链表(长度<8)
│                    ↓               │
│                  RBTree             │  ← 红黑树(长度≥8且容量≥64)
└─────────────────────────────────────┘

核心流程(put):

  1. 计算哈希值:hash = key.hashCode() ^ (key.hashCode() >>> 16) (扰动函数)
  2. 定位桶索引:index = hash & (n-1) (n 为 2 的幂次方)
  3. 桶为空 → 直接放入;桶不空 → 遍历链表/红黑树
  4. 链表长度 ≥ 8 且 table.length ≥ 64 → 树化为红黑树
  5. size > threshold(负载因子 0.75 × 容量)→ 扩容 resize()(2倍)

重要参数: 初始容量16、负载因子0.75、树化阈值8、退树化阈值6、最小树化容量64


15. HashMap 和 HashSet 的区别

对比项HashMapHashSet
本质存储 Key-Value 键值对仅存储不重复的元素
底层实现数组+链表+红黑树底层就是 HashMap(value 统一为 PRESENT 占位对象)
唯一性判断通过 key 的 hash+equals元素的 hash+equals
主要方法put/get/remove/containsKeyadd/remove/contains
用途缓存、映射、查找去重、判重
// HashSet 源码简化版
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) { return map.put(e, PRESENT) == null; }

16. HashMap 和 HashTable 的区别

对比项HashMapHashTable
线程安全非线程安全线程安全(方法加 synchronized)
null 键/值允许一个 null 键、多个 null 值不允许 null 键和 null 值
初始容量/扩容16 / 2倍扩容11 / 2n+1 扩容
哈希算法h ^ (h >>> 16)(扰动优化)key.hashCode()(直接取模)
推荐程度✅ 推荐❌ 已过时,并发场景用 ConcurrentHashMap

17. 动态代理与静态代理区别

对比项静态代理动态代理
生成时机编译期运行期
代理数量一对一(一个目标类一个代理类)一对多(一个 Handler 处理多个类)
复杂度简单直接相对复杂(依赖反射)
灵活性差(修改需改源码)好(通用性强)
应用目标明确、简单的场景AOP 框架底层、RPC 远程调用
// JDK 动态代理核心代码
UserService proxy = (UserService) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    (proxyObj, method, args) -> {
        System.out.println("前置增强");
        Object result = method.invoke(target, args);
        System.out.println("后置增强");
        return result;
    }
);

18. JDK 代理与 CGLIB 代理的区别

对比项JDK 动态代理CGLIB 代理
原理反射机制,基于 Proxy 类字节码操作,基于 ASM 框架生成子类
要求目标类必须实现接口目标类不能是 final,方法也不能是 final
代理对象代理的是接口,生成的是接口的实现类代理的是类本身,生成的是子类
性能创建快,调用稍慢(反射)创建慢(生成字节码),调用快
Spring 选择规则有接口 → 默认用 JDK Proxy无接口 → 自动切换 CGLIB

19. Stream 流的语法好在哪里?常用的方法有哪些

Stream 优势:声明式编程 + 函数式风格 + 链式调用 + 惰性求值 + 并行流

常用 API 分三类:

类型方法说明
创建stream() / parallelStream() / Stream.of() / Arrays.stream()创建流
中间操作(惰性)filter() / map() / flatMap() / distinct() / sorted() / limit() / skip() / peek()不触发计算
终止操作(触发)forEach() / count() / max/min() / findFirst() / anyMatch/allMatch() / reduce() / collect()触发计算

常用收集器: Collectors.toList()/toSet()/toMap()/joining()/groupingBy()/partitioningBy()

// 示例:按部门分组统计平均薪资
Map<String, Double> result = employees.stream()
    .filter(e -> e.getAge() > 20)
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

二、多线程 & 并发

20. 进程和线程的区别

对比项进程(Process)线程(Thread)
定义操作系统资源分配的基本单位CPU 调度和执行的基本单位
资源拥有拥有独立的内存空间、文件句柄等共享进程的资源(堆、方法区等)
独立性进程间相互隔离同一进程内的线程共享地址空间
通信方式IPC(管道、消息队列、共享内存、Socket)共享变量、wait/notify、BlockingQueue
创建销毁开销小(轻量级)
崩溃影响一个进程崩溃不影响其他一个线程崩溃可能导致整个进程崩溃

类比:进程=公司,线程=公司里的员工


21. 线程的 start() 和 run() 的区别

对比项start()run()
作用启动新线程,JVM 调用 run()普通的方法调用
线程创建并启动一个新线程当前线程中执行
并行性新线程和当前线程并发执行顺序执行
调用次数只能 start() 一次可以多次调用

⚠️ 重复调用 start() 会抛出 IllegalThreadStateException 异常。


22. 线程池创建的七大核心参数

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,          // ① 核心线程数:常驻线程,即使空闲也不回收
    maximumPoolSize,       // ② 最大线程数:队列满时可创建的最大线程数
    keepAliveTime,         // ③ 非核心线程空闲存活时间
    TimeUnit unit,         // ④ 时间单位(SECONDS/MINUTES 等)
    workQueue,             // ⑤ 任务队列(阻塞队列)
    threadFactory,         // ⑥ 线程工厂(用于自定义线程命名等)
    handler                // ⑦ 拒绝策略(队列满且达到最大线程数时的处理)
);

四种内置拒绝策略:

策略行为
AbortPolicy(默认)抛出 RejectedExecutionException
CallerRunsPolicy由提交任务的线程自己执行(降速保护)
DiscardPolicy直接丢弃任务,不抛异常
DiscardOldestPolicy丢弃队列中最老的任务,重新尝试提交当前任务

23. 线程池使用的原因

  1. 降低资源消耗:线程是昂贵资源(每个约 1MB 栈空间),复用避免频繁创建/销毁
  2. 提高响应速度:任务到达时可直接用空闲线程,无需等待创建
  3. 提高可管理性:统一管理线程,控制最大并发数,防止 OOM
  4. 提供更多功能:延迟执行、定时执行、周期执行等

24. 线程池的工作原理

提交任务
    │
    ▼
线程数 < corePoolSize?
    ├── YES → 创建【核心线程】执行
    └── NO  ▼
    队列未满?
    ├── YES → 任务放入【队列】等待
    └── NO  ▼
    线程数 < maximumPoolSize?
    ├── YES → 创建【非核心线程】执行
    └── NO → 【拒绝策略】
    
// 非核心线程空闲超过 keepAliveTime 后被回收

25. 线程池怎么启动线程

// 方式1:execute() - 提交 Runnable,无返回值
executor.execute(() -> System.out.println("task"));

// 方式2:submit() - 提交 Callable/Runnable,返回 Future
Future<String> future = executor.submit(() -> "result");
String result = future.get();  // 阻塞获取结果

// 方式3:invokeAll/invokeAny() - 批量提交
List<Future<String>> futures = executor.invokeAll(tasks);

注意:execute() 提交的异常会丢失;submit() 的异常保存在 Future.get() 中抛出 ExecutionException。


26. 线程间通信方式

方式说明适用场景
wait/notify/notifyAll基于 Object 监视器,配合 synchronized生产者-消费者模型
volatile保证可见性和有序性状态标志位
join()等待目标线程结束再继续等待线程完成
CountDownLatch倒计时门闩,归零放行一等多场景
CyclicBarrier循环屏障,互相等待到齐分阶段并行任务
Semaphore信号量,限制并发数量限流、资源池
BlockingQueue阻塞队列生产者-消费者(最常用)
Future/CompletableFuture异步任务结果获取异步编程

27. volatile 了解吗?如何保证线程可见性的?

volatile 是轻量级同步机制,保证可见性和有序性,但不保证原子性。

保证可见性的底层机制:

  • 写 volatile 变量时:强制将工作内存的值刷新回主内存(Store 屏障)
  • 读 volatile 变量时:使其他线程对应的缓存失效,从主内存重新读取(Load 屏障)
  • 底层通过插入内存屏障(Memory Barrier) 实现,禁止指令重排序

局限性: count++ 不是原子操作(读→改→写三步),volatile 无法解决!需要 AtomicInteger 或 synchronized。

适用场景: 状态标志位(volatile boolean running)、DCL 双检锁单例模式。


28. 线程池一般在什么业务场景下使用的

场景说明配置建议
Web 服务器处理 HTTP 请求(Tomcat 线程池)IO 密集:核数×2 左右
异步任务发送邮件/短信、生成报表核心 5~10,队列适中
定时任务定时清理、统计数据ScheduledThreadPool
批量数据处理批量导入/导出根据 CPU 核心数调整
并行计算大数据集并行处理ForkJoinPool
消息消费MQ 消息处理根据消息量和耗时配置
数据库连接池数据库连接管理根据数据库承载能力

29. 如何让两个线程轮流执行?/ n 个线程对一批数据相加求和

两线程交替打印(三种方案)

方案 A:wait/notify(经典方案)

int flag = 1;
Object lock = new Object();

// 线程A
synchronized (lock) {
    for (int i = 0; i < 10; i++) {
        while (flag != 1) { lock.wait(); }  // 用 while 防虚假唤醒
        System.out.println("A-" + i);
        flag = 2;
        lock.notifyAll();
    }
}

// 线程B
synchronized (lock) {
    for (int i = 0; i < 10; i++) {
        while (flag != 2) { lock.wait(); }
        System.out.println("B-" + i);
        flag = 1;
        lock.notifyAll();
    }
}

方案 B:Lock + Condition(最灵活,精确通知特定线程) 方案 C:CompletableFuture(最现代优雅)

n 个线程分片相加求和
public static long parallelSum(List<Long> data, int nThreads) {
    ExecutorService executor = Executors.newFixedThreadPool(nThreads);
    int chunkSize = (data.size() + nThreads - 1) / nThreads;  // 向上取整
    List<Future<Long>> futures = new ArrayList<>();

    for (int i = 0; i < nThreads; i++) {
        int fromIndex = i * chunkSize;
        int toIndex = Math.min(fromIndex + chunkSize, data.size());
        if (fromIndex >= data.size()) break;

        List<Long> subList = data.subList(fromIndex, toIndex);
        futures.add(executor.submit(() -> {
            long sum = 0;
            for (Long num : subList) sum += num;
            return sum;
        }));
    }

    long totalSum = 0;
    for (Future<Long> future : futures)
        totalSum += future.get();

    executor.shutdown();
    return totalSum;
}

// 更优方案:LongAdder + CountDownLatch(无锁高并发累加)

30. 线程池里的异常如何处理?

问题:线程池中任务抛出的异常会被"吞掉"!

四种解决方案:

  1. try-catch 捕获(最直接)
  2. submit() + Future.get()(推荐,异常保存在 Future 中)
  3. 自定义 UncaughtExceptionHandler
  4. 包装 Runnable 统一处理
// 推荐方案:submit + Future.get
Future<?> future = executor.submit(() -> doSomethingDangerous());
try {
    future.get();  // get() 时抛出 ExecutionException,getCause()拿到原始异常
} catch (ExecutionException e) {
    log.error("任务失败", e.getCause());
}

// 自定义 UncaughtExceptionHandler
ExecutorService pool = Executors.newFixedThreadPool(4, r -> {
    Thread t = new Thread(r);
    t.setUncaughtExceptionHandler((th, ex) -> log.error("未捕获异常", ex));
    return t;
});

31. 线程池中有一个线程长时间被使用无法回收,如何解决?

原因解决方案
任务死循环代码审查 + 设置超时
死锁固定加锁顺序 + 超时检测
外部IO阻塞HTTP请求设超时、数据库查询设超时
第三方SDK卡住Future.get(timeout) + cancel(true)
等待条件永不满足wait 加超时时间

防护措施:

  • 任务级别:Future 超时取消 future.get(30, SECONDS); future.cancel(true)
  • 配置级别:设置 keepAliveTime + allowCoreThreadTimeOut(true)
  • 监控级别:定期检查活跃线程数、队列大小,异常时告警
  • 终极手段:优雅关闭旧线程池 shutdownNow(),重建新的

什么是线程安全?

线程安全: 当多个线程同时访问同一个对象时,无论运行时环境采用何种调度方式,该对象都能表现出正确的行为。

什么导致线程不安全?(三大根源)
问题说明示例
原子性问题操作不是原子的,被中途打断count++ 读-改-写三步操作
可见性问题线程修改的值其他线程看不到变量在工作内存中未刷回主存
有序性问题指令重排序导致执行顺序混乱DCL 单例中 new 对象的重排
如何保证线程安全?(六种方法)
  1. 互斥同步synchronized / ReentrantLock
  2. 非阻塞同步(CAS)AtomicInteger / AtomicLong
  3. 不可变对象:final 字段 + 无 setter(String、Integer)
  4. ThreadLocal 线程隔离:每个线程持有自己的变量副本
  5. 线程安全集合:ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue
  6. volatile:仅限状态标志位等简单场景

33. 多个线程操作同一资源的冲突处理

经典案例:库存扣减超卖问题

// ❌ 不安全:两个线程同时读到 stock=99,各减1,最终98而不是97
public void deduct() { stock--; }

// ✅ 方案1:synchronized
public synchronized void deduct() { if (stock > 0) stock--; }

// ✅ 方案2:AtomicInteger CAS 自旋
AtomicInteger stock = new AtomicInteger(100);
int prev, next;
do { prev = stock.get(); if (prev <= 0) return false; next = prev - 1; }
while (!stock.compareAndSet(prev, next));

// ✅ 方案3(分布式):数据库乐观锁 UPDATE SET stock=stock-1 WHERE id=? AND stock>0

// ✅ 方案4(分布式):Redis Lua 脚本

其他常见冲突场景:

场景冲突表现解决方案
并发计数器多线程累加丢数AtomicLong / LongAdder
HashMap 并发 put数据丢失/死循环ConcurrentHashMap
SimpleDateFormat解析结果错乱ThreadLocal / DateTimeFormatter
懒加载单例重复初始化DCL 双检锁 + volatile
生产者-消费者队列为空还消费/满还生产wait/notify 或 BlockingQueue
转账并发转账余额不一致行锁 + 事务 + 乐观锁

34. Runnable 和 Callable 的区别

对比项RunnableCallable
方法void run()V call() throws Exception
返回值❌ 无✅ 有返回值(泛型 V)
异常处理❌ 不能抛 checked exception✅ 可以抛出 checked exception
提交execute(Runnable)submit(Callable) → 返回 Future
获取结果无法获取future.get() 获取返回值和异常

35. 线程池实际应用场景 —— 1000 任务、10 集群、每台 4 核

参数设计核心公式
IO密集型:线程数 = CPU核数 × (1 + W/C)    W=等待时间,C=计算时间   通常 核数×2~4
CPU密集型:线程数 = CPU核数 + 1
混合型:拆分为 IO 池和 CPU 池分别处理
IO 密集型配置(每台机器 4 核)
ThreadPoolExecutor ioPool = new ThreadPoolExecutor(
    12,                                    // 核心: 4核 × 3 = 12
    24,                                    // 最大: 4核 × 6 = 24
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(80),
    new ThreadFactoryBuilder().setNameFormat("io-task-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝时自我降速执行
);
CPU 密集型配置(每台机器 4 核)
ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(
    5,                                     // 4 + 1
    5,                                     // 核心 = 最大(多了没用)
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(200),         // 队列可大些
    new ThreadFactoryBuilder().setNameFormat("cpu-task-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);
完整生产级参考
@Configuration
public class ThreadPoolConfig {
    @Bean("businessExecutor")
    public ThreadPoolExecutor businessExecutor() {
        int cores = Runtime.getRuntime().availableProcessors();
        return new ThreadPoolExecutor(
            cores * 2, cores * 4,           // 根据任务类型调整倍数
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1024),
            r -> {
                Thread t = new Thread(r);
                t.setName("biz-" + t.getId());
                t.setUncaughtExceptionHandler((th, ex) -> log.error("异常", ex));
                return t;
            },
            new ThreadPoolExecutor.CallerRunsPolicy()  // 推荐:自我保护策略
        );
    }
}

参数设计黄金法则:

  • 队列不要无限大(容易 OOM),也不要太小(频繁拒绝)
  • 生产环境推荐 CallerRunsPolicy(拒绝时由调用线程执行,实现自我降速)
  • 监控活跃线程数和队列深度,设置告警阈值

36. 三个任务 a/b/c:a b 同时运行,任一完成后 c 只运行一次

推荐方案:CompletableFuture.anyOf(最优雅)✅

ExecutorService executor = Executors.newFixedThreadPool(3);

CompletableFuture<Void> futureA = CompletableFuture.runAsync(() -> {
    System.out.println("任务 A 开始 " + LocalDateTime.now());
    Thread.sleep(new Random().nextInt(3000));
    System.out.println("任务 A 完成 " + LocalDateTime.now());
}, executor);

CompletableFuture<Void> futureB = CompletableFuture.runAsync(() -> {
    System.out.println("任务 B 开始 " + LocalDateTime.now());
    Thread.sleep(new Random().nextInt(3000));
    System.out.println("任务 B 完成 " + LocalDateTime.now());
}, executor);

// anyOf: 任一完成即触发后续,且只触发一次!
CompletableFuture.anyOf(futureA, futureB).thenRunAsync(() -> {
    System.out.println("A/B 任一已完成 → 执行任务 C!");
    System.out.println("任务 C 执行完毕");
}, executor);

备选方案:CountDownLatch + AtomicBoolean

CountDownLatch latch = new CountDownLatch(1);       // 门闩计数=1
AtomicBoolean cExecuted = new AtomicBoolean(false);  // 保证c只执行一次

// 任务A/B 完成后:
if (cExecuted.compareAndSet(false, true)) {
    latch.countDown();  // 第一个完成的才触发
}

latch.await();          // 主线程等待
// 执行任务C...

37. 怎么获取某一个方法的参数?

(1)反射获取方法的参数信息(编译时需加 -parameters
Method method = MyClass.class.getMethod("myMethod", String.class, int.class);
Parameter[] params = method.getParameters();

for (Parameter param : params) {
    System.out.println("参数名: " + param.getName());   // 需要 -parameters 编译选项
    System.out.println("参数类型: " + param.getType());
}
(2)Spring AOP 拦截获取调用时的实际参数值 ⭐ 最常用
@Aspect
@Component
public class LogAspect {
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();            // 获取所有参数值
        
        MethodSignature sig = (MethodSignature) joinPoint.getSignature();
        String[] paramNames = sig.getParameterNames();  // 获取参数名
        
        for (int i = 0; i < args.length; i++) {
            System.out.printf("%s = %s%n", paramNames[i], args[i]);
        }
        
        return joinPoint.proceed(args);
    }
}
(3)拦截器中获取 Controller 方法参数
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                         Object handler) {
    HandlerMethod hm = (HandlerMethod) handler;
    MethodParameter[] params = hm.getMethodParameters();
    
    for (MethodParameter param : params) {
        System.out.println("参数名: " + param.getParameterName());
        RequestParam rp = param.getParameterAnnotation(RequestParam.class);
        if (rp != null)
            System.out.println(rp.value() + "=" + request.getParameter(rp.value()));
    }
    return true;
}

38. 什么是 OOP、API、JAR

概念全称含义示例
OOPObject-Oriented Programming面向对象编程:以对象为核心,封装、继承、多态三大特性。Java 是典型的 OOP 语言类、对象、接口
APIApplication Programming Interface应用程序编程接口:软件组件之间交互的约定和工具集合。可以是一个类的方法列表,也可以是 HTTP 接口JDK API、REST API、微信支付 API
JARJava ARchiveJava 归档文件:将多个 .class 文件和资源文件打包成一个压缩包,方便分发和部署。本质是 ZIP 格式spring-boot-app.jar、commons-lang3.jar

三者关系:

  • OOP 思想编写代码 → 编译成 .class 文件 → 打包成 JAR 文件 → 通过 API 提供功能给外部使用

39. 有没有研究过什么技术的底层,具体说说

这是一道开放性面试题,考察候选人的技术深度和学习热情。以下是常见的高分回答方向:

推荐回答方向:
技术方向底层知识点关键词
HashMap数组+链表+红黑树、哈希扰动函数、扩容机制、树化/退树化条件hash扰动、2次幂、负载因子0.75、CAS无锁扩容(JDK8+)
线程池七大参数工作流程、ctl 高3位存状态低29位存数量、Worker 继承 AQSctl变量、worker创建流程、拒绝策略
Spring IOCBeanFactory vs ApplicationContext、Bean生命周期、循环依赖三级缓存refresh()、三级缓存(singletonFactories)、AOP代理时机
Spring AOP动态代理(JDK Proxy/CGLIB)、切面链式调用、通知顺序JoinPoint、Advice chain、ProxyFactory
MySQLB+Tree索引结构、MVCC、Redo Log/Undo Log、锁机制(行锁/间隙锁/临键锁)InnoDB、聚簇索引、ReadView、WAL机制
JVM内存模型、GC算法(G1/ZGC)、类加载双亲委派、JIT编译堆栈结构、GC Roots、SafePoint、逃逸分析
RedisSDS、跳跃表(SkipList)、单线程事件驱动模型、持久化(AOF/RDB)I/O多路复用(epoll)、Reactor模式
Tomcat连接器(Container/Processor)、Servlet生命周期、NIO模型Acceptor→Poller→Worker、Connector架构
MyBatisSqlSession、动态SQL解析、一级缓存/二级缓存、插件(Interceptor)Configuration、MappedStatement、Executor

回答模板:

"我研究过 HashMap 的底层源码。它底层是数组+链表+红黑树的结构。put 操作时先通过 hashCode 做扰动运算定位桶位置,冲突时用拉链法解决。JDK8 引入了红黑树优化,当链表长度超过 8 且数组长度超过 64 时会转成红黑树,查找效率从 O(n) 提升到 O(log n)。另外它的扩容是 2 倍扩容..."


三、网络 & Web

40. HTTP 规范 / HTTP 协议是什么?

HTTP(HyperText Transfer Protocol)超文本传输协议,一种应用层协议,用于在 Web 浏览器和 Web 服务器之间传递数据。

核心特点:

  • 基于 请求-响应 模式(Request-Response)
  • 无状态:服务器不保存客户端上下文(每次请求独立)
  • 明文传输(HTTP),HTTPS 则加密
  • 默认端口:80(HTTP)、443(HTTPS)

HTTP 报文结构:

请求报文(客户端 → 服务器):
POST /api/login HTTP/1.1\r\n              ← 请求行(方法 URL 版本)
Host: www.example.com\r\n                  ← 请求头(Key: Value)
Content-Type: application/json\r\n
Content-Length: 25\r\n
\r\n                                        ← 空行
{"user":"admin","pw":"123"}               ← 请求体

响应报文(服务器 → 客户端):
HTTP/1.1 200 OK\r\n                        ← 状态行(版本 状态码 状态文本)
Content-Type: application/json\r\n         ← 响应头
Content-Length: 15\r\n
\r\n                                        ← 空行
{"code":0,"msg":"ok"}                     ← 响应体

HTTP 各版本演进:

版本特点
HTTP/1.0每个请求一个 TCP 连接(短连接)
HTTP/1.1长连接(Keep-Alive)、管线化(pipelining)
HTTP/2多路复用(一个连接并行多个流)、头部压缩、服务端推送
HTTP/3基于 QUIC(UDP),解决队头阻塞问题

41. WebSocket 连接方式、原理、如何实现

是什么?

WebSocket 是一种全双工、持久化的通信协议,建立在 TCP 之上,服务器可以主动向客户端推送消息。

与 HTTP 的关系

WebSocket 通过 HTTP 握手建立连接,之后升级为 WebSocket 协议独立通信:

握手过程:
客户端 → 服务器:  GET /ws HTTP/1.1
                 Upgrade: websocket          ← 协议升级请求
                 Connection: Upgrade
                 Sec-WebSocket-Key: xxx      ← 安全密钥
                 Sec-WebSocket-Version: 13

服务器 → 客户端:  HTTP/1.1 101 Switching Protocols   ← 101 状态码:协议切换成功
                 Upgrade: websocket
                 Connection: Upgrade
                 Sec-WebSocket-Accept: yyy    ← 服务端确认密钥
                 
之后:双方通过 TCP 长连接双向发送 WebSocket 帧
Java 实现(Spring Boot)
// 1. 服务端
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private MyWebSocketHandler handler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(handler, "/ws")
                .setAllowedOrigins("*");  // 允许跨域
    }
}

@Component
public class MyWebSocketHandler extends TextWebSocketHandler {
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        session.sendMessage(new TextMessage("收到: " + message.getPayload()));
    }
}

// 2. 前端 JS
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onmessage = (event) => console.log(event.data);
ws.send('Hello Server!');
适用场景:实时聊天、股票行情推送、在线协作、游戏实时同步、运维监控大屏

42. HTTP 与 TCP 的区别

对比项HTTPTCP
层级应用层(OSI 第7层)传输层(OSI 第4层)
职责规定了数据的内容格式和语义负责可靠的数据传输(分段、重组、确认重传)
连接无状态,基于请求-响应有状态,面向连接(三次握手建立连接)
数据格式文本格式的请求头+请求体二进制字节流(Segment)
可靠性本身不可靠,依赖下层 TCP 保证可靠传输(确认应答、超时重传、滑动窗口)
关系HTTP 运行在 TCP 之上HTTP 是 TCP 的应用层协议之一

类比理解:TCP 就像电话线路(保证通话质量),HTTP 就是打电话时说的语言(约定了说什么怎么说)。


43. TCP 和 UDP 的区别

对比项TCP(传输控制协议)UDP(用户数据报协议)
连接性面向连接(三次握手、四次挥手)无连接(直接发)
可靠性可靠(确认、重传、排序)不可靠(尽力送达,不保证)
有序性保证数据有序到达不保证顺序
速度较慢(有确认开销)(开销小)
资源消耗大(维护连接状态)
首部开销20~60 字节仅 8 字节
传输方式面向字节流面向报文(保留消息边界)
拥塞控制有(慢启动、拥塞避免)
流量控制有(滑动窗口机制)
适用场景文件传输、网页浏览、邮件、数据库连接视频/语音直播、DNS查询、游戏、物联网

44. HTTP 与 WebSocket 的区别

对比项HTTPWebSocket
通信模式半双工(请求-响应,客户端主动发起)全双工(双向同时通信)
连接方式短连接或长连接但每次需要请求持久连接(一次握手后长期保持)
服务器推送❌ 不支持(只能轮询/SSE模拟)原生支持(随时推送)
协议标识http:// / https://ws:// / wss://
数据格式纯文本(请求头+请求体)二进制帧(Frame)
状态码使用 HTTP 状态码无状态码概念
性能每次携带完整 Header 头连接后仅传输少量帧头
适用场景RESTful API、网页加载实时聊天、推送通知、协同编辑

替代方案对比:

  • 轮询:客户端定时问"有新消息吗?" — 浪费资源
  • SSE(Server-Sent Events):服务器单向推送 — 比 HTTP 好,但不能客户端发
  • WebSocket:真正的双向实时通信 — 最佳选择

45. HTTP 与 HTTPS 的区别

对比项HTTPHTTPS
全称HyperText Transfer ProtocolSecure HTTP(安全套接字层上的 HTTP)
端口80443
安全性明文传输,易被窃听篡改加密传输(对称加密+非对称加密)
证书不需要需要 SSL/TLS 证书(CA颁发)
性能快(无需加密解密)稍慢(SSL握手 + 加解密计算)
SEO相对较低排名Google 搜索排名加权
成本免费证书费用(Let's Encrypt 免费可用)

HTTPS 工作原理(简化版):

1. 客户端 → 服务器: 发送支持的加密算法套件 + 随机数
2. 服务器 → 客户端: 返回选定的加密算法 + 证书(含公钥) + 随机数
3. 客户端验证证书(CA签名有效性),生成【预主密钥】,用【公钥加密】后发送给服务器
4. 服务器用【私钥解密】获得预主密钥
5. 双方用 随机数+预主密钥 生成相同的【会话密钥】(对称密钥)
6. 之后使用【会话密钥】进行对称加密通信(高效)

一句话总结:HTTPS = HTTP + SSL/TLS 加密层,用非对称加密交换密钥,用对称加密传输数据。


46. HTTP 响应状态码常见的都有哪些

分类状态码含义常见场景
1xx 信息100 Continue继续大文件上传前的确认
2xx 成功200 OK请求成功正常的 GET/POST 响应
201 Created创建成功POST 新建资源成功
204 No Content成功无内容DELETE 删除成功
3xx 重定向301 Moved Permanently永久重定向网站换域名、HTTP→HTTPS
302 Found临时重定向登录后跳转首页
304 Not Modified未修改(走缓存)浏览器缓存命中
4xx 客户端错误400 Bad Request请求语法错误参数校验失败
401 Unauthorized未认证/未登录Token 过期
403 Forbidden无权限IP被禁止、权限不足
404 Not Found资源不存在接口路径错误
405 Method Not Allowed请求方法不允许用 GET 访问需要 POST 的接口
408 Request Timeout请求超时客户端长时间未发送完请求
409 Conflict冲突并发更新同一资源
429 Too Many Requests请求过于频繁接口被限流
5xx 服务器错误500 Internal Server Error服务器内部错误代码抛异常、空指针
502 Bad Gateway网关错误Nginx 后端服务挂了
503 Service Unavailable服务不可用服务器过载、维护中
504 Gateway Timeout网关超时后端处理太慢

记忆技巧:

  • 200 万事 OK
  • 301 永久搬家、302 临时出门
  • 400 你说错了(参数错)、401 你没证件(没登录)、403 有证但不让你进(没权限)、404 这儿没人(找不到)
  • 500 我错了(服务端异常)、502 我帮你联系的人挂了(网关后端挂了)、504 我帮你联系的人太慢了

47. Cookie 和 Session 有什么区别

对比项CookieSession
存储位置浏览器端(本地)服务器端(内存/Redis/数据库)
安全性⚠️ 较低(可被篡改/窃取)✅ 较高(敏感数据存在服务端)
存储容量4KB(每个 Cookie)理论上无限制(受服务器内存限制)
有效期可设过期时间(关闭浏览器也可保留)通常关闭浏览器失效(也可设长)
跨域支持可设 Domain/Path 跨子域名一般不支持跨域(Token方案可跨域)
服务器压力无(数据在客户端)有(大量用户占用内存)
本质一段文本数据服务端的数据结构 + 一个 SessionID

工作机制:

首次访问:
Client → Server: GET /login
Server → Client: Set-Cookie: JSESSIONID=abc123   ← 服务器下发Cookie(包含Session ID)

后续请求:
Client → Server: Cookie: JSESSIONID=abc123        ← 每次请求自动带上
Server: 根据 abc123 从 Session 存储中找到对应用户信息

分布式 Session 问题:

  • 多台服务器时,用户 A 在 Server1 登录,下次请求可能到 Server2(没有该用户的 Session)
  • 解决方案:Session 共享存储(Redis)、Session Sticky(粘滞会话)、JWT Token(无状态方案)

48. GET 和 POST 的区别

对比项GETPOST
用途获取资源(查)提交数据(增/改/删)
参数位置URL Query String?key=valueRequest Body(请求体)
参数可见性⚠️ 明文显示在URL(浏览器历史/日志可见)✅ 在 Body 中(相对隐蔽)
长度限制有限制(URL 最大约 2048 字符,浏览器不同而异)无限制(理论上)
安全性低(参数暴露、容易被 CSRF)较高(不在 URL 中)
缓存可被缓存❌ 默认不缓存(除非显式设置)
幂等性幂等(多次请求结果相同)非幂等(多次提交可能创建多条记录)
书签收藏✅ 可以收藏为书签❌ 不行
回退/刷新无影响⚠️ 浏览器会提示重新提交表单
编码类型application/x-www-form-urlencoded支持多种(form-data、json、binary 等)
TCP 包发送 1 个 TCP 包(Header+Data一起)先发 Header(100 Continue),再发 Data(2 个包)

注意:从规范上说 GET 和 POST 本质相同——都是 TCP 上发送 HTTP 请求。上述区别更多是浏览器和服务器的约定俗成。


49. 过滤器(Filter)和拦截器(Interceptor)的区别

对比项Filter(过滤器)Interceptor(拦截器)
所属层面Servlet 规范(Java EE 标准)Spring MVC 框架(Spring 特有)
作用范围所有进入容器的请求(包括静态资源)只拦截 Controller 的请求(DispatcherServlet 分发后的)
实现接口javax.servlet.Filterorg.springframework.web.servlet.HandlerInterceptor
配置方式@WebFilter 或 web.xml@Component + addInterceptors() 注册
注入能力❌ 不能直接注入 Spring Bean(需通过 SpringContextHelper)✅ 可以注入任何 Spring Bean
执行顺序在 Servlet 之前(请求最先到达)在 DispatcherServlet 之后、Controller 之前
适用场景编码转换、CORS跨域、XSS防护、请求日志、权限过滤(粗粒度)权限校验(细粒度)、日志记录、性能统计、参数预处理

执行顺序图:

请求进来:
Browser → Tomcat Container
         │
         ▼
    【Filter】① doFilter() — 编码、CORS、XSS
         │
         ▼
    DispatcherServlet (Spring前端控制器)
         │
         ▼
    【Interceptor】② preHandle() — 登录校验、权限判断
         │
         ▼
    Controller → Service → DAO
         │
         ▼
    【Interceptor】③ postHandle() — 视图渲染前
         │
         ▼
    【Interceptor】④ afterCompletion() — 视图渲染后(清理资源)
         │
         ▼
    【Filter】⑤ FilterChain继续 — 响应输出
         │
         ▼
    Response → Browser

代码示例:

// Filter 示例
@WebFilter(urlPatterns = "/*")
public class CorsFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", "*");
        chain.doFilter(req, res);
    }
}

// Interceptor 示例
@Component
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
        String token = req.getHeader("Authorization");
        if (!JwtUtil.verify(token)) {
            res.setStatus(401);
            return false;  // 拦截请求
        }
        return true;  // 放行
    }
}

// 注册拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired private AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/**")     // 拦截路径
                .excludePathPatterns("/api/login/**");  // 排除路径
    }
}