面试
# JAVA基础
# java的基础类型
Java 的基础数据类型可以分为四类,具体包括八种基本类型。它们被分为两大类:数值型和布尔型。以下是详细的分类和说明:
# 1. 数值型
数值型数据类型用于表示数字,可以进一步分为整型和浮点型。
整型
整型用于表示整数(没有小数部分)。Java 中有四种整型:
# byte
- 大小:1 字节(8 位)
- 取值范围:-128 到 127
- 适用场景:用于节省内存,通常用于处理大量数据时。
short
- 大小:2 字节(16 位)
- 取值范围:-32,768 到 32,767
- 适用场景:与
byte
类似,适用于更小的整数。
int
- 大小:4 字节(32 位)
- 取值范围:-2,147,483,648 到 2,147,483,647
- 适用场景:最常用的整型,适合大多数整数计算。
long
- 大小:8 字节(64 位)
- 取值范围:-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
- 适用场景:用于需要更大范围整数的计算。
# 浮点型
浮点型用于表示带小数的数字。Java 中有两种浮点型:
float
- 大小:4 字节(32 位)
- 取值范围:大约 ±3.40282347E+38(有效位数大约为7位)
- 适用场景:用于需要少量小数位的浮点数,节省内存。
double
- 大小:8 字节(64 位)
- 取值范围:大约 ±1.79769313486231570E+308(有效位数大约为15位)
- 适用场景:最常用的浮点类型,适用于高精度的计算。
# 2. 布尔型
- boolean
- 大小:1 字节(Java 虚拟机中可能为 1 位)
- 取值范围:
true
或false
- 适用场景:用于条件判断和控制流,例如在
if
语句和循环中。
# 3. 字符型
- char
- 大小:2 字节(16 位,支持 Unicode 字符)
- 取值范围:0 到 65,535(即 Unicode 字符集中的字符)
- 适用场景:用于表示单个字符,例如字母、数字和符号。
# 4. 引用类型(不属于基本类型,但常被提及)
在 Java 中,除了基本数据类型,还有引用数据类型,包括类、接口、数组等。引用类型用于表示复杂的数据结构和对象。
# 总结
Java 的基本数据类型总结如下:
数据类型 | 大小(字节) | 取值范围 | 适用场景 |
---|---|---|---|
byte | 1 | -128 到 127 | 节省内存 |
short | 2 | -32,768 到 32,767 | 比 byte 更大的整数 |
int | 4 | -2,147,483,648 到 2,147,483,647 | 默认整数类型 |
long | 8 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 需要大范围整数的计算 |
float | 4 | ±3.40282347E+38(有效位数约 7 位) | 少量小数位的浮点数 |
double | 8 | ±1.79769313486231570E+308(有效位数约 15 位) | 高精度浮点数 |
boolean | 1 | true 或 false | 条件判断 |
char | 2 | 0 到 65,535(Unicode 字符) | 表示单个字符 |
这些基本数据类型是 Java 编程中的基础,了解它们的特性和适用场景对编写高效的 Java 代码至关重要。如果你有任何其他问题或需要更详细的信息,请随时询问!
# 类的加载顺序
1、有继承关系的加载顺序
1、父类静态代码块和静态成员变量
2、子类静态代码块和静态成员变量
3、父类代码块和成员变量
4、父类构造方法
5、子类代码块和成员变量
6、子类构造方法
2、没有继承关系的加载顺序
静态代码块(只加载一次)
构造方法(创建一个实例就加载一次)
静态方法,调用的时候才会加载,不调用的时候不会加载
# 线程的生命周期
线程生命周期 6种
NEW:新创建
RUNNABLE:运行
BLOCKED:被阻塞
WAITING:等待
TIMED_WAITING:计时等待
TERMINATED:被终止
# wait和sleep的区别
- wait会释放所有锁而sleep不会释放锁资源.
- wait只能在同步方法和同步块中使用,而sleep任何地方都可以
- wait无需捕捉异常,而sleep需要
- sleep是Thread的方法,而wait是Object类的方法;
- sleep方法调用的时候必须指定时间
# == 和 equals
== 的作用 :
基本类型:比较的就是值是否相同 (基本类型:byte,short,int,long,float,double,boolean,char)
引用类型:比较的就是地址值
equals的作用:
引用类型默认的情况下比较的是地址值,重写该方法后比较的就是成语变量的值是否相等
# HashMap,LinkedHashMap,TreeMap的区别
# hashmap
HashMap 是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。遍历时,取得数据的顺序是完全随机的。
HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null。
HashMap不支持线程的同步(即任一时刻可以有多个线程同时写HashMap),可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。
Hashtable与 HashMap类似,它继承自Dictionary类。不同的是:它不允许记录的键或者值为空;它支持线程的同步(即任一时刻只有一个线程能写Hashtable),因此也导致了 Hashtable在写入时会比较慢。
# LinkedHashMap
LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。也可以在构造时带参数,按照应用次数排序。
在遍历的时候会比HashMap慢,不过有种情况例外:当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢。因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。
# TreeMap
TreeMap实现SortMap接口,能够把它保存的记录根据键排序。
默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。
# TODO锁
# 乐观 悲观 重量级 轻量级 偏向 公平 非公平
# 数据结构
# 二叉树
二叉树,就是一个根节点,然后左边小 ,右边大
# 红黑树
红黑树就是平衡二叉树,他不会让一边的节点比另外一边多太多
# BTree
B树,每个叶子节点都存在着data值,索引元素不重复,索引还是从左往右 左小又大
# B+Tree
B+树, 非叶子节点,不储存data,只存储索引(冗余),可以放更多的索引
叶子节点包含所有的索引字段
叶子节点用指针连接,提高区间访问的性能
# 设计模式
# 单例设计模式
1.饿汉式 私有化对象 一上来就初始化
2.懒汉式,延迟加载,什么时候调用什么时候初始化
# 策略模式
在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。
介绍
**意图:**定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
**主要解决:**在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
**何时使用:**一个系统有许多许多类,而区分它们的只是他们直接的行为。
**如何解决:**将这些算法封装成一个一个的类,任意地替换。
**关键代码:**实现同一个接口。
应用实例: 1、诸葛亮的锦囊妙计,每一个锦囊就是一个策略。 2、旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。 3、JAVA AWT 中的 LayoutManager。
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。
# 工厂模式
工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
**意图:**定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
**主要解决:**主要解决接口选择的问题。
**何时使用:**我们明确地计划不同条件下创建不同实例时。
**如何解决:**让其子类实现工厂接口,返回的也是一个抽象的产品。
**关键代码:**创建过程在其子类执行。
应用实例: 1、您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。 2、Hibernate 换数据库只需换方言和驱动就可以。
优点: 1、一个调用者想创建一个对象,只要知道其名称就可以了。 2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 3、屏蔽产品的具体实现,调用者只关心产品的接口。
**缺点:**每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
Map<String, MQManager> map = applicationContext.getBeansOfType(MQManager.class);
if (mqBeanMap.size() == 0) {
map.forEach((key, value) -> mqBeanMap.put(value.method(), value));
}
// 获取manager操作
MQManager mqManager = mqBeanMap.get(tabSyncData.getSyncTableName());
# 模板模式
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。
**意图:**定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
**主要解决:**一些方法通用,却在每一个子类都重新写了这一方法。
**何时使用:**有一些通用的方法。
**如何解决:**将这些通用算法抽象出来。
**关键代码:**在抽象类实现,其他步骤在子类实现。
应用实例: 1、在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异。 2、西游记里面菩萨定好的 81 难,这就是一个顶层的逻辑骨架。 3、spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。
优点: 1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。
**缺点:**每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
使用场景: 1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。
# Spring面试
# TODO spirng源码
# 1.SpringIOC的理解,原理与实现
控制反转:原来的对象都是由使用者,我们自己创建,new,有了spring后可以吧整个对象交个spring管理
容器:spring会通过一个容器来存储对象,使用map结构存储,整个bean的生命周期,从创建到销毁全部都是由容器来管理
DI:依赖注入,就是吧属性的值注入到具体的对象中,通过@Autowired完成bean 属性的注入
# 2.SpringIOC的底层实现
1.先通过createBean 创建一个bean工厂(defaulListableBeanFactory)
2.开始循环创建对象,因为容器中的bean默认都是单例的,所以优先通过 getBean,doGetBean从容器中查找,如果找不到的话
3.通过createBean,doCreateBean方法 ,以反射的方式创建对象,一般情况下使用的是无参构造方法(getDeclaredConstructor,newInstance)
4.进行对象的属性填充 populateBean
5.进行其他初始化的操作 (initializingBean)
# 3.SpringBean的生命周期
1.实例化bean,通过反射的方式创建对象
2.给属性赋值,Populate()
3.调用aware接口的相关方法:invokeAwareMethod(完成 BeanName,BeanFactory,BeanClassLoader对象的属性设置)
4.调用BeanPostProcessor 的前置处理方法 postProcessBeforeInitialization()
5.调用Initmethod方法:通过invokeInitmethod()判断是否实现了InitializingBean接口,如果有调用afterPropertiesSet()方法,没有就不调用
6.调用调用BeanPostProcessor 的后置处理方法 postProcessAfterInitialization()
7.获取到完整的对象,可以通过getBean的方式来完成对象获取
8.销毁流程
1.判断是否实现了 DisposableBean接口
2.destory接口方法
# BeanFactory 与 FactoryBean 有什么区别
相同点: 都是用创建对象的
不同点: BeanFactory 创建对象的时候必须遵守严格的生命周期流程,太复杂了 如果想简单的自定义某个对象的创建,同时创建完成的对象交给spring管理,那就需要实现FactoryBean 接口了
# 7.SpringAOP的底层实现原理
AOP的底层就是动态代理实现的
aop是ioc 的一个拓展功能,先有的ioc 在有的aop,aop只是在ioc整个流程中新增的一个拓展点而已:BeanPostPorcessor
分:bean在创建的过程中有一个步骤对bean进行拓展实现,aop本身就是一个拓展功能,所以在BeanPostPorcessor的后置方法中来实现
1.代理对象的创建过程(advice(通知) ,切面, 切点)
2.通过cglib或者jdk的方式来生成代理对象
3.在执行方法调用的时候,会调用到生成的字节码文件中,直接会找到DynamicAdvisoreDinterceptor 类中的intercept方法,从这个方法开始执行
4.根据定义好的通知来生成对应的拦截器链
5.从拦截器链中依次获取每一个通知开始进行执行
# 8.Spring的事务是如何回滚的
总: spring的事务是由aop来实现的,首先要生成具体的代理对象,然后按照aop的整套流程来执行具体的逻辑.
分:
1.先做准备工作,解析各个方法上事务的相关属性,根据具体的属性来判断是否需要开启新事务
2.当需要开启的时候,获取数据库连接,关闭自动提交功能,开启事务
3.执行具体的sql逻辑操作
4.在操作过程中,如果执行失败了,那么就会通过afterThrowing来完成事务的回滚操作,回滚的具体逻辑是通过doRollBack方法实现的,实现的时候也是先要获取连接对象,通过连接对象来回滚
5.如果执行过程中,没有任何意外发生,那么通过afterReturning 来完成事务的提交操作,具体逻辑是通过doCommit的方法来实现的,实现的时候也是获取连接,通过连接对象提交
6.当事务执行完毕之后需要清除相关的事务信息 cleanupTransactionInfo
# 9.Srping事务的传播
7种传播特性
'REQUIRED' 如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
# SpringBoot
# 1.SpringBoot自动装配
1.Spring的自动装配原理:
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,
这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式Spring容器配置类,通过@Bean导入到Spring容器中,
以Properties结尾命名的类是和配置文件进行绑定的。它能通过这些以Properties结尾命名的类中取得在全局配置文件中配置的属性,我们可以通过修改配置文件对应的属性来修改自动配置的默认值,来完成自定义配置
# 2.@Bean 和 @Compent
@Compent 是通过类路径扫描出来的bean, 自动装配到spring 中
@Bean 标注这个方法返回一个类, 在这个方法中我们可以自定义这个类产生的逻辑.
而且当我们使用第三方类库的某个方法时,就只能使用@Bean 来注册
# SpringCloud
# SpringCloud 5大组件
1、服务发现Netflix Eureka;
2、客服端负载均衡Netflix Ribbon;
3、断路器 Netflix Hystrix;
4、服务网关Netflix gateway;
5、配置中心
# Redis面试
# Redis支持的数据类型
String字符串:
格式: set key value
string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
string类型是Redis最基本的数据类型,一个键最大能存储512MB。
Hash(哈希)
格式: hmset name key1 value1 key2 value2
Redis hash 是一个键值(key=>value)对集合。
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
格式: lpush name value
在 key 对应 list 的头部添加字符串元素
格式: rpush name value
在 key 对应 list 的尾部添加字符串元素
格式: lrem name index
key 对应 list 中删除 count 个和 value 相同的元素
格式: llen name
返回 key 对应 list 的长度
Set(集合)
格式: sadd name value
Redis的Set是string类型的无序集合。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
zset(sorted set:有序集合)
格式: zadd name score value
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
# RDB和AOF两种持久化操作
RDB,就是每隔几分钟,或者几小时,几天,生成redis内存中的数据的一份完整的快照
AOF(append only file)持久化:以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用 是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式
1)使用AOF
开启AOF功能需要设置配置:appendonly yes,默认不开启。AOF文件名 通过appendfilename配置设置,默认文件名是appendonly.aof。保存路径同 RDB持久化方式一致,通过dir配置指定。AOF的工作流程操作:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load)
# 什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?
缓存穿透
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
如何避免?
1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
2:对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的,中,查询时通过该bitmap过滤。
缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。
如何避免?
分为3部:
1.事前 redis 做好高可用, 主从 +哨兵 , redis cluster 避免全盘崩溃 , 所有key的过期时间设置为随机,避免大量key在同一时间过期
2.事中,通过限流(hystrix),和服务降级,避免数据库被打死, 限流就我数据库1s最多处理2000个请求,一下来了5000个 , 2000个通过限流组件走到数据库,剩下3000个走服务降级,直接返回一个字符串
3.事后: 通过redis 的持久化快速 恢复缓存数据
# redis过期策略
redis有两种过期策略,定期删除和惰性删除
- 定期删除:redis每个100ms随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。
- 惰性删除:在获取某个key的时候,redis检查一下,如果该key设置了过期时间则判断该过期时间是否已经过期,如果过期了就直接删掉并不返回任何东西。
# redis淘汰机制
当redis内存快耗尽时,redis会启动内存淘汰机制,将部分key清掉以腾出内存。
redis提供6中数据淘汰策略,可在redis.conf中配置:maxmemory-policy noeviction
- noeviction:禁止驱逐数据。默认配置都是这个。当内存使用达到阀值的时候,所有引起申请内存的命令都会报错。
- volatile-lru:从设置了过期时间的数据集中挑选最近最少使用的数据淘汰。
- volatile-ttl:从已设置了过期时间的数据集中挑选即将要过期的数据淘汰。
- volatile-random:从已设置了过期时间的数据集中任意选择数据淘汰。
- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰。
- allkeys-random:从数据集中任意选择数据淘汰。
当Redis确定好要驱逐某个键值对后,会删除这个数据,并将这个数据变更消息同步到本地和从机。
# redis高可用 [Redis Cluster]
redis cluster
支撑N个redis master node,每个 master node (主节点) 都可以挂载多个 slave node (从节点)
读写分离的架构,对于每个master来说,写就写入master,然后读就从对应的slave去读
高可用,因为每个master都有salve节点,那么如果master挂掉,redis cluster这套机制,就会自动将某个slave 切换成master
redis cluster (多master + 读写分离 + 高可用)
如果数据量小,主要承载高并发场景,比如我的缓存一般就几个G,单机就够了
replication (主从复制) 一个master 多个slave,然后自己搭建一个 setinal(哨兵)集群,去保障redis的主从架构的高可用性就完了
# Redis Cluster 算法
redis cluster 有固定的16384 个 hash slot,对每个key计算 CRC16 的值,然后对16384进行取模,可以获取key对应的hash slot
redis cluster 的每个master 都会持有部分的slot,比如有3个master,那么可能每个master 持有5000多个 hash slot
hash slot 让node 的增加和移除都非常简单,增加一个master ,就将其他的hash slot 移动部分过去,减少一个master 就将他的hash slot 移动到其他master 上去
# Redis一致性HASH算法
一致性hash算法,就好比是个圆环, 然后这个圆环上 有多个master 节点, 然后有一个key过来,就是计算他的hash值,然后看这个key落在圆环的哪个地方,当key落到圆环上后,他会以顺时针的方向去寻找,离自己最近的一个master,吧数据存进去
然后每个master 都会给他均匀的分布 虚拟节点, 虚拟节点链接到真实master,这样的话,在每个区间,大量的数据都会均匀的分布到不同的节点上,而不是按照顺时针走全部涌入一个master上造成热点问题
# Redis扩容缩容
扩容:
启动新redis节点
将新节点加入集群(–cluster add-node)
为主节点分配槽位(–cluster reshard)
为主节点连接从节点(cluster replicate)
缩容:
1.删除从节点(–cluster del-node)
2.主节点槽位卸载(–cluster reshard)
3.删除主节点(–cluster del-node)
# Redis分布式锁
加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。
SET lock_key random_value NX PX 5000
//SET命令的参数
SetParams params = SetParams.setParams().nx().px(internalLockLeaseTime);
//SET命令返回OK ,则证明获取锁成功
String lock = jedis.set(lock_key, random_value, params);
if("OK".equals(lock)){
return true;
}
//否则循环等待,在timeout时间内仍未获取到锁,则获取失败
long l = System.currentTimeMillis() - start;
if (l>=timeout) {
return false;
}
值得注意的是:
random_value 是客户端生成的唯一的字符串。
NX 代表只在键不存在时,才对键进行设置操作。
PX 5000 设置键的过期时间为5000毫秒。
解锁我们通过jedis.eval来执行一段LUA就可以。将锁的Key键和生成的字符串random_value当做参数传进来。
如果redis 里面的key 对应的值 和自己随机出来的值一样,那就删掉,否则就不删
解锁: String lua =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
Object result = jedis.eval(lua, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
if("1".equals(result.toString())){
return true;
}
/* 总结: redis 加锁 ,就是给key键 设置一个随机值, 还有两个参数 一个 NX,PX , NX代表只在键不存在时,才对key进行操作,键不存在设置成功会返回 true ,PX 代表过期时间, 防止死锁.比如我PX 设置 30s 过期,30s后这个key就在redis没了
然后当我多个客户端获取锁的时候,只有一个set值能成功,其他客户端就会set失败,然后会进行轮询,比如每隔1s在次尝试获取锁,当客户端1逻辑执行完后, 释放锁, 我们是通过lua 脚本 将 key 和 随机值 放进去比较, 如果lua 查到, redis 里面对应的key 和值 是我们传给他的 ,他就会吧这个key 删了,这样其他客户端就能拿到锁, 如果对应key 的 随机值 不是我们传进去的 那就代表 其他客户端已经拿到锁了,lua就不会删除*/
# 分布式锁的特性
1.互斥性:这个应该是分布式锁的基本特性,或者说也是锁的基本特性,保证资源(这里指共享代码段或者共享变量)同时只能有一个节点的某个线程访问;
2.可重入性:类似于ReentrantLock,同一服务节点的相同线程,允许重复多次加锁;
3.锁超时:特性与本地锁一致,一旦锁超时即释放拥有的锁资源;
4.非阻塞:支持获取锁的时候直接返回结果值(true or false),而不是在没有获取到锁的时候阻塞住线程的执行;
5.公平锁与非公平锁:公平锁是指按照请求加锁的顺序获得锁,非公平锁请求加锁是无序的。
# HashMap
# JDK1.7 和 1.8的区别
JDK1.7 JDK1.8
数据结构 数组+链表 数组+链表+红黑树
数组元素 Entry Node
容量初始化时间 创建时 首次put时
hash冲突元素添加位置 链首 链尾
链表死循环情况 在 不存在
# HashMap的底层数据结构
HashMap 底层就是数组
通过put 添加数据, 根据key计算hash值得到插入数组的索引(index)
通过get(key) 进行取数据
取数据的时候会对 key 计算出来一个hash 值,然后根据这个 hash值对数组进行取模, 就会定位到数组里面的元素 (index)
# JDK 1.8中的hash 算法是如何优化的
例: 这是"张三"的hash code
1111 1111 1111 1111 1111 1010 0111 1100
h>>>16 对 hash code 进行二进制运算 往右移动16位 结果
0000 0000 0000 0000 1111 1111 1111 1111
(h=key.hashCode()) ^ (h>>>16) ^ 代表 将他们异或运算 ^ 运算代表:如果两个值相同 那就是0 不同那就是 1 结果为
1111 1111 1111 1111 0000 0101 1000 0011
那么: 1111 1111 1111 1111 1111 1010 0111 1100 //正常hashcode
0000 0000 0000 0000 1111 1111 1111 1111 //位运算16位的hashcode
1111 1111 1111 1111 0000 0101 1000 0011 //异或后的hashcode
// 这个方法的逻辑就是对这个hash值 ,在他的低16位中,让高低16位进行了异或,让他的低16位同时保存了高低16位的特征,尽量避免一些hash 值后续出现冲突,大家可能会进入同一个数组的情况
寻址算法的优化
// & 与运算 两张都为1 时结果才为1 否则就是0
使用 hash&(n-1) ---> 效果跟对 hash 对n 取模,效果是一样的,但是运算性能要比 hash 高很多
总结:
//1. hash算法的优化:对每个hash值,在他的低16位中,让高低16位进行了异或,让他的低16位同时保存了高低16位的特征,尽量避免一些hash 值后续出现冲突,大家可能会进入同一个数组的情况
//2.寻址算法的优化 :用与运算替代取模,提升性能
# hash冲突怎么解决
hash 冲突问题 , 链表+红黑树 O(n) 和 O(logn) // O(n) 数量增大几倍,耗时也增大几倍 冒泡排序
// O(logn) 当数据增大n倍时,耗时增大logn倍(例:log是以2为低,比如当数据增大256倍时,耗时只需要增大8倍) 二分查找 就是O(logn) 算法,每找一次排除一半的可能,256个数据中查找只要找8次就行了
什么是hash 冲突:
两个key 或者多个key ,他们算出来的hash 值 &(n-1), '与运算之后' 发现定位出来的数组位置还是一样的, 这就是hash 碰撞,hash冲突
解决办法: 会在这个位置挂一个链表,这个链表里面放多个元素,让多个key-value 对 ,同时放在数组的一个位置里面
get,如果定位到数组的这个位置里面挂了一个链表,此时就去遍历链表,从里面找到自己要的那个key-value
假设链表很长,可能会导致遍历链表,性能就比较差
优化: 如果链表的长度达到一定长度后('大于等于8'),会吧链表转化成红黑树,遍历红黑树找一个元素性能会高一点
# hashmap扩容机制
新容量 = 旧容量 x2
新阈值 = 新容量 x 负载因子 // 负载因子 默认0.75
说明: HashMap使用HashMap(int initialCapacity)初始化,
初始化容量计算: initialCapacity = (需要存储的元素个数 / 负载因子) + 1 // 如果不设置存储元素的个数, map的默认大小为 16
# JAVA并发
# synchronized关键字底层实现原理
synchronized 就是加锁 ,可以给对象,类加锁.
synchronized(myObject){
//一堆代码
synchronized(myObject){
//一堆代码
}
}
多个线程中,一个线程拿到锁后,执行对应的代码逻辑,其他线程就在阻塞等待,等线程1执行完后,才能在拿锁执行逻辑
简单点说,就是一个线程拿锁其他线程就等待执行
其实 synchronized 的底层原理是跟jvm指令和monitor 有关系的
如果我们用到了 synchronized 关键字,在底层编译后的jvm指令里面 monitorenter 和 monitorexit
他里面的原理大概是这样的: monitor里面有一个计数器 从0开始的。 如果一个线程要获取monitor的锁,就看他里面的程序计数器是不是0,是0的话,那说明没人获取锁,那他就可以获取到锁了,然后对计数器加1 ,当他执行完所有的逻辑后释放锁后,他会吧计数器减1 // 加几次锁计数器 就加几个1 释放一样 释放几个锁就减 几个1
# CAS的理解及其实现原理
CAS 简单点来说就是 比较并交换 // 乐观锁
//多个线程来执行这块代码
例: AtomicInteger i = new AtomicInteger(0);
public void increment(){
i.incrementAndGet();
}
// 线程1 会先读取 当前值是0 , 在设置时,会确认当前值 是否还是0 如果是0 那就累加 替换 如果不是则操作失败
// CAS 在底层硬件级别给我们保证 操作一定是原子的,同一时间只有一个线程可以执行CAS 先比较在设置操作
# ConcurrentHashMap实现线程安全的底层原理
JDK1.8 以前,吧元素存在多个数组里,然后进行分段加锁,一个数组一个锁
// 细化 对数组的每个元素 进行cas的策略 , 如果很多线程对数组里的不同元素执行 put操作 ,那么大家是没有关系的,如果某个线程执行失败了,那就代表这个位置刚刚有人放进去了,那就需要在这个位置基于链表+红黑树处理, synchronized(数组[1])加锁,基于链表或者红黑树插入自己的数据
# 你对 AQS理解吗 ? AQS 的实现原理?
AQS ,Abstract Queue Synchronizer 抽象队列同步器
ReentranLock
state变量 ---> CAS --->失败后进入队列等待--->释放锁后唤醒
// 两个线程 同时对 state 进行cas 操作, 线程1 加锁成功, 线程2就会进入 等待队列等待, 当线程1解锁后,会唤醒线程2 ,然后线程2在去CAS 加锁, 在执行
ReentranLock 默认是非公平锁
例: 线程1 将线程2 唤醒后, 冒出来了个线程3 直接 进行cas 加锁成功, 那线程2 又得进入等待队列等待
ReentranLock lock = new ReentranLock(true); // new锁的时候 ,构造函数里面加个true ,就是公平锁, 公平锁就是,线程1 将线程2 唤醒后, 冒出来个线程3 线程3看到等待队列里面有人后 ,会进入等待队列等待,排队,等线程2将他唤醒
# 说说线程池的底层工作原理可以吗
我们的系统不可能说让他无限制的去创建很多很多线程的,我们会构建一个线程池,有一定数量的线程,让他们执行各种各样的任务,执行完后不要销毁自己,继续去等待执行任务
ExecutorService threadPool = Executors.newFixedThreadPool(3);
提交任务,先看一下线程池里面的线程数量是否小于 corePoolSize(核心线程) ,如果小于那就会直接创建一个线程来执行任务,当线程执行完后,他不会死掉,他会尝试在一个无界的 LinkedBlockingQueue 里面获取新任务,如果没有新任务此时会阻塞住,等待新任务的到来, 如果所有的线程都在执行任务,那么无界队列里的任务就会越来越多
创建线程池的四种方法
1.Executors.newCachedThreadPool()//创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程
2.Executors.newFixedThreadPool(3)//创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中
3.Executors.newSingleThreadExecutor()//创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务
4.Executors.newScheduledThreadPool()//创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
所有线程池都调用了这个类
ThreadPoolExecutor(int corePoolSize 核心线程数,
int maximumPoolSize 最大线程数,
long keepAliveTime 最大存活时间,
TimeUnit unit 时间单位,
BlockingQueue<Runnable> workQueue 队列, // 4个实现类 直接提交的任务队列(SynchronousQueue),有界的任务队列(ArrayBlockingQueue),无界的任务队列(LinkedBlockingQueue)优先任务队列(PriorityBlockingQueue)
ThreadFactory threadFactory 线程工厂,
RejectedExecutionHandler handler 拒绝策略)//5个拒绝策略 AbortPolicy 该策略是线程池的默认策略。列满了丢掉这个任务并且抛出RejectedExecutionException异常, DiscardPolicy 满了就丢 , DiscardOldestPolicy 丢弃最老的。也就是说如果队列满了,会将最早进入队列的任务删掉腾出空间, CallerRunsPolicy 如果添加到线程池失败,那么主线程会自己去执行该任务, 自定义
# 如果在线程中使用无界阻塞队列会出现什么问题
面试题:在远程服务异常的情况下,使用无界阻塞队列,是否会导致内存异常飙升
服务异常,调用超时,队列会变的越来越大,此时系统内存就会无限飙升,而且还有可能会OOM 内存溢出
# 如果线程池队列满了之后会出现什么问题
1.corePoolSize:10
maximunPoolSize:Integer.MAX_VALUE //2 147 483 647
ArrayBlockingQueue(200) // 如果队列是有界的, 最大线程数是无限的, 队列满了那就会无限创建新的线程 ,去消费队列,当队列消费完,线程存活时间达到最大存活时间会自行销毁 ,如果队列任务非常非常多的话, 创建线程也会消耗内存,和CPU资源,有可能造成CPU资源飙升导致系统崩溃
// 如果队列是无界的,就有可能内存溢出OOM
// 如果 队列是有界的,最大线程数也是有限的 例如 核心线程10个 最大线程20,队列200, 当队列超后,如果没有配置拒绝策略, 走默认的那就会报异常(RejectedExecutionException) 我们可以自定义拒绝策略, 将超出的队列任务,持久化写入磁盘里去,后台专门启动一个线程,等待后续线程池的工作负载降低了,他可以慢慢从磁盘读取之前持久化的任务,重新提交到线程池里面去执行
# 如果线上的机器突然宕机,线程池中的阻塞队列怎么办
如果线上机器宕机,那我们线程池里面挤压的任务实际肯定是丢失的
// 解决方案: 我们要提交一个任务到线程池里面去,在提交之前,我们在数据库插入这个任务的信息,更新他的状态 : 未提交,已提交,已完成当系统宕机后,重启后,执行一个线程读取,未提交 ,已提交的任务 吧他们重新提交到线程池里面去执行
# java 的内存模型
// read load use assign store write
1. java 内存分为主内存 ,和工作内存, 工作内存就是CPU级别的缓存
线程在工作的时候,
1.会先从主内存中read(读)出数据
2.然后在load(加载)进工作内存里,
3.线程在use(用)工作内存的数据做逻辑计算,
4.做完计算后在 assign(更新分配)到工作内存
5.更新到工作内存后,他会store(尝试写入到主内存里)
6.最后就是write(写入)到工作内存
# 分布式
# CAP理论,AP与CP的差异
CAP:C(一致性),A(可用性),P(分区容错)
# AP:
当网络分区出现后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。
结论:违背了一致性C的要求,只满足可用性和分区容错,即AP
# CP:
当网络分区出现后,为了保证一致性,就必须拒绝请求,否则无法保证一致性。
结论:违背了可用性A的要求,只满足一致性和分区容错,即CP
# 分布式事务的解决方案
# 1.两阶段提交 / XA 解决方案
//1.第一个阶段 提交请求(投票)阶段
// 参与者执行事务中包含的操作,并记录undo日志(用于回滚)和redo日志(用于重放),但不真正提交。
//参与者向协调者返回事务操作的执行结果,执行成功返回yes,否则返回no。
//2.提交(执行)阶段
/*
分为成功与失败两种情况。
若所有参与者都返回yes,说明事务可以提交:
协调者向所有参与者发送commit请求。
参与者收到commit请求后,将事务真正地提交上去,并释放占用的事务资源,并向协调者返回ack。
协调者收到所有参与者的ack消息,事务成功完成。
若有参与者返回no或者超时未返回,说明事务中断,需要回滚:
协调者向所有参与者发送rollback请求。
参与者收到rollback请求后,根据undo日志回滚到事务执行前的状态,释放占用的事务资源,并向协调者返回ack。
协调者收到所有参与者的ack消息,事务回滚完成。
阿里的 Seata AT 模式
*/
# 2.TCC解决方案(Try Confirm Cancel)
TCC 这个其实用到的就是补偿机制
1. 第一阶段,try阶段, 这个阶段说的就是对各个服务的资源,做检测,以及对资源进行锁定或者预留 // 例: 银行转账 A 给B 转账 在try 阶段,会吧两个账户的资金进行冻结住就不让操作了
2. 第二阶段,Confrim 阶段, 这个阶段就是在各个服务中执行实际的操作 //A 给B 转账 A扣钱 B加钱
3. 第三阶段,Cancel阶段,如果如何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经成功的业务逻辑回滚操作 // A给B 转账 A扣钱了 B钱没加 ,那就得吧A扣掉的钱在加回去
'这种方法不常用,需要自己去写事务的回滚逻辑,代码耦合性极强, 除非对系统的一直性要求极高,而且还是系统中非常核心又核心的场景,那就可以用TCC'
# 是否清楚降级、流控、熔断。
降级:就是比如,访问系统,发现mysql 数据库服务挂了,自动开启服务降级, 所有的请求就不走数据库了,直接从内存中读取少量缓存直接返回
流控:就是比如突然1s 中来了一百万个请求, 然后丢了90w 只有10w才能进入系统
熔断:系统后端的一些依赖,出了一些故障,比如mysql挂了,每次请求都报错,熔断了后面来的请求就都不接收了,直接拒绝访问,10分钟之后在去尝试看看 mysql恢复没有
# JVM
# JVM的内存模型
JVM的内存结构大概分为:
方法区(Method Area):线程共享。存储类信息、常量、静态变量、即时编译器编译后的代码。
方法栈(JVM Stack):线程私有。存储局部变量表、操作栈、动态链接、方法出口,对象指针。
本地方法栈(Native Method Stack):线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。
堆(Heap):线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。
程序计数器(Program Counter Register):线程私有。较小的内存空间, 当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响;
# 垃圾回收算法
# 标记-清除算法
最基础的算法,后续的算法都是由此改进的,分为标记,清除两个步骤:
- 首先标记出需要被回收的对象。
- 然后对所有被标记的对象进行回收。
缺点:
- 效率:标记和清除的效率都不高。
- 空间:在清除之后内存中会产生大量的碎片,后续程序运行中很可能因为连续空间不足而不是实际可用空间不足而提前触发另一次收集动作。
# 复制算法
复制算法可以解决标记-清除算法的效率问题,步骤:
- 将可用内存按容量划分成大小相等的两块,每次只使用其中的一块;
- 当一块的内存用完时,就把还活着的对象复制到另一块上面;
- 然后再把已使用的内存一次性清楚干净。 这样使得每次都是对其中的一块内存回收,内存分配时也不用考虑内存碎片问题,只要移动堆顶指针即可。 缺点:
- 空间:可用的内存为原来的一半。
# 标记整理算法
复制算法在对象存活率较高时就要执行较多的复制操作,效率较低。重点是为了应对100%存活的极端情况,必须有像HotSpot虚拟机中幸存区-老年代这样另一块内存空间对自身进行分配担保。而老年代显然不能直接使用复制算法。 标记-整理算法是根据老年代的特征而产生的算法,步骤:
- 标记出所有需要被回收的对象。
- 将存活的对象一端移动。
- 清理存活边界以外的内存。
# 分代收集算法
当前商业虚拟机都采用这种算法回收。并没有引入新的回收逻辑,而是根据对象的存活周期把内存划分成几块,一般是新生代和老生代。这样就可以根据各个年代的特点采用合适的算法进行垃圾回收。
- 在新生代中,对象存活周期较短,朝生夕死,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
- 在老年代中,对象的存活率较高,没有额外的空间对它进行分配担保,必须使用“标记-清除”或“标记-整理”算法,
# RabbitMq
# rabbitMq的6种工作模式
RabbitMQ 一共有6种工作模式
简单模式:
一个生产者一个消费者,生产者发送消息 消费者接收消息
工作模式 work
一个生产者,多个消费者,生产者发送消息,多个消费者轮流接收消息
订阅模式 PubSub
生产者发送消息给交换机,需要设置 faout类型的交换机 并且交换机要和队列进行绑定,当生产者发送消息给交换机,交换机会将消息发送给绑定的队列
Routing路由模式
生产者发送消息给交换机,需要设置direct类型的交换机,交换机和队列绑定并指定 routing key 当消息发送到交换机时,交换机会根据routing key 将消息发送给对应队列
Topic 通配符模式
生产者发送消息给交换机,需要设置topic类型的交换机,交换机和队列绑定,并指定routing key ,topic 和direct 相比他们都是根据routing key 将消息发送给队列,
但是topic 可以在绑定routing key 的时候使用通配符 通配符 有两种: "#" 和 "*" #代表能匹配一个或多个词 例: item.insert.abc 或 item.abc *则代表只能匹配一个词 例: item.abc
# rabbitmq 如何保证消息的有序性
1.rabbitMQ 保证消息的顺序性很简单
保证一个 queue(队列) 只对应一个 consumer (消费者)
然后将要保证顺序的消息,发送到这一个队列里,让一个消费者消费
而不是多个消费者消费,就解决了顺序性问题
# 1.消息可靠性保障,消息补偿(如何补偿,重试,监控等机制实现最终的数据一致性。)
1.生产者将业务数据写入库
2.发送消息给队列1
3.延迟发送消息给队列3 //TTL+ 死信队列 = 延迟队列
4.消费者,监听到队列1消息,进行消费
5.消费者消费完成后,向队列2 发送确认消息, 代表消费成功
6.回调检查服务,监听队列2,收到消费成功
7.将消息写入数据库
8.回调服务监听到队列3的延迟消息, 将队列3消息的ID 去数据库中查询看是否存在, 存在代表这条消息消费成功,不存在那就调用生产者重新发送这条消息
//这样可能还是会有问题,就是在生产者发送消息, 和延迟发送消息都挂掉了, 这样系统就出现问题了
9.定时检查服务,检查业务数据的库里面的数据 和 消费库里面的数据是否一致, 业务库比消费库里面的数据多,那就调用生产者重新发送消息
# 消息幂等性保障--乐观锁
消息幂等性,可以使用数据库的乐观锁机制,加一个字段,version 版本号
例如: 每次修改时, version = version+1 where version = ?
这样即使有多条重复数据在mq里,我数据库消费只会消费一条
# Mysql
# Mysql事务的特性 以及隔离级别
MYSQL : 事务的特性
1.原子性 要么全部成功要么全部失败 例: 两个银行账户 A,B A给B 转账要么就是 A的钱少了 B的钱多了, 要么就不变
2.隔离性 就是多个事务直接是不会相互影响的 例: A,B,C A给B 转账 B给C 转账 这两个事务是没有影响的
3.一致性 一致性就是事务执行的前后状态要一直 例 A,B AB两个账户加起来是100块 那么A给B 转账完两个账户还是100块
4.持久性 一旦事务提交,那么这个事务的状态就会持久在数据库中
MySQL事务隔离级别
读未提交:第一个事务还未提交,另一个事务就可以读取,导致脏读。
提交读(不可重复读):一个事务未提交对其他事务不可见,但是会产生幻读和不可重复读。
可重复读(mysql默认隔离级别):保证同一个事务下多次读取的结果一致,但是会产生幻读。幻读:('事务A 按照一定条件进行数据读取, 期间事务B 插入了相同搜索条件的新数据,事务A再次按照原先条件进行读取时,发现了事务B 新插入的数据 称为幻读')
可串行化:严格的串行阻塞,并发能力不好。
# 索引有什么用?如何建索引和用索引
1.1 索引的目的在于提高查询效率,可以类比字典我们只要在字典的相关拼音就能查询到某个字的具体页数
1.2 alter table 表名 add index 索引名(列名, ..)
1.3 匹配最左前缀
列的精确匹配