首页 > 编程语言 >Java面试专题

Java面试专题

时间:2023-10-28 16:59:13浏览次数:28  
标签:专题 Java 索引 面试 缓存 线程 内存 key 数据

Java面试专题

面试题背后的逻辑->拆分问题讲解->回答方式及参考问题

Redis篇

使用场景

1、你在最近的项目中哪些场景使用了redis?

缓存:缓存击穿,缓存穿透,缓存雪崩,双写一致性,数据过期策略,数据淘汰策略
分布式锁:setnx,redission

2、什么是缓存穿透,怎么解决?

缓存穿透:查询一个 不存在 的数据,mysql查询不到数据也不会写入缓存,就会导致每次请求都查数据库,带来巨大压力。
	解决方案一:缓存空数据,mysql查询返回的数据为空,仍把这个空结果进行缓存
    	优点:简单
    	缺点:消耗内存,可能会造成数据不一致
	解决方案二:布隆过滤器
    	优点:内存占用较少,没有多余key
    	缺点:实现复杂,存在误判
    布隆过滤器主要依赖于bitmap,存储数据时,将请求的数据进行多次hash计算,分别将对应的hash值存到bitmap上,当查询时,只需要检查bitmap的对应位置上是否都有值,有则直接返回,没有也不访问数据库,减轻数据库的压力。但是会存在误判问题。(可设置误判率5%)

3、什么是缓存击穿,怎么解决?

给某一个key设置了过期时间,当key过期的时候,恰好在这个时间点对这个key有大量的并发请求过来,这些并发请求可能瞬间把DB压垮。
    解决方案一:互斥锁,强一致,性能差。
    	当缓存失效时,不立即去load DB,先使用redis的setnx去设置一个互斥锁,当操作成功返回时再进行load DB的操作并回设缓存,否则重试get缓存的方法。
    解决方案二:逻辑过期,高可用,性能优,不能保证数据强一致
    	在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间,当查询的时候,从redis中取出数据后判断是否过期,如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个线程不是最新的。

4、什么是缓存雪崩,怎么解决?

缓存雪崩是指在同一时间端大量的缓存key同时失效或者Redis服务宕机,导致大量的请求到达数据库,带来巨大压力。
    解决方案:
    1、给不同的key的TTL添加随机值
    2、利用Redis集群提高服务的可用性(哨兵模式、集群模式)
    3、给缓存业务添加降级限流策略(nginx或spring cloud gateway)
    4、给业务添加多级缓存
缓存三兄弟:
穿透无中生有key,布隆过滤null隔离
缓存击穿过期key,锁与非期解难题
雪崩大量过期key,过期时间要随机
面试必考三兄弟,可用限流来保底

5、redis作为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)

1、介绍自己简历上的业务,我们当时是把套餐的热点数据存入到了缓存中,虽然是热点数据,但是实时要求性没那么高,所以,我们当时采用的是异步的方案同步的数据。
2、我们当时是把抢劵的库存存入到了缓存中,这个需要实时进行数据同步,为了保证数据的强一致性,我们当时采用的是redisson提供的读写锁来保证数据的同步。

6、你来介绍一下异步和redission的方案

允许延时一致的业务,采用异步通知
    1、使用MQ消息中间件,更新数据后,通知缓存删除
    2、使用canal中间件,不需要修改业务代码,伪装为mysql的一个从节点,canal通过读取binlog数据更新缓存

强一致的,利用redisson提供的读写锁
    1、共享锁:读锁readLock,加锁之后,其他线程可以共享读操作
    2、排它锁:独占锁WriteLock,加锁之后,阻塞其他线程读写操作

7、redis作为缓存时,数据的持久化是如何做到?

Redis持久化:
    RDB全称Redis Database Backup file(Redis数据备份文件),也被叫叫做Redis数据快照,简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。
    AOF全称Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。因为是记录命令,AOF文件会比RDB文件大得多。而且AOF会对同一个key执行多次写操作,但只有最后一次操作才有意义,通过执行 bgrewriteaof可以让AOF文件执行重写功能,用最少的命令达到相同效果
    
RDB的执行原理?
    bgsave开始时会fork主进程得到子进程,子进程共享主进程的主存数据,完成fork后读取内存数据并写入RDB文件。fork采用的是copy-on-write技术:
    1、当主进程执行读操作时,访问共享内存;
    2、当主进程执行写操作时,则会拷贝一份数据,执行写操作。
    
AOF默认是关闭的,主要修改redis.conf配置文件来开启AOF:
    AOF的命令记录的频率也可以通过redis.conf文件来配
    appendfsync always:可靠性高,几乎不丢数据,但性能影响大
    appendfsync everyesc:性能适中,最多丢失1s数据
    appendfsync no:(操作系统控制)性能最好,但可靠性较差,可能丢失大量数据
这两种持久化有什么区别?
	RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis宕机恢复数据的时候,方便从RDB的快照文件中恢复数据;AOF的含义是追加文件,当redis操作写命令的时候,则会存储在这个文件中,当redis实例宕机恢复数据时,会从这个文件中再次执行一遍命令来恢复数据。
这两种方式,哪一种恢复的比较快呢?
	RDB因为是二进制文件,在保存的时候体积也是最小的,它恢复的比较快,但是它有可能会丢数据,我们在项目中也会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令。

8、假如Redis的key过期后,会立即删除吗?

Redis对数据设置数据的有效时间,数据过期之后,就需要将数据从内存中删除。可以按照不同的规则进行删除,这种删除规则则称之为数据的删除策略(数据过期策略)
    惰性删除:设置该key的过期时间后,我们不用去管它,当需要该key时,我们再检查该key是否过期,如果过期,我们就删除掉它,反之返回该key。该方法对CPU友好,智慧在使用该key的时候才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查;但是对内存不太友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放。
    定期删除:每隔一段时间,我们就会对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中过期的key)。定期删除有两种模式:slow模式是定时任务,执行频率默认10Hz,每次不超过25ms,以通过修改配置文件redis.conf的hz选项来调整这个次数;fast模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms。优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期删除,也能有效释放过期键占用的内存。缺点:难以确定删除操作执行的时长和频率。

9、假如缓存过多,内存是有限的,内存被占满了怎么办?

数据淘汰策略:当Redis中的内存不够用时,此时向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。Redis支持的八种不同的策略来选择要删除的key:
	noeviction:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这个策略;
	volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先淘汰;
    allkey-random:对全体key,随机进行淘汰;
    volatile-random:对设置了TTL的key,随机进行淘汰;
    allkey-lru:对全体key,基于LRU算法进行淘汰;(Least Recently Used:最近最少使用)
    allkey-lfu:对全体key,基于LFU算法进行淘汰;(Least Frequently Used:最少频率使用)
    volatile-lru:对设置了TTL的key,基于LRU算法进行淘汰
    volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰

10、redis分布式锁,是如何实现的?

Redis实现分布式锁主要利用redis的setnx命令。setnx是 SET if not exist(如果不存在,则set)的简写。
    SET lock value NX EX 10,添加锁,并设置超时时间为10,如果不设置锁。当一个线程获取锁成功后,这时业务超时或服务宕机,无法释放锁,会导致死锁,影响服务器性能。
我们当时使用的redisson实现的分布式锁,底层是setnx和lua脚本(保证原子性)

11、Redis实现分布式锁如何合理控制锁的有效时长?

解决方案:
    1、根据业务执行时间预估
    2、给锁续期(可行)
在redisson的分布式锁中,提供了一个WatchDog(看门狗),一个线程获取锁成功后,WatchDog会给持有锁的线程续期(默认是每隔10s续期一次)

12、redisson的锁可以重入吗?

可以重入,多个锁重入需要判断是否是当前线程,在redis中进行存储的时候使用的hash结构,来存储线程信息和重入次数。

13、redisson锁能解决主从数据一致的问题吗

不能解决,但是可以使用redisson提供的红锁来解决,但是这样的话,性能就太低了,如果业务中非要保证数据的强一致性,建议采用zookeeper实现的分布式锁。

Redis的其他面试问题

1、Redis的集群有哪些方案,知道吗?

主从复制
哨兵模式
分片集群

2、介绍一下Redis的主从同步

单点Redis的并发能力是有限的,要进一步提高redis的并发能力,并需要搭建主从集群,实现读写分离,一般是一主多从,主节点负责写数据,从节点负责读数据。

3、说一下主从同步数据的流程

全量同步
1、从节点请求主节点同步数据(replication id,offset)
2、主节点判断是否是第一次请求,是第一次则与从节点同步版本信息(replication id,offset)
3、主节点执行bgsave,生成rdb文件,发送给从节点去执行
4、在rdb执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件)
5、把生成之后的命令日志文件发送给从节点去执行

增量同步
1、从节点请求主节点同步数据,主节点判断是否是第一次请求,不是第一次请求则获取从节点的offset值
2、主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步

4、怎么保证Redis的高并发高可用?

哨兵模式:实现主从集群的自动故障恢复(监控、自动故障恢复、通知)

5、redis集群脑裂,该怎么解决?

集群脑裂是由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样还会导致redis客户端还在老的master那里写数据,新节点无法同步数据,当网络恢复后,sentinel会将老的master降为从节点,这时再从新的master同步数据就会导致数据的丢失。
    解决:我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。

6、redis的分片集群有什么作用?

集群中有多个master,每个master保存不同的数据
每个master都可以有多个salve节点
master之间通过ping检测健康状态
客户端请求可以访问集群任意节点,最终都会被转发到正确节点

7、redis分片集群中数据是怎么存储和读取的?

redis分片集群引入了哈希槽的概念,redis集群有16384个哈希槽
将16384个哈希槽分配到不通的redis实例
读写数据:根据key的有效部分计算哈希值,将16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身作为有效部分)余数作为插槽,寻找插槽所在的实例。

8、redis是单线程的,为什么还那么快?

redis是纯内存操作,执行速度非常快
采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
使用I\O多路复用模型,非阻塞IO

9、能解释一下I\O多路复用模型吗?

Redis是纯内存操作,执行速度非常快,它的瓶颈是网络延迟而不是执行速度,I\O多路复用模型主要就是实现了高效的网络请求。
    
用户空间和内核空间
    Linux系统中一个进程使用内存的情况划分两部分:用户空间、内核空间
    用户空间只能执行受限的命令,而且不能直接调用系统资源,必须通过内核提供的接口访问
    内核空间可以执行特权命令,调用一切系统资源
   
    linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:
    写数据时,要把用户缓冲区的数据拷贝到内核缓冲区,然后写入设备
    读数据时,要从设备读取数据到内核缓冲区,然后再拷贝到用户缓冲区
    
    IO多路复用是利用单个线程来同时监听多个socket,并在某个socket可读、可写时得到通知,从而避免无效等待,充分利用了cpu资源。不过监听socket的方式、通知的方式又有多种实现,常见的有:
    select
    poll
    epoll
    
    Redis网络模型
    就是使用I\O多路复用结合事件的处理器来应对多个Socket请求
    连接应答处理器
    命令回复处理器,在redis6.0之后,为了提高性能,使用了多线程来处理回复事件
    命令请求处理器,在redis6.0之后,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是多线程。
I\O多路复用是指利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知从而避免无效等待,充分利用了cpu资源。目前I\O多路复用都是采用epoll模式实现,他会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。

MySQL篇

优化

1、如何定位慢查询?

1、介绍一下当时产生问题的场景(我们当时的一个接口测试的时候非常的慢,压测结果大概5秒钟)
2、我们系统中当时采用了运维工具(Skywalking),可以监测出哪个接口,最终因为是sql的问题
3、在sql中开启了慢日志查询,我们设置的值是2秒钟,一旦sql执行超过2秒就会记录到日志中(调试阶段)
候选人:
    我们当时做压测的时候有的接口非常的慢,接口的响应时间超过了2秒以上,因为我们当时的系统部署了运维的监控系统Skywalking,在展示的报表中可以看到是哪一个接口比较慢,并且可以分析这个接口的哪部分比较慢,这里可以看到sql的具体执行时间,所以可以定位是哪个sql出了问题。
    如果,项目中没有这种运维的监控系统,其实在mysql中也提供了慢日志查询的功能,可以在mysql的系统配置文件文件中开启这个慢日志的功能,并且也可以设置sql执行超过多少时间来记录到一个日志文件中,我记得上一个项目配置的是2秒,只要sql执行时间超过了2秒就会记录到日志文件中,我们就可以在日志文件找到比较慢的sql。

2、那这个sql执行很慢,如何分析呢?

可以采用MySQL自带的分析工具explan
    通过key和key_len检查是否命中了索引(索引本身存在是否失效的情况)
    通过type字段查看sql是否有进一步优化空间,是否存在全局索引扫描或全盘扫描
    通过extra建议判断,是否出现了回表的情况,如果出现了,可以尝试添加索引和修改返回字段来修复

3、了解过索引吗?(什么是索引)

索引(index)是帮助MySQL高效获取数据的数据结构(有序)
提高数据检索的效率,降低数据库的IO成本(不需要全表扫描)
通过索引列队数据进行排序,降低数据排序的成本,降低了CPU的消耗

4、索引底层的数据结构了解过吗

mysql的innoDB引擎采用的是B+树的数据结构来存储索引
    阶数更多,路径更短
    磁盘读写代价B+树更低,非叶子节点不存储数据只存储指针,叶子节点存储数据
    B+树便于扫库和区间扫描,叶子节点是一个双向链表

5、什么是聚簇索引,什么是非聚簇索引?

聚簇索引(聚集索引):数据和索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个
非聚簇索引(二级索引):数据和索引分开存放,B+树的叶子节点保存对应的主键,可以有多个

6、知道什么是回表查询吗?

通过二级索引找到对应的主键值,到聚集索引中查找整行数据,这个过程就是回表

7、知道什么是覆盖索引吗?

覆盖索引是指:查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到
    使用id查询,直接走聚集索引,一次索引扫描,直接返回数据,性能高
    如果返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select *

8、MYSQL中超大分页怎么处理?

超大分页一般是在数据量比较大时,我们使用了limit分页查询,并且需要对数据进行排序,这个时候效率就很低,我们可以采用覆盖索引和子查询来解决
    先分页查询数据的id字段,再通过子查询来过滤,只查询这个id列表中的数据就可以了,因为查询id的时候走的是覆盖索引,所以效率可以提升很多

9、索引创建原则有哪些?

1、对于数据量较大,且查询比较频繁的表建立索引
2、对于经常作为查询条件、排序、分组操作的字段建立索引
3、尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的频率越高
4、如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引
5、尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率
6、要控制索引的数量,索引并不是越多越好,索引越多,维护索引的代价也越大
7、如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它,当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询

10、什么情况下索引会失效?

1、违反最左前缀法则
2、范围查询右边的列,不能使用索引
3、不要在索引上进行运算操作,否则索引将失效
4、字符串不加单引号,造成索引失效
5、以%开头的Like模糊查询,索引失效

11、谈谈你对sql优化的经验

表的设计优化
    设置合适的数值(tinyint int bigint)
    设置合适的字符串类型(char varchar)char定长,效率高,varchar可变长度,适用范围广,效率稍低
索引优化
SQL语句的优化
    select语句务必指明字段名称,避免直接使用select *
    sql语句要避免造成索引失效的写法
    尽量用union all代替union,union会自动进行一次过滤,去除重复数据,效率低
    避免在where子句中对字段进行表达式操作
    join优化:能用inner join就不用left join right join,如必须使用一定要以小表为驱动,内连接会对两个表进行优化,优化把小表放到外边,把大表放到里边。left join或right join,不会重新调整顺序
主从复制,读写分离
分库分表

其他面试题

1、事务的特性是什么?可以详细说一下吗?

什么是事务?
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体向系统提交或撤销操作请求,要么同时成功,要么同时失败
事务的特性:
    原子性(Atomicity):事务是不可分割的最小单位,要么全部成功,要么全部失败
    一致性(Constancy):事务完成时,必须使所有的数据都保持一致状态
    隔离性(Isolation):数据库系统提供的隔离机制,使事务在不受外界环境的影响下独立运行
    持久性(Durability):事务一旦提交或撤销,对数据库中数据的操作都是永久的
        
并发事务问题:
    脏读:一个事务读到另一个事务还没有提交的数据
    不可重复读:一个事务先后读取同一条数据,但两次读取的数据,结果不同
    幻读:一个事务按条件查询时,没有查询到数据行,但是在插入数据时,又发现该数据行已经存在

2、怎么解决并发事务的问题?

解决方案:对事务进行隔离
    读未提交
    读已提交:解决脏读的问题
    可重复读:解决脏读,不可重复读的问题,默认,性能和安全的中和选择
    串行化:解决脏读,不可重复读,幻读的问题,但性能也较差

3、undo log 和 redo log的区别?

缓冲池(buffer pool):主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),以一定的频率刷新到磁盘,从而减少磁盘IO,加快处理速度
数据页:是InnoDB存储引擎磁盘管理的最小单元,每个页的大小默认为16KB。页中存储的是行数据
候选人:好的,redo log日志记录的是数据页的物理变化,服务器宕机可用来同步数据,而undo log不同,它记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据,比如我们删除一条数据的时候,就会在undo log中新增一条对应的delete语句,如果发生回滚就执行逆操作。redo log保证了事务的持久性,undo log保证了事务的原子性和一致性

4、事务中的隔离性是如何保证的?(解释一下MVCC)?

MySQL中的多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突
隐藏字段:
    trx_id:记录每一次操作的事务id,是自增的
    roll_pointer(回滚指针):指向上一个版本的事务版本记录地址
uudo log
    回滚日志,存储老版本数据
    版本链:多个事务并行操作某一行记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表。
readView
候选人:事务的隔离性是由mvcc和锁实现的。其中mvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层主要分为三部分,第一个是隐藏字段,第二个是undo log,第三个是readView读视图。隐藏字段是指:在mysql中给每个表都设置隐藏字段,有一个是trx_id,记录每一次操作的事务id,是自增的;另一个是roll_pointer,指向上一个版本的书屋版本记录地址
    undo log主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过roller_pointer指针形成一个链表
    readView解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id判断该访问哪个版本的数据,不同的隔离级别快照读是不一样的,最终的访问结果也不一样,如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是rr隔离级别仅在第一次执行快照读时生成ReadView,后续复用。

5、主从同步原理

MySQL的主从复制的核心就是二进制文件
    二进制(BINLOG)中记录了所有的DDL(数据定义语句)语句和DML(数据操纵语句)语句,但不包括数据查询(SELECT、SHOW)语句。
    复制分三步:
    1、Master主库在数据提交时,会把数据变更记录在二进制文件binlog中
    2、salve从库中的一个线程IOThread读取二进制文件binlog,并写入到从库的中继日志Ready log中
    3、salve从库中的SQLThread重做中继文件中的日志,将数据同步到从库数据库中,主从复制完成

6、你们项目用过分库分表吗?

分库分表的时机:
    1、项目业务数据逐渐增多,业务发展比较迅速,单表的数据达1000W或20G以后
    2、优化已解决不了性能问题(主从读写分离、查询索引)
    3、IO瓶颈(磁盘IO)、CPU瓶颈(聚合查询、连接数太多)
    
拆分策略
    垂直分库:
    	垂直分库:以表为依据,根据业务将不同表拆分到不同库中,高并发下提高磁盘IO和网络连接数。特点:1、按业务对数据分级管理、维护、监控、扩展;2、在高并发下,提高磁盘IO的数据量连接数
    	垂直分表:以字段为依据,根据字段属性将不同字段拆分到不同表中,冷热数据分离,两表互不影响。1、把不常用的字段单独放在一张表中;2、把text,blob等大字段拆分出来放在附表中。
    水平分库:
    	水平分库:将一个库的数据拆分到多个库中,解决海量数据存储和高并发的问题。
    	水平分表:将一个表的数据拆分到多个表中(可以在同一个库内),解决单表存储和性能的问题。特点:优化单一表数据量过大而产生的性能问题;2、避免IO争抢并减少锁表的几率。
    
    分库会产生各种分布式事务的问题,可用mycat中间件解决。

框架篇

Spring

1、Spring框架中的单例bean是线程安全的吗?

不是线程安全的
    Spring框架中有一个@Scope注解,默认的值就是singleton,单例的
    因为一般在spring的bean中都是注入无状态的对象,没有线程安全问题,如果在bean中定义了可修改的成员变量,是要考虑线程安全问题的,可以使用多例或者加锁来解决

2、什么是AOP,你们项目中有没有使用到AOP?

面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合。常用作记录日志操作,缓存和spring实现的事务等。核心是:使用aop中的环绕通知+切点表达式(找到要记录日志的方法),通过环绕通知的参数获取请求方法的参数(类、方法、注解、请求方式等),获取到这些参数以后,保存到数据库。

image-20231024155932220

3、Spring中的事务是如何实现的?

Spring支持编程式事务管理和声明式事务管理两种方式
    编程式事务管理:需要使用TransactionTemplate来进行实现,对业务代码有侵入性,项目中很少使用;
    声明式事务管理:声明式事务管理建立在AOP之上的。其本质式通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

4、Spring中事务失效的场景有哪些?

1、异常捕获处理。自己处理了异常,没有抛出。解决:手动抛出
2、抛出检查异常。解决:配置rollbackFor属性为Exception
3、非public方法导致的事务失效。解决:改为public

5、Spring的bean的生命周期

1、通过BeanDefinition获取bean的定义信息
2、调用构造函数实例化bean
3、bean的依赖注入
4、处理Aware接口
5、Bean的前置处理器BeanPostProcessor-前置
6、初始化方法
7、Bean的后置处理器BeanPostProcessor-前置
8、销毁bean

6、Spring中的循环引用

循环引用(依赖):循环依赖其实就是循环引用,也就是两个或两个以上的bean相互持有对方,最终形成闭环,比如A依赖于B,B依赖于A。
循环依赖在spring中是允许存在的,spring框架依据三级缓存已经解决了大部分的循环依赖
    1、一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
    2、二级缓存:缓存早期的bean对象(生命周期还没走完)
    3、三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

7、构造方法出现了循环依赖怎么解决?

原因:由于bean的生命周期中构造函数是第一个执行的,spring框架并不能解决构造函数的依赖注入
    解决方法:使用@Lazy进行懒加载,什么时候需要对象再进行bean对象的创建。

8、Spring框架中常用注解?

Spring:
	@Componet、@Controller、@Service、@Repository:用于实例化bean
    @Autowired:用于根据类型依赖注入
    @Qualifier:一起使用用于根据名称进行依赖注入
    @Scope:标注Bean的范围
    @Configuration:指定当前类是一个Spring配置类
    @ComponentScan:用于指定Spring在初始化容器时要扫描的包
    @Bean:标注将该方法的返回值存储到Spring容器中
    @import:使用@Import导入的类会被Spring加载到IOC容器中
    @Aspect、@Before、@After、@Around、@Pointcut:用于切面编程
SpringMVC:
	@RequestMapping:映射请求路径
    @RequestBody:接收http请求的json数据,将json转换为java对象
	@RequestParam:指定请求参数的名称
	@PathVirable:从请求路径下获取请求参数,传递给方法的形式参数
	@ResponseBody:注解实现将controller方法返回对象转化为json对象响应给客户端
	@RequestHeader:获取指定的请求数据
	@RestController:@Controller+@ResponseBody
SpringBoot:
	@SpringBootConfiguration
	@EnableAutoConfiguration
	@ComponentScan

SpringMVC

1、SpringMVC的执行流程?

视图版本与前后端开发版本前5步一致,
1、用户发送出请求到前端控制器DispatcherServlet
2、DispatcherServlet收到请求调用HandlerMapping(处理器适配器)
3、HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet
4、DispatcherServlet调用HandlerAdapter(处理器适配器)
5、HandlerAdapter经过适配调用具体的处理器(Handler/Controller)
视图版本:
6、Controller执行完成返回ModelAndView对象
7、HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet
8、DispatcherServlet解析后返回具体的View(视图)
前后端开发:
6、方法上添加了@ResponseBody
7、通过HttpMessageConverter来返回结果转换为JSON并响应

SpringBoot

1、SpringBoot自动配置原理?

1、再spring项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan
2、其中@EnableAutoConfiguration是实现自动化配置的核心注解。该注解通过@Import注解导入对应的配置选择器。内部就是读取了该项目和该项目引用的Jar包的classpath路径下META-INF/spring.factores文件中的所配置的类的全类名。在这些配置类中所定义的bean会根据条件注解所指定的条件来决定是否需要将其导入到spring容器中。
3、条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的bean放入到spring容器中使用。

Mybatis

1、Mybatis的执行流程

1、读取Mybatis配置文件:mybatis-config.xml加载运行环境和映射文件
2、构造会话工厂SqlSessionFactory
3、会话工厂创建SqlSession对象(包含了执行SQL语句的所有方法)
4、操作数据库的接口,Executor执行器,同时负责查询缓存的维护
5、Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
6、输入参数映射
7、输出映射结果

2、Mybatis是否支持延迟加载?

延迟加载的意思是:在需要数据的时候才进行加载,不需要用到数据时就不加载
Mybatis支持一对一关联对象和一对多关联集合对象的延迟加载
在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false,默认是关闭的。

3、延迟加载的底层原理知道吗?

1、使用CGLB创建目标对象的代理对象
2、当调用目标方法时,进入拦截器invoke方法,发现目标方法是null值,执行sql查询
3、获取数据以后,调用set方法设置属性值,再继续查询目标方法,值就有了

4、Mybatis的一级、二级缓存用过吗?

一级缓存:基于PerpetualCache的hashMap本地缓存,其存储作用域session。当Session进行flush或close之后,该Session的所有Cache就将清空,默认打开一级缓存。
二级缓存:是基于namespace和mapper的作用域起作用的,不是依赖于SQL session,默认也是采用PerpetualCache,HashMap存储。需要单独开启,一个是核心配置,一个是mapper映射文件。

5、mybatis的二级缓存什么时候会清理缓存中的数据?

当某一个作用域(一级缓存Session/二级缓存Namespace)进行了新增、修改、删除操作以后,默认该作用域下所有select中的缓存将被clear。

微服务篇

Spring Cloud

1、SpringCloud5大组件

通常情况下:
Euraka:注册中心
Ribbon:负载均衡
Feign:远程调用
Hystrix:服务熔断
Zuul/Gateway:网关
    
随着SpringCloudAlibaba在国内兴起,我们在项目中使用了一些阿里巴巴的组件
Nocas:注册中心/配置中心
Ribbon:负载均衡
Feign:远程调用
sentinal:服务保护
Gateway:服务网关

2、服务注册和发现是什么意思?Spring Cloud如何实现服务注册发现?

服务注册:服务提供者需要把自己的信息注册到eureka,由eureka来保存这些信息,比如服务名称、ip、端口等等
服务发现:消费者向eureka拉取服务列表信息,如果服务提供者有集群,则消费者会利用负载均衡算法,选择一个发起调用
服务监控:服务提供者会每隔30s向eureka发送心跳,报告健康状态,如果eureka服务90秒没接收到心跳,从eureka中剔除

3、Eureka和Nacos的区别?

Nacos与euraka的共同点(注册中心)
    1、都支持服务注册和服务拉取
    2、都支持服务提供者心跳方式做健康检测
Nacos与eureka的区别(注册中心)
    1、Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
    2、临时实例心跳不正常会被剔除,非临时实例则不会被剔除
    3、Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
    4、Nacos集群默认采用AP模式(高可用),当集群中存在非临时实例时,采用CP模式(强一致);Eureka采用AP方式
Nocas还支持了配置中心,eureka则只有注册中心,也是选择使用Nocas的一个重要原因

4、你用过的负载均衡是如何实现的?

微服务的负载均衡主要使用了一个组件Ribbon,比如,我们在使用feign远程调用的过程中,底层的负载均衡就是使用了ribbon。

5、Ribbon负载均衡策略有哪些?

1、简单轮询(RandomRobinRule)
2、权重(WeightedResponseTimeRule)
3、随机(RandomRule)
4、区域敏感策略,以区域可用的服务器为基础进行服务器的选择(默认)(ZoneAvoidanceRule)
5、忽略短路的服务器,并选择并发数较低的
6、先过滤非健康的,再选择连接数较小的实例
7、重试机制的选择逻辑

6、如果想自定义负载均衡策略该如何实现?

1、创建类实现IRule接口,可以指定负载均衡策略(全局)
2、在客户端的配置文件中,可以配置某一个服务调用的负载均衡策略(局部)

7、什么是服务雪崩,怎么解决这个问题?

服务雪崩:一个服务失效,导致整条链路的服务都失效的情形
服务降级:服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃,一般在实际开发中与feign接口整合,编写降级逻辑
服务熔断:默认关闭,需要手动打开,如果检测到10秒内请求的失败率超过50%,就触发熔断机制。之后每隔5秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。如果微服务可达,则关闭熔断机制,恢复正常请求。

8、微服务监控是怎么监控的?

skywalking:一个分布式系统的应用程序性能监控工具(Application Performance Managment),提供完善的链路追踪能力,apache的顶级项目。(问题定位,性能分析,服务关系,服务告警)
    1、skyworking主要可以监控接口、服务、物理实例的一些状态。特别是在压测的时候可以看到众多服务中哪些服务和接口比较慢,我们可以针对性的分析和优化。
    2、我们还在skyworking设置了告警规则,特别是在项目上线后,如果报错,我们分别设置了可以给相关负责人发短信和发邮件,第一时间知道了项目的bug情况,第一时间修复。

业务问题

1、你们项目中有没有做过限流?怎么做的?

常规限流,为了防止恶意攻击,保护系统正常运行。
Tomcat限流
Nginx限流
    1、控制速率(突增变量)(漏桶算法)
    2、控制并发连接数
网关限流(令牌桶)
    1、定义流量对象
    2、令牌桶每秒填充速率
    3、令牌桶容量

2、解释一下CAP和Base

分布式事务方案的知道
分布式系统设计方向
根据业务指导使用正确的技术选择

3、CAP定理

1998年,加州大学的计算机科学家Eric Brewer提出,分布式系统有三个指标:
    Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致
    Availability(可用性):用户访问集群中的任意健康节点,必须得到响应,而不是超时或拒绝
    Partition tolerance(分区容错性)
    	Partition:因为网络故障或其他原因,导致分布式系统中的部分节点与其他节点失去连接,形成独立分区。
    	tolerance:在集群出现分区时,整个系统也要持续对外提供服务
Eric Brewer说,分布式系统无法同时满足这三个指标,这个结论就叫做CAP定理。
    
    结论:
    分布式系统节点之间肯定是需要网络连接的,分区(P)是必然存在的
    如果保证访问的高可用性(A),可以持续对外提供服务,但不能保证数据的强一致性
    如果保证访问的数据的强一致性(C),就要放弃高可用性

4、BASE理论

BASE理论是对CAP定理的一种解决思路,包含三个思想:
    Basically Available(基本可用):分布式系统出现故障时,必须损失部分可用性,即保证核心可用
	Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态
	Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致

5、分布式事务解决方案?

Seata:
Seata事务管理中有三个重要的角色:
    TC-事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
    TM-事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
    RM-资源管理器:管理分支事务处理的资源,与TC交谈以注册事务和报告分支事务的状态,并驱动分支事务提交或回滚
    
seata的XA模式(CP,需要互相等待各个分支事务提交,可以保证强一致性,性能差,银行业务):
    RM一阶段的工作:
    	1、注册分支事务到TC
    	2、执行分支业务sql但不提交
    	3、报告执行状态到TC
    TC二阶段的工作:
    	TC检测各分支事务执行状态
    	1、如果都成功,通知所有RM提交事务
    	2、如果有失败,通知所有RM回滚事务
    RM二阶段的工作:
    	接收TC指令,提交或回滚事务
    
seata的AT模式(AP,底层使用undo-log实现,性能好,互联网业务):
    AT模式同样是分阶段提交的事务模型,不过却弥补了XA模式中资源锁定周期过长的缺陷。
   阶段一RM的工作:
    	1、注册分支事务
    	2、记录undo-log(数据快照)
    	3、执行业务sql并提交
    	4、报告事务状态
    阶段二提交时RM的工作:
    	删除undo-log
    阶段二回滚时RM的工作:
    	根据undo-log恢复数据到更新前
    
seata的TCC模式(AP,性能较好,不过需要人工编码实现):
    1、Try:资源的检测和预留
    2、Confirm:完成资源操作业务;要求Try成功Confirm一定要能成功
    3、Cancel:预留资源释放,可以理解为try的反向操作
   
MQ模式实现分布式业务,在A服务写数据的时候,需要在同一个事务内发送消息到另外一个事务,异步,性能最好,互联网业务

6、分布式服务的接口幂等性如何设计?

幂等:多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。
  
需要幂等的场景:
    1、用户重复点击(网络波动)
    2、MQ消息重复
    3、应用使用失败或超时重试机制
    
解决方法:
    如果是新增数据,可以使用数据库的唯一索引
    如果是新增或更改数据
    1、token+redis
    2、分布式锁

7、你们项目中使用了什么分布式任务调度

xxl-job解决的问题:
    解决集群任务的重复执行问题
    cron表达式定义灵活
    定时任务失败了,重试和统计
    任务量大,分片执行

8、xxl-job的路由策略有哪些?

1、First:固定选择第一个实例
2、Last
3、轮询
4、随机
5、一致性hash
6、最不经常使用
7、最近最久未使用
8、故障转移
9、忙碌转移
10、分片广播(大数据量的任务同时都需要执行时选择)

9、xxl-job任务执行失败怎么办?

故障转移+失败重试,查看日志分析--->邮件告警

消息中间件篇

RabbitMQ

1、RabbitMQ如何保证数据不丢失

1、开启生产者确认机制,确保生产者的消息能到达队列
2、开启持久化功能,确保消息未消费前在队列不会丢失
3、开启消费者确认机制为auto,由spring确认消息成功后完成ack
4、开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机,交由人工处理

2、RabbitMQ消息的重复消费问题如何解决的?

给每条消息设置一个唯一的标识,我们在处理消息时,先到数据库查询一下,这个数据是否存在,如果不存在,说明没有处理过,这个时候就可以正常处理这个消息了,如果已经存在这个数据了,就说明消息重复消费了,我们不需要再消费了。
    另外我们也可以通过redis分布式锁,数据库锁等方法来解决。

3、RabbitMQ中死信交换机?(RabbitMQ延迟队列有了解过吗?)

延迟队列:进入队列的消息会被延迟消费的队列
场景:超时订单、限时优惠、定时发布
    
死信交换机:
    当一个队列中的消息满足下列情况,就可能成为死信(dead letter)
    1、消费者使用basic.reject或basic.nack声明消费失败,并且消息的requeue参数设置为false
    2、消息是一个过期消息,超时无人消费
    3、要投递的队列消息堆积满了,最早的消息可能成为死信
    如果该队列配合了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机。
    
TTL:
TTL也就是Time-To-Live。如果一个队列中的消息TTL结束仍未消费,则会变为死信,ttl超时分为两种情况:
    消息所在的队列设置了存活时间
    消息本身设置了存活时间。(若都设置,以时间小的为准)
候选人:我们当时的一个XX项目有一个XX业务,需要用到延迟队列,其中就是使用RabbitMQ来实现的。延迟队列就是用死信交换机和TTL来实现的。如果消息超时未消费就会变成死信,在RabbitMQ中如果消息成为死信,队列可以绑定一个死信交换机,在死信交换机上可以绑定其他队列,在我们发消息的时候可以按照需求指定TTL的时间,这样就实现了延迟队列的功能了。
    RabbitMQ还有一种方式可以实现延迟队列,在RabbitMQ中安装一个死信插件,这样更方便一些,我们只需要在声明交互的时候,指定这个就是死信交换机,然后发送消息的时候直接指定超时时间就行了,相对于死信交换机+TTL要省略了一些步骤。

4、RabbitMQ如果有100万消息堆积在MQ,如何解决?

1、增加更多消费者,提高消费速度
2、在消费者内开启线程池加快消息处理速度
3、扩大队列容积,提高堆积上限,采用惰性队列
    惰性队列:
    1、在声明队列的时候可以设置属性x-queue-mode为lazy,即为惰性队列
    2、基于磁盘存储,消息上限高
    3、性能比较稳定,但基于磁盘存储,受限于磁盘IO,时效性会降低

5、RabbitMQ的高可用机制有了解过吗?

我们可以采用镜像模式搭建集群。镜像队列结构就是一主多从,所有操作都是主节点完成,然后同步给镜像节点。主节点宕机后,镜像节点会替代成新的主节点(如果在主从同步前,主节点就已宕机,可能出现数据丢失)
    那出现丢数据怎么解决?
    我们可以采用仲裁队列,与镜像队列一样,都是主从模式,支持主从数据同步,主从同步基于Raft协议,强一致,并且使用起来也非常简单,不需要额外配置,在声明队列的时候只要指定这个是仲裁队列即可。

6、RabbitMQ的优点

RabbitMQ是基于ErLang语言编写的,正是由于ErLang语言在数据交互方面的优越性能,才使得RabbitMQ和它的名字一样,像兔子一样快速。
1、开源,性能优秀,稳定性高;
2、提供可靠性消息投递模式,返回模式;
3、与spring AMQP完美整合,API丰富
4、集群模式丰富(简单、work、订阅、主题、路由)
保证数据不丢失的前提下做到高可靠性

Kafka

1、Kafka是如何保证数据不丢失的?

需要从三个层面去解决这个问题:
    1、生成者发送消息到Broker丢失
    	设置异步发送,发送失败使用回调进行记录或重发
    	失败重试,参数设置,可以设置重试次数
    2、消息在Brocker中存储丢失
    	发送确认acks,选择all,让所有副本都参与保存数据后确认
    3、消费者从Broker接收消息丢失
    	关闭自动提交偏移量,手动提交偏移量
    	提交方式,最好是同步+异步提交

2、kafka如何保证消费的顺序性?

一个topic的数据可能存在不同的分区中,每个分区都有一个按照顺序的存储的偏移量,如果消费者关联了多个分区不能保证顺序性
    解决方案:
    1、发送消息时指定分区号
    2、发送消息时按照相同的业务设置相同的key

3、kafka的高可用机制了解过吗?

可以从两个层面回答,第一个是集群,第二个是复制机制
    集群:一个kafka集群由多个broker实例组成,即使某一台宕机,也不耽误其他broker继续对外提供服务
    复制机制:
    	1、一个topic有多个分区,每个分区有多个副本,有一个leader,其余的是follower,副本存储在不同的broker中
    	2、所有的分区副本的内容都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader,保证系统的容错性、高可用性

4、解释一下复制机制中的ISR

ISR(in-sync replica)需要同步复制保存的follower
分区副本分为了两类,一个是ISR,与leader副本同步保存数据,另外一个普通的副本,是异步同步数据,当leader挂掉之后,会优先从ISR副本列表中选取一个作为leader

5、kafka数据清理机制了解过吗?

kafka的存储结构:
    1、kafka中topic的数据存储在分区上,文件如果过大分区会分段存储segment
    2、每个分段都在磁盘上以索引和日志文件的形式存储
    3、分段的好处是,第一能够减少单个文件内容的大小,查找数据方便,第二方便kafka清理日志
日志的清理策略有两个:
    1、根据消息的保留时间,当消息保存时间超过了指定时间,就会触发清理,默认是168个小时(7天)
    2、根据topic存储数据的大小,当topic所占的日志文件大小大于一定的阈值,则开始删除最久的消息。(默认关闭)

6、kafka实现高性能的设计,有了解过吗?

1、消息分区:不受单台服务器的限制,可以不受限的处理更多的数据
2、顺序读写:磁盘顺序读写,提升读写效率
3、页缓存:把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问
4、零拷贝:减少上下文切换及数据拷贝
5、消息压缩:减少磁盘IO和网络IO
6、分批发送:将消息打包批量发送,减少网络开销

常见集合篇

算法复杂度分析

1、什么是算法时间复杂度

时间复杂度表示了算法的执行时间与数据规模之间的增长关系

2、常见的时间复杂度有哪些?

O(1)、O(n)、O(nlogn)、O(n^2)

3、什么是算法的空间复杂度

表示算法占用的额外存储空间与数据规模之间的增长关系

List相关面试题

1、数组如何获取其他元素的地址值?

通过数组下标结合寻址公式查找

2、为什么数组索引从0开始,假如从1开始不行吗?

在根据数组索引获取元素的时候,会用索引和寻址公式来计算内存所对应的元素数据,寻址公式是:数组的首地址+索引乘以存储数据的类型大小。若从1开始,则寻址公式为数组的首地址+(索引+1)乘以存储数据的类型大小,反而繁琐。

3、ArrayList底层的实现原理是什么?

Arraylist底层是用动态的数组实现的
ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
ArrayList在进行扩容的时候是原来的1.5倍,每次扩容都需要拷贝数组
ArrayList在添加数组的时候
    1、确保数组已使用长度加1之后足够存下下一个数据
    2、计算数组的容量,如果当前数组已使用长度+1后大于当前数组长度,则调用grow方法扩容
    3、确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上
    4、返回添加成功布尔值

4、ArrayList list = new ArrayList(10)中的list扩容了几次?

该语句只是声明和实例了一个ArrayList,指定了容量为10,并未扩容。

5、如何实现数组和List之间的转换

数组转List,使用JDK中java.util.Arrays工具类的asList方法
List转数组,使用List的toArray方法。无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组。
    
面试官再问:
    1、用Arrays.asList转List后,如果修改了数组内容,list受影响吗
    2、List用toArray转数组后,如果修改了List内容,数组受影响吗
再答:
    1、Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,
    因为它的底层使用Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址
    2、list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响。

6、ArrayList和LinkedList的区别是什么?

1、底层数据结构
    ArrayList是动态数组的数据结构实现
    LinkedList是双向链表的数据结构实现
2、操作数据效率
    ArrayList按下标查询的时间复杂度为O(1),内存是连续的,根据寻址公式。linkedList不支持下标查询;
    查找(未知索引):ArrayList需要遍历,LinkedList也需要遍历,时间复杂度都为O(n);
    新增和删除:
    ArrayList尾部插入和删除,时间复杂度为O(1);其他部分增删需要挪动数组,时间复杂度为O(n);
	LinkedList头尾节点增删时间复杂度是O(1);其他都需要遍历链表,时间复杂度为O(n);
3、内存空间占用:
    ArrayList底层是数组,内存连续,节省内存
    LinkedList是双向链表需要存储数据和两个指针,更占用内存
 4、线程安全
    都不是线程安全的。可以使用线程安全的ArrayList和LinkedList,用Collections.synchronizedList包装一下。

7、什么是红黑树?

红黑树:也是一种自平衡的二叉搜索树
所有的红黑树资格都是希望红黑树能够保持平衡
红黑树的时间复杂度:增加、删除、修改都是O(logn)

HashMap相关面试题

1、说一下HashMap的实现原理?

HashMap的数据结构:底层使用hash表数据结构,即数组和链表或红黑树
    1、当我们往HashMap中put元素的时候,利用key的hashcode重新hash计算出当前元素在数组中的下标。
    2、存储时。如果出现hash值相同的key,此时有两种情况:
    	是同一个key:覆盖原先key所对value值
    	不是同一个key:(出现冲突),将当前key-value放入链表或红黑树中(当链表的长度大于8,数组长度大于64时转换为红黑树)
    3、获取时,直接找到hash值对应的下标,再进一步判断key是否相同,从而找到对应值。

2、HashMap的jdk1.7和jdk1.8有什么区别?

1、数据结构:
    1.7之前只有链表加数组;1.8之后加入红黑树存储;在链表数大于8且数组等于64时转化为红黑树,在链表数小于等于6时转化为链表
2、插入数据:
    1.7链表是头插法,1.8则是尾插法
3、初始化过程
    1.7创建了HashMap实例之后,就对table数组初始化,1.8中,当我们创建了HashMap实例以后,底层并没有初始化table数组。当首次添加时,进行判断,如果发现table没有初始化,则对数组进行初始化。
4、1.7中HashMap底层定义了Entry内部类,1.8中替换为Node类。

3、HashMap的put方法的具体流程?

首先,会调用key所在的类的hashcode()方法得到哈希值1,然后对哈希值1经过某种算法hash()得到哈希值2,再对哈希值2经过某种算法就可以确定该值在table中的索引值i了。若该索引值上没有元素,则添加成功。若有元素,则需要比较两个key的哈希值是否相同,若不相同,则添加成功;若相同,则需要比较两个key的equals方法是否相同,若不相同,则添加成功,若相同则替换对应的value值。

4、讲一讲HashMap的扩容机制(resize方法)?

在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到了扩容的阈值(数组长度 * 0.75)
每次扩容的时候都是扩容之前容量的2倍
扩容之后会创建一个新的数组,再把老数组数据挪动到新数组中(先分裂,再迁移)。

5、HashMap的size为什么是2的整数次方?

HashMap为了存取高效,要尽量减少碰撞,要尽量把数据分配均匀,每个链表大致相同,这个实现就是计算把数据存放到哪个链表中的算法。
    这个算法就是取模,但是取模效率不高,源码中进行了优化,取模(hash % length)与按位与(hash & (length-1))相等的前提是length是2的n次方,2的n次方减一实际就是n个1,这样运算之后,数据就可以分散均匀。
    例:97 % 16 = 1
    (16 - 1) & 97 = 1

6、死锁产生的原因和解决方案?

死锁产生的四个条件:
    1、互斥
    2、占有且等待
    3、不可剥夺
    4、循环等待
针对条件1,我们无法改变,因为互斥是解决线程安全问题的必要条件
    针对条件2,我们可以一次性申请完全部的所需资源
    针对条件3,我们可以再进一步申请资源,申请不到就放弃已拥有的资源
    针对条件4,我们可以将资源改为线性顺序申请,申请资源时默认申请序号小的,这样就可以避免循环等待的问题了。

7、hashmap在1.7中多线程出现死循环的问题?

在jdk1.7的hashmap进行扩容的时候,因为是头插法,在进行数据迁移的过程中,有可能出现死循环的问题
    比如说,现在有两个线程
    线程1:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程2介入
    线程2:也是读取hashmap,直接进行扩容,因为是头插法,在链表的顺序会进行颠倒,比如原来的顺序AB,扩容后的顺序变为BA,线程2执行结束。
    线程1:继续执行的时候会出现死循环的问题
    线程1先将A迁移到新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以BAB,形成循环。
    JDK8采用尾插法,就避免了jdk7中的死循环问题。

8、hashMap与hashTable有什么区别?

1、线程安全性
    hashMap是线程不安全的,hashTable是线程安全的
2、是否为null
    huanMap允许键值为null,hashTable不允许键值为null
3、初始化和扩容
    hashMap的初始化容量为16,而hashTable初始化容量为11;扩容时hashMap容量翻倍,hsahTable容量翻倍+1
4、继承的类不同
5、迭代器
    hashmap的迭代器是fail-fast,hashTable不是

并发编程篇

线程基础

1、线程和进程的区别?

1、进程是正在允许程序的实例,进程中包含了线程,每个线程执行不同的任务
2、不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
3、线程更轻量,线程上下文切换成本比进程上下文切换低(上下文切换是指从一个进程切换到另一个进程)

2、并行与并发的区别?

候选人:
现在都是多核CPU,在多核CPU下:并行是同一时间动手做多件事情的能力,四核CPU同时执行四个线程;并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU。

3、线程创建的方式?

1、继承Thread类
2、实现Runnable接口
3、实现Callable接口
4、线程池创建线程( 项目中使用方式 )

4、Runnable和Callable的区别

1、Runnable接口的run方法没有返回值
2、Callable接口call()方法有返回值,是个泛型,和future,futureTask配合可以用来获取异步执行的结果
3、Callable接口的call()方法允许抛出异常,而runnable的run()方法的异常只能在内部消化,不能继续上抛

5、线程的run()和start()有什么区别?

start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
run():封装了要被线程执行的代码,可以被多次调用。

6、线程包括哪些状态,状态之间是如何变化的?

新建(NEW)、可运行(RUNNABLE)、终止(TERMINATED)、阻塞(BLOCKD)、等待(WAITING)、计时等待(TIMED_WALTING)
创建线程对象是新建状态
调用了start()方法是可运行状态
线程获取了cpu执行权,执行结束是终止状态
在执行状态的过程中,如果没有获得cpu执行权,可能会切换为其他状态

7、怎么保证线程按顺序执行?notify和notifyAll有什么区别?

可以用join()方法实现线程顺序执行,比如现在有三个线程,在线程2中join线程1,这样线程2就必须等待线程1执行完之后再执行,同理再在线程3中join线程2,这样线程的执行顺序便为线程1,线程2,线程3。
    notifyAll:唤醒所有wait的线程
    notify:只随机唤醒一个wait线程

8、在java中的wait和sleep方法有什么不同?

共同点:wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态
不同点:1、方法归属不同
    sleep(long)是Thread的静态方法
    而wait(),wait(long)都是Object的成员方法,每个对象都有
    2、醒来时机不同
    执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
    wait(long)和wait()还可以被notify唤醒,wait()如果不唤醒,就会一直等下去
    它们都可以被打断唤醒
    3、锁特性不同(重点)
    wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
    wait方法执行后会释放对象锁,允许其他线程获取该对象锁
    而sleep如果在synchronized代码中执行,并不会释放对象锁

9、如何停止一个正在允许的线程?

1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
2、使用stop方法强行终止(不推荐使用,方法已作废)
3、使用interrupt方法中断线程
    打断阻塞的线程(sleep,wait,join),线程会抛出异常
    打断正常的线程,可以根据打断状态来标记是否退出线程

线程安全

1、synchronized关键字的底层原理

线程池

JVM虚拟机篇

JVM组成

1、什么是程序计数器?

作用:程序计数器(PC寄存器)的作用是存储下一条指令的地址,也就是即将要执行的指令的地址。然后会由执行引擎来执行下一条指令。
   
介绍:
    1、它是一块很小的内存空间,小到可以忽略不计,也是运行速度最快的存储区域
    2、在JVM规范中,每个线程都有自己的程序计数器,它是线程私有的。它的生命周期与线程的生命周期是一致的
    3、任何时间一个线程只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的方法的地址,如果执行的是本地方法,则其值为undefined
    4、它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这指示数器来完成
    5、字节码解释器是通过改变计数器的值来执行下一条字节码指令
    6、它是唯一一个Java虚拟机中没有规定任何OutOfMemoryError情况的区域

2、使用PC寄存器存储字节码指令地址有什么用?

因为CPU需要不停的切换线程,切换回来以后,需要知道从哪条指令开始执行。

3、PC寄存器为什么被设定为线程私有?

Java是天然的多线程程序,所以线程的执行会被经常的中断和恢复。有了PC寄存器之后,CPU可以准确的知道当前线程执行到哪里了,就可以从上次中断的地方继续执行。为了避免多线程之间的互相干扰,保证PC寄存器的准确性,所以PC寄存器需要设定为私有的。

4、介绍一下java堆?

image-20231026172905603

线程共享的区域:主要用来保存对象实例,数组等,内存不够则抛出OutOfMemoryError异常
组成:年轻代+老年代
    年轻代被划分为三部分,Eden区和两个严格相同的Survivor区
    老年代主要保存生命周期长的对象,一般是一些老的对象
    在jdk1.7中,堆中还有永生代,存储的是类信息、静态变量,常量,编译后的代码,在1.8及之后版本中被移除,把数据存储到了本地内存的元空间中,防止内存溢出。

5、什么是虚拟机栈?

1、每个线程运行时所需要的内存,称为虚拟机栈,先进后出
2、每个栈由多个栈帧组成,对应着每次方法调用时占用的内存
3、每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

6、垃圾回收是否涉及栈内存?

垃圾回收主要指就是堆内存,当栈帧弹栈之后,内存就会被释放

7、栈内存分配的越大越好吗?

不是。一个栈默认的栈内存通常为1024K
栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数为512个,如果把栈内存改为2048K,那么能活动的栈帧就会减半

8、方法内的局部变量是否线程安全?

1、如果方法内部局部变量没有逃离方法的作用范围,它是线程安全的
2、如果是局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全

9、什么情况下会导致栈内存溢出?

1、栈帧过多导致栈内存溢出,典型问题:递归调用
2、栈帧过大导致栈内存溢出(不易出现)

10、堆栈的区别是什么?

栈内存一般会用来存储局部变量和方法调用,堆内存则用来存储java对象和数组,堆会GC垃圾回收,而栈不会
栈内存是线程私有的,而堆内存是线程共有的
两者异常错误不同
    栈空间不足:java.lang.StackOverFlowError
    堆空间不足:java.lang.OutOfMemoryError

11、能不能解释一下方法区?

1、方法区(Method Area)是各个线程共享的内存区域
2、主要存储类的信息、运行时常量池
3、虚拟机启动时创建,关闭虚拟机时释放
4、如果方法区域中的内存无法满足分配请求,则会抛出异常java.lang.OutOfMemory:MetaSpace

12、介绍一下运行时常量池

常量池:可以看作是一张表,虚拟机指令根据这张表找到要执行的类名、方法名、参数类型、字面量等信息
当类被加载,它的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址

13、你听过直接内存吗?

1、并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存
2、常见于NIO操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受JVM回收管理

类加载器

1、什么是类加载器?

JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。

2、类加载器有哪些?

1、启动类加载器。加载JAVA_HOME/jre/lib目录下的库
2、扩展类加载器。加载JAVA_HOME/jre/lib/ext目录下的类
3、应用类加载器。加载classPath下的类
4、自定义类加载器。自定义类继承ClassLoader,实现自定义类加载规则

3、什么是双亲委派机制?

需要加载某一个类时,先委托上一级的加载器进行加载,如果上一级加载器还有上级,则继续向上委托,如果该类委托上级没有被加载,则由子加载器尝试加载该类。

4、JVM为什么采用双亲委派机制?

1、通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
2、为了安全,保证类库API不会被修改

5、说一下类装载的执行过程?

加载:查找和导入class文件
验证:保证加载类的准确性
准备:为类变量分配内存并设置类变量初始值
解析:把类中的符号引用转换为直接引用
初始化:对静态变量、静态代码块执行初始化操作
使用:JVM开始从入口方法开始执行用户的程序代码
卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象

垃圾回收

1、对象什么时候会被GC回收?

如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,就有可能被垃圾回收器回收

2、定位垃圾的方式

1、引用计数法。一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收
    当对象间出现了循环引用的话,引用计数法就会失效
2、可达性分析算法(常用)
    哪些对象可以作为GC Root?
    虚拟机栈(栈帧中的本地变量表)中的对象
    方法区中的类静态属性引用的对象
    方法区中常量引用的对象
    本地方法栈中JNI(即一般说的Native方法)引用的对象

3、JVM垃圾回收算法有哪些?

1、标记清除算法
    标记清除算法分为两个阶段:标记和清除
    1、标记:根据可达性分析算法得出的垃圾进行标记
    2、清除:对这些标记为可回收的内容进行垃圾回收
    优点:标记清除速度比较快
    缺点:碎片化比较严重,内存是不连贯的

2、标记整理算法
    优点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有一定的影响。
    
3、复制算法
	优点:
		在垃圾对象比较多的时候,效率较高
		无内存碎片化问题
	缺点:
		分配的2块内存空间,在同时刻,只能使用其中的一块,内存使用率较低

4、分代收集算法-工作机制

image-20231026222439383

1、新创建的对象,会先分配到eden(伊甸园)区
2、当伊甸园区内存不足,标记伊甸园与from(现阶段没有)的存活对象
3、将存活对象复制到to,复制完成后,伊甸园和from内存得以释放
4、经一段时间伊甸园内存又不足,标记伊甸园与to中存活的对象,将存活的对象复制到from
5、当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

5、MinorGC、MixedGC、FullGC是什么?

MinorGC[YoungGC]发生在新生代的垃圾回收,暂停时间短(STW,stop-the-word,暂停所有应用程序线程,等待垃圾回收的完成)
MixedGC:新生代+老年代部分区域的垃圾回收,G1收集器特有
FullGC:新生代+老年代完整垃圾回收,暂停时间长(STW),应尽量避免

6、常见的垃圾回收器有哪些?

1、GMS(并发)垃圾回收器
    GMS是一款并发的、使用标记-清除算法的垃圾回收器,该垃圾回收器是针对老年代垃圾回收的,是一款以获得最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大的特点是在垃圾回收时,应用仍然能正常运行
2、G1垃圾回收器,作用在新生代+老年代
3、串行垃圾回收器
4、并行垃圾回收器

7、G1垃圾回收器

应用于新生代和老年代,在JDK9之后默认使用G1
划分成多个区域,每个区域都可以充当eden,survivor,old,humongous,其中humongous专为大对象准备
采用复制算法
响应时间与吞吐量兼顾
分成三个阶段:新生代回收、并发标记、混合收集
如果并发失败(即回收速度赶不上创建新对象速度),会触发Full GC

image-20231027141606662

8、强引用、软引用、弱引用、虚引用的区别

强引用:只要所有的GC Root能找到,就不会被垃圾回收
软引用:仅有软引用引用该对象时,在垃圾回收多次,内存仍不足时会再次触发垃圾回收
弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会被回收弱应用对象
虚引用:必须配合引用队列使用,被引用对象回收时,会被虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存

JVM实践

1、JVM调优参数可以在哪里设置参数值?

war包部署在tomcat中设置
    修改TOMCAT_HOME/bin/catalina.sh文件
    JAVA_OPTS="-Xms512m -XMx1024m"
jar包部署在启动参数设置
    nohup java -Xms512m -XMx1024m -jar XXX.jar --spring.profiles.active=prod &
    nohup:用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行
    参数 &:让命令在后台执行,终端退出后命令仍旧执行

2、用的JVM调优的参数都有哪些?

对于JVM调优,主要就是调整年轻代,老年代,元空间的内存空间大小及使用的垃圾回收器类型
设置堆空间大小   
	-Xms:设置堆的初始化大小
	-Xmx:设置堆的最大大小
虚拟机栈的设置
    -Xss:对每个线程stack大小的调整
年轻代的eden区和两个幸存者区的大小比例
    -XXSurvivorRatio=8,表示年轻代中的分配比率:survivor:eden=2:8
    -XXSurvivorRatio=3,表示年轻代中的分配比率:survivor:eden=2:3
年轻代晋升老年代的阈值
    -XX:MaxTenuringThreshold=threshold
设置垃圾回收器
    通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器
    -XX:+UseG1GC

4、说一下JVM调优的工具?

命令工具
jps:进程状态信息
jstack:查看java进程内线程的堆栈信息
jmap: 显示堆的信息
jhat:堆转储快照分析工具
jstat:JVM统计检测工具
可视化工具    
jconsole:
VisualVM 

5、java内存泄漏排查思路

内存泄漏通常是指堆内存,通常指一些大对象不被回收的情况
解决思路
    1、通过jmap或设置jvm参数生成一个dump文件,主要jmap生成dump文件只有当前程序在运行中才生效
    2、通过工具,VisualVM去分析dump文件,VisualVM可以加载离线的dump文件
    3、通过查看堆信息的情况,可以大概定位堆内存溢出是哪一行代码
    4、找出对应的代码,通过阅读上下文的情况,进行修复即可

6、CPU飙高排查方案与思路?

1、使用top命令查看占用CPU情况
2、通过top命令后,可以找到是哪一个进程占用cpu较高
3、使用ps命令查看进程中的线程信息
4、使用jstack查看进程中哪些线程出现了问题,最终定位

企业场景篇

设计模式

1、工厂模式

1、简单工厂
    简单工厂并不是设计模式,所有产品都共有一个工厂,如果有新增产品,则修改代码,违背了开闭原则
    是一种编码习惯,可以借鉴这种编程思路
2、工厂方法模式
    给每一个产品提供一个工厂,让工厂专门复制对应产品的生产,遵循开闭原则
    项目中用的最多
3、抽象工厂模式
    如果有多个维度的产品需要生产时,优先建议采用抽象工厂(工厂的工厂)
    一般企业开发中用的较少

标签:专题,Java,索引,面试,缓存,线程,内存,key,数据
From: https://www.cnblogs.com/clone2653/p/17790686.html

相关文章

  • javaweb--JDBC的API-Connection
    1、获取执行SQL对象2、管理事务setAutoCommit(bool)true为自动提交false为手动提交commit()提交事务rollback()回滚事务packagecom.avb.jdbc;importjava.sql.Connection;importjava.sql.DriverManager;importjava.sql.SQLException;importjava.sql.Statement;public......
  • Java-全网最详细反射
    Java-反射前言Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动......
  • Java基础 多线程的 6 种状态
       Java的虚拟机当中没有定义运行状态,因为当线程抢夺到CPU执行权的时候,虚拟机会把当前线程交给操作系统管理,虚拟机就不管了。 ......
  • javaweb--JDBC的API-DriverManager
    DriverManager可以实现的功能1、注册驱动ClassforName("com.mysql.jdbc.Driver");查看Driver类源码在静态代码块中会运行DriverManager类mysql5以后的驱动包可以省略注册驱动步骤2、获取连接java:mysql://ip地址(域名):端口号/数据库名称?参数键值对1&参数键值对2...jdbc:my......
  • Java基础 阻塞队列的方式实现等待唤醒机制,哪里体现了等待?哪里又体现了唤醒?
    Java的阻塞队列(BlockingQueue)可以用来实现等待唤醒机制,其中等待和唤醒的操作在队列的不同方法中体现:1.等待:在阻塞队列中,等待通常发生在以下情况:2.当队列为空时,消费者线程试图从队列中取出元素时,它会被阻塞,直到队列中有元素可供消费。这种等待是通过阻塞队列的take()方法来实现......
  • javaweb学习每日总结-第八天
    第八天学习Springboot今天也终于是学到了springboot的技术,springboot是一款Java开发的框架,也是当下最流行的开发方式,没有之一!今天我进行了springboot技术的入门,初步了解了springboot技术的发展和应用,也用idea写了一个最简单的springboot程序。除此之外,我还下载了postmen这个软......
  • javaweb--JDBC入门
    packagecom.avb.jdbc;importjava.sql.Connection;importjava.sql.DriverManager;importjava.sql.Statement;publicclassjdbcdemo{publicstaticvoidmain(String[]args)throwsException{//注册驱动Class.forName("com.mysql.jdbc.D......
  • Java基础 等待唤醒机制(阻塞队列方式实现)
    等待唤醒机制还可以用阻塞队列的方式进行实现    练习:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码细节:生产者和消费者必须使用同一个阻塞队列阻塞队列的创建方式(泛型:队列里面数据的类型):ArrayBlockingQueue<String> queue = new  ArrayBlockingQueue<......
  • Java基础 什么是生产者和消费者
    在Java中,"生产者-消费者"(Producer-Consumer)是一种常见的并发编程模型,用于协调多个线程之间的工作,其中一些线程充当生产者,而其他线程充当消费者。这模型通常用于处理共享数据的情况,其中生产者线程生成数据并将其放入共享缓冲区,而消费者线程则从缓冲区中取出数据并进行处理。主要特......
  • Java基础 Java 为什么非要在 锁对象身上调用 wait()方法 和 notifyAll()方法
    在Java中,wait()和notify()方法必须在同步块或同步方法内部使用,通常在锁对象上调用,有以下原因:1.互斥性:当你在同步块内部使用wait()和notify()方法时,它们与锁对象紧密相关,确保在调用wait()和notify()时能够维持互斥性。这意味着在调用wait()时,当前线程会释放锁,而在调用notify()时,......