List和Set的区别
ArrayList和LinkedList的区别
ArrayList:底层是Object数组实现的,由于数组的地址是连续的,数组支持随机访问,数组在初始化的时候,需要指定容量,数组不支持动态扩容。像ArrayList,Vector和Stack使用的时候,看似不用考虑容量的问题,,但是他们的底层实际都做了扩容,数组的扩容代价比较大,需要重新开辟一个新数组,将数据拷贝进去,数组扩容效率低。
ArrayList:适合读数据较多的场合
LinkedList:底层使用一个Node数据结构,有前后两个指针,双向链表实现的,相对数组,链表插入效率较高,只需要更改前后两个指针即可。另外,链表不存在扩容问题,因为链表不要求存储空间连续,每次插入数据都只是改变last指针,另外,链表所需要的内存比数组要多,因为他要维护前后两个指针。
LinkedList:适合删除,插入较多的场景,另外LinkedList还实现了Deque接口
数组和链表的特性差异:本质是,连续空间存储和非连续空间存储的差异。
Set集合父接口是Collection,Set集合底层实现的是Map集合,其中Set实现的是Map中的Key值,所以Set集合中的元素是不允许重复的,同时也是只能有一个null值,Set集合常用到的集合有HashSet、TreeSet
HashSet
TreeSet
Set集合如何去重的:
这是因为用到了HashCode()和equals(),这两个方法去决定的
步骤:
Map常见实现类有哪些
HashMap–继承了AbstractMap接口
数据结构
HashMap在put数据的时候,在底层代码会先判断put的值是否为null。如果为null,会固定存放在table[0]下面。如果不为null,会通过hash()方法计算对应的index地址,通过hash地址去寻找数据对应存放的table指定索引下。找到之后,会判断put的key在链表中是否存在,如果存在则为替换,如果不存在,则为新增
执行流程:
使用HashMap可能会遇到什么问题,如何避免
JDK1.7中,HashMap在并发场景中,可能出现死循环的问题,这是因为HashMap在扩容的时候,会对链表进行一次倒叙处理,假设两个线程同时执行扩容操作,第一个线程执行B->A,第二个线程又执行了A->B,这个时候就会出现B->A->B的问题,造成死循环
升级JDK版本。JDK8之后扩容不会进行倒序,因此死循环问题得到了极大的改善,但是如果是多线程,高并发的环境下,可以使用ConCurrentHashMap替代HashMap
TreeMap–继承了AbstractMap,实现了SortedMap
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-55ENsylx-1638325927282)(C:\Users\Mustang\AppData\Roaming\Typora\typora-user-images\image-20211125165332386.png)]
线程调度优先选择优先级最高的运行,但是如果出现以下情况,就会终止运行
线程调用了yield方法,让出了CPU的使用权,进入线程就绪状态
线程调用了sleep()方法,使其进入计时状态
线程由于IO受阻
另一个更高优先级线程出现
在支持的时间片系统中,该线程的时间片用完
两个方法都可以使线程进入等待状态
每个线程都具有优先级,一般来说,高优先级的在线程调度的时候,会具有优先被调用权,我们可以自定义线程的优先级,但是这并不能保证高优先级的在低优先级之前被调用,只是概率有点大。
线程优先级是1-10,1代表最低,10代表最高
java的线程优先级调度会委托操作系统去完成,所以具体的操作系统优先级也有关,如非特别需要,一般不去修改优先级
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LqD59xVH-1638325927286)(C:\Users\Mustang\AppData\Roaming\Typora\typora-user-images\image-20211125202447429.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wK0av4I0-1638325927288)(C:\Users\Mustang\AppData\Roaming\Typora\typora-user-images\image-20211125205257086.png)]
默认情况下,即使是核心线程也只能在新任务到达时才创建和启动,但是我们可以使用prestartCoreThread启动一个核心线程,或者prestartAllCoreThreads启动全部核心线程方法来提前启动核心线程
阻塞队列方法有四种形式,它们以不同的方式处理操作
抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 | |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
核心线程在获取任务时,通过阻塞队列的take()方法,实现的一直阻塞(存活)
通过利用阻塞队列的方法,在获取任务时,通过阻塞队列的poll(time,unit)方法实现的延迟死亡
虽然一直讲着核心线程和非核心线程,但是其实线程池内部是不区分核心线程和非核心线程的,只是跟根据当前线程池的工作线程数来进行调整,因此看起来像是有核心线程与非核心线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MOJGCc0v-1638325927290)(C:\Users\Mustang\AppData\Roaming\Typora\typora-user-images\image-20211125225742270.png)]
运行时异常
编译器不会检查他,就是说当程序中可能出现这种异常时,即使没有throws,或者try-catch也会编译通过。
编译时异常
java编译器会检查他,如果程序中出现此类异常,要么throws,要么try-catch,否则不能通过编译
CheckedException:编译器要求必须被处理的异常
UncheckedException:编译器不会进行检查,并且不要求必须处理的异常
习惯上,自定义一个异常类应该包含两个构造函数,一个无参构造函数,和一个带有详细描述信息的构造函数(Throwable的toString方法会打印这些详细信息)
一般来说,索引本身也很大,不能全部存储到内存中,因此索引网网以索引文件的形式储存在磁盘上
结构化的查询语言,是一种数据库查询语言。
模:模糊查询LIKE以%开头
型:数据类型错误
数:对索引字段使用内部函数
空:索引列是NULL
运:索引列进行四则运算
最:复合索引不按索引列最左开始查找
快:全表查找预计比索引更快
@RestControllerAdvice
public class SystemExe {
/**
* 说明: 需要为全局异常定义一个方法.
* 要求: 返回的统一的业务数据 SysResult
* 拦截: 指定遇到某种异常实现AOP处理.
* 注意事项: 在业务方法中不要随意添加try-catch
*/
@ExceptionHandler({RuntimeException.class})
public SysResult fail(Exception e){
e.printStackTrace();
return SysResult.fail();
}
}
(1)当需要事务控制的方法被同类方法调用时:Spring采用动态代理(AOP)实现对bean的管理和切片,它为我们的每个class生成一个代理对象。只有在代理对象之间进行调用时,可以触发切面逻辑。而在同一个class中,方法B调用方法A,调用的是原对象的方法,而不通过代理对象。所以Spring无法切到这次调用,也就无法通过注解保证事务性了。也就是说,在同一个类中的方法调用,则不会被方法拦截器拦截到,因此事务不会起作用。
解决方案:可以将方法放入另一个类,并且该类通过spring注入,即符合了在对象之间调用的条件。
(2)长事务导致数据库连接池中的连接被长时间占用,当并发度稍微高一些的时候有可能导致数据库连接池耗尽。会报CannotGetJdbcConnectionException的异常。
解决方法:使用编程式事务控制,编程式事务由开发人员手动控制事务的开启和提交以及回滚
1.基于底层的API,开发者在代码中手动的管理事务的开启、提交、回滚等操作。在spring项目中可以使用TransactionTemplate类的对象,手动控制事务
2.启动类添加@EnableAspectJAutoProxy(exposeProxy = true),方法内使用AopContext.currentProxy()获得代理类,使用事务。
用到的设计模式:代理模式(cglib动态代理和jdk自带的动态代理)
通知如何实现的:拦截器(具体的可以自己看看)
首先,spring是一个bean容器,用来管理bean的生命周期,其次,用于框架的集成
工厂模式:通过BeanFactory来创建Bean对象
单例模式:每个交给spring管理的Bean默认都是单例的
代理模式:Aop基于cglib动态代理来实现
观察者模式:SpringBoot启动过程中通过广播器发布事件和监听器监听事件来完成整个启动流程
Bean的创建:
(1)执行一系列Aware接口,向当前Bean中设置一些属性值,方便后续通过Bean直接获取,例如BeanName,BeanFactory,BeanClassLoader等。
(2)通过反射执行Bean的实例化
(3)执行Bean初始化前置处理,PostProcessBeforeInitialization方法
(4)执行Bean的初始化
(5)调用自定义Bean初始化方法
(6)执行Bean初始化后置处理,PostProcessAfterInitialization方法
Bean的销毁:
(1)执行Bean工厂关闭前置处理,PostProcessBeforeDestruction方法
(2)关闭Bean工厂
(3)执行Bean的失效方法
(1) 把sql从java代码中独立出来
(2) 封装了jdbc,基于orm方式简化了java对数据库的操作,并且将操作数据库的结果集自动转换成javabean对象
用来生产某一个唯一复杂对象,其中提供了3个方法getObject,getObjectType,getSingleton
context对象中保存的是某个作用域中包含的属性的值
整体大致流程:
(1)通过BeanDefinationReader接口的某一具体实现类Reader读取并解析Bean的定义信息
(2)将解析后的Bean的定义信息交给IoC容器
(3)BeanFactory从IoC容器中获取Bean的定义信息,通过反射来创建Bean对象,完成Bean的实例化。在获取Bean定义信息的过程中可通过BeanFactoryPostProcessor对Bean的定义信息进行修改或增强。
(4)执行Bean的初始化。在初始化前可以执行一系列BeanPostProcessor方法。
具体流程:
1.将当前启动类的字节码文件放入primarySource的LinkedHashSet中
2.将primarySource集合作为参数,new一个SpringApplication对象
3.给这个SpringApplication对象设置一些属性值:
(1)当前web应用的类型(有三种,reactive响应式,none,servlet,常见的是servlet)
(2)初始化器(不带自定义的有7个)
(3)监听器(不带自定义的有11个)
(这些初始化器和监听器都是从spring的jar包中的META-INF目录下的spring.factories文件中取出来的,取出来的仅仅是类的全路径,也就是包名加类名,方便后续通过Class.forName(类的全路径)利用反射来实例化这些对象)
4.执行run方法
(1)开启计时(用于记录启动时间)
(2)定义异常报告器
(3)从SpringApplication对象中读取监听器,并发布SpringBoot启动的事件
(4)监听器监听到启动事件,触发接下来的操作
(5)读取系统环境信息,并忽略掉 .ignore文件中的信息
(6)获取当前web应用的context上下文对象(后续会向其中设置属性值)
(7)设置当前web应用的异常报告器(刚才定义的)
(8)初始化上下文对象
i.向其中设置属性值(系统环境信息,类加载器信息,类型转换服务等)
ii.通过context对象获取BeanFactory,并注册单例对象(最常用的BeanFactory:DefaultListableBeanFactory)
iii.执行load方法,其中通过BeanDefinationReader的一个实现类对象AnnotatedBeanDefinationReader通过扫描@Component注解读取到当前启动类
(@SpringBootApplication注解一直往里点会出现@Component注解)
iiii.广播器发布事件:上下文对象准备完毕
5.refresh方法(整个启动流程的核心)
(1)开启SpringBoot容器(close属性设置为false,active属性设置为true)
(2)获取BeanFactory
(3)执行BeanFactory的准备工作,设置一些属性值
(4)配置Bean工厂的后置处理器,BeanFactoryPostProcessor(空方法,SpringBoot做了一点扩展)
(5)执行BeanFactory的实例化,并执行BeanFactoryPostProcessor
(6)向Bean工厂中注册BeanPostProcessor
(7)国际化配置 il8n
(8)初始化事件广播器
(9)onfresh方法获取Tomcat Server
(10)注册监听器
(11)完成BeanFactory初始化,并实例化所有非懒加载的Bean对象
(12)finishRefresh
1.防止用户重复提交:前端控制,用户点击注册按钮后将按钮变为一个等待图标,直到收到后端返回的数据。
2.用户点击注册按钮后,先到数据库中查找是否存在相同的用户名(邮箱),若不存在,则提示用户需要进行邮箱验证,用户点击确定后向用户注册的邮箱发送验证邮件,其中是一个激活用的url。不直接将用户信息入库以及不直接向用户填写的邮箱发送邮件是为了防止恶意脚本攻击。
3.邮箱激活(依赖,不可重入锁,失效机制):目的是防止用户输入的邮箱格式正确但实际不存在
注册的表单中有一个隐藏域,每次注册页面加载的时候会将一个基于UUID生成的字符串方到隐藏域中,提示用户需要进行邮箱验证,用户点击确定后,会将该字符串作为key,用户填写的注册表单信息作为value保存到redis中,并设置有效期,同时会将这个字符串传到后端,作为激活链接的一部分,激活链接就是基础url拼接上这个字符串,用户点击激活链接就是发起一个到redis中查询数据的请求,查的key就是那个UUID,若已过期,则提示用户重新注册,若存在,则读取将用户填写的注册信息并将redis中的这个key删除,然后将用户的注册信息添加到数据库中。
这里会出现几个问题:
(1)如果用户收到验证邮件后并没有点击激活,而是又重新注册了一遍,就会再向原来的邮箱发送一封验证邮件,如果紧接着用户点击了两封验证邮件中的激活链接,就会向数据库中插入两条用户名(邮箱)相同的记录。
(2)如果多个用户同时使用同一个邮箱进行注册,注册进度也几乎一致,都该向邮箱发送验证邮件了,此时这些用户都点击确定之后,在redis中就会产生多个记录,因为这些用户在注册时的UUID是不同的,但他们不会同时点击激活链接,因为同一时间只有一个人能登录那个邮箱,但邮箱中却会有多封邮件,就又回到了上面的问题。
问题的解决:
(1)首先在建表的时候会给用户邮箱的这个字段添加唯一约束,这样的话插入重复数据会产生异常,我们会将异常捕获处理后提示用户该邮箱已激活。
(2)其次,不再使用UUID,提示用户需要进行邮箱验证并且用户点击确定后,将用户邮箱使用MD5加密算法生成一个密文,加密时添加设定好的盐值,用这个密文取代之前的UUID,作为key保存到redis中,同样要设置有效时长,在保存之前,先到redis中查询是否存在这个key,若已存在,则提示用户已向该邮箱发送过验证邮件,若不存在,再发送验证邮件,之前激活链接中的UUID改成这个密文。
(3)最后,将发送邮件的请求放入RabbitMQ消息队列中。
其他问题:
(1)发不了邮件:主机邮箱需要开启SMTP,POP3,IMAP4服务,可以在邮箱设置中开启
(2)主机邮箱认证错误:主机用户名必须和邮箱名的@之前的内容一致,主机密码是开启SMTP服务后邮箱官方提供的授权码
(3)找不到MailLogger类:依赖导错了,将javax.mail的依赖包换成com.sun.mail的依赖包
4.分布式id(雪花算法)
64位Long类型二进制数字
第一位是符号位
之后41位是毫秒级的时间戳,可以表示69年的时间
之后10位自定义,比如定义为当前数据库所在的服务器号(假设为4位)和磁盘号(假设为6位)
雪花算法的实现主要是依赖于数据中心和数据节点id两个参数
token的存储,过期,刷新
认证
请求过滤和转发