谈谈你对面向对象的看法

1
2
3
4
封装:保证了独立性,安全性
继承:避免了大量重复代码,提升开发效率
多态:提高代码的灵活和可扩展性
抽象:强调了子类必要的核心

线程池的核心参数

1
核心线程数,最大线程数,任务队列,线程存活时间,时间单位,拒绝策略,线程工厂

核心线程数和最大线程数的区别

1
2
3
1. 核心线程数:是线程池长期维持的线程数量,即便这些线程空闲,也不会轻易被销毁,随时待命处理任务

2. 最大线程数:是线程池能创建的线程上限。当任务队列已满,且当前活动线程数小于最大线程数时,线程池会创建新线程来处理任务。这用于应对突发的高并发任务。

什么样的情况需要设置索引

1
2
3
1. 频繁查询字段:当某个字段经常在WHERE子句中作为查询条件时
2. 连接字段:在多表连接操作中,用于连接的字段设置索引可提升连接效率
3. 排序字段:若查询结果需要按某个字段排序,对该排序字段创建索引有助于快速排序

你了解的一些索引优化的方式

1
2
3
4
1. 聚焦高频查询字段:仅在常出现在WHERE、JOIN、ORDER BY子句的字段上建索引。
2. 前缀索引:对于长字符串字段,可使用前缀索引。
3. 联合索引:遵循最左前缀原则:联合索引按字段顺序使用,查询条件需从左到右匹配。
4. 覆盖索引:查询字段都包含在索引中,查询时无需回表操作

设计模式了解过吗

1
2
3
4
5
6
7
8
9
10
11
创建型模式
单例模式:确保一个类仅有一个实例,并提供全局访问点。比如数据库连接池,使用单例模式保证整个应用程序只有一个连接池实例,避免资源浪费。实现方式有饿汉式、懒汉式、双重检查锁定等。
工厂模式:包括简单工厂、工厂方法和抽象工厂。将对象创建和使用分离,通过一个工厂类负责创建对象。如游戏开发中,使用工厂模式创建不同类型的游戏角色,方便代码维护和扩展新角色类型。

结构型模式
代理模式:为其他对象提供一种代理以控制对这个对象的访问。例如在网络访问中,通过代理服务器访问目标网站,代理服务器可进行权限控制、缓存等操作。
装饰器模式:动态地给一个对象添加一些额外的职责。像给咖啡添加不同配料(牛奶、糖等),通过装饰器模式,可灵活组合配料而不改变咖啡类的核心代码。

行为型模式
观察者模式:定义对象间的一种一对多依赖关系,当一个对象状态发生改变时,所有依赖于它的对象都得到通知并自动更新。社交媒体平台上,用户发布内容,关注该用户的其他用户会收到通知,这就应用了观察者模式。
策略模式:定义一系列算法,将每个算法封装起来,并使它们可以相互替换。例如电商系统的促销活动,不同促销策略(满减、折扣等)可通过策略模式实现,便于灵活切换和扩展新策略。

动态代理了解吗

1
2
3
4
动态代理是一种在运行时创建代理对象的机制,对比静态代理在编译期就确定代理类,它更具灵活性。
新对话
动态代理是一种设计模式,它允许在运行时动态创建代理类,并在不修改原始类代码的情况下,为原始类的方法添加额外功能(如日志记录、权限验证、事务管理等)。
AOP(面向切面编程):在不修改目标对象代码情况下,为其添加通用功能,如日志记录、事务管理。

讲一下反射

1
反射是 Java 的一项强大特性,它允许程序在运行时获取、检查和操作类的各种信息,包括类的构造函数、方法、字段等,并且能够在运行时创建对象、调用方法。

SQL慢查询

1
2
3
4
5
6
1. 开启慢查询日志定位慢查询sql语句
2. Explain 分析原因
type: ALL:表示全表扫描,未使用索引。
key: NULL:确认没有命中任何索引。
rows: 4500000:预计扫描 450 万行,与慢查询日志中的 Rows_examined 一致。
Extra: Using where; Using filesort:表示需要在内存 / 磁盘中排序,进一步消耗资源。

set nx 是怎么实现的?他的底层机制是什么

1
2
1. 基于字典(Hash Table)的存储结构
2. 单线程模型保证原子性

Java 的基本数据类型

1
2
3
4
1. 整数:byte short int long
2. 浮点:float double
3. 字符:char
4. 布尔:boolean

抽象类可以被final修饰吗

1
2
3
4
5
在 Java 中,抽象类不能被 final 修饰,这是由两者的语义和设计目的决定的:
抽象类的核心作用是作为父类,供其他类继承并实现其抽象方法(抽象类中至少包含一个抽象方法,或被声明为抽象类)。它本身不完整,需要子类来完善实现。
final 修饰符的作用是限制类不能被继承(对于类)、方法不能被重写(对于方法)、变量值不能被修改(对于变量)。

报错: 编译错误:非法的修饰符组合: abstractfinal

final有什么用

1
2
3
4
修饰类:禁止类被继承
修饰方法:禁止方法被重写
修饰变量:禁止变量值被修改(不可变)

抽象类和普通类的区别

1
2
3
4
5
定义:抽象类用abstract class,普通类用class
实例化:抽象类不能直接new,普通类可以
方法:抽象类可包含无实现的抽象方法(abstract),普通类只能有具体实现的方法
继承:抽象类必须被继承,子类需实现所有抽象方法;普通类可被继承,子类可选择性重写方法
目的:抽象类作为模板,定义规范 + 共性;普通类提供完整独立的功能实现

ArrayList和LinkedList有什么区别

1
2
ArrayList:基于动态数组实现,内部通过数组存储元素,支持随机访问。 1.5 倍扩容
LinkedList:基于双向链表实现,每个元素包含前驱和后继节点的引用,不支持随机访问。

run 和 star 的区别

1
2
3
4
5
6
7
8
9
10
start() 方法
作用:启动一个新线程,使线程进入就绪状态(等待 CPU 调度),由 JVM 自动调用该线程的 run() 方法。
特性:
只能调用一次,多次调用会抛出 IllegalThreadStateException(线程状态不适合特定操作)。
调用后会创建新的线程(与主线程并行执行)。
run() 方法
作用:定义线程要执行的任务逻辑(相当于线程的 “任务体”)。
特性:
可直接调用(像普通方法一样),但此时不会创建新线程,仅在当前线程中同步执行。
多次调用无限制。

线程中的ThreadLocal是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
哈希表数据结构
ThreadLocal 是 Java 中用于实现线程本地存储的工具类,它能为每个线程创建一份独立的变量副本,确保线程之间的变量互不干扰(threadLocals中的key)。
内部结构:ThreadLocal 本身不存储数据,而是通过线程的 Thread 对象中的 threadLocals(一个 ThreadLocalMap 类型的哈希表)存储数据。以threadLocal为Key将Value储存在当前线程threadLocals中
存储机制:
当调用 threadLocal.set(value) 时,会以当前 ThreadLocal 实例为键,值为 value,存入当前线程的 threadLocals 中。
调用 threadLocal.get() 时,会从当前线程的 threadLocals 中取出对应的值。
线程隔离性:每个线程的 threadLocals 是独立的,因此不同线程通过同一个 ThreadLocal 操作的是各自的副本。

2. 内存泄漏:内存泄漏的核心原因是 ThreadLocalMap 中 Value 的强引用无法被释放
Key 的弱引用特性
ThreadLocalMap 中的 Key(ThreadLocal 实例)被设计为弱引用(WeakReference)。这意味着:当外部不再有强引用指向 ThreadLocal 实例时,即使 ThreadLocalMap 中仍有弱引用,该 ThreadLocal 实例也会被垃圾回收(Key 变为 null)。
Value 的强引用导致泄漏
虽然 Key(ThreadLocal 实例)会被回收,但 Value(线程变量)是强引用,且被 ThreadLocalMap 持有。如果此时线程仍在存活(如线程池中的核心线程长期不销毁),Thread 对象本身不会被回收,进而导致:
Thread → threadLocals → Entry → Value 的强引用链始终存在。
即使 Key 已为 null,Value 也无法被垃圾回收,造成内存泄漏。
解决:使用完threadLocal后手动清除(Remove)

Runnable和Callable有什么区别

1
2
Runnable:适用于无需返回结果的任务,是早期 Java 多线程的基础接口。只允许内部捕获异常,不允许抛出
Callable:适用于需要返回结果或需要处理异常的任务,允许抛出异常

HTTP请求中GET和POST有什么区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.核心
GET:用于获取资源(如查询数据),强调 “读取” 操作。安全性低。
例:访问网页、查询商品列表(/products?category=book)。

POST:用于提交数据(如创建、修改资源),强调 “写入” 操作。安全性高。
例:用户注册、提交表单、上传文件。

2. 数据传递方式
GET:
数据通过URL 明文传递,格式为 ?key1=value1&key2=value2(查询参数)。受 URL 长度限制
POST:
数据放在请求体(Body) 中传递,格式灵活(如表单、JSON、二进制等)。
例:{"username":"test","password":"123"}

Java中的异常捕获处理

1
2
3
4
5
1. try-catch:捕获并处理异常
2. finally: 确保释放
3. try-with-resources:自动释放资源
4. throws:声明方法可能抛出的异常
5. throw:手动抛出异常

@transactional关键字

1
2
3
4
5
6
7
8
9
10
1. propagation(传播行为)
控制多个带事务的方法相互调用时,事务如何传递,常用值:
REQUIRED(默认):若当前有事务,则加入;若无,则新建事务。
REQUIRES_NEW:无论当前是否有事务,都新建一个独立事务(原事务暂停)。
SUPPORTS:若当前有事务,则加入;若无,则以非事务方式执行。
2. rollbackFor(指定回滚异常)
默认情况下,事务仅在抛出未受检异常(RuntimeException 及其子类)时回滚,受检异常(如 IOException)不回滚。通过 rollbackFor 可指定需要回滚的异常
3. readOnly(只读事务)
对纯查询方法设置 readOnly = true,Spring 会优化事务性能(如关闭写操作支持)
4. 内部若捕获异常但没有抛出不会回滚

进程和线程的区别

1
2
3
4
5
6
进程:是资源分配的基本单位,是一个独立运行的程序实例(如一个打开的浏览器、一个 Java 程序),拥有独立的内存空间、文件描述符等系统资源。进程的独立性强

线程:是CPU 调度的基本单位,属于进程的一部分,多个线程共享所属进程的资源(内存、文件等),仅拥有独立的栈、程序计数器等少量私有资源。独立性弱

进程是独立的 “程序容器”,线程是容器内的 “执行单元”。多进程强调隔离性,多线程强调协作与高效。

Java线程池处理任务:核心线程数是5,最大线程数是100,任务队列是100,同时提交200个任务,执行过程是什么样的?

1
2
3
4
1. 提交1 - 5任务,创建5个核心线程处理任务
2. 6 - 105任务提交,进入任务队列
3. 提交106 - 200,任务队列慢->创建新的非核心线程
4. 任务完成后销毁非核心线程

int和Integer的区别

1
2
3
4
1. int是基本数据类型,Integer 是包装类,属于引用类型
2. int存储在栈内存中,Integer 储存在堆内存中,纯粹对象引用
3. int默认值为0,Integer 默认值为null

spring怎么解决循环依赖?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. Spring 通过三级缓存 + 提前暴露未完全初始化的 bean,解决了单例 bean 的 setter / 字段注入循环依赖。核心是在 bean 实例化后立即暴露其工厂对象,允许依赖方提前引用,待双方都完成初始化后,最终在一级缓存中存储完全可用的 bean。

2. 解决循环依赖的流程(以 A 依赖 B,B 依赖 A 为例)
初始化 A:
调用 A 的构造器创建实例(未设置属性,未执行初始化方法)。
将 A 的工厂对象(ObjectFactory)放入三级缓存(singletonFactories),用于后续提前暴露 A。
A 需要注入 B:
检查一级缓存,发现 B 不存在,开始初始化 B。

初始化 B:
调用 B 的构造器创建实例,同样将 B 的工厂对象放入三级缓存。
B 需要注入 A,此时检查一级缓存(A 未完成)→ 检查二级缓存(A 未在这)→ 检查三级缓存(找到 A 的工厂对象)。
通过 A 的工厂对象创建 A 的提前暴露实例(未完全初始化),放入二级缓存(earlySingletonObjects),并从三级缓存移除 A 的工厂。
B 注入 A 的提前暴露实例,完成 B 的属性注入和初始化,B 成为完全初始化的 bean,放入一级缓存(singletonObjects)。
A 完成初始化:
B 已存在于一级缓存,A 注入 B,完成属性注入和初始化,A 成为完全初始化的 bean,放入一级缓存,同时从二级缓存移除 A。

3. 三级缓存的存储对象
一级:存储完全初始化完成的单例 bean(最终可用的 bean)。
二级:存储提前暴露的未完全初始化的单例 bean(已实例化,但未注入属性和执行初始化方法)。
三级:存储bean 工厂对象(ObjectFactory),用于延迟创建提前暴露的 bean。

Spring和Spring Boot的区别

1
2
3
4
5
6
7
8
9
10
11
12
1. Spring:
需要手动引入 Spring MVC、Servlet API 等依赖,编写 XML 配置(如 DispatcherServlet 配置),并部署到
Spring Boot:
只需引入 spring-boot-starter-web 依赖,无需额外配置,内置 Tomcat,直接运行主类即可

2. Spring:
需手动配置数据源、事务管理器等
Spring Boot:
引入 spring-boot-starter-jdbc 后,只需在 application.properties 中配置连接信息,自动创建数据源

3. Spring 框架没有固定的 “启动类” 概念,通常需要手动创建并初始化 Spring 容器
Spring Boot 提供了标准化的启动类,通过 @SpringBootApplication 注解简化所有配置,底层自动完成容器初始化、组件扫描、自动配置等工作,实现 “一键启动”。

redis的事务和mysql的事务区别

1
2
3
4
5
6
7
8
9
10
11
MySQL 事务:完全支持 ACID 特性(原子性、一致性、隔离性、持久性)
原子性:通过 undo log 保证,要么全部执行,要么全部回滚
隔离性:通过锁机制和 MVCC 实现不同隔离级别(读未提交、读已提交、可重复读、串行化)
持久性:通过 redo log 保证数据写入磁盘
一致性:由原子性、隔离性和持久性共同保障

Redis 事务:仅支持部分原子性,不保证隔离性和完整的持久性
原子性:要么全部执行,要么全部不执行(但执行过程中出错不会回滚)
隔离性:事务执行时不会被其他客户端打断,但没有隔离级别概念
持久性:依赖持久化配置(RDB/AOF),事务本身不保证 (RDB为快照持久化,AOF是记录读写操作持久化)
一致性:在有限范围内保证,复杂场景可能破坏0

redis线程模型

1
单线程+多路IO复用

redis哨兵模式,如何判断挂没挂(心跳+Gossip)

1
2
3
4
1. 单个哨兵检测:哨兵通过 PING 命令检测所有节点,将无响应节点标记为 SDOWN。
2. 哨兵间信息同步:哨兵通过 Gossip 协议在频道中广播自己对主节点的 SDOWN 判断。
3. 达成客观下线共识:当超过 quorum 数量的哨兵都认为主节点 SDOWN 时,标记主节点为 ODOWN。
4. 触发故障转移:一旦主节点被标记为 ODOWN,哨兵集群会选举出一个 leader 哨兵,由其执行主从切换操作(将某个从节点升级为新主节点)。

redis缓存淘汰策略

1
2
3
4
1. LRU最近最少使用(分键有无过期时间)
2. LFU最不经常使用(分键有无过期时间)
3. 不淘汰数据,默认报错
4. 在所有键中随机淘汰数据

mysql的delete、truncate、drop

1
2
3
1. delete为命令行指令,用于删除表中的部分或全部行,保留表结构
2. truncate用于清空表中数据,不删除表结构
3. 用于删除整个表(包括表结构、数据、索引、触发器等),或其他数据库对象(如数据库、视图、索引等)

mysql持久化怎么实现的

1
2
3
4
InnoDB 的持久化 = redo log(保障修改不丢) + 双写缓冲(保障页完整) + 缓冲池(提升性能) + 异步刷脏页(最终存储)。
1. InnoDB 并非直接将数据写入磁盘文件,而是先写入内存中的 缓冲池(Buffer Pool),这是为了减少磁盘 I/O 开销(内存读写速度远快于磁盘)
2. redo log 是 InnoDB 实现持久化的核心,它记录的是 “数据页的修改动作”(而非最终数据),例如 “将表 users 的第 100 页第 5 行数据的 age 从 20 改为 30”。
3. undo log 记录的是 “数据修改前的原始状态”,例如 “将表 users 第 100 页第 5 行的 age 从 20 改为 30” 的 undo log 是 “将该位置的 age 从 30 改回 20”。

什么是MVCC以及MVCC怎么实现的?可重复读,读已提交,读未提交,串行化怎么实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1. MVCC(多版本并发控制)是 InnoDB 存储引擎实现读写并发控制的核心机制。它通过为数据记录生成多个版本,允许读写操作互不阻塞(读不加锁,写不阻塞读),从而提高数据库的并发性能。
2. InnoDB实现
隐藏列:每行含 DB_TRX_ID(事务ID)。记录最后一次修改该数据的事务 ID
undo log:存储数据历史版本,形成版本链,用于回滚到历史版本
Read View:事务读时生成的快照,含活跃事务 ID 等信息,用于判断版本可见性。事务中每次执行 SELECT 时,都会重新生成一个 Read View。
3.隔离等级:
1)读未提交:
不使用 MVCC 的版本控制,直接读取最新的数据行。
写操作加排他锁(X 锁),但读操作不加锁,也不生成 Read View(可见性快照)。
因此,未提交的修改会立即被其他事务看到,可能导致 “脏读”。

2)读已提交:
依赖 MVCC,但 每次执行 SELECT 时都会生成新的 Read View(可见性快照)。
Read View 会过滤掉未提交事务的修改,只允许看到已提交事务的版本。
解决了 “脏读”,但可能出现 “不可重复读”(同一事务中多次读取结果不一致)。

3)可重复读:
依赖 MVCC,且 事务启动时生成一次 Read View 并复用,直到事务结束。
无论其他事务是否提交新修改,当前事务始终使用初始 Read View 判断可见性,因此能重复读取到相同版本的数据。
解决了 “脏读” 和 “不可重复读”,默认避免 “幻读”。

4)串行化:
不依赖 MVCC,直接通过 表级锁 强制事务串行化。
读操作加共享锁(S 锁),写操作加排他锁(X 锁),且读写锁互斥。
一个事务持有锁时,其他事务必须等待锁释放才能执行,因此不会有任何并发问题。

volatile解释

1
2
3
4
5
6
7
8
9
10
volatile 是编程语言中用于修饰变量的关键字,主要作用是确保变量的内存可见性和禁止指令重排序,常用于多线程并发场景,防止因编译器优化或 CPU 缓存导致的线程安全问题。

1. 内存可见性
当一个变量被 volatile 修饰时,任何线程对该变量的修改会立即同步到主内存,同时其他线程读取该变量时会直接从主内存加载最新值,避免线程读取到 CPU 缓存中的旧数据。

2. 禁止指令重排序
编译器或 CPU 为优化性能可能会对指令重排序,volatile 会阻止这种重排序,保证代码执行顺序与源码一致(通过内存屏障实现)。
例子:一个get方法若发现没有对象实例则创建(new 一个)。此时cpu为了优化,jvm可能会先将引用指到被分配的空间但没有创建实例。某一时刻线程A在没有volatile的情况下会先将引用指到空地址。此时线程B来执行get发现已经有了引用则会返回那个空地址。线程并发出现问题。若加上volatile则会严格按照顺序进行,不会出现线程安全问题。

3. new的步骤:分配空间,创建实例化对象,将引用指向内存

synchronized底层实现原理,具体说Monitor的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
1. synchronized 的本质是通过 获取和释放 Monitor 锁 控制线程对临界区的访问.获取Monitor进入代码块,释放离开代码块

2. Monitor 本质是一个 C++ 结构体
_owner:
指向当前持有 Monitor 的线程。当线程获取锁成功时,JVM 会将 _owner 设为当前线程,同时 _count 加 1(支持重入)。
_count:
记录锁的重入次数。例如,同一线程多次进入同步代码块时,_count 会递增(每次加 1),退出时递减,直到 _count 为 0 时释放锁(_owner 设为 null)。
_EntryList:
存放正在竞争锁但未获取到的线程(处于 BLOCKED 状态)。当持有锁的线程释放锁后,JVM 会从 _EntryList 中唤醒一个线程,让其尝试获取锁。
_WaitSet:
存放调用 wait() 方法后阻塞的线程(处于 WAITING 状态)。这些线程已释放锁,需等待其他线程调用 notify() 或 notifyAll() 后被唤醒,重新进入 _EntryList 竞争锁。
_waiters:
记录 _WaitSet 和 _EntryList 中的总线程数,用于统计等待情况。

阻塞队列有哪些

1
2
3
4
5
6
7
8
9
10
11
12
1. ArrayBlockingQueue
特点:基于有界数组实现的阻塞队列,容量固定(创建时必须指定大小)。
2. LinkedBlockingQueue
特点:基于链表实现的阻塞队列,默认容量为 Integer.MAX_VALUE 2^31 - 1(可视为无界队列,也可指定容量)。
3. SynchronousQueue
特点:不存储元素的同步队列(容量为 0),每个插入操作必须等待对应的删除操作,反之亦然。
4. PriorityBlockingQueue
特点:基于优先级堆实现的无界阻塞队列,元素按优先级排序(需实现 Comparable 接口或指定 Comparator)。
5. DelayQueue
特点:基于 PriorityBlockingQueue 实现的延迟队列,元素必须实现 Delayed 接口,只有当延迟时间到期后才能被取出。
6. LinkedTransferQueue
特点:基于链表的无界阻塞队列,支持 transfer() 方法(插入元素后阻塞,直到有线程取走该元素),功能比 SynchronousQueue 更丰富。

bitMap的数据结构

1
2
3
4
5
BigMap 的底层基于 哈希表(Hash Table) 思想实现,但针对区块链特性做了优化:
键(Key):支持多种类型(字符串、整数、地址、字节数组等),需满足可哈希性和可比较性。
值(Value):可以是任意 Tezos 支持的数据类型(包括嵌套结构、其他 BigMap 等)。常见0 1
哈希索引:通过键的哈希值快速定位数据,避免全表扫描,查询复杂度接近 O (1)。

数据库的记录读取出来变成对象的背后实现

1
2
3
4
5
1. ORM(对象关系映射)的原理主要围绕建立映射关系、动态 SQL 生成、对象与数据转换、缓存机制这几个关键环节
2. 类与表的映射:在使用 ORM 时,开发人员需要在代码中定义与数据库表对应的类。以 Java 为例,通过@Entity 注解来标识一个类对应数据库中的表
3. 增删改操作:当在程序中调用 ORM 框架的方法来执行插入、更新、删除操作时,框架会根据配置的映射关系和传入的对象数据,自动生成对应的 SQL 语句。比如在 Java 的 Spring Data JPA 中,定义一个ProductRepository接口继承JpaRepository,通过调用save方法保存对象
4. 从数据库数据到对象:执行 SQL 查询后,数据库返回结果集。ORM 框架会遍历结果集,按照映射关系,将每一行数据转换为对应的对象实例,并为对象的属性赋值

mybatis的#和$区别

1
2
3
4
5
6
1. #:在 SQL 语句预编译之前,MyBatis 会将#{} 替换为占位符(?),然后进行预编译和参数设置。
2. $:${} 是在 MyBatis 将 SQL 语句和参数进行解析和拼接的时候,直接将参数值替换到 SQL 语句中,然后再执行 SQL。

SELECT * FROM ${tableName} -- 会生成SELECT * FROM user_2023 若From查询表名的情况则正确
SELECT * FROM #{tableName} -- 会生成SELECT * FROM 'user_2023'

讲讲你对Spring、SpringMVC、SpringSpringBoot的理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
1. spring:
Spring 是一个一站式的企业级应用开发框架,诞生于 2002 年,其核心目标是简化 Java 开发,解决传统 Java 开发中的复杂性(如代码耦合、配置繁琐等)

核心思想与功能:
1) IoC(控制反转):
这是 Spring 的核心思想。传统开发中,对象的创建和依赖关系由开发者手动控制(如 new UserService());而 IoC 则将对象的创建、管理和依赖注入交给 Spring 容器,开发者只需定义对象和依赖关系,由容器 “反向” 注入依赖。

2) AOP(面向切面编程):
允许将日志、事务、安全等横切关注点从业务逻辑中分离出来,单独定义和管理,实现代码复用和解耦。
例如:通过 @Transactional 注解即可为方法添加事务管理,无需手动编写事务控制代码。

3)生态扩展:
Spring 本身提供了基础功能,同时支持与其他技术整合(如 ORM 框架、缓存、消息队列等),形成了庞大的生态。例如:
数据访问:Spring JDBC、Spring Data JPA(整合 Hibernate 等 ORM)

2. SpringMVC:基于 Spring 的 Web 框架
SpringMVC 是 Spring 生态中的Web 层框架,专注于处理 HTTP 请求,实现 MVC(Model-View-Controller)设计模式,是 Java Web 开发的主流选择。

核心功能:
1) 请求处理流程:
DispatcherServlet:前端控制器,接收所有 HTTP 请求,负责请求的分发。
HandlerMapping:根据请求路径找到对应的处理器(Controller 中的方法)。
Controller:业务处理器,处理请求并返回数据(Model)。
ViewResolver:将 Controller 返回的逻辑视图名解析为实际视图(如 JSP、HTML)。

2) 关键注解:
@Controller:标记类为控制器。
@RequestMapping:映射请求路径(支持 GET/POST 等方法)。
@RequestParam:获取请求参数。
@ResponseBody:将返回值直接转为 JSON 等格式(无需视图解析)。

3) 优势:
与 Spring 无缝集成,通过 IoC 管理控制器和依赖,简化了 Web 层开发;支持 RESTful 风格 API,适合前后端分离架构。

3. SpringBoot:简化 Spring 应用开发的 “脚手架”
SpringBoot 诞生于 2014 年,其核心目标是 **“约定优于配置”**,简化 Spring 应用的搭建、配置和部署流程,让开发者快速启动一个生产级别的应用。

核心特性:
1) 自动配置(Auto-configuration):
基于类路径下的依赖(如引入 spring-boot-starter-web 依赖),SpringBoot 自动配置相关组件(如 Tomcat 服务器、SpringMVC 核心组件等),无需手动编写 XML 或 Java 配置。

2) 嵌入式服务器:
默认集成了 Tomcat、Jetty 等嵌入式服务器,无需单独部署 WAR 包到外部服务器,直接通过 java -jar 命令即可运行应用

3) 简化部署:
支持打包为可执行 JAR 包,结合 CI/CD 工具可快速部署;提供 Actuator 组件,方便监控应用健康状态、指标等。

Spring Bean的生命周期是怎样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
一、Spring Bean 的完整生命周期
1. 容器初始化阶段(Bean 创建与初始化)
1). 实例化 Bean(Instantiation)
容器通过反射调用 Bean 的构造方法(或工厂方法)创建 Bean 实例(内存中分配空间)。

2). 属性注入(Populate Properties)
容器根据配置(XML / 注解)将依赖的属性值或其他 Bean 注入到当前 Bean 中(如@Autowired、setter方法注入)。

3). 调用 Aware 接口方法
若 Bean 实现了Aware系列接口,容器会注入对应的容器资源:
BeanNameAware:注入当前 Bean 的名称(setBeanName())。
BeanFactoryAware:注入当前 BeanFactory(setBeanFactory())。
ApplicationContextAware:注入当前 ApplicationContext(setApplicationContext())。

4). BeanPostProcessor 前置处理(postProcessBeforeInitialization)
所有BeanPostProcessor的postProcessBeforeInitialization方法被调用,可对 Bean 进行预处理(如修改属性、生成代理等)。

5). 初始化方法(Initialization)
执行 Bean 的初始化逻辑,优先级如下:
若 Bean 实现InitializingBean接口,调用afterPropertiesSet()方法。
若在配置中指定了自定义初始化方法(如@Bean(initMethod="xxx")或 XML 的init-method),调用该方法。

6). BeanPostProcessor 后置处理(postProcessAfterInitialization)
所有BeanPostProcessor的postProcessAfterInitialization方法被调用,可对初始化后的 Bean 进行增强(如 AOP 代理就是在此阶段生成)。

7). Bean 就绪
此时 Bean 已完全初始化,可被容器或其他 Bean 使用。

2. 容器销毁阶段(Bean 销毁)
1). 触发销毁信号
容器关闭时(如ApplicationContext.close()),触发 Bean 的销毁流程。

2). 调用 DisposableBean 接口方法
若 Bean 实现DisposableBean接口,调用destroy()方法。

3). 调用自定义销毁方法
若在配置中指定了自定义销毁方法(如@Bean(destroyMethod="xxx")或 XML 的destroy-method),调用该方法。

4). Bean 销毁
资源释放(如关闭连接、释放内存等),Bean 生命周期结束。

4. MySQL的索引为什么使用B+树而不是B树或哈希表?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. 为什么不选择哈希表?
哈希表通过哈希函数将键映射到存储地址,等值查询效率极高(O (1)),但存在致命缺陷:
1). 不支持范围查询:哈希表存储无序,无法高效处理WHERE age > 20 AND age < 30这类范围条件,需全表扫描。
2). 不支持排序:索引本身无法维护数据顺序,ORDER BY操作需额外排序,成本高。
3). 哈希冲突问题:大量重复键会导致冲突,需要链表或红黑树处理,影响性能稳定性。
而 MySQL 的核心场景(如分页、区间筛选、排序)严重依赖有序性和范围查询能力,哈希表无法满足。

2. 为什么不选择 B 树?
B 树是多路平衡树,每个节点同时存储键值 + 数据,但相比 B + 树有明显劣势:
1). 查询效率不稳定:B 树的键和数据混存,若查询命中非叶子节点可快速返回,若命中叶子节点则需多访问几层,性能波动大。
2). 范围查询低效:B 树的叶子节点不相连,范围查询需多次回溯父节点,无法连续扫描。
3). 空间利用率低:非叶子节点存储数据会占用大量空间,导致树的高度更高(相同数据量下,B 树比 B + 树高 2-3 层),增加磁盘 IO 次数(数据库性能瓶颈在于磁盘 IO)。

3. 为什么选择 B + 树?
B + 树是 B 树的优化版,所有数据仅存于叶子节点,且叶子节点通过双向链表连接,完美适配数据库需求:
1). 查询效率稳定:无论查询哪个数据,都必须遍历到叶子节点,查询路径长度固定(IO 次数稳定)。
2). 范围查询高效:叶子节点形成有序链表,范围查询(如BETWEEN、IN)可直接通过链表连续扫描,无需回溯。
3). 空间利用率高:非叶子节点仅存键值(不存数据),能容纳更多键,树的高度更低(通常 3-4 层),大幅减少磁盘 IO(机械硬盘单次 IO 耗时约 10ms, 层数减少直接提升效率)。
4). 天然支持排序:叶子节点按键值有序排列,ORDER BY操作可直接利用索引顺序,无需额外排序。

syschronized 和 reentrantlock 的区别

1
2
3
4
5
6
7
8
9
10
11
1. sys底层是jvm,reentrantlock底层是AQS(state + 双向链表)
2. 两锁都可重入
3. sys不能中断,reentrantlock可以通过 lockInterruptibly() 响应中断
4. sys代码结束自动释放,reentrantlock需要手动释放
5. sys是非公平锁,reentrantlock可指定公平锁 / 非公平锁(构造器参数 fair=true
6. sys不能超时退出,reentrantlock可以通过trylock超时退出
7. sys不能查询锁状态,reentrantlock可以通过 isLocked()、isHeldByCurrentThread() 等方法查询
8. JDK 1.6 之前:synchronized 性能较差(重量级锁,依赖操作系统互斥量),ReentrantLock 性能更优。
JDK 1.6 及之后:JVM 对 synchronized 进行了大量优化(如偏向锁、轻量级锁、锁消除、锁粗化),其性能与 ReentrantLock 接近,甚至在某些场景下更优(JVM 对原生关键字的优化空间更大)。
9. sys不支持多条件等待,reentrantlock可以通过Condition来多条件等待
10. 主要根据代码是否需要公平锁,超时退出,中断,多条件等待来选择哪种

Syschronized的锁升级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
偏向锁 → 轻量级锁 → 重量级锁。根据线程竞争的激烈程度,动态切换锁的类型。所有锁的状态都存储在锁对象的对象头(Mark Word) 

1. 偏向锁 —— 无竞争时的优化:
适用场景:只有一个线程反复进入同步块,无其他线程竞争。
核心逻辑:“偏向” 第一个获取锁的线程,后续该线程再次进入时,无需竞争,直接通过 “Mark Word 标记” 确认身份即可,几乎无开销。

1) 线程 A 第一次进入同步块,JVM 检查对象 Mark Word (对象头)是 “无锁状态”。
2) 通过 CAS 操作,将 Mark Word 中的 “锁标志位” 设为 1(偏向锁),并记录线程 A 的 ID(存入 Mark Word)。
3) 后续线程 A 再次进入同步块时,只需对比 Mark Word 中的线程 ID 是否为自己:
是:直接进入(“偏向” 生效,无需 CAS,开销极低)。
否:触发偏向锁撤销,进入轻量级锁流程。
4) 撤销条件:当有其他线程(如线程 B)尝试获取该锁时,JVM 会暂停线程 A,检查线程 A 是否还在执行同步块:
若线程 A 已退出:将 Mark Word 重置为无锁状态,线程 B 可竞争。
若线程 A 仍在执行:偏向锁撤销,升级为轻量级锁。



2. 轻量级锁 —— 低竞争时的优化
适用场景:多个线程交替进入同步块(无同时竞争,即 “自旋等待” 可解决),避免阻塞线程(阻塞 / 唤醒的开销远大于自旋)。

1) 加锁过程:
当偏向锁被撤销升级为轻量级锁时,只有持有偏向锁的线程 A会在栈帧中创建锁记录(Lock Record),并通过 CAS 将对象头 Mark Word 指向该锁记录(标志位设为 00
线程 B 尝试获取锁时,会先在自己的栈帧创建锁记录,然后尝试 CAS 将对象头 Mark Word 指向自己的锁记录
线程B获取到锁进入代码块。此时线程A不会竞争锁

2)解锁过程修正:
线程 A 释放轻量级锁时,通过 CAS 将对象头恢复为 Displaced Mark Word
若 CAS 成功:表示没有其他线程竞争,直接释放锁
若 CAS 失败:说明线程 B 已触发锁膨胀(此时对象头已指向重量级锁的 monitor),线程 A 会释放重量级锁并唤醒阻塞队列中的线程 B


3. 重量级锁 —— 高竞争时的兜底
适用场景:多个线程同时竞争锁(自旋等待失效,继续自旋会浪费 CPU),此时需要通过操作系统的 “互斥量(Mutex)” 实现线程阻塞与唤醒,保证线程安全。

3.1 获取重量级锁
1). 检查对象头的锁状态(Mark Word)
Java 中每个对象的内存布局包含对象头(Mark Word),其中存储了锁的状态信息。线程首先读取目标对象的 Mark Word:
若 Mark Word 的锁标志位为10(重量级锁的标志位),说明锁已被其他线程占用;

2). 关联重量级锁结构(ObjectMonitor)
JVM 为每个重量级锁维护一个ObjectMonitor(对象监视器) 结构体(C++ 实现),其核心字段包括:
_owner:指向当前持有锁的线程;
_WaitSet:存储因等待锁而被挂起的线程队列(条件等待队列);
_EntryList:存储尝试获取锁但未成功的线程队列(入口等待队列);
_recursions:锁的重入次数(避免线程重复加锁导致死锁)。
当锁首次升级为重量级时,JVM 会为对象分配一个 ObjectMonitor,并将 Mark Word 中的 “指针” 指向该 ObjectMonitor。

3). 尝试获取锁(CAS 竞争)
线程通过CAS 操作尝试修改 ObjectMonitor 的_owner字段:
若_owner为null(锁空闲):CAS 成功,将_owner设为当前线程,_recursions设为 1,加锁成功,线程继续执行临界区代码;
若_owner已指向当前线程(重入场景):直接将_recursions加 1,加锁成功(体现 “可重入锁” 特性);
若_owner指向其他线程(锁被占用):CAS 失败,进入下一步 —— 线程入队等待。

4). 线程入队(EntryList)
CAS 失败的线程会被 JVM 放入 ObjectMonitor 的_EntryList(入口等待队列),此时线程状态从 “RUNNABLE” 变为 “BLOCKED”(阻塞状态)。

5). 线程挂起(内核态等待)
JVM 通过调用操作系统的 **pthread_mutex_lock()** 函数(Linux 系统),将_EntryList中的线程从用户态切换到内核态,挂起线程(不再占用 CPU 时间片),等待锁释放的信号。
这一步是重量级锁开销大的核心原因:用户态与内核态切换需要消耗大量 CPU 资源,且线程挂起 / 唤醒的调度由操作系统内核完成,延迟较高。

6). 锁释放后的唤醒与重试
当持有锁的线程释放锁后,会唤醒_EntryList中的一个或多个线程(具体唤醒策略由操作系统调度决定,如 FIFO)。被唤醒的线程从内核态切换回用户态,重新尝试执行步骤 3(CAS 竞争锁),重复上述过程直到获取锁。

3.2 释放重量级锁
1). 检查锁的重入次数(_recursions)
线程首先检查 ObjectMonitor 的_recursions字段:
若_recursions > 1(重入场景):仅将_recursions减 1,释放 “一次重入”,不真正释放锁(避免其他线程提前竞争);
若_recursions == 1:进入真正的锁释放流程。

2). 重置 ObjectMonitor 的_owner 字段
线程通过 CAS 操作将 ObjectMonitor 的_owner字段设为null,标记锁为 “空闲” 状态。

3). 唤醒等待队列中的线程
JVM 调用操作系统的 **pthread_mutex_unlock()** 函数(Linux 系统),唤醒 ObjectMonitor 中_EntryList(或_WaitSet,若涉及wait()/notify())中的等待线程:
唤醒策略:通常是 “随机唤醒” 或 “公平唤醒”(取决于 JVM 和操作系统实现,默认非公平);
被唤醒的线程:从 “BLOCKED” 状态变为 “RUNNABLE” 状态,重新参与锁的竞争。

4). 线程状态恢复与锁竞争重试
被唤醒的线程从内核态切换回用户态,重新尝试获取锁(重复加锁过程的步骤 3):
若此时有多个线程被唤醒,会再次通过 CAS 竞争_owner字段,未竞争到的线程会重新进入_EntryList并挂起;
若锁被其他线程抢先获取,当前线程会再次进入阻塞状态,等待下一次唤醒。

5). 特殊场景:wait ()/notify () 的影响
若持有锁的线程在临界区中调用了Object.wait()方法,会触发额外逻辑:
线程先释放锁(执行上述释放锁步骤 2-3);
线程从_EntryList转移到_WaitSet(条件等待队列),进入 “WAITING” 状态;
当其他线程调用Object.notify()/notifyAll()时,_WaitSet中的线程会被转移回_EntryList,重新参与锁竞争。

HashMap的Put过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1. 计算键的哈希值
调用 hash(key) 方法计算哈希值,通过 “高 16 位与低 16 位异或” 减少哈希冲突(让高位参与索引计算)

2. 初始化或定位哈希桶
若哈希表(table)未初始化或长度为 0,调用 resize() 方法初始化(默认初始容量为 16)。
计算索引:i = (n - 1) & hash(n 为数组长度),通过位运算快速定位元素应存入的桶索引。

3. 处理Hash冲突
哈希冲突指不同 key 计算出相同索引,此时需根据桶中已有元素的类型(链表 / 红黑树)处理:
桶为空:直接创建新节点(Node)放入桶中。
桶中存在元素:
1). 若头节点与当前 key 相同(哈希值 +equals 都相等),标记为待更新节点。
2). 若桶中是红黑树(TreeNode),调用红黑树的插入方法(保证树结构平衡)。
3). 若桶中是链表,遍历链表:
找到相同 key 的节点,标记为待更新节点。
遍历到尾部仍无相同 key,插入新节点到链表末尾。
插入后若链表长度≥8,调用 treeifyBin 尝试转为红黑树(需满足数组长度≥64,否则仅扩容)。

4. 更新或插入
若存在相同 key 的节点(e != null),更新其 value 并返回旧值。
若为新插入节点,更新元素计数(size),并检查是否需要扩容(size > threshold)。

5. 扩容(resize)
当元素数量超过阈值(threshold = 容量 × 负载因子,默认负载因子 0.75)时,触发扩容:
新容量为原容量的 2 倍(保证容量始终是 2 的幂,确保索引计算的位运算有效)。
将旧数组中的元素重新计算索引,迁移到新数组中(红黑树可能退化为链表)。

hashmap什么时候触发扩容,扩容的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 元素 > 负载因子 * 初始内存时扩容(纯村因子一般为0.75
新容量 = 旧容量 × 2(保证仍是 2 的幂,便于后续哈希计算)。

2. 创建新的哈希表(数组)
初始化一个长度为新容量的数组(Node[] newTab),作为新的哈希表容器。

3. 迁移旧元素到新表
遍历旧哈希表的每个桶(数组元素),将其中的元素(链表或红黑树)迁移到新表:
1) 若桶中是链表:
遍历链表,对每个元素计算其在新表中的位置(新索引)。
由于新容量是旧容量的 2 倍,新索引可通过 “旧索引” 或 “旧索引 + 旧容量” 计算(利用位运算优化)。
将元素按新索引拆分到两个子链表,分别放入新表的对应位置。
2) 若桶中是红黑树:
先尝试将红黑树拆分为两个子链表(若子链表长度 ≤ 6,则转为普通链表),再按上述链表迁移逻辑放入新表。

单例模式双重锁的那一套写一下?解释一下为什么要两次判断?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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();
}
}
}
return instance;
}
}

第一次判断(未加锁时):
1. 作用是避免不必要的加锁操作。当实例已经被创建后,后续所有调用 getInstance() 的线程都无需进入同步代码块

第二次判断(加锁后):
2. 作用是防止多线程并发初始化导致的实例重复创建。

volatile 关键字的作用:
1. 防止 instance = new Singleton() 这一操作被 JVM 指令重排序(可能导致其他线程获取到 “未完全初始化” 的实例)。
2. volatile 确保实例在完全初始化后才会被其他线程可见。

解决CAS的ABA问题

1
2
3
4
5
6
7
8
9
10
11
12
13
1. ABA问题:
线程 1 读取变量 V 的值为 A;
线程 2 将 V 改为 B,再改回 A;
线程 1 执行 CAS 时,发现 V 仍为 A,认为未被修改,成功更新,但实际中间已被修改过。

1. 添加版本号标识
每次修改值时,不仅更新值,还递增版本号;
CAS 操作时,同时检查值和版本号,两者都匹配才更新。

2. 时间戳
CAS开始获取一个时间戳,在每次更新后更新数据的同时更新时间戳。在CAS时比较两者时间戳,若数据的时间戳比CAS的大则表示又被更新了一次

3.

Spring事务的原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1. 通过 AOP 代理拦截事务方法,在方法执行前后通过事务管理器调用数据库的事务操作(开启、提交、回滚),并通过传播行为控制嵌套事务的关系,最终依托数据库原生事务保证数据一致性。

2. 执行流程:Spring 事务的实现依赖 AOP 动态代理 和 事务拦截器,完整流程可分为 5 个步骤:

1) spring 启动时,通过注解扫描(如 @Transactional)或 XML 配置,识别需要被事务管理的方法,为其创建 代理对象

2) 当调用被 @Transactional 标注的方法时,实际执行的是代理对象的方法,触发 事务拦截器(TransactionInterceptor) 的拦截逻辑

3. 解析事务属性并开启事务
事务拦截器执行以下操作:解析事务属性:从 @Transactional 中获取隔离级别、传播行为、超时时间等。
获取事务管理器:根据数据源类型(如 JDBC、JPA)选择对应的 PlatformTransactionManager。
开启事务:调用事务管理器的 getTransaction(TransactionDefinition) 方法,底层通过数据库连接(Connection)执行 setAutoCommit(false),关闭自动提交,正式开启事务。

4. 执行目标方法并处理结果
若目标方法执行成功(无异常):事务拦截器调用事务管理器的 commit(TransactionStatus) 方法,提交事务(底层执行 Connection.commit())。

若目标方法抛出 未被忽略的异常(如 RuntimeException,可通过 rollbackFor 配置):调用事务管理器的 rollback(TransactionStatus) 方法,回滚事务(底层执行 Connection.rollback())。

5. 清理资源
无论提交还是回滚,最终都会释放数据库连接(归还到连接池)。

AQS的唤醒流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 释放同步状态(以独占模式为例)
线程完成操作后,调用 release(int arg) 释放同步状态

2. 唤醒后继节点(unparkSuccessor 方法)

3. 被唤醒线程的后续操作
被唤醒的线程从 LockSupport.park() 处恢复后,会进入 acquireQueued 方法的循环,再次尝试获取同步状态

持有锁的线程释放同步状态(tryRelease);
通过 unparkSuccessor 找到队列中第一个有效的后继节点;
调用 LockSupport.unpark 唤醒该节点对应的线程;
4. 被唤醒的线程再次尝试获取同步状态,成功后成为新的头节点,失败则继续阻塞。

Arry阻塞队列和Linked阻塞队列区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
场景需求	        优先选择 ArrayBlockingQueue	             优先选择 LinkedBlockingQueue
队列容量是否固定 需明确、固定容量(如限流器、资源池) 容量动态变化,或无法预估(如日志收集、消息暂存)
并发强度 低并发场景(锁竞争影响小) 高并发场景(生产者 / 消费者并发度高)
内存占用敏感性 内存充足,可接受固定数组的轻微浪费 内存紧张,需按需分配(避免固定数组浪费)
迭代器安全性 需严格检测并发修改(不允许脏读) 可接受迭代旧数据,无需检测并发修改
边界风险控制 必须避免 “无界队列内存溢出”(强制有界) 可接受无界模式(需确保生产者不会无限写入)


底层数据结构 固定大小的数组(Object[]) 单向链表(节点动态创建,默认无界)
容量特性 必须指定容量(capacity),有界队列 可指定容量(有界),默认 Integer.MAX_VALUE(无界)
锁设计 单锁(ReentrantLock)+ 双条件变量(notEmpty/notFull) 双锁(takeLock/putLock)+ 双条件变量,读写分离
内存占用 初始化时分配固定数组空间,内存连续但可能有浪费 节点动态分配,内存按需使用,无空间浪费
迭代器特性 支持 fail-fast 迭代器(并发修改时抛异常) 支持 弱一致性迭代器(并发修改不抛异常,遍历旧数据)
性能特性 单锁竞争激烈,高并发下性能易瓶颈 双锁分离,生产者 / 消费者并发度更高,高并发性能更优
边界条件处理 队列满时 put 阻塞,空时 take 阻塞(无界场景不适用) 无界模式下 put 永不阻塞(可能内存溢出),有界模式同 ArrayBlockingQueue

1. Array的单锁设计
无论是 put()(存)还是 take()(取),都需要先获取同一个锁。这意味着 生产者和消费者会竞争同一把锁,高并发下锁竞争激烈,吞吐量易受影响。

2. linked的双锁,读写分离
使用 2 把独立的 ReentrantLock 分别控制 “存” 和 “取”,实现生产者和消费者的并发隔离
生产者(put)只竞争 putLock,消费者(take)只竞争 takeLock,两者无锁竞争,大幅提升高并发场景下的吞吐量。
唯一需要共享的是 “队列元素计数”(count),因此用 AtomicInteger 实现原子计数,避免锁竞争。
```0

# Spring 自动配置的过程
```Java
Spring 的自动配置是其核心特性之一,通过 @EnableAutoConfiguration 注解触发,核心逻辑是基于类路径下的依赖、配置和自定义 Bean,自动生成并注册满足条件的 Bean 到 Spring 容器。

1. 触发自动配置入口
通常在主启动类上通过 @SpringBootApplication 注解启用自动配置,该注解包含 @EnableAutoConfiguration 注解,是自动配置的触发点。
@EnableAutoConfiguration 通过 @Import(AutoConfigurationImportSelector.class) 导入关键处理器,负责加载自动配置类。

2. 扫描自动配置候选类
AutoConfigurationImportSelector 的核心逻辑是从类路径下的 META-INF/spring.factories 文件中读取自动配置类全限定名(Spring Boot 2.7+ 改为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件)。
这些文件由 Spring Boot Starter 依赖(如 spring-boot-starter-web)提供,包含大量预定义的自动配置类

3. 过滤有效的自动配置类
读取到的候选自动配置类并非全部生效,需通过以下条件过滤:
@Conditional 条件注解:自动配置类通常标注 @ConditionalOnClass(类路径存在指定类)、@ConditionalOnMissingBean(容器中不存在指定 Bean)等注解,只有满足条件的类才会被激活。
例如:@ConditionalOnClass(DataSource.class) 表示只有类路径存在 DataSource 时,数据源自动配置才生效。
排除机制:通过 @EnableAutoConfiguration(exclude = ...) 或配置文件 spring.autoconfigure.exclude 排除不需要的自动配置类。

4. 加载并注册自动配置类
过滤后的有效自动配置类会被 Spring 容器加载,这些类本质是带有 @Configuration 的配置类,内部通过 @Bean 定义默认组件。
例如:DispatcherServletAutoConfiguration 会在满足条件时,自动注册 DispatcherServlet、ServletRegistrationBean 等 Web 核心组件。

5. 自定义配置覆盖默认值
自动配置类中定义的 Bean 通常会读取外部配置(如 application.properties),允许用户通过配置修改默认行为。
例如:server.port=8080 会覆盖 ServerProperties 中 port 的默认值(8080)。
如果用户自定义了同名 Bean(通过 @Bean@Component),根据「用户定义优先」原则,会覆盖自动配置类中的默认 Bean(通过 @ConditionalOnMissingBean 实现)。


启动类通过 @EnableAutoConfiguration 触发自动配置。
加载 META-INF 目录下的自动配置候选类。
根据 @Conditional 注解过滤出有效的自动配置类。
注册自动配置类中的默认 Bean 到容器。
允许用户通过配置或自定义 Bean 覆盖默认行为。

在springboot启动的时候做一些逻辑

1
1. 实现CommendLineRunner 或者 AppliacationRunner

java中的泛型编译期间是怎么样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1. 在 Java 中,泛型的核心特性是 “类型擦除”(Type Erasure),即泛型信息只在编译期间存在,编译完成后会被擦除,字节码中不保留任何泛型类型的具体信息。这种设计是为了兼容泛型出现之前的 Java 版本(JDK 5 之前),但也带来了一些特殊的行为和限制。

2. 无边界泛型
List<String> list = new ArrayList<>(); List list = new ArrayList(); // 擦除<String>,保留原始类型List
list.add("hello"); ----> list.add("hello");
String s = list.get(0); String s = (String) list.get(0); // 自动添加强制类型转换

3. 有边界泛型
class MyClass<T extends Number> { class MyClass {
T value; Number value; // T被擦除为边界类型Number
T getValue() { return value; } ----> Number getValue() { return value; }
}
}

Java 泛型是 “编译期语法糖”,其核心机制是类型擦除:
编译时:检查类型合法性,替换泛型参数为边界类型,插入必要的类型转换。
运行时:字节码中无泛型信息,所有泛型操作都基于擦除后的原始类型执行。

包装类的自动装箱和拆箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
自动装箱(Autoboxing) 和 自动拆箱(Unboxing) 是 Java 编译器提供的语法糖,在编译阶段就已经将转换逻辑转换为了对应的方法调用。在运行时,JVM 执行的是这些已经插入了转换方法的字节码,并不存在真正的 "自动" 转换过程

1. 自动装箱(Autoboxing)
定义:将基本数据类型自动转换为对应的包装类对象。
当基本类型的值被赋值给包装类变量、作为参数传递给需要包装类的方法,或放入泛型集合(如 List<Integer>)时,编译器会自动完成转换
// 自动装箱:int → Integer
Integer num = 100; // 等价于 Integer num = Integer.valueOf(100);
// 集合中自动装箱
List<Integer> list = new ArrayList<>();
list.add(200); // 自动装箱:int → Integer,等价于 list.add(Integer.valueOf(200));

2. 自动拆箱(Unboxing)
定义:将包装类对象自动转换为对应的基本数据类型。
触发时机:当包装类对象被赋值给基本类型变量、作为参数传递给需要基本类型的方法,或参与算术运算时,编译器会自动完成转换。
// 自动拆箱:Integer → int
Integer numObj = 100;
int num = numObj; // 等价于 int num = numObj.intValue();
// 算术运算中自动拆箱
Integer a = 10;
Integer b = 20;
int sum = a + b; // 先拆箱为 int 再计算:a.intValue() + b.intValue()

3. 关键特性与注意事项
1). 缓存机制
Integer:默认缓存 -128 ~ 127 之间的整数
Byte、Short、Long:缓存 -128 ~ 127
Character:缓存 0 ~ 127(ASCII 字符)

2). null 安全问题
自动拆箱时包装类为null会报错NullPointerException

为什么有一些数据库设计规范不允许字段默认为null

1
2
3
4
5
6
7
8
9
1. NULL 破坏 “数据语义的确定性”,导致逻辑歧义
user表的phone字段,若允许 NULL。一个字段为Null,无法区分是没有手机号还是录入错误。

2. NULL 导致查询逻辑复杂,易产生 “隐性 bug”
SQL 对 NULL 的处理。不能用=/!=判断,必须用IS NULL/IS NOT NULL

3. SQL方法失效
1). COUNT(字段名)和COUNT(*)结果不一致,COUNT(*)会包含所有行,COUNT(字段名)会排除 NULL 值
2). AVG会计算排除Null外的平均值

websocket和https有什么区别

1
2
3
4
5
6
7
HTTPS:
是 HTTP 协议的加密版本(基于 TLS/SSL 加密),属于 应用层协议,构建在 TCP 之上。
核心用途是单向 / 双向的数据传输,但本质是请求 - 响应模式(客户端主动发起请求,服务器被动响应),不支持服务器主动向客户端推送数据。

WebSocket:
是独立的 应用层协议(RFC 6455 定义),同样基于 TCP 传输,但支持全双工通信。
核心用途是建立持久连接,允许客户端和服务器双向实时通信(服务器可主动向客户端推送数据,无需客户端频繁请求)。

redission分布式锁加锁和解锁的底层原理

1
setNX

string为什么要设计成不可变的

1
2
3
4
5
6
7
8
9
String 被设计为不可变(Immutable),并非偶然选择,而是基于安全性、性能优化、并发控制等核心需求的权衡结果

1. 保障多线程并发安全:避免线程安全问题,不可变的 String 天生线程安全

2. 支撑字符串常量池:大幅节省内存。不可变性是 “字符串常量池(String Constant Pool)” 实现的前提

3. 作为哈希表(HashMap)键的必要条件。键(Key)的哈希值(HashCode)必须稳定,且键的状态不能影响哈希表的存储位置

4. 降低安全风险:避免敏感数据被篡改,在安全场景中(如密码、Token、加密密钥、URL 参数等),字符串的 “不可篡改性” 至关重要

缓存雪崩,击穿,穿透

1
2
3
4
5
6
7
8
1. 缓存雪崩:
同一时间大量的Key过期导致:附加随机时间

2. 缓存穿透:
查询的数据在缓存和数据库中都不存在:布隆过滤器,短时间的替代缓存

3. 缓存击穿
热点Key过期:逻辑过期时间,高峰预热

分布式环境下,对于 MySQL 数据库而言,可以用什么手段来保证数据的唯一性?`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1. 主键(PRIMARY KEY)或唯一索引(UNIQUE INDEX)

2. 自增主键 + 分布式 ID 生成策略
当使用分库分表时,MySQL 原生自增主键(AUTO_INCREMENT)会因 “各分片独立自增” 导致全局重复,需用分布式 ID 替代:使用雪花算法(Snowflake)、UUID、Redis 自增(INCR)等生成全局唯一 ID,作为主键写入各分片

二、应用层:提前拦截重复数据

1. 分布式锁(悲观锁)
在并发写入场景中,通过分布式锁保证 “同一唯一键的写入操作串行化”,强制串行化。

2. 乐观锁(版本号机制)
通过版本号字段控制数据更新,适用于 “读多写少” 场景,避免分布式锁的性能损耗:

三、中间件 / 服务层:全局协调与校验

1. 引入分布式事务(强一致性场景)
在跨库写入时(如订单表和支付表分布在不同库),通过分布式事务保证数据一致性:

2. 前置服务统一校验
部署独立的 “唯一性校验服务”,所有写入请求必须先经过该服务校验:

String 的equals方法

1
2
3
4
5
6
7
8
9
10
1. 比较空间地址
判断是否为同一个对象,若是则直接返回true

2. 比较类型
通过 instanceof 判断是否为字符串类型,不是则直接返回false

3. 逐个比较
转为字符数组逐个比较

4. equals方法相等的前提是两对象的HashCode值必须相等

Redis AOF重写

1
2
3
4
5
6
7
8
9
AOF 文件的重写流程主要就一句话,“一次拷贝,两处日志”。
一次拷贝:重写发生的时候,主进程会 fork 出一个子进程,然后子进程与主进程共享 Redis 物理内存,让子进程将这些 Redis 数据写入重写日志。

两处日志:重写发生的时候,我们需要注意 AOF 缓冲和 AOF 重写缓冲;当数据进行重写的时候,如果此时有新的写入命令执行,会由主进程分别写入 AOF 缓冲和 AOF 重写缓冲;AOF 缓冲用于保证此时即使发生宕机了,原来的 AOF 日志也是完整的,可以用于恢复。AOF 重写缓冲用于保证新的 AOF 文件也不会丢失最新的写入操作。


# 谈谈你对面向对象的看法
```Java

StringBuffer 是线程安全的(sys同步机制实现)

1
2
3
4
1. 在 Java 中,StringBuffer 的线程安全性主要通过同步机制来保证:它的绝大多数方法(如 append()、insert()、delete() 等)都被声明为 synchronized(同步的)。
这意味着当多个线程同时操作同一个 StringBuffer 对象时,同一时间只有一个线程能执行这些同步方法,从而避免了多线程并发修改可能导致的数据不一致问题。

2. StringBuilder 和 StringBuffer 的底层数据结构和核心功能实现相同,唯一的本质区别是 StringBuffer 通过同步方法保证线程安全,而 StringBuilder 放弃同步以换取更高性能。

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1

谈谈你对面向对象的看法

1