网络基础
OSI制定的是概念框架:7层协议。
物理层:两台物理机器之间传输比特流,定义物理设备的标准,网卡
数据链路层:在传输比特流的过程会产生各种错误,数据链路层应运而生,该层定义了如何格式化数据以进行传输以及控制对物理介质的访问,还提供错误检测和纠正。该层将比特数据组成帧,交换机工作在该层,对帧解码,根据帧中的数据将信息发送到正确的接收方。
随着网络节点的增加,点对点间的通信是需要经过多个节点的,如何找到目标节点,如何选择最佳路径是首要需求,此时产生网络层
网络层:其主要功能是将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方。发送方综合考虑发送优先权、网络拥塞、服务质量、可选路由的花费决定从网络中一个节点A到另一个网络中B的最佳路径,网络层处理并智能指导数据传送,路由器连接网络各端,所以路由器属于网络层。此层的数据称为数据包。本层关注TCP/IP协议中的IP协议。
随着网络通信需求的进一步扩大,通信过程中需要发送大量数据,可能需要很长时间,网络在通信过程会中断多次,此时为了保证传输文件的准确性,需要对发出去的数据进行切分,切为数据段。当发生数据段络丢失,失序等情况时,传输层应运而生。
传输层:解决主机间的数据传输,和传输质量问题。该层传输协议进行流量控制或是基于接收方可接受数据的快慢程度规定适当的发送速率。同时分割数据包以适应在网络上传输(以太网无法接受大于1500字节的数据包),分割数据为数据片,同时在数据片上给定序号,以便接受时可按序排组。需要关注TCP和UDP协议。
为了不用用户级不要每次调用TCP打包数据,IP协议去寻找路由,现在需要自动收发包,自动寻址的功能,会话层应运而生
会话层:作用是建立和管理应用程序之间的通信。至此可以保证用用程序能够自动收发包和寻址。
考虑各应用程序的环境不同,如window和Linux之间的差异,需要表示层来消除
表示层:解决不同系统之间的通信语法的问题,该层数据将按照网络能够理解的方式进行格式化。此时完成数据转化成相应的字节。
此时发送方知道发送的数据是什么,转换成字节数据有多长,但接收方并不知道,为了发送方和接收方能够理解传输字节的含义,应用层网络协议诞生
应用层:应用层网络协议规定消息发送方和接收方必须使用一个固定长度的一个消息头,消息头必须是固定组成,记录各种信息以方便接收方解析。该层旨在让你能够更方便的从网络中接受数据,没有该层也可以在两台主机间传递,但是我们能了解的就是一堆0101编码的字节数组。需要关注TCP/IP协议中的HTTP协议。
这是一个框架来协调和组织各层所提供的服务,但是该模型仅是概念模型,来协调进程间通信标准的制定。实际的实现是TCP/IP协议。
为什么有了MAC层还要走IP层
mac地址就好像个人的身份证号,人的身份证号和人户口 所在的城市,出生的日期有关,但是和人所在的位置没有关系
,人是会移动的,知道一个人的身份证号,并不能找到它这个人,mac地址类似,它是和设备的生产者,批次,日期之类的关联起来,知道一个设备的mac,并不能 在网络中将数据发送给它,除非它和发送方的在同一个网 络内。所以要实现机器之间的通信,还需要有ip地址的概念,ip地址表达的是当前机器在网络中的位置,类似于 城市名+道路号+门牌号的概念。通过ip层的寻址,我们能知道按何种路径在全世界任意两台Internet上的的机器间传输数据
TCP的三次握手
IP协议是无连接的协议,他不会占用两个正在通信的计算机之间的通信线路,这样IP就降低了对网络线路的需求每条线可以同时满足许多不同计算机之间的通信需要。通过IP协议,消息或者其他数据会被分割为较小的包,并通过因特网在计算机之间传送,IP负责将每个包路由至他的目的地,但是IP协议未作任何事来确认数据包是否安全到达,所以他是不可靠的,需要他的上层协议来控制。传输控制协议TCP是传输层的协议。(简介)
TCP报文头:
源端口和目的端口各2个字节,TCP/UDP都是不包含IP地址信息的,两者头部都会有端口号,这是在传输层范畴的知识。两个进程在计算机内部进行通信可以有:管道、内存共享、信号量和消息队列等方法。其中最唯一要求是唯一的表示对方进程,在本地进程中可以使用PID,但是在网络中的不同主机中使用便失效了,解决方式就是在传输层中使用协议端口号,已知在网络层中IP可以唯一标识一个主机,而TCP协议和端口号可以唯一标识主机中的一个进程,这样可以利用IP地址+协议+端口号这样的唯一标识去表示网络中的一个进程,这种方式也成为套接字。这样,虽然通信的重点是应用进程,但是我们只要把要传送的报文交到目的主机的某一个合适端口中,剩下的工作就由TCP来完成了。
sequence number(seq序号)4字节,tcp连接中传送的字节流中的每个字节都有序号,例如一段报文的序号字段值是107,携带的数据字节数是100,那么下一个报文段的序号就是107+100,即从207开始。
ACK number(ack序号)4字节,表示期望收到对方下一个报文的第一个字节的序号。例如B收到A发送的报文seq序号是301,全部数据是200字节,B正确收到从A发送的序号到500(301+200-1)的数据,所以B期望收到的A的下一个数据序号是501,于是,B在发送给A的确认报文中,会把ack确认号置为501。
TCP Flags
- ACK:确认序号标志,1有效,0表示报文中不含确认信息,忽略确认号字段
- SYN:同步序号,用于建立连接过程
- FIN:finish标志,用于释放连接,1时表示发送方已经没有数据发送了,即关闭本方数据流。
Window窗口:滑动窗口大小,告知发送端接收端的缓存大小(还有多少空间可以接收数据),以此控制发送端发送数据速率,从而达到流量控制
通信过程:
当应用程序希望通过TCP和另一个应用程序通信时,会发送一个通信请求,必须有一个确切地址,在双方握手后TCP将在两个程序间建立一个全双工的通信,将占据两个计算机的通信线路,直到双方关闭为止。(三次握手)
第一次A发送seq=x,B响应时,ack=x+1表示期望下一个从A发来的报文字节从x+1开始,同时B也发送一个自己的缓存初始化序列号seq=y,这里第二次握手完成。A收到确认后,还要向B给出一个确认:小ack=y+1,表示期望从B接受到的报文字节从y+1开始,同时,自己的报文序号从seq=x+1开始(B告知的),此时TCP连接建立,其中第三个报文可以携带数据,前两个不可以。
为什么要三次连接?
为了初始化Sequence Number 的初始值,通信双方要互相通知初始化的Sequence Number,也就是x和y,这是是作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输发生错误,即TCP会用这个数值来拼接数据。所以需要第三次握手:告知服务端,客户端已经知道了你的Sequence Number 了。
第一次握手的隐患!SYN的超时问题!
连接后客户端故障怎么办?
TCP四次挥手?
即断开连接时需要客户端和服务端总共发送4各数据包来确认断开连接。
客户端开始发送连接释放报文,并且停止发送数据,FIN=1,seq=u(已传送的最后一个字节的序号);服务器收到后发出确认报文,ACK=1,小ack=u+1(表示期望从客户端收到的下一个报文从字节u+1开始),seq=v(表示自己的序列号);此时服务端进入关闭等待状态,TCP通知高层进程客户端要释放联系了,这是出于半关闭状态:即客户端已经不发送数据了,服务端发送数据客户端还是会接受的。此时客户端进入等待关闭状态2,等待服务器的第3次挥手请求。等到服务器发送完数据后,服务器便发送连接释放报文,FIN=1,ACK=1,ack=u+1(表示客户端发送的下一个报文字节号从哪开始),seq=w(表示自己的序列号);最后,客户端发送确认报文,ACK=1,ack=w+1,seq=u+1(自己的序号),此时客户端进入等待状态,还没完全关闭(2MSL),服务端已经完全关闭,完成4次挥手。
为什么客户端要等待2MSL再关闭?
为什么需要四次挥手?
因为全双工,发送方和接收方都需要FIN报文和ACK报文,即发送方和接收方各只需两次挥手,因为有一方是被动的,所以看上去是4次挥手。???????????
UDP报文
不支持像TCP滑动窗口、错误重传等机制。
吞吐量不受拥塞控制。面向报文,添加首部后直接向下交付给IP层
滑动窗口
RTT:发送一个数据包到接收到对应ACK,所花费的时间
RTO:TCP在发送一个数据报后会启用数据重传计时器,收到ACK时他就失效,未收到ACK而且计时器时间也到了,那就要重传。
为了实现数据的批量传送,同时解决可靠性和包乱序的问题,所以TCP需要知道网络实际的处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。
- 保证TCP的可靠性
- 保证TCP的流控特性
HTTP
特点:客户端像服务端请求数据的时候只需传送请求方法(get/post等)和路径。http1.1开始默认使用长连接,即服务器需要等待一段时间才断开连接。无状态协议,指协议对事物处理无记忆能力。
请求结构:
响应结构:
总结:http协议定义了web客户端如何从web服务器请求web页面,以及服务器如何把web页面传送给客户端,http协议采用了请求响应模型,客户端向服务器发送一个请求报文,包含请求方法,url,协议版本,请求头部,请求数据;服务器以一个状态行作为响应,响应内容包括,协议版本,错误或成功信息,服务器信息,响应头部和响应数据。
- 连接:客户端与web服务器的http端口建立一个TCP套接字连接
- 发送请求:即通过套接字客户端向web服务端发送一个文本的请求报文
- 响应:服务器解析请求,定位请求资源,将资源副本写到TCP套接字,由客户端读取
- 释放连接:keep-live长连接会保存一段时间,该时间内请求还会响应
- 客户端解析:首先解析状态行,查看表明请求是否成功的状态代码,解析响应头,响应头告知以下若干数据为数据信息,客户端按照格式解析数据,在浏览器窗口显示。
URL输入后的流程???
- DNS解析:浏览器会根据url逐层查询DNS服务器缓存,解析url中的域名所对应的IP地址(DNS缓存从近到远为浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存),此时找到IP地址
- TCP连接:找到IP地址后会根据IP地址和对应端口与服务器建立TCP连接(三次握手)
- 发送HTTP请求:浏览器会发起读取文件的http请求,
- 服务器处理请求并返回HTTP报文:服务器对浏览器请求响应,并把对应的带有http文本的http响应报文发送给浏览器
- 浏览器解析渲染页面:浏览器收到报文,解析渲染到浏览器
- 释放连接
状态码
GET和POST请求区别
- Http报文层:get将请求信息放在url中,请求信息和url以问号?隔开,请求信息的格式为键值对;post请求将请求信息放在报文体中,想获得请求信息必须解析报文,因此安全性较高。而且get请求会有长度限制,post无限制。
- 数据库层面:get符合幂等性和安全性,多次访问数据库的结果是一样的,不改变数据库的值;post都不符合。
- 其他层面:get请求能够被缓存,保存在浏览器的浏览记录中;post不具备。通常绝大部分的get请求都被cdn缓存了,大大减少了服务器的压力,而post请求必须要服务器来处理。
cookie和session的区别
因为Http是无状态的,每次我们访问有登陆需求的业务时都要输入账号密码,使用cookie和session避免了这种情况。
cookie:客户端机制。由服务器发送给客户端的特殊信息,以文本的形式存放在客户端,客户端每次向服务器发送请求的时候都会带上这些特殊信息;当客户使用浏览器访问一个支持cookie的网站时,用户会提供一个包括用户名在内的个人信息并且提交至服务器,紧接着服务器向客户端回传相应的超文本时也会发回这些个人信息,这些是放在http响应头中,当用户端接受来自服务器的响应后,浏览器会将这些信息存放在统一位置,自此客户端再向服务器发送请求的时候,会把相应的cookie再次发送至服务器中,这次cookie将会存放在http请求头中,有了cookie后,服务端再次接受请求后,会解析存在于请求头中的cookie得到客户端特有的信息,从而动态生成与该客户端相应的内容。
cookie的设置和发送过程:
session:服务器端机制。当程序需要为某个客户端请求创建一个session时,服务器首先检查这个客户端的请求里是否已包含了一个session标识,称为sessionId,如果包含,则说明以前已为该客户端创建过session,服务器就按照sessionId把这个session检索出来使用(检索不到就生成一个)。不包含就创建一个session和与此相关的sessionId。这个sessionId将会回发给客户端进行保存。
实现方式:
使用cookie实现,服务器给每个session分配一个唯一的JSESSIONID,并通过cookie发送给客户端,待客户端发送新的请求时,将在cookie头中携带这个JSESSIONID,这样服务器能够找到客户端对应的session。
使用URL重写实现,是指服务器在发送给浏览器页面的所有链接中都携带JSESSIONID参数,这样客户端点击任何链接都会将JSESSIONID带回服务器。如果直接在浏览器输入服务器资源url,是请求不到session的,tomcat对session的实现是同时使用cookie和url重写。
区别:
cookie数据存放在客户的浏览器上,session数据放在服务器上
session相对于本地存放的cookie更安全
session会一定时间内保存在服务器上,当缓存增多时会影响服务器性能,应当使用cookie
HTTP和HTTPS
简单说就是安全版的http协议。
SSL:
- 为网络通信提供安全及数据完整性的一种安全协议。
- 是操作系统对外的API,后更名为TLS
- 采用身份验证和数字加密保证安全性
加密的方式:
数字签名:就是在信息后面加上一段内容,这些内容证明信息没有被修改过。
HTTPS流程:
区别:
Socket
唯一标识一个进程:本地中使用PID;
网络中进程:IP地址+协议+端口号来唯一标识一个网络中的一个进程;
socket通信流程
数据库
题:如何设计一个数据库
开始设计
数据库最主要的功能是存储数据,因此他有一个存储模块,将数据持久化存入磁盘中;
我们还需要组织并且用到这些数据,所以需要程序实例来映射出物理结构。
实际程序时要考虑:存储管理(尽量优化减少IO操作),缓存机制(优化访问),sql解析(操作数据库,优化可将sql放入缓存,编译好的sql可以直接用),日志管理(记录操作),权限划分,异常机制(容灾),索引管理(优化查询),锁管理(并发)
索引模块
为什么使用索引?
直接加在到内存中,进行全表扫描,很慢。使用索引避免全表查询,加速查询数据;
什么样的信息能够称为索引?
主键,唯一键,普通键,有一定区分性
主键:唯一标识表中的每一行数据,特点不能为空!!!不能重复!!!
auto_increment
的字段必须是主键, 但是主键不一定是auto_increment
的, 只要是唯一的就可以 一个表只能有一个主键, 但是主键可以是1个或多个字段组成
唯一键:将表中的某个字段设置为不可重复值,可以将其设为唯一键!!!
唯一键不是主键,但主键有不可重复性
一张表可以有多个唯一键,但只能有一个主键
有了关键字索引还不行,还需要以某种数据结构将其组织起来才能够使检索更高效。
磁盘文件存储
页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k),主存和磁盘以页为单位交换数据。
InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。InnoDB 存储引擎中默认每个页的大小为 16 KB,因此 InnoDB 每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小 16KB 。InnoDB 在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率。
文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。
索引的数据结构?
二叉树:二分查找,缺点:每个节点只能存储两个节点,树的深度很深,IO的操作就会很多,效率就很差
B数:树的每个节点最多有m个孩子,就是m阶B树,下图3阶:
特性:
让每个索引块尽可能存储更多信息,让树的高度低,减少IO次数;
B+树:
结论:B+树更适合,原因:
B+树的磁盘读写代价更低,内部(非叶子节点)并没有指向关键字具体信息的指针,不存放数据只存放索引信息。
查询效率更稳定,每次查询都是根节点到叶子节点的路径,查询基本一样
只需要遍历叶子节点就可以完成对全部关键字的扫描,所以他更有利于对数据库的扫描。(更适合范围条件查询)
Hash索引了解一下:
缺点:
- 比较进行hash运算之后的值,仅满足等值查询,不能使用范围查询;
- 无法运用索引值来排序
- 不能利用部分索引键查询
- 不能避免表扫描,哈希值可能重复,需要全表扫描
- 大量hash值相等时,效率很底。
密集索引和稀疏索引
密集索引:叶子节点不仅保存了索引值,还保存了其同一行的其他列的数据。
所有完整的用户记录都存放在这个
聚簇索引
的叶子节点处。在InnoDB
存储引擎中,聚簇索引
就是数据的存储方式
稀疏索引:叶子节点仅保存了键位信息及其主键。
InnoDB的索引
如何定位并优化慢查询sql?
根据慢日志定位慢查询sql(较慢sql执行的记录)
使用explain等工具分析sql
- 一般放在select查询语句前,用于描述MySQL如何执行查询操作,以及MySQL成功返回结果集需要执行的行数
- 字段:type表示MySQL找到数据行的方式,性能最优到最差如图,index/all表示是全表查询。
- 字段:extra,如图
修改sql或者尽量让sql走索引
- 改用索引查
- 添加索引
使用fore_index()测试那个索引更好
联合索引的最左匹配原则的成因?
MySQL创建复合索引的规则是首先会对最左边的也就是第一个字段进行排序,在第一个字段排序的基础上再对第二个字段排序,所以第一个字段是绝对有序的,第二个字段就是无序的了,因此通常情况下直接使用第二个字段进行条件判断是用不到索引的。这就是MySQL联合索引强调最左匹配的原因。
索引建立的越多越好吗?
锁
Myisam:表级锁,不支持行级锁
InnoDB:默认行级锁,支持表级锁,
注:当SQL语句中使用索引作为条件时,使用的是行级索,当不用索引时,整张表会被锁住,使用的是表级锁,
无论表锁还是行锁,默认都分为共享锁和排他锁
场景分析:
MyISAM
- 适用频繁执行全表count语句;因为有一个变量值存储了该值
- 适用增删改不高,查询频繁;因为增删改会涉及锁表操作,会产生很多碎片,但是纯查询效率是可以的
- 适合没有事务的
InnoDB
- 适合数据增删改查都频繁;增删改时某些行被锁,避免了被阻塞,不像MyISAM每次锁住整张表
- 支持事务的系统
数据库锁分类:
- 按粒度划分:表级锁,行级锁,页级锁
- 按级别划分:共享锁,排他锁
- 按加锁方式:自动锁(意向锁,MyISAM表锁,以及增删改时的锁),显示锁
- 按操作划分:DML锁(数据操作),DDL锁(表结构变更)
- 按使用方式:乐观锁(认为数据处理过程不会发生冲突,提交更新时才会检测,实现方式是记录数据版本:版本号或者时间戳),悲观锁(全程使用排他锁)
数据库事务的特性
ACID:原子性,一致性,隔离性,持久性
事务的隔离性以及各级别下的并发问题
更新丢失——MySQL所有事务隔离级别在数据库层面上均可避免
脏读——一个事务读到另一个事务未提交的更新数据,READ-UNCOMMITTED事务隔离级别不能解决,使用READ-COMMITTED(读提交)级别,隔离一个事物读取另一个事务未提交数据
不可重复读——一事务A多次读取数据,另一事务B在此期间修改数据,导致事务A多次读取数据不一致,使用REPEATABLE-READ(可重复读)级别可以避免。
幻读——事务A读取匹配条件的行数据,事务B以插入或删除的方式修改A的结果集,导致事务A产生差错。设置为SERIALIZABLE可以避免。
InnoDB在可重复度隔离级别下是如何避免幻读?
表象:快照度(非阻塞读)——伪MVCC
内在:next-key锁(行锁+gap锁)
当前读表示:读取的是记录的最新版本,并且读取后保证其他并发事务不能修改当前记录,对读取记录加锁。
快照读是基于提升并发性能的考虑,基于多版本并发控制(MVCC),他是行级锁的变动,但是他在很多情况下避免了加锁操作,开销更低,由于是基于多版本快照,所以读到的数据不是最新版本。
RC、RR级别下InnoDB的非阻塞读(快照读)如何实现?
- 每行数据的额外字段,DB_TRX_ID(最近一次事务标识符),DB_ROLL_PTR(回滚指针段),DB_ROW_ID(行号,隐藏主键)
- undo日志,当我们对记录做了变更操作时就会产生undo日志,其中存储的是老版数据
例:数据(11,12,13)修改为(11,32,13)时,会发生:
再次修改为(11,32,45)时会发生:
- read view可见性判断,当使用快照读时,会创建一个read view,告知我们读的是哪一个版本,根据可见性算法,将DB_TRX_ID取出与系统其他活跃事务ID对比,如果大于或者等于,就取出undo log中的版本,直到取出小于活跃事务ID号(事务ID是递增状态,越新开启的事务,ID越大)
因为生成时机???的不同造成RC,RR隔离级别的不同可见性,在RR级别下,事务在开启事务的第一条快照读会创建一个快照,即read view,将当前系统中活跃的其他事务记录起来,此后在调用快照读时还是使用同一个read view;而在RC级别下,事务中每次调用快照读时都会创建一个新的快照,这就是为什么在RC下能够看到别的事务提交的对表记录的增删改。而在RR下,如果首次使用快照读是在别的事物对数据做出增删改并提交之前的,此后即便别的事物对数据进行增删改并提交还是读不到数据变动的原因。对RR来说,首次事务调用快照读的时机很重要。
正是因为上面三个原因,使得InnoDB在RR、RC级别支持非阻塞读,而读取数据时的非阻塞就是MVCC,而InnoDB的非阻塞读实现了MVCC的仿照版;MVCC代表多版本并发控制,读不加锁,读写不冲突,在读多写少的应用中,读写不冲突很重要。这里仅实现伪MVCC机制是因为并没有实现核心的多版本并存,undo log中的内容是串行化的结果,记录了多个事务的过程,不属于多版本共存。
next-key锁(行锁+gap锁)
- 行锁:对单个行记录上锁,锁定一个记录上的索引,而不是记录本身。
- gap锁:gap表示索引树中插入新纪录的空隙,gap锁即锁定一个范围但不包括记录本身,是为了方式同一事物的两次当前读出现幻读的情况
在RR及以上级别默认都支持gap锁,RC及以下级别都没有gap锁。
RR级别下gap锁的使用场景,对主键索引或唯一索引会使用gap锁吗?
- 如果where条件全部命中,就不会用gap锁,只会加记录锁(行锁)
- 如果where条件部分命中或者未命中,就会加上gap锁
gap锁会出现在非唯一索引或者不走索引的当前读中
非唯一索引:
gap会在(6,9],(9,11] 这两个区间加上gap锁,防止幻读。
不走索引
会对所有gap上锁,类似表锁,也能防止幻读。
InnoDB在RR级别主要通过引入next-key锁来避免幻读问题,next-key由行锁和gap锁,gap锁会用在非唯一索引或者不走索引的当前读以及仅命中检索条件的部分结果集并且用到主键索引以及唯一索引的当前读中。
Linux
linux体系结构
在Linux系统启动时候首先会启动内核,内核程序直接管理硬件,包括CPU、内存空间、硬盘接口、网络接口等。所有的计算机操作都要通过内核传递给硬件设备。用户态及上层应用程序的活动空间、应用程序的执行必须依托于内核提供的资源,包括cpu资源、存储资源、IO资源等,为了使上层应用能够访问这些资源,内核必须为这些上层应用提供访问的接口,即系统调用;可以将其视为一种不能再简化的操作,一个操作系统上的功能可以看作是系统调用的组合结果。公用函数库是对系统调用的组合。
shell:是一个特殊的应用程序,本质是一个命令解释器。
指令
查找文件
文本检索、
文件内容的统计
替换文本内容
Redis
Mysql数据库也有缓存,但是是针对查询内容,一般只有表中的数据没有变动的时候,缓存才有作用,这并不能减轻业务系统对数据库的I/O压力,因此缓存数据库应运而生。
缓存数据库:实现对热点数据的高速缓存,提高响应速度,缓解后端数据库的压力。
熔断:存储层挂了,直接访问缓存层。
穿透:未在缓存中换取数据,直接访问存储层
为什么Redis这么快?
- 完全基于内存操作,绝大多数请求是纯内存操作,执行效率高;
- 数据结构简单,操作也就简单;
- 使用单线程,是指主线程是单线程的,这里主线程包括I/O事件处理、过期键处理、复制协调、集群协调,这些处理IO事件的逻辑会被封装成周期性的任务由主线程周期性处理。单线程设计,对于客户端的所有读写请求,都由一个主线程串行处理,因此多个客户端同时对一个键进行写操作时候就不会有并发的问题,避免频繁的上下文切换、锁竞争问题;
- redis单线也可以处理高并发请求,并发性IO流指让一个计算单元处理来自多个客户端的流请求,redis使用单线程配合上IO多路复用可大幅度提升性能,这里的单线程是指处理网络请求只有一个单线程来处理;一个正式的redis servlet肯定是不止一个进程的。
- 多路I/O复用模型,即非阻塞I/O;redis是单线程的,所有的操作是按照顺序线性执行的,但是由于读写操作,等待用户输入或者输出都是阻塞的,所以IO操作往往不能直接返回就会导致某一文件的IO阻塞,进而进程无法对其他客户端提供服务;IO多路复用就是解决这个问题!
FD:一个打开文件通过唯一的描述符进行引用。
传统的IO阻塞模型,当进行读写操作时,不可读或者不可写时,整个redis就不会对其它操作进行响应,导致整个服务不可用。
IO多路复用调用,这其中最重要的函数调用就是select系统调用,select能够同时监控多个文件描述符的可读可写情况,当文件描述符可读可写时就会返回响应的参数,即将监听文件读写情况的任务交给select,程序就可以继续做别的而不被堵塞
基于react设计模式来实现文件事件处理器,文件事件处理器使用IO多路复用模块同时监听多个FD,当发生文件读写函数(read、write、close等)事件时,文件事件处理器就会回调FD绑定的事件处理器,虽然文件事件处理器是在单线程上,但是通过IO多路复用,实现同时对多个FD读写的监控,提高了网络通信模型的性能。
同时也有其他的IO多路复用函数:
- evport:Linux
- kqueue:MAC OS
- select:都兼容,作为保底函数,O(n)
数据类型:
String类型:最基本的,二进制安全(即可以包含任何数据),常用的key-value键值对,最大可存储512M,底层由简单动态字符串实现。
案例:记录一个用户每天访问这个网站的次数,只需要拼接用户id和代表当前时间的字符串作为key,每次用户访问时对这个页面时,对这个字符执行incr命令即可,就可以统计用户当天访问站点的次数。
Hash:String元素组成的,适合存储对象
List:按照String插入元素顺序排序,类似于栈的,使用可以用于最新消息,排行榜
Set:String类组成的无序集合,通过hash表实现,不允许重复,使用于共同关注,共同喜好值之类的交并集操作
Sorted Set:通过分数(double类型的)来为集合中的成员进行从小到大的排序,不重复
例题:从海量key中查询某一固定前缀的Key?
- 数据规模大小
- 误区:用keys pattern扫描出符合条件的key列表,问题在于当redis正在执行服务时会发生什么?当key的数量过大时会发生内存消耗过大而卡顿
- 正确:使用SCAN cursor指令,每次只会返回少量元素;基于游标的迭代器,基于上一次的游标延续之前的迭代过程
如何通过Redis实现分布式锁?
分布式锁是控制分布式系统或不同系统之间共同访问共同资源的一种锁的实现。如果不同系统或者同一系统不同主机间共享了某个资源时,往往需要互斥来防止彼此干扰,进而保证一致性。分布式锁需要解决的问题有:
- 互斥性:任意时刻,只能有一个客户端获取锁;
- 安全性:锁只能由获取该锁的客户端删除,不能由其他客户端删除
- 死锁:避免获取锁的客户端因为宕机而未能释放锁
- 容错:当部分节点(例如redis)宕机时,客户端仍能正常获取锁,释放锁
SETNX的原子操作和保持不变,可以用来实现分布式锁。在执行一段程序时,可以先尝试对某个key设值,如果设值成功则表示当时没有别的线程在执行该段代码或者说占用该独占资源。这里需要解决的就是key值的时间问题,因为SETNX设置完该值就存在了。
解决key值得时效问题:
缺点的原子性在于:如果一个线程只执行到setnx语句后就挂了,那么设置的key值将会一直存在,其他线程就无法访问后面的资源。两个原子操作(setnx和expire)组合成一个逻辑,就变得不是原子操作了。
正确操作:
如果有大量key同时过期得注意事项?
- 集中过期时处理key会很耗时,出现卡顿现象
- 解决:在设置key过期时间时,加上随机值
如何使用Redis做异步队列?
使用List作为队列,RPUSH生产消息,LPOP消费消息。
- 缺点:在于没有等待队列中有值(消息)时就直接消费,
- 处理:可以通过应用层引入Sleep机制去调用LPOP重试。
加上无消息时阻塞方式
- 缺点:生产得消息只能供一个消费者使用,用完就没了
- 改进:使用主题订阅者模式
pub/sub主题订阅者模式
- 发送者pub发送消息,订阅者sub接收消息
- 订阅者可以订阅任意数量得频道
- 缺点:消息得发布是无状态的,无法保证可达,对发布者来说消息是即发即失的,如果消息发送时一个订阅者下线了,重新上线时,消息是不会重新收到的,要解决这个问题需要使用专业的消息队列,例如kafuka等。
Redis的持久化
RDB(快照)持久化:即保存某个时间点的全量数据快照;
- 创建方式:save,使用主线程执行快照,该方法会阻塞redis的服务器进程(阻塞客户端请求操作),直到快照文件被创建完毕(很少用)
- bgsave,会fork出一个子进程来创建rdb文件,不会阻塞服务器进程,父进程通过轮询接受子进程完成的信号
- 其他自动化出发RDB持久化
BGSAVE底层实现方式:
- 查询子进程是否有冲突
- 系统调用fork()函数:创建进程,实现copy-on-write(写时复制),传统方式下fork在创建子进程时会将资源全部复制给子进程,简单但是效率低。Linux环境下该进fork方式,当父进程创建子进程时,内核只为子进程创建虚拟空间,父子进程使用的是相同的物理空间,只有父子进程发生更改时,才会为子进程分配独立的物理空间
优点在于:如果调用者没有修改资源,则不会发生复制操作,因此多个调用者只是读取操作时可以共享资源。COW调用时会维持一个为读操作请求的指针,并在读完后更新这个指针,以提升读写并发能力。因此COW也提供了数据更新过程中的原子性,提升了读写效率。当redis执行持久化时,redis会fork一个子进程,子进程将数据持久化到一个临时的RDB文件中,当完成写操作后,将原来的rdb替换掉,这样做的好处就是可以实现COW操作。
持久化时,子父进程都存在,父进程继续处理客户端请求,子进程负责将内存内容写入临时文件中,由于OS的COW操作,父子进程会共享相同的物理页面,当父进程处理写请求的时候,OS会为父进程调修改的页面创建副本,而不是写共享的页面,所以子进程的地址空间内的数据是fork时刻的整个数据库的快照,子进程完成写操作时,只要替代原快照,然后退出,这样就完成一次备份操作。
使用日志重写解决aof文件不断增大的问题
子进程在做aof重写时,会通过管道从父进程读取增量数据,并缓存下来;在以rdb格式做全量持久时,也会从管道读取数据,同时不会造成管道阻塞,也就是aof文件前半段是rdb格式的全量数据,后半段是redis命令格式的增量数据。
redis实例重启时会使用bgsave持久化文件重新构建内容,再使用aof重放近期的操作指令,使用两者结合恢复重启之前的状态。
如果同时需要执行大量命令,就需要等待上一条命令应答完成,这中间会有大量来回交互的时间和IO磁盘操作,为了提升效率,使用Pipeline可允许客户端一次发送多条命令,而不需要上一条命令的结果,客户端首先将命令写入缓存中,再一次性发送给redis,这样pipeline可以将多次io往返是的时间缩减为一次。(注:这里的命令指无相关性的命令)
Redis的同步机制
主从同步原理
- 一个M:写;
- 若干个S:读;定期的数据备份也是在其中选择一个实例完成。
这里不需要M,S的实时一致性,而是保持一种弱一致性,即一段时间后的最终一致性
同步操作:主节点做一次BGSAVE,并同时将后续修改操作记录在内存的缓存中,待完成后将RDB文件全量同步到从节点,从节点接收后,就将RDB镜像加载到内存中,加载完成后,再通知主节点将期间的修改操作记录即增量操作同步到从节点进行重放,就完成了同步过程。
Redis哨兵
Redis的集群管理
这样就实现了数据分片,通过数据分片,实现单节点服务器的压力。Redis集群才用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其它节点连接,使用Gossip协议传播信息和发现新的节点。redis节点的目的是将不同的key分散放在不同的节点,通常是获取key的hash值,但是节点动态增减时会有问题。
Redis采用的是一致性哈希算法。使用哈希环,先在环上确定节点位置,存储数据时同样计算key哈希值在环上位置,顺时针寻找离key最近的节点存储数据。
一致性哈希在节点个数过少时也有个数据倾斜的问题,解决方式是引入虚拟节点,一个节点计算多个哈希值放在环中。
Java
平台无关性
编译生成字节码,字节码保存在.class文件中,.class文件在各种JVM中运行。
为什么JVM不直接将源码解析成机器码去执行?
因为每次执行都需要各种检查(java语法句法的检查),都要重新编译一次,在程序整体的性能就很受影响。所以引入中间字节码。
JVM如何加载.class文件
Class.forName()函数返回与给定的字符串名称相关的类或者接口的class对象。(返回一个给定类或者接口的一个 Class 对象)
反射
一句话:反射就是将Java中的各种成分映射成一个个对象(下面代码中的Class,Method),然后获取。
- Clas.forName(类全路径)的方式获取类对象:rc
- 创建这个实例:r
- 获取throwHello这个方法getHello,使用Method这个对象,getDeclareMethod()能返回共有或者私有的方法,不能获取继承的方法
- 调用这个方法,invoke(对象实例,参数)
- 使用getMethod()可以调用公有方法,还能获取继承和接口实现的方法
Java类从编译到执行的过程
由上面的反射发现:之所以能获取类的属性或者方法并对其调用,必须要获取class对象,要获取该类的class对象,必须要获取该类的字节码文件对象。
ClassLoader
- 抽象类
- 提供类加载方式的接口
- loadClass():通过给定一个类名去加载这个类,返回代表这类的class的实例,发现其中有很多Classloader类
AppClassLoader会去java.class.path路径中去找编译好的class文件:ReflectSample.calss、Robot.class,执行时就会加载这两个文件。
类加载器的双亲委派机制
不同的ClassLoader加载类的方式和路径各不相同,为了明确分工,加载类的时候,各类ClassLoader按照自己管理的区域各司其职,需要一个机制管理,这就是双亲委派机制。
类的加载方式
隐示加载方法不需要使用newInstance()方法获取实例,并且支持带参数的构造器(可以使用构造函数传入参数)生成对象实例
显示加载的方式获取类对象后,需要使用newInstance()方法获取对象实例,不支持传入参数,需要通过反射,调用构造器的newInstance()方法,才能使用参数??????
都能在运行时对任意一个类,知道该类的所有属性和方法,队友任意一个对象,能调用他的任意方法和属性。
Java类的装载过程:
SpringIOC资源加载器在获取要读取的资源时,即读取spring Bean的配置文件时,如果是以classpath的方式加载,就需要使用classloader方式加载,这样做是和SpringIOC的延时加载有关,目的是为了加快初始化速度,不执行加载中链接、初始化步骤,加快加载速度,把这些工作留到实际使用时再去做。
Java内存模型
GC
标准:没有被其他对象引用;
判断函数
垃圾回收算法
新生代:
每次使用一块Eden和一块survivor,进行垃圾回收时,将这两个中存活的对象一次性复制到另一个survivor。复制一次,对象年龄加 1 ,当年龄达到15(默认的)岁时,就会进入老年代。
老年代:
当发生老年代的GC时,通常也会伴随着年轻代的GC,对整个堆进行垃圾回收,Full GC。
调用了System.gc()只是告诉虚拟机要回收,但究竟什么时候回收由虚拟机说了算。
永久代时jdk7及以下才有的,8使用元空间代替了,一个原因就是降低Full GC的频率,减小GC的负担。
GC发生时:
GC安全点,GC开始前,让程序“停在某个地方”
垃圾收集器
CMS使GC线程和用户线程并发
线程
串行→批处理(程序)→进程→线程
进程拥有一个完整的虚拟内存地址空间,当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。线程只有相关堆栈寄存器、程序计数器和线程控制表TCB组成,寄存器可用来存储局部标量,但不能存储其他线程的相关变量。
- 每一个JVM实例唯一对应一个堆,每个线程有唯一私有的栈。
- 一个程序是一个可执行文件,一个进程是一个执行中程序的实例。
Java线程中start和run的区别?
1 | public static void attack(){ |
start
方法源码调用的是一个native的方法start0
,start0
方法调用的是JVM的JVM_StarThread
方法。该层源码中的关键语句是:native_thread = new JavaThread(&thread_entry, sz)
即start方法会调用JVM_StartThread方法去创建已给新的线程,并通过Thread#run()方法去调用方法;
Thread和Runnable
如何给run()方法传参
如何实现处理线程的返回值
主线程等待方法;
使用Thread类的join()方法阻塞当前线程以等待子线程处理;
通过Callable接口实现:通过Future Or 线程池获取
线程的状态
Sleep和Wait方法
wait方法和notify方法,并不是Thread线程上的方法,它们是Object上的方法。
因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait和notify是同步对象上的方法。
wait()的意思是: 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。
notify() 的意思是,通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。
notifyAll() 的意思是,通知所有的等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。
锁池
等待池
notify和notifyAll
yield
当前线程A的暗示调度器让出CPU给线程B,但是调度器可以选择忽视或者执行。
中断线程
stop()方法,已抛弃,问题包括突然停止不好清理线程,会释放锁,造成数据不一致等。
当前方法:interrupt()方法,通知线程应该中断,
总结
同步和互斥的区别:
各个线程可以访问进程中的公共变量,资源,所以使用多线程的过程中需要注意的问题是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏数据的完整性。数据之间的相互制约包括
1、间接制约关系,即两个线程需要访问同一资源,该资源在同一时刻只能被一个线程访问,这种关系称之为线程间对资源的互斥访问,某种意义上说互斥是一种制约关系更小的同步
所谓互斥,是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
2、直接制约关系,即一个线程的处理结果,为另一个线程的输入,因此线程之间直接制约着,这种关系可以称之为同步关系
所谓同步,是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
进程间的通信方式:
1.管道(pipe)及有名管道(named pipe):
管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
2.信号(signal):
信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
3.消息队列(message queue):
消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
4.共享内存(shared memory):
可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
5.信号量(semaphore):
主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
6.套接字(socket);
这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
线程间通信
使用全局变量
主要由于多个线程可能更改全局变量,因此全局变量最好声明为volatile(易变的)
使用消息实现通信
在Windows程序设计中,每一个线程都可以拥有自己的消息队列(UI线程默认自带消息队列和消息循环,工作线程需要手动实现消息循环),因此可以采用消息进行线程间通信sendMessage,postMessage。
1)定义消息#define WM_THREAD_SENDMSG=WM_USER+20;
2)添加消息函数声明afx_msg int OnTSendmsg();
3)添加消息映射ON_MESSAGE(WM_THREAD_SENDMSG,OnTSM)
4)添加OnTSM()的实现函数;
5)在线程函数中添加PostMessage消息Post函数
使用事件CEvent类实现线程间通信
Event对象有两种状态:有信号和无信号,线程可以监视处于有信号状态的事件,以便在适当的时候执行对事件的操作。
1)创建一个CEvent类的对象:CEvent threadStart;它默认处在未通信状态;
2)threadStart.SetEvent();使其处于通信状态;
3)调用WaitForSingleObject()来监视CEvent对象
线程间的同步方式有四种
临界区
临界区对应着一个CcriticalSection对象,当线程需要访问保护数据时,调用EnterCriticalSection函数;当对保护数据的操作完成之后,调用LeaveCriticalSection函数释放对临界区对象的拥有权,以使另一个线程可以夺取临界区对象并访问受保护的数据。
PS:关键段对象会记录拥有该对象的线程句柄即其具有“线程所有权”概念,即进入代码段的线程在leave之前,可以重复进入关键代码区域。所以关键段可以用于线程间的互斥,但不可以用于同步(同步需要在一个线程进入,在另一个线程leave)
互斥量
互斥与临界区很相似,但是使用时相对复杂一些(互斥量为内核对象),不仅可以在同一应用程序的线程间实现同步,还可以在不同的进程间实现同步,从而实现资源的安全共享。
PS:1、互斥量由于也有线程所有权的概念,故也只能进行线程间的资源互斥访问,不能由于线程同步;
2、由于互斥量是内核对象,因此其可以进行进程间通信,同时还具有一个很好的特性,就是在进程间通信时完美的解决了”遗弃”问题
信号量
信号量的用法和互斥的用法很相似,不同的是它可以同一时刻允许多个线程访问同一个资源,PV操作
PS:事件可以完美解决线程间的同步问题,同时信号量也属于内核对象,可用于进程间的通信
事件
事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。由SetEvent()来触发,由ResetEvent()来设成未触发。
PS:事件是内核对象,可以解决线程间同步问题,因此也能解决互斥问题
synchronized
这时要引入互斥锁,目的是互斥访问,特性如下:
关键字synchronized可以满足上述特性。根据获取的锁的分类:获取对象锁和获取类锁!!!
2表示使用synchronized修饰的方法(method)代码解释
synchronized的底层实现
两个要点:
- Java对象头
- Monitor
轻量级锁和偏向锁是1.6后对锁的优化。
Monitor相当于Java对象天生自带的一把看不见的锁(或称监视器锁,一种同步机制或同步对象)
重量级锁指针指向的就是Monitor对象的起始地址(每个对象都有一个Monitor对象与之关联);Monitor对象中的关键属性如下:
Monitor对象存在于每个Java对象的对象头中,
查看编译后的指令,monitorenter进入同步块,monitorexit表示退出,两者配对表示monitor锁。
锁的重入:
自旋锁和自适应自旋锁
- 自旋锁
- 线程A想要获取线程B占有的资源,就让线程A执行时等一会但是不放弃CPU的时间
- 自适应自旋锁
- 应该是自旋等待时间的变化
- 锁消除
- 编译时对运行上下文进行扫描,去除不可能存在竞争的锁
- 锁粗化
- 通过扩大锁的范围,避免反复加锁和解锁
synchronized锁的四种状态
锁的内存语义
JMM
CAS
Java线程池
在web开发中,服务器要接受并处理请求,所以会为一个请求分配一个线程来处理,如果并发的请求比较多,则耗费在线程创建和回收大量时间,使用线程池来重复利用线程
3中会保证顺序执行各个任务,在任意给定线程不会有多个线程执行;8是Java8加入的
是一个根据一组执行策略调度,调用、执行和控制的异步任务的框架,目的是提供一种将任务提交和任务融合运行分离开的机制。
线程池设计
Java异常
finally是先于return执行的。
Java集合框架
集合总结
Map家族
选用
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap。
当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。
HashSet
的底层是HashMap实现的,是将元素以键的形式存入HashMap的Key中,而他的值Value是一个final类型的PRESENT对象
TreeSet
实现了排序;底层也是HashMap实现
第一种是基于元素对象自身实现comparable接口的自然排序
即让一个元素对象A实现Comparable接口,就要重写
equals();compareTo()自然排序;hashCode();
三个方法,为了能够让对象A在加入TreeSet后能正确排序,其中equals和compareTo()的方法就要按相同的规则比较两个对象是否相等。一旦equals()方法重写,就要重写hashCode(),即在使用equals方法比较两个对象相同时,也要保证两个对象的hashCode值也相等。其主要的方法就是compareTo()方法。
另一种是基于不与单元元素绑定的Comparator接口的排序,即客户化排序
首先对象A还是实现Comparable接口,但不需要在A中改变compareTo()方法,只需要在外部类AComparator实现Comparator,实现其中的compare()方法(该方法中设计一个比较方式即可)。此时A类即实现了Comparable接口,在外部又实现了Comparator,在存入Set集合中时,会优先以compare()方法中的比较规则为准。TreeSet集合中也是如此。
HashMap
Key-Value的形式存储数据,其中的key是不可重复的,使用Set来保存的,有去重功能,value是通过Collection。
混合高低位计算hash值,使之分布散列均匀。用位与操作替代取模。
1.8之前会发生扩容死锁问题;
1 | void transfer(Entry[] newTable, boolean rehash) { |
此时有两个线程T1、T2同时插入a4,则T1、T2同时进行扩容操作,它们各自新建了一个Entry数组newTable。
T2线程执行到transfer方法的Entry<K,V> next = e.next;时被挂起,T1线程执行transfer方法后Entry数组如下图:
在T1线程没返回新建Entry数组之前,T2线程恢复,因为在T2挂起时,变量e指向的是a1,变量next指向的是a2,所以在T2恢复执行完transfer之后,Entry数组如下图:
可以看到在T2执行完transfer方法后,a1元素和a2元素形成了循环引用,此时无论将T1的Entry数组还是T2的Entry数组返回作为扩容后的新数组,都会存在这个环形链表,当调用get方法获取该位置的元素时就会发生死循环
1.8之后就解决了这个问题,因为没有再使用指针引用的方式去传值,而是重新计算在新数组上的hash序列号,移动值到新HashMap上。
没解决的问题就是多线程安全问题:
如果有两个线程A和B,都进行插入数据,刚好这两条不同的数据经过哈希计算后得到的哈希码是一样的,且该位置还没有其他的数据。所以这两个线程都会进入插值处的代码中。假设一种情况:
- 线程A通过if判断,该位置没有哈希冲突,进入了if语句,还没有进行数据插入,这时候 CPU 就把资源让给了线程B,线程A停在了if语句里面,
- 线程B判断该位置没有哈希冲突(线程A的数据还没插入),也进入了if语句,线程B执行完后,轮到线程A执 行,现在线程A直接在该位置插入而不用再判断。
- 这时候,你会发现线程A把线程B插入的数据给覆盖了,发生了线程不安全情况。
HashTable
哈希表(HashTable)又叫做散列表,是根据关键码值(即键值对)而直接访问的数据结构。也就是说,它通过把关键码映射到表中一个位置来访问记录,以加快查找速度。
- 特点一:线程安全;
- 特点二:K/V都不允许为null;
使用哈希函数将被查找的键转化为数组的索引。
处理冲突的方法很多,有拉链法和线性探索法。
注:线程安全,使用synchronized关键字修饰关键方法
ConcurrentHashMap
锁细粒度化:将数组拆分成子数组,16位为一段,加上一把锁。
- 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
- Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。
更细化:Node + CAS + Synchronized
sync只锁定当前链表或者红黑树的首结点,将锁细粒化到表中每个元素,只要hash不冲突就不会发生并发。不允许插入null键,对数组元素的更新是使用CAS,需要不断地的失败重试。
容量控制参数:
(2)图示:
(4)表示发生哈希碰撞,则会锁住链表或二叉树头节点即数组元素;
只要哈希不冲突就不会出现并发获得锁的情况!
- 首先用CAS无锁操作插入头节点,如果插入失败,则表示有别的线程已经插入头节点了,需要循环重试;
- 如果头节点已经存在,就尝试获取头节点的synchronized锁,再进行操作
JUC
P91集20秒处
executor:线程执行器,一个任务执行和调度的框架;tools下还有和executor相关的executors类,用来创建executorServices等对象;
locks:Java5后增加协调共享对象的访问方法,显示锁,方便对线程之间的共享资源做更细粒度的锁控制,一个lock可以创建多个Condition,用于将线程的等待和唤醒对象化,基于AQS实现,底层是调用Locksupport.unpark/park()实现线程的阻塞和唤醒。另一个是可重入读写锁,读读可共存,读写不可同时;
atomic:原子操作类,一个操作是不可中断的(即使是多线程环境中)。4中原子更新方式:原子更新基本类型、原子更新数组、原子更新引用、原子更新字段。atomic使用的是CAS的更新方式,当某个线程在执行atomic方法时,不会被其他线程打断,别的线程就会像自旋锁一样一直等到该方法执行完成,才由JVM从等待队列中选择一个线程来执行,是非阻塞的。在多线程中的累加操作时可以使用atomic变量来实现;
tools下的并发工具类:主要用于线程的同步
闭锁:
TA主线程,事件T123,引入CountDownLatch后,主线程就陷入等待,并有一个计数器cnt(事件的个数),每一个事件执行countDown()方法后就减一,直到所有子线程(事件)执行完毕,cnt为0时,主线程重新进入执行状态。
注:这里的子线程在执行完countDown()方法会可以继续执行
栅栏:
线程T123每调用一次await()方法时,其中的计数器都会减一,只要计数器不为0,该线程都会阻塞,其中TA线程(当前线程)会和T123一起,在计数器为0时同时执行。
这里的子线程到达栅栏处便会等待,不可继续执行。
信号量:
通过acquire去获取一个许可,没有就等待;拥有资源的线程执行完后会release释放资源
交换器:
提供一个同步点,在这个同步点,两个线程会彼此交换数据。一个线程先到同步点会被阻塞,直到李刚一个线程也到该同步点。只能用于两个线程。
会发现男孩女孩说的话被交换了:
JUC:BlockingQueue阻塞队列
如果队列满了,入队操作将阻塞,如果队列空了,出队操作将阻塞
方法:boolean add():队尾添加元素方法,满了报异常;offer()队尾添加怨怒,满了返回false,或者添加等待时间;put()队尾添加元素,满了就阻塞一直等待到可添加;
E take():头部取出元素,为空就一直等待队列中有元素;poll()头部取元素,为空可设置等待时间,或返回false;
以下七个实现,均线程安全:
有边界是指容量有限,初始化时要指定大小;1,2先进先出;3按照优先级(元素具备可比性)
IO
BIO
基于流模型实现的,标志着其交互方式是基于同步阻塞方式,在读取输入流或写入输出流时,在读写操作完成前,线程会一直阻塞住,这之间的调用是可靠的线性操作;程序发送请求到内核,由内核进行通信,在内核准备好之前,该程序是被挂起的,所以在两个阶段程序都是挂起状态;
类比于C/S模式:一个连接一个线程;即客户端有连接请求时,服务端就要启动一个线程来处理,待操作系统返回结果,如果这个连接不做任何事,则会造成不必要的线程开销,可以通过线程池机制来改善。BIO的特点就是在IO执行的两个阶段都被阻塞住。
NIO-Java4引入
构建多路复用、同步非阻塞的IO操作;提供了channel、select、buffer等新的抽象可以构建多路复用、同步非阻塞的IO操作,提供更接近操作系统底层的数据操作方式。
特点是:在程序发起第一次请求时,线程并没有被阻塞,会反复检查数据有没有准备好,将原来大块阻塞不能用的时间分为小块的阻塞时间,有点类似于轮询,会有机会去执行。
类比C/S模式为:一个请求一个线程,客户端发送的请求会被注册到多路复用器上,多路复用器轮询到连接有IO请求时,才会启动一个线程处理;特点就是程序会询问内核数据有没有准备好,该阶段是非阻塞的,在后一阶段是阻塞的(数据拷贝)
基本所有的IO在NIO中都从Channel开始,有点像流,数据可以从Channel读到Buffer中(反过来亦可);
Channel有如下:
FileChannel的两个方法:
常用于:高效的网络文件的数据传输,和大文件拷贝,避免了将文件从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,避免了两次用户态和内核态的切换。
Buffers有如下:
Selector:
Selector允许单线程处理多个Channel,如果你的应用打开了多个连接(通道),每个连接的流量都很低,就可以使用selector。图示为一个单线程使用一个selector来处理3个channel的情形;使用selector得向selector注册channel,然后调用它的selector方法,这方法会一直阻塞直到某个注册的channel有事件就绪,一旦这个方法返回,线程就可以返回处理这些事件了,事件可以是有新的连接进来或者buffer里有数据可以读取等等。
selector源码:
Selector组件是由SelectorProvider创建的,底层根据不同操作系统的IO多路复用来实现,会返回PollSelectorProvider(others)、EPollSelectorProvider(Linux)等
单线程可以处理多个网络IO,IO多路复用调用系统级别的select/poll/epoll模型,由系统监控IO状态,select轮询可以监控多个IO请求,当有一个准备好时,就可以返回。
select连接是有限的,因为是基于数组的
FD:文件唯一标识符
AIO:异步非阻塞IO
应用操作直接返回,而不会阻塞在那里,当后台完成,操作系统就会通知响应线程进行后续操作。AIO属于异步模型,用户线程可以同时处理别的事务。
基于回调:实现接口,调用时把回调函数传给对应的API即可
返回Future:处理完一个事,通过isDone()方法查看是否已经准备好数据
小结
Spring
家族图谱
第一阶段Spring Core、Spring Security、Spring Data,实现将单体应用开发服务好。不仅仅提供的便捷的数据库访问,web 中SpringMVC等必要功能。使用IOC、AOP实现应用低耦合、可扩展。
利用工厂模式(DI)和代理模式(AOP)来解耦应用组件,然后实现了web应用的框架(SpringMVC);
第二阶段推出的SpringBoot不仅仅提高了开发效率,而且将程序由可用变为好用。
第三阶段的Spring Cloud,推动了微服务架构的落地
第四阶段:Spring Cloud DataFlow
DataFlow将实时消息的处理任务和临时运行的任务都作为组件处理,定义这样组件的交互。
SpringIOC
IOC是一种思想,使我们从繁琐的对象交互中解脱出来,进而专注于对象本身,更进一步了解面向对对象。
一般的设计思路:
先设计轮子,根据轮子设计底盘,根据底盘设计箱体…
这里每个类的构造函数都直接调用了底层代码的构造函数,这样底层需求变动时会影响整个上层的代码,是不可取的。
依赖注入的是思路
- set:实现特定属性的public set()方法,让IOC容器注入所依赖类型的对象
- 接口:实现特定接口,让IOC容器注入所依赖类型的对象
- 构造函数:实现特定参数的构造函数,实现在创建对象时让IOC容器注入所依赖类型的对象
- 注解:通过Java的注解机制,让IOC容器注入所依赖类型的对象
依赖倒置原则、IOC、DI、IOC容器的关系
什么是依赖倒置原则?假设我们设计行李箱:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘设计箱体,最后根据箱体设计好整个箱子。这里就出现了一个“依赖”关系:箱子依赖箱体,箱体依赖底盘,底盘依赖轮子。
上面的依赖关系是该原则所反对的,该原则的思想为高层模块不应该依赖与底层模块,两者都应该依赖于其抽象。
依赖倒置原则思想的指导才有了IOC的思路,有了IOC的思路则需要DI方法的支撑。Spring框架基于IOC才提出了容器的概念,容器管理着Bean的生命周期,控制着Bean的依赖注入。
控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。这几种概念的关系大概如下:
那什么是控制反转容器(IoC Container)呢?对行李箱类进行初始化的那段代码发生的地方,就是控制反转容器。
显然你也应该观察到了,因为采用了依赖注入,在初始化的过程中就不可避免的会写大量的new。这里IoC容器就解决了这个问题。这个容器可以自动对你的代码进行初始化,你只需要维护一个Configuration(可以是xml可以是一段代码),而不用每次初始化行李箱都要亲手去写那一大段初始化的代码。这是引入IoC Container的第一个好处。
IOC的优势:
而IoC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new(有点像深度优先遍历):
我们就像是工厂的客户。我们只需要向工厂请求一个Luggage实例,然后它就给我们按照Config创建了一个Luggage实例。我们完全不用管这个Luggage实例是怎么一步一步被创建出来。
IOC容器
实际Spring IOC容器是怎么实现对象的创建和依赖的:
1、Spring启动时读取应用程序提供的Bean配置信息,并在容器生成一份相应的Bean配置注册表
2、根据注册表加载、实例化bean、建立Bean与Bean之间的依赖关系
这里利用的是Java语言的反射功能实例化bean、并建立Bean之间的依赖关系
3、将这些准备就绪的Bean放到Map缓存池中,为上层提供就绪的运行环境,等待应用程序执行调用
Spring 作者设计这两个核心接口用以表示容器
相关接口:
Spring容器在启动时会将xml或者注解里的Bean的定义解析为Spring内部的BeanDefinition
Spring 将Bean的定义解析为BeanDefinition后会通过BeanDefinitionRegistry 以BeanName为key,BeanDefinition为value存储到BeanDefinitionMap (这是个ConcurrentHashMap类型的map结构)中。
核心接口
BeanFactory接口
包含Bean的各种定义,以便在接收客户端请求时可以实例化Bean,并在实例化对象时建立Bean 之间的依赖关系,这将使Bean从Bean客户端中解放出来。
BeanFactory源码:
各种getBean()方法,可以看到可以从Spring中按类型 / 按名称获取Bean
判断是否为单例方法,SpringIOC中,默认Bean都是以单例存在的
与上面相反,判断为多例的
ApplicationContext接口
3表示可以管理message,4表示可以发布实事件给监听器,实现监听
可以看出这不单单是个工厂,是整个应用上下文,代表整个大容器的所有功能
SpringBoot的自带启动类的run()方法,深入进去其最终会执行createApplicationContext()方法,会发现其会用Class.forName加载AnnotationConfigServleWebServerApplicationContext类。
案例分析
Bean装载案例:
Person Bean:
扫描装配Bean,使用注解@Component(指定要扫描的类进入IOC容器中)@Value()是赋值
启动类:
@SpringBootApplication注解包含了启动扫描的功能。
Bean依赖注入案例:
装载Dog 类进入IOC容器
注入Pet(Pet是个接口),@Autowired会根据属性类型找到对应的Bean进行注入(最基本是用getBean()方法根据类型注入),这里的Dog类是Pet的一种实现,所以会根据类型查找到Dog,由SpringIOC容器将Dog的实例注入Person中。
当有多个Pet的实现时(例如还有Cat、Bird类),需要在要注入的类上加上@Primary注解。
getBean()解析
会调用doGetBean()放法;
1、先获取Bean名称beanName;2、获取一个(共享的)实例;3、试着从缓存或者实例工厂中获取实例;
Bean 的作用域
Bean 的生命周期
1、实例化Bean对象以及设置属性
2、 对 Aware 接口的实现,目的是在Bean中设置对IOC容器的感知
3、Bean的前置初始化方法,对Spring容器完成实例化的Bean添加自定义的处理逻辑
4、… 5、…
6、Bena的后置初始化方法,Bean实例初始化后的自定义工作,3、6和AOP相关
SpringAOP
分类与合并(织入)
Spring 采用的方式不需要特殊的Java编译器,但性能开销多一些