ArrayList 与 LinkedList 区别

ArrayList 是一种顺序存储的线性表,底层使用数组实现

LinkedList是一种链式存储的线性表,本质是一个双向链表,实现了List、Deque接口,可以当成双向链表、队列、栈使用

ArrayList扩容机制是怎么样的? 详细说一下。

在往ArrayList add元素的时候,如果ArrayList 已有元素数量+1 大于 ArrayList 存储元素的总长度,就会触发扩容。

首先ArrayList会计算新数组的长度,长度为老数组的0.5倍,如果新数组长度还是小于插入元素需要的最小长度,那么新数组长度赋值为最小长度,如果超过ArrayList允许的最大长度Integer.MAX_VALUE(常见面试题 - 图1),那么新数组长度为Integer.MAX_VALUE,否则为Integer.MAX_VALUE - 8(为什么要-8?Why the maximum array size of ArrayList is Integer.MAX_VALUE - 8?

最后将原数组元素拷贝到新数组进行扩容

HashMap在并发下会产生什么问题?有什么替代方案?(HashTable, ConcurrentHashMap)。它们两者的实现原理。

  • HashMap并发下产生问题:由于在发生hash冲突,插入链表的时候,多线程会造成环链,再get的时候变成死循环,Map.size()不准确,数据丢失
    https://www.iteye.com/blog/hwl-sz-1897468
  • HashTable: 通过synchronized来修饰,效率低,多线程put的时候,只能有一个线程成功,其他线程都处于阻塞状态
  • ConcurrentHashMap:
    1.7 采用锁分段技术提高并发访问率
    1.8 数据依旧是分段存储,但锁采用了synchronized,内部采用Node数组+链表+红黑树的结构存储,当单个链表存储数量达到红黑树阈值8时(此时链表已有元素7),并且数组长度大于64时,存储结构转换为红黑树来存储,否则只进行数组的扩容
    https://www.cnblogs.com/banjinbaijiu/p/9147434.html

为什么用线程池?解释下线程池参数?

1、降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。

2、提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。

3、提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。

  • corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程
  • maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数

  • keepAliveTimeunit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过 setKeepAliveTime 来设置空闲时间

  • workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
  • ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂
  • Handler 任务拒绝策略,有两种情况,第一种是当我们调用shutdown 等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这是也就拒绝

简述线程池处理流程

image.png

线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?

1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。

阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源

2、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。

就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数(task > core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。

线程池中线程复用原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。

如何合理的配置Java线程池

如CPU密集型的任务,基本线程池应该配置多大?IO密集型的任务,基本线程池应该配置多大?用有界队列好还是无界队列好?任务非常多的时候,使用什么阻塞队列能获取最好的吞吐量?

CPU密集型,为了充分使用CPU,减少上下文切换,线程数配置成CPU个数+1个即可

IO密集型,由于可能大部分线程在处理IO,IO都比较耗时,因此可以配置成 2*CPU个数的线程,去处理其他任务

synchronized和Lock的区别

  1. synchronized 是Java内置关键字,Lock是Java类
  2. synchronized 无法显式的判断是否获取锁的状态,Lock可以判断是否获取到锁
  3. synchronized 会自动释放锁,Lock需要在finally中手工释放锁
  4. synchronized 不同线程获取锁只有一个线程能获取成功,其他线程会一直阻塞直到获取锁,Lock有阻塞锁,也有非阻塞锁,阻塞锁还有尝试设置,功能更强
  5. synchronized 可重入,不可中断,非公平,Lock锁可重入,可判断,有公平锁,非公平锁
  6. Lock锁适合大量同步代码的同步问题,synchronized锁适合代码少量的同步问题

说一下JVM内存模型吧,有哪些区?分别干什么的?

讲讲vm运行时数据库区 什么时候对象会进入老年代?

JVM的内存模型,Java8做了什么改

JVM运行时内存区域划分

常见面试题 - 图3

线程独享区域:程序计数器,本地方法栈,虚拟机栈
线程共享区域:元空间(<=1.7方法区), 堆

程序计数器:线程私有,是一块较小的内存空间,可以看做是当前线程执行的字节码指示器,也是唯一的没有定义OOM的区块
本地方法栈: 用于执行Native 方法时使用
虚拟机栈:用于存储局部变量,操作数栈,动态链接,方法出口等信息
元空间:存储已被虚拟机加载的类元信息,常量,静态变量,即时编译器编译后的代码等数据依旧存储在方法区中,方法区位于堆中
堆:存储对象实例

示例:

  1. /**
  2. * @author: jujun chen
  3. * @description: 使用了CGLIB来动态生成类,元空间存储类信息,-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
  4. * 如果只设置堆的大小,并不会溢出
  5. * @date: 2019/4/7
  6. */
  7. public class JavaMetaSpaceOOM {
  8. static class OOMObject{}
  9. public static void main(final String[] args) {
  10. while (true){
  11. Enhancer enhancer = new Enhancer();
  12. enhancer.setSuperclass(OOMObject.class);
  13. enhancer.setUseCache(false);
  14. enhancer.setCallback(new MethodInterceptor() {
  15. @Override
  16. public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
  17. return methodProxy.invokeSuper(o,objects);
  18. }
  19. });
  20. enhancer.create();
  21. }
  22. }
  23. }

OOM,及SOE的示例、原因,排查方法

  1. //OOM -Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError
  2. public class OOMTest {
  3. public static void main(String[] args) {
  4. List<Object> objList = new ArrayList();
  5. while(true) {
  6. objList.add(new Object());
  7. }
  8. }
  9. }
  10. //SOE栈异常 -Xss125k
  11. public class SOETest() {
  12. static int count = 0;
  13. public static void main(String[] args) {
  14. try {
  15. stackMethod();
  16. } catch(Error err) {
  17. err.printStackTrace();
  18. System.out.println("执行count=" + count);
  19. }
  20. }
  21. private static void stackMethod() {
  22. count ++;
  23. stackMethod();
  24. }
  25. }
  • OOM排查:如果能看到日志,可以从打印的日志中获取到发送异常的代码行,再去代码中查找具体哪块的代码有问题。如果没有记录日志,通过设置的 -XX:+HeapDumpOnOutOfMemoryError 在发生OOM的时候生成.hprof文件,再导入JProfiler能够看到是由于哪个对象造成的OOM,再通过这个对象去代码中寻找
  • SOE排查:栈的深度一般为1000-2000深度,超过了深度或者超过了栈大小就会导致SOE,通过打印的日志定位错误代码位置,检测是否有无限递归,发生了死循环等情况,修改代码

GC如何判断对象可以被回收

  • 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收,
  • 可达性分析法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。

引用计数法,可能会出现A 引用了 B,B 又引用了 A,这时候就算他们都不再使用了,但因为相互引用 计数器=1 永远无法被回收。

GC Roots的对象有:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。

当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”

每个对象只能触发一次finalize()方法

由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。

类加载器有几种

  1. Bootstrap ClassLoader
    负责加载JDK自带的rt.jar包中的类文件,它是所有类加载器的父加载器,Bootstrap ClassLoader没有任何父类加载器。
  2. Extension ClassLoader负责加载Java的扩展类库,也就是从jre/lib/ext目录下或者java.ext.dirs系统属性指定的目录下加载类。
  3. System ClassLoader负责从classpath环境变量中加载类文件,classpath环境变量通常由”-classpath” 或 “-cp” 命令行选项来定义,或是由 jar中 Mainfest文件的classpath属性指定,System ClassLoader是Extension ClassLoader的子加载器
  4. 自定义加载器

什么是双亲委派模型?双亲委派模型的破坏

一个类在加载的时候,首先会将加载请求委派给父加载器,只有当父加载器反馈无法加载完成这个请求时,子加载器才会尝试自己加载
双亲委派模型的破坏指的是不按照双亲委派模型来加载类,比如JNDI,它的代码由启动类加载器加载,但JDNI需要调用部署在ClassPath的JNDI接口,但启动类加载器是不知道这些代码的,所以就有了线程上下文类加载器(Thread Context ClassLoader),可以通过java.lang.Thread类setContextClassLoader设置类加载器,通过这个父加载器就可以请求子类加载器完成类加载的动作。

建立索引的原则

  1. 最左匹配原则,直到遇到范围查询(>, <, between, like)就停止,比如a = 1 and b = 2 and c >3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,abd的顺序可以任意调整
  2. = 和 in可以乱序,比如a = 1 and b =2 and c = 3建立(a, b, c) 索引可以任意顺序,mysql查询优化器会帮你优化
  3. 尽量选择区分度高的索引,区分度公式count(distinct col)/count(*) ,表示字段不重复的比例,比例越大我们的扫描记录越少,比例一般是需要join的字段要求是0.1以上,即平均1条扫描10条记录
  4. 索引不能参与计算,比如from_unixtime(create_time) = ‘2014-05-29’ 就不能使用到索引,因为b+tree中存的都是数据表中的字段值,但进行检索时,需要把素有元素都应用到函数才能比较,成本大,应该改成create_time = unix_timestamp(‘2014-05-29’)
  5. 尽量扩展索引,不要新建索引,比如表中已经有a索引,现在要加(a,b)索引,只需要修改原来的索引即可

索引失效情况总结

  1. 遵守最左匹配原则,中间断索引,使用范围查询
  2. 在索引列上做计算
  3. 索引字段使用 != 或者 <>
  4. 索引字段使用 is null 或者 is not null
  5. 使用通配符 %开头
  6. 索引字段是字符串,查询条件没有使用字符串
  7. 索引字段使用or

如何优化SQL语句

  1. 先看表的数据类型是否设计的合理,遵守选取数据类型越简单越小的原则
  2. 表中的碎片是否整理,MySQL表的碎片整理和空间回收
  3. 表的统计信息是否收集,只有统计信息准确,执行计划才可以帮助我们优化SQL
  4. 查看执行计划,检查索引的使用情况,没有用到索引,创建索引
  5. 创建索引需要判断这个字段是否适合创建索引,遵守建立索引的原则
  6. 创建索引后,通过explain分析,前后性能变化

如何分析explain执行计划

先查看type列,如果出现all关键词,就代表sql执行全表扫描

再看key列,如果null代表没有使用索引

再看rows列,如果越大,代表需要扫描的行数越多,相应耗时就长

最后看 extra列,是否有影响性能的 Using filesort 或者 Using temporary

explain 各个字段含义:https://blog.csdn.net/weixin_34062469/article/details/94498678

slect from a left join b on 条件 和 select from a left join b where 条件一样么,为什么

不一样,返回的结果不一样。

select * from a left join b on 条件 会返回 a 中没有匹配的数据

select * from a left join b where 条件 只返回where中匹配的数据

https://www.cnblogs.com/caowenhao/p/8003846.html

Spring如何解决循环依赖问题

比如A依赖B, B依赖A.

创建A的时候,会把A对应的ObjectFactory放入缓存中,当注入的时候发现需要B, 就会去调用B对象,B对象会先从singletonObjects 查找,没有再从earlySingletonObjects找,还没有就会调用singletonFactory创建对象B,B对象也是先从singletonObjects,earlySingletonObjects,singletonFactories三个缓存中搜索,只要找到就返回,相关方法AbstractBeanFactory.doGetBean()

Spring MVC运行流程

常见面试题 - 图4

  1. 客户端请求到DispatcherServlet
  2. DispatcherServlet根据请求地址查询映射处理器HandleMapping,获取Handler
  3. 请求HandlerAdatper执行Handler
  4. 执行相应的Controller方法,执行完毕返回ModelAndView
  5. 通过ViewResolver解析视图,返回View
  6. 渲染视图,将Model数据转换为Response响应
  7. 将结果返回给客户端

2,3 两步都在DispatcherServlet -> doDispatch中进行处理


Spring Boot启动流程

  1. 启动类里面调用SpringApplication.run方法
  2. 在run方法中,首先构造SpringApplication对象,然后再调用run方法
  3. 在构造SpringApplication对象中,做了如下工作
    • 将sources放入primarySources变量中
    • 判断webApplication是什么类型的
    • 设置ApplicationContextInitializer,ApplicationListener,通过加载META-INF/spring.factories中配置的类
    • 找到main方法找到启动主类
  4. run方法中,做的工作
    • StopWatch主要是监控启动过程,统计启动时间,检测应用是否已经启动或者停止。
    • 加载SpringApplicationRunListener(也是通过META-INF/spring.factories),默认加载的是EventPublishingRunListener
    • 调用RunListener.starting()方法。
    • 根据args创建应用参数解析器ApplicationArguments;
    • 准备环境变量:获取环境变量environment,将应用参数放入到环境变量持有对象中,监听器监听环境变量对象的变化(listener.environmentPrepared)
    • 打印Banner信息(SpringBootBanner)
    • 创建SpringBoot的应用上下文(AnnotationConfigEmbeddedWebApplicationContext)
    • prepareContext上下文之前的准备
    • refreshContext刷新上下文
    • afterRefresh(ApplicationRunner,CommandLineRunner接口实现类的启动)
    • 返回上下文对象

什么是 Spring Boot 自动配置?

  1. Spring Boot 在启动时扫描项目所依赖的 jar 包,寻找包含spring.factories 文件的 jar 包。
  2. 根据 spring.factories 配置加载 AutoConfigure 类。
  3. 根据 @Conditional 等条件注解的条件,进行自动配置并将 Bean 注入 Spring IoC 中。

spring事务传播机制

索引的基本原理

索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。

索引的原理:就是把无序的数据变成有序的查询

  1. 把创建了索引的列的内容进行排序
  2. 对排序结果生成倒排表
  3. 在倒排表内容上拼上数据地址链
  4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据

Redis线程模型、单线程快的原因

Redis基于Reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器 file event handler。这个文件事件处理器,它是单线程的,所以 Redis 才叫做单线程的模型,它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了 Redis 内部的线程模型的简单性。

文件事件处理器的结构包含4个部分:多个Socket、IO多路复用程序、文件事件分派器以及事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)。
多个 Socket 可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个 Socket,会将 Socket 放入一个队列中排队,每次从队列中取出一个 Socket 给事件分派器,事件分派器把 Socket 给对应的事件处理器。
然后一个 Socket 的事件处理完之后,IO多路复用程序才会将队列中的下一个 Socket 给事件分派器。文件事件分派器会根据每个 Socket 当前产生的事件,来选择对应的事件处理器来处理。

单线程快的原因:

1)纯内存操作

2)核心是基于非阻塞的IO多路复用机制

3)单线程反而避免了多线程的频繁上下文切换带来的性能问题

缓存雪崩、缓存穿透、缓存击穿

分布式锁解决方案

分布式事务解决方案

如何实现接口的幂等性