Java常见面试题 非常实用【个人经验】
Java 面试题
一. 容器部分
二. 多线程部分
三. SpringMvc部分
四. Mybatis部分
五. MySQL部分
六. Redis部分
七. RabbitMQ部分
八. JVM虚拟机部分
九. 算法知识部分
十. 其他面试部分
更新
- 时间:
2020/08/10
- 内容: JVM虚拟机部分
- 预更: 算法部分
容器部分面试题
Java 容器都有哪些
Collection
的子类List
、Set
List
的子类ArrayList
、LinkedList
等Set
的子类HashSet
、TreeSet
等Map
的子类HashMap
、TreeMao
等
Collecion 和 Collections 有什么区别
java.util,Collection
是一个集合的顶级接口,它提供了对集合对象进行基本操作的通用接口方法,Collection 接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口的有 List 和 Setjava.util.Collections
是一个包装类(工具类),它包含了各种有关集合操作的静态多态方法。此类不能被实例化,用于对集合中元素进行排序、搜索以及线程安全等各种操作,服务于 Java 的 Collection 框架
List、Set、Map 之间的区别
- List、Set 都继承 Collection 接口,而 Map 则不是
- List 是一个有序集合,元素可重复,可有多个NULL值。可以使用各种循环遍历集合,因为它是有序的
- Set 是一个无序集合,元素不可重复,重复元素会被覆盖掉(注意:元素虽然无放入顺序,但元素在 Set 中的位置是由该元素的 HashCode 决定的,其位置是固定的,加入 Set 的 Object 必须定义
equals()
方法),Set 只能用迭代,因为它是无序的- Map 是由一系列键值对组成的集合,提供了 key 和 value 的映射。在 Map 中保证 key 与 value 一一对应的关系。一个 key 对应一个 value ,不能存在相同的 key,但 value 可以相同
- Set 与 List 相比较
- Set 检索元素效率较低,删除和插入效率高,因为删除和插入不会引起元素的位置变化
- List 可动态增长,查找元素效率高,但是删除和插入效率低,因为删除或插入一条元素,会引起其他元素位置变化
- Map 适合存储键值对的数据
HashMap 和 HashTable 的区别
- 继承的父类不同,但二者都实现了 Map接口
- HashTable 继承自
Dictionary
类- HashMap 继承自
AbstractMap
类
- 线程安全不同
- HashMap 在缺省的情况下是非Synchronize的
- HashTable 的方法是Synchronize的
- 在多线程下直接使用 HashTable 不需要自己为它的方法实现同步。但使用 HashMap 时需要手动增加同步处理
Map m = Collections.synchronizeMap(hashMap);
- 是否提供 contains() 方法
- HashMap 把 HashTable 的 contains() 方法去掉了,改成了 containsValue() 和 containsKey(),因为 contains 容易让人误解
- HashTable 则保留了 contains()、containsValue()、containsKey() 三个方法,其中 contains() 与 containsValue() 功能相同
- key 和 value 是否可以为 null 值
- HashTable 中,key 和 value 都不能为 null 值
- HashMap 中,null 作为键,但这样的键只有一个。可以有多个 value 为 null 值的键。当 get() 方式返回 null 有可能是 HashMap 中没有该键,也有可能返回的 value 为 null。所以 HashMap 用
containsKey()
方法判断是否存在键
- 遍历方式不同
- HashTable 与 HashMap 都是使用 Iterator 迭代器遍历,而由于历史的原因,HashTable 还使用了
Enumeration
的方式
- hash 值不同
- 哈希值的使用不同,HashTable直接使用对象的HashCode,而 HashMap 重新计算哈希值
- hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值
- HashTable 使用的取模运算
- HashMap 使用的与运算,先用
hash & 0x7FFFFFFF
后,再对 length 取模,&0x7FFFFFFF
的目的是为了将负的 hash 值转化为正值,因为 hash 值有可能为负数,而&0x7FFFFFFF
后,只有符号外改变,而后面的位都不变
- 内部实现使用的数组初始化和扩容方式不同
- HashTable 在不指定容量的情况下默认是11,而 HashMap 为16,HashTable 不要求底层数组的容量一定要是2的整数次幂,而 HashMap 底层数组则一定为2的整数次幂
- HashTable 扩容时,将容量变成原来的2倍+1 (old * 2 + 1),而 HashMap 则直接改为原来的2倍 (old * 2)
如何决定使用 HashMap 还是 TreeMap
- 如果需要得到一个有序的 Map 集合就应该使用 TreeMap (因为 HashMap 的排序顺序不是固定的)除此之外,由于 HashMap 有比 TreeMap 更好的性能,在不需要使用排序的情况下使用 HashMap 会更好
HashMap 的实现原理
- 利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
HashSet 的实现原理
- HashSet 实际上是一个
HashMap
实例,都是一个存放链表的数组。它不保证存储元素的迭代顺序;此类允许使用 null 元素。HashSet 中不允许有重复元素,这是因为 HashSet 是基于 HashMap 实现的,HashSet 中的元素都存放在 HashMap 的 key 上面,而 value 中的值都是统一的一个固定对象private static final Object PRESENT = new Object();
- HashSet 中 add() 方法调用的是底层 HashMap 中的 put() 方法,而如果是在 HashMap 中调用 put() ,首先会判断 key 是否存在,如果 key 存在则修改 value 值,如果 key 不存在这插入这个 key-value。而在 set 中,因为 value 值没有用,也就不存在修改 value 值的说法,因此往 HashSet 中添加元素,首先判断元素(也就是key)是否存在,如果不存在这插入,如果存在着不插入,这样 HashSet 中就不存在重复值。所以判断 key 是否存在就要重写元素的类的 equals() 和 hashCode() 方法,当向 Set 中添加对象时,首先调用此对象所在类的 hashCode() 方法,计算次对象的哈希值,此哈希值决定了此对象在Set中存放的位置;若此位置没有被存储对象则直接存储,若已有对象则通过对象所在类的 equals() 比较两个对象是否相同,相同则不能被添加。
ArrayList 与 LinkList 的区别是什么
- AarrayList 是动态数组构成 LinkList 是链表组成
- AarrayList 常用于经常查询的集合,因为 LinkList 是线性存储方式,需要移动指针从前往后查找
- LinkList 常用于新增和删除的集合,因为 ArrayList 是数组构成,删除某个值会对下标影响,需要进行数据的移动
- AarrayList 自由度较低,需要手动设置固定的大小,但是它的操作比较方便的,①直接创建②添加对象③根据下标进行使用
- LinkList 自由度较高,能够动态的随数组的数据量而变化
- ArrayList 主要开销在List需要预留一定空间
- LinkList 主要开销在需要存储结点信息以及结点指针信息
如何实现数组与 List 之间的转换
- List to Array : 可以使用 List 的
toArray()
方法,传入一个数组的类型例如Stirng[] strs = strList.toArray(new String[strList.size()]);
- Array to List : 可以使用
java.util.Arrays
的asList()
方法 例如List<String> strList = Arrays.asList(strs);
ArrayList 与 Vector 的区别是什么
- ArrayList 是非线程安全的,而 Vector 使用了
Synchronized
来实现线程同步的- ArrayList 在性能方面要优于 Vector
- ArrayList 和 Vector 都会根据实际情况来动态扩容的,不同的是 ArrayList 扩容到原大小的1.5倍,而 Vector 扩容到原大小的2倍
Array 与 ArrayList 有什么区别
- Array 是数组,当定义数组时,必须指定数据类型及数组长度
- ArrayList 是动态数组,长度可以动态改变,会自动扩容,不使用泛型的时候,可以添加不同类型元素
在 Queue 中 poll() 与 remove() 有什么区别
- poll() 和 remove() 都是从队列头删除一个元素,如果队列元素为空,remove() 方法会抛出
NoSuchElementException
异常,而 poll() 方法只会返回 null
哪些集合类是线程安全的
- Vector :比 ArrayList 多了同步化机制(线程安全)
- HashTable :比 HashMap 多了线程安全
- ConcurrentHashMap :是一种高效但是线程安全的集合
- Stack :栈,继承于 Vector 也是线程安全
迭代器 Iterator 是什么
Iterator
是集合专用的遍历方式
Iterator<E> iterator()
: 返回此集合中元素的迭代器,通过集合的iterator()
方法得到,所以Iterator
是依赖于集合而存在的
Iterator 怎么使用 ? 有什么特点
Iterator 的使用方法
java.lang.Iterable
接口被java.util.Collection
接口继承,java.util.Collection
接口的iterator()
方法返回一个Iterator
对象
next()
方法获取集合中下一个元素
hasNext()
方法检查集合中是否还有元素
remove()
方法将迭代器新返回的元素删除
Iterator 的特点
- Iterator 遍历集合过程中不允许线程对集合元素进行修改
- Iterator 遍历集合过程中可以用
remove()
方法来移除元素,移除的元素是上一次Iterator.next()
返回的元素- Iterator 的
next()
方法是通过游标指向的形式返回Iterator下一个元素
Iterator 与 LinkIterator 有什么区别
- 使用范围不同
- Iterator 适用于所有集合, Set、List、Map以及这些集合的子类型,而 ListIterator 只适用于 List 及其子类型
- ListIterator 有 add() 方法,可以向 List 中添加元素,而 Iterator 不能
- ListIterator 和 Iterator 都有 hasNext() 和 next() 方法,来实现顺序向后遍历。而 ListIterator 有 hasPrevious() 和 previous() 方法,可以实现逆向遍历,但是 Iterator 不能
- ListIterator 可以使用 nextIdnex() 和 previousIndex() 方法定位到当前索引位置,而 Iterator 不能
- 它们都可以实现 remove() 删除操作,但是 ListIterator 可以使用 set() 方法实现对象修改,而 Iterator 不能
怎么确保一个集合不能被修改
- 可以采用
java.util.Collections
工具类Collections.unmodifiableMap(map)
Collections.unmodifiableList(list)
Collections.unmodifiableSet(set)
- 如诺修改则会报错
java.lang.UnsupportedOperationException
多线程部分面试题
并发和并行有什么区别
- 并发:不同的代码块交替执行
- 并行:不同的代码块同时执行
- 个人理解
- 并发就是放下手头的任务A去执行另外一个任务B,执行完任务B后,再回来执行任务A,就比如说吃饭时来电话了,去接电话,打完电话后又回来吃饭
- 并行就是执行A的同时,接受到任务B,然后我一起执行,就比如说吃饭时来电话了,一边吃饭一边打电话
线程和进程的区别
- 根本区别 :进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
- 在操作系统中能同时运行多个进程,进程中会执行多个线程
- 线程是操作系统能够进行运算调度的最小单位
守护线程是什么
- JVM内部的实现是如果运行的程序只剩下守护线程的话,程序将终止运行,直接结束。所以守护线程是作为辅助线程存在的
创建线程有哪几种方式
- 继承Thread类创建线程类
- 定义
Thread
类的子类,并重写该类的run()
方法- 创建
Thread
子类的实例,即创建了线程对象- 调用线程对象的
start()
方法来启动该线程
- 实现Runnable接口创建线程类
- 创建
runnable
接口的实现类,并重写该接口的run()
方法- 创建
Runnable
实现类的实例,并依此实例作为Thread的target
来创建Thread
对象,该Thread
对象才是真正的线程对象- 调用线程对象的
start()
方法来启动该线程
- 通过 Callable 和 Future 创建线程
- 创建
Callable
接口的实现类,并重写call()
方法,该call()
方法将作为线程执行体,并且有返回值- 创建
Callable
实现类的实例,使用FutureTask
类来包装Callable
对象,该FutureTask
对象封装了该Callable
对象的call()
方法的返回值- 使用
FutureTask
对象作为Thread
对象的target
创建并启动新线程- 调用
FutureTask
对象的get()
方法来获得子线程执行结束后的返回值
runnable 和 callable 有什么区别
- 相同点
- 都是接口
- 都可以编写多线程程序
- 都是采用
Thread.start()
启动线程
- 不同点
Runnable
没有返回值,Callable
可以返回执行结果,是个泛型和Future
、FutureTask
配合可以用来获取异步执行的结果Callable
接口的call()
方法允许抛出异常,Runnable
的run()
方法异常只能在内部消化,不能往上继续抛注意
Callalble
接口支持返回执行结果,需要调用FutureTask.get()
得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞
线程有哪些状态
- 新建 就绪 运行 阻塞 死亡
sleep() 和 wait() 的区别
- 最大区别是sleep() 休眠时不释放同步对象锁,其他线程无法访问 而wait()休眠时,释放同步对象锁其他线程可以访问
- sleep() 执行完后会自动恢复运行不需要其他线程唤起.而 wait() 执行后会放弃对象的使用权,其他线程可以访问到,需要其他线程调用 Monitor.Pulse() 或者 Monitor.PulseAll() 来唤醒或者通知等待队列
线程的 run() 和 start() 的区别
- start() 是来启动线程的
- run() 只是 Thread 类中一个普通方法,还是在主线程中执行
notify() 和 notifyAll() 有什么区别
notify()
方法随机唤醒对象的等待池中的一个线程,进入锁池notifyAll()
唤醒对象的等待池中的所有线程,进入锁池。
创建线程池有哪几种方式
- 利用
Executors
创建线程池
newCachedThreadPool()
,它是用来处理大量短时间工作任务的线程池newFixedThreadPool(int nThreads)
,重用指定数目nThreads
的线程newSingleThreadExecutor()
,它的特点在于工作线程数目限制为1newSingleThreadScheduledExecutor()
和newScheduledThreadPool(int corePoolSize)
,创建的是个ScheduledExecutorService
,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。newWorkStealingPool(int parallelism)
,这是一个经常被人忽略的线程池,Java 8
才加入这个创建方法,其内部会构建ForkJoinPool
,利用Work-Stealing
算法,并行地处理任务,不保证处理顺序
线程池都有哪些状态
- 运行 RUNNING:线程池一旦被创建,就处于
RUNNING
状态,任务数为 0,能够接收新任务,对已排队的任务进行处理- 关闭 SHUTDOWN:不接收新任务,但能处理已排队的任务。调用线程池的
shutdown()
方法,线程池由RUNNING
转变为SHUTDOWN
状态- 停止 STOP:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的
shutdownNow()
方法,线程池由(RUNNING
或SHUTDOWN
) 转变为STOP
状态- 整理 TIDYING:
SHUTDOWN
状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行terminated()
方法。线程池中的terminated()
方法是空实现,可以重写该方法进行相应的处理- 线程池在
SHUTDOWN
状态,任务队列为空且执行中任务为空,线程池就会由 SHUTDOWN 转变为TIDYING
状态- 线程池在
STOP
状态,线程池中执行中任务为空时,就会由STOP
转变为TIDYING
状态
- 终止 TERMINATED:线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为
TERMINATED
状态
线程池中的 submit() 和 execute() 有什么区别
- 两个方法都可以向线程池提交任务
execute()
方法的返回类型是void
,它定义在Executor
接口中- 而
submit()
方法可以返回持有计算结果的Future
对象,它定义在ExecutorService
接口中
在Java程序中怎么确保多线程运行安全
- 使用synchronied关键字
- 使用volatile 关键字,防止指令重排,所有对该变量读写都是直接操作共享内存
- lock锁机制
lock()
与unlock()
- 使用线程安全的类 比如
StringBuffer
、HashTable
、Vector
等线程安全问题主要是:原子性,可见性,有序性
synchronized 和 volatile 的作用什么?有什么区别
- 作用
- synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程
- volatile 表示变量在
CPU
的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性,禁止指令重排序
- 区别
synchronized
可以作用于变量、方法、对象。volatile
只能作用于变量synchronized
可以保证线程间的有序性、原子性和可见性。volatile
只保证了可见性和有序性,无法保证原子性synchronized
线程阻塞,volatile
线程不阻塞
synchronized 和 Lock 有什么区别
synchronized
是一个Java
关键字,在jvm
层面上,而Lock
是一个类synchronized
以获取锁的线程执行完同步代码,释放锁,如果线程中发生异常,jvm
会让线程释放锁。而Lock
必须在finally
中释放锁,否则容易造成线程死锁Lock
可以查看锁的状态,而synchronized
不能- 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时
Lock
的性能要远远优于synchronized
。所以说,在具体使用时要根据适当情况选择synchronized
是非公平锁,而Lock
是可公平锁
Spring Mvc面试题部分
为什么要是使用 Spring
- 轻量
非侵入型框架
- 控制反转
IOC
,促进了松耦合- 面向切面编程
AOP
- 容器 采用Java Bean 代替 沉重的
EJB
容器- 方便集成 可以和很多框架相互集成如
Mybatis
、Shiro
- 丰富的功能 Spring 已经写好了很多常的模板如
JDBC抽象类
、事务管理
、JMS
、JMX
、Web Service
...等
解释一下什么是 aop
AOP
是面向切面编程,是OOP
编程的有效补充AOP
包括Aspect
切面,Join Point
连接点,Advice
通知,Ponitcut
切点其中
Advice
通知包括5中模式
Before advice
:方法执行前‘增强’After returning advice
:方法执行后‘增强’After throwing advice
:方法执行中抛出异常时‘增强’After finally advice
:方法执行完后‘增强’Around advice
:环绕增强‘以上四种的整合’
解释一下什么是 ioc
- IOC是控制反转,是将代码本身的控制权移到外部Spring容器中进行集中管理,也是为了达到松耦合
Spring 主要有哪些模块
- Spring Core
- 框架基础部分,提供了
IOC
容器,对Bean
进行集中管理
- Spring Context
- 基于
Bean
,提供上下文信息
- Spring Dao
- 提供了
JDBC
的抽象层,它可消除冗长的JDBC
编码,提供了声明性事务管理方法
- Spring ORM
- 提供了
对象/关系
映射常用的API
集成层,如Mybatis
、Hibernate
等
- Spring Aop
- 提供了
AOP
面向切面的编程实现
- Spring Web
- 提供了
Web
开发的信息上下文,可与其他的Web
集成开发
- Spring Web Mvc
- 提供了
Web
应用的Model - View - Controller
全功能的实现
Spring 常用的注入方式有哪些
- 构造方法注入
- Setter方法注入
不过值得一提的是:Setter注入时如果写了带参构造方法,那么无参构造方法必须也要写上,否则注入失败
- 基于注解得注入
基本上有五种常用注册
Bean
的注解
@Component
:通用注解@Controller
:注册控制层Bean@Service
:注册服务层Bean@Repository
:注册Dao层Bean@Configuration
:注册配置类
Spring 中的 bean 是线程安全的吗
- 线程不安全
- Spring容器中的Bean是否线程安全,容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性
Spring 支持几种 bean 的作用域
singleton
:单例,默认的作用域prototype
:原型,每次都会创建新对象request
:请求,每次Http请求都会创建一个新对象session
:会话,同一个会话创建一个实例,不同会话使用不同实例global-session
:全局会话,所有会话共享一个实例
- 后面三种只有在Web应用中使用Spring才有效
Spring 自动装配 bean 有哪些方式
default
:默认方式和‘no’效果一样no
:不自动配置,需要使用 节点或参数byName
:根据名称进行装配byType
:根据类型进行装配constructor
:根据构造函数进行装配
Spring 事务实现方式有哪些
- 编程式事务管理对基于
POJO
的应用来说是唯一选择,我们需要在代码中调用beginTransaction()、commit()、rollback()等事务管理相关的方法,这就是编程式事务管理- 基于 TransactionProxyFactoryBean的声明式事务管理
- 基于 @Transactional 的声明式事务管理
- 基于Aspectj AOP配置事务
Spring 的事务隔离是什么
Spring Mvc 的运行流程
- 用户发送一个请求至前端控制器
DispatcherServlet
DispatcherServlet
收到请求调用处理器映射器HandlerMapping
- 处理器映射器根据请求
url
找到具体的处理器,生成处理器执行链HandlerExecutionChain
(包括处理器对象和处理器拦截器)一并返回给DispatcherServlet
DispatcherServlet
根据处理器Handler获取处理器适配器HandlerAdapter
执行HandlerAdapter
处理一系列的操作- 执行处理器
Handler
(Controller
,也叫页面控制器)Handler
执行完成返回ModelAndView
到HandlerAdapter
HandlerAdapter
将Handler
执行结果ModelAndView
返回到DispatcherServlet
DispatcherServlet``将ModelAndView
传给ViewReslover
视图解析器ViewReslover
解析后返回具体View
DispatcherServlet
对View
进行渲染视图(即将模型数据model
填充至视图中)DispatcherServlet
响应用户
Spring Mvc 有哪些组件
DispatcherServlet
:前端控制器HandlerMapping
:处理器映射器HandlerAdapter
:处理器适配器HandlerInterceptor
:拦截器ViewResolver
:视图解析器MultipartResolver
:文件上传处理器HandlerExceptionResolver
:异常处理器DataBinder
:数据转换HttpMessageConverter
:消息转换器FlashMapManager
:页面跳转参数管理器HandlerExecutionChain
:处理程序执行链RequestToViewNameTranslator
:请求转视图翻译器ThemeResolver
:LocaleResolver
:语言环境处理器
@RequestMapping 的作用是什么
@RequestMapping
是一个注解,用来标识http
请求地址与Controller
类的方法之间的映射- 指定 http 请求的类型使用
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
@Autowired 与 @Resource 的区别
@Autowired
为Spring
提供的注解,需要导入包org.springframework.beans.factory.annotation.Autowired
,采取的策略为按照类型注入@Resource
注解由J2EE提供,需要导入包javax.annotation.Resource
,默认按照ByName
自动注入
Mybatis部分面试题
Mybatis 中 #{} 和 ${} 的区别
- #{} 表示一个占位符
可以有效的防止SQL注入
- ${} 表示拼接SQL串
可以用于动态判断字段
order by ${field} desc
可以动态修改fieId来达到动态根据字段降序查询
Mybatis 有几种分页方式
- 原始分割
取出数据后,进行手动分割
- LIMIT关键字
修改执行SQL语句
- RowBounds实现分页
将
PageInfo
信息封装成RowBounds
,调用DAO层方法
- Mybatis的Interceptor实现
原理就是执行SQL语句前,在原SQL语句后加上limit关键字,不用自己去手动添加
RowBounds 是一次性查询全部结果吗?为什么
RowBounds
表面是在“所有”数据中检索数据,其实并非是一次性查询出所有数据,因为 MyBatis 是对 jdbc 的封装,在 jdbc 驱动中有一个 Fetch Size 的配置,它规定了每次最多从数据库查询多少条数据,假如你要查询更多数据,它会在你执行 next()的时候,去查询更多的数据。就好比你去自动取款机取 10000 元,但取款机每次最多能取 2500 元,所以你要取 4 次才能把钱取完。只是对于 jdbc 来说,当你调用 next()的时候会自动帮你完成查询工作。这样做的好处可以有效的防止内存溢出。
Mybatis 逻辑分页和物理分页的区别是什么
- 逻辑分页是一次性查询很多数据,然后再在结果中检索分页的数据。这样做弊端是需要消耗大量的内存、有内存溢出的风险、对数据库压力较大。
- 物理分页是从数据库查询指定条数的数据,弥补了一次性全部查出的所有数据的种种缺点,比如需要大量的内存,对数据库查询压力较大等问题
Myabtis 是否支持延迟加载,延迟加载的原理是什么
- MyBatis 支持延迟加载,设置
lazyLoadingEnabled=true
即可- 延迟加载的原理的是调用的时候触发加载,而不是在初始化的时候就加载信息。比如调用
a.getB().getName()
,这个时候发现a.getB()
的值为null
,此时会单独触发事先保存好的关联 B 对象的 SQL,先查询出来 B,然后再调用a.setB(b)
,而这时候再调用a.getB(). getName()
就有值了,这就是延迟加载的基本原理
Mybatis 一级缓存和二级缓存
- 一级缓存
- 基于
PerpetualCache
的HashMap
本地缓存,它的声明周期是和SQLSession
一致的,有多个SQLSession
或者分布式的环境中数据库操作,可能会出现脏数据- 当
Session flush
或close
之后,该Sessio
中的所有Cache
就将清空,默认一级缓存是开启的
- 二级缓存
- 也是基于
PerpetualCache
的HashMap
本地缓存,不同在于其存储作用域为Mapper
级别的,如果多个SQLSession
之间需要共享缓存,则需要使用到二级缓存- 二级缓存可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现
Serializable
序列化接口(可用来保存对象的状态)
- 扩展
- 开启二级缓存后的查询流程:
二级缓存 -> 一级缓存 -> 数据库
- 缓存更新机制:当某一个作用域(一级缓存 Session/二级缓存 Mapper)进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear
Mybatis 和 Hibernate 有哪些区别
- Mybatis 更加灵活,可以自己写SQL语句
- 也正是自己写了很多SQL语句,所以移植性比较差
- Mybatis 入门比较简单,使用门槛也低
- hibernate 可以自行把二级缓存更换为第三方的
Mybatis 有哪些执行器
SimpleExecutor
:每执行一次UPDATE\SELECT
就开启一个Statement
对象,用完后立即关闭ReuseExecutor
:执行 UPDATE\SELECT,以 SQL 作为 key 查找
Statement对象,存在就使用,不存在就创建,用完后不关闭
Statement对象,而是放置于 Map 内供下一次使用。简言之,就是重复使用
Statement`对象BatchExecutor
:执行UPDATE
(没有 select,jdbc 批处理不支持 select),将所有 SQL 都添加到批处理中addBatch()
,等待统一执行executeBatch()
,它缓存了多个Statement
对象,每个Statement
对象都是addBatch()
完毕后,等待逐一执行executeBatch()
批处理,与 jdbc 批处理相同
Mybatis 分页插件的实现原理是什么
- 分页插件的基本原理是使用 MyBatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 SQL,然后重写 SQL,根据 dialect 方言,添加对应的物理分页语句和物理分页参数
Mybatis 如何编写一个自定义插件
MyBatis 自定义插件针对 MyBatis 四大对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)进行拦截
Executor:拦截内部执行器,它负责调用 StatementHandler 操作数据库,并把结果集通过 ResultSetHandler 进行自动映射,另外它还处理了二级缓存的操作
. StatementHandler:拦截 SQL 语法构建的处理,它是 MyBatis 直接和数据库执行 SQL 脚本的对象,另外它也实现了 MyBatis 的一级缓存- ParameterHandler:拦截参数的处理
- ResultSetHandler:拦截结果集的处理
MyBatis 插件要实现 Interceptor 接口
- setProperties 方法是在 MyBatis 进行配置插件的时候可以配置自定义相关属性,即:接口实现对象的参数配置
- plugin 方法是插件用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理,可以决定是否要进行拦截进而决定要返回一个什么样的目标对象,官方提供了示例:return Plugin. wrap(target, this)
- intercept 方法就是要进行拦截的时候要执行的方法
Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
官方插件实现:
public class TestInterceptor implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation. getTarget(); //被代理对象
Method method = invocation. getMethod(); //代理方法
Object[] args = invocation. getArgs(); //方法参数
// do something . . . . . . 方法拦截前执行代码块
Object result = invocation. proceed();
// do something . . . . . . . 方法拦截后执行代码块
return result;
}
public Object plugin(Object target) {
return Plugin. wrap(target, this);
}
}
具体案例请看 Mybatis 实现自定义插件 通俗易懂
MySQL部分面试题
数据库的三范式是什么
- 第一范式:又称1NF,它指的bai是在一个应du用中的数据都可以组织zhi成由行和列的表格形式,且表格的任意一dao个行列交叉点即单元格,都不可再划分为行和列的形式,实际上任意一张表格都满足1NF
- 第二范式:又称2NF,它指的是在满足1NF的基础上,一张数据表中的任何非主键字段都全部依赖于主键字段,没有任何非主键字段只依赖于主键字段的一部分。即,可以由主键字段来唯一的确定一条记录。比如学号+课程号的联合主键,可以唯一的确定某个成绩是哪个学员的哪门课的成绩,缺少学号或者缺少课程号,都不能确定成绩的意义
- 第三范式:又称3NF,它是指在满足2NF的基础上,数据表的任何非主键字段之间都不产生函数依赖,即非主键字段之间没有依赖关系,全部只依赖于主键字段。例如将学员姓名和所属班级名称放在同一张表中是不科学的,因为学员依赖于班级,可将学员信息和班级信息单独存放,以满足3NF
如何获取当前数据库的版本
- 进入MySQL输入
select version();
- 进入cmd输入
mysql -V
说一下 ACID 是什么
- ACID,是指在可靠数据库管理系统(DBMS)中,事务(transaction)所应该具有的四个特性:
原子性(Atomicity)
、一致性(Consistency)
、隔离性(Isolation)
、持久性(Durability)
- 原子性:意味着数据库中的事务执行是作为原子。即不可再分,整个语句要么执行,要么不执行
- 一致性:即在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏
- 隔离性:事务的执行是互不干扰的,一个事务不可能看到其他事务运行时,中间某一时刻的数据
- 持久性,意味着在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚
char 和 varchar 的区别
- 长度不同
- char 长度设置后不可变
- varchar 长度可动态改变
- 效率不同
- char 类型每次修改的数据长度相同,效率更高
- varchar 类型每次修改的数据长度不同,效率更低
- 存储不同
- char 类型存储的时候是初始预计字符串再加上一个记录字符串长度的字节,占用空间较大
- varchar 类型存储的时候是实际字符串再加上一个记录字符串长度的字节,占用空间较小
float 和 double 的区别
- 在数据库中的所有计算都是使用双精度完成的,使用float(单精度)会有误差,出现意想不到的结果
- MySQL浮点型和定点型可以用类型名称后加(M,D)来表示,M表示该值的总共长度,D表示小数点后面的长度,M和D又称为精度和标度,如float(7,4)的可显示为-999.9999,MySQL保存值时进行四舍五入,如果插入999.00009,则结果为999.0001。FLOAT和DOUBLE在不指定精度时,默认会按照实际的精度来显示,而DECIMAL在不指定精度时,默认整数为10,小数为0
MySQL 内连接、左连接、右连接有什么区别
inner join ... on
内连接:只显示两表中有关联的数据left join ... on
左连接:显示左表所有数据,右表没有对应的数据用NULL补齐,多了的数据删除right join ... on
右连接:显示右表所有数据,左表没有对应的数据用NULL对齐,多了的数据删除
MySQL 的索引是怎么实现的
- 由于B+Tree数据结构的优势,目前mysql基本都采用B+Tree方式实现索引
- MySQL索引实现的数据结构:两种存储引擎都使用B+Tree(B-Tree的变种)作为索引结构
- MyISAM索引叶子节点存放的是数据的地址,主键索引与辅助索引除了值得唯一性在结构上完全一样。InnoDB索引叶子节点存放的内容因索引类型不同而不同,主键索引叶子节点存放的是数据本身,辅助索引叶子节点上存放的是主键值
MySQL 索引设计原则
- 适合索引的列是出现在where子句中的列,或者连接子句中指定的列
- 基数较小的类,索引效果较差,没有必要在此列建立索引
- 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
- 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可
怎么验证 MySQL 的索引是否满足需求
- 使用explain函数验证索引是否有效
事务的隔离级别
- Read uncommitted (读未提交):最低级别
- Read committed (读已提交):读已提交,可避免脏读情况发生。
- Repeatable Read(可重复读):确保事务可以多次从一个字段中读取相同的值,在此事务持续期间,禁止其他事务对此字段的更新,可以避免脏读和不可重复读,仍会出现幻读问题
- Serializable (串行化):最严格的事务隔离级别,要求所有事务被串行执行,不能并发执行,可避免脏读、不可重复读、幻读情况的发生
MySQL 常用的引擎
InnoDB 和 Myisam 都是用 B+Tree 来存储数据的
- InnoDB 支持事务,且支持四种隔离级别(读未提交、读已提交、可重复读、串行化),默认的为可重复读.
- Myisam 只支持表锁,且不支持事务.Myisam 由于有单独的索引文件,在读取数据方面的性能很高.
MySQL 的行锁、表锁、页锁
- 行级锁
是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。
- 行级锁的特点
开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
- 表级锁
表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)
- 表级锁的特点
开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
- 页级锁
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁
- 页级锁的特点
开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
- 扩展
- MyISAM和MEMORY采用表级锁(tabl-level locking)
- BDB采用页面锁(page-level locking)或表级锁,默认为页面锁
- InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁
乐观锁和悲观锁
- 乐观锁认为一般情况下数据不会造成冲突,所以在数据进行提交更新时才会对数据的冲突与否进行检测。如果没有冲突那就OK;如果出现冲突了,则返回错误信息并让用户决定如何去做.常见的做法有两种:版本号控制及时间戳控制
- 悲观锁,正如其名,它指的是对数据被外界(包括当前系统的其它事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排它性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)常见做法:
select ... for update
悲观锁语法锁住记录 两个事务同时修改的话,事务A先执行事务B就会被阻塞,事务A执行update完后,事务B就会看到事务A执行完后更新的结果
MySQL 问题排查都有哪些手段
- 使用 show processlist 命令查看当前所有连接信息
- 使用 explain 命令查询 SQL 语句执行计划
- 开启慢查询日志,查看慢查询的 SQL
如何做 MySQL 的性能优化
- 创建索引 尽量避免全盘扫描 首先考虑在 where 和 order by 涉及的列上创建索引
- 避免在索引上使用计算 注意就是IN关键字不走索引,它是走全盘扫描
- 使用预编译防止 sql 注入
- 尽量将多条 SQL语句压缩到一条 SQL 语句中
- 最最最好的就是少用 * , 应该写成要查询的字段名,尽量避免在 where 条件中判断 null
- 尽量不用like 的前置百分比
- 对于连续的数值,能用 between 就不要用 in
- 在新建临时表时,如果一次性插入数据量较大.可以用 select into 代替 create table
- 选择正确的存储引擎
- 垂直/水平分割、分库分表、读写分离
Redis部分面试题
Redis 是什么?有什么优点?都有哪些使用场景
- Reids 是完全开源免费的,用C语言编写的,遵守BSD协议,
是一个高性能的(key/value)分布式内存数据库,基于内存运行
并支持持久化的NoSQL数据库,是当前最热门的NoSql数据库之一,
也被人们称为数据结构服务器- 优点
- Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
- Redis支持数据的备份,即master-slave模式的数据备份
- 应用场景
- 内存存储和持久化:redis支持异步将内存中的数据写到硬盘上,同时不影响继续服务
- 取最新N个数据的操作,如:可以将最新的10条评论的ID放在Redis的List集合里面
- 模拟类似于HttpSession这种需要设定过期时间的功能
- 发布、订阅消息系统
- 定时器、计数器
Redis 为什么是单线程的
- Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈
Redis 的缓存预热
- 在项目配置文件中生命自定义的
key
,在项目启动时会判断redis是否存在key
,如果没有就会创建一个key
传入null
值- 数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中
redis 缓存雪崩是什么,怎么解决 ?
缓存雪崩是指,缓存层出现了错误,不能正常工作了.于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况.
解决方案
- redis 高可用 就是搭建 redis 集群,其中一台redis挂掉后 可以使用其他的 redis
- 限流降级 就是每一个 key 只能一个线程来查询数据和缓存,其他线程等待
数据预热 数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中.在即将发生大并发访问前手动触发加载缓存不同的 key ,设置不同的过期时间,让缓存失效的时间点尽量均匀.*
缓存穿透是什么?如何解决
就是访问redis数据库,查不到数据,就是没有命中,会去持久化数据库查询,还是没有查到.假如高并发的情况下,持久化数据库一下增加了很大压力,就相当于出现了缓存穿透
解决方案
- 缓存空对象 在存储层命中失败后,即使返回空对象也将其缓存,并设置一个过期时间,之后访问的这个数据将会从缓存中取出,很好的保护了后端数据源,这样也会有出现问题 例如空值被缓存也就会增加大量的缓存空间,设置了过期时间还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响
- 布隆过滤器 对所有可能查询的参数以 hash 形式存储,查询时发现值不存在就直接丢弃,不会去持久层查询
Redis 支持的数据类型有哪些
- String、List、Set、Hash、ZSet这5种
Redis 支持的 Java 客户端有哪些
- Redisson、Jedis、lettuce 等等,官方推荐使用 Redisson
Jedis 与 Redisson 有哪些区别
- Jedis 和 Redisson 都是Java中对Redis操作的封装。Jedis 只是简单的封装了 Redis 的API库,可以看作是Redis客户端,它的方法和Redis 的命令很类似。Redisson 不仅封装了 redis ,还封装了对更多数据结构的支持,以及锁等功能,相比于Jedis 更加大。但Jedis相比于Redisson 更原生一些,更灵活
怎么保证缓存与数据库数据的一致性
- 对删除缓存进行重试,数据的一致性要求越高,我越是重试得快。
- 定期全量更新,简单地说,就是我定期把缓存全部清掉,然后再全量加载。
- 给所有的缓存一个失效期
Redis 持久化有几种方式
- 快照方式(RDB, Redis DataBase)将某一个时刻的内存数据,以二进制的方式写入磁盘
- 文件追加方式(AOF, Append Only File),记录所有的操作命令,并以文本的形式追加到文件中
- 混合持久化方式,Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能简单数据丢失的风险
Redis 怎么实现分布式锁
SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX second
:设置键的过期时间为second秒PX millisecond
:设置键的过期时间为millisecond毫秒NX
:只在键不存在时,才对键进行设置操作XX
:只在键已经存在时,才对键进行设置操作- SET操作成功完成时,返回
OK
,否则返回nil
Redis 分布式锁有什么缺陷
- 死锁
- 设置锁的过期时间,且需要保证setNx和设置过期时间操作的原子性
- 错位解锁
- 加锁时记录当前线程ID,解锁时判断ID是否一致
- 解锁时,查询redis里记录锁的ID,以及删除redis中锁的记录,这两步操作可以使用lua脚本保持原子性
- 业务并发执行问题
- 加锁成功后开启守护线程,当临近过期时间,业务还未完成时,开始续时,重复此步骤直到业务完成
Redis 如何做内存优化
- 缩减键值对象:满足业务要求下 key 越短越好;value 值进行适当压缩
- 共享对象池:即 Redis 内部维护[0-9999]的整数对象池,开发中在满足需求的前提下,尽量使用整数对象以节省内存
- 尽可能使用散列表(hashes)
- 编码优化,控制编码类型
- 控制 key 的数量
Redis 淘汰策略有哪些
noeviction
: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。 大多数写命令都会导致占用更多的内存(有极少数会例外, 如 DEL )allkeys-lru
: 所有key通用; 优先删除最近最少使用(less recently used ,LRU) 的 keyvolatile-lru
: 只限于设置了expire
的部分; 优先删除最近最少使用(less recently used ,LRU) 的 keyallkeys-random
: 所有key通用; 随机删除一部分 keyvolatile-random
: 只限于设置了 expire 的部分; 随机删除一部分 keyvolatile-ttl
: 只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key
Redis 常见的问题有哪些? 该如何解决
- 缓存和数据库双写一致性问题
- 就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性
- 采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列
- 缓存穿透问题
- 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试
- 采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作
- 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的 Key。迅速判断出,请求所携带的 Key 是否合法有效。如果不合法,则直接返回
- 缓存雪崩问题
- 给缓存的失效时间,加上一个随机值,避免集体失效
- 使用互斥锁,但是该方案吞吐量明显下降
- 双缓存。我们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间。自己做缓存预热操作(从A中读不到,就去B读,返回数据时需要异步启动一个更新线程,更新线程同时更新缓存 A 和缓存 B)
- 缓存的并发竞争问题
- 如果对这个 Key 操作,不要求顺序
- 这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做 set 操作即可,比较简单。
- 如果对这个 Key 操作,要求顺序
- 假设有一个 key1,系统 A 需要将 key1 设置为 valueA,系统 B 需要将 key1 设置为 valueB,系统 C 需要将 key1 设置为 valueC
- 期望按照 key1 的 value 值按照 valueA > valueB > valueC 的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。
- 系统A key 1 {valueA 3:00}
- 系统B key 1 {valueB 3:05}
- 系统C key 1 {valueC 3:10}
- 那么,假设这会系统 B 先抢到锁,将 key1 设置为{valueB 3:05}。接下来系统 A 抢到锁,发现自己的 valueA 的时间戳早于缓存中的时间戳,那就不做 set 操作了,以此类推。
- 其他方法,比如利用队列,将 set 方法变成串行访问也可以。总之,灵活变通。
RabbitMQ部分面试题
RabbitMq 的使用场景有哪些
- 多个应用之间的耦合
- 跨系统的异步通信
- 流量削峰
- 比如:注册用户、发送激活邮箱、订单下单等
RabbitMq 有哪些重要的角色
- 生产者:消息的创建者,负责创建和推送数据到消息服务器
- 消费者:消息的接收方,负责处理数据和确认消息
- 代理:就是RabbiMQ本身,不生产不消费,只是快递消息
RabbitMq 有哪些重要的组件
- ConnectionFactory(连接管理器):应用程序与Rabbit之间建立连接的管理器,程序代码中使用
- Channel(信道):消息推送使用的通道
- Exchange(交换器):用于接受、分配消息
- Queue(队列):用于存储生产者的消息
- RoutingKey(路由键):用于把生成者的数据分配到交换器上
- BindingKey(绑定键):用于把交换器的消息绑定到队列上
RabbitMQ的消息存储方式
RabbitMQ 对于 queue 中的 message 的保存方式有两种方式:disc 和 ram.如果采用disc,则需要对 exchange/queue/delivery mode 都要设置成 durable 模式. Disc 方式的好处是当 RabbitMQ 失效了, message 仍然可以在重启之后恢复.而使用 ram 方式, RabbitMQ 处理 message 的效率要高很多, ram 和 disc 两种方式的效率比大概是 3:1.所以如果在有其它 HA 手段保障的情况下,选用 ram 方式是可以提高消息队列的工作效率的.
RabbitMq 中 vhost 的作用是什么
- vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的队列、绑定、交换器和权限控制
- 从 RabbitMQ 的全局角度 vhost可以作为不同权限隔离的手段(一个典型的例子,不同的应用可以跑在不同的vhost中)
RabbitMq 的消息是怎么发送的
- 生产者把生产的小心通过channel发送到Exchange上,Exchange通过绑定的router key来选择Queue,消费者监听到Queue上有新的消息,就消费调此消息
RabbitMq 怎么保证消息的稳定性
- 提供了事务的功能,通过将 channel 设置为 confirm(确认模式)
RabbitMq 怎么避免丢失消息
- 消息持久化
- 消费端的ack签收机制
- 设置集群镜像模式
- 消息补偿机制
要保证消息持久化成功的条件有哪些
- 声明队列必须设置持久化 durable 设置为 true
- 消息推送投递模式必须设置持久化,deliveryMode 设置为 2(持久)
- 消息已经到达持久化交换器
- 消息已经到达持久化队列
以上四个条件都满足才能保证消息持久化成功
RabbitMq 持久化有什么缺点
- 持久化的缺点就是降低了服务器的吞吐量,因为使用的是磁盘而非内存存储,从而降低了吞吐量。可尽量使用 ssd 硬盘来缓解吞吐量的问题
RabbitMq 有几种广播方式
fanout
广播模式:所有bind到此exchange的queue都可以接收消息direct
直接交换:通过routingKey和exchange决定的那个唯一的queue可以接收消息topic
通配符模式:所有符合routingKey(此时可以是一个表达式)的routingKey所bind的queue可以接收消息
RabbitMq 怎么实现延迟消息队列
- 通过消息过期后进入死信交换器,再由交换器转发到延迟消费队列,实现延迟功能
- 使用 RabbitMQ-delayed-message-exchange 插件实现延迟功能。
RabbitMq 集群有什么用
- 高可用:某个服务器出现问题,整个 RabbitMQ 还可以继续使用
- 高容量:集群可以承载更多的消息量。
RabbitMq 节点的类型有哪些
- 磁盘节点:消息会存储到磁盘
- 内存节点:消息都存储在内存中,重启服务器消息丢失,性能高于磁盘类型
RabbitMq 集群搭建需要注意哪些问题
- 各节点之间使用
–link
连接,此属性不能忽略- 各节点使用的 erlang cookie 值必须相同,此值相当于
秘钥
的功能,用于各节点的认证- 整个集群中必须包含一个磁盘节点
RabbitMq 每个节点是其他节点的完整拷贝吗
不是
- 如果每个节点都拥有所有队列的完全拷贝,这样新增节点,不但没有新增存储空间,反而增加了更多的冗余数据
- 如果每条消息都需要完整拷贝到每一个集群节点,那新增节点并没有提升处理消息的能力,最多是保持和单节点相同的性能甚至是更糟
RabbitMq 集群中唯一一个磁盘节点崩溃了会发生什么
- 唯一磁盘节点崩溃了,集群是可以保持运行的,但不能更改任何东西
- 不能创建队列
- 不能创建交换器
- 不能创建绑定
- 不能添加用户
- 不能更改权限
- 不能添加和删除集群节点
RabbitMq 对集群停止顺序有要求吗
- RabbitMQ 对集群的停止的顺序是有要求的,应该先关闭内存节点,最后再关闭磁盘节点。如果顺序恰好相反的话,可能会造成消息的丢失
JVM部分面试题
JVM 主要的组成部分?及其作用
类加载器(Class Loader)
:加载类文件到内存。Class loader只管加载,只要符合文件结构就加载,至于能否运行,它不负责,那是有Exectution Engine 负责的执行引擎(Execution Engine)
:也叫解释器,负责解释命令,交由操作系统执行本地库接口(Native Interface)
:本地接口的作用是融合不同的语言为java所用- 运行时数据区(Runtime Data Area)
JVM 运行时数据区是什么
堆
是java对象的存储区域,任何用new字段分配的java对象实例和数组,都被分配在堆上,java堆可用-Xms和-Xmx进行内存控制,常量池
:运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放,jdk1.7以后,运行时常量池从方法区移到了堆上方法区
:用于存储已被虚拟机加载的类信息,常量,静态变量,即是编译器编译后的代码等数据虚拟机栈
:虚拟机栈中执行每个方法的时候,都会创建一个栈桢用于存储局部变量表,操作数栈,动态链接,方法出口等信息本地方法栈
:与虚拟机发挥的作用相似,相比于虚拟机栈为Java方法服务,本地方法栈为虚拟机使用的Native方法服务,执行每个本地方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息程序计数器
:指示Java虚拟机下一条需要执行的字节码指令
堆和栈的区别
- 栈内存用来存储局部变量和方法调用
- 而堆内存用来存储 Java 中的对象.无论是成员变量,局部变量,还是类变量.,它们指向的对象都存储在堆内存中.
- 栈内存归属单个线程,一个栈对应一个线程,其中储存的变量只能在该线程中可以访问到,也可以理解为私有内存
- 而堆内存中的对象 所有线程均可见,堆内存中对象可以被所有线程访问到
- 栈内存要远小于堆内存
队列和栈是什么?有什么区别
- 队列和栈是两种不同的数据结构
- 操作名称不同
- 队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈
- 可操作的方式不同
- 队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。
- 队列先进先出 FIFO,栈是后进先出 LIFO
类加载器有哪些?什么是双亲委派模型
- 启动类加载器 Bootstrap ClassLoader:加载<JAVA_HOME>\lib目录下核心库
- 扩展类加载器 Extension ClassLoader:加载<JAVA_HOME>\lib\ext目录下扩展包
- 应用程序类加载器 Application ClassLoader: 加载用户路径(classpath)上指定的类库
双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。这里的双亲其实就指的是父类,没有mother。父类也不是我们平日所说的那种继承关系,只是调用逻辑是这样
类加载的执行过程
- 加载
- 加载指的是把class字节码文件从各个来源通过类加载器装载入内存中
- 字节码来源:一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器
- 链接
- 验证
主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误,包括对于文件格式的验证
- 准备
主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值:8种基本类型的初值,默认为0;引用类型的初值则为null;
- 解析
将常量池内的符号引用替换为直接引用的过程。在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用
- 初始化
这个阶段主要是对类变量初始化,是执行类构造器的过程
怎么判断对象是否可以收回
- 引用计数算法
- 判断对象的引用数量
- 通过判断对象的引用数量来决定对象是否可以被回收
- 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
- 任何引用计数为0的对象实例可以被当作垃圾收集
- 优缺点
- 优点:执行效率高,程序执行受影响较小
- 缺点:无法检测出循环引用的情况,导致内存泄漏
- 可达性分析算法
- 通过判断对象的引用链是否可达来决定对象是否可以被回收
- 可以作为GC Root对象的对象有
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法区中的常量引用对象
- 方法区中类静态属性引用对象
- 本地方法栈中JNI(Native方法)的引用对象
- 活跃线程中的引用对象
Java 中有哪些引用类型
- 强引用(spanreference)
就是指在程序代码之中普遍存在的,类似“Object obj=new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例
- 软引用(softreference)
是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象, 在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行 第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后,提供了
SoftReference
类来实现软引用
- 弱引用(weakreference)
也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱 引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时, 无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在 JDK 1.2 之 后,提供了
WeakReference
类来实现弱引用
- 虚引用(phantomreference)
也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用 来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象 实例被收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了 PhantomReference 类来实现虚引用
JVM 中垃圾回收算法
- 标记-清除算法
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片
- 复制除算法
复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存
- 标记-整理(压缩)算法
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题
JVM 有哪些垃圾回收器
- 新生代收集器:Serial、ParNew、Parallel Scavenge
- 老年代收集器:CMS、Serial Old、Parallel Old
- 整堆收集器: G1
介绍一下 CMS 垃圾回收器
- CMS收集器 :一种以获取最短回收停顿时间为目标的收集器
- 特点:基于标记-清除算法实现。并发收集、低停顿
- 应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务
- CMS收集器的运行过程分为下列4步:
- 初始标记:标记GC Roots能直接到的对象,速度很快但是仍存在Stop The World问题
- 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行
- 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题
- 并发清除:对标记的对象进行清除回收
- CMS收集器的内存回收过程是与用户线程一起并发执行的
- CMS收集器的缺点:
- 对CPU资源非常敏感
- 无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生
- 因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC
CMS收集器的工作过程图:
新生代垃圾回收器和老生代垃圾回收器有哪些?有什么区别
- 新生代收集器:Serial、ParNew、Parallel Scavenge
- 老年代收集器:CMS、Serial Old、Parallel Old
- 区别:
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收
简述分代垃圾回收器是怎么工作的
- 分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。 新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1
- 执行流程
- 把 Eden + From Survivor 存活的对象放入 To Survivor 区
- 清空 Eden 和 From Survivor 分区; From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor
- 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代
- 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法
- 以上这些循环往复就构成了整个分代垃圾回收的整体执行流程
JVM 调优的工具有哪些
- jconsole
jdk自带的工具
:是一个基于JMX(java management extensions)的GUI性能监测工具(jdk/bin目录下点击jconsole.exe即可启动)- VisualVM:它提供了一个可视界面,用于查看 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的基于 Java 技术的应用程序(Java 应用程序)的详细信息(jdk/bin目录下面双击jvisualvm.exe既可使用)
- MAT
第三方调优工具
:一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗(MAT以eclipse 插件的形式来安装)- GChisto:专业分析gc日志的工具,可以通过gc日志来分析:Minor GC、full gc的时间、频率等等,通过列表、报表、图表等不同的形式来反应gc的情况(配置好本地的jdk环境之后,双击GChisto.jar,在弹出的输入框中点击 add 选择gc.log日志)
- gcviewer:分析小工具,用于可视化查看由Sun / Oracle, IBM, HP 和 BEA Java 虚拟机产生的垃圾收集器的日志
JVM 调优的参数有哪些
算法题
其他部分面试题
Api 接口如何实现 ?
在类里使用 implements 关键字实现 Api 接口
MySQL 链接数据库常用的几种方式 ?
- Mybatis 框架
- Hibernate 框架
- JDBC 技术
- c3p0 连接池
- dbcp 连接池
SpringBoot 如何集成 Redis ?
在 pom.xml 文件引入 redis 依赖
dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在 application 配置文件中 书写 redis 配置
0.1
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
#spring.redis.password=
SpringCloud 的优点 ?
- 服务之间采用Restful等轻量级通讯
- 精准的制定优化服务方案,提高系统的可维护性
- 服务之间拆分细致,资源可重复利用,提高开发效率
SpringCloud 用了哪些组件 ?
- netflix Eureka 注册中心
- netflix Ribbon 负载均衡
- netflix Zuul 网关
- netflix Hystrix 熔断器
- feign 服务调用
List 和 Set 的区别
- List 允许有多个重复对象,而 Set 不允许有重复对象
- List 允许有多个NULL值,而 Set 只允许有一个NULL值
- List 是一个有序的容器,输出顺序即是输入顺序
- Set 是一个无序的容器无法保证每个元素的顺序,但是可以用 TreeSet 通过 Comparator 或者 Comparable 维护排序顺序
- List的实现类有 ArrayList、LinkList、Vector 其中 ArrayList 最常用于查询较多,随意访问.LinkList 同于新增和删除较多的情况,Vector 表示底层数组,线程安全象
- Set的实现类有 HashSet、TreeSet、LinkedHashSet 其中基于 HashMap 实现的 HashSet 最为流行,TreeSet 是一个有序的Set容器象
扩展
Map的实现类有HashMap、HashTable、TreeMap
Java 中 static 的作用
- 表示全局或静态的意思、用来修饰成员变量和成员方法,也可以形成静态代码块
- 达到了不用实例化就可以使用被 public static 修饰的变量或方法
什么单例模式 ?
保证整个项目中一个类只有一个对象的实例,实现这种功能就叫做单例模式
- 单例模式的好处:
- 单例模式节省公共资源
- 单例模式方便控制
- 如何保证是单利模式 ?
- 构造私有化
- 以静态方法返回实例
- 确保对象实例只有一个
- 单例模式有哪几个 ?
- 饿汉模式
把对象创建好,需要使用的时候直接拿到就行
- 懒汉模式
等你需要的时候在创建对象,后边就不会再次创建
SpringBoot 常用的几个注解 ?
- @SpringBootApplication SpringBoot的核心注解 启动类
- @EnableAutoConfiguration 开启SpringBoot自动配置
- @RestController 在控制层 是@ResponseBody注解与@Controller注解的合集
- @RequestMapper 处理请求地址映射的注解
- @RequestParam 获取url上传过来的参数
- @Configuration 声明配置类
- @Component 通用注解
- @Service 业务逻辑层
Java 八大数据类型
char 字符型 byte 字节型 boolean 布尔型
float 单浮点型 double 双浮点型
int 整数型 short 短整数型 long 长整数型
MySQL分页和升序降序如何实现 ?
- 可以用 limit
select
name
,age
,sex
from t_student limit(0,5);
- 升序 order by xx asc
select
name
,age
,sex
from t_student order byage
asc;
- 降序 order by xx desc
select
name
,age
,sex
from t_student order byage
desc;
maven 是干什么的,它有什么好处 ?
- maven 专门构建和管理Java项目的工具
- maven的好处在于可以将项目过程规范化、自动化、高效化以及强大的可扩展性
MySQL 如何添加索引 ?
- PRIMARY KEY (主键索引)
- 添加INDEX(普通索引) ALTER TABLE
table_name
ADD INDEX index_name (column
)- 添加UNIQUE(唯一索引) ALTER TABLE
table_name
ADD UNIQUE index_name (column
)- 添加FULLTEXT(全文索引) ALTER TABLE
table_name
ADD FULLTEXT (column
)- 添加多列索引 ALTER TABLE
table_name
ADD INDEX index_name (column1
,column2
,column3
)
MySQL 索引的实现方式?
MySQL 索引底层的实现方式是 B+Tree也就是B+树 具体查看 B+Tree实现方式
Vue的数据双向绑定原理
使用v-mode属性, 它的原理是利用了Object.defineProperty()方法重新定义了对象获取属性值(get)和设置属性值(set)的操作来实现的
ActiveMQ的消息存储方式
- 采取先进先出模式,同一时间,消息只会发送给某一个消费者,只有当该消息被消费并告知已收到时,它才能在代理的存储中被删除.
- 对于持久性订阅来说,每一个消费者都会获取消息的拷贝.为了节约空间,代理的存储介质中只存储了一份消息,存储介质的持久订阅对象为其以后的被存储的消息维护了一个指针,消费者消费时,从存储介质中复制一个消息.消息被所有订阅者获取后才能删除.
KahaDB消息存储
基于文件的消息存储机制,为了提高消息存储的可靠性和可恢复性,它整合了一个事务日志.KahaDB拥有高性能和可扩展性等特点.由于KahaDB使用的是基于文件的存储,所以不需要使用第三方数据库
转 https://www.cnblogs.com/beixuan/p/13381722.html#4725448
以上是 Java常见面试题 非常实用【个人经验】 的全部内容, 来源链接: utcz.com/z/391537.html