select/poll/epoll

在客户端请求服务端时,就是在服务端写入对应的客户端文件描述符FD,如果多个客户端同时请求一个服务端,每一次请求开一个线程,会耗费CPU资源,因此,用一个线程监听多个服务器资源,就是IO多路复用,通信模型有三种,select/poll/epoll。
第一种select,在需要判断是否有节点就绪时,会把文件描述符FD从用户态拷贝到内核态中,因为在内核态中判断,效率会比较高。
当遍历完文件描述符FD,会把事件返回,select再由用户态向内核态会耗费系统资源,效率比较低,除此,文件描述符以数组保存,在32位,有最大数量1024,64位为2048。拷贝过程中,是用bitmap位图来标记文件描述符的状态,并且使用完后需要置位。

第二种poll 为了解决数组上限和位图置位问题,引用新结构体pollfd,包含fd,event,revent,首先看fd,是由链表保存,解决连接上限问题,event是读写和异常事件,revent是返回事件,可重用,解决select中位图置位问题,需要循环遍历判断就绪,时间O(n)。

第三种epoll,可以把就绪判断控制在O(1),通过三个方法实现,epoll_create()会创建poll实例;epoll_ctl(),会FD注册到内核,避免了用户态的拷贝,内核中保存FD是用了红黑树;epoll_wait(),在
红黑树中有就绪时间时,会把它在双向链表中,让用户调用epoll_wait()时,会直接从链表中返回,这里涉及两种触发模式,LT和ET模式;LT水平触发,wait检查fd就绪时,不立即转化双链表,当下次调用wait方法时,再通知进程,这是默认的触发方式;ET边缘触发,通知后进程立即处理事件,下次调用wait方法不会通知,减少epoll时间被重复触发的次数,效率比LT高。

Spring Cloud 微服务

起初,一个项目的所有功能模块都在一个工程中编码,编译,打包,比如部署在一个tomcat容器中,一般只需要一个数据库,随着业务和用户增长,tomcat需要集群化,负载均衡则需要一个nginx,甚至用到文件或缓存服务器,这样存在单个项目过大 ,而且耦合严重的问题。流行的微服务就可以把项目拆分粒度更小,耦合度降低,并且可以独立部署和扩展,可维护性更强,一个微服务可以部署多个节点来支撑。当服务过多,需要有效识别和管理服务,就需要引入服务治理,通过服务的发现和注册,比如阿里nacos,充当注册中心,把所有启动的服务注册进来,当然,它也可以作为配置中心。

注册中心:Eureka册中心将返回所有可用的服务实例给消费者,但是一般不推荐这种情况。另一种方法就是本地Map存储服务实例列表,服务的消费者向注册中心订阅某个服务,并提交一个监听器,当注册中心中服务发生变更时,监听器会收到通知,这时消费者更新本地的服务实例列表,以保证所有的服务均是可用的。
配置中心:客户端是通过一个定时任务来检查自己监听的配置项的数据的,一旦服务端的数据发生变化时,客户端将会获取到最新的数据,并将最新的数据保存在一个 CacheData 对象,对比apollo,配置都没有心跳机制,nacos2.0 采用则统一gRBC通信,流式推送更可靠,吞吐量更少,QPS更高,缺点是没有权限配置,只提供给轻量化的配置)

之前服务注册中心,通过30秒心跳包确定服务是否存活,30秒续约一次。但是,服务扩容时,由于心跳处理不及时,如果大量服务瞬时进行注册,有一定的概率推送超时,但是会在重试后推送成功,保持数据一致性,这种重试会使系统处于达不到稳态。nacos2.0长连接心跳,响应及时,推送失败率少,快速达到稳态。

nacos 1.x

  • 每个服务实例都通过心跳续约,在Dubbo场景每个接口对应一个服务,当Dubbo的应用接口数较多时需要心跳续约TPS会很高。
    心跳续约感知时延长,需要达到续约超时时间才能删除实例,一般需要15S,时效性较差
  • 通过UDP推送变更数据不可靠,需要客户端定时进行数据全量对账保证数据的正确性,大量无效查询,整体服务的QPS很高
    通信方式基于HTTP短链接的方式,Nacos侧释放连接会进入
  • TIME_WAIT状态,当QPS较高时会有连接耗尽导致报错的风险,当然这里通过SDK引入HTTP连接池能缓解,但不能根治
  • 配置的长轮询方式会导致相关数据进入JVM Old区申请和释放内存,引起频繁的CMS GC

nacos 2.x

  • 应用POD按照长连接维度进行心跳续约,不需要按照实例级,大大降低重复请求
  • 长连接断开时可以快速感知到,不用等待续约超时时长就可以移除实例
  • NIO流式推送机制相对于UDP更可靠,并且可以降低应用对账数据频率
  • 没有连接反复创建的开销,大幅降低TIME_WAIT连接多问题
    长连接也解决了配置模块长轮询CMS GC问题

消费者服务向提供者服务请求,可以采用dubbo组件,进行RPC远程调用,通信基于HTTP2长连接,交换数据格式基于Protocolbuf。

Triple 协议是 Dubbo3 推出的主力协议。它采用分层设计,其数据交换格式基于Protobuf (Protocol Buffers) 协议开发,具备优秀的序列化/反序列化效率,当然还支持多种序列化方式,也支持众多开发语言。在传输层协议,Triple 选择了 HTTP/2,相较于 HTTP/1.1,其传输效率有了很大提升。此外HTTP/2作为一个成熟的开放标准,具备丰富的安全、流控等能力,同时拥有良好的互操作性。Triple 不仅可以用于Server端服务调用,也可以支持浏览器、移动App和IoT设备与后端服务的交互,同时 Triple协议无缝支持 Dubbo3 的全部服务治理能力。

当并发比较高,需要做负载均衡,Ribbon组件提供多种负载均衡策略,轮询,随机,最少活跃,最短响应,一致性hash。
当依赖服务无法请求,导致调用链的大量服务雪崩,可以使用 Sentinel组件,可以使用熔断降级和限流阻止情况的发生。如果所有方法都用同一个线程池第线程,A把打满线程池,导致B不可用线程池,Hystrix采用可以创建多个线程池隔离,也能进信号量隔离,舱壁模式;Sentinel用tomcat现有的线程池,不创建新池,只能信号量隔离(并发线程数限流)。防止恶意流量,需要限流,其策略采用漏斗或者令牌桶算法。

要了解多服务的调用链路和顺序,监控性能指标,需要搭建APM系统,采用Skywalking全链路服务追踪,无入侵埋点,使用java Agent探针,通过字节码注入实现拦截和数据收集。比zipkin效率高。

分布式事务

少数情况,需要使用分布式事务。单机时代,可以把多个逻辑放在同一事务中,保证原子性,当两个逻辑在A,B两个节点时,需要保证要么全部成功,要么全部失败,需要引入第三方事务协调者TC,分别向A,B下达开始began命令,A和B分别预处理本地事务,处于uncommitted状态,会被排他锁锁定,本地处理好事务,会ack通知事务协调者;所有ack成功,此时进入第二阶段提交,TC下达提交commit或回滚rollback命令,A,B返回提交成功或回滚的ack响应。但是会存在一个问题,下达二阶段提交命令,由于网络或其他原因,造成无法及时送达,A,B处于阻塞状态。

解决这个问题,需要再引入一次提交precommit命令,ack协商,保证各节点状态相同,还引入超时机制解决阻塞,这就是3阶段提交3pc。在第一次预提交事务之后,插入一次询问是否提交就绪,并设置超时时间来解决阻塞问题。 三阶段提交解决了二阶段提交的单点故障问题并减少了阻塞。引入了超时机制,仍然有一致性问题,而且整体的交互过程更长了。PreCommit后,发出abort请求,只有一个Cohort收到abort,其他继续Commit,不一致性。

2pc问题
1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。

2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

3、数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

4、二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

2PC与3PC的区别:
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

*2PC:主持人跟第一位组员通话后,主持人失忆组员得知结果并执行后痴呆,重选主持人,没人去提案,任务阻塞。

3PC:即使主持人通知一部分组员执行后失忆全体决策组员过一段时间决定全体自发提案或全体默认否决,此时没有阻塞,也没有不一致

如果主持人通知全体组员,大家再次确定,进入第三阶段

这时主持人通知第一位组员,请通过提案后两人失忆重选出主持人,所有人仍自发去提案,没有问题;如果主持人通知是否决任务,通知第一位组员否决后两人失忆重选出主持人,所有人仍自发去提案,出现不一致。

其实在我看来,解决单点故障减少阻塞的并不是将第一阶段掰成两半的操作,而是在最后提交的时候,超时自动提交。

两阶段完全也可以做成超时自动提交,只不过这样就和三阶段一样,无法保证数据的一致性。

为了得到最终一致性,我们要用补偿机制。

2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务,从思想上看和 2PC 差不多,用于补偿。

TCC是成熟的分布式事务解决方案,核心思想是事务补偿机制,包括try,confirm,cancel三个接口。try尝试锁定所有需要使用的资源,当所有回答yes,进入confirm 提交事务阶段,如果出现失败,调用cancel会滚补偿,不过有一个缺点,就编码量大。

优点:
性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
缺点:TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。

SAGA 是一种长活事务,把一个大的事务分解成一个个小事务T,每一个小事务T,都有一个对应的补偿任务C,当小事务失败,有两种补偿,1是正向重试T,2是反向恢复C。

也可以借助本地事务状态表来实现分布式事务:初始状态为1,每次成功调用一次服务,则更新一次状态,在所有状态为3时,就说明所有服务调用成功。

基于消息中间件的分布式事务解决方案:利用额外的MQ实现事务一致性,在业务A中把B的任务记录为msg B,逻辑合并在同一个事务中,并且把消息存储在本地DB表中,状态为待发送,并且开一个异步定时任务在本地轮询扫描这个表作为后续重试机制,通过MQ把消息发送到B,过程中可能存在消息投递失败的可能,此时就依靠重试机制来保证,当B收到消息时,在做对应操作前,会检查之前是否做过,因为集群或重试重复消息,需要做去重的幂等处理,保证消息不被重复消费。而后面如果B消费失败的话,则依赖MQ本身的重试来完成。B做完返回ACK到MQ,弹出MQ对应消息,同时将A接收ACK将对应消息状态更新或者消息清除;当然,后面A处理任务也是幂等。
多次重试后消息状态改为“超时”,要么回滚整个业务,要么再加一个单独地定时任务,它会间隔更长时间的定期轮训长时间处于超时状态的消息,通过一个check补偿机制来查询确认该消息对应的业务是否成功,如果对应的业务处理成功,则将消息然后将其投递给MQ,走一遍幂等业务;如果业务处理多次失败,则将对应的消息更新为失败,多次尝试失败记录警告,并通知等待人工干预,比如后台补单操作。因此在使用该方案时,消息生产者必须同时实现一个check查询服务,来供消息服务做消息的确认。

联想“支付场景”逻辑

https://www.cnblogs.com/myseries/p/10819804.html

独立消息服务

优点:
消息数据独立存储,降低业务系统与消息系统之间的耦合。
实现部分事务可重试
缺点:
强依赖MQ可靠
一次消息发送需要两次网络请求(half消息 + commit/rollback)。

业界有一些为“最终一致性”而生的消息队列,如Notify(阿里)、QMQ(去哪儿)等,其设计初衷,就是为了交易系统中的高可靠通知。

seata支持多种模式:
XA协议:2PC,3PC,实现强一致性
TCC,SAGA,独创的AT模式,实现最终一致性;

AT模式,有几个重要的角色,TM用于开启全局事务,可提交或会滚,A,B对应两个子任务,TC是事务协调者。
第一步,TM,A,B分别把自己注册给TC
第二步,TM开启全局事务,A处理事务,首先向TC注册分支事务,然后预处理A的操作,并向sql写入会滚日志,最终提交本地事务;同样,B也注册分支事务,预处理自己的逻辑,写会滚日志,提交处理成功告诉TC。
第三步,所有逻辑处理完,TM提交全局事务,TC分别向A,B提交分支事务,并且删除所有回滚日志。或者,进行全局的回滚。

单例模式的原因

双重校验锁DCL ——double check lock

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;

public static Instance getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)
instance = new Instance();//instance为volatile,现在没问题了
}
}
return instance;
}
}

1.懒汉实现惰性加载资源。

  1. volatile修饰,设置内存屏障,强制指令执行顺序,先分配内存空间,再创建对象,最后引用指向该对象。

  2. static修饰,定义属于类变量,保证单例

  3. 第一个判断null,过滤synchronized锁的性能消耗
  4. 同步块,设置临界区,同时间只允许一个线程完成对象的创建。
  5. 第二个判断null,防止多线程进入第一个判断null,而导致多对象的创建。

Q: 过去DCL不安全的原因?
JVM1.4前,允许指令混乱情况。final:一个线程构造函数为final修饰变量赋值,另一个线程可能获取final的初始化0值;volatile:只保证读可见性,不保证写的顺序

Q: 不用volatile,先临时变量,再赋值到单例变量,是否能解决指令重排?
不能,JVM只是潜在的reorder推手之一,CPU和缓存也会导致重排

Q: 新版本volatile如何保证DCL安全?
设置内存屏障,防止指令重拍,其中store-load屏障是保证读写顺序,实现volatile上的happens-before语义,即一个线程对volatile变量的写,先于其他线程对该变量的同时读。屏障的结果就是原来只保证volatile字段本身的可见性,现在保证本身以及所有相关字段的可见性,代价就是造成访问volatile字段的更大性能开销。

Q:是否可以用final实现DCL?
能。
a.对final字段的写必然先于其它线程装载该final字段的共享引用。(final字段必须被正确地赋值后其它线程才能读取到它)
b. 构造函数执行完毕后,对 final 字段的所有写以及通过这些 final 字段间接可及的变量变为“冻结”,所有在冻结之后获得对这个对象的引用的线程都会保证看到所有冻结字段的冻结值。(所有线程对final及其间接字段有一致的可见性)
c. 初始化 final 字段的写将不会与构造函数关联的冻结后面的操作一起重排序。(构造函数内部,对某个final字段而言,它的冻结点之前的操作必然先于冻结点之后的操作)

再来说一下final域的重排规则:

  1. 写final的重排规则:
    JMM禁止编译器把final域的写重排序到构造函数之外。
    在final域的写之后,插入一个StoreStore屏障。
    也就是说确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了。
  2. 读final的重排规则:
    在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。
    在读final域操作的前面插入一个LoadLoad屏障。
    也就是说确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。
  3. 如果final域是引用类型,那么增加如下约束:
    在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
    就是确保在构造函数外把这个被构造对象的引用赋值给一个引用变量之前,final域已经完全初始化并且赋值给了当前构造对象的成员域,至于初始化和赋值这两个操作则不确保先后顺序。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class FinalWrapper<T> {
public final T value;
public FinalWrapper(T value) {
this.value = value;
}
}

public class Foo {
private FinalWrapper<Helper> helperWrapper;

public Helper getHelper() {
FinalWrapper<Helper> wrapper = helperWrapper;

if (wrapper == null) {
synchronized(this) {
if (helperWrapper == null) {
helperWrapper = new FinalWrapper<Helper>(new Helper());
}
wrapper = helperWrapper;
}
}
return wrapper.value;
}
}

Q: DCL是否是实现单例的最佳方案?是否有代替?
不是。DCL的目的是懒加载和提高性能,但现代JMM加强volatile保证可靠,但副作用是性能开销变大,且加上了同步块的开销。

完美方案:
推荐通过内部类实现惰性加载:
有一个大前提,JVM保证类的加载过程是线程互斥。
第一个调用getInstance,instance只被创建1次,且赋值给instance的内存已初始化完毕,避免reorder,此外,该方法第一次使用互斥机制,解决同步块带来的性能问题;
当然,只有第一次调用getInstance时才让instance加载,所以是惰性。

1
2
3
4
5
6
7
8
9
10
11
public class Singleton{      
private Singleton(){

}
private static class SingletonContainer{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonContainer.instance;
}
}

参考:https://blog.csdn.net/doraeimo/article/details/5714239

volatile 保证可见性?

层层深入讲。

  1. 可见性是指解决多线程在操作同一变量时,多个工作内存中的值不一样的问题。
  2. JMM中定义了8种原子操作保证可见性:
    除了lock和unlock,
    还定义了
    read load use 作为一种读原子操作
    assign store write 作为一种写原子操作
    主存值read到执行引擎,执行引擎值load到一个栈存副本变量,栈存值use 到执行引擎,以便计算处理;
    执行引擎值 assign到栈存副本变量,栈存副本值store到主存,主存值write 到主存的变量,以便共享;
    避免在 读过程 或 写过程 中间操作被打断。
    虽然use 和assign存在 被打断的可能,但工作内存和主内存依旧想等。
    因此在多线程内,适合flag赋值,不适合a++等非原子操作,运算结果a会对中间加数a产生依赖。
  3. 原子操作的底层实现需要两种方式:
    一个是通过总线的,总线上有一个缓存一致性协议。总线有一个数据修改,所有的数据修改都是从总线推到主存,这个时候,其他cpu会嗅探总线上的修改,然后推送给其他cpu置旧的缓存不可用,下次再使用,就会从主存里读取(这部分可展开CPU底层)。还有一个是通过内存屏障禁止指令重排序
    内存屏障有4种,load和store指令之前执行,比如:volidate 有读和写操作,在写前加store屏障,保证写写操作,不能重排序;在写后加storeLoad屏障,保证先写再读。在第一个读后加入loadLoad,保证读读的顺序;读后加入loadStore,保证读写顺序,StoreLoad虽然是全能屏障,同时具有其他3个屏障的效果,但开销大,因为处理器通常要把当前的写缓冲区的内容全部刷新到内存。基本上这就是volidate的禁止重排序的配合。

Q:问点简单的,细说8种原子操作?

(这要背了。。。)

  1. read 读取:作用于主内存,将共享变量从主内存传动到线程的工作内存中,供后面的 load 动作使用。
  2. load 载入:作用于工作内存,把 read 读取的值放到工作内存中的副本变量中。
  3. store 存储:作用于工作内存,把工作内存中的变量传送到主内存中,为随后的 write 操作使用。
  4. write 写入:作用于主内存,把 store 传送值写到主内存的变量中。
  5. use 使用:作用于工作内存,把工作内存的值传递给执行引擎,当虚拟机遇到一个需要使用这个变量的指令,就会执行这个动作。
  6. assign 赋值:作用于工作内存,把执行引擎获取到的值赋值给工作内存中的变量,当虚拟机栈遇到给变量赋值的指令,执行该操作。比如 int i = 1;
  7. lock(锁定) 作用于主内存,把变量标记为线程独占状态。
  8. unlock(解锁) 作用于主内存,它将释放独占状态。
深入浅出Java虚拟机

深入浅出Java虚拟机

Q:解释缓存一致性协议和嗅探机制?

a. cpu三级缓存讲起,为解决主内存IO速度远远低于CPU运行速度,防止读CPU被阻塞,浪费CPU性能,CPU引入L1/L2/L3缓存行。
由于高速缓存行的引入,导致了多核CPU的并行访问缓存数据不一致的问题。比如更新一个CPU核的缓存,另外CPU依然存旧的值。
b. 为了解决脏数据和丢失更新的问题,要实现写原子操作,那么,需要两个锁:
一个总线锁,锁cpu和主内存通信,还阻塞其他CPU,保证锁期间只有一个CPU改写,但其他CPU也不能操作其他内存数据,似乎无影响操作也锁上了,开销过大。
一个缓存锁,只锁定被修改的缓存行。锁期间会进行一个CPU缓存的更新,以及其他缓存失效。这利用了缓存一致性协议mesi。缓存行有4状态:M修改,E独占,S共享,I失效。
共享表示主存和所有cpu的值一致,独占表示只有一个cpu有缓存,失效表示当前cpu存储旧值,修改表示当前cpu存储新值。
当一个cpu由共享态被主存改为修改态,cpu会更新一个排他标志,并且,通过总线,广播通知其他CPU,其他CPU异步“嗅探”事件后,把数据改为失效态。等下一次访问,再更新。

拓展性表格:
| |M |S |E |I |
|–|–|–|–|–|
|M |X |X |X |O |
|E |X |X |X |O |
|S |X |X |O |O |
|I |O |O |O |O |

Q:有缓存锁?为什么还要总线锁?

(小细节不放过)

有两种情况不能用缓存锁:
一是数据大或数据跨多缓存行,二是部分CPU不支持缓存锁。
现代CPU总线锁和缓存锁并存实现数据一致性。

Q:从硬件架构来说,CPU为什么会重排序?

​ 前面指令如果依赖的数据发生缓存缺失,那么需要去内存磁盘读取数据,这个过程很耗时,如果不乱序执行的话,后面所有的指令都会被block住,这样CPU的吞吐量上不去,所有会有乱序执行机制,让后面没有数据依赖关系的指令可以不用等前面指令执行完了再执行(IPC,指令级并发)

作者:sakura1027
链接:https://www.nowcoder.com/discuss/459561?channel=-1&source_id=profile_follow_post_nctrack
来源:牛客网

​ 首先,我们要知道,CPU是并行流水线作业。本质上是利用了电路天然的并行性。这些电路,实际上都是一个个晶体管组合而成的。想要计算得快,一方面,我们要在 CPU 里,同样的面积里面,多放一些晶体管,也就是增加密度;另一方面,我们要让晶体管“打开”和“关闭”得更快一点,也就是提升主频。而这两者,都会增加功耗,带来耗电和散热的问题。

​ 在 CPU 内部,和我们平时戴的电子石英表类似,有一个叫晶体振荡器(Oscillator Crystal)的东西,简称为晶振。我们把晶振当成 CPU 内部的电子表来使用。晶振带来的每一次“滴答”,就是时钟周期时间。Clock Cycle Time 一次晶振时间,时钟周期。简称CPI

比如2.8GHz 就是电脑的主频,CPU 在 1 秒时间内,可以执行的简单指令的数量是 2.8G 条2.0GHz意味着每秒钟它会产生20亿个时钟脉冲信号,每个时钟信号周期为0.5纳秒

对于 CPU 时钟周期数,我们可以再做一个分解,把它变成“指令数×每条指令的平均时钟周期数”,加法和乘法都对应着一条 CPU 指令但是乘法需要的 Cycles 就比加法要多

现代的 CPU 通过流水线技术(Pipeline),让一条指令需要的 CPU Cycle 尽可能地少。因此,对于 CPI 的优化,也是计算机性能优化的重要一环。

指令执行过程拆分成“取指令、译码、执行”这样三大步骤。这为一个指令周期。

CPU一个时钟周期有很多条并行的流水线。

比如,五级的流水线,就表示我们在同一个时钟周期里面,同时运行五条指令的不同阶段。这个时候,虽然执行一条指令的时钟周期变成了 5,但是我们可以把 CPU 的主频提得更高了

并行如何保证顺序流水线之间顺序?

数据冒险,其实就是同时在执行的多个指令之间,有数据依赖的情况。这些数据依赖,我们可以分成三大类,分别是先写后读(Read After Write,RAW)、先读后写(Write After Read,WAR)和写后再写(Write After Write,WAW)。

解决这些数据冒险的办法,就是流水线停顿(Pipeline Stall),或者叫流水线冒泡(Pipeline Bubbling),起到延迟再等等的作用。好像是内存屏障。

Q:什么是三级缓存?

刚刚提到,为解决主内存IO速度远远低于CPU运行速度,防止读CPU被阻塞而引入L1/L2/L3。
L1各CPU独立,保存L2的数据,最小最快,如256k;L2各CPU独立,保存L3的数据,更大偏慢,如1M;L3各CPU共享,注意,不是所有CPU核共享,有多个L3,每几个CPU核共享一个L3,保存主存取的数据,最大最慢,如6MB。具体是二级还是三级根据CPU硬件而定。

CPU需要数据的时候,会先在一级缓存中寻找数据,一般一级缓存的数据命中率可以达到80%。如果一级缓存中找不到数据,CPU就会到二级缓存中寻找数据,如果依旧找不到的话,就会到三级缓存中找。有的CPU有四级缓存,三级缓存中没有,那就到四级缓存中找。

缓存怎么存呢?用缓存行,一个缓存行为64字节。
偏爱存储连续相邻的空间位置数据,体现为经常是访问数据+相邻数据,存指令也是,这就是空间局部性,比如mysql的B+树。如果一个信息被访问一次,近期可能会再次被访问,比如循环,递归,方法的反复调用,这就是时间局部性。共同构成局部性原理。

Q:既然提到缓存行,知道伪共享问题吗?如何解决?

(深…不见底)
伪共享,不是缓存是否真假共享问题哈,而是一个性能问题。
a. 首先,我们知道,对共享数据的写操作,会加锁+EMSI操作,耗费性能。
b. 伪共享,就是几个在逻辑上相互独立但在同一个内存单元内的数据,由于被cpu加载在同一个缓存行当中,当在多线程环境下,被不同的cpu执行,导致缓存行失效而引起的大量的缓存命中率降低。
例如:当两个线程分别对一个数组中的两份数据进行写操作,每个线程操作不同index上的数据,看上去,两份数据之间是不存在同步问题的,但是,由于他们可能在同一个cpu缓存行当中,这就会使这一份缓存行出现大量的缓存失效,如前所说,一个线程更新CPU缓存,会让其他CPU缓存失效掉。
c. 解决伪共享,采用缓存行填充,Cache Line Padding。具体就是定义无用变量代码。
解决伪共享问题的一个办法是让每一份数据占据一个缓存行:因为缓存行的大小是64个字节,那我们只要让数组中每份数据的大小大于64个字节,就可以保证他们在不同的缓存行当中,就能避免这样的伪共享问题。
比如一个类当中原本只有一个long类型的属性。这样这个类型的对象只占了16个字节(java对象头有8字节),如果这个类被定义成一个长度为4的volatile数组,这个数组的所有数据都可能在一个缓存行当中,就可能出现伪共享问题,那么这个时候,就可以采用补齐(padding)的办法,在这个类中加上public long a,b,c,d,e,f,g;这6个无用的属性定义,8字节对象头+8字节long*7个,使得这个类的一个实例占用内存达到64字节,这样这个类型的伪共享问题就得到了解决,在多线程当中对这个类型的数组进行写操作就能避免伪共享问题。(妙)

Q: 8字节对象头包括什么?算了,先不问这个

Q:volatile的使用会导致什么问题?能避免吗?

(真的能问)

  1. volatile和cas使用过多会产生工作内存和主内存频繁交互、嗅探等操作,其很多事无效的操作,而系统共用一条总线,总线(bus)带宽资源有限,其中还有其他数据流、显存等交互,总线流量激增,这样就导致总线风暴。
  2. 不保证原子性。使用volatile在相关领域周围竖立记忆障碍物.好处是这不会导致线程进入”阻塞”状态。原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。但上面提到,use和assign是被打断的,以至于a++不是原子操作。只能一定程度上保证有序性。
  3. 可能导致内核态与用户态的上下文切换。 它不会像锁一样直接引起线程上下文的切换和调度。当volatile访问该字段时,程序必须刷新对中央存储器的更改并更新需要周期的高速缓存存储器。在CPU的“嗅探”到消息后,CPU中断去处理失效,此时CPU程序计数器栈会发生上下文切换。
  4. 当然,还有刚说的伪共享。几个逻辑上独立数据,加载在同一个缓存行,更新引发批量缓存失效问题。

解决原子性,可以采用Atomic原子变量,有两种写set和lazySet
set方法使用CAS + volatile,在写操作的前后都加了内存屏障。lazySet方法并不是直接的操作value字段,而是通过Unsafe类的方法先通过初始化时候计算出的value字段的偏移变量找到字段地址,然后调用本地方法进行操作的,在本地方法中只在写操作前面加了一个屏障,而后面没有加。相当于只用普通变量。
因为引入CAS轻量级锁,有自旋操作,更消耗CPU。

总之,使用volatile或Atomic,根据具体业务场景而定。

Q:CAS底层?

  1. 处理器提供总线锁定缓存锁定两个机制来保证复杂内存操作的原子性
  2. cmpxchg指令加上lock前缀(lock cmpxchg)

    3.锁住总线,使得其他处理器暂时无法通过总线访问内存。

    4.StoreLoad内存屏障,禁止该指令与之前和之后的读和写指令重排序,使得线程把不是单单被修改的数据会被回写到主内存,而是写缓存中所有的数据都回写到主内存。

    5.而将写缓冲区的数据回写到内存时,就会通过缓存一致性协议(如,MESI协议)和窥探技术来保证写入的数据被其他处理器的缓存可见。这就相当于实现了volatile的内存语义。

Q:上面有提到CPU上下文切换,来说说用户态和内核态?

(来了,来了。扯操作系统,干爆!)

从4个点讲:

a. 从CPU指令集开始讲,多个汇编指令的集合就是CPU指令集。

CPU指令集分为4个权限:ring 0,ring 1,ring 2 和 ring 3;

ring 0 权限最高,ring 3 权限最低。

  • ring 0被叫做内核态,完全在操作系统内核中运行,可以使用所有 C P U 指令集
  • ring 3被叫做用户态,在应用程序中运行,,不能使用操作硬件资源的 C P U 指令集,比如 I O 读写、网卡访问、申请内存都不行。
  • ring1与ring2主要是访问驱动程序,win10 和 linux 只有 ring 0 和ring 3。

b. 为什么划分权限?内核模式下任何异常都是灾难性的,会导致停机。用户模式下,可以限制对硬件的直接控制权限,只能通过系统提供的调用接口来控制。在这种保护模式下,即时用户程序发生崩溃也是可以恢复的,在电脑上大部分程序都是在用户模式下运行的。

c. 再说,每个进程都有两个栈,分别是用户栈与内核栈,对应用户态与内核态的使用。用户程序会使用用户栈,为了可以操作ring 0 级别的 CPU 指令集, CPU切换权限级别为 ring 0,进入内核态,CPU再执行相应的ring 0 级别的 CPU 指令集(内核代码),执行的内核代码会使用当前进程的内核栈。

d. 再一个是,用户态与内核态的虚拟内存寻址空间。操作系统会把一个进程的虚拟控制内存地址划分为两部分,以32位Linux操作系统为例,假如有4G寻址空间范围 ,高位部分为内核栈, 1G由内核使用;低位部分为用户栈, 3G由各个进程使用。

Q: 我插一句。。。

c. 先别插话,讲最后一点,用户态和内核态切换的开销大。

  • 保留用户态现场(上下文、寄存器、用户栈等)
  • 复制用户态参数,用户栈切到内核栈,进入内核态
  • 额外的检查(因为内核代码对用户不信任)
  • 执行内核态代码
  • 复制内核态代码执行结果,回到用户态
  • 恢复用户态现场(上下文、寄存器、用户栈等)

Q: 那我问一句。。。

d. 还有!还有!关于什么情况会导致用户态到内核态切换?

1-系统调用,如fork()创建新进程;

2-异常,如缺页异常 ;

3-CPU中断,如硬盘读写操作完成,中断处理后边程序。

Q: 插一句,低位空间只能由用户态使用吗?内核态空间是独立的吗?

(问这么细)

都不是1. 用户态只能操作自己的3G空间,内核态是能操作4G的所有范围;2. 内核态自己的1G是所有进程共享的,指所有进程的内核态逻辑地址是共享同一块内存地址,记住,这是虚拟映射空间!!!这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。所以,这个地址空间,包括整个进程的虚拟地址空间,对于每个进程来说都是逻辑独立的,而且,每个进程看到的逻辑地址空间都是一样的,实际上,物理内存有共享部分和独立部分。

Q:再插一句,问题来了,不同进程 有相同的逻辑地址,但是却又不同的内容,这怎么实现呢?※

(啊啊啊,这势头,内存继续深挖…)

这就要靠 每个进程的的页表了,每个进程都有一个自己的页表,使得某逻辑地址对应于某个物理内存。

首先,我们要对进程的虚拟内存空间有清晰的认识:

  • 操作系统为每个进程分配的内存空间是一样的;如32位Linux操作系统的内存空间是0-4G。

  • 操作系统为每个进程分配的内存空间所提交的物理内存空间在用户的角度一般是不同的,在内核是相同的。

  • 用户可以通过操作系统将同一块物理内存映射到不同的进程空间。

正因为 每个进程都有一个自己的页表,使得相同的逻辑地址映射到 不同 或 存在相同 的物理内存。对于线程 ,它也有自己的页表,只是页表的 逻辑地址 映射到的物理内存相同。

Q: 大概讲下,虚拟内存映射过程?

(挖了个大坑。。。预感还问页面置换算法。。。)

  1. 进程要知道哪些内存地址上的数据在物理内存上,哪些不在,还有在物理内存上的哪里,需要用页表来记录

    页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)

    2.当进程访问某个虚拟地址,去看页表,如果发现对应的数据不在物理内存中,则缺页异常

缺页异常的处理过程,就是把进程需要的数据从磁盘上拷贝到物理内存中,如果内存已经满了,没有空地方了,那就找一个页覆盖,当然如果被覆盖的页曾经被修改过,需要将此页写回磁盘。这里用到一些页面置换算法

  1. 辅助映射的硬件:内存管理单元MMU,通常是 CPU 的一部分,本身有少量的存储空间用来存放从虚拟地址到物理地址的查找表;

  2. 三种内存管理方式:分别是分段、分页、段页

    分段

    分段管理下的虚拟地址由两部分组成,段号和段内偏移量

    图片

    1. 通过段号映射段表的项
    2. 从项中获取到段基地址
    3. 段基地址+段内偏移量=使用的物理内存

    有两个不足:

    一是容易产生内存碎片:没有足够一个段的空间映射就是内存碎片,解决方法是内存碎片整理,而内存碎片整理是通过内存交换的方式来实现,即内存部分数据加载硬盘,再读时,会紧挨另一个段形成连续物理内存。

    二是内存交换效率低:因为容易造成内存碎片,导致内存交换的频率较高,又因为因为硬盘的访问速度比内存慢太多了,把一大段连续的内存写入到硬盘,再又从硬盘读取出来,如果交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿,过程也很慢的,所以说分段方式内存交换效率低。

    为了解决内存分段管理造成的内存碎片与内存交换效率低的问题,就出现了内存分页。


分页方式是这样解决的,如果内存空间不够时,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页释放掉,也就是加载到硬盘,称为换出,一旦需要的时候再加载进来,称为换入。所以一次性写入硬盘的也只有一个页或几个页,内存的交换效率自然就提升了。

分页方式使加载程序的时候,不再需要一次性都把程序加载到物理内存中。完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去(用大白话说,当你需要用到的时候才会去使用对应的物理内存)。


简单分页:是每个进程都会分配一个页表,虚拟地址会分为两部分,页号和页内偏移量,页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,页内偏移量+物理内存基地址就组成了物理内存地址,如下图所示

图片

就是下面这几步

  1. 页号找到页表中的页项
  2. 获取页项的物理页号基地址
  3. 偏移量+物理页号基地址计算出物理内存地址

不足之处:

每个进程分配一个页表会有空间上的缺陷,因为操作系统上可以运行非常多的进程,那不就意味着页表数量非常多。

以32 位的环境为例,虚拟地址空间范围共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间范围的映射就要/有 4MB 的内存来存储页表。

4MB看起来不大,但是数量上来了就很恐怖了,假设 100 个进程的话,就需要 400MB 的内存来存储页表,这是非常大的内存了,更别说 64 位的环境了。

1
2
3
4
5
贴心提示:
1B(Byte 字节)=8bit,
1KB (Kilobyte 千字节)=1024B,
1MB (Megabyte 兆字节 简称“兆”)=1024KB,
1GB (Gigabyte 吉字节 又称“千兆”)=1024MB

为了解决空间上的问题,在对分页方式的基础上,进行优化,出现了多级页表方式

多级页表

在页表的基础上做一次二级分页,把刚刚提到的100万「页表项」分为一级页表「1024个页表项」,「一级页表项」下又关联二级页表「1024个页表项」,这样一级页表的1024个页表项就覆盖到了4GB的空间范围映射,并且二级页表按需加载,这样页表占用的空间就大大降低。

做个简单的计算,假设只有 20% 的一级页表项被用到了,那么页表占用的内存空间就只有 4KB(一级页表) + 20% * 4MB(二级页表)= 0.804MB,这对比单级页表的 4MB 就是一个巨大的节约。

image-20220222221928062

在二级的基础上是不是又可以继续分级呢,能分二级,必然也能分三级、四级,在64位操作系统是做了四级分页。

TBL快表 :一种CPU的高速缓存

多级页表虽然解决了空间上的问题,但是我们发现这种方式需要走多道转换才能找到映射的物理内存地址,经过的多道转换造成了时间上的开销。

程序是局部性的,即在一段时间内,整个程序的执行仅限于程序的某一部分。相应的,执行所访问的存储空间也局限于某个内存区域。

操作系统就利用这一特性,把最多使用的几个页表项放到TBL快表缓存, CPU 在寻址时,会先查 TLB快表,如果没找到,才会继续查常规的页表,TLB 的命中率其实很高的,因为程序最常访问的页就那么几个。

内存段页

段式与页式并不是相对的,他们也可以组合在一起使用,在段的基础上进行分页分级。

  1. 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制
  2. 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页

    虚拟地址结构由段号、段内页号和页内位移三部分组成

    图片

    就是下面这几步

    1. 通过段号获取段表的段项
    2. 通过段项获取到页表地址
    3. 通过页表地址找到段页表
    4. 通过段内页号找到段页表的段页项
    5. 通过段页项获取物理页基地址
    6. 通过物理页基地址+偏移量计算出物理内存地址

总结

多级分页通过树+懒加载+缓存解决了空间占用与时间消耗的问题,虚拟地址很好的做到了让进程与物理内存地址解耦,正因如此,多进程使用物理内存时才不会有冲突,很好的做到了相互独立与隔离。正因如此,多进程使用物理内存时才不会有冲突,很好的做到了相互独立与隔离。

Q:虚拟内存的好处?

Q:页面置换算法?

( 。。。果然)

当所需页不在内存,要将其调入,但如果内存没有空闲空间,为了保证进程所需的页能够调用,必须选择另外一些页调出。此时选择页面置换算法,算法的好坏直接影响系统性能,不适当的算法可能会产生系统“抖动”。

内存进程过多,缺页越拼单,有效访问存储器的时间急速减少,换句话,就是大部分时间用于页面的换进/换出,而几乎不能完成任何有效的工作,这就是系统的“抖动状态”。

  1. 最佳置换算法

    理论算法,选择不再使用或最长时间内不再使用的页淘汰

  2. 先进先出置换算法

    选择先进的页淘汰,与进程实际运行规律不符,缺页率高,抖动高

  3. 最近最久未使用置换算法LRU

    作用如名字所说,比较符合业务。

  4. 时钟置换算法

    所有页面用指针链接成一个循环队列。当某页被访问时,其访问位置为1,当需要淘汰一个页面时,只需检查页的访问位。如果是0,就选择该页换出;如果是1,则将它置为0,暂不换出,继续检查下一个页面,若第一轮扫描中所有页面都是1,则将这些页面的访问位依次置为0后,再进行第二轮扫描。

Q:问简单的,进程和线程的区别?

进程是最小的资源分配单位,线程是最小CPU调度单位

进程:

P C B是 进程 存在的唯一标识,这意味一个 进程 一定会有对应的PCB,进程消失,P C B也会随之消失。

P C B通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列;

  • 将所有处于就绪状态的 进程 链在一起,称为就绪队列
  • 把所有因等待某事件而处于等待状态的 进程 链在一起就组成各种阻塞队列

CPU把一个进程切换到另一个进程运行的过程,称为进程上下文切换;一个进程的上下文切换,同时可能影响其他CPU核心上的进程的执行效率。

Q:插一句, 什么是CPU上下文切换?

C P U上下文 是指 C P U 寄存器 和 程序计数器

  • C P U 寄存器 是 C P U 内置的容量小,速度极快的缓存
  • 程序计数器是用来存储 是 CPU 正在执行的指令位置或即将执行的下一条指令位置

上下文切换就是把前一个任务的CPU上下文保存起来,然后加载新任务的上下文到这些指令寄存器(IR)和程序寄存器(PC)等寄存器中。这些被保存下来的上下文会存储在操作系统的内核中,等待任务重新调度执行时再次加载进来,这样就能保证任务的原来状态不受影响,让任务看起来是连续运行的。

根据场景不同,CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换。

进程是由内核管理与调度的,所以 进程上下文切换 发生在内核态,进程上下文切换的内容包含用户空间资源(虚拟内存、栈、全局变量等)与内核空间资源(内核堆栈、寄存器等)

什么时候会发生进程调度?

  • 进程的CPU时间片耗尽,被系统挂起,切换到其他等待CPU的进程运行。
  • 进程所需要的系统资源不足。要等待资源满足后才可以运行。这个时候会被系统挂起。
  • 进程通过sleep函数主动将自己挂起。
  • 当有优先级更高的进程运行时,当前进程会被挂起,由高优先级的进程运行。
  • 硬中断发生时,CPU上的进程会被挂起,转而执行内核的中断服务程序。

***

特别需要注意的是操作系统会将当前任务的虚拟内存一并保存。而Linux中通过TLB快表来管理虚拟内存到物理内存的映射关系。TLB用于虚拟地址与实地址之间的交互,提供一个寻找实地址的缓存区,能够有效减少寻找物理地址所消耗时间。当虚拟内存被刷新后,TLB也会被更新。如果没有TLB,则每次取数据都需要两次访问内存,即查页表获得物理地址和取数据。在多核的技术下,这会极大的降低程序的执行效率。因为缓存L3 Cache 是被几个核共享的。当TLB被更新后,缓存中的TLB数据会失效,每个CPU都需要从主存中重新载入,一个进程的上下文切换,同时为保证缓存一致性,可能影响其他CPU核心上的进程的执行效率。

线程:

一个进程下面能有一个或多个线程,每个线程都有独立一套的寄存器和栈,这样可以确保线程的控制流是相对独立的。

线程带来的好处有以下几点

  • 一个进程中可以同时存在多个线程
  • 让进程具备多任务并行处理能力
  • 同进程下的各个线程之间可以共享进程资源 (同进程内的多线程通信十分简单高效)
  • 更轻量与高效

线程带来的坏处有以下几点

  • 因为进程资源共享,所以会产生资源竞争,需要通过锁机制来协同
  • 当进程中的一个线程奔溃时,会导致其所属进程的所有线程奔溃(一般游戏的用户设计不会采用多线程方式)

线程与进程的对比

  • 进程是最小的资源(包括内存、打开的文件等)分配单位,线程是最小的运行单位
  • 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈
  • 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系(和进程大同小异)
  • 线程的创建、终止时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,所以线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们(线程管理的资源较少)
  • 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的,一个是快表失效,二是重新载入几个核,三是为保证缓存一致性,影响其他CPU核心上的进程
  • 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了

线程比进程不管是时间效率,还是空间效率都要高。

Q:刚聊到进程共享,进程之间的通信有哪些?

一般来说,每个进程都是独立的,操作系统为每个进程之间提供了「隔离」。所以进程之间不能直接互相访问,但可以借助它们共享的「内核空间」来实现通信。

1.管道

管道是用环形队列实现的,数据从写端流入,从读端流出,这样就实现了进程间通信。一个管道只能单向传输数据,进程之间的双向传输则需要创建两个管道。比如管道符 。

2.消息队列

消息队列是存储在内核中的消息链表,遵循队列的先进先出原则。

消息块遵循进程双方自定义的数据类型,有固定大小。

消息队列不适合传输较大的数据,因为每个消息块的大小有限制。

消息队列存在于内核中,所以进程的读写消息需要在用户态与内核态频繁切换,系统开销较大。

3.共享内存+信号量

mmap是一种内存映射文件的方法。利用mmap把普通文件映射到进程地址空间后,进程可以向访问内存的方式对文件进行访问。

信号量有效解决了进程间竞争共享资源而导致的数据不同步,用于实现进程的访问共享资源的互斥与同步。

4. 信号

信号可以理解为给进程发送一个命令

5.Socket

实现跨越网络的进程通信

Q:线程通信有哪些?

1 锁与同步 包括互斥锁、条件变量、读写锁

2 等待/通知机制 基于Object类的wait()方法和notify(), notifyAll()方法来实现的。

3 信号量 Semaphore

4 管道流 PipedOutputStreamPipedInputStream

你的代码如何保证原子性?

一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

  • synchronized同步代码块
  • cas原子类工具
  • lock锁机制

sychronized 和 lock区别

sychronized有两种方法,一种是作用在方法,另一种作用在代码块;
如进入临界区前,先判断ACC_SYCHRONIZED标记,有才可以进入moniter对象之后,执行moniterenter指令,把当前的计数器+1,然后开始执行方法,当结束时,先释放moniter对象,再执行moniterexit让计数器-1,然后结束方法。
出现异常,也能够自动释放,但有个问题,它无法中断。
而Lock,可以调用方法中断,当锁竞争激烈,性能较好。

讲讲代理模式

当client 使用RealObj时,先创建proxy代理类,在使用时抽取出RealObj接口,叫做抽象主题类,原来RealObj叫做主题实现类。那么,代理类场景,比如1. I/O处理图片文件,设置虚拟代理,当真正需要使用才会创建出来; 2.设置访问权限,设置保护代理,实现防火墙网关;3.网络访问,grpc/dubbo远程调用,使用远程代理对方式;4. 日志记录代理,AOP实现; 5. 缓存数据,需要缓存代理,AOP实现;6. 事务创建到提交代理过程代理;
一种是静态代理,自己new对象作为代理对象,另一种是动态代理,JDK反射,实现Handler,调用newProxyInstance;Cglib操作asm框架在字节码层实现 spring AOP有实现接口就用JDK,无则Cglib。

TCP和UDP区别

TCP 强调传输字节流,连接可靠;
UDP 强调报文包的发送,连接不可靠;
UDP包含源端口,目的端口,数据的长度,为了防止数据出错的校验和,发送的数据
TCP包包含以上五项,还增加了序号,确保发送的顺序,确认序号几个字段,防止丢包,还有一些状态位,建立连接时的syn,确认响应时的ack,端位连接的fin,还增加窗口大小,用来流量和拥塞控制;

TCP传输过程,建立连接三次握手,传输数据,释放连接四次挥手;