黑马的视频文档特别清楚, 这里我只是安装自己的话语总结一下.
Redis
1.什么是缓存穿透?怎么解决?
缓存穿透是指查询一个一定不存在的数据,由于存储层查不到数据因此不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。解决方案的话,我们通常都会用布隆过滤器来解决它。
2.你能介绍一下布隆过滤器吗?
布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是Redisson实现的布隆过滤器。它的底层原理是,先初始化一个比较大的数组,里面存放的是二进制0或1。一开始都是0,当一个key来了之后,经过3次hash计算,模数组长度找到数据的下标,然后把数组中原来的0改为1。这样,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。当然,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%。其实这个误判是必然存在的,要不就得增加数组的长度。5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。
3.什么是缓存击穿?怎么解决?
缓存击穿的意思是,对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这个时间点对这个Key有大量的并发请求过来。这些请求发现缓存过期,一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
解决方案有两种方式:第一,可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 SETNX 去设置一个互斥锁。当操作成功返回时,再进行 load db的操作并回设缓存,否则重试get缓存的方法。第二种方案是设置当前key逻辑过期,大概思路如下:1) 在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间;2) 当查询的时候,从redis取出数据后判断时间是否过期;3) 如果过期,则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据可能不是最新的。
当然,两种方案各有利弊:如果选择数据的强一致性,建议使用分布式锁的方案,但性能上可能没那么高,且有可能产生死锁的问题。如果选择key的逻辑删除,则优先考虑高可用性,性能比较高,但数据同步这块做不到强一致。
4.什么是缓存雪崩?怎么解决?
缓存雪崩意思是,设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重而雪崩。与缓存击穿的区别是:雪崩是很多key,而击穿是某一个key缓存。
解决方案主要是,可以将缓存失效时间分散开。比如,可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机。这样,每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
5.redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)
我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能,需要让数据库与redis高度保持一致,因为要求时效性比较高。
我们当时采用的读写锁保证的强一致性。我们使用的是Redisson实现的读写锁。在读的时候添加共享锁,可以保证读读不互斥、读写互斥。当我们更新数据的时候,添加排他锁。它是读写、读读都互斥,这样就能保证在写数据的同时,是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是,读方法和写方法上需要使用同一把锁才行。
数据同步可以有一定的延时(这符合大部分业务需求)。我们当时采用的阿里的Canal组件实现数据同步:不需要更改业务代码,只需部署一个Canal服务。Canal服务把自己伪装成mysql的一个从节点。当mysql数据更新以后,Canal会读取binlog数据,然后再通过Canal的客户端获取到数据,并更新缓存即可。
6. 那这个排他锁是如何保证读写、读读互斥的呢?
其实排他锁底层使用的也是SETNX,它保证了同时只能有一个线程操作锁住的方法。
7. 你听说过延时双删吗?为什么不用它呢?
延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据。其中,这个延时多久不太好确定。在延时的过程中,可能会出现脏数据,并不能保证强一致性,所以没有采用它。
8.redis做为缓存,数据的持久化是怎么做的?这两种持久化方式有什么区别呢?
在Redis中提供了两种数据持久化的方式:1) RDB;2) AOF。
RDB是一个快照文件。它是把redis内存存储的数据写到磁盘上。当redis实例宕机恢复数据的时候,可以从RDB的快照文件中恢复数据。AOF的含义是追加文件。当redis执行写命令的时候,都会存储到这个文件中。当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。
9.这两种方式,哪种恢复的比较快呢?
RDB因为是二进制文件,保存时体积也比较小,所以它恢复得比较快。但它有可能会丢数据。我们通常在项目中也会使用AOF来恢复数据。虽然AOF恢复的速度慢一些,但它丢数据的风险要小很多。在AOF文件中可以设置刷盘策略。我们当时设置的就是每秒批量写入一次命令。
10.Redis的数据过期策略有哪些?
在redis中提供了两种数据过期删除策略。第一种是惰性删除。在设置该key过期时间后,我们不去管它。当需要该key时,我们检查其是否过期。如果过期,我们就删掉它;反之,返回该key。第二种是定期删除。就是说,每隔一段时间,我们就对一些key进行检查,并删除里面过期的key。定期清理的两种模式是:1) SLOW模式,是定时任务,执行频率默认为10hz,每次不超过25ms,可以通过修改配置文件redis.conf的hz选项来调整这个次数;2) FAST模式,执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms。Redis的过期删除策略是:惰性删除 + 定期删除两种策略配合使用。
11.Redis的数据淘汰策略有哪些?
这个在redis中提供了很多种,默认是noeviction,不删除任何数据,内部不足时直接报错。这个可以在redis的配置文件中进行设置。里面有两个非常重要的概念:一个是LRU,另外一个是LFU。LRU的意思就是最少最近使用。它会用当前时间减去最后一次访问时间。这个值越大,则淘汰优先级越高。LFU的意思是最少频率使用。它会统计每个key的访问频率。值越小,淘汰优先级越高。我们在项目中设置的是allkeys-lru,它会挑选最近最少使用的数据进行淘汰,把一些经常访问的key留在redis中。
12.数据库有1000万数据,Redis只能缓存20w数据。如何保证Redis中的数据都是热点数据?
可以使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略。那留下来的都是经常访问的热点数据。
13.Redis的内存用完了会发生什么?
这个要看redis的数据淘汰策略是什么。如果是默认的配置,redis内存用完以后则直接报错。我们当时设置的是allkeys-lru策略,把最近最常访问的数据留在缓存中。
14.Redis分布式锁如何实现?
在redis中提供了一个命令SETNX(SET if not exists)。由于redis是单线程的,用了这个命令之后,只能有一个客户端对某一个key设置值。在没有过期或删除key的时候,其他客户端是不能设置这个key的。
15.那你如何控制Redis实现分布式锁的有效时长呢?
redis的SETNX指令不好控制这个问题。我们当时采用的是redis的一个框架Redisson实现的。在Redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间。当锁住的一个业务还没有执行完成的时候,Redisson会引入一个看门狗机制。就是说,每隔一段时间就检查当前业务是否还持有锁。如果持有,就增加加锁的持有时间。当业务执行完成之后,需要使用释放锁就可以了。还有一个好处就是,在高并发下,一个业务有可能会执行很快。客户1持有锁的时候,客户2来了以后并不会马上被拒绝。它会自旋不断尝试获取锁。如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
16.Redisson实现的分布式锁是可重入的吗?
是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计数上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数。
17.Redisson实现的分布式锁能解决主从一致性的问题吗?
这个是不能的。比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时如果当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。
我们可以利用Redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个Redis实例上创建锁,应该是在多个Redis实例上创建锁,并且要求在大多数Redis节点上都成功创建锁,红锁中要求是Redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。
但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变得非常低,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。
18.如果业务非要保证数据的强一致性,这个该怎么解决呢?
Redis本身就是支持高可用的,要做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用ZooKeeper实现的分布式锁,它是可以保证强一致性的。
19. Redis集群有哪些方案,知道吗?
在Redis中提供的集群方案总共有三种:主从复制、哨兵模式、Redis分片集群。
20.介绍一下主从同步
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中。
21.能说一下,主从同步数据的流程吗?
主从同步分为了两个阶段,一个是全量同步,一个是增量同步。
全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:
第一:从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。
第二:主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。
第三:在同时主节点会执行BGSAVE,生成RDB文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的RDB文件,这样就保持了一致。
当然,如果在RDB生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步。
增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步。
22.怎么保证Redis的高并发高可用?
首先可以搭建主从集群,再加上使用Redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证Redis的高并发高可用。
23.你们使用Redis是单点还是集群,哪种集群?
我们当时使用的是主从(1主1从)加哨兵。一般单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用Lua脚本和事务。
24.Redis集群脑裂,该怎么解决呢?
这个在项目中很少见,不过脑裂的问题是这样的,我们现在用的是Redis的哨兵模式集群的。
有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于Redis master节点和Redis slave节点和Sentinel处于不同的网络分区,使得Sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个slave为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,Sentinel会将old master降为slave,这时再从新master同步数据,这会导致old master中的大量数据丢失。
关于解决的话,我记得在Redis的配置中可以设置:第一可以设置最少的slave节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。
25.Redis的分片集群有什么作用?
分片集群主要解决的是海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点。
26.Redis分片集群中数据是怎么存储和读取的?
Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围,key通过CRC16校验后对16384取模来决定放置哪个槽,通过槽找到对应的节点进行存储。取值的逻辑是一样的。
27.Redis是单线程的,但是为什么还那么快?
这个有几个原因吧~~~
-
完全基于内存的,C语言编写。
-
采用单线程,避免不必要的上下文切换和竞争条件。
-
使用多路I/O复用模型,非阻塞IO。
例如:BGSAVE和BGREWRITEAOF都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞。
28.能解释一下I/O多路复用模型?
I/O多路复用是指利用单个线程来同时监听多个Socket,并且在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;
在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
MySQL
1.MySQL中,如何定位慢查询?
系统部署了运维监控系统Skywalking,在它的报表展示中可以看到哪个接口慢,并且能分析出接口中哪部分耗时较多,包括具体的SQL执行时间,这样就能定位到出现问题的SQL。
如果没有这种监控系统,MySQL本身也提供了慢查询日志功能。可以在MySQL的系统配置文件中开启慢查询日志,并设置SQL执行时间超过多少就记录到日志文件,比如我们之前项目设置的是2秒,超过这个时间的SQL就会记录在日志文件中,我们就可以在那里找到执行慢的SQL。
2.那这个SQL语句执行很慢,如何分析呢?
如果一条SQL执行很慢,我们通常会使用MySQL的EXPLAIN命令来分析这条SQL的执行情况。通过key和key_len可以检查是否命中了索引,如果已经添加了索引,也可以判断索引是否有效。通过type字段可以查看SQL是否有优化空间,比如是否存在全索引扫描或全表扫描。通过extra建议可以判断是否出现回表情况,如果出现,可以尝试添加索引或修改返回字段来优化。
3.了解过索引吗?(什么是索引)
索引在项目中非常常见,它是一种帮助MySQL高效获取数据的数据结构,主要用来提高数据检索效率,降低数据库的I/O成本。同时,索引列可以对数据进行排序,降低数据排序的成本,也能减少CPU的消耗。
4.索引的底层数据结构了解过吗?
MySQL的默认存储引擎InnoDB使用的是B+树作为索引的存储结构。选择B+树的原因包括:节点可以有更多子节点,路径更短;磁盘读写代价更低,非叶子节点只存储键值和指针,叶子节点存储数据;B+树适合范围查询和扫描,因为叶子节点形成了一个双向链表。
5.B树和B+树的区别是什么呢?
-
B树的非叶子节点和叶子节点都存放数据,而B+树的所有数据只出现在叶子节点,这使得B+树在查询时效率更稳定。
-
B+树在进行范围查询时效率更高,因为所有数据都在叶子节点,并且叶子节点之间形成了双向链表。
6.什么是聚簇索引什么是非聚簇索引?
聚簇索引是指数据与索引放在一起,B+树的叶子节点保存了整行数据,通常只有一个聚簇索引,一般是由主键构成。
非聚簇索引则是数据与索引分开存储,B+树的叶子节点保存的是主键值,可以有多个非聚簇索引,通常我们自定义的索引都是非聚簇索引。
7.知道什么是回表查询吗?
回表查询是指通过二级索引找到对应的主键值,然后再通过主键值查询聚簇索引中对应的整行数据的过程。
8.知道什么叫覆盖索引吗?
覆盖索引是指在SELECT查询中,返回的列全部能在索引中找到,避免了回表查询,提高了性能。使用覆盖索引可以减少对主键索引的查询次数,提高查询效率。
9.MySQL超大分页怎么处理?
超大分页通常发生在数据量大的情况下,使用LIMIT分页查询且需要排序时效率较低。可以通过覆盖索引和子查询来解决。首先查询数据的ID字段进行分页,然后根据ID列表用子查询来过滤只查询这些ID的数据,因为查询ID时使用的是覆盖索引,所以效率可以提升。
10.索引创建原则有哪些?
创建索引的原则包括:
• 表中的数据量超过10万以上时考虑创建索引。
• 选择查询频繁的字段作为索引,如查询条件、排序字段或分组字段。
• 尽量使用复合索引,覆盖SQL的返回值。
• 如果字段区分度不高,可以将其放在组合索引的后面。
• 对于内容较长的字段,考虑使用前缀索引。
• 控制索引数量,因为索引虽然可以提高查询速度,但也会影响插入、更新的速度。
11.什么情况下索引会失效?
索引可能在以下情况下失效:
• 没有遵循最左匹配原则。
• 使用了模糊查询且%号在前面。
• 在索引字段上进行了运算或类型转换。
• 使用了复合索引但在中间使用了范围查询,导致右边的条件索引失效。
12.SQL的优化经验有哪些?
SQL优化可以从以下几个方面考虑:
• 建表时选择合适的字段类型。
• 使用索引,遵循创建索引的原则。
• 编写高效的SQL语句,比如避免使用SELECT *,尽量使用UNION ALL代替UNION,以及在表关联时使用INNER JOIN。
• 采用主从复制和读写分离提高性能。
• 在数据量大时考虑分库分表。
13.创建表的时候,你们是如何优化的呢?
创建表时,我们主要参考《嵩山版》开发手册,选择字段类型时结合字段内容,比如数值类型选择TINYINT、INT、BIGINT等,字符串类型选择CHAR、VARCHAR或TEXT。
14.在使用索引的时候,是如何优化呢?
在使用索引时,我们遵循索引创建原则,确保索引字段是查询频繁的,使用复合索引覆盖SQL返回值,避免在索引字段上进行运算或类型转换,以及控制索引数量。
15.你平时对SQL语句做了哪些优化呢?
我对SQL语句的优化包括指明字段名称而不是使用SELECT *,避免造成索引失效的写法,聚合查询时使用UNION ALL代替UNION,表关联时优先使用INNER JOIN,以及在必须使用LEFT JOIN或RIGHT JOIN时,确保小表作为驱动表。
16.事务的特性是什么?可以详细说一下吗?
事务的特性是ACID,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。例如,A向B转账500元,这个操作要么都成功,要么都失败,体现了原子性。转账过程中数据要保持一致,A扣除了500元,B必须增加500元。隔离性体现在A向B转账时,不受其他事务干扰。持久性体现在事务提交后,数据要被持久化存储。
17.并发事务带来哪些问题?
并发事务可能导致脏读、不可重复读和幻读。脏读是指一个事务读到了另一个事务未提交的“脏数据”。不可重复读是指在一个事务内多次读取同一数据,由于其他事务的修改导致数据不一致。幻读是指一个事务读取到了其他事务插入的“幻行”。
18.怎么解决这些问题呢?MySQL的默认隔离级别是?
解决这些问题的方法是使用事务隔离。MySQL支持四种隔离级别:
-
未提交读(READ UNCOMMITTED):解决不了所有问题。
-
读已提交(READ COMMITTED):能解决脏读,但不能解决不可重复读和幻读。
-
可重复读(REPEATABLE READ):能解决脏读和不可重复读,但不能解决幻读,这也是MySQL的默认隔离级别。
-
串行化(SERIALIZABLE):可以解决所有问题,但性能较低。
19.undo log和redo log的区别是什么?
redo log记录的是数据页的物理变化,用于服务宕机后的恢复,保证事务的持久性。而undo log记录的是逻辑日志,用于事务回滚时恢复原始数据,保证事务的原子性和一致性。
20.事务中的隔离性是如何保证的呢?(你解释一下MVCC)
事务的隔离性通过锁和多版本并发控制(MVCC)来保证。MVCC通过维护数据的多个版本来避免读写冲突。底层实现包括隐藏字段、undo log和read view。隐藏字段包括trx_id和roll_pointer。undo log记录了不同版本的数据,通过roll_pointer形成版本链。read view定义了不同隔离级别下的快照读,决定了事务访问哪个版本的数据。
21. MySQL主从同步原理是什么?
MySQL主从复制的核心是二进制日志(Binlog)。步骤如下:
-
主库在事务提交时记录数据变更到Binlog。
-
从库读取主库的Binlog并写入中继日志(Relay Log)。
-
从库重做中继日志中的事件,反映到自己的数据中。
22.你们项目用过MySQL的分库分表吗?
我们采用微服务架构,每个微服务对应一个数据库,是根据业务进行拆分的,这个其实就是垂直拆分。
23. 那你之前使用过水平分库吗?
使用过。当时业务发展迅速,某个表数据量超过1000万,单库优化后性能仍然很慢,因此采用了水平分库。我们首先部署了3台服务器和3个数据库,使用mycat进行数据分片。旧数据也按照ID取模规则迁移到了各个数据库中,这样各个数据库可以分摊存储和读取压力,解决了性能问题。
SSM
1. Spring框架中的单例bean是线程安全的吗?
不是线程安全的。当多用户同时请求一个服务时,容器会给每个请求分配一个线程,这些线程会并发执行业务逻辑。如果处理逻辑中包含对单例状态的修改,比如修改单例的成员属性,就必须考虑线程同步问题。Spring框架本身并不对单例bean进行线程安全封装,线程安全和并发问题需要开发者自行处理。
通常在项目中使用的Spring bean是不可变状态(如Service类和DAO类),因此在某种程度上可以说Spring的单例bean是线程安全的。如果bean有多种状态(如ViewModel对象),就需要自行保证线程安全。最简单的解决办法是将单例bean的作用域由“singleton”变更为“prototype”。
2.什么是AOP?
AOP,即面向切面编程,在Spring中用于将那些与业务无关但对多个对象产生影响的公共行为和逻辑抽取出来,实现公共模块复用,降低耦合。常见的应用场景包括公共日志保存和事务处理。
3.你们项目中有没有使用到AOP?
我们之前在后台管理系统中使用AOP来记录系统操作日志。主要思路是使用AOP的环绕通知和切点表达式,找到需要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,例如类信息、方法信息、注解、请求方式等,并将这些参数保存到数据库。
4.Spring中的事务是如何实现的?
Spring实现事务的本质是利用AOP完成的。它对方法前后进行拦截,在执行方法前开启事务,在执行完目标方法后根据执行情况提交或回滚事务。
5. Spring中事务失效的场景有哪些?
在项目中,我遇到过几种导致事务失效的场景:
-
如果方法内部捕获并处理了异常,没有将异常抛出,会导致事务失效。因此,处理异常后应该确保异常能够被抛出。
-
如果方法抛出检查型异常(checked exception),并且没有在@Transactional注解上配置rollbackFor属性为Exception,那么异常发生时事务可能不会回滚。
-
如果事务注解的方法不是公开(public)修饰的,也可能导致事务失效。
6.Spring的bean的生命周期?
Spring中bean的生命周期包括以下步骤:
-
通过BeanDefinition获取bean的定义信息。
-
调用构造函数实例化bean。
-
进行bean的依赖注入,例如通过setter方法或@Autowired注解。
-
处理实现了Aware接口的bean。
-
执行BeanPostProcessor的前置处理器。
-
调用初始化方法,如实现了InitializingBean接口或自定义的init-method。
-
执行BeanPostProcessor的后置处理器,可能在这里产生代理对象。
-
最后是销毁bean。
7.Spring中的循环引用?
循环依赖发生在两个或两个以上的bean互相持有对方,形成闭环。Spring框架允许循环依赖存在,并通过三级缓存解决大部分循环依赖问题:
-
一级缓存:单例池,缓存已完成初始化的bean对象。
-
二级缓存:缓存尚未完成生命周期的早期bean对象。
-
三级缓存:缓存ObjectFactory,用于创建bean对象。
8.那具体解决流程清楚吗?
解决循环依赖的流程如下:
-
实例化A对象,并创建ObjectFactory存入三级缓存。
-
A在初始化时需要B对象,开始B的创建逻辑。
-
B实例化完成,也创建ObjectFactory存入三级缓存。
-
B需要注入A,通过三级缓存获取ObjectFactory生成A对象,存入二级缓存。
-
B通过二级缓存获得A对象后,B创建成功,存入一级缓存。
-
A对象初始化时,由于B已创建完成,可以直接注入B,A创建成功存入一级缓存。
-
清除二级缓存中的临时对象A。
9.构造方法出现了循环依赖怎么解决?
由于构造函数是bean生命周期中最先执行的,Spring框架无法解决构造方法的循环依赖问题。可以使用@Lazy懒加载注解,延迟bean的创建直到实际需要时。
10.SpringMVC的执行流程?
SpringMVC的执行流程包括以下步骤:
-
用户发送请求到前端控制器DispatcherServlet。
-
DispatcherServlet调用HandlerMapping找到具体处理器。
-
HandlerMapping返回处理器对象及拦截器(如果有)给DispatcherServlet。
-
DispatcherServlet调用HandlerAdapter。
-
HandlerAdapter适配并调用具体处理器(Controller)。
-
Controller执行并返回ModelAndView对象。
-
HandlerAdapter将ModelAndView返回给DispatcherServlet。
-
DispatcherServlet传给ViewResolver进行视图解析。
-
ViewResolver返回具体视图给DispatcherServlet。
-
DispatcherServlet渲染视图并响应用户。
11.Springboot自动配置原理?
Spring Boot的自动配置原理基于@SpringBootApplication注解,它封装了@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan。@EnableAutoConfiguration是核心,它通过@Import导入配置选择器,读取META-INF/spring.factories文件中的类名,根据条件注解决定是否将配置类中的Bean导入到Spring容器中。
12.Spring 的常见注解有哪些?
Spring的常见注解包括:
-
声明Bean的注解:@Component、@Service、@Repository、@Controller。
-
依赖注入相关注解:@Autowired、@Qualifier、@Resource。
-
设置作用域的注解:@Scope。
-
配置相关注解:@Configuration、@ComponentScan、@Bean。
-
AOP相关注解:@Aspect、@Before、@After、@Around、@Pointcut。
13.SpringMVC常见的注解有哪些?
SpringMVC的常见注解有:
• @RequestMapping:映射请求路径。
• @RequestBody:接收HTTP请求的JSON数据。
• @RequestParam:指定请求参数名称。
• @PathVariable:从请求路径中获取参数。
• @ResponseBody:将Controller方法返回的对象转化为JSON。
• @RequestHeader:获取请求头数据。
• @PostMapping、@GetMapping等。
14.Springboot常见注解有哪些?
Spring Boot的常见注解包括:
• @SpringBootApplication:由@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan组成。
• 其他注解如@RestController、@GetMapping、@PostMapping等,用于简化Spring MVC的配置。
15.MyBatis执行流程?
MyBatis的执行流程如下:
-
读取MyBatis配置文件mybatis-config.xml。
-
构造会话工厂SqlSessionFactory。
-
会话工厂创建SqlSession对象。
-
操作数据库的接口,Executor执行器。
-
Executor执行方法中的MappedStatement参数。
-
输入参数映射。
-
输出结果映射。
16.Mybatis是否支持延迟加载?
MyBatis支持延迟加载,即在需要用到数据时才加载。可以通过配置文件中的lazyLoadingEnabled配置启用或禁用延迟加载。
17.延迟加载的底层原理知道吗?
延迟加载的底层原理主要使用CGLIB动态代理实现:
-
使用CGLIB创建目标对象的代理对象。
-
调用目标方法时,如果发现是null值,则执行SQL查询。
获取数据后,设置属性值并继续查询目标方法。
18.Mybatis的一级、二级缓存用过吗?
MyBatis的一级缓存是基于PerpetualCache的HashMap本地缓存,作用域为Session,默认开启。二级缓存需要单独开启,作用域为Namespace或mapper,默认也是采用PerpetualCache,HashMap存储。
19.Mybatis的二级缓存什么时候会清理缓存中的数据?
当作用域(一级缓存Session/二级缓存Namespaces)进行了新增、修改、删除操作后,默认该作用域下所有select中的缓存将被清空。
微服务
1.Spring Cloud 5大组件有哪些?
在早期,Spring Cloud的五大组件通常指的是:
• Eureka:服务注册中心。
• Ribbon:客户端负载均衡器。
• Feign:声明式的服务调用。
• Hystrix:服务熔断器。
• Zuul/Gateway:API网关。
随着Spring Cloud Alibaba的兴起,我们项目中也融入了一些阿里巴巴的技术组件:
• 服务注册与配置中心:Nacos。
• 负载均衡:Ribbon。
• 服务调用:Feign。
• 服务保护:Sentinel。
• API网关:Gateway。
2.服务注册和发现是什么意思?Spring Cloud 如何实现服务注册发现?
服务注册与发现主要包含三个核心功能:服务注册、服务发现和服务状态监控。
我们项目中采用了Eureka作为服务注册中心,它是Spring Cloud体系中的一个关键组件。
• 服务注册:服务提供者将自己的信息(如服务名称、IP、端口等)注册到Eureka。
• 服务发现:消费者从Eureka获取服务列表信息,并利用负载均衡算法选择一个服务进行调用。
• 服务监控:服务提供者定期向Eureka发送心跳以报告健康状态;如果Eureka在一定时间内未接收到心跳,将服务实例从注册中心剔除。
3.我看你之前也用过nacos,你能说下nacos与eureka的区别?
在使用Nacos作为注册中心的项目中,我注意到Nacos与Eureka的共同点和区别:
• 共同点:两者都支持服务注册与发现,以及心跳检测作为健康检查机制。
• 区别:
a. Nacos支持服务端主动检测服务提供者状态,而Eureka依赖客户端心跳。
b. Nacos区分临时实例和非临时实例,采用不同的健康检查策略。
c. Nacos支持服务列表变更的消息推送,使服务更新更及时。
d. Nacos集群默认采用AP模式,但在存在非临时实例时,会采用CP模式;而Eureka始终采用AP模式。
4.你们项目负载均衡如何实现的?
在服务调用过程中,我们使用Spring Cloud的Ribbon组件来实现客户端负载均衡。Feign客户端在底层已经集成了Ribbon,使得使用非常简便。
当发起远程调用时,Ribbon首先从注册中心获取服务地址列表,然后根据预设的路由策略选择一个服务实例进行调用,常用的策略是轮询。
5.Ribbon负载均衡策略有哪些?
Ribbon提供了多种负载均衡策略,包括:
• RoundRobinRule:简单的轮询策略。
• WeightedResponseTimeRule:根据响应时间加权选择服务器。
• RandomRule:随机选择服务器。
• ZoneAvoidanceRule:区域感知的负载均衡,优先选择同一区域中可用的服务器。
6.如果想自定义负载均衡策略如何实现?
自定义Ribbon负载均衡策略有两种方式:
-
创建一个类实现IRule接口,这将定义全局的负载均衡策略。
-
在客户端配置文件中指定特定服务调用的负载均衡策略,这将仅对该服务生效。
7.什么是服务雪崩,怎么解决这个问题?
服务雪崩是指一个服务的失败导致整个链路的服务相继失败。我们通常通过服务降级和服务熔断来解决这个问题:
• 服务降级:在请求量突增时,主动降低服务的级别,确保核心服务可用。
• 服务熔断:当服务调用失败率达到一定阈值时,熔断机制会启动,防止系统过载。
8.你们的微服务是怎么监控的?
我们项目中采用了SkyWalking进行微服务监控:
- SkyWalking能够监控接口、服务和物理实例的状态,帮助我们识别和优化慢服务。
- 我们还设置了告警规则,一旦检测到异常,系统会通过短信或邮件通知相关负责
9.你们项目中有没有做过限流?怎么做的?
在我们的项目中,由于面临可能的突发流量,我们采用了限流策略:
• 版本1:使用Nginx进行限流,通过漏桶算法控制请求处理速率,按照IP进行限流。
• 版本2:使用Spring Cloud Gateway的RequestRateLimiter过滤器进行限流,采用令牌桶算法,可以基于IP或路径进行限流。
10.限流常见的算法有哪些?
常见的限流算法包括:
• 漏桶算法:以固定速率处理请求,平滑突发流量。
• 令牌桶算法:按照一定速率生成令牌,请求在获得令牌后才被处理,适用于请求量有波动的场景。
11.什么是CAP理论?
CAP理论是分布式系统设计的基础理论,包含一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)。在网络分区发生时,系统只能在一致性和可用性之间选择其一。
12.为什么分布式系统中无法同时保证一致性和可用性?
在分布式系统中,为了保证分区容错性,我们通常需要在一致性和可用性之间做出选择。如果系统优先保证一致性,可能需要牺牲可用性,反之亦然。
13.什么是BASE理论?
BASE理论是分布式系统设计中对CAP理论中AP方案的延伸,强调通过基本可用、软状态和最终一致性来实现系统设计。
14.你们采用哪种分布式事务解决方案?
我们项目中使用了Seata的AT模式来解决分布式事务问题。AT模式通过记录业务数据的变更日志来保证事务的最终一致性。
15.分布式服务的接口幂等性如何设计?
我们通过Token和Redis来实现接口幂等性。用户操作时,系统生成一个Token并存储在Redis中,当用户提交操作时,系统会验证Token的存在性,并在验证通过后删除Token,确保每个Token只被处理一次。
16.xxl-job路由策略有哪些?
xxl-job支持多种路由策略,包括轮询、故障转移和分片广播等。
17.xxl-job任务执行失败怎么解决?
面对任务执行失败,我们可以:
-
选择故障转移路由策略,优先使用健康的实例执行任务。
-
设置任务重试次数。
-
通过日志记录和邮件告警通知相关负责人。
18.如果有大数据量的任务同时都需要执行,怎么解决?
我们可以通过部署多个实例并使用分片广播路由策略来分散任务负载。在任务执行代码中,根据分片信息和总数对任务进行分配。
消息中间件
1.RabbitMQ如何保证消息不丢失?
我们使用RabbitMQ来确保MySQL和Redis间数据双写的一致性,这要求我们实现消息的高可用性,具体措施包括:
-
开启生产者确认机制,确保消息能被送达队列,如有错误则记录日志并修复数据。
-
启用持久化功能,保证消息在未消费前不会在队列中丢失,需要对交换机、队列和消息本身都进行持久化。
-
对消费者开启自动确认机制,并设置重试次数。例如,我们设置了3次重试,若失败则将消息发送至异常交换机,由人工处理。
2.RabbitMQ消息的重复消费问题如何解决?
我们遇到过消息重复消费的问题,处理方法是:
• 设置消费者为自动确认模式,如果服务在确认前宕机,重启后可能会再次消费同一消息。
• 通过业务唯一标识检查数据库中数据是否存在,若不存在则处理消息,若存在则忽略,避免重复消费。
3.那你还知道其他的解决方案吗?
是的,这属于幂等性问题,可以通过以下方法解决:
• 使用Redis分布式锁或数据库锁来确保操作的幂等性。
4. RabbitMQ中死信交换机了解吗?(RabbitMQ延迟队列有了解过吗?)
了解。我们项目中使用RabbitMQ实现延迟队列,主要通过死信交换机和TTL(消息存活时间)来实现。
• 消息若超时未消费则变为死信,队列可绑定死信交换机,实现延迟功能。
• 另一种方法是安装RabbitMQ的死信插件,简化配置,在声明交换机时指定为死信交换机,并设置消息超时时间。
5.如果有100万消息堆积在MQ,如何解决?
若出现消息堆积,可采取以下措施:
-
提高消费者消费能力,如使用多线程。
-
增加消费者数量,采用工作队列模式,让多个消费者并行消费同一队列。
-
扩大队列容量,使用RabbitMQ的惰性队列,支持数百万条消息存储,直接存盘而非内存。
6.RabbitMQ的高可用机制了解吗?
我们项目在生产环境使用RabbitMQ集群,采用镜像队列模式,一主多从结构。
• 主节点处理所有操作并同步给从节点,若主节点宕机,从节点可接替为主节点,但需注意数据同步的完整性。
7.那出现丢数据怎么解决呢?
使用仲裁队列,主从模式,基于Raft协议实现强一致性数据同步,简化配置,提高数据安全性。
8. Kafka是如何保证消息不丢失?
Kafka保证消息不丢失的措施包括:
-
生产者使用异步回调发送消息,设置重试机制应对网络问题。
-
在Broker中通过复制机制,设置acks参数为all,确保消息在所有副本中都得到确认。
-
消费者手动提交消费成功的offset,避免自动提交可能导致的数据丢失或重复消费。
9. Kafka中消息的重复消费问题如何解决?
通过以下方法解决Kafka中的重复消费问题:
• 禁用自动提交offset,手动控制offset提交时机。
• 确保消息消费的幂等性,例如通过唯一主键或分布式锁。
10. Kafka是如何保证消费的顺序性?
Kafka默认不保证消息顺序性,但可以通过以下方法实现:
• 将消息存储在同一个分区,通过指定分区号或相同的业务key来实现。
11. Kafka的高可用机制了解吗?
Kafka的高可用性主要通过以下机制实现:
• 集群部署,多broker实例,单点故障不影响整体服务。
• 复制机制,每个分区有多个副本,leader和follower,leader故障时从follower中选举新leader。
12. 解释一下复制机制中的ISR?
ISR(In-Sync Replicas)指与leader保持同步的follower副本。
• 当leader故障时,优先从ISR中选举新leader,因为它们数据一致性更高。
13. Kafka数据清理机制了解吗?
Kafka的数据清理包括:
• 基于消息保留时间的清理。
• 基于topic数据大小的清理,可配置删除最旧消息。
14. Kafka中实现高性能的设计有了解过吗?
Kafka高性能设计包括:
• 消息分区,提升数据处理能力。
• 顺序读写,提高磁盘操作效率。
• 页缓存,减少磁盘访问。
• 零拷贝,减少数据拷贝和上下文切换。
• 消息压缩,减少IO负载。
• 分批发送,降低网络开销。
常见集合
1.ArrayList源码分析
ArrayList底层是用动态的数组实现的
• 初始容量
ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
• 扩容逻辑
ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组
• 添加逻辑
○ 确保数组已使用长度(size)加1之后足够存下下一个数据
○ 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
○ 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
○ 返回添加成功布尔值。
2.面试题-ArrayList list=new ArrayList(10)中的list扩容几次
该语句只是声明和实例了一个 ArrayList,指定了容量为 10,未扩容
3.面试题-如何实现数组和List之间的转换
参考回答:
• 数组转List ,使用JDK中java.util.Arrays工具类的asList方法
• List转数组,使用List的toArray方法。无参toArray方法返回 Object数组,传入初始化长度的数组对象,返回该对象数组
面试官再问:
1,用Arrays.asList转List后,如果修改了数组内容,list受影响吗
2,List用toArray转数组后,如果修改了List内容,数组受影响吗
数组转List受影响
List转数组不受影响
再答:
1,用Arrays.asList转List后,如果修改了数组内容,list受影响吗
Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址
2,List用toArray转数组后,如果修改了List内容,数组受影响吗
list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响
4.面试题-ArrayList和LinkedList的区别是什么?
• 底层数据结构
○ ArrayList 是动态数组的数据结构实现
○ LinkedList 是双向链表的数据结构实现
• 操作数据效率
○ ArrayList按照下标查询的时间复杂度O(1)【内存是连续的,根据寻址公式】, LinkedList不支持下标查询
○ 查找(未知索引): ArrayList需要遍历,链表也需要链表,时间复杂度都是O(n)
○ 新增和删除
▪ ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)
▪ LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)
• 内存空间占用
○ ArrayList底层是数组,内存连续,节省内存
○ LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
• 线程安全
○ ArrayList和LinkedList都不是线程安全的
○ 如果需要保证线程安全,有两种方案:
▪ 在方法内使用,局部变量则是线程安全的
▪ 使用线程安全的ArrayList和LinkedList
5. 面试题-说一下HashMap的实现原理?
HashMap的数据结构: 底层使用hash表数据结构,即数组和链表或红黑树
-
当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
-
存储时,如果出现hash值相同的key,此时有两种情况。
a. 如果key相同,则覆盖原始值;
b. 如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中
-
获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
面试官追问:HashMap的jdk1.7和jdk1.8有什么区别
• JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
• jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时并且数组长度达到64时,将链表转化为红黑树,以减少搜索时间。扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表
6.面试题-HashMap的put方法的具体流程
-
判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)
-
根据键值key计算hash值得到数组索引
-
判断table[i]==null,条件成立,直接新建节点添加
-
如果table[i]==null ,不成立
-
4.1 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
-
4.2 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对\7. 4.3 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操 作,遍历过程中若发现key已经存在直接覆盖value
-
插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。
7.面试题-讲一讲HashMap的扩容机制
• 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75)
• 每次扩容的时候,都是扩容之前容量的2倍;
• 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
○ 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
○ 如果是红黑树,走红黑树的添加
○ 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
8.面试题-hashMap的寻址算法
首先获取key的hashCode值,然后右移16位 异或运算 原来的hashCode值,主要作用就是使原来的hash值更加均匀,减少hash冲突
有了hash值之后,就很方便的去计算当前key的在数组中存储的下标,(n-1)&hash : 得到数组中的索引,代替取模,性能更好,数组长度必须是2的n次幂
9.为何HashMap的数组长度一定是2的次幂?
-
计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
-
扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
10.面试题-hashmap在1.7情况下的多线程死循环问题
在jdk1.7的hashmap中在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环
比如说,现在有两个线程
线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入
线程二:也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。
线程一:继续执行的时候就会出现死循环的问题。
线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,
所以B->A->B,形成循环。
当然,JDK 8 将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题。
11.面试题-HashSet与HashMap的区别
(1)HashSet实现了Set接口, 仅存储对象; HashMap实现了 Map接口, 存储的是键值对.
(2)HashSet底层其实是用HashMap实现存储的, HashSet封装了一系列HashMap的方法. 依靠HashMap来存储元素值,(利用hashMap的key键进行存储), 而value值默认为Object对象. 所以HashSet也不允许出现重复值, 判断标准和HashMap判断标准相同, 两个元素的hashCode相等并且通过equals()方法返回true.
12.面试题-HashTable与HashMap的区别
13.说一说Java提供的常见集合?
在java中提供了两大类的集合框架,主要分为两类:
第一个是Collection 属于单列集合,第二个是Map 属于双列集合
• 在Collection中有两个子接口List和Set。在我们平常开发的过程中用的比较多像list接口中的实现类ArrarList和LinkedList。 在Set接口中有实现类HashSet和TreeSet。
• 在map接口中有很多的实现类,平时比较常见的是HashMap、TreeMap,还有一个线程安全的map:ConcurrentHashMap
14.hashmap是线程安全的吗
不是线程安全的, 我们可以采用ConcurrentHashMap进行使用,它是一个线程安全的HashMap.
多线程
1.线程和进程的区别?
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。在 windows 中进程是不活动的,只是作为线程的容器
二者对比
• 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
• 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
• 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
2.并行和并发有什么区别?
现在都是多核CPU,在多核CPU下
并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程
3.创建线程的四种方式
在java中一共有四种常见的创建方式,分别是:继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程。通常情况下,我们项目中都会采用线程池的方式创建线程。
4.runnable 和 callable 有什么区别
-
Runnable 接口run方法没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
-
Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
-
Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
5.线程的 run()和 start()有什么区别?
start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
run(): 封装了要被线程执行的代码,可以被调用多次。
6.线程包括哪些状态,状态之间是如何变化的
在JDK中的Thread类中的枚举State里面定义了6中线程的状态分别是:新建、可运行、终结、阻塞、等待和有时限等待六种。
关于线程的状态切换情况比较多。我分别介绍一下
当一个线程对象被创建,但还未调用 start 方法时处于新建状态,调用了 start 方法,就会由新建进入可运行状态。如果线程内代码已经执行完毕,由可运行进入终结状态。当然这些是一个线程正常执行情况。
如果线程获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,只有当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
如果线程获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁等待状态,当其它持锁线程调用 notify() 或 notifyAll() 方法,会恢复为可运行状态
还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,不需要主动唤醒,超时时间到自然恢复为可运行状态
7.新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
可以这么做,在多线程中有多种方法让线程按特定顺序执行,可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
比如说:
使用join方法,T3调用T2,T2调用T1,这样就能确保T1就会先完成而T3最后完成
8.notify()和 notifyAll()有什么区别?
notifyAll:唤醒所有wait的线程
notify:只随机唤醒一个 wait 线程
9.在 java 中 wait 和 sleep 方法的不同?
共同点
• wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
• 方法归属不同
○ sleep(long) 是 Thread 的静态方法
○ 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
• 醒来时机不同
○ 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
○ wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
○ 它们都可以被打断唤醒
• 锁特性不同(重点)
○ wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
○ wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
○ 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
10.如何停止一个正在运行的线程?
有三种方式可以停止线程
• 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
• 使用stop方法强行终止(不推荐,方法已作废)
• 使用interrupt方法中断线程
11.讲一下synchronized关键字的底层原理?
synchronized 底层使用的JVM级别中的Monitor 来决定当前线程是否获得了锁,如果某一个线程获得了锁,在没有释放锁之前,其他线程是不能或得到锁的。synchronized 属于悲观锁。
synchronized 因为需要依赖于JVM级别的Monitor ,相对性能也比较低。
monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因
monitor内部维护了三个变量
• WaitSet:保存处于Waiting状态的线程
• EntryList:保存处于Blocked状态的线程
• Owner:持有锁的线程
只有一个线程获取到的标志就是在monitor中设置成功了Owner,一个monitor中只能有一个Owner
在上锁的过程中,如果有其他线程也来抢锁,则进入EntryList 进行阻塞,当获得锁的线程执行完了,释放了锁,就会唤醒EntryList 中等待的线程竞争锁,竞争的时候是非公平的。
12.synchronized关键字的底层原理-进阶
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
一旦锁发生了竞争,都会升级为重量级锁
13.你谈谈 JMM(Java 内存模型)
Java内存模型是Java虚拟机规范中定义的一种非常重要的内存模型。它的主要作用是描述Java程序中线程共享变量的访问规则,以及这些变量在JVM中是如何被存储和读取的,涉及到一些底层的细节。
这个模型有几个核心的特点。首先,所有的共享变量,包括实例变量和类变量,都被存储在主内存中,也就是计算机的RAM。需要注意的是,局部变量并不包含在内,因为它们是线程私有的,所以不存在竞争问题。
其次,每个线程都有自己的工作内存,这里保留了线程所使用的变量的工作副本。这意味着,线程对变量的所有操作,无论是读还是写,都必须在自己的工作内存中完成,而不能直接读写主内存中的变量。
最后,不同线程之间不能直接访问对方工作内存中的变量。如果线程间需要传递变量的值,那么这个过程必须通过主内存来完成。
14.CAS 你知道吗?
CAS的全称是: Compare And Swap(比较再交换);它体现的一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性。
• CAS使用到的地方很多:AQS框架、AtomicXXX类
• 在操作共享变量的时候使用的自旋锁,效率上更高一些
CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现
15.请谈谈你对 volatile 的理解
volatile 是一个关键字,可以修饰类的成员变量、类的静态成员变量,主要有两个功能
第一:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
第二: 禁止进行指令重排序,可以保证代码执行有序性。底层实现原理是,添加了一个内存屏障,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化
16.什么是AQS?
AQS的话,其实就一个jdk提供的类AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架。
内部有一个属性 state 属性来表示资源的状态,默认state等于0,表示没有获取锁,state等于1的时候才标明获取到了锁。通过cas 机制设置 state 状态
在它的内部还提供了基于 FIFO 的等待队列,是一个双向列表,其中
• tail 指向队列最后一个元素
• head 指向队列中最久的一个元素
其中我们刚刚聊的ReentrantLock底层的实现就是一个AQS。
17.ReentrantLock的实现原理
ReentrantLock是一个可重入锁:,调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞,内部直接增加重入次数 就行了,标识这个线程已经重复获取一把锁而不需要等待锁的释放。
ReentrantLock是属于juc报下的类,属于api层面的锁,跟synchronized一样,都是悲观锁。通过lock()用来获取锁,unlock()释放锁。
它的底层实现原理主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高。
18.synchronized和Lock有什么区别 ?
第一,语法层面
• synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现,退出同步代码块锁会自动释放
• Lock 是接口,源码由 jdk 提供,用 java 语言实现,需要手动调用 unlock 方法释放锁
第二,功能层面
• 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
• Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量,同时Lock 可以实现不同的场景,如 ReentrantLock, ReentrantReadWriteLock
第三,性能层面
• 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
• 在竞争激烈时,Lock 的实现通常会提供更好的性能
统合来看,需要根据不同的场景来选择不同的锁的使用。
19.死锁产生的条件是什么?
嗯,是这样的,一个线程需要同时获取多把锁,这时就容易发生死锁,举个例子来说:
t1 线程获得A对象锁,接下来想获取B对象的锁
t2 线程获得B对象锁,接下来想获取A对象的锁
这个时候t1线程和t2线程都在互相等待对方的锁,就产生了死锁
20.如何进行死锁诊断?
我们只需要通过jdk自动的工具就能搞定
我们可以先通过jps来查看当前java程序运行的进程id
然后通过jstack来查看这个进程id,就能展示出来死锁的问题,并且,可以定位代码的具体行号范围,我们再去找到对应的代码进行排查就行了。
21.ConcurrentHashMap
ConcurrentHashMap 是一种线程安全的高效Map集合,jdk1.7和1.8也做了很多调整。
• JDK1.7的底层采用是分段的数组+链表 实现
• JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
在jdk1.7中 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一 种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构 的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修 改时,必须首先获得对应的 Segment的锁。
Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元 素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁
在jdk1.8中的ConcurrentHashMap 做了较大的优化,性能提升了不少。首先是它的数据结构与jdk1.8的hashMap数据结构完全一致。其次是放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保 证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲 突,就不会产生并发 , 效率得到提升
22.导致并发程序出现问题的根本原因是什么
Java并发编程有三大核心特性,分别是原子性、可见性和有序性。
首先,原子性指的是一个线程在CPU中的操作是不可暂停也不可中断的,要么执行完成,要么不执行。比如,一些简单的操作如赋值可能是原子的,但复合操作如自增就不是原子的。为了保证原子性,我们可以使用synchronized关键字或JUC里面的Lock来进行加锁。
其次,可见性是指让一个线程对共享变量的修改对另一个线程可见。由于线程可能在自己的工作内存中缓存共享变量的副本,因此一个线程对共享变量的修改可能不会立即反映在其他线程的工作内存中。为了解决这个问题,我们可以使用synchronized关键字、volatile关键字或Lock来确保可见性。
最后,有序性是指处理器为了提高程序运行效率,可能会对输入代码进行优化,导致程序中各个语句的执行先后顺序与代码中的顺序不一致。虽然处理器会保证程序最终执行结果与代码顺序执行的结果一致,但在某些情况下我们可能需要确保特定的执行顺序。为了解决这个问题,我们可以使用volatile关键字来禁止指令重排。
23.说一下线程池的核心参数(线程池的执行原理知道嘛)
在线程池中一共有7个核心参数:
-
corePoolSize 核心线程数目 - 池中会保留的最多线程数
-
maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
-
keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
-
unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
-
workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
-
threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
-
handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
拒绝策略有4种,当线程数过多以后,第一种是抛异常、第二种是由调用者执行任务、第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。
24.线程池中有哪些常见的阻塞队列
Jdk中提供了很多阻塞队列,开发中常见的有两个:ArrayBlockingQueue和LinkedBlockingQueue
ArrayBlockingQueue和LinkedBlockingQueue是Java中两种常见的阻塞队列,它们在实现和使用上有一些关键的区别。
首先,ArrayBlockingQueue是一个有界队列,它在创建时必须指定容量,并且这个容量不能改变。而LinkedBlockingQueue默认是无界的,但也可以在创建时指定最大容量,使其变为有界队列。
其次,它们在内部数据结构上也有所不同。ArrayBlockingQueue是基于数组实现的,而LinkedBlockingQueue则是基于链表实现的。这意味着ArrayBlockingQueue在访问元素时可能会更快,因为它可以直接通过索引访问数组中的元素。而LinkedBlockingQueue则在添加和删除元素时可能更快,因为它不需要移动其他元素来填充空间。
另外,它们在加锁机制上也有所不同。ArrayBlockingQueue使用一把锁来控制对队列的访问,这意味着读写操作都是互斥的。而LinkedBlockingQueue则使用两把锁,一把用于控制读操作,另一把用于控制写操作,这样可以提高并发性能。
25.如何确定核心线程数
① 高并发、任务执行时间短 –>( CPU核数+1 ),减少线程上下文的切换
② 并发不高、任务执行时间长
• IO密集型的任务 –> (CPU核数 * 2 + 1)
• 计算密集型任务 –> ( CPU核数+1 )
③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)
26.线程池的种类有哪些
在jdk中默认提供了4中方式创建线程池
第一个是:newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回 收空闲线程,若无可回收,则新建线程。
第二个是:newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列 中等待。
第三个是:newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
第四个是:newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
27.为什么不建议用Executors创建线程池
主要原因是如果使用Executors创建线程池的话,它允许的请求队列默认长度是Integer.MAX_VALUE,这样的话,有可能导致堆积大量的请求,从而导致OOM(内存溢出)。
所以,我们一般推荐使用ThreadPoolExecutor来创建线程池,这样可以明确规定线程池的参数,避免资源的耗尽。
28.线程池使用场景CountDownLatch、Future(你们项目哪里用到了多线程)
参考场景一:
es数据批量导入
在我们项目上线之前,我们需要把数据量的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右,一次性读取数据肯定不行(oom异常),如果分批执行的话,耗时也太久了。所以,当时我就想到可以使用线程池的方式导入,利用CountDownLatch+Future来控制,就能大大提升导入的时间。
参考场景二:
在我做那个xx电商网站的时候,里面有一个数据汇总的功能,在用户下单之后需要查询订单信息,也需要获得订单中的商品详细信息(可能是多个),还需要查看物流发货信息。因为它们三个对应的分别三个微服务,如果一个一个的操作的话,互相等待的时间比较长。所以,我当时就想到可以使用线程池,让多个线程同时处理,最终再汇总结果就可以了,当然里面需要用到Future来获取每个线程执行之后的结果才行
参考场景三:
《黑马头条》项目中使用的
我当时做了一个文章搜索的功能,用户输入关键字要搜索文章,同时需要保存用户的搜索记录(搜索历史),这块我设计的时候,为了不影响用户的正常搜索,我们采用的异步的方式进行保存的,为了提升性能,我们加入了线程池,也就说在调用异步方法的时候,直接从线程池中获取线程使用
29.如何控制某个方法允许并发访问线程的数量?
在jdk中提供了一个Semaphore[seməfɔːr]类(信号量)
它提供了两个方法,semaphore.acquire() 请求信号量,可以限制线程的个数,是一个正数,如果信号量是-1,就代表已经用完了信号量,其他线程需要阻塞了
第二个方法是semaphore.release(),代表是释放一个信号量,此时信号量的个数+1
30.谈谈你对ThreadLocal的理解
ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现了线程内的资源共享
31.那你知道ThreadLocal的底层原理实现吗?
在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
32.那关于ThreadLocal会导致内存溢出这个事情,了解吗?
嗯,我之前看过源码,我想一下~~
是因为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释放key,不过关键的是只有key可以得到内存释放,而value不会,因为value是一个强引用。
在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。