2025.2.21 一面
讲下你JUC常用的类,没答上
线程池相关
- ThreadPoolExecutor:最核心的线程池类,用于创建和管理线程池。通过它可以灵活地配置线程池的参数,如核心线程数、最大线程数、任务队列等,以满足不同的并发处理需求。
- Executors:线程池工厂类,提供了一系列静态方法来创建不同类型的线程池,如newFixedThreadPool(创建固定线程数的线程池)、newSingleThreadExecutor(创建单线程线程池)等,方便开发者快速创建线程池。
并发集合类
- ConcurrentHashMap:线程安全的HashMap,用于在多线程环境下高效地存储和访问键值对。在高并发场景下比传统的Hashtable性能更好。
- CopyOnWriteArrayList:线程安全的列表,在对列表进行修改操作时,会创建一个新的底层数组,将修改操作应用到新数组上,而读操作仍然可以在旧数组上进行,从而实现了读写分离,提高了并发读的性能,适用于读多写少的场景。
了解的
同步工具类
- Semaphore:信号量,用于控制同时访问某个资源的线程数量。它维护了一个许可计数器,线程在访问资源前需要获取许可,如果有可用许可,则获取成功并将许可计数器减一,否则线程需要等待,直到有其他线程释放许可。常用于控制对有限资源的访问,如数据库连接池、线程池中的线程数量等。
原子类
- AtomicInteger:原子整数类,提供了对整数类型的原子操作,如自增、自减、比较并交换等。通过硬件级别的原子指令来保证操作的原子性和线程安全性,避免了使用锁带来的性能开销,在多线程环境下对整数进行计数、状态标记等操作非常方便。
- AtomicReference:原子引用类,用于对对象引用进行原子操作。可以保证在多线程环境下,对对象的更新操作是原子性的,即要么全部成功,要么全部失败,不会出现数据不一致的情况。常用于实现无锁数据结构或需要对对象进行原子更新的场景。
synchronized锁与ReentrantLock区别
没答全,应该从五个方面进行:
- 用法不同:synchronized 修饰普通方法、静态方法和代码块;ReentrantLock 用在代码块上。
- 获取锁和释放锁方式不同:synchronized 会自动加锁和释放锁。 ReentrantLock 需要手动加锁和释放锁
- 锁类型不同:synchronized 属于非公平锁,而 ReentrantLock 可以是公平锁也可以是非公平锁。
- 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
- 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
synchronized使用对象,如果用在方法上是锁什么?后半个没答上
使用对象:普通方法、静态方法和代码块
锁方法的话,实例方法是锁对象实例,静态方法是锁类Class
线程池介绍, 只说了基本概念, 没吟唱
只答了:什么是线程池
没有从:核心参数,线程池流程,拒绝策略
流程
- 如果核心线程没有满,就创建一个线程,
- 如果满了,就是会加入等待队列,
- 如果等待队列满了,就会增加线程,
- 如果达到最大线程数量,如果都达到最大线程数量,就会按照一些丢弃的策略进行处理。
核心参数
- corePoolSize:线程池核心线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。
- maximumPoolSize:线程池中最多可容纳的线程数量。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程且当前线程池的线程数量小于corePoolSize,就会创建新的线程来执行任务,否则就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略。
- keepAliveTime:当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。
- unit:就是keepAliveTime时间的单位。
- workQueue:工作队列(上面提到的阻塞队列)。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。
- threadFactory:线程工厂。可以用来给线程取名字等等
- handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略
拒绝策略
- CallerRunsPolicy,使用线程池的调用者所在的线程去执行被拒绝的任务,除非线程池被停止或者线程池的任务队列已有空缺。
- AbortPolicy,直接抛出一个任务被线程池拒绝的异常。
- DiscardPolicy,不做任何处理,静默拒绝提交的任务。
- DiscardOldestPolicy,抛弃最老的任务,然后执行该任务。
- 自定义拒绝策略,通过实现接口可以自定义任务拒绝策略。
线程池实践,没答上
确实没在项目实践过,没答好,或许可以从线程池应用场景出发。
答:虽然我目前没有在项目环境直接配置线程池经验,但是我理解它是并发编程的基础组件,在这三种场景下会有所使用:
- 高并发请求处理:比如 Web 服务器(Tomcat 的线程池)需要快速响应 HTTP 请求,但直接为每个请求创建线程会导致内存耗尽。
- 批量异步任务:比如用户上传文件后需要异步生成缩略图、日志归档,线程池可管理这些后台任务的并发度。
- 资源敏感型任务:如数据库连接池,避免频繁建立连接(成本高),通过复用连接提升性能。
JMM内存模型,volatile,没吟唱
只答了JMM是什么,volatile作用,还可以继续吟唱比如:
介绍
Java内存模型(JMM)
- 主内存和工作内存:JMM规定所有变量都存储在主内存中,每个线程有自己的工作内存(类似于CPU缓存),工作内存保存了线程使用的变量的副本。线程对变量的所有操作必须在工作内存中进行,不能直接读写主内存中的变量。不同线程之间的变量值传递需要通过主内存来完成。
JMM内存模型的并发问题
并发问题
- 竞态条件(Race Condition):当多个线程尝试同时修改同一个数据项,并且最终结果依赖于这些线程的执行顺序时,就会发生竞态条件。
- 内存可见性问题:一个线程对共享变量所做的更改可能不会立即对其他线程可见。
- 指令重排序:为了优化性能,编译器和处理器可能会改变程序中的语句执行顺序,这可能导致不符合预期的行为。
所以这时我们需要
Volatile关键字
- 内存可见性:当一个变量被声明为
volatile
时,任何对该变量的写操作都会立即刷新到主内存,并且每次读取该变量时都会从主内存重新加载,而不是使用线程的工作内存中的值。这确保了所有线程能够看到最新的变量值,解决了内存可见性问题。 - 禁止重排序:
volatile
变量的读写操作不能与其他普通变量的操作重排序,保证了某些顺序依赖关系的正确性。但这并不意味着所有的读写操作都不能被重排序,只是限制了围绕volatile
变量的操作顺序。
类加载机制,初始化没讲清楚
初始化没讲清楚,实际就是执行类构造器方法
初始化:
- 执行类构造器
<clinit>()
方法,这是由编译器自动收集类中的静态变量赋值操作和静态代码块合并产生的。注意,<clinit>()
方法不是必须存在的,只有当类中有静态初始化动作时才会生成。
mysql索引失效的场景
最左匹配原则实践题
索引跳跃,没回答上
MySQL 的索引跳跃(Index Skip Scan) 是 MySQL 8.0 引入的一项优化技术,用于在复合索引(Composite Index)中跳过某些前缀列的值,直接利用索引后续列的查询条件来加速查询。它的核心思想是减少不必要的全表扫描,尤其适用于部分列缺失查询条件但索引设计包含多列的场景。
假设有一个复合索引 (A, B, C)
,传统情况下:
- 如果查询条件中只有
B
和C
(没有A
),MySQL 无法直接使用该索引,可能会退化为全表扫描。 - 索引跳跃允许在这种情况下,通过“跳过”索引前缀列
A
的不同值,直接遍历B
和C
的条件,从而利用索引加速查询。
工作原理
-- 表结构: 索引 (gender, age, city)
-- 查询: 查找年龄=25且城市='北京'的人(未指定 gender)
SELECT * FROM users WHERE age = 25 AND city = '北京';
即使没有 gender
条件,MySQL 会:
- 遍历
gender
的所有可能值(如 'male' 和 'female')。 - 对每个
gender
值,在索引中查找age=25
且city='北京'
的记录。
使用场景
- 高选择性的前缀列:索引前缀列(如
gender
)的不同值较少(例如只有 'male'/'female'),拆分后的子查询次数可控。 - 后续列的过滤性强:后续列(如
age
和city
)的条件能有效过滤数据。 - 避免全表扫描:当查询无法通过其他索引优化时,索引跳跃比全表扫描更高效。
场景题:ABC三个字段做联合索引, 逻辑删除, 保证这张表的数据是逻辑删除, 如何做到插入相同数据的时候避免索引碰撞
我答得增加一个逻辑删除字段,并且用时间戳(想的0和1区分度太低)来进行,实际上0和1也是可行的
同时给联合索引也加上这个字段
用0和1也存在些问题,好像是碰撞
介绍开源
Spring项目如何变成一个starter**
将一个Spring项目转换为一个Starter,主要是为了将其作为一个可重用的组件或库,方便其他项目通过简单的依赖引入即可使用。Spring Boot Starter是一种便捷的方式,它允许你封装自动配置、自定义配置项以及默认的行为等。以下是创建一个Spring Boot Starter的基本步骤:
1. 创建一个新的Maven或Gradle项目
首先,你需要为你的starter创建一个新的Maven或Gradle项目。这个项目将会包含所有必要的配置和代码。
2. 定义spring-boot-starter
作为父项目(对于Maven)
如果你使用的是Maven,确保在你的pom.xml
文件中定义了spring-boot-starter-parent
作为父项目。这将帮助你管理依赖版本和其他Spring Boot相关的配置。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.x</version> <!-- 请根据实际情况调整版本号 -->
<relativePath/>
</parent>
3. 添加必要的依赖
添加任何你需要的依赖到你的pom.xml
或build.gradle
文件中。通常,你会至少需要spring-boot-autoconfigure
依赖来支持自动配置功能。
对于Maven:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- 其他依赖 -->
</dependencies>
4. 创建自动配置类
创建一个自动配置类,并使用@Configuration
注解标记它。在这个类中,你可以定义bean并设置它们的默认值。如果某些bean的创建依赖于外部配置,可以使用@ConditionalOn*
系列注解来控制条件装配。
@Configuration
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 只有当上下文中没有指定类型的bean时才创建
public MyService myService() {
return new MyServiceImpl();
}
}
5. 配置META-INF/spring.factories
为了让Spring Boot知道你的自动配置类,你需要在src/main/resources/META-INF/
目录下创建一个名为spring.factories
的文件,并在其中指定你的自动配置类。
# spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration
6. 发布你的Starter
最后一步是打包并发布你的starter。如果是内部使用,可以通过私有的Maven仓库或者Nexus等工具进行发布;如果是开源,则可以考虑发布到Maven Central或JCenter。
注意事项
- 确保你的Starter命名遵循Spring Boot的命名约定,通常是
spring-boot-starter-*
的形式。 - 提供详细的文档说明如何使用你的starter,包括所有可用的配置选项。
- 如果可能的话,提供一些示例代码来展示如何集成和使用你的starter。
通过上述步骤,你就能够创建一个基本的Spring Boot Starter了。随着项目的复杂度增加,你可能还需要考虑更多高级特性,如健康检查、指标监控等。
redis实现排行榜, 面试官想问zset
提到了简历里的一个排行榜,实际上想问的应该是zset
Zset内部实现
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
- 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
- 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
预防缓存穿透, 布隆过滤器,八股吟唱失败
只答了如何预防缓存穿透,实际上还可以把缓存击穿和雪崩也讲讲
布隆过滤器也没答上
- 非法请求的限制:在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
- 设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
- 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
Springtask介绍, 分布式多节点下会存在什么问题
问题:
- 重复执行:默认情况下,每个节点都会独立执行其调度的任务。这意味着如果任务是在所有节点上部署的,则相同任务会在每个节点上被执行一次,导致重复处理的问题。
- 任务一致性:由于不同节点可能处于不同的状态或接收到的信息不一致,这可能导致任务执行结果的不同步或者冲突。
- 资源竞争:如果任务涉及对共享资源(如数据库、文件系统等)的操作,那么多个节点同时尝试访问这些资源可能会引发锁争用或其他并发问题。
解决:
- 使用分布式任务调度框架:例如Quartz可以通过集群模式来避免任务重复执行,并且能够提供更好的容错能力。
- 数据库锁定机制:在任务开始前先尝试获取数据库中的锁记录,只有成功获取锁的节点才能执行任务。这种方式要求任务具有良好的幂等性和事务管理。
- 消息队列:利用消息队列(如RabbitMQ, Kafka等)作为任务触发器,确保只有一个消费者处理特定的消息/任务。
查询优化, 多少数据量优化
可能是想问我有多少张表,有多少数据量,但是答成了怎么建立索引
redis做缓存如何保证数据一致性 (旁路缓存) 延迟双删
答了旁路缓存,面试官说可以了解下延迟双删
手撕题:用栈实现队列
没有并发, leetcode232
不足
- 回答的时候只是回答了面试官的基础问题, 没有去拓展, 有时候没有去
- 回答过快, 面试后说完问题后可以等几秒思考好了答案再回答, 不用抢着回答
2025.2.24 二面
二面主要拷打的是项目
- 介绍项目的设计与实现
- 项目中有难点和收获的地方
- 排行榜(项目中的),在此基础上问了:缓存策略,整个排行榜优化方案(答了SQL优化和Redis的Zset)
- 内存泄漏怎么避免和发现(内存泄漏答了个ThreadLocal弱引用为key,必然被回收,出现null的key)
- 自己平时用ThreadLocal有没有注意这方面
- 代码题(策略模式的一种题)
- 对实习是怎么理解的
- 面试官主动介绍自己的业务
反问:
技术栈,改进的地方