导图社区 JAVA知识体系
java程序员应该具备哪些知识体系?如何在面试中脱颖而出,收藏下图,几乎覆盖所有高频考察点,让你轻松应对面试。
编辑于2021-03-05 12:20:56JAVA知识体系
网络基础
网络基础知识
OSI开放式互联参考模型
发送进程,从上而下
接收进程,从下而上
应用层
表示层
会话层
传输层
网络层
传输层数据链路层
物理层
物理层
数据链路层
网络层
会话层
表示层
应用层
TCP/IP 模型
TCP三次握手
传输控制协议TCP简介
面向连接的、可靠的、基于字节流的 传输层通信协议
将应用层的数据流切割成报文段并发送给目标节点的 TCP层
数据包都有序号,对方接收到则发送ACK确认,未收到则重传
使用奇偶校验和 来 校验数据在传输过程中是否有误
TCP Flags
URG
紧急指针标志
ACK
确认序号标志
PSH
push标志
RST
重置连接标志
SYN
同步序号,用于建立连接过程
FIN
finish标志,用于释放连接
为什么要使用握手
握手是为了建立连接
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接
第一次握手
建立连接时,客户端发送SYN包(seq=x)到服务器,并进入SYN_SEND状态,等待服务器确认
第二次握手
服务端接收到SYN包,必须确认客户端的SYN包(ack=x+1),同时自己也发送一个SYN 包(seq=y),及发送SYN+ACK包,此时服务端进入 SYN_RECV状态
第三次握手
客户端收到服务端的SYN+ACK包,向服务端发送确认包ACK(seq=y+1),此包发送完毕,客户端和服务器都进入ESTABLLISHED状态,握手完毕
为什么使用三次握手
为了初始化Sequence Number的初始值
首次握手的隐患----SYN超时
客户端发送给服务端SYN后直接下线,服务端会等待客户端的SYN 相应,直到超时
针对SYN Flood 的防护措施
SYN队列满了以后,通过tcp_syncookies参数 回发SYN Cookie
若为正常的连接则 Client 会回发SYN Cookie,直接建立连接
建立连接后,Client出现故障怎么办
保活机制
向对方发送保活探测报文,如果未收到响应则继续发送
尝试次数达到保活探测次数仍未收到响应则中断连接
TCP四次挥手
客户端和服务端都可以发起结束连接
第一次挥手
Client发送 FIN包(seq=u),用来关闭Client 到 Server的数据传送,Client会进入FIN_WAIT1状态
第二次挥手
Server收到FIN后,会回复 ACK包,(seq=v,ack=u+1),Server进入 CLOSE_WAIT状态
第三次挥手
等待服务端处理完毕后,再次发送给客户端 FIN,(seq=w,ack=u+1),用来关闭Server 到 Client数据传输,Server进入 LAST_ACK状态
第四次挥手
Client 收到服务端的最终的 FIN包后,会回复 ACK包(seq=u+1,ack =w +1),Client进入TIME_WAIT状态,同时发送一个ACK给Server,Server进入CLOSED状态
为什么会有TIME_WAIT 状态
确保有足够的时间让对方收到ACK包
避免新旧连接混淆
为什么需要四次挥手才能断开连接
因为全双工,发送方和接收方都需要FIN和ACK报文
Linux服务器出现大量CLOSE_WAIT状态原因
对方关闭socket连接,我方忙于读或写,没及时关闭连接
检查代码,特别是释放资源的代码
检查配置,特别是处理请求的线程配置
TCP和UDP区别
UDP简介
UDP报文结构
Source Port
Destination Port
Length
Check Sum
UDP 特点
面向非连接
不维护连接状态,支持向多个客户端发送相同的信息
数据报头很短,只有8个字节,额外开销小
吞吐量紧受限于数据生成速率,传输速率,以及机器性能
尽最大努力交付,但不保证可靠交付,不需要维持复杂的链接状态表
面向报文,不用对应用程序提交的报文进行拆分或者合并
TCP 和 UDP 区别
面向连接
TCP面向连接
UDP面向报文
可靠性
TCP通过三次挥手保证可靠性
UDP不保证数据可靠性
有序性
TCP有 sequence序列号保证数据传输的有序性
UDP 不保证
速度
TCP做了大量的工作保证 数据的可靠性有序性等,所以速度慢
UDP块
量级
TCP是重量级
UDP是轻量级
TCP的滑窗
不好理解,过
RTT和 RTO
RTT
发送一个数据包到收到对方的ACK 所花费的时间
RTO
重传间隔
HTTP 超文本传输协议,应用层协议
特征
支持客户/服务器模式
简单快速
灵活
无连接
子主题
无状态
请求响应/步骤
客户端连接到web服务器
发送HTTP请求
服务器接受请求并返回HTTP响应
释放连接TCP连接
客户端浏览器解析HTML内容
连接结束
HTTP状态码 STATUS
1XX
指示信息,表示请求已受理
2XX
成功,表示请求已经被成功接收、理解、接受
3XX
重定向, 要完成请求必须要进一步操作
4XX
客户端错误,有语法错误或请求无法实现
常见状态码
200
OK,正常返回信息
400
BadRequest,客户端有语法错误,不能被服务器理解
401
unAuthorized 请求未经过授权,这种状态码必须和 WWW-Authenticate报头域 配合使用
403
forbidden 服务端收到了请求,但是拒绝提供服务
404
not found 请求资源不存在
500
Internal Server Error 服务器发生不可预期的错误
503
Server Unavaliable 服务端暂时不能处理客户端的请求,可能一段时间后就恢复正常
GET 和 POST 请求 的区别
Http报文层面
GET请求将请求信息放在URL,POST将请求内容放在报文体中
数据库层面
GET 请求符合幂等和安全性,POST 不符合
其他方面
GET可以被缓存,被存储,而POST 不可以
Cookie 和Session 区别
Cookie 是 服务端 发给客户端的 特殊信息,以文本方式存放在客户端客户端再次请求的时候,会把Cookie回发服务器收到后,会解析Cookie 生成与客户端相对应的内容
Session 是服务器端的机制,在服务器上保存信息解析客户端请求并操作Session id,按需保存状态信息
Session 存在 服务器上,Cookie存放在客户端上Session比Cookie安全若考虑减轻服务器压力,应当使用Cookie
HTTP和HTTPS区别
SSL (Security Sockets Layer) 安全套接层
为网络通信提供安全及数据完整性的一种安全协议
是操作系统对外的API,SSL3.0后更名为TLS
采用身份验证和数据加密 来保障网络通信安全和数据完整性
加密方式
对称加密
加密和解密都使用同一个密钥
非对称加密
加密使用的密钥和解密使用的密钥是不同的
哈希算法
将任意长度的数据转化为固定长度的值,算法不可逆
数字签名
证明某个消息或者文件是某个人发出/认同的
HTTPS数据传输流程
浏览器将支持的加密算法信息发送给服务器
服务器选择一套浏览器支持的加密算法,以证书的方式回发给浏览器
浏览器验证证书的合法性,并结合证书公钥加密信息发送给服务器
服务器使用私钥解密信息,验证哈希,加密响应消息回发浏览器
浏览器解密响应消息,并对消息进行验真,之后进行加密交互数据
HTTP 和 HTTPS区别
HTTPS需要到CA申请证书,HTTP不需要
HTTPS密文传输,HTTP 明文传输
连接方式不同,HTTPS 默认使用的是443端口,HTTP 默认使用80端口
HTTPS= HTTP + 加密 + 认证 + 完整性保护,比较HTTP 安全
HTTPS 真的安全吗
浏览器默认填充 http://,请求需要跳转,有被劫持的风险
HSTS(HTTP Strict Transport Security)优化
socket
socket 是对 TCP/IP 协议的抽象
socket 通信流程
数据库
数据库架构
优化你的索引-二叉查找树
特点
所有非叶子节点的最多有两个子节点
每个节点存储一个关键字
非叶子节点的左指针小于其关键字的子树,右指针指向大于其关键字的子树
TODO
优化你的索引-B树
特点
多路搜索树,不一定是二叉的
定义任意一个叶子节点最多只有M个儿子,且M>2;
根节点的的儿子树为[2,M]
除根节点以外的节点的儿子树为[M/2,M]
每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)
非叶子结点的关键字个数=指向儿子的指针个数-1;
非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
非叶子结点的指针:
P[1], P[2], …, P[M] ;其中 P[1] 指向关键字小于 K[1] 的
子树,
P[M] 指向关键字大于 K[M-1] 的子树,其它 P[i] 指向关键字属于 (K[i-1], K[i]) 的子树;
所有叶子结点位于同一层;
B树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点;
1.关键字集合分布在整颗树中; 2.任何一个关键字出现且只出现在一个结点中; 3.搜索有可能在非叶子结点结束; 4.其搜索性能等价于在关键字全集内做一次二分查找; 5.自动层次控制; 由于限制了除根结点以外的非叶子结点,至少含有M/2个儿子,确保了结点的至少利用率,其最底搜索性能为: 其中,M为设定的非叶子结点最多子树个数,N为关键字总数; 所以B树的性能总是等价于二分查找(与M值无关),也就没有B树平衡的问题; 由于M/2的限制,在插入结点时,如果结点已满,需要将结点分裂为两个各占M/2的结点;删除结点时,需将两个不足M/2的兄弟结点合并;
优化你的索引-B+树
特点
是B树的变体
非叶子节点的树指针与关键字个数相同
非叶子节点的子树指针p[i],指向关键字值属于[K[i], K[i+1])的子树,B+ 树是 开区间
为所有的叶子节点添加一个链指针
所有的关键字都在叶子节点出现
B+树的搜索只有到 叶子节点才会命中,(B树可以在非叶子节点命中),性能等价于在关键字 全集 做一次 二分查找
所有的关键字都出现在叶子节点的链表中(稠密索引),且链表是有序的
不可能在非叶子节点命中
非叶子节点相当于 叶子节点的索引 (稀疏索引),叶子节点相当于存储关键字的 数据层
更适合文件索引系统
查询效率稳定
B*树 是 B+树的变种,新建节点少,空间利用率高
优化你的索引-Hash以及BitMap
仅仅能满足 “=”,“IN”,不能使用范围查询
无法用来做数据的排序操作
不能利用部分索引键进行查询
密集索引和稀疏索引
密集索引
密集索引文件中的每个索引码都对应一个索引值
INNODB
若有主键被定义,则主键作为密集索引
若没有主键定义,该表的第一个唯一非空索引作为密集索引
若无主键,也无非空索引,innodb内部会生成一个隐藏主键(密集索引)
非主键索引存储相关键位和其对应的主键值,包含两次查找
稀疏索引
稀疏索引文件只为索引码的某些值建立索引项
MyISAM
索引优化问题-优化sql
查看慢日志定位慢sql
使用 show variable like '%quer%' 查看配置信息
slow_query_log 慢查询日志开启状态
set global slow_query_log = ON
long_query_time 慢查询时间多长会被写入
set global long_query_time =1
slow_query_log_file 慢日志文件位置
使用explain等工具分析sql
explain主要关键词
id
select 查询的 子查询序列号,表示select 子句 或操作表的顺序。id相同则从上到下。id不同则 id大的执行优先级更高
select_type
表示那种类型查询,主要用于区分 普通查询,联合查询,子查询等复杂查询
SIMPLE
PRIMARY
SUBQUERY
DERIVED
UNION
UNION RESULT
table
当前执行表
possible_keys
当前表存在的索引,不一定使用
key
实际使用的索引,如果为NULL,则表示没使用索引(包括没建索引或者索引失效)
key_len
索引中使用的字节数,可以通过该列计算查询中使用的索引长度,在不损失精度的情况下,长度越短越好。key_len表示索引字段的最大可能长度,不代表实际长度。即key_len是通过表定义计算得出的,非表查询得出的。
ref
表示索引的哪一列被使用了,如果可能的话,最好是一个常数。那些列或者常量被用于查找索引列上的值。
type
查询从最优到最差排序
system>const>eq_ref>ref>fulltext>ref_or_null>index_merage>unique_subquery>index_subquery>range>index>all
当出现 index、all时,表示走的是全表扫描
extral
extral如果出现以下情况意味着MYSQL根本不能使用索引,效率会受大影响。尽可能对此进行优化
Using filesort
表示MYSQL会对结果使用一个外部排序,而不是从表里按索引次序读取到内容。可能是磁盘或者内存上进行排序。MYSQL中无法利用索引完成的排序操作称为 “文件排序”
Using temporary
表示MYSQL 对查询结果使用临时表。常用于排序 order by 和分组查询 group by
rows
根据表统计信息及索引选用情况,大致估算出所需的记录查询需要读取的行数,这个行数越少越好。
修改sql或者尽量让sql走索引
索引最左匹配原因
最左匹配原则:MYSQL会一直向右匹配,一直匹配到范围查询就停止匹配。如建立a,b,c,d顺序索引,条件 a=1 and b=2 and c>1 and d =2, d是不会用到索引的,如果建立abdc 顺序索引,则abcd都可以用到
= 和 in 可以乱序,msql查询优化器 会帮你优化索引成可识别的形式
原因
索引底层是B+树,联合索引当然还是一个B+树,只不过联合索引的键值不止一个,而是多个。构建一个B+树只能根据一个值来构建,因此数据库根据最左字段来构建B+树
匹配的几种情况,假如建立联合索引(a,b,c)
全值匹配查询时
select * from table_name where a = '1' and b = '2' and c = '3' select * from table_name where b = '2' and a = '1' and c = '3' select * from table_name where c = '3' and b = '2' and a = '1' ......
用到的索引,子条件顺序调换不影响结果,Mysql查询优化器会自动优化查询顺序
匹配到最左列时
select * from table_name where a = '1' select * from table_name where a = '1' and b = '2' select * from table_name where a = '1' and b = '2' and c = '3'
都从最左列开始 连续匹配,用到了索引
select * from table_name where b = '2' select * from table_name where c = '3'select * from table_name where b = '1' and c = '3'
这些都没有从最左边开始,最后查询没有用到索引,走全表扫描
select * from table_name where a = '1' and c = '3'
不连续时,只用到了a列的索引,b和c都么有用到
匹配前缀
select * from table_name where a like 'As%'; //前缀都是排好序的,走索引查询select * from table_name where a like '%As'//全表查询select * from table_name where a like '%As%'//全表查询
如果是a是字符型的,比较规则是 比较字符串的第一个字符,第一个字符串的字符比较小这个字符串就比较小,如果第一个字符相同,则比较第二个字符,以此类推。所以 前缀匹配走的是索引,后缀匹配 和中缀匹配 走的全表扫描
匹配范围值
select * from table_name where a > 1 and a < 3
可以对最左列进行范围查询,走索引
select * from table_name where a > 1 and a < 3 and b > 1;
多列范围查询时,只有最左列 a范围查询可以用到索引,在1<a<3范围内,b是无序的,不能用索引。找到1<a<3的记录后,只能根据条件b>1进行逐条过滤。
精确匹配一列并范围查找一列
select * from table_name where a = 1 and b > 3;
a=1情况下 b是有序的,进行范围查询走的是联合索引
排序
一般情况下 mysql 用到文件排序,比较慢,如果order by 里有索引,可以省去文件排序步骤
select * from table_name order by a,b,c limit 10;
用到索引
select * from table_name order by b,c,a limit 10;
颠倒顺序的用不到索引
select * from table_name order by a limit 10;select * from table_name order by a,b limit 10;
用到部分索引
select * from table_name where a =1 order by b,c limit 10;
联合索引最左列是常量,后面排序可以走索引
锁模块MyISAM和INNODB区别
MyISAM 仅支持标级锁
InnoDB 支持 行级锁,也支持 表级锁
MyISAM适合的场景
频繁的全表count
MyISAM用一个变量保存了count值
InnoDB每次需要重新计算
对数据进行增删改不高,查询特别频繁
没有事物的场景
InnoDB适合的场景
数据增删改都比较频繁
可靠性要求比较高,要求支持事物
数据库锁的分类
按照锁的粒度划分, 可分为 表级锁,行级锁,页级锁
按照锁级别划分,可分为 共享锁,排他锁
按照枷锁方式划分,可分为 自动锁,显示锁
按照操作划分,可分为DML锁, DDL 锁
按照使用方式划分,可分为 乐观锁,悲观锁
事务的四大特性ACID
Automic 原子性
Consistency 一致性
Isolation 隔离性
Durability 持久性
锁模块并发产生的问题-事务隔离机制
更新丢失
mysql的所有事务隔离级别在数据库层面均可避免
脏读
READ_COMMITTED事务隔离级别及以上 可以避免
脏读的产生
READ_UNCOMMITTED 事务隔离级别下,两个事务同时对同一行进行修改,一旦其中一个事务发生了ROLL BACK(回滚),就会导致另外一个事务产生脏读。(事务A读到了事务B提交失败的数据,实际数据已经回滚)
不可重复读
不可重复读产生
事务A 多次读取一行记录,事务B,事务C 分别修改这行记录,在事务B,事务C提交事务之后,A事务还没有提交,此时事务A 先后读取事务B,事务C提交后的记录,该行记录 已经被修改,读取到的记录是不同的
幻读
幻读产生
事务A读取数据若干行,事务B以插入/删除方式修改了事务A的结果集,事务A再次操作发现了还没有操作过的数据行,就像幻觉一样,(出现了不该出现的记录)
注意,INNODB 的可重复读,采用了很巧妙的方式避免了幻读
表象快照读(非阻塞读),伪MVCC
内在 next-key锁+ gap锁
需要了解下Redo Log
当前读和快照读
当前读
select *** lock in share mode, select*** for update
update,delete,insert
快找读
不加锁的非阻塞读,select
RC,RR 级别下的InnoDB如何实现非阻塞读
数据行里的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID 字段
undo 日志
read view
RR如何避免幻读
InnoDB 会加一个 GAP锁+ next-key锁,避免了幻读
对主键索引或者唯一索引会使用GAP锁吗
如果where条件全部命中,则不会使用GAP锁,只会添加记录锁
在RR隔离级别下,Gap锁会用在非唯一索引,或者不走索引的当前读中
其他-关键语法
GROUP BY
HAVING
统计相关
COUNT,SUMMAX,MIN,AVG
Redis
redis和Memcahe区别
memcache
代码层次类似Hash
支持简单数据类型
不支持数据持久化存储
不支持主从
不支持分片
redis
数据类型丰富
支持数据 磁盘持久化存储
支持主从
支持分片
为什么redis 这么快 10W+ QPS(query per second,没秒内查询次数)
完全基于内存,绝大部分请求存粹是内存操作,执行效率高
数据结构简单,数据操作也简单
采用单线程,单线程也能处理高并发请求,想多核可以启动多实例
使用多路 I/O复用模型,非阻塞I/O
Redis采用的I/O多路复用函数
epoll/kqueue/evport/select
因地制宜,根据操作系统不同选择不同的函数
优先选择时间复杂度O(1)的I/O多路复用函数作为底层实现
以时间复杂度为O(n)的select 作为保底
基于react设计模式监听I/O事件
常用的数据类型
String
二进制安全,可以存储任何类型数据
最大存储512MB
使用场景
可以包含任何数据,比如jpg图片或者序列化的对象,一个键最大能存储512M
举例:你想干啥就干啥
Hash
存储key-value
特别适合存储对象
使用场景
特别适合存储对象,并且可以更新某个字段
举例 存储、读取、修改用户属性
List
简单的字符串列表
按照插入顺序排序
插入队头,左边
插入队尾,右边
列表最多存储 2^32 -1 个元素,(4294967295, 每个列表可存储40多亿)
使用场景
增删快,提供了操作某一段元素的API
举例 1,最新消息排行等功能(比如朋友圈的时间线) 2,消息队列
set集合
是String 的无序集合
基于哈希表实现,所以添加、删除、查找的复杂度始终是 O(1)
举例 1、添加、删除,查找的复杂度都是O(1) 2、为集合提供了求交集、并集、差集等操作
zset(sorted set) 有序集合
和set一样,是string 类型的元素集合,且不允许重复的成员
每个元素会关联一个 double类型的分数,redis通过分数 对集合中的 成员进行从小到大排序
使用场景
数据插入集合时,已经进行天然排序
举例 1、排行榜 2、带权重的消息队列
Geo
地理位置
包含经纬度信息
举例 1.附近的人
如何从海量的key中查找某固定前缀的key
keys pattern 命令
会返回全部匹配的key
阻塞的,当查询大量的key 会对正在运行的服务造成影响,因为一次性返回数据量比较大的时候会使得服务变得卡顿
SCAN cursor [MATCH pttern] [Count count]命令
SCAN是无阻塞模式的提取指令列表,每次只会返回少量元素
cursor指的是游标,MATCH pattern值得是指令,count参数指定返回的数据个数,但是count并无法严格控制数量。
scan 0 match k1* count 10 //该命令指的是大概率的返回数量为count的且以k1开头的数据
此命令只有当以0作为游标的开始依次进行新的迭代,直至命令返回的游标为0为止,即当返回的游标为0的时候表示整个迭代过程都完成了。
SCAN增量式迭代命令,并不能保证每次执行都返回某个给定数量的元素,可能为0个,当时命令返回的游标不是零,则应用程序就会继续使用上一个游标进行迭代,直至游标值为0,对于较大的数据集每次可能返回数十个数据,对于较小的数据集可能会直接返回所有数据集。
SCAN命令查出的所有命令可能对存在重复的值,所以我们可以利用hashset来实现数据去重。
如何实现分布式锁
分布式锁特点
互斥性
同时只有一个线程持有锁
可重入性
同一节点的同一线程获取锁后能再次获取锁
锁超时
和JUC包中的锁一样支持锁超时,防止死锁
高性能和高可用
加锁和解锁要保证高效,同时也要保证高可用,防止分布式锁失效
具备阻塞和非阻塞特性
能够及时从阻塞状态中被唤醒
分布式锁实现方式
基于数据库
自己查看实现
基于redis
基于zoolKeeper
自己查看实现
redis实现方式
加锁实现方式
利用setnx+expire命令 (错误的做法)
因为setnx + expire 操作不是原子的
public boolean tryLock(String key,String requset,int timeout) { Long result = jedis.setnx(key, requset); // result = 1时,设置成功,否则设置失败 if (result == 1L) { return jedis.expire(key, timeout) == 1L; } else { return false; }}
使用Lua脚本(包含setnx和expire两条指令)
public boolean tryLock_with_lua(String key, String UniqueId, int seconds) { String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" + "redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end"; List<String> keys = new ArrayList<>(); List<String> values = new ArrayList<>(); keys.add(key); values.add(UniqueId); values.add(String.valueOf(seconds)); Object result = jedis.eval(lua_scripts, keys, values); //判断是否成功 return result.equals(1L);}
使用 set key value [EX seconds][PX milliseconds][NX|XX] 命令 (正确做法)
Redis在 2.6.12 版本开始,为 SET 命令增加一系列选项:SET key value[EX seconds][PX milliseconds][NX|XX]EX seconds: 设定过期时间,单位为秒PX milliseconds: 设定过期时间,单位为毫秒NX: 仅当key不存在时设置值XX: 仅当key存在时设置值
public boolean tryLock_with_set(String key, String UniqueId, int seconds) { return "OK".equals(jedis.set(key, UniqueId, "NX", "EX", seconds));}
value必须要具有唯一性,我们可以用UUID来做,设置随机字符串保证唯一性,至于为什么要保证唯一性?假如value不是随机字符串,而是一个固定值,那么就可能存在下面的问题: 1.客户端1获取锁成功2.客户端1在某个操作上阻塞了太长时间3.设置的key过期了,锁自动释放了4.客户端2获取到了对应同一个资源的锁5.客户端1从阻塞中恢复过来,因为value值一样,所以执行释放锁操作时就会释放掉客户端2持有的锁,这样就会造成问题
所以通常来说,在释放锁时,我们需要对value进行验证
使用 set key value [EX seconds][PX milliseconds][NX|XX] 命令看上去很ok,但在redis集群模式下,还是有可能出现问题的
客户端A在Master上拿到锁,此时 Master 还没有将key同步到slave节点,Master节点挂了,某个Slave被选举为Master,此时客户端再来获取锁同样会成功,会出现多个客户端都拿到锁的局面。
解锁实现方式
解锁我们需要验证value的值,不能直接粗暴的使用del key,因为这样任何一个客户端都可以解锁。所以解锁时,我们要校验value值是否是自己的,基于value值来判断。
public boolean releaseLock_with_lua(String key,String value) { String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " + "return redis.call('del',KEYS[1]) else return 0 end"; return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L);}
redssion
redlock
实现原理
获取当前Unix时间,以毫秒为单位。
依次尝试从5个实例,使用相同的key和
具有唯一性的value (例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。
客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。
当且仅当从大多数 (N/2+1,这里是3个节点) 的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功
如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在
所有的Redis实例上进行解锁 (即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
使用方式
引入pom
<!-- https://mvnrepository.com/artifact/org.redisson/redisson --><dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.3.2</version></dependency>
获取锁
获取锁的代码为redLock.tryLock()或者redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS),两者的最终核心源码都是下面这段代码,只不过前者获取锁的默认租约时间(leaseTime)是LOCK_EXPIRATION_INTERVAL_SECONDS,即30s:
Config config = new Config();config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389") .setMasterName("masterName") .setPassword("password").setDatabase(0);RedissonClient redissonClient = Redisson.create(config);// 还可以getFairLock(), getReadWriteLock()RLock redLock = redissonClient.getLock("REDLOCK_KEY");boolean isLock;try { isLock = redLock.tryLock(); // 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。 isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS); if (isLock) { //TODO if get lock success, do something; }} catch (Exception e) {} finally { // 无论如何, 最后都要解锁 redLock.unlock();}
KEYS[1]就是Collections.singletonList(getName()),表示分布式锁的key,即REDLOCK_KEY;ARGV[1]就是internalLockLeaseTime,即锁的租约时间,默认30s;ARGV[2]就是getLockName(threadId),是获取锁时set的唯一值,即UUID+threadId:
唯一ID
实现分布式锁的一个非常重要的点就是set的value要具有唯一性,redisson的value是怎样保证value的唯一性呢?答案是UUID+threadId
protected final UUID id = UUID.randomUUID();String getLockName(long threadId) { return id + ":" + threadId;}
解锁
释放锁的代码为redLock.unlock()
如何实现异步队列
使用list作为队列
RPUSH作为生产者生产消息,LPOP作为消费者消费消息
缺点:没有等待队列,有值就直接消费弥补:可以在应用层通过sleep机制去调用LOOP进行重试如果不用sleep机制,可以使用BLPOP key[key..] timeout 阻塞直到队列有消息或者超时
一个生产者对应一个消费者
如何做到生产一次,能让多个消费者消费
pub/sub:主题订阅模式
发送者(pub)发送消息,订阅者(sub)接受消息
订阅者可以订阅任意数量的频道
缺点:消息无状态,无法保证可达
如何实现延时队列
使用sortset
拿时间戳作为score
消息内容作为key调用zadd生产消息
消费者使用zrangeBysocre指令获取N秒之前的数据轮询进行处理
持久化方式RDB (redis database)
原理
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
优势
1.一旦你采用这种方式,你的整个redis库只包含一个文件,这对于文件备份而言是十分完美的。比如你打算每小时同步一下24小时的数据,每天同步下30天数据。通过这样配置,遇到灾难性故障,我们很容易进行数据恢复。
2.对于灾难恢复而言,RDB是一个非常不错的选择,因为我们可以很轻松的将一个单独的文件压缩 再拷贝到其他存储介质上。
3.性能最大化,对于Redis服务进程而言,在开始持久化时,唯一要做的就是fork出一个子进程,其余交给子进程完成持久化操作,极大的避免服务进程进行IO操作
4.相比较AOF机制,如果数据集很大,那么RDB启动效率会更高
缺点
1.如果你想保证数据的高可用性,即最大程度避免数据丢失,RDB不是最好的选择,因为在系统在 特定持久化时间之前出现宕机,那么没来得及保存到磁盘上的数据将会丢失。
2.由于是fork子进程 来协助 进行磁盘持久化操作的,当数据集比较大时,可能会导致服务器停止服务几百毫秒,甚至1秒
配置
Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
bgsave命令执行过程中,只有fork子进程时会阻塞服务器;而对于save命令,整个过程都会阻塞服务器;因此save已基本被废弃,线上环境要杜绝save的使用;
持久化方式AOF (append only file)
原理
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
优势
该机制可以带来更高的安全性,即数据的持久性
提供了三种同步策略
每秒同步
每秒同步也是 异步操作,效率非常高,如果出现服务宕机,那么只会丢失上一秒的数据。
每修改同步
可以理解为同步持久化,效率偏低
不同步
由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
缺点
对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
配置
在Redis的配置文件中存在三种同步方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件。
appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。
appendfsync no #从不同步。高效但是数据不会被持久化。
持久化方式混合模式
redis4.0开始支持该模式
为了解决的问题
redis在重启时通常是加载AOF文件,但加载速度慢因为RDB数据不完整,所以加载AOF开启方式: aof-use-rdb-preamble true开启后,AOF在重写时会直接读取RDB中的内容
运行过程
通过bgrwriteaof完成,不同的是当开启混合持久化后,1 子进程会把内存中的数据以RDB的方式写入aof中,2 把重写缓冲区中的增量命令以AOF方式写入到文件3 将含有RDB个数和AOF格数的AOF数据覆盖旧的AOF文件新的AOF文件中,一部分数据来自RDB文件,一部分来自Redis运行过程时的增量数据
优点
混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。
缺点
兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差
主从同步
数据可以从主服务器向任意从服务器上同步,从服务器可以是关联其他服务器的主服务器。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布 记录。同步对读取操作的可扩展性和数据冗余很有帮助。
工作原理
全量同步
redis 全量同步一般发生在slave 初始化阶段,这时需要将Master上的数据都复制一份
1.从服务器连接主服务器,发送SYNC命令;
2.主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
3.主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
4.从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
5.主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
6.从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求。
增量同步
redis增量同步指的是redis slave初始化完成后,开始正常工作时主服务器发生的写操作同步到从服务器的过程
增量复制 主要是 主服务器每接收一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行相同的写命令。
redis主从同步策略
主从开始链接时候,进行全量同步,同步结束后,进行增量同步
如果有需要,slave可以在任意时刻发起全量同步
redis策略是,先进行增量同步,同步失败在进行全量同步
注意点:当多个slave 断线了,需要重启,重启时会自动发送sync请求主服务器进行全量同步,当多个同时出现的时候,会导致 Master IO 剧增 宕机
Redis Sentinel(哨兵) 架构下高可用
当Master 挂掉后,需要人工将从节点晋升到主节点,同时通知业务方变更主节点地址,对于很对的应用场景这样的故障处理方式是不能接受的。redis 在2.8版本提供了 sentinel 架构解决了这个问题
实现原理
三个定时监控任务
每隔10s,每个 sentinel节点 会向 主节点 和从节点发送 info命令获取最新拓扑结构
每隔2s,每个sentinel 节点会Redis数据节点的_sentinel_:hello 频道 发送 当前sentinel节点 对主节点的判断以及当前sentinel节点的信息,同时每个sentinel节点也会订阅该频道,来了解其他sentinel节点以及它们对主节点的判断
每隔1秒,sentinel会向主节点,从节点发送一次ping 命令做一次心跳检测,来确认这些节点当前是否可达
主观下线
因为每隔一秒,每个Sentinel节点会向主节点、从节点、其余Sentinel节点发送一条ping命令做一次心跳检测,当这些节点超过down-after-milliseconds没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线。
客观下线
当Sentinel主观下线的节点是主节点时,该Sentinel节点会向其他Sentinel节点询问对主节点的判断,当超过<quorum>个数,那么意味着大部分的Sentinel节点都对这个主节点的下线做了同意的判定,于是该Sentinel节点认为主节点确实有问题,这时该Sentinel节点会做出客观下线的决定。
领导者sentinel节点选举
Raft算法假设s1(sentinel-1)最先完成客观下线,它会向其余Sentinel节点发送命令,请求成为领导者;收到命令的Sentinel节点如果没有同意过其他Sentinel节点的请求,那么就会同意s1的请求,否则拒绝;如果s1发现自己的票数已经大于等于某个值,那么它将成为领导者。
故障转移
1.领导者Sentinel节点在从节点中选出一个节点作为新的主节点
2.上述的选取方式是与主节点复制相似度最高的从节点
3.领导者Sentinel节点 让其他的从节点成为新主节点的从节点
4.Sentinel集合会将原来的主节点变为从节点,并对其保持关注,当其恢复后命令它去复制新的主节点
Redis Cluster(集群)下的高可用
实现原理
主观下线
集群中每个节点都会定期向其他节点发送ping消息,接受节点回复ping消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接受节点标记为主观下线(pfail)状态。
客观下线
当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。
假设节点a标记节点b为主观下线,一段时间后节点a通过消息把节点b的状态发送到其他节点,当其他节点收到消息并解析出消息体中含有b的pfail状态,把节点b加入下线报告链表;
当某一节点c收到节点b的pfail状态时,此时有超过一半的槽主节点都标记了节点b为pfail状态时,则标记故障节点b为客观下线;
向集群广播一条pfail消息,通知集群内的所有节点标记故障节点b为客观下线状态并立刻生效,同时通知故障节点b的从节点触发故障转移流程。
故障恢复
资格检查
若从节点与主节点断线时间超过一定时间,则不具备资格
准备选举时间
当从节点符合故障转移资格后,要等待一段选举时间后才开始选举
在故障节点的所有从节点中,复制偏移量最大的那个从节点最先开始(与主节点的数据最一致)进行选举,然后是次大的节点开始选举.....剩下其余的从节点等待到它们的选举时间到达后再进行选举
发起选举
只有持有槽的主节点才具有一张唯一的选票,从从节点收集到N/2 + 1个持有槽的主节点投票时,从节点可以执行替换主节点操作
选举投票
替换主节点
当从节点收集到足够的选票之后,触发替换主节点操作
当前从节点取消复制变为主节点
撤销故障主节点负责的槽,并把这些槽委派给自己
向集群广播自己的pong消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息
Pipeline(管道)
Redis 使用的是客户端-服务器(CS)模型和请求/响应协议的 TCP 服务器。这意味着通常情况下一个请求会遵循以下步骤:
客户端向服务端发送一个查询请求,并监听 Socket 返回,通常是以阻塞模式,等待服务端响应。
服务端处理命令,并将结果返回给客户端。
Redis 客户端与 Redis 服务器之间使用 TCP 协议进行连接,一个客户端可以通过一个 socket 连接发起多个请求命令。每个请求命令发出后 client 通常会阻塞并等待 redis 服务器处理,redis 处理完请求命令后会将结果通过响应报文返回给 client,因此当执行多条命令的时候都需要等待上一条命令执行完毕才能执行
而管道(pipeline)可以一次性发送多条命令并在执行完后一次性将结果返回,pipeline 通过减少客户端与 redis 的通信次数来实现降低往返延时时间,而且 Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。 Pipeline 的默认的同步的个数为53个,也就是说 arges 中累加到53条数据时会把数据提交。其过程如下图所示:client 可以将三个命令放到一个 tcp 报文一起发送,server 则可以将三条命令的处理结果放到一个 tcp 报文返回。
需要注意到是用 pipeline 方式打包命令发送,redis 必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。具体多少合适需要根据具体情况测试。
由于通信会有网络延迟,假如 client 和 server 之间的包传输时间需要0.125秒。那么上面的三个命令6个报文至少需要0.75秒才能完成。这样即使 redis 每秒能处理100个命令,而我们的 client 也只能一秒钟发出四个命令。这显然没有充分利用 redis 的处理能力。
适用场景
有些系统可能对可靠性要求很高,每次操作都需要立马知道这次操作是否成功,是否数据已经写进 redis 了,那这种场景就不适合。
还有的系统,可能是批量的将数据写入 redis,允许一定比例的写入失败,那么这种场景就可以使用了,比如10000条一下进入 redis,可能失败了2条无所谓,后期有补偿机制就行了,比如短信群发这种场景,如果一下群发10000条,按照第一种模式去实现,那这个请求过来,要很久才能给客户端响应,这个延迟就太长了,如果客户端请求设置了超时时间5秒,那肯定就抛出异常了,而且本身群发短信要求实时性也没那么高,这时候用 pipeline 最好了
管道(Pipelining) VS 脚本(Scripting)
大量 pipeline 应用场景可通过 Redis 脚本(Redis 版本 >= 2.6)得到更高效的处理,后者在服务器端执行大量工作。脚本的一大优势是可通过最小的延迟读写数据,让读、计算、写等操作变得非常快(pipeline 在这种情况下不能使用,因为客户端在写命令前需要读命令返回的结果)。
应用程序有时可能在 pipeline 中发送 EVAL 或 EVALSHA 命令。Redis 通过 SCRIPT LOAD 命令(保证 EVALSHA 成功被调用)明确支持这种情况。
redis集群(redis-cluster)
Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。
结构特点
所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
节点的fail是通过集群中超过半数的节点检测失效时才生效。
客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
redis-cluster把所有的物理节点映射到[0-16383]slot上(不一定是平均分配),cluster 负责维护node<->slot<->value。(hash环) 2的14次方
Redis集群预分好16384个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。
redis cluster节点分配
现在我们是三个主节点分别是:A, B, C 三个节点,它们可以是一台机器上的三个端口,也可以是三台不同的服务器。那么,采用哈希槽 (hash slot)的方式来分配16384个slot 的话,它们三个节点分别承担的slot 区间是
节点A覆盖0-5460;
节点B覆盖5461-10922;
节点C覆盖10923-16383.
获取数据
如果存入一个值,按照redis cluster哈希槽的算法: CRC16('key')384 = 6782。 那么就会把这个key 的存储分配到 B 上了。同样,当我连接(A,B,C)任何一个节点想获取'key'这个key时,也会这样的算法,然后内部跳转到B节点上获取数据
新增主节点
新增一个节点D,redis cluster的这种做法是从各个节点的前面各拿取一部分slot到D上
节点A覆盖1365-5460
节点B覆盖6827-10922
节点C覆盖12288-16383
节点D覆盖0-1364,5461-6826,10923-12287
删除主节点
同样删除一个节点也是类似,移动完成后就可以删除这个节点了。
Redis Cluster主从模式
redis cluster 为了保证数据的高可用性,加入了主从模式,一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份,当这个主节点挂掉后,就会有这个从节点选取一个来充当主节点,从而保证集群不会挂掉
Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。
JVM
谈谈你对java的理解
平台无关性
GC
语言特性
面向对象
类库
异常处理
平台无关性如何实现 compile once,run any where
编译时
javac 编译指令
Javap 反编译指令
运行时
Java源码首先被编译成字节码,再由不同的平台的JVM 进行解析,Java在不同平台上运行时不需要重新编译,Java虚拟机执行字节码的时候,把字节码转换成具体平台上的机器指令
JVM为什么不直接将源码转换为机器码去执行
准备工作:每次执行都需要进行各种检查
兼容性: 也可以将其他的语言解析成字节码
JVM如何加载class文件
Java虚拟机
Class Loader
依据特定格式,加载class文件到内存
Execution Engine
负责对命令进行解析
Runtime Data Area
JVM 内存空间结构模型
Native Interface
本地接口,融合不同开发语言的原生库为Java所用
什么是反射
反射机制描述
在运行状态时,对于任意一个类,都能知道这个类的所有的属性和方法;对于任意一个对象,都能任意调用它的方法和属性;这种动态获取信息以及动态调用对象方法的功能 叫做 Java的反射机制
常用的反射相关(自己编码了解)
getName
Method
Field
...
ClassLoader双亲委派机制
类编译到执行的过程
编译器将.java源文件编译成.class字节码文件
ClassLoader 将.class字节码文件转换为JVM中的Class<?>对象
JVM利用Class<?>对象实例化为?对象
什么是ClassLoader
ClassLoader在Java中有着非常重要的作用,主要工作在Class文件装载的阶段,其主要的工作原理是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class文件都是通过ClassLoader进行加载的,ClassLoader负责将Class文件中的二进制数据流装载进系统,然后交给Java虚拟机进行连接,初始化等操作
ClassLoader 种类(四种)
BootstrapClassLoader
C++编写,加载核心库Java.*
ExtClassLoader
Java编写,加载扩展库javax.*
AppClassLoader
Java编写,加载程序所在目录
自定义ClassLoader
Java编写,定制化加载
双亲委派机制
自下而上的检查类是否已经被装载
Custom ClassLoader 去查找是否加载过该类,如果有就直接返回,如果没有则委派给AppClassLoader,以上同理
自上而下的尝试加载类
如果到了Bootstrap ClassLoader 任然没有检查到类,则进行类的加载,顺序从上往下
为什么要用双亲委派机制去加载类
避免多分同样的字节码的加载
loadClass 和forName区别
自己敲下代码
类的加载方式
隐式加载 new 方式
显式加载 loadClass,forName等
java类的装载过程
加载
通过ClassLoader 加载class文件字节码,生成Class对象
链接
校验
检查加载的class正确性和安全性
准备
为类变量设置存储空间并设置类变量的初始值
解析
JVM将常量池内的符号引用转换为直接引用
初始化
执行类变量赋值和静态代码块
Class.forName()得到的是已经初始化完成的
ClassLoader.loadClass()是得到还没有连接的
Java内存模型-线程独占部分
程序计数器 Program Counter Register
当前线程所执行的字节码行号指示器(逻辑)
改变计数器的值来选取下一条需要执行的字节码命令
和线程的关系是 一对一的,及线程私有
只对java计数,如果是Native方法,则计数器的值为 undefind
不会发生内存泄漏,因为是逻辑计数器,非物理计数器
虚拟机栈
Java方法执行的内存模型
包含多个栈帧
栈是后入先出
局部变量表
包含方法执行过程中的所有变量
操作数栈
入栈
出栈
复制
交换
产生消费变量
动态链接
返回地址
。。。
本地方法栈
Java内存模型-线程共享部分
MetaSpace
元空间(MetaSpace) 和 永久代(PermGen JDK7版本之前) 区别
元空间使用本地内存,永久代使用JVM内存
MetaSpace比较PermGen的优势
字符串常量池存在永久代种,容易出现性能问题和内存溢出
类和方法的大小难以确定,给永久代指定大小带来困难
永久代会给GC带来不必要的复杂性
方便HotSpot与其他JVM如Jrockit的集成
堆 (Heap)
对象实例的分配区域
子主题
Java线程模型-常考类型
JVM三大性能调优参数
-Xms
堆得初始值
-Xmx
堆能达到的最大值
-Xss
规定了每个线程虚拟机栈(堆栈)的大小
Java内存模型中,堆和栈的区别(内存分配策略)
Java的内存分配策略
静态存储
编译时确定每个数据目标在运行时的空间需求
栈式存储
数据区需求在编译时未知,运行时模块入口前确定
堆式存储
编译时或运行模块入口前时都无法确定,动态分配
Java内存模型中堆和栈的区别
重在理解
联系:引用对象,数组时,栈里定义的变量保存堆中目标的首地址
区别
管理方式
栈自动释放
堆需要GC
空间大小
栈比堆小
碎片相关
栈产生的碎片远小于堆
分配方式
栈支持静态分配和动态分配
堆仅支持动态分配
效率
栈的效率比堆高
GC
Java垃圾回收-标记算法
判定对象是否为垃圾的算法
引用计数器算法
通过判断对象引用数量来决定对象是否可以被回收
每个对象都有一个引用计数器,被引用+1,完成则减1
任何计数器为0的对象实例,是可以当作垃圾被收集
优点: 执行效率较高,程序执行影响较小
缺点:无法检测初循环依赖的情况,会导致内存泄漏
可达性算法
通过判断对象的 引用链是否可达来决定对象是否可以被回收
可以作为GC Root的对象
虚拟机栈中引用的对象(栈帧中的本地变量列表)
方法区的常量引用对象
方法区中的 类静态属性引用的对象
本地方法栈中JNI(Native 方法) 的引用对象
活跃线程的引用对象
对象被判定位垃圾的标准
没有被其他对象引用
Java垃圾回收-回收算法
聊聊你了解垃圾回收方法
标记-清除算法
标记-清除算法
先标记,后清除, 不移动对象优点是 执行快缺点是 会导致内存碎片化,当需要分配较大对象时,找不到足够大的连续内存,而触发另一次的GC
复制算法
分为对象面和空闲面
对象在对象面创建
清理后,存活的对象会从对象面 复制到 空闲面
将对象面的所有对象清除
适用于年轻代
优点:1.解决碎片化问题2.顺序分配内存,简单高效3.适用于对象存活率低的场景
标记-整理算法
避免内存的不连续性
不用设置两块互换
适用于对象存活率较高的场景
适合老年代
分代收集算法
垃圾回收算法的组合拳
按照对象的生命周期不同,划分不同区域采用不同的垃圾回收算法
目的:提高JVM的回收效率
年轻代-尽可能的快速地收集掉那些生命周期短的对象
内存空间划分Eden区两个 Survivor区
每次使用一个 Eden区 和一个 Survivor区
每次触发一次 minor GC,年龄就会加1
当年龄 默认到 15岁时对象会 进入 老年代
可以通过 -XX:MaxTenuringThreshold 调整 老年代年龄
如果创建的对象 比较大,Enden 或者Survivor 放不下,也会直接进入老年代
对象如何晋升老年代
经历一定的Minor 次数依然存活的对象
Survivor 放不下的对象
Enden区放不下时会直接出发一次 MinorGC,Eden区的对象会被清空
新生成的大对象 通过 -XX: +PretenuerSizeThreshold 控制 大对象大小
常用的调优参数
-XX: SurvivorRatio
Eden区 和 Surivor的比值, 默认 8:1
-XX:NewRatio
老年代和 年轻代 内存大小的比例
-XX:MaxTenuringThreshold
对象从年轻代晋升到老年代经过GC次数 的最大阈值
老年代
清理算法-整理算法
FullGC
Full GC 比 Major GC 慢,但是执行频率低
触发Full GC 条件
老年代空间不足
永久代的空间不足(JDK7之前)
CMS GC 出现promotion failed,concurrent mode failure
Minor GC 晋升到老年代的平均大小大于老年代的剩余空间
在程序中System.gc() 显示调用,提醒JVM回收年轻代和老年代
Major GC
Java垃圾回收-新生代垃圾收集器
Stop-the-World
JVM由于要执行GC而停止了程序的执行
任何GC算法中都会发生
多数GC优化通过减少Stop-the-World发生的时间来提高程序性能
Safepoint 安全点
分析过程中对象引用关系不会发生变化的点
产生Savepoint的地方:方法调用、跳出循环、异常跳转等
安全点选择得适中,选太少让GC等待时间太长,太多会增加运行程序负荷
JVM的运行模式
Server
重量级虚拟机,对程序做了更多的优化。 启动较慢,运行较快
Client
轻量级 启动较快,运行较慢
年轻代常见的垃圾收集器
垃圾收集器之间关系
垃圾收集器之间有连线表示可以搭配使用
Serial收集器 (-XX:UseSerialGC,复制算法)
单线程收集,进行垃圾收集时,必须暂停所有的工作线程
简单 高效,Client模式下默认的年轻代收集器
ParNew收集器 (-XX:+UseParNewGC,复制算法)
多线程收集,其余行为,和特点,和 Serial收集器一样
单核执行效率不如Serial,在多核下执行才有优势
Parallel Scavenge 收集器 (-XX:UseParallelGC,复制算法)
先了解 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
比起关注用户线程停顿时间,更关注 系统的吞吐量
在多核下执行才有优势,Server模式下默认的年轻代收集器
自适应调节策略 (-XX:UseAdaptiveSizePolicy)
把内存调优任务交给虚拟机去完成
Java垃圾回收-老年代垃圾收集器
老年代常用的垃圾收集器
Serial Old收集器 (-XX:UseSerialOldGC,标记-整理算法)
多线程,吞吐量优先
SMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)
垃圾回收线程几乎可以和工作线程同时工作, 是几乎,不是完全,尽可能缩短了停顿时间
初始化标记:Stop-the-World
需要短暂的暂停
并发标记:并发追溯标记,程序不回停顿
并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
重新标记:暂停虚拟机,扫描CMS堆中剩余的对象
需要短暂的暂停
并发清理:清理垃圾对象,程序不会停顿
并发重置:重置CMS收集器的数据结构
CMS收集器执行流程
G1收集器(-XX:+UseG1GC,复制+标记-整理算法)
Garbage First 收集器的特点
并行和并发
分代收集
空间整合
可预测的停顿
将整个Java内存划分为多个大小相等的Region
年轻代和老年代不再物理隔离
JDK11新出的垃圾收集器 Epsilon GC
JDK11新出的垃圾收集器 ZGC
Java垃圾收集器-常考问题
Object 的 finalize()方法的作用是否与C++的析构函数作用相同
与C++的析构函数不同,析构函数的调用确定,而它的是不确定的。
将未被引用的对象放置于F-Queue 队列
方法执行随时可能被终止
给予独享最后一次的重生机会
Java中的强引用,软引用,弱引用,虚引用有什么作用
强引用(Strong Reference)
最普片的引用: Object = new Object()
当内存空间不足时,会抛出OutOfMemoryError 来终止程序,也不回回收具有强引用的对象
通过将对象设置为 null 来弱化引用,使其被回收
软引用 ( Soft Reference)
对象处于有用但非必须的状态
只有当内存空间不足时,GC会回收该引用的对象的内存
可以用来实现内存敏感的告诉缓存
String str = new String("abc");//强引用SfotReferenct<String> softRef = new SoftReference<String>(Str);//软引用
弱引用(Weak Reference)
非必须的对象,比软引用更弱一些
GC 时会被回收
被回收的概率也不大,因为GC线程优先级比较低
适用于引用偶尔被使用且不影响垃圾收集的对象
String str = new String("abc");//强引用WeakReferenct<String> weakRef = new WeakReference<String>(Str);//弱引用
虚引用 (PhantomReference)
不会决定对象的生命周期
任何时候都可能被垃圾收集器回收
跟踪对象被垃圾收集器回收的活动,起哨兵作用
必须和引用队列ReferenceQueue联合使用
String str = new String("abc");ReferenceQueue queue = new ReferenceQueue();PhantomReference ref = new PhantomReference(str,queue);
引用队列(ReferenceQueue)
无实际存储结构,存储逻辑依赖内部节点之间的关系来表达
存储关联的且被GC的软引用、弱引用以及虚引用
多线程和并发
进程和线程的区别
进程是资源分配的最小单位
线程是CPU调度的的最小单位
所有与进程相关的资源,都会被记录在PCB当中
进程是抢占处理机的调度单位,线程属于某个进程,共享其资源
线程只由堆栈寄存器,程序计数器,和TCB组成
线程不能看作独立应用,进程是独立应用
进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
线程没有独立地址空间,多进程程序比多线程程序健壮
进程的切换比线程的切换开销大
线程的start 和Run方法区别
Thread.run方法会使用当前方法执行
Thread.start方法将会新建线程去执行
Thread 和 Runable 区别
自己敲下代码
Runable是接口
Thread是类 实现了Runable 接口
因为类的单一继承原则,推荐使用Runnable接口
如何给riun方法传参
构造参数传参
成员变量传参
回调函数传参
如何实现处理线程的返回值
主线程等待法
默认主线程调用了子线程的Thread.start方法,会立即执行下面代码
在主线程设置while(true),直到拿到子线程的返回值,线程休眠
实现方式比较简单缺点是需要自己实现循环等待的逻辑当需要等待的变量多时候,代码会显得臃肿
使用Thread 的Join 方法
Join会阻塞当前线程以等待子线程执行完毕
通过Callable接口实现
通过FeatureTask获取
通过线程池获取
线程的状态
new 新建
创建后尚未启动的线程状态
进入方式:new 之后,start 之前
runnable 可运行
Running/Ready
Running:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。
1.就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。2.调用线程的start()方法,此线程进入就绪状态。3.当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。4.当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。5.当线程时间片用完了,调用当前线程的yeid()方法,当前线程会进入就绪状态6.锁池里线程拿到对象锁后,进入就绪状态
blocked 阻塞
等待获取排他锁
waiting 无限等待
不会被分配CPU执行时间 ,需要被显示的唤醒
Timed Waiting 限期等待
过一段时间后会由系统自动唤醒
terminated
已终止状态,线程已经结束运行
1.当一个线程run方法执行结束,或者main函数执行完毕后,我们认为这个线程就是终止了。这个线程对象也许是活的,但是已经不再是一个单独执行的线程。线程一旦终止了,就不能复生。2.在一个已终止的线程上调用start方法,会抛出java.lang.IllegalThreadStateExecption 异常
sleep和wait区别
sleep()
Thread.sleep()
sleep方法 可以在任何地方被使用
sleep只会让出CPU,不会导致锁行为改变
wait()
Object.wait()
wait只能在synchronized方法 或者 synchronized代码块中使用
不仅让出CPU,还会释放当前已经占有的同步资源锁
notify 和notifyAll区别
锁池和等待池
锁池
假设线程A已经拥有了某个对象(不是)的锁,,而其他线程B、C想调用这个对象synchronized方法或者 synchronized 代码块,由于B、C线程 进入对象synchronized方法(或者代码块)时必须要持有该对象锁的拥有权,此时锁被A持有,那么B、C线程会被阻塞,进入一个地方去等待锁的释放,这个地方便是对象的锁池
等待吃
假设线程A调用了某个对象的wait()方法,线程A会释放该对象的锁,此时A会进入该对象的等待池中,进入到等待池的线程,不会去竞争该对象的锁
notify
会随机选择等待池中的 某一线程,进入锁池中 去竞争获取锁的机会
notifyAll
notifyAll会让所有等待池中的线程 进入 锁池中 去竞争获取锁的机会
yield 出让函数
当调用Thread.yield()函数时,会给线程调度器一个暗示,表示当前线程愿意让出CPU,但是线程调度器可能会忽略这个暗示
interrupt 中断函数
1.调用 interrupt() ,通知线程应该中断了
如果线程处于阻塞状态,则会立即退出被阻塞状态,并且抛出一个InterruptedException
目前使用方法
需要被调用的线程配合中断
正常执行任务时,需要经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程
如果处于正常活动的状态,那么将线程的中断标志位设置位true,被设置的线程将正常运行,不受影响
多线程和并发-原理
synchronized
线程安全问题诱因
存在共享数据(又称作离线资源)
存在多个线程共同操作这些共享数据
解决办法:同时只允许一个线程在操作共享资源,其他线程必须等待该线程处理完,才能操作共享资源
互斥锁的特性
互斥性
同一时间内只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间内只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性
可见性
必须确保在锁被释放之前,对共享变量所作的修改,对于随后要操作该变量的线程 时可见的,即在获得锁的时应获得最新共享变量的值,否则另一个线程肯呢个在本地缓存的某个副本继续操作,从而引起不一致
synchronized锁的不是代码,锁的时对象
获取对象锁的两种方法
同步代码块
同步代码块,synchronized(this),synchronized(实例对象),锁的时括号内的实例对象
同步非静态方法
synchronized method,锁的时当前实例对象
根据获取锁的分类
获取对象锁
同步代码块
synchronized(this),synchronized(类实例对象),锁的是()中的实例对象
同步非静态方法
synchronized method 锁的是当前对象的实例
获取类锁
同步代码块 synchronized(类.class),锁的是小括号内的 Class对象
同步静态方法 synchronized static method 锁的是当前类对象(Class对象)
synchronized底层实现原理
纯理论,需要死记硬背
对象在内存中的布局
对象头
Mark World
默认存储对象的hashCode,分代年龄。锁类型,锁标志位等信息
锁状态
无锁状态
轻量级锁
重量级锁
GC标记
偏向锁
Class Metadata Address
类型指针指向对象的类元数据,JVM通过该指针确定对象是 哪个类的数据
实例数据
对齐填充
什么是重入
从互斥锁的设计上来说,当一个线程试图进入另一个线程持有的对象锁的临界资源是,将会处于阻塞状态。当一个线程再次请求自己持有的对象锁的临界资源是时,这种情况属于重入
为什么会对synchronized 嗤之以鼻
早期版本中,synchronized 属于重量级锁,依赖Mutex Lock实现
线程之间切换需要从用户态转换到内核态,开销较大
Java6 之后synchronized 性能有较大的提升
自旋锁
自旋锁
许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
通过让线程执行忙循环等待锁的释放,不让出CPU
缺点:若锁被其他线程长时间占用,会带来许多性能上的开销
自适应自旋锁
自旋时间不再固定
由前一次在同一个锁上面自旋时间 和锁的拥有者的状态来定
锁消除
更彻底的优化
JIT编译时,会进行上下文扫描,去除不可能存在竞争的锁
锁粗化
通过扩大锁的范围,避免反复的加锁和解锁
synchronized锁四种状态
无锁
偏向锁
减少同一线程获取锁的代价。大多数情况下,锁资源不存在多线程竞争,总是由同一线程多次获得和
核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时的Mark Word的结构就变成了偏向结构。当该线程再次请求锁时,无需在做任何同步动作,即获取锁的过程只需检查Mark Word 锁的标记位时 偏向锁,以及当前线程的Id 等于 mark word 的ThreadID 即可,这样可以省去大量有关于锁的申请操作
轻量级锁
轻量级锁由偏向锁升级而来。当第二个线程加入锁竞争的时候,偏向锁就会升级为轻量级锁
适应场景:线程交替执行同步代码块若存在同一时间访问同一锁情况,则会膨胀为重量级锁
重量级锁
synchronized 和 ReenTrantLock区别
Reentrant:Lock (再入锁) 介绍
位于 j.U.C 包下
和 CountDownLatch、FutureTask、Semaphore 一样基于AQS实现
能够实现比synchronized更细颗粒度的控制,如控制 fairness
调用lock()后,必须调用unlock()解锁
性能未必比synchronized高,并且也是可重入的
ReentrantLock公平性的设置
创建公平锁例子ReentrantLock fairLock = new ReetrantLock(true);
参数为true 时,倾向于将锁赋予等待时间最久的线程
公平锁
获取锁的顺序按先后调用lock方法的顺序(慎用)
非公平锁
抢占的顺序不一定,看运气
公平锁 和非公平锁
synchronized是非公平锁
ReentrantLock 将锁对象化
判断是否有线程,或者某个特定线程,在排队等待获取锁
带超时的获取锁的尝试
感知有没有成功获取锁
能否将wait/notify/notifyAll对象化
java.util.concurrent.locks.Condition
ArrayBlockingQueue 底层数组实现的 线程安全的,有界的 阻塞队列
总结
synchronized是关键字,ReentrantLock是类
ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
ReentrantLock可以获取各种锁的信息
ReentrantLock可以灵活的实现多路通知
机制: sync 操作的是对象头的 Mark Word,lock 调用 Unsafe类的park()方法
jmm的内存可见性
什么是Java内存模型中的happen-before
Java的内存模型JMM
Java的内存模型(Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或者规范,通过对这组规范定义了程序的各个变量(包括实例字段,静态字段 和构成数组对象的元素)的访问方式。
JMM中的主内存
存储Java实例对象
包括成员变量、类信息、常亮、、静态变量等
属于数据共享的区域,多线程并发操作时会引发线程安全问题
JMM的工作内存
存储当前方法的所有的本地变量信息,本地变量对其他线程不可见
字节码行号指示器、Native方法信息
属于线程私有数据区域,不存在线程安全问题
JMM和Java内存区域划分是不同的概念层次
JMM描述的是一组规则,围绕原子性,有序性,可见性展开
相似点:存在共享区域和私有区域
主内存与工作内存的数据存储类型以及操作方式归纳
方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中
引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中
成员变量、static变量、类信息均会被存储在主内存中
主内存共享的方式是 线程各拷贝一份数据到工作内存,操作完成后刷新回主内存
JMM如何解决内存可见性问题
指令重排序 需要满足的条件
单线程环境下不能改变运行的结果
存在数据依赖关系的不允许进行重排序
即无法通过happens-before 原则推导出来的,才能进行指令的重排序
A 操作的记过需要对B操作课件,则A和B存在happens-before关系
happen-before原则
判断数据是否存在竞争、判断线程是否安全的主要依据
程序次序规则
一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则
一个unLock操作先行发生于后面对同一个锁的lock操作
volatile 变量规则
对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则
如果操作A先行发生于操作B,而操作B又先行发生于C,则可以得出 操作A先行发生于C
线程启动规则
Thread对象的start()方法先行发生于此线程的每一个动作
线程终端规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则
线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join()方法结束、Thread.isAlive()的返回值手段检测线程已终止执行
对象终结规则
一个对象的初始化完成先行发生于它的finalize()方法的开始
hanppen-before八大规则
如果两个操作不满足上述的任意一个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序;
如果操作A happens-before 操作B,那么操作A在内存上所做的操作对操作B都是可见的。
volatile
JVM提供的轻量级同步机制
保证被volatile修饰的共享变量对所有的线程总是可见的
禁止指令的重排序优化
volatile在多线程情况下,并不能保证安全性
线程不安全
线程安全
线程安全
volatile变量和为何立即可见
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中
当读取一个volatile 变量时,JMM 会把该线程对应的工作内存置为无效
volatile 如禁止重排优化
内存屏障 (Memory Barrier)
保证特定操作的执行顺序
保证某些变量的内存可见性
通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化
强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本
单例的双重检测实现
使用 volatile 禁止重排优化
volatile 和synchronized 区别
volatile 本质是告诉JVM当前变量在寄存器(工作内存)中的值不确定的,需要从主存中读取;synchronized 则是锁定当前变量,只有当前的线程可以访问该变量,其他线程被阻塞知道该线程完成变量操作为止
volatile仅能作用在变量级别,synchronized可以使用在变量、方法、类级别
volatile仅能实现变量的修改可见性,不能保证原子性,而synchronized 则可以保证变量修改的可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
CAS无锁技术(Compare and Swap)
synchronized 是悲观锁,CAS是乐观锁设计
一种高效实现线程安全性的方法
支持原子更新操作,适用于计数器,序列发生器等场景
属于乐观锁机制,号称lock-free
感知上是,实际底层还是有加锁行为
CAS操作失败由开发者决定是否继续尝试,还是执行别的操作
所以不会被 阻塞挂起
CAS思想
包含三个操作数
内存位置(V)
预期原值(A)
新值(B)
执行CAS操作时,会将内存位置的值和预期原值进行比较,如果相等,处理器会自动的将该位置的值设置为新值,否则处理器不做任何处理。内存位置的值 即主内存的值
CAS多数情况下对开发者是透明的
J.U.C 的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型 和更新操作工具,是很多线程安全程序的首选
Unsafe类虽然提供CAS服务,但因能够任意操纵内存地址读写而有隐患,所以不要轻易去手动实现
非得需要调用Unsafe,Java9以后,提供了Variable Handke API来替代Unsafe
CAS缺点
弱循环时间长,则开销很大
只能保证一个共享变量的原子操作
ABA问题
一个变量值为A,期间被另外一个线程改为了B,又被改回了A,此时会被CAS操作认为这个值没有被改变不过
解决方案: 提供了 AtomicStampedeReference 控制变量的版本解决CAS 的ABA问题
java线程池
利用Executors创建不同的线程池满足不同场景的需求
newFixedThreadPool(int nThreads)
指定工作线程数量的线程池
newCachedThreadPool(
处理大量短时间工作任务的线程池
试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程
如果线程闲置的时间超过阈值,则会被终止并移出缓存
系统长时间闲置的时候,不会消耗什么资源
newSingleThreadPool()
创建唯一的工作线程来执行任务,如果线程异常结束,会有另一个线程取代它
newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize)
定时或周期性的工作调度,区别在于单一工作线程还是多线程
newWorkStealingPool()
内部会构建ForkJoinPool,利用work-stealing算法,并行地处理任务,不保证处理
Fork/jion框架
Java7提供的并行任务框架
把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架
work-stealing算法
某个线程从其他队列里窃取任务来执行
Fork会将任务分到不同的队列里,并为每个队列,创建单独的线程去执行
当某个线程执行自己的任务队列完成后,会窃取其他线程的队列里的任务去执行
队列采取双端队列,被窃取的线程永远从双端队列的头部拿任务执行,窃取的线程永远从双端队列的尾部拿任务执行
Executor框架
是将任务的提交 和任务的运行分离开的框架
J.U.C的三个Executor接口
Executor
运行新任务的简单接口,将任务提交和任务执行细节
ExecutorService
具备管理执行器和任务声明周期的方法,提交任务机制更完善
ScheduledExecutorService
支持Future和定期执行任务
ThreadPoolExecutor的构造函数
corePoolSize
核心线程数量
maximumPoolSize
线程不够用时能够创建的最大线程数量
workQueue
任务等待队列
keepAliveTime
除了核心线程数量外,其他线程空闲时候的存活时间
抢占的顺序不一定,看运气
threadFactory
创建新线程
默认使用的是Executors.defaultThreadFactory
handler
饱和策略
AbortPolicy
直接抛出异常,这是默认策略
CallerRunsPolicy
用调用者所在的线程来执行任务
DiscardOldestPolicy
丢弃队列中最靠前的任务
discardPolicy
直接丢弃任务
实现RejectedExecutionHandler接口的自定义handler,来实现自己的业务需求
新任务提交execute执行后的判断
如果运行的线程少于corePoolSize,则会创建新线程来处理任务,即使线程池中其他线程是空闲的
如果线程池中得到线程数量大于等于corePoolSize,且小于 maximumPoolSize,则只有当workQueue满时才会创建新线程去处理,否则,塞入 workQueue
如果设置的corePoolSize 和maximumPoolSize相同,则创建的线程池大小是固定的这时候如果有新任务提交,若workQueue未满,则将任务放入workQueue中,等待有空闲线程去从workQueue中去提取任务处理
如果运行的线程数量大于等于 maximunPoolSize,且如果workQueue已经满了则通过handler所制定的策略来处理任务
线程池的状态
RUNNING
能接受新任务 并且能处理 workQueue中的任务
SHUDOWN
不再接受新任务,但是可以处理存量任务
STOP
不再接受新任务,也不出存量任务
TIDYING
所有任务已终止
TERMINATED
terminated(),方法执行后进入该状态
工作线程的生命周期
为什么使用线程池
降低资源消耗
提高线程的可管理性
线程池的大小如何选定
CPU密集型
线程数= 按照核数或者 核数+1来设定
I/O密集型
线程数= CPU核数*(1+平均等待时间/平均工作时间)
java常用类库与技巧
Java异常体系
String,StringBuilder,StringBuffer区别
String
String 是不可变的,每次操作会生成新的String 对象
StringBuilder
每次操作不会新建对象 没有加锁
StringBuffer
每次操作不会新建对象,加锁
Java异常
异常机制
what
where
why
Error和Exception区别
thorwadble
error
exception
RuntimeException异常
程序不可预知的异常
非RuntimeException异常
可预知的异常编译器会受检
概念区分
Error是程序无法处理的错误 Exception是程序可以处理的异常,捕获后可恢复 RuntimeException是程序不可预知的,应当自行避免 非RuntimeExceotion是程序可预知的,编译器检查的
责任区分
Error是JVM需要承担责任 RuntimeException是程序承担责任 非RuntimeException是编译器承担责任
异常处理机制
抛出异常
创建异常对象,交给运行时系统处理
捕获异常
寻找合适的异常处理器处理异常,否则终止运行
Java异常处理原则
具体明确:抛出的异常应当能通过类名和message 准确描述异常类型和产生异常的原因
提早抛出:应当尽早发现并抛出异常,以便精确定位问题
延迟捕获:异常捕获和处理应当延迟,让掌握更多信息的作用域处理异常
Java异常消耗性能的地方
try-catch块 影响JVM优化
异常对象实例需要保存栈快照信息,开销比较大
Java集合框架
数据结构和算法
数据结构的考点
数据结构,算法拓展
数据和链表的区别
链表的操作
反转
链表环路检测
双向链表
循环列表
队列/栈的应用
二叉树的遍历方式及其递归和非递归实现
红黑树的旋转
算法考点
内部排序
递归排序
交换排序
冒泡
快排
选择排序
插入排序
外部排序
如何使用有限的内存资源和外部海量的数据存储处理超大数据集
考点扩展
哪些排序是不稳定的,稳定意味着什么
不同的数据集,各种排序最好和最坏的情况
如何优化算法
集合之List和Set
List
特点
有序(存储和取出顺序有序)
可重复
可通过索引值操作元素
分类
底层是数组,查询快,增删慢
ArrayList
线程不安全,效率高
Vector
线程安全,效率低
底层是链表,查询慢,增删快
linkedList
线程不安全,效率高
Set
特点
无序(存储和取出无序)
元素唯一
分类
底层是哈希表
HashSet
保证元素的唯一性
hashCode()
equals()
底层是二叉树
TreeSet
保证元素排序
自然排序,让对象所属的类实现Comparable接口,无参构造
比较器接口Comparator,带参构造
HashMap
数据结构
数组+链表(JDK8之前)
时间复杂度O(n)
数组+链表+红黑树(JDK8包括之后)
时间复杂度O(logn)最理想情况下
put方法逻辑
1.若HashMap未初始化,则做初始化操作2.对Key计算Hash值,根据Hash值获取下标3.若未发生碰撞,则直接放入桶中4.若发生碰撞,则以链表方式放到后面5.若链表的长度超过阈值,且HashMap的元素超过最低树话容量,则转换为红黑树(默认的阈值是8,最小树化容量是 64)6.若节点已经存在,则用新值替换旧值7.若桶满了(默认16*扩容因子0.75),需要用resize(扩容2倍后重排)
HashMap如何减少碰撞
扰动函数:促使元素分布均匀,减少碰撞几率
使用final对象,并采用合适的equals()和hashCode()方法
HashMap扩容问题
原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize
多线程的情况下,调整大小会存在竞争,容易造成死锁
reHashing是一个非常耗时的过程
HashTable
早期Java类库提供的哈希表实现
线程安全,涉及到HashTable的修改,都用了Synchronized修饰
串行化方式运行,性能较差
如何优化HashTable
锁的颗粒度细化,将整锁拆成多个锁进行优化
ConcurrentHashMap
早期实现
通过分段锁实现Segment实现,默认配置16个segment
数据结构:数组+链表
JDK8优化
使用CAS无锁技术+Synchronized 使锁细化,保证并发安全
数据结构:数组+链表+红黑树
ConcurrentHashMap put 逻辑
1.判断Node[]数组是否初始化,没有初始化则进行初始化操作2.通过Hash定位数组的索引坐标,是否有Node节点,如果没有,则使用CAS进行添加(链表的头节点),添加失败则进入下一次循环3.检查到内部正在扩容,则帮助它一块扩容4.如果f!=null,则使用synchronized锁住f(链表/红黑树的头元素) 4.1.如果是 Node(链表结构) 则进行链表的添加操作 4.2.如果是 TreeNode(红黑树),则进行树添加操作5.判断链表长度是否到8(默认值,可以人为修改),当节点超过这个值将链表转换为红黑树
总结
比起Segment,锁拆得更细
首先使用无锁操作CAS插入头节点,失败循环重试
若头节点已存在,则尝试获取头节点的同步锁,再进行操作
ConCurrentHashMap特别注意的点
size()方法和mappingCount()方法异同,两者计算是否准确
多线程环境下如何进行扩容
问题
HashMap,HashTable,ConCurrentHashMap不同
HashMap线程不安全,数据结构 数组+链表+红黑树
HashTable线程安全,锁住整个对象,数组+链表
ConcurrentHashMap线程安全,CAS+同步锁,数组+链表+红黑树
HashMap的key和value 均可为null,其他两者不支持
J.U.C包梳理
java.util.concurrent提供了并发编程的解决方案
CAS
是java.util.concurrent.autimic包基础
AQS
是java.util.concurrent.locks包,以及一些常用类Semophore,ReentratLock等类基础
JUC包分类
线程执行器executor
Executor框架
Executors
newSingleThreadExecutor
创建单核心的线程池
newFixedThreadPool
创建固定核心数的线程池
newCachedThreadPool
创建一个自动收缩的线程池
newSecheduledThreadPool
创建一个按照任务计周期划执行的线程池
newWorkStealingPool
创建一个具有抢占式操作的线程池
ThreadPoolExecutor:线程池基类
corePoolSize
核心线程数
maximumPoolSize
最大线程数
keepAliveTime
空闲线程存活时间
unit
存活时间单位
workQueue
任务阻塞队列
threadFactory
创建新线程时所需要的工厂
handler
拒绝策略
AbortPolicy
默认,队列满了丢弃任务,抛出异常
DiscardPolicy
队列满了丢弃任务,不抛异常
DiscardOldestPolicy
将最早进入队列的任务删除,然后再尝试加入队列
CallerRunPolicy
如果添加到线程池失败,那主线程会自己去执行该任务
四种拒绝策略
SecheduleThreadPoolExecutor
主要用于给定延迟之后运行任务,或定期执行的任务
ExecutorCompletionService
内部管理者一个已经完成任务的阻塞队列
submit()
提交任务,最终会委托给内部的executor去执行任务
take()
如果阻塞队列中已经有已完成的任务,则返回任务结果,否则阻塞等待任务完成
poll()
如果队列中有任务完成就返回,否则返回null
pull(long,TimeUnit)
如果队列中有任务完成则返回任务的结果,否则等待指定的时间,如果还是没有任务完成,则返回null
Fork/join分支合并框架
ForkJoinPool
是ExecutorService的一个补充,而不是替代品,特别适合用于分而治之,递归计算的算法
RecursiveTask
有返回结果的任务
RecursiveAction
无返回结果的计算
锁locks
ReetrantLock
可重入锁,互斥锁
ReeTrantReadWriteLock
可重入读写锁,读读共享,读写互斥,写写互斥
StampedLock
带时间戳的读写锁,不可重入
原子变量类atomic
原子类建立在CAS和volatile之上,CAS是非阻塞算法的一种常用实现,相对于synchronized这种阻塞算法,性能更好
普通原子类
AtomicBoolean
AtomicLong
AtomicInteger
AtomicIntegerArray
AtomicLongArray
Reference原子类
AtomicReference
AtomicReferenceArray
解决ABA
AtomicMarkableReference
AtomicStampedReference
增强原子类
LongAccumulator
自定义实现的long类型累加器,其构造函数接受一个双目运算器接口,根据输入的两个参数返回一个计算值,另外一个参数则是累加器的初始值
DoubleAccumulator
自定义实现的double类型的累加器,同LongAccumulator
LongAdder
long类型的原子操作,并发比LongAtomic好,优先使用LongAdder是LongAccumulator的一个特例
DoubleAdder
double类型的原子操作,同LongAdder
并发工具类tools
CountDownLatch
闭锁,能够使一个线程在等另外一些线程完成各自的工作后在,再继续的执行
CyclicBarrier
栅栏,能使多个线程等待一个条件达成之后,再继续执行
Semaphore
信号量,一个计数的信号量,必须由获取它的线程释放,常用于限制可以访问某些资源的线程数
Exchanger
交换器,用于两个工作线程之间交换数据的封装工具类
并发集合collections
并发队列
ArrayBlockingQueue
数组结构的有界阻塞队列
LinkedBlockingDueue
链表结构的阻塞队列,不指定大小为无界的
LinkedBlockingQueue
链表结构的双向阻塞队列.不指定大小为无界的
PriorityBlockingQueue
数组结构带优先级的有界阻塞队列,堆排序
DelayQueue
延迟阻塞队列
内部使用了PriorityQueue实现延迟
SynchronousQueue
同步阻塞队列,没有容量,put操作会一直阻塞,直到有take操作才能继续往下执行
LinkedTransferQueue
组合了SychronousQueue和LinkedBlockingQueue的 无边界阻塞队列
阻塞队列BlockQueue,提供了阻塞的入队和出队的操作主要用于生产者和消费者模式在多线程的情况下生产者在队列的尾部添加元素消费者在队列的头部消费元素从而达到将任务的 产生和消费隔离的目的
ConcurrentLinkedDeque
链表结构的线程安全队列
ConcurrentLinkedQueue
链表结构的线程安全双向队列
非阻塞队列
并发集合
ConcurrentHashMap
线程安全的Map,数组+链表/红黑树结构
ConcurrentHashMap.newKeySet()
线程安全的操作
ConcurrentSkipListMap
线程安全,按照key排序的Map,跳表结构
ConcurrentSkipListSet
线程安全,有序的Set,跳表结构
CopyOnWriteArrayList
写使复制的List,读多写少的场景
CopyOnWriteArraySet
写时复制的Set,读多写少的场景
其他
TimeUnit
ThreadLocalRandom
多线程下随机数生成性能的提升,避免竞争同一个seed(种子)
Java的IO机制
基础概念
同步和异步(被调用方)
同步
发起一个调用后,被调用者未处理完之前,调用不返回
异步
发起一个调用后,立刻收到被调用者的返回表示已接收到请求,但是没有返回调用结果。调用者此时可以处理其他请求。被调用者通常依靠事件,回调机制来通知调用者返回结果
阻塞和非阻塞(调用方)
阻塞
调用者发起一个调用后,当前线程会被挂起,无法做其他任务
非阻塞
调用者发起一个调用后,不用一直等着结果返回,可以先去做其他事情
同步阻塞
调用方一直等在 被调用方的 同步返回
同步非阻塞
调用方在 被调用方未返回时 做点其他事情,需要时不时检查下返回结果
异步非阻塞
调用方 不等 被调用方的返回,被调用方 会通过 事件或者回调机制 通知 被调用方
BIO
同步阻塞模式
数据的读取写入必须在一个线程内完成
在阻塞式 I/O 模型中,应用程序在从IO系统调用开始,一直到到系统调用返回,这段时间是阻塞的。
采用BIO通信模型的服务端,通常由一个Acceptor线程负责监听客户端的连接,我们一般通过while(true)循环中调用服务端会调用 accept()方法等待接收客户端的连接 的方式监听请求,智能等待当前连接的客户端执行操作完成。可以通过多个线程来支持多个客户端的连接
伪异步IO
服务端可以用线程池实现伪异步IO
java虚拟机中,线程是很宝贵的资源,线程的创建和销毁非常消耗资源,同时在linux操作系统下,线程实际上就是进程,创建和销毁都是重量级系统函数,如果线程的并发访问量急剧的增加会导致线程堆栈溢出,创建新线程失败,最终导致当机,对外服务不可用。
为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
子主题
伪异步I/O通信框架采用的线程池方式实现,避免了线程资源耗尽的问题,但是底层还是BIO通信模型,无法根本解决问题
NIO(JDK1.4版本开始支持)
同步非阻塞
NIO是一种同步非阻塞的I/O模型
NIO是面向缓冲,基于通道的IO操作方法
NIO提供了和IO的socket 和 serverSocket 相对应的 socketChannel 和 serverSocketChannel 两种不同套接字的实现,两种通道都支持阻塞和非阻塞两种模式,阻塞模式和传统的用法一样,代码比较简单,但是性能不好;非阻塞模式正好相反对于低负载,低并发的应用程序,我们还是可以用同步阻塞I/O来提高开发速率和更好的维护性。对于高负载,高并发(网络)的应用,应使用NIO的 非阻塞模式来开发
IO 和NIO 区别
IO
IO流是阻塞的
IO面向流 (Stream oriented)
在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区
NIO
非阻塞
NIO面向缓冲区(buffer oriented)
在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
Channel(通道)
通道是双向的,可读可写,而流的读写是单项的无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
Selectors(选择器)
NIO有选择器,而IO没有
选择器用单线程处理多个通道所以它需要较少的线程来处理这些通道线程之间的切换是昂贵的,因此,为了提高系统效率使用选择器是有用的
NIO 读数据和写数据方式
通常来说,NIO的读写都是从Channel开始的
从通道读取:创建一个缓冲区,然后请求通道读取数据
从通道写入:创建一个缓冲区,写入数据,然后请求通道写入数据
NIO核心组件
Selector(单线程)
Channel(通道)
Buffer(缓冲区)
构建多路复用,同步非阻塞的IO操作
多路复用
在多路复用IO模型中,会有一个线程(Java中的Selector)不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
AIO(JDK1.7版本开始支持)
异步非阻塞
BIO、NIO、AIO适用场景分析
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
java框架Spring
Spring家族介绍 一
Spring Core
核心
IOC
AOP
资源抽象
数据验证和转换
Spring表达式语言
测试
单元测试
集成测试
Web
Spring MVC
Spring WebFlux
数据访问
事务处理
JDBC模板
……
Spring Data
JPA
Redis
Mongodb
Couchbase
Cassandra
ElasticSearch
Neo4j
……
Spring Boot
单体应用
嵌入式容器
依赖管理
约定大于配置
环境管理
日志管理
配置管理
自动配置
管理功能
断点
打点
监控
开发者工具&CLI
……
Spring Security
OAuth2.0
CAS
WEB安全
授权
身份验证
加密
……
Spring Session
Spring Integration
Spring REST Docs
Spring AMQP
……
Spring 家族介绍 二
Spring Cloud 配置中心
Spring Cloud Config
Client
spring-cloud-starter-config
Config Server
spring-cloud-config-server
@EnableConfigServer
Spring Cloud netflix 微服务
Eureka服务发现
Client(Service)
spring-cloud-starter-netflix-eureka-client
@EnableEurekaClient
EurekaServer
spring-cloud-starter-netflix-eureka-server
@EnableEurekasServer
Hystrix 熔断器
Client
spring-cloud-starter-netflix-hystrix-client
@EnableCircuitBreaker
Turbine 聚合服务
spring-cloud-starter-netflix-turbine
@EnableTurbine
DashBoard 后台
spring-cloud-starter-netflix-hystrix-dashboard
@EnableHystrixDashboard
Zuul 网关服务
Spring-cloud-starter-netflix-zuul
@EnableZuulProxy > @EnableZuulServer
SideCar 边车服务
spring-cloud-starter-netflix-sidecar
@EnableSidecar
Ribbion 客户端负载均衡
spring-cloud-starter-netflix-ribbion
OpenFegin REST 客户端
spring-cloud-starter-openfegin
@EnableFeginClient
Spring Cloud Sleuth 调用链路跟踪
client
spring-cloud-starter-sleuth
spring-cloud-starter-zipkin
Zipkin Server
io.zipkin.java.zipkin-server
@EnableZipkinServer
Spring cloud Stream 消息驱动微服务
Source
Sink
Processor
Binders
spring-cloud-binder-rabbit
spring-cloud-binder-kafka
spring-cloud-binder-kafka-streams
Spring Cloud Function 函数及服务
Function
Consumer
Supplier
Applications
spring-cloud-function-web
spring-cloud-function-stream
Spring Cloud Bus 消息总线
spring-cloud-starter-bus-amqp
spring-cloud-starter-bus-kafka
spring cloud Gateway 高性能网关服务
spring-cloud-starter-gateway
Route
Predicate
Filter
Spring Cloud Task 任务框架
spring-cloud-starter-task
@EnableTask
Spring Cloud Admin 社区做的管理台
de.codecentric.spring-boot-admin-starter-server
@EnableAdminServer
Spring Cloud Contract 微服务契约
spring-cloud-starter-contract-verifier
spring-cloud-starter-contract-stub-runner
Spring Cloud Data Flow
DashBoard
Application
Spring Cloud Stream App Starters
Spring Cloud Task App Starters
Server
Deployer
IOC原理
Inversion of Control 控制反转
Spring Core 最核心部分
Dependency Inversion 依赖注入
举例:上层建筑依赖下层建筑,这样的代码几乎不可维护想修改轮子,你得修改所有的类
依赖注入的含义: 将底层类作为参数 传递给上层类,实现上层对下层的 “控制”
注入方式
Set注入
接口注入
注解注入
构造器注入
IOC容器优势
避免在各处使用new 来创建类,并且可以做到统一维护
在创建实例的过程中,不需要了解其中的细节
项目启动时发生了什么
1.Spring 启动时,回读取应用程序提供的Bean信息.2.将读取到的bean 配置信息,生成一个bean配置的注册表3.根据这个bean的注册表 去实例化bean,装配好bean的依赖关系,为上层提供准备就绪的运行环境。4.利用java 的反射功能实例化bean,并建立bean之间的依赖关系
IOC支持哪些功能
依赖注入
依赖检查
自动装配
支持集合
制定初始方法 和销毁方法
支持回调某些方法
Spring IOC 容器核心接口
BeanFactory
提供了IOC的配置机制
包含了bean的各种定义,便于实例化bean
建立bean之间的依赖关系
bean的生命周期控制
ApplicationContext
继承了多个接口
BeanFactory
能够管理、装配Bean
ResourcePatternResolver
能够加载资源文件
MessageSource
能够实现国际化相关功能
ApplicationEventPublisher
能够注册监听器,实现监听机制
BeanDefinition
主要用来描述bean定义
BeanDefinitionRegistry
提供向IOC容器注册BeanDefinition对象的方法
BeanFactory 和ApplicationContext 比较
BeanFactory是Spring 的基础设施ApplicationContext是面向Spring框架的开发者
refresh方法
为IOC容器以及Bean的生命周期管理提供条件
刷新Spring上下文信息,定义Spring上下文加载流程
getBean方法
转换beanName
从缓存加载实例
实例化bean
检测parentBeanFactory
初始化依赖的bean
创建bean
常见面试题
Spring Bean 的作用域
singleton
Spring 容器默认的作用域,容器里会有唯一的Bean实例
适合无状态的bean
prototype
针对每个getBean请求,容器都会创建一个新的Bean实例
适合有状态的bean
request
会为每个Http请求创建一个Bean实例
session
会为每个Sessionc创建一个Bean实例
globalSession
会为每个全局Http Session c创建一个Bean实例,该作用域仅对Portlet生效
web容器额外支持
Bean的生命周期
实例化Bean
Aware(注入Bean ID,BeanFactory,AppCtx)
Aware接口声明依赖关系
BeanPostProcessor(s)postProcessBeforeInitalization
前置初始化方法咋Spring完成实例化之后,对Spring容器实例化的Bean 添加一些自定义处理逻辑
InitializingBeans(s).afterPropertiesSet
定制的bean.init方法
BeanPosProcessor(s).postProcessAfterInitalization
后置初始化方法去实现bean初始化完成之后自定义的操作
bean初始化完毕
Bean的创建
bean的销毁过程
如果实现了DisposableBean接口,则会调用destroy方法
若配置了destroy-method属性,则会调用期配置的销毁方法
Bean的销毁
AOP的介绍和使用
介绍
关注点分离:不同的问题交给不同的部分去解决
面向切面编程AOP就是这种技术的体现
通用化的功能实现,对应的 就是所谓的切面(Aspect)
业务功能代码和切面代码分开后,架构将变得高内聚,低耦合
确保功能的完整性,切面最终需要被整合到业务中(Weave,编入)
AOP三种织入的方式
编译时织入
需要特殊的Java编译器,入AspectJ
类加载时候织入
需要特殊的Java编译器,如AspactJ,AspectWerkz
运行时织入
Spring采用这种方式,通过动态代理,实现简单
AOP主要名词概念
Aspect
切面:通用功能的代码是实现
Target
目标:被织入Aspect对象
Join point
可以作为切入点的机会,所有的方法都可以作为切入点
Pointcut
定义Aspect实际被应用在的Join Point,支持正则
Advice
类里的方法以及这个方法如何织入到目标方法的方式
分类
前置通知Before
后置通知AfterRunning
异常通知AfterThrowing
最终通知 After
环绕通知 Around
Weaving
AOP实现的过程
JDKProxy,Cglib 来生成代理对象
具体由AopProxyFactory 根据AdvisedSupport对象的配置来决定
策略是如果目标是接口,则默认使用JDKProxy来实现,否则使用后者
JDKProxy
JDK动态代理是代理模式的一种实现方式,通过反射接收要代理的类,并且要求要代理的类必须要实现接口
JDKProxy核心
InvocationHandler接口和 Proxy类
反射机制在生成类的过程中比较高效
Cglib
以继承的方式实现目标类的代理,底层借助ASM实现
如果目标类被设置为final 而无法继承,则无法使用Cglib动态代理
ASM在生成类之后执行过程比较高效
代理模式
接口+真实实现类+代理类
Spring里的代理实现
真实实现类的逻辑包含在getBean方法里
getBean方法返回的实际上是Proxy的实例
Proxy实例是Spring 采用JDKProxy或者是Cglib动态生成的
Spring其他考点
ACID
Atomicity 原子性
原子性指的是一个事物是一个不可分割的工作单位,事物中的操作要么都发生,要么都不发生
Consistency 一致性
事物前后的数据的完整性必须保持一致
Isolation 隔离性
多个用户访问数据库时,数据库为每个用户开启的事物,不能被其他事物操作的数据所干扰,多个并发事物之间要相互隔离
Durability 持久性
一个事物一旦提交,它对数据库中的数据改变是永久性的,接下来就算是数据库有故障也不会对数据有任何影响
隔离级别
默认使用数据库的隔离级别
mysql 默认 repeatable
Oracle 默认 read commited
事务传播特性
保证同一个事务中PROPAGATION_REQUIRED 支持当前事务,如果不存在 就新建一个(默认)PROPAGATION_SUPPORTS 支持当前事务,如果不存在,就不使用事务PROPAGATION_MANDATORY 支持当前事务,如果不存在,抛出异常保证没有在同一个事务中PROPAGATION_REQUIRES_NEW 如果有事务存在,挂起当前事务,创建一个新的事务PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务PROPAGATION_NEVER 以非事务方式运行,如果有事务存在,抛出异常PROPAGATION_NESTED 如果当前事务存在,则嵌套事务执行
linux
linux体系结构
了解即可
内核、shell、文件系统和应用程序。内核、shell和文件系统一起形成了基本的操作系统结构
linux内核
Linux内核是世界上最大的开源项目之一,内核是与计算机硬件接口的易替换软件的最低级别。它负责将所有以“用户模式”运行的应用程序连接到物理硬件,并允许称为服务器的进程使用进程间通信(IPC)彼此获取信息。内核是操作系统的核心,具有很多最基本功能,它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性。Linux 内核由如下几部分组成:内存管理、进程管理、设备驱动程序、文件系统和网络管理
内存管理
进程管理
文件系统
设备驱动程序
网络接口(NET)
linux shell
shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行,是一个命令解释器。另外,shell编程语言具有普通编程语言的很多特点,用这种编程语言编写的shell程序与其他应用程序具有同样的效果。
1.Bourne Shell:是贝尔实验室开发的。
2.BASH:是GNU的Bourne Again Shell,是GNU操作系统上默认的shell,大部分linux的发行套件使用的都是这种shell。
3.Korn Shell:是对Bourne SHell的发展,在大部分内容上与Bourne Shell兼容。
4.C Shell:是SUN公司Shell的BSD版本。
linux 文件系统
用户态和内核态
应用程序是无法直接访问硬件资源的,需要通过通过内核SCI 层提供的接口来访问硬件资源。
查找特定文件
命令:find,用于在指定目录下查找文件
find path [options] params
查找home目录下的target3.java文件
find / -name "target3.java"
模糊查询:查找以"target"打头的文件:
find ~ -name "target*"
不区分大小写模糊查询
find ~ -iname "target*"
检索文件内容
搜索、查找文件当中的内容,一般最常用的是grep命令,另外还有egrep, vi命令也能搜索文件里面内容
grep
搜索某个文件里面是否包含字符串,使用grep "search content" filename1
$ grep ORA alert_gsp.log$ grep "ORA" alert_gsp.log
搜索多个文件是否包含某个字符串
grep "search content" filename1 filename2.... filenamengrep "search content" *.sql
如果需要显示搜索文本在文件中的行数,可以使用参数-n
-n "v\$temp_space_header" *.sql
如果搜索时需要忽略大小写问题,可以使用参数-i
grep -i "V\$TEMP_SPACE_HEADER" *.sql
从文件内容查找不匹配指定字符串的行
grep –v "被查找的字符串" 文件名
搜索、查找匹配的行数
grep -c "被查找的字符串" 文件名
有些场景,我们并不知道文件类型、或那些文件包含有我们需要搜索的字符串,那么可以递归搜索某个目录以及子目录下的所有文件
grep -r "v\$temp_space_header" /u01/app/oracle/product/11.1.0/dbhome_1/rdbms/admin/
如果我们只想获取那些文件包含搜索的内容,那么可以使用下命令
grep -H -r "v\$temp_space_header" /u01/app/oracle/product/11.1.0/dbhome_1/rdbms/admin/ | cut -d: -f1
如果只想获取和整个搜索字符匹配的内容,那么可以使用参数w
grep -w "ORA" utlspadv.sql
grep命令结合find命令搜索
find . -name '*.sql' -exec grep -i 'v\$temp_space_header'
对日志内容统计
awk
awk 适合处理格式化的数据,针对表格化的数据
一次读取一行文本,按照输入分隔符进行切片,切成多个组成部分
将切片直接保存在内建的变量中,$1,$2...($0表示行的全部)
支持单个切片的判断,支持循环判断,默认分隔符为空格
语法
awk [options ] 'cmd' file
批量替换文件内容
sed -i "s/len/size/g" `grep len -rl ./`
len为原字符串
size为目标字符串
-rl是递归查找所有包含字符串len的文件