必收藏的Java面试题
目录
Java 面试题
一. 容器部分
二. 多线程部分
三. SpringMvc部分
四. Mybatis部分
五. MySQL部分
六. Redis部分
七. RabbitMQ部分
八. 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 方法就是要进行拦截的时候要执行的方法
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
官方插件实现:
@Intercepts({@Signature(type = Executor. class, method = "query",
args = {MappedStatement. class, Object. class, RowBounds. class, ResultHandler. class})})
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数据库,查不到数据,就是没有命中,会去持久化数据库查询,还是没有查到.假如高并发的情况下,持久化数据库一下增加了很大压力,就相当于出
以上是关于Java常见面试题 非常实用个人经验的主要内容,如果未能解决你的问题,请参考以下文章