[{"content":"互联网协议 五层协议 五层协议 应用层(application layer) 传输层(transport layer) 网络层(network layer) 链接层(link layer) 实体层(physical layer) 越往下越涉及(物理)底层,越往上越对应用户。\n层与协议 每一层都是为了完成一种功能。为了实现这些功能,就需要大家都遵守共同的规则。\n大家都遵守的规则,就叫做\u0026quot;协议\u0026quot;(protocol)。\n实体层 实体层的功能是把电脑连接起来的物理手段。它主要规定了网络的一些电气特性,作用是负责传送0和1的电信号。\n用到的方式有:光缆、电缆、双绞线、无线电波等。\n链接层 单纯的0和1是没有意义的,如何组装,解析电信号是链接层所解决的问题。 这就是\u0026quot;链接层\u0026quot;的功能,它在\u0026quot;实体层\u0026quot;的上方,确定了0和1的分组方式。\n以太网协议(链接层的实现协议)\n以太网协议是实现了电信号分组方式,并被当前互联网所采纳。\n以太网规定,一组电信号构成一个数据包,叫做\u0026quot;帧\u0026quot;(frame)。每一帧分成两个部分:标头(head)和数据(data)。\n标头 数据 head data \u0026ldquo;标头\u0026quot;包含数据包的一些说明项,比如发送者、接受者、数据类型等等;\u0026ldquo;数据\u0026quot;则是数据包的具体内容。\n\u0026ldquo;标头\u0026quot;的长度,固定为18字节。\u0026ldquo;数据\u0026quot;的长度,最短为46字节,最长为1500字节。因此,整个\u0026quot;帧\u0026quot;最短为64字节,最长为1518字节。如果数据很长,就必须分割成多个帧进行发送。\nmac地址\n以太网数据包的\u0026quot;标头\u0026rdquo;,包含了发送者和接受者的信息。那么,发送者和接受者是如何标识呢? 以太网规定,连入网络的所有设备,都必须具有\u0026quot;网卡\u0026quot;接口。数据包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫做mac地址。\n可以理解为mac地址为每台计算机的身份证号,唯一性。\n广播\n一块网卡怎么会知道另一块网卡的mac地址? 回答是有一种arp协议,可以解决这个问题。这里我们知道以太网数据包必须知道接收方的mac地址,然后才能发送。\n其次,就算有了mac地址,系统怎样才能把数据包准确送到接收方? 回答是以太网采用了一种很\u0026quot;原始\u0026quot;的方式,它不是把数据包准确送到接收方,而是向本网络内所有计算机发送,让每台计算机自己判断,是否为接收方。\n举例:1号计算机向2号计算机发送一个数据包,同一个子网络的3号、4号、5号计算机都会收到这个包。接收方读取这个包的\u0026quot;标头\u0026rdquo;,找到接收方发送连接的mac地址,然后与自身的mac地址相比较,如果两者相同,就接受这个包,做进一步处理,否则就丢弃这个包。这种发送方式就叫做\u0026quot;广播\u0026rdquo;(broadcasting)。\n网络层 以太网只是做到了同一个网段内的数据发送,那么如果我想在上海发送数据到洛阳,那么这两个地方肯定不是在同一个网段内,通过以太网的广播肯定是无法传输的。 因此,必须找到一种方法,能够区分哪些mac地址属于同一个子网络,哪些不是。如果是同一个子网络,就采用广播方式发送,否则就采用\u0026quot;路由\u0026quot;方式发送。\n这就导致了\u0026quot;网络层\u0026quot;的诞生。它的作用是引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做\u0026quot;网络地址\u0026rdquo;,简称\u0026quot;网址\u0026quot;。\n于是,\u0026ldquo;网络层\u0026quot;出现以后,每台计算机有了两种地址,一种是mac地址,另一种是网络地址。两种地址之间没有任何联系,mac地址是绑定在网卡上的,是唯一的,而网络地址则是管理员分配的,它们只是随机组合在一起,在同一网段是唯一的,但是在不同网段是可以重复的。\n网络地址帮助我们确定计算机所在的子网络,mac地址则将数据包送到该子网络中的目标网卡。因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理mac地址。\nip协议(网络层的实现协议)\n互联网上的每一台计算机,都会分配到一个ip地址。这个地址分成两个部分,前一部分代表网络,后一部分代表主机。\n那么,怎样才能从ip地址,判断两台计算机是否属于同一个子网络呢? 这就要用到另一个参数\u0026quot;子网掩码\u0026rdquo;(subnet mask)。\n比如,ip地址172.16.254.1,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000\n总结一下,ip协议的作用主要有两个,一个是为每一台计算机分配ip地址,另一个是确定哪些地址在同一个子网络。\narp协议\n因为ip数据包是放在以太网数据包里发送的,所以我们必须同时知道两个地址,一个是对方的mac地址,另一个是对方的ip地址。通常情况下,对方的ip地址是已知的(后文会解释),但是我们不知道它的mac地址。所以,我们需要一种机制,能够从ip地址得到mac地址。 这里又可以分成两种情况。\n第一种情况,如果两台主机不在同一个子网络,那么事实上没有办法得到对方的mac地址,只能把数据包传送到两个子网络连接处的\u0026quot;网关\u0026quot;(gateway),让网关去处理。\n第二种情况,如果两台主机在同一个子网络,那么我们可以用arp协议,得到对方的mac地址。arp协议也是发出一个数据包(包含在以太网数据包中),其中包含它所要查询主机的ip地址,在对方的mac地址这一栏,填的是ff:ff:ff:ff:ff:ff,表示这是一个\u0026quot;广播\u0026quot;地址。它所在子网络的每一台主机,都会收到这个数据包,从中取出ip地址,与自身的ip地址进行比较。如果两者相同,都做出回复,向对方报告自己的mac地址,否则就丢弃这个包。\n链接层协议,只能获取到同一网段的子网mac地址,如果存在其他网段的子网只能通过网关的mac地址进行间接获取。您只能获取到网关的 mac 地址,只有网关有目标设备的 mac 地址。\n传输层 有了mac地址和ip地址,我们已经可以在互联网上任意两台主机上建立通信。\n接下来的问题是,同一台主机上有许多程序都需要用到网络,比如,你一边浏览网页,一边与朋友在线聊天。当一个数据包从互联网上发来的时候,你怎么知道,它是表示网页的内容,还是表示在线聊天的内容? \u0026ldquo;传输层\u0026quot;的功能,就是建立\u0026quot;端口到端口\u0026quot;的通信。实现程序之间的交流。 根据端口,保证发送的数据包是指定给对方的哪个程序。\nudp协议(传输层的实现协议)\n现在,我们必须在数据包中加入端口信息,这就需要新的协议。最简单的实现叫做udp协议,它的格式几乎就是在数据前面,加上端口号。\ntcp协议(更安全的实现协议)\nudp协议的优点是比较简单,容易实现,但是缺点是可靠性较差,一旦数据包发出,无法知道对方是否收到。 为了解决这个问题,提高网络可靠性,tcp协议就诞生了。这个协议非常复杂,但可以近似认为,它就是有确认机制的udp协议,每发出一个数据包都要求确认。如果有一个数据包遗失,就收不到确认,发出方就知道有必要重发这个数据包了。\n因此,tcp协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。\n应用层 应用程序收到\u0026quot;传输层\u0026quot;的数据,接下来就要进行解读。由于互联网是开放架构,数据来源五花八门,必须事先规定好格式,否则根本无法解读。 \u0026ldquo;应用层\u0026quot;的作用,就是规定应用程序的数据格式。\n举例来说,tcp协议可以为各种各样的程序传递数据,比如email、www、ftp等等。那么,必须有不同协议规定电子邮件、网页、ftp数据的格式,这些应用程序协议就构成了\u0026quot;应用层\u0026rdquo;。\n总结 网络通信就是交换数据包。电脑a向电脑b发送一个数据包,后者收到了,回复一个数据包,从而实现两台电脑之间的通信。\n发送这个包,需要知道两个地址:\n对方的mac地址 对方的ip地址 有了这两个地址,数据包才能准确送到接收者手中。但是,前面说过,mac地址有局限性,如果两台电脑不在同一个子网络,就无法知道对方的mac地址,必须通过网关(gateway)转发。\n数据包的目标地址,实际上分成两种情况:\n场景 数据包地址 同一个子网络 对方的mac地址,对方的ip地址 非同一个子网络 网关的mac地址,对方的ip地址 发送数据包之前,电脑必须判断对方是否在同一个子网络,然后选择相应的mac地址。\n系统总结 ","date":"2024-11-18","permalink":"https://www.holatto.com/posts/computer-network/","summary":"互联网协议 五层协议 五层协议 应用层(Application Layer) 传输层(Transport Layer) 网络层(Network Layer) 链接层(Link La","title":"computer network"},{"content":"rabbitmq-review 工作模型 名词解释以及作用\nbroker(代理):存储,发放转发消息;\nchannel(信道):在rabbitmq中,tcp连接通常是不关闭的。\n不关闭的原因是 建立和关闭 tcp 连接是有成本的,尤其是当需要频繁发送和接收消息时。如果每次消息传递都要创建和关闭 tcp 连接,性能会受到很大影响。因此,保持长时间的 tcp 连接可以减少这种开销。\n那么引入channel的作用是它提供了轻量级的虚拟连接,能够在一个 tcp 连接上实现并发操作、隔离消息等操作的独立性和灵活性。\n在java中调用发送和接收消息的方法中,都是建立在channel上。\nqueue(消息队列):业务数据存储在mq的数据库中,mnesia。\nexchange(交换机):生产者成产消息不直接到queue中,而是先到exchange中,exchange进行消息的转发。交换机有多种转发方式。\nfanout:广播,将消息交给所有绑定到交换机的队列 direct:定向,把消息交给符合指定routing key 的队列 topic:通配符,把消息交给符合routing pattern(路由模式) 的队列 vhost(虚拟主机):vhost 在 rabbitmq 中主要用于提供资源隔离、权限管理、多租户支持以及环境隔离的功能。 类似nacos中的namespace。\n死信交换机 死信交换机作为一个兜底方案。\n我们可以给simple.queue添加一个死信交换机,给死信交换机绑定一个队列。这样消息变成死信后也不会丢弃,而是最终投递到死信交换机,路由到与死信交换机绑定的队列。\n// 声明普通的 simple.queue队列,并且为其指定死信交换机:dl.direct @bean public queue simplequeue2(){ return queuebuilder.durable(\u0026#34;simple.queue\u0026#34;) // 指定队列名称,并持久化 .deadletterexchange(\u0026#34;dl.direct\u0026#34;) // 指定死信交换机 .build(); } // 声明死信交换机 dl.direct @bean public directexchange dlexchange(){ return new directexchange(\u0026#34;dl.direct\u0026#34;, true, false); } // 声明存储死信的队列 dl.queue @bean public queue dlqueue(){ return new queue(\u0026#34;dl.queue\u0026#34;, true); } // 将死信队列 与 死信交换机绑定 @bean public binding dlbinding(){ return bindingbuilder.bind(dlqueue()).to(dlexchange()).with(\u0026#34;simple\u0026#34;); } 什么样的消息会成为死信?\n消息被消费者reject或者返回nack 消息超时未消费 队列满了 ttl 使用死信交换机实现延迟队列。\n设置延迟时间的方式:\n消息所在的队列设置了超时时间 消息本身设置了超时时间 上图为消息本事设置了延迟发送时间。\n在consumer服务的springrabbitlistener中,定义一个新的消费者,并且声明 死信交换机、死信队列: @rabbitlistener(bindings = @queuebinding( value = @queue(name = \u0026#34;dl.ttl.queue\u0026#34;, durable = \u0026#34;true\u0026#34;), exchange = @exchange(name = \u0026#34;dl.ttl.direct\u0026#34;), key = \u0026#34;ttl\u0026#34; )) public void listendlqueue(string msg){ log.info(\u0026#34;接收到 dl.ttl.queue的延迟消息:{}\u0026#34;, msg); } spring 会自动创建队列、交换机及其绑定关系,无需再通过 @bean 显式声明。\n要给队列设置超时时间,需要在声明队列时配置x-message-ttl属性: @bean public queue ttlqueue(){ return queuebuilder.durable(\u0026#34;ttl.queue\u0026#34;) // 指定队列名称,并持久化 .ttl(10000) // 设置队列的超时时间,10秒 .deadletterexchange(\u0026#34;dl.ttl.direct\u0026#34;) // 指定死信交换机 .build(); } 注意,这个队列设定了死信交换机为dl.ttl.direct\n声明交换机,将ttl与交换机绑定: @bean public directexchange ttlexchange(){ return new directexchange(\u0026#34;ttl.direct\u0026#34;); } @bean public binding ttlbinding(){ return bindingbuilder.bind(ttlqueue()).to(ttlexchange()).with(\u0026#34;ttl\u0026#34;); } 延迟队列(插件) 因为延迟队列的需求非常多,所以rabbitmq的官方也推出了一个插件,原生支持延迟队列效果。\ndelayexchange原理\ndelayexchange需要将一个交换机声明为delayed类型。当我们发送消息到delayexchange时,流程如下:\n接收消息 判断消息是否具备x-delay属性 如果有x-delay属性,说明是延迟消息,持久化到硬盘,读取x-delay值,作为延迟时间 返回routing not found结果给消息发送者 x-delay时间到期后,重新投递消息到指定队列 使用delayexchange\n基于注解方式:\n延迟队列插件的使用步骤包括哪些?\n声明一个交换机,添加delayed属性为true\n发送消息时,添加x-delay头,值为超时时间\n惰性队列 消息堆积问题\n当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是消息堆积问题。\n惰性队列的特征如下:\n接收到消息后直接存入磁盘而非内存 消费者要消费消息时才会从磁盘中读取并加载到内存 支持数百万条的消息存储 初始化队列时声明\n@bean\n@rabbitlistener\n消息堆积问题的解决方案?\n队列上绑定多个消费者,提高消费速度 使用惰性队列,可以再mq中保存更多消息 惰性队列的优点有哪些?\n基于磁盘存储,消息上限高 没有间歇性的page-out,性能比较稳定 惰性队列的缺点有哪些?\n基于磁盘存储,消息时效性会降低 性能受限于磁盘的io 提出问题 ①在java库中也有类似queue的实现,为什么还有再使用第三方组件rabbitmq等消息中间件呢?\n类似于map集合同样可以实现缓存的效果,但是为什么要使用redis呢。因为在java层实现的缓存只能适用于这一个服务进程。现在大部分系统架构都是分布式架构,而redis,rabbitmq这种组件可以适用于分布式跨进程的环境中,并且这些组件有更多其他的功能,比如持久化。\n","date":"2024-09-21","permalink":"https://www.holatto.com/posts/mq/rabbit-mq-review/","summary":"RabbitMQ-Review 工作模型 名词解释以及作用 Broker(代理):存储,发放转发消息; Channel(信道):在RabbitMQ中,TCP连接通常是不关闭的。 不关闭的原因是 建立和","title":"rabbit mq review"},{"content":"https ssl\\tls协议 问题\nhttps存在的意义是什么? 运行的基本过程是什么? 如何建立安全连接? http通讯的风险 (1) 窃听风险(eavesdropping):第三方可以获知通信内容。\n(2) 篡改风险(tampering):第三方可以修改通信内容。\n(3) 冒充风险(pretending):第三方可以冒充他人身份参与通信。\nhttps的诞生 (1) 所有信息都是加密传播,第三方无法窃听。\n(2) 具有校验机制,一旦被篡改,通信双方会立刻发现。\n(3) 配备身份证书,防止身份被冒充。\nhttps(hypertext transfer protocol secure)是 http(hypertext transfer protocol)的安全版本,旨在为网络通信提供更高的安全性。\n运行过程 ssl/tls协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。\n(1) 客户端向服务器端索要并验证公钥。\n(2) 双方协商生成\u0026quot;对话密钥\u0026quot;。\n(3) 双方采用\u0026quot;对话密钥\u0026quot;进行加密通信。\n\u0026ldquo;对话密钥\u0026quot;进行加密信息主体,而服务器公钥只用于加密\u0026quot;对话密钥\u0026quot;本身。减少了加密运算的消耗时间。\n握手建立连接 \u0026ldquo;握手阶段\u0026quot;涉及四次通信\n①客户端发出请求(clienthello)\n在这一步,客户端主要向服务器提供以下信息。\n(1) 支持的协议版本,比如tls 1.0版。\n(2) 一个客户端生成的随机数,稍后用于生成\u0026quot;对话密钥\u0026rdquo;。\n(3) 支持的加密方法,比如rsa公钥加密。\n(4) 支持的压缩方法。\n②服务器回应(severhello)\n服务器收到客户端请求后,向客户端发出回应,这叫做severhello。服务器的回应包含以下内容。\n(1) 确认使用的加密通信协议版本,比如tls 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。\n(2) 一个服务器生成的随机数,稍后用于生成\u0026quot;对话密钥\u0026rdquo;。\n(3) 确认使用的加密方法,比如rsa公钥加密。\n(4) 服务器证书。\n③客户端回应\n客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。\n比如有时访问页面时出现的警告:是否继续访问此网站,本次连接不是一个安全的连接。意思就是https已经失效,现在是一个http不安全连接。\n如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息。\n(1) 一个随机数。该随机数用服务器公钥加密,防止被窃听。\n(2) 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。\n(3) 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。\n上面第一项的随机数,是整个握手阶段出现的第三个随机数,又称\u0026quot;pre-master key\u0026quot;。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把\u0026quot;会话密钥\u0026quot;。\n④服务器的最后回应\n服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的\u0026quot;会话密钥\u0026quot;。然后,向客户端最后发送下面信息。\n(1)编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。\n(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。\n至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的http协议,只不过用\u0026quot;会话密钥\u0026quot;加密内容。\n总结 客户端和服务端的共享变量就是这三个随机数,双方根据约定好的加密方法进行\u0026quot;对话密钥\u0026ldquo;生成,客户端通过对话密钥进行数据的加密操作生成密文,然后根据从服务端获取的公钥对密文进行再次加密,通过http请求发送至服务端;服务端根据私钥解密密文,再根据对话密钥解密,获取最终的真实数据。\nhttp协议比作两个人在炸鸡兄弟的餐馆中谈论着,这家老板是卖ice bule的,并且与墨西哥黑帮有合作;\nhttps协议比作这两个人用高等瓦雷利亚语谈论着这件事,没人能听懂他俩在说什么,但是不能保证每个人都听不懂,如果有人听懂并且告诉了炸鸡叔和汉克,那这就是信息泄露。\n系统总结 ","date":"2024-08-29","permalink":"https://www.holatto.com/posts/https/","summary":"HTTPS SSL\\TLS协议 问题 https存在的意义是什么? 运行的基本过程是什么? 如何建立安全连接? http通讯的风险 (1) 窃听风险(eavesdropping):第三","title":"https"},{"content":"多线程数组问题 背景 一个统计查询的接口,几十个机构查询统计不同类型案件的数量,涉及到查询数据库表操作,每个机构都会有许多次查表操作。那么几十个机构循环逐个查询的效率极低,性能差。当数据量稍微多点就会出现502超时错误。如何解决这个问题呢?\n解决方案 案件对应的机构是唯一的,并且统计的结果中,相互直接不会有影响。那么就可以使用多线程的方式,分批处理机构,而不用单线程低效循环。\n开始使用 // 代码待定 出现问题 出现数据被覆盖问题,即计算后总机构与原机构不同 出现空指针异常错误 出现这两个问题的原因就是未使用线程安全的集合。共享变量未保证一致性,线程安全集合即可解决这两个问题。那么具体分析这两个问题出现的原因。\n覆盖问题\n覆盖比较好理解,线程a占用索引一进行数据插入,在线程a未完全插入到集合的位置a时,线程b发现索引一未插入数据,则也开始在索引一插入数据,这时线程a完成了索引一位置的数据插入操作。线程b则也在索引一位置插入数据,导致线程a的数据被覆盖。\n空指针问题\n这里在使用list的add()方法时,会出现中间索引位置为null的情况,那么怎么出现这个问题呢?个人理解,这里涉及到集合中游标的概念。当一个集合在遍历的时候,集合如何按照索引进行逐个获取集合的元素,游标在这里扮演重要作用,初始化集合游标指向索引0的位置,当遍历时,cursor++的操作,移动游标的位置,查询获取每个索引对应的值。在add()时,元素会先插入到当前游标指向的索引位置,然后再移动游标的位置,即往后移动一位。\n在多线程的情况下,如果线程a在游标指向的索引为0的位置上插入数据前,线程b移动了游标的位置,这时,在线程a插入数据时就会把数据插入到索引为1的位置上,导致索引为0的位置无元素插入,即null。\n这么理解,索引比作一个一个的房间,每个房间都有具体的房间号,元素比作每个人。房间是固定的,也就是索引是固定不变的,而每个人怎么进入房间,就是游标要做的事情,游标指向哪个房间,人就该去哪个房间。总之,元素是跟随游标,指向索引位置。\n总结 这两个问题最根本的原因是共享变量未保证一致性。\n覆盖问题是在集合的索引这个共享变量出现竞争。\n空指针问题是在集合的游标这个共享变量上出现竞争。\n","date":"2024-08-01","permalink":"https://www.holatto.com/posts/juc/daily-study-01/","summary":"多线程数组问题 背景 一个统计查询的接口,几十个机构查询统计不同类型案件的数量,涉及到查询数据库表操作,每个机构都会有许多次查表操作。那么几十个机构循环逐个查询的效","title":"daily study array cursor"},{"content":"初学oauth 2.0 介绍:oauth是一个关于授权(authorization)的开放网络标准。\n参考理解oauth 2.0\n背景 oauth的适用场合,提出问题\n有一个\u0026quot;云冲印\u0026quot;的网站,可以将用户储存在google的照片,冲印出来。用户为了使用该服务,必须让\u0026quot;云冲印\u0026quot;读取自己储存在google上的照片。\n传统方法是,用户将自己的google用户名和密码,告诉\u0026quot;云冲印\u0026quot;,后者就可以读取用户的照片了。这样的做法有以下几个严重的缺点。\n(1)\u0026ldquo;云冲印\u0026quot;为了后续的服务,会保存用户的密码,这样很不安全。\n(2)google不得不部署密码登录,而我们知道,单纯的密码登录并不安全。\n(3)\u0026ldquo;云冲印\u0026quot;拥有了获取用户储存在google所有资料的权力,用户没法限制\u0026quot;云冲印\u0026quot;获得授权的范围和有效期。\n(4)用户只有修改密码,才能收回赋予\u0026quot;云冲印\u0026quot;的权力。但是这样做,会使得其他所有获得用户授权的第三方应用程序全部失效。\n(5)只要有一个第三方应用程序被破解,就会导致用户密码泄漏,以及所有被密码保护的数据泄漏。\noauth就是为了解决上面这些问题而诞生的。\n专有名词 (1) third-party application:第三方应用程序,本文中又称\u0026quot;客户端\u0026rdquo;(client),即上一节例子中的\u0026quot;云冲印\u0026rdquo;。\n(2)http service:http服务提供商,本文中简称\u0026quot;服务提供商\u0026quot;,即上一节例子中的google。\n(3)resource owner:资源所有者,本文中又称\u0026quot;用户\u0026quot;(user)。\n(4)user agent:用户代理,本文中就是指浏览器。\n(5)authorization server:认证服务器,即服务提供商专门用来处理认证的服务器。\n(6)resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。\noauth的作用就是让\u0026quot;客户端\u0026quot;安全可控地获取\u0026quot;用户\u0026quot;的授权,与\u0026quot;服务商提供商\u0026quot;进行互动。\noauth的理解 客户端无法直接访问服务提供商,需要通过认证授权服务器进行用户认证授权,成功认证后客户端即可访问服务提供商。\n其中几个关键几点:\n传统方法是用户将用户名密码交给客户端,让客户端进行登录访问服务提供商;但对于oauth方法即用户直接访问服务提供商,实现客户端与用户的分离。\n用户登录成功后生成token作为用户成功登录的标识发送给客户端,服务提供商根据token的权限范围和有效期,向\u0026quot;客户端\u0026quot;开放用户储存的资料。\ntoken不同于用户名密码登录,可以限制过期时间和授权范围,更加的灵活。\n可以把token(用户信息)当作门禁卡,客户端拿着这张门禁卡即可出入服务提供商的大门,但是客户端并不知道这张门禁卡如何解锁打开大门,即不需知道用户的隐私信息。\noauth运作流程 (a)用户打开客户端以后,客户端要求用户给予授权。\n(b)用户同意给予客户端授权。\n(c)客户端使用上一步获得的授权,向认证服务器申请令牌。\n(d)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。\n(e)客户端使用令牌,向资源服务器申请获取资源。\n(f)资源服务器确认令牌无误,同意向客户端开放资源\n\tb步骤是关键,即用户怎样才能给于客户端授权。有了这个授权以后,客户端就可以获取令牌,进而凭令牌获取资源。\noauth四种授权模式 客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。oauth 2.0定义了四种授权方式。\n授权码模式(authorization code) 简化模式(implicit) 密码模式(resource owner password credentials) 客户端模式(client credentials) 授权码模式 授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与\u0026quot;服务提供商\u0026quot;的认证服务器进行互动。\n(a)用户访问客户端,后者将前者导向认证服务器(客户端:login-redirect)。\n(b)用户选择是否给予客户端授权。\n(c)假设用户给予授权,认证服务器将用户导向客户端事先指定的\u0026quot;重定向uri\u0026quot;(客户端:login-redirect-callback),同时附上一个授权码。\n(d)客户端收到授权码,附上第一次请求的\u0026quot;重定向uri\u0026quot;,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。\n(e)认证服务器核对了授权码和重定向uri,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。\n下面是上面这些步骤所需要的参数。\na步骤中,客户端申请认证的uri,包含以下参数:\nresponse_type:表示授权类型,必选项,此处的值固定为\u0026quot;code\u0026quot; client_id:表示客户端的id,必选项 redirect_uri:表示重定向uri,可选项 state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。 get https://example.com/authorization/oauth/authorize? response_type=code\u0026amp; client_id=clientid\u0026amp; state=hd3b09ya\u0026amp; redirect_uri=https://example.com/client/callbackredirecturi c步骤中,认证服务器回应客户端的uri,包含以下参数:\ncode:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端id和重定向uri,是一一对应关系。 state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。 get https://example.com/client/callbackredirecturi? code=zb1ytdehuwyu67uu2djlqbl3t-ceerwvywdrtojvouc2o3bfjnl0rjtifkponhodyepzz9ewcg_ijkls4obh2f\u0026amp; state=hd3b09ya 客户端获取到关键的信code后,即可拿着code去认证服务器获取token以及用户信息。\n这步可以都在callbackredirecturi方法中实现\n简化模式 简化模式(implicit grant type)不通过第三方应用程序的服务器(客户端后台服务器),直接在浏览器中向认证服务器申请令牌,跳过了\u0026quot;授权码\u0026quot;这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。\n(a)客户端将用户导向认证服务器。\n(b)用户决定是否给于客户端授权。\n(c)假设用户给予授权,认证服务器将用户导向客户端指定的\u0026quot;重定向uri\u0026quot;,并在uri的hash部分包含了访问令牌。\n(d)浏览器向资源服务器发出请求,其中不包括上一步收到的hash值。(获取提取token的脚本)\n(e)资源服务器返回一个网页,其中包含的代码可以获取hash值中的令牌。\n(f)浏览器执行上一步获得的脚本,提取出令牌。\n(g)浏览器将令牌发给客户端。\n重点说明c、d、e、f步骤:\nc步骤中,认证服务器回应客户端的uri,包含以下参数:\naccess_token:表示访问令牌,必选项。 token_type:表示令牌类型,该值大小写不敏感,必选项。 expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。 scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。 state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。 http/1.1 302 found location: http://example.com/cb#access_token=2yotnfzfejr1zcsicmwpaa \u0026amp;state=xyz\u0026amp;token_type=example\u0026amp;expires_in=3600 在上面的例子中,认证服务器用http头信息的location栏,指定浏览器重定向的网址。注意,在这个网址的hash部分包含了令牌。\n根据上面的d步骤,下一步浏览器会访问location指定的网址,但是hash部分不会发送。接下来的e步骤,服务提供商的资源服务器发送过来的代码,会提取出hash中的令牌。\n密码模式 密码模式(resource owner password credentials grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向\u0026quot;服务商提供商\u0026quot;索要授权。\n在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。\n流程:\n(a)用户向客户端提供用户名和密码。\n(b)客户端将用户名和密码发给认证服务器,向后者请求令牌。\n(c)认证服务器确认无误后,向客户端提供访问令牌。\n个人理解,这种模式已经脱离了oauth的理念,属于正常的客户端服务端认证模式。\n客户端模式 客户端模式(client credentials grant)指客户端以自己的名义,而不是以用户的名义,向\u0026quot;服务提供商\u0026quot;进行认证。严格地说,客户端模式并不属于oauth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求\u0026quot;服务提供商\u0026quot;提供服务,其实不存在授权问题。\n流程:\n(a)客户端向认证服务器进行身份认证,并要求一个访问令牌。\n(b)认证服务器确认无误后,向客户端提供访问令牌。\n更新令牌 token作为认证令牌,其中一项体现灵活性的就是存在过期时间,而更新令牌则是作为刷新过期时间的关键点。\n如果用户访问的时候,客户端的\u0026quot;访问令牌\u0026quot;已经过期,则需要使用\u0026quot;更新令牌\u0026quot;申请一个新的访问令牌。\n客户端发出更新令牌的http请求,包含以下参数:\ngranttype:表示使用的授权模式,此处的值固定为\u0026quot;refreshtoken\u0026quot;,必选项。 refresh_token:表示早前收到的更新令牌,必选项。 scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。 post /token http/1.1 host: server.example.com authorization: basic czzcagrsa3f0mzpnwdfmqmf0m2jw content-type: application/x-www-form-urlencoded grant_type=refresh_token\u0026amp;refresh_token=tgzv3jokf0xg5qx2tlkwia ","date":"2024-06-01","permalink":"https://www.holatto.com/posts/oauth/","summary":"初学OAuth 2.0 介绍:OAuth是一个关于授权(authorization)的开放网络标准。 参考理解OAuth 2.0 背景 OAuth的适用场合,提出问题 有一个\u0026quo","title":"oauth"},{"content":"用户认证流程源码 1. usernamepasswordauthenticationfilter-实现类\n简单来说,两步。\n① 封装对象;\n② 管理认证器进行认证。\n详细。 调用authenticate认证方法 \u0026ndash;\u0026gt; 1.1 将传入的username和password封装到usernamepasswordauthenticationtoken-实现类对象中。\n1.2 调用providermanager-实现类的方法authentic进行后续认证。\n2. providermanager-实现类\n简单来说,选取合适认证器进行认证,并且封装认证结果\n详细。 调用abstractuserdetailsauthenticationprovider-抽象类的authenticate模板方法进行用户认证,并且返回认证结果。模板方法中具体调用的方法实现是daoauthenticationprovider-实现类\n3. abstractuserdetailsauthenticationprovider 和 daoauthenticationprovider 简单说,三步。\n① 根据用户名获取数据库中用户信息,并且封装成userdetails-接口对象; ② 根据userdetails对象的密码和前文封装的usernamepasswordauthenticationtoken对象的密码进行匹配; ③ 以上操作都成功的话,将userdetails对象封装到usernamepasswordauthenticationtoken对象中,然后返回。\n模板方法:\nuserdetails user = retrieveuser(username, (usernamepasswordauthenticationtoken) authentication); void additionalauthenticationchecks(user, (usernamepasswordauthenticationtoken) authentication); authentication result = createsuccessauthentication(principaltoreturn, authentication, user);\n详细。 3.1 调用retrieveuser 方法,此方法中userdetails loadeduser = this.userdetailsservice.loaduserbyusername(username);的方法根据用户名查询数据库用户信息。 (这步是需要开发人员自己实现认证业务,即实现userdetailsservice的loaduserbyusername(string username)方法; 并且封装userdetails对象,保存角色权限信息到此对象中。同样需要开发人员定义实现类);\n3.2 将通过用户名认证成功的userdetails对象的密码和usernamepasswordauthenticationtoken对象的密码进行匹配。即用户输入的密码和库中存的密码进行匹配。\n3.3 帐号密码认证成功后,将userdetails对象的用户名,密码,角色权限信息等封装到usernamepasswordauthenticationtoken对象中。(usernamepasswordauthenticationtoken对象将保存到上下文中)\n","date":"2024-04-11","permalink":"https://www.holatto.com/posts/spring-security/advance/","summary":"用户认证流程源码 1. UsernamePasswordAuthenticationFilter-实现类 简单来说,两步。 ① 封装对象; ② 管理认证器进行认证。 详细。 调用a","title":"进阶springsecurity"},{"content":"问题背景 shanghai-***-business-***-webapp(上海*****业务)的***-business-***(java模块)主要用到的分支有:\n2.x(主分支),生产环境分支\ntest(测试分支),测试环境分支\nweb项目主分支依赖java的2.x分支的代码。测试分支依赖java测试分支。\n近期测试环境正在进行底层脱敏新功能的测试,现在有一张表的敏感字段没有进行脱敏设置,需要开发进行敏感字段的脱敏。而进行脱敏问题的处理需要在专门分支进行开发,也就是在feat-mask-field(mask分支)上。\n对于我的理解,既然在mask分支上进行开发,那么最后当然要mr到测试分支,但是在我mr的时候发现最关键的一点,底层的版本号不一样,也就是测试分支和主分支的版本不一样。\n后面了解到,mask分支是从主分支上单独拉出来的分支,而不是测试分支拉出的分支。\n发现问题 为什么要从主分支上拉出一个分支,但是作用是为测试分支服务? 那么这样只能在测试分支进行开发,然后测好再将功能点cherry到mask分支(这个动作类似于dev合并master),再将mask分支合并到测试分支(保证生产和测试分支平行),完成测试环境的脱敏开发任务。 那么为什么要这样设计,本来在测试分支已经开发好了,为什么还要再把分支点加到mask分支,然后再合并到测试分支这样多此一举呢? mask分支尽然是从主分支开的,那么合并测试分支是否会出现冲突问题? 理解问题 第一个问题:\n对于主要的分支,尽量不要直接提交,应该进行mr合并到主分支的操作,对于小型的、只有一个贡献者的项目,直接push到master上是有很大概率可以保证master的稳定性的。当项目不再是小型的。当项目不再是单人的。[https://liwt31.github.io/2019/09/01/nopush/]具体解释在链接中。\n为测试分支服务,是保证测试环境的功能要向生产环境看齐,对应上。\n第二个问题:\n先解释mask分支是从主分支分出,合并测试分支问题。在第一次mask分支mr测试分支时,肯定是需要解决冲突的,在处理过冲突后,后续提交则只检测提交代码的合并冲突状态,而不需要关心类似上文中提出的版本号问题。\n在测试分支开发的功能对应到主分支,做到测试环境和生产环境功能平行。并且能够做到及时的处理测试分支和主分支的冲突问题,提前解决冲突问题。\n那么为什么需要绕一下呢,因为主分支的版本号和测试分支的版本号不一样,底层的代码会影响到web项目的调用。(web项目测试分支引用的是测试分支test的代码)。那么在本地进行项目启动的时候,如果想要模仿测试环境,则需要将java项目分支切到测试分支,保证依赖正确。\n总结 合并分支要做到从主分支向其他分支合并,而不是反过来。主次需理解清楚。 mask分支作为一个连接生产环境和测试环境的桥梁存在,既保证测试环境要有生产环境的功能点,又保证测试环境开发的新功能点无问题,即对主分支无冲突。 ","date":"2024-03-17","permalink":"https://www.holatto.com/posts/git/git-branch-01/","summary":"问题背景 shanghai-***-business-***-webapp(上海*****业务)的***-business-***(java模块)主要用到的分支有","title":"git branch,主次分支合并问题"},{"content":"sql执行顺序: from on join where group by(开始使用select中的别名,后面的语句中都可以使用) avg,sum\u0026hellip;. having select distinct order by limit 1. 条件分支 case when\n语法:\ncase when (条件1) then 结果1 when (条件2) then 结果2 ... else 其他结果 end 举例:\n\t假设有一个学生表 student,包含以下字段:name(姓名)、age(年龄)。请你编写一个 sql 查询,将学生按照年龄划分为三个年龄等级(age_level):60 岁以上为 \u0026ldquo;老同学\u0026rdquo;,20 岁以上(不包括 60 岁以上)为 \u0026ldquo;年轻\u0026rdquo;,20 岁及以下、以及没有年龄信息为 \u0026ldquo;小同学\u0026rdquo;。\n\t返回结果应包含学生的姓名(name)和年龄等级(age_level),并按姓名升序排序。\n结果:\nselect name, case when (age \u0026gt; 60) then \u0026#39;老同学\u0026#39; when (age \u0026gt; 20) then \u0026#39;年轻\u0026#39; else \u0026#39;小同学\u0026#39; end as age_level from student order by name asc; 2. 时间函数 使用时间函数获取当前日期、当前日期时间和当前时间:\n-- 获取当前日期 select date() as current_date; -- 获取当前日期时间 select datetime() as current_datetime; -- 获取当前时间 select time() as current_time; 查询结果:\ncurrent_date current_datetime current_time 2023-08-01 2023-08-01 14:30:00 14:30:00 3. 开窗函数 sum over 该函数用法为:\nsum(计算字段名) over (partition by 分组字段名) 举例:\n\t假设有一个学生表 student,包含以下字段:id(学号)、name(姓名)、age(年龄)、score(分数)、class_id(班级编号)。\n\t请你编写一个 sql 查询,返回每个学生的详细信息(字段顺序和原始表的字段顺序一致),并计算每个班级的学生平均分( (class_avg_score)。\n答案:\nselect id, name, age, score, class_id, avg(score) over ( partition by class_id ) as class_avg_score from student; sum over order by 示例用法如下:\nsum(计算字段名) over (partition by 分组字段名 order by 排序字段 排序规则) 举例:\n\t假设有一个学生表 student,包含以下字段:id(学号)、name(姓名)、age(年龄)、score(分数)、class_id(班级编号)。\n\t请你编写一个 sql 查询,返回每个学生的详细信息(字段顺序和原始表的字段顺序一致),并且按照分数升序的方式累加计算每个班级的学生总分(class_sum_score)。\n答案:\nselect id, name, age, score, class_id, sum(score) over ( partition by class_id order by score asc ) as class_sum_score from student; 运行结果:\nid name age score class_id class_sum_score 1 鸡哥 25 2.5 1 2.5 2 绿箭 18 400 1 402.5 4 摸fish 360 2 360 3 热dog 40 600 2 960 5 李阿巴 19 120 3 120 6 老李 56 500 3 620 8 王加瓦 23 0 4 0 7 李变量 24 390 4 390 9 赵派森 80 600 4 990 10 孙加加 60 100.5 5 100.5 与sum over 函数比较\n结果为:\nselect id, name, age, score, class_id, sum(score) over ( partition by class_id ) as class_sum_score from student; id name age score class_id class_sum_score 1 鸡哥 25 2.5 1 402.5 2 绿箭 18 400 1 402.5 3 热dog 40 600 2 960 4 摸fish 360 2 960 5 李阿巴 19 120 3 620 6 老李 56 500 3 620 7 李变量 24 390 4 990 8 王加瓦 23 0 4 990 9 赵派森 80 600 4 990 10 孙加加 60 100.5 5 100.5 不使用order by 将计算的最终结果放入每个数据中;而使用order by 后,可以实现累加的效果。\nrank rank 开窗函数的语法如下:\nrank() over ( partition by 列名1, 列名2, ... -- 可选,用于指定分组列 order by 列名3 [asc|desc], 列名4 [asc|desc], ... -- 用于指定排序列及排序方式 ) as rank_column 其中,partition by 子句可选,用于指定分组列,将结果集按照指定列进行分组;order by 子句用于指定排序列及排序方式,决定了计算 rank 时的排序规则。as rank_column 用于指定生成的 rank 排名列的别名。\n举例:\n\t假设有一个学生表 student,包含以下字段:id(学号)、name(姓名)、age(年龄)、score(分数)、class_id(班级编号)。\n\t请你编写一个 sql 查询,返回每个学生的详细信息(字段顺序和原始表的字段顺序一致),并且按照分数降序的方式计算每个班级内的学生的分数排名(ranking)。\n结果:\nselect id, name, age, score, class_id, rank() over ( partition by class_id order by score desc ) as ranking from student; id name age score class_id ranking 2 鱼皮 18 400 1 1 1 绿箭 25 2.5 1 2 3 热dog 40 600 2 1 4 摸fish 360 2 2 6 老李 56 500 3 1 5 李阿巴 19 120 3 2 9 赵派森 80 600 4 1 7 李变量 24 390 4 2 8 王加瓦 23 0 4 3 10 孙加加 60 100.5 5 1 row_number row_number 开窗函数的语法如下(几乎和 rank 函数一模一样):\nrow_number() over ( partition by column1, column2, ... -- 可选,用于指定分组列 order by column3 [asc|desc], column4 [asc|desc], ... -- 用于指定排序列及排序方式 ) as row_number_column 其中,partition by子句可选,用于指定分组列,将结果集按照指定列进行分组。order by 子句用于指定排序列及排序方式,决定了计算 row_number 时的排序规则。as row_number_column 用于指定生成的行号列的别名。\n它与之前讲到的 rank 函数,row_number 函数为每一行都分配一个唯一的整数值,不管是否存在并列(相同排序值)的情况。每一行都有一个唯一的行号,从 1 开始连续递增。\nlag / lead 1)lag 函数\nlag 函数用于获取 当前行之前 的某一列的值。它可以帮助我们查看上一行的数据。\nlag 函数的语法如下:\nlag(column_name, offset, default_value) over (partition by partition_column order by sort_column) 参数解释:\ncolumn_name:要获取值的列名。 offset:表示要向上偏移的行数。例如,offset为1表示获取上一行的值,offset为2表示获取上两行的值,以此类推。 default_value:可选参数,用于指定当没有前一行时的默认值。 partition by和order by子句可选,用于分组和排序数据。 2)lead 函数\nlead 函数用于获取 当前行之后 的某一列的值。它可以帮助我们查看下一行的数据。\nlead 函数的语法如下:\nlead(column_name, offset, default_value) over (partition by partition_column order by sort_column) 参数解释:\ncolumn_name:要获取值的列名。 offset:表示要向下偏移的行数。例如,offset为1表示获取下一行的值,offset为2表示获取下两行的值,以此类推。 default_value:可选参数,用于指定当没有后一行时的默认值。 partition by和order by子句可选,用于分组和排序数据。 举例:\n\t假设有一个学生表 student,包含以下字段:id(学号)、name(姓名)、age(年龄)、score(分数)、class_id(班级编号)。\n\t请你编写一个 sql 查询,返回每个学生的详细信息(字段顺序和原始表的字段顺序一致),并且按照分数降序的方式获取每个班级内的学生的前一名学生姓名(prev_name)、后一名学生姓名(next_name)。\n结果:\nselect id, name, age, score, class_id, lag(name) over ( partition by class_id order by score desc ) as prev_name, lead(name) over ( partition by class_id order by score desc ) as next_name from student; id name age score class_id prev_name next_name 2 绿箭 18 400 1 鸡哥 1 鸡哥 25 2.5 1 鱼皮 3 热dog 40 600 2 摸fish 4 摸fish 360 2 热dog 6 老李 56 500 3 李阿巴 5 李阿巴 19 120 3 老李 9 赵派森 80 600 4 李变量 7 李变量 24 390 4 赵派森 王加瓦 8 王加瓦 23 0 4 李变量 10 孙加加 60 100.5 5 ","date":"2023-08-20","permalink":"https://www.holatto.com/posts/mysql/mysql-study/study01/","summary":"SQL执行顺序: from on join where group by(开始使用select中的别名,后面的语句中都可以使用) avg,sum\u0026hellip;. having select distinct order by limit 1. 条件分支 case when 语法: CASE WHEN (条件1) THEN 结果1 WHEN (条件2) THEN 结果","title":"日常练习sql"},{"content":"\t因为发现许多博主都有rss这个功能,我想着跟个风也搞一个rss订阅源。接下来记录一下安装的心路历程。\n1 \t本想着找个其他主题模板中copy一个就能完成的事儿,没想到人家的rss订阅源都是主题模板内置的,这一下我给我整不会了。 我只好在google中搜索“rss订阅源如何生成”,发现搜出来的大多数都是使用工具来管理rss源,也就是如何订阅他人的rss源(rss.xml)。或者说使用工具(例如rsshub)进行rss订阅源的生成,但是在我搜索这个工具的时候发现人家是让先自己手动搭建一个网站,我想着这可不行啊,生成一个rss订阅源这么麻烦吗?跟我想象的不一样啊。后来在我搜索的过程中发现我用的不是hugo框架嘛,直接搜索“hugo rss订阅源”试一下。\n2 \t没想到一搜直接就有了,发现有好多博主都有安装rss订阅源的教程,并且hugo官方文档也有具体的rss生成教程,顿时感觉到会搜索跟不会搜索的差距。接下来由于hugo官方文档为英文,理解起来略显麻烦,就跟随博主的帖子进行了rss源的生成。\n3 \t第一步,先定位rss源模板的位置放在hugo架构的哪个地方?根据官方文档的介绍可以放在以下位置,并且优先权由上到下。在这里我将模板放到了layouts/rss.xml\n[layouts/index.rss.xml layouts/home.rss.xml layouts/rss.xml layouts/list.rss.xml layouts/index.xml layouts/home.xml layouts/list.xml layouts/_default/index.rss.xml layouts/_default/home.rss.xml layouts/_default/rss.xml layouts/_default/list.rss.xml layouts/_default/index.xml layouts/_default/home.xml layouts/_default/list.xml layouts/_internal/_default/rss.xml] \t第二步,生成xml文件后模板代码如何生成?我直接用了博主帖子中的模板代码,进行了copy。\n{{- /* generate rss v2 with full page content. */ -}} {{- /* upstream hugo bug - rss dates can be in future: https://github.com/gohugoio/hugo/issues/3918 */ -}} {{- $page_context := cond .ishome site . -}} {{- $pages := $page_context.regularpages -}} {{- $limit := site.config.services.rss.limit -}} {{- if ge $limit 1 -}} {{- $pages = $pages | first $limit -}} {{- end -}} {{- printf \u0026#34;\u0026lt;?xml version=\\\u0026#34;1.0\\\u0026#34; encoding=\\\u0026#34;utf-8\\\u0026#34; standalone=\\\u0026#34;yes\\\u0026#34; ?\u0026gt;\u0026#34; | safehtml }} \u0026lt;rss version=\u0026#34;2.0\u0026#34; xmlns:atom=\u0026#34;http://www.w3.org/2005/atom\u0026#34;\u0026gt; \u0026lt;channel\u0026gt; \u0026lt;title\u0026gt;{{ if ne .title site.title }}{{ with .title }}{{.}} | {{ end }}{{end}}{{ site.title }}\u0026lt;/title\u0026gt; \u0026lt;link\u0026gt;{{ .permalink }}\u0026lt;/link\u0026gt; {{- with .outputformats.get \u0026#34;rss\u0026#34; }} {{ printf \u0026#34;\u0026lt;atom:link href=%q rel=\\\u0026#34;self\\\u0026#34; type=%q /\u0026gt;\u0026#34; .permalink .mediatype | safehtml }} {{ end -}} \u0026lt;description\u0026gt;{{ .title | default site.title }}\u0026lt;/description\u0026gt; \u0026lt;generator\u0026gt;source themes academic (https://sourcethemes.com/academic/)\u0026lt;/generator\u0026gt; {{- with site.languagecode }}\u0026lt;language\u0026gt;{{.}}\u0026lt;/language\u0026gt;{{end -}} {{- with site.copyright }}\u0026lt;copyright\u0026gt;{{ replace (replace . \u0026#34;{year}\u0026#34; now.year) \u0026#34;\u0026amp;copy;\u0026#34; \u0026#34;©\u0026#34; | plainify }}\u0026lt;/copyright\u0026gt;{{end -}} {{- if not .date.iszero }}\u0026lt;lastbuilddate\u0026gt;{{ .date.format \u0026#34;mon, 02 jan 2006 15:04:05 -0700\u0026#34; | safehtml }}\u0026lt;/lastbuilddate\u0026gt;{{ end -}} {{- if .scratch.get \u0026#34;og_image\u0026#34; }} \u0026lt;image\u0026gt; \u0026lt;url\u0026gt;{{ .scratch.get \u0026#34;og_image\u0026#34; }}\u0026lt;/url\u0026gt; \u0026lt;title\u0026gt;{{ .title | default site.title }}\u0026lt;/title\u0026gt; \u0026lt;link\u0026gt;{{ .permalink }}\u0026lt;/link\u0026gt; \u0026lt;/image\u0026gt; {{end -}} {{ range $pages }} \u0026lt;item\u0026gt; \u0026lt;title\u0026gt;{{ .title }}\u0026lt;/title\u0026gt; \u0026lt;link\u0026gt;{{ .permalink }}\u0026lt;/link\u0026gt; \u0026lt;pubdate\u0026gt;{{ .date.format \u0026#34;mon, 02 jan 2006 15:04:05 -0700\u0026#34; | safehtml }}\u0026lt;/pubdate\u0026gt; \u0026lt;guid\u0026gt;{{ .permalink }}\u0026lt;/guid\u0026gt; \u0026lt;description\u0026gt;{{ .content | html }}\u0026lt;/description\u0026gt; \u0026lt;/item\u0026gt; {{ end }} \u0026lt;/channel\u0026gt; \u0026lt;/rss\u0026gt; \t直接使用默认模板会出现报错,也就是这个问题。这个问题我在其他博主的帖子中发现是因为主页文章中使用到了“一般来说,出现这些问题是因为 markdown 文件中存在一些特殊字符,例如 ^h、^e 等字符”。那么为了避免这个问题官方有两个解决方法,第一通过hugo正则表达式替换特殊字符,第二是在markdown文件中删除特殊字符。\nthis page contains the following errors: error on line 7750 at column 66: pcdata invalid char value 2 below is a rendering of the page up to the first error. \t这两种方法我都没有使用,我直接将.content替换为.summary即可只在rss中只显示文章部分。其实其中的问题我可能也没有解决,只是瞎猫碰到死耗子,碰巧给我弄对了,以后有机会了再去研究这个问题。现在主要是想把这个订阅源做出来。\n\t第三步,在主题的配置文件中(themes/virgo/config.toml)设置主页中rss跳转链接。\n[module] [module.hugoversion] # extended = true # min = \u0026#34;0.55.0\u0026#34; # max = \u0026#34;0.84.2\u0026#34; [menu] [[menu.main]] identifier = \u0026#34;rss\u0026#34; name = \u0026#34;rss\u0026#34; url = \u0026#34;/index.xml\u0026#34; weight = 5 4 \t以上是记录订阅源生成的过程和想法。\n","date":"2023-06-08","permalink":"https://www.holatto.com/posts/rss-introduction/","summary":" 因为发现许多博主都有Rss这个功能,我想着跟个风也搞一个Rss订阅源。接下来记录一下安装的心路历程。 1 本想着找个其他主题模板中copy一个就能完成的事儿,没","title":"hugo中rss安装"},{"content":"题目 https://leetcode.cn/problems/minimum-cost-to-cut-a-stick/\n分析 根据题目给出棍子的长度和切割点 得出最小成本,即当前木棒的长度+切点左边的木棒切割完毕的最小成本+切点右边的木棒切割完毕的最小成本 如何得出最小成本是这道题的关键点。这里可以通过分治递归的方法将整体的最小成本细化为一个个子集的最小成本,然后通过记忆化将最小成本子集记录下来,再得出整体的最小成本。\n实现 方法一:\n//记忆化 map\u0026lt;long, integer\u0026gt; memo = new hashmap\u0026lt;\u0026gt;(); public int mincost(int n, int[] cuts) { //递归方法 return dfs(0, n, cuts); } private int dfs(int l, int r, int[] cuts) { //防止重复 long x = l * 1000000000l + r; //获取map中是否已经获得子集最小成本 if (memo.containskey(x)) { return memo.get(x); } //结果值 int res = integer.max_value; //切割一个点的成本 int cost = r - l; // 这里太过于暴力,但是也可以过 for (int cut : cuts) { // cut是分割点 if (cut \u0026lt;= l || cut \u0026gt;= r) { continue; } int a = dfs(l, cut, cuts); int b = dfs(cut, r, cuts); //获取最小成本结果子集 res = math.min(a + b, res); } //若当前子集 无法进一步切割,则为0,否则为结果子集+当前结果集成本 int ans = res == integer.max_value ? 0 : res + cost; //记忆当前子集的最小成本 memo.put(x, ans); return ans; } 方法二:\n。。。待续\n","date":"2023-04-03","permalink":"https://www.holatto.com/posts/leecode/array1547/","summary":"题目 https://leetcode.cn/problems/minimum-cost-to-cut-a-stick/ 分析 根据题目给出棍子的长度和切割点 得出最小成本,即当前木棒的长度+切点左边的木棒切割完毕的最小成本+切点右边的木棒切割完毕的最小成本 如何得出最小成本是这道","title":"1547. 切棍子的最小成本"},{"content":" 题目 https://leetcode.cn/problems/matrix-cells-in-distance-order/\n分析 需要二维数组int[][]; 计算目标节点到坐标上各个节点的距离,并且进行由近到远的排序; 如何将节点进行排序? 在题目第三点的支持下,前两点比较好完成,但是分析的第三点如何解决将是一个关键的问题。\n对于一般的二维数组赋值为:\n//第一种方式: int a[][]={{1,2,3},{4,5,6}}; //第二种方式; int[][] ints = new int[4][2]; ints[i][j] =__; //分别赋值 //第三种方式:第二维的长度可以动态申请 int[][] arr3 = new int[5][];//五行的长度 for(int i=0; i\u0026lt;arr3.length; ++i){ arr3[i]=new int[i+1]; //列的长度每次都变化。每次都要重新申请空间(长度) for(int j=0; j\u0026lt;arr3[i].length; ++j) arr3[i][j]= i+j; } 我们专注于对每一个节点的值的使用,比如:\n//第一种方式 int a[][]={{1,2,3},{4,5,6}}; system.out.println(a[1][1]); // 结果为5 而在这一题中,我们只专注每个节点的位置,因此可以将二维数组改造为n行2列(n*2),以便记录每个节点的位置信息,比如:\nrows =2 ,cols =2; int[][] res= new int[2 * 2][2]{{0,0},{0,1},{1,0},{1,1}}; // 只专注位置信息 如何进行排序的问题就是通过改变每行的数组就可以了。比如:\n//需要将{1,1}节点的位置放在第一行就可以这样表示 res[0] = new int[]{1,1}; 因此这道题的一种解法就明确了\n实现 解法1:直接排序\npublic static int[][] allcellsdistorder(int rows, int cols, int rcenter, int ccenter) { int[][] res = new int[rows*cols][2]; int index = 0; for (int i = 0 ; i \u0026lt; rows ; i ++) { for(int j = 0 ; j \u0026lt; cols ; j ++) { int[] xy = {i,j}; res[index++] = xy; } } //根据单元格距离的算法来进行每个节点的位置排序 arrays.sort(res, comparator.comparingint(o -\u0026gt; (math.abs(o[0] - rcenter) + math.abs(o[1] - ccenter)))); return res; } 解法2:bfs\npublic static int[][] allcellsdistorder(int r, int c, int r0, int c0) { //1.准备工作 //1.1准备二维数组序列(结果集序列)** int[][] res = new int[r * c][2]; //1.2准备每个位置被记录标识 ** boolean vis[][] = new boolean[r][c]; //1.3初始化位置记录标识 for (int i = 0; i \u0026lt; r; i++) { for (int j = 0; j \u0026lt; c; j++) { vis[i][j] = false; } } //1.4创建队列 **** queue\u0026lt;node\u0026gt; q = new linkedlist\u0026lt;\u0026gt;(); //1.5用于计算当前节点四周 ** int dx[] = new int[]{1, -1, 0, 0}; int dy[] = new int[]{0, 0, 1, -1}; //1.5将第一个节点加入队列 q.add(new node(r0, c0)); int cnt = 0; //1.6设置当前节点为被记录过 vis[r0][c0] = true; //2.遍历队列 while (!q.isempty()) { node n = q.poll(); //2.1将队列首部节点放入结果集序列中 res[cnt][0] = n.x; res[cnt++][1] = n.y; //2.2辐射四周节点 ** for (int i = 0; i \u0026lt; 4; i++) { int tx = n.x + dx[i]; int ty = n.y + dy[i]; //2.3若满足需求则放入队列 ** if (tx \u0026gt;= 0 \u0026amp;\u0026amp; tx \u0026lt; r \u0026amp;\u0026amp; ty \u0026gt;= 0 \u0026amp;\u0026amp; ty \u0026lt; c \u0026amp;\u0026amp; !vis[tx][ty]) { vis[tx][ty] = true; q.add(new node(tx, ty)); } } } //3.返回结果集队列 return res; } 扩充:bfs 广度优先 (bfs:breadth first search)\n从一个开始出发,每次都把当前视野内能处理的事儿都处理好之后,然后再递进处理更深层级的事儿,滴水不漏。广度优先算法需要用到我们早期学过的一个数据结构:队列。\n算法的执行过程如下:首先选一个顶点作为起始点,从这个节点开始,我们先将其加入到队列中。然后开始不断的迭代,迭代过程中,首先,看队列还有没有未处理的元素,有则从队列的头部取出一个节点,然后将当前节点标记为已访问,然后将这个节点的所有没有被访问过的临接点都加入到队列中(若有),因为队列具有先入先出的性质,这就可以使得我们的节点总是保持当时入队时的相对顺序;若没有了,则遍历完成。\n算法执行流程如下图所示:\n广度优先算法的伪代码如下:\nfunction bfssearch(节点){ const map = new map(); const queue = []; 将节点加入queue中 while(queue不为空) { const currentnode = queue.shift(); 用map将currentnode标记为已访问 for(遍历currentnode的所有临接点) { 取出一个邻接点node if(邻接点node还没有被访问) { 将邻接点node加入queue中 } } } } ","date":"2023-04-02","permalink":"https://www.holatto.com/posts/leecode/array1030/","summary":"题目 https://leetcode.cn/problems/matrix-cells-in-distance-order/ 分析 需要二维数组int[][]; 计算目标节点到坐标上各个节点的距离,并且进行由近到远的排序; 如何将节点进行排序? 在题目第三点的支持下,前两点比较好完成,但","title":"1030.距离顺序排列矩阵单元格"},{"content":"解释我的域名 想必看到域名的家人们会有一些懵x,但是仔细看博客主页的左上角也许会有联想。没错了, 我的博客的域名就分为两部分,前面的hola和tto。\nhola是一个西班牙语单词,意思是”你好“,相当于英语的hello。这个单词的灵感来自《那个女孩》这首歌,没错,就是我喜欢的歌手陶喆的歌曲,来自这首歌主歌的第一句话“hola 最近还好吗”。收录于专辑《再见你好吗》,至今我还看不懂这张专辑的封面里面有jj lin,但是这张专辑整体跟他也妹关系鸭。还是希望陶喆能够大火吧。扯远了~~😅\ntto则是twenty two的缩写,twenty只用首字母缩写表示二十,而two则用to来代替,表示二的意思。to作为two进行谐音的缩写,整体下来我觉得这个域名挺棒的。\n整体意思就是“你好 二十二”,正好今年我也到了二十二岁,对我来说挺值得记录的。\n这里再次推荐一下b站up主小yt,那个女孩的链接跳转也是此up翻唱的版本,我可太喜欢他的音色了。\n","date":"2023-04-01","permalink":"https://www.holatto.com/posts/wiki/","summary":"解释我的域名 想必看到域名的家人们会有一些懵x,但是仔细看博客主页的左上角也许会有联想。没错了, 我的博客的域名就分为两部分,前面的Hola和tto。 Hola是一个","title":"wiki"},{"content":"设计模式简介 设计模式(design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。\n分类 创建型模式,共五种:\n工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。\n结构型模式,共七种:\n适配器模式、装饰器模式、代理模式、门面模式、桥接模式、组合模式、享元模式。\n行为型模式,共十一种:\n策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。\nj2ee 模式,共八种:\n这些设计模式特别关注表示层。这些模式是由 sun java center 鉴定的。\nmvc 模式,业务代表模式,组合实体模式,数据访问对象模式,前端控制器模式,拦截过滤器模式,服务定位器模式,传输对象模式\n七大原则 1️⃣开闭原则\n(1)概念\n①定义:一个软件实体(如类 模块 函数)应该对扩展开放,对修改关闭\n②用抽象构建框架,用实现扩展细节;\n③优点:提高软件系统的可复用性及可维护性;\n④开闭原则是所有原则的基础;\n(2)开闭原则coding\n①声明接口icourse\npublic interface icourse { integer getid(); string getname(); double getprice(); } ②声明接口实现类\npublic class javacourse implements icourse{ private integer id; private string name; private double price; public javacourse(integer id, string name, double price) { this.id = id; this.name = name; this.price = price; } public integer getid() { return this.id; } public string getname() { return this.name; } public double getprice() { return this.price; } } ③声明新的类来继承接口实现类(扩展)\npublic class javadiscountcourse extends javacourse { public javadiscountcourse(integer id, string name, double price) { super(id, name, price); } public double getdiscountprice(){ return super.getprice()*0.8; } } ④编写测试代码\npublic class test { public static void main(string[] args) { icourse icourse = new javadiscountcourse(96, \u0026#34;java开发\u0026#34;, 348d); javadiscountcourse javacourse = (javadiscountcourse) icourse; system.out.println(\u0026#34;课程id:\u0026#34; + javacourse.getid() + \u0026#34; 课程名称:\u0026#34; + javacourse.getname() + \u0026#34; 课程原价:\u0026#34; + javacourse.getprice() + \u0026#34; 课程折后价格:\u0026#34; + javacourse.getdiscountprice() + \u0026#34;元\u0026#34;); } } ⑤uml类图\n2️⃣依赖倒置原则\n(1)概念\n①定义:高层模块不应该依赖于低层模块,两者都应该依赖于抽象;\n②抽象不应该依赖细节,细节应该依赖抽象;\n③针对接口编程,不要针对实现编程;\n④优点:可以降低类间的耦合性,提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险;\n(2)依赖倒置原则coding\n①声明接口\npublic interface icourse { void studycourse(); } ②创建多个实现类(javacourse fecourse pythoncourse)\npublic class javacourse implements icourse { @override public void studycourse() { system.out.println(\u0026#34;学生在学习java课程\u0026#34;); } } //**// public class fecourse implements icourse { @override public void studycourse() { system.out.println(\u0026#34;学生在学习fe课程\u0026#34;); } } //**// public class pythoncourse implements icourse { @override public void studycourse() { system.out.println(\u0026#34;学生在学习python课程\u0026#34;); } } ③编写学生类\npublic class student { // 开放set方法实现注入 public void seticourse(icourse icourse) { this.icourse = icourse; } // 声明接口 private icourse icourse; // 核心方法 public void studycourse(){ icourse.studycourse(); } } ④编写测试方法\npublic class test { public static void main(string[] args) { student student = new student(); student.seticourse(new javacourse()); student.studycourse(); student.seticourse(new fecourse()); student.studycourse(); } } ⑤uml类图\n3️⃣单一职责原则\n(1)概念\n①定义:不要存在多于一个导致类变更的原因;\n②一个类 接口 方法只负责一项职责;\n③降低类的复杂性,提高类的可读性,提高系统的可维护性,降低变更引起的风险;\n(2)单一职责原则coding\n①创建获取课程内容的接口和管理课程的接口\n// 获取课程内容接口 public interface icoursecontent { string getcoursename(); byte[] getcoursevideo(); } // 课程管理接口 public interface icoursemanager { void studycourse(); void refundcourse(); } ②创建实现类实现两个接口\npublic class courseimpl implements icoursemanager,icoursecontent { @override public void studycourse() { } @override public void refundcourse() { } @override public string getcoursename() { return null; } @override public byte[] getcoursevideo() { return new byte[0]; } } ③uml类图\n④其实也可以通过类来实现单一职责的设计,但是使用类来实现单一职责如果控制不当会使类的数量爆炸,所以不推荐使用类来实现单一职责推荐使用接口来实现,毕竟类对于接口是可以多实现的嘛,也比较容易扩展;\n4️⃣接口隔离原则\n(1)概念\n①定义:用多个专门的接口,而不使用单一的总接口,客户端不应该依赖于不需要的接口;\n②一个类对一个类的依赖,应该建立在最小的接口上;\n③建立单一接口,不要建立庞大臃肿的接口;\n④尽量细化接口,接口中的方法尽量少;\n⑤在使用接口隔离原则的时候一定要适度(如果控制不好,会提升程序的复杂性);\n⑥优点:符合高内聚 低耦合的设计思想,从而使得类具有很好的可读性 可扩展性和可维护性;\n(2)接口隔离原则coding\n①创建多个不同的接口\npublic interface ieatanimalaction { void eat(); } //**// public interface iflyanimalaction { void fly(); } //**// public interface iswimanimalaction { void swim(); } ②创建具体的实现类通过细粒度的实现组合来实现接口\npublic class dog implements iswimanimalaction,ieatanimalaction { @override public void eat() { } @override public void swim() { } } ③uml类图\n④接口隔离原则与单一职责原则的区别\n单一职责原则:强调的是接口 类 方法的职责是单一的,这里强调的是职责,也就是说在一个接口中只要职责是单一的有多个方法也可以(例如游泳 仰泳 自由泳等);\n另外单一职责原则约束是对接口 类 方法的约束针对的是程序中的实现和细节;\n接口隔离原则:注重的是对接口依赖的隔离,针对的是对程序框架的构建\n5️⃣迪米特原则(最少知道原则)\n(1)概念\n①定义:一个对象应该对其他对象保持最少的了解,又叫最少知道原则;\n②尽量降低类与类之间的耦合;\n③优点:降低类之间的耦合;\n④强调只和朋友交流,不和陌生人说话;\n⑤朋友:出现在成员变量 方法的输入 输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类;\n(2)迪米特原则coding\n①创建boss对象\npublic class boss { public void commandchecknumber(teamleader teamleader){ teamleader.checknumberofcourses(); } } ②创建boss的\u0026quot;朋友\u0026quot;teamleader;\npublic class teamleader { public void checknumberofcourses(){ list\u0026lt;course\u0026gt; courselist = new arraylist\u0026lt;course\u0026gt;(); for(int i = 0 ;i \u0026lt; 20;i++){ courselist.add(new course()); } system.out.println(\u0026#34;在线课程的数量是:\u0026#34;+courselist.size()); } } ③创建teamleader的朋友course;\npublic class course { } ④编写测试类\npublic class test { public static void main(string[] args) { boss boss = new boss(); teamleader teamleader = new teamleader(); boss.commandchecknumber(teamleader); } } ⑤uml类图\n6️⃣里氏替换原则\n(1)概念\n①定义:如果对每一个类型为t1的对象o1,都有类型为t2的对象o2,使得以t1定义的所有程序p在所有的对象o1都替换成o2时,程序p的行为没有发生变化,那么类型t2是类型t1的子类型.\n②定义扩展:一个软件实体如果适用一个父类的话,那一定适用于其子类,所有引用父类的地方必须能透明的使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变.\n③引申意义:子类可以扩展父类的功能,但不能改变父类原有的功能;\n④含义1:子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;\n⑤含义2:子类中可以增加自己特有的方法;\n⑥含义3:当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松;\n⑦含义4:当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等;\n⑧优点1:约束继承泛滥,开闭原则的一种体现;\n⑨优点2:加强程序的健壮性,同时变更时也可以做到非常好的兼容性提高程序的维护性和扩展性,降低需求变更时引入的风险.\n(2)里氏替换原则coding\n由于里氏替换原则定义比较多,这里我们仅以含义3为参照来进行coding,其他含义大家可以对照着文字解释进行尝试;在实际的工作中大家可以根据实际的环境来尽量满足里氏替换原则;\n①定义base类\npublic class base { public void method(hashmap map){ system.out.println(\u0026#34;父类被执行\u0026#34;); } } ②定义child来实现base\npublic class child extends base { public void method(map map) { system.out.println(\u0026#34;子类hashmap入参方法被执行\u0026#34;); } } ③编写测试类\npublic class test { public static void main(string[] args) { base child = new child(); hashmap hashmap = new hashmap(); child.method(hashmap); } } 7️⃣合成复用原则\n(1)概念\n①定义:尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的(聚合has-a和组合contains-a);\n②优点:可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;\n(2)合成复用原则coding\n①创建抽象基类\npublic abstract class dbconnection { public abstract string getconnection(); } ②创建实现类来继承抽象基类\npublic class mysqlconnection extends dbconnection { @override public string getconnection() { return \u0026#34;mysql数据库连接\u0026#34;; } } //**// public class postgresqlconnection extends dbconnection { @override public string getconnection() { return \u0026#34;postgresql数据库连接\u0026#34;; } } ③创建业务应用\npublic class productdao{ private dbconnection dbconnection; public void setdbconnection(dbconnection dbconnection) { this.dbconnection = dbconnection; } public void addproduct(){ string conn = dbconnection.getconnection(); system.out.println(\u0026#34;使用\u0026#34;+conn+\u0026#34;增加产品\u0026#34;); } } ④创建测试类\npublic class test { public static void main(string[] args) { productdao productdao = new productdao(); productdao.setdbconnection(new postgresqlconnection()); productdao.addproduct(); } } ⑤uml类图\n从uml类图中可以看出,合成服用原则是满足开闭原则以及里氏替换原则的;\n创建型模式 工厂模式 工厂模式(factory pattern)是 java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。\n在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。\n简单工厂\n//接口 public interface fruit { public void print(); } //2个实现类 public class apple implements fruit{ @override public void print() { system.out.println(\u0026#34;我是一个苹果\u0026#34;); } } public class orange implements fruit{ @override public void print() { system.out.println(\u0026#34;我是一个橘子\u0026#34;); } } //工厂类 public class fruitfactory { // 使用反射来提高扩展性 public fruit getvideo(class c){ fruit fruit = null; try { fruit = (fruit) class.forname(c.getname()).newinstance(); } catch (instantiationexception e) { e.printstacktrace(); } catch (illegalaccessexception e) { e.printstacktrace(); } catch (classnotfoundexception e) { e.printstacktrace(); } return fruit; } public fruit produce(string type){ if(type.equals(\u0026#34;apple\u0026#34;)){ return new apple(); }else if(type.equals(\u0026#34;orange\u0026#34;)){ return new orange(); }else{ system.out.println(\u0026#34;请输入正确的类型!\u0026#34;); return null; } } } 使用场景:jdbc连接数据库,硬件访问,降低对象的产生和销毁\n应用简单工厂的jdk类(java.util.calendar源码)\n/** * gets a calendar using the default time zone and locale. the * \u0026lt;code\u0026gt;calendar\u0026lt;/code\u0026gt; returned is based on the current time * in the default time zone with the default * {@link locale.category#format format} locale. * * @return a calendar. */ public static calendar getinstance() { return createcalendar(timezone.getdefault(), locale.getdefault(locale.category.format)); } private static calendar createcalendar(timezone zone, locale alocale) { calendarprovider provider = localeprovideradapter.getadapter(calendarprovider.class, alocale) .getcalendarprovider(); if (provider != null) { try { return provider.getinstance(zone, alocale); } catch (illegalargumentexception iae) { // fall back to the default instantiation } } calendar cal = null; if (alocale.hasextensions()) { string caltype = alocale.getunicodelocaletype(\u0026#34;ca\u0026#34;); if (caltype != null) { switch (caltype) { case \u0026#34;buddhist\u0026#34;: cal = new buddhistcalendar(zone, alocale); break; case \u0026#34;japanese\u0026#34;: cal = new japaneseimperialcalendar(zone, alocale); break; case \u0026#34;gregory\u0026#34;: cal = new gregoriancalendar(zone, alocale); break; } } } if (cal == null) { // if no known calendar type is explicitly specified, // perform the traditional way to create a calendar: // create a buddhistcalendar for th_th locale, // a japaneseimperialcalendar for ja_jp_jp locale, or // a gregoriancalendar for any other locales. // note: the language, country and variant strings are interned. if (alocale.getlanguage() == \u0026#34;th\u0026#34; \u0026amp;\u0026amp; alocale.getcountry() == \u0026#34;th\u0026#34;) { cal = new buddhistcalendar(zone, alocale); } else if (alocale.getvariant() == \u0026#34;jp\u0026#34; \u0026amp;\u0026amp; alocale.getlanguage() == \u0026#34;ja\u0026#34; \u0026amp;\u0026amp; alocale.getcountry() == \u0026#34;jp\u0026#34;) { cal = new japaneseimperialcalendar(zone, alocale); } else { cal = new gregoriancalendar(zone, alocale); } } return cal; } 抽象工厂模式 抽象工厂模式(abstract factory pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。\n①创建抽象工厂\npublic interface coursefactory { video getvideo(); article getarticle(); } ②创建具体工厂来实现抽象工厂\npublic class javacoursefactory implements coursefactory { @override public video getvideo() { return new javavideo(); } @override public article getarticle() { return new javaarticle(); } } public class pythoncoursefactory implements coursefactory { @override public video getvideo() { return new pythonvideo(); } @override public article getarticle() { return new pythonarticle(); } } ③创建抽象产品族\npublic abstract class article { public abstract void produce(); } public abstract class article { public abstract void produce(); } ④创建具体产品\npublic class javaarticle extends article { @override public void produce() { system.out.println(\u0026#34;编写java课程手记\u0026#34;); } } public class javavideo extends video { @override public void produce() { system.out.println(\u0026#34;录制java课程视频\u0026#34;); } } public class pythonarticle extends article { @override public void produce() { system.out.println(\u0026#34;编写python课程手记\u0026#34;); } } public class pythonvideo extends video { @override public void produce() { system.out.println(\u0026#34;录制python课程视频\u0026#34;); } } ⑤创建测试类\npublic class test { public static void main(string[] args) { coursefactory coursefactory = new javacoursefactory(); video video = coursefactory.getvideo(); article article = coursefactory.getarticle(); video.produce(); article.produce(); } } ⑥uml类图\n抽象工厂的应用\n①org.apache.ibatis.session\npackage org.apache.ibatis.session; import java.sql.connection; /** * creates an {@link sqlsession} out of a connection or a datasource * * @author clinton begin */ public interface sqlsessionfactory { sqlsession opensession(); sqlsession opensession(boolean autocommit); sqlsession opensession(connection connection); sqlsession opensession(transactionisolationlevel level); sqlsession opensession(executortype exectype); sqlsession opensession(executortype exectype, boolean autocommit); sqlsession opensession(executortype exectype, transactionisolationlevel level); sqlsession opensession(executortype exectype, connection connection); configuration getconfiguration(); } ②以sqlsession opensession(executortype exectype, transactionisolationlevel level)为例\n③进入到defaultsqlsessionfactory\n@override public sqlsession opensession(executortype exectype, transactionisolationlevel level) { return opensessionfromdatasource(exectype, level, false); } ④打开opensessionfromdatesource\nprivate sqlsession opensessionfromdatasource(executortype exectype, transactionisolationlevel level, boolean autocommit) { transaction tx = null; try { final environment environment = configuration.getenvironment(); final transactionfactory transactionfactory = gettransactionfactoryfromenvironment(environment); tx = transactionfactory.newtransaction(environment.getdatasource(), level, autocommit); final executor executor = configuration.newexecutor(tx, exectype); return new defaultsqlsession(configuration, executor, autocommit); } catch (exception e) { closetransaction(tx); // may have fetched a connection so lets call close() throw exceptionfactory.wrapexception(\u0026#34;error opening session. cause: \u0026#34; + e, e); } finally { errorcontext.instance().reset(); } } ⑤uml类图\n建造者模式 将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。\n主要解决:主要解决在软件系统中,有时候面临着\u0026quot;一个复杂对象\u0026quot;的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。\n①创建computer\npublic class computer { private string cpu; private string mainboard; private string harddisk; private string displaycard; private string power; private string memory; public computer(computerbuilder computerbuilder){ this.cpu = computerbuilder.cpu; this.mainboard = computerbuilder.mainboard; this.harddisk = computerbuilder.harddisk; this.displaycard = computerbuilder.displaycard; this.power = computerbuilder.power; this.memory = computerbuilder.memory; } @override public string tostring() { return \u0026#34;computer{\u0026#34; + \u0026#34;cpu=\u0026#39;\u0026#34; + cpu + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, mainboard=\u0026#39;\u0026#34; + mainboard + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, harddisk=\u0026#39;\u0026#34; + harddisk + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, displaycard=\u0026#39;\u0026#34; + displaycard + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, power=\u0026#39;\u0026#34; + power + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, memory=\u0026#39;\u0026#34; + memory + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } public static class computerbuilder{ private string cpu; private string mainboard; private string harddisk; private string displaycard; private string power; private string memory; public computerbuilder buildcpu(string cpu){ this.cpu = cpu; return this; } public computerbuilder buildmainboard(string mainboard){ this.mainboard = mainboard; return this; } public computerbuilder buildharddisk(string harddisk){ this.harddisk = harddisk; return this; } public computerbuilder builddisplaycard(string displaycard){ this.displaycard = displaycard; return this; } public computerbuilder buildpower(string power){ this.power = power; return this; } public computerbuilder buildmemory(string memory){ this.memory = memory; return this; } public computer build(){ return new computer(this); } } } ②创建测试类\npublic class test { public static void main(string[] args) { computer computer = new computer.computerbuilder().buildcpu(\u0026#34;酷睿i7\u0026#34;).buildmainboard(\u0026#34;华硕主板\u0026#34;).build(); system.out.println(computer); set\u0026lt;string\u0026gt; set = immutableset.\u0026lt;string\u0026gt;builder().add(\u0026#34;a\u0026#34;).add(\u0026#34;b\u0026#34;).build(); system.out.println(set); } } ③uml类图\n建造者模式的实际应用\n①在jdk中的应用stringbuilder与stringbuffer,这里仅列举出stringbuilder的append方法,stringbuffer与stringbuilder的区别在于stringbuffer的append方法是同步方法(线程安全);\n@override public stringbuilder append(object obj) { return append(string.valueof(obj)); } @override public stringbuilder append(string str) { super.append(str); return this; } /** * appends the specified {@code stringbuffer} to this sequence. * \u0026lt;p\u0026gt; * the characters of the {@code stringbuffer} argument are appended, * in order, to this sequence, increasing the * length of this sequence by the length of the argument. * if {@code sb} is {@code null}, then the four characters * {@code \u0026#34;null\u0026#34;} are appended to this sequence. * \u0026lt;p\u0026gt; * let \u0026lt;i\u0026gt;n\u0026lt;/i\u0026gt; be the length of this character sequence just prior to * execution of the {@code append} method. then the character at index * \u0026lt;i\u0026gt;k\u0026lt;/i\u0026gt; in the new character sequence is equal to the character at * index \u0026lt;i\u0026gt;k\u0026lt;/i\u0026gt; in the old character sequence, if \u0026lt;i\u0026gt;k\u0026lt;/i\u0026gt; is less than * \u0026lt;i\u0026gt;n\u0026lt;/i\u0026gt;; otherwise, it is equal to the character at index \u0026lt;i\u0026gt;k-n\u0026lt;/i\u0026gt; * in the argument {@code sb}. * * @param sb the {@code stringbuffer} to append. * @return a reference to this object. */ public stringbuilder append(stringbuffer sb) { super.append(sb); return this; } @override public stringbuilder append(charsequence s) { super.append(s); return this; } /** * @throws indexoutofboundsexception {@inheritdoc} */ @override public stringbuilder append(charsequence s, int start, int end) { super.append(s, start, end); return this; } @override public stringbuilder append(char[] str) { super.append(str); return this; } /** * @throws indexoutofboundsexception {@inheritdoc} */ @override public stringbuilder append(char[] str, int offset, int len) { super.append(str, offset, len); return this; } @override public stringbuilder append(boolean b) { super.append(b); return this; } ②建造者模式在spring中的应用\npackage org.springframework.beans.factory.support; import org.springframework.beans.factory.config.runtimebeanreference; import org.springframework.util.objectutils; public class beandefinitionbuilder { private abstractbeandefinition beandefinition; private int constructorargindex; public static beandefinitionbuilder genericbeandefinition() { beandefinitionbuilder builder = new beandefinitionbuilder(); builder.beandefinition = new genericbeandefinition(); return builder; } public static beandefinitionbuilder genericbeandefinition(class\u0026lt;?\u0026gt; beanclass) { beandefinitionbuilder builder = new beandefinitionbuilder(); builder.beandefinition = new genericbeandefinition(); builder.beandefinition.setbeanclass(beanclass); return builder; } public static beandefinitionbuilder genericbeandefinition(string beanclassname) { beandefinitionbuilder builder = new beandefinitionbuilder(); builder.beandefinition = new genericbeandefinition(); builder.beandefinition.setbeanclassname(beanclassname); return builder; } public static beandefinitionbuilder rootbeandefinition(class\u0026lt;?\u0026gt; beanclass) { return rootbeandefinition((class)beanclass, (string)null); } public static beandefinitionbuilder rootbeandefinition(class\u0026lt;?\u0026gt; beanclass, string factorymethodname) { beandefinitionbuilder builder = new beandefinitionbuilder(); builder.beandefinition = new rootbeandefinition(); builder.beandefinition.setbeanclass(beanclass); builder.beandefinition.setfactorymethodname(factorymethodname); return builder; } public static beandefinitionbuilder rootbeandefinition(string beanclassname) { return rootbeandefinition((string)beanclassname, (string)null); } public static beandefinitionbuilder rootbeandefinition(string beanclassname, string factorymethodname) { beandefinitionbuilder builder = new beandefinitionbuilder(); builder.beandefinition = new rootbeandefinition(); builder.beandefinition.setbeanclassname(beanclassname); builder.beandefinition.setfactorymethodname(factorymethodname); return builder; } public static beandefinitionbuilder childbeandefinition(string parentname) { beandefinitionbuilder builder = new beandefinitionbuilder(); builder.beandefinition = new childbeandefinition(parentname); return builder; } private beandefinitionbuilder() { } public abstractbeandefinition getrawbeandefinition() { return this.beandefinition; } public abstractbeandefinition getbeandefinition() { this.beandefinition.validate(); return this.beandefinition; } public beandefinitionbuilder setparentname(string parentname) { this.beandefinition.setparentname(parentname); return this; } public beandefinitionbuilder setfactorymethod(string factorymethod) { this.beandefinition.setfactorymethodname(factorymethod); return this; } /** @deprecated */ @deprecated public beandefinitionbuilder addconstructorarg(object value) { return this.addconstructorargvalue(value); } public beandefinitionbuilder addconstructorargvalue(object value) { this.beandefinition.getconstructorargumentvalues().addindexedargumentvalue(this.constructorargindex++, value); return this; } public beandefinitionbuilder addconstructorargreference(string beanname) { this.beandefinition.getconstructorargumentvalues().addindexedargumentvalue(this.constructorargindex++, new runtimebeanreference(beanname)); return this; } public beandefinitionbuilder addpropertyvalue(string name, object value) { this.beandefinition.getpropertyvalues().add(name, value); return this; } public beandefinitionbuilder addpropertyreference(string name, string beanname) { this.beandefinition.getpropertyvalues().add(name, new runtimebeanreference(beanname)); return this; } public beandefinitionbuilder setinitmethodname(string methodname) { this.beandefinition.setinitmethodname(methodname); return this; } public beandefinitionbuilder setdestroymethodname(string methodname) { this.beandefinition.setdestroymethodname(methodname); return this; } public beandefinitionbuilder setscope(string scope) { this.beandefinition.setscope(scope); return this; } public beandefinitionbuilder setabstract(boolean flag) { this.beandefinition.setabstract(flag); return this; } public beandefinitionbuilder setlazyinit(boolean lazy) { this.beandefinition.setlazyinit(lazy); return this; } public beandefinitionbuilder setautowiremode(int autowiremode) { this.beandefinition.setautowiremode(autowiremode); return this; } public beandefinitionbuilder setdependencycheck(int dependencycheck) { this.beandefinition.setdependencycheck(dependencycheck); return this; } public beandefinitionbuilder adddependson(string beanname) { if(this.beandefinition.getdependson() == null) { this.beandefinition.setdependson(new string[]{beanname}); } else { string[] added = (string[])objectutils.addobjecttoarray(this.beandefinition.getdependson(), beanname); this.beandefinition.setdependson(added); } return this; } public beandefinitionbuilder setrole(int role) { this.beandefinition.setrole(role); return this; } } 单例模式 单例模式(singleton pattern)是 java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。\n这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。\n注意:\n1、单例类只能有一个实例。\n2、单例类必须自己创建自己的唯一实例。\n3、单例类必须给所有其他对象提供这一实例\n懒汉式\n①懒汉式简单版实现\npublic class lazysingletonv1 { private static lazysingleton lazysingleton = null; private lazysingleton(){} public static lazysingleton getinstance(){ if(lazysingleton == null){ lazysingleton = new lazysingleton(); } return lazysingleton; } 这个版本的实现会在多线程环境中出现问题\n②实现线程安全的懒汉式\npublic class lazysingletonv2 { private static lazysingleton lazysingleton = null; private lazysingleton(){} public synchronized static lazysingleton getinstance(){ if(lazysingleton == null){ lazysingleton = new lazysingleton(); } return lazysingleton; } 通过使用synchronized同步锁来实现懒汉式单例的线程安全是一种较为普遍的解决方案,但是此方案也有一定的局限; synchronized修饰static方法其实是锁的整个class文件,因为同步锁有上锁和解锁的开销所以此解决方案会存在性能开销过大的问题;\n③doublecheck双重检查实现懒汉式\npublic class lazydoublechecksingleton { private static lazydoublechecksingleton lazydoublechecksingleton = null; private lazydoublechecksingleton(){} public static lazydoublechecksingleton getinstance(){ if(lazydoublechecksingleton == null){ synchronized (lazydoublechecksingleton.class){ if(lazydoublechecksingleton == null){ lazydoublechecksingleton = new lazydoublechecksingleton(); } } } return lazydoublechecksingleton; } } 虽然这种方式兼顾了性能和安全同时也满足懒加载的情况,但是这种情况也有一定的缺陷,首先通过synchronized我们保证了多线程情况下只有一个线程可以创建对象,如果对象已经被创建则直接返回不需要在进行加锁的操作,避免了性能的开销;但是根据java规范intra-thread semantics我们知道单线程在执行操作的时候有可能会出现指令重排序的问题,指令重排序不会影响单线程的结果,如果放在多线程的情况下就会出现问题;\n如上图所示,在多线程环境下,由于线程0并没有初始化完成对象,但是线程1已经将此对象判断为非空,也就是说线程1拿到的其实是线程0正在进行初始化的对象,在这样的情况下系统就会报异常了;\n④通过volatile关键字禁止doublecheck双重检查指令重排序的问题;\npublic class lazydoublechecksingleton { private volatile static lazydoublechecksingleton lazydoublechecksingleton = null; private lazydoublechecksingleton(){} public static lazydoublechecksingleton getinstance(){ if(lazydoublechecksingleton == null){ synchronized (lazydoublechecksingleton.class){ if(lazydoublechecksingleton == null){ lazydoublechecksingleton = new lazydoublechecksingleton(); } } } return lazydoublechecksingleton; } } 通过volatile关键字我们可以禁止掉指令重排序,从而解决了多线程情况下的指令重排序问题;volatile关键字主要使用的是缓存一致性协议\n饿汉式\n①饿汉式简单实现\npublic class hungrysingleton{ private final static hungrysingleton hungrysingleton; private hungrysingleton(){} public static hungrysingleton getinstance(){ return hungrysingleton; } } ②序列化破坏单例模式原理解析及解决方案\npublic class hungrysingleton implements serializable{ private final static hungrysingleton hungrysingleton; private hungrysingleton(){} public static hungrysingleton getinstance(){ return hungrysingleton; } } public class test { public static void main(string[] args) throws exception { objectoutputstream oos = new objectoutputstream(new fileoutputstream(\u0026#34;singleton_file\u0026#34;)); oos.writeobject(instance); file file = new file(\u0026#34;singleton_file\u0026#34;); objectinputstream ois = new objectinputstream(new fileinputstream(file)); hungrysingleton newinstance = (hungrysingleton) ois.readobject(); system.out.println(instance); system.out.println(newinstance); system.out.println(instance == newinstance); } } 从上边的测试中,我们可以看出通过序列化和反序列化我们得到了两个不同的对象,这样就违背了单例的初衷;接下来我们就解决这个问题;\npublic class hungrysingleton implements serializable{ private final static hungrysingleton hungrysingleton; private hungrysingleton(){} public static hungrysingleton getinstance(){ return hungrysingleton; } private object readresolve(){ return hungrysingleton; } } 针对这个问题,我们就需要去readobject()方法中去看一下了(由于源码调用层次较深,这里不做演示,有兴趣的小伙伴可以自己尝试一下)通过看源码我们了解到底层是通过反射来创建的对象,既然是通过反射来创建的对象那么可能和原对象是不一致,这也就解释了为什么第一次比较的时候为false了;那么为什么加上了readresolve方法就能解决这个问题呢?通过继续看源码我们找到了答案,在反射的时候jdk会确认被反射的类有没有readresolve()方法,如果有则返回true;如果结果为true会通过反射调用被反射类的readresolve()方法,然后readresolve()方法会返回我们创建好的实例对象,这样就实现两个对象比较结果为true的情况了;\n③单例模式反射攻击的解决方案(暴力反射)\npublic class test { public static void main(string[] args) throws exception { class objectclass = hungrysingleton.class; constructor constructor = objectclass.getdeclaredconstructor(); constructor.setaccessible(true); staticinnerclasssingleton instance = staticinnerclasssingleton.getinstance(); staticinnerclasssingleton newinstance = (staticinnerclasssingleton) constructor.newinstance(); system.out.println(instance); system.out.println(newinstance); system.out.println(instance == newinstance); } } 我们可以看到通过反射我们依然可以得到两个对象,那我们该怎么解决这样的问题呢请往下看\npublic class hungrysingleton implements serializable{ private final static hungrysingleton hungrysingleton; static{ hungrysingleton = new hungrysingleton(); } private hungrysingleton(){ if(hungrysingleton != null){ throw new runtimeexception(\u0026#34;单例构造器禁止反射调用\u0026#34;); } } public static hungrysingleton getinstance(){ return hungrysingleton; } private object readresolve(){ return hungrysingleton; } } 单例模式的其他实现\n①enum枚举单例\npublic enum enuminstance { instance{ protected void printtest(){ system.out.println(\u0026#34;enum print test\u0026#34;); } }; protected abstract void printtest(); private object data; public object getdata() { return data; } public void setdata(object data) { this.data = data; } public static enuminstance getinstance(){ return instance; } } 序列化验证\npublic class test { public static void main(string[] args) throws exception { enuminstance instance = enuminstance.getinstance(); instance.setdata(new object()); objectoutputstream oos = new objectoutputstream(new fileoutputstream(\u0026#34;singleton_file\u0026#34;)); oos.writeobject(instance); file file = new file(\u0026#34;singleton_file\u0026#34;); objectinputstream ois = new objectinputstream(new fileinputstream(file)); enuminstance newinstance = (enuminstance) ois.readobject(); system.out.println(instance.getdata()); system.out.println(newinstance.getdata()); system.out.println(instance.getdata() == newinstance.getdata()); } } 反射验证\npublic class test { public static void main(string[] args) throws exception { class objectclass = enuminstance.class; constructor constructor = objectclass.getdeclaredconstructor(string.class,int.class); constructor.setaccessible(true); enuminstance instance = (enuminstance) constructor.newinstance(\u0026#34;测试\u0026#34;,666); } } 可以看到枚举单例可以完美的解决上述的问题;\n②threadlocal线程单例\n这种单例并不能保证全局唯一,但是可以保证线程唯一\npublic class threadlocalinstance { private static final threadlocal\u0026lt;threadlocalinstance\u0026gt; threadlocalinstancethreadlocal = new threadlocal\u0026lt;threadlocalinstance\u0026gt;(){ @override protected threadlocalinstance initialvalue() { return new threadlocalinstance(); } }; private threadlocalinstance(){} public static threadlocalinstance getinstance(){ return threadlocalinstancethreadlocal.get(); } } public class t implements runnable { @override public void run() { threadlocalinstance instance = threadlocalinstance.getinstance(); system.out.println(thread.currentthread().getname()+\u0026#34; \u0026#34;+instance); } } public class test { public static void main(string[] args) throws ioexception, classnotfoundexception, nosuchmethodexception, illegalaccessexception, invocationtargetexception, instantiationexception { system.out.println(\u0026#34;main thread\u0026#34;+threadlocalinstance.getinstance()); system.out.println(\u0026#34;main thread\u0026#34;+threadlocalinstance.getinstance()); system.out.println(\u0026#34;main thread\u0026#34;+threadlocalinstance.getinstance()); system.out.println(\u0026#34;main thread\u0026#34;+threadlocalinstance.getinstance()); system.out.println(\u0026#34;main thread\u0026#34;+threadlocalinstance.getinstance()); system.out.println(\u0026#34;main thread\u0026#34;+threadlocalinstance.getinstance()); } } 单例模式的应用\n单例模式在jdk中的应用\npublic class runtime { private static runtime currentruntime = new runtime(); /** * returns the runtime object associated with the current java application. * most of the methods of class \u0026lt;code\u0026gt;runtime\u0026lt;/code\u0026gt; are instance * methods and must be invoked with respect to the current runtime object. * * @return the \u0026lt;code\u0026gt;runtime\u0026lt;/code\u0026gt; object associated with the current * java application. */ public static runtime getruntime() { return currentruntime; } /** don\u0026#39;t let anyone else instantiate this class */ private runtime() {} ...... } 原型模式 原型模式(prototype pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式之一。\n这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。\n①创建一个发邮件的场景\npublic class mail implements cloneable{ private string name; private string emailaddress; private string content; public mail(){ system.out.println(\u0026#34;mail class constructor\u0026#34;); } public string getname() { return name; } public void setname(string name) { this.name = name; } public string getemailaddress() { return emailaddress; } public void setemailaddress(string emailaddress) { this.emailaddress = emailaddress; } public string getcontent() { return content; } public void setcontent(string content) { this.content = content; } @override public string tostring() { return \u0026#34;mail{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, emailaddress=\u0026#39;\u0026#34; + emailaddress + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, content=\u0026#39;\u0026#34; + content + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;+super.tostring(); } @override protected object clone() throws clonenotsupportedexception { system.out.println(\u0026#34;clone mail object\u0026#34;); return super.clone(); } } ②创建mailutil工具类\npublic class mailutil { public static void sendmail(mail mail){ string outputcontent = \u0026#34;向{0}同学,邮件地址:{1},邮件内容:{2}发送邮件成功\u0026#34;; system.out.println(messageformat.format(outputcontent,mail.getname(),mail.getemailaddress(),mail.getcontent())); } public static void saveoriginmailrecord(mail mail){ system.out.println(\u0026#34;存储originmail记录,originmail:\u0026#34;+mail.getcontent()); } } ③编写测试类\npublic class test { public static void main(string[] args) throws clonenotsupportedexception { mail mail = new mail(); mail.setcontent(\u0026#34;初始化模板\u0026#34;); system.out.println(\u0026#34;初始化mail:\u0026#34;+mail); for(int i = 0;i \u0026lt; 10;i++){ mail mailtemp = (mail) mail.clone(); mailtemp.setname(\u0026#34;姓名\u0026#34;+i); mailtemp.setemailaddress(\u0026#34;姓名\u0026#34;+i+\u0026#34;@test.com\u0026#34;); mailtemp.setcontent(\u0026#34;恭喜您,此次活动中奖了\u0026#34;); mailutil.sendmail(mailtemp); system.out.println(\u0026#34;克隆的mailtemp:\u0026#34;+mailtemp); } mailutil.saveoriginmailrecord(mail); } } 深克隆与浅克隆\n(1)浅克隆\n①创建pig类\npublic class pig implements cloneable{ private string name; private date birthday; public pig(string name, date birthday) { this.name = name; this.birthday = birthday; } public string getname() { return name; } public void setname(string name) { this.name = name; } public date getbirthday() { return birthday; } public void setbirthday(date birthday) { this.birthday = birthday; } @override protected object clone() throws clonenotsupportedexception { pig pig = (pig)super.clone(); } @override public string tostring() { return \u0026#34;pig{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, birthday=\u0026#34; + birthday + \u0026#39;}\u0026#39;+super.tostring(); } } ②编写测试类\npublic class test { public static void main(string[] args) throws clonenotsupportedexception, nosuchmethodexception, invocationtargetexception, illegalaccessexception { date birthday = new date(0l); pig pig1 = new pig(\u0026#34;佩奇\u0026#34;,birthday); pig pig2 = (pig) pig1.clone(); system.out.println(pig1); system.out.println(pig2); pig1.getbirthday().settime(666666666666l); system.out.println(pig1); system.out.println(pig2); } } 从结果中可以看出,先打印的pig1与pig2的内容是一致的,在设置完生日以后打印的一组结果内容也是一致的,但是我们发现在我们设置完pig1的生日以后,后打印的一组内容的生日都发生了变化,这又是为什么呢?我们通过debug找到了答案;\n通过分析我们找到了原因,pig1与pig2对象所使用的生日属性都是一个对象的,所以我们在修改完生日以后两个对象的内容都发生了变化(也就是说我们引用的克隆对象都是同一个对象,当我们修改被克隆对象的属性的时候,克隆出来的对象属性也会跟着发生变化);这就是浅克隆,同时默认的也是浅克隆;\n(2)深克隆\n从上边的结论中我们可以得出,默认的克隆方式是浅克隆,那么怎么实现深克隆呢?其实也简单,只要我们对对象的引用类型也添加克隆实现就可以解决了;\n@override protected object clone() throws clonenotsupportedexception { pig pig = (pig)super.clone(); //深克隆 pig.birthday = (date) pig.birthday.clone(); return pig; } 通过上图我们可以看出在我们进行了引用属性的深克隆以后,最后一组对象的内容已经发生了变化,同时debug的时候内存中引用的对象也发生了变化,效果已经达到了我们的预期\n注意:在使用原型模式的时候一定要进行深克隆,否则可能会出现bug\n原型模式破坏单例\n①使用hungrysingleton\npublic class hungrysingleton implements serializable,cloneable{ private final static hungrysingleton hungrysingleton; static{ hungrysingleton = new hungrysingleton(); } private hungrysingleton(){ if(hungrysingleton != null){ throw new runtimeexception(\u0026#34;单例构造器禁止反射调用\u0026#34;); } } public static hungrysingleton getinstance(){ return hungrysingleton; } private object readresolve(){ return hungrysingleton; } @override protected object clone() throws clonenotsupportedexception { return super.clone(); } } ②编写测试类\npublic class test { public static void main(string[] args) throws clonenotsupportedexception, nosuchmethodexception, invocationtargetexception, illegalaccessexception { hungrysingleton hungrysingleton = hungrysingleton.getinstance(); method method = hungrysingleton.getclass().getdeclaredmethod(\u0026#34;clone\u0026#34;); method.setaccessible(true); hungrysingleton clonehungrysingleton = (hungrysingleton) method.invoke(hungrysingleton); system.out.println(hungrysingleton); system.out.println(clonehungrysingleton); } } 可以看到通过使用clone我们依旧破坏了单例,那要如何才能解决这个问题呢?\n①单例类不实现cloneable接口;\n②单例类在重写clone方法时不使用默认的实现,将其修改为\n@override protected object clone() throws clonenotsupportedexception { return getinstance(); } 可以看到通过以上的修改,再次运行测试类的时候两个对象就完全是一致的了;\n原型模式的实际应用\njdk中原型模式的使用\npublic class arraylist\u0026lt;e\u0026gt; extends abstractlist\u0026lt;e\u0026gt; implements list\u0026lt;e\u0026gt;, randomaccess, cloneable, java.io.serializable /** * returns a shallow copy of this \u0026lt;tt\u0026gt;arraylist\u0026lt;/tt\u0026gt; instance. (the * elements themselves are not copied.) * * @return a clone of this \u0026lt;tt\u0026gt;arraylist\u0026lt;/tt\u0026gt; instance */ public object clone() { try { arraylist\u0026lt;?\u0026gt; v = (arraylist\u0026lt;?\u0026gt;) super.clone(); v.elementdata = arrays.copyof(elementdata, size); v.modcount = 0; return v; } catch (clonenotsupportedexception e) { // this shouldn\u0026#39;t happen, since we are cloneable throw new internalerror(e); } } 同理hashmap也实现了克隆接口也重写了克隆方法,也就是说hashmap也应用了原型模式;\n结构型模式 门面模式 外观模式(facade pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。\n这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。\n适用场景:当子系统越来越复杂,增加外观模式提供简单调用接口;\n构建多层系统结构,利用外观对象作为每层的入口,简化层间调用;\n①创建cpu\npublic class cpu { public void open(){ system.out.println(\u0026#34;open cpu\u0026#34;); } public void close(){ system.out.println(\u0026#34;close cpu\u0026#34;); } } ②创建硬盘disk\npublic class disk { public void open(){ system.out.println(\u0026#34;open disk\u0026#34;); } public void close(){ system.out.println(\u0026#34;close disk\u0026#34;); } } ③创建computer\npublic class computer { private cpu cpu; private disk disk; public computer(){ this.cpu = new cpu(); this.disk = new disk(); } public void open(){ this.cpu.open(); this.disk.open(); } public void close(){ this.cpu.close(); this.disk.close(); } } ④创建测试类\npublic class test { public static void main(string[] args) { computer computer = new computer(); computer.open(); system.out.println(\u0026#34;玩一会电脑\u0026#34;); computer.close(); } } uml类图\n外观模式的实际应用\n外观模式在tomcat中大量使用\n装饰者模式 定义:在不改变原有对象的基础上,将功能附加到对象上;提供了比继承更有弹性的替代方案(扩展原有对象功能);\n适用场景:扩展一个类的功能或者给一个类添加附加职责;动态的给一个对象添加功能,这些功能可以在动态的撤销;\nv1版本 ①创建基础类\npublic class battercake { protected string getdesc(){ return \u0026#34;煎饼\u0026#34;; } protected int cost(){ return 8; } } ②创建扩展类继承基础类\npublic class battercakewithegg extends battercake { @override public string getdesc() { return super.getdesc()+\u0026#34; 加一个鸡蛋\u0026#34;; } @override public int cost() { return super.cost()+1; } } ③创建扩展类继承①和②\npublic class battercakewitheggsausage extends battercakewithegg { @override public string getdesc() { return super.getdesc()+ \u0026#34; 加一根香肠\u0026#34;; } @override public int cost() { return super.cost()+2; } } ④创建测试类\npublic class test { public static void main(string[] args) { battercake battercake = new battercake(); system.out.println(battercake.getdesc()+\u0026#34; 销售价格:\u0026#34;+battercake.cost()); battercake battercakewithegg = new battercakewithegg(); system.out.println(battercakewithegg.getdesc()+\u0026#34; 销售价格:\u0026#34;+battercakewithegg.cost()); battercake battercakewitheggsausage = new battercakewitheggsausage(); system.out.println(battercakewitheggsausage.getdesc()+\u0026#34; 销售价格:\u0026#34;+battercakewitheggsausage.cost()); } } ⑤uml类图\n使用这样的方式来进行扩展的话,如果组合非常的多我们的程序会发生类爆炸的情况,怎么样才能让我们的程序更加的优雅呢?请往下看\nv2版本 ①创建抽象类\npublic abstract class abattercake { //基础功能 protected abstract string getdesc(); protected abstract int cost(); } ②创建抽象类继承①\npublic abstract class abstractdecorator extends abattercake { //在基础功能上进行功能扩充 private abattercake abattercake; public abstractdecorator(abattercake abattercake) { this.abattercake = abattercake; } protected abstract void dosomething(); @override protected string getdesc() { return this.abattercake.getdesc(); } @override protected int cost() { return this.abattercake.cost(); } } ③创建实体类继承①并实现抽象方法\npublic class battercake extends abattercake { //基础功能(功能入口) @override protected string getdesc() { return \u0026#34;煎饼\u0026#34;; } @override protected int cost() { return 8; } } ④创建实体类继承①并实现抽象方法;\npublic class eggdecorator extends abstractdecorator { //扩充功能(在基础功能之上扩充) public eggdecorator(abattercake abattercake) { super(abattercake); } @override protected void dosomething() { } @override protected string getdesc() { return super.getdesc()+\u0026#34; 加一个鸡蛋\u0026#34;; } @override protected int cost() { return super.cost()+1; } } ⑤创建实体类继承①并实现抽象方法;\npublic class sausagedecorator extends abstractdecorator{ //扩充功能(在基础功能之上扩充) public sausagedecorator(abattercake abattercake) { super(abattercake); } @override protected void dosomething() { } @override protected string getdesc() { return super.getdesc()+\u0026#34; 加一根香肠\u0026#34;; } @override protected int cost() { return super.cost()+2; } } ⑥编写测试类\npublic class test { public static void main(string[] args) { abattercake abattercake; //基础功能 abattercake = new battercake(); //在基础功能上扩充(基础功能为入参) abattercake = new eggdecorator(abattercake); abattercake = new sausagedecorator(abattercake); system.out.println(abattercake.getdesc()+\u0026#34; 销售价格:\u0026#34;+abattercake.cost()); } } 说明:由于每个实体类都继承了抽象类①,所以我们在使用的时候可以直接创建,也就是说所有的实体类都是abattercake的子类;另外关于dosomething方法,可以理解成每个实体类进行操作前的一个动作,便于扩展;\n⑦uml类图\n装饰者模式的实际应用\njdk中的使用(java io)\n适配器模式 定义:是将一个类的接口转换成客户期望的另一个接口;使原本不兼容的类可以一起工作; 适用场景:已经存在的类,方法和需求不匹配时(方法结果相同或相似);不是软件设计阶段考虑的设计模式,是随着软件维护,由于不同产品 不同厂家造成功能类似而接口不相同情况下的解决方案;\n适配器模式的扩展:对象适配器,类适配器 他们之间的主要区别就是一个是通过组合来进行操作的,一个是通过继承来进行操作的\n实现(对象适配器)\n我们有一个 mediaplayer 接口和一个实现了 mediaplayer 接口的实体类 audioplayer。默认情况下,audioplayer 可以播放 mp3 格式的音频文件。\n我们还有另一个接口 advancedmediaplayer 和实现了 advancedmediaplayer 接口的实体类。该类可以播放 vlc 和 mp4 格式的文件。\n我们想要让 audioplayer 播放其他格式的音频文件。为了实现这个功能,我们需要创建一个实现了 mediaplayer 接口的适配器类 mediaadapter,并使用 advancedmediaplayer 对象来播放所需的格式。\naudioplayer 使用适配器类 mediaadapter 传递所需的音频类型,不需要知道能播放所需格式音频的实际类。adapterpatterndemo 类使用 audioplayer 类来播放各种格式。\n①uml图\n②为媒体播放器和更高级的媒体播放器创建接口\npublic interface mediaplayer { public void play(string audiotype, string filename); } //**// public interface advancedmediaplayer { public void playvlc(string filename); public void playmp4(string filename); } ③创建实现了 advancedmediaplayer 接口的实体类。\npublic class vlcplayer implements advancedmediaplayer{ @override public void playvlc(string filename) { system.out.println(\u0026#34;playing vlc file. name: \u0026#34;+ filename); } @override public void playmp4(string filename) { //什么也不做 } } //**// public class mp4player implements advancedmediaplayer{ @override public void playvlc(string filename) { //什么也不做 } @override public void playmp4(string filename) { system.out.println(\u0026#34;playing mp4 file. name: \u0026#34;+ filename); } } ④创建实现了 mediaplayer 接口的适配器类\npublic class mediaadapter implements mediaplayer { advancedmediaplayer advancedmusicplayer; public mediaadapter(string audiotype){ if(audiotype.equalsignorecase(\u0026#34;vlc\u0026#34;) ){ advancedmusicplayer = new vlcplayer(); } else if (audiotype.equalsignorecase(\u0026#34;mp4\u0026#34;)){ advancedmusicplayer = new mp4player(); } } @override public void play(string audiotype, string filename) { if(audiotype.equalsignorecase(\u0026#34;vlc\u0026#34;)){ advancedmusicplayer.playvlc(filename); }else if(audiotype.equalsignorecase(\u0026#34;mp4\u0026#34;)){ advancedmusicplayer.playmp4(filename); } } } ⑤创建实现了 mediaplayer 接口的实体类\npublic class audioplayer implements mediaplayer { mediaadapter mediaadapter; @override public void play(string audiotype, string filename) { //播放 mp3 音乐文件的内置支持 if(audiotype.equalsignorecase(\u0026#34;mp3\u0026#34;)){ system.out.println(\u0026#34;playing mp3 file. name: \u0026#34;+ filename); } //mediaadapter 提供了播放其他文件格式的支持 else if(audiotype.equalsignorecase(\u0026#34;vlc\u0026#34;) || audiotype.equalsignorecase(\u0026#34;mp4\u0026#34;)){ mediaadapter = new mediaadapter(audiotype); mediaadapter.play(audiotype, filename); } else{ system.out.println(\u0026#34;invalid media. \u0026#34;+ audiotype + \u0026#34; format not supported\u0026#34;); } } } 使用 audioplayer 来播放不同类型的音频格式\npublic class adapterpatterndemo { public static void main(string[] args) { audioplayer audioplayer = new audioplayer(); audioplayer.play(\u0026#34;mp3\u0026#34;, \u0026#34;beyond the horizon.mp3\u0026#34;); audioplayer.play(\u0026#34;mp4\u0026#34;, \u0026#34;alone.mp4\u0026#34;); audioplayer.play(\u0026#34;vlc\u0026#34;, \u0026#34;far far away.vlc\u0026#34;); audioplayer.play(\u0026#34;avi\u0026#34;, \u0026#34;mind me.avi\u0026#34;); } } 实现(类适配器)\n①创建被适配者\npublic class adaptee { public void adapteerequest(){ system.out.println(\u0026#34;被适配者的方法\u0026#34;); } } ②创建目标适配\npublic class concretetarget implements target { @override public void request() { system.out.println(\u0026#34;concretetarget目标方法\u0026#34;); } } ③创建接口\npublic interface target { void request(); } ④创建适配方法\npublic class adapter extends adaptee implements target{ @override public void request() { //... super.adapteerequest(); //... } } ⑤创建测试类\npublic class test { public static void main(string[] args) { target target = new concretetarget(); target.request(); target adaptertarget = new adapter(); adaptertarget.request(); } } ⑥uml类图\n适配器模式实际应用\n在jdk中的应用 xmladapter\n享元模式 定义:提供了减少对象数量从而改善应用所需的对象结构的方式;运用共享技术有效地支持大量细粒度的对象;\n适用场景:常常应用于系统底层的开发,以便解决系统的性能问题;系统有大量的相似对象,需要缓冲池的场景;\n①创建employee接口\npublic interface employee { void report(); } ②创建manager类实现接口\npublic class manager implements employee { @override public void report() { system.out.println(reportcontent); } private string title = \u0026#34;部门经理\u0026#34;; private string department; private string reportcontent; public void setreportcontent(string reportcontent) { this.reportcontent = reportcontent; } public manager(string department) { this.department = department; } } ③创建employeefactory工厂类\npublic class employeefactory { private static final map\u0026lt;string,employee\u0026gt; employee_map = new hashmap\u0026lt;string,employee\u0026gt;(); public static employee getmanager(string department){ manager manager = (manager) employee_map.get(department); if(manager == null){ manager = new manager(department); system.out.print(\u0026#34;创建部门经理:\u0026#34;+department); string reportcontent = department+\u0026#34;部门汇报:此次报告的主要内容是......\u0026#34;; manager.setreportcontent(reportcontent); system.out.println(\u0026#34; 创建报告:\u0026#34;+reportcontent); employee_map.put(department,manager); } return manager; } } ④编写测试类\npublic class test { private static final string departments[] = {\u0026#34;rd\u0026#34;,\u0026#34;qa\u0026#34;,\u0026#34;pm\u0026#34;,\u0026#34;bd\u0026#34;}; public static void main(string[] args) { for(int i=0; i\u0026lt;10; i++){ string department = departments[(int)(math.random() * departments.length)]; manager manager = (manager) employeefactory.getmanager(department); manager.report(); } } } ⑤uml类图\n享元模式的应用\njdk中的integer;\n组合模式 定义:将对象组合成树形结构以表示\u0026quot;部分-整体\u0026quot;的层次结构;组合模式使客户端对单个对象和组合对象保持一致的方式处理。\n适用场景:希望客户端可以忽略组合对象与单个对象的差异时;处理一个树形结构时;\n①uml图\n②创建抽象类entry\npublic abstract class entry { //树形 protected entry parent; public abstract string getname(); public abstract int getsize(); public entry add(entry entry) throws filetreatmentexception { throw new filetreatmentexception(); } public void printlist() { printlist(\u0026#34;\u0026#34;); } protected abstract void printlist(string prefix); @override public string tostring() { return getname() + \u0026#34;(\u0026#34; + getsize() + \u0026#34;)\u0026#34;; } public string filepath() { stringbuffer fullname = new stringbuffer(); //设置当前为叶子结点 entry entry = this; //一步一步找到根节点 do { fullname.insert(0, \u0026#34;/\u0026#34; + entry.getname()); entry = entry.parent; } while (entry != null); return fullname.tostring(); } } ③创建实体类file\npublic class file extends entry{ private string name; private int size; public file(string name, int size) { this.name = name; this.size = size; } @override public string getname() { return name; } @override public int getsize() { return size; } @override protected void printlist(string prefix) { system.out.println(prefix + \u0026#34;/\u0026#34; + this); } } ④创建实体类directory\npublic class directory extends entry { private string name; private list\u0026lt;entry\u0026gt; directory = new arraylist\u0026lt;\u0026gt;(); public directory(string name) { this.name = name; } @override public string getname() { return name; } @override public int getsize() { int size = 0; iterator\u0026lt;entry\u0026gt; it = directory.iterator(); while (it.hasnext()) { entry entry = it.next(); size += entry.getsize(); } return size; } @override public entry add(entry entry) throws filetreatmentexception { directory.add(entry); entry.parent = this; return this; } @override protected void printlist(string prefix) { system.out.println(prefix + \u0026#34;/\u0026#34; + this); iterator\u0026lt;entry\u0026gt; it = directory.iterator(); while (it.hasnext()) { entry entry = it.next(); entry.printlist(prefix + \u0026#34;/\u0026#34; + name); } } } ⑤统一异常处理filetreatmentexception\npublic class filetreatmentexception extends runtimeexception { public filetreatmentexception() { } public filetreatmentexception(string message) { super(message); } } ⑥测试方法\npublic class main { public static void main(string[] args) { try { directory rootdir = new directory(\u0026#34;root\u0026#34;); directory usrdir = new directory(\u0026#34;usr\u0026#34;); rootdir.add(usrdir); directory yukidir = new directory(\u0026#34;yuki\u0026#34;); usrdir.add(yukidir); file file = new file(\u0026#34;composite.java\u0026#34;, 1000); yukidir.add(file); rootdir.printlist(); system.out.println(\u0026#34;file = \u0026#34; + file.filepath()); system.out.println(\u0026#34;yuki = \u0026#34; + yukidir.filepath()); } catch (filetreatmentexception e) { e.printstacktrace(); } } } 组合模式的实际应用\njdk java.util.hashmap\nmybatis sqlnode\n*桥接模式 定义:将抽象部分与它的具体实现部分分离,使他们都可以独立的变化;通过组合的方式建立两个类之间联系,而不是继承;\n适用场景:抽象与具体实现之间增加更多的灵活性;一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要进行独立扩展;不希望使用继承,或因为多层继承导致系统类的个数剧增;\n**意图:**将抽象部分与实现部分分离,使它们都可以独立的变化。\n①创建account接口\npublic interface account { account openaccount(); void showaccounttype(); } ②创建depositaccount类实现account接口\npublic class depositaccount implements account { @override public account openaccount() { system.out.println(\u0026#34;打开定期账号\u0026#34;); return new depositaccount(); } @override public void showaccounttype() { system.out.println(\u0026#34;这是一个定期账号\u0026#34;); } } ③创建savingaccount类实现account接口\npublic class savingaccount implements account { @override public account openaccount() { system.out.println(\u0026#34;打开活期账号\u0026#34;); //... return new savingaccount(); } @override public void showaccounttype() { system.out.println(\u0026#34;这是一个活期账号\u0026#34;); } } ④创建抽象类bank\npublic abstract class bank { protected account account; public bank(account account){ this.account = account; } abstract account openaccount(); } ⑤创建abcbank类继承bank类\npublic class abcbank extends bank { public abcbank(account account) { super(account); } @override account openaccount() { system.out.println(\u0026#34;打开中国农业银行账号\u0026#34;); //账户的类型是不确定 account.openaccount(); return account; } } ⑥创建icbcbank类继承bank类\npublic class icbcbank extends bank { public icbcbank(account account) { super(account); } @override account openaccount() { system.out.println(\u0026#34;打开中国工商银行账号\u0026#34;); //账户的类型是不确定 account.openaccount(); return account; } } ⑦uml类图\n⑧创建测试类\npublic class test { public static void main(string[] args) { bank icbcbank = new icbcbank(new depositaccount()); account icbcaccount = icbcbank.openaccount(); icbcaccount.showaccounttype(); bank icbcbank2 = new icbcbank(new savingaccount()); account icbcaccount2 = icbcbank2.openaccount(); icbcaccount2.showaccounttype(); bank abcbank = new abcbank(new savingaccount()); account abcaccount = abcbank.openaccount(); abcaccount.showaccounttype(); } } 桥接模式的实际应用\njdk java.sql.driver接口\n代理模式 定义:为其他对象提供一种代理,以控制对这个对象的访问;代理对象在客户端和目标对象之间起到中介的作用;\n适用场景:保护目标对象增强目标对象\n①uml类图\n②创建sourceable接口\npublic interface sourceable { public void method(); } ③创建source类实现sourceable\npublic class source implements sourceable { @override public void method() { system.out.println(\u0026#34;the original method!\u0026#34;); } } ④创建代理类proxy实现sourceable\npublic class proxy implements sourceable { private source source; public proxy(){ super(); this.source = new source(); } @override public void method() { before(); source.method(); atfer(); } private void atfer() { system.out.println(\u0026#34;after proxy!\u0026#34;); } private void before() { system.out.println(\u0026#34;before proxy!\u0026#34;); } } ⑤创建测试类proxytest\npublic class proxytest { public static void main(string[] args) { sourceable source = new proxy(); source.method(); } } 输出结果 before proxy! the original method! after proxy! 行为型模式 迭代器模式 定义:提供一种方法,顺序访问一个集合对象中的各个元素,而又不暴露该对象的内部表示;\n适用场景:访问一个集合对象的内容而无需暴露它的内部表示;为遍历不同的集合结构提供一个统一的接口;\n**意图:**提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。\n①创建course类\npublic class course { private string name; public course(string name) { this.name = name; } public string getname() { return name; } } ②创建接口courseaggregate\npublic interface courseaggregate { void addcourse(course course); void removecourse(course course); courseiterator getcourseiterator(); } ③创建接口courseiterator\npublic interface courseiterator { course nextcourse(); boolean islastcourse(); } ④创建courseaggregateimpl类实现courseaggregate\npublic class courseaggregateimpl implements courseaggregate { private list courselist; public courseaggregateimpl() { this.courselist = new arraylist(); } @override public void addcourse(course course) { courselist.add(course); } @override public void removecourse(course course) { courselist.remove(course); } @override public courseiterator getcourseiterator() { return new courseiteratorimpl(courselist); } } ⑤创建courseiteratorimpl实现courseiterator\npublic class courseiteratorimpl implements courseiterator { private list courselist; private int position; private course course; public courseiteratorimpl(list courselist){ this.courselist=courselist; } @override public course nextcourse() { system.out.println(\u0026#34;返回课程,位置是: \u0026#34;+position); course=(course)courselist.get(position); position++; return course; } @override public boolean islastcourse(){ if(position\u0026lt; courselist.size()){ return false; } return true; } } ⑥uml类图\n⑦编写测试类\npublic class test { public static void main(string[] args) { course course1 = new course(\u0026#34;java电商一期课程\u0026#34;); course course2 = new course(\u0026#34;java电商二期课程\u0026#34;); course course3 = new course(\u0026#34;java设计模式课程\u0026#34;); course course4 = new course(\u0026#34;python课程\u0026#34;); course course5 = new course(\u0026#34;算法课程\u0026#34;); course course6 = new course(\u0026#34;前端课程\u0026#34;); courseaggregate courseaggregate = new courseaggregateimpl(); courseaggregate.addcourse(course1); courseaggregate.addcourse(course2); courseaggregate.addcourse(course3); courseaggregate.addcourse(course4); courseaggregate.addcourse(course5); courseaggregate.addcourse(course6); system.out.println(\u0026#34;-----课程列表-----\u0026#34;); printcourses(courseaggregate); courseaggregate.removecourse(course4); courseaggregate.removecourse(course5); system.out.println(\u0026#34;-----删除操作之后的课程列表-----\u0026#34;); printcourses(courseaggregate); } public static void printcourses(courseaggregate courseaggregate){ courseiterator courseiterator= courseaggregate.getcourseiterator(); while(!courseiterator.islastcourse()){ course course=courseiterator.nextcourse(); system.out.println(course.getname()); } } } 模板方法模式 定义:定义了一个算法的骨架,并允许子类为一个或多个步骤提供实现;\n补充:模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤;\n**意图:**定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。\n①创建acourse抽象类\npublic abstract class acourse { protected final void makecourse(){ this.makeppt(); this.makevideo(); if(needwritearticle()){ this.writearticle(); } this.packagecourse(); } final void makeppt(){ system.out.println(\u0026#34;制作ppt\u0026#34;); } final void makevideo(){ system.out.println(\u0026#34;制作视频\u0026#34;); } final void writearticle(){ system.out.println(\u0026#34;编写手记\u0026#34;); } //钩子方法 protected boolean needwritearticle(){ return false; } abstract void packagecourse(); } ②创建designpatterncourse类继承acourse\npublic class designpatterncourse extends acourse { @override void packagecourse() { system.out.println(\u0026#34;提供课程java源代码\u0026#34;); } @override protected boolean needwritearticle() { return true; } } ③创建fecourse类继承acourse\npublic class fecourse extends acourse { private boolean needwritearticleflag = false; @override void packagecourse() { system.out.println(\u0026#34;提供课程的前端代码\u0026#34;); system.out.println(\u0026#34;提供课程的图片等多媒体素材\u0026#34;); } public fecourse(boolean needwritearticleflag) { this.needwritearticleflag = needwritearticleflag; } @override protected boolean needwritearticle() { return this.needwritearticleflag; } } ④uml类图\n⑤编写测试类\npublic class test { public static void main(string[] args) { // system.out.println(\u0026#34;后端设计模式课程start---\u0026#34;); // acourse designpatterncourse = new designpatterncourse(); // designpatterncourse.makecourse(); // system.out.println(\u0026#34;后端设计模式课程end---\u0026#34;); system.out.println(\u0026#34;前端课程start---\u0026#34;); acourse fecourse = new fecourse(false); fecourse.makecourse(); system.out.println(\u0026#34;前端课程end---\u0026#34;); } } 策略模式 定义: 定义了算法家族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化不会影响到使用算法的用户;\n适用场景:系统有很多类,而他们的区别仅仅在于他们的行为不同;一个系统需要动态地在几种算法中选择一种;\n主要解决:在有多种算法相似的情况下,使用 if\u0026hellip;else 所带来的复杂和难以维护。\n1 创建promotionstrategy接口\npublic interface promotionstrategy { void dopromotion(); } 2 创建不同的策略实现类\n/** * 满减策略 */ public class manjianpromotionstrategy implements promotionstrategy{ @override public void dopromotion() { system.out.println(\u0026#34;满减促销,满200-20元\u0026#34;); } } ----------------------------------------------------------------------------------------------- /** * 立减策略 */ public class lijianpromotionstrategy implements promotionstrategy { @override public void dopromotion() { system.out.println(\u0026#34;立减促销,课程的价格直接减去配置的价格\u0026#34;); } } ----------------------------------------------------------------------------------------------- /** * 返现策略 */ public class fanxianpromotionstrategy implements promotionstrategy { @override public void dopromotion() { system.out.println(\u0026#34;返现促销,返回的金额存放到慕课网用户的余额中\u0026#34;); } } 3 创建类promotionactivity\npublic class promotionactivity { private promotionstrategy promotionstrategy; public promotionactivity(promotionstrategy promotionstrategy) { this.promotionstrategy = promotionstrategy; } public void executepromotionstrategy(){ promotionstrategy.dopromotion(); } } 4 编写测试类\npublic class test { public static void main(string[] args) { promotionactivity promotionactivity618 = new promotionactivity(new lijianpromotionstrategy()); promotionactivity promotionactivity1111 = new promotionactivity(new fanxianpromotionstrategy()); promotionactivity618.executepromotionstrategy(); promotionactivity1111.executepromotionstrategy(); } } 5 uml类图\n6 但是这样的写法并不能消除if else这样的判断我们来测试一下\npublic class test { public static void main(string[] args) { promotionactivity promotionactivity = null; string promotionkey = \u0026#34;lijian\u0026#34;; if(stringutils.equals(promotionkey,\u0026#34;lijian\u0026#34;)){ promotionactivity = new promotionactivity(new lijianpromotionstrategy()); }else if(stringutils.equals(promotionkey,\u0026#34;fanxian\u0026#34;)){ promotionactivity = new promotionactivity(new fanxianpromotionstrategy()); }//...... promotionactivity.executepromotionstrategy(); } } 从上边的实例中我们可以看到,这样的代码还是会出现很多的判断并且会在命中某个条件以后创建新的对象,现在我们队这个进行一下改进,结合工厂模式;\n7 创建策略工厂promotionstrategyfactory\npublic class promotionstrategyfactory { private static map\u0026lt;string,promotionstrategy\u0026gt; promotion_strategy_map = new hashmap\u0026lt;string, promotionstrategy\u0026gt;(); static { promotion_strategy_map.put(promotionkey.lijian,new lijianpromotionstrategy()); promotion_strategy_map.put(promotionkey.fanxian,new fanxianpromotionstrategy()); promotion_strategy_map.put(promotionkey.manjian,new manjianpromotionstrategy()); } private static final promotionstrategy non_promotion = new emptypromotionstrategy(); private promotionstrategyfactory(){ } public static promotionstrategy getpromotionstrategy(string promotionkey){ promotionstrategy promotionstrategy = promotion_strategy_map.get(promotionkey); return promotionstrategy == null ? non_promotion : promotionstrategy; } private interface promotionkey{ string lijian = \u0026#34;lijian\u0026#34;; string fanxian = \u0026#34;fanxian\u0026#34;; string manjian = \u0026#34;manjian\u0026#34;; } } 8 创建空策略emptypromotionstrategy实现类\npublic class emptypromotionstrategy implements promotionstrategy { @override public void dopromotion() { system.out.println(\u0026#34;无促销活动\u0026#34;); } } 9 编写新的测试类\npublic class test { public static void main(string[] args) { string promotionkey = \u0026#34;manjianxxx\u0026#34;; promotionactivity promotionactivity = new promotionactivity(promotionstrategyfactory.getpromotionstrategy(promotionkey)); promotionactivity.executepromotionstrategy(); } } 策略模式源码解析(jdk+spring)\njdk : public interface comparator\u0026lt;t\u0026gt;\nspring : resource\n解释器模式 定义:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子(为了解释一种语言,而为语言创建的解释器).\n适用场景:某个特定类型问题的发生频率足够高的时候(例如系统中的日志服务);\n**主要解决:**对于一些固定文法构建一个解释句子的解释器\n1 先来了解一下我们要实现一个什么样的功能;\npublic class test { public static void main(string[] args) { string inputstr=\u0026#34;6 100 11 + *\u0026#34;; testexpressionparser expressionparser=new testexpressionparser(); int result=expressionparser.parse(inputstr); system.out.println(\u0026#34;解释器计算结果: \u0026#34;+result); } } 我们先自定义一个字符串(6 100 11 + *)两个字符串之间用空格隔开,先计算100 + 11,然后其结果在乘以6得到最终的结果;\n2 声明interpreter接口\npublic interface interpreter { int interpret(); } 3 编写加法解释器addinterpreter,multiinterpreter,numberinterpreter\npublic class addinterpreter implements interpreter { private interpreter firstexpression,secondexpression; public addinterpreter(interpreter firstexpression, interpreter secondexpression){ this.firstexpression=firstexpression; this.secondexpression=secondexpression; } @override public int interpret(){ return this.firstexpression.interpret()+this.secondexpression.interpret(); } @override public string tostring(){ return \u0026#34;+\u0026#34;; } } public class multiinterpreter implements interpreter { private interpreter firstexpression,secondexpression; public multiinterpreter(interpreter firstexpression, interpreter secondexpression){ this.firstexpression=firstexpression; this.secondexpression=secondexpression; } @override public int interpret(){ return this.firstexpression.interpret() * this.secondexpression.interpret(); } @override public string tostring(){ return \u0026#34;*\u0026#34;; } } public class numberinterpreter implements interpreter { private int number; public numberinterpreter(int number){ this.number=number; } public numberinterpreter(string number){ this.number=integer.parseint(number); } @override public int interpret(){ return this.number; } } 5 编写testexpressionparser\npublic class testexpressionparser { private stack\u0026lt;interpreter\u0026gt; stack = new stack\u0026lt;interpreter\u0026gt;(); public int parse(string str) { string[] stritemarray = str.split(\u0026#34; \u0026#34;); for (string symbol : stritemarray) { if (!operatorutil.isoperator(symbol)) { interpreter numberexpression = new numberinterpreter(symbol); stack.push(numberexpression); system.out.println(string.format(\u0026#34;入栈: %d\u0026#34;, numberexpression.interpret())); } else { //是运算符号,可以计算 interpreter firstexpression = stack.pop(); interpreter secondexpression = stack.pop(); system.out.println(string.format(\u0026#34;出栈: %d 和 %d\u0026#34;, firstexpression.interpret(), secondexpression.interpret())); interpreter operator = operatorutil.getexpressionobject(firstexpression, secondexpression, symbol); system.out.println(string.format(\u0026#34;应用运算符: %s\u0026#34;, operator)); int result = operator.interpret(); numberinterpreter resultexpression = new numberinterpreter(result); stack.push(resultexpression); system.out.println(string.format(\u0026#34;阶段结果入栈: %d\u0026#34;, resultexpression.interpret())); } } int result = stack.pop().interpret(); return result; } } 6 编写工具类operatorutil\npublic class operatorutil { public static boolean isoperator(string symbol) { return (symbol.equals(\u0026#34;+\u0026#34;) || symbol.equals(\u0026#34;*\u0026#34;)); } public static interpreter getexpressionobject(interpreter firstexpression, interpreter secondexpression, string symbol) { if (symbol.equals(\u0026#34;+\u0026#34;)) { return new addinterpreter(firstexpression, secondexpression); } else if (symbol.equals(\u0026#34;*\u0026#34;)) { return new multiinterpreter(firstexpression, secondexpression); } return null; } } 7 uml类图\n解释器源码解析-jdk+spring\njdk : pattern\nspring : org.springframework.expression.expressionparser\nspring的解释器可以使用以下代码进行测试\npublic class springtest { public static void main(string[] args) { expressionparser parser = new spelexpressionparser(); expression expression = parser.parseexpression(\u0026#34;100 * 2 + 400 * 1 + 66\u0026#34;); int result = (integer) expression.getvalue(); system.out.println(result); } } 补充: 解释器模式平时使用的比较少,一般都使用开源的,所以了解即可\n观察者模式 定义:定义了对象之间的一对多的依赖,让多个观察者对象同时监听某一个主题对象,当主题对象发生变化时,它的所有依赖者(观察者)都会收到通知并更新;\n适用场景:观察者与被观察者之间建立了一个抽象的耦合;观察者模式支持广播通信;\n1 创建course类\npublic class course extends observable{ private string coursename; public course(string coursename) { this.coursename = coursename; } public string getcoursename() { return coursename; } public void producequestion(course course, question question){ system.out.println(question.getusername()+\u0026#34;在\u0026#34;+course.coursename+\u0026#34;提交了一个问题\u0026#34;); setchanged(); notifyobservers(question); } } 2 创建question类\npublic class question { private string username; private string questioncontent; public string getusername() { return username; } public void setusername(string username) { this.username = username; } public string getquestioncontent() { return questioncontent; } public void setquestioncontent(string questioncontent) { this.questioncontent = questioncontent; } } 3 创建teacher类\npublic class teacher implements observer{ private string teachername; public teacher(string teachername) { this.teachername = teachername; } @override public void update(observable o, object arg) { course course = (course)o; question question = (question)arg; system.out.println(teachername+\u0026#34;老师的\u0026#34;+course.getcoursename()+\u0026#34;课程接收到一个\u0026#34;+question.getusername()+\u0026#34;提交的问答:\u0026#34;+question.getquestioncontent()); } } 4 编写测试类\npublic class test { public static void main(string[] args) { course course = new course(\u0026#34;设计模式\u0026#34;); teacher teacher1 = new teacher(\u0026#34;alpha\u0026#34;); teacher teacher2 = new teacher(\u0026#34;beta\u0026#34;); course.addobserver(teacher1); course.addobserver(teacher2); //业务逻辑代码 question question = new question(); question.setusername(\u0026#34;mufeng\u0026#34;); question.setquestioncontent(\u0026#34;java的主函数如何编写\u0026#34;); course.producequestion(course,question); } } 5 uml类图\n观察者模式源码解析\njdk : eventlistener监听器\nguava\npublic class guavaevent { @subscribe public void subscribe(string str){ //业务逻辑 system.out.println(\u0026#34;执行subscribe方法,传入的参数是:\u0026#34; + str); } } public class guavaeventtest { public static void main(string[] args) { eventbus eventbus = new eventbus(); guavaevent guavaevent = new guavaevent(); eventbus.register(guavaevent); eventbus.post(\u0026#34;post的内容\u0026#34;); } } 在使用guava来实现观察者模式的时候,可以直接使用@subscribe这个注解,将这个注解放在方法上边就直接实现了观察者;\n备忘录模式 定义:保存一个对象的某个状态,以便在适当的时候恢复对象;\n适用场景:保存及恢复数据相关的业务场景;后悔的时候,即想恢复到之前的状态;\n1 创建article\npublic class article { private string title; private string content; private string imgs; public article(string title, string content, string imgs) { this.title = title; this.content = content; this.imgs = imgs; } public string gettitle() { return title; } public void settitle(string title) { this.title = title; } public string getcontent() { return content; } public void setcontent(string content) { this.content = content; } public string getimgs() { return imgs; } public void setimgs(string imgs) { this.imgs = imgs; } public articlememento savetomemento() { articlememento articlememento = new articlememento(this.title,this.content,this.imgs); return articlememento; } public void undofrommemento(articlememento articlememento) { this.title = articlememento.gettitle(); this.content = articlememento.getcontent(); this.imgs = articlememento.getimgs(); } @override public string tostring() { return \u0026#34;article{\u0026#34; + \u0026#34;title=\u0026#39;\u0026#34; + title + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, content=\u0026#39;\u0026#34; + content + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, imgs=\u0026#39;\u0026#34; + imgs + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 2 创建articlememento\npublic class articlememento { private string title; private string content; private string imgs; public articlememento(string title, string content, string imgs) { this.title = title; this.content = content; this.imgs = imgs; } public string gettitle() { return title; } public string getcontent() { return content; } public string getimgs() { return imgs; } @override public string tostring() { return \u0026#34;articlememento{\u0026#34; + \u0026#34;title=\u0026#39;\u0026#34; + title + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, content=\u0026#39;\u0026#34; + content + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, imgs=\u0026#39;\u0026#34; + imgs + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 3 创建articlemementomanager\npublic class articlemementomanager { private final stack\u0026lt;articlememento\u0026gt; article_memento_stack = new stack\u0026lt;articlememento\u0026gt;(); public articlememento getmemento() { articlememento articlememento= article_memento_stack.pop(); return articlememento; } public void addmemento(articlememento articlememento) { article_memento_stack.push(articlememento); } } 4 创建测试类\npublic class test { public static void main(string[] args) { articlemementomanager articlemementomanager = new articlemementomanager(); article article= new article(\u0026#34;如影随行的设计模式a\u0026#34;,\u0026#34;手记内容a\u0026#34;,\u0026#34;手记图片a\u0026#34;); articlememento articlememento = article.savetomemento(); articlemementomanager.addmemento(articlememento); system.out.println(\u0026#34;标题:\u0026#34;+article.gettitle()+\u0026#34; 内容:\u0026#34;+article.getcontent()+\u0026#34; 图片:\u0026#34;+article.getimgs()+\u0026#34; 暂存成功\u0026#34;); system.out.println(\u0026#34;手记完整信息:\u0026#34;+article); system.out.println(\u0026#34;修改手记start\u0026#34;); article.settitle(\u0026#34;如影随行的设计模式b\u0026#34;); article.setcontent(\u0026#34;手记内容b\u0026#34;); article.setimgs(\u0026#34;手记图片b\u0026#34;); system.out.println(\u0026#34;修改手记end\u0026#34;); system.out.println(\u0026#34;手记完整信息:\u0026#34;+article); articlememento = article.savetomemento(); articlemementomanager.addmemento(articlememento); article.settitle(\u0026#34;如影随行的设计模式c\u0026#34;); article.setcontent(\u0026#34;手记内容c\u0026#34;); article.setimgs(\u0026#34;手记图片c\u0026#34;); system.out.println(\u0026#34;暂存回退start\u0026#34;); system.out.println(\u0026#34;回退出栈1次\u0026#34;); articlememento = articlemementomanager.getmemento(); article.undofrommemento(articlememento); system.out.println(\u0026#34;回退出栈2次\u0026#34;); articlememento = articlemementomanager.getmemento(); article.undofrommemento(articlememento); system.out.println(\u0026#34;暂存回退end\u0026#34;); system.out.println(\u0026#34;手记完整信息:\u0026#34;+article); } } 5 uml类图\n备忘录模式源码解析\norg.springframework.webflow\n命令模式 定义 : 将请求封装成对象以便使用不同的请求;命令模式解决了应用程序中对象的职责以及他们之间的通信方式;\n适用场景:请求调用者和请求接收者需要解耦,使得调用者和接收者不直接交互;需要抽象出等待执行的行为\n1 创建command接口\npublic interface command { void execute(); } 2 创建coursevideo类\npublic class coursevideo { private string name; public coursevideo(string name) { this.name = name; } public void open(){ system.out.println(this.name+\u0026#34;课程视频开放\u0026#34;); } public void close(){ system.out.println(this.name+\u0026#34;课程视频关闭\u0026#34;); } } 3 创建opencoursevideocommand类\npublic class opencoursevideocommand implements command { private coursevideo coursevideo; public opencoursevideocommand(coursevideo coursevideo) { this.coursevideo = coursevideo; } @override public void execute() { coursevideo.open(); } } 4 创建closecoursevideocommand类\npublic class closecoursevideocommand implements command { private coursevideo coursevideo; public closecoursevideocommand(coursevideo coursevideo) { this.coursevideo = coursevideo; } @override public void execute() { coursevideo.close(); } } 5 声明staff类\npublic class staff { private list\u0026lt;command\u0026gt; commandlist = new arraylist\u0026lt;command\u0026gt;(); public void addcommand(command command){ commandlist.add(command); } public void executecommands(){ for(command command : commandlist){ command.execute(); } commandlist.clear(); } } 6 uml类图\n7 编写测试类\npublic class test { public static void main(string[] args) { coursevideo coursevideo = new coursevideo(\u0026#34;java设计模式\u0026#34;); opencoursevideocommand opencoursevideocommand = new opencoursevideocommand(coursevideo); closecoursevideocommand closecoursevideocommand = new closecoursevideocommand(coursevideo); staff staff = new staff(); staff.addcommand(opencoursevideocommand); staff.addcommand(closecoursevideocommand); staff.executecommands(); } } 命令模式源码解析\npublic interface runnable\n*中介者模式 定义 : 定义了一个封装一组对象如何交互的对象;通过使对象明确的相互引用来促进松散耦合,并允许独立的改变地它们的交互;\n适用场景:系统中对象之间存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解;交互的公共行为,如果需要改变行为则可以增加新的中介者类;\n**意图:**用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互\n1 创建中介类\npublic class chatroom { public static void showmessage(user user, string message){ system.out.println(new date().tostring() + \u0026#34; [\u0026#34; + user.getname() +\u0026#34;] : \u0026#34; + message); } } 2 创建 user 类\npublic class user { private string name; public string getname() { return name; } public void setname(string name) { this.name = name; } public user(string name){ this.name = name; } public void sendmessage(string message){ chatroom.showmessage(this,message); } } 3 使用 user 对象来显示他们之间的通信\npublic class mediatorpatterndemo { public static void main(string[] args) { user robert = new user(\u0026#34;robert\u0026#34;); user john = new user(\u0026#34;john\u0026#34;); robert.sendmessage(\u0026#34;hi! john!\u0026#34;); john.sendmessage(\u0026#34;hello! robert!\u0026#34;); } } thu jan 31 16:05:46 ist 2013 [robert] : hi! john! thu jan 31 16:05:46 ist 2013 [john] : hello! robert! 4 uml图\n中介者模式源码解析\njava.util.timer\n责任链模式 定义 : 为请求创建一个接收此次请求对象的链;\n适用场景:一个请求的处理需要多个对象当中的一个或几个协作处理;\n**意图:**避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。\n1 编写问题类\npublic class trouble { private int number; public trouble(int number) { this.number = number; } public int getnumber() { return number; } @override public string tostring() { return \u0026#34;trouble{\u0026#34; + \u0026#34;number=\u0026#34; + number + \u0026#39;}\u0026#39;; } } 2 编写support抽象类\npublic abstract class support { private string name; private support next; public support(string name) { this.name = name; } public support setnext(support next) { this.next = next; return next; } public final void support(trouble trouble) { if (resolve(trouble)) { done(trouble); } else if (next != null) { next.support(trouble); } else { fail(trouble); } } protected void fail(trouble trouble) { system.out.println(trouble + \u0026#34; cannot be resolved.\u0026#34;); } protected void done(trouble trouble) { system.out.println(trouble + \u0026#34;is resolved by \u0026#34; + this + \u0026#34;.\u0026#34;); } /** * 具体解决方案由实现类编写 */ protected abstract boolean resolve(trouble trouble); @override public string tostring() { return \u0026#34;support{\u0026#34; +name + \u0026#39;}\u0026#39;; } } 3 编写limitsupport类\npublic class limitsupport extends support { private int limit; public limitsupport(string name, int limit) { super(name); this.limit = limit; } @override public boolean resolve(trouble trouble) { if (trouble.getnumber() \u0026lt;= limit) { return true; } return false; } } 4 nosupport类\npublic class nosupport extends support { public nosupport(string name) { super(name); } @override public boolean resolve(trouble trouble) { return false; } } 5 oddsupport类\npublic class oddsupport extends support { public oddsupport(string name) { super(name); } @override public boolean resolve(trouble trouble) { if(trouble.getnumber() % 2 == 1){ return true; } return false; } } 6 specialsupport类\npublic class specialsupport extends support { private int number; public specialsupport(string name, int number) { super(name); this.number = number; } @override public boolean resolve(trouble trouble) { if (trouble.getnumber() == number) { return true; } return false; } } 7 测试方法\npublic class main { public static void main(string[] args) { nosupport arrow = new nosupport(\u0026#34;arrow\u0026#34;); limitsupport bob = new limitsupport(\u0026#34;bob\u0026#34;, 100); specialsupport charlie = new specialsupport(\u0026#34;charlie\u0026#34;, 429); limitsupport diana = new limitsupport(\u0026#34;diana\u0026#34;, 200); oddsupport elli = new oddsupport(\u0026#34;elli\u0026#34;); limitsupport free = new limitsupport(\u0026#34;free\u0026#34;, 300); arrow.setnext(bob).setnext(charlie).setnext(diana).setnext(elli).setnext(free); for (int i = 0; i \u0026lt; 500; i += 33) { arrow.support(new trouble(i)); } } } 责任链模式源码解析\njavax.servlet.filter filterchain\n*访问者模式 定义 : 封装作用于某种数据结构(如list / set / map等)中的各元素的操作,可以在不改变各元素的类的前提下定义作用于这些元素的操作;\n适用场景:一个数据结构(如list / set / map等)包含很多类型对象;数据结构与数据操作分离;\n**意图:**主要将数据结构与数据操作分离。\n1 创建抽象类course\npublic abstract class course { private string name; public string getname() { return name; } public void setname(string name) { this.name = name; } public abstract void accept(ivisitor visitor); } 2 创建freecourse继承course\npublic class freecourse extends course { @override public void accept(ivisitor visitor) { visitor.visit(this); } } 3 创建codingcourse继承course\npublic class codingcourse extends course { private int price; public int getprice() { return price; } public void setprice(int price) { this.price = price; } @override public void accept(ivisitor visitor) { visitor.visit(this); } } 4 创建ivisitor接口\npublic interface ivisitor { void visit(freecourse freecourse); void visit(codingcourse codingcourse); } 5 创建visitor实现ivisitor\npublic class visitor implements ivisitor { //访问免费课程,打印所有免费课程名称 @override public void visit(freecourse freecourse) { system.out.println(\u0026#34;免费课程:\u0026#34;+freecourse.getname()); } //访问实战课程,打印所有实战课程名称及价格 @override public void visit(codingcourse codingcourse) { system.out.println(\u0026#34;实战课程:\u0026#34;+codingcourse.getname()+\u0026#34; 价格:\u0026#34;+codingcourse.getprice()+\u0026#34;元\u0026#34;); } } 6 uml类图\n7 编写测试类\npublic class test { public static void main(string[] args) { list\u0026lt;course\u0026gt; courselist = new arraylist\u0026lt;course\u0026gt;(); freecourse freecourse = new freecourse(); freecourse.setname(\u0026#34;springmvc数据绑定\u0026#34;); codingcourse codingcourse = new codingcourse(); codingcourse.setname(\u0026#34;java设计模式精讲 -- by geely\u0026#34;); codingcourse.setprice(299); courselist.add(freecourse); courselist.add(codingcourse); for(course course : courselist){ course.accept(new visitor()); } } } 状态模式 定义 : 允许一个对象在其内部状态改变时,改变它的行为;\n使用场景:一个对象存在多个状态(不同状态下行为不同)且状态可相互切换;\n**主要解决:**对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。\n1 创建coursevideostate抽象类\npublic abstract class coursevideostate { protected coursevideocontext coursevideocontext; public void setcoursevideocontext(coursevideocontext coursevideocontext) { this.coursevideocontext = coursevideocontext; } public abstract void play(); public abstract void speed(); public abstract void pause(); public abstract void stop(); } 2 创建coursevideocontext类\npublic class coursevideocontext { private coursevideostate coursevideostate; public final static playstate play_state = new playstate(); public final static stopstate stop_state = new stopstate(); public final static pausestate pause_state = new pausestate(); public final static speedstate speed_state = new speedstate(); public coursevideostate getcoursevideostate() { return coursevideostate; } public void setcoursevideostate(coursevideostate coursevideostate) { this.coursevideostate = coursevideostate; this.coursevideostate.setcoursevideocontext(this); } public void play(){ this.coursevideostate.play(); } public void speed(){ this.coursevideostate.speed(); } public void stop(){ this.coursevideostate.stop(); } public void pause(){ this.coursevideostate.pause(); } } 3 创建不同的状态类\n/** * 暂停状态 */ public class pausestate extends coursevideostate { @override public void play() { super.coursevideocontext.setcoursevideostate(coursevideocontext.play_state); } @override public void speed() { super.coursevideocontext.setcoursevideostate(coursevideocontext.speed_state); } @override public void pause() { system.out.println(\u0026#34;暂停播放课程视频状态\u0026#34;); } @override public void stop() { super.coursevideocontext.setcoursevideostate(coursevideocontext.stop_state); } } /** * 播放状态 */ public class playstate extends coursevideostate { @override public void play() { system.out.println(\u0026#34;正常播放课程视频状态\u0026#34;); } @override public void speed() { super.coursevideocontext.setcoursevideostate(coursevideocontext.speed_state); } @override public void pause() { super.coursevideocontext.setcoursevideostate(coursevideocontext.pause_state); } @override public void stop() { super.coursevideocontext.setcoursevideostate(coursevideocontext.stop_state); } } /** * 快进状态 */ public class speedstate extends coursevideostate { @override public void play() { super.coursevideocontext.setcoursevideostate(coursevideocontext.play_state); } @override public void speed() { system.out.println(\u0026#34;快进播放课程视频状态\u0026#34;); } @override public void pause() { super.coursevideocontext.setcoursevideostate(coursevideocontext.pause_state); } @override public void stop() { super.coursevideocontext.setcoursevideostate(coursevideocontext.stop_state); } } /** * 停止状态 */ public class stopstate extends coursevideostate { @override public void play() { super.coursevideocontext.setcoursevideostate(coursevideocontext.play_state); } @override public void speed() { system.out.println(\u0026#34;error 停止状态不能快进!!\u0026#34;); } @override public void pause() { system.out.println(\u0026#34;error 停止状态不能暂停!!\u0026#34;); } @override public void stop() { system.out.println(\u0026#34;停止播放课程视频状态\u0026#34;); } } 4 uml类图\n5 编写测试类\npublic class test { public static void main(string[] args) { coursevideocontext coursevideocontext = new coursevideocontext(); coursevideocontext.setcoursevideostate(new playstate()); system.out.println(\u0026#34;当前状态:\u0026#34;+coursevideocontext.getcoursevideostate().getclass().getsimplename()); coursevideocontext.pause(); system.out.println(\u0026#34;当前状态:\u0026#34;+coursevideocontext.getcoursevideostate().getclass().getsimplename()); coursevideocontext.speed(); system.out.println(\u0026#34;当前状态:\u0026#34;+coursevideocontext.getcoursevideostate().getclass().getsimplename()); coursevideocontext.stop(); system.out.println(\u0026#34;当前状态:\u0026#34;+coursevideocontext.getcoursevideostate().getclass().getsimplename()); coursevideocontext.speed(); } } 状态模式源码解析\njsf : javax.faces.lifecycle execute()\nj2ee 模式 mvc 模式 mvc 模式代表 model-view-controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。\nmodel(模型) - 模型代表一个存取数据的对象或 java pojo。它也可以带有逻辑,在数据变化时更新控制器。 view(视图) - 视图代表模型包含的数据的可视化。 controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开 1 uml图\n2 创建模型\npublic class student { private string rollno; private string name; public string getrollno() { return rollno; } public void setrollno(string rollno) { this.rollno = rollno; } public string getname() { return name; } public void setname(string name) { this.name = name; } } 3 创建视图\npublic class studentview { public void printstudentdetails(string studentname, string studentrollno){ system.out.println(\u0026#34;student: \u0026#34;); system.out.println(\u0026#34;name: \u0026#34; + studentname); system.out.println(\u0026#34;roll no: \u0026#34; + studentrollno); } } 4 创建控制器\npublic class studentcontroller { private student model; private studentview view; public studentcontroller(student model, studentview view){ this.model = model; this.view = view; } public void setstudentname(string name){ model.setname(name); } public string getstudentname(){ return model.getname(); } public void setstudentrollno(string rollno){ model.setrollno(rollno); } public string getstudentrollno(){ return model.getrollno(); } public void updateview(){ view.printstudentdetails(model.getname(), model.getrollno()); } } 5 使用 studentcontroller 方法来演示 mvc 设计模式的用法。\npublic class mvcpatterndemo { public static void main(string[] args) { //从数据库获取学生记录 student model = retrievestudentfromdatabase(); //创建一个视图:把学生详细信息输出到控制台 studentview view = new studentview(); studentcontroller controller = new studentcontroller(model, view); controller.updateview(); //更新模型数据 controller.setstudentname(\u0026#34;john\u0026#34;); controller.updateview(); } private static student retrievestudentfromdatabase(){ student student = new student(); student.setname(\u0026#34;robert\u0026#34;); student.setrollno(\u0026#34;10\u0026#34;); return student; } } //执行程序,输出结果: student: name: robert roll no: 10 student: name: john roll no: 10 *业务代表模式 业务代表模式(business delegate pattern)用于对表示层和业务层解耦。它基本上是用来减少通信或对表示层代码中的业务层代码的远程查询功能。在业务层中我们有以下实体。\n客户端(client) - 表示层代码可以是 jsp、servlet 或 ui java 代码。 业务代表(business delegate) - 一个为客户端实体提供的入口类,它提供了对业务服务方法的访问。 查询服务(lookup service) - 查找服务对象负责获取相关的业务实现,并提供业务对象对业务代表对象的访问。 业务服务(business service) - 业务服务接口。实现了该业务服务的实体类,提供了实际的业务实现逻辑。 创建 businessservice 接口\npublic interface businessservice { public void doprocessing(); } 创建实体服务类\npublic class ejbservice implements businessservice { @override public void doprocessing() { system.out.println(\u0026#34;processing task by invoking ejb service\u0026#34;); } } //**// public class jmsservice implements businessservice { @override public void doprocessing() { system.out.println(\u0026#34;processing task by invoking jms service\u0026#34;); } } 创建业务查询服务\npublic class businesslookup { public businessservice getbusinessservice(string servicetype){ if(servicetype.equalsignorecase(\u0026#34;ejb\u0026#34;)){ return new ejbservice(); }else { return new jmsservice(); } } } 创建业务代表\npublic class businessdelegate { private businesslookup lookupservice = new businesslookup(); private businessservice businessservice; private string servicetype; public void setservicetype(string servicetype){ this.servicetype = servicetype; } public void dotask(){ businessservice = lookupservice.getbusinessservice(servicetype); businessservice.doprocessing(); } } 创建客户端\npublic class client { businessdelegate businessservice; public client(businessdelegate businessservice){ this.businessservice = businessservice; } public void dotask(){ businessservice.dotask(); } } 使用 businessdelegate 和 client 类来演示业务代表模式\npublic class businessdelegatepatterndemo { public static void main(string[] args) { businessdelegate businessdelegate = new businessdelegate(); businessdelegate.setservicetype(\u0026#34;ejb\u0026#34;); client client = new client(businessdelegate); client.dotask(); businessdelegate.setservicetype(\u0026#34;jms\u0026#34;); client.dotask(); } } 执行程序,输出结果: processing task by invoking ejb service processing task by invoking jms service *组合实体模式 组合实体模式(composite entity pattern)用在 ejb 持久化机制中。一个组合实体是一个 ejb 实体 bean,代表了对象的图解。当更新一个组合实体时,内部依赖对象 beans 会自动更新,因为它们是由 ejb 实体 bean 管理的。以下是组合实体 bean 的参与者。\n组合实体(composite entity) - 它是主要的实体 bean。它可以是粗粒的,或者可以包含一个粗粒度对象,用于持续生命周期。 粗粒度对象(coarse-grained object) - 该对象包含依赖对象。它有自己的生命周期,也能管理依赖对象的生命周期。 依赖对象(dependent object) - 依赖对象是一个持续生命周期依赖于粗粒度对象的对象。 策略(strategies) - 策略表示如何实现组合实体。 创建依赖对象\npublic class dependentobject1 { private string data; public void setdata(string data){ this.data = data; } public string getdata(){ return data; } } //**// public class dependentobject2 { private string data; public void setdata(string data){ this.data = data; } public string getdata(){ return data; } } 创建粗粒度对象\npublic class coarsegrainedobject { dependentobject1 do1 = new dependentobject1(); dependentobject2 do2 = new dependentobject2(); public void setdata(string data1, string data2){ do1.setdata(data1); do2.setdata(data2); } public string[] getdata(){ return new string[] {do1.getdata(),do2.getdata()}; } } 创建组合实体\npublic class compositeentity { private coarsegrainedobject cgo = new coarsegrainedobject(); public void setdata(string data1, string data2){ cgo.setdata(data1, data2); } public string[] getdata(){ return cgo.getdata(); } } 创建使用组合实体的客户端类\npublic class client { private compositeentity compositeentity = new compositeentity(); public void printdata(){ for (int i = 0; i \u0026lt; compositeentity.getdata().length; i++) { system.out.println(\u0026#34;data: \u0026#34; + compositeentity.getdata()[i]); } } public void setdata(string data1, string data2){ compositeentity.setdata(data1, data2); } } 使用 client 来演示组合实体设计模式的用法\npublic class compositeentitypatterndemo { public static void main(string[] args) { client client = new client(); client.setdata(\u0026#34;test\u0026#34;, \u0026#34;data\u0026#34;); client.printdata(); client.setdata(\u0026#34;second test\u0026#34;, \u0026#34;data1\u0026#34;); client.printdata(); } } 执行程序,输出结果: data: test data: data data: second test data: data1 数据访问对象模式 数据访问对象模式(data access object pattern)或 dao 模式用于把低级的数据访问 api 或操作从高级的业务服务中分离出来。以下是数据访问对象模式的参与者。\n数据访问对象接口(data access object interface) - 该接口定义了在一个模型对象上要执行的标准操作。 数据访问对象实体类(data access object concrete class) - 该类实现了上述的接口。该类负责从数据源获取数据,数据源可以是数据库,也可以是 xml,或者是其他的存储机制。 模型对象/数值对象(model object/value object) - 该对象是简单的 pojo,包含了 get/set 方法来存储通过使用 dao 类检索到的数据 创建数值对象\npublic class student { private string name; private int rollno; student(string name, int rollno){ this.name = name; this.rollno = rollno; } public string getname() { return name; } public void setname(string name) { this.name = name; } public int getrollno() { return rollno; } public void setrollno(int rollno) { this.rollno = rollno; } } 创建数据访问对象接口\npublic interface studentdao { public list\u0026lt;student\u0026gt; getallstudents(); public student getstudent(int rollno); public void updatestudent(student student); public void deletestudent(student student); } 创建实现了上述接口的实体类\npublic class studentdaoimpl implements studentdao { //列表是当作一个数据库 list\u0026lt;student\u0026gt; students; public studentdaoimpl(){ students = new arraylist\u0026lt;student\u0026gt;(); student student1 = new student(\u0026#34;robert\u0026#34;,0); student student2 = new student(\u0026#34;john\u0026#34;,1); students.add(student1); students.add(student2); } @override public void deletestudent(student student) { students.remove(student.getrollno()); system.out.println(\u0026#34;student: roll no \u0026#34; + student.getrollno() +\u0026#34;, deleted from database\u0026#34;); } //从数据库中检索学生名单 @override public list\u0026lt;student\u0026gt; getallstudents() { return students; } @override public student getstudent(int rollno) { return students.get(rollno); } @override public void updatestudent(student student) { students.get(student.getrollno()).setname(student.getname()); system.out.println(\u0026#34;student: roll no \u0026#34; + student.getrollno() +\u0026#34;, updated in the database\u0026#34;); } } 使用 studentdao 来演示数据访问对象模式的用法\npublic class daopatterndemo { public static void main(string[] args) { studentdao studentdao = new studentdaoimpl(); //输出所有的学生 for (student student : studentdao.getallstudents()) { system.out.println(\u0026#34;student: [rollno : \u0026#34; +student.getrollno()+\u0026#34;, name : \u0026#34;+student.getname()+\u0026#34; ]\u0026#34;); } //更新学生 student student =studentdao.getallstudents().get(0); student.setname(\u0026#34;michael\u0026#34;); studentdao.updatestudent(student); //获取学生 studentdao.getstudent(0); system.out.println(\u0026#34;student: [rollno : \u0026#34; +student.getrollno()+\u0026#34;, name : \u0026#34;+student.getname()+\u0026#34; ]\u0026#34;); } } 执行程序,输出结果: student: [rollno : 0, name : robert ] student: [rollno : 1, name : john ] student: roll no 0, updated in the database student: [rollno : 0, name : michael ] 前端控制器模式 前端控制器模式(front controller pattern)是用来提供一个集中的请求处理机制,所有的请求都将由一个单一的处理程序处理。该处理程序可以做认证/授权/记录日志,或者跟踪请求,然后把请求传给相应的处理程序。以下是这种设计模式的实体。\n前端控制器(front controller) - 处理应用程序所有类型请求的单个处理程序,应用程序可以是基于 web 的应用程序,也可以是基于桌面的应用程序。 调度器(dispatcher) - 前端控制器可能使用一个调度器对象来调度请求到相应的具体处理程序。 视图(view) - 视图是为请求而创建的对象。 创建视图\npublic class homeview { public void show(){ system.out.println(\u0026#34;displaying home page\u0026#34;); } } //**// public class studentview { public void show(){ system.out.println(\u0026#34;displaying student page\u0026#34;); } } 创建调度器 dispatcher\npublic class dispatcher { private studentview studentview; private homeview homeview; public dispatcher(){ studentview = new studentview(); homeview = new homeview(); } public void dispatch(string request){ if(request.equalsignorecase(\u0026#34;student\u0026#34;)){ studentview.show(); }else{ homeview.show(); } } } 创建前端控制器 frontcontroller\npublic class frontcontroller { private dispatcher dispatcher; public frontcontroller(){ dispatcher = new dispatcher(); } private boolean isauthenticuser(){ system.out.println(\u0026#34;user is authenticated successfully.\u0026#34;); return true; } private void trackrequest(string request){ system.out.println(\u0026#34;page requested: \u0026#34; + request); } public void dispatchrequest(string request){ //记录每一个请求 trackrequest(request); //对用户进行身份验证 if(isauthenticuser()){ dispatcher.dispatch(request); } } } 使用 frontcontroller 来演示前端控制器设计模式\npublic class frontcontrollerpatterndemo { public static void main(string[] args) { frontcontroller frontcontroller = new frontcontroller(); frontcontroller.dispatchrequest(\u0026#34;home\u0026#34;); frontcontroller.dispatchrequest(\u0026#34;student\u0026#34;); } } 执行程序,输出结果: page requested: home user is authenticated successfully. displaying home page page requested: student user is authenticated successfully. displaying student page 拦截过滤器模式 拦截过滤器模式(intercepting filter pattern)用于对应用程序的请求或响应做一些预处理/后处理。定义过滤器,并在把请求传给实际目标应用程序之前应用在请求上。过滤器可以做认证/授权/记录日志,或者跟踪请求,然后把请求传给相应的处理程序。以下是这种设计模式的实体。\n过滤器(filter) - 过滤器在请求处理程序执行请求之前或之后,执行某些任务。 过滤器链(filter chain) - 过滤器链带有多个过滤器,并在 target 上按照定义的顺序执行这些过滤器。 target - target 对象是请求处理程序。 过滤管理器(filter manager) - 过滤管理器管理过滤器和过滤器链。 客户端(client) - client 是向 target 对象发送请求的对象。 创建过滤器接口 filter\npublic interface filter { public void execute(string request); } 创建实体过滤器\npublic class authenticationfilter implements filter { public void execute(string request){ system.out.println(\u0026#34;authenticating request: \u0026#34; + request); } } //**// public class debugfilter implements filter { public void execute(string request){ system.out.println(\u0026#34;request log: \u0026#34; + request); } } 创建 target\npublic class target { public void execute(string request){ system.out.println(\u0026#34;executing request: \u0026#34; + request); } } 创建过滤器链\npublic class filterchain { private list\u0026lt;filter\u0026gt; filters = new arraylist\u0026lt;filter\u0026gt;(); private target target; public void addfilter(filter filter){ filters.add(filter); } public void execute(string request){ for (filter filter : filters) { filter.execute(request); } target.execute(request); } public void settarget(target target){ this.target = target; } } 创建过滤管理器\npublic class filtermanager { filterchain filterchain; public filtermanager(target target){ filterchain = new filterchain(); filterchain.settarget(target); } public void setfilter(filter filter){ filterchain.addfilter(filter); } public void filterrequest(string request){ filterchain.execute(request); } } 创建客户端 client\npublic class client { filtermanager filtermanager; public void setfiltermanager(filtermanager filtermanager){ this.filtermanager = filtermanager; } public void sendrequest(string request){ filtermanager.filterrequest(request); } } 使用 client 来演示拦截过滤器设计模式\npublic class interceptingfilterdemo { public static void main(string[] args) { filtermanager filtermanager = new filtermanager(new target()); filtermanager.setfilter(new authenticationfilter()); filtermanager.setfilter(new debugfilter()); client client = new client(); client.setfiltermanager(filtermanager); client.sendrequest(\u0026#34;home\u0026#34;); } } 执行程序,输出结果: authenticating request: home request log: home executing request: home *服务定位器模式 服务定位器模式(service locator pattern)用在我们想使用 jndi 查询定位各种服务的时候。考虑到为某个服务查找 jndi 的代价很高,服务定位器模式充分利用了缓存技术。在首次请求某个服务时,服务定位器在 jndi 中查找服务,并缓存该服务对象。当再次请求相同的服务时,服务定位器会在它的缓存中查找,这样可以在很大程度上提高应用程序的性能。以下是这种设计模式的实体。\n服务(service) - 实际处理请求的服务。对这种服务的引用可以在 jndi 服务器中查找到。 context / 初始的 context - jndi context 带有对要查找的服务的引用。 服务定位器(service locator) - 服务定位器是通过 jndi 查找和缓存服务来获取服务的单点接触。 缓存(cache) - 缓存存储服务的引用,以便复用它们。 客户端(client) - client 是通过 servicelocator 调用服务的对象。 创建服务接口 service\npublic interface service { public string getname(); public void execute(); } 创建实体服务\npublic class service1 implements service { public void execute(){ system.out.println(\u0026#34;executing service1\u0026#34;); } @override public string getname() { return \u0026#34;service1\u0026#34;; } } //**// public class service2 implements service { public void execute(){ system.out.println(\u0026#34;executing service2\u0026#34;); } @override public string getname() { return \u0026#34;service2\u0026#34;; } } 为 jndi 查询创建 initialcontext\npublic class initialcontext { public object lookup(string jndiname){ if(jndiname.equalsignorecase(\u0026#34;service1\u0026#34;)){ system.out.println(\u0026#34;looking up and creating a new service1 object\u0026#34;); return new service1(); }else if (jndiname.equalsignorecase(\u0026#34;service2\u0026#34;)){ system.out.println(\u0026#34;looking up and creating a new service2 object\u0026#34;); return new service2(); } return null; } } 创建缓存 cache\npublic class cache { private list\u0026lt;service\u0026gt; services; public cache(){ services = new arraylist\u0026lt;service\u0026gt;(); } public service getservice(string servicename){ for (service service : services) { if(service.getname().equalsignorecase(servicename)){ system.out.println(\u0026#34;returning cached \u0026#34;+servicename+\u0026#34; object\u0026#34;); return service; } } return null; } public void addservice(service newservice){ boolean exists = false; for (service service : services) { if(service.getname().equalsignorecase(newservice.getname())){ exists = true; } } if(!exists){ services.add(newservice); } } } 创建服务定位器\npublic class servicelocator { private static cache cache; static { cache = new cache(); } public static service getservice(string jndiname){ service service = cache.getservice(jndiname); if(service != null){ return service; } initialcontext context = new initialcontext(); service service1 = (service)context.lookup(jndiname); cache.addservice(service1); return service1; } } 使用 servicelocator 来演示服务定位器设计模式\npublic class servicelocatorpatterndemo { public static void main(string[] args) { service service = servicelocator.getservice(\u0026#34;service1\u0026#34;); service.execute(); service = servicelocator.getservice(\u0026#34;service2\u0026#34;); service.execute(); service = servicelocator.getservice(\u0026#34;service1\u0026#34;); service.execute(); service = servicelocator.getservice(\u0026#34;service2\u0026#34;); service.execute(); } } 执行程序,输出结果: looking up and creating a new service1 object executing service1 looking up and creating a new service2 object executing service2 returning cached service1 object executing service1 returning cached service2 object executing service2 传输对象模式 传输对象模式(transfer object pattern)用于从客户端向服务器一次性传递带有多个属性的数据。传输对象也被称为数值对象。传输对象是一个具有 getter/setter 方法的简单的 pojo 类,它是可序列化的,所以它可以通过网络传输。它没有任何的行为。服务器端的业务类通常从数据库读取数据,然后填充 pojo,并把它发送到客户端或按值传递它。对于客户端,传输对象是只读的。客户端可以创建自己的传输对象,并把它传递给服务器,以便一次性更新数据库中的数值。以下是这种设计模式的实体。\n业务对象(business object) - 为传输对象填充数据的业务服务。 传输对象(transfer object) - 简单的 pojo,只有设置/获取属性的方法。 客户端(client) - 客户端可以发送请求或者发送传输对象到业务对象。 创建传输对象\npublic class studentvo { private string name; private int rollno; studentvo(string name, int rollno){ this.name = name; this.rollno = rollno; } public string getname() { return name; } public void setname(string name) { this.name = name; } public int getrollno() { return rollno; } public void setrollno(int rollno) { this.rollno = rollno; } } 创建业务对象\npublic class studentbo { //列表是当作一个数据库 list\u0026lt;studentvo\u0026gt; students; public studentbo(){ students = new arraylist\u0026lt;studentvo\u0026gt;(); studentvo student1 = new studentvo(\u0026#34;robert\u0026#34;,0); studentvo student2 = new studentvo(\u0026#34;john\u0026#34;,1); students.add(student1); students.add(student2); } public void deletestudent(studentvo student) { students.remove(student.getrollno()); system.out.println(\u0026#34;student: roll no \u0026#34; + student.getrollno() +\u0026#34;, deleted from database\u0026#34;); } //从数据库中检索学生名单 public list\u0026lt;studentvo\u0026gt; getallstudents() { return students; } public studentvo getstudent(int rollno) { return students.get(rollno); } public void updatestudent(studentvo student) { students.get(student.getrollno()).setname(student.getname()); system.out.println(\u0026#34;student: roll no \u0026#34; + student.getrollno() +\u0026#34;, updated in the database\u0026#34;); } } 使用 studentbo 来演示传输对象设计模式\npublic class transferobjectpatterndemo { public static void main(string[] args) { studentbo studentbusinessobject = new studentbo(); //输出所有的学生 for (studentvo student : studentbusinessobject.getallstudents()) { system.out.println(\u0026#34;student: [rollno : \u0026#34; +student.getrollno()+\u0026#34;, name : \u0026#34;+student.getname()+\u0026#34; ]\u0026#34;); } //更新学生 studentvo student =studentbusinessobject.getallstudents().get(0); student.setname(\u0026#34;michael\u0026#34;); studentbusinessobject.updatestudent(student); //获取学生 studentbusinessobject.getstudent(0); system.out.println(\u0026#34;student: [rollno : \u0026#34; +student.getrollno()+\u0026#34;, name : \u0026#34;+student.getname()+\u0026#34; ]\u0026#34;); } } 执行程序,输出结果: student: [rollno : 0, name : robert ] student: [rollno : 1, name : john ] student: roll no 0, updated in the database student: [rollno : 0, name : michael ] ","date":"2023-03-31","permalink":"https://www.holatto.com/posts/design-pattern/","summary":"设计模式简介 设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般","title":"初学设计模式"},{"content":"1. springsecurity完整流程 springsecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。\n图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。\nusernamepasswordauthenticationfilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。\n**exceptiontranslationfilter:**处理过滤器链中抛出的任何accessdeniedexception和authenticationexception 。\n**filtersecurityinterceptor:**负责权限校验的过滤器。\nspringsecurity过滤器链中的过滤器及它们的顺序 2. 认证流程详解 概念速查:\nauthentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。\nauthenticationmanager接口:定义了认证authentication的方法\nuserdetailsservice接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。\nuserdetails接口:提供核心用户信息。通过userdetailsservice根据用户名获取处理的用户信息要封装成userdetails对象返回。然后将这些信息封装到authentication对象中。\n代码流程:\nsecurityconfig配置\n@configuration public class securityconfig extends websecurityconfigureradapter { @bean public passwordencoder passwordencoder(){ return new bcryptpasswordencoder(); } @override protected void configure(httpsecurity http) throws exception { http //关闭csrf .csrf().disable() //不通过session获取securitycontext .sessionmanagement().sessioncreationpolicy(sessioncreationpolicy.stateless) .and() .authorizerequests() // 对于登录接口 允许匿名访问 .antmatchers(\u0026#34;/user/login\u0026#34;).anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyrequest().authenticated(); } @bean @override public authenticationmanager authenticationmanagerbean() throws exception { return super.authenticationmanagerbean(); } } 登录:\n@restcontroller public class logincontroller { @autowired private userservice userservice; @postmapping(\u0026#34;/user/login\u0026#34;) public responseresult login(@requestbody user user){ return userservice.login(user); } } @service public class userserviceimpl implements userservice { @override public responseresult login(user user) { usernamepasswordauthenticationtoken authenticationtoken = new usernamepasswordauthenticationtoken(user.getusername(),user.getpassword()); //调用userdetailsserviceimpl authentication authenticate = authenticationmanager.authenticate(authenticationtoken); if(objects.isnull(authenticate)){ throw new runtimeexception(\u0026#34;用户名或密码错误\u0026#34;); } loginuser loginuser = (loginuser) authenticate.getprincipal(); string userid = loginuser.getuser().getid().tostring(); string jwt = jwtutil.createjwt(userid); rediscache.setcacheobject(\u0026#34;login:\u0026#34;+userid,loginuser); //把token响应给前端 hashmap\u0026lt;string,string\u0026gt; map = new hashmap\u0026lt;\u0026gt;(10); map.put(\u0026#34;token\u0026#34;,jwt); return new responseresult(200,\u0026#34;登陆成功\u0026#34;,map); } } 创建一个类实现userdetailsservice接口,重写其中的方法。使用户名从数据库中查询用户信息 @service public class userdetailsserviceimpl implements userdetailsservice { @autowired private usermapper usermapper; @autowired private menumapper menumapper; @override public userdetails loaduserbyusername(string s) throws usernamenotfoundexception { lambdaquerywrapper\u0026lt;user\u0026gt; wrapper = new lambdaquerywrapper\u0026lt;\u0026gt;(); wrapper.eq(user::getusername,s); user user = usermapper.selectone(wrapper); if(objects.isnull(user)){ throw new runtimeexception(\u0026#34;用户名或密码错误\u0026#34;); } //todo 查询权限信息 list\u0026lt;string\u0026gt; list = menumapper.selectpermsbyuserid(user.getid()); return new loginuser(user,list); } } 因为userdetailsservice方法的返回值是userdetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。 @data @noargsconstructor @allargsconstructor public class loginuser implements userdetails { private user user; @override public collection\u0026lt;? extends grantedauthority\u0026gt; getauthorities() { return null; } @override public string getpassword() { return user.getpassword(); } @override public string getusername() { return user.getusername(); } @override public boolean isaccountnonexpired() { return true; } @override public boolean isaccountnonlocked() { return true; } @override public boolean iscredentialsnonexpired() { return true; } @override public boolean isenabled() { return true; } } 2.1密码加密存储 实际项目中我们不会把密码明文存储在数据库中。\n\t默认使用的passwordencoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换passwordencoder。\n\t我们一般使用springsecurity为我们提供的bcryptpasswordencoder。\n\t我们只需要使用把bcryptpasswordencoder对象注入spring容器中,springsecurity就会使用该passwordencoder来进行密码校验。\n\t我们可以定义一个springsecurity的配置类,springsecurity要求这个配置类要继承websecurityconfigureradapter。\n@configuration public class securityconfig extends websecurityconfigureradapter { @bean public passwordencoder passwordencoder(){ return new bcryptpasswordencoder(); } } 2.2 认证过滤器 \t我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。\n\t使用userid去redis中获取对应的loginuser对象。\n\t然后封装authentication对象存入securitycontextholder\n@component public class jwtauthenticationtokenfilter extends onceperrequestfilter { @autowired private rediscache rediscache; @override protected void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain filterchain) throws servletexception, ioexception { //获取token string token = request.getheader(\u0026#34;token\u0026#34;); if (!stringutils.hastext(token)) { //放行 filterchain.dofilter(request, response); return; } //解析token string userid; try { claims claims = jwtutil.parsejwt(token); userid = claims.getsubject(); } catch (exception e) { e.printstacktrace(); throw new runtimeexception(\u0026#34;token非法\u0026#34;); } //从redis中获取用户信息 string rediskey = \u0026#34;login:\u0026#34; + userid; loginuser loginuser = rediscache.getcacheobject(rediskey); if(objects.isnull(loginuser)){ throw new runtimeexception(\u0026#34;用户未登录\u0026#34;); } //存入securitycontextholder //todo 获取权限信息封装到authentication中 usernamepasswordauthenticationtoken authenticationtoken = new usernamepasswordauthenticationtoken(loginuser,null,null); securitycontextholder.getcontext().setauthentication(authenticationtoken); //放行 filterchain.dofilter(request, response); } } @configuration public class securityconfig extends websecurityconfigureradapter {\t@bean @override public authenticationmanager authenticationmanagerbean() throws exception { return super.authenticationmanagerbean(); } } 2.3 退出登陆 @service public class loginserviceimpl implements loginservcie { @autowired private authenticationmanager authenticationmanager; @autowired private rediscache rediscache; @override public responseresult login(user user) { usernamepasswordauthenticationtoken authenticationtoken = new usernamepasswordauthenticationtoken(user.getusername(),user.getpassword()); authentication authenticate = authenticationmanager.authenticate(authenticationtoken); if(objects.isnull(authenticate)){ throw new runtimeexception(\u0026#34;用户名或密码错误\u0026#34;); } //使用userid生成token loginuser loginuser = (loginuser) authenticate.getprincipal(); string userid = loginuser.getuser().getid().tostring(); string jwt = jwtutil.createjwt(userid); //authenticate存入redis rediscache.setcacheobject(\u0026#34;login:\u0026#34;+userid,loginuser); //把token响应给前端 hashmap\u0026lt;string,string\u0026gt; map = new hashmap\u0026lt;\u0026gt;(); map.put(\u0026#34;token\u0026#34;,jwt); return new responseresult(200,\u0026#34;登陆成功\u0026#34;,map); } @override public responseresult logout() { authentication authentication = securitycontextholder.getcontext().getauthentication(); loginuser loginuser = (loginuser) authentication.getprincipal(); long userid = loginuser.getuser().getid(); rediscache.deleteobject(\u0026#34;login:\u0026#34;+userid); return new responseresult(200,\u0026#34;退出成功\u0026#34;); } } 3. 授权 总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。\n3.1 授权基本流程 \t在springsecurity中,会使用默认的filtersecurityinterceptor来进行权限校验。在filtersecurityinterceptor中会从securitycontextholder获取其中的authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。\n\t所以我们在项目中只需要把当前登录用户的权限信息也存入authentication。\n\t然后设置我们的资源所需要的权限即可。\n3.2 授权实现 先开启相关配置\n@enableglobalmethodsecurity(prepostenabled = true) 然后就可以使用对应的注解。@preauthorize\n@restcontroller public class hellocontroller { @requestmapping(\u0026#34;/hello\u0026#34;) @preauthorize(\u0026#34;hasauthority(\u0026#39;test\u0026#39;)\u0026#34;) public string hello(){ return \u0026#34;hello\u0026#34;; } } \t我们前面在写userdetailsserviceimpl的时候说过,在查询出用户后还要获取对应的权限信息,封装到userdetails中返回。\n\t我们先直接把权限信息写死封装到userdetails中进行测试。\n\t我们之前定义了userdetails的实现类loginuser,想要让其能封装权限信息就要对其进行修改。\n@data @noargsconstructor public class loginuser implements userdetails { private user user; //存储权限信息 private list\u0026lt;string\u0026gt; permissions; public loginuser(user user,list\u0026lt;string\u0026gt; permissions) { this.user = user; this.permissions = permissions; } //存储springsecurity所需要的权限信息的集合 @jsonfield(serialize = false) private list\u0026lt;grantedauthority\u0026gt; authorities; //框架调用此方法封装信息 @override public collection\u0026lt;? extends grantedauthority\u0026gt; getauthorities() { if(authorities!=null){ return authorities; } //把permissions中字符串类型的权限信息转换成grantedauthority对象存入authorities中 authorities = permissions.stream(). map(simplegrantedauthority::new) .collect(collectors.tolist()); return authorities; } @override public string getpassword() { return user.getpassword(); } @override public string getusername() { return user.getusername(); } @override public boolean isaccountnonexpired() { return true; } @override public boolean isaccountnonlocked() { return true; } @override public boolean iscredentialsnonexpired() { return true; } @override public boolean isenabled() { return true; } } 4. 自定义失败处理 我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道springsecurity的异常处理机制。\n\t在springsecurity中,如果我们在认证或者授权的过程中出现了异常会被exceptiontranslationfilter捕获到。在exceptiontranslationfilter中会去判断是认证失败还是授权失败出现的异常。\n\t如果是认证过程中出现的异常会被封装成authenticationexception然后调用authenticationentrypoint对象的方法去进行异常处理。\n\t如果是授权过程中出现的异常会被封装成accessdeniedexception然后调用accessdeniedhandler对象的方法去进行异常处理。\n\t所以如果我们需要自定义异常处理,我们只需要自定义authenticationentrypoint和accessdeniedhandler然后配置给springsecurity即可。\n认证 @component public class authenticationentrypointimpl implements authenticationentrypoint { @override public void commence(httpservletrequest request, httpservletresponse response, authenticationexception authexception) throws ioexception, servletexception { responseresult result = new responseresult(httpstatus.unauthorized.value(), \u0026#34;认证失败请重新登录\u0026#34;); string json = json.tojsonstring(result); webutils.renderstring(response,json); } } 授权 @component public class accessdeniedhandlerimpl implements accessdeniedhandler { @override public void handle(httpservletrequest request, httpservletresponse response, accessdeniedexception accessdeniedexception) throws ioexception, servletexception { responseresult result = new responseresult(httpstatus.forbidden.value(), \u0026#34;权限不足\u0026#34;); string json = json.tojsonstring(result); webutils.renderstring(response,json); } } 配置给springsecurity @autowired private authenticationentrypoint authenticationentrypoint; @autowired private accessdeniedhandler accessdeniedhandler; http.exceptionhandling().authenticationentrypoint(authenticationentrypoint). accessdeniedhandler(accessdeniedhandler); 5. 跨域 浏览器出于安全的考虑,使用 xmlhttprequest对象发起 http请求时必须遵守同源策略,否则就是跨域的http请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。\n\t前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。\n\t所以我们就要处理一下,让前端能进行跨域请求。\n①先对springboot配置,运行跨域请求\n@configuration public class corsconfig implements webmvcconfigurer { @override public void addcorsmappings(corsregistry registry) { // 设置允许跨域的路径 registry.addmapping(\u0026#34;/**\u0026#34;) // 设置允许跨域请求的域名 .allowedoriginpatterns(\u0026#34;*\u0026#34;) // 是否允许cookie .allowcredentials(true) // 设置允许的请求方式 .allowedmethods(\u0026#34;get\u0026#34;, \u0026#34;post\u0026#34;, \u0026#34;delete\u0026#34;, \u0026#34;put\u0026#34;) // 设置允许的header属性 .allowedheaders(\u0026#34;*\u0026#34;) // 跨域允许时间 .maxage(3600); } } ②开启springsecurity的跨域访问\n由于我们的资源都会收到springsecurity的保护,所以想要跨域访问还要让springsecurity运行跨域访问。\n@override protected void configure(httpsecurity http) throws exception { http //关闭csrf .csrf().disable() //不通过session获取securitycontext .sessionmanagement().sessioncreationpolicy(sessioncreationpolicy.stateless) .and() .authorizerequests() // 对于登录接口 允许匿名访问 .antmatchers(\u0026#34;/user/login\u0026#34;).anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyrequest().authenticated(); //添加过滤器 http.addfilterbefore(jwtauthenticationtokenfilter, usernamepasswordauthenticationfilter.class); //配置异常处理器 http.exceptionhandling() //配置认证失败处理器 .authenticationentrypoint(authenticationentrypoint) .accessdeniedhandler(accessdeniedhandler); //允许跨域 http.cors(); } 6. 其它权限校验方法 我们前面都是使用@preauthorize注解,然后在在其中使用的是hasauthority方法进行校验。springsecurity还为我们提供了其它方法例如:hasanyauthority,hasrole,hasanyrole等。\n@preauthorize(\u0026#34;hasanyauthority(\u0026#39;admin\u0026#39;,\u0026#39;test\u0026#39;,\u0026#39;system:dept:list\u0026#39;)\u0026#34;) public string hello(){ return \u0026#34;hello\u0026#34;; } \thasrole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 role_ 后再去比较。所以这种情况下要用用户对应的权限也要有 role_ 这个前缀才可以。\n@preauthorize(\u0026#34;hasrole(\u0026#39;system:dept:list\u0026#39;)\u0026#34;) public string hello(){ return \u0026#34;hello\u0026#34;; } 自定义权限校验方法 @component(\u0026#34;ex\u0026#34;) public class sgexpressionroot { public boolean hasauthority(string authority){ //获取当前用户的权限 authentication authentication = securitycontextholder.getcontext().getauthentication(); loginuser loginuser = (loginuser) authentication.getprincipal(); list\u0026lt;string\u0026gt; permissions = loginuser.getpermissions(); //判断用户权限集合中是否存在authority return permissions.contains(authority); } } \t在spel表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的hasauthority方法\n@requestmapping(\u0026#34;/hello\u0026#34;) @preauthorize(\u0026#34;@ex.hasauthority(\u0026#39;system:dept:list\u0026#39;)\u0026#34;) public string hello(){ return \u0026#34;hello\u0026#34;; } 基于配置的权限控制 @override protected void configure(httpsecurity http) throws exception { http .antmatchers(\u0026#34;/testcors\u0026#34;).hasauthority(\u0026#34;system:dept:list222\u0026#34;) } 7. csrf \tcsrf是指跨站请求伪造(cross-site request forgery),是web常见的攻击之一。\n\thttps://blog.csdn.net/freeking101/article/details/86537087\n\tspringsecurity去防止csrf攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。\n\t我们可以发现csrf攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以csrf攻击也就不用担心了。\n@override protected void configure(httpsecurity http) throws exception { http //关闭csrf .csrf().disable() } 8. 认证处理器 认证成功处理器 \t实际上在usernamepasswordauthenticationfilter进行登录认证的时候,如果登录成功了是会调用authenticationsuccesshandler的方法进行认证成功后的处理的。authenticationsuccesshandler就是登录成功处理器。\n@component public class sgsuccesshandler implements authenticationsuccesshandler { @override public void onauthenticationsuccess(httpservletrequest request, httpservletresponse response, authentication authentication) throws ioexception, servletexception { system.out.println(\u0026#34;认证成功了\u0026#34;); } } @configuration public class securityconfig extends websecurityconfigureradapter { @autowired private authenticationsuccesshandler successhandler; @override protected void configure(httpsecurity http) throws exception { http.formlogin().successhandler(successhandler); http.authorizerequests().anyrequest().authenticated(); } } 认证失败处理器 \t实际上在usernamepasswordauthenticationfilter进行登录认证的时候,如果认证失败了是会调用authenticationfailurehandler的方法进行认证失败后的处理的。authenticationfailurehandler就是登录失败处理器。\n\t我们也可以自己去自定义失败处理器进行失败后的相应处理。\n@component public class sgfailurehandler implements authenticationfailurehandler { @override public void onauthenticationfailure(httpservletrequest request, httpservletresponse response, authenticationexception exception) throws ioexception, servletexception { system.out.println(\u0026#34;认证失败了\u0026#34;); } } @configuration public class securityconfig extends websecurityconfigureradapter { @autowired private authenticationsuccesshandler successhandler; @autowired private authenticationfailurehandler failurehandler; @override protected void configure(httpsecurity http) throws exception { http.formlogin() // 配置认证成功处理器 .successhandler(successhandler) // 配置认证失败处理器 .failurehandler(failurehandler); http.authorizerequests().anyrequest().authenticated(); } } 登出成功处理器 @component public class sglogoutsuccesshandler implements logoutsuccesshandler { @override public void onlogoutsuccess(httpservletrequest request, httpservletresponse response, authentication authentication) throws ioexception, servletexception { system.out.println(\u0026#34;注销成功\u0026#34;); } } @configuration public class securityconfig extends websecurityconfigureradapter { @autowired private authenticationsuccesshandler successhandler; @autowired private authenticationfailurehandler failurehandler; @autowired private logoutsuccesshandler logoutsuccesshandler; @override protected void configure(httpsecurity http) throws exception { http.formlogin() // 配置认证成功处理器 .successhandler(successhandler) // 配置认证失败处理器 .failurehandler(failurehandler); http.logout() //配置注销成功处理器 .logoutsuccesshandler(logoutsuccesshandler); http.authorizerequests().anyrequest().authenticated(); } } ","date":"2023-03-31","permalink":"https://www.holatto.com/posts/spring-security/rookie/","summary":"1. SpringSecurity完整流程 SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。 图中只展示了核心过滤器,其它的非核","title":"初学spring security"},{"content":"mysql的数据目录 mysql8的主要目录结构 数据库文件的存放路径 mysql\u0026gt; show variables like \u0026#39;datadir\u0026#39;; +---------------+-----------------+ | variable_name | value | +---------------+-----------------+ | datadir | /var/lib/mysql/ | +---------------+-----------------+ 1 row in set (0.04 sec) 相关命令目录 相关命令目录:/usr/bin(mysqladmin、mysqlbinlog、mysqldump等命令)和/usr/sbin。\n配置文件目录 配置文件目录:/usr/share/mysql-8.0(命令及配置文件),/etc/mysql(如my.cnf)\n数据库和文件系统的关系 查看默认数据库 mysql mysql 系统自带的核心数据库,它存储了mysql的用户账户和权限信息,一些存储过程、事件的定 义信息,一些运行过程中产生的日志信息,一些帮助信息以及时区信息等。\ninformation_schema mysql 系统自带的数据库,这个数据库保存着mysql服务器 维护的所有其他数据库的信息 ,比如有 哪些表、哪些视图、哪些触发器、哪些列、哪些索引。这些信息并不是真实的用户数据,而是一些 描述性信息,有时候也称之为 元数据 。在系统数据库 information_schema 中提供了一些以 innodb_sys 开头的表,用于表示内部系统表。\nmysql\u0026gt; use information_schema; database changed mysql\u0026gt; show tables like \u0026#39;innodb_sys%\u0026#39;; +--------------------------------------------+ | tables_in_information_schema (innodb_sys%) | +--------------------------------------------+ | innodb_sys_datafiles | | innodb_sys_virtual | | innodb_sys_indexes | | innodb_sys_tables | | innodb_sys_fields | | innodb_sys_tablespaces | | innodb_sys_foreign_cols | | innodb_sys_columns | | innodb_sys_foreign | | innodb_sys_tablestats | +--------------------------------------------+ 10 rows in set (0.00 sec) performance_schema mysql 系统自带的数据库,这个数据库里主要保存mysql服务器运行过程中的一些状态信息,可以 用来 监控 mysql 服务的各类性能指标 。包括统计最近执行了哪些语句,在执行过程的每个阶段都 花费了多长时间,内存的使用情况等信息。\nsys mysql 系统自带的数据库,这个数据库主要是通过 视图 的形式把 information_schema 和 performance_schema 结合起来,帮助系统管理员和开发人员监控 mysql 的技术性能。\n表在文件系统中的表示 innodb存储引擎模式\n表结构 为了保存表结构, innodb 在 数据目录 下对应的数据库子目录下创建了一个专门用于 描述表结构的文 件 ,文件名是这样:表名.frm\n表中数据和索引 系统表空间(system tablespace)\n默认情况下,innodb会在数据目录下创建一个名为 ibdata1 、大小为 12m 的文件,这个文件就是对应 的 系统表空间 在文件系统上的表示。怎么才12m?注意这个文件是 自扩展文件 ,当不够用的时候它会自 己增加文件大小。\n独立表空间(file-per-table tablespace)\n在mysql5.6.6以及之后的版本中,innodb并不会默认的把各个表的数据存储到系统表空间中,而是为 每 一个表建立一个独立表空间 ,也就是说我们创建了多少个表,就有多少个独立表空间。使用 独立表空间 来 存储表数据的话,会在该表所属数据库对应的子目录下创建一个表示该独立表空间的文件,文件名和表 名相同,只不过添加了一个 .ibd 的扩展名而已,所以完整的文件名称长这样:表名.ibd\nmyisam存储引擎模式 表结构 在存储表结构方面, myisam 和 innodb 一样,也是在 数据目录 下对应的数据库子目录下创建了一个专 门用于描述表结构的文件:表名.frm\n表中数据和索引 test.frm 存储表结构 test.myd 存储数据 (mydata) test.myi 存储索引 (myindex) 小结 举例: 数据库a , 表b 。\n1、如果表b采用 innodb ,data\\a中会产生1个或者2个文件:\nb.frm :描述表结构文件,字段长度等 如果采用 系统表空间 模式的,数据信息和索引信息都存储在 ibdata1 中 如果采用 独立表空间 存储模式,data\\a中还会产生 b.ibd 文件(存储数据信息和索引信息) 此外:\n① mysql5.7 中会在data/a的目录下生成 db.opt 文件用于保存数据库的相关配置。比如:字符集、比较 规则。而mysql8.0不再提供db.opt文件。\n② mysql8.0中不再单独提供b.frm,而是合并在b.ibd文件中。\n2、如果表b采用 myisam ,data\\a中会产生3个文件:\nmysql5.7 中: b.frm :描述表结构文件,字段长度等。 mysql8.0 中 b.xxx.sdi :描述表结构文件,字段长度等 b.myd (mydata):数据信息文件,存储数据信息(如果采用独立表存储模式) b.myi (myindex):存放索引信息文件 用户与权限管理 用户管理 登录mysql服务器 启动mysql服务后,可以通过mysql命令来登录mysql服务器,命令如下:\nmysql –h hostname|hostip –p port –u username –p databasename –e \u0026#34;sql语句\u0026#34; 下面详细介绍命令中的参数:\n-h参数 后面接主机名或者主机ip,hostname为主机,hostip为主机ip。\n-p参数 后面接mysql服务的端口,通过该参数连接到指定的端口。mysql服务的默认端口是3306, 不使用该参数时自动连接到3306端口,port为连接的端口号。\n-u参数 后面接用户名,username为用户名。\n-p参数 会提示输入密码。\ndatabasename参数 指明登录到哪一个数据库中。如果没有该参数,就会直接登录到mysql数据库 中,然后可以使用use命令来选择数据库。\n-e参数 后面可以直接加sql语句。登录mysql服务器以后即可执行这个sql语句,然后退出mysql 服务器。\n#举例 mysql -uroot -p -hlocalhost -p3306 mysql -e \u0026#34;select host,user from user\u0026#34; 创建用户 create user语句的基本语法形式如下:\ncreate user 用户名 [identified by \u0026#39;密码\u0026#39;][,用户名 [identified by \u0026#39;密码\u0026#39;]]; 用户名参数表示新建用户的账户,由 用户(user) 和 主机名(host) 构成;\n“ ”表示可选,也就是说,可以指定用户登录时需要密码验证,也可以不指定密码验证,这样用户 可以直接登录。不过,不指定密码的方式不安全,不推荐使用。如果指定密码值,这里需要使用 identified by指定明文密码值。\ncreate user语句可以同时创建多个用户。\n#举例 create user zhang3 identified by \u0026#39;123123\u0026#39;; # 默认host是 % create user \u0026#39;kangshifu\u0026#39;@\u0026#39;localhost\u0026#39; identified by \u0026#39;123456\u0026#39;; 修改用户 update mysql.user set user=\u0026#39;li4\u0026#39; where user=\u0026#39;wang5\u0026#39;; flush privileges; 删除用户 使用drop user语句来删除用户时,必须用于drop user权限。drop user语句的基本语法形式如下:\ndrop user user[,user]…; 举例\ndrop user li4 ; # 默认删除host为%的用户 drop user \u0026#39;kangshifu\u0026#39;@\u0026#39;localhost\u0026#39;; mysql.user 表中 host和user为联合主键\n设置当前用户密码 使用set语句来修改当前用户密码\n使用root用户登录mysql后,可以使用set语句来修改密码,具体 sql语句如下:\nset password=\u0026#39;new_password\u0026#39;; 修改其它用户密码 使用set命令来修改普通用户的密码\n使用root用户登录到mysql服务器后,可以使用set语句来修改普 通用户的密码。set语句的代码如下:\nset password for \u0026#39;username\u0026#39;@\u0026#39;hostname\u0026#39;=\u0026#39;new_password\u0026#39;; 权限管理 权限列表 \t(1) create和drop权限 ,可以创建新的数据库和表,或删除(移掉)已有的数据库和表。如果将 mysql数据库中的drop权限授予某用户,用户就可以删除mysql访问权限保存的数据库。\n\t(2) select、insert、update和delete权限 允许在一个数据库现有的表上实施操作。\n\t(3) select权限 只有在它们真正从一个表中检索行时才被用到。\n\t(4) index权限 允许创建或删除索引,index适用于已 有的表。如果具有某个表的create权限,就可以在create table语句中包括索引定义。\n\t(5) alter权 限 可以使用alter table来更改表的结构和重新命名表。\n\t(6) create routine权限 用来创建保存的 程序(函数和程序),alter routine权限用来更改和删除保存的程序, execute权限 用来执行保存的 程序。\n\t(7) grant权限 允许授权给其他用户,可用于数据库、表和保存的程序。\n\t(8) file权限 使用 户可以使用load data infile和select \u0026hellip; into outfile语句读或写服务器上的文件,任何被授予file权 限的用户都能读或写mysql服务器上的任何文件(说明用户可以读任何数据库目录下的文件,因为服务 器可以访问这些文件)。\n授予权限的原则\n权限控制主要是出于安全因素,因此需要遵循以下几个 经验原则 :\n1、只授予能 满足需要的最小权限 ,防止用户干坏事。比如用户只是需要查询,那就只给select权限就可 以了,不要给用户赋予update、insert或者delete权限。 2、创建用户的时候 限制用户的登录主机 ,一般是限制成指定ip或者内网ip段。 3、为每个用户 设置满足密码复杂度的密码 。 \t4、 定期清理不需要的用户 ,回收权限或者删除用户。\n授予权限 授权命令:\ngrant 权限1,权限2,…权限n on 数据库名称.表名称 to 用户名@用户地址 [identified by ‘密码口令’]; 该权限如果发现没有该用户,则会直接新建一个用户。 例如:\n给li4用户用本地命令行方式,授予atguigudb这个库下的所有表的插删改查的权限。 grant select,insert,delete,update on atguigudb.* to li4@localhost ; 授予通过网络方式登录的joe用户 ,对所有库所有表的全部权限,密码设为123。注意这里唯独不包 括grant的权限 grant all privileges on *.* to joe@\u0026#39;%\u0026#39; identified by \u0026#39;123\u0026#39;; 查看权限 查看当前用户 show grants; # 或 show grants for current_user; # 或 show grants for current_user(); 查看某用户的全局权限 show grants for \u0026#39;user\u0026#39;@\u0026#39;主机地址\u0026#39; ; 收回权限 #语法 revoke 权限1,权限2,…权限n on 数据库名称.表名称 from 用户名@用户地址; #举例 #收回全库全表的所有权限 revoke all privileges on *.* from joe@\u0026#39;%\u0026#39;; #收回mysql库下的所有表的插删改查权限 revoke select,insert,update,delete on mysql.* from joe@localhost; 权限表 user表\nuser表是mysql中最重要的一个权限表, 记录用户账号和权限信息 ,有49个字段。如下图:\n1.范围列(或用户列)\nhost : 表示连接类\n% 表示所有远程通过 tcp方式的连接 ip 地址 如 (192.168.1.2、127.0.0.1) 通过制定ip地址进行的tcp方式的连接 机器名 通过制定网络中的机器名进行的tcp方式的连接 ::1 ipv6的本地ip地址,等同于ipv4的 127.0.0.1 localhost 本地方式通过命令行方式的连接 ,比如mysql -u xxx -p xxx 方式的连接。 user : 表示用户名,同一用户通过不同方式链接的权限是不一样的。\npassword : 密码\n所有密码串通过 password(明文字符串) 生成的密文字符串。mysql 8.0 在用户管理方面增加了 角色管理,默认的密码加密方式也做了调整,由之前的 sha1 改为了 sha2 ,不可逆 。同时 加上 mysql 5.7 的禁用用户和用户过期的功能,mysql 在用户管理方面的功能和安全性都较之 前版本大大的增强了。 mysql 5.7 及之后版本的密码保存到 authentication_string 字段中不再使用password 字 段。 权限列 grant_priv字段 表示是否拥有grant权限 shutdown_priv字段 表示是否拥有停止mysql服务的权限 super_priv字段 表示是否拥有超级权限 execute_priv字段 表示是否拥有execute权限。拥有execute权限,可以执行存储过程和函数。 select_priv , insert_priv等 为该用户所拥有的权限。 安全列\n安全列只有6个字段,其中两个是ssl相关的(ssl_type、ssl_cipher),用于 加密 ;两个是x509 相关的(x509_issuer、x509_subject),用于 标识用户 ;另外两个plugin字段用于 验证用户身份 的插件, 该字段不能为空。如果该字段为空,服务器就使用内建授权验证机制验证用户身份。\n资源控制列 资源控制列的字段用来 限制用户使用的资源 ,包含4个字段,分别为:\n①max_questions,用户每小时允许执行的查询操作次数;\n②max_updates,用户每小时允许执行的更新 操作次数;\n③max_connections,用户每小时允许执行的连接操作次数;\n④max_user_connections,用户 允许同时建立的连接次数。\n角色管理(mysql8.0) 引入角色的目的是 方便管理拥有相同权限的用户 。恰当的权限设定,可以确保数据的安全性,这是至关 重要的。\n创建角色 创建角色使用 create role 语句,语法如下:\ncreate role \u0026#39;role_name\u0026#39;[@\u0026#39;host_name\u0026#39;] [,\u0026#39;role_name\u0026#39;[@\u0026#39;host_name\u0026#39;]]... # 举例 create role \u0026#39;manager\u0026#39;@\u0026#39;localhost\u0026#39;; 给角色赋予权限 创建角色之后,默认这个角色是没有任何权限的,我们需要给角色授权。给角色授权的语法结构是:\ngrant privileges on table_name to \u0026#39;role_name\u0026#39;[@\u0026#39;host_name\u0026#39;];\t#举例 grant select on demo.settlement to \u0026#39;manager\u0026#39;; grant select on demo.goodsmaster to \u0026#39;manager\u0026#39;; grant select on demo.invcount to \u0026#39;manager\u0026#39;; 查看角色的权限 赋予角色权限之后,我们可以通过 show grants 语句,来查看权限是否创建成功了:\nmysql\u0026gt; show grants for \u0026#39;manager\u0026#39;; +-------------------------------------------------------+ | grants for manager@% | +-------------------------------------------------------+ | grant usage on *.* to `manager`@`%` | | grant select on `demo`.`goodsmaster` to `manager`@`%` | | grant select on `demo`.`invcount` to `manager`@`%` | | grant select on `demo`.`settlement` to `manager`@`%` | +-------------------------------------------------------+ 回收角色的权限 撤销角色权限的sql语法如下:\nrevoke privileges on tablename from \u0026#39;rolename\u0026#39;; 练习1:撤销school_write角色的权限。\n使用如下语句撤销school_write角色的权限。\nrevoke insert, update, delete on school.* from \u0026#39;school_write\u0026#39;; 删除角色 当我们需要对业务重新整合的时候,可能就需要对之前创建的角色进行清理,删除一些不会再使用的角 色。删除角色的操作很简单,你只要掌握语法结构就行了。\ndrop role role [,role2]... 注意, 如果你删除了角色,那么用户也就失去了通过这个角色所获得的所有权限 。\n给用户赋予角色 角色创建并授权后,要赋给用户并处于 激活状态 才能发挥作用。给用户添加角色可使用grant语句,语 法形式如下:\ngrant role [,role2,...] to user [,user2,...]; 练习:给kangshifu用户添加角色school_read权限。\n(1)使用grant语句给kangshifu添加school_read权 限,sql语句如下。\ngrant \u0026#39;school_read\u0026#39; to \u0026#39;kangshifu\u0026#39;@\u0026#39;localhost\u0026#39;; (2)添加完成后使用show语句查看是否添加成功,sql语句如下。\nshow grants for \u0026#39;kangshifu\u0026#39;@\u0026#39;localhost\u0026#39;; (3)使用kangshifu用户登录,然后查询当前角色,如果角色未激活,结果将显示none。sql语句如 下。\nselect current_role(); 激活角色 使用set default role 命令激活角色\nset default role all to \u0026#39;kangshifu\u0026#39;@\u0026#39;localhost\u0026#39;; 举例:使用 set default role 为下面4个用户默认激活所有已拥有的角色如下:\nset default role all to \u0026#39;dev1\u0026#39;@\u0026#39;localhost\u0026#39;, \u0026#39;read_user1\u0026#39;@\u0026#39;localhost\u0026#39;, \u0026#39;read_user2\u0026#39;@\u0026#39;localhost\u0026#39;, \u0026#39;rw_user1\u0026#39;@\u0026#39;localhost\u0026#39;; 撤销用户的角色 撤销用户角色的sql语法如下:\nrevoke role from user; 练习:撤销kangshifu用户的school_read角色。\n撤销的sql语句如下\nrevoke \u0026#39;school_read\u0026#39; from \u0026#39;kangshifu\u0026#39;@\u0026#39;localhost\u0026#39;; 逻辑架构 逻辑架构剖析 服务器处理客户端请求 那服务器进程对客户端进程发送的请求做了什么处理,才能产生最后的处理结果呢?这里以查询请求为 例展示:\n第1层:连接层 系统(客户端)访问 mysql 服务器前,做的第一件事就是建立 tcp 连接。\n经过三次握手建立连接成功后, mysql 服务器对 tcp 传输过来的账号密码做身份认证、权限获取。\n用户名或密码不对,会收到一个access denied for user错误,客户端程序结束执行 用户名密码认证通过,会从权限表查出账号拥有的权限与连接关联,之后的权限判断逻辑,都将依 赖于此时读到的权限 第2层:服务层 sql interface: sql接口\n接收用户的sql命令,并且返回用户需要查询的结果。比如select \u0026hellip; from就是调用sql interface mysql支持dml(数据操作语言)、ddl(数据定义语言)、存储过程、视图、触发器、自定义函数等多种sql语言接口 parser: 解析器\n在解析器中对 sql 语句进行语法分析、语义分析。将sql语句分解成数据结构,并将这个结构 传递到后续步骤,以后sql语句的传递和处理就是基于这个结构的。如果在分解构成中遇到错 误,那么就说明这个sql语句是不合理的。 在sql命令传递到解析器的时候会被解析器验证和解析,并为其创建 语法树 ,并根据数据字 典丰富查询语法树,会 验证该客户端是否具有执行该查询的权限 。创建好语法树后,mysql还 会对sql查询进行语法上的优化,进行查询重写。 optimizer: 查询优化器\nsql语句在语法解析之后、查询之前会使用查询优化器确定 sql 语句的执行路径,生成一个 执行计划 。 这个执行计划表明应该 使用哪些索引 进行查询(全表检索还是使用索引检索),表之间的连 接顺序如何,最后会按照执行计划中的步骤调用存储引擎提供的方法来真正的执行查询,并将 查询结果返回给用户。 它使用“ 选取-投影-连接 ”策略进行查询。例如: select id,name from student where gender = \u0026#39;女\u0026#39;; 这个select查询先根据where语句进行 选取 ,而不是将表全部查询出来以后再进行gender过 滤。 这个select查询先根据id和name进行属性 投影 ,而不是将属性全部取出以后再进行过 滤,将这两个查询条件 连接 起来生成最终查询结果。\n第3层:引擎层 插件式存储引擎层( storage engines),真正的负责了mysql中数据的存储和提取,对物理服务器级别 维护的底层数据执行操作,服务器通过api与存储引擎进行通信。不同的存储引擎具有的功能不同,这样 我们可以根据自己的实际需要进行选取。\nmysql 8.0.25默认支持的存储引擎如下:\n存储层 所有的数据,数据库、表的定义,表的每一行的内容,索引,都是存在 文件系统 上,以 文件 的方式存 在的,并完成与存储引擎的交互。当然有些存储引擎比如innodb,也支持不使用文件系统直接管理裸设 备,但现代文件系统的实现使得这样做没有必要了。在文件系统之下,可以使用本地磁盘,可以使用 das、nas、san等各种存储系统。\n小结 简化为三层结构:\n连接层:客户端和服务器端建立连接,客户端发送 sql 至服务器端; sql 层(服务层):对 sql 语句进行查询处理;与数据库文件的存储方式无关; 存储引擎层:与数据库文件打交道,负责数据的存储和读取。 sql执行流程 mysql的查询流程:\n查询缓存:server 如果在查询缓存中发现了这条 sql 语句,就会直接将结果返回给客户端;如果没 有,就进入到解析器阶段。需要说明的是,因为查询缓存往往效率不高,所以在 mysql8.0 之后就抛弃 了这个功能。\n解析器:在解析器中对 sql 语句进行语法分析、语义分析。\n分析器先做“ 词法分析 ”。你输入的是由多个字符串和空格组成的一条 sql 语句,mysql 需要识别出里面 的字符串分别是什么,代表什么。 mysql 从你输入的\u0026quot;select\u0026quot;这个关键字识别出来,这是一个查询语 句。它也要把字符串“t”识别成“表名 t”,把字符串“id”识别成“列 id”。\n接着,要做“ 语法分析 ”。根据词法分析的结果,语法分析器(比如:bison)会根据语法规则,判断你输 入的这个 sql 语句是否 满足 mysql 语法 。\n优化器:在优化器中会确定 sql 语句的执行路径,比如是根据 全表检索 ,还是根据 索引检索 等。\n举例:如下语句是执行两个表的 join:\nselect * from test1 join test2 using(id) where test1.name=\u0026#39;zhangwei\u0026#39; and test2.name=\u0026#39;mysql高级课程\u0026#39;; 方案1:可以先从表 test1 里面取出 name=\u0026#39;zhangwei\u0026#39;的记录的 id 值,再根据 id 值关联到表 test2,再判 断 test2 里面 name的值是否等于 \u0026#39;mysql高级课程\u0026#39;。 方案2:可以先从表 test2 里面取出 name=\u0026#39;mysql高级课程\u0026#39; 的记录的 id 值,再根据 id 值关联到 test1, 再判断 test1 里面 name的值是否等于 zhangwei。 这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。优化 器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。 如果你还有一些疑问,比如优化器是怎么选择索引的,有没有可能选择错等。后面讲到索引我们再谈。 在查询优化器中,可以分为 逻辑查询 优化阶段和 物理查询 优化阶段。\n执行器: 截止到现在,还没有真正去读写真实的表,仅仅只是产出了一个执行计划。于是就进入了 执行器阶段 。\nsql语法顺序 随着mysql版本的更新换代,其优化器也在不断的升级,优化器会分析不同执行顺序产生的性能消耗不同 而动态调整执行顺序。\n需求:查询每个部门年龄高于20岁的人数且高于20岁人数不能少于2人,显示人数最多的第一名部门信息 下面是经常出现的查询顺序:\n数据库缓冲池(buffer pool) innodb 存储引擎是以页为单位来管理存储空间的,我们进行的增删改查操作其实本质上都是在访问页 面(包括读页面、写页面、创建新页面等操作)。而磁盘 i/o 需要消耗的时间很多,而在内存中进行操 作,效率则会高很多,为了能让数据表或者索引中的数据随时被我们所用,dbms 会申请 占用内存来作为 数据缓冲池 ,在真正访问页面之前,需要把在磁盘上的页缓存到内存中的 buffer pool 之后才可以访 问。\n这样做的好处是可以让磁盘活动最小化,从而 减少与磁盘直接进行 i/o 的时间 。要知道,这种策略对提 升 sql 语句的查询性能来说至关重要。如果索引的数据在缓冲池里,那么访问的成本就会降低很多。\n问题:缓冲池和查询缓存是一个东西吗?不是。\n缓冲池 vs 查询缓存 缓冲池(buffer pool)\n首先我们需要了解在 innodb 存储引擎中,缓冲池都包括了哪些。 在 innodb 存储引擎中有一部分数据会放到内存中,缓冲池则占了这部分内存的大部分,它用来存储各种 数据的缓存,如下图所示:\n从图中,你能看到 innodb 缓冲池包括了数据页、索引页、插入缓冲、锁信息、自适应 hash 和数据字典 信息等。\n缓存池的重要性:\n缓存原则:\n“ 位置 * 频次 ”这个原则,可以帮我们对 i/o 访问效率进行优化。\n首先,位置决定效率,提供缓冲池就是为了在内存中可以直接访问数据。 其次,频次决定优先级顺序。\n因为缓冲池的大小是有限的,比如磁盘有 200g,但是内存只有 16g,缓冲 池大小只有 1g,就无法将所有数据都加载到缓冲池里,这时就涉及到优先级顺序,会优先对使用频次高 的热数据进行加载 。\n查询缓存\n那么什么是查询缓存呢?\n查询缓存是提前把 查询结果缓存 起来,这样下次不需要执行就可以直接拿到结果。\n需要说明的是,在 mysql 中的查询缓存,不是缓存查询计划,而是查询对应的结果。因为命中条件苛刻,而且只要数据表 发生变化,查询缓存就会失效,因此命中率低。\n缓冲池如何读取数据 缓冲池管理器会尽量将经常使用的数据保存起来,在数据库进行页面读操作的时候,首先会判断该页面 是否在缓冲池中,如果存在就直接读取,如果不存在,就会通过内存或磁盘将页面存放到缓冲池中再进 行读取。\n缓存在数据库中的结构和作用如下图所示:\n存储引擎 查看存储引擎 show engines; 查看默认的存储引擎:\nshow variables like \u0026#39;%storage_engine%\u0026#39;; #或 select @@default_storage_engine; 设置表的存储引擎 创建表时指定存储引擎\n我们之前创建表的语句都没有指定表的存储引擎,那就会使用默认的存储引擎 innodb 。如果我们想显 式的指定一下表的存储引擎,那可以这么写:\ncreate table 表名( 建表语句; ) engine = 存储引擎名称; 修改表的存储引擎\n如果表已经建好了,我们也可以使用下边这个语句来修改表的存储引擎:\nalter table 表名 engine = 存储引擎名称; 比如我们修改一下 engine_demo_table 表的存储引擎:\nmysql\u0026gt; alter table engine_demo_table engine = innodb; query ok, 0 rows affected (0.05 sec) records: 0 duplicates: 0 warnings: 0 引擎介绍 innodb 引擎:具备外键支持功能的事务存储引擎 mysql从3.23.34a开始就包含innodb存储引擎。 大于等于5.5之后,默认采用innodb引擎 。\ninnodb是mysql的 默认事务型引擎 ,它被设计用来处理大量的短期(short-lived)事务。可以确保事务 的完整提交(commit)和回滚(rollback)。\n除了增加和查询外,还需要更新、删除操作,那么,应优先选择innodb存储引擎。\n除非有非常特别的原因需要使用其他的存储引擎,否则应该优先考虑innodb引擎。\n数据文件结构:\n表名.frm 存储表结构(mysql8.0时,合并在表名.ibd中) 表名.ibd 存储数据和索引 innodb是 为处理巨大数据量的最大性能设计 。\n在以前的版本中,字典数据以元数据文件、非事务表等来存储。现在这些元数据文件被删除 了。比如: .frm , .par , .trn , .isl , .db.opt 等都在mysql8.0中不存在了。 对比myisam的存储引擎, innodb写的处理效率差一些 ,并且会占用更多的磁盘空间以保存数据和 索引。\nmyisam只缓存索引,不缓存真实数据;innodb不仅缓存索引还要缓存真实数据, 对内存要求较 高 ,而且内存大小对性能有决定性的影响。\nmyisam 引擎:主要的非事务处理存储引擎 myisam提供了大量的特性,包括全文索引、压缩、空间函数(gis)等,但myisam 不支持事务、行级 锁、外键 ,有一个毫无疑问的缺陷就是 崩溃后无法安全恢复 。\n5.5之前默认的存储引擎\n优势是访问的 速度快 ,对事务完整性没有要求或者以select、insert为主的应用\n针对数据统计有额外的常数存储。故而 count(*) 的查询效率很高\n数据文件结构:\n表名.frm 存储表结构 表名.myd 存储数据 (mydata) 表名.myi 存储索引 (myindex) 应用场景:只读应用或者以读为主的业务\n索引的数据结构 为什么使用索引 innodb中索引的推演 先来看一个精确匹配的例子:\nselect [列名列表] from 表名 where 列名 = xxx; 在没有索引的情况下,不论是根据主键列或者其他列的值进行查找,由于我们并不能快速的定位到记录 所在的页,所以只能 从第一个页 沿着 双向链表 一直往下找,在每一个页中根据我们上面的查找方式去查 找指定的记录。因为要遍历所有的数据页,所以这种方式显然是 超级耗时 的。如果一个表有一亿条记录 呢?此时 索引 应运而生。\n一个简单的索引设计方案 我们在根据某个搜索条件查找一些记录时为什么要遍历所有的数据页呢?因为各个页中的记录并没有规 律,我们并不知道我们的搜索条件匹配哪些页中的记录,所以不得不依次遍历所有的数据页。所以如果 我们 想快速的定位到需要查找的记录在哪些数据页 中该咋办?我们可以为快速定位记录所在的数据页而 建 立一个目录 ,建这个目录必须完成下边这些事:\n下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。 给所有的页建立一个目录项。 所以我们为上边几个页做好的目录就像这样子:\n以 页28 为例,它对应 目录项2 ,这个目录项中包含着该页的页号 28 以及该页中用户记录的最小主 键值 5 。我们只需要把几个目录项在物理存储器上连续存储(比如:数组),就可以实现根据主键 值快速查找某条记录的功能了。比如:查找主键值为 20 的记录,具体查找过程分两步:\n先从目录项中根据 二分法 快速确定出主键值为 20 的记录在 目录项3 中(因为 12 \u0026lt; 20 \u0026lt; 209 ),它对应的页是 页9 。 再根据前边说的在页中查找记录的方式去 页9 中定位具体的记录。 至此,针对数据页做的简易目录就搞定了。这个目录有一个别名,称为 索引 。\ninnodb中的索引方案 ① 迭代1次:目录项纪录的页\n我们把前边使用到的目录项放到数据页中的样子就是这样:\n从图中可以看出来,我们新分配了一个编号为30的页来专门存储目录项记录。这里再次强调 目录项记录 和普通的 用户记录 的不同点:\n目录项记录 的 record_type 值是1,而 普通用户记录 的 record_type 值是0。 目录项记录只有 主键值和页的编号 两个列,而普通的用户记录的列是用户自己定义的,可能包含 很 多列 ,另外还有innodb自己添加的隐藏列。 相同点:两者用的是一样的数据页,都会为主键值生成 page directory (页目录),从而在按照主键 值进行查找时可以使用 二分法 来加快查询速度。\n现在以查找主键为 20 的记录为例,根据某个主键值去查找记录的步骤就可以大致拆分成下边两步:\n先到存储 目录项记录 的页,也就是页30中通过 二分法 快速定位到对应目录项,因为 12 \u0026lt; 20 \u0026lt; 209 ,所以定位到对应的记录所在的页就是页9。 再到存储用户记录的页9中根据 二分法 快速定位到主键值为 20 的用户记录。 ② 迭代2次:多个目录项纪录的页\n从图中可以看出,我们插入了一条主键值为320的用户记录之后需要两个新的数据页:\n为存储该用户记录而新生成了 页31 。 因为原先存储目录项记录的 页30的容量已满 (我们前边假设只能存储4条目录项记录),所以不得 不需要一个新的 页32 来存放 页31 对应的目录项。 现在因为存储目录项记录的页不止一个,所以如果我们想根据主键值查找一条用户记录大致需要3个步 骤,以查找主键值为 20 的记录为例:\n确定 目录项记录页\n我们现在的存储目录项记录的页有两个,即 页30 和 页32 ,又因为页30表示的目录项的主键值的 范围是 [1, 320) ,页32表示的目录项的主键值不小于 320 ,所以主键值为 20 的记录对应的目 录项记录在 页30 中。\n通过目录项记录页 确定用户记录真实所在的页 。\n在一个存储 目录项记录 的页中通过主键值定位一条目录项记录的方式说过了。\n在真实存储用户记录的页中定位到具体的记录。\n③ 迭代3次:目录项记录页的目录页\n如图,我们生成了一个存储更高级目录项的 页33 ,这个页中的两条记录分别代表页30和页32,如果用 户记录的主键值在 [1, 320) 之间,则到页30中查找更详细的目录项记录,如果主键值 不小于320 的 话,就到页32中查找更详细的目录项记录。\n我们可以用下边这个图来描述它:\n这个数据结构,它的名称是 b+树 。\n④ b+tree\n一个b+树的节点其实可以分成好多层,规定最下边的那层,也就是存放我们用户记录的那层为第 0 层, 之后依次往上加。之前我们做了一个非常极端的假设:存放用户记录的页 最多存放3条记录 ,存放目录项 记录的页 最多存放4条记录 。其实真实环境中一个页存放的记录数量是非常大的,假设所有存放用户记录 的叶子节点代表的数据页可以存放 100条用户记录 ,所有存放目录项记录的内节点代表的数据页可以存 放 1000条目录项记录 ,那么:\n如果b+树只有1层,也就是只有1个用于存放用户记录的节点,最多能存放 100 条记录。 如果b+树有2层,最多能存放 1000×100=10,0000 条记录。 如果b+树有3层,最多能存放 1000×1000×100=1,0000,0000 条记录。 如果b+树有4层,最多能存放 1000×1000×1000×100=1000,0000,0000 条记录。相当多的记 录!!! 你的表里能存放 100000000000 条记录吗?所以一般情况下,我们 用到的b+树都不会超过4层 ,那我们 通过主键值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页),又 因为在每个页面内有所谓的 page directory (页目录),所以在页面内也可以通过 二分法 实现快速 定位记录。\n常见索引概念 索引按照物理实现方式,索引可以分为 2 种:聚簇(聚集)和非聚簇(非聚集)索引。我们也把非聚集 索引称为二级索引或者辅助索引。\n聚簇索引\n使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:\n页内 的记录是按照主键的大小顺序排成一个 单向链表 。 各个存放 用户记录的页 也是根据页中用户记录的主键大小顺序排成一个 双向链表 。 存放 目录项记录的页 分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键 大小顺序排成一个 双向链表 。 b+树的 叶子节点 存储的是完整的用户记录。\n所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。\n二级索引(辅助索引、非聚簇索引)\n概念:回表\n我们根据这个以c2列大小排序的b+树只能确定我们要查找记录的主键值,所以如果我们想根 据c2列的值查找到完整的用户记录的话,仍然需要到 聚簇索引 中再查一遍,这个过程称为 回表 。也就 是根据c2列的值查询一条完整的用户记录需要使用到 2 棵b+树!\n联合索引\n我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让b+树按 照 c2和c3列 的大小进行排序,这个包含两层含义:\n先把各个记录和页按照c2列进行排序。 在记录的c2列相同的情况下,采用c3列进行排序 注意一点,以c2和c3列的大小为排序规则建立的b+树称为 联合索引 ,本质上也是一个二级索引。它的意 思与分别为c2和c3列分别建立索引的表述是不同的,不同点如下:\n建立 联合索引 只会建立如上图一样的1棵b+树。 为c2和c3列分别建立索引会分别以c2和c3列的大小为排序规则建立2棵b+树。 innodb的b+树索引的注意事项:\n根页面位置万年不动 内节点中目录项记录的唯一性 一个页面最少存储2条记录 myisam中的索引方案 myisam引擎使用 b+tree 作为索引结构,叶子节点的data域存放的是 数据记录的地址 。\nmyisam 与 innodb对比 myisam的索引方式都是“非聚簇”的,与innodb包含1个聚簇索引是不同的。\n① 在innodb存储引擎中,我们只需要根据主键值对 聚簇索引 进行一次查找就能找到对应的记录,而在 myisam 中却需要进行一次 回表 操作,意味着myisam中建立的索引相当于全部都是 二级索引 。\n② innodb的数据文件本身就是索引文件,而myisam索引文件和数据文件是 分离的 ,索引文件仅保存数 据记录的地址。\n③ innodb的非聚簇索引data域存储相应记录主键的值 ,而myisam索引记录的是 地址 。换句话说, innodb的所有非聚簇索引都引用主键作为data域。\n④ myisam的回表操作是十分 快速 的,因为是拿着地址偏移量直接到文件中取数据的,反观innodb是通 过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址去访问。\n⑤ innodb要求表 必须有主键 ( myisam可以没有 )。如果没有显式指定,则mysql系统会自动选择一个 可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则mysql自动为innodb表生成一个隐 含字段作为主键,这个字段长度为6个字节,类型为长整型。\ninnodb数据存储结构 innodb中页的概念 页,是innodb中数据管理的最小单位。当我们查询数据时,其是以页为单位,将磁盘中的数据加载到缓冲池中的。同理,更新数据也是以页为单位,将我们对数据的修改刷回磁盘。\npage是innodb存储的最基本结构,也是innodb磁盘管理的最小单位,与数据库相关的所有内容都存储在page结构里。page分为几种类型:\n数据页(b-tree node), undo页(undo log page), 系统页(system page), 事务数据页(transaction system page) 页的大小\n每个数据页的大小为16kb,每个page使用一个32位(一位表示的就是0或1)的int值来表示,正好对应innodb最大64tb的存储容量(16kb * 2^32=64tib)\nmysql中的具体数据是存储在行中的,而行是存储在页中的,每页的默认大小为16k(大小可以通过配置文件修改),页的结构如下图所示\n页的结构\n一开始生成页的时候并没有user records这个部分.每当我们插⼊⼀条记录,都会从free space部分,也就是尚未使⽤的存储空间中申请⼀个记录⼤⼩的空间划分到user records部分,当free space部分的空间全部被user records部分替代掉之后,也就意味着这个页使⽤完了,如果还有新的记录插⼊的话,就需要去申请新的页了。\n页的内部结构 第一部分:file header(文件头部)和 file trailer(文件尾部) file header 构成\nfil_page_offset: 记录数据页编号,类似主键;\nfil_page_type: 数据页类型,例如:索引页,undo日志页\nfil_page_prv 和 file_page_next: 两个数据页逻辑相邻的标识\nfil_page_space_or_chksum: 磁盘与内存数据交互之前计算一个校验和,交互完成后再计算一个校验和,如果两次校验和相同,表示传输正常;如何不相等,表示传输有问题。\n校验和是用于检测传输过程中可能产生的错误,将其置于数据后,随数据一同发送,接收端通过同样的算法进行检查,若正确就接受,错误就丢弃\n第二部分:user record(用户记录)、最大最小记录、free space(空闲空间) 第二部分是记录部分,页的主要作用是存储记录,所以“最大和最小记录”和“用户记录”部分占了页结构的主要空间。\nfree space\n一开始生成页的时候并没有user records这个部分.每当我们插⼊⼀条记录,都会从free space部分,也就是尚未使⽤的存储空间中申请⼀个记录⼤⼩的空间划分到user records部分,当free space部分的空间全部被user records部分替代掉之后,也就意味着这个页使⽤完了,如果还有新的记录插⼊的话,就需要去申请新的页了。\nuser records\nuser records的记录按照指定的行格式列在user records中,相互之间形成单链表\n最大最小记录(heap_no中)\n如何形成单链表\u0026ndash;\u0026gt; 记录头信息\ndelete_mask:\n是否被删除,1表示删除,0表示未删除;\nmin_rec_mask:\n记录是否为叶子结点,1不是 叶子几点,0表示 叶子结点 ;\nrecord_type:\n0表示普通节点,1表示b+树非叶子节点,2表示最小记录,3表示最大记录\nheap_no:\n表示记录的当前位置,0为最小记录,1为最大记录\nn_owned(page_directory)\n分组后的个数\nnext_record\n记录下一条数据的地址偏移量\n删除操作:①删除记录的delete_mask设置为1,②被删除记录的上一条记录指向删除记录的下一条记录,③将第二组的n_owned修改;如果删除多条记录,那么多被删除的记录也会形成单链表(此单链表标识在页目录头部的字段中)\n添加操作:严格按照主键的大小进行插入,而不是按照添加的顺序插入(页目录的头部中page_direction字段记录当前插入主键的位置在前还是在后)\n第三部分:page directory(页目录)、page header(页面头部) 页目录\n为什么需要页目录?\n在页中,记录是以单向链表的形成存储。单向链表特点是插入、删除方便,而查找效率不高。因此给记录做一个目录,通过二分法进行检索。\n使用二分法查找记录:\n最小记录分为一组,其余记录尽量平分,并且记录每一组的最大值,作为每个槽位上的值。\n页面头部\ncompact行格式 额外信息 变长字段长度列表\n记录变长字段真实的长度\n这里存储变长长度和字段顺序为反过来的\n在变长字段中存储为060408\nnull值列表\ncompact行格式会把可以为null的列统一管理起来,存在一个标记为null值列表中。如果表中没有允许存储 null 的列,则 null值列表也不存在了。\n为什么使用null值列表?\n之所以要存储null是因为数据都是需要对齐的,如果没有标注出来null值的位置,就有可能在查询数据的时候出现混乱。如果使用一个特定的符号放到相应的数据位表示空置的话,虽然能达到效果,但是这样很浪费空间,所以直接就在行数据得头部开辟出一块空间专门用来记录该行数据哪些是非空数据,哪些是空数据,格式如下:\n二进制位的值为1时,代表该列的值为null。\n二进制位的值为0时,代表该列的值不为null。\n记录头信息(第二部分)\n真实数据 三个隐藏列\n例如:\n变长字段,\n03:\u0026lsquo;ccc\u0026rsquo;;\n02: \u0026lsquo;bb\u0026rsquo;;\n01: \u0026lsquo;a\u0026rsquo;\nchar为定长所以不用写入变长字段列表\n没有null所以为00。设置非空的字段不用记录null值\n记录头信息: 2c为下一个指针位置的偏移量\nrow_id:因为此表没有主键\ntransaction_id;\nroll_pointer;\na\nbb\nbb 因为char为定长的,20是空的意思\nccc\ndynamic行格式 dynamic基本与compact一致;\n只是在行溢出情况有不同的处理情况:\ndynamic对于行溢出的数据全部转移到另一个位置 而compact会将溢出的部分转移到另一个位置 区、段、碎片区 如果我们有大量的记录,那么表空间中页的数量就会非常的多,为了更好的管理这些页,设计者又提出了区的概念。对于16kb的页来说,连续的64个页(连续的)就是一个区,也就是说一个区的大小为1mb。(256个区被划分成一个组)\n为什么要有区 通过页其实已经形成了完整的功能,我们查询数据时这样沿着双向链表就可以查到数据,但是页与页之间在物理位置上可能不是连续的,如果相隔太远,那么我们从一个页移动到另一个页的时候,磁盘就要重新定义磁头的位置,产生随机io,影响性能,所以我们才要引入区的概念,一个区就是物理位置连续的64个页。区是属于某一个段的(或者是混合)。\n什么是段 正常情况下,我们检索都是在叶子节点的双向链表进行的,也就是说我们会把区进行区分,如果不区分把叶子节点和非叶子结点混在一起,那么效果就会打大折扣,所以对于一个索引b+树来说,我们区别对待叶子节点的区和非叶子节点的区,并把存放叶子节点的区的集合称为一个段把存放非叶子节点区的集合也称为一个段,所以一个索引会生成两个段:叶子节点段和非叶子节点段。\n碎片区 默认情况下,假如我们新建一个索引就会生成两个段(叶子节点段和非叶子节点段),而一个段中至少包含一个区,也就是需要2mb的空间,假如我们这个表压根没有多少数据,那么一次就要申请2mb的空间明显是浪费的。为了解决这个问题,设计者提出了碎片区的概念,碎片区中的页可能属于不同的段,也可以用于不同的目的,至于如何控制应不应该给一个段申请专属的区,会进行以下控制:\n刚开始向表插入数据,都是从某一个碎片区以页为单位来分配存储空间。 当一个段占用的空间达到了32个碎片区的页之后,就会开始给这个段申请专属的区 索引的创建与设计原则 索引的分类 mysql的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。\n从 功能逻辑 上说,索引主要有 4 种,分别是普通索引、唯一索引、主键索引、全文索引。\n按照 物理实现方式 ,索引可以分为 2 种:聚簇索引和非聚簇索引。\n按照 作用字段个数 进行划分,分成单列索引和联合索引。\n普通索引、唯一性索引 、主键索引 、单列索引 、多列(组合、联合)索引 、全文索引、空间索引\n创建表的时候创建索引 基本语法\ncreate table table_name [col_name data_type] [unique | fulltext | spatial] [index | key] [index_name] (col_name [length]) [asc | desc] unique 、 fulltext 和 spatial 为可选参数,分别表示唯一索引、全文索引和空间索引; index 与 key 为同义词,两者的作用相同,用来指定创建索引; index_name 指定索引的名称,为可选参数,如果不指定,那么mysql默认col_name为索引名; col_name 为需要创建索引的字段列,该列必须从数据表中定义的多个列中选择; length 为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度; asc 或 desc 指定升序或者降序的索引值存储。 普通索引\ncreate table book( book_id int , book_name varchar(100), authors varchar(100), info varchar(100) , comment varchar(100), year_publication year, index(year_publication) #~ ); 唯一索引\ncreate table test1( id int not null, name varchar(30) not null, unique index uk_idx_id(id) #~ ); 主键索引\n设定为主键后数据库会自动建立索引,innodb为聚簇索引\n# 随表一起建索引: create table student ( id int(10) unsigned auto_increment , student_no varchar(200), student_name varchar(200), primary key(id) #~ ); # 删除主键索引: alter table student drop primary key ; 修改主键索引:必须先删除掉(drop)原索引,再新建(add)索引\n单列索引\ncreate table test2( id int not null, name char(50) null, index single_idx_name(name(20)) #~ ); 组合索引\ncreate table test3( id int(11) not null, name char(30) not null, age int(11) not null, info varchar(255), index multi_idx(id,name,age) #~ ); 全文索引\n# 在表中的info字段上建立全文索引 create table test4( id int not null, name char(30) not null, age int not null, info varchar(255), fulltext index futxt_idx_info(info) #~ ) engine=myisam; # 创建了一个给title和body字段添加全文索引的表。 create table articles ( id int unsigned auto_increment primary key, title varchar (200), body text, fulltext index (title, body) #~ ) engine = innodb ; 在mysql5.7及之后版本中可以不指定最后的engine了,因为在此版本中innodb支持全文索引。\ncreate table `papers` ( `id` int(10) unsigned not null auto_increment, `title` varchar(200) default null, `content` text, primary key (`id`), fulltext key `title` (`title`,`content`)#~ ) engine=myisam default charset=utf8; #不同于like方式的的查询: select * from papers where content like ‘%查询字符串%’; #全文索引用match+against方式查询: select * from papers where match(title,content) against (‘查询字符串’); 注意点\n使用全文索引前,搞清楚版本支持情况; 全文索引比 like + % 快 n 倍,但是可能存在精度问题; 如果需要全文索引的是大量数据,建议先添加数据,再创建索引。 空间索引\n# 空间索引创建中,要求空间类型的字段必须为 非空 。 create table test5( geo geometry not null, spatial index spa_idx_geo(geo) #~ ) engine=myisam; 已经存在的表上创建索引 在已经存在的表中创建索引可以使用alter table语句或者create index语句。\n使用alter table语句创建索引,基本语法 alter table table_name add [unique | fulltext | spatial] [index | key] [index_name] (col_name[length],...) [asc | desc] 使用create index创建索引,基本语法 create [unique | fulltext | spatial] index index_name on table_name (col_name[length],...) [asc | desc] 删除索引 使用alter table删除索引 alter table删除索引的基本语法格式如下: alter table table_name drop index index_name; 使用drop index语句删除索引 drop index删除索引的基本语法格式如下: drop index index_name on table_name; 索引新特性mysql8.0 降序索引 举例\ncreate table ts1(a int,b int,index idx_a_b(a,b desc)); 执行计划:\nexplain select * from ts1 order by a,b desc limit 5; mysql5.7 从结果可以看出,执行计划中扫描数为799,而且使用了using filesort。 mysql8.0 从结果可以看出,执行计划中扫描数为5,而且没有使用 using filesort。 隐藏索引 从mysql 8.x开始支持 隐藏索引(invisible indexes) ,只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引), 确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。 这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除 。\n创建表时直接创建 在mysql中创建隐藏索引通过sql语句invisible来实现,其语法形式如下: create table tablename( propname1 type1[constraint1], propname2 type2[constraint2], …… propnamen typen, index [indexname](propname1 [(length)]) invisible ); #上述语句比普通索引多了一个关键字invisible,用来标记索引为不可见索引。 在已经存在的表上创建可以为已经存在的表设置隐藏索引,其语法形式如下: create index indexname on tablename(propname[(length)]) invisible; 切换索引可见状态 已存在的索引可通过如下语句切换可见状态: alter table tablename alter index index_name invisible; #切换成隐藏索引 alter table tablename alter index index_name visible; #切换成非隐藏索引 注意 当索引被隐藏时,它的内容仍然是和正常索引一样实时更新的。如果一个索引需要长期被隐 藏,那么可以将其删除,因为索引的存在会影响插入、更新和删除的性能。\n使隐藏索引对查询优化器可见 在mysql 8.x版本中,为索引提供了一种新的测试方式,可以通过查询优化器的一个开关 (use_invisible_indexes)来打开某个设置,使隐藏索引对查询优化器可见。如果 use_invisible_indexes 设置为off(默认),优化器会忽略隐藏索引。如果设置为on,即使隐藏索引不可见,优化器在生成执行计 划时仍会考虑使用隐藏索引。\n索引的设计原则 适合创建索引 字段的数值有唯一性的限制\n业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。\n说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的。\n频繁作为 where 查询条件的字段\n某个字段在select语句的 where 条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在 数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。\n例:比如student_info数据表(含100万条数据),假设我们想要查询 student_id=123110 的用户信息。\n经常 group by 和 order by 的列\n索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 group by 对数据进行分组查询,或者 使用 order by 对数据进行排序的时候,就需要对分组或者排序的字段进行索引 。如果待排序的列有多个,那么可以在这些列上建立组合索引 。\nupdate、delete 的 where 条件列\n对数据按照某个条件进行查询后再进行 update 或 delete 的操作,如果对 where 字段创建了索引,就 能大幅提升效率。原理是因为我们需要先根据 where 条件列检索出来这条记录,然后再对它进行更新或 删除。\n如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更 新不需要对索引进行维护。\ndistinct 字段需要创建索引\n有时候我们需要对某个字段进行去重,使用 distinct,那么对这个字段创建索引,也会提升查询效率。\n多表 join 连接操作时,创建索引注意事项\n首先, 连接表的数量尽量不要超过 3 张 ,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增 长会非常快,严重影响查询的效率。\n其次, 对 where 条件创建索引 ,因为 where 才是对数据条件的过滤。如果在数据量非常大的情况下, 没有 where 条件过滤是非常可怕的。\n最后, 对用于连接的字段创建索引 ,并且该字段在多张表中的类型必须一致 。比如 course_id 在 student_info 表和 course 表中都为 int(11) 类型,而不能一个为 int 另一个为 varchar 类型。\n使用列的类型小的创建索引\n类型大小为该类型的范围大小。\n数据类型越小,在查询进行的比较操作快;数据类型越小,索引占用存储空间越小,在一个数据页内可以放下更多的数据,减少磁盘i/o。\n使用字符串前缀创建索引\n创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引\ncreate table shop(address varchar(120) not null); alter table shop add index(address(12)); 问题是,截取多少呢?截取得多了,达不到节省索引存储空间的目的;截取得少了,重复内容太多,字 段的散列度(选择性)会降低。怎么计算不同的长度的选择性呢?\n#截取公式 count(distinct left(列名, 索引长度))/count(*) 【 强制 】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本 区分度决定索引长度。\n说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会 高达 90% 以上 ,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。\n区分度高(散列性高)的列适合作为索引\n使用最频繁的列放到联合索引的左侧\n在多个字段都要创建索引的情况下,联合索引优于单值索引\n不适合创建索引 在where中使用不到的字段,不要设置索引\n数据量小的表最好不要使用索引\n在数据表中的数据行数比较少的情况下,比如不到 1000 行,是不需要创建索引的。\n有大量重复数据的列上不要建立索引\n当数据重复度大,比如 高于 10% 的时候,也不需要对这个字段使用索引。\n避免对经常更新的表创建过多的索引\n不建议用无序的值作为索引\n例如身份证、uuid(在索引比较时需要转为ascii,并且插入时可能造成页分裂)、md5、hash、无序长字 符串等\n删除不再使用或者很少使用的索引\n不要定义冗余或重复的索引\n冗余: 我们知道,通过 idx_name_birthday_phone_number 索引就可以对 name 列进行快速搜索,再创建一 个专门针对 name 列的索引就算是一个 冗余索引 ,维护这个索引只会增加维护的成本,并不会对搜索有 什么好处。\n重复: col1 既是主键、又给它定义为一个唯一索引,还给它定义了一个普通索引,可是主键本身就 会生成聚簇索引,所以定义的唯一索引和普通索引是重复的,这种情况要避免。\n性能分析工具使用 分析查询语句explain 基本语法:\nexplain select select_options 或者 describe select select_options explain 语句输出的各个列的作用如下:\ntable\n不论我们的查询语句有多复杂,里边儿 包含了多少个表 ,到最后也是需要对每个表进行 单表访问 的,所 以mysql规定explain语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该 表的表名(有时不是真实的表名字,可能是简称)。\nid\nid如果相同,可以认为是一组,从上往下顺序执行\n在所有组中,id值越大,优先级越高,越先执行\n关注点:id号每个号码,表示一趟独立的查询, 一个sql的查询趟数越少越好\nselect_type\nselect关键字对应的那个查询的类型,确定小查询在整个大查询中扮演了一个什么角色\nsimple:查询语句中不包含union或者子查询的查询都算作是simple类型,连接查询也算是simple类型 primary:对于包含union或者union all或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个询的select_type值就是primary union:对于包含union或者union all的大查询来说,它是由几个小查询组成的,其中除了最左边的那个小查询以外,其余的小查询的select_type值就是union subquery(子查询):如果包含子查询的查询语句不能够转为对应的semi-join(子查询没有转换为多表连接)的形式,并且该子查询是不相关子查询。该子查询的第一个select关键字代表的那个查询的select_type就是subquery dependent subquery(相关子查询): 如果包含子查询的查询语句不能够转为对应的semi-join的形式,并且该子查询是相关子查询,则该子查询的第一个select关键字代表的那个查询的select_type就是dependent subquery dependent union:在包含union或者union all的大查询中,如果各个小查询都依赖于外层查询的话,那除了最左边的那个小查询之外,其余的小查询的select_type的值就是dependent union。 derived:用于 from 子句里有子查询的情况。mysql 会递归执行这些子查询, 把结果放在临时表里 materialized:当查询优化器在执行包含子查询的语句时,选择将子查询物化之后与外层查询进行连接查询时,该子查询对应的select_type属性就是materialized 特别关注 dependent subquery ,会严重消耗性能,不会进行子查询,会先进行外部查询,生成结果集,再在内部进行关联查询。子查询的执行效率受制于外层查询的记录数\ntype\n常用的类型有: all、index、range、 ref、eq_ref、const、system(从左到右,性能从差到好)\nall: mysql将遍历全表以找到匹配的行 index: 遍历索引树 range: 只检索给定范围的行,使用索引来选择行 ref: 查找条件列使用了索引而且不为主键和unique。就是虽然使用了索引,但该索引列的值并不唯一,有重复。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描 ref_eq: 使用了主键或者唯一性索引进行查找 const: 常量连接,表最多只有一行匹配,通用用于主键或者唯一索引比较时(where后面是逐渐或者唯一索引) system: 表中只有一行 possible_keys\n预测用到的索引\nkey\n实际用到的索引\nkey_len\n表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度\nref\n哪些列或者常量被用于查找索引列上的值(只有当type为ref的时候,ref这列才会有值)\nrows\n估算的找到所需的记录所需要读取的行数\nextra\n包含mysql解决查询的详细信息\nusing filesort: mysql的排序方法主要分为两大类,一种是排序的字段是有索引的,因为索引是有序的,所以不需要另外排序,另一种是排序的字段没有索引,所以需要对结果进行排序,在这种情况下才会如上所示显示一个using filesort using temporary:性能损耗大,用到临时表(常见于group by) using where:表明虽然用到了索引,但是没有索引覆盖,产生了回表。 using index:索引覆盖,查询的内容可以直接在索引中拿到 using index condition:索引下推 索引优化与查询优化 索引失效 最佳左前缀法则\n索引文件具有 b-tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。\n主键插入顺序\n我们自定义的主键列 id 拥有 auto_increment 属性,在插入记录时存储引擎会自动为我们填入自增的 主键值。这样的主键占用空间小,顺序写入,减少页分裂。\n计算、函数、类型转换(自动或手动)导致索引失效\n#此语句比下一条要好!(能够使用上索引) explain select sql_no_cache * from student where student.name like \u0026#39;abc%\u0026#39;; #使用函数导致索引失效 explain select sql_no_cache * from student where left(student.name,3) = \u0026#39;abc\u0026#39;; 类型转换导致索引失效\n# name为varchar的需要类型转换,失效 explain select sql_no_cache * from student where name = 123; # 有效 explain select sql_no_cache * from student where name = \u0026#39;123\u0026#39;; 范围条件右边的列索引失效\ncreate index idx_age_classid_name on student(age,classid,name); explain select sql_no_cache * from student where student.age=30 and student.classid\u0026gt;20 and student.name = \u0026#39;abc\u0026#39; ; 不等于(!= 或者\u0026lt;\u0026gt;)索引失效\nis null可以使用索引,is not null无法使用索引\nlike以通配符%开头索引失效\nexplain select sql_no_cache * from student where name like \u0026#39;ab%\u0026#39;; explain select sql_no_cache * from student where name like \u0026#39;%ab%\u0026#39;; or 前后存在非索引的列,索引失效\ncreate index idx_age on student(age); explain select sql_no_cache * from student where age = 10 or classid = 100; 数据库和表的字符集统一使用utf8mb4\n关联查询优化 采用左外连接 explain select sql_no_cache * from `type` left join book on type.card = book.card; # 结论:type 有all 添加索引优化\nalter table book add index y ( card); #【被驱动表】,可以避免全表扫描 explain select sql_no_cache * from `type` left join book on type.card = book.card; left join 条件用于确定如何从右表搜索行,左边一定都有,所以 右边是我们的关键点,一定需要建立索引 。\n采用内连接 explain select sql_no_cache * from type inner join book on type.card=book.card; #结论:type 有all 添加索引优化\nalter table book add index y ( card); explain select sql_no_cache * from type inner join book on type.card=book.card; alter table type add index x (card); explain select sql_no_cache * from type inner join book on type.card=book.card; inner join 中,两张表权重一样,优化器会进行优化,数据量小的表作为驱动表,数据量大的作为被驱动表。\njoin语句原理 index nested-loop join explain select * from t1 straight_join t2 on (t1.a=t2.a); 如果直接使用join语句,mysql优化器可能会选择表t1或t2作为驱动表,这样会影响我们分析sql语句的 执行过程。所以,为了便于分析执行过程中的性能问题,我改用 straight_join 让mysql使用固定的 连接方式执行查询,这样优化器只会按照我们指定的方式去join。在这个语句里,t1 是驱动表,t2是被驱 动表。\n可以看到,在这条语句里,被驱动表t2的字段a上有索引,join过程用上了这个索引,因此这个语句的执 行流程是这样的:\n从表t1中读入一行数据 r; 从数据行r中,取出a字段到表t2里去查找; 取出表t2中满足条件的行,跟r组成一行,作为结果集的一部分; 重复执行步骤1到3,直到表t1的末尾循环结束。 这个过程是先遍历表t1,然后根据从表t1中取出的每行数据中的a值,去表t2中查找满足条件的记录。在 形式上,这个过程就跟我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为 “index nested-loop join”,简称nlj。\n小结\n使用join语句,性能比强行拆成多个单表执行sql语句的性能要好; 如果使用join语句的话,需要让小表做驱动表。 block nested-loop join 举个简单的例子:外层循环结果集有1000行数据,使用nlj算法需要扫描内层表1000次,但如果使用bnl算法,则先取出外层表结果集的100行存放到join buffer, 然后用内层表的每一行数据去和这100行结果集做比较,可以一次性与100行数据进行比较,这样内层表其实只需要循环1000/100=10次,减少了9/10。\n总结\n保证被驱动表的join字段已经创建了索引 需要join 的字段,数据类型保持绝对一致。 left join 时,选择小表作为驱动表, 大表作为被驱动表 。减少外层循环的次数。 inner join 时,mysql会自动将 小结果集的表选为驱动表 。选择相信mysql优化策略。 能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数) 不建议使用子查询,建议将子查询sql拆开结合程序多次查询,或使用 join 来代替子查询。 衍生表建不了索引 排序优化 问题:在 where 条件字段上加索引,但是为什么在 order by 字段上还要加索引呢?\n优化建议:\nsql 中,可以在 where 子句和 order by 子句中使用索引,目的是在 where 子句中 避免全表扫描 ,在 order by 子句 避免使用 filesort 排序 。当然,某些情况下全表扫描,或者 filesort 排 序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。 尽量使用 index 完成 order by 排序。如果 where 和 order by 后面是相同的列就使用单索引列; 如果不同就使用联合索引。 无法使用 index 时,需要对 filesort 方式进行调优。 index a_b_c(a,b,c) order by 能使用索引最左前缀 - order by a - order by a,b - order by a,b,c - order by a desc,b desc,c desc 如果where使用索引的最左前缀定义为常量,则order by 能使用索引 - where a = const order by b,c - where a = const and b = const order by c - where a = const order by b,c - where a = const and b \u0026gt; const order by b,c 不能使用索引进行排序 - order by a asc,b desc,c desc /* 排序不一致 */ - where g = const order by b,c /*丢失a索引*/ - where a = const order by c /*丢失b索引*/ - where a = const order by a,d /*d不是索引的一部分*/ - where a in (...) order by b,c /*对于排序来说,多个相等条件也是范围查询*/ filesort算法:双路排序和单路排序 双路排序 (慢)\nmysql 4.1之前是使用双路排序 ,字面意思就是两次扫描磁盘,最终得到数据, 读取行指针和 order by列 ,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取 对应的数据输出 从磁盘取排序字段,在buffer进行排序,再从 磁盘取其他字段 。 取一批数据,要对磁盘进行两次扫描,众所周知,io是很耗时的,所以在mysql4.1之后,出现了第二种 改进的算法,就是单路排序。\n单路排序 (快)\n从磁盘读取查询需要的 所有列 ,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输 出, 它的效率更快一些,避免了第二次读取数据。并且把随机io变成了顺序io,但是它会使用更多的空 间, 因为它把每一行都保存在内存中了。\ngroup by优化 group by 使用索引的原则几乎跟order by一致 ,group by 即使没有过滤条件用到索引,也可以直接 使用索引。 group by 先排序再分组,遵照索引建的最佳左前缀法则 当无法使用索引列,增大 max_length_for_sort_data 和 sort_buffer_size 参数的设置 where效率高于having,能写在where限定的条件就不要写在having中了 减少使用order by,和业务沟通能不排序就不排序,或将排序放到程序端去做。 order by、group by、distinct这些语句较为耗费cpu,数据库的cpu资源是极其宝贵的。 包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行 以内,否则sql会很慢。 优先考虑覆盖索引 什么是覆盖索引?\n理解方式一:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它 不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数 据,那就不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引。\n理解方式二:非聚簇复合索引的一种形式,它包括在查询里的select、join和where子句用到的所有列 (即建索引的字段正好是覆盖查询条件中所涉及的字段)。\n简单说就是, 索引列+主键 包含 select 到 from之间查询的列 。\n给字符串添加索引 如果使用的是index1(即email整个字符串的索引结构),执行顺序是这样的:\n从index1索引树找到满足索引值是’ zhangssxyz@xxx.com ’的这条记录,取得id2的值; 到主键上查到主键值是id2的行,判断email的值是正确的,将这行记录加入结果集; 取index1索引树上刚刚查到的位置的下一条记录,发现已经不满足email=\u0026rsquo; zhangssxyz@xxx.com ’的 条件了,循环结束。 这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。 如果使用的是index2(即email(6)索引结构),执行顺序是这样的:\n从index2索引树找到满足索引值是’zhangs’的记录,找到的第一个是id1;\n到主键上查到主键值是id1的行,判断出email的值不是’ zhangssxyz@xxx.com ’,这行记录丢弃;\n取index2上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出id2,再到id索引上取整行然 后判断,这次值对了,将这行记录加入结果集;\n重复上一步,直到在idxe2上取到的值不是’zhangs’时,循环结束。\n也就是说使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。前面 已经讲过区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。\n结论: 使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素.\n索引下推 index condition pushdown(icp)是mysql 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优 化方式。icp可以减少存储引擎访问基表的次数以及mysql服务器访问存储引擎的次数。\n在不使用icp索引扫描的过程:\n例: select * from emp where x and y;\nstorage层:只将满足index key条件的索引记录对应的整行记录取出(x),返回给server层\nserver 层:对返回的数据,使用后面的where条件过滤(y),直至返回最后一行。\n使用icp扫描的过程:\nstorage层: 首先将index key条件满足的索引记录区间确定,然后在索引上使用index filter进行过滤。将满足的index filter条件的索引记录才去回表取出整行记录返回server层。不满足index filter条件的索引记录丢弃,不回表、也不会返回server层。\nserver 层: 对返回的数据,使用table filter条件做最后的过滤。\n使用前后的成本差别\n使用前,存储层多返回了需要被index filter过滤掉的整行记录\n使用icp后,直接就去掉了不满足index filter条件的记录,省去了他们回表和传递到server层的成本。\nicp的 加速效果 取决于在存储引擎内通过 icp筛选 掉的数据的比例。\nicp的使用条件\n① 只能用于二级索引(secondary index)\n② 基本使用在联合索引上\n③ 并非全部where条件都可以用icp筛选,如果where条件的字段不在索引列中,还是要读取整表的记录 到server端做where过滤。\n④ icp可以用于myisam和innnodb存储引擎\n⑤ mysql 5.6版本的不支持分区表的icp功能,5.7版本的开始支持。\n⑥ 当sql使用覆盖索引时,不支持icp优化方法\n⑦explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者 ref_or_null 。\n案例:\nexplain select * from s1 where key1 \u0026gt; \u0026#39;z\u0026#39; and key1 like \u0026#39;%a\u0026#39;; 未使用icp: 通过key1索引查找符合索引数据回表查询数据,再进行key1 like '%a'的条件过滤\n使用icp: 通过key1索引查找符合索引的数据,然后key1 like '%a'的条件过滤,再进行回表\n数据库的设计规范 为什么需要数据库设计 糟糕的数据库设计可能造成问题:\n数据冗余、信息重复、存储空间浪费 数据更新、插入、删除异常 无法正常表示信息 丢失有效信息 程序性能差 良好数据库设计优点:\n节省数据库存储空间 能保证数据的完整性 方便进行数据库应用系统的开发 总之,为了建立冗余较小,结构合理的数据库,设计数据库必须遵循一定的规则。\n范 式 范式简介\n在关系型数据库中,关于数据表设计的基本原则、规则就称为范式。可以理解为,一张数据表的设计结 构需要满足的某种设计标准的 级别 。要想设计一个结构合理的关系型数据库,必须满足一定的范式。\n目前关系型数据库有六种常见范式,按照范式级别,从低到高分别是:第一范式(1nf)、第二范式 (2nf)、第三范式(3nf)、巴斯-科德范式(bcnf)、第四范式(4nf)和第五范式(5nf,又称完美 范式)。\n键和相关属性的概念\n举例:\n这里有两个表:\n球员表(player) :球员编号 | 姓名 | 身份证号 | 年龄 | 球队编号\n球队表(team) :球队编号 | 主教练 | 球队所在地\n超键 :能唯一标识一条记录的属性集叫做超键。属性集就是多个属性的一个集合,如果有一个属性可以唯一的标识一条记录,这个属性和任何的属性组合到一起构成的属性集都能作为超键。 对于球员表来说,超键就是包括球员编号或者身份证号的任意组合,比如(球员编号) (球员编号,姓名)(身份证号,年龄)等。 候选键: 如果超键中不包括多余的属性,那么这个超键就是一个候选键。 对于球员表来说,候选键就是(球员编号)或者(身份证号)。 主键: 用户可以从候选键中选择一个作为主键 外键: 如果数据表r1的某个属性不是r1的主键,而是另一个数据表r2的主键,那么这个属性就是数据表r1的外键。 球员表中的球队编号。 主属性 、 非主属性 :包含在任意候选键中的属性都称之为主属性。与主属性相对,指的是不包含在任何一个候选键中的属性。 在球员表中,主属性是(球员编号)(身份证号),其他的属性(姓名) (年龄)(球队编号)都是非主属性。 第一范式 符合1nf的关系(你可以理解为数据表。“关系模式”和“关系”的区别,类似于面向对象程序设计中”类“与”对象“的区别。”关系“是”关系模式“的一个实例,你可以把”关系”理解为一张带数据的表,而“关系模式”是这张数据表的表结构。1nf的定义为:符合1nf的关系中的每个属性都不可再分,表的每个属性必须具有原子(单个)值。第一范式针对解决属性。\n符合1nf的数据表\n1nf中属性的原子性是 主观的 。\n第二范式 2nf在1nf的基础之上,消除了非主属性对于码(主键)的部分函数依赖。第二范式针对解决非主属性对主属性的依赖关系。\n举例1:\n成绩表 (学号,课程号,成绩)关系中,(学号,课程号)可以决定成绩,但是学号不能决定成绩,课 程号也不能决定成绩,所以“(学号,课程号)→成绩”就是 完全依赖关系 。\n举例2:\n比赛表 player_game ,里面包含球员编号、姓名、年龄、比赛编号、比赛时间和比赛场地等属性,这 里候选键和主键都为(球员编号,比赛编号),我们可以通过候选键(或主键)来决定如下的关系:\n(球员编号, 比赛编号) → (姓名, 年龄, 比赛时间, 比赛场地,得分) 但是这个数据表不满足第二范式,因为数据表中的字段之间还存在着如下的对应关系:\n对于非主属性来说,并非完全依赖候选键(主键)。\n(球员编号) → (姓名,年龄) (比赛编号) → (比赛时间, 比赛场地) 这样的话,每张数据表都符合第二范式,也就避免了异常情况的发生。\n1nf 告诉我们字段属性需要是原子性的,而 2nf 告诉我们一张表就是一个独立的对象,一张表只表达一个意思。\n第三范式 3nf在2nf的基础之上,消除了非主属性对于码(候选码)的传递函数依赖。也就是说, 如果存在非主属性对于码的传递函数依赖,则不符合3nf的要求。所有非主属性之间不能存在依赖关系,必须相互独立。第三范式针对解决非主属性之间的依赖关系。\n举例1:\n部门信息表 :每个部门有部门编号(dept_id)、部门名称、部门简介等信息。\n员工信息表 :每个员工有员工编号、姓名、部门编号。列出部门编号后就不能再将部门名称、部门简介 等与部门有关的信息再加入员工信息表中。\n如果不存在部门信息表,则根据第三范式(3nf)也应该构建它,否则就会有大量的数据冗余。\n举例2:\n球员player表 :球员编号、姓名、球队名称和球队主教练。现在,我们把属性之间的依赖关系画出 来,如下图所示:\n你能看到球员编号决定了球队名称,同时球队名称决定了球队主教练,非主属性球队主教练就会传递依 赖于球员编号,因此不符合 3nf 的要求。\n如果要达到 3nf 的要求,需要把数据表拆成下面这样:\n巴斯范式 在3nf的基础上消除了主属性对候选键的部分依赖或者传递依赖。巴斯范式针对解决主属性之间的依赖关系。\n案例\n在这个表中,一个仓库只有一个管理员,同时一个管理员也只管理一个仓库。我们先来梳理下这些属性 之间的依赖关系。\n仓库名决定了管理员,管理员也决定了仓库名,同时(仓库名,物品名)的属性集合可以决定数量这个 属性。这样,我们就可以找到数据表的候选键。\n候选键 :是(管理员,物品名)和(仓库名,物品名),然后我们从候选键中选择一个作为 主键 ,比 如(仓库名,物品名)。\n主属性 :包含在任一候选键中的属性,也就是仓库名,管理员和物品名。\n非主属性 :数量这个属性。\n是否符合三范式\n如何判断一张表的范式呢?我们需要根据范式的等级,从低到高来进行判断。\n首先,数据表每个属性都是原子性的,符合 1nf 的要求;\n其次,数据表中非主属性”数量“都与候选键全部依赖,(仓库名,物品名)决定数量,(管理员,物品 名)决定数量。因此,数据表符合 2nf 的要求;\n最后,数据表中的非主属性,不传递依赖于候选键,非主属性之间相互独立。因此符合 3nf 的要求。\n存在的问题\n既然数据表已经符合了 3nf 的要求,是不是就不存在问题了呢?我们来看下面的情况:\n增加一个仓库,但是还没有存放任何物品。根据数据表实体完整性的要求,主键不能有空值,因此会出现插入异常 ; 如果仓库更换了管理员,我们就可能会 修改数据表中的多条记录 ; 如果仓库里的商品都卖空了,那么此时仓库名称和相应的管理员名称也会随之被删除。 你能看到,即便数据表符合 3nf 的要求,同样可能存在插入,更新和删除数据的异常情况。 问题解决\n首先我们需要确认造成异常的原因:主属性仓库名对于候选键(管理员,物品名)是部分依赖的关系(对管理员有依赖,物品名没有), 这样就有可能导致上面的异常情况。因此引入bcnf,它在 3nf 的基础上消除了主属性对候选键的部分依 赖或者传递依赖关系。\n如果在关系r中,u为主键,a属性是主键的一个属性,若存在a-\u0026gt;y,y为主属性,则该关系不属于 bcnf 巴斯范式针对解决主属性之间的依赖关系 总结 第一范式针对解决属性 第二范式针对解决非主属性对主属性的依赖关系。 第三范式针对解决非主属性之间的依赖关系。 巴斯范式针对解决主属性之间的依赖关系。 反范式化 规范化 vs 性能\n为满足某种商业目标 , 数据库性能比规范化数据库更重要 在数据规范化的同时 , 要综合考虑数据库的性能 通过在给定的表中添加额外的字段,以大量减少需要从中搜索信息所需的时间 通过在给定的表中插入计算列,以方便查询 举例1:\n员工的信息存储在 employees 表 中,部门信息存储在 departments 表 中。通过 employees 表中的 department_id字段与 departments表建立关联关系。如果要查询一个员工所在部门的名称:\nselect employee_id,department_name from employees e join departments d on e.department_id = d.department_id; 如果经常需要进行这个操作,连接查询就会浪费很多时间。可以在 employees 表中增加一个冗余字段 department_name,这样就不用每次都进行连接操作了。\n举例2:\n反范式化的 goods商品信息表 设计如下:\n反范式的适用场景\n当冗余信息有价值或者能 大幅度提高查询效率 的时候,我们才会采取反范式的优化。\n数据表的设计原则 综合以上内容,总结出数据表设计的一般原则:\u0026ldquo;三少一多\u0026rdquo;\n数据表的个数越少越好 数据表中的字段个数越少越好 数据表中联合主键的字段个数越少越好 使用主键和外键越多越好 注意:这个原则并不是绝对的,有时候我们需要牺牲数据的冗余度来换取数据处理的效率。\n数据库对象编写建议 关于库 【强制】库的名称必须控制在32个字符以内,只能使用英文字母、数字和下划线,建议以英文字 母开头。 【强制】库名中英文 一律小写 ,不同单词采用 下划线 分割。须见名知意。 【强制】库的名称格式:业务系统名称_子系统名。 【强制】库名禁止使用关键字(如type,order等)。 【强制】创建数据库时必须 显式指定字符集 ,并且字符集只能是utf8或者utf8mb4。 创建数据库sql举例:create database crm_fund default character set \u0026lsquo;utf8\u0026rsquo; ; 【建议】对于程序连接数据库账号,遵循 权限最小原则 使用数据库账号只能在一个db下使用,不准跨库。程序使用的账号 原则上不准有drop权限 。 【建议】临时库以 tmp_ 为前缀,并以日期为后缀; 备份库以 bak_ 为前缀,并以日期为后缀。 关于表、列 【强制】表和列的名称必须控制在32个字符以内,表名只能使用英文字母、数字和下划线,建议 以 英文字母开头 。 【强制】 表名、列名一律小写 ,不同单词采用下划线分割。须见名知意。 【强制】表名要求有模块名强相关,同一模块的表名尽量使用 统一前缀 。比如:crm_fund_item 【强制】创建表时必须 显式指定字符集 为utf8或utf8mb4。 【强制】表名、列名禁止使用关键字(如type,order等)。 【强制】创建表时必须 显式指定表存储引擎 类型。如无特殊需求,一律为innodb。 【强制】建表必须有comment。 【强制】字段命名应尽可能使用表达实际含义的英文单词或 缩写 。如:公司 id,不要使用 corporation_id, 而用corp_id 即可。 【强制】布尔值类型的字段命名为 is_描述 。如member表上表示是否为enabled的会员的字段命 名为 is_enabled。_ 【强制】禁止在数据库中存储图片、文件等大的二进制数据 通常文件很大,短时间内造成数据量快速增长,数据库进行数据库读取时,通常会进行大量的随 机io操作,文件很大时,io操作很耗时。通常存储于文件服务器,数据库只存储文件地址信息。 【建议】建表时关于主键: 表必须有主键 (1)强制要求主键为id,类型为int或bigint,且为 auto_increment 建议使用unsigned无符号型。 (2)标识表里每一行主体的字段不要设为主键,建议 设为其他字段如user_id,order_id等,并建立unique key索引。因为如果设为主键且主键值为随机 插入,则会导致innodb内部页分裂和大量随机i/o,性能下降。 【建议】核心表(如用户表)必须有行数据的 创建时间字段 (create_time)和 最后更新时间字段 (update_time),便于查问题。 【建议】表中所有字段尽量都是 not null 属性,业务可以根据需要定义 default值 。 因为使用 null值会存在每一行都会占用额外存储空间、数据迁移容易出错、聚合函数计算结果偏差等问 题。 【建议】所有存储相同数据的 列名和列类型必须一致 (一般作为关联列,如果查询时关联列类型 不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。 【建议】中间表(或临时表)用于保留中间结果集,名称以 tmp_ 开头。 备份表用于备份或抓取源表快照,名称以 bak_ 开头。中间表和备份表定期清理。 【示范】一个较为规范的建表语句: create table user_info ( `id` int unsigned not null auto_increment comment \u0026#39;自增主键\u0026#39;, `user_id` bigint(11) not null comment \u0026#39;用户id\u0026#39;, `username` varchar(45) not null comment \u0026#39;真实姓名\u0026#39;, `email` varchar(30) not null comment \u0026#39;用户邮箱\u0026#39;, `nickname` varchar(45) not null comment \u0026#39;昵称\u0026#39;, `birthday` date not null comment \u0026#39;生日\u0026#39;, `sex` tinyint(4) default \u0026#39;0\u0026#39; comment \u0026#39;性别\u0026#39;, `short_introduce` varchar(150) default null comment \u0026#39;一句话介绍自己,最多50个汉字\u0026#39;, `user_resume` varchar(300) not null comment \u0026#39;用户提交的简历存放地址\u0026#39;, `user_register_ip` int not null comment \u0026#39;用户注册时的源ip\u0026#39;, `create_time` timestamp not null default current_timestamp comment \u0026#39;创建时间\u0026#39;, `update_time` timestamp not null default current_timestamp on update current_timestamp comment \u0026#39;修改时间\u0026#39;, `user_review_status` tinyint not null comment \u0026#39;用户资料审核状态,1为通过,2为审核中,3为未 通过,4为还未提交审核\u0026#39;, primary key (`id`), unique key `uniq_user_id` (`user_id`), key `idx_username`(`username`), key `idx_create_time_status`(`create_time`,`user_review_status`) ) engine=innodb default charset=utf8 comment=\u0026#39;网站用户基本信息\u0026#39; 关于索引 【强制】innodb表必须主键为id int/bigint auto_increment,且主键值 禁止被更新 。 【强制】innodb和myisam存储引擎表,索引类型必须为 btree 。 【建议】主键的名称以 pk_ 开头,唯一键以 uni_ 或 uk_ 开头,普通索引以 idx_ 开头,一律 使用小写格式,以字段的名称或缩写作为后缀。 【建议】多单词组成的columnname,取前几个单词首字母,加末单词组成column_name。如: sample 表 member_id 上的索引:idx_sample_mid。 【建议】单个表上的索引个数 不能超过6个 。 【建议】在建立索引时,多考虑建立 联合索引 ,并把区分度最高的字段放在最前面。 【建议】在多表 join 的sql里,保证被驱动表的连接列上有索引,这样join 执行效率最高。 【建议】建表或加索引时,保证表里互相不存在 冗余索引 。 比如:如果表里已经存在key(a,b), 则key(a)为冗余索引,需要删除。 sql编写 【强制】程序端select语句必须指定具体字段名称,禁止写成 *。 【建议】程序端insert语句指定具体字段名称,不要写成insert into t1 values(…)。 【建议】除静态表或小表(100行以内),dml语句必须有where条件,且使用索引查找。 【建议】insert into…values(xx),(xx),(xx).. 这里xx的值不要超过5000个。 值过多虽然上线很 快,但会引起主从同步延迟。 【建议】select语句不要使用union,推荐使用union all,并且union子句个数限制在5个以 内。 【建议】线上环境,多表 join 不要超过5个表。 【建议】减少使用order by,和业务沟通能不排序就不排序,或将排序放到程序端去做。order by、group by、distinct 这些语句较为耗费cpu,数据库的cpu资源是极其宝贵的。 【建议】包含了order by、group by、distinct 这些查询的语句,where 条件过滤出来的结果 集请保持在1000行以内,否则sql会很慢。 【建议】对单表的多次alter操作必须合并为一次 对于超过100w行的大表进行alter table,必须经过dba审核,并在业务低峰期执行,多个alter需整 合在一起。 因为alter table会产生 表锁 ,期间阻塞对于该表的所有写入,对于业务可能会产生极 大影响。 【建议】批量操作数据时,需要控制事务处理间隔时间,进行必要的sleep。 【建议】事务里包含sql不超过5个。 因为过长的事务会导致锁数据较久,mysql内部缓存、连接消耗过多等问题。 【建议】事务里更新语句尽量基于主键或unique key,如update… where id=xx;否则会产生间隙锁,内部扩大锁定范围,导致系统性能下降,产生死锁。 事务基础知识 数据库事务概述 事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。\n事务处理的原则:保证所有事务都作为 一个工作单元 来执行,即使出现了故障,都不能改变这种执行方 式。当在一个事务中执行多个操作时,要么所有的事务都被提交( commit ),那么这些修改就 永久 地保 存下来;要么数据库管理系统将 放弃 所作的所有 修改 ,整个事务回滚( rollback )到最初状态。\n事务的acid特性:\n原子性(atomicity):原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。 一致性(consistency):根据定义,一致性是指事务执行前后,数据从一个 合法性状态 变换到另外一个 合法性状态 。这种状态 是 语义上 的而不是语法上的,跟具体的业务有关。几个并行执行的事务,其执行结果必须与按某一顺序 串行执行的结果相一致。 隔离型(isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。 持久性(durability):持久性是指一个事务一旦被提交,它对数据库中数据的改变就是 永久性的 ,接下来的其他操作和数据库 故障不应该对其有任何影响。 事务的状态:\n活动的(active):事务对应的数据库操作正在执行过程中时,我们就说该事务处在 活动的 状态。 部分提交的(partially committed): 当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并 没有刷新到磁盘 时,我们就说该事务处在 部分提交的 状态。 失败的(failed): 当事务处在 活动的 或者 部分提交的 状态时,可能遇到了某些错误(数据库自身的错误、操作系统 错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在 失 败的 状态。 中止的(aborted): 如果事务执行了一部分而变为 失败的 状态,那么就需要把已经修改的事务中的操作还原到事务执 行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。我们把这个撤销的过程称 之为 回滚 。当 回滚 操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了中止的 状态。 提交的(committed):当一个处在 部分提交的 状态的事务将修改过的数据都 同步到磁盘 上之后,我们就可以说该事务处 在了 提交的 状态。 使用事务 显式事务 步骤1: start transaction 或者 begin ,作用是显式开启一个事务。\nmysql\u0026gt; begin; #或者 mysql\u0026gt; start transaction; start transaction 语句相较于 begin 特别之处在于,后边能跟随几个 修饰符 :\n① read only :标识当前事务是一个 只读事务 ,也就是属于该事务的数据库操作只能读取数据,而不 能修改数据。\n② read write :标识当前事务是一个 读写事务 ,也就是属于该事务的数据库操作既可以读取数据, 也可以修改数据。\n③ with consistent snapshot :启动一致性读。\n**步骤2:**一系列事务中的操作(主要是dml,不含ddl)\n步骤3:提交事务 或 中止事务(即回滚事务)\n# 提交事务。当提交事务后,对数据库的修改是永久性的。 mysql\u0026gt; commit; # 回滚事务。即撤销正在进行的所有没有提交的修改 mysql\u0026gt; rollback; # 将事务回滚到某个保存点。 mysql\u0026gt; rollback to [savepoint] 隐式事务 mysql中有一个系统变量 autocommit :\nmysql\u0026gt; show variables like \u0026#39;autocommit\u0026#39;; +---------------+-------+ | variable_name | value | +---------------+-------+ | autocommit | on | +---------------+-------+ 1 row in set (0.01 sec) 当然,如果我们想关闭这种 自动提交 的功能,可以使用下边两种方法之一:\n显式的的使用 start transaction 或者 begin 语句开启一个事务。这样在本次事务提交或者回 滚前会暂时关闭掉自动提交的功能。 把系统变量 autocommit 的值设置为 off ,就像这样: set autocommit = off; #或 set autocommit = 0; 隐式提交数据的情况\n数据定义语言(data definition language,缩写为:ddl)\n隐式使用或修改mysql数据库中的表\n事务控制或关于锁定的语句\n① 当我们在一个事务还没提交或者回滚时就又使用 start transaction 或者 begin 语句开启了 另一个事务时,会 隐式的提交 上一个事务。即:\n② 当前的 autocommit 系统变量的值为 off ,我们手动把它调为 on 时,也会 隐式的提交 前边语 句所属的事务。\n③ 使用 lock tables 、 unlock tables 等关于锁定的语句也会 隐式的提交 前边语句所属的事 务。\n加载数据的语句\n关于mysql复制的一些语句\n其它的一些语句\n事务隔离级别 mysql是一个 客户端/服务器 架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每 个客户端与服务器连接上之后,就可以称为一个会话( session )。每个客户端都可以在自己的会话中 向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理 多个事务。事务有 隔离性 的特性,理论上在某个事务 对某个数据进行访问 时,其他事务应该进行 排 队 ,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样对 性能影响太大 ,我们既想保持 事务的隔离性,又想让服务器在处理访问同一数据的多个事务时 性能尽量高些 ,那就看二者如何权衡取 舍了。\n数据并发问题 脏写( dirty write ):对于两个事务 session a、session b,如果事务session a 修改了 另一个 未提交 事务session b 修改过 的数 据,那就意味着发生了 脏写 脏读( dirty read ):对于两个事务 session a、session b,session a 读取 了已经被 session b 更新 但还 没有被提交 的字段。 之后若 session b 回滚 ,session a 读取 的内容就是 临时且无效 的。 不可重复读( non-repeatable read ): 对于两个事务session a、session b,session a 读取 了一个字段,然后 session b 更新 了该字段。 之后 session a 再次读取 同一个字段, 值就不同了。那就意味着发生了不可重复读。 幻读( phantom ): 对于两个事务session a、session b, session a 从一个表中读取了一个字段, 然后 session b 在该表中 插 入 了一些新的行。之后, 如果 session a 再次读取同一个表, 就会多出几行。那就意味着发生了幻读。 幻读的错误解释:\n说幻读是 事务a 执行两次 select 操作得到不同的数据集,即 select 1 得到 10 条记录,select 2 得到 15 条记录。这其实并不是幻读,既然第一次和第二次读取的不一致,那不还是不可重复读吗,所以这是不可重复读的一种。\n幻读的正确解释:\n幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。\nsql中的四种隔离级别 上面介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题有轻重缓急之分,我们给这些问题 按照严重性来排一下序:\n脏写 \u0026gt; 脏读 \u0026gt; 不可重复读 \u0026gt; 幻读 我们愿意舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,并 发问题发生的就越多。 sql标准 中设立了4个 隔离级别 :\nread uncommitted :读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。 read committed :读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做 的改变。这是大多数数据库系统的默认隔离级别(但不是mysql默认的)。可以避免脏读,但不可 重复读、幻读问题仍然存在。 repeatable read :可重复读,事务a在读到一条数据之后,此时事务b对该数据进行了修改并提 交,那么事务a再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍 然存在。这是mysql的默认隔离级别。 serializable :可串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止 其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避 免脏读、不可重复读和幻读。 设置事务的隔离级别 通过下面的语句修改事务的隔离级别:\nset [global|session] transaction isolation level 隔离级别; #其中,隔离级别格式: \u0026gt; read uncommitted \u0026gt; read committed \u0026gt; repeatable read \u0026gt; serializable 或者\nset [global|session] transaction_isolation = \u0026#39;隔离级别\u0026#39; #其中,隔离级别格式: \u0026gt; read-uncommitted \u0026gt; read-committed \u0026gt; repeatable-read \u0026gt; serializable 关于设置时使用global或session的影响:\n使用 global 关键字(在全局范围影响):\nset global transaction isolation level serializable; #或 set global transaction_isolation = \u0026#39;serializable\u0026#39;; 当前已经存在的会话无效 只对执行完该语句之后产生的会话起作用 使用 session 关键字(在会话范围影响):\nset session transaction isolation level serializable; #或 set session transaction_isolation = \u0026#39;serializable\u0026#39;; 对当前会话的所有后续的事务有效 如果在事务之间执行,则对后续的事务有效 该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务 不同隔离级别举例 演示1. 读未提交之脏读\n脏读问题:\n事务1修改的数据未提交,事务2就能够读到事务1未提交的修改数据,出现脏读。\n读未提交,其实就是可以读到其他事务未提交的数据。\n脏写问题:\n演示2:读已提交\n如果事务2进行了提交,id为2的余额修改为100。事务1再次进行查询就会出现余额为100,出现了不可重复读问题。\n演示3:可重复读\n出现演示4问题\n演示4:幻读\nmysql事务日志 事务有4种特性:原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢?\n事务的隔离性由 锁机制 实现。 而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。 redo log 称为 重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持 久性。 undo log 称为 回滚日志 ,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。 有的dba或许会认为 undo 是 redo 的逆过程,其实不然。\nredo日志 为什么需要redo日志?\n一方面,缓冲池可以帮助我们消除cpu和磁盘之间的鸿沟,checkpoint机制可以保证数据的最终落盘,然 而由于checkpoint 并不是每次变更的时候就触发 的,而是master线程隔一段时间去处理的。所以最坏的情 况就是事务提交后,刚写完缓冲池,数据库宕机了,那么这段数据就是丢失的,无法恢复。\n另一方面,事务包含 持久性 的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩 溃,这个事务对数据库中所做的更改也不能丢失。\n那么如何保证这个持久性呢? 一个简单的做法 :在事务提交完成之前把该事务所修改的所有页面都刷新 到磁盘,但是这个简单粗暴的做法有些问题\n另一个解决的思路 :我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系 统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内 存中修改过的全部页面刷新到磁盘,只需要把 修改 了哪些东西 记录一下 就好。比如,某个事务将系统 表空间中 第10号 页面中偏移量为 100 处的那个字节的值 1 改成 2 。我们只需要记录一下:将第0号表 空间的10号页面的偏移量为100处的值更新为 2 。\nredo日志的好处、特点 好处 redo日志降低了刷盘频率 redo日志占用的空间非常小 特点 redo日志是顺序写入磁盘的 事务执行过程中,redo log不断记录 redo的组成 redo log可以简单分为以下两个部分:\n重做日志的缓冲 (redo log buffer) ,保存在内存中,是易失的。 重做日志文件 (redo log file) ,保存在硬盘中,是持久的。 redo的整体流程 第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝 第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值 第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加 写的方式 第4步:定期将内存中修改的数据刷新到磁盘中 redo log的刷盘策略 redo log的写入并不是直接写入磁盘的,innodb引擎会在写redo log的时候先写redo log buffer,之后以 一 定的频率 刷入到真正的redo log file 中。这里的一定频率怎么看待呢?这就是我们要说的刷盘策略。\n注意,redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到 文件系统缓存 (page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系 统自己来决定(比如page cache足够大了)。那么对于innodb来说就存在一个问题,如果交给系统来同 步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。\n针对这种情况,innodb给出 innodb_flush_log_at_trx_commit 参数,该参数控制 commit提交事务 时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:\n设置为0 :表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日 志的同步) 第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝 第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值 第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加 写的方式 第4步:定期将内存中修改的数据刷新到磁盘中 设置为1 :表示每次事务提交时都将进行同步,刷盘操作( 默认值 ) 设置为2 :表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os自 己决定什么时候同步到磁盘文件。 undo日志 redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中 更新数据 的 前置操作 其实是要 先写入一个 undo log 。\n如何理解undo日志 事务需要保证 原子性 ,也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半 会出现一些情况,\n比如: 情况一:事务执行过程中可能遇到各种错误,比如 服务器本身的错误 , 操作系统错误 ,甚至是突 然 断电 导致的错误。 情况二:程序员可以在事务执行过程中手动输入 rollback 语句结束当前事务的执行。 以上情况出现,我们需要把数据改回原先的样子,这个过程称之为 回滚 ,这样就可以造成一个假象:这 个事务看起来什么都没做,所以符合 原子性 要求。\nundo日志的作用 作用1:回滚数据 作用2:mvcc undo的存储结构 回滚段与undo页 innodb对undo log的管理采用段的方式,也就是 回滚段(rollback segment) 。每个回滚段记录了 1024 个 undo log segment ,而在每个undo log segment段中进行 undo页 的申请。\n在 innodb1.1版本之前 (不包括1.1版本),只有一个rollback segment,因此支持同时在线的事务 限制为 1024 。虽然对绝大多数的应用来说都已经够用。 从1.1版本开始innodb支持最大 128个rollback segment ,故其支持同时在线的事务限制提高到 了 128*1024 。 回滚段与事务\n每个事务只会使用一个回滚段,一个回滚段在同一时刻可能会服务于多个事务。\n当一个事务开始的时候,会制定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数 据会被复制到回滚段。\n在回滚段中,事务会不断填充盘区,直到事务结束或所有的空间被用完。如果当前的盘区不够 用,事务会在段中请求扩展下一个盘区,如果所有已分配的盘区都被用完,事务会覆盖最初的盘 区或者在回滚段允许的情况下扩展新的盘区来使用。\n回滚段存在于undo表空间中,在数据库中可以存在多个undo表空间,但同一时刻只能使用一个 undo表空间。\n当事务提交时,innodb存储引擎会做以下两件事情:\n将undo log放入列表中,以供之后的purge操作 判断undo log所在的页是否可以重用,若可以分配给下个事务使用 回滚段中的数据分类\n未提交的回滚数据(uncommitted undo information)\n已经提交但未过期的回滚数据(committed undo information)\n事务已经提交并过期的数据(expired undo information)\nundo log的生命周期 当我们执行insert时:\nbegin; insert into user (name) values (\u0026#34;tom\u0026#34;); 当我们执行update时:\nupdate user set id=2 where id=1; undo log是如何回滚的?\n以上面的例子来说,假设执行rollback,那么对应的流程应该是这样:\n通过undo no=3的日志把id=2的数据删除 通过undo no=2的日志把id=1的数据的deletemark还原成0 通过undo no=1的日志把id=1的数据的name还原成tom 通过undo no=0的日志把id=1的数据删除 undo log的删除\n针对于insert undo log\n因为insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删 除,不需要进行purge操作。\n针对于update undo log\n该undo log可能需要提供mvcc机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等 待purge线程进行最后的删除。\npurge线程是什么:\n为了节省磁盘空间,innodb有专门的purge线程来清理deleted_bit为true的记录。 为了不影响mvcc的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view); 如果某个记录的deleted_bit为true,并且db_trx_id相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。 总结 事务锁 事务的隔离性由这章讲述的 锁 来实现。\nmysql并发事务访问相同记录 读-读情况 读-读 情况,即并发事务相继 读取相同的记录 。读取操作本身不会对记录有任何影响,并不会引起什么 问题,所以允许这种情况的发生。\n写-写情况 写-写 情况,即并发事务相继对相同的记录做出改动。\n在这种情况下会发生 脏写 的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务 相继对一条记录做改动时,需要让它们 排队执行 ,这个排队的过程其实是通过 锁 来实现的。这个所谓 的锁其实是一个 内存中的结构 ,在事务执行前本来是没有锁的,也就是说一开始是没有 锁结构 和记录进 行关联的,如图所示:\n当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的 锁结构 ,当没有的时候 就会在内存中生成一个 锁结构 与之关联。比如,事务 t1 要对这条记录做改动,就需要生成一个 锁结构 与之关联:\n小结几种说法:\n不加锁 意思就是不需要在内存中生成对应的锁结构 ,可以直接执行操作。 获取锁成功,或者加锁成功 意思就是在内存中生成了对应的锁结构 ,而且锁结构的 is_waiting属性为 false ,也就是事务 可以继续执行操作。 获取锁失败,或者加锁失败,或者没有获取到锁 意思就是在内存中生成了对应的 锁结构 ,不过锁结构的 is_waiting 属性为 true ,也就是事务需要等待,不可以继续执行操作。 读-写或写-读情况 读-写 或 写-读 ,即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生 脏读 、 不可重 复读 、 幻读 的问题。\n各个数据库厂商对 sql标准 的支持都可能不一样。比如mysql在 repeatable read 隔离级别上就已经 解决了 幻读 问题。\n并发问题的解决方案 怎么解决 脏读 、 不可重复读 、 幻读 这些问题呢?其实有两种可选的解决方案:\n方案一:读操作利用多版本并发控制( mvcc ),写操作进行 加锁 。 普通的select语句在read committed和repeatable read隔离级别下会使用到mvcc读取记录。\n在 read committed (读已提交)隔离级别下,一个事务在执行过程中每次执行select操作时都会生成一 个readview,readview的存在本身就保证了 事务不可以读取到未提交的事务所做的更改 ,也就 是避免了脏读现象; 在 repeatable read(可重复读) 隔离级别下,一个事务在执行过程中只有 第一次执行select操作 才会 生成一个readview,之后的select操作都 复用 这个readview,这样也就避免了不可重复读 和幻读的问题。 方案二:读、写操作都采用 加锁 的方式。 小结对比发现: 采用 mvcc 方式的话, 读-写 操作彼此并不冲突, 性能更高 。 采用 加锁 方式的话, 读-写 操作彼此需要 排队执行 ,影响性能。 一般情况下我们当然愿意采用 mvcc 来解决 读-写 操作并发执行的问题,但是业务在某些特殊情况 下,要求必须采用 加锁 的方式执行。下面就讲解下mysql中不同类别的锁。\n锁的不同角度分类 从数据操作的类型划分:读锁、写锁 读锁 :也称为 共享锁 、英文用 s 表示。针对同一份数据,多个事务的读操作可以同时进行而不会 互相影响,相互不阻塞的。 写锁 :也称为 排他锁 、英文用 x 表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样 就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。 需要注意的是对于 innodb 引擎来说,读锁和写锁可以加在表上,也可以加在行上。\n从数据操作的粒度划分:表级锁、页级锁、行锁 表锁(table lock) ① 表级别的s锁、x锁\nmysql的表级锁有两种模式:(以myisam表进行操作的演示,因为innodb有更强大的行级锁)\n表共享读锁(table read lock)\n表独占写锁(table write lock)\n锁类型 自己可读 自己可写 自己可操作其他表 他人可读 他人可写 读锁 是 否 否 是 否 写锁 是 是 否 否 否 ② 意向锁 (intention lock)\ninnodb 支持 多粒度锁(multiple granularity locking) ,它允许 行级锁 与 表级锁 共存,而意向 锁就是其中的一种 表锁 。\n意向锁分为两种:\n意向共享锁(intention shared lock, is):事务有意向对表中的某些行加共享锁(s锁)\n-- 事务要获取某些行的 s 锁,必须先获得表的 is 锁。 select column from table ... lock in share mode; 意向排他锁(intention exclusive lock, ix):事务有意向对表中的某些行加排他锁(x锁)\n-- 事务要获取某些行的 x 锁,必须先获得表的 ix 锁。 select column from table ... for update; 即:意向锁是由存储引擎 自己维护的 ,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前, inoodb 会先获取该数据行 所在数据表的对应意向锁 。\n作用:如果另一个任务试图在该表级别上应用共享或排它锁,则受到由第一个任务控制的表级别意向锁的阻塞。第二个任务在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁。解决表锁与之前可能存在的行锁冲突,避免为了判断表是否存在行锁而去扫描全表的系统消耗。\n意向锁的并发性\n意向锁不会与行级的共享 / 排他锁互斥!正因为如此,意向锁并不会影响到多个事务对不同数据行加排 他锁时的并发性。\n结论\ninnodb 支持 多粒度锁 ,特定场景下,行级锁可以与表级锁共存。 意向锁之间互不排斥,但除了 is 与 s 兼容外, 意向锁会与 共享锁 / 排他锁 互斥 。 ix,is是表级锁,不会和行级的x,s锁发生冲突。只会和表级的x,s发生冲突。 意向锁在保证并发性的前提下,实现了 行锁和表锁共存 且 满足事务隔离性 的要求。 ③ 自增锁(auto-inc锁)\ninnodb 是如何保证主键值正确的进行自增的?自增锁解决!\n自增锁是一种比较特殊的表级锁。并且在事务向包含了 auto_increment 列的表中新增数据时就会去持有自增锁,假设事务 a 正在做这个操作,如果另一个事务 b 尝试执行 insert语句,事务 b 会被阻塞住,直到事务 a 释放自增锁。\n④ 元数据锁(mdl锁)\nmysql5.5引入了meta data lock,简称mdl锁,属于表锁范畴。mdl 的作用是,保证读写的正确性。比 如,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个 表结构做变更 ,增加了一 列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。 因此,当对一个表做增删改查操作的时候,加 mdl读锁;当要对表做结构变更操作的时候,加 mdl 写 锁。\ninnodb中的行锁 ① 记录锁(record locks)\n记录锁也就是仅仅把一条记录锁上,官方的类型名称为: lock_rec_not_gap 。比如我们把id值为8的 那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为8的记录,对周围的数据没有影响。\n记录锁是有s锁和x锁之分的,称之为 s型记录锁 和 x型记录锁 。\n当一个事务获取了一条记录的s型记录锁后,其他事务也可以继续获取该记录的s型记录锁,但不可 以继续获取x型记录锁; 当一个事务获取了一条记录的x型记录锁后,其他事务既不可以继续获取该记录的s型记录锁,也不 可以继续获取x型记录锁。 ② 间隙锁(gap locks)\nmysql 在 repeatable read 隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用 mvcc 方 案解决,也可以采用 加锁 方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读 取操作时,那些幻影记录尚不存在,我们无法给这些 幻影记录 加上 记录锁 。innodb提出了一种称之为 gap locks 的锁,官方的类型名称为: lock_gap ,我们可以简称为 gap锁 。比如,把id值为8的那条 记录加一个gap锁的示意图如下。\n图中id值为8的记录加了gap锁,意味着 不允许别的事务在id值为8的记录前边的间隙插入新记录 ,其实就是 id列的值(3, 8)这个区间的新记录是不允许立即插入的。比如,有另外一个事务再想插入一条id值为4的新 记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入 操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3, 8)中的新记录才可以被插入。\ngap锁的提出仅仅是为了防止插入幻影记录而提出的。\n③ 临键锁(next-key locks)\n有时候我们既想 锁住某条记录 ,又想 阻止 其他事务在该记录前边的 间隙插入新记录 ,所以innodb就提 出了一种称之为 next-key locks 的锁,官方的类型名称为: lock_ordinary ,我们也可以简称为 next-key锁 。next-key locks是在存储引擎 innodb 、事务级别在 可重复读 的情况下使用的数据库锁, innodb默认的锁就是next-key locks。\nbegin; select * from student where id \u0026lt;=8 and id \u0026gt; 3 for update; 例:间隙锁在(3,8)区间内,3,8都不可取。如果我们想要保证8也进行加锁,那就需要用到临键锁。\n临键锁兼具记录锁与间隙锁的特征。\n④ 插入意向锁(insert intention locks)\n插入意向锁是间隙锁的一种。\n我们说一个事务在 插入 一条记录时需要判断一下插入位置是不是被别的事务加了 gap锁 ( next-key锁 也包含 gap锁 ),如果有的话,插入操作需要等待,直到拥有 gap锁 的那个事务提交。但是innodb规 定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个 间隙 中 插入 新记录,但是 现在在等待。innodb就把这种类型的锁命名为 insert intention locks ,官方的类型名称为: lock_insert_intention ,我们称为 插入意向锁 。插入意向锁是一种 gap锁 ,不是意向锁,在insert 操作时产生。\n插入意向锁是在插入一条记录行前,由 insert 操作产生的一种间隙锁 。\n事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。\n页锁 页锁就是在 页的粒度 上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我 们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销 介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。\n每个层级的锁数量是有限制的,因为锁会占用内存空间, 锁空间的大小是有限的 。当某个层级的锁数量 超过了这个层级的阈值时,就会进行 锁升级 。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如 innodb 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。\n从对待锁的态度划分:乐观锁、悲观锁 从对待锁的态度来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待 数据并发的思维方式 。需要注意的是,乐观锁和悲观锁并不是锁,而是锁的 设计思想 。\n悲观锁(pessimistic locking) 悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上 锁,这样别人想拿这个数据就会 阻塞 直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞, 用完后再把资源转让给其它线程)。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当 其他线程想要访问数据时,都需要阻塞挂起。java中 synchronized 和 reentrantlock 等独占锁就是 悲观锁思想的实现。\n乐观锁(optimistic locking) 乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新 的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过 程序来实现。在程序上,我们可以采用 版本号机制 或者 cas机制 实现。乐观锁适用于多读的应用类型, 这样可以提高吞吐量。在java中 java.util.concurrent.atomic 包下的原子变量类就是使用了乐观锁 begin; select * from student where id \u0026lt;=8 and id \u0026gt; 3 for update; 的一种实现方式:cas实现的。\n①乐观锁的版本号机制\n在表中设计一个 版本字段 version ,第一次读的时候,会获取 version 字段的取值。然后对数据进行更 新或删除操作时,会执行 update \u0026hellip; set version=version+1 where version=version 。此时 如果已经有事务对这条数据进行了更改,修改就不会成功。\n②乐观锁的时间戳机制\n时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行 比较,如果两者一致则更新成功,否则就是版本冲突。\n你能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或 者时间戳),从而证明当前拿到的数据是否最新。\n从这两种锁的设计思想中,我们总结一下乐观锁和悲观锁的适用场景:\n乐观锁 适合 读操作多 的场景,相对来说写的操作比较少。它的优点在于 程序实现 , 不存在死锁 问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。 悲观锁 适合 写操作多 的场景,因为写的操作具有 排它性 。采用悲观锁的方式,可以在数据库层 面阻止其他事务对该数据的操作权限,防止 读 - 写 和 写 - 写 的冲突。 按加锁的方式划分:显式锁、隐式锁 隐式锁 情景一:对于聚簇索引记录来说,有一个 trx_id 隐藏列,该隐藏列记录着最后改动该记录的 事务 id 。那么如果在当前事务中新插入一条聚簇索引记录后,该记录的 trx_id 隐藏列代表的的就是 当前事务的 事务id ,如果其他事务此时想对该记录添加 s锁 或者 x锁 时,首先会看一下该记录的 trx_id 隐藏列代表的事务是否是当前的活跃事务,如果是的话,那么就帮助当前事务创建一个 x 锁 (也就是为当前事务创建一个锁结构, is_waiting 属性是 false ),然后自己进入等待状态 (也就是为自己也创建一个锁结构, is_waiting 属性是 true )。 情景二:对于二级索引记录来说,本身并没有 trx_id 隐藏列,但是在二级索引页面的 page header 部分有一个 page_max_trx_id 属性,该属性代表对该页面做改动的最大的 事务id ,如 果 page_max_trx_id 属性值小于当前最小的活跃 事务id ,那么说明对该页面做修改的事务都已 经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记 录,然后再重复 情景一 的做法。 隐式锁的逻辑过程如下:\na. innodb的每条记录中都一个隐含的trx_id字段,这个字段存在于聚簇索引的b+tree中。\nb. 在操作一条记录前,首先根据记录中的trx_id检查该事务是否是活动的事务(未提交或回滚)。如果是活 动的事务,首先将 隐式锁 转换为 显式锁 (就是为该事务添加一个锁)。\nc. 检查是否有锁冲突,如果有冲突,创建锁,并设置为waiting状态。如果没有冲突不加锁,跳到e。\nd. 等待加锁成功,被唤醒,或者超时。\ne. 写数据,并将自己的trx_id写入trx_id字段。\n显式锁 通过特定的语句进行加锁,我们一般称之为显示加锁,例如:\n显示加共享锁:\nselect .... lock in share mode 显示加排它锁:\nselect .... for update 其它锁之:全局锁、死锁 全局锁 全局锁就是对 整个数据库实例 加锁。当你需要让整个库处于 只读状态 的时候,可以使用这个命令,之后 其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结 构等)和更新类事务的提交语句。全局锁的典型使用 场景 是:做 全库逻辑备份 。 全局锁的命令:\n死锁 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。\n死锁示例:\n这时候,事务1在等待事务2释放id=2的行锁,而事务2在等待事务1释放id=1的行锁。 事务1和事务2在互 相等待对方的资源释放,就是进入了死锁状态。当出现死锁以后,有 两种策略 :\n一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级 排他锁的事务进行回滚),让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on ,表示开启这个逻辑。 锁的内存结构 innodb 存储引擎中的 锁结构 如下:\n结构解析:\n锁所在的事务信息 : 不论是 表锁 还是 行锁 ,都是在事务执行过程中生成的,哪个事务生成了这个 锁结构 ,这里就记录这个 事务的信息。\n此 锁所在的事务信息 在内存结构中只是一个指针,通过指针可以找到内存中关于该事务的更多信息,比 方说事务id等。\n索引信息 : 对于 行锁 来说,需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针。\n表锁/行锁信息 : 表锁结构 和 行锁结构 在这个位置的内容是不同的:\n表锁\n记载着是对哪个表加的锁,还有其他的一些信息。\n行锁\n记载了三个重要的信息:\nspace id :记录所在表空间。 page number :记录所在页号。 n_bits :对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同 的比特位来区分到底是哪一条记录加了锁。为此在行锁结构的末尾放置了一堆比特位,这个 n_bits 属性代表使用了多少比特位。 type_mode : 这是一个32位的数,被分成了 lock_mode 、 lock_type 和 rec_lock_type 三个部分,如图所示:\n锁的模式( lock_mode ),占用低4位,可选的值如下:\nlock_is (十进制的 0 ):表示共享意向锁,也就是 is锁 。 lock_ix (十进制的 1 ):表示独占意向锁,也就是 ix锁 。 lock_s (十进制的 2 ):表示共享锁,也就是 s锁 。 lock_x (十进制的 3 ):表示独占锁,也就是 x锁 。 lock_auto_inc (十进制的 4 ):表示 auto-inc锁 。 在innodb存储引擎中,lock_is,lock_ix,lock_auto_inc都算是表级锁的模式,lock_s和 lock_x既可以算是表级锁的模式,也可以是行级锁的模式。\n锁的类型( lock_type ),占用第5~8位,不过现阶段只有第5位和第6位被使用:\nlock_table (十进制的 16 ),也就是当第5个比特位置为1时,表示表级锁。 lock_rec (十进制的 32 ),也就是当第6个比特位置为1时,表示行级锁。 行锁的具体类型( rec_lock_type ),使用其余的位来表示。只有在 lock_type 的值为 lock_rec 时,也就是只有在该锁为行级锁时,才会被细分为更多的类型:\nlock_ordinary (十进制的 0 ):表示 next-key锁 。 lock_gap (十进制的 512 ):也就是当第10个比特位置为1时,表示 gap锁 。 lock_rec_not_gap (十进制的 1024 ):也就是当第11个比特位置为1时,表示正经 记录 锁 。 lock_insert_intention (十进制的 2048 ):也就是当第12个比特位置为1时,表示插入 意向锁。 is_waiting 属性呢?基于内存空间的节省,所以把 is_waiting 属性放到了 type_mode 这个32 位的数字中:\nlock_wait (十进制的 256 ) :当第9个比特位置为 1 时,表示 is_waiting 为 true ,也 就是当前事务尚未获取到锁,处在等待状态;当这个比特位为 0 时,表示 is_waiting 为 false ,也就是当前事务获取锁成功。 多版本并发控制 什么是mvcc mvcc (multiversion concurrency control),多版本并发控制。顾名思义,mvcc 是通过数据行的多个版本管理来实现数据库的 并发控制 。这项技术使得在innodb的事务隔离级别下执行 一致性读 操作有了保 证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样 在做查询的时候就不用等待另一个事务释放锁。\n快照读与当前读 mvcc在mysql innodb中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突 ,做到 即使有读写冲突时,也能做到 不加锁 , 非阻塞并发读 ,而这个读指的就是 快照读 , 而非 当前读 。当前 读实际上是一种加锁的操作,是悲观锁的实现。而mvcc本质是采用乐观锁思想的一种方式。\n快照读 快照读又叫一致性读,读取的是快照数据。不加锁的简单的 select 都属于快照读,即不加锁的非阻塞 读;\nselect * from user where ...; 之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于mvcc,它在很多情况下, 避免了加锁操作,降低了开销。 既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。\n快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。\n当前读 当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务 不能修改当前记录,会对读取的记录进行加锁。加锁的 select,或者对数据进行增删改都会进行当前 读。比如:\nselect * from student lock in share mode; # 共享锁 select * from student for update; # 排他锁 insert into student values ... # 排他锁 delete from student where ... # 排他锁 update student set ... # 排他锁 前文复习 隔离级别 我们知道事务有 4 个隔离级别,可能存在三种并发问题:\n另图:\n隐藏字段、undo log版本链 回顾一下undo日志的版本链,对于使用 innodb 存储引擎的表来说,它的聚簇索引记录中都包含两个必 要的隐藏列。\ntrx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的 事务id 赋值给 trx_id 隐藏列。 roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然 后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。 insert undo只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的undo log segment也会被系统回收(也就是该undo日志占用的undo页面链表要么被重用,要么被释 放)。\n假设之后两个事务id分别为 10 、 20 的事务对这条记录进行 update 操作,操作流程如下:\n每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个 roll_pointer 属性 ( insert 操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo日志 都连起来,串成一个链表:\n对该记录每次更新后,都会将旧值放到一条 undo日志 中,就算是该记录的一个旧版本,随着更新次数 的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为 版本链 ,版 本链的头节点就是当前记录最新的值。\n每个版本中还包含生成该版本时对应的 事务id 。\nmvcc实现原理之readview mvcc 的实现依赖于:隐藏字段、undo log、read view。\n什么是readview 使用 read committed 和 repeatable read 隔离级别的事务,都必须保证读到 已经提交了的 事务修改 过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问 题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是readview要解决的主要问题。\n这个readview中主要包含4个比较重要的内容,分别如下:\ncreator_trx_id ,创建这个 read view 的事务 id。\n说明:只有在对表中的记录做改动时(执行insert、delete、update这些语句时)才会为 事务分配事务id,否则在一个只读事务中的事务id值都默认为0。\ntrx_ids ,表示在生成readview时当前系统中活跃的读写事务的 事务id列表 。\nup_limit_id ,活跃的事务中最小的事务 id\nlow_limit_id ,表示生成readview时系统中应该分配给下一个事务的 id 值。low_limit_id 是系 统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务id。\n注意:low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有id为1, 2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成readview时, trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。\nreadview的规则 有了这个readview,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见。\n如果被访问版本的trx_id属性值与readview中的 creator_trx_id 值相同,意味着当前事务在访问 它自己修改过的记录,所以该版本可以被当前事务访问。 如果被访问版本的trx_id属性值小于readview中的 up_limit_id 值,表明生成该版本的事务在当前 事务生成readview前已经提交,所以该版本可以被当前事务访问。 如果被访问版本的trx_id属性值大于或等于readview中的 low_limit_id 值,表明生成该版本的事 务在当前事务生成readview后才开启,所以该版本不可以被当前事务访问。 如果被访问版本的trx_id属性值在readview的 up_limit_id 和 low_limit_id 之间,那就需要判 断一下trx_id属性值是不是在 trx_ids 列表中。 如果在,说明创建readview时生成该版本的事务还是活跃的,该版本不可以被访问。 如果不在,说明创建readview时生成该版本的事务已经被提交,该版本可以被访问。 mvcc整体操作流程 了解了这些概念之后,我们来看下当查询一条记录的时候,系统如何通过mvcc找到它:\n首先获取事务自己的版本号,也就是事务 id; 获取 readview; 查询得到的数据,然后与 readview 中的事务版本号进行比较; 如果不符合 readview 规则,就需要从 undo log 中获取历史快照; 最后返回符合规则的数据。 在隔离级别为读已提交(read committed)时,一个事务中的每一次 select 查询都会重新获取一次 read view。如表所示:\n注意,此时同样的查询语句都会重新获取一次 read view,这时如果 read view 不同,就可能产生 不可重复读或者幻读的情况。\n当隔离级别为可重复读的时候,就避免了不可重复读,这是因为一个事务只在第一次 select 的时候会 获取一次 read view,而后面所有的 select 都会复用这个 read view,如下表所示:\n举例说明 read committed隔离级别下 read committed :每次读取数据前都生成一个readview。\n现在有两个 事务id 分别为 10 、 20 的事务在执行:\n# transaction 10 begin; update student set name=\u0026#34;李四\u0026#34; where id=1; update student set name=\u0026#34;王五\u0026#34; where id=1; # transaction 20 begin; # 更新了一些别的表的记录 ... 此刻,表student 中 id 为 1 的记录得到的版本链表如下所示:\n假设现在有一个使用 read committed 隔离级别的事务开始执行:\n# 使用read committed隔离级别的事务 begin; # select1:transaction 10、20未提交 select * from student where id = 1; # 得到的列name的值为\u0026#39;张三\u0026#39; 之后,我们把 事务id 为 10 的事务提交一下:\n# transaction 10 begin; update student set name=\u0026#34;李四\u0026#34; where id=1; update student set name=\u0026#34;王五\u0026#34; where id=1; commit; 然后再到 事务id 为 20 的事务中更新一下表 student 中 id 为 1 的记录:\n# transaction 20 begin; # 更新了一些别的表的记录 ... update student set name=\u0026#34;钱七\u0026#34; where id=1; update student set name=\u0026#34;宋八\u0026#34; where id=1; 此刻,表student中 id 为 1 的记录的版本链就长这样:\n然后再到刚才使用 read committed 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:\n# 使用read committed隔离级别的事务 begin; # select1:transaction 10、20均未提交 select * from student where id = 1; # 得到的列name的值为\u0026#39;张三\u0026#39; # select2:transaction 10提交,transaction 20未提交 select * from student where id = 1; # 得到的列name的值为\u0026#39;王五\u0026#39; repeatable read隔离级别下 使用 repeatable read 隔离级别的事务来说,只会在第一次执行查询语句时生成一个 readview ,之 后的查询就不会重复生成了。\n比如,系统里有两个 事务id 分别为 10 、 20 的事务在执行:\n# transaction 10 begin; update student set name=\u0026#34;李四\u0026#34; where id=1; update student set name=\u0026#34;王五\u0026#34; where id=1; # transaction 20 begin; # 更新了一些别的表的记录 ... 此刻,表student 中 id 为 1 的记录得到的版本链表如下所示: 假设现在有一个使用 repeatable read 隔离级别的事务开始执行:\n# 使用repeatable read隔离级别的事务 begin; # select1:transaction 10、20未提交 select * from student where id = 1; # 得到的列name的值为\u0026#39;张三\u0026#39; 之后,我们把 事务id 为 10 的事务提交一下,就像这样:\n# transaction 10 begin; update student set name=\u0026#34;李四\u0026#34; where id=1; update student set name=\u0026#34;王五\u0026#34; where id=1; commit; 然后再到 事务id 为 20 的事务中更新一下表 student 中 id 为 1 的记录:\n# transaction 20 begin; # 更新了一些别的表的记录 ... update student set name=\u0026#34;钱七\u0026#34; where id=1; update student set name=\u0026#34;宋八\u0026#34; where id=1; 此刻,表student 中 id 为 1 的记录的版本链长这样:\n然后再到刚才使用 repeatable read 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:\n# 使用repeatable read隔离级别的事务 begin; # select1:transaction 10、20均未提交 select * from student where id = 1; # 得到的列name的值为\u0026#39;张三\u0026#39; # select2:transaction 10提交,transaction 20未提交 select * from student where id = 1; # 得到的列name的值仍为\u0026#39;张三\u0026#39; 总结 这里介绍了 mvcc 在 read committd 、 repeatable read 这两种隔离级别的事务在执行快照读操作时 访问记录的版本链的过程。这样使不同事务的 读-写 、 写-读 操作并发执行,从而提升系统性能。\n核心点在于 readview 的原理, read committd 、 repeatable read 这两个隔离级别的一个很大不同 就是生成readview的时机不同:\nread committd 在每一次进行普通select操作前都会生成一个readview repeatable read 只在第一次进行普通select操作前生成一个readview,之后的查询操作都重复 使用这个readview就好了 其他数据库日志 mysql支持的日志 mysql有不同类型的日志文件,用来存储不同类型的日志,分为 二进制日志 、 错误日志 、 通用查询日志 和 慢查询日志 ,这也是常用的4种。mysql 8又新增两种支持的日志: 中继日志 和 数据定义语句日志 。使 用这些日志文件,可以查看mysql内部发生的事情。\n这6类日志分别为:\n慢查询日志:记录所有执行时间超过long_query_time的所有查询,方便我们对查询进行优化。 通用查询日志:记录所有连接的起始时间和终止时间,以及连接发送给数据库服务器的所有指令, 对我们复原操作的实际场景、发现问题,甚至是对数据库操作的审计都有很大的帮助。 错误日志:记录mysql服务的启动、运行或停止mysql服务时出现的问题,方便我们了解服务器的 状态,从而对服务器进行维护。 二进制日志:记录所有更改数据的语句,可以用于主从服务器之间的数据同步,以及服务器遇到故 障时数据的无损失恢复。 中继日志:用于主从服务器架构中,从服务器用来存放主服务器二进制日志内容的一个中间文件。 从服务器通过读取中继日志的内容,来同步主服务器上的操作。 数据定义语句日志:记录数据定义语句执行的元数据操作。 除二进制日志外,其他日志都是 文本文件 。默认情况下,所有日志创建于 mysql数据目录 中。\n慢查询日志(slow query log) 开启慢查询日志参数 开启slow_query_log mysql \u0026gt; set global slow_query_log=\u0026#39;on\u0026#39;; 然后我们再来查看下慢查询日志是否开启,以及慢查询日志文件的位置:\n你能看到这时慢查询分析已经开启,同时文件保存在 /var/lib/mysql/atguigu02-slow.log 文件 中。\n修改long_query_time阈值 接下来我们来看下慢查询的时间阈值设置,使用如下命令:\nmysql \u0026gt; show variables like \u0026#39;%long_query_time%\u0026#39;; 这里如果我们想把时间缩短,比如设置为 1 秒,可以这样设置:\n#测试发现:设置global的方式对当前session的long_query_time失效。对新连接的客户端有效。所以可以一并 执行下述语句 mysql \u0026gt; set global long_query_time = 1; mysql\u0026gt; show global variables like \u0026#39;%long_query_time%\u0026#39;; mysql\u0026gt; set long_query_time=1; mysql\u0026gt; show variables like \u0026#39;%long_query_time%\u0026#39;; 查看慢查询数目 查询当前系统中有多少条慢查询记录\nshow global status like \u0026#39;%slow_queries%\u0026#39;; 慢查询日志分析工具:mysqldumpslow 在生产环境中,如果要手工分析日志,查找、分析sql,显然是个体力活,mysql提供了日志分析工具 mysqldumpslow 。\n查看mysqldumpslow的帮助信息\nmysqldumpslow 命令的具体参数如下:\n-a: 不将数字抽象成n,字符串抽象成s -s: 是表示按照何种方式排序: c: 访问次数 l: 锁定时间 r: 返回记录 t: 查询时间 al:平均锁定时间 ar:平均返回记录数 at:平均查询时间 (默认方式) ac:平均查询次数 -t: 即为返回前面多少条的数据; -g: 后边搭配一个正则匹配模式,大小写不敏感的; 工作常用参考:\n#得到返回记录集最多的10个sql mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log #得到访问次数最多的10个sql mysqldumpslow -s c -t 10 /var/lib/mysql/atguigu-slow.log #得到按照时间排序的前10条里面含有左连接的查询语句 mysqldumpslow -s t -t 10 -g \u0026#34;left join\u0026#34; /var/lib/mysql/atguigu-slow.log #另外建议在使用这些命令时结合 | 和more 使用 ,否则有可能出现爆屏情况 mysqldumpslow -s r -t 10 /var/lib/mysql/atguigu-slow.log | more 关闭慢查询日志 方式1:永久性方式\n[mysqld] slow_query_log=off #或者,把slow_query_log一项注释掉 或 删除 [mysqld] #slow_query_log =off 重启mysql服务,执行如下语句查询慢日志功能\nshow variables like \u0026#39;%slow%\u0026#39;; #查询慢查询日志所在目录 show variables like \u0026#39;%long_query_time%\u0026#39;; #查询超时时长 方式2:临时性方式\n使用set语句来设置。\n(1)停止mysql慢查询日志功能,具体sql语句如下。\nset global slow_query_log=off (2)重启mysql服务,使用show语句查询慢查询日志功能信息,具体sql语句如下\nshow variables like \u0026#39;%slow%\u0026#39;; #以及 show variables like \u0026#39;%long_query_time%\u0026#39;; 通用查询日志(general query log) 通用查询日志用来 记录用户的所有操作 ,包括启动和关闭mysql服务、所有用户的连接开始时间和截止 时间、发给 mysql 数据库服务器的所有 sql 指令等。当我们的数据发生异常时,查看通用查询日志, 还原操作时的具体场景,可以帮助我们准确定位问题。\n查看当前状态 mysql\u0026gt; show variables like \u0026#39;%general%\u0026#39;; +------------------+------------------------------+ | variable_name | value | +------------------+------------------------------+ | general_log | off | #通用查询日志处于关闭状态 | general_log_file | /var/lib/mysql/atguigu01.log | #通用查询日志文件的名称是atguigu01.log +------------------+------------------------------+ 2 rows in set (0.03 sec) 启动日志 方式1:永久性方式\n修改my.cnf或者my.ini配置文件来设置。在[mysqld]组下加入log选项,并重启mysql服务。格式如下:\n[mysqld] general_log=on general_log_file=[path[filename]] #日志文件所在目录路径,filename为日志文件名 如果不指定目录和文件名,通用查询日志将默认存储在mysql数据目录中的hostname.log文件中, hostname表示主机名。\n方式2:临时性方式\nset global general_log=on; # 开启通用查询日志 set global general_log_file=’path/filename’; # 设置日志文件保存位置 对应的,关闭操作sql命令如下:\nset global general_log=off; # 关闭通用查询日志 show variables like \u0026#39;general_log%\u0026#39;; # 查看设置后的状态 查看日志 通用查询日志是以 文本文件 的形式存储在文件系统中的,可以使用 文本编辑器 直接打开日志文件。每台 mysql服务器的通用查询日志内容是不同的。\nshow variables like \u0026#39;general_log%\u0026#39;; 删除\\刷新日志 如果数据的使用非常频繁,那么通用查询日志会占用服务器非常大的磁盘空间。数据管理员可以删除很 长时间之前的查询日志,以保证mysql服务器上的硬盘空间。 手动删除文件\n使用如下命令重新生成查询日志文件,具体命令如下。刷新mysql数据目录,发现创建了新的日志文 件。前提一定要开启通用日志。\nmysqladmin -uroot -p flush-logs 错误日志(error log) 启动日志 在mysql数据库中,错误日志功能是 默认开启 的。而且,错误日志 无法被禁止 。 默认情况下,错误日志存储在mysql数据库的数据文件夹下,名称默认为 mysqld.log (linux系统)或 hostname.err (mac系统)。如果需要制定文件名,则需要在my.cnf或者my.ini中做如下配置:\n[mysqld] log-error=[path/[filename]] #path为日志文件所在的目录路径,filename为日志文件名 查看日志 mysql错误日志是以文本文件形式存储的,可以使用文本编辑器直接查看。 查询错误日志的存储路径:\n删除\\刷新日志 对于很久以前的错误日志,数据库管理员查看这些错误日志的可能性不大,可以将这些错误日志删除, 以保证mysql服务器上的 硬盘空间 。mysql的错误日志是以文本文件的形式存储在文件系统中的,可以 直接删除 。\n[root@atguigu01 log]# mysqladmin -uroot -p flush-logs enter password: mysqladmin: refresh failed; error: \u0026#39;could not open file \u0026#39;/var/log/mysqld.log\u0026#39; for error logging.\u0026#39; 二进制日志(bin log) binlog可以说是mysql中比较 重要 的日志了,在日常开发及运维过程中,经常会遇到。 binlog即binary log,二进制日志文件,也叫作变更日志(update log)。它记录了数据库所有执行的 ddl 和 dml 等数据库更新事件的语句,但是不包含没有修改任何数据的语句(如数据查询语句select、 show等)。\nbinlog主要应用场景:\n一是用于 数据恢复 二是用于 数据复制 查看默认情况 查看记录二进制日志是否开启:在mysql8中默认情况下,二进制文件是开启的。\nmysql\u0026gt; show variables like \u0026#39;%log_bin%\u0026#39;; +---------------------------------+----------------------------------+ | variable_name | value | +---------------------------------+----------------------------------+ | log_bin | on | | log_bin_basename | /var/lib/mysql/binlog | | log_bin_index | /var/lib/mysql/binlog.index | | log_bin_trust_function_creators | off | | log_bin_use_v1_row_events | off | | sql_log_bin | on | +---------------------------------+----------------------------------+ 6 rows in set (0.00 sec) 日志参数设置 方式1:永久性方式\n[mysqld] #启用二进制日志 log-bin=atguigu-bin binlog_expire_logs_seconds=600 max_binlog_size=100m 重新启动mysql服务,查询二进制日志的信息,执行结果:\nmysql\u0026gt; show variables like \u0026#39;%log_bin%\u0026#39;; +---------------------------------+----------------------------------+ | variable_name | value | +---------------------------------+----------------------------------+ | log_bin | on | | log_bin_basename | /var/lib/mysql/atguigu-bin | | log_bin_index | /var/lib/mysql/atguigu-bin.index | | log_bin_trust_function_creators | off | | log_bin_use_v1_row_events | off | | sql_log_bin | on | +---------------------------------+----------------------------------+ 6 rows in set (0.00 sec) 如果想改变日志文件的目录和名称,可以对my.cnf或my.ini中的log_bin参数修改如下:\n[mysqld] log-bin=\u0026#34;/var/lib/mysql/binlog/atguigu-bin\u0026#34; # 注意:新建的文件夹需要使用mysql用户,使用下面的命令即可。 chown -r -v mysql:mysql binlog 方式2:临时性方式\n# global 级别 mysql\u0026gt; set global sql_log_bin=0; error 1228 (hy000): variable \u0026#39;sql_log_bin\u0026#39; is a session variable and can`t be used with set global # session级别 mysql\u0026gt; set sql_log_bin=0; query ok, 0 rows affected (0.01 秒) 查看日志 查看当前的二进制日志文件列表及大小。指令如下:\nmysql\u0026gt; show binary logs; +--------------------+-----------+-----------+ | log_name | file_size | encrypted | +--------------------+-----------+-----------+ | atguigu-bin.000001 | 156 | no | +--------------------+-----------+-----------+ 1 行于数据集 (0.02 秒) binlog格式查看\nmysql\u0026gt; show variables like \u0026#39;binlog_format\u0026#39;; +---------------+-------+ | variable_name | value | +---------------+-------+ | binlog_format | row | +---------------+-------+ 1 行于数据集 (0.02 秒) 除此之外,binlog还有2种格式,分别是statement和mixed\nstatement 每一条会修改数据的sql都会记录在binlog中。 优点:不需要记录每一行的变化,减少了binlog日志量,节约了io,提高性能。\nrow 5.1.5版本的mysql才开始支持row level 的复制,它不记录sql语句上下文相关信息,仅保存哪条记录被修 改。 优点:row level 的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下 的存储过程,或function,以及trigger的调用和触发无法被正确复制的问题。\nmixed 从5.1.8版本开始,mysql提供了mixed格式,实际上就是statement与row的结合。\n使用日志恢复数据 mysqlbinlog恢复数据的语法如下:\nmysqlbinlog [option] filename|mysql –uuser -ppass; 这个命令可以这样理解:使用mysqlbinlog命令来读取filename中的内容,然后使用mysql命令将这些内容 恢复到数据库中。\nfilename :是日志文件名。 option :可选项,比较重要的两对option参数是\u0026ndash;start-date、\u0026ndash;stop-date 和 \u0026ndash;start-position、\u0026ndash; stop-position。 \u0026ndash;start-date 和 \u0026ndash;stop-date :可以指定恢复数据库的起始时间点和结束时间点。 \u0026ndash;start-position和\u0026ndash;stop-position :可以指定恢复数据的开始位置和结束位置。 注意:使用mysqlbinlog命令进行恢复操作时,必须是编号小的先恢复,例如atguigu-bin.000001必 须在atguigu-bin.000002之前恢复。\n删除二进制日志 mysql的二进制文件可以配置自动删除,同时mysql也提供了安全的手动删除二进制文件的方法。 purge master logs 只删除指定部分的二进制日志文件, reset master 删除所有的二进制日志文 件。具体如下:\npurge master logs:删除指定日志文件\npurge {master | binary} logs to ‘指定日志文件名’ purge {master | binary} logs before ‘指定日期’ 再谈二进制日志(binlog) 写入机制 binlog的写入时机也非常简单,事务执行过程中,先把日志写到 binlog cache ,事务提交的时候,再 把binlog cache写到binlog文件中。因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一 次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。\nwrite和fsync的时机,可以由参数 sync_binlog 控制,默认是 0 。为0的时候,表示每次提交事务都只 write,由系统自行判断什么时候执行fsync。虽然性能得到提升,但是机器宕机,page cache里面的 binglog 会丢失。如下图:\n为了安全起见,可以设置为 1 ,表示每次提交事务都会执行fsync,就如同redo log 刷盘流程一样。 最后还有一种折中方式,可以设置为n(n\u0026gt;1),表示每次提交事务都write,但累积n个事务后才fsync。\n在出现io瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。同样的,如果机器宕 机,会丢失最近n个事务的binlog日志。\nbinlog与redolog对比 redo log 它是 物理日志 ,记录内容是“在某个数据页上做了什么修改”,属于 innodb 存储引擎层产生 的。 而 binlog 是 逻辑日志 ,记录内容是语句的原始逻辑,类似于“给 id=2 这一行的 c 字段加 1”,属于 mysql server 层。 两阶段提交 在执行更新语句过程,会记录redo log与binlog两块日志,以基本的事务为单位,redo log在事务执行过程 中可以不断写入,而binlog只有在提交事务时才写入,所以redo log与binlog的 写入时机 不一样。\nredo log与binlog两份日志之间的逻辑不一致,会出现什么问题?\n由于binlog没写完就异常,这时候binlog里面没有对应的修改记录。\n为了解决两份日志之间的逻辑一致问题,innodb存储引擎使用两阶段提交方案。\n使用两阶段提交后,写入binlog时发生异常也不会有影响\n另一个场景,redo log设置commit阶段发生异常,那会不会回滚事务呢?\n并不会回滚事务,它会执行上图框住的逻辑,虽然redo log是处于prepare阶段,但是能通过事务id找到对 应的binlog日志,所以mysql认为是完整的,就会提交事务恢复数据。\n中继日志(relay log) 中继日志只在主从服务器架构的从服务器上存在。从服务器为了与主服务器保持一致,要从主服务器读 取二进制日志的内容,并且把读取到的信息写入 本地的日志文件 中,这个从服务器本地的日志文件就叫 中继日志 。然后,从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主 从服务器的 数据同步 。\n文件名的格式是: 从服务器名 -relay-bin.序号 。中继日志还有一个索引文件: 从服务器名 -relay\u0002bin.index ,用来定位当前正在使用的中继日志。\n主从复制 主从复制概述 此外,一般应用对数据库而言都是“ 读多写少 ”,也就说对数据库读取数据的压力比较大,有一个思路就 是采用数据库集群的方案,做 主从架构 、进行 读写分离 ,这样同样可以提升数据库的并发处理能力。但 并不是所有的应用都需要对数据库进行主从架构的设置,毕竟设置架构本身是有成本的。 如果我们的目的在于提升数据库高并发访问的效率,那么首先考虑的是如何 优化sql和索引 ,这种方式 简单有效;其次才是采用 缓存的策略 ,比如使用 redis将热点数据保存在内存数据库中,提升读取的效 率;最后才是对数据库采用 主从架构 ,进行读写分离。\n主从复制的作用 第1个作用:读写分离\n第2个作用就是数据备份。\n第3个作用是具有高可用性。\n主从复制的原理 slave 会从 master 读取 binlog 来进行数据同步。\n复制三步骤\n步骤1: master 将写操作记录到二进制日志( binlog )。\n步骤2: slave 将 master 的binary log events拷贝到它的中继日志( relay log );\n步骤3: slave 重做中继日志中的事件,将改变应用到自己的数据库中。 mysql复制是异步的且串行化 的,而且重启后从 接入点 开始复制。\n复制的最大问题: 延时\n复制的基本原则\n每个 slave 只有一个 master 每个 slave 只能有一个唯一的服务器id 每个 master 可以有多个 slave 同步数据一致性问题 主从同步的要求:\n读库和写库的数据一致(最终一致);\n写数据必须写到写库;\n读数据必须到读库(不一定);\n主从延迟问题:\n进行主从同步的内容是二进制日志,它是一个文件,在进行 网络传输 的过程中就一定会 存在主从延迟 (比如 500ms),这样就可能造成用户在从库上读取的数据不是最新的数据,也就是主从同步中的 数据 不一致性 问题。\n主从延迟问题原因:\n在网络正常的时候,日志从主库传给从库所需的时间是很短的,即t2-t1的值是非常小的。即,网络正常 情况下,主备延迟的主要来源是备库接收完binlog和执行完这个事务之间的时间差。\n主备延迟最直接的表现是,从库消费中继日志(relay log)的速度,比主库生产binlog的速度要慢。造 成原因:\n1、从库的机器性能比主库要差\n2、从库的压力大\n3、大事务的执行\n如何减少主从延迟:\n若想要减少主从延迟的时间,可以采取下面的办法:\n降低多线程大事务并发的概率,优化业务逻辑 优化sql,避免慢sql, 减少批量操作 ,建议写脚本以update-sleep这样的形式完成。 提高从库机器的配置 ,减少主库写binlog和从库读binlog的效率差。 尽量采用 短的链路 ,也就是主库和从库服务器的距离尽量要短,提升端口带宽,减少binlog传输 的网络延时。 实时性要求的业务读强制走主库,从库只做灾备,备份。 如何解决一致性问题:\n如果操作的数据存储在同一个数据库中,那么对数据进行更新的时候,可以对记录加写锁,这样在读取 的时候就不会发生数据不一致的情况。但这时从库的作用就是 备份 ,并没有起到 读写分离 ,分担主库 读压力 的作用。\n读写分离情况下,解决主从同步中数据不一致的问题, 就是解决主从之间 数据复制方式 的问题,如果按 照数据一致性 从弱到强 来进行划分,有以下 3 种复制方式。\n方法 1:异步复制\n方法 2:半同步复制\n方法 3:组复制\n异步复制和半同步复制都无法最终保证数据的一致性问题,半同步复制是通过判断从库响应的个数来决 定是否返回给客户端,虽然数据一致性相比于异步复制有提升,但仍然无法满足对数据一致性要求高的 场景,比如金融领域。mgr 很好地弥补了这两种复制模式的不足。 组复制技术,简称 mgr(mysql group replication)。是 mysql 在 5.7.17 版本中推出的一种新的数据复 制技术,这种复制技术是基于 paxos 协议的状态机复制。\nmgr 是如何工作的:\n首先我们将多个节点共同组成一个复制组,在 执行读写(rw)事务 的时候,需要通过一致性协议层 (consensus 层)的同意,也就是读写事务想要进行提交,必须要经过组里“大多数人”(对应 node 节 点)的同意,大多数指的是同意的节点数量需要大于 (n/2+1),这样才可以进行提交,而不是原发起 方一个说了算。而针对 只读(ro)事务 则不需要经过组内同意,直接 commit 即可。\n在一个复制组内有多个节点组成,它们各自维护了自己的数据副本,并且在一致性协议层实现了原子消 息和全局有序消息,从而保证组内数据的一致性。\nmgr 将 mysql 带入了数据强一致性的时代,是一个划时代的创新,其中一个重要的原因就是mgr 是基 于 paxos 协议的。paxos 算法是由 2013 年的图灵奖获得者 leslie lamport 于 1990 年提出的,有关这个算 法的决策机制可以搜一下。事实上,paxos 算法提出来之后就作为 分布式一致性算法 被广泛应用,比如 apache 的 zookeeper 也是基于 paxos 实现的。\n数据库备份与恢复 物理备份与逻辑备份 物理备份:备份数据文件,转储数据库物理文件到某一目录。物理备份恢复速度比较快,但占用空间比 较大,mysql中可以用 xtrabackup 工具来进行物理备份。\n逻辑备份:对数据库对象利用工具进行导出工作,汇总入备份文件内。逻辑备份恢复速度慢,但占用空 间小,更灵活。mysql 中常用的逻辑备份工具为 mysqldump 。逻辑备份就是 备份sql语句 ,在恢复的 时候执行备份的sql语句实现数据库数据的重现。\nmysqldump实现逻辑备份 备份一个数据库\nmysqldump –u 用户名称 –h 主机名称 –p密码 待备份的数据库名称[tbname, [tbname...]]\u0026gt; 备份文件名 称.sql 说明: 备份的文件并非一定要求后缀名为.sql,例如后缀名为.txt的文件也是可以的。\n举例:使用root用户备份atguigu数据库:\nmysqldump -uroot -p atguigu\u0026gt;atguigu.sql #备份文件存储在当前目录下 mysqldump -uroot -p atguigudb1 \u0026gt; /var/lib/mysql/atguigu.sql 备份全部数据库\n若想用mysqldump备份整个实例,可以使用 \u0026ndash;all-databases 或 -a 参数:\nmysqldump -uroot -pxxxxxx --all-databases \u0026gt; all_database.sql mysqldump -uroot -pxxxxxx -a \u0026gt; all_database.sql 备份部分数据库\n使用 \u0026ndash;databases 或 -b 参数了,该参数后面跟数据库名称,多个数据库间用空格隔开。如果指定 databases参数,备份文件中会存在创建数据库的语句,如果不指定参数,则不存在。语法如下:\nmysqldump –u user –h host –p --databases [数据库的名称1 [数据库的名称2...]] \u0026gt; 备份文件名 称.sql 举例:\nmysqldump -uroot -p --databases atguigu atguigu12 \u0026gt;two_database.sql #或者 mysqldump -uroot -p -b atguigu atguigu12 \u0026gt; two_database.sql 备份部分表\n比如,在表变更前做个备份。语法如下:\nmysqldump –u user –h host –p 数据库的名称 [表名1 [表名2...]] \u0026gt; 备份文件名称.sql 举例:备份atguigu数据库下的book表\nmysqldump -uroot -p atguigu book\u0026gt; book.sql 可以看到,book文件和备份的库文件类似。不同的是,book文件只包含book表的drop、create和 insert语句。\n备份多张表使用下面的命令,比如备份book和account表:\n#备份多张表 mysqldump -uroot -p atguigu book account \u0026gt; 2_tables_bak.sql 备份单表的部分数据\n有些时候一张表的数据量很大,我们只需要部分数据。这时就可以使用 \u0026ndash;where 选项了。where后面附 带需要满足的条件。\n举例:备份student表中id小于10的数据:\nmysqldump -uroot -p atguigu student --where=\u0026#34;id \u0026lt; 10 \u0026#34; \u0026gt; student_part_id10_low_bak.sql 只备份结构或只备份数据\n只备份结构的话可以使用 \u0026ndash;no-data 简写为 -d 选项;只备份数据可以使用 \u0026ndash;no-create-info 简写为 -t 选项\n只备份结构 mysqldump -uroot -p atguigu --no-data \u0026gt; atguigu_no_data_bak.sql #使用grep命令,没有找到insert相关语句,表示没有数据备份。 [root@node1 ~]# grep \u0026#34;insert\u0026#34; atguigu_no_data_bak.sql 只备份数据 mysqldump -uroot -p atguigu --no-create-info \u0026gt; atguigu_no_create_info_bak.sql #使用grep命令,没有找到create相关语句,表示没有数据结构。 [root@node1 ~]# grep \u0026#34;create\u0026#34; atguigu_no_create_info_bak.sql 备份中包含存储过程、函数、事件\nmysqldump备份默认是不包含存储过程,自定义函数及事件的。可以使用 \u0026ndash;routines 或 -r 选项来备 份存储过程及函数,使用 \u0026ndash;events 或 -e 参数来备份事件。\n举例:备份整个atguigu库,包含存储过程及事件:\n使用下面的sql可以查看当前库有哪些存储过程或者函数 mysql\u0026gt; select specific_name,routine_type ,routine_schema from information_schema.routines where routine_schema=\u0026#34;atguigudb1\u0026#34;; mysqldump -uroot -p -r -e --databases atguigu \u0026gt; fun_atguigu_bak.sql mysql命令恢复数据 基本语法:\nmysql –u root –p [dbname] \u0026lt; backup.sql 单库备份中恢复单库\n使用root用户,将之前练习中备份的atguigu.sql文件中的备份导入数据库中,命令如下: 如果备份文件中包含了创建数据库的语句,则恢复的时候不需要指定数据库名称,如下所示\nmysql -uroot -p \u0026lt; atguigu.sql 否则需要指定数据库名称,如下所示\nmysql -uroot -p atguigu4\u0026lt; atguigu.sql 全量备份恢复\n如果我们现在有昨天的全量备份,现在想整个恢复,则可以这样操作:\nmysql –u root –p \u0026lt; all.sql mysql -uroot -pxxxxxx \u0026lt; all.sql 从全量备份中恢复单库\n可能有这样的需求,比如说我们只想恢复某一个库,但是我们有的是整个实例的备份,这个时候我们可 以从全量备份中分离出单个库的备份。\n举例:\nsed -n \u0026#39;/^-- current database: `atguigu`/,/^-- current database: `/p\u0026#39; all_database.sql \u0026gt; atguigu.sql #分离完成后我们再导入atguigu.sql即可恢复单个库 表的导出与导入 表的导出 使用select…into outfile导出文本文件\n举例:使用select…into outfile将atguigu数据库中account表中的记录导出到文本文件。\n(1)选择数 据库atguigu,并查询account表,执行结果如下所示。\nuse atguigu; select * from account; mysql\u0026gt; select * from account; +----+--------+---------+ | id | name | balance | +----+--------+---------+ | 1 | 张三 | 90 | | 2 | 李四 | 100 | | 3 | 王五 | 0 | +----+--------+---------+ 3 rows in set (0.01 sec) (2)mysql默认对导出的目录有权限限制,也就是说使用命令行进行导出的时候,需要指定目录进行操 作。\n查询secure_file_priv值:\nmysql\u0026gt; show global variables like \u0026#39;%secure%\u0026#39;; +--------------------------+-----------------------+ | variable_name | value | +--------------------------+-----------------------+ | require_secure_transport | off | | secure_file_priv | /var/lib/mysql-files/ | +--------------------------+-----------------------+ 2 rows in set (0.02 sec) (3)上面结果中显示,secure_file_priv变量的值为/var/lib/mysql-files/,导出目录设置为该目录,sql语 句如下。\nselect * from account into outfile \u0026#34;/var/lib/mysql-files/account.txt\u0026#34;; (4)查看 /var/lib/mysql-files/account.txt`文件。\n1 张三 90 2 李四 100 3 王五 0 表的导入 使用load data infile方式导入文本文件\n举例:\n使用select\u0026hellip;into outfile将atguigu数据库中account表的记录导出到文本文件\nselect * from atguigu.account into outfile \u0026#39;/var/lib/mysql-files/account_0.txt\u0026#39;; 删除account表中的数据:\ndelete from atguigu.account; 从文本文件account.txt中恢复数据:\nload data infile \u0026#39;/var/lib/mysql-files/account_0.txt\u0026#39; into table atguigu.account; 查询account表中的数据:\nmysql\u0026gt; select * from account; +----+--------+---------+ | id | name | balance | +----+--------+---------+ | 1 | 张三 | 90 | | 2 | 李四 | 100 | | 3 | 王五 | 0 | +----+--------+---------+ 3 rows in set (0.00 sec) mysql常用命令 mysql 该mysql不是指mysql服务,而是指mysql的客户端工具。\n连接选项\n#参数 : -u, --user=name 指定用户名 -p, --password[=name] 指定密码 -h, --host=name 指定服务器ip或域名 -p, --port=# 指定连接端口 #示例 : mysql -h 127.0.0.1 -p 3306 -u root -p mysql -h127.0.0.1 -p3306 -uroot -p密码 执行选项\n-e, --execute=name 执行sql语句并退出 此选项可以在mysql客户端执行sql语句,而不用连接到mysql数据库再执行,对于一些批处理脚本,这 种方式尤其方便。\n#示例: mysql -uroot -p db01 -e \u0026#34;select * from tb_book\u0026#34;; mysqladmin mysqladmin 是一个执行管理操作的客户端程序。可以用它来检查服务器的配置和当前状态、创建并删除 数据库等。\n可以通过 : mysqladmin \u0026ndash;help 指令查看帮助文档\n#示例 : mysqladmin -uroot -p create \u0026#39;test01\u0026#39;; mysqladmin -uroot -p drop \u0026#39;test01\u0026#39;; mysqladmin -uroot -p version; mysqlbinlog 由于服务器生成的二进制日志文件以二进制格式保存,所以如果想要检查这些文本的文本格式,就会使 用到mysqlbinlog 日志管理工具。\n语法 :\nmysqlbinlog [options] log-files1 log-files2 ... #选项: -d, --database=name : 指定数据库名称,只列出指定的数据库相关操作。 -o, --offset=# : 忽略掉日志中的前n行命令。 -r,--result-file=name : 将输出的文本格式日志输出到指定文件。 -s, --short-form : 显示简单格式, 省略掉一些信息。 --start-datatime=date1 --stop-datetime=date2 : 指定日期间隔内的所有日志。 --start-position=pos1 --stop-position=pos2 : 指定位置间隔内的所有日志。\tmysqldump mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移。备份内容包含创建表,及插 入表的sql语句。\n语法 :\nmysqldump [options] db_name [tables] mysqldump [options] --database/-b db1 [db2 db3...] mysqldump [options] --all-databases/-a 连接选项\n#参数 : -u, --user=name 指定用户名 -p, --password[=name] 指定密码 -h, --host=name 指定服务器ip或域名 -p, --port=# 指定连接端口 输出内容选项\n#参数: --add-drop-database 在每个数据库创建语句前加上 drop database 语句 --add-drop-table 在每个表创建语句前加上 drop table 语句 , 默认开启 ; 不开启 (-- skip-add-drop-table) -n, --no-create-db 不包含数据库的创建语句 -t, --no-create-info 不包含数据表的创建语句 -d --no-data 不包含数据 -t, --tab=name 自动生成两个文件:一个.sql文件,创建表结构的语句; 一个.txt文件,数据文件,相当于select into outfile #示例 : mysqldump -uroot -p db01 tb_book --add-drop-database --add-drop-table \u0026gt; a mysqldump -uroot -p -t /tmp test city mysqlimport/source mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -t 参数后导出的文本文件。\n语法:\nmysqlimport [options] db_name textfile1 [textfile2...] 实例:\nmysqlimport -uroot -p test /tmp/city.txt # 如果需要导入sql文件,可以使用mysql中的source 指令 source /root/tb_book.sql mysqlshow mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索 引。\n语法:\nmysqlshow [options] [db_name [table_name [col_name]]] #参数 --count 显示数据库及表的统计信息(数据库,表 均可以不指定) -i 显示指定数据库或者指定表的状态信息 示例:\n#查询每个数据库的表的数量及表中记录的数量 mysqlshow -uroot -p --count [root@node1 atguigu2]# mysqlshow -uroot -p --count enter password: +--------------------+--------+--------------+ | databases | tables | total rows | +--------------------+--------+--------------+ | atguigu | 24 | 30107483 | | atguigu12 | 1 | 1 | | atguigu14 | 6 | 14 | | atguigu17 | 1 | 1 | | atguigu18 | 0 | 0 | | atguigu2 | 1 | 3 | | atguigu_myisam | 1 | 4 | | information_schema | 79 | 34034 | | mysql | 38 | 4029 | | performance_schema | 110 | 399957 | | sys | 101 | 7028 | +--------------------+--------+--------------+ 11 rows in set. #查询test库中每个表中的字段书,及行数 mysqlshow -uroot -p atguigu --count [root@node1 atguigu2]# mysqlshow -uroot -p atguigu --count enter password: database: atguigu +------------+----------+------------+ | tables | columns | total rows | +------------+----------+------------+ | account | 3 | 3 | | book | 3 | 100 | | dept | 3 | 3 | | emp | 8 | 10 | | order1 | 2 | 5715448 | | order2 | 2 | 8000327 | | order_test | 2 | 8000327 | | salgrade | 3 | 0 | | stu2 | 6 | 5 | | student | 5 | 8100010 | | t1 | 3 | 210000 | | t_class | 3 | 0 | | test | 2 | 0 | | test_frm | 2 | 0 | | test_paper | 1 | 0 | | ts1 | 2 | 79999 | | type | 2 | 240 | | undo_demo | 3 | 1 | | user | 1 | 1 | | user1 | 4 | 1000 | +------------+----------+------------+ 20 rows in set. #查询test库中book表的详细情况 mysqlshow -uroot -p atguigu book --count [root@node1 atguigu2]# mysqlshow -uroot -p atguigu book --count enter password: database: atguigu table: book rows: 100 +--------+--------------+-----------+------+-----+---------+----------------+--------- ------------------------+---------+ | field | type | collation | null | key | default | extra | privileges | comment | +--------+--------------+-----------+------+-----+---------+----------------+--------- ------------------------+---------+ | bookid | int unsigned | | no | pri | | auto_increment | select,insert,update,references | | | card | int unsigned | | no | mul | | | select,insert,update,references | | | test | varchar(255) | utf8_bin | yes | | | | select,insert,update,references | | +--------+--------------+-----------+------+-----+---------+----------------+--------- ------------------------+--------- ","date":"2023-03-31","permalink":"https://www.holatto.com/posts/mysql/mysql-advanced/","summary":"MySQL的数据目录 MySQL8的主要目录结构 数据库文件的存放路径 mysql\u0026gt; show variables like \u0026#39;datadir\u0026#39;; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | datadir | /var/lib/mysql/ | +---------------+-----------------+ 1 row in set (0.04 sec) 相关命令目录 相关命令目录:/usr/bin(m","title":"初学mysql数据库-高级"},{"content":"进程与线程 进程与线程 进程 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 cpu,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 io 的 。 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等) 线程 一个进程之内可以分为一到多个线程。\n一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 cpu 执行\njava 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作 为线程的容器\n二者对比 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集\n进程拥有共享的资源,如内存空间等,供其内部的线程共享\n进程间通信较为复杂\n同一台计算机的进程通信称为 ipc(inter-process communication) 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 http 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量\n线程更轻量,线程上下文切换成本一般上要比进程上下文切换低\n并发与并行 单核cpu下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是: 微观串行,宏观并行 。\n一般会将这种线程轮流使用 cpu 的做法称为并发, concurrent\n多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。\n引用 rob pike 的一段描述:\n并发(concurrent)是同一时间应对(dealing with)多件事情的能力 并行(parallel)是同一时间动手做(doing)多件事情的能力 应用 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任 务都能拆分(参考后文的【阿姆达尔定律】) 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义 io 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 io】,这时相当于线程虽然不用 cpu,但需要一 直等待 io 结束,没能充分利用线程。所以才有后面的【非阻塞 io】和【异步 io】优化。 java线程 创建和运行线程 方法一,直接使用 thread // 创建线程对象 thread t = new thread() { public void run() { // 要执行的任务 } }; // 启动线程 t.start(); 例如:\n// 构造方法的参数是给线程指定名字,推荐 thread t1 = new thread(\u0026#34;t1\u0026#34;) { @override // run 方法内实现了要执行的任务 public void run() { log.debug(\u0026#34;hello\u0026#34;); } }; t1.start(); 输出:\n19:19:00 [t1] c.threadstarter - hello 方法二,使用 runnable 配合 thread 把【线程】和【任务】(要执行的代码)分开\nthread 代表线程 runnable 可运行的任务(线程要执行的代码) runnable runnable = new runnable() { public void run(){ // 要执行的任务 } }; // 创建线程对象 thread t = new thread( runnable ); // 启动线程 t.start(); 例如:\n// 创建任务对象 runnable task2 = new runnable() { @override public void run() { log.debug(\u0026#34;hello\u0026#34;); } }; // 参数1 是任务对象; 参数2 是线程名字,推荐 thread t2 = new thread(task2, \u0026#34;t2\u0026#34;); t2.start(); 输出:\n9:19:00 [t2] c.threadstarter - hello java 8 以后可以使用 lambda 精简代码\n// 创建任务对象 runnable task2 = () -\u0026gt; log.debug(\u0026#34;hello\u0026#34;); // 参数1 是任务对象; 参数2 是线程名字,推荐 thread t2 = new thread(task2, \u0026#34;t2\u0026#34;); t2.start(); thread 与 runnable 的关系 (源码)\n分析 thread 的源码,理清它与 runnable 的关系\n//runnable源码 public interface runnable { public abstract void run(); } //thread源码(部分) public class thread implements runnable { /* what will be run. */ private runnable target; public thread(runnable target) { init(null, target, \u0026#34;thread-\u0026#34; + nextthreadnum(), 0); } private void init(threadgroup g, runnable target, string name, long stacksize, accesscontrolcontext acc, boolean inheritthreadlocals) { //... this.target = target; //... } @override public void run() { if (target != null) { target.run(); } } 小结\n对于方法一的创建线程方式,是创建了thread的子类并且重写了父类的run方法,因此会调用子类的重写实现方法 对于方法二的创建线程方式,thread类会判断当前target是否为空,不为空则使用runnable的实现 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了 方法三,futuretask 配合 thread futuretask 能够接收 callable 类型的参数,用来处理有返回结果的情况\n// 创建任务对象 futuretask\u0026lt;integer\u0026gt; task3 = new futuretask\u0026lt;\u0026gt;(() -\u0026gt; { log.debug(\u0026#34;hello\u0026#34;); return 100; }); // 参数1 是任务对象; 参数2 是线程名字,推荐 new thread(task3, \u0026#34;t3\u0026#34;).start(); // 主线程阻塞,同步等待 task 执行完毕的结果 integer result = task3.get(); log.debug(\u0026#34;结果是:{}\u0026#34;, result); 输出\n19:22:27 [t3] c.threadstarter - hello 19:22:27 [main] c.threadstarter - 结果是:100 源码分析\n。。。\n查看进程线程的方法 windows 任务管理器可以查看进程和线程数,也可以用来杀死进程 tasklist 查看进程 tasklist | findstr (查找关键字) taskkill 杀死进程 taskkill /f(彻底杀死)/pid(进程pid) linux ps -fe 查看所有进程 ps -ft -p 查看某个进程(pid)的所有线程 kill 杀死进程 top 按大写 h 切换是否显示线程 top -h -p 查看某个进程(pid)的所有线程 java jps 命令查看所有 java 进程 jstack 查看某个 java 进程(pid)的所有线程状态 jconsole 来查看某个 java 进程中线程的运行情况(图形界面) jconsole 远程监控配置\n需要以如下方式运行你的 java 类\njava -djava.rmi.server.hostname=`ip地址` -dcom.sun.management.jmxremote - dcom.sun.management.jmxremote.port=`连接端口` -dcom.sun.management.jmxremote.ssl=是否安全连接 - dcom.sun.management.jmxremote.authenticate=是否认证 java类 关闭防火墙,允许端口\n修改 /etc/hosts 文件将 127.0.0.1 映射至主机名\n如果要认证访问,还需要做如下步骤\n复制 jmxremote.password 文件 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写 连接时填入 controlrole(用户名),r\u0026amp;d(密码) 原理之线程运行 栈与栈帧 java virtual machine stacks (java 虚拟机栈)\n我们都知道 jvm 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟 机就会为其分配一块栈内存。\n每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法 举例:\n方法区中存储每个方法的代码;\n程序计数器为每个线程栈私有的,记录了每个线程执行到了哪行代码;\njvm生成一个main线程栈,主线程中有一个主方法,main线程栈中生成一个main栈帧。\n调用method1(),main线程栈为method1()生成栈帧,并且记录局部变量和返回值地址\nmethod2()同理,当method2()执行完成后,就会将栈帧内存释放掉。\n线程上下文切换(thread context switch) 因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码\n线程的 cpu 时间片用完 垃圾回收 有更高优先级的线程需要运行 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法 当 context switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,java 中对应的概念 就是程序计数器(program counter register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的\n状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等 context switch 频繁发生会影响性能 举例:\n当执行主线程的分片时间到期后(代码还没有执行完成),cpu调用另一个线程,当t1线程执行 完成后,或者分片时间到期后,才能再次执行主线程。\n在这期间,主线程信息将会被保存到内存中,降低性能。\n常见方法 方法 功能 说明 public void start() 启动一个新线程;java虚拟机调用此线程的run方法 start 方法只是让线程进入就绪,里面代码不一定立刻 运行(cpu 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 illegalthreadstateexception public void run() 线程启动后调用该方法 如果在构造 thread 对象时传递了 runnable 参数,则 线程启动后会调用 runnable 中的 run 方法,否则默 认不执行任何操作。但可以创建 thread 的子类对象, 来覆盖默认行为 public void setname(string name) 给当前线程取名字 public void getname() 获取当前线程的名字。线程存在默认名称:子线程是thread-索引,主线程是main public static thread currentthread() 获取当前线程对象,代码在哪个线程中执行 public static void sleep(long time) 让当前线程休眠多少毫秒再继续执行。thread.sleep(0) : 让操作系统立刻重新进行一次cpu竞争 public static native void yield() 提示线程调度器让出当前线程对cpu的使用 主要是为了测试和调试 public final int getpriority() 返回此线程的优先级 public final void setpriority(int priority) 更改此线程的优先级,常用1 5 10 java中规定线程优先级是1~10 的整数,较大的优先级 能提高该线程被 cpu 调度的机率 public void interrupt() 中断这个线程,异常处理机制 public static boolean interrupted() 判断当前线程是否被打断,清除打断标记 public boolean isinterrupted() 判断当前线程是否被打断,不清除打断标记 public final void join() 等待这个线程结束 public final void join(long millis) 等待这个线程死亡millis毫秒,0意味着永远等待 public final native boolean isalive() 线程是否存活(还没有运行完毕) public final void setdaemon(boolean on) 将此线程标记为守护线程或用户线程 public long getid() 获取线程长整型 的 id id 唯一 public state getstate() 获取线程状态 java 中线程状态是用 6 个 enum 表示,分别为: new, runnable, blocked, waiting, timed_waiting, terminated public boolean isinterrupted() 判断是否被打 断 不会清除 打断标记 sleep 调用 sleep 会让当前线程从 running 进入 timed waiting 状态(阻塞)\n其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 interruptedexception\npublic static void main(string[] args) throws interruptedexception { thread t1 = new thread(\u0026#34;t1\u0026#34;) { @override public void run() { log.debug(\u0026#34;enter sleep...\u0026#34;); try { thread.sleep(2000); } catch (interruptedexception e) { log.debug(\u0026#34;wake up...\u0026#34;); e.printstacktrace(); } } }; t1.start(); thread.sleep(1000); log.debug(\u0026#34;interrupt...\u0026#34;); t1.interrupt(); } 输出结果:\n03:47:18.141 c.test7 [t1] - enter sleep... 03:47:19.132 c.test7 [main] - interrupt... 03:47:19.132 c.test7 [t1] - wake up... java.lang.interruptedexception: sleep interrupted at java.lang.thread.sleep(native method) at cn.itcast.test.test7$1.run(test7.java:14) 睡眠结束后的线程未必会立刻得到执行\n建议用 timeunit 的 sleep 代替 thread 的 sleep 来获得更好的可读性 。其底层还是sleep方法。\n@slf4j(topic = \u0026#34;c.test8\u0026#34;) public class test8 { public static void main(string[] args) throws interruptedexception { log.debug(\u0026#34;enter\u0026#34;); timeunit.seconds.sleep(1); log.debug(\u0026#34;end\u0026#34;); // thread.sleep(1000); } } 在循环访问锁的过程中,可以加入sleep让线程阻塞时间,防止大量占用cpu资源。 yield 调用 yield 会让当前线程从 running 进入 runnable 就绪状态,然后调度执行其它线程 具体的实现依赖于操作系统的任务调度器 线程优先级 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用 应用 sleep 实现 在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权 给其他程序\nwhile(true) { try { thread.sleep(50); } catch (interruptedexception e) { e.printstacktrace(); } } 可以用 wait 或 条件变量达到类似的效果 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景 sleep 适用于无需锁同步的场景 join 为什么需要 join 下面的代码执行,打印 r 是什么?\nstatic int r = 0; public static void main(string[] args) throws interruptedexception { test1(); } private static void test1() throws interruptedexception { log.debug(\u0026#34;开始\u0026#34;); thread t1 = new thread(() -\u0026gt; { log.debug(\u0026#34;开始\u0026#34;); sleep(1); log.debug(\u0026#34;结束\u0026#34;); r = 10; }); t1.start(); log.debug(\u0026#34;结果为:{}\u0026#34;, r); log.debug(\u0026#34;结束\u0026#34;); } 分析\n因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0 解决方法\n用 sleep 行不行?为什么? 用 join,加在 t1.start() 之后即可 join类似阻塞队列,等到线程完成业务后放行 有时效的join 当线程执行时间没有超过join设定时间\nstatic int r1 = 0; static int r2 = 0; public static void main(string[] args) throws interruptedexception { test3(); } public static void test3() throws interruptedexception { thread t1 = new thread(() -\u0026gt; { sleep(1); r1 = 10; }); long start = system.currenttimemillis(); t1.start(); // 线程执行结束会导致 join 结束 t1.join(1500); long end = system.currenttimemillis(); log.debug(\u0026#34;r1: {} r2: {} cost: {}\u0026#34;, r1, r2, end - start); } 输出\n20:48:01.320 [main] c.testjoin - r1: 10 r2: 0 cost: 1010 当执行时间超时\nstatic int r1 = 0; static int r2 = 0; public static void main(string[] args) throws interruptedexception { test3(); } public static void test3() throws interruptedexception { thread t1 = new thread(() -\u0026gt; { sleep(2); r1 = 10; }); long start = system.currenttimemillis(); t1.start(); // 线程执行结束会导致 join 结束 t1.join(1500); long end = system.currenttimemillis(); log.debug(\u0026#34;r1: {} r2: {} cost: {}\u0026#34;, r1, r2, end - start); } 输出\n20:52:15.623 [main] c.testjoin - r1: 0 r2: 0 cost: 1502 interrupt interrupt说明\ninterrupt的本质是将线程的打断标记设为true,并调用线程的三个parker对象(c++实现级别)unpark该线程。\n基于以上本质,有如下说明:\n打断线程不等于中断线程,有以下两种情况: 打断正在运行中的线程并不会影响线程的运行,但如果线程监测到了打断标记为true,可以自行决定后续处理。 打断阻塞中的线程会让此线程产生一个interruptedexception异常,结束线程的运行。但如果该异常被线程捕获住,该线程依然可以自行决定后续处理(终止运行,继续运行,做一些善后工作等等) 打断 sleep,wait,join 的线程 这几个方法都会让线程进入阻塞状态\n打断 sleep 的线程, 会清空打断状态,以 sleep 为例\nprivate static void test1() throws interruptedexception { thread t1 = new thread(()-\u0026gt;{ sleep(1); }, \u0026#34;t1\u0026#34;); t1.start(); sleep(0.5); t1.interrupt(); log.debug(\u0026#34; 打断状态: {}\u0026#34;, t1.isinterrupted()); } 输出\njava.lang.interruptedexception: sleep interrupted at java.lang.thread.sleep(native method) at java.lang.thread.sleep(thread.java:340) at java.util.concurrent.timeunit.sleep(timeunit.java:386) at cn.itcast.n2.util.sleeper.sleep(sleeper.java:8) at cn.itcast.n4.testinterrupt.lambda$test1$3(testinterrupt.java:59) at java.lang.thread.run(thread.java:745) 21:18:10.374 [main] c.testinterrupt - 打断状态: false 打断正常运行的线程 打断正常运行的线程, 不会清空打断状态\nprivate static void test2() throws interruptedexception { thread t2 = new thread(()-\u0026gt;{ while(true) { thread current = thread.currentthread(); boolean interrupted = current.isinterrupted(); if(interrupted) { log.debug(\u0026#34; 打断状态: {}\u0026#34;, interrupted); break; } } }, \u0026#34;t2\u0026#34;); t2.start(); sleep(0.5); t2.interrupt(); } 输出\n20:57:37.964 [t2] c.testinterrupt - 打断状态: true 两阶段终止模式 错误思路\n使用线程对象的 stop() 方法停止线程 stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁, 其它线程将永远无法获取锁 使用 system.exit(int) 方法停止线程 目的仅是停止一个线程,但这种做法会让整个程序都停止 利用 isinterrupted\ninterrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行\nclass tptinterrupt { private thread thread; public void start(){ thread = new thread(() -\u0026gt; { while(true) { thread current = thread.currentthread(); if(current.isinterrupted()) { log.debug(\u0026#34;料理后事\u0026#34;); break; } try { thread.sleep(1000); log.debug(\u0026#34;将结果保存\u0026#34;); } catch (interruptedexception e) { current.interrupt(); } // 执行监控操作 } },\u0026#34;监控线程\u0026#34;); thread.start(); } public void stop() { thread.interrupt(); } } 调用\ntptinterrupt t = new tptinterrupt(); t.start(); thread.sleep(3500); log.debug(\u0026#34;stop\u0026#34;); t.stop(); 结果\n11:49:42.915 c.twophasetermination [监控线程] - 将结果保存 11:49:43.919 c.twophasetermination [监控线程] - 将结果保存 11:49:44.919 c.twophasetermination [监控线程] - 将结果保存 11:49:45.413 c.testtwophasetermination [main] - stop 11:49:45.413 c.twophasetermination [监控线程] - 料理后事 打断 park 线程 打断 park 线程, 不会清空打断状态\nprivate static void test3() throws interruptedexception { thread t1 = new thread(() -\u0026gt; { log.debug(\u0026#34;park...\u0026#34;); locksupport.park(); log.debug(\u0026#34;unpark...\u0026#34;); log.debug(\u0026#34;打断状态:{}\u0026#34;, thread.currentthread().isinterrupted()); }, \u0026#34;t1\u0026#34;); t1.start(); sleep(0.5); t1.interrupt(); } 输出\n21:11:52.795 [t1] c.testinterrupt - park... 21:11:53.295 [t1] c.testinterrupt - unpark... 21:11:53.295 [t1] c.testinterrupt - 打断状态:true 如果打断标记已经是 true, 则 park 会失效\nprivate static void test4() { thread t1 = new thread(() -\u0026gt; { for (int i = 0; i \u0026lt; 5; i++) { log.debug(\u0026#34;park...\u0026#34;); locksupport.park(); log.debug(\u0026#34;打断状态:{}\u0026#34;, thread.currentthread().isinterrupted());//获取当前线程被打断状态 log.debug(\u0026#34;打断状态:{}\u0026#34;, thread.interrupted()); // 获取当前线程被打断状态,并且设置状态为false } }); t1.start(); sleep(1); t1.interrupt(); } 输出\n21:13:48.783 [thread-0] c.testinterrupt - park... 21:13:49.809 [thread-0] c.testinterrupt - 打断状态:true 21:13:49.812 [thread-0] c.testinterrupt - park... 21:13:49.813 [thread-0] c.testinterrupt - 打断状态:true 21:13:49.813 [thread-0] c.testinterrupt - park... 21:13:49.813 [thread-0] c.testinterrupt - 打断状态:true 21:13:49.813 [thread-0] c.testinterrupt - park... 21:13:49.813 [thread-0] c.testinterrupt - 打断状态:true 21:13:49.813 [thread-0] c.testinterrupt - park... 21:13:49.813 [thread-0] c.testinterrupt - 打断状态:true 提示\n可以使用 thread.interrupted() 清除打断状态\n主线程与守护线程 默认情况下,java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。\n例:\nlog.debug(\u0026#34;开始运行...\u0026#34;); thread t1 = new thread(() -\u0026gt; { log.debug(\u0026#34;开始运行...\u0026#34;); sleep(2); log.debug(\u0026#34;运行结束...\u0026#34;); }, \u0026#34;daemon\u0026#34;); // 设置该线程为守护线程 t1.setdaemon(true); t1.start(); sleep(1); log.debug(\u0026#34;运行结束...\u0026#34;); 输出:\n08:26:38.123 [main] c.testdaemon - 开始运行... 08:26:38.213 [daemon] c.testdaemon - 开始运行... 08:26:39.215 [main] c.testdaemon - 运行结束... 注意\n垃圾回收器线程就是一种守护线程 tomcat 中的 acceptor 和 poller 线程都是守护线程,所以 tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求 五种状态(操作系统) 这是从 操作系统 层面来描述的\n【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 cpu 调度执行 【运行状态】指获取了 cpu 时间片运行中的状态 当 cpu 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换 【阻塞状态】 如果调用了阻塞 api,如 bio 读写文件,这时该线程实际不会用到 cpu,会导致线程上下文切换,进入 【阻塞状态】 等 bio 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态 六种状态(java api) 根据 thread.state 枚举,分为六种状态\nnew 线程刚被创建,但是还没有调用 start() 方法 runnable 当调用了 start() 方法之后,注意,java api 层面的 runnable 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 bio 导致的线程阻塞,在 java 里无法区分,仍然认为 是可运行) blocked , waiting , timed_waiting 都是 java api 层面对【阻塞状态】的细分,后面会在状态转换一节 详述 terminated 当线程代码运行结束 public static void main(string[] args) throws interruptedexception { thread t1 = new thread(() -\u0026gt; log.debug(\u0026#34;running\u0026#34;), \u0026#34;t1\u0026#34;); thread t2 = new thread(() -\u0026gt; log.debug(\u0026#34;running\u0026#34;), \u0026#34;t2\u0026#34;); t2.start(); thread t3 = new thread(() -\u0026gt;{ while(true){ } }, \u0026#34;t3\u0026#34;); t3.start(); thread t4 = new thread(() -\u0026gt;{ synchronized (test10.class){ try { thread.sleep(10000000); } catch (interruptedexception e) { e.printstacktrace(); } } }, \u0026#34;t4\u0026#34;); t4.start(); thread t5 = new thread(() -\u0026gt;{ try { t3.join(); } catch (interruptedexception e) { e.printstacktrace(); } }, \u0026#34;t5\u0026#34;); t5.start(); thread t6 = new thread(() -\u0026gt;{ synchronized (test10.class){ try { thread.sleep(10000000); } catch (interruptedexception e) { e.printstacktrace(); } } }, \u0026#34;t6\u0026#34;); t6.start(); thread.sleep(500); log.debug(\u0026#34;{}\u0026#34;,t1.getstate()); log.debug(\u0026#34;{}\u0026#34;,t2.getstate()); log.debug(\u0026#34;{}\u0026#34;,t3.getstate()); log.debug(\u0026#34;{}\u0026#34;,t4.getstate()); log.debug(\u0026#34;{}\u0026#34;,t5.getstate()); log.debug(\u0026#34;{}\u0026#34;,t6.getstate()); } //输出 20:05:31 [t2] c.t10 - running 20:05:32 [main] c.t10 - new 20:05:32 [main] c.t10 - terminated 20:05:32 [main] c.t10 - runnable 20:05:32 [main] c.t10 - timed_waiting 20:05:32 [main] c.t10 - waiting 20:05:32 [main] c.t10 - blocked 本章小结 本章的重点在于掌握\n线程创建 线程重要 api,如 start,run,sleep,join,interrupt 等 线程状态 应用方面 异步调用:主线程执行期间,其它线程异步执行耗时操作 提高效率:并行计算,缩短运算时间 同步等待:join 统筹规划:合理使用线程,得到最优效果 原理方面 线程运行流程:栈、栈帧、上下文切换、程序计数器 thread 两种创建方式 的源码 模式方面 终止模式之两阶段终止 共享模型之管程 共享带来问题 例如对于 i++ 而言(i 为静态变量),实际会产生如下的 jvm 字节码指令:\ngetstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 iadd // 自增 putstatic i // 将修改后的值存入静态变量i 而对应 i\u0026ndash; 也是类似:\ngetstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 isub // 自减 putstatic i // 将修改后的值存入静态变量i 而 java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:\n如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:\n但多线程下这 8 行代码可能交错运行: 出现负数的情况:\n出现正数的情况:\n临界区 critical section\n一个程序运行多个线程本身是没有问题的\n问题出在多个线程访问共享资源\n多个线程读共享资源其实也没有问题\n在多个线程对共享资源读写操作时发生指令交错,就会出现问题\n一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区\nstatic int counter = 0; static void increment() // 临界区 { counter++; } static void decrement() // 临界区 { counter--; } 竞态条件 race condition\n多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件\nsynchronized 解决方案 synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换\n注意\n虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:\n互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点 synchronized 语法\nsynchronized(对象) // 线程1, 线程2(blocked) { 临界区 } 解决\nstatic int counter = 0; static final object room = new object(); public static void main(string[] args) throws interruptedexception { thread t1 = new thread(() -\u0026gt; { for (int i = 0; i \u0026lt; 5000; i++) { synchronized (room) { counter++; } } }, \u0026#34;t1\u0026#34;); thread t2 = new thread(() -\u0026gt; { for (int i = 0; i \u0026lt; 5000; i++) { synchronized (room) { counter--; } } }, \u0026#34;t2\u0026#34;); t1.start(); t2.start(); t1.join(); t2.join(); log.debug(\u0026#34;{}\u0026#34;,counter); } 图示流程\n思考\nsynchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。\n为了加深理解,请思考下面的问题\n如果把 synchronized(obj) 放在 for 循环的外面,如何理解?\u0026ndash; 原子性角度 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?\u0026ndash; 锁对象角度 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?\u0026ndash; 锁对象角度 方法上的 synchronized class test{ public synchronized void test() { } } //等价于 class test{ public void test() { synchronized(this) { } } } //---------------------------------- class test{ public synchronized static void test() { } } //等价于 class test{ public static void test() { synchronized(test.class) { } } } 下方情况4 理解对象与类对象差异\n“线程八锁”\n其实就是考察 synchronized 锁住的是哪个对象\n情况1:12 或 21\n@slf4j(topic = \u0026#34;c.number\u0026#34;) class number{ public synchronized void a() { log.debug(\u0026#34;1\u0026#34;); } public synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(string[] args) { number n1 = new number(); new thread(()-\u0026gt;{ n1.a(); }).start(); new thread(()-\u0026gt;{ n1.b(); }).start(); } 情况2:1s后12,或 2 1s后 1\n@slf4j(topic = \u0026#34;c.number\u0026#34;) class number{ public synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(string[] args) { number n1 = new number(); new thread(()-\u0026gt;{ n1.a(); }).start(); new thread(()-\u0026gt;{ n1.b(); }).start(); } 情况3:3 1s 12 或 23 1s 1 或 32 1s 1\n@slf4j(topic = \u0026#34;c.number\u0026#34;) class number{ public synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } public void c() { log.debug(\u0026#34;3\u0026#34;); } } public static void main(string[] args) { number n1 = new number(); new thread(()-\u0026gt;{ n1.a(); }).start(); new thread(()-\u0026gt;{ n1.b(); }).start(); new thread(()-\u0026gt;{ n1.c(); }).start(); } 情况4:2 1s 后 1\n@slf4j(topic = \u0026#34;c.number\u0026#34;) class number{ public synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(string[] args) { number n1 = new number(); number n2 = new number(); new thread(()-\u0026gt;{ n1.a(); }).start(); new thread(()-\u0026gt;{ n2.b(); }).start(); } 情况5:2 1s 后 1\n@slf4j(topic = \u0026#34;c.number\u0026#34;) class number{ public static synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(string[] args) { //锁住的是不同的对象(类对象和普通对象) number n1 = new number(); new thread(()-\u0026gt;{ n1.a(); }).start(); new thread(()-\u0026gt;{ n1.b(); }).start(); } 情况6:1s 后12, 或 2 1s后 1\n@slf4j(topic = \u0026#34;c.number\u0026#34;) class number{ public static synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public static synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(string[] args) { number n1 = new number(); new thread(()-\u0026gt;{ n1.a(); }).start(); new thread(()-\u0026gt;{ n1.b(); }).start(); } 情况7:2 1s 后 1\n@slf4j(topic = \u0026#34;c.number\u0026#34;) class number{ public static synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(string[] args) { number n1 = new number(); number n2 = new number(); new thread(()-\u0026gt;{ n1.a(); }).start(); new thread(()-\u0026gt;{ n2.b(); }).start(); } 情况8:1s 后12, 或 2 1s后 1\n@slf4j(topic = \u0026#34;c.number\u0026#34;) class number{ public static synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public static synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(string[] args) { number n1 = new number(); number n2 = new number(); new thread(()-\u0026gt;{ n1.a(); }).start(); new thread(()-\u0026gt;{ n2.b(); }).start(); } 变量的线程安全分析 成员变量和静态变量是否线程安全?\n如果它们没有共享,则线程安全 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况 如果只有读操作,则线程安全 如果有读写操作,则这段代码是临界区,需要考虑线程安全 局部变量是否线程安全?\n局部变量是线程安全的 但局部变量引用的对象则未必 如果该对象没有逃离方法的作用访问,它是线程安全的 如果该对象逃离方法的作用范围,需要考虑线程安全(成员变量线程分析中验证) 局部变量线程安全分析 public static void test1() { int i = 10; i++; } 每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享\n成员变量线程安全分析 class threadunsafe { arraylist\u0026lt;string\u0026gt; list = new arraylist\u0026lt;\u0026gt;(); public void method1(int loopnumber) { for (int i = 0; i \u0026lt; loopnumber; i++) { // { 临界区, 会产生竞态条件 method2(); method3(); // } 临界区 } } private void method2() { list.add(\u0026#34;1\u0026#34;); } private void method3() { list.remove(0); } } 执行\nstatic final int thread_number = 2; static final int loop_number = 200; public static void main(string[] args) { threadunsafe test = new threadunsafe(); for (int i = 0; i \u0026lt; thread_number; i++) { new thread(() -\u0026gt; { test.method1(loop_number); }, \u0026#34;thread\u0026#34; + i).start(); } } 其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:\nexception in thread \u0026#34;thread1\u0026#34; java.lang.indexoutofboundsexception: index: 0, size: 0 at java.util.arraylist.rangecheck(arraylist.java:657) at java.util.arraylist.remove(arraylist.java:496) at cn.itcast.n6.threadunsafe.method3(testthreadsafe.java:35) at cn.itcast.n6.threadunsafe.method1(testthreadsafe.java:26) at cn.itcast.n6.testthreadsafe.lambda$main$0(testthreadsafe.java:14) at java.lang.thread.run(thread.java:748) 分析:\n无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量 method3 与 method2 分析相同 将 list 修改为局部变量\nclass threadsafe { public final void method1(int loopnumber) { arraylist\u0026lt;string\u0026gt; list = new arraylist\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; loopnumber; i++) { method2(list); method3(list); } } private void method2(arraylist\u0026lt;string\u0026gt; list) { list.add(\u0026#34;1\u0026#34;); } private void method3(arraylist\u0026lt;string\u0026gt; list) { list.remove(0); } } 那么就不会有上述问题了\n分析:\nlist 是局部变量,每个线程调用时会创建其不同实例,没有共享 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象 method3 的参数分析与 method2 相同 方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?\n情况1:有其它线程调用 method2 和 method3 情况2:在 情况1 的基础上,为 threadsafe 类添加子类,子类覆盖 method2 或 method3 方法 class threadsafe { public final void method1(int loopnumber) { arraylist\u0026lt;string\u0026gt; list = new arraylist\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; loopnumber; i++) { method2(list); method3(list); } } private void method2(arraylist\u0026lt;string\u0026gt; list) { list.add(\u0026#34;1\u0026#34;); } public void method3(arraylist\u0026lt;string\u0026gt; list) { list.remove(0); } } class threadsafesubclass extends threadsafe{ @override public void method3(arraylist\u0026lt;string\u0026gt; list) { new thread(() -\u0026gt; { list.remove(0); }).start(); } } 常见线程安全类 string integer stringbuffer random vector hashtable java.util.concurrent 包下的类 这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为\nhashtable table = new hashtable(); new thread(()-\u0026gt;{ table.put(\u0026#34;key\u0026#34;, \u0026#34;value1\u0026#34;); }).start(); new thread(()-\u0026gt;{ table.put(\u0026#34;key\u0026#34;, \u0026#34;value2\u0026#34;); }).start(); 它们的每个方法是原子的 但注意它们多个方法的组合不是原子的,见后面分析 线程安全类方法的组合\n分析下面代码是否线程安全?\nhashtable table = new hashtable(); // 线程1,线程2 if( table.get(\u0026#34;key\u0026#34;) == null) { table.put(\u0026#34;key\u0026#34;, value); } 实例分析 例1:\npublic class myservlet extends httpservlet { // 是否安全 0 map\u0026lt;string,object\u0026gt; map = new hashmap\u0026lt;\u0026gt;(); // 是否安全? 1 string s1 = \u0026#34;...\u0026#34;; // 是否安全? 1 string类型为线程安全 final string s2 = \u0026#34;...\u0026#34;; // 是否安全? 0 date d1 = new date(); // 是否安全? 0 date引用地址没有改变,但是对象可以被改变 final date d2 = new date(); public void doget(httpservletrequest request, httpservletresponse response) { // 使用上述变量 } } 例2:\n如果为单实例,从下往上分析\npublic class myservlet extends httpservlet { // 是否安全? 0 private userservice userservice = new userserviceimpl(); public void doget(httpservletrequest request, httpservletresponse response) { userservice.update(...); } } public class userserviceimpl implements userservice { // 记录调用次数 0 修改共享变量 private int count = 0; public void update() { // ... count++; } } 例3:\n@aspect @component public class myaspect { // 是否安全?0 bean为单实例,即使为多实例也不能保证线程安全,前置增强与后置增强对象可能不一样。 private long start = 0l; @before(\u0026#34;execution(* *(..))\u0026#34;) public void before() { start = system.nanotime(); } @after(\u0026#34;execution(* *(..))\u0026#34;) public void after() { long end = system.nanotime(); system.out.println(\u0026#34;cost time:\u0026#34; + (end-start)); } } 例4:\npublic class myservlet extends httpservlet { // 是否安全 1 private userservice userservice = new userserviceimpl(); public void doget(httpservletrequest request, httpservletresponse response) { userservice.update(...); } } public class userserviceimpl implements userservice { // 是否安全 1 private userdao userdao = new userdaoimpl(); public void update() { userdao.update(); } } public class userdaoimpl implements userdao { public void update() { string sql = \u0026#34;update user set password = ? where username = ?\u0026#34;; // 是否安全 1 没有共享变量 try (connection conn = drivermanager.getconnection(\u0026#34;\u0026#34;,\u0026#34;\u0026#34;,\u0026#34;\u0026#34;)){ // ... } catch (exception e) { // ... } } } 例5:\npublic class myservlet extends httpservlet { // 是否安全 0 private userservice userservice = new userserviceimpl(); public void doget(httpservletrequest request, httpservletresponse response) { userservice.update(...); } } public class userserviceimpl implements userservice { // 是否安全 0 private userdao userdao = new userdaoimpl(); public void update() { userdao.update(); } } public class userdaoimpl implements userdao { // 是否安全 0 共享变量可能别修改 private connection conn = null; public void update() throws sqlexception { string sql = \u0026#34;update user set password = ? where username = ?\u0026#34;; conn = drivermanager.getconnection(\u0026#34;\u0026#34;,\u0026#34;\u0026#34;,\u0026#34;\u0026#34;); // ... conn.close(); } } 例6:\npublic class myservlet extends httpservlet { // 是否安全 1 没有共享变量 private userservice userservice = new userserviceimpl(); public void doget(httpservletrequest request, httpservletresponse response) { userservice.update(...); } } public class userserviceimpl implements userservice { public void update() { userdao userdao = new userdaoimpl(); userdao.update(); } } public class userdaoimpl implements userdao { // 是否安全 1 调用不同的connection private connection = null; public void update() throws sqlexception { string sql = \u0026#34;update user set password = ? where username = ?\u0026#34;; conn = drivermanager.getconnection(\u0026#34;\u0026#34;,\u0026#34;\u0026#34;,\u0026#34;\u0026#34;); // ... conn.close(); } } 例7:\npublic abstract class test { public void bar() { // 是否安全 0 虽然没有共享变量,但是子类可能修改变量状态 simpledateformat sdf = new simpledateformat(\u0026#34;yyyy-mm-dd hh:mm:ss\u0026#34;); foo(sdf); } public abstract foo(simpledateformat sdf); public static void main(string[] args) { new test().bar(); } } 其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法\npublic void foo(simpledateformat sdf) { string datestr = \u0026#34;1999-10-11 00:00:00\u0026#34;; for (int i = 0; i \u0026lt; 20; i++) { new thread(() -\u0026gt; { try { sdf.parse(datestr); } catch (parseexception e) { e.printstacktrace(); } }).start(); } } 请比较 jdk 中 string 类的实现\nmonitor 概念 java 对象头 以 32 位虚拟机为例\n普通对象\n|--------------------------------------------------------------| | object header (64 bits) | |------------------------------------|-------------------------| | mark word (32 bits) | klass word (32 bits) | |------------------------------------|-------------------------| 数组对象\n|---------------------------------------------------------------------------------| | object header (96 bits) | |--------------------------------|-----------------------|------------------------| | mark word(32bits) | klass word(32bits) | array length(32bits) | |--------------------------------|-----------------------|------------------------| 其中 mark word 结构为\n|-------------------------------------------------------|--------------------| | mark word (32 bits) | state | |-------------------------------------------------------|--------------------| | hashcode:25 | age:4 | biased_lock:0 | 01 | normal | |-------------------------------------------------------|--------------------| |thread:23|epoch:2| age:4 | biased_lock:1 | 01 | biased | |-------------------------------------------------------|--------------------| | ptr_to_lock_record:30 | 00 | lightweight locked | |-------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:30 | 10 | heavyweight locked | |-------------------------------------------------------|--------------------| | | 11 | marked for gc | |-------------------------------------------------------|--------------------| 64 位虚拟机 mark word\n|--------------------------------------------------------------------|--------------------| | mark word (64 bits) | state | |--------------------------------------------------------------------|--------------------| | unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | normal | |--------------------------------------------------------------------|--------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | biased | |--------------------------------------------------------------------|--------------------| | ptr_to_lock_record:62 | 00 | lightweight locked | |--------------------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:62 | 10 | heavyweight locked | |--------------------------------------------------------------------|--------------------| | | 11 | marked for gc | |--------------------------------------------------------------------|--------------------| monitor(锁)原理 monitor 被翻译为监视器或管程\n每个 java 对象都可以关联一个 monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 mark word 中就被设置指向 monitor 对象的指针\nmonitor 结构如下\n刚开始 monitor 中 owner 为 null 当 thread-2 执行 synchronized(obj) 就会将 monitor 的所有者 owner 置为 thread-2,monitor中只能有一 个 owner 在 thread-2 上锁的过程中,如果 thread-3,thread-4,thread-5 也来执行 synchronized(obj),就会进入 entrylist blocked thread-2 执行完同步代码块的内容,然后唤醒 entrylist 中等待的线程来竞争锁,竞争的时是非公平的 图中 waitset 中的 thread-0,thread-1 是之前获得过锁,但条件不满足进入 waiting 状态的线程 注意:\nsynchronized 必须是进入同一个对象的 monitor 才有上述的效果 不加 synchronized 的对象不会关联监视器,不遵从以上规则 小故事 故事角色\n老王 - jvm 小南 - 线程 小女 - 线程 房间 - 对象 房间门上 - 防盗锁 - monitor 房间门上 - 小南书包 - 轻量级锁 房间门上 - 刻上小南大名 - 偏向锁 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向 小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样, 即使他离开了,别人也进不了门,他的工作就是安全的。(重量级锁)\n但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女 晚上用。每次上锁太麻烦了,有没有更简单的办法呢? 小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因 此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是 自己的,那么就在门外等,并通知对方下次用锁门的方式。 (轻量级锁)\n后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍 然觉得麻烦。 于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那 么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦 掉,升级为挂书包的方式。 (偏向锁)\n同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老 家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。(锁膨胀)老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字。(批量重偏向)\n后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包。(批量撤销)\nsynchronized原理 static final object lock = new object(); static int counter = 0; public static void main(string[] args) { synchronized (lock) { counter++; } } 对应的字节码为\npublic static void main(java.lang.string[]); descriptor: ([ljava/lang/string;)v flags: acc_public, acc_static code: stack=2, locals=3, args_size=1 0: getstatic #2 // \u0026lt;- lock引用 (synchronized开始) 获取lock对象的引用地址 3: dup\t//复制一份 4: astore_1 // lock引用 -\u0026gt; slot 1\t引用地址存放到slot 1 , 目的是解锁用 5: monitorenter // 将 lock对象 markword 置为 monitor 指针 6: getstatic #3 // \u0026lt;- i 6-11是i++操作 9: iconst_1 // 准备常数 1 10: iadd // +1 11: putstatic #3 // -\u0026gt; i 14: aload_1 // \u0026lt;- lock引用 获取引用地址,准备归还锁 15: monitorexit // 将 lock对象 markword 重置, 唤醒 entrylist 16: goto 24 19: astore_2 // e -\u0026gt; slot 2 出现异常 20: aload_1 // \u0026lt;- lock引用 21: monitorexit // 将 lock对象 markword 重置, 唤醒 entrylist 22: aload_2 // \u0026lt;- slot 2 (e) 23: athrow // throw e 24: return exception table: from to target type 6 16 19 any 19 22 19 any linenumbertable: line 8: 0 line 9: 6 line 10: 14 line 11: 24 localvariabletable: start length slot name signature 0 25 0 args [ljava/lang/string; stackmaptable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 19 locals = [ class \u0026#34;[ljava/lang/string;\u0026#34;, class java/lang/object ] stack = [ class java/lang/throwable ] frame_type = 250 /* chop */ offset_delta = 4 注意\n方法级别的 synchronized 不会在字节码指令中有所体现\nsynchronized 进阶原理 轻量级锁 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。\n轻量级锁对使用者是透明的,即语法仍然是 synchronized\n假设有两个方法同步块,利用同一个对象加锁\nstatic final object obj = new object(); public static void method1() { synchronized( obj ) { // 同步块 a method2(); } } public static void method2() { synchronized( obj ) { // 同步块 b } } 创建锁记录(lock record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁对象的 mark word\n让锁记录中 object reference 指向锁对象(获取锁的地址),并尝试用 cas (保证原子性)替换 object 的 mark word,将锁对象的mark word 的值存入锁记录\n如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下\n如果 cas 失败,有两种情况\n如果是其它线程已经持有了该 object 的轻量级锁,这时表明有竞争,进入锁膨胀过程 如果是自己执行了 synchronized 锁重入,那么再添加一条 lock record 作为重入的计数 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重 入计数减一\n当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用cas将mark word的值恢复给对象头\n成功,则解锁成功 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程 锁膨胀 如果在尝试加轻量级锁的过程中,cas 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有 竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。\nstatic object obj = new object(); public static void method1() { synchronized( obj ) { // 同步块 } } 当 thread-1 进行轻量级加锁时,thread-0 已经对该对象加了轻量级锁 这时 thread-1 加轻量级锁失败,进入锁膨胀流程 即为 object 对象申请 monitor 锁,让 object 指向重量级锁地址 然后自己进入 monitor 的 entrylist blocked 当 thread-0 退出同步块解锁时,使用 cas 将 mark word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 monitor 地址找到 monitor 对象,设置 owner 为 null,唤醒 entrylist 中 blocked 线程 自旋优化 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步 块,释放了锁),这时当前线程就可以避免阻塞。\n自旋重试成功的情况\n线程1 ( core 1上) 对象mark 线程2 ( core 2上) - 10(重量锁) - 访问同步块,获取monitor 10(重量锁)重量锁指针 - 成功(加锁) 10(重量锁)重量锁指针 - 执行同步块 10(重量锁)重量锁指针 - 执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor 执行同步块 10(重量锁)重量锁指针 自旋重试 执行完毕 10(重量锁)重量锁指针 自旋重试 成功(解锁) 01(无锁) 自旋重试 - 10(重量锁)重量锁指针 成功(加锁) - 10(重量锁)重量锁指针 执行同步块 - \u0026hellip; \u0026hellip; 自旋重试失败的情况\n线程1 ( core 1上) 对象mark 线程2( core 2上) - 10(重量锁) - 访问同步块,获取monitor 10(重量锁)重量锁指针 - 成功(加锁) 10(重量锁)重量锁指针 - 执行同步块 10(重量锁)重量锁指针 - 执行同步块 10(重量锁)重量锁指针 访问同步块,获取monitor 执行同步块 10(重量锁)重量锁指针 自旋重试 执行同步块 10(重量锁)重量锁指针 自旋重试 执行同步块 10(重量锁)重量锁指针 自旋重试 执行同步块 10(重量锁)重量锁指针 阻塞 - \u0026hellip; \u0026hellip; 自旋会占用 cpu 时间,单核 cpu 自旋就是浪费,多核 cpu 自旋才能发挥优势。 在 java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。 java 7 之后不能控制是否开启自旋功能 偏向锁 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 cas 操作。\njava 6 中引入了偏向锁来做进一步优化:只有第一次使用 cas 将线程 id 设置到对象的 mark word 头,之后发现 这个线程 id 是自己的就表示没有竞争,不用重新 cas。以后只要不发生竞争,这个对象就归该线程所有\n例如:\nstatic final object obj = new object(); public static void m1() { synchronized( obj ) { // 同步块 a m2(); } } public static void m2() { synchronized( obj ) { // 同步块 b m3(); } } public static void m3() { synchronized( obj ) { // 同步块 c } } graph lr subgraph 偏向锁 t5(\u0026#34;m1内调用synchronized(obj)\u0026#34;) t6(\u0026#34;m2内调用synchronized(obj)\u0026#34;) t7(\u0026#34;m2内调用synchronized(obj)\u0026#34;) t8(对象) t5 -.用threadid替换markword.-\u0026gt; t8 t6 -.检查threadid是否是自己.-\u0026gt; t8 t7 -.检查threadid是否是自己.-\u0026gt; t8 end subgraph 轻量级锁 t1(\u0026#34;m1内调用synchronized(obj)\u0026#34;) t2(\u0026#34;m2内调用synchronized(obj)\u0026#34;) t3(\u0026#34;m2内调用synchronized(obj)\u0026#34;) t1 -.生成锁记录.-\u0026gt; t1 t2 -.生成锁记录.-\u0026gt; t2 t3 -.生成锁记录.-\u0026gt; t3 t4(对象) t1 -.用锁记录替换markword.-\u0026gt; t4 t2 -.用锁记录替换markword.-\u0026gt; t4 t3 -.用锁记录替换markword.-\u0026gt; t4 end 偏向状态 回忆一下对象头格式\n|--------------------------------------------------------------------|--------------------| | mark word (64 bits) | state | |--------------------------------------------------------------------|--------------------| | unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | normal | |--------------------------------------------------------------------|--------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | biased | |--------------------------------------------------------------------|--------------------| | ptr_to_lock_record:62 | 00 | lightweight locked | |--------------------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:62 | 10 | heavyweight locked | |--------------------------------------------------------------------|--------------------| | | 11 | marked for gc | |--------------------------------------------------------------------|--------------------| 一个对象创建时:\n如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 vm 参数- xx:biasedlockingstartupdelay=0来禁用延迟 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值 撤销 调用对象 hashcode 调用了对象的 hashcode,但偏向锁的对象 markword 中存储的是线程 id,如果调用 hashcode 会导致偏向锁被 撤销\n轻量级锁会在锁记录中记录 hashcode 重量级锁会在 monitor 中记录 hashcode 其它线程使用对象 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁\n调用 wait/notify 批量重偏向 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 t1 的对象仍有机会重新偏向 t2,重偏向会重置对象 的 thread id\n当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至 加锁线程\n批量撤销 当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象 都会变为不可偏向的,新建的对象也是不可偏向的\nwait notify wait / notify 原理 owner 线程发现条件不满足,调用 wait 方法,即可进入 waitset 变为 waiting 状态 blocked 和 waiting 的线程都处于阻塞状态,不占用 cpu 时间片 blocked 线程会在 owner 线程释放锁时唤醒 waiting 线程会在 owner 线程调用 notify 或 notifyall 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 entrylist 重新竞争 api 介绍 obj.wait() 让进入 object 监视器的线程到 waitset 等待 obj.notify() 在 object 上正在 waitset 等待的线程中挑一个唤醒 obj.notifyall() 让 object 上正在 waitset 等待的线程全部唤醒 wait() 方法会释放对象的锁,进入 waitset 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止\nwait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify\nwait notify 的正确姿势 开始之前先看看\nsleep(long n)和 wait(long n) 的区别 sleep 是 thread 方法,而 wait 是 object 的方法 sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用 sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁 它们 状态 timed_waiting 五步走 step1\nstatic final object room = new object(); static boolean hascigarette = false; static boolean hastakeout = false; 思考下面的解决方案好不好,为什么?\nnew thread(() -\u0026gt; { synchronized (room) { log.debug(\u0026#34;有烟没?[{}]\u0026#34;, hascigarette); if (!hascigarette) { log.debug(\u0026#34;没烟,先歇会!\u0026#34;); sleep(2); } log.debug(\u0026#34;有烟没?[{}]\u0026#34;, hascigarette); if (hascigarette) { log.debug(\u0026#34;可以开始干活了\u0026#34;); } } }, \u0026#34;小南\u0026#34;).start(); for (int i = 0; i \u0026lt; 5; i++) { new thread(() -\u0026gt; { synchronized (room) { log.debug(\u0026#34;可以开始干活了\u0026#34;); } }, \u0026#34;其它人\u0026#34;).start(); } sleep(1); new thread(() -\u0026gt; { // 这里能不能加 synchronized (room)? hascigarette = true; log.debug(\u0026#34;烟到了噢!\u0026#34;); }, \u0026#34;送烟的\u0026#34;).start(); 输出\n20:49:49.883 [小南] c.testcorrectposture - 有烟没?[false] 20:49:49.887 [小南] c.testcorrectposture - 没烟,先歇会! 20:49:50.882 [送烟的] c.testcorrectposture - 烟到了噢! 20:49:51.887 [小南] c.testcorrectposture - 有烟没?[true] 20:49:51.887 [小南] c.testcorrectposture - 可以开始干活了 20:49:51.887 [其它人] c.testcorrectposture - 可以开始干活了 20:49:51.887 [其它人] c.testcorrectposture - 可以开始干活了 20:49:51.888 [其它人] c.testcorrectposture - 可以开始干活了 20:49:51.888 [其它人] c.testcorrectposture - 可以开始干活了 20:49:51.888 [其它人] c.testcorrectposture - 可以开始干活了 其它干活的线程,都要一直阻塞,效率太低 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加 synchronized 就好像 main 线程是翻窗户进来的 解决方法,使用 wait - notify 机制 step 2\n思考下面的实现行吗,为什么?\nnew thread(() -\u0026gt; { synchronized (room) { log.debug(\u0026#34;有烟没?[{}]\u0026#34;, hascigarette); if (!hascigarette) { log.debug(\u0026#34;没烟,先歇会!\u0026#34;); try { room.wait(2000); } catch (interruptedexception e) { e.printstacktrace(); } } log.debug(\u0026#34;有烟没?[{}]\u0026#34;, hascigarette); if (hascigarette) { log.debug(\u0026#34;可以开始干活了\u0026#34;); } } }, \u0026#34;小南\u0026#34;).start(); for (int i = 0; i \u0026lt; 5; i++) { new thread(() -\u0026gt; { synchronized (room) { log.debug(\u0026#34;可以开始干活了\u0026#34;); } }, \u0026#34;其它人\u0026#34;).start(); } sleep(1); new thread(() -\u0026gt; { synchronized (room) { hascigarette = true; log.debug(\u0026#34;烟到了噢!\u0026#34;); room.notify(); } }, \u0026#34;送烟的\u0026#34;).start(); 解决了其它干活的线程阻塞的问题 但如果有其它线程也在等待条件呢? step 3\nnew thread(() -\u0026gt; { synchronized (room) { log.debug(\u0026#34;有烟没?[{}]\u0026#34;, hascigarette); if (!hascigarette) { log.debug(\u0026#34;没烟,先歇会!\u0026#34;); try { room.wait(); } catch (interruptedexception e) { e.printstacktrace(); } } log.debug(\u0026#34;有烟没?[{}]\u0026#34;, hascigarette); if (hascigarette) { log.debug(\u0026#34;可以开始干活了\u0026#34;); } else { log.debug(\u0026#34;没干成活...\u0026#34;); } } }, \u0026#34;小南\u0026#34;).start(); new thread(() -\u0026gt; { synchronized (room) { thread thread = thread.currentthread(); log.debug(\u0026#34;外卖送到没?[{}]\u0026#34;, hastakeout); if (!hastakeout) { log.debug(\u0026#34;没外卖,先歇会!\u0026#34;); try { room.wait(); } catch (interruptedexception e) { e.printstacktrace(); } } log.debug(\u0026#34;外卖送到没?[{}]\u0026#34;, hastakeout); if (hastakeout) { log.debug(\u0026#34;可以开始干活了\u0026#34;); } else { log.debug(\u0026#34;没干成活...\u0026#34;); } } }, \u0026#34;小女\u0026#34;).start(); sleep(1); new thread(() -\u0026gt; { synchronized (room) { hastakeout = true; log.debug(\u0026#34;外卖到了噢!\u0026#34;); room.notify(); } }, \u0026#34;送外卖的\u0026#34;).start(); 输出\n20:53:12.173 [小南] c.testcorrectposture - 有烟没?[false] 20:53:12.176 [小南] c.testcorrectposture - 没烟,先歇会! 20:53:12.176 [小女] c.testcorrectposture - 外卖送到没?[false] 20:53:12.176 [小女] c.testcorrectposture - 没外卖,先歇会! 20:53:13.174 [送外卖的] c.testcorrectposture - 外卖到了噢! 20:53:13.174 [小南] c.testcorrectposture - 有烟没?[false] 20:53:13.174 [小南] c.testcorrectposture - 没干成活... notify 只能随机唤醒一个 waitset 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线 程,称之为【虚假唤醒】 解决方法,改为 notifyall step 4\nnew thread(() -\u0026gt; { synchronized (room) { hastakeout = true; log.debug(\u0026#34;外卖到了噢!\u0026#34;); room.notifyall(); } }, \u0026#34;送外卖的\u0026#34;).start(); 输出\n20:55:23.978 [小南] c.testcorrectposture - 有烟没?[false] 20:55:23.982 [小南] c.testcorrectposture - 没烟,先歇会! 20:55:23.982 [小女] c.testcorrectposture - 外卖送到没?[false] 20:55:23.982 [小女] c.testcorrectposture - 没外卖,先歇会! 20:55:24.979 [送外卖的] c.testcorrectposture - 外卖到了噢! 20:55:24.979 [小女] c.testcorrectposture - 外卖送到没?[true] 20:55:24.980 [小女] c.testcorrectposture - 可以开始干活了 20:55:24.980 [小南] c.testcorrectposture - 有烟没?[false] 20:55:24.980 [小南] c.testcorrectposture - 没干成活... 用 notifyall 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了 解决方法,用 while + wait,当条件不成立,再次 wait step 5\n将 if 改为 while\nif (!hascigarette) { log.debug(\u0026#34;没烟,先歇会!\u0026#34;); try { room.wait(); } catch (interruptedexception e) { e.printstacktrace(); } } 改动后\nwhile (!hascigarette) { log.debug(\u0026#34;没烟,先歇会!\u0026#34;); try { room.wait(); } catch (interruptedexception e) { e.printstacktrace(); } } 输出\n20:58:34.322 [小南] c.testcorrectposture - 有烟没?[false] 20:58:34.326 [小南] c.testcorrectposture - 没烟,先歇会! 20:58:34.326 [小女] c.testcorrectposture - 外卖送到没?[false] 20:58:34.326 [小女] c.testcorrectposture - 没外卖,先歇会! 20:58:35.323 [送外卖的] c.testcorrectposture - 外卖到了噢! 20:58:35.324 [小女] c.testcorrectposture - 外卖送到没?[true] 20:58:35.324 [小女] c.testcorrectposture - 可以开始干活了 20:58:35.324 [小南] c.testcorrectposture - 没烟,先歇会! synchronized(lock) { while(条件不成立) { lock.wait(); } // 干活 } //另一个线程 synchronized(lock) { lock.notifyall(); } 保护性暂停模式 定义 即 guarded suspension,用在一个线程等待另一个线程的执行结果\n要点\n有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 guardedobject 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者) jdk 中,join 的实现、future 的实现,采用的就是此模式 因为要等待另一方的结果,因此归类到同步模式 实现 class guardedobject { private object response; private final object lock = new object(); public object get() { synchronized (lock) { // 条件不满足则等待 while (response == null) { try { lock.wait(); } catch (interruptedexception e) { e.printstacktrace(); } } return response; } } public void complete(object response) { synchronized (lock) { // 条件满足,通知等待线程 this.response = response; lock.notifyall(); } } } 测试\n一个线程等待另一个线程的执行结果\n@override public void run() { guardedobject guardedobject = mailboxes.getguardedobject(id); guardedobject.set(mail); log.debug(\u0026#34;送信成功,id={},内容:{}\u0026#34;,id,mail); } public static void main(string[] args) { guardedobject guardedobject = new guardedobject(); new thread(() -\u0026gt; { try { // 子线程执行下载 list\u0026lt;string\u0026gt; response = download(); log.debug(\u0026#34;download complete...\u0026#34;); guardedobject.complete(response); } catch (ioexception e) { e.printstacktrace(); } }).start(); log.debug(\u0026#34;waiting...\u0026#34;); // 主线程阻塞等待 object response = guardedobject.get(); log.debug(\u0026#34;get response: [{}] lines\u0026#34;, ((list\u0026lt;string\u0026gt;) response).size()); } 执行结果\n08:42:18.568 [main] c.testguardedobject - waiting... 08:42:23.312 [thread-0] c.testguardedobject - download complete... 08:42:23.312 [main] c.testguardedobject - get response: [3] lines 带超时版 (guardedobject) class guardedobjectv2 { private object response; private final object lock = new object(); public object get(long millis) { synchronized (lock) { // 1) 记录最初时间 long begin = system.currenttimemillis(); // 2) 已经经历的时间 long timepassed = 0; while (response == null) { // 4) 假设 millis 是 1000,结果在 400 时唤醒了,那么还有 600 要等 long waittime = millis - timepassed; log.debug(\u0026#34;waittime: {}\u0026#34;, waittime); if (waittime \u0026lt;= 0) { log.debug(\u0026#34;break...\u0026#34;); break; } try { lock.wait(waittime); //动态时间等待 } catch (interruptedexception e) { e.printstacktrace(); } // 3) 如果提前被唤醒,这时已经经历的时间假设为 400 timepassed = system.currenttimemillis() - begin; log.debug(\u0026#34;timepassed: {}, object is null {}\u0026#34;, timepassed, response == null); } return response; } } public void complete(object response) { synchronized (lock) { // 条件满足,通知等待线程 this.response = response; log.debug(\u0026#34;notify...\u0026#34;); lock.notifyall(); } } } 测试,没有超时\npublic static void main(string[] args) { guardedobjectv2 v2 = new guardedobjectv2(); new thread(() -\u0026gt; { sleep(1); v2.complete(null); sleep(1); v2.complete(arrays.aslist(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;)); }).start(); object response = v2.get(2500); if (response != null) { log.debug(\u0026#34;get response: [{}] lines\u0026#34;, ((list\u0026lt;string\u0026gt;) response).size()); } else { log.debug(\u0026#34;can\u0026#39;t get response\u0026#34;); } } 输出\n08:49:39.917 [main] c.guardedobjectv2 - waittime: 2500 08:49:40.917 [thread-0] c.guardedobjectv2 - notify... 08:49:40.917 [main] c.guardedobjectv2 - timepassed: 1003, object is null true 08:49:40.917 [main] c.guardedobjectv2 - waittime: 1497 08:49:41.918 [thread-0] c.guardedobjectv2 - notify... 08:49:41.918 [main] c.guardedobjectv2 - timepassed: 2004, object is null false 08:49:41.918 [main] c.testguardedobjectv2 - get response: [3] lines 测试超时\n// 等待时间不足 list\u0026lt;string\u0026gt; lines = v2.get(1500); 输出\n08:47:54.963 [main] c.guardedobjectv2 - waittime: 1500 08:47:55.963 [thread-0] c.guardedobjectv2 - notify... 08:47:55.963 [main] c.guardedobjectv2 - timepassed: 1002, object is null true 08:47:55.963 [main] c.guardedobjectv2 - waittime: 498 08:47:56.461 [main] c.guardedobjectv2 - timepassed: 1500, object is null true 08:47:56.461 [main] c.guardedobjectv2 - waittime: 0 08:47:56.461 [main] c.guardedobjectv2 - break... 08:47:56.461 [main] c.testguardedobjectv2 - can\u0026#39;t get response 08:47:56.963 [thread-0] c.guardedobjectv2 - notify... join原理 join 体现的是【保护性暂停】模式\n源码:\n//不带参 public final void join() throws interruptedexception { join(0); } //带参 //等待时长的实现类似于之前的保护性暂停 public final synchronized void join(long millis) throws interruptedexception { long base = system.currenttimemillis(); long now = 0; if (millis \u0026lt; 0) { throw new illegalargumentexception(\u0026#34;timeout value is negative\u0026#34;); } if (millis == 0) { // 一般等待 while (isalive()) { wait(0); } } else { while (isalive()) { // 超时等待 long delay = millis - now; if (delay \u0026lt;= 0) { break; } wait(delay); now = system.currenttimemillis() - base; } } } 生产者消费者模式 定义 要点\n与前面的保护性暂停中的 guardobject 不同,不需要产生结果和消费结果的线程一一对应 消费队列可以用来平衡生产和消费的线程资源 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据 jdk 中各种阻塞队列,采用的就是这种模式 实现 class message { private int id; private object message; public message(int id, object message) { this.id = id; this.message = message; } public int getid() { return id; } public object getmessage() { return message; } } class messagequeue { private linkedlist\u0026lt;message\u0026gt; queue; private int capacity; public messagequeue(int capacity) { this.capacity = capacity; queue = new linkedlist\u0026lt;\u0026gt;(); } public message take() { synchronized (queue) { while (queue.isempty()) { log.debug(\u0026#34;没货了, wait\u0026#34;); try { queue.wait(); //保护性暂停 } catch (interruptedexception e) { e.printstacktrace(); } } message message = queue.removefirst(); queue.notifyall(); return message; } } public void put(message message) { synchronized (queue) { while (queue.size() == capacity) { log.debug(\u0026#34;库存已达上限, wait\u0026#34;); try { queue.wait(); // 保护性暂停 } catch (interruptedexception e) { e.printstacktrace(); } } queue.addlast(message); queue.notifyall(); } } } park \u0026amp; unpark 基本使用\n它们是 locksupport 类中的方法\n// 暂停当前线程 locksupport.park(); // 恢复某个线程的运行 locksupport.unpark(暂停线程对象) 先 park 再 unpark\nthread t1 = new thread(() -\u0026gt; { log.debug(\u0026#34;start...\u0026#34;); sleep(1); log.debug(\u0026#34;park...\u0026#34;); locksupport.park(); log.debug(\u0026#34;resume...\u0026#34;); },\u0026#34;t1\u0026#34;); t1.start(); sleep(2); log.debug(\u0026#34;unpark...\u0026#34;); locksupport.unpark(t1); 输出\n18:42:52.585 c.testparkunpark [t1] - start... 18:42:53.589 c.testparkunpark [t1] - park... 18:42:54.583 c.testparkunpark [main] - unpark... 18:42:54.583 c.testparkunpark [t1] - resume... 先 unpark 再 park\nthread t1 = new thread(() -\u0026gt; { log.debug(\u0026#34;start...\u0026#34;); sleep(2); log.debug(\u0026#34;park...\u0026#34;); locksupport.park(); log.debug(\u0026#34;resume...\u0026#34;); }, \u0026#34;t1\u0026#34;); t1.start(); sleep(1); log.debug(\u0026#34;unpark...\u0026#34;); locksupport.unpark(t1); 输出\n18:43:50.765 c.testparkunpark [t1] - start... 18:43:51.764 c.testparkunpark [main] - unpark... 18:43:52.769 c.testparkunpark [t1] - park... 18:43:52.769 c.testparkunpark [t1] - resume... 特点\n与 object 的 wait \u0026amp; notify 相比\nwait,notify 和 notifyall 必须配合 object monitor 一起使用,而 park,unpark 不必 park \u0026amp; unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyall 是唤醒所有等待线程,就不那么【精确】 park \u0026amp; unpark 可以先 unpark,而 wait \u0026amp; notify 不能先 notify park\u0026amp;unpark原理\n每个线程都有自己的一个 parker 对象(由c++编写,java中不可见),由三部分组成 _counter , _cond 和 _mutex 打个比喻\n线程就像一个旅人,parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中 的备用干粮(0 为耗尽,1 为充足) 调用 park 就是要看需不需要停下来歇息 如果备用干粮耗尽,那么钻进帐篷歇息 如果备用干粮充足,那么不需停留,继续前进 调用 unpark,就好比令干粮充足 如果这时线程还在帐篷,就唤醒让他继续前进 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮 当前线程调用 unsafe.park() 方法 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁 线程进入 _cond 条件变量阻塞 设置 _counter = 0 调用 unsafe.unpark(thread_0) 方法,设置 _counter 为 1 唤醒 _cond 条件变量中的 thread_0 thread_0 恢复运行 设置 _counter 为 0 调用 unsafe.unpark(thread_0) 方法,设置 _counter 为 1 当前线程调用 unsafe.park() 方法 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行 设置 _counter 为 0 调用park()或unpark()第一步都会对_counter进行判断\n重新理解线程状态转换 假设有线程 thread t\n情况 1 new --\u0026gt; runnable\n当调用 t.start() 方法时,由 new \u0026ndash;\u0026gt; runnable 情况 2 runnable \u0026lt;--\u0026gt; waiting\nt 线程用 synchronized(obj) 获取了对象锁后\n调用 obj.wait() 方法时,t 线程从 runnable --\u0026gt; waiting 调用 obj.notify() , obj.notifyall() , t.interrupt() 时 竞争锁成功,t 线程从 waiting --\u0026gt; runnable 竞争锁失败,t 线程从 waiting --\u0026gt; blocked 情况 3 runnable \u0026lt;--\u0026gt; waiting\n当前线程调用 t.join() 方法时,当前线程从 runnable --\u0026gt; waiting 注意是当前线程在t 线程对象的监视器上等待 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 waiting --\u0026gt; runnable 情况 4 runnable \u0026lt;--\u0026gt; waiting\n当前线程调用 locksupport.park() 方法会让当前线程从 runnable --\u0026gt; waiting 调用 locksupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 waiting --\u0026gt; runnable 情况 5 runnable \u0026lt;--\u0026gt; timed_waiting\nt 线程用 synchronized(obj) 获取了对象锁后\n调用 obj.wait(long n) 方法时,t 线程从 runnable --\u0026gt; timed_waiting t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyall() ,t.interrupt()时 竞争锁成功,t 线程从 timed_waiting --\u0026gt; runnable 竞争锁失败,t 线程从 timed_waiting --\u0026gt; blocked 情况 6 runnable \u0026lt;--\u0026gt; timed_waiting\n当前线程调用 t.join(long n) 方法时,当前线程从 runnable --\u0026gt; timed_waiting 注意是当前线程在t 线程对象的监视器上等待 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 timed_waiting --\u0026gt; runnable 情况 7 runnable \u0026lt;--\u0026gt; timed_waiting\n当前线程调用 thread.sleep(long n) ,当前线程从 runnable --\u0026gt; timed_waiting 当前线程等待时间超过了 n 毫秒,当前线程从 timed_waiting --\u0026gt; runnable 情况 8 runnable \u0026lt;--\u0026gt; timed_waiting\n当前线程调用 locksupport.parknanos(long nanos) 或 locksupport.parkuntil(long millis) 时,当前线程从 runnable --\u0026gt; timed_waiting 调用 locksupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 timed_waiting--\u0026gt; runnable 情况 9 runnable \u0026lt;--\u0026gt; blocked\nt 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 runnable --\u0026gt; blocked 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 blocked 的线程重新竞争,如果其中 t 线程竞争 成功,从 blocked --\u0026gt; runnable ,其它失败的线程仍然 blocked 情况 10 runnable \u0026lt;--\u0026gt; terminated\n当前线程所有代码运行完毕,进入 terminated 多把锁 多把不相干的锁\n一间大屋子有两个功能:睡觉、学习,互不相干。\n现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低\n解决方法是准备多个房间(多个对象锁)\n例如\nclass bigroom { public void sleep() { synchronized (this) { log.debug(\u0026#34;sleeping 2 小时\u0026#34;); sleeper.sleep(2); } } public void study() { synchronized (this) { log.debug(\u0026#34;study 1 小时\u0026#34;); sleeper.sleep(1); } } } 执行\nbigroom bigroom = new bigroom(); new thread(() -\u0026gt; { bigroom.compute(); },\u0026#34;小南\u0026#34;).start(); new thread(() -\u0026gt; { bigroom.sleep(); },\u0026#34;小女\u0026#34;).start(); 结果\n12:13:54.471 [小南] c.bigroom - study 1 小时 12:13:55.476 [小女] c.bigroom - sleeping 2 小时 改进\nclass bigroom { private final object studyroom = new object(); private final object bedroom = new object(); public void sleep() { synchronized (bedroom) { log.debug(\u0026#34;sleeping 2 小时\u0026#34;); sleeper.sleep(2); } } public void study() { synchronized (studyroom) { log.debug(\u0026#34;study 1 小时\u0026#34;); sleeper.sleep(1); } } } 某次执行结果\n12:15:35.069 [小南] c.bigroom - study 1 小时 12:15:35.069 [小女] c.bigroom - sleeping 2 小时 将锁的粒度细分\n好处,是可以增强并发度 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁 前提:两把锁锁住的两段代码互不相关 活跃性 死锁 有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁\nt1 线程 获得 a对象 锁,接下来想获取 b对象 的锁 t2 线程 获得 b对象 锁,接下来想获取 a对象 的锁 例:\nobject a = new object(); object b = new object(); thread t1 = new thread(() -\u0026gt; { synchronized (a) { log.debug(\u0026#34;lock a\u0026#34;); sleep(1); synchronized (b) { log.debug(\u0026#34;lock b\u0026#34;); log.debug(\u0026#34;操作...\u0026#34;); } } }, \u0026#34;t1\u0026#34;); thread t2 = new thread(() -\u0026gt; { synchronized (b) { log.debug(\u0026#34;lock b\u0026#34;); sleep(0.5); synchronized (a) { log.debug(\u0026#34;lock a\u0026#34;); log.debug(\u0026#34;操作...\u0026#34;); } } }, \u0026#34;t2\u0026#34;); t1.start(); t2.start(); 结果\n12:22:06.962 [t2] c.testdeadlock - lock b 12:22:06.962 [t1] c.testdeadlock - lock a 解决方式:\nreentrantlock 定位死锁 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:\ncmd \u0026gt; jps picked up java_tool_options: -dfile.encoding=utf-8 12320 jps 22816 kotlincompiledaemon 33200 testdeadlock // jvm 进程 11508 main 28468 launcher cmd \u0026gt; jstack 33200 picked up java_tool_options: -dfile.encoding=utf-8 2018-12-29 05:51:40 full thread dump java hotspot(tm) 64-bit server vm (25.91-b14 mixed mode): \u0026#34;destroyjavavm\u0026#34; #13 prio=5 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition [0x0000000000000000] java.lang.thread.state: runnable \u0026#34;thread-1\u0026#34; #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x000000001f54f000] java.lang.thread.state: blocked (on object monitor) at thread.testdeadlock.lambda$main$1(testdeadlock.java:28) - waiting to lock \u0026lt;0x000000076b5bf1c0\u0026gt; (a java.lang.object) - locked \u0026lt;0x000000076b5bf1d0\u0026gt; (a java.lang.object) at thread.testdeadlock$$lambda$2/883049899.run(unknown source) at java.lang.thread.run(thread.java:745) \u0026#34;thread-0\u0026#34; #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry [0x000000001f44f000] java.lang.thread.state: blocked (on object monitor) at thread.testdeadlock.lambda$main$0(testdeadlock.java:15) - waiting to lock \u0026lt;0x000000076b5bf1d0\u0026gt; (a java.lang.object) - locked \u0026lt;0x000000076b5bf1c0\u0026gt; (a java.lang.object) at thread.testdeadlock$$lambda$1/495053715.run(unknown source) at java.lang.thread.run(thread.java:745) // 略去部分输出 found one java-level deadlock: ============================= \u0026#34;thread-1\u0026#34;: waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.object), which is held by \u0026#34;thread-0\u0026#34; \u0026#34;thread-0\u0026#34;: waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.object), which is held by \u0026#34;thread-1\u0026#34; java stack information for the threads listed above: =================================================== \u0026#34;thread-1\u0026#34;: at thread.testdeadlock.lambda$main$1(testdeadlock.java:28) - waiting to lock \u0026lt;0x000000076b5bf1c0\u0026gt; (a java.lang.object) - locked \u0026lt;0x000000076b5bf1d0\u0026gt; (a java.lang.object) at thread.testdeadlock$$lambda$2/883049899.run(unknown source) at java.lang.thread.run(thread.java:745) \u0026#34;thread-0\u0026#34;: at thread.testdeadlock.lambda$main$0(testdeadlock.java:15) - waiting to lock \u0026lt;0x000000076b5bf1d0\u0026gt; (a java.lang.object) - locked \u0026lt;0x000000076b5bf1c0\u0026gt; (a java.lang.object) at thread.testdeadlock$$lambda$1/495053715.run(unknown source) at java.lang.thread.run(thread.java:745) found 1 deadlock. 避免死锁要注意加锁顺序 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 cpu 占用高的 java 进程,再利用 top -hp 进程id 来定位是哪个线程,最后再用 jstack 排查 活锁 活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如\npublic class testlivelock { static volatile int count = 10; static final object lock = new object(); public static void main(string[] args) { new thread(() -\u0026gt; { // 期望减到 0 退出循环 while (count \u0026gt; 0) { sleep(0.2); count--; log.debug(\u0026#34;count: {}\u0026#34;, count); } }, \u0026#34;t1\u0026#34;).start(); new thread(() -\u0026gt; { // 期望超过 20 退出循环 while (count \u0026lt; 20) { sleep(0.2); count++; log.debug(\u0026#34;count: {}\u0026#34;, count); } }, \u0026#34;t2\u0026#34;).start(); } } 解决方式:\n错开线程的运行时间,使得一方不能改变另一方的结束条件。 将睡眠时间调整为随机数。 饥饿 很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 cpu 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题\n下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题\n顺序加锁的解决方案\n说明:\n顺序加锁可以解决死锁问题,但也会导致一些线程一直得不到锁,产生饥饿现象。 解决方式:reentrantlock reentrantlock 相对于 synchronized 它具备如下特点\n可中断 可以设置超时时间 可以设置为公平锁 支持多个条件变量 与 synchronized 一样,都支持可重入\n基本语法\n// 获取锁 reentrantlock.lock(); try { // 临界区 } finally { // 释放锁 reentrantlock.unlock(); } 可重入 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。\nstatic reentrantlock lock = new reentrantlock(); public static void main(string[] args) { method1(); } public static void method1() { lock.lock(); try { log.debug(\u0026#34;execute method1\u0026#34;); method2(); } finally { lock.unlock(); } } public static void method2() { lock.lock(); try { log.debug(\u0026#34;execute method2\u0026#34;); method3(); } finally { lock.unlock(); } } public static void method3() { lock.lock(); try { log.debug(\u0026#34;execute method3\u0026#34;); } finally { lock.unlock(); } } 输出\n17:59:11.862 [main] c.testreentrant - execute method1 17:59:11.865 [main] c.testreentrant - execute method2 17:59:11.865 [main] c.testreentrant - execute method3 可打断 可打断指的是处于阻塞状态等待锁的线程可以被打断等待。注意lock.lockinterruptibly()和lock.trylock()方法是可打断的,lock.lock()不是。可打断的意义在于避免得不到锁的线程无限制地等待下去,防止死锁的一种方式。\n示例\nreentrantlock lock = new reentrantlock(); thread t1 = new thread(() -\u0026gt; { log.debug(\u0026#34;启动...\u0026#34;); try { lock.lockinterruptibly(); } catch (interruptedexception e) { e.printstacktrace(); log.debug(\u0026#34;等锁的过程中被打断\u0026#34;); return; } try { log.debug(\u0026#34;获得了锁\u0026#34;); } finally { lock.unlock(); } }, \u0026#34;t1\u0026#34;); lock.lock(); log.debug(\u0026#34;获得了锁\u0026#34;); t1.start(); try { sleep(1); t1.interrupt(); log.debug(\u0026#34;执行打断\u0026#34;); } finally { lock.unlock(); } 输出\n18:02:40.520 [main] c.testinterrupt - 获得了锁 18:02:40.524 [t1] c.testinterrupt - 启动... 18:02:41.530 [main] c.testinterrupt - 执行打断 java.lang.interruptedexception at java.util.concurrent.locks.abstractqueuedsynchronizer.doacquireinterruptibly(abstractqueuedsynchr onizer.java:898) at java.util.concurrent.locks.abstractqueuedsynchronizer.acquireinterruptibly(abstractqueuedsynchron izer.java:1222) at java.util.concurrent.locks.reentrantlock.lockinterruptibly(reentrantlock.java:335) at cn.itcast.n4.reentrant.testinterrupt.lambda$main$0(testinterrupt.java:17) at java.lang.thread.run(thread.java:748) 18:02:41.532 [t1] c.testinterrupt - 等锁的过程中被打断 注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断\nreentrantlock lock = new reentrantlock(); thread t1 = new thread(() -\u0026gt; { log.debug(\u0026#34;启动...\u0026#34;); lock.lock(); try { log.debug(\u0026#34;获得了锁\u0026#34;); } finally { lock.unlock(); } }, \u0026#34;t1\u0026#34;); lock.lock(); log.debug(\u0026#34;获得了锁\u0026#34;); t1.start(); try { sleep(1); t1.interrupt(); log.debug(\u0026#34;执行打断\u0026#34;); sleep(1); } finally { log.debug(\u0026#34;释放了锁\u0026#34;); lock.unlock(); } 输出\n18:06:56.261 [main] c.testinterrupt - 获得了锁 18:06:56.265 [t1] c.testinterrupt - 启动... 18:06:57.266 [main] c.testinterrupt - 执行打断 // 这时 t1 并没有被真正打断, 而是仍继续等待锁 18:06:58.267 [main] c.testinterrupt - 释放了锁 18:06:58.267 [t1] c.testinterrupt - 获得了锁 锁超时 立刻失败\nreentrantlock lock = new reentrantlock(); thread t1 = new thread(() -\u0026gt; { log.debug(\u0026#34;启动...\u0026#34;); if (!lock.trylock()) { log.debug(\u0026#34;获取立刻失败,返回\u0026#34;); return; } try { log.debug(\u0026#34;获得了锁\u0026#34;); } finally { lock.unlock(); } }, \u0026#34;t1\u0026#34;); lock.lock(); log.debug(\u0026#34;获得了锁\u0026#34;); t1.start(); try { sleep(2); } finally { lock.unlock(); } 输出\n18:15:02.918 [main] c.testtimeout - 获得了锁 18:15:02.921 [t1] c.testtimeout - 启动... 18:15:02.921 [t1] c.testtimeout - 获取立刻失败,返回 超时失败\nreentrantlock lock = new reentrantlock(); thread t1 = new thread(() -\u0026gt; { log.debug(\u0026#34;启动...\u0026#34;); try { if (!lock.trylock(1, timeunit.seconds)) { log.debug(\u0026#34;获取等待 1s 后失败,返回\u0026#34;); return; } } catch (interruptedexception e) { e.printstacktrace(); } try { log.debug(\u0026#34;获得了锁\u0026#34;); } finally { lock.unlock(); } }, \u0026#34;t1\u0026#34;); lock.lock(); log.debug(\u0026#34;获得了锁\u0026#34;); t1.start(); try { sleep(2); } finally { lock.unlock(); } 输出\n18:19:40.537 [main] c.testtimeout - 获得了锁 18:19:40.544 [t1] c.testtimeout - 启动... 18:19:41.547 [t1] c.testtimeout - 获取等待 1s 后失败,返回 条件变量 synchronized 中也有条件变量,就是我们讲原理时那个 waitset 休息室,当条件不满足时进入 waitset 等待\nreentrantlock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比\nsynchronized 是那些不满足条件的线程都在一间休息室等消息 而 reentrantlock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒 使用要点 await 前需要获得锁 await 执行后,会释放锁,进入 conditionobject 等待 await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁 竞争 lock 锁成功后,从 await 后继续执行 详细api public interface condition { void await() throws interruptedexception; void awaituninterruptibly(); // ... boolean awaituntil(date deadline) throws interruptedexception; /** * wakes up one waiting thread. */ void signal(); /** * wakes up all waiting threads. */ void signalall(); } 例子 static reentrantlock lock = new reentrantlock(); static condition waitcigarettequeue = lock.newcondition(); static condition waitbreakfastqueue = lock.newcondition(); static volatile boolean hascigrette = false; static volatile boolean hasbreakfast = false; public static void main(string[] args) { new thread(() -\u0026gt; { try { lock.lock(); while (!hascigrette) { try { waitcigarettequeue.await(); } catch (interruptedexception e) { e.printstacktrace(); } } log.debug(\u0026#34;等到了它的烟\u0026#34;); } finally { lock.unlock(); } }).start(); new thread(() -\u0026gt; { try { lock.lock(); while (!hasbreakfast) { try { waitbreakfastqueue.await(); } catch (interruptedexception e) { e.printstacktrace(); } } log.debug(\u0026#34;等到了它的早餐\u0026#34;); } finally { lock.unlock(); } }).start(); sleep(1); sendbreakfast(); sleep(1); sendcigarette(); } private static void sendcigarette() { lock.lock(); try { log.debug(\u0026#34;送烟来了\u0026#34;); hascigrette = true; waitcigarettequeue.signal(); } finally { lock.unlock(); } } private static void sendbreakfast() { lock.lock(); try { log.debug(\u0026#34;送早餐来了\u0026#34;); hasbreakfast = true; waitbreakfastqueue.signal(); } finally { lock.unlock(); } } 输出\n18:52:27.680 [main] c.testcondition - 送早餐来了 18:52:27.682 [thread-1] c.testcondition - 等到了它的早餐 18:52:28.683 [main] c.testcondition - 送烟来了 18:52:28.683 [thread-0] c.testcondition - 等到了它的烟 顺序控制模式 固定运行顺序 比如,必须先 2 后 1 打印\nwait notify 版\n// 用来同步的对象 static object obj = new object(); // t2 运行标记, 代表 t2 是否执行过 static boolean t2runed = false; public static void main(string[] args) { thread t1 = new thread(() -\u0026gt; { synchronized (obj) { // 如果 t2 没有执行过 while (!t2runed) { try { // t1 先等一会 obj.wait(); } catch (interruptedexception e) { e.printstacktrace(); } } } system.out.println(1); }); thread t2 = new thread(() -\u0026gt; { system.out.println(2); synchronized (obj) { // 修改运行标记 t2runed = true; // 通知 obj 上等待的线程(可能有多个,因此需要用 notifyall) obj.notifyall(); } }); t1.start(); t2.start(); } park unpark 版\n可以看到,实现上wait\u0026amp;notify很麻烦:\n首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该 wait 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决 此问题 最后,唤醒对象上的 wait 线程需要使用 notifyall,因为『同步对象』上的等待线程可能不止一个 可以使用 locksupport 类的 park 和 unpark 来简化上面的题目:\nthread t1 = new thread(() -\u0026gt; { try { thread.sleep(1000); } catch (interruptedexception e) { } // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行 locksupport.park(); system.out.println(\u0026#34;1\u0026#34;); }); thread t2 = new thread(() -\u0026gt; { system.out.println(\u0026#34;2\u0026#34;); // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』) locksupport.unpark(t1); }); t1.start(); t2.start(); park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』, 不需要『同步对象』和『运行标记』\n交替输出 线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现\nwait notify 版\nclass syncwaitnotify { private int flag; private int loopnumber; public syncwaitnotify(int flag, int loopnumber) { this.flag = flag; this.loopnumber = loopnumber; } public void print(int waitflag, int nextflag, string str) { for (int i = 0; i \u0026lt; loopnumber; i++) { synchronized (this) { while (this.flag != waitflag) { try { this.wait(); } catch (interruptedexception e) { e.printstacktrace(); } } system.out.print(str); flag = nextflag; this.notifyall(); } } } } syncwaitnotify syncwaitnotify = new syncwaitnotify(1, 5); new thread(() -\u0026gt; { syncwaitnotify.print(1, 2, \u0026#34;a\u0026#34;); }).start(); new thread(() -\u0026gt; { syncwaitnotify.print(2, 3, \u0026#34;b\u0026#34;); }).start(); new thread(() -\u0026gt; { syncwaitnotify.print(3, 1, \u0026#34;c\u0026#34;); }).start(); lock 条件变量版\nclass awaitsignal extends reentrantlock { public void start(condition first) { this.lock(); try { log.debug(\u0026#34;start\u0026#34;); first.signal(); } finally { this.unlock(); } } public void print(string str, condition current, condition next) { for (int i = 0; i \u0026lt; loopnumber; i++) { this.lock(); try { current.await(); log.debug(str); next.signal(); } catch (interruptedexception e) { e.printstacktrace(); } finally { this.unlock(); } } } // 循环次数 private int loopnumber; public awaitsignal(int loopnumber) { this.loopnumber = loopnumber; } } awaitsignal as = new awaitsignal(5); condition awaitset = as.newcondition(); condition bwaitset = as.newcondition(); condition cwaitset = as.newcondition(); new thread(() -\u0026gt; { as.print(\u0026#34;a\u0026#34;, awaitset, bwaitset); }).start(); new thread(() -\u0026gt; { as.print(\u0026#34;b\u0026#34;, bwaitset, cwaitset); }).start(); new thread(() -\u0026gt; { as.print(\u0026#34;c\u0026#34;, cwaitset, awaitset); }).start(); as.start(awaitset); park unpark 版\npublic class test9 { static thread t1; static thread t2; static thread t3; public static void main(string[] args) { parkunpark up = new parkunpark(4); t1 = new thread(() -\u0026gt; { up.print(\u0026#34;a\u0026#34;,t2); }, \u0026#34;t1\u0026#34;); t2 = new thread(() -\u0026gt; { up.print(\u0026#34;b\u0026#34;,t3); }, \u0026#34;t2\u0026#34;); t3 = new thread(() -\u0026gt; { up.print(\u0026#34;c\u0026#34;,t1); }, \u0026#34;t3\u0026#34;); t1.start(); t2.start(); t3.start(); locksupport.unpark(t1); } } class parkunpark{ private int loopnum; public parkunpark(int loopnum) { this.loopnum = loopnum; } public void print(string str,thread next){ for (int i = 0; i \u0026lt; loopnum; i++) { locksupport.park(); system.out.print(str); locksupport.unpark(next); } } } 本章小结 本章我们需要重点掌握的是\n分析多线程访问共享资源时,哪些代码片段属于临界区 使用 synchronized 互斥解决临界区的线程安全问题 掌握 synchronized 锁对象语法 掌握 synchronzied 加载成员方法和静态方法语法 掌握 wait/notify 同步方法 使用 lock 互斥解决临界区的线程安全问题 掌握 lock 的使用细节:可打断、锁超时、条件变量 学会分析变量的线程安全性、掌握常见线程安全类的使用 线程安全类的方法是原子性的,但方法之间的组合要具体分析。 了解线程活跃性问题:死锁、活锁、饥饿。 解决死锁、饥饿的方式:reentranlock 应用方面 互斥:使用 synchronized 或 lock 达到共享资源互斥效果 同步:使用 wait/notify 或 lock 的条件变量来达到线程间通信效果 原理方面 monitor、synchronized 、wait/notify 原理 synchronized 进阶原理 park \u0026amp; unpark 原理 模式方面 同步模式之保护性暂停 异步模式之生产者消费者 同步模式之顺序控制 共享模型之内存 java 内存模型 jmm 即 java memory model,它定义了主存、工作内存抽象概念,底层对应着 cpu 寄存器、缓存、硬件内存、 cpu 指令优化等。\njmm的意义\n计算机硬件底层的内存结构过于复杂,jmm的意义在于避免程序员直接管理计算机底层内存,用一些关键字synchronized、volatile等可以方便的管理内存。 jmm 体现在以下几个方面\n原子性 - 保证指令不会受到线程上下文切换的影响 可见性 - 保证指令不会受 cpu 缓存的影响 有序性 - 保证指令不会受 cpu 指令并行优化的影响 可见性 退不出的循环 先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:\nstatic boolean run = true; public static void main(string[] args) throws interruptedexception { thread t = new thread(()-\u0026gt;{ while(run){ // .... } }); t.start(); sleep(1); run = false; // 线程t不会如预想的停下来 } 为什么呢?分析一下:\n初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。 因为 t 线程要频繁从主内存中读取 run 的值,jit 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, 减少对主存中 run 的访问,提高效率 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 的值,结果永远是旧值 解决方法(volatile) volatile(易变关键字)\n它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存\n可见性 vs 原子性 前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可 见, 不能保证原子性,仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的:\ngetstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true putstatic run // 线程 main 修改 run 为 false, 仅此一次 getstatic run // 线程 t 获取 run false 比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i\u0026ndash; ,只能保证看到最新值,不能解决指令交错\n// 假设i的初始值为0 getstatic i // 线程2-获取静态变量i的值 线程内i=0 getstatic i // 线程1-获取静态变量i的值 线程内i=0 iconst_1 // 线程1-准备常量1 iadd // 线程1-自增 线程内i=1 putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1 iconst_1 // 线程2-准备常量1 isub // 线程2-自减 线程内i=-1 putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1 注意\nsynchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低 。\njmm关于synchronized的两条规定:\n1)线程解锁前,必须把共享变量的最新值刷新到主内存中\n2)线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值\n(注意:加锁与解锁需要是同一把锁)\n通过以上两点,可以看到synchronized能够实现可见性。同时,由于synchronized具有同步锁,所以它也具有原子性\n如果在前面示例的死循环中加入 system.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到 对 run 变量的修改了,想一想为什么?(println方法中有synchronized代码块保证了可见性)\nsynchronized关键字不能阻止指令重排,但在一定程度上能保证有序性(如果共享变量没有逃逸出同步代码块的话)。因为在单线程的情况下指令重排不影响结果,相当于保障了有序性。\n两阶段终止模式 two phase termination\n在一个线程 t1 中如何“优雅”终止线程 t2?这里的【优雅】指的是给 t2 一个料理后事的机会。\n错误思路\n使用线程对象的 stop() 方法停止线程 stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁, 其它线程将永远无法获取锁 使用 system.exit(int) 方法停止线程 目的仅是停止一个线程,但这种做法会让整个程序都停止 利用volatile修饰的停止标记\n// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性 // 我们的例子中,即主线程把它修改为 true 对 t1 线程可见 class tptvolatile { private thread thread; private volatile boolean stop = false; public void start(){ thread = new thread(() -\u0026gt; { while(true) { thread current = thread.currentthread(); if(stop) { log.debug(\u0026#34;料理后事\u0026#34;); break; } try { thread.sleep(1000); log.debug(\u0026#34;将结果保存\u0026#34;); } catch (interruptedexception e) { } // 执行监控操作 } },\u0026#34;监控线程\u0026#34;); thread.start(); } public void stop() { stop = true; //让线程立即停止而不是等待sleep结束 thread.interrupt(); } } 调用\ntptvolatile t = new tptvolatile(); t.start(); thread.sleep(3500); log.debug(\u0026#34;stop\u0026#34;); t.stop(); 结果\n11:54:52.003 c.tptvolatile [监控线程] - 将结果保存 11:54:53.006 c.tptvolatile [监控线程] - 将结果保存 11:54:54.007 c.tptvolatile [监控线程] - 将结果保存 11:54:54.502 c.testtwophasetermination [main] - stop 11:54:54.502 c.tptvolatile [监控线程] - 料理后事 balking模式 定义 balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做 了,直接结束返回。\n实现 例如:\npublic class monitorservice { // 用来表示是否已经有线程已经在执行启动了 private volatile boolean starting; public void start() { log.info(\u0026#34;尝试启动监控线程...\u0026#34;); synchronized (this) { if (starting) { return; } starting = true; } //其实synchronized外面还可以再套一层if,或者改为if(!starting),if框后直接return // 真正启动监控线程... } } 当前端页面多次点击按钮调用 start 时\n输出\n[http-nio-8080-exec-1] cn.itcast.monitor.service.monitorservice - 该监控线程已启动?(false) [http-nio-8080-exec-1] cn.itcast.monitor.service.monitorservice - 监控线程已启动... [http-nio-8080-exec-2] cn.itcast.monitor.service.monitorservice - 该监控线程已启动?(true) [http-nio-8080-exec-3] cn.itcast.monitor.service.monitorservice - 该监控线程已启动?(true) [http-nio-8080-exec-4] cn.itcast.monitor.service.monitorservice - 该监控线程已启动?(true) 它还经常用来实现线程安全的单例\npublic final class singleton { private singleton() { } private static singleton instance = null; public static synchronized singleton getinstance() { if (instance != null) { return instance; } instance = new singleton(); return instance; } } 对比一下保护性暂停模式:保护性暂停模式用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待。\n有序性 jvm 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码\nstatic int i; static int j; // 在某个线程内执行如下赋值操作 i = ...; j = ...; 可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是\ni = ...; j = ...; 也可以是\nj = ...; i = ...; 这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 cpu 执行指令的原理来理解一下吧\n指令级并行原理 鱼罐头的故事\n加工一条鱼需要 50 分钟,只能一条鱼、一条鱼顺序加工\u0026hellip;\n可以将每个鱼罐头的加工流程细分为 5 个步骤:\n去鳞清洗 10分钟 蒸煮沥水 10分钟 加注汤料 10分钟 杀菌出锅 10分钟 真空封罐 10分钟 即使只有一个工人,最理想的情况是:他能够在 10 分钟内同时做好这 5 件事,因为对第一条鱼的真空装罐,不会 影响对第二条鱼的杀菌出锅\u0026hellip;\n指令重排序优化\n事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 cpu 指令。为什么这么做呢?可以想到指令 还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段\n在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在 80\u0026rsquo;s 中 叶到 90\u0026rsquo;s 中叶占据了计算架构的重要地位。\n提示:\n分阶段,分工是提升效率的关键!\n指令重排的前提是,重排指令不能影响结果,例如\n// 可以重排的例子 int a = 10; // 指令1 int b = 20; // 指令2 system.out.println( a + b ); // 不能重排的例子 int a = 10; // 指令1 int b = a - 5; // 指令2 诡异的结果 int num = 0; boolean ready = false; // 线程1 执行此方法 public void actor1(i_result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } // 线程2 执行此方法 public void actor2(i_result r) { num = 2; ready = true; } i_result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?\n有同学这么分析\n情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1\n情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1\n情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)\n但我告诉你,结果还有可能是 0 😁😁😁,信不信吧!\n这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2\n相信很多人已经晕了 😵😵😵\n输出我们感兴趣的结果,摘录其中一次结果:\n*** interesting tests some interesting behaviors observed. this is for the plain curiosity. 2 matching test results. [ok] test.concurrencytest (jvm args: [-xx:-tieredcompilation]) observed state occurrences expectation interpretation 0 1,729 acceptable_interesting !!!! 1 42,617,915 acceptable ok 4 5,146,627 acceptable ok [ok] test.concurrencytest (jvm args: []) observed state occurrences expectation interpretation 0 1,652 acceptable_interesting !!!! 1 46,460,657 acceptable ok 4 4,571,072 acceptable ok 可以看到,出现结果为 0 的情况有 638 次,虽然次数相对很少,但毕竟是出现了。\n解决方法 volatile 修饰的变量,可以禁用指令重排\n@jcstresstest @outcome(id = {\u0026#34;1\u0026#34;, \u0026#34;4\u0026#34;}, expect = expect.acceptable, desc = \u0026#34;ok\u0026#34;) @outcome(id = \u0026#34;0\u0026#34;, expect = expect.acceptable_interesting, desc = \u0026#34;!!!!\u0026#34;) @state public class concurrencytest { int num = 0; volatile boolean ready = false; @actor public void actor1(i_result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } @actor public void actor2(i_result r) { num = 2; ready = true; } } 结果为:\n*** interesting tests some interesting behaviors observed. this is for the plain curiosity. 0 matching test results. volatile原理 volatile 的底层实现原理是内存屏障,memory barrier(memory fence)\n对 volatile 变量的写指令后会加入写屏障 对 volatile 变量的读指令前会加入读屏障 保证可见性\n写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中\npublic void actor2(i_result r) { num = 2; ready = true; // ready 是 volatile 赋值带写屏障 // 写屏障 } 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据\npublic void actor1(i_result r) { // 读屏障 // ready 是 volatile 读取值带读屏障 if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } 保证有序性\n写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后\npublic void actor2(i_result r) { num = 2; ready = true; // ready 是 volatile 赋值带写屏障 // 写屏障 } 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前\npublic void actor1(i_result r) { // 读屏障 // ready 是 volatile 读取值带读屏障 if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } 还是那句话,不能解决指令交错:\n写屏障仅仅是保证读能够读到最新的结果,但不能保证读跑到它前面去 而有序性的保证也只是保证了本线程内相关代码不被重排序 double-checked locking 问题\n以著名的 double-checked locking 单例模式为例\npublic final class singleton { private singleton() { } private static singleton instance = null; public static singleton getinstance() { if(instance == null) { // t2 // 首次访问会同步,而之后的使用没有 synchronized synchronized(singleton.class) { if (instance == null) { // t1 instance = new singleton(); } } } return instance; } } 以上的实现特点是:\n懒惰实例化 首次使用 getinstance() 才使用 synchronized 加锁,后续使用时无需加锁 有隐含的,但很关键的一点:第一个 if 使用了 instance 变量,是在同步块之外 但在多线程环境下,上面的代码是有问题的,getinstance 方法对应的字节码为:\n0: getstatic #2 // field instance:lcn/itcast/n5/singleton; 3: ifnonnull 37 6: ldc #3 // class cn/itcast/n5/singleton 8: dup 9: astore_0 10: monitorenter 11: getstatic #2 // field instance:lcn/itcast/n5/singleton; 14: ifnonnull 27 17: new #3 // class cn/itcast/n5/singleton 20: dup 21: invokespecial #4 // method \u0026#34;\u0026lt;init\u0026gt;\u0026#34;:()v 24: putstatic #2 // field instance:lcn/itcast/n5/singleton; 27: aload_0 28: monitorexit 29: goto 37 32: astore_1 33: aload_0 34: monitorexit 35: aload_1 36: athrow 37: getstatic #2 // field instance:lcn/itcast/n5/singleton; 40: areturn 其中\n17 表示创建对象,将对象引用入栈 // new singleton 20 表示复制一份对象引用 // 引用地址 21 表示利用一个对象引用,调用构造方法 24 表示利用一个对象引用,赋值给 static instance 也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:\n关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取 instance 变量的值\n这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例\n对 instance 使用 volatile 修饰即可,可以禁用指令重排。\ndouble-checked locking 解决\npublic final class singleton { private singleton() { } private static volatile singleton instance = null; public static singleton getinstance() { // 实例没创建,才会进入内部的 synchronized代码块 if (instance == null) { synchronized (singleton.class) { // t2 // 也许有其它线程已经创建实例,所以再判断一次 if (instance == null) { // t1 instance = new singleton(); } } } return instance; } } 字节码上看不出来 volatile 指令的效果\n// -------------------------------------\u0026gt; 加入对 instance 变量的读屏障 0: getstatic #2 // field instance:lcn/itcast/n5/singleton; 3: ifnonnull 37 6: ldc #3 // class cn/itcast/n5/singleton 8: dup 9: astore_0 10: monitorenter -----------------------\u0026gt; 保证原子性、可见性 11: getstatic #2 // field instance:lcn/itcast/n5/singleton; 14: ifnonnull 27 17: new #3 // class cn/itcast/n5/singleton 20: dup 21: invokespecial #4 // method \u0026#34;\u0026lt;init\u0026gt;\u0026#34;:()v 24: putstatic #2 // field instance:lcn/itcast/n5/singleton; // -------------------------------------\u0026gt; 加入对 instance 变量的写屏障 27: aload_0 28: monitorexit ------------------------\u0026gt; 保证原子性、可见性 29: goto 37 32: astore_1 33: aload_0 34: monitorexit 35: aload_1 36: athrow 37: getstatic #2 // field instance:lcn/itcast/n5/singleton; 40: areturn 如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(memory barrier(memory fence)),保证下面 两点:\n可见性 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据 有序性 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前 更底层是读写变量时使用 lock 指令来多核 cpu 之间的可见性与有序性 happens-before happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛 开以下 happens-before 规则,jmm 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见\n线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见(synchronized关键字的可见性、监视器规则)\nstatic int x; static object m = new object(); new thread(()-\u0026gt;{ synchronized(m) { x = 10; } },\u0026#34;t1\u0026#34;).start(); new thread(()-\u0026gt;{ synchronized(m) { system.out.println(x); } },\u0026#34;t2\u0026#34;).start(); 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见(volatile关键字的可见性、volatile规则)\nvolatile static int x; new thread(()-\u0026gt;{ x = 10; },\u0026#34;t1\u0026#34;).start(); new thread(()-\u0026gt;{ system.out.println(x); },\u0026#34;t2\u0026#34;).start(); 线程 start 前对变量的写,对该线程开始后对该变量的读可见(程序顺序规则+线程启动规则)\nstatic int x; x = 10; new thread(()-\u0026gt;{ system.out.println(x); },\u0026#34;t2\u0026#34;).start(); 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isalive() 或 t1.join()等待 它结束)(线程终止规则)\nstatic int x; thread t1 = new thread(()-\u0026gt;{ x = 10; },\u0026#34;t1\u0026#34;); t1.start(); t1.join(); system.out.println(x); 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isinterrupted)(线程中断机制)\nstatic int x; public static void main(string[] args) { thread t2 = new thread(()-\u0026gt;{ while(true) { if(thread.currentthread().isinterrupted()) { system.out.println(x); break; } } },\u0026#34;t2\u0026#34;); t2.start(); new thread(()-\u0026gt;{ sleep(1); x = 10; t2.interrupt(); },\u0026#34;t1\u0026#34;).start(); while(!t2.isinterrupted()) { thread.yield(); } system.out.println(x); } 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见\n具有传递性,如果 x hb-\u0026gt; y 并且 y hb-\u0026gt; z 那么有 x hb-\u0026gt; z ,配合 volatile 的防指令重排,有下面的例子\nvolatile static int x; static int y; new thread(()-\u0026gt;{ y = 10; x = 20; },\u0026#34;t1\u0026#34;).start(); new thread(()-\u0026gt;{ // x=20 对 t2 可见, 同时 y=10 也对 t2 可见 system.out.println(x); },\u0026#34;t2\u0026#34;).start(); 变量都是指成员变量或静态成员变量\nhappens-before 翻译成:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。\nhappens-before规则如下:\n程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。 监视器规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。 volatile规则:对一个volatile变量的写,happens-before于任意后续对一个volatile变量的读。 传递性:若果a happens-before b,b happens-before c,那么a happens-before c。 线程启动规则:thread对象的start()方法,happens-before于这个线程的任意后续操作。 线程终止规则:线程中的任意操作,happens-before于该线程的终止监测。我们可以通过thread.join()方法结束、thread.isalive()的返回值等手段检测到线程已经终止执行。 线程中断操作:对线程interrupt()方法的调用,happens-before于被中断线程的代码检测到中断事件的发生,可以通过thread.interrupted()方法检测到线程是否有中断发生。 对象终结规则:一个对象的初始化完成,happens-before于这个对象的finalize()方法的开始。 本章小结 本章重点讲解了 jmm 中的\n可见性 - 由 jvm 缓存优化引起 有序性 - 由 jvm 指令重排序优化引起 happens-before 规则 原理方面 cpu 指令并行 volatile 模式方面 两阶段终止模式的 volatile 改进 同步模式之 balking 共享模型之无锁 问题提出 (应用之互斥) 有如下需求,保证 account.withdraw 取款方法的线程安全\npackage cn.itcast; import java.util.arraylist; import java.util.list; interface account { // 获取余额 integer getbalance(); // 取款 void withdraw(integer amount); /** * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作 * 如果初始余额为 10000 那么正确的结果应当是 0 */ static void demo(account account) { list\u0026lt;thread\u0026gt; ts = new arraylist\u0026lt;\u0026gt;(); long start = system.nanotime(); for (int i = 0; i \u0026lt; 1000; i++) { ts.add(new thread(() -\u0026gt; { account.withdraw(10); })); } ts.foreach(thread::start); ts.foreach(t -\u0026gt; { try { t.join(); } catch (interruptedexception e) { e.printstacktrace(); } }); long end = system.nanotime(); system.out.println(account.getbalance() + \u0026#34; cost: \u0026#34; + (end-start)/1000_000 + \u0026#34; ms\u0026#34;); } } 原有实现并不是线程安全的\nclass accountunsafe implements account { private integer balance; public accountunsafe(integer balance) { this.balance = balance; } @override public integer getbalance() { return balance; } @override public void withdraw(integer amount) { balance -= amount; } } 执行测试代码\npublic static void main(string[] args) { account.demo(new accountunsafe(10000)); } 某次的执行结果\n330 cost: 306 ms 为什么不安全 withdraw 方法\npublic void withdraw(integer amount) { balance -= amount; } 对应的字节码\naload 0 // \u0026lt;- this aload 0 getfield cn/itcast/accountunsafe.balance : ljava/lang/integer; // \u0026lt;- this.balance invokevirtual java/lang/integer.intvalue ()i // 拆箱 aload 1 // \u0026lt;- amount invokevirtual java/lang/integer.intvalue ()i // 拆箱 isub // 减法 invokestatic java/lang/integer.valueof (i)ljava/lang/integer; // 结果装箱 putfield cn/itcast/accountunsafe.balance : ljava/lang/integer; // -\u0026gt; this.balance 多线程执行\naload 0 // thread-0 \u0026lt;- this aload 0 getfield cn/itcast/accountunsafe.balance // thread-0 \u0026lt;- this.balance invokevirtual java/lang/integer.intvalue // thread-0 拆箱 aload 1 // thread-0 \u0026lt;- amount invokevirtual java/lang/integer.intvalue // thread-0 拆箱 isub // thread-0 减法 invokestatic java/lang/integer.valueof // thread-0 结果装箱 putfield cn/itcast/accountunsafe.balance // thread-0 -\u0026gt; this.balance aload 0 // thread-1 \u0026lt;- this aload 0 getfield cn/itcast/accountunsafe.balance // thread-1 \u0026lt;- this.balance invokevirtual java/lang/integer.intvalue // thread-1 拆箱 aload 1 // thread-1 \u0026lt;- amount invokevirtual java/lang/integer.intvalue // thread-1 拆箱 isub // thread-1 减法 invokestatic java/lang/integer.valueof // thread-1 结果装箱 putfield cn/itcast/accountunsafe.balance // thread-1 -\u0026gt; this.balance 原因:integer虽然是不可变类,其方法是线程安全的,但是以上操作涉及到了多个方法的组合,等价于以下代码:\nbalance = new integer(integer.valueof(balance) - amount);\n前一个方法(valueof)的结果决定后一个方法(构造方法),这种组合在多线程环境下线程不安全。\n解决思路-锁(悲观互斥) 首先想到的是给 account 对象加锁\nclass accountunsafe implements account { private integer balance; public accountunsafe(integer balance) { this.balance = balance; } @override public synchronized integer getbalance() { return balance; } @override public synchronized void withdraw(integer amount) { balance -= amount; } } 结果为\n0 cost: 399 ms 解决思路-无锁(乐观重试) class accountsafe implements account { private atomicinteger balance; public accountsafe(integer balance) { this.balance = new atomicinteger(balance); } @override public integer getbalance() { return balance.get(); } @override public void withdraw(integer amount) { while (true) { int prev = balance.get(); int next = prev - amount; if (balance.compareandset(prev, next)) { break; } } // 可以简化为下面的方法 // balance.addandget(-1 * amount); } } 执行测试代码\npublic static void main(string[] args) { account.demo(new accountsafe(10000)); } 某次的执行结果\n0 cost: 302 ms cas 与 volatile 前面看到的 atomicinteger 的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?\npublic void withdraw(integer amount) { while(true) { // 需要不断尝试,直到成功为止 while (true) { // 比如拿到了旧值 1000 int prev = balance.get(); // 在这个基础上 1000-10 = 990 int next = prev - amount; /* compareandset 正是做这个检查,在 set 前,先比较 prev 与当前值 - 不一致了,next 作废,返回 false 表示失败 比如,别的线程已经做了减法,当前值已经被减成了 990 那么本线程的这次 990 就作废了,进入 while 下次循环重试 - 一致,以 next 设置为新值,返回 true 表示成功 */ if (balance.compareandset(prev, next)) { break; } //或者简洁一点: //balance.getandadd(-1 * amount); } } } 其中的关键是 compareandset,它的简称就是 cas (也有 compare and swap 的说法),它必须是原子操作。\n注意\n其实 cas 的底层是 lock cmpxchg 指令(x86 架构),在单核 cpu 和多核 cpu 下都能够保证【比较-交 换】的原子性。\n在多核状态下,某个核执行到带 lock 的指令时,cpu 会让总线锁住,当这个核把此指令执行完毕,再 开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子 的。\nvolatile 获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。\n它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。\n注意\nvolatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)\ncas 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。\n为什么无锁效率高\n无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,类似于自旋。而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。线程的上下文切换是费时的,在重试次数不是太多时,无锁的效率高于有锁。 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火, 等被唤醒又得重新打火、启动、加速\u0026hellip; 恢复到高速运行,代价比较大 但无锁情况下,因为线程要保持运行,需要额外 cpu 的支持,cpu 在这里就好比高速跑道,没有额外的跑 道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还 是会导致上下文切换。所以总的来说,当线程数小于等于cpu核心数时,使用无锁方案是很合适的,因为有足够多的cpu让线程运行。当线程数远多于cpu核心数时,无锁效率相比于有锁就没有太大优势,因为依旧会发生上下文切换。 cas 的特点 结合 cas 和 volatile 可以实现无锁并发,适用于线程数少、多核 cpu 的场景下。\ncas 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再 重试呗。 synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想 改,我改完了解开锁,你们才有机会。 cas 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响 原子整数 j.u.c 并发包提供了:\natomicboolean atomicinteger atomiclong 以 atomicinteger 为例\natomicinteger i = new atomicinteger(0); // 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++ system.out.println(i.getandincrement()); // 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i system.out.println(i.incrementandget()); // 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i system.out.println(i.decrementandget()); // 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i-- system.out.println(i.getanddecrement()); // 获取并加值(i = 0, 结果 i = 5, 返回 0) system.out.println(i.getandadd(5)); // 加值并获取(i = 5, 结果 i = 0, 返回 0) system.out.println(i.addandget(-5)); // 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0) // 其中函数中的操作能保证原子,但函数需要无副作用 system.out.println(i.getandupdate(p -\u0026gt; p - 2)); // 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0) // 其中函数中的操作能保证原子,但函数需要无副作用 system.out.println(i.updateandget(p -\u0026gt; p + 2)); // 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0) // 其中函数中的操作能保证原子,但函数需要无副作用 // getandupdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的 // getandaccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final system.out.println(i.getandaccumulate(10, (p, x) -\u0026gt; p + x)); // 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0) // 其中函数中的操作能保证原子,但函数需要无副作用 system.out.println(i.accumulateandget(-10, (p, x) -\u0026gt; p + x)); 说明:\n以上方法都是以cas为基础进行了封装,保证了方法的原子性和变量的可见性。\nupdateandget方法的手动实现:\npublic static int updateandget(atomicinteger i, intunaryoperator operator){ while (true){ int prev = i.get(); int next = operator.applyasint(prev); if(i.compareandset(prev,next)){ return next; } } } 原子引用 为什么需要原子引用类型?\natomicreference atomicmarkablereference atomicstampedreference 实际开发的过程中我们使用的不一定是int、long等基本数据类型,也有可能时bigdecimal这样的类型,这时就需要用到原子引用作为容器。原子引用设置值使用的是unsafe.compareandswapobject()方法。原子引用中表示数据的类型需要重写equals()方法。\n有如下方法\npublic interface decimalaccount { // 获取余额 bigdecimal getbalance(); // 取款 void withdraw(bigdecimal amount); /** * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作 * 如果初始余额为 10000 那么正确的结果应当是 0 */ static void demo(decimalaccount account) { list\u0026lt;thread\u0026gt; ts = new arraylist\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; 1000; i++) { ts.add(new thread(() -\u0026gt; { account.withdraw(bigdecimal.ten); })); } ts.foreach(thread::start); ts.foreach(t -\u0026gt; { try { t.join(); } catch (interruptedexception e) { e.printstacktrace(); } }); system.out.println(account.getbalance()); } } 试着提供不同的 decimalaccount 实现,实现安全的取款操作\n不安全实现 class decimalaccountunsafe implements decimalaccount { bigdecimal balance; public decimalaccountunsafe(bigdecimal balance) { this.balance = balance; } @override public bigdecimal getbalance() { return balance; } @override public void withdraw(bigdecimal amount) { bigdecimal balance = this.getbalance(); this.balance = balance.subtract(amount); } } 安全实现-使用锁 class decimalaccountsafelock implements decimalaccount { private final object lock = new object(); bigdecimal balance; public decimalaccountsafelock(bigdecimal balance) { this.balance = balance; } @override public bigdecimal getbalance() { return balance; } @override public void withdraw(bigdecimal amount) { synchronized (lock) { bigdecimal balance = this.getbalance(); this.balance = balance.subtract(amount); } } } 安全实现-使用 cas class decimalaccountsafecas implements decimalaccount { atomicreference\u0026lt;bigdecimal\u0026gt; ref; public decimalaccountsafecas(bigdecimal balance) { ref = new atomicreference\u0026lt;\u0026gt;(balance); } @override public bigdecimal getbalance() { return ref.get(); } @override public void withdraw(bigdecimal amount) { while (true) { bigdecimal prev = ref.get(); bigdecimal next = prev.subtract(amount); if (ref.compareandset(prev, next)) { break; } } } } 测试代码\ndecimalaccount.demo(new decimalaccountunsafe(new bigdecimal(\u0026#34;10000\u0026#34;))); decimalaccount.demo(new decimalaccountsafelock(new bigdecimal(\u0026#34;10000\u0026#34;))); decimalaccount.demo(new decimalaccountsafecas(new bigdecimal(\u0026#34;10000\u0026#34;))); 运行结果\n4310 cost: 425 ms 0 cost: 285 ms 0 cost: 274 ms aba 问题及解决 aba 问题\nstatic atomicreference\u0026lt;string\u0026gt; ref = new atomicreference\u0026lt;\u0026gt;(\u0026#34;a\u0026#34;); public static void main(string[] args) throws interruptedexception { log.debug(\u0026#34;main start...\u0026#34;); // 获取值 a // 这个共享变量被它线程修改过? string prev = ref.get(); other(); sleep(1); // 尝试改为 c log.debug(\u0026#34;change a-\u0026gt;c {}\u0026#34;, ref.compareandset(prev, \u0026#34;c\u0026#34;)); } private static void other() { new thread(() -\u0026gt; { log.debug(\u0026#34;change a-\u0026gt;b {}\u0026#34;, ref.compareandset(ref.get(), \u0026#34;b\u0026#34;)); }, \u0026#34;t1\u0026#34;).start(); sleep(0.5); new thread(() -\u0026gt; { log.debug(\u0026#34;change b-\u0026gt;a {}\u0026#34;, ref.compareandset(ref.get(), \u0026#34;a\u0026#34;)); }, \u0026#34;t2\u0026#34;).start(); } 输出\n11:29:52.325 c.test36 [main] - main start... 11:29:52.379 c.test36 [t1] - change a-\u0026gt;b true 11:29:52.879 c.test36 [t2] - change b-\u0026gt;a true 11:29:53.880 c.test36 [main] - change a-\u0026gt;c true 主线程仅能判断出共享变量的值与最初值 a 是否相同,不能感知到这种从 a 改为 b 又 改回 a 的情况,如果主线程 希望:\n只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号\natomicstampedreference\nstatic atomicstampedreference\u0026lt;string\u0026gt; ref = new atomicstampedreference\u0026lt;\u0026gt;(\u0026#34;a\u0026#34;, 0); public static void main(string[] args) throws interruptedexception { log.debug(\u0026#34;main start...\u0026#34;); // 获取值 a string prev = ref.getreference(); // 获取版本号 int stamp = ref.getstamp(); log.debug(\u0026#34;版本 {}\u0026#34;, stamp); // 如果中间有其它线程干扰,发生了 aba 现象 other(); sleep(1); // 尝试改为 c log.debug(\u0026#34;change a-\u0026gt;c {}\u0026#34;, ref.compareandset(prev, \u0026#34;c\u0026#34;, stamp, stamp + 1)); } private static void other() { new thread(() -\u0026gt; { log.debug(\u0026#34;change a-\u0026gt;b {}\u0026#34;, ref.compareandset(ref.getreference(), \u0026#34;b\u0026#34;, ref.getstamp(), ref.getstamp() + 1)); log.debug(\u0026#34;更新版本为 {}\u0026#34;, ref.getstamp()); }, \u0026#34;t1\u0026#34;).start(); sleep(0.5); new thread(() -\u0026gt; { log.debug(\u0026#34;change b-\u0026gt;a {}\u0026#34;, ref.compareandset(ref.getreference(), \u0026#34;a\u0026#34;, ref.getstamp(), ref.getstamp() + 1)); log.debug(\u0026#34;更新版本为 {}\u0026#34;, ref.getstamp()); }, \u0026#34;t2\u0026#34;).start(); } 输出为\n15:41:34.891 c.test36 [main] - main start... 15:41:34.894 c.test36 [main] - 版本 0 15:41:34.956 c.test36 [t1] - change a-\u0026gt;b true 15:41:34.956 c.test36 [t1] - 更新版本为 1 15:41:35.457 c.test36 [t2] - change b-\u0026gt;a true 15:41:35.457 c.test36 [t2] - 更新版本为 2 15:41:36.457 c.test36 [main] - change a-\u0026gt;c false atomicstampedreference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: a -\u0026gt; b -\u0026gt; a -\u0026gt; c ,通过atomicstampedreference,我们可以知道,引用变量中途被更改了几次。\n但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了 atomicmarkablereference\ngraph td s(保洁阿姨) m(主人) g1(垃圾袋) g2(新垃圾袋) s -. 倒空 .-\u0026gt; g1 m -- 检查 --\u0026gt; g1 g1 -- 已满 --\u0026gt; g2 g1 -- 还空 --\u0026gt; g1 atomicmarkablereference\nclass garbagebag { string desc; public garbagebag(string desc) { this.desc = desc; } public void setdesc(string desc) { this.desc = desc; } @override public string tostring() { return super.tostring() + \u0026#34; \u0026#34; + desc; } } @slf4j public class testabaatomicmarkablereference { } 输出\n2019-10-13 15:30:09.264 [main] 主线程 start... 2019-10-13 15:30:09.270 [main] cn.itcast.garbagebag@5f0fd5a0 装满了垃圾 2019-10-13 15:30:09.293 [thread-1] 打扫卫生的线程 start... 2019-10-13 15:30:09.294 [thread-1] cn.itcast.garbagebag@5f0fd5a0 空垃圾袋 2019-10-13 15:30:10.294 [main] 主线程想换一只新垃圾袋? 2019-10-13 15:30:10.294 [main] 换了么?false 2019-10-13 15:30:10.294 [main] cn.itcast.garbagebag@5f0fd5a0 空垃圾袋 可以注释掉打扫卫生线程代码,再观察输出\n原子数组 atomicintegerarray atomiclongarray atomicreferencearray 有如下方法\n/** 参数1,提供数组、可以是线程不安全数组或线程安全数组 参数2,获取数组长度的方法 参数3,自增方法,回传 array, index 参数4,打印数组的方法 */ // supplier 提供者 无中生有 ()-\u0026gt;结果 // function 函数 一个参数一个结果 (参数)-\u0026gt;结果 , bifunction (参数1,参数2)-\u0026gt;结果 // consumer 消费者 一个参数没结果 (参数)-\u0026gt;void, biconsumer (参数1,参数2)-\u0026gt; private static \u0026lt;t\u0026gt; void demo( supplier\u0026lt;t\u0026gt; arraysupplier, function\u0026lt;t, integer\u0026gt; lengthfun, biconsumer\u0026lt;t, integer\u0026gt; putconsumer, consumer\u0026lt;t\u0026gt; printconsumer ) { list\u0026lt;thread\u0026gt; ts = new arraylist\u0026lt;\u0026gt;(); t array = arraysupplier.get(); int length = lengthfun.apply(array); for (int i = 0; i \u0026lt; length; i++) { // 每个线程对数组作 10000 次操作 ts.add(new thread(() -\u0026gt; { for (int j = 0; j \u0026lt; 10000; j++) { putconsumer.accept(array, j%length); } })); } ts.foreach(t -\u0026gt; t.start()); // 启动所有线程 ts.foreach(t -\u0026gt; { try { t.join(); } catch (interruptedexception e) { e.printstacktrace(); } }); // 等所有线程结束 printconsumer.accept(array); } 不安全的数组\ndemo( ()-\u0026gt;new int[10], (array)-\u0026gt;array.length, (array, index) -\u0026gt; array[index]++, //出现线程问题 array-\u0026gt; system.out.println(arrays.tostring(array)) ); 结果\n[9870, 9862, 9774, 9697, 9683, 9678, 9679, 9668, 9680, 9698] 安全的数组\ndemo( ()-\u0026gt; new atomicintegerarray(10), (array) -\u0026gt; array.length(), (array, index) -\u0026gt; array.getandincrement(index), //解决线程问题 array -\u0026gt; system.out.println(array) ); 结果\n[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000] 字段更新器 atomicreferencefieldupdater // 域 字段 atomicintegerfieldupdater atomiclongfieldupdater 利用字段更新器,可以针对对象的某个域(field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现 异常 exception in thread \u0026#34;main\u0026#34; java.lang.illegalargumentexception: must be volatile type public class test5 { private volatile int field; public static void main(string[] args) { atomicintegerfieldupdater fieldupdater = atomicintegerfieldupdater.newupdater(test5.class, \u0026#34;field\u0026#34;); test5 test5 = new test5(); fieldupdater.compareandset(test5, 0, 10); // 修改成功 field = 10 system.out.println(test5.field); // 修改成功 field = 20 fieldupdater.compareandset(test5, 10, 20); system.out.println(test5.field); // 修改失败 field = 20 fieldupdater.compareandset(test5, 10, 30); system.out.println(test5.field); } } 输出\n10 20 20 原子累加器 累加器性能比较 private static \u0026lt;t\u0026gt; void demo(supplier\u0026lt;t\u0026gt; addersupplier, consumer\u0026lt;t\u0026gt; action) { t adder = addersupplier.get(); long start = system.nanotime(); list\u0026lt;thread\u0026gt; ts = new arraylist\u0026lt;\u0026gt;(); // 4 个线程,每人累加 50 万 for (int i = 0; i \u0026lt; 40; i++) { ts.add(new thread(() -\u0026gt; { for (int j = 0; j \u0026lt; 500000; j++) { action.accept(adder); } })); } ts.foreach(t -\u0026gt; t.start()); ts.foreach(t -\u0026gt; { try { t.join(); } catch (interruptedexception e) { e.printstacktrace(); } }); long end = system.nanotime(); system.out.println(adder + \u0026#34; cost:\u0026#34; + (end - start)/1000_000); } 比较 atomiclong 与 longadder\nfor (int i = 0; i \u0026lt; 5; i++) { demo(() -\u0026gt; new longadder(), adder -\u0026gt; adder.increment()); } for (int i = 0; i \u0026lt; 5; i++) { demo(() -\u0026gt; new atomiclong(), adder -\u0026gt; adder.getandincrement()); } 输出\n1000000 cost:43 1000000 cost:9 1000000 cost:7 1000000 cost:7 1000000 cost:7 1000000 cost:31 1000000 cost:27 1000000 cost:28 1000000 cost:24 1000000 cost:22 longadder与atomiclong性能对比\n性能提升的原因很简单,就是在有竞争时,设置多个累加单元,therad-0 累加 cell[0],而 thread-1 累加 cell[1]\u0026hellip; 最后将结果汇总。这样它们在累加时操作的不同的 cell 变量,因此减少了 cas 重试失败,从而提高性能。\n伪共享原理(cpu 缓存结构) 其中 cell 即为累加单元\n// 防止缓存行伪共享 @sun.misc.contended static final class cell { volatile long value; cell(long x) { value = x; } // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值 final boolean cas(long prev, long next) { return unsafe.compareandswaplong(this, valueoffset, prev, next); } // 省略不重要代码 } 得从缓存说起\n缓存与内存的速度比较\ncpu 缓存结构\n速度比较\n从cpu到 大约需要的时钟周期 寄存器 1 cycle l1 3~4 cycle l2 10~20 cycle l3 40~45 cycle 内存 120~240 cycle 因为 cell 是数组形式,在内存中是连续存储的,一个 cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 cell 对象。\n这样问题来了: core-0 要修改 cell[0] core-1 要修改 cell[1] 无论谁修改成功,都会导致对方 core 的缓存行失效,比如 core-0 中 cell[0]=6000, cell[1]=8000 要累加 cell[0]=6001, cell[1]=8000 ,这时会让 core-1 的缓存行失效\n@sun.misc.contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,从而让 cpu 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效\n说白了就是对数组进行解耦\nlongadder源码 longadder 是并发大师 @author doug lea (大哥李)的作品,设计的非常精巧\nlongadder 类有几个关键域\n// 累加单元数组, 懒惰初始化 transient volatile cell[] cells; // 基础值, 如果没有竞争, 则用 cas 累加这个域 transient volatile long base; // 在 cells 创建或扩容时, 置为 1, 表示加锁 transient volatile int cellsbusy; 累加主要调用下面的方法\npublic void add(long x) { // as 为累加单元数组 // b 为基础值 // x 为累加值 cell[] as; long b, v; int m; cell a; // 进入 if 的两个条件 // 1. as 有值, 表示已经发生过竞争, 进入 if // 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if if ((as = cells) != null || !casbase(b = base, b + x)) { // uncontended 表示 cell 没有竞争 boolean uncontended = true; if ( // as 还没有创建 as == null || (m = as.length - 1) \u0026lt; 0 || // 当前线程对应的 cell 还没有 // getprobe()方法返回的是线程中的threadlocalrandomprobe字段 // 它是通过随机数生成的一个值,对于一个确定的线程这个值是固定的 // 除非刻意修改它 (a = as[getprobe() \u0026amp; m]) == null || // cas 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell ) !(uncontended = a.cas(v = a.value, v + x)) ) { // 进入 cell 数组创建、cell 创建的流程 longaccumulate(x, null, uncontended); } } } add 流程图\n流程 :\n如果已经有了累加数组或给base累加发生了竞争导致失败 如果累加数组没有创建或者累加数组长度为1或者当前线程还没有对应的cell或者累加cell失败 进入累加数组的创建流程 否者说明累加成功,退出。 否则累加成功 final void longaccumulate(long x, longbinaryoperator fn, boolean wasuncontended) { int h; if ((h = getprobe()) == 0){...} boolean collide = false; for (;;) { cell[] as; cell a; int n; long v; if ((as = cells) != null \u0026amp;\u0026amp; (n = as.length) \u0026gt; 0){...} //cells数组已经创建 else if (cellsbusy == 0 \u0026amp;\u0026amp; cells == as \u0026amp;\u0026amp; cascellsbusy()){...} // cells数组未创建 else if (casbase(v = base, ((fn == null) ? v + x : fn.applyaslong(v, x)))) break; } } longaccumulate 流程图\nelse if (cellsbusy == 0 \u0026amp;\u0026amp; cells == as \u0026amp;\u0026amp; cascellsbusy()){...} // cells数组未创建 else if (casbase(v = base, ((fn == null) ? v + x : fn.applyaslong(v, x)))) 是否被加锁;是否被其他线程创建并且修改;是否加锁成功 创建cell对象 (yes) 累加 (no) 退出循环 (yes) 继续循环 (no) if ((as = cells) != null \u0026amp;\u0026amp; (n = as.length) \u0026gt; 0){ if ((a = as[(n - 1) \u0026amp; h]) == null){...} } 存在cells数组,但是cell对象未创建 if (cellsbusy == 0 \u0026amp;\u0026amp; cascellsbusy())成立,加锁成功,创建cell 对象 if (cellsbusy == 0 \u0026amp;\u0026amp; cascellsbusy())不成立,continue,继续循环 每个线程刚进入 longaccumulate 时,会尝试对应一个 cell 对象(找到一个坑位)\nelse if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyaslong(v, x)))) else if (n \u0026gt;= ncpu || cells != as) else if (cellsbusy == 0 \u0026amp;\u0026amp; cascellsbusy()){...} h = advanceprobe(h); cells存在并且cell对象存在,继续cas cell累加 返回(yes) 判断硬件条件,进行加锁扩容(no) 进行扩容(yes) 改变当前线程的cell(no) 获取最终结果通过 sum 方法\npublic long sum() { cell[] as = cells; cell a; long sum = base; if (as != null) { for (int i = 0; i \u0026lt; as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; } unsafe 概述\nunsafe 对象提供了非常底层的,操作内存、线程的方法,unsafe 对象不能直接调用,只能通过反射获得。jdk8直接调用unsafe.getunsafe()获得的unsafe不能用。\npublic class unsafeaccessor { static unsafe unsafe; static { try { field theunsafe = unsafe.class.getdeclaredfield(\u0026#34;theunsafe\u0026#34;); theunsafe.setaccessible(true); unsafe = (unsafe) theunsafe.get(null); } catch (nosuchfieldexception | illegalaccessexception e) { throw new error(e); } } static unsafe getunsafe() { return unsafe; } } unsafe cas 操作 unsafe实现字段更新\n@data class student { volatile int id; volatile string name; } unsafe unsafe = unsafeaccessor.getunsafe(); //不能正常调用unsafe方法,需要用到反射调用。即封装反射类unsafeaccessor field id = student.class.getdeclaredfield(\u0026#34;id\u0026#34;); field name = student.class.getdeclaredfield(\u0026#34;name\u0026#34;); // 获得成员变量的偏移量 long idoffset = unsafeaccessor.unsafe.objectfieldoffset(id); long nameoffset = unsafeaccessor.unsafe.objectfieldoffset(name); student student = new student(); // 使用 cas 方法替换成员变量的值 unsafeaccessor.unsafe.compareandswapint(student, idoffset, 0, 20); // 返回 true unsafeaccessor.unsafe.compareandswapobject(student, nameoffset, null, \u0026#34;张三\u0026#34;); // 返回 true system.out.println(student); 输出\nstudent(id=20, name=张三) unsafeaccessor\nclass unsafeaccessor{ public static unsafe getunsafe(){ field field; unsafe unsafe = null; try { field = unsafe.class.getdeclaredfield(\u0026#34;theunsafe\u0026#34;); field.setaccessible(true); unsafe = (unsafe)field.get(null); } catch (exception e) { e.printstacktrace(); } return unsafe; } } unsafe实现原子整数 class atomicdata { private volatile int data; static final unsafe unsafe; static final long data_offset; static { unsafe = unsafeaccessor.getunsafe(); try { // data 属性在 datacontainer 对象中的偏移量,用于 unsafe 直接访问该属性 data_offset = unsafe.objectfieldoffset(atomicdata.class.getdeclaredfield(\u0026#34;data\u0026#34;)); } catch (nosuchfieldexception e) { throw new error(e); } } public atomicdata(int data) { this.data = data; } public void decrease(int amount) { int oldvalue; while(true) { // 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解 oldvalue = data; // cas 尝试修改 data 为 旧值 + amount,如果期间旧值被别的线程改了,返回 false if (unsafe.compareandswapint(this, data_offset, oldvalue, oldvalue - amount)) { return; } } } public int getdata() { return data; } } account 实现\naccount.demo(new account() { atomicdata atomicdata = new atomicdata(10000); @override public integer getbalance() { return atomicdata.getdata(); } @override public void withdraw(integer amount) { atomicdata.decrease(amount); } }); 本章小结 cas 与 volatile api 原子整数 原子引用 原子数组 字段更新器 原子累加器 unsafe 原理方面 longadder 源码 伪共享 共享模型之不可变 日期转换的问题 问题提出 下面的代码在运行时,由于 simpledateformat 不是线程安全的\nsimpledateformat sdf = new simpledateformat(\u0026#34;yyyy-mm-dd\u0026#34;); for (int i = 0; i \u0026lt; 10; i++) { new thread(() -\u0026gt; { try { log.debug(\u0026#34;{}\u0026#34;, sdf.parse(\u0026#34;1951-04-21\u0026#34;)); } catch (exception e) { log.error(\u0026#34;{}\u0026#34;, e); } }).start(); } 有很大几率出现 java.lang.numberformatexception 或者出现不正确的日期解析结果,例如:\n19:10:40.859 [thread-2] c.testdateparse - {} java.lang.numberformatexception: for input string: \u0026#34;\u0026#34; at java.lang.numberformatexception.forinputstring(numberformatexception.java:65) at java.lang.long.parselong(long.java:601) at java.lang.long.parselong(long.java:631) at java.text.digitlist.getlong(digitlist.java:195) at java.text.decimalformat.parse(decimalformat.java:2084) at java.text.simpledateformat.subparse(simpledateformat.java:2162) at java.text.simpledateformat.parse(simpledateformat.java:1514) at java.text.dateformat.parse(dateformat.java:364) at cn.itcast.n7.testdateparse.lambda$test1$0(testdateparse.java:18) at java.lang.thread.run(thread.java:748) 19:10:40.859 [thread-1] c.testdateparse - {} java.lang.numberformatexception: empty string at sun.misc.floatingdecimal.readjavaformatstring(floatingdecimal.java:1842) at sun.misc.floatingdecimal.parsedouble(floatingdecimal.java:110) at java.lang.double.parsedouble(double.java:538) at java.text.digitlist.getdouble(digitlist.java:169) at java.text.decimalformat.parse(decimalformat.java:2089) at java.text.simpledateformat.subparse(simpledateformat.java:2162) at java.text.simpledateformat.parse(simpledateformat.java:1514) at java.text.dateformat.parse(dateformat.java:364) at cn.itcast.n7.testdateparse.lambda$test1$0(testdateparse.java:18) at java.lang.thread.run(thread.java:748) 19:10:40.857 [thread-8] c.testdateparse - sat apr 21 00:00:00 cst 1951 19:10:40.857 [thread-9] c.testdateparse - sat apr 21 00:00:00 cst 1951 19:10:40.857 [thread-6] c.testdateparse - sat apr 21 00:00:00 cst 1951 19:10:40.857 [thread-4] c.testdateparse - sat apr 21 00:00:00 cst 1951 19:10:40.857 [thread-5] c.testdateparse - mon apr 21 00:00:00 cst 178960645 19:10:40.857 [thread-0] c.testdateparse - sat apr 21 00:00:00 cst 1951 19:10:40.857 [thread-7] c.testdateparse - sat apr 21 00:00:00 cst 1951 19:10:40.857 [thread-3] c.testdateparse - sat apr 21 00:00:00 cst 1951 思路 - 同步锁 这样虽能解决问题,但带来的是性能上的损失,并不算很好:\nsimpledateformat sdf = new simpledateformat(\u0026#34;yyyy-mm-dd\u0026#34;); for (int i = 0; i \u0026lt; 50; i++) { new thread(() -\u0026gt; { synchronized (sdf) { try { log.debug(\u0026#34;{}\u0026#34;, sdf.parse(\u0026#34;1951-04-21\u0026#34;)); } catch (exception e) { log.error(\u0026#34;{}\u0026#34;, e); } } }).start(); } 思路 - 不可变 如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!这样的对象在 java 中有很多,例如在 java 8 后,提供了一个新的日期格式化类:\ndatetimeformatter dtf = datetimeformatter.ofpattern(\u0026#34;yyyy-mm-dd\u0026#34;); for (int i = 0; i \u0026lt; 10; i++) { new thread(() -\u0026gt; { localdate date = dtf.parse(\u0026#34;2018-10-01\u0026#34;, localdate::from); log.debug(\u0026#34;{}\u0026#34;, date); }).start(); } 可以看 datetimeformatter 的文档:\n@implspec //this class is immutable and thread-safe. 不可变对象,实际是另一种避免竞争的方式。\n不可变设计 string类的设计 另一个大家更为熟悉的 string 类也是不可变的,以它为例,说明一下不可变设计的要素\npublic final class string implements java.io.serializable, comparable\u0026lt;string\u0026gt;, charsequence { /** the value is used for character storage. */ private final char value[]; /** cache the hash code for the string */ private int hash; // default to 0 // ... } 说明:\n将类声明为final,避免被带外星方法的子类继承,从而破坏了不可变性。 将字符数组声明为final,避免被修改 hash虽然不是final的,但是其只有在调用hash()方法的时候才被赋值,除此之外再无别的方法修改。 final 的使用 发现该类、类中所有属性都是 final 的\n属性用 final 修饰保证了该属性是只读的,不能修改 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性 保护性拷贝 但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是 如何实现的,就以 substring 为例:\npublic string substring(int beginindex) { if (beginindex \u0026lt; 0) { throw new stringindexoutofboundsexception(beginindex); } int sublen = value.length - beginindex; if (sublen \u0026lt; 0) { throw new stringindexoutofboundsexception(sublen); } return (beginindex == 0) ? this : new string(value, beginindex, sublen); } 发现其内部是调用 string 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 final char[] value 做出 了修改:\npublic string(char value[], int offset, int count) { if (offset \u0026lt; 0) { throw new stringindexoutofboundsexception(offset); } if (count \u0026lt;= 0) { if (count \u0026lt; 0) { throw new stringindexoutofboundsexception(count); } if (offset \u0026lt;= value.length) { this.value = \u0026#34;\u0026#34;.value; return; } } if (offset \u0026gt; value.length - count) { throw new stringindexoutofboundsexception(offset + count); } this.value = arrays.copyofrange(value, offset, offset+count); } 结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避 免共享的手段称之为【保护性拷贝(defensive copy)】\n享元模式 定义 享元模式(flyweight pattern)又叫轻量级模式,是对象池的一种标签。类似线程池,线程池可以避免不停的创建和销毁对象,消耗性能。享元模式可以减少对象数量,其宗旨是共享细粒度对象,将多个对同一对象的访问集中起来,属于结构型设计模式\n英文名称:flyweight pattern. 当需要重用数量有限的同一类对象时\n实现 包装类\n在jdk中 boolean,byte,short,integer,long,character 等包装类提供了 valueof 方法,例如 long 的 valueof 会缓存 -128~127 之间的 long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 long 对 象:\npublic static long valueof(long l) { final int offset = 128; if (l \u0026gt;= -128 \u0026amp;\u0026amp; l \u0026lt;= 127) { // will cache return longcache.cache[(int)l + offset]; } return new long(l); } 注意:\nbyte, short, long 缓存的范围都是 -128~127 character 缓存的范围是 0~127 integer的默认范围是 -128~127 最小值不能变 但最大值可以通过调整虚拟机参数 -djava.lang.integer.integercache.high 来改变 boolean 缓存了 true 和 false 手动实现一个连接池\n例如:一个线上商城应用,qps 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时 预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约 了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。\nclass pool { // 1. 连接池大小 private final int poolsize; // 2. 连接对象数组 private connection[] connections; // 3. 连接状态数组 0 表示空闲, 1 表示繁忙 private atomicintegerarray states; // 4. 构造方法初始化 public pool(int poolsize) { this.poolsize = poolsize; this.connections = new connection[poolsize]; this.states = new atomicintegerarray(new int[poolsize]); for (int i = 0; i \u0026lt; poolsize; i++) { connections[i] = new mockconnection(\u0026#34;连接\u0026#34; + (i+1)); } } // 5. 借连接 public connection borrow() { while(true) { for (int i = 0; i \u0026lt; poolsize; i++) { // 获取空闲连接 if(states.get(i) == 0) { if (states.compareandset(i, 0, 1)) { log.debug(\u0026#34;borrow {}\u0026#34;, connections[i]); return connections[i]; } } } // 如果没有空闲连接,当前线程进入等待 synchronized (this) { try { log.debug(\u0026#34;wait...\u0026#34;); this.wait(); } catch (interruptedexception e) { e.printstacktrace(); } } } } // 6. 归还连接 public void free(connection conn) { for (int i = 0; i \u0026lt; poolsize; i++) { if (connections[i] == conn) { states.set(i, 0); synchronized (this) { log.debug(\u0026#34;free {}\u0026#34;, conn); this.notifyall(); } break; } } } } class mockconnection implements connection { // 实现略 } 使用连接池:\npool pool = new pool(2); for (int i = 0; i \u0026lt; 5; i++) { new thread(() -\u0026gt; { connection conn = pool.borrow(); try { thread.sleep(new random().nextint(1000)); } catch (interruptedexception e) { e.printstacktrace(); } pool.free(conn); }).start(); } 以上实现没有考虑:\n连接的动态增长与收缩 连接保活(可用性检测) 等待超时处理 分布式 hash 对于关系型数据库,有比较成熟的连接池实现,例如c3p0, druid等 对于更通用的对象池,可以考虑使用apache commons pool,例如redis连接池可以参考jedis中关于连接池的实现\nfinal原理 设置 final 变量的原理\n理解了 volatile 原理,再对比 final 的实现就比较简单了\npublic class testfinal { final int a = 20; } 字节码\n0: aload_0 1: invokespecial #1 // method java/lang/object.\u0026#34;\u0026lt;init\u0026gt;\u0026#34;:()v 4: aload_0 5: bipush 20 7: putfield #2 // field a:i \u0026lt;-- 写屏障 10: return 发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,这样对final变量的写入不会重排序到构造方法之外,保证在其它线程读到 它的值时不会出现为 0 的情况。普通变量不能保证这一点了。\njvm对final变量的访问做出了优化:另一个类中的方法调用final变量,不是从final变量所在类中获取(共享内存),而是直接复制一份到方法栈栈帧中的操作数栈中(工作内存),这样可以提升效率,是一种优化。\n总结:\n对于较小的static final变量:复制一份到操作数栈中 对于较大的static final变量:复制一份到当前类的常量池中 对于非静态final变量,优化同上。 final总结\nfinal关键字的好处:\n(1)final关键字提高了性能。jvm和java应用都会缓存final变量。\n(2)final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。\n(3)使用final关键字,jvm会对方法、变量及类进行优化。\n关于final的重要知识点\n1、final关键字可以用于成员变量、本地变量、方法以及类。\n2、final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。\n3、你不能够对final变量再次赋值。\n4、本地变量必须在声明时赋值。\n5、在匿名类中所有变量都必须是final变量。\n6、final方法不能被重写。\n7、final类不能被继承。\n8、final关键字不同于finally关键字,后者用于异常处理。\n9、final关键字容易与finalize()方法搞混,后者是在object类中定义的方法,是在垃圾回收之前被jvm调用的方法。\n10、接口中声明的所有变量本身是final的。\n11、final和abstract这两个关键字是反相关的,final类就不可能是abstract的。\n12、final方法在编译阶段绑定,称为静态绑定(static binding)。\n13、没有在声明时初始化final变量的称为空白final变量(blank final variable),它们必须在构造器中初始化,或者调用this()初始化。不这么做的话,编译器会报错“final变量(变量名)需要进行初始化”。\n14、将类、方法、变量声明为final能够提高性能,这样jvm就有机会进行估计,然后优化。\n15、按照java代码惯例,final变量就是常量,而且通常常量名要大写。\n16、对于集合对象声明为final指的是引用不能被更改,但是你可以向其中增加,删除或者改变内容。\n无状态 在 web 阶段学习时,设计 servlet 时为了保证其线程安全,都会有这样的建议,不要为 servlet 设置成员变量,这 种没有任何成员变量的类是线程安全的 。\n因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】\n共享模型之工具 线程池 threadpoolexecutor 说明:\nscheduledthreadpoolexecutor是带调度的线程池 threadpoolexecutor是不带调度的线程池 线程池状态 threadpoolexecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量\n状态名 高3位 接收新任务 处理阻塞队列任务 说明 running 111 y y shutdown 000 n y 不会接收新任务,但会处理阻塞队列剩余 任务 stop 001 n n 会中断正在执行的任务,并抛弃阻塞队列 任务 tidying 010 任务全执行完毕,活动线程为 0 即将进入 终结 terminated 011 终结状态 从数字上比较,terminated \u0026gt; tidying \u0026gt; stop \u0026gt; shutdown \u0026gt; running\n这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作 进行赋值\n// c 为旧值, ctlof 返回结果为新值 ctl.compareandset(c, ctlof(targetstate, workercountof(c)))); // rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们 private static int ctlof(int rs, int wc) { return rs | wc; } 构造方法 public threadpoolexecutor(int corepoolsize, int maximumpoolsize, long keepalivetime, timeunit unit, blockingqueue\u0026lt;runnable\u0026gt; workqueue, threadfactory threadfactory, rejectedexecutionhandler handler) corepoolsize 核心线程数目 (最多保留的线程数) maximumpoolsize 最大线程数目 keepalivetime 生存时间 - 针对救急线程 unit 时间单位 - 针对救急线程 workqueue 阻塞队列 threadfactory 线程工厂 - 可以为线程创建时起个好名字 handler 拒绝策略 工作方式 graph lr subgraph 阻塞队列 size=2 t3(任务3) t4(任务4) end subgraph 线程池c-2,m=3 ct1(核心线程1) ct2(核心线程2) mt1(救急线程1) ct1 --\u0026gt; t1(任务1) ct2 --\u0026gt; t2(任务2) end t1(任务1) style ct1 fill:#ccf,stroke:#f66,stroke-width:2px style ct2 fill:#ccf,stroke:#f66,stroke-width:2px style mt1 fill:#ccf,stroke:#f66,stroke-width:2px,stroke-dasharray:5,5 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。\n当线程数达到 corepoolsize 并没有线程空闲,这时再加入任务,新加的任务会被加入workqueue 队列排 队,直到有空闲的线程。\n如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumpoolsize - corepoolsize 数目的线程来救急。\n如果线程到达 maximumpoolsize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它 著名框架也提供了实现\nabortpolicy 让调用者抛出 rejectedexecutionexception 异常,这是默认策略 callerrunspolicy 让调用者运行任务 discardpolicy 放弃本次任务 discardoldestpolicy 放弃队列中最早的任务,本任务取而代之 dubbo 的实现,在抛出 rejectedexecutionexception 异常之前会记录日志,并 dump 线程栈信息,方 便定位问题 netty 的实现,是创建一个新线程来执行任务 activemq 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略 pinpoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略 当高峰过去后,超过corepoolsize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepalivetime 和 unit 来控制。\n根据这个构造方法,jdk executors 类中提供了众多工厂方法来创建各种用途的线程池。\nnewfixedthreadpool public static executorservice newfixedthreadpool(int nthreads) { return new threadpoolexecutor(nthreads, nthreads, 0l, timeunit.milliseconds, new linkedblockingqueue\u0026lt;runnable\u0026gt;()); } 内部调用了:threadpoolexecutor的一个构造方法\npublic threadpoolexecutor(int corepoolsize, int maximumpoolsize, long keepalivetime, timeunit unit, blockingqueue\u0026lt;runnable\u0026gt; workqueue) { this(corepoolsize, maximumpoolsize, keepalivetime, unit, workqueue, executors.defaultthreadfactory(), defaulthandler); } 默认工厂以及默认构造线程的方法:\ndefaultthreadfactory() { securitymanager s = system.getsecuritymanager(); group = (s != null) ? s.getthreadgroup() : thread.currentthread().getthreadgroup(); nameprefix = \u0026#34;pool-\u0026#34; + poolnumber.getandincrement() + \u0026#34;-thread-\u0026#34;; } public thread newthread(runnable r) { thread t = new thread(group, r, nameprefix + threadnumber.getandincrement(), 0); if (t.isdaemon()) t.setdaemon(false); if (t.getpriority() != thread.norm_priority) t.setpriority(thread.norm_priority); return t; } 默认拒绝策略:抛出异常\nprivate static final rejectedexecutionhandler defaulthandler = new abortpolicy(); 特点\n核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间 阻塞队列是无界的,可以放任意数量的任务 评价 适用于任务量已知,相对耗时的任务\nnewcachedthreadpool public static executorservice newcachedthreadpool() { return new threadpoolexecutor(0, integer.max_value, 60l, timeunit.seconds, new synchronousqueue\u0026lt;runnable\u0026gt;()); } 特点\n核心线程数是 0, 最大线程数是 integer.max_value,救急线程的空闲生存时间是 60s, 意味着全部都是救急线程(60s 后可以回收) 救急线程可以无限创建 队列采用了 synchronousqueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货) synchronousqueue\u0026lt;integer\u0026gt; integers = new synchronousqueue\u0026lt;\u0026gt;(); new thread(() -\u0026gt; { try { log.debug(\u0026#34;putting {} \u0026#34;, 1); integers.put(1); log.debug(\u0026#34;{} putted...\u0026#34;, 1); log.debug(\u0026#34;putting...{} \u0026#34;, 2); integers.put(2); log.debug(\u0026#34;{} putted...\u0026#34;, 2); } catch (interruptedexception e) { e.printstacktrace(); } },\u0026#34;t1\u0026#34;).start(); sleep(1); new thread(() -\u0026gt; { try { log.debug(\u0026#34;taking {}\u0026#34;, 1); integers.take(); } catch (interruptedexception e) { e.printstacktrace(); } },\u0026#34;t2\u0026#34;).start(); sleep(1); new thread(() -\u0026gt; { try { log.debug(\u0026#34;taking {}\u0026#34;, 2); integers.take(); } catch (interruptedexception e) { e.printstacktrace(); } },\u0026#34;t3\u0026#34;).start(); 输出\n11:48:15.500 c.testsynchronousqueue [t1] - putting 1 11:48:16.500 c.testsynchronousqueue [t2] - taking 1 11:48:16.500 c.testsynchronousqueue [t1] - 1 putted... 11:48:16.500 c.testsynchronousqueue [t1] - putting...2 11:48:17.502 c.testsynchronousqueue [t3] - taking 2 11:48:17.503 c.testsynchronousqueue [t1] - 2 putted... 评价 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线 程。 适合任务数比较密集,但每个任务执行时间较短的情况\nnewsinglethreadexecutor public static executorservice newsinglethreadexecutor() { return new finalizabledelegatedexecutorservice (new threadpoolexecutor(1, 1, 0l, timeunit.milliseconds, new linkedblockingqueue\u0026lt;runnable\u0026gt;())); } 使用场景:\n希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程 也不会被释放。\n区别:\n自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一 个线程,保证池的正常工作 executors.newsinglethreadexecutor() 线程个数始终为1,不能修改 finalizabledelegatedexecutorservice 应用的是装饰器模式,在调用构造方法时将threadpoolexecutor对象传给了内部的executorservice接口。只对外暴露了 executorservice 接口,因此不能调用 threadpoolexecutor 中特有的方法,也不能重新设置线程池的大小。 executors.newfixedthreadpool(1) 初始时为1,以后还可以修改 对外暴露的是 threadpoolexecutor 对象,可以强转后调用 setcorepoolsize 等方法进行修改 提交任务 // 执行任务 void execute(runnable command); // 提交任务 task,用返回值 future 获得任务执行结果 \u0026lt;t\u0026gt; future\u0026lt;t\u0026gt; submit(callable\u0026lt;t\u0026gt; task); // 提交 tasks 中所有任务 \u0026lt;t\u0026gt; list\u0026lt;future\u0026lt;t\u0026gt;\u0026gt; invokeall(collection\u0026lt;? extends callable\u0026lt;t\u0026gt;\u0026gt; tasks) throws interruptedexception; // 提交 tasks 中所有任务,带超时时间,时间超时后,会放弃执行后面的任务 \u0026lt;t\u0026gt; list\u0026lt;future\u0026lt;t\u0026gt;\u0026gt; invokeall(collection\u0026lt;? extends callable\u0026lt;t\u0026gt;\u0026gt; tasks, long timeout, timeunit unit) throws interruptedexception; // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消 \u0026lt;t\u0026gt; t invokeany(collection\u0026lt;? extends callable\u0026lt;t\u0026gt;\u0026gt; tasks) throws interruptedexception, executionexception; // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间 \u0026lt;t\u0026gt; t invokeany(collection\u0026lt;? extends callable\u0026lt;t\u0026gt;\u0026gt; tasks, long timeout, timeunit unit) throws interruptedexception, executionexception, timeoutexception; 测试submit\nprivate static void method1(executorservice pool) throws interruptedexception, executionexception { future\u0026lt;string\u0026gt; future = pool.submit(() -\u0026gt; { log.debug(\u0026#34;running\u0026#34;); thread.sleep(1000); return \u0026#34;ok\u0026#34;; }); log.debug(\u0026#34;{}\u0026#34;, future.get()); } public static void main(string[] args) throws executionexception, interruptedexception { executorservice pool = executors.newfixedthreadpool(1); method1(pool); } 测试结果\n18:36:58.033 c.testsubmit [pool-1-thread-1] - running 18:36:59.034 c.testsubmit [main] - ok 测试invokeall\nprivate static void method2(executorservice pool) throws interruptedexception { list\u0026lt;future\u0026lt;string\u0026gt;\u0026gt; futures = pool.invokeall(arrays.aslist( () -\u0026gt; { log.debug(\u0026#34;begin\u0026#34;); thread.sleep(1000); return \u0026#34;1\u0026#34;; }, () -\u0026gt; { log.debug(\u0026#34;begin\u0026#34;); thread.sleep(500); return \u0026#34;2\u0026#34;; }, () -\u0026gt; { log.debug(\u0026#34;begin\u0026#34;); thread.sleep(2000); return \u0026#34;3\u0026#34;; } )); futures.foreach( f -\u0026gt; { try { log.debug(\u0026#34;{}\u0026#34;, f.get()); } catch (interruptedexception | executionexception e) { e.printstacktrace(); } }); } public static void main(string[] args) throws executionexception, interruptedexception { executorservice pool = executors.newfixedthreadpool(1); method2(pool); } 测试结果\n19:33:16.530 c.testsubmit [pool-1-thread-1] - begin 19:33:17.530 c.testsubmit [pool-1-thread-1] - begin 19:33:18.040 c.testsubmit [pool-1-thread-1] - begin 19:33:20.051 c.testsubmit [main] - 1 19:33:20.051 c.testsubmit [main] - 2 19:33:20.051 c.testsubmit [main] - 3 测试invokeany\nprivate static void method3(executorservice pool) throws interruptedexception, executionexception { string result = pool.invokeany(arrays.aslist( () -\u0026gt; { log.debug(\u0026#34;begin 1\u0026#34;); thread.sleep(1000); log.debug(\u0026#34;end 1\u0026#34;); return \u0026#34;1\u0026#34;; }, () -\u0026gt; { log.debug(\u0026#34;begin 2\u0026#34;); thread.sleep(500); log.debug(\u0026#34;end 2\u0026#34;); return \u0026#34;2\u0026#34;; }, () -\u0026gt; { log.debug(\u0026#34;begin 3\u0026#34;); thread.sleep(2000); log.debug(\u0026#34;end 3\u0026#34;); return \u0026#34;3\u0026#34;; } )); log.debug(\u0026#34;{}\u0026#34;, result); } public static void main(string[] args) throws executionexception, interruptedexception { executorservice pool = executors.newfixedthreadpool(3); //executorservice pool = executors.newfixedthreadpool(1); method3(pool); } 测试结果\n19:44:46.314 c.testsubmit [pool-1-thread-1] - begin 1 19:44:46.314 c.testsubmit [pool-1-thread-3] - begin 3 19:44:46.314 c.testsubmit [pool-1-thread-2] - begin 2 19:44:46.817 c.testsubmit [pool-1-thread-2] - end 2 19:44:46.817 c.testsubmit [main] - 2 19:47:16.063 c.testsubmit [pool-1-thread-1] - begin 1 19:47:17.063 c.testsubmit [pool-1-thread-1] - end 1 19:47:17.063 c.testsubmit [pool-1-thread-1] - begin 2 19:47:17.063 c.testsubmit [main] - 1 关闭线程池 shutdown\n/* 线程池状态变为 shutdown - 不会接收新任务 - 但正在执行的任务会执行完 - 此方法不会阻塞调用shutdown线程的执行 */ void shutdown(); public void shutdown() { final reentrantlock mainlock = this.mainlock; mainlock.lock(); try { checkshutdownaccess(); // 修改线程池状态 advancerunstate(shutdown); // 仅会打断空闲线程 interruptidleworkers(); onshutdown(); // 扩展点 scheduledthreadpoolexecutor } finally { mainlock.unlock(); } // 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也不会等) tryterminate(); } **shutdownnow **\n/* 线程池状态变为 stop - 不会接收新任务 - 会将队列中的任务返回 - 并用 interrupt 的方式中断正在执行的任务 - 不会阻止main线程的执行 */ list\u0026lt;runnable\u0026gt; shutdownnow(); public list\u0026lt;runnable\u0026gt; shutdownnow() { list\u0026lt;runnable\u0026gt; tasks; final reentrantlock mainlock = this.mainlock; mainlock.lock(); try { checkshutdownaccess(); // 修改线程池状态 advancerunstate(stop); // 打断所有线程 interruptworkers(); // 获取队列中剩余任务 tasks = drainqueue(); } finally { mainlock.unlock(); } // 尝试终结 tryterminate(); return tasks; } 模式之 worker thread 定义\n让有限的工作线程(worker thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现 就是线程池,也体现了经典设计模式中的享元模式。\n例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那 么成本就太高了(对比另一种多线程设计模式:thread-per-message)\n注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率\n例如,如果一个餐馆的工人既要招呼客人(任务类型a),又要到后厨做菜(任务类型b)显然效率不咋地,分成 服务员(线程池a)与厨师(线程池b)更为合理,当然你能想到更细致的分工\n饥饿\n固定大小线程池会有饥饿现象\n两个工人是同一个线程池中的两个线程\n他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作\n客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待 后厨做菜:没啥说的,做就是了 比如工人a 处理了点餐任务,接下来它要等着 工人b 把菜做好,然后上菜,他俩也配合的蛮好\n但现在同时来了两个客人,这个时候工人a 和工人b 都去处理点餐了,这时没人做饭了,饥饿\npublic class testdeadlock { static final list\u0026lt;string\u0026gt; menu = arrays.aslist(\u0026#34;地三鲜\u0026#34;, \u0026#34;宫保鸡丁\u0026#34;, \u0026#34;辣子鸡丁\u0026#34;, \u0026#34;烤鸡翅\u0026#34;); static random random = new random(); static string cooking() { return menu.get(random.nextint(menu.size())); } public static void main(string[] args) { executorservice executorservice = executors.newfixedthreadpool(2); executorservice.execute(() -\u0026gt; { log.debug(\u0026#34;处理点餐...\u0026#34;); future\u0026lt;string\u0026gt; f = executorservice.submit(() -\u0026gt; { log.debug(\u0026#34;做菜\u0026#34;); return cooking(); }); try { log.debug(\u0026#34;上菜: {}\u0026#34;, f.get()); } catch (interruptedexception | executionexception e) { e.printstacktrace(); } }); /* executorservice.execute(() -\u0026gt; { log.debug(\u0026#34;处理点餐...\u0026#34;); future\u0026lt;string\u0026gt; f = executorservice.submit(() -\u0026gt; { log.debug(\u0026#34;做菜\u0026#34;); return cooking(); }); try { log.debug(\u0026#34;上菜: {}\u0026#34;, f.get()); } catch (interruptedexception | executionexception e) { e.printstacktrace(); } }); */ } } 输出\n17:21:27.883 c.testdeadlock [pool-1-thread-1] - 处理点餐... 17:21:27.891 c.testdeadlock [pool-1-thread-2] - 做菜 17:21:27.891 c.testdeadlock [pool-1-thread-1] - 上菜: 烤鸡翅 当注释取消后,可能的输出\n17:08:41.339 c.testdeadlock [pool-1-thread-2] - 处理点餐... 17:08:41.339 c.testdeadlock [pool-1-thread-1] - 处理点餐... 解决方法可以增加线程池的大小,不过不是根本解决方案,还是前面提到的,不同的任务类型,采用不同的线程 池,例如:\npublic class testdeadlock { static final list\u0026lt;string\u0026gt; menu = arrays.aslist(\u0026#34;地三鲜\u0026#34;, \u0026#34;宫保鸡丁\u0026#34;, \u0026#34;辣子鸡丁\u0026#34;, \u0026#34;烤鸡翅\u0026#34;); static random random = new random(); static string cooking() { return menu.get(random.nextint(menu.size())); } public static void main(string[] args) { executorservice waiterpool = executors.newfixedthreadpool(1); executorservice cookpool = executors.newfixedthreadpool(1); waiterpool.execute(() -\u0026gt; { log.debug(\u0026#34;处理点餐...\u0026#34;); future\u0026lt;string\u0026gt; f = cookpool.submit(() -\u0026gt; { log.debug(\u0026#34;做菜\u0026#34;); return cooking(); }); try { log.debug(\u0026#34;上菜: {}\u0026#34;, f.get()); } catch (interruptedexception | executionexception e) { e.printstacktrace(); } }); waiterpool.execute(() -\u0026gt; { log.debug(\u0026#34;处理点餐...\u0026#34;); future\u0026lt;string\u0026gt; f = cookpool.submit(() -\u0026gt; { log.debug(\u0026#34;做菜\u0026#34;); return cooking(); }); try { log.debug(\u0026#34;上菜: {}\u0026#34;, f.get()); } catch (interruptedexception | executionexception e) { e.printstacktrace(); } }); } } 输出\n17:25:14.626 c.testdeadlock [pool-1-thread-1] - 处理点餐... 17:25:14.630 c.testdeadlock [pool-2-thread-1] - 做菜 17:25:14.631 c.testdeadlock [pool-1-thread-1] - 上菜: 地三鲜 17:25:14.632 c.testdeadlock [pool-1-thread-1] - 处理点餐... 17:25:14.632 c.testdeadlock [pool-2-thread-1] - 做菜 17:25:14.632 c.testdeadlock [pool-1-thread-1] - 上菜: 辣子鸡丁 newscheduledthreadpool scheduledexecutorservice executor = executors.newscheduledthreadpool(2); // 添加两个任务,希望它们都在 1s 后执行 executor.schedule(() -\u0026gt; { system.out.println(\u0026#34;任务1,执行时间:\u0026#34; + new date()); try { thread.sleep(2000); } catch (interruptedexception e) { } }, 1000, timeunit.milliseconds); executor.schedule(() -\u0026gt; { system.out.println(\u0026#34;任务2,执行时间:\u0026#34; + new date()); }, 1000, timeunit.milliseconds); 输出\n任务1,执行时间:thu jan 03 12:45:17 cst 2019 任务2,执行时间:thu jan 03 12:45:17 cst 2019 scheduleatfixedrate 例子:\nscheduledexecutorservice pool = executors.newscheduledthreadpool(1); log.debug(\u0026#34;start...\u0026#34;); pool.scheduleatfixedrate(() -\u0026gt; { log.debug(\u0026#34;running...\u0026#34;); }, 1, 1, timeunit.seconds); 输出\n21:45:43.167 c.testtimer [main] - start... 21:45:44.215 c.testtimer [pool-1-thread-1] - running... 21:45:45.215 c.testtimer [pool-1-thread-1] - running... 21:45:46.215 c.testtimer [pool-1-thread-1] - running... 21:45:47.215 c.testtimer [pool-1-thread-1] - running... scheduleatfixedrate 例子(任务执行时间超过了间隔时间):\nscheduledexecutorservice pool = executors.newscheduledthreadpool(1); log.debug(\u0026#34;start...\u0026#34;); pool.scheduleatfixedrate(() -\u0026gt; { log.debug(\u0026#34;running...\u0026#34;); sleep(2); }, 1, 1, timeunit.seconds); 输出分析:一开始,延时 1s,接下来,由于任务执行时间 \u0026gt; 间隔时间,间隔被『撑』到了 2s\n21:44:30.311 c.testtimer [main] - start... 21:44:31.360 c.testtimer [pool-1-thread-1] - running... 21:44:33.361 c.testtimer [pool-1-thread-1] - running... 21:44:35.362 c.testtimer [pool-1-thread-1] - running... 21:44:37.362 c.testtimer [pool-1-thread-1] - running... schedulewithfixeddelay 例子:\nscheduledexecutorservice pool = executors.newscheduledthreadpool(1); log.debug(\u0026#34;start...\u0026#34;); pool.schedulewithfixeddelay(()-\u0026gt; { log.debug(\u0026#34;running...\u0026#34;); sleep(2); }, 1, 1, timeunit.seconds); 输出分析:一开始,延时 1s,schedulewithfixeddelay 的间隔是 上一个任务结束 \u0026lt;-\u0026gt; 延时 \u0026lt;-\u0026gt; 下一个任务开始 所 以间隔都是 3s\n21:40:55.078 c.testtimer [main] - start... 21:40:56.140 c.testtimer [pool-1-thread-1] - running... 21:40:59.143 c.testtimer [pool-1-thread-1] - running... 21:41:02.145 c.testtimer [pool-1-thread-1] - running... 21:41:05.147 c.testtimer [pool-1-thread-1] - running... 评价 整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线 程也不会被释放。用来执行延迟或反复执行的任务\nfork/join 概念\nfork/join 是 jdk 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型 运算\n所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计 算,如归并排序、斐波那契数列、都可以用分治思想进行求解\nfork/join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运 算效率\nfork/join 默认会创建与 cpu 核心数大小相同的线程池\n应用之求和\n提交给 fork/join 线程池的任务需要继承 recursivetask(有返回值)或 recursiveaction(没有返回值),例如下 面定义了一个对 1~n 之间的整数求和的任务\n@slf4j(topic = \u0026#34;c.addtask\u0026#34;) class addtask1 extends recursivetask\u0026lt;integer\u0026gt; { int n; public addtask1(int n) { this.n = n; } @override public string tostring() { return \u0026#34;{\u0026#34; + n + \u0026#39;}\u0026#39;; } @override protected integer compute() { // 如果 n 已经为 1,可以求得结果了 if (n == 1) { log.debug(\u0026#34;join() {}\u0026#34;, n); return n; } // 将任务进行拆分(fork) addtask1 t1 = new addtask1(n - 1); t1.fork(); // 另一个线程执行拆分 log.debug(\u0026#34;fork() {} + {}\u0026#34;, n, t1); // 合并(join)结果 int result = n + t1.join(); log.debug(\u0026#34;join() {} + {} = {}\u0026#34;, n, t1, result); return result; } } 然后提交给 forkjoinpool 来执行\npublic static void main(string[] args) { forkjoinpool pool = new forkjoinpool(4); system.out.println(pool.invoke(new addtask1(5))); } 结果\n[forkjoinpool-1-worker-0] - fork() 2 + {1} [forkjoinpool-1-worker-1] - fork() 5 + {4} [forkjoinpool-1-worker-0] - join() 1 [forkjoinpool-1-worker-0] - join() 2 + {1} = 3 [forkjoinpool-1-worker-2] - fork() 4 + {3} [forkjoinpool-1-worker-3] - fork() 3 + {2} [forkjoinpool-1-worker-3] - join() 3 + {2} = 6 [forkjoinpool-1-worker-2] - join() 4 + {3} = 10 [forkjoinpool-1-worker-1] - join() 5 + {4} = 15 15 用图来表示\n改进\nclass addtask3 extends recursivetask\u0026lt;integer\u0026gt; { int begin; int end; public addtask3(int begin, int end) { this.begin = begin; this.end = end; } @override public string tostring() { return \u0026#34;{\u0026#34; + begin + \u0026#34;,\u0026#34; + end + \u0026#39;}\u0026#39;; } @override protected integer compute() { // 5, 5 if (begin == end) { log.debug(\u0026#34;join() {}\u0026#34;, begin); return begin; } // 4, 5 if (end - begin == 1) { log.debug(\u0026#34;join() {} + {} = {}\u0026#34;, begin, end, end + begin); return end + begin; } // 1 5 int mid = (end + begin) / 2; // 3 addtask3 t1 = new addtask3(begin, mid); // 1,3 t1.fork(); addtask3 t2 = new addtask3(mid + 1, end); // 4,5 t2.fork(); log.debug(\u0026#34;fork() {} + {} = ?\u0026#34;, t1, t2); int result = t1.join() + t2.join(); log.debug(\u0026#34;join() {} + {} = {}\u0026#34;, t1, t2, result); return result; } } 然后提交给 forkjoinpool 来执行\npublic static void main(string[] args) { forkjoinpool pool = new forkjoinpool(4); system.out.println(pool.invoke(new addtask3(1, 10))); } 结果\n[forkjoinpool-1-worker-0] - join() 1 + 2 = 3 [forkjoinpool-1-worker-3] - join() 4 + 5 = 9 [forkjoinpool-1-worker-0] - join() 3 [forkjoinpool-1-worker-1] - fork() {1,3} + {4,5} = ? [forkjoinpool-1-worker-2] - fork() {1,2} + {3,3} = ? [forkjoinpool-1-worker-2] - join() {1,2} + {3,3} = 6 [forkjoinpool-1-worker-1] - join() {1,3} + {4,5} = 15 15 用图来表示\naqs 原理 概述 全称是 abstractqueuedsynchronizer,是阻塞式锁和相关的同步器工具的框架\n特点:\n用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取 锁和释放锁 getstate - 获取 state 状态 setstate - 设置 state 状态 compareandsetstate - cas 机制设置 state 状态 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源 提供了基于 fifo 的等待队列,类似于 monitor 的 entrylist 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 monitor 的 waitset 子类主要实现这样一些方法(默认抛出 unsupportedoperationexception)\ntryacquire tryrelease tryacquireshared tryreleaseshared isheldexclusively 获取锁的姿势\n// 如果获取锁失败 if (!tryacquire(arg)) { // 入队, 可以选择阻塞当前线程 park unpark } 释放锁的姿势\n// 如果释放锁成功 if (tryrelease(arg)) { // 让阻塞线程恢复运行 } 实现不可重入锁 自定义同步器\nfinal class mysync extends abstractqueuedsynchronizer { @override protected boolean tryacquire(int acquires) { if (acquires == 1){ if (compareandsetstate(0, 1)) { setexclusiveownerthread(thread.currentthread()); return true; } } return false; } @override protected boolean tryrelease(int acquires) { if(acquires == 1) { if(getstate() == 0) { throw new illegalmonitorstateexception(); } setexclusiveownerthread(null); setstate(0); return true; } return false; } protected condition newcondition() { return new conditionobject(); } @override protected boolean isheldexclusively() { return getstate() == 1; } } 自定义锁\n有了自定义同步器,很容易复用 aqs ,实现一个功能完备的自定义锁\nclass mylock implements lock { static mysync sync = new mysync(); @override // 尝试,不成功,进入等待队列 public void lock() { sync.acquire(1); } @override // 尝试,不成功,进入等待队列,可打断 public void lockinterruptibly() throws interruptedexception { sync.acquireinterruptibly(1); } @override // 尝试一次,不成功返回,不进入队列 public boolean trylock() { return sync.tryacquire(1); } @override // 尝试,不成功,进入等待队列,有时限 public boolean trylock(long time, timeunit unit) throws interruptedexception { return sync.tryacquirenanos(1, unit.tonanos(time)); } @override // 释放锁 public void unlock() { sync.release(1); } @override // 生成条件变量 public condition newcondition() { return sync.newcondition(); } } 测试一下\nmylock lock = new mylock(); new thread(() -\u0026gt; { lock.lock(); try { log.debug(\u0026#34;locking...\u0026#34;); sleep(1); } finally { log.debug(\u0026#34;unlocking...\u0026#34;); lock.unlock(); } },\u0026#34;t1\u0026#34;).start(); new thread(() -\u0026gt; { lock.lock(); try { log.debug(\u0026#34;locking...\u0026#34;); } finally { log.debug(\u0026#34;unlocking...\u0026#34;); lock.unlock(); } },\u0026#34;t2\u0026#34;).start(); 输出\n22:29:28.727 c.testaqs [t1] - locking... 22:29:29.732 c.testaqs [t1] - unlocking... 22:29:29.732 c.testaqs [t2] - locking... 22:29:29.732 c.testaqs [t2] - unlocking... 不可重入测试\n如果改为下面代码,会发现自己也会被挡住(只会打印一次 locking)\nlock.lock(); log.debug(\u0026#34;locking...\u0026#34;); lock.lock(); log.debug(\u0026#34;locking...\u0026#34;); 心得 起源\n早期程序员会自己通过一种同步器去实现另一种相近的同步器,例如用可重入锁去实现信号量,或反之。这显然不 够优雅,于是在 jsr166(java 规范提案)中创建了 aqs,提供了这种通用的同步器机制。\n目标\naqs 要实现的功能目标\n阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryacquire 获取锁超时机制 通过打断取消机制 独占机制及共享机制 条件不满足时的等待机制 设计 aqs 的基本思想其实很简单\n获取锁的逻辑\nwhile(state 状态不允许获取) { if(队列中还没有此线程) { 入队并阻塞 } } 当前线程出队 释放锁的逻辑\nif(state 状态允许了) { 恢复阻塞的线程(s) } 要点\n原子维护 state 状态 阻塞及恢复线程 维护队列 state 设计\nstate 使用 volatile 配合 cas 保证其修改时的原子性 state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想 阻塞恢复设计 早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume 那么 suspend 将感知不到 解决方法是使用 park \u0026amp; unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark 再 park 也没 问题 park \u0026amp; unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细 park 线程还可以通过 interrupt 打断 队列设计 使用了 fifo 先入先出队列,并不支持优先级队列 设计时借鉴了 clh 队列,它是一种单向无锁队列 队列中有 head 和 tail 两个指针节点,都用 volatile 修饰配合 cas 使用,每个节点有 state 维护节点状态 入队伪代码,只需要考虑 tail 赋值的原子性\ndo { // 原来的 tail node prev = tail; // 用 cas 在原来 tail 的基础上改为 node } while(tail.compareandset(prev, node)) 出队伪代码\n// prev 是上一个节点 while((node prev=node.prev).state != 唤醒状态) { } // 设置头节点 head = node; clh 好处:\n无锁,使用自旋 快速,无阻塞 aqs 在一些方面改进了 clh\nprivate node enq(final node node) { for (;;) { node t = tail; // 队列中还没有元素 tail 为 null if (t == null) { // 将 head 从 null -\u0026gt; dummy if (compareandsethead(new node())) tail = head; } else { // 将 node 的 prev 设置为原来的 tail node.prev = t; // 将 tail 从原来的 tail 设置为 node if (compareandsettail(t, node)) { // 原来 tail 的 next 设置为 node t.next = node; return t; } } } } 主要用到 aqs 的并发工具类 reentrantlock 原理 非公平锁实现原理 加锁解锁流程 先从构造器开始看,默认为非公平锁实现\npublic reentrantlock() { sync = new nonfairsync(); } nonfairsync 继承自 aqs 没有竞争时\n第一个竞争出现时\nthread-1 执行了\ncas 尝试将 state 由 0 改为 1,结果失败 进入 tryacquire 逻辑,这时 state 已经是1,结果仍然失败 接下来进入 addwaiter 逻辑,构造 node 队列 图中黄色三角表示该 node 的 waitstatus 状态,其中 0 为默认正常状态 node 的创建是懒惰的 其中第一个 node 称为 dummy(哑元)或哨兵,用来占位,并不关联线程 当前线程进入 acquirequeued 逻辑\nacquirequeued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞 如果自己是紧邻着 head(排第二位),那么再次 tryacquire 尝试获取锁,当然这时 state 仍为 1,失败 进入 shouldparkafterfailedacquire 逻辑,将前驱 node,即 head 的 waitstatus 改为 -1,这次返回 false shouldparkafterfailedacquire 执行完毕回到 acquirequeued ,再次 tryacquire 尝试获取锁,当然这时 state 仍为 1,失败 当再次进入 shouldparkafterfailedacquire 时,这时因为其前驱 node 的 waitstatus 已经是 -1,这次返回 true 进入 parkandcheckinterrupt, thread-1 park(灰色表示) 再次有多个线程经历上述过程竞争失败,变成这个样子\nthread-0 释放锁,进入 tryrelease 流程,如果成功\n设置 exclusiveownerthread 为 null state = 0 当前队列不为 null,并且 head 的 waitstatus = -1,进入 unparksuccessor 流程\n找到队列中离 head 最近的一个 node(没取消的),unpark 恢复其运行,本例中即为 thread-1\n回到 thread-1 的 acquirequeued 流程\n如果加锁成功(没有竞争),会设置\nexclusiveownerthread 为 thread-1,state = 1 head 指向刚刚 thread-1 所在的 node,该 node 清空 thread 原本的 head 因为从链表断开,而可被垃圾回收 如果这时候有其它线程来竞争(非公平的体现),例如这时有 thread-4 来了\n如果不巧又被 thread-4 占了先\nthread-4 被设置为 exclusiveownerthread,state = 1 thread-1 再次进入 acquirequeued 流程,获取锁失败,重新进入 park 阻塞 可重入原理 当持有锁的线程再次尝试获取锁时,会将state的值加1,state表示锁的重入量。\nstatic final class nonfairsync extends sync { // ... // sync 继承过来的方法, 方便阅读, 放在此处 final boolean nonfairtryacquire(int acquires) { final thread current = thread.currentthread(); int c = getstate(); if (c == 0) { if (compareandsetstate(0, acquires)) { setexclusiveownerthread(current); return true; } } // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入 else if (current == getexclusiveownerthread()) { // state++ int nextc = c + acquires; if (nextc \u0026lt; 0) // overflow throw new error(\u0026#34;maximum lock count exceeded\u0026#34;); setstate(nextc); return true; } return false; } // sync 继承过来的方法, 方便阅读, 放在此处 protected final boolean tryrelease(int releases) { // state-- int c = getstate() - releases; if (thread.currentthread() != getexclusiveownerthread()) throw new illegalmonitorstateexception(); boolean free = false; // 支持锁重入, 只有 state 减为 0, 才释放成功 if (c == 0) { free = true; setexclusiveownerthread(null); } setstate(c); return free; } } 可打断原理 不可打断模式\n在此模式下,即使它被打断,仍会驻留在 aqs 队列中,并将打断信号存储在一个interrupt变量中。一直要等到获得锁后方能得知自己被打断了,并且调用selfinterrupt方法打断自己。\n// sync 继承自 aqs static final class nonfairsync extends sync { // ... private final boolean parkandcheckinterrupt() { // 如果打断标记已经是 true, 则 park 会失效 locksupport.park(this); // interrupted 会清除打断标记 return thread.interrupted(); } final boolean acquirequeued(final node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final node p = node.predecessor(); if (p == head \u0026amp;\u0026amp; tryacquire(arg)) { sethead(node); p.next = null; failed = false; // 还是需要获得锁后, 才能返回打断状态 return interrupted; } if ( shouldparkafterfailedacquire(p, node) \u0026amp;\u0026amp; parkandcheckinterrupt() ) { // 如果是因为 interrupt 被唤醒, 返回打断状态为 true //仅仅将打断标记设置为true,线程仍然会进行循环,再次进入阻塞状态 interrupted = true; } } } finally { if (failed) cancelacquire(node); } } public final void acquire(int arg) { if ( !tryacquire(arg) \u0026amp;\u0026amp; acquirequeued(addwaiter(node.exclusive), arg) ) { // 如果打断状态为 true selfinterrupt(); } } //响应打断标记,打断自己 static void selfinterrupt() { // 重新产生一次中断 thread.currentthread().interrupt(); } } 可打断模式\n此模式下即使线程在等待队列中等待,一旦被打断,就会立刻抛出打断异常。\nstatic final class nonfairsync extends sync { public final void acquireinterruptibly(int arg) throws interruptedexception { if (thread.interrupted()) throw new interruptedexception(); // 如果没有获得到锁, 进入 ㈠ if (!tryacquire(arg)) doacquireinterruptibly(arg); } // ㈠ 可打断的获取锁流程 private void doacquireinterruptibly(int arg) throws interruptedexception { final node node = addwaiter(node.exclusive); boolean failed = true; try { for (;;) { final node p = node.predecessor(); if (p == head \u0026amp;\u0026amp; tryacquire(arg)) { sethead(node); p.next = null; // help gc failed = false; return; } if (shouldparkafterfailedacquire(p, node) \u0026amp;\u0026amp; parkandcheckinterrupt()) { // 在 park 过程中如果被 interrupt 会进入此 // 这时候抛出异常, 而不会再次进入 for (;;) throw new interruptedexception(); } } } finally { if (failed) cancelacquire(node); } } } 公平锁实现原理 简而言之,公平与非公平的区别在于,公平锁中的tryacquire方法被重写了,新来的线程即便得知了锁的state为0,也要先判断等待队列中是否还有线程等待,只有当队列没有线程等待式,才获得锁。\nstatic final class fairsync extends sync { private static final long serialversionuid = -3000897897090466540l; final void lock() { acquire(1); } // aqs 继承过来的方法, 方便阅读, 放在此处 public final void acquire(int arg) { if ( !tryacquire(arg) \u0026amp;\u0026amp; acquirequeued(addwaiter(node.exclusive), arg) ) { selfinterrupt(); } } // 与非公平锁主要区别在于 tryacquire 方法的实现 protected final boolean tryacquire(int acquires) { final thread current = thread.currentthread(); int c = getstate(); if (c == 0) { // 先检查 aqs 队列中是否有前驱节点, 没有才去竞争 if (!hasqueuedpredecessors() \u0026amp;\u0026amp; compareandsetstate(0, acquires)) { setexclusiveownerthread(current); return true; } } else if (current == getexclusiveownerthread()) { int nextc = c + acquires; if (nextc \u0026lt; 0) throw new error(\u0026#34;maximum lock count exceeded\u0026#34;); setstate(nextc); return true; } return false; } // ㈠ aqs 继承过来的方法, 方便阅读, 放在此处 //存疑 public final boolean hasqueuedpredecessors() { node t = tail; node h = head; node s; // h != t 时表示队列中有 node return h != t \u0026amp;\u0026amp; ( // (s = h.next) == null 表示队列中还有没有老二 (s = h.next) == null || // 或者队列中老二线程不是此线程 s.thread != thread.currentthread() ); } } 条件变量实现原理 每个条件变量其实就对应着一个等待队列,其实现类是 conditionobject\nawait 流程 开始 thread-0 持有锁,调用 await,进入 conditionobject 的 addconditionwaiter 流程\n创建新的 node 状态为 -2(node.condition),关联 thread-0,加入等待队列尾部\n接下来进入 aqs 的 fullyrelease 流程,释放同步器上的锁\nunpark aqs 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 thread-1 竞争成功\npark 阻塞 thread-0\n总结:\n创建一个节点,关联当前线程,并插入到当前condition队列的尾部 调用fullrelease,完全释放同步器中的锁,并记录当前线程的锁重入数 唤醒(park)aqs队列中的第一个线程 调用park方法,阻塞当前线程。 signal 流程 假设 thread-1 要来唤醒 thread-0\n进入 conditionobject 的 dosignal 流程,取得等待队列中第一个 node,即 thread-0 所在 node\n执行 transferforsignal 流程,将该 node 加入 aqs 队列尾部,将 thread-0 的 waitstatus 改为 0,thread-3 的 waitstatus 改为 -1\nthread-1 释放锁,进入 unlock 流程,略\n总结:\n当前持有锁的线程唤醒等待队列中的线程,调用dosignal或dosignalall方法,将等待队列中的第一个(或全部)节点插入到aqs队列中的尾部。 将插入的节点的状态从condition设置为0,将插入节点的前一个节点的状态设置为-1(意味着要承担唤醒后一个节点的责任) 当前线程释放锁,parkaqs队列中的第一个节点线程。 读写锁 reentrantreadwritelock 当读操作远远高于写操作时,这时候使用读写锁让读-读可以并发,提高性能。 类似于数据库中的select ... from ... lock in share mode\n提供一个数据容器类内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法\n注意事项\n读锁不支持条件变量 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待 r.lock(); try { // ... w.lock(); try { // ... } finally{ w.unlock(); } } finally{ r.unlock(); } 重入时降级支持:即持有写锁的情况下去获取读锁 class cacheddata { object data; // 是否有效,如果失效,需要重新计算 data volatile boolean cachevalid; final reentrantreadwritelock rwl = new reentrantreadwritelock(); void processcacheddata() { rwl.readlock().lock(); if (!cachevalid) { // 获取写锁前必须释放读锁 rwl.readlock().unlock(); rwl.writelock().lock(); try { // 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新 if (!cachevalid) { data = ... cachevalid = true; } // 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存 rwl.readlock().lock(); } finally { rwl.writelock().unlock(); } } // 自己用完数据, 释放读锁 try { use(data); } finally { rwl.readlock().unlock(); } } } 缓存更新策略 更新时,是先清缓存还是先更新数据库\n先清缓存\n先更新数据库(支持)\n补充一种情况,假设查询线程 a 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询\n这种情况的出现几率非常小\n读写锁原理 图解流程 读写锁用的是同一个 sycn 同步器,因此等待队列、state 等也是同一个\nt1 w.lock,t2 r.lock\n1) t1 成功上锁,流程与 reentrantlock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁 使用的是 state 的高 16 位\n2)t2 执行 r.lock,这时进入读锁的 sync.acquireshared(1) 流程,首先会进入 tryacquireshared 流程。如果有写锁占据,那么 tryacquireshared 返回 -1 表示失败\ntryacquireshared 返回值表示\n-1 表示失败 0 表示成功,但后继节点不会继续唤醒 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1 3)这时会进入 sync.doacquireshared(1) 流程,首先也是调用 addwaiter 添加节点,不同之处在于节点被设置为 node.shared 模式而非 node.exclusive 模式,注意此时 t2 仍处于活跃状态\n4)t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryacquireshared(1) 来尝试获取锁\n5)如果没有成功,在 doacquireshared 内 for (;;) 循环一次,把前驱节点的 waitstatus 改为 -1,再 for (;;) 循环一 次尝试 tryacquireshared(1) 如果还不成功,那么在 parkandcheckinterrupt() 处 park\nt3 r.lock,t4 w.lock\n这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子\nt1 w.unlock\n这时会走到写锁的 sync.release(1) 流程,调用 sync.tryrelease(1) 成功,变成下面的样子\n接下来执行唤醒流程 sync.unparksuccessor,即让老二恢复运行,这时 t2 在 doacquireshared 内 parkandcheckinterrupt() 处恢复运行\n这回再来一次 for (;;) 执行 tryacquireshared 成功则让读锁计数加一\n这时 t2 已经恢复运行,接下来 t2 调用 setheadandpropagate(node, 1),它原本所在节点被置为头节点\n事情还没完,在 setheadandpropagate 方法内还会检查下一个节点是否是 shared,如果是则调用 doreleaseshared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doacquireshared 内 parkandcheckinterrupt() 处恢复运行\n这回再来一次 for (;;) 执行 tryacquireshared 成功则让读锁计数加一\n这时 t3 已经恢复运行,接下来 t3 调用 setheadandpropagate(node, 1),它原本所在节点被置为头节点\n下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点\nt2 r.unlock,t3 r.unlock\nt2 进入 sync.releaseshared(1) 中,调用 tryreleaseshared(1) 让计数减一,但由于计数还不为零\nt3 进入 sync.releaseshared(1) 中,调用 tryreleaseshared(1) 让计数减一,这回计数为零了,进入 doreleaseshared() 将头节点从 -1 改为 0 并唤醒老二,即\n之后 t4 在 acquirequeued 中 parkandcheckinterrupt 处恢复运行,再次 for (;;) 这次自己是老二,并且没有其他 竞争,tryacquire(1) 成功,修改头结点,流程结束\nstampedlock 该类自 jdk 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用 加解读锁\nlong stamp = lock.readlock(); lock.unlockread(stamp); 加解写锁\nlong stamp = lock.writelock(); lock.unlockwrite(stamp); 乐观读,stampedlock 支持 tryoptimisticread() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通 过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。\nlong stamp = lock.tryoptimisticread(); // 验戳 if(!lock.validate(stamp)){ // 锁升级 } 提供一个数据容器类内部分别使用读锁保护数据的read()方法,写锁保护数据的write()方法\nclass datacontainerstamped { private int data; private final stampedlock lock = new stampedlock(); public datacontainerstamped(int data) { this.data = data; } public int read(int readtime) { //获取戳 long stamp = lock.tryoptimisticread(); log.debug(\u0026#34;optimistic read locking...{}\u0026#34;, stamp); //读取数据 sleep(readtime); //读取数据之后再验戳 if (lock.validate(stamp)) { log.debug(\u0026#34;read finish...{}, data:{}\u0026#34;, stamp, data); return data; } //如果验戳失败,说明已经数据已经被修改,需要升级锁重新读。 // 锁升级 - 读锁 log.debug(\u0026#34;updating to read lock... {}\u0026#34;, stamp); try { stamp = lock.readlock(); log.debug(\u0026#34;read lock {}\u0026#34;, stamp); sleep(readtime); log.debug(\u0026#34;read finish...{}, data:{}\u0026#34;, stamp, data); return data; } finally { log.debug(\u0026#34;read unlock {}\u0026#34;, stamp); lock.unlockread(stamp); } } public void write(int newdata) { long stamp = lock.writelock(); log.debug(\u0026#34;write lock {}\u0026#34;, stamp); try { sleep(2); this.data = newdata; } finally { log.debug(\u0026#34;write unlock {}\u0026#34;, stamp); lock.unlockwrite(stamp); } } } 测试读-读可以优化\npublic static void main(string[] args) { datacontainerstamped datacontainer = new datacontainerstamped(1); new thread(() -\u0026gt; { datacontainer.read(1); }, \u0026#34;t1\u0026#34;).start(); sleep(0.5); new thread(() -\u0026gt; { datacontainer.read(0); }, \u0026#34;t2\u0026#34;).start(); } 输出结果,可以看到实际没有加读锁\n15:58:50.217 c.datacontainerstamped [t1] - optimistic read locking...256 15:58:50.717 c.datacontainerstamped [t2] - optimistic read locking...256 15:58:50.717 c.datacontainerstamped [t2] - read finish...256, data:1 15:58:51.220 c.datacontainerstamped [t1] - read finish...256, data:1 测试读-写时优化读补加读锁\npublic static void main(string[] args) { datacontainerstamped datacontainer = new datacontainerstamped(1); new thread(() -\u0026gt; { datacontainer.read(1); }, \u0026#34;t1\u0026#34;).start(); sleep(0.5); new thread(() -\u0026gt; { datacontainer.write(100); }, \u0026#34;t2\u0026#34;).start(); } 输出结果\n15:57:00.219 c.datacontainerstamped [t1] - optimistic read locking...256 15:57:00.717 c.datacontainerstamped [t2] - write lock 384 15:57:01.225 c.datacontainerstamped [t1] - updating to read lock... 256 15:57:02.719 c.datacontainerstamped [t2] - write unlock 384 15:57:02.719 c.datacontainerstamped [t1] - read lock 513 15:57:03.719 c.datacontainerstamped [t1] - read finish...513, data:1000 15:57:03.719 c.datacontainerstamped [t1] - read unlock 513 注意\nstampedlock 不支持条件变量 stampedlock 不支持可重入 semaphore 基本使用 信号量,用来限制能同时访问共享资源的线程上限。\npublic static void main(string[] args) { // 1. 创建 semaphore 对象 semaphore semaphore = new semaphore(3); // 2. 10个线程同时运行 for (int i = 0; i \u0026lt; 10; i++) { new thread(() -\u0026gt; { // 3. 获取许可 try { semaphore.acquire(); //对于非打断式获取,如果此过程中被打断,线程依旧会等到获取了信号量之后才进入catch块。 //catch块中的线程依旧持有信号量,捕获该异常后catch块可以不做任何处理。 } catch (interruptedexception e) { e.printstacktrace(); } try { log.debug(\u0026#34;running...\u0026#34;); sleep(1); log.debug(\u0026#34;end...\u0026#34;); } finally { // 4. 释放许可 semaphore.release(); } }).start(); } } 输出\n07:35:15.485 c.testsemaphore [thread-2] - running... 07:35:15.485 c.testsemaphore [thread-1] - running... 07:35:15.485 c.testsemaphore [thread-0] - running... 07:35:16.490 c.testsemaphore [thread-2] - end... 07:35:16.490 c.testsemaphore [thread-0] - end... 07:35:16.490 c.testsemaphore [thread-1] - end... 07:35:16.490 c.testsemaphore [thread-3] - running... 07:35:16.490 c.testsemaphore [thread-5] - running... 07:35:16.490 c.testsemaphore [thread-4] - running... 07:35:17.490 c.testsemaphore [thread-5] - end... 07:35:17.490 c.testsemaphore [thread-4] - end... 07:35:17.490 c.testsemaphore [thread-3] - end... 07:35:17.490 c.testsemaphore [thread-6] - running... 07:35:17.490 c.testsemaphore [thread-7] - running... 07:35:17.490 c.testsemaphore [thread-9] - running... 07:35:18.491 c.testsemaphore [thread-6] - end... 07:35:18.491 c.testsemaphore [thread-7] - end... 07:35:18.491 c.testsemaphore [thread-9] - end... 07:35:18.491 c.testsemaphore [thread-8] - running... 07:35:19.492 c.testsemaphore [thread-8] - end... 说明:\nsemaphore有两个构造器:semaphore(int permits)和semaphore(int permits,boolean fair) permits表示允许同时访问共享资源的线程数。 fair表示公平与否,与之前的reentrantlock一样。 原理 加锁解锁流程\nsemaphore有点像一个停车场,permits就好像停车位数量,当线程获得了permits就像是获得了停车位,然后停车场显示空余车位减一。\n刚开始,permits(state)为 3,这时 5 个线程来获取资源\n假设其中 thread-1,thread-2,thread-4 cas 竞争成功,而 thread-0 和 thread-3 竞争失败,进入 aqs 队列 park 阻塞\n这时 thread-4 释放了 permits,状态如下\n接下来 thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接 下来的 thread-3 节点,但由于 permits 是 0,因此 thread-3 在尝试不成功后再次进入 park 状态\n加锁流程总结:\nacquire-\u0026gt;acquiresharedinterruptibly(1)-\u0026gt;tryacquireshared(1)-\u0026gt;nonfairtryacquireshared(1),如果资源用完了,返回负数,tryacquireshared返回负数,表示失败。否则返回正数,tryacquireshared返回正数,表示成功。 如果成功,获取信号量成功。 如果失败,调用doacquiresharedinterruptibly,进入for循环: 如果当前驱节点为头节点,调用tryacquireshared尝试获取锁 如果结果大于等于0,表明获取锁成功,调用setheadandpropagate,将当前节点设为头节点,之后又调用doreleaseshared,唤醒后继节点。 调用shoudparkafterfailure,第一次调用返回false,并将前驱节点改为-1,第二次循环如果再进入此方法,会进入阻塞并检查打断的方法。 解锁流程总结:\nrelease-\u0026gt;sync.releaseshared(1)-\u0026gt;tryreleaseshared(1),只要不发生整数溢出,就返回true 如果返回true,调用doreleaseshared,唤醒后继节点。 如果返回false,解锁失败。 countdownlatch 用来进行线程同步协作,等待所有线程完成倒计时。\n其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countdown() 用来让计数减一\npublic static void main(string[] args) throws interruptedexception { countdownlatch latch = new countdownlatch(3); new thread(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); sleep(1); latch.countdown(); log.debug(\u0026#34;end...{}\u0026#34;, latch.getcount()); }).start(); new thread(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); sleep(2); latch.countdown(); log.debug(\u0026#34;end...{}\u0026#34;, latch.getcount()); }).start(); new thread(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); sleep(1.5); latch.countdown(); log.debug(\u0026#34;end...{}\u0026#34;, latch.getcount()); }).start(); log.debug(\u0026#34;waiting...\u0026#34;); latch.await(); log.debug(\u0026#34;wait end...\u0026#34;); } 输出\n18:44:00.778 c.testcountdownlatch [main] - waiting... 18:44:00.778 c.testcountdownlatch [thread-2] - begin... 18:44:00.778 c.testcountdownlatch [thread-0] - begin... 18:44:00.778 c.testcountdownlatch [thread-1] - begin... 18:44:01.782 c.testcountdownlatch [thread-0] - end...2 18:44:02.283 c.testcountdownlatch [thread-2] - end...1 18:44:02.782 c.testcountdownlatch [thread-1] - end...0 18:44:02.782 c.testcountdownlatch [main] - wait end... 相比于join,countdownlatch能配合线程池使用。\npublic static void main(string[] args) throws interruptedexception { countdownlatch latch = new countdownlatch(3); executorservice service = executors.newfixedthreadpool(4); service.submit(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); sleep(1); latch.countdown(); log.debug(\u0026#34;end...{}\u0026#34;, latch.getcount()); }); service.submit(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); sleep(1.5); latch.countdown(); log.debug(\u0026#34;end...{}\u0026#34;, latch.getcount()); }); service.submit(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); sleep(2); latch.countdown(); log.debug(\u0026#34;end...{}\u0026#34;, latch.getcount()); }); service.submit(()-\u0026gt;{ try { log.debug(\u0026#34;waiting...\u0026#34;); latch.await(); log.debug(\u0026#34;wait end...\u0026#34;); } catch (interruptedexception e) { e.printstacktrace(); } }); } cyclicbarrier countdownlatch的缺点在于不能重用,见下:\nprivate static void test1() { executorservice service = executors.newfixedthreadpool(5); for (int i = 0; i \u0026lt; 3; i++) { countdownlatch latch = new countdownlatch(2); service.submit(() -\u0026gt; { log.debug(\u0026#34;task1 start...\u0026#34;); sleep(1); latch.countdown(); }); service.submit(() -\u0026gt; { log.debug(\u0026#34;task2 start...\u0026#34;); sleep(2); latch.countdown(); }); try { latch.await(); } catch (interruptedexception e) { e.printstacktrace(); } log.debug(\u0026#34;task1 task2 finish...\u0026#34;); } service.shutdown(); } 想要重复使用countdownlatch进行同步,必须创建多个countdownlatch对象。\n[ˈsaɪklɪk ˈbæriɚ] 循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执 行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行\npublic static void main(string[] args) { executorservice pool = executors.newfixedthreadpool(2); cyclicbarrier barrier = new cyclicbarrier(2,()-\u0026gt;{ log.debug(\u0026#34;finish 1 2\u0026#34;); }); for (int i = 0; i \u0026lt; 3; i++) { pool.submit(()-\u0026gt;{ log.debug(\u0026#34;begin 1\u0026#34;); try { thread.sleep(1000); barrier.await(); } catch (interruptedexception | brokenbarrierexception e) { e.printstacktrace(); } }); pool.submit(()-\u0026gt;{ log.debug(\u0026#34;begin 2\u0026#34;); try { thread.sleep(2000); barrier.await(); } catch (interruptedexception | brokenbarrierexception e) { e.printstacktrace(); } }); } pool.shutdown(); } 16:03:01 [pool-2-thread-1] c.t4 - begin 1 16:03:01 [pool-2-thread-2] c.t4 - begin 2 16:03:03 [pool-2-thread-2] c.t4 - finish 1 2 16:03:03 [pool-2-thread-1] c.t4 - begin 1 16:03:03 [pool-2-thread-2] c.t4 - begin 2 16:03:05 [pool-2-thread-2] c.t4 - finish 1 2 16:03:05 [pool-2-thread-1] c.t4 - begin 2 16:03:05 [pool-2-thread-2] c.t4 - begin 1 16:03:07 [pool-2-thread-1] c.t4 - finish 1 2 注意\ncyclicbarrier 与 countdownlatch 的主要区别在于 cyclicbarrier 是可以重用的 cyclicbarrier 可以被比 喻为『人满发车』 countdownlatch的计数和阻塞方法是分开的两个方法,而cyclicbarrier是一个方法。 cyclicbarrier的构造器还有一个runnable类型的参数,在计数为0时会执行其中的run方法。 线程安全集合类概述 线程安全集合类可以分为三大类:\n遗留的线程安全集合如hashtable,vector 使用collections装饰的线程安全集合,如: collections.synchronizedcollection collections.synchronizedlist collections.synchronizedmap collections.synchronizedset collections.synchronizednavigablemap collections.synchronizednavigableset collections.synchronizedsortedmap collections.synchronizedsortedset 说明:以上集合均采用修饰模式设计,将非线程安全的集合包装后,在调用方法时包裹了一层synchronized代码块。其并发性并不比遗留的安全集合好。 java.util.concurrent.* 重点介绍java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词: blocking、copyonwrite、concurrent\nblocking 大部分实现基于锁,并提供用来阻塞的方法 copyonwrite 之类容器修改开销相对较重 concurrent 类型的容器 内部很多操作使用 cas 优化,一般可以提供较高吞吐量 弱一致性 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍 历,这时内容是旧的 求大小弱一致性,size 操作未必是 100% 准确 读取弱一致性 遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出 concurrentmodificationexception,不再继续遍历\nconcurrenthashmap 应用之单词计数 搭建练习环境:\npublic class test { public static void main(string[] args){ //在main方法中实现两个接口 } //开启26个线程,每个线程调用get方法获取map,从对应的文件读取单词并存储到list中,最后调用accept方法进行统计。 public static \u0026lt;v\u0026gt; void calculate(supplier\u0026lt;map\u0026lt;string,v\u0026gt;\u0026gt; supplier, biconsumer\u0026lt;map\u0026lt;string,v\u0026gt;, list\u0026lt;string\u0026gt;\u0026gt; consumer) { map\u0026lt;string, v\u0026gt; map = supplier.get(); countdownlatch count = new countdownlatch(26); for (int i = 1; i \u0026lt; 27; i++) { int k = i; new thread(()-\u0026gt;{ arraylist\u0026lt;string\u0026gt; list = new arraylist\u0026lt;\u0026gt;(); read(list,k); consumer.accept(map,list); count.countdown(); }).start(); } try { count.await(); } catch (interruptedexception e) { e.printstacktrace(); } system.out.println(map.tostring()); } //读单词方法的实现 public static void read(list\u0026lt;string\u0026gt; list,int i){ try{ string element; bufferedreader reader = new bufferedreader(new filereader(i + \u0026#34;.txt\u0026#34;)); while((element = reader.readline()) != null){ list.add(element); } }catch (ioexception e){ } } //生成测试数据 public void construct(){ string str = \u0026#34;abcdefghijklmnopqrstuvwxyz\u0026#34;; arraylist\u0026lt;string\u0026gt; list = new arraylist\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; str.length(); i++) { for (int j = 0; j \u0026lt; 200; j++) { list.add(string.valueof(str.charat(i))); } } collections.shuffle(list); for (int i = 0; i \u0026lt; 26; i++) { try (printwriter out = new printwriter(new filewriter(i + 1 + \u0026#34;.txt\u0026#34;))) { string collect = list.sublist(i * 200, (i + 1) * 200).stream().collect(collectors.joining(\u0026#34;\\n\u0026#34;)); out.println(collect); } catch (ioexception e) { e.printstacktrace(); } } } } 实现\ndemo( // 创建 map 集合 // 创建 concurrenthashmap 对不对? () -\u0026gt; new concurrenthashmap\u0026lt;string, integer\u0026gt;(), // 进行计数 (map, words) -\u0026gt; { for (string word : words) { integer counter = map.get(word); int newvalue = counter == null ? 1 : counter + 1; map.put(word, newvalue); } } ); 输出:\n{a=186, b=192, c=187, d=184, e=185, f=185, g=176, h=185, i=193, j=189, k=187, l=157, m=189, n=181, o=180, p=178, q=185, r=188, s=181, t=183, u=177, v=186, w=188, x=178, y=189, z=186} 47 错误原因:\nconcurrenthashmap虽然每个方法都是线程安全的,但是多个方法的组合并不是线程安全的。 正确答案一: demo( () -\u0026gt; new concurrenthashmap\u0026lt;string, longadder\u0026gt;(), (map, words) -\u0026gt; { for (string word : words) { // 注意不能使用 putifabsent,此方法返回的是上一次的 value,首次调用返回 null map.computeifabsent(word, (key) -\u0026gt; new longadder()).increment(); } } ); 说明:\ncomputifabsent方法的作用是:当map中不存在以参数1为key对应的value时,会将参数2函数式接口的返回值作为value,put进map中,然后返回该value。如果存在key,则直接返回value 以上两部均是线程安全的。 正确答案二: demo( () -\u0026gt; new concurrenthashmap\u0026lt;string, integer\u0026gt;(), (map, words) -\u0026gt; { for (string word : words) { // 函数式编程,无需原子变量 //如果存在执行lambda返回,不存在返回第二个参数 map.merge(word, 1, integer::sum); } } ); concurrenthashmap 原理 jdk 7 hashmap 并发死链\njdk 8 concurrenthashmap 重要属性和内部类 // 默认为 0 // 当初始化时, 为 -1 // 当扩容时, 为 -(1 + 扩容线程数) // 当初始化或扩容完成后,为 下一次的扩容的阈值大小 private transient volatile int sizectl; // 整个 concurrenthashmap 就是一个 node[] static class node\u0026lt;k,v\u0026gt; implements map.entry\u0026lt;k,v\u0026gt; {} // hash 表 transient volatile node\u0026lt;k,v\u0026gt;[] table; // 扩容时的 新 hash 表 private transient volatile node\u0026lt;k,v\u0026gt;[] nexttable; // 扩容时如果某个 bin 迁移完毕, 用 forwardingnode 作为旧 table bin 的头结点 static final class forwardingnode\u0026lt;k,v\u0026gt; extends node\u0026lt;k,v\u0026gt; {} // 用在 compute 以及 computeifabsent 时, 用来占位, 计算完成后替换为普通 node static final class reservationnode\u0026lt;k,v\u0026gt; extends node\u0026lt;k,v\u0026gt; {} // 作为 treebin 的头节点, 存储 root 和 first static final class treebin\u0026lt;k,v\u0026gt; extends node\u0026lt;k,v\u0026gt; {} // 作为 treebin 的节点, 存储 parent, left, right static final class treenode\u0026lt;k,v\u0026gt; extends node\u0026lt;k,v\u0026gt; {} 重要方法 // 获取 node[] 中第 i 个 node static final \u0026lt;k,v\u0026gt; node\u0026lt;k,v\u0026gt; tabat(node\u0026lt;k,v\u0026gt;[] tab, int i) // cas 修改 node[] 中第 i 个 node 的值, c 为旧值, v 为新值 static final \u0026lt;k,v\u0026gt; boolean castabat(node\u0026lt;k,v\u0026gt;[] tab, int i, node\u0026lt;k,v\u0026gt; c, node\u0026lt;k,v\u0026gt; v) // 直接修改 node[] 中第 i 个 node 的值, v 为新值 static final \u0026lt;k,v\u0026gt; void settabat(node\u0026lt;k,v\u0026gt;[] tab, int i, node\u0026lt;k,v\u0026gt; v) 构造器分析 可以看到实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建\npublic concurrenthashmap(int initialcapacity, float loadfactor, int concurrencylevel) { if (!(loadfactor \u0026gt; 0.0f) || initialcapacity \u0026lt; 0 || concurrencylevel \u0026lt;= 0) throw new illegalargumentexception(); if (initialcapacity \u0026lt; concurrencylevel) // use at least as many bins initialcapacity = concurrencylevel; // as estimated threads long size = (long)(1.0 + (long)initialcapacity / loadfactor); // tablesizefor 仍然是保证计算的大小是 2^n, 即 16,32,64 ... int cap = (size \u0026gt;= (long)maximum_capacity) ? maximum_capacity : tablesizefor((int)size); this.sizectl = cap; } get流程 public v get(object key) { node\u0026lt;k,v\u0026gt;[] tab; node\u0026lt;k,v\u0026gt; e, p; int n, eh; k ek; // spread 方法能确保返回结果是正数 int h = spread(key.hashcode()); if ((tab = table) != null \u0026amp;\u0026amp; (n = tab.length) \u0026gt; 0 \u0026amp;\u0026amp; (e = tabat(tab, (n - 1) \u0026amp; h)) != null) { // 如果头结点已经是要查找的 key if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null \u0026amp;\u0026amp; key.equals(ek))) return e.val; } // hash 为负数表示该 bin 在扩容中或是 treebin, 这时调用 find 方法来查找 else if (eh \u0026lt; 0) return (p = e.find(h, key)) != null ? p.val : null; // 正常遍历链表, 用 equals 比较 while ((e = e.next) != null) { if (e.hash == h \u0026amp;\u0026amp; ((ek = e.key) == key || (ek != null \u0026amp;\u0026amp; key.equals(ek)))) return e.val; } } return null; } 总结:\n如果table不为空且长度大于0且索引位置有元素 if 头节点key的hash值相等 头节点的key指向同一个地址或者equals 返回value else if 头节点的hash为负数(bin在扩容或者是treebin) 调用find方法查找 进入循环(e不为空): 节点key的hash值相等,且key指向同一个地址或equals 返回value 返回null put 流程 以下数组简称(table),链表简称(bin)\npublic v put(k key, v value) { return putval(key, value, false); } final v putval(k key, v value, boolean onlyifabsent) { if (key == null || value == null) throw new nullpointerexception(); // 其中 spread 方法会综合高位低位, 具有更好的 hash 性 int hash = spread(key.hashcode()); int bincount = 0; for (node\u0026lt;k,v\u0026gt;[] tab = table;;) { // f 是链表头节点 // fh 是链表头结点的 hash // i 是链表在 table 中的下标 node\u0026lt;k,v\u0026gt; f; int n, i, fh; // 要创建 table if (tab == null || (n = tab.length) == 0) // 初始化 table 使用了 cas, 无需 synchronized 创建成功, 进入下一轮循环 tab = inittable(); // 要创建链表头节点 else if ((f = tabat(tab, i = (n - 1) \u0026amp; hash)) == null) { // 添加链表头使用了 cas, 无需 synchronized if (castabat(tab, i, null, new node\u0026lt;k,v\u0026gt;(hash, key, value, null))) break; } // 帮忙扩容 else if ((fh = f.hash) == moved) // 帮忙之后, 进入下一轮循环 tab = helptransfer(tab, f); else { v oldval = null; // 锁住链表头节点 synchronized (f) { // 再次确认链表头节点没有被移动 if (tabat(tab, i) == f) { // 链表 if (fh \u0026gt;= 0) { bincount = 1; // 遍历链表 for (node\u0026lt;k,v\u0026gt; e = f;; ++bincount) { k ek; // 找到相同的 key if (e.hash == hash \u0026amp;\u0026amp; ((ek = e.key) == key || (ek != null \u0026amp;\u0026amp; key.equals(ek)))) { oldval = e.val; // 更新 if (!onlyifabsent) e.val = value; break; } node\u0026lt;k,v\u0026gt; pred = e; // 已经是最后的节点了, 新增 node, 追加至链表尾 if ((e = e.next) == null) { pred.next = new node\u0026lt;k,v\u0026gt;(hash, key, value, null); break; } } } // 红黑树 else if (f instanceof treebin) { node\u0026lt;k,v\u0026gt; p; bincount = 2; // puttreeval 会看 key 是否已经在树中, 是, 则返回对应的 treenode if ((p = ((treebin\u0026lt;k,v\u0026gt;)f).puttreeval(hash, key, value)) != null) { oldval = p.val; if (!onlyifabsent) p.val = value; } } } // 释放链表头节点的锁 } if (bincount != 0) { if (bincount \u0026gt;= treeify_threshold) // 如果链表长度 \u0026gt;= 树化阈值(8), 进行链表转为红黑树 treeifybin(tab, i); if (oldval != null) return oldval; break; } } } // 增加 size 计数 addcount(1l, bincount); return null; } private final node\u0026lt;k,v\u0026gt;[] inittable() { node\u0026lt;k,v\u0026gt;[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizectl) \u0026lt; 0) thread.yield(); // 尝试将 sizectl 设置为 -1(表示初始化 table) else if (u.compareandswapint(this, sizectl, sc, -1)) { // 获得锁, 创建 table, 这时其它线程会在 while() 循环中 yield 直至 table 创建 try { if ((tab = table) == null || tab.length == 0) { int n = (sc \u0026gt; 0) ? sc : default_capacity; node\u0026lt;k,v\u0026gt;[] nt = (node\u0026lt;k,v\u0026gt;[])new node\u0026lt;?,?\u0026gt;[n]; table = tab = nt; sc = n - (n \u0026gt;\u0026gt;\u0026gt; 2); } } finally { sizectl = sc; } break; } } return tab; } // check 是之前 bincount 的个数 private final void addcount(long x, int check) { countercell[] as; long b, s; if ( // 已经有了 countercells, 向 cell 累加 (as = countercells) != null || // 还没有, 向 basecount 累加 !u.compareandswaplong(this, basecount, b = basecount, s = b + x) ) { countercell a; long v; int m; boolean uncontended = true; if ( // 还没有 countercells as == null || (m = as.length - 1) \u0026lt; 0 || // 还没有 cell (a = as[threadlocalrandom.getprobe() \u0026amp; m]) == null || // cell cas 增加计数失败 !(uncontended = u.compareandswaplong(a, cellvalue, v = a.value, v + x)) ) { // 创建累加单元数组和cell, 累加重试 fulladdcount(x, uncontended); return; } if (check \u0026lt;= 1) return; // 获取元素个数 s = sumcount(); } if (check \u0026gt;= 0) { node\u0026lt;k,v\u0026gt;[] tab, nt; int n, sc; while (s \u0026gt;= (long)(sc = sizectl) \u0026amp;\u0026amp; (tab = table) != null \u0026amp;\u0026amp; (n = tab.length) \u0026lt; maximum_capacity) { int rs = resizestamp(n); if (sc \u0026lt; 0) { if ((sc \u0026gt;\u0026gt;\u0026gt; resize_stamp_shift) != rs || sc == rs + 1 || sc == rs + max_resizers || (nt = nexttable) == null || transferindex \u0026lt;= 0) break; // newtable 已经创建了,帮忙扩容 if (u.compareandswapint(this, sizectl, sc, sc + 1)) transfer(tab, nt); } // 需要扩容,这时 newtable 未创建 else if (u.compareandswapint(this, sizectl, sc, (rs \u0026lt;\u0026lt; resize_stamp_shift) + 2)) transfer(tab, null); s = sumcount(); } } } 总结:\n进入for循环: if table为null或者长度 为0 初始化表 else if 索引处无节点 创建节点,填入key和value,放入table,退出循环 else if 索引处节点的hash值为move(forwardingnode),表示正在扩容和迁移 帮忙 else 锁住头节点 if 再次确认头节点没有被移动 if 头节点hash值大于0(表示这是一个链表) 遍历链表找到对应key,如果没有,创建。 else if 节点为红黑树节点 调用puttreeval查看是否有对应key的数节点 如果有且为覆盖模式,将值覆盖,返回旧值 如果没有,创建并插入,返回null 解锁 if bincount不为0 如果bincount大于树化阈值8 树化 如果旧值不为null 返回旧值 break 增加size计数 return null size 计算流程 size 计算实际发生在 put,remove 改变集合元素的操作之中\n没有竞争发生,向 basecount 累加计数 有竞争发生,新建 countercells,向其中的一个 cell 累加计 countercells 初始有两个 cell 如果计数竞争比较激烈,会创建新的 cell 来累加计数 public int size() { long n = sumcount(); return ((n \u0026lt; 0l) ? 0 : (n \u0026gt; (long)integer.max_value) ? integer.max_value : (int)n); } final long sumcount() { countercell[] as = countercells; countercell a; // 将 basecount 计数与所有 cell 计数累加 long sum = basecount; if (as != null) { for (int i = 0; i \u0026lt; as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; } 总结 java 8 数组(node) +( 链表 node | 红黑树 treenode ) 以下数组简称(table),链表简称(bin)\n初始化,使用 cas 来保证并发安全,懒惰初始化 table 树化,当 table.length \u0026lt; 64 时,先尝试扩容,超过 64 时,并且 bin.length \u0026gt; 8 时,会将链表树化,树化过程 会用 synchronized 锁住链表头 put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素 添加至 bin 的尾部 get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 forwardingnode 它会让 get 操作在新 table 进行搜索 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可 做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中 size,元素个数保存在 basecount 中,并发时的个数变动保存在 countercell[] 当中。最后统计数量时累加 即可 blockingqueue 原理 基本的入队出队 public class linkedblockingqueue\u0026lt;e\u0026gt; extends abstractqueue\u0026lt;e\u0026gt; implements blockingqueue\u0026lt;e\u0026gt;, java.io.serializable { static class node\u0026lt;e\u0026gt; { e item; /** * 下列三种情况之一 * - 真正的后继节点 * - 自己, 发生在出队时 * - null, 表示是没有后继节点, 是最后了 */ node\u0026lt;e\u0026gt; next; node(e x) { item = x; } } } 初始化链表 last = head = new node(null);dummy 节点用来占位,item 为 null\n当一个节点入队 last = last.next = node;\n再来一个节点入队last = last.next = node;\n出队 //临时变量h用来指向哨兵 node\u0026lt;e\u0026gt; h = head; //first用来指向第一个元素 node\u0026lt;e\u0026gt; first = h.next; h.next = h; // help gc //head赋值为first,表示first节点就是下一个哨兵。 head = first; e x = first.item; //删除first节点中的数据,表示真正成为了哨兵,第一个元素出队。 first.item = null; return x; h = head\nfirst = h.next\nh.next = h\nhead = first\ne x = first.item; first.item = null; return x; 加锁分析 高明之处在于用了两把锁和 dummy 节点\n用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行 消费者与消费者线程仍然串行 生产者与生产者线程仍然串行 线程安全分析\n当节点总数大于 2 时(包括 dummy 节点),putlock 保证的是 last 节点的线程安全,takelock 保证的是 head 节点的线程安全。两把锁保证了入队和出队没有竞争 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notempty 条件阻塞,有竞争,会阻塞 // 用于 put(阻塞) offer(非阻塞) private final reentrantlock putlock = new reentrantlock(); // 用户 take(阻塞) poll(非阻塞) private final reentrantlock takelock = new reentrantlock(); put 操作\npublic void put(e e) throws interruptedexception { //linkedblockingqueue不支持空元素 if (e == null) throw new nullpointerexception(); int c = -1; node\u0026lt;e\u0026gt; node = new node\u0026lt;e\u0026gt;(e); final reentrantlock putlock = this.putlock; // count 用来维护元素计数 final atomicinteger count = this.count; putlock.lockinterruptibly(); try { // 满了等待 while (count.get() == capacity) { // 倒过来读就好: 等待 notfull notfull.await(); } // 有空位, 入队且计数加一 enqueue(node); c = count.getandincrement(); // 除了自己 put 以外, 队列还有空位, 由自己叫醒其他 put 线程 if (c + 1 \u0026lt; capacity) notfull.signal(); } finally { putlock.unlock(); } // 如果队列中有一个元素, 叫醒 take 线程 if (c == 0) // 这里调用的是 notempty.signal() 而不是 notempty.signalall() 是为了减少竞争 signalnotempty(); } take 操作\npublic e take() throws interruptedexception { e x; int c = -1; final atomicinteger count = this.count; final reentrantlock takelock = this.takelock; takelock.lockinterruptibly(); try { while (count.get() == 0) { notempty.await(); } x = dequeue(); c = count.getanddecrement(); if (c \u0026gt; 1) notempty.signal(); } finally { takelock.unlock(); } // 如果队列中只有一个空位时, 叫醒 put 线程 // 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c \u0026lt; capacity if (c == capacity) // 这里调用的是 notfull.signal() 而不是 notfull.signalall() 是为了减少竞争 signalnotfull() return x; } 由 put 唤醒 put 是为了避免信号不足\n性能比较 主要列举 linkedblockingqueue 与 arrayblockingqueue 的性能比较\nlinked 支持有界,array 强制有界 linked 实现是链表,array 实现是数组 linked 是懒惰的,而 array 需要提前初始化 node 数组 linked 每次入队会生成新 node,而 array 的 node 是提前创建好的 linked 两把锁,array 一把锁 concurrentlinkedqueue concurrentlinkedqueue 的设计与 linkedblockingqueue 非常像,也是\n两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行 dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争 只是这【锁】使用了 cas 来实现 事实上,concurrentlinkedqueue 应用还是非常广泛的\n例如之前讲的 tomcat 的 connector 结构时,acceptor 作为生产者向 poller 消费者传递事件信息时,正是采用了 concurrentlinkedqueue 将 socketchannel 给 poller 使用\ngraph lr subgraph connector-\u0026gt;nio endpoint t1(limitlatch) t2(acceptor) t3(socketchannel 1) t4(socketchannel 2) t5(poller) subgraph executor t7(worker1) t8(worker2) end t1 --\u0026gt; t2 t2 --\u0026gt; t3 t2 --\u0026gt; t4 t3 --有读--\u0026gt; t5 t4 --有读--\u0026gt; t5 t5 --socketprocessor--\u0026gt; t7 t5 --socketprocessor--\u0026gt; t8 end copyonwritearraylist copyonwritearrayset是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更 改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。 以新增为例:\npublic boolean add(e e) { synchronized (lock) { // 获取旧的数组 object[] es = getarray(); int len = es.length; // 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程) es = arrays.copyof(es, len + 1); // 添加新元素 es[len] = e; // 替换旧的数组 setarray(es); return true; } } 这里的源码版本是 java 11,在 java 1.8 中使用的是可重入锁而不是 synchronized\n其它读操作并未加锁,例如:\npublic void foreach(consumer\u0026lt;? super e\u0026gt; action) { objects.requirenonnull(action); for (object x : getarray()) { @suppresswarnings(\u0026#34;unchecked\u0026#34;) e e = (e) x; action.accept(e); } } 适合『读多写少』的应用场景\nget 弱一致性 时间点 操作 1 thread-0 getarray() 2 thread-1 getarray() 3 thread-1 setarray(arraycopy) 4 thread-0 array[index] 不容易测试,但问题确实存在\n迭代器弱一致性 copyonwritearraylist\u0026lt;integer\u0026gt; list = new copyonwritearraylist\u0026lt;\u0026gt;(); list.add(1); list.add(2); list.add(3); iterator\u0026lt;integer\u0026gt; iter = list.iterator(); new thread(() -\u0026gt; { list.remove(0); system.out.println(list); }).start(); sleep1s(); //此时主线程的iterator依旧指向旧的数组。 while (iter.hasnext()) { system.out.println(iter.next()); } 不要觉得弱一致性就不好\n数据库的 mvcc 都是弱一致性的表现 并发高和一致性是矛盾的,需要权衡 ","date":"2023-03-31","permalink":"https://www.holatto.com/posts/juc/advance-study/","summary":"进程与线程 进程与线程 进程 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备","title":"初学并发编程"},{"content":"什么是jvm 定义: java virtual machine - java 程序的运行环境(java 二进制字节码的运行环境)\n好处: 一次编写,到处运行\n自动内存管理,垃圾回收功能\n数组下标越界检查\n多态\n比较:jvm jre jdk jvm学习路线 jvm内存结构 内存结构 程序计数器\n虚拟机栈\n本地方法栈\n堆\n方法区\n程序计数器 定义\nprogram counter register 程序计数器(寄存器,物理地址是使用寄存器作为程序计数器),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。\n特点\n是线程私有的\n不会存在内存溢出\n作用\n作用,是记住下一条jvm指令的执行地址\n0: getstatic #20 // printstream out = system.out; 3: astore_1 // -- 4: aload_1 // out.println(1); 5: iconst_1 // -- 6: invokevirtual #26 // -- 9: aload_1 // out.println(2); 10: iconst_2 // -- 11: invokevirtual #26 // -- 14: aload_1 // out.println(3); 15: iconst_3 // -- 16: invokevirtual #26 // -- 19: aload_1 // out.println(4); 20: iconst_4\t// -- 21: invokevirtual #26 // -- 24: aload_1 // out.println(5); 25: iconst_5 // -- 26: invokevirtual #26 // -- 29: return 如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是 native 方法,这个数器值则为空 (undefined)。\n虚拟机栈 定义\njava virtual machine stacks (java 虚拟机栈)\n每个线程运行时所需要的内存,称为虚拟机栈\n每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存\n每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法\n问题辨析\n垃圾回收是否涉及栈内存?\t不需要,每次栈帧使用完毕也就是方法执行完成后,会进行栈帧的弹出。\n栈内存分配越大越好吗?\t不是,栈内存越大,线程数越少。栈内存增大只会增多方法递归的调用。\n方法内的局部变量是否线程安全?\n如果方法内局部变量没有逃离方法的作用访问,它是线程安全的\n如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全\n栈内存溢出\n栈帧过多导致栈内存溢出\n栈帧过大导致栈内存溢出\n线程运行诊断\n案例:cpu 占用过多\n定位\n用top定位哪个进程对cpu的占用过高\nps h -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)\njstack 进程id\n可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号 本地方法栈 调用本地方法的方法使用的栈内存。本地方法常用native关键字标识;\n本地方法栈 (native method stack) 与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 native 方法服务\n本地方法是java层面不能实现的方法,而使用c或者c++进行方法实现底层的逻辑。\n堆 定义\nheap 堆,通过 new 关键字,创建对象都会使用堆内存\n特点\n它是线程共享的,堆中对象都需要考虑线程安全的问题\n有垃圾回收机制\n堆内存溢出诊断\njps 工具 查看当前系统中有哪些 java 进程\njmap 工具 查看堆内存占用情况 jmap - heap 进程id\njconsole 工具 图形界面的,多功能的监测工具,可以连续监测\n案例\n垃圾回收后,内存占用仍然很高 方法区 定义 java 虚拟机有一个在所有 java 虚拟机线程之间共享的方法区域。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化以及接口初始化中使用的特殊方法。方法区域在逻辑上是堆的一部分。\n组成\n方法区内存溢出\n1.8 以前会导致永久代内存溢出\n演示永久代内存溢出 java.lang.outofmemoryerror: permgen space\n-xx:maxpermsize=8m\n1.8 之后会导致元空间内存溢出\n演示元空间内存溢出 java.lang.outofmemoryerror: metaspace -xx:maxmetaspacesize=8m 这区域的内存回收目标主要是针对常量池的回收和对类型的卸载\n运行时常量池\n常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址 运行时常量池相对于 class 文件常量池的另外一个重要特征是具备动态性,java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放人池中,这种特性被开发人员利用得比较多的便是 string 类的 intern() 方法。 stringtable(串池)\n先看几道面试题:\nstring s1 = \u0026#34;a\u0026#34;; string s2 = \u0026#34;b\u0026#34;; string s3 = \u0026#34;a\u0026#34; + \u0026#34;b\u0026#34;;\t//值是固定的,因此在编译期间就能确定s3的值 string s4 = s1 + s2;\t//new stringbuilder().append(\u0026#34;a\u0026#34;).append(\u0026#34;b\u0026#34;).tostring(); string s5 = \u0026#34;ab\u0026#34;;\t// 串池中有 string s6 = s4.intern(); // 问 system.out.println(s3 == s4); //false system.out.println(s3 == s5); //true system.out.println(s3 == s6); //true string x2 = new string(\u0026#34;c\u0026#34;) + new string(\u0026#34;d\u0026#34;); //new string(\u0026#34;cd\u0026#34;) x2.intern();\t//1.8放入串池并且返回给x2 //1.6复制一份放入串池 string x1 = \u0026#34;cd\u0026#34;;\t// 如果是jdk1.6呢 //1.8 true //1.6 false system.out.println(x1 == x2); stringtable 特性\n常量池中的字符串仅是符号,第一次用到时才变为对象\n利用串池的机制,来避免重复创建字符串对象\n字符串变量拼接的原理是 stringbuilder (1.8)\n字符串常量拼接的原理是编译期优化\n可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池\n1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池 string.intern() 是一个 native 方法,它的作用是: 如果字符串常量池中已经包含一个等于此 string 对象的字符串,则返回代表池中这个字符串的 string 对象;否则,将此 string 对象包含的字符串添加到常量池中,并且返回此 string 对象的引用。\nstringtable 位置\n1.6在permgen永久代 1.8在heap堆内存 stringtable 性能调优\n调整 -xx:stringtablesize=桶个数\n考虑将字符串对象是否入池\n直接内存 定义\ndirect memory\n常见于 nio 操作时,用于数据缓冲区\n属于操作系统内存,分配回收成本较高,但读写性能高\n不受 jvm 内存回收管理\njava io读取文件\nnio读取文件\n分配和回收原理\n使用了 unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freememory 方法 bytebuffffer 的实现类内部,使用了 cleaner (虚引用)来监测 bytebuffffer 对象,一旦bytebuffffer 对象被垃圾回收,那么就会由 referencehandler 线程通过 cleaner 的 clean 方法调用 freememory 来释放直接内存 hotspot虚拟机对象 对象创建\n对象(文中讨论的对象限于普通 java 对象,不包括数组和 class 对象等) 的创建又是怎样一个过程呢?\n虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。在类加载检查通过后,接下来虚拟机将为新生对象分配内存。\n在使用 serial、parncw 等带 compact 过程的收集器时,系统采用的分配算法是指针碰撞,而使用 cms 这种基于 mark-sweep 算法的收集器时,通常采用空闲列表。\n对象的内存布局\n在 hotspot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头(header).实例数据 (instance data) 和对齐填充 (padding)。\n对象头包括两部分信息 :① 第一部分用于存储对象自身的运行时数据,如哈希码 (hashcode)、gc 分代年龄、锁状态标志、线程持有的锁、偏向线程id、偏向时间戳等,这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针) 中分别为 32bit 和64bit,官方称它为“mark word”。② 对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 如果对象是一个 java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 java 对象的元数据信息确定 java 对象的大小,但是从数组的元数据中却无法确定数组的大小。\n接下来的实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。\n第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作。\n对象的访问定位\n目前主流的访问方式有使用句柄和直接指针两种\n句柄 访问\n直接指针 访问\noutofmemoryerror 虚拟机栈和本地方法方法栈溢出\n关于虚拟机栈和本地方法栈,在 java 虚拟机规范中描述了两种异常:\n如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 stackoverfiowerror 异常\n如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 outofmemoryerror 异常\n在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是 stackoverflowerror 异常。\n垃圾回收 判断对象可以回收 可达性分析算法 java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象 扫描堆中的对象,看是否能够沿着 gc root对象 为起点的引用链找到该对象,找不到,表示可以回收 这个算法的基本思路就是通过一系列的称为“gc roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链 (reference chain),当一个对象到 gc roots 没有任何引用链相连(用图论的话来说,就是从 gc roots 到这个对象不可达)时,则证明此对象是不可用的 哪些对象可以作为 gc root ? 回收方法区 永久代的垃圾收集主要回收两部分内容 : 废弃常量和无用的类\n类需要同时满足下面 3 个条件才能算是“无用的类”\n该类所有的实例都已经被回收,也就是 java 堆中不存在该类的任何实例。 加载该类的 classloader 已经被回收。 该类对应的java.lang.class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 五种引用 强引用\n只有所有 gc roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收 软引用(softreference)\n仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象可以配合引用队列来释放软引用自身 弱引用(weakreference)\n仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象可以配合引用队列来释放弱引用自身 虚引用(phantomreference)\n必须配合引用队列使用,主要配合 bytebuffffer 使用,被引用对象回收时,会将虚引用入队,由 reference handler 线程调用虚引用相关方法释放直接内存 终结器引用(finalreference)\n无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 finalizer 线程通过终结器引用找到被引用对象并调用它的 fifinalize方法,第二次 gc 时才能回收被引用对象 垃圾回收算法 标记清除\n定义: mark sweep\n速度较快\n会造成内存碎片\n注意:标记清除中清除并不会直接将内存清零,而是将内存地址起始结束地址保存在空闲列表中,下次使用时通过保存的地址信息进行合理分配。\n标记整理\n定义:mark compact\n速度慢 没有内存碎片 复制\n定义:copy\n不会有内存碎片 需要占用双倍内存空间 分代垃圾回收 对象首先分配在伊甸园区域 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit) 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,stw的时间更长 为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了maxtenuringthreshold才能晋老年代,如果在survivor 空间中相同年龄所有对象大小的总和大下survivor 空间的半,年龄大于或等于该年龄的对象就可以直接进大老年代,无须等待maxtenuringthreshold中要求的年龄。\n垃圾回收器 串行\n单线程 堆内存较小,适合个人电脑 吞吐量优先\n所谓吞吐量就是 cpu 用于运行用户代码的时间与 cpu 总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 +垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是99%。\n多线程\n堆内存较大,多核 cpu\n让单位时间内,stw 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高。(最短的时间完成垃圾回收,关注回收量)\n响应时间优先\n多线程 堆内存较大,多核 cpu 尽可能让单次 stw 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5(关注时间短) 串行 -xx:+useserialgc = serial + serialold 串行垃圾回收:复制(新生代)+ 标记整理(老年代)\n吞吐量优先 -xx:+useparallelgc ~ -xx:+useparalleloldgc 并行垃圾回收:复制(新生代)+ 标记整理(老年代)\n-xx:+useadaptivesizepolicy\t动态调整伊甸园和幸存区的大小\n-xx:gctimeratio=ratio\t调整吞吐率(1/(1+radio))\n-xx:maxgcpausemillis=ms\t最大暂停毫秒数(默认200ms)\n-xx:parallelgcthreads=n\t垃圾回收线程数\n响应时间优先(cms) -xx:+useconcmarksweepgc ~ -xx:+useparnewgc ~ serialold\tcms垃圾回收器(老年代);copy算法垃圾回收器(新生代);串行化垃圾回收器(并发失败后)\n-xx:parallelgcthreads=n ~ -xx:concgcthreads=threads\t垃圾回收线程数;标记线程(一般为整体的四分之一)\n-xx:cmsinitiatingoccupancyfraction=percent\t垃圾回收内存占比,预留内存给浮动垃圾占中\n-xx:+cmsscavengebeforeremark\t重新标记前,再对新生代进行垃圾回收,以防止新生代引用老生代的对象,导致扫描整个堆\n重新标记进行stw,防止在并发标记的时候其他线程干扰对象引用,从而重新标记\n并发清理过程中其他线程产生的垃圾,称为浮动垃圾\ng1 定义:garbage first\n2004 论文发布\n2009 jdk 6u14 体验\n2012 jdk 7u4 官方支持\n2017 jdk 9 默认\n适用场景\n同时注重吞吐量(throughput)和低延迟(low latency),默认的暂停目标是 200 ms\n超大堆内存,会将堆划分为多个大小相等的 region\n整体上是 标记+整理 算法,两个区域之间是 复制 算法\n相关 jvm 参数\n-xx:+useg1gc\n-xx:g1heapregionsize=size\n-xx:maxgcpausemillis=time\ng1 垃圾回收阶段\nyoung collection 会 stw\nyoung collection + cm(concurrent mark) 在 young gc 时会进行 gc root 的初始标记 老年代占用堆空间比例达到阈值时,进行并发标记(不会 stw),由下面的 jvm 参数决定 -xx:initiatingheapoccupancypercent=percent (默认45%)\nmixed collection\n会对 e、s、o 进行全面垃圾回收\n\t最终标记(remark)会 stw\n\t拷贝存活(evacuation)会 stw\n-xx:maxgcpausemillis=ms 设置暂停时间,在暂停时间内复制部分老年代\nfull gc\nserialgc\n新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足发生的垃圾收集 - full gc parallelgc\n新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足发生的垃圾收集 - full gc cms\n新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足 g1\n新生代内存不足发生的垃圾收集 - minor gc 老年代内存不足 young collection 跨代引用\n新生代回收的跨代引用(老年代引用新生代)问题 卡表与 remembered set\n在引用变更时通过 post-write barrier + dirty card queue\nconcurrent refinement threads 更新 remembered set\njdk 8u20 字符串去重\n优点:节省大量内存 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加 -xx:+usestringdeduplication 默认开启\nstring s1 = new string(\u0026#34;hello\u0026#34;); // char[]{\u0026#39;h\u0026#39;,\u0026#39;e\u0026#39;,\u0026#39;l\u0026#39;,\u0026#39;l\u0026#39;,\u0026#39;o\u0026#39;} string s2 = new string(\u0026#34;hello\u0026#34;); // char[]{\u0026#39;h\u0026#39;,\u0026#39;e\u0026#39;,\u0026#39;l\u0026#39;,\u0026#39;l\u0026#39;,\u0026#39;o\u0026#39;} 将所有新分配的字符串放入一个队列 当新生代回收时,g1并发检查是否有字符串重复 如果它们值一样,让它们引用同一个 char[] 注意,与 string.intern() 不一样\nstring.intern() 关注的是字符串对象\n而字符串去重关注的是 char[]\n在 jvm 内部,使用了不同的字符串表\njdk 8u40 并发标记类卸载\n所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类\n-xx:+classunloadingwithconcurrentmark 默认启用\njdk 8u60 回收巨型对象\n一个对象大于 region 的一半时,称之为巨型对象 g1 不会对巨型对象进行拷贝 回收时被优先考虑 g1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉 垃圾回收调优 调优领域\n内存 锁竞争 cpu 占用 io 确定目标\n【低延迟】还是【高吞吐量】,选择合适的回收器 cms,g1,zgc\nparallelgc\nzing\n最快的 gc\n答案是不发生 gc\n查看 fullgc 前后的内存占用,考虑下面几个问题\n数据是不是太多? resultset = statement.executequery(\u0026ldquo;select * from 大表 limit n\u0026rdquo;) 数据表示是否太臃肿? 对象图 对象大小 16 integer 24 int 4 是否存在内存泄漏?\nstatic map map =\n软\n弱\n第三方缓存实现\n新生代调优\n新生代的特点 所有的 new 操作的内存分配非常廉价 tlab thread-local allocation buffffer 死亡对象的回收代价是零 大部分对象用过即死 minor gc 的时间远远低于 full gc 老年代调优\n以 cms 为例\ncms 的老年代内存越大越好\n先尝试不做调优,如果没有 full gc 那么已经\u0026hellip;,否则先尝试调优新生代\n观察发生 full gc 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3\n-xx:cmsinitiatingoccupancyfraction=percent 案例分析\n① 堆外内存导致的溢出错误\n垃圾收集进行时,虚拟机虽然会对 direct memory 进行回收,但是 directmemory 却不能像新生代、老年代那样,发现空间不足了就通知收集器进行垃圾回收,它只能等待老年代满了后 full gc,然后“顺便地”帮它清理掉内存的废弃对象。否则它只能一直等到抛出内存溢出异常时,先 catch 掉,再在 catch 块里面“大喊”一声:“system.gc()!”。要是虚拟机还是不听(如打开了 -xx:+disableexplicitgc 开关),那就只能眼睁睁地看着堆中还有许多空闲内存,自己却不得不抛出内存溢出异常了\n从实践经验的角度出发,除了 java 堆和永久代之外,我们注意到下面这些区域还会占用较多的内存,这里所有的内存总和受到操作系统进程最大内存的限制。\n② 外部命令导致系统缓慢\n每个用户请求的处理都需要执行一个外部 shell 脚本来获得系统的一些信息。执行这个 shell 脚本是通过java.runtime.getruntime(.exec0 方法来调用的\njava 虚拟机执行这个命令的过程是:首先克隆一个和当前虚拟机拥有一样环境变量的进程,再用这个新的进程去执行外部命令,最后再退出这个进程。如果频繁执行这个操作,系统的消耗会很大,不仅是 cpu,内存负担也很重。\n类加载与字节码技术 概述 无关性的基石\n实现语言无关性的基础仍然是虚拟机和字节码存储格式;\njava虚拟机不和包括java在内的任何语言绑定,它只与“class文件”这种特定的二进制文件格式所关联,class文件中包含了java虚拟机指令集和符号表以及若干其他辅助信息;\n作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将java虚拟机作为语言的产品交付媒介\n例如,使用java编译器可以把java代码编译为存储字节码的class文件,使用jruby等其他语言的编译器一样可以把程序代码编译成class文件,虚拟机并不关心class的来源是何种语言,如图 介绍\n类文件结构 字节码指令 编译期处理 类加载阶段 类加载器 运行期优化 类文件结构 整个class文件实质上就是一张表。\nclassfile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; } 以helloworld.class为例\n魔数\n0~3 字节,每个class文件的头4个字节称为魔术,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的class文件。\n0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09\n版本\n4~7 字节,表示类的版本 00 34(52) 表示是 java 8\n0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09\n常量池\n他是class文件结构中与其他项目相关最多的数据类型,也是占用class文件空间最大的数据项目之一,同时还是class文件中第一个出现表类型数据项目; 8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34项,注意 #0 项不计入,也没有值; 设计者将第0项常量空出来的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以吧索引值设置为0来表示;\nclass文件只有常量池容量计数是从1开始,其他集合类型,包括接口索引集合,字段集合等都是从0开始计数。 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09\n字段表集合\n描述符的作用是用来描述字段的数据类型,方法的参数列表(包括数量、类型以及顺序)和返回值。\n方法java.lang.string.tostring()的描述符为\u0026quot;()ljava/lang/string\u0026quot;\n方法表集合\n类构造器”\u0026lt;c|init\u0026gt;“方法和是实例构造器\u0026quot;\u0026lt;init\u0026gt;\u0026ldquo;方法\n属性表集合\n如果大家注意到javap中输出的“args_size”的值,可能会有疑问:这个类有两个方法——实例构造器\u0026lt;init\u0026gt;()和inc(),这两个方法很明显都是没有参数的,为什么args_size会为1?而且无论是在参数列表里还是方法体内,都没有定义任何局部变量,那locals又为什么会等于1?如果有这样的疑问,大家可能是忽略了一点:在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。\n这个访问机制对java程序的编写很重要,而它的实现却非常简单,仅仅是通过javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个slot位来存放对象实例的引用,方法参数值从1开始计算。这个处理只对实例方法有效,如果代码清单6-1中的inc()方法声明为static,那args_size就不会等于1而是等于0了\n访问标识与继承信息\n21 表示该 class 是一个类,公共的(access_flags)\n0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01\n05 表示根据常量池中 #5 找到本类全限定名(this_class)\n0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01\n06 表示根据常量池中 #6 找到父类全限定名(super_class)\n0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01\n表示接口的数量,本类为 0(interfaces_count)\n0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01\nfield 信息\n表示成员变量数量,本类为 0\n0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01\nmethod 信息\n表示方法数量,本类为 2\n0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01\n附加属性\n00 01 表示附加属性数量\n00 13 表示引用了常量池 #19 项,即【sourcefile】,用于记录生成这个class文件的源码文件名称\n对于大多数类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外,如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。\n00 00 00 02 表示此属性的长度\n00 14 表示引用了常量池 #20 项,即【helloworld.java】\n0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00\n0001120 00 00 02 00 14\n字节码指令 入门\n接着上一节,研究一下两组字节码指令,一个是\npublic cn.itcast.jvm.t5.helloworld(); 构造方法的字节码指令\n2a b7 00 01 b1 2a =\u0026gt; aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数\nb7 =\u0026gt; invokespecial 预备调用构造方法,哪个方法呢?\n00 01 引用常量池中 #1 项,即【 method java/lang/object.\u0026quot;\u0026rdquo;:()v 】\nb1 表示返回\n另一个是 public static void main(java.lang.string[]); 主方法的字节码指令\nb2 00 02 12 03 b6 00 04 b1 b2 =\u0026gt; getstatic 用来加载静态变量,哪个静态变量呢? 00 02 引用常量池中 #2 项,即【field java/lang/system.out:ljava/io/printstream;】 12 =\u0026gt; ldc 加载参数,哪个参数呢? 03 引用常量池中 #3 项,即 【string hello world】 b6 =\u0026gt; invokevirtual 预备调用成员方法,哪个方法呢? 00 04 引用常量池中 #4 项,即【method java/io/printstream.println:(ljava/lang/string;)v】 b1 表示返回 字节码与数据类型\n大部分的指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolcan类型。编译器会在编译期或运行期将byte和short类型的数据带符号扩展(sign-extend)为相应的int类型数据,将boolean和char类型数据零位扩展(zero-extend)为相应的int类型数据。与之类似,在处理boolean、byte、short和char 类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型。\n局部变量表\nreference类型表示对一个对象实例的引用,虚拟机实现至少都应该通过这个引用做到两点,一是从此引用中直接或者间接的查找到对象在java堆中的数据存放的起始地址索引,二是此引用中直接或者间接的查找到对象所属数据类型在方法区中的存储类型信息。\n因此,即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,不要认为java中任何情况下都存在诸如整型变量默认为0,布尔型变量默认为false等这样的默认值\n图解方法执行流程 原始 java 代码\npackage cn.itcast.jvm.t3.bytecode; /** * 演示 字节码指令 和 操作数栈、常量池的关系 */ public class demo3_1 { public static void main(string[] args) { int a = 10; int b = short.max_value + 1; int c = a + b; system.out.println(c); } } 常量池载入运行时常量池\n方法字节码载入方法区\nmain 线程开始运行,分配栈帧内存\n(stack=2,locals=4)\n执行引擎开始执行字节码\nbipush 10\n将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有\nsipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)\nldc 将一个 int 压入操作数栈\nldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)\n这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池\nistore_1\n将操作数栈顶数据弹出,存入局部变量表的 slot 1\nldc #3\n从常量池加载 #3 数据到操作数栈\n注意 short.max_value 是 32767,所以 32768 = short.max_value + 1 实际是在编译期间计算\n好的\nistore_2 iload_1 iload_2 iadd istore_3 getstatic #4 \tiload_3 invokevirtual #5\n找到常量池 #5 项\n定位到方法区 java/io/printstream.println:(i)v 方法\n生成新的栈帧(分配 locals、stack等)\n传递参数,执行新栈帧中的字节码\n\t执行完毕,弹出栈帧\n\t清除 main 操作数栈内容\nreturn\n完成 main 方法调用,弹出 main 栈帧\n程序结束\n练习 - 分析 i++ package cn.itcast.jvm.t3.bytecode; /** * 从字节码角度分析 a++ 相关题目 */ public class demo3_2 { public static void main(string[] args) { int a = 10; int b = a++ + ++a + a--; system.out.println(a); system.out.println(b); } } 分析:\n注意 iinc 指令是直接在局部变量 slot 上进行运算\na++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc\n条件判断指令 byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节\ngoto 用来进行跳转到指定行号的字节码\npublic class demo3_3 { public static void main(string[] args) { int a = 0; if(a == 0) { a = 10; } else { a = 20; } } } 字节码:\n0: iconst_0 1: istore_1 2: iload_1 3: ifne 12 6: bipush 10 8: istore_1 9: goto 15 12: bipush 20 14: istore_1 15: return 循环控制指令 public class demo3_4 { public static void main(string[] args) { int a = 0; while (a \u0026lt; 10) { a++; } } } 字节码是:\n0: iconst_0 1: istore_1 2: iload_1 3: bipush 10 5: if_icmpge 14 8: iinc 1, 1 11: goto 2 14: return 再比如 do while 循环:\npublic class demo3_5 { public static void main(string[] args) { int a = 0; do { a++; } while (a \u0026lt; 10); } } 字节码是:\n0: iconst_0 1: istore_1 2: iinc 1, 1 5: iload_1 6: bipush 10 8: if_icmplt 2 11: return 最后再看看 for 循环:\npublic class demo3_6 { public static void main(string[] args) { for (int i = 0; i \u0026lt; 10; i++) { } } } 字节码是:\n0: iconst_0 1: istore_1 2: iload_1 3: bipush 10 5: if_icmpge 14 8: iinc 1, 1 11: goto 2 14: return 构造方法 \u0026lt;cinit\u0026gt;()v(静态构造)\npublic class demo3_8_1 { static int i = 10; static { i = 20; } static { i = 30; } } 编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 \u0026lt;cinit\u0026gt;()v :\n0: bipush 10 2: putstatic #2 // field i:i 5: bipush 20 7: putstatic #2 // field i:i 10: bipush 30 12: putstatic #2 // field i:i 15: return \u0026lt;cinit\u0026gt;()v 方法会在类加载的初始化阶段被调用\n\u0026lt;init\u0026gt;()v(构造方法)\npublic class demo3_8_2 { private string a = \u0026#34;s1\u0026#34;; { b = 20; } private int b = 10; { a = \u0026#34;s2\u0026#34;; } public demo3_8_2(string a, int b) { this.a = a; this.b = b; } public static void main(string[] args) { demo3_8_2 d = new demo3_8_2(\u0026#34;s3\u0026#34;, 30); system.out.println(d.a); system.out.println(d.b); } } 编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后\npublic cn.itcast.jvm.t3.bytecode.demo3_8_2(java.lang.string, int); descriptor: (ljava/lang/string;i)v flags: acc_public code: stack=2, locals=3, args_size=3 0: aload_0 1: invokespecial #1 // super.\u0026lt;init\u0026gt;()v 4: aload_0 5: ldc #2 // \u0026lt;- \u0026#34;s1\u0026#34; 7: putfield #3 // -\u0026gt; this.a 10: aload_0 11: bipush 20 // \u0026lt;- 20 13: putfield #4 // -\u0026gt; this.b 16: aload_0 17: bipush 10 // \u0026lt;- 10 19: putfield #4 // -\u0026gt; this.b 22: aload_0 23: ldc #5 // \u0026lt;- \u0026#34;s2\u0026#34; 25: putfield #3 // -\u0026gt; this.a 28: aload_0 // ------------------------------ 29: aload_1 // \u0026lt;- slot 1(a) \u0026#34;s3\u0026#34; | 30: putfield #3 // -\u0026gt; this.a | 33: aload_0 | 34: iload_2 // \u0026lt;- slot 2(b) 30 | 35: putfield #4 // -\u0026gt; this.b -------------------- 38: return linenumbertable: ... localvariabletable: start length slot name signature 0 39 0 this lcn/itcast/jvm/t3/bytecode/demo3_8_2; 0 39 1 a ljava/lang/string; 0 39 2 b i methodparameters: ... 方法调用 public class demo3_9 { public demo3_9() { } private void test1() { } private final void test2() { } public void test3() { } public static void test4() { } public static void main(string[] args) { demo3_9 d = new demo3_9(); d.test1(); d.test2(); d.test3(); d.test4(); demo3_9.test4(); } } 字节码:\n0: new #2 // class cn/itcast/jvm/t3/bytecode/demo3_9 3: dup 4: invokespecial #3 // method \u0026#34;\u0026lt;init\u0026gt;\u0026#34;:()v 7: astore_1 8: aload_1 9: invokespecial #4 // method test1:()v 12: aload_1 13: invokespecial #5 // method test2:()v 16: aload_1 17: invokevirtual #6 // method test3:()v 20: aload_1 21: pop 22: invokestatic #7 // method test4:()v 25: invokestatic #7 // method test4:()v 28: return new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈 dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 \u0026ldquo;\u0026rdquo;:()v (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量 最终方法(fifinal),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用 invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了😂,静态方法的调用不需要对象 还有一个执行 invokespecial 的情况是通过 super 调用父类方法 分派\n静态分派 所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。\ninvokevirtual指令的运行时解析过程大致分为以下几个步骤\n1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作c。\n2)如果在类型c中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang,ilegalaccesserror异常\n3)否则,按照继承关系从下往上依次对c的各个父类进行第2步的搜索和验证过程。\n4)如果始终没有找到合适的方法,则抛出java.lang,abstractmethoderror异常\n动态分派 由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中(不同实现类调用同一个抽象方法)的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。\n多态的原理 当执行 invokevirtual 指令时,\n先通过栈帧中的对象引用找到对象\n分析对象头,找到对象的实际 class\nclass 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了\n查表得到方法的具体地址\n执行方法的字节码\n异常处理 try-catch\npublic class demo3_11_1 { public static void main(string[] args) { int i = 0; try { i = 10; } catch (exception e) { i = 20; } } } 字节码\npublic static void main(java.lang.string[]); descriptor: ([ljava/lang/string;)v flags: acc_public, acc_static code: stack=1, locals=3, args_size=1 0: iconst_0 1: istore_1 2: bipush 10 4: istore_1 5: goto 12 8: astore_2 9: bipush 20 11: istore_1 12: return exception table: from to target type 2 5 8 class java/lang/exception linenumbertable: ... localvariabletable: start length slot name signature 9 3 2 e ljava/lang/exception; 0 13 0 args [ljava/lang/string; 2 11 1 i i stackmaptable: ... methodparameters: ... } 可以看到多出来一个 exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号\n8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置\n多个 single-catch 块的情况\npublic class demo3_11_2 { public static void main(string[] args) { int i = 0; try { i = 10; } catch (arithmeticexception e) { i = 30; } catch (nullpointerexception e) { i = 40; } catch (exception e) { i = 50; } } } 字节码\npublic static void main(java.lang.string[]); descriptor: ([ljava/lang/string;)v flags: acc_public, acc_static code: stack=1, locals=3, args_size=1 0: iconst_0 1: istore_1 2: bipush 10 4: istore_1 5: goto 26 8: astore_2 9: bipush 30 11: istore_1 12: goto 26 15: astore_2 16: bipush 40 18: istore_1 19: goto 26 22: astore_2 23: bipush 50 25: istore_1 26: return exception table: from to target type 2 5 8 class java/lang/arithmeticexception 2 5 15 class java/lang/nullpointerexception 2 5 22 class java/lang/exception linenumbertable: ... localvariabletable: start length slot name signature 9 3 2 e ljava/lang/arithmeticexception; 16 3 2 e ljava/lang/nullpointerexception; 23 3 2 e ljava/lang/exception; 0 27 0 args [ljava/lang/string; 2 25 1 i i stackmaptable: ... methodparameters: .. 因为异常出现时,只能进入 exception table 中一个分支,所以局部变量表 slot 2 位置被共用 finally\npublic class demo3_11_4 { public static void main(string[] args) { int i = 0; try { i = 10; } catch (exception e) { i = 20; } finally { i = 30; } } } 字节码\npublic static void main(java.lang.string[]); descriptor: ([ljava/lang/string;)v flags: acc_public, acc_static code: stack=1, locals=4, args_size=1 0: iconst_0 1: istore_1 // 0 -\u0026gt; i 2: bipush 10 // try -------------------------------------- 4: istore_1 // 10 -\u0026gt; i | 5: bipush 30 // finally | 7: istore_1 // 30 -\u0026gt; i | 8: goto 27 // return ----------------------------------- 11: astore_2 // catch exceptin -\u0026gt; e ---------------------- 12: bipush 20 // | 14: istore_1 // 20 -\u0026gt; i | 15: bipush 30 // finally | 17: istore_1 // 30 -\u0026gt; i | 18: goto 27 // return ----------------------------------- 21: astore_3 // catch any -\u0026gt; slot 3 ---------------------- 22: bipush 30 // finally | 24: istore_1 // 30 -\u0026gt; i | 25: aload_3 // \u0026lt;- slot 3 | 26: athrow // throw ------------------------------------ 27: return exception table: from to target type 2 5 11 class java/lang/exception 2 5 21 any // 剩余的异常类型,比如 error 11 15 21 any // 剩余的异常类型,比如 error linenumbertable: ... localvariabletable: start length slot name signature 12 3 2 e ljava/lang/exception; 0 28 0 args [ljava/lang/string; 2 26 1 i i stackmaptable: ... methodparameters: ... 可以看到 fifinally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程 练习 - finally 面试题 finally 出现了 return\npublic class demo3_12_2 { public static void main(string[] args) { int result = test(); system.out.println(result); } public static int test() { try { return 10; } finally { return 20; } } } 字节码\npublic static int test(); descriptor: ()i flags: acc_public, acc_static code: stack=1, locals=2, args_size=0 0: bipush 10 // \u0026lt;- 10 放入栈顶 2: istore_0 // 10 -\u0026gt; slot 0 (从栈顶移除了) 3: bipush 20 // \u0026lt;- 20 放入栈顶 5: ireturn // 返回栈顶 int(20) 6: astore_1 // catch any -\u0026gt; slot 1 7: bipush 20 // \u0026lt;- 20 放入栈顶 9: ireturn // 返回栈顶 int(20) exception table: from to target type 0 3 6 any linenumbertable: ... stackmaptable: ... 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准\n至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子\n跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常😱😱😱,可以试一下下面的代码\n尽量不要在finally中使用return\nfinally 对返回值影响\npublic class demo3_12_2 { public static void main(string[] args) { int result = test(); system.out.println(result); } public static int test() { int i = 10; try { return i; } finally { i = 20; } } } 字节码\npublic static int test(); descriptor: ()i flags: acc_public, acc_static code: stack=1, locals=3, args_size=0 0: bipush 10 // \u0026lt;- 10 放入栈顶 2: istore_0 // 10 -\u0026gt; i 3: iload_0 // \u0026lt;- i(10) 4: istore_1 // 10 -\u0026gt; slot 1,暂存至 slot 1,目的是为了固定返回值 5: bipush 20 // \u0026lt;- 20 放入栈顶 7: istore_0 // 20 -\u0026gt; i 8: iload_1 // \u0026lt;- slot 1(10) 载入 slot 1 暂存的值 9: ireturn // 返回栈顶的 int(10) 10: astore_2 11: bipush 20 13: istore_0 14: aload_2 15: athrow exception table: from to target type 3 5 10 any linenumbertable: ... localvariabletable: start length slot name signature 3 13 0 i i 固定了return的值,finally修改变量的值也不会影响返回值 编译期处理 javac这类编译器对代码的运行效率几乎没有任何优化措施,虚拟机设计团队把对性能的优化集中到了后端的即时编译器中,这样可以让那些不是由javac产生的class (如juby、groovy等语言的class 文件)文件也同样能享受到编译器优化所带来的好处,但是javac 做了许多针对java语言编码过程的优化措施来改善程序员的编码风格和提高编码效率。java 中即时编译器在运行期的优化过程对于程序运行来说更重要,而前端编译器在编译期的优化过程对于程序编码来说关系更加密切。\n所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃嘛)\n注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外,编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。\njavac编译器 语义分析与字节码生成\n//方法一带有final修饰 public void foo (final int arg){ final int var = 0; // do something } //方法二没有 final修饰 public void foo(int arg){ int var = 0 ; //do something } 两段代码编译出来的 class 文件是没有任何一点区别的\n原因:局部变量与字段(实例变量、类变量) 是有区别的,它在常量池中没有constant fieldref info 的符号引用,自然就没有访问标志 (access flags)的信息,甚至可能连名称都不会保留下来 (取决于编译时的选项),自然在 class 文件中不可能知道一个局部变量是不是声明为 final了。因此,将局部变量声明为 final,对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保障。\n默认构造器 public class candy1 { } 编译成class后的代码:\npublic class candy1 { // 这个无参构造是编译器帮助我们加上的 public candy1() { super(); // 即调用父类 object 的无参构造方法,即调用 java/lang/object.\u0026#34; \u0026lt;init\u0026gt;\u0026#34;:()v } } 自动拆装箱 这个特性是 jdk 5 开始加入的, 代码片段1 :\npublic class candy2 { public static void main(string[] args) { integer x = 1; int y = x; } } 这段代码在 jdk 5 之前是无法编译通过的,必须改写为 代码片段2 :\npublic class candy2 { public static void main(string[] args) { integer x = integer.valueof(1); int y = x.intvalue(); } } 显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 jdk 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为 代码片段2\n泛型集合取值 泛型也是在 jdk 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 object 类型来处理:\npublic class candy3 { public static void main(string[] args) { list\u0026lt;integer\u0026gt; list = new arraylist\u0026lt;\u0026gt;(); list.add(10); // 实际调用的是 list.add(object e) integer x = list.get(0); // 实际调用的是 object obj = list.get(int index); } } 所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:\n// 需要将 object 转为 integer integer x = (integer)list.get(0); 如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:\n// 需要将 object 转为 integer, 并执行拆箱操作 int x = ((integer)list.get(0)).intvalue(); 擦除的是字节码上的泛型信息,可以看到 localvariabletypetable 仍然保留了方法参数泛型的信息 使用反射,仍然能够获得这些信息 方法重载 public class generictypes{ public static string method(list\u0026lt;string\u0026gt; list){ system.out.println(\u0026#34;invoke method(list\u0026lt;string\u0026gt; list)\u0026#34;); return \u0026#34;\u0026#34;; } public static int method(list\u0026lt;integer\u0026gt; list){ system.out.println(\u0026#34;invoke method(list\u0026lt;integer\u0026gt; list)\u0026#34;); return 1; } public static void main(string[] args){ method(new arraylist\u0026lt;string\u0026gt;()); method(new arraylist\u0026lt;integer\u0026gt;()); } } 代码清单中的重载当然不是根据返回值来确定的,之所以这次能编译和执行成功是因为两个 method方法加入了不同的返回值后才能共存在一个 ciass 文件之中。第 6 章介绍 class 文件方法表 (method info) 的数据结构时曾经提到过,方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择,但是在class 文件格式之中,只要描述符不是完全一致的两个方法就以共存。也就是说,两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个 class文件中的。\n类加载阶段 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(loading)、验证(verifcation)、准备(preparation)、解析(resolution)、初始化(initialization)、使用(using)和卸载(unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(linking),这7个阶段的发生顺序如图7-1所示。\n加载 加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作) 是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这此夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。\n将类的字节码载入方法区中,内部采用 c++ 的 instanceklass 描述 java 类,它的重要 field 有:\n_java_mirror 即 java 的类镜像,例如对 string 来说,就是 string.class,作用是把 klass 暴\n露给 java 使用\nsuper 即父类\nfields 即成员变量\nmethods 即方法\nconstants 即常量池\nclass_loader 即类加载器\nvtable 虚方法表\n_itable 接口方法表\n如果这个类还有父类没有加载,先加载父类\n加载和链接可能是交替运行的\n链接 验证\n验证类是否符合 jvm规范,安全性检查;验证阶段是非常重要的,这个阶段是否严谨,直接决定了 java虚拟机是否能承受恶意代码的攻击,从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中又占了相当大的一部分\n准备\n为 static 变量分配空间,设置默认值\nstatic 变量在 jdk 7 之前存储于 instanceklass 末尾(存储在方法区中),从 jdk 7 开始,存储于 _java_mirror 末尾(存储在堆中) static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,首先,这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 java 堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:\npublic static int value = 123; 那变量 value 在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic 指令是程序被编译后,存放于类构造器0方法之中,所以把value 赋值为 123的动作将在初始化阶段才会执行。 那相对的会有一些“特殊情况”: 如果类字段的字段属性表中存在 constantvalue 属性,那在准备阶段变量 value 就会被初始化为 constantvalue 属性所指定的值,假设上面类变量 value 的定义变为:\npublic static final int value = 123; 编译时javac 将会为 value 生成 constantvalue 属性,在准备阶段虚拟机就会根据 constantvalue的设置将 value 赋值为123。 解析\n解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程\n符号引用(symbolic references): 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在 java 虚拟机规范的 class 文件格式中。 直接引用(direct references): 直接用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同-个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。 初始化 \u0026lt;cinit\u0026gt;()v 方法\n初始化即调用 \u0026lt;cinit\u0026gt;()v ,虚拟机会保证这个类的『构造方法』的线程安全\n初始化阶段,虚拟机规范则是严格规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):\n1)遇到 new、getstatic、putstatic 或 invokestatic 这 4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。\n2)使用java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化则需要先触发其初始化。\n3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。\n4)当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类\n5)当使用jdk 1.7 的动态语言支持时,如果一个java.lang.invoke.methodhandle 实例最后的解析结果 ref_getstatic、ref putstatic、ref invokestatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。\n当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量) 才会初始化。\n静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问\npublic class test{ static{ i = 0;\t//赋值正常编译通过 system.out.print(i); //无法编译通过,‘非法向前引用’ } static int i = 1; } 类构造函数 0 方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 0方法。\n接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成 0-方法。但接口与类不同的是,执行接口的 ()方法不需要先执行父接口的 0方法。只存当父接口中定义的变量使用时,父接口才会初始化另外,接口的实现类在初始化时也一样不会执行接口的 0 方法。\n虚拟机会保证一个类的0) 方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 o 方法其他线程都需要阻塞等待,直到活动线程执行 0 方法完毕。如果在\u0026ndash;个类的0 方法中有耗时很长的操作,就可能造成多个进程阻塞2,在实际应用中这种阻塞往往是很隐蔽的\n发生的时机\n概括得说,类初始化是【懒惰的】\nmain 方法所在的类,总会被首先初始化 首次访问这个类的静态变量或静态方法时 子类初始化,如果父类还没初始化,会引发 子类访问父类的静态变量,只会触发父类的初始化class.forname new 会导致初始化 不会导致类初始化的情况\n访问类的 static fifinal 静态常量(基本类型和字符串)不会触发初始化 类对象.class 不会触发初始化 创建该类的数组不会触发初始化 类加载器的 loadclass 方法 class.forname 的参数 2 为 false 时 实例\nclass a { static int a = 0; static { system.out.println(\u0026#34;a init\u0026#34;); } } class b extends a { final static double b = 5.0; static boolean c = false; static { system.out.println(\u0026#34;b init\u0026#34;); } } public class load3 { static { system.out.println(\u0026#34;main init\u0026#34;); } public static void main(string[] args) throws classnotfoundexception { // 1. 静态常量(基本类型和字符串)不会触发初始化 system.out.println(b.b); // 2. 类对象.class 不会触发初始化 system.out.println(b.class); // 3. 创建该类的数组不会触发初始化 system.out.println(new b[0]); // 4. 不会初始化类 b,但会加载 b、a classloader cl = thread.currentthread().getcontextclassloader(); cl.loadclass(\u0026#34;cn.itcast.jvm.t3.b\u0026#34;); // 5. 不会初始化类 b,但会加载 b、a classloader c2 = thread.currentthread().getcontextclassloader(); class.forname(\u0026#34;cn.itcast.jvm.t3.b\u0026#34;, false, c2); // 1. 首次访问这个类的静态变量或静态方法时 system.out.println(a.a); // 2. 子类初始化,如果父类还没初始化,会引发 system.out.println(b.c); // 3. 子类访问父类静态变量,只触发父类初始化 system.out.println(b.a); // 4. 会初始化类 b,并先初始化类 a class.forname(\u0026#34;cn.itcast.jvm.t3.b\u0026#34;); } } 类加载器 以 jdk 8 为例:\n名称 加载哪的类 说明 bootstrap classloader java_home/jre/lib 无法直接访问 extension classloader java_home/jre/lib/ext 上级为 bootstrap,显示为 null application classloader classpath 上级为 extension 自定义类加载器 自定义 上级为 application 比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。\n这里所指的“相等”,包括代表类的 class 对象的 equals0 方法、isassignablefrom() 方法isinstance0) 方法的返结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。\n启动类加载器 用 bootstrap 类加载器加载类:\npublic class f { static { system.out.println(\u0026#34;bootstrap f init\u0026#34;); } } 执行\npackage cn.itcast.jvm.t3.load; public class load5_1 { public static void main(string[] args) throws classnotfoundexception { class\u0026lt;?\u0026gt; aclass = class.forname(\u0026#34;cn.itcast.jvm.t3.load.f\u0026#34;); system.out.println(aclass.getclassloader()); } } 输出\ne:\\git\\jvm\\out\\production\\jvm\u0026gt;java -xbootclasspath/a:. cn.itcast.jvm.t3.load.load5 bootstrap f init null -xbootclasspath 表示设置 bootclasspath\n其中 /a:. 表示将当前目录追加至 bootclasspath 之后\n可以用这个办法替换核心类\njava -xbootclasspath:\njava -xbootclasspath/a:\u0026lt;追加路径\u0026gt;\njava -xbootclasspath/p:\u0026lt;追加路径\u0026gt;\n扩展类加载器 public class g { static { system.out.println(\u0026#34;ext g init\u0026#34;); } } 执行\npublic class load5_2 { public static void main(string[] args) throws classnotfoundexception { class\u0026lt;?\u0026gt; aclass = class.forname(\u0026#34;cn.itcast.jvm.t3.load.g\u0026#34;); system.out.println(aclass.getclassloader()); } } 打个 jar 包\ne:\\git\\jvm\\out\\production\\jvm\u0026gt;jar -cvf my.jar cn/itcast/jvm/t3/load/g.class 已添加清单 正在添加: cn/itcast/jvm/t3/load/g.class(输入 = 481) (输出 = 322)(压缩了 33%) 将 jar 包拷贝到 java_home/jre/lib/ext\n重新执行 load5_2\n输出\next g init sun.misc.launcher$extclassloader@29453f44 双亲委派模式 双亲委派模型的工作过程是:如果个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器丢完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范中没有找到所需的类)时,子加载器才会尝试自己丢加载。\n所谓的双亲委派,就是指调用类加载器的 loadclass 方法时,查找类的规则\n注意\n这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系\n执行流程为:\nsun.misc.launcher$appclassloader //1 处, 开始查看已加载的类,结果没有\nsun.misc.launcher$appclassloader // 2 处,委派上级sun.misc.launcher$extclassloader.loadclass()\nsun.misc.launcher$extclassloader // 1 处,查看已加载的类,结果没有\nsun.misc.launcher$extclassloader // 3 处,没有上级了,则委派 bootstrapclassloader查找\nbootstrapclassloader 是在 java_home/jre/lib 下找 h 这个类,显然没有\nsun.misc.launcher$extclassloader // 4 处,调用自己的 findclass 方法,是在java_home/jre/lib/ext 下找 h 这个类,显然没有,回到 sun.misc.launcher$appclassloader的 // 2 处\n继续执行到 sun.misc.launcher$appclassloader // 4 处,调用它自己的 fifindclass 方法,在classpath 下查找,找到了\n线程上下文类加载器 有了线程上下文类加载器,就可以做一些“舞弊”的事情了,jndi服务使用这个线程上下文类加载器去加载所需要的 spi代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,\n使用 class.forname 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载 它就是大名鼎鼎的 service provider interface (spi) 约定如下,在 jar 包的 meta-inf/services 包下,以接口全限定名名为文件,文件内容是实现类名称 serviceloader\u0026lt;接口类型\u0026gt; allimpls = serviceloader.load(接口类型.class); iterator\u0026lt;接口类型\u0026gt; iter = allimpls.iterator(); while(iter.hasnext()) { iter.next(); } 来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:\njdbc servlet 初始化器 spring 容器 dubbo(对 spi 进行了扩展) 线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由class.forname 调用了线程上下文类加载器完成类加 为了打破双亲委派模式 自定义类加载器 问问自己,什么时候需要自定义类加载器\n1)想加载非 classpath 随意路径中的类文件\n2)都是通过接口来使用实现,希望解耦时,常用在框架设计\n3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器\n步骤:\n继承 classloader 父类\n要遵从双亲委派机制,重写 fifindclass 方法\n\t注意不是重写 loadclass 方法,否则不会走双亲委派机制\n读取类文件的字节码\n调用父类的 defifineclass 方法来加载类\n使用者调用该类加载器的 loadclass 方法\n运行期优化 解释器与编译器 解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率\n编译器和解释器的主要区别在于编译器需要预先将源代码转换成目标代码,并生成可执行文件,而解释器则在运行时直接读取源代码并解释执行\n即时编译 分层编译\npublic class jit1 { public static void main(string[] args) { for (int i = 0; i \u0026lt; 200; i++) { long start = system.nanotime(); for (int j = 0; j \u0026lt; 1000; j++) { new object(); } long end = system.nanotime(); system.out.printf(\u0026#34;%d\\t%d\\n\u0026#34;,i,(end - start)); } } } 0 96426 1 52907 2 44800 3 119040 4 65280 5 47360 6 45226 7 47786 8 48640 9 60586 10 42667 11 48640 ... 82 18774 83 17067 84 21760 85 23467 86 17920 87 17920 88 18774 89 18773 90 19200 91 20053 92 18347 ... 157 854 158 853 159 853 160 854 原因是什么呢?\njvm 将执行状态分成了 5 个层次:\n0 层,解释执行(interpreter)\n1 层,使用 c1 即时编译器编译执行(不带 profifiling)\n2 层,使用 c1 即时编译器编译执行(带基本的 profifiling)\n3 层,使用 c1 即时编译器编译执行(带完全的 profifiling)\n4 层,使用 c2 即时编译器编译执行\nprofifiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等\n分层编译流程\n第0层,程序解释执行,解释器不开启性能监控功能 (profling),可触发第1层编译\n第1层,也称为 c1 编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。\n第2层(或2层以上),也称为 c2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化\n实施分层编译后,client compiler 和 server compiler 将会同时工作,许多代码都可能会被多次编译,用 client compiler 取更高的编译速度,用 server compiler 来获取更好的编译质量,在解释执行的时候也无须再承担收集性能监控信息的任务。\n即时编译器(jit)与解释器的区别\n解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释\njit 是将一些字节码编译为机器码,并存入 code cache,下次遇到相同的代码,直接执行,无需再编译\n解释器是将字节码解释为针对所有平台都通用的机器码\njit 会根据平台类型,生成平台特定的机器码\n对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运\n行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速\n度。 执行效率上简单比较一下 interpreter \u0026lt; c1 \u0026lt; c2,总的目标是发现热点代码(hotspot名称的由\n来),优化之\n方法内联\nprivate static int square(final int i) { return i * i; } system.out.println(square(9)); 如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置:\nsystem.out.println(9 * 9); 还能够进行常量折叠(constant folding)的优化\nsystem.out.println(81); 反射优化 public class reflect1 { public static void foo() { system.out.println(\u0026#34;foo...\u0026#34;); } public static void main(string[] args) throws exception { method foo = reflect1.class.getmethod(\u0026#34;foo\u0026#34;); for (int i = 0; i \u0026lt;= 16; i++) { system.out.printf(\u0026#34;%d\\t\u0026#34;, i); foo.invoke(null); } system.in.read(); } } foo.invoke 前面 0 ~ 15 次调用使用的是 methodaccessor 的 nativemethodaccessorimpl 实现\n当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到类名为 sun.reflflect.generatedmethodaccessor1\npublic object invoke(object object, object[] arrobject) throws invocationtargetexception { // 比较奇葩的做法,如果有参数,那么抛非法参数异常 block4 : { if (arrobject == null || arrobject.length == 0) break block4; throw new illegalargumentexception(); } try { // 可以看到,已经是直接调用了😱😱😱 reflect1.foo(); // 因为没有返回值 return null; } catch (throwable throwable) { throw new invocationtargetexception(throwable); } catch (classcastexception | nullpointerexception runtimeexception) { throw new illegalargumentexception(object.super.tostring()); } } } ","date":"2023-03-31","permalink":"https://www.holatto.com/posts/jvm/","summary":"什么是JVM 定义: Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境) 好处: 一次编写,到处运行 自动内存管理,垃圾回收功能 数组下标越界检查 多态 比较:jvm jre","title":"初学jvm虚拟机"},{"content":"hello world,this is my first blog!!!!\n","date":"2023-03-31","permalink":"https://www.holatto.com/posts/how-do-i-blog/","summary":"Hello World,this is my first blog!!!!","title":"我的第一个博客"},]
✖