一、Java基础
二、并发
1、是否用过synchronized
(1)回答思路
synchronized
:可重入锁、非公平锁用于静态方法锁 class 类,用于非静态方法锁对象
悲观锁、重量级锁
wait()
、notify()
、notifyAll()
必须要在Synchronized
中使用
synchronized
实际工作中使用的场景较少,更多的是使用 redis 分布式锁。
(2)字典缓存
实际应用场景:字典、系统参数缓存,单机系统情况下,缓存到内存中。
当修改了参数时,清除全部缓存,并重新加载缓存
在清除缓存,并且还未重新加载时,如果其他线程来获取字典数据,则获取到null
为了解决这个问题,必须保证清除缓存和重新加载两个步骤的原子性,必须一起执行,所以修改字典的方法和获取字典的方法必须加锁。
(3)懒汉式单例-双重检查锁
单例模式
下面为懒汉式双重检查锁
public class DoubleChecked {
private volatile static DoubleChecked instance;
// 防止产生多个对象
private DoubleChecked(){};
public static DoubleChecked getInstance() {
if (instance == null) { //Single Checked
synchronized (DoubleChecked.class) {
if (instance == null) { //Double Checked
instance = new DoubleChecked();
}
}
}
return instance ;
}
}
(4)Spring循环依赖
Spring 源码解决循环依赖获取 Bean 的时候
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// Consistent creation of early reference within full singleton lock
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
(5)延伸问题
- 锁升级
- redis 分布式锁
- volatile
- Spring 循环依赖
2、聊聊redis分布式锁
3、是否使用过多线程(池)
(1)回答思路
实际工作中使用多线程一般是采用线程池的方式。
- 七大参数、三大创建方法、四个拒绝策略
- 一个项目中一个功能一个线程池
(2)大批量数据导出
客户每月数据量五万以上,每月初需导出上个月的数据,由于数据量大,导出时还得对数据进行处理,该处理步骤耗时较长。最大数据量峰值能达到接近10万。导出的时候,数据量超过4万会出现超时问题(30秒超时)。通过线程池批量处理的方式加快相应速度,每个线程处理1万条数据。能做到导出10万条数据时间在20到25秒之间。
所以采用线程池 + CompletableFuture
实现导出功能。
线程池参数设置:
- 核心线程数:0,每月导一次,不需要线程长期存活
- 最大线程数:9(CPU核心数 + 1,尽量跑满cpu)
- 阻塞队列:队列长度128,为提高响应速度,不应缓存任务
- 最大空闲时间:60(默认)
- 拒绝策略:
AbortPolicy
- 线程工厂:默认
private final LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(128);
@Bean
public ThreadPoolExecutor threadPoolExecutor(){
return new ThreadPoolExecutor(
0,
9,
60,
TimeUnit.SECONDS,
workQueue,
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
List<T> data = list(condition);
int step = 5000;
for (int i = 0; i < data.size(); i += step) {
CompletableFuture<List> future = CompletableFuture.supplyAsync(() -> {
// 处理数据
for (int z = i; z < i + step; i ++) {
}
return list;
}, threadPoolExecutor);
futures.add(future);
}
List<T> resList = new ArrayList<>();
// 收集处理结果
futures.forEach(result -> {
if(result != null) resList.addAll(result.join());
});
三、JVM
1、是否做过JVM调优
没,99%的项目不需要
四、数据库
1、是否做过sql优化
XX项目因后期数据量增大,做过一段时间的 sql 优化
基本思路:
- 先检查有没有冗余的查询字段或者联表
- 用
EXPLAIN
关键字确保查询使用索引 - 优化sql,或新增字段索引
在进行优化时,遇到过一个比较懵逼的问题,至今映像深刻,两张表在联表的情况下,两张表联表字段类型不同,Long 和 varchar 类型,导致查询不使用索引,字段类型不同不会使用索引。
(1)EXPLAIN结果说明
这里对explain的结果进行简单说明:
select_type:查询类型
SIMPLE 简单查询
PRIMARY 最外层查询
UNION union后续查询
SUBQUERY 子查询
type:查询数据时采用的方式
- ALL 全表(性能最差)
- index 基于索引的全表
- range 范围 (< > in)
- ref 非唯一索引单值查询
- const 使用主键或者唯一索引等值查询
possible_keys:可能用到的索引
key:真正用到的索引
rows:预估扫描多少行记录
key_len:使用了索引的字节数
Extra:额外信息
- Using where 索引回表
- Using index 索引直接满足条件(覆盖索引)
- Using filesort 需要排序
- Using temprorary 使用到临时表
(2)索引失效场景
- 最左前缀原则:联合索引,查询从索引的最左列开始,并且不跳过索引中的列,否则索引失效
- or
- 左模糊查询
- 带有运算
- 使用函数
- 表或字段编码不一致
- 字段数据类型不一致
- in、not in、exists、not exists、<、>、<=、>=、between and等,根据数据分布决定
- is null、is not null 也是根据数据分布决定
(3)索引优化技术
覆盖索引
只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。
explain的输出结果Extra字段为Using index时,能够触发索引覆盖。
索引下推
索引下推就是指在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数来提高查询效率。
2、索引分类
(1)按表列属性分类
- 主键索引
- 唯一索引
- 普通索引
- 联合索引
- 全文索引
(2)按存储结构分类
- 聚簇索引:一般为主键索引列,B+树子节点存储数据
- 非聚簇索引(二级索引):其他索引列,B+树子节点存储主键和索引字段
五、RabbitMQ
1、异步发送短信
2、异步存储日志
目的:提高接口响应速度,解耦,日志与业务解耦,让业务代码专注于业务
六、ES
1、业务、接口日志存储
2、ELK日志,全链路追踪
3、开户行模糊查询
七、Spring源码
1、Ioc启动流程
Spring的IoC容器的启动过程,核心流程是将bean的配置项从不同渠道,包括XML、注解或者配置文件中读取和解析后,生成 BeanDefinition
的过程,在这过程中IoC容器会进行refresh操作,这个过程可以设置一些 BeanPostProcesser
的前置或后置操作,在执行完这些操作后,BeanDefinition
就会被注册到 BeanDefinitionRegistry
容器中。
整体IoC容器的启动过程分为3个阶段:定位—>加载–>注册
- 定位:通过
ResourceLoader
来完成资源的定位,后续的Bean配置项透视通过Resource
资源文件来读取和解析的。 - 加载:
ApplicationContext
容器调用refresh()
方法,解析Bean配置信息将其解析成BeanDefinition
,然后调用一系列容器、Bean级别的前置后置处理器,包括调用BeanFactoryPostProcessor
的前置操作。 - 注册:
BeanDefinition
实例注册,将生成的BeanDefinition
放到容器的缓存池BeanDefinitionRegistry
中。 - 调用已注册的
BeanFactoryPostProcessor
- 注册
BeanPostProcessor
- Bean 实例化
- Bean 初始化
2、Bean的生命周期
3、SpringMVC执行流程
面试官你好,我叫***,学历是本科学历,至今已从事Java相关工作三年了,参与过多个项目,并在部分项目中担任过研发组长职位,并且在业余时间也会积极学习研究新知识,有撰写博客的习惯,至今个人博客已有260余篇文章。
我在上家已经工作了3年多了,个人感觉在这边技术提升到达了瓶颈,个人希望在职业生涯中获得更好的发展机会,离开当前工作是为了开阔视野,并寻求更好的学习机会。