hsgcc:面试笔记
1、java当中八种基础数据类型
byte(1字节)-128 ~ 127, short(2字节), int(4字节), long(8字节), double(4字节), float(8字节), boolean, char(2字节)
2、ACID分别代表什么
事务的四个属性:ACID
原子性:原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚
一致性:数据从一个 合法性状态 变换到另外一个合法性状态 。这种状态 是语义上的而不是语法上的,跟具体的业务有关。
隔离性:事务的隔离性是指一个事务的执行不能被其他事务干扰 ,即一个事务内部的操作及使用的数据对并发的 其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性:持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的
3、MySql数据并发问题
脏写:对于两个事务 Session A、Session B,如果事务Session A 修改了另一个未提交事务Session B 修改过的数 据,那就意味着发生了脏写
脏读:对于两个事务 Session A、Session B,Session A 读取 了已经被 Session B 更新但还没有被提交 的字段。 之后若 Session B 回滚 ,Session A 读取 的内容就是 临时且无效 的。
不可重复读:对于两个事务Session A、Session B,Session A 读取了一个字段,然后 Session B 更新 了该字段并且提交了。之后 Session A 再次读取同一个字段,值就不同了。那就意味着发生了不可重复读。
幻读:对于两个事务Session A、Session B, Session A 从一个表中读取 了一个字段, 然后 Session B 在该表中插 入了一些新的行。 之后, 如果 Session A 再次读取同一个表, 就会多出几行。那就意味着发生了幻读。
4、mySql中四种隔离级别
READ UNCOMMITTED :读未提交(解决了脏写)
READ COMMITTED :读已提交(解决了脏写、脏读)
REPEATABLE READ :可重复读 (解决了脏写、脏读、不可重复读)-->MySql默认的隔离级别
SERIALIZABLE :可串行化(解决了脏写、脏读、不可重复读、幻读)
数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性 就越好,但并发性越弱。
5、Mysql索引的分类
从功能逻辑上说,索引主要有 4 种,分别是普通索引、唯一索引、主键索引、全文索引。 按照 物理实现方式 ,索引可以分为 2 种:聚簇索引和非聚簇索引。 按照 作用字段个数 进行划分,分成单列索引和联合索引。
1. 使用全文索引前,搞清楚版本支持情况;
2. 全文索引比 like + % 快 N 倍,但是可能存在精度问题;
3. 如果需要全文索引的是大量数据,建议先添加数据,再创建索引。
6、那些情况适合创建索引
1.字段数值有唯一性限制
业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba)
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的。
2.频繁作为where查询条件的字段
3.经常group by和 order by的字段
如果查询语句中同时有这两个,联合索引要先创建group by,因为遵循最左侧原则
4、distinct字段需要创建索引
5、join连接的字段需要创建索引
6、使用字符串前缀创建索引
【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本
区分度决定索引长度。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达
90% 以上 ,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。
7、区分度高的列适合作为索引
7、创建索引需要注意的事项
连接表的数量尽量不要超过3张:每增加一张表就相当于时增加了一次嵌套循环,数量级增长非常快,严重影响查询效率
用于连接的字段需要创建索引,并且数据类型要一致(隐式转换数据类型索引会失效)
使用最频繁的列放在联合索引的左侧(最左前缀原则)
多个字段都要创建索引的情况下,联合索引优于单值索引
单张表的索引最好不超过6个
1.每个索引都需要占据物理磁盘空间,索引越多所占磁盘空间越大
2.索引会影响update、delete、insert的效率(因为表中数据更改时,索引也会随之更改)
3.优化器在查询优化时,会生成执行计划,如果有很多索引都可以用于查询,会影响优化器生成执行计划的时间,降低执行效率
避免创建重复索引(如主键索引与联合索引)
在开发中要将范围查询放在语句的最后(防止索引失效),并且创建的联合索引中,务必把范围涉及到的索引放到最后
使用列类型小的创建索引(节省磁盘空间)
保证被驱动表join字段已经创建了索引
10.当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然。
8、哪些情况不适合用作索引
where中用不到的列不要设置索引
数据量比较小的表不适合创建索引(1000条以下)
大量重复数据的列不要创建索引
避免对经常更新的表创建过多索引
不建议用无序的值作为索引(页分裂)
9、定位执行慢的sql
慢查询日志
分析查询语句:EXPLAIN看是否走索引....
10、有哪些维度可以进行数据库调优
索引失效、没有充分利用到索引
关联查询太多join(sql优化)
含有子查询(优化表结构)
数据过多(读写分离、分库分表)
服务器调优以及参数的设置
硬件加强
11、索引失效的案例
SQL语句是否使用索引跟数据库版本、数据量、数据选择度都有关系
1.最佳左前缀法则
在检索数据时从联合索引的最左侧开始匹配,一旦跳过某个字段索引后面的字段都无法被使用
2.计算、函数、类型转换导致索引失效
3.范围条件右边的列索引失效(> 、< 、! =)
4.is null可以使用索引,is not null无法使用索引
5.like以通配符%开头的索引失效
页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决
6.or 前后存在非索引的列
7.order by时顺序错误,索引失效
INDEX a_b_c(a,b,c) ORDER BY a ASC,b DESC,c DESC 索引失效
12、关联查询的优化
1.左外连接
左外连接右侧的条件是关键点,一点要建立索引,避免全表扫描
保证被驱动表join字段已经创建了索引
left join选择小表作为驱动表,大表作为被驱动表,减少内层表循环的次数
增大join buffer size的大小(一次缓存的数据越多,内层表的扫描次数越少)
减少驱动表不必要的字段查询(字段越少, join buffer 所缓存的数据越多)
能使用多表关联查询尽量关联查询,不用子查询(会在下面讲)或者将子查询sql拆开结合程序多次查询
什么叫做小表?
两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。
2.内连接
内连接:查询优化器可以决定谁为驱动表,谁作为被驱动表,如果表的连接条件中只有一个字段有索引,那么有索引的字段将会视为被驱动表
在两个表的连接条件都存在索引的情况下,会选择小表作为驱动表,“小表驱动大表”
如果两个表的连接条件都不存在索引,也是小表驱动大表
13、子查询优化
子查询的执行效率不高的原因:
1.执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表 ,然后外层查询语句从临时表
中查询记录。查询完毕后,再撤销这些临时表 。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
2.子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都不会存在索引 ,所以查询性能会
受到一定的影响。
3.对于返回结果集比较大的子查询,其对查询性能的影响也就越大。
14、排序优化
1.在 WHERE 条件字段上加索引,但是为什么在 ORDER BY 字段上还要加索引呢?
mysql中支持两种排序方式FileSort和Index,Index是利用索引可以保证数据的有序性,不需要在进行排序,FileSort排序一般在内存中排序,占用cpu过多
2.创建的索引默认为升序,在排序时索引如果都是desc,asc大概率可以使用
3.切记是否使用索引是根据数据量来决定的!!
15、什么是索引覆盖
定义:一个索引包含了满足查询结果的数据就叫做索引覆盖
例子: 索引列+主键包含select到from之间的所有查询字段
优点:避免进行回表操作,可以把随机io变为顺序io加快查询效率
create index idx_age_name on student (age,name)
#该条语句可以使用索引(索引覆盖),索引的失效与使用不是绝对的,是基于效率来考虑的
select age,name from student where age <> 20
16、什么是索引下推Index Condition Pushdown(ICP)
定义:更多针对的是联合索引,索引中索引失效了,但是我们可以使用索引下推来做进一步筛选
解释:索引当中有该字段,但是该字段索引失效,在回表之前可以使用该索引做进一步筛选
在回表之前进一步整理
17、普通索引 vs唯一索引
从性能的角度考虑,你选择唯一索引还是普通索引呢?选择的依据是什么呢?
查询过程:这个不同带来的性能差距会有多少呢?答案是,微乎其微 。
更新过程:普通索引比唯一索引效率高(普通索引更新时可以使用change buffer,唯一索引不可以)
18、count(*),count(1),count(具体字段)
count(*)和count(1)本质上没有区别
MyIsam统计表的行数只需要o(1),因为meta信息存储了一个row_count信息
在innodb中,如果采用count(具体字段),要尽量采用二级索引,因为主键采用的是聚簇索引,聚簇索引包含的信息多,明显会大于非聚簇索引,对于count(*)和count(1)会自动采用占用空间更小的二级索引来进行统计
19、select *
mysql在解析过程中,会通过查询数据字典将 * 按序转换成所有列名,会大大消耗资源
无法使用索引覆盖
20、主键的设置原则
主键应该为单调递增的(这样的主键占用空间小,顺序写入,减少页分裂)
2.MySql自增id存在的问题
可靠性不高(8.0之前存在自增id回溯的问题)
安全性不高(对外暴露的接口可以非常容易的猜测对应信息)
性能差(自增id需要在数据库服务器生成)
交互多(业务还需要额外执行一次类似 last_insert_id()的函数才能知道刚才插入的自增值)
局部唯一性(不是全局唯一的,对于分布式系统就是噩梦)
非核心业务(日志等):主键自增即可
核心业务:主键设计至少应该是全局唯一且是单调递增
比如:uuid倒序 + 业务字段
21、OSI 七层模型
22、应用层常见协议
1.HTTP:超文本传输协议
HTTP 协是基于 TCP协议,发送 HTTP 请求之前首先要建立 TCP 连接也就是要经历 3 次握手。HTTP 协议是”无状态”的协议,它无法记录客户端用户的状态,一般我们都是通过 Session 来记录客户端用户的状态
2.HTTPS:是 HTTP 的加强安全版本。HTTPS 是基于 HTTP 的,也是用 TCP 作为底层协议,并额外使用 SSL/TLS 协议用作加密和安全认证
3.SMTP:简单邮件传输(发送协议)
23、HTTP和HTTPS之间的区别
HTTP请求协议:4部分
请求行:请求方式(get,post)、URL、HTTP协议版本号
请求头:请求主机、主机端口号、浏览器信息、cookie等
空白行:作用是区分请求头和请求体
请求体:向服务器发送的具体数据
HTTP响应协议:4部分
状态行:HTTP协议版本号、状态码、状态描述信息(ok,not found)
响应头:响应类型、响应时间等
空白行:作用是区分响应头和响应体
响应体:响应的正文
HTTP优点:扩展性强、速度快、跨平台支持性好。
HTTPS优点:保密性好、信任度高
HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
24、HTTP 1.0和1.1的区别
连接方式 : HTTP 1.0 为短连接,HTTP 1.1 支持长连接。
状态响应码 : HTTP/1.1中新加入了大量的状态码,光是错误响应状态码就新增了24种。比如说,100 (Continue)——在请求大资源前的预热请求,206 (Partial Content)——范围请求的标识码,409 (Conflict)——请求与当前资源的规定冲突,410 (Gone)——资源已被永久转移,而且没有任何已知的转发地址。
缓存处理 : 在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。
带宽优化及网络连接的使用 :HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
Host头处理 : HTTP/1.1在请求头中加入了Host字段。
25、HTTP常见的状态码
3xx Redirection(重定向状态码)
301 Moved Permanently : 资源被永久重定向了。比如你的网站的网址更换了。
302 Found :资源被临时重定向了。比如你的网站的某些资源被暂时转移到另外一个网址。
4xx Client Error(客户端错误状态码)
400 Bad Request : 发送的HTTP请求存在问题。比如请求参数不合法、请求方法错误。
401 Unauthorized : 未认证却请求需要认证之后才能访问的资源。
403 Forbidden :直接拒绝HTTP请求,不处理。一般用来针对非法请求。
404 Not Found : 你请求的资源未在服务端找到。比如你请求某个用户的信息,服务端并没有找到指定的用户。
409 Conflict : 表示请求的资源与服务端当前的状态存在冲突,请求无法被处理。
26、TCP 三次握手和四次挥手
建立一个 TCP 连接需要“三次握手”,缺一不可 :
一次握手:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 SYN_SEND 状态,等待服务器的确认;
二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 SYN_RECV 状态
三次握手:客户端发送带有带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务器端都进入ESTABLISHED 状态,完成TCP三次握手。
1.为什么要三次握手?
双方确认自己与对方的发送与接收是正常的
TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接。
第一次握手 :Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常
第二次握手 :Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常
第三次握手 :Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常
第三次握手是可以携带数据的,前两次握手是不可以携带数据的
第一次挥手 :客户端发送一个 FIN(SEQ=X) 标志的数据包->服务端,用来关闭客户端到服务器的数据传送。然后,客户端进入 FIN-WAIT-1 状态。
第二次挥手 :服务器收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (SEQ=X+1)标志的数据包->客户端 。然后,此时服务端进入CLOSE-WAIT状态,客户端进入FIN-WAIT-2状态。
第三次挥手 :服务端关闭与客户端的连接并发送一个 FIN (SEQ=y)标志的数据包->客户端请求关闭连接,然后,服务端进入LAST-ACK状态。
第四次挥手 :客户端发送 ACK (SEQ=y+1)标志的数据包->服务端并且进入TIME-WAIT状态,服务端在收到 ACK (SEQ=y+1)标志的数据包后进入 CLOSE 状态。此时,如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后,客户端也可以关闭连接了。
为什么要四次挥手?
TCP是全双工通信,可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。
TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让服务器端接收,从而帮助其正常关闭
27、TCP与UDP的区别
TCP:三次握手四次挥手
传输过程中可进行大数据量的传输
传输完毕,需要释放已建立的连接,效率低
是基于字节流
TCP 是一对一的两点服务,即一条连接只有两个端点
TCP 有拥塞控制和流量控制机制,保证数据传输的安全性
TCP 是流式传输,没有边界,但保证顺序和可靠
UDP:将数据封装成数据包不需要建立连接
发送数据是不可靠的,无需确认
发送数据结束时无需释放资源,开销小,速度快
UDP 支持一对一、一对多、多对多的交互通信
UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率
UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序
TCP和UDP的可以使用相同的端口号,因为两者是不同的协议,他会根据协议来判断由哪个模块来处理
28、计算机网络的正向代理和反向代理
29、TCP粘包和拆包
为什么UDP没有粘包?
UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。
定义:
因为TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。
解决方案:
1.发送端将每个包都封装成固定的长度,比如100字节大小。如果不足100字节可通过补0或空等进行填充到指定长度;
2.发送端在每个包的末尾使用固定的分隔符,例如\r\n。如果发生拆包需等待多个包发送过来之后再找到其中的\r\n进行合并;例如,FTP协议;
3.将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
30、git的常见命令
-git commit 将代码推到本地库
-git status 查看工作区的状态
-git reset 版本 回滚版本
-git clone 克隆代码到本地仓库
-git pull 拉取代码到本地仓库
-git push 将代码推送到远程仓库
-git add 将代码从本地推到暂存区
-git branch -v 查看分支
-git branch <分支名> 创建分支
-git checkout <分支名> 切换分支
-git merge 合并分支
代码冲突:手动merge,多人协同各有分工,避免操作相同代码
merge和rebase都是合并代码操作
merge:每次merge都会自动创建一个新的commit,并且保留了完整的记录
rebase:合并之前的commit历史,git分支简洁
31、进程线程的区别
进程:资源分配的基本单位
线程:程序执行的基本单位
进程可以细化为多个线程。每个线程,拥有自己独立的:栈、程序计数器多个线程,共享同一个进程中的结构:方法区、堆
32、进程间的通信方式
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的, 所以进程之间要通信必须通过内核。
进程间通信目的一般有共享数据,数据传输,消息通知,进程控制等。以 Unix/Linux为例,介绍几种重要的进程间通信方式:管道、消息队列、共享内存、信号量、信号、Socket。
33、List
定义:存储有序的,可重复的数据
ArrayList:线程不安全,效率高,底层使用Object[] elementData存储
LinkedList:对于频繁插入和删除操作效率比ArrayList高,底层使用双向链表存储,线程不安全
Vector:List的古老实现类,线程安全,效率低,底层使用Object[] elementData存储
ArrayList源码分析:
jdk1.7:底层创建了长度是10的Object[]数组elementData,如果此次的添加导致底层 elementData数组容量不够,则扩容。默认情况下,扩容为原来的容量的1.5倍。
jdk1.8:懒加载,底层Object[] elementData初始化为{}.并没创建长度为10的数组,第一次调用add()时,底层才创建了长度10的数组,并将数据添加到elementData[0],后续的添加和扩容操作与jdk 7 无异。
LinkedList源码分析:
内部声明了Node类型的first和last属性,默认值为null
Vector源码分析:
jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。在扩容方面,默认扩容为原来的数组长度的2倍。
34、Set
定义:存储无序的,不可重复的元素
无序性:不等于随机性。存储的数据在底层数组中并非照数组索引的顺序添加,而是根据数据的哈希值决定的
不可重复性:保证添加的元素equals()判断时,不能返回true.即:相同的元素只能添加一个
HashSet:线程不安全,可以储存null值
LinkedHashSet:遍历其内部数据时,可以按照添加的顺序遍历在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。对于频繁的遍历操作,LinkedHashSet效率高HashSet
TreeSet:可以照添加对象的指定属性,进行排序。
注意:向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()
元素的添加过程(HashSet为例):我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置,判断数组此位置上是否已经元素:如果此位置上没其他元素,则元素a添加成功。 如果此位置上其他元素b(或以链表形式存在的多个元素,则比较元素a与元素b的hash值: 如果hash值不相同,则元素a添加成功。 如果hash值相同,进而需要调用元素a所在类的equals()方法:equals()返回true,元素a添加失败,equals()返回false,则元素a添加成功。
jdk 7 :元素a放到数组中,指向原来的元素。
jdk 8 :原来的元素在数组中,指向元素a
35、Map
定义:存储key-value对数据
HashMap:线程不安全的,效率高;可以存储null的key和value
LinkedHashMap:可以按照添加的顺序实现遍历。
原因:在原的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap。
TreeMap:保证照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序底层使用红黑树
Hashtable:线程安全的,效率低;不能存储null的key和value
HashMap底层:数组+链表 (jdk7及之前),数组+链表+红黑树 (jdk 8)
对于key和value 的理解:
1.Map中的key:无序的、不可重复的,使用Set存储所有的key ---> key所在的类要重写equals()和hashCode() (以HashMap为例)
2.Map中的value:无序的、可重复的,使用Collection存储所有的value --->value所在的类要重写equals()
3.一个键值对:key-value构成了一个Entry对象。
4.Map中的entry:无序的、不可重复的,使用Set存储所有的entry
HashMap源码分析:
在实例化以后,底层创建了长度是16的一维数组Entry[] table。首先,添加元素时调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。如果此位置上的数据为空,此时的key1-value1添加成功。如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:如果equals()返回false:此时key1-value1添加成功。如果equals()返回true:使用value1替换value2。当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原的数据复制过来。
HashMap在jdk8中相较于jdk7在底层实现方面的不同:
底层没创建一个长度为16的数组
jdk 8底层的数组是:Node[],而非Entry[]
首次调用put()方法时,底层创建长度为16的数组
jdk7底层结构只:数组+链表。jdk8中底层结构:数组+链表+红黑树。
形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
36、hashmap多线程使用会有问题吗?解决方案
Hashmap是线程不安全的,多线程使用会导致a) 导致存储结果错误b) 多个线程同时扩容会导致循环链表
解决方案:a)Hashtable(废弃)b)ConcurrentHashMap(此处要解释)
37、线程有几种,创建线程的方式
用户线程和守护线程(垃圾回收器)
继承Thread类
实现Runnable接口
实现Callable接口
使用线程池new ThreadPoolExecutor()
38、MySql的存储引擎
Innodb:版本大于5.5之后默认的存储引擎,具备外键支持的功能事务存储引擎,支持聚簇索引
MyIsam:不支持事务、外键、行级锁,不支持聚簇索引
两者优缺点:
1.小表,增加和查询多用myisam,更新删除多用innodb
2.innodb为处理巨大数据量的最大性能设计的
3.innobd写的效率差一些,并且会占用更多的磁盘空间保存数据和索引
4.myisam只缓存只缓存索引,不缓存真实数据,innodb都缓存,对内存要求较高
5.myisam索引和数据分开存放,innodb索引和数据放到一起(聚簇索引)
两者在索引上的区别:(都是采用B+树存储的)
1.在InnoDB存储引擎中,我们只需要根据主键值对聚簇索引进行一次查找就能找到对应的记录,而在
MyISAM中却需要进行一次回表操作,意味着MyISAM中建立的索引相当于全部都是二级索引 。
2.InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是分离的 ,索引文件仅保存数
据记录的地址。
3.InnoDB的非聚簇索引data域存储相应记录主键的值 ,而MyISAM索引记录的是地址 。换句话说,
InnoDB的所有非聚簇索引都引用主键作为data域
4.MyISAM的回表操作是十分快速的,因为是拿着地址偏移量直接到文件中取数据的,反观InnoDB是通
过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址去访问。
Memory:置于内存的表
主要特征:支持哈希索引和b+树索引
适用于数据量小,需要频繁访问的数据
如果数据丢失没有太大关系,也可考虑用memory存储
39、聚簇索引和非聚簇索引的区别
按照物理实现方式:分为聚簇索引和非聚簇索引
聚簇索引:针对主键创建索引
非聚簇索引(二级索引):非主键创建索引
聚簇索引特点:页内的记录是按照主键的大小顺序排列成一个单向链表
每个页之间按照主键大小顺序排列成一个双向链表
B+树的叶子节点存储的是完整的用户记录
优点:数据访问快、节省io操作
缺点:插入速度严重依赖于插入顺序(主键单调增)、更新主键的代价很高、二级索引需要回表
非聚簇索引的特点:页内的记录是按照主键的大小顺序排列成一个单向链表
每个页之间按照主键大小顺序排列成一个双向链表
B+树的叶子节点存储的是主键id和二级索引
40、索引数据结构为什么使用 B+ 树,解释B+树(结合39)
索引:是一种数据结构
优点:减少磁盘io次数、加速表和表之间的连接、减少查询时间
缺点:创建和维护索引需要耗费时间、索引需要占据磁盘空间、降低表的更新速度
b+树最多不超过4层,树的层次越少,需要的io次数就越少,查询效率就越快
B+树:是一个多叉平衡树
每一条记录由record_type,next_record,实际数据,其他信息组成,各个记录根据主键id大小用单向链表连接,许多记录又构成了一个数据页,数据页之间根据主键id大小通过双向链表连接。
每当为某个表创建B+树索引时,都会为这个索引创建一个根节点页面,随后向表中插入用户记录,先把用户记录存到根节点,当根节点页面用完时(16kb),会将根节点中的所有记录复制到一个新分配的页,比如页a,然后对这个新页进行页分裂操作,得到页b,此时插入记录根据键值自动分配到页a或者页b中,根节点变升级为存储目录项记录的页。(根节点自诞生之日起,便不会在移动)
41、为什么MySql中b+树不超过4层
假设所有存放用户记录的叶子节点的数据页可以存放100条记录,存放目录项的非叶子节点可以存放1000条记录,两层就可以存10万条数据,三层就可以存1亿条数据,四层就可以存放1000亿的数据,索引b+树最多不超过4层!
42、B树和B+树的区别
B 树的所有节点既存放键(key) 也存放 数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
B+树通常比B树更矮胖,索引查询所用到的磁盘io少,效率高
在范围查找上,B+树的效率要高,因为叶子节点之间会有指针,数据又是递增的,但是B树只能通过中序遍历才能完成范围查找。
43、为什么不建议使用过长的字段作为主键
因为所有二级索引都引用主键索引,过长的主键索引会令二级索引变得过大。
44、Hash结构效率那么高,那为什么索引结构要设计成树形的呢?
Hash索引仅能满足 =, <>, in的操作,如果进行范围查找时间复杂度会退化为o(n)
Hash索引存储的数据是没有顺序的,order by效率很差
对于联合索引,hash值是通过将联合索引合并之后一起来计算的,无法对单独或者几个索引键进行查询
45、接口和抽象类的区别
相同点:都不能被实例化、接口的实现类或抽象类的子类只有实现了接口或者抽象类的方法才能实例化
不同点:1.接口只有定义,不能有方法的实现(static和default可以有实现方法),而抽象类可以有定 义与实现(抽象类中的方法可以实现,但是抽象方法不可以被实现)
2.实现接口用implements,集成抽象类用extend,一个类可以实现多个接口,但是只能继承 一个父类
3.接口中属性只能是public的,默认是public static final修饰,而抽象类无限制
4.接口中的方法只能是public的,抽象类中的方法不做限制,但是抽象方法必须是public的
5.抽象类可以有静态代码块,接口不可以有
46、Java 到底是值传递还是引用传递?
值传递:如果是基本数据类型是值传递,引用数据类型传递的该引用对象的内存地址。
47、Redis的数据结构
简单动态字符串(Simple Dynamic String):
c语言中字符串的缺点:获取字符串长度需要进行运算、非二进制安全(结尾都以\0结尾)、不可修改
结构:len(已保存字符串的字节数)、alloc(申请的总字节数)、flags(SDS头类型),buf(存 储数据)
特点:1.动态扩容:新字符串小于1m扩展为字符串长度的二倍+1,大于1m扩展为字符串长度 +1m+1
2.获取字符串的长度时间复杂度为o(1)
3.减少内存分配次数(动态扩容)
4.二进制安全
IntSet
InSet是set集合的一种实现方式,基于整数数组来实现
结构:encoding(编码方式,支持存放16、32、64位整数)、length(元素个数)、contents[] (整数数组,保存集合数据)
特点:1.Redis会将inset中所有整数按照升序依次保存在contents数组中
2.如果存的是{5,10,20}采用INTSET_ENC_INT16,如果添加一个数字50000,超过 int16会将编码方式自动全部升级,之后倒序依次将数组中的元素拷贝到扩容后的正确位 置
3.底层采用二分查找来查询
3.Dict
Redis是键值形的数据库,这种映射关系正是通过Dict来实现的
结构:哈希表(dictht)、哈希节点(dictEntry)、字典(dict)
Dict的扩容:dict在每次新增键值对时都会检查负载因子(used/size),当负载因子大于1并且服务器 没有执行BGSAVE或者其他后台进程时或者负载因子大于5时都会扩容,扩容为大于等于 dict.ht[0].used + 1的第一个2的n次方
Dict收缩:当负载因子小于0.1时,会进行收缩,收缩到大于等于 dict.ht[0].used第一个2的n次方
rehash:不管是扩容还是收缩,必定会创建新的哈希表进行rehash
过程:申请新的内存空间将dict.ht[0]的每一个哈希节点都rehash到dict.ht[1],并释放[0]的内存
这个过程不是一次性完成的,所以又称为渐进性的rehash,当每次执行新增,查询,修 改,删除操作时,都会查询是否在进行rehash,如果是就进行一次rehash,直到数据迁 移结束
注意:新增操作直接写入[1],查询修改删除[0][1]都会涉及
4.ZipList
它是一种双端链表由连续内存组成(不用链表的前后指针,节省内存),可以在任意一端压入/弹出操作,并且时间复杂度是o(1)
结构:zlbytes(总字节数)、zltail(尾偏移量)、zllen(节点个数)、entry(节点)、zlend(结束标 识)
特点:1.entry中记录了上一个节点和本节点长度来寻址所以不用前后指针
2.如果列表数据过多,影响查询性能,所以链表不宜过长
3.增或删较大数据时可能发生连续更新的问题
连续更新:因为entry中有一个记录上一个节点大小的变量,如果上一个节点小于254字节就用一个字节 来存储,否则就用5个字节来存储,如果恰好很多entry都处于254附近,如果更新某些数据 就有可能发生连续更新。
5.QuickList
问题:虽然ZipList节省内存,但是申请内存必须是连续的,所以ZipList最好不要存储大量数据,所以就引用了QuickList,他是一个双向链表,只不过链表中的每个节点都是一个ZipList
特点:可以对中间节点的ZipList进行压缩
6.SkipList
跳表首先是链表,元素按照升序排列,节点可能包含多个指针,指针跨度不同
特点:1.每个节点都包含score和element值
2.节点按照score排序,score值一样则根据element排序
3.每个节点包含多层指针
4.增删改效率与红黑树基本一致,但是实现更简单
7.RedisObject
Redis中的任意一个键和值都会被封装成一个RedisObject对象
48、Redis的数据类型
五种基本的数据结构:String、List、Set、ZSet、Hash
三种特殊的数据类型:Bitmap、HyperLogLog、GEO
1.String:
基本编码方式是raw,基于SDS实现的,如果SDS长度小于44字节,则会采用embstr编码,此时
RedisObject与SDS是一段连续的存储空间,如果存储的字符串是整数值,并且在Long的范围内,则会采用int编码,直接将数据存储在RedisObject的ptr指针上(刚好8字节)
2.List:
少数据用ZipList,数据过多采用QuickList实现List
3.Set:
当存储的所有数据都是整数,并且元素数量不超过一定数量时,会采用intSet,否则就会使用Dict,key存储值,value存储null
4.ZSet
因为是键值存储,可排序所以采用SkipList和Dict构成,当元素数量不多时会采用ZipList存储,score和element是紧挨在一起的entry,element在前score在后,score按照升序排列。
什么采用2种数据结构(压缩列表+跳表)实现?
Dict的作用是存储键值对,避免元素重复,方便元素查找
SkipList的作用是根据score值来对所有元素进行排序
5.Hash
Hash底层是由Dict实现的。当元素数量不多时采用ZipList存储,相邻的两个entry分别保存key和value
49、缓存穿透,缓存雪崩,缓存击穿
1.缓存穿透
定义:客户端请求的数据在数据库和缓存都不存在,这样缓存永远不会生效,这些请求都会由数据库来 处理
方案:1.缓存空对象(造成内存消耗)
2.布隆过滤(存在误判的可能)
以下是应对网络攻击
3.增加id复杂度防止id规律被猜测
4.做好基础的数据校验格式
2.缓存雪崩
定义:在同一时间段的大量缓存key同时失效或者redis宕机,导致大量请求都由数据库来处理
方案:1.给不同的key添加不同的TTL随机值
2.利用Redis集群提高服务可用性
3.缓存击穿
定义:被高并发访问并且缓存重建业务比较复杂的key突然消失了,无数的请求访问会给数据库带来巨 大冲击
方案:1.互斥锁(线程需要等待,可能有死锁的风险,数据一致性高)
2.逻辑过期(不保证一致性,线程无需等待性能好)
50、数据库自增 ID 和 UUID 对比
自增ID:需要由数据库自己生成影响性能,而且有时候还要利用last_insert_id来获取自增主键,在 mysql8.0版本之前存在id回溯问题,并且不能保证id全局唯一
UUID:由程序生成效率高,id全局唯一,但不是自增对于数据库的插入性能有影响,底层会频繁的进行 页分裂,指针重排,所以建议将UUID倒序变为递增,在加入一些业务字段作为主键id使用
51、为什么重写 equals 还要重写 hashCode,不重写会有什么问题
1.提高效率
重写hashCode方法,先进行hashCode比较,如果不同就没必要进行equals的比较了这就大大减少了queals的比较次数
2.为了保证同一个对象
如果重写了equals方法而未重写hashCode,在利用HashMap存储的过程中,就会出现明明两个对象的所有属性都相同,但是进行判断的hashCode值不同,造成数据的重复存储,不能保证数据的唯一性
52、equals和==
==:基本数据类型比较的是值是否相等,引用数据类型比较的是内存地址是否相同
equals:Object类中equals就是利用==实现的,但是像String、Date、File、包装类 等都重写Object类中的equals()方法,重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的"实体容"是否相同
53、如果让你自己实现哈希表,你会考虑什么问题?
1.设计原则:
一致性:如果a == b,则两者哈希值相同
高效性:计算高效简便
均匀性:哈希值均匀分布
2.避免hash冲突
如果hash冲突,直接形成链表存储,如果该链表过长,hash表会退化为链表,所以一旦链表超过某个阈值就采用树来存储(如红黑树,AVL树)
54、HashMap和HashTable的区别
线程:HashMap线程不安全,HashTable线程安全
效率:HashMap效率比HashTable高
HashMap可以存储null key和value,HashTable都不允许
HashMap在jdk8之后使用红黑树存储,而HashTable没有
HashTable基本不再使用了
55、Object 类你了解哪些方法
equals()
toString()
基本数据类型就是该数值,引用数据类型就是内存地址
hashCode()
计算hash值,便于使用HashMap等集合
finalize()
这个方法不需要程序员手动调用,JVM负责调用这个方法,GC会用到
用于在对象被回收时进行资源的释放
为什么永远不要主动调用finalize():
调用该方法可能会导致对象的复活
它的执行时间是没有保障的,完全由GC线程决定,一个糟糕的finalize()会严重影响GC性能
虚拟机中的对象的三种状态:
可触及的:从根节点开始,可以到达这个对象
可复活的:对象所有引用都被释放,但是对象有可能在finalize()中复活
不可触及的:对象的finalize()被调用,并且没有复活
wait()
一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
notify()
一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程,就会唤起优先级最高的那个
notifyAll()
所有被wait的线程都会被唤醒
注意:wait(),notify(),notifyAll()必须在同步代码块或者同步方法中,而且调用者必须是同步代码块或者同步方法中的同步监视器
56、Redis怎么实现排行榜
排行榜的功能,主要是使用sortset的数据结构,但是这里有一个比较要注意的情况就是时间的问题。比如相同战力,先上榜的玩家肯定排名靠前,这是毫无疑问的,但是总有些同学会忘记这件事情,同时,在更新的时候,要把原来值的小数去掉,再加上当前的时间运算之后的值把玩家id 当做key,把玩家战力当做score,把时间当做小数进行累加
57、expire 过期机制如何实现的?(Redis内存策略)
内存过期策略
在数据结构上(reidsDb),有两个Dict:一个是用来存储键值对,另一个是用来存储key的TTL
TTL的删除策略:
惰性删除:
他不是在TTL到期后就立即删除,而是在访问key的时候检查该key的存活时间,如果已经过期才删除
周期删除:
周期性的抽样部分的key,然后执行删除,周期清理有有两种方式
slow
在Redis服务初始化函数中设置定时任务,默认执行频率为每秒执行10次,每次不超过25毫秒
fast
Fast在每个事件循环前会调用BeforeSleep()函数,执行过期key清理,执行频率不固定,每次耗时不超过1ms
内存淘汰策略
Redis会在内存达到上限时,主动挑选部分key来释放更多内存。
默认:不淘汰任何key,内存满不允许写入新数据
volatile-ttl:对设置了TTL时间的key进行淘汰,TTL越小越先被淘汰
allkeys-random:对全体key随机进行淘汰
volatile-random:对设置了TTL的key进行随机淘汰
allkeys-lru:对全体key基于Lru算法进行淘汰
volatile-lru:对设置了TTL的key基于lru算法进行淘汰
allkeys-lfu:对全体key基于lfu算法进行淘汰
volatile-lfu:对设置了TTL的key基于lfu算法进行淘汰
LRU(Least Recently Used),最少最近使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
LFU(LeastFrequentlyUsed),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。
58、项目中如何解决的超卖和限制一人一单?分布式锁如何实现?
分布式锁:使用缓存技术(比如redis)
mysql:
一人一单:使用synchronized对userId进行加锁,解决一人一单问题
超卖:使用CAS法来解决超卖问题(实际上货物数量大于0即可)
问题:1.每次都要通过数据库,并发量过小,效率不高
2.单体项目可以适用,但是如果是分布式的项目多个jvm不能共享锁和内存,所以加入redis解决。
Redis:
因为Redis在处理核心业务是单线程的,所以不会存在线程安全问题,在创建订单的过程中Redis会保存用户订单并且扣减库存(lua脚本)直接就解决了以上问题。并且采用其他线程处理这些订单(消息队列),或者启用阻塞队列异步处理
59、Spring 事务了解吗,Spring 事务的注解不生效,是什么原因
Spring事物的传播行为
2.Spring不生效的原因:
方法权限只支持public
方法用final()修饰,动态代理不能代理final方法
未被spring管理的(自己new的对象)
方法内部调用,同一对象内调用没有使用代理,未被aop事务管理器控制
事务要生效是因为spring对该类做了动态代理,拿到了他的代理对象进行事务处理
解决方法:拿到事务代理对象(对象类型)AopContext.currentProxy()
自定义的回滚异常与事务回滚异常不一致
表不支持事务
60、跳跃表为什么不用平衡树这些数据结构实现?
1.从内存占用上来比较,跳表比平衡树更灵活一些。平衡树每个节点包含 2 个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为 1/(1-p),具体取决于参数 p 的大小。如果像 Redis里的实现一样,取 p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
2.在做范围查找的时候,跳表比平衡树操作要简单。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历就可以实现。
3.从算法实现难度上来比较,跳表比平衡树要简单得多。平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。
61、Java类加载的过程
加载
通过类的加载器在内存中生成类的对象
2.链接
2.1验证
文件格式、字节码验证等
2.2准备
1.为类变量分配内存并且设置类变量的默认初始值0
2.final static在编译阶段就会分配了,准备阶段会显式初始化
3.实例变量不会在这个阶段初始化,是随着对象一起分配到java堆中
2.3解析
将常量池中的符号引用转换为直接引用
3.初始化
执行类的构造方法<clinit>
会初始化除准备阶段的所有情况
62、类加载器的分类、双亲委派机制
类加载器分类:
启动类加载器(加载java核心库)
扩展类加载器(加载的是ext子目录下的类库)
系统类加载器(java的应用类)
双亲委派机制:
如果一个类加载器收到了类加载请求,他不会自己先去加载而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类,就继续向上委托,如果父类加载器可以完成类加载就成功返回,否则就由子加载器加载。
优点:避免类的重复加载、保护程序安全防止核心api被篡改
怎么打破双亲委派机制:
打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法
怎么判定两个class是相同的?
Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。同一个类只会被类加载器加载一次
63、PC寄存器
作用:存储指向下一条指令的地址(没有OutOfMemory),他是线程私有的
为什么使用PC寄存器记录当前线程的执行地址?
因为cpu需要不停的切换各个线程,来回切换的过程中需要知道接着从哪开始继续执行
64、虚拟机栈
栈是运行时的单位,堆是存储的单位,即:栈是解决程序运行的问题(如何执行),堆解决的是数据存储的问题(数据放在哪,怎么放),虚拟机栈是线程私有的,GC不包含虚拟机栈
存储单位:栈帧
栈帧内部结构:1.局部变量表(其中表中的槽位(slot)是可以重用的)
作用:数字数组,存储方法参数和定义在方法体内的局部变量(8种基本数据类型, 引用类型),32位以内只占用一个slot,64位(long、double)占两个slot
存储单位:slot(槽)
其他作用:也是垃圾回收的根节点,只要被局部变量表中的直接或者间接引用的对 象就不会被回收
访问方式:通过slot上的索引来访问局部变量表种指定的值
2.操作数栈
作用:保存计算过程的中间结果
访问方式:入栈出栈
3.动态链接
作用:将符号引用转换为调用方法的直接引用(指向运行时常量池的方法引用)
方法调用的 早期绑定:目标方法在编译器可知(final方法,静态方法等)
晚期绑定:目标方法在编译器不可知(多态)
4.方法返回地址
作用:存放调用该方法的pc寄存器的值
5.其他信息
65、开发中遇到的异常
Java虚拟机允许java栈的大小是动态的或者固定不变的,如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量就会抛出一个StackOverFlow
如果没有足够的内存去创建对应的虚拟机栈,那么会抛出一个OutOfMemory,没有空闲的内存,并且垃圾收集器也无法提供更多内存
66、堆
作用:几乎所有对象实例以及数组都应当在运行时分配在堆上,堆是所有线程共享的,其中还可以 划分线程私有的缓冲区(Thread Local又叫TLAB)
分区:年轻代(1/3)、老年代(2/3)
年轻代:存储生命周期比较短的对象
伊甸园区(8/10)、幸存者0区(1/8)、幸存者1区(1/8)
老年代:存储生命周期比较长的对象
分代的目的:优化GC性能
对象分配的过程:1.new的对象放入伊甸园区
2.伊甸园满,对伊甸园区minor GC,存活对象放入幸存者0区
3.如果伊甸园区再次满,minor GC,存活对象和幸存者0区对象放入幸存者1区
4.如果这个循环达到了15次,就进入了老年代
5.如果老年代满了进行major GC
关于垃圾回收:频繁在新生代,很少在养老代,几乎不在元空间
minor GC:新生代收集
major GC:老年代收集
full GC:对整个堆和元空间收集
GC都会stop the world:major GC比minor GC慢十倍以上
Full GC触发情况:调用System.gc(),系统建议执行,但是不一定会执行
老年代或者元空间空间不足
TLAB(Thread Local):包含在伊甸园区(占用1%),在并发的条件下使用(线程安全)
静态变量和StringTable也在堆中:
StringTable之前是放在永久代当中,但是永久代回收效率很低,在full gc才会触发,放到堆里能够被及时回收
67、元空间
为什么用元空间替代永久代?
永久代的空间大小很难确定、对永久代调优十分困难
栈、堆、元空间交互关系
作用:元空间在逻辑上是属于堆的一部分,但是元空间在实现上是独立于堆的
区别:7,8的主要区别元空间不在虚拟机设置的内存中(7),而是使用本地内存(8)
内部结构:
1.类型信息、域信息:
类的完整的有效名称、权限修饰符、父类名称等
2.运行时常量池:包含多种不同的常量、编译器确定的字面量、运行期解析的方法或者字段引用
字节码文件包含了常量池:包含数值量。类,方法的符号引用
作用:减少类、接口编译产生的字节码文件的大小
元空间内部包含运行时常量池:具备动态性
3.方法信息:
方法名称、返回类型、形参数量和类型、修饰符等
68、如何解决OOM
内存泄漏:通过工具查看GC Roots的引用链,定位内存泄漏代码的位置
内存溢出:调大堆参数,代码上检查是否存在生命周期过长的对象
69、创建对象的步骤(对象头)
1.判断对象的类是否加载、链接、初始化
先检查在元空间中的运行时常量池是否有该类的符号引用,如果没有会在双亲委派模式下进行类加载(找不到会抛出异常)。
2.为对象分配内存
内存规整:指针碰撞(标记压缩算法),内存不规整:空闲列表分配(标记清除算法)
3.处理并发安全问题(每个线程分配一块TLAB)
4.初始化分配到的空间(所有属性设置默认值)
5.设置对象的对象头
synchronized的信息就在对象头当中
6.执行init方法
70、对象的内存布局
71、字符串拼接操作
常量与常量拼接的结果在字符串常量池(Stringtable),原理是编译期优化
Stringtable中不会存在相同内容的常量
只要有一个变量结果就存在堆中(不在Stringtable中),拼接原理是StringBuilder,相当于
new String(),具体的内容为拼接的结果
如果调用intern()方法,则主动将Stringtable中还没有的字符串对象放入池中,并返回此对象的地址
String s = new String("a") + new String("a");
s.intern();
String s1 = "aa";
System.out.println(s3 == s4);//jdk1.6 false, jdk1.7true
/*
解释: 1:相当于new String("aa");//字符串常量池中没有2:字符串常量池没有"aa",将s对象的地址存入常量池所以true总结:1.new String("a");会创建两个对象,一个是new的,一个是Stringtable中的2.intern():jdk6:如果Stringtable有,并不会放入,返回Stringtable的地址如果Stringtable没有,会把此对象复制一份,放入Stringtable,并返回Stringtable的地址jdk1.7以后:如果Stringtable有则不会放入,返回Stringtable的地址如果Stringtable没有,会把对象的引用地址复制一份,放入Stringtable,并返回Stringtable的引用地址
*/
72、什么是垃圾
垃圾是指在运行程序中没有任何指针指向的对象
73、GC标记阶段的算法
引用计数算法
实现:对每个对象保存一个整数的属性,用来记录对象被引用的情况
优点:实现简单,判定效率高
缺点:需要单独字段存储该值,每次赋值都需要更新该值,并且无法处理循环引用的情况
可达性分析算法
实现:以GC Roots为起始点,按照从上至下的方式搜索,判断该对象是否可达,如果目标没有任何引用链,那么就要被回收,可以解决循环引用
GC Roots:1.虚拟机栈中局部变量表中的参数
2.堆中静态属性所引用的对象
3.Stringtable中的引用对象
4.被同步锁持有的对象
5.如果是堆空间中的可以用虚拟机栈,元空间作为root
如果是元空间可以用堆空间,虚拟机栈作为root...
74、垃圾回收为什么需要STW
因为要使用可达性分析算法来判断内存是否回收,那么分析工作一定要在能保障一致性的快照中进行,所以必须要STW,一般是在标记阶段(枚举根节点)进行stw
75、垃圾清除阶段的算法
标记清除算法
标记:标记所有被引用的对象
清除:如果发现某个对象没有标记为可达对象,则将其回收
缺点:效率不高,清理出来的空闲内存不连续,有内存碎片,需要维护一个空闲列表
何为清除:并不是真的置空,而是把需要清除的内存地址放到空闲列表中。
复制算法
将活着的内存分为两块,每次只使用其中一块,垃圾回收时将正在使用时活着的对象复制到未被使用的内存中,之后清楚正在使用的内存块中的所有对象,交换两个内存的角色。
优点:没有标记和清除过程,实现简单,不会出现内存碎片
缺点:需要两倍的内存空间,适合于垃圾对象很多,存活对象很少的场景(年轻代的幸存者0、1区)
标记压缩算法
标记:标记所有被引用的对象
压缩:将所有的存活的对象压缩到内存的一端,按顺序排放,清除边界外的所有空间
优点:消除了复制算法中,内存减半的高额代价
缺点:效率低于复制算法、需要STW、
76、System.gc()
会显式触发full GC,但是无法保证一定进行full GC,
安全点:垃圾回收并非在所有的地方都能停顿下来进行GC,只有在特定的位置才能停顿下来做GC
77、并发与并行
并发:多个事情,在同一时间内同时发生了(会互相抢占资源)
并行:多个事情,在同一时间点上同时发生了(不会互相抢占资源)
78、强引用、软引用、弱引用、虚引用
强引用:Object obj = new Object();就是强引用,只要强引用还在垃圾回收器就永远不会回收该对象
强引用是可触及的可达的,java中99%都是强引用,他是内存泄漏的主要原因之一
软引用:在系统将要发生溢出之前,将会把这些对象列入回收范围内进行二次回收
比如:高速缓存,描述一些还有用但是非必须的对象
new SoftReference<>();
弱引用:该种对象生存到下一次GC之前,无论空间是否够用都会被回收
跟软引用类似都是保存一些可有可无的缓存数据,从而起到加速系统的作用
在ThreadLocal源码中也使用到了弱引用
new WeakReference<>();
虚引用:一个对象是否有虚引用不会对其生命周期产生影响,它的作用是该对象被回收时收到一个系统通知(跟踪垃圾回收过程)
79、垃圾回收器
Serial回收器:串行回收
采用复制算法,是HotSpot中Client模式下默认的新生代垃圾回收器
Serial Old回收器:串行回收
使用标记压缩算法,是Client模式下默认的老年代垃圾回收器
可以与新生代Parallel Scavenge配合使用,作为老年代CMS的后备方案
优点:简单高效,对于限定单核cpu效率高,javaweb中是不会使用该收集器的
3.parNew回收器:并行回收
年轻代采用复制算法,stw也存在
parNew效率一定比Serial高吗?
多cpu效率高,单个cpu效率比不上Serial,因为采用并行会来回切换cpu,产生额外开销
4、parallel Scavenge回收器:吞吐量优先
采用复制算法、并行回收和stw机制,与parNew的区别是它具有自适应调节,目标是吞吐量优先
自适应调节策略:年轻代的大小、伊甸园区与幸存者区的比例、晋升老年代的次数都会自动调整,以达到最大吞吐量
5、parallel Old收集器
采用标记压缩算法,基于并行回收和stw机制
两者配合是java8默认的垃圾回收器
6、CMS回收器:低延迟
采用标记清除算法(老年代回收器),也会stw,只能与parNew与Serial配合
初始标记:出现stw,标记出能和GC Roots直接关联到的对象,速度很快
并发标记:遍历整个内存,找出所有不可达对象
重新标记:出现stw,修正并发标记期间,程序继续执行而导致的变化
并发清理:进行垃圾回收,不会stw
优点:由于并发标记和并发清理不会stw,所以是低延迟的,但是如果清理期间内存不足就会使用Serial Old收集器作为后备方案,stw
为什么要用标记清除算法:因为如果用标记压缩算法,在并发清理阶段原来用户的线程就不可用,需要保证用户线程持续执行 ,防止stw
缺点:会有内存碎片、吞吐量变低、产生浮动垃圾,只有在下一次清理时才会回收
7、G1回收器:区域化分代式
在延迟可控的情况下尽可能获得高的吞吐量,他是全功能收集器(年轻代与老年代回收都用它)
他在后台维护一个优先级列表:根据每次允许的时间,优先收集价值最大的区域
特点:1.可以有多个GC线程同时工作,此时stw,也可以与应用程序交替执行
2.将堆分为,这些区域包含了年轻代与老年代
3.内存的回收是以region为基本单位,region间是复制算法,整体上看是标记压缩算法
4.针对于多核cpu大内存机器
设置H的原因:对于大对象默认会直接分配到老年代,但是如果是一个短期存活的大对象,就会对GC产生负面影响,为了解决这个问题所以划分出了H区
最多有2048个region,每个region的大小为堆大小/2048,并且大小必须是2的倍数
年轻代GC(Young GC)
并行独占式(stw)的收集器,使用复制算法
脏卡表队列作用:
Reset更新需要线程同步,所以开销会很大,因此不能实时更新,因此我们需要把引用对象被其他对象引用的关系放在一个脏卡表队列中,当年轻代回收的时候会进行STW,所以我们也正好把脏卡表队列中的值更新到Rset中,这样不仅没有涉及到开销问题,还可以保证Rset中的数据是准确的
老年代并发标记过程
当堆内存到达45%,开始老年代并发标记,标记完成马上开始混合回收(与CMS类似)
混合回收
它不需要对整个老年代进行回收,一次只回收一部分Region
80、每次Minor GC都需要全局扫描吗
不是,都是使用Remembered Set来避免全局扫描,对于G1来讲每个Region都对应一个Remembered Set
Remembered Set的作用就是记录当前Region中哪些对象被外部引用指向,当引用类型写操作时,先暂时中断(脏卡表,不会中断),然后判断当前引用数据类型是否被其他对象所指向,如果不指向直接放入Region中,如果被指向其他对象,那么要判断其他对象是否在要插入的Region中,如果不在就放入Remembered Set中
81、如何避免频繁的FULL GC
如果堆中经常存放大对象,我们就要加大堆空间
如果是在并发处理的过程之前空间耗尽,调小触发GC周期的java堆占用阈值
最大停顿时间过短,导致在规定的时间无法完成GC,要加大最大停顿时间
82、Redis中的大key问题
Big Key:1、key本身数据量过大:一个String类型的key值为5M
2、key中成员过多:ZSET类型,成员数量为10000个
3、key中成员数据量过大:Hash类型,成员数量100但是Value占用了100MB内存空间
危害:1.对于Big Key执行读请求,少量的QPS就可能导致带宽使用率被占满
2.对Big Key数据序列化反序列化会导致cpu使用率飙升
3.Big Key所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
4.元素较多的集合做运算会耗时
序列化与反序列化:
定义:将java对象转换为字节序列的过程,反序列化是指将字节恢复为java对象
作用:便于在磁盘或者内存中存储这些字节流进行io传输或者网络传输
1.如何发现Big Key
1.使用redis-cli --bigkeys遍历分析所有的key
2.利用第三方工具
2.如何避免Big Key
使用恰当的数据类型存储对象
合理拆分数据,拒绝Big Key
3.假如有hash类型的key,其中有100万对field和value,field是自增id,这个key存在什么问题?如何优化?
存在的问题:hash的entry超过500会使用哈希表存储而不是zipList,内存占用较多
可以设置参数让其使用zipList,但是就会导致big key问题
解决方案:拆分为小的hash,将id/100作为key,将id%100作为field,这样每100个元素为一个Hash
83、ThreadLocal
1.应用
1.在实际业务中存放当前线程User对象
2.Spring的@Trasactional注解源码中用到了ThreadLocal,比如在其中对数据库操作必须要保证是从数据库连接池中拿到的是同一个连接,不能拿不同的连接(否则事务不生效)
2.ThreadLocal源码分析
1.set方法
每一个ThreadLocal都是线程独有的,并且其中包含线程独有的map(key-value),key是当前ThreadLocal对象,value是我们要存储的值他们是由Entry来管理的,key使用了弱引用指向了ThreadLocal
3.ThreadLocal内存泄漏怎么解决
为了防止key变为null,value内存泄漏,所以调用完成后一定要加tl.remove()
弱引用解决了key的内存泄漏,remove解决了value的内存泄漏
引申问题:线程池源码中每次将线程放回去也会remove?
长时间在内存中存在,线程池每拿一次,就放入一次,如果不remove,就会被填满
84、CAS(compare and swap)
ABA:这个ABA问题是这样的,假如说你有一个值,我拿到这个值是1,想把它变成2,我拿到1用cas操作,期 望值是1,准备变成2,这个对象Object,在这个过程中,没有一个线程改过我肯定是可以更改的,但是 如果有一个线程先把这个1变成了2后来又变回1,中间值更改过,它不会影响我这个cas下面操作,这就 是ABA问题
解决方法:加入版本号
85、new Object()占了多少个字节
1.对象头:8字节、类型指针:4字节(开启压缩)、实例数据:0字节(Object中什么也没有)、padding:4字节(之前的数据必须要被8整除,不整除自动补齐)共16字节
2.对象头:8字节、类型指针:8字节(不开启压缩)、实例数据:0字节(Object中什么也没有)、padding:0字节(之前的数据必须要被8整除,不整除自动补齐)共16字节
new User(); 其中有int age,string name几个字节
对象头:8字节、类型指针:4字节(开启压缩)、实例数据:4+4字节(int + String指针)、padding:4字节(之前的数据必须要被8整除,不整除自动补齐)共24字节
86、synchronized锁升级过程
无锁 - 偏向锁 - 轻量级锁 (自旋锁,自适应自旋,无锁cas)- 重量级锁
synchronized是一把非公平锁
过程:1.new 对象,刚开始来了一个线程是偏向锁状态,相当于只有一个标识(只有该线程可用)
2.如果好多线程同时访问,锁升级到轻量级锁(cas)其他线程循环等待或者调用wait()也会升级
3.超过限制(cpu占用过多),升级到重量级锁(向内核态申请),底层会维护一个队列(等待 不消耗cpu)
锁降级在特定情况下(gc)会发生,但是没有意义
synchronized实现过程:1.java代码:synchronized
2.字节码:monitorenter moniterexit
3.jvm在执行过程中锁自动升级
4.汇编:lock comxchg
偏向锁:不是一把锁,就是一个简单的标记
轻量级锁:jvm级别处理完成,while循环(cas操作),不需要经过操作系统
重量级锁:交给操作系统处理(锁其中有个等待队列)
可以通过对象头来确定该对象到底是什么锁
自旋锁一定比重量级锁效率高吗?不一定,讲一下锁升级过程即可
synchronized可以保证原子性和可见性,但是无法保证有序性!
86、单例模式(懒汉式)
//懒汉式
//懒汉式:线程安全
class Singleton{//私有的构造器private Singleton(){}private static Singleton instance = null;private static Singleton getInstance(){if(instance == null){synchronized(Singleton.class){if(instance == null){instance = new Singleton();}}}return instance;}
}
87、并发领域的三大特性
原子性:
指一个操作不可以被打断,即多线程环境下,操作不能被其他线程所干扰
可见性:
当一个线程修改了某一个共享变量的值,其他线程是否能够立刻知道该变更,JMM规定所有的变量都存储在主内存中
原理:缓存一致性协议(通常是指MESI)
有序性:
为了提升性能,编译器和处理器通常会对指令重排序。
hanppens-before原则(jvm规定重排序必须遵守的原则):以下情况不允许指令重排序(不用背)
原理:JMM模型中有8个指令完成数据的读写,通过其中load和store指令相互组合成4个内存屏障实现禁止指令重排序(jvm层面)
注意:cpu层面每个厂商都不一样,但是jvm规范规定了这四种屏障的作用(jvm层面)
cpu是由一条lock指令来实现的
88、缓存行
多核cpu有多级缓存如L1,L2,L3一般L1和L2是在核的内部,L3是共享的缓存
多级缓存带来的问题就是数据不一致,所以需要缓存一致性协议
缓存行:从内存读到缓存是按块读的也就是cache line(缓存行),一个缓存行是8字节
缓存一致性协议(因特尔的缓存一致性协议MESI):当一个核需要读取另外一个核的脏缓存行时发生
注意:如果发现缓存行数据不一致,硬件就会发起通知,这是硬件层面的我们无法控制
89、volatile
java层面:加上volatile
字节码层面:有Access_flags:volatile
怎么保证可见性:每次读取都是从主存当中读,不会在缓存中读
90、DCL要不要加volatile?(指令重排)
DCL(Double Check Lock):86单例模式就是如此
不上锁的代码可以直接读取到上锁代码的中间态(p18 23:00)
底层进行指令重排,在左侧线程半初始化过程中,右侧线程在执行(INSTANCE == null)时读取到上锁代码的中间状态,导致多线程出现并发问题,所以需要加volatile禁止指令重排,来保证指令的有序性
91、线程池的核心参数与运行过程
corePoolSize 核心线程数
maximumPoolSize最大线程数如果线程数超过了核心线程数+阻塞队列最大容量并且没超过最大线程数就会生成另一个线程处理
keepAliveTime 超出核心线程数的最大线程(非核心线程)空闲时间
Timeunit超出核心线程数的最大线程(非核心线程)存活空闲的单位
BlockingQueue<Runnable> 阻塞队列:核心线程不够用,其他任务就会放入阻塞队列
ThreadFactory 线程工厂创建线程
RejectedExecutionHandler 拒绝策略核心线程 非核心线程 阻塞队列都满的情况下去走拒绝策略
执行流程:
拒绝策略:1.丢弃任务并抛出异常。
2.丢弃任务,但是不抛出异常。
3.丢弃队列最前面的任务,然后重新提交被拒绝的任务
4.由调用线程(提交任务的线程)处理该任务
92、公平锁与非公平锁
公平锁:每个线程在执行lock方法时,会先查看是否有线程排队,如果有,直接去排队,如果没有才回去尝试竞争锁资源
优点:所有线程都会得到资源不会饿死在队列中
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他线程都会阻塞,cpu唤醒阻塞线程的开销会很大
非公平锁:每个线程都会在执行lock方法时,先尝试获取锁资源,获取不到在排队
优点:减少cpu唤醒线程的开销,吞吐量比公平锁稍高,
缺点:可能会导致队列中的线程一直获取不到锁,导致饿死
93、AQS(AbstractQueuedSynchronier)
AQS实际上就是一个队列(线程的等待队列):由双向链表组成
state:锁资源是否被占用(0未被占用、1被占用)
head、tail:头、尾指针
Node:线程节点
thread:到底是哪个线程
waitStatus:CANCELLED:表示线程取消了等待 1
SIGNAL:表示后续节点需要被唤醒 -1
CONDITION:线程等待在条件变量队列中 -2
PROPAGATE:在共享模式下 -3
0: 初始状态
其实可以归于两个状态 >0 取消 <0 等待
为什么要设置尾节点?
如果没有tail指针,应对高并发场景我们肯定要将整个双向链表上锁,加入tail只需要CAS操作即可添加尾节点,根本不需要上锁,效率提高明显
为什么是双向链表?
因为添加一个线程节点时,需要看一下前面这个节点的状态,如果前面的节点是持有线程的过程中,就需要等待,如果这个节点处于撤销状态,那么直接跳过这个节点,所以需要双向链表来考虑前一个节点的状态
94、ReentrantLock
1.lock
ReentrantLock可以实现公平锁和非公平锁,所以lock也实现了两种锁
//非公平锁
final void lock() {//以CAS的方式,尝试将state从0改为1if (compareAndSetState(0, 1))//成功,将当前线程设置到AQS的exclusiveOwnerThread,代表当前线程拿着锁资源(与可重入锁也有关)setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}
//公平锁
final void lock() {acquire(1);
}
2.acquire()
public final void acquire(int arg) {//tryAcquire有公平锁和非公平锁两种实现//公平锁:如果state为0,再看是否有线程进行排队,如果有就去排队,如果是锁重入,直接获取锁//非公平锁:如果state为0,直接尝试CAS修改,如果是锁重入,直接获取if (!tryAcquire(arg) &&//addWaiter():线程没有通过tryAcquire()拿到锁,需要将当前线程封装为Node对象,去AQS内部排队//acquireQueued():查看当前线程是否在队列前面,如果是就尝试获取锁,否则挂起acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg))selfInterrupt();
}
3.tryAcquire()
//非公平锁的tryAcquire()
final boolean nonfairTryAcquire(int acquires) {//拿到当前线程final Thread current = Thread.currentThread();//拿到AQS的stateint c = getState();//如果state等于0,表示没有线程占用资源if (c == 0) {//使用CAS尝试修改state,如果成功就代表拿到锁资源if (compareAndSetState(0, acquires)) {//将当前线程设置到AQS的exclusiveOwnerThreadsetExclusiveOwnerThread(current);//返回truereturn true;}}//state不为0,当前锁被占用//如果获得的线程与AQS的exclusiveOwnerThread相同,代表重入锁else if (current == getExclusiveOwnerThread()) {//锁重入:state加1int nextc = c + acquires;//如果加到上限会变为负数,健壮考虑会报错if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");//设置statesetState(nextc);//返回truereturn true;}return false;
}
//公平锁的tryAcquire()
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {//首先查看有没有线程排队if (!hasQueuedPredecessors() &&//没有线程排队,尝试获取锁compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}
这两者区别就是:公平锁会查看是否有线程进行排队,如果有,直接返回false,如果没有才会CAS获取锁。非公平锁直接CAS获取锁
总结:获取锁成功返回true,获取锁失败返回false
4、addWaiter()
private Node addWaiter(Node mode) {// 将线程封装为Node对象,传入mode为nullNode node = new Node(Thread.currentThread(), mode);// 获取尾节点Node pred = tail;//如果尾节点不为nullif (pred != null) {//当前节点的上一个节点就是尾节点//此处线程不安全,有许许多多线程将自己prev指针指向当前尾节点node.prev = pred;//为了避免并发问题,以CAS的方式将tail指向当前节点//其中只有一个会CAS成功,其余就会去enq重排if (compareAndSetTail(pred, node)) {//将之前的tail的next指向现在的节点(当前尾节点)pred.next = node;//返回当前节点return node;}}//尾节点为null(队列为空),或者CAS操作失败后,会执行enq,将当前node排到队列尾enq(node);return node;
}
//enq()重排
private Node enq(final Node node) {//死循环for (;;) {//将尾节点赋值给tNode t = tail;//如果尾节点为null,初始化这个AQSif (t == null) { //CAS执行head头节点,head节点本身不存储任何线程if (compareAndSetHead(new Node()))//头节点等于尾节点tail = head;} else {//AQS不为null,说明addWaiter()有许多线程在抢第一次CAS进入尾节点失败//或者第一次创建AQS队列//循环CAS去加入AQS双向链表尾部node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;//返回nodereturn t;}}}
}
总结:1.先初始化Node节点,将当前线程传入,并标记为互斥锁
2.尝试将当前Node节点插入尾部(CAS尝试)
3.AQS队列为空,或者当前线程CAS失败插入尾部,执行enq()
4.如果是队列为空,那么初始化头节点,如果队列不为空for循环CAS直到插入尾部成功
5.返回当前节点(尾节点)
5、acquireQueued()
final boolean acquireQueued(final Node node, int arg) {//竞争锁资源失败boolean failed = true;try {//中断标识boolean interrupted = false;for (;;) {//predecessor()获取当前节点的上一个节点final Node p = node.predecessor();//如果上一个节点是头节点那么就执行tryAcquire()竞争锁资源if (p == head && tryAcquire(arg)) {//锁资源竞争成功//将head设为当前节点setHead(node);//当前节点的上一个节点设为null,调用GC清理上一个节点p.next = null; //竞争锁资源成功failed = false;//返回中断return interrupted;}//下一个代码块讲解//判断当前线程能否被挂起if (shouldParkAfterFailedAcquire(p, node) &&//找到上一个节点是正常的节点后,调用方法将该线程挂起//就是调用Unsafe的park方法parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//获取上一个节点的状态int ws = pred.waitStatus;//如果是SIGNAL状态直接返回true,表示当前节点可以被挂起if (ws == Node.SIGNAL)return true;//如果状态大于0,说明肯定是CANCELLED状态,绕过这个节点找上一个节点的上一个if (ws > 0) {//循环直到找到小于0的节点//这一步相当于筛选大于0的节点,将那些节点删除do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {//可能为0,-2,-3,直接以CAS的方式将节点状态改为SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}//百分之99都不会走到这return false;
}
总结:1.如果当前节点的上一个节点是头节点那么就用tryAcquire()尝试获取锁
2.获取锁成功将头节点设置为当前节点
3.不是头节点或者获取锁失败将当前节点挂起
6、unlock()
释放锁的核心:就是将state从大于0的数值更改为0即为释放锁成功
public void unlock() {//每次state只减1sync.release(1);
}
7、release()
public final boolean release(int arg) {//下一个代码块讲解//尝试释放锁是否成功if (tryRelease(arg)) {//锁被成功释放Node h = head;//如果头节点不为null,并且head的状态不等于0(初始状态),就将head节点唤醒if (h != null && h.waitStatus != 0)//unpark唤醒节点unparkSuccessor(h);return true;}//锁释放失败(释放一次没有完全释放掉)返回falsereturn false;
}
protected final boolean tryRelease(int releases) {//直接获取state之后 -1int c = getState() - releases;//如果当前线程不是锁线程那么直接报错if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();//标识boolean free = false;//state等于0if (c == 0) {//锁被释放free = true;//exclusiveOwnerThread == nullsetExclusiveOwnerThread(null);}//state不为0,将-1后的值赋给statesetState(c);//返回锁是否成功释放标志return free;
}
private void unparkSuccessor(Node node) {//获取当前节点的状态int ws = node.waitStatus;//如果当前节点状态小于0,将head节点状态设置为0if (ws < 0)compareAndSetWaitStatus(node, ws, 0);Node s = node.next;//当前节点下一个节点为null或者当前节点的状态大于0//需要找到离head节点最近的有效Nodeif (s == null || s.waitStatus > 0) {s = null;//从后向前找(addWaiter添加方式决定的)for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}//找到最近的node后直接唤醒if (s != null)LockSupport.unpark(s.thread);
}
95、ConcurrentHashMap
1.为什么HashMap线程不安全
putVal()若桶为空,多线程操作,值会出现覆盖情况
2.为什么HashTable效率低下
用synchronized锁住整个表来同步。线程竞争激烈时,一个线程访问HashTable同步方法,其他线程访问会处于阻塞或轮询状态
3.ConcurrentHashMap是什么,具体操作内容
1)sizeCtl 来控制了初始化、扩容大小,是否正在进行初始化和扩容
2)Node 继承至Entry,用于存储数据,是存储的基本单元,同时在基于Node的基础上,为了实现红黑树,扩展了TreeNode、TreeBin;TreeNode用于在红黑树存储数据,TreeBin封装了TreeNode,提供了读写锁;
3)get方法:计算hash值,如果定位到数组本身,直接返回;如果不是,根据当前节点类型,分别按照链表和红黑树的方式去查找当前元素所在的位置(使用voliatile修饰,不用加锁)
4)put方法:如果没有初始化,首先进行初始化;使用CAS无锁方式插入,如果初始化完成,并且该数组索引上有数据采用加锁方式插入,如果发现需要扩容,首先进行扩容;如果存在hash冲突,需要挂在table节点下面,先将当前table节点加锁,链表按照尾插入方式进行插入,红黑树按照红黑树的结构进行插入,同时put在插入过程中,如果发现链表长度超过8个并且数组长度大于64,就将链表改造成红黑树,并且还会进行元素个数的统计,并检查是否需要扩容;
1.根据 key 计算出 hashcode 。
2.判断是否需要进行初始化。
3.即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败 则自旋保证成功。
4.如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
5.如果都不满足,则利用 synchronized 锁写入数据。
6.如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
transfer方法:扩容方法,如果第一次扩容将sizeCtl-2,如果发现sizeCtl为负数,不是第一次扩容那么就协助扩容,sizeCtl-1,这个方法是用synchronized包上的
同时为了避免多个线程有并发冲突,每个线程会进行步长的方式在节点之间来进行操作
5)扩容方法:1.8里面,为了提高效率,工作线程会进行并发扩容,同时为了避免多个线程有并发冲突,每个线程会进行步长的方式在节点之间来进行操作;
4.ConcurrentHashMap的get方法是否要加锁,为什么
不用,Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改因为hash冲突修改结点的val或者新增节点的时候是对线程B可见的。
5.ConcurrentHashMap 的 Key 和 Value 都不能为 null,而 HashMap 却可以,设计的原因是什么
如果 Value 为 null 会增加二义性,即多线程情况下 map.get(key) 返回 null,我们无法区分 Value 原本就是 null 还是 Key 没有映射,Key 也是类似的原因。
而用于单线程状态的hashmap却可以用containKey(key) 去判断到底是否包含了这个null
但是多线程状态可能线程a返回null就是没有该key,但是线程b在线程a containKey(key)之前却添加了这个key,这就出现了二义性
6.ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁synchronized 来代替重入锁 ReentrantLock
JVM 开发团队在 1.8 中对基于 JVM 的 synchronized 做了大量性能优化(锁升级),更大优化空间,更自然。
7.ConcurrentHashMap能完全取代HashTable呢?迭代器是强一致性还是弱一致性?
不能。两者迭代器一致性不同,HashTable的迭代器是强一致性的,而ConcurrentHashMap是弱一致的,如get,clear,iterator方法。
Hashtable的任何操作都会把整个表锁住,是阻塞的。好处是总能获取最实时的更新,比如说线程A调用putAll写入大量数据,期间线程B调用get,线程B就会被阻塞,直到线程A完成putAll,因此线程B肯定能获取到线程A写入的完整数据。坏处是所有调用都要排队,效率较低。
ConcurrentHashMap 是非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。
选择哪一个,是在性能与数据一致性之间权衡。ConcurrentHashMap适用于追求性能的场景,大多数线程都只做insert/delete操作,对读取数据的一致性要求较低。
8.为什么ConcurrentHashMap和HashMap要求数组的长度都为2^n
因为在计算数组位置下标时,做与运算时是用n-1有很多1可以参与与运算,不容易导致哈希冲突如果设置为别的数值很有可能导致哈希冲突
9.为什么链表长度大于8才扩容?
8这个数值是基于泊松分布算出来的,链表长度大于8的概率已经很低很低了,所以如果设计的比8大那么可能链表永远都不会转换为红黑树
10.transfer中的lastRun机制是什么?
如果发现链表中有连续的节点都要放到新数组的同一个位置,那么就统一挪动,不用挨个挪动
96、如果获得分布式锁的机器宕机了,如何解决死锁问题?
redis设置过期时间即可解决死锁问题
97、为什么要使用线程池
让线程对象可以反复复用,不需要每次执行任务时,构建一个线程,等到任务处理完在销毁(减少资源消耗)
98、线程池有哪几种状态
99、使用队列时有什么需要注意的吗
使用有界队列时,需要注意线程池满了之后执行拒绝策略
使用无界队列时,需要注意如果任务提交的速度大于线程池处理的速度,可能会导致内存溢出
为什么线程池要使用阻塞队列:
a.线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲
b.阻塞队列可以保证任务队列中没有任务时阻塞队列获取任务的线程,使得线程进入wait状态,释放cpu
100、为什么JVM转到老年代要用15次
因为对象头中只占了4个bit位,最多只能表示15
101、ReentrantLock与synchronized的区别
ReentrantLock可以实现公平锁与非公平锁,而synchronized只有非公平锁
synchronized有锁的升级机制
ReentrantLock要手动上锁,释放锁,并且有锁的超时时间(可以预防死锁),synchronized没有
二者的底层实现不一样:synchronized是同步阻塞,采用的是悲观并发策略;Lock是同步非阻塞,采用的是乐观并发策略(底层基于CAS+AQS实现)
102、Bitmap底层数据结构是什么?为什么考虑用Redis实现签到功能?
利用String数据类型实现了BitMap,最大上限为512M,转换为bit为2^32个bit位,常用于签到统计
因为如果使用mysql关系型数据库一个签到功能的日期就会设计很多字段,浪费许多系统资源,但是使用redis一个bitmap就可以解决上述问题
103、GEO 底层采用的哪种数据结构?为什么考虑用GEO实现附近的人功能
Sorted Set实现的,底层是利用dict数据结构来实现的
因为redis很快,需要实时信息,并且redis提供了很多内置api接口供我们调用
104、Redis单线程模型?为什么?
如果仅仅聊Redis的核心业务部分(命令处理),答案是单线程,如果是聊整个Redis,那么答案就是多线程
Redis v4.0:引入多线程异步处理一些耗时较旧的任务,uRedis v6.0:在核心网络模型中引入多线程,进一步提高对于多核CPU的利用率
因此,对于Redis的核心网络模型,在Redis 6.0之前确实都是单线程。是利用epoll(Linux系统)这样的IO多路复用技术在事件循环中不断处理客户端情况
为什么要使用单线程模型?
抛开持久化不谈,Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升
多线程会导致过多的上下文切换,带来不必要的开销
引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣
105、HyperLog
Redis中的HLL是基于string结构实现的,单个HLL的内存永远小于16kb,作为代价,其测量结果是概率性的,有小于0.81%的误差。不过对于UV统计来说,这完全可以忽略
106、mysql中的锁
从操作的类型划分:读锁、写锁
读锁:也称为共享锁用S表示,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞
写锁:也称为排他锁用X表示,当前写操作没有完成前,他会阻断其他写锁和读锁
从数据的操作粒度上分:表级锁、页级锁、行锁
为了尽可能的提高系统的并发性能,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发量,但是管理锁很消耗系统的资源,因此数据库要在高并发响应和系统性能两方面进行平衡
表锁:在对表执行ALTER TABLE、DROP TABLE、这类DDL语句时就会加表锁,表级别的S和X锁了解即可
意向锁(表级锁):InnoDB支持多粒度锁,表级锁和行级锁可以共存。意向锁是系统自己维护的,无法手动操作
意向共享锁:事务中对表中的某一行加共享锁
意向排他锁:事务中对表中某一行加排他锁
这里的共享锁和排他锁指的是表级别的锁
行锁:锁的力度小,并发高,但是锁开销比较大,容易出现死锁
间隙锁:解决幻读问题(间隙锁、MVCC)可重复读下添加
比如给插入id=8的记录,那么(3,8)都会加上间隙锁,不允许在其中插入新纪录
邻键锁:可重复读下添加
有时候我们既想锁住某条记录又想阻止其他事务在该记录前面的间隙插入新纪录
间隙锁是(),邻键锁是[]
#select显示加锁
select * from table where id = ? for update
107、死锁
死锁是指两个或两个以上的线程或者事务在同一资源上互相占用,并请求锁定对方占用的资源,从而导致恶性循环
产生死锁的必要条件:1.两个或者两个以上的事务(线程)
2.每个线程已经持有锁,并且申请新的锁
3.锁资源只能被同一个线程持有
4.线程之间因为持有锁和申请锁相互等待
解决办法:设置超时时间、死锁检测
108、redolog、undolog、binlog
redo log
重写日志,为了保持数据库的持久性(物理操作的日志)
undolog
回滚日志,为了保持数据库的原子性(逻辑操作日志)
binlog(二进制日志)
数据恢复和数据复制日志,只包含数据库更新删除操作(逻辑日志)
binlog的两阶段提交
总结:
中继日志
中继日志只在主从服务器架构的从服务器上存在
主从复制流程
主从复制的复制策略:
109、Sql走了索引依旧很慢应该怎么做?使用mysql查询大数据量会不会出现慢查询,怎么解决
答:主从复制、读写分离
数据库调优的策略:1.SQL索引 2.更改表结构减少join 3.分库分表读写分离
主从复制的作用:读写分离、数据备份(热备份机制)、具备高可用性
主从复制的原理:Slave会从Master读取binlog来进行数据同步
优化数据库结构:冷热数据分离
大表优化:1.垂直拆分(冷热数据分离)
缺点:主键出现冗余,并且会引起join操作,垂直拆分会让事务变得更复杂
2.水平拆分:一定要注意水平拆分策略(热数据要分开存放,不是所有数据越均等越好)
110、MVCC
MVCC(Multiversion Concurrency Control);多版本并发控制
InnoDB mysql虽然默认是可重复读,但是通过MVCC(或者间隙锁)实际上也解决了幻读的问题
MVCC只在READ COMMITTED和REPEATABLE READ两个隔离级别下工作
1.快照读
一致性读,读取的是快照数据,不加锁的简单select都是快照读(不加锁)(基于MVCC实现)
它读取的不一定是最新版本
2.当前读
当前读读的是记录的最新版本(最新数据)(加悲观锁)
3.MVCC实现原理
MVCC的实现依赖于:隐藏字段、Undo log、Read View
Read View:innodb为每个事务构造了一个数组,用来记录并维护系统当前活跃(启动但还没提交)事务的id,主要解决了版本链中哪个版本是当前事务可见的
READ COMMITTED:每select一次都会生成read view
REPEATABLE READ:只会在第一次select生成read view
4.Read View中的参数
1. creator_trx_id ,创建这个 Read View 的事务 ID
说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为
事务分配事务id,否则在一个只读事务中的事务id值都默认为0
2. trx_ids ,表示在生成ReadView时当前系统中活跃的读写事务的事务id列表
3. up_limit_id ,活跃的事务中最小的事务 ID
4.low_limit_id ,表示生成ReadView时系统中应该分配给下一个事务的 id值。low_limit_id 是系
统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID
5.Read View规则
1.如果被访问版本的trx_id属性值与ReadView中的 creator_trx_id值相同,意味着当前事务在访问
它自己修改过的记录,所以该版本可以被当前事务访问
2.如果被访问版本的trx_id属性值小于ReadView中的 up_limit_id值,表明生成该版本的事务在当前
事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
3.如果被访问版本的trx_id属性值大于或等于ReadView中的 low_limit_id值,表明生成该版本的事
务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
4.如果被访问版本的trx_id属性值在ReadView的 up_limit_id和 low_limit_id之间,那就需要判
断一下trx_id属性值是不是在 trx_ids列表中。
如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问
注意:读已提交,事务每select一次都会获取一次Read View
可重复读:一个事务只在第一次select的时候产生Read View
111、redis的持久化
1.RDB持久化
Redis的数据备份文件,Redis停机时会执行一次RDB
save:阻塞主线程,用主线程写入文件
bgsave:开启子线程生成RDB文件,不会阻塞
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件
fork采用的是copy-on-write技术:当主进程读操作时,访问共享内存,当主进程写操作的时候,拷贝一份数据执行写操作
缺点:RDB执行时间长,两次RDB写入数据有丢失的风险、fork子进程写出RDB文件都比较耗时
2.AOF持久化
aof(Append Only File)追加文件,Redis的每一个写命令都会记录在AOF中(与binlog类似)
刷盘策略:always:每执行一次写命令,立刻记录到aof中(与redolog类似)
everysec:每秒刷盘
no:操作系统控制
112、Redis的redis的三种集群模式
主从模式
作用:读写分离、实现高并发读
数据同步的原理:
全量同步的流程:1.slave节点请求增量同步
2.master节点判断replid,发现不一致拒绝增量同步
3.将master中完整内存数据生成RDB,发送RDB到slave
4.slave清空本地数据,加载master的RDB
5.master将RDB期间的命令记录在repl_baklog,并发给slave
6.slave执行接收到的命令,保持与master之间的同步
增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave
repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步。
什么时候执行全量同步:1.slave节点第一次连接master节点时
2.slave节点断开时间太久,repl_baklog中的offset已经被覆盖时
2.哨兵模式
作用:实现主从集群的自动故障恢复
1.监控:Sentinel会不断检查您的master和slave是否按预期工作
Sentinel是基于心跳机制检测服务状态,每隔一秒向集群中每个实例发送ping命令
主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。 quorum值最好超过Sentinel实例数量的一半。
2.自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
选举新master:
首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
最后是判断slave节点的运行id大小,越小优先级越高
故障转移的步骤:
首先选定一个slave作为新的master,执行slaveof no one
然后让所有节点都执行slaveof 新master
修改故障节点,执行slaveof 新master
3.通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
3.分片集群
分片集群作用:海量数据存储、高并发写
特点:每个master保存不同的数据并且互相通过ping来检测彼此的健康状态
散列插槽:1.将16384个插槽分配到不同的实例
2.根据key的有效部分计算哈希值,对16384取余余数作为插槽,寻找插槽所在实例即可
113、输入URL之后会执行什么流程
DNS域名解析
TCP连接
发送HTTP请求
服务器处理请求并返回HTTP报文
浏览器解析渲染页面
连接结束
114、start()和run()的区别
115、数据库的设计规范(一范式、二范式)
116、快排
117、冒泡排序
118、单例bean线程安全吗
大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题。单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。
常见的有两种解决办法:
在 Bean 中尽量避免定义可变的成员变量。
在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。
将作用域从singleton变为prototype,这样每次都会new Bean(),是线程安全的
不过,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。
119、Redis的常用命令
120、Linux的常用命令
121、Cglib动态代理和JDK动态代理的区别
动态代理
使用jdk的反射机制,创建对象的能力,创建的是代理类的对象,而不用你创建类文件
动态:在程序执行时,调用jdk提供的方法才能创建代理类的对象
作用:功能增强、控制访问
jdk动态代理:使用jdk反射包中的类和接口实现动态代理功能
cglib动态代理:第三方的工具库,原理是继承,cglib通过继承目标类,创建它的子类,在子类中重写父类中同名的方法,实现功能的修改,因为cglib是继承重写方法,所以方法不能是final的
jdk动态代理的实现
1)InvocationHandler 接口(调用处理器):表示你的代理要干什么(要重写invoke)
就一个方法invoke()
invoke():表示代理对象要执行的功能代码。你的代理类要完成的功能就写在invoke()方法中。
public Object invoke(Object proxy, Method method, Object[] args)
Object proxy:jdk创建的代理对象,无需赋值。
作用:核心的对象,创建代理对象,代替new的使用
静态方法 newProxyInstance()
Method method:目标类中的方法,jdk提供method对象的
作用:通过Method可以执行某个目标类的方法,Method.invoke(目标对象,方法的参数);
Object[] args:目标类中方法的参数, jdk提供的
区别:
CGLIB代理使用字节码处理框架asm,对代理对象类的class文件加载进来,通过修改字节码生成子类
JDK代理使用的是反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
JDK创建代理对象效率较高,执行效率较低;
CGLIB创建代理对象效率较低,执行效率高。
122、spring中管理bean的五种作用域
singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。
request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
application (仅 Web 应用可用): 每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
123、如何保证缓存与数据库双写时的数据一致性
1.先删除缓存后更新数据库
存在问题:如果线程a更新数据库,删除缓存,线程b此时查询发现没有缓存重新缓存(缓存的是旧值),但是此时a线程才更新完成!
解决办法:延时双删
1)先淘汰缓存
2)再写数据库(这两步和原来一样)
3)休眠1秒,再次淘汰缓存,这么做,可以将1秒内所造成的缓存脏数据,再次删除。确保读请求结束,写请求可以删除读请求造成的缓存脏数据。自行评估自己的项目的读数据业务逻辑的耗时,写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。
2.先更新数据库后删除缓存
存在问题:如果redis删除缓存失败怎么办
解决方案:如果发现redis报错,可以使用消息队列来处理未删除的缓存
124、LRU
class LRUCache {public DoubleList doubleList;public LRUCache(int capacity) {doubleList = new DoubleList(capacity);}public int get(int key) {return doubleList.get(key);}public void put(int key, int value) {doubleList.putHead(key, value);}
}
class DoubleList {/**虚拟头尾节点*/public Node head;public Node tail;public int capacity;public int size;/**get put达到o(1)复杂度*/public HashMap<Integer, Node> map;public DoubleList(int capacity) {this.capacity = capacity;head = new Node(-1, -1);tail = new Node(-1, -1);head.next = tail;tail.pre = head;map = new HashMap<>();}public int get(int key) {Node node = map.get(Integer.valueOf(key));if (node == null) {return -1;}int result = node.value;putHead(key, result);return result;}public void deleteNode(int key) {Node node = map.get(Integer.valueOf(key));node.pre.next = node.next;if (node.next != null) {node.next.pre = node.pre;}map.remove(key);size--;}public void putHead(int key, int value) {Node node = map.get(Integer.valueOf(key));if (node != null) {deleteNode(key);}if (size >= capacity) {Node last = tail.pre;deleteNode(last.key);}node = new Node(key, value);if (size == 0) {head.next = node;node.pre = head;node.next = tail;tail.pre = node;} else {node.pre = head;node.next = head.next;node.next.pre = node;head.next = node;}size++;map.put(key, node);}
}//链表中的节点
class Node {public int key;public int value;public Node next;public Node pre;public Node(int key, int value) {this.key = key;this.value = value;}
}
125、IO多路复用
126、TCP的流量控制和拥塞控制
1.流量控制:
接收方动态调整自己的接收窗口大小,发送方跟着调整自己的发送窗口大小
流量控制就是让发送方的发送速率不要太快,让接收方来得及接收。利用滑动窗口机制实现对发送方的流量控制。TCP的窗口单位是字节,不是报文段
因为当发送方速率大于接收方速率,接收方就会丢包,丢包之后,发送方又重传,重传又丢包,浪费带宽
发送接收缓冲区:
应用程序调用write时,数据只是写入了本机的发送缓冲区里面,并没有立刻发送到网络上
应用程序read时,并不会等待网络上的数据到来,只是读取本地接收缓冲区的数据,读不到就直接返回了
滑动窗口:
发送窗口:发送缓冲区里面的一段范围
接收窗口:接收缓冲区里面的一段范围
存在问题:窗口收缩到0,可能引起死锁问题
发送窗口收缩到0,发送方将不能继续发送和接收通信,要等待接收方新的ack到来,确定新的发送窗口大小。如果新的ack丢失,发送方将一直等待接收方扩大窗口,接收方如果一直等待发送方发新数据,可能造成“死锁”,互相等待
解决办法:发送窗口收缩到0之后,发送方启动一个定时器,定期给接收方发送探测报文,看窗口是否扩大了,多次探测,如果还是0那么就关闭连接
2.拥塞控制:
解决的是“中间节点问题(交换机、路由器)处理不过来”,网络拥塞,发送方收不到ack就会重传,重传导致网络更加堵塞,浪费网络带宽
发送方怎么知道网络拥塞了:
发送一个包出去,ack迟迟收不到,两种可能:接收方处理能力有问题、网络拥塞
所以引入拥塞窗口概念:发送窗口= min(拥塞窗口、接收窗口)
发送方根据ack快慢程度,动态调成拥塞窗口的大小,也就间接调整了发送窗口的大小
快重传:
接收方一旦收到失序的报文,马上ack
发送方一连接到3个重复的ack马上重传未收到的
127、什么应用是cpu密集型?io密集型
cpu密集型:一般做运算比较多,比如图像处理,序列化与反序列化
网络密集型:redis
io密集型:一般与磁盘io较多,比如web程序、微信、支付宝、查询数据库比较多
128、Spring中各种注解的实现方式
本质上都是通过反射来实现的
例如@AutoWired通过在内存中解析xml或者properties之后再通过反射来根据type或者name来创建这些对象,实现各种装配
129、什么是Cookie和Session
Cookie:是服务器发送到用户浏览器并保存到本地的数据,他会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上,如保持登陆状态等
Session:Session代表服务器和客户端一次会话的过程,Session存储特定用户的属性以及配置信息
用户第第一次请求服务器,服务器创建对应Session,请求返回时,将此Session的唯一标SessionID返回给浏览器,浏览器接收到服务器返回的SessionID,会将此信息存入到Cookie当中,同时Cookie记录此SessionID属于哪个域名
区别:作用范围不同:Cookie只保存在浏览器,Session保存在服务端
Cookie只能保存ASCII,Session能保存任意数据类型
有效期不同,Cookie可以设置为长时间保持,客户端关闭或者Session超时都会失效
130、UDP怎么防止丢包、在哪分片
产生原因:1.缓存太小,不能及时接收数据
2.接收方接收到数据处理太慢
解决办法:
增加接收方系统的缓冲区
增加应答机制,处理完一个包后在继续发包
在应用层实现丢包重传和超时机制
在哪分片:因为UDP协议没有实现对应的分片、所以他在IP层进行分片
TCP协议有自己的分片规则
hsgcc:面试笔记相关推荐
- 应有尽有!这可能是最全的 AI 面试笔记了
点击上方"视学算法",选择"星标"公众号 重磅干货,第一时间送达 今天给大家推荐一个非常全面的 AI 面试笔记集锦,包含 2018.2019 年的校招.春招.秋 ...
- springaop事务逻辑原理_太狠了!阿里大牛手写的Spring核心面试笔记:IOC+AOP+MVC+事务...
Spring作为现在最流行的java 开发技术,其内部源码设计非常优秀.如果你不会Spring,那么很可能面试官会让你回家等通知. Spring是什么? 有一个工地,几百号人在用铁锹铲子挖坑. 如果开 ...
- 炼丹面试官的面试笔记
作者:无名,某小公司算法专家 排版:一元,四品炼丹师 公众号:炼丹笔记 关于Attention和Transformer的灵魂拷问 背景 现在几乎很多搞深度学习的朋友都把attention和Transf ...
- Java高级开发工程师面试笔记
最近在复习面试相关的知识点,然后做笔记,后期(大概在2018.02.01)会分享给大家,尽自己最大的努力做到最好,还希望到时候大家能给予建议和补充 ----------------2018.03.05 ...
- labuladong的算法小抄pdf_真漂亮!这份GitHub上爆火的算法面试笔记,助你圆满大厂梦...
前言 Github作为程序员们的后花园,一直以来都是程序员最喜欢逛逛.学习的地方,小编也不例外,最近看到一份对标BAT等一线大厂的算法面试笔记,已经标星68+K了,很是惊讶,看了一下,觉得知识点整理得 ...
- jsjq面试笔记(下)
js&jq面试笔记,该部分为下部分. 字符串相关 1.定义一个方法,用于将string中的每个字符之间加一个空格,并输出 如:'hello' -> 'h e l l o'function ...
- 怎么判断自己启动的线程是否执行完成 java_Java面试笔记(上)
面试整体流程(HR 或技术面) 1.请简单的自我介绍 我叫***,工作*年了,先后做过**项目.**项目. 2.请你简单的介绍一下**项目 该系统主要有哪些部分组成,简单介绍项目的整体架构,具体参与某 ...
- 网易被裁后,68天吃透这份阿里学长甩我的Android面试笔记,竟让我收到字节跳动和小米offer
自我情况介绍一下: 楼主双非本科,17年毕业,学历背景一般,之前一直在网易工作,生活状态还算是稳定,国庆节后突然被裁彻底打破了我的生活节奏,将近一个月都处在懵逼状态(哪个环节出问题了,导致被裁),在咨 ...
- GitHub上AI岗位面试笔记(机器学习算法/深度学习/ NLP/计算机视觉)
目录 机器学习 深度学习 自然语言处理与数学 算法题和笔试题 推荐阅读 工具 最近在GitHub上淘到一个很棒的AI算法面试笔记,特地分享给小伙伴们~ GitHub地址:https://github. ...
- 剖析Framework面试-笔记(二)
剖析Framework面试-笔记 其他应用组件相关 Service的启动原理 Service的绑定原理 使用 原理 动态广播的注册与收发原理 动态广播的注册原理 广播的发送原理 广播的接受原理 静态广 ...
最新文章
- BaseRecyclerViewAdapterHelper结合autolayout使用
- 在您的构建过程中添加微基准测试
- UVa 816 (BFS求最短路)
- javamailsender注入失败_使用Spring3.x框架的java mail支持来发送邮件
- STM32F103代码远程升级(六)基于小米IoT开发者平台远程升级代码的实现
- 阿里云 OSS 客户端直传 Policy 模式使用
- 计算机主机装机注意,自己组装电脑要注意什么?DIY老司机教你装机注意事项 (全文)...
- LAMMPS生成粗糙表面的in文件脚本(可调节微结构高、长和宽)
- Nginx学习笔记(反向代理搭建集群)
- 手机打印文件怎么打印出来,如何用手机打印文件
- 【操作系统】内存管理
- java 生成水印图片工具类, MultipartFile接收上传的图片,处理成加水印之后的MultipartFile
- 视频超分:Zooming Slow-Mo(Zooming Slow-Mo: Fast and Accurate One-Stage Space-Time Video Super-Resolution)
- Java基础面试题精选汇总,备战面试突破篇--直击offer
- 美信GMSL技术让汽车数据传输更为高效
- 大数据挖掘技术在企业创新中的应用
- OpenCV技巧 | 二值图孔洞填充方法与实现(附Python/C++源码)
- 本地文件包含漏洞详解
- 解决conda创建新环境慢 conda install 速度慢 报错问题
- Java 匿名内部类的构造器