Java中的周杰伦
2021-08-30 15:19:19 103 举报
AI智能生成
java后端相关面试知识点
作者其他创作
大纲/内容
锁
各种锁
自旋锁
概念
调用者在获取自旋锁时,如果当前锁被其他调用者调用,他不会让进程进入睡眠状态,而是一直在原地自旋等待其他调用者释放锁
缺点
他是忙等待锁
会一直尝试获取锁,会消耗大cpu资源
可能死锁
比如一个低优先级的进程占有锁,此时来了一个高优先级的进程抢占了cpu,它也在获取锁,但是这个锁却被低优先级的进程所占有,这就会引发死锁
场景
无锁机制,CAS,原子类中
互斥锁
概念
调用者在获取互斥锁时,如果当前锁被其他进程所占用,它会让进程进入到睡眠状态,加入到等待队列中,然后使用调度机制让进程处理其他业务,如果其他调用者释放了锁,那么就从等待队列中拿出这个进程获取锁
公平锁
概念
当多个线程想要获取公平锁时,如果当前锁被其他线程所持有,那么就会按照等待的先后顺序将这些线程加入到等待队列当中,释放锁时,从队列头部获取最早开始等待的线程来持有锁
非公平锁
概念
不能保证公平性,当多个线程获取非公平锁时,如果当前锁被其他线程所持有那么他不会按照先后顺序进行加锁,而是会抢锁,那个线程抢到了哪个线程获取锁
共享锁
概念
多个线程可以同时获取锁
独占锁
概念
一个线程只能获取一个锁
应用
ReentrantLock使用独占方式实现互斥锁
ReentrantReadWriteLock中写操作的独占锁,读操作是共享锁
读写锁
概念
高级别锁,可以区分读写操作,符合条件时允许多个线程访问对象,当是读操作时,允许多个线程来获取读锁,但不允许写锁,如果时写操作时,所有线程都要睡眠等待
递归锁
概念
也叫可重入锁,可以理解为是一种 特殊的互斥锁,也是一个线程可以获取一个锁,允许在还没有释放锁时,反复对该锁进行加锁操作
乐观锁
是一种乐观策略,当获取一个资源时,它会乐观的认为这个资源没有被其他线程所修改,如果被修改就放弃本次操作,会一直重复当前操作直到成功
场景
写操作比较少的情况下,即冲突发生很少的情况,减少锁竞争带来消耗,增大了吞吐量
悲观锁
是一种悲观的策略,当获取一个资源时,会悲观的认为所有其它线程都会对这个资源进行修改,所以获取数据之前需要对数据进行加锁
场景
写操作很多的情况下使用
设计模式
说说你熟悉的设计模式
单例模式
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。
实现的步骤
(1)将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;(2)在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
场景
(1)需要生成唯一序列的环境;(2)需要频繁实例化然后销毁的对象;(3)创建对象时耗时过多或者耗资源过多,但又经常用到的对象;(4)方便资源相互通信的环境。
优点
可以避免全局使用的类总是被创建和销毁,可以节省系统资源,确保只有一个实例
缺点
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化
实现方式
懒汉式
典型的时间换空间
第一次调用才初始化,不用的时候节省了空间
线程不安全
饿汉式
典型的空间换时间
当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要判断了,节省了运行时间
天生线程安全的,类加载时就加载了实例,避免了多线程问题
双重检验锁
(线程安全,效率高):此种实现中不用每次需要获得锁,减少了获取锁和等待的事件。注意volatile关键字的使用,保证了各线程对singleton静态实例域修改的可见性
静态内部类
(线程安全、效率高):这种方式下 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化instance。
枚举类
创建一个枚举,他就代表一个instance实例
懒汉式和饿汉式的区别
线程安全方面:饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,懒汉式本身是非线程安全的。
资源加载和性能方面:饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
资源加载和性能方面:饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
破坏单例模式
序列化
克隆
反射
解决方案
解决方案如下:
1、防止反射
定义一个全局变量,当第二次创建的时候抛出异常
2、防止克隆破坏
重写clone(),直接返回单例对象
3、防止序列化破坏
添加readResolve()方法 , 该返回Object对象 也就是instance
1、防止反射
定义一个全局变量,当第二次创建的时候抛出异常
2、防止克隆破坏
重写clone(),直接返回单例对象
3、防止序列化破坏
添加readResolve()方法 , 该返回Object对象 也就是instance
工厂模式
我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象
简单工厂
提供一个统一的工厂类
(1)需要创建的对象较少。
(2)客户端不关心对象的创建过程。
(2)客户端不关心对象的创建过程。
工厂方法
不在提供一个统一的工厂类,针对每个对象来提供一个工厂
(1)客户端不需要知道它所创建的对象的类。例子中我们不知道每个图片加载器具体叫什么名,只知道创建它的工厂名就完成了床架过程。
(2)客户端可以通过子类来指定创建对应的对象。
(2)客户端可以通过子类来指定创建对应的对象。
抽象工厂
工厂类不单单可以创建一个对象,而是可以创建一组对象
(1)和工厂方法一样客户端不需要知道它所创建的对象的类。
(2)需要一组对象共同完成某种功能时。并且可能存在多组对象完成不同功能的情况。
(3)系统结构稳定,不会频繁的增加对象。
(2)需要一组对象共同完成某种功能时。并且可能存在多组对象完成不同功能的情况。
(3)系统结构稳定,不会频繁的增加对象。
代理模式
代理模式,通过代理来处理对象,可以详细的访问某个对象的方法
静态代理
通过程序员创建或工具类生成源码,在运行时生成代理类
动态代理
不需要代理类实现某个接口,使用jdk的API实现,动态的在内存中生成代理对象
模板方法模式
定义一个模板结构,将具体内容延迟到子类去实现。在不改变模板结构的前提下在子类中重新定义模板中的内容。
提高代码复用性
将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中
实现了反向控制
通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 & 符合“开闭原则”
将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中
实现了反向控制
通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 & 符合“开闭原则”
观察者模式
属于行为型模式的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。
适配器模式
将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)
7大原则
单一职责
一个类应该只关注一个区域功能的实现
里氏替换
在需要使用到父类时,可以使用子类代替父类,反之则不可以
接口隔离
应当使用多个子接口而不是一个总接口,类的依赖关系应当建立在最小接口上
合成复用
尽可能使用对象组合而不是继承关系
依赖倒转
抽象不应当依赖于细节,细节应当依赖于抽象
开闭原则
一个程序应该对扩展开放,对修改关闭
迪米特法则
一个类应该只和与它有直接关系的类通信,即只和直接朋友来往,并且对朋友了解的最少,减少耦合度
数据结构
树
平衡二叉树
平衡二叉树满足叶子节点的高度差的绝对值不超过1,只要不满足条件就会通过旋转的方式来完成,这个过程很消耗时间,所以平衡二叉树用在插入和删除比较少的情况下,用来查询比较多的情况
二叉搜索树
保证它的左节点的值小于根节点,右节点的值大于根节点,一次类推,它的中序遍历是一个递增的顺序
时间复杂度为O(log n)
极端情况下会退变成链表,导致查询复杂度为O(n)
平衡二叉查找树
查找插入O(log n),删除为2log n
红黑树
它是一种平衡二叉搜索树,通过根节点到叶子结点中对节点的着色限制,保证了红黑树中没有一条路径是其他路径的两倍
性质
根节点必须为黑色,叶子节点下的null节点也必须为黑色
根节点到每个叶子节点经过的黑色节点个数都是相同的
不允许出现连续两个红色结点,每个红色结点下都有两个黑色节点
复杂度
查询复杂度O(log n)
AVL和红黑树的区别
都是用来检索数据的
平衡二叉树是高度平衡,红黑树是基本平衡,所以AVL>RBT
平衡二叉树的最坏查询复杂度为O(log n)而RBT最好为O(log n) 所以AVL略优于RBT
当插入一个节点的时候,都最多只需要2次旋转即可,但是删除的时候AVL需要经过log n次而RBT只需要3次,所以在删除上,红黑树要优于AVL
他们的插入删除的代价主要消耗在处理的节点上,所以与O(log n)成正比
AVL的旋转操作更难操作与维持
红黑树更加通用,在查找,添加,删除上性能都比较好,而AVL的查找更加快,但是代价是删除和添加速度慢
排序
堆排序
思想
将待排序的数组构建成大顶堆,第一个元素就是最大元素,让它与最后一个数进行调换位置,然后将整下的n-1个数继续组成大顶堆,以此类推
大根堆
每一个节点都比他的左右节点大的完全二叉树
小根堆
每一个节点都比他的左右节点小的完全二叉树
复杂度
最坏为O(n 2),平均最好都为O(nlog 2 n) 空间O(1),不稳定
有1千万个数据,要求找出其中最大的10个数
大根堆那么堆里是一千万个元素,然后10次取堆顶,占用内存大。用小根堆堆里就10个元素,第11个元素进入后调整堆,弹出堆顶的那个最小元素,那么就可以保证堆里的元素就是最大的那10个,省内存。
快排
思想
从一个数组中随机选出一个数N作为基数,通过一趟排序将数组分割成三个部分,1、小于N的区域 2、等于N的区域 3、大于N的区域,然后再按照此方法对小于区的和大于区分别递归进行,从而达到整个数据变成有序数组。
复杂度
时间O(N*logN) 空间O(log N),不稳定
基数选取
固定选取
序列首值,中值,尾值
随机选取
三数取中
选择首元素,中间元素,尾元素,这三个元素中不大不小的那个元素作为基数。这种方式能很好的解决待排数组基本有序的情况,而且选取的基准没有随机性
冒泡
思想
存在含有n个元素的数组,每一趟对相邻的两个元素进行大小判断,大的放到后面,这样一趟下来,最大的元素将放到最后,此时,最后的元素固定不动,第二趟再继续循环前面的n-1个元素,循环n趟后,整个数组排序完成
复杂度
O(n 2) 空间o(1),稳定
选择
思想
1.存在n个元素的数组
2.第一趟从n个元素中找到最小的值放到索引为0的位置
3.第二趟从n-1个元素中找到最小的值放到索引为1的位置
循环往复,直到循环结束
第一趟比较n-1次,第二趟比较n-2次,又是一个等差数列,时间复杂度为O(N^2),额外空间复杂度O(1),不稳定性排序
2.第一趟从n个元素中找到最小的值放到索引为0的位置
3.第二趟从n-1个元素中找到最小的值放到索引为1的位置
循环往复,直到循环结束
第一趟比较n-1次,第二趟比较n-2次,又是一个等差数列,时间复杂度为O(N^2),额外空间复杂度O(1),不稳定性排序
插入
1.第一次保证0-1位置的元素有序(比较索引0和索引1的位置)
2.第二次保证0-2位置的元素有序(拿索引为2的元素与前面的元素比较)
循环往复,直到保证0-(n-1)位置的元素有序
第一次比较1次,第二次比较2次,第三次比较3次,又是一个等差数列,时间复杂度可以判断为O(N^2),额外空间复杂度O(1),稳定性排序
2.第二次保证0-2位置的元素有序(拿索引为2的元素与前面的元素比较)
循环往复,直到保证0-(n-1)位置的元素有序
第一次比较1次,第二次比较2次,第三次比较3次,又是一个等差数列,时间复杂度可以判断为O(N^2),额外空间复杂度O(1),稳定性排序
归并
思想
假设初始序列有N个记录,则可以看成是N个有序的子序列,每个子序列的长度为1,然后两两归并,得到N/2个长度为2或者1的有序子序列,再两两归并…如此重复,直到得到一个长度为N的有序序列为止,这种排序方法称为2路归并排序
复杂度
O(nlog n),空间O(1),稳定
堆和栈的区别
栈是一种具有先进后出特性的数据结构
堆是一种经过排序后的树形数据结构,每一个节点都有一个值,它的根节点是最小或最大值,它的左右子树的也同时是一个堆结构,常用在优先队列
项目介绍
难点以及解决方案
超卖问题
在使用jmeter进行压测时,当多个线程同时秒杀一个商品时,那么他们所访问到的库存是相同的,所以同时秒杀当线程数大于库存数时就会出现超卖现象
更新库存的sql语句加一个条件也就是库存大于0的条件,这样当有100个线程,1个库存时,就会只有1个线程成功。利用mysql的排它锁写锁。如果一个事务获取了一行数据的排他锁,那么其他事务就不能获取任何锁,但是获取排它锁的事务可以修改数据
在订单表中加了一个唯一索引,防止用户重复秒杀
使用redis做预减库存,具体做法是实现InitializingBean接口,实现它里面的afterPropertiesSet方法,然后在这个方法以商品id生成一个key,该商品的库存作为一个value,加入到redis当中,每当一个线程进行秒杀时该商品库存就会-1,所以它规定了库存数量个线程进行秒杀,当库存为0时剩下的线程请求就会不进行处理了,所以也不会出现超卖状况,同时还可以减轻数据库的压力
限流问题
通过创建注解的方式,来实现限制用户点击,比如在一定时间内规定最多点击次数,超出次数报出错误
创建一个拦截器继承HandlerInterceptorAdapter 重写preHandle方法,然后使用request从客户端浏览器获取用户信息,将用户信息传入到ThreadLocal中确保线程安全,然后获取注解参数和URI,判断是否登录如果未登录抛出异常,如果登录则通过URI和用户的ID来拼串,使用这个串来作为缓存的key,最后从redis当中获取用户访问次数的缓存,如果为空则设置为1,如果不为空则+1,当值大于设置的最大次数时就会在前端抛出相应的错误
项目知识点
使用redis来做缓存,减少访问数据库,减轻数据库的压力
当发送大量的秒杀商品请求时,不是立即让其访问数据库,而是加入到消息队列当中,进行缓冲,所以引进rabbitmq
分布式Session:登录成功之后给用户生成一个SessionId也就是token,来表示这个用户,写到cookie当中,传递给客户端,客户端在实际使用中都再cookie当中上传这个token,服务端拿到这个token 就根据这个来取出用户的Session信息 使用redis将用户的id值+自定义前缀制作成sessionId,加入到redis当中,然后再加入到Cookie当中,然后传递给response到客户端 多个秒杀商品共享一个用户Session
将页面静态化缓存到浏览器,实现前后端分离,减少访问服务端,当页面没有发生变化时就直接从客户端浏览器直接取数据即可
保证数据库与缓存的一致性,先删除缓存再更新数据库
项目流程
用户登录到商品列表页面,静态资源缓存
用户点击详情进入到商品详情页面,静态资源缓存,Ajax动态获取验证码等信息
用户点击秒杀按钮,将验证码信息和商品id信息传送给服务端,服务端根据商品id,用户id和自定义前缀生成秒杀路径path,加入到redis当中,将path传送给前端,获取path之后,在根据path进行秒杀服务
服务端根据相应的path值,检查redis当中是否有缓存,如果存在,并且该商品还有库存的情况下,会对redis做预减库存操作,如果没有,则不会马上访问mysql,需要加入到rabbitmq队列当中,然后再从消息队列当中取消息,根据商品id和用户id,在mysql当中检查库存是否充足,如果充足则下单
下单,生成订单,跳转到订单详情页面
订单详情页面轮询订单结果
项目上线高并发问题处理
首先前端页面使用静态HTML,缓解服务器压力,降低与数据库进行交互
做一个文件服务器,因为访问图片会很消耗资源,所以将图片等资源单独部署在一个服务器上,减少压力
使用负载均衡,将大量工作分摊到多个操作单元上进行执行
使用反向代理的方式,当我们访问负载均衡的服务器时,由它来分配到相应的服务器上进行处理,处理完成之后在通过负载均衡服务器返回给用户
动静分离,将动态资源和静态资源进行分离存储。提高访问静态资源的速度,可以将静态资源分配到nginx上,动态资源放在后台服务器上
数据库sql语句优化
使用数据缓存如redis,降低对数据库的访问次数,减少数据库的压力,还可以提高查询速度
数据库可以做成集群的方式,实现读写分离,主服务器用来写,从服务器用来读,降低数据库的压力,减少io读写次数
微服务
微服务架构的核心目标是把复杂问题简单化,通过服务划分,把一个完整的系统拆分成多个高内聚、低耦合的小的子系统。使每个子系统可以独立的运行、升级和测试。然后再通过一些集成手段将这些子系统组合在一起,对外提供完整功能的过程
缺点
微服务架构整个应用分散成多个服务,定位故障点非常困难。
稳定性下降。服务数量变多导致其中一个服务出现故障的概率增大,并且一个服务故障可能导致整个系统挂掉。事实上,在大访问量的生产场景下,故障总是会出现的。
服务数量非常多,部署、管理的工作量很大。
开发方面:如何保证各个服务在持续开发的情况下仍然保持协同合作。
测试方面:服务拆分后,几乎所有功能都会涉及多个服务。原本单个程序的测试变为服务间调用的测试。测试变得更加复
稳定性下降。服务数量变多导致其中一个服务出现故障的概率增大,并且一个服务故障可能导致整个系统挂掉。事实上,在大访问量的生产场景下,故障总是会出现的。
服务数量非常多,部署、管理的工作量很大。
开发方面:如何保证各个服务在持续开发的情况下仍然保持协同合作。
测试方面:服务拆分后,几乎所有功能都会涉及多个服务。原本单个程序的测试变为服务间调用的测试。测试变得更加复
好处
整个系统的分工更加明确,责任更加清晰,每个人专心负责为其他人提供更好的服务
每个微服务都很小,足够内聚,足够小,代码容易理解
分布式
不同的业务模块部署在不同的服务器上或者同一个业务模块分拆多个子业务,部署在不同的服务器上,解决高并发的问题
分布式和微服务的区别
分布式是分散压力,微服务是分散能力
分布式是一种微服务,但是微服务不一定是分布式,因为微服务可以不用分配到多个服务器,使用同一个服务器也可以
优点
易维护,降低服务压力,系统扩展性更高
缺点
业务变的复杂
运维和测试多会变的复杂
消息队列
RabbitMQ
为什么要用消息队列?
异步
如果在下单时需要进行短信通知的话,如果不引入消息队列,那么业务流程会直接为下单然后再到短信,需要等到短信发送成功才完成整个业务,但是引入消息队列那么在下单成功后,订单业务也就完成了,不需要等待短信通知的完成。提高了效率
削峰
比如在疫情时,有大量的人感染或者以为感染,然后流量激增的开始挂号治疗,那么在此时我们的服务就有可能会扛不住,那么引入消息队列的话,将下单请求加入到消息队列当中,每次完成最大的处理量,然后那其他请求等待而不是立刻请求到服务器上
解耦
每一个服务只需要管好自己的业务即可,比如下单服务,只需要管理好下单即可,不要管理短息通知业务
使用消息队列可能出现的问题
增加程序复杂性,因为引入其他组件
会出现,消息重复,消息丢失,消息顺序问题
数据一致性问题也就是我们下单成功了但是短息通知却没有发送成功
可用性:我们把请求都放在消息队列当中了,那么当消息队列挂了怎么办?
会出现,消息重复,消息丢失,消息顺序问题
数据一致性问题也就是我们下单成功了但是短息通知却没有发送成功
可用性:我们把请求都放在消息队列当中了,那么当消息队列挂了怎么办?
市面上四种MQ的区别
ActiveMQ和RabbitMQ的吞吐量都是万级的
kafka和RocketMQ都是十万级的
时效性其他都是毫秒级而RabbitMQ是微秒
RabbitMQ是基于erlang语言开发的
kafka和RocketMQ都是十万级的
时效性其他都是毫秒级而RabbitMQ是微秒
RabbitMQ是基于erlang语言开发的
解决消息重复问题
进行接口幂等,幂等也就是同样的参数调用这个接口时,无论调用多少次结果都是一个
如果不做幂等,那么调用多少次就会生成多少次结果
如何确保幂等:
比如要拿个数据要写库,先根据主键查一下,如果已经有了,那么就别插入了
如果要是写Redis,那就没问题了,set本身就是幂等的
如果需要生产者发送每条消息的时候,里面加一个全局唯一的ID,然后消费之后根据这个id去redis当中查找,如果之前没有消费过那么就处理
或者是在数据库中加一个唯一索引来确保不会重复加入数据
如果不做幂等,那么调用多少次就会生成多少次结果
如何确保幂等:
比如要拿个数据要写库,先根据主键查一下,如果已经有了,那么就别插入了
如果要是写Redis,那就没问题了,set本身就是幂等的
如果需要生产者发送每条消息的时候,里面加一个全局唯一的ID,然后消费之后根据这个id去redis当中查找,如果之前没有消费过那么就处理
或者是在数据库中加一个唯一索引来确保不会重复加入数据
解决消息不按顺序问题
情景:一个队列,多个消费者。生产者向消息队列当中添加了三条数据,有三个消费者分别从MQ中消费数据中的一条,结果消费者2先执行完了操作,那么就会乱
解决:拆分多个队列,每个队列对应一个消费者,或者就一个队列但是对应一个消费者,然后这个消费者内部用内存队列做排队
解决:拆分多个队列,每个队列对应一个消费者,或者就一个队列但是对应一个消费者,然后这个消费者内部用内存队列做排队
消息丢失问题
生产者处丢失
在发送到Mq时丢失,此时可以选择用 RabbitMQ 提供的事务功能,就是生产者**发送数据之前**开启 RabbitMQ 事务 `channel.txSelect` ,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务 `channel.txRollback` ,然后重试发送消息;如果收到了消息,那么可以提交事务 `channel.txCommit` 。但是开启事务会影响性能
如果要确保写mq的消息别丢,可以开启confirm模式,在生产者那里设置开启 `confirm` 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 `ack` 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 `nack` 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发
如果要确保写mq的消息别丢,可以开启confirm模式,在生产者那里设置开启 `confirm` 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 `ack` 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 `nack` 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发
MQ处丢失
就是 RabbitMQ 自己弄丢了数据,这个你必须**开启 RabbitMQ 的持久化**,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,**恢复之后会自动读取之前存储的数据**,一般数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,自己就挂了,**可能导致少量数据丢失**,但是这个概率较小。
设置持久化有**两个步骤**:
- 创建 queue 的时候将其设置为持久化<br>
这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
- 第二个是发送消息的时候将消息的 `deliveryMode` 设置为 2<br>
就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。
注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。
==所以,持久化可以跟生产者那边的 `confirm` 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 `ack` 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 `ack` ,你也是可以自己重发的。==
设置持久化有**两个步骤**:
- 创建 queue 的时候将其设置为持久化<br>
这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
- 第二个是发送消息的时候将消息的 `deliveryMode` 设置为 2<br>
就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。
注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。
==所以,持久化可以跟生产者那边的 `confirm` 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 `ack` 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 `ack` ,你也是可以自己重发的。==
消费者处丢失
主要是因为你消费的时候,**刚消费到,还没处理,结果进程挂了**,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。
这个时候得用 RabbitMQ 提供的 `ack` 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 `ack` ,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 `ack` 一把。这样的话,如果你还没处理完,不就没有 `ack` 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
这个时候得用 RabbitMQ 提供的 `ack` 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 `ack` ,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 `ack` 一把。这样的话,如果你还没处理完,不就没有 `ack` 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
总结
生产者 开启事务:同步,不推荐
开启confirm模式 异步,推荐
消费者 关闭mq自动ack
mq 开启持久化
开启confirm模式 异步,推荐
消费者 关闭mq自动ack
mq 开启持久化
高可用问题
开启镜像集群模式
跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会**存在于多个实例上**,就是说,每个 RabbitMQ 节点都有这个 queue 的一个**完整镜像**,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把**消息同步**到多个实例的 queue 上。
那么**如何开启这个镜像集群模式**呢?其实很简单,RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是**镜像集群模式的策略**
跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会**存在于多个实例上**,就是说,每个 RabbitMQ 节点都有这个 queue 的一个**完整镜像**,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把**消息同步**到多个实例的 queue 上。
那么**如何开启这个镜像集群模式**呢?其实很简单,RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是**镜像集群模式的策略**
消息过期
批量重倒,在晚上用户量少的时候,这个时候写一个临时程序将丢失的数据都查出来,进行恢复
消息堆满
消费一个丢弃一个,都不要了,然后在采用过期失效的方法解决
分布式问题
分布式锁
使用redis的nx实现
使用setnx做分布式锁,如果当前不存在就创建锁,如果当前存在锁就返回null
集群下使用setnx会出现的问题
1. 客户端1作为主节点拿到lock1 2.客户端1再向从节点客户端2同步数据时,发生宕机,导致没有同步成功 3. 之后发生主从切换客户端2变成主节点 4.客户端2照样可以拿到lock1,这就会导致同一把锁被多个用户拿到
使用RedLok
采用n个master主节点的方式,来实现,n必须为奇数,保证p
具体为,1. 获取当前时间戳,单位是毫秒 2. 跟上面类似,轮流尝试在每个master节点上创建锁,过期时间较短 3. 尝试在大多数节点上建立一个锁,比如5个节点就是3个 4. 客户端计算获取锁的时间,这有这个时间小于过期时间时才算建立成功 5. 要是锁建立失败了,那么就依次之前建立过的锁删除 6. 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁
zookeeper实现
1.创建一个锁目录/lock 2. 当一个客户端需要获取锁时,在/lock下创建临时且有序的子节点 3. 客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的节点,如果是则获取锁成功,如果不是则需要监听自己的前一个节点,获取子节点的变更通知后不断重复此步骤直到获取到了锁 4. 执行业务代码,完成后删除子节点
为什么是监听前一个节点的变化,这是因为如果监听所有的子节点,那么任意一个子节点的改变,其他的子节点都会收到通知,而我们只希望它的后一个子节点收到通知。 羊群效应
redis和zookeeper实现分布式锁的区别
redis,需要自己不断去尝试获取锁,比较消耗性能
zookeeper,获取不到锁,注册一个监听器即可,不需要不断的主动获取锁
另外一点就是,如果是 Redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后 才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就 自动释放锁
分布式事务
2pc
2阶段提交,分为准备和提交两个阶段,2pc引入一个事务协调者的角色来协调管理各参与者的提交和回滚
2pc是一个同步阻塞协议,像第一阶段协调者会等待所有参与者响应才会进行下一步操作,当然第一步也有超时机制
第一阶段准备阶段,协调者会给各参与者发送准备命令,你可以把准备命令理解成除了提交事务之外啥事都做完了
同步等待所有资源的响应之后就会进入到第二阶段提交阶段
假如在第一阶段所有参与者都返回准备成功,那么协调者就会发送提交命令,然后等所有事务都提交成功时,返回事务执行成功
假如在第一阶段有一个参与者返回准备失败,那么协调者就会发送回滚命令
当在第二阶段提交失败时,无论是提交还是回滚操作都要不断的重试,直到所有的回滚成功或提交成功
协调者的单点故障问题
如果发生故障,则需要选举得出新协调者
如果处于第一阶段,骑士影响不大,都回滚好了,在第一阶段事务肯定还没提交
如果处于第二阶段,则需要考虑每个参与者的自身情况只有自己和协调者知道,因此新协调者无法通过在场的参与者的状态推断出挂了的参与者是什么情况。 这里可以说那个日志解决,让协调者将自己发过的请求记录一下,这样新的协调者就会知道什么时候应该发送了
总结
2pc是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体效率低,还有单点故障问题,在极端条件下存在数据不一致的风险
3pc
3pc相比于2pc,它在参与者处也增加了超时机制,并且新增了一个阶段,使得参与者可以利用这个阶段统一各自的状态
准备阶段,预提交阶段和提交阶段
准备阶段变更成不会直接执行事务,而是会先去询问此时的参与者是否有条件接这个事务,因此不会一来就直接锁定资源,不会阻塞
预提交阶段统一状态的作用,假如一个参与者进入到了预提交状态那么它就可以推断出其他的参与者也都进入到了预提交状态
参与者引入超时状态,防止像2pc一样 ,协调者挂了提交命令没有发出,参与者都已经锁定资源并且已经阻塞,参与者引入超时状态,当超时了,就会自动提交事务
超时机制会带来数据不一致的情况,比如在等待提交命令时超时了,参与者默认执行的是提交事务操作,但是有可能这个命令是回滚操作,这样数据就会出现不一致
总结
增加了预提交阶段使得故障恢复之后协调者的决策复杂度降低,但整体的交互过程更长了,性能有所下降,并且还会存在数据不一致
TCC
2pc和3pc都是数据库层面的,TCC是业务层面的分布式事务
TCC指的是Try Confirm Cancel
Try指的是预留,资源的锁定和预留
Confirm指的是确认操作,这一步其实就是真正的执行了
Cancel指的是撤销
和2pc一样的流程,比如一个事务要执行3个操作,那么先对3个操作执行预留动作,如果预留成功就执行确认操作,如果有一个预留失败就执行撤销动作
TCC对业务的侵入较大和业务紧耦合,撤销和确认操作的执行可能需要重试,因此还需要保证操作的幂等,TCC可以跨数据库,跨不同的业务系统来实现事务
本地消息表
本地消息表是利用了各系统本地的事务来实现分布式事务
就是本地存一张消息表,一般都是放在数据库中,然后执行业务时将业务的执行和消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中的业务肯定的执行成功的,然后在调用下一个操作,如果调用成功,消息状态直接改成已成功即可,如果失败也没事,后台任务定时去读取本地消息表,筛选出还未成功的消息在调用对应的服务,服务更新成功了在变更消息的状态
本地消息表实现的是最终一致性,容忍了数据暂时不一致的情况
可靠消息事务
系统A发送一个prepared消息到MQ,如果这个消息发送失败就直接取消了,如果发送成功,那么就接着执行本地事务,将成功告诉mq发送确认消息,如果失败就发回滚消息,如果发送确认消息,那么此时B系统会接收到确认消息,执行本地事务,mq会自动定时轮询所有prepared消息回调你的借口,问你,这个消息不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看下事务是否执行了,如果回滚那这里也会滚,避免本地事务执行成功,而确认消息发送失败了。
如果B的事务失败了咋办,重试,自动不断重试直到成功,如果实在是不行,就针对重要的资金类业务进行回滚
最大努力通知
系统A本地事务执行完之后,发送个消息到MQ,这里会有个专门消费MQ的最大努力通知服务,这个服务会消费MQ并加入到Mysql当中,或者是放入内存队列中,直接调用系统B的接口,要是系统B执行成功了就ok,失败就尽最大努力通知服务,重新调用系统B,重复N次,最后还是不行就放弃
总结
2pc和3pc和TCC是强一致性,但是还是会出现数据不一致,阻塞等风险,而且只能用在数据库层面
TCC是一种补偿性事务思想,使用的范围更广,在业务层面实现,对业务层的侵入较大
本地消息,可靠消息,最大努力都是最终一致性事务,适用于对时间不敏感的业务
CAP理论
C 一致性
等同于所有节点访问同一份最新的数据副本
A 可用性
在分布式系统中,会出现许多的问题来干扰系统,在强大的压力之下使得系统不会停止,这就是可用性
P 分区容错性
分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须在C和A中选择一个
BASE理论
它是CAP理论的延伸,核心思想是即使达不到强一致性,但应用可以采用是和的方式达到最终一致性
基本可用
指在分布式系统中出现故障时,允许损失部分可用性,即保证核心可用
软状态
指允许系统存在中间状态,而该中间状态不会影响系统整体可用性
最终一致性
指系统中的所有数据副本经过一定时间后,最终可以达到一致的状态
集合
HashMap
HashMap的数据结构
1.7时HashMap底层由数组+链表的组成
1.8时HashMap由数组+链表或红黑树组成
为什么是线程不安全的
HashMap在多线程情况下,在put的时候,会有很多线程同时添加值,并且扩容,扩容会产生很多新的数组,这样只有最后一个线程扩容成功,但是会丢失很多值。在多线程的环境下,同时存在其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的
1.7和1.8的区别
1.7中采用头插法,会导致值的引用循环,1.8采用尾插法解决了这个问题
引用循环
resize方法中的transfer方法 线程A和线程B, 线程A进入while循环时, 执行到Entry<K,V> next = e.next;时被挂起了, 这时该线程A中的e=1, e.next=11. 此时线程B进入while循环, 这时线程B中的e=1, e.next=11, 然后线程B继续向下执行, 执行完第一次while循环之后, 链表的顺序就变为11,1,12,13,14,15. 这时元素11的下一个元素时1,
而此时线程A中元素1的下一个元素为11. 完美! 产生了循环链表!
而此时线程A中元素1的下一个元素为11. 完美! 产生了循环链表!
1.7和1.8的数据结构不相同
1.7是先扩容再赋值,1.8中是先赋值在扩容
1.7中是超出阈值并且发生hash冲突时才会扩容,1.8是超出阈值。阈值=容量*加载因子
1.8中简化了hash方法,采用(key.hashcode>>>16)^(key.hashcode)的方法这个方法可以减少发生hash碰撞,也就是那些高16位不同的值和低位相同的值发生碰撞,混合高16位和低16位的值,得到一个更加散列的低16的值。
头插法的效率更高,尾插法需要遍历到链表的尾部进行插入,而头插法不需要,节省了遍历的时间
1.8中put的流程
put方法的实现是由putVal方法实现的,put方法总会计算key的hash值
首先会判断数组是否需要进行初始化,需要则使用resize方法进行扩容
根据key的hash值定位出要存放的位置,如果头节点为空则直接插入
头节点不为空,则说明发生hash冲突
需要判断是否为红黑树节点,如果是红黑树节点需要在红黑树中查找
在链表中进行查找,如果链表中有与key相同的key值和hash值,则直接覆盖旧值。如果没有则采用尾插法插入。插入完成之后需要判断链表长度是否大于阈值8并且数组长度是否大于64,如果满足则将链表转化为红黑树
修改次数+1,容量+1,判断是否大于阈值,如果大于则扩容
1.8中扩容机制
首先会保留老数组的容量和阈值,初始化新数组的容量和阈值
如果老数组容量大于0,如果大于0判断老数组容量是否大于最大值,如果大于则返回老数组。如果不大于最大值,创建新数组新数组的容量是旧数组的两倍小于最大值并且老数组容量大于等于默认值,则将老阈值扩大两倍赋值给新阈值
如果容量小于0,判断老阈值是否大于0,满足将老阈值赋值给新容量,老容量小于0老阈值大于0说明使用的构造方法创建的HashMap用户自定义了容量和加载因子
当老容量小于等于0,老阈值小于等于0,将默认的大小赋值给新数组容量,将阈值赋值给新数组的阈值
如果新阈值等于0,则重新指定
将新阈值赋值给新数组的阈值,创建新数组,将新数组容量赋值给新数组
接下来的步骤就是将旧数组上的键值对转移到新数组上
过程为遍历老数组,释放掉老数组需要转移走的元素的引用,如果元素没有下一个节点,说明不存在hash冲突,直接定位出要存放在新数组中的位置。如果有下一个节点说明发生hash冲突,则需要判断是否为红黑树节点,如果是调用split方法进行赋值。如果不是红黑树节点,则说明在链表当中。会定义4个指针。低位首尾节点和高位首尾节点分别为0---oldCap-1、oldCap---newCap-1.遍历链表,通过判断该元素的hash值和老数组的容量做 按位与 运算是否等于0,等于0说明该hash值的最高位一定为0,说明和存放在老数组中的位置一样。不等于0说明超出旧数组的范围放在高位。最后判断高低位尾结点是否为空,低位尾结点不为空时,低位元素还放在原来的位置,高位尾结点不为空时,存放的位置偏移了老数组长度个位置。
扩容结束,为原来的两倍
为什么HashMap每次扩容都是2的幂次方倍
HashMap中计算数组下标的公式为(n-1)& hash,当数组长度为2的幂次方时它-1所得的值所有位都为1,所以这个公式会完全取决于hash值的散列性,如果hash值分配均匀那么下标就会分配均匀
HashMap与HashTable的区别
HashTable的底层是数组+链表的方式,它是线程安全的采用Synchronize进行加锁,HashMap不是线程安全的,HashMap的效率会比HashTable高一点,HashTable的默认初始容量为11每次扩容为2n+1,而HashMap默认初始容量时16,每次扩容为2的幂次方倍。并且当传入指定的容量时,HashTable会立即使用这个值,而HashMap会使用大于它最近的2的幂次方倍的容量
HashTable
扩容2n+1,是为了每一次扩容的值都是奇数它寻找下标是根据取模,所以减少hash冲突,但是效率低
为什么加载因子为0.75f
加载因子是用来表示hash表中填满程度,加载因子 = 填入表中的元素个数 / 散列表的长度
加载因子越大,表示表中的数据越多,空间利用率高,发生hash冲突的概率也就越大
是根据泊松分布,它适用于描述单位时间内随机事件发生的次数的概率分布,为了在时间和空间进行折中的选择
并发Map
并发Map的数据结构
1.7时,由Segment+HashEntry+链表的方式组成
1.8时,由数组+链表或红黑树的方式存在
如何保证并发安全
1.7时采用分段锁,对每一个Segment加锁,每一把锁只锁一部分数据,这样可以避免发生锁竞争,Segment一旦初始化就无法改变,默认为16,也就是最大支持16个线程并发
1.8使用CAS+Synchronize保证并发安全Synchronize只锁定红黑树和链表的头节点,这样只要不发生hash冲突就不会出现并发安全问题
put流程
put方法调用putVal方法
首先会判断key和value是否为空,如果为空则直接抛出空指针异常
判断数组是否需要进行初始化,需要则进行resize方法初始化
通过spread方法定位出key的hash值要存放的位置,spread方法保证hash值的范围在int范围之内, (h ^ (h>>>16))&HASH_BITS,HASH_BITS是 最大正整型数 除最最高位为0,其余位都为1
根据定位出的位置判断头节点是否为空,为空则说明不发生hash冲突,则直接cas插入,插入失败自旋保证成功
判断hash==move==-1,如果等于-1则说明数组正在进行扩容helptransfer方法帮助进行扩容
如果头节点不为空,则说明发生hash冲突
进入同步代码块
判断当前头节点是否小于0,小于0则说明在红黑树中,然后调用相应方法进行插入
再次判断当前key的hash值是否等于头节点的hash值,如果当前hash值大于0则说明要在链表当中查找,如果有和key的值相等的hash值和key值则直接CAS覆盖插入,返回旧值。如果没有则采用尾插法CAS插入
判断当前链表是否需要转化为红黑树
addCount+1
1.7put流程
首先判断value是否为空,为空抛出空指针
计算key的hash值,根据hash值无符号向右移28位与segmentMask=15做按位与操作,定位出存放的Segment位置
初始化Segment流程
检查计算得到的位置的Segment是否为null
为null继续初始化,使用Segment[0]的容量和负载因子创建一个HashEntry数组
再次检查指定位置的Segment是否为null,因为这时可能其他线程进行操作
使用创建的HashEntry数组初始化Segment
自旋判断计算得到的位置的Segment是否为null,使用CAS在这个位置赋值为Segment
Segment.put插入key,value
首先利用Segment继承了ReentrantLock。使用tryLock方法获取锁,获取不到scanAndLockForPut方法继续获取,这个方法是不断自旋tryLock获取锁,当超过一定次数,使用lock方法阻塞锁。
计算put的数据要放入的index位置,然后获取这个位置上的HashEntry
遍历put的HashEntry数组
当这个位置上的HashEntry不存在,如果当前容量大于扩容阈值,小于最大容量,进行扩容。头插法插入
如果这个位置上的HashEntry存在,判断key的值和hash值是否与要put的一样,一样则直接覆盖
不一致,获取链表下一个节点直到相同进行替换或者没有不同的则头插法插入,判断当前容量是否大于阈值小于最大容量。
如果要插入的位置在之前已经存在,替换后返回旧值,否则返回null
1.7put流程
判断value是否为空,为空抛出空指针异常
计算出key要存放的Segment
判断当前Segment是否需要进行初始化
使用Segment.put进行插入
tryLock获取锁
再次计算key要存放的索引位置,然后获取这个位置上的HashEntry
判断当前位置的HashEntry是否存在,如果不存在,则头插法插入,如果存在则判断该位置的key是否与插入的key相等,如果相等则覆盖,如果不等则在链表的下一个节点处直到遇到相同的值或者没有相同的值,采用头插法插入
头插法插入,判断是否超出阈值并小于最大值,满足扩容
1.7扩容机制
每一次扩容只会扩容为当前的两倍
保存老数组的容量和阈值,新建新容量和新阈值
遍历老数组
计算新的位置,新的位置只可能是不变或者是老位置索引+老容量
计算新位置的hash值,当前位置的hash值&sizeMark,sizeMark为新容量-1
get流程
首先获取key的hash值定位出要查询的位置
找到对应的数组中头节点下标
如果使我们要的值,则直接获取,返回value
如果是null则返回null
如果当前key的hash值小于0则需要在红黑树中查找
大于0则说明要在链表中查找
size流程
并发map有两个获取size的方法,size方法返回的是int类型,mappingCount方法返回的是long类型
mappingCount方法中使用sumCount方法实现
sumCount中有两个辅助变量countCells和baseCount还有一个CountCell辅助内部类
sumCount就是迭代countCells的过程,来统计sum
size也是通过sumCount方法,这个方法返回一个long类型的值,如果这个值大于int的最大值,则返回最大值,如果不大于则直接返回
put方法后size需要改变,在put方法最后调用addCount方法对数组容量+1
addCount:如果countCells==null,则对baseCount进行cas自旋自增操作
如果并发导致baseCount自旋失败,则使用countCells
如果countCells并发自旋失败,则在fullAddCount方法中进行死循环,直到成功
CountCell类是一个标有@sum.misc.Contended标识的类,其中还有一个volatile标识的变量
@sum.misc.Contended这个标识是为了防止伪共享,伪共享就是当修改互相独立的变量时,如果这些变量共享同一行缓存行,那么就会无意之间影响性能。
List、Map、Set
List、Set都是实现与Collection接口的
List中存储的元素是有序的,可重复的
Set中存放的元素是无序的,不可重复的
Map中存放的元素格式为Key-Value格式,key和value都是无序的,key是不可重复的,value是可重复的
LinkedList和ArrayList
底层实现
LinkedList底层使用的是双向链表,1.6之前使用双向循环链表,1.7取消了循环。不支持快速随机查询,添加或删除具体位置元素的时间复杂度为o(n)因为要先移动到该位置在进行插入或删除
ArrayList底层使用的是数组,它可以理解为动态数组,会动态增加数组长度,可以快速随机查询查询时间复杂度o(1) ,添加或删除具体位置元素时间复杂度为o(n-i)数组长度-当前索引,因为当前索引之后的元素会向后移动一位
区别
底层数据结构不同
都是线程不安全的
一个支持随机访问一个不支持
LinkedList比ArrayList中的元素更加消耗内存因为要存储前后节点
ArrayList插入和删除受元素的位置所影响,LinkedList不受影响
LInkedList主要实现List和Deque接口,ArrayList主要实现List和RandomAccess
ArrayList扩容机制
如果未指定初始容量则初始容量为0
当调用add方法时才会进行扩容
ensureCapacityInternal进行判断是否是使用的默认构造器创建的,初始值容量为10,求出最小需求空间=当前传入的最小需求容量和10做比较
初始化完成之后,如果再添加数据的数据,就会调用ensureExplicitCapacity方法进行判断,如果最小需要空间大于当前空间就会进入grow方法进行扩容
进入grow方法 扩容为原来的1.5倍
如果扩容之后还是无法满足空间,就将传入的大小赋值给新容量
如果满足空间但是大于默认最大值那么就会调用hugeCapacity方法进行检查溢出问题
最后调用Arrays.copyOf(elementData, newCapacity)将原数组中的数据复制到新数组中
ArrayList使用for循环删除元素出现的问题
报出Exception in thread "main" java.util.ConcurrentModificationException 并发修改问题
ArrayList有两种修改方法一个是remove另一个是fastRemove,一个根据对象删除一个根据索引删除,其实foreach写法是对实际的Iterable、hasNext、next方法的简写,问题在fastRemove方法中,它的第一行是把modCount变量的值加一,这里会做迭代器内部修改次数检查,因为上面的remove(Object)方法修改了modCount的值,所以才会报出并发修改异常。
不要使用ArrayList的remove,改为用Iterator的remove即可
Java.lang.IllegalStateException异常(非法状态异常)出现的原因是 删除了一个不满足条件的元素。通过Iterator来删除,首先需要使用next方法迭代出集合中的元素,然后才能调用remove方法,否则集合可能抛出java.lang.IllegalStateException异常。
总结
通过foreach方式进行删除的modCount变量的改变,会出现非法状态异常,可通过iterator迭代器的方式进行判断,删除。
通过for循环变量list的长度,正序来进行list中元素的移除,会出现漏删除的情况,可通过倒序删除的方式来解决。
System.arraycopy方法,导致删除元素时涉及到数组元素的移动。
TreeMap
底层结构
TreeMap底层使用的是红黑树,具有有序性
LinkedHashSet和HashSet和TreeSet
LinkedHashSet底层基于LinkedHashMap
HashSet是基于HashMap实现的,但是它用来存储对象,而不是键值对
TreeSet底层是红黑树
LinkedHashSet和TreeSet都能根据顺序来遍历,但是TreeSet还支持自然排序和自定义排序
comparable和comparator接口的区别
Comparable是Java.lang包下的其中有一个compareTo(Object obj1)方法
Comparator是java.util包下的其中有一个compare(Object obj1, Object obj2)方法
Comparator接口中不只有compare一个方法。
在我们一般定义类的时候使用Comparable接口,在类中添加功能的时候使用Comparator接口
集合层级
Collection
List
ArrayList
LinkedList
Vector
Stack
栈继承于动态数组
Queue
Deque接口
ArrayDeque
PriorityQueue
优先级队列,底层采用的数组来维护一个小顶堆,优先队列不允许空值,而且不支持non-comparable(不可比较)的对象,比如用户自定义的类。添加进来的值可以通过默认排序规则排序,还可以通过实现Comparator接口来实现自定义排序,它是线程不安全的,如果要用线程安全的需要使用PriorityBlockingQueue,通过ReentrantLock可重入锁保证多线程场景下队列集合的安全性
Set
HashSet
LinkedHashSet
SortSet接口
TreeSet
Map
HashMap
LinkedHashMap
底层采用数组+链表或红黑树组成,在这个基础上增加了一个双向链表来保证插入数据的有序性
使用场景
LRU系统
TreeMap
底层红黑树
场景
需要排序的统计功能,可以自定义排序功能
Hashtable
Java基础
equals和==的区别
它的作用是判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象。基本数据类型==比较的是值,引用数据类型==比较的是内存地址
equals() : 它的作用也是判断两个对象是否相等,它不能用于比较基本数据类型的变量。`equals()`方法存在于`Object`类中,而`Object`类是所有类的直接或间接父类。
我们一般重写equals方法的时候,将他重写后的功能为比较双方的值内容是否相同,例如String中重写的equals方法
hashcode和equals
因为hash值可能会发生hash冲突
两个相同的对象,他们的hashcode一定相同
两个相同的对象,他们调用equals方法时一定会返回true
具有相同的hashcode的两个对象不一定相等
所以在重写equals时必须重写hashcode
hashcode的默认行为是对堆上的对象产生独特值,如果没有对象没有重写hashcode,那么他们无论如何都不会相等,即使指向相同的值
接口和抽象类的区别
接口中的方法必须为抽象方法,抽象类中的方法可以为抽象方法也可以为普通方法
抽象类是基于继承关系而接口是基于实现关系
一个抽象类可以只能被一个类所继承,一个接口却可以被多个类所实现
抽象类中具有构造器,但是不能实例化,而接口中不具有构造器
抽象类中有public,protected等修饰符修饰,但接口中只有public
String,Stringbuffer,Stringbuilder
String中的字符串是不可以变的,因为用final修饰,但是其它那两个没有,所以是可变的
操作少量字符串时使用String
单线程下操作大量字符串时使用StringBuilder,它是非线程安全的,效率高一点
多线程下操作大量字符串时使用StringBuffer,它是线程安全的,采用加锁的方式,效率低
String中的char[] value 是被private final修饰的,而StringBuilder是继承AbstractStringBuilder类的它其中的char[] value没有修饰符修饰,所以String是不可变的
速度上 在拼接字符串的时候,每次拼接String都会新创建一个对象,而StringBuilder会一直使用一个对象
在需要编译器优化的时候,String的速度会比较快,比如String s="a"+"b"+"c"; 在编译时就会为String s="abc";
String类型为什么设置为不可变的
1.方便使用字符串池,会有大量的String变量,如果每一个都创建一个对象的话则会极大的浪费空间,所以提出了字符串池的概念,当创建一个字符串时首先会去池中查找是否具有相同的字符串如果有就不会在创建了
2.在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全
3.安全因素,在网络中经常使用字符串类型当做参数,为了防止这个参数在传递的过程中被恶意修改
4.提高速度,创建一个字符串为不可变时,保证了hashcode的唯一性,那么他的hashcode也会被确定存储在缓存当中,不需要重新计算
异常
异常的最上层父类为Throwable,Throwable分为Exception和Error
常见的受检查异常ClassNoFoundException和SQLException等,非受检查异常常见的有空指针异常,ClassCastException类类型转换异常,ArrayIndexOutOfBoundsException,数组下标越界异常
Error错误程序自身无法解决,比如内存溢出OOM,NoClassDefFoundError类定义错误
throw和throws
throw
用在方法体内部用来抛出一个Throwable类型的异常,如果是检查异常则需要在方法声明处,声明要抛出的错误,该调用者必须处理异常或继续抛出异常
throws
用在方法声明处,用来声明程序有可能需要抛出的异常,仅当是受检查异常时调用者必须进行处理或者继续抛出异常。当调用者无法处理异常时应当继续抛出异常
异常的机制
当程序中抛出一个异常后,程序从程序中导致异常的代码处跳出,java虚拟机检测寻找和try关键字匹配的处理该异常的catch块,如果找到,将控制权交到catch块中的代码,然后继续往下执行程序,try块中发生异常的代码不会被重新执行。如果没有找到处理该异常的catch块,在所有的finally块代码执行后,遇到异常的当前线程被中止。
为什么不将所有的代码用try-catch修饰
因为异常是一种耗费资源的机制,每次抛出异常时,异常的堆栈都会建立,异常的信息也会被加载,这样给程序带来一些负担。所以,能加则加,不能加的决不盲目的加。
泛型
泛型是jdk1.5之后提出的一种类型安全监测机制,它将类型当做参数传入。
泛型擦除,在java编译期间所有的泛型信息都会被消除
泛型可以用来修饰方法,类,接口
深浅拷贝
浅拷贝,对于基本数据而言传递的是值,对于引用数据类型是传递引用般的拷贝
深拷贝,对于基本数据而言是值传递,对于引用数据类型,先创建一个对象,然后将其内容全部拷贝到这个对象中
final关键字
修饰成员变量,必须要赋初始值,而且只能初始化一次,可以直接赋值或在构造方法中赋初值。如果final修饰的成员变量是基本类型,则表示这个变量的值不能改变,如果修饰的成员变量是一个引用类型,则引用的地址不能改变,但是这个引用所指向的对象里面的内容可以改变。
被final修饰额变量只初始化一次
在构造方法完毕前
在构造方法完毕前
被final修饰的方法不能被重写覆盖,可以被重载,一个private方会隐式的指定为final方法
被final修饰的类不能被继承
面向对象
面向对象的三大特征为,封装继承多态
封装:将一个对象中的属性信息隐藏在对象的内部,使其他对象不能直接访问,但是会向外界提供访问方法来进行访问
继承:在一个类的基础上构建新的类,子类可以拥有父类中的所有方法,但是不能使用被private所修饰的方法只是拥有
多态:一个对象具有多种状态,具体为父类的引用指向子类的实例,不能调用只在子类中存在而父类中没有的方法
重载和重写
重载(Overload)实质表现就是多个具有不同的参数个数或者类型的同名函数(返回值类型可随意,不能以返回类型作为重载函数的区分标准)同时存在于同一个类中,是一个类中多态性的一种表现(调用方法时通过传递不同参数个数和参数类型来决定具体使用哪个方法的多态性)
重写(Override)是父类与子类之间的多态性,实质是对父类的函数进行重新定义,如果在子类中定义某方法与其父类有相同的名称和参数则该方法被重写,不过子类函数的访问修饰权限不能小于父类的
重载与重写是 Java 多态性的不同表现。
重写是父类与子类之间多态性的表现,在运行时起作用(动态多态性,譬如实现动态绑定)
而重载是一个类中多态性的表现,在编译时起作用(静态多态性,譬如实现静态绑定)
重写是父类与子类之间多态性的表现,在运行时起作用(动态多态性,譬如实现动态绑定)
而重载是一个类中多态性的表现,在编译时起作用(静态多态性,譬如实现静态绑定)
static关键字
static关键字可以修饰成员方法,成员变量,代码块
修饰方法时,该方法不属于任何一个对象的实例,而是属于类,在类加载的时候,该方法就会进行执行,在静态方法中,不能访问非静态变量和非静态方法
修饰变量时,该变量属于类,而不属于对象,被所有对象所共享,在内存中只存在一个副本,随着类初次加载而初始化
修饰代码块时,该代码块属于类,可以加在类中任意位置,随着类的初次加载而先被初始化,会按照顺序执行
Object中的方法
hashcode方法,计算hash值
equals方法
notify方法:会唤醒一个在此对象监视器下的线程,如果有多个也只会唤醒一个
notifyAll方法:会唤醒所有在此对象监视器下的线程
wait方法:使线程发生阻塞,知道其他线程通知唤醒。有三个不同的参数wait方法
finalize方法:实例被垃圾回收器回收的时候触发的操作
clone方法:用于创建并返回当前对象的一份拷贝,没有实现cloneable接口需要重写clone方法,否则会发生克隆不支持异常CloneNotSupportedException
toStirng方法:返回类的名字@实例的哈希码的16进制的字符串
getClass方法:获取当前类对象
自动拆装箱
装箱:将基础数据类型用他们对应的包装类进行包装
拆箱:将包装类转化为对应的基础数据类型
反射机制
反射机制指的是,在运行状态时,可以动态的知道一个类中的所有属性和方法,对于任意一个对象都能动态的调用他的属性和方法
运行期类型判断,动态加载类,提高代码灵活度
性能低,反射是一系列的解释操作,通知jvm要做的事情
安全问题,可以在运行期间动态的获取信息
反射是实现框架的灵魂,创建的应用有JDBC中Class.forName和Spring ioc中动态加载管理bean信息,AOP动态代理中
功能
在运行时判断任意一个对象所属的类,在运行时构造任意一个类的对象,在运行时判断任意一个类所具有的成员变量和方法,在运行时调用任意一个对象的方法,生成动态代理。
内部类
成员内部类
与成员变量一起定义,可以访问外部类中所有变量,包括private修饰的
静态内部类
与成员一起定义,只能访问外部类的静态资源,不能访问非静态资源,非静态内部类在编译完之后会默认保存一个指向外部类的引用
局部内部类
定义在方法中,访问局部变量时,必须访问带有final修饰的变量,保证一致性,局部类中的局部变量实际是通过构造方法传入的,如果不用final修饰会导致引用发生改变,而内部类却不知道发生改变,这就引发了错误
匿名内部类
没有名字,具体为一个类的子类或者实现接口的子类匿名对象
BIO、NIO、AIO的区别
BIO是同步阻塞的IO模型,面向流的,数据的读写都必须阻塞在一个线程中完成,无用的请求也会占用一个线程,没有数据到达也会阻塞。不能用在高并发的情况下
因为服务端会通过accept方法来监听客户端的连接,每当产生一个连接,就会分配一个socket进行处理。这个accept方法是一个阻塞方法。
在获取到Socket连接后,从流中读取数据时也会发生阻塞
NIO是同步非阻塞IO模型,基于通道面向缓冲区的,它提供了与BIO相对应的套接字,ServerSocketChannel,SocketChannel,可以实现阻塞和非阻塞的切换,在BIO中需要将数据读入或者写入到Stream对象当中,而NIO将数据写入到Buffer中进行处理,Selector选择器可以同时处理多个通道,NIO中是通过selector.select()方法去查询每个通道是否有到达事件,如果没有事件就会一直阻塞在那里,因此这种方式会导致用户线程阻塞。
IO多路复用
会有一个线程不断去轮询多个Socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作,多路复用IO需要轮询负责select/epoll查询调用的线程,查找出可以进行IO操作的一个连接,对于每一个可以查询的socket,一般需要设置为non-blocking
优点
select/epoll 可以**同时处理**成百上千的连接,与之前的一个线程维护一个连接相比,IO多路复用则不需要创建线程,也就不需要维护,从而**减少系统开销**。
缺点
select/epoll系统调用,属于**阻塞**的模式,**同步IO**。读写事件就绪之后,**用户自己进行读写**,这个**读写过程也是阻塞的**
select/epoll 可以**同时处理**成百上千的连接,与之前的一个线程维护一个连接相比,IO多路复用则不需要创建线程,也就不需要维护,从而**减少系统开销**。
缺点
select/epoll系统调用,属于**阻塞**的模式,**同步IO**。读写事件就绪之后,**用户自己进行读写**,这个**读写过程也是阻塞的**
select和epoll的区别
select的timeout是微秒的,epoll是毫秒,所以select的实时性会更好
select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024或2048,因此默认只能监听少于 1024或2048 个描述符,epoll没有描述符数量限制
select支持全平台,epoll只支持linux
需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试
epoll支持LT和ET 行触发,和边缘触发,行触发是epoll_wait检测到了描述符时间到达,将其加载到进程中,进程可以不用立即处理,而ET需要立即处理
epoll使用了epoll_event结构来处理,没有最大连接数的限制
select函数每次需要遍历fd_set,判断标志位有没有发生变化,如果发生变化则通知程序做中断处理
epoll函数不需要每次遍历全部文件描述符,epoll把就绪文件的文件描述符专门维护了一块空间,每次从就绪队列当中拿就可以了,这是两者最大区别
fd_set就是bitmap的数据结构,可以简单理解为只要位为0,那说明还没数据到缓冲区,为1反之
select函数每次需要遍历fd_set,判断标志位有没有发生变化,如果发生变化则通知程序做中断处理
epoll函数不需要每次遍历全部文件描述符,epoll把就绪文件的文件描述符专门维护了一块空间,每次从就绪队列当中拿就可以了,这是两者最大区别
fd_set就是bitmap的数据结构,可以简单理解为只要位为0,那说明还没数据到缓冲区,为1反之
AIO
java1.7引入。当应用操作之后会直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作
值传递
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)
- 一个方法可以改变一个对象参数的状态
- 一个方法不能让对象参数引用一个新的对象
- 一个方法可以改变一个对象参数的状态
- 一个方法不能让对象参数引用一个新的对象
Thread和Runnable的区别
Thread其实就是一个Runnable它实现了Runnable接口,它是对Runnable的一种包装,他还有一个Runnable类型的变量target,用来标注当前要在这个线程中执行的任务,Runnable中有一个void run方法。当该线程中执行这个任务时,就会调用Runnable中的run方法,否则虚拟机会调用该线程自身的run方法
Thread是继承Runnable是实现接口,避免单继承的问题具有优势
线程池中只允许实现Runnable的类线程,而不允许Thread
git
解决版本冲突
原因
在本地有不止一个版本,在提交时git不知道要提交哪一个
方案
首先使用 git pull,把本地版本更新成最新的
git diff,此命令比较的是工作目录(Working tree)和暂存区域快照(index)之间的差异(也就是修改之后还没有暂存起来的变化内容)
编辑一下冲突的文件:vim 日记
把那些多余的字段都删除掉,然后就commit一下再push一下就可以了
把那些多余的字段都删除掉,然后就commit一下再push一下就可以了
并发
volatile关键字
作用
指令重排
cpu和编译器为了更好的执行程序,它们会对指令进行优化,这往往会引起一些逻辑上的错误,这个逻辑错误指的就是,程序代码的顺序。只有当单线程下不改变结果的情况下就会发生指令重排,两条语句间是相互独立的。jvm提供了happens-before8条原则,还提供了4个屏障(读读,读写,写读,写写)来防止发生指令重排
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
volatile关键字可以保证被修饰变量的可见性,防止指令重排,被它修饰的变量不能保证原子性
为什么不能保证原子性,因为在java中之只有基础变量具有原子性像int i=1,但是int j=i 或 i++这种操作就不是原子性的,所以不能保证原子性
底层原理
volatile底层遵循MESI缓存一致性协议。
当需要修改本地内存中的变量时,需要经过一些步骤,首先修改全局变量在工作内存中的缓存,并且要读取到主内存当中
在读取到主内存之前需要将其他使用该变量的缓存失效
线程修改完成之后将数据保存在主内存中给变量
其他线程在使用该变量时就会发现自己的工作内存中的缓存失效了,就会重新从主内存中获取到最新值
lock前缀,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中
volatile关键字在双重锁实现单例作用
保证instance实例在主内存中的可见性
Synchronize关键字
作用
用于解决多线程之间访问数据同步性的问题
底层原理
当synchronize修饰代码块的时候,它使用的是monitorEnter和monitorExit来实现,monitorEnter表示进入同步代码块,尝试获取对象监视器monitor的使用权,线程尝试获取锁,如果计数器为0表示可以获取,获取到了计数器+1,monitorExit表示退出同步代码块,锁计数器设为0
当synchronize修饰方法的时候使用的是ACC_SYNCHRONIZE标识来实现,该标识指明该方法是同步方法
性能
1.6之前它是重量级锁,因为monitor对象监视器是依赖于操作系统互斥锁所实现的,而java线程是映射到操作系统上的线程的,而操作系统实现线程之间的切换需要从内核态转化为用户态,所以需要很长时间
1.6之后对它做了很大优化,如引入自旋锁,适应性自旋锁,偏向锁,轻量级锁,锁消除,锁粗化等
1.6之后引入锁升级
只能升不能降,无锁->偏向锁->轻量级锁->重量级锁
偏向锁和轻量级锁都是为了解决重量级锁在没有多线程竞争的条件下,使用底层操作系统互斥量带来的性能问题
轻量级锁和偏向锁不同的是,轻量级锁在无竞争的条件下会使用CAS来替代使用互斥量,而偏向锁在无竞争的条件下直接会把整个同步消除
偏向锁的加锁
当一个线程获取同步代码中的锁时,会在该线程的对象头和栈帧中的锁记录中记录锁偏向的线程ID,以后该线程进入同步块时就不会需要在获取锁,只需要检测一下,锁对象对象头中的MarkWord里是否记录指向当前线程的偏向锁,如果有则获取成功,如果没有则失败,失败会判断当前线程对象头中MarkWord里偏向锁标识是否设置为1,如果没有设置为1,则CAS竞争锁,如果设置为1,则CAS将锁对象头中的偏向锁指向当前线程
偏向锁解锁
直到发生锁竞争的时候就会释放掉偏向锁,首先会暂停持有偏向锁的线程,再检查锁对象是否存活,如果存活则将锁对象对象头中的MarkWord和栈帧中的锁记录里的偏向锁指向其他线程或者设置为无锁状态,如果当前锁对象不存活,则设置为无锁状态
锁消除
如果虚拟机编译器运行时检测到一些数据根本不可能发生竞争,就会使用将锁消除,来避免不必要的损失
锁粗化
凡事都需要有一个度,有些情况下我们需要将多个同步请求合并成一个同步请求,这样反而更加快速
可重入锁
可重入锁的实现原理是怎么样的?
加锁时,需要判断锁是否已经被获取。如果已经被获取,则判断获取锁的线程是否是当前线程。如果是当前线程,则给获取次数加1。如果不是当前线程,则需要等待。
释放锁时,需要给锁的获取次数减1,然后判断,次数是否为0了。如果次数为0了,则需要调用锁的唤醒方法,让锁上阻塞的其他线程得到执行的机会。
加锁时,需要判断锁是否已经被获取。如果已经被获取,则判断获取锁的线程是否是当前线程。如果是当前线程,则给获取次数加1。如果不是当前线程,则需要等待。
释放锁时,需要给锁的获取次数减1,然后判断,次数是否为0了。如果次数为0了,则需要调用锁的唤醒方法,让锁上阻塞的其他线程得到执行的机会。
ReentrantLock
特性
它是一种可重入锁,即可以再次进入到当前锁对象的内部
等待可中断
可以调用lock.interrupt方法来使当先线程停止等待,去做其他事情
可以实现公平锁和非公平锁,默认非公平
可以实现选择性通知
使用Condition类和newCondition方法来实现,一个lock可以创建多个condition实例,多个线程对象可以注册到condition中,从而可以实现多路通知功能,使用condition中的signalAll方法来实现选择性通知功能,唤醒注册在该condition中的线程对象
synchronize会自动释放锁,而Lock不行必须手动释放锁
原理
Lock底层实现基于AQS实现,采用线程独占的方式,在硬件层面依赖特殊的CPU指令(CAS)。
简单来说,ReentrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态
简单来说,ReentrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态
使用AQS实现公平锁和非公平锁的原理
ReentrantLock中有一个sync内部类,该类继承了AQS
非公平锁的实现和公平锁的实现,非公平锁在调用lock时,首先会进行cas抢锁,如果当前锁没有被其它线程所占有,那么会直接占用,如果当前锁被占用,则会和公平锁一样会进入到tryAcquire方法,如果发现锁被释放了,那么非公平锁会直接cas抢锁,而公平锁需要判断当前是否存在CLH等待队列,如果存在则等待队列中最先等待的线程获取锁,该线程会加入到等待队列中排队。
如何保证可重入
非公平锁为例
lock首先会使用cas对state进行判断,如果是0则说明当前锁没有被占用,则使当前线程占有锁
如果为1则被其他占用,进入acquire方法,里面有一个if( ! tryAcquire(1)&&···)的判断进入到tryAcquire方法
进入到这个方法时会调用nofairTryAcquire方法进行判断,判断当前状态值是否为0,为0则设置当前线程占用锁将state设置为1,如果不为0,则会判断当前对象与当前持有锁对象是否相等,相等会增加状态值,从这里就能看出来可重入锁的原则,同一个线程可以反复的使用它占用的锁
如果都不满足返回false,在acquire中就会执行acquireQueued方法也就是CLH队列抢占
Java内存模型(JMM)
线程可以把变量保存到本地内存当中,而不需要每一次都从主存中获取,这就导致了一致性问题
线程不能直接读写主内存的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中
线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本
并发编程三大特征
原子性
一个操作或多个操作对一个数据进行修改,要么全都执行而且不受外界干扰而中断,要么都不执行
一致性
当一个线程对一个共享变量进行修改时,另一个线程可以立即看到修改后的最新值
有序性
程序的执行有先后顺序
CAS
底层原理
CAS底层使用的是UnSafe类,这个类中很多的方法都是Native修饰的方法也就是本地方法,它是用来实现java和操作系统进行交互的方法。cas方法有三个参数,内存位置,预期原值,新值。
概念
比较并交换,是一种乐观策略,通过与预期的值做比较,如果等于预期则更新新的值,如果不等于则自旋直到成功为止,用来鉴别线程冲突,一旦检测到冲突就会重复当前操作直到没有冲突为止,减少了锁竞争带来的开销和线程间频繁切换带来的开销,它是一组原子操作是CPU指令级别的,所以非常快
应用
AutomaticInteger类中使用cas,AQS中使用cas,HashMap中使用cas等等
问题
会引发ABA问题
使用一个版本号解决ABA问题,ABA问题是一个线程获取到变量最开始的值,进入等待状态,另外一个线程也获取到了该变量它将当前变量改为另外一个数,改完之后又改回原来的数退出。此时第一个线程被唤醒,它读到的还是原来的数,但是这个数已经被修改过了。使用原子引用AtomicReference + 某种记录机制类似于时间戳=AtomicStampedReference
自选时间过长
只能保证一个共享变量的原子操作
可以讲多个变量封装为一个对象解决
AQS
概念
AbstractQueuedSynchronizer的缩写,一个用来构建锁和同步器的框架
思想
当一个线程获取一个共享资源的时候,如果这个共享资源没有被其他线程所持有,那么会将该资源设置为持有状态,如果这个资源已经被持有,那么就需要一个等待通知唤醒的机制来实现线程获取资源的机制。这个机制AQS使用了一个CLH虚拟队列来完成,将需要进行等待的线程放入到CLH队列当中,AQS还有一个int型变量state来表示当前同步状态。
原理
AQS使用一个int型变量state来表示同步状态和一个FIFO队列来完成获取线程的操作,它通过CAS来完成对值的原子操作修改
CLH队列,虚拟队列,即不存在对象实例,将请求共享资源的线程封装称为CLH的节点来实现锁的分配,只存在结点之间的关系,FIFO
资源共享方式
独占:只有一个线程可以获取资源,比如ReentrantLock
共享:多个线程可以获取资源,比如CountDownLatch,Semaphore
应用
ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch等
CountDownLatch
允许count个线程阻塞在同一个地方,直到线程完成工作为止
有一个多线程下处理多个文件的任务
使用线程池处理任务,每一次处理文件count数都会-1,调用它的wait方法直到所有文件都获取完之后才会执行其他业务
线程的生命周期
新建
线程刚刚被创建出来
就绪
线程已经创建出来,调用start方法,但是还没有获取到cpu时间片
运行
线程获取到cpu时间片,开始运行
阻塞
当前线程因为某种原因放弃了cpu时间片的使用权,就会进入到阻塞状态,直到其进入到就绪状态才可以被cpu所调度,阻塞又分为三种
等待阻塞
调用了wait方法使线程进入到等待状态,需要通过其他线程执行notify或notifyAll方法进行唤醒
同步阻塞
当线程尝试获取同步锁时,如果当前锁已经被别人线程所占用,那么将会进入到阻塞状态
其他阻塞
当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
终止
线程执行完毕或者抛出异常退出run方法,被销毁
线程池和线程
7大参数
核心线程数
最小同时运行线程数
最大线程数
最大同时执行线程数量
等待时间
如果当前线程池中的数量大于核心线程数,此时没有新来的请求,那么核心线程之外的线程不会立即销毁,而是会等待这个时间后被销毁
时间单位
等待时间的时间单位
等待队列
当线程池中线程数量大于核心线程数,那么新来的请求就会加入到阻塞队列当中
工厂
生成新的线程
饱和策略
如果当前线程池数量大于最大线程数并且阻塞队列中也已经满了,那么就会使用饱和策略还处理
AbortPolicy终止策略,当来新任务时会直接拒绝,排除RejectExecutionException异常
CallerRunsPolicy,只要当前线程池未关闭,在调用者线程内,直接调用被丢弃的任务,会导致性能急剧下降
DiscardPolicy丢弃策略,不处理新任务,直接丢弃
DiscardOldPolicy丢弃最早策略,丢弃最早进行等待的线程
为什么引入阻塞队列
线程池是基于生产者和消费者模式的,线程池是消费者,当一个任务加载进来的时候,如果大于核心线程数就会加入到阻塞队列当中,然后执行wait方法释放cpu,从而可以提升cpu的利用率
线程池处理任务的流程
当一个任务提交过来,首先判断是否当前线程数是否大于核心线程数,如果小于则创建线程,如果大于接着判断
判断等待队列中是否已满,如果没满,则加入到等待队列当中,如果满了则继续判断
判断是否大于最大线程数,如果小于创建线程来处理,如果大于则使用饱和策略进行处理
创建线程池的方式
使用ThreadPoolExecutor构造方法来创建
使用Executor中Executors工具类创建
FixedThreadPool固定线程数量的线程池
SignalThreadPool只有一个线程的线程池,会反复使用这一个线程
newCachedThreadPool根据实际情况调整线程数量的线程池
线程池的好处
降低资源消耗,不用反复的创建线程,每一次都使用已经创建好的线程
提高响应速度,不用每一次都创建线程,节省了创建销毁线程的时间
提高程序的可管理性,线程池可以对线程进行统一管理,线程是稀缺资源,如果无限创建不仅会消耗许多资源,还会降低系统稳定性
不使用线程池的坏处
如果不使用线程池有可能导致创建大量的同类线程导致内存不足还有过度切换的问题, 线程池主要用来解决线程生命周期开销问题和资源不足问题
创建线程的方式
继承Thread类
实现runnable接口
实现Callable接口
使用Executor工具类创建,实现Runnable和Callable的切换
Runnable和Callable的区别
Runnable没有返回结果也不会抛出异常
Callable有返回结果也会抛出异常
execute和submit的区别
execute方法是使用在不需要有返回值的情况下,不能判断任务是否执行成功
submit方法是使用在需要有返回值的情况,会返回一个Future类的值,通过这个Future对象来判断任务是否执行成功,可以通过Future.get方法来获取返回值,get方法会阻塞当前线程,它会阻塞当前线程一段时间后立即返回,此时可能这个任务还没有执行完
池化的概念
”池” 是一种” 以空间换时间” 的做法,我们在内存中保存一系列整装待命的对象,供人随时差遣。
与系统效率相比,这些对象所占用的内存空间太微不足道了。
与系统效率相比,这些对象所占用的内存空间太微不足道了。
sleep和wait方法的区别
两者都可以是线程进入到阻塞状态
两者最重要的区别在于sleep方法没有释放锁,而wait方法释放了锁
使用sleep方法的线程会进入阻塞状态,超过一定时间就会自动恢复回运行状态,而wait方法则需要其他线程调用notify或者notifyAll方法进行唤醒
wait方法常用于线程之间的交互与通信,sleep方法用于暂停当前线程
wait方法是object类下的,sleep方法是Thread类下的
JUC下的集合
ConcurrentHashMap
CopyOnWriteArrayList
多个线程访问List是线程安全的,它的读取操作完全不用加锁,写入也不会阻塞当前读操作,只有写写操作时会进入同步等待。大大提高性能。所有可变的操作都是通过创建底层数组的新副本来实现。
当处于写操作时,不会立即写入到容器当中,而是会复制一份新的容器,然后添加到新的容器当中,添加完成之后再将旧容器的引用指向新的容器
在写操作时会进行加锁,插入删除更改需要加锁,读不需要加锁
问题
内存问题,因为在写入的时候是需要将当前容器进行复制的
数据一致性问题,只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。【当执行add或remove操作没完成时,get获取的仍然是旧数组的元素】
BlockingQueue
它与普通队列的区别是,当队列为空时,获取队列数据会被阻塞,当队列满的时候,向队列中添加数据会进入阻塞,靠wait和notifyAll方法实现,常用于生产者-消费者问题
ConcurrentLinkedQueue
非阻塞队列,使用CAS实现线程安全
ArrayBlockingQueue
底层使用数组,并发控制采用可重入锁来实现,不管是插入还是取出都需要获取锁,不能保证公平性,使用ReentrantLock和Condition实现生产者消费者的同步问题
LinkedBlockingQueue
底层使用单向链表,未指定容量使用Integer.MAX_VALUE,使用ReentrantLock和Condition实现生产者消费者的同步问题
PriorityBlockingQueue
支持优先级的无界阻塞队列,并发控制采用ReentrantLock,不可以插入null值,必须插入的值可以比较大小,否则会报出ClassCastException异常
ArrayBlockingQueue和LinkedBlockingQueue的比较
他们都是使用ReentrantLock和Condition实现生产者消费者的同步问题
A是基于数组的,L是基于链表的
A中生产和消费都是同一把锁,而L中生产和消费使用的是不同的锁,生产是putLock,消费是takeLock
A必须指定容量大小,L可以不指定大小但是默认为Integer.MAX_VALUE,也可以自己指定大小
统计队列中元素个数的方式不一样,A是建立了一个int的变量,L是定义了一个AutomaticInteger对象
实现生产或消费的方式不一样,A中插入和删除一个元素时,直接删除枚举对象,不会产生任何或销毁任何额外实例,L中是维护一个Node<E>节点会额外产生一个Node对象,这在高并发的情况下,会影响到性能。所以A效率会更高一点
ConcurrentSkipListMap
底层使用跳表来实现
跳表
跳表是一种用来快速查询的数据结构,跳表的插入和删除只需要对局部进行操作即可,在高并发的情况下,只需要部分加锁即可 时间复杂度为o(log n)
跳表的本质是同时维护了多个链表,来做索引,并且分层,最低级链表是全部元素,每上一层就是下面一层的子集,跳表内所有元素都是排序的,查找时从顶层链表开始查找,一旦发现该元素大于链表中的某个值就会进入下一层继续寻找
Automatic原子类
概念
Automatic指的是一个操作是不可以中断的,即使是多个线程一起执行,一旦操作开始,就不会受到其他线程影响
4个种类
基础
数组
引用
对象的属性修改
AutomaticInteger
原理
主要使用CAS+volatile和Native方法来保证原子性,从而避免synchronize的开销,使用unsafe类的中objectFieldOffset方法,返回一个valueOffset,另外还有一个被volatile修饰的变量value,所以jvm可以保证任何时候都能拿到最新值。还有一个getAndIncrement方法调用unsafe中getAndAddSet传入3个参数 this,valueOffset,1分别为当前地址,预期地址,如果相等则+1,里面有一个dowhile循环
ThreadLocal
概念
当我们创建变量时,这个变量可以被所有线程所获取,如果我们创建一个变量只允许一个线程访问的时候就需要用到ThreadLocal
作用
为每一个线程提供一个专属变量,实现线程之间数据的隔离性
原理
ThreadLocal底层使用的是ThreadLocalMap静态内部类实现,存储每一个Entry对象,ThreadLocal为key,ThreadLocal对象调用set方法设置的值为value,map的结构是为了让每个线程关联多个ThreadLocal对象
ThreadLocalMap底层实际使用的只有数组,他没有像HashMap中数组间具有next。
散列值的求法
key.threadLocalHashCode& len-1
减少hash碰撞
在set时,如果存在与key的hash值相等的,就使用线性探测方式来尝试在数组的下一个索引存放值
扩容机制
会经过两次判断,都成功才会进行扩容。在set方法中,首先会清理掉key为null的Entry,如果此时还大于等于数组长度的3分之2,则进入rehash方法中,在rehash中,会进行一次全量清理,如果当前数组长度大于等于数组定义长度的2分之一,就会进行扩容。扩容会使Entry数组增加2倍
使用场景
事务隔离
Spring中的事务隔离就是使用ThreadLocal来实现的,保证单个线程中的数据库操作是同一个数据库连接下的,采用这种方式时,不需要业务层感知并管理connection对象,通过传播级别,管理事务间的切换,恢复,挂起。在TransactionSynchronizationManager中
问题
内存泄露
Map中是以ThreadLocal为key,Entry是继承于weakReference弱引用的,而value又是强引用,当这个key与其他对象之间没有强引用,那么在GC时就会对该Key进行垃圾回收,所以会出现大量key为null,但是value还存在的Entry,这就会造成内存泄漏
解决
ThreadLocal已经考虑了这个问题,所以在get,set,remove方法时都会判断,将key为null的Entry对象清除。除此之外我们最好在使用完ThreadLocal之后再次调用remove方法进行清除
对象存储
ThreadLocal虽然会使数据或对象变成线程专属变量,但是这个变量或对象没有存储在栈中,还是存储在堆中,但是使用了一些机制将可见性改为了线程可见性
数据共享
使用InheritableThreadLocal来实现线程共享
父子线程局部变量共享
在父线程中实例化一个inheritableThreadLocal对象,然后调用set方法,在子线程中使用get方法进行获取
在最终执行的Thread构造方法中会判断父线程的inheritableThreadLocal是否为null,如果不为null,则传递个子线程
内存泄露和内存溢出
内存泄露
指程序中动态分配的堆内存由于某些原因不能释放,造成系统资源浪费,严重的可以使系统速度变慢,甚至崩溃
内存溢出
指内存空间不足,当要分配的内存已经不足以在内存中分配时就会造成内存溢出问题
线程过多
当过多的线程执行固定数量的任务时,会使每一个线程分配的任务过少,导致浪费
增加上下文切换次数
线程过少
使每个线程分配的任务过多,造成性能影响
线程数的设置
CPU密集型
这种任务主要消耗的是cpu资源,可以设置为n+1,n为cpu核心数,多出来的一个是用来防止多线程偶发的缺页中断,或者其他原因导致线程暂停所带来的的影响
IO密集型
系统会用大部分时间来处理大量的IO交互,而这个过程线程不需要占用cpu处理,可以将cpu交给其他线程完成其他工作,可以设置为2n
CPU密集型就是利用cpu的计算能力比如排序,io密集型就是需要进行大量的Io交互比如读取文件,网络读取等,特点就是cpu的消耗会比IO消耗小很多
为什么线程切换效率低
操作系统需要从内核态转化为用户态
会进行上下文切换
一个cpu只能处理一个线程,cpu采用时间片轮转的方式调度线程,当一个线程从使用完时间片到重新回到就绪状态的时候就是一个上下文切换,也就是任务从保存到再加载的过程被称为上下文切换,是用来计算密集型任务的,需要大量的处理器时间
JVM
内存模型
本地方法库
本地库接口
执行引擎
运行时数据区
线程共享,不安全
方法区
用来存放类信息,静态变量,常量,即时编译后的代码
1.8中使用元空间替换永久代,元空间存储在本地内存中
1.8中将字符串常量池放在堆中,而不是放在元空间中而运行时常量池放在方法区中
堆
用于存放对象实例和数组,创建的对象都存在这里
堆内存
年轻代
Eden
分为Eden和Survivor区分配比例为8:1:1,当Eden区中满了会触发MinorGC将Eden区中的存活的对象放在FromSurvivor区
Survivor
分为FromSurvivor和ToSurvivor区,当FromSurvivor区满了会将对象转移到To区,然后调换指针,使一个区域为空,为空的区域使用To指针指向
因为堆内存是线程共享的所以线程不安全,在并发情况下有可能两个对象使用同一块内存空间。虚拟机解决这个问题每个线程都会预先分配连续的内存空间,并且规定对象存储的位置,如果内存空间不足会申请多块内存空间
老年代
每一次MinorGC都会将存活的对象年龄+1,如果存活对象的年龄达到15的时候就会加入到老年代。如果老年代满了,会触发FullGC,FullGC会暂停线程的运行,所以要避免,如果FullGC之后还是无法存储对象,就会报出OOM
线程私有,安全
虚拟机栈
用于存放局部变量,操作数栈,动态链表,方法出口
会报StackOverFlowError
因为虚拟机栈不能动态扩展,当线程请求的栈深度大于栈的深度时,就会报出这个错误
OOM
堆中的容量不足,而垃圾收集器也不能提供足够的内存来存储,就会报OOM
局部变量表
用于存放方法参数,和方法内部自定义的变量
它的容量是以slot为最小单位,为了节省栈空间,这些slot可以复用
本地方法栈
存放本地方法
程序计数器
指向下一行需要执行的字节码指令,唯一不能报出oom的地方
堆和栈的区别
堆主要用来存放对象,栈主要是用来执行程序的。相较于堆,栈的存取速度更快,但栈的大小和生存周期必须确定,因此缺乏一定的灵活性。而堆却可以在运行时动态的分配内存,生存期不用提前告诉编译器,但这也导致了其存取速度的缓慢
类加载机制
加载
将文件转为二进制流
静态结构转化为运行时数据结构
生成class文件
验证
验证生成的class文件中的字节流是否符合jvm的规范
准备
在方法区中分配内存空间
解析
虚拟机将常量池中的符号引用转化为直接引用
直接引入是指向目标对象的指针,必须在内存中已经存在
符号引用是引用的目标不一定已经加载到内存中
初始化
真正赋值,执行类构造方法
类加载时机
new指令
loaderClass或forName
forName
得到的class对象已经是被初始化过的
loaderClass·
访问类的静态变量,或者为静态变量赋值
访问类的静态方法
初始化一个子类
使用反射机制创建某个类或接口的Class对象
直接使用java.exe命令执行某个类
java从编译到执行的过程
编译
经过语法语义的分析以及注解的处理最后才会生成class文件
加载
加载又可以分为,装载 连接 初始化
装载
把Class文件装载到jvm
连接
连接则校验class信息,分配内存空间及附默认值
验证
准备
解析
初始化
为变量赋值为正确的初始值
解释
把字节码转换成操作系统可识别的执行指令,jvm字节码解释器和即时编译器
执行
调用系统的硬件执行最终程序指令
双亲委派机制
类加载器
启动类加载器
扩展类加载器
应用类加载器
用户自定义类加载器
该机制指,每一次类进行加载时候,它首先不会自己加载这个类,而是委派父类进行加载,只有当父类加载加载不了的时候才会自己加载,不管是哪个类进行加载都会委派到启动类加载器,这样保证了使用不同的类加载器都会判断到启动类类加载器,使最终得到的都是同样一个Object类
沙箱机制
我们创建一个自定义string类,但是在加载自定义String类的时候会率先使用启动类加载器加载,而启动类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
打破双亲委派机制
自定义类加载器,重写loadClass方法
指定全限定类名加载器进行加载,重写这个方法就可以指定哪个加载器来加载,也可以使用自定义加载器,如果不想打破就重写findClass方法
使用线程上下文类加载器
通过Thread中的setContextClassLoader方法设置,如果全局未设置,则使用的就是应用类加载器
spi机制
为某个接口寻找服务实现的机制
例子
JDBC
在JDBC4.0之后就打破了双亲委派机制
Class.ClassLoder()使用的是调用者的加载器,而调用者的DriverManager的加载器又是启动类加载器,而获取数据库驱动肯定不在lib包下,所以就需要使用应用类加载器加载
所以就需要使用线程上下文加载器加载,如果没有指定默认就需要使用应用类加载器
引用
强引用
虚拟机中无论如何都不会回收这个引用所引用的对象,来解决OOM问题,永远不会被GC
软引用
当内存空间充足的时候,这个引用所引用的对象不会被回收,反之被回收,使用softReference实现
实现一个系统,这个系统中需要大量的图片,如果一次性全部加载有可能会引起OOM,此时可以使用HashMap保存图片的路径和图片对象关联的软引用的映射关系,当内存不足时就会回收这些图片对象,来确保不会报出OOM
弱引用
无论当内存空间是否充足还是不充足,都会回收这个引用所引用的对象,一旦失去强引入就会被立即回收,使用weekReference实现
虚引用
必须与引用队列一起使用,随时有可能被回收,用来跟踪对象的回收状态,当要回收虚引用所引用的对象时,会先加入到引用队列中,然后程序可以通过判断引用队列中有哪些对象,来判断哪些对象将要被回收
判断一个对象是否需要被回收
引用计数法
当一个对象被引用时,计数器就会+1,失效时-1,为0时说明可以被回收。无法处理循环依赖的对象
可达性分析
类似于二叉树的遍历,以GCRoots作为起始存活对象集,向下遍历,遍历到的对象之间的路径被称为引用链,如果一个对象到GCRoots没有任何引用的时候,说明此对象不可达
需要标记两次才可以确定对象是否真的需要被回收
第一次标记出不可达对象
第二次当这些不可达对象执行finalize方法时,如果在这个期间与GCRoots有了引用关系,则成功逃脱不需要被回收,如果没有则说明真的需要被回收了
GCRoots
方法区中类静态属性引用的对象
方法区中常量引用的对象
虚拟机栈中局部变量所引用的对象
本地方法栈本地方法中引用的对象
就是一组必须活跃的引用,特质垃圾回收器的对象
new一个对象的流程
类加载检查
在执行到new关键字时,会检查在常量池中是否能找到与这个指令后的参数一样的符号引用,这个引用是否被加载过
分配内存空间
为新生对象分配内存空间
指针碰撞
空闲链表
初始化零值
虚拟机为需要分配内存空间的对象都设置零值,为了让对象实例在不赋值的情况下也可以被使用
设置对象头
设置对象头中的信息,包括这是哪个类的实例,类的元信息,GC分代年龄情况等等
执行init方法初始化
到了这步才是java程序方面创建对象的过程,执行init方法进行初始化
垃圾回收算法
标记整理
将不需要被回收的对象标记出来,此时不会立即回收没有标记的对象,而是将所有标记的对象都移到一端,然后在清理边界以外的内存空间
解决内存碎片问题
由于要移动存活对象,所以要更新引用,效率比较低
标记清除
将需要被收集的对象标记出来,然后对这些标记出来的对象进行回收
会产生大量内存碎片
效率低
不占用额外的内存空间
复制
将内存空间划分为两个等部分,每次只是用其中的一部分,复制到另一部分进行垃圾收集
内存缩水,对堆内存使用率下降
避免了内存碎片
分代收集
jvm采用分代收集,年轻代因为会产生大量对象,也需要回收大量对象,所以使用复制算法,只需要复制少量存活对象,就可以对内存进行回收,老年代因为没有额外的空间进行担保,对象存活率高所以使用标记整理或标记清除算法
GC
MinorGC
minor GC:在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
FullGC
oom的分析
原因
虚拟机分配的内存太少
应用太多,用完没有释放资源,造成内存泄露或内存溢出
分析
使用内存映像分析工具分析堆转储的快照,判断对象是否还需要存在,也就是判断是内存泄露引起还是内存溢出引起
解决
如果是内存泄露引起的,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收他们的。掌握了泄漏对象的类型信息及GC Roots引用链的信息,就可以比较准确的定位出泄漏代码的位置
如果不是内存泄露引起的,换句话说,就是内存中的对象确实还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗
垃圾收集器
新生代
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old
Parallel Scavenge 采用标记复制算法,它注重CPU的吞吐量(高效率利用CPU)(CPU执行代码的时间/总时间)
1 Old是它的老年代版本,采用标记整理算法
老年代
CMS
CMS,并发标记收集回收器,使用标记-清除算法,是以获取最短垃圾回收停顿时间为目的的垃圾回收器,非常适用于使用在注重用户体验的服务上
年轻代使用ParNew 老年代使用CMS+SerialOld的组合(SerialOld作为CMS出错后的后备收集器,CMS必须要在老年代堆内存使用完之前完成垃圾回收,否则CMS回收失败,采用SerialOld)
收集的步骤
初始标记
暂停所有其他线程,标记出与GCRoots具有引用的对象,速度很快
需要stw
并发标记
同时开启GC和用户线程,使用一个闭包结构去记录可达对象,但这个闭包不能保证记录的都是可达对象,因为用户线程也在运行,会不断的更新对象的引用域
重新标记
为了修改那些在并发标记期间因为用户线程的运行导致引用发生改变的对象的标记记录
需要stw
并发清除
与用户线程一起运行,GC线程收集那些没有被标记的对象
缺点与优点
会产生大量内存碎片
对cpu资源敏感
无法处理浮动的垃圾
并发收集
低停顿
G1
G1垃圾回收器是面向客户端服务器的垃圾回收器,它适用于配置多颗处理器和大容量内存的机器使用,以极高的概率满足GC回收停顿时间的同时,还能满足极高的吞吐量,1.7中取消了堆内存年轻代和老年代物理区域划分,但是保留了概念,他将堆内存划分为若干region区域,它的后台维护了一个优先级列表,每一次都会回收都会回收价值最大的区域
与CMS的区别
它不仅可以满足获取最短垃圾回收停顿时间,还可以建立一个可预测的回收停顿模型
收集整体是使用“标记-整理”,Region之间基于“复制”算法,GC后会将存活对象复制到可用分区(未分配的分区),所以不会产生空间碎片。
优点
它可以通过多个cpu来缩短回收停顿时间,G1回收器可以通过并发的方式让java线程继续运行
可根据用户设置停顿时间,制定回收计划(但是也可能存在超出用户的停顿时间).
缺点
region 大小和大对象很难保证一致,这会导致空间的浪费;特别大的对象是可能占用超过一个 region 的。并且,region 太小不合适,会令你在分配大对象时更难找到连续空间,这是一个长久存在的情况。
收集步骤
初始标记
标记那些与GCRoots相连的对象,会暂停所有其他线程Stop the world
并发标记
并发标记整个堆中存活的对象
最终标记
标记之初为存活的对象创建一个快照,加快标记速度
筛选回收
筛选回收价值最大的区域
一些问题
元数据区也会报出OOM,错误信息为java.lang.OutOfMemoryError: Matespace,使用 -XX:MaxMetaspaceSzie
Mysql
InnoDB和Myisam的区别
m是mysql5.5之前的存储引擎,i是5.6之后的
是否支持事务和崩溃后的安全修复,m不支持m强调性能,每一次操作都是原子操作,i支持具有回滚提交崩溃后修复等功能
是否支持外键,m不支持,i支持
是否支持MVCC,m不支持,i支持
是否支持锁,m只支持表级锁,i支持表级锁和行级锁默认行级锁
事务
特性
原子性
事务是最小的执行单位,原子性保证事务要么都执行,要么都不执行
使undo log实现,保存修改之前的值
一致性
执行事务后,保证事务从一个正确的状态转换到另一个正确的状态
有其他三个特性共同实现
隔离性
并发访问数据库时,一个事务的操作不受其他事务影响
使用读写锁+MVCC实现
持久性
事务执行完之后对数据库中的数据进行修改或更新,对这个数据修改是永久的
使用redo log实现,保存修改之后的值
隔离级别
read_uncommited读取未提交
允许读取未提交的数据
会导致幻读,不可重复读,脏读
read_commited读取已提交
允许读取并发访问期间已经提交的数据
不可重复读,幻读
repeatbale_read可重复读
对同一字段的多次读取都是相同的结果,除非是数据内部自己修改了值
幻读
serialzable序列化
最高隔离级别,完全符合ACID,事务会依次逐个执行
问题
脏读
一个事务读取的数据是在另一个事务中还没有被提交的数据
不可重复读
并发读取数据时,一个事务读取数据但是还未提交,另一个事务修改这个数据导致两次读取结果不一致
幻读
并发读取数据时,一个事务前后两次查询范围数据,后一次查询看到了第一次未读到的数据,就像幻觉一样
幻读
产生原因
行锁只能锁住行,但是幻读产生的原因是插入要更新的记录是行与行之间的间隙,所以行锁无法锁住
解决
使用间隙锁+行锁=next-key Lock
间隙锁锁住的是行与行之间的间隙,比如在一个表中插入6行数据,就会产生7个间隙
间隙锁 Gap Lock
用来解决索引记录之间间隙上的锁,保证某个间隙内的数据在锁定情况下不会发生任何变化
日志
redo log
保存事务提交后的数据
undo log
保存事务提交前的数据
undo log是逻辑日志,记录是事务提交之前操作记录的日志,用于回滚,redo log是物理日志,记录的是事务提交后的新数据,用于服务器宕机之后恢复数据
binlog
每一个事务提交后,保存这个事务中的sql语句
errorlog
记录mysql的启动与停止,以及在运行过程中出现的异常记录
general query log普通查询日志
记录服务器接收的每一个查询和命令,无论是否出错都会记录
慢查询日志
记录执行时间过长和没有使用索引的查询语句,只能保存没有报错的语句
MVCC多版本并发控制
底层原理
底层使用undo log 版本链+readView一致性视图组成,只在读取已提交和可重复读状态下存在
undo log版本链
所有的版本数据只会存储一份,使用回滚指针将他们连接起来
readView一致性试图
由所有未提交的事务ID和已提交的最大事务ID组成
在读取已提交的隔离级别下每一次查询都会生成一个readView,而在可重复读下,只会在第一次SELECT时生成一个readView
作用
是解决读写冲突的无锁并发控制,实现对数据库并发访问。每一次修改都会保存一个版本号,版本与事务的时间戳相关,读操作只会读取事务开始前数据库的快照,这样读操作不会发生阻塞而写操作也不会阻塞读操作
数据库在每一行的后面都隐藏了两列数据,一个是版本,而这个版本就是事务的ID,一个是回滚指针
索引
概念
索引是用于快速查询检索的数据结构
聚集索引
索引和数据在一起。在树中,非叶子节点只存放索引,叶子节点存放索引和数据
主键索引
优点
查询速度非常快,因为节点都是有序的
缺点
依赖于有序,如果是整型还好,但如果传入的是UUID或者字符串的话,就会大大降低速度
更新代价大,叶子节点存放着数据如果修改索引列的数据时,对应的索引也需要修改
辅助索引
索引和数据不在一起。在树中,非叶子节点只存放索引,叶子节点存放索引和数据对应的指针
二级索引
唯一索引
全文索引
前缀索引
普通索引
优点
更新代价小,叶子节点不存放数据了
缺点
有可能会二次回表,第一次根据索引查找到对应的数据指针,第二次会根据数据指针在进行查询
常见的索引
hash索引
优点
hash索引也就是根据hash表实现,可以快速的进行查询,这是b树无法比拟的
缺点
会发生hash碰撞
不支持顺序和范围查询,比如where 后是b<500 因为hash是根据hash算法在hash表中查找,难不成需要计算1-499的全部数据?显然是不行的
b树索引
b树索引的非叶子节点存放索引和数据,叶子节点也存放索引和数据
b+树索引
b+树索引的非叶子节点存放的是索引,叶子节点存放的是数据和索引,叶子节点之间具有一条引用链进行
为什么使用b+树而不使用b树
可以降低磁盘IO读写次数
因为b树无论是叶子还是非叶子都会有数据,而b+树只有叶子节点有数据,那么在相同内存的情况下,显然b+树存放的数据更多,那么一次读写可以读到的数据也就更多
查询具有稳定性
b+树每一次查询都需要遍历到叶子节点就可以获得数据,b树不一定
遍历效率更高
b+树只需要遍历一次叶子节点即可,而b树每一个节点都存放数据,所以需要进行一次中序遍历来完成
索引覆盖
就是要查询的数据就是索引的值,不需要进行回表操作
最左前缀原则
当我们创建组合索引时,需要将经常使用的索引放在最前面也就是最左边
alter table user add index index_name(userid,name,age)
如果我们使用where name='?'时就不会走索引,而使用where userid=1and name='?'时就会走索引
索引的使用
什么值可以作为索引
经常用来查询的字段
经常用于连接的字段
不为null的字段
经常被作为条件查询的字段
什么值不可以作为索引
不经常用于查询的字段
经常修改的字段
注意冗余索引
字符串类型考虑使用前缀索引
尽量建立联合索引而不是单列索引
索引的创建方式
alter table tablename add index index_name( )
create index index_name on tablename
索引存储的位置
在innDB中
创建表时会生成一个user.ibd用于存放索引和user.frm用于存放数据结构
因为innDB创建表时默认使用主键作为索引,所以不需要myi文件
索引失效
(1)组合索引未使用最左前缀,例如组合索引(A,B),where B=b不会使用索引;
(2)like使用模糊查询,where A like '%China';
(3)搜索一个索引而在另一个索引上做order by,where A=a order by B,只使用A上的索引,因为查询只使用一个索引 ;
(4)or会使索引失效。如果查询字段相同,也可以使用索引。例如where A=a1 or A=a2(生效),where A=a or B=b(失效)
(5)如果列类型是字符串,要使用引号。例如where A='China',否则索引失效(会进行类型转换);
(6)在索引列上的操作,函数(upper()等)、or、!=(<>)、not in等;
(2)like使用模糊查询,where A like '%China';
(3)搜索一个索引而在另一个索引上做order by,where A=a order by B,只使用A上的索引,因为查询只使用一个索引 ;
(4)or会使索引失效。如果查询字段相同,也可以使用索引。例如where A=a1 or A=a2(生效),where A=a or B=b(失效)
(5)如果列类型是字符串,要使用引号。例如where A='China',否则索引失效(会进行类型转换);
(6)在索引列上的操作,函数(upper()等)、or、!=(<>)、not in等;
UUID不适合做索引的原因
UUID是无序的,会对他进行排序,有可能会造成页的断裂或合并
UUID占用的字节数太多,每一个叶子节点都存放主键id,所以浪费空间
什么时候建立索引
通过慢日志来查询出哪些sql执行的速度慢,来响应的加索引
一条sql语句执行的速度慢的原因
偶尔很慢
数据库有可能在刷新脏页
脏页:内存中的页数据与磁盘中的页数据不一样
因为在执行一条更新修改语句时,内存中的页数据不会立刻同步到磁盘中,而是先存储在redo log日志上,如果redo log日志满了,他就会暂停其他线程执行,将内存中的页数据同步到磁盘中
语句执行的时候遇到了锁
这条语句上涉及的表或行有可能被其他用户加锁了,这是我们就需要等待锁的释放
一直慢
没有用到索引或者是索引失效
数据库本身有可能选错索引
数据库在查询时会根据索引来判断,是走索引快还是全表扫描,这个依据是索引的区分度,一个索引上的不同的数据越多,代表出现相同的索引越少,区分度也就越大,区分度越大代表基数越大,基数越大代表查询的行数越少。而选择基数是根据采样的方式,这种方式会造成失误,也就会让这条语句走全表扫描
explain命令
table:表示属于哪张数据表
type:最重要的参数,表示连接使用了何种类型。从最好到最差的连接类型为const,eq_reg,ref,range,index和ALL。
possible_keys:显示可能应用在这张表中的索引。如果为null,则表示没有可能的索引。
key:实际使用的索引。如果为null,则表示没有使用索引。
key_len:使用的索引的长度,在不损失精确性的情况下,长度越短越好。
ref:表示索引的哪一列被使用了,如果可能的话,是一个常数。
rows:Mysql认为必须检查的用来返回请求数据的行数
type:最重要的参数,表示连接使用了何种类型。从最好到最差的连接类型为const,eq_reg,ref,range,index和ALL。
possible_keys:显示可能应用在这张表中的索引。如果为null,则表示没有可能的索引。
key:实际使用的索引。如果为null,则表示没有使用索引。
key_len:使用的索引的长度,在不损失精确性的情况下,长度越短越好。
ref:表示索引的哪一列被使用了,如果可能的话,是一个常数。
rows:Mysql认为必须检查的用来返回请求数据的行数
三大范式
第一
必须不包含重复组关系,即每一列都是不可再分的原子项
第二
必须满足第一范式,在此基础上所有非主属性都要完全依赖于主建
还会出现数据冗余的情况和更新异常
第三
在满足第二范式的基础上,所有非主属性和任何候选关键字之间都不存在传递关系,也就是非主属性与主键之间是直接关系而不是间接关系
select查询语句执行流程
首先会查询是否具有权限,如果没有权限则会报错,如果有权限在mysql8.0之前会查询缓存,以这条sql语句为key在内存中查询是否有结果,如果有直接缓存,如果没有执行下一步
通过分析器进行词法分析,提取sql语句的关键元素,比如提取上面这个语句是查询select,提取需要查询的表名,列名,以及查询条件。然后会判断是否发生语法错误,比如关键词是否正确等等,如果检查没有问题进行下一步
接下来就是优化器进行确定执行方案,优化器会选择一个执行效率最好的方案但这个方案有时不是最好。那么确认了执行计划后就准备开始执行了
进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎执行结果
更新语句流程
先查询这一条数据,如果在缓存中有则直接在缓存中获取
然后拿到查询的语句,把要修改的值修改,然后调用引擎API接口,写入这一行数据,InnoDB引擎把数据保存在内存中,同时记录redo log,此时redo log处于prepare状态,然后告知执行器,执行完成了,随时可以进行提交
执行器接收到通知后记录binlog,然后调用引擎接口,提交redolog为提交状态
更新完成
分库分表
大表优化,crud慢
限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内。;
读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读;
缓存: 使用MySQL的缓存,另外对重量级、更新少的数据可以考虑使用应用级别的缓存;
读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读;
缓存: 使用MySQL的缓存,另外对重量级、更新少的数据可以考虑使用应用级别的缓存;
分库分表进行优化
垂直分区
根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。
简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。
简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。
优点缺点
垂直拆分的优点: 可以使得行数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
场景
1、如果一个表中某些列常用,另外一些列不常用
2、可以使数据行变小,一个数据页能存储更多数据,查询时减少I/O次数
2、可以使数据行变小,一个数据页能存储更多数据,查询时减少I/O次数
水平分区
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分使用多个库或者表来扛更高的并发,可以支撑非常大的数据量
优点缺点
水平拆分能够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨界点Join性能较差,逻辑复杂
场景
子主题
1、表中的数据本身就有独立性,例如表中分表记录各个地区的数据或者不同时期的数据,特别是有些数据常用,有些不常用。
2、需要把数据存放在多个介质上
2、需要把数据存放在多个介质上
数据库分片常见的解决方案
客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现
中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中
分库分表的方式
1.按照range来分,一般是时间范围,但是这个方式会引起热点问题,导致大量请求都打在最新的数据上了
2.对某个字段进行hash 均匀分布,可以平均分配每个库的数据量,这种方式的弊端在于扩容比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算在库中的位置
分库分表的中间件
Sharding-jdbc
Sharding-jdbc 这种 client 层方案的**优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高**,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要**耦合** Sharding-jdbc 的依赖;
Mycat
Mycat 这种 proxy 层方案的**缺点在于需要部署**,自己运维一套中间件,运维成本高,但是**好处在于对于各个项目是透明的**,如果遇到升级之类的都是自己中间件那里搞就行了
分库分表会出现id问题
每个表中的主键id 都是从1开始的就会出现问题
可以使用redis生成id 灵活不依赖于数据库,但是引入新的程序增加了复杂度
雪花算法
采用Scala语言实现,是一个64位的long类型id,有1位是不用的,41位表示时间戳,10位记录当前工作机器id,代表服务器最多为2的10次方也就是1024个,12位记录同一毫秒内产生的不同id,12位可以代表的最大正整数是2的12次方-1也就是4096,就是如果跟你上次生成 id 的时间还在一个毫秒内,那么会把顺序给你累加,最多在 4096 个序号以内
一些问题
为什么最好不要用delete进行删除
delete物理删除既不能释放磁盘空间,又会产生大量内存碎片,会导致索引频繁分裂,影响sql执行的稳定性
解决
应该使用逻辑标记删除
在mysql规范中的4个公共字段,id主键,is_delete是否删除 0代表删除,1代表没删除,创建时间这个列为索引,更新时间
死锁
解决
1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
乐观锁和悲观锁
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。
主从复制
有两台MySQL数据库服务器Master和slave,Master为主服务器,slave为从服务器,初始状态时,Master和slave中的数据信息相同,当Master中的数据发生变化时,slave也跟着发生相应的变化,使得master和slave的数据信息同步,达到备份的目的。
负责在主、从服务器传输各种修改动作的媒介是主服务器的二进制变更日志(binlog),这个日志记载着需要传输给从服务器的各种修改动作。因此,主服务器必须激活二进制日志功能。从服务器必须具备足以让它连接主服务器并请求主服务器把二进制变更日志传输给它的权限
redis
数据结构
String
是简单的key/value类型使用sds实现简单动态字符串。没有采用c的字符串因为c的字符串最后会有/0又可能出现内存泄漏等一些问题
应用
点赞数
计数器
访问次数
共享Session
只要保证redis的高可用,每一次用户Session获取和更新都可以很快的完成
sds
底层有三个属性
字节数组中使用的长度
字节数组中未使用的长度
字节数组
好处
取字符长度,SDS直接读取len属性。
杜绝缓冲区溢出。C字符串拼接时时假定已经为拼接的字符串预留了足够多的内存,如果这个假定不成立,那么就会产生缓冲区溢出。而SDS是这样做的:SDS的API会会先检查SDS的空间是否满足所需的要求,如果不满足,API自动将空间扩展至所需大小。
减少修改字符串长度时所需的内存重分配次数。
二进制安全。
兼容部分C字符串函数。
杜绝缓冲区溢出。C字符串拼接时时假定已经为拼接的字符串预留了足够多的内存,如果这个假定不成立,那么就会产生缓冲区溢出。而SDS是这样做的:SDS的API会会先检查SDS的空间是否满足所需的要求,如果不满足,API自动将空间扩展至所需大小。
减少修改字符串长度时所需的内存重分配次数。
二进制安全。
兼容部分C字符串函数。
List
是一种链表,c中没有链表所以redis自建了双向链表,可以支持反向查询和遍历
应用
消息队列
阻塞队列
发布或订阅
使用lrange可以读取某个闭区间的分页情况,可以实现类似与微博不断下滑获取新的信息
3.2之前使用linkedlist和ziplist
3.2之后使用quicklist
3.2之后使用quicklist
linkedlist每个元素占用的内存空间较大,因为还要维护前后指针
ziplist存储在一段连续的内存上,所以存储效率很高。但是,它不利于修改操作,插入和删除操作需要频繁的申请和释放内存。
Set
是一种无序集合
应用
用于实现交集或并集
共同关注
共同好友
底层使用的是intset或hashtable
intset其实底层就是一个数组,并且按照插入的顺序来存储。只有当满足两个条件时才会使用intset
当集合中的元素全部位整型的时候
当集合中元素个数不大于512的时候
zSet
相比于set增加了一个权重score,底层使用跳表
应用
适用于直播间的礼物榜,弹幕消息都需要实时排序
底层使用ziplist或skiplist
ziplist每个集合元素使用两个紧挨在一起的压缩列表来表示,第一个节点表示元素的成员,第二个节点表示元素的分值
只有当有序集合的元素个数小于128个
每一个元素的大小都小于64个字节的时候
ziplist可以使用更加紧凑的数据结构来实现多个元素的连续存储,节省空间
skiplist使用dict来表示元素与分值之间的关系
跳表时间复杂度O(log n)分析
1.查找包括两个循环,外层循环是从上层Level至底层Level,内层循环是在同一个Level,从左到右;
2.由上述分析知,跳表高度以极大概率为O(logn),所以外层循环的次数以极大概率为O(logn ).
3.在上层查找比对过的key ,不会在下层再次查找比对,任意一个Key被查找比对的概率是1/2,因此内层循环的比对次数的期望约等于2,即O(1);
4.最终查找的时间=O(1)* O(logn),也即O(logn).
2.由上述分析知,跳表高度以极大概率为O(logn),所以外层循环的次数以极大概率为O(logn ).
3.在上层查找比对过的key ,不会在下层再次查找比对,任意一个Key被查找比对的概率是1/2,因此内层循环的比对次数的期望约等于2,即O(1);
4.最终查找的时间=O(1)* O(logn),也即O(logn).
hash
使用ziplist或hashtable实现。hash使用string类的field和value表示映射关系
应用
适用于存储对象,存储商品等
缓存问题
缓存穿透
查询大量不存在的缓存,使这些查询都落在数据库上
解决
对为null的数据,加一个空值
使用参数校验
使用布隆过滤器,快速查询这个key是否在缓存当中
缓存击穿
并发情况下查询一个数据这个数据属于热点数据,当它失效时会有大量的请求直接访问到数据库中
让热点数据永不失效,或者快要过期时,使用另一个异步线程为该数据重新设置过期时间
使用分布式锁,当读取的数据为null时,重新为查询数据库的过程加锁
缓存雪崩
指大量缓存数据在同一时刻失效,使这些访问都直接访问数据库,使数据库崩溃
可以设置随机的过期时间防止同时过期
使用redis高可用的集群机制,主从机制,防止全盘崩溃
使用本地内存和服务限流降级策略,防止mysql崩溃
使用redis的AOF和RDB,进行持久化,快速恢复数据
布隆过滤器
组成
由位图和多个hash算法组成
作用
可以快速判断大量数据是否在缓存中
原理
通过判断数据的hash值是否在位图中,来判断数据是否在缓存中,使用多个hash算法是为了减少hash冲突的发生
结果
布隆过滤器会有一定的错误
如果布隆过滤器判断当前元素在缓存中,则不一定在
如果判断不在缓存,则一定不在
redis单线程问题
组成
redis是依靠Reactor反应器设计模式来设计开发的一个高效的事件处理器,这个事件处理器也就是对应redis的文件事件处理器,因为这个文件事件处理器的单线程的所以称redis是单线程的
redis使用IO多路复用来实现一个线程监听多个Socket套接字,它将感兴趣的事件也就是读写事件注册到内核当中,并监听事件是否发生,使用IO多路复用可以使redis不需要创建多余的线程来监听事件的发生,降低资源消耗
文件事件处理器
组成
多个Socket套接字
客户端连接
IO多路复用程序
支持多个客户端连接的关键
文件事件分配器
将文件事件分配到对应的处理器
事件处理器
连接应答,命令请求,命令回复
Socket
是应用层和TCP/IP协议通信的中间软件通信层,是一组接口,一个Socket就是进程中通信的一端
select/epoll
select
select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作
成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0
epoll
仅在linux系统中存在
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。
从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。
从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符
持久化机制
RDB快照
通过建立快照的方式来获取存储在内存中的数据在某一时间点的副本,可以将快照进行备份,可以复制到别的服务器使别的服务器与该服务器具有相同的副本,也可以留在本服务器中在重启服务器时使用
RDB在保存RDB文件时,父进程会fork出一个子进程来完成保存操作,而父进程可以进行其他IO操作,所以RDB可以最大的提高redis的性能,如果数据集比较大的时候,fork是非常耗时的,而forks子进程的过程是阻塞的,所以bgsave也会发生
优点
文件尺寸小,恢复快,对redis的性能影响小
AOF只追加文件
它的实时性比rdb好,AOF默认是关闭的,在配置文件中的appendly中开启,开启后,每进行一次修改redis的操作就会写入到内存中aof文件来记录操作,默认为appendly.aof文件
设置
appendsync-always
每次操作都写入文件
appendsync-everysec
每秒钟同步一次
appendsync-no
让操作系统决定何时进行同步
优点
实时性比较好,更加安全,秒级持久化,可追加写,可读性好
aof重写
可以将aof中的命令集缩短到最小,压缩aof文件,提高恢复速度
通过配置文件中开启,也可以使用bgrewriteaof命令,也会forks一个子进程
两者区别
相同数据下aof文件比rdb文件要大,数据恢复的速度也较慢,但是内容更加完整
RDB和AOF混合
redis4.0中支持混合化,aof重写操作就将rdb文件写入到aof文件头中,这样结合了RDB和AOF的优点可以快速加载的同时避免丢失更多的数据,但是rdb在aof文件中的格式不是aof,所以可读性差
内存淘汰机制
8种
volatile-lfu
从已过期的数据集中挑选最不经常使用的数据进行淘汰
volatile-ttl
从已过期的数据集中挑选将要被淘汰的数据进行淘汰
volatile-lru
从已过期的数据集中挑选最近最少使用的数据进行淘汰
volatile-random
从已过期的数据集中随机挑选数据淘汰
allkeys-lfu
当内存不能容下新的数据时,挑选最不经常使用的进行淘汰
allkeys-random
当内存不足时,随机挑选淘汰
allkeys-lru
当内存不足时,挑选最近最少使用的进行淘汰
no-eviction
禁止驱逐数据,当内存不足以容纳新的数据时,新写入就会报错
集群数据同步问题
你启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave。
如何判断对象是否过期
redis过期字典来保存数据的过期时间,是一个hash表,这个字典指向redis中的某个key键,它的value是一个longlong类型的保存过期的时间
惰性删除
只有当要使用该数据的时候才会检查它是否已经过期,这样会在内存中遗留大量过期的数据
定时删除
每隔一段时间就抽出来一些数据进行删除
redis使用惰性和定时的结合来完成判断
常见的性能问题和解决方案
Master主节点写内存快照
主节点中写内存快照需要调用save命令,save命令调用rdbSave方法,这个方法会阻塞当前主线程,如果当数据量比较大的时候,会造成很大影响,在slave副节点上进行写内存快照
Master主节点AOF持久化
随着AOF文件不断增大,会影响主节点重启和恢复速度
主节点进行aof持久化时,重写aof文件会调用 berewriteaof:aof命令,这个命令会占用大量的cpu和内存,导致服务load过高,出现短暂的服务暂停
单点故障问题
为了主节点的稳定性,redis集群不应该采用图状结构,应该采用单向链表的形式
一致性hash算法
引出
分布式的情况下经常需要将数据库中的数据进行分片,避免全部数据出现在单库或者单表中,造成对数据库的查询压力。所以通常采用hash算法来进行分库分表,求出数据库中的主键或唯一键的hash值然后跟数据库的主机数量取余,从而决定放在哪个库中
当数据库的主机宕机或者主机不够用时,机器的数量就会下降,造成数据的命中率下降
原理
求出数据库中的主键或唯一键的hash值与2的32次方进行取余
2的32次方,IPV4最大数量就是2的32次方,所以这样就能保证所有的ip的地址进行取余时不会重复—对应hash环上面的正数
一致性hash算法将整个hash值组成一个虚拟的环,对数据库的主机名求hash值,找到在环中的位置,不采用ip的原因是因为ip有可能发生改变,这样每一个主机都会在环中有对应的服务器节点来存储,寻找时会顺时针寻找,找到的第一个就将他存放在哪个服务器节点。当增加服务器时,只有这个位置和逆时针第一个服务器会受到影响
问题
数据倾斜
如果服务器节点数太少,就会造成分配不均匀,导致大量的数据存放在一个服务器节点上
解决
引入虚拟节点的概念,就是将每个服务器计算多个hash值,即多个很多个服务器,具体的做法可以是将主机名后边加上编号
redis没有采用一致性hash算法而是采用slot槽的概念,hash槽可以将数据均匀的分配到n个节点上,保证每个节点都承担n分之一的容量,这一点时一致性hash算法不能做到的,hash槽采用 CRC16(key)%16384实现,16384是槽的数量,CRC16是同步校验规则
分布式锁
操作
加锁时,使用setnx 来加锁(nx代表if not exists 如果存在这个值则不进行操作,如果不存在,则设置),使用UUID生成一个随机值作为value,目的是在释放锁时进行判断,并使用一个expire命令为当前锁添加一个超时时间,如果超过这个时间,则自动释放锁
获取锁时,调用setnx,如果获取到的时0,则说明当前锁没有释放,如果获取到的时1,则获取到锁,还需要设置一个超时时间,如果超过这个时间放弃获取锁
释放锁时,通过UUID判断是否是该锁,如果是,则使用delete继续释放锁
如何解决主从架构的redis分布式锁主节点宕机锁丢失的问题
使用redisson封装的redlock算法实现的分布式锁用法。
redlock算法思想:不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,必须在大多数redis节点上都成功创建锁,才能算这个整体的RedLock加锁成功,避免仅仅在一个redis实例上加锁而带来的问题。
关键在于set的value要具有唯一性,redisson怎样保持value的一致性?
UUID+threadId
redlock算法思想:不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,必须在大多数redis节点上都成功创建锁,才能算这个整体的RedLock加锁成功,避免仅仅在一个redis实例上加锁而带来的问题。
关键在于set的value要具有唯一性,redisson怎样保持value的一致性?
UUID+threadId
原理
1、加锁机制:某个客户端要加锁,如果该客户端面对的是一个redis cluster集群,首先会根据hash节点选择一台机器。紧接着,就会发送一段lua脚本到redis上,保证这段复杂业务逻辑执行的原子性。lua脚本中包含的参数有加锁的key,key的默认生存时间,加锁的客户端的ID。加锁的逻辑是SETNX,即不存在才加锁。
2、锁互斥机制:客户端2来尝试加锁时会发现锁已存在并且客户端ID不是自己的,此时会进入一个while循环,不停的尝试加锁。
3、watch dog自动延期机制:客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。
4、可重入加锁机制:如果客户端1已经持有锁了还进行可重入的加锁那么就会对客户端1的加锁次数累加1。
5、释放锁机制:执行lock.unlock()就可以释放分布式锁,每次对加锁次数减1,当加锁次数为0时从redis里删除这个key,然后另外的客户端2就可以尝试完成加锁了。
2、锁互斥机制:客户端2来尝试加锁时会发现锁已存在并且客户端ID不是自己的,此时会进入一个while循环,不停的尝试加锁。
3、watch dog自动延期机制:客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。
4、可重入加锁机制:如果客户端1已经持有锁了还进行可重入的加锁那么就会对客户端1的加锁次数累加1。
5、释放锁机制:执行lock.unlock()就可以释放分布式锁,每次对加锁次数减1,当加锁次数为0时从redis里删除这个key,然后另外的客户端2就可以尝试完成加锁了。
缺点
redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。
集群
哨兵机制
问题
解决的是redis中主从机制中的主节点挂了不能自动故障转移的问题
原理
(三个定时任务)(心跳机制+投票裁决)
问题一:sentinel没有配置从节点信息如何知道从节点信息的?
每隔10秒,sentinel向主节点发送info命令,用于发现新的slave节点
问题二、如何加入新的sentinel?
每隔2秒,向redis数据节点_ sentinel_:hello频道发送本sentinel节点的信息和对主节点的判断:这是进行对主节点进行客观下线和领导者选举的重要依据;也是发现新sentinel节点的重要依据
问题三:如何判断一个节点的需要主观下线的?
每隔1秒每个sentinel对其他的redis节点(master,slave,sentinel)执行ping操作,对于master来说,若超过down-after-milliseconds内没有回复,就对该节点进行主观下线并询问其他的Sentinel节点是否可以客观下线
问题一:sentinel没有配置从节点信息如何知道从节点信息的?
每隔10秒,sentinel向主节点发送info命令,用于发现新的slave节点
问题二、如何加入新的sentinel?
每隔2秒,向redis数据节点_ sentinel_:hello频道发送本sentinel节点的信息和对主节点的判断:这是进行对主节点进行客观下线和领导者选举的重要依据;也是发现新sentinel节点的重要依据
问题三:如何判断一个节点的需要主观下线的?
每隔1秒每个sentinel对其他的redis节点(master,slave,sentinel)执行ping操作,对于master来说,若超过down-after-milliseconds内没有回复,就对该节点进行主观下线并询问其他的Sentinel节点是否可以客观下线
作用
1)监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
2)提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
3)自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器。
2)提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
3)自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器。
客观下线和主观下线
主观下线:每隔1秒每个sentinel对其他的redis节点(master,slave,sentinel)执行ping操作,若master超过down-after-milliseconds内没有回复,就对该节点进行主观下线,每个sentinel节点对redis节点失败的“偏见”
客观下线:当sentinel主观下线的节点是主节点时,sentinel会通过命令sentinel is-master-down-by-addr来询问其sentinel对主节点的判断,如果超过quorum个数就认为主节点需要客观下线, 所有sentinel节点对redis节点失败达成共识
客观下线:当sentinel主观下线的节点是主节点时,sentinel会通过命令sentinel is-master-down-by-addr来询问其sentinel对主节点的判断,如果超过quorum个数就认为主节点需要客观下线, 所有sentinel节点对redis节点失败达成共识
如何选举sentinel的leader
使用raft协议(解决分布式系统一致性问题的协议)
1)每个做主观下线的sentinel节点向其他sentinel节点发送命令,要求将自己设置为leader
2)收到的sentinel可以同意或拒绝
3)如果该sentinel节点发现自己的票数已经超过了半数并且超过了quorum那么当选leader
4)如果此过程选举出了多个leader,那么将等待一段时间重新进行选举
1)每个做主观下线的sentinel节点向其他sentinel节点发送命令,要求将自己设置为leader
2)收到的sentinel可以同意或拒绝
3)如果该sentinel节点发现自己的票数已经超过了半数并且超过了quorum那么当选leader
4)如果此过程选举出了多个leader,那么将等待一段时间重新进行选举
sentinel的leader如何选举从节点成为主节点
1)过滤故障节点
2)根据slave-priority优先级进行选择
3)选择复制偏移量大的从节点为主节点
4)选择runid最小的从成为主(说明重启的时间靠前)
2)根据slave-priority优先级进行选择
3)选择复制偏移量大的从节点为主节点
4)选择runid最小的从成为主(说明重启的时间靠前)
主从复制
概念
集群中的每个节点都有N-1个复制品,其中一个为主节点,其余的为从节点,如果主节点下线了,集群就会把这个主节点的一个从节点设置为新的主节点继续工作,这样集群就不会因为一个主节点的下线而无法正常工作
方式
全量同步,增量同步
如何保证缓存和数据库数据一致性问题
延迟双删+缓存过期
先删除缓存,再更新数据库,休眠一定时间,再删除缓存
休眠的目的
确保读请求结束,写请求可以删除读请求造成的缓存脏数据
弊端
这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时
异步更新缓存(基于订阅binlog的同步机制)
读redis当中的数据,mysql负责增删改操作,然后利用消息队列异步更新Redis当中的缓存
一旦mysql当中的进行了增删改的操作之后,就会记录到binlog当中,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新
网络
TCP与UDP
区别
UDP
UDP是无连接的,不可靠的,基于数据报文段的用户数据报协议,传输的数据少,速度快,头字节为8个字节
传输数据时无需建立连接,远程主机在接收到UDP报文后不需要确认
TCP
TCP是面向连接的,可靠的,基于字节流的传输层协议,它传输的数据多,速度慢,头字节为20-60个字节
TCP会在传输数据时建立连接,数据传送后释放连接,可靠性体现在,在建立连接和释放连接时有三次握手和四次挥手,在传输数据时还有确认,窗口,重传,拥塞控制等。所需资源多,需要确认,计数器,连接控制等
使用场景
UDP
qq语音,视频等
TCP
文件传输发送,远程登录等
TCP拥塞控制
作用
拥塞控制是为了防止过多的数据出现在网络中导致路由器或链路出现过载
实现
TCP实现拥塞控制需要维护一个窗口CWND,拥塞窗口的大小取决于网络拥塞的状况,会动态变化
发送方维护一个拥塞窗口,先进行慢开始算法,一开始发送方发送一个字节,在收到接收方的确认,然后发送的字节数量增大一倍,按照指数逐步增大窗口大小,直到达到慢开始门限,然后使用拥塞控制算法,增长速率变为线性增长,直到出现超时,重新将窗口大小调整为1个字节,使用慢开始算法,同时慢开始门限调整为超时点的一半,达到门限后继续执行拥塞避免方法,如果收到3-ACK,可能是报文丢失,使用快重传算法发送缺失的报文段,同时执行快恢复算法将门限调整为此时窗口的一半,并执行拥塞避免算法。
4种算法
慢开始
在不知道网络的情况下,先测试一下,窗口指数增长
拥塞避免
将拥塞窗口缓慢增大,每经过一个往返时间就把发送方的窗口+1
快重传和快恢复
没有 FRR,如果数据包丢失了,TCP将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。有了 FRR,就不会因为重传时要求的暂停被耽误
滑动窗口
解决
滑动窗口主要解决的是流量控制
流量控制
控制发送端发送数据的效率,使接收端来的及接受
机制
滑动窗口解决的是流量控制的的问题,就是如果接收端和发送端对数据包的处理速度不同,如何让双方达成一致,流量控制解决的是这个问题。
TCP会话的双方都各自维护一个发送窗口和一个接收窗口。发送窗口只有收到发送窗口内字节的ACK确认,才会移动发送窗口的左边界。接收窗口只有在前面所有的段都确认的情况下才会移动左边界。当在前面还有字节未接收但收到后面字节的情况下,窗口不会移动,并不对后续字节确认。以此确保发送端会对这些数据重传。发送方发的window size = 8192;就是接收端最多发送8192字节,这个8192一般就是发送方接收缓存的大小。
TCP会话的双方都各自维护一个发送窗口和一个接收窗口。发送窗口只有收到发送窗口内字节的ACK确认,才会移动发送窗口的左边界。接收窗口只有在前面所有的段都确认的情况下才会移动左边界。当在前面还有字节未接收但收到后面字节的情况下,窗口不会移动,并不对后续字节确认。以此确保发送端会对这些数据重传。发送方发的window size = 8192;就是接收端最多发送8192字节,这个8192一般就是发送方接收缓存的大小。
tcp和http对头阻塞
tcp
原因
一个TCP分节丢失,导致其后续分节不按序到达接收端的时候。该后续分节将被接收端一直保持直到丢失的第一个分节被发送端重传并到达接收端为止
解决
放弃tcp协议
http
原因
HTTP管道化要求服务端必须按照请求发送的顺序返回响应,那如果一个响应返回延迟了,那么其后续的响应都会被延迟,直到队头的响应送达
解决
HTTP2.0实现多路复用,一个连接即可实现并行
tcp粘包问题
概念
TCP粘包问题就是发送端发送若干包数据到接收端成了一包数据,从缓冲区上来看,后一包数据指向第一包数据
方式
TCP在发送数据时,会将小的数据合并成大的数据一起发送到接收端,造成粘包
由于服务端的recv方法中的buffer_size较小,不能一次性的将数据接收完成,所以在下一次数据到来时,处理的仍是上一次未处理完的数据,造成粘包
解决
第一种造成的解决方式
在发送端两个send方法中直接使用recv方法防止连续发送
第二种造成的解决方式
由于粘包问题是由接收端无止境的接收,所以发送端发送数据之前可以先告诉接收端发送数据的大小
udp不会粘包
对于UDP,不会使用块的合并优化算法,这样,实际上目前认为,是由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。
DNS域名解析
使用到了TCP和UDP协议
在区域传送时使用TCP
DNS规定了两个服务器,一个是主域名服务器,一个是辅助域名服务器,因为辅助域名服务器会定时的向主域名服务器进行查询为了了解数据是否发生变动,如果发生变动,则需要进行一个区域查询来解决数据同步的问题,区域传送使用TCP是因为数据同步的传送量比一个请求应答的数据量要大很多,所以使用TCP协议,因为他是可靠的,传送的数据量很大
其他情况下使用UDP
客户端向DNS服务器发送解析请求,一般请求结果都不会超过512个字节,所以使用UDP不用三次握手来建立连接,提高速度
过程
(1) 输入域名后, 先查找自己主机对应的域名服务器,域名服务器先查找自己的数据库中的数据.
(2) 如果没有, 就向上级域名服务器进行查找, 依次类推
(3) 最多回溯到根域名服务器, 肯定能找到这个域名的IP地址
(4) 域名服务器自身也会进行一些缓存, 把曾经访问过的域名和对应的IP地址缓存起来, 可以加速查找过程
(2) 如果没有, 就向上级域名服务器进行查找, 依次类推
(3) 最多回溯到根域名服务器, 肯定能找到这个域名的IP地址
(4) 域名服务器自身也会进行一些缓存, 把曾经访问过的域名和对应的IP地址缓存起来, 可以加速查找过程
TCP建立连接和释放连接的过程
三次握手
流程
客户端向服务器发送一个带有SYN标识的请求,该请求的序列号为x,处于同步发送状态
服务器接收客户端的请求,并向客户端发送一个带有SYN和ACK标识的响应,序列号为y,确认序列号为x+1,服务器同步接收状态
客户端接收到服务器的响应,并向服务器发送一个带有ACK标识的响应,序列号为x+1,确认序列号为y+1
都会处于已建立状态
问题
两次握手不可以吗?
第一次握手服务器可以确定客户端发送正常,服务端接收正常
第二次握手服务器可以知道客户端发送正常,客户端可以知道服务器发送正常接收正常
第三次握手就什么都可以知道了
第二次握手传回了ACK,为什么还要回传SYN?
传ACK是为了告诉客户端服务端接收到的信息就是客户端传过来的,而SYN标识是为了确认服务器到客户端的通信
3次都可以携带数据吗?
1,2不可以3可以,第一次不可以是因为客户端可能将大量的请求注入到SYN报文中,不会理睬服务端的处理能力,所以服务端会消耗很长时间进行处理
第二次不可以是因为服务端的资源分配是第二次握手是给的,而客户端是第三次时给的,这就会让服务器受到SYN洪泛攻击,这个攻击指客户端向服务器发送大量不存在的ip地址,并发送大量syn报文,服务端进行处理,向客户端发送报文,由于这些ip都是不存在的,那么服务端将会一直等待客户端响应,这样就会导致网络拥堵甚至瘫痪,导致那些正常的请求不会被处理
四次挥手
流程
客户端向服务器发送一个带有FIN标识的请求,序列号为u,此时客户端处于FIN_CLOSE阶段
服务器接收到客户端的请求,并发送给客户端一个响应,该响应是带有ACK标识的序列号为v,确认序列号为u+1,此时服务器处于半关闭状态,CLOSE_WAIT,客户端处于FIN_CLOSE2阶段
当服务器完成自己的业务后也想要断开连接,回向客户端发送一个带有FIN和ACK的请求,序列号为w,确认序列号为u+1,服务端处于LAST_ACK阶段
客户端向服务端发送一个带有ACK标识响应,序列号为u+1,确认序列号为w+1,发送完之后客户端会等待2倍的最大生存时间MSL,处于TIME_WAIT阶段,之后客户端服务器都会处于关闭状态
问题
为什么会等待2MSL
为了防止那些已经失效的连接报文段出现在连接中
使最后的报文有效的到达服务器
Time_wait
作用
1.确保正常释放连接
2.允许老的重复分节在网络中消逝
过多的危害
会占有大量内存
还会消耗cpu资源
解决过多
timewait快速回收和重用。快速回收:通过修改参数启用快速回收,此时timewait只有一个rto(恢复时间目标)的时间。重用有两个条件,1.新连接的初始序列号比TW老连接的末序列号大。2.如果使用了时间戳,那么新到来的连接的时间戳比老连接的时间戳大。并且同一个ip和端口号的才能重用。
参数
tcp_tw_recycle,回收tw
问题
tcp_tw_recycle和tcp_timestamps导致connect失败问题
当使用net方式联网时与服务端进行交互,服务器看到的是同一个IP相当于这些客户端都是一样的,但是客户端的时间戳可能会有差异,于是从服务端看的话时间戳会出现错乱,导致时间戳较小的丢失
tcp_tw_reuse,复用tw
tcp_tw_reuse,请激活tcp_timestamps,否则无效。
为什么要4次挥手
因为服务端有可能还有一些报文没有处理所以会先发送一个ack标识的报文,需要等待服务器处理完报文后再关闭
网络层数
物理层
确定与传输媒体有关的物理特性,如电气特性,机械特性等,传输比特流
数据链路层
物理寻址,将原始比特流转变为逻辑传送线路
CMSA/CD载波监听多点接入协议,ppp点对点协议
网络层
控制子网的运行,如路由选择,分组传输
ARP地址解析协议,IP子网协议
传输层
接收应用层传来的数据,对这些数据进行必要的切割,有效的传送到网络层
TCP和UDP
应用层
各种应用层协议
HTTP,FTP文件传输协议
OSI7层
物理层
数据链路层
网络层
传输层
会话层
不同机器用户之间建立和管理会话
SSL安全套接字层协议,TLS传输层安全协议
表示层
信息的语法语义以及他们的关联,如加密解密,转行翻译等
XDP外部数据表示协议
应用层
子网掩码的作用
子网掩码和IP地址做“与”运算,分离出IP地址中的网络地址和主机地址,用于判断该IP地址是在本地网络上,还是在远程网络网上。
子网掩码还用于将网络进一步划分为若干子网,以避免主机过多而拥堵或过少而IP浪费
子网掩码还用于将网络进一步划分为若干子网,以避免主机过多而拥堵或过少而IP浪费
浏览器中输入url显示页面的过程
步骤
首先根据域名解析出对应的ip地址
浏览器向web服务器发送一个http请求
web服务器接收请求
web服务器向客户端浏览器发回一个html响应
浏览器接收响应转化为html页面
1.DNS域名解析,2.建立tcp连接,3.客户端向服务器发送http请求,4.服务器处理请求并返回报文,5.客户端浏览器渲染页面
具体步骤
应用层DNS域名解析协议,根据相应的域名解析出对应的ip地址,首先会从本地查找,如果本地没有就从DNS服务器上进行查找,直到找到或到根服务器
应用层客户端发送http请求,http请求包括请求头和请求主体两部分组成,请求头包括目标方法,目标url,状态码等等
传输层建立tcp连接,它为了方便传输,将大块的数据分割成以报文段为单位的数据包进行管理,并为它们编号,方便服务器接收时能准确地还原报文信息
网络层ip协议查询MAC地址,ip协议的作用就是将传输层分割好的数据包传送给接收方,而需要有效的传送到接收方还需要MAC地址,也就是物理地址。ip地址与MAC地址是一一对应的关系,一般ip地址是可以变动的而MAC地址一般是不可以变动的,ARP地址解析协议可以将ip地址转化为物理地址,当通信双方不在同一局域网下,就需要多次中转才能到达最终目标,在中转的过程中需要通过下一个中转站的MAC地址来搜索下一个中转目标
在找到MAC地址之后,就将数据传送到链路层上进行传输,此时客户端发送完毕
服务端接收请求,服务端从数据链路层接收请求,然后层层的到达应用层,在这个过程当中,需要将分割的数据包根据编号整合为完整的数据包
服务端响应请求, 服务接收到客户端发送的HTTP请求后,查找客户端请求的资源,并返回响应报文,响应报文中包括一个重要的信息——状态码
渲染页面
用到的协议
DNS域名解析协议
解析域名时使用
tcp传输协议
建立连接
ip协议
发送数据时使用到ip协议
ospf路由协议
ip数据包在路由器之间,路由器选择这个协议
ARP
地址解析协议,路由器在服务器中通信时,需要将ip地址转化为MAC地址
http协议
建立连接后使用http访问页面
HTTP
HTTP1.0和HTTP1.1的区别
1.0中使用短连接,1.1使用长连接
短连接
每一次请求都会建立一次连接
长连接
当访问一个页面时,客户端与服务器之间用于传输http的tcp连接不会关闭,而之后的客户端再一次请求这个服务器都使用这个连接
减少了创建tcp连接的时间和次数,降低了网络拥堵,减少后续请求的时间
新增了错误状态码
如409
请求资源的状态与它现在的状态不符
410
服务器中某个资源被永久的删除
新增了缓存处理机制
1.0
主要使用header中的if-Modified-Since和Expires
1.1
新增了Entry tag和if-Unmodified-Since
带宽优化
1.0中会直接将整个对象请求过来
1.1中引入了range头域,允许请求资源的一部分,会返回206
HTTP2.0和1.1的区别
2.0增加了多路复用
一个连接可以并发处理多个请求
2.0中可以头部数据压缩
可以将header中的数据进行压缩
服务器推送
它允许服务端推送消息给客户端,在服务端确认连接之前,免得客户端向服务器发送连接请求
HTTP和HTTPS
HTTP使用的连接开头为http://默认端口号为80,HTTPS使用的连接开头为https://默认端口号为443
http是建立在tcp连接之上的,传输的数据都是明文,客户端与服务器之间都无法知道验证对方的身份
https是建立在ssl/tls之上的http协议,而ssl/tls又是建立在tcp上的协议,所以它传输的数据都是密文,采用加密的方式,会使用CA机构证书来验证服务端与客户端的身份
https比http更安全,但是消耗的资源多,速度慢
ssl/tls的加密过程
客户端向服务器端索要并验证公钥。
双方协商生成"对话密钥"。
双方采用"对话密钥"进行加密通信。
双方协商生成"对话密钥"。
双方采用"对话密钥"进行加密通信。
HTTPS过程
上述过程就是两次HTTP请求,其详细过程如下:
1.客户端向服务器发起HTTPS的请求,连接到服务器的443端口;
2.服务器将非对称加密的公钥传递给客户端,以证书的形式回传到客户端
3.客户端接受到该公钥进行验证,就是验证2中证书,如果有问题,则HTTPS请求无法继续;如果没有问题,则上述公钥是合格的。(第一次HTTP请求)客户端这个时候随机生成一个私钥,成为client key,客户端私钥,用于对称加密数据的。使用前面的公钥对client key进行非对称加密;
4.进行二次HTTP请求,将加密之后的client key传递给服务器;
5.服务器使用私钥进行解密,得到client key,使用client key对数据进行对称加密
6.将对称加密的数据传递给客户端,客户端使用对称解密,得到服务器发送的数据,完成第二次HTTP请求
原文链接:https://blog.csdn.net/seujava_er/article/details/90018326
1.客户端向服务器发起HTTPS的请求,连接到服务器的443端口;
2.服务器将非对称加密的公钥传递给客户端,以证书的形式回传到客户端
3.客户端接受到该公钥进行验证,就是验证2中证书,如果有问题,则HTTPS请求无法继续;如果没有问题,则上述公钥是合格的。(第一次HTTP请求)客户端这个时候随机生成一个私钥,成为client key,客户端私钥,用于对称加密数据的。使用前面的公钥对client key进行非对称加密;
4.进行二次HTTP请求,将加密之后的client key传递给服务器;
5.服务器使用私钥进行解密,得到client key,使用client key对数据进行对称加密
6.将对称加密的数据传递给客户端,客户端使用对称解密,得到服务器发送的数据,完成第二次HTTP请求
原文链接:https://blog.csdn.net/seujava_er/article/details/90018326
对称加密和非对称加密
对称加密就是使用同一把秘钥进行加密解密,这把秘钥作为私钥
常见算法
DES算法和3DES算法
缺点 服务器和浏览器之间传递秘钥可以被监听,相当于明文传输
非对称加密,就是加密和解密不使用同一把秘钥,一把作为公开公钥,一把作为私钥,公钥公开加密的信息,私钥处理加密的信息
常用算法
RSA、Elgamal
浏览器使用公钥来对信息进行加密,服务端使用私钥进行解密,公钥是所有人都知道的,所有人都可以读取服务端回复的消息来进行解密,所以解决不了服务端向浏览器传递消息。
对称加密无法安全地将密钥传输给通信方,速度快,反之
优点
简单快速,只需要传输请求方法和路径,灵活允许传输任何数据的数据对象,支持B/S和C/S
作用
HTTP是超文本传输协议,是在两点之间用来传送文本,图片,音频,视频等超文本数据的约定和规范
错误状态码
200
请求成功
301
请求成功,但是资源被永久转移
302
暂时地把内容转移到一个新的URL,但是老的URL还没有废除
303
请求的资源并没有被修改过
304
表示服务器允许访问,但是因为请求不符合条件
403
请求资源的访问被拒绝了
404
请求内容不存在
405
禁止请求中指定的方法
500
服务器错误
502
网关错误
503
服务器暂时处于超负载或者停机维护
504
网关超时,服务器作为网关或代理,没有及时从上层接收到请求
错误状态码
200请求成功
300重定向
400客户端错误
500服务器错误
100信息提示
Cookie和Session
作用
Cookie和Session都是用来跟踪浏览器用户信息的会话方式
Cookie经常用来记录用户的信息,常用在登录
Session经常用来通过服务端记录用户的状态,典型用在购物车,因为在添加商品到购物车时,系统不知道是哪个用户操作的因为http是状态的,服务端给定用户创建特定的Session之后就可以使用这个跟踪用户的状态了
区别
安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。
存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。
Cookie保持登录状态
当用户登陆成功后,会在服务器中记录用户信息
用户再次发送来请求时,就会传过来Cookie中的SessionId
浏览器根据传过来的sessionId,去Session当中检查是否有用户信息,如果有则处于登录
要退出会话,直接删除Session文件即可
Cookie被禁用
第一种方案,每次请求中都携带一个 SessionID 的参数,也可以 Post 的方式提交,也可以在请求的地址后面拼接 xxx?SessionID=123456...。
第二种方案,Token 机制。Token 机制多用于 App 客户端和服务器交互的模式,也可以用于 Web 端做用户状态管理
第二种方案,Token 机制。Token 机制多用于 App 客户端和服务器交互的模式,也可以用于 Web 端做用户状态管理
jwt (json web token)
可以存储在Cookie当中也可以存储在Authorization,存储在HTTP头部的A当中可以进行跨域
jwt分为三个部分组成
Header(头部)
是一段json数据,存储jwt的元信息
Payload(负载)
是一段json数据,可以添加自定义的内容
Signature(签名)
是对前两部分的签名,防止数据篡改
与token的区别
Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。
JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。
URL和URI
区别
URI统一资源标识符
URL统一资源定位符,可以提供资源的路径,它是一种具体的URI,不仅可以标识资源,还指明了如何定位这个资源
GET和POST的区别
get不会改变服务器资源,post会改变
get是幂等的,post不是幂等的
get的请求参数在url上,post的请求体中
url有最长地址长度所以get的请求参数受url影响
get没有post的安全性高,get将请求参数在url地址上暴露
GET产生一个TCP数据包;POST产生两个TCP数据包
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)
操作系统
进程与线程
区别
进程是资源分配的最小单元,线程是cpu调度的最小单元,一个应用的开始与结束,就是一个进程的创建与销毁的过程,进程是独立的不可分割的
一个进程中会有多个线程,同一个进程中的线程之间不是相互独立的他们可能互相影响
线程开销小,但不利于资源的维护,进程反之
一个进程中有多个线程,他们共享进程的堆和方法区,他们私有自己的本地方法栈,虚拟机栈和程序计数器
同步方式不一样
内核态和用户态
用户态下的进程,可以直接读取用户程序的数据
内核态下的进程,几乎可以访问计算机中所有的资源
系统调用
我们的程序一般都处于用户态,那么当我们想要调用系统级别的子功能时,我们就需要使用系统调用来完成(文件管理,内存管理,进程控制)
中断
中断是指来自CPU执行指令以外的事件发生后,处理机暂停正在运行的程序,转去执行处理该事件的程序的过程
用户态到内核态的转换就是通过中断机制实现的,并且中断是唯一途径
有了进程为什么还要有线程
进程只能在同一时间干一件事,但是在同一时间干多件事就无能为力了
如果进程在运行过程中发生阻塞,如等待输入,即使程序没有用到输入的数据也会发生阻塞
进程的生命周期
就绪
进程已经获取到除cpu以外所有的资源时
执行
获取到cpu时间片,被cpu调度
阻塞
进程因为某种原因放弃被cpu调度
进程调度算法
先到先服务
在就绪队列中,取最先等待的进程,为其分配资源,一直到运行为止或因某种原因发生阻塞放弃cpu调度
短作业优先
在就绪队列当中,挑选预计执行时间最短的进程先执行,为其分配资源
时间片轮转
为每一个进程分配一个时间段
多级反馈队列算法
即能使优先级高的作业得到调度,又能使短作业得到执行,最好的一种算法 UNIX默认算法
优先级算法
为每个进程分配一个优先级,可以以执行时间,内存要求等来设置,按照优先级来执行进程以此类推
进程间的通信方式
匿名管道
只有使父子进程或兄弟进程之间通信
有名管道
任意两个进程间都可以通信
信号
用于通知接收进程的某个事件的发生
消息队列
是一个消息链表,解决了信号承载量少,通道只能基于无格式字节流且缓冲区大小受限的问题
信号量
是一种计数器,用于多进程对共享数据的访问,重点在于进程间的同步,解决同步相关的问题,避免发生竞争
套接字
用于客户端与服务器之间的通信,它是TCP/IP的基本操作单元,可以看做是不同主机之间进程通信的双向端点,是通信双方的一种约定
共享内存
使多个进程可以使用同一块内存空间,不同的进程可以及时看到对方进程对共享内存中数据的修改
线程间的同步方式
互斥量
采用互斥对象机制,只有拥有互斥对象的线程才能有访问公共资源的权限
信号量
它允许同一时刻多个线程同时访问资源,但是需要控制最大线程数
事件
通过使用wait/notify的事件来实现访问资源的同步,还能比较多线程的优先级
内存管理
连续分配管理
为用户程序分配一个连续的内存空间
块式管理
将内存区域分成几个块,每一个块中只有一个进程,如果程序运行需要内存的话,就分配给它一块内存,如果程序需要很少的内存,但是也会分配一块内存,就会造成浪费
非连续分配管理
允许用户程序使用分散的内存区域
页式管理
将内存区域等分为一页一页的形式,页较小,相比于块式分配的更细,提高了内存利用率减少了碎片,使用页表来对应逻辑地址和物理地址
快表
为了解决虚拟地址到物理地址转换的速度
多级联表
避免把全部页表都存储在一个内存当中占用过多的内存空间
段式管理
将内存区域分成为一段段的形式,比页更小,页不具有物理意义,但是段具有,每个段定义了一组逻辑信息,如主程序段和子程序段。使用段表来对应逻辑地址和物理地址
段页式管理
结合了段式管理和页式管理的优点,简单来说就是将内存区域分成段,将段划分成一页一页的
虚拟内存
为每个进程提供一致的私有的内存空间,让每个进程都产生一种独享主存的错觉。更有效的管理内存,防止出错
页面调度算法
FIFO先进先出
先到的页先出,也会造成一个频繁使用的页,频繁加入问题
LRU
最近最久未使用的页
LFU
最近最不经常使用的页
死锁
必要条件
互斥条件
进程对于分配的资源,其他进程不能够访问只有当这个进程释放了这个资源,它才可以访问,否则会发生阻塞
请求与保持
进程获取一定资源后,又请求获取其他资源,但是这个资源有可能被其他进程所占用,这个事件会发生阻塞,但是它获取的资源还会继续使用
不可抢占
进程在使用资源时,在没有使用完之前,不允许其他进程剥夺
循环依赖
在发生死锁时必定存在一个进程-资源的循环链
产生原因
竞争不可抢占的资源
进程推进顺序不当也会引起死锁
处理死锁
预防死锁
通过设置一些限制条件,破坏必要条件中的一个或多个来预防
避免
在获取资源过程中,使用某种方法让进程不会进入到不安全状态,也就是不会进入死锁
检测
允许死锁的发生,但是经过系统检验之后来消除死锁
解除
检验死锁的发生之后将进程从死锁的状态中脱离出来
银行家算法解决死锁
进程运行之前先声明对各种资源的最大需求量,当进程在执行中继续申请资源时,先测试进程已占用的资源数与本次申请的资源数之和是否超过该进程声明的最大需求量,若超过则拒绝分配资源,若未超过则再测试系统现存的资源能否满足该进程所需的最大资源量,若能满足则按当前的申请量分配资源,否则也要推迟分配
控制加锁顺序
所有线程获取锁的顺序都是相同的,所以要提前知道有可能用到的锁,可以使用hash值的大小来确定加锁的先后
尽可能的缩小加锁的范围,等到使用共享变量时在加锁
使用可释放的定时锁,如果一段时间内获取不到锁的权限了,直接释放掉
概念
两个或两个以上的进程在运行时,因为争夺某个资源而造成互相等待,如果没有外力的作用,他们都将不会推进下去
linux
僵尸进程
僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,没有释放子进程占用的资源,此时子进程将成为一个僵尸进程
如果父进程先退出,子进程被init接管,子进程退出后init会回收其占用的相关资源
孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程所收养,并由init进程管理他们
命令
查看进程运行情况
ps -aux 查询内存中进程信息;
ps -aux | grep 查询进程的详细信息;
ps -aux | grep 查询进程的详细信息;
查看某个端口号是否运行
netstat -anp | grep 端口号
netstat -nultp 查看全部端口号
查看cpu使用率
top
htop和top区别
与top不同,htop提供了正在运行的完整进程列表,而不是最消耗资源的进程
htop使用颜色并提供关于处理器,交换和内存状态的视觉信息,htop需要下载
htop使用颜色并提供关于处理器,交换和内存状态的视觉信息,htop需要下载
linux下进程的状态
R (TASK_RUNNING),可执行状态
放入对应的CPU的可执行队列中
S (TASK_INTERRUPTIBLE),可中断的睡眠状态
放入对应事件的等待队列中
D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态
是指进程不响应异步信号,内核的某些处理流程是不能被打断的
T(TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态
向进程发送一个SIGSTOP信号,它就会因响应信号而进入TASK_STOPPED状态,除非处于D状态
进程在断点处停下来的时候就处于TASK_TRACED状态,处于TASK_TRACED状态的进程不能响应SIGCONT信号而被唤醒,只能等到调试进程通过系统调用
Z(TASK_DEAD-EXIT_ZOMBIE)退出状态,进程称为僵尸进程
不可被kill,即不相应任务信号,无法用SIGKILL杀死
X (TASK_DEAD - EXIT_DEAD),退出状态,进程即将被销毁
常见的命令
ls
-l
显示详情,以 - 开头的为文件,以d开头的为目录
pwd
显示当前目录所在的绝对路径
mkdir
-p
创建目标目录的上一层目录
touch
生成一个空的文件
mv
用来更名或者移动文件到另一个目录当中
cat
显示文件内容
-n 显示行数
-b 显示行数,但是不显示空的行
cp
-r 递归拷贝
-p 保留原有的属性,权限,修改时间
-f 强制拷贝
rm
-r 递归删除
-f 强制删除
tail 和head
-n 分别是显示文件后10行,和文件前十行
wc
统计指定文档中的字节数、单词数、行数
-c 字节数 -l 行数 -m 字符数 -w 单词数
grep
查找文件里符合条件的字符串
-c 统计多少行被匹配
-i 不区分大小写
-v 取反 不包含的行数
vim和vi
文本编辑器
命令模式,输入模式(插入模式)底行模式
setnu 显示行号
setnonu 关闭行号
vim的问题
它会将文本全部加载到内存当中
tar -zxvf
解压
x : 从 tar 包中把文件提取出来
z : 表示 tar 包是被 gzip 压缩过的,所以解压时需要用 gunzip 解压
v : 显示详细信息
f xxx.tar.gz : 指定被处理的文件是 xxx.tar.gz
z : 表示 tar 包是被 gzip 压缩过的,所以解压时需要用 gunzip 解压
v : 显示详细信息
f xxx.tar.gz : 指定被处理的文件是 xxx.tar.gz
Spring
IOC
控制反转,是一种思想,它指的是资源双方不会管理资源,而是交给不使用资源的第三方管理
降低了系统的耦合性,提高资源的易管理性
IOC容器
实际上它是一个HashMap,里面存放各种对象,IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖,IOC容器像一个工厂一样,当我们创建对象时只需要配置配置文件或者相应的注解,完全不用考虑对象是怎么创建出来的
IOC容器初始化流程可以简单的分为3步
Resource资源定位,这个Resource资源就是BeanDefinition,也就是容器找数据的过程
BeanDefinition载入过程,这个载入过程就是把用户定义好的Bean表示成容器内部的数据结构,而这个数据结构就是BeanDefinition
将这些BeanDefinition注册进容器,就是将前面保存的BeanDefinition加入到HashMap中
DI
DI是对IoC更准确的描述,即组件之间的依赖关系由容器在运行期决定,即由容器动态的将某种依赖关系注入到组件之中。
依赖注入可以通过setter方法注入(设值注入)、构造器注入和接口注入三种方式来实现,Spring支持setter注入和构造器注入,通常使用构造器注入来注入必须的依赖关系,对于可选的依赖关系,则setter注入是更好的选择,setter注入需要类提供无参构造器或者无参的静态工厂方法来创建对象
依赖注入可以通过setter方法注入(设值注入)、构造器注入和接口注入三种方式来实现,Spring支持setter注入和构造器注入,通常使用构造器注入来注入必须的依赖关系,对于可选的依赖关系,则setter注入是更好的选择,setter注入需要类提供无参构造器或者无参的静态工厂方法来创建对象
依赖注入是从应用程序的角度在描述:应用程序依赖容器创建并注入它所需要的外部资源;
控制反转是从容器的角度在描述:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源
控制反转是从容器的角度在描述:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源
为什么不支持接口注入
spring遵循高内聚低耦合的思想,如果是接口注入的话,必须要实现接口中的方法,所以不可以
BeanFactory和ApplicationContext
区别
BeanFactory是每次获取对象时才会创建对象
ApplicationContext是每次容器启动时就将配置好的对象创建出来
BeanFactory和FactoryBean的区别
BeanFactory是IOC容器的最上层接口,用来配置Bean,以及获取Bean
FactoryBean可以将Bean进行更改,然后返回新的Bean,是一个接口,由工厂 bean 产生的其他 bean 实例,不再由 Spring 容器产生,因此与普通 bean 的配置不同,不再需要提供 class 元素
Bean的生命周期
实例化一个Bean,将这个Bean包装在BeanWrapper中,避免使用反射机制设置属性
按照spring上下文对实例化的Bean进行配置,也就是IOC注入
如果该实例关联了BeanNameAware接口,就会调用他的setBeanName方法,传入名字
如果该实例关联了*Aware接口就调用它相应的方法
如果这个Bean包含了BeanPostProcessor接口,就会调用postProcessBeforeInitialzation方法,这个方法在init之前执行所以被称为前置通知
经过这个方法该Bean已经被初始化了
如果这个Bean实现了InitialzingBean接口,就执行afterPropertiesSet方法进行初始化,如果在配置文件中指定了init-method,就执行指定的方法
如果这个Bean关联了BeanPostProcessor接口,就会调用postProcessAfterInitialzation方法,这个方法被称为后置通知
当要销毁Bean时,如果实现了DisposableBean接口,就会调用destroy方法,如果在配置文件中指定了destroy-method方法,就会执行相应的方法进行销毁
Bean是否为线程安全的
prototype作用域下时,因为每次请求都会创建一个新的bean,所以是线程安全的
singleton作用域下时,因为每次请求都会使用一个Bean所以是线程不安全的,但是当Bean是无状态的Bean时,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Controller类、Service类和Dao等,这些Bean大多是无状态的,只关注于方法本身。
有状态Bean(Stateful Bean) :就是有实例变量的对象,可以保存数据,是非线程安全的。
无状态Bean(Stateless Bean):就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。
无状态Bean(Stateless Bean):就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。
解决的方法
对于有状态的bean(比如ModelAndView),就需要自行保证线程安全,最浅显的解决办法就是将有状态的bean的作用域由“singleton”改为“prototype”。也可以采用ThreadLocal解决线程安全问题,为每个线程提供一个独立的变量副本,不同线程只操作自己线程的副本变量
AOP
概念
面向切面编程,也就是那些与业务无关,但却能为业务模块所共同调用的逻辑或责任封装起来,SpringAOP是通过代理模式来为目标对象创建代理对象的,并将横切逻辑插入到目标方法执行前后
应用
事务,日志
优点
将共同的代码抽取出来,可以降低代码的重复性,降低耦合度,提高未来的可管理性
原理
AOP使用动态代理实现,如果代理的类实现了某个接口,那么就使用JDK代理类进行代理,如果要代理的类没有实现某个接口,那就使用cglib进行动态代理
JDK动态代理
被代理的类与目标类实现了同一个接口
cglib代理
被代理的类是目标类的子类
代理模式,通过代理来处理对象,可以详细的访问某个对象的方法,在这个方法之前调用,或在这个方法之后调用
静态代理
通过程序员创建或工具类生成源码,会在编译阶段生成AOP代理类,因此也称为编译时增强。
动态代理
在程序运行期,创建目标对象的代理对象,并对目标对象中的方法进行功能性增强的一种技术,可以理解为运行期间,对象中方法的动态拦截,在拦截方法的前后执行功能操作。
JDK动态代理
利用反射机制生成一个实现代理接口的匿名类,在调用具体方法时使用InvokeHandler来处理
为什么必须实现一个接口呢
通过jdk代理生成的类都继承Proxy类:
因为Java是单继承的,而代理类又必须继承自Proxy类,所以通过jdk代理的类必须实现接口
因为Java是单继承的,而代理类又必须继承自Proxy类,所以通过jdk代理的类必须实现接口
cglib动态代理
使用asm开源包,把代理对象类的class文件加载进来,通过修改其字节码来生成子类
原理2
SpringAOP可以使用动态代理来实现,利用截取消息的形式,并对这个消息进行修饰,然后取代原有对象的执行逻辑
AOP还可以使用AspectJ静态织入实现,应用特定的语法创建一个切面,从而使编译器在编译的期间织入有关代码
Spring AOP和AspectJ AOP
区别
springaop是基于代理模式实现的,而AspectJ Aop是基于字节码操作
SpringAOP是运行时增强,那个是编译时增强
事务
编程式事务
在程序中硬编码
声明式事务
使用配置文件生成
基于XML文件的事务配置
基于注解的事务配置
@Transaction注解的原理
当一个类被使用这个注解时,它里面的所有public方法会全部开启事务,也可以单独修饰在类中的public方法上
它实际使用的是TransactionIntercepter类中的invoke方法,在执行目标方法前开启事务,如果执行目标方法出现异常则会回滚,在方法执行完毕之后提交事务,它的实现是基于AOP的,需要AOP拦截及事务的处理;当执行到被这个注解注解到的方法时,就会读取它上面的参数来实现对方法的增强
事务的7种传播行为
支持当前事物的
TransactionDefinition.propagation_required
如果当前存在事务,则加入事务,如果不存在则建立一个新事务
TransactionDefinition.propagation_supports
如果当前存在事务,则加入事务,如果不存在,则以非事务方式继续运行
TransactionDefinition.propagation_mandatory
如果存在事务,则加入事务,如果没有则抛出异常
不支持当前事物的
TransactionDefinition.propagation_required_new
创建一个新的任务,当个存在事务,则将事务挂起
TransactionDefinition.propagation_not_supported
以非事务方式运行,存在事务则挂起
TransactionDefinition.propagation_never
以非事务方式运行,存在事务抛出异常
其他
TransactionDefinition.propagation_nested
如果当前存在事务,则建立一个事务作为当前事务的嵌套事务,如果当前没有事务,则等价于require
是解决业务层方法互相调用事务问题
回滚
默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚,如果想要回滚特定异常类型则在@Transaction中使用 rollbackFor
事务核心
Spring 框架中,事务管理相关最重要的 3 个接口如下:
PlatformTransactionManager: (平台)事务管理器,Spring 事务策略的核心。
TransactionDefinition: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
TransactionStatus: 事务运行状态。
我们可以把 **`PlatformTransactionManager`** 接口可以被看作是事务上层的管理者,而 **`TransactionDefinition`** 和 **`TransactionStatus`** 这两个接口可以看作是事务的描述。
**`PlatformTransactionManager`** 会根据 **`TransactionDefinition`** 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 **`TransactionStatus`** 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等
PlatformTransactionManager: (平台)事务管理器,Spring 事务策略的核心。
TransactionDefinition: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
TransactionStatus: 事务运行状态。
我们可以把 **`PlatformTransactionManager`** 接口可以被看作是事务上层的管理者,而 **`TransactionDefinition`** 和 **`TransactionStatus`** 这两个接口可以看作是事务的描述。
**`PlatformTransactionManager`** 会根据 **`TransactionDefinition`** 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 **`TransactionStatus`** 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等
设计模式
工厂模式
Spring在使用BeanFactory或ApplicationContext创建Bean时,使用到了工厂模式
单例模式
Spring中的Bean默认是单例的
代理模式
SpringAop使用动态代理实现
模板方法模式
Spring中以Template结尾的类都是以模板方法模式实现的
观察者模式
Spring事件驱动模型是以观察者模式实现的
适配器模式
SpringAop中增强和通知Avice使用到了适配器模式,SpringMVC中寻找相应的处理器也就是寻找Controller时也用到了
包装器模式
保证用户可以动态的切换数据源,比如项目需要连接很多数据库,项目中每个用户可能访问不同的数据库
注解
Autowired
与Resource的区别
Autowired默认是按照类型进行加载,Resource默认使用名称加载,如果Autowired需要使用name来进行加载则应配合@Qualifier注解使用,Resource(type=“”)如果指定name名称,则只会根据name进行装配。当找不到指定名称类型的Bean时才会用类型进行装配
Autowired时Spring下的,Resource时jdk1.6之后提出来的
原理
在容器启动,为对象赋值的时候,遇到@Autowired注解,会用后置处理器机制,来创建属性的实例,然后再利用反射机制,将实例化好的属性,赋值给对象上
可见它间接实现InstantiationAwareBeanPostProcessor,就具备了实例化前后(而不是初始化前后)管理对象的能力,实现了BeanPostProcessor,具有初始化前后管理对象的能力,实现BeanFactoryAware,具备随时拿到BeanFactory的能力,也就是说,这个注解具备一切后置处理器的能力。
@Controller和@RestController
区别
@Controller返回一个页面
@RestController只返回对象,对象数据直接以 JSON 或 XML 形式写入 HTTP 响应(Response)中,是@Controller和@ResponseBody的结合
@Bean和@Component
区别
@Component用于配置在类上,通过指定的路径来加入到容器当中
@Bean用在方法上,用来告诉Spring容器这个哪个类的实例
@Bean比@Component的自定义性更强,并且有些地方只能通过@Bean来加入到容器中,比如第三方库中的类需要加载到容器中
循环依赖
两个或两个以上的Bean互相持有对方,循环成环
构造器注入循环依赖
无法解决只能报出BeanCurrentlyCreationException异常,所以不要使用构造器注入
prototype模式field属性注入循环依赖
无法解决
属性注入循环依赖
一般采用提前暴露对象的方法
setter注入,单例情况下,通过递归方法找出当前Bean所依赖的Bean,然后提前将它加入到缓存当中,通过提前暴露,暴露一个exposedObject用于返回提前暴露的Bean
循环依赖的理论依据是java引用传递,当传递引用时,对象的属性是可以延后在设置的,而构造器必须要在获取引用之前设置属性
单例Bean的初始化简单流程可以分为三步
Bean实例化
填充属性
执行init方法
三级缓存
singletonFactories : 单例对象工厂的cache
earlySingletonObjects :提前暴光的单例对象的Cache 。【用于检测循环引用,与singletonFactories互斥】
singletonObjects:单例对象的cache
earlySingletonObjects :提前暴光的单例对象的Cache 。【用于检测循环引用,与singletonFactories互斥】
singletonObjects:单例对象的cache
为什么使用三级缓存
如果没有三级缓存,在AOP场景则为了循环依赖中最终取到的实例是代理对象,则必须在对象实例化的一开始就得执行后置处理器(依赖注入及init方法前)就先生成代理对象,并放到二级缓存中,这样的化就不符合Spring生命周期的设计。按照Spring生命周期的设计原则,AOP生成代理对象的基础是当前对象实例已经初始化完毕(实例+依赖注入+init方法调用+相关BeanPostProcessor调用完毕),因此引入了三级缓存,A如果是AOP增量对象,B依赖A时
Spring 需要三级缓存的目的是为了在没有循环依赖的情况下,延迟代理对象的创建,使 Bean 的创建符合 Spring 的设计原则
getSingleton
分析getSingleton()的整个过程,Spring首先从一级缓存singletonObjects中获取。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存singletonFactory.getObject()(三级缓存)获取,如果获取到了则:从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存
解决过程
如果有一个A对象,它的属性是B对象,而B对象的属性也是A对象
首先A对象实例化然后属性注入,发现依赖于B对象,但是B对象此时还没有创建出来,就会出实例化B对象,B对象实例化之后,发现需要依赖A对象,但此时A对象已经实例化了,所以B对象最终完成创建,B对象返回到A对象的属性注入的方法上,A对象完成创建
使用三级缓存解决,三级缓存我理解的就是3个map,首先需要明确的是我对三级缓存的定义是
singletonObjects(一级,日常实际获取Bean的地方);
earlySingletonObjects(二级,还没进行属性注入,由三级缓存放进来);
singletonFactories(三级,Value是一个对象工厂)
A对象实例化之后,属性注入之前,其实会把A对象放入三级缓存中,key是BeanName,Value是ObjectFactory
等到A对象属性注入时,发现还依赖B,又会去实例化B
B属性注入需要获取A,这里就是从三级缓存中拿出ObjectFactory,从ObjectFactory得到对应的Bean
拿到之后将三级缓存中的A记录消除,然后放在二级缓存中
显然二级缓存Key是BeanName,Value是Bean(这里的Bean还是没有属性注入的Bean)
等到完成初始化之后,就会从二级缓存中消除,放到一级缓存中
我们自己去getBean时,实际拿到的是一级缓存的
singletonObjects(一级,日常实际获取Bean的地方);
earlySingletonObjects(二级,还没进行属性注入,由三级缓存放进来);
singletonFactories(三级,Value是一个对象工厂)
A对象实例化之后,属性注入之前,其实会把A对象放入三级缓存中,key是BeanName,Value是ObjectFactory
等到A对象属性注入时,发现还依赖B,又会去实例化B
B属性注入需要获取A,这里就是从三级缓存中拿出ObjectFactory,从ObjectFactory得到对应的Bean
拿到之后将三级缓存中的A记录消除,然后放在二级缓存中
显然二级缓存Key是BeanName,Value是Bean(这里的Bean还是没有属性注入的Bean)
等到完成初始化之后,就会从二级缓存中消除,放到一级缓存中
我们自己去getBean时,实际拿到的是一级缓存的
简介
spring是一款轻量级框架,可以提高开发人员开发效率,以及提高系统的可管理性
SpringMVC
简介
MVC是一种设计模式,SpringMVC是一款优秀的MVC框架,它天生与Spring结合,在使用SpringMVC时我们可以更加简洁的对web层开发,我们会把后端分为Service业务层,Dao持久层负责与数据库交互,Entity实体类层,Controller应用层
MVC设计模式
在我们实际开发的最后到产品上线,供给客户使用,客户通过浏览器或者app等进行数据的操作,这个程序的功能有,处理发送请求,业务逻辑处理以及访问数据库,这三个功能我们是可以放到一块使用的,但是如果放在一起,代码便会很臃肿,不利于维护,于是便出现了代码分层思想,把代码按照功能分为三层,即模型层(Model)、显示层(View)、控制层(Controller),这种代码的组织架构就叫MVC模式
Model
Service
处理业务逻辑
Dao
与数据库进行交互
View
Controller
工作原理
客户端发送一个请求到DispatcherServlet
DispatcherServlet将请求发送到对应的处理器映射器上
处理器映射器在根据请求找到相应的处理器,找到之后再由处理器适配器HandlerAdapter
适配器再将请求发送到真正的处理器上进行处理
处理器处理请求返回一个ModelAndView对象,model就是数据对象,此时view就是逻辑上的view
视图解析器ViewResolver会根据逻辑view找到实体View
DispatcherServlet将model数据传给view,进行视图渲染
把view返回给请求者(浏览器)
Mybatis
简介
Mybatis是一个优秀的持久层框架,它支持定制sql,存储过程以及高级映射。可以通过配置文件或注解的形式配置和映射原生类型,接口,以及java实体类为数据库中的数据,它是一款半orm框架,因为它还需要我们去写sql语句
orm
ORM,即Object Relational Mapping,它是对象关系模型的简称。它的作用是在关系型数据库和对象之间作一个映射。使程序能够通过操纵描述对象方式来操纵数据库
${}和#{}的区别
${}
变量占位符,它是使用在Properties文件中的,可以用在标签属性值和sql内部,属于静态文本转换
#{}
参数占位符,Mybatis中使用它来替换问号,例如#{item.name}就是通过反射机制从item对象中获取name属性值
#{}可以防止sql注入
sql注入,在实现定义好的查询语句的结尾添加额外的sql语句,欺骗数据库服务器进行非授权的任意查询,盗取数据库数据。
resultMap和resultType的区别
Mybatis每一次查询结果的结构都是resultMap,只有当我们指定了resultType的时候Mybatis才会自动把我们的值赋给resultType相应的类型,如果指定的时resultMap,将数据库中的列数据复制到对应属性上,可以进行复制查询,两者不能同时使用
resultType
JDK提供的数据类型
resultMap
用户自定义的数据类型
一级缓存和二级缓存
一级缓存
一级缓存是SqlSession层的,是不可以关闭的,在同一个SQLSession中执行相同的sql语句时会从缓存中获取, Mybatis的内部缓存使用一个HashMap,key为hashcode+statementId+sql语句,value为查询出来的结果集映射成的Java对象,两次查询sql中间如果有增删改操作会清空缓存。
二级缓存
Mapper级别的缓存,跨SqlSession,默认没有开启,SqlSession1第一次调用Mapper下的SQL进行查询后会将结果存放在Mapper对应的二级缓存区域,SqlSession2再调用Mapper中相同的SQL查询时,会去对应的二级缓存内取结果。如果SqlSession3执行commit提交,将会清空该Mapper映射下的二级缓存区域的数据。
如何根据映射器mapper.xml生成sql语句
XMLConfigBuilder解析mapper.xml文件时,会将每一个sql语句和其配置的内容保存起来
mybatis中一条SQL与它相关的配置信息是由MappedStatement、SqlSource和BoundSql三个部分组成
MappedStatement的作用是保存一个映射器节点(select|insert|delete|update)的内容,它是一个类,包括许多我们配置的SQL、SQL的id、resultMap等重要配置内容,同时还有一个重要的属性sqlSource。mybatis通过读取MappedStatement来获得某条SQL配置的所有信息。
SqlSource是提供BoundSql对象的地方,它是一个接口,使用它就可以得到一个BoundSql对象。
BoundSql是一个结果对象,是建立SQL和参数的地方
MappedStatement的作用是保存一个映射器节点(select|insert|delete|update)的内容,它是一个类,包括许多我们配置的SQL、SQL的id、resultMap等重要配置内容,同时还有一个重要的属性sqlSource。mybatis通过读取MappedStatement来获得某条SQL配置的所有信息。
SqlSource是提供BoundSql对象的地方,它是一个接口,使用它就可以得到一个BoundSql对象。
BoundSql是一个结果对象,是建立SQL和参数的地方
SpringBoot
自动配置和启动加载的流程
自动配置
@SpringBootApplication
@SpringBootApplication中有三个注解:@SpringBootConfiguration @EnableAutoConfiguration和@ComponentScan
@EnableAutoConfiguration:帮助SpringBoot将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器,而这个注解也是一个复合注解,其中的关键功能由@Import提供,其导入的AutoConfigurationImportSelector的selectImports()方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories的jar包
@ComponentScan:组件扫描,可自动发现和装配Bean
@EnableAutoConfiguration:帮助SpringBoot将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器,而这个注解也是一个复合注解,其中的关键功能由@Import提供,其导入的AutoConfigurationImportSelector的selectImports()方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories的jar包
@ComponentScan:组件扫描,可自动发现和装配Bean
启动加载流程
appliction.run()
主要创建了配置环境(environment)、事件监听(listeners)、应用上下文(applicationContext),并基于以上条件,在容器中开始实例化我们需要的Bean
Exception又分为受检查异常和非受检查异常,受检查异常是如果程序在catch中没有处理,那么程序就无法运行
0 条评论
下一页