java 面试题
数据库相关
1.sql 语句执行的流程
1.第一步首先建立链接,进行账号密码和权限验证,还有数据库表的访问权限。
2.服务端查看是否命中缓存,命中直接返回,然后直接往下,mysql8.0 已经删除了缓存查询。但是需要特别注意的是,无论大小写、空格还是注释,都会影响缓存的命中结果,也就是说必须完全一样!
3.接着来到解析器,进行语法分析,关键字的校验,如果都符合规范,则进行下一步优化器。
4.然后 sql 优化器进行优化,mysql 基于自己的公式会计算出成本最低的执行方式。
5.执行引擎的作用在于查看是否有执行权限,然后在打开表,根据表的存储引擎去调用接口。
6.这里存储引擎回去缓存池查询,执行器只需调用底层存储引擎提供的调用接口,获取到数据后返回给客户端程序就好了。
一条更新 sql 的区别
存储引擎判断当前缓冲池中是否存在需要更新的数据,存在就直接返回,否则去从磁盘加载数据。
执行引擎调用存储引擎 API 去更新数据。
存储引擎更新数据,同时写入 undo_log、redo_log 信息。
执行引擎写 binlog,提交事务,流程结束。
redo_log:
undo_log:
binlog:
2.索引的原理,索引的种类以及原理
索引的原理:就是用更短的时间,更少占用空间定位到完成查询和检索。
索引种类:b 树,b+树,hash 树,红黑树
hash 的缺点:大量重复数据产生 hash 和冲突,不支持范围查询和顺序查询,
3.btree 和 b+ree 的区别
本质上数据库的数据存放在磁盘上,而磁盘主要是通过一个扇区保存的,扇区保存的数据有限,而且每次移动扇区都需要时间和消耗,这就要求我们用一种高效的数据结构来用尽可能少的 io 次数来访问尽可能多的数据。这其实也是索引的本质,就是能更快的定位我们所谓查询的数据,而不是通过全表扫描。
二叉树:比较依赖它的平衡程度,这里会出现一种情况,如果是按顺序插入就会生成一颗 n 具有高度的树,这样并不来减少查询时间。
平衡二叉树:平衡二叉树相比二叉树的好处在于它的特点是保证任何节点的左右子树高度之差不超过 1,它主要是采用了旋转操作来保持平衡,因此有较大的计算开销降低查询性能,而且每个树节点仅存储一个数据,如果需要查询的分布在多个节点上,就需要进行多个磁盘 io。
b 树和 b+树:b 树和 b+树相比平衡二叉树,主要节点中存放的 key 不止一个,而且还存放关键字,关键字记录的指针和指向子节点指针,b+树只在叶子结点存放 key 和 data,而且每个叶子结点都有引用链指向相邻的叶子结点,b+树的范围查询只需要遍历叶子结点就行,所以 b+树和 b 树相比,具备更少的 io 次数,更稳定的查询效率和更适于范围范围查询。
平衡 btree 的每次插入删除更新都会重新破坏树的结构,重新进行调整,所以也是为什么不建议在频繁更新的列上使用索引的原因。而且
而 b+tree 的区别在于,只有叶子节点才存放所有数据,而在子节点上存放的是副本,在 B+Tree 的每个叶子节点增加一个指向相邻叶子节点的指针,这样就形成了链表,就形成了带有顺序访问指针的 B+Tree。当进行查询时或者访问查询,我们只需要查询到叶子结点上进行查询就可以了。空间利用率更好,子节点
btree 的查询效率基本都是一致,因为树的层数都是 2-4 层,查找一条数据最多需要 1-3 次操作。
https://blog.csdn.net/zhizhengguan/article/details/109193886
hash tree 和 hash 表的实现基本一致,键值 key 通过 hash 找到桶 bucket,这里 bucket 指的是存储一条或者多条的存储单元,一个桶的结构包含了一个内存指针数组,桶中的每行数据都会指向下一行,形成链表结构。
hash 查询基本一次检索就能找到数据,而 b+树需要自顶向下查找,经过多次 io。
在查询速度上,如果是等值查询那么 hash 索引明显有绝对优势,只需要经过一次 hash 算法即可找到相应的键值,如果键值不唯一,需要先找到该键的所载位置,然后在根据俩表往后扫描。
hash 索引只支持等值比较查询,无法索成范围查询检索,b+tree 索引的叶子结点形成有序链表,便于范围查询。
hash 索引无法做 like 这样的模糊查询,因为需要对完成的 key 做 hash 计算,定位 bucket,而 b+tree 索引具有最左前缀匹配,可以进行部分模糊查询。
hash 索引存放的是经过 hash 计算之后的 hash 值,而且 hash 值的大小关系并不一定和 hash 运算前的完全一样,所以数据库无法利用索引的数据来避免任何排序运算。b+tree 可以形成有序链表用于排序。
因为存在哈希碰撞问题,在有大量重复键值情况下,哈希索引的效率低,b+树所有查询都要找到叶子节点,性能稳定。大多数场景下,都会有组合查询,范围查询,排序,分组,模糊查询等查询特征,hash 索引无法满足要求,建议数据库使用 b+树。
3.聚簇索引和非聚簇索引
聚簇索引:索引结构和数据一起存放的索引。innodb 的主键索引就是聚簇索引。
非聚簇索引:索引结构和数据分开存放的索引。innodb 的二级 i 索引就是非聚簇索引,叶子结点存放的为主键,根据主键在回表查询。
优缺点:聚簇索引的优点为如果数据更新频繁,那么索引也要被更新。而这也是非聚簇索引的优点。
4.事务隔离级别,原理,出现的问题及解决办法。
事务有 4 个特点:原子性、一致性、隔离性、持久性。简称为 acid 特性。持久性通过 redo log 来保证的,原子性通过 undo log+buff pool 来保证的,隔离性通过 mvcc 来或者锁机制来保证的,一致性通过持久性+隔离性+原子性来保证的。
读取未提交:最低的隔离级别,可能读取别人未提交的内容,造成脏读。
读取已提交:能解决脏读,但是可能造成多次读取不一致的情况。通过 readview 解决脏读的问题,每一次读取之前生成一个 readview。
可重复读:通过 mvcc 能解决不可重复读的问题。启动事务前生成一个 readview,然后整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。
可串行化:能解决脏读、幻读、不可重复读。
undo log:mysql 回滚日志,所有事务进行的修改都会记录在这个日志上。
大致原理:
首先我们应明确几个概念
表格的隐藏列:每个表都会有 3 个隐藏的字段,记录操作该数据的事务 id,指向上一个版本的 undo log 的指针,主要用来做回滚用。
undo log:在每次更新数据之前会把数据拷贝到 log 里,做备份,在事务进行回滚的时候利用记录进行回滚,还有个作用用 mvcc 的快照读,可以实现不同的事务版本号都拥有自己的一份独立的快照数据版本。
readview:mids(当前活动的事务 id 结合),maxid(下一个事务 id),minids(当前活动的最小事务 id),creatorid(当前事务 id)。
步骤:
1.当我们执行一个修改操作时,会生成一个事务 id,然后将当前数据备份到 undolog 中,在将数据中指针指向,以及事务 id 修改成生成的事务 id
2.开启事务后,会生成 readview,主要是通过这个实现了可重复读和重复提交的功能,通过比较 mids,来判断是否有活动事务正在操作,如果是,则从 undo log 中重新读取。
关于 mvcc 是否能解决幻读的问题:mvcc 是基于快照读,那么就没有幻读的问题,在当前读的情况下则需要使用间隙锁来解决幻读问题的。
参考:
数据库 MVCC 如何解决可重复读问题? - 小林 coding 的回答 - 知乎https://www.zhihu.com/question/333421386/answer/2630886782
https://zhuanlan.zhihu.com/p/426950475
https://zhuanlan.zhihu.com/p/553286085
https://blog.csdn.net/qq_34318539/article/details/116157560
mysql 事务和锁
https://baijiahao.baidu.com/s?id=1709900087493236868&wfr=spider&for=pc
inndb 的行级锁
https://www.cnblogs.com/huangfuyuan/p/9510022.html
innodb mvcc 的实现原理
https://zhuanlan.zhihu.com/p/52977862
4.索引的失效的场景及原理
where 语句中的 or 前条件是索引列,or 后不是导致索引失效,如果使用了
or
关键字,那么它前面和后面的字段都要加索引,不然所有的索引都会失效,这是一个大坑。like %在左边会全表扫描,导致索引失效
隐式类型转换导致索引失效
不遵循最做匹配原则
使用了函数
索引列有计算
5.最左前缀匹配原则
MySQL 一定是遵循最左前缀匹配的,这句话在以前是正确的,但是在 MySQL 8.0 出现了索引跳跃扫描。
但是跳跃扫描机制也有很多限制,比如多表联查时无法触发、分组操作时无法触发、使用了 DISTINCT 去重无法触发等。
且只有在唯一性较差的情况下,才能发挥出不错的效果;否则相当于走一次全表扫描。不过最后还是由优化器决定是否使用该策略。
6.mysql 的存储引擎有哪些,有什么区别?
基于几点回答,首先从《高性能 mysql》中可以看出,支持事务,采用 mvcc 支持高并发,基于聚簇索引,崩溃后的安全恢复,myisam 采用非聚簇索引。
myisam 在备份和恢复的表现较好,由于其数据采用了文件存储的形式,跨平台使用也更加方便,这点相比 innodb 大数据备份更加友好。而崩溃恢复主要 innodb 基于 redo log 和 undo log,这个是引擎层特有的,而 binlog 是服务层特有的。
7.索引创建规则
数据量较小的不需要创建索引,经常频繁更新的列不要建立索引,不经常使用的列不需要建立索引
并发编程相关
1.可能带来的问题:内存泄漏、上下文切换、线程安全、死锁
2.并发编程的三个要素:原子性、可见性、有序性
3.创建线程的方式?
本质上两种,thread.start(),线程池创建
继承 thread
实现 runnable
实现 callable+futuretask
可以拿到结果,阻塞式,使用 futuretask 的 get 方法获取结果,运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future 对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
创建线程池
通过线程池初始化,性能稳定,也可以获取执行结果,并捕获异常。但是在业务复杂情况下,一个异步调用可能会依赖于另一个异步调用的执行结果
corepoolsize:核心线程数
maximumpoolsize:最大线程数
keepalivetime:存活时间
unit:时间单位
workqueue:阻塞队列
threadfactory:
handler:阻绝策略
1.线程池刚创建时,里面没有一个线程,任务队列是作为参数传进来的,不过,就算队列里面有任务,线程池也不会马上执行它们。
2.当调用 execute()方法添加一个任务时,线程池会做如下判断:
2.1.如果正则运行的线程数小于 corePoolSize,那么马上创建线程运行这个任务。
2.2.如果正在运行的线程数量大于等于 corePoolSize,那么将这个任务放入队列。
2.3.如果这时候队列满了,而且正在运行的线程数量小于 maxmumPoolSize,那么还是要创建非核心线程立即运行这个任务。
2.4.如果队列满了,而且正在运行的线程数量大于或等于 maxmumPoolSize,那么线程池会执行拒绝策略,抛出 RejectExecutionException 异常。
3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
4.当一个线程无事可做,超过一定的时间(keepAliveTime)那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
4.设计一个线程池
初始化线程池,指定线程池的大小
向线程池中放入任务执行
如果线程池中创建的线程数目未到指定大小,则创建我们自定义的线程类放入线程池集合,并执行任务。执行完了后该线程会一直监听队列
如果线程池中创建的线程数目已满,则将任务放入缓冲任务队列
线程池中所有创建的线程,都会一直从缓存任务队列中取任务,取到任务马上执行
5.悲观锁和乐观锁
悲观锁就是当共享资源被访问时,每次只给一个线程使用,并给当前持有者上锁,当前这个线程执行完成之后,才会把锁释放。实现方式例如 synchronized 和ReentrantLock
。适用场景是写少读多。
乐观锁是只有在访问的的时候采取验证,实现方式于 CAS 和版本号。实用场景是读多写少。
CAS
CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。CAS 涉及到 3 个操作树,写入的新值,预期值,要更新的变量,如果预期值和要更新的变量一样相等就更新,如果不一样就不更新。
缺点:循环时间大,开销大,失败就进行 do while,aba 问题。
会产生的问题:aba 问题,就是当读取并赋值的时候其他线程也进行了同样的操作了同样的值,这样只能通过 AtomicStampReference 时间戳原子引用解决。
版本号比较
每次操作前获取版本号,操作后版本号+1,在进行比对,如果不一致,无法提交,操作被驳回。
https://zhuanlan.zhihu.com/p/571637324
参考:https://zhuanlan.zhihu.com/p/196789486
springboot 相关
1.自动装配原理
SpringBootapplication 其中主要有两个主要的注解 SpringBootConfiguration 和 EnableAutoConfiguration
2.spring 和 springboot 区别
springboot 减少了大量 xml 的配置,主要配置写于 properties 文件中,还支持 yaml 文件中,propertis 加载顺序优于 yam 文件,当在同一目录中时。
springboot 内置 tomacat 容器,内嵌 servet 容器,能直接打成 jar 包,通过 java -jar 命令。spring 打包 war 包,交给 servlet 容器启动,
java 基础
1.hashmap 相关知识点
hashmap 的结构,以及 1.7 和 1.8 版本的区别?
结构,hashmap 由数组+链表组成,1.8 以后 hashmap 由数组+链表/红黑树组成,由 entry 构成,每个 entry 保存了 key、value、hashcode 值。
1.7 1.8 版本的 hashmap 区别主要体现在几个方面:创建区别、扩容时机、resize、hash 规则、解决了多线程下的死循环问题、新增元素插入位置不同,从这几个方面进行出发
https://blog.csdn.net/m0_59806423/article/details/129933209
创建
1.7 版本:hashmap 在进行无参构造时,默认初始化容量为 16,默认负载因子为 0.75,默认扩容阈值为 9。指定容量和负载因子时,会首先进行判断,判断初始化容量大于 0 小于 2 的 30 次方,判断负载因子是否小于 0,并令其容量为 2 的 n 次方,设置扩容阈值。
1.7 版本 put 步骤
1.首先判断是否为 null,如果为空直接插入。
2.然后根据 hashcode 插入到指定位置,插入算法为 int i = indexFor(hash, table.length),然后判断该位置上是否存在 hashcode 相同且 key 值相同的,如果有直接覆盖,没有的话则进行插入链头。注意这里 indexfor 的内部实现逻辑为 h&(length-1),h 值的计算如下,这里做的目的是尽量减少 hash 冲突,均匀分布。
1.7 版本 resize
扩容原理
1.7 在 hashmap 初始化时已经赋予数组容量,扩容原理是复制原数组,并赋予 2 倍容量,然后遍历每个键值对,得到键的 hashcode 与数组长度进行 &运算得到新数组的位置,而且还会造成死锁情况。
1.8 扩容原理大致相同
待添加
hashmap 多线程死循环原理
push 的执行顺序?
1.首先进行初始化数组,容量为 16,每次扩容按照当前的一倍来。
2.然后进行计算出插入 index,计算公式为(n-1)& hash 值,hash 值=hashcode^(hashcode>>>16)无符号右移 16,这样做的目的是减少 hash 冲突。
3.然后对 key 值进行比较,如果一样进行更新,不一样在查看是否为链表或者红黑树,选择特定的类型进行插入更新操作,这里要判断当前节点数是否大于 8。
4.然后插入完毕之后看是否要进行扩容操作。负载因子为 0.75。
避免 HashMap 发生死循环常用的解决方案:
1)是用安全线程 ConcurrentHashMap 代替 HashMap(推荐使用)
2)使用线程安全容器 HashTable 代替,但是性能低(不推荐使用)
3)使用 Synchronized 或 Lock 加锁后,再进行操作,相当于多线程排队执行,也会影响性能(不建议使用
2.数组和链表的区别
数组是由相同类型的元素的集合所组成的线性数据架构,当我们创建时,内存分配了一块连续的内存空间。所以数组有几个特点:内存是连续的,相同类型元素,根据索引(下标)获取元素。
这里有几个问题,数组为什么必须是相同类型的元素,因为要知道申请多大的空间。数组为什么查找快,这里其实我感觉也跟前面那个有点关系,就是你知道长度和元素类型后,你就能跟根据索引快速的那个元素,用偏移量计算就可得出。数组插入和删除的过程,插入就是把下标后面的元素往后移一位。删除就是把后面的元素往前挪一位。如何进行扩容?创建一个新数组,然后把数据复制进去。
链表的特点:内存不连续,每个元素存储一个 pre 指针和一个 next 指针,用脸链接前面的元素和后面的元素。链表查找、更新和删除?链表查找需要从头遍历的尾,而更新和删除比较快,需要查找到元素 next 和 pre 指针重新指向即可。
特点分析:数组占用的空间较大,因为她是预先分配好的,而且由于连续内存存储的特点,查找起来效率较高,删除更新等效率较低。
https://zhuanlan.zhihu.com/p/363343364
3.arraylist、linklist、array
array 和 arraylist:array 是指定大小,能存储基本类型, 但不支持 remove、add 等操作。大小已经固定,主要是存储和遍历他们,用 array 较好,arraylist 能动态扩展大小。
arraylist 和 linklist,linklist 查找指定位置元素主要是先定位是看 index 是在前部分还是后部分,然后再决定向前向后遍历查询,而 arraylist 主要是根据偏移量和第几个元素进行定位,这样 arraylist 的定位速度就比较快速,而插入方面数组,可能还会触发扩容逻辑,而链表则较为简单,arraylist 是连续内存,linklist 是不连续内存。
在 java8 之前默认不指定容量大小时默认初始化为容量 10 的数组,java8 之后默认不指定容量时默认初始化为空数组,只有当 add 时才会初始化容量为 10。
数组新增和插入逻辑
arraylist 是实现了快速随机访问接口 randomaccess,使用 for 循环获取数据比用迭代器遍历快,而没有实现的用迭代器较好。
arraylist 每次初始化都会预留多余的空间,造成空间浪费,但是 linklist 由于存储了前后指向的指针,所浪费的空间大于 arraylist。
arraylist 和 linklist 都不保证线程安全。
删除新增操作,arraylist 都要对容量进行判断,在复制一份数组,进行位移,在复制。而 arraylist 查找比较快。
4.java 常见的容器
collection 和 map。collection 中包括 list 和 set,list 包括 arraylist、linklist、vector,set 包括 hashset、linkset、treeset。map 包括 hashmap、ConcurrentHashMap、LinkedHashMap、HashTable、TreeMap。
5.hashcode 相同的值是否 equal 为 true?
这里要弄清楚几个问题,hashcode 的生成原理,equal 是通过比较什么来判断的。equal 是通过判断对象地址是否相等或者是否是同一个对象(这里有个知识点就是在 string 中,equal 由于被重写即判断地址也判断内容,所以如果重写了的按照其重写规则而定),而 hashcode 其实获取哈希值,其本质是加快 hashmap、hashset、hahstable 这样的结构的查询速度,通过比较 hashcode 的值,在通过 equal 来判断是否是相等,不同的值有可能 hashcode 一样是因为可能会产生哈希碰撞。
这里引申一个问题:为什么重写 equal 就要重写 hashcode,因为你改变了规则,但是根据两个对象相等,其 hashcode 一定相等,所以你必须来重写 hashcode 来匹配其在 equal 重写方法中改变的规则。
6.==和 equal
==比较的(基本数据类型比较的是值,引用数据类型比较的是内存地址),而 equal 依照其重写规则而定,不仅仅是比较地址。
7.bio、aio、nio 什么区别?
bio:阻塞式 io,例如 socket 这种通讯。
nio:非阻塞式 io,基于字节流和字符流进行操作,三大核心概念,通道,缓冲区,选择器。例如聊天服务器。客户端发送的请求都会注册到多路复用器上,然后由多路复用器进行轮询处理。
aio:多线程异步回调通知,jdk7 之后。
8.自动装箱和自动拆箱
自动装箱:将基础数据类型转化为对应的包装器类型。
自动拆箱:将包装器类型转化为基础数据类型。
装箱主要是利用了 valueof(),拆箱利用的是 intValue(),vauleOf 已经在 cache 中创建了[-128,127]范围的对象数组,当新建的 Integer 数值在该范围中,会从对象数组里返回相应的对象。如果超出范围会重新创建一个对象。
9.值传递和引用传递
值传递:将参数复制一份,修改形参将不会对实参造成影响。
引用传递:将实参的地址传递给形参,修改形参也是在修改实参。
两者的最主要区别就是是直接传递的,还是传递的是一个副本。
对于这个例子,由于这个值传递的是对象的地址。
其他
1.开发规范相关
数据命名:大小写,驼峰结构,尽量表现出使用场景含义用英语,避免使用汉字拼音等。接口中如果是使用了设计模式,尽量用命名体现出来。
注释和文档相结合,接口必须用在线文档网站或者文档的形式提供给前端,最好可直接在线调试,接口文档规范,基本就是入参出参数字段的含义结构,还有返回的错误码等等。
mysql 建表:表的命名是业务名称+表的作用,禁止用数字开头用下划线隔开,小数类型用 demical,用合适的类型和长度,tinyint 和 char 等等。
索引:一般 type 最少要达到 range 级别,索引命名规范为类型+字段。
上线:打包部署的时候备份之前版本+时间,每次更新有版本更新记录,相关责任人,功能说明,发布在公众号或者是记录在文档中。
git 提交:对于功能说明作者版本都有记录。
接口设计:作者名,时间,功能说明。单一性原则,职责原则。
评论