面试八股加油背
2023-11-19 22:19:21 8 举报
AI智能生成
面试八股加油背,是求职者在面试前为了应对各种常见问题而进行的一种背诵练习。这种练习通常包括对自我介绍、工作经历、职业规划等问题的回答。面试官通常会问到这些问题,因此求职者需要提前准备好答案,以便在面试时能够流利地回答。此外,面试八股加油背还包括对一些常见面试问题的分析和解答技巧的学习。这些技巧可以帮助求职者更好地展示自己的优势,提高面试成功率。总之,面试八股加油背是一种有效的面试准备方法,能够帮助求职者在面试中脱颖而出。
作者其他创作
大纲/内容
排序算法
冒泡排序
通过双重循环来维护数据,内循环保证每次找到未排序的数组中最大的数,他是稳定的排序,时间复杂度是n^2
快速排序
对于冒泡排序的一种改进,每次找到一个基准数,设置两个指针分别在数组的左右两边,先从右向左遍历,找到一个
比基准数小的位置,再从左向右找到一个比基准数大的位置,然后交换两个数,继续遍历直到两个指针相遇,然后交换指针
和基准数的位置,此时基准数左侧都比基准数小,基准数右侧都比基准数大(也包含等于)是不稳定的
时间复杂度 nlogn
比基准数小的位置,再从左向右找到一个比基准数大的位置,然后交换两个数,继续遍历直到两个指针相遇,然后交换指针
和基准数的位置,此时基准数左侧都比基准数小,基准数右侧都比基准数大(也包含等于)是不稳定的
时间复杂度 nlogn
归并排序
将数据两两分组,每次将相邻的分组进行排序合并,层层向上合并最后完成,他的排序是稳定的,时间复杂度nlogn
杂项
数据的物理结构和逻辑结构有哪些?
逻辑结构
树形
图形
集合
线性
物理结构
链式
顺序
进程间通信的方式
管道
信号量
消息队列
共享内存
socket套接字
阻塞非阻塞和同步异步
阻塞和非阻塞
做一件事情能不能立即得到返回应答,如果不能立即获得返回,需要等待,那么就阻塞了,否则可以理解为非阻塞
阻塞是指一个服务调用另一个服务以后他只能够等待调用的服务完成再进行操作
非阻塞就是当一个服务调用了另一个服务以后它可以进行其他服务
同步和异步
做完一件事情以后再去做另一件,不管是否需要时间等待
异步就是可以同时做很多件事,并不一定需要一个做完以后再做另一个。在调用结束之后,通过消息回调来通知调用者是否调用成功。
同步是指一个服务调用另一个服务以后只能等待这个服务的结果完成后再进行下一步
异步就是通过异步的结果回调
1、同步阻塞:小明啥都不干等奶茶。
2、同步非阻塞:小明一边玩手机一边等奶茶。
3、异步阻塞:小明拿着小票啥都不干等着奶茶妹告诉他奶茶好了
4、异步非阻塞:小明拿着小票玩着手机等着奶茶妹告诉他奶茶好了
2、同步非阻塞:小明一边玩手机一边等奶茶。
3、异步阻塞:小明拿着小票啥都不干等着奶茶妹告诉他奶茶好了
4、异步非阻塞:小明拿着小票玩着手机等着奶茶妹告诉他奶茶好了
设计模式
单例模式
构造方法私有,由类来保证唯一的实例
懒汉式
时间换空间,在第一次访问实例的时候创建
有线程安全的问题
双重检查机制,锁住部分方法体
synchronized关键字来异步锁住方法
使用静态内部类实现懒汉式
创建一个内部类Holder,在内部类中定义实例,由于内部类只有在被调用时才被加载,
用JVM实现了懒汉式,并且没有线程安全的问题
用JVM实现了懒汉式,并且没有线程安全的问题
饿汉式
用空间换时间,在类加载时就创建
没有线程安全问题
枚举类实现饿汉式
观察者模式
定义一种一对多的关系,当一个对象的状态发生改变时,会通知所有依赖于它的对象并自动更新
又称为发布订阅模式
代理模式
静态代理
有一个代理对象和目标对象,目标对象实现具体的业务,两个对象实现同一个接口,业务在调用时调用
代理对象的方法,代理对象内部有一个目标对象的属性,在执行方法时执行目标对象的方法,同时加上业务
代理对象的方法,代理对象内部有一个目标对象的属性,在执行方法时执行目标对象的方法,同时加上业务
动态代理
JDK动态代理
要求传入的对象实现了接口
创建一个ProxyFactory来根据目标对象去生成一个代理对象
,ProxyFactory通过java.lang.reflect.Proxy类的newProxyInstance方法
传入目标对象的类加载器,对象实现的接口,以及一个InvocationHandler的匿名内部类
实现他的invoke方法,来调用他的方法并且在中间增加事务
,ProxyFactory通过java.lang.reflect.Proxy类的newProxyInstance方法
传入目标对象的类加载器,对象实现的接口,以及一个InvocationHandler的匿名内部类
实现他的invoke方法,来调用他的方法并且在中间增加事务
CGlib动态代理
基于子类来实现,传入的目标对象类不能是final修饰
当目标对象没有实现接口时使用CGlib动态代理
工厂模式
简单工厂模式
通过静态方法传递不同的参数实现创建不同的对象
1、一个目标对象被多个 观察者观察 List<Observer> observers;
2、目标提供对观察者注册和退订的维护 attach(Observer observer)、detach(Observer observer)
3、当目标的状态发生变化时,目标通知所有的注册的观察者 notifyObservers()方法通知所有观察者
2、目标提供对观察者注册和退订的维护 attach(Observer observer)、detach(Observer observer)
3、当目标的状态发生变化时,目标通知所有的注册的观察者 notifyObservers()方法通知所有观察者
工厂模式
通过设置抽象工厂,让工厂类符合开闭原则,工厂类都实现工厂接口,在创建工厂时创建抽象的引用,new出具体的实现类,来实现开闭原则,
在扩展时不需要修改业务代码,只需要更改new出的具体的工厂对象即可
在扩展时不需要修改业务代码,只需要更改new出的具体的工厂对象即可
缺点是对于每一种产品都需要创建对应的工厂,使项目中的类的数目增多
抽象工厂模式
抽象工厂就是在工厂模式的基础上增加了产品簇的概念,就是对于一个工厂,他不但能生产一种类型的产品,它有多个
create方法去创建不同类型的产品去形成一个产品簇
create方法去创建不同类型的产品去形成一个产品簇
设计模式的原则
开闭原则
对于修改关闭,对于扩展开发,意思就是在扩展程序的功能时
应该尽量少的去修改原来的代码,而是在设计时提供灵活的扩展
方式
应该尽量少的去修改原来的代码,而是在设计时提供灵活的扩展
方式
比如对于工厂方法模式,就是扩展产品只需要编写对应的产品工厂以及产品类,在创建时修改具体的实现类
就可以扩展能添加的产品
就可以扩展能添加的产品
当软件发生变化的时候,通过拓展来实现变化而不是通过修改实现变化,因为原有的功能可能被其他类所引用,如果修改的话会
影响其他类的使用
影响其他类的使用
迪米特法则
最少知道原则
类与类之间的调用应该保持最少的了解
只是用public的方法进行调用
只是用public的方法进行调用
依赖倒转原则
核心就是面向接口编程,业务代码应该依赖于抽象而不是依赖于具体的实现类
也就是让实现依赖于抽象,而不是让抽象依赖于具体
也就是让实现依赖于抽象,而不是让抽象依赖于具体
里氏代换原则
子类应该可以替换掉父类
尽量少的去修改父类的方法,而是在继承时去扩展方法
git相关
git commit
提交当前的代码
git checkout
切换分支
git checkout -b
创建分支并修改
git merge
将当前分支与另一个分支合并,将两个分支直接进行合并
merge 会将生成一个commit,这个commit是来自于两个分支的,也就是基于公共分支生成的
而rebase是基于当前分支生成的,在当前checkout分支的基础上生成commit
而rebase是基于当前分支生成的,在当前checkout分支的基础上生成commit
git rebase
也是合并,但是区别是,是基于当前分支合并
git reset HEAD^
撤销上一次commit
git revert
去撤销某一次的提交,生成一次新的提交,新的提交不包含撤销的提交的内容
git fetch
将云服务器的分支获取下来,但是不合并
git pull
将云服务器的分支获取下来并且merge合并
git push的流程
两种方式,一种是fetch将云服务器上别人的代码获得下来,然后git rebase进行合并
另一种方式是直接git pull 直接进行合并
git tag
为版本添加标签方便管理
git amend
修改上一次的提交但是不产生新的提交
git branch
创建分支
计算机网络
三次挥手四次握手
首先服务端转化为监听状态,客户端发送SYN报文,并且状态切换为SYN_SEND状态,服务端接收报文,状态由listen转化为
SYN_RECEIVE状态,服务端发送SYN+ACK报文,客户端接收后状态转化为establish,并且发送ACK报文,服务端收到报文后也转化为
establish状态
SYN_RECEIVE状态,服务端发送SYN+ACK报文,客户端接收后状态转化为establish,并且发送ACK报文,服务端收到报文后也转化为
establish状态
客户端发送FIN报文,状态转换为FIN_WAIT1,客户端收到报文后发送ACK报文,状态转换为CLOSE_WAIT,此时两台服务器中的双向连接已经断开,但是
服务器进程的数据还没有断开,在服务器数据传输完毕后,服务器发送FIN+ACK报文,表示断开,状态转化为LAST_ACK状态
客户端收到报文后发送ACK报文,在TIME_WAIT等待两个MSL(Maximum Segment Lifetime)指的是TCP报文段在网络上的存活时间后断开连接
服务端收到ACK报文后断开连接
服务器进程的数据还没有断开,在服务器数据传输完毕后,服务器发送FIN+ACK报文,表示断开,状态转化为LAST_ACK状态
客户端收到报文后发送ACK报文,在TIME_WAIT等待两个MSL(Maximum Segment Lifetime)指的是TCP报文段在网络上的存活时间后断开连接
服务端收到ACK报文后断开连接
TCP和UDP
- TCP是面向连接的,UDP是无连接的
TCP的连接是可靠的,UDP的连接是不可靠的
UDP传输效率高,适合传输视频直播这些对可靠性要求不是那么高的数据
TCP的效率低可靠性高,适合用于网络通信
如何保证传输的可靠性
流量控制
TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接收的数据
,如果接受端来不及接收数据,就提示发送方降低发送的速率,防止包丢失
,如果接受端来不及接收数据,就提示发送方降低发送的速率,防止包丢失
拥塞控制
当网络拥塞时减少数据的发送
校验和
超时重传
HTTP和HTTPS
HTTPS是在http的基础上加入了TLS认证,首先客户端生成随机数R1和客户端支持的加密方式发送给服务端
服务端生成随机数R2并选择加密方式,同时发送给客户端证书信息
客户端验证证书信息,生成随机数R3,并且将R1 R2 R3生成报文摘要,再用服务端的公钥对R3进行加密,发送给服务端
服务端再用私钥进行解密 ,然后双方用R1R2R3作为密钥进行对称式加密,进行后续的加密
服务端生成随机数R2并选择加密方式,同时发送给客户端证书信息
客户端验证证书信息,生成随机数R3,并且将R1 R2 R3生成报文摘要,再用服务端的公钥对R3进行加密,发送给服务端
服务端再用私钥进行解密 ,然后双方用R1R2R3作为密钥进行对称式加密,进行后续的加密
HTTP是无状态的,通过cookie保存用户的身份
HTTP是持续性的,一次连接以后可以一直通信
HTTP是持续性的,一次连接以后可以一直通信
javaSe相关
==和equals的区别
==如果修饰基本数据类型,那么比较的就是值是否相等,如果比较的是引用类型,就会比较堆的地址是否相同
而equals是Object类中的方法,只能比较引用类型,如果不进行重写,那么底层也是==,比较的还是引用类型
的地址,而子类可以对equals方法进行重写,比如String类,重写后比较的就是字符串是否相等
而equals是Object类中的方法,只能比较引用类型,如果不进行重写,那么底层也是==,比较的还是引用类型
的地址,而子类可以对equals方法进行重写,比如String类,重写后比较的就是字符串是否相等
抽象类和接口的区别
抽象类要被继承,接口要被实现
抽象类可以定义成员变量和成员方法,接口只能定义常量
抽象类可以有构造器,接口不能有构造器
反射的缺点
从性能角度来说
JVM无法对于反射的代码进行优化,所以反射的效率会比非反射的效率慢很多
内部暴露
反射允许代码一些普通代码无法执行的操作,比如访问对象的私有变量,这样就破坏了代码的抽象性
HashMap
put(K,V)方法
首先将Key和value封装成一个node对象,将key的值
使用哈希算法进行哈希运算,得到数组下标,然后根据数组下标
到hashmap对应的数组中查找,如果数组为空,那么将node对象存放在该数组的位置
如果数组不为空,那么一次从头开始使用equals方法对key的值进行比较,
如果返回是true,那么就将链表中对应的value值覆盖,
如果返回都是false,就将节点放置在链表的尾部
使用哈希算法进行哈希运算,得到数组下标,然后根据数组下标
到hashmap对应的数组中查找,如果数组为空,那么将node对象存放在该数组的位置
如果数组不为空,那么一次从头开始使用equals方法对key的值进行比较,
如果返回是true,那么就将链表中对应的value值覆盖,
如果返回都是false,就将节点放置在链表的尾部
get(K)方法
首先将Key进行哈希运算获得数组下标,然后通过下标查找数组,如果对应数组中
没有值,就返回null,如果有值就依次与节点的key执行equals方法,如果返回true,
就返回对应的value值,如果都是false,就返回null
没有值,就返回null,如果有值就依次与节点的key执行equals方法,如果返回true,
就返回对应的value值,如果都是false,就返回null
为什么一定要重写equals方法和hashcode方法
如果重写hashcode方法,那么所有的哈希返回值都不同,最终哈希表
会退化成一个数组
如果不重写equals方法,所有添加的节点的equals方法只要不同就会返回false,
最终他会退化成一个链表
会退化成一个数组
如果不重写equals方法,所有添加的节点的equals方法只要不同就会返回false,
最终他会退化成一个链表
hashmap如何保证唯一性
通过hashcode方法和equals方法保证,对于传入的key首先通过两次hashcode()运算再去取余他的容量
来获得对应的数组下标,如果对应下标没有数据,就将对应的node存储在位置,如果有数据就会执行equals方法
如果返回为true就覆盖对应的value值,如果为false,就在链表的尾端添加对应的node
来获得对应的数组下标,如果对应下标没有数据,就将对应的node存储在位置,如果有数据就会执行equals方法
如果返回为true就覆盖对应的value值,如果为false,就在链表的尾端添加对应的node
ConcurrentHashMap
jdk1.7
使用大小数组+链表的形式,每次锁只锁住整个小数组,不对大数组加锁,大数组初始
容量大小为16,大数组不会扩容,只有小数组会扩容,小数组使用扩容因子进行扩容,每次
扩容两倍,因为大数组不会扩容,所以他的并发度是固定的,整体比较臃肿
容量大小为16,大数组不会扩容,只有小数组会扩容,小数组使用扩容因子进行扩容,每次
扩容两倍,因为大数组不会扩容,所以他的并发度是固定的,整体比较臃肿
jdk1.8
采用和hashmap一样的数据结构,数组+链表/红黑树,在hashmap中加入了线程
安全的操作,使用node+cas+synchronized方式来保证安全
安全的操作,使用node+cas+synchronized方式来保证安全
初始化数组时使用cas锁
添加元素时,如果数组对应位置没有元素,就用cas自旋锁添加,如果有元素,就
对一个数组下标加锁,来实现线程安全
对一个数组下标加锁,来实现线程安全
hashmap和hashtable的区别
hashmap是线程不安全的,hashtable是线程安全的,因为hashtable底层使用了synchronized锁来保证线程
安全性,
安全性,
hashmap使用了数组+链表/红黑树,hashtable使用了数组+链表
hashmap允许null为键和value,hashtable不允许
hashmap效率高,hashtable效率低
lambda表达式
对于函数式接口,可以使用Lambda表达式来 简化
实现
实现
对于只使用一次的接口实现类,最开始使用外部类实现接口,然后使用静态内部类
然后使用局部内部类,然后使用匿名内部类,最后我们使用Lambda表达式来简化
函数式接口的实现
然后使用局部内部类,然后使用匿名内部类,最后我们使用Lambda表达式来简化
函数式接口的实现
必须是对函数式接口!
lambda表达式的简化
简化参数类型
简化括号
必须方法只有一个参数
简化花括号
必须是实现类只有一行
Stream流
流操作
获取流操作
对于集合对象使用对象.stream()
对于数组对象使用 Arrays.stream(数组)来创建
对于数组对象使用 Arrays.stream(数组)来创建
中间操作
filter,过滤集合中的元素
map讲流中的元素进行计算或转换
distinct对流中的元素进行去重
sorted对流中的元素进行排序
limit限制流的长度
flatMap将一个对象转换为另一个对象来作为流中的元素
最终操作
foreach遍历流中的元素
max min获取流中的最大值最小值
查找与匹配
anyMatch
allMatch
noneMatch
findAny
findFirst
为什么框架要用反射
使用反射可以动态的创建类和对象,比如可以通过协议xml或者properity文件
来读取信息来动态的创建
来读取信息来动态的创建
Java多线程相关
创建线程的方法
继承Thread类
调用start方法启动
实现Runnable接口
传入目标对象+Thread().start
实现Callable接口
通过线程池创建
线程的方法
线程停止
使用标志位flag来停止
利用次数让线程自动停止
不要使用stop方法来停止线程
要让线程在内部自动停止,而不是调用外部方法停止线程
sleep方法
让线程进入阻塞态指定的时间,每一个对象
都有一把锁,sleep不会释放锁
都有一把锁,sleep不会释放锁
join方法
合并线程,当当前线程执行完毕后其他线程才能执行,其他线程阻塞
yield方法
线程的礼让,让当前的线程进入到阻塞态
礼让不一定会成功,会让cpu重新调度
礼让不一定会成功,会让cpu重新调度
守护线程,通过setDaemo方法设置线程为守护线程
守护线程是否结束不会影响虚拟机,虚拟机也不会等待守护
线程结束后再结束,当前台线程结束后,虚拟机就会结束,
虚拟机结束后守护线程也会跟着结束
守护线程是否结束不会影响虚拟机,虚拟机也不会等待守护
线程结束后再结束,当前台线程结束后,虚拟机就会结束,
虚拟机结束后守护线程也会跟着结束
wait方法
wait方法时线程停止并且释放锁,进入到等待池中,直到其他线程调用notify/notifyall方法将他唤醒,它才会去重新申请锁资源,进入到就绪态和
运行态
运行态
wait方法和sleep方法的区别
sleep是Thread类下的方法,它可以在任何线程中使用,而sleep方法只能在线程同步有锁的场景下使用
sleep方法不释放锁执行后依然会对线程进行监控,而wait方法执行后会释放锁
线程的状态
创建态
就绪态
运行态
阻塞态
死亡状态
当线程执行wait方法时,需要用到锁(synchronized,否则会报错,wait后
会进入到等待队列中,同时释放锁,当其他线程notify唤醒这个线程时,会进入到
锁池,去请求锁,在获得锁后进入就绪态,对于synchronized锁住的代码块或方法,
调用时也会去锁池去申请锁,在申请到后继续
会进入到等待队列中,同时释放锁,当其他线程notify唤醒这个线程时,会进入到
锁池,去请求锁,在获得锁后进入就绪态,对于synchronized锁住的代码块或方法,
调用时也会去锁池去申请锁,在申请到后继续
wait和sleep方法的区别
wait是Object的方法,sleep是Thread类的方法
wait会释放锁,sleep不会释放锁,还是对线程进行监控,在达到要求的是时间以后
会进入到就绪态,wait会释放自己的锁资源,进入到等待队列中去,等待其他的线程
notify/notifyall唤醒它,再去获取锁,进入到就绪态再到运行态执行
会进入到就绪态,wait会释放自己的锁资源,进入到等待队列中去,等待其他的线程
notify/notifyall唤醒它,再去获取锁,进入到就绪态再到运行态执行
线程池
为什么要用线程池?
对于java中创建线程和销毁线程的开销特别大,如果一个jvm中创建过多的线程
那么可能会使系统资源不足,为了防止资源不足,采用一些方法来限制给定时刻处理的
目标请求数目,尽可能的减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程
的创建和销毁,尽量利用已有对象来进行服务
那么可能会使系统资源不足,为了防止资源不足,采用一些方法来限制给定时刻处理的
目标请求数目,尽可能的减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程
的创建和销毁,尽量利用已有对象来进行服务
线程池的执行流程?
首先会查看核心线程数量满没满,如果没满直接创建线程来执行任务
如果核心线程满了,那就将任务存放到阻塞队列中去,等待核心线程执行完毕后再创建线程执行
如果阻塞队列满了,那么就继续创建线程来执行任务,直到达到线程池规定的最大线程数
如果到达线程池规定的最大线程数,那么就直接执行拒绝策略(四种
线程池的创建
参数
核心线程数
也叫常驻线程数,一般情况下,不管有没有任务
都会一直在线程池中存活
都会一直在线程池中存活
最大线程数
线程池所能容纳的最大线程数
当活动线程达到这个值后,后面的新任务就会被阻塞
当活动线程达到这个值后,后面的新任务就会被阻塞
等待时间
线程闲置时的超时时长,
一般对于非核心线程来说,如果线程闲置的时间超过
这个时间,就会终止该线程
一般对于非核心线程来说,如果线程闲置的时间超过
这个时间,就会终止该线程
等待时间的单位
单位,TimeUnit枚举类,
阻塞队列
当核心线程数达到最大时,新任务会放在任务队列也就是阻塞队列中等待执行
线程工厂
一个接口,为线程池创建新线程
拒绝的策略
AbortPolicy(默认
直接抛出异常
CallerRunsPolicy
让调用者线程进行调用
DiscardOldestPolicy
抛弃等待队列中等待最久的任务,把当前任务尝试再次提交当前任务
DiscardPolicy
默默丢弃无法处理的任务,不抛出异常也不做任何处理
当提交的任务数大于阻塞队列的长度+线程池的最大线程数,时,出发拒绝策略
创建的方法
Executors类创建
newCachedThreadPool
根据需求创建线程池,可以扩容
newSingleThreadExecuto
一个任务一个任务的执行
newFixedThreadPool (int)
一池N线程
newScheduledThreadPool
通过ThreadPoolExecutor创建
指定参数创建线程池
为什么要使用ThreadPoolExecutor创建而不是Executors?
Executors创建的线程池,其中FixedThreadPool和SingleThreadExecut请求队列的
长度是Integer.MAX_VALUES,而CachedThread 和 ScheduledThread允许创建线程
的数量是Integer.MAX_VALUE,
请求队列过长会堆积大量的请求,而允许创建线程数过大会创建大量的线程,这都会导致
内存溢出 OOM,所以要通过ThreadPoolExecutor创建,来指定参数创建线程池
Executors创建的线程池,其中FixedThreadPool和SingleThreadExecut请求队列的
长度是Integer.MAX_VALUES,而CachedThread 和 ScheduledThread允许创建线程
的数量是Integer.MAX_VALUE,
请求队列过长会堆积大量的请求,而允许创建线程数过大会创建大量的线程,这都会导致
内存溢出 OOM,所以要通过ThreadPoolExecutor创建,来指定参数创建线程池
核心线程数设置
对于CPU密集型的任务,设置成CPU的数量 + 1
对于IO密集型的任务,设置成CPU数量 * 2
JUC并发编程相关
锁相关
悲观锁和乐观锁
乐观锁
乐观锁的实现方式
版本号机制
给数据表加上一个版本号字段,当数据被修改时,版本号会+1,
那么在提交数据时会检查数据表上的版本号和自己读取的版本号是否
一致,如果一致就提交,如果不一致就重新读取版本号进行更新操作
那么在提交数据时会检查数据表上的版本号和自己读取的版本号是否
一致,如果一致就提交,如果不一致就重新读取版本号进行更新操作
CAS算法
公平锁和非公平锁
公平锁:就是对于线程执行必须先来先执行,不允许插队
非公平锁:允许插队,比如对于一个3s的线程和一个3h的线程,可以让3s的先执行
不需要等待3h才执行
不需要等待3h才执行
synchronized和lock默认都是非公平锁,lock锁也可以设置成非公平锁
可重入锁
表示一个线程如果之前获得了锁,还可以再增加锁而不会产生死锁,加多少次锁就需要
解多少次锁
解多少次锁
ReentrantLock 和synchronized 都是 可重入锁
Lock锁
通过lock方法加锁通过unlock方法解锁,对于加锁和
解锁必须严格使用try catch finally代码块进行保护,对于加锁
必须有对应的解锁,防止产生死锁
解锁必须严格使用try catch finally代码块进行保护,对于加锁
必须有对应的解锁,防止产生死锁
ReentrantLock
ReentrantReadWriteLock
在这个类中又有ReadLock和WriteLock,来实现读锁
和写锁
和写锁
ReentrantReadWriteLock.ReadLock
读锁与读锁兼容与写锁互斥
ReentrantReadWriteLock.WriteLock
实现Lock接口
写锁与读锁和写锁都互斥
synchronized和lock(ReentrantLock)锁的区别
synchronized是java自带的关键字,lock锁是一个java类
synchronized会自动解锁,而lock锁需要手动解锁
synchronized不可以判断是否加锁,而lock有isLock方法判断是否加锁
synchronized锁的线程会一直等待,而lock锁有trylock()方法判断能否加锁,设定等待一定
时间后结束
时间后结束
synchronized适合少量同步的代码,lock锁适合大量同步的代码
synchronized和lock都是悲观锁,synchronized是非公平锁,lock默认是非公平锁,可以设置成公平锁
生产者和消费者问题
有一个生产者线程和消费者线程和一个资源类
执行过程
循环判断
为什么要用while循环判断,而不是用
if判断?防止虚假唤醒
if判断?防止虚假唤醒
修改资源
通知其他线程
volatile关键字
保证可见性
当一个线程修改一个共享变量时,另一个线程能够
读到修改的这个值
读到修改的这个值
不保证原子性
比如对于i++,就是不保证原子性的,i++分为三步,
1.读取i的值
2.将读取的值+1
3.将结果赋值给i
对于这种情况,当读取完i的值之后另一个线程完成了对
i的值的修改,就会导致结果赋给i时出现错误,不保证原子性
1.读取i的值
2.将读取的值+1
3.将结果赋值给i
对于这种情况,当读取完i的值之后另一个线程完成了对
i的值的修改,就会导致结果赋给i时出现错误,不保证原子性
AtomicIntege保证运算的原子性
禁止指令重排
对于单例模式的双重检查机制,如果发生指令重排可能
会导致检查发生错误,因此需要加volatile关键字
会导致检查发生错误,因此需要加volatile关键字
CAS,比较并交换
包括的三个位置
需要写入的内存地址
存入前他的预期值
需要存入的值
逻辑是,如果需要写入的内存地址是我要预期的值,那么就
存入需要存入的值,如果不是就一直自旋尝试,直到成功位置
存入需要存入的值,如果不是就一直自旋尝试,直到成功位置
是一个乐观锁
他要求必须是多核cpu
通过CPU来保证它的原子性
优缺点
优点是不需要进行线程状态切换,进行线程切换会消耗性能
缺点是他是自旋锁,也就是一直会循环判断浪费时间,
并且由于是基于cpu的操作,它只能保证一个共享变量的原子性
并且由于是基于cpu的操作,它只能保证一个共享变量的原子性
CAS锁的ABA问题
为什么要解决ABA问题?
对于某些场景下,ABA问题是不需要解决的,比如在买票卖票
的场景下,cas自旋锁自旋等待修改为预期值是,数值已经由A到
B再由B到A了,此时执行了修改语句,这种情况下相当于有人退票
另一个人把别人退的票抢到了,这种情况下是没有问题的
的场景下,cas自旋锁自旋等待修改为预期值是,数值已经由A到
B再由B到A了,此时执行了修改语句,这种情况下相当于有人退票
另一个人把别人退的票抢到了,这种情况下是没有问题的
而对于有些应用场景,ABA问题是不被允许的,就比如对于开保险箱的场景下,一个保险箱如果
已经被开启过又关闭又开启,那这种情况是不被允许的,就需要处理ABA问题
已经被开启过又关闭又开启,那这种情况是不被允许的,就需要处理ABA问题
如何解决ABA问题
最常用的方式就是使用版本号机制+CAS自旋锁,当版本号与修改前不一致
就废弃这次操作,重新执行
就废弃这次操作,重新执行
AtomicStampedReference就可以解决ABA问题,AtomicInteger类就存在ABA问题
死锁
死锁是一个互相抢占的过程,线程互相拥有其他线程所需要的资源
一直在阻塞请求,并且不释放
一直在阻塞请求,并且不释放
四个必要条件
互斥条件
请求和保持条件
不剥夺条件
环路等待条件
只要打破四个必要条件之一就能有效预防死锁的发生
在代码中避免死锁
ThreadLocal
作用
实现多个线程之间资源对象的线程隔离,对于不同的线程创建不同的资源对象,对于线程
内部,调用同一个资源对象
内部,调用同一个资源对象
因此它还有第二个作用,就是实现线程内的资源共享
应用场景
比如对于jdbc操作,当你需要在service层中执行两条SQL语句时,如果每条SQL语句都创建新的
Connection连接对象,那么就会导致无法实现事务,事务需要在同一个连接内进行开启和回滚,因此
就可以使用ThreadLocal来创建sql连接对象,方便一起进行事务的回滚和提交
Connection连接对象,那么就会导致无法实现事务,事务需要在同一个连接内进行开启和回滚,因此
就可以使用ThreadLocal来创建sql连接对象,方便一起进行事务的回滚和提交
具体的实现就是,当调用getConnection对象时,会先检查ThreadLocal对象中有没有Connection对象
如果有就代表该线程的之前的方法已经创建过连接,就直接返回连接,如果没有就创建新的连接存在ThreadLocal中
如果有就代表该线程的之前的方法已经创建过连接,就直接返回连接,如果没有就创建新的连接存在ThreadLocal中
如果这样的话如何关闭Connection呢,对于jdbc来说,以前每执行完sql就用finally关闭connection
而对于ThreadLocal创建的对象如果这样就没有意义了,因此要在service层中创建生成连接对象并用finally
在线程层面上去关闭连接
而对于ThreadLocal创建的对象如果这样就没有意义了,因此要在service层中创建生成连接对象并用finally
在线程层面上去关闭连接
原理
ThreadLocal底层做了一个ThreadLocalMap,类似于hashMap实现了线程的隔离
方法
set方法,ThreadLocal以自己做key,用set的对象做value
get,以自己做key,获取对应的value
remove,以自己做key,删除当前线程对应的相关信息
spring相关
AOP(面向切面编程
代理模式
静态代理
首先存在一个代理对象和目标对象,目标对象执行具体的方法,他们同时实现同一个接口,创建代理对象
代理对象中有目标对象的属性,代理对象实现方法时会先实现目标对象的方法,然后在实现方法前后去加入增强代码
比如权限控制,日志处理
代理对象中有目标对象的属性,代理对象实现方法时会先实现目标对象的方法,然后在实现方法前后去加入增强代码
比如权限控制,日志处理
动态代理
为什么要用动态代理而不用静态代理,对于静态代理需要创建大量的代理类,并且如果要维护代理的方法就需要该的代码很多,因此有了动态代理
JDK动态代理
JDK自带的代理方式,首先创建一个代理工厂类,然后在工厂类的静态方法中传入目标对象,就能获取一个接口的类型
的代理对象,通过调用代理对象来实现动态代理
的代理对象,通过调用代理对象来实现动态代理
工厂内部调用了java.lang.reflect.Proxy类的newProxyInstance方法,他会传入目标对象的类加载器,实现的接口以及一个
InnovationHandler的事件处理器,去实现他的invoke方法,通过调用method.invoke方法来实现目标对象的方法,在前后增加
增强的代码
InnovationHandler的事件处理器,去实现他的invoke方法,通过调用method.invoke方法来实现目标对象的方法,在前后增加
增强的代码
CGlib动态代理
JDK动态代理有一个限制就是只能为实现了接口的对象生成代理对象,CGlib动态代理通过生成子类代理对象实现
如果一个类是final修饰,那么就不能使用CGlib实现动态代理
如果一个类是final修饰,那么就不能使用CGlib实现动态代理
Spring中使用AOP
通过@Aspect注解来定义切面
设置通知的类型
@Before前置通知
@AfterReturning后置通知
有时候需要知道方法的返回值和执行结果,因此需要后置使用通知
@Round环绕通知
切面表达式,表示对于哪些返回值的那些类的哪些方法进行aop
最终通知
异常通知
PointCut切入点
表示需要切入的位置,某些类或者某些方法
IOC
控制反转
在程序中创建具体的实现类会使代码的耦合性太高,我们希望能够只面对接口编程,而不出现具体的实现类
将具体的实现类交给Spring容器去管理和创建
DI依赖注入
将Spring容器中的bean的中的属性进行赋值
注入的方法
构造器注入
setter方法注入
通过标签注入
@Autowired
根据接口的实现类进行注入
如果有多个实现类,就需要再加上@Qualifier注解来指定具体的实现类名称
@Resource
通过实现类的名称进行注入
MyBatis
MyBatis缓存
一级缓存
默认情况下只开启一级缓存,对于一级缓存只针对一个SqlSession而言,对于相同参数的相同SQL语句
MyBatis会存储他的结果,只针对同一个SqlSession,当一次会话结束后就会销毁
MyBatis会存储他的结果,只针对同一个SqlSession,当一次会话结束后就会销毁
二级缓存
针对namespace会创建一个缓存区,被多个SqlSession共享,当一个sqlsession结束后会将一级缓存的内容
存储到二级缓存中
存储到二级缓存中
默认不开启
动态SQL
常用的标签
if
where
foreach
choose
when
otherwise
set
为什么要用mybatis
mybatis它是对jdbc的封装
他可以很轻松的实现数据库单表和多表之间的操作
他有动态标签可以简化sql操作
有一级缓存和二级缓存来提高查询的效率
SpringMVC相关
SpringMVC的执行流程
首先请求会发送到一个DispatcherServlet,他是作为一个控制中心
然后接收到请求后,他会根据请求的路径去向HandlerMapping请求路径对应的方法
获得对应的Handler方法以后,他会去请求HandlerAdapter去执行对应的Handler方法,
Adapter找到对应的Handler方法执行完毕后会向DispatherServlet返回一个ModelAndView对象
Adapter找到对应的Handler方法执行完毕后会向DispatherServlet返回一个ModelAndView对象
收到以后,回去视图解析器将视图的路径解析到真实的路径,然后再通过View去渲染视图
最后返回接收到的数据通过response返回
Filter 和 Spring中的Interceptor的区别
Filter是servlet中的概念,他只能在web项目上使用
Interceptor拦截器是Spring的组件,除了web项目在其他的Spring项目中也能使用
Interceptor拦截器是Spring的组件,除了web项目在其他的Spring项目中也能使用
过滤器和拦截器的出发时间不同,过滤器先触发,在进入Servlet再进入Interceptor,再
进入Controller
进入Controller
拦截器的三个方法的执行时机
preHandler
在controller执行之前拦截
postHandler
在执行完controller方法之后执行
afterCompletion
在渲染完视图以后执行
spring的controller是单例的还是多例的?怎么保证并发安全
controller是单例的
阿里巴巴编码规范要求不要在controller里定义成员变量
如果一定要定义非静态成员变量,就通过通过注解@Scope(“prototype”),将其设置为多例模式
controller中可以使用ThreadLocal来实现controller中的成员变量
循环依赖问题
循环依赖的场景,为什么会有循环依赖问题
bean的获取/创建流程
首先他会从单例池中getBean去获取bean对象
如果bean对象中没有对应的bean对象就回去实例化bean
再对bean进行属性填充
再对bean进行初始化
检查是否已经创建了aop的代理对象,如果没有就调用后置处理去创建aop的代理方法放在单例池中
有的话就将半成品池中的代理对象放到单例池中,再创建
有的话就将半成品池中的代理对象放到单例池中,再创建
循环依赖要解决哪些问题
首先解决当A包含B,B包含A时,如何不循环创建
再解决aop代理对象所产生的问题,单例池中应该是代理对象而不是目标对象
循环依赖的解决流程
首先通过三级缓存 单例池 半成品池和工厂池来解决循环依赖问题,当A实例化以后,他会在工厂池中创建对应的factory(a),然后开始进行属性注入
当注入时需要beanB,那么开始创建beanB,首先实例化B,然后在工厂池中创建factoryB,然后注入beanA,此时通过factoryA,去调用aop的前置处理
去创建a的aop代理对象在半成品池中,然后将a的半成品注入到B中,然后完成B的初始化,初始化以后调用beanpostprocesser对应的aop后置处理创建
aop的代理对象放到单例池中去,然后删除工厂池中的factoryB,然后对A进行属性注入B(从单例池中),然后完成BeanA的初始化,同时检查有没有提前
生成aop的代理对象,有就将半成品池中的bean放到单例池中,然后删除factoryA,完成beanA的初始化
当注入时需要beanB,那么开始创建beanB,首先实例化B,然后在工厂池中创建factoryB,然后注入beanA,此时通过factoryA,去调用aop的前置处理
去创建a的aop代理对象在半成品池中,然后将a的半成品注入到B中,然后完成B的初始化,初始化以后调用beanpostprocesser对应的aop后置处理创建
aop的代理对象放到单例池中去,然后删除工厂池中的factoryB,然后对A进行属性注入B(从单例池中),然后完成BeanA的初始化,同时检查有没有提前
生成aop的代理对象,有就将半成品池中的bean放到单例池中,然后删除factoryA,完成beanA的初始化
spring异常处理
使用注解处理异常
在controller层上添加类上加上@restcontrolleradvice在里面的方法上加上@ExecptionHandler
如果浏览器禁用了cookie,那么还能够使用session吗
可以将sessionid追加到浏览器请求的url中
在浏览器发送的参数中携带sessionId
在http请求头中携带sessionId
redis
redis支持的数据结构
底层的实现原理?
普通字符串
不但是存储字符串,也能存储二进制数据,数字等等,可以使用方法将数字+1(文章点赞记录),可以接收任何格式的数据,图片也可以,最多
512M
512M
哈希类型map
用于实现个人信息的展示和购物车信息的展示
集合类型set
用于实现一些集合操作,并集交集差集,用来实现小程序抽奖(随机获取set集合中的指定的值,有两种方法spop和SRANDMEMBER分别代表会将
抽中的数据删除和不删除抽中的数据)以及共同关注的人功能
抽中的数据删除和不删除抽中的数据)以及共同关注的人功能
列表类型list
它是一个双端的队列,可以作为消息队列
有序集合类型sortedSet
可以用作各种排行榜,权重就代表热度
redis的一些问题
缓存穿透
攻击者故意搜索id特别大或为-1的不存在的数据
增加数据库的压力
增加数据库的压力
解决方案,对于没有的数据也在redis生成一个过期时间为1分钟的缓存数据
缓存击穿
对于数据库中有但是redis中没有的数据,用户并发量过大,导致数据库
压力增大
压力增大
解决方案
热门数据永不过期
加互斥锁
缓存雪崩
在某个时间点,大量的缓存失效,导致数据库瞬间需要承载大量请求,导致数据库宕机
解决方案
针对redis热点内存失效的情况
设置热门数据永不过期
设置随机的过期时间
针对redis服务不可用的情况
采用redis集群
限流
redis事务相关
redis事务的相关操作
redis事务分为两个阶段,一个是组队阶段,一个是执行阶段
multi开启事务
exec执行事务
discard放弃事务
redis事务的特性
单独的隔离操作
不保证原子性
没有隔离级别的概念
redis事务的注意事项
如果在排队阶段的命令有语法上的错误(就类似于编译时错误),那么就不会进入执行阶段,直接报错
但是如果是运行时错误,比如说对于一个字符串进行+1操作,在运行时才能发现错误,这种操作即使报错了也不会进行回滚
redis事务冲突的问题
悲观锁解决问题
乐观锁解决问题
采用版本号机制来解决问题实现事务
使用wathc key命令来监视一个数据,如果在执行前该数据有改动,就会将当前事务打断
redis事务秒杀超卖问题
redis秒杀场景
redis中两个字段分别是库存和购买的用户id的集合,当购买以后将库存-1,将用户id加入到set集合中去,并且
在每次购买前检查用户是否已经购买过
在每次购买前检查用户是否已经购买过
redis秒杀产生的问题
对于不采用任何措施的场景下,如果大量线程并发涌入,会导致多卖的情况产生,在别的线程修改库存之前,
其他线程已经通过if判断,导致商品多买
其他线程已经通过if判断,导致商品多买
悲观锁解决:使用synchronized关键字可以解决这个问题,加锁让所有线程串行执行,问题就是速度会大大降低
使用redis事务+乐观锁解决超卖问题,会产生问题:
1.并不是先来先抢到,使用版本号机制实现,那么100个线程同时到达,那么哪个线程先完成
先修改了版本号,哪个线程先抢到,并不是谁先来谁抢到
先修改了版本号,哪个线程先抢到,并不是谁先来谁抢到
2.产生库存遗留问题,对于5000个商品,100个人去抢,只有一个人能抢到,其他人因为版本号修改而
作废,导致产生库存遗留问题
作废,导致产生库存遗留问题
使用lua脚本解决库存遗留问题
通过lua脚本解决争抢问题,实际上是redis利用其单线程的特性,用任务队列的方式解决多任务并发问题
lua脚本又一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作
redis的持久化
RDB持久化
redis的数据备份文件,也就是将redis中的数据备份到磁盘中去,当redis
出现宕机,就可以根据RDB文件去恢复redis中的数据
出现宕机,就可以根据RDB文件去恢复redis中的数据
每次redis服务端关闭时都会自动的执行一次RDB,来实现数据的持久化操作
RDB的操作
save方法实现RDB,这种方法是不被推荐的,他是使用redis的主进程去备份redis中的数据,那么都知道redis的单线程的
如果主进程去进行RDB操作,就会影响到用户的后续操作
如果主进程去进行RDB操作,就会影响到用户的后续操作
bgsave方法,启动后台线程去执行rdb操作,不影响主进程
配置文件中可以设置RDB的文件名,RDB的频率
AOF持久化
AOF追加文件,将redis的所有写命令写入到aof文件中去
默认是关闭的
aof的三种频率
always,每次aof命令都直接写入到aof文件中去
everysec每执行一次命令,就将命令写入到aof缓冲区,在每秒将缓冲区的数据写入到aof文件中去
no,命令执行完写入缓存区,什么时候写入到aof由操作系统决定,这种可靠性是最差的
为了解决aof文件过大的问题,引入了bgrewriteaof命令
优化aof日志中的命令,比如对于同一个key的多次写只记录最后一次,用最少的命令达到相同的效果
redis分布式缓存
主从集群的搭建
将B设置为A的从服务器,方法就是在B的客户端上执行slaveof A的ip地址 A的端口
主从同步的原理
全量同步
master执行bgsave命令,生成后台进程生成RDB文件,发送RDB文件,
slave清空本地数据,加载RDB文件,再将RDB期间的所有命令写到缓冲区
中去,然后将缓冲区中的内容发送到slave中
slave清空本地数据,加载RDB文件,再将RDB期间的所有命令写到缓冲区
中去,然后将缓冲区中的内容发送到slave中
比较消耗性能的,比较慢,因为RDB文件他是需要将所有的数据拷贝到RDB文件中去,所以只有第一次会进行全量同步
master如何判断slave是第一次同步数据
Replication Id
用来标记数据集,如果id一致说明是同一数据集,每一个master都有唯一的replid
offset
偏移量,slave完成同步以后会记录当前同步的offset,如果slave的offset小于master的offset,说明slave数据落后于master,需要更新
判断一个slave是不是第一次来,基于id来判断,如果id不一样,就是第一次同步
增量同步
如果slave重启后同步,则执行增量同步
增量同步的同步的内容是去缓冲区中offset后的数据,只要offset不超过repl_baklog的上限
slave断开的时间过久,当尚未同步的数据太多大于缓冲区中的数据量,那么就只能去做全量同步,而不是增量同步
主从同步的优化
减少全量同步的次数
提高repl_baklog缓冲区的大小
发现slave宕机时尽快恢复,尽可能避免全量同步
提高全量同步的效率
在master中配置无磁盘复制,当要写RDB文件时不写到磁盘中,直接发到网络的IO流中
磁盘比较慢,网络比较快的时候用
磁盘比较慢,网络比较快的时候用
降低单节点上的内存占用不要太大,减少RDB导致的过多的磁盘IO
采用主从从链式结构的redis节点,减少master的压力
redis哨兵机制
哨兵的作用
用来实现主从集群的故障恢复
监控:不断的检查您的master和slave是否正常工作
自动故障恢复:如果master故障,sentinel会将一个slave提升为master,当故障实例恢复后也以
新的master为主
新的master为主
通知redis客户端的服务发现的来源,当故障转移时将最新信息
推送给客户端
推送给客户端
哨兵的服务状态监控
每隔1秒向集群的每个实例发送ping命令
主观下线:如果某个实例规定时间内未响应,则认为该实例主观下线
客观下线:若超过一定数量的sentinel认为该实例下线,则认为该实例客观下线
选举新的master
排除于master断开时间超过指定值的slave节点
判断slave节点和slave-priority值,在配置文件里配置的,优先级值,默认大家一样
判断offset值,值越大越新,优先级越高
最后判断slave节点的运行id大小,越小优先级越高
故障转移
sentinel向新的master执行slaveof no one,让他成为master
向其他的节点更新master节点的ip和端口号 slaveof 新masterip 端口号
对于旧的master也发送将新的master认为是主节点的命令
redis的过期策略
过期删除,可以给键设置过期时间,让它过期自动删除
定时删除,当redis内存满的时候会将过期时间最近的键删除
惰性删除,当访问key的时候再检查对应的key有没有过期,如果过期了就进行删除
redis和mysql保证最终一致性的方案
先删redis 再改mysql 再删redis
先改mysql,再删redis
先改mysql,再通过binlog改redis
JVM相关
JVM的位置
JVM是在一个操作系统的进程虚拟机,java程序通过JRE来进行编译,转换成字节码
对象在通过操作系统去操作硬件体系
对象在通过操作系统去操作硬件体系
JVM类加载的过程
加载
类加载器
作用:加载Class文件
包括虚拟机自带的加载器
启动类加载器
扩展类加载器
应用程序加载器
双亲委派机制
作用:保证安全
当jvm加载一个类时,他会先去根据根加载器去加载这个类,如果根加载器
中没有这个类,再去扩展加载器,再去应用程序加载器
中没有这个类,再去扩展加载器,再去应用程序加载器
为什么要这么做?为了保护java定义的类,比如我们自己定义了一个
java.lang.String的类,然后去执行,那么java不会去优先去找我们自己定义
的,而是先去跟加载器里找,也就是找java自己定义的类,这样就保证了代码的
安全性,防止有人去写一个String类去破坏java代码
java.lang.String的类,然后去执行,那么java不会去优先去找我们自己定义
的,而是先去跟加载器里找,也就是找java自己定义的类,这样就保证了代码的
安全性,防止有人去写一个String类去破坏java代码
java无法查看根加载器,因为它是一个本地方法用native修饰,也就是
c语言写的方法,所以java无法读取到
c语言写的方法,所以java无法读取到
链接
验证
确保Classs文件的字节流中包含的信息是否符合当前虚拟机的需求保证被加载类的正确性,不会危害虚拟机自身的安全
准备
创建类的static变量创建内存地址并且设置成默认值
解析
将常量池内的符号引用转换为直接引用的过程
什么是符号引用,什么是直接引用
当java文件编译成class文件时,虚拟机并不知道引用类的地址,所以使用符号来
代替引用,当进入解析阶段,就将符号引用替换成真正的地址
代替引用,当进入解析阶段,就将符号引用替换成真正的地址
String str = "abc" 编译时没有分配内存,先用符号代替,解析阶段把符号引用替换成直接引用
初始化
对于静态资源在<clinit>类加载器方法中初始化,执行静态代码块里的操作和静态变量的赋值操作
将加载的类信息存放在方法区中的内存空间
运行时数据区
方法区
堆
堆相关
堆是所有线程共享的,需要考虑线程安全的问题
有垃圾回收机制
设置堆空间最大值的虚拟机参数 -Xmx
堆内存溢出 OOM
堆内存诊断
jps工具
查看当前系统有哪些Java进程
jmap工具
查看当前时刻堆内存占用情况
使用方法 jmap -heap 进程id
jconsole工具
图形界面,多功能监测工具,可以连续监测
如果垃圾回收后内存占用仍然很高怎么办
可以使用jvitualvm工具
将内存转储,再查看占用内存最大的前几个对象
堆的区域
新生代
伊甸园区
所有类的对象都是从伊甸园区创建
当伊甸园区满后,会执行轻GC,对伊甸园区和幸存者区的from区进行gc,将gc后仍存活的对象
存放在幸存者区的to区
存放在幸存者区的to区
幸存者1区
幸存者0区
当经过15次轻gc后仍然存活的对象会放置在老年区
老年代
用来存放经过15次请GC后仍存在的对象
方法区
他是JVM的一个规范,并不一定要在堆中
JDK8是元空间
元空间是在方法区中,逻辑上属于堆
JDK7之前是永久代
永久代是存放在堆内存中的
线程私有的缓冲区
对于每个线程都生成一个私有的缓冲区,用来存放每个线程私有的变量,提高并发性
Java栈
栈相关
栈是线程运行需要的内存空间
栈帧
一个栈帧对应了一次方法的调用
包含返回地址,参数,局部变量等方法调用信息
栈由多个栈帧组成
每个线程只能有一个活动栈帧,对应着当前正在执行的方法
垃圾回收不回收栈内存
栈内存不是越大越大,过大会导致进程能创建的线程数减少,能增加递归调用的层数
通过虚拟机参数-Xss设置栈内存大小
栈内存溢出
栈帧过多导致栈内存溢出(方法递归调用)
栈帧过大
线程诊断
cpu占用过多
可能有死循环等问题
程序运行很长时间没有结果
可能发生了死锁
通过top命令动态查看进程cpu占用率
通过ps命令查看线程进程的cpu占用率
java自带的jstack命令
根据线程id找到有问题的线程,并且进一步定位到问题代码的源代码行数
程序计数器
程序计数器
用来记录JVM要运行的下一条执行
程序计数器是线程私有的,属于当前线程
不会存在内存溢出
本地方法栈
局部变量一定线程安全吗?
不一定,如果局部变量没有逃离方法的作用访问,也就是只在方法内部使用,那么他是线程安全的
,但是如果他逃离了,比如引用变量引用了局部变量的对象(他必须是引用类型,基本数据类型没有问题)
或者作为返回值返回了,那么就可能产生线程安全的问题
,但是如果他逃离了,比如引用变量引用了局部变量的对象(他必须是引用类型,基本数据类型没有问题)
或者作为返回值返回了,那么就可能产生线程安全的问题
GC垃圾收集
什么时候进行GC?
如何确定哪些对象需要GC?标记算法
引用计数法
对于每个对象添加一个计数器,如果他增加一个引用就+1,减少一个引用就-1,如果一个对象的引用是0
,那就清除这个对象
,那就清除这个对象
缺点
对于环形引用的多个对象,循环引用,当引用的入口关闭以后,仍然会有引用,导致内存泄露
也就是GC不会去清理他,所有java不使用这种方法
也就是GC不会去清理他,所有java不使用这种方法
需要为每个对象保存一个整形计数器,占用空间,需要时刻维护计数器,费时间
优点
实现简单,效率高
可达性算法
可达性算法会从GCroot出发,形成一个引用链,将引用链的所有对象标记为可达,不需要清理,
对于不可达的对象进行清理
对于不可达的对象进行清理
优点
同样简单高效
解决了循环依赖导致内存泄露的问题
哪些对象是GCroot对象?
对于虚拟机栈中栈帧存储的对象的引用地址指向的对象,也就是方法的参数,方法的局部变量
静态变量的引用,对于静态变量,除非类销毁,否则不会消除这个对象
synchronized锁持有的对象,如果锁持有的对象被回收,锁就会失效
注意事项
当要进行可达性算法分析的时候,需要保持在一个快照中进行,因此进行可行性算法需要
STW,也就是stop the world 暂停所有的用户线程,这也是GC需要STW的一个重要原因
STW,也就是stop the world 暂停所有的用户线程,这也是GC需要STW的一个重要原因
清除阶段
标记-清除算法
清除标记阶段没有标记的内存对象
会产生内存碎片,需要去维护空闲列表
需要STW,用户体验比较查
标记-清除-压缩算法
第一阶段和标记清除阶段一样,从根节点开始标记所有被引用对象
第二阶段将所有存活的对象压缩到内存的一段按顺序排放,之后清理边界以外的所欲空间
优点
消除了标记清除法中会产生内存碎片的问题
相比于标记复制法,不需要内存减半
缺点
效率上比标记清除法低,因为有碎片整理的过程
移动过程中,需要暂停所有的用户线程,时间比较长
标记复制算法
没有标记的过程,将可达的对象,直接复制到内存大小相同的另一个区域中区,并且连续存放,
复制完成后,A区全部清理,下一次再从B区复制到A区,A区全部清理
复制完成后,A区全部清理,下一次再从B区复制到A区,A区全部清理
优点
运行效率高
不会产生内存碎片,复制过去保证了空间的连续性
缺点
需要两倍的内存空间
引用场景
对于新生区这种频繁创建对象的区域,垃圾对象很多,存活对象很少的场景
比如幸存者1区和幸存者0区就是用于GC进行标记复制法清理内存的
比如幸存者1区和幸存者0区就是用于GC进行标记复制法清理内存的
如果活动对象太多,每层都需要复制很多,效率很低
对于老年代的GC,存在很多的活动对象,效率就比较低
对于老年代的GC,存在很多的活动对象,效率就比较低
分代
对于不同的对象的声明周期是不同的,因此对于不同的声明周期的对象采用不同的收集方式,来提高回收效率
java将堆分为新生代和老年代,根据不同代的特点使用不同的回收算法,提高效率
新生代
对象生命周期短,存活率低,频繁回收
使用标记复制算法最合适,因为存活的对象少,复制的对象的数量少,效率就高,速度快
老年代
区域大,对象生命周期长,存活率高,回收没有年轻代频繁
由1.标记清除 2.标记清楚和标记清除压缩算法混合实现
数据库相关
数据库索引,sql调优
索引的数据类型
B+树索引
最常用的索引结构
B树和B+树的区别
B树每个节点都存储数据,而B+树非叶子节点只存放索引指针和索引的字段,对于B+树的查询会更加稳定
对于B树可能需要查询1层也可能查询10层,而B+树必须查询到叶子节点
对于B树可能需要查询1层也可能查询10层,而B+树必须查询到叶子节点
B+树叶子节点实现了单向链表,而在MySql中B+树索引对原B+树增加了一个
链表指针,形成了单向链表,提高了区间访问的性能,利于数据库的排序工作
链表指针,形成了单向链表,提高了区间访问的性能,利于数据库的排序工作
Innodb默认的数据页大小是16K,B+树的非叶子节点只存放索引字段和索引指针
不存放数据,这样他就能够存储更多的索引字段和索引指针,树的深度就会更低,查询
效率就会更高
不存放数据,这样他就能够存储更多的索引字段和索引指针,树的深度就会更低,查询
效率就会更高
hash索引
不支持范围查询,只支持单个查询
R-tree索引
MyIsam引擎特有的索引,用于地理空间数据类型
全文索引
用于快速匹配文档
为什么不用其他数据结构?
二叉树
红黑树
B树
索引的类型
复合索引
多个字段组成的索引
单一索引
单个字段的索引
主键索引
根据主键生成的索引
唯一索引
唯一索引的字段不能重复
innoDB下的索引类型
二级索引(辅助索引
根据索引的字段建立B+树,叶子节点存放主键ID,再根据sql语句
查询的字段看有没有覆盖索引,要不要进行回表查询(再根据主键对聚集索引进行查询
查询的字段看有没有覆盖索引,要不要进行回表查询(再根据主键对聚集索引进行查询
聚集索引
根据主键建立的B+树,数据存放在叶子节点
最左前缀法则
对于索引,存在最左前缀法则,也就是索引最优先根据最左边的字段进行查询,如果中间断开也会差生索引失效
比如你有ABC,三个字段的联合索引,那么如果你只根据A和C进行查询,那么索引到B就会失效,导致BC的索引都无法使用
比如你有ABC,三个字段的联合索引,那么如果你只根据A和C进行查询,那么索引到B就会失效,导致BC的索引都无法使用
索引失效的情况
模糊查询首部不要进行%匹配,不然会导致索引失效
对于or关键字如果一边字段没有索引就会索引失效
不要在索引列上增加函数运算否则索引失效
索引尽量使用>=,<=,如果范围匹配只有> 和<,索引会失效
字符串不加单引号会形成隐式类型转换导致索引失效
如果mysql认为全表扫描比用索引快,索引失效
is null 和 is not null也会引起索引失效
sql优化的方式
80%还是通过索引
当数据达到百万级时,再考虑
分表
读写分离
redis缓存
索引的使用原则
对于数据量比较大的查询使用索引
添加索引的字段应该重复性不高
主键和外键必须有索引
对于过长的字段不应该加索引,如果加可以对于字段的前几位加索引
如果可以尽量使用复合索引
数据库存储引擎
innoDB存储引擎
DML(数据库操作语言,也就是增删改)支持ACID特性的事务
行级锁,提高并发性能
支持外键
逻辑存储结构
表空间
段
区
页
行
一个区是1M,一个页是16K,一个区有64个连续的页
架构
内存结构
磁盘结构
MyISAM
不支持事务,不支持外键
支持表锁,不支持行锁
访问速度快
表结构和表数据是分开存储的
锁相关
全局锁
锁定数据库中的所有表,让整个数据库处于只读状态,在进行数据库备份时使用
在备份期间不能更行,如果从从库上备份会造成主从库延迟
也可以使用快照读来进行备份
在备份期间不能更行,如果从从库上备份会造成主从库延迟
也可以使用快照读来进行备份
表级锁
表锁
读锁(读共享锁
对于当前客户端,只能读,不能写,对于其他客户端,也是
只能读不能写。
只能读不能写。
与写锁互斥,与读锁不互斥
写锁(写独占锁或者叫排他锁
对于当前客户端,能读也能写,对于其他客户端,不能读也
不能写,与其他写锁和读锁都互斥
不能写,与其他写锁和读锁都互斥
意向锁
用于避免当添加表锁时,需要一次检查每一行的行锁是否与表锁互斥,因此
增加意向锁,当添加行锁时,会向表添加意向锁,这样后面其他表锁要添加时
只需要判断与意向锁是否互斥就可以了,不需要扫描所有行锁
增加意向锁,当添加行锁时,会向表添加意向锁,这样后面其他表锁要添加时
只需要判断与意向锁是否互斥就可以了,不需要扫描所有行锁
意向共享锁
select ... lock in share mode 语句添加
与读锁兼容,与排他锁互斥
与读锁兼容,与排他锁互斥
意向排他锁
由insert update delete select ... for update语句添加
与表锁读锁以及表锁写锁锁都互斥 意向锁之间不会互斥
与表锁读锁以及表锁写锁锁都互斥 意向锁之间不会互斥
元数据锁
DQL和DML都会自动加上共享读锁和共享写锁,相互之间不是互斥的
对于alter 语句该表表结构,那么会和共享读锁和共享写锁互斥
为了解决执行DML语句和DDL语句之间的冲突问题的,所以这个锁它无论是读锁和写锁
他都是兼容的,但是对于alter语句也就是DDL语句,他是互斥的
他都是兼容的,但是对于alter语句也就是DDL语句,他是互斥的
行级锁
每次锁住对应行的数据,锁定的粒度最小,发生冲突概率最低,并发度高,Innodb引擎支持
行锁通过对于索引上的索引项来加锁实现的而不是对记录加锁
行锁通过对于索引上的索引项来加锁实现的而不是对记录加锁
行锁
共享锁 和共享锁兼容 和排他锁冲突
select ... lock in share mode
排他锁 和共享锁和排他锁都冲突
insert update delete select ... for update
间隙锁
保证索引记录间隙,确保索引记录间隙不变,防止其他事务在这个间隙insert,产生幻读
在RR隔离级别下支持
在RR隔离级别下支持
临键锁
行锁和临键锁 ,在RR隔离级别下支持
间隙锁和临键锁是为了锁住一定范围的数据,防止产生幻读的问题
事务的原理
事务的ACID特性
原子性
指对于事务,要么全部成功,要么全部失败,通过undo log日志实现事务的回滚操作
当事务中的语句发生错误时,通过undo log 日志回滚之前的操作实现原子性
当事务中的语句发生错误时,通过undo log 日志回滚之前的操作实现原子性
一致性
通过其他的三个属性来保证,意思是事务的执行结束后,如果事务回滚,那么数据库
中的数据和事务执行前保持一致,如果事务提交,那么提交后的数据库的数据是完整的
中的数据和事务执行前保持一致,如果事务提交,那么提交后的数据库的数据是完整的
隔离性
隔离性的三个级别
读未提交
读已提交
可重复读
串行读
spring相关事务隔离性
使用@Transactional注解添加事务
使用@Transactional注解添加事务
事务的传播特性
PROPAGATION_REQUIRED:需要, 如果存在一个事务,则支持当前事务, 如果没有事务则开启(原来有就使用原来事物,没有就创建事物,一般Service的中更新类的操作)
PROPAGATION_SUPPORTS:支持, 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行(原来有就使用原来事物,没有就算了,一般Service的中查询操作)
PROPAGATION_REQUIRES_NEW:总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
PROPAGATION_SUPPORTS:支持, 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行(原来有就使用原来事物,没有就算了,一般Service的中查询操作)
PROPAGATION_REQUIRES_NEW:总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
事务的隔离级别
隔离性是底层是通过MVCC+锁来实现的,快照读也就是不同的select语句是通过MVCC来实现的,而当前读也就是DML语句和select...for update 和
select ...lock in share mode 是通过加锁来实现的,对于不同的语句加的锁的粒度也是不同的,
select ...lock in share mode 是通过加锁来实现的,对于不同的语句加的锁的粒度也是不同的,
持久性
持久性是底层是通过redo log 日志来实现的,对于已经提交的事务,redo log会记录数据的物理信息,并且提交到磁盘上,当磁盘刷新脏页
时出现错误,就可以通过redo log 日志来实现数据的恢复,保证数据的持久性
时出现错误,就可以通过redo log 日志来实现数据的恢复,保证数据的持久性
产生的三个问题
读脏数据
指的是一个事务读取到其他事务还没有提交的数据
事务A去执行update语句,但是还没有提交,此时事务B去执行select语句,读取到事务A没有
提交的修改
提交的修改
不可重复读
一个事务先后读取同一条记录,但是读取的结果不同
事务A两次读取了一条数据,在这个过程中事务B修改了这个数据
导致事务A两次读取的内容不同
导致事务A两次读取的内容不同
幻读
一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在
当事务A对一个范围内的数据进行两次查询,在两次查询之间事务B对范围内的数据进行了
insert ,delete 语句 导致数据消失或增加,导致事务A 的两次范围查询结果不一致,称为幻读
insert ,delete 语句 导致数据消失或增加,导致事务A 的两次范围查询结果不一致,称为幻读
redo log日志
保障事务的持久性
当执行update delete insert语句时,会首先从内存缓冲池中找有没有对应的数据,如果没有就会去磁盘将对应的数据更新到缓冲区,
然后再在buffer pool缓冲区中更新数据 更新完成后提交事务,然后缓冲区中被修改的数据页被称为脏页,会按照一定的频率将脏页刷新到
磁盘空间中去,而这时如果磁盘刷新发生错误,就无法保证数据的一致性,这时就要引入redo log 日志,redo log 是一个物理日志,会存储
数据页的数据变化,他为两个部分,一部分在缓冲区,一部分在磁盘中,当事务提交时,会同步将缓冲区的redo log 日志刷新到磁盘中去,如果刷新磁盘
产生错误,就会借助redo log 日志来恢复,保证数据的持久性
然后再在buffer pool缓冲区中更新数据 更新完成后提交事务,然后缓冲区中被修改的数据页被称为脏页,会按照一定的频率将脏页刷新到
磁盘空间中去,而这时如果磁盘刷新发生错误,就无法保证数据的一致性,这时就要引入redo log 日志,redo log 是一个物理日志,会存储
数据页的数据变化,他为两个部分,一部分在缓冲区,一部分在磁盘中,当事务提交时,会同步将缓冲区的redo log 日志刷新到磁盘中去,如果刷新磁盘
产生错误,就会借助redo log 日志来恢复,保证数据的持久性
undo log 日志
保障事务的原子性
undo log日志是一个逻辑日志,它会记录事务执行的sql语句修改前的数据,在事务回滚时,通过undo log日志实现数据的回滚,恢复到
事务开始之前的状态,保证原子性,同时undo log并不都会在事务提交后删除,Insert语句的undo log会立即删除,而update delete语句会保留
形成版本链,用于MVCC,并且配合两个隐式的字段 最近修改的事务ID,上一个版本的地址引用保证事务的隔离性
事务开始之前的状态,保证原子性,同时undo log并不都会在事务提交后删除,Insert语句的undo log会立即删除,而update delete语句会保留
形成版本链,用于MVCC,并且配合两个隐式的字段 最近修改的事务ID,上一个版本的地址引用保证事务的隔离性
MVCC(多版本并发控制
保障事务的隔离性
当前读
读取的是最新版本,会加对应的行锁 比如select ....lock in share mode 会加共享锁
select ... for update,insert,update,delete 会加独占锁
他们的读都是当前读
select ... for update,insert,update,delete 会加独占锁
他们的读都是当前读
快照读
一种非阻塞读,它读取的不一定是数据的最新版本
undo log版本链
不同事务或相同事务对同一行数据的修改会产生一条undo log版本链
readView
读视图,它是快照读sql读取版本的依据,包含四个字段
当前活跃的事务id集合
活跃的最小事务id
预备事务的ID集合(活跃的最大事务的ID+1
ReadView创建者的事务ID
当前活跃的事务id集合
活跃的最小事务id
预备事务的ID集合(活跃的最大事务的ID+1
ReadView创建者的事务ID
通过版本链数据访问规则来确定快照读到底访问undo Log版本链中的哪段数据
版本链数据访问规则
1. trx_id == creator_trx_id 可以访问该版本
2. trx_id == min_trx_id 可以访问,说明事务已经提交了
3. trx_id > max_trx_id 不可以访问该版本 不可以访问 说明该事务是在ReadView 生成后才开启的
4. min_trx_id <= trx_id <= max_trx_id 如果trx_id 不在m_ids 中是可以访问该版本的 说明数据已经提交
1. trx_id == creator_trx_id 可以访问该版本
2. trx_id == min_trx_id 可以访问,说明事务已经提交了
3. trx_id > max_trx_id 不可以访问该版本 不可以访问 说明该事务是在ReadView 生成后才开启的
4. min_trx_id <= trx_id <= max_trx_id 如果trx_id 不在m_ids 中是可以访问该版本的 说明数据已经提交
RC 和RR隔离级别下的MVCC区别
在RC隔离级别下,每次快照读都会生成新的ReadView,实现每次读取
undo log版本链中已经提交事务的版本
undo log版本链中已经提交事务的版本
在RR隔离级别下,只有第一次快照读会生成ReadView,后面的快照读沿用第一次的
ReadView,因此后面每一次读取的结果都是一致的,这也就是为什么叫可重复读,事务
每次重复读的ReadView都相同,那么读取的undo log版本链中的数据也相同
ReadView,因此后面每一次读取的结果都是一致的,这也就是为什么叫可重复读,事务
每次重复读的ReadView都相同,那么读取的undo log版本链中的数据也相同
其他区别
在RR的隔离级别下又增加了间隙锁和临键锁来方式幻读问题
RC锁不会升级?
存储过程
包含一系列sql语句的集合,完成一个复杂的功能,存储过程可以被反复使用,执行效率高
视图
他不是一个真正的表,用来查询数据和展示数据,但是不能修改数据
触发器
一个由事件触发的存储过程
sql语句编写相关
union和unionAll有什么区别
union和unionAll都是将两个sql查询结果合并,区别是union它会将查询的相同查询结果合并,而unionAll不会合并
varchar和char的区别
varchar是不定长存储
char是定长存储
char是定长存储
从速度上定长的速度快
varchar因为是不定长的所有需要有一个数据表示存储了多少长度
如果对于长度有时候很长有时候很短的,这种如果用char存储空间利用率
就很低,就最好用varchar
就很低,就最好用varchar
对于性别这种一个1长度的就最好用char,利用率高,查询速度快
内连接与外连接
内连接就是匹配两个表中符合on连接条件的所有行
外连接
左外连接left join
获取左表中的全部内容,并且显示右表中匹配的内容,如果右表中没有匹配的内容,对应字段就显示为null
右外连接 right join
获取右表中所有的内容,并显示左表中匹配的内容,如果左表中没有匹配的内容的字段,对应位置为null
全外连接 full join
full join结合了左外连接和右外连接,左右表的内容都会展示,左表没有匹配的右表字段的和右表没有匹配左表字段的也会展示,并且外表字段为null
sql执行的顺序
from语句
where
group by having
聚合函数
select
order by
sql语句优化
尽量避免使用子查询
读取尽量用limit,不要全部查询
字段不要用*,而是查询需要的字段
用in来代替or
禁止没必要的orderby
避免全表扫描
创建索引的语句
alter table table_name add index ('coloum1');
mysql的取整函数
round()四舍五入
floor 向下取整
ceil()向上取整
SpringCloud分布式微服务
什么是集群?什么是分布式?
集群就是指多台服务器做相同的功能
而分布式是多台服务器每台都做不一样的功能,一起组成一个项目
而分布式是多台服务器每台都做不一样的功能,一起组成一个项目
微服务的相关概念
网关
用户传过来的技术会经过springCloudGateway网关,去注册中心去进行注册与发现,
再去具体的微服务进行访问
再去具体的微服务进行访问
SpringCloudGateway
服务集群
服务集群就是多台服务器开启同样的服务,负责相同的服务
负载均衡
对于微服务的调用,不只调用一台服务器的服务,而是根据调用的策略去进行调用,比如随机调用、轮询调用策略
注册中心
所有的微服务都会去注册中心去进行注册,让注册中心去进行分配调用
服务限流、熔断
服务限流就是说对于服务接收大量的请求,会使用服务限流策略,限制一部分流量的进入
对于有些依赖其他服务的服务,可能服务会出现异常,导致整个服务的资源被占用(多个线程都在等待这个服务),
此时会有一个异常比例的概念,如果异常比例达到会直接切断这个服务,以免局部的异常导致整个服务的资源崩溃
此时会有一个异常比例的概念,如果异常比例达到会直接切断这个服务,以免局部的异常导致整个服务的资源崩溃
分布式事务
使用seata实现分布式事务,对于单体应用来说,事务可以使用trational注解来实现,但是对于分布式系统,服务
都是分离的,比如对于支付订单,可能需要调用支付,仓库,订单相关的微服务来一起完成,这个时候就无法使用
trational注解来实现,因此使用seata实现分布式的事务,保证操作的原子性
都是分离的,比如对于支付订单,可能需要调用支付,仓库,订单相关的微服务来一起完成,这个时候就无法使用
trational注解来实现,因此使用seata实现分布式的事务,保证操作的原子性
使用seata完成
0 条评论
下一页