微众银行 面试准备

微众银行 面试准备


汇总问题(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)
:判断两个对象是否“相等”。默认实现是“==”(比较内存地址),自定义类通常会重写(如比较对象的属性值)。
hashCode()
:返回对象的哈希码(int值)。默认实现与对象内存地址相关,重写
equals
时必须同步重写
hashCode
(保证equals相等的对象hashCode也相等)。
toString()
:返回对象的字符串表示(默认格式为“类名@哈希码十六进制”)。重写后可自定义输出(如“User{id=1, name=‘张三’}”),方便日志打印和调试。
getClass()
:返回对象的运行时类(Class对象)。作用是获取类的元信息(如类名、方法、属性),常用于反射。
clone()
:创建并返回对象的“浅拷贝”。使用时需让类实现
Cloneable
接口(标记接口,无实际方法),否则会抛
CloneNotSupportedException

wait()
:使当前线程释放对象锁,进入等待状态,直到被
notify()
/
notifyAll()
唤醒或超时。需在
synchronized
代码块中调用(否则抛
IllegalMonitorStateException
)。
notify()
/
notifyAll()
:唤醒当前对象锁上等待的一个(notify)或所有(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)
继承关系 继承
Exception
,非
RuntimeException
继承
RuntimeException
编译要求 必须显式处理(try-catch捕获或throws声明) 无需显式处理,编译不报错
常见场景 外部环境导致的问题(如文件不存在、数据库连接失败) 代码逻辑错误(如空指针、数组越界)
举例
IOException
(文件读取异常)、
SQLException
(数据库异常)

NullPointerException
(空指针)、
IllegalArgumentException
(参数非法)
(3)OutOfMemoryError的类型


OutOfMemoryError
属于
Error
(错误),而非“受检查异常”。它是JVM内存耗尽时抛出的严重错误,程序无法通过try-catch处理,只能通过JVM调优(如扩大堆内存)或优化代码(如避免内存泄漏)预防。

4. 自定义类时需要注意哪些事项?重写equals()和hashCode()的注意事项是什么?equals()相等的对象,hashCode()一定相等吗?

答:#### (1)自定义类的注意事项

类名符合“驼峰命名法”(首字母大写,如
User
而非
user
),避免与JDK自带类重名(如不命名为
String
)。属性私有化(用private修饰),通过public的get/set方法对外暴露(封装思想),避免直接暴露属性导致随意修改。若类需被序列化(如网络传输、持久化),需实现
Serializable
接口,并定义
serialVersionUID
(避免反序列化时因版本不一致报错)。重写
toString()
方法:默认
toString()
输出格式不直观,重写后可清晰展示对象属性(如“User{id=1, name=‘张三’}”),方便调试。若类需比较“内容相等”(而非内存地址),需重写
equals()

hashCode()
,且保证二者逻辑一致。

(2)重写equals()的注意事项

自反性:
x.equals(x)
必须返回true(对象自己等于自己)。对称性:若
x.equals(y)
为true,则
y.equals(x)
也必须为true(A等于B,B也等于A)。传递性:若
x.equals(y)

y.equals(z)
为true,则
x.equals(z)
也必须为true(A等于B,B等于C,A等于C)。一致性:若x和y的属性未修改,多次调用
x.equals(y)
结果必须一致(不随时间变化)。非空性:
x.equals(null)
必须返回false(任何对象都不等于null)。逻辑聚焦:仅比较“核心属性”(如User的id),避免比较临时属性(如
lastLoginTime
)。

(3)重写hashCode()的注意事项

一致性:若x和y的
equals()
返回true,则
x.hashCode()

y.hashCode()
必须相等(核心原则,否则HashMap等集合会出错)。离散性:若x和y的
equals()
返回false,
x.hashCode()

y.hashCode()
尽量不相等(减少哈希冲突,提高HashMap查询效率)。稳定性:若对象的属性未修改,
hashCode()
的返回值必须不变(避免HashMap中对象“丢失”)。

(4)equals()与hashCode()的关系

必须满足:若
x.equals(y)
为true,则
x.hashCode()
==
y.hashCode()
(否则HashMap无法正确存储和查找对象)。不强制满足:若
x.equals(y)
为false,
x.hashCode()
也可能等于
y.hashCode()
(即哈希冲突,HashMap通过链表/红黑树解决)。结论:
equals()
相等的对象,
hashCode()
一定相等
hashCode()
相等的对象,
equals()
不一定相等

5. String、StringBuilder、StringBuffer的区别是什么?为什么有了String还需要后两者?

答:三者核心区别在于可变性线程安全性,具体如下:

特性 String StringBuilder StringBuffer
可变性 不可变(底层是final char数组) 可变(底层是char数组,无final修饰) 可变(同StringBuilder)
线程安全 安全(不可变对象天然线程安全) 不安全(无同步锁) 安全(方法加synchronized锁)
性能 低(拼接时创建新对象,如
a+b
生成新String)
高(直接修改原数组,无锁开销) 中(有锁开销,比String快)
适用场景 字符串不频繁修改(如常量定义、固定文本) 单线程下频繁修改字符串(如循环拼接) 多线程下频繁修改字符串(如多线程日志拼接)
为什么需要StringBuilder和StringBuffer?

因为String是不可变对象:每次对String进行拼接(如
str += "a"
)、替换等操作时,都会创建一个新的String对象,原对象成为垃圾(需GC回收)。若频繁修改字符串(如循环拼接1000次),会产生大量临时对象,导致内存浪费和性能下降。

而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可重复
核心接口方法
get(int index)
(按索引取元素)、
add(E e)
(添加到末尾)

add(E e)
(添加,重复则失败)、
contains(E e)
(判断是否包含)

put(K k, V v)
(存键值对)、
get(K k)
(按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的初始化容量分两种情况,取决于构造方法:

无参构造(
new ArrayList<>()
):JDK1.7及之前默认初始容量为10;JDK1.8及之后优化为“延迟初始化”——初始时底层数组为
EMPTY_ELEMENTDATA
(空数组),第一次添加元素时才将容量扩容为10。有参构造(如
new ArrayList<>(10)
):初始容量为传入的参数值(如10);若传入
Collection
对象(如
new ArrayList<>(set)
),初始容量为该Collection的元素个数。

(2)扩容机制

当ArrayList的元素个数(
size
)达到当前容量(
capacity
)时,会触发扩容,步骤如下:

计算新容量:默认扩容为“当前容量的1.5倍”(公式:
newCapacity = oldCapacity + (oldCapacity >> 1)
,如10→15,15→22)。特殊情况处理:
若新容量小于“最小需求容量”(如添加大量元素时,需直接扩容到需求容量),则新容量=最小需求容量。若新容量超过ArrayList的最大容量(
Integer.MAX_VALUE - 8
,避免内存溢出),则新容量=
Integer.MAX_VALUE
(极端情况)。
数组拷贝:创建一个新容量的数组,将原数组的元素通过
Arrays.copyOf()
拷贝到新数组,底层数组引用指向新数组。

注意点

扩容是“按需触发”的,且每次扩容都会产生数组拷贝(有性能开销)。若已知元素个数(如1000个),建议使用有参构造指定初始容量(
new ArrayList<>(1000)
),避免多次扩容。

8. 线程的创建方式有哪些?线程的生命周期包含哪些状态?

答:#### (1)线程的创建方式(4种)
Java中创建线程的核心是“实现
Runnable
接口”或“继承
Thread
类”,衍生出4种常用方式:

继承
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
枚举中)


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
(调用
start()
) → (CPU调度)执行
run()

TERMINATED
(执行完毕);

RUNNABLE

BLOCKED
(竞争锁失败) →
RUNNABLE
(获取锁);

RUNNABLE

WAITING
/
TIMED_WAITING
(调用等待方法) →
RUNNABLE
(被唤醒/超时)。

9. Thread.sleep()会释放持有的锁吗?为什么?

答:不会释放。原因如下:

(1)Thread.sleep()的设计目的


sleep(long millis)
的作用是“让当前线程暂停执行指定时间”,期间线程放弃CPU使用权(进入
TIMED_WAITING
状态),但不释放已持有的资源(包括
synchronized
锁、Lock锁等)。设计初衷是“暂停执行”,而非“释放资源”——比如线程持有锁后需要等待某个时间(如1秒后执行下一步),若释放锁会导致其他线程抢占锁,破坏原有逻辑。

(2)对比“会释放锁”的方法


sleep()
不同,
Object.wait()
方法会释放当前对象的
synchronized


wait()
的设计目的是“让线程等待某个条件满足”(如等待队列不为空),释放锁后其他线程可修改条件(如往队列加元素),条件满足后再通过
notify()
唤醒线程并重新竞争锁。示例:若线程A持有
synchronized (lock)
锁,调用
lock.wait()
会释放锁;若调用
Thread.sleep(1000)
,则会保持锁1秒,期间其他线程无法获取
lock
锁。

结论


Thread.sleep()
:暂停执行,不释放锁,超时后自动恢复
RUNNABLE
状态。
Object.wait()
:暂停执行,释放
synchronized
,需被
notify()
唤醒。

10. 线程间的通信方式有哪些?

答:线程间通信的核心是“让线程感知其他线程的状态或数据变化”,常用方式有5种,具体如下:

等待/通知机制(wait()/notify()/notifyAll())

基于
Object
类的方法,需在
synchronized
代码块中调用(确保线程持有对象锁)。流程:线程A调用
lock.wait()
,释放
lock
锁并进入等待队列;线程B修改条件后调用
lock.notify()
,唤醒A线程;A线程重新竞争
lock
锁,获取后继续执行。适用场景:线程间需按“条件”协作(如生产者-消费者模型:生产者生产后通知消费者消费)。

join()方法


Thread.join()
的作用是“让当前线程等待目标线程执行完毕后再继续”。示例:主线程中调用
threadA.join()
,主线程会阻塞,直到
threadA

run()
方法执行完毕,主线程才恢复执行。适用场景:需保证线程执行顺序(如主线程需等待子线程计算完结果后再汇总)。

volatile关键字


volatile
修饰的变量具有“可见性”:一个线程修改变量后,其他线程能立即看到最新值(避免CPU缓存导致的“数据不一致”)。流程:线程A修改
volatile
变量
flag

true
,线程B循环读取
flag
,一旦读取到
true
则执行后续逻辑。适用场景:简单的状态传递(如用
volatile boolean stop
控制线程停止),不支持原子性(如
volatile int count

count++
仍需锁保证线程安全)。

管道流(PipedInputStream/PipedOutputStream)

基于IO流的通信方式,仅适用于“两个线程”之间的字节数据传输(如线程A写数据到
PipedOutputStream
,线程B从
PipedInputStream
读数据)。特点:传输数据是“双向的”,但需注意线程安全(避免同时读写导致死锁)。适用场景:线程间需传递二进制数据(如子线程处理文件流后传给主线程)。

并发工具类(如CountDownLatch、CyclicBarrier)

基于AQS实现的高级通信工具,适用于复杂场景:

CountDownLatch
:让一个线程等待多个线程执行完毕(如主线程等待5个子线程初始化完成,每个子线程完成后
countDown()
,主线程
await()
等待)。
CyclicBarrier
:让多个线程等待彼此到达“屏障点”后再共同继续(如3个线程都执行到
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)

原理:“延迟初始化”(第一次调用
getInstance()
时创建实例),通过“双重if检查+volatile”避免线程安全问题。代码:


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
不可省略——
INSTANCE = new Singleton()
分3步(1.分配内存;2.初始化实例;3.引用指向内存),若发生指令重排序(1→3→2),线程B可能获取到“未初始化的实例”并报错。优点:延迟初始化、线程安全、性能高(仅第一次创建时加锁);缺点:代码稍复杂。

静态内部类(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的核心区别

synchronized
是Java原生关键字,
Lock

java.util.concurrent.locks
包下的接口(需手动实现,如
ReentrantLock
),二者区别如下:

维度 synchronized Lock(以ReentrantLock为例)
锁的获取与释放 自动:进入
synchronized
代码块时获取锁,退出时自动释放(包括异常退出)
手动:需调用
lock()
获取锁,
unlock()
释放锁(必须在finally中调用,避免异常导致锁泄漏)
可中断性 不可中断:线程获取锁失败时,会一直阻塞(除非被中断,但需特殊处理) 可中断:支持
lockInterruptibly()
,线程阻塞时可通过
interrupt()
中断并退出阻塞
公平性 非公平锁:默认不保证线程获取锁的顺序(先请求的线程可能后获取),无法设置为公平锁 可配置:构造器可指定
fair=true
(公平锁,按请求顺序分配锁)或
fair=false
(非公平锁)
条件变量 仅支持一个条件变量(通过
wait()
/
notify()
/
notifyAll()
支持多个条件变量(通过
newCondition()
创建,如生产者-消费者模型中“空队列等待”和“满队列等待”可分开)
锁状态查询 无法查询:无API获取锁的持有状态、等待队列长度等 可查询:提供
isLocked()
(是否被锁)、
getQueueLength()
(等待队列长度)等方法
性能 JDK1.6后优化(如偏向锁、轻量级锁),性能接近Lock 性能稳定,在高并发下略优于synchronized(无JVM层面的优化,但可控性强)
(2)为什么需要Lock?


synchronized
虽简单易用(自动释放锁、低学习成本),但在复杂场景下存在局限性,Lock的出现正是为了弥补这些不足:

解决“死锁”的可能性
synchronized无法中断阻塞中的线程(若线程A持有锁,线程B一直阻塞等待,无法主动让B退出);而Lock的
lockInterruptibly()
支持中断,可在超时或特定条件下中断线程,避免死锁。

支持公平锁
synchronized仅是非公平锁,可能导致“线程饥饿”(某些线程长期得不到锁);Lock可配置为公平锁,按“先到先得”的顺序分配锁,适合对公平性有要求的场景(如金融交易排队)。

多条件变量分离
synchronized仅一个条件变量,若线程需等待不同条件(如生产者-消费者模型中,生产者等“队列不满”,消费者等“队列不空”),只能用一个等待队列,唤醒时需
notifyAll()
(唤醒所有线程,效率低);而Lock的多条件变量可分开等待队列,唤醒时只需唤醒对应条件的线程(如
notEmpty.signal()
只唤醒消费者),提升效率。

精细化锁控制
Lock提供锁状态查询(如
isHeldByCurrentThread()
判断当前线程是否持有锁)、超时获取锁(
tryLock(long timeout, TimeUnit unit)
,超时后放弃获取,避免无限阻塞)等功能,适合需要精细化控制的场景(如限时任务)。

总结

简单场景(如单线程安全的方法):用
synchronized
(代码简洁,不易出错)。复杂场景(如公平锁、多条件等待、超时控制):用
Lock
(灵活性高,可控性强)。

13. synchronized能否保证可见性?为什么?

答:能保证。原因与
synchronized
的“内存语义”和JVM的“内存屏障”机制有关。

(1)可见性的定义

可见性是指“一个线程修改共享变量后,其他线程能立即看到该变量的最新值”。若缺乏可见性,可能导致线程A修改了变量
x
,线程B仍读取到
x
的旧值(因CPU缓存未刷新到主内存)。

(2)synchronized保证可见性的原理

synchronized的内存语义包含两点,共同保证可见性:

锁释放时的“写回主内存”
当线程退出
synchronized
代码块(释放锁)时,JVM会强制将该线程在锁期间修改的所有共享变量,从线程的“工作内存”(CPU缓存、寄存器)刷新到“主内存”。

示例:线程A在
synchronized
块中修改
x=1
,释放锁时
x=1
会被写回主内存。

锁获取时的“读取主内存”
当线程进入
synchronized
代码块(获取锁)时,JVM会强制将该线程的“工作内存”中所有共享变量的值置为无效,后续读取这些变量时,必须从“主内存”重新加载最新值。

示例:线程B获取锁后,读取
x
时会从主内存获取
x=1
(而非工作内存中的旧值)。

(3)底层支撑:内存屏障

JVM通过在
synchronized
的“锁获取”和“锁释放”位置插入内存屏障(Memory Barrier),禁止指令重排序并强制刷新内存,从而保证可见性:

锁获取时:插入“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()

原理:
LockSupport
是JDK提供的线程阻塞工具,
park()
让当前线程暂停(进入
WAITING
状态),
unpark(Thread t)
唤醒指定线程。支持“先unpark后park”(不会导致永久阻塞)。代码示例(管理多个线程):


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

await()
方法让线程等待“计数器归0”,
countDown()
方法减少计数器。若计数器初始为1,线程调用
await()
时暂停,外部调用
countDown()
(计数器归0)时恢复。代码示例:


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()
会暂停线程但不释放锁,导致其他线程阻塞;
Thread.stop()
会强制终止线程,可能导致资源未释放(如文件流未关闭、锁未释放),JDK已明确废弃这两个方法。

保证暂停标志的可见性:若用标志位控制(如方式1),标志位必须用
volatile
修饰,或通过
synchronized
/
Lock
保证可见性,否则线程可能读取到旧的标志位值,无法暂停。

避免CPU空转:线程暂停时,不要用“空循环”(如
while(pause) {}
),应调用
Thread.sleep()

LockSupport.park()
让线程放弃CPU,减少资源消耗。

处理中断状态:若线程在暂停时被中断(如
interrupt()
),需保留中断状态(
Thread.currentThread().interrupt()
),避免后续逻辑无法感知中断(如线程需根据中断状态退出)。

线程安全的列表管理:若管理多个线程(如方式2),添加/删除线程的列表操作需加锁(如
synchronized

CopyOnWriteArrayList
),避免并发修改异常。

15. 线程A和线程B同时对同一变量做加法,如何保证结果正确(线程安全)?

答:线程A和线程B并发执行
count++
(非原子操作)时,会出现“数据覆盖”(如A读取count=1,B也读取count=1,均加1后写回,最终count=2而非3)。保证线程安全的核心是“将非原子操作变为原子操作”,常用方案有5种:

15. 线程A和线程B同时对同一变量做加法,如何保证结果正确(线程安全)?

答:线程并发执行
count++
时,因
count++
是“读取-修改-写入”非原子操作,易出现“数据覆盖”(如A、B同时读
count=1
,均加1后写回,最终
count=2
而非预期的
3
)。保证线程安全的核心是“将非原子操作原子化”或“避免并发修改共享变量”,常用方案有5种:

(1)使用
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
手动获取/释放锁,逻辑与
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

原理:JUC(
java.util.concurrent
)包下的原子类,通过CAS(Compare and Swap,比较并交换) 操作实现原子化修改,无需加锁(无锁并发)。
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
(任务结果异步汇总)

原理:将“加法任务”提交到线程池,每个任务返回计算结果(如单个线程加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

incrementAndGet()
等方法底层通过CAS自旋实现原子操作;内核级优化:JVM的“自适应自旋”(根据历史自旋成功率动态调整自旋次数,成功率高则增加次数,低则减少)。

17. ThreadLocal的原理是什么?使用时需注意哪些问题(如内存泄漏)?

答:#### (1)核心原理:线程私有存储
ThreadLocal的作用是“为每个线程提供独立的变量副本”,让线程操作自己的私有变量,避免共享变量的并发冲突。其底层依赖
Thread
类中的
ThreadLocalMap
实现,原理可概括为“3个核心组件”:

Thread类:每个
Thread
对象内部维护一个
ThreadLocalMap
(成员变量
threadLocals
,初始为null);ThreadLocalMap:线程私有的哈希表,key是
ThreadLocal
实例(弱引用),value是线程的私有变量值(强引用);ThreadLocal类:作为“工具类”,提供
get()
/
set()
/
remove()
方法,本质是操作当前线程的
ThreadLocalMap

原理流程(以
ThreadLocal.set(T value)
为例):

获取当前线程:
Thread currentThread = Thread.currentThread();
获取线程的
ThreadLocalMap

ThreadLocalMap map = currentThread.threadLocals;

map
为null,创建新的
ThreadLocalMap
并赋值给线程;若
map
不为null,以当前
ThreadLocal
实例为key,将
value
存入
map
(覆盖已有值);
ThreadLocal.get()
方法则相反:通过当前线程的
ThreadLocalMap
,以自身为key获取value,若不存在则调用
initialValue()
初始化(默认返回null)。

(2)关键设计:弱引用的key


ThreadLocalMap
的key(
ThreadLocal
实例)使用弱引用
WeakReference
),原因是:

若key是强引用:当外部不再使用
ThreadLocal
实例(如
tl = null
)时,
ThreadLocalMap
仍持有强引用,导致
ThreadLocal
实例无法被GC回收,引发内存泄漏;若key是弱引用:当外部
ThreadLocal
实例被回收(
tl = null
)时,弱引用的key会被GC标记为可回收,后续
ThreadLocalMap
清理时可删除对应的key-value对。

(3)使用时需注意的问题
① 内存泄漏(最核心问题)

泄漏原因:虽然key是弱引用,但
ThreadLocalMap
的value是强引用。若线程长期存活(如线程池的核心线程),且未调用
ThreadLocal.remove()
,则:

外部
ThreadLocal
实例被回收(key变为null);value的强引用仍被
ThreadLocalMap
持有,且线程不结束,value无法被GC回收;最终导致“key为null的value堆积”,引发内存泄漏。

解决方案

每次使用完
ThreadLocal
后,必须调用
remove()
方法删除value(建议在
finally
中调用,避免异常导致未执行);避免在长期存活的线程(如线程池核心线程)中使用
ThreadLocal
,或确保线程退出前清理
ThreadLocal
;JDK优化:
ThreadLocalMap

get()
/
set()
方法会自动清理“key为null的value”,但无法覆盖所有场景(如长期不调用
get()
/
set()
的线程)。

② 线程池中的“线程复用”问题

线程池的核心线程会复用,若前一个任务使用
ThreadLocal
后未
remove()
,下一个任务复用该线程时,会读取到前一个任务的value(脏数据)。

示例


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
中调用
tl.remove()
,清除当前线程的value。

③ 无法跨线程共享数据

ThreadLocal的变量是“线程私有”的,子线程无法读取父线程的
ThreadLocal
值(如主线程设置
tl.set("main")
,子线程
tl.get()
返回null)。

解决方案:若需跨线程共享(如父子线程),使用
InheritableThreadLocal
(继承
ThreadLocal
,会将父线程的
ThreadLocalMap
复制到子线程),但需注意“复制时机”(子线程创建时复制,后续父线程修改不影响子线程)。

④ 初始值的初始化方式


ThreadLocal
的默认初始值为null,若需自定义初始值,有两种方式:

重写
initialValue()
方法(JDK8前):


ThreadLocal<Integer> tl = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
        return 0; // 初始值为0
    }
};

使用
ThreadLocal.withInitial()
静态方法(JDK8+,更简洁):


ThreadLocal<Integer> tl = ThreadLocal.withInitial(() -> 0);

18. 线程池的核心参数有哪些?线程池的阻塞队列有哪些实现方式及特点?

答:线程池是“管理线程的容器”,通过复用线程减少线程创建/销毁的开销,核心是“核心参数”和“阻塞队列”的配合,实现对线程的动态管理。

(1)线程池的核心参数(7个,基于
ThreadPoolExecutor


ThreadPoolExecutor
的构造方法定义了7个核心参数,决定线程池的行为:


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)

用于创建线程的工厂(默认是
Executors.defaultThreadFactory()
);可自定义线程工厂,设置线程名称(如“pool-1-thread-1”)、优先级、是否为守护线程等(便于日志排查)。

拒绝策略(handler)

当“阻塞队列已满 + 线程数达到最大线程数”时,对新提交的任务采取的处理策略(默认是
AbortPolicy
,抛出
RejectedExecutionException
);JDK提供4种默认策略:

AbortPolicy
:直接抛出异常(默认,推荐用于需明确感知任务拒绝的场景);
CallerRunsPolicy
:由提交任务的线程(如主线程)自己执行任务(避免任务丢失,适合并发度低的场景);
DiscardPolicy
:直接丢弃任务(不抛异常,适合任务可丢失的场景);
DiscardOldestPolicy
:丢弃阻塞队列中最旧的任务(队列头部任务),然后提交新任务(适合任务时效性要求高的场景)。

(2)线程池的阻塞队列实现方式及特点

阻塞队列(
BlockingQueue
)是线程池的“任务缓冲池”,需实现“线程安全的入队/出队”,且支持“阻塞特性”(队列空时,出队线程阻塞;队列满时,入队线程阻塞)。JDK提供5种常用阻塞队列实现,适用于不同场景:

队列类型 底层结构 核心特点 适用场景

ArrayBlockingQueue
有界数组 1. 必须指定容量(无界构造器不存在);
2. FIFO(先进先出);
3. 支持公平/非公平锁(构造器指定
fair
参数)。
适合“任务量固定、需控制队列容量”的场景(如核心业务任务,避免队列无限堆积导致OOM)。

LinkedBlockingQueue
链表(节点) 1. 默认无界(容量为
Integer.MAX_VALUE
),可指定容量(推荐显式指定,避免OOM);
2. FIFO;
3. 入队/出队用两把锁(
takeLock

putLock
),并发性能高于
ArrayBlockingQueue
适合“任务量波动大,但需避免OOM”的场景(显式指定容量,如
new LinkedBlockingQueue<>(1000)
);
Executors.newFixedThreadPool()
默认使用此队列(无界,需谨慎)。

SynchronousQueue
无存储节点 1. 容量为0(不存储任务,仅作为“任务传递通道”);
2. 入队操作(
put()
)必须等待出队操作(
take()
),反之亦然(“同步移交”);
3. 支持公平/非公平模式。
适合“任务需立即执行,不允许缓冲”的场景(如
Executors.newCachedThreadPool()
,非核心线程空闲60秒销毁,避免线程堆积)。

PriorityBlockingQueue
优先级堆(数组) 1. 无界队列(容量可指定,但满时自动扩容);
2. 任务按优先级排序(默认自然排序,或自定义
Comparator
);
3. 优先级高的任务先执行(不保证FIFO)。
适合“任务有优先级差异”的场景(如紧急任务优先执行,如订单处理中的“VIP订单”)。

DelayQueue
优先级堆 1. 无界队列;
2. 任务必须实现
Delayed
接口(重写
getDelay(TimeUnit)
方法,指定延迟时间);
3. 只有任务“延迟时间到期”后,才能被出队执行。
适合“定时任务”场景(如定时清理缓存、定时发送消息,如
ScheduledThreadPoolExecutor
底层使用此队列)。
(3)核心参数与阻塞队列的配合逻辑(任务提交流程)

任务提交到线程池;若当前核心线程数 <
corePoolSize
:创建核心线程执行任务;若核心线程已满:将任务加入阻塞队列;若阻塞队列已满,且当前线程数 <
maximumPoolSize
:创建非核心线程执行任务;若线程数达到
maximumPoolSize
:触发拒绝策略;非核心线程执行完任务后,若空闲时间超过
keepAliveTime
:销毁非核心线程。

19. AQS(AbstractQueuedSynchronizer)的核心原理是什么?在哪些并发工具中被使用?

答:AQS是JUC包的“同步器基石”,定义了一套“基于状态和队列的同步框架”,几乎所有JUC并发工具(如
ReentrantLock

CountDownLatch
)都基于AQS实现。其核心是“用CAS管理同步状态,用双向链表管理等待线程”。

(1)AQS的核心组成

AQS的底层由“2个核心部分”构成,可概括为“一个状态 + 一个队列”:

同步状态(state)


volatile int state
修饰的变量,存储同步状态(如锁的持有次数、计数器值);线程通过
getState()

setState(int newState)

compareAndSetState(int expect, int update)
(CAS)操作状态,确保线程安全;不同同步工具对
state
的定义不同(如
ReentrantLock
中,
state=0
表示未锁定,
state>0
表示锁定次数;
CountDownLatch
中,
state
表示计数器值)。

双向同步队列(CLH队列)

全称“Craig, Landin, and Hagersten队列”,是一个双向链表,用于存储“获取同步状态失败的线程”;每个节点(
Node
)包含:

thread
:当前等待的线程;
prev
/
next
:前驱/后继节点,构成双向链表;
waitStatus
:节点状态(如
0
=初始态、
SIGNAL
=-1=当前节点的后继节点需被唤醒、
CANCELLED
=1=节点已取消等待);
队列特性:FIFO(先进先出),确保线程等待的公平性(默认非公平,可配置为公平)。

条件队列(Condition Queue)


ConditionObject
实现(AQS的内部类),用于“线程等待特定条件”(如
ReentrantLock

await()
/
signal()
);每个
Condition
对应一个单向条件队列,当线程调用
await()
时,从同步队列转移到条件队列;调用
signal()
时,从条件队列转移回同步队列,重新竞争同步状态。

(2)AQS的核心逻辑:获取与释放同步状态

AQS定义了模板方法(如
acquire(int arg)

release(int arg)
),子类需重写“钩子方法”(如
tryAcquire(int arg)

tryRelease(int arg)
)实现具体的同步逻辑。核心流程分为“获取状态”和“释放状态”:

① 获取同步状态(以独占锁为例,如
ReentrantLock

线程调用
acquire(1)

arg=1
表示获取1个单位的状态);调用子类重写的
tryAcquire(1)
:尝试用CAS修改
state
(如
state=0

1
),若成功则获取锁,直接返回;若失败则进入下一步;调用
addWaiter(Node.EXCLUSIVE)
:创建独占模式的
Node
,将当前线程封装为节点,加入同步队列的尾部(CAS确保线程安全);调用
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) 同一时间只有一个线程能获取同步状态(如锁),其他线程需等待。
ReentrantLock

Synchronized
(底层类似)
共享模式(Shared) 同一时间多个线程可获取同步状态(如计数器),线程获取后不排斥其他线程。
CountDownLatch

Semaphore

ReadWriteLock
(读锁)
(4)基于AQS实现的JUC并发工具

几乎所有JUC工具都依赖AQS,核心工具及AQS的使用方式如下:

并发工具 模式
state
的含义
核心钩子方法实现

ReentrantLock
(重入锁)
独占模式 锁的持有次数(
0
=未锁,
n
=已锁
n
次)

tryAcquire(1)
:CAS将
state

0

1
(非公平)或检查队列(公平);
tryRelease(1)

state
减1,为
0
时释放锁。

CountDownLatch
(倒计时器)
共享模式 计数器值(
0
=计数完成,
n
=剩余
n
次)

tryAcquireShared(int arg)
:若
state==0
则返回
1
(获取成功),否则返回
-1
(加入队列);
tryReleaseShared(int arg)
:CAS将
state
减1,为
0
时唤醒所有等待线程。

Semaphore
(信号量)
共享模式 可用信号量数量(
0
=无可用,
n
=
n
个可用)

tryAcquireShared(int arg)
:CAS将
state

arg
,若结果≥
0
则获取成功;
tryReleaseShared(int arg)
:CAS将
state

arg
,唤醒等待线程。

ReentrantReadWriteLock
(读写锁)
读锁(共享)、写锁(独占) 高16位=读锁次数,低16位=写锁次数 写锁
tryAcquire(1)
:检查读锁是否为
0
且写锁为
0
,CAS修改低16位;读锁
tryAcquireShared(1)
:检查写锁是否为
0
,CAS修改高16位。

CyclicBarrier
(循环屏障)
共享模式(间接依赖) 无直接
state
,内部用
ReentrantLock
+
Condition
实现
依赖AQS的
Condition
队列,线程调用
await()
时加入条件队列,直到所有线程到达屏障点后唤醒。
(5)AQS的核心优势

标准化同步框架:封装了“线程排队、阻塞/唤醒、CAS操作”等通用逻辑,子类只需重写少量钩子方法,即可实现复杂同步工具(避免重复开发);线程安全:基于
volatile
和CAS保证同步状态的线程安全,基于双向链表和
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的
SET NX
),锁的key为“消息ID或业务唯一标识”:
若获取锁成功:执行业务逻辑,释放锁;若获取锁失败:说明有其他线程正在处理该消息,当前线程直接返回成功(等待其他线程处理结果)。
注意:需设置锁的过期时间(避免死锁),且业务逻辑执行时间需小于锁过期时间(或实现锁续期)。

⑤ 消息中间件自身机制(辅助方案)

部分消息中间件提供幂等相关特性,可配合使用:

RocketMQ:支持“事务消息”和“消息重试次数限制”,避免无限重试;Kafka:通过
offset
机制确保消息有序消费,结合消费者组
group.id
避免重复消费(需正确提交
offset
);RabbitMQ:通过
messageId

correlationId
标记消息,结合死信队列处理消费失败的消息。

(4)总结

优先使用“唯一ID + 幂等表”(通用性最强,适合大多数场景);有天然业务唯一标识时,用“业务标识去重”(减少表设计);业务状态明确时,用“状态机校验”(逻辑更贴合业务);分布式锁作为补充方案,需注意锁的有效性和性能。

21. JVM的运行时内存区域包含哪些部分?哪个区域不会发生内存溢出?

答:JVM运行时内存区域是Java程序执行时内存分配和管理的区域,根据《Java虚拟机规范》(Java SE 8),分为5个核心区域,各自有明确的功能和生命周期。

(1)运行时内存区域及功能

程序计数器(Program Counter Register)

功能:记录当前线程执行的字节码指令地址(如分支、循环、跳转、异常处理等);特点
线程私有(每个线程有独立的程序计数器,互不干扰);是JVM中唯一不会发生OutOfMemoryError的区域(内存占用极小,由JVM直接管理,无需手动分配);若线程执行的是Native方法(本地方法),计数器值为
undefined
(Native方法不通过字节码执行)。

Java虚拟机栈(Java Virtual Machine Stacks)

功能:存储线程执行方法时的“栈帧”(Stack Frame),每个栈帧包含:
局部变量表(方法内的局部变量,如基本类型、对象引用);操作数栈(方法执行时的临时数据栈);动态链接(指向运行时常量池的方法引用);方法返回地址(方法执行完后回到的调用位置)。
特点
线程私有(与线程生命周期一致);栈深度有限制(默认1MB左右),若方法调用层级过深(如递归无终止条件),会抛出
StackOverflowError
;若JVM无法为新的栈帧分配内存(如线程数过多导致总栈内存超过上限),会抛出
OutOfMemoryError

本地方法栈(Native Method Stacks)

功能:与虚拟机栈类似,但专门为Native方法(如C/C++实现的方法)服务;特点
线程私有;可能抛出
StackOverflowError
(栈深度超限)和
OutOfMemoryError
(内存分配失败);具体实现由JVM厂商决定(如HotSpot虚拟机将本地方法栈与虚拟机栈合并实现)。

Java堆(Java Heap)

功能:存储所有对象实例和数组(是Java内存管理的核心区域);特点
线程共享(所有线程可访问堆中的对象);是垃圾收集器(GC)的主要工作区域(“GC堆”);内存分配动态扩展(通过
-Xms

-Xmx
参数设置初始和最大堆大小);若堆内存不足(无法为新对象分配空间,且GC后仍无足够空间),会抛出
OutOfMemoryError: Java heap space

方法区(Method Area)

功能:存储已被JVM加载的类信息(类名、字段、方法、接口)、常量、静态变量、即时编译器(JIT)编译后的代码等;特点
线程共享;Java 8前,方法区的实现为“永久代”(PermGen),受
-XX:PermSize

-XX:MaxPermSize
限制;Java 8及后,永久代被“元空间(Metaspace)”替代,元空间直接使用本地内存(Native Memory),默认无上限(可通过
-XX:MetaspaceSize

-XX:MaxMetaspaceSize
限制);若方法区无法分配内存(如加载过多类、常量池过大),会抛出
OutOfMemoryError: Metaspace
(Java 8+)或
OutOfMemoryError: PermGen space
(Java 7及前)。

(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
中的对象若在
finalize()
中重新建立与GCRoots的引用(如
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()

user
引用的对象);生命周期与类一致(类卸载前,该对象始终被GCRoots引用)。

方法区中常量引用的对象

编译期确定的常量(如
public static final String CONST = "abc"

CONST
引用的字符串对象);常量一旦初始化,引用关系不会改变,始终被视为GCRoots。

活跃线程对象(Thread实例)

正在运行的线程(
Thread
对象)及其关联的资源(如线程的栈、上下文);线程未终止前,始终作为GCRoots(否则线程相关资源可能被误回收)。

JNI(Java Native Interface)引用的对象

由JNI接口创建的全局引用对象(如通过
NewGlobalRef
创建的对象);这类对象需显式释放,否则会一直作为GCRoots存在(可能导致内存泄漏)。

(2)GCRoots与对象引用关系的确定方式

JVM通过“遍历对象引用链”确定GCRoots与对象的关系,具体流程如下:

收集GCRoots集合

GC触发时,JVM首先遍历上述GCRoots类型(如虚拟机栈的局部变量、静态变量等),收集所有GCRoots对象,形成初始根集合。

标记可达对象

从GCRoots出发,沿对象的“引用字段”(如对象的成员变量)递归遍历:
若对象A被GCRoots直接引用,则A是可达的;若对象B被A引用,则B是可达的;以此类推,所有被间接引用的对象均标记为“可达”(存活)。
遍历方式:采用“深度优先”或“广度优先”算法,确保所有可达对象被标记。

处理引用类型

Java中的引用分为4种(强引用、软引用、弱引用、虚引用),GCRoots对对象的引用类型会影响可达性判断:
强引用(如
User u = new User()
):GCRoots的强引用对象一定是可达的,不会被回收;软引用
SoftReference<User>
):内存不足时,软引用对象可能被回收(GCRoots的软引用在内存充足时视为可达);弱引用
WeakReference<User>
):无论内存是否充足,下次GC时弱引用对象会被回收(GCRoots的弱引用不视为可达);虚引用
PhantomReference<User>
):仅用于跟踪对象回收,不影响可达性。

(3)实际应用中的注意事项

内存泄漏排查:若对象长期无法被回收(内存泄漏),常因“无意识的GCRoots引用”(如静态集合缓存未及时清理,
static List<User> list
中的对象始终被GCRoots引用)。GC日志分析:通过
-XX:+PrintGCDetails
等参数输出GC日志,可观察GCRoots数量及可达对象占比,辅助优化内存使用。引用类型选择:根据业务场景选择引用类型(如缓存用软引用,临时数据用弱引用),避免不必要的GCRoots强引用导致内存浪费。

24. StackOverflowError和OutOfMemoryError的产生场景是什么?如何排查解决?

答:
StackOverflowError

OutOfMemoryError
是JVM中两种常见的致命错误,均与内存管理相关,但产生原因和场景不同,排查和解决方式也有差异。

(1)StackOverflowError(栈溢出)

定义:当虚拟机栈(或本地方法栈)的栈深度超过JVM允许的最大值时抛出,属于“栈内存不足”的错误。

产生场景

无限递归调用:方法递归调用时未设置终止条件,导致栈帧(每个递归调用生成一个栈帧)不断叠加,超过栈深度限制(如
public void f() { f(); }
);方法调用层级过深:非递归但调用链极长(如多层嵌套的方法调用,每层生成一个栈帧);单个栈帧过大:方法的局部变量过多或过大(如大数组作为局部变量),导致单个栈帧占用内存过大,总深度未达上限但总内存超限。

排查方法

查看错误堆栈(Stack Trace):错误信息会显示最后执行的方法和行号,通常可定位到递归或深层调用的代码(如
at com.example.Demo.f(Demo.java:5)
重复出现);分析调用链:通过IDE的“调用层次结构”功能,检查方法调用深度是否合理。

解决方式

修复递归逻辑:为递归添加终止条件(如
if (n == 0) return;
),避免无限递归;减少调用层级:将深层调用拆分为多个步骤(如用循环替代递归,或增加中间层处理);调整栈内存大小:通过
-Xss
参数增大栈内存(如
-Xss2m
,默认1MB左右),但需注意线程过多时总栈内存可能超限(导致OOM)。

(2)OutOfMemoryError(内存溢出)

定义:JVM无法为对象分配内存(且GC后仍无足够空间)时抛出,根据内存区域不同,有多种细分类型。

常见类型及产生场景

Java heap space(堆内存溢出)

场景:创建大量对象且未及时回收(如无限循环创建对象、内存泄漏);示例:
List<Object> list = new ArrayList<>(); while (true) { list.add(new Object()); }
(list始终引用对象,无法GC)。

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)
导出堆快照:通过
jmap -dump:format=b,file=heap.hprof <pid>
导出内存快照;分析快照:用MAT(Memory Analyzer Tool)或JProfiler分析,定位“内存泄漏对象”(如未释放的大集合、静态缓存)。
元空间溢出(Metaspace)
查看类加载数量:
jmap -clstats <pid>
统计已加载的类数量;检查动态类生成逻辑:如CGLib、反射是否过度使用。
线程创建失败
查看线程数量:
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
    }
}

解决
避免用静态集合存储大量临时对象;为缓存设置过期清理机制(如用
LinkedHashMap
实现LRU缓存,或使用Guava的
Cache
);定期调用
clear()
清理无用对象。

未关闭的资源(IO流、数据库连接等)

场景
FileInputStream

Connection
等资源未关闭,资源对象及其内部数据被JVM或底层库引用,无法回收。示例


public void readFile() {
    FileInputStream fis = null;
    try {
        fis = new FileInputStream("data.txt");
        // 读取数据...
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 未关闭fis,导致资源对象泄漏
}

解决
资源操作必须放在
try-with-resources
中(自动关闭,Java 7+);或在
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未清理

场景
ThreadLocal
的value是强引用,若线程长期存活(如线程池核心线程)且未调用
remove()
,value会一直被线程的
ThreadLocalMap
持有,导致泄漏(详见第17题)。解决
每次使用
ThreadLocal
后,在
finally
中调用
remove()
;线程池任务执行完毕后,清理线程的
ThreadLocal
数据。

第三方库使用不当

场景:部分第三方库(如日志框架、JSON解析库)若配置不当,可能缓存大量对象(如日志上下文、解析器实例)导致泄漏。解决
查阅库的官方文档,正确配置缓存大小和过期策略;避免在循环中频繁创建库的实例(如
new Gson()
,应复用单例)。

(3)内存泄漏的排查工具与流程

监控内存趋势:通过
jconsole

VisualVM
监控堆内存使用,若内存持续增长且GC后不下降,可能存在泄漏;导出堆快照:用
jmap -dump:format=b,file=heap.hprof <pid>
导出内存快照;分析快照:用MAT(Memory Analyzer Tool)的“Leak Suspects”功能定位泄漏对象,查看引用链(找出谁在持有无用对象);修复代码:根据引用链定位到具体代码,移除不必要的引用(如清理集合、关闭资源)。

(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个隐藏列:


DB_TRX_ID
:最近一次修改该数据的事务ID(事务开始时分配的唯一ID,自增);
DB_ROLL_PTR
:回滚指针,指向该数据的上一个版本(存储在undo日志中);
DB_ROW_ID
:默认自增的行ID(当表无主键时,作为聚簇索引的主键)。

undo日志(数据版本的存储载体)

当事务修改数据时,InnoDB会先将数据的“旧版本”写入undo日志(如UPDATE前的原始数据);undo日志按“事务ID”和“回滚指针”串联,形成数据的版本链(最新版本在表中,历史版本在undo日志中);示例:数据行被3个事务依次修改,版本链为“当前版本(trx3)→ 版本2(trx2)→ 版本1(trx1)→ NULL”。

Read View(一致性视图,判断版本可见性)
Read View是事务启动时生成的“视图”,记录当前活跃的事务ID(未提交的事务),用于判断数据版本是否对当前事务可见。核心规则:

数据版本的
DB_TRX_ID
< Read View中最小活跃事务ID:该版本是“已提交事务”修改的,可见;数据版本的
DB_TRX_ID
> Read View中最大活跃事务ID:该版本是“未来事务”修改的,不可见;数据版本的
DB_TRX_ID
在活跃事务ID范围内:若该事务ID是当前事务自身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锁(读-写、写-写均冲突);加锁方式:
SELECT ... FOR UPDATE;
(手动加锁),或INSERT/UPDATE/DELETE自动加X锁。

(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(记录锁)

锁定具体的数据行(如
WHERE id=1
锁定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内置死锁检测机制(通过“等待图”判断是否存在循环等待),检测到死锁后,会选择“事务权重小”(如修改行数少、执行时间短)的事务回滚,释放锁,让其他事务继续执行;查看死锁日志
SHOW ENGINE INNODB STATUS;
(在“LATEST DETECTED DEADLOCK” section中查看死锁详情)。

(4)死锁的避免策略(破坏4个必要条件)

破坏“循环等待条件”:统一事务的锁申请顺序(如所有事务都按“id从小到大”的顺序更新数据);
示例:事务A和B都先更新id=1,再更新id=2,避免循环等待。
破坏“持有并等待条件”:事务启动时一次性申请所有需要的锁(如批量更新时,用
WHERE id IN (1,2)
一次性锁定多行);
注意:需避免锁粒度过大(如全表锁)。
设置事务超时时间:通过
innodb_lock_wait_timeout
(默认50秒)设置锁等待超时时间,超时后事务自动回滚;
示例:
SET GLOBAL innodb_lock_wait_timeout=10;
(超时时间设为10秒)。
避免长事务:长事务会持有锁时间过长,增加死锁概率,尽量将事务拆分为短事务(如“查询-修改-提交”一气呵成)。使用低隔离级别:如“读已提交”隔离级别下,InnoDB的锁竞争更少(Next-Key Lock失效,仅用Record Lock),死锁概率降低。

33. Redis的数据结构有哪些?各自的适用场景是什么?

Redis支持多种高性能数据结构,每种结构都有特定的底层实现和适用场景,是Redis灵活高效的核心。

(1)核心数据结构(5种基础+3种扩展)

String(字符串)

底层实现:动态字符串(SDS),支持扩容(预分配空间减少内存碎片);核心特性:存储字符串、数字(整数/浮点数),支持原子操作(如自增、自减);常用命令:
SET key value

GET key

INCR key
(自增)、
APPEND key str
(追加);适用场景:缓存用户信息(JSON字符串)、计数器(文章阅读量)、分布式锁(
SET NX EX
)。

Hash(哈希)

底层实现:哈希表(字典),小数据量时用压缩列表(节省内存);核心特性:存储“键值对集合”(如对象的多个属性),支持单独操作某个字段;常用命令:
HSET key field value

HGET key field

HGETALL key
(获取所有字段);适用场景:缓存对象(如用户信息:id、name、age)、购物车(用户ID→商品ID→数量)。

List(列表)

底层实现:双向链表(支持两端高效操作),小数据量时用压缩列表;核心特性:有序(按插入顺序)、可重复,支持两端插入/删除(O(1))、中间查询(O(n));常用命令:
LPUSH key value
(左插)、
RPOP key
(右删)、
LRANGE key start end
(获取区间元素);适用场景:消息队列(生产者LPUSH,消费者RPOP)、排行榜(最新评论列表)。

Set(集合)

底层实现:哈希表(判断元素是否存在O(1));核心特性:无序、不可重复,支持集合运算(交集、并集、差集);常用命令:
SADD key member
(添加元素)、
SISMEMBER key member
(判断存在)、
SINTER key1 key2
(交集);适用场景:标签(用户兴趣标签)、好友关系(共同好友:交集运算)、去重(抽奖用户去重)。

Sorted Set(有序集合)

底层实现:跳表(Skip List)+ 哈希表,跳表支持快速排序和范围查询;核心特性:有序(按“分数”排序)、不可重复,支持按分数/排名查询;常用命令:
ZADD key score member
(添加元素+分数)、
ZRANGE key start end
(按排名查询)、
ZSCORE key member
(获取分数);适用场景:排行榜(游戏积分排名)、带权重的消息队列(按优先级消费)、范围查询(查询分数80-100的用户)。

BitMap(位图)

底层实现:字符串(SDS),每个字节存储8个bit位;核心特性:存储二进制位(0/1),高效节省内存(如存储100万用户的状态,仅需125KB);常用命令:
SETBIT key offset value
(设置bit位)、
GETBIT key offset
(获取bit位)、
BITCOUNT key
(统计1的个数);适用场景:用户签到(日期→bit位,1=签到)、状态标记(用户是否在线)。

HyperLogLog(基数统计)

底层实现:概率数据结构,基于伯努利试验估算基数;核心特性:不存储具体元素,仅统计“不重复元素个数”(基数),误差率约0.81%;常用命令:
PFADD key element
(添加元素)、
PFCOUNT key
(统计基数);适用场景:UV统计(网站独立访客数)、搜索关键词去重统计(无需存储具体关键词)。

Geo(地理信息)

底层实现:Sorted Set(将经纬度编码为分数,按距离排序);核心特性:存储地理坐标(纬度、经度),支持距离计算、范围查询;常用命令:
GEOADD key longitude latitude member
(添加坐标)、
GEODIST key member1 member2
(计算距离);适用场景:附近的人(查询距离当前用户10公里内的商家)、地理围栏(范围筛选)。

(2)数据结构选择原则

存储单个值(如字符串、数字)→ String;存储对象(多属性)→ Hash(比String更灵活,支持单独修改属性);有序且需两端操作→ List;无序去重或集合运算→ Set;有序且需排序/排名→ Sorted Set;二进制状态或大数据量去重统计→ BitMap/HyperLogLog;地理坐标相关→ Geo。

34. Redis的持久化机制(RDB和AOF)的区别和优缺点?

Redis是内存数据库,数据默认存储在内存中,重启后数据丢失。持久化机制通过将内存数据写入磁盘,保证数据不丢失,核心有两种方式:RDB和AOF。

(1)RDB(Redis Database)

定义:在指定时间间隔内,将内存中的“全量数据”快照(Snapshot)写入磁盘(生成.rdb文件);触发方式
手动触发:
SAVE
(同步,阻塞Redis)、
BGSAVE
(异步,fork子进程执行,不阻塞);自动触发:配置文件中设置
save <seconds> <changes>
(如
save 60 1000
: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后发现数据不存在,在缓存中存储“空值”(如
key=-1, value=null
),设置短期过期时间(如5分钟),避免重复穿透;布隆过滤器(Bloom Filter):将所有存在的有效数据(如用户ID)存入布隆过滤器,查询前先通过过滤器判断数据是否存在,不存在则直接返回,无需查询缓存和DB;
原理:布隆过滤器是概率数据结构,通过多个哈希函数将数据映射到bit位,判断“不存在”时100%准确,“存在”时可能有误差(可通过调整参数降低)。

(2)缓存击穿(Cache Breakdown)

定义:查询“热点数据”(如热门商品详情),缓存中该数据刚好过期,此时大量并发请求穿透到DB,DB瞬间压力剧增;解决方案
互斥锁(分布式锁):缓存过期时,只有一个线程能获取锁查询DB,其他线程等待,查询结果写入缓存后释放锁,其他线程从缓存获取数据;
实现:用Redis的
SET NX EX
命令获取分布式锁;
热点数据永不过期:热点数据不设置过期时间,通过后台线程定期更新缓存(如每小时更新一次);缓存预热:系统启动时,提前将热点数据加载到缓存中,避免运行时缓存过期。

(3)缓存雪崩(Cache Avalanche)

定义:大量缓存数据在同一时间过期,或缓存服务(如Redis)宕机,导致所有请求穿透到DB,DB不堪重负而宕机;解决方案
过期时间随机化:为缓存数据设置过期时间时,添加随机值(如
expire = 30分钟 + 随机(0-10分钟)
),避免大量数据同时过期;缓存集群部署:采用Redis集群(如主从+哨兵、Redis Cluster),避免单点故障(某节点宕机,其他节点正常提供服务);服务降级/熔断:缓存失效时,通过降级策略返回默认数据(如“系统繁忙,请稍后重试”),或熔断停止调用DB,避免DB宕机;DB限流:通过限流组件(如Guava RateLimiter)限制DB的并发请求数,避免DB被压垮。

(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后,用
SETNX
(仅当key不存在时写入)写入缓存(避免覆盖已更新的缓存)。

延迟双删

流程:写操作→更DB→删缓存→延迟1秒(根据业务调整)→再次删缓存;适用场景:解决“DB更新后,缓存删除前,已有请求读取旧值并写入缓存”的问题(延迟删缓存可覆盖旧值)。

分布式锁

核心思路:写操作时获取分布式锁,确保同一时间只有一个线程更新DB和删缓存,避免并发冲突;适用场景:高并发写场景(如秒杀商品库存更新)。

(3)总结

优先选择Cache-Aside策略(实现简单,平衡一致性和性能);兜底方案:为缓存设置合理的过期时间(即使出现不一致,过期后自动恢复);高并发场景:结合延迟双删、分布式锁,进一步保证一致性。

我可以帮你整理剩余问题(Spring IoC、AOP、事务原理、Spring Boot自动配置等)的详细答案,需要继续推进吗?

37. Spring IoC容器的核心原理是什么?Bean的生命周期是什么?

(1)IoC核心原理

IoC(控制反转)是Spring核心,核心是“将对象创建、依赖注入的控制权交给Spring容器”,而非手动
new
对象。

核心组件:
BeanFactory
(IoC容器顶层接口,延迟初始化)、
ApplicationContext
(BeanFactory子类,立即初始化,功能更全);实现流程:加载配置(XML/注解)→ 解析Bean定义→ 容器初始化Bean→ 依赖注入(DI)→ 提供Bean实例。

(2)Bean生命周期(核心阶段)

实例化:容器通过构造器创建Bean对象;属性注入:注入依赖的Bean(@Autowired/setter方法);初始化:执行
@PostConstruct
注解方法、
InitializingBean
接口的
afterPropertiesSet()
、XML配置的
init-method
;就绪:Bean可被应用调用;销毁:容器关闭时,执行
@PreDestroy
注解方法、
DisposableBean
接口的
destroy()
、XML配置的
destroy-method

38. Spring AOP的核心概念和实现原理是什么?

(1)核心概念

切面(Aspect):封装横切逻辑(如日志、事务)的类;连接点(JoinPoint):程序执行点(如方法调用、字段赋值);切入点(Pointcut):筛选连接点的规则(如
execution(* com..*Service.*(..))
);通知(Advice):切面的具体逻辑(前置@Before、后置@After、返回@AfterReturning、异常@AfterThrowing、环绕@Around);目标对象(Target):被增强的原始对象。

(2)实现原理

底层:动态代理,默认优先JDK动态代理(目标类实现接口),无接口时用CGLIB动态代理(子类增强);流程:扫描切面→ 解析切入点→ 为目标对象创建代理→ 调用目标方法时,代理类织入通知逻辑。

39. Spring事务的实现方式和传播机制是什么?

(1)实现方式

声明式事务(推荐):通过
@Transactional
注解或XML配置,Spring自动管理事务(AOP织入);编程式事务:通过
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
(类存在)、
@ConditionalOnMissingBean
(Bean不存在)等,按需生效;配置优先级:用户配置(application.yml/properties)> 自动配置类默认值。

41. 消息中间件的核心作用是什么?常用的消息中间件有哪些?

(1)核心作用

解耦:服务间通过消息通信,无需直接依赖;异步:非核心流程异步处理(如下单后发送通知),提升响应速度;削峰填谷:高并发场景下,消息队列缓冲请求,避免下游服务被压垮;可靠通信:保证消息的投递、消费确认(ACK)、重试机制。

(2)常用消息中间件对比
中间件 优点 缺点 适用场景
RocketMQ 高吞吐、低延迟、支持事务消息 生态不如Kafka完善 金融、电商等核心业务
Kafka 超高吞吐、适合大数据 不支持事务消息,重试复杂 日志收集、大数据场景
RabbitMQ 路由灵活、支持多种交换机 高并发下性能略弱 中小型系统、路由复杂场景

42. 分布式事务的解决方案有哪些?

分布式事务是跨多个服务/数据库的事务一致性问题,核心方案:

2PC(两阶段提交):协调者分“准备阶段”和“提交阶段”,强一致但性能低、阻塞风险高;TCC(Try-Confirm-Cancel):业务层面拆分事务(预留资源→ 确认提交→ 取消回滚),无锁但开发成本高;本地消息表+消息队列:本地事务与消息写入同库(原子性),异步通知其他服务,最终一致;事务消息(RocketMQ支持):消息先预发送(半消息),本地事务成功后确认发送,失败则回滚;SAGA模式:长事务拆分为短事务,通过补偿操作回滚(如下单→ 支付→ 发货,支付失败则取消订单)。

43. 分布式锁的实现方式和优缺点是什么?

(1)核心实现方式

Redis分布式锁:通过
SET NX EX key value
(原子操作),加锁时设置过期时间,解锁时用Lua脚本保证原子性;
优点:高性能、易实现;缺点:需处理锁超时、主从切换数据不一致问题。
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种引用类型(从强到弱),用于灵活控制对象生命周期:

强引用:默认引用(
User u = new User()
),GC不会回收被强引用的对象,OOM根源;软引用(SoftReference):内存不足时GC回收,适合缓存(如图片缓存);弱引用(WeakReference):下次GC必回收,适合临时数据(如ThreadLocal的key);虚引用(PhantomReference):仅用于跟踪对象回收,必须结合ReferenceQueue,无实际引用意义。

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
:查看进程(
ps -ef | grep java
查找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
(目录);按修改时间查找:
find /root -mtime -3
(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 车牌号.txt > 排序后.txt
(系统自带sort命令)。

61. 为什么Redis用单线程还能这么快?

纯内存操作(无磁盘IO开销);单线程避免线程切换和锁竞争;IO多路复用(epoll)处理并发连接;高效数据结构(跳表、哈希表),查询/操作效率高。

62. MySQL索引为什么用B+树?不用B树或红黑树?

对比B树:B+树叶子节点链表化(支持范围查询),非叶子节点仅存索引(减少IO次数);对比红黑树:红黑树是二叉树,高并发下树高过高(IO次数多),B+树是多路平衡树,树高更低(百万数据树高仅3-4层)。

63. 什么是回表查询和索引下推?

回表查询:非聚簇索引(如普通索引)查询时,先查索引获取主键,再查聚簇索引获取完整数据(两次查询);索引下推:MySQL5.6+特性,将“过滤条件”在索引遍历阶段执行(而非回表后),减少回表次数(如
where name like "张%" and age=20
,age过滤在索引层执行)。

64. 什么是深度分页?如何解决?

深度分页:
limit 10000, 20
(跳过1万条取20条),MySQL需扫描前10020条,效率低;解决方案:
主键自增:
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句标准回答,直接套用)吗?

© 版权声明

相关文章

暂无评论

none
暂无评论...