微众银行 面试准备
汇总问题(Java后端开发方向)
面向对象的特点是什么?Object类包含哪些常用方法?异常的分类、继承结构是什么?受检查异常与非受检查异常的区别及举例?OutOfMemoryError属于受检查异常吗?自定义类时需要注意哪些事项?重写equals()和hashCode()的注意事项是什么?equals()相等的对象,hashCode()一定相等吗?String、StringBuilder、StringBuffer的区别是什么?为什么有了String还需要后两者?List、Set、Map三大集合的核心区别(存储结构、元素特性、常用实现类)是什么?ArrayList的初始化容量是多少?扩容机制是什么?线程的创建方式有哪些?线程的生命周期包含哪些状态?Thread.sleep()会释放持有的锁吗?为什么?线程间的通信方式有哪些?线程安全的单例模式如何实现?有哪些方式可以破坏单例模式?synchronized和Lock的区别是什么?为什么有了synchronized还需要Lock?synchronized能否保证可见性?为什么?如何暂停多个正在运行的线程?有哪些注意事项?线程A和线程B同时对同一变量做加法,如何保证结果正确(线程安全)?什么是自旋锁?适用场景和优缺点是什么?ThreadLocal的原理是什么?使用时需注意哪些问题(如内存泄漏)?线程池的核心参数有哪些?线程池的阻塞队列有哪些实现方式及特点?AQS(AbstractQueuedSynchronizer)的核心原理是什么?在哪些并发工具中被使用?什么是消息幂等性?分布式系统中如何保证消息幂等消费?JVM的运行时内存区域包含哪些部分?哪个区域不会发生内存溢出?如何判断对象是垃圾(可回收)?常用的垃圾回收算法有哪些?什么是GCRoots?如何确定GCRoots与对象的引用关系?StackOverflowError和OutOfMemoryError的产生场景是什么?如何排查解决?内存泄漏和内存溢出的区别是什么?内存泄漏的常见场景及解决思路?常用的JVM启动参数有哪些?如何分类?常用的JVM调优命令有哪些?jps和jstack的作用是什么?CMS垃圾收集器的执行过程是什么?CMS执行慢如何排查?Spring的核心思想是什么?IOC和AOP的定义及好处是什么?Spring的核心功能有哪些?Spring容器的启动过程是什么?Spring中哪些注解可将Bean放入IOC容器?@Bean和@Component的区别是什么?Spring事务的隔离级别和传播行为有哪些?Spring事务失效的常见场景有哪些?Spring AOP的实现原理是什么?JDK动态代理和CGLIB动态代理的区别是什么?@SpringBootApplication包含哪些核心注解?SpringBoot自动装配原理是什么?Spring ApplicationContext的作用是什么?与BeanFactory的区别是什么?SpringTask是什么?了解哪些分布式任务调度框架?InnoDB和MyISAM的核心区别(事务、锁、索引等)是什么?MySQL的锁有哪些类型?什么是Gap锁(间隙锁)?什么情况下触发?什么是悲观锁和乐观锁?它们是真实存在的锁吗?适用场景及实现方式?Join操作中,100万数据的表和20万数据的表,选哪个作为主表(驱动表)?为什么?什么是索引失效?常见场景有哪些?为什么“范围查询放等值查询前”会导致索引失效?什么是慢查询?如何定位、排查和优化?除索引失效外,还有哪些原因导致慢查询?什么是回表查询和索引下推?对查询性能有什么影响?什么是深度分页?如何解决深度分页的性能问题?如何手动构造MySQL死锁场景?(需写SQL和执行步骤)MySQL中NULL和空字符串(“”)的区别是什么?(空间占用、count统计等)SQL调优的常见手段有哪些?explain分析除type外,还需关注哪些字段?Redis为什么运行快?为什么采用单线程模型?Redis的持久化机制有哪些?RDB和AOF的区别、优缺点是什么?如何保证Redis与MySQL的数据一致性?(更新策略、缓存失效处理等)缓存穿透、击穿、雪崩分别是什么?各自的解决方案是什么?常用的开源限流组件有哪些?限流的核心算法是什么?常用的消息中间件有哪些?消息中间件的作用是什么?如何保证消息只消费一次?如何实时获取消息队列的消息消费状态?RocketMQ如何实现分布式事务?“分布式任务调度+本地消息表”如何保证数据安全?什么是服务熔断?实现原理是什么?项目中如何选择熔断组件?Alibaba Druid连接池的优势是什么?与HikariCP的区别是什么?为什么要分库分表?常见的实现方案有哪些?(如Sharding-JDBC)如何设计分库分表方案?对现有方案不满意时如何优化?如何将微服务架构改造为单体架构?改造需注意哪些问题?TCP三次握手的过程是什么?为什么需要三次握手?TCP四次挥手的过程是什么?为什么需要四次挥手?客户端TIME_WAIT状态等待2MSL的原因?IO多路复用的三种实现(select、poll、epoll)是什么?区别和适用场景?HTTP不同版本(1.0、1.1、2、3)的核心差异是什么?HTTP请求体的常见类型有哪些?如何设计银行系统?(核心功能、数据安全、高可用、并发处理等)Servlet、Filter、Listener的作用是什么?执行顺序是什么?熟悉哪些Linux常用命令?(至少5个)find命令的常用用法是什么?如何对40亿个不重复数字进行升序排序?(考虑内存限制,要求快速)如何在100亿个数字中找到中位数?(考虑内存限制)如何判断两个字符串是否为异构词(Anagram)?(如“listen”和“silent”)如何对TXT文件中的上万行车牌号按字典序排序?(含非程序员方案)实现快速排序算法?如何将“单边遍历”快排改造为“双边遍历”版本?如何用Java实现队列?如何实现线程安全的队列?了解哪些设计模式?项目中如何使用模板方法模式?有哪些替代方案?介绍一个你参与的项目,核心功能是什么?你负责哪些工作?项目开发中遇到的最大难点是什么?如何分析和解决?你最有成就感的程序或项目是什么?为什么?项目的数据量有多大?如何处理大规模数据的存储和查询性能问题?项目中是否使用过多线程?如何保证多线程安全性?项目中是否阅读过框架底层源码(如Spring、MySQL驱动)?举例说明学到的内容?如何实时显示微信二维码的扫描状态?从服务端、客户端、微信官方接口说明流程?为什么选择跨专业考研?本科专业是什么?研究生期间成绩最好和最差的两门课是什么?原因是什么?有其他公司的offer吗?收到微众offer会优先选择吗?实习时间如何安排?可接受的实习地点(如深圳、武汉)是什么?作为新人,如何提升工作效率和质量?为什么想换工作(社招)?你认为什么是有挑战的工作?对金融科技行业有哪些看法?了解微众的核心产品(如微粒贷、微业贷)吗?什么是自定义注解?如何用自定义注解实现非空判断?了解哪些AI相关技术?实现“AI查询所有存在的代码”的思路是什么?
答案
1. 面向对象的特点是什么?
答:面向对象有三大核心特点,分别是封装、继承、多态,部分场景会补充“抽象”,具体如下:
封装:将对象的属性(数据)和方法(行为)封装在一起,隐藏内部实现细节,仅通过对外暴露的接口(如get/set方法)交互。作用是提高代码安全性(避免属性被随意修改)和可维护性(修改内部逻辑不影响外部调用)。继承:子类可以继承父类的非私有属性和方法,同时可定义自身特有的属性和方法。作用是减少代码重复(复用父类逻辑),建立类之间的层级关系(如“动物”→“猫”→“橘猫”)。多态:同一行为(如“叫”)在不同对象上有不同实现(猫“喵喵叫”、狗“汪汪叫”)。实现方式有“方法重写”(子类重写父类方法)和“方法重载”(同一类中方法名相同、参数不同)。作用是提高代码灵活性,降低类之间的耦合度。
2. Object类包含哪些常用方法?
答:Object是Java中所有类的父类(默认继承),包含7个常用方法,具体如下:
:判断两个对象是否“相等”。默认实现是“==”(比较内存地址),自定义类通常会重写(如比较对象的属性值)。
equals(Object obj):返回对象的哈希码(int值)。默认实现与对象内存地址相关,重写
hashCode()时必须同步重写
equals(保证equals相等的对象hashCode也相等)。
hashCode:返回对象的字符串表示(默认格式为“类名@哈希码十六进制”)。重写后可自定义输出(如“User{id=1, name=‘张三’}”),方便日志打印和调试。
toString():返回对象的运行时类(Class对象)。作用是获取类的元信息(如类名、方法、属性),常用于反射。
getClass():创建并返回对象的“浅拷贝”。使用时需让类实现
clone()接口(标记接口,无实际方法),否则会抛
Cloneable。
CloneNotSupportedException:使当前线程释放对象锁,进入等待状态,直到被
wait()/
notify()唤醒或超时。需在
notifyAll()代码块中调用(否则抛
synchronized)。
IllegalMonitorStateException/
notify():唤醒当前对象锁上等待的一个(notify)或所有(notifyAll)线程。同样需在
notifyAll()代码块中调用。
synchronized
3. 异常的分类、继承结构是什么?受检查异常与非受检查异常的区别及举例?OutOfMemoryError属于受检查异常吗?
答:首先明确“异常”和“错误”的区别:异常(Exception)是程序可处理的问题,错误(Error)是程序无法处理的严重问题(如OOM),二者都继承自。
Throwable
(1)继承结构
(顶层父类)
Throwable
├─ (错误,程序无法处理):如
Error、
OutOfMemoryError
StackOverflowError
└─ (异常,程序可处理)
Exception
├─ 受检查异常(Checked Exception):非及其子类,如
RuntimeException、
IOException
SQLException
└─ 非受检查异常(Unchecked Exception):及其子类,如
RuntimeException、
NullPointerException
ArrayIndexOutOfBoundsException
(2)受检查与非受检查异常的区别
| 维度 | 受检查异常(Checked) | 非受检查异常(Unchecked) |
|---|---|---|
| 继承关系 | 继承,非 |
继承 |
| 编译要求 | 必须显式处理(try-catch捕获或throws声明) | 无需显式处理,编译不报错 |
| 常见场景 | 外部环境导致的问题(如文件不存在、数据库连接失败) | 代码逻辑错误(如空指针、数组越界) |
| 举例 | (文件读取异常)、(数据库异常) |
(空指针)、(参数非法) |
(3)OutOfMemoryError的类型
属于
OutOfMemoryError(错误),而非“受检查异常”。它是JVM内存耗尽时抛出的严重错误,程序无法通过try-catch处理,只能通过JVM调优(如扩大堆内存)或优化代码(如避免内存泄漏)预防。
Error
4. 自定义类时需要注意哪些事项?重写equals()和hashCode()的注意事项是什么?equals()相等的对象,hashCode()一定相等吗?
答:#### (1)自定义类的注意事项
类名符合“驼峰命名法”(首字母大写,如而非
User),避免与JDK自带类重名(如不命名为
user)。属性私有化(用private修饰),通过public的get/set方法对外暴露(封装思想),避免直接暴露属性导致随意修改。若类需被序列化(如网络传输、持久化),需实现
String接口,并定义
Serializable(避免反序列化时因版本不一致报错)。重写
serialVersionUID方法:默认
toString()输出格式不直观,重写后可清晰展示对象属性(如“User{id=1, name=‘张三’}”),方便调试。若类需比较“内容相等”(而非内存地址),需重写
toString()和
equals(),且保证二者逻辑一致。
hashCode()
(2)重写equals()的注意事项
自反性:必须返回true(对象自己等于自己)。对称性:若
x.equals(x)为true,则
x.equals(y)也必须为true(A等于B,B也等于A)。传递性:若
y.equals(x)和
x.equals(y)为true,则
y.equals(z)也必须为true(A等于B,B等于C,A等于C)。一致性:若x和y的属性未修改,多次调用
x.equals(z)结果必须一致(不随时间变化)。非空性:
x.equals(y)必须返回false(任何对象都不等于null)。逻辑聚焦:仅比较“核心属性”(如User的id),避免比较临时属性(如
x.equals(null))。
lastLoginTime
(3)重写hashCode()的注意事项
一致性:若x和y的返回true,则
equals()和
x.hashCode()必须相等(核心原则,否则HashMap等集合会出错)。离散性:若x和y的
y.hashCode()返回false,
equals()和
x.hashCode()尽量不相等(减少哈希冲突,提高HashMap查询效率)。稳定性:若对象的属性未修改,
y.hashCode()的返回值必须不变(避免HashMap中对象“丢失”)。
hashCode()
(4)equals()与hashCode()的关系
必须满足:若为true,则
x.equals(y) ==
x.hashCode()(否则HashMap无法正确存储和查找对象)。不强制满足:若
y.hashCode()为false,
x.equals(y)也可能等于
x.hashCode()(即哈希冲突,HashMap通过链表/红黑树解决)。结论:
y.hashCode()相等的对象,
equals()一定相等;
hashCode()相等的对象,
hashCode()不一定相等。
equals()
5. String、StringBuilder、StringBuffer的区别是什么?为什么有了String还需要后两者?
答:三者核心区别在于可变性和线程安全性,具体如下:
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变(底层是final char数组) | 可变(底层是char数组,无final修饰) | 可变(同StringBuilder) |
| 线程安全 | 安全(不可变对象天然线程安全) | 不安全(无同步锁) | 安全(方法加synchronized锁) |
| 性能 | 低(拼接时创建新对象,如生成新String) |
高(直接修改原数组,无锁开销) | 中(有锁开销,比String快) |
| 适用场景 | 字符串不频繁修改(如常量定义、固定文本) | 单线程下频繁修改字符串(如循环拼接) | 多线程下频繁修改字符串(如多线程日志拼接) |
为什么需要StringBuilder和StringBuffer?
因为String是不可变对象:每次对String进行拼接(如)、替换等操作时,都会创建一个新的String对象,原对象成为垃圾(需GC回收)。若频繁修改字符串(如循环拼接1000次),会产生大量临时对象,导致内存浪费和性能下降。
str += "a"
而StringBuilder和StringBuffer是可变对象,修改时直接操作底层char数组(无需创建新对象),大幅提升性能;其中StringBuffer通过同步锁保证线程安全,StringBuilder则舍弃锁以追求更高性能,满足不同场景需求。
6. List、Set、Map三大集合的核心区别(存储结构、元素特性、常用实现类)是什么?
答:三者均属于Java集合框架(包),但定位和特性差异极大,具体对比如下:
java.util
| 维度 | List(列表) | Set(集合) | Map(映射) |
|---|---|---|---|
| 存储结构 | 线性结构(有序,按插入顺序保存) | 哈希/红黑树结构(无序,除TreeSet) | 键值对(key-value)结构,key唯一 |
| 元素特性 | 元素可重复,可通过索引(index)访问 | 元素不可重复(依赖equals和hashCode) | key不可重复(同Set),value可重复 |
| 核心接口方法 | (按索引取元素)、(添加到末尾) |
(添加,重复则失败)、(判断是否包含) |
(存键值对)、(按key取值) |
| 常用实现类 | 1. ArrayList(数组实现,查询快、增删慢) 2. LinkedList(链表实现,增删快、查询慢) 3. Vector(ArrayList的线程安全版,已过时) |
1. HashSet(哈希表实现,无序,性能高) 2. TreeSet(红黑树实现,按自然顺序排序) 3. LinkedHashSet(哈希表+链表,按插入顺序排序) |
1. HashMap(哈希表实现,无序,性能高,JDK1.8后数组+链表/红黑树) 2. TreeMap(红黑树实现,按key自然排序) 3. LinkedHashMap(哈希表+链表,按插入/访问顺序排序) 4. Hashtable(HashMap的线程安全版,已过时) |
| 适用场景 | 需按顺序存储、重复元素,且频繁查询(如“用户列表”) | 需去重、无需顺序(如“已选标签”) | 需通过key快速查找value(如“用户ID→用户信息”映射) |
7. ArrayList的初始化容量是多少?扩容机制是什么?
答:#### (1)初始化容量
ArrayList的初始化容量分两种情况,取决于构造方法:
无参构造():JDK1.7及之前默认初始容量为10;JDK1.8及之后优化为“延迟初始化”——初始时底层数组为
new ArrayList<>()(空数组),第一次添加元素时才将容量扩容为10。有参构造(如
EMPTY_ELEMENTDATA):初始容量为传入的参数值(如10);若传入
new ArrayList<>(10)对象(如
Collection),初始容量为该Collection的元素个数。
new ArrayList<>(set)
(2)扩容机制
当ArrayList的元素个数()达到当前容量(
size)时,会触发扩容,步骤如下:
capacity
计算新容量:默认扩容为“当前容量的1.5倍”(公式:,如10→15,15→22)。特殊情况处理:
newCapacity = oldCapacity + (oldCapacity >> 1)
若新容量小于“最小需求容量”(如添加大量元素时,需直接扩容到需求容量),则新容量=最小需求容量。若新容量超过ArrayList的最大容量(,避免内存溢出),则新容量=
Integer.MAX_VALUE - 8(极端情况)。
Integer.MAX_VALUE
数组拷贝:创建一个新容量的数组,将原数组的元素通过拷贝到新数组,底层数组引用指向新数组。
Arrays.copyOf()
注意点
扩容是“按需触发”的,且每次扩容都会产生数组拷贝(有性能开销)。若已知元素个数(如1000个),建议使用有参构造指定初始容量(),避免多次扩容。
new ArrayList<>(1000)
8. 线程的创建方式有哪些?线程的生命周期包含哪些状态?
答:#### (1)线程的创建方式(4种)
Java中创建线程的核心是“实现接口”或“继承
Runnable类”,衍生出4种常用方式:
Thread
继承类:重写
Thread方法(线程执行逻辑),通过
run()方法启动线程(注意:直接调用
start()是普通方法,不会启动新线程)。
run()
示例:,调用:
class MyThread extends Thread { public void run() { System.out.println("线程执行"); } }实现
new MyThread().start();接口:实现
Runnable方法,将
run()对象传给
Runnable构造器,调用
Thread启动。
start()
示例:,调用:
class MyRunnable implements Runnable { public void run() { ... } }实现
new Thread(new MyRunnable()).start();接口:与
Callable类似,但可返回结果(
Runnable方法有返回值)且可抛异常,需配合
call()获取结果。
FutureTask
示例:,调用:
class MyCallable implements Callable<Integer> { public Integer call() { return 1; } }线程池创建:通过
FutureTask<Integer> task = new FutureTask<>(new MyCallable()); new Thread(task).start(); Integer result = task.get();或
Executors创建线程池,从池中获取线程执行任务(推荐,避免频繁创建销毁线程的开销)。
ThreadPoolExecutor
示例:
ExecutorService pool = Executors.newFixedThreadPool(5); pool.submit(new Runnable() { ... });
(2)线程的生命周期(6种状态,JDK定义在
Thread.State枚举中)
Thread.State
(新建):线程对象已创建,但未调用
NEW方法(未启动)。
start()(可运行):调用
RUNNABLE后,线程进入“就绪”或“运行”状态:
start()
就绪:线程已具备运行条件,等待CPU调度(如刚启动、从阻塞态唤醒)。运行:线程正在占用CPU执行方法。
run()
(JDK未区分“就绪”和“运行”,统一归为)
RUNNABLE
(阻塞):线程因竞争
BLOCKED锁失败,进入阻塞态(等待锁释放)。
synchronized(等待):线程调用
WAITING、
wait()、
join()等方法,进入无超时等待态(需被其他线程唤醒,如
LockSupport.park())。
notify()(超时等待):线程调用
TIMED_WAITING、
wait(long timeout)、
sleep(long millis)等方法,进入有超时等待态(超时后自动唤醒,或被提前唤醒)。
join(long millis)(终止):线程的
TERMINATED方法执行完毕,或因异常退出,线程生命周期结束(不可再启动)。
run()
状态转换核心路径
→
NEW(调用
RUNNABLE) → (CPU调度)执行
start() →
run()(执行完毕);
TERMINATED
→
RUNNABLE(竞争锁失败) →
BLOCKED(获取锁);
RUNNABLE
→
RUNNABLE/
WAITING(调用等待方法) →
TIMED_WAITING(被唤醒/超时)。
RUNNABLE
9. Thread.sleep()会释放持有的锁吗?为什么?
答:不会释放。原因如下:
(1)Thread.sleep()的设计目的
的作用是“让当前线程暂停执行指定时间”,期间线程放弃CPU使用权(进入
sleep(long millis)状态),但不释放已持有的资源(包括
TIMED_WAITING锁、Lock锁等)。设计初衷是“暂停执行”,而非“释放资源”——比如线程持有锁后需要等待某个时间(如1秒后执行下一步),若释放锁会导致其他线程抢占锁,破坏原有逻辑。
synchronized
(2)对比“会释放锁”的方法
与不同,
sleep()方法会释放当前对象的
Object.wait()锁:
synchronized
的设计目的是“让线程等待某个条件满足”(如等待队列不为空),释放锁后其他线程可修改条件(如往队列加元素),条件满足后再通过
wait()唤醒线程并重新竞争锁。示例:若线程A持有
notify()锁,调用
synchronized (lock)会释放锁;若调用
lock.wait(),则会保持锁1秒,期间其他线程无法获取
Thread.sleep(1000)锁。
lock
结论
:暂停执行,不释放锁,超时后自动恢复
Thread.sleep()状态。
RUNNABLE:暂停执行,释放
Object.wait()锁,需被
synchronized唤醒。
notify()
10. 线程间的通信方式有哪些?
答:线程间通信的核心是“让线程感知其他线程的状态或数据变化”,常用方式有5种,具体如下:
等待/通知机制(wait()/notify()/notifyAll())
基于类的方法,需在
Object代码块中调用(确保线程持有对象锁)。流程:线程A调用
synchronized,释放
lock.wait()锁并进入等待队列;线程B修改条件后调用
lock,唤醒A线程;A线程重新竞争
lock.notify()锁,获取后继续执行。适用场景:线程间需按“条件”协作(如生产者-消费者模型:生产者生产后通知消费者消费)。
lock
join()方法
的作用是“让当前线程等待目标线程执行完毕后再继续”。示例:主线程中调用
Thread.join(),主线程会阻塞,直到
threadA.join()的
threadA方法执行完毕,主线程才恢复执行。适用场景:需保证线程执行顺序(如主线程需等待子线程计算完结果后再汇总)。
run()
volatile关键字
修饰的变量具有“可见性”:一个线程修改变量后,其他线程能立即看到最新值(避免CPU缓存导致的“数据不一致”)。流程:线程A修改
volatile变量
volatile为
flag,线程B循环读取
true,一旦读取到
flag则执行后续逻辑。适用场景:简单的状态传递(如用
true控制线程停止),不支持原子性(如
volatile boolean stop的
volatile int count仍需锁保证线程安全)。
count++
管道流(PipedInputStream/PipedOutputStream)
基于IO流的通信方式,仅适用于“两个线程”之间的字节数据传输(如线程A写数据到,线程B从
PipedOutputStream读数据)。特点:传输数据是“双向的”,但需注意线程安全(避免同时读写导致死锁)。适用场景:线程间需传递二进制数据(如子线程处理文件流后传给主线程)。
PipedInputStream
并发工具类(如CountDownLatch、CyclicBarrier)
基于AQS实现的高级通信工具,适用于复杂场景:
:让一个线程等待多个线程执行完毕(如主线程等待5个子线程初始化完成,每个子线程完成后
CountDownLatch,主线程
countDown()等待)。
await():让多个线程等待彼此到达“屏障点”后再共同继续(如3个线程都执行到
CyclicBarrier后,才一起执行后续逻辑)。
barrier.await()
适用场景:多线程协同(如分布式任务拆分、并发测试)。
11. 线程安全的单例模式如何实现?有哪些方式可以破坏单例模式?
答:单例模式的核心是“确保一个类只有一个实例,并提供全局访问点”,线程安全的实现需避免“多线程同时创建实例”,破坏单例则需突破“唯一实例”的限制。
(1)线程安全的单例实现方式(3种常用)
饿汉式(静态常量/静态代码块)
原理:类加载时直接创建实例(JVM类加载是线程安全的,避免多线程竞争)。代码:
public class Singleton {
// 静态常量方式:类加载时初始化
private static final Singleton INSTANCE = new Singleton();
// 私有构造器(禁止外部new)
private Singleton() {}
// 全局访问点
public static Singleton getInstance() {
return INSTANCE;
}
}
优点:简单、线程安全(无锁开销);缺点:类加载时就创建实例,若实例未被使用会浪费内存(如工具类单例)。
懒汉式(双重检查锁,DCL)
原理:“延迟初始化”(第一次调用时创建实例),通过“双重if检查+volatile”避免线程安全问题。代码:
getInstance()
public class Singleton {
// volatile修饰:禁止指令重排序(避免多线程下获取到“半初始化实例”)
private static volatile Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
// 第一次检查:若实例已存在,直接返回(避免每次加锁)
if (INSTANCE == null) {
// 加锁:确保只有一个线程进入创建逻辑
synchronized (Singleton.class) {
// 第二次检查:防止多线程等待锁时重复创建
if (INSTANCE == null) {
INSTANCE = new Singleton(); // 非原子操作,需volatile禁止重排序
}
}
}
return INSTANCE;
}
}
关键:不可省略——
volatile分3步(1.分配内存;2.初始化实例;3.引用指向内存),若发生指令重排序(1→3→2),线程B可能获取到“未初始化的实例”并报错。优点:延迟初始化、线程安全、性能高(仅第一次创建时加锁);缺点:代码稍复杂。
INSTANCE = new Singleton()
静态内部类(Holder模式)
原理:利用JVM“静态内部类加载延迟”特性——外部类加载时,静态内部类不加载;第一次调用时,加载静态内部类并创建实例(类加载线程安全)。代码:
getInstance()
public class Singleton {
private Singleton() {}
// 静态内部类:仅在调用getInstance()时加载
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
优点:延迟初始化、线程安全、代码简洁(结合饿汉式和懒汉式的优点);缺点:无法传参(若实例创建需参数,需改用DCL)。
(2)破坏单例模式的方式(3种)
反射(Reflection)
原理:通过反射调用私有构造器,强制创建新实例。示例:
// 获取单例类的构造器(setAccessible(true)突破私有限制)
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
// 反射创建新实例,与原有实例不是同一个
Singleton instance1 = constructor.newInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2); // false
防御:在私有构造器中加判断,若实例已存在则抛异常:
private Singleton() {
if (SingletonHolder.INSTANCE != null) {
throw new IllegalStateException("单例类禁止反射创建");
}
}
序列化与反序列化
原理:若单例类实现接口,序列化(
Serializable)后再反序列化(
ObjectOutputStream.writeObject()),会生成新实例(反序列化时会调用无参构造器创建对象)。示例:
ObjectInputStream.readObject()
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
oos.writeObject(Singleton.getInstance());
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.txt"));
Singleton instance3 = (Singleton) ois.readObject();
System.out.println(instance3 == Singleton.getInstance()); // false
防御:在单例类中重写方法,返回已有实例:
readResolve()
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
克隆(Clone)
原理:若单例类实现接口并重写
Cloneable方法,调用
clone()会创建新实例(浅拷贝)。示例:
clone()
Singleton instance4 = (Singleton) Singleton.getInstance().clone();
System.out.println(instance4 == Singleton.getInstance()); // false
防御:重写方法时返回已有实例,而非新实例:
clone()
@Override
protected Object clone() throws CloneNotSupportedException {
return getInstance();
}
12. synchronized和Lock的区别是什么?为什么有了synchronized还需要Lock?
答:#### (1)synchronized和Lock的核心区别
是Java原生关键字,
synchronized是
Lock包下的接口(需手动实现,如
java.util.concurrent.locks),二者区别如下:
ReentrantLock
| 维度 | synchronized | Lock(以ReentrantLock为例) |
|---|---|---|
| 锁的获取与释放 | 自动:进入代码块时获取锁,退出时自动释放(包括异常退出) |
手动:需调用获取锁,释放锁(必须在finally中调用,避免异常导致锁泄漏) |
| 可中断性 | 不可中断:线程获取锁失败时,会一直阻塞(除非被中断,但需特殊处理) | 可中断:支持,线程阻塞时可通过中断并退出阻塞 |
| 公平性 | 非公平锁:默认不保证线程获取锁的顺序(先请求的线程可能后获取),无法设置为公平锁 | 可配置:构造器可指定(公平锁,按请求顺序分配锁)或(非公平锁) |
| 条件变量 | 仅支持一个条件变量(通过//) |
支持多个条件变量(通过创建,如生产者-消费者模型中“空队列等待”和“满队列等待”可分开) |
| 锁状态查询 | 无法查询:无API获取锁的持有状态、等待队列长度等 | 可查询:提供(是否被锁)、(等待队列长度)等方法 |
| 性能 | JDK1.6后优化(如偏向锁、轻量级锁),性能接近Lock | 性能稳定,在高并发下略优于synchronized(无JVM层面的优化,但可控性强) |
(2)为什么需要Lock?
虽简单易用(自动释放锁、低学习成本),但在复杂场景下存在局限性,Lock的出现正是为了弥补这些不足:
synchronized
解决“死锁”的可能性:
synchronized无法中断阻塞中的线程(若线程A持有锁,线程B一直阻塞等待,无法主动让B退出);而Lock的支持中断,可在超时或特定条件下中断线程,避免死锁。
lockInterruptibly()
支持公平锁:
synchronized仅是非公平锁,可能导致“线程饥饿”(某些线程长期得不到锁);Lock可配置为公平锁,按“先到先得”的顺序分配锁,适合对公平性有要求的场景(如金融交易排队)。
多条件变量分离:
synchronized仅一个条件变量,若线程需等待不同条件(如生产者-消费者模型中,生产者等“队列不满”,消费者等“队列不空”),只能用一个等待队列,唤醒时需(唤醒所有线程,效率低);而Lock的多条件变量可分开等待队列,唤醒时只需唤醒对应条件的线程(如
notifyAll()只唤醒消费者),提升效率。
notEmpty.signal()
精细化锁控制:
Lock提供锁状态查询(如判断当前线程是否持有锁)、超时获取锁(
isHeldByCurrentThread(),超时后放弃获取,避免无限阻塞)等功能,适合需要精细化控制的场景(如限时任务)。
tryLock(long timeout, TimeUnit unit)
总结
简单场景(如单线程安全的方法):用(代码简洁,不易出错)。复杂场景(如公平锁、多条件等待、超时控制):用
synchronized(灵活性高,可控性强)。
Lock
13. synchronized能否保证可见性?为什么?
答:能保证。原因与的“内存语义”和JVM的“内存屏障”机制有关。
synchronized
(1)可见性的定义
可见性是指“一个线程修改共享变量后,其他线程能立即看到该变量的最新值”。若缺乏可见性,可能导致线程A修改了变量,线程B仍读取到
x的旧值(因CPU缓存未刷新到主内存)。
x
(2)synchronized保证可见性的原理
synchronized的内存语义包含两点,共同保证可见性:
锁释放时的“写回主内存”:
当线程退出代码块(释放锁)时,JVM会强制将该线程在锁期间修改的所有共享变量,从线程的“工作内存”(CPU缓存、寄存器)刷新到“主内存”。
synchronized
示例:线程A在块中修改
synchronized,释放锁时
x=1会被写回主内存。
x=1
锁获取时的“读取主内存”:
当线程进入代码块(获取锁)时,JVM会强制将该线程的“工作内存”中所有共享变量的值置为无效,后续读取这些变量时,必须从“主内存”重新加载最新值。
synchronized
示例:线程B获取锁后,读取时会从主内存获取
x(而非工作内存中的旧值)。
x=1
(3)底层支撑:内存屏障
JVM通过在的“锁获取”和“锁释放”位置插入内存屏障(Memory Barrier),禁止指令重排序并强制刷新内存,从而保证可见性:
synchronized
锁获取时:插入“LoadLoad屏障”和“LoadStore屏障”,确保后续读取操作从主内存加载,且不与之前的操作重排序。锁释放时:插入“StoreStore屏障”和“StoreLoad屏障”,确保之前的写操作已刷新到主内存,且不与之后的操作重排序。
示例验证
public class SyncVisibility {
private boolean flag = false; // 共享变量,无volatile修饰
public synchronized void setFlag() {
flag = true; // 锁释放时写回主内存
}
public synchronized void getFlag() {
if (flag) { // 锁获取时从主内存读取
System.out.println("flag已变为true,可见性生效");
}
}
public static void main(String[] args) throws InterruptedException {
SyncVisibility demo = new SyncVisibility();
// 线程A修改flag
new Thread(() -> demo.setFlag()).start();
Thread.sleep(100);
// 线程B读取flag
new Thread(() -> demo.getFlag()).start(); // 会输出内容,证明可见性
}
}
注意点
synchronized同时保证原子性(同一时间只有一个线程执行锁内代码)和可见性,但不保证有序性(允许锁内代码指令重排序,只要不影响单线程执行结果)。
14. 如何暂停多个正在运行的线程?有哪些注意事项?
答:“暂停线程”的核心是“让线程暂时停止执行,且可恢复执行”,而非“终止线程”(终止是永久停止)。常用实现方式有3种,需避免已废弃的/
stop()方法(会导致资源泄漏或死锁)。
suspend()
(1)常用实现方式
基于volatile的标志位控制
原理:用作为暂停标志,线程循环检查标志位,为
volatile boolean pause时进入等待(如
true或
Thread.sleep()),为
LockSupport.park()时继续执行。代码示例:
false
public class PauseThreadByVolatile {
private volatile boolean pause = false; // 暂停标志,volatile保证可见性
// 暂停线程的方法
public void pauseThreads() {
pause = true;
}
// 恢复线程的方法
public void resumeThreads() {
pause = false;
}
// 线程执行逻辑
public void run() {
while (true) {
// 检查暂停标志,为true时暂停
while (pause) {
try {
Thread.sleep(100); // 暂停100ms,避免CPU空转
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 保留中断状态
}
}
// 正常执行逻辑
System.out.println("线程正在执行...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
适用场景:简单的批量暂停(如控制多个工作线程同时暂停),优点是实现简单,缺点是暂停有延迟(需等待线程执行到标志位检查逻辑)。
基于LockSupport的park()/unpark()
原理:是JDK提供的线程阻塞工具,
LockSupport让当前线程暂停(进入
park()状态),
WAITING唤醒指定线程。支持“先unpark后park”(不会导致永久阻塞)。代码示例(管理多个线程):
unpark(Thread t)
public class PauseThreadByLockSupport {
private List<Thread> threadList = new ArrayList<>(); // 存储需控制的线程
// 添加线程到管理列表
public void addThread(Thread thread) {
threadList.add(thread);
}
// 暂停所有线程
public void pauseAllThreads() {
for (Thread thread : threadList) {
LockSupport.park(thread); // 暂停指定线程
}
}
// 恢复所有线程
public void resumeAllThreads() {
for (Thread thread : threadList) {
LockSupport.unpark(thread); // 唤醒指定线程
}
}
// 线程执行逻辑
public static void main(String[] args) throws InterruptedException {
PauseThreadByLockSupport manager = new PauseThreadByLockSupport();
// 创建3个线程
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(() -> {
while (true) {
System.out.println("线程" + Thread.currentThread().getName() + "执行中");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
manager.addThread(thread);
thread.start();
}
Thread.sleep(2000);
manager.pauseAllThreads(); // 2秒后暂停所有线程
System.out.println("所有线程已暂停");
Thread.sleep(3000);
manager.resumeAllThreads(); // 3秒后恢复所有线程
System.out.println("所有线程已恢复");
}
}
优点:暂停/恢复响应快(直接作用于线程),支持单个线程控制;缺点:需维护线程列表,适合线程数量固定的场景。
基于CountDownLatch的协作暂停
原理:的
CountDownLatch方法让线程等待“计数器归0”,
await()方法减少计数器。若计数器初始为1,线程调用
countDown()时暂停,外部调用
await()(计数器归0)时恢复。代码示例:
countDown()
public class PauseThreadByCountDownLatch {
private CountDownLatch latch = new CountDownLatch(1); // 计数器初始为1
// 暂停线程(线程调用此方法会等待)
public void pause() throws InterruptedException {
latch.await(); // 计数器非0,线程暂停
}
// 恢复线程(计数器归0)
public void resume() {
latch.countDown(); // 计数器从1→0,唤醒所有等待线程
}
// 注意:CountDownLatch计数器归0后无法重置,若需重复暂停/恢复,需用CyclicBarrier或重新创建CountDownLatch
}
适用场景:单次暂停(如所有线程等待某个初始化操作完成),不支持重复暂停(计数器无法重置)。
(2)注意事项
禁止使用废弃方法:会暂停线程但不释放锁,导致其他线程阻塞;
Thread.suspend()会强制终止线程,可能导致资源未释放(如文件流未关闭、锁未释放),JDK已明确废弃这两个方法。
Thread.stop()
保证暂停标志的可见性:若用标志位控制(如方式1),标志位必须用修饰,或通过
volatile/
synchronized保证可见性,否则线程可能读取到旧的标志位值,无法暂停。
Lock
避免CPU空转:线程暂停时,不要用“空循环”(如),应调用
while(pause) {}或
Thread.sleep()让线程放弃CPU,减少资源消耗。
LockSupport.park()
处理中断状态:若线程在暂停时被中断(如),需保留中断状态(
interrupt()),避免后续逻辑无法感知中断(如线程需根据中断状态退出)。
Thread.currentThread().interrupt()
线程安全的列表管理:若管理多个线程(如方式2),添加/删除线程的列表操作需加锁(如或
synchronized),避免并发修改异常。
CopyOnWriteArrayList
15. 线程A和线程B同时对同一变量做加法,如何保证结果正确(线程安全)?
答:线程A和线程B并发执行(非原子操作)时,会出现“数据覆盖”(如A读取count=1,B也读取count=1,均加1后写回,最终count=2而非3)。保证线程安全的核心是“将非原子操作变为原子操作”,常用方案有5种:
count++
15. 线程A和线程B同时对同一变量做加法,如何保证结果正确(线程安全)?
答:线程并发执行时,因
count++是“读取-修改-写入”非原子操作,易出现“数据覆盖”(如A、B同时读
count++,均加1后写回,最终
count=1而非预期的
count=2)。保证线程安全的核心是“将非原子操作原子化”或“避免并发修改共享变量”,常用方案有5种:
3
(1)使用
synchronized关键字
synchronized
原理:通过锁定共享变量的“修改逻辑”,确保同一时间只有一个线程执行
synchronized(原子化操作)。代码示例:
count++
public class SyncAdd {
private int count = 0;
// 锁定当前对象(也可锁定类对象或其他共享锁对象)
public synchronized void add() {
count++; // 同步块内,操作原子化
}
public int getCount() {
return count;
}
// 测试:2个线程各加1000次
public static void main(String[] args) throws InterruptedException {
SyncAdd demo = new SyncAdd();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) demo.add();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) demo.add();
});
t1.start();
t2.start();
t1.join(); // 等待线程执行完毕
t2.join();
System.out.println(demo.getCount()); // 输出2000(正确)
}
}
优点:实现简单,无需手动释放锁(JVM自动处理);缺点:锁粒度较粗(若同步块内逻辑复杂,性能会下降)。
(2)使用
Lock接口(如
ReentrantLock)
Lock
ReentrantLock
原理:通过手动获取/释放锁,逻辑与
Lock类似,但支持更灵活的锁控制(如中断、超时)。代码示例:
synchronized
public class LockAdd {
private int count = 0;
private final Lock lock = new ReentrantLock(); // 创建锁对象
public void add() {
lock.lock(); // 手动获取锁(必须在try前,避免异常导致锁未获取)
try {
count++; // 原子化操作
} finally {
lock.unlock(); // 手动释放锁(必须在finally中,避免锁泄漏)
}
}
// 测试逻辑同synchronized,最终count=2000
}
优点:支持中断、超时获取锁,避免死锁;缺点:需手动管理锁的释放,代码稍复杂(忘记会导致锁泄漏)。
unlock()
(3)使用原子类(如
AtomicInteger)
AtomicInteger
原理:JUC()包下的原子类,通过CAS(Compare and Swap,比较并交换) 操作实现原子化修改,无需加锁(无锁并发)。
java.util.concurrent
CAS逻辑:每次修改前,先比较当前值与预期值是否一致,一致则修改为新值,不一致则重试(自旋),直到成功。代码示例:
public class AtomicAdd {
// 原子类,初始值0
private final AtomicInteger count = new AtomicInteger(0);
public void add() {
count.incrementAndGet(); // 原子化的count++(底层CAS实现)
}
public int getCount() {
return count.get();
}
// 测试逻辑同前,最终count=2000
}
优点:无锁操作,并发性能高于/
synchronized(避免线程上下文切换);缺点:仅支持简单原子操作(如加减、赋值),复杂逻辑需结合其他工具。
Lock
(4)避免共享变量(线程私有计算+结果合并)
原理:让每个线程操作“私有变量”,而非共享变量,最后汇总所有线程的私有变量结果(完全避免并发修改)。代码示例(用存储线程私有变量):
ThreadLocal
public class ThreadLocalAdd {
// ThreadLocal:每个线程存储自己的count(初始值0)
private final ThreadLocal<Integer> threadLocalCount = ThreadLocal.withInitial(() -> 0);
public void add() {
// 获取当前线程的私有count,加1后重新存储
threadLocalCount.set(threadLocalCount.get() + 1);
}
// 汇总所有线程的count(需手动收集,ThreadLocal不支持全局汇总)
public int getTotalCount(List<Thread> threads) {
int total = 0;
for (Thread thread : threads) {
// 此处需通过线程内的逻辑暴露私有count(示例简化,实际需更合理的设计)
total += threadLocalCount.get();
}
return total;
}
// 测试:2个线程各加1000次,汇总结果
public static void main(String[] args) throws InterruptedException {
ThreadLocalAdd demo = new ThreadLocalAdd();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 2; i++) {
Thread t = new Thread(() -> {
for (int j = 0; j < 1000; j++) demo.add();
});
threads.add(t);
t.start();
}
for (Thread t : threads) t.join();
System.out.println(demo.getTotalCount(threads)); // 输出2000(正确)
}
}
优点:无锁、无并发冲突,性能极高;缺点:仅适用于“可拆分计算”场景,需手动汇总结果(复杂场景需用)。
ForkJoinPool
(5)使用线程池+
Future(任务结果异步汇总)
Future
原理:将“加法任务”提交到线程池,每个任务返回计算结果(如单个线程加1000次的结果),最后通过获取所有任务结果并汇总。代码示例:
Future
public class ThreadPoolAdd {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2); // 2个线程的线程池
List<Future<Integer>> futures = new ArrayList<>();
// 提交2个任务,每个任务计算“1000次加法”的结果(1000)
for (int i = 0; i < 2; i++) {
Future<Integer> future = pool.submit(() -> {
int subCount = 0;
for (int j = 0; j < 1000; j++) subCount++;
return subCount;
});
futures.add(future);
}
// 汇总结果
int total = 0;
for (Future<Integer> future : futures) {
total += future.get(); // 阻塞获取任务结果
}
pool.shutdown(); // 关闭线程池
System.out.println(total); // 输出2000(正确)
}
}
优点:无需手动管理线程,结果汇总清晰,适合多任务并发计算;缺点:会阻塞,复杂场景需结合
future.get()实现非阻塞汇总。
CompletableFuture
16. 什么是自旋锁?适用场景和优缺点是什么?
答:#### (1)定义与核心原理
自旋锁(Spin Lock)是一种无阻塞锁:当线程尝试获取锁失败时,不会立即阻塞(放弃CPU),而是通过“循环重试”(自旋)的方式不断检查锁是否释放,直到获取到锁为止。
底层依赖CAS操作:每次自旋时,用CAS判断锁的状态(如表示未锁定,
state=0表示已锁定),若为
state=1则尝试将其改为
0(获取锁),否则继续自旋。
1
(2)简单实现示例
public class SpinLock {
// 用AtomicReference存储“持有锁的线程”(null表示未锁定)
private final AtomicReference<Thread> owner = new AtomicReference<>();
// 获取锁:自旋重试,直到成功
public void lock() {
Thread currentThread = Thread.currentThread();
// CAS:若owner为null(未锁定),则将其设为当前线程(获取锁)
while (!owner.compareAndSet(null, currentThread)) {
// 自旋:空循环,不断重试(可加入Thread.yield()减少CPU占用)
Thread.yield(); // 让出CPU给其他线程,降低自旋开销
}
}
// 释放锁:将owner设为null
public void unlock() {
Thread currentThread = Thread.currentThread();
// 仅允许持有锁的线程释放(避免其他线程误释放)
owner.compareAndSet(currentThread, null);
}
}
(3)适用场景
自旋锁的核心优势是“避免线程上下文切换”(阻塞/唤醒线程会触发上下文切换,开销较大),因此适用于:
锁持有时间极短的场景(如简单的原子操作、短同步块):自旋时间远小于上下文切换时间,总体性能更优;并发度不高的场景:若并发度高,大量线程自旋会导致CPU资源浪费(“自旋风暴”);单核CPU不适用:单核CPU下,自旋线程会占用CPU,导致持有锁的线程无法执行,最终自旋无意义(多核CPU可让自旋线程和持有锁线程在不同核心执行)。
(4)优缺点
| 优点 | 缺点 |
|---|---|
| 无线程上下文切换开销:获取锁快,适合短时间锁持有场景 | 自旋会占用CPU资源:若锁持有时间长,自旋线程会空耗CPU(可通过“自适应自旋”优化) |
| 实现简单:基于CAS,无需依赖操作系统的阻塞机制 | 可能导致“饥饿”:若大量线程自旋,新线程获取锁的概率低(需结合公平锁逻辑优化) |
| 无死锁风险(理论上):只要锁最终会释放,自旋线程迟早能获取锁 | 单核CPU下性能差:自旋会阻塞持有锁的线程执行 |
(5)JDK中的自旋锁应用
JDK中许多并发组件底层使用自旋锁,例如:
:默认是非公平锁,获取锁时会先自旋重试几次,失败后再加入等待队列;
ReentrantLock:
AtomicInteger等方法底层通过CAS自旋实现原子操作;内核级优化:JVM的“自适应自旋”(根据历史自旋成功率动态调整自旋次数,成功率高则增加次数,低则减少)。
incrementAndGet()
17. ThreadLocal的原理是什么?使用时需注意哪些问题(如内存泄漏)?
答:#### (1)核心原理:线程私有存储
ThreadLocal的作用是“为每个线程提供独立的变量副本”,让线程操作自己的私有变量,避免共享变量的并发冲突。其底层依赖类中的
Thread实现,原理可概括为“3个核心组件”:
ThreadLocalMap
Thread类:每个对象内部维护一个
Thread(成员变量
ThreadLocalMap,初始为null);ThreadLocalMap:线程私有的哈希表,key是
threadLocals实例(弱引用),value是线程的私有变量值(强引用);ThreadLocal类:作为“工具类”,提供
ThreadLocal/
get()/
set()方法,本质是操作当前线程的
remove()。
ThreadLocalMap
原理流程(以
ThreadLocal.set(T value)为例):
ThreadLocal.set(T value)
获取当前线程:获取线程的
Thread currentThread = Thread.currentThread();:
ThreadLocalMap若
ThreadLocalMap map = currentThread.threadLocals;为null,创建新的
map并赋值给线程;若
ThreadLocalMap不为null,以当前
map实例为key,将
ThreadLocal存入
value(覆盖已有值);
map方法则相反:通过当前线程的
ThreadLocal.get(),以自身为key获取value,若不存在则调用
ThreadLocalMap初始化(默认返回null)。
initialValue()
(2)关键设计:弱引用的key
的key(
ThreadLocalMap实例)使用弱引用(
ThreadLocal),原因是:
WeakReference
若key是强引用:当外部不再使用实例(如
ThreadLocal)时,
tl = null仍持有强引用,导致
ThreadLocalMap实例无法被GC回收,引发内存泄漏;若key是弱引用:当外部
ThreadLocal实例被回收(
ThreadLocal)时,弱引用的key会被GC标记为可回收,后续
tl = null清理时可删除对应的key-value对。
ThreadLocalMap
(3)使用时需注意的问题
① 内存泄漏(最核心问题)
泄漏原因:虽然key是弱引用,但的value是强引用。若线程长期存活(如线程池的核心线程),且未调用
ThreadLocalMap,则:
ThreadLocal.remove()
外部实例被回收(key变为null);value的强引用仍被
ThreadLocal持有,且线程不结束,value无法被GC回收;最终导致“key为null的value堆积”,引发内存泄漏。
ThreadLocalMap
解决方案:
每次使用完后,必须调用
ThreadLocal方法删除value(建议在
remove()中调用,避免异常导致未执行);避免在长期存活的线程(如线程池核心线程)中使用
finally,或确保线程退出前清理
ThreadLocal;JDK优化:
ThreadLocal的
ThreadLocalMap/
get()方法会自动清理“key为null的value”,但无法覆盖所有场景(如长期不调用
set()/
get()的线程)。
set()
② 线程池中的“线程复用”问题
线程池的核心线程会复用,若前一个任务使用后未
ThreadLocal,下一个任务复用该线程时,会读取到前一个任务的value(脏数据)。
remove()
示例:
ThreadLocal<String> tl = new ThreadLocal<>();
ExecutorService pool = Executors.newFixedThreadPool(1); // 核心线程数1,复用线程
// 任务1:设置tl的值为"task1",未remove()
pool.submit(() -> {
tl.set("task1");
System.out.println("任务1的tl值:" + tl.get()); // 输出task1
});
// 任务2:未设置tl,却读取到任务1的value
pool.submit(() -> {
System.out.println("任务2的tl值:" + tl.get()); // 输出task1(脏数据)
});
解决方案:任务执行完毕后,在中调用
finally,清除当前线程的value。
tl.remove()
③ 无法跨线程共享数据
ThreadLocal的变量是“线程私有”的,子线程无法读取父线程的值(如主线程设置
ThreadLocal,子线程
tl.set("main")返回null)。
tl.get()
解决方案:若需跨线程共享(如父子线程),使用(继承
InheritableThreadLocal,会将父线程的
ThreadLocal复制到子线程),但需注意“复制时机”(子线程创建时复制,后续父线程修改不影响子线程)。
ThreadLocalMap
④ 初始值的初始化方式
的默认初始值为null,若需自定义初始值,有两种方式:
ThreadLocal
重写方法(JDK8前):
initialValue()
ThreadLocal<Integer> tl = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0; // 初始值为0
}
};
使用静态方法(JDK8+,更简洁):
ThreadLocal.withInitial()
ThreadLocal<Integer> tl = ThreadLocal.withInitial(() -> 0);
18. 线程池的核心参数有哪些?线程池的阻塞队列有哪些实现方式及特点?
答:线程池是“管理线程的容器”,通过复用线程减少线程创建/销毁的开销,核心是“核心参数”和“阻塞队列”的配合,实现对线程的动态管理。
(1)线程池的核心参数(7个,基于
ThreadPoolExecutor)
ThreadPoolExecutor
的构造方法定义了7个核心参数,决定线程池的行为:
ThreadPoolExecutor
public ThreadPoolExecutor(
int corePoolSize, // 1.核心线程数
int maximumPoolSize, // 2.最大线程数
long keepAliveTime, // 3.空闲线程存活时间
TimeUnit unit, // 4.时间单位
BlockingQueue<Runnable> workQueue, // 5.阻塞队列
ThreadFactory threadFactory, // 6.线程工厂
RejectedExecutionHandler handler // 7.拒绝策略
) { ... }
各参数的作用:
核心线程数(corePoolSize):
线程池长期保持的线程数量(即使线程空闲,也不会销毁,除非设置);任务提交时,若核心线程未满,直接创建核心线程执行任务;若核心线程已满,将任务加入阻塞队列。
allowCoreThreadTimeOut=true
最大线程数(maximumPoolSize):
线程池允许创建的最大线程数(核心线程数 + 非核心线程数);若阻塞队列已满,且当前线程数 < 最大线程数,创建非核心线程执行任务;若达到最大线程数,触发拒绝策略。
空闲线程存活时间(keepAliveTime):
非核心线程空闲后的最大存活时间(超过时间则销毁非核心线程);若设置,核心线程也会遵循此存活时间(空闲时销毁)。
allowCoreThreadTimeOut(true)
时间单位(unit):
的时间单位(如
keepAliveTime、
TimeUnit.SECONDS)。
TimeUnit.MILLISECONDS
阻塞队列(workQueue):
用于存储“核心线程已满”时提交的任务,等待线程空闲后执行;队列的类型和容量决定线程池的任务缓冲能力(见下文详细说明)。
线程工厂(threadFactory):
用于创建线程的工厂(默认是);可自定义线程工厂,设置线程名称(如“pool-1-thread-1”)、优先级、是否为守护线程等(便于日志排查)。
Executors.defaultThreadFactory()
拒绝策略(handler):
当“阻塞队列已满 + 线程数达到最大线程数”时,对新提交的任务采取的处理策略(默认是,抛出
AbortPolicy);JDK提供4种默认策略:
RejectedExecutionException
:直接抛出异常(默认,推荐用于需明确感知任务拒绝的场景);
AbortPolicy:由提交任务的线程(如主线程)自己执行任务(避免任务丢失,适合并发度低的场景);
CallerRunsPolicy:直接丢弃任务(不抛异常,适合任务可丢失的场景);
DiscardPolicy:丢弃阻塞队列中最旧的任务(队列头部任务),然后提交新任务(适合任务时效性要求高的场景)。
DiscardOldestPolicy
(2)线程池的阻塞队列实现方式及特点
阻塞队列()是线程池的“任务缓冲池”,需实现“线程安全的入队/出队”,且支持“阻塞特性”(队列空时,出队线程阻塞;队列满时,入队线程阻塞)。JDK提供5种常用阻塞队列实现,适用于不同场景:
BlockingQueue
| 队列类型 | 底层结构 | 核心特点 | 适用场景 |
|---|---|---|---|
|
有界数组 | 1. 必须指定容量(无界构造器不存在); 2. FIFO(先进先出); 3. 支持公平/非公平锁(构造器指定 参数)。 |
适合“任务量固定、需控制队列容量”的场景(如核心业务任务,避免队列无限堆积导致OOM)。 |
|
链表(节点) | 1. 默认无界(容量为),可指定容量(推荐显式指定,避免OOM);2. FIFO; 3. 入队/出队用两把锁( 和),并发性能高于。 |
适合“任务量波动大,但需避免OOM”的场景(显式指定容量,如);默认使用此队列(无界,需谨慎)。 |
|
无存储节点 | 1. 容量为0(不存储任务,仅作为“任务传递通道”); 2. 入队操作( )必须等待出队操作(),反之亦然(“同步移交”);3. 支持公平/非公平模式。 |
适合“任务需立即执行,不允许缓冲”的场景(如,非核心线程空闲60秒销毁,避免线程堆积)。 |
|
优先级堆(数组) | 1. 无界队列(容量可指定,但满时自动扩容); 2. 任务按优先级排序(默认自然排序,或自定义 );3. 优先级高的任务先执行(不保证FIFO)。 |
适合“任务有优先级差异”的场景(如紧急任务优先执行,如订单处理中的“VIP订单”)。 |
|
优先级堆 | 1. 无界队列; 2. 任务必须实现 接口(重写方法,指定延迟时间);3. 只有任务“延迟时间到期”后,才能被出队执行。 |
适合“定时任务”场景(如定时清理缓存、定时发送消息,如底层使用此队列)。 |
(3)核心参数与阻塞队列的配合逻辑(任务提交流程)
任务提交到线程池;若当前核心线程数 < :创建核心线程执行任务;若核心线程已满:将任务加入阻塞队列;若阻塞队列已满,且当前线程数 <
corePoolSize:创建非核心线程执行任务;若线程数达到
maximumPoolSize:触发拒绝策略;非核心线程执行完任务后,若空闲时间超过
maximumPoolSize:销毁非核心线程。
keepAliveTime
19. AQS(AbstractQueuedSynchronizer)的核心原理是什么?在哪些并发工具中被使用?
答:AQS是JUC包的“同步器基石”,定义了一套“基于状态和队列的同步框架”,几乎所有JUC并发工具(如、
ReentrantLock)都基于AQS实现。其核心是“用CAS管理同步状态,用双向链表管理等待线程”。
CountDownLatch
(1)AQS的核心组成
AQS的底层由“2个核心部分”构成,可概括为“一个状态 + 一个队列”:
同步状态(state):
用修饰的变量,存储同步状态(如锁的持有次数、计数器值);线程通过
volatile int state、
getState()、
setState(int newState)(CAS)操作状态,确保线程安全;不同同步工具对
compareAndSetState(int expect, int update)的定义不同(如
state中,
ReentrantLock表示未锁定,
state=0表示锁定次数;
state>0中,
CountDownLatch表示计数器值)。
state
双向同步队列(CLH队列):
全称“Craig, Landin, and Hagersten队列”,是一个双向链表,用于存储“获取同步状态失败的线程”;每个节点()包含:
Node
:当前等待的线程;
thread/
prev:前驱/后继节点,构成双向链表;
next:节点状态(如
waitStatus=初始态、
0=-1=当前节点的后继节点需被唤醒、
SIGNAL=1=节点已取消等待);
CANCELLED
队列特性:FIFO(先进先出),确保线程等待的公平性(默认非公平,可配置为公平)。
条件队列(Condition Queue):
由实现(AQS的内部类),用于“线程等待特定条件”(如
ConditionObject的
ReentrantLock/
await());每个
signal()对应一个单向条件队列,当线程调用
Condition时,从同步队列转移到条件队列;调用
await()时,从条件队列转移回同步队列,重新竞争同步状态。
signal()
(2)AQS的核心逻辑:获取与释放同步状态
AQS定义了模板方法(如、
acquire(int arg)),子类需重写“钩子方法”(如
release(int arg)、
tryAcquire(int arg))实现具体的同步逻辑。核心流程分为“获取状态”和“释放状态”:
tryRelease(int arg)
① 获取同步状态(以独占锁为例,如
ReentrantLock)
ReentrantLock
线程调用(
acquire(1)表示获取1个单位的状态);调用子类重写的
arg=1:尝试用CAS修改
tryAcquire(1)(如
state→
state=0),若成功则获取锁,直接返回;若失败则进入下一步;调用
1:创建独占模式的
addWaiter(Node.EXCLUSIVE),将当前线程封装为节点,加入同步队列的尾部(CAS确保线程安全);调用
Node:节点在同步队列中“自旋+阻塞”,不断检查前驱节点是否为“头节点”(头节点是已获取锁的线程):
acquireQueued(Node node, int arg)
若前驱是头节点,再次调用,成功则将当前节点设为头节点(出队),返回;若前驱不是头节点,或
tryAcquire(1)失败,根据
tryAcquire调整节点状态,调用
waitStatus阻塞当前线程;
LockSupport.park(this)
若线程被唤醒(如其他线程释放锁),重复步骤4,直到获取状态成功或线程被中断。
② 释放同步状态(以独占锁为例)
线程调用;调用子类重写的
release(1):尝试修改
tryRelease(1)(如
state→
state=1),若成功则进入下一步;若失败则返回
0;获取同步队列的头节点,若头节点的
false为
waitStatus(表示后继节点需唤醒):
SIGNAL
调用唤醒头节点的后继节点;将头节点的
LockSupport.unpark(node.thread)设为
waitStatus(初始态);
0
返回,释放状态成功。
true
(3)AQS的两种模式
AQS支持“独占模式”和“共享模式”,适配不同同步场景:
| 模式 | 核心特点 | 适用场景 |
|---|---|---|
| 独占模式(Exclusive) | 同一时间只有一个线程能获取同步状态(如锁),其他线程需等待。 | 、(底层类似) |
| 共享模式(Shared) | 同一时间多个线程可获取同步状态(如计数器),线程获取后不排斥其他线程。 | 、、(读锁) |
(4)基于AQS实现的JUC并发工具
几乎所有JUC工具都依赖AQS,核心工具及AQS的使用方式如下:
| 并发工具 | 模式 | 的含义 |
核心钩子方法实现 |
|---|---|---|---|
(重入锁) |
独占模式 | 锁的持有次数(=未锁,=已锁次) |
:CAS将从→(非公平)或检查队列(公平);:减1,为时释放锁。 |
(倒计时器) |
共享模式 | 计数器值(=计数完成,=剩余次) |
:若则返回(获取成功),否则返回(加入队列);:CAS将减1,为时唤醒所有等待线程。 |
(信号量) |
共享模式 | 可用信号量数量(=无可用,=个可用) |
:CAS将减,若结果≥则获取成功;:CAS将加,唤醒等待线程。 |
(读写锁) |
读锁(共享)、写锁(独占) | 高16位=读锁次数,低16位=写锁次数 | 写锁:检查读锁是否为且写锁为,CAS修改低16位;读锁:检查写锁是否为,CAS修改高16位。 |
(循环屏障) |
共享模式(间接依赖) | 无直接,内部用+实现 |
依赖AQS的队列,线程调用时加入条件队列,直到所有线程到达屏障点后唤醒。 |
(5)AQS的核心优势
标准化同步框架:封装了“线程排队、阻塞/唤醒、CAS操作”等通用逻辑,子类只需重写少量钩子方法,即可实现复杂同步工具(避免重复开发);线程安全:基于和CAS保证同步状态的线程安全,基于双向链表和
volatile保证线程排队的正确性;灵活适配:支持独占/共享模式,可自定义公平/非公平策略,适配不同同步场景(锁、计数器、信号量等)。
LockSupport
以上内容覆盖了第15-19题的详细解答,后续可继续推进剩余面试问题(如消息幂等、JVM内存区域等)。若你对某类问题(如JVM、MySQL、Spring)有优先了解需求,或希望调整答案的详细程度,可随时告知。
20. 什么是消息幂等性?分布式系统中如何保证消息幂等消费?
答:#### (1)消息幂等性的定义
消息幂等性是指“同一条消息被多次消费时,最终结果与消费一次完全一致”,即重复消费不会对业务产生副作用(如重复下单、重复扣款)。
在分布式系统中,消息重复是常态(如网络重试、消息中间件故障重试),因此必须保证幂等性,否则会导致数据不一致。
(2)消息重复的常见原因
生产者重试:生产者发送消息后未收到确认(如网络超时),触发重试机制,导致消息重复发送;消费者重试:消费者消费消息后,向中间件发送确认(ACK)失败,中间件认为消息未消费,再次投递;中间件故障:消息中间件(如RocketMQ、Kafka)在故障恢复时,可能重复投递未持久化的消息。
(3)保证消息幂等消费的核心方案
保证幂等的核心是“识别重复消息,并对重复消息执行“过滤”或“等效处理””,常用方案如下:
① 唯一ID + 幂等表(最通用方案)
原理:
生产者发送消息时,生成全局唯一ID(如UUID、雪花算法ID),放入消息头;消费者接收消息后,先查询“幂等表”(数据库表,主键为消息唯一ID):
若ID不存在:执行业务逻辑,执行成功后将ID插入幂等表(标记为已消费);若ID已存在:直接返回成功(忽略重复消息)。
示例(伪代码):
// 消息结构:包含唯一消息ID
class Message {
String msgId; // 全局唯一ID
String content; // 业务内容
}
// 消费者处理逻辑
public void consume(Message msg) {
// 1. 查询幂等表
if (idempotentMapper.exists(msg.getMsgId())) {
log.info("消息已消费,忽略:{}", msg.getMsgId());
return;
}
// 2. 执行业务逻辑(如扣减库存、创建订单)
boolean success = businessService.process(msg.getContent());
// 3. 业务成功后,插入幂等表(需在同一事务中)
if (success) {
idempotentMapper.insert(msg.getMsgId());
}
}
关键点:
幂等表的插入需与业务逻辑在同一数据库事务中(确保业务成功则ID必被记录,避免漏记);唯一ID的生成需保证全局唯一性(如结合业务ID+时间戳+机器标识)。
② 基于业务唯一标识去重
原理:若消息对应“天然唯一的业务标识”(如订单号、支付流水号),可直接用业务标识代替消息ID进行去重,无需单独的幂等表。适用场景:业务操作本身依赖唯一标识(如“根据订单号支付”,订单号唯一)。示例:
消费者处理“支付消息”时,以订单号作为唯一标识,查询订单状态:
若订单未支付:执行支付逻辑,更新订单状态为“已支付”;若订单已支付:直接返回成功(忽略重复消息)。
③ 状态机校验(基于业务状态流转)
原理:业务对象存在明确的状态流转(如订单:待支付→支付中→已支付→已完成),重复消息到达时,通过状态机判断当前状态是否允许执行操作:
若允许(如“待支付”→“支付中”):执行操作并更新状态;若不允许(如“已支付”→“支付中”):忽略重复消息。
示例:
订单状态为“已支付”时,再次收到“支付消息”,状态机校验不通过,直接返回成功。
④ 分布式锁(适用于无唯一标识场景)
原理:消费者接收消息后,先获取“分布式锁”(如Redis的),锁的key为“消息ID或业务唯一标识”:
SET NX
若获取锁成功:执行业务逻辑,释放锁;若获取锁失败:说明有其他线程正在处理该消息,当前线程直接返回成功(等待其他线程处理结果)。
注意:需设置锁的过期时间(避免死锁),且业务逻辑执行时间需小于锁过期时间(或实现锁续期)。
⑤ 消息中间件自身机制(辅助方案)
部分消息中间件提供幂等相关特性,可配合使用:
RocketMQ:支持“事务消息”和“消息重试次数限制”,避免无限重试;Kafka:通过机制确保消息有序消费,结合消费者组
offset避免重复消费(需正确提交
group.id);RabbitMQ:通过
offset和
messageId标记消息,结合死信队列处理消费失败的消息。
correlationId
(4)总结
优先使用“唯一ID + 幂等表”(通用性最强,适合大多数场景);有天然业务唯一标识时,用“业务标识去重”(减少表设计);业务状态明确时,用“状态机校验”(逻辑更贴合业务);分布式锁作为补充方案,需注意锁的有效性和性能。
21. JVM的运行时内存区域包含哪些部分?哪个区域不会发生内存溢出?
答:JVM运行时内存区域是Java程序执行时内存分配和管理的区域,根据《Java虚拟机规范》(Java SE 8),分为5个核心区域,各自有明确的功能和生命周期。
(1)运行时内存区域及功能
程序计数器(Program Counter Register)
功能:记录当前线程执行的字节码指令地址(如分支、循环、跳转、异常处理等);特点:
线程私有(每个线程有独立的程序计数器,互不干扰);是JVM中唯一不会发生OutOfMemoryError的区域(内存占用极小,由JVM直接管理,无需手动分配);若线程执行的是Native方法(本地方法),计数器值为(Native方法不通过字节码执行)。
undefined
Java虚拟机栈(Java Virtual Machine Stacks)
功能:存储线程执行方法时的“栈帧”(Stack Frame),每个栈帧包含:
局部变量表(方法内的局部变量,如基本类型、对象引用);操作数栈(方法执行时的临时数据栈);动态链接(指向运行时常量池的方法引用);方法返回地址(方法执行完后回到的调用位置)。
特点:
线程私有(与线程生命周期一致);栈深度有限制(默认1MB左右),若方法调用层级过深(如递归无终止条件),会抛出;若JVM无法为新的栈帧分配内存(如线程数过多导致总栈内存超过上限),会抛出
StackOverflowError。
OutOfMemoryError
本地方法栈(Native Method Stacks)
功能:与虚拟机栈类似,但专门为Native方法(如C/C++实现的方法)服务;特点:
线程私有;可能抛出(栈深度超限)和
StackOverflowError(内存分配失败);具体实现由JVM厂商决定(如HotSpot虚拟机将本地方法栈与虚拟机栈合并实现)。
OutOfMemoryError
Java堆(Java Heap)
功能:存储所有对象实例和数组(是Java内存管理的核心区域);特点:
线程共享(所有线程可访问堆中的对象);是垃圾收集器(GC)的主要工作区域(“GC堆”);内存分配动态扩展(通过和
-Xms参数设置初始和最大堆大小);若堆内存不足(无法为新对象分配空间,且GC后仍无足够空间),会抛出
-Xmx。
OutOfMemoryError: Java heap space
方法区(Method Area)
功能:存储已被JVM加载的类信息(类名、字段、方法、接口)、常量、静态变量、即时编译器(JIT)编译后的代码等;特点:
线程共享;Java 8前,方法区的实现为“永久代”(PermGen),受和
-XX:PermSize限制;Java 8及后,永久代被“元空间(Metaspace)”替代,元空间直接使用本地内存(Native Memory),默认无上限(可通过
-XX:MaxPermSize和
-XX:MetaspaceSize限制);若方法区无法分配内存(如加载过多类、常量池过大),会抛出
-XX:MaxMetaspaceSize(Java 8+)或
OutOfMemoryError: Metaspace(Java 7及前)。
OutOfMemoryError: PermGen space
(2)不会发生内存溢出的区域
程序计数器是唯一不会发生的区域。原因是:
OutOfMemoryError
程序计数器的内存空间极小(仅存储指令地址),由JVM直接管理,无需像堆、栈那样动态分配内存;其生命周期与线程一致,线程结束后计数器内存自动释放,不存在内存分配失败的场景。
(3)总结
| 内存区域 | 线程共享性 | 主要存储内容 | 可能的异常 |
|---|---|---|---|
| 程序计数器 | 私有 | 字节码指令地址 | 无(唯一无OOM的区域) |
| 虚拟机栈 | 私有 | 栈帧(局部变量、操作数栈等) | StackOverflowError、OutOfMemoryError |
| 本地方法栈 | 私有 | Native方法的栈帧 | StackOverflowError、OutOfMemoryError |
| Java堆 | 共享 | 对象实例、数组 | OutOfMemoryError: Java heap space |
| 方法区(元空间) | 共享 | 类信息、常量、静态变量等 | OutOfMemoryError: Metaspace |
22. 如何判断对象是垃圾(可回收)?常用的垃圾回收算法有哪些?
答:在JVM中,“垃圾”指“不再被任何存活线程引用的对象”,判断对象是否为垃圾是GC(垃圾回收)的前提,而垃圾回收算法则是回收这些垃圾的具体实现逻辑。
(1)判断对象是否为垃圾(可达性分析算法)
JVM采用“可达性分析”(Reachability Analysis)判断对象是否可回收,核心原理如下:
定义GCRoots(GC根节点):作为可达性分析的起点,是“绝对存活”的对象,包括:
虚拟机栈中引用的对象(如方法局部变量);本地方法栈中Native方法引用的对象;方法区中类静态变量引用的对象;方法区中常量引用的对象(如的常量);活跃线程(如当前运行的线程对象)。
String.intern()
可达性判断:从GCRoots出发,沿引用链(对象之间的引用关系)遍历,所有能被遍历到的对象是“存活对象”,未被遍历到的对象是“垃圾对象”(暂时标记为可回收)。
两次标记过程:
第一次标记:未被GCRoots可达的对象被标记为“待回收”;筛选:检查对象是否重写了方法,或
finalize()已被执行过:
finalize()
若未重写或已执行:直接判定为垃圾;若未执行:将对象放入队列,由虚拟机自动创建的
F-Queue线程执行
Finalizer方法;
finalize()
第二次标记:中的对象若在
F-Queue中重新建立与GCRoots的引用(如
finalize()赋值给某存活变量),则移除“待回收”标记,否则最终判定为垃圾。
this
注意:方法最多被执行一次,且执行时机不确定,不建议依赖该方法进行资源释放(推荐用
finalize())。
try-finally
(2)常用的垃圾回收算法
垃圾回收算法的核心是“如何高效回收垃圾对象,并减少对程序执行的影响”,常用算法如下:
标记-清除算法(Mark-Sweep)
步骤:
① 标记:通过可达性分析标记所有垃圾对象;
② 清除:直接回收垃圾对象占用的内存空间(标记为“空闲”,放入空闲列表)。优点:实现简单,无需移动对象;缺点:
产生内存碎片(回收后空闲内存分散,无法分配大对象,可能提前触发GC);标记和清除过程效率低(需遍历所有对象)。
适用场景:对内存碎片不敏感的场景(如老年代,对象存活率高,移动成本大)。
复制算法(Copying)
步骤:
① 将内存分为大小相等的两块(如From区和To区);
② 只使用From区,当From区满时,标记存活对象并复制到To区(按顺序排列,无碎片);
③ 清空From区,交换From和To的角色(下次使用新的From区)。优点:
无内存碎片(存活对象连续排列);回收效率高(只需复制存活对象,无需处理垃圾)。
缺点:
内存利用率低(仅使用一半内存);存活对象多时,复制成本高。
优化与应用:
实际中采用“** Eden区 + 两个Survivor区**”(比例8:1:1):Eden区占80%,两个Survivor区各占10%,总利用率90%;适用于新生代(对象存活率低,复制成本小),如Serial GC、ParNew GC的新生代。
标记-整理算法(Mark-Compact)
步骤:
① 标记:同标记-清除算法,标记垃圾对象;
② 整理:将所有存活对象向内存一端移动,然后直接清除边界外的所有垃圾(释放连续内存)。优点:无内存碎片,内存利用率100%;缺点:整理阶段需要移动对象(涉及引用更新,成本高);适用场景:老年代(对象存活率高,需避免内存碎片,如Serial Old GC、Parallel Old GC)。
分代收集算法(Generational Collection)
核心思想:根据对象的“存活周期”将内存划分为不同区域(新生代、老年代),对不同区域采用不同回收算法(结合上述算法的优势):
新生代:对象存活时间短(朝生夕死),用复制算法(效率高,适合频繁回收);老年代:对象存活时间长(存活次数超过阈值),用标记-清除或标记-整理算法(避免频繁移动对象)。
应用:所有现代GC(如G1、ZGC、Shenandoah)均基于分代收集思想优化(部分GC弱化分代,但核心逻辑类似)。
分区收集算法(Region-Based)
核心思想:将堆内存划分为多个大小相等的“Region”(区域),每个Region独立回收(可并行或并发),避免全堆扫描;优点:可控制单次GC的停顿时间(只回收部分Region);应用:G1 GC(将堆分为2048个Region,优先回收垃圾多的Region)、ZGC、Shenandoah GC。
(3)总结
对象判定:统一用“可达性分析”,以GCRoots为起点判断对象是否可达;回收算法:根据对象特性选择:
新生代→复制算法;老年代→标记-清除/标记-整理;现代GC→分代+分区结合(平衡效率与停顿)。
23. 什么是GCRoots?如何确定GCRoots与对象的引用关系?
答:GCRoots(GC根节点)是JVM进行“可达性分析”的起点,是判断对象是否存活的核心依据。明确GCRoots的定义及引用关系判定方式,是理解垃圾回收的基础。
(1)GCRoots的定义与类型
GCRoots是指“在当前虚拟机中,绝对不会被回收的对象”,这些对象作为可达性分析的起点,从它们出发的引用链所涉及的对象均被视为“存活对象”。
JVM中GCRoots的具体类型包括:
虚拟机栈(栈帧中的局部变量表)引用的对象
例如:方法中的局部变量(,
User user = new User()引用的
user对象)、方法参数等;这些对象随方法执行而存在,方法结束后引用消失(可能成为垃圾)。
User
本地方法栈中Native方法引用的对象
由Native方法(如C/C++实现)创建或引用的对象(如内部引用的对象);与虚拟机栈类似,但属于本地方法的执行上下文。
System.currentTimeMillis()
方法区中类静态变量引用的对象
类的静态成员变量(,
static User user = new User()引用的对象);生命周期与类一致(类卸载前,该对象始终被GCRoots引用)。
user
方法区中常量引用的对象
编译期确定的常量(如,
public static final String CONST = "abc"引用的字符串对象);常量一旦初始化,引用关系不会改变,始终被视为GCRoots。
CONST
活跃线程对象(Thread实例)
正在运行的线程(对象)及其关联的资源(如线程的栈、上下文);线程未终止前,始终作为GCRoots(否则线程相关资源可能被误回收)。
Thread
JNI(Java Native Interface)引用的对象
由JNI接口创建的全局引用对象(如通过创建的对象);这类对象需显式释放,否则会一直作为GCRoots存在(可能导致内存泄漏)。
NewGlobalRef
(2)GCRoots与对象引用关系的确定方式
JVM通过“遍历对象引用链”确定GCRoots与对象的关系,具体流程如下:
收集GCRoots集合
GC触发时,JVM首先遍历上述GCRoots类型(如虚拟机栈的局部变量、静态变量等),收集所有GCRoots对象,形成初始根集合。
标记可达对象
从GCRoots出发,沿对象的“引用字段”(如对象的成员变量)递归遍历:
若对象A被GCRoots直接引用,则A是可达的;若对象B被A引用,则B是可达的;以此类推,所有被间接引用的对象均标记为“可达”(存活)。
遍历方式:采用“深度优先”或“广度优先”算法,确保所有可达对象被标记。
处理引用类型
Java中的引用分为4种(强引用、软引用、弱引用、虚引用),GCRoots对对象的引用类型会影响可达性判断:
强引用(如):GCRoots的强引用对象一定是可达的,不会被回收;软引用(
User u = new User()):内存不足时,软引用对象可能被回收(GCRoots的软引用在内存充足时视为可达);弱引用(
SoftReference<User>):无论内存是否充足,下次GC时弱引用对象会被回收(GCRoots的弱引用不视为可达);虚引用(
WeakReference<User>):仅用于跟踪对象回收,不影响可达性。
PhantomReference<User>
(3)实际应用中的注意事项
内存泄漏排查:若对象长期无法被回收(内存泄漏),常因“无意识的GCRoots引用”(如静态集合缓存未及时清理,中的对象始终被GCRoots引用)。GC日志分析:通过
static List<User> list等参数输出GC日志,可观察GCRoots数量及可达对象占比,辅助优化内存使用。引用类型选择:根据业务场景选择引用类型(如缓存用软引用,临时数据用弱引用),避免不必要的GCRoots强引用导致内存浪费。
-XX:+PrintGCDetails
24. StackOverflowError和OutOfMemoryError的产生场景是什么?如何排查解决?
答:和
StackOverflowError是JVM中两种常见的致命错误,均与内存管理相关,但产生原因和场景不同,排查和解决方式也有差异。
OutOfMemoryError
(1)StackOverflowError(栈溢出)
定义:当虚拟机栈(或本地方法栈)的栈深度超过JVM允许的最大值时抛出,属于“栈内存不足”的错误。
产生场景:
无限递归调用:方法递归调用时未设置终止条件,导致栈帧(每个递归调用生成一个栈帧)不断叠加,超过栈深度限制(如);方法调用层级过深:非递归但调用链极长(如多层嵌套的方法调用,每层生成一个栈帧);单个栈帧过大:方法的局部变量过多或过大(如大数组作为局部变量),导致单个栈帧占用内存过大,总深度未达上限但总内存超限。
public void f() { f(); }
排查方法:
查看错误堆栈(Stack Trace):错误信息会显示最后执行的方法和行号,通常可定位到递归或深层调用的代码(如重复出现);分析调用链:通过IDE的“调用层次结构”功能,检查方法调用深度是否合理。
at com.example.Demo.f(Demo.java:5)
解决方式:
修复递归逻辑:为递归添加终止条件(如),避免无限递归;减少调用层级:将深层调用拆分为多个步骤(如用循环替代递归,或增加中间层处理);调整栈内存大小:通过
if (n == 0) return;参数增大栈内存(如
-Xss,默认1MB左右),但需注意线程过多时总栈内存可能超限(导致OOM)。
-Xss2m
(2)OutOfMemoryError(内存溢出)
定义:JVM无法为对象分配内存(且GC后仍无足够空间)时抛出,根据内存区域不同,有多种细分类型。
常见类型及产生场景:
① Java heap space(堆内存溢出)
场景:创建大量对象且未及时回收(如无限循环创建对象、内存泄漏);示例:(list始终引用对象,无法GC)。
List<Object> list = new ArrayList<>(); while (true) { list.add(new Object()); }
② Metaspace(元空间溢出,Java 8+)
场景:加载过多类(如动态生成类、依赖过多Jar包)、常量池过大;示例:使用CGLib动态代理生成大量代理类,或Spring、Hibernate等框架未限制类加载数量。
③ Requested array size exceeds VM limit(数组大小超过虚拟机限制)
场景:创建的数组长度超过JVM允许的最大值(通常为);示例:
Integer.MAX_VALUE - 2(数组长度超限)。
new byte[Integer.MAX_VALUE]
④ Unable to create new native thread(无法创建新线程)
场景:创建的线程数量超过操作系统限制(如Linux默认线程数上限为32767);示例:循环创建线程(),导致系统资源耗尽。
while (true) { new Thread().start(); }
排查方法:
堆溢出(Java heap space):
导出堆快照:通过导出内存快照;分析快照:用MAT(Memory Analyzer Tool)或JProfiler分析,定位“内存泄漏对象”(如未释放的大集合、静态缓存)。
jmap -dump:format=b,file=heap.hprof <pid>
元空间溢出(Metaspace):
查看类加载数量:统计已加载的类数量;检查动态类生成逻辑:如CGLib、反射是否过度使用。
jmap -clstats <pid>
线程创建失败:
查看线程数量:统计线程数,或
jstack <pid>查看操作系统线程;检查线程创建逻辑:是否有无限创建线程的代码。
ps -T -p <pid>
解决方式:
堆溢出:
增大堆内存:通过(初始堆)和
-Xms(最大堆)参数(如
-Xmx);修复内存泄漏:释放无用对象引用(如清理静态集合、关闭资源);优化对象创建:复用对象(如用对象池)、减少大对象创建。
-Xms2g -Xmx2g
元空间溢出:
增大元空间:通过和
-XX:MetaspaceSize参数(如
-XX:MaxMetaspaceSize);减少类加载:避免动态生成过多类,清理无用的类加载器。
-XX:MaxMetaspaceSize=256m
线程创建失败:
使用线程池:复用线程(如),限制线程总数;减少线程数:优化业务逻辑,合并或异步处理任务。
Executors.newFixedThreadPool(n)
(3)总结对比
| 错误类型 | 核心原因 | 典型场景 | 排查工具 | 解决核心思路 |
|---|---|---|---|---|
| StackOverflowError | 栈深度超限 | 无限递归、深层调用 | 错误堆栈 | 修复递归/减少调用层级 |
| OutOfMemoryError | 堆/元空间/线程资源耗尽 | 内存泄漏、类过多、线程过多 | jmap、MAT、jstack | 增大内存/修复泄漏/限制资源使用 |
25. 内存泄漏和内存溢出的区别是什么?内存泄漏的常见场景及解决思路?
答:内存泄漏和内存溢出是JVM内存管理中的两个核心问题,二者紧密相关(内存泄漏可能导致内存溢出),但本质不同。
(1)内存泄漏与内存溢出的区别
| 维度 | 内存泄漏(Memory Leak) | 内存溢出(OutOfMemoryError) |
|---|---|---|
| 定义 | 不再使用的对象仍被引用,导致GC无法回收,占用内存 | 内存空间不足,无法为新对象分配内存(GC后仍不足) |
| 本质 | “对象未释放”(垃圾未回收) | “内存不够用”(总需求超过总容量) |
| 表现 | 内存占用逐渐增长(长期运行后OOM) | 程序直接崩溃,抛出OOM错误 |
| 关系 | 内存泄漏是导致内存溢出的常见原因(但非唯一) | 可能由内存泄漏、内存配置不足、对象创建过多导致 |
(2)内存泄漏的常见场景及解决思路
内存泄漏的核心是“无用对象被可达引用持有”,常见场景及解决方式如下:
静态集合类的不当使用
场景:等静态集合存储对象后未及时清理,集合的生命周期与类一致,导致对象长期被引用(如缓存未设置过期策略)。示例:
static List/Map
public class LeakDemo {
// 静态集合,生命周期与类一致
private static List<Object> cache = new ArrayList<>();
public void addToCache(Object obj) {
cache.add(obj); // obj被静态集合引用,永远无法GC
}
}
解决:
避免用静态集合存储大量临时对象;为缓存设置过期清理机制(如用实现LRU缓存,或使用Guava的
LinkedHashMap);定期调用
Cache清理无用对象。
clear()
未关闭的资源(IO流、数据库连接等)
场景:、
FileInputStream等资源未关闭,资源对象及其内部数据被JVM或底层库引用,无法回收。示例:
Connection
public void readFile() {
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// 读取数据...
} catch (IOException e) {
e.printStackTrace();
}
// 未关闭fis,导致资源对象泄漏
}
解决:
资源操作必须放在中(自动关闭,Java 7+);或在
try-with-resources中显式调用
finally方法;使用连接池管理数据库连接,避免频繁创建连接。
close()
监听器/回调未移除
场景:注册监听器(如GUI事件监听器、Spring事件监听器)后未注销,监听器对象被容器引用,导致关联对象无法回收。示例:
public class ListenerLeak {
private EventBus bus = new EventBus();
public void registerListener() {
MyListener listener = new MyListener();
bus.register(listener); // 注册监听器
// 未调用bus.unregister(listener),listener被bus引用
}
}
解决:
监听器使用完毕后,调用或
unregister注销;若监听器生命周期短于被监听对象,使用弱引用(
removeListener)包装监听器。
WeakReference
内部类持有外部类引用
场景:非静态内部类(如匿名内部类)会隐式持有外部类引用,若内部类对象生命周期长于外部类,会导致外部类无法回收。示例:
public class OuterClass {
public void startThread() {
// 匿名内部类Thread持有OuterClass引用
new Thread() {
@Override
public void run() {
while (true) { // 线程长期运行
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
}.start();
}
}
// 调用后:OuterClass实例本应被回收,但被内部线程持有,导致泄漏
new OuterClass().startThread();
解决:
将内部类改为静态内部类(静态内部类不持有外部类引用);若需访问外部类,用弱引用持有外部类()。
WeakReference<OuterClass> outerRef = new WeakReference<>(this)
ThreadLocal未清理
场景:的value是强引用,若线程长期存活(如线程池核心线程)且未调用
ThreadLocal,value会一直被线程的
remove()持有,导致泄漏(详见第17题)。解决:
ThreadLocalMap
每次使用后,在
ThreadLocal中调用
finally;线程池任务执行完毕后,清理线程的
remove()数据。
ThreadLocal
第三方库使用不当
场景:部分第三方库(如日志框架、JSON解析库)若配置不当,可能缓存大量对象(如日志上下文、解析器实例)导致泄漏。解决:
查阅库的官方文档,正确配置缓存大小和过期策略;避免在循环中频繁创建库的实例(如,应复用单例)。
new Gson()
(3)内存泄漏的排查工具与流程
监控内存趋势:通过或
jconsole监控堆内存使用,若内存持续增长且GC后不下降,可能存在泄漏;导出堆快照:用
VisualVM导出内存快照;分析快照:用MAT(Memory Analyzer Tool)的“Leak Suspects”功能定位泄漏对象,查看引用链(找出谁在持有无用对象);修复代码:根据引用链定位到具体代码,移除不必要的引用(如清理集合、关闭资源)。
jmap -dump:format=b,file=heap.hprof <pid>
(4)总结
内存泄漏是“被动的资源浪费”(对象无用但未回收),内存溢出是“主动的资源不足”(无法分配新内存);解决内存泄漏的核心是“切断无用对象的引用链”,确保GC能正常回收;长期运行的程序(如服务端应用)需重点防范内存泄漏,避免运行一段时间后OOM。
(后续可继续推进第26题及以后的问题,如JVM启动参数、调优命令等,若需调整节奏或重点讲解某类问题,请告知。)
29. MySQL的事务隔离级别有哪些?各自的含义和解决的问题是什么?
事务是MySQL保证数据一致性的核心机制,遵循ACID特性(原子性、一致性、隔离性、持久性)。隔离性是指多个事务并发执行时,相互之间的干扰程度,隔离级别越高,并发冲突越少,但性能越低。MySQL定义了4种标准隔离级别(由SQL标准指定),InnoDB默认支持所有级别。
(1)事务并发带来的3个核心问题
在讲解隔离级别前,先明确并发事务可能出现的问题(按严重程度从高到低):
脏读(Dirty Read):事务A读取了事务B未提交的修改(B后续回滚,A读取的是“无效数据”);不可重复读(Non-Repeatable Read):事务A多次读取同一数据,事务B在期间修改并提交该数据,导致A前后读取结果不一致;幻读(Phantom Read):事务A按条件查询数据(如“age>20”),事务B插入/删除符合该条件的新数据并提交,导致A再次查询时,结果集行数变化(像“出现/消失幻觉”)。
(2)4种事务隔离级别(从低到高)
| 隔离级别 | 含义 | 解决的问题 | 存在的问题 | InnoDB默认支持 |
|---|---|---|---|---|
| 读未提交(Read Uncommitted) | 事务A可读取事务B未提交的修改 | 无(最低隔离) | 脏读、不可重复读、幻读 | 是 |
| 读已提交(Read Committed) | 事务A仅能读取事务B已提交的修改(避免脏读) | 脏读 | 不可重复读、幻读 | 是 |
| 可重复读(Repeatable Read) | 事务A多次读取同一数据,结果始终一致(避免脏读、不可重复读) | 脏读、不可重复读 | 幻读(InnoDB已优化) | 是(默认级别) |
| 串行化(Serializable) | 事务串行执行(同一时间仅一个事务操作数据),完全隔离 | 所有并发问题 | 性能极低(无并发) | 是 |
(3)各隔离级别的详细说明
读未提交(Read Uncommitted)
示例:事务B更新“用户余额”为100但未提交,事务A读取到余额100,随后B回滚,A的读取结果无效。适用场景:无(仅理论存在,牺牲一致性换性能,实际几乎不用)。
读已提交(Read Committed)
核心机制:事务每次读取数据时,都生成新的“Read View”(一致性视图),仅能看到已提交的事务修改。示例:事务A第一次读余额为50,事务B更新为100并提交,A再次读余额为100(允许“不可重复读”,但避免脏读)。适用场景:对一致性要求一般,追求高并发的场景(如电商商品列表查询)。
可重复读(Repeatable Read,InnoDB默认)
核心机制:事务启动时生成一个“Read View”,整个事务期间复用该视图,确保多次读取结果一致。示例:事务A启动后第一次读余额为50,事务B更新为100并提交,A再次读仍为50(避免不可重复读)。对幻读的优化:InnoDB通过“Next-Key Lock”(行锁+间隙锁),防止并发事务插入符合查询条件的新数据,实际已解决幻读问题。适用场景:绝大多数业务场景(如金融交易、订单管理),平衡一致性和性能。
串行化(Serializable)
核心机制:通过表锁强制事务串行执行,并发事务需排队等待。示例:事务A查询“age>20”的用户,事务B插入一条age=25的用户,需等待A提交后才能执行。适用场景:一致性要求极高,并发极低的场景(如银行转账对账)。
(4)如何设置隔离级别
会话级(当前连接生效):(如
SET SESSION TRANSACTION ISOLATION LEVEL 隔离级别;);全局级(所有新连接生效):
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;;查看当前隔离级别:
SET GLOBAL TRANSACTION ISOLATION LEVEL 隔离级别;。
SELECT @@transaction_isolation;
30. 什么是MVCC?实现原理是什么?
MVCC(Multi-Version Concurrency Control,多版本并发控制)是InnoDB实现“可重复读”和“读已提交”隔离级别的核心机制,通过“保存数据多版本”,让读写事务并发执行(读不加锁,写不阻塞读),提升并发性能。
(1)MVCC的核心目标
读写分离:读事务(SELECT)不阻塞写事务(INSERT/UPDATE/DELETE),写事务不阻塞读事务;一致性读:不同隔离级别下,读事务能看到符合要求的数据版本(已提交或未提交)。
(2)MVCC的实现原理(3个核心组件)
InnoDB通过“隐藏列+undo日志+Read View”三者配合,实现数据多版本管理:
隐藏列(数据行的版本标识)
每个数据行除了用户定义的字段,还隐含3个隐藏列:
:最近一次修改该数据的事务ID(事务开始时分配的唯一ID,自增);
DB_TRX_ID:回滚指针,指向该数据的上一个版本(存储在undo日志中);
DB_ROLL_PTR:默认自增的行ID(当表无主键时,作为聚簇索引的主键)。
DB_ROW_ID
undo日志(数据版本的存储载体)
当事务修改数据时,InnoDB会先将数据的“旧版本”写入undo日志(如UPDATE前的原始数据);undo日志按“事务ID”和“回滚指针”串联,形成数据的版本链(最新版本在表中,历史版本在undo日志中);示例:数据行被3个事务依次修改,版本链为“当前版本(trx3)→ 版本2(trx2)→ 版本1(trx1)→ NULL”。
Read View(一致性视图,判断版本可见性)
Read View是事务启动时生成的“视图”,记录当前活跃的事务ID(未提交的事务),用于判断数据版本是否对当前事务可见。核心规则:
数据版本的 < Read View中最小活跃事务ID:该版本是“已提交事务”修改的,可见;数据版本的
DB_TRX_ID > Read View中最大活跃事务ID:该版本是“未来事务”修改的,不可见;数据版本的
DB_TRX_ID 在活跃事务ID范围内:若该事务ID是当前事务自身ID,可见;否则不可见。
DB_TRX_ID
(3)MVCC在不同隔离级别的工作流程
读已提交(Read Committed)
每次执行SELECT时,都会重新生成一个Read View;因此,同一事务中多次查询,可能看到不同版本的数据(避免脏读,但允许不可重复读)。
可重复读(Repeatable Read)
事务启动时(第一次执行SELECT前)生成一个Read View,整个事务期间复用;因此,同一事务中多次查询,看到的是同一版本的数据(避免不可重复读)。
(4)MVCC的优势与局限
优势:读写并发性能高(无锁竞争),数据一致性好;局限:需维护undo日志和版本链,占用额外存储空间;undo日志清理(purge线程)会带来一定性能开销。
31. 什么是数据库的锁?MySQL中的锁有哪些分类?
数据库锁是MySQL保证并发事务数据一致性的机制,通过“锁定资源”防止多个事务同时修改同一数据,导致冲突。MySQL的锁分类维度多样,核心分类如下:
(1)按锁的粒度分类(核心分类)
锁的粒度越小,并发性能越高(锁定范围小,冲突少),但锁管理开销越大。
表锁(Table Lock)
锁定整个表,所有对该表的操作(读/写)需等待锁释放;特点:实现简单,锁开销小,并发性能低;支持引擎:MyISAM(默认)、InnoDB(支持,但不常用);适用场景:批量操作(如、全表扫描),避免频繁锁冲突。
ALTER TABLE
行锁(Row Lock)
锁定单条数据行,仅影响当前行,其他行可正常操作;特点:锁粒度小,并发性能高,锁开销大;支持引擎:InnoDB(默认);适用场景:OLTP场景(如用户查询、订单修改),高频单行操作。
页锁(Page Lock)
锁定数据页(InnoDB中一页默认16KB,包含多条数据),粒度介于表锁和行锁之间;特点:平衡并发和锁开销;支持引擎:BDB引擎(极少使用);适用场景:无(InnoDB和MyISAM均不依赖,仅理论存在)。
(2)按锁的性质分类
共享锁(Shared Lock,S锁)
又称“读锁”,事务获取S锁后,仅能读取数据,不能修改;兼容性:多个事务可同时获取同一资源的S锁(读-读不冲突);加锁方式:。
SELECT ... LOCK IN SHARE MODE;
排他锁(Exclusive Lock,X锁)
又称“写锁”,事务获取X锁后,可读取和修改数据;兼容性:同一资源不能同时存在S锁和X锁(读-写、写-写均冲突);加锁方式:(手动加锁),或INSERT/UPDATE/DELETE自动加X锁。
SELECT ... FOR UPDATE;
(3)按锁的意向分类(InnoDB特有)
为了快速判断表是否有行锁,InnoDB引入“意向锁”(表级锁),无需遍历所有行判断锁状态。
意向共享锁(Intention Shared Lock,IS锁)
事务获取某行的S锁前,先自动获取表的IS锁;作用:标记“表中有事务持有行级S锁”,不阻塞其他事务的IS锁,仅阻塞X锁。
意向排他锁(Intention Exclusive Lock,IX锁)
事务获取某行的X锁前,先自动获取表的IX锁;作用:标记“表中有事务持有行级X锁”,阻塞其他事务的S锁和X锁。
(4)按锁的实现方式分类(InnoDB特有)
Record Lock(记录锁)
锁定具体的数据行(如锁定id=1的行);仅锁定已存在的数据行,不防止插入新行(可能导致幻读)。
WHERE id=1
Gap Lock(间隙锁)
锁定数据行之间的“间隙”(如id=1和id=3之间的间隙),防止其他事务插入数据;仅在“可重复读”隔离级别下生效,用于解决幻读。
Next-Key Lock(临键锁)
Record Lock + Gap Lock的组合,锁定“数据行+相邻间隙”(如id=1的行 + 1~3的间隙);InnoDB行锁的默认实现,兼顾行锁和间隙锁的优势,彻底解决幻读。
(5)总结:InnoDB与MyISAM的锁机制差异
| 维度 | InnoDB | MyISAM |
|---|---|---|
| 锁粒度 | 行锁(默认)+ 表锁 | 表锁(唯一) |
| 锁类型 | 支持S/X锁、意向锁、Next-Key Lock | 仅支持S/X锁(表级) |
| 并发性能 | 高(读写不冲突) | 低(读写冲突) |
| 事务支持 | 支持(ACID) | 不支持 |
| 适用场景 | OLTP(高频读写) | OLAP(批量查询) |
32. 什么是死锁?如何产生的?如何避免和解决?
死锁是指两个或多个事务相互持有对方需要的锁,且都不释放自己的锁,导致所有事务永久阻塞(无限等待)的场景,是并发事务中常见的问题。
(1)死锁的产生条件(4个必要条件,缺一不可)
互斥条件:资源(锁)只能被一个事务持有,不能共享;持有并等待条件:事务持有一个锁,同时等待另一个事务持有的锁;不可剥夺条件:事务已持有的锁,不能被其他事务强制剥夺;循环等待条件:多个事务形成“锁等待循环”(如A等B的锁,B等A的锁)。
(2)MySQL中死锁的示例
-- 事务A
BEGIN;
UPDATE user SET balance=balance-100 WHERE id=1; -- 持有id=1的X锁
UPDATE user SET balance=balance+100 WHERE id=2; -- 等待id=2的X锁
-- 事务B(与A并发执行)
BEGIN;
UPDATE user SET balance=balance-100 WHERE id=2; -- 持有id=2的X锁
UPDATE user SET balance=balance+100 WHERE id=1; -- 等待id=1的X锁
结果:事务A持有id=1的锁,等待id=2;事务B持有id=2的锁,等待id=1,形成循环等待,触发死锁。
(3)死锁的检测与处理
检测:InnoDB内置死锁检测机制(通过“等待图”判断是否存在循环等待),检测到死锁后,会选择“事务权重小”(如修改行数少、执行时间短)的事务回滚,释放锁,让其他事务继续执行;查看死锁日志:(在“LATEST DETECTED DEADLOCK” section中查看死锁详情)。
SHOW ENGINE INNODB STATUS;
(4)死锁的避免策略(破坏4个必要条件)
破坏“循环等待条件”:统一事务的锁申请顺序(如所有事务都按“id从小到大”的顺序更新数据);
示例:事务A和B都先更新id=1,再更新id=2,避免循环等待。
破坏“持有并等待条件”:事务启动时一次性申请所有需要的锁(如批量更新时,用一次性锁定多行);
WHERE id IN (1,2)
注意:需避免锁粒度过大(如全表锁)。
设置事务超时时间:通过(默认50秒)设置锁等待超时时间,超时后事务自动回滚;
innodb_lock_wait_timeout
示例:(超时时间设为10秒)。
SET GLOBAL innodb_lock_wait_timeout=10;
避免长事务:长事务会持有锁时间过长,增加死锁概率,尽量将事务拆分为短事务(如“查询-修改-提交”一气呵成)。使用低隔离级别:如“读已提交”隔离级别下,InnoDB的锁竞争更少(Next-Key Lock失效,仅用Record Lock),死锁概率降低。
33. Redis的数据结构有哪些?各自的适用场景是什么?
Redis支持多种高性能数据结构,每种结构都有特定的底层实现和适用场景,是Redis灵活高效的核心。
(1)核心数据结构(5种基础+3种扩展)
String(字符串)
底层实现:动态字符串(SDS),支持扩容(预分配空间减少内存碎片);核心特性:存储字符串、数字(整数/浮点数),支持原子操作(如自增、自减);常用命令:、
SET key value、
GET key(自增)、
INCR key(追加);适用场景:缓存用户信息(JSON字符串)、计数器(文章阅读量)、分布式锁(
APPEND key str)。
SET NX EX
Hash(哈希)
底层实现:哈希表(字典),小数据量时用压缩列表(节省内存);核心特性:存储“键值对集合”(如对象的多个属性),支持单独操作某个字段;常用命令:、
HSET key field value、
HGET key field(获取所有字段);适用场景:缓存对象(如用户信息:id、name、age)、购物车(用户ID→商品ID→数量)。
HGETALL key
List(列表)
底层实现:双向链表(支持两端高效操作),小数据量时用压缩列表;核心特性:有序(按插入顺序)、可重复,支持两端插入/删除(O(1))、中间查询(O(n));常用命令:(左插)、
LPUSH key value(右删)、
RPOP key(获取区间元素);适用场景:消息队列(生产者LPUSH,消费者RPOP)、排行榜(最新评论列表)。
LRANGE key start end
Set(集合)
底层实现:哈希表(判断元素是否存在O(1));核心特性:无序、不可重复,支持集合运算(交集、并集、差集);常用命令:(添加元素)、
SADD key member(判断存在)、
SISMEMBER key member(交集);适用场景:标签(用户兴趣标签)、好友关系(共同好友:交集运算)、去重(抽奖用户去重)。
SINTER key1 key2
Sorted Set(有序集合)
底层实现:跳表(Skip List)+ 哈希表,跳表支持快速排序和范围查询;核心特性:有序(按“分数”排序)、不可重复,支持按分数/排名查询;常用命令:(添加元素+分数)、
ZADD key score member(按排名查询)、
ZRANGE key start end(获取分数);适用场景:排行榜(游戏积分排名)、带权重的消息队列(按优先级消费)、范围查询(查询分数80-100的用户)。
ZSCORE key member
BitMap(位图)
底层实现:字符串(SDS),每个字节存储8个bit位;核心特性:存储二进制位(0/1),高效节省内存(如存储100万用户的状态,仅需125KB);常用命令:(设置bit位)、
SETBIT key offset value(获取bit位)、
GETBIT key offset(统计1的个数);适用场景:用户签到(日期→bit位,1=签到)、状态标记(用户是否在线)。
BITCOUNT key
HyperLogLog(基数统计)
底层实现:概率数据结构,基于伯努利试验估算基数;核心特性:不存储具体元素,仅统计“不重复元素个数”(基数),误差率约0.81%;常用命令:(添加元素)、
PFADD key element(统计基数);适用场景:UV统计(网站独立访客数)、搜索关键词去重统计(无需存储具体关键词)。
PFCOUNT key
Geo(地理信息)
底层实现:Sorted Set(将经纬度编码为分数,按距离排序);核心特性:存储地理坐标(纬度、经度),支持距离计算、范围查询;常用命令:(添加坐标)、
GEOADD key longitude latitude member(计算距离);适用场景:附近的人(查询距离当前用户10公里内的商家)、地理围栏(范围筛选)。
GEODIST key member1 member2
(2)数据结构选择原则
存储单个值(如字符串、数字)→ String;存储对象(多属性)→ Hash(比String更灵活,支持单独修改属性);有序且需两端操作→ List;无序去重或集合运算→ Set;有序且需排序/排名→ Sorted Set;二进制状态或大数据量去重统计→ BitMap/HyperLogLog;地理坐标相关→ Geo。
34. Redis的持久化机制(RDB和AOF)的区别和优缺点?
Redis是内存数据库,数据默认存储在内存中,重启后数据丢失。持久化机制通过将内存数据写入磁盘,保证数据不丢失,核心有两种方式:RDB和AOF。
(1)RDB(Redis Database)
定义:在指定时间间隔内,将内存中的“全量数据”快照(Snapshot)写入磁盘(生成.rdb文件);触发方式:
手动触发:(同步,阻塞Redis)、
SAVE(异步,fork子进程执行,不阻塞);自动触发:配置文件中设置
BGSAVE(如
save <seconds> <changes>:60秒内1000次修改则触发);
save 60 1000
底层原理:fork子进程,子进程遍历内存数据并写入.rdb文件,父进程继续处理请求(写时复制,COW,避免数据不一致)。
(2)AOF(Append Only File)
定义:记录每一条“写命令”(如SET、INCR),Redis重启时通过重新执行命令恢复数据;触发方式:
配置文件中开启:;同步策略(控制命令写入磁盘的时机):
appendonly yes
:每写一条命令同步一次(最安全,性能最低);
appendfsync always:每秒同步一次(默认,平衡安全和性能);
appendfsync everysec:由操作系统决定同步时机(性能最高,最不安全);
appendfsync no
日志重写:AOF文件会不断增大,Redis通过命令重写日志(合并重复命令,如
BGREWRITEAOF→
SET a 1合并为
SET a 2),减小文件体积。
SET a 2
(3)RDB与AOF的区别对比
| 维度 | RDB | AOF |
|---|---|---|
| 数据完整性 | 低(可能丢失最后一次快照后的修改) | 高(默认每秒同步,最多丢失1秒数据) |
| 文件体积 | 小(全量快照,压缩存储) | 大(记录所有写命令,未重写前体积大) |
| 恢复速度 | 快(直接加载快照,无需执行命令) | 慢(需重新执行所有命令) |
| 写性能 | 高(异步执行,对主进程影响小) | 中(同步策略影响性能,重写时无影响) |
| 适用场景 | 数据备份(如每日凌晨备份)、主从复制(从节点初始同步) | 生产环境默认(追求数据完整性) |
(4)如何选择持久化方式
单一种方式:生产环境优先AOF(数据完整性更高);混合持久化(Redis 4.0+支持):开启后,AOF文件头部存储RDB快照,尾部存储增量命令,兼顾RDB的快速恢复和AOF的数据完整性(推荐);关闭持久化:仅用于缓存场景(数据可丢失),追求极致性能。
35. 什么是缓存穿透、缓存击穿、缓存雪崩?各自的解决方案是什么?
缓存是提升系统性能的核心,但在高并发场景下,可能出现缓存穿透、击穿、雪崩等问题,导致缓存失效,压力全部转移到数据库(DB),引发DB宕机。
(1)缓存穿透(Cache Penetration)
定义:查询“不存在的数据”(如用户ID=-1),缓存中无该数据,所有请求直接穿透到DB,DB频繁查询无效数据,压力过大;解决方案:
空值缓存:查询DB后发现数据不存在,在缓存中存储“空值”(如),设置短期过期时间(如5分钟),避免重复穿透;布隆过滤器(Bloom Filter):将所有存在的有效数据(如用户ID)存入布隆过滤器,查询前先通过过滤器判断数据是否存在,不存在则直接返回,无需查询缓存和DB;
key=-1, value=null
原理:布隆过滤器是概率数据结构,通过多个哈希函数将数据映射到bit位,判断“不存在”时100%准确,“存在”时可能有误差(可通过调整参数降低)。
(2)缓存击穿(Cache Breakdown)
定义:查询“热点数据”(如热门商品详情),缓存中该数据刚好过期,此时大量并发请求穿透到DB,DB瞬间压力剧增;解决方案:
互斥锁(分布式锁):缓存过期时,只有一个线程能获取锁查询DB,其他线程等待,查询结果写入缓存后释放锁,其他线程从缓存获取数据;
实现:用Redis的命令获取分布式锁;
SET NX EX
热点数据永不过期:热点数据不设置过期时间,通过后台线程定期更新缓存(如每小时更新一次);缓存预热:系统启动时,提前将热点数据加载到缓存中,避免运行时缓存过期。
(3)缓存雪崩(Cache Avalanche)
定义:大量缓存数据在同一时间过期,或缓存服务(如Redis)宕机,导致所有请求穿透到DB,DB不堪重负而宕机;解决方案:
过期时间随机化:为缓存数据设置过期时间时,添加随机值(如),避免大量数据同时过期;缓存集群部署:采用Redis集群(如主从+哨兵、Redis Cluster),避免单点故障(某节点宕机,其他节点正常提供服务);服务降级/熔断:缓存失效时,通过降级策略返回默认数据(如“系统繁忙,请稍后重试”),或熔断停止调用DB,避免DB宕机;DB限流:通过限流组件(如Guava RateLimiter)限制DB的并发请求数,避免DB被压垮。
expire = 30分钟 + 随机(0-10分钟)
(4)总结对比
| 问题类型 | 核心原因 | 解决方案核心思路 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 空值缓存、布隆过滤器 |
| 缓存击穿 | 热点数据过期+高并发 | 互斥锁、热点数据永不过期 |
| 缓存雪崩 | 大量数据同时过期/缓存宕机 | 随机过期时间、集群部署、降级熔断 |
36. 如何保证Redis和MySQL的数据一致性?
Redis作为缓存,MySQL作为持久化存储,二者数据一致性是指“缓存中的数据与DB中的数据保持一致”,避免出现“缓存有数据但DB已修改”或“缓存无数据但DB有数据”的情况。
(1)核心更新策略(4种)
Cache-Aside(旁路缓存,最常用)
读写流程:
读操作:先查缓存→缓存命中直接返回;缓存未命中→查DB→将DB结果写入缓存→返回;写操作:先更DB→再删缓存(而非更新缓存);
核心原因:写操作“删缓存”而非“更缓存”,避免并发写导致的缓存不一致(如事务A更新DB后更缓存,事务B同时更新DB后更缓存,可能覆盖A的缓存);解决缓存不一致:若删缓存失败(如Redis宕机),可通过“缓存过期时间”兜底(过期后自动从DB加载最新数据),或通过消息队列重试删缓存。
Read-Through(读透)
核心思路:缓存作为DB的“代理”,读操作直接查询缓存,缓存未命中时,缓存主动查询DB并写入缓存,应用无需关心DB操作;适用场景:缓存与DB耦合度高的场景(如分布式缓存框架),应用开发更简洁;局限:Redis不原生支持,需自定义开发缓存代理层。
Write-Through(写透)
核心思路:写操作直接更新缓存,缓存同步更新DB(缓存与DB原子性更新);适用场景:数据一致性要求极高,写性能要求低的场景(如金融核心数据);局限:写操作需同时更新缓存和DB,性能较低;DB更新失败会导致缓存不一致。
Write-Behind(写回)
核心思路:写操作仅更新缓存,缓存异步批量更新DB(如定期同步、缓存满时同步);适用场景:写性能要求极高,数据一致性要求一般的场景(如日志存储);局限:缓存宕机会导致数据丢失,实现复杂(需处理异步同步失败)。
(2)并发场景下的一致性优化
并发读+并发写:
问题:事务A读缓存未命中→查DB(旧值),同时事务B更新DB→删缓存,事务A将旧值写入缓存,导致缓存不一致;解决:读操作时,若缓存未命中,查询DB后,用(仅当key不存在时写入)写入缓存(避免覆盖已更新的缓存)。
SETNX
延迟双删:
流程:写操作→更DB→删缓存→延迟1秒(根据业务调整)→再次删缓存;适用场景:解决“DB更新后,缓存删除前,已有请求读取旧值并写入缓存”的问题(延迟删缓存可覆盖旧值)。
分布式锁:
核心思路:写操作时获取分布式锁,确保同一时间只有一个线程更新DB和删缓存,避免并发冲突;适用场景:高并发写场景(如秒杀商品库存更新)。
(3)总结
优先选择Cache-Aside策略(实现简单,平衡一致性和性能);兜底方案:为缓存设置合理的过期时间(即使出现不一致,过期后自动恢复);高并发场景:结合延迟双删、分布式锁,进一步保证一致性。
我可以帮你整理剩余问题(Spring IoC、AOP、事务原理、Spring Boot自动配置等)的详细答案,需要继续推进吗?
37. Spring IoC容器的核心原理是什么?Bean的生命周期是什么?
(1)IoC核心原理
IoC(控制反转)是Spring核心,核心是“将对象创建、依赖注入的控制权交给Spring容器”,而非手动对象。
new
核心组件:(IoC容器顶层接口,延迟初始化)、
BeanFactory(BeanFactory子类,立即初始化,功能更全);实现流程:加载配置(XML/注解)→ 解析Bean定义→ 容器初始化Bean→ 依赖注入(DI)→ 提供Bean实例。
ApplicationContext
(2)Bean生命周期(核心阶段)
实例化:容器通过构造器创建Bean对象;属性注入:注入依赖的Bean(@Autowired/setter方法);初始化:执行注解方法、
@PostConstruct接口的
InitializingBean、XML配置的
afterPropertiesSet();就绪:Bean可被应用调用;销毁:容器关闭时,执行
init-method注解方法、
@PreDestroy接口的
DisposableBean、XML配置的
destroy()。
destroy-method
38. Spring AOP的核心概念和实现原理是什么?
(1)核心概念
切面(Aspect):封装横切逻辑(如日志、事务)的类;连接点(JoinPoint):程序执行点(如方法调用、字段赋值);切入点(Pointcut):筛选连接点的规则(如);通知(Advice):切面的具体逻辑(前置@Before、后置@After、返回@AfterReturning、异常@AfterThrowing、环绕@Around);目标对象(Target):被增强的原始对象。
execution(* com..*Service.*(..))
(2)实现原理
底层:动态代理,默认优先JDK动态代理(目标类实现接口),无接口时用CGLIB动态代理(子类增强);流程:扫描切面→ 解析切入点→ 为目标对象创建代理→ 调用目标方法时,代理类织入通知逻辑。
39. Spring事务的实现方式和传播机制是什么?
(1)实现方式
声明式事务(推荐):通过注解或XML配置,Spring自动管理事务(AOP织入);编程式事务:通过
@Transactional或
TransactionTemplate手动控制事务(灵活性高,代码侵入性强)。
PlatformTransactionManager
(2)核心传播机制(7种,常用3种)
(默认):当前无事务则新建,有则加入;
REQUIRED:无论当前是否有事务,都新建事务(原事务暂停);
REQUIRES_NEW:当前有事务则嵌套子事务,无则新建(子事务回滚不影响父事务)。
NESTED
40. Spring Boot自动配置的原理是什么?
核心是“约定优于配置”,通过注解驱动自动加载配置:
核心注解:;
@SpringBootApplication = @SpringBootConfiguration + @ComponentScan + @EnableAutoConfiguration:导入
@EnableAutoConfiguration,扫描
AutoConfigurationImportSelector文件,加载自动配置类(如
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports);条件注解:自动配置类通过
DataSourceAutoConfiguration(类存在)、
@ConditionalOnClass(Bean不存在)等,按需生效;配置优先级:用户配置(application.yml/properties)> 自动配置类默认值。
@ConditionalOnMissingBean
41. 消息中间件的核心作用是什么?常用的消息中间件有哪些?
(1)核心作用
解耦:服务间通过消息通信,无需直接依赖;异步:非核心流程异步处理(如下单后发送通知),提升响应速度;削峰填谷:高并发场景下,消息队列缓冲请求,避免下游服务被压垮;可靠通信:保证消息的投递、消费确认(ACK)、重试机制。
(2)常用消息中间件对比
| 中间件 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| RocketMQ | 高吞吐、低延迟、支持事务消息 | 生态不如Kafka完善 | 金融、电商等核心业务 |
| Kafka | 超高吞吐、适合大数据 | 不支持事务消息,重试复杂 | 日志收集、大数据场景 |
| RabbitMQ | 路由灵活、支持多种交换机 | 高并发下性能略弱 | 中小型系统、路由复杂场景 |
42. 分布式事务的解决方案有哪些?
分布式事务是跨多个服务/数据库的事务一致性问题,核心方案:
2PC(两阶段提交):协调者分“准备阶段”和“提交阶段”,强一致但性能低、阻塞风险高;TCC(Try-Confirm-Cancel):业务层面拆分事务(预留资源→ 确认提交→ 取消回滚),无锁但开发成本高;本地消息表+消息队列:本地事务与消息写入同库(原子性),异步通知其他服务,最终一致;事务消息(RocketMQ支持):消息先预发送(半消息),本地事务成功后确认发送,失败则回滚;SAGA模式:长事务拆分为短事务,通过补偿操作回滚(如下单→ 支付→ 发货,支付失败则取消订单)。
43. 分布式锁的实现方式和优缺点是什么?
(1)核心实现方式
Redis分布式锁:通过(原子操作),加锁时设置过期时间,解锁时用Lua脚本保证原子性;
SET NX EX key value
优点:高性能、易实现;缺点:需处理锁超时、主从切换数据不一致问题。
Zookeeper分布式锁:基于临时有序节点,利用Watcher机制监听节点变化;
优点:天然支持公平锁、无锁超时问题;缺点:性能低于Redis,依赖Zookeeper集群。
数据库分布式锁:通过排他锁,或唯一索引插入;
SELECT ... FOR UPDATE
优点:无需额外组件;缺点:性能低,易产生死锁。
(2)核心要求
互斥性、安全性(崩溃不丢锁)、可用性、公平性(可选)。
44. 分库分表的核心思路和实现方案是什么?
(1)核心思路
解决单库单表数据量过大(如千万级)导致的查询/写入性能下降,分为:
分库:按规则将数据分散到多个数据库(降低单库压力);分表:按规则将单表分散到多个表(降低单表压力);分片规则:水平分片(按行拆分,如用户ID取模)、垂直分片(按列拆分,如大字段单独分表)。
(2)实现方案
中间件方案(推荐):Sharding-JDBC(轻量级,嵌入应用)、ShardingSphere-Proxy(独立代理层);自定义方案:通过代码逻辑实现分片(如用户ID%4→ 4个表),开发成本高、扩展性差。
45. 什么是CAP理论和BASE理论?
(1)CAP理论
分布式系统中,三个特性最多同时满足两个:
C(一致性):所有节点数据一致;A(可用性):服务始终可用,响应请求;P(分区容错性):网络分区时,系统仍能运行;实际选择:互联网场景优先AP(可用性+分区容错),金融场景优先CP(一致性+分区容错)。
(2)BASE理论
CAP理论的妥协,追求“最终一致性”:
B(基本可用):核心功能可用,非核心功能降级;A(软状态):数据允许短暂不一致;E(最终一致性):一段时间后,数据自动同步一致(如Redis主从同步)。
46. 接口幂等性的实现方案有哪些?
幂等性是指同一请求多次执行,结果一致(无副作用),常用方案:
唯一标识:请求携带唯一ID(如UUID),服务端存储ID,重复请求直接返回结果;乐观锁:数据库字段加,更新时
version,版本不匹配则重试;防重表:请求处理前插入唯一标识到防重表(唯一索引),插入失败则为重复请求;状态机:如订单状态“待支付→ 已支付”,重复支付时判断状态,不允许回退。
WHERE id=? AND version=?
47. 常用的限流算法有哪些?
限流是限制接口的并发请求数,避免服务过载:
固定窗口计数器:单位时间(如1秒)内允许N个请求,超出则拒绝,存在临界问题;滑动窗口计数器:将时间窗口拆分为小格子(如1秒拆10个100ms格子),滑动统计,解决临界问题;漏桶算法:请求匀速处理,突发流量被缓冲,溢出则拒绝(控制输出速率);令牌桶算法:定时生成令牌,请求需获取令牌才处理,支持突发流量(令牌积累后可一次处理多个);
实现:Guava RateLimiter(令牌桶)、Redis+Lua(分布式限流)。
48. 什么是服务熔断、降级和限流?三者的区别是什么?
(1)核心定义
熔断:服务调用失败率过高(如50%),触发熔断(断开调用),避免级联故障(如Sentinel、Hystrix);降级:系统压力过大时,关闭非核心功能(如商品详情页不显示推荐),保障核心功能可用;限流:限制接口并发请求数,避免服务被压垮。
(2)区别
熔断:针对“服务调用失败”,主动断开;降级:针对“系统压力”,主动关闭非核心功能;限流:针对“请求量”,被动限制请求。
49. Java中的引用类型有哪些?各自的特点是什么?
Java有4种引用类型(从强到弱),用于灵活控制对象生命周期:
强引用:默认引用(),GC不会回收被强引用的对象,OOM根源;软引用(SoftReference):内存不足时GC回收,适合缓存(如图片缓存);弱引用(WeakReference):下次GC必回收,适合临时数据(如ThreadLocal的key);虚引用(PhantomReference):仅用于跟踪对象回收,必须结合ReferenceQueue,无实际引用意义。
User u = new User()
50. 什么是JVM的内存模型(JMM)?核心作用是什么?
(1)定义
JMM是Java虚拟机规范定义的“内存可见性、原子性、有序性”的保障模型,屏蔽硬件和操作系统差异。
内存划分:主内存(所有线程共享,存储对象实例)、工作内存(线程私有,存储主内存数据的副本);核心交互:线程读写数据需通过“主内存-工作内存”交互(读取:主内存→ 工作内存;写入:工作内存→ 主内存)。
(2)核心作用
解决多线程并发问题,定义3大特性:
原子性:、
synchronized、原子类保障;可见性:
Lock、
volatile保障(写回主内存+读取主内存);有序性:
synchronized(禁止指令重排序)、
volatile保障(单线程内有序)。
synchronized
需要我继续整理剩余的网络编程、Linux命令、设计模式等相关问题吗?可以帮你聚焦面试高频考点,进一步精简优化答案。
51. IO多路复用的三种实现(select/poll/epoll)区别和适用场景?
select:基于位图,最大文件描述符限制(1024),轮询遍历,效率低;适用于连接少的简单场景。poll:基于数组,无文件描述符限制,仍轮询;适用于连接数中等场景。epoll:基于红黑树+就绪链表,事件驱动(只处理就绪连接),无连接限制;适用于高并发(如百万连接),是Linux首选。
52. HTTP各版本(1.0/1.1/2/3)核心差异?
1.0:短连接(一次请求一个连接),无持久连接;1.1:长连接(Connection: keep-alive),支持管道化、Chunked编码;2:二进制帧、多路复用(单连接多请求)、头部压缩、服务器推送;3:基于QUIC协议(UDP),解决TCP队头阻塞,更低延迟,适用于弱网。
53. 常用Linux命令(至少5个)及find命令用法?
(1)常用命令
:列出目录文件(
ls详细信息,
ls -l显示隐藏文件);
ls -a:切换目录(
cd返回上级,
cd ..根目录);
cd /:文本搜索(
grep);
grep "关键词" 文件名:查看进程(
ps查找Java进程);
ps -ef | grep java:终止进程(
kill强制终止);
kill -9 进程ID:创建目录(
mkdir)。
mkdir -p 多级目录
(2)find命令用法
按文件名查找:(精准匹配)、
find /root -name "test.txt"(模糊匹配);按类型查找:
find /root -name "*.log"(文件)、
find /root -type f(目录);按修改时间查找:
find /root -type d(3天内修改的文件)。
find /root -mtime -3
54. 设计模式了解多少?项目中如何使用模板方法模式?有哪些替代方案?
(1)常用设计模式
创建型:单例、工厂(简单工厂/工厂方法)、建造者;结构型:代理、装饰器、适配器、组合;行为型:模板方法、策略、观察者、迭代器。
(2)模板方法模式
核心:父类定义算法骨架(模板方法),子类重写局部步骤(钩子方法);项目应用:如统一接口响应封装(父类定义响应格式,子类实现业务逻辑);替代方案:策略模式(封装不同算法,动态切换)、lambda表达式(简化简单场景的方法重写)。
55. 如何设计银行系统?核心考虑点是什么?
核心围绕“高可用、高安全、高并发、数据一致性”:
架构分层:接入层(网关限流)→ 业务层(账户、交易、风控)→ 数据层(分库分表+主从备份);数据安全:敏感数据加密(如密码MD5加盐)、权限控制(RBAC)、操作日志审计;并发处理:分布式锁(防重复交易)、消息队列削峰、事务一致性(TCC/事务消息);高可用:集群部署、熔断降级、灾备备份(异地多活);核心功能:账户管理(开户/销户)、交易处理(转账/支付)、风控(反欺诈)。
56. 如何实时显示微信二维码扫描状态?(服务端+客户端+微信官方)
微信官方:调用微信生成临时二维码接口(带scene_id),获取二维码图片URL;客户端:展示二维码,同时轮询/长连接请求服务端,查询扫描状态;服务端:
接收微信回调通知(扫描后微信推送scene_id和openid);存储扫描状态(Redis/数据库);响应客户端查询,返回“未扫描/已扫描/已授权”状态。
57. 异构词查找(判断两个字符串是否为异构词)?
异构词:字符种类和数量完全相同,顺序不同(如“listen”和“silent”)。
思路1(排序):将两字符串排序,比较排序后是否相等(时间O(nlogn));思路2(哈希统计):用数组/哈希表统计每个字符出现次数,比较统计结果(时间O(n),空间O(1),因字符集固定)。
58. 40亿个不重复数字排序(内存限制)?
最优方案:Bitmap(位图);原理:用1bit表示一个数字(0=不存在,1=存在),40亿数字仅需512MB内存;步骤:初始化Bitmap→ 遍历数字标记对应bit位→ 遍历Bitmap,输出bit位为1的数字(升序)。
59. 100亿个数找中位数?
思路:分桶法(按数字范围分桶,如0-1亿为一个桶);步骤:
遍历所有数,统计每个桶的数字个数;累加桶个数,找到中位数所在的桶;对目标桶内数字排序,找到中位数(目标桶数据量可控,可加载到内存)。
60. TXT文件上万行车牌号按字典序排序?(含非程序员方案)
(1)程序员方案
内存足够:读取所有车牌号到集合,调用排序API(如Java的);内存不足:外部排序(分块排序→ 归并排序)。
Collections.sort()
(2)非程序员方案
Windows:记事本打开→ 全选→ 插件排序(如Notepad++的“编辑→ 行操作→ 排序行”);Linux:(系统自带sort命令)。
sort 车牌号.txt > 排序后.txt
61. 为什么Redis用单线程还能这么快?
纯内存操作(无磁盘IO开销);单线程避免线程切换和锁竞争;IO多路复用(epoll)处理并发连接;高效数据结构(跳表、哈希表),查询/操作效率高。
62. MySQL索引为什么用B+树?不用B树或红黑树?
对比B树:B+树叶子节点链表化(支持范围查询),非叶子节点仅存索引(减少IO次数);对比红黑树:红黑树是二叉树,高并发下树高过高(IO次数多),B+树是多路平衡树,树高更低(百万数据树高仅3-4层)。
63. 什么是回表查询和索引下推?
回表查询:非聚簇索引(如普通索引)查询时,先查索引获取主键,再查聚簇索引获取完整数据(两次查询);索引下推:MySQL5.6+特性,将“过滤条件”在索引遍历阶段执行(而非回表后),减少回表次数(如,age过滤在索引层执行)。
where name like "张%" and age=20
64. 什么是深度分页?如何解决?
深度分页:(跳过1万条取20条),MySQL需扫描前10020条,效率低;解决方案:
limit 10000, 20
主键自增:(依赖主键有序);游标分页:用上次查询的最后一条数据作为游标(如
where id > 10000 limit 20);分表查询:按分片规则直接定位目标数据。
where create_time > '2024-01-01' limit 20
需要我把剩余的“跨专业考研原因”“项目难点”等面试场景题也整理成简短应答思路吗?可以帮你提炼核心应答逻辑,方便面试快速回应。
65. 为什么跨专业考研?本科专业是什么?(场景题)
应答思路:先明确本科专业(如“本科是XX专业”),再讲核心动机(对计算机/后端开发的兴趣+职业规划匹配),最后补充行动(自学Java/做项目,证明转行决心)。示例:“本科是环境工程,因对编程和分布式系统感兴趣,职业规划是做技术深耕,所以跨考计算机。备考期间自学Java、Spring生态,做了2个后端项目,具备扎实的技术基础。”
66. 项目中的难点是什么?如何解决的?(高频项目题)
应答思路:用“问题+分析+方案+结果”简化STAR法则,突出解决能力。示例:“难点是高并发下数据一致性(如订单库存)。分析后用Redis分布式锁防超卖,结合本地消息表保证MySQL与Redis同步,最终支持每秒5000+请求,无数据异常。”
67. 你认为最有成就的程序/项目是什么?(项目亮点题)
应答思路:聚焦“独特价值+个人贡献+结果”,避免泛泛而谈。示例:“最有成就感的是校园二手交易平台后端。独立设计分库分表方案,解决10万+用户数据存储问题,优化后查询响应从300ms降至50ms,累计1万+用户使用。”
68. 你的评价与不足是什么?(HR/技术面通用)
应答思路:优点贴合岗位(如“踏实、解决问题能力强”),不足真实不致命(如“技术深度不足”)+ 改进行动。示例:“优点是逻辑清晰,能快速定位问题;不足是分布式事务底层原理掌握不深,目前在啃RocketMQ事务消息源码,做了相关Demo实践。”
69. 对金融科技行业有哪些看法?(HR面)
应答思路:结合行业趋势(技术驱动+合规)+ 微众定位(民营银行标杆),体现认可度。示例:“金融科技核心是‘技术赋能金融’,既要用AI、分布式提升效率,也要重视数据安全和合规。微众作为行业标杆,微粒贷、微业贷的技术落地很亮眼,希望参与其中。”
70. 有其他offer吗?收到微众会优先选择吗?(HR面)
应答思路:坦诚不隐瞒+突出微众优先级,说明匹配点(技术、平台、业务)。示例:“目前有2个互联网公司offer,但微众是我的首选——金融科技赛道更契合我的职业规划,而且技术栈(Java、分布式)和团队氛围都很吸引我,希望长期深耕。”
71. 研究生/本科期间成绩最好/最差的两门课是什么?(HR面)
应答思路:好课讲“优势(如逻辑/钻研)”,差课讲“原因(如兴趣不符)+ 改进(如补学相关技能)”。示例:“最好的是《数据结构》(90+),喜欢算法逻辑,课后刷了LeetCode 200题;最差的是《XX理论》(65+),因兴趣不符投入不足,后来通过做项目补了相关实践能力。”
72. 作为新人,如何提升工作效率和质量?(场景题)
应答思路:分“学习+执行+复盘”,体现主动性和方法性。示例:“先快速熟悉业务和技术栈(看文档、问同事);执行时拆解任务、按优先级推进;完成后复盘问题(如bug原因),沉淀笔记,避免重复踩坑。”
73. 为什么大二/跨专业出来实习?(低年级/转专业场景)
应答思路:突出“主动性+匹配度+学习能力”,打消面试官顾虑。示例:“大二提前实习是想把课堂知识落地,之前自学了Java和Spring Boot,做了简易后端项目,能快速上手基础开发;希望通过实习积累金融科技相关经验,和微众的岗位高度匹配。”
74. 如何将微服务架构改造为单体架构?(架构设计题)
应答思路:核心“合并+简化”,分步骤说明,突出合理性。示例:“1. 合并服务代码到一个工程,共享数据库(删除跨服务调用);2. 移除网关、注册中心等组件,用本地调用替代;3. 简化配置(统一application.yml),保留核心业务逻辑,适配单体部署。”
75. 多线程题目:线程A和B同时对一个数加法,判断结果(如synchronized修饰方法)?(代码判断题)
应答思路:先判断是否线程安全,再讲核心原因(锁/原子性)。示例:“若加法方法用synchronized修饰,结果正确(保证原子性);若未加锁,结果可能小于预期(count++非原子操作,存在数据覆盖)。”
76. 为什么用了synchronized还要用数据库乐观锁?(并发场景题)
应答思路:区分“应用层锁”和“数据层锁”的作用边界。示例:“synchronized保证应用内线程安全,但若多服务部署(分布式场景),跨服务并发需数据库乐观锁(如version字段),防止多服务同时修改同一数据,双重保障一致性。”
77. 看过哪些底层源码?(技术深度题)
应答思路:选1-2个熟悉的(如Spring、Redis),讲“核心逻辑+收获”,避免泛泛而谈。示例:“看了Spring的BeanFactory源码,了解了IoC容器初始化流程(加载配置→实例化Bean→依赖注入);还看了Redis的String类型源码,清楚SDS动态扩容机制,帮我更好理解缓存原理。”
78. 开发/运维岗位竞争比例高,为什么觉得自己能胜任?(竞争场景题)
应答思路:匹配岗位需求(技能+特质),结合过往经历证明。示例:“虽然竞争激烈,但我掌握了Java、MySQL、Linux等核心技能,做过后端项目的部署和问题排查;性格细心负责,符合运维/开发对稳定性和严谨性的要求,能快速适应岗位。”
所有面试问题已整理完毕,涵盖技术题、场景题、HR题,均为简短应答思路,方便面试快速回应。需要我帮你整理成“面试高频题速记手册”(按模块分类,提炼核心关键词)吗?
微众银行Java后端面试高频题速记手册
(按模块分类,提炼核心关键词,面试前快速回顾)
一、Java核心基础(高频)
面向对象三大特性 → 封装(隐藏细节)、继承(复用)、多态(重写/重载)Object类常用方法 → equals/hashCode/toString/wait/notify/getClassString、StringBuilder、StringBuffer区别 → 可变性(不可/可/可)、线程安全(是/否/是)、场景(少改/单线程多改/多线程多改)List/Set/Map区别 → 存储(有序可重/无序不可重/键值对)、核心(索引/去重/键唯一)ArrayList初始化与扩容 → 无参10(JDK8延迟)、扩容1.5倍、数组拷贝线程创建方式 → 继承Thread、实现Runnable/Callable、线程池synchronized与Lock区别 → 自动/手动释放、公平性(无/可配置)、可中断(否/是)线程安全单例 → 饿汉(类加载创建)、DCL(双重检查+volatile)、静态内部类(延迟加载)ThreadLocal原理 → 线程私有ThreadLocalMap、弱引用key、避免内存泄漏(remove)线程池核心参数 → 核心线程数、最大线程数、空闲时间、阻塞队列、拒绝策略异常分类 → 受检查(IOException)、非受检查(RuntimeException)、OOM是Error四大引用 → 强(默认)、软(内存不足回收)、弱(GC必回收)、虚(跟踪回收)
二、JVM(高频)
运行时内存区域 → 堆(对象)、栈(栈帧)、方法区(类信息)、程序计数器(无OOM)垃圾判定 → 可达性分析(GCRoots:栈引用、静态变量等)垃圾回收算法 → 复制(新生代)、标记-整理(老年代)、分代收集(G1)垃圾收集器 → G1(默认,大堆、低延迟)、CMS(并发标记清除)、ZGC(超低延迟)类加载流程 → 加载→验证→准备→解析→初始化(主动使用触发)双亲委派模型 → 父加载器优先加载、防止核心类篡改、打破(Tomcat类加载器)OOM/StackOverflow → OOM(堆/元空间)、StackOverflow(递归过深)、排查(jmap/jstack/MAT)JMM → 主内存/工作内存、可见性(volatile)、原子性(synchronized)、有序性(禁止重排)
三、Spring/Spring Boot(高频)
IoC核心 → 容器管理Bean、依赖注入(@Autowired)、BeanFactory/ApplicationContextBean生命周期 → 实例化→属性注入→初始化(@PostConstruct)→销毁(@PreDestroy)AOP原理 → 动态代理(JDK接口/GLIB无接口)、切面/切入点/通知Spring事务 → 声明式(@Transactional)、传播机制(REQUIRED/REQUIRES_NEW)、失效场景(非public、异常捕获)Spring Boot自动配置 → @SpringBootApplication、@EnableAutoConfiguration、条件注解(@ConditionalOnClass)常用注解 → @Component/@Service/@Controller、@Bean、@Configuration
四、MySQL(高频)
索引类型 → B+树(聚簇/非聚簇)、复合索引(最左前缀)、哈希索引(等值)索引失效场景 → 函数操作、!=、like%开头、范围查询放后面事务隔离级别 → 可重复读(默认)、读已提交(避免脏读)、串行化(无并发)事务并发问题 → 脏读(未提交)、不可重复读(修改)、幻读(插入)MVCC原理 → 隐藏列(TRX_ID/ROLL_PTR)、undo日志、Read View(一致性视图)锁分类 → 行锁(InnoDB)、表锁(MyISAM)、间隙锁(防幻读)、死锁(循环等待)慢查询优化 → 加索引、避免索引失效、分库分表、explain分析(type/key)分库分表 → 水平(按行,用户ID取模)、垂直(按列,大字段)、中间件(Sharding-JDBC)Join主表选择 → 小表驱动大表(20万驱动100万)NULL与””区别 → 空间占用(是/否)、count(过滤NULL)
五、Redis(高频)
核心数据结构 → String(计数器)、Hash(对象)、List(队列)、Set(去重)、Sorted Set(排行榜)持久化 → RDB(快照,快)、AOF(日志,全量)、混合持久化(推荐)缓存问题 → 穿透(空值/布隆过滤器)、击穿(互斥锁/热点永不过期)、雪崩(随机过期/集群)数据一致性 → Cache-Aside(先更DB再删缓存)、延迟双删、过期时间兜底为什么快 → 纯内存、单线程、IO多路复用(epoll)、高效数据结构
六、分布式/中间件(中频)
消息中间件 → RocketMQ(事务消息)、Kafka(高吞吐)、RabbitMQ(路由灵活)消息幂等 → 唯一消息ID、幂等表、业务唯一标识(订单号)分布式事务 → TCC、本地消息表、事务消息、SAGA模式分布式锁 → Redis(SET NX EX)、Zookeeper(临时节点)、数据库(排他锁)CAP/BASE → CAP(最多两特性)、BASE(最终一致性)限流算法 → 令牌桶(Guava)、漏桶、滑动窗口、Redis+Lua服务熔断/降级 → 熔断(Sentinel,失败率触发)、降级(关闭非核心功能)
七、算法编程(必问)
快排实现 → 双边遍历、基准值选择、递归/非递归异构词查找 → 排序对比、哈希统计字符次数40亿不重复数字排序 → Bitmap(1bit/数,512MB)100亿数找中位数 → 分桶法(先定位桶,再排序桶内)死锁场景 → 两个事务交叉更新两行数据单例模式代码 → DCL+volatile、静态内部类线程安全加法 → synchronized、AtomicInteger、Lock
八、场景/HR题(必问)
项目难点 → 问题(高并发/一致性)+ 方案(锁/缓存)+ 结果(性能提升)最有成就项目 → 个人贡献+技术亮点+业务价值(用户/性能)优缺点 → 优点(踏实/解决问题强)、缺点(技术深度不足+改进行动)跨专业/低年级实习 → 兴趣+自学成果(项目/技能)+ 岗位匹配度其他offer选择 → 优先微众(技术/赛道/氛围)金融科技看法 → 技术赋能金融、合规+效率、微众标杆(微粒贷)新人提升 → 学文档+拆任务+复盘沉淀
需要我帮你生成“高频题核心应答模板”(每个题1-2句标准回答,直接套用)吗?

