参考资料:

Java工程师面试 宝典学习说明_互联网校招面试真题面经汇总_牛客网 (nowcoder.com)

Java | JavaGuide(Java面试 + 学习指南)

一.Java基础面试题

1.谈谈你对面向对象的理解

​ 对比面向过程,是两种不同处理问题的角度,面向过程更注重事情的每一步骤及顺序,面向对象更注重事情有哪些参与者(对象),以及各自需要做什么。面向过程比较直接高效,面向对象易于复用、扩展和维护。

面向对象的三大基本特征:封装、继承、多态(父类应用指向子类对象)

2.JDK、JRE、JVM之间的区别

JDK java 开发工具

JRE(Java Runtime Environment Java 运行环境)

JVM java虚拟机

JDK = JRE + 开发工具集(例如 Javac,java 编译工具等)

JRE = JVM + Java SE 标准类库(java 核心类库)

3.==和equals⽅法的区别

==:如果是基本数据类型,⽐较是值,如果是引⽤类型,⽐较的是引⽤地址
equals:具体看各个类重写equals⽅法之后的⽐较逻辑,⽐如String类,虽然是引⽤类型,但是String类中重写了equals⽅法,⽅法内部⽐较的是字符串中的各个字符是否全部相等。

4.String,StringBuffer,StringBuilder的区别

1.String是不可变的,如果尝试去修改,会新⽣成⼀个字符串对象,StringBuffer和StringBuilder是可变的,修改是在原对象上操作的

2.StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更⾼

性能: StringBuilder > StringBuffer > String

5.重载和重写的区别

重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同、方法返回值和访问修饰符可以不同,发生在编译时。

重写:发生在父子类中,方法名,参数列表必须相同,返回值范围小于等于父类,抛出异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类访问修饰符为私有,子类不能重写该方法。

6.接口和抽象类的区别

1.抽象类可以有实现的方法和抽象的方法

2.抽象类的成员变量可以是各种类型的,接口中的成员变量只能是public static final类型(默认)的

3.抽象类只能继承一个,接口可以实现多个

4.关键字不同,接口的关键字是interface,抽象类的关键字是abstract

7.List和set的区别

List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用iterator取出所有的元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素。

Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用iterator接口取得所有元素,再逐一遍历各个元素。

8.hashCode()与equals()之间的关系

hashCode()的作用是获取哈希码,也称散列码,哈希码的作用是确定该对象在哈希表中的索引位置。

  • 如果两个对象相等,那么它们的hashCode()值一定相同

  • 如果两个对象hashCode()相等,它们并不一定相等

9.ArrayList和LinkedList的区别

ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问)。扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,将老数组的数据拷贝到新数组,然后插入需要加入到数组中的数据。

LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询。遍历LinkedList必须使用iterator不能使用for循环,因为for循环体内通过get(i)取得某一元素时都需要对list重新遍历,性能消耗大。

10.HashMap和HashTable的区别?底层实现是什么?

区别:

1.HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全

2.HashMap允许key和value为null,而HashTable不允许

底层实现: 数组 + 链表

11.ConcurrentHashMap

ConcurrentHashMap和HashTable都是线程安全的,但ConcurrentHashMap相比HashTable性能更高

12.如何实现一个IOC容器

1.配置文件配置包扫描路径

2.递归包扫描获取.class文件

3.反射、确定需要交给IOC管理的类

4.对需要注入的类进行依赖注入

13.什么是字节码?采用字节码的好处是什么?

什么是字节码?

Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转化为特定系统的机器码执行,在java中,这种供虚拟机理解的代码叫做字节码(即扩展名为.class的文件)。

采用字节码的好处是什么?

Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无需重新编译便可在多种计算机上运行。

14.Java类加载器有哪些

JDK有三个类加载器:

  1. bootstrap ClassLoader是ExClassLoader 的父类加载器,默认负责加载%JAVA_HOME%bin下的jar包和class文件
  2. ExClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/bin/ext文件夹下的jar包和class文件
  3. AppClassLoader是自定义加载器的父类,负责加载classpath下的类文件,系统类加载器,线程上下文加载器

自定义加载器的方法: 继承ClassLoader实现自定义加载器

15.双亲委派模型

双亲委派模型的执行流程是这样的:

1、当加载一个类时,会先从应用程序类加载器的缓存里查找相应的类,如果能找到就返回对象,如果找不到就执行下面流程;

2、在扩展加载器缓存中查找相应的类,如果能找到就返回对象,如果找不到就继续下面流程;

3、在启动类加载器中查询相应的类,如果找到就返回对象,如果找不到就继续下面流程;

4、在扩展加载器中查找并加载类,如果能找到就返回对象,并将对象加入到缓存中,如果找不到就继续下面流程;

5、在应用程序类加载器中查找并加载类,如果能找到就返回对象,并将对象加入到缓存中,如果找不到就返回 ClassNotFound 异常。

加载流程如下图所示:

image-20230904161407239

一般“双亲”指的是“父亲”和“母亲”,而在这里“双亲”指的是类加载类先向上找,再向下找的流程就叫做双亲委派模型。

双亲委派模型的好处

  1. 主要是为了安全性,避免用户自己编写的类动他替换java的一些核心类,比如String
  2. 同时避免了类的重复加载,因为jvm中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类

16.Java中的异常体系

  1. Java中所有异常都来自顶级父类Throwable
  2. Throwable下面有两个子类Exception和Error
  3. Error是程序无法处理的错误,一旦出现这个错误,程序将被迫停止运行;Exception不会导致程序停止
  4. Exception有分为两个部分RunTimeException运行时异常和CheckedException检查异常
  5. RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过。

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

  • 引用计数法:每一个对象有一个引用计数属性,新增一个引用计数加1,引用释放减1,计数为0时可以回收。(java中没有使用这个方法,原因:可能会出现A引用了B,B又引用了A,这时就算他们都不再使用了,但是因为他们互相引用,计数器=1永远无法被回收)
  • 可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就可以判断是可回收对象。

18.线程的生命周期,线程有哪些状态

线程通常有五种状态:创建、就绪、运行、阻塞、死亡状态

阻塞又分为三种情况:等待阻塞、同步阻塞、其他阻塞

  1. 新建状态:新创建了一个线程对象
  2. 就绪状态:线程对象创建后,其他线程调用了该对象的start()方法。该线程位于可运行线程池中,变得可运行,等待获取CPU的使用权
  3. 运行状态:就绪状态的线程获取了CPU,执行了程序代码
  4. 阻塞状态:阻塞状态是线程因为某种原因放弃了CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态
  5. 死亡状态:线程执行完了或者因为异常退出了run()方法,该线程结束生命周期

19.sleep(),wait(),join(),yield()的区别

sleep(),wait()的区别

  1. sleep()是Thread类的静态本地方法,wait()是Object类的本地方法
  2. sleep()不会释放锁(把锁带着进入冻结状态),wait会释放锁
  3. sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
  4. sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要(不指定时间需要被别人中断)
  5. sleep一般用于当前线程休眠,或者轮循暂停操作,wait则用于多线程之间的通信
  6. sleep会让出cpu执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行

yield()执行后线程直接进入就绪状态,马上释放了cpu,但是依然保留了cpu的执行资格,所有有可能cpu下次进行线程调度还会让这个线程取到执行权

join()执行后线程进入阻塞状态,例如在线程B中调用了A的join(),那线程B会进入到阻塞队列,直到线程A结束或者中断线程

20.说说你对线程安全的理解

线程安全讲的不是线程安全,应该是内存安全堆是共享内存,可以被所有线程访问

线程安全的定义:当多个线程访问一个对象的时候,如果不用进行额外的同步控制或者其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个线程数是安全的。

产生线程安全问题的原因: 在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内所有线程都可以访问到该区域,这就是造成问题的潜在原因。

21.说说你对守护线程的理解

守护线程:为非守护线程(用户线程)提供服务的线程,任何一个守护线程都是整个jvm中所有非守护线程的守护线程。

守护线程的作用:

举例:GC垃圾回收机制,就是一个经典的守护线程,当我们的程序不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就没事可做,所以当垃圾回收线程是jvm上仅剩的线程时,垃圾回收线程会自动离开,它始终再低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

应用场景:

  1. 为其他的线程提供服务支持情况
  2. 或者在任何情况下,程序结束时,这个线程必须正常的且立刻关闭,就可以作为守护线程来使用

22.ThreadLocal的原理和使用场景

同一个线程中通过ThreadLocal存进去的数据,在任何位置取出来是一致的

原理

每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及对应的值。

当执行set()方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。

get方法的执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。

由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立,互不影响,因此不会存在线程安全性问题,从而无需使用同步机制来保证多条线程访问容器的互斥性。

使用场景

  1. 在进行对象的跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束
  2. 线程间数据隔离
  3. 进行事务操作,用于存储事务信息
  4. 数据库连接,Session会话管理

23.ThreadLocal内存泄漏原因,怎么避免

内存泄漏:不会使用的对象或者变量占用的内存不能被回收,就是内存泄漏(内存泄漏为程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏的危害可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早会被占光,导致OOM。)

强引用:使用最普遍的引用(通过new),一个对象具有强应用,不会被垃圾回收器回收,即使是内存不足。我们想要取消强引用,可以显示的将引用赋值为null,jvm在合适的时间就会回收该对象。

ThreadLocal内存泄漏的根源

由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应key就会导致内存泄漏

怎么避免

  1. 每次使用ThreadLocal都调用它的remove()方法清除数据
  2. 将ThreadLocal变量定义成private static,这样就一直村子ThreadLocal的强应用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

24.产生内存泄漏的原因有哪些

  1. 资源未关闭或释放导致内存泄露(io资源,数据库的连接)
  2. 使用 ThreadLocal 造成内存泄露
  3. 静态集合类引起内存泄漏,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。生命周期长的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
  4. 重写了 finalize() 的类,如果 finalize() 方法重写的不合理或 finalizer 队列无法跟上 Java 垃圾回收器的速度,那么迟早,应用程序会出现 OutOfMemoryError 异常

25.并发、并行、串行的区别

串行在时间上不可能发生重叠,前一个任务没有搞定,下一个任务只能等

并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行

并发允许两个任务彼此干扰,同一时间点,只有一个任务运行,交替执行

26.并发的三大特性

  • 原子性:是指在在一个操作中,CPU不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。
  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他的线程能够立即看到修改的值。
  • 有序性:虚拟机再进行代码编译的时,对于那些改变顺序之后不会对最终的结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将它们重排序。

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

为什么

  1. 降低资源的消耗;提高线程的利用率,降低创建和销毁现成的消耗
  2. 提高响应速度,任务来了,直接有线程可用执行,不是创建线程之后再执行
  3. 提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控

线程池参数

  • corePoolSize代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建之后不会消除,而是一种常驻线程。
  • maxnumPoolSize代表最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足要求时,此时就会创建新的线程,但是线程池总数不会超过最大线程数
  • keepAliveTime空闲线程存活时间,当一个可被回收的线程的空闲时间大于keepAliveTime,就会被回收; unit:keepAliveTime的时间单位
  • workQueue工作队列,存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在工作队列,任务调度时再从队列中取出任务。它仅仅用来存放被execute()方法提交的Runnable任务。工作队列实现了BlockingQueue接口。
  • threadFactory线程工厂,创建线程的工厂,可以设定线程名、线程编号等。
  • handler拒绝策略,当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需实现RejectedExecutionHandler接口。

28.简述线程池的处理流程

image-20230905204135018

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

线程池中阻塞队列的作用?

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

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

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

为什么是先添加队列而不是先创建最大线程?

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

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

30.线程池中线程复用原理

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

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

31.Spring是什么

轻量级的开源的j2EE框架。他是一个容器框架,用来装javabean(java对象),中间层框架(万能胶)可以起一个连接作用,比如说把struts和hibernate粘合在一起运用,可以让我们的企业开发更快、更简洁

Spring是一个轻量级的控制反转(ioc)和面向切面(aop)的容器框架

  • 从大小与开销两方面而言Spring都是轻量级的
  • 通过控制反转(ioc)的技术达到松耦合的目的
  • 提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚的开发
  • 包含并管理应用对象(bean)的配置和生命周期,这个意义上是一个容器
  • 将简单的组件配置、组合成复杂的应用,这个意义上是一个框架

32.谈谈你对AOP的理解

系统是由许多不同的组件所组成的,每一个组件各负责一块特定的功能。除了实现自身的核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为他们会跨域系统的多个组件。

当我们需要将分散的对象引入公共行为的时候,OOP则显得无能为力,也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能

AOP:将程序中的交叉业务逻辑(比如安全、日志、事务),封装成一个切面。然后注入到目标对象(具体业务逻辑)中去。AOP可以对某个对象或者某些功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外做一些事情,在某个方法执行之后额外做一些事情。

33.谈谈你对IOC的理解

容器概念、控制反转、依赖注入三方面理解

容器概念

ioc容器:实际上就是个map(key、value),里面存的就是各种对象(在xml里配置bean节点、@Repository、@Service、@Controller),在项目启动的时候会读取配置文件路面的bean节点。

控制反转

在没有引入ioc容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。

引入ioc容器之后,对象A和对象B之间失去了直接联系。当对象A运行到需要对象B的时候,ioc容器会主动创建一个对象B注入到对象A需要的地方。

通过前后的对比,不难看出来:对象A获得依赖对象B的过程,由主动行为变成了被动行为,控制权颠倒过来了,这就是控制反转这个名称的由来。

依赖注入

“获得依赖对象的过程被反转了”。控制器反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入。依赖注入是实现IOC的方法,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。

34.BeanFactory和ApplicationContext有什么区别?

ApplicationContext是BeanFactory的子接口

ApplicationContext提供了更完整的功能

1、继承了MessageSource,因此支持国际化

2、统一资源文件的访问方式

3、提供在监听器中注册bean的事件

4、同时加载多个配置文件

5、载入多个(有继承关系)上下文,使得每一个上下文都专注于一个特定的层次,比如应用的web层

35.深拷贝和浅拷贝

深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实列对象的引用

  • 浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是一个对象
  • 深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的类执行指向的不是同一个对象

举例:A对象中有一个user属性,A1拷贝A对象,两个user指向同一个对象的话,就是浅拷贝,反之是深拷贝

36.TCP和UDP有什么区别?TCP为什么是三次握手,而不是两次?

TCP (Transfer Control Protocol)是一种面向连接的、可靠的、传输层通信协议

  • 面向连接的,点对点的通信,高可靠,效率比较低、占用系统的资源比较多(类似于打电话)

UDP (User Datagram Protocal) 是一种无连接、不可靠的、传输层通信协议

  • 不需要连接、发送方不管接收方有没有准备好,直接发消息;可以进行广播发送的;传输不可靠,有可能会丢失消息;效率比较高;协议比较简单,占用的系统资源少(类似于广播)

TCP为什么是三次握手,而不是两次?

如果是两次握手,可能会造成连接资源浪费的问题

37.JVM中有哪些垃圾回收算法?

MarkSweep标记清除算法

Copying拷贝算法

MarkCompack标记压缩算法

38.集合相关的面试题

image-20230926110554758

image-20230926122104134

image-20230926121325686

image-20230926121517150

image-20230926122420662

image-20230926122546930

image-20230926133043725

image-20230926133301807

二.框架篇相关面试题

1.Spring Bean的生命周期

1.创建前准备阶段

这个阶段主要是在开始Bean加载之前,从Spring上下文和相关配置中解析并查找Bean有关的配置内容,比如init-method-容器在初始化bean时调用的方法、destory-method,容器在销毁Bean时调用的方法。以及,BeanFactoryPostProcessor这类的bean加载过程中的前置和后置处理。这些类或者配置其实是Spring提供给开发者,用来实现Bean加载过程中的扩展机制,在很多和Spring集成的中间件经常使用,比如Dubbo。

2.创建实例阶段

这个阶段主要是通过反射来创建Bean的实例对象,并且扫描和解析Bean声明的一些属性。

3.依赖注入阶段

在这个阶段,会检测被实例化的Bean是否存在其他依赖,如果存在其他依赖,就需要对这些被依赖Bean进行注入。比如通过@Autowired、@Setter等依赖注入的配置。在这个阶段还会触发一些扩展的调用,比如常见的扩展类:BeanPostProcessors(用来实现Bean初始化前后的回调)、InitializingBean类(这个类有一个afterPropertiesSet()方法,给属性赋值)、还有BeanFactoryAware等等。

4.容器缓存阶段

容器缓存阶段主要是把Bean保存到IoC容器中缓存起来,到了这个阶段,Bean就可以被开发者使用了。这个阶段涉及到的操作,常见的有init-method这个属性配置的方法,会在这个阶段调用。比如BeanPostProcessors方法中的后置处理器方法postProcessAfterInitialization,也是在这个阶段触发的。

5.销毁实例阶段

这个阶段,是完成Spring应用上下文关闭时,将销毁Spring上下文中所有的Bean。如果Bean实现了DisposableBean接口,或者配置了destory-method属 性,将会在这个阶段被调用。

实例化 -> 属性赋值 -> 初始化 -> 销毁

2.Spring框架中的单例Bean是线程安全的吗?

不是线程安全的!

Sping中Bean默认是单例模式的,框架中并没有对Bean进行多线程的封装处理。

线程安全这个问题,要从单例与原型Bean分别进行说明。

「原型Bean」对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。

「单例Bean」对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。

如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行「查询」以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。

3.Spring Bean作用域

Spring 的 bean 作用域(scope)类型有5种:

1、singleton:单例,默认作用域。

2、prototype:原型,每次创建一个新对象。

3、request:请求,每次Http请求创建一个新对象,适用于WebApplicationContext环境下。

4、session:会话,同一个会话共享一个实例,不同会话使用不用的实例。

5、global-session:全局会话,所有会话共享一个实例。

4.Spring框架中用到了哪些设计模式?

  1. 简单工厂:由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。Spring中的BeanFactory就是简单工厂模式的体现,根据传入的一个唯一的标识来获得Bean对象,但是否在传入参数后创建还是传入参数前创建这个要根据具体情况来定。
  2. 工厂方法:实现了FactoryBean接口的bean是一类叫做factory的bean.其特点是,spring会在使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个bean.getObject()方法的返回值。
  3. 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。spring对单例的实现:spring中的单例模式完成了后半句话,即提供了全局访问点BeanFactory,但没有从构造器级别去控制单例,这时因为spring管理的是任意的java对象。
  4. 适配器模式:spring中定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类,让适配器代替controller执行响应的方法。这样扩展controller时,只需要增加一个适配类就完成了springMvc的扩展了。
  5. 装饰器模式:动态地给一个对象增加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。spring中用到的装饰器模式在类名上有两种表现:一是类名中含有wrapper,另一种时类名中含有Decorator.
  6. 动态代理:切面在应用运行的时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面的。 织入:把切面应用到目标对象并创建新代理对象的过程。
  7. 观察者模式:Spring的事件驱动模型使用的是观察者模式,spring中observer模式常用的地方是listener的实现。
  8. 策略模式:spring框架的资源访问Resource接口。该接口提供了更强的资源访问能力,spring框架本身大量使用了Resource接口来访问底层资源。

5.spring中事务实现方和原理式以及隔离级别?

实现方式

在使用Spring框架时,可以有两种使用事务的方式,一种是编程式,一种是申明式的,@Transactional注解就是申明式的。

首先,事务这个概念是数据库层面的,Spring只是基于数据库中的事务进行了扩展,以及提供了一些能让程序员更加方便操作事务的方式。比如我们可以通过在某个方法上增加@Transactional注解,就可以开启事务,这个方法中所有的sql都会在一个事务中执行,统一成功失败。

原理

在一个方法上加了@Transactional注解后,Spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当在使用这个代理对象的方法时,如果这个方法存在@Transactional注解,那么代理逻辑会先把事务的自动提价设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有异常,那么代理逻辑中就会将事务进行提交,如果执行业务逻辑方法出现了异常,那么则会将事务进行回滚。当然,针对哪些异常回滚事务是可以配置的,可以利用@Transactional注解中的rollbackFor属性进行配置,默认情况下会对RuntimeException和Error进行回滚。

隔离级别

spring的事务隔离级别就是数据库的隔离级别外加一个默认级别

  1. read uncommitted(未提交读)
  2. read committed(提交读、不可重复读)
  3. repeatable read (可重复读)
  4. serializable(可串行化)

注:数据库设置的隔离级别会被spring的配置覆盖

6.Spring事务传播机制

多个事务方法相互调用的时,事务是如何在这些方法间传播

方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。

REQUIRED(Spring默认的事务传播类型)如果当前没有事务,则自己创建一个事务,如果当前存在事务,则加入这个事务

SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行

MANDATORY:当前存在事务,则加入当前事务,如果当前的事务不存在,则抛出异常

REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起该事务

NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务

NEVER:不使用事务,如果当前事务存在,则抛出异常

NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)

7.Spring事务什么时候会失效?

Spring事务原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了,常见情况有如下几种

  1. 发生自调用,类里面使用this调用本类的方法,此时这个this对象不是代理类,而是UserService对象本身
  2. 方法不是public的,非要在非public上使用事务,可以开启Aspectj代理模式
  3. 数据库不支持事务,例如使用的MyISAM存储引擎
  4. 没有被spring管理
  5. 异常被吃掉,事务不会回滚(或者抛出的异常没有定义,默认为RuntimeException)

8.什么是bean的自动装配,有哪些方式?

autowire属性有五种装配的方式

  • no – 缺省情况下,自动配置是通过“ref”属性手动设定
1
<!--手动装配:以value或ref的方式明确指定属性值都是手动装配。 需要通过‘ref’属性来连接bean-->
  • byName-根据bean的属性名称进行自动装配
1
2
3
<!--Cutomer的属性名称是person,Spring会将bean id为person的bean通过setter方法进行自动装配-->
< bean id=“cutomer” class=“com.xxx.xxx.Cutomer” autowire=“byName”/>
< bean id=“person” class=“com.xxx.xxx.Person”/>
  • byType-根据bean的类型进行自动装配
1
2
3
<!--Cutomer的属性person的类型为Person,Spirng会将Person类型通过setter方法进行自动装配-->
< bean> id=“cutomer” class=“com.xxx.xxx.Cutomer” autowire=“byType”/>
< bean> id=“person” class=“com.xxx.xxx.Person”/>
  • constructor-类似byType,不过是应用于构造器的参数。如果一个bean与构造器参数的类型形
    同,则进行自动装配,否则导致异常
1
2
3
<!--Cutomer构造函数的参数person的类型为Person,Spirng会将Person类型通过构造方法进行自动装配-->
< bean> id=“cutomer” class=“com.xxx.xxx.Cutomer” autowire=“construtor”/>
< bean> id=“person” class=“com.xxx.xxx.Person”/>
  • autodetect-如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType方式 进行自动装配
1
<!--如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType方式进行自动装配-->

9.Spring Boot、SpringMVC和Spring有什么区别?

Spring是一个IOC容器,用来管理Bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补OOP的代码重复问题,更方便将不同方法中的共同处理抽取成切面,自动注入给方法执行,比如日志、异常等

SpringMvc是spring对web框架的一个解决方案,提供了一个总的前端控制器Servlet,用来接受请求,然后定义了一套路由策略(url到handle的映射)及适配执行handle,将handle结果使用视图解析技术生成视图展现给前端

SpringBoot是Spring提供的一个快速开发工具包,让程序员更方便、更快速的开发spring + springmvc应用,简化了配置(约定了默认配置),整合了一系列的解决方案(starter机制),redis、mongodb、es可以开箱即用

10.SpringMVC的工作流程

  1. 用户发送请求到前端控制器DispatcherServlet
  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器
  3. 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器及处理器拦截器(如果有则生成)一并返回给DispatcherServlet
  4. DispatcherServlet调用HandlerAdapter处理器适配器
  5. HandlerAdapter调用具体的处理器(controller,也叫后端控制器)
  6. Controller执行完成返回ModelAndView
  7. HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet
  8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器
  9. ViewReslover解析后返回具体view
  10. DispatcherServlet根据view进行渲染视图(即将模型数据填充到视图中)
  11. DispatcherServlet响应给用户

image-20230911141826287

11.SpringMvc中的九大组件

Handler是用来干活的工具;HandlerMapping用于根据需要干的活找到相应的工具;HandlerAdapter是使用工具干活的人

  1. HandlerMapping是用来查找Handler的。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行处理呢?这就是HandlerMapping需要做的事。

  2. HandlerAdapter,从名字上看,它就是一个适配器。因为SpringMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。

  3. HandlerExceptionResolver其它组件都是用来干活的。在干活的过程中难免会出现问题,出问题后怎么办呢?这就需要有一个专门的角色对异常情况进行处理,在SpringMVC中就是HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。

  4. ViewResolverViewResolver用来将String类型的视图名和Locale解析为View类型的视图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html(也可能是其它类型)文件。这里就有两个关键问题:使用哪个模板?用什么技术(规则)填入参数?这其实是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交由不同的视图自己完成。

  5. RequestToViewNameTranslatorViewName是根据ViewName查找View,但有的Handler处理完后并没有设置View也没有设置ViewName,这时就需要从request获取ViewName了,如何从request中获取ViewName就是RequestToViewNameTranslator要做的事情了。RequestToViewNameTranslator在Spring MVC容器里只可以配置一个,所以所有request到ViewName的转换规则都要在一个Translator里面全部实现。

  6. LocaleResolver解析视图需要两个参数:一是视图名,另一个是Locale。视图名是处理器返回的,Locale是从哪里来的?这就是LocaleResolver要做的事情。LocaleResolver用于从request解析出Locale,Locale就是zh-cn之类,表示一个区域,有了这个就可以对不同区域的用户显示不同的结果。SpringMVC主要有两个地方用到了Locale:一是ViewResolver视图解析的时候;二是用到国际化资源或者主题的时候。

  7. ThemeResolver用于解析主题。SpringMVC中一个主题对应一个properties文件,里面存放着跟当前主题相关的所有资源、如图片、css样式等。SpringMVC的主题也支持国际化,同一个主题不同区域也可以显示不同的风格。SpringMVC中跟主题相关的类有 ThemeResolver、ThemeSource和Theme。主题是通过一系列资源来具体体现的,要得到一个主题的资源,首先要得到资源的名称,这是ThemeResolver的工作。然后通过主题名称找到对应的主题(可以理解为一个配置)文件,这是ThemeSource的工作。最后从主题中获取资源就可以了。

  8. MultipartResolver用于处理上传请求。处理方法是将普通的request包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调用getFileMap得到FileName->File结构的Map。此组件中一共有三个方法,作用分别是判断是不是上传请求,将request包装成MultipartHttpServletRequest、处理完后清理上传过程中产生的临时资源。

  9. FlashMapManager用来管理FlashMap的,FlashMap主要用在redirect中传递参数。

12.SpringBoot自动配置原理(简答的阐述见下面15条)

自动装配,简单来说就是自动把第三方组件的Bean装载到Spring IOC器里面,不需要开发人员再去写Bean的装配配置。

在Spring Boot应用里面,只需要在启动类加上@SpringBootApplication注解就可以实现自动装配。@SpringBootApplication是一个复合注解,真正实现自动装配的注解是@EnableAutoConfiguration。

image-20230911195638524

13.MyBatis的优缺点

优点:

  1. 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用
  2. 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接
  3. 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)
  4. 能够与Spring很好的集成
  5. 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护

缺点:

1.
SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求
2. SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库

14.MyBatis中#{}和${}的区别是什么?

  1. 功能不同:${} 是直接替换,而 #{} 是预处理
  2. 使用场景不同:普通参数使用 #{},如果传递的是 SQL 命令或 SQL 关键字,需要使用 ${},但在使用前一定要做好安全验证
  3. 安全性不同:使用 ${} 存在安全问题(SQL注入),而 #{} 则不存在安全问题

15.Spring Boot自动装配的过程

image-20230921145126572

整个自动装配的过程是:Spring Boot通过@EnableAutoConfiguration注解开启自动配置,加载spring.factories中注册的各AutoConfiguration类,当某个AutoConfiguration类满足其注解@Conditional指定的生效条件(Starters提供的依赖、配置或Spring容器中是否存在某个Bean等)时,实例化该AutoConfiguration类中定义的Bean(组件等),并注入Spring容器,就可以完成依赖框架的自动配置。

二.Mysql相关面试题

1.存储引擎InnoDB 与MyISAM 的区别

  1. 数据存储的方式不同,MyISAM中的数据和索引是分开存储的,而InnoDB是把索引和数据存储在同一个文件里面
  2. 对于事务的支持不同,MyISAM不支持事务,而InnoDB支持ACID特性的事务处理
  3. 对于锁的支持不同,MyISAM只支持表锁,而InnoDB可以根据不同的情况,支持行锁,表锁,间隙锁,临键锁
  4. MyISAM不支持外键,InnoDB支持外键因此基于这些特性,我们在实际应用中,可以根据不同的场景来选择合适的存储引擎
  5. 比如如果需要支持事务,那必须要选择InnoDB。如果大部分的表操作都是查询,可以选择MyISAM

2.索引的基本原理

原理:把无序的数据变成有序的查询

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

3.索引失效的场景

  1. 索引在使用的时候没有遵循最左匹配法则.
  2. 模糊查询,如果%号在前面也会导致索引失效。
  3. 在添加索引的字段上进行了运算操作或者类型转换也都会导致索引失效。
  4. 如果使用了复合索引,中间使用了范围查询,右边的条件索引也会失效
  5. 查询的时候发生了类型转换,在查询的时候做了运算的操作和模糊查询也会导致索引失效

4.事务的隔离级别有哪些?MySQL 的默认隔离级别是什么?

读未提交(Read Uncommitted)可能读到其他事务未提交的数据,也叫做脏读
读已提交(Read Committed)两次读取结果不一致,叫做不可重复读
可重复读(Repeatable Read)是mysql默认的隔离级别,每次读取的结果都一样,当时可能产生幻读
串行化(Serializable)一般是不会使用的,他给每一行读取的数据加锁,会导致大量超时和锁竞争的问题
Mysql 默认的事务隔离级别是可重复读(Repeatable Read)

5.事务的基本特性

事务的基本特性ACID分别是:

  1. A 原子性:一个事务的操作要么全部成功,要么全部失败
  2. C 一致性:数据库从一个一致性的状态转换到另一个一致性的状态
  3. I 隔离性:一个事务的修改在最终提交前,对其他事务是不可见的
  4. D 持久性 一旦事务提交,所做的修改就会永远的保存到数据库中

6.怎么处理慢查询

SQL查询慢的原因

  1. 查询没有命中索引
  2. 查询了不需要的数据列
  3. 数据量太大

根据上面的原因给出优化的措施

  1. 分析语句的执行计划,然后获得其使用索引的情况,然后修改语句或者索引,使得语句可以尽可能的命中索引
  2. 分析语句,是否查询了不需要的数据列
  3. 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表

7.ACID靠什么保证的

A 原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql

C 一致性由其他的三大特性保证 程序代码要保证业务上的一致性

I 隔离性由MVCC来保证

D 持久性由内存 + redo log 来保证。Mysql修改数据同时在内存和redo log记录这次操作,宕机的时候可以从redo log恢复

三.Redis相关面试题

1.Redis过期键的删除策略

redis是key-value数据库,我们可以设置redis中缓存的key的过期时间。redis的过期策略就是指当redis中缓存的key过期了,redis该如何处理。

  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则删除。该策略可以最大化地节省CU资源,但是对内存非常的不友好。极端地情况可能出现大量地过期key没有再次被访问,从而不被清除,占用大量内存。
  • **定期过期:**每隔一定地时间,会扫描一定数量地数据库地expires字典中一定数量地key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过定时扫描的时间间隔和每次扫描的限定功耗,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

在Redis中同时使用了这两种策略

2.Redis的线程模型,单线程为什么快

IO多路复用机制监听多个Socket

单线程快的原因:

  1. 纯内存操作
  2. 核心是基于非阻塞的IO多路复用机制
  3. 单线程反而避免了多线程的频繁上下文切换带来的性能问题

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

缓存雪崩 缓存在同一时间大面积失效,所以,后面的请求都会落在数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案:

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
  2. 给每一个缓存数据增加响应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存
  3. 缓存预热,启动系统之前先把热点数据放在缓存中去
  4. 互斥锁

缓存穿透 缓存和数据库中都没有数据,导致所有的请求都落在数据库上,造成数据库短时间内承受大量请求

解决方案:

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截掉
  2. 从缓存取不到数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短一些,如30秒(设置太长会导致正常的情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

缓存击穿 缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没有读到数据,又同时去数据库取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

  1. 设置热点数据永不过期
  2. 加互斥锁

4.Redis的数据结构

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(无序集合)及zset(有序集合)

image-20230914154911197

PDF