Java后端高频面试题
目录
- Java集合框架
- Java并发编程
- JVM相关
- MySQL数据库
- Redis缓存
- Spring框架
Java集合框架
HashMap的数据结构是什么,为什么在JDK8要引入红黑树?
HashMap数据结构:
- JDK7:数组 + 链表
- JDK8:数组 + 链表 + 红黑树
引入红黑树的原因:
- 性能优化:当链表长度过长时,查询效率从O(1)退化为O(n)
- 阈值设计:当链表长度达到或超过8时,考虑转换为红黑树(注意是考虑,不是立即转换),只有当HashMap的数组容量达到或超过64时,才会真正执行树化操作。≤6时转回链表
- 平衡性能:红黑树保证最坏情况下O(log n)的查询时间复杂度
HashMap的扩容机制是什么,为什么其数组长度是2的幂次?
扩容机制:
- 触发条件:当size > threshold(容量 × 负载因子0.75)时触发扩容
- 扩容过程:数组长度扩大为原来的2倍,重新hash分布元素
- rehash优化:JDK8中元素要么在原位置,要么在原位置+oldCap
2的幂次的原因:
一种更高效的取模运算,只用当length为2的幂次时,才可以用位运算替代
// 计算索引位置:hash & (length - 1)
// 当length为2的幂次时,length-1的二进制全为1
// 例如:length=16时,length-1=15(二进制1111)
// 这样可以充分利用hash值的所有位,减少hash冲突
为什么在JDK7到8要把头插改为尾插?
头插法问题(JDK7):
- 死循环风险:多线程扩容时可能形成环形链表
- 线程不安全:并发操作可能导致数据丢失
尾插法优势(JDK8):
- 避免死循环:保持原有顺序,不会形成环
- 更直观:插入顺序更符合逻辑
- 配合红黑树:为树化做准备
为什么它解决问题的方式是用链表加红黑树?
设计考虑:
- 兼容性:保持原有链表结构的简单性
- 性能平衡:红黑树维护成本比AVL树低
- 动态调整:根据冲突程度动态选择数据结构
- 空间效率:红黑树节点比链表节点占用更多空间,只在必要时使用
ArrayList和LinkedList的区别是什么?
特性 | ArrayList | LinkedList |
---|---|---|
底层结构 | 动态数组 | 双向链表 |
随机访问 | O(1) | O(n) |
插入删除(头尾) | O(n) | O(1) |
插入删除(中间) | O(n) | O(1) |
内存占用 | 较少 | 较多(存储指针) |
缓存友好性 | 好 | 差 |
使用建议:
- 频繁随机访问:ArrayList
- 频繁插入删除:LinkedList
- 内存敏感:ArrayList
Java并发编程
ConcurrentHashMap是怎么实现的,其在JDK7到8做了什么升级?
JDK7实现:
// 分段锁(Segment)+ HashEntry数组
// 默认16个Segment,每个Segment管理一部分数据
// 并发度 = Segment数量
JDK8升级:
// Node数组 + CAS + synchronized
// 取消Segment,使用Node数组
// 锁粒度更细:只锁链表头节点或红黑树根节点
// 并发度 = 数组长度
主要改进:
- 更高并发度:从16提升到数组长度
- 更少内存占用:去除Segment层级
- 更好性能:CAS + 局部锁
什么是乐观锁和悲观锁?
悲观锁:
- 概念:假设会发生冲突,每次操作都加锁
- 实现:synchronized、ReentrantLock
- 适用场景:写多读少
乐观锁:
- 概念:假设不会发生冲突,操作时检查是否被修改
- 实现:CAS、版本号机制
- 适用场景:读多写少
CAS是怎么实现的?
CAS(Compare And Swap):
// 伪代码
boolean compareAndSwap(int expectedValue, int newValue) {if (currentValue == expectedValue) {currentValue = newValue;return true;}return false;
}
底层实现:
- 硬件支持:CPU提供原子性指令
- 内存屏障:保证可见性和有序性
- 自旋机制:失败时重试
ABA问题解决:
- 使用版本号(AtomicStampedReference)
- 使用标记位(AtomicMarkableReference)
synchronized和ReentrantLock有什么区别?
特性 | synchronized | ReentrantLock |
---|---|---|
实现方式 | JVM内置 | JDK实现 |
锁释放 | 自动 | 手动(finally) |
公平性 | 非公平 | 可选公平/非公平 |
条件等待 | wait/notify | Condition |
中断响应 | 不可中断 | 可中断 |
尝试获取锁 | 不支持 | tryLock() |
原子类是如何实现的?
核心机制:
public class AtomicInteger {private volatile int value;public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}
}
实现要点:
- volatile保证可见性
- Unsafe类提供CAS操作
- 自旋重试机制
volatile关键字有什么用?
主要作用:
- 保证可见性:修改立即刷新到主内存
- 禁止指令重排序:通过内存屏障实现
- 不保证原子性:复合操作仍需同步
使用场景:
- 状态标记
- 双重检查锁定
- 单例模式
什么是JMM?
Java内存模型(JMM):
- 定义:规范多线程中变量访问规则
- 主内存:所有线程共享的内存区域
- 工作内存:每个线程的私有内存区域
核心规则:
- 所有变量存储在主内存
- 线程对变量的操作在工作内存进行
- 工作内存与主内存同步
什么是指令重排序?
概念:
编译器和处理器为优化性能,可能改变指令执行顺序
类型:
- 编译器重排序:编译时优化
- 指令级重排序:CPU执行时优化
- 内存系统重排序:缓存和写缓冲区优化
影响:
在多线程环境下可能导致程序行为异常
什么是happens-before原则?
核心规则:
- 程序顺序规则:单线程内按程序顺序执行
- 监视器锁规则:unlock happens-before lock
- volatile规则:写 happens-before 读
- 线程启动规则:start() happens-before 线程内操作
- 线程终止规则:线程操作 happens-before join()
- 传递性:A happens-before B,B happens-before C,则A happens-before C
synchronized的锁升级流程
升级路径:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
详细流程:
- 偏向锁:单线程访问,在对象头记录线程ID
- 轻量级锁:多线程竞争不激烈,使用CAS
- 重量级锁:竞争激烈,使用操作系统互斥量
synchronized是不是可重入锁,可重入锁是为了保证什么?
可重入性:
- synchronized是可重入锁
- 同一线程可以多次获取同一把锁
实现机制:
// 锁记录中维护获取次数
// 每次重入计数+1,退出时计数-1
// 计数为0时释放锁
保证目的:
- 避免死锁:防止线程自己阻塞自己
- 支持递归调用
- 简化编程模型
AQS队列是怎么实现的,其如何实现一个公平锁?
AQS(AbstractQueuedSynchronizer):
// 核心结构:双向链表 + state状态
static final class Node {Node prev;Node next;Thread thread;int waitStatus;
}
实现机制:
- 状态管理:使用int state表示同步状态
- 队列管理:FIFO队列管理等待线程
- 模板方法:子类实现具体的同步语义
公平锁实现:
protected final boolean tryAcquire(int acquires) {// 检查队列中是否有等待的线程if (hasQueuedPredecessors()) {return false;}// 尝试CAS获取锁return compareAndSetState(0, acquires);
}
线程池的核心参数是什么,它提交任务的流程是怎么样的,核心参数如何计算?
核心参数:
ThreadPoolExecutor(int corePoolSize, // 核心线程数int maximumPoolSize, // 最大线程数long keepAliveTime, // 空闲线程存活时间TimeUnit unit, // 时间单位BlockingQueue<Runnable> workQueue, // 任务队列ThreadFactory threadFactory, // 线程工厂RejectedExecutionHandler handler // 拒绝策略
)
提交流程:
- 当前线程数 < corePoolSize:创建新线程
- 核心线程已满:任务入队
- 队列已满且线程数 < maximumPoolSize:创建新线程
- 达到最大线程数:执行拒绝策略
参数计算:
// CPU密集型:核心线程数 = CPU核数 + 1
// IO密集型:核心线程数 = CPU核数 × (1 + IO时间/CPU时间)
JVM相关
接口和抽象类的区别
特性 | 接口 | 抽象类 |
---|---|---|
继承关系 | implements(可多个) | extends(单个) |
方法实现 | JDK8前只能抽象方法 | 可以有具体实现 |
变量 | public static final | 任意访问修饰符 |
构造方法 | 不能有 | 可以有 |
设计理念 | 能力契约 | 模板设计 |
什么是单例模式?
定义:
确保一个类只有一个实例,并提供全局访问点
实现方式:
- 饿汉式:类加载时创建
- 懒汉式:首次使用时创建
- 双重检查锁定:线程安全的懒汉式
- 枚举实现:最安全的实现
写一个双重锁检查
public class Singleton {private volatile static Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) {if (instance == null) { // 第二次检查instance = new Singleton();}}}return instance;}
}
关键点:
- volatile关键字:防止指令重排序
- 双重检查:减少同步开销
- synchronized:保证线程安全
JVM调优做过吗?
调优目标:
- 降低GC频率:减少Stop-The-World时间
- 提高吞吐量:单位时间处理更多任务
- 降低延迟:减少响应时间
常用参数:
# 堆内存设置
-Xms2g -Xmx2g# 新生代设置
-Xmn800m# 垃圾收集器选择
-XX:+UseG1GC# GC日志
-XX:+PrintGC -XX:+PrintGCDetails
JVM垃圾回收算法
标记-清除(Mark-Sweep):
- 标记所有需要回收的对象,然后清除
- 优点:简单
- 缺点:产生内存碎片
复制算法(Copying):
- 将内存分为两块,每次只使用一块
- 优点:无碎片,效率高
- 缺点:内存利用率低
标记-整理(Mark-Compact):
- 标记后将存活对象向一端移动
- 优点:无碎片,内存利用率高
- 缺点:移动对象成本高
分代收集:
- 新生代:复制算法
- 老年代:标记-清除或标记-整理
JVM内存空间如何分配?
堆内存:
- 新生代:Eden + Survivor0 + Survivor1(8:1:1)
- 老年代:长期存活的对象
非堆内存:
- 方法区:类信息、常量池
- 直接内存:NIO使用
- 栈内存:局部变量、方法调用
分配流程:
- 新对象在Eden区分配
- Eden满时触发Minor GC
- 存活对象进入Survivor区
- 经过多次GC后进入老年代
什么是逃逸分析?
定义:
分析对象的作用域,判断对象是否会逃出方法或线程
优化策略:
- 栈上分配:不逃逸的对象可在栈上分配
- 标量替换:将对象分解为基本类型
- 锁消除:消除不必要的同步
判断条件:
- 对象是否被返回
- 对象是否被外部引用
- 对象是否在其他线程中使用
如何避免Out Of Memory这个错误?
预防措施:
- 合理设置堆内存:-Xms和-Xmx
- 避免内存泄漏:及时释放资源
- 使用对象池:重用对象
- 选择合适的数据结构
排查步骤:
- 堆dump分析:使用MAT、jhat
- 监控工具:jstat、jvisualvm
- 代码review:检查循环引用、大对象
你项目中遇到过内存泄漏的问题吗,如何解决?
常见内存泄漏场景:
- 集合类持有对象引用
- 监听器未正确移除
- 数据库连接未关闭
- ThreadLocal使用不当
解决方案:
// 使用try-with-resources
try (Connection conn = getConnection()) {// 数据库操作
}// ThreadLocal及时清理
threadLocal.remove();// 弱引用处理监听器
WeakHashMap<Object, Listener> listeners;
MySQL数据库
MySQL的事务隔离级别有哪些?
四个隔离级别:
-
READ UNCOMMITTED(读未提交)
- 最低级别,可能出现脏读、不可重复读、幻读
-
READ COMMITTED(读已提交)
- Oracle默认级别,避免脏读,可能出现不可重复读、幻读
-
REPEATABLE READ(可重复读)
- MySQL默认级别,避免脏读、不可重复读,可能出现幻读
-
SERIALIZABLE(序列化)
- 最高级别,避免所有问题,但性能最差
什么是ACID?
原子性(Atomicity):
- 事务中的所有操作要么全部成功,要么全部失败
- 通过undo log实现
一致性(Consistency):
- 事务执行前后,数据库状态保持一致
- 通过其他三个特性保证
隔离性(Isolation):
- 并发事务之间相互隔离,不受影响
- 通过锁和MVCC实现
持久性(Durability):
- 事务提交后,对数据的修改永久保存
- 通过redo log实现
在MVCC机制下可重复读是怎么实现的,它还会幻读吗?
MVCC实现原理:
-- 每行记录包含两个隐藏字段
-- trx_id: 创建该版本的事务ID
-- roll_pointer: 指向undo log的指针-- ReadView包含:
-- m_ids: 当前活跃事务列表
-- min_trx_id: 最小活跃事务ID
-- max_trx_id: 下一个事务ID
-- creator_trx_id: 创建ReadView的事务ID
可重复读实现:
- 事务开始时创建ReadView
- 根据ReadView判断数据版本可见性
- 整个事务期间使用同一个ReadView
幻读问题:
- 快照读:通过MVCC避免幻读
- 当前读:通过Next-Key Lock避免幻读
什么是间隙锁,什么是临键锁?
间隙锁(Gap Lock):
- 锁定记录之间的间隙,防止插入新记录
- 只在可重复读级别下生效
临键锁(Next-Key Lock):
- 记录锁 + 间隙锁的组合
- 锁定记录本身和记录前面的间隙
示例:
-- 假设索引值:1, 3, 5, 7, 10
-- 查询条件:WHERE id > 3 AND id < 7
-- Gap Lock: (3,5), (5,7)
-- Next-Key Lock: (3,5], (5,7]
什么是索引的回表查询,如何避免?
回表查询:
通过二级索引找到主键值,再通过主键索引查找完整记录
避免方法:
- 覆盖索引:查询字段都在索引中
- 联合索引:将常用查询字段组合成索引
- 主键选择:使用自增主键减少回表
示例:
-- 需要回表
SELECT * FROM user WHERE name = 'John';-- 覆盖索引,无需回表
SELECT id, name FROM user WHERE name = 'John';
MySQL有哪些常见的索引?
按数据结构分类:
- B+Tree索引:InnoDB默认索引类型
- Hash索引:Memory存储引擎支持
- Full-Text索引:全文检索
按物理存储分类:
- 聚簇索引:数据和索引存储在一起(主键索引)
- 非聚簇索引:索引和数据分别存储(二级索引)
按逻辑分类:
- 主键索引:唯一且不为空
- 唯一索引:值唯一但可为空
- 普通索引:无唯一性限制
- 联合索引:多个字段组合
索引在什么情况下会失效?
常见失效场景:
-- 1. 违反最左前缀原则
-- 索引:(a, b, c)
SELECT * FROM t WHERE b = 1 AND c = 1; -- 失效-- 2. 使用函数或计算
SELECT * FROM t WHERE UPPER(name) = 'JOHN'; -- 失效-- 3. 类型转换
SELECT * FROM t WHERE id = '123'; -- 可能失效-- 4. 使用NOT、!=、<>
SELECT * FROM t WHERE name != 'John'; -- 失效-- 5. LIKE以通配符开头
SELECT * FROM t WHERE name LIKE '%John'; -- 失效-- 6. OR条件中有未建索引的字段
SELECT * FROM t WHERE name = 'John' OR age = 25; -- 可能失效
explain关键字
主要字段:
EXPLAIN SELECT * FROM user WHERE name = 'John';-- id: 查询序列号
-- select_type: 查询类型(SIMPLE、PRIMARY、SUBQUERY等)
-- table: 表名
-- type: 访问类型(system > const > eq_ref > ref > range > index > ALL)
-- possible_keys: 可能使用的索引
-- key: 实际使用的索引
-- key_len: 索引长度
-- ref: 索引比较的列
-- rows: 扫描的行数
-- Extra: 额外信息
type字段重要性(性能从好到坏):
- system/const: 最优
- eq_ref: 唯一性索引扫描
- ref: 非唯一性索引扫描
- range: 范围扫描
- index: 索引全扫描
- ALL: 全表扫描(最差)
InnoDB下MySQL索引的数据结构是什么,为什么选它不选别的?
数据结构:B+Tree
选择原因:
- 减少磁盘IO:树高度低,通常3-4层
- 范围查询友好:叶子节点链表结构
- 插入性能好:相比红黑树更适合磁盘存储
- 空间利用率高:非叶子节点只存储键值
对比其他结构:
- Hash:等值查询快,但不支持范围查询
- 二叉树:树高度高,IO次数多
- B-Tree:非叶子节点存储数据,空间利用率低
了解过MySQL的三大日志吗?
redo log(重做日志):
- 作用:保证事务持久性
- 机制:WAL(Write-Ahead Logging)
- 刷盘策略:innodb_flush_log_at_trx_commit
undo log(回滚日志):
- 作用:保证事务原子性,实现MVCC
- 内容:记录数据修改前的值
- 清理:事务提交后可以清理
binlog(二进制日志):
- 作用:主从复制、数据恢复
- 格式:STATEMENT、ROW、MIXED
- 位置:MySQL Server层
Redis缓存
Redis有哪些数据类型,他们有哪些主要的应用场景?
基本数据类型:
-
String(字符串)
SET key value GET key
- 应用:缓存、计数器、session存储
-
Hash(哈希)
HSET user:1 name "John" age 25 HGET user:1 name
- 应用:存储对象、用户信息
-
List(列表)
LPUSH list1 "a" "b" "c" RPOP list1
- 应用:消息队列、最新列表
-
Set(集合)
SADD tags "java" "redis" "mysql" SINTER set1 set2
- 应用:标签、去重、交集运算
-
Sorted Set(有序集合)
ZADD leaderboard 100 "player1" 200 "player2" ZRANGE leaderboard 0 -1
- 应用:排行榜、优先级队列
缓存穿透、缓存击穿、缓存雪崩
缓存穿透:
- 问题:查询不存在的数据,缓存和数据库都没有
- 解决方案:
- 布隆过滤器
- 缓存空值(设置较短过期时间)
缓存击穿:
- 问题:热点key过期,大量请求直接打到数据库
- 解决方案:
- 互斥锁重建缓存
- 热点数据永不过期
- 提前异步刷新
缓存雪崩:
- 问题:大量key同时过期或Redis宕机
- 解决方案:
- 过期时间随机化
- 多级缓存
- 限流降级
Redis和数据库的一致性问题怎么解决?
一致性策略:
-
Cache Aside(旁路缓存)
// 读取 data = cache.get(key); if (data == null) {data = db.get(key);cache.set(key, data); }// 更新 db.update(key, data); cache.delete(key); // 删除缓存
-
延迟双删
cache.delete(key); db.update(key, data); Thread.sleep(500); // 延迟 cache.delete(key);
-
使用消息队列
- 数据库更新后发送消息
- 消费者异步更新缓存
Redis为什么快,可以说说Redis的IO多路复用模型吗?
Redis快的原因:
- 内存存储:数据存储在内存中
- 单线程模型:避免线程切换和锁竞争
- 高效数据结构:针对性优化的数据结构
- IO多路复用:高效处理网络IO
IO多路复用模型:
Client1 ----\
Client2 ------> Redis Server (单线程)
Client3 ----/
工作原理:
- 事件循环:主线程在事件循环中处理IO事件
- 多路复用器:使用epoll/kqueue监听多个socket
- 非阻塞IO:socket设置为非阻塞模式
- 事件驱动:有数据可读/写时触发事件
用Redis实现一套登录的机制
实现方案:
// 登录
public String login(String username, String password) {// 验证用户名密码if (validateUser(username, password)) {// 生成tokenString token = UUID.randomUUID().toString();// 存储到Redis,设置过期时间String key = "session:" + token;Map<String, String> userInfo = new HashMap<>();userInfo.put("username", username);userInfo.put("loginTime", String.valueOf(System.currentTimeMillis()));redisTemplate.opsForHash().putAll(key, userInfo);redisTemplate.expire(key, 30, TimeUnit.MINUTES);return token;}return null;
}// 验证登录状态
public boolean isLogin(String token) {String key = "session:" + token;return redisTemplate.hasKey(key);
}// 登出
public void logout(String token) {String key = "session:" + token;redisTemplate.delete(key);
}// 续期
public void refreshSession(String token) {String key = "session:" + token;if (redisTemplate.hasKey(key)) {redisTemplate.expire(key, 30, TimeUnit.MINUTES);}
}
用Redis做防抖和节流
防抖实现(Debounce):
public class RedisDebounce {@Autowiredprivate RedisTemplate<String, String> redisTemplate;public boolean debounce(String key, long delayMs) {String debounceKey = "debounce:" + key;// 设置键值,如果键已存在则不执行Boolean result = redisTemplate.opsForValue().setIfAbsent(debounceKey, "1", delayMs, TimeUnit.MILLISECONDS);return result != null && result;}
}
节流实现(Throttle):
public class RedisThrottle {@Autowiredprivate RedisTemplate<String, String> redisTemplate;// 固定窗口节流public boolean throttle(String key, int maxRequests, long windowMs) {String throttleKey = "throttle:" + key;Long count = redisTemplate.opsForValue().increment(throttleKey);if (count == 1) {redisTemplate.expire(throttleKey, windowMs, TimeUnit.MILLISECONDS);}return count <= maxRequests;}// 滑动窗口节流public boolean slidingWindowThrottle(String key, int maxRequests, long windowMs) {String throttleKey = "sliding:" + key;long now = System.currentTimeMillis();long windowStart = now - windowMs;// 使用Lua脚本保证原子性String luaScript = "redis.call('zremrangebyscore', KEYS[1], 0, ARGV[1]) " +"local count = redis.call('zcard', KEYS[1]) " +"if count < tonumber(ARGV[2]) then " +" redis.call('zadd', KEYS[1], ARGV[3], ARGV[3]) " +" redis.call('expire', KEYS[1], ARGV[4]) " +" return 1 " +"else " +" return 0 " +"end";Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),Collections.singletonList(throttleKey),String.valueOf(windowStart),String.valueOf(maxRequests),String.valueOf(now),String.valueOf(windowMs / 1000));return result != null && result == 1;}
}
Redis主从同步
主从复制原理:
-
全量同步:
- 从库发送PSYNC命令
- 主库执行BGSAVE生成RDB文件
- 主库将RDB文件发送给从库
- 从库加载RDB文件
-
增量同步:
- 主库将写命令记录到复制缓冲区
- 异步发送给从库执行
配置示例:
# 从库配置
slaveof 192.168.1.100 6379
# 或
replicaof 192.168.1.100 6379# 只读模式
slave-read-only yes
主从切换(哨兵模式):
# 哨兵配置
sentinel monitor mymaster 192.168.1.100 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
Redis的持久化机制
RDB(Redis Database):
- 原理:某个时间点的数据快照
- 触发条件:
save 900 1 # 900秒内至少1个key变化 save 300 10 # 300秒内至少10个key变化 save 60 10000 # 60秒内至少10000个key变化
- 优点:文件小,恢复快
- 缺点:可能丢失数据
AOF(Append Only File):
- 原理:记录所有写操作命令
- 同步策略:
appendfsync always # 每次写入都同步 appendfsync everysec # 每秒同步(默认) appendfsync no # 操作系统决定何时同步
- 优点:数据安全性高
- 缺点:文件大,恢复慢
混合持久化(RDB + AOF):
aof-use-rdb-preamble yes
- RDB作为基础数据
- AOF记录RDB之后的增量操作
Redis中的大key和热key如何去优化?
大key优化:
-
拆分策略:
// 原来:一个大hash HSET user:info name age email address ...// 拆分后:多个小hash HSET user:basic name age HSET user:contact email phone HSET user:address province city
-
分片存储:
// 将大list分片存储 for (int i = 0; i < totalSize; i += batchSize) {String shardKey = key + ":" + (i / batchSize);// 存储分片数据 }
热key优化:
-
多级缓存:
// 本地缓存 + Redis缓存 Object data = localCache.get(key); if (data == null) {data = redisCache.get(key);if (data != null) {localCache.put(key, data);} }
-
读写分离:
- 读操作分散到多个从节点
- 热key复制到多个实例
-
热key发现:
// 使用监控工具 redis-cli --hotkeys // 或使用应用层统计
Spring框架
Spring Boot设计了哪些设计模式?
主要设计模式:
- 单例模式:Spring Bean默认是单例
- 工厂模式:BeanFactory创建Bean实例
- 代理模式:AOP的实现基础
- 模板方法模式:JdbcTemplate、RestTemplate
- 观察者模式:ApplicationEvent事件机制
- 策略模式:不同的Bean创建策略
- 装饰器模式:BeanWrapper装饰Bean
- 适配器模式:HandlerAdapter适配不同Controller
什么是IOC?
控制反转(Inversion of Control):
- 定义:将对象的创建和依赖关系的管理交给容器
- 核心思想:不要主动创建依赖对象,而是被动接收
DI(依赖注入)实现方式:
// 1. 构造器注入
@Component
public class UserService {private final UserRepository userRepository;public UserService(UserRepository userRepository) {this.userRepository = userRepository;}
}// 2. Setter注入
@Component
public class UserService {private UserRepository userRepository;@Autowiredpublic void setUserRepository(UserRepository userRepository) {this.userRepository = userRepository;}
}// 3. 字段注入
@Component
public class UserService {@Autowiredprivate UserRepository userRepository;
}
SpringBoot的循环依赖怎么解决?
循环依赖场景:
@Component
public class A {@Autowiredprivate B b;
}@Component
public class B {@Autowiredprivate A a;
}
解决机制(三级缓存):
// 第一级缓存:成品对象
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();// 第二级缓存:半成品对象
private final Map<String, Object> earlySingletonObjects = new HashMap<>();// 第三级缓存:对象工厂
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();
解决流程:
- 创建A的实例,放入三级缓存
- 填充A的属性,发现需要B
- 创建B的实例,放入三级缓存
- 填充B的属性,发现需要A
- 从三级缓存获取A的实例,放入二级缓存
- B创建完成,放入一级缓存
- A创建完成,放入一级缓存
AOP是怎么实现的?
AOP(面向切面编程)实现原理:
-
JDK动态代理(接口代理):
public class JdkProxyExample implements InvocationHandler {private Object target;public Object invoke(Object proxy, Method method, Object[] args) {// 前置通知System.out.println("Before method: " + method.getName());Object result = method.invoke(target, args);// 后置通知System.out.println("After method: " + method.getName());return result;} }
-
CGLIB代理(类代理):
public class CglibProxyExample implements MethodInterceptor {public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {System.out.println("Before method: " + method.getName());Object result = proxy.invokeSuper(obj, args);System.out.println("After method: " + method.getName());return result;} }
AOP配置示例:
@Aspect
@Component
public class LoggingAspect {@Pointcut("execution(* com.example.service.*.*(..))")public void serviceMethods() {}@Before("serviceMethods()")public void logBefore(JoinPoint joinPoint) {System.out.println("执行方法: " + joinPoint.getSignature().getName());}@AfterReturning(pointcut = "serviceMethods()", returning = "result")public void logAfterReturning(JoinPoint joinPoint, Object result) {System.out.println("方法返回: " + result);}
}
除了JDK的动态代理你还了解过其他的代理模式吗?
静态代理:
// 接口
interface UserService {void addUser(String name);
}// 目标类
class UserServiceImpl implements UserService {public void addUser(String name) {System.out.println("添加用户: " + name);}
}// 代理类
class UserServiceProxy implements UserService {private UserService userService;public UserServiceProxy(UserService userService) {this.userService = userService;}public void addUser(String name) {System.out.println("权限检查");userService.addUser(name);System.out.println("日志记录");}
}
字节码生成代理:
- CGLIB:运行时生成字节码
- Javassist:编译时字节码操作
- ASM:低级别字节码操作框架
Spring的AOP是运行时代理还是编译时代理?
Spring AOP是运行时代理:
- 在应用启动时创建代理对象
- 使用JDK动态代理或CGLIB生成代理类
- 代理对象在运行时拦截方法调用
对比编译时代理:
- AspectJ:编译时织入,性能更好
- Spring AOP:运行时代理,使用简单
配置AspectJ编译时织入:
<plugin><groupId>org.aspectj</groupId><artifactId>aspectj-maven-plugin</artifactId><configuration><complianceLevel>1.8</complianceLevel></configuration>
</plugin>
事务注解什么时候失效?
常见失效场景:
-
方法不是public:
@Transactional private void updateUser() { // 失效// 更新操作 }
-
自调用问题:
@Service public class UserService {public void method1() {this.method2(); // 失效,没有通过代理调用}@Transactionalpublic void method2() {// 事务方法} }
-
异常被捕获:
@Transactional public void updateUser() {try {// 数据库操作} catch (Exception e) {// 异常被捕获,事务不回滚} }
-
异常类型不匹配:
@Transactional // 默认只回滚RuntimeException public void updateUser() throws Exception {throw new Exception(); // 不会回滚 }// 正确配置 @Transactional(rollbackFor = Exception.class) public void updateUser() throws Exception {throw new Exception(); // 会回滚 }
-
数据库引擎不支持:
- MyISAM不支持事务
- 需要使用InnoDB引擎
解决方案:
// 1. 使用ApplicationContext获取代理对象
@Autowired
private ApplicationContext applicationContext;public void method1() {UserService proxy = applicationContext.getBean(UserService.class);proxy.method2();
}// 2. 使用@EnableAspectJAutoProxy(exposeProxy = true)
public void method1() {UserService proxy = (UserService) AopContext.currentProxy();proxy.method2();
}// 3. 注入自己
@Autowired
private UserService userService;public void method1() {userService.method2();
}
总结
本文涵盖了Java后端开发中的核心知识点,包括:
- 集合框架:HashMap、ArrayList等核心数据结构的实现原理
- 并发编程:线程安全、锁机制、线程池等并发处理技术
- JVM调优:内存管理、垃圾回收、性能优化策略
- 数据库:MySQL事务、索引、查询优化等数据库技术
- 缓存技术:Redis数据类型、持久化、集群等缓存方案
- 框架原理:Spring IOC、AOP、事务管理等框架核心
这些知识点不仅是面试的重点,更是日常开发中需要深入理解和灵活运用的核心技术。建议结合实际项目经验,深入理解每个技术点的适用场景和最佳实践。