2026Java面试必杀技:让面试官刮目相看的深度技术指南
🎯 写在前面:又到了一年一度的求职旺季,你是否还在为面试发愁?面对面试官的灵魂拷问,你是否能从容应对?这篇文章,将帮你把Java核心技术讲透,让你在面试中脱颖而出!
前言
又到了一年一度的求职旺季。作为一名Java开发者,你是否也有这样的烦恼:
"HashMap的底层结构是什么?"
"synchronized和ReentrantLock有什么区别?"
"JVM调优怎么做?"
"MySQL索引为什么选择B+树?"
"Redis分布式锁怎么实现?"
这些问题几乎是Java面试的"八股文",但你知道面试官真正想听到的是什么吗?
不是背答案,而是理解原理!
今天这篇文章,我将从高频面试题出发,深入剖析背后的技术原理,让你在面试中不仅能答出来,还能答出深度,让面试官刮目相看!
一、Java基础:从入门到精通
1.1 String为什么是不可变的?
这是一道经典的Java基础面试题,看似简单,实则暗藏玄机。
表面回答:
// String类的核心实现
public final class String {
private final char[] value; // JDK 8
// private final byte[] value; // JDK 9+ 使用byte数组更节省内存
public String(String original) {
this.value = original.value;
}
// ...
}
深层理解:
┌─────────────────────────────────────────────────────────────────────┐
│ String不可变的秘密 │
│ │
│ 为什么String要设计成不可变? │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 五大原因 │ │
│ │ │ │
│ │ 1️⃣ 字符串常量池(String Pool) │ │
│ │ String s1 = "hello"; // 直接从常量池获取 │ │
│ │ String s2 = "hello"; // 同一个引用! │ │
│ │ 如果可变,s1修改会影响s2 │ │
│ │ │ │
│ │ 2️⃣ 安全性 │ │
│ │ 数据库连接、文件路径、用户名密码... │ │
│ │ 如果可变,黑客可以轻易篡改! │ │
│ │ │ │
│ │ 3️⃣ 线程安全 │ │
│ │ 不可变对象天然线程安全,无需同步 │ │
│ │ │ │
│ │ 4️⃣ 哈希码缓存 │ │
│ │ String的hashCode只计算一次,之后缓存 │ │
│ │ public int hashCode() { │ │
│ │ int h = hash; // 只初始化一次 │ │
│ │ return h; │ │
│ │ } │ │
│ │ │ │
│ │ 5️⃣ 类加载器机制 │ │
│ │ 类的加载依赖字符串不可变,否则可能导致类加载错误 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
面试加分回答:
面试官问:"String真的完全不可变吗?"
高情商回答:"从设计层面看,String通过final修饰类、private修饰字段、且不提供setter方法实现了不可变。
但Java的不可变性并非绝对,通过反射可以绕过封装:
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
valueField.set(str, new char[]{'a','b','c'});
不过这是破坏性操作,生产环境绝不能这样用。"
1.2 HashMap源码深度解析
HashMap绝对是Java面试的"钉子户",必问无疑!
JDK 8数据结构演进:
┌─────────────────────────────────────────────────────────────────────┐
│ HashMap数据结构演进 │
│ │
│ JDK 7:数组 + 链表 │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ [0] │ [1] │ [2] │ [3] │ [4] │ [5] │ ← 数组(桶) │
│ └──┬──┴──┬──┴──┬──┴─────┴──┬──┴─────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ K │→│ K │→│ K │ │ K │ ← 链表 │
│ │ V │ │ V │ │ V │ │ V │ │
│ └───┘ └───┘ └───┘ └───┘ │
│ │
│ 问题:链表过长时,查找效率退化到O(n) │
│ │
│ JDK 8:数组 + 链表 + 红黑树(桶内链表长度≥8时) │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ [0] │ [1] │ [2] │ [3] │ [4] │ [5] │ ← 数组(桶) │
│ └──┬──┴──┬──┴──┬──┴─────┴──┬──┴─────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌───┐ ┌───┐ 红黑树 ┌───┐ │
│ │ K │→│ K │ ┃ │ K │ │
│ │ V │ │ V │ ┃ │ V │ │
│ └───┘ └───┘ ┃ └───┘ │
│ ▼ │
│ ┌─────┐ │
│ │ BST │ ← 红黑树,查找O(logn) │
│ └─────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
put()方法源码解析:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 核心put方法
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// 1️⃣ 首次put时,初始化数组(懒加载)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2️⃣ 计算下标,如果该位置为空,直接放入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 3️⃣ 该位置已有元素(碰撞)
Node<K,V> e; K k;
// 4️⃣ key相同,覆盖value
if (p.hash == hash &&
((k = p.key) == key || key.equals(k)))
e = p;
// 5️⃣ 是红黑树节点,调用树插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>) p).putTreeVal(this, tab, hash, key, value);
else {
// 6️⃣ 链表遍历
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 插入链表尾部
p.next = newNode(hash, key, value, null);
// 链表长度≥8,转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// key已存在,覆盖
if (e.hash == hash &&
((k = e.key) == key || key.equals(k)))
break;
p = e;
}
}
// value覆盖
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 7️⃣ 元素数量超过阈值,扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
扩容机制:
/**
* 扩容机制:2倍扩容,重新hash
*
* 关键点:扩容时为什么不需要重新计算hash?
* 答案:因为容量始终是2的n次方,新位置 = 原位置 + 原容量
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 原容量 > 0
if (oldCap > 0) {
// 超过最大容量,不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 新容量 = 原容量 * 2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
// ...
// 创建新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 迁移元素
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 只有一个节点,直接计算新位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 红黑树分裂
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 链表分裂:不需要重新hash
// 原位置节点 和 原位置+旧容量节点
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 根据hash & oldCap判断
if ((e.hash & oldCap) == 0) {
// 原位置
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} else {
// 原位置 + 旧容量
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 放到新数组对应位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
面试高频追问:
Q1: HashMap为什么线程不安全?
A1:
1. 多线程put可能导致数据覆盖
2. resize时可能形成环形链表,导致死循环
3. JDK 7头插法:并发时可能把链表顺序搞反
4. JDK 8尾插法:解决了顺序问题,但仍可能丢数据
Q2: ConcurrentHashMap如何保证线程安全?
A2:
JDK 7:Segment分段锁
JDK 8:CAS + synchronized,锁住单个bucket
分段锁 → 锁住数组某个节点
Q3: 为什么HashMap的负载因子是0.75?
A3:
统计学原理:泊松分布,链表长度超过8的概率已经很小
空间和时间权衡:太大会增加碰撞,太小会浪费空间
二、并发编程:硬核知识
2.1 synchronized关键字深度解析
synchronized是Java中最常见的同步手段,但你真的理解它吗?
对象锁的monitor机制:
┌─────────────────────────────────────────────────────────────────────┐
│ synchronized原理 │
│ │
│ 每个对象都有一个monitor(监视器锁) │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 对象内存结构 │ │
│ │ │ │
│ │ 对象头(Object Header) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Mark Word(标记字段) │ │ │
│ │ │ ┌───────────────────────────────────────────────┐ │ │ │
│ │ │ │ 无锁状态 │ 偏向锁 │ 轻量级锁 │ 重量级锁 │ GC标记 │ │ │ │
│ │ │ │ 01 │ 01/10 │ 00 │ 10 │ 11 │ │ │ │
│ │ │ └───────────────────────────────────────────────┘ │ │ │
│ │ │ Klass Pointer(指向类元数据) │ │ │
│ │ │ 实例数据(Instance Data) │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 锁的升级过程(不可逆): │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 无锁 ────→ 偏向锁 ────→ 轻量级锁 ────→ 重量级锁 │ │
│ │ (01) (01/10) (00) (10) │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ 第一次访问 竞争出现 竞争激烈 │ │
│ │ │ 加锁 CAS失败 自旋超过阈值 │ │
│ │ │ │
│ │ 升级时机: │ │
│ │ 1. 偏向锁:只有一个线程访问同步块 │ │
│ │ 2. 轻量级锁:有线程竞争,但很快就能获取 │ │
│ │ 3. 重量级锁:竞争激烈,自旋失败 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
synchronized的四种用法:
public class SynchronizedDemo {
// 1️⃣ 修饰代码块:锁定指定对象
public void method1() {
synchronized (this) { // 锁当前实例
// 临界区
}
}
public void method2() {
synchronized (SynchronizedDemo.class) { // 锁类对象
// 临界区
}
}
// 2️⃣ 修饰实例方法:锁定当前实例
public synchronized void method3() {
// 等价于 synchronized(this)
}
// 3️⃣ 修饰静态方法:锁定类对象
public static synchronized void method4() {
// 等价于 synchronized(SynchronizedDemo.class)
}
}
synchronized vs ReentrantLock:
import java.util.concurrent.locks.ReentrantLock;
/**
* synchronized vs ReentrantLock 对比
*/
public class LockComparison {
// synchronized:自动释放锁
public synchronized void syncMethod() {
try {
// 临界区
} finally {
// 不需要手动释放,编译器会自动处理
}
}
// ReentrantLock:手动释放锁
private final ReentrantLock lock = new ReentrantLock();
public void lockMethod() {
lock.lock(); // 获取锁
try {
// 临界区
} finally {
lock.unlock(); // 必须手动释放
}
}
// ReentrantLock的高级功能
public void advancedLockMethod() {
// 1. 可中断锁
try {
lock.lockInterruptibly();
// 可以被中断的等待
} catch (InterruptedException e) {
e.printStackTrace();
}
// 2. 尝试获取锁(非阻塞)
if (lock.tryLock()) {
try {
// 获取成功
} finally {
lock.unlock();
}
} else {
// 获取失败
}
// 3. 尝试获取锁(带超时)
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 5秒内获取到了锁
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 4. 公平锁
ReentrantLock fairLock = new ReentrantLock(true);
}
}
┌─────────────────────────────────────────────────────────────────────┐
│ synchronized vs ReentrantLock │
│ │
│ ┌────────────────────┬────────────────────┐ │
│ │ synchronized │ ReentrantLock │ │
│ ├────────────────────┼────────────────────┤ │
│ │ JVM内置 │ JDK显式实现 │ │
│ ├────────────────────┼────────────────────┤ │
│ │ 自动释放 │ 手动释放 │ │
│ ├────────────────────┼────────────────────┤ │
│ │ 不可中断 │ 可中断 │ │
│ ├────────────────────┼────────────────────┤ │
│ │ 非公平锁 │ 支持公平/非公平 │ │
│ ├────────────────────┼────────────────────┤ │
│ │ 粒度粗 │ 粒度细 │ │
│ ├────────────────────┼────────────────────┤ │
│ │ 条件锁 │ 支持多个条件 │ │
│ │ (wait/notify) │ (Condition) │ │
│ ├────────────────────┼────────────────────┤ │
│ │ 无法获取锁状态 │ 可查询锁状态 │ │
│ └────────────────────┴────────────────────┘ │
│ │
│ 选择建议: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ✓ 简单同步 → synchronized │ │
│ │ ✓ 需要高级功能 → ReentrantLock │ │
│ │ ✓ 高并发性能 → ReentrantLock + 分段锁 │ │
│ │ ✓ 代码简洁优先 → synchronized │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
2.2 ThreadLocal源码解析
ThreadLocal是面试中的"隐藏Boss",看似简单,实则暗藏玄机!
核心原理图解:
┌─────────────────────────────────────────────────────────────────────┐
│ ThreadLocal原理 │
│ │
│ 每个Thread对象都有一个 ThreadLocalMap │
│ │
│ Thread ─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ThreadLocal.ThreadLocalMap │ │
│ │ ┌───────────────────────────────────────────────────┐ │ │
│ │ │ table[] │ │ │
│ │ │ ┌─────────┬─────────┬─────────┬─────────┐ │ │ │
│ │ │ │ Entry │ Entry │ Entry │ ... │ │ │ │
│ │ │ │ (k,v) │ (k,v) │ (k,v) │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ └─────────┴─────────┴─────────┴─────────┘ │ │ │
│ │ │ │ │ │
│ │ │ Entry结构: │ │ │
│ │ │ ┌─────────────────────────────────────────────┐ │ │ │
│ │ │ │ static class Entry extends WeakReference< │ │ │ │
│ │ │ │ ThreadLocal<?>> { │ │ │ │
│ │ │ │ Object value; │ │ │ │
│ │ │ │ } │ │ │ │
│ │ │ └─────────────────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ThreadLocal变量本身不存在于线程中,只是一个Key │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ThreadLocal<String> tl = new ThreadLocal<>(); │ │
│ │ tl.set("Hello"); │ │
│ │ │ │
│ │ 实际存储位置: │ │
│ │ Thread.currentThread() │ │
│ │ ↓ │ │
│ │ threadLocals (ThreadLocalMap) │ │
│ │ ↓ │ │
│ │ { ThreadLocal对象 → "Hello" } │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
源码实现:
public class ThreadLocal<T> {
// ThreadLocal的hash值(原子递增)
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
// 获取下一个hash值
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 设置值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 获取值
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T) e.value;
}
return setInitialValue();
}
// 移除值(重要!防止内存泄漏)
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
// 创建ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 获取当前线程的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}
内存泄漏问题(重点):
/**
* ThreadLocal内存泄漏的原因:
*
* ThreadLocalMap的Entry继承自WeakReference<ThreadLocal<?>>
* 这意味着Entry对ThreadLocal对象是弱引用
*
* 但是!Entry的value是强引用!
*/
public class MemoryLeakDemo {
public static void main(String[] args) {
// 1. 创建ThreadLocal
ThreadLocal<byte[]> tl = new ThreadLocal<>();
tl.set(new byte[1024 * 1024 * 10]); // 10MB
// 2. tl变量被置为null
tl = null;
// 3. 此时ThreadLocal对象可以被GC回收(弱引用)
// 但Thread还在,ThreadLocalMap还在,Entry还在
// Entry.value还持有10MB数据,无法回收!
System.gc();
// 正确的做法:
// ThreadLocal使用完毕后,手动调用remove()
ThreadLocal<String> safeTl = new ThreadLocal<>();
try {
safeTl.set("some data");
// 使用...
} finally {
safeTl.remove(); // 重要!防止内存泄漏
}
}
}
┌─────────────────────────────────────────────────────────────────────┐
│ ThreadLocal内存泄漏分析 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 引用链 │ │
│ │ │ │
│ │ ThreadLocal对象 ──弱引用──→ Entry (key) │ │
│ │ │ │ │
│ │ │ 强引用 │ │
│ │ ▼ │ │
│ │ value对象 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 场景分析: │
│ │
│ 情况1:ThreadLocal变量置为null │
│ ThreadLocal对象(key) → 被GC回收(弱引用) │
│ 但Entry.value仍持有数据,且Thread还在 → 内存泄漏! │
│ │
│ 情况2:ThreadLocal.remove() │
│ Entry被显式移除 → value无引用 → 被GC回收 → 安全! │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 最佳实践 │ │
│ │ │ │
│ │ try { │ │
│ │ ThreadLocal<User> userLocal = new ThreadLocal<>(); │ │
│ │ userLocal.set(currentUser); │ │
│ │ // 业务逻辑 │ │
│ │ } finally { │ │
│ │ userLocal.remove(); // !必须调用 │ │
│ │ } │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
三、JVM虚拟机:底层奥秘
3.1 类加载机制深度解析
类加载是JVM的起点,也是面试高频考点。
类加载的七个阶段:
┌─────────────────────────────────────────────────────────────────────┐
│ 类加载生命周期 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ① 加载 ──→ ② 验证 ──→ ③ 准备 ──→ ④ 解析 │ │
│ │ │ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ⑤ 初始化 │ │
│ │ │ │ │
│ │ │ │ │
│ │ ⑦ 卸载 ◀────────────────┘ │ │
│ │ │ │ │
│ │ │ │ │
│ │ └──────────────────⑥ 使用 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 详细说明: │
│ │
│ ① 加载(Loading) │
│ - 通过类的全限定名获取类的二进制字节流 │
│ - 将字节流转化为方法区的运行时数据结构 │
│ - 在堆中生成java.lang.Class对象,作为访问入口 │
│ │
│ ② 验证(Verification) │
│ - 文件格式验证(魔数、版本号等) │
│ - 元数据验证(语义分析) │
│ - 字节码验证(数据流、控制流分析) │
│ - 符号引用验证(解析阶段) │
│ │
│ ③ 准备(Preparation) │
│ - 分配内存,初始化静态变量 │
│ - 注意:静态变量赋默认值,不是赋值! │
│ static int a = 10; // 准备阶段 a=0,初始化阶段 a=10 │
│ │
│ ④ 解析(Resolution) │
│ - 符号引用 → 直接引用 │
│ - 类/接口、字段、类方法、方法类型等 │
│ │
│ ⑤ 初始化(Initialization) │
│ - 执行类构造器<clinit> │
│ - 静态变量赋值、静态代码块执行 │
│ - 线程安全,由JVM保证 │
│ │
│ 使用/卸载... │
│ │
└─────────────────────────────────────────────────────────────────────┘
双亲委派模型:
/**
* 双亲委派模型
*
* 当类加载器收到加载请求时,先委托给父类加载器处理
* 只有父类加载器无法完成时,才自己尝试加载
*/
public class ClassLoaderDemo {
// 类加载器层级
public static void showClassLoaderHierarchy() {
ClassLoader current = ClassLoaderDemo.class.getClassLoader();
do {
System.out.println(current);
current = current.getParent();
} while (current != null);
// 输出:
// sun.misc.Launcher$AppClassLoader@xxxxx (应用类加载器)
// sun.misc.Launcher$ExtClassLoader@xxxxx (扩展类加载器)
// null (Bootstrap启动类加载器)
}
}
/**
* 双亲委派模型源码
*/
public abstract class ClassLoader {
// 核心方法:loadClass
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1️⃣ 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 2️⃣ 委托给父类加载器
try {
ClassLoader parent = this.getParent();
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 父类为null,说明是BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类找不到,不处理
}
// 3️⃣ 父类都找不到,自己加载
if (c == null) {
c = findClass(name);
}
return c;
}
}
// findClass由子类实现
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
┌─────────────────────────────────────────────────────────────────────┐
│ 双亲委派模型 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Bootstrap ClassLoader │ │
│ │ (启动类加载器,C++实现) │ │
│ │ 加载 JAVA_HOME/lib 核心类 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Extension ClassLoader │ │
│ │ (扩展类加载器) │ │
│ │ 加载 JAVA_HOME/lib/ext 扩展类 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Application ClassLoader │ │
│ │ (应用类加载器) │ │
│ │ 加载 classpath 下的类 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 加载过程: │
│ │
│ 用户想要加载 java.lang.String │
│ │ │
│ ▼ │
│ ApplicationClassLoader.loadClass() │
│ │ │
│ ▼ │
│ 委托给父类:ExtensionClassLoader.loadClass() │
│ │ │
│ ▼ │
│ 委托给父类:BootstrapClassLoader.loadClass() │
│ │ │
│ ▼ │
│ Bootstrap找到了!返回java.lang.String │
│ │
│ 好处: │
│ ✓ 防止核心类被篡改(安全性) │
│ ✓ 防止类被重复加载(唯一性) │
│ │
└─────────────────────────────────────────────────────────────────────┘
3.2 JVM垃圾回收深度解析
GC是JVM最复杂的部分,也是面试最常问的难点!
垃圾回收算法:
/**
* JVM垃圾回收算法演示
*/
public class GCDemo {
public static void main(String[] args) {
// ========== 1. 引用计数法(已弃用) ==========
// 问题:循环引用无法回收
// A引用B,B引用A,但它们都不再被其他对象引用
// 引用计数都是1,无法回收!
// ========== 2. 可达性分析算法 ==========
// GC Roots 向下搜索,搜索路径叫"引用链"
// 不在引用链中的对象 = 垃圾
// GC Roots包括:
// - 虚拟机栈(栈帧中的本地变量表)中引用的对象
// - 方法区中类静态属性引用的对象
// - 方法区中常量引用的对象
// - 本地方法栈中JNI引用的对象
// - Java虚拟机内部的引用(Class对象、异常对象等)
// - 所有被同步锁(synchronized)持有的对象
// - 反映Java虚拟机内部情况的JMXBean、回调类、缓存类等
// ========== 3. 标记-清除算法 ==========
// 缺点:产生内存碎片
// ========== 4. 复制算法 ==========
// 把内存分成两块,每次只使用一块
// 清理时,把存活的对象复制到另一块,清理当前块
// 缺点:可用内存减半
// ========== 5. 标记-整理算法 ==========
// 标记后,让存活对象向一端移动,然后清理边界外内存
// 适合老年代
// ========== 6. 分代收集算法 ==========
// 新生代:复制算法(少量对象存活)
// 老年代:标记-整理算法(大量对象存活)
}
}
JVM内存分代模型:
┌─────────────────────────────────────────────────────────────────────┐
│ JVM内存分代模型 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ JVM进程内存 │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────┐ │ │
│ │ │ 方法区(Method Area) │ │ │
│ │ │ - 类信息、常量、静态变量 │ │ │
│ │ │ - 运行时常量池 │ │ │
│ │ │ - JIT编译后的代码 │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────┐ │ │
│ │ │ 堆(Heap) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 老年代(Old Generation) │ │ │ │
│ │ │ │ 容量: Eden + Survivor × 2 │ │ │ │
│ │ │ │ 垃圾回收:Major GC / Full GC │ │ │ │
│ │ │ │ 算法:标记-整理 / 标记-清除 │ │ │ │
│ │ │ └─────────────────────────────────────────────────┘ │ │ │
│ │ │ ↑ │ │ │
│ │ │ │ 晋升 │ │ │
│ │ │ │ │ │ │
│ │ │ ┌────────────┬────────┴────────┬───────────┐ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ Eden │ Survivor From │ SurvivorTo│ │ │ │
│ │ │ │ (伊甸园) │ (From) │ (To) │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ 8/10 │ 1/10 │ 1/10 │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ └────────────┴─────────────────┴───────────┘ │ │ │
│ │ │ 新生代(Young Generation) │ │ │
│ │ │ 垃圾回收:Minor GC / Young GC │ │ │
│ │ │ 算法:复制算法 │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────┐ │ │
│ │ │ 虚拟机栈(VM Stack) │ │ │
│ │ │ 每个线程私有 │ │ │
│ │ │ 栈帧:局部变量表、操作数栈、动态链接、方法返回 │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────┐ │ │
│ │ │ 本地方法栈(Native Method Stack) │ │ │
│ │ │ Native方法使用 │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────┐ │ │
│ │ │ 程序计数器(Program Counter) │ │ │
│ │ │ 每个线程私有,记录当前线程执行的字节码行号 │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
对象分配与回收流程:
┌─────────────────────────────────────────────────────────────────────┐
│ 对象分配与回收流程 │
│ │
│ 1️⃣ 新对象优先在Eden区分配 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Eden区 Survivor区 │ │
│ │ ┌──────────────────┐ ┌───────────┬───────────┐ │ │
│ │ │ │ │ From │ To │ │ │
│ │ │ [新对象] │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ └──────────────────┘ └───────────┴───────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 2️⃣ Minor GC(新生代垃圾回收) │
│ │
│ Eden区满 → Minor GC → 存活对象复制到Survivor区 │
│ 经历N次Minor GC后,晋升到老年代(默认15岁) │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 回收前 │ │
│ │ │ │
│ │ Eden区: [存活] [存活] [垃圾] [存活] [垃圾] [垃圾] │ │
│ │ Survivor From:[存活] [存活] │ │
│ │ Survivor To: (空) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ │ │
│ ▼ GC │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 回收后 │ │
│ │ │ │
│ │ Eden区: (清空) │ │
│ │ Survivor From:(清空) │ │
│ │ Survivor To: [存活] [存活] │ │
│ │ │ │
│ │ 年龄+1 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 3️⃣ Full GC(老年代垃圾回收) │
│ │
│ 触发条件: │
│ - 老年代空间不足 │
│ - System.gc() │
│ - MetaSpace空间不足 │
│ - Minor GC时,Survivor区放不下 │
│ │
└─────────────────────────────────────────────────────────────────────┘
常见GC器组合:
┌─────────────────────────────────────────────────────────────────────┐
│ 常见GC器组合 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Serial + Serial Old │ │
│ │ 特点:单线程、最简单、Stop The World最长 │ │
│ │ 适用:客户端模式、单核CPU │ │
│ │ -XX:+UseSerialGC │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Parallel Scavenge + Serial Old │ │
│ │ 特点:并行、吞吐量优先 │ │
│ │ 适用:后台运算,不需要太多交互 │ │
│ │ -XX:+UseParallelGC │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Parallel Scavenge + Parallel Old │ │
│ │ 特点:并行、吞吐量优先 │ │
│ │ 适用:注重吞吐量的场景 │ │
│ │ -XX:+UseParallelOldGC │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ParNew + CMS │ │
│ │ 特点:并发、低延迟 │ │
│ │ 适用:注重响应时间的场景 │ │
│ │ -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection │ │
│ │ │ │
│ │ CMS工作流程: │ │
│ │ 1. 初始标记(STW)— 标记GC Roots直接引用的对象 │ │
│ │ 2. 并发标记 — 遍历引用链 │ │
│ │ 3. 重新标记(STW)— 修正并发标记期间的变动 │ │
│ │ 4. 并发清除 — 清除垃圾 │ │
│ │ │ │
│ │ 缺点: │ │
│ │ - 无法处理浮动垃圾(并发时新产生的垃圾) │ │
│ │ - 内存碎片化(需要Full GC整理) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ G1 │ │
│ │ 特点:分区、可控STW、可并发 │ │
│ │ 适用:JDK 9+默认,推荐使用 │ │
│ │ -XX:+UseG1GC │ │
│ │ │ │
│ │ G1将堆划分为多个大小相等的Region: │ │
│ │ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │
│ │ │ E │ E │ S │ O │ O │ H │ E │ O │ │ │
│ │ │(Eden)│ │(Sur)│(Old)│ │(大对象)│ │ │ │ │
│ │ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │ │
│ │ │ │
│ │ 回收过程: │ │
│ │ 1. Young GC — 年轻代Region收集 │ │
│ │ 2. Mixed GC — 年轻代 + 若干老年代Region收集 │ │
│ │ 3. Full GC — 当G1无法跟上分配速度时 │ │
│ │ │ │
│ │ 设置目标延迟:-XX:MaxGCPauseMillis=200 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ZGC / Shenandoah │ │
│ │ 特点:极低延迟(<10ms)、并发、不压缩堆 │ │
│ │ 适用:大内存、低延迟要求的场景 │ │
│ │ -XX:+UseZGC │ │
│ │ -XX:+UseShenandoahGC │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
四、MySQL数据库:索引与事务
4.1 InnoDB索引结构:B+树
为什么MySQL选择B+树作为索引结构?这是面试必问题!
二叉树 vs 红黑树 vs B树 vs B+树:
-- 创建索引
CREATE INDEX idx_user_name ON user(name);
-- 查看表结构
SHOW CREATE TABLE user\G
┌─────────────────────────────────────────────────────────────────────┐
│ 索引结构对比 │
│ │
│ 1️⃣ 二叉树(Binary Tree) │
│ │
│ 50 │
│ / \ │
│ 30 70 │
│ / \ / \ │
│ 20 40 60 80 │
│ │
│ 问题:如果数据递增插入,退化成链表! │
│ 时间复杂度从O(logN)退化成O(N) │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 50 → 60 → 70 → 80 → 90 → ... │ │
│ │ / / / / / │ │
│ │ 50 60 70 80 90 (变成链表) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 2️⃣ 红黑树(自平衡二叉查找树) │
│ │
│ 特点:自动平衡,但高度仍然较高 │
│ 问题:数据量大时,树高 = logN可能达到20+, │
│ 每层都是一次IO操作,查询太慢! │
│ │
│ 3️⃣ B树(B-Tree,多路平衡查找树) │
│ │
│ [50] │
│ / | \ │
│ [20,30] [40] [60,70,80] │
│ │
│ B树特点: │
│ - 每个节点可以有多个数据 │
│ - 节点包含数据,叶子节点不在同一层 │
│ - 所有节点都存储数据 │
│ │
│ 问题:B树每个节点都存储数据,如果数据大, │
│ 每个节点能容纳的key就少,树高增加 │
│ │
│ 4️⃣ B+树(B+Tree)— MySQL选择的结构 │
│ │
│ [50] │
│ / | \ │
│ [20,30] [40] [60,70,80] │
│ | | | │
│ ┌──────────────┼──────┼───────┼──────────────────┐ │
│ │ │ │ │ │ │
│ [所有数据] [所有数据] [所有数据] ... │ │
│ │
│ B+树特点: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ✓ 非叶子节点只存储键(索引),不存储数据 │ │
│ │ ✓ 叶子节点包含所有数据,按顺序链表连接 │ │
│ │ ✓ 查询稳定,无论查哪个数据,都需要走到叶子节点 │ │
│ │ ✓ 树高更矮(通常3-4层),IO次数更少 │ │
│ │ ✓ 范围查询友好(链表遍历) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
B+树在MySQL中的实现:
┌─────────────────────────────────────────────────────────────────────┐
│ InnoDB B+树索引结构 │
│ │
│ InnoDB存储引擎以页为基本单位管理磁盘 │
│ 默认页大小 = 16KB │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ B+树结构 │ │
│ │ │ │
│ │ [根节点] │ │
│ │ 16KB / 14B ≈ 1170 个key │ │
│ │ │ │
│ │ ┌──────────┼──────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ [索引页1] [索引页2] [索引页3] ... │ │
│ │ 1170个key 1170个key 1170个key │ │
│ │ │ │
│ │ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ │ │
│ │ 数据 数据 数据 数据 数据 数据 数据 数据 │ │
│ │ 页1 页2 页3 页4 页5 页6 页7 页8 │ │
│ │ │ │
│ │ 每个数据页 ≈ 16KB / 每行数据大小 │ │
│ │ 如果每行1KB,则每页约16条数据 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 计算一棵3层B+树能存多少数据: │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 根节点:1170个索引 (1页) │ │
│ │ │ │
│ │ 每个索引指向一个下一页 │ │
│ │ 1170个索引 → 1170个叶子节点 │ │
│ │ │ │
│ │ 每个叶子节点:16KB / 1KB ≈ 16条数据 │ │
│ │ 1170 × 16 ≈ 18,720 条数据 │ │
│ │ │ │
│ │ 结论:3层B+树可存储约2万条数据 │ │
│ │ │ │
│ │ 实际场景(每行数据更小,如200B): │ │
│ │ 16KB / 200B = 80条/页 │ │
│ │ 1170 × 1170 × 80 ≈ 1亿+ 条数据 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
主键索引 vs 聚簇索引 vs 辅助索引:
-- 主键索引(聚簇索引)
-- 数据和索引在一起,叶子节点存储整行数据
ALTER TABLE user ADD PRIMARY KEY (id);
-- 辅助索引(非聚簇索引)
-- 叶子节点存储主键值
CREATE INDEX idx_name ON user(name);
┌─────────────────────────────────────────────────────────────────────┐
│ 聚簇索引 vs 辅助索引 │
│ │
│ 聚簇索引(Clustered Index): │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 主键 → 数据 │ │
│ │ │ │
│ │ 叶子节点存储: │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ id=1, name=Tom, age=25, email=Tom@xxx.com │ │ │
│ │ │ id=2, name=Jack, age=30, email=Jack@xxx.com │ │ │
│ │ │ id=3, name=Lucy, age=28, email=Lucy@xxx.com │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 特点: │ │
│ │ ✓ 一个表只能有一个聚簇索引 │ │
│ │ ✓ 数据按主键顺序物理存储 │ │
│ │ ✓ 主键查询快(直接命中) │ │
│ │ ✓ 范围查询快 │ │
│ │ ✓ 插入/删除可能导致页分裂 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 辅助索引(Secondary Index): │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ name → 主键id │ │
│ │ │ │
│ │ 叶子节点存储: │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Jack → id=2 │ │ │
│ │ │ Lucy → id=3 │ │ │
│ │ │ Tom → id=1 │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 特点: │ │
│ │ ✓ 一个表可以有多个辅助索引 │ │
│ │ ✓ 叶子节点存储主键值,不是完整数据 │ │
│ │ ✓ 先查辅助索引获取主键,再查聚簇索引(回表) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 回表查询: │
│ │
│ SELECT * FROM user WHERE name = 'Tom'; │
│ │
│ 执行过程: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1️⃣ 在idx_name(辅助索引)中查找'Tom' │ │
│ │ → 得到主键 id=1 │ │
│ │ │ │
│ │ 2️⃣ 拿着id=1回到主键索引(聚簇索引)查找 │ │
│ │ → 得到完整行数据 │ │
│ │ │ │
│ │ 这就是"回表查询" │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 优化:覆盖索引(using index) │
│ │
│ SELECT id, name FROM user WHERE name = 'Tom'; │
│ -- 只查id和name,而name是索引列 │
│ -- 只需要扫描辅助索引,无需回表 │
│ EXPLAIN: type=index, Extra=Using index │
│ │
└─────────────────────────────────────────────────────────────────────┘
五、Redis缓存:分布式系统必备
5.1 Redis为什么这么快?
Redis的高性能是面试必问,但要答出深度!
Redis为什么快?
┌─────────────────────────────────────────────────────────────────────┐
│ Redis高性能的秘密 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 六大原因 │ │
│ │ │ │
│ │ 1️⃣ 内存存储 │ │
│ │ 数据存在内存中,读写速度极快(纳秒级) │ │
│ │ RAM速度 >> SSD >> HDD │ │
│ │ │ │
│ │ 2️⃣ 单线程模型 │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ Redis Server │ │ │
│ │ │ ┌───────────┐ │ │ │
│ │ │ │ 主线程 │ │ │ │
│ │ │ │ 单线程 │ │ │ │
│ │ │ └───────────┘ │ │ │
│ │ │ │ │ │
│ │ │ 优点:无需考虑锁问题,代码简单 │ │ │
│ │ │ 缺点:CPU是瓶颈时,无法利用多核 │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 3️⃣ IO多路复用 │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ select/poll/epoll/kqueue │ │ │
│ │ │ 一个线程同时监听多个socket │ │ │
│ │ │ 有数据就处理,没有就阻塞 │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 4️⃣ 高效数据结构 │ │
│ │ String / List / Hash / Set / ZSet / Bitmap / Geo... │ │
│ │ 每种数据结构针对不同场景优化 │ │
│ │ │ │
│ │ 5️⃣ 纯C语言编写 │ │
│ │ 接近操作系统,性能好 │ │
│ │ │ │
│ │ 6️⃣ 客户端与服务端在同一个机器上 │ │
│ │ 网络延迟小 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
5.2 Redis分布式锁
分布式锁是面试高频题,也是实际开发中的难点!
Redis实现分布式锁:
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Redis分布式锁实现
*/
public class RedisLock {
private final RedisTemplate<String, Object> redisTemplate;
// 锁前缀
private static final String LOCK_PREFIX = "lock:";
// 唯一标识(区分不同JVM实例)
private final String instanceId;
public RedisLock(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.instanceId = UUID.randomUUID().toString();
}
/**
* 获取锁(SETNX + EXPIRE)
*
* 注意:分开执行不是原子操作!
* 更好的方式是 SET key value NX EX seconds
*/
public boolean tryLock(String key, long expireTime) {
String lockKey = LOCK_PREFIX + key;
String lockValue = instanceId + ":" + Thread.currentThread().getId();
// 方式1:分开执行(不安全)
// Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue);
// if (success) {
// redisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
// }
// 方式2:原子操作(推荐)
// SET key value NX EX seconds
return Boolean.TRUE.equals(
redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS)
);
}
/**
* 释放锁
*
* 注意:不能直接删除!
* 必须判断锁是否是自己的才能删除
* 否则可能释放别人的锁
*/
public boolean unlock(String key) {
String lockKey = LOCK_PREFIX + key;
String lockValue = instanceId + ":" + Thread.currentThread().getId();
// Lua脚本保证原子性
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(lockKey), lockValue);
return result != null && result > 0;
}
/**
* 带重试的获取锁
*/
public boolean tryLockWithRetry(String key, long expireTime,
int maxRetries, long retryInterval) {
for (int i = 0; i < maxRetries; i++) {
if (tryLock(key, expireTime)) {
return true;
}
try {
Thread.sleep(retryInterval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
/**
* 看门狗机制(自动续期)
*
* 问题:如果持有锁的线程执行时间过长,
* 锁会在expireTime后自动释放,导致其他线程获取锁
*
* 解决:启动一个后台线程,自动续期
*/
private static final long WATCHDOG_EXPIRE_TIME = 30; // 30秒
private static final long WATCHDOG_RENEW_INTERVAL = 10; // 10秒续期一次
public class WatchdogTask implements Runnable {
private final String lockKey;
private final String lockValue;
private volatile boolean running = true;
public WatchdogTask(String lockKey, String lockValue) {
this.lockKey = lockKey;
this.lockValue = lockValue;
}
public void stop() {
running = false;
}
@Override
public void run() {
while (running) {
try {
Thread.sleep(WATCHDOG_RENEW_INTERVAL * 1000);
// 续期
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
lockValue,
WATCHDOG_EXPIRE_TIME
);
} catch (Exception e) {
// 续期失败
break;
}
}
}
}
}
Redis分布式锁注意事项:
┌─────────────────────────────────────────────────────────────────────┐
│ Redis分布式锁注意事项 │
│ │
│ ⚠️ 问题1:锁续期 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 场景:线程A获取锁,30秒过期,但业务需要35秒 │ │
│ │ 结果:30秒后锁自动释放,线程B获取锁 │ │
│ │ 线程A执行完,释放线程B的锁 │ │
│ │ │ │
│ │ 解决:看门狗机制(Watchdog),自动续期 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ 问题2:主从切换 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 场景:Redis主从架构 │ │
│ │ 线程A在master获取锁 │ │
│ │ master宕机,slave还没同步 │ │
│ │ slave升级为master │ │
│ │ 线程B在新master获取锁(同一key!) │ │
│ │ 两个线程同时持有锁! │ │
│ │ │ │
│ │ 解决:RedLock算法(多master多数投票) │ │
│ │ 或使用Redisson客户端 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ 问题3:可重入性 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 场景:同一个线程多次获取同一把锁 │ │
│ │ │ │
│ │ 解决:计数器记录重入次数 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ✅ 推荐方案: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. Redisson(推荐) │ │
│ │ - 支持可重入锁、读写锁、公平锁 │ │
│ │ - 内置看门狗,自动续期 │ │
│ │ - 支持RedLock │ │
│ │ │ │
│ │ 2. 开源框架 │ │
│ │ - Redisson │ │
│ │ - ShardingJedis │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
六、Spring框架:核心原理
6.1 Spring循环依赖解决
循环依赖是Spring最经典的面试题之一!
三级缓存解决循环依赖:
/**
* Spring三级缓存源码解析
*/
public class CircularDependencyDemo {
public static void main(String[] args) {
// Spring的三级缓存
// DefaultSingletonBeanRegistry类
// 一级缓存:已完成的单例Bean
// private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:提前暴露的Bean(未完成属性填充)
// private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:Bean工厂
// private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);
}
}
/**
* getSingleton源码
*/
public class DefaultSingletonBeanRegistry {
public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1️⃣ 先从一级缓存获取
Object singletonObject = singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 2️⃣ 一级缓存没有,从二级缓存获取
synchronized (this.singletonObjects) {
singletonObject = earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 3️⃣ 二级缓存也没有,从三级缓存获取工厂
ObjectFactory<?> singletonFactory = singletonFactories.get(beanName);
if (singletonFactory != null) {
// 调用工厂获取早期引用
singletonObject = singletonFactory.getObject();
// 放入二级缓存
earlySingletonObjects.put(beanName, singletonObject);
// 从三级缓存移除
singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
/**
* 添加单例工厂到三级缓存
*/
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
}
}
}
}
循环依赖解决流程图:
┌─────────────────────────────────────────────────────────────────────┐
│ Spring三级缓存解决循环依赖 │
│ │
│ 场景:ServiceA依赖ServiceB,ServiceB依赖ServiceA │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 三级缓存结构 │ │
│ │ │ │
│ │ 一级缓存 singletonObjects │ │
│ │ ├── 完全初始化好的Bean │ │
│ │ └── 可以直接使用 │ │
│ │ │ │
│ │ 二级缓存 earlySingletonObjects │ │
│ │ ├── 提前曝光的Bean │ │
│ │ └── 未完全初始化(属性填充、初始化方法未执行) │ │
│ │ │ │
│ │ 三级缓存 singletonFactories │ │
│ │ ├── ObjectFactory工厂 │ │
│ │ └── 用于创建早期Bean的引用 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 解决流程: │
│ │
│ 步骤1:创建ServiceA │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1️⃣ 调用构造方法,创建原始对象A │ │
│ │ 2️⃣ 放入三级缓存singletonFactories │ │
│ │ { "serviceA": ObjectFactory<ServiceA> } │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 步骤2:填充ServiceA的属性,发现依赖ServiceB │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ @Autowired │ │
│ │ private ServiceB serviceB; │ │
│ │ │ │
│ │ → 尝试获取ServiceB │ │
│ │ → ServiceB还没创建!需要创建ServiceB │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 步骤3:创建ServiceB │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1️⃣ 调用构造方法,创建原始对象B │ │
│ │ 2️⃣ 放入三级缓存singletonFactories │ │
│ │ { "serviceB": ObjectFactory<ServiceB> } │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 步骤4:填充ServiceB的属性,发现依赖ServiceA │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ @Autowired │ │
│ │ private ServiceA serviceA; │ │
│ │ │ │
│ │ → 尝试获取ServiceA │ │
│ │ → 从三级缓存获取ServiceA的ObjectFactory │ │
│ │ → 调用getObject()获取早期引用 │ │
│ │ → 放入二级缓存earlySingletonObjects │ │
│ │ → 从三级缓存移除 │ │
│ │ │ │
│ │ 此时: │ │
│ │ - 一级缓存:{} │ │
│ │ - 二级缓存:{"serviceA": 早期A} │ │
│ │ - 三级缓存:{"serviceB": ObjectFactory} │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 步骤5:ServiceB填充完成 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1️⃣ 放入一级缓存singletonObjects │ │
│ │ 2️⃣ 从二级缓存移除 │ │
│ │ 3️⃣ 从三级缓存移除 │ │
│ │ │ │
│ │ 此时: │ │
│ │ - 一级缓存:{"serviceB": 完全B} │ │
│ │ - 二级缓存:{"serviceA": 早期A} │ │
│ │ - 三级缓存:{} │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 步骤6:回到ServiceA,填充ServiceB │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 从一级缓存获取ServiceB │ │
│ │ → ServiceA填充完成 │ │
│ │ → 放入一级缓存singletonObjects │ │
│ │ │ │
│ │ 完成!循环依赖解决! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
七、面试高频问题总结
7.1 技术问题清单
┌─────────────────────────────────────────────────────────────────────┐
│ 面试高频问题 │
│ │
│ 【Java基础】 │
│ ├── String为什么不可变? │
│ ├── String、StringBuilder、StringBuffer区别? │
│ ├── HashMap和Hashtable区别? │
│ ├── HashMap底层实现(JDK 7 vs JDK 8)? │
│ ├── ConcurrentHashMap如何保证线程安全? │
│ ├── Object的hashCode和equals方法? │
│ └── final关键字作用? │
│ │
│ 【并发编程】 │
│ ├── synchronized和ReentrantLock区别? │
│ ├── volatile关键字作用? │
│ ├── ThreadLocal原理?内存泄漏问题? │
│ ├── 线程池有哪些?拒绝策略? │
│ ├── CAS原理?ABA问题? │
│ ├── AQS原理? │
│ └── 生产者消费者模式实现? │
│ │
│ 【JVM】 │
│ ├── JVM内存区域划分? │
│ ├── 什么对象会被回收?GC Roots有哪些? │
│ ├── 垃圾回收算法? │
│ ├── 常见垃圾收集器?G1和CMS区别? │
│ ├── 类加载机制?双亲委派模型? │
│ ├── JVM调优参数? │
│ └── OOM如何排查? │
│ │
│ 【MySQL】 │
│ ├── InnoDB和MyISAM区别? │
│ ├── 索引数据结构?为什么是B+树? │
│ ├── 主键索引和辅助索引区别? │
│ ├── 聚簇索引和非聚簇索引? │
│ ├── 事务隔离级别?MVCC原理? │
│ ├── MySQL如何实现可重复读? │
│ ├── 慢查询如何优化? │
│ └── 分库分表策略? │
│ │
│ 【Redis】 │
│ ├── Redis为什么快? │
│ ├── Redis数据类型?应用场景? │
│ ├── Redis持久化?RDB和AOF区别? │
│ ├── Redis过期策略?内存淘汰策略? │
│ ├── Redis分布式锁?主从切换问题? │
│ ├── Redis和Memcached区别? │
│ └── Redis缓存问题?穿透、击穿、雪崩? │
│ │
│ 【Spring】 │
│ ├── Spring IOC流程? │
│ ├── Bean生命周期? │
│ ├── 循环依赖如何解决?三级缓存? │
│ ├── AOP原理?JDK和CGLIB区别? │
│ ├── @Transactional失效场景? │
│ └── Spring事务传播行为? │
│ │
│ 【分布式/微服务】 │
│ ├── CAP定理? │
│ ├── BASE理论? │
│ ├── 一致性Hash算法? │
│ ├── 分布式Session共享? │
│ ├── 幂等性如何保证? │
│ └── 分布式ID生成方案? │
│ │
└─────────────────────────────────────────────────────────────────────┘
7.2 面试技巧
┌─────────────────────────────────────────────────────────────────────┐
│ 面试技巧大公开 │
│ │
│ 1️⃣ STAR法则回答问题 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ S - Situation(情境): 项目背景是什么 │ │
│ │ T - Task(任务): 你负责什么 │ │
│ │ A - Action(行动): 你做了什么 │ │
│ │ R - Result(结果): 结果如何,学到什么 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 2️⃣ 技术问题回答结构 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 是什么 → 解决了什么问题 → 底层原理 → 实际应用 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 3️⃣ 不会的问题处理 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ✓ 诚实承认不了解 │ │
│ │ ✓ 但展示你的思路和好奇心 │ │
│ │ ✓ 尝试联系到已知知识 │ │
│ │ ✗ 不要瞎编乱造 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 4️⃣ 反问环节必问 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ✓ 技术栈是什么 │ │
│ │ ✓ 团队规模和分工 │ │
│ │ ✓ 技术挑战和成长机会 │ │
│ │ ✓ 业务和技术比重 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 5️⃣ 薪资谈判技巧 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ✓ 先了解市场行情 │ │
│ │ ✓ 给出期望区间,而非具体数字 │ │
│ │ ✓ 强调你的价值和不可替代性 │ │
│ │ ✓ 不要只谈钱,也谈成长和平台 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
结语
求职旺季,是每一位开发者展示自己的舞台。希望这篇文章能帮助你在面试中:
✅ 不仅能答出"是什么",更能答出"为什么"
✅ 不仅能说出结论,更能讲清原理
✅ 不仅能应对面试官,更能展现你的技术深度
记住:好的面试不是背答案,而是真正理解技术的本质。
最后,祝每一位小伙伴都能拿到心仪的Offer!💰
🎯 讨论话题: 大家在面试中还遇到过哪些"灵魂拷问"?欢迎在评论区分享!
👇 点击下方投票,告诉我你最想深入了解哪个专题:
- 并发编程(JUC)
- JVM调优
- MySQL优化
- Redis高级特性
- 分布式系统设计
本文首发于掘金,同步更新于CSDN
如果对你有帮助,请点赞 👍 收藏 ⭐ 关注 👀
你的支持是我创作的最大动力!