This commit is contained in:
Dragon
2020-10-19 20:26:04 +08:00
parent 0bbdc1d9d2
commit a6fff83b0d
15 changed files with 16288 additions and 2 deletions

0
.nojekyll Normal file
View File

View File

@@ -1,2 +1,90 @@
# JavaYouth
主要是Java方面面试的知识如并发源码AQS源码mysql常考面试redsi,mq等等
- [Java](#java)
- [基础](#基础)
- [容器](#容器)
- [并发](#并发)
- [JVM](#JVM)
- [各版本新特性](#各版本新特性)
- [计算机网络](#计算机网络)
- [ElasticSearch](#ElasticSearch)
# Java
## 基础
1、总结【TODO】
**重难点**
1、[泛型详解【万字长文】](docs/Java/Basis/keyAndDifficultPoints/Generic/泛型.md)
## 容器
1、HashMap源码讲解JDK7和JDK8【TODO】
2、ConcurrentHashMap源码讲解JDK7和JDK8【TODO】
## 并发
> 这个系列全是万字长文,希望读者可以耐心看下去,相信会有很大收获。
1、[Java并发体系-第一阶段-多线程基础知识](docs/Java/concurrency/Java并发体系-第一阶段-多线程基础知识.md)
2、[Java并发体系-第二阶段-锁与同步-[1]](docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[1].md)
3、[Java并发体系-第二阶段-锁与同步-[2]](docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[2].md)
4、[Java并发体系-第二阶段-锁与同步-[3]](docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[3].md)
5、[Java并发体系-第三阶段-JUC并发包-[1]](docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[1].md)
6、[Java并发体系-第三阶段-JUC并发包-[2]](docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[2].md)
AQS阻塞队列源码还在准备中。预计12月前可以写的差不多
## JVM
【TODO】
## 各版本新特性
1、[Java8新特性](docs/Java/Basis/Java8_New_Features/Java8新特性.md)
# 计算机网络
**总结篇**
1、[计算机网络-总结-秋招篇](docs/Computer_NetWork/计算机网络-总结.md)
# ElasticSearch
## 用法
1、[ElasticSearch-入门](docs/ElasticSearch/usage/ElasticSearch-入门.md)
2、[ElasticSearch-进阶](docs/ElasticSearch/usage/ElasticSearch-进阶.md)

13
_coverpage.md Normal file
View File

@@ -0,0 +1,13 @@
<p align="center">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/favicon.png" width="200" height="200"/>
</p>
<h1 align="center">JavaYouth</h1>
[我的博客](https://youthlql.gitee.io/)
[GitHub](<https://github.com/youthlql/JavaYouth>)
[开始阅读](#Java)

View File

@@ -0,0 +1,923 @@
# 备注
1、打【】的是有印象能大致说出来即可但是没打【】的也不要背理解性记忆。
2、如果有知识点有的博客讲的很清楚我就直接贴链接了。
3、《计算机网络7》是一本很好的书讲的很详细而且不难懂读者有时间的话建议看一下。
4、如果有错误欢迎在评论区指正
> 1、本人正在准备秋招秋招完之后会持续更新博客。这些总结的部分也是准备面试期间看了很多很多博客写下来的。
> 2、本人博客https://youthlql.gitee.io/
> 3、等忙完秋招会陆续更新一些内容。喜欢的朋友可以收藏一下博客
>
# 补充
这里面的就是讲的比较详细的博客
## 关于计网比较好的博客
https://blog.csdn.net/ThinkWon/article/details/104903925
https://www.cnblogs.com/xjtu-lyh/p/12439036.html
## 三次挥手,四次握手
https://blog.csdn.net/qzcsu/article/details/72861891
## DNS解析
https://blog.csdn.net/weixin_40470303/article/details/80642190
## http1.0http1.1http2.0介绍
https://segmentfault.com/a/1190000016656529
## https建立链接过程
https://blog.csdn.net/iispring/article/details/51615631
## IO的一些博客
https://blog.csdn.net/sehanlingfeng/article/details/78920423
https://www.cnblogs.com/felixzh/p/10345929.html
# OSI七层模型与TCP/IP 五层模型
## 物理层
物理层考虑的是怎样才能在连接各种计算机的传输介质上传输数据比特流。现有的计算机网络中的硬件设备和传输媒体(介质)的种类非常多,而通信手段也有许多不同方式。物理层的作用正是要尽可能地屏蔽掉这些传输媒体和硬件设备的差异,使物理层上面的数据链路层感觉不到这些差异,这样就可使数据链路层只考虑如何完成本层的协议和服务,而不必考虑网络具体的传输媒体和通信手段是什么。
> 参考《计算机网络7》P51
## 数据链路层
### 概念
数据链路层研究的是分组怎样从一台主机传送到另一台主机,但并不经过路由器转发。从整个互联网来看,局域网仍属于数据链路层的范围。数据传送单位是帧。
**简单的过程:**
1、在两个相邻节点之间传送数据时数据链路层将网络层交下来的**IP 数据报**添加首部和尾部组装成帧****在两个相邻节点间的链路上传送帧**。**每一帧包括数据和必要的控制信息【如同步信息,地址信息,差错控制等】。
2、数据链路层在收到一个帧后通过控制信息检测收到的帧中是否有差错如果没有就可从中提出**IP数据报**部分,上交给网络层**。**如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以避免继续在网络中传送下去白白浪费网络资源 【在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。控制信息还使接收端能够检测到所收到的帧中有误差错。如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以避免继续在网络中传送下去白白浪费网络资源。如果需要改正数据在链路层传输时出现差错(这就是说,数据链路层不仅要检错,而且还要纠错),那么就要采用可靠性传输协议来纠正出现的差错。这种方法会使链路层的协议复杂些】
### 一些小细节
**数据链路层使用的信道主要有两种:**
①点对点信道
②广播信道
**三个基本问题:**
封装成帧、透明传输和差错检测。
> 更细致的看《计算机网络7》 P82
## 网络层
1、在计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点确保数据及时传送。网络层向上只提供简单灵活的、无连接的、尽最大努力交付的IP数据报服务。【其实数据报或IP数据报就是我们经常使用的“分组”】
2、网络层在发送分组时不需要先建立连接没有给分组进行上编号所传送的分组可能出错、丢失、重复和失序。如果主机即端系统中的进程之间的通信需要是可靠的那么就由网络的主机中的运输层负责包括差错处理、流量控制等
> 更多细节-->《计算机网络7》 P124
## 运输层
1、运输层的主要任务就是负责向两台主机的进程之间提供通用的数据传输服务。
2、运输层有一个很重要的功能 复用和分用。这里的“复用"是指在发送方不同的应用进程都可以使用同一个运输层协议传送数据(当然需要加上适当的首部),而“分用”是指接收方的运输层在剥去报文的首部后能够把这些数据正确交付目的应用进程
3、运输层向高层用户屏蔽了下面网络核心的细节【如网络拓扑、所采用的路由选择协议等】**它使应用进程看见的就是好像在两个运输层实体之间有一条端到端的逻辑通信信道**。但这条逻辑通信信道对上层的表现却因运输层使用的不同协议而有很大的差别。当运输层釆用面向连接的TCP协议时尽管下面的网络层是不可靠的只提供尽最大努力服务但这种逻辑通信信道就相当于一条全双工的可靠信道。但当运输层采用无连接的udp协议时这种逻辑通信信道仍然是一条不可靠信道。
## 应用层
(在上一章,我们巳学习了运输层为应用进程提供了端到端的通信服务)。
**1.** **不同的网络应用的应用进程之间,还需要有不同的通信规则。因此在运输层协议之上,还需要有应用层协议****。**
**2.** **每个应用层协议都是为了解决某一类应用问题,**(而问题的解决又必须通过位于不同主机中的多个应用进程之间的通信和协同工作来完成)**。应用进程之间的这种通信必须遵循严格的规则。应用层的具体内容就是精确定义这些通信规则。**
3.**运输层是两台主机间进程的交互。应用层是为了更加细化不同网络应用的交互规则。**
# 常见应用层协议和运输层、网络层协议
## 各层协议
**应用层:**HTTP(超文本传输协议) DNS(域名系统) FTP(文件传输协议) SMTP(简单邮件传送协议)
**运输层:**TCP UDP
**网络层:** IP, ARP(地址解析协议)--> 见《计网7》P134
## 硬件如路由器之类在哪一层
- 路由器在网络层,用来进行路由选择
# TCP与UDP区别和应用场景基于TCP的协议有哪些基于UDP的有哪些
## 区别+应用场景
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/computer_network/summary/0001.png" width=90%>
**总结:**
1、UDP的主要特点是
①无连接②尽最大努力交付③面向报文④无拥塞控制⑤支持一对一,一对多,多对一和多对多的交互通信⑥首部开销小(只有四个字段:源端口,目的端口,长度和检验和)
2、TCP的主要特点是
①面向连接②每一条TCP连接只能是一对一的③提供可靠交付④提供全双工通信⑤面向字节流
## 基于TCP的协议有哪些基于UDP的有哪些
**TCP**
HTTP, 超文本传输协议
FTP, 文件传输协议
SMTP,简单邮件传输协议,用来发送电子邮件
SSH 安全外壳协议,用于加密安全登陆
**UDP**
DHCP协议动态主机配置协议动态配置IP地址
NTP协议网络时间协议用于网络时间同步
RIP路由选择协议
DNS
# TCP可靠传输的保证拥塞控制目的和过程
## 如何保证可靠传输
TCP通过三次握手建立可靠连接
①数据被分割成 TCP 认为最适合发送的数据包。TCP 给发送的每一个包进行编号,接收方对数据包进行排序,将有序数据传送给应用层。**TCP通过序列号和确认应答提高可靠性**
②校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和目的是检测数据在传输过程中的任何变化。如果收到端的校验和有差错TCP 将丢弃这个报文段和不确认收到此报文段。
③流量控制: TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。
④拥塞控制: 当网络拥塞时,减少数据的发送。
⑤ARQ协议分为停止等待ARQ协议和连续ARQ协议
5.1 它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
5.2 超时重传::当 TCP 发出一个报文段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
5.3 TCP 的接收端会丢弃重复的数据
## ARQ
- 停止等待ARQ它的基本原理就是每发完一个分组就停止发送等待对方确认回复ACK。如果过了一段时间超时时间后还是没有收到 ACK 确认说明没有发送成功需要重新发送直到收到确认后再发下一个分组。停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认就重传前面发送过的分组认为刚才发送过的分组丢失了。因此每发送完一个分组需要设置一个超时计时器其重转时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为自动重传请求ARQ。另外在停止等待协议中若收到重复分组就丢弃该分组但同时还要发送确认。
- 连续ARQ协议(流水线的传输方式)可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。
## 拥塞控制
**目的:**
拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。
**过程:**
为了进行拥塞控制TCP 发送方要维持一个 拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。**慢开始和拥塞避免都是基于窗口的拥塞控制**。
**区别:**
> https://blog.csdn.net/ligupeng7929/article/details/79597423
* **慢开始:** 慢开始算法的思路是当主机开始发送数据时如果立即把大量数据字节注入到网络那么可能会引起网络阻塞因为现在还不知道网络的符合情况。经验表明较好的方法是先探测一下即由小到大逐渐增大发送窗口也就是由小到大逐渐增大拥塞窗口数值。cwnd初始值为1每经过一个传播轮次cwnd加倍。
* **拥塞避免:** 拥塞避免算法的思路是让拥塞窗口cwnd缓慢增大即每经过一个往返时间RTT就把发送放的cwnd加1.
- 为了防止拥塞窗口cwnd增长过大引起网络拥塞还需要设置一个慢开始门限ssthresh状态变量如何设置ssthresh。慢开始门限ssthresh的用法如下
当 cwnd < ssthresh 时,使用上述的慢开始算法。
当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。
- 拥塞避免算法让拥塞窗口cwnd缓慢地增大即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长比慢开始算法的拥塞窗口增长速率缓慢得多。
**快重传和快恢复:**
1、**快重传**算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时才进行捎带确认。
2、发送方知道现在只是丢失了个别的报文段。于是不启动慢开始而是执行**快恢复算法**。这时发送方调整门限值ssthresh= (cwnd/2=8,同时设置拥塞窗口cwnd = ssthresh=8 ,并开始执行拥塞避免算法。
3、请注意也有的快恢复实现是把快恢复开始时的拥塞窗口cwnd值再增大一一些(增大3个报文段的长度)即等于新的ssthresh + 3 x MSS。这样做的理由是:既然发送方收到3个重复的确认就表明有3个分组已经离开了网络。这3个分组不再消耗网络的资源而是停留在接收方的缓存中(接收方发送出3个重复的确认就证明了这个事实)。可见现在网络中并不是堆积了分组而是减少了3个分组。因此可以适当把拥塞窗口扩大些。
在采用快恢复算法时慢开始算法只是在TCP连接建立时和网络出现超时时才使用。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/computer_network/summary/0002.png" width=80%>
## 为什么要进行流量控制
一般来说,我们总是希望数据传输更快一些。但如果发送方把数据发送的过快,接收方就可能来不及接收,这就会造成数据的丢失(丢包)。
# TCP粘包现象原因和解决方法
## 原因
1.UDP协议的保护消息边界使得每一个消息都是独立的
2.而tcp是基于流的传输流传输却把数据当作一串数据流他不认为数据是一个一个的消息
3.发送端需要等缓冲区满才发送出去,造成粘包
4.接收方不及时接收缓冲区的包,造成多个包粘包
具体点:
1发送方引起的粘包是由TCP协议本身造成的TCP为提高传输效率发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少通常TCP会根据优化算法把这些数据合成一包后一次发送出去这样接收方就收到了粘包数据。
2接收方引起的粘包是由于接收方用户进程不及时接收数据从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区用户进程从该缓冲区取数据若下一包数据到达时前一包数据尚未被用户进程取走则下一包数据放到系统接收缓冲区时就接到前一包数据之后而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据这样就一次取到了多包数据。
## 解决方法
1对于发送方引起的粘包现象用户可通过编程设置来避免TCP提供了强制数据立即传送的操作指令pushTCP程序收到该操作指令后就立即将本段数据发送出去而不必等待发送缓冲区满
2对于接收方引起的粘包则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施使其及时接收数据从而尽量避免出现粘包现象
3由接收方控制将一包数据按结构字段人为控制分多次接收然后合并通过这种手段来避免粘包。
## 为什么粘包需要处理?
不是所有的粘包现象都需要处理,若传输的数据为不带结构的连续流数据(如文件传输),则不必把粘连的包分开(简称分包)。但在实际工程应用中,传输的数据一般为带结构的数据,这时就需要做分包处理。分包一般难度较大,所以尽量避免粘包
# 三次握手相关问题
## 过程+状态改变
把**补充**里面的**第二个博客**的过程背下来。(有的地方需要参考第一个博客)
## 为什么三次,两次为什么不行?
### 第一种答案
两次握手只能保证单向连接是畅通的。只有经过第三次握手,才能确保双向都可以接收到对方的发送的 数据。两次握手接收方这里不能确定自己的的发送是正常的,发送方的接收是正常的。
**具体点:**
**三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。**
第一次握手Client 什么都不能确认Server 确认了对方发送正常,自己接收正常
第二次握手Client 确认了自己发送、接收正常对方发送、接收正常Server 确认了:对方发送正常,自己接收正常
第三次握手Client 确认了自己发送、接收正常对方发送、接收正常Server 确认了:自己发送、接收正常,对方发送、接收正常
所以三次握手就能确认双发收发功能都正常,缺一不可。
### 第二种答案
一句话主要防止已经失效的连接请求报文突然又传送到了服务器从而产生错误。如果使用的是两次握手建立连接假设有这样一种场景客户端发送了第一个请求连接并且没有丢失只是因为在网络结点中滞留的时间太长了由于TCP的客户端迟迟没有收到确认报文以为服务器没有收到此时重新向服务器发送这条报文此后客户端和服务器经过两次握手完成连接传输数据然后关闭连接。此时此前滞留的那一次请求连接网络通畅了到达了服务器这个报文本该是失效的但是两次握手的机制将会让客户端和服务器再次建立连接这将导致不必要的错误和资源的浪费。如果采用的是三次握手就算是那一次失效的报文传送过来了服务端接受到了那条失效报文并且回复了确认报文但是客户端不会再次发出确认。由于服务器收不到确认就知道客户端并没有请求连接。
## 如果已经建立了连接,但是客户端突然出现故障了怎么办?
<https://blog.csdn.net/qzcsu/article/details/72861891>
# 四次挥手相关问题
## 过程及状态改变
就是把**补充**里面的**第二个博客**的过程背下来。(有的地方需要参考第一个博客)
## 为什么四次挥手
因为只有在客户端和服务端都没有数据要发送的时候才能断开TCP。而客户端发出FIN报文时只能保证客户端没有数据发了服务端还有没有数据发客户端是不知道的。而服务端收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端我服务端已经收到你的FIN报文了但我服务端还有一些数据没发完等这些数据发完了服务端才能给客户端发FIN报文(所以不能一次性将确认报文和FIN报文发给客户端就是这里多出来了一次)。
## CLOSE-WAIT和TIME-WAIT存在的意义
- Close-wait存在的意义: 就是服务端还有数据要发送,这个时间内就是服务端发送完最后的数据
- Time-wait存在的意义:
- 第一这里同样是要考虑丢包的问题如果第四次挥手的报文丢失服务端没收到确认ack报文就会重发第三次挥手的报文这样报文一去一回最长时间就是2MSL所以需要等这么长时间来确认服务端确实已经收到了
- 第二防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后在这个2MSL时间中就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
## 如何查看TIME-WAIT状态的链接数量(TODO)
## 为什么会TIME-WAIT过多解决方法是怎样的 (TODO)
## 为什么客户端最后还要等待2MSL
补充里的第二个博客
https://blog.csdn.net/qzcsu/article/details/72861891
# TCP
## 报文首部
<img src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC8xLzcvMTZmN2UwM2IxOWU2YzEzNA?x-oss-process=image/format,png">
**重要字段:**
1序号seq序号占32位用来标识从TCP源端向目的端发送的字节流发起方发送数据时对此进行标记。
2确认序号ack序号占32位只有ACK标志位为1时确认序号字段才有效ack=seq+1。
3标志位共6个即URG、ACK、PSH、RST、SYN、FIN等具体含义如下
ACK确认序号有效。
FIN释放一个连接。
PSH接收方应该尽快将这个报文交给应用层。
RST重置连接。
SYN发起一个新连接。
URG紧急指针urgent pointer有效。
**需要注意的是:**
不要将确认序号ack与标志位中的ACK搞混了。
确认方ack=发起方seq+1两端配对。
# 浏览器输入URL并回车的过程以及相关协议DNS查询过程。
## 过程
1、DNS域名解析得到IP地址
DNS解析流程:
> 参考https://blog.csdn.net/yanshuanche3765/article/details/82589210
>
> https://blog.csdn.net/u014465934/article/details/83241097
11. 浏览器先检查自身缓存中有没有被解析过的这个域名对应的ip地址(如果有解析结束。同时域名被缓存的时间也可通过TTL属性来设置。)
2在主机查询操作系统DNS缓存也就是hosts文件里配置的。
3如果浏览器和系统缓存都没有系统的 gethostname 函数就会像本地 DNS 服务器发送请求。而网络服务一般都会先经过路由器以及网络服务商(电信),所以会先查询路由器缓存,然后再查询 ISP 的 DNS 缓存。
4如果至此还没有命中域名才会真正的请求本地域名服务器LDNS来解析这个域名这台服务器一般在你的城市的某个角落距离你不会很远并且这台服务器的性能都很好一般都会缓存域名解析结果大约80%的域名解析到这里就完成了。
5如果LDNS仍然没有命中本地的DNS服务器向根域名服务器发送查询请求根域名服务器返回该域名的顶级级域名服务器。依次类推根域名服务器-顶级域名服务器-主域名服务器
2、解析出IP地址后根据IP地址和默认端口80和服务器建立连接发送http请求
3、服务器对浏览器的请求作出响应并把对应的html文本发送给浏览器
4、释放TCP连接四次挥手断开连接
6、浏览器解析该HTML文本并显示内容
## 用到的协议
- TCP:与服务器建立TCP连接
- IP: 建立TCP协议时需要发送数据发送数据在网络层使用IP协议
- OPSF: IP数据包在路由器之间路由选择使用OPSF协议
- ARP: 路由器在与服务器通信时需要将ip地址转换为MAC地址需要使用ARP协议
- HTTP:在TCP建立完成后使用HTTP协议访问网页
# HTTP1.0、1.1、2.0之间的区别
## TCP队头阻塞和HTTP队头阻塞
> http://www.mamicode.com/info-detail-2983775.html
**管道化请求的致命弱点**:
1、会造成队头阻塞前一个响应未及时返回后面的响应被阻塞
2、请求必须是幂等请求不能修改资源。因为意外中断时候客户端需要把未收到响应的请求重发非幂等请求会造成资源破坏。
3、由于这个原因目前大部分浏览器和Web服务器都关闭了管道化采用非管道化模式。无论是非管道化还是管道化都会造成队头阻塞(请求阻塞)。
**解决http队头阻塞的方法**
**1. 并发TCP连接**浏览器一个域名采用6-8个TCP连接并发HTTP请求
**2. 域名分片**多个域名可以建立更多的TCP连接从而提高HTTP请求的并发
**2. HTTP2方式**
http2使用一个域名单一TCP连接发送请求请求包被二进制分帧不同请求可以互相穿插避免了http层面的请求队头阻塞。
但是不能避免TCP层面的队头阻塞。
## http1.0
1.在HTTP/1.0中默认使用短连接。也就是说客户端和服务器每进行一次HTTP操作就建立一次连接任务结束就中断连接。
2.http队列头阻塞
请求队列的第一个请求因为服务器正忙(或请求格式问题等其他原因),导致后面的请求被阻塞
## http1.1
1、支持长链接
在request和response中的header中的connection是close或者Keep-Alive进行控制
一个TCP链接可以传送多个http请求和响应减少了TCP建立链接和关闭链接的消耗。
2、支持http管道(也就是流水线方式)
不使用管道的http请求在使用持久链接时必须严格满足先进先出的队列顺序FIFO即发送请求等待响应完成再发送客户端队列中的下一个请求。管道可以让我们把 FIFO 队列从客户端(请求队列)迁移到服务器(响应队列),即客户端可以并行,服务端串行。客户端可以不用等待前一个请求返回,发送请求,但服务器端必须顺序的返回客户端的请求响应结果。
但是http1.1任然无法解决http队头阻塞
**还有更细的就看补充里的第四个博客**
## http2.0
1.HTTP2.0最主要的特点是在不改动HTTP语义、方法、状态码、URI及首部字段的情况下大幅度提高了web性能。
**2.** **二进制传输**:大幅提高性能的一个原因就是: HTTP2.0中所有加强性能的核心是二进制传输在HTTP1.x中我们是通过文本的方式传输数据。基于文本的方式传输数据存在很多缺陷文本的表现形式有多样性因此要做到健壮性考虑的场景必然有很多但是二进制则不同只有0和1的组合因此选择了二进制传输实现方便且健壮。在HTTP2.0中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。
**3.** **多路复用**: HTTP2.0中有两个概念非常重要frame和流stream。帧是最小的数据单位每个帧会标识出该帧属于哪个流流是多个帧组成的数据流。所谓多路复用即在一个TCP连接中存在多个流即可以同时发送多个请求
对端可以通过帧中的表示知道该帧属于哪个请求。在客户端这些帧乱序发送到对端后再根据每个帧首部的流标识符重新组装【请求包被二进制分帧不同请求可以互相穿插】。通过该技术可以避免HTTP旧版本的队头阻塞问题极大提高传输性能
**4.** **Header压缩**
**5.** **服务器Push**: 在HTTP2.0中,服务端可以在客户端某个请求后,主动推送其他资源
**6.** **更安全**: HTTP2.0对tls的安全性做了近一步加强
# HTTP与HTTPS
## 区别
**1、端口**HTTP的URL由“http://”起始且默认使用端口80而HTTPS的URL由“https://”起始且默认使用端口443。
**2、安全性和资源消耗** HTTP协议运行在TCP之上所有传输的内容都是明文客户端和服务器端都无法验证对方的身份。HTTPS是运行在SSL/TLS之上的HTTP协议SSL/TLS 运行在TCP之上。所有传输的内容都经过加密加密采用对称加密但对称加密的密钥用服务器方的证书进行了非对称加密。所以说HTTP 安全性没有 HTTPS高但是 HTTPS 比HTTP耗费更多服务器资源
## HTTPS链接建立的过程
HTTPS在传输的过程中会涉及到三个密钥
- 服务器端的公钥和私钥,用来进行非对称加密
- 客户端生成的随机密钥,用来进行对称加密
一个HTTPS请求实际上包含了两次HTTP传输可以细分为8步。
1. 客户端向服务器发起HTTPS请求连接到服务器的443端口。
2. 服务器端有一个密钥对,即公钥和私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人。
3. 服务器将自己的公钥发送给客户端。
4. 客户端收到服务器端的公钥之后会对公钥进行检查验证其合法性如果发现发现公钥有问题那么HTTPS传输就无法继续。【严格的说这里应该是验证服务器发送的数字证书的合法性关于客户端如何验证数字证书的合法性下文会进行说明。】如果公钥合格那么客户端会生成一个随机值这个随机值就是用于进行对称加密的密钥我们将该密钥称之为client key即客户端密钥这样在概念上和服务器端的密钥容易进行区分。然后用服务器的公钥对客户端密钥进行非对称加密这样客户端密钥就变成密文了至此HTTPS中的第一次HTTP请求结束。
5. 客户端会发起HTTPS中的第二个HTTP请求将加密之后的客户端密钥发送给服务器。
6. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文。
7. 然后服务器将加密后的密文发送给客户端。
8. 客户端收到服务器发送来的密文用客户端密钥对其进行对称解密得到服务器发送的数据。这样HTTPS中的第二个HTTP请求结束整个HTTPS传输完成。
**详细就看补充-5博客**
## 了解对称加密算法和非对称加密算法不?
1.**对称加密**密钥只有一个加密解密为同一个密码且加解密速度快典型的对称加密算法有DES、AES等
**存在的问题:** 这种方式存在的最大问题就是密钥发送问题,即如何安全地将密钥发给对方,对称加密所使用的密钥我们可以通过非对称加密的方式发送出去。
**2.** **非对称加密**密钥成对出现且根据公钥无法推知私钥根据私钥也无法推知公钥加密解密使用不同密钥公钥加密需要私钥解密私钥加密需要公钥解密相对对称加密速度较慢典型的非对称加密算法有RSA、DSA等。
# HTTP请求有哪些。Post和get区别。
## 有哪些http请求
<https://www.runoob.com/http/http-methods.html>
## post和get的区别
1.GET提交的数据放在URL中POST数据放在Request body。(但这种情况仅限于浏览器发请求的场景)。这点意味着GET更不安全POST也不安全因为HTTP是明文传输抓包就能获取数据内容要想安全还得加密使用https
2.GET回退浏览器无害POST会再次提交请求GET方法回退后浏览器再缓存中拿结果POST每次都会创建新资源
3.GET提交的数据大小有限制是因为浏览器对URL的长度有限制GET本身没有限制POST没有
4.GET可以被保存为书签POST不可以。这一点也能感受到。这点可能没啥用
5.GET请求会被浏览器主动cache而POST不可缓存
6.对参数的数据类型GET只接受ASCII字符而POST没有限制。
7.GET会保存再浏览器历史记录中POST不会。这点也能感受到。
下面的就是深层次的
8.可以把 get 和 post 当作两个不同的行为,两者并没有什么本质区别,底层都是 TCP 连接
对于第1点的补充(如果需要举例子可以这样举)
> 其中的“<METHOD>"可以是GET也可以是POST或者其他的HTTP Method如PUT、DELETE、OPTION……。从协议本身看并没有什么限制说GET一定不能没有bodyPOST就一定不能把参放到<URL>的querystring上。因此其实可以更加自由的去利用格式。比如Elastic Search的_search api就用了带body的GET也可以自己开发接口让POST一半的参数放在url的querystring里另外一半放body里你甚至还可以让所有的参数都放Header里——可以做各种各样的定制只要请求的客户端和服务器端能够约定好
>
> 摘自---https://www.zhihu.com/question/28586791
# HTTP常见响应状态码从1xx到5xx都要说
<https://www.runoob.com/http/http-status-codes.html>
需要记住的(下面的应该足够了)
100, 101
200, 201, 202, 204
301, 302 , 304, 305
400, 401, 403, 404, 408
500, 502 ,504
# 重定向和转发区别
1、重定向是两次请求转发是一次请求。因此转发的速度要快于重定向
**重定向过程**:第一次客户端request一个网址,服务器响应并response回来告诉浏览器你应该去别一个网址。
2、重定向之后地址栏上的地址会发生变化变化成第二次请求的地址转发之后地址栏上的地址不会变化还是第一次请求的地址
3、转发是服务器行为重定向是客户端行为。
4、重定向时的网址可以是任何网址转发的网址必须是本站点的网址
# Session、Cookie和Token的主要区别
HTTP协议本身是无状态的。什么是无状态呢即服务器无法判断用户身份。
## 什么是cookie
cookie是由Web服务器保存在用户浏览器上的小文件key-value格式包含用户相关的信息。客户端向服务器发起请求如果服务器需要记录该用户状态就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie以此来辨认用户身份。
## 什么是session
session是依赖Cookie实现的。session是服务器端对象
session 是浏览器和服务器会话过程中服务器分配的一块储存空间。服务器默认为浏览器在cookie中设置 sessionid浏览器在向服务器请求过程中传输 cookie 包含 sessionid ,服务器根据 sessionid 获取出会话中存储的信息,然后确定会话的身份信息。
## cookie与session区别
存储位置与安全性cookie数据存放在客户端上安全性较差session数据放在服务器上安全性相对更高
存储空间单个cookie保存的数据不能超过4K很多浏览器都限制一个站点最多保存20个cookiesession无此限制
占用服务器资源session一定时间内保存在服务器上当访问增多占用服务器性能考虑到服务器性能方面应当使用cookie。
## 什么是Token
- Token的引入Token是在客户端频繁向服务端请求数据服务端频繁的去数据库查询用户名和密码并进行对比判断用户名和密码正确与否并作出相应提示在这样的背景下Token便应运而生。
- Token的定义Token是服务端生成的一串字符串以作客户端进行请求的一个令牌当第一次登录后服务器生成一个Token便将此Token返回给客户端以后客户端只需带上这个Token前来请求数据即可无需再次带上用户名和密码。
- 使用Token的目的Token的目的是为了减轻服务器的压力减少频繁的查询数据库使服务器更加健壮。
- Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位
- Token常用JWT实现原来需要客户端需要请求服务端验证session合法性现在只需要通过算法检验Token是否合法减少了查数据库或者访问服务器比对用户密码用户身份的消耗。也就是通过算法和密钥代替了访问数据库或服务器比对的过程
## session与token区别
- session机制存在服务器压力增大CSRF跨站伪造请求攻击扩展性不强等问题
- session存储在服务器端token存储在客户端
- token提供认证和授权功能作为身份认证token安全性比session好
- session这种会话存储方式方式只适用于客户端代码和服务端代码运行在同一台服务器上token适用于项目级的前后端分离前后端代码运行在不同的服务器下
# Reactor和Proactor区别
1.Reactor被动的等待指示事件的到来并做出反应它有一个等待的过程做什么都要先注册到监听事件集合中等待socket可读时再进行操作
2.Proactor直接调用异步读写操作调用完后放到到用户线程指定的缓存区接着通知用户线程直接使用即可。
3.Proactor是真正意义上的用于异步IO但是依赖操作系统对异步的支持。而Reactor用于同步IO
通俗语言:
reactor能收数据了你跟我说一声。
proactor: 这有十个字节数据,收好了跟我说一声。
> 想要理解这两设计模式,还是要真正的学习设计模式。
# 五种IO模型的区别
> 从下面这两个文章总结的:
>
> * https://blog.csdn.net/sehanlingfeng/article/details/78920423
> * http://www.tianshouzhi.com/api/tutorials/netty/221
>
> 下面总结的地方如果有不懂的,看上面的文章内容
>
> 下面这两篇文章看起来不错,不过还没认真看:
>
> * https://blog.csdn.net/ocean_fan/article/details/79622956
> * https://blog.csdn.net/ZWE7616175/article/details/80591587
在这里我们以一个网络IO来的read来举例它会涉及到两个东西一个是产生这个IO的进程另一个就是系统内核(kernel)。当一个read操作发生时它会经历两个阶段
**阶段1**等待数据准备
**阶段2**将数据从内核拷贝到进程中
## 阻塞IO
当用户进程进行recvfrom这个系统调用内核就开始了IO的第一个阶段等待数据准备。对于network io来说很多时候数据在一开始还没有到达比如还没有收到一个完整的UDP包这个时候**内核**就要等待足够的数据到来。而在用户进程这边,整 个进程会被阻塞。当**内核**一直等到数据准备好了,它就会将数据从**内核**中拷贝到用户内存,然后**内核**返回果,用户进程才解除 block的状态重新运行起来。**所以blocking IO的特点就是在IO执行的两个阶段都被block了。**
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/computer_network/summary/0003.png" width=80%>
<img src="http://images.cnitblog.com/blog/405877/201411/142330286789443.png" width=70%>
## 非阻塞IO
1. 当用户进程发出read操作时如果kernel中的数据还没有准备好那么它并不会block用户进程而是立刻返回一个error。
2. 从用户进程角度讲 它发起一个read操作后并不需要等待而是马上就得到了一个结果。用户进程判断结果是一个error时它就知道数据还没有准备好。用户线程需要不断地发起IO请求直到数据到达后才真正读取到数据继续执行。
3. 虽然用户线程每次发起IO请求后可以立即返回但是为了等到数据仍需要不断地轮询、重复请求消耗了大量的CPU的资源。一般很少直接使用这种模型而是在其他IO模型中使用非阻塞IO这一特性。
4. **所以,用户进程第一个阶段不是阻塞的,需要不断的主动询问内核数据好了没有;第二个阶段依然总是阻塞的。**
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/computer_network/summary/0004.png" width=80%>
<img src="http://images.cnitblog.com/blog/405877/201411/142332004602984.png" width=70%>
## 多路复用IO
1. IO多路复用模型是建立在内核提供的多路分离函数select基础之上的使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。利用了新的select系统调用由内核来负责本来是请求进程该做的轮询操作
2. 它的基本原理就是select /epoll这个函数会不断的轮询所负责的所有socket当某个socket有数据到达了就通知用户进程正式发起read请求。
3. 从流程上来看使用select函数进行IO请求和同步阻塞模型没有太大的区别甚至还多了添加监视socket以及调用select函数的额外操作效率更差。但是使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket然后不断地调用select读取被激活的socket(也就是数据准备好了的socket)即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中必须通过多线程的方式才能达到这个目的。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/computer_network/summary/0005.png" width=80%>
### select函数的其它好处
> handle_events实现事件循环
>
> handle_event进行读/写等操作
1. 使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求但是每个IO请求的过程还是阻塞的在select函数上阻塞平均时间甚至比同步阻塞IO模型还要长。
2. 如果用户线程只注册自己感兴趣的socket或者IO请求然后去做自己的事情等到数据到来时再进行处理则可以提高CPU的利用率。
3. IO多路复用模型使用了Reactor设计模式实现了这一机制。
4. 通过Reactor的方式可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作异步而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时(就是数据准备好的时候)则通知相应的用户线程或执行用户线程的回调函数执行handle_event进行数据读取、处理的工作。
5. 由于select函数是阻塞的因此多路IO复用模型也被称为异步阻塞IO模型。注意这里的所说的阻塞是指select函数执行时线程被阻塞而不是指socket。(一般在使用IO多路复用模型时socket都是设置为NONBLOCK的不过这并不会产生影响因为用户发起IO请求时数据已经到达了用户线程一定不会被阻塞。)
<img src="http://images.cnitblog.com/blog/405877/201411/142333254136604.png" width=80%>
## 信号驱动IO
1. 在信号驱动IO模型中当用户线程发起一个IO请求操作会给对应的socket注册一个信号函数然后用户线程会继续执行当内核数据就绪时会发送一个信号给用户线程用户线程接收到信号之后便在信号函数中调用IO读写操作来进行实际的IO请求操作。
2. 这个一般用于UDP中对TCP套接口几乎是没用的原因是该信号产生得过于频繁并且该信号的出现并没有告诉我们发生了什么事情
3. 信号驱动IO放佛很像异步IO它的第一阶段不是阻塞的。但是很遗憾它的数据拷贝阶段(第二阶段),任然是阻塞的。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/computer_network/summary/0006.png" width=80%>
## 异步IO
1. 真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中由用户线程自行读取数据、处理数据。
2. 而在异步IO模型中用户进程发起read操作之后立刻就可以开始去做其它的事。
3. 而另一方面,从**内核**的角度,当它受到一个异步读之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都 完成之后,**内核**会给用户进程发送一个信号告诉它read操作完成了用户线程直接使用即可。 在这整个过程中,进程完全没有被阻塞。
4. 异步IO模型使用了Proactor设计模式实现了这一机制。**(具体怎么搞得,看上面的文章链接)**
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/computer_network/summary/0007.png" width=80%>
<img src="http://images.cnitblog.com/blog/405877/201411/142333511475767.png">
# select、poll、epoll的区别
> select, poll, epoll 都是I/O多路复用的具体的实现之所以有这三个存在其实是他们出现是有先后顺序的。
>
> * https://blog.csdn.net/nanxiaotao/article/details/90612404
> * https://www.cnblogs.com/aspirant/p/9166944.html
> * https://www.zhihu.com/question/32163005
## select
1. 它仅仅知道了有I/O事件发生了却并不知道是哪那几个流可能有一个多个甚至全部只能无差别轮询所有流找出能读出数据或者写入数据的流对他们进行操作。所以**select具有O(n)的无差别轮询复杂度**,同时处理的流越多,无差别轮询时间就越长。
2. 单个进程可监视的fd_set(监听的端口个数)数量被限制32位机默认是1024个64位机默认是2048。
## poll
poll本质上和select没有区别采用**链表**的方式替换原有fd_set数据结构,而使其**没有连接数的限制**。
## epoll
1. epoll可以理解为event poll不同于忙轮询和无差别轮询epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动每个事件关联上fd此时我们对这些流的操作都是有意义的。复杂度降低到了O(1)
2. 效率提升不是轮询的方式不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数。即Epoll最大的优点就在于它只管你“活跃”的连接而跟连接总数无关因此在实际的网络环境中Epoll的效率就会远远高于select和poll。
3. epoll通过内核和用户空间共享一块内存来实现的。select和poll都是内核需要将消息传递到用户空间都需要内核拷贝动作
4. epoll有EPOLLLT和EPOLLET两种触发模式。(**暂时不去记,有个印象,大致是什么样就可以**)
# IO疑难点
https://blog.csdn.net/m0_38109046/article/details/89449305
https://www.zhihu.com/question/19732473
[漫画讲IO](https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41&scene=21#wechat_redirect)

View File

@@ -0,0 +1,652 @@
---
title: ElasticSearch-入门篇
tags:
- ElasticSearch
- ELK
- 全文检索
categories:
- ElasticSearch
- 用法
keywords: ElasticSearch,全文检索
description: ElasticSearch-入门篇,适合做入门,或者知识回顾。
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/ElasticSearch/logo.jpg'
top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg'
abbrlink: 7f60dde9
date: 2020-02-03 13:11:45
---
> 文章很长,喜欢的话,可以关注下博客。这段时间秋招忙完之后,会持续更新新内容
# ElasticSearch介绍
## 介绍
1、elasticsearch是一个基于Lucene的高扩展的分布式搜索服务器支持开箱即用。
2、elasticsearch隐藏了Lucene的复杂性对外提供Restful 接口来操作索引、搜索。
**突出优点:**
1. 扩展性好可部署上百台服务器集群处理PB级数据。
2. 近实时的去索引数据、搜索数据。
**es和solr选择哪个**
1. 如果你公司现在用的solr可以满足需求就不要换了。
2. 如果你公司准备进行全文检索项目的开发建议优先考虑elasticsearch因为像Github这样大规模的搜索都在用它。
## 倒排索引
下图是ElasticSearch的索引结构下边黑色部分是物理结构上边黄色部分是逻辑结构逻辑结构也是为了更好的 去描述ElasticSearch的工作原理及去使用物理结构中的索引文件。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/ElasticSearch/Introduction/0001.png" width=80%>
逻辑结构部分是一个倒排索引表:
1、将要搜索的文档内容分词所有不重复的词组成分词列表。
2、将搜索的文档最终以Document方式存储起来。
3、每个词和docment都有关联。
如下:
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/ElasticSearch/Introduction/0002.png" width=40%>
现在,如果我们想搜到`quick brown`我们只需要查找包含每个词条的文档:
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/ElasticSearch/Introduction/0003.png" width=80%>
两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳
# 基本概念
1.创建索引库 --------------------->类似于:数据库的建表
2.创建映射 --------------------->类似于:数据库的添加表中字段
3.创建(添加)文档 --------------------->类似于:数据库的往表中添加数据。术语称这个过程为:创建索引
5.搜索文档 --------------------->类似于:从数据库里查数据
6.文档 --------------------->类似于:数据库中的一行记录(数据)
7.Field(域) --------------------->类似于:数据库中的字段
## 创建索引库
### 概念:
ES的索引库是一个逻辑概念它包括了分词列表及文档列表同一个索引库中存储了相同类型的文档。它就相当于MySQL中的表或相当于Mongodb中的集合。
索引(index)
```shell
# 索引是 ES 对逻辑数据的逻辑存储,所以可以被分为更小的部分
# 可以将索引看成 MySQL 的 Table索引的结构是为快速有效的全文索引准备的特别是它不存储原始值
# 可以将索引存放在一台机器,或分散在多台机器上
# 每个索引有一或多个分片(shard),每个分片可以有多个副本(replica)
```
### 操作:
使用postman这样的工具创建 put http://localhost:9200/索引库名称
```shell
# ES 中提供非结构化索引,实际上在底层 ES 会进行结构化操作,对用户透明
PUT http://localhost:9200/索引库名称
{
"settings":{
"index":{
"number_of_shards":"1", # 分片数
"number_of_replicas":"0" # 副本数
}
}
}
```
- number_of_shards设置分片的数量在集群中通常设置多个分片表示一个索引库将拆分成多片分别存储不同 的结点提高了ES的处理能力和高可用性入门程序使用单机环境这里设置为1。
- number_of_replicas设置副本的数量设置副本是为了提高ES的高可靠性单机环境设置为0.
## 创建映射
### 概念
在索引中每个文档都包括了一个或多个field创建映射就是向索引库中创建field的过程下边是document和field 与关系数据库的概念的类比:
文档Document----- Row记录
字段Field----- Columns 列
注意6.0之前的版本有type类型概念type相当于关系数据库的表ES官方将在ES9.0版本中彻底删除type。 上边讲的创建索引库相当于关系数据库中的数据库还是表?
1、如果相当于数据库就表示一个索引库可以创建很多不同类型的文档这在ES中也是允许的。
2、如果相当于表就表示一个索引库只能存储相同类型的文档ES官方建议在一个索引库中只存储相同类型的文档。
3、所以索引库相当于数句酷的一个表
### 操作
1、我们要把课程信息存储到ES中这里我们创建课程信息的映射先来一个简单的映射如下
发送post http://localhost:9200/索引库名称/类型名称/_mapping
2、创建类型为xc_course的映射共包括三个字段name、description、studymondel 由于ES6.0版本还没有将type彻底删除所以暂时把type起一个没有特殊意义的名字doc。post 请求http://localhost:9200/xc_course/doc/_mapping
表示在xc_course索引库下的doc类型下创建映射。doc是类型名可以自定义在ES6.0中要弱化类型的概念, 给它起一个没有具体业务意义的名称。
```json
{
"properties": {
"name": {
"type": "text"
},
"description":{
"type": "text"
},
"studymodel":{
"type":"keyword"
}
}
}
```
## 创建文档
### 概念
ES中的文档相当于MySQL数据库表中的记录。
```shell
# 存储在 ES 中的主要实体叫文档,可以看成 MySQL 的一条记录
# ES 与 Mongo 的 document 类似,都可以有不同的结构,但 ES 相同字段必须有相同类型
# document 由多个字段组成,每个字段可能多次出现在一个文档里,这样的字段叫多值字段(multivalued)
# 每个字段的类型,可以使文本、数值、日期等。
# 字段类型也可以是复杂类型,一个字段包含其他子文档或者数组
# 在 ES 中,一个索引对象可以存储很多不同用途的 document例如一个博客App中可以保存文章和评论
# 每个 document 可以有不同的结构
# 不同的 document 不能为相同的属性设置不同的类型,例 : title 在同一索引中所有 Document 都应该相同数据类型
```
### 操作
发送put 或Post http://localhost:9200/xc_course/doc/id值
如果不指定id值ES会自动生成ID
http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000
```json
{
"name":Bootstrap",
"description" : "BootstrapTwitter,使广 CSSJS() ,
"studymodel": "201001"
}
```
## 搜索文档
1、根据课程id查询文档
发送get http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000
使用postman测试
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/ElasticSearch/Introduction/0004.png">
2、查询所有记录
发送 get http://localhost:9200/xc_course/doc/_search
3、查询名称中包括spring 关键字的的记录
发送get http://localhost:9200/xc_course/doc/_search?q=name:bootstrap
4、查询学习模式为201001的记录
发送 get http://localhost:9200/xc_course/doc/_search?q=studymodel:201001
**查询结果分析:**
```json
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 0.2876821,
"hits": [
{
"_index": "xc_course",
"_type": "doc",
"_id": "4028e58161bcf7f40161bcf8b77c0000",
"_score": 0.2876821,
"_source": {
"name": "Bootstrap开发框架",
"description": "Bootstrap是由Twitter推出的一个前台页面开发框架在行业之中使用较 为广泛。此开发框架包含了大量的CSS、JS程序代码可以帮助开发者尤其是不擅长页面开发的程序人员轻松的实现 一个不受浏览器限制的精美界面效果。",
"studymodel": "201001"
}
}
]
}
}
```
**结果说明:**
took本次操作花费的时间单位为毫秒。timed_out请求是否超时
_shards说明本次操作共搜索了哪些分片hits搜索命中的记录
hits.total 符合条件的文档总数 hits.hits 匹配度较高的前N个文档
hits.max_score文档匹配得分这里为最高分
_score每个文档都有一个匹配度得分按照降序排列。
_source显示了文档的原始内容。
# 分词
## 内置分词
### 分词API
分词是将一个文本转换成一系列单词的过程,也叫文本分析,在 ES 中称之为 Analysis
例如 : 我是中国人 -> 我 | 是 | 中国人
```json
#
POST http://['自己的ip 加 port']/_analyze
{
"analyzer":"standard",
"text":"hello world"
}
#
#
POST http://['自己的ip 加 port']/beluga/_analyze
{
"analyzer":"standard",
"field":"hobby",
"text":"听音乐"
}
```
### Standard
```shell
# Standard 标准分词,按单词切分,并且会转换成小写
POST http://['自己的ip 加 port']/_analyze
{
"analyzer":"standard",
"text": "A man becomes learned by asking questions."
}
```
### Simple
```shell
# Simple 分词器,按照非单词切分,并且做小写处理
POST http://['自己的ip 加 port']/_analyze
{
"analyzer":"simple",
"text":"If the document does't already exist"
}
```
### Whitespace
```shell
# Whitespace 是按照空格切分
POST http://['自己的ip 加 port']/_analyze
{
"analyzer":"whitespace",
"text":"If the document does't already exist"
}
```
### Stop
```shell
# Stop 去除 Stop Word 语气助词,如 the、an 等
POST http://['自己的ip 加 port']/_analyze
{
"analyzer":"stop",
"text":"If the document does't already exist"
}
```
### Keyword
```shell
# keyword 分词器,意思是传入就是关键词,不做分词处理
POST http://['自己的ip 加 port']/_analyze
{
"analyzer":"keyword",
"text":"If the document does't already exist"
}
```
### 中文分词
```shell
# 中文分词的难点在于,汉语中没有明显的词汇分界点
# 常用中文分词器IK jieba THULAC 等,推荐 IK
# IK Github 站点<自定义词典扩展,禁用词典扩展等>
https://github.com/medcl/elasticsearch-analysis-ik
```
## IK分词器
安装过程这里不介绍,主要是解决常见中文分词的问题
Github地址https://github.com/medcl/elasticsearch-analysis-ik
### 两种分词模式
ik分词器有两种分词模式ik_max_word和ik_smart模式。
1、ik_max_word
会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、 华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。
2、ik_smart
会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。 测试两种分词模式:
# 映射
上边章节安装了ik分词器如果在索引和搜索时去使用ik分词器呢如何指定其它类型的field比如日期类型、数 值类型等。本章节学习各种映射类型及映射维护方法。
## 映射维护方法
1、查询所有索引的映射
GET http://localhost:9200/_mapping
2、创建映射
post 请求http://localhost:9200/xc_course/doc/_mapping
在上面提到过
```
{
"properties": {
"name": {
"type": "text"
},
"description":{
"type": "text"
},
"studymodel":{
"type":"keyword"
}
}
}
```
3、更新映射
映射创建成功可以添加新字段,已有字段不允许更新。
4、删除映射
通过删除索引来删除映射。
## 常用映射类型
### text文本字段
**1text**
字符串包括text和keyword两种类型 通过analyzer属性指定分词器。
下边指定name的字段类型为text使用ik分词器的ik_max_word分词模式。
```json
{
"name": {
"type": "text",
"analyzer": "ik_max_word"
}
}
```
上边指定了analyzer是指在索引和搜索都使用ik_max_word如果单独想定义搜索时使用的分词器则可以通过search_analyzer属性。
对于ik分词器建议是索引时使用ik_max_word将搜索内容进行细粒度分词搜索时使用ik_smart提高搜索精确性。
```json
{
"name": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
```
**2 index**
通过index属性指定是否索引。
默认为index=true即要进行索引只有进行索引才可以从索引库搜索到。
但是也有一些内容不需要索引比如商品图片地址只被用来展示图片不进行搜索图片此时可以将index设置 为false。
删除索引重新创建映射将pic的index设置为false尝试根据pic去搜索结果搜索不到数据
```json
{
"pic": {
"type": "text",
"index": false
}
}
```
**3store**
是否在source之外存储每个文档索引后会在 ES中保存一份原始文档存放在"_source"中,一般情况下不需要设置 store为true因为在_source中已经有一份原始文档了。
### keyword关键字字段
上边介绍的text文本字段在映射时要设置分词器keyword字段为关键字字段通常搜索keyword是按照整体搜 索所以创建keyword字段的索引时是不进行分词的比如邮政编码、手机号码、身份证等。keyword字段通常 用于过虑、排序、聚合等。
**测试:**
更改映射:
```json
{
"properties": {
"studymodel": {
"type": "keyword"
},
"name": {
"type": "keyword"
}
}
}
```
添加文档:
```json
{
"name": "java编程基础",
"description": "java语言是世界第一编程语言在软件开发领域使用人数最多。",
"pic": "group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
"studymodel": "201001"
}
```
根据name查询文档。搜索http://localhost:9200/xc_course/_search?q=name:java name是keyword类型所以查询方式是精确查询。
### 日期类型
日期类型不用设置分词器。
通常日期类型的字段用于排序。
1)format
通过format设置日期格式例子
下边的设置允许date字段存储年月日时分秒、年月日及毫秒三种格式
```json
{
"properties": {
"timestamp": {
"type": "date",
"format": "yyyyMMdd HH:mm:ss||yyyyMMdd"
}
}
}
```
插入文档:
Post :http://localhost:9200/xc_course/doc/3
```json
{
"name": "spring开发基础",
"description": "spring 在java领域非常流行java程序员都在用。",
"studymodel": "201001",
"pic": "group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
"timestamp": "20180704 18:28:58"
}
```
### 综合例子
posthttp://localhost:9200/xc_course/doc/_mapping
```json
{
"properties": {
"description": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"name": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"pic": {
"type": "text",
"index": false
},
"price": {
"type": "float"
},
"studymodel": {
"type": "keyword"
},
"timestamp": {
"type": "date",
"format": "yyyyMMdd HH:mm:ss||yyyyMMdd||epoch_millis"
}
}
}
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,801 @@
> - 本阶段文章讲的略微深入,一些基础性问题不会讲解,如有基础性问题不懂,可自行查看我前面的文章,或者自行学习。
> - 本篇文章比较适合校招和社招的面试笔者在2020年面试的过程中也确实被问到了下面的一些问题。
# 并发编程中的三个问题
> 由于这个东西,和这篇文章比较配。所以虽然在第一阶段写过了,这里再回顾一遍。
## 可见性
### 可见性概念
可见性Visibility是指一个线程对共享变量进行修改另一个先立即得到修改后的新值。
### 可见性演示
```java
/* 笔记
* 1.当没有加Volatile的时候,while循环会一直在里面循环转圈
* 2.当加了之后Volatile,由于可见性,一旦num改了之后,就会通知其他线程
* 3.还有注意的时候不能用if,if不会重新拉回来再判断一次。(也叫做虚假唤醒)
* 4.案例演示:一个线程对共享变量的修改,另一个线程不能立即得到新值
* */
public class Video04_01 {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "\t come in ");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//睡3秒之后再修改num,防止A线程先修改了num,那么到while循环的时候就会直接跳出去了
myData.addTo60();
System.out.println(Thread.currentThread().getName() + "\t come out");
},"A").start();
while(myData.num == 0){
//只有当num不等于0的时候,才会跳出循环
}
}
}
class MyData{
int num = 0;
public void addTo60(){
this.num = 60;
}
}
```
由上面代码可以看出,并发编程时,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值。
## 原子性
### 原子性概念
原子性Atomicity在一次或多次操作中要么所有的操作都成功执行并且不会受其他因素干扰而中 断,要么所有的操作都不执行或全部执行失败。不会出现中间状态
### 原子性演示
案例演示:5个线程各执行1000次 i++;
```java
/**
* @Author: 吕
* @Date: 2019/9/23 15:50
* <p>
* 功能描述: volatile不保证原子性的代码验证
*/
public class Video05_01 {
public static void main(String[] args) {
MyData03 myData03 = new MyData03();
for (int i = 0; i < 20; i++) {
new Thread(() ->{
for (int j = 0; j < 1000; j++) {
myData03.increment();
}
},"线程" + String.valueOf(i)).start();
}
//需要等待上面的20个线程计算完之后再查看计算结果
while(Thread.activeCount() > 2){
Thread.yield();
}
System.out.println("20个线程执行完之后num:\t" + myData03.num);
}
}
class MyData03{
static int num = 0;
public void increment(){
num++;
}
}
```
1、控制台输出由于并发不安全每次执行的结果都可能不一样
> 20个线程执行完之后num: 19706
正常来说如果保证原子性的话20个线程执行完结果应该是20000。控制台输出的值却不是这个说明出现了原子性的问题。
2、使用javap反汇编class文件对于num++可以得到下面的字节码指令:
```java
9: getstatic #12 // Field number:I 取值操作
12: iconst_1
13: iadd
14: putstatic #12 // Field number:I 赋值操作
```
由此可见num++是由多条语句组成,以上多条指令在一个线程的情况下是不会出问题的,但是在多线程情况下就可能会出现问题。
比如num刚开始值是7。A线程在执行13: iadd时得到num值是8B线程又执行9: getstatic得到前一个值是7。马上A线程就把8赋值给了num变量。但是B线程已经拿到了之前的值7B线程是在A线程真正赋值前拿到的num值。即使A线程最终把值真正的赋给了num变量但是B线程已经走过了getstaitc取值的这一步B线程会继续在7的基础上进行++操作最终的结果依然是8。本来两个线程对7进行分别进行++操作得到的值应该是9因为并发问题导致结果是8。
3、并发编程时会出现原子性问题当一个线程对共享变量操作到一半时另外的线程也有可能来操作共 享变量,干扰了前一个线程的操作。
## 有序性
### 有序性概念
有序性Ordering是指程序中代码的执行顺序Java在编译时和运行时会对代码进行优化重排序来加快速度会导致程序终的执行顺序不一定就是我们编写代码时的顺序
```java
instance = new SingletonDemo() 是被分成以下 3 步完成
memory = allocate(); 分配对象内存空间
instance(memory); 初始化对象
instance = memory; 设置 instance 指向刚分配的内存地址此时 instance != null
```
步骤2 和 步骤3 不存在数据依赖关系,重排与否的执行结果单线程中是一样的。这种指令重排是被 Java 允许的。当 3 在前时instance 不为 null但实际上初始化工作还没完成会变成一个返回 null 的getInstance。这时候数据就出现了问题。
### 有序性演示
jcstress是java并发压测工具。https://wiki.openjdk.java.net/display/CodeTools/jcstress 修改pom文件添加依赖
```Java
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>${jcstress.version}</version>
</dependency>
```
```
import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;
@JCStressTest
// @Outcome: 如果输出结果是1或4我们是接受的(ACCEPTABLE)并打印ok
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
//如果输出结果是0我们是接受的并且感兴趣的并打印danger
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Test03Ordering {
int num = 0;
boolean ready = false;
// 线程1执行的代码
@Actor //@Actor表示会有多个线程来执行这个方法
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2执行的代码
// @Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
```
1、实际上上面两个方法会有很多线程来执行为了讲解方便我们只提出线程1和线程2来讲解。
2、I_Result 是一个保存int类型数据的对象有一个属性 r1 用来保存结果,在多线程情况下可能出现几种结果?
情况1线 程1先执行actor1这时ready = false所以进入else分支结果为1。
情况2线程2执行到actor2执行了num = 2;和ready = true线程1执行这回进入 if 分支,结果为 4。
情况3线程2先执行actor2只执行num = 2但没来得及执行 ready = true线程1执行还是进入 else分支结果为1。
情况40发生了指令重排
```java
// 线程2执行的代码
// @Actor
public void actor2(I_Result r) {
num = 2; //pos_1
ready = true;//pos_2
}
```
pos_1处代码和pos_2处代码没有什么数据依赖关系或者说没有因果关系。Java可能对其进行指令重排排成下面的顺序。
```java
// 线程2执行的代码
// @Actor
public void actor2(I_Result r) {
ready = true;//pos_2
num = 2; //pos_1
}
```
此时如果线程2先执行到`ready = true;`还没来得及执行 `num = 2;` 。线程1执行直接进入if分支此时num默认值为0。 得到的结果也就是0。
# 指令重排
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。
## 为什么指令重排序可以提高性能?
简单地说,每一个指令都会包含多个步骤,每个步骤可能使用不同的硬件。因此,**流水线技术**产生了它的原理是指令1还没有执行完就可以开始执行指令2而不用等到指令1执行结束之后再执行指令2这样就大大提高了效率。
但是,流水线技术最害怕**中断**,恢复中断的代价是比较大的,所以我们要想尽办法不让流水线中断。指令重排就是减少中断的一种技术。
我们分析一下下面这个代码的执行情况:
```java
a = b + c;
d = e - f ;
```
先加载b、c**注意即有可能先加载b也有可能先加载c**但是在执行add(b,c)的时候需要等待b、c装载结束才能继续执行也就是增加了停顿那么后面的指令也会依次有停顿,这降低了计算机的执行效率。
为了减少这个停顿我们可以先加载e和f,然后再去加载add(b,c),这样做对程序(串行)是没有影响的,但却减少了停顿。既然add(b,c)需要停顿,那还不如去做一些有意义的事情。
综上所述,**指令重排对于提高CPU处理性能十分必要。虽然由此带来了乱序的问题但是这点牺牲是值得的。**
指令重排一般分为以下三种:
* **编译器优化重排**
编译器在**不改变单线程程序语义**的前提下,可以重新安排语句的执行顺序。
* **指令并行重排**
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果**不存在数据依赖性**(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。
* **内存系统重排**
由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0001.png">
**指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致**。所以在多线程下,指令重排序可能会导致一些问题。
## as-if-serial语义
as-if-serial语义的意思是不管编译器和CPU如何重排序必须保证在单线程情况下程序的结果是正确的。 以下数据有依赖关系,不能重排序。
写后读:
```
int a = 1;
int b = a;
```
写后写:
```
int a = 1;
int a = 2;
```
读后写:
```
int a = 1;
int b = a;
int a = 2;
```
编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
```
int a = 1;
int b = 2;
int c = a + b;
```
# Java内存模型(JMM)
在介绍Java内存模型之前先来看一下到底什么是计算机内存模型。
## 计算机结构
### 计算机结构简介
冯诺依曼,提出计算机由五大组成部分,输入设备,输出设备存储器,控制器,运算器。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0002.png">
输入设备:鼠标,键盘等等
输出设备:显示器,打印机等等
存储器:内存条
运算器和控制器组成CPU
### CPU
中央处理器是计算机的控制和运算的核心我们的程序终都会变成指令让CPU去执行处理程序中 的数据。
### 内存
我们的程序都是在内存中运行的内存会保存程序运行时的数据供CPU处理。
### 缓存
CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。内 存的读写速度成为了计算机运行的瓶颈。于是就有了在CPU和主内存之间增加缓存的设计。靠近CPU 的缓存称为L1然后依次是 L2L3和主内存CPU缓存模型如图下图所示。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0003.png">
CPU Cache分成了三个级别: L1 L2 L3。级别越小越接近CPU速度也更快同时也代表着容量越小。速度越快的价格越贵。
1、L1是接近CPU的它容量小例如32K速度快每个核上都有一个L1 Cache。
2、L2 Cache 更大一些例如256K速度要慢一些一般情况下每个核上都有一个独立的L2 Cache。
3、L3 Cache是三级缓存中大的一级例如12MB同时也是缓存中慢的一级在同一个CPU插槽 之间的核共享一个L3 Cache。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0004.png">
上面的图中有一个Latency指标。比如Memory这个指标为59.4ns表示CPU在操作内存的时候有59.4ns的延迟一级缓存最快只有1.2ns。
**CPU处理数据的流程**
Cache的出现是为了解决CPU直接访问内存效率低下问题的。
1、程序在运行的过程中CPU接收到指令 后它会先向CPU中的一级缓存L1 Cache去寻找相关的数据如果命中缓存CPU进行计算时就可以直接对CPU Cache中的数据进行读取和写人当运算结束之后再将CPUCache中的新数据刷新 到主内存当中CPU通过直接访问Cache的方式替代直接访问主存的方式极大地提高了CPU 的吞吐能 力。
2、但是由于一级缓存L1 Cache容量较小所以不可能每次都命中。这时CPU会继续向下一级的二 级缓存L2 Cache寻找同样的道理当所需要的数据在二级缓存中也没有的话会继续转向L3 Cache、内存(主存)和硬盘。
## Java内存模型
1、Java Memory Molde (Java内存模型/JMM)千万不要和Java内存结构JVM划分的那个堆方法区混淆。关于“Java内存模型”的权威解释参考 https://download.oracle.com/otn-pub/jcp/memory_model1.0-pfd-spec-oth-JSpec/memory_model-1_0-pfd-spec.pdf。
2、 Java内存模型是Java虚拟机规范中所定义的一种内存模型Java内存模型是标准化的屏蔽掉了底层不同计算机的区别。 Java内存模型是一套规范描述了Java程序中各种变量(线程共享变量)的访问规则以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节具体如下。
3、Java内存模型根据官方的解释主要是在说两个关键字一个是`volatile`,一个是`synchronized`
**主内存**
主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。
**工作内存**
每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操 作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接 访问对方工作内存中的变量。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0005.png">
Java的线程不能直接在主内存中操作共享变量。而是首先将主内存中的共享变量赋值到自己的工作内存中再进行操作操作完成之后刷回主内存。
**Java内存模型的作用**
Java内存模型是一套在多线程读写共享数据时对共享数据的可见性、有序性、和原子性的规则和保障。 synchronized,volatile
## CPU缓存内存与Java内存模型的关系
- 通过对前面的CPU硬件内存架构、Java内存模型以及Java多线程的实现原理的了解我们应该已经意识到多线程的执行终都会映射到硬件处理器上进行执行。 但Java内存模型和硬件内存架构并不完全一致。
- 对于硬件内存来说只有寄存器、缓存内存、主内存的概念并没有工作内存和主内存之分也就是说Java内存模型对内存的划分对硬件内存并没有任何影响 因为JMM只是一种抽象的概念是一组规则不管是工作内存的数据还是主内存的数据对于计算机硬 件来说都会存储在计算机主内存中当然也有可能存储到CPU缓存或者寄存器中因此总体上来说 Java内存模型和计算机硬件内存架构是一个相互交叉的关系是一种抽象概念划分与真实物理硬件的交叉。
JMM内存模型与CPU硬件内存架构的关系
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0006.png">
工作内存可能对应CPU寄存器也可能对应CPU缓存也可能对应内存。
- Java内存模型是一套规范描述了Java程序中各种变量(线程共享变量)的访问规则以及在JVM中将变量 存储到内存和从内存中读取变量这样的底层细节Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。
## 再谈可见性
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0007.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0008.png">
1、图中所示是 个双核 CPU 系统架构 每个核有自己的控制器和运算器其中控制器包含一组寄存器和操作控制器运算器执行算术逻辅运算。每个核都有自己的1级缓存在有些架构里面还有1个所有 CPU 共享的2级缓存。 那么 Java 内存模型里面的工作内存,就对应这里的 Ll 或者 L2 存或者 CPU 寄存器。
2、一个线程操作共享变量时它首先从主内存复制共享变量到自己的工作内存然后对工作内存里的变量进行处理处理完后将变量值更新到主内存。
3、那么假如线程A和线程B同时处理一个共享变量会出现什么情况?我们使用图所示CPU架构假设线程A和线程B使用不同CPU执行并且当前两级Cache都为空那么这时候由于Cache的存在将会导致内存不可见问题具体看下面的分析。
- 线程A首先获取共享变量X的值由于两级Cache都没有命中所以加载主内存中X的值假如为0。然后把X=0的值缓存到两级缓存线程A修改X的值为1然后将其写入两级Cache并且刷新到主内存。线程A操作完毕后线程A所在的CPU的两级Cache 内和主内存里面的X的值都是1。
- 线程B获取X的值首先一级缓存没有命中然后看二级缓存二级缓存命中了所以返回X=1;到这里一切都是正常的因为这时候主内存中也是X=1。然后线程B修改X的值为2并将其存放到线程2所在的一级Cache和共享二级Cache中最后更新主内存中X 的值为2;到这里一切都是好的。
- 线程A 这次又需要修改X的值获取时一级缓存命中并且X=1到这里问题就出现了明明线程B已经把X的值修改为了2为何线程A获取的还是1呢?这就是共享变量的内存不可见问题也就是线程B写入的值对线程A不可见。那么如何解决共享变量内存不可见问题?使用Java中的volatile和synchronized关键字就可以解决这个问题下面会有讲解。
# 主内存与工作内存之间的交互
为了保证数据交互时数据的正确性Java内存模型中定义了8种操作来完成这个交互过程这8种操作本身都是原子性的。虚拟机实现时必须保证下面 提及的每一种操作都是原子的、不可再分的。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0009.png">
> (1)lock:作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
>
> (2)unlock:作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。
>
> (3)read:作用于主内存的变量它把一个变量的值从主内存传输到线程的工作内存中以便随后的load动作使用。
>
> (4)load:作用于工作内存的变量它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
>
> (5)use:作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时都会执行这个操作。
>
> (6)assign:作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
>
> (7)store:作用于工作内存的变量它把工作内存中一个变量的值传送到主内存中以便随后的write使用。
>
> (8)write:作用于主内存的变量它把store操作从工作内存中得到的变量的值放入主内存的变量中。
注意:
1. 如果对一个变量执行lock操作将会清空工作内存中此变量的值
2. 对一个变量执行unlock操作之前必须先把此变量同步到主内存中
3. lock和unlock操作只有加锁才会有。synchronized就是通过这样来保证可见性的。
如果没有synchronized那就是下面这样的
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0010.png">
# happens-before
## 什么是happens-before?
一方面程序员需要JMM提供一个强的内存模型来编写代码另一方面编译器和处理器希望JMM对它们的束缚越少越好这样它们就可以最可能多的做优化来提高性能希望的是一个弱的内存模型。
JMM考虑了这两种需求并且找到了平衡点对编译器和处理器来说**只要不改变程序的执行结果(单线程程序和正确同步了的多线程程序),编译器和处理器怎么优化都行。**
而对于程序员JMM提供了**happens-before规则**JSR-133规范满足了程序员的需求——**简单易懂,并且提供了足够强的内存可见性保证。**换言之程序员只要遵循happens-before规则那他写的程序就能保证在JMM中具有强的内存可见性。
JMM使用happens-before的概念来定制两个操作之间的执行顺序。这两个操作可以在一个线程以内也可以是不同的线程之间。因此JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。
happens-before关系的定义如下
1. 如果一个操作happens-before另一个操作那么第一个操作的执行结果将对第二个操作可见而且第一个操作的执行顺序排在第二个操作之前。
2. **两个操作之间存在happens-before关系并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果与按happens-before关系来执行的结果一致那么JMM也允许这样的重排序。**
happens-before关系本质上和as-if-serial语义是一回事。
as-if-serial语义保证单线程内重排序后的执行结果和程序代码本身应有的结果是一致的happens-before关系保证正确同步的多线程程序的执行结果不被重排序改变。
总之,**如果操作A happens-before操作B那么操作A在内存上所做的操作对操作B都是可见的不管它们在不在一个线程。**
## 天然的happens-before关系
在Java中有以下天然的happens-before关系
* 1、程序次序规则一个线程内按照代码顺序书写在前面的操作先行发生于书写在后面的操作
* 2、锁定规则一个unLock操作先行发生于后面对同一个锁的lock操作比如说在代码里有先对一个lock.lock()lock.unlock()lock.lock()
* 3、volatile变量规则对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作volatile变量写再是读必须保证是先写再读
* 4、传递规则如果操作A先行发生于操作B而操作B又先行发生于操作C则可以得出操作A先行发生于操作C
* 5、线程启动规则Thread对象的start()方法先行发生于此线程的每个一个动作thread.start()thread.interrupt()
* 6、线程中断规则对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
* 7、线程终结规则线程中所有的操作都先行发生于线程的终止检测我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
* 8、对象终结规则一个对象的初始化完成先行发生于他的finalize()方法的开始
上面这8条原则的意思很显而易见就是程序中的代码如果满足这个条件就一定会按照这个规则来保证指令的顺序。
**举例1**
```java
int a = 1; // A操作
int b = 2; // B操作
int sum = a + b;// C 操作
System.out.println(sum);
```
根据以上介绍的happens-before规则假如只有一个线程那么不难得出
```
1> A happens-before B
2> B happens-before C
3> A happens-before C
```
注意真正在执行指令的时候其实JVM有可能对操作A & B进行重排序因为无论先执行A还是B他们都对对方是可见的并且不影响执行结果。
如果这里发生了重排序这在视觉上违背了happens-before原则但是JMM是允许这样的重排序的。
所以我们只关心happens-before规则不用关心JVM到底是怎样执行的。只要确定操作A happens-before操作B就行了。
重排序有两类JMM对这两类重排序有不同的策略
* 会改变程序执行结果的重排序,比如 A -> CJMM要求编译器和处理器都禁止这种重排序。
* 不会改变程序执行结果的重排序,比如 A -> BJMM对编译器和处理器不做要求允许这种重排序。
**举例2**
```Java
//伪代码
volatile boolean flag = false;
//线程1
prepare();
flag = false;
//线程2
while(!flag){
sleep();
}
//基于准备好的资源进行操作
execute();
```
这8条原则是避免说出现乱七八糟扰乱秩序的指令重排要求是这几个重要的场景下比如是按照顺序来但是8条规则之外可以随意重排指令。
比如这个例子如果用volatile来修饰flag变量一定可以让prepare()指令在flag = true之前先执行这就禁止了指令重排。
因为volatile要求的是volatile前面的代码一定不能指令重排到volatile变量操作后面volatile后面的代码也不能指令重排到volatile前面。
# volatile
volatile不保证原子性只保证可见性和禁止指令重排
## CPU术语介绍
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0011.png">
```
private static volatile SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 执行单例构造函数");
}
public static SingletonDemo getInstance(){
if(instance == null){
synchronized (SingletonDemo.class){
if(instance == null){
instance = new SingletonDemo(); //pos_1
}
}
}
return instance;
}
```
**pos_1处的代码转换成汇编代码如下**
```shell
0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp);
```
## volatile保证可见性原理
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码通过查IA-32架 构软件开发者手册可知Lock前缀的指令在多核处理器下会引发了两件事情。
1将当前处理器缓存行的数据写回到系统内存。
2这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度处理器不直接和主内存进行通信而是先将系统内存的数据读到内部缓存L1L2或其他后再进行操作但操作完不知道何时会写到内存。如果对声明了volatile的 变量进行写操作JVM就会向处理器发送一条Lock前缀的指令将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存如果其他处理器缓存的值还是旧的再执行计算操作就会有问题。所以在多处理器下为了保证各个处理器的缓存是一致的就会实现MESI缓存一致性协议每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了当处理器发现自己缓存行对应的内存地址被修改就会将当前处理器的缓存行设置成无效状态当处理器对这个数据进行修改操作的时候会重新从系统内存中把数据读到处理器缓存里。
> 注意lock前缀指令是同时保证可见性和有序性也就是禁止指令重排
> 注意lock前缀指令相当于一个内存屏障【后文讲】
## volatile禁止指令重排的原理
```java
public class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // step 1
flag = true; // step 2
}
public void reader() {
if (flag) { // step 3
System.out.println(a); // step 4
}
}
}
```
在JSR-133之前的旧的Java内存模型中是允许volatile变量与普通变量重排序的。那上面的案例中可能就会被重排序成下列时序来执行
1. 线程A写volatile变量step 2设置flag为true
2. 线程B读同一个volatilestep 3读取到flag为true
3. 线程B读普通变量step 4读取到 a = 0
4. 线程A修改普通变量step 1设置 a = 1
可见如果volatile变量与普通变量发生了重排序虽然volatile变量能保证内存可见性也可能导致普通变量读取错误。
所以在旧的内存模型中volatile的写-读就不能与锁的释放-获取具有相同的内存语义了。为了提供一种比锁更轻量级的**线程间的通信机制****JSR-133**专家组决定增强volatile的内存语义严格限制编译器和处理器对volatile变量与普通变量的重排序。
编译器还好说JVM是怎么还能限制处理器的重排序的呢它是通过**内存屏障**来实现的。
什么是内存屏障硬件层面内存屏障分两种读屏障Load Barrier和写屏障Store Barrier。内存屏障有两个作用
1. 阻止屏障两侧的指令重排序;
2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。
> 注意这里的缓存主要指的是上文说的CPU缓存如L1L2等
### 保守策略下
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
编译器在**生成字节码时**,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。编译器选择了一个**比较保守的JMM内存屏障插入策略**,但它可以保证在任意处理器平台,任意的程序中都能 得到正确的volatile内存语义。
> 再逐个解释一下这几个屏障。注下述Load代表读操作Store代表写操作
>
> **LoadLoad屏障**对于这样的语句Load1; LoadLoad; Load2在Load2及后续读取操作要读取的数据被访问前保证Load1要读取的数据被读取完毕。
> **StoreStore屏障**对于这样的语句Store1; StoreStore; Store2在Store2及后续写入操作执行前这个屏障会吧Store1强制刷新到内存保证Store1的写入操作对其它处理器可见。
> **LoadStore屏障**对于这样的语句Load1; LoadStore; Store2在Store2及后续写入操作被刷出前保证Load1要读取的数据被读取完毕。
> **StoreLoad屏障**对于这样的语句Store1; StoreLoad; Load2在Load2及后续所有读取操作执行前保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的冲刷写缓冲器清空无效化队列。在大多数处理器的实现中这个屏障是个万能屏障兼具其它三种内存屏障的功能
对于连续多个volatile变量读或者连续多个volatile变量写编译器做了一定的优化来提高性能比如
> 第一个volatile读;
>
> LoadLoad屏障
>
> 第二个volatile读
>
> LoadStore屏障
**1、下面是保守策略下volatile写插入内存屏障后生成的指令序列示意图**
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0012.png">
> 图中的StoreStore屏障可以保证在volatile写之前其前面的所有普通写操作已经对任 意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。这里比较有意思的是volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与 后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面 是否需要插入一个StoreLoad屏障比如一个volatile写之后方法立即return。为了保证能正确 实现volatile的内存语义JMM在采取了保守策略在每个volatile写的后面或者在每个volatile 读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑JMM最终选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个 写线程写volatile变量多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时 选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM在实现上的一个特点首先确保正确性然后再去追求执行效率
**2、下面是在保守策略下volatile读插入内存屏障后生成的指令序列示意图**
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0013.png">
> 图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。 LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。 上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。
**优化举例:**
```java
class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite() {
int i = v1; // 第一个volatile读
int j = v2; // 第二个volatile读
a = i + j; // 普通写
v1 = i + 1; // 第一个volatile写
v2 = j * 2; // 第二个 volatile写
}
// 其他方法 }
}
```
针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0014.png">
注意最后的StoreLoad屏障不能省略。因为第二个volatile写之后方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写为了安全起见编译器通常会在这里插入一个StoreLoad屏障。
上面的优化针对任意处理器平台由于不同的处理器有不同“松紧度”的处理器内存模型内存屏障的插入还可以根据具体的处理器内存模型继续优化。以X86处理器为例图中除最后的StoreLoad屏障外其他的屏障都会被省略。
### X86处理器优化
前面保守策略下的volatile读和写在X86处理器平台可以优化成如下图所示。
X86处理器仅会对写-读操作做重排序。X86不会对读-读、读-写和写-写操作 做重排序因此在X86处理器中会省略掉这3种操作类型对应的内存屏障。在X86中JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在X86处理器中volatile写的开销比volatile读的开销会大很多因为执行StoreLoad屏障开销会比较大
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0015.png">
## volatile的用途
> 下面的代码在前面可能已经写过了,这里总结一下
从volatile的内存语义上来看volatile可以保证内存可见性且禁止重排序。
在保证内存可见性这一点上volatile有着与锁相同的内存语义所以可以作为一个“轻量级”的锁来使用。但由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁可以保证整个**临界区代码**的执行具有原子性。所以**在功能上锁比volatile更强大在性能上volatile更有优势**。
在禁止重排序这一点上volatile也是非常有用的。比如我们熟悉的单例模式其中有一种实现方式是“双重锁检查”比如这样的代码
```java
public class Singleton {
private static Singleton instance; // 不使用volatile关键字
// 双重锁检验
public static Singleton getInstance() {
if (instance == null) { // 第7行
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 第10行
}
}
}
return instance;
}
}
```
如果这里的变量声明不使用volatile关键字是可能会发生错误的。它可能会被重排序
```java
instance = new Singleton(); // 第10行
// 可以分解为以下三个步骤
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址
// 上述三个步骤可能会被重排序为 1-3-2也就是
1 memory=allocate();// 分配内存 相当于c的malloc
3 s=memory //设置s指向刚分配的地址
2 ctorInstanc(memory) //初始化对象
```
而一旦假设发生了这样的重排序比如线程A在第10行执行了步骤1和步骤3但是步骤2还没有执行完。这个时候另一个线程B执行到了第7行它会判定instance不为空然后直接返回了一个未初始化完成的instance
所以JSR-133对volatile做了增强后volatile的禁止重排序功能还是非常有用的。

View File

@@ -0,0 +1,482 @@
# 可见性设计的硬件
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0016.png">
从硬件的级别来考虑一下可见性的问题
**1、第一个可见性的场景**每个处理器都有自己的寄存器register所以多个处理器各自运行一个线程的时候可能导致某个变量给放到寄存器里去接着就会导致各个线程没法看到其他处理器寄存器里的变量的值修改了就有可能在寄存器的级别导致变量副本的更新无法让其他处理器看到。
**2、第二个可见性的场景**然后一个处理器运行的线程对变量的写操作都是针对写缓冲来的store buffer并不是直接更新主内存所以很可能导致一个线程更新了变量但是仅仅是在写缓冲区里罢了没有更新到主内存里去。这个时候其他处理器的线程是没法读到他的写缓冲区的变量值的所以此时就是会有可见性的问题。
**3、第三个可见性的场景**然后即使这个时候一个处理器的线程更新了写缓冲区之后将更新同步到了自己的高速缓存里cache或者是主内存然后还把这个更新通知给了其他的处理器但是其他处理器可能就是把这个更新放到无效队列里去没有更新他的高速缓存。此时其他处理器的线程从高速缓存里读数据的时候读到的还是过时的旧值。【处理器是优先从自己的高速缓存里取读取变量副本】
可见性发生的问题
如果要实现可见性的话其中一个方法就是通过MESI协议这个MESI协议实际上有很多种不同的时间因为他不过就是一个协议罢了具体的实现机制要靠具体底层的系统如何实现。
根据具体底层硬件的不同MESI协议的实现是有区别的。比如说MESI协议有一种实现就是一个处理器将另外一个处理器的高速缓存中的更新后的数据拿到自己的高速缓存中来更新一下这样大家的缓存不就实现同步了然后各个处理器的线程看到的数据就一样了。
# MESI-缓存一致性协议(简介)
1、为了实现MESI协议有两个配套的专业机制要给大家说一下flush处理器缓存、refresh处理器缓存。
- flush处理器缓存他的意思就是把自己更新的值刷新到高速缓存里去或者是主内存因为必须要刷到高速缓存或者是主内存才有可能在后续通过一些特殊的机制让其他的处理器从自己的高速缓存或者是主内存里读取到更新的值
- 除了flush以外他还会发送一个消息到总线bus通知其他处理器某个变量的值被他给修改了
- refresh处理器缓存他的意思就是说处理器中的线程在读取一个变量的值的时候如果发现其他处理器的线程更新了变量的值必须从其他处理器的高速缓存或者是主内存读取这个最新的值更新到自己的高速缓存中
2、所以说为了保证可见性在底层是通过MESI协议、flush处理器缓存和refresh处理器缓存这一整套机制来保障的
3、要记住flush和refresh这两个操作flush是强制刷新数据到高速缓存主内存不要仅仅停留在写缓冲器里面refresh是从总线嗅探发现某个变量被修改必须强制从其他处理器的高速缓存或者主内存加载变量的最新值到自己的高速缓存里去。【不同的硬件实现可能略有不同】
4、内存屏障的使用在底层硬件级别的原理其实就是在执行flush和refreshMESI协议是如何与内存屏障搭配使用的flush、refresh
```java
volatile boolean isRunning = true;
isRunning = false; => 写volatile变量就会通过执行一个内存屏障在底层会触发flush处理器缓存的操作while(isRunning) {}读volatile变量也会通过执行一个内存屏障在底层触发refresh操作
```
# 内存屏障的相关讲解
> 上面的文章可能已经把读者搞混了,其实可见性和有序性最主要的就是内存屏障,下面来介绍下内存屏障帮读者梳理一下。
内存屏障是被插入两个CPU指令之间的一种指令用来禁止处理器指令发生重排序像屏障一样从而保障有序性的。另外为了达到屏障的效果它也会使处理器写入、读取值之前将写缓冲器的值写入高速缓存清空无效队列实现可见性。
举例:将写缓冲器数据写入高速缓存,能够避免不同处理器之间不能访问写缓冲器而导致的可见性问题,以及有效地避免了存储转发问题;清空无效队列保证该处理器上高速缓存中不存在旧的副本,进而拿到最新数据
## 基本内存屏障
- LoadLoad屏障 对于这样的语句 Load1; LoadLoad; Load2在Load2及后续读取操作要读取的数据被访问前保证Load1要读取的数据被读取完毕。
- StoreStore屏障对于这样的语句 Store1; StoreStore; Store2在Store2及后续写入操作执行前保证Store1的写入操作对其它处理器可见。
- LoadStore屏障对于这样的语句Load1; LoadStore; Store2在Store2及后续写入操作被执行前保证Load1要读取的数据被读取完毕。
- StoreLoad屏障对于这样的语句Store1; StoreLoad; Load2在Load2及后续所有读取操作执行前保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的冲刷写缓冲器清空无效化队列。在大多数处理器的实现中这个屏障是个万能屏障兼具其它三种内存屏障的功能。
以上的四种屏障主要依据不同处理器支持的重排序读写读读写写写读来确定的比如某些处理器只支持写读重排序因此只需要StoreLoad屏障
下面对上述的基本屏障进行利用,以针对不同的目的用相应的屏障。
## 可见性保障
主要分为加载屏障Load Barrier和存储屏障Store Barrier
- 加载屏障StoreLoad屏障作为万能屏障作用是冲刷写缓冲器清空无效化队列这样处理器在读取共享变量时因为本高速缓存中的数据是无效的因此先从主内存或其他处理器的高速缓存中读取相应变量更新到自己的缓存中
- 存储屏障同样使用StoreLoad屏障作用是将写缓冲器内容写入高速缓存中使处理器对共享变量的更新写入高速缓存或者主内存中 ,同时解决存储转发问题,使得写缓冲器中的数据不存在旧值
以上两种屏障解决可见性问题。
## 有序性保障
主要分为获取屏障Acquire Barrier和释放屏障Release Barrier
- 获取屏障相当于LoadLoad屏障和LoadStore屏障的组合它能禁止该屏障之前的任何读操作与该屏障之后的任何读、写操作之间进行重排序
- 释放屏障相当于StoreLoad屏障与StoreStore屏障的组合它能够禁止该屏障之前的任何读、写操作与该屏障之后的任何写操作之间进行重排序
对于其实大家记住volatile修饰的字段和普通修饰的字段同样不可以重排序因此只要存在读写、写写、写读、读读等操作包含了volatile关键字都会在操作指令之间插入屏障的具体插入什么屏障可以根据对应的操作插入。
## synchronized
```
结论:
1原子性加锁和释放锁ObjectMonitor
2可见性加了Load屏障和Store屏障释放锁flush数据加锁会refresh数据
3有序性Acquire屏障和Release屏障保证同步代码块内部的指令可以重排但是同步代码块内部的指令和外面的指令是不能重排的
```
举个例子说明加屏障的顺序:
```java
int b = 0;
int c = 0;
synchronized(this) { -> monitorenter
//Load内存屏障
//Acquire内存屏障
int a = b;
c = 1; // synchronized代码块里面还是可能会发生指令重排
//Release内存屏障
} -> monitorexit
//Store内存屏障
```
1、java的并发技术底层很多都对应了内存屏障的使用包括synchronized他底层也是依托于各种不同的内存屏障来保证可见性和有序性的
2、按照可见性来划分的话内存屏障可以分为Load屏障和Store屏障。
- Load屏障的作用是执行refresh处理器缓存的操作说白了就是对别的处理器更新过的变量从其他处理器的高速缓存或者主内存加载数据到自己的高速缓存来确保自己看到的是最新的数据。
- Store屏障的作用是执行flush处理器缓存的操作说白了就是把自己当前处理器更新的变量的值都刷新到高速缓存或者主内存里去
+ 在monitorexit指令之后会有一个Store屏障让线程把自己在同步代码块里修改的变量的值都执行flush处理器缓存的操作刷到高速缓存或者主内存里去然后在monitorenter指令之后会加一个Load屏障执行refresh处理器缓存的操作把别的处理器修改过的最新值加载到自己高速缓存里来
+ 所以说通过Load屏障和Store屏障就可以让synchronized保证可见性。
3、按照有序性保障来划分的话还可以分为Acquire屏障和Release屏障。
- 在monitorenter指令之后Load屏障之后会加一个Acquire屏障这个屏障的作用是禁止读操作和读写操作之间发生指令重排序。在monitorexit指令之前会加一个Release屏障这个屏障的作用是禁止写操作和读写操作之间发生重排序。
- 所以说,通过 Acquire屏障和Release屏障就可以让synchronzied保证有序性只有synchronized内部的指令可以重排序但是绝对不会跟外部的指令发生重排序。
## volatile
```
前面讲过lock前缀指令相当于一个内存屏障lock前缀指令同时保证可见性和有序性
1可见性加了Load屏障和Store屏障释放锁flush数据加锁会refresh数据
2有序性Acquire屏障和Release屏障保证同步代码块内部的指令可以重排但是同步代码块内部的指令和外面的指令是不能重排的
3不保证原子性
```
1、volatile对原子性的保证真的是非常的有限其实主要就是32位jvm中的long/double类型变量的赋值操作是不具备原子性的加上volatile就可以保证原子性了。但是总体上就说不保证原子性。
2、
```java
volatile boolean isRunning = true;
线程1
Release屏障
isRunning = false;
Store屏障
线程2
Load屏障
while(isRunning) {
Acquire屏障
// 代码逻辑
}
```
- 在volatile变量写操作的前面会加入一个Release屏障然后在之后会加入一个Store屏障这样就可以保证volatile写跟Release屏障之前的任何读写操作都不会指令重排然后Store屏障保证了写完数据之后立马会执行flush处理器缓存的操作
- 在volatile变量读操作的前面会加入一个Load屏障这样就可以保证对这个变量的读取时如果被别的处理器修改过了必须得从其他处理器的高速缓存或者主内存中加载到自己本地高速缓存里保证读到的是最新数据在之后会加入一个Acquire屏障禁止volatile读操作之后的任何读写操作会跟volatile读指令重排序
跟之前讲解的volatie读写内存屏障的知识对比一下其实你看一下是类似的意思的。
## 强调
- 其实不要对内存屏障这个东西太较真因为说句实话不同版本的JVM不同的底层硬件都可能会导致加的内存屏障有一些区别所以这个本来就没完全一致的。你只要知道内存屏障是如何保证volatile的可见性和有序性的就可以了
- 看各种并发相关的书和文章,对内存屏障到底是加的什么屏障,莫衷一是,没有任何一个官方权威的说法,因为这个内存屏障太底层了,底层到了涉及到了硬件,硬件不同对内存屏障的实现是不一样的
- 内存屏障这个东西大概来说其实就是大概的给你说一下这个意思尤其是Release屏障Store屏障和Load屏障还好理解一些比较简单Acqurie屏障莫衷一是我也没法给你一个官方的定论
- 如果你一定 要了解清除,到底加的准确的屏障是什么?到底是如何跟上下的指令避免重排的,你自己去研究吧。【我也看过很多的资料,做过很多的研究,硬件对这个东西的实现和承诺,莫衷一是,没有标准和官方定论。-----这句话是某BAT大佬说的】
内存屏障对应的底层的一些基本的硬件级别的原理,也都讲清楚了
# MESI-缓存一致性协议(进阶)
## MESI-初步
1、处理器高速缓存的底层数据结构实际是一个拉链散列表的结构就是有很多个bucket每个bucket挂了很多的cache entry每个cache entry由三个部分组成tag、cache line和flag其中的cache line【缓存行】就是缓存的数据。tag指向了这个缓存数据在主内存中的数据的地址flag标识了缓存行的状态另外要注意的一点是cache line中可以包含多个变量的值
2、处理器会操作一些变量怎么在高速缓存里定位到这个变量呢
- 那么处理器在读写高速缓存的时候实际上会根据变量名执行一个内存地址解码的操作解析出来3个东西index、tag和offset。index用于定位到拉链散列表中的某个buckettag是用于定位cache entryoffset是用于定位一个变量在cache line中的位置
- 如果说可以成功定位到一个高速缓存中的数据而且flag还标志着有效则缓存命中否则不满足上述条件就是缓存未命中。如果是读数据未命中的话会从主内存重新加载数据到高速缓存中现在处理器一般都有三级高速缓存L1、L2、L3越靠前面的缓存读写速度越快
3、因为有高速缓存的存在所以就导致各个处理器可能对一个变量会在自己的高速缓存里有自己的副本这样一个处理器修改了变量值别的处理器是看不到的所以就是为了这个问题引入了缓存一致性协议MESI协议
4、MESI协议规定对一个共享变量的读操作可以是多个处理器并发执行的但是如果是对一个共享变量的写操作只有一个处理器可以执行其实也会通过排他锁的机制保证就一个处理器能写
之前说过那个cache entry的flag代表了缓存数据的状态MESI协议中划分为
- invalid无效的标记为I这个意思就是当前cache entry无效里面的数据不能使用
- shared共享的标记为S这个意思是当前cache entry有效而且里面的数据在各个处理器中都有各自的副本但是这些副本的值跟主内存的值是一样的各个处理器就是并发的在读而已
- exclusive独占的标记为E这个意思就是当前处理器对这个数据独占了只有他可以有这个副本其他的处理器都不能包含这个副本
- modified修改过的标记为M只能有一个处理器对共享数据更新所以只有更新数据的处理器的cache entry才是exclusive状态表明当前线程更新了这个数据这个副本的数据跟主内存是不一样的
MESI协议规定了一组消息就说各个处理器在操作内存数据的时候都会往总线发送消息而且各个处理器还会不停的从总线嗅探最新的消息通过这个总线的消息传递来保证各个处理器的协作
**下面来详细的图解MESI协议的工作原理**
1、处理器0读取某个变量的数据时首先会根据index、tag和offset从高速缓存的拉链散列表读取数据如果发现状态为I也就是无效的此时就会发送read消息到总线
2、接着主内存会返回对应的数据给处理器0处理器0就会把数据放到高速缓存里同时cache entry的flag状态是S
3、在处理器0对一个数据进行更新的时候如果数据状态是S则此时就需要发送一个invalidate消息到总线尝试让其他的处理器的高速缓存的cache entry全部变为I以获得数据的独占锁。
4、其他的处理器1会从总线嗅探到invalidate消息此时就会把自己的cache entry设置为I也就是过期掉自己本地的缓存然后就是返回invalidate ack消息到总线传递回处理器0处理器0必须收到所有处理器返回的ack消息
5、接着处理器0就会将cache entry先设置为E独占这条数据在独占期间别的处理器就不能修改数据了因为别的处理器此时发出invalidate消息这个处理器0是不会返回invalidate ack消息的除非他先修改完再说
6、接着处理器0就是修改这条数据接着将数据设置为M也有可能是把数据此时强制写回到主内存中具体看底层硬件实现
7、然后其他处理器此时这条数据的状态都是I了那如果要读的话全部都需要重新发送read消息从主内存或者是其他处理器来加载这个具体怎么实现要看底层的硬件了都有可能的
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0017.jpg">
## MESI-优化
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0018.png">
MESI协议如果每次写数据的时候都要发送invalidate消息等待所有处理器返回ack然后获取独占锁后才能写数据那可能就会导致性能很差了因为这个对共享变量的写操作实际上在硬件级别变成串行的了。所以为了解决这个问题硬件层面引入了写缓冲器和无效队列
1、
写缓冲器的作用是一个处理器写数据的时候直接把数据写入缓冲器同时发送invalidate消息然后就认为写操作完成了接着就干别的事儿了不会阻塞在这里。接着这个处理器如果之后收到其他处理器的ack消息之后才会把写缓冲器中的写结果拿出来通过对cache entry设置为E加独占锁同时修改数据然后设置为M。
其实写缓冲器的作用就是处理器写数据的时候直接写入缓冲器不需要同步阻塞等待其他处理器的invalidate ack返回这就大大提升了硬件层面的执行效率了
包括查询数据的时候,会先从写缓冲器里查,因为有可能刚修改的值在这里,然后才会从高速缓存里查,这个就是存储转发
2、
引入无效队列就是说其他处理器在接收到了invalidate消息之后不需要立马过期本地缓存直接把消息放入无效队列就返回ack给那个写处理器了这就进一步加速了性能然后之后从无效队列里取出来消息过期本地缓存即可
通过引入写缓冲器和无效队列一个处理器要写数据的话这个性能其实很高的他直接写数据到写缓冲器发送一个validate消息出去就立马返回执行别的操作了其他处理器收到invalidate消息之后直接放入无效队列立马就返回invalidate ack
## 硬件层面的MESI协议为何会引发有序性和可见性的问题
MESI协议在硬件层面的原理其实大家都已经了解的很清晰了。
讲了这么多再来看一下MESI-协议为何会引发可见性和有序性的问题
- 可见性写缓冲器和无效队列导致的写数据不一定立马写入自己的高速缓存或者主内存是因为可能写入了写缓冲器读数据不一定立马从别人的高速缓存或者主内存刷新最新的值过来invalidate消息在无效队列里面
- 有序性:
简单的举两个例子
1StoreLoad重排序
```java
int a = 0;
int c = 1;
线程1
a = 1; //Store操作
int b = c; //因为要读C的值所以这个是load操作
```
这个很简单吧第一个是Store第二个是Load。但是可能处理器对store操作先写入了写缓冲器此时这个写操作相当于没执行。然后就执行了第二行代码第二行代码的b是局部变量那这个操作等于是读取c的值是load操作。
- 第一个store操作写到写缓冲器里去了导致其他的线程是读不到的看不到的好像是第一个写操作没执行一样第二个load操作成功的执行了
- 这就导致好像第二行代码的load先执行了第一行代码的store后执行
StoreLoad重排明明Store先执行Load后执行看起来好像Load先执行Store后执行
2StoreStore重排序
```java
resource = loadResource();
loaded = true;
```
- 两个写操作但是可能第一个写操作写入了写缓冲器然后第二个写操作是直接修改的高速缓存【可能此时第二个数据的状态是m】这个时候不就导致了两个写操作顺序颠倒了诸如此类的重排序都可能会因为MESI的机制发生
- 可见性问题也是一样的写入写缓冲器之后没刷入高速缓存导致别人读不到读数据的时候可能invalidate消息在无效队列里导致没法立马感知到过期的缓存立马加载最新的数据
## 内存屏障在硬件层面的实现原理
1、可见性问题
> Store屏障 + Load屏障
如果加了Store屏障之后就会强制性要求你对一个写操作必须阻塞等待到其他的处理器返回invalidate ack之后对数据加锁然后修改数据到高速缓存中必须在写数据之后强制执行flush操作。
他的效果,要求一个写操作必须刷到高速缓存(或者主内存),不能停留在写缓冲里
如果加了Load屏障之后在从高速缓存中读取数据的时候如果发现无效队列里有一个invalidate消息此时会立马强制根据那个invalidate消息把自己本地高速缓存的数据设置为I过期然后就可以强制从其他处理器的高速缓存中加载最新的值了。这就是refresh操作
2、有序性问题
> 内存屏障Acquire屏障Release屏障但是都是由基础的StoreStore屏障,StoreLoad屏障可以避免指令重排序的效果
StoreStore屏障会强制让写数据的操作全部按照顺序写入写缓冲器里他不会让你第一个写到写缓冲器里去第二个写直接修改高速缓存了。
```java
resource = loadResource();
StoreStore屏障
loaded = true;
```
StoreLoad屏障他会强制先将写缓冲器里的数据写入高速缓存中接着读数据的时候强制清空无效队列对里面的validate消息全部过期掉高速缓存中的条目然后强制从主内存里重新加载数据
a = 1; // 强制要求必须直接写入高速缓存,不能停留在写缓冲器里,清空写缓冲器里的这条数据 store
int b = c; //load
> java内存模型是对底层的硬件模型cpu缓存模型做了大幅度的简化提供一个抽象和统一的模型给java程序员易于理解很多时候如果要理解一些技术的本质还是要深入到底层去研究的。
# 原子操作的实现原理
原子atomic本意是“不能被进一步分割的最小粒子”而原子操作atomic operation意为“不可被中断的一个或一系列操作”。在多处理器上实现原子操作就变得有点复杂。让我们一起来聊一聊在Intel处理器和Java里是如何实现原子操作的。
## 相关术语
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0019.png">
## 处理器如何实现原子操作
32位IA-32处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操 作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的意思是当一个处理器读取一个字节时其他处理器不能访问这个字节的内存地址。Pentium 6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的但是复杂的内存操作处理器是不能自动保证其原子性的比如跨总线宽度、跨多个缓存行和跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
### 使用总线锁保证原子性
**第一个机制是通过总线锁保证原子性。**如果多个处理器同时对共享变量进行读改写操作 i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操 作就不是原子的操作完之后共享变量的值会和期望的不一致。举个例子如果i=1我们进行 两次i++操作我们期望的结果是3但是有可能结果是2如图所示。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0020.png">
原因可能是多个处理器同时从各自的缓存中读取变量i分别进行加1操作然后分别写入 系统内存中。那么想要保证读改写共享变量的操作是原子的就必须保证CPU1读改写共享 变量的时候CPU2不能操作缓存了该共享变量内存地址的缓存。
处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK信号当一个处理器在总线上输出此信号时其他处理器的请求将被阻塞住那么该处理器可以独占共享内存。
### 使用缓存锁保证原子性
**第二个机制是通过缓存锁定来保证原子性**。在同一时刻,我们只需保证对某个内存地址 的操作是原子性即可但总线锁定把CPU和内存之间的通信锁住了这使得锁定期间其他处 理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里那么原子操作就可以直接在处理器内部缓存中进行并不需要声明总线锁在Pentium 6和目前的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存 行中并且在Lock操作期间被锁定那么当它执行锁操作回写到内存时处理器不在总线上声言LOCK信号而是修改内部的内存地址并允许它的缓存一致性机制来保证操作的原子性因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据当其他处理器回写已被锁定的缓存行的数据时会使缓存行无效在如上图所示的例子中当CPU1修改缓存行中的i时使用了缓存锁定那么CPU2就不能同时缓存i的缓存行。
**但是有两种情况下处理器不会使用缓存锁定。**
第一种情况是当操作的数据不能被缓存在处理器内部或操作的数据跨多个缓存行cache line则处理器会调用总线锁定。
第二种情况是有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。针对以上两个机制我们通过Intel处理器提供了很多Lock前缀的指令来实现。例如位测试和修改指令BTS、BTR、BTC交换指令XADD、CMPXCHG以及其他一些操作数和逻辑指令如ADD、OR被这些指令操作的内存区域就会加锁导致其他处理器不能同时访问它。
## Java如何实现原子操作
1使用循环CAS实现原子操作
2使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁JVM实现锁的方式都用了循环CAS即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁当它退出同步块的时候使用循环CAS释放锁。
传统的锁也就是下文要说的重量级锁依赖于系统的同步函数在linux上使用`mutex`互斥锁,最底层实现依赖于`futex`,这些同步函数都涉及到用户态和内核态的切换、进程的上下文切换,成本较高。对于加了`synchronized`关键字但**运行时并没有多线程竞争,或两个线程接近于交替执行的情况**,使用传统锁机制无疑效率是会比较低的。
futex由一个内核层的队列和一个用户空间层的atomic integer构成。当获得锁时尝试cas更改integer如果integer原始值是0则修改成功该线程获得锁否则就将当期线程放入到 wait queue中即操作系统的等待队列。【及其类似于AQS的设计思想可能AQS就参考了futex的思想】

File diff suppressed because it is too large Load Diff

63
index.html Normal file
View File

@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaYouth</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/prism.css">
<!--主题-->
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
</head>
<body>
<div id="app"></div>
<!-- docsify-edit-on-github -->
<script src="//unpkg.com/docsify-edit-on-github/index.js"></script>
<script>
window.$docsify = {
name: 'JavaYouth',
repo: 'https://github.com/youthlql/JavaYouth',
maxLevel: 4,//最大支持渲染的标题层级
coverpage: true,
auto2top: true,//切换页面后是否自动跳转到页面顶部
search: {
//maxAge: 86400000, // 过期时间,单位毫秒,默认一天
paths: 'auto',
placeholder: '搜索',
noData: '找不到结果',
// 搜索标题的最大程级, 1 - 6
depth: 3,
},
// 字数统计
count: {
countable: true,
fontsize: '0.9em',
color: 'rgb(90,90,90)',
language: 'chinese'
},
plugins: [
EditOnGithubPlugin.create('https://github.com/youthlql/JavaYouth')
],
}
</script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
<!--Java代码高亮-->
<script src="//unpkg.com/prismjs/components/prism-java.js"></script>
<!--全文搜索,直接用官方提供的无法生效-->
<script src="https://cdn.bootcss.com/docsify/4.5.9/plugins/search.min.js"></script>
<!-- 复制到剪贴板 -->
<script src="//unpkg.com/docsify-copy-code"></script>
<!-- 图片缩放 -->
<script src="//unpkg.com/docsify/lib/plugins/zoom-image.js"></script>
<!-- 表情 -->
<script src="//unpkg.com/docsify/lib/plugins/emoji.js"></script>
<!-- 字数统计 -->
<script src="//unpkg.com/docsify-count/dist/countable.js"></script>
</body>
</html>