diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index dbad3e3..1abc839 100644 --- a/README.md +++ b/README.md @@ -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) \ No newline at end of file diff --git a/_coverpage.md b/_coverpage.md new file mode 100644 index 0000000..a4a7008 --- /dev/null +++ b/_coverpage.md @@ -0,0 +1,13 @@ +

+ +

+ + + +

JavaYouth

+ +[我的博客](https://youthlql.gitee.io/) +[GitHub]() +[开始阅读](#Java) + + diff --git a/docs/Computer_NetWork/计算机网络-总结.md b/docs/Computer_NetWork/计算机网络-总结.md new file mode 100644 index 0000000..aba837b --- /dev/null +++ b/docs/Computer_NetWork/计算机网络-总结.md @@ -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.0,http1.1,http2.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的有哪些 + +## 区别+应用场景 + + + +**总结:** + +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连接建立时和网络出现超时时才使用。 + + + + + + + + + +## 为什么要进行流量控制 + +一般来说,我们总是希望数据传输更快一些。但如果发送方把数据发送的过快,接收方就可能来不及接收,这就会造成数据的丢失(丢包)。 + + + + + +# TCP粘包现象原因和解决方法 + + + +## 原因 + +1.UDP协议的保护消息边界使得每一个消息都是独立的 + +2.而tcp是基于流的传输,流传输却把数据当作一串数据流,他不认为数据是一个一个的消息 + +3.发送端需要等缓冲区满才发送出去,造成粘包 + +4.接收方不及时接收缓冲区的包,造成多个包粘包 + +具体点: + +(1)发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。 + +(2)接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。 + + + +## 解决方法 + +(1)对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP程序收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满; + +(2)对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象; + +(3)由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。 + + + +## 为什么粘包需要处理? + +不是所有的粘包现象都需要处理,若传输的数据为不带结构的连续流数据(如文件传输),则不必把粘连的包分开(简称分包)。但在实际工程应用中,传输的数据一般为带结构的数据,这时就需要做分包处理。分包一般难度较大,所以尽量避免粘包 + + + + + +# 三次握手相关问题 + + + +## 过程+状态改变 + +把**补充**里面的**第二个博客**的过程背下来。(有的地方需要参考第一个博客) + + + +## 为什么三次,两次为什么不行? + +### 第一种答案 + +两次握手只能保证单向连接是畅通的。只有经过第三次握手,才能确保双向都可以接收到对方的发送的 数据。两次握手接收方这里不能确定自己的的发送是正常的,发送方的接收是正常的。 + +**具体点:** + +**三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。** + +第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常 + +第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常 + +第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常 + +所以三次握手就能确认双发收发功能都正常,缺一不可。 + + + +### 第二种答案 + +​ 一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。 + + + + + + + +## 如果已经建立了连接,但是客户端突然出现故障了怎么办? + + + + + + + +# 四次挥手相关问题 + + + +## 过程及状态改变 + +就是把**补充**里面的**第二个博客**的过程背下来。(有的地方需要参考第一个博客) + + + +## 为什么四次挥手 + +​ 因为只有在客户端和服务端都没有数据要发送的时候才能断开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 + +## 报文首部 + + + +**重要字段:** + +(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 + +(1)1. 浏览器先检查自身缓存中有没有被解析过的这个域名对应的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请求 + + + + + +## 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点的补充(如果需要举例子可以这样举) + +> 其中的“"可以是GET也可以是POST,或者其他的HTTP Method,如PUT、DELETE、OPTION……。从协议本身看,并没有什么限制说GET一定不能没有body,POST就一定不能把参放到的querystring上。因此其实可以更加自由的去利用格式。比如Elastic Search的_search api就用了带body的GET;也可以自己开发接口让POST一半的参数放在url的querystring里,另外一半放body里;你甚至还可以让所有的参数都放Header里——可以做各种各样的定制,只要请求的客户端和服务器端能够约定好 +> +> 摘自---https://www.zhihu.com/question/28586791 + + + + + +# HTTP常见响应状态码,从1xx到5xx都要说 + + + +需要记住的(下面的应该足够了) + +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个cookie,session无此限制 +占用服务器资源: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了。** + + + + + +## 非阻塞IO + +1. 当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。 +2. 从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好。用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。 +3. 虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。 +4. **所以,用户进程第一个阶段不是阻塞的,需要不断的主动询问内核数据好了没有;第二个阶段依然总是阻塞的。** + + + + + + + +## 多路复用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请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。 + + + + + +### 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请求时,数据已经到达了,用户线程一定不会被阻塞。) + + + + + +## 信号驱动IO + +1. 在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。 +2. 这个一般用于UDP中,对TCP套接口几乎是没用的,原因是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么事情 +3. 信号驱动IO放佛很像异步IO,它的第一阶段不是阻塞的。但是很遗憾,它的数据拷贝阶段(第二阶段),任然是阻塞的。 + + + + + +## 异步IO + +1. 真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,由用户线程自行读取数据、处理数据。 +2. 而在异步IO模型中,用户进程发起read操作之后,立刻就可以开始去做其它的事。 +3. 而另一方面,从**内核**的角度,当它受到一个异步读之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都 完成之后,**内核**会给用户进程发送一个信号,告诉它read操作完成了,用户线程直接使用即可。 在这整个过程中,进程完全没有被阻塞。 +4. 异步IO模型使用了Proactor设计模式实现了这一机制。**(具体怎么搞得,看上面的文章链接)** + + + + + + + + + +# 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) + diff --git a/docs/ElasticSearch/usage/ElasticSearch-入门.md b/docs/ElasticSearch/usage/ElasticSearch-入门.md new file mode 100644 index 0000000..eca1fb7 --- /dev/null +++ b/docs/ElasticSearch/usage/ElasticSearch-入门.md @@ -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的工作原理及去使用物理结构中的索引文件。 + + + +逻辑结构部分是一个倒排索引表: + +1、将要搜索的文档内容分词,所有不重复的词组成分词列表。 + +2、将搜索的文档最终以Document方式存储起来。 + +3、每个词和docment都有关联。 + +如下: + + + +现在,如果我们想搜到`quick brown`我们只需要查找包含每个词条的文档: + + + +两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 , 那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳 + +# 基本概念 + + + +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" : "Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包含 了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现个不受浏览器限制的精美界面 效果。”, + + "studymodel": "201001" + +} +``` + + + +## 搜索文档 + +1、根据课程id查询文档 + +发送:get http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000 + +使用postman测试: + + + + + +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文本字段 + +**1)text** + +字符串包括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 + } +} +``` + + + +**3)store** + +是否在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": "yyyy‐MM‐dd HH:mm:ss||yyyy‐MM‐dd" + } + } +} +``` + +插入文档: + +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": "2018‐07‐04 18:28:58" +} +``` + + + +### 综合例子 + +post:http://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": "yyyy‐MM‐dd HH:mm:ss||yyyy‐MM‐dd||epoch_millis" + } + } +} +``` + diff --git a/docs/ElasticSearch/usage/ElasticSearch-进阶.md b/docs/ElasticSearch/usage/ElasticSearch-进阶.md new file mode 100644 index 0000000..ab20eb2 --- /dev/null +++ b/docs/ElasticSearch/usage/ElasticSearch-进阶.md @@ -0,0 +1,1732 @@ +--- +title: ElasticSearch-进阶篇 +tags: + - ElasticSearch + - ELK + - 全文检索 +categories: + - ElasticSearch + - 用法 +keywords: ElasticSearch,全文检索 +description: ElasticSearch-进阶篇,ElasticSearch的一些实战用法,集成SpringBoot。 +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: 50e81c79 +date: 2020-02-08 18:06:23 +--- + + + +# 搭建工程 + +ES提供多种不同的客户端: + +1、TransportClient + +ES提供的传统客户端,官方计划8.0版本删除此客户端。 + +2、RestClient + +RestClient是官方推荐使用的,它包括两种:Java Low Level REST Client和 Java High Level REST Client。 + +ES在6.0之后提供 Java High Level REST Client, 两种客户端官方更推荐使用 Java High Level REST Client,不过当 + +前它还处于完善中,有些功能还没有。 + + + +我们采用SpringBoot2.x与ElasticSearch集成 + +## Maven依赖 + +部分依赖 + +```maven + + UTF-8 + UTF-8 + 1.8 + 6.3.2 + + + + + + + org.elasticsearch.client + transport + ${elasticsearch.version} + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + ${elasticsearch.version} + + + org.elasticsearch + elasticsearch + ${elasticsearch.version} + + + +``` + +## application.properties + +```properties +#elasticsearch配置 +anshe.elasticsearch.hostlist=${eshostlist:你的IP地址:9200} +``` + + + +## 配置类 + +```java +package com.anshe.common.config.es; + +import com.anshe.web.service.ISearchService; +import org.apache.http.HttpHost; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.transport.client.PreBuiltTransportClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.net.InetAddress; + +/** + * @author Administrator + * @version 1.0 + **/ +@Configuration +public class ElasticsearchConfig { + private static final Logger logger = LoggerFactory.getLogger(ISearchService.class); + + @Value("${anshe.elasticsearch.hostlist}") + private String hostlist; + + @Bean + public RestHighLevelClient restHighLevelClient(){ + //解析hostlist配置信息 + String[] split = hostlist.split(","); + //创建HttpHost数组,其中存放es主机和端口的配置信息 + HttpHost[] httpHostArray = new HttpHost[split.length]; + for(int i=0;i jsonMap = new HashMap<>(); + jsonMap.put("name", "spring cloud实战"); + jsonMap.put("description", "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。"); + jsonMap.put("studymodel", "201001"); + SimpleDateFormat dateFormat =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + jsonMap.put("timestamp", dateFormat.format(new Date())); + jsonMap.put("price", 5.6f); + + //创建索引创建对象 + IndexRequest indexRequest = new IndexRequest("xc_course","doc"); + //文档内容 + indexRequest.source(jsonMap); + //通过client进行http的请求 + IndexResponse indexResponse = client.index(indexRequest); + DocWriteResponse.Result result = indexResponse.getResult(); + System.out.println(result); + +} +``` + + + +## 查询文档 + +### API + +格式如下: GET /{index}/{type}/{id} + +### Java客户端 + +```java +//查询文档 +@Test +public void testGetDoc() throws IOException { + //查询请求对象 + GetRequest getRequest = new GetRequest("xc_course","doc","0fOCF2sBEYTsNRZ43I8b"); + GetResponse getResponse = client.get(getRequest); + //得到文档的内容 + Map sourceAsMap = getResponse.getSourceAsMap(); + System.out.println(sourceAsMap); +} +``` + + + +## 更新文档 + +### API + +ES更新文档的顺序是:先检索到文档、将原来的文档标记为删除、创建新文档、删除旧文档,创建新文档就会重建 + +索引。 + +通过请求Url有两种方法: + +**1、完全替换** + +Post:http://localhost:9200/xc_test/doc/3 + +```json +{ + "name": "spring cloud实战", + "description": "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。", + "studymodel": "201001", + "price": 5.6 +} +``` + +**2、局部更新** + +下边的例子是只更新price字段。 + +post: http://localhost:9200/xc_test/doc/3/_update + +```json +{ + "doc": { + "price": 66.6 + } +} +``` + + + +### Java客户端 + +使用 Client Api更新文档的方法同上边第二种局部更新方法。 + +可以指定文档的部分字段也可以指定完整的文档内容。 + +``` +//更新文档 +@Test public void updateDoc() throws IOException { + UpdateRequest updateRequest = new UpdateRequest("xc_course", "doc", "4028e581617f945f01617f9dabc40000"); + Map map = new HashMap<>(); + map.put("name", "spring cloud实战"); + updateRequest.doc(map); + UpdateResponse update = client.update(updateRequest); + RestStatus status = update.status(); + System.out.println(status); +} +``` + + + + + +## 删除文档 + +### API + +1、根据id删除,格式如下: + +DELETE /{index}/{type}/{id} + +2、搜索匹配删除,将搜索出来的记录删除,格式如下: + +POST /{index}/{type}/_delete_by_query + +下边是搜索条件例子: + +```json +{ + "query": { + "term": { + "studymodel": "201001" + } + } +} +``` + +上边例子的搜索匹配删除会将studymodel为201001的记录全部删除 + +### Java客户端 + +```java +//根据id删除文档 +@Test +public void testDelDoc() throws IOException { + //删除文档id + String id = "eqP_amQBKsGOdwJ4fHiC"; + //删除索引请求对象 + DeleteRequest deleteRequest = new DeleteRequest("xc_course","doc",id); + //响应对象 + DeleteResponse deleteResponse = client.delete(deleteRequest); + //获取响应结果 + DocWriteResponse.Result result = deleteResponse.getResult(); + System.out.println(result); +} +``` + +搜索匹配删除还没有具体的api,可以采用先搜索出文档id,根据文档id删除。 + + + +# -----下面是DSL搜索的内容----- + +# DSL搜索环境准备 + +## 创建映射 + +创建xc_course索引库。 + +创建如下映射 + +post:http://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": "yyyy‐MM‐dd HH:mm:ss||yyyy‐MM‐dd||epoch_millis" + } + } +} +``` + + + + + +## 插入原始数据 + +向xc_course/doc中插入以下数据: + +```json +http://localhost:9200/xc_course/doc/1 +{ + "name": "Bootstrap开发", + "description": "Bootstrap是由Twitter推出的一个前台页面开发框架,是一个非常流行的开发框架,此框架集成了 多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松 的实现一个不受浏览器限制的精美界面效果。", + "studymodel": "201002", + "price": 38.6, + "timestamp": "2018‐04‐25 19:11:35", + "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg" +} + + +http://localhost:9200/xc_course/doc/2 +{ + "name": "java编程基础", + "description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。", + "studymodel": "201001", + "price": 68.6, + "timestamp": "2018‐03‐25 19:11:35", + "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg" +} + + +http://localhost:9200/xc_course/doc/3 +{ + "name": "spring开发基础", + "description": "spring 在java领域非常流行,java程序员都在用。", + "studymodel": "201001", + "price": 88.6, + "timestamp": "2018‐02‐24 19:11:35", + "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg" +} + +``` + + + + + +> DSL(Domain Specifific Language)是ES提出的基于json的搜索方式,在搜索时传入特定的json格式的数据来完成不 同的搜索需求。 DSL比URI搜索方式功能强大,在项目中建议使用DSL方式来完成搜索。 + + + + + +# 查询所有文档 + +## API + +查询所有索引库的文档。 + +发送:post http://localhost:9200/_search + +查询指定索引库指定类型下的文档。(通过使用此方法) + +发送:post http://localhost:9200/xc_course/doc/_search + +```json +{ + "query": { + "match_all": {} + }, + "_source": [ + "name", + "studymodel" + ] +} +``` + +_source:source源过虑设置,指定结果中所包括的字段有哪些。 + +**结果说明:** + +took:本次操作花费的时间,单位为毫秒。 + +timed_out:请求是否超时 + +_shards:说明本次操作共搜索了哪些分片 + +hits:搜索命中的记录 + +hits.total : 符合条件的文档总数 hits.hits :匹配度较高的前N个文档 + +hits.max_score:文档匹配得分,这里为最高分 + +_score:每个文档都有一个匹配度得分,按照降序排列。 + +_source:显示了文档的原始内容。 + +## Java客户端 + +```java + @Autowired + RestHighLevelClient client; + + @Autowired + RestClient restClient; + +//搜索全部记录 + @Test + public void testSearchAll() throws IOException, ParseException { + //搜索请求对象 + SearchRequest searchRequest = new SearchRequest("xc_course"); + //指定类型 + searchRequest.types("doc"); + //搜索源构建对象 + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + //搜索方式 + //matchAllQuery搜索全部 + searchSourceBuilder.query(QueryBuilders.matchAllQuery()); + //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 + searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); + //向搜索请求对象中设置搜索源 + searchRequest.source(searchSourceBuilder); + //执行搜索,向ES发起http请求 + SearchResponse searchResponse = client.search(searchRequest); + //搜索结果 + SearchHits hits = searchResponse.getHits(); + //匹配到的总记录数 + long totalHits = hits.getTotalHits(); + //得到匹配度高的文档 + SearchHit[] searchHits = hits.getHits(); + //日期格式化对象 +// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + for(SearchHit hit:searchHits){ + //文档的主键 + String id = hit.getId(); + //源文档内容 + Map sourceAsMap = hit.getSourceAsMap(); + String name = (String) sourceAsMap.get("name"); + //由于前边设置了源文档字段过虑,这时description是取不到的 + String description = (String) sourceAsMap.get("description"); + //学习模式 + String studymodel = (String) sourceAsMap.get("studymodel"); + //价格 + Double price = (Double) sourceAsMap.get("price"); + //日期 + Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); + System.out.println(name); + System.out.println(studymodel); + System.out.println(description); + } + + } +``` + + + +# 分页查询 + +## API + +ES支持分页查询,传入两个参数:from和size。 + +form:表示起始文档的下标,从0开始。 + +size:查询的文档数量。 + +发送:post http://localhost:9200/xc_course/doc/_search + +```json +{ + "from": 0, + "size": 1, + "query": { + "match_all": {} + }, + "_source": [ + "name", + "studymodel" + ] +} +``` + + + +## Java客户端 + +```java +//分页查询 +@Test +public void testSearchPage() throws IOException, ParseException { + //搜索请求对象 + SearchRequest searchRequest = new SearchRequest("xc_course"); + //指定类型 + searchRequest.types("doc"); + //搜索源构建对象 + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + //设置分页参数 + //页码 + int page = 1; + //每页记录数 + int size = 1; + //计算出记录起始下标 + int from = (page-1)*size; + searchSourceBuilder.from(from);//起始记录下标,从0开始 + searchSourceBuilder.size(size);//每页显示的记录数 + //搜索方式 + //matchAllQuery搜索全部 + searchSourceBuilder.query(QueryBuilders.matchAllQuery()); + //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 + searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); + //向搜索请求对象中设置搜索源 + searchRequest.source(searchSourceBuilder); + //执行搜索,向ES发起http请求 + SearchResponse searchResponse = client.search(searchRequest); + //搜索结果 + SearchHits hits = searchResponse.getHits(); + //匹配到的总记录数 + long totalHits = hits.getTotalHits(); + //得到匹配度高的文档 + SearchHit[] searchHits = hits.getHits(); + //日期格式化对象 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + for(SearchHit hit:searchHits){ + //文档的主键 + String id = hit.getId(); + //源文档内容 + Map sourceAsMap = hit.getSourceAsMap(); + String name = (String) sourceAsMap.get("name"); + //由于前边设置了源文档字段过虑,这时description是取不到的 + String description = (String) sourceAsMap.get("description"); + //学习模式 + String studymodel = (String) sourceAsMap.get("studymodel"); + //价格 + Double price = (Double) sourceAsMap.get("price"); + //日期 + Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); + System.out.println(name); + System.out.println(studymodel); + System.out.println(description); + } + +} +``` + + + +# Term Query + +## API + +Term Query为精确查询,在搜索时会整体匹配关键字,不再将关键字分词。 + +发送:post http://localhost:9200/xc_course/doc/_search + +```java +{ + "query": { + "term": { + "name": "spring" + } + }, + "_source": [ + "name", + "studymodel" + ] +} +``` + +上边的搜索会查询name包括“spring”这个词的文档。 + + + +## Java客户端 + +```java +//TermQuery +@Test +public void testTermQuery() throws IOException, ParseException { + //搜索请求对象 + SearchRequest searchRequest = new SearchRequest("xc_course"); + //指定类型 + searchRequest.types("doc"); + //搜索源构建对象 + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + //设置分页参数 + //页码 + int page = 1; + //每页记录数 + int size = 1; + //计算出记录起始下标 + int from = (page-1)*size; + searchSourceBuilder.from(from);//起始记录下标,从0开始 + searchSourceBuilder.size(size);//每页显示的记录数 + //搜索方式 + //termQuery + searchSourceBuilder.query(QueryBuilders.termQuery("name","spring")); + //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 + searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); + //向搜索请求对象中设置搜索源 + searchRequest.source(searchSourceBuilder); + //执行搜索,向ES发起http请求 + SearchResponse searchResponse = client.search(searchRequest); + //搜索结果 + SearchHits hits = searchResponse.getHits(); + //匹配到的总记录数 + long totalHits = hits.getTotalHits(); + //得到匹配度高的文档 + SearchHit[] searchHits = hits.getHits(); + //日期格式化对象 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + for(SearchHit hit:searchHits){ + //文档的主键 + String id = hit.getId(); + //源文档内容 + Map sourceAsMap = hit.getSourceAsMap(); + String name = (String) sourceAsMap.get("name"); + //由于前边设置了源文档字段过虑,这时description是取不到的 + String description = (String) sourceAsMap.get("description"); + //学习模式 + String studymodel = (String) sourceAsMap.get("studymodel"); + //价格 + Double price = (Double) sourceAsMap.get("price"); + //日期 + Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); + System.out.println(name); + System.out.println(studymodel); + System.out.println(description); + } + +} +``` + + + +# 根据id精确匹配 + +## API + +ES提供根据多个id值匹配的方法: + +测试: + +post: http://127.0.0.1:9200/xc_course/doc/_search + +```json +{ + "query": { + "ids": { + "type": "doc", + "values": [ + "3", + "4", + "100" + ] + } + } +} +``` + + + +## Java客户端 + +```java +//根据id查询 +@Test +public void testTermQueryByIds() throws IOException, ParseException { + //搜索请求对象 + SearchRequest searchRequest = new SearchRequest("xc_course"); + //指定类型 + searchRequest.types("doc"); + //搜索源构建对象 + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + //搜索方式 + //根据id查询 + //定义id + String[] ids = new String[]{"1","2"}; + searchSourceBuilder.query(QueryBuilders.termsQuery("_id",ids)); + //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 + searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); + //向搜索请求对象中设置搜索源 + searchRequest.source(searchSourceBuilder); + //执行搜索,向ES发起http请求 + SearchResponse searchResponse = client.search(searchRequest); + //搜索结果 + SearchHits hits = searchResponse.getHits(); + //匹配到的总记录数 + long totalHits = hits.getTotalHits(); + //得到匹配度高的文档 + SearchHit[] searchHits = hits.getHits(); + //日期格式化对象 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + for(SearchHit hit:searchHits){ + //文档的主键 + String id = hit.getId(); + //源文档内容 + Map sourceAsMap = hit.getSourceAsMap(); + String name = (String) sourceAsMap.get("name"); + //由于前边设置了源文档字段过虑,这时description是取不到的 + String description = (String) sourceAsMap.get("description"); + //学习模式 + String studymodel = (String) sourceAsMap.get("studymodel"); + //价格 + Double price = (Double) sourceAsMap.get("price"); + //日期 + Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); + System.out.println(name); + System.out.println(studymodel); + System.out.println(description); + } + +} +``` + + + + + +# match Query + +## API + +match Query即全文检索,它的搜索方式是先将搜索字符串分词,再使用各各词条从索引中搜索。 + +match query与Term query区别是match query在搜索前先将搜索关键字分词,再拿各各词语去索引中搜索。 + +发送:post http://localhost:9200/xc_course/doc/_search + +```json +{ + "query": { + "match": { + "description": { + "query": "spring开发", + "operator": "or" + } + } + } +} +``` + +query:搜索的关键字,对于英文关键字如果有多个单词则中间要用半角逗号分隔,而对于中文关键字中间可以用 + +逗号分隔也可以不用。 + +operator:or 表示 只要有一个词在文档中出现则就符合条件,and表示每个词都在文档中出现则才符合条件。 + +上边的搜索的执行过程是: + +1、将“spring开发”分词,分为spring、开发两个词 + +2、再使用spring和开发两个词去匹配索引中搜索。 + +3、由于设置了operator为or,只要有一个词匹配成功则就返回该文档。 + + + +## Java客户端 + +```java +//MatchQuery +@Test +public void testMatchQuery() throws IOException, ParseException { + //搜索请求对象 + SearchRequest searchRequest = new SearchRequest("xc_course"); + //指定类型 + searchRequest.types("doc"); + //搜索源构建对象 + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + + //搜索方式 + //MatchQuery + searchSourceBuilder.query(QueryBuilders.matchQuery("description","spring开发框架") + .minimumShouldMatch("80%")); + //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 + searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); + //向搜索请求对象中设置搜索源 + searchRequest.source(searchSourceBuilder); + //执行搜索,向ES发起http请求 + SearchResponse searchResponse = client.search(searchRequest); + //搜索结果 + SearchHits hits = searchResponse.getHits(); + //匹配到的总记录数 + long totalHits = hits.getTotalHits(); + //得到匹配度高的文档 + SearchHit[] searchHits = hits.getHits(); + //日期格式化对象 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + for(SearchHit hit:searchHits){ + //文档的主键 + String id = hit.getId(); + //源文档内容 + Map sourceAsMap = hit.getSourceAsMap(); + String name = (String) sourceAsMap.get("name"); + //由于前边设置了源文档字段过虑,这时description是取不到的 + String description = (String) sourceAsMap.get("description"); + //学习模式 + String studymodel = (String) sourceAsMap.get("studymodel"); + //价格 + Double price = (Double) sourceAsMap.get("price"); + //日期 + Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); + System.out.println(name); + System.out.println(studymodel); + System.out.println(description); + } + +} +``` + + + +# multi Query + +## API + +**1、基本使用** + +上边学习的termQuery和matchQuery一次只能匹配一个Field,本节学习multiQuery,一次可以匹配多个字段。 + +单项匹配是在一个fifield中去匹配,多项匹配是拿关键字去多个Field中匹配。 + +例子: + +发送:post http://localhost:9200/xc_course/doc/_search + +拿关键字 “spring css”去匹配name 和description字段。 + +```json +{ + "query": { + "multi_match": { + "query": "spring css", + "minimum_should_match": "50%", + "fields": [ + "name", + "description" + ] + } + } +} +``` + + + +**2、提升boost** + +匹配多个字段时可以提升字段的boost(权重)来提高得分 + +例子: + +提升boost之前,执行下边的查询: + +```json +{ + "query": { + "multi_match": { + "query": "spring框架", + "minimum_should_match": "50%", + "fields": [ + "name", + "description" + ] + } + } +} +``` + +通过查询发现Bootstrap排在前边。 + +提升boost,通常关键字匹配上name的权重要比匹配上description的权重高,这里可以对name的权重提升 + +```json +{ + "query": { + "multi_match": { + "query": "spring框架", + "minimum_should_match": "50%", + "fields": [ + "name^10", + "description" + ] + } + } +} +``` + +“name^10” 表示权重提升10倍,执行上边的查询,发现name中包括spring关键字的文档排在前边。 + + + +## Java客户端 + +```java +//MultiMatchQuery +@Test +public void testMultiMatchQuery() throws IOException, ParseException { + //搜索请求对象 + SearchRequest searchRequest = new SearchRequest("xc_course"); + //指定类型 + searchRequest.types("doc"); + //搜索源构建对象 + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + + //搜索方式 + //MultiMatchQuery + searchSourceBuilder.query(QueryBuilders.multiMatchQuery("spring css","name","description") + .minimumShouldMatch("50%") + .field("name",10)); + //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 + searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); + //向搜索请求对象中设置搜索源 + searchRequest.source(searchSourceBuilder); + //执行搜索,向ES发起http请求 + SearchResponse searchResponse = client.search(searchRequest); + //搜索结果 + SearchHits hits = searchResponse.getHits(); + //匹配到的总记录数 + long totalHits = hits.getTotalHits(); + //得到匹配度高的文档 + SearchHit[] searchHits = hits.getHits(); + //日期格式化对象 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + for(SearchHit hit:searchHits){ + //文档的主键 + String id = hit.getId(); + //源文档内容 + Map sourceAsMap = hit.getSourceAsMap(); + String name = (String) sourceAsMap.get("name"); + //由于前边设置了源文档字段过虑,这时description是取不到的 + String description = (String) sourceAsMap.get("description"); + //学习模式 + String studymodel = (String) sourceAsMap.get("studymodel"); + //价格 + Double price = (Double) sourceAsMap.get("price"); + //日期 + Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); + System.out.println(name); + System.out.println(studymodel); + System.out.println(description); + } + +} +``` + + + +# 布尔查询 + +## API + +布尔查询对应于Lucene的BooleanQuery查询,实现将多个查询组合起来。 + +- 三个参数: + + - must:文档必须匹配must所包括的查询条件,相当于 “AND” + + - should:文档应该匹配should所包括的查询条件其中的一个或多个,相当于 "OR" + + - must_not:文档不能匹配must_not所包括的该查询条件,相当于“NOT” + +分别使用must、should、must_not测试下边的查询: + +发送:POST http://localhost:9200/xc_course/doc/_search + +```json +{ + "_source": [ + "name", + "studymodel", + "description" + ], + "from": 0, + "size": 1, + "query": { + "bool": { + "must": [ + { + "multi_match": { + "query": "spring框架", + "minimum_should_match": "50%", + "fields": [ + "name^10", + "description" + ] + } + }, + { + "term": { + "studymodel": "201001" + } + } + ] + } + } +} +``` + +must:表示必须,多个查询条件必须都满足。(通常使用must) + +should:表示或者,多个查询条件只要有一个满足即可。 + +must_not:表示非。 + + + +## Java客户端 + +```java +//BoolQuery其实是一个过滤搜索 +@Test +public void testBoolQuery() throws IOException, ParseException { + //搜索请求对象 + SearchRequest searchRequest = new SearchRequest("xc_course"); + //指定类型 + searchRequest.types("doc"); + //搜索源构建对象 + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + + //boolQuery搜索方式 + //先定义一个MultiMatchQuery + MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("spring css", "name", "description") + .minimumShouldMatch("50%") + .field("name", 10); + //再定义一个termQuery + TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("studymodel", "201001"); + + //定义一个boolQuery + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); + boolQueryBuilder.must(multiMatchQueryBuilder); + boolQueryBuilder.must(termQueryBuilder); + + searchSourceBuilder.query(boolQueryBuilder); + //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 + searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); + //向搜索请求对象中设置搜索源 + searchRequest.source(searchSourceBuilder); + //执行搜索,向ES发起http请求 + SearchResponse searchResponse = client.search(searchRequest); + //搜索结果 + SearchHits hits = searchResponse.getHits(); + //匹配到的总记录数 + long totalHits = hits.getTotalHits(); + //得到匹配度高的文档 + SearchHit[] searchHits = hits.getHits(); + //日期格式化对象 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + for(SearchHit hit:searchHits){ + //文档的主键 + String id = hit.getId(); + //源文档内容 + Map sourceAsMap = hit.getSourceAsMap(); + String name = (String) sourceAsMap.get("name"); + //由于前边设置了源文档字段过虑,这时description是取不到的 + String description = (String) sourceAsMap.get("description"); + //学习模式 + String studymodel = (String) sourceAsMap.get("studymodel"); + //价格 + Double price = (Double) sourceAsMap.get("price"); + //日期 + Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); + System.out.println(name); + System.out.println(studymodel); + System.out.println(description); + } + +} +``` + + + + + +# 过虑器 + +## API + +​ 过虑是针对搜索的结果进行过虑,过虑器主要判断的是文档是否匹配,不去计算和判断文档的匹配度得分,所以过 虑器性能比查询要高,且方便缓存,推荐尽量使用过虑器去实现查询或者过虑器和查询共同使用。 过虑器在布尔查询中使用,下边是在搜索结果的基础上进行过虑: + +```json +{ + "_source": [ + "name", + "studymodel", + "description", + "price" + ], + "query": { + "bool": { + "must": [ + { + "multi_match": { + "query": "spring框架", + "minimum_should_match": "50%", + "fields": [ + "name^10", + "description" + ] + } + } + ], + "filter": [ + { + "term": { + "studymodel": "201001" + } + }, + { + "range": { + "price": { + "gte": 60, + "lte": 100 + } + } + } + ] + } + } +} +``` + +range:范围过虑,保留大于等于60 并且小于等于100的记录。 + +term:项匹配过虑,保留studymodel等于"201001"的记录。 + +注意:range和term一次只能对一个Field设置范围过虑。 + + + +## Java客户端 + +```java +//filter +@Test +public void testFilter() throws IOException, ParseException { + //搜索请求对象 + SearchRequest searchRequest = new SearchRequest("xc_course"); + //指定类型 + searchRequest.types("doc"); + //搜索源构建对象 + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + + //boolQuery搜索方式 + //先定义一个MultiMatchQuery + MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("spring css", "name", "description") + .minimumShouldMatch("50%") + .field("name", 10); + + //定义一个boolQuery + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); + boolQueryBuilder.must(multiMatchQueryBuilder); + //定义过虑器 + boolQueryBuilder.filter(QueryBuilders.termQuery("studymodel","201001")); + boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(90).lte(100)); + + searchSourceBuilder.query(boolQueryBuilder); + //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 + searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); + //向搜索请求对象中设置搜索源 + searchRequest.source(searchSourceBuilder); + //执行搜索,向ES发起http请求 + SearchResponse searchResponse = client.search(searchRequest); + //搜索结果 + SearchHits hits = searchResponse.getHits(); + //匹配到的总记录数 + long totalHits = hits.getTotalHits(); + //得到匹配度高的文档 + SearchHit[] searchHits = hits.getHits(); + //日期格式化对象 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + for(SearchHit hit:searchHits){ + //文档的主键 + String id = hit.getId(); + //源文档内容 + Map sourceAsMap = hit.getSourceAsMap(); + String name = (String) sourceAsMap.get("name"); + //由于前边设置了源文档字段过虑,这时description是取不到的 + String description = (String) sourceAsMap.get("description"); + //学习模式 + String studymodel = (String) sourceAsMap.get("studymodel"); + //价格 + Double price = (Double) sourceAsMap.get("price"); + //日期 + Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); + System.out.println(name); + System.out.println(studymodel); + System.out.println(description); + } + +} +``` + + + + + + + +# 排序 + +## API + +可以在字段上添加一个或多个排序,支持在keyword、date、flfloat等类型上添加,text类型的字段上不允许添加排 + +序。 + +发送 POST http://localhost:9200/xc_course/doc/_search + +过虑0--10元价格范围的文档,并且对结果进行排序,先按studymodel降序,再按价格升序 + +```json +{ + "_source": [ + "name", + "studymodel", + "description", + "price" + ], + "query": { + "bool": { + "filter": [ + { + "range": { + "price": { + "gte": 0, + "lte": 100 + } + } + } + ] + } + }, + "sort": [ + { + "studymodel": "desc" + }, + { + "price": "asc" + } + ] +} +``` + + + +## Java客户端 + +```java +//Sort +@Test +public void testSort() throws IOException, ParseException { + //搜索请求对象 + SearchRequest searchRequest = new SearchRequest("xc_course"); + //指定类型 + searchRequest.types("doc"); + //搜索源构建对象 + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + + //boolQuery搜索方式 + //定义一个boolQuery + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); + //定义过虑器 + boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(0).lte(100)); + + searchSourceBuilder.query(boolQueryBuilder); + //添加排序 + searchSourceBuilder.sort("studymodel", SortOrder.DESC); + searchSourceBuilder.sort("price", SortOrder.ASC); + //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 + searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); + //向搜索请求对象中设置搜索源 + searchRequest.source(searchSourceBuilder); + //执行搜索,向ES发起http请求 + SearchResponse searchResponse = client.search(searchRequest); + //搜索结果 + SearchHits hits = searchResponse.getHits(); + //匹配到的总记录数 + long totalHits = hits.getTotalHits(); + //得到匹配度高的文档 + SearchHit[] searchHits = hits.getHits(); + //日期格式化对象 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + for(SearchHit hit:searchHits){ + //文档的主键 + String id = hit.getId(); + //源文档内容 + Map sourceAsMap = hit.getSourceAsMap(); + String name = (String) sourceAsMap.get("name"); + //由于前边设置了源文档字段过虑,这时description是取不到的 + String description = (String) sourceAsMap.get("description"); + //学习模式 + String studymodel = (String) sourceAsMap.get("studymodel"); + //价格 + Double price = (Double) sourceAsMap.get("price"); + //日期 + Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); + System.out.println(name); + System.out.println(studymodel); + System.out.println(description); + } + +} +``` + + + + + +# 高亮显示 + +## API + +高亮显示可以将搜索结果一个或多个字突出显示,以便向用户展示匹配关键字的位置。 + +在搜索语句中添加highlight即可实现,如下: + +Post: http://127.0.0.1:9200/xc_course/doc/_search + +```json +{ + "_source": [ + "name", + "studymodel", + "description", + "price" + ], + "query": { + "bool": { + "must": [ + { + "multi_match": { + "query": "开发框架", + "minimum_should_match": "50%", + "fields": [ + "name^10", + "description" + ], + "type": "best_fields" + } + } + ], + "filter": [ + { + "range": { + "price": { + "gte": 0, + "lte": 100 + } + } + } + ] + } + }, + "sort": [ + { + "price": "asc" + } + ], + "highlight": { + "pre_tags": [ + "" + ], + "post_tags": [ + "" + ], + "fields": { + "name": {}, + "description": {} + } + } +} +``` + + + +## Java客户端 + +```java + //Highlight + @Test + public void testHighlight() throws IOException, ParseException { + //搜索请求对象 + SearchRequest searchRequest = new SearchRequest("xc_course"); + //指定类型 + searchRequest.types("doc"); + //搜索源构建对象 + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + + //boolQuery搜索方式 + //先定义一个MultiMatchQuery + MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("开发框架", "name", "description") + .minimumShouldMatch("50%") + .field("name", 10); + + //定义一个boolQuery + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); + boolQueryBuilder.must(multiMatchQueryBuilder); + //定义过虑器 + boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(0).lte(100)); + + searchSourceBuilder.query(boolQueryBuilder); + //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 + searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); + + //设置高亮 + HighlightBuilder highlightBuilder = new HighlightBuilder(); + highlightBuilder.preTags(""); + highlightBuilder.postTags(""); + highlightBuilder.fields().add(new HighlightBuilder.Field("name")); + highlightBuilder.fields().add(new HighlightBuilder.Field("description")); + searchSourceBuilder.highlighter(highlightBuilder); + + //向搜索请求对象中设置搜索源 + searchRequest.source(searchSourceBuilder); + //执行搜索,向ES发起http请求 + SearchResponse searchResponse = client.search(searchRequest); + //搜索结果 + SearchHits hits = searchResponse.getHits(); + //匹配到的总记录数 + long totalHits = hits.getTotalHits(); + //得到匹配度高的文档 + SearchHit[] searchHits = hits.getHits(); + //日期格式化对象 +// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS Z"); + for(SearchHit hit:searchHits){ + //文档的主键 + String id = hit.getId(); + //源文档内容 + Map sourceAsMap = hit.getSourceAsMap(); + //源文档的name字段内容 + String name = (String) sourceAsMap.get("name"); + //取出高亮字段 + Map highlightFields = hit.getHighlightFields(); + if(highlightFields!=null){ + //取出name高亮字段 + HighlightField nameHighlightField = highlightFields.get("name"); + if(nameHighlightField!=null){ + Text[] fragments = nameHighlightField.getFragments(); + StringBuffer stringBuffer = new StringBuffer(); + for(Text text:fragments){ + stringBuffer.append(text); + } + name = stringBuffer.toString(); + } + } + + //由于前边设置了源文档字段过虑,这时description是取不到的 + String description = (String) sourceAsMap.get("description"); + //学习模式 + String studymodel = (String) sourceAsMap.get("studymodel"); + //价格 + Double price = (Double) sourceAsMap.get("price"); + //日期 + Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); + System.out.println(name); + System.out.println(studymodel); + System.out.println(description); + } + + } +``` \ No newline at end of file diff --git a/docs/Java/Basis/Java8_New_Features/Java8新特性.md b/docs/Java/Basis/Java8_New_Features/Java8新特性.md new file mode 100644 index 0000000..9d277da --- /dev/null +++ b/docs/Java/Basis/Java8_New_Features/Java8新特性.md @@ -0,0 +1,1531 @@ +# Java8新特性纵览 + +> 本篇文章只讲解比较重要的 + + + + + +# Lambda表达式 + +## 为什么使用Lambda表达式? + +- Lambda 是一个**匿名函数**,我们可以把 Lambda 表达式理解为是**一段可以传递的代码**(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。 +- 在Java8之后的很多源码里用到了Lambda表达式,不学的话可能看不懂源码。 + + + +## 简单使用 + +```java + @Test + public void test1(){ + //原始写法 + Runnable r1 = new Runnable() { + @Override + public void run() { + System.out.println("我爱北京天安门"); + } + }; + + r1.run(); + + System.out.println("***********************"); + + //lambda表达式,可以表达一样的意思 + Runnable r2 = () -> System.out.println("我爱北京故宫"); + + r2.run(); + } + + + @Test + public void test2(){ + + Comparator com1 = new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return Integer.compare(o1,o2); + } + }; + + int compare1 = com1.compare(12,21); + System.out.println(compare1); + + System.out.println("***********************"); + + //Lambda表达式的写法 + Comparator com2 = (o1,o2) -> Integer.compare(o1,o2); + + int compare2 = com2.compare(32,21); + System.out.println(compare2); + + + System.out.println("***********************"); + //方法引用 + Comparator com3 = Integer :: compare; + + int compare3 = com3.compare(32,21); + System.out.println(compare3); + } +``` + + + +## Lambda语法规则 + +```Java +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.function.Consumer; + +/** + * Lambda表达式的使用 + *

+ * 1.举例: (o1,o2) -> Integer.compare(o1,o2); + * 2.格式: + * -> :lambda操作符 或 箭头操作符 + * ->左边:lambda形参列表 (其实就是接口中的抽象方法的形参列表) + * ->右边:lambda体 (其实就是重写的抽象方法的方法体) + *

+ * 3. Lambda表达式的使用:(分为6种情况介绍) + *

+ * 总结: + * ->左边:lambda形参列表的参数类型可以省略(类型推断);如果lambda形参列表只有一个参数,其一对()也可以省略 + * ->右边:lambda体应该使用一对{}包裹;如果lambda体只有一条执行语句(可能是return语句),省略这一对{}和return关键字 + *

+ * 4.Lambda表达式的本质:作为函数式接口的实例 + *

+ * 5. 如果一个接口中,只声明了一个抽象方法,则此接口就称为函数式接口。我们可以在一个接口上使用 @FunctionalInterface 注解, + * 这样做可以检查它是否是一个函数式接口。 + *

+ * 6. 所以以前用匿名实现类表示的现在都可以用Lambda表达式来写。 + */ +public class LambdaTest1 { + //语法格式一:无参,无返回值 + @Test + public void test1() { + Runnable r1 = new Runnable() { + @Override + public void run() { + System.out.println("我爱北京天安门"); + } + }; + + r1.run(); + + System.out.println("***********************"); + + Runnable r2 = () -> { + System.out.println("我爱北京故宫"); + }; + + r2.run(); + } + + //语法格式二:Lambda 需要一个参数,但是没有返回值。 + @Test + public void test2() { + + Consumer con = new Consumer() { + @Override + public void accept(String s) { + System.out.println(s); + } + }; + con.accept("谎言和誓言的区别是什么?"); + + System.out.println("*******************"); + + Consumer con1 = (String s) -> { + System.out.println(s); + }; + con1.accept("一个是听得人当真了,一个是说的人当真了"); + + } + + //语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断” + @Test + public void test3() { + + Consumer con1 = (String s) -> { + System.out.println(s); + }; + con1.accept("一个是听得人当真了,一个是说的人当真了"); + + System.out.println("*******************"); + + Consumer con2 = (s) -> { + System.out.println(s); + }; + con2.accept("一个是听得人当真了,一个是说的人当真了"); + + } + + @Test + public void test4() { + + ArrayList list = new ArrayList<>();//类型推断 + + int[] arr = {1, 2, 3};//类型推断 + + } + + //语法格式四:Lambda 若只需要一个参数时,参数的小括号可以省略 + @Test + public void test5() { + Consumer con1 = (s) -> { + System.out.println(s); + }; + con1.accept("一个是听得人当真了,一个是说的人当真了"); + + System.out.println("*******************"); + + Consumer con2 = s -> { + System.out.println(s); + }; + con2.accept("一个是听得人当真了,一个是说的人当真了"); + + + } + + //语法格式五:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值 + @Test + public void test6() { + + Comparator com1 = new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + System.out.println(o1); + System.out.println(o2); + return o1.compareTo(o2); + } + }; + + System.out.println(com1.compare(12, 21)); + + System.out.println("*****************************"); + Comparator com2 = (o1, o2) -> { + System.out.println(o1); + System.out.println(o2); + return o1.compareTo(o2); + }; + + System.out.println(com2.compare(12, 6)); + + + } + + //语法格式六:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略 + @Test + public void test7() { + + Comparator com1 = (o1, o2) -> { + return o1.compareTo(o2); + }; + + System.out.println(com1.compare(12, 6)); + + System.out.println("*****************************"); + + Comparator com2 = (o1, o2) -> o1.compareTo(o2); + + System.out.println(com2.compare(12, 21)); + + } + + @Test + public void test8() { + Consumer con1 = s -> { + System.out.println(s); + }; + con1.accept("一个是听得人当真了,一个是说的人当真了"); + + System.out.println("*****************************"); + + Consumer con2 = s -> System.out.println(s); + + con2.accept("一个是听得人当真了,一个是说的人当真了"); + + } + +} +``` + + + +# 函数式接口 + + + +## 什么是函数式(Functional)接口 + +- 只包含一个抽象方法的接口,称为**函数式接口**。 + +- 你可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)。 + +- 我们可以在一个接口上使用 **@FunctionalInterface** 注解,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。 + +- 在java.util.function包下定义了Java 8 的丰富的函数式接口 + + + +## 如何理解函数式接口 + +- Java从诞生日起就是一直倡导“一切皆对象”,在Java里面面向对象(OOP)编程是一切。但是随着python、scala等语言的兴起和新技术的挑战,Java不得不做出调整以便支持更加广泛的技术要求,也即java不但可以支持OOP还可以支持OOF(面向函数编程) +- 在函数式编程语言当中,函数被当做一等公民对待。在将函数作为一等公民的编程语言中,Lambda表达式的类型是函数。但是在Java8中,有所不同。在Java8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的对象类型——函数式接口。 +- 简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例。这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示。 +- 所以以前用匿名实现类表示的现在都可以用Lambda表达式来写。 + + + +## Java内置函数式接口 + + + +**核心函数式接口** + + + + + +**其它函数式接口** + + + + + + + +**Consumer** + +```java + @Test + public void test1(){ + + happyTime(500, new Consumer() { + @Override + public void accept(Double aDouble) { + System.out.println("学习太累了,去天上人间买了瓶矿泉水,价格为:" + aDouble); + } + }); + + System.out.println("********************"); + + happyTime(400,money -> System.out.println("学习太累了,去天上人间喝了口水,价格为:" + money)); + } + + public void happyTime(double money, Consumer con){ + con.accept(money); + } +``` + + + +**结果:** + +```Java +学习太累了,去天上人间买了瓶矿泉水,价格为:500.0 +******************** +学习太累了,去天上人间喝了口水,价格为:400.0 + +Process finished with exit code 0 +``` + + + +**Predicate** + +```java +@Test + public void test2(){ + List list = Arrays.asList("北京","南京","天津","东京","西京","普京"); + + List filterStrs = filterString(list, new Predicate() { + @Override + public boolean test(String s) {//这里是定义一个校验规则 + return s.contains("京"); + } + }); + + System.out.println(filterStrs); + + //用lambda表达式会很简单 + List filterStrs1 = filterString(list,s -> s.contains("京")); + System.out.println(filterStrs1); + } + + //根据给定的规则,过滤集合中的字符串。此规则由Predicate的方法决定 + public List filterString(List list, Predicate pre){ + + ArrayList filterList = new ArrayList<>(); + + for(String s : list){ + if(pre.test(s)){ + filterList.add(s); + } + } + + return filterList; + + } + +``` + + + +**结果:** + +``` +[北京, 南京, 东京, 西京, 普京] +[北京, 南京, 东京, 西京, 普京] + +Process finished with exit code 0 +``` + + + +## 自定义函数式接口 + +```java +/** + * 自定义函数式接口 + * 只是说加上@FunctionalInterface之后可以校验 + */ +@FunctionalInterface +public interface MyFunInterface { + + public T getValue(T t); + +} +``` + + + +```java +public class Test { + + public static void main(String[] args) { + //这个方法的第一个参数是lambda表达式,相当于是实例化了那个函数式接口 + String s = toUpperString(str -> str.toUpperCase(), "abcd"); + System.out.println(s); + } + + public static String toUpperString(MyFunInterface mf,String str){ + return mf.getValue(str); + } +} +``` + + + +# 方法引用 + +- 当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用! + +- 方法引用可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖。 + +- 要求:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致! + +- 格式:使用操作符 “::” 将类(或对象) 与 方法名分隔开来。 + +- 如下三种主要使用情况: + + - 对象 :: 实例方法名 + + - 类 :: 静态方法名 + + - 类 :: 实例方法名 + + + +我们直接拿例子来说明情况,先提前准备两个类: + +```Java + +public class Employee { + + private int id; + private String name; + private int age; + private double salary; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public double getSalary() { + return salary; + } + + public void setSalary(double salary) { + this.salary = salary; + } + + public Employee() { + System.out.println("Employee()....."); + } + + public Employee(int id) { + this.id = id; + System.out.println("Employee(int id)....."); + } + + public Employee(int id, String name) { + this.id = id; + this.name = name; + } + + public Employee(int id, String name, int age, double salary) { + + this.id = id; + this.name = name; + this.age = age; + this.salary = salary; + } + + @Override + public String toString() { + return "Employee{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", salary=" + salary + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Employee employee = (Employee) o; + + if (id != employee.id) + return false; + if (age != employee.age) + return false; + if (Double.compare(employee.salary, salary) != 0) + return false; + return name != null ? name.equals(employee.name) : employee.name == null; + } + + @Override + public int hashCode() { + int result; + long temp; + result = id; + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + age; + temp = Double.doubleToLongBits(salary); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } +} + +``` + + + +```Java +/** + * 提供用于测试的数据 + */ +public class EmployeeData { + + public static List getEmployees(){ + List list = new ArrayList<>(); + + list.add(new Employee(1001, "马化腾", 34, 6000.38)); + list.add(new Employee(1002, "马云", 12, 9876.12)); + list.add(new Employee(1003, "刘强东", 33, 3000.82)); + list.add(new Employee(1004, "雷军", 26, 7657.37)); + list.add(new Employee(1005, "李彦宏", 65, 5555.32)); + list.add(new Employee(1006, "比尔盖茨", 42, 9500.43)); + list.add(new Employee(1007, "任正非", 26, 4333.32)); + list.add(new Employee(1008, "扎克伯格", 35, 2500.32)); + + return list; + } + +} +``` + + + +**下面来通过实际例子讲解方法引用:** + +```java + + +/** + * 方法引用的使用 + * + * 1.使用情境:当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用! + * + * 2.方法引用,本质上就是Lambda表达式,而Lambda表达式作为函数式接口的实例。所以 + * 方法引用,也是函数式接口的实例。 + * + * 3. 使用格式: 类(或对象) :: 方法名 + * + * 4. 具体分为如下的三种情况: + * 情况1 对象 :: 非静态方法 + * 情况2 类 :: 静态方法 + * + * 情况3 类 :: 非静态方法 + * + * 5. 方法引用使用的要求: + * 接口中的抽象方法的形参列表和返回值类型 + * 与 + * 方法引用的方法的形参列表和返回值类型相同!(针对于情况1和情况2) + * + */ +public class MethodRefTest { + + // 情况一:对象 :: 实例方法 + //Consumer中的void accept(T t) + //PrintStream中的void println(T t) + @Test + public void test1() { + Consumer con1 = str -> System.out.println(str); + con1.accept("北京"); + + System.out.println("*******************"); + PrintStream ps = System.out; + Consumer con2 = ps::println; + con2.accept("beijing"); + } + + //Supplier中的T get() + //Employee中的String getName() + @Test + public void test2() { + Employee emp = new Employee(1001,"Tom",23,5600); + + Supplier sup1 = () -> emp.getName(); + System.out.println(sup1.get()); + + System.out.println("*******************"); + Supplier sup2 = emp::getName; + System.out.println(sup2.get()); + + } + + // 情况二:类 :: 静态方法 + //Comparator中的int compare(T t1,T t2) + //Integer中的int compare(T t1,T t2) + @Test + public void test3() { + Comparator com1 = (t1,t2) -> Integer.compare(t1,t2); + System.out.println(com1.compare(12,21)); + + System.out.println("*******************"); + + Comparator com2 = Integer::compare; + System.out.println(com2.compare(12,3)); + + } + + //Function中的R apply(T t) + //Math中的Long round(Double d) + @Test + public void test4() { + Function func = new Function() { + @Override + public Long apply(Double d) { + return Math.round(d); + } + }; + + System.out.println("*******************"); + + Function func1 = d -> Math.round(d); + System.out.println(func1.apply(12.3)); + + System.out.println("*******************"); + + Function func2 = Math::round; + System.out.println(func2.apply(12.6)); + } + + // 情况三:类 :: 实例方法 (有难度) + // Comparator中的int comapre(T t1,T t2) 第一个参数T t1,也可以变成方法的调用者 + // String中的int t1.compareTo(t2) 看上面说的,t1变成了调用者等价于第一个参数T t1 + @Test + public void test5() { + Comparator com1 = (s1,s2) -> s1.compareTo(s2); + System.out.println(com1.compare("abc","abd")); + + System.out.println("*******************"); + + Comparator com2 = String :: compareTo; + System.out.println(com2.compare("abd","abm")); + } + + //BiPredicate中的boolean test(T t1, T t2); + //String中的boolean t1.equals(t2) + @Test + public void test6() { + BiPredicate pre1 = (s1,s2) -> s1.equals(s2); + System.out.println(pre1.test("abc","abc")); + + System.out.println("*******************"); + BiPredicate pre2 = String :: equals; + System.out.println(pre2.test("abc","abd")); + } + + // Function中的R apply(T t) + // Employee中的String getName(); 第一个参数T t相当于方法调用者emp,返回值R和String对应 + @Test + public void test7() { + Employee employee = new Employee(1001, "Jerry", 23, 6000); + + + Function func1 = e -> e.getName(); + System.out.println(func1.apply(employee)); + + System.out.println("*******************"); + + + Function func2 = Employee::getName; + System.out.println(func2.apply(employee)); + + + } + +} + +``` + + + + + +# 构造器引用 + +格式:ClassName :: new + +与函数式接口相结合,自动与函数式接口中方法兼容。可以把构造器引用赋值给定义的方法,要求构造器参数列表要与接口中抽象方法的参数列表一致!且方法的返回值即为构造器对应类的对象。 + + + +```Java + +import org.junit.Test; + +import java.util.Arrays; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * 一、构造器引用 + * 和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。 + * 抽象方法的返回值类型即为构造器所属的类的类型 + * + * 二、数组引用 + * 大家可以把数组看做是一个特殊的类,则写法与构造器引用一致。 + * + */ +public class ConstructorRefTest { + + /** + * 构造器引用 + * Supplier中的T get() + * Employee的空参构造器:Employee() + * 1、和方法引用一样的理解方法,你的get()方法没有参数,我的Employee()也没有参数。 + * 2、你的get()方法有返回值T,我的Employee()方法返回值也是一个对象 + * 3、所以刚好可以用 + */ + @Test + public void test1(){ + + Supplier sup = new Supplier() { + @Override + public Employee get() { + return new Employee(); + } + }; + System.out.println("*******************"); + + Supplier sup1 = () -> new Employee(); + System.out.println(sup1.get()); + + System.out.println("*******************"); + + Supplier sup2 = Employee :: new; + System.out.println(sup2.get()); + } + + //Function中的R apply(T t) + @Test + public void test2(){ + Function func1 = id -> new Employee(id); + Employee employee = func1.apply(1001); + System.out.println(employee); + + System.out.println("*******************"); + + Function func2 = Employee :: new; + Employee employee1 = func2.apply(1002); + System.out.println(employee1); + + } + + //BiFunction中的R apply(T t,U u) + @Test + public void test3(){ + BiFunction func1 = (id,name) -> new Employee(id,name); + System.out.println(func1.apply(1001,"Tom")); + + System.out.println("*******************"); + + BiFunction func2 = Employee :: new; + System.out.println(func2.apply(1002,"Tom")); + + } + + //数组引用 + //Function中的R apply(T t) + @Test + public void test4(){ + Function func1 = length -> new String[length]; + String[] arr1 = func1.apply(5); + System.out.println(Arrays.toString(arr1)); + + System.out.println("*******************"); + + Function func2 = String[] :: new; + String[] arr2 = func2.apply(10); + System.out.println(Arrays.toString(arr2)); + + } +} + +``` + + + +# 强大的Stream API + +## Stream API说明 + +- Java8中有两大最为重要的改变。第一个是 **Lambda** **表达式**;另外一个则是 **Stream API**。 + +- Stream API ( java.util.stream) 把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。 + +- Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 **使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。**也可以使用 Stream API 来并行执行操作。简言之,Stream API 提供了一种高效且易于使用的处理数据的方式 + + + +## 为什么要使用Stream API + +- 实际开发中,项目中多数数据源都来自于Mysql,Oracle等,很多一些复杂的数据获取可以直接在sql层面去解决。但现在数据源可以更多了,有MongDB,Radis等,而这些NoSQL的数据本身不支持一些复杂的数据计算,这个时候就需要Java层面去处理。 + +- Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构,而 Stream 是有关计算的。前者是主要面向内存,存储在内存中,后者主要是面向 CPU,通过 CPU 实现计算。 + + + +## 什么是Stream + +Stream到底是什么呢? + +是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。 + +**“集合讲的是数据,Stream讲的是计算!”** + +**注意:** + +①Stream 自己不会存储元素。 + +②Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。 + +③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行 + + + + + +**Stream** **的操作三个步骤** + +1、创建Stream + +一个数据源(如:集合、数组),获取一个流 + +2、中间操作 + +一个中间操作链,对数据源的数据进行处理 + +3、终止操作(终端操作) + +一旦执行终止操作,就执行中间操作链,才产生结果【也就是所谓的延迟执行】。之后,不会再被使用 + + + + + + + + + +## 创建Stream + + + +```java + +public class StreamAPITest { + + //创建 Stream方式一:通过集合 + @Test + public void test1(){ + List employees = EmployeeData.getEmployees(); + +// default Stream stream() : 返回一个顺序流,顺序流等会中间操作拿数据的时候按顺序拿 + Stream stream = employees.stream(); + +// default Stream parallelStream() : 返回一个并行流 + Stream parallelStream = employees.parallelStream(); + + } + + //创建 Stream方式二:通过数组 + @Test + public void test2(){ + int[] arr = new int[]{1,2,3,4,5,6}; + //调用Arrays类的static Stream stream(T[] array): 返回一个流 + IntStream stream = Arrays.stream(arr); + + Employee e1 = new Employee(1001,"Tom"); + Employee e2 = new Employee(1002,"Jerry"); + Employee[] arr1 = new Employee[]{e1,e2}; + Stream stream1 = Arrays.stream(arr1); + + } + //创建 Stream方式三:通过Stream的of(),通过显示值创建一个流。它可以接收任意数量的参数 + @Test + public void test3(){ + + Stream stream = Stream.of(1, 2, 3, 4, 5, 6); + + } + + //创建 Stream方式四:创建无限流【用的少,了解下就行】 + @Test + public void test4(){ + +// 迭代 +// public static Stream iterate(final T seed, final UnaryOperator f) + //遍历前10个偶数 + Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println); + + +// 生成 +// public static Stream generate(Supplier s) + Stream.generate(Math::random).limit(10).forEach(System.out::println); + + } + +} + +``` + + + + + +## 中间操作 + +```Java + +/** + * 测试Stream的中间操作 + */ +public class StreamAPITest1 { + + //1-筛选与切片 + @Test + public void test1(){ + List list = EmployeeData.getEmployees(); +// filter(Predicate p)——过滤 接收 Lambda , 从流中排除某些元素。 + Stream stream = list.stream(); + //练习:查询员工表中薪资大于7000的员工信息 + /** + * List filterStrs1 = filterString(list,s -> s.contains("京")); + * 跟之前的这个lambda表达式代码是一个意思 + */ + stream.filter(e -> e.getSalary() > 7000).forEach(System.out::println); + + System.out.println(); +// limit(n)——截断流,使其元素不超过给定数量。 + list.stream().limit(3).forEach(System.out::println); + System.out.println(); + +// skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补 + list.stream().skip(3).forEach(System.out::println); + + System.out.println(); +// distinct()——筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素 + + list.add(new Employee(1010,"刘强东",40,8000)); + list.add(new Employee(1010,"刘强东",41,8000)); + list.add(new Employee(1010,"刘强东",40,8000)); + list.add(new Employee(1010,"刘强东",40,8000)); + list.add(new Employee(1010,"刘强东",40,8000)); + +// System.out.println(list); + + list.stream().distinct().forEach(System.out::println); + } + + //映射 + @Test + public void test2(){ +// map(Function f)——接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将其映射成一个新的元素。 + List list = Arrays.asList("aa", "bb", "cc", "dd"); + list.stream().map(str -> str.toUpperCase()).forEach(System.out::println); + +// 练习1:获取员工姓名长度大于3的员工的姓名。 + List employees = EmployeeData.getEmployees(); + Stream namesStream = employees.stream().map(Employee::getName); + namesStream.filter(name -> name.length() > 3).forEach(System.out::println); + System.out.println(); + //练习2: + Stream> streamStream = list.stream().map(StreamAPITest1::fromStringToStream); + //这个还需要两层遍历 + streamStream.forEach(s ->{ + s.forEach(System.out::println); + }); + System.out.println(); +// flatMap(Function f)——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。 + //flatMap一层遍历即可拿到想要的结果 + Stream characterStream = list.stream().flatMap(StreamAPITest1::fromStringToStream); + characterStream.forEach(System.out::println); + + } + + //将字符串中的多个字符构成的集合转换为对应的Stream的实例 + public static Stream fromStringToStream(String str){//aa + ArrayList list = new ArrayList<>(); + for(Character c : str.toCharArray()){ + list.add(c); + } + return list.stream(); + + } + + + //3-排序 + @Test + public void test4(){ +// sorted()——自然排序 + List list = Arrays.asList(12, 43, 65, 34, 87, 0, -98, 7); + list.stream().sorted().forEach(System.out::println); + //抛异常,原因:Employee没有实现Comparable接口 +// List employees = EmployeeData.getEmployees(); +// employees.stream().sorted().forEach(System.out::println); + + +// sorted(Comparator com)——定制排序 + + List employees = EmployeeData.getEmployees(); + employees.stream().sorted( (e1,e2) -> { + + int ageValue = Integer.compare(e1.getAge(),e2.getAge()); + if(ageValue != 0){ + return ageValue; + }else{ + return -Double.compare(e1.getSalary(),e2.getSalary()); + } + + }).forEach(System.out::println); + } + +} + +``` + + + +## 终止操作 + + + +```Java + +/** + * 测试Stream的终止操作 + * + */ +public class StreamAPITest2 { + + //1-匹配与查找 + @Test + public void test1(){ + List employees = EmployeeData.getEmployees(); + +// allMatch(Predicate p)——检查是否匹配所有元素。 +// 练习:是否所有的员工的年龄都大于18 + boolean allMatch = employees.stream().allMatch(e -> e.getAge() > 18); + System.out.println(allMatch); + +// anyMatch(Predicate p)——检查是否至少匹配一个元素。 +// 练习:是否存在员工的工资大于 10000 + boolean anyMatch = employees.stream().anyMatch(e -> e.getSalary() > 10000); + System.out.println(anyMatch); + +// noneMatch(Predicate p)——检查是否没有匹配的元素。 +// 练习:是否存在员工姓“雷” + boolean noneMatch = employees.stream().noneMatch(e -> e.getName().startsWith("雷")); + System.out.println(noneMatch); +// findFirst——返回第一个元素 + Optional employee = employees.stream().findFirst(); + System.out.println(employee); +// findAny——返回当前流中的任意元素 + Optional employee1 = employees.parallelStream().findAny(); + System.out.println(employee1); + + } + + @Test + public void test2(){ + List employees = EmployeeData.getEmployees(); + // count——返回流中元素的总个数 + long count = employees.stream().filter(e -> e.getSalary() > 5000).count(); + System.out.println(count); +// max(Comparator c)——返回流中最大值 +// 练习:返回最高的工资: + Stream salaryStream = employees.stream().map(e -> e.getSalary()); + Optional maxSalary = salaryStream.max(Double::compare); + System.out.println(maxSalary); +// min(Comparator c)——返回流中最小值 +// 练习:返回最低工资的员工 + Optional employee = employees.stream().min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())); + System.out.println(employee); + System.out.println(); +// forEach(Consumer c)——内部迭代 + employees.stream().forEach(System.out::println); + + //使用集合的遍历操作 + employees.forEach(System.out::println); + } + + //2-归约 + @Test + public void test3(){ +// reduce(T identity, BinaryOperator)——可以将流中元素反复结合起来,得到一个值。返回 T +// 练习1:计算1-10的自然数的和 + List list = Arrays.asList(1,2,3,4,5,6,7,8,9,10); + Integer sum = list.stream().reduce(0, Integer::sum); + System.out.println(sum); + + +// reduce(BinaryOperator) ——可以将流中元素反复结合起来,得到一个值。返回 Optional +// 练习2:计算公司所有员工工资的总和 + List employees = EmployeeData.getEmployees(); + Stream salaryStream = employees.stream().map(Employee::getSalary); +// Optional sumMoney = salaryStream.reduce(Double::sum); + Optional sumMoney = salaryStream.reduce((d1,d2) -> d1 + d2); + System.out.println(sumMoney.get()); + + } + + //3-收集 + @Test + public void test4(){ +// collect(Collector c)——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法 +// 练习1:查找工资大于6000的员工,结果返回为一个List或Set + + List employees = EmployeeData.getEmployees(); + List employeeList = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toList()); + + employeeList.forEach(System.out::println); + System.out.println(); + Set employeeSet = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toSet()); + + employeeSet.forEach(System.out::println); + + + + + } +} + +``` + + + +# Optional类 + +## 什么是Optional? + + + +- 到目前为止,臭名昭著的空指针异常是导致Java应用程序失败的最常见原因。以前,为了解决空指针异常,Google公司著名的Guava项目引入了Optional类,Guava通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到Google Guava的启发,Optional类已经成为Java 8类库的一部分。 + +- Optional 类(java.util.Optional) 是一个容器类,它可以保存类型T的值,代表这个值存在。或者仅仅保存null,表示这个值不存在。原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。 + +- Optional类的Javadoc描述如下:这是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。 + + + +## 常用API + + + + + + + +## 举例 + +首先准备两个类 + +```java + +public class Boy { + private Girl girl; + + @Override + public String toString() { + return "Boy{" + + "girl=" + girl + + '}'; + } + + public Girl getGirl() { + return girl; + } + + public void setGirl(Girl girl) { + this.girl = girl; + } + + public Boy() { + + } + + public Boy(Girl girl) { + + this.girl = girl; + } +} +``` + + + +```java +public class Girl { + + private String name; + + @Override + public String toString() { + return "Girl{" + + "name='" + name + '\'' + + '}'; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Girl() { + + } + + public Girl(String name) { + + this.name = name; + } +} +``` + + + +这里只是简单的测试两个API + +```Java +/** + * Optional类:为了在程序中避免出现空指针异常而创建的。 + * + * 常用的方法:ofNullable(T t) + * orElse(T t) + * + */ +public class OptionalTest { + +/* +Optional.of(T t) : 创建一个 Optional 实例,t必须非空; +Optional.empty() : 创建一个空的 Optional 实例 +Optional.ofNullable(T t):t可以为null + + */ + @Test + public void test1(){ + Girl girl = new Girl(); +// girl = null; + //of(T t):保证t是非空的 + Optional optionalGirl = Optional.of(girl); + + } + + @Test + public void test2(){ + Girl girl = new Girl(); +// girl = null; + //ofNullable(T t):t可以为null + Optional optionalGirl = Optional.ofNullable(girl); + System.out.println(optionalGirl); + //orElse(T t1):如果单前的Optional内部封装的t是非空的,则返回内部的t. + //如果内部的t是空的,则返回orElse()方法中的参数t1. + Girl girl1 = optionalGirl.orElse(new Girl("赵丽颖")); + System.out.println(girl1); + + } +} +``` + + + +**实际场景使用** + +可能出现空指针的例子: + +```java +public String getGirlName(Boy boy){ + return boy.getGirl().getName(); + } + + @Test + public void test3(){ + Boy boy = new Boy(); + boy = null; + String girlName = getGirlName(boy); + System.out.println(girlName); + + } +``` + + + +结果: + +``` +java.lang.NullPointerException + at com.atguigu.java4.OptionalTest.getGirlName(OptionalTest.java:47) + at com.atguigu.java4.OptionalTest.test3(OptionalTest.java:54) +... +... + +Process finished with exit code -1 +``` + + + +没有Optional的解决办法,但是如果调用层数过多,就得一层一层判断是否为null,写起来很麻烦。 + +```Java +//优化以后的getGirlName(): + public String getGirlName1(Boy boy){ + if(boy != null){ + Girl girl = boy.getGirl(); + if(girl != null){ + return girl.getName(); + } + } + + return null; + + } +@Test + public void test4(){ + Boy boy = new Boy(); + boy = null; + String girlName = getGirlName1(boy); + System.out.println(girlName); + + } +``` + + + +使用Optional解决问题: + +```Java + //使用Optional类的getGirlName(): + public String getGirlName2(Boy boy){ + + Optional boyOptional = Optional.ofNullable(boy); + //此时的boy1一定非空 + Boy boy1 = boyOptional.orElse(new Boy(new Girl("迪丽热巴"))); + + Girl girl = boy1.getGirl(); + + Optional girlOptional = Optional.ofNullable(girl); + //girl1一定非空 + Girl girl1 = girlOptional.orElse(new Girl("古力娜扎")); + + return girl1.getName(); + } + + @Test + public void test5(){ + Boy boy = null; + boy = new Boy(); + boy = new Boy(new Girl("苍老师")); + String girlName = getGirlName2(boy); + System.out.println(girlName); + + } +``` + +这种是绝对不会出现空指针的。 + + + +# 接口的增强 + +``` +JDK7及以前:只能定义全局常量和抽象方法 + >全局常量:public static final的.但是书写时,可以省略不写 + >抽象方法:public abstract的 + +JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法 +``` + + + +```java +/* + * JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法 + */ +public interface CompareA { + + //静态方法 + public static void method1() { + + System.out.println("CompareA:北京"); + } + + //默认方法 + public default void method2() { + System.out.println("CompareA:上海"); + } + //接口中的public 可以省略,自动就是public + default void method3() { + System.out.println("CompareA:上海"); + } +} +``` + + + +```java +public class SuperClass { + + public void method3(){ + System.out.println("SuperClass:北京"); + } + +} +``` + + + +``` +public interface CompareB { + + default void method3(){ + System.out.println("CompareB:上海"); + } + +} +``` + + + + + +```java +public class SubClassTest { + + public static void main(String[] args) { + SubClass s = new SubClass(); + +// s.method1(); +// SubClass.method1(); + //知识点1:接口中定义的静态方法,只能通过接口来调用。实现类用不了 + CompareA.method1(); + //知识点2:通过实现类的对象,可以调用接口中的默认方法。 + //如果实现类重写了接口中的默认方法,调用时,仍然调用的是重写以后的方法 + s.method2(); + //知识点3:如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的默认方法, + //那么子类在没有重写此方法的情况下,默认调用的是父类中的同名同参数的方法。-->类优先原则 + //知识点4:如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法, + //那么在实现类没有重写此方法的情况下,报错。-->接口冲突。 + //这就需要我们必须在实现类中重写此方法 + s.method3(); + + } + +} + +class SubClass extends SuperClass implements CompareA,CompareB{ + + public void method2(){ + System.out.println("SubClass:上海"); + } + + public void method3(){ + System.out.println("SubClass:深圳"); + } + + //知识点5:如何在子类(或实现类)的方法中调用父类、接口中被重写的方法 + public void myMethod(){ + method3();//调用自己定义的重写的方法 + super.method3();//调用的是父类中声明的 + //调用接口中的默认方法 + CompareA.super.method3(); + CompareB.super.method3(); + } +} +``` + + + + + +# 日期API【TODO】 + + + + + +# 注解【TODO】 + + + diff --git a/docs/Java/Basis/keyAndDifficultPoints/Generic/泛型.md b/docs/Java/Basis/keyAndDifficultPoints/Generic/泛型.md new file mode 100644 index 0000000..1e6d7d3 --- /dev/null +++ b/docs/Java/Basis/keyAndDifficultPoints/Generic/泛型.md @@ -0,0 +1,2137 @@ +# 简介 + +## 泛型的优点 + +1、泛型的本质是为了参数化类型,也就是在在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型,很明显这种方法提高了代码的复用性。 + +2、泛型的引入提高了安全性,泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。。 + +3、在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。 + +那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。 + +``` +public class GlmapperGeneric { + private T t; + public void set(T t) { this.t = t; } + public T get() { return t; } + + public static void main(String[] args) { + // do nothing + } + + /** + * 不指定类型 + */ + public void noSpecifyType(){ + GlmapperGeneric glmapperGeneric = new GlmapperGeneric(); + glmapperGeneric.set("test"); + // 需要强制类型转换 + String test = (String) glmapperGeneric.get(); + System.out.println(test); + } + + /** + * 指定类型 + */ + public void specifyType(){ + GlmapperGeneric glmapperGeneric = new GlmapperGeneric(); + glmapperGeneric.set("test"); + // 不需要强制类型转换 + String test = glmapperGeneric.get(); + System.out.println(test); + } +} +``` + + + +## 为什么提高了安全性? + +再举例子说明一下 + +**不安全举例** + +```Java +package keyAndDifficultPoints.Generic; + + +import java.util.ArrayList; +import java.util.List; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/15 16:09 + *

+ * 功能描述: + */ +public class Test_Safe { + + public static void main(String[] args) { + test(); + } + + public static void test() { + List arrayList = new ArrayList(); + arrayList.add("aaaa"); + arrayList.add(100); + + for (int i = 0; i < arrayList.size(); i++) { + String s = (String) arrayList.get(i); + System.out.println(s); + + } + } +} +``` + +结果: + +``` +aaaa +Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String + at keyAndDifficultPoints.Generic.Test_Safe.test(Test_Safe.java:25) + at keyAndDifficultPoints.Generic.Test_Safe.main(Test_Safe.java:16) +``` + +很明显的一个类型转换错误。ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。 + + + +**泛型提高安全性** + +将上面的代码稍微改一下 + +```Java + public static void test01(){ + List arrayList = new ArrayList<>(); + arrayList.add("aaaa"); + //下面代码编译时就直接报错了 + arrayList.add(100); + + for (int i = 0; i < arrayList.size(); i++) { + String s = (String) arrayList.get(i); + System.out.println(s); + + } + } +``` + +通过泛型来提前检测类型,编译时就通不过。 + + + +## 泛型为什么很重要 + +我们看一下比较常用的JUC包 + +```Java +public CompletableFuture thenComposeAsync( + Function> fn) { + return uniComposeStage(asyncPool, fn); + } + + public CompletableFuture thenComposeAsync( + Function> fn, + Executor executor) { + return uniComposeStage(screenExecutor(executor), fn); + } + + public CompletableFuture whenComplete( + BiConsumer action) { + return uniWhenCompleteStage(null, action); + } + + public CompletableFuture whenCompleteAsync( + BiConsumer action) { + return uniWhenCompleteStage(asyncPool, action); + } +``` + +这些都大量的用到了泛型,如果不把泛型学好,想真正深入源码了解一些东西,可能就完全看不懂了。 + +# 泛型类 + +泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。 + +**最普通的泛型类:** + +```Java +package keyAndDifficultPoints.Generic; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/15 16:38 + *

+ * 功能描述: + */ +public class Test_GenericClass { + public static void main(String[] args) { + test(); + } + + public static void test(){ + /** + * 1、泛型的类型参数只能是类类型(包括自定义类),不能是简单数据类型(比如int,long这些) + * 2、传入的实参类型需与泛型的类型参数类型相同,即为这里的Integer。 + * 3、new 后面的泛型参数可以省略 + */ + Generic genericInteger1 = new Generic(123); + Generic genericInteger = new Generic<>(123); + + Generic genericString = new Generic("my"); + + System.out.println(genericInteger.getVar()); + System.out.println(genericString.getVar()); + } + + +} + +/** + * 1、此处T虽然可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。 + * 但是为了代码的可读性一般来说: + * K,V用来表示键值对 + * E是Element的缩写,常用来遍历时表示 + * T就是Type的缩写,常用在普通泛型类上 + * 2、还有一些不常见的U,R啥的 + */ +class Generic { + //key这个成员变量的类型为T,T的类型由外部指定 + private T var; + + public Generic(T var) { //泛型构造方法形参key的类型也为T,T的类型由外部指定 + this.var = var; + } + + public T getVar() { //泛型方法getKey的返回值类型为T,T的类型由外部指定 + return var; + } +} + +class MyMap { // 此处指定了两个泛型类型 + private K key; // 此变量的类型由外部决定 + private V value; // 此变量的类型由外部决定 + + public K getKey() { + return this.key; + } + + public V getValue() { + return this.value; + } + + public void setKey(K key) { + this.key = key; + } + + public void setValue(V value) { + this.value = value; + } +}; +``` + +**结果:** + +``` +123 +my + +Process finished with exit code 0 +``` + + + +- 定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。 + + + +还是以上面的泛型类为例进行测试 + +```Java + public static void test01() { + Generic generic = new Generic("我是字符串"); + Generic generic1 = new Generic(123); + Generic generic2 = new Generic(123.123); + Generic generic3 = new Generic(false); + + System.out.println(generic.getVar()); + System.out.println(generic1.getVar()); + System.out.println(generic2.getVar()); + System.out.println(generic3.getVar()); + } +``` + +**结果:** + +``` +我是字符串 +123 +123.123 +false + +Process finished with exit code 0 +``` + +没有报错,正确输出了。 + +# 泛型接口 + +泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子: + +```Java +interface Info{ // 在接口上定义泛型 + public T getVar() ; // 定义方法,方法的返回值就是泛型类型 +} +``` + +**当实现泛型接口的类,未传入泛型实参时:** + +```java +/** + * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中 + * 即:class InfoImpl implements Info + * 如果不声明泛型,如:class InfoImpl implements Info,编译器会报错:"Unknown class" + */ +class InfoImpl implements Info { // 定义泛型接口的子类 + private T var; + + public InfoImpl(T var) { + this.setVar(var); + } + + public void setVar(T var) { + this.var = var; + } + + public T getVar() { + return this.var; + } +} +``` + +当实现泛型接口的类,传入泛型实参时: + +```Java +/** + * 传入泛型实参时: + * 定义一个是先烈实现这个接口,虽然我们只创建了一个泛型接口Info + * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型 + * 即:InfoImpl01,public String getVar();中的的T都要替换成传入的String类型。 + */ +class InfoImpl01 implements Info { // 定义泛型接口的子类 + private String var; + + public InfoImpl01(String var) { + this.setVar(var); + } + + public void setVar(String var) { + this.var = var; + } + + public String getVar() { + return this.var; + } +} +``` + +# 泛型方法 + +在java中,泛型类和接口的定义非常简单,但是泛型方法就比较复杂了。 + +泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。 + + + +**最简单的一个泛型方法** + +```Java +public class Test_GenericMethod { + + public static void main(String[] args) { + Test_GenericMethod test_genericMethod = new Test_GenericMethod(); + Integer integer = test_genericMethod.genericMethod(12); + System.out.println(integer); + } + + /** + * 说明: + * 1、public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。 + * 2、只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。 + * 3、表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。 + * 4、 后面的这个T,代表这个方法的返回值类型 + * 4、与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。 + */ + public T genericMethod(T a) { + + return a; + } +} +``` + + + + + +## 基本用法(非泛型类中的泛型方法) + +下面来细说一下泛型方法 + +首先说一个误区 + +```Java +class Generic01 { + private T key; + + public Generic01(T key) { + this.key = key; + } + + + /** + * 1、这个虽然在方法中使用了泛型,但这并不是一个泛型方法。这只是类中一个普通的 + * 成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。所以在这个方法中才 + * 可以继续使用 T 这个泛型。 + */ + public T getKey() { + return key; + } + + /** + * 1、这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E" + * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。 + */ +// public E setKey(E key) { +// this.key = key; +// } + +} +``` + + + +**基本用法**(非) + +```Java +package keyAndDifficultPoints.Generic; + + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/15 17:46 + *

+ * 功能描述: + */ +public class Test_GenericMethod { + + public static void main(String[] args) { + Test_GenericMethod test_genericMethod = new Test_GenericMethod(); + Generic01 generic01 = new Generic01<>(123); + + Generic01 generic02 = new Generic01<>("AAAAA"); + + test_genericMethod.genericMethod_test01(generic01); + test_genericMethod.genericMethod_test02(generic02, "我是T"); + + test_genericMethod.Method01(generic01); + } + + /** + * 说明: + * 1、public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。 + * 2、只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。 + * 3、表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。 + * 4、 后面的这个T,代表这个方法的返回值类型 + * 4、与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。 + */ + public T genericMethod(T a) { + + return a; + } + + + /** + * 1、这才是一个真正的泛型方法。 + * 2、首先在public与返回值之间的必不可少,这表明这是一个泛型方法,并且声明了一个泛型T。 + * 3、这个T可以出现在这个泛型方法的任意位置.泛型的数量也可以为任意多个 + */ + public T genericMethod_test01(Generic01 generic01) { + System.out.println("我是genericMethod_test01:" + generic01.getKey()); + T test = generic01.getKey(); + return test; + } + + public T genericMethod_test02(Generic01 generic01, V value) { + System.out.println("我是genericMethod_test02:" + generic01.getKey() + "==> value:" + value); + + T test = generic01.getKey(); + return test; + } + + + //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic这个泛型类做形参而已。 + public void Method01(Generic01 generic01) { + System.out.println(generic01.getKey()); + } + + + //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符? + //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类 + public void Method02(Generic01 generic01) { + System.out.println(generic01.getKey()); + } + + /** + * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' " + * 虽然我们声明了,也表明了这是一个可以处理泛型的类型的泛型方法。 + * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。 + */ +// public T showKeyName(Generic01 generic01, T t) { +// return t; +// } + +} + +``` + +**结果:** + +``` +我是genericMethod_test01:123 +我是genericMethod_test02:AAAAA==> value:我是T +123 + +Process finished with exit code 0 +``` + +## 泛型类中的泛型方法 + +当然这并不是泛型方法的全部,泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下。 + +```Java +package keyAndDifficultPoints.Generic; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/15 20:14 + *

+ * 功能描述: + */ +public class Test_GenericMethod01 { + public static void main(String[] args) { + Apple apple = new Apple(); + Person person = new Person(); + + GenerateTest generateTest = new GenerateTest(); + //apple是Fruit的子类,所以这里可以 + generateTest.show_1(apple); + //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person + //generateTest.show_1(person); + + //使用这两个方法都可以成功 + generateTest.show_2(apple); + generateTest.show_2(person); + + //使用这两个方法也都可以成功 + generateTest.show_3(apple); + generateTest.show_3(person); + } +} + +abstract class GenericFruit { + +} + +class Fruit { + @Override + public String toString() { + return "fruit"; + } +} + +class Apple extends Fruit { + @Override + public String toString() { + return "apple"; + } +} + +class Person { + @Override + public String toString() { + return "Person"; + } +} + +class GenerateTest { + + public void show_1(T t) { + System.out.println(t.toString()); + } + + + /** + * 1、在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。 + * 2、由于泛型方法在声明的时候会声明泛型,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。 + */ + public void show_3(E t) { + System.out.println(t.toString()); + } + + + /** + * 1、在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T + * 不是同一种类型。也就是说main函数中使用的时候也可以是不一样的泛型类型 + */ + public void show_2(T t) { + System.out.println(t.toString()); + } +} +``` + + + +**结果:** + +``` +apple +apple +Person +apple +Person + +Process finished with exit code 0 +``` + + + +## 泛型方法与可变参数 + +再看一个泛型方法和可变参数的例子: + +```Java +public class Test_GenericMethod02 { + public static void main(String[] args) { + print("123",753,123.12); + } + + + //必须是三个点 + public static void print(T... args) { + for (T t : args) { + System.out.println(t); + } + } +} +``` + +**结果:** + +``` +123 +753 +123.12 + +Process finished with exit code 0 +``` + +## 静态方法与泛型 + +静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:**静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。** + +即:**如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法** 。 + + public class StaticGenerator { + + /** + * 1、如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法) + * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。 + * 如:public static void show(T t){..},此时编译器会提示错误信息: + "StaticGenerator cannot be refrenced from static context" + * 2、泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系。换句话说, + * 泛型方法所属的类是不是泛型类都没有关系。 + * 3、泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在初始化类时确定,所以无所谓 + */ + public static List copyFromArrayToList(E[] arr){ + + ArrayList list = new ArrayList<>(); + + for(E e : arr){ + list.add(e); + } + return list; + + } + } + + + +## 细枝末节 + +> 可能合上面的有一些重复 + +1、泛型异常类 + +``` +//异常类不能声明为泛型类,编译报错 +class MyException extends Exception{ +} +``` + + + +2、 + +```Java +package keyAndDifficultPoints.Generic.Minutiae; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/15 22:28 + *

+ * 功能描述: + */ +public class Test_Minutiae1 { +} +class Order { + + String orderName; + int orderId; + + //类的内部结构就可以使用类的泛型 + + T orderT; + + public Order(){ + //编译不通过 +// T[] arr = new T[10]; + //编译通过 + T[] arr = (T[]) new Object[10]; + } + + public Order(String orderName,int orderId,T orderT){ + this.orderName = orderName; + this.orderId = orderId; + this.orderT = orderT; + } + + //如下的三个方法都不是泛型方法 + public T getOrderT(){ + return orderT; + } + + public void setOrderT(T orderT){ + this.orderT = orderT; + } + + @Override + public String toString() { + return "Order{" + + "orderName='" + orderName + '\'' + + ", orderId=" + orderId + + ", orderT=" + orderT + + '}'; + } + //静态方法中不能使用类的泛型。 +// public static void show(T orderT){ +// System.out.println(orderT); +// } + + public void show(){ + //编译不通过 +// try{ +// +// +// }catch(T t){ +// +// } + + } + + + /** + * 2、泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系。换句话说, + * 泛型方法所属的类是不是泛型类都没有关系。 + * 3、泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在初始化类时确定,所以无所谓 + */ + public static List copyFromArrayToList(E[] arr){ + + ArrayList list = new ArrayList<>(); + + for(E e : arr){ + list.add(e); + } + return list; + + } +} +class SubOrder extends Order {//SubOrder:不是泛型类 + + + public static List copyFromArrayToList(E[] arr) { + + ArrayList list = new ArrayList<>(); + + for (E e : arr) { + list.add(e); + } + return list; + + } + +} + +class SubOrder1 extends Order {//SubOrder1:仍然是泛型类 +} + +``` + + + + + + + +# 泛型数组 + +```Java +package keyAndDifficultPoints.Generic; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/15 12:10 + *

+ * 功能描述: 测试泛型数组 + */ +public class Test_GenericArray { + + public static void main(String[] args) { + test02(); + } + + public static void test() { + //编译错误 +// List[] ls = new ArrayList[10]; + } + + + public static void test01() { + //这样声明是正确的 + List[] ls = new ArrayList[10]; + ls[1] = new ArrayList(); + + //这样写编译就报错了 +// ls[1].add(1); + + } + + /** + * 下面是sun官方文档里写的。其实不用太纠结,平时泛型虽然用的多,但也不会用的这么奇葩。 + */ + public static void test02(){ + List[] lsa = new List[10]; // OK, array of unbounded wildcard type. + Object o = lsa; + Object[] oa = (Object[]) o; + List li = new ArrayList(); + li.add(new Integer(3)); + oa[1] = li; // Correct. + Integer i = (Integer) lsa[1].get(0); // OK + System.out.println(i); + } + + //正确 + public static void test03() { + List[] ls = new ArrayList[10]; + ls[0] = new ArrayList(); + ls[1] = new ArrayList(); + + ls[0].add("x"); + + } + +} + +``` + +[sun文档](http://docs.oracle.com/javase/tutorial/extra/generics/fineprint.html) + + + +# 泛型在继承方面的细节 + +直接看代码注释 + +```java + /* + 1. 泛型在继承方面的体现 + + 虽然类A是类B的父类,但是G 和G二者不具备子父类关系,二者是并列关系。 + + 补充:类A是类B的父类,A 是 B 的父类 + + */ + @Test + public void test1() { + /** + * 下面是有继承关系,所以可以赋值 + */ + Object obj = null; + String str = null; + obj = str; + + Object[] arr1 = null; + String[] arr2 = null; + arr1 = arr2; + + /** + * 下面属于并列关系,无继承关系。无法赋值 + */ + + //编译不通过 +// Date date = new Date(); +// str = date; + List list1 = null; + List list2 = new ArrayList(); + //此时的list1和list2的类型不具有子父类关系 + //编译不通过 +// list1 = list2; + /* + 反证法: + 假设list1 = list2; + list1.add(123);导致混入非String的数据。出错。 + + */ + + } + + + @Test + public void test2() { + + AbstractList list1 = null; + List list2 = null; + ArrayList list3 = null; + + list1 = list3; + list2 = list3; + + List list4 = new ArrayList<>(); + + } +``` + + + +# 泛型通配符 + +我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V,? 等等,下面来详细讲一下这些通配符。 + +## 常用的通配符 + +本质上都是通配符没啥区别,只不过是编码时的一种约定俗成的东西(可以说提高了代码可读性)。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个大小写字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,? 是这样约定的: + +* ? 表示不确定的 java 类型 +* T (Type) 表示具体的一个java类型 +* K V (Key Value) 分别代表java键值中的Key Value +* E (element) 代表Element + + + +比较难的就是`?`通配符,下面就着重讲一下 + +## ' ? '无界通配符 + +### 基本用法 + +```Java +List listAnimals +``` + +但是如果用通配符的话: + +```Java +List listAnimals +``` + +为什么要使用通配符而不是简单的泛型呢?通配符其实在声明局部变量时是没有什么意义的,但是当你为一个方法声明一个参数时,它是非常重要的。 + +```Java +package keyAndDifficultPoints.Generic; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/15 21:25 + *

+ * 功能描述: 泛型通配符测试 + */ +public class Test_Wildcard_Character { + + public static void main(String[] args) { + List dogList = new ArrayList<>(); + test(dogList); + test1(dogList); + } + + static void test(List animals) { + System.out.println("test输出:"); + for (Animal animal : animals) { + System.out.print(animal.toString() + "-"); + } + } + + static void test1(List animals) { + System.out.println("test1输出:"); + for (Animal animal : animals) { + System.out.print(animal.toString() + "-"); + } + } + + +} + +class Animal { + @Override + public String toString() { + return "Animal"; + } +} + +class Dog extends Animal { + @Override + public String toString() { + return "Dog"; + } +} +``` + +`test1()`在编译时就会飘红 + + + + + +所以,对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 ),表示可以持有任何类型。像 `test()`方法中,限定了上界,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错,而`test1()`就不行。 + + + +### ' ? '通配符的继承 + +```Java + /* + 2. 通配符的使用 + 通配符:? + + 类A是类B的父类,G和G是没有关系的,二者共同的父类是:G + */ + + @Test + public void test3() { + List list1 = null; + List list2 = null; + + List list = null; + + list = list1; + list = list2; + //编译通过 +// print(list1); +// print(list2); + + + // + List list3 = new ArrayList<>(); + list3.add("AA"); + list3.add("BB"); + list3.add("CC"); + list = list3; + //添加(写入):对于List就不能向其内部添加数据。 + //除了添加null之外。 +// list.add("DD"); +// list.add('?'); + + list.add(null); + + //获取(读取):允许读取数据,读取的数据类型为Object。 + Object o = list.get(0); + System.out.println(o); + + + } +``` + + + +## extends和super上下界 + +### 上界通配符 < ? extends E> + +> 上结:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。 + +在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处: + +* 如果传入的类型不是 E 或者 E 的子类,编译不成功 +* 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用 + + + + +### 下界通配符 < ? super E> + +> 下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object + +在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。 + + + +### 举例 + +```Java + /* + 3.有限制条件的通配符的使用。 + ? extends A: + G 可以作为G和G的父类,其中B是A的子类 + + ? super A: + G 可以作为G和G的父类,其中B是A的父类 + + */ + @Test + public void test4() { + + List list1 = null; //[-无穷,Person] + List list2 = null; //[Person,+无穷] + + List list3 = new ArrayList(); + List list4 = new ArrayList(); + List list5 = new ArrayList(); + + list1 = list3; + list1 = list4; +// list1 = list5; + +// list2 = list3; + list2 = list4; + list2 = list5; + + + + //下面的东西很奇怪 + + //读取数据: + list1 = list3; + Person p = list1.get(0); + //编译不通过 + //Student s = list1.get(0); + + list2 = list4; + Object obj = list2.get(0); + ////编译不通过 +// Person obj = list2.get(0); + + //写入数据: + //编译不通过 +// list1.add(new Student()); + + //编译通过 + list2.add(new Person()); + list2.add(new Student()); + + } +} + +class Person { +} + +class Student extends Person { +} +``` + + + +##? 和 T 的区别 + +?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ? 不行,比如如下这种 : + +```Java +// 可以 +T t = operate(); + +// 不可以 +? car = operate(); +``` + +简单总结下: + +T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。 + +### 区别1:通过T来确保泛型参数的一致性 + +```Java +package keyAndDifficultPoints.Wildcard_Character; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/16 11:28 + *

+ * 功能描述: + */ +public class Test_difference { + + public static void main(String[] args) { + List integerList = new ArrayList<>(); + List floatList = new ArrayList<>(); + + //编译报错 +// test(integerList, floatList); + //编译通过 + test1(integerList, floatList); + + + //编译通过 + test(integerList, integerList); + test1(integerList, integerList); + + } + + + + // 通过 T 来 确保 泛型参数的一致性 + public static void test(List dest, List src){ + + } + + //通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型 + public static void test1(List dest, List src){ + + } +} +``` + +### 区别2:T可以通过&进行多重限定 + +```Java +public class Test_difference { + + public static void main(String[] args) { + + + /*---------------------测试多重限定符---------------------*/ + ArrayList list = new ArrayList<>(); + ArrayDeque deque = new ArrayDeque<>(); + LinkedList linkedList = new LinkedList<>(); + + //多重限定时,在编译的时候取最小范围或共同子类 + + test2(list); +// test3(list); 编译报错 + + //编译报错 +// test2(deque); +// test3(deque); + + //编译通过 + test2(linkedList); + test3(linkedList); + + + } + + + //可以进行多重限定 + public static void test2(T t) { + + } + + //可以进行多重限定 + public static void test3(T t) { + + } + + //编译报错,无法进行多重限定 +// public static void test4(List dest, List src){ +// +// } + +} +``` + + + +### 区别3:?通配符可以使用超类限定而T不行 + +类型参数 T 只具有 一种 类型限定方式: + + T extends A + +但是通配符 ? 可以进行 两种限定: + + ? extends A + ? super A + +-------------------------- + +# 关于反射和泛型的一点东西 + +```Java +package keyAndDifficultPoints.Wildcard_Character; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/16 12:09 + *

+ * 功能描述: 泛型反射 + */ +public class Test_Reflect { + + public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { + A a = createInstance(A.class); + B b = createInstance(B.class); + } + + /** + * 这样写明显是要安全很多的 + */ + public static T createInstance(Class clazz) throws IllegalAccessException, InstantiationException { + return clazz.newInstance(); + } + + public static void getA(String path) throws ClassNotFoundException, IllegalAccessException, InstantiationException { + A a = (A) Class.forName("keyAndDifficultPoints.Wildcard_Character.A").newInstance(); + //很明显下面的这行代码是错的,但是写代码的时候你不知道path是哪个 +// B b = (B)Class.forName("keyAndDifficultPoints.Wildcard_Character.A").newInstance(); + System.out.println(a.toString()); + } +} + +class A { + String name; + + @Override + public String toString() { + return "我是对象A"; + } +} + +class B { + String name; + + @Override + public String toString() { + return "我是对象B"; + } +} + + +class C { + //所以当不知道声明什么类型的 Class 的时候可以定义一 个Class。 + public Class clazz1; + + //因为T没有声明,所以编译报错 +// public Class clazz2; +} + +class D { + public Class clazz; + // 不会报错 + public Class clazzT; +} + +``` + + + +# 泛型原理(泛型擦除) + +## 类型擦除简介 + +​ Java的泛型是伪泛型,为什么说Java的泛型是伪泛型呢?因为在编译期间,所有的泛型信息都会被擦除掉,我们常称为**泛型擦除**。 + +​ Java中的泛型基本上都是在编译器这个层次来实现的,在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,编译器在编译的时候去掉,这个过程就称为类型擦除。 + +​ 如在代码中定义的List和List等类型,在编译后都会编程List,JVM看到的只是List。而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。 + +可以通过两个例子,来证明java泛型的类型擦除。 + +**例1:** + +```Java + @Test + public void test() { + List stringList = new ArrayList(); + stringList.add("my"); + List integerList = new ArrayList(); + integerList.add(123); + System.out.println(stringList.getClass() == integerList.getClass()); + + } +``` + +**结果:** + +``` +true + +Process finished with exit code 0 +``` + +在这个例子中,我们定义了两个List,不过一个是List泛型类型,只能存储字符串。一个是List泛型类型,只能存储整形。最后,我们通过stringList对象和integerList对象的getClass方法获取它们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下了原始类型。 + + + +**例2:** + +```Java + @Test + public void test01() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + List list = new ArrayList(); + //这样调用add方法只能存储整形,因为泛型类型的实例为Integer + list.add(1); + //这样写编译就会报错 +// list.add("my"); + + //通过反射的方式则可以存储String + list.getClass().getMethod("add", Object.class).invoke(list, "my"); + for (int i = 0; i < list.size(); i++) { + System.out.println(list.get(i)); + } + } +``` + +结果: + +``` +1 +my + +Process finished with exit code 0 +``` + +在程序中定义了一个List泛型类型,如果直接调用add方法,那么只能存储整形的数据。不过当我们利用反射调用add方法的时候,却可以存储字符串。这说明了Integer泛型实例在编译之后被擦除了,只保留了 原始类型。 + +## 类型擦除后保留的原始类型 + +1、在上面,几次提到了**原始类型**。什么是原始类型?原始类型就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除,并使用其限定类型(无限定的变量用Object替换)。 + +例3: + +```Java +package keyAndDifficultPoints.principle; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/16 23:01 + *

+ * 功能描述: + */ +public class Test_principle02 { + public static void main(String[] args) { + } +} +class Test_Generic { + private T value; + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } +} +``` + +下面我们用IDEA的工具,查看这个类的字节码信息。我把完整的字节码复制在下方: + + + +```Java +// class version 52.0 (52) +// access flags 0x20 +// signature Ljava/lang/Object; +// declaration: keyAndDifficultPoints/principle/Test_Generic +class keyAndDifficultPoints/principle/Test_Generic { + + // compiled from: Test_principle02.java + + // access flags 0x2 + // signature TT; + // declaration: T + private Ljava/lang/Object; value + + // access flags 0x0 + ()V + L0 + LINENUMBER 13 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this LkeyAndDifficultPoints/principle/Test_Generic; L0 L1 0 + // signature LkeyAndDifficultPoints/principle/Test_Generic; + // declaration: keyAndDifficultPoints.principle.Test_Generic + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + // signature ()TT; + // declaration: T getValue() + public getValue()Ljava/lang/Object; + L0 + LINENUMBER 17 L0 + ALOAD 0 + GETFIELD keyAndDifficultPoints/principle/Test_Generic.value : Ljava/lang/Object; + ARETURN + L1 + LOCALVARIABLE this LkeyAndDifficultPoints/principle/Test_Generic; L0 L1 0 + // signature LkeyAndDifficultPoints/principle/Test_Generic; + // declaration: keyAndDifficultPoints.principle.Test_Generic + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + // signature (TT;)V + // declaration: void setValue(T) + public setValue(Ljava/lang/Object;)V + L0 + LINENUMBER 21 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD keyAndDifficultPoints/principle/Test_Generic.value : Ljava/lang/Object; + L1 + LINENUMBER 22 L1 + RETURN + L2 + LOCALVARIABLE this LkeyAndDifficultPoints/principle/Test_Generic; L0 L2 0 + // signature LkeyAndDifficultPoints/principle/Test_Generic; + // declaration: keyAndDifficultPoints.principle.Test_Generic + LOCALVARIABLE value Ljava/lang/Object; L0 L2 1 + // signature TT; + // declaration: T + MAXSTACK = 2 + MAXLOCALS = 2 +} + +``` + +可以明显的看到**泛型T**被替换成了**Object**。 + +​ 因为在Test_Generic中,T是一个无限定的类型变量,所以用Object替换。其结果就是一个普通的类,如同泛型加入java变成语言之前已经实现的那样。在程序中可以包含不同类型的Test_Generic,如Test_Generic或Test_Generic,但是,擦除类型后它们就成为原始的Test_Generic类型了,原始类型都是Object。 + +​ 从上面的那个例2中,我们也可以明白List被擦除类型后,原始类型也变成了Object,所以通过反射我们就可以存储字符串了。 + + + +2、如果类型变量有限定,那么原始类型就用第一个边界的类型变量来替换。 + +比如Test_Generic这样声明 + +```Java +class Test_Generic1 +``` + +我们还是看字节码(后面如无必须,只截取部分字节码) + +```Java +// class version 52.0 (52) +// access flags 0x20 +// signature Ljava/lang/Object; +// declaration: keyAndDifficultPoints/principle/Test_Generic1 +class keyAndDifficultPoints/principle/Test_Generic1 { + + // compiled from: Test_principle03.java + + // access flags 0x2 + // signature TT; + // declaration: T + private Ljava/util/List; value +``` + +会发现T变成了List + + + +如果顺序变一下 + +``` +class Test_Generic1 +``` + +字节码就变了 + +T变成了Collection + +``` +// class version 52.0 (52) +// access flags 0x20 +// signature Ljava/lang/Object; +// declaration: keyAndDifficultPoints/principle/Test_Generic1 +class keyAndDifficultPoints/principle/Test_Generic1 { + + // compiled from: Test_principle03.java + + // access flags 0x2 + // signature TT; + // declaration: T + private Ljava/util/Collection; value +``` + +也就是说在进行字节码编译的时候是使用离T最近的一个类型。 + + + +### 解答一个疑惑 + +在上文说到&的多重限定时 + +```Java +package keyAndDifficultPoints.principle; + +import java.util.*; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/16 23:30 + *

+ * 功能描述: + */ +public class Test_principle04 { + + public static void main(String[] args) { + + + /*---------------------测试多重限定符---------------------*/ + List list = new ArrayList<>(); + Queue queue = new ArrayDeque<>(); + LinkedList linkedList = new LinkedList<>(); + + //多重限定时,在编译的时候取最小范围或共同子类 + test2(list); +// test3(list); 编译报错 + test4(list); + + + //编译报错 +// test2(deque); +// test3(deque); +// test4(queue); + + //编译通过 + test2(linkedList); + test3(linkedList); + test4(linkedList); + + + } + + + //可以进行多重限定 + public static void test2(T t) { + + } + + //可以进行多重限定 + public static void test3(T t) { + + } + + //可以进行多重限定 + public static void test4(T t) { + + } + + //编译报错,无法进行多重限定 +// public static void test4(List dest, List src){ +// +// } + +} + +``` + +首先来看一下字节码 + +```Java +// class version 52.0 (52) +// access flags 0x21 +public class keyAndDifficultPoints/principle/Test_principle04 { + + // compiled from: Test_principle04.java + + // access flags 0x1 + public ()V + L0 + LINENUMBER 11 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this LkeyAndDifficultPoints/principle/Test_principle04; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x9 + public static main([Ljava/lang/String;)V + L0 + LINENUMBER 17 L0 + NEW java/util/ArrayList + DUP + INVOKESPECIAL java/util/ArrayList. ()V + ASTORE 1 + L1 + LINENUMBER 18 L1 + NEW java/util/ArrayDeque + DUP + INVOKESPECIAL java/util/ArrayDeque. ()V + ASTORE 2 + L2 + LINENUMBER 19 L2 + NEW java/util/LinkedList + DUP + INVOKESPECIAL java/util/LinkedList. ()V + ASTORE 3 + L3 + LINENUMBER 22 L3 + ALOAD 1 + INVOKESTATIC keyAndDifficultPoints/principle/Test_principle04.test2 (Ljava/util/List;)V + L4 + LINENUMBER 24 L4 + ALOAD 1 + INVOKESTATIC keyAndDifficultPoints/principle/Test_principle04.test4 (Ljava/util/Collection;)V + L5 + LINENUMBER 33 L5 + ALOAD 3 + INVOKESTATIC keyAndDifficultPoints/principle/Test_principle04.test2 (Ljava/util/List;)V + L6 + LINENUMBER 34 L6 + ALOAD 3 + INVOKESTATIC keyAndDifficultPoints/principle/Test_principle04.test3 (Ljava/util/Queue;)V + L7 + LINENUMBER 35 L7 + ALOAD 3 + INVOKESTATIC keyAndDifficultPoints/principle/Test_principle04.test4 (Ljava/util/Collection;)V + L8 + LINENUMBER 38 L8 + RETURN + L9 + LOCALVARIABLE args [Ljava/lang/String; L0 L9 0 + LOCALVARIABLE list Ljava/util/List; L1 L9 1 + LOCALVARIABLE queue Ljava/util/Queue; L2 L9 2 + LOCALVARIABLE linkedList Ljava/util/LinkedList; L3 L9 3 + // signature Ljava/util/LinkedList; + // declaration: java.util.LinkedList + MAXSTACK = 2 + MAXLOCALS = 4 + + // access flags 0x9 + // signature (TT;)V + // declaration: void test2(T) + public static test2(Ljava/util/List;)V + L0 + LINENUMBER 44 L0 + RETURN + L1 + LOCALVARIABLE t Ljava/util/List; L0 L1 0 + // signature TT; + // declaration: T + MAXSTACK = 0 + MAXLOCALS = 1 + + // access flags 0x9 + // signature (TT;)V + // declaration: void test3(T) + public static test3(Ljava/util/Queue;)V + L0 + LINENUMBER 49 L0 + RETURN + L1 + LOCALVARIABLE t Ljava/util/Queue; L0 L1 0 + // signature TT; + // declaration: T + MAXSTACK = 0 + MAXLOCALS = 1 + + // access flags 0x9 + // signature (TT;)V + // declaration: void test4(T) + public static test4(Ljava/util/Collection;)V + L0 + LINENUMBER 54 L0 + RETURN + L1 + LOCALVARIABLE t Ljava/util/Collection; L0 L1 0 + // signature TT; + // declaration: T + MAXSTACK = 0 + MAXLOCALS = 1 +} + +``` + + + +`test4()`方法里离T最近的是`Collection`,那么T在编译后就被`Collection`代替了。那按理来说 + +``` +test4(queue); +``` + +1、这里我们传一个Collection的实现类Queue,也应该是可以的啊,但是为什么报错了呢?注意一点报错报的是编译错误,泛型提供编译前检测机制,也就是说在没运行前,泛型规定了`多重限定时,在编译的时候取最小范围或共同子类`。 + +2、那实际上到底可以不可以传Queue呢?根据之前的讲解,我相信大家已经有了结论。实际上是可以的,只不过要跳过编译检测机制,通过反射来放Queue。 + + + +### 泛型方法调用 + +​ 在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。 + +``` +class Test { + public static void main(String[] args) { + //不指定泛型的时候 + int a1 = add(1, 2); //这两个参数都是Integer,所以T为Integer类型 + Number b1 = add(1, 1.2);//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number + Object c1 = add(1, "my");//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object + + //指定泛型的时候 + int a = Test.add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类 +// int b = Test.add(1, 2.2);//编译错误,指定了Integer,不能为Float + Number c = Test.add(1, 2.2); //指定为Number,所以可以为Integer和Float + } + + //这是一个简单的泛型方法 + public static T add(T x, T y) { + return x; + } +} +``` + + + +## 类型擦除引起的问题及解决方法 + +### 类型检测针对谁? + + + +```Java + public static void main(String[] args) { + ArrayList arrayList=new ArrayList(); + arrayList.add("123"); + arrayList.add(123);//编译错误 + } +``` + +类型擦除后,原始类型为Object,是应该运行任意引用类型的添加的。可实际上却不是这样,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 + +那么,这么类型检查是针对谁的呢?我们来看例子: + +```Java +public static void main(String[] args) { + ArrayList arrayList = new ArrayList(); + arrayList.add(1); //编译报错 + + ArrayList arrayList1 = new ArrayList(); //第一种 情况 + arrayList1.add(1); //编译报错 + + ArrayList arrayList2 = new ArrayList();//第二种 情况 + arrayList2.add(1); + } +``` + +通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 + + + +### 自动类型转换 + +因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢? + +我么来看一下List的get()方法: + +```Java +public E get(int index) { + rangeCheck(index); + + return elementData(index); + } + +E elementData(int index) { + return (E) elementData[index]; + } +``` + +可以看到基本各个类都已经自动帮你转了。 + + + +### 类型擦除与多态的冲突和解决方法 + +这个其实是类型擦除引起的最大的问题了。 + +```Java +public class Test_principle05 { + public static void main(String[] args) { + } +} +class Generic { + //key这个成员变量的类型为T,T的类型由外部指定 + private T var; + + public T getVar() { + return var; + } + + public void setVar(T var) { + this.var = var; + } +} + +class MyGeneric extends Generic{ + @Override + public Integer getVar() { + return super.getVar(); + } + @Override + public void setVar(Integer var) { + super.setVar(var); + } +} +``` + +实际上,从他们的@Override标签中也可以看到,在子类中重写这两个方法一点问题也没有,实际上是这样的吗? + + + +**分析:** + +泛型擦除后,父类是下面这样子 + +```java +class Generic { + //key这个成员变量的类型为T,T的类型由外部指定 + private Object var; + + public Object getVar() { + return var; + } + + public void setVar(Object var) { + this.var = var; + } +} +``` + +子类还是这样 + +```java +class MyGeneric extends Generic{ + @Override + public Integer getVar() { + return super.getVar(); + } + @Override + public void setVar(Integer var) { + super.setVar(var); + } +} +``` + + + +先来分析setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载。 + +> **重载(Overload):首先是位于一个类之中或者其子类中,具有相同的方法名,但是方法的参数不同,返回值类型可以相同也可以不同。** +> +> (1):方法名必须相同。 +> +> (2):方法的参数列表一定不一样。 +> +> (3):访问修饰符和返回值类型可以相同也可以不同。 +> +> **重写(override):一般都是表示子类和父类之间的关系,其主要的特征是:方法名相同,参数相同,但是具体的实现不同。** +> +> **重写的特征:** +> +> (1):方法名必须相同,返回值类型必须相同 +> +> (2):参数列表必须相同 +> +> (3):访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。 +> +> (4):子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。 +> +> (5):构造方法不能被重写 + + + +我们来测试下到底是重载还是重写 + +```Java + public static void main(String[] args) { + MyGeneric myGeneric = new MyGeneric(); + myGeneric.setVar(new Integer(1)); + myGeneric.setVar(new Object());//编译错误 + } +``` + +如果是重载的话,第四行代码是不会报错的,因为调的是不同的重载方法。但是发现编译报错了,也就是说没有参数是Object的这样的重载函数。所以说是重写了,导致MyGeneric对象只能调用自己重写的方法。 + + + +为什么会这样呢? + +原因是这样的,我们传入父类的泛型类型是Integer,Generic,我们的本意是将泛型类变为如下: + +```java +class Generic { + //key这个成员变量的类型为T,T的类型由外部指定 + private Integer var; + + public Integer getVar() { + return var; + } + + public void setVar(Integer var) { + this.var = var; + } +} +``` + +然后再子类中重写参数类型为Date的那两个方法,实现继承中的多态。 + +​ 可是由于种种原因,虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。JVM知道你的本意吗?知道,可是它能直接实现吗,不能。如果真的不能的话,那我们怎么去重写我们想要的Integer类型参数的方法啊。 + + + +JVM采用了一个特殊的方法,来完成这项功能,那就是**桥方法**。 + +我们对下面这个类进行编译,看其字节码 + +```Java +public class MyGeneric extends Generic{ + public static void main(String[] args) { + + } + + @Override + public Integer getVar() { + return super.getVar(); + } + @Override + public void setVar(Integer var) { + super.setVar(var); + } +} +``` + + + +字节码: + +```Java +// class version 52.0 (52) +// access flags 0x21 +// signature LkeyAndDifficultPoints/principle/Generic; +// declaration: keyAndDifficultPoints/principle/MyGeneric extends keyAndDifficultPoints.principle.Generic +public class keyAndDifficultPoints/principle/MyGeneric extends keyAndDifficultPoints/principle/Generic { + + // compiled from: MyGeneric.java + + // access flags 0x1 + public ()V + L0 + LINENUMBER 9 L0 + ALOAD 0 + INVOKESPECIAL keyAndDifficultPoints/principle/Generic. ()V + RETURN + L1 + LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x9 + public static main([Ljava/lang/String;)V + L0 + LINENUMBER 12 L0 + RETURN + L1 + LOCALVARIABLE args [Ljava/lang/String; L0 L1 0 + MAXSTACK = 0 + MAXLOCALS = 1 + + // access flags 0x1 + public getVar()Ljava/lang/Integer; //这是我们重写的getVar()方法 + L0 + LINENUMBER 16 L0 + ALOAD 0 + INVOKESPECIAL keyAndDifficultPoints/principle/Generic.getVar ()Ljava/lang/Object; + CHECKCAST java/lang/Integer + ARETURN + L1 + LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public setVar(Ljava/lang/Integer;)V ////这是我们重写的setVar()方法 + L0 + LINENUMBER 20 L0 + ALOAD 0 + ALOAD 1 + INVOKESPECIAL keyAndDifficultPoints/principle/Generic.setVar (Ljava/lang/Object;)V + L1 + LINENUMBER 21 L1 + RETURN + L2 + LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L2 0 + LOCALVARIABLE var Ljava/lang/Integer; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1041 + public synthetic bridge setVar(Ljava/lang/Object;)V //编译时由编译器生成的桥方法 + L0 + LINENUMBER 9 L0 + ALOAD 0 + ALOAD 1 + CHECKCAST java/lang/Integer + INVOKEVIRTUAL keyAndDifficultPoints/principle/MyGeneric.setVar (Ljava/lang/Integer;)V + RETURN + L1 + LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L1 0 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1041 + public synthetic bridge getVar()Ljava/lang/Object; //编译时由编译器生成的桥方法 + L0 + LINENUMBER 9 L0 + ALOAD 0 + INVOKEVIRTUAL keyAndDifficultPoints/principle/MyGeneric.getVar ()Ljava/lang/Integer; + ARETURN + L1 + LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 +} + +``` + +​ 从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法。最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。 + +所以,虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。 + diff --git a/docs/Java/concurrency/Java并发体系-第一阶段-多线程基础知识.md b/docs/Java/concurrency/Java并发体系-第一阶段-多线程基础知识.md new file mode 100644 index 0000000..58f6c96 --- /dev/null +++ b/docs/Java/concurrency/Java并发体系-第一阶段-多线程基础知识.md @@ -0,0 +1,2031 @@ +--- +title: Java并发体系-第一阶段-多线程基础知识 +tags: + - Java并发 + - 原理 + - 源码 +categories: + - Java并发 +keywords: Java并发,原理,源码 +description: Java并发体系-第一阶段-多线程基础知识,后续会陆续出更深入的。 +cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png' +top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg' +abbrlink: efc79183 +date: 2020-10-05 22:40:58 +--- + + + + + +# 程序、进程、线程的理解 + +1、程序(programm) +概念:是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。 + +2、进程(process) +概念:程序的一次执行过程,或是正在运行的一个程序。 +说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域 + +3、线程(thread) +概念:进程可进一步细化为线程,是一个程序内部的一条执行路径。 +说明:线程作为CPU调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。 + + + + + +补充: + + + +进程可以细化为多个线程。 +每个线程,拥有自己独立的:栈、程序计数器 +多个线程,共享同一个进程中的结构:方法区、堆。 + + + +# 并行与并发 + +## 单核CPU与多核CPU的理解 +- 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费。)但是因为CPU时间单元特别短,因此感觉不出来。 +- 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的) +- 一个Java应用程序java.exe,其实至少三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。 + +## 并行与并发的理解 +并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。 +并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事 + +# 创建线程的几种方法 + + + +## 继承Thread类创建线程 + +多线程的创建,方式一:继承于Thread类 +1. 创建一个继承于Thread类的子类 +2. 重写Thread类的run() --> 将此线程执行的操作声明在run()中 +3. 创建Thread类的子类的对象 +4. 通过此对象调用start() + +* 例子:遍历100以内的所有的偶数 + +```Java + +//1. 创建一个继承于Thread类的子类 +class MyThread extends Thread { + //2. 重写Thread类的run() + @Override + public void run() { + for (int i = 0; i < 100; i++) { + if(i % 2 == 0){ + System.out.println(Thread.currentThread().getName() + ":" + i); + } + } + } +} + + +public class ThreadTest { + public static void main(String[] args) { + //3. 创建Thread类的子类的对象 + MyThread t1 = new MyThread(); + + //4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run() + t1.start(); + //问题一:我们不能通过直接调用run()的方式启动线程。 +// t1.run(); + + //问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException +// t1.start(); + //我们需要重新创建一个线程的对象 + MyThread t2 = new MyThread(); + t2.start(); + + + //如下操作仍然是在main线程中执行的。 + for (int i = 0; i < 100; i++) { + if(i % 2 == 0){ + System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()************"); + } + } + } + +} + +``` + + + +## 实现Runnable接口创建线程 + +1、创建多线程的方式二:实现Runnable接口 + +1. 创建一个实现了Runnable接口的类 +2. 实现类去实现Runnable中的抽象方法:run() +3. 创建实现类的对象 +4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 +5. 通过Thread类的对象调用start() + +2、 比较创建线程的两种方式。 + 开发中:优先选择:实现Runnable接口的方式 + 原因:实现的方式没有类的单继承性的局限性,实现的方式更适合来处理多个线程有共享数据的情况。 + + + +```java + +//1. 创建一个实现了Runnable接口的类 +class MThread implements Runnable{ + + //2. 实现类去实现Runnable中的抽象方法:run() + @Override + public void run() { + for (int i = 0; i < 100; i++) { + if(i % 2 == 0){ + System.out.println(Thread.currentThread().getName() + ":" + i); + } + + } + } +} + + +public class ThreadTest1 { + public static void main(String[] args) { + //3. 创建实现类的对象 + MThread mThread = new MThread(); + //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 + Thread t1 = new Thread(mThread); + t1.setName("线程1"); + //5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run() + t1.start(); + + //再启动一个线程,遍历100以内的偶数 + Thread t2 = new Thread(mThread); + t2.setName("线程2"); + t2.start(); + } + +} +``` + + + +## Thread和Runnable的关系 + + 联系:public class Thread implements Runnable + 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。 + + + +### Runnable接口构造线程源码 + +``` +/*下面是Thread类的部分源码*/ + +//1.用Runnable接口创建线程时会进入这个方法 +public Thread(Runnable target) { + init(null, target, "Thread-" + nextThreadNum(), 0); + } + +//2.接着调用这个方法 +private void init(ThreadGroup g, Runnable target, String name, + long stackSize) { + init(g, target, name, stackSize, null, true); + } + +//3.再调用这个方法 +private void init(ThreadGroup g, Runnable target, String name, + long stackSize, AccessControlContext acc, + boolean inheritThreadLocals) { + if (name == null) { + throw new NullPointerException("name cannot be null"); + } + + this.name = name; + + Thread parent = currentThread(); + SecurityManager security = System.getSecurityManager(); + if (g == null) { + /* Determine if it's an applet or not */ + + /* If there is a security manager, ask the security manager + what to do. */ + if (security != null) { + g = security.getThreadGroup(); + } + + /* If the security doesn't have a strong opinion of the matter + use the parent thread group. */ + if (g == null) { + g = parent.getThreadGroup(); + } + } + + /* checkAccess regardless of whether or not threadgroup is + explicitly passed in. */ + g.checkAccess(); + + /* + * Do we have the required permissions? + */ + if (security != null) { + if (isCCLOverridden(getClass())) { + security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); + } + } + + g.addUnstarted(); + + this.group = g; + this.daemon = parent.isDaemon(); + this.priority = parent.getPriority(); + if (security == null || isCCLOverridden(parent.getClass())) + this.contextClassLoader = parent.getContextClassLoader(); + else + this.contextClassLoader = parent.contextClassLoader; + this.inheritedAccessControlContext = + acc != null ? acc : AccessController.getContext(); + //4.最后在这里将Runnable接口(target)赋值给Thread自己的target成员属性 + this.target = target; + setPriority(priority); + if (inheritThreadLocals && parent.inheritableThreadLocals != null) + this.inheritableThreadLocals = + ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); + /* Stash the specified stack size in case the VM cares */ + this.stackSize = stackSize; + + /* Set thread ID */ + tid = nextThreadID(); + } + +/*如果你是实现了runnable接口,那么在上面的代码中target便不会为null,那么最终就会通过重写的规则去调用真正实现了Runnable接口(你之前传进来的那个Runnable接口实现类)的类里的run方法*/ +@Override + public void run() { + + if (target != null) { + target.run(); + } + } +``` + +1、多线程的设计之中,使用了代理设计模式的结构,用户自定义的线程主体只是负责项目核心功能的实现,而所有的辅助实现全部交由Thread类来处理。 +2、在进行Thread启动多线程的时候调用的是start()方法,而后找到的是run()方法,但通过Thread类的构造方法传递了一个Runnable接口对象的时候,那么该接口对象将被Thread类中的target属性所保存,在start()方法执行的时候会调用Thread类中的run()方法。而这个run()方法去调用实现了Runnable接口的那个类所重写过run()方法,进而执行相应的逻辑。多线程开发的本质实质上是在于多个线程可以进行同一资源的抢占,那么Thread主要描述的是线程,而资源的描述是通过Runnable完成的。如下图所示: + + + + + + + +### Thread类构造线程源码 + +```java +MyThread t2 = new MyThread(); //这个构造函数会默认调用Super();也就是Thread类的无参构造 +``` + + + +``` +//代码从上往下顺序执行 +public Thread() { + init(null, null, "Thread-" + nextThreadNum(), 0); + } + +private void init(ThreadGroup g, Runnable target, String name, + long stackSize) { + init(g, target, name, stackSize, null, true); + } + +private void init(ThreadGroup g, Runnable target, String name, + long stackSize, AccessControlContext acc, + boolean inheritThreadLocals) { + if (name == null) { + throw new NullPointerException("name cannot be null"); + } + + this.name = name; + + Thread parent = currentThread(); + SecurityManager security = System.getSecurityManager(); + if (g == null) { + /* Determine if it's an applet or not */ + + /* If there is a security manager, ask the security manager + what to do. */ + if (security != null) { + g = security.getThreadGroup(); + } + + /* If the security doesn't have a strong opinion of the matter + use the parent thread group. */ + if (g == null) { + g = parent.getThreadGroup(); + } + } + + /* checkAccess regardless of whether or not threadgroup is + explicitly passed in. */ + g.checkAccess(); + + /* + * Do we have the required permissions? + */ + if (security != null) { + if (isCCLOverridden(getClass())) { + security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); + } + } + + g.addUnstarted(); + + this.group = g; + this.daemon = parent.isDaemon(); + this.priority = parent.getPriority(); + if (security == null || isCCLOverridden(parent.getClass())) + this.contextClassLoader = parent.getContextClassLoader(); + else + this.contextClassLoader = parent.contextClassLoader; + this.inheritedAccessControlContext = + acc != null ? acc : AccessController.getContext(); + this.target = target; + setPriority(priority); + if (inheritThreadLocals && parent.inheritableThreadLocals != null) + this.inheritableThreadLocals = + ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); + /* Stash the specified stack size in case the VM cares */ + this.stackSize = stackSize; + + /* Set thread ID */ + tid = nextThreadID(); + } + +/*由于这里是通过继承Thread类来实现的线程,那么target这个东西就是Null。但是因为你继承了Runnable接口并且重写了run(),所以最终还是调用子类的run()*/ + @Override + public void run() { + if (target != null) { + target.run(); + } + } +``` + + + +### 最直观的代码描述 + +``` +class Window extends Thread{ + + + private int ticket = 100; + @Override + public void run() { + + while(true){ + + if(ticket > 0){ + System.out.println(getName() + ":卖票,票号为:" + ticket); + ticket--; + }else{ + break; + } + + } + + } +} + + +public class WindowTest { + public static void main(String[] args) { + Window t1 = new Window(); + Window t2 = new Window(); + Window t3 = new Window(); + + + t1.setName("窗口1"); + t2.setName("窗口2"); + t3.setName("窗口3"); + + t1.start(); + t2.start(); + t3.start(); + + } +} +``` + + + +``` +class Window1 implements Runnable{ + + private int ticket = 100; + + @Override + public void run() { + while(true){ + if(ticket > 0){ + System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); + ticket--; + }else{ + break; + } + } + } +} + + +public class WindowTest1 { + public static void main(String[] args) { + Window1 w = new Window1(); + + Thread t1 = new Thread(w); + Thread t2 = new Thread(w); + Thread t3 = new Thread(w); + + t1.setName("窗口1"); + t2.setName("窗口2"); + t3.setName("窗口3"); + + t1.start(); + t2.start(); + t3.start(); + } + +} +``` + +1、继承Thread类的方式,new了三个Thread,实际上是有300张票。 + +2、实现Runnable接口的方式,new了三个Thread,实际上是有100张票。 + +3、也就是说实现Runnable接口的线程中,成员属性是所有线程共有的。但是继承Thread类的线程中,成员属性是各个线程独有的,其它线程看不到,除非采用static的方式才能使各个线程都能看到。 + +4、就像上面说的Runnable相当于资源,Thread才是线程。用Runnable创建线程时,new了多个Thread,但是传进去的参数都是同一个Runnable(资源)。用Thread创建线程时,就直接new了多个线程,每个线程都有自己的Runnable(资源)。在Thread源码中就是用target变量(这是一个Runnable类型的变量)来表示这个资源。 + +5、同时因为这两个的区别,在并发编程中,继承了Thread的子类在进行线程同步时不能将成员变量当做锁,因为多个线程拿到的不是同一把锁,不过用static变量可以解决这个问题。而实现了Runnable接口的类在进行线程同步时没有这个问题。 + + + + + +## 实现Callable接口创建线程 + +```Java +//Callable实现多线程 +class MyThread implements Callable {//线程的主体类 + + @Override + public String call() throws Exception { + for (int x = 0; x < 10; x++) { + System.out.println("*******线程执行,x=" + x + "********"); + } + return "线程执行完毕"; + } +} + +public class Demo1 { + public static void main(String[] args) throws Exception { + FutureTask task = new FutureTask<>(new MyThread()); + new Thread(task).start(); + System.out.println("线程返回数据" + task.get()); + + } +} +``` + +Callable最主要的就是提供带有返回值的call方法来创建线程。不过Callable要和Future实现类连着用,关于Future的一系列知识会在后面几个系列讲到。 + + + +# 策略模式在Thread和Runnable中的应用 + +Runnable接口最重要的方法-----run方法,使用了**策略者模式**将执行的逻辑(run方法)和程序的执行单元(start0方法)分离出来,使用户可以定义自己的程序处理逻辑,更符合面向对象的思想。 + + + + + + + +# Thread的构造方法 + +- 创建线程对象Thread,`默认有一个线程名,以Thread-开头,从0开始计数`,如“Thread-0、Thread-1、Thread-2 …” + +- 如果没有传递Runnable或者没有覆写Thread的run方法,`该Thread不会调用任何方法` + +- 如果传递Runnable接口的实例或者覆写run方法,则`会执行该方法的逻辑单元`(逻辑代码) + +- 如果构造线程对象时,未传入ThreadGroup,`Thread会默认获取父线程的ThreadGroup作为该线程的ThreadGroup`,此时子线程和父线程会在同一个ThreadGroup中 + +- stackSize可以`提高线程栈的深度`,放更多栈帧,但是会`减少能创建的线程数目` + +- stackSize默认是0,`如果是0,代表着被忽略,该参数会被JNI函数调用`,但是注意某些平台可能会失效,`可以通过“-Xss10m”设置` + +具体的介绍可以看Java的API文档 + +```Java +/*下面是Thread 的部分源码*/ + +public Thread(Runnable target) { + init(null, target, "Thread-" + nextThreadNum(), 0); +} + +public Thread(String name) { + init(null, null, name, 0); +} + ↓ ↓ ↓ + ↓ ↓ + ↓ +private void init(ThreadGroup g, Runnable target, String name, + long stackSize) { + init(g, target, name, stackSize, null, true); +} + ↓ ↓ ↓ + ↓ ↓ + ↓ +private void init(ThreadGroup g, Runnable target, String name, + long stackSize, AccessControlContext acc, + boolean inheritThreadLocals) { + //中间源码省略 + this.target = target;//① +} + +/* What will be run. */ +private Runnable target; //Thread类中的target属性 + +@Override +public void run() { + if (target != null) { //② + target.run(); + } +} + +``` + +> 源码标记解读: +> +> 1、如果Thread类的构造方法传递了一个Runnable接口对象 +> +> ①那么该接口对象将被Thread类中的target属性所保存。 +> +> ②在start()方法执行的时候会调用Thread类中的run()方法。因为target不为null, target.run()就去调用实现Runnable接口的子类重写的run()。 +> +> 2、如果Thread类的构造方传没传Runnable接口对象 +> +> ①Thread类中的target属性保存的就是null。 +> +> ②在start()方法执行的时候会调用Thread类中的run()方法。因为target为null,只能去调用继承Thread的子类所重写的run()。 + + + +JVM一旦启动,虚拟机栈的大小已经确定了。但是如果你创建Thread的时候传了stackSize(该线程占用的stack大小),该参数会被JNI函数去使用。如果没传这个参数,就默认为0,表示忽略这个参数。注:stackSize在有一些平台上是无效的。 + + + +# start()源码 + +```Java +public synchronized void start() { + + if (threadStatus != 0) + throw new IllegalThreadStateException();//① + + + group.add(this); + + boolean started = false; + try { + start0(); + started = true; + } finally { + try { + if (!started) { + group.threadStartFailed(this); + } + } catch (Throwable ignore) { + /* do nothing. If start0 threw a Throwable then + it will be passed up the call stack */ + } + } +} + +private native void start0(); + + +@Override +public void run() { + if (target != null) { + target.run(); + } +} +``` + +> 源码标记解读: +> +> ①当多次调用start(),会抛出throw new IllegalThreadStateException()异常。也就是每一个线程类的对象只允许启动一次,如果重复启动则就抛出此异常。 + + + + + +## 为什么线程的启动不直接使用run()而必须使用start()呢? + +1、如果直接调用run()方法,相当于就是简单的调用一个普通方法。 + + + +2、run()的调用是在start0()这个Native C++方法里调用的 + + + +# 线程生命周期 + +Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态,这几个状态在Java源码中用枚举来表示。 + + + +线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示。 + + + +> 图中 wait到 runnable状态的转换中,`join`实际上是`Thread`类的方法,但这里写成了`Object`。 + +1、由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。 + +2、操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。 + +3、调用sleep()方法,会进入Blocked状态。sleep()结束之后,Blocked状态首先回到的是Runnable状态中的Ready(也就是可运行状态,但并未运行)。只有拿到了cpu的时间片才会进入Runnable中的Running状态。 + + + +# Thread常用API + +* 获取当前存活的线程数:`public int activeCount()` +* 获取当前线程组的线程的集合:`public int enumerate(Thread[] list)` + + + +# 一个Java程序有哪些线程? + +1、当你调用一个线程start()方法的时候,此时至少有两个线程,一个是调用你的线程,还有一个是被你创建出来的线程。 + +例子: + +```java +public static void main(String[] args) { + Thread t1 = new Thread() { + @Override + public void run() { + System.out.println("=========="); + } + }; + t1.start(); +} +``` + +这里面就是一个调用你的线程(main线程),一个被你创建出来的线程(t1,名字可能是Thread-0) + +2、当JVM启动后,实际有多个线程,但是至少有一个非守护线程(比如main线程)。 + + + +- Finalizer:GC守护线程 + +- RMI:Java自带的远程方法调用(秋招面试,有个面试官问过) + +- Monitor :是一个守护线程,负责监听一些操作,也在main线程组中 + +- 其它:我用的是IDEA,其它的应该是IDEA的线程,比如鼠标监听啥的。 + + + + + + + +# 守护线程 + + + +```Java +public static void main(String[] args) throws InterruptedException { + + Thread t = new Thread() { + + @Override + public void run() { + try { + System.out.println(Thread.currentThread().getName() + " running"); + Thread.sleep(100000);//① + System.out.println(Thread.currentThread().getName() + " done."); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; //new + + t.setDaemon(true);//② + t.start(); + Thread.sleep(5_000); //JDK1.7 + System.out.println(Thread.currentThread().getName()); +} +``` + +> 源码标记解读: +> +> ①变量名为t的线程Thread-0,睡眠100秒。 +> +> ②但是在主函数里Thread-0设置成了main线程的守护线程。所以5秒之后main线程结束了,即使在①这里守护线程还是处于睡眠100秒的状态,但由于他是守护线程,非守护线程main结束了,守护线程也必须结束。 +> +> 1、但是如果Thread-0线程不是守护线程,即使main线程结束了,Thread-0线程仍然会睡眠100秒再结束。 +> +> * 当主线程死亡后,守护线程会跟着死亡 +> * 可以帮助做一些辅助性的东西,如“心跳检测” +> * 设置守护线程:`public final void setDaemon(boolean on)` + +## 用处 + +A和B之间有一条网络连接,可以用守护线程来进行发送心跳,一旦A和B连接断开,非守护线程就结束了,守护线程(也就是心跳没有必要再发送了)也刚好断开。 + +```Java +public static void main(String[] args) { + + Thread t = new Thread(() -> { + Thread innerThread = new Thread(() -> { + try { + while (true) { + System.out.println("Do some thing for health check."); + Thread.sleep(1_000); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + + // innerThread.setDaemon(true); + innerThread.start(); + + try { + Thread.sleep(1_000); + System.out.println("T thread finish done."); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + //t.setDaemon(true); + t.start(); +} + +/* +设置该线程为守护线程必须在启动它之前。如果t.start()之后,再t.setDaemon(true);会抛出IllegalThreadStateException +*/ +``` + +> 输出结果: +> +> Do some thing for health check. +> Do some thing for health check. +> T thread finish done. //此时main线程已经结束,但是由于innerThread还在发送心跳,应用不会关闭 +> Do some thing for health check. +> Do some thing for health check. +> Do some thing for health check. +> Do some thing for health check. + + + +> 守护线程还有其它很多用处,在后面的文章里还会有出现。 + + + +# join方法 + + + +**例子1** + +```java +public static void main(String[] args) throws InterruptedException { + Thread t1 = new Thread(() -> { + IntStream.range(1, 1000) + .forEach(i -> System.out.println(Thread.currentThread().getName() + "->" + i)); + }); + Thread t2 = new Thread(() -> { + IntStream.range(1, 1000) + .forEach(i -> System.out.println(Thread.currentThread().getName() + "->" + i)); + }); + + t1.start(); + t2.start(); + t1.join(); + t2.join(); + + Optional.of("All of tasks finish done.").ifPresent(System.out::println); + IntStream.range(1, 1000) + .forEach(i -> System.out.println(Thread.currentThread().getName() + "->" + i)); +} +``` + +* 默认传入的数字为0,这里是在main线程里调用了两个线程的join(),所以main线程会等到Thread-0和Thread-1线程执行完再执行它自己。 +* join必须在start方法之后,并且join()是对wait()的封装。(源码中可以清楚的看到) + +- 也就是说,t.join()方法阻塞调用此方法的线程(calling thread)进入 TIMED_WAITING或WAITING 状态。直到线程t完成,此线程再继续。 +- join也有人理解成插队,比如在main线程中调用t.join(),就是t线程要插main线程的队,main线程要去等待。 + + + +**例子2** + +```java +public static void main(String[] args) throws InterruptedException { + Thread t1 = new Thread(() -> { + IntStream.range(1, 1000) + .forEach(i -> System.out.println(Thread.currentThread().getName() + "->" + i)); + }); + Thread t2 = new Thread(() -> { + try { + t1.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + IntStream.range(1, 1000) + .forEach(i -> System.out.println(Thread.currentThread().getName() + "->" + i)); + }); + + t1.start(); + t2.start(); +// t1.join(); + t2.join(); + + Optional.of("All of tasks finish done.").ifPresent(System.out::println); + IntStream.range(1, 1000) + .forEach(i -> System.out.println(Thread.currentThread().getName() + "->" + i)); + } +``` + +- 这里是在t2(我们以后就都用变量名来称呼线程了)线程了。t1.join()了。所以t2线程会等待t1线程打印完,t2自己才会打印。然后t2.join(),main线程也要等待t2线程。总体执行顺序就是t1-->t2-->main +- 通过上方例子可以用join实现类似于CompletableFuture的异步任务编排。(后面会讲) + + + +# 中断 + +记住中断只是一个状态,Java的方法可以选择对这个中断进行响应,也可以选择不响应。响应的意思就是写相对应的代码执行相对应的操作,不响应的意思就是什么代码都不写。 + +## wait()中断测试 + +```Java +public static void main(String[] args) { + + Thread t = new Thread() { + @Override + public void run() { + while (true) { + synchronized (MONITOR) { + try { + MONITOR.wait(10); + } catch (InterruptedException e) { + System.out.println("wait响应中断");//pos_1 + e.printStackTrace();//pos_2 + System.out.println(isInterrupted());//pos_3 + } + } + } + } + }; + + t.start(); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + System.out.println("sleep响应中断"); + e.printStackTrace(); + } + System.out.println(t.isInterrupted());//pos_4 + t.interrupt(); + System.out.println(t.isInterrupted());//pos_5 +} +``` + +> 注释掉e.printStackTrace();的输出 +> +> false //pos_4 +> true //pos_5 t.isInterrupted()之后会立即清除中断状态 +> wait响应中断 //pos_1 +> false //pos_3 因为pos_5清除了中断状态,所以这里检测到就是flase,没有被中断过 + +* 当该线程在wait()、join()、sleep(long, int)状态时,如果被打断,则会收到一个异常提醒。因为这些方法都抛出了 + + ```throws InterruptedException``` 这个异常,通过try catch可以做相应的处理。 + +* 但是只要线程被打断,无论哪个方法都可以通过`isInterrupted()`方法检测到打断的状态。 + + + + + +## join中断测试 + +``` +Thread main = Thread.currentThread(); +Thread t2 = new Thread() { + @Override + public void run() { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + main.interrupt(); //pos_1 + System.out.println("interrupt"); + } +}; + +t2.start(); +try { + t.join(); //pos_2 +} catch (InterruptedException e) { + e.printStackTrace(); +} +``` + +1、pos_2这里join的是main线程,所以pos_1这里需要中断main线程,才能收到中断信息。 + + + +## 两个判断中断状态的方法 + + + +```java +public static boolean interrupted() + +public boolean isInterrupted()//这个会清除中断状态 + +``` + +为什么要这么设置呢?原因在于: + +* interrupted()是一个静态方法,可以在Runnable接口实例中使用 +* isInterrupted()是一个Thread的实例方法,在重写Thread的run方法时使用 + + + +```java +public class ThreadInterrupt { + public static void main(String[] args) throws InterruptedException { + Thread t1 = new Thread(() -> { + System.out.println(Thread.interrupted()); + }); //这个new Thread用的是runnable接口那个构造函数 + + Thread t2 = new Thread(){ + @Override + public void run() { + System.out.println(isInterrupted()); + } + };//这个new Thread用的就是Thread的空参构造 + + } +} + +``` + +也就是说接口中不能调用Thread的实例方法,只能通过静态方法来判断是否发生中断 + + + +# 关闭线程 + +## 优雅的关闭(通过一个Boolean) + +```Java +private static class Worker extends Thread { + private volatile boolean start = true; + + @Override + public void run() { + while (start) { + //执行相应的工作 + } + } + + public void shutdown() { + this.start = false; + } +} + +public static void main(String[] args) { + Worker worker = new Worker(); + worker.start(); + + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + worker.shutdown(); +} +``` + + + + + +## 通过判断中断状态 + +```java +private static class Worker extends Thread { + + @Override + public void run() { + while (true) { + if (Thread.interrupted()){ + break; + } + //pos_1 + } + + } +} + +public static void main(String[] args) { + Worker worker = new Worker(); + worker.start(); + + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + worker.interrupt(); +} +``` + +1、但是如果pos_1位置有一个很费时的IO操作,就没有机会执行到if判断那里,也就不能关闭线程。所以就需要下面的暴力方法 + + + +## 暴力关闭(守护线程) + +```java +public class ThreadService { + + //执行线程 + private Thread executeThread; + + private boolean finished = false; + + public void execute(Runnable task) { + executeThread = new Thread() { + @Override + public void run() { + Thread runner = new Thread(task); + runner.setDaemon(true);//创建一个守护线程,让守护线程来执行工作 + + runner.start(); + try { +/** + * 1、要让executeThread等守护线程执行完,才能执行executeThread自己的逻辑。不然守护线程 + * 可能就来不及执行真正的工作就死了。所以这里要join + * 2、runner.join(),所以实际上等待的是executeThread //pos_1 + */ + runner.join(); + finished = true; + } catch (InterruptedException e) { + //e.printStackTrace(); + } + } + }; + + executeThread.start(); + } + + public void shutdown(long mills) { + long currentTime = System.currentTimeMillis(); + while (!finished) { + if ((System.currentTimeMillis() - currentTime) >= mills) { + System.out.println("任务超时,需要结束他!"); + /* + * pos_1那里,由于实际等待的是executeThread,所以这里中断executeThread。 + * pos_1就可以捕获到中断,执行线程(executeThread)就结束了,进而真正执行任务的 + * 守护线程runner也结束了 + */ + executeThread.interrupt(); + break; + } + + try { + executeThread.sleep(1); + } catch (InterruptedException e) { + System.out.println("执行线程被打断!"); + break; + } + } + + finished = false; + } +} + + +public class ThreadCloseForce { + + + public static void main(String[] args) { + + ThreadService service = new ThreadService(); + long start = System.currentTimeMillis(); + service.execute(() -> { + //load a very heavy resource. 模拟任务超时 + /*while (true) { + + }*/ + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + service.shutdown(10000); + long end = System.currentTimeMillis(); + System.out.println(end - start); + } +} +``` + +**使用场景**:分布式文件拷贝,如果拷贝的时间过长,则关闭该程序,防止程序一直阻塞。或者其他执行耗时很长的任务 + +守护线程的应用场景有很多 + + + +# 并发编程中的三个问题 + +## 可见性 + + + +### 可见性概念 + +可见性(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 + *

+ * 功能描述: 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值是8,B线程又执行9: getstatic得到前一个值是7。马上A线程就把8赋值给了num变量。但是B线程已经拿到了之前的值7,B线程是在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 + + org.openjdk.jcstress +jcstress-core +${jcstress.version} + +``` + + + +```java +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。 + +情况4:0,发生了指令重排 + +```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。 + + + + + +# volatile + +> 1、关于可见性,重排序等等的硬件原理,MESI缓存一致性,内存屏障,JMM等等这些,请看我的后面文章。第一阶段只是介绍下用法,不涉及原理。 +> +> 2、如果你在第一篇文章没有找到你想要的内容,请看我后面的内容。并发的体系,我自认为讲的还是比较全面的。 + +## volatile保证可见性代码 + +> 读者可以把两个代码运行一下,就能明显看到不加volatile的死循环(就是程序一直显示没结束) + +```java +/* 笔记 + * 1.当没有加Volatile的时候,while循环会一直在里面转圈 + * 2.当加了之后Volatile,由于可见性,一旦num改了之后,就会通知其他线程 + * 3.还有注意的时候不能用if,if不会重新拉回来再判断一次 + * */ +public class Video04_02 { + + public static void main(String[] args) { + MyData2 myData = new MyData2(); + + 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 MyData2{ + + volatile int num = 0; + + public void addTo60(){ + this.num = 60; + } +} +``` + +## volatile保证有序性代码 + +```java +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 { + + volatile int num = 0; + volatile 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; + } +} +``` + +读者可以将运行结果对比着来看,就能发现区别。 + +volatile只能保证可见性和有序性(禁止指令重排),无法保证原子性。 + + + +# CAS + +volatile自己虽然不能保证原子性,但是和CAS结合起来就可以保证原子性了。CAS+volatile一起用就可以同时解决**并发编程中的三个问题**了,保证并发安全。 + +## CAS 是什么? + +- CAS:比较并交换,它是一条 CPU 并发原语 + 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子性的。 + +- 例: AtomicInteger 的 compareAndSet('期望值','设置值') 方法 + 期望值与目标值一致时,修改目标变量为设置值 + 期望值与目标值不一致时,返回 false 和最新主存的变量值 + +- CAS 的底层原理 + 例: AtomicInteger.getAndIncrement() + 调用 Unsafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令 + 这是一种完全依赖于硬件的功能,通过它实现原子操作。 + 原语的执行必须是连续的,在执行过程中不允许被中断,CAS 是 CUP 的一条原子指令。 + +- CAS的思想就是乐观锁的思想 + +## AtomicInteger + +在JUC并发包中,CAS和AtomicInteger(原子类的value值都被volatile修饰了)一起保证了并发安全。下面我们以AtomicInteger.getAndIncrement() 方法讲一下。 + + + + + + + +```java +/** + * unsafe: rt.jar/sun/misc/Unsafe.class + * Unsafe 是 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地方法来访问 + * Unsafe 相当于一个后门,基于该类可以直接操作特定内存的数据 + * Unsafe 其内部方法都是 native 修饰的,可以像 C 的指针一样直接操作内存 + * Java 中的 CAS 操作执行依赖于 Unsafe 的方法,直接调用操作系统底层资源执行程序 + * + * this: 当前对象 + * 变量 value 由 volatile 修饰,保证了多线程之间的内存可见性、禁止重排序 + * + * valueOffset: 内存地址 + * 表示该变量值在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据 + * + * 1: 固定写死,原值加1 + */ +public final int getAndIncrement(){ + return unsafe.getAndAddInt(this,valueOffset,1); +} + +/** + * Unsafe.getAndAddInt() + * getIntVolatile: 通过内存地址去主存中取对应数据 + * + * while(!this.compareAndSwapInt(var1,var2,var5,var5 + var4)): + * 将本地 value 与主存中取出的数据对比,如果相同,对其作运算, + * 此时返回 true,取反后 while 结束,返回最终值。 + * 如果不相同,此时返回 false,取反后 while 循环继续运行,此时为自旋锁<重复尝试> + * 由于 value 是被 volatile 修饰的,所以拿到主存中最新值,再循环直至成功。 + */ +public final int getAndAddInt(Object var1,long var2,int var4){ + int var5; + do{ + var5 = this.getIntVolatile(var1,var2); // 从主存中拷贝变量到本地内存 + } while(!this.compareAndSwapInt(var1,var2,var5,var5 + var4)); + return var5; +} +``` + + + +## CAS 代码演示 + +```Java +public class CASDemo { + + public static void main(String[] args) { + AtomicInteger num = new AtomicInteger(5); + // TODO + System.out.println(num.compareAndSet(5, 1024) + "\t current num" + num.get()); + System.out.println(num.compareAndSet(5, 2019) + "\t current num" + num.get()); + } +``` + + + +## CAS三大问题 + +- 如果 CAS 长时间一直不成功,会给 CPU 带来很大的开销,在Java的实现中是一只通过while循环自旋CAS获取锁。 + +- 只能保证一个共享变量的原子操作 + +- 引出了 ABA 问题 + + + +## ABA问题 + + + +### 什么是ABA问题? + +```java +/** + * @Author: 吕 + * @Date: 2019/9/24 16:43 + *

+ * 功能描述: CAS引发的ABA问题 + */ +public class Video19_01 { + static AtomicReference num = new AtomicReference<>(100); + + public static void main(String[] args) { + + new Thread(() ->{ + num.compareAndSet(100, 101); + num.compareAndSet(101,100); + },"线程A").start(); + + new Thread(() ->{ + //保证A线程已经修改完 + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + boolean b = num.compareAndSet(100, 2019); + System.out.println(b + "\t 当前最新值" + num.get().toString()); + },"线程B").start(); + } +} +``` + +**CAS 会导致 ABA 问题:** + +例: A、B线程从主存取出变量 value + +-> A 在 N次计算中改变 value 的值 +-> A 最终计算结果还原 value 最初的值 +-> B 计算后,比较主存值与自身 value 值一致,修改成功 + +尽管各个线程的 CAS 都操作成功,但是并不代表这个过程就是没有问题的。 + + + +### ABA问题的解决 + +```java +/** + * @Author: 吕 + * @Date: 2019/9/24 16:49 + *

+ * 功能描述: ABA问题的解决 + */ +public class Video19_02 { + static AtomicStampedReference num = new AtomicStampedReference<>(100,1); + + public static void main(String[] args) { + int stamp = num.getStamp();//初始版本号 + + new Thread(() ->{ + num.compareAndSet(100,101,num.getStamp(),num.getStamp() + 1); + System.out.println(Thread.currentThread().getName() + "\t 版本号" + num.getStamp()); + num.compareAndSet(101,100,num.getStamp(),num.getStamp() + 1); + System.out.println(Thread.currentThread().getName() + "\t 版本号" + num.getStamp()); + },"线程A").start(); + + + new Thread(() ->{ + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + boolean b = num.compareAndSet(100, 209, stamp, num.getStamp() + 1); + System.out.println(b + "\t 当前版本号: \t" + num.getStamp()); + System.out.println("当前最新值 \t" + num.getReference().toString()); + },"线程B").start(); + } +} +``` + +思想很简单,可以很明显的看出来用版本号的方式解决了ABA的问题。 + +* 除了对象值,AtomicStampedReference内部还维护了一个“状态戳”。 +* 状态戳可类比为时间戳,是一个整数值,每一次修改对象值的同时,也要修改状态戳,从而区分相同对象值的不同状态。 +* 当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功。 + +## 只能保证一个共享变量的原子操作 + +- 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个方法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a合并一下ij=2a,然后用CAS来操作ij。从java1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。 +- 所以一般来说为了同时解决ABA问题和只能保证一个共享变量,原子类使用时大部分使用的是`AtomicStampedReference` + + + +# UnSafe + +Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。 + +Java和C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和释放内存。 **Java中的Unsafe类为我们提供了类似C++手动管理内存的能力,同时也有了指针的问题。** + +首先,Unsafe类是"final"的,不允许继承。且构造函数是private的: + +```java +public final class Unsafe { + private static final Unsafe theUnsafe; + public static final int INVALID_FIELD_OFFSET = -1; + + private static native void registerNatives(); + + private Unsafe() { + } + ... + +} +``` + +**因此我们无法在外部对Unsafe进行实例化。** + +## 获取Unsafe + +Unsafe无法实例化,那么怎么获取Unsafe呢?答案就是通过反射来获取Unsafe: + +```java +public Unsafe getUnsafe() throws IllegalAccessException { + Field unsafeField = Unsafe.class.getDeclaredFields()[0]; + unsafeField.setAccessible(true); + Unsafe unsafe = (Unsafe) unsafeField.get(null); + return unsafe; +} +``` + +Unsafe的功能如下图: + + + +## CAS相关 + +JUC中大量运用了CAS操作,可以说CAS操作是JUC的基础,因此CAS操作是非常重要的。Unsafe中提供了int,long和Object的CAS操作: + +```java +public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); + +public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); + +public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6); +``` + +## 偏移量相关 + +```java +public native long staticFieldOffset(Field var1); + +public native long objectFieldOffset(Field var1); + +``` + +* staticFieldOffset方法用于获取静态属性Field在对象中的偏移量,读写静态属性时必须获取其偏移量。 +* objectFieldOffset方法用于获取非静态属性Field在对象实例中的偏移量,读写对象的非静态属性时会用到这个偏移量 + +## 类加载 + +```java +public native Class defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6); + +public native Class defineAnonymousClass(Class var1, byte[] var2, Object[] var3); + +public native Object allocateInstance(Class var1) throws InstantiationException; + +public native boolean shouldBeInitialized(Class var1); + +public native void ensureClassInitialized(Class var1); +``` + +* defineClass方法定义一个类,用于动态地创建类。 +* defineAnonymousClass用于动态的创建一个匿名内部类。 +* `allocateInstance`方法用于创建一个类的实例,但是不会调用这个实例的构造方法,如果这个类还未被初始化,则初始化这个类。 +* shouldBeInitialized方法用于判断是否需要初始化一个类。 +* ensureClassInitialized方法用于保证已经初始化过一个类。 + +**举例** + +```java +public class UnsafeFooTest { + private static Unsafe geUnsafe() { + + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + return (Unsafe) f.get(null); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return null; + + } + + static class Simple { + private long l = 0; + + public Simple() { + this.l = 1; + System.out.println("我被初始化了"); + } + + public long getL() { + return l; + } + } + + public static void main(String[] args) throws Exception { + + Unsafe unsafe = geUnsafe(); + + Simple s = (Simple) unsafe.allocateInstance(Simple.class); + System.out.println(s.getL()); + } +} +``` + +**结果:** + +> 0 + +* 可以发现,利用Unsafe获取实例,不会调用构造方法 + +## 普通读写 + +通过Unsafe可以读写一个类的属性,即使这个属性是私有的,也可以对这个属性进行读写。 + +读写一个Object属性的相关方法 + +```java +public native int getInt(Object var1, long var2); + +public native void putInt(Object var1, long var2, int var4); +``` + +* getInt用于从对象的指定偏移地址处读取一个int。 +* putInt用于在对象指定偏移地址处写入一个int。其他的primitive type也有对应的方法。 + +**举例** + +```java +public class UnsafeFooTest { + private static Unsafe geUnsafe() { + + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + return (Unsafe) f.get(null); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return null; + + } + + static class Guard{ + private int ACCESS_ALLOWED = 1; + private boolean allow(){ + return 50 == ACCESS_ALLOWED; + } + + public void work(){ + if (allow()){ + System.out.println("我被允许工作...."); + } + } + } + + public static void main(String[] args) throws Exception { + Unsafe unsafe = geUnsafe(); + Guard guard = new Guard(); + + Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED"); + + unsafe.putInt(guard,unsafe.objectFieldOffset(f),50); + System.out.println("强行赋值..."); + guard.work(); + + } +} + +``` + +**结果** + +> 强行赋值... + +我被允许工作... + +## 类加载 + +```java +public native Class defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6); + +public native Class defineAnonymousClass(Class var1, byte[] var2, Object[] var3); + +public native Object allocateInstance(Class var1) throws InstantiationException; + +public native boolean shouldBeInitialized(Class var1); + +public native void ensureClassInitialized(Class var1); +``` + +* defineClass方法定义一个类,用于动态地创建类。 +* defineAnonymousClass用于动态的创建一个匿名内部类。 +* allocateInstance方法用于创建一个类的实例,但是不会调用这个实例的构造方法,如果这个类还未被初始化,则初始化这个类。 +* shouldBeInitialized方法用于判断是否需要初始化一个类。 +* ensureClassInitialized方法用于保证已经初始化过一个类。 + +## 内存屏障 + +```java +public native void loadFence(); + +public native void storeFence(); + +public native void fullFence(); +``` + +* loadFence:保证在这个屏障之前的所有读操作都已经完成。 +* storeFence:保证在这个屏障之前的所有写操作都已经完成。 +* fullFence:保证在这个屏障之前的所有读写操作都已经完成。 + +## 线程调度 + +```java +public native void unpark(Object var1); + +public native void park(boolean var1, long var2); + +public native void monitorEnter(Object var1); + +public native void monitorExit(Object var1); + +public native boolean tryMonitorEnter(Object var1); +``` + +* park方法和unpark方法相信看过LockSupport类的都不会陌生,这两个方法主要用来挂起和唤醒线程。 +* LockSupport中的park和unpark方法正是通过Unsafe来实现的: + +```java +public static void park(Object blocker) { + Thread t = Thread.currentThread(); + setBlocker(t, blocker); + UNSAFE.park(false, 0L); + setBlocker(t, null); +} + +public static void unpark(Thread thread) { + if (thread != null) + UNSAFE.unpark(thread); +} +``` + +**monitorEnter方法和monitorExit方法用于加锁,Java中的synchronized锁就是通过这两个指令来实现的。** + +# synchronized优化 + +> synchronized可以同时保证可见性,有序性,原子性。这个东西就不讲了 + +从JDk 1.6开始,JVM就对synchronized锁进行了很多的优化。synchronized说是锁,但是他的底层加锁的方式可能不同,偏向锁的方式来加锁,自旋锁的方式来加锁,轻量级锁的方式来加锁 + + + +## 锁消除 + +锁消除是JIT编译器对synchronized锁做的优化,在编译的时候,JIT会通过逃逸分析技术,来分析synchronized锁对象,是不是只可能被一个线程来加锁,没有其他的线程来竞争加锁,这个时候编译就不用加入monitorenter和monitorexit的指令。这就是,仅仅一个线程争用锁的时候,就可以消除这个锁了,提升这段代码的执行的效率,因为可能就只有一个线程会来加锁,不涉及到多个线程竞争锁 + + + +## 锁粗化 + + + +```java +synchronized(this) { + +} + + + +synchronized(this) { + +} + + + +synchronized(this) { + +} + +``` + +这个意思就是,JIT编译器如果发现有代码里连续多次加锁释放锁的代码,会给合并为一个锁,就是锁粗化,把一个锁给搞粗了,避免频繁多次加锁释放锁 + +## 偏向锁 + +这个意思就是说,monitorenter和monitorexit是要使用CAS操作加锁和释放锁的,开销较大,因此如果发现大概率只有一个线程会主要竞争一个锁,那么会给这个锁维护一个偏好(Bias),后面他加锁和释放锁,基于Bias来执行,不需要通过CAS,性能会提升很多。但是如果有偏好之外的线程来竞争锁,就要收回之前分配的偏好。可能只有一个线程会来竞争一个锁,但是也有可能会有其他的线程来竞争这个锁,但是其他线程唉竞争锁的概率很小。如果有其他的线程来竞争这个锁,此时就会收回之前那个线程分配的那个Bias偏好 + + + +## 轻量级锁 + +如果偏向锁没能成功实现,就是因为不同线程竞争锁太频繁了,此时就会尝试采用轻量级锁的方式来加锁,就是将对象头的Mark Word里有一个轻量级锁指针,尝试指向持有锁的线程,然后判断一下是不是自己加的锁,如果是自己加的锁,那就执行代码就好了。如果不是自己加的锁,那就是加锁失败,说明有其他人加了锁,这个时候就是升级为重量级锁 + + + +## 适应性锁 + +这是JIT编译器对锁做的另外一个优化,如果各个线程持有锁的时间很短,那么一个线程竞争锁不到,就会暂停,发生上下文切换,让其他线程来执行。但是其他线程很快释放锁了,然后暂停的线程再次被唤醒。也就是说在这种情况下,线程会频繁的上下文切换,导致开销过大。所以对这种线程持有锁时间很短的情况,是可以采取忙等策略的,也就是一个线程没竞争到锁,进入一个while循环不停等待,不会暂停不会发生线程上下文切换,等到机会获取锁就继续执行好了 + + + + + + + + + + +# 参考: + +- https://blog.csdn.net/qq_43040688/article/details/103979628 +- 《Java 并发编程艺术》 +- B站汪文君系列并发 + + + +# 强调 + +1、第一阶段只是简单的讲一下,在后面的系列里,会从硬件,C++源码层面讲解volatile,synchronized,内存屏障,MESI-缓存一致性等等进行讲解。 + +2、还有一些问题,在基础阶段可能不太好讲。比如中断这个东西,可能理解的云里雾里的,后面的系列讲到AQS的时候,结合Java源码再讲的话,你会非常好理解。 \ No newline at end of file diff --git a/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[1].md b/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[1].md new file mode 100644 index 0000000..b67bead --- /dev/null +++ b/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[1].md @@ -0,0 +1,1855 @@ +# AtomicXXXFieldUpdater + +> 算是一个小补充 + +## 简介 + +```java +public class AtomicIntegerFieldUpdaterTest { + + public static void main(String[] args) { + AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(Test.class, "value"); + Test ts = new Test(); + + IntStream.rangeClosed(0, 2).forEach(item -> { + new Thread(() -> { + int value = updater.getAndIncrement(ts); + System.out.println("oldV: " + value); + }).start(); + }); + } + } + +class Test { + volatile int value; +} + +``` + +1、以`AtomicIntegerFieldUpdater`为例,看上面代码。Test类的value属性被volatile修饰了,但是volatile只能保证可见性和有序性。在以往的文章里我们讲过是可以通过volatile+CAS同时解决可见性,有序性,原子性。 + +2、JUC提供了一种新的功能来保证原子性,AtomicXXXFieldUpdater修饰的类对应的字段,在进行更新时同样可以保证原子性。 + +## 使用场景 + +- 想让类的属性操作具备原子性, + +- 但是不想使用锁。 + +- 大量需要原子类型修饰的对象,相比较比较耗费内存 + +举个例子: + +如果你想要保证原子性,一般是使用`AtomicStampedReference`来包装Node对象。 + +```Java +import org.apache.lucene.util.RamUsageEstimator; + +import java.util.concurrent.atomic.AtomicStampedReference; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/11 16:51 + *

+ * 功能描述: 计算对象内存大小 + * https://blog.csdn.net/yunqiinsight/article/details/80431831 + * https://www.cnblogs.com/libin6505/p/10648091.html + */ +public class Calculate_Java_Object_Size { + + public static void main(String[] args) { + Node node = new Node(); + //计算指定对象本身在堆空间的大小,单位字节 + long b = RamUsageEstimator.shallowSizeOf(node); + System.out.println(b); + + AtomicStampedReference nodeAtomicStampedReference = new AtomicStampedReference(node,0); + long c = RamUsageEstimator.shallowSizeOf(nodeAtomicStampedReference); + System.out.println(c + b); + } + + /** + * 功能描述: Api说明 + */ + public static void Api(Object o){ + //下面三个方法参数都是 Object类型 + + //计算指定对象及其引用树上的所有对象的综合大小,单位字节 + long a = RamUsageEstimator.sizeOf(o); + System.out.println(a); + + //计算指定对象本身在堆空间的大小,单位字节 + long b = RamUsageEstimator.shallowSizeOf(o); + System.out.println(b); + + //计算指定对象及其引用树上的所有对象的综合大小,返回可读的结果,如:2KB + String c = RamUsageEstimator.humanSizeOf(o); + System.out.println(c); + } +} + +class Node { + Node pre; + Node next; + Integer value; +} + +``` + +> 输出结果: +> +> 24 +> +> 40 + +可以看出如果用了`AtomicStampedReference`,会多出16个字节。如果对象有10000个,那么会多出很多字节。生产过程中的内存都是很贵的。为了减少内存消耗,同时可以保证原子性,就可以使用`AtomicXXXFieldUpdater`。 + + + +# CountDownLatch + +## 简介 + +* countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。 +* 是通过一个 state(相当于计数器)的东西来实现的,计数器的初始值是 **线程的数量或者任务的数量**。 +* 每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。 +* CountDownLatch的方便之处在于,你可以在一个线程中使用, **也可以在多个线程上使用,一切只依据状态值**,这样便不会受限于任何的场景。 + +## 使用场景一 + +**需求** + +* 可能刚从数据库读取了一批数据 +* 利用并发处理这批数据 +* 当所有的数据处理完成后,再去执行后面的操作 + +**解决方案** + +* **第一种**:可以利用 join 的方法,但是在线程池中,比较麻烦。 +* **第二种**:利用线程池的awaitTermination,阻塞一段时间。 + - 当使用awaitTermination时,主线程会处于一种等待的状态,等待线程池中所有的线程都运行完毕后才继续运行。 +* **第三种**:利用CountDownLatch,每当任务完成一个,就计数器减一。 + +```java +package _05_AQS; + +import java.util.concurrent.CountDownLatch; + +/** + * @Author: youthlql-吕 + * @Date: 2019/9/26 10:05 + *

+ * 功能描述: + */ +public class Video32 { + + public static void main(String[] args) throws InterruptedException { + CountDownLatch countDownLatch = new CountDownLatch(6); + + for (int i = 0; i < 6; i++) { + new Thread(() ->{ + System.out.println("\t\t" + Thread.currentThread().getName() + "处理完毕~~~"); + countDownLatch.countDown(); + System.out.println("非调用者线程-" + Thread.currentThread().getName() + "-还可以干点其他事"); + }, Country.forEach_Country(i + 1).getCountryName()).start(); + } + + countDownLatch.await(); + System.out.println("-----------------------------"); + System.out.println("\t 所有任务都已经处理完毕,可以往后执行了!"); + + } +} + +enum Country{ + + ONE(1,"1号任务"), + TWO(2,"2号任务"), + THREE(3,"3号任务"), + FOUR(4,"4号任务"), + FIVE(5,"5号任务"), + SIX(6,"6号任务"); + + private Integer index; + private String countryName; + + + public static Country forEach_Country(Integer index){ + Country[] values = Country.values(); + for (Country c: values) { + if(c.getIndex() == index){ + return c; + } + } + return null; + } + + Country(Integer index, String countryName) { + this.index = index; + this.countryName = countryName; + } + + public Integer getIndex() { + return index; + } + + public String getCountryName() { + return countryName; + } + +} + +``` + +**结果** + +```java + 1号任务处理完毕~~~ + 5号任务处理完毕~~~ +非调用者线程-5号任务-还可以干点其他事 + 6号任务处理完毕~~~ + 3号任务处理完毕~~~ +非调用者线程-3号任务-还可以干点其他事 + 4号任务处理完毕~~~ + 2号任务处理完毕~~~ +非调用者线程-4号任务-还可以干点其他事 +非调用者线程-6号任务-还可以干点其他事 +非调用者线程-1号任务-还可以干点其他事 +----------------------------- + 所有任务都已经处理完毕,可以往后执行了! +非调用者线程-2号任务-还可以干点其他事 +``` + +因为countdown只会阻塞调用者,其它线程干完任务就可以干其他事。这里的调用者线程就是main线程。 + +## 使用场景二 + +**需求** + +* 多个线程协同工作 +* 多个线程需要等待其他线程的工作之后,再进行其后续工作。 +* 被唤醒后继续执行其他操作 + +```java +public class CountDownLatchExample { + + public static void main(String[] args) throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + new Thread(() -> { + System.out.println(Thread.currentThread().getName() + " Do some initial working."); + try { + Thread.sleep(1000); + latch.await(); + System.out.println(Thread.currentThread().getName() + " Do other working."); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }).start(); + + new Thread(() -> { + System.out.println(Thread.currentThread().getName() + " Do some initial working."); + try { + Thread.sleep(1000); + latch.await(); + System.out.println(Thread.currentThread().getName() + " Do other working."); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }).start(); + + new Thread(() -> { + System.out.println("asyn prepare for some data."); + try { + Thread.sleep(2000); + System.out.println("Data prepare for done."); + } catch (InterruptedException e) { + e.printStackTrace(); + }finally { + latch.countDown(); + } + }).start(); + + } + +} +``` + +**结果** + +```java +Thread-0 Do some initial working. +Thread-1 Do some initial working. +asyn prepare for some data. +Data prepare for done. +Thread-0 Do other working. +Thread-1 Do other working. +``` + +总体来说意思都差不多 + + + +## 常用API + +**构造方法只有一个** + +* `CountDownLatch(int count)` :构造一个以给定计数 + +**实例方法** + +* `public void await()` + - **当前线程等到锁存器计数到零** + - 可以被 **打断** +* `public boolean await(long timeout,TimeUnit unit)` + - 等待一段时间 + - **timeout** - 等待的最长时间 , **unit** - timeout参数的时间单位 + - 如果 **指定的等待时间过去**,则返回值false + - 如果 **计数达到零**,则方法返回值为true +* `public void countDown()` + - 减少锁存器的计数, **如果计数达到零,释放所有等待的线程**。 +* `public long getCount()` + - 返回当前计数 + +## 给离散的平行任务增加逻辑层次关系 + +**需求** + +* 并发的从很多的数据库读取大量数据 +* 在读取数据的过程中,某个表可能会出现: 数据丢失、数据精度丢失、数据大小不匹配。 +* 需要进行对数据的各个情况进行检测,这个检测是并发的完成的 +* 所以需要控制如果一个表所有的情况检测完成,再进行后续的操作 + +**解决** + +* 利用 `CountDownLatch`的计数器 +* 每当一个检测完成,计数器减一 +* 如果计数为0,执行后面操作 + +```java +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/11 21:05 + *

+ * 功能描述: + */ +public class test { + private static final Random RANDOM = new Random(); + + public static void main(String[] args) throws Exception { + //2个事件请求,这里只演示校验数据行,和数据schema + Event[] events = {new Event(1), new Event(2)}; + + ExecutorService service = Executors.newFixedThreadPool(5); + + for (Event event : events) { + //2个事件请求中,可能涉及多个表,可以再分成多个表 + List tables = capture(event); + for (Table table : tables) { + TaskBatch taskBatch = new TaskBatch(2); + TrustSourceColumns sourceColumns = new TrustSourceColumns(table, taskBatch); + TrustSourceRecordCount recordCount = new TrustSourceRecordCount(table, taskBatch); + service.submit(sourceColumns); + service.submit(recordCount); + } + } + } + + static class Event { + private int id; + + Event(int id) { + this.id = id; + } + } + + interface Watcher { + void done(Table table); + } + + static class TaskBatch implements Watcher { + + private final CountDownLatch latch; + + TaskBatch(int size) { + this.latch = new CountDownLatch(size); + } + + @Override + public void done(Table table) { + latch.countDown(); + if (latch.getCount() == 0) { + System.out.println("The table " + table.tableName + " finished work , " + table.toString()); + } + } + } + + static class Table { + String tableName; + long sourceRecordCount; + long targetCount; + String columnSchema = "columnXXXType = varchar"; + + String targetColumnSchema = ""; + + public Table(String tableName, long sourceRecordCount) { + this.tableName = tableName; + this.sourceRecordCount = sourceRecordCount; + + } + + @Override + public String toString() { + return "Table{" + + "tableName='" + tableName + '\'' + + ", sourceRecordCount=" + sourceRecordCount + + ", targetCount=" + targetCount + + ", columnSchema='" + columnSchema + '\'' + + ", targetColumnSchema='" + targetColumnSchema + '\'' + + '}'; + } + } + + private static List
capture(Event event) { + List
list = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + list.add(new Table("table-" + event.id + "-" + i, i * 1000)); + } + return list; + } + + //校验数据行数是否一致 + static class TrustSourceRecordCount implements Runnable { + + private final Table table; + + private final TaskBatch taskBatch; + + TrustSourceRecordCount(Table table, TaskBatch taskBatch) { + this.table = table; + this.taskBatch = taskBatch; + } + + @Override + public void run() { + try { + Thread.sleep(RANDOM.nextInt(100)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + table.targetCount = table.sourceRecordCount; + + taskBatch.done(table); + + } + + } + //校验数据列属性以及对应的表 + static class TrustSourceColumns implements Runnable { + + private final Table table; + + private final TaskBatch taskBatch; + + TrustSourceColumns(Table table, TaskBatch taskBatch) { + this.table = table; + this.taskBatch = taskBatch; + } + + @Override + public void run() { + try { + Thread.sleep(RANDOM.nextInt(100)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + table.targetColumnSchema = table.columnSchema; + + taskBatch.done(table); + } + + } +} + +``` + +结果 + +``` +The table table-1-1 finished work , Table{tableName='table-1-1', sourceRecordCount=1000, targetCount=1000, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'} +The table table-1-0 finished work , Table{tableName='table-1-0', sourceRecordCount=0, targetCount=0, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'} +The table table-2-0 finished work , Table{tableName='table-2-0', sourceRecordCount=0, targetCount=0, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'} +The table table-1-2 finished work , Table{tableName='table-1-2', sourceRecordCount=2000, targetCount=2000, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'} +The table table-1-3 finished work , Table{tableName='table-1-3', sourceRecordCount=3000, targetCount=3000, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'} +The table table-2-1 finished work , Table{tableName='table-2-1', sourceRecordCount=1000, targetCount=1000, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'} +The table table-2-2 finished work , Table{tableName='table-2-2', sourceRecordCount=2000, targetCount=2000, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'} +The table table-2-3 finished work , Table{tableName='table-2-3', sourceRecordCount=3000, targetCount=3000, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'} + +``` + + + +## 利用CountDownLatch实现回调函数 + +**实现:** + +* 在每个线程使计数器减一的时候,利用getCount判断,当前是否所有线程任务执行完成 + +```java +public class CyclicBarrierTest { + static class MyCountDownLatch extends CountDownLatch{ + private final Runnable runnable; + + public MyCountDownLatch(int count,Runnable runnable) { + super(count); + this.runnable = runnable; + } + + @Override + public void countDown() { + super.countDown(); + if (getCount()==0){ + this.runnable.run(); + } + } + + } + + public static void main(String[] args) { + + final MyCountDownLatch latch = new MyCountDownLatch(2, ()->{ + System.out.println("All of work finish done. This is call back."); + }); + + new Thread(){ + @Override + public void run() { + try { + Thread.sleep(1000); + latch.countDown(); + System.out.println(getName() + " finished."); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }.start(); + + new Thread(){ + @Override + public void run() { + try { + Thread.sleep(1000); + latch.countDown(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(getName() + " finished."); + } + }.start(); + + } +} +``` + + + + + +# CyclicBarrier + +## 引出 + +- 栅栏类似于闭锁(CountDownLatch),它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的关键区别在于, **所有的线程必须同时到达栅栏位置,才能继续执行**。闭锁用于等待事件,而 **栅栏用于等待其他线程**。 + +- CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。 **当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。** 如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。 + +```java +package _05_AQS; + +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CyclicBarrier; + +/** + * @Author: youthlql-吕 + * @Date: 2019/9/26 10:34 + *

+ * 功能描述: + */ +public class Video33 { + + public static void main(String[] args) { + CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> System.out.println("收集到7颗龙珠,召唤神龙")); + + for (int i = 0; i < 7; i++) { + final int temp = i + 1; + new Thread(() ->{ + System.out.println(Thread.currentThread().getName() + "\t收集到第" + temp + "颗龙珠"); + try { + int await = cyclicBarrier.await(); + System.out.println("还剩几个:" + await); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (BrokenBarrierException e) { + e.printStackTrace(); + } + },"线程" + String.valueOf(i)).start(); + } + } +} + +``` + +**结果:** + +``` +线程0 收集到第1颗龙珠 +线程2 收集到第3颗龙珠 +线程3 收集到第4颗龙珠 +线程1 收集到第2颗龙珠 +线程4 收集到第5颗龙珠 +线程5 收集到第6颗龙珠 +线程6 收集到第7颗龙珠 +收集到7颗龙珠,召唤神龙 +还剩几个:0 +还剩几个:6 +还剩几个:3 +还剩几个:4 +还剩几个:5 +还剩几个:1 +还剩几个:2 +``` + +可以明显的看到cyclicBarrier会阻塞所有线程,和countdownlatch不一样。 + +## API使用 + +### 构造方法 + +```java +public CyclicBarrier(int parties) +public CyclicBarrier(int parties, Runnable barrierAction) +``` + +* `parties` 是参与线程的个数 +* 第二个构造方法有一个 `Runnable` 参数,这个参数的意思是 **最后一个到达线程要做的任务* + +### 重要方法 + +```java +public int await() throws InterruptedException, BrokenBarrierException +public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException +``` + +* 线程调用 await() **表示自己已经到达栅栏** +* BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时 + +### 其他方法 + +```java +public void reset() +``` + +* 将屏障重置为初始状态。 如果任何一方正在等待屏障,他们将返回 **BrokenBarrierException** 。 +* 这样就可以重复利用这个屏障 + +## CyclicBarrier 与 CountDownLatch 区别 + +* `CountDownLatch` 是一次性的。 `CyclicBarrier` 是可循环利用的 +* `CountDownLatch` 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。 `CyclicBarrier` 参与的线程职责是一样的 + +> 牛客-京东2019春招Java开发笔试卷-T27 + +> https://blog.csdn.net/liangyihuai/article/details/83106584 + +从jdk作者设计的目的来看,javadoc是这么描述它们的: + +- CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes. +- CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. +- 从javadoc的描述可以得出: CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行; CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。 +- 对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。 而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须互相等待,然后继续一起执行。 CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。 按照这个题目的描述等所有线程都到达了这一个阀门处,再一起执行,此题强调的是,一起继续执行,我认为 选B 比较合理! + + + +- 像上文中CountDownLatch的例子,main线程在等待,其余6个线程任务做完之后,main线程才苏醒干后面的事。因为countdown只会阻塞调用者,其它线程干完任务就可以干其他事。这里的调用者线程就是main线程。 + +- 像上文中CyclicBarrier的例子,是7个线程互相等待对方,7个任务都完成后,执行注册的回调任务。 + +# Exchanger + +## 简介 + +* 用于两个工作线程之间交换数据的封装工具类 +* 简单说就是一个线程在完成一定的事务后想与另一个线程交换数据,则 **第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据* + +`Exchanger` 泛型类型,其中 **V 表示可交换的数据类型** + +## 简单的应用 + +```java +import java.util.concurrent.*; + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/11 21:05 + *

+ * 功能描述: + */ +public class ExchangerTest { + + public static void main(String[] args) { + final Exchanger exchanger = new Exchanger<>(); + + + new Thread(()->{ + System.out.println(Thread.currentThread().getName() + " start . "); + try { + + /** + * 如果这里睡200ms的话,应该是B线程先拿出数据,然后B线程等待A线程。因为是B先给的数据, + * 所以最后A线程会先拿到B给的数据,也就是先打印 + */ + TimeUnit.MILLISECONDS.sleep(200); + String exchange = exchanger.exchange("I am come from T-A"); + System.out.println(Thread.currentThread().getName() + " get value : " + exchange); + System.out.println(Thread.currentThread().getName() + " end . "); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + },"A").start(); + + + new Thread(()->{ + System.out.println(Thread.currentThread().getName() + " start . "); + try { + String exchange = exchanger.exchange("I am come from T-B"); + System.out.println(Thread.currentThread().getName() + " get value : " + exchange); + System.out.println(Thread.currentThread().getName() + " end . "); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + },"B").start(); + } +} +``` + +**结果:** + +```java +A start . +B start . +A get value : I am come from T-B +B get value : I am come from T-A +B end . +A end . +``` + + + +## 方法介绍 + +```java +public V exchange(V x) throws InterruptedException +``` + +* 等待另一个线程到达此交换点,然后将给定对象传输给它,接收其对象作为回报。 +* 可以被打断 +* 如果已经有个线程正在等待了,则直接交换数据 + +## 数据的分析 + +```java +import java.util.concurrent.*; + + +/** + * @Author: youthlql-吕 + * @Date: 2020/10/11 21:05 + *

+ * 功能描述: + */ +public class test { + public static void main(String[] args) { + + + final Exchanger exchanger = new Exchanger<>(); + + new Thread(() -> { + Object Aobj = new Object(); + System.out.println("A将会发送:" + Aobj); + try { + Object Robj = exchanger.exchange(Aobj); + System.out.println("A接收的:" + Robj); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + }, "A").start(); + + new Thread(() -> { + Object Bobj = new Object(); + System.out.println("B将会发送:" + Bobj); + try { + Object Robj = exchanger.exchange(Bobj); + System.out.println("B接收的:" + Robj); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + + }, "B").start(); + } +} + +``` + +**结果** + +``` +A将会发送:java.lang.Object@7be2d776 +B将会发送:java.lang.Object@5eca8d3a +B接收的:java.lang.Object@7be2d776 +A接收的:java.lang.Object@5eca8d3a +``` + + + +**从这个例子可以看出一个很严重的问题**:发送的对象和接收的对象是同一个对象,可能会用严重的线程安全问题。 + +# ReentrantLock + +## 简介 + +当多个线程需要访问某个公共资源的时候,我们知道需要通过加锁来保证资源的访问不会出问题。java提供了两种方式来加锁: + +* 一种是关键字: `synchronized`,一种是concurrent包下的基于API实现的。 +* synchronized是 JVM底层支持的,而concurrent包则是 `jdk`实现。 + + + +## 公平锁和非公平锁 + +* 当有线程竞争锁时,当前线程会首先尝试获得锁而不是在队列中进行排队等候,这对于那些已经在队列中排队的线程来说显得不公平,这也是非公平锁的由来 + +* 默认情况下为非公平锁。 + + + +* 锁的存储结构就两个东西:"双向链表" + "int类型状态"。ReenTrantLock的实现是一种自旋锁, 通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。 + + + + + +## 构造方法 + +```java +public ReentrantLock() +public ReentrantLock(boolean fair) +``` + +* `fair`:该参数为true时,会尽力维持公平 + +## 获得锁 + +```java +public void lock() +public void lockInterruptibly() throws InterruptedException +public boolean tryLock() +public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException +``` + +**lock** + +* 正常的获取锁,如果没有获得到锁,就会被阻塞 + +**lockInterruptibly** + +* 获取锁,如果没有获得到锁,就会被阻塞 +* 可以被打断 + +**tryLock** + +* 如果获得到锁,返回true +* 如果没有获得到锁,返回false +* `timeout`:表示等待的时间 +* `tryLock()`在获取的锁的时候,不会考虑此时是否有其他线程在等待,会破坏公平。 +* 如果你希望遵守公平设置此锁,然后用 `tryLock(0, TimeUnit.SECONDS)` 这几乎是等效的(它也检测中断)。 + +## 释放锁 + +```java +public void unlock() +``` + +* 尝试释放此锁。 +* 必须是锁的持有者才能释放锁 + +## 锁的调试 + +```java +protected Thread getOwner() +public final boolean hasQueuedThreads() +public final int getQueueLength() +protected Collection getQueuedThreads() +``` + +**getOwner** + +* 返回持有锁的线程 + +**hasQueuedThreads** + +* 是否有线程在等待获取锁 + +**getQueueLength** + +* 获取等待锁的线程数目 + +**getQueuedThreads** + +* 返回正在等待的线程集合 + +## Lock和synchronized的区别 + +**底层实现**: + +* Lock基于 `AQS`实现,通过state和一个CLH队列来维护锁的获取与释放 +* synchronized需要通过 `monitor`,经历一个从用户态到内核态的转变过程,更加耗时 + +**其他区别** + +| synchronized | Lock | +| --------------------------- | ------------------------------------------------------------ | +| 是java内置关键字,在jvm层面 | 是个java类 | +| 无法判断是否获取锁的状态 | 可以判断是否获取到锁 | +| 会自动释放锁 | 需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁 | +| 线程会一直等待下去 | 如果尝试获取不到锁,线程可以不用一直等待就结束 | + +**总结来说** +synchronized的锁可重入、不可中断、非公平。而Lock锁可重入、可判断、可公平(两者皆可)。 + + + +# Semaphore + +## 简介 + +- Semaphore是一种在多线程环境下使用的设施,该设施负责协调各个线程,以**保证它们能够正确、合理的使用公共资源的设施**,也是操作系统中用于控制进程同步互斥的量。 + +- Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。**它相当于给线程规定一个量从而控制允许活动的线程数**。 + +- Semaphore用于限制可以访问某些资源(物理或逻辑的)的线程数目,他维护了一个许可证集合,有多少资源需要限制就维护多少许可证集合,假如这里有N个资源,那就对应于N个许可证,同一时刻也只能有N个线程访问。 + +- 一个线程获取许可证就调用acquire方法,用完了释放资源就调用release方法。 + + + +除了JDK定义的哪些锁,Semaphore也可以定义锁。Semaphore可以做的功能相当的多,比如秒杀限流 + +## 用Semaphore自定义Lock + +```java +public class SemaphoreTest { + + public static void main(String[] args) { + final SemaphoreLock lock = new SemaphoreLock(); + for (int i = 0; i < 2; i++) { + new Thread(()->{ + try { + lock.lock(); + System.out.println(Thread.currentThread().getName() + " get the lock."); + Thread.sleep(3000); + }catch (Exception e){ + e.printStackTrace(); + }finally { + System.out.println(Thread.currentThread().getName() + " release the lock."); + lock.unlock(); + } + }).start(); + } + + } + static class SemaphoreLock{ + private final Semaphore semaphore = new Semaphore(1); + + public void lock() throws InterruptedException { + semaphore.acquire(); + } + + public void unlock() { + semaphore.release(); + } + } +} +``` + + +​ + +**结果** + +> Thread-0 get the lock. +> Thread-0 release the lock. +> Thread-1 get the lock. +> Thread-1 release the lock. + +**跟synchronized的区别** + +* 可以看出最大的区别就是可以控制多个线程访问多份资源,而不是只用一个线程访问一份资源 + + + +## 用Semaphore做限流 + +Semaphore可以控制多个线程访问多份资源,而不是只用一个线程访问一份资源,所以在限流方面也有应用。 + +```java +package _05_AQS; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +/** + * @Author: youthlql-吕 + * @Date: 2019/9/26 10:58 + *

+ * 功能描述: + */ +public class Video34 { + + public static void main(String[] args) { + Semaphore semaphore = new Semaphore(3); + + for (int i = 0; i < 10; i++) { + new Thread(() ->{ + try { + semaphore.acquire(); + System.out.println(Thread.currentThread().getName() + "\t 进入抢购秒杀页面,准备抢小米9"); + //停3秒后离开 + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread().getName() + "\t 离开抢购秒杀页面,成功抢到小米9"); + } catch (InterruptedException e) { + e.printStackTrace(); + }finally { + semaphore.release(); + } + },"用户" + String.valueOf(i)).start(); + } + } +} + +``` + +**结果:** + +``` +用户0 进入抢购秒杀页面,准备抢小米9 +用户2 进入抢购秒杀页面,准备抢小米9 +用户1 进入抢购秒杀页面,准备抢小米9 +用户1 离开抢购秒杀页面,成功抢到小米9 +用户2 离开抢购秒杀页面,成功抢到小米9 +用户0 离开抢购秒杀页面,成功抢到小米9 +用户4 进入抢购秒杀页面,准备抢小米9 +用户3 进入抢购秒杀页面,准备抢小米9 +用户5 进入抢购秒杀页面,准备抢小米9 +用户3 离开抢购秒杀页面,成功抢到小米9 +用户4 离开抢购秒杀页面,成功抢到小米9 +用户6 进入抢购秒杀页面,准备抢小米9 +用户8 进入抢购秒杀页面,准备抢小米9 +用户5 离开抢购秒杀页面,成功抢到小米9 +用户7 进入抢购秒杀页面,准备抢小米9 +``` + +代码里限制了资源为3个,所以同一时间段只能有3个线程进来抢购小米9手机。 + + + +## 常用API + +### 构造方法 + + public Semaphore(int permits) + public Semaphore(int permits , boolean fair) + + +* `permits`:同一时间可以访问资源的线程数目 +* `fair`:尽可能的保证公平 + +### 重要方法 + + public void acquire() throws InterruptedException + public void release() + + +* `acquire()`:**获取一个许可证**,如果许可证用完了,则陷入阻塞。可以被打断。 + +* `release()`:**释放一个许可证** + + acquire(int permits) + public void release(int permits) + +**acquire多个时,如果没有足够的许可证可用,那么当前线程将被禁用以进行线程调度**,并且处于休眠状态,直到发生两件事情之一: + +* 一些其他线程调用此信号量的一个release方法,当前线程旁边将分配许可证,并且可用许可证的数量满足此请求; +* 要么一些其他线程interrupts当前线程。 + +**release多个时,会使许可证增多,最终可能超过初始值** + + public boolean tryAcquire(int permits) + public boolean tryAcquire(int permits, + long timeout, + TimeUnit unit) + throws InterruptedException + + +* 尝试去拿,**拿到返回true** + +### 其他方法 + + public int availablePermits() + + +* 返回此信号量中当前可用的许可数。 + + protected Collection getQueuedThreads() + public final int getQueueLength() + +* `getQueuedThreads`返回正在阻塞的线程集合 + +* `getQueueLength`返回阻塞获取的线程数 + + public void acquireUninterruptibly() + public void acquireUninterruptibly(int permits) + +* 可以`不被打断`获取许可证 + + public int drainPermits() + +* 获取当前全部的许可证目标 + + + +# Condition + +## 简介 + +- Condition主要是用于线程通信的,也就是和Object类的wait,notify有同样的功能。不过Condition的功能更加多样,Conditon可以绑定锁,实现选择性唤醒。 +- Condition和Lock搭配使用; condition必须使用`lock.newCondition();`来创建condition。必须存放在Lock中。否则抛出异常。 + +```Java +package _07_LockAndCondition; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @Author: youthlql-吕 + * @Date: 2019/9/26 15:41 + *

+ * 功能描述: A线程打印3次,B线程打印6次,C线程9次,持续两轮 + */ +public class ConditionDemo { + + public static void main(String[] args) { + ShareResource shareResource = new ShareResource(); + new Thread(() ->{ + for (int i = 0; i < 2; i++) { + shareResource.print3(); + } + },"A").start(); + + new Thread(() ->{ + for (int i = 0; i < 2; i++) { + shareResource.print6(); + } + },"B").start(); + + new Thread(() ->{ + for (int i = 0; i < 2; i++) { + shareResource.print9(); + } + },"C").start(); + + + } +} + +//以前是一个lock只有一个钥匙,现在是一个lock多个钥匙 +class ShareResource{ + //标志位,可取值为1,2,3 + private volatile Integer flag = 1; + private Lock lock = new ReentrantLock(); + private Condition c1 = lock.newCondition(); + private Condition c2 = lock.newCondition(); + private Condition c3 = lock.newCondition(); + + public void print3(){ + lock.lock(); + try { + while(flag != 1){ + c1.await(); + } + for (int i = 0; i < 3 ; i++) { + System.out.println(Thread.currentThread().getName() + "\t" + (i+1)); + } + flag = 2; + c2.signal();//唤醒2号线程 + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public void print6(){ + lock.lock(); + try { + while(flag != 2){ + c2.await(); + } + for (int i = 0; i < 6 ; i++) { + System.out.println(Thread.currentThread().getName() + "\t" + (i+1)); + } + flag = 3; + c3.signal();//唤醒3号线程 + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + + public void print9(){ + lock.lock(); + try { + while(flag != 3){ + c3.await(); + } + for (int i = 0; i < 9 ; i++) { + System.out.println(Thread.currentThread().getName() + "\t" + (i+1)); + } + flag = 1; + c1.signal();//唤醒1号线程 + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + +} +``` + +**结果:** + +``` +A 1 +A 2 +A 3 +B 1 +B 2 +B 3 +B 4 +B 5 +B 6 +C 1 +C 2 +C 3 +C 4 +C 5 +C 6 +C 7 +C 8 +C 9 +A 1 +A 2 +A 3 +B 1 +B 2 +B 3 +B 4 +B 5 +B 6 +C 1 +C 2 +C 3 +C 4 +C 5 +C 6 +C 7 +C 8 +C 9 +``` + + + +## Condition版生产者消费者 + +``` +/** + * @Author: youthlql-吕 + * @Date: 2019/9/26 15:03 + *

+ * 功能描述: 使用Lock和Condition实现 + */ +public class Producer_04 { + public static void main(String[] args) { + Consumer4 consumer = new Consumer4(); + + //生产者线程A + new Thread(() ->{ + for (int i = 0; i < 10; i++) { + try { + consumer.increment(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + },"生产者A").start(); + + new Thread(() ->{ + for (int i = 0; i < 10; i++) { + try { + consumer.decrement(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + },"消费者B").start(); + + new Thread(() ->{ + for (int i = 0; i < 10; i++) { + try { + consumer.increment(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + },"生产者C").start(); + + new Thread(() ->{ + for (int i = 0; i < 10; i++) { + try { + consumer.decrement(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + },"消费者D").start(); + } +} +class Consumer4{ + private volatile Integer num = 0; + private Lock lock = new ReentrantLock(); + private Condition condition = lock.newCondition(); + + public void increment() throws InterruptedException { + lock.lock(); + try { + while(num != 0){ + condition.await(); + } + num++; + System.out.println(Thread.currentThread().getName() + "\t" + num); + condition.signalAll(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + + } + + public void decrement() throws InterruptedException { + lock.lock(); + try { + while(num == 0){ + condition.await(); + } + num--; + System.out.println(Thread.currentThread().getName() + "\t" + num); + condition.signalAll(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + +} +``` + + + + + +# ReentrantReadWriteLock读写锁 + +读写锁的目的是为了让读 — 读之间不加锁 + +| 冲突 | 策略 | +| ------- | ------ | +| 读 — 读 | 并行化 | +| 读 — 写 | 串行化 | +| 写 — 读 | 串行化 | +| 写 — 写 | 串行化 | + +```java +package _05_AQS; + +import java.util.*; + +/** + * @Author: youthlql-吕 + * @Date: 2019/9/26 10:58 + *

+ * 功能描述: + */ +public class Video34 { + + private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + + private static final Lock readLock = lock.readLock(); + private static final Lock writeLock = lock.writeLock(); + + private static final List data = new ArrayList<>(); + + public static void main(String[] args) throws InterruptedException { + new Thread(()->write(),"write-1").start(); + + Thread.sleep(1000); + new Thread(()->read(),"read-1").start(); + + new Thread(()->read(),"read-2").start(); + + } + + public static void write(){ + try{ + writeLock.lock(); + data.add("数据XXX"); + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + System.out.println("写锁释放"); + writeLock.unlock(); + } + } + + public static void read(){ + try{ + readLock.lock(); + for (String datum : data) { + System.out.println(Thread.currentThread().getName() + " 读入:"+datum); + } + Thread.sleep(3000); + } catch (Exception e) { + e.printStackTrace(); + } finally { + System.out.println("读锁释放"); + readLock.unlock(); + } + } +} + +``` + +**结果:** + +``` +写锁释放 +read-1 读入:数据XXX +read-2 读入:数据XXX +读锁释放 +读锁释放 +``` + +可以看到读写-串行化,读读可以并发。 + +# StampedLock读写锁 + +## ReadWriteLock出现的问题 + +1、深入分析ReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写线程去抢锁,这是一种悲观的读锁,会出现写饥饿。 + +2、有100个线程访问某个资源,如果有99线程个需要读锁,1个线程需要写锁,此时,写的线程很难得到执行。 + + + +## StampedLock改进 + +3、StampedLock和ReadWriteLock相比,改进之处在于:`读的过程中也允许获取写锁后写入` 。这样一来,我们读的数据就可能不一致,所以,**需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁**。 + +4、乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。 + + + +## 用StampedLock去悲观的读 + +StampedLock可以完全实现ReadWriteLock的功能。 + +```java +public class StampedLockDemo1 { + + + private static final StampedLock stampedLock = new StampedLock(); + + private static Integer DATA = 0; + + public static void write() { + long stamp = -1; + try { + stamp = stampedLock.writeLock();// 获取写锁 + DATA++; + System.out.println("写-->" + DATA); + } finally { + stampedLock.unlockWrite(stamp); // 释放写锁 + } + } + + public static void read() { + long stamp = -1; + + try { + stamp = stampedLock.readLock();// 获取悲观读锁 + System.out.println("读-->" + DATA); + } finally { + stampedLock.unlockRead(stamp); // 释放悲观读锁 + } + } + + public static void main(String[] args) { + ExecutorService executor = Executors.newFixedThreadPool(10); + //写任务 + Runnable writeTask = () -> { + for (; ; ) { + write(); + } + }; + //读任务 + Runnable readTask = () -> { + for (; ; ) { + read(); + } + }; + + //一个线程写,9个线程读 + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(writeTask);//写线程要写最后 + + + } +} +``` + +独线程一旦先执行,写线程很难再获得锁 + +> 输出结果: +> +> 读-->0 +> 读-->0 +> 读-->0 +> 读-->0 +> 读-->0 +> 读-->0 +> 读-->0 +> 读-->0 +> 读-->0 +> 读-->0 +> 读-->0 +> 读-->0 +> 写-->1 + +## 用StampedLock去乐观的读 + +只需要改一下read() + +```java +public class StampedLockDemo2 { + private static final StampedLock stampedLock = new StampedLock(); + + private static Integer DATA = 0; + + public static void write() { + long stamp = -1; + try { + stamp = stampedLock.writeLock();// 获取写锁 + DATA++; + System.out.println("写-->" + DATA); + } finally { + stampedLock.unlockWrite(stamp); // 释放写锁 + } + } + + public static void read() { + long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁 + + //在这块可能会有写锁抢锁,修改数据,所以用validate检查乐观读锁后是否有其他写锁发生 + /** + * 1、在这块可能会有写锁抢锁,修改数据,所以用validate检查乐观读锁后是否有其他写锁发生 + * 判断执行读操作期间,是否存在写操作,如果存在则validate返回false + * 2、如果有写锁抢锁,修改了数据,那么就要获取悲观锁。因为写锁在修改数据的过程中,你不能直接 + * 去读,只能老老实实拿到读锁再去读,才不会发生线程安全问题 + */ + if (!stampedLock.validate(stamp)) {//检查乐观读锁后是否有其他写锁发生 + try { + stamp = stampedLock.readLock();// 获取悲观读锁 + System.out.println("悲观读-->" + DATA); + return; + } finally { + stampedLock.unlockRead(stamp); // 释放悲观读锁 + } + } + System.out.println("乐观读-->" + DATA); + + } + + public static void main(String[] args) { + ExecutorService executor = Executors.newFixedThreadPool(10); + //写任务 + Runnable writeTask = () -> { + for (; ; ) { + write(); + } + }; + //读任务 + Runnable readTask = () -> { + for (; ; ) { + read(); + } + }; + + //一个线程写,9个线程读 + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(readTask); + executor.submit(writeTask); + + + } +} +``` + + + +> 悲观读-->704 +> 写-->705 +> 写-->706 +> 写-->707 +> 悲观读-->707 +> 写-->708 +> 悲观读-->708 +> 写-->709 +> 写-->710 +> 悲观读-->710 +> 乐观读-->710 +> 乐观读-->690 +> 乐观读-->690 +> 乐 + + + + + +# ForkJoin框架 + +思想:分而治之。将一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。 + +**举例说明** + +1、我们举个例子:如果要计算一个超大数组的和,最简单的做法是用一个循环在一个线程内完成: + +2、还有一种方法,可以把数组拆成两部分,分别计算,最后加起来就是最终结果,这样可以用两个线程并行执行: + +3、如果拆成两部分还是很大,我们还可以继续拆,用4个线程并行执行: + +这就是Fork/Join任务的原理:判断一个任务是否足够小。如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务。 + + + +**编码实现** + +**整个任务流程如下所示**: + +* 首先继承任务,覆写任务的执行方法 +* 通过判断阈值,判断该线程是否可以执行 +* 如果不能执行,则将任务继续递归分配,利用fork方法,并行执行 +* 如果是有返回值的,才需要调用join方法,汇集数据。 + + + +**主要的两个类:** + +* `RecursiveAction` 一个递归无结果的ForkJoinTask(没有返回值) +* `RecursiveTask` 一个递归有结果的ForkJoinTask(有返回值) + +## RecursiveTask + +```java +public class ForkJoinRecursiveTask { + /* + 1、分到哪种程度可以不用分了 + 2、也就是设置一个任务处理最大的阈值 + */ + private final static int MAX_THRESHOLD = 3; + + + public static void main(String[] args) { + final ForkJoinPool joinPool = new ForkJoinPool(); + ForkJoinTask future = joinPool.submit(new CalculatedRecursiveTask(0, 1000)); + try { + Integer integer = future.get(); + System.out.println("执行结果:" + integer); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + + private static class CalculatedRecursiveTask extends RecursiveTask { + + + private final int start;//任务开始的上标 + private final int end;//任务开始的下标 + + private CalculatedRecursiveTask(int start, int end) { + this.start = start; + this.end = end; + } + + @Override + protected Integer compute() { + if (end - start <= MAX_THRESHOLD) {//如果自己能处理,就自己计算 + return IntStream.rangeClosed(start, end).sum(); + } else {//自己处理不了,拆分任务 + int middle = (end + start) / 2; + CalculatedRecursiveTask leftTask = new CalculatedRecursiveTask(start, middle); + CalculatedRecursiveTask rightTask = new CalculatedRecursiveTask(middle + 1, end); + + //分别去执行 + leftTask.fork(); + rightTask.fork(); + + //把返回结果合起来 + return leftTask.join() + rightTask.join(); + } + } + } +} + +``` + +这个是有返回值的 + + + +## RecursiveAction + +这个是没有返回值的 + +```java +public class ForkJoinRecursiveAction { + + private final static int MAX_THRESHOLD = 3;//设置一个任务处理最大的阈值 + + private final static AtomicInteger SUM = new AtomicInteger(); + + + public static void main(String[] args) throws InterruptedException { + ForkJoinPool forkJoinPool = new ForkJoinPool(); + + forkJoinPool.submit(new CalculateRecursiveAction(0,1000)); + + //任务执行需要事件,这里可以等一下 + forkJoinPool.awaitTermination(10, TimeUnit.SECONDS); + + System.out.println("执行结果为:" + SUM); + } + + private static class CalculateRecursiveAction extends RecursiveAction{ + + private final int start; + private final int end; + + private CalculateRecursiveAction(int start, int end) { + this.start = start; + this.end = end; + } + + @Override + protected void compute() { + if ((end-start)<=MAX_THRESHOLD){ + SUM.addAndGet(IntStream.rangeClosed(start,end).sum()); + }else { + int middle = (start+end)/2; + CalculateRecursiveAction leftAction = new CalculateRecursiveAction(start,middle); + CalculateRecursiveAction rightAction = new CalculateRecursiveAction(middle+1,end); + leftAction.fork(); + rightAction.fork(); + } + } + } +} + +``` + + + +## 原理 + +**整个流程需要三个类完成**: + +1、**ForkJoinPool** + +- 既然任务是被逐渐的细化的,那就需要把这些任务存在一个池子里面,这个池子就是ForkJoinPool。 + +- 它与其它的ExecutorService区别主要在于它使用**"工作窃取"**,那什么是工作窃取呢? + +- 工作窃取:一个大任务会被划分成无数个小任务,这些任务被分配到不同的队列,这些队列有些干活干的块,有些干得慢。于是干得快的,一看自己没任务需要执行了,就去隔壁的队列里面拿去任务执行。 + +2、**ForkJoinTask** + +ForkJoinTask就是ForkJoinPool里面的每一个任务。他主要有两个子类:`RecursiveAction`和`RecursiveTask`。然后通过fork()方法去分配任务执行任务,通过join()方法汇总任务结果。 + + + +## 小总结 + +* Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。 +* ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction。 +* 使用Fork/Join模式可以进行并行计算以提高效率。 + + + diff --git a/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[2].md b/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[2].md new file mode 100644 index 0000000..b766305 --- /dev/null +++ b/docs/Java/concurrency/Java并发体系-第三阶段-JUC并发包-[2].md @@ -0,0 +1,2720 @@ + + +# Phaser工具 + +## 简介 + +java7中引入了一种新的可重复使用的同步屏障,称为移相器Phaser。Phaser拥有与`CyclicBarrier`和`CountDownLatch`类似的功能. + +但是这个类提供了更加灵活的应用。CountDownLatch和CyclicBarrier都是只适用于固定数量的参与者。移相器适用于可变数目的屏障,在这个意义上,可以在任何时间注册新的参与者。并且在抵达屏障是可以注销已经注册的参与者。因此,注册到同步移相器的参与者的数目可能会随着时间的推移而变化。 + +如CyclicBarrier一样,移相器可以重复使用,这意味着当前参与者到达移相器后,可以再一次注册自己并等待另一次到达. + +移相器的另一个重要特征是:移相器可能是分层的,这允许你以树形结构来安排移相器以减少竞争 + +**简单例子:** + +```Java +/** + * @Author: youthlql-吕 + * @Date: 2020/10/11 21:57 + *

+ * 功能描述: + */ +public class PhaserTest { + + private final static Random RANDOM = new Random(); + + public static void main(String[] args) { + final Phaser phaser = new Phaser(); + //JDK8语法,相当于创建5个线程 + IntStream.rangeClosed(1,5).boxed().map(i->phaser).forEach(Task::new); + //主线程也注册进去 + phaser.register(); + + phaser.arriveAndAwaitAdvance();//main线程 到达并等待前行 + System.out.println("All of work are finished."); + } + + static class Task extends Thread{ + private final Phaser phaser; + + Task(Phaser phaser) { + this.phaser = phaser; + phaser.register();//把自己加入计数器中 + start(); + } + + @Override + public void run() { + System.out.println("The worker[ "+getName()+ " ]" +" is working."); + try { + TimeUnit.SECONDS.sleep(RANDOM.nextInt(5)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + phaser.arriveAndAwaitAdvance();//自己完成,等待其他线程完成。 到达并等待前行 + } + } +} +``` + +**结果:** + + The worker[ Thread-1 ] is working. + The worker[ Thread-2 ] is working. + The worker[ Thread-0 ] is working. + The worker[ Thread-4 ] is working. + The worker[ Thread-3 ] is working. + All of work are finished. + +## 重复使用的例子 + + + + +​ + +```Java +/* +跑完步,需要去骑自行车,骑完自行车需要去跳高 +*/ +public class PhaserTest { + +private final static Random RANDOM = new Random(); + + + public static void main(String[] args) { + final Phaser phaser = new Phaser(3); + + for (int i = 1; i < 4; i++) { + new Athletes(i,phaser).start(); + } + } + + static class Athletes extends Thread { + private final int no; + private final Phaser phaser; + + + Athletes(int no, Phaser phaser) { + this.no = no; + this.phaser = phaser; + + } + + @Override + public void run() { + try { + System.out.println(no + " start running."); + TimeUnit.MILLISECONDS.sleep(RANDOM.nextInt(100)); + System.out.println(no + " end running."); + phaser.arriveAndAwaitAdvance(); + + System.out.println(no + " start bicycle."); + TimeUnit.MILLISECONDS.sleep(RANDOM.nextInt(100)); + System.out.println(no + " end bicycle."); + phaser.arriveAndAwaitAdvance(); + + + System.out.println(no + " start long jump."); + TimeUnit.MILLISECONDS.sleep(RANDOM.nextInt(100)); + System.out.println(no + " end long jump."); + phaser.arriveAndAwaitAdvance(); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} + +``` + + +​ + + +**结果**: + + 1 start running. + 2 start running. + 3 start running. + 3 end running. + 2 end running. + 1 end running. + 1 start bicycle. + 2 start bicycle. + 3 start bicycle. + 3 end bicycle. + 2 end bicycle. + 1 end bicycle. + 1 start long jump. + 2 start long jump. + 3 start long jump. + 2 end long jump. + 1 end long jump. + 3 end long jump. + +可以看到栅栏被重复利用了。 + +## 动态减少 + +```java +import java.util.Random; +import java.util.concurrent.*; + +public class test { + private final static Random RANDOM = new Random(); + + + public static void main(String[] args) { + final Phaser phaser = new Phaser(3); + + for (int i = 1; i < 3; i++) { + new Athletes(i,phaser).start(); + } + + //假设3号运动员受伤了 + new InjuredAthletes(3, phaser).start(); + } + + //运动员受伤了,需要减少 + static class InjuredAthletes extends Thread { + private final int no; + private final Phaser phaser; + + + InjuredAthletes(int no, Phaser phaser) { + this.no = no; + this.phaser = phaser; + + } + + @Override + public void run() { + try { + sport(no, phaser, " start running.", " end running."); + sport(no, phaser, " start bicycle.", " end bicycle."); + System.out.println(no + "号运动员受伤了"); + phaser.arriveAndDeregister();//动态减少 + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + static class Athletes extends Thread { + private final int no; + private final Phaser phaser; + + + Athletes(int no, Phaser phaser) { + this.no = no; + this.phaser = phaser; + + } + + @Override + public void run() { + try { + sport(no, phaser, " start running.", " end running."); + sport(no, phaser, " start bicycle.", " end bicycle."); + sport(no, phaser, " start long jump.", " end long jump."); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + + } + + private static void sport(int no, Phaser phaser, String x, String y) throws InterruptedException { + System.out.println(no + x); + TimeUnit.MILLISECONDS.sleep(RANDOM.nextInt(100)); + System.out.println(no + y); + phaser.arriveAndAwaitAdvance(); + } + +} + +``` + + + +结果: + +``` +2 start running. +1 start running. +3 start running. +2 end running. +1 end running. +3 end running. +3 start bicycle. +1 start bicycle. +2 start bicycle. +2 end bicycle. +3 end bicycle. +1 end bicycle. +1 start long jump. +2 start long jump. +3号运动员受伤了 +2 end long jump. +1 end long jump. +``` + +3号运动员受伤了,那么他就不能完成jump,3号运动员的`phaser.arriveAndAwaitAdvance()`也就无法执行,就会导致程序无法终止。因为Phaser数量是3个,只要三个线程都到了才会结束。所以说3号运动员受伤后,可以减少Phaser的数量:`phaser.arriveAndDeregister();//动态减少` + + +​ + + + + +​ + + + + +​ + +常用API +----- + +### 注册 + + public int register() + public int bulkRegister(int parties) + + +**register** + +* 是注册一个线程数,比较常用 + +**bulkRegister** + +* 可以批量注册 + +### 到达 + + public int arrive() + public int arriveAndDeregister() + public int arriveAndAwaitAdvance() + +**arrive** + +* 这个到达后,不会阻塞,相当于`countdown`机制【因为countdown只会阻塞调用者,其它线程干完任务就可以干其他事】 +* 大家要理解一点,party 数和线程是没有关系的,不能说一个线程代表一个 party,因为我们完全可以在一个线程中重复调用 arrive() 方法。这么表达纯粹是方便理解用。 + +**arriveAndAwaitAdvance** + +* 到达后会阻塞,相当于`CyclicBarrier`机制 + +**arriveAndDeregister** + +* 当线程出现异常,不能正常到达时,可以调用该方法,`动态减少注册数` + +**举例** + + public class PhaserTest { + + private static final Random RANDOM = new Random(); + + public static void main(String[] args) throws InterruptedException { + final Phaser phaser = new Phaser(5); + + for (int i = 0; i < 4; i++) { + new ArriveTask(i,phaser).start(); + } + //等待全部任务进行完成 + phaser.arriveAndAwaitAdvance(); + System.out.println("The phase 1 work finish done."); + + } + + private static class ArriveTask extends Thread{ + private final Phaser phaser; + + private ArriveTask(int no,Phaser phaser) { + super(String.valueOf(no)); + + this.phaser = phaser; + } + + @Override + public void run() { + System.out.println(getName() + " start working. "); + threadSleep(); + System.out.println(getName() + " The phase one is running."); + phaser.arrive(); + + threadSleep(); + System.out.println(getName() + " keep to other thing. "); + + } + } + + private static void threadSleep() { + try { + TimeUnit.SECONDS.sleep(RANDOM.nextInt(5)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + } + +### onAdvance() + +这个方法是 protected 的,所以它不是 phaser 提供的 API,从方法名字上也可以看出,它会在一个 phase 结束的时候被调用。 + +它的返回值代表是否应该终结(terminate)一个 phaser,之所以拿出来说,是因为我们经常会见到有人通过覆写该方法来自定义 phaser 的终结逻辑,如: + +```java +protected boolean onAdvance(int phase, int registeredParties) { + return phase >= N || registeredParties == 0; +} +``` + +> 1、我们可以通过 `phaser.isTerminated()` 来检测一个 phaser 实例是否已经终结了 +> +> 2、当一个 phaser 实例被终结以后,register()、arrive() 等这些方法都没有什么意义了,大家可以玩一玩,观察它们的返回值,原本应该返回 phase 值的,但是这个时候会返回一个负数。 + + + +### 监控子线程任务 + + public int awaitAdvance(int phase) + public int awaitAdvanceInterruptibly(int phase) throws InterruptedException + + +* 相当于起到监控的作用 +* 如果子线程还没有执行完成,主线程就会阻塞 +* 相较而言,可以不用增加注册量 + +**举例** + + public static void main(String[] args) throws InterruptedException { + final Phaser phaser = new Phaser(4); + + for (int i = 0; i < 4; i++) { + new AwaitAdvance(i,phaser).start(); + } + //等待全部任务进行完成 + phaser.awaitAdvance(phaser.getPhase()); + System.out.println("The phase 1 work finish done."); + } + + +### 强制关闭 + + public void forceTermination() + public boolean isTerminated() + + +* 强制关闭phaser,但是`如果线程陷入阻塞,不会唤醒` + +监控API +----- + +### 获取阶段数 + + public final int getPhase() + + +* 返回当前相位数。 最大相位数为Integer.MAX_VALUE +* 每增加一轮就会加一 + +**举例** + + public class PhaserTest { + public static void main(String[] args) { + final Phaser phaser = new Phaser(1); + System.out.println(phaser.getPhase()); + + phaser.arriveAndAwaitAdvance(); + System.out.println(phaser.getPhase()); + + phaser.arriveAndAwaitAdvance(); + System.out.println(phaser.getPhase()); + + phaser.arriveAndAwaitAdvance(); + System.out.println(phaser.getPhase()); + + } + } + + +**结果**: + + 0 + 1 + 2 + 3 + + +### 获取注册的数 + + public int getRegisteredParties() + + +* 获得注册的线程数,相当于Countdown初始的的计数器 +* 可以动态更改 + +### 获得到达和未到达的数目 + + public int getArrivedParties() + public int getUnarrivedParties() + + +**getArrivedParties** + +* 获得已经到达的线程数,和没有到达的线程数 + +**getUnarrivedParties** + +* 获得没有到达的线程数,和没有到达的线程数 + + + +## Phaser的分层结构 + + **Tiering** 这个词本身就不好翻译,大家将就一下,要表达的意思就是,将多个 Phaser 实例构造成一棵树。 + + 1、第一个问题来了,为什么要把多个 Phaser 实例构造成一棵树,解决什么问题?有什么优点? + + Phaser 内部用一个 `state` 来管理状态变化,随着 parties 的增加,并发问题带来的性能影响会越来越严重。 + + /** + * 0-15: unarrived + * 16-31: parties, 所以一个 phaser 实例最大支持 2^16-1=65535 个 parties + * 32-62: phase, 31 位,那么最大值是 Integer.MAX_VALUE,达到最大值后又从 0 开始 + * 63: terminated + */ + private volatile long state; + + + > 通常我们在说 0-15 位这种,说的都是从低位开始的 + + state 的各种操作依赖于 CAS,典型的无锁操作,但是,在大量竞争的情况下,可能会造成很多的自旋。 + + 而构造一棵树就是为了降低每个节点(每个 Phaser 实例)的 parties 的数量,从而有效降低单个 state 值的竞争。 + + 2、第二个问题,它的结构是怎样的? + + 这里我们不讲源码,用通俗一点的语言表述一下。我们先写段代码构造一棵树: + + Phaser root = new Phaser(5); + + Phaser n1 = new Phaser(root, 5); + Phaser n2 = new Phaser(root, 5); + + Phaser m1 = new Phaser(n1, 5); + Phaser m2 = new Phaser(n1, 5); + Phaser m3 = new Phaser(n1, 5); + + Phaser m4 = new Phaser(n2, 5); + + + 根据上面的代码,我们可以画出下面这个很简单的图: + + ![phaser](image/0002.png) + + 这棵树上有 7 个 phaser 实例,每个 phaser 实例在构造的时候,都指定了 parties 为 5,但是,对于每个拥有子节点的节点来说,每个子节点都是它的一个 party,我们可以通过 phaser.getRegisteredParties() 得到每个节点的 parties 数量: + + * m1、m2、m3、m4 的 parties 为 5 + * n1 的 parties 为 5 + 3,n2 的 parties 为 5 + 1 + * root 的 parties 为 5 + 2 + + 结论应该非常容易理解,我们来阐述一下过程。 + + 在子节点注册第一个 party 的时候,这个时候会在父节点注册一个 party,注意这里说的是子节点添加第一个 party 的时候,而不是说实例构造的时候。 + + 在上面代码的基础上,大家可以试一下下面的这个代码: + + Phaser m5 = new Phaser(n2); + System.out.println("n2 parties: " + n2.getRegisteredParties()); + m5.register(); + System.out.println("n2 parties: " + n2.getRegisteredParties()); + + + 第一行代码中构造了 m5 实例,但是此时它的 parties == 0,所以对于父节点 n2 来说,它的 parties 依然是 6,所以第二行代码输出 6。第三行代码注册了 m5 的第一个 party,显然,第四行代码会输出 7。 + + 当子节点的 parties 降为 0 的时候,会从父节点中"剥离",我们在上面的基础上,再加两行代码: + + m5.arriveAndDeregister(); + System.out.println("n2 parties: " + n2.getRegisteredParties()); + + + 由于 m5 之前只有一个 parties,所以一次 arriveAndDeregister() 就会使得它的 parties 变为 0,此时第二行代码输出父节点 n2 的 parties 为 6。 + + > 还有一点有趣的是,在非树的结构中,此时 m5 应该处于 terminated 状态,因为它的 parties 降为 0 了,不过在树的结构中,这个状态由 root 控制,所以我们依然可以执行 m5.register()... + + 3、每个 phaser 实例的 phase 周期有快有慢,怎么协调的? + + 在组织成树的这种结构中,每个 phaser 实例的 phase 已经不受自己控制了,由 root 来统一协调,也就是说,root 当前的 phase 是多少,每个 phaser 的 phase 就是多少。 + + 那又有个问题,如果子节点的一个周期很快就结束了,要进入下一个周期怎么办?需要等!这个时候其实要等所有的节点都结束当前 phase,因为只有这样,root 节点才有可能结束当前 phase。 + + 我觉得 Phaser 中的树结构我们要这么理解,我们要把整棵树当做一个 phaser 实例,每个节点只是辅助用于降低并发而存在,整棵树还是需要满足 Phaser 语义的。 + + + +# 阻塞队列 + +## 请谈谈对阻塞队列的理解 + +```shell +# 阻塞队列 + # 阻塞队列为空时,从队列中获取元素的操作将会被阻塞 + # 阻塞队列为满时,往队列里添加元素的操作将会被阻塞 + +# 阻塞队列的好处 + # 多线程领域中,所谓阻塞,即某些情况下会挂起线程,一旦条件满足,线程唤醒。 + +# 为什么需要 BlockingQueue + # 我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程了 + # 在 JUC 包发布以前,多线程环境下,程序员需要自己控制这些细节,并且兼顾效率与线程安全 +``` + +## 种类 + +```shell +# ArrayBlockingQueue + # 数组结构组成的有界阻塞队列 + +# LinkedBlockingQueue + # 由链表结构组成的有界(但大小默认值为 Integer.MAX_VALUE) 阻塞队列 + +# PriorityBlockingQueue + # 支持优先级排序的无界阻塞队列 + +# DelayQueue + # 使用优先级队列实现的延迟无界阻塞队列 + +# SynchronousQueue + # 不存储元素的阻塞队列,也即单个元素的队列 + +# LinkedTransferQueue + # 由链表结构组成的无界阻塞队列 + +# LinkedBlockingDeque + # 由链表结构组成的双向阻塞队列 +``` + +## 核心方法 + +```shell +# 抛出异常组 + # add(e) + # 队列满时 add 会抛出 java.lang.IllegalStateException: Queue full + # remove() + # 队列空时 remove 会抛出 java.util.NoSuchElementException + # element() + # 得到队首元素,队列为空时,抛出 java.util.NoSuchElementException + +# 返回布尔值组 + # offer(e) + # 往阻塞队列插入数据,成功时返回 true,失败时返回 false + # poll() + # 从阻塞队列取出数据,成功时返回 数据,队列为空时返回 null + # peek() + # 取出队首元素,成功时返回 数据,队列为空时返回 null + +# 阻塞 + # put(e) + # 往阻塞队列插入数据,无返回值,插入不成功时阻塞线程,直至插入成功 Or 线程中断 + # take() + # 从阻塞队列取出数据,成功返回数据,不成功时阻塞线程,直至取出成功 Or 线程中断 + +# 超时 + # offer(e,time,unit) + # 往阻塞队列插入数据,成功返回 true,不成功时线程阻塞等待超时时间,过时返回false 并放弃操作 + # poll(time,unit) + # 从阻塞队列取出数据,成功返回 数据,队列为空时线程阻塞等待超时时间,过时返回false 并放弃操作 +``` + +## 阻塞队列的使用场景 + +```shell +# 生产者消费者模式 + +# 线程池 + +# 消息中间件 +``` + +### 传统版生产者消费者模式 Demo + +```java +package ProducerAndConsumer; + +/** + * @Author: youthlql-吕 + * @Date: 2019/9/26 14:56 + *

+ * 功能描述: 功能描述: 4个线程的if语句 + * 要求:生产者线程消费一个,消费者线程消费一个。num只能为1或0 + * 改用while循环的4个线程 + * + */ +public class Producer_03 { + public static void main(String[] args) { + Consumer3 consumer = new Consumer3(); + + //生产者线程A + new Thread(() ->{ + for (int i = 0; i < 10; i++) { + try { + consumer.increment(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + },"生产者A").start(); + + new Thread(() ->{ + for (int i = 0; i < 10; i++) { + try { + consumer.decrement(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + },"消费者B").start(); + + new Thread(() ->{ + for (int i = 0; i < 10; i++) { + try { + consumer.increment(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + },"生产者C").start(); + + new Thread(() ->{ + for (int i = 0; i < 10; i++) { + try { + consumer.decrement(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + },"消费者D").start(); + } +} +class Consumer3{ + private Integer num = 0; + + public synchronized void increment() throws InterruptedException { + while(num != 0){ + this.wait(); + } + num++; + System.out.println(Thread.currentThread().getName() + "\t" + num); + notifyAll(); + + } + + public synchronized void decrement() throws InterruptedException { + while(num == 0){ + this.wait(); + } + num--; + System.out.println(Thread.currentThread().getName() + "\t" + num); + notifyAll(); + } + +} +``` + +### 阻塞队列版生产者消费者模式Demo + +```java +/** + * @Author: youthlql-吕 + * @Date: 2019/9/26 16:04 + *

+ * 功能描述: + */ +public class Video44 { + public static void main(String[] args) { + MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10)); + new Thread(() ->{ + System.out.println("----------生产者线程启动-----------"); + try { + myResource.produce(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + },"Producer").start(); + + + new Thread(() ->{ + System.out.println("----------消费者线程启动-----------"); + try { + myResource.consume(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + },"Consumer").start(); + + + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + myResource.stop(); + System.out.println("********5秒之后,main叫停生产,生产结束*********"); + } +} + +class MyResource{ + private volatile Boolean FLAG = Boolean.TRUE; + private AtomicInteger atomicInteger = new AtomicInteger(); + private BlockingQueue blockingQueue = null; + + public MyResource(BlockingQueue blockingQueue){ + this.blockingQueue = blockingQueue; + //打印日志一般需要看类信息 + System.out.println(blockingQueue.getClass().getName()); + } + + public void produce() throws InterruptedException { + String data = null; + Boolean returnValue; + while(FLAG){ + data = atomicInteger.incrementAndGet() + ""; + returnValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS); + if (returnValue){ + System.out.println(Thread.currentThread().getName() + "\t\t 插入队列成功 \t" + data); + }else { + System.out.println(Thread.currentThread().getName() + "\t 插入超时 \t" + data); + } + try { + TimeUnit.MILLISECONDS.sleep(300); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + System.out.println(Thread.currentThread().getName() + "\t 大老板叫停,生产者停止生产"); + } + + public void consume() throws InterruptedException { + String data = null; + while(FLAG){ +// Thread.sleep(500); + data = blockingQueue.poll(2L,TimeUnit.SECONDS); + if (data == null || data.equalsIgnoreCase("")){ + FLAG = false; + System.out.println(Thread.currentThread().getName() + "\t 消费超时,消费者退出" ); + return; + } + System.out.println(Thread.currentThread().getName() + "\t 消费队列成功 \t" + data); + } + } + + public void stop(){ + this.FLAG = false; + } +} +``` + + + +# 线程池 + +## 主要优点 + +- 第一:降低资源消耗.通过重复利用自己创建的线程降低线程创建和销毁造成的消耗. +- 第二: 提高响应速度.当任务到达时,任务可以不需要等到线程的创建,就能立即执行. +- 第三: 提高线程的可管理性.线程是稀缺资源,如果无限的创阿金,不仅会消耗资源,还会较低系统的稳定性,使用线程池可以进行统一分配,调优和监控. + +Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor、Executors、ExecutorService、ThreadPoolExecutor 这几个类。 + +## 线程池七大参数入门简介 + +**流程举例** + +一个银行网点 <线程池>,共 10* 个窗口 ,开放 5* 个窗口 +。今天办理业务的特别多,其余5个窗口加班一天 ,办理业务的人在窗口前排队* 。银行*里的A*职员、B职员... 给办理业务 最多排10个,来了11个,并且每个窗口都有人在办理业务,多的人怎么拒绝呢? + + + +**七大参数** + +- corePoolSize + 线程池中的常驻核心线程数 + 创建线程池后,当有请求任务进来,就安排池中的线程去执行请求任务 + 当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列中 + +- maximumPoolSize + 线程池能够容纳同时执行的最大线程数,此值必须大于等于1 + +- keepAliveTime + 多余的空闲线程的存活时间 + 当前线程池数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 值时, + 多余空闲线程会被销毁直到只剩下 corePoolSize 个线程为止 + +- unit + keepAliveTime 的单位 + +- workQueue + 任务队列,被提交但尚未被执行的任务 + +- threadFactory + 表示生成线程池中工作线程的线程工厂<线程名字、线程序数...> + 用于创建线程一般用默认的即可 + +- handler + 拒接策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时 + 如何拒绝新的任务 + + + +```java +import java.util.concurrent.*; + +public class ThreadPoolDemo { + + public static void main(String[] args) { + ExecutorService threadPool = + new ThreadPoolExecutor(2, + 5, + 1L, + TimeUnit.SECONDS, + new LinkedBlockingDeque(3), + Executors.defaultThreadFactory(), + new ThreadPoolExecutor.CallerRunsPolicy()); + + for(int i = 1;i <= 9; i++ ) + threadPool.execute(() -> { + System.out.println(Thread.currentThread().getName() + "\t 办理业务" ); + try { + TimeUnit.SECONDS.sleep(1L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + } + +} +``` + + + +## 线程池的底层工作流程 + + + +1、创建线程池后,等待请求任务 + +2、当调用 execute() 方法添加请求任务时,线程池做如下判断 + +- 如果正在运行的线程数量小于 corePoolSize,马上创建线程执行请求任务 +- 如果正在运行的线程数量大于或等于 corePoolSize,将请求任务放入阻塞队列 +- 如果阻塞队列满了,且正在运行的线程数小于 mamimumPoolSize,创建非核心线程执行请求任务 +- 如果队列满了且线程池线程达到最大线程数,线程池启动饱和拒绝策略来执行 + +3、当一个线程完成任务时,从阻塞队列中取出下一个任务来执行 + +4、当一个线程无事可做超过一定时间时,线程池会判断 + +- 如果当前运行的线程数大于 corePoolSize,该线程被销毁 +- 所以,线程池完成所有请求任务后,最终会收缩到 corePoolSize 的大小 + + + +## 线程池的4种拒绝策略 + + **JDK 内置的拒绝策略** + +- AbortPolicy(默认) + - 直接抛出 RejectedExecutionException 异常阻止系统正常运行 + + +- CallerRunsPolicy + - "调用者运行" 一种调节机制 + - 该策略既不会抛弃任务,也不会抛出异常 + - 而是将某些任务回退到调用者,从而降低新任务的流量 + + +- DiscardOldestPolicy + - 抛弃队列中等待最久的任务 + - 然后把当前任务中加入队列中尝试再次提交当前任务 + +- DiscardPolicy + - 直接丢弃任务,不予任何处理也不抛出异常 + - 如果允许任务丢失,这是最好的一种方案 + + 以上拒绝策略都是实现了 RejectedExecutionHandler 接口 + + + +## 线程池在实际生产中使用哪一个 + +> 后文会介绍Java内置的几个线程池 + + 阿里巴巴 Java 开发手册 + 线程池不允许使用 Executors 创建,而是通过 ThreadPoolExecutor 的方式 + +FixedThreadPool 和 SingleThreadPool + 允许的阻塞队列容量为 Integer.MAX_VALUE,可能会堆积大量的请求,导致 OOM + +CachedThreadPool 和 ScheduledThreadPool + 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,导致 OOM + + + +## 线程池合理配置参数 + +**1、CPU 密集型** + 意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行 + CPU 密集任务只有在真正的多核 CPU 上才可能得到加速(通过多线程) + CPU 密集型任务配置尽可能少的线程数量 + 一般公式 : CPU 核数 + 1个线程的线程池最大线程数 + +**2、IO 密集型** + 由于 IO 密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程 + 一般公式 : CPU 核数* 2 + +**3、IO 密集型 2** + IO 密集型、即该任务需要大量的 IO,即大量的堵塞 + 在单线程上运行 IO 密集型的任务会导致浪费大量的 CPU 算力浪费在等待上 + 所以,IO 密集型任务中使用多线程可以大大的加速程序运行,即时在单核 CPU 上 + 这种加速主要就是利用了被浪费掉的阻塞时间 + 参考公式 : CPU 核数 / (1 - 阻塞系数) + 例: 8 核CPU 8/(1-0.9) = 80 个线程数 + + + +## 线程池的状态 + +**线程池状态含义如下** + +• RUNNING 接受新任务并且处理阻塞队列里的任务 + +• SHUTDOWN :拒绝新任务但是处理阻塞队列里的任务 + +• STOP :拒绝新任务并且放弃阻塞队列里的任务,同时会中断正在处理的任务。 + +• TIDYING:所有任务都执行完(包含阻塞队列里面的任务)后,当前线程池活动线程,数为0,将要调用 terminated 方法 + +• TERMINATED:终止状态,terminated 方法调用完成以后的状态 + +**线程池状态转换列举如下** + +• RUNNING -> SHUTDOWN 显式调用shutdown () 方法 或者隐式调用了 finalize()方法里面的 shutdown() 方法 + +• RUNNING或SHUTDOWN) -> STOP 显式调用 shutdownNow() 方法 + +• SHUTDOWN ->TIDYING 当线程池和任务队列都为空时 + +• STOP -> TIDYING 当线程池为空时 + +• TIDYING -> TERMNATED terminated() hook 方法执行完成 + + + +## 线程池的关闭 + +关闭有两个方法:`shutdown`和`shutdownNow` + +**shutdown** + +```java +public void shutdown() { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + checkShutdownAccess(); + advanceRunState(SHUTDOWN); + interruptIdleWorkers(); + onShutdown(); // hook for ScheduledThreadPoolExecutor + } finally { + mainLock.unlock(); + } + tryTerminate(); +} + + +private void interruptIdleWorkers() { + interruptIdleWorkers(false); +} + + +private void interruptIdleWorkers(boolean onlyOne) { + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + for (Worker w : workers) { + Thread t = w.thread; + if (!t.isInterrupted() && w.tryLock()) { + try { + t.interrupt(); + } catch (SecurityException ignore) { + } finally { + w.unlock(); + } + } + if (onlyOne) + break; + } + } finally { + mainLock.unlock(); + } +} + +``` + +* 从源码可以看出,本质上执行的是`interrupt`方法 +* 如果线程是空闲的,执行的是Condition的await的方法,会被直接打断,被回收 +* 如果正在工作,该线程会被打上一个标记,等任务执行后被回收 + +**shutdownNow** + +```java +public List shutdownNow() { + List tasks; + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + checkShutdownAccess(); + advanceRunState(STOP); + interruptWorkers();//先打断 + tasks = drainQueue();//再把任务队列没有执行的任务取出 + } finally { + mainLock.unlock(); + } + tryTerminate();//不断的打断 + return tasks; +} +``` + +* 先打断空闲的打断 +* 然后清空任务队列 +* 然后不断的尝试打断正在执行的线程 +* 最后会返回一个List集合,包含还没有执行的任务 + +**awaitTermination 操作** + +当线程调用`awaitTermination`方法后,当前线程会被阻塞,直到线程池状态变为TERMINATED 才返回 或者等待时间超时才返回。 + + + +# Executors + +> 内置线程池用的不多,不用太在意 + +## 简介 + +Java通过Executors提供五种线程池,分别为: + +* `newCachedThreadPool`:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 + +* `newFixedThreadPool`:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 + +* `newScheduledThreadPool`:创建一个定长线程池,支持定时及周期性任务执行。 + + **和一个线程的区别** + + | newSingleThreadExecutor | Thread | + | -------------------------------------- | ------------------------------ | + | 任务执行完成后,不会自动销毁,可以复用 | 任务执行完成后,会自动销毁 | + | 可以将任务存储在阻塞队列中,逐个执行 | 无法存储任务,只能执行一个任务 | + +* `newSingleThreadExecutor`:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 + +* `newWorkStealingPool`:创建一个ForkJoin线程池,线程数是CPU核数,可以充分利用CPU资源。从1.8开始有的 + +**简单例子:** + +```Java +/** + * @Author: youthlql-吕 + * @Date: 2020/4/23 10:49 + *

+ * 功能描述: 线程池的三个常用方式 + */ +public class Video47 { + + public static void main(String[] args){ + /** + * 一池5个处理线程 + */ + //ExecutorService threadPool= Executors.newFixedThreadPool(5); + /** + * 一池一线程 + */ +// ExecutorService threadPool= Executors.newSingleThreadExecutor(); + /** + * 一池N线程 + */ + ExecutorService threadPool = Executors.newCachedThreadPool(); + //模拟10个用户来办理业务 没有用户就是来自外部的请求线程. + try { + for (int i = 1; i <= 20; i++) { + threadPool.execute(() -> { + System.out.println(Thread.currentThread().getName() + "\t 办理业务"); + }); + try { + TimeUnit.MICROSECONDS.sleep(200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + threadPool.shutdown(); + } + } +} +``` + + + +有三个内置线程池比较简单,下面介绍下稍复杂的两个内置线程池。 + +## newWorkStealingPool + + +```Java +public static ExecutorService newWorkStealingPool(int parallelism) { + return new ForkJoinPool + (parallelism, + ForkJoinPool.defaultForkJoinWorkerThreadFactory, + null, true); +} +public static ExecutorService newWorkStealingPool() { + return new ForkJoinPool + (Runtime.getRuntime().availableProcessors(), + ForkJoinPool.defaultForkJoinWorkerThreadFactory, + nul, true); +} + //Returns the number of processors available to the Java virtual machine. + Runtime.getRuntime().availableProcessors() + +``` + +**分析源码我们可以得知** + +* 采用的ForkJoin框架,可以将任务进行分割,同时线程之间会互相帮助 +* 最大的线程数是CPU核数,充分利用CPU资源 + +## newScheduledThreadPool + + public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { + return new ScheduledThreadPoolExecutor(corePoolSize); + } + + +* 创建的是一个定时的任务,每隔一段时间就会运行一次 + +**首先可以对比的就是Timer这个类** + + public class ExecutorsTest { + + public static void main(String[] args) throws InterruptedException { + Timer timer = new Timer(); + final TimerTask task = new TimerTask() { + @Override + public void run() { + System.out.println("=====" + System.currentTimeMillis()); + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + //1秒执行一次 + timer.schedule(task,0,1000); + } + } + + +**结果** + + =====1602597314888 + =====1602597316897 + =====1602597318898 + =====1602597320898 + =====1602597322899 + =====1602597324899 + + +可以发现:**如果任务时间超过了定时时长,就无法按照预定的时间执行** + +**其他工具的解决方式**: + +* `crontab`定时处理器**为了确保时间的正确性,会重新启一个线程** + +**有三个方法** + +* schedule(commod,delay,unit) ,这个方法是说系统启动后,需要等待多久执行,delay是等待时间。只执行一次,没有周期性。 + +* scheduleAtFixedRate(commod,initialDelay,period,unit),这个是以period为固定周期时间,按照一定频率来重复执行任务,initialDelay是说系统启动后,需要等待多久才开始执行。例如:如果设置了period为5秒,线程启动之后执行了大于5秒,线程结束之后,立即启动线程的下一次,如果线程启动之后只执行了3秒就结束了那执行下一次,需要等待2秒再执行。这个是优先保证任务执行的频率, + +* scheduleWithFixedDelay(commod,initialDelay,delay,unit),这个是以delay为固定延迟时间,按照一定的等待时间来执行任务,initialDelay意义与上面的相同。例如:设置了delay为5秒,线程启动之后不管执行了多久,结束之后都需要先生5秒,才能执行下一次。这个是优先保证任务执行的间隔。 + + + +# ExecutorService + +``` +public class Video53 { + public static void main(String[] args) { + ExecutorService threadPool = new ThreadPoolExecutor( + 2, + 5, + 1L, + TimeUnit.SECONDS, + new LinkedBlockingDeque(3), + Executors.defaultThreadFactory(), + //默认抛出异常 + //new ThreadPoolExecutor.AbortPolicy() + //回退调用者 + //new ThreadPoolExecutor.CallerRunsPolicy() + //处理不来的不处理 + //new ThreadPoolExecutor.DiscardOldestPolicy() + new ThreadPoolExecutor.DiscardPolicy() + ); + //模拟10个用户来办理业务 没有用户就是来自外部的请求线程. + try { + for (int i = 1; i <= 10; i++) { + threadPool.execute(() -> { + System.out.println(Thread.currentThread().getName() + "\t 办理业务"); + }); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + threadPool.shutdown(); + } + //threadPoolInit(); + } + + +} +``` + +ExecutorService一般就是用来作为我们自定义线程池的引用。 + +**API** + +1、`getActiveCount()`:获取当前线程池中活跃的线程个数;若是没有`execute(Runnable)`任务的话,是不会创建线程的;提交一个任务,也只会创建一个线程去执行,而不会一次性直接创建`corePoolSize`个线程。 + +2、`allowCoreThreadTimeOut(true)`:当任务执行完成的时候,释放线程池;**若使用的线程池的keepAliveTime为0,需要手动修改**,因为不允许keepAliveTime为0的线程池,调用此方法; + +3、`invokeAny(Call)`:此方法是一个同步方法,会阻塞调用线程;若其中有一个任务返回了,则其它的任务取消,不会继续执行; 此方法也存在超时设置重构方法;防止线程一直等待;无法结束。 + +# Future + +## Future API + +1、`get()`:此方法是阻塞的,但是抛出了InterruptedException,所以是可以被打断的;使用`interrupt()`进行打断的时候,打断的是调用get()的线程,让当前线程不再阻塞的等待获取数据;并不是真正执行任务的那个线程。 + +2、`get(TimeOut)`:若是获取数据超时了,但是任务还是依旧执行,只是不再等待任务的返回值。 + +3、`isDone()`:执行任务期间不管是否执行成功了,还是执行失败了(抛出异常)。只要结束,isDone()就会返回true。 + + + +4、`boolean cancel(boolean mayInterruptIfRunning)`:取消任务。 + +返回false的情况:1.任务已经执行完成了,是无法被取消的。2.之前已经被cancel过 + +```java +public static void main(String[] args) { + try { + testCancel(); + } catch (InterruptedException e) { + e.printStackTrace(); + } +} + +private static void testCancel() throws InterruptedException { + // 把线程设置为守护线程, 根据启动线程dead. + ExecutorService executorService = Executors.newCachedThreadPool(); + AtomicBoolean running = new AtomicBoolean(true); + + Future future = executorService.submit(() -> { + + while (running.get()){ + //模拟一个执行很久的任务 + } + System.out.println("1111111"); + return 10; + }); + + TimeUnit.MILLISECONDS.sleep(500); + System.out.println(future.cancel(true)); + System.out.println(future.isCancelled()); + System.out.println(future.isDone()); + } +``` + +> 输出: +> +> true +> true +> true + +根据例子我们可以看到,cancel虽然取消了任务,但是任务任然在执行,这是为什么呢? + +> https://blog.csdn.net/stephen8341/article/details/50433656 + +其实我们如果查看FutureTask的源码就会发现cancel只不过是调用了Thread的interrupt方法,而interrupt只能是停掉线程中有sleep,wait,join逻辑的线程,抛出一个InterruptException。这样看来FutureTask的cancel方法并不能停掉一切正在执行的异步任务。但是这里我们有一个妥协的做法就是在判断条件中加!Thread.currentThread().isInterrupted()这个判断即可. + +**改进代码1** + +```java +private static void testCance2() throws InterruptedException { + // 把线程设置为守护线程, 根据启动线程dead. + ExecutorService executorService = Executors.newCachedThreadPool(); + + Future future = executorService.submit(() -> { + + while (!Thread.interrupted()){ + //模拟一个执行很久的任务 + } + System.out.println("1111111"); + return 10; + }); + + TimeUnit.MILLISECONDS.sleep(500); + System.out.println(future.cancel(true)); + System.out.println(future.isCancelled()); + System.out.println(future.isDone()); +} +``` + +> 输出: +> +> true +> true +> true +> 1111111 + +可以看到任务是真正被终止了。 + + + +还有一个场景 + +```java +while (!Thread.interrupted()){ + //模拟一个执行很久的任务 +} +``` + +在上面改进代码的第一步,第一行代码是个IO操作,假设耗时非常长,那就根本没有机会判断while条件。此时如果cancel,一样不会真正的终止任务的执行。 + +**改进代码2** + +``` +private static void testCance3() throws InterruptedException { + // 把线程设置为守护线程, 根据启动线程dead. + AtomicBoolean running = new AtomicBoolean(true); + ExecutorService executorService = Executors.newCachedThreadPool( r -> { + Thread t = new Thread(r); + t.setDaemon(true); + return t; + }); + + Future future = executorService.submit(() -> { +// while (!Thread.interrupted()){ +// //模拟一个执行很久的任务 +// } + + while (running.get()){ + //模拟一个执行很久的任务 + } + + System.out.println("1111111"); + return 10; + }); + + TimeUnit.SECONDS.sleep(2); + System.out.println(future.cancel(true)); // 可以取消掉任务, 但是无法终止任务的执行. + System.out.println(future.isCancelled()); + System.out.println(future.isDone()); + } +``` + +> 控制台输出: +> +> true +> true +> true +> +> Process finished with exit code 0 + +可以看到直接结束了,思想就是将线程设置为守护线程,一旦主线程执行完,守护线程无论在干什么都会马上结束。所以后面的`System.out.println("1111111");`都没有打印 + + + +## 已经被cancel的任务,是否还能拿到结果? + +``` +private static void testCance2() throws Exception { + // 把线程设置为守护线程, 根据启动线程dead. + ExecutorService executorService = Executors.newCachedThreadPool(); + + Future future = executorService.submit(() -> { + + while (!Thread.interrupted()){ + //模拟一个执行很久的任务 + } + System.out.println("1111111"); + return 10; + }); + + TimeUnit.MILLISECONDS.sleep(500); + System.out.println(future.cancel(true)); + System.out.println(future.isCancelled()); + System.out.println(future.isDone()); + TimeUnit.SECONDS.sleep(1); + System.out.println(future.get()); +} +``` + +> 输出: +> +> true +> true +> true +> 1111111 +> java.util.concurrent.CancellationException +> at java.util.concurrent.FutureTask.report(FutureTask.java:121) +> at java.util.concurrent.FutureTask.get(FutureTask.java:192) +> at Future.FutureExample1.testCance2(FutureExample1.java:63) +> at Future.FutureExample1.main(FutureExample1.java:19) + +输出了111111,说明程序已经走到了return那一行,但是可以看到拿不到了爆出了异常。 + + + +## Future的缺陷以及解决方案 + +1、缺陷一:使用Future可以保证任务的异步执行;但是,只要去获取任务的结果,就会导致程序的阻塞;从而,**从异步再次变为了同步**。 + +2、缺陷二:假设批量执行一些异步任务,大部分任务都是几秒完成的,有少许任务是几个小时才完成。那你get()的时候,万一拿到了几个小时执行的任务,就会一直阻塞,导致几秒完成的任务拿不到结果。 + +3、像netty会有回调的callback + +**缺陷代码** + +```java +private static void futureExecSomeTask() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newFixedThreadPool(2); + final List> callableList = Arrays.asList( + () -> { + TimeUnit.SECONDS.sleep(10); + System.out.println("Thread 10 finished!"); + return 10; + }, + () -> { + TimeUnit.SECONDS.sleep(20); + System.out.println("Thread 20 finished!"); + return 20; + } + ); + + // invokeAll会阻塞等待所有的future执行完成. + List> futureList = executorService.invokeAll(callableList); + for (Future future : futureList) { + System.out.println(future.get()); + } + } + +``` + + + +**JDK7解决方案** + +```java +private static void futureDefect() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(2); + + final List> callableList = Arrays.asList( + () -> { + TimeUnit.SECONDS.sleep(10); + System.out.println("Thread 10 finished!"); + return 10; + }, + () -> { + TimeUnit.SECONDS.sleep(20); + System.out.println("Thread 20 finished!"); + return 20; + } + ); + + List> futureList = new ArrayList<>(); + futureList.add(executorService.submit(callableList.get(0))); + futureList.add(executorService.submit(callableList.get(1))); + + for (Future future : futureList) {// 其实相当于把批量任务, 单个的提交给线程池去执行. + System.out.println(future.get()); + } +} + +``` + + + + + +**JDK8解决方案** + +CompletionService:具体见下面 + + + +# CompletionService + +## 简介 + +- CompletionService的实现目标是任务先完成可优先获取到,即结果按照完成先后顺序排序。 + +- ExecutorCompletionService类是常用的CompletionService实现类,该类只有三个成员变量: + +```java +public class ExecutorCompletionService implements CompletionService { + private final Executor executor; + private final AbstractExecutorService aes; + private final BlockingQueue> completionQueue; + .... +} +``` + +* 可以看到ExecutorCompletionService主要是**增强executor线程池的。** +* Task包装后被塞入completionQueue,当Task结束,其Future就可以从completionQueue中获取到。 + +**执行流程:** + +![image-20200914182032618](image/0004.png) + + + +## 阻塞和非阻塞获取 + +```java +public Future take()throws InterruptedException + +public Future poll() +public Future poll(long timeout,TimeUnit unit) throws InterruptedException +1234 +``` + +**阻塞获取** + +take方法回使调用者阻塞,可以保证一定会有Future取出 + +**非阻塞获取** + +poll方法会去查看是否有任务完成,有则取出;没有,就会返回一个null + + + +## 代码解决Future缺陷 + +```java +public class CompletionServiceExample1 { + + public static void main(String[] args) { + try { + testCompleteExecutorService(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + + private static void testCompleteExecutorService() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newFixedThreadPool(2); + final List> callableList = Arrays.asList( + () -> { + TimeUnit.MILLISECONDS.sleep(200); + System.out.println("Thread 10 finished!"); + return 10; + }, + () -> { + TimeUnit.MILLISECONDS.sleep(400); + System.out.println("Thread 20 finished!"); + return 20; + } + ); + + // 参数值为线程池对象. + ExecutorCompletionService completionService = new ExecutorCompletionService<>(executorService); + // 提交需要执行的任务. + callableList.stream().forEach(item -> completionService.submit(item)); + + Future future; + // 阻塞的获取任务结果. 但是, 不是等待全部任务完成, 而是, 完成一个任务, 获取一个任务结果. + while ((future = completionService.take()) != null) { + System.out.println(future.get()); + } + //因为take阻塞住了,所以你是看不到下面这个打印的 + System.out.println("Main is finished!"); + } + +} +``` + +结果: + +``` +Thread 10 finished! +10 +Thread 20 finished! +20 +``` + + + +稍微改一下就可以打印出来了 + +```Java +public class CompletionServiceExample3 { + + public static void main(String[] args) { + try { + testCompleteExecutorService(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + + private static void testCompleteExecutorService() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newFixedThreadPool(2); + final List> callableList = Arrays.asList( + () -> { + TimeUnit.MILLISECONDS.sleep(200); + System.out.println("Thread 10 finished!"); + return 10; + }, + () -> { + TimeUnit.MILLISECONDS.sleep(400); + System.out.println("Thread 20 finished!"); + return 20; + } + ); + + // 参数值为线程池对象. + ExecutorCompletionService completionService = new ExecutorCompletionService<>(executorService); + // 提交需要执行的任务. + callableList.stream().forEach(item -> completionService.submit(item)); + + int taskCount = callableList.size(); + + + for (int i = 0; i < taskCount; i++) { + Integer result = completionService.take().get(); + System.out.println(result); + } + + // + System.out.println("Main is finished!"); + + //记得关闭线程池 + executorService.shutdown(); + } +} + +``` + + + +结果: + +``` +Thread 10 finished! +10 +Thread 20 finished! +20 +Main is finished! + +Process finished with exit code 0 +``` + + + + + +## 按完成顺序获取结果验证 + +```java +public class CompletionServiceTest { + + public static void main(String[] args) { + Long start = System.currentTimeMillis(); + //开启3个线程 + ExecutorService exs = Executors.newFixedThreadPool(5); + try { + int taskCount = 10; + // 结果集 + List list = new ArrayList(); + + // 1.定义CompletionService + CompletionService completionService = new ExecutorCompletionService(exs); + + // 2.添加任务 + for(int i=0;i{ + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }).whenComplete((v,t)->{ + System.out.println("Done"); + }); + Thread.currentThread().join(); + } +} + +``` + + + +控制台输出: + +> Done + + + +2、可以改为此方法runAsync(Runnable, Executors), 让线程池去去管理线程. 不会跟随调用线程消失; 但是, 需要注意关闭线程池. + +```java +public static void testrunAsync() { + + ExecutorService threadPool = Executors.newFixedThreadPool(2); + CompletableFuture.runAsync(() -> { + System.out.println("starting"); + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("end!"); + + }, threadPool).whenComplete((v, t) -> { + System.out.println("Finished!"); + }); + + System.out.println("All finished!"); + threadPool.shutdown(); +} +``` + + + +控制台输出: + +> starting +> All finished! +> end! +> Finished! +> +> Process finished with exit code 0 + +## 构造CompleableFuture + +创建`CompleableFuture`不建议使用构造方法,而是使用静态的工厂方法构建。 + +```Java +public static CompletableFuture allOf(CompletableFuture... cfs) + + +public static CompletableFuture anyOf(CompletableFuture... cfs) + + +public static CompletableFuture completedFuture(U value) + + +public static CompletableFuture runAsync(Runnable runnable,Executor executor) +public static CompletableFuture runAsync(Runnable runnable) + +public static CompletableFuture supplyAsync(Supplier supplier) +public static CompletableFuture supplyAsync(Supplier supplier, Executor executor) +``` + + +* `allOf(CompletableFuture... cfs)`:这个方法会返回一个全新的CompletableFuture,传递进去的所有CompletableFuture执行完才算是执行完成。 +* `anyOf(CompletableFuture... cfs)`:这个方法会返回一个全新的CompletableFuture,只要传递进去的有一个CompletableFuture执行完,就算是执行完成 +* `completedFuture(U value)` :可以假设一个执行出了一个结果,进行下面的级联操作。 +* `runAsync`:异步的执行Runnable,没有返回值。 +* `supplyAsync`:异步的执行Supplier实例,会有返回值。 + + + +### runAsync + +```Java +public static CompletableFuture runAsync(Runnable runnable,Executor executor) +public static CompletableFuture runAsync(Runnable runnable) +``` + +特点就是没有返回值,并且参数是`Runnable`。比一般的提交一个Runnable相比,可以更加灵活点使用,级联、并联等操作 + +**举例:** + +```java +public class Test_runAsync { + public static ExecutorService executor = Executors.newFixedThreadPool(10); + + public static void main(String[] args) { + System.out.println("main....start...."); + CompletableFuture future = CompletableFuture.runAsync(() -> { + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("当前线程:" + Thread.currentThread().getId()); + int i = 10 / 2; + System.out.println("运行结果:" + i); + }, executor); + + //通过前面的睡眠5秒,也可以验证出,shutdown会处理已经在阻塞队列里的 + executor.shutdown(); + } +} +``` + + + +**结果:** + +``` +main....start.... +当前线程:12 +运行结果:5 + +Process finished with exit code 0 +``` + + + +### supplyAsync + +```Java +public static CompletableFuture supplyAsync(Supplier supplier) +public static CompletableFuture supplyAsync(Supplier supplier, Executor executor) +``` + +需要给`supplyAsync`提供一个Supplier + +**举例:** + +```Java +public class Test_supplyAsync { + + public static ExecutorService executor = Executors.newFixedThreadPool(10); + + public static void main(String[] args) throws ExecutionException, InterruptedException { + System.out.println("main....start...."); + + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + System.out.println("当前线程:" + Thread.currentThread().getName()); + int i = 10 / 0; + System.out.println("运行结果:" + i); + return i; + }, executor).whenComplete((res, excption) -> { //虽然能得到异常信息,但是没法修改返回数据 + System.out.println("异步任务成功完成了...结果是:" + res + ";异常信息是" + excption); + }).exceptionally(throwable -> { //可以感知异常,同时返回默认值 + return 10; + }); //成功以后干啥事 + + System.out.println("future获取结果:" + future.get()); + + + CompletableFuture future1 = CompletableFuture.supplyAsync(() -> { + System.out.println("当前线程:" + Thread.currentThread().getName()); + int i = 10 / 4; + System.out.println("运行结果:" + i); + return i; + }, executor).handle((res, thr) -> { + if (res != null) { + return res * 2; + } + if (thr != null) { //异常不等于空了,就返回0 + return 0; + } + return 0; + }); + + System.out.println("future1获取结果:" + future1.get()); + + + executor.shutdown(); + } +} +``` + + +**结果**: + + main....start.... + 当前线程:pool-1-thread-1 + 异步任务成功完成了...结果是:null;异常信息是java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero + future获取结果:10 + 当前线程:pool-1-thread-2 + 运行结果:2 + future1获取结果:4 + + Process finished with exit code 0 + + + + + + +### anyOf + +```` +public static CompletableFuture anyOf(CompletableFuture... cfs) +```` + +**举例:** + +```Java +public class Test_anyOf { + public static ExecutorService executor = Executors.newFixedThreadPool(10); + public static void main(String[] args) throws Exception{ + CompletableFuture futureImg = CompletableFuture.supplyAsync(() -> { + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("查询商品的图片信息"); + return "hello.jpg"; + },executor); + CompletableFuture futureAttr = CompletableFuture.supplyAsync(() -> { + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("查询商品的属性"); + return "黑色+256G"; + },executor); + CompletableFuture futureDesc = CompletableFuture.supplyAsync(() -> { + System.out.println("查询商品的介绍"); + return "华为"; + },executor); + + /** + * 1、因为anyOf是等待最早的一个CompletableFuture就能结束,所以返回值是最早执行完的那个任务。 + * 2、直接通过原来的future.get()可能会有空指针异常 + */ + CompletableFuture anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc); + anyOf.get();//等待所有结果完成 + System.out.println("最早完成的任务返回值为:"+anyOf.get()); + + executor.shutdown(); + } +} +``` + + +**结果:** + + 查询商品的介绍 + 最早完成的任务返回值为:华为 + 查询商品的图片信息 + 查询商品的属性 + + +这个例子中,前两个`CompletableFuture`都睡了两秒,所以执行最快的肯定是第三个,从结果中也得到了验证。 + +需要注意一点,虽然是异步的从一个地方取值,但是其他任务依然会执行完成,而并非不再执行了。 + +### allOf + +```Java +public static CompletableFuture allOf(CompletableFuture... cfs) +``` + + + +```Java +public class Test_allOf { + public static ExecutorService executor = Executors.newFixedThreadPool(10); + + public static void main(String[] args) throws Exception { + CompletableFuture futureImg = CompletableFuture.supplyAsync(() -> { + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("查询商品的图片信息"); + return "hello.jpg"; + }, executor); + CompletableFuture futureAttr = CompletableFuture.supplyAsync(() -> { + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("查询商品的属性"); + return "黑色+256G"; + }, executor); + CompletableFuture futureDesc = CompletableFuture.supplyAsync(() -> { + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("查询商品的介绍"); + return "华为"; + }, executor); + + System.out.println("等待Future返回------"); + //因为allOf是等待所有CompletableFuture完成才能结束,所以没有返回值,直接通过原来的future.get()就一定会有返回值 + CompletableFuture allOf = CompletableFuture.allOf(futureImg, futureAttr, futureDesc); + System.out.println("最终得到的结果:" + futureImg.get() + "=>" + futureAttr.get() + "=>" + futureDesc.get()); + + executor.shutdown(); + } +} +``` + +结果: + +``` +等待Future返回------ +查询商品的图片信息 +查询商品的介绍 +查询商品的属性 +最终得到的结果:hello.jpg=>黑色+256G=>华为 + +Process finished with exit code 0 +``` + + + +## 组合方法 + +### 组合两个任务,同时处理两个结果 + +```Java +public CompletableFuture thenAcceptBoth(CompletionStage other, + BiConsumer action) +public CompletableFuture thenAcceptBothAsync(CompletionStage other, + BiConsumer action) +public CompletableFuture thenAcceptBothAsync(CompletionStage other, + BiConsumer action, + Executor executor) +``` + + + + +**举例**: + +```Java +public class Test_Accept { + public static ExecutorService executor = Executors.newFixedThreadPool(10); + + public static void main(String[] args) throws Exception { + CompletableFuture completableFuture1 = CompletableFuture.supplyAsync(() -> "我是任务1"); + CompletableFuture completableFuture2 = CompletableFuture.supplyAsync(() -> "我是任务2"); + completableFuture1.thenAcceptBothAsync(completableFuture2, (s, i) -> { + System.out.println(s + "==>" + i); + }, executor); + + executor.shutdown(); + } +} +``` + + +**结果**: + + 我是任务1==>我是任务2 + + Process finished with exit code 0 + + +**分析** + +* 可以看出是两个任务组合,然后同时将两个结果一起处理 + + + +### 组合两个任务,任务完成后做的操作 + + public CompletableFuture runAfterBoth(CompletionStage other, + Runnable action) + public CompletableFuture runAfterBothAsync(CompletionStage other, + Runnable action) + public CompletableFuture runAfterBothAsync(CompletionStage other, + Runnable action) + + + +### 当两个任务任意一个执行完成后,执行一个操作 + +```Java +public CompletableFuture runAfterEither(CompletionStage other, + Runnable action) +public CompletableFuture runAfterEitherAsync(CompletionStage other, + Runnable action) +public CompletableFuture runAfterEitherAsync(CompletionStage other, + Runnable action,Executor executor)) +``` + +**举例** + +```Java +public class Test_runAfterEither { + public static ExecutorService executor = Executors.newFixedThreadPool(10); + + public static void main(String[] args) throws Exception { + CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> { + System.out.println("我是任务1"); + return "a"; + },executor); + + CompletableFuture future = completableFuture.runAfterEitherAsync(CompletableFuture.supplyAsync(() -> { + System.out.println("我是任务2"); + return "b"; + }), + () -> System.out.println("两个任务执行完,我才执行"),executor); + + System.out.println("end"); + executor.shutdown(); + } + +} +``` + + +**结果**: + + 我是任务1 + 我是任务2 + end + 两个任务执行完,我才执行 + + Process finished with exit code 0 + + + +### 组合两个任务,处理后,返回一个结果 + + public CompletableFuture thenCombine(CompletionStage other, + BiFunction fn) + public CompletableFuture thenCombineAsync(CompletionStage other, + BiFunction fn) + public CompletableFuture thenCombineAsync(CompletionStage other, + BiFunction fn,, Executor executor) + + +​ + +**举例** + +```Java +public class Test_thenCombine { + public static ExecutorService executor = Executors.newFixedThreadPool(10); + + public static void main(String[] args) throws Exception { + CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> "a"); + + CompletableFuture future = completableFuture.thenCombineAsync(CompletableFuture.supplyAsync(() -> 100), + (s, i) -> { + System.out.println("s: " + s + " , i : " + i); + return true; + }, executor); + System.out.println(future.get()); + executor.shutdown(); + } +} +``` + +**结果**: + + s: a , i : 100 + true + + Process finished with exit code 0 + + + +### 第一个任务的输出是第二个任务的输入 + +```Java +public CompletableFuture thenCompose(Function> fn) +public CompletableFuture thenComposeAsync(Function> fn) +public CompletableFuture thenComposeAsync(Function> fn, Executor executor) +``` + +相当于一次级联操作 + +举例: + +```java +public class Test_thenCompose { + public static ExecutorService executor = Executors.newFixedThreadPool(10); + + /** + * public CompletableFuture thenComposeAsync(Function> fn, Executor executor) + */ + public static void main(String[] args) throws Exception { + CompletableFuture completableFuture1 = CompletableFuture.supplyAsync(() -> "我是任务1"); + CompletableFuture completableFuture2 = CompletableFuture.supplyAsync(() -> "我是任务2"); + CompletableFuture future = completableFuture1.thenComposeAsync(s -> completableFuture2, executor); + System.out.println(future.get()); + + executor.shutdown(); + } +} +``` + + + +**结果:** + +``` +我是任务2 + +Process finished with exit code 0 +``` + +## 中转方法 + +### 有返回值 + +#### 当执行完成时执行的操作 + + public CompletableFuture whenComplete(BiConsumer action) + public CompletableFuture whenCompleteAsync(BiConsumer action) + public CompletableFuture whenCompleteAsync(BiConsumer action, Executor executor) + + +**举例** + +```java +public class Test_whenComplete { + + public static void main(String[] args) throws Exception { + CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> "Hello"); + CompletableFuture future = completableFuture.whenComplete((v, t) + -> { + System.out.println(v + " World !"); + //这个t是Throwable,只有报错了才会打印 + System.out.println(t); + }); + System.out.println("future:" + future.get()); + Thread.currentThread().join(); + } +} +``` + + + + +​ + +**结果** + + Hello World ! + Hello + + +**分析** + +* 当执行完成时执行的回调方法 + +* 该方法会接收执行的结果以及异常 + +* 回调完成会,会把原来任务执行的结果传递回去 + +* whenCompleteAsync是异步的;whenComplete是同步的,会卡住主线程 + +* 需要传递一个`BiConsumer`接口,如下所示: + + ```java + public CompletableFuture whenComplete(BiConsumer action)l; + ``` + + + +```java + public interface BiConsumer { + void accept(T t, U u); + } + ``` + + + +* T是执行的结果,U是执行时产生的异常 + + + +#### 级联操作 + + public CompletableFuture thenApply(Function fn) + public CompletableFuture thenApplyAsync(Function fn) + public CompletableFuture thenApplyAsync(Function fn,Executor executor) + + +**举例** + +```java +public class Test_thenApplyAsync { + public static void main(String[] args) throws Exception { + CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> "Hello"); + + CompletableFuture future = completableFuture.thenApplyAsync(t -> { + String s = t + " World !"; + System.out.println(s); + return s.length(); + }); + System.out.println(future.get()); + Thread.currentThread().join(); + } +} +``` + + + + + +```java +public CompletableFuture thenApplyAsync(Function fn); + +public interface Function { + R apply(T t); +} +``` + +**结果** + + Hello World ! + 13 + + Process finished with exit code -1 + + +**分析** + +* 是一个级联操作,即拿着上个任务的结果,做下个任务,同时返回一个新的结果 + + + +#### 处理结果的操作 + + public CompletableFuture handle(BiFunction fn) + public CompletableFuture handleAsync(BiFunction fn) + public CompletableFuture handleAsync(BiFunction fn,Executor executor) + + +**举例** + + public class CompletableFutureTest { + + public static void main(String[] args) throws Exception { + CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> "Hello"); + + CompletableFuture future = completableFuture.handleAsync((s,t) -> { + String aaa = t + " World !"; + System.out.println(aaa); + return aaa.length(); + }); + System.out.println(future.get()); + Thread.currentThread().join(); + } + + } + + +**结果**: + + Hello World ! + 13 + + +**分析**: + +* 相比于`whenComplete`返回值可以自己处理,相当于一次级联 +* 相比于`thenApply`,可以处理异常 + + + +### 无返回值 + +#### 处理结果 + + public CompletableFuture thenAccept(Consumer action) + public CompletableFuture thenAcceptAsync(Consumer action) + public CompletableFuture thenAcceptAsync(Consumer action,Executor executor) + + +**举例** + + public class CompletableFutureTest { + + public static void main(String[] args) throws Exception { + CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> "Hello"); + + CompletableFuture future = completableFuture.thenAccept(t -> { + String aaa = t + " World !"; + System.out.println(aaa); + }); + System.out.println(future.get()); + Thread.currentThread().join(); + } + + } + + +**结果** + + Hello World ! + null + + +**分析** + +* 相当于一次级联,但是没有返回值 + +#### 执行完全部任务 + + public CompletableFuture thenRun(Runnable action) + public CompletableFuture thenRunAsync(Runnable action) + public CompletableFuture thenRunAsync(Runnable action,Executor executor) + + +**分析** + +* 相较`thenAccept`,不处理任务的执行结果 + + + +## 终结方法 + +#### 处理异常 + + public CompletableFuture exceptionally(Function fn) + + +```Java +public class Test_exceptionally { + + public static void main(String[] args) throws Exception { + CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> { + int a = 1 / 0; + return "World "; + }); + completableFuture.exceptionally(Throwable::getMessage).thenAccept(t -> { + System.out.println(t); + }); + + Thread.currentThread().join(); + } +} +``` + + + +**结果:** + +``` +java.lang.ArithmeticException: / by zero + +Process finished with exit code -1 +``` + + + +#### 立马获取结果 + + public T getNow(T valueIfAbsent) + + +**举例** + +```java +public class Test_getNow { + public static void main(String[] args) throws Exception { + CompletableFuture completableFuture = CompletableFuture.supplyAsync( + () -> { + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "World"; + }); + + + String now = completableFuture.getNow("Hello"); + System.out.println(now); + System.out.println(completableFuture.get()); + Thread.currentThread().join(); + } +} +``` + + + +**结果** + + Hello + World + + Process finished with exit code -1 + + +**分析** + +* 如果结果完成返回结果,如果未完成,返回传入进去的值 + + + +#### 判断结果是否完成,如果未完成则赋予结果 + + public boolean complete(T value) + + +#### 判断结果是否完成,如果未完成返回异常 + + public boolean completeExceptionally(Throwable ex) + + +#### 后续获取结果会产生异常 + + public void obtrudeException(Throwable ex) + + +总结 +== + +* `thenAccept`()处理正常结果; +* `exceptionally`()处理异常结果; +* `thenApplyAsync`()用于串行化另一个`CompletableFuture`; +* `anyOf`()和`allOf`()用于并行化多个`CompletableFuture`。 + + + +# 参考: + +《Java并发编程之美》 + +https://www.cnblogs.com/yuandengta/p/12887361.html \ No newline at end of file diff --git a/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[1].md b/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[1].md new file mode 100644 index 0000000..03d09b7 --- /dev/null +++ b/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[1].md @@ -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 + *

+ * 功能描述: 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值是8,B线程又执行9: getstatic得到前一个值是7。马上A线程就把8赋值给了num变量。但是B线程已经拿到了之前的值7,B线程是在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 + + org.openjdk.jcstress +jcstress-core +${jcstress.version} + +``` + + + +``` +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。 + +情况4:0,发生了指令重排 + +```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)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。 + + + + + +**指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致**。所以在多线程下,指令重排序可能会导致一些问题。 + +## 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内存模型之前,先来看一下到底什么是计算机内存模型。 + +## 计算机结构 + +### 计算机结构简介 + +冯诺依曼,提出计算机由五大组成部分,输入设备,输出设备存储器,控制器,运算器。 + + + +输入设备:鼠标,键盘等等 + +输出设备:显示器,打印机等等 + +存储器:内存条 + +运算器和控制器组成CPU + + + +### CPU + +中央处理器,是计算机的控制和运算的核心,我们的程序终都会变成指令让CPU去执行,处理程序中 的数据。 + +### 内存 + +我们的程序都是在内存中运行的,内存会保存程序运行时的数据,供CPU处理。 + +### 缓存 + +CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。内 存的读写速度成为了计算机运行的瓶颈。于是就有了在CPU和主内存之间增加缓存的设计。靠近CPU 的缓存称为L1,然后依次是 L2,L3和主内存,CPU缓存模型如图下图所示。 + + + +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。 + + + +上面的图中有一个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`。 + + + +**主内存** + +主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。 + +**工作内存** + +每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操 作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接 访问对方工作内存中的变量。 + + + +Java的线程不能直接在主内存中操作共享变量。而是首先将主内存中的共享变量赋值到自己的工作内存中,再进行操作,操作完成之后,刷回主内存。 + +**Java内存模型的作用** + +Java内存模型是一套在多线程读写共享数据时,对共享数据的可见性、有序性、和原子性的规则和保障。 synchronized,volatile + +## CPU缓存,内存与Java内存模型的关系 + +- 通过对前面的CPU硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应该已经意识到,多线程的执行终都会映射到硬件处理器上进行执行。 但Java内存模型和硬件内存架构并不完全一致。 +- 对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存和主内存之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响, 因为JMM只是一种抽象的概念,是一组规则,不管是工作内存的数据还是主内存的数据,对于计算机硬 件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说, Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。 + +JMM内存模型与CPU硬件内存架构的关系: + + + +工作内存:可能对应CPU寄存器,也可能对应CPU缓存,也可能对应内存。 + +- Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量 存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。 + + + +## 再谈可见性 + + + + + +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种操作本身都是原子性的。虚拟机实现时必须保证下面 提及的每一种操作都是原子的、不可再分的。 + + + +> (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,那就是下面这样的 + + + + + +# 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 -> C,JMM要求编译器和处理器都禁止这种重排序。 +* 不会改变程序执行结果的重排序,比如 A -> B,JMM对编译器和处理器不做要求,允许这种重排序。 + + + +**举例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术语介绍 + + + + + + + +``` +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里缓存了该内存地址的数据无效。 + +​ 为了提高处理速度,处理器不直接和主内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了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读同一个volatile,step 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缓存,如L1,L2等 + +### 保守策略下 + + + +- 在每个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写插入内存屏障后生成的指令序列示意图** + + + +> 图中的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读插入内存屏障后生成的指令序列示意图** + + + +> 图中的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()方法,编译器在生成字节码时可以做如下的优化 + + + +​ 注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器通常会在这里插入一个StoreLoad屏障。 + +​ 上面的优化针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以X86处理器为例,图中除最后的StoreLoad屏障外,其他的屏障都会被省略。 + + + +### X86处理器优化 + +前面保守策略下的volatile读和写,在X86处理器平台可以优化成如下图所示。 + +X86处理器仅会对写-读操作做重排序。X86不会对读-读、读-写和写-写操作 做重排序,因此在X86处理器中会省略掉这3种操作类型对应的内存屏障。在X86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在X86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。 + + + + + +## 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的禁止重排序功能还是非常有用的。 \ No newline at end of file diff --git a/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[2].md b/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[2].md new file mode 100644 index 0000000..aba6660 --- /dev/null +++ b/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[2].md @@ -0,0 +1,482 @@ +# 可见性设计的硬件 + + + +从硬件的级别来考虑一下可见性的问题 + + + +**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和refresh,MESI协议是如何与内存屏障搭配使用的(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用于定位到拉链散列表中的某个bucket,tag是用于定位cache entry,offset是用于定位一个变量在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消息,从主内存(或者是其他处理器)来加载,这个具体怎么实现要看底层的硬件了,都有可能的 + + + + + + + +## MESI-优化 + + + +​ MESI协议如果每次写数据的时候都要发送invalidate消息等待所有处理器返回ack,然后获取独占锁后才能写数据,那可能就会导致性能很差了,因为这个对共享变量的写操作,实际上在硬件级别变成串行的了。所以为了解决这个问题,硬件层面引入了写缓冲器和无效队列 + +1、 + + 写缓冲器的作用是,一个处理器写数据的时候,直接把数据写入缓冲器,同时发送invalidate消息,然后就认为写操作完成了,接着就干别的事儿了,不会阻塞在这里。接着这个处理器如果之后收到其他处理器的ack消息之后才会把写缓冲器中的写结果拿出来,通过对cache entry设置为E加独占锁,同时修改数据,然后设置为M。 + +​ 其实写缓冲器的作用,就是处理器写数据的时候直接写入缓冲器,不需要同步阻塞等待其他处理器的invalidate ack返回,这就大大提升了硬件层面的执行效率了 + + 包括查询数据的时候,会先从写缓冲器里查,因为有可能刚修改的值在这里,然后才会从高速缓存里查,这个就是存储转发 + +2、 + +​ 引入无效队列,就是说其他处理器在接收到了invalidate消息之后,不需要立马过期本地缓存,直接把消息放入无效队列,就返回ack给那个写处理器了,这就进一步加速了性能,然后之后从无效队列里取出来消息,过期本地缓存即可 + +​ 通过引入写缓冲器和无效队列,一个处理器要写数据的话,这个性能其实很高的,他直接写数据到写缓冲器,发送一个validate消息出去,就立马返回,执行别的操作了;其他处理器收到invalidate消息之后直接放入无效队列,立马就返回invalidate ack + + + +## 硬件层面的MESI协议为何会引发有序性和可见性的问题? + +MESI协议在硬件层面的原理其实大家都已经了解的很清晰了。 + +讲了这么多,再来看一下MESI-协议为何会引发可见性和有序性的问题 + + + +- 可见性:写缓冲器和无效队列导致的,写数据不一定立马写入自己的高速缓存(或者主内存),是因为可能写入了写缓冲器;读数据不一定立马从别人的高速缓存(或者主内存)刷新最新的值过来,invalidate消息在无效队列里面 + + + +- 有序性: + + 简单的举两个例子 + +(1)StoreLoad重排序 + + + + ```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后执行 + + + +(2)StoreStore重排序 + + ```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里是如何实现原子操作的。 + +## 相关术语 + + + + + +## 处理器如何实现原子操作 + +32位IA-32处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操 作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。Pentium 6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器是不能自动保证其原子性的,比如跨总线宽度、跨多个缓存行和跨页表的访问。但是,处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。 + +### 使用总线锁保证原子性 + +**第一个机制是通过总线锁保证原子性。**如果多个处理器同时对共享变量进行读改写操作 (i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操 作就不是原子的,操作完之后共享变量的值会和期望的不一致。举个例子,如果i=1,我们进行 两次i++操作,我们期望的结果是3,但是有可能结果是2,如图所示。 + + + +​ 原因可能是多个处理器同时从各自的缓存中读取变量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的思想】 \ No newline at end of file diff --git a/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[3].md b/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[3].md new file mode 100644 index 0000000..d8b1fee --- /dev/null +++ b/docs/Java/concurrency/Java并发体系-第二阶段-锁与同步-[3].md @@ -0,0 +1,1258 @@ +# synchronized保证三大特性 + +**synchronized保证原子性的原理** + +对num++;增加同步代码块后,保证同一时间只有一个线程操作num++;。就不会出现安全问题。 + + + +**synchronized保证可见性的原理** + +synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变 量的值。 + + + +**synchronized保证有序性的原理** + +我们加synchronized后,依然会发生重排序,只不过我们有同步 代码块,可以保证只有一个线程执行同步代码中的代码。保证有序性。 + + + +# synchronized的特性 + +## 可重入特性 + +意思就是一个线程可以多次执行synchronized,重复获取同一把锁。 + +```java +/* + 目标:演示synchronized可重入 + 1.自定义一个线程类 + 2.在线程类的run方法中使用嵌套的同步代码块 + 3.使用两个线程来执行 + */ +public class Demo01 { + public static void main(String[] args) { + new MyThread().start(); + new MyThread().start(); + } + + public static void test01() { + synchronized (MyThread.class) { + String name = Thread.currentThread().getName(); + System.out.println(name + "进入了同步代码块2"); + } + } +} + +// 1.自定义一个线程类 +class MyThread extends Thread { + @Override + public void run() { + synchronized (MyThread.class) { + System.out.println(getName() + "进入了同步代码块1"); + + Demo01.test01(); + } + } +} +``` + +**可重入原理** + +synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁.。在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。可重入的好处 + +1. 可以避免死锁 +2. 可以让我们更好的来封装代码 + + + +## 不可中断特性 + +**什么是不可中断** + +一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第 二个线程会一直阻塞或等待,不可被中断。 + +### synchronized不可中断演示 + +``` +public class Test { + private static Object obj = new Object(); + public static void main(String[] args) throws InterruptedException { + // 1.定义一个Runnable + Runnable run = () -> { + // 2.在Runnable定义同步代码块 + synchronized (obj) { + String name = Thread.currentThread().getName(); + System.out.println(name + "进入同步代码块"); + // 保证不退出同步代码块 + try { + Thread.sleep(888888); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + + // 3.先开启一个线程来执行同步代码块 + Thread t1 = new Thread(run); + t1.start(); + Thread.sleep(1000); + // 4.后开启一个线程来执行同步代码块(阻塞状态) + Thread t2 = new Thread(run); + t2.start(); + + // 5.停止第二个线程 + System.out.println("停止线程前"); + t2.interrupt(); + System.out.println("停止线程后"); + + System.out.println(t1.getState()); + System.out.println(t2.getState()); + } +} +``` + +**输出结果:** + +> Thread-0进入同步代码块 +> 停止线程前 +> 停止线程后 +> TIMED_WAITING +> BLOCKED + +### ReentrantLock可中断演示 + +```java +public class Test { + private static Lock lock = new ReentrantLock(); + public static void main(String[] args) throws InterruptedException { +// test01(); + test02(); + } + + // 演示Lock可中断 + public static void test02() throws InterruptedException { + Runnable run = () -> { + String name = Thread.currentThread().getName(); + boolean b = false; + try { + b = lock.tryLock(3, TimeUnit.SECONDS); + if (b) { + System.out.println(name + "获得锁,进入锁执行"); + Thread.sleep(88888); + } else { + System.out.println(name + "在指定时间没有得到锁做其他操作"); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + if (b) { + lock.unlock(); + System.out.println(name + "释放锁"); + } + } + }; + + Thread t1 = new Thread(run); + t1.start(); + Thread.sleep(1000); + Thread t2 = new Thread(run); + t2.start(); + + System.out.println("停止t2线程前"); + t2.interrupt(); + System.out.println("停止t2线程后"); + + Thread.sleep(4000); + System.out.println(t1.getState()); + System.out.println(t2.getState()); + } + + // 演示Lock不可中断 + public static void test01() throws InterruptedException { + Runnable run = () -> { + String name = Thread.currentThread().getName(); + try { + lock.lock(); + System.out.println(name + "获得锁,进入锁执行"); + Thread.sleep(88888); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + System.out.println(name + "释放锁"); + } + }; + + Thread t1 = new Thread(run); + t1.start(); + Thread.sleep(1000); + Thread t2 = new Thread(run); + t2.start(); + + System.out.println("停止t2线程前"); + t2.interrupt(); + System.out.println("停止t2线程后"); + + Thread.sleep(1000); + System.out.println(t1.getState()); + System.out.println(t2.getState()); + } +} +``` + + + +控制台输出: + +> Thread-0获得锁,进入锁执行 +> 停止t2线程前 +> 停止t2线程后 +> java.lang.InterruptedException +> +> atjava.util.concurrent.locks.AbstractQueuedSynchronizer.tryAcquireNanos(AbstractQueuedSynchronizer.java:1245) +> at java.util.concurrent.locks.ReentrantLock.tryLock(ReentrantLock.java:442) +> at Test.lambda$test02$0(Test.java:24) +> at java.lang.Thread.run(Thread.java:748) +> TIMED_WAITING +> TERMINATED + +关于ReentranLock锁中断的原理,在AQS里讲。 + + + +# synchronized简单原理 + +> 我相信这个原理大部分人应该都知道,很多资料都讲过,我这里简单描述一下。 + + + +```java +public class SyncTest { + public void syncBlock(){ + synchronized (this){ + System.out.println("hello block"); + } + } + public synchronized void syncMethod(){ + System.out.println("hello method"); + } +} +``` + +使用javap对其进行反汇编,部分信息如下 + +``` +{ + public void syncBlock(); + descriptor: ()V + flags: ACC_PUBLIC + Code: + stack=2, locals=3, args_size=1 + 0: aload_0 + 1: dup + 2: astore_1 + 3: monitorenter // monitorenter指令进入同步块 + 4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; + 7: ldc #3 // String hello block + 9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V + 12: aload_1 + 13: monitorexit // monitorexit指令退出同步块 + 14: goto 22 + 17: astore_2 + 18: aload_1 + 19: monitorexit // monitorexit指令退出同步块 + 20: aload_2 + 21: athrow + 22: return + Exception table: + from to target type + 4 14 17 any + 17 20 17 any + + + public synchronized void syncMethod(); + descriptor: ()V + flags: ACC_PUBLIC, ACC_SYNCHRONIZED //添加了ACC_SYNCHRONIZED标记 + Code: + stack=2, locals=1, args_size=1 + 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; + 3: ldc #5 // String hello method + 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V + 8: return + +} +``` + + + +## synchronized修饰代码块时 + +#### monitorenter + +首先我们来看一下JVM规范中对于monitorenter的描述: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter + +> Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count. • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership. + +翻译过来: 每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获 取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应 的monitor的所有权。其过程如下: + +1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为 monitor的owner(所有者) +2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1 +3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直 到monitor的进入数变为0,才能重新尝试获取monitor的所有权。 + +monitorenter小结: synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个 同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner:拥有 这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待 + +#### monitorexit + +首先我们来看一下JVM规范中对于monitorexit的描述: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit + +> The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so. + +翻译过来: + +1.能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。 + +1. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出 monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个 monitor的所有权 + +monitorexit释放锁。 monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。 + + + +**总结**:synchronized在修饰代码块时,是通过`monitorenter` 和 `monitorexit`来保证并发安全。 + + + +## synchronized 修饰方法的的情况 + +`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 + +**不过两者的本质都是对对象监视器 monitor 的获取。** + + + + + +# Java对象的布局(C++代码层面) + +> 在学习synchronized最底层的C++源码级别前,我们需要先了解这个知识点,不然后面的可能看不懂 + +术语参考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html 在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示: + + + +## 对象头 + +当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?是 存在锁对象的对象头中的。 HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头,arrayOopDesc对象用来描述数组类型。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,另外,arrayOopDesc 的定义对应 arrayOop.hpp + + + +从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义载Hotspot 源码中的 oop.hpp 文件中。 + + + +- 在普通实例对象中,oopDesc的定义包含两个成员,分别是 _mark 和 _metadata + +- _mark 表示对象标记、属于markOop类型,也就是接下来要讲解的Mark World,它记录了对象和锁有关的信息 + +- _metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示 普通指针、 _compressed_klass 表示压缩类指针。 + +- 对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指 针,及对象指向它的类元数据的指针。 + +> 关于Klass,Class这些JVM的东西,推荐去看《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》。强调一点,是第三版。第三版相比第二版改了太多东西了,我看第二版的时候总感觉讲的不够深入缺点什么东西,不过第三版补齐了很多东西。 + +### Mark Word + +Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、 线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。Mark Word对应的类 型是 markOop 。源码位于 markOop.hpp 中。 + + + + + +在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下: + + + +在32位虚拟机下,Mark Word是32bit大小的,其存储结构如下: + + + +再加一个图对比一下,有一丁点的补充 + + + + + + + +### klass pointer + +这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的 实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。 如果应用的对 象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内 存。为了节约内存可以使用选项**-XX:+UseCompressedOops** 开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位: + +1. 每个Class的属性指针(即静态变量) +2. 每个对象的属性指针(即对象变量) +3. 普通对象数组的每个元素指针 + +当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对 象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。 对象头 = Mark Word + 类型指针(未开启指针压缩的情况下) 在32位系统中,Mark Word = 4 bytes,类型指针 = 4bytes,对象头 = 8 bytes = 64 bits; 在64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits; + +## 实例数据 + +就是类中定义的成员变量。 + +## 对齐填充 + +对齐填充并不是必然存在的,也没有什么特别的意义,他仅仅起着占位符的作用,由于HotSpot VM的 自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是**8字节的 整数倍**。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充 来补全。 + + + +## 查看Java对象布局的方法 + +``` + + org.openjdk.jol + jol-core + 0.9 + +``` + + + + + +# Lock Record + +字面意思就是锁记录。通过对Java对象头的介绍可以看到锁信息也是存在于对象的`mark word`中的。当对象状态为偏向锁(biasable)时,`mark word`存储的是偏向的线程ID;当状态为轻量级锁(lightweight locked)时,`mark word`存储的是指向线程栈中`Lock Record`的指针;当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。 + + + +## Lock Record的结构 + +线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个`Lock Record`,其包括一个用于存储对象头中的 `mark word`(官方称之为`Displaced Mark Word`)以及一个指向对象的指针。下图右边的部分就是一个`Lock Record`。 + + + + + + + +# synchronized偏向锁原理(C++源码层面) + +## 概述 + +1、偏向锁是JDK 6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。 偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对 象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及 ThreadID即可。 + +## 举例 + +Java是支持多线程的语言,因此在很多基础库中为了保证代码在多线程的情况下也能正常运行,也就是我们常说的线程安全,都会加入如`synchronized`这样的同步语义。但是在应用在实际运行时,很可能只有一个线程会调用相关同步方法。比如下面这个demo: + +```java +public class Demo{ + private List list = new ArrayList<>(); + + public static void main(String[] args) { + Demo Demo = new Demo(); + for (int i = 0; i < 50; i++) { + Demo.add("Demo--->" + i); + } + } + + public synchronized void add(String s) { + list.add(s); + } + +} +``` + +在这个demo中为了保证对list操纵时线程安全,对add方法加了`synchronized`的修饰,但实际使用时却只有一个线程调用到该方法。对于轻量级锁而言,每次调用add时,加锁解锁都有一个CAS操作;对于重量级锁而言,加锁也会有一个或多个CAS操作(这里的一个和多个 只是针对该demo,并不适用于所有场景)。 + + + +## 对象的Mark Word + +- 偏向锁在Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用-XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争 状态,可以通过 XX:-UseBiasedLocking=false 参数关闭偏向锁。 +- 当JVM启用了偏向锁模式(1.6以上默认开启),当新创建一个对象的时候,如果该对象所属的class没有关闭偏向锁模式(什么时候会关闭一个class的偏向模式下文会说,默认所有class的偏向模式都是是开启的),那新创建对象的`mark word`将是可偏向状态,此时`mark word中`的thread id(参见上文偏向状态下的`mark word`格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。 + + + +## 加锁过程 + +> 偏向锁加锁的C++代码过于复杂,这里只是用文字描述了几种情况。正常面试的时候,也不会让你说C++代码执行过程。 + +加锁过程分为几种情况(case),注意下面的不是顺序,只是加锁的几种情况。 + +case 1:当该对象第一次被线程获得锁的时候,发现是匿名偏向状态,则会用CAS指令,将`mark word`中的thread id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁。 + +case 2:当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后,会往当前线程的栈中添加一条`Displaced Mark Word`为空的`Lock Record`中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下,`synchronized`关键字带来的性能开销基本可以忽略。 + +case 3:当其他线程进入同步块时,发现已经有偏向的线程了,则会进入到**撤销偏向锁**的逻辑里,一般来说,会在`safepoint`中去查看偏向的线程是否还存活,如果存活且还在同步块中则将锁升级为轻量级锁,原偏向的线程继续拥有锁,当前线程则走入到锁升级的逻辑里;如果偏向的线程已经不存活或者不在同步块中,则将对象头的`mark word`改为无锁状态(unlocked),之后再升级为轻量级锁。 + +## 解锁过程 + +当有其他线程尝试获得锁时,是根据遍历偏向线程的`lock record`来确定该线程是否还在执行同步块中的代码。因此偏向锁的解锁很简单,仅仅将栈中的**最近一条**`lock record`的`obj`字段设置为null。偏向锁的解锁步骤中**并不会修改对象头中的thread id。** + + + +## 偏向锁升级时机 + +一般来说(批量重偏向除外),偏向锁升级的时机为:当锁已经发生偏向后,只要有另一个线程尝试获得偏向锁,则该偏向锁就会升级成轻量级锁。 + + + +## 偏向锁撤销过程 + +这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块时的过程。 + +> 撤销逻辑有很多,我们只分析最常见的情况:假设锁已经偏向线程A,这时B线程尝试获得锁。 + +1. 查看偏向的线程是否存活,如果已经不存活了,则直接撤销偏向锁。JVM维护了一个集合存放所有存活的线程,通过遍历该集合判断某个线程是否存活。 +2. 偏向的线程是否还在同步块中,如果不在了,则撤销偏向锁。我们回顾一下偏向锁的加锁流程:每次进入同步块(即执行`monitorenter`)的时候都会以从高往低的顺序在栈中找到第一个可用的`Lock Record`,将其obj字段指向锁对象。每次解锁(即执行`monitorexit`)的时候都会将最低的一个相关`Lock Record`移除掉。所以可以通过遍历线程栈中的`Lock Record`来判断线程是否还在同步块中。 +3. 将偏向线程所有相关`Lock Record`的`Displaced Mark Word`设置为null,然后将最高位的`Lock Record`的`Displaced Mark Word` 设置为无锁状态,最高位的`Lock Record`也就是第一次获得锁时的`Lock Record`(这里的第一次是指重入获取锁时的第一次),然后将对象头指向最高位的`Lock Record`,这里不需要用CAS指令,因为是在`safepoint`。 执行完后,就升级成了轻量级锁。原偏向线程的所有Lock Record都已经变成轻量级锁的状态。【轻量级锁加锁过程会在下文讲到,不要慌】 + + + +触发时机: +释放:对应的就是synchronized方法的退出或synchronized块的结束。 +撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候。 + + + +# synchronized轻量级锁原理(C++源码层面) + +## 加锁过程 + +1.在线程栈中创建一个`Lock Record`,将其`obj`(即Object reference)字段指向锁对象。 + +2.会把锁的Mark Word复制到自己的Lock Record的Displaced Mark Word里面。然后线程尝试直接通过CAS指令将`Lock Record`的地址存储在对象头的`mark word`中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。如果失败,进入到步骤3。 + +3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置`Lock Record`第一部分(`Displaced Mark Word`)为null,起到了一个重入计数器的作用。然后结束。 + +4.如果都失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,需要膨胀为重量级锁。【这就是轻量级锁升级为重量级锁的时机】 + + + +## 解锁过程 + +1.遍历线程栈,找到所有`obj`字段等于当前锁对象的`Lock Record`。 + +2.如果`Lock Record`的`Displaced Mark Word`为null,代表这是一次重入,将`obj`设置为null后continue。 + +3.如果`Lock Record`的`Displaced Mark Word`不为null,则利用CAS指令将对象头的`mark word`恢复成为`Displaced Mark Word`。如果成功,则continue,否则膨胀为重量级锁。 + + + +## 轻量级锁重入示例图 + +我们看个demo,在该demo中重复3次获得锁。 + +``` +synchronized(obj){ + synchronized(obj){ + synchronized(obj){ + } + } +} +``` + + + +## 轻量级锁什么时候升级为重量级锁? + +> 其实在加锁的时候已经说过了,这里再以一个具体场景说下 + +- 线程1获取轻量级锁时会把锁的Mark Word复制到自己的Lock Record的Displaced Mark Word里面。然后线程尝试直接通过CAS指令将`Lock Record`的地址存储在对象头的`mark word`中 + +- 如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2在CAS的时候,发现线程1已经把对象头换了,**线程2的CAS失败**。那么此时就代表发生了锁竞争,准备升级为重量级锁 + + + + + +## 轻量级锁CAS的问题 + +1、**结论:**没有自旋这回事,只有重量级锁获取失败才会自旋,网上的文章好多都是错的,我个人认为轻量级锁的意义就是在没有线程争用锁时不用创建monitor。**【源码得到的结论,实践才是硬道理】** + +2、**轻量级锁和偏向锁区别:**只要存在竞争就会升级重量级。**轻量级锁的存在就是用于线程之间交替获取锁的场景**,但是和偏向锁是有区别的啊。一个线程获取偏向锁之后,那么这个锁自然而然就属于这个线程(就算该线程释放了偏向锁也不会改变这把锁偏向这个线程的【也就是之前说的不会修改Thread ID】,这个前提是没有发生过批量重偏向使锁的epoch与其对应class类的epoch不相等)。所以说偏向锁的场景是用于一个线程不断的获取锁,如果把它放在轻量级锁的场景下线程之间交替获取的话会发生偏向锁的撤销的。也就是说在偏向锁的情况下,线程1之前释放了锁,线程2再获取锁,即使此时没有**同时锁竞争**的情况,依然是要升级为轻量级锁的。而轻量级锁只要没有同时去获取锁,就可以不升级为重量级锁,也就代表你可以不同线程交替获取这个锁。 +3、效率上来看偏向锁只有在获取的时候进行一次CAS,以后的释放和获取只需要简单的一些判断操作。而轻量级锁的获取和释放都要都要CAS,单纯的看效率还是偏向锁效率高。 + + + +# synchronized重量级锁原理(C++源码层面) + +> 重量级锁面试可能问的多,就多写了点C++代码 + +当出现多个线程**同时竞争**锁时,如果不是**同时竞争**,轻量级锁依然可以实现线程交替运行。 + + + +## Monitor监视器锁 + +- 重量级锁通过对象的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的互斥量(mutex) 实现的实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。这也是为什么重量级锁效率不高的原因。 + +- 重量级锁的状态下,对象的`mark word`为指向一个堆中monitor对象的指针。一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。 + + + +在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于HotSpot虚 拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor主 要数据结构如下: + +```C++ +ObjectMonitor() { + _header = NULL; + _count = 0; + _waiters = 0, + _recursions = 0; // 线程的重入次数 + _object = NULL; // 存储该monitor的对象 + _owner = NULL; // 标识拥有该monitor的线程 + _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet + _WaitSetLock = 0 ; + _Responsible = NULL; + _succ = NULL; + _cxq = NULL; // 多线程竞争锁时的单向列表 + FreeNext = NULL; + _EntryList = NULL; // 处于等待锁block状态的线程,会被加入到该列表 + _SpinFreq = 0; + _SpinClock = 0; + OwnerIsThread = 0; +} +``` + +> Contention List:所有请求锁的线程将被首先放置到该竞争队列 +> Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List +> Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set +> OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck +> Owner:获得锁的线程称为Owner +> !Owner:释放锁的线程 + +1、当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个`ObjectWaiter`对象插入到Contention List的队列的队首,然后调用`park`函数挂起当前线程。 + +2、当线程释放锁时,会从Contention List或EntryList中挑选一个线程唤醒,被选中的线程叫做`Heir presumptive`即假定继承人,假定继承人【Ready线程】被唤醒后会尝试获得锁,但`synchronized`是非公平的,所以假定继承人不一定能获得锁。这是因为对于重量级锁,线程先自旋尝试获得锁,这样做的目的是为了减少执行操作系统同步操作带来的开销。如果自旋不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平。 + +3、如果线程获得锁后调用`Object.wait`方法,则会将线程加入到WaitSet中,当被`Object.notify`唤醒后,会将线程从WaitSet移动到Contention List或EntryList中去。需要注意的是,当调用一个锁对象的`wait`或`notify`方法时,**如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁**。 + +4、**关于Contention List(cxq)和EntryList的区别:**cxq是单向链表,指的是如果已经有t1线程获取到monitor对象拿到锁后,t2和t3没有竞争到,t2、t3线程会进行到cxq队列,先自己尝试竞争锁,如果竞争不到则自旋再去挣扎一下获取锁,当t1执行完同步代码块,释放锁后,由t1、t2、t3再去争抢锁,如果t1再次抢到锁,那么t2、t3会进行到EntryList阻塞队列,如果此时又有t4、t5线程过来会被放到cxq队列,t2,t3,t4,t5,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当 前线程挂起,等待被唤醒。如果t1被释放, 根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过 ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作终由unpark完成,被唤醒的线程,继续执行monitor 的竞争。当获取锁的线程释放后,EntryList中的线程和WaitSet中的线程被唤醒都可能去获取锁变成owner的拥有者。 + + + +- 每一个Java对象都可以与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized圈起来的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象 对应的monitor。 我们的Java代码里不会显示地去创造这么一个monitor对象,我们也无需创建,事实上可以这么理解: monitor并不是随着对象创建而创建的。我们是通过synchronized修饰符告诉JVM需要为我们的某个对 象创建关联的monitor对象。每个线程都存在两个ObjectMonitor对象列表,分别为free和used列表。 同时JVM中也维护着global locklist。当线程需要ObjectMonitor对象时,首先从线程自身的free表中申请,若存在则使用,若不存在则从global list中分配一批`monitor`到free中。 + +- free对应C++代码:omFreeList +- global locklist对应C++代码:gFreeList + + + +## monitor竞争 + +1、执行monitorenter时,会调用InterpreterRuntime.cpp (位于:src/share/vm/interpreter/interpreterRuntime.cpp) 的 InterpreterRuntime::monitorenter函 数。具体代码可参见HotSpot源码。 + + + + + +2、对于重量级锁,monitorenter函数中会调用 ObjectSynchronizer::slow_enter 3.终调用 ObjectMonitor::enter(位于:src/share/vm/runtime/objectMonitor.cpp),源码如下: + + + +```C++ +void ATTR ObjectMonitor::enter(TRAPS) { + + Thread * const Self = THREAD ; + void * cur ; + // owner为null代表无锁状态,如果能CAS设置成功,则当前线程直接获得锁 + cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ; + if (cur == NULL) { + ... + return ; + } + // 如果是重入的情况,_recursions++ + if (cur == Self) { + // TODO-FIXME: check for integer overflow! BUGID 6557169. + _recursions ++ ; + return ; + } + /* + 1、当前线程是之前持有轻量级锁的线程。由轻量级锁膨胀且第一次调用enter方法,那cur是指向Lock Record的指针。 + */ + if (Self->is_lock_owned ((address)cur)) { + assert (_recursions == 0, "internal state error"); + // 重入计数重置为1 + _recursions = 1 ; + // 设置owner字段为当前线程(之前owner是指向Lock Record的指针) + _owner = Self ; + OwnerIsThread = 1 ; + return ; + } + + ... + + // 在调用系统的同步操作之前,先尝试自旋获得锁 + if (Knob_SpinEarly && TrySpin (Self) > 0) { + ... + //自旋的过程中获得了锁,则直接返回 + Self->_Stalled = 0 ; + return ; + } + + ... + + { + ... + + for (;;) { + jt->set_suspend_equivalent(); + // 在该方法中调用系统同步操作。也就是获得锁或阻塞 + EnterI (THREAD) ; + ... + } + Self->set_current_pending_monitor(NULL); + + } + + ... + +} +``` + + + +## Monitor等待或获取锁 + +```C++ +void ATTR ObjectMonitor::EnterI (TRAPS) { + Thread * Self = THREAD ; + ... + // 尝试获得锁 + if (TryLock (Self) > 0) { + ... + return ; + } + + DeferredInitialize () ; + + // 自旋 + if (TrySpin (Self) > 0) { + ... + return ; + } + + ... + + // 当前线程被封装成ObjectWaiter对象node + ObjectWaiter node(Self) ; + Self->_ParkEvent->reset() ; + node._prev = (ObjectWaiter *) 0xBAD ; + node.TState = ObjectWaiter::TS_CXQ ; + + // 通过CAS将node节点插入到_cxq队列的头部,cxq是一个单向链表 + ObjectWaiter * nxt ; + for (;;) { + node._next = nxt = _cxq ; + if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ; + + // CAS失败的话 再尝试获得锁,这样可以降低插入到_cxq队列的频率 + if (TryLock (Self) > 0) { + ... + return ; + } + } + + // SyncFlags默认为0,如果没有其他等待的线程,则将_Responsible设置为自己 + if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) { + Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ; + } + + + TEVENT (Inflated enter - Contention) ; + int nWakeups = 0 ; + int RecheckInterval = 1 ; + + for (;;) { + //线程在被挂起前再做一下挣扎,看能不能获取到锁 + if (TryLock (Self) > 0) break ; + assert (_owner != Self, "invariant") ; + + ... + + // park self + if (_Responsible == Self || (SyncFlags & 1)) { + // 当前线程是_Responsible时,调用的是带时间参数的park + TEVENT (Inflated enter - park TIMED) ; + Self->_ParkEvent->park ((jlong) RecheckInterval) ; + // Increase the RecheckInterval, but clamp the value. + RecheckInterval *= 8 ; + if (RecheckInterval > 1000) RecheckInterval = 1000 ; + } else { + //否则直接调用park挂起当前线程 + TEVENT (Inflated enter - park UNTIMED) ; + Self->_ParkEvent->park() ; + } + + if (TryLock(Self) > 0) break ; + + ... + + if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ; + + ... + // 在释放锁时,_succ会被设置为EntryList或_cxq中的一个线程 + if (_succ == Self) _succ = NULL ; + + // Invariant: after clearing _succ a thread *must* retry _owner before parking. + OrderAccess::fence() ; + } + + // 走到这里说明已经获得锁了 + + assert (_owner == Self , "invariant") ; + assert (object() != NULL , "invariant") ; + + // 将当前线程的node从cxq或EntryList中移除 + UnlinkAfterAcquire (Self, &node) ; + if (_succ == Self) _succ = NULL ; + if (_Responsible == Self) { + _Responsible = NULL ; + OrderAccess::fence(); + } + ... + return ; +} +``` + +当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁,TryLock方 法实现如下: + +```C++ +int ObjectMonitor::TryLock (Thread * Self) { + for (;;) { + void * own = _owner ; + if (own != NULL) return 0 ; + if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) { + // Either guarantee _recursions == 0 or set _recursions = 0. + assert (_recursions == 0, "invariant") ; + assert (_owner == Self, "invariant") ; + // CONSIDER: set or assert that OwnerIsThread == 1 + return 1 ; + } + // The lock had been free momentarily, but we lost the race to the lock. + // Interference -- the CAS failed. + // We can either return -1 or retry. + // Retry doesn't make as much sense because the lock was just acquired. + if (true) return -1 ; + } +} +``` + +以上代码的具体流程概括如下: + +1. 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ。 +2. 在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node 节点push到_cxq列表中。 +3. node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当 前线程挂起,等待被唤醒。 +4. 当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁。 + + + +## monitor释放 + +当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其它线程机会执行同步代码,在 HotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于 ObjectMonitor的exit方法中。(位于:src/share/vm/runtime/objectMonitor.cpp),源码如下所 示: + +````C++ +void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) { + Thread * Self = THREAD ; + // 如果_owner不是当前线程 + if (THREAD != _owner) { + // 当前线程是之前持有轻量级锁的线程。由轻量级锁膨胀后还没调用过enter方法,_owner会是指向Lock Record的指针。 + if (THREAD->is_lock_owned((address) _owner)) { + assert (_recursions == 0, "invariant") ; + _owner = THREAD ; + _recursions = 0 ; + OwnerIsThread = 1 ; + } else { + // 异常情况:当前不是持有锁的线程 + TEVENT (Exit - Throw IMSX) ; + assert(false, "Non-balanced monitor enter/exit!"); + if (false) { + THROW(vmSymbols::java_lang_IllegalMonitorStateException()); + } + return; + } + } + // 重入计数器还不为0,则计数器-1后返回 + if (_recursions != 0) { + _recursions--; // this is simple recursive enter + TEVENT (Inflated exit - recursive) ; + return ; + } + + // _Responsible设置为null + if ((SyncFlags & 4) == 0) { + _Responsible = NULL ; + } + + ... + + for (;;) { + assert (THREAD == _owner, "invariant") ; + + // Knob_ExitPolicy默认为0 + if (Knob_ExitPolicy == 0) { + // code 1:先释放锁,这时如果有其他线程进入同步块则能获得锁 + OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock + OrderAccess::storeload() ; // See if we need to wake a successor + // code 2:如果没有等待的线程或已经有假定继承人 + if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) { + TEVENT (Inflated exit - simple egress) ; + return ; + } + TEVENT (Inflated exit - complex egress) ; + + // code 3:要执行之后的操作需要重新获得锁,即设置_owner为当前线程 + if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) { + return ; + } + TEVENT (Exit - Reacquired) ; + } + ... + + ObjectWaiter * w = NULL ; + // code 4:根据QMode的不同会有不同的唤醒策略,默认为0 + int QMode = Knob_QMode ; + // QMode == 2 : cxq中的线程有更高优先级,直接绕过EntryList队列,唤醒cxq的队首线程 + if (QMode == 2 && _cxq != NULL) { + + w = _cxq ; + assert (w != NULL, "invariant") ; + assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ; + ExitEpilog (Self, w) ; + return ; + } + // QMode == 3 将cxq中的元素插入到EntryList的末尾 + if (QMode == 3 && _cxq != NULL) { + + w = _cxq ; + for (;;) { + assert (w != NULL, "Invariant") ; + ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; + if (u == w) break ; + w = u ; + } + assert (w != NULL , "invariant") ; + + ObjectWaiter * q = NULL ; + ObjectWaiter * p ; + for (p = w ; p != NULL ; p = p->_next) { + guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; + p->TState = ObjectWaiter::TS_ENTER ; + p->_prev = q ; + q = p ; + } + + // Append the RATs to the EntryList + // TODO: organize EntryList as a CDLL so we can locate the tail in constant-time. + ObjectWaiter * Tail ; + for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail = Tail->_next) ; + if (Tail == NULL) { + _EntryList = w ; + } else { + Tail->_next = w ; + w->_prev = Tail ; + } + + // Fall thru into code that tries to wake a successor from EntryList + } + // QMode == 4,将cxq插入到EntryList的队首 + if (QMode == 4 && _cxq != NULL) { + + w = _cxq ; + for (;;) { + assert (w != NULL, "Invariant") ; + ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; + if (u == w) break ; + w = u ; + } + assert (w != NULL , "invariant") ; + + ObjectWaiter * q = NULL ; + ObjectWaiter * p ; + for (p = w ; p != NULL ; p = p->_next) { + guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; + p->TState = ObjectWaiter::TS_ENTER ; + p->_prev = q ; + q = p ; + } + + // Prepend the RATs to the EntryList + if (_EntryList != NULL) { + q->_next = _EntryList ; + _EntryList->_prev = q ; + } + _EntryList = w ; + + // Fall thru into code that tries to wake a successor from EntryList + } + + w = _EntryList ; + if (w != NULL) { + // 如果EntryList不为空,则直接唤醒EntryList的队首元素 + assert (w->TState == ObjectWaiter::TS_ENTER, "invariant") ; + ExitEpilog (Self, w) ; + return ; + } + + // EntryList为null,则处理cxq中的元素 + w = _cxq ; + if (w == NULL) continue ; + + // 因为之后要将cxq的元素移动到EntryList,所以这里将cxq字段设置为null + for (;;) { + assert (w != NULL, "Invariant") ; + ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; + if (u == w) break ; + w = u ; + } + TEVENT (Inflated exit - drain cxq into EntryList) ; + + assert (w != NULL , "invariant") ; + assert (_EntryList == NULL , "invariant") ; + + + if (QMode == 1) { + // QMode == 1 : 将cxq中的元素转移到EntryList,并反转顺序 + ObjectWaiter * s = NULL ; + ObjectWaiter * t = w ; + ObjectWaiter * u = NULL ; + while (t != NULL) { + guarantee (t->TState == ObjectWaiter::TS_CXQ, "invariant") ; + t->TState = ObjectWaiter::TS_ENTER ; + u = t->_next ; + t->_prev = u ; + t->_next = s ; + s = t; + t = u ; + } + _EntryList = s ; + assert (s != NULL, "invariant") ; + } else { + // QMode == 0 or QMode == 2‘ + // 将cxq中的元素转移到EntryList + _EntryList = w ; + ObjectWaiter * q = NULL ; + ObjectWaiter * p ; + for (p = w ; p != NULL ; p = p->_next) { + guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; + p->TState = ObjectWaiter::TS_ENTER ; + p->_prev = q ; + q = p ; + } + } + + + // _succ不为null,说明已经有个继承人了,所以不需要当前线程去唤醒,减少上下文切换的比率 + if (_succ != NULL) continue; + + w = _EntryList ; + // 唤醒EntryList第一个元素 + if (w != NULL) { + guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ; + ExitEpilog (Self, w) ; + return ; + } + } +} +```` + +在进行必要的锁重入判断以及自旋优化后,进入到主要逻辑: + +`code 1` 设置owner为null,即释放锁,这个时刻其他的线程能获取到锁。这里是一个非公平锁的优化; + +`code 2` 如果当前没有等待的线程则直接返回就好了,因为不需要唤醒其他线程。或者如果说succ不为null,代表当前已经有个"醒着的"继承人线程,那当前线程不需要唤醒任何线程; + +`code 3` 当前线程重新获得锁,因为之后要操作cxq和EntryList队列以及唤醒线程; + +`code 4`根据QMode的不同,会执行不同的唤醒策略; + +根据QMode的不同,有不同的处理方式: + +1. QMode = 2且cxq非空:取cxq队列队首的ObjectWaiter对象,调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回,后面的代码不会执行了; +2. QMode = 3且cxq非空:把cxq队列插入到EntryList的尾部; +3. QMode = 4且cxq非空:把cxq队列插入到EntryList的头部; +4. QMode = 0:暂时什么都不做,继续往下看; + +只有QMode=2的时候会提前返回,等于0、3、4的时候都会继续往下执行: + +1.如果EntryList的首元素非空,就取出来调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回; +2.如果EntryList的首元素为空,就将cxq的所有元素放入到EntryList中,然后再从EntryList中取出来队首元素执行ExitEpilog方法,然后立即返回; + + + +1. 退出同步代码块时会让_recursions减1,当_recursions的值减为0时,说明线程释放了锁。 +2. 根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过 ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作终由unpark完成,实现 如下: + +```Java +void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) { + assert (_owner == Self, "invariant") ; + _succ = Knob_SuccEnabled ? Wakee->_thread : NULL ; + P + arkEvent * Trigger = Wakee->_event ; + Wakee = NULL ; +// Drop the lock + OrderAccess::release_store_ptr (&_owner, NULL) ; + OrderAccess::fence() ; // ST _owner vs LD in unpark() + if (SafepointSynchronize::do_call_back()) { + TEVENT (unpark before SAFEPOINT) ; + } + DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self); + Trigger->unpark() ; // 唤醒之前被pack()挂起的线程. +// Maintain stats and report events to JVMTI + if (ObjectMonitor::_sync_Parks != NULL) { + ObjectMonitor::_sync_Parks->inc() ; + } +} +``` + + + +被唤醒的线程,会回到 void ATTR ObjectMonitor::EnterI (TRAPS) 的第600行,继续执行monitor 的竞争。 + +```C++ +// park self +if (_Responsible == Self || (SyncFlags & 1)) { + TEVENT (Inflated enter - park TIMED) ; + Self->_ParkEvent->park ((jlong) RecheckInterval) ; +// Increase the RecheckInterval, but clamp the value. + RecheckInterval *= 8 ; + if (RecheckInterval > 1000) RecheckInterval = 1000 ; +} else { + TEVENT (Inflated enter - park UNTIMED) ; + Self->_ParkEvent->park() ; +} +if (TryLock(Self) > 0) break ; +``` + +## monitor是重量级锁 + +可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数, 执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就 会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语 言中是一个重量级(Heavyweight)的操作。 用户态和和内核态是什么东西呢?要想了解用户态和内核态还需要先了解一下Linux系统的体系架构: + + + +从上图可以看出,Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。 内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。 用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存 储资源、I/O资源等。 系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。 + +所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);但是当它调用系统调用执 行某些操作时,例如 I/O调用,此时需要陷入内核中运行,我们就称进程处于内核运行态(或简称为内 核态)。 系统调用的过程可以简单理解为: + +1. 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈, 以此表明需要操作系统提供的服务。 +2. 用户态程序执行系统调用。 +3. CPU切换到内核态,并跳到位于内存指定位置的指令。 +4. 系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。 +5. 系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。 由此可见用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在 synchronized未优化之前,效率低的原因。 + + + +# 锁降级的争论 + +1、先说结论,**在openjdk的hotsopt jdk8u里是有锁降级的机制的**,锁降级是什么时候加入到hotspot的这个我没去关注,所以我只说看过代码的jdk8u版本,另外根据R大的这个[回答](https://www.zhihu.com/question/19882320),我相信sunj dk也一样。 + +2、然后再详细说: + +- 锁降级的代码在[`deflate_idle_monitors`](http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/runtime/synchronizer.cpp#l1503)方法中,其调用点在进入SafePoint的方法`SafepointSynchronize::begin()`中。 + 在`deflate_idle_monitors`中会找到已经`idle`的monitor(也就是重量级锁的对象),然后调用`deflate_monitor`方法将其降级。 + +- 因为锁降级是发生在safepoint的,所以如果降级时间过长会导致程序一直处于STW的阶段。在[这里](http://openjdk.java.net/jeps/8183909)有篇文章讨论了优化机制。jdk8中本身也有个`MonitorInUseLists`的开关,其影响了寻找`idle monitor`的方式,对该开关的一些讨论看[这里](https://bugs.openjdk.java.net/browse/JDK-8149442)。 + +- 至于为什么《java并发编程的艺术》中说锁不能降级,我**猜测**可能该书作者看的jdk版本还没有引入降级机制。 + + + + + +# 细节/容易混淆的地方 + +## java语言规范 + +1、java语言规范里面,`int i = 0,resource = loadedResoures,flag = true`,各种变量的简单的赋值操作,规定都是原子的包括引用类型的变量的赋值写操作,也是原子的。 + +2、但是很多复杂的一些操作,i++,先读取i的值,再跟新i的值,i = y + 2,先读取y的值,再更新i的值,这种复杂操作,不是简单赋值写,他是有计算的过程在里面的,此时java语言规范默认是不保证原子性的。 + +## 32位Java虚拟机中的long和double变量写操作为何不是原子的? + +原子性这块,特例,32位虚拟机里的long/double类型的变量的简单赋值写操作,不是原子的,long i = 30,double c = 45.0,在32位虚拟机里就不是原子的,因为long和double是64位的 + + ``` +0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 + ``` + +如果多个线程同时并发的执行long i = 30,long是64位的,就会导致有的线程在修改i的高32位,有的线程在修改i的低32位,多线程并发给long类型的变量进行赋值操作,在32位的虚拟机下,是有问题的 + + 就可能会导致多线程给long i = 30赋值之后,导致i的值不是30,可能是-3333344429,乱码一样的数字,就是因为高低32位赋值错了,就导致二进制数字转换为十进制之后是一个很奇怪的数字 + +## volatile保不保证原子性? + +1、volatile对原子性保障的语义,在java里很有限的,几乎可以忽略不计。32位的java虚拟机里面,对long/double变量的赋值写是不原子的,此时如果对变量加上了volatile,就可以保证在32位java虚拟机里面,对long/double变量的赋值写是原子的了。(这是一个特列,可以通过volatile来保证原子性)但总体来说volatiel不保证原子性 + +例子: + +`volatile long i;` 多个线程执行:`i = 30`,此时就不要紧了,因为volatile修饰了,就可以保证这个赋值操作是原子的了。 + +2、`int i = 0`,这种原子性的保证,不是靠volatile,java语言规范本身就规定了这种操作是原子性的。 + + + +**结论:volatile不保证原子性** + + + + + + + + + + + + + +# wait和notify + +## 为什么要出现咋同步代码块中 + +> 通过前面对monitor的C++源码讲解,答案应该很明显了。 + +1、如果一个线程在同步块中调用了`Object#wait`方法,会将该线程对应的ObjectWaiter从EntryList移除并加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的ObjectWaiter从WaitSet移动到EntryList中。 + +2、注意,如果没有获取到监视器锁,wait 方法是会抛异常的,而且注意这个异常是IllegalMonitorStateException 异常。这是重要知识点,要考。 + +3、如果线程获得锁后调用`Object.wait`方法,则会将线程加入到WaitSet中。而WaitSet这个结构是在Monitor对象里,如果你没有获取到监视器锁,你就没有Monitor。那就无法加入到WaitSet里。 + + + + + + + +# 参考: + + + +- 《Java并发编程的艺术》 + +- 《Java并发编程之美》 + +- b站一些机构的视频 + +- https://www.cnblogs.com/yanlong300/p/8986041.html + +- https://github.com/farmerjohngit/myblog + +- 公总号:儒猿技术窝 + + + + + +# 题外话: + +1、《Java并发编程的艺术》这本书非常好。不过我第一遍看的时候,就属于云里雾里的,就是感觉抓不到重点。直到我秋招结束之后,再次回顾的时候才开始有点感觉了。这本书从硬件层面,从设计层面讲的一些内容讲的很好,给了我很大的帮助。这本书结合《Java并发编程之美》可能会有更好的阅读体验和理解(仅仅是个人觉得)。读者有时间还是尽量要看下这两本书,我的博客只是总结了面试经常会问到的一些内容,以及一些很难找到的一些资料。 + +2、相信大家也能明显的感觉到写这篇博客的时候,涉及到了很多**操作系统**的知识。其实越往后学,你就越能发现**操作系统**对你理解很多东西的原理会有很大帮助。比如说这篇博客的内存屏障,MESI缓存一致性协议,cpu指令;还有netty的一些通信原理,还有mysql,redis与操作系统的交互,rokcetmq的零拷贝,消息储存机制等等。希望读者有时间能够好好看一下**计算机网络和操作系统**,真的是很重要,并且校招和社招面试大厂问的都比较多。 + +3、因为东西写的比较多,略微有点乱,写了3万多字。所以可能目录顺序不是那么的好,敬请读者见谅。 + +4、感谢各个博主的博客,对我都有很大的帮助。笔者能力有限,总结的博客可能有错误。如果有错误请及时联系我,谢谢~。 + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..5ea27bb --- /dev/null +++ b/index.html @@ -0,0 +1,63 @@ + + + + + + JavaYouth + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + \ No newline at end of file