后端面试补充知识

补充知识

索引下推

索引下推是 MySQL 5.6 版本中提供的一项索引优化功能,可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

MySQL优化器:优化分为:1. 条件优化,2.计算全表扫描成本,3. 找出所有能用到的索引,4. 针对每个索引计算不同的访问方式的成本,5. 选出成本最小的索引以及访问方式

输入URL到页面展示

动态主机配置协议DHCP,DNS(浏览器缓存、路由器缓存、DNS缓存) HTTP协议生成针对目标Web服务器的HTTP请求报文 TCP协议为了方便通信,将HTTP请求报文按序号分割成多个报文段,把每个报文段可靠的传给对方 IP TCP从对方那里接收到报文段。按序号以原来的顺序重组请求报文 HTTP协议对Web服务器请求的内容的处理,请求的处理结果也同样利用TCP/IP通信协议向用户回传

数据链路层

数据链路层的作用是将网络层交下来的 IP 数据报添加首部和尾部组装成帧,在两个相邻节点间的链路上传送帧。数据链路层解决三个基本问题:封装成帧、透明传输、差错检测。封装成帧就是在IP数据报前后分别添加首部和尾部,这样就构成一个帧;帧的开始和结束的标记使用专门指明的控制字符,字节填充和字符填充;循环冗余校验CRC。

CSMA/CD:多点接入、载波监听、碰撞检测:发送前先监听,边发送边监听,一旦发现总线上出现了碰撞,就立即停止发送。然后按照退避算法等待一段随机时间后再次发送

MAC地址就是适配器地址或适配器标识符

以太网交换机内部的帧交换表(又称地址表)是通过自学习算法自动建立起来的。

网络层

地址解析协议ARP;网际控制报文协议ICMP;网际组管理协议IGMP

Ip地址的编址方式:1、分类的IP地址;2、子网的划分;3、构成超网

两个问题:1、主机或路由器怎样知道应当在MAC帧的首部填入什么样的硬件地址;2、路由器的路由表怎么得出的

ARP:每一台主机都设有一个ARP高速缓存,里面有本局域网上各主机和路由器的IP地址到硬件地址的映射表,每一个映射地址项目都设置生存时间(如果更换设配器,MAC地址也变了)
在这里插入图片描述

IP层根据路由表的内容进行分组转发

在这里插入图片描述

划分子网和构成超网

两级IP地址到三级IP地址:网络号、子网号、主机号
在这里插入图片描述

无分类编址CIDR(构成超网)
在这里插入图片描述
网际控制报文协议ICMP:ICMP差错报告报文和ICMP询问报文
在这里插入图片描述
路由表中路由是怎么得出的,常用的路由选择协议

内部网关协议IGP:RIP和OSPF;外部网关协议EGP:BGP-4
在这里插入图片描述
RIP是一种分布式的基于距离向量的路由选择协议,从一路由器到直接连接的网络的距离定义为1.从一路由器到非直接连接的网络的距离定义为所经过的路由器加1.
在这里插入图片描述
开放最短路劲优先OSPF
在这里插入图片描述
外部网关协议BGP:1、互联网规模太大,使得自治系统之间路由选择非常困难;2、自治系统之间的路由选择策略必须考虑有关策略。边界网关协议BGP只能是力求寻找一条能够到达目的网络比较好的路由,而不是一条最佳路由。

网际组管理协议IGMP:让连接在本地局域网上的多播路由器知道本局域网上是否有主体参加或退出了某个多播组

运输层

在这里插入图片描述
在这里插入图片描述

HTTP1.1

缓存处理

HTTP/1.0提供的缓存机制非常简单。服务器端使用Expires标签来标志(时间)一个响应体,在Expires标志时间内的请求,都会获得该响应体缓存。服务器端在初次返回给客户端的响应体中,有一个Last-Modified标签,该标签标记了被请求资源在服务器端的最后一次修改。在请求头中,使用If-Modified-Since标签,该标签标志一个时间,意为客户端向服务器进行问询:“该时间之后,我要请求的资源是否有被修改过?”通常情况下,请求头中的If-Modified-Since的值即为上一次获得该资源时,响应体中的Last-Modified的值。

如果服务器接收到了请求头,并判断If-Modified-Since时间后,资源确实没有修改过,则返回给客户端一个304 not modified响应头,表示”缓冲可用,你从浏览器里拿吧!”。

如果服务器判断If-Modified-Since时间后,资源被修改过,则返回给客户端一个200 OK的响应体,并附带全新的资源内容,表示”你要的我已经改过的,给你一份新的”。

HTTP1.0cache1

HTTP1.0cache2

响应状态码:HTTP/1.0仅定义了16种状态码。HTTP/1.1中新加入了大量的状态码,光是错误响应状态码就新增了24种。比如说,100 (Continue)——在请求大资源前的预热请求,206 (Partial Content)——范围请求的标识码,409 (Conflict)——请求与当前资源的规定冲突,410 (Gone)——资源已被永久转移,而且没有任何已知的转发地址。

范围请求

HTTP/1.1引入了范围请求(range request)机制,以避免带宽的浪费。当客户端想请求一个文件的一部分,或者需要继续下载一个已经下载了部分但被终止的文件,HTTP/1.1可以在请求中加入Range头部,以请求(并只能请求字节型数据)数据的一部分。服务器端可以忽略Range头部,也可以返回若干Range响应。如果一个响应包含部分数据的话,那么将带有206 (Partial Content)状态码。该状态码的意义在于避免了HTTP/1.0代理缓存错误地把该响应认为是一个完整的数据响应,从而把他当作为一个请求的响应缓存。在范围响应中,Content-Range头部标志指示出了该数据块的偏移量和数据块的长度。

状态码100:HTTP/1.1中新加入了状态码100。该状态码的使用场景为,存在某些较大的文件请求,服务器可能不愿意响应这种请求,此时状态码100可以作为指示请求是否会被正常响应。

压缩

许多格式的数据在传输时都会做预压缩处理。数据的压缩可以大幅优化带宽的利用。然而,HTTP/1.0对数据压缩的选项提供的不多,不支持压缩细节的选择,也无法区分端到端(end-to-end)压缩或者是逐跳(hop-by-hop)压缩。HTTP/1.1则对内容编码(content-codings)和传输编码(transfer-codings)做了区分。内容编码总是端到端的,传输编码总是逐跳的。HTTP/1.0包含了Content-Encoding头部,对消息进行端到端编码。HTTP/1.1加入了Transfer-Encoding头部,可以对消息进行逐跳传输编码。HTTP/1.1还加入了Accept-Encoding头部,是客户端用来指示他能处理什么样的内容编码

Spring
AOP

在这里插入图片描述

有AOP的话放到单例池中的对象是代理对象,同时代理对象中的属性都为空,因为和被代理对象是两个对象,所以没有被代理对象中的属性注入

但是调用代理方法的时候,先执行切面逻辑,然后会执行被代理对象的方法,但是代理对象中没有属性注入,怎么执行方法的呢,是因为创建代理对象的时候,里面有个target对象,会把被代理对象赋值给代理对象,切面逻辑执行完后实际上执行的是target的方法,

先推断构造方法,然后再根据构造方法创建一个实例普通对象
#####初始化前
利用反射在看一下有没有加了@postConstruct注解的方法,有就执行初始化前的方法

初始化

实现InitializingBean接口,重写afterPropertiesSet方法(怎么知道有没有实现这个接口呢。通过bean instanceof InitializingBean判断,如果true,在调用afterPropertiesSet方法)

实例化其实就是通过构造方法得到一个实例对象,初始化其实就是调用这个接口的这个方法

初始化后(AOP)

Spring事务

在这里插入图片描述

事务失效

在这里插入图片描述

事务注解失效,test方法中调用a方法,a方法的事务会失效,原因是相当于new了一个新对象直接调用a方法

总结Transactional注解有没有失效:看是什么对象在调用这个方法,只有代理对象在调用这个方法的时候。注解才有效

解决1:新建一个类,把a方法放到新类里面,因为放在单例池中的类对象就是代理对象,所以是代理对象调用这个方法

解决2:自己注入自己,把自己的代理bean对象注入

在这里插入图片描述

循环依赖

三级缓存,其实二级缓存就可以解决正常的循环依赖,因为a依赖b,属性注入b的时候需要从单例池中找b,这时b需要初始化,但是b又依赖于a,所以可以a在实例化之后属性注入之前放入二级缓存,这样b就可以注入a,这样a又可以注入b,进而注入其他属性,这时b注入的a的属性也注入了,这样就解决了循环依赖。但是不能解决AOP,需要使用三级缓存

在这里插入图片描述

第三级缓存singletonFactory放得是一个lambda表达式,表达式中包含bean\beanDefinition\beanName,BeanDefinition 是定义 Bean 的配置元信息接口,包含:Bean 的类名;设置父 bean 名称、是否为 primary、
Bean 行为配置信息,作用域、自动绑定模式、生命周期回调、延迟加载、初始方法、销毁方法等;Bean 之间的依赖设置,dependencies;构造参数、属性设置

addSingletonFactory(beanName, ()-> getEarlyBeanReference(BeanName, mbd, bean))

a属性注入b,b属性注入a,先去单例池中找a,没有看a是不是正在创建,如果是说明出现了循环依赖,就去二级缓存earlySingletonObjects找,没有就去三级缓存找,然后三级缓存执行那个lambda表达式,判断有没有进行AOP,有的话就先执行AOP并将代理对象放入二级缓存中,将lambda表达式从三级缓存中remove。

二级缓存earlySingletonObjects作用,假如A依赖b和c,b和c又依赖a,a属性注入b,b初始化注入a,a进行AOP给b注入,完成后a又需要注入c,c初始化注入a,a又要进行AOP给c注入,但是b和c中的a应该是同一个对象,所以b进行属性注入a时,提前初始化后实例对象应该放入二级缓存中,然后c在中二级缓存中得到同一个a对象

在这里插入图片描述

5.5步,a还执不执行AOP,如果出现循环依赖,有AOP的化肯定先执行AOP,执行AOP之前会把当前bean放到一个map中,然后初始化之后执行这个map的remove方法,得到的对象是不是当前bean,是的话说明已经进行过AOP,就不用执行了

循环依赖无效:1、用的是构造方法注入,因为不能创建实例对象了。2、多例bean,prototype,因为a依赖b,b依赖a,a是新的对象,又依赖b,所以解决不了。

SpringMVC父子容器

在这里插入图片描述

在这里插入图片描述

JVM
Tomcat底层类加载打破双亲委派机制

一个tomcatweb容器可能需要部署两个应用程序,不同的应用程序可能会依赖于同一个第三方类库的不同版本,不能要求在同一个类库在同一个服务器中只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。比如两个War包应用程序都依赖到了Spring,一个是4.几版本,一个是5.几版本,如果有相同的类要加载,只会加载一个,所以要打破。实现自定义类加载器,实现了要加载相同类名,但是不同jar包版本的类
在这里插入图片描述

在这里插入图片描述

对象分配内存的问题

在这里插入图片描述

在这里插入图片描述

CMS

会产生多标和少标问题,多标就是在并发标记期间假如一个方法执行结束的,局部变量啥的应该被垃圾回收,但是办不到,所以产生浮动垃圾。少标就是并发标记期间一个方法定义了别的变量,引用一个新的对象,在重新标记期间增量更新解决

在这里插入图片描述

并发标记期间用户线程还在执行,导致产生了很多新的对象,可能导致fullGC
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

ZooKeeper

在这里插入图片描述

观察者结点的作用:zookeeper集群中如果增加新的节点的话会影响写的性能,follower节点变多,需要接收一办以上节点的ack才能提交数据,或这前面要向更多的节点发送日志zxid,follower多的话,写的性能就降低。增加观察者节点,观察者节点既不会参加领导者选举,也不会参与两阶段提交的过程,只会同步数据。

增加观察者节点可以增加读的性能,至少也不影响写的性能。

zookeeper集群不会出现脑裂问题,过半机制。

zookeeper和eureka的区别

在这里插入图片描述

分布式
分布式id

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

分布式缓存寻址算法

在这里插入图片描述

session共享

在这里插入图片描述

分布式事务

在这里插入图片描述

两阶段协议

在这里插入图片描述
在这里插入图片描述

三阶段协议
在这里插入图片描述
在这里插入图片描述

TCC(补偿事务)和消息队列的事务消息

前面两阶段、三阶段都是针对数据库层面的,TCC时针对业务层面的

在这里插入图片描述

分布式锁

1、数据库:执行插入操作,根据主键唯一性加锁

2、redis实现

3、zookeeper实现

在这里插入图片描述
在这里插入图片描述

负载均衡

在这里插入图片描述

源地址哈希法可以实现共享Session

ArrayList

默认初始容量大小为10

扩容

以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。

调用add方法,首先调用了ensureCapacityInternal(size + 1),当 要 add 进第 1 个元素时,minCapacity 为 1,在 Math.max()方法比较后,minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity),minCapacity 为 10(首先判断当前的elementData是不是默认的初始化数组,如果是在比较当前size+1和默认数组大小10,如果小于,则minCapacity为10)。然后调用 ensureExplicitCapacity(minCapacity)方法。这个方法中会判断minCapacity - elementData.length > 0,如果是就执行grow(minCapacity)方法,int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)。

在这里插入图片描述

当 add 第 1 个元素时,oldCapacity 为 0,newCapacity为0 ,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX_ARRAY_SIZE 大,则不会进入 hugeCapacity 方法。数组容量为 10,add 方法中 return true,size 增为 1。
当 add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。

从上面 grow() 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果 minCapacity 大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8。

扩容和执行以下方法

elementData = Arrays.copyOf(elementData, newCapacity);

System.arraycopy()

Object
常用方法

getClass() native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。

hashCode() native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。

equals 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。

clone() naitive 方法,protected修饰,用于创建并返回当前对象的一份拷贝。

protected native Object clone() throws CloneNotSupportedException

对于protected能访问的范围其实就是本类 同包中的类 还有子类中,实际上对于protected修饰有这么一段描述"对于protected的成员或方法,要分子类和父类是否在同一个包中。与父类不在同一个包中的子类,只能访问自身从父类继承而来的受保护成员,而不能访问父类实例本身的受保护成员"

CloneNotSupportedException:如果Object的子类没有实现Cloneable接口,但是子类重写(override)了clone方法,子类就会抛出异常

toString()返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。

notify() notifyAll() wait(long timeout) wait(long timeout, int nanos) wait() native方法,使用了 final 关键字修饰,不能重写。

finalize() 实例被垃圾回收器回收的时候触发的操作

== 和 equals() 的区别

== 对于基本类型和引用类型的作用效果是不同的:对于基本数据类型来说,== 比较的是值。对于引用数据类型来说,== 比较的是对象的内存地址。因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。

equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法

equals() 方法存在两种使用情况:类没有重写 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法,比较的对象的内存地址。类重写了 equals()方法 :一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

hashCode

hashCode() 有什么用?
hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

为什么要有 hashCode?
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?下面这段内容摘自我的 Java 启蒙书《Head First Java》:

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

其实, hashCode() 和 equals()都是用于比较两个对象是否相等。那为什么 JDK 还要同时提供这两个方法呢?

这是因为在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)!我们在前面也提到了添加元素进HashSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。

那为什么不只提供 hashCode() 方法呢?这是因为两个对象的hashCode 值相等并不代表两个对象就相等。

那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。

为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

String
String 为什么是不可变的
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
	//...
}

String 类中使用 final 关键字修饰字符数组来保存字符串。我们知道被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final 关键字修饰的数组保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况)。String 真正不可变有下面几点原因:

1、保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
2、String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?

ReentrantLock

AQS,AbstractQueuedSynchronizer,抽象队列同步器,定义了一套多线程访问共享资源的同步器框架,许多并发工具的实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。

AQS使用一个volatile的int类型的成员变量state来表示同步状态,通过CAS修改同步状态的值。当线程调用 lock 方法时 ,如果 state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将 state=1。如果 state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态(独占或共享 )构造成为一个节点(Node)并将其加入同步队列并进行自旋,当同步状态释放时,会把首节中的后继节点对应的线程唤醒,使其再次尝试获取同步状态。

img
在这里插入图片描述
在这里插入图片描述

acquire -> tryAcquire

非公平锁,lock方法先判断compareAndSetState(0,1),成果就说明获得锁,否则执行acquire(1)方法

在这里插入图片描述

acquire方法会调用tryAcquire和acquireQueued(addWaiter(Node.EXCLUSIVE))方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

tryacquire方法会在尝试获得getState(),如果此时刚好State为0,则就可以CAS获得当前锁,如果state不等于0就又会判断当前线程是不是占用锁的线程(实现可重入锁),如果是(判断加上当前线程后有没有超过Int最大值,超过就报错)State值加1,返回true,就是又两种情况返回true,当前无锁和当前锁是自己的线程,否则返回false,

acquireQueued : addWaiter

返回false是!tryacquire为true,执行后面的acquireQueued(addWaiter(Node.EXCLUSIVE))方法,先执行addWaiter方法

在这里插入图片描述
在这里插入图片描述

把当前线程和锁模式(共享、排他)封装成一个Node,先判断尾节点是不是为null,如果是则执行enq(Node)方法

enq是一个自旋,先判断tail是不是为null,是的话先进行初始化,new了一个空节点,头节点和尾节点都指向它,然后继续自旋,这时候尾节点已经不是null了,就将之前尾节点指向的Node的next指向当前线程的Node,尾节点指向当前节点;
在这里插入图片描述

双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。

上面addWaiter方法中尾节点不是Null的时候,就将尾节点指向当前节点,然后维护前后指针

acquireQueued

在这里插入图片描述

先自旋,获得当前node的上一个节点,判断上一个节点是不是头节点,如果是再执行tryAcquire方法

如果不是头节点或者获取锁不成功,则执行shouldParkAfterFailedAcquire(p, node) 和parkAndCheckInterrupt()方法

shouldParkAfterFailedAcquire

在这里插入图片描述

先将初始哨兵节点waitStatus为-1,如果waitStatus的值大于1,说明是cancelled节点,do while循环判断前驱节点是否也为cancelled状态,忽略该状态的节点,重新连接队列。
在这里插入图片描述

如果前驱节点的waitStatus是signal状态-1,即shouldParkAfterFailedAcquire方法会返回true,程序会继续向下执行parkAndCheckInterrupt方法,用于将当前线程挂起
在这里插入图片描述

从上面可以知道,AQS虽然有用CAS进行尝试加锁,但是加锁失败后还是通过LockSupport来实现阻塞和解锁的。那么LockSupport又是通过什么实现阻塞的呢?可惜我们点进去源码会发现它是一个native方法,通过观察源码我们可以发现,实际上阻塞park和唤醒unpark是用到了mutex和condition的方法调用。LockSupport也是基于mutex实现的。要知道,synchronized的重量级锁底层依赖的是mutex lock,会有用户态和内核态的切换,才会有AQS的一堆优化,然而我们会发现,AQS也是通过mutex来实现的!、

实际上ReentrantLock在CAS加锁失败之后会封装成一个Node类型的对象加入CLH队列中,然后调用LockSupport.park(this)进行阻塞
而LockSupport是一个native方法实现的工具类,在hotspot源码中也是通过mutex来实现的,一些情况下它的开销可能不会Synchronized好。
Lock是通过自旋CAS和Unsafe.park/unpark挂起唤醒线程来实现的,而synchronized在jdk1.6后重量级锁也是通过CAS自旋以及park/unpark来实现的,都有进行用户态和内核态的切换。但是synchronized做了优化后在前面的偏向锁拿到锁的线程不会进行CAS自旋,而轻量级锁也只是进行CAS自旋不会阻塞挂起,只有膨胀到重量级锁后才会自旋CAS+park/unpark来挂起唤醒线程!所以理论上synchronized的效率应该比Lock快一点点,但是Lock提供的API比较简单方便

unlock -> realse

在这里插入图片描述

tryRelease

在这里插入图片描述
在这里插入图片描述

Unlock会执行release(1)方法,release方法先执行tryRelease(1)方法,先获得当前State减去1,如果是可重入锁减去1后重新设置当前状态,如果前去1和等于0,就返回true,并设置当前线程为Null,执行realse中的unparkSuccessor方法。

在这里插入图片描述

unparkSuccessor方法中,先获得当前节点的下一个节点,如果下个节点是null或者下个节点被cancelled,就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节,就找到队列最开始的非cancelled的节点,如果当前节点的下个节点不为空,而且状态<=0,就把当前节点unpark

为什么要从后往前找第一个非 Cancelled 的节点呢

private Node addWaiter(Node mode) {
	Node node = new Node(Thread.currentThread(), mode);
	// Try the fast path of enq; backup to full enq on failure
	Node pred = tail;
	if (pred != null) {
		node.prev = pred;
		if (compareAndSetTail(pred, node)) {
			pred.next = node;
			return node;
		}
	}
	enq(node);
	return node;
}

之前的 addWaiter 方法中,节点入队并不是原子操作,也就是说,执行compareAndSetTail(pred, node),后,此时 pred.next = node;还没执行,如果这个时候执行了 unparkSuccessor 方法,就没办法从前往后找了,所以需要从后往前找。还有一点原因,在产生 CANCELLED 状态节点的时候,(先断开的是 Next 指针,Prev 指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的 Node)。】
在这里插入图片描述

综上所述,如果是从前往后找,由于极端情况下入队的非原子操作和 CANCELLED 节点产生过程中断开 Next 指针的操作,可能会导致无法遍历所有的节点。所以,唤醒对应的线程后,对应的线程就会继续往下执行

设计模式 设计原则
开闭原则

对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。是为了使程序的扩展性好,易于维护和升级。
想要达到这样的效果,我们需要使用接口和抽象类。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

里氏代换原则

任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。就是说子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

依赖倒转原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。(面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。)

接口隔离原则

客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。

迪米特法则

迪米特法则又叫最少知识原则。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

合成复用原则

合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
通常类的复用分为继承复用和合成复用两种。
继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所
    以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展
    与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可
    能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已
有对象的功能,它有以下优点:

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”
    复用。
  2. 对象间的耦合度低。可以在类的成员位置声明抽象。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的
    对象
Java为什么是单继承

Java是一个面向对象的编程语言,如果可以继承两个类,那这个类是什么,显然有点不太合理

假设现在有一个A类,然后,随后又编写了两个类,B类以及C类。B类和C类分别继承了A类,并且,对于A类的同一个方法进行了重写。之后,又编写了一个D类,D类以多继承的方式同时继承了B类和C类。这样的话,D类也会继承B类和C类从A类中重写的方法。这是就会出现结构上的混乱

单继承体系保证了所有新建的或JDK中已有的类都必须继承自OBJECT。保证所有类中都有一些基本的方法,hashCode,equals等

两个接口具有相同方法

实现了具有相同方法(名字相同、参数相同、返回类型相同)的多个接口时,这个方法实现是共用的
具有相同方法时一定要重写,即使是default方法也要重写
当方法参数不同时,方法之间为重载,互不影响
当返回值不同时,会发生错误,不能编译

MQ

在这里插入图片描述

在zk中会维护ISR和OSR,ISR的意思就是和leader节点中的数据一致,当leader宕机后会选取ISR中的节点成为新的leader

在这里插入图片描述

在这里插入图片描述

进程与线程
进程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

线程

一个进程之内可以分为一到多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
Java 中,线程作为小调度单位,进程作为资源分配的小单位。 在 windows 中进程是不活动的,只是作 为线程的容器
二者对比
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集 进程拥有共享的资源,如内存空间等,供其内部的线程共享

进程间通信较为复杂 同一台计算机的进程通信称为 IPC(Inter-process communication)

不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP

线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

进程和线程的切换
上下文切换

内核为每一个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。包括以下内容:

通用目的寄存器
浮点寄存器
程序计数器
用户栈
状态寄存器
内核栈
各种内核数据结构:比如描绘地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表

进程切换和线程切换的主要区别

最主要的一个区别在于进程切换涉及虚拟地址空间的切换而线程不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换

页表查找是一个很慢的过程,因此通常使用cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是快表TLB(translation Lookaside Buffer,用来加速页表查找)。由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程线程无需切换地址空间,因此我们通常说线程切换要比较进程切换快

而且还可能出现缺页中断,这就需要操作系统将需要的内容调入内存中,若内存已满则还需要将不用的内容调出内存,这也需要花费时间

为什么TLB能加快访问速度

快表可以避免每次都对页号进行地址的有效性判断。快表中保存了对应的物理块号,可以直接计算出物理地址,无需再进行有效性检查

线程安全

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值