ShanTianQi

Java 八股文

Java八股文


第一章 Redis


20230707214627

1.1redis使用场景——穿透


缓存穿透:查询一个不存在的数据,mysql查询不到的数据也不会写入缓存,就会导致每次请求都查询数据库

**解决方案一:**缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存

优点:简单

缺点:消耗内存,可能会发生不一致的问题

**解决方案二:**布隆过滤器

20230707215302

布隆过滤器

bitmap(位图):相当于是一个以(bit)位为单位的数组,数组中每个单元只能存储二进制数0或1

20230707215625

误判现象

20230707215734

布隆过滤器的实现方案:Redisson和Guava

优点:内存占用较少,没有多余key

缺点:实现复杂,存在误判


1.2redis使用场景——击穿


**缓存击穿:**给某一个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发请求可能会瞬间把DB压垮

20230707220432

解决方案一:互斥锁

20230707220622

分布式锁

强一致、性能差

解决方案二:逻辑过期

20230707220800

高可用、性能优、不能保证数据的绝对一致


1.3redis使用场景——雪崩


缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力

20230707221156

解决方案:


1.4redis使用场景——双写一致性


**双写一致性:**当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致

20230707221836 20230707222216

先删除缓存或者先修改数据库都会出现脏数据,而延时双删可以有效避免脏数据的产生

20230707222247

解决办法:

一、分布式锁

20230707222545

性能比较低

二、前提:存入缓存的数据大部分是读多写少

20230707222816

强一致、但性能低

三、异步通知保证数据的最终一致性

20230707222959

基于Canal的异步通知:

20230707223028

二进制日志(BINLOG)记录了所有的DDL(数据定义语言)语句和DML(数据操纵语言)语句,但不包括数据查询(SELECT/SHOW)语句


1.5redis使用场景——持久化


在Redis中提供了两种数据持久化的方式:RDB和AOF

一、RDB

RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照,简单来说就是把内存中的数据都记录到磁盘中,当Redis实例故障重启后,从磁盘读取快照文件,恢复数据

20230708200847

Redis内部有触发RDB的机制,可以在redis.conf文件中找到

save 900 1    #900秒内,如果至少有1个key被修改,则执行bgsave
save 300 10
save 60 10000

RDB的执行原理

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入RDB文件

fork采用的是copy-on-write技术:

20230708201552

AOF

AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件

20230708202354

AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF

#是否开启AOF功能,默认是no
appendonly yes
#AOF文件的名称
appendfilename "appendonly.aof"

AOF的命令记录频率也可以通过redis.conf来配置

#表示每执行一次写命令,立即记录到AOF文件
appendfsync always
#写命令执行完先放入AOF缓存区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
#写命令执行完先放入AOF缓存区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no

20230708202906

因为是记录命令,AOF文件会比RDB文件大很多。而且AOF会记录对同一个Key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果

20230708203136

Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置

#AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
#AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb

对比

RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用

20230708203426

1.6redis使用场景——数据过期策略


Redis对数据设置数据的有效时间,数据过期后,就需要将数据从内存中删除掉。可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)

一、惰性删除

设置该key过期时间后,我们不去管它,当需要该key时,我们再检查其是否过期,如果过期,我们就删掉它,反之返回该key

优点:对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查

缺点:对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放

二、定期删除

每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中过期的key)

有两种模式:

优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响,另外定期删除,也能有效释放过期键占用的内存

缺点:难以确定删除操作执行的时长和频率

Redis的过期删除策略:惰性删除+定期删除两种策略进行配合使用


1.7redis使用场景——数据淘汰策略


当Redis中的内存不够用时,此时再向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略

Redis支持8种不同策略来选择要删除的key:

20230708215447

建议:


1.8redis分布式锁


一、使用场景

抢卷场景

20230708220456

正常执行过程

20230708220524

异常模式

20230708220555

解决(加锁,本地互斥锁,单台服务器):

20230708220650

分布式就会出问题

20230708220808

解决:加分布式锁

20230708220919

二、实现原理(setnx、redisson)

Redis实现分布式锁主要利用Redis的setnx命令,setnx是SET if not exists(如果不存在,则SET)的简写

获取锁:

#添加锁,NX是互斥,EX是设置超时时间
SET lock value NX EX 10

释放锁:

#释放锁,删除即可
DEL key
20230708221825

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

redisson

20230708222313

redisson实现的分布式锁,底层是setnx和lua脚本(保证原子性)

Redis缓存中的重试机制,当while循环一定次数时,还没有拿到锁,则会停止

在redisson的分布式锁中,提供了一个WatchDog,一个线程获取锁成功后,WatchDog会给持有锁的线程续期(默认是每隔10s续期一次)

20230708222541

redisson——可重入

20230708222728

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

redisson——主从一致性

20230708222945

当一个线程访问主节点时,主节点获取锁后突然宕机,此时会从从节点中选择一个作为主节点,以后的线程可以访问这个节点,这个新的主节点也会获取锁,此时,两个线程同时持有一把锁,会产生脏数据

解决方法:

RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁**(n / 2 + 1)**,避免在一个redis实例上加锁

例:有三个redis节点,3 / 2 + 1 = 2.5, 加锁的redis实例应该>=2(超过一半)

20230708223506

Redisson锁能解决主从数据一致的问题吗?

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


1.9redis集群方案


一、主从复制

单节点的Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离

20230708225202

主从全量同步

20230708225530

1、从节点请求主节点同步数据(replication id、offset)

2、主节点判断是否是第一次请求,是第一次就与从节点同步版本信息(replication id和offset)

3、主节点执行bgsave,生成rdb文件后,发送给从节点去执行

4、在rdb生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件)

5、把生成之后的命令日志文件发送给从节点进行同步

主从增量同步 (slave重启或后期数据变化)

20230708225741

1、从节点请求主节点同步数据,主节点判断是不是第一次请求,不是第一次请求就获取从节点的offset值

2、主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步

二、哨兵模式

Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构作用如下:

20230708230504

服务状态监控

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:

哨兵选主规则

20230708232922

redis集群(哨兵模式)脑裂

20230708233052

脑裂:出现多个master

20230708233153

当网络恢复后,原master会被降级为slave,这时再从新master同步数据,就会导致数据丢失

解决方案:

redis中有两个配置参数

min-replicas-to-write 1 表示最少的salve节点为1个

min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒

我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失

三、分片集群

主从和哨兵可以解决高并发读、高可用的问题,但是依然有两个问题没有解决:

使用分片集群可以解决以上问题,分片集群特征:

20230708234355

Redis分片集群数据的存储和读取

Redis分片集群引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽

20230708234901

1.10redis单线程——为什么还那么快?


多路复用模型

Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,I/O多路复用模型主要就是实现了高效的网络请求

用户空间和内核空间

Linux系统中的一个进程使用的内存情况划分两部分:内核空间、用户空间

用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问

内核空间可以执行特权命令(Ring0),调用一切系统资源

20230709121701

Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:

20230709122109 20230709122340 20230709122506

IO多路复用

是利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前I/O多路复用采用的都是epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。

不过监听Socket的方式、通知的方式又有多种实现,常见的有:

差异:

Redis网络模型

Redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装,提供了统一的高性能事件库

20230709124731

就是使用I/O多路复用结合事件的处理器来应对多个Socket的请求


第二章 MySQL


2.1优化——定位慢查询


表象:页面加载过慢、接口测试响应时间过长(超过1s)

方案一:开源工具

方案二:MySQL自带慢日志

慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志,如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:

#开启MySQL慢日志开关
slow_query_log=1
#设置慢日志的时间为2s,SQL语句执行时间超过2s,就会视为慢查询,记录慢查询日志
long_query_time=2

配置完毕后,通过以下指令重新启动MySQL服务器进行测试,查看慢日志中记录的信息


2.2优化——优化慢查询


SQL执行计划(找到慢的原因)

可以采用EXPLAIN或者DESC命令获取MySQL如何执行SELECT语句的信息

20230709202647

1、possible_keys 当前sql可能会使用到的索引

2、key 当前sql实际命中的索引

3、key_len 索引占用的大小

通过2和3查看是否可能会命中索引

4、Extra 额外的优化建议

20230709203453

5、type这条sql的连接的类型,性能由好到差为NULL,system,const,eq_ref,ref,range,index,all

当前type为index或all时,这条sql就需要进行优化了


2.3优化——索引


索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构(B+树),这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。

一、B+树

20230709211424

时间复杂度:

B-Tree,B树是一种多叉路平衡查找树,相对于二叉树,B树每个节点可以有多个分支,即多叉。

以一颗最大度数(max-degree)为5(5阶)的b-tree为例,那么这个B树每个节点最多存储4个key

20230709211904

B+Tree是在BTree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构

20230709212157

B树与B+树对比:

问题一:什么是索引

问题二:索引的底层数据结构

MySQL的InnoDB引擎采用的是B+树的数据结构来存储索引

二、聚簇索引和非聚簇索引

分类含义特点
聚集索引(Clustered Index)将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据必须有,而且只由一个
二级索引(非聚集索引Secondary Index)将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键可以存在多个

聚集索引选取规则:

20230709223919

回表查询

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

三、覆盖索引、超大分页优化

覆盖索引是指查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到。(不需要回表查询)

20230709225457

MySQL超大分页

在数据量比较大时,如果进行limit分页查询,在查询时,越往后,分页查询效率越低

20230709230234

因为,在分页查询时,如果执行limit 9000000,10,此时需要MySQL排序前9000010记录,仅仅返回9000000-9000010的记录,其他记录丢弃,查询排序的代价非常大。

优化思路:一般分页查询时,通过创建覆盖索引能够比较好地提升性能,可以通过覆盖索引加子查询形式进行优化

select *
from tb_sku t,
	(select id from tb_sku order by id limit 9000000,10)a
where t.id = a.id;

MySQL超大分页

在数据量比较大时,limit分页查询,需要对数据进行排序,效率低

解决方案:覆盖索引 + 子查询

索引创建的原则

1、针对于数据量较大,且查询比较频繁的表建立索引。单表超过10万数据(增加用户体验) 重要

2、针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引 重要

3、尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高

4、如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引

5、尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率 重要

6、要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价就越大,会影响增删改的效率 重要

7、如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它,当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询

索引会失效的情况

1、违反最左前缀法则

如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列,匹配最左前缀法则,走索引

2、范围查询右边的列,不能使用索引

3、不要在索引列上进行运算操作,索引将失效

4、字符串不加单引号,造成索引失效,MySQL的查询优化器,会自动的进行类型转换,造成索引失效

5、以%开头的Like模糊查询,索引失效。如果仅仅是尾部模糊查询,索引不会失效,如果是头部模糊匹配,索引失效

谈谈对sql优化的经验

表的设计优化(参考阿里开发手册《嵩山版》)

SQL语句优化

主从复制、读写分离

如果数据库的使用场景读的操作比较多的时候,为了避免写的操作所造成的性能影响,可以采用读写分离的架构。读写分离解决的是,数据库的写入,影响了查询的效率

20230710200058

2.4优化——其他面试题


一、事务相关

事务的特性 ACID

事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败

并发事务、隔离级别

并发事务问题:脏读、不可重复读、幻读

解决办法:

隔离级别:读未提交、读已提交、可重复读、串行化

20230710201550 20230710201812

x:可以解决

√:不可以解决

注意:事务隔离级别越高,数据越安全,但是性能越低

undo log和redo log的区别

redo log

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性

该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。

20230710203246

undo log

回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚和MVCC(多版本并发控制)。uodo log和redo log记录物理日志不一样,它是逻辑日志

undo log可以实现事务的一致性和原子性

区别:

事务中的隔离性是如何保证的?

锁:排他锁(如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁)

MVCC

多版本并发控制

全称Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个多个版本,使得读写操作没有冲突

MVCC的具体实现,主要依赖于数据库记录中的隐藏字段,undo log日志,readView

20230710225613

隐藏字段

20230710225822

undo log

回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志

当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除

而update、delete的时候,产生的undo log日志不仅在回滚时需要,mvcc版本访问也需要,不会立即被删除

undo log版本链

20230710230623

不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链表,链表的头部是最新的旧纪录,链表尾部是最早的旧纪录

readview

ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁,对于我们日常的操作,如:select…lock in share mode(共享锁), select … for update、update、insert、delete(排他锁)都是一种当前读

简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读

20230710231443

20230710231529

不同的隔离级别,生成ReadView的时机不同:

20230710232152

二、主从同步原理

MySQL主从复制的核心就是二进制日志

二进制日志(BINLOG)记录了所有的DDL(数据定义语言)语句和DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句

20230710232431

MySQL主从复制的核心就是二进制日志binlog(DDL和DML)

三、分库分表

分库分表时机:

20230710232955

垂直分库

以表为依据,根据业务将不同表拆分到不同库中

20230710233135

垂直分表

以字段为依据,根据字段属性将不同字段拆分到不同表中

拆分规则:

20230710233411

水平分库

将一个库的数据拆分到多个库中

路由规则:

20230710233618

水平分表

将一个表的数据拆分到多个表中(可以在同一个库内)

20230710233750

策略

20230710233953 20230710234028

第三章 框架


3.1Spring


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

@Service
@Scope("singleton")
public class UserServiceIpml implements UserService{

}

20230711201458

解答:

不是线程安全的

Spring框架中有一个@Scope注解,默认的值就是singleton,单例的

因为一般在Spring的bean中都是驻入无状态的对象,没有线程安全问题,如果在bean中定义了可修改的成员变量,是要考虑线程安全问题的,可以使用多例或者加锁来解决

二、AOP

AOP称为面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为”切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

常见的AOP使用场景:

记录操作日志思路

20230711202310 20230711203138

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

Spring支持编程式事务管理和声明式事务管理两种方式

20230711203602

事务失效的场景

20230711204036 20230711204203 20230711204311

三、bean的声明周期

BeanDefinition: bean的定义信息

Spring容器在进行实例化时,会将xml配置的<bean>的信息封装成一个BeanDefinition对象,Spring根据BeanDefinition来创建Bean对象,里面有很多的属性用来描述Bean

20230711205453

1、通过BeanDefinition获取bean的定义信息

2、调用构造函数实例化bean

3、bean的依赖注入

4、处理Aware接口(BeanNameAware、BeanFactoryAware、ApplicationContextAware)

5、Bean的后置处理器BeanPostProcessor-前置

6、初始化方法(InitializingBean、init-method)

7、Bean的后置处理器BeanPostProcessor-后置

8、销毁bean

四、Spring的循环引用

20230711210349

循环依赖

20230711210450

Spring解决循环依赖是通过三级缓存,对应的三级缓存如下:

20230711210657

一级缓存作用:限制bean在beanFactory中只存一份,即实现singleton scope,解决不了循环依赖

如果想打破循环依赖,就需要一个中间人的参与,这个中间人就是二级缓存

20230711210935

二级缓存可以解决一般对象的循环依赖问题,但是不能解决代理对象的循环依赖问题

20230711211216

构造方法出现循环依赖问题怎么解决?

20230711211319

问题答案:

1、一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象

2、二级缓存:缓存早期的bean对象(生命周期还没走完)

3、三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

构造方法出现循环依赖问题怎么解决?


3.2SpringMVC


一、SpringMVC的执行流程

视图阶段(老旧JSP等)

20230711221516

前后端分离阶段(接口开发,异步)

20230711221644

3.3SpringBoot


一、自动配置原理

20230711223035

20230711223356

20230711223524

1、在SpringBoot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:

2、其中**@EnableAutoConfiguration是实现自动化配置的核心注解,该注解通过@Import注解导入对应的配置选择器,内部就是读取了该项目和该项目引用的jar包的classpath路径下的META-INF/spring.factories文件中的所配置的类的全类名。在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定**是否需要将其导入到Spring容器中

3、条件判断会有像**@ConditionalOnClass**这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用

二、Spring框架常见注解

Spring中:

20230711224503

SpringMVC中:

20230711224604

SpringBoot中:

20230711224717

3.4Mybatis


一、Mybatis执行流程

20230711225108

1、读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件

2、构造会话工厂SqlSessionFactory

3、会话工厂创建SqlSession对象(包含了执行SQL语句的所有方法)

4、操作数据库的接口,Executor执行器,同时负责查询缓存的维护

5、Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息

6、输入参数映射

7、输出结果映射

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

支持,默认不开启

20230711225637

**立即加载:**查询用户时候,把用户所属订单数据也查询出来

**延迟加载:**查询用户时候,暂时不查询订单数据,当需要时,再去查询订单

某个映射文件设置懒加载:fetchType = “lazy”

20230711230006

全局设置延迟加载(在mybatis配置文件中设置):lazyLoadingEnabled true

20230711230230

延迟加载底层原理

1、使用CGLIB创建目标对象的代理对象

2、当调用目标方法时,进入拦截器invoke方法,发现目标方法是null值,执行sql查询

3、获取数据后,调用set方法设置属性值,再继续查询目标方法,就有值了

20230711230433

三、Mybatis的一级、二级缓存

本地缓存,基于PerpetualCache,本质是一个HashMap

**一级缓存:**作用域是session(sqlSession)级别

基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存

20230711231426 20230711231545

**二级缓存:**作用域是namespace和mapper的作用域,不依赖于session

基于namespace和mapper的作用域起作用的,不是依赖于SQL session,默认也是采用PerpetualCache,HashMap存储,需要单独开启,一个是核心配置,一个是mapper映射文件。

二级缓存默认是关闭的

开启:

1、全局配置文件

<settings>
	<setting name="cacheEnabled" value="true"/>
</settings>

2、映射文件

使用<cache/>标签让当前mapper生效二级缓存

注意事项:

二级缓存扫描时候会清理缓存中的数据 答案在第一条


第四章 微服务&消息中间件


SpringCloud相关问题

一、SpringCloud5大组件

20230730191019 20230730191344

4.1服务注册


一、服务注册和服务发现是什么意思?SpringCloud如何实现服务注册发现的?

**服务注册:**服务提供者需要把自己的信息注册到Eureka,由Eureka来保存这些信息,比如服务名称,ip,端口等等

**服务发现:**消费者向Eureka拉取服务列表信息,如果服务提供者有集群,则消费者会利用负载均衡算法,选择一个发起调用

**服务监控:**服务提供者会每隔30s向Eureka发送心跳,报告健康状态,如果Eureka服务90s没接收到心跳,从Eureka中剔除

二、Nacos和Eureka的区别

20230730192507 20230730193152

4.2负载均衡


一、Ribbon负载均衡的流程

20230730193632

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

20230730193948

三、自定义负载均衡策略怎么实现?

可以自己创建类实现IRule接口,然后再通过配置类或者配置文件配置即可,通过定义IRule实现可以修改负载均衡规则,有两种方式:

20230730194240 20230730194310

4.3熔断、降级


一、什么是服务雪崩,怎么解决?

20230730194442

预防:限流

解决:Hystix 服务熔断降级[hɪst’rɪks]

服务降级

服务降级是服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃

20230730194836 20230730194945

服务熔断

Hystrix熔断机制,用于监控微服务调用情况,默认是关闭的,如果需要开启需要在引导类上添加注解:@EnableCircuitBreaker,如果检测到10秒内请求的失败率超过50%,就触发熔断机制,之后每隔5秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。如果微服务可达,则关闭熔断机制,恢复正常请求。

20230730195355 20230730195420

4.4监控


一、为什么需要监控

20230730211807

skywalking

一个分布式系统的应用程序性监控工具(Application Performance Managment),提供了完善的链路追踪能力,apache的的顶级项目(前华为产品经理吴晟主导开源)

20230730212206 20230730212815

业务问题


4.5限流


为什么要限流?

1、并发的确大(突发流量)

2、防止用于恶意刷接口

20230801205128

限流的实现方式:

一、Tomcat:可以设置最大连接数(单项目推荐,分布式不推荐)

20230801205318

二、Nginx:漏桶算法(了解原理即可)

1、控制速率(突发流量)

20230801205614 20230801205528

2、控制并发连接数

20230801210101

三、网关:令牌桶算法

yml配置文件中,微服务路由设置添加局部过滤器RequestRateLimiter

20230801214042 20230801214142

四、自定义拦截器

20230801214509

4.6分布式系统理论


一、解释一下CAP定理和BASE理论

CAP

1998年,加州大学的计算机科学家Eric Brewer提出,分布式系统有三个指标:

20230802205039

Eric Brewer说,分布式系统无法同时满足这三个指标。这个结论就叫做CAP定理

1、Consistency(一致性)

用户访问分布式系统中的任意节点,得到的数据必须一致

20230802205503

2、Availability(可用性)

用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝

20230802205604

3、Partiton tolerance(分区容错性)

Partiton (分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区

tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务

20230802205847

结论:

BASE

BASE理论是对CAP的一种解决思路,包括三个思想:

20230802211205 20230802211227

4.7分布式事务


一、分布式事务解决方案

1、Seata框架(XA、AT、TCC)

Seata事务管理中有三个重要的角色

20230802213456

XA模式

20230802213618

AT模式

20230802213738

TCC模式

20230802213923

2、MQ

20230802214023 20230802214052

4.8分布式服务接口幂等


幂等:多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致

需要幂等场景

接口幂等

基于RESTful API的角度对部分常见类型请求的幂等性特点进行分析

20230802220035

解决方案

token + redis

20230802220406

分布式锁

20230802220451 20230802220525

4.9分布式任务调度


xxl-job

xxl-job解决的问题:

**一、xxl-job路由策略有哪些?**记住三个即可

20230802220929

二、xxl-job任务执行失败怎么解决?

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

三、如果有大数据量的任务同时都需要执行,怎么解决?

执行器集群部署时,任务路由策略选择分片广播情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务

20230802223502 20230802223524

4.10RabbitMQ


一、RabbitMQ如何保证消息不丢失

使用场景:

20230806232325

生产者确认机制

RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失,消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。

20230806233138

消息失败之后如何处理?

消息持久化

MQ默认是内存存储消息,开启持久化功能可以确保缓存在MQ中的消息不丢失。

1、交换机持久化

20230806233517

2、队列持久化

20230806233524

3、消息持久化,SpringAMQP中的消息默认是持久的,可以通过MessageProperties中的DeliveryMode来指定

20230806233532

消费者确认

RabbitMQ支持消费者确认机制,即:消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息。而SpringAMQP则允许配置三种确认模式:

20230806234141 20230806234202

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

产生原因:

20230806234406

解决方案:

三、RabbitMQ中死信交换机(RabbitMQ延迟队列有了解过么)

延迟队列 = 死信交换机 + TTL(生存时间)

①死信交换机

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter)

如果该队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)

20230806235236 20230806235248

②TTL

TTL,也就是Time-To-Live,如果一个队列中的消息TTL结束仍未消费,则会变为死信,ttl超时分为两种情况:

20230806235530 20230806235541

延迟队列插件

DelayExchange插件,需要安装在RabbitMQ中

DelayExchange的本质还是官方的三种交换机,只是添加了延迟功能,因此使用时只需要声明一个交换机,交换机的类型可以是任意类型,然后设定delayed属性为true即可

20230806235809 20230806235824 20230806235854

四、RabbitMQ如果有100万消息堆积在MQ,如何解决(消息堆积怎么解决)

当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是消息堆积问题

解决思路:

惰性队列

惰性队列的特征如下:

20230807000259 20230807000356

五、RabbitMQ的高可用机制

1、普通集群

普通集群,或者叫标准集群(class cluster),具备下列特征

20230807000652

2、镜像集群

镜像集群:本质是主从模式,具备下面的特征:

20230807001133

3、仲裁队列

仲裁队列:仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列,具备下列特征:

20230807001328 20230807001354

4.11Kafka


一、Kafka是如何保证消息不丢失

使用Kafka在消息的收发过程都会出现消息丢失,Kafka分别给出了解决方案

20230821195643

1、生产者发送消息到Brocker丢失

20230824140334 20230824140422

2、消息在Brocker中存储丢失

20230824140539 20230824140529

3、消费者从Brocker接收消息丢失

20230824140641 20230824141634

重平衡:当消费者组内某一个消费者宕机了,就会把实例分配给组内其它消费者,这个重新分配的过程就是重平衡

20230824141806 20230824141829

try代码块里设置异步提交,在finally里设置同步提交

20230824142118

二、Kafka是如何保证消费的顺序性

应用场景:

20230824143355

topic分区中消息只能由消费者组中的唯一一个消费者处理,所以消息肯定是按照先后顺序进行处理的、但是它也仅仅是保证Topic的一个分区顺序处理,不能保证跨分区的消息先后处理顺序。所以,如果想要顺序的处理Topic的所有消息,难就只提供一个分区。

20230824144443 20230824144512

三、Kafka的高可用机制

1、集群模式

20230824144848

2、分区备份机制

20230824145204 20230824151212

ISR(in-sync replica)需要同步复制保存的follower

如果leader失效后,需要选出新的leader,选举的原则如下:

第一:选举时优先从ISR中选定,因为这个列表中的follower的数据是与leader同步的

第二:如果ISR列表中的follower都不行了,就只能从其他follower中选取

20230824151445

四、Kafka数据清理机制

1、Kafka文件存储机制

20230824151853

2、数据清理机制

日志的清理策略有两个

1、根据消息的保留时间,当消息在Kafka中保存的时间超过了指定的时间,就会触发清理过程

20230824152005

默认168个小时,七天

2、根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阈值,则开始删除最久的消息。需手动开启

20230824152129 20230824152155

五、Kafka中实现高性能的设计

零拷贝

以前:需要拷贝四次

20230824152832

零拷贝:需要拷贝两次

20230824152901 20230824152938

第五章 集合


20230712215405


5.1数据结构


算法复杂度分析

时间复杂度:来评估代码的执行耗时

20230712220059

常对幂指阶

20230712220558

空间复杂度

空间复杂度全称是渐进空间复杂度,表示算法占用的额外存储空间与数据规模之间的增长关系

20230712224229

5.2List


一、数组

数组(Array)是一种用连续的内存空间存储相同数据类型数据的线性数据结构

20230712224543

寻址公式:a[i] = baseAddress + i * dataTypeSize

索引为什么从0开始,1开始不行吗?

在根据数组索引获取元素的时候,会用索引和寻址公式来计算内存所对应的元素数据,寻址公式是:数组的首地址+索引乘以存储数据的类型大小

如果数组的索引从1开始,寻址公式中,就需要增加一次减法操作,对于CPU来说就多了一次指令,性能不高

操作数组的时间复杂度(查找)

1、随机查询(根据索引查询)

数组元素的访问是通过下标来访问的,计算机通过数组的首地址和寻址公式能够很快速的找到想要访问的元素

O(1)

2、未知索引查询

排序前:遍历,平均时间复杂度O(n)

排序后:二分,O(log n)

操作数组的时间复杂度(插入,删除)

数组是一段连续的内存空间,因此为了保证数组的连续性会使数组的插入和删除的效率变的很低

需要挪动数组元素,最好情况下是O(1)的,最坏情况下是O(n)的,平均情况下的时间复杂度是O(n)

二、ArrayList

1、源码分析

成员变量

20230712231140

构造方法

20230712231430 20230712231535

添加和扩容操作(第1次添加数据)

20230712231958

20230712232104

20230712232212

第一次添加数据时初始化默认长度是10,当位置不够时,增加1.5倍(右移1位,除2,再加上原来的长度)

2、底层原理

20230712233341

3、实现数组和List之间的转换

数组转List:使用JDK中java.util.Arrays工具类的asList方法

List转数组:使用List的toArray方法。无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组

20230712233527

用Arrays.asList转List后,如果修改数组内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的是同一个地址

List用toArray转数组后,如果修改List内容,数组不受影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没有关系了,所以即使list修改了以后,数组也不会受到影响

20230712234005

三、LinkedList

单向链表

链表中的每一个元素称之为结点(Node),物理存储单元上,非连续、非顺序的存储结构

单向链表:每个结点包括两部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。记录下一个结点地址的指针叫作后继指针next

20230713202750

20230713202758

查询操作

插入/删除操作

双向链表

每个结点不止有一个后继指针next指向后面的结点,有一个前驱指针prev指向前面的结点

20230713203321

20230713203642

查询操作

查询头尾结点的时间复杂度是O(1),平均的查找时间复杂度是O(n),给定结点找前驱节点的时间复杂度是O(1)

增删操作

头尾节点增删的时间复杂度为O(1),其他部分节点增删的时间复杂度是O(n),给定节点进行前后增删时间复杂度是O(1)

对比:

ArrayList和LinkedList的区别

1、底层数据结构

2、操作数据效率

3、空间占用

4、线程安全


5.3Map


一、二叉树

二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。

二叉树每个节点的左子树和右子树也分别满足二叉树的定义

用链式存储的树的节点可定义如下:

20230715224552

二叉树分类

二叉搜索树

(Binary Search Tree, BST)又名二叉查找树,有序二叉树或者排序二叉树,是二叉树中比较常用的一种类型,二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。(没有键值相等的节点)

20230715225835

时间复杂度

插入、查找、删除的时间复杂度O(logn)

特殊情况下:当二叉树的节点只有左或右子节点时,二叉查找树退化成链表,左右子树极度不平衡,插入、查找、删除的时间复杂度O(n)

二、红黑树

(Red Black Tree):也是一种自平衡的二叉搜索树(BST),之前叫做平衡二叉B树(Symmetric Binary B-Tree)

性质:

20230715230330

在添加或删除节点的时候,如果不符合这些性质会发生旋转,以达到所有的性质(保证平衡)

时间复杂度

查找:红黑树也是一棵BST,查找操作的时间复杂度为:O(log n)

添加:添加要先从根节点开始查找到元素添加的位置,时间复杂度为O(log n),添加完成后涉及到复杂度为O(1)的旋转调整操作,所以整体的时间复杂度是O(log n)

删除:删除要先从根节点开始查找到元素删除的位置,时间复杂度为O(log n),添加完成后涉及到复杂度为O(1)的旋转调整操作,所以整体的时间复杂度是O(log n)

三、散列表

在HashMap中的最重要的一个数据结构就是散列表,在散列表中又使用到了红黑树和链表

概念

散列表(Hash Table)又名哈希表/Hash表,是根据键(Key)直接访问在内存存储位置值(Value)的数据结构,它是由数组演化而来的,利用了数组支持按照下标进行随机访问数据的特性

散列函数

将键(Key)映射为数组下标的函数叫做散列函数。可以表示为:hashValue = hash(Key)

基本要求:

散列冲突

实际情况下想找一个散列函数能够做到对于不同的key计算得到的散列值都不同几乎是不可能的,即便像著名的MD5,SHA等哈希算法也无法避免这一情况,这就是散列冲突(或者哈希冲突,哈希碰撞,就是指多个Key映射到同一个数组下标位置)

拉链法

在散列表中,数组的每个下标位置我们可以称之为桶(bucket)或者槽(slot),每个桶(槽)会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。

插入操作,通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可,插入的时间复杂度是O(1)

查找和删除,我们同样通过散列函数计算出对应的槽,然后遍历链表查找或删除

DDos攻击

分布式拒绝服务攻击(Distributed Denial of Service)

指处于不同位置的多个攻击者同时向一个或数个目标发动攻击,或者一个攻击者控制了位于不同位置的多台机器并利用这些机器对受害者同时实施攻击,由于攻击的发出点是分布在不同地方的,这类攻击称为分布式拒绝服务攻击,其中的攻击者可以有多个

三、HashMap实现原理

HashMap的数据结构:底层使用hash表数据结构,即数组和链表或红黑树

1、当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标

2、存储时,如果出现hash值相同的key,此时有两种情况

tips:链表长度大于8且数组长度大于64转换为红黑树

3、获取时,直接找到hash值对应的下标,再进一步判断key是否相同,从而找到对应值

jdk1.7和jdk1.8的hashMap有什么区别?

HashMap的put方法的具体流程

常见属性

20230718203342 20230718203459

添加数据流程图

20230718203840

1、判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)

2、根据键值对key计算hash值得到数组索引

3、判断table[i] == null,条件成立,直接新建节点添加

4、如果table[i] == null,不成立

5、插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold(数组长度*0.75),如果超过,进行扩容

HashMap的扩容机制

20230718205055

hashMap的寻址算法

20230718211157

数组长度为什么必须是2的n次幂?

1、计算索引时效率更高,如果是2的n次幂可以使用位与运算代替取模

2、扩容时重新计算索引效率更高,hash & oldCap == 0的元素留在原来的位置,否则新位置 = 旧位置 + oldCap

hashMap在1.7情况下的多线程死循环问题

底层数据结构:数据 + 链表

在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环

20230718211759 20230718211955

比如说,现在有两个线程

线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入

线程二:也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来,比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。

线程一:继续执行的时候就会出现死循环的问题。

线程一先将A移入新的链表,再将B插入到链头,由于另一个线程的原因,B的next指向了A,所以B -> A -> B,形成循环

当然,JDK8将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题。


第六章 并发编程


6.1线程基础


一、线程和进程的区别

进程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存,在指令运行过程中还需要用到磁盘、网络等设备,进程就是用来加载指令、管理内存、管理IO的。

当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程

线程

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行,一个进程之内可以分为一到多个线程

20230718213041

对比

二、并行和并发的区别

单核CPU

多核CPU

每个核(core)都可以调度运行线程,这时候线程是可以并行的

并发是同一时间应对多件事情的能力,并行是同一时间动手做多件事情的能力

三、创建线程的方式有哪些

1、继承Thread类

20230718214237

2、实现runnable接口

20230718214301

3、实现Callable接口

20230718214344

4、线程池创建线程

20230718214412

runnable和callable的区别?

run方法和start方法的区别?

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

run():封装了要被线程执行的代码,可以被调用多次

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

线程的状态可以参考JDK中的Thread类中的枚举State

20230718215053 20230718215345

1、线程包括哪些状态

新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、时间等待(TIMED_WALTING)、终止(TERMINATED)

2、线程状态之间是如何变化的

sleep(0):调用sleep(0)可以释放cpu时间,让线程马上重新回到就绪队列而非等待队列,sleep(0)释放当前线程所剩余的时间片(如果有剩余的话),这样可以让操作系统切换其他线程来执行,提升效率。

五、新建T1、T2、T3三个线程,如何保证它们按顺序执行

可以使用线程中的join方法解决

20230718221038

六、notify()和notifyAll()有什么区别

七、java中wait和sleep方法的不同

共同点:效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态

不同点:

1、方法归属不同

2、醒来时机不同

3、锁特性不同

八、如果停止一个正在运行的线程

1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止 布尔值flag while循环

2、使用stop方法强行终止(不推荐,方法已作废)

3、使用interrupt方法中断线程


6.2线程安全


一、synchronized关键字的底层原理

Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。

20230719201959

Monitor

Monitor被翻译为监视器,是由jvm提供,C++语言实现

20230719202356

1、Synchronized对象锁采用互斥的方式让同一时刻至多只有一个线程能持有对象锁

2、它的底层由monitor实现,moniter是jvm级别的对象(c++实现),线程获得锁需要使用对象(锁)关联monitor

3、在monitor内部有三个属性,分别是owner、entrylist、waitset

4、其中owner是关联的获得锁的线程,并且只能关联一个线程,entrylist关联的是处于阻塞状态的线程,waitset关联的是处于waiting状态的线程

重量级锁与锁升级

Monitor实现的锁属于重量级锁,里面设计到了用户态和内核态的切换、进程的上下文切换,成本较高。性能比较低

在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。

对象是怎么关联上的Monitor?

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象对象头的Mark Word中就被设置指向Monitor对象的指针。

对象的内存结构

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充

20230719203707

MarkWord

20230719204032

轻量级锁

在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码,这种情况下,用重量级锁是每必要的,因此JVM引入了轻量级锁的概念。

加锁流程

1、在线程栈中创建一个Lock Record,将其obj字段指向锁对象

2、通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁

3、如果是当前线程已经持有该锁了,代表这是一次锁重入,设置Lock Record第一部分为null,起到了一个重入计数器的作用

4、如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁

20230719204829 20230719204840 20230719204921

解锁过程

1、遍历线程栈,找到所有obj字段为等于当前锁对象的Lock Record

2、如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue

3、如果Lock Record的Mark Word不为null,则利用CSA指令将对象头的mark word恢复成无锁状态。如果失败则碰撞为重量级锁

20230719204933 20230719204947

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍需要执行CAS操作。

Java6中引入了偏向锁来做进一步优化,只有第一次使用CAS将线程ID设置到对象头的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有

20230719210024 20230719210033

对比

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种。

20230719210239

一旦锁发生了竞争,都会升级为重量级锁

二、JMM(Java内存模型)

JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性

JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)

线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存

20230719223618

三、CAS

CAS:Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在锁情况下保证线程操作共享数据的原子性。

在JUC(java.util.concurrent)包下实现的很多类都用到了CAS操作

20230719224417

CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CSA指令

20230719224606

乐观锁和悲观锁

CSA基于乐观锁思想,最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,可以重试

synchronized基于悲观锁,最悲观的估计,防止其它线程来修改共享变量,释放锁之后,其它线程才可以修改

四、对volatile的理解

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,那么就具备了两层语义:

1、保证线程间的可见性

用volatile修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见

20230719225435

线程一打印正常,线程二打印true,但是线程三一直不结束

原因:主要因为在JVM虚拟机种有一个JIT(即时编译器)给代码做了优化

20230719225608

解决方案一:在程序运行的时候加入vm参数**-Xint**表示禁用即时编译器,不推荐,其它程序还要使用

解决方案二:在修饰stop变量的时候加上volatile,当前告诉JIT,不要对volatile修饰的变量做优化

2、禁止指令重排序

用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果

20230719230200

注解**@Actory**保证方法内的代码在同一个线程下进行

20230719230611 20230719230644

使用技巧:

五、AQS

全称是 AbsrtactQueuedSynchronizer,即抽象队列同步器,它是构建锁或其他同步组件的基础框架

AQS与Synchronized的区别

20230719231057

AQS常见的实现类

基本工作机制

AQS内部维护了一个先进先出的双向队列,队列中存储排队的线程

在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的一个线程修改成功了state为1,则当前线程就相当于获取了资源

20230719231342

多个线程同时去抢资源,如果保证原子性?

CAS设置state状态,保证操作的原子性

AQS是公平锁还是非公平锁?

都可以实现

20230719231619 20230719231626 20230719231633

线程0结束后,把state状态改为0,此时唤醒队列中的head线程1,这时又来了一个不在队列中的线程5,恰巧线程5修改了state的状态(0 -> 1),此时就是非公平锁

线程0结束后,把state状态改为0,此时唤醒队列中的head线程1,这时进入队列,处在tail,线程1修改;了state的状态(0 -> 1),此时就是公平锁

六、ReentrantLock的实现原理

ReentrantLock翻译过来是可重入锁,相对于synchronized

20230719232510

ReentrantLock主要利用CAS + AQS队列来实现,它支持公平锁和非公平锁,两者的实现类似

构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

20230719232849 20230719232947

七、synchronized和Lock的区别

synchronized是关键字,源码在jvm中,用c ++ 语言实现

Lock是接口,源码由jdk提供,用java语言实现

使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁

二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能

Lock提供了许多synchronized不具备的功能,例如公平锁,可打断锁,可超时,多条件变量

Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock(读写锁)

在没有竞争时,synchronized做了很多优化,如偏向锁,轻量级锁,性能不赖

在竞争激烈时,Lock的实现通常会提供更好的性能

八、死锁产生的条件

死锁:一个线程需要同时获取多把锁,这是就容易发生死锁

20230720222218 20230720222240

此时程序并没有结束,这种现象就是死锁现象,线程t1持有A的锁等待获取B锁,线程t2持有B的锁等待获取A的锁

如何进行死锁诊断?

当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和jstack

输入jps后,会显示这个死锁进程的id,然后输入jstack -l id

20230720222741 20230720222723

可视化工具:

用于对jvm的内存,线程,类的监控,是一个基于jmx的GUI性能监控工具,打开方式:java安装目录bin目录下 直接启动jconsole.exe运行

能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈,打开方式:java安装目录bin目录下 直接启动jvisualvm.exe就行

九、ConcurrentHashMap

ConcurrentHashMap是一种线程安全的高效Map集合

底层数据结构:

jdk1.7

20230720223418

put操作

20230720223518

jdk1.8

放弃了Segment臃肿的设计,数据结构跟HashMap1.8的数据结构一样:数组 + 链表 + 红黑树,采用CAS + Synchronized来保证并发安全进行实现

20230720223833

十、导致并发程序出现问题的根本原因/Java程序中怎么保证多线程的执行安全

Java并发编程三大特性:原子性,可见性,有序性

原子性

一个线程在CPU中操作不可暂停,也不可中断,要么执行完成,要么不执行

解决:

1、synchronized:同步加锁

2、JUC里面的lock:加锁

内存可见性

让一个线程对共享变量的修改对另一个线程可见

解决:

1、加锁,synchronized或Lock

2、用volatile修饰共享变量

有序性

指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的

解决:

用volatile修饰变量


6.3线程池


一、线程池的执行原理(线程池的核心参数)

20230722165454 20230722170159

二、线程池中有哪些常见的阻塞队列

workQueue:当没有空闲核心线程时,新来任务会加入到此队列排队,队列满时会创建救急线程执行任务

1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO

2、LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO

3、DelayedWorkQueue:是一个优先级队列,可以保证每次出队的任务都是当前队列中执行时间最靠前的

4、SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作

ArrayBlockingQueue和LinkedBlockingQueue的区别

20230722171420

无界:最大为Integer的最大值

三、如何确定核心线程数

一般来说:文件读写,DB读写,网络请求等 核心线程数大小设置为CPU核数 * 2 + 1

一般来说:计算型代码,Bitmap转换,Gson转换等 核心线程数大小设置为CPU核数 + 1

20230722171926 20230722172059

四、线程池的种类有哪些

在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有四种

1、newFixedThreadPool

创建使用固定线程数的线程池——适用于任务量已知,相对耗时的任务

20230722181532

2、newSingleThreadExecutor

单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行——适用于按照顺序执行的任务

20230722182217

3、newCachedThreadPool

可缓存线程池——适合任务数比较密集,但每个任务执行时间较短的情况

20230722182628

4、newScheduledThreadPool

提供了“延迟”和“周期执行”功能的ThreadPoolExecutor

20230722183008

五、为什么不建议用Executors创建线程池

20230722183524

6.4使用场景


一、线程池的使用场景(CountDownLatch、Future)

CountDownLatch(闭锁、倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)

20230722223331

使用场景一:es数据批量导入

在我们项目上线之前,我们需要把数据库中的数据一次性同步到es索引库中,但是当时的数据好像是1000万左右,一次性读取数据肯定不行(oom异常),当时我就想到可以使用线程池的方式导入,利用CountDownLatch来控制,就能避免一次性加载过多,防止内存溢出

20230722231132 20230722231232

使用场景二:数据汇总

在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息,包含的商品,物流信息,这三块信息都在不同的微服务中进行实现的,我们如何完成这个业务呢?

20230722232205

在实际开发的过程中,难免需要调用多个接口来汇总数据,如果所有接口(或部分接口)没有依赖关系,就可以使用线程池+future来提升性能

使用场景三:异步调用

20230722232650

为了避免下一级方法(保存历史记录)影响上一级方法(查询结果)(性能考虑),可使用异步线程调用下一个方法(前提:上一级方法不需要下一级方法返回值),可以提升方法响应时间

二、如何控制某个方法允许并发访问线程的数量

Semaphore:信号量,是JUC包下的一个工具类,底层是AQS,我们可以通过其限制执行的线程数量

使用场景:通常用于那些资源有明确访问数量限制的场景,常用于限流

20230722233445

Semaphore使用步骤

三、谈谈你对ThreadLocal的理解

ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal同时实现了线程内的资源共享

案例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的Connection上进行数据库的操作,避免A线程关闭了B线程的连接

ThreadLocal基本使用

20230722234920

ThreadLocal实现原理&源码解析

ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内部的值,从而实现线程数据隔离

20230722235106

set方法

20230722235228

get方法/remove方法

20230722235250

ThreadLocal内存泄漏的问题

Java对象中的四种引用类型:强引用、软引用、弱引用、虚引用

User user = new User();
User user = new User();
WeakReference weakReference = new WeakReference(user);

每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承了WeakReference。其中Key为使用弱引用的ThreadLocal实例,value为线程变量的副本

20230722235952 20230723000219

第七章 JVM


Java Virtual Machine Java程序的运行环境(java二进制字节码的运行环境)

好处:

20230723123729

运行流程

20230723123857 20230723124040

7.1JVM组成


一、什么是程序计数器

程序计数器:线程私有的,每个线程一份,内部保存的字节码的行号,用于记录正在执行的字节码指令的地址

javap -v xx.class //打印堆栈大小,局部变量的数量和方法的参数
20230724192306

二、Java堆

线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError(OOM)(内存溢出)异常

20230724193129

年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor(幸存者)区,根据JVM的策略,在经过几次垃圾收集后,仍然存活于Survivor的对象将被移动到老年代区间

老年代主要保存生命周期长的对象,一般是一些老的对象

元空间保存的类信息,静态变量,常量,编译后的代码

java7和java8的JVM内存结构的区别

20230724213217 20230724213324

三、什么是虚拟机栈

Java Virtual machine Stacks(java虚拟机栈)

20230724213851

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

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

2、栈内存分配越大越好吗?

未必,默认的栈内存通常为1024k

栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半

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

20230724214355

四、栈内存溢出(OOM)情况

20230724214624

五、栈和堆的区别

六、方法区/元空间

20230724215710

常量池

可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息

20230724220022 20230724220313

运行时常量池

常量池是***.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池**,并把里面的符合地址变为真实地址

20230724220539 20230724220610

七、直接内存

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

举例:Java代码完成文件拷贝

20230724220918 20230724221043 20230724221206

7.2类加载器


一、什么是类加载器,有哪些?

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

20230724221814

二、双亲委派模型

加载某一个类,先委托上一级的加载器进行加载,如果上一级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类

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

20230724222826

三、类装载的执行过程

类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段,其中,验证、准备和解析这三个部分统称为连接(linking)

20230724223113

1、加载

20230724223350

2、验证

验证类是否符合JVM规范,安全性检查

前三项都是格式检查,如:文件格式是否错误,语法是否错误,字节码是否合规

最后一项:Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法,检查它们是否存在

20230724223644

3、准备

为类变量分配内存并设置类变量初始值

4、解析

把类中的符号引用转换为直接引用

比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法

20230724224134 20230724224149

#1,#2就是符号引用,找到java/io/PrintStream这个类,println这个方法就是直接引用

5、初始化

对类的静态变量,静态代码块执行初始化操作

6、使用

JVM开始从入口方法开始执行用户的程序代码

7、卸载

当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象


7.3垃圾回收


一、对象什么时候可以被垃圾器回收

如果一个或多个对象没有任何引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法

引用计数法

一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收

有缺点:当对象间出现了循环引用的话,则引用计数法就会失效

20230726221516 20230726221529

可达性分析算法

现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾

20230726221653

X,Y这两个节点是可回收的

哪些对象可以作为GC Root?

20230726222050

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

1、标记清除算法

标记清除法,是将垃圾回收分为2个阶段,分别是标记和清除

20230726222640

2、标记整理算法

20230726222714

优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记整理算法多了一步,对象移动内存位置的步骤,其效率也有一定的影响

3、复制算法

20230726222926

优点:

缺点:

20230726223122

三、JVM中的分代回收

1、区域划分

在java8中,堆被分为了两份:新生代和老年代【1:2】

20230726223319

对于新生代,内存又被分成了三个区域

2、对象回收分代回收策略

①新创建的对象,都会先分配到Eden区

②当Eden区内存不足,标记Eden和from(现阶段没有)的存活对象

③将存活对象采用复制算法复制到to中,复制完毕后,Eden和from内存都得到释放

④经过一段时间后,Eden区的内存又出现不足,标记Eden区和to区存活的对象,将其复制到from区

⑤当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存者内存不足或大对象会提前晋升)

MinorGC、MixedGC、FullGC的区别

STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成

四、JVM有哪些垃圾回收器

1、串行垃圾收集器

Serial和Serial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑

垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成

20230726224919

2、并行垃圾收集器

Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器

垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成

20230726225133

3、CMS(并发)垃圾收集器

CMS全称Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间段,用户体验就好。其最大的特点是在进行垃圾回收时,应用仍然能正常运行。

20230726225542 20230726225611

初始标记:只会标记GC Roots

并发标记:找到与GC Roots相关联的对象

重新标记:比如在并发标记之后,X对象与A关联了,这时就不能把X清除,所以要重新标记

4、G1垃圾收集器

作用于新生代和老年代

五、详细的聊一下G1垃圾回收器

20230726230210

1、Young Collection 年轻代垃圾回收

2、Young Collection + Concurrent Mark 年轻代垃圾回收 + 并发标记

3、Mixed Collection 混合垃圾回收

20230726231514

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

1、强引用

只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

20230726231712

2、软引用

仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收

20230726231827

3、弱引用

仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象

20230726231935 20230726231955

4、虚引用

必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存

20230726232247 20230726232305

7.4JVM实践


一、JVM调优的参数可以在哪里设置参数值

1、war包部署在tomcat中设置

修改TOMCAT_HOME/bin/catalina.sh文件(Linux下)windows下是catalina.bat文件 (卡塔琳娜)

20230727220445

2、jar包部署在启动参数设置

通常在linux系统下直接加参数启动springboot项目

20230727220815

nohup:用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行

参数&:让命令在后台执行,终端退出后命令仍旧执行

二、JVM调优的参数都有哪些

对于JVM调优,主要就是调整年轻代、老年代、元空间的内存空间大小以及使用的垃圾回收器类型

1、设置堆空间大小

设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大,初始大小设置为相同的值。

20230727221509

堆空间设置多少合适?

2、虚拟机栈的设置

虚拟机栈的设置:每个线程默认会开启1M的内存,用于存放栈帧,调用参数,局部变量等,但一般256k就够用,通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

20230727222019

3、年轻代中Eden区和两个Survivor区的大小比例

设置年轻代中Eden区和两个Survivor区的大小比例,该值如果不设置,默认比例为8:1:1。通过增大Eden区的大小,来减少YGC发生的次数,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优。

20230727222259

5、年轻代晋升老年代阈值

20230727222329

6、设置垃圾回收收集器

通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器

20230727222656

三、JVM调优工具

1、命令工具

20230727223338 20230727223402 20230727223438 20230727223621 20230727225302

是JVM统计监测工具,可以用来显示垃圾回收信息,类加载信息,新生代统计信息等。

20230727225445

2、可视化工具

用于对jvm的内存,线程,类的监控,是一个基于jmx的GUI性能监控工具

打开方式

java安装目录bin目录下 直接启动jconsole.exe就行

能够监控线程,内存情况,查看方法的CPU时间二号内存中的对象,已被GC的对象,反向查看分配的堆栈

打开方式:java安装目录bin目录下 直接启动jvisualvm.exe就行

查看运行中的dump文件 Dump文件是进程的内存镜像,可以把程序的执行状态通过调试器保存到dump文件中

四、Java内存泄漏的排查思路

20230727230500 20230727230604

1、获取堆内存快照dump

20230727230732

2、VisualVM去分析dump文件

3、通过查看堆信息的情况,定位内存溢出问题

20230727230913

五、CPU飙高排查方案与思路

1、使用top命令查看占用cpu的情况

20230727231020

2、通过top命令查看后,可以查看是哪一个进程占用cpu较高,上图所示的进程为:40940

3、查看进程中的线程信息

20230727231558

通过以上分析,在进程40940中的线程40950占用cpu较高

4、可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号

20230727231749

#八股文#面试