java知识点
2021-02-19 17:45:59 5 举报
AI智能生成
java体系,不断完善中
作者其他创作
大纲/内容
网络
网络协议
OSI网络七层协议(Open System Interconnection)
7、应用层
是参考模型的最高层。
主要功能是:
为应用软件提供了很多服务,
比如文件服务器、数据库服务、电子邮件与其他网络软件服务。
主要功能是:
为应用软件提供了很多服务,
比如文件服务器、数据库服务、电子邮件与其他网络软件服务。
TELNET
HTTP
HTTPS
FTP
TFTP
NFS
SMTP
等
子主题
子主题
6、表示层
是参考模型的第六层。
主要功能是:
用于处理在两个通信系统中交换信息的表示方法,
主要包括数据格式变换、数据加密与解密、数据压缩与恢复等功能
主要功能是:
用于处理在两个通信系统中交换信息的表示方法,
主要包括数据格式变换、数据加密与解密、数据压缩与恢复等功能
5、会话层
是参考模型的第五层。
主要功能是:
负责维扩两个结点之间的传输连接,
以便确保点到点传输不中断,
以及管理数据交换等功能。
主要功能是:
负责维扩两个结点之间的传输连接,
以便确保点到点传输不中断,
以及管理数据交换等功能。
Socket抽象层(非OSI之中)
一种连接模式,不是协议,socket是对TCP/IP协议的封装,是一个调用接口(API)
其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面
用socket可以创建tcp连接,也可以创建udp连接
适合于对传输速度,安全性,实时交互,费用等要求高的应用中,如网络游戏,手机应用,银行内部交互等
于通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开
用socket可以创建tcp连接,也可以创建udp连接
适合于对传输速度,安全性,实时交互,费用等要求高的应用中,如网络游戏,手机应用,银行内部交互等
于通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开
4、传输层
是参考模型的第四层。
主要功能是:
向用户提供可靠地端到端服务,
处理数据包错误、数据包次序,
以及其他一些关键传输问题。
传输层向高层屏蔽了下层数据通信的细节。
因此,它是计算机通信体系结构中关键的一层。
主要功能是:
向用户提供可靠地端到端服务,
处理数据包错误、数据包次序,
以及其他一些关键传输问题。
传输层向高层屏蔽了下层数据通信的细节。
因此,它是计算机通信体系结构中关键的一层。
TCP
传输控制协议(Transmission Control Protocol)
是一种面向连接的、可靠的、基于字节流的传输层通信协议
可靠性
应用数据分割成适合TCP的数据块,称为报文或段;
当TCP发出一个段后启动是一个定时器,如果在一定时间内没有收到目的端收到段的确认,则重新发送这个报文段,如收到,TCP也会发送一个确认;TCP的延时确认功能,开启时,有定时器触发确认时间点,未开启就立即确认。
TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。必要时,TCP对收到的数据重新排序后交给应用层。
提供流量控制
可靠性
应用数据分割成适合TCP的数据块,称为报文或段;
当TCP发出一个段后启动是一个定时器,如果在一定时间内没有收到目的端收到段的确认,则重新发送这个报文段,如收到,TCP也会发送一个确认;TCP的延时确认功能,开启时,有定时器触发确认时间点,未开启就立即确认。
TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。必要时,TCP对收到的数据重新排序后交给应用层。
提供流量控制
UDP
用户数据报协议(User Datagram Protocol)
无连接的传输层协议
传输速度优于TCP
从报文角度看,udp协议开销小,流量不可控制
一个不可靠的,但它是分发信息的一个理想协议。
UDP也用在路由信息协议RIP(Routing Information Protocol)中修改路由表。
在这些应用场合下,如果有一个消息丢失,在几秒之后另一个新的消息就会替换它。
无连接的传输层协议
传输速度优于TCP
从报文角度看,udp协议开销小,流量不可控制
一个不可靠的,但它是分发信息的一个理想协议。
UDP也用在路由信息协议RIP(Routing Information Protocol)中修改路由表。
在这些应用场合下,如果有一个消息丢失,在几秒之后另一个新的消息就会替换它。
总结
tcp
udp适合传输少量数据
udp适合传输少量数据
SPX
序列分组交换协议(Sequenced Packet Exchange protocol)
3、网络层
是参考模型的第三层。
主要功能是:
为数据在节点之间传输创建逻辑链路,
通过路由选择算法为分组通过通信子网选择最适当的路径,
以及实现拥塞控制、网络互连等功能。
IP
不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。
IPX
主要功能是:
为数据在节点之间传输创建逻辑链路,
通过路由选择算法为分组通过通信子网选择最适当的路径,
以及实现拥塞控制、网络互连等功能。
IP
不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。
IPX
2、数据链路层
参考模型的第二层。
主要功能是:
在物理层提供的服务基础上,
在通信的实体间建立数据链路连接,
传输以“帧”为单位的数据包,
并采用差错控制与流量控制方法,
使有差错的物理线路变成无差错的数据链路。
主要功能是:
在物理层提供的服务基础上,
在通信的实体间建立数据链路连接,
传输以“帧”为单位的数据包,
并采用差错控制与流量控制方法,
使有差错的物理线路变成无差错的数据链路。
1、物理层
是参考模型的最低层。
该层是网络通信的数据传输介质,由连接不同结点的电缆与设备共同构成。
主要跟功能是:
利用传输介质为数据链路层提供物理连接,
负责处理数据传输并监控数据出错率,
以便数据流的透明传输。
有关传输介质的特性,这些规范通常也参考了其他组织制定的标准。连接头、帧、帧的使用、电流、编码及光调制等都属于各种物理层规范中的内容
该层是网络通信的数据传输介质,由连接不同结点的电缆与设备共同构成。
主要跟功能是:
利用传输介质为数据链路层提供物理连接,
负责处理数据传输并监控数据出错率,
以便数据流的透明传输。
有关传输介质的特性,这些规范通常也参考了其他组织制定的标准。连接头、帧、帧的使用、电流、编码及光调制等都属于各种物理层规范中的内容
HTTP/HTTPS/HTTP2
HTTPS
数字证书
数字证书由信任的第三方,即认证中心使用自己的私钥对A的公钥加密,加密后的文件
HSTS(HTTP严格安全传输)
好处1:浏览器会自动采用HTTPS访问网站地址,从而保证用户始终访问到网站的加密连接,保护数据传输安全
好处2:浏览器强制拒绝不安全的链接,可以有效防范中间人的攻击,同时也会省去网站301/302跳转花费的时间,大大提升安全系数和用户体验
配置header:strict-Transport-Security:max-age=expireTime[;includeSubDomains] [;preload]
https访问响应头中添加
max-age 参数,时间设置不宜过长,建议设置时间为6个月
加入HTTS Preload List
阅读资料
子主题
HTTP
http协议的作用及特点
超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。1960年美国人Ted Nelson构思了一种通过计算机处理文本信息的方法,并称之为超文本(hypertext),这成为了HTTP超文本传输协议标准架构的发展根基。Ted Nelson组织协调万维网协会(World Wide Web Consortium)和互联网工程工作小组(Internet Engineering Task Force )共同合作研究,最终发布了一系列的RFC,其中著名的RFC 2616定义了HTTP 1.1。
特点
1.基于请求/响应模型的协议。请求和响应必须成对,先有请求后有响应
2.http协议默认端口:80
3.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
4.灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
5.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
6.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
HTTP是一个客户端和服务器端请求和应答的标准(TCP)。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认端口为80)的HTTP请求。(我们称这个客户端)叫用户代理(user agent)。应答的服务器上存储着(一些)资源,比如HTML文件和图像。(我们称)这个应答服务器为源服务器(origin server)。在用户代理和源服务器中间可能存在多个中间层,比如代理,网关,或者隧道(tunnels)。尽管TCP/IP协议是互联网上最流行的应用,HTTP协议并没有规定必须使用它和(基于)它支持的层。 事实上,HTTP可以在任何其他互联网协议上,或者在其他网络上实现。HTTP只假定(其下层协议提供)可靠的传输,任何能够提供这种保证的协议都可以被其使用。
通常,由HTTP客户端发起一个请求,建立一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端发送过来的请求。一旦收到请求,服务器(向客户端)发回一个状态行,比如"HTTP/1.1 200 OK",和(响应的)消息,消息的消息体可能是请求的文件、错误消息、或者其它一些信息。HTTP使用TCP而不是UDP的原因在于(打开)一个网页必须传送很多数据,而TCP协议提供传输控制,按顺序组织数据,和错误纠正。
通过HTTP或者HTTPS协议请求的资源由统一资源标示符(Uniform Resource Identifiers)(或者,更准确一些,URLs)来标识。
通常,由HTTP客户端发起一个请求,建立一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端发送过来的请求。一旦收到请求,服务器(向客户端)发回一个状态行,比如"HTTP/1.1 200 OK",和(响应的)消息,消息的消息体可能是请求的文件、错误消息、或者其它一些信息。HTTP使用TCP而不是UDP的原因在于(打开)一个网页必须传送很多数据,而TCP协议提供传输控制,按顺序组织数据,和错误纠正。
通过HTTP或者HTTPS协议请求的资源由统一资源标示符(Uniform Resource Identifiers)(或者,更准确一些,URLs)来标识。
协议功能
HTTP协议(HyperText Transfer Protocol,超文本传输协议)是用于从WWW服务器传输超文本到本地浏览器的传输协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机正确快速地传输超文本文档,还确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本先于图形)等。
HTTP是客户端浏览器或其他程序与Web服务器之间的应用层通信协议。在Internet上的Web服务器上存放的都是超文本信息,客户机需要通过HTTP协议传输所要访问的超文本信息。HTTP包含命令和传输信息,不仅可用于Web访问,也可以用于其他因特网/内联网应用系统之间的通信,从而实现各类应用资源超媒体访问的集成。
我们在浏览器的地址栏里输入的网站地址叫做URL (Uniform Resource Locator,统一资源定位符)。就像每家每户都有一个门牌地址一样,每个网页也都有一个Internet地址。当你在浏览器的地址框中输入一个URL或是单击一个超级链接时,URL就确定了要浏览的地址。浏览器通过超文本传输协议(HTTP),将Web服务器上站点的网页代码提取出来,并翻译成漂亮的网页。
HTTP是客户端浏览器或其他程序与Web服务器之间的应用层通信协议。在Internet上的Web服务器上存放的都是超文本信息,客户机需要通过HTTP协议传输所要访问的超文本信息。HTTP包含命令和传输信息,不仅可用于Web访问,也可以用于其他因特网/内联网应用系统之间的通信,从而实现各类应用资源超媒体访问的集成。
我们在浏览器的地址栏里输入的网站地址叫做URL (Uniform Resource Locator,统一资源定位符)。就像每家每户都有一个门牌地址一样,每个网页也都有一个Internet地址。当你在浏览器的地址框中输入一个URL或是单击一个超级链接时,URL就确定了要浏览的地址。浏览器通过超文本传输协议(HTTP),将Web服务器上站点的网页代码提取出来,并翻译成漂亮的网页。
http协议的版本
HTTP/1.0,发送请求,创建一次连接,获得一个web资源,连接断开
HTTP/1.1,发送请求,创建一次连接,获得多个web资源,连接断开
Http协议的组成
Http协议由Http请求和Http响应组成,当在浏览器中输入网址访问某个网站时, 你的浏览器会将你的请求封装成一个Http请求发送给服务器站点,服务器接收到请 求后会组织响应数据封装成一个Http响应返回给浏览器。即没有请求就没有响应。
http请求包括:请求行、请求头、请求体
http响应包括:响应行、响应头、响应体
HTTP请求报文
HTTP请求报文由3部分组成(请求行+请求头+请求体):
请求行:
例如:POST /chapter17/user.html HTTP/1.1
格式:请求方式 资源路径 协议/版本
请求行必须在http请求格式的第一行。
格式:请求方式 资源路径 协议/版本
请求行必须在http请求格式的第一行。
get请求:
将请求参数追加在url后面,不安全
url长度限制get请求方式数据的大小
没有请求体
一般的HTTP请求大多都是GET。
将请求参数追加在url后面,不安全
url长度限制get请求方式数据的大小
没有请求体
一般的HTTP请求大多都是GET。
post请求:
请求参数在请求体处,较安全。
请求数据大小没有显示
只有表单设置为method=“post”才是post请求,其他都是get请求
常见get请求:地址栏直接访问、<a href="">、<img src="">等
请求参数在请求体处,较安全。
请求数据大小没有显示
只有表单设置为method=“post”才是post请求,其他都是get请求
常见get请求:地址栏直接访问、<a href="">、<img src="">等
HEAD请求:
HEAD跟GET相似,不过服务端接收到HEAD请求时只返回响应头,不发送响应内容。所以,如果只需要查看某个页面的状态时,用HEAD更高效,因为省去了传输页面内容的时间。
HEAD跟GET相似,不过服务端接收到HEAD请求时只返回响应头,不发送响应内容。所以,如果只需要查看某个页面的状态时,用HEAD更高效,因为省去了传输页面内容的时间。
DELETE请求:
删除某一个资源。
删除某一个资源。
OPTIONS请求:
用于获取当前URL所支持的方法。若请求成功,会在HTTP头中包含一个名为“Allow”的头,值是所支持的方法,如“GET, POST”。
用于获取当前URL所支持的方法。若请求成功,会在HTTP头中包含一个名为“Allow”的头,值是所支持的方法,如“GET, POST”。
PUT请求:
把一个资源存放在指定的位置上。
本质上来讲, PUT和POST极为相似,都是向服务器发送数据,但它们之间有一个重要区别,PUT通常指定了资源的存放位置,而POST则没有,POST的数据存放位置由服务器自己决定。
把一个资源存放在指定的位置上。
本质上来讲, PUT和POST极为相似,都是向服务器发送数据,但它们之间有一个重要区别,PUT通常指定了资源的存放位置,而POST则没有,POST的数据存放位置由服务器自己决定。
TRACE请求:
回显服务器收到的请求,主要用于测试或诊断。
回显服务器收到的请求,主要用于测试或诊断。
CONNECT请求:
CONNECT方法是HTTP/1.1协议预留的,能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接与非加密的HTTP代理服务器的通信。
CONNECT方法是HTTP/1.1协议预留的,能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接与非加密的HTTP代理服务器的通信。
在 REST 架构风格中,有严格规定对于不同的请求类型要设置合适的请求方法。
也是避免出 现因为乱用导致混乱的问题。这里提到了 REST 架构,现在很多同学都在写 REST,有没有人 能够明白为什么要定义 REST 这个架构风格?
随着服务化架构的普及,http 协议的使用频率越来越高
很多人在错误的使用 http 协议定义接口,比如各种各样的命名,什么 getUserInfoById, deleteById 之类的、有状态和无状态请求混用。
对于 http 协议本身提供的规则并没有很好的利用
所以,为了更好的解决这些问题,干脆就定义一套规则,这套规则并没有引入新的东西,无 非就是对 http 协议本身的使用做了一些约束,比如说
REST 是面向资源,每一个 URI 代表一个资源
强调无状态化,服务器端不能存储来自某个客户的某个请求中的信息,并在该客户的其他 请求中使用
强调 URL 暴露资源时,不要在 URI 中出现动词
合理的利用 http 状态码、请求方法。
因此大家在参照这种标准去使用 REST 风格时,要明白你遵循的是什么以及要解决什么问题。
也是避免出 现因为乱用导致混乱的问题。这里提到了 REST 架构,现在很多同学都在写 REST,有没有人 能够明白为什么要定义 REST 这个架构风格?
随着服务化架构的普及,http 协议的使用频率越来越高
很多人在错误的使用 http 协议定义接口,比如各种各样的命名,什么 getUserInfoById, deleteById 之类的、有状态和无状态请求混用。
对于 http 协议本身提供的规则并没有很好的利用
所以,为了更好的解决这些问题,干脆就定义一套规则,这套规则并没有引入新的东西,无 非就是对 http 协议本身的使用做了一些约束,比如说
REST 是面向资源,每一个 URI 代表一个资源
强调无状态化,服务器端不能存储来自某个客户的某个请求中的信息,并在该客户的其他 请求中使用
强调 URL 暴露资源时,不要在 URI 中出现动词
合理的利用 http 状态码、请求方法。
因此大家在参照这种标准去使用 REST 风格时,要明白你遵循的是什么以及要解决什么问题。
请求头:
例如:Host: 39.108.107.149:8080
请求头从第二行开始,到第一个空格结束。请求头和请求体之间存在一个空格(如下)
请求头从第二行开始,到第一个空格结束。请求头和请求体之间存在一个空格(如下)
请求头通常以键值对{key:value}方式传递数据。
key为规范的固定值
value为key对应的取值,通常是一个值,可能是一组。
key为规范的固定值
value为key对应的取值,通常是一个值,可能是一组。
HTTP请求报文头属性
常见请求头
Referer:表示这个请求是从哪个url跳过来的,通过百度来搜索淘宝网,那么在进入淘宝网的请求报文中,Referer的值就是:www.baidu.com。如果是直接访问就不会有这个头。
Referer:表示这个请求是从哪个url跳过来的,通过百度来搜索淘宝网,那么在进入淘宝网的请求报文中,Referer的值就是:www.baidu.com。如果是直接访问就不会有这个头。
常用于:防盗链。
Referrer Policy: no-referrer-when-downgrade
Referrer Policy: no-referrer-when-downgrade
Accept:告诉服务端,该请求所能支持的响应数据类型,专业术语称为MIME 类型(文件类型的一种描述方式)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
MIME格式:大类型/小类型[;参数]
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
例如:
text/html,html文件
text/css,css文件
text/javascript,js文件
image/*,所有图片文件
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
例如:
text/html,html文件
text/css,css文件
text/javascript,js文件
image/*,所有图片文件
if-Modified-Sincce:浏览器通知服务器,本地缓存的最后变更时间。与另一个响应头组合控制浏览器页面的缓存
Cokkie:客户端的Cookie就是通过这个报文头属性传给服务端的哦!
Cokkie:客户端的Cookie就是通过这个报文头属性传给服务端的哦!
Cookie: JSESSIONID=15982C27F7507C7FDAF0F97161F634B5
这里就出了一个问题,网站A怎么保证自己请求体中保存的cookie就是网站A的cookie而不是网站B的cookie呢,这就和cookie里面的jsessionid有关系了,关于cookie,session,sessionid,jsessionid的区别联系,可以参考这个博文:
这里就出了一个问题,网站A怎么保证自己请求体中保存的cookie就是网站A的cookie而不是网站B的cookie呢,这就和cookie里面的jsessionid有关系了,关于cookie,session,sessionid,jsessionid的区别联系,可以参考这个博文:
User-Agent:浏览器通知服务器,客户端浏览器与操作系统相关信息
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Connection:表示客户端与服务连接类型;Keep-Alive表示持久连接,close已关闭
Connection: keep-alive
Host:请求的服务器主机名
Host: sczpkj.f3322.net:3000
Content-Length:请求体的长度
POST http://39.108.107.149:8080/vk/app/rest/ddp/iModelServiceImpl/findModelByType HTTP/1.1
User-Agent: Fiddler
Host: 39.108.107.149:8080
Content-Length: 11
name=城市
User-Agent: Fiddler
Host: 39.108.107.149:8080
Content-Length: 11
name=城市
Content-Type:请求的与实体对应的MIME信息。如果是post请求,会有这个头,默认值为application/x-www-form-urlencoded,表示请求体内容使用url编码
Content-Type: application/x-www-form-urlencoded
Accept-Encoding:浏览器通知服务器,浏览器支持的数据压缩格式。如GZIP压缩
Accept-Encoding: gzip, deflate
Accept-Language:浏览器通知服务器,浏览器支持的语言。各国语言(国际化i18n)
Accept-Language: zh-CN,zh;q=0.9
Cache-Control:指定请求和响应遵循的缓存机制
对缓存进行控制,如一个请求希望响应返回的内容在客户端要被缓存一年,或不希望被缓存就可以通过这个报文头达到目的。
Cache-Control: no-cache
更多请求头属性可以参考这篇文章:HTTP响应头和请求头信息对照表
请求体
当请求方式是post的时,请求体会有请求的参数,格式如下:
username=zhangsan&password=123
username=zhangsan&password=123
POST http://39.108.107.149:8080/vk/app/rest/ddp/iModelServiceImpl/findModelByType HTTP/1.1
User-Agent: Fiddler
Host: 39.108.107.149:8080
Content-Length: 20
name=城市&status=1
User-Agent: Fiddler
Host: 39.108.107.149:8080
Content-Length: 20
name=城市&status=1
HTTP响应报文
HTTP的响应报文也由三部分组成(响应行+响应头+响应体)
响应行:
①报文协议及版本;
HTTP/1.1 200 OK
②状态码及状态描述;
状态码:由3位数字组成,第一个数字定义了响应的类别
1xx:指示信息,表示请求已接收,继续处理
2xx:成功,表示请求已被成功接受,处理。
200 OK:客户端请求成功
204 No Content:无内容。服务器成功处理,但未返回内容。一般用在只是客户端向服务器发送信息,而服务器不用向客户端返回什么信息的情况。不会刷新页面。
206 Partial Content:服务器已经完成了部分GET请求(客户端进行了范围请求)。响应报文中包含Content-Range指定范围的实体内容
3xx:重定向
301 Moved Permanently:永久重定向,表示请求的资源已经永久的搬到了其他位置。
302 Found:临时重定向,表示请求的资源临时搬到了其他位置
303 See Other:临时重定向,应使用GET定向获取请求资源。303功能与302一样,区别只是303明确客户端应该使用GET访问
307 Temporary Redirect:临时重定向,和302有着相同含义。POST不会变成GET
304 Not Modified:表示客户端发送附带条件的请求(GET方法请求报文中的IF…)时,条件不满足。返回304时,不包含任何响应主体。虽然304被划分在3XX,但和重定向一毛钱关系都没有
4xx:客户端错误
400 Bad Request:客户端请求有语法错误,服务器无法理解。
401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
403 Forbidden:服务器收到请求,但是拒绝提供服务
404 Not Found:请求资源不存在。比如,输入了错误的url
415 Unsupported media type:不支持的媒体类型
5xx:服务器端错误,服务器未能实现合法的请求。
500 Internal Server Error:服务器发生不可预期的错误。
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常,
1xx:指示信息,表示请求已接收,继续处理
2xx:成功,表示请求已被成功接受,处理。
200 OK:客户端请求成功
204 No Content:无内容。服务器成功处理,但未返回内容。一般用在只是客户端向服务器发送信息,而服务器不用向客户端返回什么信息的情况。不会刷新页面。
206 Partial Content:服务器已经完成了部分GET请求(客户端进行了范围请求)。响应报文中包含Content-Range指定范围的实体内容
3xx:重定向
301 Moved Permanently:永久重定向,表示请求的资源已经永久的搬到了其他位置。
302 Found:临时重定向,表示请求的资源临时搬到了其他位置
303 See Other:临时重定向,应使用GET定向获取请求资源。303功能与302一样,区别只是303明确客户端应该使用GET访问
307 Temporary Redirect:临时重定向,和302有着相同含义。POST不会变成GET
304 Not Modified:表示客户端发送附带条件的请求(GET方法请求报文中的IF…)时,条件不满足。返回304时,不包含任何响应主体。虽然304被划分在3XX,但和重定向一毛钱关系都没有
4xx:客户端错误
400 Bad Request:客户端请求有语法错误,服务器无法理解。
401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
403 Forbidden:服务器收到请求,但是拒绝提供服务
404 Not Found:请求资源不存在。比如,输入了错误的url
415 Unsupported media type:不支持的媒体类型
5xx:服务器端错误,服务器未能实现合法的请求。
500 Internal Server Error:服务器发生不可预期的错误。
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常,
响应头:
响应报文头,也是由多个属性组成;响应头也是用键值对k:v
服务器通过响应头来控制浏览器的行为,不同的头浏览器操作不同
服务器通过响应头来控制浏览器的行为,不同的头浏览器操作不同
响应体
响应报文体,服务器发送给浏览器的正文,即我们真正要的“干货” ;
响应体,响应体是服务器回写给客户端的页面正文,浏览器将正文加载到内存,然后解析渲染 显示页面内容
响应体,响应体是服务器回写给客户端的页面正文,浏览器将正文加载到内存,然后解析渲染 显示页面内容
Content-Type详解
前段时间在工作中负责接口的开发,使用到postman工具调试接口,发现对http理解一直不是很深入,下来又总结了一遍,发现很多东西确实是实践出真知。
application/x-www-form-urlencoded
最常见的post提交数据的方式。浏览器原生的form表单,如果不设置enctype属性,那么最终就会以application/x-www-form-urlencoded 方式提交数据
POST http://39.108.107.149:8080/vk/app/rest/ddp/iModelServiceImpl/findModelByType HTTP/1.1
Content-Type: application/x-www-form-urlencoded
cache-control: no-cache
Postman-Token: 8e602802-b4f5-4d05-96d7-e1c7a1951719
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: 39.108.107.149:8080
cookie: JSESSIONID=6CD80B7028062D9190717CEE001C3194
accept-encoding: gzip, deflate
content-length: 32
Connection: keep-alive
name=%E5%9F%8E%E5%B8%82&status=1
Content-Type: application/x-www-form-urlencoded
cache-control: no-cache
Postman-Token: 8e602802-b4f5-4d05-96d7-e1c7a1951719
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: 39.108.107.149:8080
cookie: JSESSIONID=6CD80B7028062D9190717CEE001C3194
accept-encoding: gzip, deflate
content-length: 32
Connection: keep-alive
name=%E5%9F%8E%E5%B8%82&status=1
首先,Content-Type 被指定为 application/x-www-form-urlencoded;其次,提交的数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL 转码。大部分服务端语言都对这种方式有很好的支持。
很多时候,我们用 Ajax 提交数据时,也是使用这种方式。例如 JQuery 和 QWrap 的 Ajax,Content-Type 默认「application/x-www-form-urlencoded;charset=utf-8」。
很多时候,我们用 Ajax 提交数据时,也是使用这种方式。例如 JQuery 和 QWrap 的 Ajax,Content-Type 默认「application/x-www-form-urlencoded;charset=utf-8」。
multipart/form-data
这又是一个常见的 POST 数据提交的方式。我们使用表单上传文件时,必须让 form 的 enctyped 等于这个值。
POST http://39.108.107.149:8080/vk/app/rest/ddp/iDataSourcesBaseService/file HTTP/1.1
Content-Type: multipart/form-data; boundary=--------------------------629236571647111133881449
cache-control: no-cache
Postman-Token: 2146b4b3-2d30-469c-bbcd-fbc4693934d9
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: 39.108.107.149:8080
cookie: JSESSIONID=6CD80B7028062D9190717CEE001C3194
accept-encoding: gzip, deflate
content-length: 435
Connection: keep-alive
----------------------------629236571647111133881449
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
test upload
----------------------------629236571647111133881449
Content-Disposition: form-data; name="extCode"
test
----------------------------629236571647111133881449
Content-Disposition: form-data; name="extId"
3306
----------------------------629236571647111133881449-- //结束标识
Content-Type: multipart/form-data; boundary=--------------------------629236571647111133881449
cache-control: no-cache
Postman-Token: 2146b4b3-2d30-469c-bbcd-fbc4693934d9
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: 39.108.107.149:8080
cookie: JSESSIONID=6CD80B7028062D9190717CEE001C3194
accept-encoding: gzip, deflate
content-length: 435
Connection: keep-alive
----------------------------629236571647111133881449
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
test upload
----------------------------629236571647111133881449
Content-Disposition: form-data; name="extCode"
test
----------------------------629236571647111133881449
Content-Disposition: form-data; name="extId"
3306
----------------------------629236571647111133881449-- //结束标识
首先生成了一个 boundary 用于分割不同的字段,为了避免与正文内容重复,boundary 很长很复杂。然后 Content-Type 里指明了数据是以 mutipart/form-data 来编码,本次请求的 boundary 是什么内容。
消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以 --boundary 开始,紧接着内容描述信息,然后是回车,最后是最后是字段具体内容(文本或二进制),如果传输的是文件,还要包含文件名和文件类型信息。消息主体最后以 --boundary-- 标示结束。
上面提到的这两种 POST 数据的方式,都是浏览器原生支持的,而且现阶段原生 form 表单也只支持这两种方式。但是随着越来越多的 Web 站点,尤其是 WebApp,全部使用 Ajax 进行数据交互之后,我们完全可以定义新的数据提交方式,给开发带来更多便利。
消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以 --boundary 开始,紧接着内容描述信息,然后是回车,最后是最后是字段具体内容(文本或二进制),如果传输的是文件,还要包含文件名和文件类型信息。消息主体最后以 --boundary-- 标示结束。
上面提到的这两种 POST 数据的方式,都是浏览器原生支持的,而且现阶段原生 form 表单也只支持这两种方式。但是随着越来越多的 Web 站点,尤其是 WebApp,全部使用 Ajax 进行数据交互之后,我们完全可以定义新的数据提交方式,给开发带来更多便利。
application/json
application/json 这个 Content-Type 作为响应头大家肯定不陌生。实际上,现在越来越多的人把它作为请求头,用来告诉服务端消息主体是序列化后的 JSON 字符串。由于 JSON 规范的流行,除了低版本 IE 之外的各大浏览器都原生支持 JSON.stringify,服务端语言也都有处理 JSON 的函数,使用 JSON 不会遇上什么麻烦。 JSON 格式支持比键值对复杂得多的结构化数据,这一点也很有用。
POST http://39.108.107.149:8080/vk/app/rest/ddp/vkIndexsService/queryVkIndxs HTTP/1.1
Content-Type: application/json
cache-control: no-cache
Postman-Token: 5014bc39-0777-49d5-bb8a-73db9a981e49
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: 39.108.107.149:8080
cookie: JSESSIONID=6CD80B7028062D9190717CEE001C3194
accept-encoding: gzip, deflate
content-length: 132
Connection: keep-alive
{
"name":"828验证继承",
"getresultType":"2",
"createTime":"Tue Sep 11 2018 00:00:00 GMT+0800 (中国标准时间)"
}
Content-Type: application/json
cache-control: no-cache
Postman-Token: 5014bc39-0777-49d5-bb8a-73db9a981e49
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: 39.108.107.149:8080
cookie: JSESSIONID=6CD80B7028062D9190717CEE001C3194
accept-encoding: gzip, deflate
content-length: 132
Connection: keep-alive
{
"name":"828验证继承",
"getresultType":"2",
"createTime":"Tue Sep 11 2018 00:00:00 GMT+0800 (中国标准时间)"
}
这种方案,可以方便的提交复杂的结构化数据,特别适合 RESTful 的接口。各大抓包工具如 Chrome 自带的开发者工具、Firebug、Fiddler,都会以树形结构展示 JSON 数据,非常友好。
text/xml
Http 协议中的扩展
如果传输的文件过大怎么办
服务器上返回的资源文件比较大,比如有些 js 文件大小可能就有几兆。文件过大就会影响传 输的效率,同时也会带来带宽的消耗。怎么办呢?
1. 常见的手段是,对文件进行压缩,减少文件大小。那压缩和解压缩的流程怎么实现呢? 首先服务端需要能支持文件的压缩功能,其次浏览器能够针对被压缩的文件进行解压缩。浏 览器可以指定 Accept-Encoding 来高速服务器我当前支持的编码类型 Accept-Encoding:gzip,deflate 那服务端会根据支持的编码类型,选择合适的类型进行压缩。常见的编码方式有:gzip/deflate
2. 分割传输 在传输大容量数据时,通过把数据分割成多块,能够让浏览器逐步显示页面。这种把实体主 体分块的功能称为分块传输编码(Chunked Transfer Coding)。
1. 常见的手段是,对文件进行压缩,减少文件大小。那压缩和解压缩的流程怎么实现呢? 首先服务端需要能支持文件的压缩功能,其次浏览器能够针对被压缩的文件进行解压缩。浏 览器可以指定 Accept-Encoding 来高速服务器我当前支持的编码类型 Accept-Encoding:gzip,deflate 那服务端会根据支持的编码类型,选择合适的类型进行压缩。常见的编码方式有:gzip/deflate
2. 分割传输 在传输大容量数据时,通过把数据分割成多块,能够让浏览器逐步显示页面。这种把实体主 体分块的功能称为分块传输编码(Chunked Transfer Coding)。
每次请求都要建立连接吗?
在最早的 http 协议中,每进行一次 http 通信,就需要做一次 tcp 的连接。而一次连接需要进 行 3 次握手,这种通信方式会增加通信量的开销。
所以在 HTTP/1.1 中改用了持久连接,就是在一次连接建立之后,只要客户端或者服务端没有 明确提出断开连接,那么这个 tcp 连接会一直保持连接状态 持久连接的一个最大的好处是:大大减少了连接的建立以及关闭时延。 HTTP1.1 中有一个 Transport 段。会携带一个 Connection:Keep-Alive,表示希望将此条连接 作为持久连接。
HTTP/1.1 持久连接在默认情况下是激活的,除非特别指明,否则 HTTP/1.1 假定所有的连接都 是持久的,要在事务处理结束之后将连接关闭,HTTP/1.1 应用程序必须向报文中显示地添加 一个 Connection:close 首部。
HTTP1.1 客户端加载在收到响应后,除非响应中包含了 Connection:close 首部,不然 HTTP/1.1 连接就仍然维持在打开状态。但是,客户端和服务器仍然可以随时关闭空闲的连接。不发送 Connection:close 并不意味这服务器承诺永远将连接保持在打开状态。
管道化连接: http/1.1 允许在持久连接上使用请求管道。以前发送请求后需等待并收到响应, 才能发送下一个请求。管线化技术出现后,不用等待响应亦可直接发送下一个请求。这样就 能够做到同时并行发送多个请求,而不需要一个接一个地等待响应了。
HTTP/1.1 持久连接在默认情况下是激活的,除非特别指明,否则 HTTP/1.1 假定所有的连接都 是持久的,要在事务处理结束之后将连接关闭,HTTP/1.1 应用程序必须向报文中显示地添加 一个 Connection:close 首部。
HTTP1.1 客户端加载在收到响应后,除非响应中包含了 Connection:close 首部,不然 HTTP/1.1 连接就仍然维持在打开状态。但是,客户端和服务器仍然可以随时关闭空闲的连接。不发送 Connection:close 并不意味这服务器承诺永远将连接保持在打开状态。
管道化连接: http/1.1 允许在持久连接上使用请求管道。以前发送请求后需等待并收到响应, 才能发送下一个请求。管线化技术出现后,不用等待响应亦可直接发送下一个请求。这样就 能够做到同时并行发送多个请求,而不需要一个接一个地等待响应了。
Http 协议的特点
Http 无状态协议
HTTP 协议是无状态的,什么是无状态呢?就是说 HTTP 协议本身不会对请求和响应之间的 通信状态做保存。 但是现在的应用都是有状态的,如果是无状态,那这些应用基本没人用,你想想,访问一个 电商网站,先登录,然后去选购商品,当点击一个商品加入购物车以后又提示你登录。这种 用户体验根本不会有人去使用。那我们是如何实现带状态的协议呢?
客户端支持的 cookie
Http 协议中引入了 cookie 技术,用来解决 http 协议无状态的问题。通过在请求和响应报文 中写入 Cookie 信息来控制客户端的状态;Cookie 会根据从服务器端发送的响应报文内的一 个叫做 Set-Cookie 的首部字段信息,通知客户端保存 Cookie。当下次客户端再往该服务器 发送请求时,客户端会自动在请求报文中加入 Cookie 值后发送出去。
服务端支持的 session
服务端是通过什么方式来保存状态的呢? 在基于 tomcat 这类的 jsp/servlet 容器中,会提供 session 这样的机制来保存服务端的对象状态,服务器使用一种类似于散列表的结构来保存信 息,当程序需要为某个客户端的请求创建一个 session 的时候,服务器首先检查这个客户端 的请求是否包含了一个 session 标识- session id; 如果已包含一个 session id 则说明以前已经为客户端创建过 session,服务器就按照 session id 把这个 session 检索出来使用(如果检索不到,会新建一个); 如果客户端请求不包含 sessionid,则为此客户端创建一个 session 并且生成一个与此 session 相关联的 session id, session id 的值是一个既不会重复,又不容易被找到规律的仿造字符 串,这个 session id 将会返回给客户端保存
HTTP与TCP的区别
长连接/短连接,单工、半双工和全双工
1、长连接与短连接
所谓长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持。
短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接,一般银行都使用短连接。
比如http的,只是连接、请求、关闭,过程时间较短,服务器若是一段时间内没有收到请求即可关闭连接。
其实长连接是相对于通常的短连接而说的,也就是长时间保持客户端与服务端的连接状态。
比如http的,只是连接、请求、关闭,过程时间较短,服务器若是一段时间内没有收到请求即可关闭连接。
其实长连接是相对于通常的短连接而说的,也就是长时间保持客户端与服务端的连接状态。
长连接与短连接的操作过程
通常的短连接操作步骤是:
连接→数据传输→关闭连接;
而长连接通常就是:
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;
这就要求长连接在没有数据通信时,定时发送数据包(心跳),以维持连接状态,短连接在没有数据传输时直接关闭就行了
什么时候用长连接,短连接?
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。
总之,长连接和短连接的选择要视情况而定。
连接→数据传输→关闭连接;
而长连接通常就是:
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;
这就要求长连接在没有数据通信时,定时发送数据包(心跳),以维持连接状态,短连接在没有数据传输时直接关闭就行了
什么时候用长连接,短连接?
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。
总之,长连接和短连接的选择要视情况而定。
发送接收方式
1.1、异步
报文发送和接收是分开的,相互独立的,互不影响。这种方式又分两种情况:
(1)异步双工:接收和发送在同一个程序中,由两个不同的子进程分别负责发送和接收
(2)异步单工:接收和发送是用两个不同的程序来完成。
(1)异步双工:接收和发送在同一个程序中,由两个不同的子进程分别负责发送和接收
(2)异步单工:接收和发送是用两个不同的程序来完成。
1.2、同步
报文发送和接收是同步进行,既报文发送后等待接收返回报文。 同步方式一般需要考虑超时问题,即报文发出去后不能无限等待,需要设定超时时间,超过该时间发送方不再等待读返回报文,直接通知超时返回。
在长连接中一般是没有条件能够判断读写什么时候结束,所以必须要加长度报文头。读函数先是读取报文头的长度,再根据这个长度去读相应长度的报文。
在长连接中一般是没有条件能够判断读写什么时候结束,所以必须要加长度报文头。读函数先是读取报文头的长度,再根据这个长度去读相应长度的报文。
2、单工、半双工和全双工
根据通信双方的分工和信号传输方向可将通信分为三种方式:单工、半双工与全双工。
在计算机网络中主要采用双工方式,其中:局域网采用半双工方式,城域网和广域网采用全双年方式。
1. 单工(Simplex)方式:
通信双方设备中发送器与接收器分工明确,只能在由发送器向接收器的单一固定方向上传送数据。采用单工通信的典型发送设备如早期计算机的读卡器,典型的接收设备如打印机。
2. 半双工(Half Duplex)方式
通信双方设备既是发送器,也是接收器,两台设备可以相互传送数据,但某一时刻则只能向一个方向传送数据。例如,步话机是半双工设备,因为在一个时刻只能有一方说话。
3. 全双工(Full Duplex)方式
通信双方设备既是发送器,也是接收器,两台设备可以同时在两个方向上传送数据。例如,电话是全双工设备,因为双方可同时说话。
滑动窗口
netty
选择Netty的理由
NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
而Netty使用启动器封装了这些复杂的操作
而Netty使用启动器封装了这些复杂的操作
NIO处理多条链路线程模型需要自己实现,Netty有高效的Reactor线程模型
IO线程处理多条链路,它的调试和跟踪非常麻烦,特别是生产环境我们无法进行有效的调试和跟踪,往往只能靠一些日志来辅助分析,定位难度很大
处理网络的闪断、客户端的重复接入、客户端的安全认证、消息的编解码、半包读写
功能强大,预置了多种编解码功能,支持多种主流协议;
灵活的TCP参数配置能力
Netty提供了自己的Channel和其子类实现,不受JVM厂家标准限制,扩展性强
Netty为了解决ByteBuffer的缺陷,重写了一个新的数据接口ByteBuf,新增了高级和实用的特性
定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
对于NIO非阻塞Socket通信,JDK并没有提供现成可用的类库简化用户开发。Netty通过JDK的SSLEngine以SslHandler的方式提供对SSL/TLS安全传输的支持,极大的简化了用户的开发工作量
Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时更多的新功能会加入;
Netty应用场景
Http代理服务实现
百万级长连接通信
RPC自定义协议开发
网络事件
典型的网络事件
链路注册、链路激活、链路断开、链路发生异常
接收到请求消息、请求消息接收并处理完毕
发送应答消息
发生用户自定义事件
Netty事件
事实上inbound和outbound事件是Netty自身根据事件在pipeline中的流向抽象出来的术语,事件在pipeline中得到传播和处理,它是事件处理的总入口。
inbound事件
通常由I/O线程触发,例如TCP链路建立事件、链路关闭事件、读事件、异常通知事件等
Inbound 事件是通知事件, 当某件事情已经就绪后, 通知上层。
Inbound事件发起者是unsafe,默认的处理者是 TailContext, 并且其处理方法是空实现
Inbound 事件在 Pipeline 中传输方向是 headContext -> tailContext
outbound事件
通常是由用户主动发起的网络I/O操作,例如用户发起的连接操作、绑定操作、消息发送等操作
Outbound 事件是请求事件(由 Connect 发起一个请求, 并最终由 unsafe 处理这个请求)
Outbound 事件的发起者是 Channel,Outbound 事件的处理者是 unsafe
Outbound 事件在 Pipeline 中的传输方向是 tailContext -> headContext
高性能
IO线程模型
2种fd
listenfd
一般情况只有一个,用来监听一个特定的端口(如8080)
connfd
每个连接都会打开一个connfd,用来收发数据
3种事件
listenfd进行accept阻塞监听,创建一个connfd
用户态内核态之间copy数据。每个connfd对应着2个应用缓冲区:readbuf和writebuf
处理connfd发来的数据,进行业务逻辑处理,准备response到writebuf
客户端请求的5个阶段
read
接收到请求,读取数据
decode
解码数据
compute
业务逻辑处理
encode
返回数据编码
send
发送数据
其中,以read和send阶段IO最为频繁
Reactor
Reactor模型基于事件驱动,来了事件我通知你,你来处理
三种角色
Reactor
事件监听和响应主线程,通过Select监控客户端请求事件,收到事件后进行dispatch分发
如果是连接事件,由acceptor线程接受连接
如果是读写事件,分发给事件绑定函数Handler处理
如果是连接事件,由acceptor线程接受连接
如果是读写事件,分发给事件绑定函数Handler处理
Acceptor
接受客户端新连接,创建新的SocketChannel将其注册到Reactor主线程的selecor上,并创建事件响应函数handler处理后续事件
Handler
事件响应函数,完成channel的读取数据,完成处理业务逻辑后,负责将结果写入channel发送给客户端
可用资源池来管理
可用资源池来管理
三种Reactor线程模型
单Reactor单线程模型
Reactor单主线程所有事件的响应,如果是连接事件分发给Acceptor线程处理,如果是读写事件则Reactor单主线程完成整个Handler处理
理论上一个Reactor主线程可以独立处理所有IO相关的操作,但一个NIO线程同时处理成百上千的链路,会导致该线程负载过重,处理速度将变慢,性能上无法支撑
Redis使用单Reactor单进程的模型
理论上一个Reactor主线程可以独立处理所有IO相关的操作,但一个NIO线程同时处理成百上千的链路,会导致该线程负载过重,处理速度将变慢,性能上无法支撑
Redis使用单Reactor单进程的模型
单Reactor多线程模型
相对于第一种模型来说,Reactor主线程在获取到IO的读写事件之后从channel中read数据,处理业务逻辑交由Worker线程池来处理(decode-compute-encode),handler收到Worker线程响应后通过send将响应结果返回给客户端。这样可以降低Reactor主线程的性能开销,从而更专注的做事件分发工作了,提升整个应用的吞吐。
Reactor单主线程虽然只承担所有事件的监听和响应,不承担业务逻辑处理,只要是单线程可能会存在性能问题
Reactor单主线程虽然只承担所有事件的监听和响应,不承担业务逻辑处理,只要是单线程可能会存在性能问题
主从Reactor多线程模型
比起第二种模型,它是将Reactor分成两部分mainReactor和subReactor
mainReactor
负责监听server socket,用来处理网络IO连接建立操作,将建立的socketChannel指定注册给subReactor
从mainReactor线程池中随机选择一个Reactor线程作为acceptor线程,用于绑定监听端口,接收客户端连接
acceptor线程接收客户端连接请求之后创建新的SocketChannel,将其注册到mainReactor线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作
业务层的链路正式建立,将SocketChannel从主线程池的Reactor线程的多路复用器上摘除,重新注册到subReactor线程池的线程上
创建一个Handler用于下面处理其它读写事件
subReactor
主要做和建立起来的socket做数据交互和事件业务处理操作,通常subReactor个数上可与CPU个数等同
当有新的事件发生时,SubReactor会调用已创建的Handler进行响应
Handler通过read从channel读取数据后,会分发给后面的Worker线程池进行业务处理
Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果返回给Handler进行处理
Handler收到响应结果后通过Send将响应结果返回给Client
Nginx、Swoole、Memcached和Netty都是采用这种实现
Proactor
来了事件我来处理,处理完了我通知你
理论上proactor比Reactor效率要高一些,异步IO能够充分利用DMA特性,让IO操作与计算重叠,但是实现真正的异步IO,操作系统需要做大量的工作
目前Window下通过IOCP实现了真正的异步IO,而在Linux系统下的AIO并不完善,因此在Linux下实现高并发网络编程时都是以Reactor模式为主
多线程
无锁化的串行设计的
NioEventLoop读取到消息之后,直接调用 ChannelPipeline的 fireChannelRead( Object msg),只要用户不主动切换线程,一直会由 NioEventLoop调用到用户的 Handler,期间不进行线程切换。这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优
高效的并发编程
volatile的大量、正确使用
CAS和原子类的广泛使用
线程安全容器的使用
通过读写锁提升并发性能
高性能的序列化框架
Netty默认提供了对 Google Protobuf的支持,用户通过扩展 Netty的编解码接口,可以实现其他的高性能序列化框架,例如 Thrift
零拷贝
Netty的接收和发送 ByteBuffer采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket读写
实现 CompositeByteBuf,它对外将多个 ByteBuf封装成一个 ByteBuf
Netty文件传输类 DefaultFileRegion通过 transferTo方法将文件发送到目标 Channel中
可靠性
网络通信类故障
客户端连接超时
创建连接超时定时任务之后,会由NioEventLoop负责执行。如果已经连接超时,但是服务端仍然没有返回 TCP握手应答,则关闭连接。
设置完连接超时之后, Netty在发起连接的时候,会根据超时时间创建 ScheduledFuture挂载在 Reactor线程上,用于定时监测是否发生连接超时。
Netty的客户端连接超时参数与其他常用的 TCP参数一起配置,使用起来非常方便,上层用户不用关心底层的超时实现机制。这既满足了用户的个性化需求,又实现了故障的分层隔离。
通信对端强制关闭
由于TCP是全双工的,通信双方都需要关闭和释放Socket句柄才不会发生句柄的泄漏,Netty底层已经自动对该故障进行了处理,及时关闭了句柄
场景
连接网络闪断、对方进程突然宕机或者其他非正常关闭链路事件时,TCP链路就会发生异常。
发生编码异常等不可恢复错误时,需要主动关闭连接
链路主动关闭
在实际的NIO编程过程中,经常存在一种误区:认为只要是对方关闭连接,就会发生 IO异常,捕获 IO异常之后再关闭连接即可。实际上,连接的合法关闭不会发生 IO异常,它是一种正常场景,如果遗漏了该场景的判断和处理就会导致连接句柄泄漏
场景
心跳超时,需要主动关闭连接
对于短连接协议,例如HTTP协议,通信双方数据交互完成之后,通常按照双方的约定由服务端关闭连接,客户端获得TCP连接关闭请求之后,关闭自身的 Socket连接,双方正式断开连接
定制I/O故障
客户端的断连重连机制
消息的缓存重发
接口日志中详细记录故障细节
运维相关功能,例如告警、触发邮件/短信等
链路有效性检测
链路有效性检测需要周期性的心跳对链路进行有效性检测,一旦发生问题,可以及时关闭链路,重建TCP连接
心跳检测机制
心跳检测的目的就是确认当前链路可用,对方活着并且能够正常接收和发送消息。做为高可靠的NIO框架, Netty也提供了心跳检测机制,下面我们一起熟悉下心跳的检测原理
三种层面的心跳
TCP层面的心跳检测,即 TCP的 Keep - Alive机制,它的作用域是整个 TCP协议栈
协议层的心跳检测,主要存在于长连接协议中。例如SMPP协议
应用层的心跳检测,它主要由各业务产品通过约定方式定时给对方发送心跳消息实现
心跳机制
Netty的心跳检测实际上是利用了链路空闲检测机制实现
空闲检测机制
读空闲超时
链路持续时间T没有读取到任何消息
写空闲超时
链路持续时间T没有发送任何消息
读写空闲超时
链路持续时间t没有接收或者发送任何消息
两种心跳检测
Ping - Pong型心跳
由通信一方定时发送 Ping消息,对方接收到 Ping消息之后,立即返回 Pong应答消息给对方,属于请求 - 响应型心跳
Ping - Ping型心跳
不区分心跳请求和应答,由通信双方按照约定定时向对方发送心跳 Ping消息,它属于双向心跳
重连机制
为了保证服务端能够有充足的时间释放句柄资源,在首次断连时客户端需要等待INTERVAL时间之后再发起重连,而不是失败后就立即重连
为了保证句柄资源能够及时释放,无论什么场景下的重连失败,客户端都必须保证自身的资源被及时释放
重连失败后,需要打印异常堆栈信息,方便后续的问题定位
当客户端握手成功之后,在链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常状态下反复重连导致句柄资源被耗尽
内存保护机制
链路总数的控制
每条链路都包含接收和发送缓冲区,链路个数太多容易导致内存溢出
单个缓冲区的上限控制
防止非法长度或者消息过大导致内存溢出
消息解码的时候,对消息长度进行判断,如果超过最大容量上限,则抛出解码异常,拒绝分配内存
消息解码的时候,对消息长度进行判断,如果超过最大容量上限,则抛出解码异常,拒绝分配内存
缓冲区内存释放
防止因为缓冲区使用不当导致的内存泄露
为了防止因为用户遗漏导致内存泄漏,Netty在Pipe line的TailHandler中自动对内存进行释放
为了防止因为用户遗漏导致内存泄漏,Netty在Pipe line的TailHandler中自动对内存进行释放
NIO消息发送队列的长度上限控制
Netty的 NIO消息发送队列 ChannelOutboundBuffer并没有容量上限控制,它会随着消息的积压自动扩展,直到达到 0x7fffffff
通过启动项的ChannelOption设置发送队列的长度,或者通过 - D启动参数配置该长度
通过启动项的ChannelOption设置发送队列的长度,或者通过 - D启动参数配置该长度
流量整形
流量整形( Traffic Shaping)是一种主动调整流量输出速率的措施。一个典型应用是基于下游网络结点的 TP指标来控制本地流量的输出
流量整形对流量监管中需要丢弃的报文进行缓存——通常是将它们放入缓冲区或队列内,也称流量整形
流量整形原理是对每次读取到的ByteBuf可写字节数进行计算,获取当前的报文流量,然后与流量整形阈值对比。如果已经达到或者超过了阈值。则计算等待时间delay,将当前的ByteBuf放到定时任务Task中缓存,由定时任务线程池在延迟delay之后继续处理该ByteBuf
流量整形与流控的最大区别在于流控会拒绝消息,流量整形不拒绝和丢弃消息,无论接收量多大,它总能以近似恒定的速度下发消息,跟变压器的原理和功能类似
流量整形有两个作用
防止由于上下游网元性能不均衡导致下游网元被压垮,业务流程中断
防止由于通信模块接收消息过快,后端业务线程处理不及时导致的”撑死”问题
流量整形分类
全局流量整形
它的作用域针对所有的Channel
单链路流量整形
单链路流量整形与全局流量整形的最大区别就是它以单个链路为作用域,可以对不同的链路设置不同的整形策略
优雅停机接口
Java的优雅停机通常通过注册 JDK的 ShutdownHook来实现,当系统接收到退出指令后,首先标记系统处于退出状态,不再接收新的消息,然后将积压的消息处理完,最后调用资源回收接口将资源销毁,最后各线程退出执行
通常优雅退出有个时间限制,例如 30S,如果到达执行时间仍然没有完成退出前的操作,则由监控脚本直接 kill - 9 pid,强制退出
安全性
场景
仅限内部使用的 RPC通信框架
不需要做握手、黑白名单、SSL/TLS等
对第三方开放的通信框架
在企业内网
开放给内部其他模块调用的服务,通常不需要进行安全认证和 SSL / TLS传输
被外部其他模块调用的服务,往往需要利用 IP黑白名单、握手登陆等方式进行安全认证
开放给企业外部第三方应用访问
对于敏感的服务往往需要通过 SSL / TLS进行安全传输
应用层协议的安全性
Netty需要向上层提供通信层的安全传输,也就是需要支持SSL/TLS
接入认证
Netty握手
握手的发起是在客户端和服务端TCP链路建立成功通道激活时,握手消息的接入和安全认证在服务端处理
IP地址黑名单认证机制
安全认证
SSL安全传输
SSL单向认证
SSL双向认证
第三方CA认证
TCP
传输层协议,只负责传输数据,拆包/粘包
TCP三次握手
握手的目的
TCP握手成功,建立连接,链路状态ESTABLISHED
TCP四次挥手
TCP挥手成功,关闭连接,链路状态CLOSED
滑动窗口
如果你说完一句话,我在处理其他事情,没有及时回复你,那你不是要干等着我做完其他事情后,我回复你,你才能说下一句话。数据包的往返时间越长,通信的效率就越低。为解决这个问题,TCP 引入了窗口这个概念
窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。
窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值
窗口大小由接收端决定,接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
私有协议
协议栈消息定义包含两部分
消息头(header)
消息体(body)
数据结构定义
NettyMessage
消息编解码
NettyMessageDecoder
NettyMessageEncoder
握手和安全认证Handler
LoginAuthReqHandler
LoginAuthRespHandler
心跳检测Handler
HeartBeatReqHandler
HeartBeatRespHandler
心跳超时Handler
ReadTimeoutHandler
启动类
Builder模式进行对象初始化;门面模式对各种功能进行封装
ServerBootstrap
创建服务端的辅助启动类ServerBootstrap对象,目的是降低服务端的开发复杂度
Bootstrap
创建客户端辅助启动类Bootstrap对象
常用方法
group(parentGroup,childGroup)
参数配置Reactor线程模型
channel(Channel)
参数配置Channel类型,工厂模式创建Channel实例
服务端配置为NioServerSocketChannel,客户端配置为NioSocketChannel
option()
参数配置parentGroup的Option
handler(ChannelHandler)
参数配置parentGroup中accept线程的Handler
childOption(ChannelOption)
参数配置TCP属性
childAttr(AttributeKey)
参数配置childGroup的Reactor线程Attr
childHandler(ChildChannelHandler)
参数配置childGroup的Reactor线程Handler
bind(ip,port)
服务端监听端口,返回ChannelFuture
connect(ip,port)
客户端连接Tcp服务端,返回ChannelFuture
EventLoopGroup
通过适当的参数配置,就可以支持三种Reactor线程模型,类似于web应用中的tomcat
继承关系
NioEventLoopGroup->EventLoopGroup->EventExcutorGroup->ScheduledExcutorService->ExcutorService
EventLoopGroup实际就是EventLoop的数组
线程模型
服务端启动的时候,创建了两个NioEventLoopGroup,它们实际是两个独立的Reactor线程池。线程池的隔离术,一个用于接收客户端的TCP连接,另一个用于处理I/O相关的读写操作,或者执行系统Task、定时任务Task等。
常用方法
shutdownGracefully()
优雅退出操作,通过该方法方便的关闭各种资源
优雅退出操作,通过该方法方便的关闭各种资源
EventLoop线程
EventLoop的职责是处理所有注册到本线程多路复用器Selector上的Channel的网络IO事件
Selector的轮询操作由绑定的EventLoop线程run方法驱动
用户自定义Task
通过调用 NioEventLoop的 execute(Runnable task)方法实现
当I/O线程和用户线程同时操作网络资源时,为了防止并发操作导致的锁竞争,将用户线程的操作封装成Task放入消息队列中,由I/O线程负责执行,这样就实现了局部无锁化。
定时任务
通过调用 NioEventLoop的 schedule(Runnable command,long delay,TimeUnit unit)方法实现
channel
类似web应用的request&response
Netty提供了自己的Channel和其子类实现
NioServerSocketChannel
NioSocketChannel
常用方法
关联引用方法
pipeline()、Unsafe()
返回ChannelPipeline、Unsafe
返回ChannelPipeline、Unsafe
closeFuture()
返回channel对象中的closeFuture对象
bind()、connect()、read()、write()、flush()、writeAndFlush()
与NIO中Channel对应的方法功能相似
入参可以新增ChannelPromise
返回参数ChannelFuture
config()
获取当前Channel的配置信息,例如CONNECT_ TIMEOUT_ MILLIS
attr()
设置用户自定义的属性,类似于request上的attribute
unsafe接口
封装了java底层的socket操作,作为连接netty和java底层NIO的重要桥梁,channel会初始化unsafe来和socket打交道
ChannelPipeline
Netty的Channel过滤器实现原理与 Servlet、Filter机制一致,它将Channel的数据管道抽象为ChannelPipeline,消息在ChannelPipeline中流动和传递。ChannelPipeline持有I/O事件拦截器 ChannelHandler的链表,由ChannelHandler对I/O事件进行拦截和处理,可以方便地通过新增和删除 ChannelHandler来实现不同的业务逻辑定制,不需要对已有的ChannelHandler进行修改,能够实现对修改封闭和对扩展的支持。
责任链模式,类似于web应用中所有Filter、Servlet行为管理类
ChannelPipeline并不是NIO服务端必需的
本质是一个负责处理网络事件的职责链,网络事件以事件流的形式在ChannelPipeline中流转
ChannelPipeline的事件处理流程
ChannelPipeline的事件处理流程
1.底层的SocketChannel read()方法读取ByteBuf,触发ChannelRead事件,由I/O线程NioEventLoop调用ChannelPipeline的 fireChannelRead(Object msg)方法,将消息(ByteBuf传输到ChannelPipeline中。
2.消息依次被HeadHandler、ChannelHandler1、ChannelHandler2…… TailHandler拦截和处理,在这个过程中,任何ChannelHandler都可以中断当前的流程,结束消息的传递。
3.调用ChannelHandlerContext的write()方法发送消息,消息从TailHandler开始,途经ChannelHandlerN…… ChannelHandler1、 HeadHandler,最终被添加到消息发送缓冲区中等待刷新和发送,在此过程中也可以中断消息的传递,例如当编码失败时,就需要中断流程,构造异常的Future返回。
1.底层的SocketChannel read()方法读取ByteBuf,触发ChannelRead事件,由I/O线程NioEventLoop调用ChannelPipeline的 fireChannelRead(Object msg)方法,将消息(ByteBuf传输到ChannelPipeline中。
2.消息依次被HeadHandler、ChannelHandler1、ChannelHandler2…… TailHandler拦截和处理,在这个过程中,任何ChannelHandler都可以中断当前的流程,结束消息的传递。
3.调用ChannelHandlerContext的write()方法发送消息,消息从TailHandler开始,途经ChannelHandlerN…… ChannelHandler1、 HeadHandler,最终被添加到消息发送缓冲区中等待刷新和发送,在此过程中也可以中断消息的传递,例如当编码失败时,就需要中断流程,构造异常的Future返回。
用户自定义ChannelHandler会插入到head和tail之间
如果是ChannelInboundHandler的回调,根据插入的顺序从左向右进行链式调用,ChannelOutboundHandler则相反
常用方法
获取关联引用方法
channel()、context()
返回Channel、ChannelHandlerContext
返回Channel、ChannelHandlerContext
触发outbound事件方法
ChannelPipeline拥有channel所有方法
触发Inbound事件方法
fireChannelActive()、fireChannelRead()、fireChannelRegistered()
返回ChannelPipeline
返回ChannelPipeline
addLast、addFirst、addBefore、addAfter((EventExecutorGroup,Name,ChannelHandler))
ChannelHandlerContext
类似于web应用中的配置类ServletContext,所有servlet都共享这个对象。所以一个责任链(ChannelPipeline)上所有的ChannelHandler都共用这个对象
默认处理者
常用方法
关联引用方法
channel()、handler()、pipeline()、executor()
返回Channel、ChannelHandler、ChannelPipeline、EventExecutor
返回Channel、ChannelHandler、ChannelPipeline、EventExecutor
触发outbound事件方法
ChannelHandlerContext拥有channel所有方法
触发Inbound事件方法
fireChannelActive()、fireChannelRead()、fireChannelRegistered()
返回ChannelHandlerContext
返回ChannelHandlerContext
attr()
ChannelHandlerContext的自定义属性
ChannelHandler
对于大多数的ChannelHandler会选择性地拦截和处理某个或者某些事件,其他的事件会忽略,由下一个ChannelHandler进行拦截和处理。这就会导致一个问题:用户 ChannelHandler必须要实现 ChannelHandler的所有接口,包括它不关心的那些事件处理接口。为了解决这个问题, Netty提供了 ChannelHandlerAdapter基类,它的所有接口实现都是事件透传,如果用户 ChannelHandler关心某个事件,只需要覆盖ChannelHandlerAdapter对应的方法即可,对于不关心的,可以直接继承使用父类的方法
IO事件的处理类,类似于web应用中单个Filter、Servlet配置中的实际逻辑处理类
@Sharable
该注解表示一个ChannelHandler可以属于多个ChannelPipeline,保证线程安全
@Skip
被Skip注解的方法不会被调用,直接被忽略,直接跳到下一个ChannelHandler中执行对应的方法
子类
ChannelHandlerAdapter
选择性地拦截和处理某个或者某些事件
ChannelInitializer
初始化Server端新接入的SocketChannel对象
initChannel()
Channel被注册到EventLoop的时候会被调用
Channel被注册到EventLoop的时候会被调用
ChannelInboundHandler
ChannelInboundHandlerAdapter
处理read()输入数据
处理read()输入数据
ChannelOutboundHandler
ChannelOutboundHandlerAdapter
处理write()输出数据
常用方法
生命周期方法
channelRegistered
当前channel注册到EventLoop
channelUnregistered
当前channel从EventLoop取消注册
channelActive
当前channel激活的时候
channelInactive
当前channel不活跃的时候,也就是当前channel到了它生命周期末
数据处理方法
channelRead
当前channel从远端读取到数据
channelReadComplete
channel read消费完读取的数据的时候被触发
userEventTriggered
用户事件触发的时候
channelWritabilityChanged
channel的写状态变化的时候触发
exceptionCaught
当发生异常时,关闭ChannelHandlerContext,释放和ChannelHandlerContext相关联的句柄等资源
预置的ChannelHandler
ByteToMessageCodec
系统编解码框架
LengthFieldBasedFrameDecoder
通用基于长度的半包解码器
LoggingHandler
码流日志打印
SslHandler
SSL安全认证
IdleStateHandler
链路空闲检测
ChannelTrafficShapingHandler
流量整形
Base64Decoder和Base64Encoder
Base64编解码
HashedWheelTimer
一种时间轮管理 Timeout 事件的方式。其设计非常巧妙,并且类似时钟的运行
AttributeMap
AttributeMap和Map的格式很像,key是AttributeKey,value是Attribute,我们可以根据AttributeKey找到对应的Attribute
Channel上的AttributeMap就是大家共享并且线程安全,每一个ChannelHandler都能获取到
ChannelHandlerContext上的AttributeMap是每个ChannelHandler独有的
AttributeKey
Attribute
set(value)
get()
get()
ChannelOption
内存配置&线程池配置&TCP参数配置
SO_BACKLOG
对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列
服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小。默认大小100
CONNECT_TIMEOUT_MILLIS
客户端连接超时设置,默认值30000毫秒即30秒
SO_SNDBUF和SO_RCVBUF
对应于套接字选项中的SO_SNDBUF和SO_RCVBUF,这两个参数用于操作接收缓冲区和发送缓冲区的大小
接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功
SO_KEEPALIVE
对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。
当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
TCP_NODELAY
对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关
Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时
TCP_NODELAY参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送
SO_REUSEADDR
对应于socket选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口
如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口
SO_LINGER
对应于套接字选项中的SO_LINGER,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送
Linux内核默认的处理方式是当用户调用close方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证
会发生剩余的数据,造成了数据的不确定性
会发生剩余的数据,造成了数据的不确定性
ChannelFuture
Netty中所有的I/O操作都是异步的。这意味着任何的I/O调用都将立即返回ChannelFuture
当一个I/O操作开始的时候,一个新的future对象就会被创建
常用方法
await()、sync()
调用者线程会阻塞,直到操作完成
sync()内部调用了await()
sync()内部调用了await()
get()
获取线程处理结果
channel()
cannel()
子主题
addListener(GenericFutureListener)
removeListener(GenericFutureListener)
ChannelPromise
Promise模式就是一种回调模式
JDK非增强的Future并没有回调处理的模式,Netty通过Promise对Future进行扩展,用于设置I/O操作的异步回调处理
实现类
DefaultPromise
sync()、await()
调用者线程会阻塞,直到操作完成
sync()内部调用了await()
setSuccess()、setFailure()
trySuccess()、tryFailure()
调用者线程会阻塞,直到操作完成
sync()内部调用了await()
setSuccess()、setFailure()
trySuccess()、tryFailure()
ByteBuf
ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当我们对ByteBuffer进行 put操作的时候,如果缓冲区剩余可写空间不够,就会发生 BufferOverflowException异常。
ByteBuf进行 put操作的时候会对剩余可用空间进行校验。如果剩余空间不足,需要重新创建一个新的 ByteBuffer,并将之前的 ByteBuffer复制到新创建的 ByteBuffer中,最后释放老的 ByteBuffer,
ByteBuffer只有一个标识位控的指针position,读写的时候需要手工调用flip()
ByteBuf通过两个位置指针来协助缓冲区的读写操作,读操作使用 readerIndex,写操作使用 writerIndex。随着数据的写入writerIndex会增加,读取数据会使 readerIndex增加,但是它不会超过 writerIndex。
《netty权威指南》
第2章
传统的同步阻塞式I/O编程
基于NIO的非阻塞编程
缓冲区buffer
概念
buffer是一个对象,它包含一些要写入或者要读出的数据。在原I/O中,可以将数据直接写入或者将数据直接读到stream对象中,在NIO库中,所有的数据都是用缓冲区处理的,缓冲区实质上是一个数组。
通道channel
channel是一个通道,网络数据通过channel读取和写入,通道和流不一样的是通道是双向的,流只是一个方向上移动。因为channel是全双工的,所以它可以比流更好地隐射底层操作系统的API,特别是在unix网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。
多路复用器selector
它是nio编程的基础,熟练掌握selector对于nio编程至关重要,多路复用器提供选择已经就绪的任务的能力,简单来讲,selector会不断轮询注册在其上的channel,如果某个channel上面发生了读或者写事件,这个channel就处于就绪状态,会被selector轮询出来,然后通过selectionkey可以获取就绪channel的集合,进行后续的I/O操作。一个多路复用器selector可以同时轮询多个channel,由于jdk使用了epoll()代替传统的select实现,所有它并没有最大连接句柄1024/2048的限制,这也就意味着一个线程负责selector的轮询,可以接入成千上万的客户端。
基于nio2.0的异步非阻塞(AIO)编程
为什么要使用NIO编程
为什么选择Netty
IO模型
select
基本原理:select函数监视的文件描述符分3类:分别是writefds,readfds,exceptfds,调用select函数会阻塞,直到有描述符就绪(有数据可读,可写或者有except),或者超时(timeout指定等待时间,如果立即返回设为nul即可),函数返回,当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
缺点1:select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,32位机默认是1024个,64位默认是2048,cat/proc/sys/fs/file-max
缺点2:对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低
缺点3:需要维护一个用来存放大量fd的数据结构,这样会使的用户空间和内核在传递该结构时复制开销大
poll
基本原理:poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,知道设备就绪或者主动超时,被唤醒后它又要再次遍历fd,这个过程经历了多次无谓的遍历,它没有最大连接数的限制,原因是它是基于链表来存储的。
缺点1:大量的fd的数组被整体复制于用户态和内核地址之间,而不管这样的复制是不是有意义
epoll
epoll更加灵活,没有描述符限制,epoll使用一个文件描述符管理多个描述符,将yoghurt关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次
基本原理:epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便是可以收到通知
epoll的优点:1 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听到10个端口)
优点2:效率提升,不是轮询的方式,不会随着FD的数目增加效率下降,只有活跃可用的FD才会调用callback函数,即epoll最大的优点在于它只管你活跃的连接,而跟连接总数无关
内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递,即epoll使用mmap减少复制开销
操作系统
linux基础
磁盘存储结构
常见目录
物理设备的命名规则
文件和目录的基本概念
Linux 的目录结构
Linux 系统的优势
多用户多任务的系统 安全性更高 开源免费
Linux系统的特点
将所有的目录和文件数据组织为一个树型的目录结构,整个系统中 只存在一个根目录,所有的分区、目录、文件都在同一个根目录下面。
命令提示符
[root@localhost~]#
root:当前登录的用户名
localhost:本机的主机名
~:用户当前所在目录
~代表家目录 root 用户的家目录是:/root ;普通用户的家目录是:/home
#:当前登录的用户为管理员用户
$:当前登录的用户为普通用户
用户
su 用户名 : 切换用户
由 root 用户切换到普通用户不需要输入密码 由普通用户切换到其他用户就必须输入密码
users 查看当前有哪些用户登录
exit :退出当前用户登录
tty :查看当前登录的终端
who :查看登录用户的详细用户
显示当前目录所在的完整路径
pwd
目录切换命令
根目录:/ 家目录:普通用户的家目录在/home 下面,root 的家目录是:/root
cd
cd /etc 切换到/etc/目录
cd .. 返回上一级目录
cd 返回家目录
cd .. 返回上一级目录
cd 返回家目录
绝对路径和相对路径
相对路径:是当前目录下的某个文件或路径
绝对路径:是指目录或文件的完整路径
绝对路径:是指目录或文件的完整路径
命令行编辑的几个辅助操作
Tab 键:自动补齐
分号“;”:在同一行中输入多个命令,中间用;隔开
反斜杠“\”:在一行命令后加上\,表示另起一行继续输入
上下方向键:找出曾经执行过的历史命令
history:查看历史命令
Ctrl+U:清空至行首
Ctrl+K:清空至行尾
Ctrl+L:清屏
Ctrl+C:终止命令的执行
分号“;”:在同一行中输入多个命令,中间用;隔开
反斜杠“\”:在一行命令后加上\,表示另起一行继续输入
上下方向键:找出曾经执行过的历史命令
history:查看历史命令
Ctrl+U:清空至行首
Ctrl+K:清空至行尾
Ctrl+L:清屏
Ctrl+C:终止命令的执行
关机命令:shutdown
[-t] 在改变到其它 runlevel 之前﹐告诉 init 多久以后关机。
[-r] 重启计算器。
[-k] 并不真正关机﹐只是送警告信号给每位登录者〔login〕
[-h] 关机后关闭电源〔halt〕。
[-n] 不用 init﹐而是自己来关机。不鼓励使用这个选项﹐而且该选项所产生的后 果往往不总是你所预期得到的。
[-c] cancel current process 取消目前正在执行的关机程序。所以这个选项当然 没有时间参数﹐但是可以输入一个用来解释的讯息﹐而这信息将会送到每位使用者。
[-f] 在重启计算器〔reboot〕时忽略 fsck。
[-F] 在重启计算器〔reboot〕时强迫 fsck。
[-time] 设定关机〔shutdown〕前的时间
[-r] 重启计算器。
[-k] 并不真正关机﹐只是送警告信号给每位登录者〔login〕
[-h] 关机后关闭电源〔halt〕。
[-n] 不用 init﹐而是自己来关机。不鼓励使用这个选项﹐而且该选项所产生的后 果往往不总是你所预期得到的。
[-c] cancel current process 取消目前正在执行的关机程序。所以这个选项当然 没有时间参数﹐但是可以输入一个用来解释的讯息﹐而这信息将会送到每位使用者。
[-f] 在重启计算器〔reboot〕时忽略 fsck。
[-F] 在重启计算器〔reboot〕时强迫 fsck。
[-time] 设定关机〔shutdown〕前的时间
文件内容查看命令
显示文件的内容
cat
文件名 :一次性显示完成,然后退出
-n 文件名 :一次性显示完成,带行号,然后退出
-n 文件名 :一次性显示完成,带行号,然后退出
more :分页显示,显示完成后退出
less:分页显示,完成后不退出,用pgup,pgon,上下键观看,退出按q键
head :默认查看文件头10行
tail :默认查看文件后10行
文件内容统计 wc
-l 统计行数
-w 统计单词数
-c 统计字节数
-w 统计单词数
-c 统计字节数
查找命令
find 命令:搜索文件与目录
-name 按名称查找,允许使用通配符。
-type 按文件类型查找文件类型包括:普通文件(f)、目录(d)、块设备 文件(b)、字符设备文件(c)
-user 按文件所有者查找,根据文件是否属于某个目标用户进行查找
-size 按文件大小查找,使用“+”、“-”号设置超过或小于指定的大小 作为查找条件。常用的容量单位包括 k(注意是小写)、M、G
-type 按文件类型查找文件类型包括:普通文件(f)、目录(d)、块设备 文件(b)、字符设备文件(c)
-user 按文件所有者查找,根据文件是否属于某个目标用户进行查找
-size 按文件大小查找,使用“+”、“-”号设置超过或小于指定的大小 作为查找条件。常用的容量单位包括 k(注意是小写)、M、G
grep 命令:查找文件内容
-c :计算找到 '搜寻字符串' 的次数
-i :忽略大小写的不同,所以大小写视为相同
-n :顺便输出行号
-v :反向选择,亦即显示出没有 '搜寻字符串' 内容的那一行!
-i :忽略大小写的不同,所以大小写视为相同
-n :顺便输出行号
-v :反向选择,亦即显示出没有 '搜寻字符串' 内容的那一行!
其他辅助命令
查看历史命令:history
history [n] n 为数字,列出最近的 n 条命令
! number 执行第几条命令
!! 执行上一条
! command 从最近的命令查到以 command 开头的命令执行
! number 执行第几条命令
!! 执行上一条
! command 从最近的命令查到以 command 开头的命令执行
.help 命令:只能查看内部命令的帮助信息
举例:查看 pwd 命令的帮助信息 pwd --help 如果使用 help 命令查看外部命令的帮助信息时则会报错 使用--help 选项查以查看外部命令的帮助信息
man 命令:查看命令的帮助手册
使用“↑ ”、“↓ ”方向键滚动文本 使用 Page Up 和 Page Down 键翻页 按 Q 或 q 键退出阅读环境、按“/”键后查找内容
输入输出重定向
输出重定向:>或>>
“>”后面指定的文件如果不存在,将先建立该文件,再保存命令结果到文件中(会覆盖原来额内容)
">”后面指定的文件如果存在,将先清空文件的内容,再保存命令结果到文件中(会覆盖原来额内容)
">>”可以将命令结果重定向并追加到指定文件的末尾保存,而不覆盖文件中原有的内容
输入重定向:<或<<
"<<"可以将命令结果重定向并追加到指定文件的末尾保存,而不覆盖文件中原有的内容
"<"后面指定的文件如果存在,将先清空文件的内容,再保存命令结果到文件中(会覆盖原来额内容)
“<”后面指定的文件如果不存在,将先建立该文件,再保存命令结果到文件中(会覆盖原来额内容)
错误重定向:2>或2>>
管道符 |
管道符用于将“|”左边命令的执行结果作为“|”右边命令的输入。
文件权限
文件属性
第 1 组:文件类型
其中第一个字符代表文件的类别。
- 普通文件 d 目录 l 符号链接 c 字符设备 b 块设备
文件的权限 r:可读 w:可写 x:可执行 -:没有权限
第 2 组:文件件的连接数
第 3 组:文件所有者
第 4 组:文件属组
第 5 组:文件大小,默认单位为字节
第 6 组:文件创建时间
第 7 组:文件名称
其中第一个字符代表文件的类别。
- 普通文件 d 目录 l 符号链接 c 字符设备 b 块设备
文件的权限 r:可读 w:可写 x:可执行 -:没有权限
第 2 组:文件件的连接数
第 3 组:文件所有者
第 4 组:文件属组
第 5 组:文件大小,默认单位为字节
第 6 组:文件创建时间
第 7 组:文件名称
修改文件的权限:chmod
字母方式修改权限
chmod u +或-权限 :修改所有者权限
chmod g+或-权限 :修改所属组权限
chmod o +或-权限 :修改其他用户权限
chmod a+或-权限 :修改所有用户权限
chmod g+或-权限 :修改所属组权限
chmod o +或-权限 :修改其他用户权限
chmod a+或-权限 :修改所有用户权限
数字方式修改权限
chom 权限之和
修改文件的所有者与所属组:chown
改所有者 :chown 所有者 文件名
改所属组 :chown :所属组 文件名
同时修改: chown 所有者:所属组 文件名
改所属组 :chown :所属组 文件名
同时修改: chown 所有者:所属组 文件名
子主题
特殊权限
ACL:访问控制
创建ACL规则 setfacl -m u:用户名:权限 文件名
查看ACL规则 :getfacl 文件名
删除一条规则 :setfacl -x u:用户名 文件名
删除所有规则 :setfacl -b 文件名
查看ACL规则 :getfacl 文件名
删除一条规则 :setfacl -x u:用户名 文件名
删除所有规则 :setfacl -b 文件名
SBIT:粘滞位权限(强制位权限)
chmod o+t 文件名 :针对其他用户,只能删除自己的文件,不能删除其他的用户的文件
SGID:
chmod g+s 文件名 ::针对其他用户设置,以所属组的身份去执行
SUID
chmod u+s 文件名 : 以文件所有者的身份执行文件,只针对可执行文件。
VSFTP服务器
FTP:文件传输协议
工作模式[客户端/服务器],FTP服务的端口号:tcp/21、tcp/20
FTP主动模式
客户端从一个任意的端口N(N>1024)连接到FTP服务器的port 21命令端口,客户端开始监听端口N+1,并发送FTP命令“port N+1”到FTP服务器,FTP服务器以数据端口(20)连接到客户端指定的数据端口(N+1)
FTP被动模式
客户端从一个任意的端口N(N>1024)连接到FTP服务器的port 21命令端口,客户端开始监听端口N+1,客户端提交 PASV命令,服务器会开启一个任意的端口(P >1024),并发送PORT P命令给客户端。客户端发起从本地端口N+1到服务器的端口P的连接用来传送数据
FTP服务器简介
FTP(File Transfer Protocol):文件传输协议,是一个客户机/服务器系统。使用FTP的用户需要经过验证后才能登录,FTP服务器的用户可分成3类。
系统用户:即系统本机的用户。Linux一般不会针对实体用户进行限制,因此实体用户可以针对整个文件
系统进行工作。但通常不希望他们通过FTP方式远程访问系统。
虚拟用户:只能采用FTP方式使用系统的用户,不能直接使用Shell登录系统,即虚拟用户,访问服务器
时需要验证。大多数FTP用户是这类用户。
匿名用户:对于公共性质的服务器可以提供匿名用户访问,用户名:anonymous。但在使用匿名用户时,
应对其进行尽可能多的限制,权限较低,如:同时连接的用户数量受限,访问的文件数目受限,
不能上传文件,允许操作的指令较少,设置匿用户同时登入的最大联机数量等
系统用户:即系统本机的用户。Linux一般不会针对实体用户进行限制,因此实体用户可以针对整个文件
系统进行工作。但通常不希望他们通过FTP方式远程访问系统。
虚拟用户:只能采用FTP方式使用系统的用户,不能直接使用Shell登录系统,即虚拟用户,访问服务器
时需要验证。大多数FTP用户是这类用户。
匿名用户:对于公共性质的服务器可以提供匿名用户访问,用户名:anonymous。但在使用匿名用户时,
应对其进行尽可能多的限制,权限较低,如:同时连接的用户数量受限,访问的文件数目受限,
不能上传文件,允许操作的指令较少,设置匿用户同时登入的最大联机数量等
FTP服务器的安装
使用默认yum源安装vsftpd软件包
#yum install vsftpd ftp
2、启动服务并设置开机自启
#systemctl start vsftpd.service 启动服务
#systemctl enable vsftpd.service 开机自启
#systemctl start vsftpd.service 启动服务
#systemctl enable vsftpd.service 开机自启
设置防火墙与selinux机制
#firewall-cmd --permanent --zone=public --add-service=ftp
#firewall-cmd --reload
#systemctl stop firewalld
#setenforce 0
#firewall-cmd --reload
#systemctl stop firewalld
#setenforce 0
访问ftp服务器
Linux系统中使用ftp命令进行访问:
在windows客户端访问ftp服务器:软件
在windows客户端访问ftp服务器:软件
三种用户访问ftp
匿名用户访问FTP
__outline__设置匿名用户访问ftp的默认目录
__outline__设置匿名用户访问ftp的默认目录
anonymous_enable=YES
设置匿名用户登录ftp时不输入密码
no_anon_password=YES
ftpd_banner= welcome to our home! //设置提示内容
ftpd_banner= welcome to our home! //设置提示内容
设置匿名用户上传、新建、删除文件
anon_upload_enable=YES 匿名用户上传权限
anon_mkdir_write_enable=YES 匿名用户新建文件夹的权限
anon_other_write_enable=yes 匿名用户删除和修改文件的权限
anon_mkdir_write_enable=YES 匿名用户新建文件夹的权限
anon_other_write_enable=yes 匿名用户删除和修改文件的权限
设置文件权限
chmod 777 pub 不推荐使用
setfacl -m u:ftp:rwx pub 推荐使用
setfacl -m u:ftp:rwx pub 推荐使用
设置匿名用户上传或新建文件的权限
umask值:决定了新建文件或文件夹时的默认权限
umask值的表示方法:0022
如果使用vsftp的是本地用户,则要修改配置文件中的 local_umask 的值;
如果使用vsftp的是虚拟用户,则要修改配置文件中的 anon_umask 的值。
umask = 022 时,新建的目录 权限是755,文件的权限是 644;
umask = 077 时,新建的目录 权限是700,文件的权限时 600
umask值的表示方法:0022
如果使用vsftp的是本地用户,则要修改配置文件中的 local_umask 的值;
如果使用vsftp的是虚拟用户,则要修改配置文件中的 anon_umask 的值。
umask = 022 时,新建的目录 权限是755,文件的权限是 644;
umask = 077 时,新建的目录 权限是700,文件的权限时 600
设置不允许系统用户登录,只能匿名用户登录
anonymous_enable=YES
local_enable=no
local_enable=no
设置匿名用户访问ftp的默认目录
anon_root=/var/ftp
anon_root=/var/public
anon_root=/var/public
配置系统用户访问FTP
系统用户默认访问主目录,具有上传、下载、新建、删除的权限,还可以切换到的系统其他目录
anonymous_enable=no 关闭匿名用户访问
local_enable=yes 允许系统用户访问
local_umask=022
local_enable=yes 允许系统用户访问
local_umask=022
设置系统用户只能访问自己的主目录并具有所有权限,不允许切换到系统的其他目录
chroot_local_user=YES 控制用户访问路径访问自己的主目录,不能切换到其他目录
allow_writeable_chroot=YES 允许写入
ftpd_banner= welcome to our home!
max_clients=30 设置最大连接数
idle_session_timeout=600 设置超时时间
allow_writeable_chroot=YES 允许写入
ftpd_banner= welcome to our home!
max_clients=30 设置最大连接数
idle_session_timeout=600 设置超时时间
设置系统用户访问指定目录,不允许切换到系统的其他目录,并具上传、下载、新建、删除的权限
local_root=/mnt/public/ 设置系统用户访问ftp的默认目录
write_enable=YES 允许写入
chroot_local_user=YES 控制用户访问路径访问指定目录,不能切换到其他目录
allow_writeable_chroot=YES 允许写入
修改/mnt/public/目录下的文件权限
write_enable=YES 允许写入
chroot_local_user=YES 控制用户访问路径访问指定目录,不能切换到其他目录
allow_writeable_chroot=YES 允许写入
修改/mnt/public/目录下的文件权限
基于本地用户的访问控制
默认情况下,ftp服务器中所有系统用户都可以访问ftp,如何来限定只有指定的系统用户可以访问呢?vsftp中提供了两个与系统用户相关的配置文件:
/etc/vsftpd/ftpusers 提供了一份用于禁止登录的ftp用户列表(黑名单)。
此文件中包含的用户将被禁止登录vsftpd服务器,不管该用户是否在
/etc/vfsftpd/user_list中出现。
/etc/vsftpd/user_list 提供了一份用于允许登录的ftp用户列表(白名单),
此文件中包含的用户可能被禁止登录,可能被允许登录。
具体在主配置文件vsftpd.conf中决定:
当存在userlist_enable=YES时,user_list文件生效
/etc/vsftpd/ftpusers 提供了一份用于禁止登录的ftp用户列表(黑名单)。
此文件中包含的用户将被禁止登录vsftpd服务器,不管该用户是否在
/etc/vfsftpd/user_list中出现。
/etc/vsftpd/user_list 提供了一份用于允许登录的ftp用户列表(白名单),
此文件中包含的用户可能被禁止登录,可能被允许登录。
具体在主配置文件vsftpd.conf中决定:
当存在userlist_enable=YES时,user_list文件生效
Vsftpd日志管理
Vsftp软件搭建的FTP服务器的日志文件
xferlog_enable=YES 开启FTP服务器记录上传下载的情况
xferlog_std_format=YES 日志格式
xferlog_file=路径 指定日志文件
默认为:/var/log/xferlog
xferlog_std_format=YES 日志格式
xferlog_file=路径 指定日志文件
默认为:/var/log/xferlog
日志文件输出格式
(1)当前时间(为本地时间),格式为:DDD MMM dd hh:mm:ss YYYY
(2)传输时间:传送文件所用时间,单位为秒
(3)远程主机名称/IP:
(4)文件大小:传输文件的大小,单位为byte
(5)文件名:传输文件名,包括路径
(6)传输类型:a--以ASCII传输;b--以二进制文件传输
(7)特殊处理标志:
_:不做任何特殊处理
c:文件是压缩格式
u:文件是非压缩格式
t:文件时tar格式
(8)传输方向:o 从FTP服务器向客户端传输;i 从客户端向FTP服务器传输
(9)访问模式:a 匿名用户;g 来宾用户;r 系统中的用户
(10)用户名
(11)服务名:一般为FTP
(12)认证方式:0 无; 1 RFC931认证
(13)认证用户id:如果使用*,测表示无法获得该id
(14)完成状态:i 传输未完成;c表示传输已完成。
(2)传输时间:传送文件所用时间,单位为秒
(3)远程主机名称/IP:
(4)文件大小:传输文件的大小,单位为byte
(5)文件名:传输文件名,包括路径
(6)传输类型:a--以ASCII传输;b--以二进制文件传输
(7)特殊处理标志:
_:不做任何特殊处理
c:文件是压缩格式
u:文件是非压缩格式
t:文件时tar格式
(8)传输方向:o 从FTP服务器向客户端传输;i 从客户端向FTP服务器传输
(9)访问模式:a 匿名用户;g 来宾用户;r 系统中的用户
(10)用户名
(11)服务名:一般为FTP
(12)认证方式:0 无; 1 RFC931认证
(13)认证用户id:如果使用*,测表示无法获得该id
(14)完成状态:i 传输未完成;c表示传输已完成。
配置虚拟用户访问FTP
安装Vsftpd虚拟用户需要用到的软件及认证模块
#yum install pam* libdb-utils libdb* --skip-broken -y
创建虚拟用户临时文件/etc/vsftpd/ftpusers.txt
用户名密码格式如下:
Techftp 用户名
123456 密码
Techftp 用户名
123456 密码
生成Vsftpd虚拟用户数据库认证文件,设置权限为600
#db_load -T -t hash -f/etc/vsftpd/ftpusers.txt/etc/vsftpd/vsftp_login.db
#chmod 600 /etc/vsftpd/vsftp_login.db
#chmod 600 /etc/vsftpd/vsftp_login.db
配置PAM认证文件:vim /etc/pam.d/vsftpd.vu
auth required pam_userdb.so db=/etc/vsftpd/vsftp_login
account required pam_userdb.so db=/etc/vsftpd/vsftp_login
account required pam_userdb.so db=/etc/vsftpd/vsftp_login
创建一个系统用户,用于虚拟用户映射
#useradd -s /sbin/nologin ftpuser
修改配置文件:#vim /etc/vsftpd/vsftpd.conf
#pam_service_name=vsftpd
pam_service_name=vsftpd.vu
guest_enable=YES 开启系统虚拟用户访问
guest_username=ftpuser 指定系统虚拟用户
user_config_dir=/etc/vsftpd/vsftpd_user_conf 指定虚拟用户的配置文件目录
virtual_use_local_privs=YES 允许虚拟用户访问
pam_service_name=vsftpd.vu
guest_enable=YES 开启系统虚拟用户访问
guest_username=ftpuser 指定系统虚拟用户
user_config_dir=/etc/vsftpd/vsftpd_user_conf 指定虚拟用户的配置文件目录
virtual_use_local_privs=YES 允许虚拟用户访问
分别为虚拟用户创建私有的虚拟目录与独立的配置文件
#mkdir /home/ftpuser/techftp
#mkdir /etc/vsftpd/vsftpd_user_conf
#mkdir /home/ftpuser/netftp
#vim netftp
local_root=/home/ftpuser/netftp
write_enable=YES
virtual_use_local_privs=NO #NO时虚拟用户配置文件生效;YES时默认执行ftpuser权限,虚拟用户配置文件生效
anon_world_readable_only=YES #可浏览目录
anon_upload_enable=YES
anon_mkdir_write_enable=YES
anon_other_write_enable=YES
#mkdir /etc/vsftpd/vsftpd_user_conf
#mkdir /home/ftpuser/netftp
#vim netftp
local_root=/home/ftpuser/netftp
write_enable=YES
virtual_use_local_privs=NO #NO时虚拟用户配置文件生效;YES时默认执行ftpuser权限,虚拟用户配置文件生效
anon_world_readable_only=YES #可浏览目录
anon_upload_enable=YES
anon_mkdir_write_enable=YES
anon_other_write_enable=YES
远程访问及控制
web服务
用户与用户组
用户
超级用户:拥有对系统的最高管理权限,默认是 root 用户。
普通用户:只能对自己目录下的文件进行访问和修改,具有登录系统的权限。
虚拟用户:也叫“伪”用户,这类用户最大的特点是不能登录系统,它们的存在主要是 方便系统管理,满足相应的系统进程对文件属主的要求。
普通用户:只能对自己目录下的文件进行访问和修改,具有登录系统的权限。
虚拟用户:也叫“伪”用户,这类用户最大的特点是不能登录系统,它们的存在主要是 方便系统管理,满足相应的系统进程对文件属主的要求。
新建用户
useradd 用户名
查看系统中有哪些用户
cat /etc/passwd
查看用户属于哪些用户组
groups 用户名
为用户设置密码
passwd 用户名
查看用户是否设置密码
cat /etc/shadow | grep 用户名
将用户加入组
gpaaswd -a 用户名 组名
将用户从组中删除
gpasswd -d 用户名 组名
将用户设置为组长
gpasswd -A 用户名 组名
将用户临时加入到某个用户组中
进入用户 newgrp 用户组
用户配置文件
用户组配置文件
用户配置文件/etc/passwd
用户配置文件/etc/passwd,在 passwd 配置 文件中,从左到右各个字段的含义如下:
用户名:用户登录系统时使用的用户名。
口令:存放加密的口令,被/etc/shadow 文件保护。
用户标识号(UID):系统内部用它来标识用户,每个用户的 UID 都是唯一的。
用户组标识号(GID):系统内部用它来标识用户所属的组,这里的 GID 是主组 GID
。
注释性描述:为了方便管理和记忆该用户而添加的信息。
用户主目录:也称家目录,用户登录系统后所进入的目录。
命令解释器:指示该用户使用的 Shell,CentOS Linux 7 默认的是 bash。如果指定 Shell 为/sbin/nologin,则代表用户无法登录系统。
用户名:用户登录系统时使用的用户名。
口令:存放加密的口令,被/etc/shadow 文件保护。
用户标识号(UID):系统内部用它来标识用户,每个用户的 UID 都是唯一的。
用户组标识号(GID):系统内部用它来标识用户所属的组,这里的 GID 是主组 GID
。
注释性描述:为了方便管理和记忆该用户而添加的信息。
用户主目录:也称家目录,用户登录系统后所进入的目录。
命令解释器:指示该用户使用的 Shell,CentOS Linux 7 默认的是 bash。如果指定 Shell 为/sbin/nologin,则代表用户无法登录系统。
用户密码配置文件/etc/shadow
用户密码配置文件/etc/shadow
用户名:用户账户名
。
密码:用户的加密密码。
最后一次修改的时间:从 1970 年 1 月 1 日起,到用户最后一次更改密码的天数。
最小时间间隔:从 1970 年 1 月 1 日起,到用户可以更改密码的天数,(0 表示随时可以变更)。
最大时间间隔:从 1970 年 1 月 1 日起,到必须更改密码的天数,否则密码将过期,(99999 表示永远不 过期)。
警告时间:在密码过期之前多少天提醒用户更新,默认值是 7 天。
不活动时间:在用户密码过期之后到禁用账户的天数。
失效时间:从 1970 年 1 月 1 日起,到账户被禁用的天数。
标志:保留位。
用户名:用户账户名
。
密码:用户的加密密码。
最后一次修改的时间:从 1970 年 1 月 1 日起,到用户最后一次更改密码的天数。
最小时间间隔:从 1970 年 1 月 1 日起,到用户可以更改密码的天数,(0 表示随时可以变更)。
最大时间间隔:从 1970 年 1 月 1 日起,到必须更改密码的天数,否则密码将过期,(99999 表示永远不 过期)。
警告时间:在密码过期之前多少天提醒用户更新,默认值是 7 天。
不活动时间:在用户密码过期之后到禁用账户的天数。
失效时间:从 1970 年 1 月 1 日起,到账户被禁用的天数。
标志:保留位。
用户组
主要组(主组):每个用户有且只有一个主要组,创建用户时默认创建。
附属组(补充组):用户可以是零个或多个附属组成员。一般用于帮助确保用户具有对 系统中文件及其他资源的访问权限。
附属组(补充组):用户可以是零个或多个附属组成员。一般用于帮助确保用户具有对 系统中文件及其他资源的访问权限。
新建组
groupadd 组名
查看系统中的组名
cat /etc/group
为用户组设置密码
gpasswd 用户组
修改组名
group-n 新组名
连接文件
软连接
ln -s 连接文件名 软连接文件名
硬连接
ln 连接源文件 硬连接文件名
用户管理
用 useradd 命令创建用户
-d 指定用户主目录
-g 指定用户组
-m 若主目录不存在,则创建
-s 指定登录时使用的 Shell 类型,默认为/bin/bash,如果为 /bin/nologin 就是虚拟用户
-c 设置对该账号的注释说明文字
-r 创建系统账号(用户 ID 小于 1000,从 999 起按照递减的顺序创建), 默认不创建对应的主目录
-u 手工指定新用户的 ID 值,该值必须唯一,且大于 999。
-M 不创建主目录
-g 指定用户组
-m 若主目录不存在,则创建
-s 指定登录时使用的 Shell 类型,默认为/bin/bash,如果为 /bin/nologin 就是虚拟用户
-c 设置对该账号的注释说明文字
-r 创建系统账号(用户 ID 小于 1000,从 999 起按照递减的顺序创建), 默认不创建对应的主目录
-u 手工指定新用户的 ID 值,该值必须唯一,且大于 999。
-M 不创建主目录
用 passwd 命令管理用户登录密码
-l 锁定用户密码
-u 解锁用户密码
-S 查询用户密码状态
-d 删除用户密码
-u 解锁用户密码
-S 查询用户密码状态
-d 删除用户密码
用 usermod 命令修改用户属性
-l 修改用户名
-c 修改用户描述信息
-d 修改主目录
-s 修改用户登录的 shell 类型
-c 修改用户描述信息
-d 修改主目录
-s 修改用户登录的 shell 类型
用于修改用户口令有效期限的 chage 命令
-m 密码可更改的最小天数。为零时代表任何时候都可以更改密码
-W 用户密码到期前,提前收到警告信息的天数
-M 密码保持有效的最大天数
-E 账号到期的日期。过了这天,此账号将不可用
-d 上一次更改的日期
-I 停滞时期。如果一个密码已过期这些天,那么此账号将不可用
-l 列出当前的设置。由非特权用户来确定他们的密码或账号何时过期
-W 用户密码到期前,提前收到警告信息的天数
-M 密码保持有效的最大天数
-E 账号到期的日期。过了这天,此账号将不可用
-d 上一次更改的日期
-I 停滞时期。如果一个密码已过期这些天,那么此账号将不可用
-l 列出当前的设置。由非特权用户来确定他们的密码或账号何时过期
修改用户注释信息的 chfn 命令
-f 设置真实姓名。
-h 设置家中的电话号码。
-o 设置办公室的地址。
-p 设置办公室的电话号码。
-h 设置家中的电话号码。
-o 设置办公室的地址。
-p 设置办公室的电话号码。
用户别名
定义别名
alias 别名='命令行'
删除别名
unalias 别名
查看系统别名
unalias 别名
sudo
sudo配置
查看sudo软件包:# rpm -qi sudo
用vim编辑sudoers的模板配置文件:# vim /etc/sudoers
用vim编辑sudoers的模板配置文件:# vim /etc/sudoers
给普通用户配置权限
第一个ALL:多个系统之间部署 sudo 环境时,该ALL代表所有主机。
也可以换成相应的主机名,表示改规则只适用主机名对应的系统
第二个ALL(即括号内的):指出规定的 user 用户能够以何种身份来执行命令。
该ALL表示user用户能够以任何用户的身份执行命令
第三个ALL:表示能执行"命令表",ALL表示用户能够执行系统中的所有命令。
也可以换成相应的主机名,表示改规则只适用主机名对应的系统
第二个ALL(即括号内的):指出规定的 user 用户能够以何种身份来执行命令。
该ALL表示user用户能够以任何用户的身份执行命令
第三个ALL:表示能执行"命令表",ALL表示用户能够执行系统中的所有命令。
用户名 ALL=(ALL) NOPASSWD: ALL
磁盘分区与文件系统挂载
磁盘在Linux系统的表示方法
硬盘的类型: IDE接口 hda hdb .....
hda1 hda2 hda3 .......
SATA接口 SCSI接口 USB
sda sdb
sda1 sda2 sda3 ......
所有磁盘设备及分区都以文件的形式存储在/dev/,但是这些文件不能直接使用,如果要往这些分区内写入数据就需要挂载分区
hda1 hda2 hda3 .......
SATA接口 SCSI接口 USB
sda sdb
sda1 sda2 sda3 ......
所有磁盘设备及分区都以文件的形式存储在/dev/,但是这些文件不能直接使用,如果要往这些分区内写入数据就需要挂载分区
挂载点
所谓的挂载点就是文件系统中存在的一个目录,通常情况下,创建在/mnt目录下,挂载成功后,访问挂载点就是访问新的存储设备。
挂载点应该是空目录,否则原来该挂载点中存在的文件将会被隐藏。而且,挂载点在实施挂载操作之前就应该存在。
挂载点应该是空目录,否则原来该挂载点中存在的文件将会被隐藏。而且,挂载点在实施挂载操作之前就应该存在。
磁盘分区
分区 :fdisk 磁盘绝对路径
格式化磁盘文件系统格式:mkfs -t 文件格式 磁盘绝对路径
扩展分区 是不能格式化的
LVM卷管理
quota磁盘配额管理
网络连接
主机名的配置
使用 hostname 命令临时设置主机名
命令格式:hostname [新主机名]
永久设置主机名
命令格式:hostnamectl set-hostname 新主机名
网卡信息的配置
网卡配置文件
ifcfg-ens33
DEVICE=ens33 //定义该网卡的识别名称。 BOOTPROTO=dhcp //启动该网卡的方式,
dhcp 表示通过 BOOTP 或 DHCP 协议动态取得 IP 地址。
ONBOOT=yes //启动 network 服务时,是否启动该网卡。
TYPE=Ethernet //网卡的类型。 IPADDR=192.168.64.128 //静态方式指定网卡的 IP 地址
NETMASK=255.255.255.0 //定义网卡的子网掩码 MTU=1500 //网卡传输的最大数据包 GATEWAY=192.168.64.254 //网络的默认网关 DNS1=
dhcp 表示通过 BOOTP 或 DHCP 协议动态取得 IP 地址。
ONBOOT=yes //启动 network 服务时,是否启动该网卡。
TYPE=Ethernet //网卡的类型。 IPADDR=192.168.64.128 //静态方式指定网卡的 IP 地址
NETMASK=255.255.255.0 //定义网卡的子网掩码 MTU=1500 //网卡传输的最大数据包 GATEWAY=192.168.64.254 //网络的默认网关 DNS1=
配置网卡信息
直接修改网卡配置文件
ifconfig 命令
①查看网卡信息 命令格式:ifconfig [选项] 无选项:显示当前活动的网卡。
–a :显示系统中所有网卡配置信息。 网卡设备名:显示指定网卡配置信息。
②设置 IP 地址(临时的) 命令格式:ifconfig 网卡设备名 IP 地址 netmask 子网掩码
③ 修改网卡的 MAC 地址(先禁用网卡,修改后重启网卡) 命令格式:ifconfig 网卡设备名 hw ether MAC 地址 (
–a :显示系统中所有网卡配置信息。 网卡设备名:显示指定网卡配置信息。
②设置 IP 地址(临时的) 命令格式:ifconfig 网卡设备名 IP 地址 netmask 子网掩码
③ 修改网卡的 MAC 地址(先禁用网卡,修改后重启网卡) 命令格式:ifconfig 网卡设备名 hw ether MAC 地址 (
网卡常用命令
① ifdown(禁用网卡) 命令格式:ifdown 网卡设备名
② ifup(重启网卡) 命令格式:ifup 网卡设备名
③绑定 IP 和 MAC 地址
② ifup(重启网卡) 命令格式:ifup 网卡设备名
③绑定 IP 和 MAC 地址
常用网络调试命令与故障排查
.ping 命令:测试网络中是否畅通以及网络质量。
-c : 指定向目的主机发送多少个报文。
-s :指定发送报文的大小,以字节为单位。 -W: 设置等待接收回应报文的时间间隔,以秒为单位
-s :指定发送报文的大小,以字节为单位。 -W: 设置等待接收回应报文的时间间隔,以秒为单位
netstat 命令:显示网络连接、路由表、正在监听的端口等信息。
-l : 显示正在监听的服务或端口。
-a : 显示当前主机开放的所有端口
-n : 不进行域名解析。
-p : 显示端口是由哪个进程和程序在监听。
-c : 动态显示网络连接和端口监听信息。 -i : 显示网卡相关信息。
-r : 显示当前主机的路由表信息
-a : 显示当前主机开放的所有端口
-n : 不进行域名解析。
-p : 显示端口是由哪个进程和程序在监听。
-c : 动态显示网络连接和端口监听信息。 -i : 显示网卡相关信息。
-r : 显示当前主机的路由表信息
traceroute 命令:路由跟踪。
命令格式:traceroute 目的 IP 地址
nslookup 命令:检测指定的 DNS 服务器工作是否正常
软件安装与包管理工具的使用
RPM 包管理工具的使用
RPM 工具的使用
RPM 主要有5种基本功能:查询、安装、升级、刷新、卸载
命令格式:rpm [选项] [文件]
选项 说明
-a 显示所有软件包
-q 查询功能
-i 安装指定的软件包,通常和-v,-h 选项结合使用
-e 删除指定的软件包
-f 查询拥有指定文件的软件包
-i 显示软件包的相关信息,通常和
-q 选项结合使用
-l 显示软件包的文件列表
-p 查询待安装的软件包
-R 显示软件包的关联性信息
-s 显示文件状态,通常结合
-l 选项使用
-U 升级指定的软件包
选项 说明
-a 显示所有软件包
-q 查询功能
-i 安装指定的软件包,通常和-v,-h 选项结合使用
-e 删除指定的软件包
-f 查询拥有指定文件的软件包
-i 显示软件包的相关信息,通常和
-q 选项结合使用
-l 显示软件包的文件列表
-p 查询待安装的软件包
-R 显示软件包的关联性信息
-s 显示文件状态,通常结合
-l 选项使用
-U 升级指定的软件包
查询
软件包的查询功能主要由-q选项完成 rpm -q
安装
#rpm -ivh telnet-server-0.17-59.el7.x86_64.rpm
刷新
#rpm -Fvh telnet-server-0.17-59.el7.x86_64.rpm
卸载
#rpm -e telnet-server
常见问题
·在执行#rpm -qa 命令时,如果输出信息过多,不易查找指定信息。 解决办法:使用 rpm -qa | grep ·在查询软件包信息或文件时,想保存执行结果。 解决办法:使用输出重定向 ·“软件名称”和“软件包名称”。 解决办法:安装软件时使用软件包名称,查询与卸载时使用软件名称
·软件包依赖问题 解决办法:可以使用 --nodeps 选项忽略,但安装时建议不用 ·RPM 数据库损坏 解决办法:rpm 数据库存放在/var/lib/rpm 目录下,使用 rpm -rebuilddb 修复 ·软件安装的时间问题 解决办法:安装软件时如果系统提示“warning:clock skew detected” 使用 date -s “2019-5-9” 14:00 修改系统时间 使用 hwclick -w 将更新时间写入 CMOS
·软件包依赖问题 解决办法:可以使用 --nodeps 选项忽略,但安装时建议不用 ·RPM 数据库损坏 解决办法:rpm 数据库存放在/var/lib/rpm 目录下,使用 rpm -rebuilddb 修复 ·软件安装的时间问题 解决办法:安装软件时如果系统提示“warning:clock skew detected” 使用 date -s “2019-5-9” 14:00 修改系统时间 使用 hwclick -w 将更新时间写入 CMOS
使用 RPM 管理 telnet-server 软件包
在服务器端安装 telnet-server 和 xinetd,启动相应的服务。
#rpm -q telnet-server xinetd #mount /dev/sr0 /mnt/cdrom #cd /mnt/cdrom # rpm -ivh telnet-server xinetd # systemctl start telnet.socket xinetd.service # systemctl status telnet.socket xinetd.service
在服务器端开启防火墙(图形界面与命令)
# systemctl status firewalld.service # firewall-cmd --zone=public --add-port=23/tcp --permanent # systemctl restart firewalld.service
在客户端访问(默认不允许 root 用户 telnet 登录)
开始--运行--cmd Telnet 192.168.64.128
YUM 工具的使用
设置本地 YUM 源
(1)挂载光盘到/mnt/cdrom (2)创建自定义 yum 文件:#vim/etc/yum.d/media.repo [media] name=CentOS7 baseurl=file:///mnt/cdrom enabled=1 gpgcheck=0 gpqkey=gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
YUM 工具主要有查询、安装、升级、刷新、卸载软件包等功能。 命令格式:yum [选项] [指令] [软件包]
install package1 [package2][…] 使用 YUM 源安装软件包。 update [package][packge2][…] 使用 YUM 源升级软件包。 check-update 检查 YUM 源中所有可用的升级。 remove|erase package1 [package2][…] 卸载软件包。 list[…]系统中已经安装的以及 YUM 源中所有可用的软件包。 info[…] 查看软件包信息。 clean all 清空所有的缓存信息。 makecache all 生成所有的缓存信息 groupinstall group1 [group2] […] 使用 YUM 源安装组包。 groupremove group1 [group2] […] 卸载组包。
TAR 包管理工具简介
命令格式:tar [主选项+辅助选项] [文件或目录]
选项 说明
-c 创建新的 tar 包
-t 列出 tar 包文件的列表
-x 从 tar 包中释放文件
-r 把备份文件追加到已备份文件的末尾 辅助选项 说明
-f 备份文件或设备,必选项
-v 显示命令执行的详细信息
-z 用 gzip 来压缩/解压缩文件
-j 用 bzip2 来压缩/解压缩文件
-C 指定文件解压后的存放路径
选项 说明
-c 创建新的 tar 包
-t 列出 tar 包文件的列表
-x 从 tar 包中释放文件
-r 把备份文件追加到已备份文件的末尾 辅助选项 说明
-f 备份文件或设备,必选项
-v 显示命令执行的详细信息
-z 用 gzip 来压缩/解压缩文件
-j 用 bzip2 来压缩/解压缩文件
-C 指定文件解压后的存放路径
服务与进程
Linux 系统的运行级别
七种不同的运行级别
运行级别 0:关机模式。poweroff.target 运行级别 1:单用户模式,仅用于 root 用户对系统进行维护时。rescue.target 运行级别 2:多用户模式(没有 NFS)。multi-user.target 运行级别 3:完全多用户模式,即多用户文本界面模式,是标准的运行级别。multi-user.target 运行级别 4:特定运行级别,基本不用的用户模式。multi-user.target 运行级别 5:X11,Linux 系统的图形界面运行级别。graphical.target 运行级别 6:重新启动。reboot.target target 文件保存在:/lib/system/system 目录中。 运行级别的设置由/etc/systemd/system/default.target 文件来控制。 运行级别服务程序存放位置:/etc/rc.d/ 服务进程存放在运行级别对应的目录中:Snnxxxx 或 Knnxxxxx 服务程序的脚本文件存放位置:/etc/rc.d/init.d/
系统运行级别之间的切换
查看系统的当前运行级别:runlevel 查看系统启动时的运行级别:ll /etc/systemd/system/default.target
系统运行级别之间的切换
使用 init 命令进行切换
使用 init 命令进行切换
关闭 Ctrl+Alt+Del 键的功能
字符界面按 Ctrl+Alt+Del 键系统会自动重启,为避免误操作带来的损失,需要禁用此功能
字符界面按 Ctrl+Alt+Del 键系统会自动重启,为避免误操作带来的损失,需要禁用此功能
使用 systemctl 管理服务
服务的启动与停止
命令格式:systemctl 选项 服务名 选项说明: start:启动;stop:停止;restart:重启;status:服务状态 服务名:一般以“.service”结尾,输入服务名时了可省略
设置服务的自启动状态
①使用 ntsysv 命令设置服务的自启动 命令格式:ntsysv [--back][--level] 选项说明:back,在互动界面不显示 cancel;level,在指定的运行级别中设置服务的自启动。 ②使用 systemctl 命令设置服务的自启动 查看服务的自启动状态:systemctl is-enabled [服务名称] 开启服务的自启动:systemctl enable 服务名称 关闭服务的自启动:systemctl disable 服务名称
查看所有的服务
命令格式:systemctl [选项] [–type=TYPE] [–all] 选项说明: (1) list-units:依据 unit 列出所有启动的 unit。加上–all 会列出没启动的 unit; (2) list-unit-files:依据/usr/lib/systemd/system/ 内的启动文件,列出启动文件列表
进程的延迟与周期调度
进程的概念
进程:开始执行但是还没有结束的程序的实例 程序:包含可执行代码的文件 进程由程序产生,是一个运行着的、要占系统资源的程序,进程不等于程序。 进程分为:交互进程、批处理进程、守护进程。
进程的查看:ps 命令
命令语法:ps [选项] 选项说明: -A:显示系统中所有进程的信息,与-e 具有相同的功能。 -a:显示所有用户进程的信息。 -f:完整的显示进程的所有信息。 -l:以长格式显示进程信息。 -r:只显示正在运行的进程。 -u:显示面向用户的格式(包括用户名、CPU 及内存使用情况等信息)。 -x:显示所有非控制终端上的进程信息。 -p:显示由进程 ID 指定的进程的信息。 -t:显示指定终端上的进程的信息。
查看各进程继承关系的命令:pstree
pstree 命令以树状结构显示系统进程的继承关系。树状图将会以 pid (如果有指定) 或是以 init 为根,如果指定 user,则树状结构只显示该用户所拥有的进程。 命令格式:pstree [选项] [pid|user] 选项说明: -a:显示该进程的完整继承关系,如果是被内存置换出去的进程则会加上括号。 -c 如果有重复的进程名, 则分开列出 (默认值是会在前面加上 * )。 pid|user:查看指定根进程(pid)或用户(user)所拥有的进程
进程的终止
kill [信号代码] pid Killall [信号代码] 进程名
进程的调度
①延迟性调试命令:at 功能:指定时间运行指定的程序 格式:at [-f 文件] [-m ] 时间 -f : 指定存放计划执行命令的文件 -m: 作业结束后发送邮件给执行 at 命令的用户 时间:任务执行的时间,可采用相对和绝对两种方法 绝对表示法: MMDDYY 或 MM/DD/YYYY, today 或 tomorrow 来表示今天或明天 相对表示法:now+num+时间单位,其中的时间间隔可以是 minutes, hours, days, weeks ②周期性调度命令 crontab 功能:计划调度服务 crontab –u user {-e | -l | -r} -u:为指定的用户设置计划任务 -e:编辑计划任务列表 -l:查看指定用户的计划任务列表,默认为当前用户 -r:删除用户的计划任务列表 crontab –u user file (其中 file 代表任务列表文件)
samba服务
samba服务器的搭建
首先:在服务端安装samba服务
其次:配置samba服务(共享文件的路径、访问权限)
最后:做测试
其次:配置samba服务(共享文件的路径、访问权限)
最后:做测试
samba具体操作步骤
第一步:准备软件仓库:yum安装(本地源)
配置本地源进行安装:挂载光盘(准备软件仓库)---编辑本地yum源文件
第二步:安装samba服务----yum install samba samba-client
samba :服务端程序
samba-client:客户端程序
samba-client:客户端程序
第三步:配置samba服务
准备共享目录:/mnt/public
配置文件:/etc/samba/smb.conf
配置服务:vim /etc/samba/smb.conf
[company] //共享名称为
company comment = company share //共享注释
path = /home/company //指定共享路径
browseable = yes //所有人可见
guest ok = no //拒绝匿名访问
writeable = yes //支持写入数据
配置文件:/etc/samba/smb.conf
配置服务:vim /etc/samba/smb.conf
[company] //共享名称为
company comment = company share //共享注释
path = /home/company //指定共享路径
browseable = yes //所有人可见
guest ok = no //拒绝匿名访问
writeable = yes //支持写入数据
第四步:启动服务(重启服务)【每次修改完配置文件后都需要重启才生效】
服务名称:smb.service nmb.service
重启服务:systemctl restart smb nmb
重启服务:systemctl restart smb nmb
第五步:关闭防火墙和selinux配置
systemctl stop firewalld 关闭防火墙
setenforce 0 关闭selinux
setenforce 0 关闭selinux
第六步:新建samba用户并设置密码
useradd -s /bin/nologin smbsuer1 新建一个不能登录到系统的用户
mbpasswd -a smbsuer1 将用户设置为samba用户
mbpasswd -a smbsuer1 将用户设置为samba用户
第七步:测试
1.在Linux客户端测试:
查报服务器有哪些共享文件:smbclient -L ens33地址
访问服务器的共享文件:smbclient -U smbsuer1 //ens33地址/共享文件名
访问服务器的共享文件:smbclient -U smbsuer1 //ens33地址/共享文件名
2.在windows客户端测试
在资源管理器中输入:\\Linux地址\共享文件名
补充:访问权限受两个地方影响
文件本身的权限
配置文件中的共享权限:
配置文件中的共享权限:
补充:访问权限受两个地方影响
shell
shell基础
什么是Shell
Shell环境
第一个shell环境
注释
Shell变量
命名
命名只能使用英文字母,数字和下划线,首个字符不能以数字开头
区分大小写
中间不能有空格
不能使用标点符号
不能使用关键字
区分大小写
中间不能有空格
不能使用标点符号
不能使用关键字
赋值
字符串
表达
string='my_str'
string="my_str"
string=my_str
string="my_str"
string=my_str
使用
${#string}
${string:start_index:end_index}
`expr index "$string" search_str`
数组
表达
数组名=(值1 值2 ... 值n)
array_name=(value0 value1 value2 value3)
array_name=(
value0
value1
)
array_name[0]=value0
array_name[1]=value1
array_name[n]=valuen
array_name=(
value0
value1
)
array_name[0]=value0
array_name[1]=value1
array_name[n]=valuen
使用
${my_array[index]}
${my_array[*]}
${my_array[@]}
${my_array[@]}
${#my_array[@]}
${#my_array[*]}
${#my_array[*]}
只读
readonly temp
删除
unset temp
引用
$temp
${temp}
${temp}
类型
用户自定义变量
环境变量
$HOME
$PATH
$PS1
$PS2
$IFS
$0
$#
$$
位置参数变量
$1、$2、...
$*
$@
预定义变量
位置参数变量
$?
显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。
$!
后台运行的最后一个进程的ID号
$-
显示Shell使用的当前选项,与set命令功能相同。
Shell字符串
单引号
双引号
拼接字符串
获取字符串长度
提取子字符串
查找子字符串
Shell数组
定义数组
读取数组
获取数组的长度
运算符
算术运算符
=
a=$b
+
`expr 20 + 10`
-
`expr 20 - 10`
*
`expr 20 * 10`
/
`expr 5 / 2`
%
`expr 5 % 2`
==
`expr 20 == 10`
!=
`expr 20 != 10`
位运算符
<<
5 << 2 = 20
-5 << 2 = -20
-5 << 2 = -20
>>
5 >> 2 = 1
-5 >> 2 = -2
-5 >> 2 = -2
&
5 & 2 = 0
-5 & 2 = 2
-5 & 2 = 2
|
5 | 2 = 7
-5 | 2 = -5
-5 | 2 = -5
^
5 ^ 2 = 7
-5 ^ 2 = -7
-5 ^ 2 = -7
~
~5 = -6
~-5 = 4
~-5 = 4
关系运算符
-eq
[ 20 -eq 10 ]
-ne
[ 20 -ne 10 ]
-gt
[ 20 -gt 10 ]
-ge
[ 20 -ge 10 ]
-lt
[ 20 -lt 10 ]
-le
[ 20 -le 10 ]
布尔运算符
!
非运算,表达式为 true 则返回 false,否则返回 true。
[ ! false ]
返回true。
-o
或运算,有一个表达式为 true 则返回 true。
[ 10 -lt 20 -o 20 -gt 100 ]
返回true。
-a
与运算,两个表达式都为 true 才返回 true。
[ 10 -lt 20 -a 20 -gt 100 ]
返回false。
逻辑运算符
&&
逻辑的AND
[[ 10 -lt 100 && 20 -gt 100 ]]
返回false。
||
逻辑的OR
[[ 10 -lt 100 || 20 -gt 100 ]]
返回true。
字符串运算符
=
检测两个字符串是否相等,相等返回 true。
[ "abc" = "def" ]
返回false。
!=
检测两个字符串是否相等,不相等返回 true。
[ "abc" != "def" ]
返回true。
-z
检测字符串长度是否为0,为0返回 true。
[ -z "abc" ]
返回false。
-n
检测字符串长度是否不为 0,不为 0 返回 true。
[ -n "abc" ]
返回true。
$
检测字符串是否为空,不为空返回 true。
为空:
""
" "
为空:
""
" "
[ $string ]
返回true。
文件测试运算符
-b
检测文件是否是块设备文件,如果是,则返回 true。
[ -b $file ]
-c
检测文件是否是字符设备文件,如果是,则返回 true。
[ -c $file ]
-d
检测文件是否是目录,如果是,则返回 true。
[ -d $file ]
-f
检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。
[ -f $file ]
-k
检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。
[ -k $file ]
-p
检测文件是否是有名管道,如果是,则返回 true。
[ -p $file ]
-u
检测文件是否设置了 SUID 位,如果是,则返回 true。
[ -u $file ]
-g
检测文件是否设置了 SGID 位,如果是,则返回 true。
[ -g $file ]
-r
检测文件是否可读,如果是,则返回 true。
[ -r $file ]
-w
检测文件是否可写,如果是,则返回 true。
[ -w $file ]
-x
检测文件是否可执行,如果是,则返回 true。
[ -x $file ]
-s
检测文件是否为空(文件大小是否大于0),不为空返回 true。
[ -s $file ]
-e
检测文件(包括目录)是否存在,如果是,则返回 true。
[ -e $file ]
-S
判断某文件是否 socket。
[ -S $file ]
-L
检测文件是否存在并且是一个符号链接。
[ -L $file ]
命令
输入
read [OPTIONS] [ARGUMENTS]
read -p "请输入一个数num1=" NUM1
read -t 10 -p "请在10秒内输入一个数num2=" NUM2
read -t 10 -p "请在10秒内输入一个数num2=" NUM2
select
同流程控制中的select
输出
echo [OPTIONS] [ARGUMENTS]
echo "\"A\""
输出结果:
"A"
"A"
echo -e "A\nA"
输出结果:
A
A
A
A
echo -e "A\c"
echo "A"
echo "A"
\c:不换行
输出结果:
AA
输出结果:
AA
printf [FORMAT-STRING] [ARGUMENTS]
printf "%-10s %-8s %-4s\n" A B C
printf '%d %s\n' 1 "abc"
printf %s abcdef
printf %s abc def
printf "%s and %d \n"
检查
test
[ ]
模式匹配
[[ ]]
[[ hello == hell? ]]
返回true。
数学运算
$[ ]
$(( ))
命令替换
变量替换
${ }
${变量#关键词}
若变量内容从**头**开始的数据符合 关键词 ,则将符合的最**短**数据删除。
${变量##关键词}
若变量内容从**头**开始的数据符合 关键词 ,则将符合的最**长**数据删除。
${变量%关键词}
若变量内容从**尾**开始的数据符合 关键词 ,则将符合的最**短**数据删除。
${变量%%关键词}
若变量内容从**尾**开始的数据符合 关键词 ,则将符合的最**长**数据删除。
${变量/旧关键词/新关键词}
若变量内容符合 旧关键词 ,则**第一个**旧的字符串会被新的字符串替换。
${变量//旧关键词/新关键词}
若变量内容符合 旧关键词 ,则**全的**旧的字符串会被新的字符串替换。
路径/文件名
basename [PATHNAME] [SUFFIX]
basename /usr/bin/sort
basename include/stdio.h
basename include/stdio.h .h
dirname [PATHNAME]
dirname /usr/bin/
dirname dir1/str dir2/str
dirname stdio.h
文件包含
. filename
source filename
shell传递参数
特殊参数说明
特殊参数案例一
特殊参数案例二
Shell流程控制
if
if else
if else-if else
for循环
while语句
无限循环
until 循环
case
跳出循环
break命令
continue
Shell关键词
实例一
实例二
printf的转义序列
Shell函数
[ function ] 函数名 [()]
{
{
操作;[return 返回值;]
}
}
my_func(){
echo "hello"
}
my_func
输出结果: hello
function my_func(){
echo "hello"
return 0;
}
my_func
echo $?
输出结果: hello 0
my_add()
{
result=0
for i in $*
do
result=`expr $result + $i`
done
return $result
}
my_add 1 2 3
echo $?
输出结果: 6
echo "hello"
}
my_func
输出结果: hello
function my_func(){
echo "hello"
return 0;
}
my_func
echo $?
输出结果: hello 0
my_add()
{
result=0
for i in $*
do
result=`expr $result + $i`
done
return $result
}
my_add 1 2 3
echo $?
输出结果: 6
CPU
内存
网络
IO
进程
调优
零拷贝
好处
节省了CPU周期,空出的CPU可以完成更多其他的任务
减少了内存区域之间数据拷贝,节省内存带宽
减少用户态和内核态之间数据拷贝,提升数据传输效率
应用零拷贝技术,减少用户态和内核态之间的上下文切换
传统拷贝
原理
sendfile(2次的上下文切换,3次的I/O拷贝)
支持scatter-gather特性的sendfile(2次的上下文切换,2次的I/O拷贝)
mmap
性能优化&故障排查
内核参数优化
JVM优化
自带命令行监测工具
jps
查看java进程ID
jinfo
查看及调整虚拟机参数
jmap
查看堆(heap)使用情况及生产线程快照
jstack
查看线程运行状态及生成线程快照
jstat
显示进程中的类装载,内存,垃圾收集等运行数据
自带可视化监测
jconsole
均可监测本地及远程的java应用,包括堆使用情况,线程使用,cpu使用,类加载情况,gc情况
jvisualvm
还可以生成相应的堆和线程快照,同时还可以使用相应的插件
第三方诊断工具
MAT
eclipse的内存分析插件,通过MAT可以对dump出来的堆快照进行分析,并且辅助分析内存泄露原因,快速的计算出在内存中对象的占用大小,垃圾收集器的回收工作情况,并可以通过报表直观的查看到可能造成这种结果的对象
BTrace
SUN推出的一款JAVA 动态,安全追踪监控工具,可以在不停机的情况下监控系统运行情况,并且做到最少的侵入,占用最少的系统资源
Arthas
阿里开源的在线java诊断工具,同样可以在不停机情况下监控系统,包括内存情况,线程情况,GC情况,运行时数据,也可以监测方法参数,返回值,异常返回等数据。
GCViewer
免费的GC日志图形分析工具,下载jar包直接运行
gceasy
免费的GC日志图形分析工具,web工具,上传GC日志在线使用
网络参数优化
事务优化
数据库优化
连接池优化
内存溢出排查
堆外内存排查
IO排查
高负载排查
开源框架
spring
框架注解
核心注解
@Required
用于Bean的Setter方法上,以指示该Bean组装时必须要有该属性,否则抛出BeanInitializationException
@Autowired
此注释应用于字段,setter方法和构造函数。
当在字段上使用并使用属性名称传递字段的值时,Spring会自动为字段分配传递的值。
当在setter方法上使用时,Spring尝试在该方法上执行by Type自动装配。
在构造函数上使用时,构造函数注入发生在对象创建时。
当加上(required=false)时,就算找不到bean也不报错。
JSR规范中也有相对应的注解:JSR250的@Resource,JSR330的@Inject
当在字段上使用并使用属性名称传递字段的值时,Spring会自动为字段分配传递的值。
当在setter方法上使用时,Spring尝试在该方法上执行by Type自动装配。
在构造函数上使用时,构造函数注入发生在对象创建时。
当加上(required=false)时,就算找不到bean也不报错。
JSR规范中也有相对应的注解:JSR250的@Resource,JSR330的@Inject
@Qualifier
这个注解通常和@Autowired一起使用。
当你想对注入的过程做更多的控制,@Qualifier可以帮助你指定做更详细配置。
一般在两个或者多个Bean是相同的类型,spring在注入的时候会出现混乱,使用该注解可以消除这种混乱。
当你想对注入的过程做更多的控制,@Qualifier可以帮助你指定做更详细配置。
一般在两个或者多个Bean是相同的类型,spring在注入的时候会出现混乱,使用该注解可以消除这种混乱。
@Configuration
等同于spring的XML配置文件;使用Java代码可以检查类型安全。
如果有些第三方库需要用到xml文件,建议仍然通过@Configuration类作为项目的配置主类,
并使用@ImportResource注解加载xml配置文件
如果有些第三方库需要用到xml文件,建议仍然通过@Configuration类作为项目的配置主类,
并使用@ImportResource注解加载xml配置文件
@ComponentScan
与@Configuration注解一起,发现和装配Bean。
可通过basePackageClasses 或basePackage指定扫描的基类或基包。
如果声明的包未定义的话,会从声明该注解的类所在的包及子包开始扫描
可通过basePackageClasses 或basePackage指定扫描的基类或基包。
如果声明的包未定义的话,会从声明该注解的类所在的包及子包开始扫描
@Bean
等价于XML中配置的bean。
放在方法的上面,而不是类,产生一个Bean,并交给Spring容器管理。
放在方法的上面,而不是类,产生一个Bean,并交给Spring容器管理。
@PreDestroy 和@PostConstruct
指示Bean的初始化方法和销毁方法
@Lazy
用在组件类上。Spring默认在启动时自动装载依赖类。使用此注解时,会在第一次请求使用时才初始化该类。
也可与@Configuration一起使用,则所有@Bean注解的方法会延时初始化。
也可与@Configuration一起使用,则所有@Bean注解的方法会延时初始化。
@Value
可用于字段,构造器参数,方法参数,以指示一个默认的值。
支持#{...}和${...}这两种占位符。
注入Spring boot application.properties配置的属性的值
支持#{...}和${...}这两种占位符。
注入Spring boot application.properties配置的属性的值
@Import
用来导入其他配置类
@ImportResource
用来加载xml配置文件
@PropertySource
通过声明方式将属性资源添加到spring环境
原型注解
@Component
可配合CommandLineRunner使用,在程序启动后执行一些基础任务;
当组件不好归类的时候,可以使用这个注解进行标注
当组件不好归类的时候,可以使用这个注解进行标注
@Controller
用于定义控制器类,在spring 项目中由控制器负责将用户发来的URL请求转发到对应的服务接口(service层),
一般这个注解在类中,通常方法需要配合注解@RequestMapping
一般这个注解在类中,通常方法需要配合注解@RequestMapping
@Service
一般用于修饰service层的组件
@Repository
声明该类是一个Repository
SpringBoot注解
@EnableAutoConfiguration
自动配置。尝试根据你添加的jar依赖自动配置你的Spring应用。
例如,如果classpath下存在HSQLDB,并且你没有手动配置任何数据库连接beans,将自动配置一个内存型(in-memory)数据库”。
你可以将@EnableAutoConfiguration或@SpringBootApplication注解添加到一个@Configuration类上来选择自动配置。
如果发现应用了你不想要的特定自动配置类,可使用@EnableAutoConfiguration注解的排除属性来禁用它们
例如,如果classpath下存在HSQLDB,并且你没有手动配置任何数据库连接beans,将自动配置一个内存型(in-memory)数据库”。
你可以将@EnableAutoConfiguration或@SpringBootApplication注解添加到一个@Configuration类上来选择自动配置。
如果发现应用了你不想要的特定自动配置类,可使用@EnableAutoConfiguration注解的排除属性来禁用它们
@SpringBootApplication
包含了@ComponentScan、@Configuration和@EnableAutoConfiguration注解
其中@ComponentScan让spring Boot扫描到Configuration类并把它加入到程序上下文
其中@ComponentScan让spring Boot扫描到Configuration类并把它加入到程序上下文
SpringCloud注解
@EnableConfigServer
配置服务器
@EnableEurekaServer
注册服务器
@EnableDiscoveryClient
发现客户端
@EnableCircuitBreaker
熔断器
@HystrixCommand
服务降级
缓存注解
@Cacheable
方法级
@CachePut
方法级,更新缓存
@CacheEvict
方法级,清除缓存
@CacheConfig
在类级别配置缓存,以避免多次声明
任务执行和调度注解
@Scheduled
方法级,具有该注解的方法应无返回值,且不接受任何参数
@Async
方法级,每个方法均都在单独的线程中,可接受参数;可返回值,也可不返回值。
测试注解
@BootstrapWith
类级别,配置Spring测试上下文框架的启动
@ContextConfiguration
类级别,指定配置文件
@WebAppConfiguration
类级别,指示ApplicationContext加载的集成测试环境应为WebApplicationContext
@Timed
指定测试方法的执行时间,超时失败
@Repeat
运行次数
@Commit
类级和方法级,指示事务的提交
@RollBack
类级和方法级,指示事务的回滚
@DirtiesContext
类级和方法级,指示ApplicationContext已修改,触发重加载以进行后续的测试
支持3种关闭上下文的方式
BEFORE_METHOD
BEFORE_CLASS
BEFORE_EACH_TEST_METHOD
支持3种关闭上下文的方式
BEFORE_METHOD
BEFORE_CLASS
BEFORE_EACH_TEST_METHOD
@BeforeTransaction
测试类的void方法上,指示具有该注解的方法应该在所有@Transactional注解的方法之前执行
@AfterTransaction
测试类的void方法上,指示具有该注解的方法应该在所有@Transactional注解的方法之后执行
@Sql
类级和方法级,运行Sql脚本
方法上的@Sql会覆盖类级别的@Sql
方法上的@Sql会覆盖类级别的@Sql
@SqlConfig
同@Sql一起工作,定义了如何解析和执行SQL脚本的元数据
用于类级时对该类下的所有脚本起作用
用于类级时对该类下的所有脚本起作用
@SqlGroup
方法级,包含多个@Sql
@SpringBootTest
用于启动集成测试上下文
@DataJpaTest
@DataMongoTest
@WebMVCTest
主要用于controller层测试,只覆盖应用程序的controller层,
HTTP请求和响应是Mock出来的,因此不会创建真正的连接。因此需要用@MockBean注解创建所需的Bean进行模拟接口调用。
如果Controller层对Service层中的其他bean有依赖关系,那么需要使用Mock提供所需的依赖项。
WebMvcTest要快得多,因为我们只加载了应用程序的一小部分。
HTTP请求和响应是Mock出来的,因此不会创建真正的连接。因此需要用@MockBean注解创建所需的Bean进行模拟接口调用。
如果Controller层对Service层中的其他bean有依赖关系,那么需要使用Mock提供所需的依赖项。
WebMvcTest要快得多,因为我们只加载了应用程序的一小部分。
@AutoConfigureMockMVC
类似于@WebMVCTest,只不过启动的是整个SpringBoot上下文
@MockBean
创建和注入一个Mockito Mock
@JsonTest
限制SpringBoot的自动化配置,以处理JSON
该注解会自动化配置出一个JacksonTester 或 GsonTester实例
该注解会自动化配置出一个JacksonTester 或 GsonTester实例
@TestPropertySource
类级别,指派测试类的属性源
数据访问注解
@Transactional
用于接口、接口中的方法、类、类中的公有方法
光靠该注解并不足以实现事务
仅是一个元数据,运行时架构会使用它配置具有事务行为的Bean
该注解还支持以下特性:
传播类型
隔离级别
操作超时
只读标记
光靠该注解并不足以实现事务
仅是一个元数据,运行时架构会使用它配置具有事务行为的Bean
该注解还支持以下特性:
传播类型
隔离级别
操作超时
只读标记
SpringMVC和
REST注解
REST注解
@Controller
用于定义控制器类,在spring 项目中由控制器负责将用户发来的URL请求转发到对应的服务接口(service层),
一般这个注解在类中,通常方法需要配合注解@RequestMapping
一般这个注解在类中,通常方法需要配合注解@RequestMapping
@RestController
@Controller和@ResponseBody的合集,表示这是个控制器bean,
并且是将函数的返回值直接填入HTTP响应体中,是REST风格的控制器
并且是将函数的返回值直接填入HTTP响应体中,是REST风格的控制器
@RequestMapping
RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。
用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。
该注解有六个属性:
params:指定request中必须包含某些参数值是,才让该方法处理
headers:指定request中必须包含某些指定的header值,才能让该方法处理请求
value:指定请求的实际地址,指定的地址可以是URI Template 模式
method:指定请求的method类型, GET、POST、PUT、DELETE等
consumes:指定处理请求的提交内容类型(Content-Type),如application/json,text/html;
produces:指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回
用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。
该注解有六个属性:
params:指定request中必须包含某些参数值是,才让该方法处理
headers:指定request中必须包含某些指定的header值,才能让该方法处理请求
value:指定请求的实际地址,指定的地址可以是URI Template 模式
method:指定请求的method类型, GET、POST、PUT、DELETE等
consumes:指定处理请求的提交内容类型(Content-Type),如application/json,text/html;
produces:指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回
@RequestMapping变体
@GetMapping
等于@RequestMapping(method = RequestMethod.GET)
@PostMapping
等于@RequestMapping(method = RequestMethod.POST)
@PutMapping
等于@RequestMapping(method = RequestMethod.PUT)
@PatchMapping
等于@RequestMapping(method = RequestMethod.PATCH)
@DeleteMapping
等于@RequestMapping(method = RequestMethod.DELETE)
等于@RequestMapping(method = RequestMethod.GET)
@PostMapping
等于@RequestMapping(method = RequestMethod.POST)
@PutMapping
等于@RequestMapping(method = RequestMethod.PUT)
@PatchMapping
等于@RequestMapping(method = RequestMethod.PATCH)
@DeleteMapping
等于@RequestMapping(method = RequestMethod.DELETE)
@CookieValue
用于方法参数上获得cookie
假设有一个如下的cookie```JSESSIONID=418AB76CD83EF94U85YD34W```
要获取该cookie的值
```
@RequestMapping("/cookieValue")
public void getCookieValue(@CookieValue "JSESSIONID" String cookie){
}
```
要获取该cookie的值
```
@RequestMapping("/cookieValue")
public void getCookieValue(@CookieValue "JSESSIONID" String cookie){
}
```
@CrossOrigin
用于类和方法上,以实现跨域请求。
有时,运行JavaScript的主机和服务数据的主机不是同一个,此时就涉及到跨域(CORS)
有时,运行JavaScript的主机和服务数据的主机不是同一个,此时就涉及到跨域(CORS)
@ExceptionHandler
用于方法上,指示异常处理类
@InitBinder
初始化绑定器,用于数据绑定、设置数据转换器等
@Mappings
@Mapping
@Mapping
用于字段上。
Mapping是一个Meta注解,以指示web映射注解
Mappings可用于多个
Mapping是一个Meta注解,以指示web映射注解
Mappings可用于多个
@MatrixVariable
矩阵变量
@PathVariable
路径变量,获取路径上传过来的参数
@RequestAttribute
绑定请求属性到handler方法参数
@RequestBody
指示方法参数应该绑定到Http请求Body
HttpMessageConveter负责将HTTP请求消息转为对象
HttpMessageConveter负责将HTTP请求消息转为对象
@RequestHeader
映射控制器参数到请求头的值
@RequestParam
用在方法的参数前面
@RequestPart
替代@RequestParam以获得多部的内容并绑定到方法参数
@ResponseBody
指示方法返回值应该直接写入Response Body(不再走视图处理器)
Spring使用HttpMessageConverter实现了返回对象转为响应体
Spring使用HttpMessageConverter实现了返回对象转为响应体
@ResponseStatus
用于方法和异常类上。以一个状态码作为指示,且原因必须返回。
也可注解于Controller,其所有的@RequestMapping方法都会继承它
SessionAttribute
用于方法参数。绑定方法参数到会话属性
@SessionAttributes
用于将会话属性用Bean封装
@JsonBackReference
解决嵌套外链问题
@RepositoryRestResourcepublic
配合spring-boot-starter-data-rest使用
@PathVariable
路径变量
@ModelAttribute
把值绑定到Model中,使全局@RequestMapping可以获取到该值
@Valid
验证器,一般配合@InitBinder使用
全局异常处理
@ControllerAdvice
包含@Component。可以被扫描到。
统一处理异常,一般与@ExceptionHandler一起使用
统一处理异常,一般与@ExceptionHandler一起使用
@RestControllerAdvice
@ControllerAdvice 和 @ResponseBody的组合
核心思想
什么是Spring
Spring是一个轻量级的IOC和AOP容器的开源框架
Spring提倡以最少侵入(可以理解为耦合度)的方式来管理应用中的代码
Spring提倡以最少侵入(可以理解为耦合度)的方式来管理应用中的代码
IOC
概念
控制反转
控制:对对象的创建和管理
反转:对对象的控制由程序员转变为框架
控制:对对象的创建和管理
反转:对对象的控制由程序员转变为框架
原理
如何创建对象
通过IO读取配置文件
利用反射创建对象:class.forName(配置文件中的全路径名)
如果是全自动模式,则通过IO读取配置文件读到的是包,然后再通过IO去扫描这个包下所有的类(包括子包)
扫描的类如果由Component注解标记,则创建该类对象,如果没有,则忽略
重点是反射+IO
利用反射创建对象:class.forName(配置文件中的全路径名)
如果是全自动模式,则通过IO读取配置文件读到的是包,然后再通过IO去扫描这个包下所有的类(包括子包)
扫描的类如果由Component注解标记,则创建该类对象,如果没有,则忽略
重点是反射+IO
如何管理依赖关系
扫描所有成员变量,如果成员变量带有自动注入注解,则从自己容器中寻找要注入的对象,利用反射对其进行注入,如果
找到相应对象,暴力破解直接赋值,如果没找到,则报错
找到相应对象,暴力破解直接赋值,如果没找到,则报错
核心目的
让spring来管理对象
优点
单例
降低耦合(类与类之间的依赖关系)
降低耦合(类与类之间的依赖关系)
IoC核心容器
顶层接口BeanFactory和
ApplicationContext的区别
ApplicationContext的区别
单例对象适用
AplicationContext在构建核心容器时,创建对象采用的是立即加载的方式,即配置文件一读取完,立马创建对象
多例对象适用
BeanFactory在构建核心容器时,创建对象采用的是延迟加载方式,即什么时候通过Id获取对象,什么时候创建
AplicationContext在构建核心容器时,创建对象采用的是立即加载的方式,即配置文件一读取完,立马创建对象
多例对象适用
BeanFactory在构建核心容器时,创建对象采用的是延迟加载方式,即什么时候通过Id获取对象,什么时候创建
ApplicationContext常用实现类
ClassPathXmlApplicationContext
读取类路径下的配置文件
FileSystemXmlApplicationContext
读取电脑任意位置的配置文件
AnnotationConfigApplicationContext
用于使用新注解+配置类的方式代替xml配置文件时
读取类路径下的配置文件
FileSystemXmlApplicationContext
读取电脑任意位置的配置文件
AnnotationConfigApplicationContext
用于使用新注解+配置类的方式代替xml配置文件时
IOC创建对象方式
无参构造器
在spring配置文件中用bean标签配置
bean标签的属性
id
创建的对象名
class
需要被管理的类的全路径
bean标签的属性
id
创建的对象名
class
需要被管理的类的全路径
静态工厂
条件:需要工厂类,该工厂类中需要有静态方法
配置文件语法
<bean id="factory" class="com.wxs.factory.BeanFactory" factory-method="静态方法名" />
当用spring容器调用getBean方法时会创建工厂类对象,并执行工厂类中的方法返回需要的对象并放入spring容器中
配置文件语法
<bean id="factory" class="com.wxs.factory.BeanFactory" factory-method="静态方法名" />
当用spring容器调用getBean方法时会创建工厂类对象,并执行工厂类中的方法返回需要的对象并放入spring容器中
实例工厂
条件:需要工厂类,这个工厂类中需要有普通方法
配置文件语法
<bean id="factory" class="com.wxs.factory.BeanFactory" />
<bean id="car" factory-bean="factory" factory-method="普通方法名">
需要先创建工厂对象再调用工厂中的普通方法
配置文件语法
<bean id="factory" class="com.wxs.factory.BeanFactory" />
<bean id="car" factory-bean="factory" factory-method="普通方法名">
需要先创建工厂对象再调用工厂中的普通方法
DI(依赖注入)的方式
setter注入
条件:属性必须有setter方法
bean标签下<property>标签
name:属性名
value:属性值,针对基本类型和String类型
ref:针对对象类型,指向的是bean标签的id属性的值
name:属性名
value:属性值,针对基本类型和String类型
ref:针对对象类型,指向的是bean标签的id属性的值
复杂注入(map、list、[]...)
list类型语法
数组类型与list类型类似只是没有ref标签
map类型
<bean id="empService" class="com.wxs.service.EmpService">
<property name="list">
<map>
<entry key="" value=""></entry>
</map>
</property>
</bean>
<property name="list">
<map>
<entry key="" value=""></entry>
</map>
</property>
</bean>
properties类型
<bean id="empService" class="com.wxs.service.EmpService">
<property name="p">
<props>
<prop key="" value=""></prop>
</props>
</property>
</bean>
<property name="p">
<props>
<prop key="" value=""></prop>
</props>
</property>
</bean>
有参构造器注入
条件:必须要有构造器
bean标签下<constructor-arg>标签
name属性:属性名
value:值,针对基本类型和Stirng
ref:针对对象类型
name属性:属性名
value:值,针对基本类型和Stirng
ref:针对对象类型
p名称空间注入
条件:需要在配置文件中导入p的命名空间(spring提供的),底层还是set方式,所以属性也必须有setter方法
IOC三种开发方式
手动配置方式
bean标签:在配置文件中用bean标签配置一个类
id属性:用来给对象命名
class属性:用于写实体类的全路径名,供反射适用
scope属性
singleton
单例
只要配置文件被加载,就会创建对象,创建的对象放在spring容器中,这个容器底层为
map集合,key为bean标签的id值,value为这个对象
当调用容器的getBean方法的时候,总是获取到唯一的实例
只要配置文件被加载,就会创建对象,创建的对象放在spring容器中,这个容器底层为
map集合,key为bean标签的id值,value为这个对象
当调用容器的getBean方法的时候,总是获取到唯一的实例
proyotype
多例
当配置文件加载的时候不创建对象,当调用容器的getBean方法时,创建对象并返回,调用一次getBean则创建一次
action适合多例
当配置文件加载的时候不创建对象,当调用容器的getBean方法时,创建对象并返回,调用一次getBean则创建一次
action适合多例
request
作用于web应用的请求范围
session
作用于web应用的会话范围
globle-session
作用于集群环境的会话范围(全局范围)
对象销毁时机
singleton
当spring容器关闭的时候对象销毁
proyotype
长时间不用则被GC回收
若需要给一个类中的成员变量赋初始值则需要在bean标签中定义property子标签
name属性:类的属性名
value:定义普通成员变量的值
ref:如果一个类中引用到其他类的对象,需要用此属性注入
***注意:在配置文件中给属性设置初始值需要提供该属性的setter方法
value:定义普通成员变量的值
ref:如果一个类中引用到其他类的对象,需要用此属性注入
***注意:在配置文件中给属性设置初始值需要提供该属性的setter方法
property子标签<list>
专门用于给list集合类型的成员变量赋初始值
格式
<bean id="empService" class="com.wxs.service.EmpService">
<property name="list">
<list>
<value>11</value>
</list>
</property>
</bean>
<property name="list">
<list>
<value>11</value>
</list>
</property>
</bean>
半自动配置方式
半自动配置方式主要是将类与类之间的依赖关系用注解的方式实现
类的对象还是需要手动配置,但类的依赖关系用注解@Autowired自动实现,
例如A类中引用了B类,在配置文件中用bean标签配置A,在A中用注解标记引用的B
@Autowired
private EmpDAO empDAO;
***注意:Spring框架为了效率,默认在扫描类的时候不会扫描注解,所以默认情况下只加Autowired注解是无法
自动注入的,需要在配置文件用<context:annotation-config/>进行配置
类的对象还是需要手动配置,但类的依赖关系用注解@Autowired自动实现,
例如A类中引用了B类,在配置文件中用bean标签配置A,在A中用注解标记引用的B
@Autowired
private EmpDAO empDAO;
***注意:Spring框架为了效率,默认在扫描类的时候不会扫描注解,所以默认情况下只加Autowired注解是无法
自动注入的,需要在配置文件用<context:annotation-config/>进行配置
全自动配置方式
基本不用配置文件
在类上用@Component注解标记,类中的依赖关系用@Autowired注解标记
在配置文件中用<context:component-scan base-package="com.wxs" />配置需要扫描的包
在全自动配置方式中,对于私有的成员变量,也无须提供setter方法,反射会自动打开访问权限强制访问
***注意:如果通过一个类型匹配到了多个实现类,则会报错,解决方式为在引用类型上添加@Qualifier("")注解指明需要的类
四个创建对象的注解
Component:无法划分类的时候
Repository:一般用于标注dao层或者说标注数据库操作层
Service:业务层
Controller:控制层/表现层(springmvc替代servlet)
**以上四个注解没有区别,只是用于不同场景
Scope:定义类的单例多例
PreDestory
指定销毁方法
PostConstruct
指定初始化方法
三个依赖注入的注解
Value
针对基本数据类型和String类型
Autowired
该注解由Spring框架提供
按类型去找,如果找不到就是没有,如果找到多个就报错,解决方式是搭配Qualifier注解指明适用哪个注解
Resource
该注解由JDK提供
先按名字去找,第一种是name属性配置名字@Resource(name = "名字"),如果没有指定名字
则把变量名当作要寻找的属性名,如果再找不到,最后按类型去找
在类上用@Component注解标记,类中的依赖关系用@Autowired注解标记
在配置文件中用<context:component-scan base-package="com.wxs" />配置需要扫描的包
在全自动配置方式中,对于私有的成员变量,也无须提供setter方法,反射会自动打开访问权限强制访问
***注意:如果通过一个类型匹配到了多个实现类,则会报错,解决方式为在引用类型上添加@Qualifier("")注解指明需要的类
四个创建对象的注解
Component:无法划分类的时候
Repository:一般用于标注dao层或者说标注数据库操作层
Service:业务层
Controller:控制层/表现层(springmvc替代servlet)
**以上四个注解没有区别,只是用于不同场景
Scope:定义类的单例多例
PreDestory
指定销毁方法
PostConstruct
指定初始化方法
三个依赖注入的注解
Value
针对基本数据类型和String类型
Autowired
该注解由Spring框架提供
按类型去找,如果找不到就是没有,如果找到多个就报错,解决方式是搭配Qualifier注解指明适用哪个注解
Resource
该注解由JDK提供
先按名字去找,第一种是name属性配置名字@Resource(name = "名字"),如果没有指定名字
则把变量名当作要寻找的属性名,如果再找不到,最后按类型去找
spring新注解
写一个类,用新注解标注,可以让该类的作用和application.xml的作用一样
@Configuration
表名该被标注的类是一个配置类,但本质作用并不是标注它是一个配置类,而是加上这个注解之后,该类中的所有方法会被
CGLib代理,方法会跟原来的方法完全不一样,这样就保证了对象的生命周期和作用域。
如果不加该注解,在该类中用Bean注解照样好用,用AnnotationApplicationContext去容器取对象也好用,只不过如果该类
中的一个创建对象的方法调用了另一个创建对象的方法,那么另一个对象将被创建多次,不能保证对象的单例,即作用域scope
不能被保证
CGLib代理,方法会跟原来的方法完全不一样,这样就保证了对象的生命周期和作用域。
如果不加该注解,在该类中用Bean注解照样好用,用AnnotationApplicationContext去容器取对象也好用,只不过如果该类
中的一个创建对象的方法调用了另一个创建对象的方法,那么另一个对象将被创建多次,不能保证对象的单例,即作用域scope
不能被保证
@ComponentScan
通过该注解告诉spring在创建容器时需要扫描的包
属性value,和xml配置文件中的<context:component-scan>标签中的basePackage属性作用一样
属性value,和xml配置文件中的<context:component-scan>标签中的basePackage属性作用一样
@Bean
用于标注配置类中的方法,将方法的返回值对象存入spring容器中
name属性用于定义bean的id,当不写时,存入容器集合时key默认为方法名,值为对象
name属性用于定义bean的id,当不写时,存入容器集合时key默认为方法名,值为对象
细节:当我们使用注解配置方法时,如果方法有参数,spring会自动去容器中找有没有可用的bean,查找的方式与@Autowired
注解的方式一致
此时不再使用ClassPathXmlApplicationContext,而是要用AnnotationConfigApplication,并将该配置类的class对象传入
注解的方式一致
此时不再使用ClassPathXmlApplicationContext,而是要用AnnotationConfigApplication,并将该配置类的class对象传入
AOP
概念
AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性
面向切面编程思想:对共同内容进行抽取,在需要用到的地方用动态代理的方式进行插入,在不修改源代码的基础
上,还能对源码进行加强
上,还能对源码进行加强
底层为动态代理:对目标类进行功能增强
对目标类方法进行加强的方式
继承
缺点:需要直到要增强的方法的类才能继承
装饰者模式
缺点:需要有接口,这个接口下除了要增强的方法外别的方法也要实现
动态代理模式
JDK
需要接口但可以指定增强方法,不需要实现全部方法
Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)
返回一个指定接口的代理类实例
参数列表:和目标对象相同的类加载器;目标类所有的接口;增强类(可以自定义增强类并实现InvlcationHandler
接口并重写其内部方法,也可以在此处直接定义匿名内部类)
此处的InvocationHandler可以是一个匿名内部类,实现内部的invoke方法,在该方法中定义增强逻辑
该方法的参数:代理类对象;要被增强的方法(在将目标类所有接口传进去时就已经知道所有的代理方法了);
方法需要的参数。
细节
proxyPerson.run(); // 执行这个方法 invoke都会执行一遍 执行的内容就是针对该方法的增强
返回值 return 谁调用返回给谁 返回的内容就是最终值而不是需要增强的方法的返回值
通过if-else判断方法名来增强指定方法
Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)
返回一个指定接口的代理类实例
参数列表:和目标对象相同的类加载器;目标类所有的接口;增强类(可以自定义增强类并实现InvlcationHandler
接口并重写其内部方法,也可以在此处直接定义匿名内部类)
此处的InvocationHandler可以是一个匿名内部类,实现内部的invoke方法,在该方法中定义增强逻辑
该方法的参数:代理类对象;要被增强的方法(在将目标类所有接口传进去时就已经知道所有的代理方法了);
方法需要的参数。
细节
proxyPerson.run(); // 执行这个方法 invoke都会执行一遍 执行的内容就是针对该方法的增强
返回值 return 谁调用返回给谁 返回的内容就是最终值而不是需要增强的方法的返回值
通过if-else判断方法名来增强指定方法
CGLIB
不需要接口,可以指定增强方法,但代码量比较大
CGLIB原理
该代理模式是用拦截器和过滤器的方式进行继承,对目标类的方法进行增强
CGLIB原理
该代理模式是用拦截器和过滤器的方式进行继承,对目标类的方法进行增强
AOP的三种方式
自由式
步骤
确定目标类(要被切的类,即需要被加强的类),需要定义切入点,此处切入点为需要增强的方法
确定切面类,即用来切类的刀,需要定义增强后的方法
织入配置,把增强方法指定在切入点之前、之后、环绕、异常、最终 执行
确定切面类,即用来切类的刀,需要定义增强后的方法
织入配置,把增强方法指定在切入点之前、之后、环绕、异常、最终 执行
配置文件
织入时机
前置增强
用<aop:before>标签配置
切入点即需要增强的方法,在切入点前介入即在需要增强的方法前介入其他业务逻辑,例如在执行
原方法前打印日志或者其他操作
切入点即需要增强的方法,在切入点前介入即在需要增强的方法前介入其他业务逻辑,例如在执行
原方法前打印日志或者其他操作
后置增强
用<aop:after>标签配置
在原方法执行结束后介入增强后的方法
在原方法执行结束后介入增强后的方法
环绕增强
用<aop:around>标签配置,需要在增强后的方法中传参:ProceedingJoinPoint类型的对象,这个对象
中包含着目标类需要增强的方法,需要在增强后的方法中调用pjp.proceed()方法,相当于调用原方法,
在该方法前后需要写自己的业务逻辑实现环绕介入
中包含着目标类需要增强的方法,需要在增强后的方法中调用pjp.proceed()方法,相当于调用原方法,
在该方法前后需要写自己的业务逻辑实现环绕介入
异常增强
用<aop:after-throwing>标签配置
在原方法发生异常时会介入,一般用于对数据库事务的处理
在原方法发生异常时会介入,一般用于对数据库事务的处理
最终增强
用<aop:after-returning>标签配置
在需要增强的方法正确地返回之后执行
在需要增强的方法正确地返回之后执行
接口规范式
步骤
定义切面类,让其实现特定接口(五类织入时机)
确定目标类(被介入的类),确定其切点
确定切面类(做介入角色的类)
配置文件
确定目标类(被介入的类),确定其切点
确定切面类(做介入角色的类)
配置文件
前置增强
定义的切面类需要实现MethodBeforeAdvice接口并实现befor方法
该方式底层会调用目标类中需要增强的方法,在该切面类中的before方法中只需要写自己的业务逻辑即可
该方式底层会调用目标类中需要增强的方法,在该切面类中的before方法中只需要写自己的业务逻辑即可
后置增强
定义的切面类需要实现AfterReturnAdvice接口并实现afterReturn方法
参数
Object returnValue
原方法返回值
Method method
目标方法
Object[] args
目标方法执行需要的参数数组
Object target
目标对象,即被代理的类的对象
参数
Object returnValue
原方法返回值
Method method
目标方法
Object[] args
目标方法执行需要的参数数组
Object target
目标对象,即被代理的类的对象
环绕增强
切面类需要实现MethodInterceptor接口并实现invoke(MethodInvocation invocation)方法
这种增强方式需要在invoke方法中调用invocation的proceed()方法调用目标方法,在该语句前后实现自己的
业务逻辑达到环绕增强的效果
这种增强方式需要在invoke方法中调用invocation的proceed()方法调用目标方法,在该语句前后实现自己的
业务逻辑达到环绕增强的效果
异常增强
切面类实现ThrowsAdvice接口,该接口是一个空接口,需要在该切面类中自定义名为afterThrowing(Exception e)方法
最终增强
注解介入
开发模式
jsp开发模式
开发效率较高
执行效率与交互效果低
每次请求都是整个页面刷新,当页面数据量大,用户设备老旧或者网速较差,会出现页面卡顿问题
以至于交互效果较差
以至于交互效果较差
不灵活,解决多端变化问题较难
如果有多个前端页面例如手机版、电脑版、iPad版,那得写多套控制层,较难
前后端分离
后台一律响应数据(json格式)而不响应页面,前端利用前端语言和后台的网络接口进行接收数据和显示
优点
灵活,易于解决多端变化问题
只需要写一套后台,不同前端用不同方式与后台交互
只需要写一套后台,不同前端用不同方式与后台交互
缺点
开发效率较低
前后端分离开发模式开发前需要先设计文档,规定后台与前端所需要接口和参数的标准格式,以便于前后端同时开发且
不容易出现前端调用后台接口出问题的情况
不容易出现前端调用后台接口出问题的情况
跨域问题
跨域带数据
跨域带数据
同源策略
同源
协议相同、ip相同、端口号相同
DOM 同源策略:禁止对不同源页面 DOM 进行操作。
这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
做一个假网站,里面用 iframe 嵌套一个银行网站 http://mybank.com。
把 iframe 宽高啥的调整到页面全部,这样用户进来除了域名,
别的部分和银行的网站没有任何差别。
这时如果用户输入账号密码,我们的主网站可以跨域访问到
http://mybank.com 的 dom 节点,就可以拿到用户的账户密码了。
把 iframe 宽高啥的调整到页面全部,这样用户进来除了域名,
别的部分和银行的网站没有任何差别。
这时如果用户输入账号密码,我们的主网站可以跨域访问到
http://mybank.com 的 dom 节点,就可以拿到用户的账户密码了。
XMLHttpRequest 同源策略:
禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。
禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。
在不同源情况下,一个服务器向另一个服务器发送ajax
请求,浏览器默认是禁止的,,会产生跨域错误
请求,浏览器默认是禁止的,,会产生跨域错误
解决方案
1. 设置后台允许接受跨域请求
在后端设置响应头
response.setHeader("Access-Control-Allow-Origin", "http://localhost:63342");
该服务器路径可以通过请求对象动态获取
request.getHeader("Origin")
该服务器路径可以通过请求对象动态获取
request.getHeader("Origin")
或者通过注解设置
在每个Controller类上加@CrossOrigin注解,该注解允许请求服务器默认为*
但是当前端设置为允许跨域携带参数后不允许将跨域访问路径设为*,虽然该注解可以设置路径,但需要在每个注解中设置
跨域请求服务器路径,所以该方式不太方便
但是当前端设置为允许跨域携带参数后不允许将跨域访问路径设为*,虽然该注解可以设置路径,但需要在每个注解中设置
跨域请求服务器路径,所以该方式不太方便
通过拦截器设置
在mvc.xml配置文件中配置拦截器
将该拦截器设置在所有拦截器的最上方,所有请求来之后都先被该拦截器拦截
在拦截器类中为response设置响应头
response.setHeader("Access-Control-Allow-Origin", "http://localhost:63342");
将该拦截器设置在所有拦截器的最上方,所有请求来之后都先被该拦截器拦截
在拦截器类中为response设置响应头
response.setHeader("Access-Control-Allow-Origin", "http://localhost:63342");
2. 设置前端允许跨域请求携带数据
在前端页面设置xhr对象的属性
在ajax请求中设置属性
xhrFields:{
withCredentials:true
},
xhrFields:{
withCredentials:true
},
XHR原生对象的withCredentials是用于跨域请求的,默认为false
如果想要跨域请求并携带数据则需要将其打开
如果想要跨域请求并携带数据则需要将其打开
***在这种可携带数据的跨域模式下不可以设置为*
3. 后台允许跨域请求携带数据
在拦截器类中设置响应头
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Credentials", "true");
springmvc
什么是springmvc
springmvc实际上是springframwork的一个模块,这个模块主要是对web的支持
springmvc是基于IoC和aop的
springmvc是基于IoC和aop的
原理
springmvc提供了一个servlet,我们把所有的请求都发送给这个servlet,这个servlet我们称之为核心控制器
核心控制器在tomcat启动时就会创建,在其init方法中会读取配置文件并扫描指定的包,会为由@Controller注解标记
的类创建对象,并把所有的由@RequestMapping注解标记的映射关系放到HandlerMapping中,键放注解的地址,值放
方法
当在地址栏输入地址,核心控制器会根据资源路径从HandlerMapping中寻找要执行的方法,找到后并调用
在控制类的方法中处理请求和响应,用return实现请求转发(携带的参数放到ModelMap中)和重定向(在返回的字符串前加redirect)
核心控制器在tomcat启动时就会创建,在其init方法中会读取配置文件并扫描指定的包,会为由@Controller注解标记
的类创建对象,并把所有的由@RequestMapping注解标记的映射关系放到HandlerMapping中,键放注解的地址,值放
方法
当在地址栏输入地址,核心控制器会根据资源路径从HandlerMapping中寻找要执行的方法,找到后并调用
在控制类的方法中处理请求和响应,用return实现请求转发(携带的参数放到ModelMap中)和重定向(在返回的字符串前加redirect)
servlet缺点
获取参数比较繁琐,如果类型不是字符串类型则需要进行类型转换,还需要判断是否为空
如果每个servlet对应一个请求,则会产生大量的servlet,如果一个servlet对应多个请求,则违反可单一原则
如果每个servlet对应一个请求,则会产生大量的servlet,如果一个servlet对应多个请求,则违反可单一原则
三大核心组件
处理器映射器
作用:帮我们找到对应的controller
HandlerMapping
传统开发方式,即配置文件方式
依赖BeanNameUrlHandlerMapping类
org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping
注解开发方式,已过时类
AnnotationMethodHandlerAdapter(过时的注解开发方式)
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
注解开发方式,最新类
RequestMappingHandlerAdapter(最新版本的注解开发方式)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
在springmvc内部配置文件中,注解开发方式配置的还是过时的注解驱动类,需要在springmvc配置文件中用
<mvc:annotation-driven/>标签配置新版注解驱动类
<mvc:annotation-driven/>标签配置新版注解驱动类
处理器适配器
作用:帮我们找到响应的方法
方法返回ModelAndView
方法返回ModelAndView
视图解析器
在springmvc配置文件中配置,用来配置contorller类中方法返回的字符串类型的前缀和后缀,简化返回的静态地址的字符串
将结果渲染成页面
将结果渲染成页面
控制类中的注解
@Controller
标记控制类,该类拥有处理请求的能力
@RequesMapping
标注方法,定义请求路径
窄化映射,可以定义到controller类上,隔离各个控制类中的方法
value和path属性
标注这个方法处理请求的地址,支持传数组,可以响应多个请求
method
设置接收的请求的请求方式
RequestMethod.GET, RequestMethod.POST
窄化映射,可以定义到controller类上,隔离各个控制类中的方法
value和path属性
标注这个方法处理请求的地址,支持传数组,可以响应多个请求
method
设置接收的请求的请求方式
RequestMethod.GET, RequestMethod.POST
@ResponseBody
用此注解标记该方法,则底层会将该方法的返回值转换为json格式,spring没有内置转json的类,需要依赖三方jar包
@CrossOrigin
设置跨域访问
传统方式需要设置响应头setHeader("Access-Control-Allow-Origin", "协议+ip+端口")
浏览器的同源策略
同源指的是同一个服务器
如果a服务器向b服务器发送ajax请求,b服务器接收并响应数据,在默认情况下,a服务器的ajax拒绝接收b
服务器的响应,所以需要在b服务器端设置跨域访问,解决跨域错误问题
传统方式需要设置响应头setHeader("Access-Control-Allow-Origin", "协议+ip+端口")
浏览器的同源策略
同源指的是同一个服务器
如果a服务器向b服务器发送ajax请求,b服务器接收并响应数据,在默认情况下,a服务器的ajax拒绝接收b
服务器的响应,所以需要在b服务器端设置跨域访问,解决跨域错误问题
@PathVaribale
用于绑定url的占位符,例如:在请求的url中,/emplist/{empId},{empId}就是一个占位符,在参数列表中想要对应占位符的参数前用
该注解标注,该注解中的值应当与占位符的值一致。url支持占位符是在spring3.0以后引入的
用于实现restful风格
如果想要实现restful风格,则需要将web.xml文件中核心控制器的请求路径设置为/,但此时将会把所有的静态文件例如js、css等
也作为请求发送到核心控制器并去找相应的方法执行,此时就会访问不到静态资源,所以释放静态资源
在springmvc配置文件中配置静态资源,用<mvc:resources location="请求地址例如:/js/(以js开头的请求)"
mapping="映射位置例如 :/js/**(项目下js文件夹下所有的文件的子文件)" />
restful风格
用占位符的方式接收参数,占位符的参数名叫啥,前端name应该叫啥
传统风格
RequestMapping("/delete")
localhost:8080/delete?id=10
restful风格
RequestMapping("/delete/{id}")
restful风格:localhost:8080/delete/10
该注解标注,该注解中的值应当与占位符的值一致。url支持占位符是在spring3.0以后引入的
用于实现restful风格
如果想要实现restful风格,则需要将web.xml文件中核心控制器的请求路径设置为/,但此时将会把所有的静态文件例如js、css等
也作为请求发送到核心控制器并去找相应的方法执行,此时就会访问不到静态资源,所以释放静态资源
在springmvc配置文件中配置静态资源,用<mvc:resources location="请求地址例如:/js/(以js开头的请求)"
mapping="映射位置例如 :/js/**(项目下js文件夹下所有的文件的子文件)" />
restful风格
用占位符的方式接收参数,占位符的参数名叫啥,前端name应该叫啥
传统风格
RequestMapping("/delete")
localhost:8080/delete?id=10
restful风格
RequestMapping("/delete/{id}")
restful风格:localhost:8080/delete/10
@ModelAttribute
被该注解标注的方法会先执行
适用场景:当前端提交表单,带着表单数据向控制类中的某个方法发送请求,但表单数据并不是一个完整的JavaBean对象的
数据,此时可以定义一个新的方法,用该注解标注,那么在执行对应请求方法时会先执行该方法,该方法也可以从请求中
获取请求参数,可以在该方法中通过请求参数从数据库查询完整数据,并将最后JavaBean对象返回,这样数据就会完整,另
一种方式是无返回值方法,可以参参数列表定义一个map集合,将最后的JavaBean放入map集合中,在对应的请求方法的
参数列表中也用ModelAttribute注解标注参数,并在注解中给出放入map集合的key
适用场景:当前端提交表单,带着表单数据向控制类中的某个方法发送请求,但表单数据并不是一个完整的JavaBean对象的
数据,此时可以定义一个新的方法,用该注解标注,那么在执行对应请求方法时会先执行该方法,该方法也可以从请求中
获取请求参数,可以在该方法中通过请求参数从数据库查询完整数据,并将最后JavaBean对象返回,这样数据就会完整,另
一种方式是无返回值方法,可以参参数列表定义一个map集合,将最后的JavaBean放入map集合中,在对应的请求方法的
参数列表中也用ModelAttribute注解标注参数,并在注解中给出放入map集合的key
注解语法糖
在spring4.2.x版本及以后出现了复合注解
@GetMapping("/")
@PostMapping
直接指明请求方式
@RestController
@Controller和@ResponseBody的符合注解
@GetMapping("/")
@PostMapping
直接指明请求方式
@RestController
@Controller和@ResponseBody的符合注解
参数绑定
默认参数绑定
request
response
session
ModelMap
response
session
ModelMap
基本数据类型
在对应方法的参数列表中定义请求参数
类型写你需要的,底层会帮你转,要求参数列表中的参数名与请求参数名一致
原理
mvc会反射你的方法参数列表,根据参数名去找请求参数对应的值,会尝试将数据转成你想要的类型,
如果不能转成你想要的,抛异常
类型写你需要的,底层会帮你转,要求参数列表中的参数名与请求参数名一致
原理
mvc会反射你的方法参数列表,根据参数名去找请求参数对应的值,会尝试将数据转成你想要的类型,
如果不能转成你想要的,抛异常
bean方式
可以使用对象接收,在参数列表定义对象类型,mvc可以直接自动封装成对象,前提是对象的属性名跟请求参数名一致
绑定包装的bean
即对象中有对象引用,要求前端数据的name为bean中的bena名称.属性名,例如订单实体中有商品实体(pro),如果要绑定商品
id,则在前端需要将参数name定义为pro.id
id,则在前端需要将参数name定义为pro.id
数组绑定
一般用于批量删除,在前端定义复选框,复选框的名称相同且和控制类对应方法的数组名一致,springmvc可自动帮你获取参数
集合绑定
应用场景不多,一般用于批量修改,前端修改n条数据,提交多个对象到后台,但只能支持向对象中接收集合,即
控制类对应方法中需要定义一个集合,接收参数时会接收到该对象的集合中,而且要求前端name属性为集合名[下标].对象属性名
在jsp页面的c:foreach标签中的status属性可以获取遍历的集合的每次索引值
控制类对应方法中需要定义一个集合,接收参数时会接收到该对象的集合中,而且要求前端name属性为集合名[下标].对象属性名
在jsp页面的c:foreach标签中的status属性可以获取遍历的集合的每次索引值
注解方式
@RequestParam注解,标记参数列表
注解属性
value/name
指明要获取的参数名,用于跟请求参数名匹配
required
默认为true,要求请求参数必须有,如果没有,则出现400,设为false则可以没有
defaultValue
用于定义参数列表的默认值,如果请求参数没有传来,则默认值生效
注解属性
value/name
指明要获取的参数名,用于跟请求参数名匹配
required
默认为true,要求请求参数必须有,如果没有,则出现400,设为false则可以没有
defaultValue
用于定义参数列表的默认值,如果请求参数没有传来,则默认值生效
自定义转换器
当前端参数出现springmvc无法自动转换的参数时,例如时间,可以使用自定义转换器
步骤
创建一个类,实现Converter<T, V>,T:源,V:目标,即需要将什么类型转换为什么类型
实现convert(T t)方法并返回想要的
将自定义转换器配置到springmvc容器中
在注册新版处理器映射器,处理器适配器驱动时,将自定义转换器配置
<mvc:annotation-driven conversion-service="自定义转换器id"/>
配置自定义转换器
<bean id="" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="convers">
<set><bean class="自己定义的转换器类的全限定名"/></set>
</property>
</bean>
注解方式解决mvc不支持的参数绑定
将spring不支持的绑定类型参数上用@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
步骤
创建一个类,实现Converter<T, V>,T:源,V:目标,即需要将什么类型转换为什么类型
实现convert(T t)方法并返回想要的
将自定义转换器配置到springmvc容器中
在注册新版处理器映射器,处理器适配器驱动时,将自定义转换器配置
<mvc:annotation-driven conversion-service="自定义转换器id"/>
配置自定义转换器
<bean id="" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="convers">
<set><bean class="自己定义的转换器类的全限定名"/></set>
</property>
</bean>
注解方式解决mvc不支持的参数绑定
将spring不支持的绑定类型参数上用@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
springmvc如何响应
返回String类型的地址
在方法中直接return页面地址默认就是请求转发
在返回的页面地址字符串前加redirect:则为重定向
请求转发携带的参数需要放到ModelMap中
例如
在返回的页面地址字符串前加redirect:则为重定向
请求转发携带的参数需要放到ModelMap中
例如
配置视图解析器
在spring配置文件中配置前缀和后缀
配置语法
将配置文件的前缀和后缀与控制类中方法return的字符串拼接即可得到想要的路径
配置语法
将配置文件的前缀和后缀与控制类中方法return的字符串拼接即可得到想要的路径
拦截器
框架提供的有跟过滤器功能类似但更强大的拦截器
拦截器会拦截Controller类中方法的调用
在controller类中方法执行前被拦截
在controller类的方法执行之后但在视图解析前被拦截
方法执行完且视图解析之后拦截
在controller类的方法执行之后但在视图解析前被拦截
方法执行完且视图解析之后拦截
快速入门
1. 定义一个类实现HandlerInterceptor接口
2. 实现该接口中的方法,所有的方法
返回值为boolean,真放行,假拦截
返回值为boolean,真放行,假拦截
该接口中的方法为默认方法,每个方法都有不同的拦截时机
preHandle(req,resp)
在controller的方法被调用之前执行该方法
postHandle(req, resp)
在方法被调用视图解析之前调用
afterCompletion(req, resp)
在方法执行完视图解析之后调用
preHandle(req,resp)
在controller的方法被调用之前执行该方法
postHandle(req, resp)
在方法被调用视图解析之前调用
afterCompletion(req, resp)
在方法执行完视图解析之后调用
3. 在配置文件中配置拦截器,可以配置多个拦截器,
哪个在上边,最先执行哪个拦截器
哪个在上边,最先执行哪个拦截器
<mvc:interceptors>
<mvc:interceptor>
<!-- 配置哪个方法需要拦截 -->
<mvc:mapping path="/**">
<!-- 配置哪个方法不需要拦截 -->
<mvc:exclued-mapping path="">
<!-- 设置拦截器类路径 -->
<bean class="拦截器类全限定名">
</mvc:interceptor>
</mvc:interceptors>
<mvc:interceptor>
<!-- 配置哪个方法需要拦截 -->
<mvc:mapping path="/**">
<!-- 配置哪个方法不需要拦截 -->
<mvc:exclued-mapping path="">
<!-- 设置拦截器类路径 -->
<bean class="拦截器类全限定名">
</mvc:interceptor>
</mvc:interceptors>
执行顺序
两个兰拦截器,1拦截器配置在前,2拦截器配置在后
文件上传
springmvc文件上传原理
浏览器通过input标签将需要上传的文件自动读入到内存,当提交表单的时候,会将该文件发送到后台核心控制器,在请求中
会携带这个文件,此时会调用到文件解析器,文件解析器会解析请求对象,将请求对象的文件解析出来返回给核心控制器
核心控制器再调用处理器映射器,找到相应的控制类的方法,通过参数绑定的形式,绑定给该方法,该方法的参数类型必须
是MultipartFile类型,参数名必须要和input标签中的name属性保持一致,最后调用该MultipartFile对象的方法进行上传
会携带这个文件,此时会调用到文件解析器,文件解析器会解析请求对象,将请求对象的文件解析出来返回给核心控制器
核心控制器再调用处理器映射器,找到相应的控制类的方法,通过参数绑定的形式,绑定给该方法,该方法的参数类型必须
是MultipartFile类型,参数名必须要和input标签中的name属性保持一致,最后调用该MultipartFile对象的方法进行上传
文件解析器配置
前后端要求
页面要求
传统表单方式
1. 表单提交方式一定是post
2. 表单的enctype的值一定是multipart/form-data
3. input的type一定是file
ajax方式
1. type:post
2. data:FormData
3. processData: false
4. contentType: false,
2. data:FormData
3. processData: false
4. contentType: false,
springmvc要求
1. 需要两个jar包commons-io.jar和commons-fileupload.jar
2. 在ppringmvc配置文件中配置文件解析器
3. 绑定参数类型一定为MultipartFile,参数名字要和input的name属性的值保持一致
2. 在ppringmvc配置文件中配置文件解析器
3. 绑定参数类型一定为MultipartFile,参数名字要和input的name属性的值保持一致
springmvc和struts2优劣
相同点
都基于mvc设计模式
底层都是对ServletAPI的封装
处理请求的机制都是一个核心控制器
底层都是对ServletAPI的封装
处理请求的机制都是一个核心控制器
区别
springmvc的入口是Servlet,struts2的入口是Filter
springmvc的最小单元是方法,是基于方法设计的,struts2的最小单元是基于类,每次执行都会创建一个动作类,所以mvc更快
springmvc使用更简洁,发送ajax更方便
s2的OGNL表达式使页面开发效率更高,但执行效率没有并没有比JSTL有所提升
springmvc的最小单元是方法,是基于方法设计的,struts2的最小单元是基于类,每次执行都会创建一个动作类,所以mvc更快
springmvc使用更简洁,发送ajax更方便
s2的OGNL表达式使页面开发效率更高,但执行效率没有并没有比JSTL有所提升
spring整合junit
在spring下junit的问题
程序的入口为main
junit单元测试中没有main方法也能执行
junit内部集成了一个main方法
当执行时,会利用反射判断该测试类有没有被@Test标注的方法
如果有,.invoke执行该方法
junit不会管我们是否用框架
在执行测试方法的时候,junit根本不知道我们是否使用了框架,所以在执行的时候根本不会为我们通过配置文件或者
配置类来创建spring容器
所以,在用junit测试的时候根本没有ioc容器,就算使用Autowired方法也不会有效果
junit单元测试中没有main方法也能执行
junit内部集成了一个main方法
当执行时,会利用反射判断该测试类有没有被@Test标注的方法
如果有,.invoke执行该方法
junit不会管我们是否用框架
在执行测试方法的时候,junit根本不知道我们是否使用了框架,所以在执行的时候根本不会为我们通过配置文件或者
配置类来创建spring容器
所以,在用junit测试的时候根本没有ioc容器,就算使用Autowired方法也不会有效果
整合思路
导入spring整合junit的jar包--->spring-test.jar
使用junit提供的一个注解,把原有的main方法替换了,替换成spring自己的main
@RunWith(SpringJUnit4ClassRunner.class)
告知spring运行器,spring和ioc创建是基于xml还是注解,并说明位置
用@ContextConfiguration--->locations属性:指定xml文件位置,加上classpath关键字,表示该文件在类路径下
classes属性:指定配置类所在位置
使用junit提供的一个注解,把原有的main方法替换了,替换成spring自己的main
@RunWith(SpringJUnit4ClassRunner.class)
告知spring运行器,spring和ioc创建是基于xml还是注解,并说明位置
用@ContextConfiguration--->locations属性:指定xml文件位置,加上classpath关键字,表示该文件在类路径下
classes属性:指定配置类所在位置
当我们用spring5.x版本的时候,要求junit版本在4.12及以上版本
spring事务管理
API方式/硬编码
PlatformTransactionManager 平台事务管理器 是一个接口
定义了开启事务、提交事务、回滚的方法
我们使用DataSourceTransactionManager这一实现类 主要针对
dbutils和jdbcTemplate 对jdbc的封装这个实现类就是一个切面类
TransactionDefinition:定义事务参数的接口
事务的隔离级别
事务的超时时间
事务的是否只读
事务的传播行为
TransactionStatus:事务运行状态接口
查看当前事务是否完成
查看是否为新事务
查看是否回滚
定义了开启事务、提交事务、回滚的方法
我们使用DataSourceTransactionManager这一实现类 主要针对
dbutils和jdbcTemplate 对jdbc的封装这个实现类就是一个切面类
TransactionDefinition:定义事务参数的接口
事务的隔离级别
事务的超时时间
事务的是否只读
事务的传播行为
TransactionStatus:事务运行状态接口
查看当前事务是否完成
查看是否为新事务
查看是否回滚
声明式/配置文件式
通过配置文件告诉spring,让spring去使用事务控制,这种配置文件的方式实际上是对API的封装
jar包
事务包spring-tx.jar
事务依赖包
AOP联盟
aspectj.jar
spring-aspects.jar
步骤
确定切面类即配置
事务管理器
用<bean>标签配置DataSourceTransactionManager类,
该类需要一个数据源用ref属性配置
<bean id="事务管理器名" class="事务类全路径">
<property name="" ref="bean标签中的数据源">
</bean>
细节:需要为事务配置一些事务参数:是否只读、超时时间、传播行为(标签)
用<tx:advice transaction-manager="bean标签中配置的平台事务管理器">标签配置,
必须有这个标签进行配置,事务参数可以不写,不写就为默认值
配置声明事务即切面
<tx:advice id="事务名" transaction-manager="需要的事务管理器名">
<tx:attributes>
<!-- 需要进行事务管理的方法 -->
<tx:method name="方法名"/>
</tx:attributes>
</tx:advice>
配置织入
用<aop:advisor>标签配置切面类,用<aop:pointcut>标签配置切点
用事务作为切点
jar包
事务包spring-tx.jar
事务依赖包
AOP联盟
aspectj.jar
spring-aspects.jar
步骤
确定切面类即配置
事务管理器
用<bean>标签配置DataSourceTransactionManager类,
该类需要一个数据源用ref属性配置
<bean id="事务管理器名" class="事务类全路径">
<property name="" ref="bean标签中的数据源">
</bean>
细节:需要为事务配置一些事务参数:是否只读、超时时间、传播行为(标签)
用<tx:advice transaction-manager="bean标签中配置的平台事务管理器">标签配置,
必须有这个标签进行配置,事务参数可以不写,不写就为默认值
配置声明事务即切面
<tx:advice id="事务名" transaction-manager="需要的事务管理器名">
<tx:attributes>
<!-- 需要进行事务管理的方法 -->
<tx:method name="方法名"/>
</tx:attributes>
</tx:advice>
配置织入
用<aop:advisor>标签配置切面类,用<aop:pointcut>标签配置切点
用事务作为切点
注解
ssm整合
在web.xml中配置
配置核心控制器
核心控制器路径,此处的url-pattern如果是/则指的是除jsp文件以外的所有请求都包含,/*则包含jsp
核心控制器创建时机
核心控制器需要的配置文件
核心控制器创建时机
核心控制器需要的配置文件
配置监听器
当服务器启动时,该监听器会通过配置文件来初始化spring容器,实现ioc
如果想通过配置监听来实现ioc,需要配下方的配置全局参数
如果想通过配置监听来实现ioc,需要配下方的配置全局参数
配置全局参数
该参数指向spring配置文件(该配置文件用于整合spring、mybatis),监听器会加载该配置文件,将配置文件
中的数据源、对象以及mapper扫描器创建
中的数据源、对象以及mapper扫描器创建
持久层(dao)
mybatis.xml(可有可无),一般在该文件下配置settings和typeAliases,不过在配置sqlFactoryFactoryBean时也可以配置别名
applicationContext-dao.xml
数据源(druid)
sqlSessionFactory
数据源
别名
<property name="typeAliasesPackage" value="com.wxs.entity">
配置mapper扫描器
在配置sqlSession工厂时,通过property标签,让name值为mapperLocations
value值为classpath:com/wxs/mapper,这里需要使用斜线
用这种方式可以扫描项目下任意路径,可以解决mapper映射器文件和接口不在同意路径下的问题
mapper扫描器
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
在该bean标签下配置sqlSessionFactoryBean,因为mapper接口的底层还是需要由sqlSession得到的
配置需要扫描的包
sqlSessionFactory
数据源
别名
<property name="typeAliasesPackage" value="com.wxs.entity">
配置mapper扫描器
在配置sqlSession工厂时,通过property标签,让name值为mapperLocations
value值为classpath:com/wxs/mapper,这里需要使用斜线
用这种方式可以扫描项目下任意路径,可以解决mapper映射器文件和接口不在同意路径下的问题
mapper扫描器
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
在该bean标签下配置sqlSessionFactoryBean,因为mapper接口的底层还是需要由sqlSession得到的
配置需要扫描的包
业务层
applicationContext-service.xml
事务
包扫描器(主要针对@Service注解)
事务
包扫描器(主要针对@Service注解)
web层(有mvc支持)
springmvc.xml
注解驱动版本
<mvc:annotation-driven/>
配置处理器适配器、处理器映射器最新驱动
包扫描器,主要针对@Controller注解
视图解析器
以setter方式注入前缀和后缀
异常处理器,文件解析器,拦截器
注解驱动版本
<mvc:annotation-driven/>
配置处理器适配器、处理器映射器最新驱动
包扫描器,主要针对@Controller注解
视图解析器
以setter方式注入前缀和后缀
异常处理器,文件解析器,拦截器
***父子容器关系
spring容器(配置文件)与springmvc容器(配置文件)是父子容器关系
在这两个容器中都只能出现一个<context:property-placeholder location="">
标签加载文件,且子容器可以访问父容器加载到的配置文件,而父容器不能访问子容器加载的文件
在这两个容器中都只能出现一个<context:property-placeholder location="">
标签加载文件,且子容器可以访问父容器加载到的配置文件,而父容器不能访问子容器加载的文件
源码分析
springMVC
springboot
springcloud
关于Cloud各种组件的停更/升级/替换
服务注册中心
Eureka
重度患者
Zookeeper
Consul
Nacos
推荐
重度患者
Zookeeper
Consul
Nacos
推荐
服务调用
Ribbon
轻度患者
LoadBalancer
轻度患者
LoadBalancer
服务调用
Feign
OpenFeign
OpenFeign
服务降级
Hystrix
resilience4j
国外使用多
alibaba Sentinel
国内使用多
resilience4j
国外使用多
alibaba Sentinel
国内使用多
服务网关
Zuul
Zuul2
胎死腹中
gateway
Zuul2
胎死腹中
gateway
服务配置
Config
Nacos
apollo
Nacos
apollo
服务主线
Bus
Nacos
Nacos
微服务架构编码 构建
约定>配置>编码
slave会从master读取binlog来进行数据同步
三步骤+原理图
master将改变记录到二进制日志(binary log)。这些记录过程叫做二进制日志事件,binary log events
slave将master的binary log events拷贝到它的中继日志(relay log)
slave重做中继日志中的事件,将改变应用到自己的数据库中。 MySQL复制是异步的且串行化的
IDEA新建project工作空间
微服务cloud整体聚合工程
父工程步骤
1.New Project
2.聚合总父工程名字
3. Maven选版本
4. 工程名字
5. 字符编码
6. 注解生效激活
7. java编译版本选8
8.File Type过滤
2.聚合总父工程名字
3. Maven选版本
4. 工程名字
5. 字符编码
6. 注解生效激活
7. java编译版本选8
8.File Type过滤
父工程POM
<
< xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<4.0.0<
<com.atguigu.springcloud<
<cloud2020<
<1.0-SNAPSHOT<...
< xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<4.0.0<
<com.atguigu.springcloud<
<cloud2020<
<1.0-SNAPSHOT<...
Maven工程落地细节复习
Maven中的DependencyManagement和Dependencies
这样做的好处就是: 如果有多个子项目都引用同一样的依赖,则可以避免在每个使用的子项目里都声明一个版本号,这样想升级或切换到另一个版本时,只需在顶层父容器里更新,而不需要一个一个子项目的修改l;另外如果某个子项目需要另外的一个版本,只需声明version版本
maven中跳过单元测试
父工程创建完成执行mvn:insall将父工程发布到仓库方便子工程继承
Rest微服务工程搭建
构建步骤
1.Cloud-provider-payment8001 微服务提供者Module模块
1 建module
创建完成后回到父工程查看pom文件变化
2 改POM
3 写YML
4 主启动
5 业务类
1.建表sql
CREATE TABLE `payment` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`serial` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '支付流水号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '支付表' ROW_FORMAT = Dynamic;
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`serial` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '支付流水号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '支付表' ROW_FORMAT = Dynamic;
2.emtities
主实体Payment
Json封装体CommonResult
Json封装体CommonResult
3.dao
接口PaymentDao
mybatis的映射文件PaymentMapper.xml
4.service
接口PaymentService
实现类
实现类
5.controller
测试
http://localhost:8001/payment/get/31
postman模拟post
运行
通过修改idea的workspace.xml的方式快速打开Run Dashboard窗口
开启Run DashBoard
部分同学可能由于idea版本不同,需要关闭重启
开启Run DashBoard
部分同学可能由于idea版本不同,需要关闭重启
小总结
1 建module
2 改POM
3 写YML
4 主启动
5 业务类
2 改POM
3 写YML
4 主启动
5 业务类
2.热部署Devtools
3.cloud-consumer-order80 微服务消费者订单Module模块
4. 工程重构
目前工程样图
Eureka服务注册与发现
Eureka基础知识
什么是服务治理
什么是服务注册
Eureka两组件
单机Eureka构建步骤
IDEA生成EurekaServer端服务注册中心 类似物业公司
EurekaClient端cloud-provider-payment8001 将注册进EurekaServer成为服务提供者provider,类似于尚硅谷学校对外提供授课服务
EurekaClient端cloud-consumer-order80 将注册进EurekaServer成为服务消费者consumer,类似于尚硅谷学校上课消费的各位同学
cloud-provider-payment8001
改POM
<
<org.springframework.cloud<
<spring-cloud-starter-netflix-eureka-server<
<
<org.springframework.cloud<
<spring-cloud-starter-netflix-eureka-server<
<
写YML
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
主启动
@EnableEurekaClient
测试
bug
bug
集群Eureka构建步骤
Eureka集群原理说明
解决办法: 搭建Eureka注册中心集群,实现负载均衡+故障容错
Eureka集群环境构建步骤
参考cloud-eureka-server7001
新建cloud-eureka-server7002
改POM
修改映射配置
找到C:\Windows\System32\drivers\etc路径下的hosts文件
修改映射配置添加hosts文件
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
127.0.0.1 eureka7002.com
刷新hosts文件
ipconfig /flushdns
写YMl(以前单机)
7001
server:
port: 7001
spring:
application:
name: cloud-eureka-service
eureka:
instance:
# eureka服务端的实例名称
hostname: eureka7001.com
client:...
port: 7001
spring:
application:
name: cloud-eureka-service
eureka:
instance:
# eureka服务端的实例名称
hostname: eureka7001.com
client:...
7002
server:
port: 7002
spring:
application:
name: cloud-eureka-service2
eureka:
instance:
hostname: eureka7002.com
client:
register-with-eureka: false...
port: 7002
spring:
application:
name: cloud-eureka-service2
eureka:
instance:
hostname: eureka7002.com
client:
register-with-eureka: false...
主启动
将支付服务8001微服务发布到上面2台Eureka集群配置中
YAML
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
# 集群版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
client:
register-with-eureka: true
fetch-registry: true
service-url:
# 集群版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
将订单服务80微服务发布到上面2台Eureka集群配置中
YAML
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
测试01
支付服务提供者8001集群环境搭建
负载均衡
测试02
actuator微服务信息完善
服务发现Discovery
eureka自我保护
故障现象
导致原因
一句话:某时刻 一个微服务不可用了,Eureka不会立刻清理,依旧会对该服务的信息进行保存
属于CAP里面的AP分支
属于CAP里面的AP分支
怎么禁止自我保护
注册中心eurekaServer端7001
出产默认,自我保护机制是开启的
eureka.server.enable-self-preservation=true
使用eureka.server.enable-self-preservation=false 可以禁用自我保护模式
关闭效果
在eurekaServer端7001处设置关闭自我保护机制
eureka.server.enable-self-preservation=true
使用eureka.server.enable-self-preservation=false 可以禁用自我保护模式
关闭效果
在eurekaServer端7001处设置关闭自我保护机制
生产者客户端eurekaClient端8001
默认
eureka.instance.lease-renewal-interval-in-seconds=30
Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
eureka.instance.lease-expiration-duration-in-seconds=90
Eureka服务端在收到最后一次心跳后等待时间上限 ,单位为秒(默认是90秒),超时剔除服务
配置
测试
Zookeeper服务注册与发现
Eureka停止更新了,你怎么办
SpringCloud整合Zookeeper替代Eureka
Consul服务注册与发现
Consul简介
安装并运行Consul
服务提供者
服务消费者
三个注册中心异同点
Ribbon负载均衡调用
概述
是什么
官网资料
https://github.com/Netflix/ribbon/wiki/Getting-Started
Ribbon目前也进入维护模式
未来替换方案
SpringCloud loadBalancer
能干嘛
LB(负载均衡)
集中式LB
进程内LB
进程内LB
前面我们讲解过了80通过轮询负载访问8001/8002
一句话
负载均衡+RestTemplate调用
Ribbon负载均衡演示
架构说明
总结: Ribbon其实就是一个软负载均衡的客户端组件, 他可以和其他所需请求的客户端结合使用,和eureka结合只是其中一个实例.
POM
二说RestTemplate的使用
官网
getForObject方法/getForEntity方法
postForObject/postEntity
GET请求方法
POST请求方法
getForObject方法/getForEntity方法
postForObject/postEntity
GET请求方法
POST请求方法
Ribbon核心组件IRule
IRule:根据特定算法从服务列表中选取一个要访问的服务
com.netflix.loadbalancer.RoundRobinRule
轮询
com.netflix.loadbalancer.RandomRule
随机
com.netflix.loadbalancer.RetryRule
先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内进行重试,获取可用的服务
WeightedResponseTimeRule
对RoundRobinRule的扩展,响应速度越快的实例选择权重越多大,越容易被选择
BestAvailableRule
会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
AvailabilityFilteringRule
先过滤掉故障实例,再选择并发较小的实例
ZoneAvoidanceRule
默认规则,复合判断server所在区域的性能和server的可用性选择服务器
如何替换
修改cloud-consumer-order80
注意配置细节
新建package
com.atguigu.myrule
上面包下新建MySelfRule规则类
package com.atguigu.myrule;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 自定义负载均衡路由规则类
*...
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 自定义负载均衡路由规则类
*...
主启动类添加@RibbonClient
package com.atguigu.springcloud;
import com.atguigu.myrule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
/**
* @author zzyy...
import com.atguigu.myrule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
/**
* @author zzyy...
子主题
http://localhost/consumer/payment/get/31
Ribbon负载均衡算法
原理
源码
手写
自己试着写一个本地负载均衡器试试
7001/7002集群启动
8001/8002集群启动
controller
子主题
@GetMapping(value = "/payment/lb")
public String getPaymentLB() {
return serverPort;
}
public String getPaymentLB() {
return serverPort;
}
80订单微服务改造
1.ApplicationContextBean去掉注解@LoadBalanced
2.LoadBalancer接口
3.MyLB
4.OrderController
5.测试
效果
http://localhost/consumer/payment/lb
2.LoadBalancer接口
3.MyLB
4.OrderController
5.测试
效果
http://localhost/consumer/payment/lb
OpenFeign服务接口调用
概述
OpenFeign是什么
Feign是一个声明式的Web服务客户端,让编写Web服务客户端变得非常容易,只需 创建一个接口并在接口上添加注解即可
https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/#spring-cloud-openfeign
https://github.com/spring-cloud/spring-cloud-openfeign
能干嘛
Feign和OpenFeign两者区别
OpenFeign使用步骤
接口+注解
微服务调用接口+@FeignClient
新建cloud-consumer-feign-order80
Feign在消费端使用
POM
YML
主启动
业务类
业务逻辑接口+@FeignClient配置调用provider服务
新建PaymentFeignService接口并新增注解@FeignClient
@FeignClient
控制层Controller
新建PaymentFeignService接口并新增注解@FeignClient
@FeignClient
控制层Controller
测试
先启动2个eureka集群7001/7002
再启动2个微服务8001/8002
启动OpenFeign
http://localhost/consumer/payment/get/31
Feign自带负载均衡配置项
再启动2个微服务8001/8002
启动OpenFeign
http://localhost/consumer/payment/get/31
Feign自带负载均衡配置项
小总结
OpenFeign超时控制
超时设置,故意设置超时演示出错情况
服务提供方8001故意写暂停程序
服务消费方80添加超时方法PaymentFeignService
服务消费方80添加超时方法OrderFeignController
测试
http://localhost/consumer/payment/feign/timeout
错误页面
服务消费方80添加超时方法PaymentFeignService
服务消费方80添加超时方法OrderFeignController
测试
http://localhost/consumer/payment/feign/timeout
错误页面
OpenFeign默认等待1秒钟,超过后报错
是什么
OpenFeign默认支持Ribbon
YML文件里需要开启OpenFeign客户端超时控制
server:
port: 80
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
# 设置feign客户端超时时间(OpenFeign默认支持ribbon)...
port: 80
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
# 设置feign客户端超时时间(OpenFeign默认支持ribbon)...
OpenFeign日志打印功能
日志打印功能
是什么
日志级别
配置日志bean
package com.atguigu.springcloud.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* OpenFeignClient配置
*
* @author zzyy...
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* OpenFeignClient配置
*
* @author zzyy...
YML文件里需要开启日志的Feign客户端
server:
port: 80
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
# 设置feign客户端超时时间(OpenFeign默认支持ribbon)...
port: 80
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
# 设置feign客户端超时时间(OpenFeign默认支持ribbon)...
后台日志查看
Hystrix熔断器
概述
分布式系统面临的问题
分布式系统面临的问题
复杂分布式体系结构中的应用程序 有数10个依赖关系,每个依赖关系在某些时候将不可避免地失败
复杂分布式体系结构中的应用程序 有数10个依赖关系,每个依赖关系在某些时候将不可避免地失败
是什么
能干嘛
服务降级
服务熔断
接近实时的监控
服务熔断
接近实时的监控
官网资料
https://github.com/Netflix/hystrix/wiki
Hystrix官宣,停更进维
被动修复bugs
不再接受合并请求
不再发布新版本
不再接受合并请求
不再发布新版本
HyStrix重要概念
服务降级
服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback
哪些情况会发出降级
程序运行异常
超时
服务熔断触发服务降级
线程池/信号量也会导致服务降级
超时
服务熔断触发服务降级
线程池/信号量也会导致服务降级
服务熔断
类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示
就是保险丝
服务的降级->进而熔断->恢复调用链路
就是保险丝
服务的降级->进而熔断->恢复调用链路
服务限流
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
hystrix案例
构建
新建cloud-provider-hystrix-payment8001
POM
YML
主启动
子主题
业务类
正常测试
高并发测试
上述在非高并发情形下,还能勉强满足 but...
Jmeter压测测试
下载地址
https://jmeter.apache.org/download_jmeter.cgi
https://jmeter.apache.org/download_jmeter.cgi
开启Jmeter,来20000个并发压死8001,20000个请求都去访问paymentInfo_TimeOut服务
再来一个访问
http://localhost:8001/payment/hystrix/timeout/31
看演示结果
两个都在转圈圈
为什么会被卡死
为什么会被卡死
tomcat的默认工作线程数被打满了,没有多余的线程来分解压力和处理
Jmeter压测结论
上面还只是服务提供者8001自己测试,假如此时外部的消费者80也来访问,那消费者只能干等,最终导致消费端80不满意,服务端8001直接被拖死
故障和导致现象
8001同一层次的其他接口被困死,因为tomcat线程池里面的工作线程已经被挤占完毕
80此时调用8001,客户端访问响应缓慢,转圈圈
80此时调用8001,客户端访问响应缓慢,转圈圈
上述结论
正因为有上述故障或不佳表现 才有我们的降级/容错/限流等技术诞生
如何解决?解决的要求
超时导致服务器变慢(转圈)
超时不再等待
出错(宕机或程序运行出错)
出错要有兜底
解决
对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级
对方服务(8001)down机了,调用者(80)不能一直卡死等待,必须有服务降级
对方服务(8001)ok,调用者(80)自己有故障或有自我要求(自己的等待时间小于服务提供者)
对方服务(8001)down机了,调用者(80)不能一直卡死等待,必须有服务降级
对方服务(8001)ok,调用者(80)自己有故障或有自我要求(自己的等待时间小于服务提供者)
服务降级
降级配置
@HystrixCommand
8001先从自身找问题
设置自身调用超时时间的峰值,峰值内可以正常运行, 超过了需要有兜底的方法处理,做服务降级fallback
8001fallback
业务类启用
@HystrixCommand(fallbackMethod = "payment_TimeOutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
@HystrixCommand报异常后如何处理
一旦调用服务方法失败并抛出了错误信息后,会自动调用@HystrixCommand标注好的fallbckMethod调用类中的指定方法
主启动类激活
@EnableCircuitBreaker
80fallback
目前问题
每个业务方法对应一个兜底的方法,代码膨胀
统一和自定义的分开
统一和自定义的分开
解决办法
每个方法配置一个???膨胀
和业务逻辑混在一起???混乱
服务熔断
断路器
一句话就是家里的保险丝
熔断是什么
子主题
实操
原理/小总结
大神结论
熔断类型
熔断打开
请求不再调用当前服务,内部设置一般为MTTR(平均故障处理时间),当打开长达导所设时钟则进入半熔断状态
熔断关闭
熔断关闭后不会对服务进行熔断
熔断半开
部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
官网断路器流程图
官网步骤
断路器在什么情况下开始起作用
断路器开启或者关闭的条件
当满足一定的阈值的时候(默认10秒钟超过20个请求次数)
当失败率达到一定的时候(默认10秒内超过50%的请求次数)
到达以上阈值,断路器将会开启
当开启的时候,所有请求都不会进行转发
一段时间之后(默认5秒),这个时候断路器是半开状态,会让其他一个请求进行转发. 如果成功,断路器会关闭,若失败,继续开启.重复4和5
断路器打开之后
ALl配置
熔断类型
熔断打开
请求不再调用当前服务,内部设置一般为MTTR(平均故障处理时间),当打开长达导所设时钟则进入半熔断状态
熔断关闭
熔断关闭后不会对服务进行熔断
熔断半开
部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
官网断路器流程图
官网步骤
断路器在什么情况下开始起作用
断路器开启或者关闭的条件
当满足一定的阈值的时候(默认10秒钟超过20个请求次数)
当失败率达到一定的时候(默认10秒内超过50%的请求次数)
到达以上阈值,断路器将会开启
当开启的时候,所有请求都不会进行转发
一段时间之后(默认5秒),这个时候断路器是半开状态,会让其他一个请求进行转发. 如果成功,断路器会关闭,若失败,继续开启.重复4和5
断路器打开之后
ALl配置
服务限流
后面高级篇讲解alibaba的Sentinel说明
hystrix工作流程
服务监控hystrixDashboard
zuul路由网关
概述描述
路由基本配置
路由访问映射规则
查看路由信息
过滤器
Gateway新一代网关
概述简介
是什么
一句话:
SpringCloud Gateway使用的是Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架
SpringCloud Gateway使用的是Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架
源码架构
能干嘛
反向代理
鉴权
流量控制
熔断
日志监控
.....
鉴权
流量控制
熔断
日志监控
.....
微服务架构中网关在哪里
有Zuull了怎么又出来gateway
我们为什么选择Gateway?
1.netflix不太靠谱,zuul2.0一直跳票,迟迟不发布
2.SpringCloud Gateway具有如下特性
3.SpringCloud Gateway与Zuul的区别
1.netflix不太靠谱,zuul2.0一直跳票,迟迟不发布
2.SpringCloud Gateway具有如下特性
3.SpringCloud Gateway与Zuul的区别
三大核心概念
Route(路由)
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如断言为true则匹配该路由
Predicate(断言)
参考的是Java8的java.util.function.Predicate 开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
Filter(过滤)
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改.
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如断言为true则匹配该路由
Predicate(断言)
参考的是Java8的java.util.function.Predicate 开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
Filter(过滤)
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改.
Gateway工作流程
官网总结
核心逻辑
路由转发+执行过滤器链
核心逻辑
路由转发+执行过滤器链
入门配置
通过服务名实现动态
Predicate
Route Predicate Factories这个是什么东东
常用的Route Predicate
1.After Route Predicate
2.Before Route Predicate
3.Between Route Predicate
4.Cookie Route Predicate
不带cookies访问
带上cookies访问
加入curl返回中文乱码
https://blog.csdn.net/leedee/article/details/82685636
5.Header Route Predicate
6.Host Route Predicate
7.Method Route Predicate
8.Path Route Predicate
9.Query Route Predicate
YML
10.RemoteAddr Route Predicate
11.Weight Route Predicate
小总结
ALL
说白了,Predicate就是为了实现一组匹配规则, 让请求过来找到对应的Route进行处理
常用的Route Predicate
1.After Route Predicate
2.Before Route Predicate
3.Between Route Predicate
4.Cookie Route Predicate
不带cookies访问
带上cookies访问
加入curl返回中文乱码
https://blog.csdn.net/leedee/article/details/82685636
5.Header Route Predicate
6.Host Route Predicate
7.Method Route Predicate
8.Path Route Predicate
9.Query Route Predicate
YML
10.RemoteAddr Route Predicate
11.Weight Route Predicate
小总结
ALL
说白了,Predicate就是为了实现一组匹配规则, 让请求过来找到对应的Route进行处理
Filter的使用
Spring Cloud Gateway的filter
生命周期,Only Two
pre
post
种类,Only Two
GatewayFilter
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gatewayfilter-factories
GlobalFilter
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#global-filters
常用的GatewayFilter
AddRequestParameter
YML
省略
自定义过滤器
自定义全局GlobalFilter
两个主要接口介绍
implments GlobalFilter,OrderId
能干嘛
全局日志记录
统一网关鉴权
.....
案例代码
package com.atguigu.springcloud.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;...
测试
启动
正确
http://localhost:9527/payment/lb?uname=z3
错误
生命周期,Only Two
pre
post
种类,Only Two
GatewayFilter
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gatewayfilter-factories
GlobalFilter
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#global-filters
常用的GatewayFilter
AddRequestParameter
YML
省略
自定义过滤器
自定义全局GlobalFilter
两个主要接口介绍
implments GlobalFilter,OrderId
能干嘛
全局日志记录
统一网关鉴权
.....
案例代码
package com.atguigu.springcloud.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;...
测试
启动
正确
http://localhost:9527/payment/lb?uname=z3
错误
SpringCloud config分布式配置中心
概述
Config服务端配置与测试
Config客户端配置与测试
Config客户端之动态刷新
SpringCloud Bus消息总线
概述
RabbitMQ环境配置
SpringCloud Bus动态刷新全局广播
SpringCloud Bus动态刷新定点通知
SpringCloud Stream消息驱动
消息驱动概述
案例说明
消息驱动之生产者
消息驱动之消费者
分组消费与持久化
SpringCloud Sleuth分布式链路跟踪
概述
搭建链路监控步骤
SpringCloud Alibaba入门简介
why会出现SpringCloud alibaba
SpringCloud alibaba带来了什么
SpringCloud alibaba学习资料获取
SpringCloud Alibaba Nacos服务注册和配置中心
Nacos简介
安装并运行Nacos
Nacos作为服务注册中心演示
Nacos作为服务配置中心演示
Nacos集群和持久化配置(重要)
SpringCloud Alibaba Sentinel实现熔断与限流
Sentiel
安装Sentiel控制台
初始化演示功能
流控规则
降级规则
热点key限流
系统规则
@SentinelResource
服务熔断功能
规则持久化
SpringCloud Alibaba Seata处理分布式事务
分布式事务问题
Seata简介
Seata-Server安装
订单/库存/账户业务数据库准备
订单/库存/账户业务微服务准备
Test
一部分补充
微服务
常用微服务框架
常用RPC框架
治理
安全
sso
SQL注入
XSS
CSRF
DDos
加密解密
对称密钥
1.定义
对称加密算法即,加密和解密使用相同密钥的算法。(加密Key=解密key);对称密钥算法又分为分组密码 (Block Cipher)和流密码(Stream Cipher)。
2.优缺点
优点:加密速度快,便于硬件实现和大规模生产
缺点:需要保障密钥安全;无法用来签名和抗抵赖;
缺点:需要保障密钥安全;无法用来签名和抗抵赖;
每对用户每次使用对称加密算法时,都需要使用其他人不知道的惟一钥匙,这会使得发收信双方所拥有的钥匙数量呈几何级数增长,密钥管理成为用户的负担。对称加密算法在分布式网络系统上使用较为困难,主要是因为密钥管理困难,使用成本较高。
3.算法
(1)DES(Data Encryption Standard,数据加密算法)
DES是最基本的对称加密算法,也是使用频率最高的一种算法,加密密钥与解密密钥相同。DES出身比较好,出自IBM之手,后被美国军方采纳,之后便广泛流传,但是近些年使用越来越少,因为DES使用56位密钥,以现代计算能力,24小时内即可被破解。虽然如此,在某些简单应用中,我们还是可以使用DES加密算法。DES使用56位长度的密钥,有些提供商可能附加8位奇偶校验位。
算法流程:
发送者构建秘钥-->发送秘钥--> 接收者
发送者明文-->DES算法+秘钥加密--> 密文--> 接收者
接收者--> DES算法+秘钥解密--> 明文
算法流程:
发送者构建秘钥-->发送秘钥--> 接收者
发送者明文-->DES算法+秘钥加密--> 密文--> 接收者
接收者--> DES算法+秘钥解密--> 明文
(2)3DES(Triple Data Encryption Algorithm,三重数据加密算法)
3DES(或称为Triple DES)是三重数据加密算法(TDEA,Triple Data Encryption Algorithm)块密码的通称。它相当于是对每个数据块应用三次DES加密算法。由于计算机运算能力的增强,原版DES密码的密钥长度变得容易被暴力破解;3DES即是设计用来提供一种相对简单的方法,即通过增加DES的密钥长度来避免类似的攻击,而不是设计一种全新的块密码算法。
DES被很多密码学机构质疑,因为算法是半公开的,因此违反柯克霍夫原则,所以在这个基础上,延伸了3重DES.
算法流程:
发送者构建秘钥-->发送秘钥--> 接收者
发送者明文-->3DES算法+秘钥加密--> 密文--> 接收者
接收者--> 3DES算法+秘钥解密--> 明文
DES被很多密码学机构质疑,因为算法是半公开的,因此违反柯克霍夫原则,所以在这个基础上,延伸了3重DES.
算法流程:
发送者构建秘钥-->发送秘钥--> 接收者
发送者明文-->3DES算法+秘钥加密--> 密文--> 接收者
接收者--> 3DES算法+秘钥解密--> 明文
(3)AES(Advanced Encryption Standard,高级加密标准,又称Rijndael加密法)
高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于2001年11月26日发布于FIPS PUB 197,并在2002年5月26日成为有效的标准。2006年,高级加密标准已然成为对称密钥加密中最流行的算法之一。
AES是目前使用最多的对称加密算法之一,AES至今尚未听说有被破解的案例。
AES通常用于移动通信系统加密以及基于SSH协议的软件,如SSH Client,secureCRT
AES是用来替代DES的,因为DES有很多被破解,而3DES效率又比较慢
AES加密算法的默认密钥长度为128,还可以选择192、256.
注意:JDK实现AES算法,使用256位密钥长度时要获得无政策限制权限文件,BC不会存在该问题。无政策限制权限是指某些国家的进口管制限制,java发布的运行环境包中中的加解密有一定限制。
算法流程:
发送者构建秘钥-->发送秘钥--> 接收者
发送者明文-->AES算法+秘钥加密--> 密文--> 接收者
接收者--> AES算法+秘钥解密--> 明文
AES是目前使用最多的对称加密算法之一,AES至今尚未听说有被破解的案例。
AES通常用于移动通信系统加密以及基于SSH协议的软件,如SSH Client,secureCRT
AES是用来替代DES的,因为DES有很多被破解,而3DES效率又比较慢
AES加密算法的默认密钥长度为128,还可以选择192、256.
注意:JDK实现AES算法,使用256位密钥长度时要获得无政策限制权限文件,BC不会存在该问题。无政策限制权限是指某些国家的进口管制限制,java发布的运行环境包中中的加解密有一定限制。
算法流程:
发送者构建秘钥-->发送秘钥--> 接收者
发送者明文-->AES算法+秘钥加密--> 密文--> 接收者
接收者--> AES算法+秘钥解密--> 明文
(4)PBE(Password-based encryption,基于密码验证)
PBE算法(Password Based Encryption,基于口令加密)是一种基于口令的加密算法,其特点是使用口令代替了密钥,而口令由用户自己掌管,采用随机数杂凑多重加密等方法保证数据的安全性。PBE算法在加密过程中并不是直接使用口令来加密,而是加密的密钥由口令生成,这个功能由PBE算法中的KDF函数完成。KDF函数的实现过程为:将用户输入的口令首先通过“盐”(salt)的扰乱产生准密钥,再将准密钥经过散列函数多次迭代后生成最终加密密钥,密钥生成后,PBE算法再选用对称加密算法对数据进行加密,可以选择DES、3DES、RC5等对称加密算法。
特点:
结合了消息摘要算法和对称加密算法的优点,本质上是对MD5/SHA以及DES/3DES/AES算法的包装,不是新的算法,不过也是最为牛逼的一种方式。
盐:指加密的随机字符串或者口令等,也可以人为是一些扰码,防止密码的暴力破解
算法流程:
发送者构建口令-->发送口令--> 接收者
发送者构建盐
发送者明文-->PBE算法+口令+盐加密--> 密文(同时发送盐)--> 接收者
接收者--> PBE算法+口令+盐解密--> 明文
特点:
结合了消息摘要算法和对称加密算法的优点,本质上是对MD5/SHA以及DES/3DES/AES算法的包装,不是新的算法,不过也是最为牛逼的一种方式。
盐:指加密的随机字符串或者口令等,也可以人为是一些扰码,防止密码的暴力破解
算法流程:
发送者构建口令-->发送口令--> 接收者
发送者构建盐
发送者明文-->PBE算法+口令+盐加密--> 密文(同时发送盐)--> 接收者
接收者--> PBE算法+口令+盐解密--> 明文
(5)RC4(来自Rivest Cipher 4的缩写)
RC4于1987年提出,和DES算法一样。是一种对称加密算法,也就是说使用的密钥为单钥(或称为私钥)。
但不同于DES的是。RC4不是对明文进行分组处理,而是字节流的方式依次加密明文中的每个字节。解密的时候也是依次对密文中的每个字节进行解密。
特点:
算法简单,执行速度快;
并且密钥长度是可变的,可变范围为1-256字节(8-2048比特)。
(在现在技术支持的前提下,当密钥长度为128比特时,用暴力法搜索密钥已经不太可行,所以能够预见RC4的密钥范围任然能够在今后相当长的时间里抵御暴力搜索密钥的攻击。实际上,现在也没有找到对于128bit密钥长度的RC4加密算法的有效攻击方法。)
关键变量:
1、密钥流:RC4算法的关键是依据明文和密钥生成相应的密钥流,密钥流的长度和明文的长度是相应的。也就是说明文的长度是500字节,那么密钥流也是500字节。当然,加密生成的密文也是500字节。由于密文第i字节=明文第i字节^密钥流第i字节;
2、状态向量S:长度为256。S[0],S[1].....S[255]。每一个单元都是一个字节。算法执行的不论什么时候。S都包含0-255的8比特数的排列组合,仅仅只是值的位置发生了变换;
3、暂时向量T:长度也为256,每一个单元也是一个字节。
假设密钥的长度是256字节。就直接把密钥的值赋给T,否则,轮转地将密钥的每一个字节赋给T。
4、密钥K:长度为1-256字节。注意密钥的长度keylen与明文长度、密钥流的长度没有必定关系。通常密钥的长度趣味16字节(128比特)。
但不同于DES的是。RC4不是对明文进行分组处理,而是字节流的方式依次加密明文中的每个字节。解密的时候也是依次对密文中的每个字节进行解密。
特点:
算法简单,执行速度快;
并且密钥长度是可变的,可变范围为1-256字节(8-2048比特)。
(在现在技术支持的前提下,当密钥长度为128比特时,用暴力法搜索密钥已经不太可行,所以能够预见RC4的密钥范围任然能够在今后相当长的时间里抵御暴力搜索密钥的攻击。实际上,现在也没有找到对于128bit密钥长度的RC4加密算法的有效攻击方法。)
关键变量:
1、密钥流:RC4算法的关键是依据明文和密钥生成相应的密钥流,密钥流的长度和明文的长度是相应的。也就是说明文的长度是500字节,那么密钥流也是500字节。当然,加密生成的密文也是500字节。由于密文第i字节=明文第i字节^密钥流第i字节;
2、状态向量S:长度为256。S[0],S[1].....S[255]。每一个单元都是一个字节。算法执行的不论什么时候。S都包含0-255的8比特数的排列组合,仅仅只是值的位置发生了变换;
3、暂时向量T:长度也为256,每一个单元也是一个字节。
假设密钥的长度是256字节。就直接把密钥的值赋给T,否则,轮转地将密钥的每一个字节赋给T。
4、密钥K:长度为1-256字节。注意密钥的长度keylen与明文长度、密钥流的长度没有必定关系。通常密钥的长度趣味16字节(128比特)。
6)SM1
定义:国密 SM1( SM1 cryptographic algorithm),国密 SM1 算法是由国家密码管理局编制的一种商用密码分组标准对称算法。
SM1 为对称加密。其加密强度与AES相当。该算法不公开,调用该算法时,需要通过加密芯片的接口进行调用;该算法是国家密码管理部门审批的 SM1 分组密码算法 , 分组长度和密钥长度都为 128 比特, 仅以 IP 核的形式存在于芯片中。采用该算法已经研制了系列芯片、智能 IC 卡、智能密码钥匙、加密卡、加 密机等安全产品,广泛应用于电子政务、电子商务及国民经济的各个应用领域(包括国家政 务
通、警务通等重要领域)。
SM1 为对称加密。其加密强度与AES相当。该算法不公开,调用该算法时,需要通过加密芯片的接口进行调用;该算法是国家密码管理部门审批的 SM1 分组密码算法 , 分组长度和密钥长度都为 128 比特, 仅以 IP 核的形式存在于芯片中。采用该算法已经研制了系列芯片、智能 IC 卡、智能密码钥匙、加密卡、加 密机等安全产品,广泛应用于电子政务、电子商务及国民经济的各个应用领域(包括国家政 务
通、警务通等重要领域)。
7)SM4
2012年3月,国家密码管理局正式公布了包含SM4分组密码算法在内的《祖冲之序列密码算法》等6项密码行业标准。其分组长度为128bit,密钥长度也为128bit。加密算法与密钥扩展算法均采用32轮非线性迭代结构,以字(32位)为单位进行加密运算,每一次迭代运算均为一轮变换函数F。SM4算法加/解密算法的结构相同,只是使用轮密钥相反,其中解密轮密钥是加密轮密钥的逆序。
3.分组密码 (Block Cipher)
一次加密解密操作作用于一个数据块,例如64位算法:DES、 3DES、AES 、DES替代者、RC2、 RC5、BLOWFISH 、TWOFISH
分组密码设计原则:
安全性角度:
(1)“混乱原则”:为了避免密码分析者利用明文与密文之间的依赖关系进行破译,密码的设计应该保证这种依赖关系足够复杂。
(2)“扩散原则” :为避免密码分析者对密钥逐段破译,密码的设计应该保证密钥的每位数字能够影响密文中的多位数字 ;同时,为了避免密码分析者利用明文的统计特性,密码的设计应该保证明文的每位数字能够影响密文中的多位数字,从而隐藏明文的统计特性。
可实现性角度:
(1)应该具有标准的组件结构 (子模块),以适应超大规模集成电路的实现。
(2)分组密码的运算能在子模块上通过简单的运算进行
4.流密码(Stream Cipher)
一次加密解密操作作用于一位或一个字节。如算法:RC4
5.对称密码模式:
电子密码本(Eletronic CoodBook, ECB),密码分组链(Cipher Block Chaining , CBC),密文反馈( Cipher FeedBack , CFB),输出反馈(Output FeedBack , OFB )。
电子密码本模式 Electronic Code Book:
3.分组密码 (Block Cipher)
一次加密解密操作作用于一个数据块,例如64位算法:DES、 3DES、AES 、DES替代者、RC2、 RC5、BLOWFISH 、TWOFISH
分组密码设计原则:
安全性角度:
(1)“混乱原则”:为了避免密码分析者利用明文与密文之间的依赖关系进行破译,密码的设计应该保证这种依赖关系足够复杂。
(2)“扩散原则” :为避免密码分析者对密钥逐段破译,密码的设计应该保证密钥的每位数字能够影响密文中的多位数字 ;同时,为了避免密码分析者利用明文的统计特性,密码的设计应该保证明文的每位数字能够影响密文中的多位数字,从而隐藏明文的统计特性。
可实现性角度:
(1)应该具有标准的组件结构 (子模块),以适应超大规模集成电路的实现。
(2)分组密码的运算能在子模块上通过简单的运算进行
4.流密码(Stream Cipher)
一次加密解密操作作用于一位或一个字节。如算法:RC4
5.对称密码模式:
电子密码本(Eletronic CoodBook, ECB),密码分组链(Cipher Block Chaining , CBC),密文反馈( Cipher FeedBack , CFB),输出反馈(Output FeedBack , OFB )。
电子密码本模式 Electronic Code Book:
优点:
(1)简单;(2)有利于并行计算;(3)误差不会被传送;
缺点:
(1)不能隐藏明文的模式;(2)可能对明文进行主动攻击;
密码分组链模式 Cipher Block Chaining:
(1)简单;(2)有利于并行计算;(3)误差不会被传送;
缺点:
(1)不能隐藏明文的模式;(2)可能对明文进行主动攻击;
密码分组链模式 Cipher Block Chaining:
优点:不容易主动攻击,安全性好于ECB,适合传输长度长的报文,是SSL、IPSec的 标准。
缺点:(1)不利于并行计算;(2)误差传递;(3)需要初始化向量IV
缺点:(1)不利于并行计算;(2)误差传递;(3)需要初始化向量IV
非对称密钥
1定义
非对称密码体制也叫公开密钥密码体制、双密钥密码体制。其原理是加密密钥与解密密钥不同,形成一个密钥对,用其中一个密钥加密的结果,可以用另一个密钥来解密 。
加密和解密使用不同的密钥;一个密钥公开,称公钥;一个密钥保密,称私钥
2优缺点
优点:密钥分配,不必保持信道的保密性 ;可以用来签名和防抵赖
3 算法
1)RSA加密签名特性:
RSA是1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。当时他们三人都在麻省理工学院工作。RSA就是他们三人姓氏开头字母拼在一起组成的。
在RSA加密算法,由于PADDING的不一致,会导致加密结果每次不一致;使用PKCS1Padding加密结果不一致;使用NoPadding加密结果一致;签名结果每次是一致的。
RSA算法的安全性:
在理论上,rsa 的安全性取决于模n分解的困难性,但数学上至今还未证明分解模就是攻击rsa的最佳办法。
人们完全可以设想有另外的途径破译rsa,如求解密指数d或找到(p-1)(q-1)等。但这些途径都不比分解n来的容易。
已公开的或已知的攻击方法:
1,针对RSA最流行的攻击一般是基于大数因数分解。
1999年,RSA-155 (512 bits)被成功分解,花了五个月时间(约8000 MIPS年)和224 CPU hours在一台有3.2G中央内存的Cray C916计算机上完成。
ECC 160bit算法等级,相当于RSA 1024bit密钥提供的保密强度,210bit与RSA2048算法强度相当,计算量则更小,处理速度更快,存储空间和传输带宽占用较少
在RSA加密算法,由于PADDING的不一致,会导致加密结果每次不一致;使用PKCS1Padding加密结果不一致;使用NoPadding加密结果一致;签名结果每次是一致的。
RSA算法的安全性:
在理论上,rsa 的安全性取决于模n分解的困难性,但数学上至今还未证明分解模就是攻击rsa的最佳办法。
人们完全可以设想有另外的途径破译rsa,如求解密指数d或找到(p-1)(q-1)等。但这些途径都不比分解n来的容易。
已公开的或已知的攻击方法:
1,针对RSA最流行的攻击一般是基于大数因数分解。
1999年,RSA-155 (512 bits)被成功分解,花了五个月时间(约8000 MIPS年)和224 CPU hours在一台有3.2G中央内存的Cray C916计算机上完成。
ECC 160bit算法等级,相当于RSA 1024bit密钥提供的保密强度,210bit与RSA2048算法强度相当,计算量则更小,处理速度更快,存储空间和传输带宽占用较少
2,秀尔算法
量子计算里的秀尔算法能使穷举的效率大大的提高。由于RSA算法是基于大数分解(无法抵抗穷举攻击),因此在未来量子计算能对RSA算法构成较大的威胁。一个拥有N量子比特的量子计算机,每次可进行2^N次运算,理论上讲,密钥为1024位长的RSA算法,用一台512量子比特位的量子计算机在1秒内即可破解。
(2)非对称密钥算法-ECC:(ECC-椭圆曲线加密 Ellipse Curve Cryptography)
基于椭圆曲线理论的公钥加密技术(1985)与传统的基于大质数因子分解困难性的加密方法不同,ECC通过椭圆曲线方程式的性质产生密钥
(3)DSA(Digital Signature Algorithm):数字签名算法,是一种标准的 DSS(数字签名标准),严格来说不算加密算法;
ECDSA签名算法:
(1)选择一条椭圆曲线Ep(a,b),和基点G;
(2)选择私有密钥k(k<n,n为G的阶),利用基点G计算公开密钥K=kG;
(3)产生一个随机整数r(r<n),计算点R=rG;
(4)将原数据和点R的坐标值x,y作为参数,计算SHA1做为hash,即Hash=SHA1(原数据,x,y);
(5)计算s≡r - Hash * k (mod n)
(6)r和s做为签名值,如果r和s其中一个为0,重新从第3步开始执行
ECDSA验证:
(1)接受方在收到消息(m)和签名值(r,s)后,进行以下运算
(2)计算:sG+H(m)P=(x1,y1), r1≡ x1 mod p。
(3)验证等式:r1 ≡ r mod p。
(4)如果等式成立,接受签名,否则签名无效。
各取所长:对称密码和非对称密码结合;使用对称密码加密数据;协商对称密钥算法;非对称密码加密密钥;公钥加密密钥;私钥解密;数字证书解决了数据完整性和不可否认性
(2)非对称密钥算法-ECC:(ECC-椭圆曲线加密 Ellipse Curve Cryptography)
基于椭圆曲线理论的公钥加密技术(1985)与传统的基于大质数因子分解困难性的加密方法不同,ECC通过椭圆曲线方程式的性质产生密钥
(3)DSA(Digital Signature Algorithm):数字签名算法,是一种标准的 DSS(数字签名标准),严格来说不算加密算法;
ECDSA签名算法:
(1)选择一条椭圆曲线Ep(a,b),和基点G;
(2)选择私有密钥k(k<n,n为G的阶),利用基点G计算公开密钥K=kG;
(3)产生一个随机整数r(r<n),计算点R=rG;
(4)将原数据和点R的坐标值x,y作为参数,计算SHA1做为hash,即Hash=SHA1(原数据,x,y);
(5)计算s≡r - Hash * k (mod n)
(6)r和s做为签名值,如果r和s其中一个为0,重新从第3步开始执行
ECDSA验证:
(1)接受方在收到消息(m)和签名值(r,s)后,进行以下运算
(2)计算:sG+H(m)P=(x1,y1), r1≡ x1 mod p。
(3)验证等式:r1 ≡ r mod p。
(4)如果等式成立,接受签名,否则签名无效。
各取所长:对称密码和非对称密码结合;使用对称密码加密数据;协商对称密钥算法;非对称密码加密密钥;公钥加密密钥;私钥解密;数字证书解决了数据完整性和不可否认性
3.HASH算法:
特点:输入长度不定,输出长度为定值;不可逆;碰撞几率大
常见算法:
MD5 128Bit
Sha-1 160Bit
SHA-224, SHA-256, SHA-384 和SHA-512
HASH算法主要应用于:散列算法最重要的用途在于给证书、文档、密码等高安全系数的内容添加加密保护。这一方面的用途主要是得益于散列算法的不可逆性,这种不可逆性体现在,你不仅不可能根据一段通过散列算法得到的指纹来获得原有的文件,也不可能简单地创造一个文件并让它的指纹与一段目标指纹相一致。
在密码学中,hash算法的作用主要是用于消息摘要和签名
一个优秀的 hash 算法,将能实现:
(1)正向快速:给定明文和 hash 算法,在有限时间和有限资源内能计算出 hash 值。
(2)逆向困难:给定(若干) hash 值,在有限时间内很难(基本不可能)逆推出明文。
(3)输入敏感:原始输入信息修改一点信息,产生的 hash 值看起来应该都有很大不同。
(4)冲突避免:很难找到两段内容不同的明文,使得它们的 hash 值一致(发生冲突)。即对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。
(ECDSA每次签名信息是否一样:不一样,因为ECDSA签名算法的第(3)步产生一个随机整数r(r<n),计算点R=rG;导致最终的签名信息不一样。)
Sha-1 160Bit
安全哈希加密技术,是当今世界最先近的加密算法。主要用于文件身份识别、数字签名和口令加密等。
对于明文信息A,通过SHA1算法,生成一条160位长的识别码B。且明文信息A和识别码B之间同时满足以下条件:
1、对于任意两条不同的明文信息A1、A2,其识别码B1、B2都不相同。
2、无法通过逆向算法由识别码B倒推出明文信息A。
MOONCRM的用户密码采用SHA1加密存储,即服务器上存储的只是由用户密码生成的识别码,而用户密码本身并没有存储在服务器上。用户输入登陆口令时,系统会根据输入口令生成相应识别码并与系统中所存储的识别码进行比较,如二者一致,则认为口令正确。系统中没有存储用户原始的口令值,即使有人获得口令文件,也无法破解用户登陆密码,确保用户密码绝对安全。
MD5 128Bit
Message-Digest泛指字节串(Message)的Hash变换,就是把一个任意长度的字节串变换成一定长的大整数。请注意是“字节串”而不是“字符串”这个词,是因为这种变换只与字节的值有关,与字符集或编码方式无关。
MD5将任意长度的“字节串”变换成一个128bit的大整数,并且它是一个不可逆的字符串变换算法,换句话说就是,即使你看到源程序和算法描述,也无法将一个MD5的值变换回原始的字符串,从数学原理上说,是因为原始的字符串有无穷多个,这有点象不存在反函数的数学函数。
MD5的典型应用是对一段Message(字节串)产生fingerprint(指纹),以防止被“篡改”。举个例子,你将一段话写在一个叫 readme.txt文件中,并对这个readme.txt产生一个MD5的值并记录在案,然后你可以传播这个文件给别人,别人如果修改了文件中的任何内容,你对这个文件重新计算MD5时就会发现(两个MD5值不相同)。如果再有一个第三方的认证机构,用MD5还可以防止文件作者的“抵赖”,这就是所谓的数字签名应用。
MD5是一种不可逆的加密算法 安全性很高 一般在网上用作判断文件完整性
如果一个文件被修改或不完整 算出来的MD5码是和原来不一样的 所以当你害怕下的东西有病毒或木马或不完整 可以用MD5计算器算一下 再和网站上提供的值对比关于软件很多地方有下的 软件就一个作用 把软件拖进去 然后算出MD5码 。
这个基本上是软件内部利用MD5加密导致,本身几乎无效验方法,因为MD5不可以反编译,方法写个过程,或下载MD5文件 调用MD5(PASSWORD)
MD5中的MD代表Message Digest,就是信息摘要的意思,不过这个信息摘要不是信息内容的缩写,而是根据公开的MD5算法对原信息进行数学变换后得到的一个128位(bit)的特征码。
这个特征码有如下特性,首先它不可逆,例如我有一段秘密的文字如:"My Secret Words",经算法变换后得到MD5码(b9944e9367d2e40dd1f0c4040d4daaf7),把这个码告诉其他人,他们根据这个MD5码是没有系统的方法可以知道你原来的文字是什么的。其次,这个码具有高度的离散性,也就是说,原信息的一点点变化就会导致MD5的巨大变化,例如"ABC" MD5(902fbdd2b1df0c4f70b4a5d23525e932)和"ABC "(多了一空格)MD5(12c774468f981a9487c30773d8093561)差别非常大,而且之间没有任何关系,也就是说产生的MD5码是不可预测的。最后由于这个码有128位那么长,所以任意信息之间具有相同MD5码的可能性非常之低,通常被认为是不可能的。 所以一般认为MD5码可以唯一地代表原信息的特征,通常用于密码的加密存储,数字签名,文件完整性验证等。2004年,已经被山东大学的王小云教授破解了。比如:2345 两个相加的和再相乘 2+3=5 4+5=9 5*9=45
常见算法:
MD5 128Bit
Sha-1 160Bit
SHA-224, SHA-256, SHA-384 和SHA-512
HASH算法主要应用于:散列算法最重要的用途在于给证书、文档、密码等高安全系数的内容添加加密保护。这一方面的用途主要是得益于散列算法的不可逆性,这种不可逆性体现在,你不仅不可能根据一段通过散列算法得到的指纹来获得原有的文件,也不可能简单地创造一个文件并让它的指纹与一段目标指纹相一致。
在密码学中,hash算法的作用主要是用于消息摘要和签名
一个优秀的 hash 算法,将能实现:
(1)正向快速:给定明文和 hash 算法,在有限时间和有限资源内能计算出 hash 值。
(2)逆向困难:给定(若干) hash 值,在有限时间内很难(基本不可能)逆推出明文。
(3)输入敏感:原始输入信息修改一点信息,产生的 hash 值看起来应该都有很大不同。
(4)冲突避免:很难找到两段内容不同的明文,使得它们的 hash 值一致(发生冲突)。即对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。
(ECDSA每次签名信息是否一样:不一样,因为ECDSA签名算法的第(3)步产生一个随机整数r(r<n),计算点R=rG;导致最终的签名信息不一样。)
Sha-1 160Bit
安全哈希加密技术,是当今世界最先近的加密算法。主要用于文件身份识别、数字签名和口令加密等。
对于明文信息A,通过SHA1算法,生成一条160位长的识别码B。且明文信息A和识别码B之间同时满足以下条件:
1、对于任意两条不同的明文信息A1、A2,其识别码B1、B2都不相同。
2、无法通过逆向算法由识别码B倒推出明文信息A。
MOONCRM的用户密码采用SHA1加密存储,即服务器上存储的只是由用户密码生成的识别码,而用户密码本身并没有存储在服务器上。用户输入登陆口令时,系统会根据输入口令生成相应识别码并与系统中所存储的识别码进行比较,如二者一致,则认为口令正确。系统中没有存储用户原始的口令值,即使有人获得口令文件,也无法破解用户登陆密码,确保用户密码绝对安全。
MD5 128Bit
Message-Digest泛指字节串(Message)的Hash变换,就是把一个任意长度的字节串变换成一定长的大整数。请注意是“字节串”而不是“字符串”这个词,是因为这种变换只与字节的值有关,与字符集或编码方式无关。
MD5将任意长度的“字节串”变换成一个128bit的大整数,并且它是一个不可逆的字符串变换算法,换句话说就是,即使你看到源程序和算法描述,也无法将一个MD5的值变换回原始的字符串,从数学原理上说,是因为原始的字符串有无穷多个,这有点象不存在反函数的数学函数。
MD5的典型应用是对一段Message(字节串)产生fingerprint(指纹),以防止被“篡改”。举个例子,你将一段话写在一个叫 readme.txt文件中,并对这个readme.txt产生一个MD5的值并记录在案,然后你可以传播这个文件给别人,别人如果修改了文件中的任何内容,你对这个文件重新计算MD5时就会发现(两个MD5值不相同)。如果再有一个第三方的认证机构,用MD5还可以防止文件作者的“抵赖”,这就是所谓的数字签名应用。
MD5是一种不可逆的加密算法 安全性很高 一般在网上用作判断文件完整性
如果一个文件被修改或不完整 算出来的MD5码是和原来不一样的 所以当你害怕下的东西有病毒或木马或不完整 可以用MD5计算器算一下 再和网站上提供的值对比关于软件很多地方有下的 软件就一个作用 把软件拖进去 然后算出MD5码 。
这个基本上是软件内部利用MD5加密导致,本身几乎无效验方法,因为MD5不可以反编译,方法写个过程,或下载MD5文件 调用MD5(PASSWORD)
MD5中的MD代表Message Digest,就是信息摘要的意思,不过这个信息摘要不是信息内容的缩写,而是根据公开的MD5算法对原信息进行数学变换后得到的一个128位(bit)的特征码。
这个特征码有如下特性,首先它不可逆,例如我有一段秘密的文字如:"My Secret Words",经算法变换后得到MD5码(b9944e9367d2e40dd1f0c4040d4daaf7),把这个码告诉其他人,他们根据这个MD5码是没有系统的方法可以知道你原来的文字是什么的。其次,这个码具有高度的离散性,也就是说,原信息的一点点变化就会导致MD5的巨大变化,例如"ABC" MD5(902fbdd2b1df0c4f70b4a5d23525e932)和"ABC "(多了一空格)MD5(12c774468f981a9487c30773d8093561)差别非常大,而且之间没有任何关系,也就是说产生的MD5码是不可预测的。最后由于这个码有128位那么长,所以任意信息之间具有相同MD5码的可能性非常之低,通常被认为是不可能的。 所以一般认为MD5码可以唯一地代表原信息的特征,通常用于密码的加密存储,数字签名,文件完整性验证等。2004年,已经被山东大学的王小云教授破解了。比如:2345 两个相加的和再相乘 2+3=5 4+5=9 5*9=45
私钥用来加签解密,公钥用来验签加密
证书体系
1.带有私钥的证书
由Public Key Cryptography Standards #12,PKCS#12标准定义,包含了公钥和私钥的二进制格式的证书形式,以pfx作为证书文件后缀名。
2.二进制编码的证书
证书中没有私钥,DER 编码二进制格式的证书文件,以cer作为证书文件后缀名。
3.Base64编码的证书
证书中没有私钥,BASE64 编码格式的证书文件,也是以cer作为证书文件后缀名。
由定义可以看出,只有pfx格式的数字证书是包含有私钥的,cer格式的数字证书里面只有公钥没有私钥。
解析pfx文件命令
openssl pkcs12 -export -out ssl2_me.pfx -inkey ssl2_me.key -in ssl2_me.crt
网络隔离
DNS
概念
域名系统(英文:Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。
DMZ
1、DMZ是什么?
英文全名“Demilitarized Zone”,中文含义是“隔离区”。在安全领域的具体含义是“内外网防火墙之间的区域”。
2、DMZ做什么?
DMZ区是一个缓冲区,在DMZ区存放着一些公共服务器,比如论坛等。
用户要从外网访问到的服务,理论上都可以放到DMZ区。
内网可以单向访问DMZ区、外网也可以单向访问DMZ区。
用户要从外网访问到的服务,理论上都可以放到DMZ区。
内网可以单向访问DMZ区、外网也可以单向访问DMZ区。
3、为什么设置DMZ区?
为了安全。做个假设,如果你公司的内网可以从互联网被访问的话,那么还存在什么安全?但是有些对外的服务还必须要能够从外网进行访问,在这种情况下“DMZ区”就应运而生了。
DMZ区是一个区域,她提供了对外服务器存放的位置,有了安全,也有了方便。通过下面DMZ区布置图可以加深理解:
DMZ区是一个区域,她提供了对外服务器存放的位置,有了安全,也有了方便。通过下面DMZ区布置图可以加深理解:
OAuth
JVM
对象
对象的创建
根据new的参数是否能在常量池中定位到一个类的符号引用
如果没有,说明还未定义该类,抛出ClassNotFoundException
如果没有,说明还未定义该类,抛出ClassNotFoundException
检查符号引用对应的类是否加载过
如果没有,则进行类加载
如果没有,则进行类加载
根据方法区的信息确定为该类分配的内存空间大小
从堆中划分一块对应大小的内存空间给该对象
指针碰撞
java堆内存空间规整的情况下使用
Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
空闲列表
java堆空间不规整的情况下使用
对象中的成员变量赋上初始值
设置对象头信息
调用对象的构造函数进行初始化
对象的内存布局
对象头
Mark Word
对象的hashCode
CG年代
锁信息(偏向锁,轻量级锁,重量级锁)
GC标志
Class Metadata Address
指向对象实例的指针
对象的实例数据
对齐填充
对象的访问方式
指针
reference中存储的直接就是对象地址
句柄
java堆中将会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象数据与类型数据各自的具体地址信息
两种方式的额比较
使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销。HotSpot虚拟机使用的是直接指针访问的方式。
内存结构
线程共享区域
堆
新生代
Eden区
Survivor(from)区
设置Survivor是为了减少送到老年代的对象
Survivor(to)区
设置两个Survivor区是为了解决碎片化的问题
eden:survivor:survivor8:1:1
老年代
老年代:新生代=2:1
方法区
运行时常量池
Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
存储信息
符号引用
符号引用包含的常量
类符号引用
方法符号引用
字段符号引用
概念解释
一个java类(假设为People类)被编译成一个class文件时,如果People类引用了Tool类,但是在编译时People类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。
而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,及直接引用地址。
即在编译时用符号引用来代替引用类,在加载时再通过虚拟机获取该引用类的实际地址
以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局是无关的,引用的目标不一定已经加载到内存中。
字面量
文本字符串
String a = "abc",这个abc就是字面量
八种基本类型
int a = 1; 这个1就是字面量
声明为final的常量
静态变量
final类型常量
类信息
类的完整有效名
返回值类型
修饰符(public,private...)
变量名
方法名
方法代码
这个类型直接父类的完整有效名(除非这个类型是interface或是 java.lang.Object,两种情况下都没有父类)
类的直接接口的一个有序列表
线程私有区域
虚拟机栈
栈帧
动态链接
符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。
前提是每一个栈帧内部都要包含一个指向运行时常量池的引用,来支持动态链接的实现。
操作数栈
保存着Java 虚拟机执行过程中的数据
局部变量表
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
存放的信息
基本数据类型
对象引用
returnAddress类型
方法返回地址
方法被调用的位置
方法退出的过程实际上就等同于把当前栈帧出栈
方法退出可能包含的操作
恢复上层方法的局部变量表和操作数栈
把返回值(如果有的话)压入调用者栈帧的操作数栈中
调整PC计数器的值以指向方法调用指令后面的一条指令
异常
线程请求的栈深度大于虚拟机所允许的深度
StackOverflowError
StackOverflowError
JVM动态扩展时无法申请到足够的内存时
OutOfMemoryError
OutOfMemoryError
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
本地方法栈
和虚拟机栈类似,区别是本地方法栈为使用到的Native方法服务
程序计数器
如果线程正在执行的是一个Java方法,则指明当前线程执行的代字节码行数
此内存区域是唯一一个不会出现OutOfMemoryError情况的区域。
如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)
上述三个区域的生命周期和线程相同
直接内存
使用Native函数库直接分配堆外内存
并不是JVM运行时数据区域的一部分,但是会被频繁使用
避免了在Java 堆和Native 堆中来回复制数据,能够提高效率
内存相关
内存分配
对象优先在Eden区分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间分配时,虚拟机将发起一次Minor GC。
大对象直接进入老年代
最典型的大对象是那种很长的字符串以及数组。
避免在 Eden 区和 Survivor 区之间的大量内存复制。
避免在 Eden 区和 Survivor 区之间的大量内存复制。
长期存活对象进入老年区
如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1,对象在Survivor区中每熬过一次 Minor GC,年龄就增加1,当它的年龄增加到一定程度(默认为15)_时,就会被晋升到老年代中。
对象年龄动态判定
如果在 Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,如果担保失败则会进行一次Full GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
内存回收
Minor GC
特点
发生在新生代上,发生的较频繁,执行速度较快
触发条件
Eden区空间不足
空间分配担保
Full GC
特点
发生在老年代上,较少发生,执行速度较慢
触发条件
调用 System.gc()
老年代区域空间不足
空间分配担保失败
JDK 1.7 及以前的永久代(方法区)空间不足
CMS GC处理浮动垃圾时,如果新生代空间不足,则采用空间分配担保机制,如果老年代空间不足,则触发Full GC
内存溢出
程序在申请内存时,没有足够的内存空间
内存溢出的构造方式
堆溢出
OutOfMemoryError:不断创建对象
栈溢出
StackOverflowError: 增大本地变量表,例如不合理的递归
OutOfMemoryError:不断建立线程
方法区和运行时常量池溢出
OutOfMemoryError:通过String.intern()方法不断向常量池中添加常量,例如String.valueOf(i++).intern()
机内存直接溢出
内存泄漏
程序在申请内存后,无法释放已申请的内存空间
原因
长生命周期的对象持有短生命周期对象的引用
例如将ArrayList设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏
连接未关闭
如数据库连接、网络连接和IO连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。
变量作用域不合理
例如,1.一个变量的定义的作用范围大于其使用范围,2.如果没有及时地把对象设置为null
内部类持有外部类
Java的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏
解决方法
将内部类定义为static
用static的变量引用匿名内部类的实例
或将匿名内部类的实例化操作放到外部类的静态方法中
垃圾收集
7种作用于不同分代的收集器
Serial收集器
-xx:+UseSerialGC指定
一个单线程的收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,进行垃圾收集时必须暂停其他所有的工作线程,直到它收集结束
ParNew收集器
-XX:+UseParNewGC指定
serial收集器的多线程版本,包括serial收集器可用的所有控制参数,收集算法,Stop The World 对象分配规则,回收策略等都与serial收集器完全一样
首选的新生代收集器,可用和cms老年代回收器配合使用,当old带采用CMS GC时new代默认采用ParNew
Parallel Scavenge 收集器
XX:+UseParallelGC指定
新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器
server模式下的默认GC方式
高吞吐量则可用高效地利用cpu时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
CMS收集器
-XX:+UseConcMarkSweepGC指定
Concurrent Mark and Sweep 并发-标记-清除,是一款基于并发,使用标记清除算法的垃圾回收算法,只针对老年代垃圾回收
CMS收集器工作时,尽可能让GC线程和用户线程并发执行,以达到降低STW时间的目的
七个阶段
阶段1:初始化标记(Initial Mark)
阶段2:并发标记(concurrent Mark)
阶段3:并发预清理(concurrent preclean)
阶段4:并发可取消的预清理(concurrent abortable preclean)
阶段5:最终标记(final remark)
阶段6:并发清除(concurrent sweep)
阶段7:并发重置(concurrent reset)
CMS常见问题
最终标记阶段停顿时间过长问题
并发模式失败(concurrent mode failure)& 晋升失败(promotion failed)问题
并发模式失败
晋升失败
内存碎片问题
常见参数
-XX:+UseCMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+ExplicitGCInvokesConcurrentAndUnloadClasses
-XX:CMSScavengeBeforeRemark
-XX:UseCMSCompactAtFullCollection
-XX:+CMSClassUnloadingEnabled
-XX:CMSFullGCsBeforeCompaction
Serial Old 收集器
Serial 收集器的老年代版本
可用于Client模式下
用于Serrver模式下时
在JDK1.5以及之前版本(Parallel Old诞生以前)中与Parllel Scavenge 收集器搭配使用
作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
ParNew Old收集器
Parllel Scavenge 收集器的老年代版本
注重程序的吞吐量
G1收集器
G1(Garbage -First)是一款面向服务器的垃圾收集器,支持新生代和老年代空间的垃圾收集,主要针对配备多核处理器及大容量内存的机器
最主要的设计目标是:实现可预期及可配置的STW停顿时间
G1堆空间划分
Region
巨型对象
每次GC不必都去处理整个堆空间,而是每次只处理一部分region,实现大容量内存的GC
通过计算每个Region的回收价值,包括回收所需时间,可回收空间,在有限时间内尽可能回收更多的垃圾对象,把垃圾回收造成的停顿时间控制在预期配置的时间范围内,这也是G1名称的由来,garbage-first
G1工作模式
Young GC
Mixed GC
全局并发标记
初始化标记(Initail Mark)
根区域扫描(Root region scan)
并发标记(concurrent Marking)
再次标记(Remark)
清理
整理更新每个region各自的Rset(remember set,hashmap结构,记录有哪些老年代对象指向本region,key为指向本region的对象的引用,value为指向本region的具体card区域,通过Rset可以确定region中对象存活信息,避免全堆扫描)
回收不包含存活对象的region
统计计算回收收益高(基于释放空间和暂停目标)的老年代分区集合
G1调优
Full GC问题
原因
程序主动执行System.gc()
全局并发标记期间老年代空间被填满(并发模式失败)
mixed gc期间老年代空间被填满(晋升失败)
young GC时survivor空间和老年代没有足够空间容纳存活对象
解决
增大-XX:concGCtHreads=n 选择增加并发标记线程的数据,或者STW期间并行线程的数量:-XX:ParallelGCThreads=n
减少-XX:InitiatingHeapOccupancyPercent提前启动标记周期
增大预留内存, -XX:G1ResevePercent=n,默认值是10,代表使用10%的堆内存为预留内存,当survivor区域没有足够空间容纳晋升对象时会尝试使用预留内存
巨型对象分配
不要设置young区的大小
平均响应时间设置
常用参数
新生代GC(Minor GC)
触发条件
当年轻代Eden区域满的时候
老年代(major GC/Full GC)
触发条件
显示调用System.gc方法
方法区空间不足(JDK8及之后不会有这种情况了)
老年代空间不足
吞吐量
判断对象是需要回收
引用计数法已经不用了
可达性分析法
GC Roots
JVM栈中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象
判断一个对象是否可回收的过程
1 找到GC Root 不可达对象,如果没有重写finalize()或者调用过finalize(),则将该对象加入到F-Queue中
2 再次进行标记,如果此时对象还未与GC Root建立引用关系,则被回收
GC日志
Young GC日志
Full GC日志
Stop the word
GC过程中分析对象引用关系,为了保证分析结果的准确性,需要通过停顿所有java执行线程,保证引用关系不再动态变化,该停顿事件称为stop the world (STW)
safpoint
代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前状态是安全的,如果有需要GC,线程可以在这个位置暂停,hotspot采用主动中断的方式,让执行线程在运行期间轮询是否需要暂停的标志,若需要则终端挂起
类加载机制
类的生命周期
加载
验证
准备
解析
初始化
使用
卸载
类加载器
启动类加载器
C++实现,是虚拟机自身的一部分
负责将存放在<JRE_HOME>\li目录中的类库加载都虚拟机内存中
其他加载器
由java实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.classloader
分类
启动类加载器
扩展类加载器
它负责将<JAVA_HOME>/lib/ext 或者被java.ext.dir 系统变量所指定路径中的所有类库加载到内存中
应用程序类加载器
它负责加载用户类路径(classPath)上所有指定的类库
自定义类加载器
用户根据需求自己定义的,也需要继承自classLoader
双亲委派模型
内容
如果一个雷加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给弗雷加载器完成,只有当父加载器在自己的搜素范围内找不到指定的类时,(即classnotfoundexception),子加载器才会尝试自己去加载
实现
首先检查类是否被加载
若未加载,则调用弗雷加载器的loadclass方法
若该方法抛出classnotfoundexception异常,则表示父类加载器无法加载,则当前类加载器调用findclass加载类
若弗雷加载器可以加载,则直接返回class对象
好处
保证java类库中的类不受用户类影响,防止用户自定义一个类库中的同名类,引起问题
破坏
基础类需要调用用户代码
解决方式
线程上下文类加载器
也就是父类加载器请求了子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则
实现方法
重写classloader类的loadclass()
子主题
子主题
子主题
子主题
类加载过程
加载
将编译后的.class静态文件转换到内存中(方法区),然后暴露出来让程序员能访问到
验证
确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
准备
准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存
解析
将class文件的常量池的符合引用替换为直接引用的过程(是静态链接)
可能发生在初始化阶段之前,也可能发生在初始化之后,后者是为了支持java的动态绑定
初始化
为类的静态变量赋予程序中指定的初始值,还有执行静态代码块中的程序(执行<cinit>()方法)
类加载方式
1 命令行启动应用由JVM初始化加载
2 通过class.forName()方法动态加载
3 通过classLoader.loadClass()方法动态加载
类加载时机
遇到new,getStatic,putStatic,invokeStatic这四条指令
new一个对象时
调用一个类的静态方法
直接操作一个类的static属性
使用java.lang.reflect进行反射调用
初始化类时,没有初始化父类,先初始化父类
虚拟机启动时,用户指定的主类(main)
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.class对象,用来封装类在方法区内的数据结构,类的加载的最终产品是位于堆中的class对象。class对象封装了类在方法区内的数据结构,并且向java程序员提供了访问方法区的数据结构的接口。
class文件结构
魔数
唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。
版本号
常量池
字面量
符号引用
访问标志
用于识别一些类或接口层次的访问信息
是否final
是否public,否则是private
是否是接口
是否可用invokespecial字节码指令
是否是abstact
是否是注解
是否是枚举
类索引,父类索引,接口索引集合
这三项数据主要用于确定这个类的继承关系。
类索引
用于确定这个类的全限定名
父类索引
用于确定这个类父类的全限定名
接口索引
描述这个类实现了哪些接口
字段表集合
表结构
访问标志
名称索引
描述符索引
属性表集合
字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的变量
简单来说,字段表集合存储字段的修饰符+名称
变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示。
方法表集合
访问标志
名称索引
描述符索引
属性表集合
Java代码经过编译器编译为字节码之后,存储在方法属性表集合中一个名叫“Code”的属性中
属性表集合
在 Class 文件、字段表、方法表都可以携带子集的属性表集合,以用于描述某些场景专有的信息。
JVM调优
常见参数
Xms
Xmx
Xmn
Xss
-XX:SurvivorRatio
-XX:NewRatio
-XX:+PrintGCDetails
-XX:ParallelGCThreads
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseG1GC
-XX:MaxGCPauseMillis
调优思路
确定是否有频繁Full GC现象
1.1 如果Full GC频繁,那么考虑内存泄漏的情况
内存泄露角度
1.使用jps -l命令获取虚拟机的LVMID
2.使用jstat -gc lvmid命令获取虚拟机的执行状态,判断full GC次数
使用jmap -histo:live 分析当前堆中存活对象数量
4.如果还不能定位到关键信息,使用 jmap -dump打印出当前堆栈映像dump文件
jmap -dump:format=b,file=/usr/local/base/02.hprof 12942
5.使用MAT等工具分析dump文件,一般使用的参数是Histogram或者Dominator Tree,分析出各个对象的内存占用率,并根据对象的引用情况找到泄漏点
1.2 如果Full GC并不频繁,各个区域内存占用也很正常,那么考虑线程阻塞,死锁,死循环等情况
线程角度
1.使用jps -l命令获取虚拟机的LVMID
2.使用 jstack 分析各个线程的堆栈内存使用情况,如果说系统慢,那么要特别关注Blocked,Waiting on condition,如果说系统的cpu耗的高,那么肯定是线程执行有死循环,那么此时要关注下Runable状态。
3.如果还不能定位到关键信息,使用 jmap -dump打印出当前堆栈映像dump文件
4.使用MAT等工具分析dump文件,一般使用的参数是Histogram或者Dominator Tree,分析出各个对象的内存占用率,并根据对象的引用情况找到泄漏点。
1.3 如果都不是,考虑堆外存溢出,或者是外部命令等情况
Runtime.getRuntime.exec()
内存分配策略
对象优先在Eden区分配
大多数情况下,对象先在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Young GC
大对象之间进入老年代
JVM提供了一个对象大小阈值参数(-XX:PretenureSizeThreshold,默认值为0,代表不管多大都是先在Eden中分配内存),大于参数设置的阈值值的对象直接在老年代分配,这样可以避免对象在Eden及两个Survivor直接发生大内存复制
长期存活的对象将进入老年代
对象每经历一次垃圾回收,且没被回收掉,它的年龄就增加1,大于年龄阈值参数(-XX:maxTenuring Threshold,默认15)的对象,将晋升到老年代中
空间分配担保
当进行Young GC之前,JVM需要预估:老年代是否能够容纳Young GC后新生代晋升到老年代的存活对象,以确定是否需要提前触发GC回收老年代空间,基于空间分配担保策略来计算
空间分配担保策略
动态年龄判定
新生代对象的年龄可能没达到阈值(MacT恩uringThreshold参数指定)就晋升老年代
并发
并发(concurrency)
并行(parallel)
java内存模型
CPU多级缓存和缓存一致性
处理器优化和指令重排
原子性
可见性
有序性
解决并发问题主要采用两种方式
限制处理器优化
使用内存屏障
其他知识
动态绑定
指的是在程序运行过程中,根据具体的实例对象才能具体确定是哪个方法。
编译阶段,根据引用本身的类型(Father)在方法表中查找匹配的方法,如果存在则编译通过
运行阶段,根据实例变量的类型(Son)在方法表中查找匹配的方法,如果实例变量重写了方法,则调用重写的方法,否则调用父类方法
以 Father ft=new Son();ft.say();为例
表中记录了这个类定义的方法的指针,每个表项指向一个具体的方法代码。如果这个类重写了父类中的某个方法,则对应表项指向新的代码实现处。从父类继承来的方法位于子类定义的方法的前面。
参数传递
值传递
引用传递
Java 在参数传递的时候,实际上是传递的当前引用的一个拷贝
如果参数是基本类型,传递的是基本类型的字面量值的拷贝。
如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值的拷贝。
常用工具
jenkins
java基础
数据类型
基本类型
8大基本数据类型
数值型
整数类型
字节(byte)8位
存储空间:8位
表数范围:-128~127
表数范围:-128~127
短整型(short)
存储空间:16位
表数范围:-2^15~2^15-1
表数范围:-2^15~2^15-1
整型(int)
存储空间:32位
表数范围:-2^31~2^31-1
表数范围:-2^31~2^31-1
长整型(long)
存储空间:64位
表数范围:-2^63~2^63-1
表数范围:-2^63~2^63-1
浮点类型
单精度浮点数(float)
存储空间:32位
表数范围:-3.403E38~3.403E38
表数范围:-3.403E38~3.403E38
双精度浮点数(double)
存储空间:64位
表数范围:-17.98E308~17.98E308
表数范围:-17.98E308~17.98E308
字符型(char)
存储空间:16位
取值范围:0~65535
取值范围:0~65535
布尔型(boolean)
存储空间:1位
取值范围:true 和 false
引用数据类型
类(Class)
Class 类与类的关系
Java程序运行时,系统一直对所有的对象进行所谓的运行时类型标识。这项信息纪录了每个对象所属的类。虚拟机通常使用运行时类型信息选准正确方法去执行,用来保存这些类型信息的类是Class类。Class类封装一个对象和接口运行时的状态,当装载类时,Class类型的对象自动创建。说白了,Class类对象就是封装了一个类的类型信息,可以通过该对象操作其对应的类,即反射机制。
Class 类是在Java语言中定义一个特定类的实现。一个类的定义包含成员变量,成员方法,还有这个类实现的接口,以及这个类的父类。Class类的对象用于表示当前运行的 Java 应用程序中的类和接口。Class类提取这些类的一些共同特征,比如说这些类都有类名,都有对应的hashcode,可以判断类型属于class、interface、enum还是annotation。这些可以封装成Class类的域,另外可以定义一些方法,比如获取某个方法、获取类型名等等。这样就封装了一个表示类型(type)的类。程序员可以在程序运行时发现和使用类型信息。
在java中,每个类都有一个相应的Class对象。也就是说,当编写一个类,编译完成后,在生成的.class文件中,就会产生一个Class对象,用于表示这个类的类型信息。Class类的每个实例则代表运行中的一个类。
Class 没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。
虚拟机为每种类型管理一个独一无二的Class对象。也就是说,每个类(型)都有一个Class对象。运行程序时,Java虚拟机(JVM)首先检查是否所要加载的类对应的Class对象是否已经加载。如果没有加载,JVM就会根据类名查找.class文件,并将其Class对象载入。虚拟机只会产生一份字节码, 用这份字节码可以产生多个实例对象。
基本的 Java 类型(boolean、byte、char、short、int、long、float 和 double)和关键字 void 也都对应一个 Class 对象。 每个数组属于被映射为 Class 对象的一个类,所有具有相同元素类型和维数的数组都共享该 Class 对象。
Class 类是在Java语言中定义一个特定类的实现。一个类的定义包含成员变量,成员方法,还有这个类实现的接口,以及这个类的父类。Class类的对象用于表示当前运行的 Java 应用程序中的类和接口。Class类提取这些类的一些共同特征,比如说这些类都有类名,都有对应的hashcode,可以判断类型属于class、interface、enum还是annotation。这些可以封装成Class类的域,另外可以定义一些方法,比如获取某个方法、获取类型名等等。这样就封装了一个表示类型(type)的类。程序员可以在程序运行时发现和使用类型信息。
在java中,每个类都有一个相应的Class对象。也就是说,当编写一个类,编译完成后,在生成的.class文件中,就会产生一个Class对象,用于表示这个类的类型信息。Class类的每个实例则代表运行中的一个类。
Class 没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。
虚拟机为每种类型管理一个独一无二的Class对象。也就是说,每个类(型)都有一个Class对象。运行程序时,Java虚拟机(JVM)首先检查是否所要加载的类对应的Class对象是否已经加载。如果没有加载,JVM就会根据类名查找.class文件,并将其Class对象载入。虚拟机只会产生一份字节码, 用这份字节码可以产生多个实例对象。
基本的 Java 类型(boolean、byte、char、short、int、long、float 和 double)和关键字 void 也都对应一个 Class 对象。 每个数组属于被映射为 Class 对象的一个类,所有具有相同元素类型和维数的数组都共享该 Class 对象。
反射机制
定义
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。指的是我们可以于运行时加载、探知、使用编译期间完全未知的classes。换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设值、或唤起其methods。
与Class类关系
Java程序在运行时,Java运行时系统一直对所有的对象进行所谓的运行时类型标识。这项信息纪录了每个对象所属的类。虚拟机通常使用运行时类型信息选准正确方法去执行,用来保存这些类型信息的类是Class类。也就是说,ClassLoader找到了需要调用的类时(java为了调控内存的调用消耗,类的加载都在需要时再进行,很抠但是很有效),就会加载它,然后根据.class文件内记载的类信息来产生一个与该类相联系的独一无二的Class对象。该Class对象记载了该类的字段,方法等等信息。以后jvm要产生该类的实例,就是根据内存中存在的该Class类所记载的信息(Class对象应该和我所了解的其他类一样会在堆内存内产生、消亡)来进行。而java中的Class类对象是可以人工自然性的(也就是说开放的)得到的(虽然你无法像其他类一样运用构造器来得到它的实例,因为Class对象都是jvm产生的。不过话说回来,客户产生的话也是无意义的),而且,更伟大的是,基于这个基础,java实现了反射机制。
反射机制功能
在运行时判断任意一个对象所属的类
在运行时构造任意一个类的对象
在运行时判断任意一个类所具有的成员变量和方法
在运行时调用任意一个对象的方法
在运行时构造任意一个类的对象
在运行时判断任意一个类所具有的成员变量和方法
在运行时调用任意一个对象的方法
反射机制API
在JDK中,主要由以下类来实现Java反射机制,这些类(除了第一个)都位于java.lang.reflect包中:
Class类:代表一个类,位于java.lang包下
Field类:代表类的成员变量(成员变量也称为类的属性)
Method类:代表类的方法
Constructor类:代表类的构造方法
Array类:提供了动态创建数组,以及访问数组的元素的静态方法
Class类:代表一个类,位于java.lang包下
Field类:代表类的成员变量(成员变量也称为类的属性)
Method类:代表类的方法
Constructor类:代表类的构造方法
Array类:提供了动态创建数组,以及访问数组的元素的静态方法
获取Class对象
1 、调用Object类的getClass()方法来得到 Class 对象 ,这也是最常见的产生 Class 对象 的方法。
使用此种方式的前提是我们需要产生相应的实例对象。 例如:
MyObject x;
Class c1 = x.getClass();
MyObject x;
Class c1 = x.getClass();
2 、使用Class类的中静态forName()方法获得与字符串对应的 Class 对象 。
产生Class对象的同时会对该Class对象进行初始化(若以前没有初始化过)。 例如:
Class c2=Class.forName("MyObject"), MyObject必须是接口或者类的名字。
Class c2=Class.forName("MyObject"), MyObject必须是接口或者类的名字。
3 、获取Class类型对象的第三个方法非常简单。如果T是一个 Java 类型,那么T.class就代表了匹配的类对象。
此种方式产生Class对象较前两种简单,无需第一种还要进行异常处理(在编译期就进行错误的检查了)。此种方式不会自动初始化该Class对象。初始化被延迟到对静态方法(包括构造方法)或非常数静态域进行首次引用时才进行. 例如
Class cl1 = Manager.class;
Class cl2 = int.class;
Class cl3 = Double[].class;
注意: Class 对象 实际上描述的只是类型,而这类型未必是类或者接口。例如上面的int.class是一个Class类型的对象。由于历史原因,数组类型的getName方法会返回奇怪的名字。
Class cl1 = Manager.class;
Class cl2 = int.class;
Class cl3 = Double[].class;
注意: Class 对象 实际上描述的只是类型,而这类型未必是类或者接口。例如上面的int.class是一个Class类型的对象。由于历史原因,数组类型的getName方法会返回奇怪的名字。
Class类的常用方法
getName(),一个Class对象描述了一个特定类的特定属性,而这个方法就是返回String形式的该类的简要描述。
newInstance(),可以根据某个Class对象产生其对应类的实例。需要强调的是,它调用的是此类的默认构造方法。例如:
MyObject x = new MyObject();
MyObject y = x.getClass().newInstance();
getClassLoader(),返回该Class对象对应的类的类加载器。
getSuperClass(),返回某子类所对应的直接父类所对应的Class对象
isArray(),判定此Class对象所对应的是否是一个数组对象
newInstance(),可以根据某个Class对象产生其对应类的实例。需要强调的是,它调用的是此类的默认构造方法。例如:
MyObject x = new MyObject();
MyObject y = x.getClass().newInstance();
getClassLoader(),返回该Class对象对应的类的类加载器。
getSuperClass(),返回某子类所对应的直接父类所对应的Class对象
isArray(),判定此Class对象所对应的是否是一个数组对象
基本方法
getClassLoader()
获取该类的类装载器。
getComponentType()
如果当前类表示一个数组,则返回表示该数组组件的 Class 对象,否则返回 null。
getConstructor(Class[])
返回当前 Class 对象表示的类的指定的公有构造子对象。
getConstructors()
返回当前 Class 对象表示的类的所有公有构造子对象数组。
getDeclaredConstructor(Class[])
返回当前 Class 对象表示的类的指定已说明的一个构造子对象。
getDeclaredConstructors()
返回当前 Class 对象表示的类的所有已说明的构造子对象数组。
getDeclaredField(String)
返回当前 Class 对象表示的类或接口的指定已说明的一个域对象。
getDeclaredFields()
返回当前 Class 对象表示的类或接口的所有已说明的域对象数组。
getDeclaredMethod(String, Class[])
返回当前 Class 对象表示的类或接口的指定已说明的一个方法对象。
getDeclaredMethods()
返回 Class 对象表示的类或接口的所有已说明的方法数组。
getField(String)
返回当前 Class 对象表示的类或接口的指定的公有成员域对象。
getFields()
返回当前 Class 对象表示的类或接口的所有可访问的公有域对象数组。
getInterfaces()
返回当前对象表示的类或接口实现的接口。
getMethod(String, Class[])
返回当前 Class 对象表示的类或接口的指定的公有成员方法对象。
getMethods()
返回当前 Class 对象表示的类或接口的所有公有成员方法对象数组,包括已声明的和从父类继承的方法。
getModifiers()
返回该类或接口的 Java 语言修改器代码。
getName()
返回 Class 对象表示的类型(类、接口、数组或基类型)的完整路径名字符串。
getResource(String)
按指定名查找资源。
getResourceAsStream(String)
用给定名查找资源。
getSigners()
获取类标记。
getSuperclass()
如果此对象表示除 Object 外的任一类, 那么返回此对象的父类对象。
isArray()
如果 Class 对象表示一个数组则返回 true, 否则返回 false。
isAssignableFrom(Class)
判定 Class 对象表示的类或接口是否同参数指定的 Class 表示的类或接口相同,或是其父类。
isInstance(Object)
此方法是 Java 语言 instanceof 操作的动态等价方法。
isInterface()
判定指定的 Class 对象是否表示一个接口类型。
isPrimitive()
判定指定的 Class 对象是否表示一个 Java 的基类型。
newInstance()
创建类的新实例。
toString()
将对象转换为字符串。
获取该类的类装载器。
getComponentType()
如果当前类表示一个数组,则返回表示该数组组件的 Class 对象,否则返回 null。
getConstructor(Class[])
返回当前 Class 对象表示的类的指定的公有构造子对象。
getConstructors()
返回当前 Class 对象表示的类的所有公有构造子对象数组。
getDeclaredConstructor(Class[])
返回当前 Class 对象表示的类的指定已说明的一个构造子对象。
getDeclaredConstructors()
返回当前 Class 对象表示的类的所有已说明的构造子对象数组。
getDeclaredField(String)
返回当前 Class 对象表示的类或接口的指定已说明的一个域对象。
getDeclaredFields()
返回当前 Class 对象表示的类或接口的所有已说明的域对象数组。
getDeclaredMethod(String, Class[])
返回当前 Class 对象表示的类或接口的指定已说明的一个方法对象。
getDeclaredMethods()
返回 Class 对象表示的类或接口的所有已说明的方法数组。
getField(String)
返回当前 Class 对象表示的类或接口的指定的公有成员域对象。
getFields()
返回当前 Class 对象表示的类或接口的所有可访问的公有域对象数组。
getInterfaces()
返回当前对象表示的类或接口实现的接口。
getMethod(String, Class[])
返回当前 Class 对象表示的类或接口的指定的公有成员方法对象。
getMethods()
返回当前 Class 对象表示的类或接口的所有公有成员方法对象数组,包括已声明的和从父类继承的方法。
getModifiers()
返回该类或接口的 Java 语言修改器代码。
getName()
返回 Class 对象表示的类型(类、接口、数组或基类型)的完整路径名字符串。
getResource(String)
按指定名查找资源。
getResourceAsStream(String)
用给定名查找资源。
getSigners()
获取类标记。
getSuperclass()
如果此对象表示除 Object 外的任一类, 那么返回此对象的父类对象。
isArray()
如果 Class 对象表示一个数组则返回 true, 否则返回 false。
isAssignableFrom(Class)
判定 Class 对象表示的类或接口是否同参数指定的 Class 表示的类或接口相同,或是其父类。
isInstance(Object)
此方法是 Java 语言 instanceof 操作的动态等价方法。
isInterface()
判定指定的 Class 对象是否表示一个接口类型。
isPrimitive()
判定指定的 Class 对象是否表示一个 Java 的基类型。
newInstance()
创建类的新实例。
toString()
将对象转换为字符串。
接口(interface)
java中abstract和interface的区别
1.相同点
A. 两者都是抽象类,都不能实例化。
B. interface实现类及abstrctclass的子类都必须要实现已经声明的抽象方法。
2. 不同点
A. interface需要实现,要用implements,而abstract class需要继承,要用extends。
B. 一个类可以实现多个interface,但一个类只能继承一个abstract class。
C. interface强调特定功能的实现,而abstractclass强调所属关系。
D. 尽管interface实现类及abstrct class的子类都必须要实现相应的抽象方法,但实现的形式不同。interface中的每一个方法都是抽象方法,都只是声明的(declaration,没有方法体),实现类必须要实现。而abstractclass的子类可以有选择地实现。
这个选择有两点含义:
一是Abastract class中并非所有的方法都是抽象的,只有那些冠有abstract的方法才是抽象的,子类必须实现。那些没有abstract的方法,在Abstrct class中必须定义方法体。
二是abstract class的子类在继承它时,对非抽象方法既可以直接继承,也可以覆盖;而对抽象方法,可以选择实现,也可以通过再次声明其方法为抽象的方式,无需实现,留给其子类来实现,但此类必须也声明为抽象类。既是抽象类,当然也不能实例化。
E. abstract class是interface与Class的中介。
interface是完全抽象的,只能声明方法,而且只能声明pulic的方法,不能声明private及protected的方法,不能定义方法体,也不能声明实例变量。然而,interface却可以声明常量变量,并且在JDK中不难找出这种例子。但将常量变量放在interface中违背了其作为接口的作用而存在的宗旨,也混淆了interface与类的不同价值。如果的确需要,可以将其放在相应的abstractclass或Class中。
abstract class在interface及Class中起到了承上启下的作用。一方面,abstract class是抽象的,可以声明抽象方法,以规范子类必须实现的功能;另一方面,它又可以定义缺省的方法体,供子类直接使用或覆盖。另外,它还可以定义自己的实例变量,以供子类通过继承来使用。
1.相同点
A. 两者都是抽象类,都不能实例化。
B. interface实现类及abstrctclass的子类都必须要实现已经声明的抽象方法。
2. 不同点
A. interface需要实现,要用implements,而abstract class需要继承,要用extends。
B. 一个类可以实现多个interface,但一个类只能继承一个abstract class。
C. interface强调特定功能的实现,而abstractclass强调所属关系。
D. 尽管interface实现类及abstrct class的子类都必须要实现相应的抽象方法,但实现的形式不同。interface中的每一个方法都是抽象方法,都只是声明的(declaration,没有方法体),实现类必须要实现。而abstractclass的子类可以有选择地实现。
这个选择有两点含义:
一是Abastract class中并非所有的方法都是抽象的,只有那些冠有abstract的方法才是抽象的,子类必须实现。那些没有abstract的方法,在Abstrct class中必须定义方法体。
二是abstract class的子类在继承它时,对非抽象方法既可以直接继承,也可以覆盖;而对抽象方法,可以选择实现,也可以通过再次声明其方法为抽象的方式,无需实现,留给其子类来实现,但此类必须也声明为抽象类。既是抽象类,当然也不能实例化。
E. abstract class是interface与Class的中介。
interface是完全抽象的,只能声明方法,而且只能声明pulic的方法,不能声明private及protected的方法,不能定义方法体,也不能声明实例变量。然而,interface却可以声明常量变量,并且在JDK中不难找出这种例子。但将常量变量放在interface中违背了其作为接口的作用而存在的宗旨,也混淆了interface与类的不同价值。如果的确需要,可以将其放在相应的abstractclass或Class中。
abstract class在interface及Class中起到了承上启下的作用。一方面,abstract class是抽象的,可以声明抽象方法,以规范子类必须实现的功能;另一方面,它又可以定义缺省的方法体,供子类直接使用或覆盖。另外,它还可以定义自己的实例变量,以供子类通过继承来使用。
接口可以实现多继承,即一个接口可以extends多个接口
数组
数组的定义及初始化
首先,我们要了解什么是数组。数组是存放相同类型数据的集合,数组的内存分布是连续的。
我们创建数组一般有三种方式:
int[] arr = new int[]{1,2,3}; //动态初始化
int[] arr = {1,2,3}; //静态初始化 也产生了对象
int[] arr = new int[3]; //定义数组
int[] arr = {1,2,3}; //静态初始化 也产生了对象
int[] arr = new int[3]; //定义数组
即数组的初始化可以:
1、动态初始化:数据类型[] 数据名称 = new 数据类型 {初始化数据};
2、静态初始化:数据类型[] 数据名称 = {初始化数据}; (比较常用)
3、定义数组:要注意定义数组并没有初始化数组,只是指定了数组的大小,即就是在堆上给数组开辟了一块连续的空间。
1、动态初始化:数据类型[] 数据名称 = new 数据类型 {初始化数据};
2、静态初始化:数据类型[] 数据名称 = {初始化数据}; (比较常用)
3、定义数组:要注意定义数组并没有初始化数组,只是指定了数组的大小,即就是在堆上给数组开辟了一块连续的空间。
数组的使用
1.获取数组arr长度
int[] arr = {1,2,3};
System.out.println(arr.length);
System.out.println(arr.length);
2.访问数组通过下标来访问,注意数组下标是从0开始的,下标属于[0,length-1]
int[] arr = {1,2,3};
System.out.println(arr);
System.out.println(arr);
这会输出什么呢---[I@16d3586 其实这就类似于c语言中数组存放首元素的地址,不过这个诡异的数字是地址的哈希码,而且地址和哈希码是一一对应的关系。
3.遍历数组:遍历数组就是访问数组的元素,有三种方式
①用for循环遍历数组
int[] arr = {1,2,3};
for (int i = 0; i < arr.length ; i++) {
System.out.println(arr[i]);
}
for (int i = 0; i < arr.length ; i++) {
System.out.println(arr[i]);
}
②for-each循环遍历数组
int[] arr = {1,2,3};
//x是用来接收arr数组中的元素,arr是要遍历的数组
for (int x: arr) {
System.out.println(x);
}
//x是用来接收arr数组中的元素,arr是要遍历的数组
for (int x: arr) {
System.out.println(x);
}
③Array.toString()直接访问数组--使用时必须导入java.util.Arrays包
int[] arr = {1,2,3};
System.out.println(Arrays.toString(arr)); //将数组以字符串形式输出
System.out.println(Arrays.toString(arr)); //将数组以字符串形式输出
数组作为方法的参数及返回值
数组作为方法的参数:
public static void printArr(int[] arr1) {
for (int x: arr1) {
System.out.println(x);
}
}
public static void main(String[] args) {
int[] arr = {1,2,3};
printArr(arr);
}
for (int x: arr1) {
System.out.println(x);
}
}
public static void main(String[] args) {
int[] arr = {1,2,3};
printArr(arr);
}
这样可以依次打印出数组的元素,实际上就是这样的:方法名arr是一个引用,传参的时候按照引用传参。
数组作为方法的返回值:
我们原来可以返回int,float,boolean ....,其实数组也可以当做参数返回的。注意返回的是你new出来的数组,接收当然必须也是同类型数组。
public static int[] transform(int[] arr) { //不改变原来的数组把数组大小扩大为原来的两倍,并返回
int[] tmp = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
tmp[i] = arr[i]*2;
}
return tmp;
}
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
int[] arr2 = transform(arr);
System.out.println(Arrays.toString(arr2));
}
int[] tmp = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
tmp[i] = arr[i]*2;
}
return tmp;
}
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
int[] arr2 = transform(arr);
System.out.println(Arrays.toString(arr2));
}
数组应用
数组转字符串:方便打印
public static void main(String[] args) {
int[] arr = {1,2,3};
String newArr = Arrays.toString(arr);
System.out.println(newArr);
}
int[] arr = {1,2,3};
String newArr = Arrays.toString(arr);
System.out.println(newArr);
}
如何自己实现数组转化为字符串呢?其实就是遍历数组字符串拼接的工作啦。。
public static String tostring(int[] arr) {
String ret = "[";
for (int i = 0; i < arr.length; i++) {
ret += arr[i];
if(i != arr.length-1) {
ret += ",";
}
}
ret += "]";
return ret;
}
public static void main(String[] args) {
int[] arr = {1,2,3};
System.out.println(tostring(arr));
}
String ret = "[";
for (int i = 0; i < arr.length; i++) {
ret += arr[i];
if(i != arr.length-1) {
ret += ",";
}
}
ret += "]";
return ret;
}
public static void main(String[] args) {
int[] arr = {1,2,3};
System.out.println(tostring(arr));
}
数组拷贝:
数组的拷贝共有四种方式,对于数组当中如果是简单类型,就是深拷贝。如果是引用类型,那么就是浅拷贝。
1、for循环进行拷贝
int[] array = {1,2,3,4,5};
int[] array2 = new int[array.length];
for (int i = 0; i < array.length; i++) {
array2[i] = array[i];
}
System.out.println(Arrays.toString(array2));
int[] array2 = new int[array.length];
for (int i = 0; i < array.length; i++) {
array2[i] = array[i];
}
System.out.println(Arrays.toString(array2));
2、System.arraycopy拷贝
arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
//src:代表源数组
//srcPos:从源数组的pos位置开始拷贝
//dest:目的数组
//destPos:目的数组的位置
//length:要拷贝的长度
//src:代表源数组
//srcPos:从源数组的pos位置开始拷贝
//dest:目的数组
//destPos:目的数组的位置
//length:要拷贝的长度
int[] array = {1,2,3,4,5};
int[] array2 = new int[array.length];
System.arraycopy(array,0,array2,0,array.length);
System.out.println(Arrays.toString(array2));
int[] array2 = new int[array.length];
System.arraycopy(array,0,array2,0,array.length);
System.out.println(Arrays.toString(array2));
3、Array.copyOf拷贝
copyOf(int[] original, int newLength)
//original:源数组
//newLength:拷贝的长度
//original:源数组
//newLength:拷贝的长度
int[] arr = {1,2,3};
int[] arr2 = Arrays.copyOf(arr,arr.length); //arr2只是存放拷贝完的引用
System.out.println(Arrays.toString(arr2));
int[] arr2 = Arrays.copyOf(arr,arr.length); //arr2只是存放拷贝完的引用
System.out.println(Arrays.toString(arr2));
4、数组名.clone clone是Object的方法,返回一个副本
int[] arr = {1,2,3};
int[] arr2 = arr.clone();
System.out.println(Arrays.toString(arr2));
int[] arr2 = arr.clone();
System.out.println(Arrays.toString(arr2));
arrayCopy和copyOf的区别是什么?
①返回值:arrayCopy返回类型是void,copyOf返回类型是int[](具体的类型)
②代码层次:Array.copyOf()底层调用System.arraycopy()
③速度:System.arraycopy()速度更快(native)
这四种拷贝方式对于简单类型是深拷贝,直接运用于引用类型来说是浅拷贝,若要深拷贝,还要拷贝对象。
②代码层次:Array.copyOf()底层调用System.arraycopy()
③速度:System.arraycopy()速度更快(native)
这四种拷贝方式对于简单类型是深拷贝,直接运用于引用类型来说是浅拷贝,若要深拷贝,还要拷贝对象。
浅拷贝只是拷贝了引用,两个引用指向同一块空间,通过其中一个引用改变对象的值,通过另一个引用访问该对象时值也是改变的;深拷贝不仅拷贝了其引用,还拷贝了其对象。通过一个引用改变对象的值,不影响另一个引用和对象。
二维数组
枚举(enum)
常量
把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。
public enum Color {
RED, GREEN, BLANK, YELLOW
}
RED, GREEN, BLANK, YELLOW
}
switch
JDK1.6之前的switch语句只支持int,char,enum类型,使用枚举,能让我们的代码可读性更强。
enum Signal {
GREEN, YELLOW, RED
}
public class TrafficLight {
Signal color = Signal.RED;
public void change() {
switch (color) {
case RED:
color = Signal.GREEN;
break;
case YELLOW:
color = Signal.RED;
break;
case GREEN:
color = Signal.YELLOW;
break;
}
}
}
GREEN, YELLOW, RED
}
public class TrafficLight {
Signal color = Signal.RED;
public void change() {
switch (color) {
case RED:
color = Signal.GREEN;
break;
case YELLOW:
color = Signal.RED;
break;
case GREEN:
color = Signal.YELLOW;
break;
}
}
}
向枚举中添加新方法
如果打算自定义自己的方法,那么必须在enum实例序列的最后添加一个分号。而且 Java 要求必须先定义 enum 实例。
public enum Color {
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
// 普通方法
public static String getName(int index) {
for (Color c : Color.values()) {
if (c.getIndex() == index) {
return c.name;
}
}
return null;
}
// get set 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
// 普通方法
public static String getName(int index) {
for (Color c : Color.values()) {
if (c.getIndex() == index) {
return c.name;
}
}
return null;
}
// get set 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}
覆盖枚举的方法
public enum Color {
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
//覆盖方法
@Override
public String toString() {
return this.index+"_"+this.name;
}
}
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
//覆盖方法
@Override
public String toString() {
return this.index+"_"+this.name;
}
}
实现接口
所有的枚举都继承自java.lang.Enum类。由于Java 不支持多继承,所以枚举对象不能再继承其他类。
public interface Behaviour {
void print();
String getInfo();
}
public enum Color implements Behaviour{
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
//接口方法
@Override
public String getInfo() {
return this.name;
}
//接口方法
@Override
public void print() {
System.out.println(this.index+":"+this.name);
}
}
void print();
String getInfo();
}
public enum Color implements Behaviour{
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
//接口方法
@Override
public String getInfo() {
return this.name;
}
//接口方法
@Override
public void print() {
System.out.println(this.index+":"+this.name);
}
}
使用接口组织枚举
public interface Food {
enum Coffee implements Food{
BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO
}
enum Dessert implements Food{
FRUIT, CAKE, GELATO
}
}
enum Coffee implements Food{
BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO
}
enum Dessert implements Food{
FRUIT, CAKE, GELATO
}
}
/**
* 测试继承接口的枚举的使用(by 大师兄 or 大湿胸。)
*/
private static void testImplementsInterface() {
for (Food.DessertEnum dessertEnum : Food.DessertEnum.values()) {
System.out.print(dessertEnum + " ");
}
System.out.println();
//我这地方这么写,是因为我在自己测试的时候,把这个coffee单独到一个文件去实现那个food接口,而不是在那个接口的内部。
for (CoffeeEnum coffee : CoffeeEnum.values()) {
System.out.print(coffee + " ");
}
System.out.println();
//搞个实现接口,来组织枚举,简单讲,就是分类吧。如果大量使用枚举的话,这么干,在写代码的时候,就很方便调用啦。
//还有就是个“多态”的功能吧,
Food food = Food.DessertEnum.CAKE;
System.out.println(food);
food = CoffeeEnum.BLACK_COFFEE;
System.out.println(food);
}
* 测试继承接口的枚举的使用(by 大师兄 or 大湿胸。)
*/
private static void testImplementsInterface() {
for (Food.DessertEnum dessertEnum : Food.DessertEnum.values()) {
System.out.print(dessertEnum + " ");
}
System.out.println();
//我这地方这么写,是因为我在自己测试的时候,把这个coffee单独到一个文件去实现那个food接口,而不是在那个接口的内部。
for (CoffeeEnum coffee : CoffeeEnum.values()) {
System.out.print(coffee + " ");
}
System.out.println();
//搞个实现接口,来组织枚举,简单讲,就是分类吧。如果大量使用枚举的话,这么干,在写代码的时候,就很方便调用啦。
//还有就是个“多态”的功能吧,
Food food = Food.DessertEnum.CAKE;
System.out.println(food);
food = CoffeeEnum.BLACK_COFFEE;
System.out.println(food);
}
枚举集合的使用
java.util.EnumSet和java.util.EnumMap是两个枚举集合。EnumSet保证集合中的元素不重复;EnumMap中的 key是enum类型,而value则可以是任意类型。关于这个两个集合的使用就不在这里赘述,可以参考JDK文档。
注解(annotation)
理解Java注解
注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。
基本语法
声明注解与元注解
注解元素及其数据类型
编译器对默认值的限制
注解不支持继承
快捷方式
Java内置注解与其它元注解
注解与反射机制
运行时注解处理器
Java 8中注解增强
元注解Repeatable
新增的两种ElementType
关键字
this
作用域
this表示某个对象
this可以出现在实例方法和构造方法中,但是不可以出现在类方法中
this出现在类的构造方法中,就代表该构造方法所创建的对象
this出现在实例方法中,就代表正在调用该方法的当前对象
this不能出现在类方法中,是因为类方法可以通过类名直接调用。在这个时候,可能还没有任何对象诞生。
this可以出现在实例方法和构造方法中,但是不可以出现在类方法中
this出现在类的构造方法中,就代表该构造方法所创建的对象
this出现在实例方法中,就代表正在调用该方法的当前对象
this不能出现在类方法中,是因为类方法可以通过类名直接调用。在这个时候,可能还没有任何对象诞生。
方法
实例方法:
实例方法只能通过对象来调用,不能用类名来调用
当实例成员变量在实例方法中出现时,默认格式为 :this.成员变量
当static成员变量在实例方法中出现时,默认格式为: 类.成员变量
实例方法只能通过对象来调用,不能用类名来调用
当实例成员变量在实例方法中出现时,默认格式为 :this.成员变量
当static成员变量在实例方法中出现时,默认格式为: 类.成员变量
class A{
int x;
static int y;
void f(){
this.x=100;
A.y=200;
}
}
int x;
static int y;
void f(){
this.x=100;
A.y=200;
}
}
当一个对象调用方法时,方法中的实例成员变量就是指分配给该对象的实例成员变量。而static变量与其他对象共享。所以通常情况可以省略实例成员变量名字前边的“this”,以及static变量前的“类名”。但是当实例成员名字和局部变量名字相同时,成员变量前的“this”以及“类名”就不可以省略。
class A {
private String name ;
public void f(String name ){
this.name =name;//this不能省略
}
}
private String name ;
public void f(String name ){
this.name =name;//this不能省略
}
}
我们知道类的实例方法可以调用类的其他方法,对于实例方法调用的默认格式是:this.方法
class B {
void f(){
this.g();//this可省略
B.h();//B(类名)可省略
}
void g(){
System.out.print("Hello");
}
static void h(){
System.out.print("World!");
}
}
void f(){
this.g();//this可省略
B.h();//B(类名)可省略
}
void g(){
System.out.print("Hello");
}
static void h(){
System.out.print("World!");
}
}
static
概念
static是静态修饰符,什么叫静态修饰符呢?大家都知道,在程序中任何变量或者代码都是在编译时由系统自动分配内存来存储的,而所谓静态就是指在编译后所分配的内存会一直存在,直到程序退出内存才会释放这个空间,也就是只要程序在运行,那么这块内存就会一直存在。这样做有什么意义呢?在Java程序里面,所有的东西都是对象,而对象的抽象就是类,对于一个类而言,如果要使用他的成员,那么普通情况下必须先实例化对象后,通过对象的引用才能够访问这些成员,但是用static修饰的成员可以通过类名加“.”进行直接访问。
static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块,但是Java语言中没有全局变量的概念。
被static修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。因此,static对象可以在它的任何对象创建之前访问,无需引用任何对象。
用public修饰的static成员变量和成员方法本质是全局变量和全局方法,当声明它类的对象市,不生成static变量的副本,而是类的所有实例共享同一个static变量。
static变量前可以有private修饰,表示这个变量可以在类的静态代码块中,或者类的其他静态成员方法中使用(当然也可以在非静态成员方法中使用--废话),但是不能在其他类中通过类名来直接引用,这一点很重要。实际上你需要搞明白,private是访问权限限定,static表示不要实例化就可以使用,这样就容易理解多了。static前面加上其它访问权限关键字的效果也以此类推。
static修饰的成员变量和成员方法习惯上称为静态变量和静态方法,可以直接通过类名来访问,访问语法为:
类名.静态方法名(参数列表...)
类名.静态变量名
用static修饰的代码块表示静态代码块,当Java虚拟机(JVM)加载类时,就会执行该代码块(用处非常大,呵呵)。
static变量
按照是否静态的对类成员变量进行分类可分两种:一种是被static修饰的变量,叫静态变量或类变量;另一种是没有被static修饰的变量,叫实例变量。两者的区别是:
对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。
对于实例变量,没创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响(灵活)。
对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。
对于实例变量,没创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响(灵活)。
static方法
静态方法可以直接通过类名调用,任何的实例也都可以调用,因此静态方法中不能用this和super关键字,不能直接访问所属类的实例变量和实例方法(就是不带static的成员变量和成员成员方法),只能访问所属类的静态成员变量和成员方法。因为实例成员与特定的对象关联!这个需要去理解,想明白其中的道理,不是记忆!!!因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。
static代码块
static代码块也叫静态代码块,是在类中独立于类成员的static语句块,可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
public class Test5 {
private static int a;
private int b;
static {
Test5.a = 3;
System.out.println(a);
Test5 t = new Test5();
t.f();
t.b = 1000;
System.out.println(t.b);
}
static {
Test5.a = 4;
System.out.println(a);
}
public static void main(String[] args) {
// TODO 自动生成方法存根
}
static {
Test5.a = 5;
System.out.println(a);
}
public void f() {
System.out.println("hhahhahah");
}
}
private static int a;
private int b;
static {
Test5.a = 3;
System.out.println(a);
Test5 t = new Test5();
t.f();
t.b = 1000;
System.out.println(t.b);
}
static {
Test5.a = 4;
System.out.println(a);
}
public static void main(String[] args) {
// TODO 自动生成方法存根
}
static {
Test5.a = 5;
System.out.println(a);
}
public void f() {
System.out.println("hhahhahah");
}
}
java static块和static方法的使用区别
如果有些代码必须在项目启动的时候就执行,就需要使用静态代码块,这种代码是主动执行的;需要在项目启动的时候就初始化但是不执行,在不创建对象的情况下,可以供其他程序调用,而在调用的时候才执行,这需要使用静态方法,这种代码是被动执行的。 静态方法在类加载的时候 就已经加载 可以用类名直接调用。
静态代码块是自动执行的;
• 静态方法是被调用的时候才执行的.
• 静态方法:如果我们在程序编写的时候需要一个不实例化对象就可以调用的方法,我们就可以使用静态方法,具体实现是在方法前面加上static,如下:
public static void method(){}
• 静态方法是被调用的时候才执行的.
• 静态方法:如果我们在程序编写的时候需要一个不实例化对象就可以调用的方法,我们就可以使用静态方法,具体实现是在方法前面加上static,如下:
public static void method(){}
final
Java中的final关键字
在 java 中, final 关键字可以来 修饰类、方法和变量。下面就来详细的了解一下 final 关键字的一些基本用法。
final关键字的基本用法
1、修饰类
当使用 final 修饰一个类时,表示这个类不能被继承。所以在自己设计一个类时,如果不想继承则可以将类设置为 final ,一般在设计工具类时我们往往都会设计成一个 final 类,例如 JDK 中的 String 、System等。
要点:
被 final 修饰的类,类中的成员变量可以根据自己的实际需要设计为 final。
通常 final 类中的成员方法都会被隐式的指定为 final 方法。
要点:
被 final 修饰的类,类中的成员变量可以根据自己的实际需要设计为 final。
通常 final 类中的成员方法都会被隐式的指定为 final 方法。
2、修饰方法
被 final 修饰的方法不能被重写
要点:
一个类的 private 方法会隐式的被指定为 final 方法。
如果父类中有 final 修饰的方法,那么子类中不能去重写。
final修饰的方法表示此方法已经是“最后的、最终的”含义,亦即此方法不能被重写(可以重载多个final修饰的方法)。
此处需要注意的一点是:
因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。
要点:
一个类的 private 方法会隐式的被指定为 final 方法。
如果父类中有 final 修饰的方法,那么子类中不能去重写。
final修饰的方法表示此方法已经是“最后的、最终的”含义,亦即此方法不能被重写(可以重载多个final修饰的方法)。
此处需要注意的一点是:
因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。
3、修饰成员变量
必须要赋值初始值,而且只能初始化一次
被 final 修饰的成员变量赋值有两种方式:
1、直接赋值
2、全部在构造方法中赋值。
如果修饰的成员变量是基本类型,则表示这个变量的值不能改变。
如果修饰的成员变量是一个引用类型,则是说这个引用的地址的值不可以改变,但是这个引用所指向的对象里面的内容还是可以改变的。
当函数的参数类型声明为final时,说明该参数是只读型的。即你可以读取使用该参数,但是无法改变该参数的值。
被 final 修饰的成员变量赋值有两种方式:
1、直接赋值
2、全部在构造方法中赋值。
如果修饰的成员变量是基本类型,则表示这个变量的值不能改变。
如果修饰的成员变量是一个引用类型,则是说这个引用的地址的值不可以改变,但是这个引用所指向的对象里面的内容还是可以改变的。
当函数的参数类型声明为final时,说明该参数是只读型的。即你可以读取使用该参数,但是无法改变该参数的值。
volatile
概念
volatile 是 Java 中的关键字,是一个变量修饰符,被用来修饰会被不同线程访问和修改的变量。
Java 内存模型
可见性
可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
在 Java 中 volatile、synchronized 和 final 实现可见性。
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
在 Java 中 volatile、synchronized 和 final 实现可见性。
原子性
原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
有序性
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性。
1)、volatile 是因为其本身包含“禁止指令重排序”的语义,
2)、synchronized 是由“一个变量,在同一个时刻,只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了:持有同一个对象锁的两个同步块,只能串行执行。
1)、volatile 是因为其本身包含“禁止指令重排序”的语义,
2)、synchronized 是由“一个变量,在同一个时刻,只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了:持有同一个对象锁的两个同步块,只能串行执行。
Volatile原理
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。
当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
当一个变量定义为 volatile 之后,将具备两种特性:
1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。
2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
当一个变量定义为 volatile 之后,将具备两种特性:
1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。
2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
transient
transient关键字的定义
定义:transient只能用来修饰成员变量(field),被transient修饰的成员变量不参与序列化过程。
简析:Java中的对象如果想要在网络上传输或者存储在磁盘时,就必须要序列化。Java中序列化的本质是Java对象转换为字节序列。但是在序列化的过程中,可以允许被序列对象中的某个成员变量不参与序列化,即该对象完成序列化之后,被transient修饰的成员变量会在字节序列中消失。
举例:
小美的昵称希望被人看到,但是小美的真名不希望被人看到。
简析:Java中的对象如果想要在网络上传输或者存储在磁盘时,就必须要序列化。Java中序列化的本质是Java对象转换为字节序列。但是在序列化的过程中,可以允许被序列对象中的某个成员变量不参与序列化,即该对象完成序列化之后,被transient修饰的成员变量会在字节序列中消失。
举例:
小美的昵称希望被人看到,但是小美的真名不希望被人看到。
public class XiaoMei implements Serializable {
private static final long serialVersionUID = -4575083234166325540L;
private String nickName;
private transient String realName;
public XiaoMei(String nickName,String realName){
this.nickName = nickName;
this.realName = realName;
}
public String toString(){
return String.format("XiaoMei.toString(): nickName=%s,realName=%s", nickName,realName);
}
}
private static final long serialVersionUID = -4575083234166325540L;
private String nickName;
private transient String realName;
public XiaoMei(String nickName,String realName){
this.nickName = nickName;
this.realName = realName;
}
public String toString(){
return String.format("XiaoMei.toString(): nickName=%s,realName=%s", nickName,realName);
}
}
public class Test {
public static void main(String[] args){
String realName="王小美", nickName="王美美";
XiaoMei x = new XiaoMei(nickName, realName);
System.out.println("序列化前:"+x.toString());
ObjectOutputStream outStream;
ObjectInputStream inStream;
//文件保存在本地,把这个路径换成自己的文件路径
//mac的同学把jiangyoujun换成自己的用户名
//windows的同学前面要加D:/这样的磁盘符号
String filePath = "/Users/jiangyoujun/Documents/test.log";
try {
outStream = new ObjectOutputStream(new FileOutputStream(filePath));
outStream.writeObject(x);
inStream = new ObjectInputStream(new FileInputStream(filePath));
XiaoMei readObject = (XiaoMei)inStream.readObject();
System.out.println("序列化后:"+readObject.toString());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void main(String[] args){
String realName="王小美", nickName="王美美";
XiaoMei x = new XiaoMei(nickName, realName);
System.out.println("序列化前:"+x.toString());
ObjectOutputStream outStream;
ObjectInputStream inStream;
//文件保存在本地,把这个路径换成自己的文件路径
//mac的同学把jiangyoujun换成自己的用户名
//windows的同学前面要加D:/这样的磁盘符号
String filePath = "/Users/jiangyoujun/Documents/test.log";
try {
outStream = new ObjectOutputStream(new FileOutputStream(filePath));
outStream.writeObject(x);
inStream = new ObjectInputStream(new FileInputStream(filePath));
XiaoMei readObject = (XiaoMei)inStream.readObject();
System.out.println("序列化后:"+readObject.toString());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
序列化前:XiaoMei.toString(): nickName=王美美,realName=王小美
序列化后:XiaoMei.toString(): nickName=王美美,realName=null
序列化后:XiaoMei.toString(): nickName=王美美,realName=null
transient关键字设计思路和底层实现思路
毫无疑问,这是一个平常的编程语言设计思路,即实现两种编码转化的时候,我们希望用户在转化过程中可以控制一些内容。
理解transient的关键在于理解序列化,序列化是Java对象转换为字节序列。
详细的说,就是Java对象在电脑中是存于内存之中的,内存之中的存储方式毫无疑问和磁盘中的存储方式不同(一个显而易见的区别就是对象在内存中的存储分为堆和栈两部分,两部分之间还有指针;但是存到磁盘中肯定不可能带指针,一定是某种文本形式)。序列化和反序列化就是在这两种不同的数据结构之间做转化。
序列化:JVM中的Java对象转化为字节序列。
反序列化:字节序列转化为JVM中的Java对象。
理解到这里,实现原理也是显而易见的,只要在处理两个数据结构转化的过程中,把标为transient的成员变量特殊处理一下就好了。
理解transient的关键在于理解序列化,序列化是Java对象转换为字节序列。
详细的说,就是Java对象在电脑中是存于内存之中的,内存之中的存储方式毫无疑问和磁盘中的存储方式不同(一个显而易见的区别就是对象在内存中的存储分为堆和栈两部分,两部分之间还有指针;但是存到磁盘中肯定不可能带指针,一定是某种文本形式)。序列化和反序列化就是在这两种不同的数据结构之间做转化。
序列化:JVM中的Java对象转化为字节序列。
反序列化:字节序列转化为JVM中的Java对象。
理解到这里,实现原理也是显而易见的,只要在处理两个数据结构转化的过程中,把标为transient的成员变量特殊处理一下就好了。
静态成员变量不加transient关键字也不能被序列化
在Java中,静态成员变量是不能被序列化的,不管有没有transient关键字。
大家可以看Serializable的相关文档:
大家可以看Serializable的相关文档:
/**
*The readObject method is responsible for reading from the stream and
* restoring the classes fields. It may call in.defaultReadObject to invoke
* the default mechanism for restoring the object's non-static and
* non-transient fields.
*The readObject method is responsible for reading from the stream and
* restoring the classes fields. It may call in.defaultReadObject to invoke
* the default mechanism for restoring the object's non-static and
* non-transient fields.
在所有Serializable的实现类中,都明确说明了实例化过程中不包含静态成员变量和被transient修饰的关键字。
使用Externalizable自定义序列化
Externalizable这个接口也是实现序列化的,但是和Serializable有不同。首先,Externalizable是继承Serializable的,其次Externalizable是需要程序员自己指定成员变量实现序列化的。
也就是说,使用Externalizable接口,程序员需要实现writeExternal以及readExternal这两个方法,来自己实现序列化和反序列化。实现的过程中,需要自己指定需要序列化的成员变量,此时,static和transient关键词都是不生效的,因为你重写了序列化中的方法。
也就是说,使用Externalizable接口,程序员需要实现writeExternal以及readExternal这两个方法,来自己实现序列化和反序列化。实现的过程中,需要自己指定需要序列化的成员变量,此时,static和transient关键词都是不生效的,因为你重写了序列化中的方法。
public class XiaoMei implements Externalizable {
private String nickName;
private transient String realName;
private static String childName="美美";
public XiaoMei(){
}
public XiaoMei(String nickName,String realName){
this.nickName = nickName;
this.realName = realName;
}
public String toString(){
return String.format("XiaoMei.toString(): nickName=%s,realName=%s,childName=%s", nickName,realName,childName);
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(realName);
out.writeUTF(childName);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
realName = in.readUTF();
childName = in.readUTF();
}
}
private String nickName;
private transient String realName;
private static String childName="美美";
public XiaoMei(){
}
public XiaoMei(String nickName,String realName){
this.nickName = nickName;
this.realName = realName;
}
public String toString(){
return String.format("XiaoMei.toString(): nickName=%s,realName=%s,childName=%s", nickName,realName,childName);
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(realName);
out.writeUTF(childName);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
realName = in.readUTF();
childName = in.readUTF();
}
}
序列化前:XiaoMei.toString(): nickName=王美美,realName=王小美,childName=美美
序列化后:XiaoMei.toString(): nickName=null,realName=王小美,childName=美美
序列化后:XiaoMei.toString(): nickName=null,realName=王小美,childName=美美
可以看出,Externalizable接口中,指定的成员变量被序列化了,不管是否有static和transient关键词,但是不被指定的成员变量不能被序列化。
Object通用方法
Object()
clone()
protected native Object clone() throws CloneNotSupportedException;
此方法返回当前对象的一个副本。
这是一个protected方法,提供给子类重写。但需要实现Cloneable接口,这是一个标记接口,如果没有实现,当调用object.clone()方法,会抛出CloneNotSupportedException。
此方法返回当前对象的一个副本。
这是一个protected方法,提供给子类重写。但需要实现Cloneable接口,这是一个标记接口,如果没有实现,当调用object.clone()方法,会抛出CloneNotSupportedException。
public class CloneTest implements Cloneable {
private int age;
private String name;
//省略get、set、构造函数等
@Override
protected CloneTest clone() throws CloneNotSupportedException {
return (CloneTest) super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
CloneTest cloneTest = new CloneTest(23, "9龙");
CloneTest clone = cloneTest.clone();
System.out.println(clone == cloneTest);
System.out.println(cloneTest.getAge()==clone.getAge());
System.out.println(cloneTest.getName()==clone.getName());
}
}
//输出结果
//false
//true
//true
private int age;
private String name;
//省略get、set、构造函数等
@Override
protected CloneTest clone() throws CloneNotSupportedException {
return (CloneTest) super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
CloneTest cloneTest = new CloneTest(23, "9龙");
CloneTest clone = cloneTest.clone();
System.out.println(clone == cloneTest);
System.out.println(cloneTest.getAge()==clone.getAge());
System.out.println(cloneTest.getName()==clone.getName());
}
}
//输出结果
//false
//true
//true
从输出我们看见,clone的对象是一个新的对象;但原对象与clone对象的 String类型 的name却是同一个引用,这表明,super.clone方法对成员变量如果是引用类型,进行是浅拷贝。
那什么是浅拷贝?对应的深拷贝?
浅拷贝:拷贝的是引用。
深拷贝:新开辟内存空间,进行值拷贝。
那如果我们要进行深拷贝怎么办呢?看下面的例子。
那什么是浅拷贝?对应的深拷贝?
浅拷贝:拷贝的是引用。
深拷贝:新开辟内存空间,进行值拷贝。
那如果我们要进行深拷贝怎么办呢?看下面的例子。
class Person implements Cloneable{
private int age;
private String name;
//省略get、set、构造函数等
@Override
protected Person clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
//name通过new开辟内存空间
person.name = new String(name);
return person;
}
}
public class CloneTest implements Cloneable {
private int age;
private String name;
//增加了person成员变量
private Person person;
//省略get、set、构造函数等
@Override
protected CloneTest clone() throws CloneNotSupportedException {
CloneTest clone = (CloneTest) super.clone();
clone.person = person.clone();
return clone;
}
public static void main(String[] args) throws CloneNotSupportedException {
CloneTest cloneTest = new CloneTest(23, "9龙");
Person person = new Person(22, "路飞");
cloneTest.setPerson(person);
CloneTest clone = cloneTest.clone();
System.out.println(clone == cloneTest);
System.out.println(cloneTest.getAge() == clone.getAge());
System.out.println(cloneTest.getName() == clone.getName());
Person clonePerson = clone.getPerson();
System.out.println(person == clonePerson);
System.out.println(person.getName() == clonePerson.getName());
}
}
//输出结果
//false
//true
//true
//false
//false
private int age;
private String name;
//省略get、set、构造函数等
@Override
protected Person clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
//name通过new开辟内存空间
person.name = new String(name);
return person;
}
}
public class CloneTest implements Cloneable {
private int age;
private String name;
//增加了person成员变量
private Person person;
//省略get、set、构造函数等
@Override
protected CloneTest clone() throws CloneNotSupportedException {
CloneTest clone = (CloneTest) super.clone();
clone.person = person.clone();
return clone;
}
public static void main(String[] args) throws CloneNotSupportedException {
CloneTest cloneTest = new CloneTest(23, "9龙");
Person person = new Person(22, "路飞");
cloneTest.setPerson(person);
CloneTest clone = cloneTest.clone();
System.out.println(clone == cloneTest);
System.out.println(cloneTest.getAge() == clone.getAge());
System.out.println(cloneTest.getName() == clone.getName());
Person clonePerson = clone.getPerson();
System.out.println(person == clonePerson);
System.out.println(person.getName() == clonePerson.getName());
}
}
//输出结果
//false
//true
//true
//false
//false
可以看到,即使成员变量是引用类型,我们也实现了深拷贝。 如果成员变量是引用类型,想实现深拷贝,则成员变量也要实现Cloneable接口,重写clone方法。
finalize()
getClass()
public final native ClassgetClass():这是一个public的方法,我们可以直接通过对象调用。
类加载的第一阶段类的加载就是将.class文件加载到内存,并生成一个java.lang.Class对象的过程。getClass()方法就是获取这个对象,这是当前类的对象在运行时类的所有信息的集合。这个方法是反射三种方式之一。
类加载的第一阶段类的加载就是将.class文件加载到内存,并生成一个java.lang.Class对象的过程。getClass()方法就是获取这个对象,这是当前类的对象在运行时类的所有信息的集合。这个方法是反射三种方式之一。
反射三种方式
对象的getClass();
类名.class;
Class.forName();
类名.class;
Class.forName();
反射主要用来获取运行时的信息,可以将java这种静态语言动态化,可以在编写代码时将一个子对象赋值给父类的一个引用,在运行时通过反射可以或许运行时对象的所有信息,即多态的体现。对于反射知识还是很多的,这里就不展开讲了。
hashCode()
public native int hashCode();这是一个public的方法,所以 子类可以重写 它。这个方法返回当前对象的hashCode值,这个值是一个整数范围内的(-2^31 ~ 2^31 - 1)数字。
对于hashCode有以下几点约束
在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改;
如果两个对象 x.equals(y) 方法返回true,则x、y这两个对象的hashCode必须相等。
如果两个对象x.equals(y) 方法返回false,则x、y这两个对象的hashCode可以相等也可以不等。 但是,为不相等的对象生成不同整数结果可以提高哈希表的性能。
默认的hashCode是将内存地址转换为的hash值,重写过后就是自定义的计算方式;也可以通过System.identityHashCode(Object)来返回原本的hashCode。
在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改;
如果两个对象 x.equals(y) 方法返回true,则x、y这两个对象的hashCode必须相等。
如果两个对象x.equals(y) 方法返回false,则x、y这两个对象的hashCode可以相等也可以不等。 但是,为不相等的对象生成不同整数结果可以提高哈希表的性能。
默认的hashCode是将内存地址转换为的hash值,重写过后就是自定义的计算方式;也可以通过System.identityHashCode(Object)来返回原本的hashCode。
推荐使用Objects.hash(Object… values)方法。相信看源码的时候,都看到计算hashCode都使用了31作为基础乘数, 为什么使用31呢?我比较赞同与理解result * 31 = (result<<5) - result。JVM底层可以自动做优化为位运算,效率很高;还有因为31计算的hashCode冲突较少,利于hash桶位的分布。final int prime = 31;是怎么回事呢,为什么偏偏是31呢
原理:
31是个不大不小的质数(2的5次方-1(即1<<<<<-1))使<pre name="code" class="java">ashCode()中的result值不会大于int的取值范围
原理:
31是个不大不小的质数(2的5次方-1(即1<<<<<-1))使<pre name="code" class="java">ashCode()中的result值不会大于int的取值范围
equals(Object obj)
public boolean equals(Object obj);用于比较当前对象与目标对象是否相等,默认是比较引用是否指向同一对象。为public方法,子类可重写。
为什么需要重写equals方法?
因为如果不重写equals方法,当将自定义对象放到map或者set中时;如果这时两个对象的hashCode相同,就会调用equals方法进行比较,这个时候会调用Object中默认的equals方法,而默认的equals方法只是比较了两个对象的引用是否指向了同一个对象,显然大多数时候都不会指向,这样就会将重复对象存入map或者set中。这就 破坏了map与set不能存储重复对象的特性,会造成内存溢出 。
因为如果不重写equals方法,当将自定义对象放到map或者set中时;如果这时两个对象的hashCode相同,就会调用equals方法进行比较,这个时候会调用Object中默认的equals方法,而默认的equals方法只是比较了两个对象的引用是否指向了同一个对象,显然大多数时候都不会指向,这样就会将重复对象存入map或者set中。这就 破坏了map与set不能存储重复对象的特性,会造成内存溢出 。
重写equals方法的几条约定:
自反性:即x.equals(x)返回true,x不为null;
对称性:即x.equals(y)与y.equals(x)的结果相同,x与y不为null;
传递性:即x.equals(y)结果为true, y.equals(z)结果为true,则x.equals(z)结果也必须为true;
一致性:即x.equals(y)返回true或false,在未更改equals方法使用的参数条件下,多次调用返回的结果也必须一致。x与y不为null。
如果x不为null, x.equals(null)返回false。
自反性:即x.equals(x)返回true,x不为null;
对称性:即x.equals(y)与y.equals(x)的结果相同,x与y不为null;
传递性:即x.equals(y)结果为true, y.equals(z)结果为true,则x.equals(z)结果也必须为true;
一致性:即x.equals(y)返回true或false,在未更改equals方法使用的参数条件下,多次调用返回的结果也必须一致。x与y不为null。
如果x不为null, x.equals(null)返回false。
建议equals及hashCode两个方法,需要重写时,两个都要重写,一般都是将自定义对象放至Set中,或者Map中的key时,需要重写这两个方法。
notify()
notifyAll()
toString()
public String toString();这是一个public方法,子类可重写, 建议所有子类都重写toString方法,默认的toString方法,只是将当前类的全限定性类名+@+十六进制的hashCode值。
返回当前对象的字符串表示,可以将其打印方便查看对象的信息,方便记录日志信息提供调试。
我们可以选择需要表示的重要信息重写到toString方法中。为什么Object的toString方法只记录类名跟内存地址呢?因为Object没有其他信息了
我们可以选择需要表示的重要信息重写到toString方法中。为什么Object的toString方法只记录类名跟内存地址呢?因为Object没有其他信息了
wait()
wait(long timeout)
wait(long timeout, int nanos)
hashCode()与equals(Object obj)的联系与区别
hashCode()方法和equal()方法的作用其实一样,在Java里都是用来对比两个对象是否相等一致,那么equal()既然已经能实现对比的功能了,为什么还要hashCode()呢?
因为重写的equal()里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对比,则只要生成一个hash值进行比较就可以了,效率很高,那么hashCode()既然效率这么高为什么还要equal()呢?
因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出:
1.equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的。
2.hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。
因为重写的equal()里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对比,则只要生成一个hash值进行比较就可以了,效率很高,那么hashCode()既然效率这么高为什么还要equal()呢?
因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出:
1.equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的。
2.hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。
所以对于需要大量并且快速的对比的话如果都用equal()去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等(也就是不必再用equal()去再对比了),如果hashCode()相同,此时再对比他们的equal(),如果equal()也相同,则表示这两个对象是真的相同了,这样既能大大提高了效率也保证了对比的绝对正确性!
这种大量的并且快速的对象对比一般使用的hash容器中,比如hashset,hashmap,hashtable等等,比如hashset里要求对象不能重复,则他内部必然要对添加进去的每个对象进行对比,而他的对比规则就是像上面说的那样,先hashCode(),如果hashCode()相同,再用equal()验证,如果hashCode()都不同,则肯定不同,这样对比的效率就很高了。
这种大量的并且快速的对象对比一般使用的hash容器中,比如hashset,hashmap,hashtable等等,比如hashset里要求对象不能重复,则他内部必然要对添加进去的每个对象进行对比,而他的对比规则就是像上面说的那样,先hashCode(),如果hashCode()相同,再用equal()验证,如果hashCode()都不同,则肯定不同,这样对比的效率就很高了。
1、equals方法用于比较对象的内容是否相等(覆盖以后)
2、hashcode方法只有在集合中用到
3、当覆盖了equals方法时,比较对象是否相等将通过覆盖后的equals方法进行比较(判断对象的内容是否相等)。
4、将对象放入到集合中时,首先判断要放入对象的hashcode值与集合中的任意一个元素的hashcode值是否相等,如果不相等直接将该对象放入集合中。如果hashcode值相等,然后再通过equals方法判断要放入对象与集合中的任意一个对象是否相等,如果equals判断不相等,直接将该元素放入到集合中,否则不放入。
2、hashcode方法只有在集合中用到
3、当覆盖了equals方法时,比较对象是否相等将通过覆盖后的equals方法进行比较(判断对象的内容是否相等)。
4、将对象放入到集合中时,首先判断要放入对象的hashcode值与集合中的任意一个元素的hashcode值是否相等,如果不相等直接将该对象放入集合中。如果hashcode值相等,然后再通过equals方法判断要放入对象与集合中的任意一个对象是否相等,如果equals判断不相等,直接将该元素放入到集合中,否则不放入。
Java中equals和==的区别
== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。
equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。
equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。
多态
将What(是什么)从how中剥离出来
增加的程序可扩展性
只作用于行为,不能用于属性
同一个行为,不同的子类表现出不同的行为/形态
Polymorphism is the ability of an entity
to take several forms.
多态来自于继承特性
来源于继承机制允许不但能将对象视其为本身,
而且能视为其基础类型(Base Type)
to take several forms.
多态来自于继承特性
来源于继承机制允许不但能将对象视其为本身,
而且能视为其基础类型(Base Type)
实现
有继承
子类override父类的方法
父类引用指向子类对象
等号左侧: 编译器类型
等号右侧:运行时类型
late binding/dynamic binding
除非声明为final,均可以后期绑定
final的目的
防止篡改
告知编译器,不需要后期绑定
防止篡改
告知编译器,不需要后期绑定
等号右侧:运行时类型
实际发生了 Upcast
编译期间,父类并不能调用子类增加的属性和方法
解决方法:Downcasting,
由此可以获得子类的特殊访问。
eg: Object.equals(Object o)
由此可以获得子类的特殊访问。
eg: Object.equals(Object o)
父类当方法的形参,然后传入子类的对象,
然后调用同一方法,根据传入子类的不同展现不同行为,
构成多态。
然后调用同一方法,根据传入子类的不同展现不同行为,
构成多态。
类型
静态(Static Polymorphism)
during compile time => Mothod overloading
动态(Dynamic Polymorphism)
during run time => overriden mothod
具体实现中,在父类中空间每一个方法有
一个指针,执行期间会根据当前的对象类
型指向与其匹配的具体代码段。
具体实现中,在父类中空间每一个方法有
一个指针,执行期间会根据当前的对象类
型指向与其匹配的具体代码段。
使用场景
经典方式:使用父类做方法形参,实现多态调用
使用父类做方法的返回值类型,真实返回
的对象可以是该类的任意一个子类对象
的对象可以是该类的任意一个子类对象
简单工厂模式
参见设计模式
定义一个static方法,通过类名直接调用
返回值类型是父类类型,
返回可以是其任意子类类型
传入一个字符串类型的参数,
工厂根据参数创建对应的子类产品
参见设计模式
定义一个static方法,通过类名直接调用
返回值类型是父类类型,
返回可以是其任意子类类型
传入一个字符串类型的参数,
工厂根据参数创建对应的子类产品
接口当做方法的形参,传入具体的实现类对象
接口当做方法的返回值,返回的是具体的实现类的对象
优势
支持动态绑定
提高代码的扩展性
多态可以提高扩展性,但扩展性没有达到最好(参考反射)
提高代码的扩展性
多态可以提高扩展性,但扩展性没有达到最好(参考反射)
泛型
概念
泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本
质是参数化类型,也就是说所操作的数据类型被指定为一个参数。比如我们要写一个排序方法,
能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,我们就可以使用 Java 泛型。
质是参数化类型,也就是说所操作的数据类型被指定为一个参数。比如我们要写一个排序方法,
能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,我们就可以使用 Java 泛型。
好处
元组
public class Tuple<A, B, C> {
private A a;
private B b;
private C c;
public Tuple(A a, B b, C c){
this.a = a;
this.b = b;
this.c = c;
}
public String toString(){
return "(" + a + "." + b + "." + c + ")";
}
}
private A a;
private B b;
private C c;
public Tuple(A a, B b, C c){
this.a = a;
this.b = b;
this.c = c;
}
public String toString(){
return "(" + a + "." + b + "." + c + ")";
}
}
泛型方法(<E>)
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数
类型,编译器适当地处理每一个方法调用。
// 泛型方法 printArray
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
}
1. <? extends T>表示该通配符所代表的类型是 T 类型的子类。
2. <? super T>表示该通配符所代表的类型是 T 类型的父类。
类型,编译器适当地处理每一个方法调用。
// 泛型方法 printArray
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
}
1. <? extends T>表示该通配符所代表的类型是 T 类型的子类。
2. <? super T>表示该通配符所代表的类型是 T 类型的父类。
泛型类(<T>)
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一
样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,
也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,
这些类被称为参数化的类或参数化的类型。
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,
也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,
这些类被称为参数化的类或参数化的类型。
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
类型通配符?
类 型 通 配 符 一 般 是 使 用 ? 代 替 具 体 的 类 型 参 数 。 例 如 List<?> 在 逻 辑 上 是
List<String>,List<Integer> 等所有 List<具体类型实参>的父类。
List<String>,List<Integer> 等所有 List<具体类型实参>的父类。
类型擦除
Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛
型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个
过程就称为类型擦除。如在代码中定义的 List<Object>和 List<String>等类型,在编译之后
都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。
类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般
是 Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换
成具体的类。
型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个
过程就称为类型擦除。如在代码中定义的 List<Object>和 List<String>等类型,在编译之后
都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。
类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般
是 Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换
成具体的类。
边界
通配符和泛型上界和下界
上界< ? extends Class>
下界< ? super Class>
PECS原则
反射
动态语言
概念
动态语言,是指程序在运行时可以改变其结构:新的函数可以引进,已有的函数可以被删除等结
构上的变化。比如常见的 JavaScript 就是动态语言,除此之外 Ruby,Python 等也属于动态语言,
而 C、C++则不属于动态语言。从反射角度说 JAVA 属于半动态语言
构上的变化。比如常见的 JavaScript 就是动态语言,除此之外 Ruby,Python 等也属于动态语言,
而 C、C++则不属于动态语言。从反射角度说 JAVA 属于半动态语言
反射机制概念 (运行状态中知道类所有的属性和方法)
Java Reflection
ClassLoader
动态代理
Proxy.proxynewInstance
InvocationHandle
常见用例
数据库连接以及事物管理
单元测试中的动态Mock'对象
Generic
泛型方法返回类型
泛型方法参数类型
Annotation
getAnnotations
getDeclaredAnnotations
Array
在 Java 中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;
反射的应用场合
编译时类型和运行时类型
在 Java 程序中许多对象在运行是都会出现两种类型:编译时类型和运行时类型。 编译时的类型由
声明对象时实用的类型来决定,运行时的类型由实际赋
声明对象时实用的类型来决定,运行时的类型由实际赋
编译时类型无 法获取具体方法
程序在运行时还可能接收到外部传入的对象,该对象的编译时类型为 Object,但是程序有需要调用
该对象的运行时类型的方法。为了解决这些问题,程序需要在运行时发现对象和类的真实信息。
然而,如果编译时根本无法预知该对象和类属于哪些类,程序只能依靠运行时信息来发现该对象
和类的真实信息,此时就必须使用到反射了。
该对象的运行时类型的方法。为了解决这些问题,程序需要在运行时发现对象和类的真实信息。
然而,如果编译时根本无法预知该对象和类属于哪些类,程序只能依靠运行时信息来发现该对象
和类的真实信息,此时就必须使用到反射了。
Java 反射 API
反射 API 用来生成 JVM 中的类、接口或则对象的信息。
1. Class 类:反射的核心类,可以获取类的属性,方法等信息。
2. Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性
值。
3. Method 类: Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或
者执行方法。
4. Constructor 类: Java.lang.reflec 包中的类,表示类的构造方法。
2. Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性
值。
3. Method 类: Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或
者执行方法。
4. Constructor 类: Java.lang.reflec 包中的类,表示类的构造方法。
反射使用步骤(获取 Class 对象、调用对象方法)
1. 获取想要操作的类的 Class 对象,他是反射的核心,通过 Class 对象我们可以任意调用类的方
法。
2. 调用 Class 类中的方法,既就是反射的使用阶段。
3. 使用反射 API 来操作这些信息。
法。
2. 调用 Class 类中的方法,既就是反射的使用阶段。
3. 使用反射 API 来操作这些信息。
获取 Class 对象的 3 种方法
调用某个对象的 getClass() 方法
Person p=new Person();
Class clazz=p.getClass();
Class clazz=p.getClass();
调用某个类的 class 属性来获取该类对应的 Class 对象
Class clazz=Person.class;
使用 Class 类中的 forName() 静态方法 ( 最安全 / 性能最好 )
Class clazz=Class.forName("类的全路径"); (最常用)
当我们获得了想要操作的类的 Class 对象后,可以通过 Class 类中的方法获取并查看该类中的方法
和属性。
//获取 Person 类的 Class 对象
Class clazz=Class.forName("reflection.Person");
//获取 Person 类的所有方法信息
Method[] method=clazz.getDeclaredMethods();
for(Method m:method){
System.out.println(m.toString());
}
//获取 Person 类的所有成员属性信息
Field[] field=clazz.getDeclaredFields();
for(Field f:field){
System.out.println(f.toString());
}
//获取 Person 类的所有构造方法信息
Constructor[] constructor=clazz.getDeclaredConstructors();
for(Constructor c:constructor){
System.out.println(c.toString());
}
当我们获得了想要操作的类的 Class 对象后,可以通过 Class 类中的方法获取并查看该类中的方法
和属性。
//获取 Person 类的 Class 对象
Class clazz=Class.forName("reflection.Person");
//获取 Person 类的所有方法信息
Method[] method=clazz.getDeclaredMethods();
for(Method m:method){
System.out.println(m.toString());
}
//获取 Person 类的所有成员属性信息
Field[] field=clazz.getDeclaredFields();
for(Field f:field){
System.out.println(f.toString());
}
//获取 Person 类的所有构造方法信息
Constructor[] constructor=clazz.getDeclaredConstructors();
for(Constructor c:constructor){
System.out.println(c.toString());
}
创建对象的两种方法
Class 对象的 newInstance()
使用 Class 对象的 newInstance()方法来创建该 Class 对象对应类的实例,但是这种方法要求
该 Class 对象对应的类有默认的空构造器。
该 Class 对象对应的类有默认的空构造器。
调用 Constructor 对象的 newInstance()
先使用 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance()
方法来创建 Class 对象对应类的实例,通过这种方法可以选定构造方法创建实例。
//获取 Person 类的 Class 对象
Class clazz=Class.forName("reflection.Person");
//使用.newInstane 方法创建对象
Person p=(Person) clazz.newInstance();
//获取构造方法并创建对象
Constructor c=clazz.getDeclaredConstructor(String.class,String.class,int.class);
方法来创建 Class 对象对应类的实例,通过这种方法可以选定构造方法创建实例。
//获取 Person 类的 Class 对象
Class clazz=Class.forName("reflection.Person");
//使用.newInstane 方法创建对象
Person p=(Person) clazz.newInstance();
//获取构造方法并创建对象
Constructor c=clazz.getDeclaredConstructor(String.class,String.class,int.class);
序列化
序列化概念
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
java平台允许我们在内存中创建可复制的java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即这些对象的生命周期不会比JVM的生命周期更长,但是在现实运用中,就可能要求在JVM停止运行之后能够保存(序列化)指定的对象,并在将来重新读取被保存的对象,java对象序列化就能帮助我们实现该功能。
怎么运用
序列化
序列化就是把对象的状态信息转化为可存储或者传输的形式过程,也就是把对象转化为字节序列化的过程称为对象的序列化
反序列化
序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程成为对象的反序列化
序列化的不足
评价一个序列化算法优劣的两个重要指标
数据大小
序列化操作本身的速度及系统,资源开销(CPU内存)
序列化的数据比较大,传输效率低
其他语言无法识别和对接
实现一个序列化
在java中只要一个类实现了java.io.Serializable接口,那么它就可以被序列化
基于JDK序列化方式实现
JDK提供了java对象的序列化方式,主要通过输出流和对象输入流来实现,其中,被序列化的对象要实现java中只要一个类实现了java.io.Serializable接口
序列化高阶
SerialVersionUID的作用
java的序列化机制是通过判断类的SerialVersionUID来验证版本一致性的,在进行反序列化时,JVM会把传来的字节流中的SerialVersionUID与本地相应实体类的SerialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException
如果没有为指定的class配置SerialVersionUID,那么java编译器会自动给这个class进行一个摘要算法,类似于指纹算法,只要这个文件有任何改动,得到的UID就会截图不同,可以保证在这么多类中,这个编号是唯一的
SerialVersionUID有两种生成方式
默认的1L,比如
根据类名,接口名,成员方法及属性等来生成一个64位的哈希字段
当实现java.io.Serializable接口类没有显式地定义一个SerialVersionUID变量时候,java序列化机制会根据编译的class自动生成一个SerialVersionUID作序列化版本比较用,这种情况下,如果class文件(类名,方法名等)没有发生变化(增加空格,换行,增加注释等),就算再编译多次,SerialVersionUID也不会变化的
静态变量序列化
序列化时不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量
父类的序列化
一个子类实现了Serializable接口,它的父类都没有实现Serializable接口,在子类中设置父类的成员变量的值,接着序列化该子类对象,再反序列化出来以后输出父类属性的值,结果是
当一个父类没有实现序列化时,子类集成父类并且实现了序列化,在反序列化该子类后,是没办法获取到父类的属性值的
当一个父类实现序列化,子类自动实现序列化,不需要再显示实现Serializable接口
当一个对象的实例变量引用了其他对象,序列化
Transient关键字
作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,Transinent变量的值设为初始值,如int型的是0
序列化的存储规则
同一个对象两次(开始写入文件到最终关闭流这个过程算异一次),如果不关闭流写入文件两次,则第二次写入对象时文件只增加5字节
java序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的5字节的存储空间就是新增引用和一些控制信息的空间,反序列化时,恢复引用关系,该存储规则极大的节省了存储空间
序列化实现深克隆
在java中存在一个Clineable接口,通过实现这个接口的类都会具备clone的能力,同时clone在内存中进行,在新能方面会比我们直接通过new生产对象要高一些,特别是一些大的对象的生成,性能提升相对比较明显
常见的序列化技术
java序列化
优点
java语言本省提供,使用比较方便和简单
缺点
不支持跨语言处理,序列化以后生产的数据相对比较大
XML序列化
XML序列化的好处在于可读性好,方便阅读和调试,但是序列化以后的字节码文件比较大,而且效率不高,适应于对性能不高,而且QPS较低的企业级内部系统之间的数据交换的场景,同时XML又具有言语无惯性,所以还是用于异构系统之间的数据交换和协议,比如我们熟悉的Websercie就是采用XML格式对数据进行序列化的
JSON序列化
JSON(javascript object notation)是一种轻量级的数据交换格式,相对于xml来说,json的字节流较小,而且可读性也非常好,现在json数据格式的其他运用最普遍
开源工具
jackson
fastjson
gson
Hessian序列化框架
Hessian是一个支持跨言语传输的二进制序列化协议,相对于java默认的序列化机制来说,hessian具有更好的性能和易用性,而且支持对不同的语言,实际上DUBBO采用的就是Hessian序列化来实现,只不过dubbo对hessian进行了重构,性能更高
Protobuf序列化框架
Protobuf是goole的一种数据交换格式,它独立于语言,独立于平台
google提供了多种语言,比如java,c,go等,每一种实现都包含了响应语言的编译器和库文件,Protobuf使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的RPC调用,另外由于解析性能比较高,序列化以后数据量相对比较少,所以也可以应用在对象的持久化场景中但是要使用Protobuf会相对来说麻烦些,因为他有自己的语法,有自己的编译器
总结
Protobuf的性能好,主要体现在序列化后的数据体积小&序列化速度快,最终使得传输效率高,其原因如下:
编码/解码 方式简单(只需要简单的数学运算=位移等等)
采用Protobuf buffer自身的框架代码和编译器共同完成
序列化后的数据量体积小(即数据压缩效果好)的原因
采用了独特的编码方式,如Varint,zigzag编码方式等等
采用t_l_v的数据存储方式,减少了分隔符的使用&数据存储紧凑
各个序列化技术的性能比较
使用场景
技术层面
序列化空间开销,也就是序列化产生的结果大小,这个影响到传输的性能
序列化过程中消耗的时长,序列化消耗时间过长影响到业务的响应时间
序列化协议是否性支持跨平台,跨语言,因为现在的架构更加灵活,如果存在异构系统通信需求,那么这个必须要考虑的
可扩展性/兼容性,在实际业务开发中,系统往往需要随着需求的快速迭代来实现快速更新,这就需要我们采用的序列化协议基于良好的可扩展性兼容性,比如在现有序列化数据机构中新增一个业务字段,不会影响到现有业务
技术的流程程度,越流行的技术意味着使用的公司多,那么很多坑都已经趟过去了,技术解决方案也相对成熟
学习难度和易用性
选型建议
对性能要求不高的场景,可以采用基于XML的SOAP协议
对性能和间接性有比较高要求的场景,那么hessian,protobuf,Thrift,Avro都可以
基于前后端分离,或者独立的对外的api服务,选用json是比较好的,对于调试,可读性都很不错
Avro设计理念偏于动态类型语言,那么这类的场景使用Avro是可以的
注解
概念
A nnotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(metadata)的途径
和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation
对象,然后通过该 Annotation 对象来获取注解中的元数据信息。
和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation
对象,然后通过该 Annotation 对象来获取注解中的元数据信息。
4 种标准 元注解
@Target 修饰的对象范围
@Target说明了Annotation所修饰的对象范围: Annotation可被用于 packages、types(类、
接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数
和本地变量(如循环变量、catch 参数)。在 Annotation 类型的声明中使用了 target 可更加明晰
其修饰的目标
接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数
和本地变量(如循环变量、catch 参数)。在 Annotation 类型的声明中使用了 target 可更加明晰
其修饰的目标
@Retention 定义 被保留的时间长短
Retention 定义了该 Annotation 被保留的时间长短:表示需要在什么级别保存注解信息,用于描
述注解的生命周期(即:被描述的注解在什么范围内有效),取值(RetentionPoicy)由:
? SOURCE:在源文件中有效(即源文件保留)
? CLASS:在 class 文件中有效(即 class 保留)
? RUNTIME:在运行时有效(即运行时保留)
述注解的生命周期(即:被描述的注解在什么范围内有效),取值(RetentionPoicy)由:
? SOURCE:在源文件中有效(即源文件保留)
? CLASS:在 class 文件中有效(即 class 保留)
? RUNTIME:在运行时有效(即运行时保留)
@Documented 描述-javadoc
@ Documented 用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因
此可以被例如 javadoc 此类的工具文档化。
此可以被例如 javadoc 此类的工具文档化。
@Inherited 阐述了某个被标注的类型是被继承的
@Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一
个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个 annotation 将被用于该
class 的子类。
个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个 annotation 将被用于该
class 的子类。
注解 处理器
如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了。使用注解的过程中,
很重要的一部分就是创建于使用注解处理器。Java SE5扩展了反射机制的API,以帮助程序员快速
的构造自定义注解处理器。下面实现一个注解处理器。
/1:*** 定义注解*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/**供应商编号*/
public int id() default -1;
/*** 供应商名称*/
public String name() default "";
/** * 供应商地址*/
public String address() default "";
}
//2:注解使用
public class Apple {
@FruitProvider(id = 1, name = "陕西红富士集团", address = "陕西省西安市延安路")
private String appleProvider;
public void setAppleProvider(String appleProvider) {
this.appleProvider = appleProvider;
}
public String getAppleProvider() {
return appleProvider;
}
}
/3:*********** 注解处理器 ***************/
public class FruitInfoUtil {
public static void getFruitInfo(Class<?> clazz) {
String strFruitProvicer = "供应商信息:";
Field[] fields = clazz.getDeclaredFields();//通过反射获取处理注解
for (Field field : fields) {
if (field.isAnnotationPresent(FruitProvider.class)) {
FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
//注解信息的处理地方
strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:"
+ fruitProvider.name() + " 供应商地址:"+ fruitProvider.address();
System.out.println(strFruitProvicer);
}
}
}
}
public class FruitRun {
public static void main(String[] args) {
FruitInfoUtil.getFruitInfo(Apple.class);
/***********输出结果***************/
// 供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延
}
}
很重要的一部分就是创建于使用注解处理器。Java SE5扩展了反射机制的API,以帮助程序员快速
的构造自定义注解处理器。下面实现一个注解处理器。
/1:*** 定义注解*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/**供应商编号*/
public int id() default -1;
/*** 供应商名称*/
public String name() default "";
/** * 供应商地址*/
public String address() default "";
}
//2:注解使用
public class Apple {
@FruitProvider(id = 1, name = "陕西红富士集团", address = "陕西省西安市延安路")
private String appleProvider;
public void setAppleProvider(String appleProvider) {
this.appleProvider = appleProvider;
}
public String getAppleProvider() {
return appleProvider;
}
}
/3:*********** 注解处理器 ***************/
public class FruitInfoUtil {
public static void getFruitInfo(Class<?> clazz) {
String strFruitProvicer = "供应商信息:";
Field[] fields = clazz.getDeclaredFields();//通过反射获取处理注解
for (Field field : fields) {
if (field.isAnnotationPresent(FruitProvider.class)) {
FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
//注解信息的处理地方
strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:"
+ fruitProvider.name() + " 供应商地址:"+ fruitProvider.address();
System.out.println(strFruitProvicer);
}
}
}
}
public class FruitRun {
public static void main(String[] args) {
FruitInfoUtil.getFruitInfo(Apple.class);
/***********输出结果***************/
// 供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延
}
}
异常
概念
如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下
会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用
这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。
会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用
这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。
异常分类
Throwable 是 Java 语言中所有错误或异常的超类。下一层分为 Error 和 Exception
Error
Error 类是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果
出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
Exception
RuntimeException
NullPointerException 、 ClassCastException ; 一 个 是 检 查 异 常
CheckedException,如 I/O 错误导致的 IOException、SQLException。 RuntimeException 是
那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。 如果出现 RuntimeException,那么一
定是程序员的错误.
CheckedException,如 I/O 错误导致的 IOException、SQLException。 RuntimeException 是
那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。 如果出现 RuntimeException,那么一
定是程序员的错误.
CheckedException
一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强
制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一
般包括几个方面:
1. 试图在文件尾部读取数据
2. 试图打开一个错误格式的 URL
3. 试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在
制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一
般包括几个方面:
1. 试图在文件尾部读取数据
2. 试图打开一个错误格式的 URL
3. 试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在
异常的处理方式
遇到问题不进行具体处理,而是继续抛给调用者 ( throw,throws
try catch 捕获异常针对性处理方式
Throw 和 throws 的区别:
位置不同
throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的
是异常对象。
是异常对象。
功能不同
throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方
式;throw抛出具体的问题对象,执行到throw,功能就已经结束了,跳转到调用者,并
将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语
句,因为执行不到。
3. throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,
执行 throw 则一定抛出了某种异常对象。
式;throw抛出具体的问题对象,执行到throw,功能就已经结束了,跳转到调用者,并
将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语
句,因为执行不到。
3. throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,
执行 throw 则一定抛出了某种异常对象。
内部类
静态内部类
定义在类内部的静态类,就是静态内部类。
public class Out {
private static int a;
private int b;
public static class Inner {
public void print() {
System.out.println(a);
}
}
}
1. 静态内部类可以访问外部类所有的静态变量和方法,即使是 private 的也一样。
2. 静态内部类和一般类一致,可以定义静态变量、方法,构造方法等。
3. 其它类使用静态内部类需要使用“外部类.静态内部类”方式,如下所示:Out.Inner inner =
new Out.Inner();inner.print();
4. Java集合类HashMap内部就有一个静态内部类Entry。Entry是HashMap存放元素的抽象,
HashMap 内部维护 Entry 数组用了存放元素,但是 Entry 对使用者是透明的。像这种和外部
类关系密切的,且不依赖外部类实例的,都可以使用静态内部类。
public class Out {
private static int a;
private int b;
public static class Inner {
public void print() {
System.out.println(a);
}
}
}
1. 静态内部类可以访问外部类所有的静态变量和方法,即使是 private 的也一样。
2. 静态内部类和一般类一致,可以定义静态变量、方法,构造方法等。
3. 其它类使用静态内部类需要使用“外部类.静态内部类”方式,如下所示:Out.Inner inner =
new Out.Inner();inner.print();
4. Java集合类HashMap内部就有一个静态内部类Entry。Entry是HashMap存放元素的抽象,
HashMap 内部维护 Entry 数组用了存放元素,但是 Entry 对使用者是透明的。像这种和外部
类关系密切的,且不依赖外部类实例的,都可以使用静态内部类。
成员 内部类
定义在类内部的非静态类,就是成员内部类。成员内部类不能定义静态方法和变量(final 修饰的
除外)。这是因为成员内部类是非静态的,类初始化的时候先初始化静态成员,如果允许成员内
部类定义静态变量,那么成员内部类的静态变量初始化顺序是有歧义的。
public class Out {
private static int a;
private int b;
public class Inner {
public void print() {
System.out.println(a);
System.out.println(b);
}
}
}
除外)。这是因为成员内部类是非静态的,类初始化的时候先初始化静态成员,如果允许成员内
部类定义静态变量,那么成员内部类的静态变量初始化顺序是有歧义的。
public class Out {
private static int a;
private int b;
public class Inner {
public void print() {
System.out.println(a);
System.out.println(b);
}
}
}
局部 内部类 ( 定义在方法中的类 )
定义在方法中的类,就是局部类。如果一个类只在某个方法中使用,则可以考虑使用局部类
public class Out {
private static int a;
private int b;
public void test(final int c) {
final int d = 1;
class Inner {
public void print() {
System.out.println(c);
}
}
}
}
public class Out {
private static int a;
private int b;
public void test(final int c) {
final int d = 1;
class Inner {
public void print() {
System.out.println(c);
}
}
}
}
匿名 内部类 ( 要继承一个父类或者实现一个接口、直接使用
new 来生成一个对象的引用
new 来生成一个对象的引用
匿名内部类我们必须要继承一个父类或者实现一个接口,当然也仅能只继承一个父类或者实现一
个接口。同时它也是没有class关键字,这是因为匿名内部类是直接使用new来生成一个对象的引
用。
public abstract class Bird {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract int fly();
}
public class Test {
public void test(Bird bird){
System.out.println(bird.getName() + "能够飞 " + bird.fly() + "米");
}
public static void main(String[] args) {
Test test = new Test();
test.test(new Bird() {
public int fly() {
return 10000;
}
public String getName() {
return "大雁";
}
});
}
}
个接口。同时它也是没有class关键字,这是因为匿名内部类是直接使用new来生成一个对象的引
用。
public abstract class Bird {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract int fly();
}
public class Test {
public void test(Bird bird){
System.out.println(bird.getName() + "能够飞 " + bird.fly() + "米");
}
public static void main(String[] args) {
Test test = new Test();
test.test(new Bird() {
public int fly() {
return 10000;
}
public String getName() {
return "大雁";
}
});
}
}
集合容器
Collection
List
ArrayList
底层实现
动态数组
能够实现随机存取
实现了RandomAccess接口
fail-fast机制
在使用迭代器遍历list时,如果modCount和expectedCount不匹配,就会直接抛出异常
modCount在AbstractList中定义
使用迭代器自带的remove()函数的时候,如果删除了list中元素,不会出现fail-fast,因为迭代器会调整modCount和expectedCount值
modCount在AbstractList中定义
使用迭代器自带的remove()函数的时候,如果删除了list中元素,不会出现fail-fast,因为迭代器会调整modCount和expectedCount值
自定义了序列化方法
因为arrayList的底层数组中,可能存在值为null的元素,序列化这些元素是没有意义的,因此自定义了序列化方法,只序列化数组中非null的元素
通过readObject()和writeObject()方法实现
通过readObject()和writeObject()方法实现
源码实现
扩容:capacity=1.5*capacity
通过Arrays.copyOf()
System.copyOf()
每次扩容的时候,都会传入一个minCapacity,即扩容之后的数组长度,对于add方法,是原size+1,对于addAll方法,是size+newSize,如果原数组长度*1.5仍不能存放所需的元素,那么就会直接令数组长度为minCapacity
ArrayList是插入前扩容,扩容逻辑为 ensureCapacityInternal()--->ensureExplicitCapacity()---->grow()
通过Arrays.copyOf()
System.copyOf()
每次扩容的时候,都会传入一个minCapacity,即扩容之后的数组长度,对于add方法,是原size+1,对于addAll方法,是size+newSize,如果原数组长度*1.5仍不能存放所需的元素,那么就会直接令数组长度为minCapacity
ArrayList是插入前扩容,扩容逻辑为 ensureCapacityInternal()--->ensureExplicitCapacity()---->grow()
LinkedList
底层实现
双向链表
常用api
add
offer
remove
offer
remove
适合插入删除多的场合
CopyOnWriteArrayList
和ArrayList基本一模一样,但它是线程安全的
任何对array在结构上有所改变的操作(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,再在copy的数据上修改,这样就不会影响COWIterator中的数据了,不会抛ConcurrentModificationException异常(对set,add没有作用,因为set,add本来就要加锁),修改完成之后改变原有数据的引用即可。
读操作不加锁,写操作加锁,在进行add,set等操作时,会通过ReentrantLock进行加锁
适合多读少写的场景
任何对array在结构上有所改变的操作(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,再在copy的数据上修改,这样就不会影响COWIterator中的数据了,不会抛ConcurrentModificationException异常(对set,add没有作用,因为set,add本来就要加锁),修改完成之后改变原有数据的引用即可。
读操作不加锁,写操作加锁,在进行add,set等操作时,会通过ReentrantLock进行加锁
适合多读少写的场景
缺点
1.复制的数组会消耗内存
2.不能读取实时性的数据
3.会产生大量的对象
2.不能读取实时性的数据
3.会产生大量的对象
Set
HashSet
底层是数组+链表+红黑树
基于HashMap实现
所有元素的value值都是一个static final Object
hashSet的存储是无序的
因为HashSet是根据对象的hashCode,进行计算后存储的
最后一个构造函数,为包访问权限是不对外公开,主要是为了支持LinkedHashSet
HashSet(int initialCapacity, float loadFactor, boolean dummy)
多了一个dummy变量
多了一个dummy变量
TreeSet
基于TreeMap实现
add()时,value值都是一个static final Object对象,因此当key相等时就会覆盖,也实现了没有重复元素的问题
add()时,value值都是一个static final Object对象,因此当key相等时就会覆盖,也实现了没有重复元素的问题
LinkedHashSet
基于LinkHashMap实现
Map
HashMap
底层实现
1.7 数组+链表
数组的优点是访问速度快,但是插入删除操作慢
因为数组在内存中是连续存放的,因此存取很快
链表的优点是插入删除速度快,但是访问速度慢
由于链表不是连续存放的,因此插入删除时,只需要修改前后指针的指向即可,不需要移动元素位置
数组的优点是访问速度快,但是插入删除操作慢
因为数组在内存中是连续存放的,因此存取很快
链表的优点是插入删除速度快,但是访问速度慢
由于链表不是连续存放的,因此插入删除时,只需要修改前后指针的指向即可,不需要移动元素位置
1.8 数组+链表+红黑树
拉链法由头插法改为了尾插法
因为头插法在多线程的时候可能会导致死循环
链表长度大于8的时候转化为红黑树
红黑树的时间复杂度为logn,线性表查找的平均时间复杂度为n/2,因此在链表长度为8时进行转化效率最高
红黑树的转化也是比较消耗性能的
链表个数超过8则链表转换成树结构,链表个数小于6则树结构转换成链表
拉链法由头插法改为了尾插法
因为头插法在多线程的时候可能会导致死循环
链表长度大于8的时候转化为红黑树
红黑树的时间复杂度为logn,线性表查找的平均时间复杂度为n/2,因此在链表长度为8时进行转化效率最高
红黑树的转化也是比较消耗性能的
链表个数超过8则链表转换成树结构,链表个数小于6则树结构转换成链表
特点
存取的时间复杂度为O(1)
源码分析
put()
1.判断key是否为null,如果为null,调用putlForNullKey,将key插入到数组下标为0的位置
2.调用hash()方法计算key的hashcode,得到hash值
3.调用indexFor()方法进行取模运算,得到元素的下标位置
1.indexFor方法为:h&(length - 1)
2.使用与运算,计算速度更快,因为二进制运算比十进制运算效率更高(十进制运算还需要将二进制转化为十进制)
3.length之所以要设定为2次幂,就是为了这个indexFor方法服务
4.可以让散列更加均匀,length-1的最后一位为1,因此进行与运算时,可以散列到奇数和偶数的下标位置,如果对length直接取模,由于length为2次幂,所以最后一位一定为0,所以与运算的结果一定是偶数,这也就导致奇数下标的位置不能被散列到。
4.依次和该下标位置上的链表中的node节点比较key是否相等
e.hash == hash && ((k = e.key) == key || key.equals(k))
首先判断e.hash==hash是因为不同的key值也可能被散列到同一个位置,因此首先判断hash值,如果不相等则两个key肯定不等
如果相等,再通过==和equals比较是否相等,之所以要先判断hash值是否相等,是因为equal()很耗性能,因此先判断hash值能够提高效率
重写了hashcode()方法就必须重写equals方法
5.如果相等,更新value值,如果不相等,使用头插法(1.7)/尾插法(1.8)将entry(1.7)/Node(1.8)插入到链表中
get()
和put()方法类似,获取到桶的下标,再在链表上查找key值,再获取key对应的value值
resize()
当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容
扩容时,令 capacity 为原来的两倍。
1.7时,需要new 一个新数组,并对旧数组上的所有元素进行indexFor()操作确定下标地址,这一步很费时,1.8时只需判断hash值的左边新增的那一位是否为1,即可判断此节点是留在原地lo还是移动去高位hi,如果为1,则移动去高位,否则不变
1.7时,扩容的时候可能出现死循环,1.8没有这个问题
构造方法
在第一次put()的时候,数组才初始化
数组的长度为大于指定值的最小二次幂
数组默认大小为16
1.判断key是否为null,如果为null,调用putlForNullKey,将key插入到数组下标为0的位置
2.调用hash()方法计算key的hashcode,得到hash值
3.调用indexFor()方法进行取模运算,得到元素的下标位置
1.indexFor方法为:h&(length - 1)
2.使用与运算,计算速度更快,因为二进制运算比十进制运算效率更高(十进制运算还需要将二进制转化为十进制)
3.length之所以要设定为2次幂,就是为了这个indexFor方法服务
4.可以让散列更加均匀,length-1的最后一位为1,因此进行与运算时,可以散列到奇数和偶数的下标位置,如果对length直接取模,由于length为2次幂,所以最后一位一定为0,所以与运算的结果一定是偶数,这也就导致奇数下标的位置不能被散列到。
4.依次和该下标位置上的链表中的node节点比较key是否相等
e.hash == hash && ((k = e.key) == key || key.equals(k))
首先判断e.hash==hash是因为不同的key值也可能被散列到同一个位置,因此首先判断hash值,如果不相等则两个key肯定不等
如果相等,再通过==和equals比较是否相等,之所以要先判断hash值是否相等,是因为equal()很耗性能,因此先判断hash值能够提高效率
重写了hashcode()方法就必须重写equals方法
5.如果相等,更新value值,如果不相等,使用头插法(1.7)/尾插法(1.8)将entry(1.7)/Node(1.8)插入到链表中
get()
和put()方法类似,获取到桶的下标,再在链表上查找key值,再获取key对应的value值
resize()
当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容
扩容时,令 capacity 为原来的两倍。
1.7时,需要new 一个新数组,并对旧数组上的所有元素进行indexFor()操作确定下标地址,这一步很费时,1.8时只需判断hash值的左边新增的那一位是否为1,即可判断此节点是留在原地lo还是移动去高位hi,如果为1,则移动去高位,否则不变
1.7时,扩容的时候可能出现死循环,1.8没有这个问题
构造方法
在第一次put()的时候,数组才初始化
数组的长度为大于指定值的最小二次幂
数组默认大小为16
多线程可能出现的问题
1.扩容时可能出现死循环
2.put的时候可能被失效/覆盖
线程A,B同时调用addEntry方法,同时获取到了相同的头节点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。
3.修改的时候可能被覆盖
线程A,B先后修改同一key值的value,会导致覆盖
4.put非null元素后get出来的却是null
扩容时调用的transfer方法,在获取数组的每个头节点的时候,在将e=头节点之后,都会将头节点置空,此时get可能导致获取到的值为0
ConcurrentHashMap
底层实现
1.7 segment数组+HashEntry数组(数组+链表)
chm由一个segment数组组成
segment
每个segment元素包含一个HashEntry数组,每个HashEntry包含一个链表
HashEntry大部分成员变量都为final
final k key
volatile V value
final int hash
final HashEntry<K,V> next
chm由一个segment数组组成
segment
每个segment元素包含一个HashEntry数组,每个HashEntry包含一个链表
HashEntry大部分成员变量都为final
final k key
volatile V value
final int hash
final HashEntry<K,V> next
1.8 数组+链表+红黑树
源码分析
put()
基本流程
基本流程
1.7 通过两次hash确定
第一次Hash定位到Segment
通过segmentFor()函数进行,计算方式也和indexFor()相同
SegmentMask
ssize-1
SegmentShift
32-sshift
ssize
是大于ConcurrentLevel的最小二次幂
第二次Hash定位到元素所在的链表的头部
定位方法和HashMap中的indexFor()相同
通过segment.lock加锁
1.8通过两次hash确定
通过CAS+synchronized加锁
1.如果没有hash冲突就直接通过CAS插入
2.如果有hash冲突或者CAS操作失败,说明存在并发情况,使用synchronized加锁
3.如果插入成功就调用addCount()方法统计size,并且检查是否需要扩容
1.7 通过两次hash确定
第一次Hash定位到Segment
通过segmentFor()函数进行,计算方式也和indexFor()相同
SegmentMask
ssize-1
SegmentShift
32-sshift
ssize
是大于ConcurrentLevel的最小二次幂
第二次Hash定位到元素所在的链表的头部
定位方法和HashMap中的indexFor()相同
通过segment.lock加锁
1.8通过两次hash确定
通过CAS+synchronized加锁
1.如果没有hash冲突就直接通过CAS插入
2.如果有hash冲突或者CAS操作失败,说明存在并发情况,使用synchronized加锁
3.如果插入成功就调用addCount()方法统计size,并且检查是否需要扩容
源码分析
1.ensureSegment
1.判断是否被其他线程初始化,这里使用了getObjectVolatile()方法
2.使用segment[0]的属性来初始化其他槽
3.使用while()循环,内部使用CAS操作,尝试初始化槽
2.使用segment[0]的属性来初始化其他槽
3.使用while()循环,内部使用CAS操作,尝试初始化槽
2.segment.put()
get()
get不需要加锁,因为HashEntry的value值设定为了volatile
如果get()到的是null值,则可能这个key,value对正在put的过程中,如果出现这种情况,那么就通过lock加锁来保证取出的value是完整的
如果get()到的是null值,则可能这个key,value对正在put的过程中,如果出现这种情况,那么就通过lock加锁来保证取出的value是完整的
resize()
构造函数
先根据ConcurrentLevel构造出Segment数组
Segment数组大小是不大于concurrentLevel的最大的2的指数
每个Segment中的HashEntry数组的大小都是大于指定大小的最小二次幂
每个hashEntry的大小为大于initialCapacity/concurrentLevel的最小二次幂
初始参数
initialCapacity(每个HashEntry的长度)
loadFactor:扩容因子
concurrencyLevel:并发度,指Segment数组的长度
Segment数组大小是不大于concurrentLevel的最大的2的指数
每个Segment中的HashEntry数组的大小都是大于指定大小的最小二次幂
每个hashEntry的大小为大于initialCapacity/concurrentLevel的最小二次幂
初始参数
initialCapacity(每个HashEntry的长度)
loadFactor:扩容因子
concurrencyLevel:并发度,指Segment数组的长度
remove
在定位到待删除元素的位置以后,程序就将待删除元素前面的那一些元素全部复制一遍,然后再一个一个重新接到链表上去。尾结点指向e的下一个结点。e后面的结点不需要复制,它们可以重用。
因为HashEntry中的next是final,所以只能先把待删除之前的元素复制了再删除
因为HashEntry中的next是final,所以只能先把待删除之前的元素复制了再删除
size
size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,就需要将所有的Segment都锁住,然后一个一个遍历了,
HashTable
HashTable是线程安全的,因为所有方法上都加了synchronized关键字。
HashTable的key和value都不可以为null。
扩容时,capacity=2*capacity+1
数组默认大小为11
查找下标时,没有使用hash&length-1,而是直接进行计算的
HashTable的key和value都不可以为null。
扩容时,capacity=2*capacity+1
数组默认大小为11
查找下标时,没有使用hash&length-1,而是直接进行计算的
TreeMap
底层实现为红黑树
能够保证树总是平衡的,如果插入删除导致树不平衡,会自动进行调整
变色
左旋
右旋
查找的平均时间复杂度为O(logN)
主要规则
1.每个节点或者是黑色,或者是红色。
2.根节点是黑色
3.叶子节点为黑色
4.如果一个节点是红色的,则它的子节点必须是黑色的
5.从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
TreeMap是一个有序的key-value集合,基于红黑树实现。该映射根据其键的自然顺序进行排序,或者根据创建时提供的Comparator进行排序
变色
左旋
右旋
查找的平均时间复杂度为O(logN)
主要规则
1.每个节点或者是黑色,或者是红色。
2.根节点是黑色
3.叶子节点为黑色
4.如果一个节点是红色的,则它的子节点必须是黑色的
5.从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
TreeMap是一个有序的key-value集合,基于红黑树实现。该映射根据其键的自然顺序进行排序,或者根据创建时提供的Comparator进行排序
TreeMap是一个有序的key-value集合,基于红黑树实现。该映射根据其键的自然顺序进行排序,或者根据创建时提供的Comparator进行排序
接口实现
NavigableMap
是SortedMap接口的子接口,在其基础上扩展了一些方法,例如floorEntry,lowEntry,ceilingEntry等
为了防止外部修改Entry,使用了ExportEntry修饰floorEntry等方法
是SortedMap接口的子接口,在其基础上扩展了一些方法,例如floorEntry,lowEntry,ceilingEntry等
为了防止外部修改Entry,使用了ExportEntry修饰floorEntry等方法
SortedMap
定义按照key排序的Map结构,能够令Map按照key的自然顺序或者构造器顺序进行排序。
定义按照key排序的Map结构,能够令Map按照key的自然顺序或者构造器顺序进行排序。
Entry
包含了left,right,parent节点
LinkedHashMap
底层是数组+链表+红黑树+双向链表
同时使用一个额外的双向链表来维护链表的访问顺序
维护链表顺序和访问顺序
LinkedHashMap 可以通过构造参数 accessOrder 来指定双向链表是否在元素被访问后改变其在双向链表中的位置。
当accessOrder为true时,get方法和put方法都会调用recordAccess方法使得最近使用的Entry移到双向链表的末尾;当accessOrder为默认值false时,recordAccess方法什么也不会做。
LRU实现
插入数据后对调用afterNodeInsertion,afterNodeInsertion()方法中会调用removeEldestEntry,如果removeEldestEntry(first)返回true,按照LRU策略,那么会删除头节点(注意这里删除的是头节点!!!所以每次访问元素或者插入元素之后都要将该元素放到链表末尾)。这个也是实现LRU的关键点!!!!!
关联关系提问
Set与Map的关系
1、Set的实现类
(1)HashSet:底层HashMap
(2)LinkedHashSet:底层是LinkedHashMap
(3)TreeSet:底层是TreeMap
2、Map的实现类
(1)HashMap
(2)LinkedHashMap
(3)TreeMap
3、源码跟踪
(1)问?HashSet和HashMap有关系吗?有, HashSet的底层就是HashMap
跟踪源码发现:
public HashSet() {
map = new HashMap<>(); ==> HashSet的底层就是HashMap
}
(2)问?但是我们存储到HashSet中的是一个一个的对象,而HashMap要求的是一对一对的键值对?
那么,怎么处理从一个对象到一对键值对的呢?
跟踪HashSet的add方法的源码:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
==>添加到set中的元素,是作为底层map的key
value是一个PRESENT对象。
再跟踪源码,HashSet中有一个:
private static final Object PRESENT = new Object();
==>所有HashSet中的(key,value)的value是共享同一个对象,Object类型的PRESENT
(1)HashSet:底层HashMap
(2)LinkedHashSet:底层是LinkedHashMap
(3)TreeSet:底层是TreeMap
2、Map的实现类
(1)HashMap
(2)LinkedHashMap
(3)TreeMap
3、源码跟踪
(1)问?HashSet和HashMap有关系吗?有, HashSet的底层就是HashMap
跟踪源码发现:
public HashSet() {
map = new HashMap<>(); ==> HashSet的底层就是HashMap
}
(2)问?但是我们存储到HashSet中的是一个一个的对象,而HashMap要求的是一对一对的键值对?
那么,怎么处理从一个对象到一对键值对的呢?
跟踪HashSet的add方法的源码:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
==>添加到set中的元素,是作为底层map的key
value是一个PRESENT对象。
再跟踪源码,HashSet中有一个:
private static final Object PRESENT = new Object();
==>所有HashSet中的(key,value)的value是共享同一个对象,Object类型的PRESENT
问题1:HashMap与Hashtable都是哈希表,有什么区别?
类似的区别:
StringBuffer和StringBuilder,Vector和ArrayList
Hashtable:旧版的,JDK1.0就有的
线程安全的
不允许key,value为null值
HashMap:后增的,JDK1.2加入的
线程不安全的
允许key,value为null值,key为null会特殊处理,存储在特殊的位置
StringBuffer和StringBuilder,Vector和ArrayList
Hashtable:旧版的,JDK1.0就有的
线程安全的
不允许key,value为null值
HashMap:后增的,JDK1.2加入的
线程不安全的
允许key,value为null值,key为null会特殊处理,存储在特殊的位置
问题2: HashMap和 LinkedHashMap的区别
类似的区别:
HashSet和LinkedHashSet
HashMap:无序的,存储和遍历的顺序和添加的顺序无关。
LinkedHashMap:会记录键值对的添加顺序,遍历时,会按照添加的顺序遍历
LinkedHashMap是HashMap的子类
HashSet和LinkedHashSet
HashMap:无序的,存储和遍历的顺序和添加的顺序无关。
LinkedHashMap:会记录键值对的添加顺序,遍历时,会按照添加的顺序遍历
LinkedHashMap是HashMap的子类
问题3:TreeMap有什么特点呢?
TreeMap的键值对存储和key的大小有关,所以要求key必须实现java.lang.Comparable接口
或者在创建TreeMap时传入java.util.Comparator接口的实现类对象
或者在创建TreeMap时传入java.util.Comparator接口的实现类对象
问题4:Properties有什么特点?
(1)Properties不需要指定泛型
(2)Properties继承Hashtable,不允许key,value为null
(3)Properties的key和value都是String类型
(4)它的作用是用来存储 系统属性
(5)建议添加(key,value)使用的是setProperty
(6)获取value的时候使用的是getProperty
(2)Properties继承Hashtable,不允许key,value为null
(3)Properties的key和value都是String类型
(4)它的作用是用来存储 系统属性
(5)建议添加(key,value)使用的是setProperty
(6)获取value的时候使用的是getProperty
类的初始化
1、静态代码块、静态成员变量只有第一次加载类时才会执行。
2、执行顺序为:父类静态代码块及父类静态成员变量(并列优先级)--->子类静态代码块及子类静态成员变量(并列优先级)--->父类普通代码块及父类成员变量--->父类构造器--->子类普通代码块及子类成员变量--->子类构造器。
3、静态内部类,只有在第一次调用的时候才会被初始化。
2、执行顺序为:父类静态代码块及父类静态成员变量(并列优先级)--->子类静态代码块及子类静态成员变量(并列优先级)--->父类普通代码块及父类成员变量--->父类构造器--->子类普通代码块及子类成员变量--->子类构造器。
3、静态内部类,只有在第一次调用的时候才会被初始化。
String
String类
特点
字符串不可变
举例
String s="java is good";String s2="hello";
基本操作
s.length()//返回长度
s.substring(int n,int m)//从字符串的下标n开始到m结束产生一个子字符串
s.toUpperCase()//将字符串转换成大写字母
s.toLowerCase()//将字符串转换成小写字母
s.trim()//返回删除了前后空格的字符串对象
s.isEmpty()//返回该字符串是否为空,返回false或者true
s.concat(s2);//两个字符串合成一个
s.replace(char old,char new);//将字符串中所有old字符改成new字符
s.charAt(int n)//返回字符串s的第n个字符
s.substring(int n,int m)//从字符串的下标n开始到m结束产生一个子字符串
s.toUpperCase()//将字符串转换成大写字母
s.toLowerCase()//将字符串转换成小写字母
s.trim()//返回删除了前后空格的字符串对象
s.isEmpty()//返回该字符串是否为空,返回false或者true
s.concat(s2);//两个字符串合成一个
s.replace(char old,char new);//将字符串中所有old字符改成new字符
s.charAt(int n)//返回字符串s的第n个字符
字符串查找
s.indexOf(int ch)//查找字符ch第一次出现的位置
s.lastIndexOf(int ch)//最后一次
s.indexOf(String str)//查找字符串str第一次出现的位置
s.lastIndexOf(String str)//最后一次
s.lastIndexOf(int ch)//最后一次
s.indexOf(String str)//查找字符串str第一次出现的位置
s.lastIndexOf(String str)//最后一次
字符串转数组
s.toCharArray()//将字符串转换为字符数组
s.getChars(int begin,int end,char[]a,int start)//将字符串中从begin到end的位置的字符复制给a[]数组,start为a[]数组的起始位置
byte a[]=s.getbyte()//将字符串中的字符编码成字节序列,返回给一个byte数组
s.getChars(int begin,int end,char[]a,int start)//将字符串中从begin到end的位置的字符复制给a[]数组,start为a[]数组的起始位置
byte a[]=s.getbyte()//将字符串中的字符编码成字节序列,返回给一个byte数组
字符串比较
s.equals(s2)//比较字符串s和s2的内容是否相等
s.equalsIgnoreCase(s2)//比较字符串s和s2的内容是否相等,不区分大小写
s.compareTo(s2)//比较字符串s和s2的大小,根据字符串的Unicode值进行比较 s>s2 返回正数
s.equalsIgnoreCase(s2)//比较字符串s和s2的内容是否相等,不区分大小写
s.compareTo(s2)//比较字符串s和s2的大小,根据字符串的Unicode值进行比较 s>s2 返回正数
字符串的拆分组合
String str[]=s.split(String regex)//regex是正则表达式,根据它来将字符串拆成字符串数组
s.matches(String regex)//返回字符串是否与给定的正则表达式匹配
String.join(Char delimiter,String[])//根据分隔符delimiter将字符数组组合成一个新的字符串,与s.split()刚好相反
正则表达式:正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
s.matches(String regex)//返回字符串是否与给定的正则表达式匹配
String.join(Char delimiter,String[])//根据分隔符delimiter将字符数组组合成一个新的字符串,与s.split()刚好相反
正则表达式:正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
String对象的不变性
在java中,一旦创建一个String对象,它的内容就不能修改了,它是不可变的。而一些对它操作的方法并没有改变它,只是操作后产生了一个新的字符串
命令行参数
public static void main(String []args){}中String []args是一个字符串数组,该参数在运行时通过命令行传递给main()方法
String字符串
特点
字符串可变
StringBuilder类
创建对象
public StringBuilder()/public StringBuilder(String str) 构造方法
在创建对象时,系统除了给字符串分配对象外,还会多分配16个字符的缓冲区。主要是为了方便对象的修改,它是可变对象
StringBuilder str=new StringBuilder("hello");
public StringBuilder()/public StringBuilder(String str) 构造方法
在创建对象时,系统除了给字符串分配对象外,还会多分配16个字符的缓冲区。主要是为了方便对象的修改,它是可变对象
StringBuilder str=new StringBuilder("hello");
StringBuffer类
创建对象
StringBuffer() :构造一个没有任何字符的StringBuffer类。
StringBuffer(int length) ::构造一个没有任何字符的StringBuffer类,并且,其长度为length。
StringBuffer(String str) :以str为初始值构造一个StringBuffer类。
StringBuffer() :构造一个没有任何字符的StringBuffer类。
StringBuffer(int length) ::构造一个没有任何字符的StringBuffer类,并且,其长度为length。
StringBuffer(String str) :以str为初始值构造一个StringBuffer类。
访问与修改
s.capacity()//返回当前字符缓冲区的总容量
s.setCharAt(int n,char ch)//用ch修改n位置的元素
s.append(String str)//在字符串末尾添加一个str字符串(它有很多重载方法,可插入各种数据类型 int char......)
s.insert(int n,String str)//在指定位置n处插入字符串(有多个重载方法,与上面类似)
s.deleteCharAt(int n)//删除指定位置的字符,后面的字符向前移动
s.delete(int start,int end)//删除从start到end的元素,不包括end
s.replace(int n,int m,String str)//用字符串str替换从n到m的字符,不包括m
s.reverse()//将字符串所有字符反转
s.substring(int start,int end)//返回从start到end 的字符串,不包括end
s.substring(int start)//返回从start开始到字符串末尾的子字符串
s.setCharAt(int n,char ch)//用ch修改n位置的元素
s.append(String str)//在字符串末尾添加一个str字符串(它有很多重载方法,可插入各种数据类型 int char......)
s.insert(int n,String str)//在指定位置n处插入字符串(有多个重载方法,与上面类似)
s.deleteCharAt(int n)//删除指定位置的字符,后面的字符向前移动
s.delete(int start,int end)//删除从start到end的元素,不包括end
s.replace(int n,int m,String str)//用字符串str替换从n到m的字符,不包括m
s.reverse()//将字符串所有字符反转
s.substring(int start,int end)//返回从start到end 的字符串,不包括end
s.substring(int start)//返回从start开始到字符串末尾的子字符串
Java字符串格式化
常规类型的格式化
format()方法有两种重载形式。
format(String format, Object… args) 新字符串使用本地语言环境,制定字符串格式和参数生成格式化的新字符串。
format(Locale locale, String format, Object… args) 使用指定的语言环境,制定字符串格式和参数生成格式化的字符串。
String str1=String.format("Hi,%s", "哈士奇");
System.out.println(str1);
String str2=String.format("Hi,%s:%s.%s", "老鹰","是一种","鸟类");
System.out.println(str2);
System.out.printf("字母h的大写是:%c %n", 'H');
System.out.printf("12.34>33.22的结果是:%b %n", 12.34>33.22);
System.out.printf("100的一半是:%d %n", 100/2);
System.out.printf("100的16进制数是:%x %n", 100);
System.out.printf("100的8进制数是:%o %n", 100);
System.out.printf("100元的书包打8.5折扣是:%f 元%n", 100*0.85);
System.out.printf("100的16进制浮点数是:%a %n", 100*0.85);
System.out.printf("100的指数表示:%e %n", 100*0.85);
System.out.printf("10的指数和浮点数结果的长度较短的是:%g %n", 100*0.85);
System.out.printf("100的折扣是%d%% %n", 85);
System.out.printf("字母A的散列码是:%h %n", 'A');
System.out.println(str1);
String str2=String.format("Hi,%s:%s.%s", "老鹰","是一种","鸟类");
System.out.println(str2);
System.out.printf("字母h的大写是:%c %n", 'H');
System.out.printf("12.34>33.22的结果是:%b %n", 12.34>33.22);
System.out.printf("100的一半是:%d %n", 100/2);
System.out.printf("100的16进制数是:%x %n", 100);
System.out.printf("100的8进制数是:%o %n", 100);
System.out.printf("100元的书包打8.5折扣是:%f 元%n", 100*0.85);
System.out.printf("100的16进制浮点数是:%a %n", 100*0.85);
System.out.printf("100的指数表示:%e %n", 100*0.85);
System.out.printf("10的指数和浮点数结果的长度较短的是:%g %n", 100*0.85);
System.out.printf("100的折扣是%d%% %n", 85);
System.out.printf("字母A的散列码是:%h %n", 'A');
搭配转换符的标志
日期和事件字符串格式化
Date date=new Date();
//c的使用
System.out.printf("全部日期和时间信息:%tc%n",date);
//f的使用
System.out.printf("年-月-日格式:%tF%n",date);
//d的使用
System.out.printf("月/日/年格式:%tD%n",date);
//r的使用
System.out.printf("HH:MM:SS PM格式(12时制):%tr%n",date);
//t的使用
System.out.printf("HH:MM:SS格式(24时制):%tT%n",date);
//R的使用
System.out.printf("HH:MM格式(24时制):%tR",date);
//c的使用
System.out.printf("全部日期和时间信息:%tc%n",date);
//f的使用
System.out.printf("年-月-日格式:%tF%n",date);
//d的使用
System.out.printf("月/日/年格式:%tD%n",date);
//r的使用
System.out.printf("HH:MM:SS PM格式(12时制):%tr%n",date);
//t的使用
System.out.printf("HH:MM:SS格式(24时制):%tT%n",date);
//R的使用
System.out.printf("HH:MM格式(24时制):%tR",date);
时间格式转换符
子主题
Date date = new Date();
//H的使用
System.out.printf("2位数字24时制的小时(不足2位前面补0):%tH%n", date);
//I的使用
System.out.printf("2位数字12时制的小时(不足2位前面补0):%tI%n", date);
//k的使用
System.out.printf("2位数字24时制的小时(前面不补0):%tk%n", date);
//l的使用
System.out.printf("2位数字12时制的小时(前面不补0):%tl%n", date);
//M的使用
System.out.printf("2位数字的分钟(不足2位前面补0):%tM%n", date);
//S的使用
System.out.printf("2位数字的秒(不足2位前面补0):%tS%n", date);
//L的使用
System.out.printf("3位数字的毫秒(不足3位前面补0):%tL%n", date);
//N的使用
System.out.printf("9位数字的毫秒数(不足9位前面补0):%tN%n", date);
//p的使用
String str = String.format(Locale.US, "小写字母的上午或下午标记(英):%tp", date);
System.out.println(str);
System.out.printf("小写字母的上午或下午标记(中):%tp%n", date);
//z的使用
System.out.printf("相对于GMT的RFC822时区的偏移量:%tz%n", date);
//Z的使用
System.out.printf("时区缩写字符串:%tZ%n", date);
//s的使用
System.out.printf("1970-1-1 00:00:00 到现在所经过的秒数:%ts%n", date);
//Q的使用
System.out.printf("1970-1-1 00:00:00 到现在所经过的毫秒数:%tQ%n", date);
//H的使用
System.out.printf("2位数字24时制的小时(不足2位前面补0):%tH%n", date);
//I的使用
System.out.printf("2位数字12时制的小时(不足2位前面补0):%tI%n", date);
//k的使用
System.out.printf("2位数字24时制的小时(前面不补0):%tk%n", date);
//l的使用
System.out.printf("2位数字12时制的小时(前面不补0):%tl%n", date);
//M的使用
System.out.printf("2位数字的分钟(不足2位前面补0):%tM%n", date);
//S的使用
System.out.printf("2位数字的秒(不足2位前面补0):%tS%n", date);
//L的使用
System.out.printf("3位数字的毫秒(不足3位前面补0):%tL%n", date);
//N的使用
System.out.printf("9位数字的毫秒数(不足9位前面补0):%tN%n", date);
//p的使用
String str = String.format(Locale.US, "小写字母的上午或下午标记(英):%tp", date);
System.out.println(str);
System.out.printf("小写字母的上午或下午标记(中):%tp%n", date);
//z的使用
System.out.printf("相对于GMT的RFC822时区的偏移量:%tz%n", date);
//Z的使用
System.out.printf("时区缩写字符串:%tZ%n", date);
//s的使用
System.out.printf("1970-1-1 00:00:00 到现在所经过的秒数:%ts%n", date);
//Q的使用
System.out.printf("1970-1-1 00:00:00 到现在所经过的毫秒数:%tQ%n", date);
数据结构和算法
数据结构
数组,链表
链表
概念
链表是一种上一个元素的引用指向下一个元素的存储结构,链表通过指针来连接元素与元素;
链表是线性表的一种,所谓的线性表包含顺序线性表和链表,顺序线性表是用数组实现的,在内存中有顺序排列,通过改变数组大小实现。而链表不是用顺序实现的,用指针实现,在内存中不连续。意思就是说,链表就是将一系列不连续的内存联系起来,将那种碎片内存进行合理的利用,解决空间的问题。
所以,链表允许插入和删除表上任意位置上的节点,但是不允许随即存取。链表有很多种不同的类型:单向链表、双向链表及循环链表。
链表是线性表的一种,所谓的线性表包含顺序线性表和链表,顺序线性表是用数组实现的,在内存中有顺序排列,通过改变数组大小实现。而链表不是用顺序实现的,用指针实现,在内存中不连续。意思就是说,链表就是将一系列不连续的内存联系起来,将那种碎片内存进行合理的利用,解决空间的问题。
所以,链表允许插入和删除表上任意位置上的节点,但是不允许随即存取。链表有很多种不同的类型:单向链表、双向链表及循环链表。
单向链表
单向链表包含两个域,一个是信息域,一个是指针域。也就是单向链表的节点被分成两部分,一部分是保存或显示关于节点的信息,第二部分存储下一个节点的地址,而最后一个节点则指向一个空值。
双向链表
从上图可以很清晰的看出,每个节点有2个链接,一个是指向前一个节点(当此链接为第一个链接时,指向的是空值或空列表),另一个则指向后一个节点(当此链接为最后一个链接时,指向的是空值或空列表)。意思就是说双向链表有2个指针,一个是指向前一个节点的指针,另一个则指向后一个节点的指针。
循环链表
循环链表就是首节点和末节点被连接在一起。循环链表中第一个节点之前就是最后一个节点,反之亦然。
数组与链表的区别
数组便于查询和修改,但是不方便新增和删除
链表的插入删除元素相对数组较为简单,不需要移动元素,且较为容易实现长度扩充,但是寻找某个元素较为困难;
树
二叉树概念
二叉树是每个结点最多有两个子树的树结构。
度为 2 的树要求每个节点最多只能有两棵子树,并且至少有一个节点有两棵子树。二叉树的要求是度不超过 2,就是说度也可以是 1 或者 0。二叉树还有一个重要特点,是左子树和右子树不一样;普通的树不分左右子树。
另外树的根节点数目可以是0或1,也就是可以有0个结点。而二叉树的根节点有且只有一个。这个是互相矛盾的,但是就是这么规定的。一些概念题可能会考到。
度为 2 的树要求每个节点最多只能有两棵子树,并且至少有一个节点有两棵子树。二叉树的要求是度不超过 2,就是说度也可以是 1 或者 0。二叉树还有一个重要特点,是左子树和右子树不一样;普通的树不分左右子树。
另外树的根节点数目可以是0或1,也就是可以有0个结点。而二叉树的根节点有且只有一个。这个是互相矛盾的,但是就是这么规定的。一些概念题可能会考到。
Trie(字典树)
字典树,又称为单词查找树,Tire数,是一种树形结构,它是一种哈希树的变种。
子主题
字典树满足:
1.根节点不包含字符,除根节点外的每一个子节点都包含一个字符
2.从根节点到某一节点。路径上经过的字符连接起来,就是该节点对应的字符串
3.每个节点的所有子节点包含的字符都不相同
字典树的典型应用是用于统计,排序和保存大量的字符串(不仅限于字符串),经常被搜索引擎系统用于文本词频统计。
利用字符串的公共前缀来减少查询时间,最大限度的减少无谓的字符串比较,查询效率比哈希树高。
1.根节点不包含字符,除根节点外的每一个子节点都包含一个字符
2.从根节点到某一节点。路径上经过的字符连接起来,就是该节点对应的字符串
3.每个节点的所有子节点包含的字符都不相同
字典树的典型应用是用于统计,排序和保存大量的字符串(不仅限于字符串),经常被搜索引擎系统用于文本词频统计。
利用字符串的公共前缀来减少查询时间,最大限度的减少无谓的字符串比较,查询效率比哈希树高。
Trie树的实现可以参考字典树(Trie树)实现与应用
KD树
k-d tree即k-dimensional tree,常用来作空间划分及近邻搜索,是二叉空间划分树的一个特例。
具体可以参考k-d tree算法原理及实现
KD树
二叉搜素树
概念
二叉排序树,又叫二叉查找树,它或者是一棵空树;或者是具有以下性质的二叉树:
1.若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
2.若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
3.它的左右子树也分别为二叉排序树。
如下图所示:
2.若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
3.它的左右子树也分别为二叉排序树。
如下图所示:
子主题
二叉搜索树的插入和删除可以参考算法与数据结构(十) 二叉排序树的查找、插入与删除。
二叉排序树的中序遍历可以得到元素的有序数组。
平衡二叉树
AVL树
概念
平衡二叉树,又称AVL树、平衡二叉排序树,它是一种特殊的二叉排序树。AVL树或者是一棵空树,或者是具有以下性质的二叉树:
1.左子树和右子树都是平衡二叉树;
2.左子树和右子树的深度(高度)之差的绝对值不超过1。
2.左子树和右子树的深度(高度)之差的绝对值不超过1。
子主题
其实就是平衡的二叉排序树,且每个子树都是平衡二叉排序树。这样,平衡二叉树的查找时间复杂度可以保证在O(logn)。
关于平衡二叉树失衡的调整——旋转:
旋转操作主要包括LL(左左)旋转、LR(左右)旋转、RR(右右)旋转、RL(右左)旋转,LL旋转与RR旋转对称,LR旋转与RL旋转对称。旋转操作是在插入结点或删除结点导致原AVL树不平衡时进行的。我的理解是当二叉树失衡的原因出现在“最低失衡根结点左子树的左子树”(所谓“最低失衡根结点”,则是从新增结点开始向根部回溯,所遇到的第一个失衡的根节点)时,则使用LL旋转来调整;当失衡出现在“最低失衡根节点左子树的右子树”,则使用LR旋转调整;RR旋转,RL旋转同理。
LL旋转即“最低失衡根节点”左子树的左子树上的节点导致失衡(又叫R旋转:让目标节点变成右孩子)。对应旋转方法:将“最低失衡根节点”左孩子作为新根节点;根节点作为新根节点右孩子;新根节点本来的右孩子作为根节点左孩子。
RR旋转即“最低失衡根节点”右子树的右子树上的节点导致失衡(又叫L旋转:让目标节点变成左孩子)。对应旋转方法:将“最低失衡根节点”右孩子作为新根节点;根节点作为新根节点左孩子;新根节点本来的左孩子作为根节点右孩子。
LR旋转即“最低失衡根节点”左子树的右子树上的节点导致失衡,需要两次旋转:对“最低失衡根节点”左孩子进行RR旋转;对“最低失衡根节点”进行LL旋转。
关于平衡二叉树失衡的调整——旋转:
旋转操作主要包括LL(左左)旋转、LR(左右)旋转、RR(右右)旋转、RL(右左)旋转,LL旋转与RR旋转对称,LR旋转与RL旋转对称。旋转操作是在插入结点或删除结点导致原AVL树不平衡时进行的。我的理解是当二叉树失衡的原因出现在“最低失衡根结点左子树的左子树”(所谓“最低失衡根结点”,则是从新增结点开始向根部回溯,所遇到的第一个失衡的根节点)时,则使用LL旋转来调整;当失衡出现在“最低失衡根节点左子树的右子树”,则使用LR旋转调整;RR旋转,RL旋转同理。
LL旋转即“最低失衡根节点”左子树的左子树上的节点导致失衡(又叫R旋转:让目标节点变成右孩子)。对应旋转方法:将“最低失衡根节点”左孩子作为新根节点;根节点作为新根节点右孩子;新根节点本来的右孩子作为根节点左孩子。
RR旋转即“最低失衡根节点”右子树的右子树上的节点导致失衡(又叫L旋转:让目标节点变成左孩子)。对应旋转方法:将“最低失衡根节点”右孩子作为新根节点;根节点作为新根节点左孩子;新根节点本来的左孩子作为根节点右孩子。
LR旋转即“最低失衡根节点”左子树的右子树上的节点导致失衡,需要两次旋转:对“最低失衡根节点”左孩子进行RR旋转;对“最低失衡根节点”进行LL旋转。
RL对称。具体流程以及代码可以参考:平衡二叉树。
红黑树
概念
红黑树是一棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是RED或者BLACK。通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,红黑树可以确保没有一条路径会比其他路径长出2倍,因而是近似平衡的。
红黑树满足红黑性质:
1.每个节点是红色或者黑色的
2.根节点是黑色的,叶节点(NIL)是黑色的
3.红色节点的两个子节点都是黑色的
4.对每个结点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。(因为所有路径的黑色节点相同,且红色节点的子节点必然是黑色,于是最长的路径中,是红黑相间的(2n-1),最短路径是纯黑大的,差为(n-1),小于n,故没有一条路径会比其他路径长出2倍)。
红黑树的插入和删除时间复杂度都在O(logn)。
具体可以参考:看这篇得到红黑树插入的整体结构 看这篇的“红叔”部分讲解,其他有错。
稍稍总结一下:
红黑树的插入分为一下情况:
--黑父: 直接插入
--红父
---红叔: 向上变黑
---黑叔: 看结构,和AVL四种旋转基本相同。
红黑树的删除可以参考。
1.每个节点是红色或者黑色的
2.根节点是黑色的,叶节点(NIL)是黑色的
3.红色节点的两个子节点都是黑色的
4.对每个结点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。(因为所有路径的黑色节点相同,且红色节点的子节点必然是黑色,于是最长的路径中,是红黑相间的(2n-1),最短路径是纯黑大的,差为(n-1),小于n,故没有一条路径会比其他路径长出2倍)。
红黑树的插入和删除时间复杂度都在O(logn)。
具体可以参考:看这篇得到红黑树插入的整体结构 看这篇的“红叔”部分讲解,其他有错。
稍稍总结一下:
红黑树的插入分为一下情况:
--黑父: 直接插入
--红父
---红叔: 向上变黑
---黑叔: 看结构,和AVL四种旋转基本相同。
红黑树的删除可以参考。
看这篇得到红黑树插入的整体结构
看这篇的“红叔”部分讲解,其他有错。
红黑树的删除可以参考。
红黑树和AVL平衡二叉树的区别:
红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。
平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
B树
我们都知道二叉查找树的查找的时间复杂度是O(log N),其查找效率已经足够高了,那为什么还有B树和B+树的出现呢?难道它两的时间复杂度比二叉查找树还小吗?
答案当然不是,B树和B+树的出现是因为另外一个问题,那就是磁盘IO;众所周知,IO操作的效率很低,那么,当在大量数据存储中,查询时我们不能一下子将所有数据加载到内存中,只能逐一加载磁盘页,每个磁盘页对应树的节点。造成大量磁盘IO操作(最坏情况下为树的高度)。平衡二叉树由于树深度过大而造成磁盘IO读写过于频繁,进而导致效率低下。
答案当然不是,B树和B+树的出现是因为另外一个问题,那就是磁盘IO;众所周知,IO操作的效率很低,那么,当在大量数据存储中,查询时我们不能一下子将所有数据加载到内存中,只能逐一加载磁盘页,每个磁盘页对应树的节点。造成大量磁盘IO操作(最坏情况下为树的高度)。平衡二叉树由于树深度过大而造成磁盘IO读写过于频繁,进而导致效率低下。
所以,我们为了减少磁盘IO的次数,就你必须降低树的深度,将“瘦高”的树变得“矮胖”。一个基本的想法就是:
(1)、每个节点存储多个元素
(2)、摒弃二叉树结构,采用多叉树
这样就引出来了一个新的查找树结构 ——多路查找树。 根据AVL给我们的启发,一颗平衡多路查找树(B~树)自然可以使得数据的查找效率保证在O(logN)这样的对数级别上。
(1)、每个节点存储多个元素
(2)、摒弃二叉树结构,采用多叉树
这样就引出来了一个新的查找树结构 ——多路查找树。 根据AVL给我们的启发,一颗平衡多路查找树(B~树)自然可以使得数据的查找效率保证在O(logN)这样的对数级别上。
一个m阶的B树(Balance Tree)具有如下几个特征:B树中所有结点的孩子结点最大值称为B树的阶,通常用m表示。一个结点有k个孩子时,必有k-1个关键字才能将子树中所有关键字划分为k个子集。
1.根结点至少有两个子女。
2.每个中间节点都包含k-1个元素和k个孩子,其中 ceil(m/2) ≤ k ≤ m
3.每一个叶子节点都包含k-1个元素,其中 ceil(m/2) ≤ k ≤ m
4.所有的叶子结点都位于同一层。
5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域划分
6.每个结点的结构为:(n,A0,K1,A1,K2,A2,… ,Kn,An)
其中,Ki(1≤i≤n)为关键字,且Ki<Ki+1(1≤i≤n-1)。
Ai(0≤i≤n)为指向子树根结点的指针。且Ai所指子树所有结点中的关键字均小于Ki+1。
n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1。
1.根结点至少有两个子女。
2.每个中间节点都包含k-1个元素和k个孩子,其中 ceil(m/2) ≤ k ≤ m
3.每一个叶子节点都包含k-1个元素,其中 ceil(m/2) ≤ k ≤ m
4.所有的叶子结点都位于同一层。
5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域划分
6.每个结点的结构为:(n,A0,K1,A1,K2,A2,… ,Kn,An)
其中,Ki(1≤i≤n)为关键字,且Ki<Ki+1(1≤i≤n-1)。
Ai(0≤i≤n)为指向子树根结点的指针。且Ai所指子树所有结点中的关键字均小于Ki+1。
n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1。
查询
以上图为例:若查询的数值为5:
第一次磁盘IO:在内存中定位(与17、35比较),比17小,左子树;
第二次磁盘IO:在内存中定位(与8、12比较),比8小,左子树;
第三次磁盘IO:在内存中定位(与3、5比较),找到5,终止。
插入
整个过程中,我们可以看出:比较的次数并不比二叉查找树少,尤其适当某一节点中的数据很多时,但是磁盘IO的次数却是大大减少。比较是在内存中进行的,相比于磁盘IO的速度,比较的耗时几乎可以忽略。所以当树的高度足够低的话,就可以极大的提高效率。相比之下,节点中的元素多点也没关系,仅仅是多了几次内存交互而已,只要不超过磁盘页的大小即可。
对高度为k的m阶B树,新结点一般是插在叶子层。通过检索可以确定关键码应插入的结点位置。然后分两种情况讨论:
1、 若该结点中关键码个数小于m-1,则直接插入即可。
2、 若该结点中关键码个数等于m-1,则将引起结点的分裂。以中间关键码为界将结点一分为二,产生一个新结点,并把中间关键码插入到父结点(k-1层)中
重复上述工作,最坏情况一直分裂到根结点,建立一个新的根结点,整个B树增加一层。
以上图为例:若查询的数值为5:
第一次磁盘IO:在内存中定位(与17、35比较),比17小,左子树;
第二次磁盘IO:在内存中定位(与8、12比较),比8小,左子树;
第三次磁盘IO:在内存中定位(与3、5比较),找到5,终止。
插入
整个过程中,我们可以看出:比较的次数并不比二叉查找树少,尤其适当某一节点中的数据很多时,但是磁盘IO的次数却是大大减少。比较是在内存中进行的,相比于磁盘IO的速度,比较的耗时几乎可以忽略。所以当树的高度足够低的话,就可以极大的提高效率。相比之下,节点中的元素多点也没关系,仅仅是多了几次内存交互而已,只要不超过磁盘页的大小即可。
对高度为k的m阶B树,新结点一般是插在叶子层。通过检索可以确定关键码应插入的结点位置。然后分两种情况讨论:
1、 若该结点中关键码个数小于m-1,则直接插入即可。
2、 若该结点中关键码个数等于m-1,则将引起结点的分裂。以中间关键码为界将结点一分为二,产生一个新结点,并把中间关键码插入到父结点(k-1层)中
重复上述工作,最坏情况一直分裂到根结点,建立一个新的根结点,整个B树增加一层。
子主题
子主题
如图就是在一个二阶B树中插入4的过程,35,26节点依次分裂。
删除
B树的删除较为复杂,具体参考B树和B+树的插入、删除图文详解或简单剖析B树。
总结一下,B树B+树的插入:可以插(插完仍满足定义要求)直接插;不可插(插完节点太大),分裂向上调整,中值作为分裂子树的根节点,归入父节点,若归入后父节点不满足定义,递归此过程。
删除:可以删(删完仍满足定义要求)直接删;删完节点过小(k<[m/2]-1),先从左右兄弟找结点,不然从父节点找结点向下调整,若调整完父节点不满足定义,递归此过程。
B+树也一样,不过更简单一些,只需在叶子节点进行操作。
删除
B树的删除较为复杂,具体参考B树和B+树的插入、删除图文详解或简单剖析B树。
总结一下,B树B+树的插入:可以插(插完仍满足定义要求)直接插;不可插(插完节点太大),分裂向上调整,中值作为分裂子树的根节点,归入父节点,若归入后父节点不满足定义,递归此过程。
删除:可以删(删完仍满足定义要求)直接删;删完节点过小(k<[m/2]-1),先从左右兄弟找结点,不然从父节点找结点向下调整,若调整完父节点不满足定义,递归此过程。
B+树也一样,不过更简单一些,只需在叶子节点进行操作。
应用
1.B树主要用于文件系统以及部分数据库索引,例如: MongoDB。而大部分关系数据库则使用B+树做索引,例如:mysql数据库;
2.从查找效率考虑一般要求B树的阶数m >= 3;
3.B-树上算法的执行时间主要由读、写磁盘的次数来决定,故一次I/O操作应读写尽可能多的信息。因此B-树的结点规模一般以一个磁盘页为单位。一个结点包含的关键字及其孩子个数取决于磁盘页的大小。
1.B树主要用于文件系统以及部分数据库索引,例如: MongoDB。而大部分关系数据库则使用B+树做索引,例如:mysql数据库;
2.从查找效率考虑一般要求B树的阶数m >= 3;
3.B-树上算法的执行时间主要由读、写磁盘的次数来决定,故一次I/O操作应读写尽可能多的信息。因此B-树的结点规模一般以一个磁盘页为单位。一个结点包含的关键字及其孩子个数取决于磁盘页的大小。
B+树
B+树是B树的变种,有着比B树更高的查询效率。一个m阶的B+树具有如下几个特征:
1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
4.B+树中,叶子元素指向对应元素以及该索引下其他相关元素,且这些元素之间按顺序有链式指针链接起来。(下图中,5->8->9->10->15->18->...->96->99)
1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
4.B+树中,叶子元素指向对应元素以及该索引下其他相关元素,且这些元素之间按顺序有链式指针链接起来。(下图中,5->8->9->10->15->18->...->96->99)
子主题
B+树的优势在于查找效率上, 首先,B+树的查找和B树一样,类似于二叉查找树。起始于根节点,自顶向下遍历树,选择其分离值在要查找值的任意一边的子指针。在节点内部典型的使用是二分查找来确定这个位置。
不同的是,B+树中间节点没有卫星数据(索引元素所指向的数据记录),只有索引,而B树每个结点中的每个关键字都有卫星数据;这就意味着同样的大小的磁盘页可以容纳更多节点元素,在相同的数据量下,B+树更加“矮胖”,IO操作更少 ; 其次,因为卫星数据的不同,导致查询过程也不同。B树的查找只需找到匹配元素即可,最好情况下查找到根节点,最坏情况下查找到叶子结点,所说性能很不稳定,而B+树每次必须查找到叶子结点,性能稳定; 最后,在范围查询方面,所有叶子节点形成有序链表,便于范围查询,B+树的优势更加明显。
B树的范围查找需要不断依赖中序遍历。首先二分查找到范围下限,在不断通过中序遍历,知道查找到范围的上限即可。整个过程比较耗时。 而B+树的范围查找则简单了许多。首先通过二分查找,找到范围下限,然后同过叶子结点的链表顺序遍历,直至找到上限即可,整个过程简单许多,效率也比较高。
例如:同样查找范围[3-11],两者的查询过程如下:
B树的查找过程:
不同的是,B+树中间节点没有卫星数据(索引元素所指向的数据记录),只有索引,而B树每个结点中的每个关键字都有卫星数据;这就意味着同样的大小的磁盘页可以容纳更多节点元素,在相同的数据量下,B+树更加“矮胖”,IO操作更少 ; 其次,因为卫星数据的不同,导致查询过程也不同。B树的查找只需找到匹配元素即可,最好情况下查找到根节点,最坏情况下查找到叶子结点,所说性能很不稳定,而B+树每次必须查找到叶子结点,性能稳定; 最后,在范围查询方面,所有叶子节点形成有序链表,便于范围查询,B+树的优势更加明显。
B树的范围查找需要不断依赖中序遍历。首先二分查找到范围下限,在不断通过中序遍历,知道查找到范围的上限即可。整个过程比较耗时。 而B+树的范围查找则简单了许多。首先通过二分查找,找到范围下限,然后同过叶子结点的链表顺序遍历,直至找到上限即可,整个过程简单许多,效率也比较高。
例如:同样查找范围[3-11],两者的查询过程如下:
B树的查找过程:
子主题
B+树的查找过程:
图
栈
队列
单端队列
双端队列
并查集
布隆过滤器
LRU缓存
LFU缓存
二叉堆
单调栈
算法
排序
递归
贪心
搜素
位运算
最短路径
高级搜素
动态规划
二分搜素
滑动窗口
字符串
并发
多线程基础
实现多线程的方式
没有返回值
继承Thread类
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
实现Runnable接口
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
public void run() {
System.out.println("MyThread.run()");
}
}
为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
Thread thread = new Thread(myThread);
thread.start();
事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码
public void run() {
if (target != null) {
target.run();
}
}
if (target != null) {
target.run();
}
}
带返回值
实现Callable接口通过FutureTask包装器来创建Thread线程
Callable接口(也只有一个方法)定义如下
public class SomeCallable<V> extends OtherClass implements Callable<V> {
@Override
public V call() throws Exception {
// TODO Auto-generated method stub
return null;
}
}
@Override
public V call() throws Exception {
// TODO Auto-generated method stub
return null;
}
}
Callable<V> oneCallable = new SomeCallable<V>();
//由Callable<Integer>创建一个FutureTask<Integer>对象:
FutureTask<V> oneTask = new FutureTask<V>(oneCallable);
//注释:FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。
//由FutureTask<Integer>创建一个Thread对象:
Thread oneThread = new Thread(oneTask);
oneThread.start();
//至此,一个线程就创建完成了。
//由Callable<Integer>创建一个FutureTask<Integer>对象:
FutureTask<V> oneTask = new FutureTask<V>(oneCallable);
//注释:FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。
//由FutureTask<Integer>创建一个Thread对象:
Thread oneThread = new Thread(oneTask);
oneThread.start();
//至此,一个线程就创建完成了。
使用ExecutorService、Callable、Future实现有返回结果的多线程
ExecutorService、Callable、Future三个接口实际上都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,有了这种特征就不需要再为了得到返回值而大费周折了。而且自己实现了也可能漏洞百出。
可返回值的任务必须实现Callable接口。类似的,无返回值的任务必须实现Runnable接口。
执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。
注意:get方法是阻塞的,即:线程无返回结果,get方法会一直等待。
再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。
可返回值的任务必须实现Callable接口。类似的,无返回值的任务必须实现Runnable接口。
执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。
注意:get方法是阻塞的,即:线程无返回结果,get方法会一直等待。
再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。
下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:
import java.util.concurrent.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
/**
* 有返回值的线程
*/
@SuppressWarnings("unchecked")
public class Test {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
System.out.println("----程序开始运行----");
Date date1 = new Date();
int taskSize = 5;
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取Future对象
Future f = pool.submit(c);
// System.out.println(">>>" + f.get().toString());
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>" + f.get().toString());
}
Date date2 = new Date();
System.out.println("----程序结束运行----,程序运行时间【"
+ (date2.getTime() - date1.getTime()) + "毫秒】");
}
}
class MyCallable implements Callable<Object> {
private String taskNum;
MyCallable(String taskNum) {
this.taskNum = taskNum;
}
public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任务启动");
Date dateTmp1 = new Date();
Thread.sleep(1000);
Date dateTmp2 = new Date();
long time = dateTmp2.getTime() - dateTmp1.getTime();
System.out.println(">>>" + taskNum + "任务终止");
return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
}
}
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
/**
* 有返回值的线程
*/
@SuppressWarnings("unchecked")
public class Test {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
System.out.println("----程序开始运行----");
Date date1 = new Date();
int taskSize = 5;
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取Future对象
Future f = pool.submit(c);
// System.out.println(">>>" + f.get().toString());
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>" + f.get().toString());
}
Date date2 = new Date();
System.out.println("----程序结束运行----,程序运行时间【"
+ (date2.getTime() - date1.getTime()) + "毫秒】");
}
}
class MyCallable implements Callable<Object> {
private String taskNum;
MyCallable(String taskNum) {
this.taskNum = taskNum;
}
public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任务启动");
Date dateTmp1 = new Date();
Thread.sleep(1000);
Date dateTmp2 = new Date();
long time = dateTmp2.getTime() - dateTmp1.getTime();
System.out.println(">>>" + taskNum + "任务终止");
return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
}
}
代码说明:
上述代码中Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。
上述代码中Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。
如何停止线程
使用标志位终止线程
在 run() 方法执行完毕后,该线程就终止了。但是在某些特殊的情况下,run() 方法会被一直执行;比如在服务端程序中可能会使用 while(true) { ... } 这样的循环结构来不断的接收来自客户端的请求。此时就可以用修改标志位的方式来结束 run() 方法
public class ServerThread extends Thread {
//volatile修饰符用来保证其它线程读取的总是该变量的最新的值
public volatile boolean exit = false;
@Override
public void run() {
ServerSocket serverSocket = new ServerSocket(8080);
while(!exit){
serverSocket.accept(); //阻塞等待客户端消息
...
}
}
public static void main(String[] args) {
ServerThread t = new ServerThread();
t.start();
...
t.exit = true; //修改标志位,退出线程
}
}
//volatile修饰符用来保证其它线程读取的总是该变量的最新的值
public volatile boolean exit = false;
@Override
public void run() {
ServerSocket serverSocket = new ServerSocket(8080);
while(!exit){
serverSocket.accept(); //阻塞等待客户端消息
...
}
}
public static void main(String[] args) {
ServerThread t = new ServerThread();
t.start();
...
t.exit = true; //修改标志位,退出线程
}
}
使用 stop() 终止线程
通过查看 JDK 的 API,我们会看到 java.lang.Thread 类型提供了一系列的方法如 start()、stop()、resume()、suspend()、destory()等方法来管理线程。但是除了 start() 之外,其它几个方法都被声名为已过时(deprecated)。
虽然 stop() 方法确实可以停止一个正在运行的线程,但是这个方法是不安全的,而且该方法已被弃用,最好不要使用它。
JDK 文档中还引入用一篇文章来解释了弃用这些方法的原因:《Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?》
为什么弃用stop:
调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
例如,存在一个对象 u 持有 ID 和 NAME 两个字段,假如写入线程在写对象的过程中,只完成了对 ID 的赋值,但没来得及为 NAME 赋值,就被 stop() 导致锁被释放,那么当读取线程得到锁之后再去读取对象 u 的 ID 和 Name 时,就会出现数据不一致的问题,如下图:
虽然 stop() 方法确实可以停止一个正在运行的线程,但是这个方法是不安全的,而且该方法已被弃用,最好不要使用它。
JDK 文档中还引入用一篇文章来解释了弃用这些方法的原因:《Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?》
为什么弃用stop:
调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
例如,存在一个对象 u 持有 ID 和 NAME 两个字段,假如写入线程在写对象的过程中,只完成了对 ID 的赋值,但没来得及为 NAME 赋值,就被 stop() 导致锁被释放,那么当读取线程得到锁之后再去读取对象 u 的 ID 和 Name 时,就会出现数据不一致的问题,如下图:
使用 interrupt() 中断线程
现在我们知道了使用 stop() 方式停止线程是非常不安全的方式,那么我们应该使用什么方法来停止线程呢?答案就是使用 interrupt() 方法来中断线程。
需要明确的一点的是:interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。
也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。这一点很重要,如果中断后,线程立即无条件退出,那么我们又会遇到 stop() 方法的老问题。
事实上,如果一个线程不能被 interrupt,那么 stop 方法也不会起作用。
我们来看一个使用 interrupt() 的例子:
需要明确的一点的是:interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。
也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。这一点很重要,如果中断后,线程立即无条件退出,那么我们又会遇到 stop() 方法的老问题。
事实上,如果一个线程不能被 interrupt,那么 stop 方法也不会起作用。
我们来看一个使用 interrupt() 的例子:
public class InterruptThread1 extends Thread{
public static void main(String[] args) {
try {
InterruptThread1 t = new InterruptThread1();
t.start();
Thread.sleep(200);
t.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
super.run();
for(int i = 0; i <= 200000; i++) {
System.out.println("i=" + i);
}
}
}
public static void main(String[] args) {
try {
InterruptThread1 t = new InterruptThread1();
t.start();
Thread.sleep(200);
t.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
super.run();
for(int i = 0; i <= 200000; i++) {
System.out.println("i=" + i);
}
}
}
线程的生命周期与状态
关于Java中线程的生命周期,首先看一下下面这张较为经典的图
子主题
上图中基本上囊括了Java中多线程各重要知识点。掌握了上图中的各知识点,Java中的多线程也就基本上掌握了。主要包括:
Java线程具有七种基本状态
新建状态(New)
至今尚未启动的线程的状态。线程刚被创建,但尚未启动。如:Thread t = new MyThread();
就绪状态(Runnable)
当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running)
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
无限期等待(Waiting)
位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中,这涉及到“线程通信”的内容。处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线程显示唤醒。以下方法会让线程陷入无限期的等待状态:
没有设置timeout参数的Object::wait()方法
没有设置timeout参数的Thread::join()方法
LockSupport::park()方法
没有设置timeout参数的Thread::join()方法
LockSupport::park()方法
限期等待(Timed Waiting)
处于这种状态的线程也不会被分配处理器执行时间,不过无须等待其他线程显示唤醒,在一定时间后它们由系统自动唤醒。以下方法会让线程进入期限等待状态:
Thread::sleep()方法
设置了timeout参数的Object::wait()方法
设置了timeout参数的Thread::join()方法
LockSupport::parkNanos()方法
LockSupport::parkUntil()方法
设置了timeout参数的Object::wait()方法
设置了timeout参数的Thread::join()方法
LockSupport::parkNanos()方法
LockSupport::parkUntil()方法
阻塞状态(Blocked)
处于运行状态中的线程由于某种(当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中,这涉及到“线程同步”的内容。【线程在获取synchronized同步锁失败(因为锁被其它线程所占用)】)原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。
“阻塞状态”与“等待状态”的区别:“阻塞状态”在等待着获取一个排它锁,这个事件将在另一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作发生。在程序进入同步区域的时候,线程就会进入阻塞状态。
死亡状态(Dead)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
JVM线程运行状态 (JVM Thread Status)
在 java.lang.Thread.State 中定义了线程的状态:
NEW
至今尚未启动的线程的状态。线程刚被创建,但尚未启动。
RUNNABLE
可运行线程的线程状态。线程正在JVM中执行,有可能在等待操作系统中的其他资源,比如处理器。
BLOCKED
受阻塞并且正在等待监视器的某一线程的线程状态。处于受阻塞状态的某一线程正在等待监视器锁,以便进入一个同步的块/方法,或者在调用 Object.wait 之后再次进入同步的块/方法。在Thread Dump日志中通常显示为 java.lang.Thread.State: BLOCKED (on object monitor) 。
WAITING
某一等待线程的线程状态。线程正在无期限地等待另一个线程来执行某一个特定的操作,线程因为调用下面的方法之一而处于等待状态:
不带超时的 Object.wait 方法,日志中显示为 java.lang.Thread.State: WAITING (on object monitor)
不带超时的 Thread.join 方法
LockSupport.park 方法,日志中显示为 java.lang.Thread.State: WAITING (parking)
不带超时的 Object.wait 方法,日志中显示为 java.lang.Thread.State: WAITING (on object monitor)
不带超时的 Thread.join 方法
LockSupport.park 方法,日志中显示为 java.lang.Thread.State: WAITING (parking)
TIMED_WAITING
指定了等待时间的某一等待线程的线程状态。线程正在等待另一个线程来执行某一个特定的操作,并设定了指定等待的时间,线程因为调用下面的方法之一而处于定时等待状态:
Thread.sleep 方法
指定超时值的 Object.wait 方法
指定超时值的 Thread.join 方法
LockSupport.parkNanos
LockSupport.parkUntil
Thread.sleep 方法
指定超时值的 Object.wait 方法
指定超时值的 Thread.join 方法
LockSupport.parkNanos
LockSupport.parkUntil
TERMINATED
线程处于终止状态。
根据Java Doc中的说明,在给定的时间上,一个只能处于上述的一种状态之中,并且这些状态都是JVM的状态,跟操作系统中的线程状态无关。
根据Java Doc中的说明,在给定的时间上,一个只能处于上述的一种状态之中,并且这些状态都是JVM的状态,跟操作系统中的线程状态无关。
JAVA虚拟机启动程序步骤:
(1) Main是启动时候的主线程,即程序入口
(2) 在main函数结束后,虚拟机会自动启动一个DestroyJavaVM线程,该线程会等待所有user thread 线程结束后退出(即,只剩下daemon 线程和DestroyJavaVM线程自己,整个虚拟机就退出,此时daemon线程被终止),因此,如果不希望程序退出,只要创建一个非daemon的子线程,让线程不停的sleep即可。
线程的创建
Thread类,有一个start方法,即启动该线程。 启动的线程会执行该类的run方法。注意:因为启动线程时要执行某个过程,因此,通常是需要重新实现run方法的
线程的结束
run模块执行完成主动退出,或者被其他线程强行终止。
Thread类,有一个start方法,即启动该线程。 启动的线程会执行该类的run方法。注意:因为启动线程时要执行某个过程,因此,通常是需要重新实现run方法的
线程的结束
run模块执行完成主动退出,或者被其他线程强行终止。
案例
通过jstack pid >1.txt
线程状态样例
等待状态样例
子主题
"IoWaitThread" prio=6 tid=0x0000000007334800 nid=0x2b3c waiting on condition [0x000000000893f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007d5c45850> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:156)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1987)
at java.util.concurrent.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:440)
at java.util.concurrent.LinkedBlockingDeque.take(LinkedBlockingDeque.java:629)
at com.nbp.theplatform.threaddump.ThreadIoWaitState$IoWaitHandler2.run(ThreadIoWaitState.java:89)
at java.lang.Thread.run(Thread.java:662)
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007d5c45850> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:156)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1987)
at java.util.concurrent.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:440)
at java.util.concurrent.LinkedBlockingDeque.take(LinkedBlockingDeque.java:629)
at com.nbp.theplatform.threaddump.ThreadIoWaitState$IoWaitHandler2.run(ThreadIoWaitState.java:89)
at java.lang.Thread.run(Thread.java:662)
上面例子中,IoWaitThread 线程保持等待状态并从 LinkedBlockingQueue 接收消息,如果 LinkedBlockingQueue 一直没有消息,该线程的状态将不会改变。
阻塞状态样例
子主题
"BLOCKED_TEST pool-1-thread-1" prio=6 tid=0x0000000006904800 nid=0x28f4 runnable [0x000000000785f000]
java.lang.Thread.State: RUNNABLE
at java.io.FileOutputStream.writeBytes(Native Method)
at java.io.FileOutputStream.write(FileOutputStream.java:282)
at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:65)
at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:123)
- locked <0x0000000780a31778> (a java.io.BufferedOutputStream)
at java.io.PrintStream.write(PrintStream.java:432)
- locked <0x0000000780a04118> (a java.io.PrintStream)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:202)
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:272)
at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:85)
- locked <0x0000000780a040c0> (a java.io.OutputStreamWriter)
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:168)
at java.io.PrintStream.newLine(PrintStream.java:496)
- locked <0x0000000780a04118> (a java.io.PrintStream)
at java.io.PrintStream.println(PrintStream.java:687)
- locked <0x0000000780a04118> (a java.io.PrintStream)
at com.nbp.theplatform.threaddump.ThreadBlockedState.monitorLock(ThreadBlockedState.java:44)
- locked <0x0000000780a000b0> (a com.nbp.theplatform.threaddump.ThreadBlockedState)
at com.nbp.theplatform.threaddump.ThreadBlockedState$1.run(ThreadBlockedState.java:7)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
Locked ownable synchronizers:
- <0x0000000780a31758> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
"BLOCKED_TEST pool-1-thread-2" prio=6 tid=0x0000000007673800 nid=0x260c waiting for monitor entry [0x0000000008abf000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.nbp.theplatform.threaddump.ThreadBlockedState.monitorLock(ThreadBlockedState.java:43)
- waiting to lock <0x0000000780a000b0> (a com.nbp.theplatform.threaddump.ThreadBlockedState)
at com.nbp.theplatform.threaddump.ThreadBlockedState$2.run(ThreadBlockedState.java:26)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
Locked ownable synchronizers:
- <0x0000000780b0c6a0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
"BLOCKED_TEST pool-1-thread-3" prio=6 tid=0x00000000074f5800 nid=0x1994 waiting for monitor entry [0x0000000008bbf000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.nbp.theplatform.threaddump.ThreadBlockedState.monitorLock(ThreadBlockedState.java:42)
- waiting to lock <0x0000000780a000b0> (a com.nbp.theplatform.threaddump.ThreadBlockedState)
at com.nbp.theplatform.threaddump.ThreadBlockedState$3.run(ThreadBlockedState.java:34)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
Locked ownable synchronizers:
- <0x0000000780b0e1b8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
java.lang.Thread.State: RUNNABLE
at java.io.FileOutputStream.writeBytes(Native Method)
at java.io.FileOutputStream.write(FileOutputStream.java:282)
at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:65)
at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:123)
- locked <0x0000000780a31778> (a java.io.BufferedOutputStream)
at java.io.PrintStream.write(PrintStream.java:432)
- locked <0x0000000780a04118> (a java.io.PrintStream)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:202)
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:272)
at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:85)
- locked <0x0000000780a040c0> (a java.io.OutputStreamWriter)
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:168)
at java.io.PrintStream.newLine(PrintStream.java:496)
- locked <0x0000000780a04118> (a java.io.PrintStream)
at java.io.PrintStream.println(PrintStream.java:687)
- locked <0x0000000780a04118> (a java.io.PrintStream)
at com.nbp.theplatform.threaddump.ThreadBlockedState.monitorLock(ThreadBlockedState.java:44)
- locked <0x0000000780a000b0> (a com.nbp.theplatform.threaddump.ThreadBlockedState)
at com.nbp.theplatform.threaddump.ThreadBlockedState$1.run(ThreadBlockedState.java:7)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
Locked ownable synchronizers:
- <0x0000000780a31758> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
"BLOCKED_TEST pool-1-thread-2" prio=6 tid=0x0000000007673800 nid=0x260c waiting for monitor entry [0x0000000008abf000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.nbp.theplatform.threaddump.ThreadBlockedState.monitorLock(ThreadBlockedState.java:43)
- waiting to lock <0x0000000780a000b0> (a com.nbp.theplatform.threaddump.ThreadBlockedState)
at com.nbp.theplatform.threaddump.ThreadBlockedState$2.run(ThreadBlockedState.java:26)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
Locked ownable synchronizers:
- <0x0000000780b0c6a0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
"BLOCKED_TEST pool-1-thread-3" prio=6 tid=0x00000000074f5800 nid=0x1994 waiting for monitor entry [0x0000000008bbf000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.nbp.theplatform.threaddump.ThreadBlockedState.monitorLock(ThreadBlockedState.java:42)
- waiting to lock <0x0000000780a000b0> (a com.nbp.theplatform.threaddump.ThreadBlockedState)
at com.nbp.theplatform.threaddump.ThreadBlockedState$3.run(ThreadBlockedState.java:34)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
Locked ownable synchronizers:
- <0x0000000780b0e1b8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
在上面的例子中,BLOCKED_TEST pool-1-thread-1 线程占用了 <0x0000000780a000b0> 锁,然而 BLOCKED_TEST pool-1-thread-2 和 BLOCKED_TEST pool-1-thread-3 threads 正在等待获取锁。
死锁状态样例
"DEADLOCK_TEST-1" daemon prio=6 tid=0x000000000690f800 nid=0x1820 waiting for monitor entry [0x000000000805f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.goMonitorDeadlock(ThreadDeadLockState.java:197)
- waiting to lock <0x00000007d58f5e60> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.monitorOurLock(ThreadDeadLockState.java:182)
- locked <0x00000007d58f5e48> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.run(ThreadDeadLockState.java:135)
Locked ownable synchronizers:
- None
"DEADLOCK_TEST-2" daemon prio=6 tid=0x0000000006858800 nid=0x17b8 waiting for monitor entry [0x000000000815f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.goMonitorDeadlock(ThreadDeadLockState.java:197)
- waiting to lock <0x00000007d58f5e78> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.monitorOurLock(ThreadDeadLockState.java:182)
- locked <0x00000007d58f5e60> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.run(ThreadDeadLockState.java:135)
Locked ownable synchronizers:
- None
"DEADLOCK_TEST-3" daemon prio=6 tid=0x0000000006859000 nid=0x25dc waiting for monitor entry [0x000000000825f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.goMonitorDeadlock(ThreadDeadLockState.java:197)
- waiting to lock <0x00000007d58f5e48> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.monitorOurLock(ThreadDeadLockState.java:182)
- locked <0x00000007d58f5e78> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.run(ThreadDeadLockState.java:135)
Locked ownable synchronizers:
- None
java.lang.Thread.State: BLOCKED (on object monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.goMonitorDeadlock(ThreadDeadLockState.java:197)
- waiting to lock <0x00000007d58f5e60> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.monitorOurLock(ThreadDeadLockState.java:182)
- locked <0x00000007d58f5e48> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.run(ThreadDeadLockState.java:135)
Locked ownable synchronizers:
- None
"DEADLOCK_TEST-2" daemon prio=6 tid=0x0000000006858800 nid=0x17b8 waiting for monitor entry [0x000000000815f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.goMonitorDeadlock(ThreadDeadLockState.java:197)
- waiting to lock <0x00000007d58f5e78> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.monitorOurLock(ThreadDeadLockState.java:182)
- locked <0x00000007d58f5e60> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.run(ThreadDeadLockState.java:135)
Locked ownable synchronizers:
- None
"DEADLOCK_TEST-3" daemon prio=6 tid=0x0000000006859000 nid=0x25dc waiting for monitor entry [0x000000000825f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.goMonitorDeadlock(ThreadDeadLockState.java:197)
- waiting to lock <0x00000007d58f5e48> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.monitorOurLock(ThreadDeadLockState.java:182)
- locked <0x00000007d58f5e78> (a com.nbp.theplatform.threaddump.ThreadDeadLockState$Monitor)
at com.nbp.theplatform.threaddump.ThreadDeadLockState$DeadlockThread.run(ThreadDeadLockState.java:135)
Locked ownable synchronizers:
- None
上面的例子中,当线程 A 需要获取线程 B 的锁来继续它的任务,然而线程 B 也需要获取线程 A 的锁来继续它的任务的时候发生的。在 thread dump 中,你能看到 DEADLOCK_TEST-1 线程持有 0x00000007d58f5e48 锁,并且尝试获取 0x00000007d58f5e60 锁。你也能看到 DEADLOCK_TEST-2 线程持有 0x00000007d58f5e60,并且尝试获取 0x00000007d58f5e78,同时 DEADLOCK_TEST-3 线程持有 0x00000007d58f5e78,并且在尝试获取 0x00000007d58f5e48 锁,如你所见,每个线程都在等待获取另外一个线程的锁,这状态将不会被改变直到一个线程丢弃了它的锁。
无限等待的Runnable状态样例
子主题
"socketReadThread" prio=6 tid=0x0000000006a0d800 nid=0x1b40 runnable [0x00000000089ef000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:264)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:306)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158)
- locked <0x00000007d78a2230> (a java.io.InputStreamReader)
at sun.nio.cs.StreamDecoder.read0(StreamDecoder.java:107)
- locked <0x00000007d78a2230> (a java.io.InputStreamReader)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:93)
at java.io.InputStreamReader.read(InputStreamReader.java:151)
at com.nbp.theplatform.threaddump.ThreadSocketReadState$1.run(ThreadSocketReadState.java:27)
at java.lang.Thread.run(Thread.java:662)
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:264)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:306)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158)
- locked <0x00000007d78a2230> (a java.io.InputStreamReader)
at sun.nio.cs.StreamDecoder.read0(StreamDecoder.java:107)
- locked <0x00000007d78a2230> (a java.io.InputStreamReader)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:93)
at java.io.InputStreamReader.read(InputStreamReader.java:151)
at com.nbp.theplatform.threaddump.ThreadSocketReadState$1.run(ThreadSocketReadState.java:27)
at java.lang.Thread.run(Thread.java:662)
上例中线程的状态是RUNNABLE,但在下面的堆栈日志中发现socketReadThread 线程正在无限等待读取 socket,因此不能单纯通过线程的状态来确定线程是否处于阻塞状态,应该根据详细的堆栈信息进行分析。
wait.notify.notifyAll注意事项
1.为什么 wait 方法必须在 synchronized 保护的同步代码中使用?
官方文档
java.lang.Object,java Doc 中 wait() 相关的描述
java.lang.Object,java Doc 中 wait() 相关的描述
* this method should always be used in a loop:
* <pre>
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait();
* ... // Perform action appropriate to condition
* }
* </pre>
* This method should only be called by a thread that is the owner
* of this object's monitor.
* <pre>
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait();
* ... // Perform action appropriate to condition
* }
* </pre>
* This method should only be called by a thread that is the owner
* of this object's monitor.
英文部分的意思是说,在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁,也就是通常所说的 synchronized 锁。那么设计成这样有什么好处呢?
示例分析
public class WaitNotifyDemo {
static Queue<String> buffer = new LinkedList<>();
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
new Thread(new Consumer(lock), "consumer-thread").start();
for (int i = 0; i < 10; i++) {
new Thread(new Producer("元素" + i, lock), "producer-thread").start();
}
}
static class Producer implements Runnable {
private String number;
private Object lock;
public Producer(String number, Object lock) {
this.number = number;
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
buffer.add(number);
lock.notify();
System.out.println(Thread.currentThread().getName() + "-> 添加" + number);
}
}
}
static class Consumer implements Runnable {
private Object lock;
public Consumer(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
while (buffer.isEmpty()) {
try {
System.out.println(Thread.currentThread().getName() + "-> 等待...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "-> 消费元素:" + buffer.poll());
}
}
}
}
static Queue<String> buffer = new LinkedList<>();
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
new Thread(new Consumer(lock), "consumer-thread").start();
for (int i = 0; i < 10; i++) {
new Thread(new Producer("元素" + i, lock), "producer-thread").start();
}
}
static class Producer implements Runnable {
private String number;
private Object lock;
public Producer(String number, Object lock) {
this.number = number;
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
buffer.add(number);
lock.notify();
System.out.println(Thread.currentThread().getName() + "-> 添加" + number);
}
}
}
static class Consumer implements Runnable {
private Object lock;
public Consumer(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
while (buffer.isEmpty()) {
try {
System.out.println(Thread.currentThread().getName() + "-> 等待...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "-> 消费元素:" + buffer.poll());
}
}
}
}
有问题的代码出现在 51行 while (buffer.isEmpty()) 和 54行 lock.wait(); 这个过程中间
消费者线程先执行,buffer.isEmpty() 条件成立,程序继续执行,在执行 lock.wait(); 方法之前,如果线程被切换走。
比如此时生产者线程开始执行,执行 buffer.add(number); 和 lock.notify(); 方法,但 notify 并没有任何效果,因为消费者线程的 wait 方法没来得及执行,所以没有线程在等待被唤醒。
此时再切换为消费者线程执行,继续执行 lock.wait(); 方法并进入了等待。
那么问题就出现了,我们希望的是顺序是 线程 wait()->线程 notify(),但是实际上正好相反,如果没有新的生产者线程进行生产,消费者线程将一直陷入等待中。
所以增加 synchronized 锁为了保证生产者/消费者,整个过程的原子性。
消费者线程先执行,buffer.isEmpty() 条件成立,程序继续执行,在执行 lock.wait(); 方法之前,如果线程被切换走。
比如此时生产者线程开始执行,执行 buffer.add(number); 和 lock.notify(); 方法,但 notify 并没有任何效果,因为消费者线程的 wait 方法没来得及执行,所以没有线程在等待被唤醒。
此时再切换为消费者线程执行,继续执行 lock.wait(); 方法并进入了等待。
那么问题就出现了,我们希望的是顺序是 线程 wait()->线程 notify(),但是实际上正好相反,如果没有新的生产者线程进行生产,消费者线程将一直陷入等待中。
所以增加 synchronized 锁为了保证生产者/消费者,整个过程的原子性。
2.为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?
Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。
如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。
3.wait 和 sleep 方法的异同?
相同点
1.它们都可以让线程阻塞。
2.它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。
2.它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。
不同点
1 wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
2 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
3 sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
4 wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。
2 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
3 sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
4 wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。
生产消费者模式实现方式
参考上面的案例
守护线程
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)
用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
synchronized
synchronized关键字最主要的三种使用方式:
1.修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
2.修饰静态方法: :也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
3.修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。
synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
ThreadLocal
使用ThreadLocal<UserInfo> userInfo = new ThreadLocal<UserInfo>()的方式,让每个线程内部都会维护一个ThreadLocalMap,里边包含若干了 Entry(K-V 键值对),每次存取都会先获取到当前线程ID,然后得到该线程对象中的Map,然后与Map交互。
原子类
概念
Java中提供的原子操作类 AtomicInteger,AtomicBoolean,AtomicLong,AtomicReference等。这是由硬件提供原子操作指令实现的。在非激烈竞争的情况下,开销更小,速度更快。为了确保线程的全权,“检查再运行”操作(如惰性初始化)和读改写操作(如自增)必须是原子操作。我们将“检查再运行” 和读改写操作的全部执行过程看作是符合操作。为了保证线程安全,操作必须原子
原子更新基本类型
使用原子的方式更新基本类型,Atomic包提供了以下3个类。
(1)AtomicBoolean: 原子更新布尔类型。
(2)AtomicInteger: 原子更新整型。
(3)AtomicLong: 原子更新长整型。
(2)AtomicInteger: 原子更新整型。
(3)AtomicLong: 原子更新长整型。
以上3个类提供的方法几乎一模一样,以AtomicInteger为例进行详解,AtomicIngeter的常用方法如下:
(1)int addAndGet(int delta): 以原子的方式将输入的数值与实例中的值相加,并返回结果。
(2)boolean compareAndSet(int expect, int update): 如果输入的值等于预期值,则以原子方式将该值设置为输入的值。
(3)int getAndIncrement(): 以原子的方式将当前值加1,注意,这里返回的是自增前的值。
(4)void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
(5)int getAndSet(int newValue): 以原子的方式设置为newValue,并返回旧值。
Atomic包里的类基本都是使用Unsafe下的CAS方法实现,方法如下:
(2)boolean compareAndSet(int expect, int update): 如果输入的值等于预期值,则以原子方式将该值设置为输入的值。
(3)int getAndIncrement(): 以原子的方式将当前值加1,注意,这里返回的是自增前的值。
(4)void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
(5)int getAndSet(int newValue): 以原子的方式设置为newValue,并返回旧值。
Atomic包里的类基本都是使用Unsafe下的CAS方法实现,方法如下:
Unsafe只提供了三种CAS方法: compareAndSwapObject、compareAndSwapInt和compateAndSwapLong,其他类型都是转成这三种类型再使用对应的方法去原子更新的。
原子更新数组
通过原子的方式更新数组里的某个元素,Atomic包提供了以下的4个类:
(1)AtomicIntegerArray: 原子更新整型数组里的元素。
(2)AtomicLongArray: 原子更新长整型数组里的元素。
(3)AtomicReferenceArray: 原子更新引用类型数组里的元素。
(1)AtomicIntegerArray: 原子更新整型数组里的元素。
(2)AtomicLongArray: 原子更新长整型数组里的元素。
(3)AtomicReferenceArray: 原子更新引用类型数组里的元素。
这三个类的最常用的方法是如下两个方法:
(1)get(int index):获取索引为index的元素值。
(2)compareAndSet(int i,E expect,E update): 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值。
以AtomicReferenceArray举例如下:
(2)compareAndSet(int i,E expect,E update): 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值。
以AtomicReferenceArray举例如下:
输出结果为:index100 \n index1。
需要注意的是,数组value通过构造方法传递进去,然后AtoReferenceArray会将当前数组复制一份,所以当AtoReferenceArray对内部的数组元素修改的时候,不会影响原先传入的referenceArray数组。
需要注意的是,数组value通过构造方法传递进去,然后AtoReferenceArray会将当前数组复制一份,所以当AtoReferenceArray对内部的数组元素修改的时候,不会影响原先传入的referenceArray数组。
原子更新引用类型
Atomic包提供了以下三个类:
(1)AtomicReference: 原子更新引用类型。
(2)AtomicReferenceFieldUpdater: 原子更新引用类型的字段。
(3)AtomicMarkableReferce: 原子更新带有标记位的引用类型。
(2)AtomicReferenceFieldUpdater: 原子更新引用类型的字段。
(3)AtomicMarkableReferce: 原子更新带有标记位的引用类型。
这三个类提供的方法都差不多,首先构造一个引用对象,然后把引用对象set进Atomic类,然后调用compareAndSet等一些方法去进行原子操作,原理都是基于Unsafe实现,但AtomicReferenceFieldUpdater略有不同,更新的字段必须用volatile修饰,下面提供一段示例代码:
原子更新字段类
Atomic包提供了四个类进行原子字段更新:
(1)AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
(2)AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
(3)AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
(4)AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。
这四个类的使用方式都差不多,示例代码如上一小节的AtomicReferenceFieldUpdater一样,要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段必须使用public volatile修饰。
(2)AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
(3)AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
(4)AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。
这四个类的使用方式都差不多,示例代码如上一小节的AtomicReferenceFieldUpdater一样,要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段必须使用public volatile修饰。
线程安全
什么是线程安全
线程安全问题
场景案例解析
多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能
表现正确的行为。
线程安全有以下几种实现方式:
1.不可变
不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全
保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个
线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满
足线程安全。
2.互斥同步
synchronized 和 ReentrantLock。
1).非阻塞同步
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同
步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,
那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁
(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加
锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤
醒等操作。
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进
行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿
措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不
需要将线程阻塞,因此这种同步操作称为非阻塞同步。
2).CAS
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥
同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比
较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分
别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等
于 A,才将 V 的值更新为 B。
3).ABA
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为
A,那 CAS 操作就会误认为它从来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解
决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情
况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用
传统的互斥同步可能会比原子类更高效。
3.无同步方案
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数
据,那它自然就无须任何同步措施去保证正确性。
1).栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部
变量存储在虚拟机栈中,属于线程私有的。
2).线程本地存储(Thread Local Storage)
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据
的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数
据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不
出现数据争用的问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产
者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最
重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器
线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很
多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
它所对应的底层结构图:
每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。
当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的
ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该
Map 中。
ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存
在多线程竞争。
在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的
底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用
ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄
漏甚至是造成自身业务混乱的风险。
3).可重入代码(Reentrant Code)
这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断
它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回
后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统
资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
表现正确的行为。
线程安全有以下几种实现方式:
1.不可变
不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全
保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个
线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满
足线程安全。
2.互斥同步
synchronized 和 ReentrantLock。
1).非阻塞同步
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同
步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,
那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁
(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加
锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤
醒等操作。
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进
行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿
措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不
需要将线程阻塞,因此这种同步操作称为非阻塞同步。
2).CAS
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥
同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比
较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分
别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等
于 A,才将 V 的值更新为 B。
3).ABA
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为
A,那 CAS 操作就会误认为它从来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解
决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情
况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用
传统的互斥同步可能会比原子类更高效。
3.无同步方案
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数
据,那它自然就无须任何同步措施去保证正确性。
1).栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部
变量存储在虚拟机栈中,属于线程私有的。
2).线程本地存储(Thread Local Storage)
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据
的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数
据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不
出现数据争用的问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产
者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最
重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器
线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很
多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
它所对应的底层结构图:
每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。
当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的
ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该
Map 中。
ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存
在多线程竞争。
在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的
底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用
ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄
漏甚至是造成自身业务混乱的风险。
3).可重入代码(Reentrant Code)
这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断
它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回
后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统
资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
执行结果错误
多线程进行i++操作
不恰当的初始化
对象初始化过程中使用多线程
活跃性问题
死锁
活锁
饥饿
哪些场景需要注意线程安全
访问共享变量或资源
依赖时序操作
不同数据之间存在绑定关系
对方没有声明自己是线程安全的
多线程性能问题
上下文切换
线程协作
缓存失效
内存模型 JMM
定义
定义:JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性
实现:volatile、synchronized、final、concurrent包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字
主内存:所有变量都保存在主内存中
工作内存:每个线程的独立内存,保存了该线程使用到的变量的主内存副本拷贝,线程对变量的操作必须在工作内存中进行
每个线程都有自己的本地内存共享副本,如果A线程要更新主内存还要让B线程获取更新后的变量,那么需要:
将本地内存A中更新共享变量
将更新的共享变量刷新到主内存中
线程B从主内存更新最新的共享变量
实现:volatile、synchronized、final、concurrent包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字
主内存:所有变量都保存在主内存中
工作内存:每个线程的独立内存,保存了该线程使用到的变量的主内存副本拷贝,线程对变量的操作必须在工作内存中进行
每个线程都有自己的本地内存共享副本,如果A线程要更新主内存还要让B线程获取更新后的变量,那么需要:
将本地内存A中更新共享变量
将更新的共享变量刷新到主内存中
线程B从主内存更新最新的共享变量
Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。
read:把一个变量的值从主内存传输到工作内存中
load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
use:把工作内存中一个变量的值传递给执行引擎
assign:把一个从执行引擎接收到的值赋给工作内存的变量
store:把工作内存的一个变量的值传送到主内存中
write:在 store 之后执行,把 store 得到的值放入主内存的变量中
lock:作用于主内存的变量
unlock
load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
use:把工作内存中一个变量的值传递给执行引擎
assign:把一个从执行引擎接收到的值赋给工作内存的变量
store:把工作内存的一个变量的值传送到主内存中
write:在 store 之后执行,把 store 得到的值放入主内存的变量中
lock:作用于主内存的变量
unlock
三大特性:
原子性
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock
操作具有原子性。
例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。
但是 ,Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,
double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和
write 操作可以不具备原子性
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock
操作具有原子性。
例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。
但是 ,Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,
double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和
write 操作可以不具备原子性
可见性
指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内
存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变
量值来实现可见性的。
主要有三种实现可见性的方式:
volatile 不能保证操作的原子性。
synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回
主内存。
final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有
发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),
那么其它线程就能看见 final 字段的值。
指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内
存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变
量值来实现可见性的。
主要有三种实现可见性的方式:
volatile 不能保证操作的原子性。
synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回
主内存。
final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有
发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),
那么其它线程就能看见 final 字段的值。
有序性
有序性是指:
在本线程内观察,所有操作都是有序的。
在一个线程观察另一个线程,所有操作都是无序的,
无序是因为发生了指令重排序。
在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会
影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后
面的指令放到内存屏障之前。
也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同
步代码,相当于是让线程顺序执行同步代码。
有序性是指:
在本线程内观察,所有操作都是有序的。
在一个线程观察另一个线程,所有操作都是无序的,
无序是因为发生了指令重排序。
在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会
影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后
面的指令放到内存屏障之前。
也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同
步代码,相当于是让线程顺序执行同步代码。
先行发生原则(happens-before原则)
可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行
发生原则,让一个操作无需控制就能先于另一个操作完成。
发生原则,让一个操作无需控制就能先于另一个操作完成。
1.单一线程原则 Single Thread rule
在一个线程内,在程序前面的操作先行发生于后面的操作。
2.管程锁定规则 Monitor Lock Rule
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
3.volatile 变量规则 Volatile Variable Rule
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
4.线程启动规则 Thread Start Rule
Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
5.线程加入规则 Thread Join Rule
Thread 对象的结束先行发生于 join() 方法返回。
6.线程中断规则 Thread Interruption Rule
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件
的发生,可以通过 interrupted() 方法检测到是否有中断发生。
7.对象终结规则 Finalizer Rule
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法
的开始。
8.传递性 Transitivity
如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行
发生于操作 C。
在一个线程内,在程序前面的操作先行发生于后面的操作。
2.管程锁定规则 Monitor Lock Rule
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
3.volatile 变量规则 Volatile Variable Rule
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
4.线程启动规则 Thread Start Rule
Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
5.线程加入规则 Thread Join Rule
Thread 对象的结束先行发生于 join() 方法返回。
6.线程中断规则 Thread Interruption Rule
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件
的发生,可以通过 interrupted() 方法检测到是否有中断发生。
7.对象终结规则 Finalizer Rule
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法
的开始。
8.传递性 Transitivity
如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行
发生于操作 C。
对于程序次序规则来说,就是一段程序代码的执行在单个线程中看起来是有序的。
注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,
这个应该是程序看起来执行的顺序是按照代码顺序执行的,但是虚拟机可能会对程序代码进行指令重排序。
虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。
因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。
事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果处于被锁定的状态,
那么必须先对锁进行了释放操作,后面才能继续进行lock操作。第三条规则是一条比较重要的规则。
直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写
入操作肯定会先行发生于读操作。第四条规则实际上就是体现happens-before原则具备传递性。
注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,
这个应该是程序看起来执行的顺序是按照代码顺序执行的,但是虚拟机可能会对程序代码进行指令重排序。
虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。
因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。
事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果处于被锁定的状态,
那么必须先对锁进行了释放操作,后面才能继续进行lock操作。第三条规则是一条比较重要的规则。
直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写
入操作肯定会先行发生于读操作。第四条规则实际上就是体现happens-before原则具备传递性。
线程模型
线程
java线程
操作系统线程
线程池
起源
new Thread弊端:
1.每次启动线程都需要new Thread新建对象与线程,性能差。线程池能重用存在的线程,减少对象创建、回收的开销。
2.线程缺乏统一管理,可以无限制的新建线程,导致OOM。线程池可以控制可以创建、执行的最大并发线程数。
3.缺少工程实践的一些高级的功能如定期执行、线程中断。线程池提供定期执行、并发数控制功能
1.每次启动线程都需要new Thread新建对象与线程,性能差。线程池能重用存在的线程,减少对象创建、回收的开销。
2.线程缺乏统一管理,可以无限制的新建线程,导致OOM。线程池可以控制可以创建、执行的最大并发线程数。
3.缺少工程实践的一些高级的功能如定期执行、线程中断。线程池提供定期执行、并发数控制功能
线程池核心参数
corePoolSize:核心线程数量,线程池中应该常驻的线程数量
maximumPoolSize:线程池允许的最大线程数,非核心线程在超时之后会被清除
workQueue:阻塞队列,存储等待执行的任务
keepAliveTime:线程没有任务执行时可以保持的时间
unit:时间单位
threadFactory:线程工厂,来创建线程
rejectHandler:当拒绝任务提交时的策略(抛异常、用调用者所在的线程执行任务、丢弃队列中第一个任务执行当前任务、直接丢弃任务)
maximumPoolSize:线程池允许的最大线程数,非核心线程在超时之后会被清除
workQueue:阻塞队列,存储等待执行的任务
keepAliveTime:线程没有任务执行时可以保持的时间
unit:时间单位
threadFactory:线程工厂,来创建线程
rejectHandler:当拒绝任务提交时的策略(抛异常、用调用者所在的线程执行任务、丢弃队列中第一个任务执行当前任务、直接丢弃任务)
创建线程的逻辑
以下任务提交逻辑来自ThreadPoolExecutor.execute方法:
1.如果运行的线程数 < corePoolSize,直接创建新线程,即使有其他线程是空闲的
2.如果运行的线程数 >= corePoolSize
2.1 如果插入队列成功,则完成本次任务提交,但不创建新线程
2.2 如果插入队列失败,说明队列满了
2.2.1 如果当前线程数 < maximumPoolSize,创建新的线程放到线程池中
2.2.2 如果当前线程数 >= maximumPoolSize,会执行指定的拒绝策略
1.如果运行的线程数 < corePoolSize,直接创建新线程,即使有其他线程是空闲的
2.如果运行的线程数 >= corePoolSize
2.1 如果插入队列成功,则完成本次任务提交,但不创建新线程
2.2 如果插入队列失败,说明队列满了
2.2.1 如果当前线程数 < maximumPoolSize,创建新的线程放到线程池中
2.2.2 如果当前线程数 >= maximumPoolSize,会执行指定的拒绝策略
阻塞队列的策略
1.直接提交。SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take。将任务直接提交给线程而不保持它们。
2.无界队列。当使用无限的 maximumPoolSizes 时,将导致在所有corePoolSize线程都忙时新任务在队列中等待。
3.有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。
2.无界队列。当使用无限的 maximumPoolSizes 时,将导致在所有corePoolSize线程都忙时新任务在队列中等待。
3.有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。
如何创建
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
Executors 返回线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
通过构造方法实现 ThreadPoolExecutor构造方法
private static ExecutorService executor = new ThreadPoolExecutor(13, 13,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(13));
这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。
private static ExecutorService executor = new ThreadPoolExecutor(13, 13,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(13));
这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。
通过Executor 框架的工具类Executors来实现 我们可以创建三种类型的ThreadPoolExecutor:
1.FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
2.SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
3.CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
1.FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
2.SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
3.CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
使用开源类库,如apache和guava等,如guava提供的ThreadFactoryBuilder来创建线程池
使用场景
锁
synchronized
synchronized在JVM级别实现,会在生成的字节码中加上monitorenter和monitorexit,任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。monitor是JVM的一个同步工具,synchronized还通过内存指令屏障来保证共享变量的可见性
lock
拥有synchronize相同的语义,但是添加一些其他特性,如中断锁等候和定时锁等候,所以可以使用lock代替synchronize,但必须手动加锁释放锁
ReentrantLock
ReentrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁,所谓的公平锁就是先等待的线程先获得锁
ReentrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程
ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制
ReentrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程
ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制
锁优化
自旋锁
互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享
数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请
求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能
获得锁,就可以避免进入阻塞状态。
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作
占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定
了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请
求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能
获得锁,就可以避免进入阻塞状态。
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作
占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定
了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁
ReentrantLock通过构造函数指定该锁是否是公平锁,默认是非公平锁。Synchronized是一种非公平锁
ReentrantLock通过构造函数指定该锁是否是公平锁,默认是非公平锁。Synchronized是一种非公平锁
可重入锁
指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
ReentrantLock, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁
Synchronized,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁
ReentrantLock, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁
Synchronized,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁
独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有
ReentrantLock是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的
Synchronized是独享锁
ReentrantLock是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的
Synchronized是独享锁
乐观锁/悲观锁
悲观锁在Java中的使用,就是各种锁
乐观锁在Java中的使用,是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新
乐观锁在Java中的使用,是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新
锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被
其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的
锁进行消除。
对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符
串拼接代码就隐式加了锁。
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5
之前,会转化为 StringBuffer 对象的连续 append() 操作:
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的
动态
作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃
逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被
其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的
锁进行消除。
对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符
串拼接代码就隐式加了锁。
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5
之前,会转化为 StringBuffer 对象的连续 append() 操作:
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的
动态
作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃
逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。
锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导
致性能损耗。
上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到
由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)
到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操
作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
致性能损耗。
上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到
由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)
到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操
作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
轻量级锁
JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:
无锁状态(unlocked)、偏向锁状态(biasble)、
轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用
互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此
也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS
失败了再改用互斥量进行同步。
当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定
(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然
后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操
作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记
变为 00,表示该对象处于轻量级锁状态。
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程
的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进
入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两
条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
无锁状态(unlocked)、偏向锁状态(biasble)、
轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用
互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此
也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS
失败了再改用互斥量进行同步。
当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定
(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然
后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操
作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记
变为 00,表示该对象处于轻量级锁状态。
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程
的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进
入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两
条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
偏向锁
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就
不再需要进行同步操作,甚至连 CAS 操作也不再需要。
当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用
CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后
每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏
向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
不再需要进行同步操作,甚至连 CAS 操作也不再需要。
当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用
CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后
每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏
向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
JUC工具包
Atomic原子类
基本类型
AtomicInteger的使用
常用方法
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
AtomicInteger 类的原理
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
数组类型
引用类型
对象的属性修改类型
AQS
概念
原理
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
AQS对资源的共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock
使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock
组件
CountDownLatch
计数器闭锁是一个能阻塞主线程,让其他线程满足特定条件下主线程再继续执行的线程同步工具。
图中,A为主线程,A首先设置计数器的数到AQS的state中,当调用await方法之后,A线程阻塞,随后每次其他线程调用countDown的时候,将state减1,直到计数器为0的时候,A线程继续执行。
使用场景:
并行计算:把任务分配给不同线程之后需要等待所有线程计算完成之后主线程才能汇总得到最终结果
模拟并发:可以作为并发次数的统计变量,当任意多个线程执行完成并发任务之后统计一次即可
图中,A为主线程,A首先设置计数器的数到AQS的state中,当调用await方法之后,A线程阻塞,随后每次其他线程调用countDown的时候,将state减1,直到计数器为0的时候,A线程继续执行。
使用场景:
并行计算:把任务分配给不同线程之后需要等待所有线程计算完成之后主线程才能汇总得到最终结果
模拟并发:可以作为并发次数的统计变量,当任意多个线程执行完成并发任务之后统计一次即可
Semaphore
信号量是一个能阻塞线程且能控制统一时间请求的并发量的工具。比如能保证同时执行的线程最多200个,模拟出稳定的并发量。
public class CountDownLatchTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
Semaphore semaphore = new Semaphore(3); //配置只能发布3个运行许可证
for (int i = 0; i < 100; i++) {
int finalI = i;
executorService.execute(() -> {
try {
semaphore.acquire(3); //获取3个运行许可,如果获取不到会一直等待,使用tryAcquire则不会等待
Thread.sleep(1000);
System.out.println(finalI);
semaphore.release(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
由于同时获取3个许可,所以即使开启了100个线程,但是每秒只能执行一个任务
使用场景:
数据库连接并发数,如果超过并发数,等待(acqiure)或者抛出异常(tryAcquire)
public class CountDownLatchTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
Semaphore semaphore = new Semaphore(3); //配置只能发布3个运行许可证
for (int i = 0; i < 100; i++) {
int finalI = i;
executorService.execute(() -> {
try {
semaphore.acquire(3); //获取3个运行许可,如果获取不到会一直等待,使用tryAcquire则不会等待
Thread.sleep(1000);
System.out.println(finalI);
semaphore.release(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
由于同时获取3个许可,所以即使开启了100个线程,但是每秒只能执行一个任务
使用场景:
数据库连接并发数,如果超过并发数,等待(acqiure)或者抛出异常(tryAcquire)
CyclicBarrier
可以让一组线程相互等待,当每个线程都准备好之后,所有线程才继续执行的工具类
与CountDownLatch类似,都是通过计数器实现的,当某个线程调用await之后,计数器减1,当计数器大于0时将等待的线程包装成AQS的Node放入等待队列中,当计数器为0时将等待队列中的Node拿出来执行。
与CountDownLatch的区别:
CountDownLatch是一个线程等其他线程,CyclicBarrier是多个线程相互等待
CyclicBarrier的计数器能重复使用,调用多次
使用场景: 有四个游戏玩家玩游戏,游戏有三个关卡,每个关卡必须要所有玩家都到达后才能允许通过。其实这个场景里的玩家中如果有玩家A先到了关卡1,他必须等到其他所有玩家都到达关卡1时才能通过,也就是说线程之间需要相互等待。
与CountDownLatch类似,都是通过计数器实现的,当某个线程调用await之后,计数器减1,当计数器大于0时将等待的线程包装成AQS的Node放入等待队列中,当计数器为0时将等待队列中的Node拿出来执行。
与CountDownLatch的区别:
CountDownLatch是一个线程等其他线程,CyclicBarrier是多个线程相互等待
CyclicBarrier的计数器能重复使用,调用多次
使用场景: 有四个游戏玩家玩游戏,游戏有三个关卡,每个关卡必须要所有玩家都到达后才能允许通过。其实这个场景里的玩家中如果有玩家A先到了关卡1,他必须等到其他所有玩家都到达关卡1时才能通过,也就是说线程之间需要相互等待。
对象头信息
高性能高可用
缓存
分布式锁
限流
缓存同步
常用缓存
redis
数据结构
String
hash
list
set
zset
其他
事件模型
单线程
采用IO多路复用机制
高效原因
1 纯内存操作
2 基于非阻塞的IO多路复用机制
3 C语言实现
4 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题
过期策略
定期删除
惰性删除
内存淘汰机制
noeviction
新写入操作会报错
allkeys-lru
在键空间中,移除最近最少使用的key。最常用
allkeys-random
在键空间中,随机移除某个key
volatile-lru
在设置了过期时间的键空间中,随机移除最近最少使用的key
volatile-random
在设置了过期时间的键空间中,随机移除某个key
volatile-ttl
在设置了过期时间的键空间中,有更早过期时间的key优先移除
持久化
RDB
RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,非常适合做冷备,可以将数据文件发送到一些远程的安全存储上去
RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个字进程,让子进程执行磁盘IO操作来进行RDB持久化即可
相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速
一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据
RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒
AOF
AOF可以更好的保持数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据
AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复
AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写,因为在rewrite log的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来,在创建 新日志文件的时候,老的日志文件还是照常写入。当新的 merge后的日志文件ready的时候,再交换新老日志文件即可
AOF日志文件的命苦通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复
对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件
集群高可用
主从架构
异步方式复制数据导slave节点
一个master node 是可以配置多个slave node的
salve node也可以连接其他的slave node
slave node做复制的时候,不会block master node的工作
slave node 在做复制的时候,也不会block对自己的查询操作,他会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了
slave node 主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量
如果采用了主从架构,那么建议必须开启master node的持久化
复制原理
流程
断点续传
过期key处理
子主题
功能
集群监控:负责监控redis master和slave进程是否正常工作
消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
故障转移:如果master node挂掉了,会自动转移到slave node上
配置中心:如果故障转移发生了,通知client客户端新的master地址
重点
至少3个实例
需要majority来允许执行故障转移
不保证数据零丢失的,只能保证redis集群的高可用性
数据丢失
异步复制导致的数据丢失
脑裂导致的数据丢失
min-slave-to-write 1 min-slave-max-log 10 解决方案
原理
sdown和odown转换机制
自动发现机制
slave配置的自动纠正
slave-master选举算法
quorum和majority
最佳实践
memcache
guava
缓存常见问题
缓存穿透
查询一个数据库一定不存在的数据,每次查询都穿透缓存落到DB中
解决方案
每次系统A从数据库中只要没查到,就写一个空值到缓存里去
缓存击穿
指一个key非常热点,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库
解决方案
将热点数据设置为永远不过期
基于redis or zookeeper 实现互拆锁,等待第一个请求构建完缓存之后,再释放锁。进而其它请求才能通过key访问数据
缓存雪崩
指在某一个时间段,缓存集中过期失效,或缓存服务节点的宕机,海量请求同时到达DB
解决方案
redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
本地ehcache 缓存+hystrix限流&降级,避免mysql被打死
redis持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据
消息中间件
常用MQ
MQ用途
缺点
最佳实践
集群高可用
事务消息
ACK级别
监控报警
提问
如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,怎么解决?
如何设计一个消息队列
数据库
读写分离
分库分表
常用中间件
shardingJDBC
mycat
mysql route
altas
zebra
binlog
搜索引擎
分布式
理论
CAP
概念
在一个分布式系统中(指互相连接并共享数据的节点集合)中,当涉及到读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。
一致性(Consistence)
写操作之后进行读操作无论在哪个节点都需要返回写操作的值
可用性(Availability)
非故障的节点在合理的时间内返回合理的响应
分区容忍性(Partition Tolerance)
当网络出现分区后,系统依然能够继续履行职责
BASE
概念
BASE理论是对CAP的延伸和补充,是对CAP中的AP方案的一个补充,即使在选择AP方案的情况下,如何更好的最终达到C。
BASE是基本可用,柔性状态,最终一致性三个短语的缩写,核心的思想是即使无法做到强一致性,但应用可以采用适合的方式达到最终一致性。
BASE是基本可用,柔性状态,最终一致性三个短语的缩写,核心的思想是即使无法做到强一致性,但应用可以采用适合的方式达到最终一致性。
基本可用(Basically Available)
分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性
柔性状态(Soft State)
允许系统存在中间状态,而该中间状态不会影响系统整体可用性
最终一致性(Eventual Consistency)
系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况
Raft
Paxos
Gosip
分片
副本
Quorum/NWR
幂等
一致性哈希
ID生成器
常用中间件
Zookeeper
ETCD
Consul
Eureka
分布式事务
两阶段提交(2PC)
概念
- 2PC(tow phase commit)两阶段提交。顾名思义它分成两个阶段,先由一方进行提议(propose)并收集其他节点的反馈(vote),再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts)
- 在异步(asynchronus)环境无节点宕机(fail-stop)情况下,2PC是分布式一致性的一种解决方案。但是如果加上节点宕机(fail-recover)的考虑,2PC就会存在一些问题。
- coordinator发起提议后宕机,participants将进入阻塞(block)状态,一直等待coordinator的响应以完成该次决议。这时候需要增加一个协调者备份角色(coordinator watchdog)把系统从不可结束状态带出来。coordinator宕机一定时间之后,watchdog会接替coordinator工作,通过查询(query)各participants的状态,决定阶段2是否提交或取消事务。为此,coordinator/participants记录(logging)历史状态,以备coordinator宕机后watchdog查询participants状态或participants宕机恢复后重新找回状态。
- 从coordinator接收到一次事务请求、发起提议到事务完成,经过2PC协议后增加了2次RTT(propose+commit),带来的时延(latency)增加相对较少
- 在异步(asynchronus)环境无节点宕机(fail-stop)情况下,2PC是分布式一致性的一种解决方案。但是如果加上节点宕机(fail-recover)的考虑,2PC就会存在一些问题。
- coordinator发起提议后宕机,participants将进入阻塞(block)状态,一直等待coordinator的响应以完成该次决议。这时候需要增加一个协调者备份角色(coordinator watchdog)把系统从不可结束状态带出来。coordinator宕机一定时间之后,watchdog会接替coordinator工作,通过查询(query)各participants的状态,决定阶段2是否提交或取消事务。为此,coordinator/participants记录(logging)历史状态,以备coordinator宕机后watchdog查询participants状态或participants宕机恢复后重新找回状态。
- 从coordinator接收到一次事务请求、发起提议到事务完成,经过2PC协议后增加了2次RTT(propose+commit),带来的时延(latency)增加相对较少
性能问题
在事务进行过程中,节点都处于阻塞状态。各个操作数据库的节点都在占用着数据库资源,只有等所有节点都准备完毕,事务协调者才会通知全局提交,参与者进行本地事务提交才会释放数据库资源。整个过程比较漫长,对性能影响比较大
事务协调者宕机问题
事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,会导致参与者收不到事务提交或回滚的通知,从而导致事务参与者一直处于无法完成事务的中间状态
丢失消息导致的数据不一致问题
在第二阶段,当发生局部网络问题时,一部分节点收到事务提交通知,一部分节点没有收到提交通知,那么就会导致节点数据的不一致
应用场景
适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低
三阶段提交(3PC)
TCC
Try 阶段
Confirm 阶段
Cancel 阶段
Doquery阶段
本地消息表
可靠消息最终一致性方案
最大努力通知方案
分布式,集群,微服务的区别
数据库
MYSQL高级
mysql框架介绍
mysql简介
高手的 mysql 是怎样炼成的?
数据库内部结构和原理
数据库建模优化
数据库索引的建立
sql语句优化
mysql 服务器的安装配置
数据库的性能监控分析与系统优化
mysql服务器的优化
各种参数常量设定
查询语句优化
主从复制
分布式架构搭建,垂直切割和水平切割
软硬件升级
容灾备份与恢复
shell 或 python 等脚本语言开发
对开源数据库进行二次开发
sql编程(自定义函数,存储过程,触发器,定时任务)
mysql linux的安装(5.7)
检查当前系统是否安装过mysql
安装mysql服务端
安装mysql客户端
查看mysql安装时创建的mysql用户和mysql组
mysql服务的启动和停止
service mysql start 启动服务
service mysql stop 停止服务
mysql服务启动后,开始连接
mysql服务状态
启动 mysql
systemctl start mysqld
重启 mysql
systemctl restart mysqld
停止 mysql
systemctl stop mysqld
查看mysql 状态
systemctl status mysqld
修改配置文件的位置
问题
linux 向表中插入一条中文字符报错误
修改全局配置文件(vim etc/my.cnf)
character-set-server=utf8
表修改类型字符集
alter table xs modify name varchar(30) character set utf8;
修改已有数据库的字符集
alter database 库名 character set 'utf8';
mysql的用户与权限管理(远程访问mysql)
mysql 用户管理
创建用户
reate User zs identified by '123123'
授权
grant all privileges on *.* to root @'%' identified by '123123'
修改用户密码
set password = password('123456')
修改的是硬盘里面的数据,内存中的数据并没有修改,索引需要刷新内存中的数据使用 flush privileges;
如果navite 远程连接失败 注意:防火墙要开放3306端口
查看防火墙状态
systemctl status firewalld
开启防火墙
service firewalld start
关闭防火墙
service firewalld stop
重启防火墙
service firewalld restart
如果开启防火墙则需要开放端口
开放3306
firewall-cmd --zone=public --add-port=3306/tcp --permanent
重启防火墙
firewall-cmd --reload
查看防火墙开放端口
firewall-cmd --list-ports
mysql 的一些杂项配置
sql_mode
创建表
问题
结论: group by 使用原则 select 后面只能放函数 和 group by 后的字段
如何查看sql_mode
show variables like '%sql_mode%';
把公司服务器的sql_mode验证修改为本地的服务器的sql_mode
mysql 配置文件
二进制日志 log-bin
主从复制
错误日志 log-err
默认是关闭的,记录严重的警告和错误信息,每次启动和关闭的详细信息
查询日志 log
默认是关闭的,记录查询的sql语句,如果开启会降低mysql的整体性能,因为记录日志会增加服务器负担
数据文件
windows
mysql安装路径/data
linux
使用命令查看全部库
frm文件
存放表结构
myd文件
存放表数据
myi文件
存放表索引
如何配置
windows配置文件
my.ini
linux配置文件
/etc/my.conf
mysql 逻辑架构介绍
总体概览
执行步骤为以下几步
1), mysql之外类型Java 程序访问 (Connectors)
2), 和连接池进行沟通 ( Connection Pool )
3), 缓存,缓冲查询 ( Caches )
4), SQL接口分析sql ( SQL Interface )
5), 解析器复杂sql 解析 ( Parser )
6), 优化器,不影响结果进行优化,生成执行计划 ( Optimizer )
7), 存储引擎按执行计划分类执行 ( Pluggable Storage Engines )
8), 存入缓存 ( Buffers )
利用 show profile 查看sql的执行计划
修改配置文件 /etc/my.cnf
query_cache_type=1
开启 profiling
查看 profiling 是否开启
show variables like '%profiling%';
开启这样设置 set profiling =1 ;
select * from 表名
show profiles; 查看执行计划
show profile cpu,block io for query ?
mysql 存储引擎
查看命令
innodb 和 myisam
行表锁谁会发生死锁?
行锁
产生死锁的原因
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
解决办法
子主题
什么情况下会用到 MyISAM存储引擎?
Mysql 自带的表都是用 MyISAM存储引擎,不会出现高并发,节省资源
各个存储引擎介绍
InnoDB
InnoDB 是mysql 的默认事务型引擎,它被用来处理大量短期事务,除非有非常特别的原因需要使用其它的存储引擎,否则应该优先考虑 InnoDB 引擎。
MyISAM
MyISAM 提供了大量的特性,包含全文索引,压缩,空间函数等,但 MyIsam 不支持事务和行级锁,有一个缺陷就是崩溃后无法安全恢复。
Archive
Archive 档案存储引擎只支持 insert 和 select 操作,在 mysql 5.1之前不支持索引。 Archive 表适合日志和数据采集类应用。
Blackhole
Blockhole 引擎没有实现任何的存储机制,它会丢弃所有插入的数据,不做任何保存。但服务器会记录 Blackhole 表的日志,所以可以用于复制数据库到备库,或者简单地记录到日志。但这种应用方式会碰到很多问题,因此不推荐使用
CSV
CSV 引擎可以将普通的csv文件作为mysql的表来处理,但不支持索引,CSV引擎可以作为一种数据交换机制,非常有用,CSV存储的数据直接可以在操作系统中,用文本编译器,或者 excel 读取
memory
如果需要快速地访问数据,并且这些数据不会被修改,重启以后丢失也没有关系,那么使用 memory 表是非常有用的 moemory 表至少比 myisam 表要快一个数量级别
federated
federated 引擎是访问其它服务器的一个代理,尽管该引擎看起来提供了很好的跨服务器的灵活性,但也经常带来问题,因此默认禁止
索引优化分析
性能下降 sql 慢了 (执行时间长,等待时间长)
查询语句写的烂
查询语句写的烂
索引失效(前提是你建了索引,没有使用上索引)
单值索引
复合索引
关联查询有太多的join(设计缺陷或不得已的需求)
sql优化
服务器调优及各个参数设置(缓冲,线程数等)
调整 my.cnf
没有充分利用到索引
建立索引
数据过多
分库分表
常见通用的join查询
sql的执行顺序
手写
机读
总结
7种join图
建表sql语句
查询语句
实际工作中都是使用左外连接,很少使用右外连接
索引简介
索引是什么?
排好序的快速查找数据结构就是索引
详解(二叉树)
Mysql索引为什么没有使用二叉树???
二叉树有一个缺点,例如,把5的那条数据删了新增一个92,然后再把23删了,新增一个93,然后再把22删了,新增一个95,然后再把77删了在添加一个98,这时,就变为了链表的数据结构,最好的情况是平衡的一棵树,最坏的情况是一个链表
结论
**数据本身之外,数据库还维护着一个满足特定查找算法的数据结构,这些数据结构以某种方式指向数据,这样就可以在这些数据结构的基础上实现高级查找算法,这种数据结构就是索引(B树 或 Btree)**
一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上
我们平常说的索引,如果没有特别指明,都是指 B树(多路搜索树,并不一定是二叉树的)结构组织的索引。
优势
类似于大学图书馆建立书目录索引,提高数据的检索的效率,降低了数据库的IO成本
通过索引对数据列进行排序,降低数据排序的成本,降低了CPU的消耗
劣势
实际上索引是一张表,该表保存了主键和索引字段,并指向实体表的记录,所以索引列也要占用空间的
实际上索引大大提高了查询速度,但同时也降低了更新表的速度
索引只是提高效率的一个因素,如果你的mysql有大数据量的表,就需要花大量时间研究建立最优秀的索引,或者优化查询
Mysql 索引结构(平衡树)
什么是平衡树
BTree索引
原理图
红色小方块 ->指向数据指针, 数据 ,向下指针
B+Tree索引
原理图
数据,向下指针
B树和B+树的区别
B树的关键字和记录是放在一起的,叶子节点可以看作外部节点,不包含任何信息;B+树的非叶子节点中只有关键字和指向下一个节点的索引,记录只放在叶子节点中。
在B树中,越靠近根节点的记录查找时间越快,只要找到关键字即可确定记录的存在;而B+树中每个记录的查找时间基本是一样的,都需要从根节点走到叶子节点,而且在叶子节点中还要再比较关键字。
那么为什么说B+树比B-树更适合实际应用中操作系统的文件索引和数据库索引?
B+树的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
时间复杂度(时间一去不复返)
同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率,算法分析的目的在于选择合适算法和改进算法
我们希望随着问题规模的增长复杂度是趋于稳定地上升,但是上升的幅度不能太大
聚簇索引与非聚簇索引
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。
术语‘聚簇’表示数据行和相邻的键值聚簇的存储在一起。
如下图,左侧的索引就是聚簇索引,因为数据行在磁盘的排列和索引排序保持一致
术语‘聚簇’表示数据行和相邻的键值聚簇的存储在一起。
如下图,左侧的索引就是聚簇索引,因为数据行在磁盘的排列和索引排序保持一致
除了自己建的主键之外,建的所有字段都是非聚簇索引
空间复杂度(可以用钱买)
空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。
mysql索引分类
单值索引
即一个索引只包含某个单列,一个表中可以有多个单列索引
create index 索引名 on 表名(索引字段);
唯一索引
索引列的值必须唯一,但允许为null值
create unique index 索引名 on 表名(表字段)
主键索引
设定为主键后数据库会自动建立索引, innoDB 为聚簇索引
复合索引
即一个索引包含多个列
create index 索引名 on 表名(索引字段1,索引字段2...);
基本语法
索引
alter四种方式建立索引
不给你索引名根据表名,库名如何删除索引,只剩下主键索引
查询索引名(information_schema库的STATISTICS 表)
SELECT s.INDEX_NAME,s.COLUMN_NAME,s.INDEX_TYPE,s.CARDINALITY FROM information_schema.STATISTICS s WHERE s.TABLE_NAME = 't_emp' AND s.TABLE_SCHEMA = 'datelog' and index_name <> 'PRIMARY' and seq_in_index=1
怎么把字符串转变为sql
使用游标 和 预编译
使用存储过程进行删除
mysql索引结构
BTree索引
hash索引
full-text全文索引
r-tree索引
那些情况下可以建立索引
主键自动建立唯一索引
频繁作为查询字段应该建立索引
查询中与其他表关联的字段,外键建立索引
频繁更新的字段不适合建立索引
where 条件用不到字段不需要创建索引
单键/组合索引选择问题 who? 在高并发的情况下使用组合索引
查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
查询中统计或者分组字段
那些情况下不要建立索引
表记录太少
经常增删改的字段
where 条件里用不到的字段不创建索引
过滤性不好的不适合建立索引
数据重复且分布均匀的表字段,因为应该只为最经常查询和最经常排序的数据列建立索引
性能分析
Mysql Query Optimizer
mysql常见性能瓶颈
CPU: CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据的时候
IO: 磁盘I/O 瓶颈发生在装入数据远大于内存容量的时候
服务器硬件的性能瓶颈 : top, free , iostal 和 vmstat 来查看系统的性能状态
Explain
是什么(执行计划)
能干嘛
表的读取顺序(id)
数据读取操作的操作类型(selecttype)
那些索引可以使用(possiblekeys)
那些索引被实际使用(key)
表之间的引用(ref)
每张表有多少行被物理查询(rows)
怎么玩
Explain + sql语句
执行计划包含的信息
字段解释
id
select 查询的序列号,包含一组数字,表示查询中执行 select 子句或操作表的顺序
又分为三种顺序
id 相同,执行顺序从上到下
id不同,如果是子查询,id序号会递增,id值越大优先级越高,越先被执行
id相同又不同,同时存在 先从大到小,然后再从上到下
id 号每个号码,表示一趟独立的查询。一个sql的查询趟数越少越好
select_type
有哪些类型
table
partitions
type
显示查询使用了何种类型 从最佳到最差依次是: system > const > eq_ref > ref > range > index > ALL
system
表只有一行数据(等于系统表),这是const类型的特例,平时不会出现,这个也可以忽略不计
const
表示通过索引一次就找到了,const 用于比较 primary key 或者 union 索引。因为只匹配一行数据,所以很快如将逐渐置于 where 列表中, mysql 就能将该查询转换为 一个常量
eq_ref
唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描
ref
非唯一索引扫描,返回匹配某个单独值的所有行,本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而它可能找会找打多个符合条件的行,所以它应该属于查找和扫描的混合体
range
只检索给定范围的行,使用一个索引来选择行,key列显示使用了那个索引,一般就是在你 where 语句中出现 between < , > , in等语句 ,这种扫描索引比全表扫描要好,因为它只需要开始与索引的某一点,而结束语另一点,不用全表扫描
all
full table scan 将遍历全表以找到匹配的行
index_merge
在查询过程中需要多个索引组合使用,通常出现有 or 的关键字sql中
ref_or_null
对于某个字段即需要关联条件,也需要null值的情况下,查询优化器会
index_subquery
利用索引来关联子查询,不在全表扫描
unique_subquery
该连接类型类似于 index_subquery 子查询中的唯一索引
备注 : 一般来说,将保证查询至少达到range 级别最好能达到 ref。
如果你的type类型为 all 并且你的数据在 百万以上的请你优化你的sql语句
possiblekeys
显示可能应用在这张表中的索引,一个或多个。查询设计到的字段上若存在索引,则改索引将被列出,但不一定被查询实际使用
key
实际使用的索引,如果为null ,则没有使用索引
查询中使用了覆盖索引,则改索引仅出现在 key 列表中
key_len
表示索引中使用的字节数,可通过该列计算查询中使用的索引长度。在不损失精确性的情况下长度越短越好,key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出来的(key_len 越大越好)
同样查询结果的前提下,精度越小越好
ref
显示索引的哪一列被使用了,如果可能的话,是一个常数,那些列或常量被用于查找索引列上的值
查询中与其它关联表的字段,外键关联建立索引
rows
rows 列显示Mysql 认为它执行查询时必须检查的行数 (值越小越好)
filtered
filtered列表示将按表条件筛选的表行的估计百分比。最大值为100,这意味着没有对行进行筛选。值从100减小表示过滤量增加。rows显示检查的估计行数,rows×filtered显示将与下表联接的行数。例如,如果rows为1000,filtered为50.00(50%),则要与下表联接的行数为1000×50%=500。
Extra
包含不适合在其它列中显示但十分重要的额外信息
Using filesort(要你命三千)
Using temporary(要你命三万)
Using index
Using where
表名使用了 where 过滤
Using join buffer
使用了连接缓存
Impossible WHERE
如何出现这个问题?
WHERE子句总是false,不能用来获取任何元组
Select tables optimized away
在没有 group by子句的情况下,基于索引优化 min/max 操作或者对于 Myisam 存储引擎优化 count(*) 操作,不必等到执行阶段在进行计算,查询执行计划生成的阶段即完成优化
Distinct
优化 distinct 操作,第一个匹配的行后,它将停止为当前行组合搜索更多行
热身case
索引优化
索引分析
单表优化
创表语句
问题及优化
问题?
解决思路1
解决思路2
俩表优化
建表语句
left join案例分析1
left join案例分析2
right join ,和left join 基本没变化,记住一句话: left join(左)把索引建立在 join后的那个表,right join(右连接)把索引建立在 right 前面那个表 (相反建立索引)
三表优化
建表语句
索引优化
Join优化总结
1),尽可能减少Join语句中的NestedLoop的循环总次数,永远用小结果集驱动大的结果集(小表驱动大表)
2),优先优化 NestedLoop的内层循环
3),保证Join语句中被驱动表上Join条件字段已经被索引
4),当无法保证驱动表的Join条件字段被索引且内存资源充足的前提下,不要太吝啬JoinBuffer的设置
索引失效(应该避免)
建表sql
单表-索引失效的原因
单表-一般性建议
对于单键索引,尽量选择针对当前query 过滤性更好的索引
在选择组合索引的时候,当前Query中过滤性最好的字段顺序中,位置越靠前越好
在选择组合索引的时候,尽量选择可以能够包括当前query中 where 子句中更多字段的索引
在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序列的最后
书写sql语句时,尽量避免造成索引的失效情况
单表-口诀
关联表查询优化
建表语句
left /right join案例
explain select * from class left join book on class.card=book.card;
结论: type类型是all ,这时的迪达尔结果集为 400
为 book表的card 建立索引,如下 create index idx_book_card on book(card); 然后重新执行 explain语句
结论: 这是 book表使用了索引 type类型为 ref 笛卡尔结果集为 20 其中Extra 也没有了 Using join buffer
如果我们在给class 表的card 也建立索引是否能更快的查询到数据呢? create index idx_class_card on class(card); 再次执行 explain 语句
结论: 就算我们为俩个表的主外键都建立了索引 它们的笛卡尔乘积都是20 性能也没有得到很大的提升 ,class表也是进行了全表的扫描,索引,当我们进行关联查询的时候,尽量给被驱动表建立索引,驱动表不必建立索引
inner join 案例
小表驱动大表 ,小表放在驱动表 ,大表放在被驱动表
关联查询-建议
保证被驱动表的join字段已经被索引
left join 时,选择小表作为驱动表,大表作为被驱动表
inner join时,mysql会自己帮你把小结果集选为驱动表
子查询尽量不要放在被驱动表,有可能使用不到索引
能够直接多表关联的尽量直接关联,不用子查询
子查询优化
尽量不要使用 not in 或者 not exists 用left join 表名 on xxx is null 来代替它
面试题讲解
定值,范围,还是排序,一般 order by 是给个范围
group by 基本上都需要进行排序,如果有错乱的产生,会有临时表产生
一般性建议
在对于单键索引,尽量避免选择针对当前 Query 过滤性更好的索引
在选择组合索引的时候,当前 Query 中过滤性最好的字段在索引字段顺序中,位置越靠左越好
在选择组合索引的时候,尽量选择可以能够包含当前 Query 中的 where 子句中更多字段的索引
尽可能通过分析统计信息和调整Query 的写法来达到选择合适索引的目的
索引优化口诀
全值匹配我最爱,最左前缀要遵守
带头大哥不能死,中间兄弟不能丢
索引列少计算,范围之后全失效
like 百分写最右,覆盖索引不写*
不等null还有or,索引失效要少用
查询截取分析
查询优化
永远小表驱动大表 (类似嵌套循环 Nested Loop)
order by关键字优化
order by 尽量使用Index方式排序,避免使用 filesort 方式排序
如果排序的字段不在索引列上,filesort有俩种算法
单路排序
双路排序
结论及引申出来的问题?
优化策略
增大 sort_buffer_size的容量
增大 max_length_for_sort_data参数的设置
why?(提高order by 的速度)
1), order by 时 select * 是一个大忌只 Query 需要字段,这点非常重要。
2),尝试提高 sort_buffer_size
不管用那种算法,提高这个参数都会提高效率,当然,要根据系统的能力去提高,因为这个参数是针对每个线程的 1M-8M之间(一个sql在I兆8兆之间)
3),尝试提高 max_length_for_sort_data
提高这个参数,会增加用改进算法的概率,但是如果设的太高,数据总量超出 sort_buffer_size 的概率会增大,明显症状是高的磁盘I/O活动和低的处理器使用率。
1024-8192之间
1024-8192之间
总结和查询要点(为排序使用索引)
1),mysql 俩种排序方式: 文件排序或扫描有序索引排序
2),mysql能为排序与查询使用相同的索引
Case (创建索引 key_a_b_c)
order by使用最左前缀
order by a
order by a,b
order by a,b,c
order by a desc,b desc ,c desc
where 使用索引的最左前缀定义为常量,则 order by 能使用索引
where a=const order by b,c
where a=const and b=const order by c
where a=const and b > const order by b,c
不能使用索引进行排序
order a desc , b asc ,c desc (排序不一致)
where g=const order by b,c (带头大哥a不能丢)
where a=const order by c (中间兄弟 b 不能丢)
where a=const order by a,d (d不是索引的一部分)
where a in (...) order by b,c (对于排序来说,多个相等条件也可是范围查询)
Group by关键字优化
group by 使用索引的原则几乎根 order by 一致,唯一区别就是 group by 即使没有过滤条件也能用到索引
当无法使用索引列,增大 max_length_for_sort_data 参数的设置 + 增大 sort_buffer_size参数的设置
where 高于 having,能写在 where 限定的条件就不要去 having限定了
最后使用索引的手段: 覆盖索引(简单来说就是 select 和到 from 之间查询的列 <= 使用的索引列 + 主键)
慢查询日志
是什么?
1.Mysql 的慢查询日志是Mysql提供的一种日志记录,它用来记录在 Mysql 中相应时间超过阈值的sql 语句,具体指运行时间超过
**long_query_time**值的sql,则会被记录在日志文件中。
2.具体指超过 long_query_time 值的sql,则会被记录在慢查询日志中。 long_query_time的默认值为10 ,意思是超过10s的sql语句。
3.由他来查看那些sql语句超过我们最大忍耐时间值,比如一条sql超过5秒钟,我们就算慢sql,希望他能说收集超过5秒的sql,结合 explain 进行优化。
**long_query_time**值的sql,则会被记录在日志文件中。
2.具体指超过 long_query_time 值的sql,则会被记录在慢查询日志中。 long_query_time的默认值为10 ,意思是超过10s的sql语句。
3.由他来查看那些sql语句超过我们最大忍耐时间值,比如一条sql超过5秒钟,我们就算慢sql,希望他能说收集超过5秒的sql,结合 explain 进行优化。
怎么玩?
说明
查看是否开启及如何开启
默认
show variables like '%slow_query_log%';
全局开启
set global slow_query_log=1;
永久开启慢查询日志如何做呢?
如果开启了慢查询日志,什么样的sql将被记录在慢查询日志里呢?
查看默认阈值
查看默认阈值
show variables like '%query_time%';
修改为阈值大于3的慢sql
set long_query_time=3;
Case分析
默认睡4秒的sql : select sleep(4);
然后去你的 slow_query_file 路径下面查看你的慢sql
如何查看当前系统存在多少条慢sql
show global status like '%Slow_queries%';
配置版本怎么玩呢?
日志分析工具 mysqldumpslow
s : 是表示按照何种方式排序
c : 访问次数
l : 锁定时间
r : 返回记录
t : 查询时间
al :平均锁定时间
ar : 平均返回记录数
at : 平均查询时间
t : 即返回前面多少条的数据
g : 后面搭配一个正则匹配模式,大小写不敏感的
c : 访问次数
l : 锁定时间
r : 返回记录
t : 查询时间
al :平均锁定时间
ar : 平均返回记录数
at : 平均查询时间
t : 即返回前面多少条的数据
g : 后面搭配一个正则匹配模式,大小写不敏感的
mysqldumpslow 工作常用参考
得到返回记录集最多的10个sql集合
mysqldumpslow -s r -t 10 /var/lib/mysql/host_name-slow.log
得到返回次数最多的10个sql
mysqldumpslow -s c -t 10 /var/lib/mysql/host_name-slow.log
按照时间排序的前10条里面包含有左连接的查询语句
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/host_name-slow.log
建议 : 使用这些命令时结合 | more 使用,否则有可能出现爆屏的情况
批量数据脚本
建表语句
设置参数 log_bin_trust_function_creators
默认
show variables like '%log_bin_trust_function_creators%';
开启
set global log_bin_trust_function_creators=1;
创建函数,保证每条数据都不相同
随机产生字符串
随机产生部门编号
创建存储过程
创建emp表中插入数据的存储过程
创建dept表中插入数据的存储过程
插入数据
dept表插入
call insert_dept(100,10);
emp表插入
call insert_emp(10001,500000);
show Profile
是什么?
是 Mysql 提供可以用来分析当前会话语句执行的资源消耗情况,可以用于sql的调优测量
默认情况是关闭的
show variables like '%profiling%';
保存最近15次的运行结果
开启 profile
set profiling=1;
分析
运行sql
select * from emp group by id%20 order by 5;
select * from emp group by id%10 limit 150000;
查看结果
show profiles
诊断sql , show profile cpu,block io for query 上一步前面的问题sql数字号码
all : 显示所有的开销信息
block io : 显示块IO相关开销
context switches : 上下文切换相关开销
cpu : 显示cpu相关开销信息
ipc : 显示发送和接受相关开销的信息
memory : 显示内存相关开销信息
page faults : 显示页面错误相关开销信息
source : 显示和source_function , source_file , source_line 相关开销信息
swaps : 显示交换次数相关开销信息
block io : 显示块IO相关开销
context switches : 上下文切换相关开销
cpu : 显示cpu相关开销信息
ipc : 显示发送和接受相关开销的信息
memory : 显示内存相关开销信息
page faults : 显示页面错误相关开销信息
source : 显示和source_function , source_file , source_line 相关开销信息
swaps : 显示交换次数相关开销信息
日常开发中需要出现的问题
status状态栏出现下面几种情况
Creating tmp table 创建临时表
拷贝数据到临时表
用完再删除
converting HEAP to MyISAM 查询结果太大,内存都不够用了往磁盘上搬数据了
Copying to tmp table on disk 把内存中临时表的数据复制到磁盘,危险
locked
show processlist(展现进程列表)
可以使用 kill id
全局查询日志
永远不要在生产环境开启这个功能
配置启用
编码启用
1), set global general_log =1;
2), set global log_output='TABLE';
查看 select * from mysql.general_log;
2), set global log_output='TABLE';
查看 select * from mysql.general_log;
工具和技巧拾遗
视图 View
什么是视图?
将一段查询的sql封装为一个虚拟的表
虚拟表会不会对sql有优化查询的作用? 不会,因为视图只是进行了封装
这个虚拟表只保存了sql逻辑,不会保存任何查询结果
虚拟表会不会对sql有优化查询的作用? 不会,因为视图只是进行了封装
这个虚拟表只保存了sql逻辑,不会保存任何查询结果
作用
封装复杂sql语句,提高复用性
逻辑放在数据库上面,更新不需要发布程序,面对频繁的需求变更更灵活
逻辑放在数据库上面,更新不需要发布程序,面对频繁的需求变更更灵活
适用场景
很多地方可以共用的一组查询结果
报表
报表
创建/更新语法
创建
reate view 视图名 as sql语句
使用
select * from view_test
更新
create or replace view view_test as
select d.deptName, if(avg(e.age)>50,'老鸟','菜鸟') 老鸟or菜鸟 from t_dept d
inner join t_emp e
on d.id=e.deptId
group by d.deptName,d.id
注意事项(使用 5.5)
mysql 的视图中不允许有form 后面的子查询,但 oracle可以
mysql锁机制
概念
什么是锁?
生活购物
锁的分类
从对数据库操作的类型进行分类
读锁
读锁 (共享锁) : 针对同一份数据,多个读操作可以同时进行而不会互相影响
写锁
写锁(排它锁) : 当前写操作没有完成之前,它会阻止其它写锁和读锁。
从对数据库操作的粒度分类
表锁(偏读)
偏向 **MyISAM** 存储引擎,开销小,加锁块,无死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低。
行锁(偏写)
偏向于 **InnoDB**存储引擎,开销大,加锁慢,会出现死锁,锁定粒度小,发生锁冲突的概率最低,并发读也很高。
InnoDB 和 Myisam 区别
1.支持事务 (transaction)
2.采用了行级锁
InnoDB 和 Myisam 区别
1.支持事务 (transaction)
2.采用了行级锁
页锁
开销和加锁时间介于表锁和行锁之间:会出现死锁,锁定的粒度介于表锁和行锁之间,并发度一般
主从复制
主从复制的基本原理
slave 会从mast读取binlog来进行数据同步
原理图
mysql复制过程分成三步
1.master 将改变记录到二进制日志(binary log) ,这些记录过程叫做二进制事件, binary log events。
2.slave 将 master 的binary log events 拷贝到它的中继日志(reay log)。
3.slave 重做中继日志中的事件,将改变应用到自己的数据库中。mysql复制式异步的且串行化的。
2.slave 将 master 的binary log events 拷贝到它的中继日志(reay log)。
3.slave 重做中继日志中的事件,将改变应用到自己的数据库中。mysql复制式异步的且串行化的。
复制的基本原则
每个salve只有一个 master
每个salve只能有一个唯一的服务器ID
每个master 可以有多个salve
每个salve只能有一个唯一的服务器ID
每个master 可以有多个salve
复制的最大问题
网络延时
一主一从的常见配置
前提条件
mysql 版本一致且后台以服务运行
双向网络是否ping通
主从复制都配置在 [mysqld]节点下面.都是小写
双向网络是否ping通
主从复制都配置在 [mysqld]节点下面.都是小写
修改主机配置文件 (my.ini)文件
server-id=1
必填项
log-bin=mysql-bin
启用二进制配置文件
如下
log-bin=mysql-bin
如下
log-bin=mysql-bin
log-err=本地mysql路径/mysqlerr
可选的,不是必填的
如下
log-err=E:/mysql/mysql-5.7.21-winx64/data/mysqlerr
如下
log-err=E:/mysql/mysql-5.7.21-winx64/data/mysqlerr
basedir=mysql本地路径
可选的
如下
basedir=E:/mysql/mysql-5.7.21-winx64
如下
basedir=E:/mysql/mysql-5.7.21-winx64
tmpdir=mysql本地路径
临时文件 ,可选的
如下
tmpdir=E:/mysql/mysql-5.7.21-winx64
如下
tmpdir=E:/mysql/mysql-5.7.21-winx64
datadir=mysql本地路径/data
mysql数据库的数据库[数据目录]
如下
datadir=E:/mysql/mysql-5.7.21-winx64/data
如下
datadir=E:/mysql/mysql-5.7.21-winx64/data
read-obly=0
主机,读写都可以
binlog-ignore-db=mysql
设置不可复制的数据库(不像复制的数据库,填写在下面)
binlog-do-db=数据库名
设置需要复制的数据库名
binlog_format=STATEMEN(默认)
设置binlog格式
三种
1), STATMEN (bin-log中会记录所有的写sql,如果写操作里面包含 函数 now()会造成主从复制不一致)
2),row (不在bin-log中记录写的sql语句,而是记录执行完sql后每一行的改变,这样可以避免函数主从复制不一致的情况,这也是有问题的? 效率低,当表大数据的时候)
3),mixed
判断写操作中是否有函数,如果有函数则使用row 行模式 没有函数则使用 statmen 写模式
问题?
如果出现 @@host_name 系统变量,mixed 也没办法判断
三种
1), STATMEN (bin-log中会记录所有的写sql,如果写操作里面包含 函数 now()会造成主从复制不一致)
2),row (不在bin-log中记录写的sql语句,而是记录执行完sql后每一行的改变,这样可以避免函数主从复制不一致的情况,这也是有问题的? 效率低,当表大数据的时候)
3),mixed
判断写操作中是否有函数,如果有函数则使用row 行模式 没有函数则使用 statmen 写模式
问题?
如果出现 @@host_name 系统变量,mixed 也没办法判断
修改从机my.cnf文件
server-id=2
log-bin=mysql-bin
log-bin=mysql-bin
重启 主机mysql服务 和从机mysql服务,注意关闭 linux的防火墙
linux 查看防火墙状态
systemctl status firewalld
主机和从机设置
主机
grant replication slave on *.* to '账号'@'从机数据库ip' identified by '密码' ;
grant replication slave on *.* to 'zhangsan'@'192.168.73.129' identified by '123456' ;
刷新权限
flush privileges;
查看主机的状态
show master status;
从机
change master to master_host='192.168.73.1',master_user='zhangsan',master_password='123456', master_log_file='mysql-bin.000004',master_log_pos=606;
change master to master_host='192.168.73.1',master_user='zhangsan',master_password='123456', master_log_file='mysql-bin.000004',master_log_pos=606;
启动slave
start slave
查看从机状态
show slave status \G;
如下执行成功
从机出现 问题 ERROR 3021 (HY000): This operation cannot be performed with a running slave io thread; run STOP SLAVE IO_THREAD FOR CHANNEL '' first.
上一次已经启动了一个主从复制,需要把上一个主从复制关闭,然后再次启动从机的语句
stop slave;
reset master;
stop slave;
reset master;
测试内容
主机新建表新建库增加数据
从机测试数据
mycat 8066端口
介绍
是什么?
数据库中间件
干什么的
读写分离
数据分片
垂直拆分
水平拆分
垂直+水平拆分
水平拆分
垂直+水平拆分
多数据源整合
原理
拦截
Mycat 的原理中最重要的一个动词就是 拦截 , 它拦截了用户发送过来的sql语句。首先对sql语句做了一些特定的分析,: 如分片分析 , 路由分析, 读写分离分析 ,缓存分析等,然后将此sql发往后端的真实数据库,并返回的结果做适当的处理,最终返回给用户
安装
下载地址 https://github.com/MyCATApache/Mycat-download
把 mycat 文件的所有内容 copy 到/user/local/
cp -r mycat/ /usr/local/
修改配置文件
server.xml
为了让防止与mysql名字相同,修改mycat的用户名
user name="mycat" name="password" 123456 name="schemas">TESTDB
user name="mycat" name="password" 123456 name="schemas">TESTDB
schema.xml
删除 <schema></schema> 里面的所有内容
显示行数
:set nu
删除6行,32行所有内容
: 6,32 d
:set nu
删除6行,32行所有内容
: 6,32 d
添加 <schema dataNode="dn1"></schema>
修改 <dataNode dataHost="host1" database="自己数据库名"></dataNode>
修改 <dataHost name="host1"></dataHost>
<writeHost url="192.168.73.1:3306" user="root" password="root"></writeHost>
写主机 ,红色是需要修改的字段
<readHost host="hostS2" url="192.168.1.200:3306" user="root" password="root" />
写主机 ,红色是需要修改的字段
<readHost host="hostS2" url="192.168.1.200:3306" user="root" password="root" />
修改完如下
rule.xml
验证数据库访问情况
(2个主机的mysql都需进行检验)mysql -uroot -proot -h 192.168.73.131 -P 3306
如果出现错误(ERROR 1045 (28000): Access denied for user 'root'@'192.168.73.131' (using password: YES))
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'root' WITH GRANT OPTION;
flush privileges;
如果出现错误(ERROR 1045 (28000): Access denied for user 'root'@'192.168.73.131' (using password: YES))
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'root' WITH GRANT OPTION;
flush privileges;
启动程序
控制台启动
在 mycat/bin目录下
mycat console
mycat console
后台启动
在 mycat/bin目录下
mycat start
mycat start
启动时可能出现报错
域名解析失败
修改 etc/hosts文件添加linux用户名
如何查看linux用户名
hostname
添加系统名etc/hosts文件中
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 localhost.localdomain
如何查看linux用户名
hostname
添加系统名etc/hosts文件中
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 localhost.localdomain
Unable to start JVM: No such file or directory (2)
修改 mycat中conf里面的wrapper.conf
wrapper.java.command=java安装目录/bin/java
wrapper.java.command=java安装目录/bin/java
Unable to start JVM: Permission denied (13)
修改 mycat中conf里面的wrapper.conf
wrapper.java.command=java安装目录/bin/java
wrapper.java.command=java安装目录/bin/java
登录
后台管理窗口
mysql -urooot -proot -P9066 -h ip地址
数据窗口(默认启动)
mysql -urooot -proot -P8066 -h ip地址
读写分离(先实现主从复制然后才能实现读写分离)
schema.xml(balance=?)
0(默认)
不开启读写分离机制,所有的读操作都发送到当前可用的 writeHost 上
1
全部的 readHost 与 stand by writerHost 参与 select 语句的负载均衡,简单的说就是 当双主双从模式( M1 -> S1, M2 -> S2,并且M1 与 M2互为主备)正常情况下 M2,S1,S2 都参与 select 语句的负载均衡,M2才能写入
2
所有读操作都随机的在 writerHost, readHost 上分发
3
所有读请求随机分发到readHost上执行,writerHost 不负担读压力
@@hostname 使俩个库插入不同的用户名来查看
分库
如何选择分库表,;栗子 把客户表拆分出来一个独立的服务器,订单表和其它关联的表一个服务器
为啥 要把客户相关的表拆分出来呢,因为 客户表与订单表没有join连接的关系,可以独立拆分出来
schema.xml
配置完成如下
分表操作(订单表 -orders)
订单表 根据customer_id 进行分表
schema.xml
增加一行
<table name="orders" dataNode="dn1,dn2" rule="mod_rule" />
<table name="orders" dataNode="dn1,dn2" rule="mod_rule" />
rule.xml
<tableRule name="mod_rule">
<rule>
<columns>customer_id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
mod-long对customer_id进行取模运算
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
<!-- how many data nodes -->
<property name="count">2</property>
</function>
<rule>
<columns>customer_id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
mod-long对customer_id进行取模运算
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
<!-- how many data nodes -->
<property name="count">2</property>
</function>
向orders 插入6行数据,得到如下内容
问题? orders 不能使用数据库默认的自增主键,应该在插入的时候指定主键,
跨库join
ER表(订单详情表-orders_detail)
添加orders的字表 orders_detail的 schema.xml
<table name="orders" dataNode="dn1,dn2" rule="mod_rule">
<childTable name="orders_detail" primaryKey="id" joinKey="order_id" parentKey="id" />
</table>
name="orders_detail" :字表订单详情表表名
primaryKey="id" id 是订单标详情的主键id
joinKey="order_id" order_id 是订单详情表的字段关联父类的字表字段
parentKey="id" id 是orders 表关联字表的字段
<childTable name="orders_detail" primaryKey="id" joinKey="order_id" parentKey="id" />
</table>
name="orders_detail" :字表订单详情表表名
primaryKey="id" id 是订单标详情的主键id
joinKey="order_id" order_id 是订单详情表的字段关联父类的字表字段
parentKey="id" id 是orders 表关联字表的字段
向订单详情表插入数据,并执行查询的sql语句
插入
insert into orders_detail (id,detail,order_id) values (1,'details',1);
insert into orders_detail (id,detail,order_id) values (2,'details',2);
insert into orders_detail (id,detail,order_id) values (3,'details',3);
insert into orders_detail (id,detail,order_id) values (4,'details',4);
insert into orders_detail (id,detail,order_id) values (5,'details',5);
insert into orders_detail (id,detail,order_id) values (6,'details',6);
insert into orders_detail (id,detail,order_id) values (2,'details',2);
insert into orders_detail (id,detail,order_id) values (3,'details',3);
insert into orders_detail (id,detail,order_id) values (4,'details',4);
insert into orders_detail (id,detail,order_id) values (5,'details',5);
insert into orders_detail (id,detail,order_id) values (6,'details',6);
关联查询sql
select o.*,d.* from orders o inner join orders_detail d on o.id = d.order_id;
全局表(dict_order_type)
schema.xml
<table name="dict_order_type" dataNode="dn1,dn2" type="global" />
向订单字典表插入数据
insert into dict_order_type (id,order_type) values (101,'type1');
insert into dict_order_type (id,order_type) values (102,'type2');
insert into dict_order_type (id,order_type) values (102,'type2');
MYSQL基础
JDK
jdk1.8新特性
函数式接口
概念
当一个接口有且只有一个抽象方法时,该接口为函数接口
注解
@FunctionalInterface
Lambda表达式
前提
必须是在函数式接口中使用
格式
完全格式
(形参列表) -> { 方法体 }
简化格式1: 当参数只有一个
m ->{方法体}
可以省略()
可以省略()
简化格式2:当方法体只有一条语句
(形参列表) -> 一条语句
可以省略{}
可以省略{}
简化格式3:当方法有返回值并且只有一条语句
形参列表) -> 省略retrun关键词和{} 必须同时省略
注意
lambda表达式 和接口的抽象方法名没有关系。 参数的类名没有体现
使用
接口名 引用 = Lambda表达式
方法引用
前提
必须是函数式接口作为类型,来存储方法引用
本质
就是将方法体作为一个可以存储的内容。并且进行赋值给 函数式接口
格式
非静态方法
对象名::方法名
静态方法
类名::静态方法名
内置函数式接口
消费型接口
接口名: Supplier<T>
抽象方法:T get();
方法的特点:没有参数,但有返回值
抽象方法:T get();
方法的特点:没有参数,但有返回值
供给型接口
接口名: Supplier<T>
抽象方法:T get();
方法的特点:没有参数,但有返回值
抽象方法:T get();
方法的特点:没有参数,但有返回值
函数型接口
接口名:Function<T,R>
抽象方法: R apply(T t)
方法的特点:有一个参数,同时有返回值
抽象方法: R apply(T t)
方法的特点:有一个参数,同时有返回值
断言型接口
接口名: Predicate<T>
抽象方法: boolean test(T t)
方法的特点:有一个参数,返回值类型是boolean
抽象方法: boolean test(T t)
方法的特点:有一个参数,返回值类型是boolean
Streaming API
作用:针对单列集合,提供了一个Stream类型,可以对集合中的元素进行处理
优点:避免了对于集合中元素的遍历
获取stream类型的方式
Collection
通过调用 stream() ,返回Stream类型
Map
通过将双列集合转变为单列集合之后,再调用stream方法
数组
通过Stream类型中的静态方法 of()
说明:数组中的元素作为可变参数传入方法中
说明:数组中的元素作为可变参数传入方法中
常用方法
延续方法
调用完成之后,返回值依然是Stream类型,可以继续调用Stream类中的方法
具体方法
Stream<T> filter(Predicate<? super T> predicate)
过滤元素
Stream<T> limit(long maxSize)
截取
Stream<T> skip(long n)
跳过
Stream<T> sorted()
排序
终结方法
过滤元素
Stream<T> limit(long maxSize)
截取
Stream<T> skip(long n)
跳过
Stream<T> sorted()
排序
终结方法
终结方法
调用完成之后,返回值不是stream类型。无法继续调用stream类中的方法
具体方法
void forEach(Consumer<? super T> action)
遍历 (经常用于打印)
long count()
显示元素个数
遍历 (经常用于打印)
long count()
显示元素个数
设计模式
创建型模式(6种)
简单工厂方法模式(Simple Factory)
定义
是指由一个工厂对象决定创建出哪一种产品类的实例,但它不属于23种设计模式,
简单工厂适用于工厂类负责创建的对象较少的场景,且客户端只需要传入工厂类的参数,
对于如何创建对象的逻辑不需要关心
简单工厂适用于工厂类负责创建的对象较少的场景,且客户端只需要传入工厂类的参数,
对于如何创建对象的逻辑不需要关心
案例
通俗理解:FACTORY—追MM少不了请吃饭了,麦当劳的鸡翅和肯德基的鸡翅都是MM爱吃的东西,
虽然口味有所不同,但不管你带MM去麦当劳或肯德基,只管向服务员说“来四个鸡翅”就行了。麦当劳和肯德基就是生产鸡翅的Factory
虽然口味有所不同,但不管你带MM去麦当劳或肯德基,只管向服务员说“来四个鸡翅”就行了。麦当劳和肯德基就是生产鸡翅的Factory
源码应用
源码中的应用:在JDK源码中无处不在:
1:Calendar类,Calendar.getInstance();
2: logback LoggerFactory有多个重载方法getLogger()
1:Calendar类,Calendar.getInstance();
2: logback LoggerFactory有多个重载方法getLogger()
类结构图
抽象工厂模式(Abstract Factory)
定义
提供一个创建一系列相关活相互依赖对象的接口,无序指定他们具体的类,客户端(应用层)不依赖与产品类实现如何被创建,实现等细节,强调的是一系列相关的产品对象(属于同一产品族)一起使用常见对象需要大量重复的代码,需要提供一个产品类的库,所有的产品已同样的接口出现,从而使客户端不依赖与具体实现;
理解
同一类产品在一个工厂内处理,因为有相同的属性
源码中的应用
使用场景
根据不同的支付类型处理
优缺点
1:规定了所有可能被常见的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂接口,
2:增加了系统的抽象性和理解难度
2:增加了系统的抽象性和理解难度
类结构图
工厂方法模式(Factory Method)
定义
是指定义一个创建对象的接口,但让实现这个接口的类来界定实例化那个类,
工厂方法让类的实例化推迟到子类中进行,在工厂方法模式中用户只需要关心所需产品对应的工厂,
无序关心创建细节,而且加入新的产品符合开闭原则
工厂方法让类的实例化推迟到子类中进行,在工厂方法模式中用户只需要关心所需产品对应的工厂,
无序关心创建细节,而且加入新的产品符合开闭原则
理解
根据单一职责,工厂方法可认为专人干专事
源码中的应用
logback中工厂方法 ILoggerFactory(接口)
实现有:LoggerContext(),SubstituteLoggerFactory(),NOPLoggerFactory()
实现有:LoggerContext(),SubstituteLoggerFactory(),NOPLoggerFactory()
使用场景
1:创建对象需要大量重复的代码,
2:客户端(应用层)不依赖与产品类实列对河被创建,实现等细节
3:一个类通过其子类来指定创建那个对象
2:客户端(应用层)不依赖与产品类实列对河被创建,实现等细节
3:一个类通过其子类来指定创建那个对象
优缺点
1:类的个数容易过多,增加复杂度
2:增加了系统的抽象性和理解难度
2:增加了系统的抽象性和理解难度
类结构图
建造者模式(Builder)
定义
将一个复杂对象的构建过程与它的表示分离,使得同样的构建过程可以创建不同的表示,
使用创建者模式对于用户而言只需要指定需要建造的类型就可以获得对象,建造过程及细节不需要了解
或者
将产品的内部表象和产品的生成过程分割开来,从而使一个建造过程生成具有不同的内部表象的产品对象。
建造模式使得产品内部表象可以独立的变化,客户不必知道产品内部组成的细节。建造模式可以强制实行一种分步骤进行的建造过程。
使用创建者模式对于用户而言只需要指定需要建造的类型就可以获得对象,建造过程及细节不需要了解
或者
将产品的内部表象和产品的生成过程分割开来,从而使一个建造过程生成具有不同的内部表象的产品对象。
建造模式使得产品内部表象可以独立的变化,客户不必知道产品内部组成的细节。建造模式可以强制实行一种分步骤进行的建造过程。
理解
BUILDER—MM最爱听的就是“我爱你”这句话了,见到不同地方的MM,要能够用她们的方言跟她说这句话哦,我有一个多种语言翻译机,上面每种语言都有一个按键,见到MM我只要按对应的键,它就能够用相应的语言说出“我爱你”这句话了,国外的MM也可以轻松搞掂,这就是我的“我爱你”builder。
优缺点
1:封装性好,创建与使用分离;
2:扩展性好,建造类之间独立,一定程度上解耦了;
1:产生多余的Builder对象;
2:产品内部发生变化,建造者都要修改,成本大
2:扩展性好,建造类之间独立,一定程度上解耦了;
1:产生多余的Builder对象;
2:产品内部发生变化,建造者都要修改,成本大
源码中的应用
1:JDK的StringBuilder 提供append()方法
2:MyBatis中, CacheBuilder cacheBuilder.build()
3:在MyBatis中,SqlSessionFactoryBuilder 通过调用build()方法获得的是一个SqlSessionFactory类
4:Spring中,BeanDefinitionBuilder通过调用getBeanDefinition()方法获得一个BeanDefinition对象
5:还有个主要的应用,就是链式编程,也是java8的新特性
2:MyBatis中, CacheBuilder cacheBuilder.build()
3:在MyBatis中,SqlSessionFactoryBuilder 通过调用build()方法获得的是一个SqlSessionFactory类
4:Spring中,BeanDefinitionBuilder通过调用getBeanDefinition()方法获得一个BeanDefinition对象
5:还有个主要的应用,就是链式编程,也是java8的新特性
与工厂模式的区别
1:建造者模式更加关注方法的调用顺序,工厂更加注重与创建对象。
2:创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的都一样。
3:关注重点不一样,工厂迷失只需要把对象创建出来就可以了,而建造者模式中不仅要创建出这个对象,还要知道这个对象由哪些部件组成。
4:建造者模式根据建造过程中的顺序不一样,最终的对象部件组成也不一样。
2:创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的都一样。
3:关注重点不一样,工厂迷失只需要把对象创建出来就可以了,而建造者模式中不仅要创建出这个对象,还要知道这个对象由哪些部件组成。
4:建造者模式根据建造过程中的顺序不一样,最终的对象部件组成也不一样。
类的结构图
原型模式(Prototype)
定义
是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,属于创建型模式
理解
PROTOTYPE—跟MM用QQ聊天,一定要说些深情的话语了,我搜集了好多肉麻的情话,
需要时只要copy出来放到QQ里面就行了,这就是我的情话prototype了
需要时只要copy出来放到QQ里面就行了,这就是我的情话prototype了
原型模式
浅克隆
深克隆
深克隆
源码中的应用
JDK中Cloneable,
ArrayList clone()
ArrayList clone()
优缺点
1:性能优良,Java 自带的原型模式,是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多
2:可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用,可辅助实现撤销操作
3:需要为没一个类配置一个克隆方法
4:克隆方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违法了开闭原则
5:在实现深克隆时需要编写较为复杂的代码,
2:可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用,可辅助实现撤销操作
3:需要为没一个类配置一个克隆方法
4:克隆方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违法了开闭原则
5:在实现深克隆时需要编写较为复杂的代码,
单例模式(Singleton)
定义
是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点,单列模式在实现生活中应用也非常广泛,
ServletContext ,ServletContextConfig ,Sring 框架应用中ApplicationContext,数据库的连接池也都是单例形式;
ServletContext ,ServletContextConfig ,Sring 框架应用中ApplicationContext,数据库的连接池也都是单例形式;
理解
SINGLETON—A有6个漂亮的老婆,她们的老公都是A,A就是他们家里的老公Sigleton,她们只要说道“老公”,都是指的同一个人,那就是A
单列模式
饿汉式单列模式
项目初始化。类加载的时候就立即初始化,并且创建单例对象线程安全的,
在线程还没出现以前就实例化了,不可能存在访问安全问题
优缺点:可以保证线程安全,但是导致内存浪费,
在线程还没出现以前就实例化了,不可能存在访问安全问题
优缺点:可以保证线程安全,但是导致内存浪费,
懒汉式单列模式
单列对象在被使用时才会初始化
优缺点:在多线程环境下,就会出现线程安全问题
优缺点:在多线程环境下,就会出现线程安全问题
注册式单列模式
1:枚举式单例模式
2:容器式单例
2:容器式单例
线程单例实现ThreadLocal
ThreadLocal不能保证其创建的对象是全局唯一的,但是能保证在单个线程中是唯一的,天生是线程安全的,
给方法上锁,以时间换空间,ThreadLocal将所有的对象全部放在ThreadLocalMap中,为每个线程都提供一个对象,
实际上是以空间换时间来实现线程隔离的
给方法上锁,以时间换空间,ThreadLocal将所有的对象全部放在ThreadLocalMap中,为每个线程都提供一个对象,
实际上是以空间换时间来实现线程隔离的
源码中的应用
JDK 中的 RunTime类
结构型模式(7种)
适配器模式(Adapter)
定义
把一个类的接口变成客户端所期待的另一种接口,从而是原本因接口原因不匹配二无法一起工作的两个类能够一起工作,
适配类可以根据参数返还一个合适的实列给客户端。
也可理解为: 当前系统存在两种接口 A 跟B ,客户只支持访问A 接口,但是当前系统没有A接口对象,但是有B接口对象,但客户无法识别B接口,因此需要通过一个适配器C,将B接口内转换成A接口,从而使得客户能够从A接口获取得到B接口内容
适配类可以根据参数返还一个合适的实列给客户端。
也可理解为: 当前系统存在两种接口 A 跟B ,客户只支持访问A 接口,但是当前系统没有A接口对象,但是有B接口对象,但客户无法识别B接口,因此需要通过一个适配器C,将B接口内转换成A接口,从而使得客户能够从A接口获取得到B接口内容
理解
在朋友聚会上碰到了一个美女Sarah,从香港来的,可我不会说粤语,她不会说普通话,只好求助于我的朋友kent了,
他作为我和Sarah之间的Adapter,让我和Sarah可以相互交谈了
他作为我和Sarah之间的Adapter,让我和Sarah可以相互交谈了
优缺点
优点:1:能提高类的透明性和复用,现有的类复用但不需要改变
2:目标类和适配器解耦,提高程序的扩展性
3:在很多业务场景中符合开闭原则
缺点:
1:适配器编写过程需要全面考虑,可能会增加系统的复杂性
2:增加代码阅读难度, 降低代码可读性,过多使用适配器会是系统代码变的凌乱
2:目标类和适配器解耦,提高程序的扩展性
3:在很多业务场景中符合开闭原则
缺点:
1:适配器编写过程需要全面考虑,可能会增加系统的复杂性
2:增加代码阅读难度, 降低代码可读性,过多使用适配器会是系统代码变的凌乱
源码中的应用
SpringAOP 中的 AdvisorAdapter类 有三个实现类,MethodBeforeAdviceAdapter, AfterReturningAdviceAdapter 和 ThrowsAdviceAdapter
SpringMVC 中的HandlerAdapter类 也有多个子类 关键适配主要在DispatcherServlet 的doDisPatch();
SpringMVC 中的HandlerAdapter类 也有多个子类 关键适配主要在DispatcherServlet 的doDisPatch();
类结构图
桥接模式(Bridge)
定义
桥接模式也称为桥梁模式,接口模式或柄体模式,是将首相部分与它的具体实现部分分离,是他们都可以独立的变化
倩姐模式主要目的是通过组合的方式建立两个类之间的联系,而不是继承,但有类似于多重继承方案,但是多重继承方案往往
违背了类的单一职责原则,器复用性比较差,掐季节模式是必多重继承更好的替代方案,
桥接模式的核心在于解耦抽象和实现 完全代替继承
遵循了里氏替换原则与依赖倒置原则,最终实现了开闭原则
倩姐模式主要目的是通过组合的方式建立两个类之间的联系,而不是继承,但有类似于多重继承方案,但是多重继承方案往往
违背了类的单一职责原则,器复用性比较差,掐季节模式是必多重继承更好的替代方案,
桥接模式的核心在于解耦抽象和实现 完全代替继承
遵循了里氏替换原则与依赖倒置原则,最终实现了开闭原则
理解
早上碰到MM,要说早上好,晚上碰到MM,要说晚上好;碰到MM穿了件新衣服,要说你的衣服好漂亮哦,碰到MM新做的发型,要说你的头发好漂亮哦。不要问我“早上碰到MM新做了个发型怎么说”这种问题,自己用BRIDGE组合一下不就行了
优缺点
优点:
1:分离抽象部分及具体实现部分
2:提高了系统的扩展性
3:符合开闭原则
4:符合合成复用原则
缺点:
1:增加了系统的理解与设计难度
2:需要正确地识别系统中的两个独立变化的维度
1:分离抽象部分及具体实现部分
2:提高了系统的扩展性
3:符合开闭原则
4:符合合成复用原则
缺点:
1:增加了系统的理解与设计难度
2:需要正确地识别系统中的两个独立变化的维度
源码中的应用
JDBC API 有个Driver 就是桥接模式,Class.forName()方法 driverManager 连接 数据库无关 ~ 与数据库有关
类结构图
组合模式(Composite)
定义
也称为整体—部分模式,它的宗旨是通过将单个对象(叶子节点)和组合对象(树枝节点)用相同的接口进行标示,
是的客户对单个对象和组合对象的使用具有一致性,
强调的是整体与部分的关系,他讲对象组织到树形结构中,最顶层的节点称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又包含树枝节点和叶子节点
是的客户对单个对象和组合对象的使用具有一致性,
强调的是整体与部分的关系,他讲对象组织到树形结构中,最顶层的节点称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又包含树枝节点和叶子节点
理解
COMPOSITE—Mary今天过生日。“我过生日,你要送我一件礼物。”“嗯,好吧,去商店,你自己挑。”“这件T恤挺漂亮,买,这条裙子好看,买,这个包也不错,买。”“喂,买了三件了呀,我只答应送一件礼物的哦。”“什么呀,T恤加裙子加包包,正好配成一套呀,小姐,麻烦你包起来。
与聚合模式的区别
1: 组合模式:在古代皇帝三宫六院,贵妃很多,但是每个贵妃只属于皇帝(具有相同的生命周期)
2:聚合关系: 一个老师有很多个学生,但是每一个学生又属于多个老师(具有不同的生命周期)
2:聚合关系: 一个老师有很多个学生,但是每一个学生又属于多个老师(具有不同的生命周期)
优缺点
1:清楚的定义分层次的复杂对象,表示对象的全部或部分层次
2:让客户端忽略了层次的差异,方便对整个层次结构进行控制
3:简化客户端代码
4:符合开闭原则
2:让客户端忽略了层次的差异,方便对整个层次结构进行控制
3:简化客户端代码
4:符合开闭原则
源码中的应用
1: HashMap putAll()
2:ArrayList addAll()
3:Mybatis 中的 Mapping sql 其中有个 sqlNode类
2:ArrayList addAll()
3:Mybatis 中的 Mapping sql 其中有个 sqlNode类
类结构图
装饰器模式(Decorator)
定义
装饰器模式以对客户端透明的方式扩展对象功能,是继承关系的一个替代方案,提供继承更多的灵活性,
动态给一个对象增加功能,这些功能可以在动态撤销,增加有一些基本功能的排列组合而产生的非常大的功能
动态给一个对象增加功能,这些功能可以在动态撤销,增加有一些基本功能的排列组合而产生的非常大的功能
理解
Mary过完轮到Sarly过生日,还是不要叫她自己挑了,不然这个月伙食费肯定玩完,拿出我去年在华山顶上照的照片,
在背面写上“最好的的礼物,就是爱你的Fita”,再到街上礼品店买了个像框(卖礼品的MM也很漂亮哦),再找隔壁搞美术设
计的Mike设计了一个漂亮的盒子装起来……,我们都是Decorator,最终都在修饰我这个人呀
在背面写上“最好的的礼物,就是爱你的Fita”,再到街上礼品店买了个像框(卖礼品的MM也很漂亮哦),再找隔壁搞美术设
计的Mike设计了一个漂亮的盒子装起来……,我们都是Decorator,最终都在修饰我这个人呀
优缺点
优点:装饰器是继承的有力补充,比继承还要灵活,不改变原有对象的情况下动态地给一个对象扩展功能,
即插即用,通过使用不同的装饰器自己这些装饰类的排列组合,尅实现不同的效果
缺点:会出现更多的代码,更多的类,增加程序的复杂性
动态装饰时,多层装饰时会更复杂
即插即用,通过使用不同的装饰器自己这些装饰类的排列组合,尅实现不同的效果
缺点:会出现更多的代码,更多的类,增加程序的复杂性
动态装饰时,多层装饰时会更复杂
源码中的应用
JDK中BUfferedReader, INputStream, OutputStream,
Spring中的 TransactionAwareCacheDecorator 主要是处理事务缓存的
myBatis 中 TransactionlCache
Spring中的 TransactionAwareCacheDecorator 主要是处理事务缓存的
myBatis 中 TransactionlCache
与代理模式的区别
代理模式 :强调对代理过程的控制,简单的理解,就是所有的事情都是在代理类中完成的,简单理解为一个人,或者平台干了
装饰器 :主要是每个类都去干了,平台上的每个人甚至与自己也算在里面都要去做
装饰器 :主要是每个类都去干了,平台上的每个人甚至与自己也算在里面都要去做
类结构图
门面模式(Facade)
定义
又叫做外观模式,提供了一个统一的接口,用来访问子系统中的一群接口,器主要特征是蒂尼了一个高层接口,让子系统更容易使用。
外部与一个子系统的通信必须通过一个统一的门面对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。每一个子系统只有一个门面类,
而且此门面类只有一个实例,也就是说它是一个单例模式。但整个系统可以有多个门面类
外部与一个子系统的通信必须通过一个统一的门面对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。每一个子系统只有一个门面类,
而且此门面类只有一个实例,也就是说它是一个单例模式。但整个系统可以有多个门面类
理解
我有一个专业的Nikon相机,我就喜欢自己手动调光圈、快门,这样照出来的照片才专业,但MM可不懂这些,教了半天也不会。幸好相机有Facade设计模式,把相机调整到自动档,只要对准目标按快门就行了,一切由相机自动调整,这样MM也可以用这个相机给我拍张照片了
优缺点
优点:
简化了调用过程,无需深入了解子系统,以防给子系统带来风险;
减少系统依赖,松散耦合
更好地规划访问层次,提高了安全性
遵循迪米特法则,即最少知道原则;
缺点:
当增加子系统和扩展子系统行为时,可能容易带来未知风险
不符合开闭原则
某些情况下可能违背单一职责原则;
简化了调用过程,无需深入了解子系统,以防给子系统带来风险;
减少系统依赖,松散耦合
更好地规划访问层次,提高了安全性
遵循迪米特法则,即最少知道原则;
缺点:
当增加子系统和扩展子系统行为时,可能容易带来未知风险
不符合开闭原则
某些情况下可能违背单一职责原则;
源码中的应用
1:Spring JDBC jdbcUtils 封装了和JDBC相关的所有操作,
2:MyBatis中的Configuration
3:Tomact 中RequestFacade 封装了非常多的request的操作
2:MyBatis中的Configuration
3:Tomact 中RequestFacade 封装了非常多的request的操作
类结构图
子主题
享元模式(Flyweight)
定义
称为轻量级模式,是对象池的一种实现,类似于线程池,线程池可以避免不停的创建和销毁多个对象,
消耗性能,提供了减少对象数量从而改善应用所需的对象结构的方式,其宗旨是共享细粒度对象,将多个同一对象的访问几种起来,不必为每个访问着创建一个单独的对象,以此来降低内存的消耗,
消耗性能,提供了减少对象数量从而改善应用所需的对象结构的方式,其宗旨是共享细粒度对象,将多个同一对象的访问几种起来,不必为每个访问着创建一个单独的对象,以此来降低内存的消耗,
理解
每天跟MM发短信,手指都累死了,最近买了个新手机,可以把一些常用的句子存在手机里,要用的时候,直接拿出来,在前面加上MM的名字就可以发送了,再不用一个字一个字敲了。共享的句子就是Flyweight,MM的名字就是提取出来的外部特征,根据上下文情况使用。
对象数量并节约内存的目的
状态区分: 1:内部状态 是不变化的,
2:外部状态:是变化的 通过共享不变的部分,达到减少对象数量并节约内存的目的
比如:连接池中的连接对象,保存在连接对象中的用户名,密码,连接信息URL,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态,而每个连接要会后利用时,我们需要给它标记为可用状态,这些为外部状态
优缺点 优点:1:减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率
2:外部状态:是变化的 通过共享不变的部分,达到减少对象数量并节约内存的目的
比如:连接池中的连接对象,保存在连接对象中的用户名,密码,连接信息URL,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态,而每个连接要会后利用时,我们需要给它标记为可用状态,这些为外部状态
优缺点 优点:1:减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率
优缺点
优点:1:减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率
2:减少内存之外的其他资源占用
缺点:1:关注内,外部状态,关注线程安全问题
2:使系统,程序的逻辑复杂化
2:减少内存之外的其他资源占用
缺点:1:关注内,外部状态,关注线程安全问题
2:使系统,程序的逻辑复杂化
源码中的应用
一般是有缓存的使用
Java 中 String final
Integer 中 目标值在 -128 ~ 127 之间
Long
Apache Commons Pool2 中的享元模式
Java 中 String final
Integer 中 目标值在 -128 ~ 127 之间
Long
Apache Commons Pool2 中的享元模式
类结构图
代理模式(Proxy)
定义
是指为其他对象提供一种代理,以控制对这个对象的访问,在某种情况下,
一个对象不适合或者不能直接引用另外一个对象,而代理对象可以在客户端和目标对象之前起到中介的作用
一个对象不适合或者不能直接引用另外一个对象,而代理对象可以在客户端和目标对象之前起到中介的作用
理解
跟MM在网上聊天,一开头总是“hi,你好”,“你从哪儿来呀?”“你多大了?”“身高多少呀?”这些话,
真烦人,写个程序做为我的Proxy吧,凡是接收到这些话都设置好了自动的回答,接收到其他的话时再通知我回答,
真烦人,写个程序做为我的Proxy吧,凡是接收到这些话都设置好了自动的回答,接收到其他的话时再通知我回答,
代理模式分类
静态代理:
类结构图
动态代理 :
类机构图
优缺点
1:代理模式能将代理对象与真实被调用目标对象分离
2:在一定程度上降低了系统的耦合性,扩展性好
3:可以起到保护目标对象的作用
4:可以增强目标对象的功能
缺点 ; 1;代理模式会造成系统设计中的类的数量增加
2:在客户端和目标对象中增加一个代理对象,会导致请求处理速度变慢
3:增加了系统的复杂性
2:在一定程度上降低了系统的耦合性,扩展性好
3:可以起到保护目标对象的作用
4:可以增强目标对象的功能
缺点 ; 1;代理模式会造成系统设计中的类的数量增加
2:在客户端和目标对象中增加一个代理对象,会导致请求处理速度变慢
3:增加了系统的复杂性
源码中的应用
基于SPring生态中 SpringAOP
行为型模式(11种)【委派模式不包含】
策略模式(Strategy)
定义
幼教政策模式,它是将定义的算法家族,分别封装起来,让他们之间可以互相替换,从而让算法的变化不会影响到使用算法的用户
策略模式使用的就是面向对象的继承和多态机制,从而实现同一行为在不同场景下具备不同的实现。主要解决if()else.switch()等
策略模式使用的就是面向对象的继承和多态机制,从而实现同一行为在不同场景下具备不同的实现。主要解决if()else.switch()等
理解
跟不同类型的MM约会,要用不同的策略,有的请电影比较好,有的则去吃小吃效果不错,
有的去海边浪漫最合适,单目的都是为了得到MM的芳心,追MM锦囊中有好多Strategy哦
有的去海边浪漫最合适,单目的都是为了得到MM的芳心,追MM锦囊中有好多Strategy哦
优缺点
1:符合开闭原则
2:避免使用多重条件转移语句,如if...else....语句,switch 语句
3:可以提高算法的保密性和安全性
缺点 : 1:客户端必须知道所有的策略,并且紫荆解决使用哪一个策略
2:代码中会产生非常多的策略类,增加维护难度
2:避免使用多重条件转移语句,如if...else....语句,switch 语句
3:可以提高算法的保密性和安全性
缺点 : 1:客户端必须知道所有的策略,并且紫荆解决使用哪一个策略
2:代码中会产生非常多的策略类,增加维护难度
源码中的应用
1:JDk中的Comparator Compare(),会传入Arrays, parallelSort
2:TreeMap 的构造方法
3:Spring中Resource的子类,有两种策略 SimplelnstantiationStrategy 与 CglibSubclassingInstantiationStrategy
2:TreeMap 的构造方法
3:Spring中Resource的子类,有两种策略 SimplelnstantiationStrategy 与 CglibSubclassingInstantiationStrategy
类结构图
子主题
模板方法模式(Template Method)
定义
是指定义一个操作中的算法的框架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义算法的某些特定步骤,
固定一个模板。如果要修改就让他的子类去实现,去修改 ,但是模板
固定一个模板。如果要修改就让他的子类去实现,去修改 ,但是模板
理解
准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先制定一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。
优缺点
1:利用模板方法将相同处理逻辑的代码放到抽象父类中,可以提高代码的复用性
2:将不同的代码放到不同的子类中,通过对子类的扩展增加新的行为,提高代码的扩展性
3:吧不变的行为写在父类上,去除子类的重复代码,提供了一个很好的代码复用平台,符合开闭原则
缺点: 1:类数据的增加,每一个手续类都需要一个子类来实现,导致类的个数增加
2:类的数量增加,见解的增加了系统实现的复杂度
3:继承关系自身缺点,如果父类添加新的抽象方法,所有的子类都要改一遍
2:将不同的代码放到不同的子类中,通过对子类的扩展增加新的行为,提高代码的扩展性
3:吧不变的行为写在父类上,去除子类的重复代码,提供了一个很好的代码复用平台,符合开闭原则
缺点: 1:类数据的增加,每一个手续类都需要一个子类来实现,导致类的个数增加
2:类的数量增加,见解的增加了系统实现的复杂度
3:继承关系自身缺点,如果父类添加新的抽象方法,所有的子类都要改一遍
源码中的应用
mybatis中 的连接信息
类结构图
子主题
责任链模式(Chain of Responsibility)
定义
是将链中的没一个节点看作是一个对象,每个节点处理的请求均不同,且内部自动维护一个下一节点对象,当一个请求从链式的首段发出时,会沿着链的路径依次传递给没一个节点对象,直至有对象处理这个请求为止
理解
晚上去上英语课,为了好开溜坐到了最后一排,哇,前面坐了好几个漂亮的MM哎,找张纸条,写上“Hi,可以做我的女朋友吗?如果不愿意请向前传”,
纸条就一个接一个的传上去了,糟糕,传到第一排的MM把纸条传给老师了,听说是个老处女呀,快跑!
纸条就一个接一个的传上去了,糟糕,传到第一排的MM把纸条传给老师了,听说是个老处女呀,快跑!
优缺点
1:将请求与处理解耦
2:请求处理这只需关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,直接转发给下一个节点
3:具备链式传递请求功能,请求发送者无需知晓链路结构,只需要等待请求处理结果
4:链路结构灵活,可以通过改变链路结构动态地新增或者删减责任
5:易于扩展新的请求处理类,符合开闭原则
缺点:1:责任链太长或者处理时间过长,会影响整体性能
2:如果节点对象存在循环引用时,或造成死循环,导致系统崩溃
2:请求处理这只需关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,直接转发给下一个节点
3:具备链式传递请求功能,请求发送者无需知晓链路结构,只需要等待请求处理结果
4:链路结构灵活,可以通过改变链路结构动态地新增或者删减责任
5:易于扩展新的请求处理类,符合开闭原则
缺点:1:责任链太长或者处理时间过长,会影响整体性能
2:如果节点对象存在循环引用时,或造成死循环,导致系统崩溃
应用场景:
源码中的应用
1:JDK j2ee Filter
2:netty 中的Pipeline ChannelHandler
2:netty 中的Pipeline ChannelHandler
类结构图
命令模式(Command)
定义
命令模式把一个请求或者操作封装到一个对象中。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。命令模式允许请求的一方和发送的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否执行,何时被执行以及是怎么被执行的。系统支持命令的撤消。
理解
俺有一个MM家里管得特别严,没法见面,只好借助于她弟弟在我们俩之间传送信息,她对我有什么指示,就写一张纸条让她弟弟带给我。这不,她弟弟又传送过来一个COMMAND,为了感谢他,我请他吃了碗杂酱面,哪知道他说:“我同时给我姐姐三个男朋友送COMMAND,就数你最小气,才请我吃面 -> 叫你干啥就干啥
优缺点
1:通过引入中间件(抽象接口)姐耦了命令请求与实现;
2:扩展性能好,可以很容易的增减新命令
3:支持组合命令,支持命令队列
4:可以在现有命令的基础上,增加额外功能
2:扩展性能好,可以很容易的增减新命令
3:支持组合命令,支持命令队列
4:可以在现有命令的基础上,增加额外功能
源码中的应用
1: JDK 中的Runnable 接口, 实际上Runnable 就相当于是命令的抽象,只要是实现了Runnable接口的类都被认为是一个线程
2:junit.framework.Test接口 Test 接口中有两个方法,第一个是countTestCases()方法用来统计当前需要执行的测试用例总数
2:junit.framework.Test接口 Test 接口中有两个方法,第一个是countTestCases()方法用来统计当前需要执行的测试用例总数
类结构图
迭代器模式(Iterator)
定义
他提供一直顺序访问集合/容器对象元素的方法,而又无序暴露集合内部表示,迭代器模式可以为不同的容器提供一致的遍历行为,
而不尬不系容器内容元素组成结构,本质是抽离集合迭代行为到迭代器中,提供一致访问接口
而不尬不系容器内容元素组成结构,本质是抽离集合迭代行为到迭代器中,提供一致访问接口
理解
按照顺序执行
优缺点
1:多态迭代:为不同的聚合接口提供一个的遍历接口,即一个迭代接口可以访问不同的集合对象
2:简化集合对象接口:迭代器模式将集合对象本身应该提供的元素迭代接口收取到了迭代器中,使集合对象无须关心具体迭代行为
3:元素迭代功能多样化:每个集合对象都可以提供一个或多个不同的迭代器,使的同种元素聚合结合可以有不同的迭代行为
4:解耦迭代与集合:迭代器模式封装了具体的迭代算法,迭代算法的变化,不会影响到集合对象的架构;
缺点:1: 对于简单的遍历,使用迭代器方式遍历变得复杂繁琐
2:日常开发中不需要写迭代器,除非我们自己定制实现的数据结构对应的迭代器,
2:简化集合对象接口:迭代器模式将集合对象本身应该提供的元素迭代接口收取到了迭代器中,使集合对象无须关心具体迭代行为
3:元素迭代功能多样化:每个集合对象都可以提供一个或多个不同的迭代器,使的同种元素聚合结合可以有不同的迭代行为
4:解耦迭代与集合:迭代器模式封装了具体的迭代算法,迭代算法的变化,不会影响到集合对象的架构;
缺点:1: 对于简单的遍历,使用迭代器方式遍历变得复杂繁琐
2:日常开发中不需要写迭代器,除非我们自己定制实现的数据结构对应的迭代器,
源码中的应用
1:JDK中的迭代源码实现 Iterator ;有两个主要的方法定义hasNext()和next()方法,
2:ArrayList中有个内部实现类 Iterator 也是有两个主要的方法 hasNext()和next()方法
2:ArrayList中有个内部实现类 Iterator 也是有两个主要的方法 hasNext()和next()方法
类结构图
备忘录模式(Memento)
定义
是指在不破坏封装的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态,这样以后就可将对象恢复到原先保存的状态,也就是说可以 从而可以在将来合适的时候把这个对象还原到存储起来的状态。
理解
同时跟几个MM聊天时,一定要记清楚刚才跟MM说了些什么话,不然MM发现了会不高兴的哦,幸亏我有个备忘录,刚才与哪个MM说了什么话我都拷贝一份放到备忘录里面保存,这样可以随时察看以前的记录啦
优缺点
优点:
1:简化发起人实体类职责,隔离状态存储与获取,实现了信息的封装,客户端无需关心状态的保存细节
2:提供状态回滚功能
缺点:
1:消耗资源
1:简化发起人实体类职责,隔离状态存储与获取,实现了信息的封装,客户端无需关心状态的保存细节
2:提供状态回滚功能
缺点:
1:消耗资源
源码中的应用
很少用到这个设计模式 Spring 中的 webflow StateManageableMessageContext接口
类结构图
观察者模式(Obserber)
定义
观察者模式定义了一种一队多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使他们能够自动更新自己。
理解
想知道咱们公司最新MM情报吗?加入公司的MM情报邮件组就行了,tom负责搜集情报,他发现的新情报不用一个一个通知我们,直接发布给邮件组,我们作为订阅者(观察者)就可以及时收到情报啦
优缺点
1:观察这和被观察这是松耦合的,符合依赖倒置原则
2:分离了表示层(观察者)和数据逻辑层(被观察者),并且建立了一套处方机制,使得数据的变化可以响应到多个表示层
3:实现了一对多的通讯机制,支持事件注册机制,支持兴趣分发机制,当被观察这触发事件时,只有感兴趣的观察者可以接受到通知
缺点:1: 如果观察者数量过多,则事件通知会号是较长、
2:如果存在依赖关系,可能导致循环调用,导致系统奔溃
3:事件通知呈线性关系,如果一个观察者卡壳,后续的时间都会有影响
2:分离了表示层(观察者)和数据逻辑层(被观察者),并且建立了一套处方机制,使得数据的变化可以响应到多个表示层
3:实现了一对多的通讯机制,支持事件注册机制,支持兴趣分发机制,当被观察这触发事件时,只有感兴趣的观察者可以接受到通知
缺点:1: 如果观察者数量过多,则事件通知会号是较长、
2:如果存在依赖关系,可能导致循环调用,导致系统奔溃
3:事件通知呈线性关系,如果一个观察者卡壳,后续的时间都会有影响
源码中的应用
ContextLoaderListener 实现了 ServletContextListener 接口,
类结构图
状态模式(State)
定义
是运行对象在内部状态发生改变改变他的行为,对象看起来好像修改了它的类,状态模式中的类的行为有状态决定,不同的状态下有不同的行为,其意图是让一个对象在其每部改变的时候,其行为也随之改变,状态模式的核心是状态与行为绑定,不同的状态对应不同的行为,
理解
跟MM交往时,一定要注意她的状态哦,在不同的状态时她的行为会有不同,比如你约她今天晚上去看电影,对你没兴趣的MM就会说“有事情啦”,对你不讨厌但还没喜欢上的MM就会说“好啊,不过可以带上我同事么?”,已经喜欢上你的MM就会说“几点钟?看完电影再去泡吧怎么样?”,当然你看电影过程中表现良好的话,也可以把MM的状态从不讨厌不喜欢变成喜欢哦
优缺点
1:结构清晰,将状态独立为类,消除了更多的if---else,使代码更加简洁,提高系统可维护性,跟策略模式的UML图几乎完全一样,但是有区别,场景不一样,策略的话是多种算法中选一种就能满足,彼此独立,用户可自行更换策略,状态的话,是各个状态之间是存在相互关系的,彼此之间在一定条件下存在自动切换状态效果,且用户无法指定状态,只能设置初始状态
2:将状态转换显示化,通常的对象每部都是使用数值类型来定义状态,状态的切换是通过复制进行表现,不够直观,而使用状态类,在切换状态时,是以不同的类进行表示。转换目的更加明确,
3:状态类职责明确且具备扩展性
缺点:1:类膨胀
2:状态模式结构与实现都较为复杂,
3:状态模式对开闭原则的支持并不太好
2:将状态转换显示化,通常的对象每部都是使用数值类型来定义状态,状态的切换是通过复制进行表现,不够直观,而使用状态类,在切换状态时,是以不同的类进行表示。转换目的更加明确,
3:状态类职责明确且具备扩展性
缺点:1:类膨胀
2:状态模式结构与实现都较为复杂,
3:状态模式对开闭原则的支持并不太好
源码中的应用
很少,一般是在业务中用到,状态顺序不能改变
类结构图
解释器模式(Interpreter)
定义
是指给定移门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用改表示来解释语言中的句子,是一种按照规定的预发(文法)进行解析的模式
理解
有一个《泡MM真经》,上面有各种泡MM的攻略,比如说去吃西餐的步骤、去看电影的方法等等,跟MM约会时,只要做一个Interpreter,照着上面的脚本执行就可以了。
优缺点
优点:
1:扩展性强:在解释器模式中由于预发是由很多类表示的,当预发规则更改时,只需修改相应的非终结者表达式即可,若扩展语法时,只需要添加相应非终结符类即可
2:增加了新的解释表达式的方法
3:易于实现文法
缺点: 预发规则较为复杂是,会引起类膨胀
执行效率比较低
1:扩展性强:在解释器模式中由于预发是由很多类表示的,当预发规则更改时,只需修改相应的非终结者表达式即可,若扩展语法时,只需要添加相应非终结符类即可
2:增加了新的解释表达式的方法
3:易于实现文法
缺点: 预发规则较为复杂是,会引起类膨胀
执行效率比较低
源码中的应用
JDK中 Pattern对正则表达式的编译和解析
类结构图
访问者模式Visitor
定义
是一直将数据结构与数据操作分离的设计模式,是指分装一些作用与某种数据结构中的各元素的操作,他可以在不改变数据结构的前提下定义作用于这些原色的新的操作,
理解
情人节到了,要给每个MM送一束鲜花和一张卡片,可是每个MM送的花都要针对她个人的特点,每张卡片也要根据个人的特点来挑,我一个人哪搞得清楚,还是找花店老板和礼品店老板做一下Visitor,让花店老板根据MM的特点选一束花,让礼品店老板也根据每个人特点选一张卡,这样就轻松多了
优缺点
1:解耦了数据结构与数据操作,使得操作集合可以独立变化
2:扩展性好,可以通过扩展访问这角色,实现对数据集的不同操作
3:元素具体类型并非单一,访问者均可操作
4:各角色职责分离,符合单一职责原则
缺点:1:无法增加元素类型
2:具体元素变得更困难
3:违背了依赖倒置原则
2:扩展性好,可以通过扩展访问这角色,实现对数据集的不同操作
3:元素具体类型并非单一,访问者均可操作
4:各角色职责分离,符合单一职责原则
缺点:1:无法增加元素类型
2:具体元素变得更困难
3:违背了依赖倒置原则
源码中的应用
JDK 的 NIO模块下的FileVisitor ,它的接口提供了递归遍历文件树的支持
类结构图
调解器模式->中介者模式(Mediator)
定义
用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示相互作用,从而使其耦合松散,而且可以独立改变他们之间的交互
理解
四个MM打麻将,相互之间谁应该给谁多少钱算不清楚了,幸亏当时我在旁边,按照各自的筹码数算钱,赚了钱的从我这里拿,赔了钱的也付给我,一切就OK啦,俺得到了四个MM的电话
优缺点
1:减少类之前的依赖,将多对多依赖转化成了一对多,降低了类之间的耦合性
2:类间各司其职,符合迪米特法则
缺点:中介者模式中将原本多个对象直接的相互的依赖变成了中介者和多个同事类的依赖关系,当同事类越多时,中介者就会越臃肿,变得复杂且难以维护
2:类间各司其职,符合迪米特法则
缺点:中介者模式中将原本多个对象直接的相互的依赖变成了中介者和多个同事类的依赖关系,当同事类越多时,中介者就会越臃肿,变得复杂且难以维护
源码中的应用
JDK 中的Timerl类中有很多的schedule()重载方法
类结构图
设计模式总结:
装饰者模式、命令模式、代理模式区别?
命令模式:叫你干啥就干啥
代理模式:叫你干啥你还干了点别的啥
装饰者模式:让你能多干点啥,但不马上干
命令模式:叫你干啥就干啥
代理模式:叫你干啥你还干了点别的啥
装饰者模式:让你能多干点啥,但不马上干
适配器模式:复用老接口,暴露新接口
状态模式:当前状态业务执行完,会流转到下一个状态
抽象工厂模式:创造一系列统一接口的对象工厂
原型模式:clone/创建一个一摸一样的对象,分深浅拷贝
建造者模式:创造一系列对象,通过他们最后构建成目标对象
模板模式:控制核心逻辑,扩展交给实现类
状态模式:当前状态业务执行完,会流转到下一个状态
抽象工厂模式:创造一系列统一接口的对象工厂
原型模式:clone/创建一个一摸一样的对象,分深浅拷贝
建造者模式:创造一系列对象,通过他们最后构建成目标对象
模板模式:控制核心逻辑,扩展交给实现类
服务器
F5
概念
将负载(工作任务)进行平衡、分摊到多个操作单元上进行执行,例如Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等,从而共同完成工作任务。
优点
能够直接通过智能交换机实现,处理能力更强,而且与系统无关,负载性能强,更适用于一大堆设备、大访问量、简单应用。
缺点
成本高,除设备价格高昂,而且配置冗余,很难想象后面服务器做一个集群,但最关键的负载均衡设备却是单点配置,无法有效掌握服务器及应用状态。
硬件负载均衡,一般都不管实际系统与应用的状态,而只是从网络层来判断,所以有时候系统处理能力已经不行了,但网络可能还来得及反应(这种情况非常典型,比如应用服务器后面内存已经占用很多,但还没有彻底不行,如果网络传输量不大就未必在网络层能反映出来)
nginx
Nginx是什么?
Nginx 是一个高性能的Http 和反向代理服务器,特点是 占有内存少,并发能力强,事实上 nginx 的并发能力确实在同类型的网页服务器中表现较好,例如 百度,京东,阿里,腾讯都在用
在哪下
http://nginx.org/en/download.html
怎么玩
1),安装nginx运行环境
yum install -y gcc-c++ pcre pcre-devel zlib zlib-devel openssl openssl-devel
2),判断 pcre 是否安装成功
pcre-config --version
3),解压nginx ,然后进入目录执行 ./configure
4),编译安装 make && make install
5),在 /user/local/nginx/sbin 有启动nginx的启动脚本
在 /user/local/nginx/sbin (常见命令)
启动 ./nginx
快速停止 ./nginx -s stop
重新加载
./nginx -s reload
查看nginx版本号 ./nginx -v
快速停止 ./nginx -s stop
重新加载
./nginx -s reload
查看nginx版本号 ./nginx -v
注意 : 需要防火墙开放 80 端口
nginx.conf配置文件
全局快
从配置文件开始到 events 块之间内容,主要设置了一些影响nginx 服务器整体运行的配置指令
主要包括配置运行 Nginx 服务器的用户(组),允许生成的 worker process 数,进程PID 存放路径,日志存放路径和类似以及配置文件的引入等。
worker_processes 1;
这是Nginx服务器并发处理服务相关的关键配置, worker processes 值越大,可以支持的并发处理量也越多,但是受到硬件,软件的制约
主要包括配置运行 Nginx 服务器的用户(组),允许生成的 worker process 数,进程PID 存放路径,日志存放路径和类似以及配置文件的引入等。
worker_processes 1;
这是Nginx服务器并发处理服务相关的关键配置, worker processes 值越大,可以支持的并发处理量也越多,但是受到硬件,软件的制约
events块
涉及的指令主要影响 Nginx 服务器与用户的网络连接,常用的设置包括是否开启对多 work process 下的网络连接进行序列化
是否允许同时接受多个网络连接,选取哪种事件驱动模型来处理连接请求,每个 work process 可以同时支持的最大连接数等。
worker_connections 1024;
支持的最大连接数
是否允许同时接受多个网络连接,选取哪种事件驱动模型来处理连接请求,每个 work process 可以同时支持的最大连接数等。
worker_connections 1024;
支持的最大连接数
http
http 全局块
这块和虚拟主机有密切关系,虚拟主机从用户角度看,和一台独立的硬件主机是完全一样的,该技术的产生是为了节省互联网服务器硬件成本。
每个http块可以包含多个server块,而每个server块相当于一个虚拟主机。
而每个server 块也分为全局 server块,以及可以同时包含多个 locaton 块
每个http块可以包含多个server块,而每个server块相当于一个虚拟主机。
而每个server 块也分为全局 server块,以及可以同时包含多个 locaton 块
server 块
Nginx 原理
master--只有一个
worker--可以有多个work
这样有什么好处呢?
1), 每个worker 都是一个独立的进程,不需要加锁,省掉了锁带来的开销,如果其中一个worker 出现了问题,其它worker独立的继续争抢,实现请求过程,不会造成服务中断
2),可以使用 nginx -s reload 热部署,利用nginx 进行热部署操作
3), 设置多少个worker 才是合适的
Nginx 和Redis 类似都采用了 io的多路复用机制,每个 worker 都是一个独立的进程,但每个进程里只有一个主线程
通过异步非阻塞的方式来处理请求,即便上千万请求也不在话下,每个worker的线程可以把cpu的性能发挥到机制
所以 worker 数和服务器的cpu 数相等 最为适宜,设置少了会浪费cpu 设置多了会造成cpu频繁切换上下文带来的消耗
通过异步非阻塞的方式来处理请求,即便上千万请求也不在话下,每个worker的线程可以把cpu的性能发挥到机制
所以 worker 数和服务器的cpu 数相等 最为适宜,设置少了会浪费cpu 设置多了会造成cpu频繁切换上下文带来的消耗
代码设置 worker
4),连接数 worker_connection
发送一个请求占用了 worker 的几个连接数?
2 个或者 4个
Nginx 有一个master 有4个worker ,每个worker 支持最大连接数是1024,支持的最大并发数是多少?
worker的最大连接数是 4*1024 /2
worker的最大连接数是 4*1024 /4
2 个或者 4个
Nginx 有一个master 有4个worker ,每个worker 支持最大连接数是1024,支持的最大并发数是多少?
worker的最大连接数是 4*1024 /2
worker的最大连接数是 4*1024 /4
Nginx 作为web服务器
Nginx 可以作为静态页面的 web服务器,同时还支持 CGI 协议的动态语言,比如 perl,php等,但不支持java
Java程序只能通过与tomcat 配合完成,Nginx 专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率,能经受负载的考验,有报告表名能支持高并发达到 50000 个并发连接数
Java程序只能通过与tomcat 配合完成,Nginx 专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率,能经受负载的考验,有报告表名能支持高并发达到 50000 个并发连接数
反向代理
什么是 正向代理?
在客户端(浏览器)配置代理服务器,通过代理服务器进行互联网访问
什么是反向代理?
其实客户端对代理时无感知的,因为客户端不需要任何配置就可以访问
我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器的地址,隐藏了真实的服务器IP地址
我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器的地址,隐藏了真实的服务器IP地址
nginx 实现反向代理1
需求1: 本地通过访问www.123.com 通过nginx的80端口跳转到 服务器8080端口的tomcat
1),修改本地host文件 把 www.123.com域名指向 服务器ip地址
在host文件添加 192.168.247.110 www.123.com
修改 nginx配置文件 nginx.conf文件
server {
listen 80;
server_name 192.168.247.110;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
proxy_pass http://127.0.0.1:8080;
index index.html index.htm;
}
重新启动nginx 访问 www.123.com成功可以访问
1),修改本地host文件 把 www.123.com域名指向 服务器ip地址
在host文件添加 192.168.247.110 www.123.com
修改 nginx配置文件 nginx.conf文件
server {
listen 80;
server_name 192.168.247.110;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
proxy_pass http://127.0.0.1:8080;
index index.html index.htm;
}
重新启动nginx 访问 www.123.com成功可以访问
nginx 实现反向代理2
需求: 访问127.0.0.1:9001/edu/ 直接跳转到 127.0.0.1:8080 访问 127.0.0.1:9001/vod/ 直接跳转到 127.0.0.1:8081
1), 修改 nginx.conf 配置文件
server {
listen 9001;
server_name 192.168.247.110;
location ~ /edu/ {
proxy_pass http://127.0.0.1:8080;
}
location ~ /vod/{
proxy_pass http://127.0.0.1:8081;
}
}
2),服务器开放 9001 端口 ,然后通过 ip:9001/vod/a.html 进行访问
1), 修改 nginx.conf 配置文件
server {
listen 9001;
server_name 192.168.247.110;
location ~ /edu/ {
proxy_pass http://127.0.0.1:8080;
}
location ~ /vod/{
proxy_pass http://127.0.0.1:8081;
}
}
2),服务器开放 9001 端口 ,然后通过 ip:9001/vod/a.html 进行访问
正则表达式
负载均衡
是什么
单个服务器解决不了高并发的请求,我们可以增加服务器的数量,然后将请求分发到各个服务器上,将原先请求集中到单个服务器上的情况改为分发到多个服务器上,将负载分发到不同服务器,也就是我们所说的负载均衡
nginx 怎么做
需求: 浏览器输入 ip/edu/a.html 负载均衡平分 8080 和 8081 端口(其中俩个tomcat 都有edu/a.html)
修改nginx.conf配置文件
http块添加
upstream myserver {
[ip_hash;]
server 192.168.247.110:8080 [weight=5];
server 192.168.247.110:8081 [weight=10];
[fair;]
}
server块添加监听
server {
listen 9002;
server_name 192.168.247.110;
location / {
proxy_pass http://myserver;
}
}
修改nginx.conf配置文件
http块添加
upstream myserver {
[ip_hash;]
server 192.168.247.110:8080 [weight=5];
server 192.168.247.110:8081 [weight=10];
[fair;]
}
server块添加监听
server {
listen 9002;
server_name 192.168.247.110;
location / {
proxy_pass http://myserver;
}
}
nginx分配服务器的策略
轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除
weight
weight weight 代表权重,权重默认值为1,权重越高被分配的客户端越多
ip_hash
每个请求访问ip的hash结果进行分配,这样每个访客固定访问一个后端服务器,可以解决sesson共享问题
fair(第三方)
按后端服务器的响应时间来分配请求,响应时间越短的优先分配
动静分离
是什么
为了加快网站的解析速度,可以把动态页面和静态页面由不同的服务器来解析,加快解析速度,降低原来单个服务器的压力
nginx 怎么做
前提条件
nginx.conf 配置
#动静分离
server {
listen 9003;
server_name 192.168.247.110;
location /html/ {
root /home/data/;
index index.html index.html;
}
location /images/ {
root /home/data/;
#是否在浏览器列出整个文件夹
autoindex on;
}
}
server {
listen 9003;
server_name 192.168.247.110;
location /html/ {
root /home/data/;
index index.html index.html;
}
location /images/ {
root /home/data/;
#是否在浏览器列出整个文件夹
autoindex on;
}
}
Nginx高可用的集群
问题 : 当一个用户发送一个请求,若nginx宕机了,则用户的请求无法发送至tomcat服务器
前提要求:
1),需要2台nginx服务器
2), 需要keepalived
3),虚拟ip地址
2), 需要keepalived
3),虚拟ip地址
怎么玩
在2台服务器安装 keepalived
官网 https://www.keepalived.org/download.html
手动安装
进行解压目录
./configure --prefix=/usr/local/keepalived
make && make install
启动/usr/local/keepalived/sbin/
./keepalived -f /usr/local/keepalived/etc/keepalived/keepalived.conf
安装位置 /usr/local/keepalived/etc/keepalived/keepalived.conf
./configure --prefix=/usr/local/keepalived
make && make install
启动/usr/local/keepalived/sbin/
./keepalived -f /usr/local/keepalived/etc/keepalived/keepalived.conf
安装位置 /usr/local/keepalived/etc/keepalived/keepalived.conf
自动安装
yum install keepalived -y
检查是否安装成功
rpm -q -a keepalived
安装位置 /etc/keepalived/keepalived.conf
检查是否安装成功
rpm -q -a keepalived
安装位置 /etc/keepalived/keepalived.conf
修改 keepalived.conf 配置文件
global_defs {
notification_email {
acassen@firewall.loc
failover@firewall.loc
sysadmin@firewall.loc
}
notification_email_from Alexandre.Cassen@firewall.loc
smtp_ server 192.168.17.129
smtp_connect_timeout 30
router_id LVS_DEVEL # LVS_DEVEL这字段在/etc/hosts文件中看;通过它访问到主机
}
vrrp_script chk_http_ port {
script "/usr/local/src/nginx_check.sh"
interval 2 # (检测脚本执行的间隔)2s
weight 2 #权重,如果这个脚本检测为真,服务器权重+2
}
vrrp_instance VI_1 {
state BACKUP # 备份服务器上将MASTER 改为BACKUP
interface ens33 //网卡名称 通过 ap addr命令来查看
virtual_router_id 51 # 主、备机的virtual_router_id必须相同
priority 100 #主、备机取不同的优先级,主机值较大(100),备份机值较小(90)
advert_int 1 #每隔1s发送一次心跳
authentication { # 校验方式, 类型是密码,密码1111
auth type PASS
auth pass 1111
}
virtual_ipaddress { # 虛拟ip
192.168.17.50 // VRRP H虛拟ip地址
}
}
notification_email {
acassen@firewall.loc
failover@firewall.loc
sysadmin@firewall.loc
}
notification_email_from Alexandre.Cassen@firewall.loc
smtp_ server 192.168.17.129
smtp_connect_timeout 30
router_id LVS_DEVEL # LVS_DEVEL这字段在/etc/hosts文件中看;通过它访问到主机
}
vrrp_script chk_http_ port {
script "/usr/local/src/nginx_check.sh"
interval 2 # (检测脚本执行的间隔)2s
weight 2 #权重,如果这个脚本检测为真,服务器权重+2
}
vrrp_instance VI_1 {
state BACKUP # 备份服务器上将MASTER 改为BACKUP
interface ens33 //网卡名称 通过 ap addr命令来查看
virtual_router_id 51 # 主、备机的virtual_router_id必须相同
priority 100 #主、备机取不同的优先级,主机值较大(100),备份机值较小(90)
advert_int 1 #每隔1s发送一次心跳
authentication { # 校验方式, 类型是密码,密码1111
auth type PASS
auth pass 1111
}
virtual_ipaddress { # 虛拟ip
192.168.17.50 // VRRP H虛拟ip地址
}
}
添加脚本文件在 /usr/local/src/nginx_check.sh
#! /bin/bash
A=`ps -C nginx -no-header | wc - 1`
if [ $A -eq 0];then
/usr/local/nginx/sbin/nginx
sleep 2
if [`ps -C nginx --no-header| wc -1` -eq 0 ];then
killall keepalived
fi
fi
可以通过查看 ap addr 的ens33 里面查找到 端口为 17.50的是否绑定到 ens33
把主机nginx 开启,然后在开启 主机的keepalived 从机也开启 nginx 然后在开启 keepalivd
访问 192.168.17.50 如果主机的nginx 和 keepalivd 关闭 ,则从机默认替换主机
A=`ps -C nginx -no-header | wc - 1`
if [ $A -eq 0];then
/usr/local/nginx/sbin/nginx
sleep 2
if [`ps -C nginx --no-header| wc -1` -eq 0 ];then
killall keepalived
fi
fi
可以通过查看 ap addr 的ens33 里面查找到 端口为 17.50的是否绑定到 ens33
把主机nginx 开启,然后在开启 主机的keepalived 从机也开启 nginx 然后在开启 keepalivd
访问 192.168.17.50 如果主机的nginx 和 keepalivd 关闭 ,则从机默认替换主机
可以通过查看 ap addr 的ens33 里面查找到 端口为 17.50的是否绑定到 ens33
把主机nginx 开启,然后在开启 主机的keepalived 从机也开启 nginx 然后在开启 keepalivd
访问 192.168.17.50 如果主机的nginx 和 keepalivd 关闭 ,则从机默认替换主机
0 条评论
下一页