面试题解答

130 阅读19分钟

Java基础

创建线程的方式有哪些?

  • 继承Thread类创建线程
  • 实现Runnable接口创建线程
  • 使用Callable和Future创建线程
  • 使用线程池例如用Executor框架

三种方式的优缺点

  • 采用继承Thread类方式:

   (1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。

  • 采用实现Runnable接口方式:

   (1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

  • Runnable和Callable的区别:

(1)Callable规定的方法是call(),Runnable规定的方法是run().
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
(3)call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
(4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

start()和run()的区别

  • start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行
  • run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)

Callable和Runnable有什么区别?

(1)Callable规定的方法是call(),Runnable规定的方法是run().
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
(3)call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
(4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

简要描述线程池的基本原理,并介绍常见的阻塞队列

Java 线程池的实现原理其实就是一个线程集合 workerSet 和一个阻塞队列 workQueue。当用户向线程池提交一个任务(也就是线程)时, 线程池会先将任务放入 workQueue 中。workerSet 中的线程会不断的从 workQueue 中获取线程然后执行。 当 workQueue 中没有任务的时候, worker 就会阻塞, 直到队列中有任务了就取出来继续执行。\

线程池阻塞队列 image.png

简要描述线程池实现原理

Java 线程池的实现原理其实就是一个线程集合 workerSet 和一个阻塞队列 workQueue。当用户向线程池提交一个任务(也就是线程)时, 线程池会先将任务放入 workQueue 中。workerSet 中的线程会不断的从 workQueue 中获取线程然后执行。 当 workQueue 中没有任务的时候, worker 就会阻塞, 直到队列中有任务了就取出来继续执行。

volatile关键字有什么用处?

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的(可见性)。 2)禁止进行指令重排序,在一定程度上保证有序性。

Java内存中的哪些区域会发生OOM?

JVM划分:

image.png

  • 堆内存不足是最常见的 OOM 原因之一,抛出错误信息 java.lang.OutOfMemoryError:Java heap space,原因也不尽相同,可能是内存泄漏,也有可能是堆的大小设置不合理。
  • 对于虚拟机栈和本地方法栈,导致 OOM 一般为对方法自身不断的递归调用,且没有结束点,导致不断的压栈操作。类似这种情况,JVM 实际会抛出 StackOverFlowError , 但是如果 JVM 试图去拓展栈空间的时候,就会抛出 OOM.
  • 对于老版的 JDK, 因为永久代大小是有限的,并且 JVM 对老年代的内存回收非常不积极,所以当我们添加新的对象,老年代发生 OOM 的情况也非常常见。
  • 随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的 OOM 有所改观,出现 OOM,异常信息则变成了:“java.lang.OutOfMemoryError: Metaspace”。

常见垃圾回收算法有哪些?

标记-清除算法

标记的过程是:遍历所有的 GC Roots,然后将所有 GC Roots 可达的对象标记为存活的对象

清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。与此同时,清除那些被标记过的对象的标记,以便下次的垃圾回收。

这种方法有两个不足

  • 效率问题:标记和清除两个过程的效率都不高。
  • 空间问题:标记清除之后会产生大量不连续的内存碎片,碎片太多可能导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法(新生代)

为了解决效率问题,“复制”收集算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完,需要进行垃圾收集时,就将存活者的对象复制到另一块上面,然后将第一块内存全部清除。这种算法有优有劣:

  • 优点:不会有内存碎片的问题。
  • 缺点:内存缩小为原来的一半,浪费空间。

为了解决空间利用率问题,可以将内存分为三块: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一块 Survivor。回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才使用的 Survivor 空间。这样只有 10% 的内存被浪费。

但是我们无法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够,需要依赖其他内存(指老年代)进行分配担保。

分配担保

为对象分配内存空间时,如果 Eden+Survivor 中空闲区域无法装下该对象,会触发 MinorGC 进行垃圾收集。但如果 Minor GC 过后依然有超过 10% 的对象存活,这样存活的对象直接通过分配担保机制进入老年代,然后再将新对象存入 Eden 区。

标记-整理算法(老年代)

标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历 GC Roots,然后将存活的对象标记。

整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。

这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,因此每次垃圾回收会有大量对象存活,如果采用复制算法,每次需要复制大量存活的对象,效率很低。

分代收集算法

根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,针对各个年代的特点采用最适当的收集算法。

  • 新生代:复制算法
  • 老年代:标记-清除算法、标记-整理算法

哪些对象可以作为GC Root?

常说的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象。

一个对象可以属于多个root,GC root有几下种:

Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots。

Thread - 活着的线程

Stack Local - Java方法的local变量或参数

JNI Local - JNI方法的local变量或参数

JNI Global - 全局JNI引用

Monitor Used - 用于同步的监控对象

Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。

在Java语言里,可作为GC Roots对象的包括如下几种: 
a.虚拟机栈(栈桢中的本地变量表)中的引用的对象 
b.方法区中的类静态属性引用的对象 
c.方法区中的常量引用的对象 
d.本地方法栈中JNI的引用的对象

常见垃圾回收器有哪些?

JVM垃圾回收器

如何避免ThreadLocal产生内存泄漏?

ThreadLocal介绍

Java 集合

简要描述HashMap存入K:V的过程

jdk7的put过程

  1. 先判断key是否为null,如为null则调用putfornullkey(value)方法,在数组的表头位置插入或覆盖null kye的值;
  2. 根据键值key计算hash值得到插入的数组索引;
  3. 若该索引位置有值,则遍历该处的链表,若链表操作相同key,则覆盖并返回旧值;
  4. 若key对应桶位位null,或者链表中没有相同的key,则将修改计算加一;
  5. 判断实际存在的键值对数量size是否大于阈值,若大于则进行扩容并重新计算key对应新数组的桶位;
  6. 最后向对应桶位的表头插入新值,size加一。

jdk7的resize过程

  1. 先判断原数组的大小是否已经到达最大值,若已经到达最大值,则将阈值设为int的最大值,然后返回,至此以后该HashMap就不会再进行扩容了;

  2. 初始化一个新的entry数组,数组大小为原数组的两倍;

  3. 将原数组的数据转移至新数组里,转移的过程为:

    • 遍历数组,查询计算桶位,并已头插发插入新数组对应的位置,若当前位置是个链表,则继续遍历链表
  4. 最后将新数组的引用赋给table变量,并继续新的阈值。

jdk8的put过程

①.判断键值对数组是否为空或为null,若条件成立则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引,如果索引位置的值为null,直接新建节点添加,转向⑥,如果不为空,转向③;
③.判断桶位的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断桶位是否为treeNode,即该节点是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历该桶位的节点,判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作(这里用的尾插发);遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超过阈值,如果超过,进行扩容。

LinkedHashMap是如何实现有序性的?

LinkedHashMap之所以能保证有序性是因为在HashMap的Node基础上又增加了after和before字段,相当有又是一个双向链表来维护有序性。 csdn-HashMap#put (K key, V value)过程白话文

简要描述ArrayList扩容机制与过程

ArrayList中有两个属性,size:表示ArrayList的长度,elementData数组:用来存储数据的数组。 在Arraylist扩容时,调用Add方法,方法中会首先判断得到最小扩容量,如果你构造的ArrayList是用无参构造,即你创建ArrayList时没有确定它的长度,最小扩容量就为10和size+1当中的最大值,然后再判断是否需要扩容,如果最小扩容量大于elementData.length,就需要扩容,然后调用grow()方法,其中旧容量为elementData.length,新容量为elementData.length的1.5倍(new=old+old>>1),若新容量大于size+1,最小需要容量就为新容量,若新容量小于size+1,最小需要容量就为size+1,之后再将原来的数组用Arrays.copyOf方法复制到新数组,使新数组的长度为最小需要容量。

常用框架

简要描述Spring Bean生命周期

Spring Bean的完整生命周期从创建Spring容器开始,直到最终Spring容器销毁Bean(Spring Bean的生命周期) image.png

image.png 若容器注册了以上各种接口,程序那么将会按照以上的流程进行。

Spring中用到了哪些设计模式?

工厂设计模式

Spring使用工厂模式可以通过 BeanFactory 或 ApplicationContext 创建 bean 对象。

单例设计模式

Spring中bean的默认作用域就是singleton(单例)的.

在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。

代理设计模式

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

模板方法

模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。

观察者模式

用到的ApplicationEvent。

适配器模式

适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。

Spring AOP中的适配器模式 我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter 。Advice 常用的类型有:BeforeAdvice(目标方法调用前,前置通知)、AfterAdvice(目标方法调用后,后置通知)、AfterReturningAdvice(目标方法执行结束后,return之前)等等。每个类型Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptor、AfterReturningAdviceAdapter、AfterReturningAdviceInterceptor。Spring预定义的通知要通过对应的适配器,适配成 MethodInterceptor接口(方法拦截器)类型的对象(如:MethodBeforeAdviceInterceptor 负责适配 MethodBeforeAdvice)。

Spring MVC中的适配器模式 在Spring MVC中,DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter 适配器处理。HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。

装饰者模式

装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个Decorator套在原有代码外面。其实在 JDK 中就有很多地方用到了装饰者模式,比如 InputStream家族,InputStream 类下有 FileInputStream (读取文件)、BufferedInputStream (增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream 代码的情况下扩展了它的功能。

image.png

装饰者模式示意图

简要描述AOP实现原理

Spring提供了两种方式来生成代理对象: JdkProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。

  1. aop底层是采用动态代理机制实现的:接口+实现类
  2. 接口:spring采用jdk的动态代理proxy
  3. 实现类:spring采用cglib实现字节码增强
  4. 就是由代理创建出一个和impl实现类平级的一个对象,但是这个对象不是一个真正的对象,只是一个代理对象,但它可以实现和impl相同的功能,这个就是aop的横向机制原理,这样就不需要修改源代码

MySQL数据库

简要描述SQL语句执行过程

image.png

简要描述WAL机制及其实现原理

WAL,全称是Write-Ahead Logging, 预写日志系统。指的是 MySQL 的写操作并不是立刻更新到磁盘上,而是先记录在日志上,然后在合适的时间再更新到磁盘上。这样的好处是错开高峰期。日志主要分为 undo log、redo log、binlog。作用分别是 " 完成MVCC从而实现 MySQL 的隔离级别 "、" 降低随机写的性能消耗(转成顺序写),同时防止写操作因为宕机而丢失 "、" 写操作的备份,保证主从一致 "。

MySQL的WAL(Write-Ahead Logging)机制 - 简书 (jianshu.com)

简要描述MySQL事务实现原理

原子性,持久性和一致性主要是通过redo log、undo log、Force Log at Commit和DoubleWrite机制来完成的。 redo log用于在崩溃时恢复数据 undo log用于对事务回滚时进行撤销,也会用于隔离性的多版本控制。 Force Log at Commit机制保证事务提交后redo log日志都已经持久化。 Double Write机制用来提高数据库的可靠性,用来解决脏页落盘时部分写失效问题。

MySQL是如何实现事务的?

如果一条查询SQL优化到了极限,还有哪些方法可以提升查询速度?

分库分表

Redis

简要介绍Redis的5种基础数据类型及其对应数据结构

redis数据类型与数据结构

Redis为什么快?

  1. Redis是基于内存存储,内存读写速度快。

  2. 使用了非阻塞的IO多路复用机制。

  3. Redis对请求的处理是单线程的,避免线程切换的上下文切换和资源竞争问题。

  4. 使用高性能的数据结构,如跳跃表、哈希表等。

Redis在什么情况下会删除Key?是怎么删除的?

定时删除

  • 创建一个定时器,当设置的key到达到期时间时,由定时器任务立即执行对key的删除操作(这里的删除是将key和expires区域的数据全部删除)
  • 优点:节约内存,到时就删,快速释放掉不必要的内存占用
  • 缺点:CPU压力变大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器的响应时间和指令吞吐量
  • 一句话:用CPU换内存

惰性删除

  • 数据到期时不做删除,等下次访问时进行删除

    • 如果未过期返回数据
    • 发现已过期删除返回不存在
  • 优点:节约CPU的性能,发现必须删除的时候才删除

  • 确定:长期大量占用内存

  • 一句话:内存换CPU

定期删除

  • redis启动服务器初始化时,读取配置server.hz的值,默认是10(如图)\

image.png

image.png

  • 每秒钟执行server.hz次serverCron()(对服务器进行定时轮询)→ databasesCron()(对redis中每一个库进行访问)→activeExpireCycle()

  • activeExpireCycle()对每一个expires[*]逐一进行检测,每次执行250ms/server.hz

  • 对某一个expires[*]检测时,随机挑选W个key检测

    • 如果key超时,删除key
    • 如果一轮中删除的key数量>W*25%,继续检测该expires(循环该过程)
    • 如果一轮中删除的key数量<=W*25%,则检查下一个expires
    • W=ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP属性值
  • 参数current_db用于记录activeExpireCycle()进入哪个expires[*]执行(防止轮询时把某个expires落下)

  • 如果activeExpireCycle()执行时间到期,下次从current_db继续向下执行

  • 简单说就是周期性轮询redis库中的有时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度

  • 特点1:CPU性能占用设置有峰值,检测频度可自定义设置

  • 特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理

  • 总结:周期性抽查存储空间

删除策略比对

image.png

RabbitMQ

RabbitMQ是如何做到消息不丢失的

RabbitMQ是如何做到消息不丢失的?

简要描述RabbitMQ延迟队列实现原理

消息延迟队列实现的原理如下图:

在这里插入图片描述

原理解析: 1、生产者发送消息的队列中,且针对该队列没有消费者 2、由于队列没有消费者,消息在过期之后会进入到死信队列中 3、监听死信队列,并对死信队列中的消息进行消费

image.png 通过队列中的消息延迟转发实现延迟队列。