文章目录
这是本人整理的数万字的面试笔记,基本上涵盖了 Java 领域的所有技术栈,本人也是凭借这份面试笔记斩获了近 10 个 offer,面试成功率高达80%。当然这份笔记是我根据自身的经验和技术栈整理的,自己觉得很重要的或者记不清的就会记录记录下来,面试被问到的时候也有回答的思路。现在共享给大家,希望对准备面试的小伙伴有帮助。
1. 微服务
1.1 主流注册中心对比
zookeeper:zookeeper 作为注册中心主要是因为它具有节点变更通知功能。只要客户端监听相关服务节点,服务节点有所变更就能及时的通知到监听客户端,非常方便。zookpeeper 是cp模式的。
eureka:是 netflix 开源的 基于RestFulAPI 风格开发的服务注册和发现组件。现在不会再更新。
consul:是使用go 语言开发的支持多数据中心分布式高可用的服务发布和注册的服务软件。
nacos:是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
1.2 eureka 心跳检测和自我保护机制
心跳检测:eureka 每隔30 秒就会向注册中心续约心跳一次,也就是报活。如果没有续约,租约在90s 后到期,然后服务就会失效,每隔30s的续约操作,就是心跳检测。
自我保护:eureka 服务端如果在15 分钟内,超过 85% 的客户端节点都没有正常心跳,那么Eureka 就认为客户端和注册中心出现了网络故障。而微服务本身是可正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制。在自我保护状态下,不会移除任何服务,能接受新服务的注册和查询请求,但是不会和集群中其他节点同步。
1.3 Ribbon 负载均衡策略
1、轮寻策略
2、随机策略
3、重试策略:一定时间内循环重试。继承随机策略。
4、最小连接数策略:遍历serverList,选出可用且连接数最小的server.
5、可用过滤策略:扩展了轮寻策略,会先通过默认的轮询选取一个 server,再判断 server 是否超时可用。
6、区域权衡策略:扩展了轮询策略。除了过滤连接超时和连接数过多的server,还会过滤掉不符合要求的 zone 区域里面所有的节点。然后在剩下的节点中轮询获取。
1.4 Hystrix 舱壁模式
使用舱壁避免了单个工作负载(或服务)消耗掉所有资源,从而导致其他服务出现故障的场景。这种模式主要是通过防止由一个服务引起的级联故障来增加系统的弹性。通过应用舱壁模式,可以保护有限的资源不被耗尽。为了避免问题服务请求过多导致正常服务⽆法访问,Hystrix 不是采⽤增加线程数,⽽是单独的为每⼀个控制⽅法创建⼀个线程池的⽅式,这种模式叫做“舱壁模式"。
1.5 Hystrix 工作模式
1、当调⽤出现问题时,开启⼀个时间窗(10s)
2、在这个时间窗内,统计调⽤次数是否达到最⼩请求数?
- 如果没有达到,则重置统计信息,回到第1步;
- 如果达到了,则统计失败的请求数占所有请求数的百分⽐,是否达到阈值?
- 如果达到,则跳闸(不再请求对应服务)
- 如果没有达到,则重置统计信息,回到第1步
3、如果跳闸,则会开启⼀个活动窗⼝(默认5s),每隔5s,Hystrix 会让⼀个请求通过,到达那个问题服务,看是否调⽤成功,如果成功,重置断路器回到第1步,如果失败,回到第3步。
1.6 Fein
fegin 是NetFlix 开发的一个轻量级 RestFulf 的 HTTP 服务客户端。用来进行远程调用。
1.7 gateway
gateway 核心逻辑就是路由转发加执行过滤器链。
客户端向gateway 发送请求。然后在gateway handler Mapping 中找到与请求相匹配的路由,然后handler 通过制定了过滤器链来将请求发送到我们实际服务执行业务逻辑,然后返回。在过滤器中可以进行参数加解密,参数校验、权限校验、日志输出、协议转换等等。
2. 并发编程
2.1 多线程
2.1.1 创建一个阻塞队列
核心思想就是当队列为空时,调用出队列的方法,会进行wait() 等待。直到有数据入队列时,调用notify() 方法。同样的在队列满是,入队列会阻塞,直到有出队列才唤醒。
public class MyBlockingQueue {
private int[] data=new int[10];
private int putIndex;
private int getIndex;
private int size;
public synchronized void put(int val){
if(size==data.length){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
put(val);
}else {
data[putIndex]=val;
putIndex++;
if(putIndex==data.length){
putIndex=0;
}
size++;
}
}
public synchronized int get(){
if(size==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
return get();
}else {
int val = data[getIndex];
getIndex++;
if(getIndex==data.length){
getIndex=0;
}
size--;
return val;
}
}
}
2.1.2 为什么wait() 方法要释放锁。
因为当一个线程进入一个 synchronized 修饰的同步方法后,会锁住当前对象,调用 wait() 方法进入阻塞状态。如果不释放当前对象的锁的话,其他线程永远获取不到当前对象的锁,也就没有办法唤醒当前线程了,这样就形成了死锁。所以在调用wait() 方法会释放锁,等待其他线程获取当前对象锁,调用notify() 唤醒此线程。然后此线程重新获得锁,执行剩下的操作,执行结束后,会再次的释放锁。
2.1.3 volatile 关键字的作用
1、保证64位写入的原子性。
2、内存可见性
3、禁止指令重新排序。
2.1.4 synchronized
synchronized 实现原理是获取不到锁先自旋,自旋依然获取不到锁,再阻塞。
2.2 JUC
2.2.1 阻塞队列
阻塞队列有基于数组实现的ArrayBlockQueue 和基于 链表实现的 LinkBlockQueue。以及还有优先级的 PriorityBlockQuere,按照元素的优先级出队列,实现了Comparable 接口。以及延时队列。根据延时时间大小出队列的,实际上是未来时间减去当前时间放入,DelayQueue 中的元素。如果getDelay() 小于等于0,说明该元素到期,可以出队列了。同步队列没有容量,先调用put(),线程就会阻塞,只要等另一个线程调用了task(),两个线程才会唤醒。
2.2.2 CopyOnWrite
是指在写的时候,不会直接操作源数据,而是先copy 一份数据进行修改,然后通过悲观锁或者乐观锁的方式写回。这样的好处是,读不用加锁。
CopyOnwriteArrayList 读操作的时候不会加锁,只有写的时候才会加同步锁,所以是线程安全的 ArrayList。
CopyOnwriteArraySet 就是用array 实现的一个线程安全的 Set .保证所有元素不重复,封装的是CopyOnwriteArrayList 。利用CopyOnwriteArrayList 的addAllAbsent() 方法。
2.2.3 锁
为了实现一把具有阻塞或唤醒功能的锁,需要几个核心要素:
-
需要一个state变量,标记该锁的状态。state变量至少有两个值:0、1。对state变量的操作,使用CAS保证线程安全。
-
需要记录当前是哪个线程持有锁。
-
需要底层支持对一个线程进行阻塞或唤醒操作。
-
需要有一个队列维护所有阻塞的线程。这个队列也必须是线程安全的无锁队列,也需要使用CAS。
2.2.4 AQS
AbstractQueuedSynchronizer 的核心就是一个双向链表形成的阻塞队列以及CAS.
2.2.5 CAS
CAS 名为:compare and swap。是比较内存中的值是否和预期值一致,如果一致就进行更新。这个过程是原子的。cas 有三个操作数,内存值,预期值和新值。只有当内存值和预期值相等时,才会将内存值更新为新值。否则什么都不做。底层是通过unsafe 类保证原子性的,unsafe类中都是native 方法,可以直接操作内存。
缺点:存在ABA 问题。解决方案就是加版本号,或者加时间戳。
2.2.5 ReentrantLock 互斥锁
Condition 本身也是一个接口,其功能和wait/notify类似,Condition 也必须和Lock一起使用。因此,在Lock的接口中,有一个与Condition相关的接口。
await() 是获取锁线程阻塞方法。signal() 是唤醒线程的方法。
2.3 线程池
线程池是一个典型的生产者消费者模型。线程池的核心是使用阻塞队列。我们常用的就是 ThreadPoolExecutor。
2.3.1 ThreadPoolExecutor
ThreadPoolExecutor 主要是 包含一个阻塞队列和一组线程集合 Workers。每个线程是一个worker 对象,worker 继承了AQS 。
ThreadPoolExecutor 包含7 个参数。
corePoolSize:在线程池中始终维护的线程个数。核心线程数
maxPoolSize:在corePooSize已满、队列也满的情况下,扩充线程至此值。最大线程数
keepAliveTime/TimeUnit:maxPoolSize 中的空闲线程,销毁所需要的时间,总线程数收缩回corePoolSize。存活时间和单位
blockingQueue:线程池所用的队列类型。阻塞队列
threadFactory:线程创建工厂
RejectedExecutionHandler:corePoolSize已满,队列已满,maxPoolSize 已满,最后的拒绝策略。
提交过程的核心流程:
1、先判读当前线程数是否小于 corePoolSize 。如果小于就新建线程执行
2、如果大于,就判断阻塞队列是否已经满了,如果没有满,就加入阻塞队列中。
3、如果满了,就判断当前线程数是否小于 maxPoolSize 。如果小于,就直接创建线程执行。如果大于就根据拒绝策略,拒绝任务。
线程池的关闭:
在调用shutdown() 或者shutdownNow() 之后,线程池并不会立即关闭,会等待所有任务执行完成之后,才会关闭线程池。
shutdown() 不会清空任务队列,并且只会中断空闲线程。
shutdownNow() 会清空任务队列,并且中断所有的线程。
2.3.2 线程池拒绝策略
线程池有四种拒绝策略
1、丢弃任务,并抛出异常
2、丢弃任务,单不抛出异常
3、丢弃队列最前面的任务,然后重新提交当前任务。
4、线程池什么都不做,由当前线程自己处理。
2.3.3 ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor实现了按时间调度来执行任务。有延迟任务和周期性任务。底层实现一个延迟队列来实现的。
3. java 基础
3.1 spring 启动的流程(12步)
1、prepareRefresh()。刷新前预处理。主要是设置启动时间,以及初始化配置文件中的占位符,以及校验配置信息是否正确。
2、obtainFreshBeanFactory()。获取 Beanfactory。
3、prepareBeanFactory(beanFactory)。Beanfactory 的准备工作,对BeanFactory 的一些属性进行配置。比如 context 的类加载器
4、postProcessBeanFactory()。BeanFactory 准备工作完成后,进行的后置处理工作。是一个钩子函数。
5、invokeBeanFactoryPostProcessors(beanFactory)。实例化并调⽤实现了BeanFactoryPostProcessor接⼝的Bean