Java
2020-08-07 12:58:50 30 举报
AI智能生成
java 学习框架
作者其他创作
大纲/内容
Java
大数据
HDFS
HDFS的思想
hdfs是通过分布式集群来存储文件,同时为客户端提供便捷的访问方式,一个虚拟的访问目录结构
文件存储到hdfs集群中去的时候是被切分成block的
文件的block存放在若干台datanode节点上
HDFS文件系统的文件与真实的block之间有映射关系,由namenode管理
每一个block在集群中会存储多个副本,好处是可以提高数据的可靠性,还可以提高访问的吞吐量
构成
NameNode
职责
维护元数据信息
维护hdfs的目录树(虚拟目录)
响应客户端的请求
DataNode
Secondary NameNode
上传文件过程
1 客户端上传文件时,NN首先往edits log文件中记录元数据操作日志
2 客户端开始上传文件,完成后返回成功信息给NN,NN就在内存中写入这次上传操作的新产生的元数据信息
3 每当editslog写满时,需要将这一段时间的新的元数据刷到fsimage文件中去
1 NN通知SN进行checkpoint操作
2 停止editslog文件写操作,接下来的操作写在edits.new文件中
3 SN将NN中的fsimage和editslog下载
4 进行合并成 fsimage.chkpoint
5 上传给NN,NN将fsimage.chkpoint和edits.new改名
Hadoop的RPC实现
1 生成调用端socket程序动态代理对象,代理对象要实现抽象方法
2 通过proxy调用业务方法
3 调用socket的抽象方法
4 发送调用请求
5 生成实现接口的动态代理对象
6 调用业务代理对象的具体业务方法
7 获取调用结果
8 返回调用结果
Yarn(资源调度)
yarn执行流程
1 执行jar包,jar包(代码:job.waitForCompletion())执行RunJar进程,进程向 resourcemanager申请一个job
2 resourcemanager返回job相关资源提交的路径(staging-dir)和为本job产生的jobID
3 Runjar 提交资源到对应的staging-dir中
4 Runjar向resourcemanager汇报提交结果
5 resourcemanager将本job加入任务队列
6 node manager 向resourcemanager领取任务
7 nodemanager分配运行资源容器
8 resourcemanager选取一台nodemanager启动mrappmaster
9 mrappmaster操作其他nodemanager启动yarn-child,执行map task和reduce task
框架
Spring
aop
功能
让关注点代码与业务代码分离
关注点
重复代码就叫做关注点
切面
关注点形成的类,就叫切面
切入点
执行目标对象方法,动态植入切面代码
可以通过切入点表达式,指定拦截哪些类的哪些方法,给指定的类在运行的时候植入切面类代码
bean
bean的作用域
singleton
prototype
request
session
globalSession
bean的实例化流程
实例化bean对象
设置对象属性
检查Aware相关接口并设置相关依赖
BeanPostProcessor前置处理
检查是否是InitializingBean以决定是否调用afterPropertiesSet方法
检查是否配置有自定义的init-method
BeanPostProcessor后置处理
注册必要的Destruction相关回调接口
使用
是否实现DisposableBean接口
是否配置有自定义的destroy方法
bean的生命周期
创建(调用构造函数)
set方法注入属性
BeanNameAware
BeanFactoryAware
ApplicationContextAware
BeanPostProcessor的before方法
InitalizingBean
自定义init方法
BeanPostProcessor的after方法
容器的销毁
DisposableBean的destroy
自定义的销毁方法
Mybatis
运行过程
读取配置文件的Configuration对象,用于创建SqlSessionFactory
构建SqlSessionFactory
缓存
一级缓存
基于sqlSession
二级缓存
基于mapper文件的namespace,多个sqlSession可以共享一个mapper中的二级缓存区域
如果多个mapper的namespace相同,即使是多个mapper,那么这几个mapper中执行sql查询到的数据也将在相同的二级缓存区域
项目
主从不一致扫描
问题
交易那有个集群有部分key主从不一致,key数量非常大,主从结果复杂
开发考虑
key数量大,采用多线程
forkjoinpool
对ops要求高,多参数限制
scanMaster
避免对线上的影响,不扫主
count
一次取出来的key的数量
sleepTime
扫描完每个槽位后的等待时间
threadSleepTime
每个线程的执行等待时间
主从同步延迟
waitSyncTime
等待同步时间,如果扫描两个实例后,有不同的key,先等一会,等待同步,再次扫描确认
内存泄漏
多个线程,每个线程保持一组连接,需要全部关掉
每个线程通过threadlocal保存一组连接,保证连接重复使用
jedis操作redis的时候,对底层执行redis命令做了缓存,所以如果某一次redis操作出现异常,jedis实例中的缓存数据不会被清空,而直接放回连接池中。下一次从池中取出了同一个jedis对象,发送的命令用的还是上一个线程的数据
暂停
failover
sentinel检测实例,有半数以上报告故障,持续一段时间
将故障实例放入故障池,通过cm补缺失实例
将新拓扑结构更新到cfs
客户端 拉取配置信息
日常问题
某个指令ops远低于预期
先质疑,问怎么埋点
查看指令,有些特殊的指令,例如synIncr等同步指令,效率低
是否是大key的问题
是否存在跨机房访问
moved问题
检查该实例是否存在执行中断的迁移任务
消息队列
为什么要使用消息队列
通过异步处理提高系统性能(削峰、减少响应所需时间)
在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善
因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败。因此使用消息队列进行异步处理之后,需要适当修改业务流程进行配合,比如用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。
降低系统耦合度
消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来
为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息
使用消息队列带来的问题
系统可用性降低
系统可用性在某种程度上降低了,在加入MQ之前,不用考虑消息丢失或者MQ挂掉的情况
系统复杂性提高
加入MQ之后,需要保证消息没有被重复消费、处理消息丢失的情况和暴增消息传递的顺序性
一致性问题
由于消息队列实现异步,会导致消息的真正消费者并没有正确消费消息
数据库
索引
什么是索引
是一种快速查询表中内容的机制
运用在表中某些字段上,存储时,独立于表之外
为什么使用索引
索引可以加快数据库的检索速度
为什么索引可以加快查询速度
将无序的数据变成有序
为什么增删改会降低速度
索引的页是B+树实现的,即平衡树的一种
对树进行增删改会破坏原有结构,为了维持平衡树,就必须做额外的工作
什么情况下使用索引
表经常进行查询操作要建立索引
表很大,记录内容分布范围很广
列名经常在WHERE子句或连接条件中出现
表经常进行 INSERT/UPDATA/DELETE操作不要建立索引,索引会较低插入、删除、修改的速度
表较小的时候,不建立索引,索引会占物理和数据空间
索引的最左匹配原则
对于单索引来说,索引是有序排列的数据结构
对于复合索引来说,索引对最左边的数据进行排序,然后再第一个字段的基础上,对第二个字段进行排序
也就是为了使用第二个索引,应先使用第一个索引,且第一个索引是等值匹配
聚集索引和非聚集索引
聚集索引是以主键创建的索引,聚集索引的物理存储位置与索引的逻辑顺序有关,一个表中只能有一个聚集索引
非聚集索引是以非主键创建的索引
一个表中可以有多个非聚集索引
二者区别
聚集索引在叶子结点存储的是表中的数据
非聚集索引在叶子节点存储的是主键和索引列
使用非聚集索引查询出数据时,拿到叶子上的主键再去查询想要的数据(回表)
建立索引的原则
最左匹配原则
MySql会一直向右匹配直到遇到范围查询
选择区分度高的列作为索引
尽可能的扩展索引,不要新建立索引
单个多列组合索引和多个单列索引的检索查询效果不同
MySql在执行SQL时,只能使用一个索引,会从多个单列索引中选择一个限制最为严格的索引
索引失效
对索引进行运算
单独使用复合索引非第一位置的索引
字符型索引为数字时在where条件里不添加引号
where子句中有 !=判断
where子句中有null值
为什么索引用B+树而不是B树
B+树的数据都集中在叶子节点。分支节点只负责索引。B树的分支节点也有数据,B+树的层高会小于B树
B+树更擅长范围查询,叶子节点用顺序访问指针实现。B+树范围查询只能中序遍历
索引节点没有数据,比较小
为什么索引不用红黑树
数据多的话树太长太高
锁
表锁
开销小,加锁快
不会出现死锁
锁定粒度大,发生锁冲突概率高,并发度最低
行锁
开销大,加锁慢
会出现死锁
锁定粒度小,发生锁冲突的概率低,并发度低
共享锁(读锁)
允许一个事务去读一行,阻止其他事务获得相同的排他锁
多个客户可以同时读取同一个资源,但不允许其他客户修改
排他锁(写锁)
允许获得排他锁的事务更新数据,组织其他事务取得相同数据集的共享读锁和排他写锁
写锁是排他的,写锁会阻塞其他的写锁和读锁
间隙锁GAP
间隙锁只在Repeatable read隔离级别下使用
在用范围条件检索数据并请求共享或排他锁,对于键值在条件范围内但并不存在的记录,也会加锁
作用:
防止幻读
满足恢复和复制的需要
在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录
死锁
避免死锁的方式
以固定的顺序访问表和行
大事务拆小
在同一事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率
降低隔离级别
为表添加合理的索引
不走索引将会为表的每一行记录添加上锁,死锁的概率大大增加
丢失更新
一个事务的更新覆盖了其他事务的更新结果
解决方法
使用Serializable隔离级别,事务是串行执行的
乐观锁
表中有一个版本字段,第一次读的时候,获取到这个字段。当更新时,再次查看该字段的值是否和第一次一样,如果一样更新,反之拒绝
实现方式
加version版本字段
悲观锁
悲观锁是数据库层面的加锁,每次操作都会阻塞去等待锁
手动加行锁
例如:select * from xx for update
乐观锁和悲观锁的应用场景
乐观锁使用于多读场景(写比较少),即冲突很少发生的时候
悲观锁使用于多写场景,容易发生冲突的场景
for update
使用到索引
有数据
无数据
不加锁
没有使用到索引
事务
什么是事务
满足ACID
事务特性
原子性
一致性
隔离性
持久性
事务隔离级别
Serializable
可避免脏读,不可重复读,虚读
Repeatable read
可避免脏读,不可重复读
Read committed
可避免脏读
Read uncommitted
级别最低,什么都避免不了
脏读、不可重复读、虚读(幻读)
脏读
脏读就是指当事务A对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务B也访问这个数据,然后使用了这个数据。
不可重复读
不可重复读是指在事务1内,读取了一个数据,事务1还没有结束时,事务2也访问了这个数据,修改了这个数据,并提交。紧接着,事务1又读这个数据。由于事务2的修改,那么事务1两次读到的的数据可能是不一样的,因此称为是不可重复读。
虚读
所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。InnoDB存储引擎通过多版本并发控制(MVCC)解决了幻读的问题。
存储过程
优点
能够将代码封装起来
保存在数据库之中
让编程语言进行调用
存储过程是一个预编译的代码块,并行效率比较高
一个存储过程替代大量T_SQL语句,可以降低网络通信量,提高通信速率
缺点
每个数据库的存储语法几乎都不一样,十分难以维护
业务逻辑放在数据库上,难以迭代
三个范式
第一范式
数据库表中的字段都是单一属性的,不可再分
第二范式
满足第一范式,表中的字段必须完全依赖于全部主键而非部分主键
第三范式
满足第二范式,非主键外的所有字段必须互不依赖
视图
什么是视图
视图是一种虚表
视图建立在已有表的基础上,视图赖以建立的这些表称为基表
向视图提供数据内容的语句为SELECT语句,可以将视图理解为存储起来的SELECT语句
视图向用户提供基表数据的另一种表现形式
视图没有存储真正的数据,真正的数据还是存储在基表中
程序员虽然操作的是视图,但最终视图还会转成操作基表
一个基表可以有0个或多个视图
drop、delete和truncate分别在什么场景之下使用
drop
不可回滚
不可带where
表内容和结构删除
删除速度快
truncate
表内容删除
delete
可回滚
可带where
表结构在,表内容要看where执行的情况
删除速度慢,需要逐条删除
使用场景
不在需要一张表的时候,用drop
想删除部分数据行的时候,用delete,并且带上where语句
保留表而删除所有数据的时候用truncate
mysql中in和exists区别
in语句把外表和内表hash连接,exists语句对外表作loop循环,每次loop循环再对内表进行查询
如果查询的两个表大小相当,那么in和exists差别不大; 如果两个表中一个较小,一个较大,则子查询表大的用exists,子查询表小的用in;
not in 和not exists如果查询语句使用了not in 那么内外表都进行全表扫描,没有用到索引;而not exists 的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。
EXISTS只返回TRUE或FALSE,不会返回UNKNOWN。IN当遇到包含NULL的情况,那么就会返回UNKNOWN。
SQL约束有哪几种
NOT NULL
用于控制字段的内容一定不能为空
UNIQUE
控制字段内容不能重复,一个表允许有多个Unique约束
PRIMARY KEY
用于控制字段内容不能重复,一个表只允许出现一个
FOREIGN KEY
用于预防破环表之间连接的动作,也能防止非法数据插入外键列
CHECK
用于控制字段的值范围
数据库优化的思路
选取最有效率的表名顺序
数据库的解析器按照从右到左的顺序处理FROM子句中的表名,FROM子句中写在最后的表将被最先处理
如果三个表是完全 无关系的话,将记录和列名最少的表,写在最后,即选择记录条数最少的表放在最后
如果三个表是有关系的话,将引用最多的表,放在最后,即被其他表所引用的表放在最后
WHERE子句中的连接顺序
数据库采用自右向左的顺序解析WHERE子句,根据这个原理,表之间的连接必须写在其他WHERE条件之左,那些可以过滤掉最大数量记录的条件必须写在WHERE子句之右
SELECT子句中避免使用*号
*号可以获取表中全部的字段数据,但它是通过查询数据字典完成的,这意味着将耗费更多的时间
使用*号写出来的SQL语句也不够直观
使用TRUNCATE替代DELETE
对于删除表的全部记录,DELETE是逐条删除,Truncate是将整个表删除
多使用内部函数提高SQL效率
例如 : 使用mysql的concat()函数会比使用||来进行拼接快,因为concat()函数已经被sql优化过了
使用表或列的别名
一些简短的别名也能稍微提高一些SQL的性能
善用索引
索引就是为了提高我们的查询数据的速度的,当表的记录量非常大时,我们就可以使用索引了
SQL写大写
Oracle服务器总是先将小写字母转成大写后,才执行
避免在索引上使用NOT
Oracle服务器在遇到NOT后,他就会停止目前的工作,转而执行全表扫描
避免在索引列上使用计算
WHERE子句中,如果索引列是函数的一部分,优化器将不使用索引而使用全表扫描,这样会变慢
使用>=替代>
使用>会先定位到>的记录,然后扫描第一个大于的记录。>=会直接跳到第一个=的记录
使用IN替代OR
总是使用索引的第一个列
如果索引是建立在多个列上,只有在它的第一个列别WHERE子句引用时,优化器才会选择使用该索引
优化答题
SQL优化的一般步骤是什么,怎么查看执行计划
查看慢日志(show [session|gobal] status ),定位慢查询,查看慢查询执行计划 根据执行计划确认优化方案
MyISAM和InnoDB
区别
count运算上的区别
因为MyISAM缓存有表meta-data(行数等),因此在做COUNT(*)时对于一个结构很好的查询是不需要消耗多少资源的。而对于InnoDB来说,则没有这种缓存
是否支持事务和崩溃后的安全恢复
是否支持外键
MyISAM不支持,而InnoDB支持
MyISAM更适合读密集的表,而InnoDB更适合写密集的的表。 在数据库做主从分离的情况下,经常选择MyISAM作为主库的存储引擎。一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM的表锁的粒度太大,所以当该表写并发量较高时,要等待的查询就会很多了),InnoDB是不错的选择。如果你的数据量很大(MyISAM支持压缩特性可以减少磁盘的空间占用),而且不需要支持事务时,MyISAM是最好的选择
MVCC多版本并发控制
通过保存数据在某个时间点的快照来实现的。MVCC主要作用于事务性的,有行锁控制的数据库模型
InnoDB存储引擎MVCC实现策略
版本链
额外存储每次对某条聚簇索引记录进行修改的事务id和指向这条聚簇索引上一个版本的位置,上一版本的数据会保存在Undo日志中
ReadView
ReadView中有个列表存储系统中未提交的事务,通过这个列表来判断记录的某个版本是否对当前事务可见
当进行查询时,如果该行数据的版本号在列表记录前,就可以访问,在列表记录中和后,就不能访问
explain
通过explain命令可以知道:表的读取顺序、数据读取操作的类型、哪些索引可以使用、哪些索引实际使用了、表之间的引用、每张表有多少行被优化器查询等信息
经典面试题
计算机网络
五层协议的体系结构
物理层
数据链路层
两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议
网络层
网络层负责为分组交换网上的不同主机提供通信服务。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送
运输层(transport layer)
运输层的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文
传输层协议
传输控制协议 TCP(Transmisson Control Protocol)
提供面向连接的,可靠的数据传输服务
TCP的主要特点
TCP是面向连接的
每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的(一对一)
TCP提供可靠支付的服务,通过TCP连接传送的数据,无差错、不丢失、不重复、并且按序到达
TCP提供全双工通信。TCP允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双方通信的数据
面向字节流。TCP中的“流”(stream)指的是流入进程或从进程流出的字节序列。“面向字节流”的含义是:虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。
用户数据协议 UDP(User Datagram Protocol)
提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)
UDP的主要特点
UDP是无连接的
UDP使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的链接状态
UDP是面向报文的
UDP没有阻塞控制,因此网络出现阻塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
UDP支持一对一、一对多、多对一和多对多的交互通信
UDP的首部开销小,只有8个字节,比TCP的20个字节的首部要短
为什么UDP有时比TCP更有优势
网速的提升给UDP的稳定性提供可靠网络保障,丢包率很低,如果使用应用层重传,能够确保传输的可靠性
TCP为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程
TCP一旦发生丢包,TCP会将后续的包缓存起来,等前面的包重传并接收后再继续发送,延时会越来越大;UDP采用自定义重传机制,能够把丢包产生的延迟降到最低
应用层(application layer)
应用层的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程间的通信和交互的规则。对于不同的网络需要不同的应用层协议
应用层协议
域名系统DNS(Domain Name System)
域名系统是因特网的一项核心服务,它作为可以将域名和IP地址相互映射的一个分布式数据库,能够使人更方便的访问互联网
超文本传输协议HTTP(HyperText Transfer Protocol)
超文本传输协议是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准
应用层交互的数据单元称为报文
TCP/IP的体系结构
网络接口层
网际层 IP
运输层 (TCP或UDP)
应用层(各种应用层协议,如TELNET,FTP,SMTP等)
TCP三次握手和四次分手
三次握手
四次分手
第二次:服务器发回ACK=1,ack=x+1,表明自己收到客户端关闭连接的请求,但还没准备好关闭,进入CLOST_WAIT状态,客户端收到这个确认包后,进入FIN+WAIT_2状态
第四次:客户端接收到服务端的关闭请求,发回ACK=1,ack=y+1报文确认,进入TIME_WAIT状态,等待可能的重传ACK包,服务端接收到这个确认包后,关闭连接,进入CLOSED状态,客户端等待两个最大段生命周期,进入CLOSED状态
三次握手的原因
第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接
第三次握手失败怎么办
直接发送RTS报文段,进入CLOSED状态,这样做的目的是为了防止SYN泛洪攻击
四次挥手的原因
为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文
TCP报文首部
源端口和目的端口
各占2个字节,分别写入源端口和目的端口
序号seq
占4个字节,TCP连接中传送的字节流中的每个字节都按顺序编号
确认号ack
占4个字节,是期望收到对方下一个报文的第一个数据字节的序号
数据偏移
占4位,它指出TCP报文的数据距离TCP报文段的起始处有多远
保留
占6位,保留今后使用
紧急URG
确认ACK
仅当ACK=1时,确认号字段才有效。TCP规定,在连接建立后所有报文的传输都必须把ACK置1
推送PSH
当两个应用进程进行交互式通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应,这时候就PSH=1
复位RST
当RST=1,表明TCP连接中出现严重差错,必须释放连接,然后再重新建立连接
同步SYN
终止FIN
用来释放连接。当FIN=1,表明此报文的发送方的数据已经发送完毕,并且要求释放
窗口
占2个字节,指的是通知接收方,发送本报文你需要有多大的空间来接受
检验和
占2个字节,校验首部和数据两部分
紧急指针
占2个字节,指出本报文段中的紧急数据的字节数
选项
长度可变,定义一些其他的可选的参数
IP数据报首部
版本
IPV4 和 IPV6两个版本
首部长度
区分服务
总长度
包括首部长度和数据部分长度
生存时间
防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位,当TTL为0时就丢弃
协议
指出携带的数据应该上交给哪个协议进行处理
首部检验和
因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包括数据部分可以减少计算的工作量
源地址
目的地址
标识
在数据报长度过长而发生分片的情况下,相同数据报的分片具有相同的标识符
片偏移
和标识符一起,用于发生分片的情况。片偏移的单位为8字节
TCP拥塞控制机制
原因
我们知道TCP通过一个定时器(timer)采样了RTT并计算RTO,但是,如果网络上的延时突然增加,那么,TCP对这个事做出的应对只有重传数据,然而重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这就导致了恶性循环,最终形成“网络风暴” —— TCP的拥塞控制机制就是用于应对这种情况
在某段时间,若对网络中某一个资源的需求超过了该资源所能提供的可用部分,网络的性能就会变坏,就是拥塞
拥塞窗口
在发送数据时,将拥塞窗口的大小与接收端ack的窗口的大小作比较,取较小者作为发送数据量的上限
拥塞控制算法
慢启动
刚刚加入网络的连接,一点一点的提速,不要一上来就把路占满,指数上升
拥塞避免
当拥塞窗口达到一个阈值时,窗口不再呈指数上升,而是以线性上升,避免增长过快导致网络拥塞
快重传
接收方在收到一个失序的报文段后就立即发出重复确认,而不要等到自己发送数据时捎带确认。发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必等待设置的重传时间
快恢复
慢开始只在TCP连接建立时和网络出现超时时才使用
当发送方连续收到三个重复确认时,就执行“乘法减小”算法,吧门限减半,但是不执行慢开始算法
拥塞控制与流量控制的区别
拥塞控制是防止过多的数据注入到网络中,可以使网络中的路由器或链路不致过载,是一个全局性的过程
流量控制是点对点通信量的控制,是一个端到端的问题,主要就是抑制发送端发送数据的速率,以便接收端来得及接收
滑动窗口
TCP滑动窗口技术通过动态改变窗口大小来调节两台主机间数据传输
状态码
1XX
信息性状态码
接收的请求正在处理
2XX
成功状态码
请求正常处理完毕
3XX
重定向状态码
需要进行附加操作以完成请求
301 永久性重定向
302 零时性重定向
4XX
客户端错误状态码
服务器无法处理请求
400 请求报文中存在语法错误
401 发送的请求需要有认证信息,若果之前已经进行过一次请求则标识用户认证失败
403 请求被拒绝
5XX
服务器错误状态码
服务器处理请求出错
session和cookie
cookie
cookie是由服务器发送给客户端的小量信息,以{key:value}的形式存在
原理
客户端请求服务器时,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。而客户端浏览器会把Cookie保存起来。当浏览器再请求 服务器时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器通过检查该Cookie来获取用户状态
分类
会话期cookie
浏览器关闭之后会被自动删除,也就是仅在会话期内有效
持久性cookie
指定一个特定的过期时间或有效期之后就成为了持久性的cookie
session以服务端保存状态
服务端返回的响应报文的set-cookie首部字段包含这个sessionID,客户端将该Cookie值存入浏览器中
客户端再次请求时会包含该cookie,服务器收到之后提取出sessionID,取出用户信息
使用区别
登录信息等重要信息保存在session里
其他信息保存在cookie里
GET和POST的区别
对于RESTful规范来说,get用来获取数据,post用来提交数据
get是幂等的,post是非幂等的
post更安全,不会被缓存、保存在服务器日志以及浏览器记录中
get要快于post
HTTP1.0和2.0的区别
HTTP1.1
缓存处理
header里引入更多可供选择的缓存头来控制缓存策略
带宽优化及网络连接的使用
断点续传,请求头引入range头域,允许只请求资源的某个部分
错误通知的管理
新增24个错误状态响应码
Host头处理
支持虚拟主机
长连接
HTTP2.0
新的二进制格式
HTTP1.X解析是基于文本,HTTP2.0的协议解析采用二进制格式,方便且健壮
多路复用
连接共享,一个request对应一个id,一个连接上可以有多个request,每个连接的request可以随机的混杂在一起
header压缩
通讯双方各自cache一份header fields,避免重复header的传输,HTTP2.0 也使用哈夫曼编码对首部字段进行压缩
服务端推送
浏览器请求index.html,服务器把相应的style.css,example.png全部发送给浏览器,减少HTTP通信轮数
转发与重定向区别
转发在服务器端完成,重定向在客户端完成
转发速度快,重定速度慢
转发的是同一次请求,重定向是两次不同请求
转发地址栏没有变化,重定向地址栏有变化
TCP和UDP的特点
UDP是无连接的,尽最大可能交付,没有拥塞控制,面向报文,支持一对一,一对多,多对一,多对多的交互通信
TCP是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流,每一条TCP连接只能是一对一的
TCP和UDP的区别
TCP是面向连接的,UDP是无连接的
TCP提供可靠的服务,即通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
UDP具有较好的实时性,工作效率比TCP高,适用于高速传输和对实时性有较高的要求的通信或广播通信
每一条TCP连接只能是点到点的,UDP支持一对一,一对多,多对一,多对多的交互通信
TCP对系统资源要求较多,UDP对系统资源要求较少
使用代理服务器的原因
缓存一些公有资源
负载均衡
网络访问控制
访问日志记录
HTTPS
HTTP + 加密 + 身份认证 + 完整性保护
DNS
浏览器根据访问的域名找到其IP地址
DNS查找过程
浏览器缓存:浏览器会缓存DNS记录一段时间
系统缓存:浏览器会做一个系统调用,这样便可获得系统缓存中的记录
路由器缓存
向路由器发送查询请求,路由器一般会有自己的DNS缓存
ISP DNS缓存
请求缓存DNS的服务器
一个网页从开始请求到最终显示的完整过程
在浏览器中输入网址
发送至DNS服务器并获得域名对应的WEB服务器的IP地址
与WEB服务器通过三次握手建立TCP连接
浏览器向WEB服务器的IP地址发送相应的HTTP请求
WEB服务器响应请求并返回指定URL的数据,或错误信息,如果设定重定向,则重定向到新的URL地址
浏览器下载数据后解析HTML源文件,解析的过程中实现对页面的排版,解析完成后再浏览器中显示基础页面
分析页面中的超链接并显示在当前页面,重复以上过程直至无超链接需要发送,完成全部显示
操作系统
多线程
synchronized
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行
synchronized使用方式
修饰实例方法
被修饰的方法称为同步方法,作用的范围是整个方法,作用的对象是调用这个方法的对象
修饰静态方法
作用的范围是整个静态方法,作用的对象是这个类的所有对象
修饰代码块
被修饰的代码块称为同步代码块,作用的范围是大括号{}内的代码,作用的对象是调用这个代码块的对象
修饰类
作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象
sychronized修饰不加static的方法,锁是加在单个对象上,不同的对象没有竞争关系;修饰加了static的方法,锁是加在类上,这个类所有的对象竞争一把锁
sychronized缺陷
获取锁的线程由于等待IO或者sleep等被阻塞了,但是没有释放锁,影响其他线程执行
当一个线程在进行读操作时,其他线程只能等待无法进行读操作
volatile
作用
保证变量的可见性
防止指令重排序
详解
volatile关键字
volatile的原理和实现机制
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令
lock前缀指令实际上相当于一个内存屏障
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
它会强制将对缓存的修改操作立即写入主存
如果是写操作,它会导致其他CPU中对应的缓存行无效
synchronized关键字与volatile关键字的区别
volatile关键字是线程同步的轻量级实现,所以性能比synchronized关键字要好
volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证
volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性
synchronized与Lock的区别
两者都是可重入锁
可重入锁指自己可以再次获取自己的内部锁
synchronized依赖于JVM,reenTrantLock依赖于API
reenTrantLock比synchronized增加了一些高级功能
新功能
等待可中断
ReenTrantLock可以通过 lock.lockInterruptibly()将正在等待的线程放弃,转而处理其他事情
可实现公平锁
synchronized只能是非公平锁
ReenTrantLock默认是非公平的,可以通过 ReenTrantLock(boolean fair) 构造方法来指定是否是公平的
可实现选择性通知(锁可以绑定多个条件)
synchronized关键字与 wait()和notify/notifyAll()方法结合可以实现等待/通知机制,但执行notifyAll()方法会通知所有处于等待状态的线程,从而造成很大的效率问题
ReenTrantLock借助于Condition接口和newCondition()方法,线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活
synchronized不需要用户手动释放锁,系统会把执行完的synchronized方法或者代码块的锁释放,而lock则必须用户手动释放锁
实现Runnable接口和Callable接口的区别
Callable规定的方法是call(),Runnable规定的方法是run()
Runnable接口不会返回结果但是Callable接口可以返回结果
call()方法可抛出异常,run()方法不能抛出异常
执行execute()方法和submit()方法的区别
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
线程池
线程池提供了一种限制和管理资源。每个线程池还维护一些基本信息,例如已完成任务的数量
线程池的好处
降低资源消耗
通过重复利用已创建的线程降低线程创建和销毁造成的消耗
提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行
提高线程的可管理性
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
如何创建线程池
ThreadPoolExecutor类的四个构造方法
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行
newSingleThreadPool
弊端
newFixed 和newSingle 堆积的请求处理队列可能会耗费非常大的内存
newCached和newScheduled 线程数最大数是 Integer.MAX_VALUE 可能创建数量非常多的线程
线程池的参数
corePoolSize 线程池核心线程数量
maximumPoolSize 线程池最大线程数量
keepAliverTime 当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
unit 存活时间的单位
workQueue 存放任务的队列(阻塞队列),在任意时刻,不论并发有多高,永远只有一个线程能够进行入队或出队操作
handler 超出线程范围和队列容量的任务的处理程序
ThreadFactory 线程工厂
线程池的实现原理
提交一个任务到线程池,线程池判断线程池里的核心线程是否都在执行任务,如果不是,则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,就进行下个流程
线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里,如果工作队列满了,则进入下个流程
判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务,如果已经满了,则交给饱和策略(直接抛出异常、只用调用所在的线程运行任务、丢弃队列里最近的一个任务,并执行当前任务、不处理,丢弃掉)来处理这个任务
线程池的关闭方式
shutdown()
shutdown()后线程池将变成shutdown状态,此时不接收新任务,但会处理正在运行的和在阻塞队列中等待处理的任务
shutdownNow()
shutdownNow()后线程池将变成stop状态,此时不接收新任务,不再处理在阻塞队列中等待的任务,还会尝试中断正在处理中的工作线程。
什么时候使用线程池
单个任务处理时间比较短
防止任务堆积
需要处理的任务数量很大
线程池里线程数量的确定
CPU密集型
任务需要大量的运算,需要将线程与核心对应,即取cpu核心数+1个线程
IO密集型
有大量的IO操作,如读取数据库,即会有线程阻塞,所以多个线程轮流,利用cpu资源,取CPU核数*2
java同步机制
wait、notify
ThreadLocal机制
锁的相关概念
可重入锁
一个线程执行某个同步方法,该同步方法调用另一个同步方法,则该线程不需要再重新申请锁,而是直接执行另一个方法
可中断锁
synchronized是不可中断锁,lock是可中断锁
线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它
公平锁
尽量以请求锁的顺序来获取锁
如何减少线程上下文切换
无锁并发编程
多线程处理数据时,用一些办法避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据
CAS
Java的Atomic包使用CAS算法更新数据,而不需要加锁
控制线程数量
控制线程数量,避免创建不需要的线程,造成大量线程都处于等待状态
协程
在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
进程的状态与切换
进程状态
新建
就绪
运行
阻塞
终止
状态切换
就绪态->运行态
得到处理机调度
运行态->就绪态
时间片用完;更高优先级的进程就绪时,可以调度优先级更高的程序执行
运行态->阻塞态
请求资源失败
阻塞态->就绪态
资源来了
一个进程由运行态到阻塞态是主动的行为,而阻塞态变成就绪态是被动行为,需要其他相关进程的协助
线程模型
用户线程(ULT)
由应用管理,应用提供创建,同步,调度等操作。不需要用户态/核心态切换
为什么有用户态到核心态的切换
内存分为用户空间和核心空间,当应用需要调用硬件时,需要内核线程调用
内核线程(KLT)
由操作系统管理
JVM在用户空间创建线程,通过库调度器,调用系统库创建内核线程。cpu通过调度算法调用内核线程
进程间通信的方式
共享内存
多个进程地址空间映射到同一块内存中
内存映射文件
管道
用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,只存在于内存中
写进程以字符流形式将大量的数据送入管道;读进程从管道中接收数据
只支持半双工通信(单向交替传输)
只能在亲源进程中使用
有名管道
去除了管道只能在父子进程中使用的限制
消息队列是消息的链表,存放在内核中并由消息队列标识符表示,提供了一个从一个进程向另一个进程发送数据快的方法
特点
生命周期随内核,消息队列会一直存在,需要我们显示的调用接口删除或使用命令删除
消息队列可以双向通信
克服了管道只能承载无格式字节流的缺点
消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取
客户机-服务器系统通信
套接字(socket)
远程过程调用
通过RPC协议,允许运行于一台主机系统上的进程调用另一台主机系统的进程
信号量
是一个计数器,用于为多个进程提供共享数据对象的访问
它常作为一种锁的机制,防止某进程正在访问共享资源时,其他进程也访问该资源。主要作为不同进程或者同一进程之间不同线程之间同步的手段
进程同步
临界区
对临界资源进行访问的代码称为临界区
为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查
用于进程间传递信号的一个整数值
pv
管程
由若干个变量以及方法所组织成的一种特殊的结构
将分散在各个进程中的临界区集中起来进行统一控制和管理,并且将系统中的共享资源用数据结构抽象的描述出来,然后对临界区的访问通过管程进行统一管理
特性
互斥性
任何时刻只能最多一个进程进入管程活动,其他想进入必须等待
安全性
管程中的局部变量只能由管程的方法访问,其他进程或管程不能够对该局部变量进行直接访问
共享性
管程中的特定的方法可以被其他管程或进程访问,这样的方法有特殊说明
线程间通信的方式
加锁
synchronized加锁的线程中的 wait()/notify()
lock中condition类的await()/signal()
管道流用于在不同线程间直接传送数据
一个线程发送数据到输出管道,另一个线程从输入管道读数据
wait() notify() notifyAll()的区别
wait: 线程自动释放其占有的对象锁,并等待notify()
notify: 唤醒一个正在wait当前对象锁的线程,并让它拿到对象锁
notifyAll:唤醒所有正在wait当前对象锁的线程
死锁的必要条件
互斥条件
不可抢占条件
请求和保持
环路等待
解决死锁的方法
死锁检测和死锁恢复
死锁预防
死锁避免
鸵鸟策略
进程和线程的区别
一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
拥有资源:进程是资源分配的基本单位,线程几乎不拥有资源(只需要少量的资源(指令指针IP,寄存器,栈),线程可以访问隶属进程的资源
通信方面:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助IPC
调度:线程是独立调度的基本单位,在统一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程会引起进程切换
系统开销:创建和销毁进程时,系统都要为之分配和回收资源,如内存空间、I/O设备等,所符出的开销远大于创建或撤销线程时的开销。类似的,在进行进程切换时,设计当前执行进程CPU环境的保存及新调度进程CPU环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小
进程调度的基本方式
时间片轮转
优先级调度
多级反馈队列
进程同步和进程通信的区别
进程同步:控制多个进程按一定顺序执行
进程通信:进程间传输信息
sleep()和wait()的区别
当在一个Synchronized方法中调用sleep()时,线程虽然休眠了,但是对象的机锁没有被释放,其他线程任然不可以访问这个对象
wait()方法则会在线程休眠的同时释放掉机锁,其他线程可以访问该对象。
Yield()方法是停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么Yield() 方法将不会起作用。
join()方法使当前线程停下来等待,直至另一个调用join方法的线程终止。
AQS
同步队列
一个虚拟双端队列,遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会被构造成一个Node节点加入到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用
静态链接和动态链接
静态链接
在生成可执行程序的时候,把目标文件和静态库,使用Id连接器,链接生成一个可执行程序
动态链接
在生成可执行程序的时候,只是引用的未定义的符号作了标识,到加载到内存中的时候才进行符号重定位
守护线程
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程
并行和并发的区别
并行是指两个或多个事件在同一时刻发生,并发是指两个或多个事件在同一时间间隔发生
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件
在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务
fork()和exec()
fork()
Linux的进程都通过init进程或init进程的子进程fork出来的
fork()会产生一个和父进程完全相同的子进程(除了pid)
exec()
装载一个新的程序(可执行映像)覆盖当前进程内存空间中的映像,从而执行不同的任务
exec()系列函数在执行时会直接替换掉当前进程的地址空间
锁的膨胀
https://www.cnblogs.com/twoheads/p/10148598.html
集合框架
ArrayList与LinkedList异同
是否保证线程安全
ArrayList和LinkedList都是不同步的,也就是不保证线程安全
底层数据结构
ArrayList底层使用的是Object数组
LinkedList底层使用的双向循环链表数据结构
插入和删除是否受元素位置的影响
LinkedList采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似O(1),查询近似O(n)
是否支持快速随机访问
LinkedList不支持高效的随机元素访问
ArrayList实现了RandmoAcess接口,所有有随机访问功能。快速随机访问就是通过元素的序号快速获取元素对象
内存空间占用
ArrayList的空间浪费主要体现在list列表的结尾会预留一定的容量空间
LinkedList的空间浪费体现在它的每一个元素都需要消耗比ArrayList更多的空间(存放直接前驱和直接后继以及数据)
ArrayList与Vector的区别
Vector的所有方法都是同步的,可以由两个线程安全的访问一个Vector对象,但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间
ArrayList不是同步的,所以不需要保证线程安全时建议使用ArrayList
ArrayList和Array的区别
Array可以包含基本类型和引用类型,ArrayList只能包含引用类型
Array大小是固定的,ArrayList的大小是动态变化的
Hash
HashMap的底层实现
JDK 1.8之前HashMap是通过 数组+链表 实现的。链表主要用来解决哈希冲突
如果定位到的数组位置不含链表,那么对于查找、添加操作很快,为O(1)
如果定位到的数组位置包含链表,对于添加操作,时间复杂度仍为O(1),对于查找操作,需要遍历链表
JDK 1.8之后解决哈希冲突,当链表长度大于阈值(默认为8),采用红黑树,以减少搜索时间
为什么1.7采用头插
效率问题,尾插需要多次遍历
一贯的思维,新插入的节点要更可能被再次调用
HashMap和HashTable的区别
线程是否安全
HashMap是非线程安全的
HashTable是线程安全的
效率
因为要保证线程安全,所以HashMap效率要比HashTable高
对Null key和Null value的支持
HashMap中,null可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为null
HashTable中,put进的键值只要有一个为null,直接抛出NullPointerException
初始容量大小和每次扩充容量大小的不同
HashMap默认的初始化大小为16,每次扩充,容量变为原来的2倍。当初始给出大小后,HashMap会将其扩充到2的幂次方
HashTable初始的容量大小为11,每次扩容,容量变为原来的2n+1
为什么HashMap的长度是2的幂次方
为什么HashMap的加载因子是0.75
因子大,链表长度越长,扩容耗时长
因子小,扩容频繁,空间浪费
HashMap和HashSet的区别
实现接口
HashMap实现Map接口
HashSet实现Set接口
存储对象
HashMap存储键值对
HashSet仅存储对象
添加方式
HashMap调用 put() 向map中添加元素
HashSet调用add()方法向Set中添加元素
ConCurrentHashMap与HashTable的区别
JDK 1.7的ConcurrentHashMap采用 分段的数组+链表实现,JDK1.8采用数组+链表/红黑树实现
HashTable采用数组+链表实现
实现线程安全的方式
ConcurrentHashMap,在JDK 1.8直接使用Node数组 + 链表 + 红黑树的数据结构,并发控制使用 synchronized和CAS来操作。
HashTable,使用synchronized来保证线程安全,效率低
hashCode()与equals()的相关规定
如果两个对象相等,则hashcode一定也是相同的
两个对象相等,对两个对象分别调用equals方法都返回true
两个对象有相同的hashcode值,他们也不一定是相等的
重写equals()方法,hashcode方法也必须重写
为什么重写equals()方法一定要重写hashcode
ConcurrentHashMap为什么要放弃Segment
锁的粒度
锁的粒度变细,并发度更大
get、put性能
java1.7需要先hash得到segment,在hash得到具体的索引值,1.8直接通过数组加链表得到索引值
扩容
在jdk1.8中,其他线程可以通过检测数组中的节点决定是否对这条链表进行扩容,减小了扩容的粒度,提高了扩容的效率
ConcurrentHashMap插入流程
校验key value 值,都不能是null
得到 key 的 hash 值
根据 hash 值找到数组下标,如果对应的位置为空,就创建一个 Node 对象用CAS方式添加到容器
如果 hash 冲突,也就是对应的位置不为 null,则判断该槽是否被扩容了(-1 表示被扩容了),如果被扩容了,返回新的数组
如果 hash 冲突 且 hash 值不是 -1,表示没有被扩容。则进行链表操作或者红黑树操作,注意,这里的 f 头节点被锁住了,保证了同时只有一个线程修改链表。防止出现链表成环
如果链表树超过8,则修改链表为红黑树
集合框架总结
Collection
List
ArrayList
Object数组
Vector
LinkedList
双向循环链表
Set
HashSet
无序,唯一
基于HashMap实现,底层采用HashMap来保存元素
LinkedHashSet
LinkedHashSet继承于HashSet,并且其内部是通过LinkedHashMap实现的
TreeSet
有序,唯一
红黑树实现(自平衡的二叉树)
Map
HashMap
JDK 1.8之前采用数组 + 链表实现,后使用 数组+ 链表+ 红黑树实现
LinkedHashMap
继承自HashMap,在此基础上,增加了一条双向链表
HashTable
数组+链表
TreeMap
红黑树
集合安全的ArrayList
Collections.synchronizedArrayList<>()
CopyOnWriteArrayList
copyOnWrite
A读取数据时,B修改数据,会先加锁,然后将数组复制一份,并扩容,然后在扩容的索引增加值,在返回后解锁
redis
数据类型
基本数据结构
SDS
字节数组 char[] buf[] 保存字符串
int len 记录字节数组中已使用的字节数量,也是字符串的长度
int free 记录字节数组中未使用的字节数量
SDS优点
用len记录字符串的长度,获取字符串的长度时,时间复杂度为O(1)
不会发生溢出的问题,如果修改SDS时,空间不足,会先扩展空间,再进行修改(内部实现了动态扩展机制)
SDS可以减少内存分配的次数(空间预分配机制)。在扩展空间时,除了分配必要的空间,还会分配额外的空闲空间
二进制安全,所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据
链表
list
listNode
listNode *pre 前置节点
listNode *post 后置结点
void *value 结点的值
listNode *head 表头结点
listNode *tail 表尾结点
long len 链表长度
void *(*dup) 节点值复制函数
void *(*free) 节点值释放函数
int (*match) 节点值对比函数
链表特性
无环双向链表,获取某个节点的前置节点和后置节点的复杂度都是O(1)
获取表头节点,表尾节点,链表节点长度的事件复杂度均为O(1)
链表使用void * 指针保存节点值,可以保存各种不同类型的值
字典
数据结构
dict
dictType *type 类型特定函数
void *privdata 私有数据
dictht ht[2] 哈希表
int rehashidx rehash索引,当rehash不进行时,值为-1
dictht
dictEntry **table 哈希表数组
long size 哈希表大小
long sizemark 哈希表大小掩码,用于计算索引值(总是等于size - 1)
long used 哈希表已有节点数量
dictEntry
void * key 键
值
void * value
unit64_tu64
unit64_ts64
dictEntry *next 指向下一个哈希节点,组成链表
这个指针可以将多个哈希值相同的键值对连接在一次,以此来解决键冲突(collision)的问题
hash值的计算
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值
即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快
解决键冲突
定义
当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,我们称这些键发生了冲突
链地址法
每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题
因为dictEntry节点组成的链表没有指向链表表尾的指针,所以为了速度考虑,程序总是将新节点添加到链表的表头位置(复杂度为O(1)),排在其他已有节点的前面
因为新增加的节点往往调用频率更高,所以会放在头节点
rehash
为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩
大小
为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也即是ht[0].used属性的值)
如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的2 n(2的n次方幂)
过程
为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始
在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一
随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成
细节
在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行。例如,要在字典里面查找一个键的话,程序会先在ht[0]里面进行查找,如果没找到的话,就会继续到ht[1]里面进行查找,诸如此类。另外,在渐进式rehash执行期间,新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作,这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表
rehash触发条件
负载因子
负载因子= 哈希表已保存节点数量/哈希表大小
当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作
服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1
服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5
根据BGSAVE命令或BGREWRITEAOF命令是否正在执行,服务器执行扩展操作所需的负载因子并不相同,这是因为在执行BGSAVE命令或BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表扩展操作,这可以避免不必要的内存写入操作,最大限度地节约内存
跳跃表
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的
跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点
Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构
zskiplistNode
zskiplistNode *backward 后退指针
double score 分值
robj *obj 成员对象
level[] 层
* forward 前进指针
span 跨度
zskiplist
length 节点数量
level 最大层数
参数解释
层
跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快
前进指针
每个层都有一个指向表尾方向的前进指针(level[i].forward属性),用于从表头向表尾方向访问节点
跨度
层的跨度(level[i].span属性)用于记录两个节点之间的距离
两个节点之间的跨度越大,它们相距得就越远
指向NULL的所有前进指针的跨度都为0,因为它们没有连向任何节点
后退指针
节点的后退指针(backward属性)用于从表尾向表头方向访问节点:跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点
分值和成员
节点的分值(score属性)是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序
节点的成员对象(obj属性)是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值
intset
contents[]
整数集合的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项
length
记录了整数集合包含的元素数量,也即是contents数组的长度
encoding
contents数组的真正类型取决于encoding属性的值
升级
当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级(upgrade),然后才能将新元素添加到整数集合里面
根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变
将新元素添加到底层数组里面
好处
提升整数集合的灵活性
因为整数集合可以通过自动升级底层数组来适应新元素,所以我们可以随意地将int16_t、int32_t或者int64_t类型的整数添加到集合中,而不必担心出现类型错误,这种做法非常灵活
尽可能地节约内存
整数集合现在的做法既可以让集合能同时保存三种不同类型的值,又可以确保升级操作只会在有需要的时候进行,这可以尽量节省内存
整数集合只支持升级操作,不支持降级操作
ziplist
当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现
压缩列表被用作列表键和哈希键的底层实现之一
sortset 有序集合
String类型的无序集合,每个元素关联一个double类型的分数,通过分数为集合中的成员排序,double可以重复。通过HashMap和跳跃表实现,HashMap存放成员到score的映射,跳跃表存放所有成员
对象
string
hash
set
zset
持久化
把内存的数据写到磁盘中,防止服务宕机内存数据丢失
方式
RDB(默认)
生成方式
SAVE
SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求
BGSAVE
BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求
AOF
存储结构
内容是redis通讯协议(RESP)格式的命令文本存储
AOF通过保存redis服务器所执行的写命令来记录数据库的数据的
持久化流程
写入AOF缓存区
服务器在执行完一个写命令之后,以协议格式将被执行的命令追加到aof_buf缓冲区的末尾中
考虑是否写入AOF文件
随后会调用flushAppendOnlyFile函数考虑是否需要将aof_buf缓冲区的内容写入到AOF文件中
考虑是否同步AOF文件
最后确定是否将AOF文件内容同步
如果两个都配置了,优先加载AOF
AOF文件的更新频率通常比RDB文件的更新频率高
对比
RDB
载入时恢复数据快,文件体积小
会一定程度上丢失数据
丢失数据少
恢复数据相对较慢,文件体积大
redis分布式锁是怎么实现的
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间
setnx key value
将key的值设为value,当且仅当key不存在,若key存在,则不做任何动作
redis异步队列
使用list结构作为队列,rpush生产消息,lpop消费消息,当lpop没有消息的时候,适当sleep一会
redis存在的问题
缓存穿透
恶意请求故意查询不存在的key,请求量很大,跳过缓存直接查询数据库
对查询为空的情况也进行缓存,时间设置短一点,或者对该key有insert操作之后清理缓存
使用布隆过滤器过滤一定不存在的key
缓存雪崩
当缓存服务器重启或者大量缓存集中在某个时间段失效,在失效的时候,会给后端系统带来很大压力
保证缓存层服务高可用性
使用消息队列为后端限流
错开键的过期时间
主从复制相关
前置工作
从服务器设置主服务器的IP和端口
建立与主服务器的Socket连接
发送PING命令
身份验证
从服务器给主服务器发送端口的信息,主服务器记录监听的端口
完整重同步
从服务器向主服务器发送PSYNC命令
收到PSYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件。并用一个缓存区来记录从现在开始执行的所有写命令
当主服务器的BGSAVE命令执行完后,将生成的RDB文件发送给从服务器,从服务器接收和载入RBD文件将自己的数据库状态更新至与主服务器执行BGSAVE命令时的状态
主服务器将所有缓冲区的写命令发送给从服务器,从服务器执行这些写命令,达到数据最终一致性
部分重同步
模块
主从服务器的复制偏移量
执行复制的双方都会分别维护一个复制偏移量
通过对比主从复制的偏移量,判断主从服务器数据是否一致
主服务器的复制积压缓冲区
主服务器进行命令传播时,不仅会将命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里面。
如果复制积压缓冲区存在丢失的偏移量的数据,那就执行部分重同步,否则,执行完整重同步
服务器运行的ID(run ID)
服务器run ID用来比对ID是否相同,判断从服务器断线之前复制的主服务器和当前连接的主服务器是否是同一台服务器,不是则进行完整重同步
心跳检测
在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令
REPLCONF ACK <replication_offset>
replication_offset是从服务器当前的复制偏移量
检测主从服务器的网络连接状态
辅助实现min-slaves选项
检测命令丢失
哨兵机制
哨兵的含义就是监控redis系统的运行状态
监控所有节点数据库是否在正常运行。
master数据库出现故障时,可以自动通过投票机制,从slave节点中选举新的master,实现将从数据库转换为主数据库的自动切换
redis高并发和快速的原因
redis是基于内存的,内存的读写速度非常快
redis是单线程的,省去了很多上下文切换线程的时间
redis使用多路复用技术,可以处理并发的连接。
redis过期键处理
判断键是否过期
在redis中维护一个expires字典,保存数据库中所有设置了过期时间的键的过期时间。通过key去字典中获取key的过期毫秒时间戳,再减去当前时间戳,得到剩余生存时间
过期键删除策略
惰性删除策略(被动)
程序在取出键时才对key进行过期检查,若过期则删除,否则照常执行
对CPU时间来说是最友好的:程序只会在取出键时才对键进行过期检查,这可以保证删除过期键的操作只会在非做不可的情况下进行,并且删除的目标仅限于当前处理的键,这个策略不会在删除其他无关的过期键上花费任何CPU时间
对内存是最不友好的:如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放
定期删除策略(主动)
每隔一段时间执行一次随机删除过期键的操作,并通过限制删除操作执行的时常和频率来减少删除操作对cpu时间的影响
通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的内存浪费
内存淘汰机制
定期删除漏掉很多过期key,且没及时执行惰性删除,大量过期key堆积在内存,导致redis内存块耗尽
volatile-lru
从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl
从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random
从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru
从所有数据集中挑选最近最少使用的数据淘汰
allkeys-random
从所有数据集中任意选择数据进行淘汰
noeviction
禁止驱逐数据
一般场景
使用redis缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。即将内存最大使用量设置为热点数据占用的内存量,然后启用allkeys-lru 淘汰策略,将最近最少使用的数据淘汰
持久化过程对过期键的处理
执行SAVE或者BGSAVE命令创建出的RDB文件,程序会对数据库中的过期键检查,已过期的键不会保存在RDB文件中
载入RDB文件时,程序同样会对RDB文件中的键进行检查,过期的键会被忽略
如果数据库的键已过期,但没被删除,AOF会保留该键,当过期的键被删除以后,会追加一条DEL命令来显示记录删除信息
重写AOF文件时,程序会对AOF文件中的键进行检查,过期的键会被忽略
主从复制
从在读到过期键的时候,不会删除,而是返回该值,只有在主显示删除改过期键时通知从才会删除该键,这样保证主从一致性
Redis会将EXEC命令前的命令放入一个队列,当遇到EXEC时批量执行队列中的命令,但是Redis的事务不支持回滚
架构模式
单机
简单
内存容量有限
处理能力有限
无法高可用
数据相同,主服务器会把数据同步到从服务器
主服务器用来写,从服务器用来读
无法保证高可用
没有解决master写的压力
哨兵
特征
监控 主服务器和从服务器是否运行正常
提醒 当被监控的redis服务器发现问题,可以通过api向管理员或者其他应用发送通知
自动故障迁移,将从服务器升级为主服务器
保证高可用
监控各个节点
自动保障迁移
切换主从模式需要时间丢数据
集群
代理集群
直连型集群
无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接
所有的redis节点互联
节点的fail是通过集群中超过半数的节点检测失效时才生效
客户端与redis节点直连,不需要中间proxy层
分布式
zookeeper
主备模式
所有客户端写入数据都是写入到主进程(Leader),然后,由Leader复制到备份进程(Follower)中,从而保证数据一致性
zab协议
一种一致性协议
为分布式协调服务zookeeper专门设计的一种支持崩溃恢复和消息广播协议
消息广播(Leader正常)
对于客户端发送的写请求,全部由Leader接收,Leader将请求封装成一个事务Proposal,将其发送给所有Follower,然后根据所有Follower的反馈,如果超过半数成功响应,则执行commit操作(先提交自己,再发送commit给所有Follower)
Leader在收到客户端请求之后,会将这个请求封装成一个事务,并给这个事务分配一个全局递增的唯一ID,称为事务ID,zab协议需要保证事务的顺序,因此将每个事务按照事务ID先排序再处理
再Leader和Follower之间还有一个消息队列,用来解耦他们之间的耦合,接触同步阻塞
zookeeper集群为保证任何所有进程能够有序的顺序执行,只能是Leader服务器接受写请求,即使是Follower服务器接受到客户端的请求,也会转发到Leader服务器进行处理
崩溃恢复(Leader崩溃)
两个原则
zab协议确保那些已经在Leader提交的事务最终会被所有服务器提交
新的 leader 与 follower 建立先进先出的队列, 先将自身有而 follower 没有的 proposal 发送给 follower,再将这些 proposal 的 COMMIT 命令发送给 follower,以保证所有的 follower 都保存了所有的 proposal、所有的 follower 都处理了所有的消息
zab协议确保丢弃那些只在Leader 提出/复制,但没有提交的事务
实现
通过选举算法(选举出来的Leader服务器拥有集群中事务ID最大的服务器),保证这个新选举出来的Leader一定具有所有已经提交的提案
数据同步
奔溃恢复之后,需要在正式工作之前(接受客户端请求),Leader服务器首先确认事务是否都已经被过半的Follower提交了,即是否完成了数据同步。
当完成Leader选举后,进行故障恢复的第二步就是数据同步: Leader服务器会为每一个Follower服务器准备一个队列,并将那些没有被各个Follower服务器同步的事务以Proposal的形式逐条发给各个Follower服务器,并在每一个Proposal后都紧跟一个commit消息,表示该事务已经被提交,当follower服务器将所有尚未同步的事务proposal都从leader服务器同步过来并成功应用到本地后,leader服务器就会将该follower加入到真正可用的follower列表中。(新选举周期,epoch已经更新了)
节点类型
持久节点
节点创建后,一直存在,直到主动删除了该节点
临时节点
生命周期和客户端会话绑定,一旦客户端会话失效,这个节点就会自动删除
序列节点
多个线程创建同一个顺序节点时,每个线程会得到一个带有编号的节点,节点编号是递增不重复的
脑裂
例如两个机房内的实例组成一个集群,如果两个机房间的网络出现问题,但机房内部通信没有问题,可能导致两个机房都选举出一个leader,一旦网络恢复,就存在脑裂问题
由于zk使用的是过半选举机制,所以不会出现脑裂问题
dubbo
随机,按照权重设置随机概率,可以动态调整权重
轮询,按公约后的权重设置轮询比率
最少活跃数,相同活跃数的随机,活跃数指调用前后计数差
一致性哈希
其他
抽象类和接口的区别
设计层面上,抽象类提供了 IS-A关系,接口是LIKE-A关系
使用上,一个类可以实现多个接口,但是不能继承多个抽象类
接口的字段只能是static和final的,抽象类的字段没有限制
接口的方法只能是public的,抽象类的方法可以有多种访问权限
java8之前接口的方法不能有方法体,java8之后可以有具体实现
IO模型
BIO(阻塞IO)
一个连接一个线程
NIO(非阻塞IO)
一个请求一个线程
核心
Channel
Buffer
Selector
NIO基于Reactor,不是一个连接对应一个处理线程,而是一个有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。NIO最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上,所有的连接只需要一个线程就可以,当这个线程中的多路复用器进行轮询时,发现连接上有请求的话,才开启一个线程进行处理。
Reactor线程模型
Reactor 模式是处理并发 I/O 比较常见的一种模式,用于同步 I/O,中心思想是将所有要处理的IO事件注册到一个中心 I/O 多路复用器上,同时主线程/进程阻塞在多路复用器上;一旦有 I/O 事件到来或是准备就绪(文件描述符或 socket 可读、写),多路复用器返回并将事先注册的相应 I/O 事件分发到对应的处理器中
NIO服务端流程
生成一个selector对象
将ServerSocketChannel注册到selector上,并设置关心的事件为 OP_ACCEPT
循环等待客户端连接
如果selector对象调用select()没有事件发送,则返回
如果有事件发生,就获取到相关的 selectionKey集合,通过集合里的selectionKeys反向获取通道
如果是OP_READ事件,通过selectionKeys获取对应通道,并读取他的buffer
AIO(异步非阻塞IO)
一个有效请求一个线程
AIO需要一个连接注册读写事件和回调方法,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数
Netty简易
BossGroup维护一个selector,这个selector只处理client的accept事件
当worker线程接受到读写事件,就将这事件交给handler处理
单例模式
单例模式可以保证系统中一个类只有一个实例
单例模式只能有一个实例
单例模式类必须自己创建自己的唯一实例
单例模式类必须给所有其他对象提供这一实例
饿汉式
实现简单,不存在多线程同步问题,避免了synchronized所造成的性能问题,是线程安全的(JVM保证线程安全)
JVM怎么保证线程安全
JVM以同步的形式完成类加载的整个过程
保证实例的唯一性
初始化过程只会执行一次
类加载的时候,静态变量被创建并分配内存空间。因此在特定条件下会耗费内存
懒汉式
实现简单,类加载的时候不会创建,当getInstance方法第一次被调用时,才初始化并分配内存
再并发环境下很可能出现多个实例
饱汉式
使用synchronized关键字避免多线程访问
同步方法频繁调用时,效率低
双检锁
避免了整个方法被锁,只需对需要锁的代码部分加锁,提高执行效率
静态内部类
不会在单例加载时就加载,而是在调用getInstance()方法时才进行加载,且是线程安全的
遇到序列化对象时,默认的方式运行得到的结果就是多例的
内部枚举类
自由序列化
保证只有一个实例
线程安全
虚拟节点解决hash环的偏斜
反射
在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法。对于任意一个对象,都能够调用它的任意一个方法和属性
获取Class对象的三种方式
通过对象的getClass()方法进行获取。这种方式需要具体的类和该类的对象,以及调用getClass方法
任何数据类型都具备一个静态的属性class,通过它可直接获取到该类型对应的Class对象。这种方式要使用具体的类,然后调用类中的静态属性class完成,无需调用方法,性能更好
通过Class.forName()方法获取。这种方式仅需使用类名
动态代理
代理是一种设计模式,提供了对目标对象另外的访问方式;即通过代理访问目标对象
代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息,在目标对象实现的基础上,增强额外的功能操作。代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务
深拷贝和浅拷贝的区别
浅拷贝
①对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。②对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
深拷贝
深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间
Exception
Error和Exception的区别
Error是系统的错误,程序员是不能改变和处理的,是在程序编译时出现的错误,只能通过修改程序才能修正
Exception表示程序可以处理的异常,可以捕获且可能恢复
CheckedException
需要用try-catch显示的捕捉,对于可恢复的异常使用
RuntimeException
不需要捕获,对于程序错误
序列化
把对象转换为字节序列的过程称为对象的序列化
反序列化
把字节序列恢复为对象的过程称为对象的反序列化
序列化的用途
把内存中的对象状态保存到一个文件中或者数据库中
用socket在网络上传送对象的时候
排序算法
选择排序
n平方
每一趟从待排序的记录中选出最小的元素,顺序放在已排好序的序列最后,直到全部记录排序完毕。
插入排序
n
从整个待排序列中选出一个元素插入到已经有序的子序列中去,得到一个有序的、元素加一的子序列,直到整个序列的待插入元素为0,则整个序列全部有序
冒泡排序
比较两个相邻的元素,将值大的元素交换至右端
希尔排序
n2
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
快速排序
nlgn
堆排序
归并排序
CPU缓存一致性协议
https://www.cnblogs.com/yanlong300/p/8986041.html
同步,异步,阻塞,非阻塞概念
https://blog.csdn.net/lengxiao1993/article/details/78154467
RPC
僵尸进程与孤儿进程
https://blog.csdn.net/Eunice_fan1207/article/details/81387417
分布式锁
通过setnx拿到key,然后通过expire对该key设置过期时间,将这两个操作通过加锁整合成原子性操作
多线程创建同一个临时节点,由于节点的唯一性,所以只会一个线程创建成功,其他线程watch这个节点。
羊群问题
当获得锁的线程释放锁后,所有等待的线程一起去抢这把锁,zk压力大。
每个线程都去创建一个顺序临时节点,相互监听,将非公平锁化为公平锁
zookeeper相对于redis有什么优势
redis当主拿到锁后,如果没有及时同步就宕机,可能导致锁失效
zk通过zab协议的限制,从而保证了只有超过半数实例拿到数据才算写成功,不会出现这个问题
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接
JVM
运行时数据区域
线程私有
程序计数器
字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了
虚拟机栈
局部变量表
各种基本数据类型
对象引用
操作数栈
操作数的临时存放内存
动态链接存储。在main方法中调用某个类的方法,该类的实例对象存在堆中,对象头记录着对应类元信息所在的方法区的指针。在调用这个方法的时候,会将该指针所存储的对应方法的字节码位置存入动态链接
方法出口
下一步main方法要执行的位置
栈里每个方法都有自己的栈帧,用于存自己的局部变量表
本地方法栈
为虚拟机使用到的Native方法服务
线程共享
堆
存放对象实例
年轻代
伊甸园区
minor GC(将剩余存活对象移动到survivor区)
survivor区
From区
minor GC(将剩余存活对象移动到To区,两者来回挪动,直到java分代年龄到15,移动到老年代)
To区
老年代
方法区
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
直接内存
避免Java堆和Native堆之间来回复制数据
对象创建过程
类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程
分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来
分配方式
指针碰撞
空闲列表
初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式
执行init方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来
类加载过程
加载。在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据入口
验证。确保class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
准备。为变量分配内存并设置变量的初始值阶段,类变量分配在方法区,实例变量随着对象一起分配到Java堆
解析。虚拟机将常量池中的符号引用替换为直接引用的过程,即将class文件中字面量形式的符号引用替换为直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
初始化。执行类构造器方法(<clinit>)的过程,此方法是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
内存溢出与内存泄漏
内存溢出
程序在申请内存时,没有足够的内存空间供其使用
程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早会被占光
常发性内存泄漏
发生内存泄漏的代码会被多次执行,每次执行都会导致一块内存泄漏
偶发性内存泄漏
发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生
一次性内存泄漏
发生内存泄漏的代码会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。例如:类的构造函数中分配内存,在析构函数中没有释放该内存
隐式内存泄漏
程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。对于一个服务器程序,需要运行很久,不及时释放内存可能导致最终耗尽系统的所有内存
内存泄漏原因
无用对象持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费
长生命周期的对象持有短生命周期的对象的引用就很可能发生内存泄漏
常见的内存泄漏
单例造成内存泄漏
GC
GC功能
分配内存,为每个新建的对象分配空间
确保还在使用的对象的内存一直都在,不能把有用的空间当垃圾回收
释放不再使用的对象所占用的内存
GC Roots
虚拟机栈中局部变量表中引用的对象
本地方法栈中JNI中引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
内存分类
大部分对象初始化都是在年轻代
老年代存放经过几次年轻代垃圾收集还活着的对象,还有部分大对象因为比较大,所以直接分配在老年代
永久代
永久代通常也叫方法区,用于存储已加载类的元数据,以及存储运行时常量池
垃圾回收类型
minor GC
当年轻代被填满后,会进行一次年轻代垃圾收集
fulll GC
当老年代或永久代被填满后,会触发full GC,full GC会收集所有区域,先进行年轻代的收集,使用年轻代专用的垃圾回收算法,然后使用老年代的垃圾回收算法回收老年代和永久代,如果算法带有压缩,每个代分别独立的进行压缩
Full GC本身不会先进行Minor GC,我们可以配置,让Full GC之前先进行一次Minor GC,因为老年代很多对象都会引用到新生代的对象,先进行一次Minor GC可以提高老年代GC的速度。比如老年代使用CMS时,设置CMSScavengeBeforeRemark优化,让CMS remark之前先进行一次Minor GC
如果垃圾收集完成后,存在大片连续的内存可用于分配新对象,此时使用指针碰撞分配对象空间
对于多线程应用,对象分配必须要保证线程安全性,如果使用全局锁,那么分配空间将成为瓶颈,降低程序性能。HotSpot使用TLABs,给每个线程一部分内存作为缓存区,每个线程缓存区进行指针碰撞,就不用获取全局锁
GC算法
引用计数
复制
标记清除
标记整理
收集器
串行
有一个线程进行垃圾回收,会暂停所有的用户线程
并行
多个线程进行垃圾回收,会暂停所有的用户线程
CMS
在年轻代中,CMS和并行收集器一样,即:并行、stop-the-world、复制
在老年代中,大部分收集任务是和应用程序并发执行的
CMS 收集过程首先是一段小停顿 stop-the-world,叫做 初始标记阶段(initial mark),用于确定 GC Roots。然后是 并发标记阶段(concurrent mark),标记 GC Roots 可达的所有存活对象,由于这个阶段应用程序同时也在运行,所以并发标记阶段结束后,并不能标记出所有的存活对象。为了解决这个问题,需要再次停顿应用程序,称为 再次标记阶段(remark),遍历在并发标记阶段应用程序修改的对象(标记出应用程序在这个期间的活对象),由于这次停顿比初始标记要长得多,所以会使用多线程并行执行来增加效率。再次标记阶段结束后,能保证所有存活对象都被标记完成,所以接下来的 并发清理阶段(concurrent sweep) 将就地回收垃圾对象所占空间
CMS是唯一不进行压缩的收集器,在它释放了垃圾对象占用的空间后,不会移动存活对象到一边去,节省垃圾回收的时间
由于空闲空间不是连续的,所以也就不能使用简单的 指针碰撞(bump-the-pointer) 进行对象空间分配了。它需要维护一个 空闲列表,将所有的空闲区域连接起来,当分配空间时,需要寻找到一个可以容纳该对象的区域
CMS 收集器相比其他收集器需要使用更大的堆内存。因为在并发标记阶段,程序还需要执行,所以需要留足够的空间给应用程序
虽然收集器能保证在标记阶段识别出所有的存活对象,但是由于应用程序并发运行,所以刚刚标记的存活对象很可能立马成为垃圾,而且这部分由于已经被标记为存活对象,所以只能到下次老年代收集才会被清理,这部分垃圾称为 浮动垃圾
由于缺少压缩环节,堆将会出现碎片化问题。为了解决这个问题,CMS 收集器需要追踪统计最常用的对象大小,评估将来的分配需求,可能还需要分割或合并空闲区域
G1
年轻代收集
G1收集器将堆内存分为大小固定的区块,整堆分为约2000块,每块大小是一致的逻辑上,也会分为Eden、Survivor、Old区,但是各个区的大小不是固定的,未分配区可以用于任何一代年轻代由几个不连续的区块组成,这样需要的时候可以很容易扩容、缩容Young GC是并行、stop-the-world的将活着的对象复制到Survivor区,或晋升到Old区为了下一次Young GC,需要计算Eden区和Survivor区的大小
老年代收集
初始标记:stop-the-world,它伴随着一次普通的 Young GC 发生,然后对 Survivor 区(root region)进行标记,因为该区可能存在对老年代的引用。扫描根引用区:扫描 Survivor 到老年代的引用,该阶段必须在下一次 Young GC 发生前结束。并发标记:寻找整个堆的存活对象,该阶段可以被 Young GC 中断。重新标记:stop-the-world,完成最后的存活对象标记。使用了比 CMS 收集器更加高效的 snapshot-at-the-beginning (SATB) 算法。清理:清理阶段真正回收的内存很少
G1调优(避免Full GC)
增加堆大小,调整老年代和年轻代的比例
怎加并发周期的线程数量,缩短并发周期
设置堆占用比,让并发周期尽早开始
在混合垃圾回收周期中回收更多的老年代区块
G1对比CMS
G1是一个有整理内存过程的垃圾收集器,不会产生过多内存碎片
用户可以指定期望停顿时间
共同点
都是并发收集器
强引用,软引用,弱引用,虚引用
强引用
只要引用存在,垃圾回收器永远不会回收
软引用
非必须引用,内存溢出之前进行回收
弱引用
一定会被回收,被弱引用关联的对象只能存活到下一次垃圾回收之前
虚引用
虚引用是每次垃圾回收的时候都会被回收
双亲委派模型
除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。类加载器之间的父子关系不会以继承的关系来实现,而是使用组合关系来复用父加载器的代码
工作过程
一个类加载器收到了类加载的请求,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载
优势
避免类的重复加载
保护程序安全,防止核心API被篡改
0 条评论
回复 删除
下一页