好懂事一男的

好懂事一男的 查看完整档案

北京编辑  |  填写毕业院校京东  |  java开发工程师 编辑填写个人主网站
编辑

微信公众号: java小杰要加油
目前就职于京东

个人动态

好懂事一男的 发布了文章 · 3月7日

多图慎入,从四层模型看网络是怎么连接的

大家好,我是公众号:java小杰要加油
今天来分享一个关于计算机网络的知识点——网络到底是怎么连接的?
  • 话不多说,直接开车

浏览器生成消息且发送

  • 发送一个消息的总体流程如下

生成HTTP请求消息

举个栗子,当我们在浏览器输入https://www.jdl.cn/img/servic...网络地址的时候

  • 浏览器首先会对URL进行解析

    • https:表示访问数据源的机制,也就是协议
    • www.jdl.cn: web服务器名称
    • img :表示目录名
    • service.843585b7.png:表示文件名

然后就要生成HTTP消息了,它大概长这样


这些字段具体内容是什么可以参考这篇文章五千来字小作文,是的,我们是有个HTTP。

DNS域名解析为IP地址

浏览器生成了这个HTTP消息后,它要往哪里发送呢?当然是服务器啦,所以就要解析这个域名对应的是哪台服务器,IP地址是什么,因为IP地址不好记,所以才有了对应的域名,便于我们人类记忆。

  1. 浏览器会检查缓存有没有这个域名对应的ip地址
  2. 操作系统会检查缓存(就是我们平常说的hosts文件)
  3. 操作系统会发送给本地区的DNS服务器,让它帮忙解析下

DNS服务器接受来自客户端的查询,包括以下三个内容

  • 域名: 服务器,邮件服务器的名称
  • Class: 在最早设计DNS时,DNS在互联网以外的其他网络中的应用也被考虑到了,而Class就是用来识别网络信息的,不过如今除了互联网就没有其他网络了,因此Class的值永远代表互联网的IN
  • 记录类型: 表示域名对应何种记录类型

    • A记录时,域名直接对应IP地址
    • CNAME时,此域名对应其他域名
    • MX时,表示域名对应的是邮件服务器

    对于不同的记录类型,响应数据也不一样

域名的层次结构

  • 越靠右层次越高,从右向左一级一级的划分 : 例如 www.jdl.cn 就是cn->jdl->www
  • 具有这种层次结构的域名信息都会注册到DNS服务器中,而每个域都是作为一个整体来处理的

客户端和DNS服务器交互流程大概如下

  • 上级DNS服务器中要注册其下级域的DNS服务器IP地址,然后上级DNS服务器IP地址要注册到更上一级的DNS服务器中,此次类推
  • 根域的DNS服务器信息保存到互联网中所有的DNS服务器中,这样的话,所有的DNS服务器都会找到根域,然后一级一级的往下找,直到找到自己想要的那个域名
  • 分配给根域的IP地址仅有13个,就是顶级域名(com,cn等)对应的ip地址


具体交互就是下面这样


但是一台服务器存不下这么多,所以一般都是DNS服务器大接力来寻找这个ip地址,图如下

客户端找到最近的DNS服务器,查找www.jdl.cn的信息,可是最近的DNS服务器没有这个信息,就转发到了根域服务器下,经过判断发现是cn的顶级域名的,于是根域DNS服务器会返回它所管理的cn域中的DNS服务器的ip地址,接下来,最近的这个DNS服务器又回去访问com域名的服务器,以此类推,最终会找到 www.jdl.cn这个服务器的IP地址

委托协议栈发送消息

知道了IP地址后,就可以委托操作系统内部的协议栈向这个目标IP地址发送消息了

  • 协议栈的内部结构

  • 浏览器、邮件等一般应用程序收发数据时用TCP
  • DNS查询等收发较短的控制数据用UDP

网络分层

  • OSI七层模型
开放式系统互联通信参考模型(英语:Open System Interconnection Reference Model,缩写为 OSI),简称为OSI模型(OSI model),一种概念模型,由国际标准化组织提出,一个试图使各种计算机在世界范围内互连为网络的标准框架。定义于ISO/IEC 7498-1。
  • TCP/IP四次模型

    • 应用层: HTTP、DNS、FTP
    • 传输层: TCP、UDP
    • 网络层: IP
    • 网络接口层
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)TCP/IP协议不仅仅指的是TCP 和IP两个协议,而是指一个由FTP、SMTP、TCP、UDP、IP等协议构成的协议簇, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议

客户端服务器传递数据流程

  • 一个数据包从客户端到服务端中间经过每一层都需要加工处理
  • 客户端这边需要不断的给数据包添加头部
  • 服务端这边需要不断的拆分这个数据包

三次握手

当两台计算机要传递数据的时候,一定要先连接,得经过TCP三次握手吧(仅仅指指走TCP协议需要连接的),我们平常都说TCP连接要经过三次握手,我们就来看一下到底什么是TCP三次握手,如图所示

  • 客户端要发送的时候,主动从closed状态打开,服务器启动后就一直处于监听LISTEN状态
  • 客户端发送 SYN = 1,seq = x 给服务端,客户端处于SYN_SEND状态。
  • 服务端收到后给客户端发送 SYN = 1,ACK =1, seq = y,ack = x+1。此时服务端处于SYN_RCVD状态
  • 客户端收到后发送ACK =1, seq = x+1,ack = y+1给服务器,此时客户端状态是ESTAB-LISHED
  • 服务端收到后状态变为ESTAB-LISHED
  • 三次握手通过后,就代表客户端和服务端可以传递数据包进行交互啦
  • 我们说到SYN,ACK,seq,ack这些又是什么呢?这些其实是TCP数据包里的属性,我们接着往下看(在传输层中有解释)

应用层

HTTP数据包拆分

  • 一般HTTP请求消息不会太长,一个网络包就能装的下
  • 发送缓冲区中的数据如果超过MSS的长度,就会被以MSS长度进行拆分放进单独的网络包中
  • MTU(Maximum Transmission Unit): 一个网络包的最大长度,以太网中一般是1500字节
  • MSS(Maximum Segment Size): 除去头部之后,一个网络包所容纳的TCP数据的最大长度

传输层

  • 然后上面应用层的这个网络包再加上TCP头部

TCP报文格式

  • 源端口号(16位): 发送网络包的端口号
  • 目的端口号(16位): 网络包的接受方的端口号
  • 序号(发送数据的顺序编号)(32位): 发送方告知接收方已经收到了所有数据的第几个字节
  • 确认序号(接收数据的顺序编号)(32位): 接收方告知发送方接收方已经收到了所有数据的第几个字节
  • 头部长度(4位): 表示数据的起始部分,数据偏移量
  • 保留(6位): 该字段为保留,现在未使用
  • 控制位(6位): 该字段中的每个比特位分别表示以下通信控制的含义

    • URG: 表示紧急指针字段有效
    • ACK: 表示接收数据序号字段有效,一般表示数据已被接收方收到
    • PSH: 表示通过flush操作发送的数据
    • RST: 强制断开连接,用于异常中断的情况
    • SYN: 发送方和接收方相互确认序号,表示连接操作
    • FIN: 表示断开操作
  • 窗口大小(16位): 接收方告知发送方窗口大小(即无需等待确认可一起发送的数据)
  • 校验和(16位): 用来检查是否出现错误
  • 紧急指针(16位): 表示应急处理的数据位置
  • 可选字段(可变长度): 除了上面的固定头部字段外,还可以添加可选字段,但除了连接操作外,很少使用可选字段
还记得三次握手提到过的各种序号吗,就是这个报文里的属性

网络层

  • 然后上面这个网络包再加上IP头部

IP报文格式

  • 版本号(4比特): IP协议版本号,目前是版本4
  • 头部长度(4比特): IP头部的长度,可选字段可导致头部长度的变化,因此这里需要指定头部的长度
  • 服务类型(TOS)(8比特): 表示包传输优先级。最初的协议规格里对这个参数的定义很模糊,最近DIFFServ规则重新定义了这个字段的用法
  • 总长度(16比特): 表示IP消息的总长度
  • ID号(16比特): 用于识别包的编号,一般为的序列号。如果一个包被IP分片,则所有分片都拥有相同的ID
  • 标志(Flag)(3比特): 该字段有3个比特,其中2个比特有效,分别代表是否允许分片,以及当前分片是否为分片包
  • 分片偏移量(13比特): 表示当前包的内容为整个IP消息的第几个字节开始的内容
  • 生存时间(TTL)(8比特): 表示包的生存时间,这是为了避免网络出现回环时一个包永远在网络中打转。每经过一个路由器,这个值就会减一,减到0的是hi这个包就会被丢弃
  • 协议号(8比特): 协议号表示协议的类型(以下均为16进制)

    • TCP: 06
    • UDP: 17
    • ICMP: 01
  • 头部校验和(16比特): 用于检查错误,现在已经不在使用
  • 发送方IP地址(32比特): 网络包发送方的IP地址
  • 接收方IP地址(32比特): 网络包接收方的IP地址
  • 可选字段(可变长度): 除了上面的固定头部字段外,还可以添加可选字段,但除了连接操作外,很少使用可选字段
  • 然后这个网络包再加上MAC头部

MAC数据包

  • 接收方MAC地址(48比特): 网络包接收方的MAC地址,在局域网中使用这一地址来传输网络包
  • 发送方MAC地址(48比特): 网络包发送方的MAC地址,接收方通过它来判断是谁发送了这个网络包
  • 以太类型(16比特): 使用的协议类型。下面是一些常见的类型,一般在TCP/IP通信中只是用0800和0806这两种。

    • 0000-05DC: IEEE 802.3
    • 0800 : IP协议
    • 0806 : ARP协议
    • 86DD : IPV6

MAC地址 VS IP地址

  • IP头部前面还会加上MAC头部
  • 为什么需要MAC数据包呢?因为在以太网的世界中,TCP/IP这个思路是行不通的。
  • 以太网在判断网络包目的地时和TCP/IP的方式不同,因此必须采用想匹配的方式才能在以太网中将包发往目的地,而MAC地址就是干这个的
  • 发送方MAC地址:MAC地址是写在网卡生产时写入ROM里的,只需要将这个值读取出来写入MA头部就好了
发送方的MAC地址还比较容易获取到,但是接收方的MAC地址就不太容易获取到了

ARP广播

  • ARP :Addresss Resolution Protocal 地址解析协议
  • 根据IP地址查询接收方MAC地址的时候会用到ARP广播
  • 在同一个子网中,利用广播对所有设备提问 XXX这个ip地址是谁的,其他设备发现自己的ip地址是这个xxx的话,那么他就会把它的MAC地址告诉提问者,这样就会检测到接收方的MAC地址了,如果发现自己的ip地址不是这个XXX,那么则会丢弃这个消息并不去理会。

  • 如果每次都去广播的话,那么网络中就会增加很多ARP包,所以为了提高效率,我们有ARP缓存在内存中。查询之前先去查询ARP缓存。
  • 当目的地的IP地址对应的MAC地址变了的话,那么这个MAC缓存就会出问题,所以为了避免这种问题发生,这个缓存几分钟后会被删除,非常简单粗暴。

    • 静态ARP: 手工维护,不会自动失效
    • 动态ARP: 会过段时间自动失效(文中说的就是它)
  • IP 模块负责添加如下两个头部:

    • MAC头部: 以太网用的头部,包含MAC地址
    • IP头部: IP用的头部,包含IP地址

总体数据包

这个时候的数据包变成了这个样子

  • MTU(Maximum Transmission Unit): 一个网络包的最大长度,以太网中一般是1500字节
  • MSS(Maximum Segment Size): 除去头部之后,一个网络包所容纳的TCP数据的最大长度
  • 然后这数据包,沿着网卡出去,到集线器,路由器一顿传输(中间涉及到电信号转换等等),到达服务端那边,再一层一层的扒皮(前往中说过,一层一层的拆分数据包)

断开连接

四次挥手

两台计算机最后连接结束后要断开连接,进行四次挥手

其实三次握手四次挥手还有好多好多知识点要说,像什么为什么握手需要三次,而挥手需要四次啦这些问题,以后我会单独和大家聊这个,记得收看呀

image

查看原文

赞 0 收藏 0 评论 0

好懂事一男的 发布了文章 · 2月21日

mysql中的各种锁把我搞糊涂啦~

大家好,我是公众号:java小杰要加油
今天来分享一个关于mysql的知识点——mysql中的锁
  • 话不多说,直接开车

事务并发访问情况

读-读 情况

  • 并发事务读取相同的数据,并不会对数据造成影响,允许并发读

写-写 情况

  • 多事务并发写写时会发生脏写的情况,不过任何一个事务隔离级别都不允许此情况发生,通过加锁来杜绝脏写

脏写

  • 事务T1 将数据改成了A,但是还未提交,可此时事务T2又将数据改成了B,覆盖了事务T1的更改,T1更新丢失,这种情况叫做脏写

加锁

  • 例如,现在事务T1,T2对这条记录进行并发更改,刚才说是隔离级别是通过加锁来杜绝此脏写的,流程如下


这个锁结构中有两个比较关键的信息(其实还有很多信息,后面再聊)

  • trx信息:表示这个锁结构是和哪个事务所关联的
  • is_waiting信息:表示当前事务是否正在等待
Q: 能描述一下两个事务并发修改同一条数据时,mysql这个锁是怎么避免脏写的吗?

A :事务T1在更改这条数据前,就先内存中生成一把锁与此数据相关联(is_waiting为false,代表没有等待),然后咔咔一顿操作更改数据,这个时候,事务T2来了,发现此记录已经有一把锁与之相关联了(就是T1那一把锁),然后就开始等待(is_waiting为true代表正在等待),事务T1更改完数据提交事务后,就会把此事务对应的所结构释放掉,然后检测一下还有没有与此记录相关联的锁,结果发现T2还在苦苦的等待,就把T2的锁结构的(is_waiting为false,代表没有等待)然后把T2事务对应的线程唤醒,T2获取锁成功继续执行,总体流程如上。

读-写 /写-读 情况

在读-写 / 写 -读的情况下会出现脏读,不可重复读,幻读的现象,不同的隔离级别可以避免不同的问题,具体相关内容可以看小杰的这篇文章 京东面试官问我:“聊聊MySql事务,MVCC?”

不过贴心的我还是列出来了 注:√代表可能发生,×代表不可能发生

隔离级别脏读不可重复读幻读
读未提交(read uncommitted RU)
读提交(read committed RC)×
可重复读(repeatable read RR)××
串行化(serializable )×××

但是 RR在某些程度上避免了幻读的发生

怎么避免脏读、不可重复读、幻读这些现象呢?其实有两种方案

  • 方案一 :读操作使用MVCC写操作进行加锁
  • mvcc里面最重要的莫过于ReadView了,它的存在保证了事务不可以读取到未提交的事务所作的更改,避免了脏读。
  • 在RC隔离级别下,每次select读操作都会生成ReadView
  • 在RR隔离级别下,只有第一次select读操作才会生成ReadView,之后的select读操作都复用这一个ReadView
  • 方案二:读写操作都用加锁
某些业务场景不允许读取旧记录的值,每次读取都要读取最新的值。
例如银行取款事务中,先把余额读取出来,再对余额进行操作。当这个事务在读取余额时,不允许其他事务对此余额进行访问读取,直到取款事务结束后才可以访问余额。所以在读数据的时候也要加锁

锁分类

当使用读写都加锁这个方案来避免并发事务写-写读-写写-读时而产生的脏读不可重复读幻读现象时,那么这个锁它就要做到,读读时不相互影响,上面三种情况时要相互阻塞,这时锁也分了好几类,我们继续往下看

锁定读

  • 共享锁(Shared Lock):简称S锁,在事务要读取一条记录时,需要先获取该记录的S锁
  • 独占锁(Exclusive Lock):简称X锁,也称排他锁,在事务要改动一条记录时,需要先获取该记录的X锁

他们之间兼容关系如下 √代表可以兼容,×代表不可兼容

兼容性S锁X锁
S锁×
X锁××

事务T1获取某记录的S锁后,

  • 事务T2也可以获取此记录的S锁,(兼容)
  • 事务T2不可以获取此记录的X锁,直到T1提交后将S锁释放 (不兼容)

事务T1获取某记录的X锁后,

  • 事务T2不可以获取此记录的S锁,直到T1提交后将X锁释放 (不兼容)
  • 事务T2不可以获取此记录的X锁,直到T1提交后将X锁释放 (不兼容)

锁定读语句

SELECT .. LOCK IN SHARE MODE   # 对读取的记录添加S锁

SELECT .. FOR UPDATE # 对读取的记录添加X锁

多粒度锁

前面提到的锁都是针对记录的,其实一个事务也可以在表级进行加锁(S锁、X锁)

  • T1给表加了S锁,那么

    • T2可以继续获取此表的S锁
    • T2可以继续获取此表中的某些记录的S锁
    • T2不可以继续获取此表的X锁
    • T2不可以继续获取此表中的某些记录的X锁
  • T1给表加了X锁,那么

    • T2不可以继续获取此表的S锁
    • T2不可以继续获取此表中的某些记录的S锁
    • T2不可以继续获取此表的X锁
    • T2不可以继续获取此表中的某些记录的X锁

可是怎么可能平白无故的就给表加锁呢,难道没什么条件吗?答案是肯定有条件的

  • 若想给表加S锁,得先确保表中记录没有X锁
  • 若想给表加X锁,得先确保表中记录没有X锁和S锁

但是这个怎么确保呢?难道要一行一行的遍历表中的所有数据吗?当然不是啦,聪明的大佬们想出了下面这两把锁

  • 意向共享锁(Intention Shared Lock):简称IS锁,当事务准备在某记录上加S锁时,需要先在表级别加上一个IS锁
  • 意向独占锁(Intention Exclusive Lock):简称IX锁,当事务准备在某记录上加X锁时,需要先在表级别加上一个IX锁

让我们来看下加上这两把锁之后的效果是什么样子的

  • 当想给记录加S锁时,先给表加一个IS锁,然后再给记录加S锁

  • 当想给记录加X锁时,先给表加IX锁,然后再给记录加X锁

然后 经过上面的操作之后

  • 如果想给表加S锁,先看下表加没加IX锁,如果有的话,则表明此表中的记录有X锁,则需要等到IX锁释放掉后才可以加S锁

  • 如果想给表加X锁,先看下表加没加IS锁或者IX锁,如果有的话,则表明此表中的记录有S锁或者X锁,则需要等到IS锁或者IX锁释放掉后才可以加X锁

这几种锁的兼容性如下表

兼容性IS锁(表级锁)S锁IX锁(表级锁)X锁
IS锁(表级锁)×
S锁××
IX锁(表级锁)××
X锁××××
  • IS、IX锁都是表级锁,他们可以共存。
  • 他们的提出仅仅是为了在之后加表级别的S锁或者X锁时可以快速判断表中的记录是否被上锁,避免用遍历的方式来查看一行一行的去查看而已

InnoDB中的行级锁

Record Lock(记录锁)

  • 官方名字 LOCK_REC_NOT_GAP
  • 仅仅锁住一条记录
  • 有S型和X型之分

Gap Lock(间隙锁)

  • 官方名字 LOCK_GAP
  • 给某记录加此锁后,阻塞数据在此记录和上一个记录的间隙插入,但是不锁定此记录
  • 有S型和X型之分,可是并没有什么区别他们的作用是相同的,gap锁的作用仅仅是为了防止插入幻影记录而已,如果对一条记录加了gap锁(无论S/X型)并不会限制其他事务对这条记录加Record Lock或者Gap Lock

Next-Key Lock(记录锁+间隙锁)

  • 官方名字 LOCK_ORDINARY
  • 既可以锁住某条记录,又可以组织其他事务在该记录面前插入新记录

Insert Intention Lock(插入意向锁锁)

  • 官方名字 LOCK_INSERT_INTENTION
  • 事务在插入记录时,如果插入的地方加了gap锁,那么此事务需要等待,此时此事务在等待时也需要生成一个锁结构,就是插入意向锁

锁内存结构

  • 我们难道锁一条记录就要生成一个锁结构吗?

当然不是!

一个锁结构

如果被加锁的记录符合下面四条状态的话,那么这些记录的锁则会合到一个锁结构

  • 在同一个事务中进行加锁操作
  • 被加锁的记录在同一个页面中
  • 加锁的类型是一样的
  • 等待的状态是一样的

锁结构信息

然后我们再来依此看下这个所结构每个部分的信息都是什么意思

  • 锁所在的事务信息:无论是表级锁还是行级锁,一个锁属于一个事务,这里记载着该锁对应的信息
  • 索引信息:对于行级锁来说,需要记录一下加锁的记录属于哪个索引
  • 表锁/行锁信息:行级锁

    • Space_ID:记录所在的表空间

    * Page Number:记录所在的页号

    • n_bits:一条记录对应着一个比特;一个页面包含多条记录,用不同的比特来区分到底是那一条记录加了锁,有个计算公式如下(公式中是取商)n_bits = (1+(n_recs+LOCK_PAGE_BITMAP_MARGIN)/ 8)x 8LOCK_PAGE_BITMAP_MARGIN是固定的值为64,n_recs指当前界面一共有多少条记录(包含伪记录以及在垃圾链表中的记录),
  • type_mode:32比特的数

    • lock_mode(锁模式):低4比特位表示

      • LOCK_AUTO_INC(十进制的4):表示AUTO-INC锁
      • LOCK_IS(十进制的0):表示共享意向锁,IS锁
      • LOCK_IX(十进制的1):表示独占意向锁,IX锁
      • LOCK_S(十进制的2):表示共享锁,也就是S锁
      • LOCK_X(十进制的3):表示独占锁,也就是X锁
    • lock_type(锁类型):第5~8比特位表示

      • LOCK_TABLE(十进制的1):当第5比特位设置为1时,表示表级锁
      • LOCK_REC(十进制的32):当第6比特位设置为1时,表示行级锁
    • rec_lock_type(行锁的具体类型):其余的比特位表示

      • LOCK_ORDINARY(十进制的0):表示next-key锁
      • LOCK_GAP(十进制的512):当第10比特位是1时,表示gap锁
      • LOCK_REC_NOT_GAP(十进制的1024):也就是当第11比特设置为1时,表示Record Lock(记录锁)
      • LOCK_INSERT_INTENTION(十进制的2048):也就是当第12比特设置为1时,表示Insert Intention Lock(插入意向锁)
      • LOCK_WAIT(十进制的256):也就是当

        • 第9比特设置为1时,表示is_waiting为true,即当前事务获取锁失败,处于等待状态
        • 第9比特设置为0时,表示is_waiting为false,即当前事务获取锁成功
    • 其他信息:此文章不讨论
    • 一堆比特位:此文章不讨论

    举个例子

    事务T1 要给user表中的记录加锁,假设这些记录存储在表空间号为20,页号为21的页面上,T1给id=1的记录加S型Record Lock锁,假如当前页面一共有5条记录(3条用户记录和2条伪记录)

    过程:先给表加IS锁,不过我们现在不关心,只关心行级锁
    具体生成的所结构如下图所示

    最后

    • 快过年啦,小杰可能也需要休息一下下,因为最近都周更(虽然上周有点事没更,打脸),周末完全没有其余时间了
    • 感觉和朋友家人们联系有点少了,过年回家巩固下感情和朋友们聊聊天吹吹牛逼,顺便维护下峡谷的治安
    • 最后祝关注java小杰要加油的宝贝儿们
    • 脱单暴富事事顺,升职加薪牛哄哄!

    好文推荐

    查看原文

    赞 1 收藏 1 评论 0

    好懂事一男的 发布了文章 · 1月26日

    京东面试官问我:“聊聊MySql事务,MVCC?”

    大家好,我是公众号:java小杰要加油
    今天来分享一个京东面试真题,也是这是我前阵子听我旁边高T(高,实在是高)面试候选人的时候问的一个问题,他问,你能说说 mysql的事务吗? MVCC有了解吗?
    • 话不多说,直接开干

    事务定义及四大特性

    • 事务是什么?

      就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元。

    • 事务的四大特性(简称ACID):

      • 原子性(Atomicity):一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
      • 一致性(Consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
      • 隔离性(Isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰.
      • 持久性(Durability):指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,接下来的其他操作或故障不应该对其有任何影响。

    事务中常见问题

    • 脏读(dirty read):就是一个A事务即便没有提交,它对数据的修改也可以被其他事务B事务看到,B事务读到了A事务还未提交的数据,这个数据有可能是错的,有可能A不想提交这个数据,这只是A事务修改数据过程中的一个中间数据,但是被B事务读到了,这种行为被称作脏读,这个数据被称为脏数据
    • 不可重复读(non-repeatable read):在A事务内,多次读取同一个数据,但是读取的过程中,B事务对这个数据进行了修改,导致此数据变化了,那么A事务再次读取的时候,数据就和第一次读取的时候不一样了,这就叫做不可重复读
    • 幻读(phantom read):A事务多次查询数据库,结果发现查询的数据条数不一样,A事务多次查询的间隔中,B事务又写入了一些符合查询条件的多条数据(这里的写入可以是update,insert,delete),A事务再查的话,就像发生了幻觉一样,怎么突然改变了这么多,这种现象这就叫做幻读

    隔离级别——产生问题的原因

    多个事务互相影响,并没有隔离好,就是我们刚才提到的事务的四大特性中的 隔离性(Isolation) 出现了问题 事务的隔离级别并没有设置好,下面我们来看下事务究竟有哪几种隔离级别

    • 隔离级别

      • 读未提交(read uncommitted RU): 一个事务还没提交时,它做的变更就能被别的事务看到
      • 读提交(read committed RC): 一个事务提交之后,它做的变更才会被其他事务看到。
      • 可重复读(repeatable read RR): 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
      • 串行化(serializable ): 顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

    我们来看个例子,更加直观的了解这四种隔离级别和上述问题脏读,不可重复读,幻读的关系

    下面我们讨论下当事务处于不同隔离级别情况时,V1,V2,V3分别是什么不同的值吧

    • 读未提交 (RU): A事务可以读取到B事务修改的值,即便B事务没有提交。所以V1就是200

      • V1 : 200
      • V2 : 200
      • V3 : 200
    • 读提交(RC): 当B事务没有提交的时候,A事务不可以看到B事务修改的值,只有提交以后才可以看到

      • V1 : 100
      • V2 : 200
      • V3 : 200
    • 可重复读(RR): A事务多次读取数据,数据总和第一次读取的一样,

      • V1 : 100
      • V2 : 100
      • V3 : 200
    • 串行化(S): 事务A在执行的时候,事务B会被锁住,等事务A执行结束后,事务B才可以继续执行

      • V1 : 100
      • V2 : 100
      • V3 : 200

    MVCC原理

    MVCC(Multi-Version Concurrency Control)多版本并发控制,是数据库控制并发访问的一种手段。

    • 特别要注意MVCC只在 读已提交(RC)可重复度(RR) 这两种事务隔离级别下才有效
    • 数据库引擎(InnoDB) 层面实现的,用来处理读写冲突的手段(不用加锁),提高访问性能

    MVCC是怎么实现的呢?它靠的就是版本链一致性视图

    1. 版本链

    • 版本链是一条链表,链接的是每条数据曾经的修改记录

    那么这个版本链又是如何形成的呢,每条数据又是靠什么链接起来的呢?

    其实是这样的,对于InnoDB存储引擎的表来说,它的聚簇索引记录包含两个隐藏字段

    • trx_id: 存储修改此数据的事务id,只有这个事务操作了某些表的数据后当更改操作发生的时候(update,delete,insert),才会分配唯一的事务id,并且此事务id是递增的
    • roll_pointer: 指针,指向上一次修改的记录
    • row_id(非必须): 当有主键或者有不允许为null的unique键时,不包含此字段

    假如说当前数据库有一条这样的数据,假设是事务ID为100的事务插入的这条数据,那么此条数据的结构如下

    后来,事务200,事务300,分别来修改此数据

    时间Ttrx_id 200trx_id 300
    T1开始事务开始事务
    T2更改名字为A
    T3更改名字为B
    T4提交事务更改名字为C
    T6提交事务

    所以此时的版本链如下

    我们每更改一次数据,就会插入一条undo日志,并且记录的roll_pointer指针会指向上一条记录,如图所示

    1. 第一条数据是小杰,事务ID为100
    2. 事务ID为200的事务将名称从小杰改为了A
    3. 事务ID为200的事务将名称从A又改为了B
    4. 事务ID为300的事务将名称从B又改为了C

    所以串成的链表就是 C -> B -> A -> 小杰 (从最新的数据到最老的数据)

    2. 一致性视图(ReadView)

    需要判断版本链中的哪个版本是是当前事务可见的,因此有了一致性视图的概念。其中有四个属性比较重要

    • m_ids: 在生成ReadView时,当前活跃的读写事务的事务id列表
    • min_trx_id: m_ids的最小值
    • max_trx_id: m_ids的最大值+1
    • creator_trx_id: 生成该事务的事务id,单纯开启事务是没有事务id的,默认为0,creator_trx_id是0。

    版本链中的当前版本是否可以被当前事务可见的要根据这四个属性按照以下几种情况来判断

    • 当 trx_id = creator_trx_id 时:当前事务可以看见自己所修改的数据, 可见
    • 当 trx_id < min_trx_id 时 : 生成此数据的事务已经在生成readView前提交了, 可见
    • 当 trx_id >= max_trx_id 时 :表明生成该数据的事务是在生成ReadView后才开启的, 不可见
    • 当 min_trx_id <= trx_id < max_trx_id 时

      • trx_id 在 m_ids 列表里面 :生成ReadView时,活跃事务还未提交,不可见
      • trx_id 不在 m_ids 列表里面 :事务在生成readView前已经提交了,可见

    如果某个版本数据对当前事务不可见,那么则要顺着版本链继续向前寻找下个版本,继续这样判断,以此类推。

    注:RR和RC生成一致性视图的时机不一样 (这也是两种隔离级别实现的主要区别)

    • 读提交(read committed RC) 是在每一次select的时候生成ReadView的
    • 可重复读(repeatable read RR)是在第一次select的时候生成ReadView的

    下面咱们一起来举个例子实战一下。

    ## RR与RC和MVCC的例子实战
    假如说,我们有多个事务如下执行,我们通过这个例子来分析当数据库隔离级别为RC和RR的情况下,当时读数据的一致性视图版本链,也就是MVCC,分别是怎么样的。

    • 假设数据库中有一条初始数据 姓名是java小杰要加油,id是1 (id,姓名,trx_id,roll_point),插入此数据的事务id是1
    • 尤其要指出的是,只有这个事务操作了某些表的数据后当更改操作发生的时候(update,delete,insert),才会分配唯一的事务id,并且此事务id是递增的,单纯开启事务是没有事务id的,默认为0,creator_trx_id是0。
    • 以下例子中的A,B,C的意思是将姓名更改为A,B,C 读也是读取当前时刻的姓名,默认全都开启事务,并且此事务都经历过某些操作产生了事务id

    | 时间 | 事务100 | 事务200 | 事务300 | 事务400 |
    | --- | --- | --- | --- | --- |
    | T1 | A | | | |
    | T2 | B | | | |
    | T3 | | C | | |
    | T4 | | | 读 | |
    | T5 | 提交 | | | |
    | T6 | | | D | |
    | T7 | | | | 读 |
    | T8 | | E | | |
    | T9 | | 提交 | | |
    | T10 | | | 读 | |

    ## 读已提交(RC)与MVCC

    • 一个事务提交之后,它做的变更才会被其他事务看到

      每次读的时候,ReadView(一致性视图)都会重新生成
    1. 当T1时刻时,事务100修改名字为A
    2. 当T2时刻时,事务100修改名字为B
    3. 当T3时刻时,事务200修改名字为C
    4. 当T4时刻时,事务300开始读取名字
    • 此时这条数据的版本链如下

    同颜色代表是同一事务内的操作

    • 来我们静下心来好好分析一下此时T4时刻事务300要读了,究竟会读到什么数据

    当前最近的一条数据是,C,事务200修改的,还记得我们前文说的一致性视图的几个属性吗,和按照什么规则判断这个数据能不能被当前事务读。我们就分析这个例子。

    此时 (生成一致性视图ReadView

    • m_ids 是[100,200]: 当前活跃的读写事务的事务id列表
    • min_trx_id 是 100: m_ids的最小值
    • max_trx_id 是 201: m_ids的最大值+1

    当前数据的trx_id(事务id)是 200,符合min_trx_id<=trx_id<max_trx_id 此时需要判断
    trx_id 是否在m_ids活跃事务列表里面,一看,活跃事务列表里面是【100,200】,只有两个事务活跃,而此时的trx_id是200,则trx_id在活跃事务列表里面,活跃事务列表代表还未提交的事务,所以该版本数据不可见,就要根据roll_point指针指向上一个版本,继续这样的判断,上一个版本事务id是100,数据是B,发现100也在活跃事务列表里面,所以不可见,继续找到上个版本,事务是100,数据是A,发现是同样的情况,继续找到上个版本,发现事务是1,数据是小杰,1小于100,trx_id<min_trx_id,代表生成这个数据的事务已经在生成ReadView前提交了,此数据可以被读到。所以读取的数据就是小杰

    分析完第一个读,我们继续向下分析

    1. 当T5时刻时,事务100提交
    2. 当T6时刻时,事务300将名字改为D
    3. 当T7时刻时,事务400读取当前数据
    • 此时这条数据的版本链如下

    此时 (重新生成一致性视图ReadView

    • m_ids 是[200,300]: 当前活跃的读写事务的事务id列表
    • min_trx_id 是 200: m_ids的最小值
    • max_trx_id 是 301: m_ids的最大值+1

    当前数据事务id是300,数据为D,符合min_trx_id<=trx_id<max_trx_id 此时需要判断数据是否在活跃事务列表里,300在这里面,所以就是还未提交的事务就是不可见,所以就去查看上个版本的数据,上个版本事务id是200,数据是C,也在活跃事务列表里面,也不可见,继续向上个版本找,上个版本事务id是100,数据是B,100小于min_trx_id,就代表,代表生成这个数据的事务已经在生成ReadView前提交了,此数据可见,所以读取出来的数据就是B

    分析完第二个读,我们继续向下分析

    1. 当T8时刻时,事务200将名字改为E
    2. 当T9时刻时,事务200提交
    3. 当T10时刻时,事务300读取当前数据
    • 此时这条数据的版本链如下

    此时 (重新生成一致性视图ReadView

    • m_ids 是[300]: 当前活跃的读写事务的事务id列表
    • min_trx_id 是 300: m_ids的最小值
    • max_trx_id 是 301: m_ids的最大值+1

    当前事务id是200,200<min_trx_id ,代表生成这个数据的事务已经在生成ReadView前提交了,此数据可见,所以读出的数据就是E.

    当隔离级别是读已提交RC的情况下,每次读都会重新生成 一致性视图(ReadView)

    • T4时刻 事务300读取到的数据是小杰
    • T7时刻 事务400读取到的数据是B
    • T10时刻 事务300读取到的数据是E

    ## 可重复读(RR)与MVCC

    • 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的
    所以对于事务300来讲,它分别在T4和T10的时候,读取数据,但是它的一致性视图,用的永远都是第一次读取时的视图,就是T3时刻产生的一致性视图

    RR和RC的版本链是一样的,但是判断当前数据可见与否用到的一致性视图不一样

    在此可重复读RR隔离级别下,

    1. T4时刻时事务300第一次读时的分析和结果与RC都一样,可以见上文分析与结果
    2. T7时刻时事务400第一次读时的分析和结果与RC都一样,可以见上文分析与结果
    3. T10时刻时事务300第二次读时的一致性视图和第一次读时的一样,所以此时到底读取到什么数据就要重新分析了

    此时 (用的是第一次读时生成的一致性视图ReadView

    • m_ids 是[100,200]: 当前活跃的读写事务的事务id列表
    • min_trx_id 是 100: m_ids的最小值
    • max_trx_id 是 201: m_ids的最大值+1

    此时的版本链是

    当前数据的事务id是200,数据是E,在当前事务活跃列表里面,所以数据不可见,根据回滚指针找到上个版本,发现事务id是300,当前事务也是300,可见,所以读取的数据是D

    • 我们可以自己思考下,要是没有事务300这条更改的这条记录,又该怎么继续向下分析呢?

    当隔离级别是可重复读RR的情况下,每次读都会用第一次读取数据时生成的一致性视图(ReadView)

    • T4时刻 事务300读取到的数据是小杰
    • T7时刻 事务400读取到的数据是B
    • T10时刻 事务300读取到的数据是D

    往期精彩推荐

    絮絮叨叨

    如果大家觉得这篇文章对自己有一点点帮助的话,欢迎关注此公众号 java小杰要加油

    若文章有误欢迎指出,靓仔靓女们,我们下篇文章见,扫一扫,开启我们的故事

    查看原文

    赞 2 收藏 2 评论 0

    好懂事一男的 发布了文章 · 1月26日

    同学,二叉树的各种遍历方式,我都帮你总结了,附有队列堆栈图解(巩固基础,强烈建议收藏)

    靓仔靓女们大家好,我是公众号:java小杰要加油,今天我来分享一篇关于二叉树的文章(建议收藏,便于巩固基础)。

    • 看完此文leetcode至少解决八道题
    • 掌握二叉树的前序、中序、后序遍历以及两种不同的实现方式:递归与非递归
    • 非递归时遍历与层次遍历时,有详细的图解表示队列/栈中的元素是如何移动的,有助于理解代码的运行

    二叉树介绍

    二叉树(binary tree) 是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。

    二叉树的递归定义为: 二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树

    • 逻辑上二叉树有五种基本形态,如图所示

      1. 空二叉树
      2. 只有一个根结点的二叉树
      3. 只有左子树
      4. 完全二叉树
      5. 只有右子树

    二叉树相关属性解释:

    • 结点:包含一个数据元素及若干指向子树分支的信息。
    • 结点的度:一个结点拥有子树的数目称为结点的度。
    • 叶子结点:也称为终端结点,没有子树的结点或者度为零的结点。
    • 分支结点:也称为非终端结点,度不为零的结点称为非终端结点。
    • 树的度:树中所有结点的度的最大值。
    • 结点的层次:从根结点开始,假设根结点为第1层,根结点的子节点为第2层,依此类推,如果某一个结点位于第L层,则其子节点位于第L+1层。
    • 树的深度:也称为树的高度,树中所有结点的层次最大值称为树的深度。
    • 有序树:如果树中各棵子树的次序是有先后次序,则称该树为有序树。
    • 无序树:如果树中各棵子树的次序没有先后次序,则称该树为无序树。

    二叉树遍历方式

    • 二叉树遍历方式分为三种

      • 前序遍历(根左右): 访问根结点,再访问左子树、再访问右子树。
      • 中序遍历(左根右): 先访问左子树,再访问根结点、再访问右子树。
      • 后续遍历(左右根): 先访问左子树,再访问右子树,再访问根结点。

    例如一个这个样子的二叉树,按三种遍历方法分别遍历,输出的结果分别是

    • 前序遍历: ABDECFG
    • 中序遍历: DBEAFCG
    • 后续遍历: DEBFGCA

    下面我们一起来用代码实现下这三种遍历

    • 注:以上前序、中序、后序每一种遍历方式都有递归非递归两种实现方法
    • 前序遍历就是深度优先遍历(DFS)
    • 层次遍历就是广度优先遍历(BFS)

    二叉树递归遍历

    * 前序遍历 (LeetCode 144)

    
    class Solution {
        //声明列表
        ArrayList<Integer> list = new ArrayList<>();
        public List<Integer> preorderTraversal(TreeNode root) {
            // 如果根节点为空,则直接返回空列表
            if (root == null){
                return  new ArrayList<>();
            }
            //节点不为空,将节点的值添加进列表中    
            list.add(root.val);
            //判断此节点的左节点是否为空,如果不为空则将递归遍历左子树
            if (root.left != null){
                preorderTraversal(root.left);
            }
            //判断此节点的右节点是否为空,如果不为空则将递归遍历右子树
            if (root.right != null){
                preorderTraversal(root.right);
            }
            //最后返回列表
            return list;
        }
    }
    
    class Solution {
        //声明列表
        ArrayList<Integer> list = new ArrayList<>();
        public List<Integer> inorderTraversal(TreeNode root) {
            // 如果根节点为空,则直接返回空列表
            if (root == null){
                return  new ArrayList<>();
            }
            //判断此节点的左节点是否为空,如果不为空则将递归遍历此节点的左子树
            if (root.left != null){
                inorderTraversal(root.left);
            }
            //节点不为空,将节点的值添加进列表中
            list.add(root.val);
            //判断此节点的右节点是否为空,如果不为空则将递归遍历此节点的右子树
            if (root.right != null){
                inorderTraversal(root.right);
            }
            //最后返回列表
            return list;
        }
    }
    
    class Solution {
        //声明列表
        ArrayList<Integer> list = new ArrayList<>();
        public List<Integer> postorderTraversal(TreeNode root) {
            // 如果根节点为空,则直接返回空列表
            if (root == null){
                return  new ArrayList<>();
            }
            //判断此节点的左节点是否为空,如果不为空则将递归遍历此节点的左子树
            if (root.left != null){
                postorderTraversal(root.left);
            }
            //判断此节点的右节点是否为空,如果不为空则将递归遍历此节点的右子树
            if (root.right != null){
                postorderTraversal(root.right);
            }
            //节点不为空,将节点的值添加进列表中
            list.add(root.val);
            //最后返回列表
            return list;
        }
    }

    我们通过观察发现,这代码怎么这么像,是的就是很像,他们唯一的区别就是list.add(root.val);代码的位置不一样,这行代码就代表文中的 遍历(访问)
    下图中为前序遍历(左右)


    下图中为中序遍历(左右)

    下图中为后序遍历(左右

    二叉树非递归遍历

    • 用到栈(FILO 先进后出的特性)
    • 每段代码后,都有栈和其中元素的关系具体过程,建议静下心来慢慢看,有助于理解代码如何运行
    • 前序遍历
    
    class Solution {
        List list =   new ArrayList();
        public List<Integer> preorderTraversal(TreeNode root) {
            //如果根节点为空,则直接返回空列表
            if(root==null){
                return  new ArrayList();
            }
            //声明一个栈
            Stack<TreeNode> stack = new Stack<>();
            //将节点入栈
            stack.push(root);
            //如果栈不为空
            while (!stack.empty()){
                //从栈弹出这个节点
                TreeNode node = stack.pop();
                //添加进列表中
                list.add(node.val);
                // 如果这个节点的右子节点不为空
                if (node.right!=null){
                    // 将其入栈  因为栈是先进后出,所以先压栈右子节点  后出
                    stack.push(node.right);
                }
                // 如果这个节点的左子节点不为空
                if (node.left!=null){
                    // 将其入栈 因为栈是先进后出,所以后压栈左子节点 先出
                }
            }
            //返回列表
            return list;
        }
    }

    • 中序遍历
    
    class Solution {
        public List<Integer> inorderTraversal(TreeNode root) {
            //判断节点是否为空,为空的话直接返回空列表
            if (root == null){
                return new ArrayList();
            }
            //声明列表存储结果
            List<Integer> list =  new ArrayList();
            //声明一个栈
            Stack<TreeNode> stack = new Stack<>();
            //当节点不为空或者栈不为空时
            while (root != null || !stack.empty()){
                //当节点不为空时
                while (root != null){
                    //将节点压栈
                    stack.push(root);
                    //将节点指向其左子节点
                    root = root.left;
                }
                //如果栈不为空
                if (!stack.empty()){
                    //将栈里元素弹出来
                    TreeNode node = stack.pop();
                    //添加进列表中
                    list.add(node.val);
                    //将节点指向其右子节点
                    root = node.right;
                }
            }
            return list;
        }
    }

    • 后序遍历
    
    class Solution {
        public List<Integer> postorderTraversal(TreeNode root) {
            // 如果根节点为空,则直接返回空列表
            if (root == null){
                return  new ArrayList<>();
            }
            //声明列表
            ArrayList<Integer> list = new ArrayList<>();
            //声明栈A
            Stack<TreeNode> stackA = new Stack<TreeNode>();
            //声明栈B
            Stack<TreeNode> stackB = new Stack<TreeNode>();
            //将次元素压入栈A
            stackA.push(root);
            //当栈A不为空时
            while (!stackA.empty()){
                //取出其中压入的元素
                TreeNode node = stackA.pop();
                //压入栈B中
                stackB.push(node);
                //当此节点左子节点不为空时
                if (node.left != null){
                    //压入栈A
                    stackA.push(node.left);
                }
                //当此节点右子节点不为空时
                if (node.right != null){
                    //压入栈A
                    stackA.push(node.right);
                }
            }
            //当栈B不为空时
            while (!stackB.empty()){
                //取出其元素并且添加至列表中
                TreeNode node = stackB.pop();
                list.add(node.val);
            }
            //最后返回列表
            return list;
        }
    }

    二叉树层序遍历(BFS)

    • LeetCode 102 二叉树的层序遍历
    • 用到队列(FIFO 先进先出的特性)代码后有队列和其中元素的关系具体过程,建议静下心来慢慢看,有助于理解代码如何运行
    class Solution {
        public List<List<Integer>> levelOrder(TreeNode root) {
            if (root == null) {
                return new ArrayList<List<Integer>>();
            }
            // 声明一个列表存储每一行的数据
            List<List<Integer>> result = new ArrayList<>();
            //声明一个队列
            LinkedList<TreeNode> queue = new LinkedList<>();
            //如果根节点不为空,将其入队
            queue.offer(root);
            //当队列不为空时,代表队列里有数据
            while (!queue.isEmpty()) {
                //存储每一行的数据line
                List<Integer> line = new ArrayList<Integer>();
                //保存队列中现有数据的个数,这些就是要添加至每一行列表的值
                int size = queue.size();
                for (int i=0;i<size;i++){
                //取出队列的节点 (FIFO 先进先出)
                TreeNode node = queue.poll();
                line.add(node.val);
                  if (node.left != null) {
                      queue.offer(node.left);
                  }
                  if (node.right != null) {
                      queue.offer(node.right);
                  }
                }
                result.add(line);
            }
            return result;
        }
    }

    leetcode二叉树相关练习

    • 我们看到了这里,对二叉树的前序(DFS)、中序、后序、递归/非递归以及层次遍历(BFS)都有了一定的了解(如果上面的图都消化了的话

    然后我们趁热打铁来几道leetcode题目试试手!(总体代码和上面只有稍微的改动,因为大致思想是一样的,把上面的内容都消化了的话就很简单啦)

    
    class Solution {
        public List<String> binaryTreePaths(TreeNode root) {
            if (root == null){
                return new ArrayList<>();
            }
            ArrayList<String> list = new ArrayList<>();
            Stack<TreeNode> stack = new Stack<TreeNode>();
            //这个栈存储路径,与上一个存储节点的栈一样的操作
            Stack<String> path = new Stack<String>();
            stack.push(root);
            path.push(root.val+"");
            while (!stack.empty()){
                TreeNode node = stack.pop();
                String p = path.pop();
                //当是叶子节点的时候,此时栈中的路径即为一条完整的路径,可以加入到结果中
                if (node.right == null && node.left == null ){
                    list.add(p);
                }
                //如果右子节点不为空
                if (node.right != null){
                    stack.push(node.right);
                    //将临时路径继续压栈
                    path.push(p+"->"+node.right.val);
                }
                //如果左子节点不为空
                if (node.left != null){
                    stack.push(node.left);
                    //将临时路径继续压栈
                    path.push(p+"->"+node.left.val);
                }
            }
            return list;
        }
    }
    
    class Solution {
        public int maxDepth(TreeNode root) {
           if (root == null){
               return 0;
           }
            LinkedList<TreeNode> queue = new LinkedList<>();
            int result = 0;
            queue.offer(root);
            while (!queue.isEmpty()){
                //层数+1
                result++;
                //这是当前层的节点的个数
                int size = queue.size();
                for (int i=0;i<size;i++){
                    //要将其全部出队后,才可以再次计数
                    TreeNode node = queue.poll();
                    if (node.left != null){
                        //如果出队的节点还有左子节点,就入队
                        queue.offer(node.left);
                    }
                    if (node.right != null){
                        //如果出队的节点还有右子节点,就入队
                        queue.offer(node.right);
                    }
                }
            }
            //返回层数
            return result;
    
        }
    }
    
    class Solution {
        public List<List<Integer>> levelOrderBottom(TreeNode root) {
            if (root == null){
                return new ArrayList<List<Integer>>() ;
            }
            List<List<Integer>> result = new ArrayList<List<Integer>>() ;
            LinkedList<TreeNode> queue = new LinkedList<>();
            //声明一个栈,用来存储每一层的节点
            Stack<ArrayList<Integer> > stack = new Stack<>();
            queue.offer(root);
            while (!queue.isEmpty()){
                int size = queue.size();
                ArrayList<Integer> list = new ArrayList<>();
                for (int i=0;i<size;i++){
                    TreeNode node = queue.poll();
                    list.add(node.val);
                    if (node.left != null){
                        queue.offer(node.left);
                    }
                    if (node.right != null){
                        queue.offer(node.right);
                    }
                }
                //将这一层的节点压入栈中
                stack.push(list);
            }
            //当栈不为空时,弹出结果,从而达到从下往上遍历二叉树的效果
            while (!stack.isEmpty()){
                ArrayList<Integer> list = stack.pop();
                result.add(list);
            }
            return result;
        }
    }

    总结

    我们通过这篇文章,至少可以解决leetcode上以下几道题目

    往期精彩推荐

    絮絮叨叨

    如果大家觉得这篇文章对自己有一点点帮助的话,欢迎关注此公众号 java小杰要加油

    若文章有误欢迎指出,靓仔靓女们,我们下篇文章见,扫一扫,关注我,开启我们的故事

    查看原文

    赞 0 收藏 0 评论 0

    好懂事一男的 发布了文章 · 1月26日

    近万字,图文并茂详解AQS加锁流程

    靓仔靓女们好,我们又见面了,我是公众号:java小杰要加油,现就职于京东,致力于分享java相关知识,包括但不限于并发、多线程、锁、mysql以及京东面试真题

    AQS介绍

    • AQS全称是AbstractQueuedSynchronizer,是一个抽象队列同步器,JUC并发包中的大部分的并发工具类,都是基于AQS实现的,所以理解了AQS就算是四舍五入掌握了JUC了(好一个四舍五入学习法)那么AQS到底有什么神奇之处呢?有什么特点呢?让我们今天就来拔光它,一探究竟!

      • state:代表被抢占的锁的状态
      • 队列:没有抢到锁的线程会包装成一个node节点存放到一个双向链表中

    AQS大概长这样,如图所示:

    你说我随便画的,我可不是随便画的啊,我是有bear而来,来看下AQS基本属性的代码

    那么这个Node节点又包含什么呢?来吧,展示。

    那么我们就可以把这个队列变的更具体一点

    怎么突然出来个exclusiveOwnerThread?还是保存当前获得锁的线程,哪里来的呢
    还记得我们AQS一开始继承了一个类吗

    这个exclusiveOwnerThread就是它里面的属性

    再次回顾总结一下,AQS属性如下:

    1. state:代表被抢占的锁的状态
    2. exclusiveOwnerThread:当前获得锁的线程
    3. 队列:没有抢到锁的线程会包装成一个node节点存放到一个双向链表中

      • Node节点 :

         * thread:  当前node节点包装的线程
         * waitStatus:当前节点的状态
         * pre: 当前节点的前驱节点
         * next: 当前节点的后继节点
         * nextWaiter:表示当前节点对锁的模式,独占锁的话就是null,共享锁为Node()
         

    好了,我们对AQS大概是什么东西什么结构长什么样子有了个清楚的认知,下面我们直接上硬菜,从源码角度分析下,AQS加锁,它这个结构到底是怎么变化的呢?

    注:以下分析的都是独占模式下的加锁

    • 独占模式 : 锁只允许一个线程获得 NODE.EXCLUSIVE
    • 共享模式 :锁允许多个线程获得 NODE.SHARED

    AQS加锁源码——acquire

     public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }

    乍一看这是什么啊,没关系,我们可以把它画成流程图方便我们理解,流程图如下

    下面我们来一个一个分析,图文并茂,来吧宝贝儿。

    AQS加锁源码——tryAcquire

     protected boolean tryAcquire(int arg) {
            throw new UnsupportedOperationException();
        }

    这是什么情况?怎么直接抛出了异常?其实这是由AQS子类重写的方法,就类似lock锁,由子类定义尝试获取锁的具体逻辑

    我们平常使用lock锁时往往如下 (若不想看lock锁怎么实现的可以直接跳转到下一节

    ReentrantLock lock = new ReentrantLock();
            lock.lock();
            try{
               //todo
            }finally {
                lock.unlock();
            }

    我们看下lock.lock()源码

        public void lock() {
            sync.lock();
        }

    这个sync又是什么呢,我们来看下lock类的总体属性就好了

    所以我们来看下 默认非公平锁的加锁实现

    static final class NonfairSync extends Sync {
            private static final long serialVersionUID = 7316153563782823691L;
            
            final void lock() {
                //将state状态从0设为1 CAS方式
                if (compareAndSetState(0, 1))
                    //如果设定成功的话,则将当前线程(就是自己)设为占有锁的线程
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    //设置失败的话,就当前线程没有抢到锁,然后进行AQS父类的这个方法
                    acquire(1);
            }
    
            protected final boolean tryAcquire(int acquires) {
                //调用非公平锁的方法
                return nonfairTryAcquire(acquires);
            }
        }

    现在压力又来到了nonfairTryAcquire(acquires)这里

     final boolean nonfairTryAcquire(int acquires) {
                //获得当前线程
                final Thread current = Thread.currentThread();
                //获得当前锁的状态
                int c = getState();
                //如果锁的状态是0的话,就表明还没有线程获取到这个锁
                if (c == 0) {
                    //进行CAS操作,将锁的状态改为acquires,因为是可重入锁,所以这个数字可能是>0的数字
                    if (compareAndSetState(0, acquires)) {
                        //将当前持有锁的线程设为自己
                        setExclusiveOwnerThread(current);
                        //返回 获取锁成功
                        return true;
                    }
                }// 如果当前锁的状态不是0,判断当前获取锁的线程是不是自己,如果是的话
                else if (current == getExclusiveOwnerThread()) {
                    //则重入数加acquires  (这里acquires是1)  1->2  3->4 这样
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow   异常检测
                        throw new Error("Maximum lock count exceeded");
                    //将锁的状态设为当前值
                    setState(nextc);
                    //返回获取锁成功
                    return true;
                }
                //当前获取锁的线程不是自己,获取锁失败,返回
                return false;
            }

    由此可见,回到刚才的问题,AQS中的tryAcquire是由子类实现具体逻辑的

    AQS加锁源码——addWaiter

    如果我们获取锁失败的话,就要把当前线程包装成一个Node节点,那么具体是怎么包装的呢,也需要化妆师经纪人吗?
    我们来看下源码就知道了
    addWaiter(Node.EXCLUSIVE), arg) 这就代表添加的是独占模式的节点

     private Node addWaiter(Node mode) {
            //将当前线程包装成一个Node节点
            Node node = new Node(Thread.currentThread(), mode);
            // 声明一个pred指针指向尾节点
            Node pred = tail;
            //尾节点不为空
            if (pred != null) {
                //将当前节点的前置指针指向pred
                node.prev = pred;
                //CAS操作将当前节点设为尾节点,tail指向当前节点
                if (compareAndSetTail(pred, node)) {
                    //pred下一节点指针指向当前节点
                    pred.next = node;
                    //返回当前节点 (此时当前节点就已经是尾节点)
                    return node;
                }
            }
            //如果尾节点为空或者CAS操作失败
            enq(node);
            return node;
        }

    其中node的构造函数是这样的

     Node(Thread thread, Node mode) {     // Used by addWaiter
          this.nextWaiter = mode;
          this.thread = thread;
      }

    我们可以通过图解的方法来更直观的来看下addWaiter做了什么

    由图可知,如果曾经尾节点不为空的时候,node节点会加入到队列末尾,那么如果曾经尾节点为空或者CAS失败调用
    enq(node);会怎么样呢?

    AQS加锁源码——enq

     private Node enq(final Node node) {
            //死循环,直到有返回值
            for (;;) {
                //声明一个t的指针指向tail
                Node t = tail;
                //如果尾巴节点为空
                if (t == null) { // Must initialize
                    //则CAS设置一个节点为头节点(头节点并没有包装线程!)这也是延迟初始化头节点
                    if (compareAndSetHead(new Node()))
                        //将尾指针指向头节点
                        tail = head;
                } else {  //如果尾节点不为空,则说明这是CAS失败
                  // 将node节点前驱节点指向t
                    node.prev = t;
                    //继续CAS操作将自己设为尾节点
                    if (compareAndSetTail(t, node)) {
                        //将t的next指针指向自己 (此时自己真的是尾节点了)
                        t.next = node;
                        //返回自己节点的前置节点,队列的倒数第二个
                        return t;
                    }
                }
            }
        }
    • 队列中的头节点,是延迟初始化的,加锁时用到的时候才去输出话,并不是一开始就有这个头节点的
    • 头节点并不保存任何线程

    end 尾分叉

         // 将node节点前驱节点指向t              
          node.prev = t;                    1
          //继续CAS操作将自己设为尾节点
          if (compareAndSetTail(t, node)) { 2
              //将t的next指针指向自己 (此时自己真的是尾节点了)
              t.next = node;                3
              //返回自己节点的前置节点,队列的倒数第二个
              return t;
          }

    我们注意到,enq函数有上面三行代码,3是在2执行成功后才会执行的,由于我们这个代码无时无刻都在并发执行,存在一种可能就是

    1执行成功,2执行失败(cas并发操作),3没有执行,所以就只有一个线程1,2,3都执行成功,其他线程1执行成功,2,3没有执行成功,出现尾分叉情况,如图所示

    这些分叉失败的节点,在以后的循环中他们还会执行1,直总会指向新的尾节点,1,2,3这么执行,早晚会入队

    AQS加锁源码——acquireQueued

    final boolean acquireQueued(final Node node, int arg) {
            // 是否有异常发生
            boolean failed = true;
            try {
                //中断标志
                boolean interrupted = false;
                //开始自旋,要么当前线程尝试去拿到锁,要么抛出异常
                for (;;) {
                    //获得当前节点的上一个节点
                    final Node p = node.predecessor();
                    //判断这个节点是不是头节点,如果是头节点则获取锁
                    if (p == head && tryAcquire(arg)) {
                        //将此节点设为头节点
                        setHead(node);
                        //原来的头节点出队
                        p.next = null; // help GC
                        failed = false;
                        //返回是否中断
                        return interrupted;
                    }
                    // 说明p不是头节点
                    // 或者
                    // p是头节点但是获取锁失败
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        // 中断标志设为true
                        interrupted = true;
                }
            } finally {
               //如果有异常发生的话
                if (failed)
                //取消当前线程竞争锁,将当前node节点状态设置为cancel
                    cancelAcquire(node);
            }
        }

    其中有一行代码是setHead(node);

     private void setHead(Node node) {
            head = node;
            node.thread = null;  //将head节点的线程置为空
            node.prev = null;
        }
    • 为什么要将头节点的线程置为空呢,是因为在 tryAcquire(arg)中就已经记录了当前获取锁的线程了,在记录就多此一举了,我们看前文中提到的nonfairTryAcquire(acquires)其中有一段代码
       if (compareAndSetState(0, acquires)) {
              //将当前持有锁的线程设为自己   
              setExclusiveOwnerThread(current);  
              //返回 获取锁成功
              return true;
          }

    可见 setExclusiveOwnerThread(current);就已经记录了获得锁的线程了

    我们acquireQueued返回值是中断标志,true表示中断过,false表示没有中断过,还记得我们一开始吗,回到最初的起点

    public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }

    如果返回了true,代表此线程有中断过,那么调用 selfInterrupt();方法,将当前线程中断一下

     static void selfInterrupt() {
            Thread.currentThread().interrupt();
        }

    AQS加锁源码——shouldParkAfterFailedAcquire

    程序运行到这里就说明

     // 说明p不是头节点
    // 或者
    // p是头节点但是获取锁失败
    if (shouldParkAfterFailedAcquire(p, node) &&
        parkAndCheckInterrupt())
        // 中断标志设为true
        interrupted = true;
    }

    我们来分析下shouldParkAfterFailedAcquire(p, node)的源码里面到底做了什么?

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            //获取当前节点的前置节点的状态
            int ws = pred.waitStatus;
            if (ws == Node.SIGNAL)
               //如果是SIGNAL(-1)状态直接返回true,代表此节点可以挂起
                //因为前置节点状态为SIGNAL在适当状态 会唤醒后继节点
                return true;
            if (ws > 0) {
                //如果是cancelled
                do {
                    //则从后往前依此跳过cancelled状态的节点
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                //将找到的符合标准的节点的后置节点指向当前节点
                pred.next = node;
            } else {
                //否则将前置节点等待状态设置为SIGNAL
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            return false;
        }

    其中的node.prev = pred = pred.prev;可以看成

    pred = pred.prev;
    
    node.prev = pred;

    可见一顿操作后,队列中跳过了节点状态为cancelled的节点

    AQS加锁源码——parkAndCheckInterrupt

    shouldParkAfterFailedAcquire返回true时就代表允许当前线程挂起然后就执行 parkAndCheckInterrupt()这个函数

     private final boolean parkAndCheckInterrupt() {
            // 挂起当前线程   线程卡在这里不再下执行,直到unpark唤醒
            LockSupport.park(this);
            return Thread.interrupted();
        }

    所以当前线程就被挂起啦

    AQS加锁源码——cancelAcquire

    我们还记得前文中提到acquireQueued中的一段代码

      try {
              
            } finally {
                if (failed)
                    cancelAcquire(node);
            }

    这是抛出异常时处理节点的代码,下面来看下源代码

    private void cancelAcquire(Node node) {
            //过滤掉无效节点
            if (node == null)
                return;
            //当前节点线程置为空
            node.thread = null;
            //获取当前节点的前一个节点
            Node pred = node.prev;
            //跳过取消的节点
            while (pred.waitStatus > 0)
                node.prev = pred = pred.prev;
    
            //记录过滤后的节点的后置节点
            Node predNext = pred.next;
            //将当前节点状态改为CANCELLED
            node.waitStatus = Node.CANCELLED;
    
            // 如果当前节点是tail尾节点 则将从后往前找到第一个非取消状态的节点设为tail尾节点
            if (node == tail && compareAndSetTail(node, pred)) {
                //如果设置成功,则tail节点后面的节点会被设置为null
                compareAndSetNext(pred, predNext, null);
            } else {
    
                int ws;
                //如果当前节点不是首节点的后置节点
                if (pred != head &&  //并且
                        //如果前置节点的状态是SIGNAL
                    ((ws = pred.waitStatus) == Node.SIGNAL || //或者
                            //状态小于0 并且设置状态为SIGNAL成功
                     (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                        //并且前置节点线程不为null时
                    pred.thread != null) {
                    //记录下当前节点的后置节点
                    Node next = node.next;
                    //如果后置节点不为空 并且后置节点的状态小于0
                    if (next != null && next.waitStatus <= 0)
                        //把当前节点的前驱节点的后继指针指向当前节点的后继节点
                        compareAndSetNext(pred, predNext, next);
                } else {
                    //唤醒当前节点的下一个节点
                    unparkSuccessor(node);
                }
                //将当前节点下一节点指向自己
                node.next = node; // help GC
            }
        }

    看起来太复杂了,不过没关系,我们可以拆开看,其中有这一段代码

           //当前节点线程置为空
            node.thread = null;
            //获取当前节点的前一个节点
            Node pred = node.prev;
            //跳过取消的节点
            while (pred.waitStatus > 0)
                node.prev = pred = pred.prev;
    
            //记录过滤后的节点的后置节点
            Node predNext = pred.next;
            //将当前节点状态改为CANCELLED
            node.waitStatus = Node.CANCELLED;

    如图所示

    通过while循环从后往前找到signal状态的节点,跳过中间cancelled状态的节点,同时将当前节点状态改为CANCELLED

    我们可以把这复杂的判断条件转换成图来直观的看一下

    • 当前节点是尾节点时,队列变成这样

    • 当前节点是head后继节点

    • 当前节点不是尾节点也不是头节点的后继节点(队列中的某个普通节点)

    总结

    太不容易了家人们,终于到了这里,我们再来总结一下整体的流程

        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }

    1.基于AQS实现的子类去实现tryAcquire尝试获取锁

    2.如果获取锁失败,则把当前节点通过addWaiter方法包装成node节点插入队列

    • 如果尾节点为空或者CAS操作失败则调用enq方法保证成功插入到队列,若节点为空则初始化头节点

    3.acquireQueued方法,入队后的节点继续获取锁(此节点的前置节点是头节点)或者挂起

    • shouldParkAfterFailedAcquire判断节点是否应该挂起

      • 如果当前节点的前置节点是signal状态,则返回true,可以挂起
      • 如果当前节点的前置节点是cancelled,则队列会从当前节点的前一个节点开始从后向前遍历跳过cacelled状态的节点,将当前节点和非cacelled状态的节点连接起来,返回false,不可以挂起
      • 否则将前置节点等待状态设置为SIGNAL,返回false,不可以挂起
    • parkAndCheckInterrupt挂起当前线程
    • cancelAcquire将当前节点状态改为cancelld
    1. selfInterrupt(); 设置中断标志,将中断补上

    往期精彩推荐

    絮絮叨叨

    如果大家觉得这篇文章对自己有一点点帮助的话

    若文章有误欢迎指出,靓仔靓女,我们下篇文章见,扫一扫,关注我,开启我们的故事

    查看原文

    赞 0 收藏 0 评论 0

    好懂事一男的 发布了文章 · 1月25日

    五千多字,图文并茂详解HTTP报文格式、请求响应头、cookie以及HTTPS加密方式

    • 靓仔靓女们大家好,我们又见面了,公众号:java小杰要加油,这周来分享一篇关于HTTP协议相关的文章
    • 看完此文可以对

      • HTTP报文格式HTTP各种请求头HTTP响应码cookie属性以及HTTPS为什么安全(涉及到三种加密方式) 有个清晰的认知
    • 全文五千来字,强烈建议收藏,巩固基础
    • 若文中涉及到的知识点有所偏差的话,还请大佬们指出,小杰感激不尽,冲冲冲!~
    • 话不多说,直接开搞

    HTTP简介

    • 超文本传输协议(Hypertext Transfer Protocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应
    • 现在主要应用 http1.1 协议
    • http是无状态协议,不会保存多次请求之间的关系,使用cookie做状态管理
    • 持久连接节省通信量(HTTP1.1和部分HTTP1.0)
    • 通过请求方法告知服务器意图,get,post

    HTTP报文

    • 用于HTTP协议交互的信息叫做HTTP报文
    • 报文由报文首部报文主体来组成,其中由空行分割
    • 请求报文响应报文报文结构不一样,其中最大的区别就是在报文首部中,各有各的特定的首部

    • 报文首部:服务器或者客户端需要处理的请求或者响应的内容及其属性
    • 报文主体:被发送的数据

    HTTP请求报文结构

    • 客户端发送的报文叫做请求报文

    • 请求行:包含用于请求的方法,请求URI和HTTP版本
    • 请求首部字段:请求报文里特有的字段(后文会提到)
    • 通用首部字段:请求报文和响应报文都会用到的首部
    • 实体首部字段:针对请求报文的实体部分使用的首部
    • 其他:可能包含HTTP的RFC里未定义的首部(如Cookie等)

    HTTP响应报文结构

    • 服务端发送的报文叫做响应报文

    • 状态行:包含表明响应结果的状态码,原因短语和HTTP版本
    • 响应首部字段:响应报文里特有的字段(后文会提到)
    • 通用首部字段:请求报文和响应报文都会用到的首部
    • 实体首部字段:针对响应报文的实体部分使用的首部
    • 其他:可能包含HTTP的RFC里未定义的首部(如Set-Cookie等)

    注:若HTTP首部字段重复了的话,不同的浏览器处理机制不一样

    • 有些浏览器会优先处理第一次出现的字段
    • 有些浏览器会优先处理最后一次出现的字段

    HTTP响应码

    2xx 成功

    2xx的响应结果就代表请求被正常处理了
    • 200 OK:表示客户端发来的请求被服务器正常处理了
    • 204 Not Content:请求被成功处理,但是返回的响应报文不包含实体的主体部分
    • 206 Partial Content:客户端进行范围请求,而服务器重新执行了这部分的GET请求

    3xx 重定向

    3xx的响应结果就表明浏览器需要执行某些特殊的处理以正确处理请求
    • 301 Moved Permanently:永久重定向。表示请求的资源已经被分配了新的URI,以后应该使用新的URI
    • 302 Found:临时重定向。代表资源只是暂时的移动了以后还可能会移动为新的URI
    • 303 See Other:由于请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源
    • 304 Not Modified:客户端发送附带条件(请求首部中if开头的属性中的一种)的请求的时候,服务端允许访问资源,但是那些请求并没有满足,直接返回304,即服务端资源未改变,可以直接使用客户端未过期的缓存,304返回时,不包含任何响应的主体部分(虽然被划分为3xx里,但是和重定向没有任何关系)

    4xx 客户端错误

    4xx的响应结果就表明客户端是发生错误的原因所在
    • 400 Bad Requset:请求报文中存在语法错误,请修改请求内容后再发送请求
    • 401 Unauthorized:客户端未认证授权
    • 403 Forbidden:服务端禁止客户端访问此资源
    • 404 Not Found:URL写错了,找不到此路径

    5xx 服务器错误

    5xx的响应结果就表明服务器本身发生错误
    • 500 Internal Server Error:服务器内部故障,可能是bug导致的
    • 503 Service Unavaliable:服务器暂时不可用(停机维护或者超负载),如果事先知道解除这种情况所需的时间,最好写入响应头中的Retry-After这个字段再返回给客户端

    HTTP报文首部

    HTTP1.1 规定了 以下47种首部字段

    通用首部 (共9种)

    首部字段解释
    1. Cache-Control控制缓存的行为
    2. Connection逐跳首部、连接的管理
    3. Date创建报文的日期和时间
    4. Pragma报文指令
    5. Trailer报文末端的首部一览
    6. Transfer-Encoding指定报文主体的传输编码方式
    7. Upgrade升级为其他协议
    8. Via代理服务器的相关信息
    9. Warning错误通知

    下面我们来看挑几个重要的属性来看下~

    1. Connection 他有两个作用

      • 控制不再转发给代理的首部字段
    GET / HTTP/1.1
    Upgrade: HTTP/1.1    // 就会把次字段删除后再从代理服务器转发出去
    Connection: Upgrade   // 不再转发的首部字段名

    • 管理持久链接(这个比较常见)

      • HTTP/1.1默认连接都是持久连接Connction: Keep-Alive
      • 当服务器想断开的时候,需要指定Connction: close
    1. Pragme :是HTTP/1.1之前版本遗留的字段,仅仅是为了与HTTP/1.0向后兼容而定义

      • Pragm:no-cache :通用首部字段,在请求头中,表示所有的中间服务器不返回缓存的资源
      • 可是所有的中间服务器都以HTTP/1.1为基准的话,可以直接采用 Cache-Control:no-cache
      • 所以一般会发送两个字段Cache-Control:no-cachePragm:no-cache

    请求报文首部 (共19种)

    首部字段解释
    1.Accrpt用户代理可处理的媒体类型
    2.Accrpt-Charset优先的字符集
    3.Accept-Encoding优先的内容编码
    4.Accept-Language优先的语言(自然语言)
    5.Authorizationweb认证信息
    6.Expect期待服务器的特定行为
    7.From用户的电子邮箱地址
    8.Host请求资源所在服务器
    9.If-Match比较实体标记(ETag)
    10.If-Modified-Since比较资源的更新时间
    11.If-None-Match比较实体标记(与If-Match相反)
    12.If-Range资源未更新时发送实体Byte的范围请求
    13.If-Unmodified-Since比较资源的更新时间(与If-Modified-Since相反)
    14.Max-Forwards最大传输逐跳数
    15.Proxy-Authorization代理服务器要求客户端的认证信息
    16.Range实体的字节范围要求
    17.Referer对请求中URI的原始获取方
    18.TE传输编码的优先级
    19.User-AgentHTTP客户端程序的信息
    1. If-Match:只有当 If-Match 字段值跟 ETag 值匹配一致时,服务器才会接受请求

      • 它会告知服务器匹配资源所用的实体标记(ETag)值,这时服务器无法使用弱ETag值
      • 仅当两者一致时才会执行请求,否则返回412 Precondition Failed的响应
      • 还可以使用 * 号指定If-Match的字段值,如果这样的话,那么服务器将会忽略ETag的值,只要资源存在就处理请求。
    2. If-Modified-Since

      • 若资源更新时间确实在此字段指定时间之后的话,则处理该请求,否则返回304 Not Modified
      • 用于确认代理或客户端拥有本地资源的有效性,若想获取资源的更新日期时间的话可以通过确认首部字段Last-Modified来确定
    3. If-None-Match

      • 只有在If-None-Match的字段值与ETag值不一致时,才可以处理该请求,与前文中提到的If-Match作用相反
    4. If-Range

      • 他告知服务器若指定的If-Range字段值(ETag值或者时间)和请求资源的ETag值或时间一致时,则作为范围请求处理,否则,返回全体资源
    5. If-Unmodified-Since

      • 指定的请求资源只有在字段值内指定的日期时间之后未发生更新,才会执行这个请求,否则,返回412 Precondition Failed状态响应,与If-Modified-Since作用相反
    6. Max-Forwards

      • 每次请求转发时数值减一,直到0时返回响应
      • 有可能这个请求经过了多台服务器代理转发,如果突然间请求出现了什么问题导致转发失败,而客户端不知道,此时就可以用此属性来定位问题,这个时候我们就可以掌握一个出问题的转发路径,从而方便进一步的排查问题。
    7. Range:

      • 对于只需要获取部分资源的范围请求,Range字段可以指定获取资源范围Range: bytes=10001-20000
      • 例子中表示请求获取从第10001字节到20000字节的资源
      • 服务器处理请求后会返回206 Partial Content的响应。无法处理时,则会返回状态码200 OK的响应及其全部资源

    响应报文首部 (共9种)

    首部字段名解释
    1.Accept-Ranges是否接受字节范围请求
    2.Age推算资源创建经过时间
    3.ETag资源的匹配信息
    4.Location令客户端重定向至指定URI
    5.Proxy-Authenticate代理服务器对客户端的认证信息
    6.Retry-After对再次发起请求的时机要求
    7.ServerHTTP服务器的安装信息
    8.Vary代理服务器缓存的管理信息
    9.WWW-Authenticate服务器对客户端的认证信息
    1. Accept-Ranges

      • Accept-Ranges:bytes 可以处理范围请求
      • Accept-Ranges:none 不可以处理范围请求
    2. Age

      • 可以告知客户端,源服务器多久之前创建了资源,单位是秒
      • 若创建该响应的是缓存服务器,则Age值是指缓存后的响应再次发起发起认证到认证完成的时间值。代理创建响应时必须加上首部字段Age
    3. ETag

      它是一种可将资源以字符串形式做唯一标识的方式,服务器会为每份资源分配对应的ETag值,资源被更新时,ETag值也会被更新,并没有统一的算法规则,而是由服务器来分配
      • ETag:无论实体发生多么细微的变化都会改变其值
      • ETag:只用于提示资源是否相同,只有资源发生了根本的改变才会改变ETag值,这时会在字段值最开始加W/,

    ETag:W/"XXX"

    1. Location

      • 使用该响应字段可以将响应接收方引导至某个与请求的URI位置不同的资源
      • 基本上,该字段配合3XX,Redirection的响应,提供重定向的URI
    2. Vary

      首部字段vary可对缓存进行控制,源服务器会向代理服务器传达关于本地缓存使用方法的命令
      • 当代理服务器接收到服务器返回包含Vary指定项的响应后,仅对请求中含有相同Vary指定首部字段的请求返回缓存
      • 即使对相同资源发起请求,但是由于Vary指定的首部字段不相同,因此必须从源服务器重新获取资源
      • 例如下面这个,如果使用的Accept-Language:en-us字段的值相同,那么直接从缓存返回响应,否则从源服务器请求资源后再返回响应

    实体报文首部 (共10种)

    首部字段名解释
    1.Allow资源可支持的HTTP方法
    2.Content-Encoding实体主体适用的编码方式
    3.Content-Language实体主体的自然语言
    4.Content-Length实体主体的大小(单位:字节)
    5.Content-Location代替对应资源的URI
    6. Content-MD5实体主体的报文摘要
    7. Content-Range实体主体的位置范围
    8. Content-Type实体主体的媒体类型
    9.Expires实体主体过期的日期时间
    10.Last-Modified资源的最后修改日期时间

    其他字段(cookie等)

    • cookie,我们下面单独讲这个

    cookie

    • 注 : 文中例子中的各种请求,报文,均来自 京东物流官网
    • ps:小杰个人挺喜欢JDL的标语的,有速度,更有温度,祝JDL越来越好!

    set-cookie

    属性解释
    NAME=VALUE赋予Cookie的名称和其值(必需项)
    expires = DATECookie的有效期(若不指定则默认为浏览器关闭为止)
    path=PATH将服务器上的文件目录作为Cookie的适用对象(若不指定则默认为文档所在的文件目录)
    domin=域名作为Cookie适用对象的域名(若不指定则默认为创建Cookie的服务器域名)
    Secure仅在HTTPS安全通信时才会发送Cookie
    HttpOnly加以限制,使Cookie不能被JS脚本访问,主要目的是为了防止跨站脚本攻击(Cross Site Scripting,XSS)对cookie的窃取

    谷歌浏览器控制台查看cookie

    • cookie中的thorJSESSIONID这两个key的后HttpOnly属性被打上了√,就表明,此key无法被js脚本访问,防止跨站脚本攻击(Cross Site Scripting,XSS)对cookie的窃取

    我们来看下再console控制台输入document.cookie
    得出的cookie无法找到这两个key

    因为这个属性JSESSIONID比较重要,存储的是sessionId,这个要是被别人拿到的话,别人就可以冒充我在网站上做某些事情了,像我自己一样请求某些数据了

    postman 模拟拿到cookie后发送请求

    我把网页上的cookie拿下来,放到postman里测试,发现和我自己在网站上请求数据是一样的

    cookie存储的地方,清理缓存到底是清理什么?

    清理缓存主要就是清理cookie,抹去自己登陆痕迹以及浏览器中的资源缓存,重新请求网站资源

    HTTP 与 HTTPS

    HTTP不足

    • 通信使用明文(不加密),内容可能会被篡改
    • 不验证通信方的身份,因此有可能遭遇伪装
    • 无法证明报文的完整性,所以有可能已遭遇篡改

    HTTPS结构

    HTTPS是身披SSL(Secure Socket Layer)外壳的HTTP

    • 在采用SSL后,HTP就拥有了HTTPS的加密、证书和完整性保护这些功能
    • 想要了解HTTPS是怎么加密的,得先了解下下面两种提到的加密技术

    对称加密原理

    • 客户端和服务端约定好用同一把密钥
    • 这把密钥可以对数据进行加密/解密

    • 客户端和服务端之间的共享密钥的传送问题也是一个问题,如果能够安全传送不被截获的话,那岂不是数据也可以安全的传送到不被截获?鸡生蛋蛋生鸡的问题。
    • 图中客户端和服务端传输加密数据的时候,如果双方的共享密钥泄露的被黑客截取到的话,黑客就可以用它来解开这加密的数据,所以对称加密不安全

    非对称加密原理(公开密钥加密)

    • 一共有两把密钥(是一对),一个公开密钥,一个私人密钥
    • 公开密钥加密的数据,只有对应的私钥才可以解密,
    • 私钥加密的数据,公钥也可以解密

    • 问题就是,从服务端发送给客户端数据时无法保证数据的安全性,因为此时有可能黑客截获到了公钥,对私钥加密的数据进行了解密
    • 服务器端为什么不发送用公钥加密的数据?因为客户端没有私钥,无法解密。

    混合加密原理

    聪明的大佬们用两种加密算法混合了一下

    • 客户端一开始向服务端传输的是,用公开密钥加密的共享密钥!
    • 这样的话,服务端收到这个加密的数据后用自己的私钥密钥解密后得到的就是共享密钥,以后和客户端交互时都用这个共享密钥就可以啦,因为黑客是无法获得这个共享密钥的,毕竟公开密钥加密的数据,只有对应的私钥才可以解密,而这个私钥一开始就在服务端手里而不在黑客手里

    我曾经以为这样就万无一失了,文章也就到此结束了,可以和血包杀手愉快的timi了,可是,你有没有听说过,中间人攻击

    中间人攻击

    黑客拦截”用公开加密密钥机密后的共享密钥“后不是解密不了吗,好,那我就不拦截这个了,我拦截第一个请求好吧,我拦截服务端传给你的公开密钥,我拦截到了,我再给你个假的,(像极了《让子弹飞》中,张麻子与马邦德的关系,出任鹅城县长)。从根上就伪装成你,以后就等于我是个中间人(中转站),所有的请求,数据都要经过我,那我就可以记录下来其中你的敏感数据,可怕。

    • 其实中间人攻击还要有好多种,以后有机会写一写,我们先大概了解下是什么意思就好~

    数字证书

    • 所以现在问题又到了这里,我无法确保这个公钥是服务端发给我的,还是中间人发给我的?可这世上没有用钱解决不了的问题,虽然我确保不了,可是有人可以确保,就是得花钱,我们可以使用由数字证书认证机构(CA)和其他相关机构颁发的公开密钥证书
    • 数字证书认证机构处于客户端和服务器双方都信赖的第三方机构的立场上。有兴趣的同学可以自行去了解下~
    • 所以HTTPS靠非对称性加密及数字证书保证了安全性

    写在最后

    总结

    • 此文章从HTTP报文结构开始,到HTTP首部,到返回状态码,到cookie,再延伸到HTTPS加密方式,每一部分都进行了详细的介绍,希望对大家有用!

    往期精彩推荐

    絮絮叨叨

    • 如果大家觉得这篇文章对自己有一点点帮助的话,欢迎关注此公众号 java小杰要加油
    • 原创不易,实需鼓励
    若文章有误欢迎指出,靓仔靓女们,我们下篇文章见,扫一扫,开启我们的故事

    查看原文

    赞 0 收藏 0 评论 0

    好懂事一男的 发布了文章 · 2020-11-30

    工具人实锤!我用java中的文件IO流帮同事处理了足足18M的文本数据,泪目(一)

    更多精彩请关注微信公众号java小杰要加油,京东工程师和你一起成长
    • 全篇是基于磁盘文件IO操作

    关注此公众号java小杰要加油 ,后台回复“09IO” 即可获得此思维导图以及文中全套代码,重要的地方都有备注及注释

    流的概念

    流,其实是个抽象的概念,就像我们生活中常见的水流一样,那么水流就有从哪里来?到哪里去?这两个问题,就分别对应的java中的数据源目的地,流中传送的是java中要处理的数据,可以是字符形式也可以是字节形式

    流的分类有以下几种:

    1. 按流的传送方向分:输入流 Input,输出流 Output
    2. 按流中的数据格式分:字节流,字符流

      • 字节流(Stream)可以处理一些文件照片视频ppt等
      • 字符流(Writer Reader)只能处理纯文本文件,例如txt文件

    如下图所示

    我以前学的时候总是搞不清楚输入流输出流到底是从哪里来到哪里去,今天总结一下,感觉还挺便于理解的。

    首先我们始终记住一点:我们的输入流、输出流是相对我们编写的应用程序来说的。

    假如说我们有一个A.txt文件,我们编写了一个java程序,想操作这个A文件,将操作后的结果变为B文件。 那么这时

    • 输入流就是从A文件到我们应用程序的这段流(从A文件输入到了我们的应用程序中,读,就是读取A文件中的数据)
    • 输出流就是从我们的应用程序到B文件的这段流(从应用程序输出到了B文件中,写,就是写入到B文件中)

    实战演练之需求思路

    说到API,这个IO流确实真的是太太太讨厌了,API真的是太多太多了,就像高中背课文一样,还总忘,着实很尴尬,不过我今天就把我的一些总结理解通过这个真实的例子写出来(只是操作磁盘文件API),感觉或许会帮到一些忙呢
    • 需求:现在有一个A文件,A文件每一行的语句都有双引号,我们需要编写个程序,将每一行的双引号去掉,再把结果写到B文件中,达到下图的效果就行

    A文件

    A文件

    注意: 每一行的双引号都去掉了

    B文件

    B文件

    1. 首先,我们操作A文件的话,肯定得有A文件这个对象对吧 ,他就是File, 以后的输入输出流缓冲区等等都是围绕它的
    2. 其次,就像我们上一节说到的,我们得定义个输入流对吧,得把A文件的数据出来,输入到我们编写的应用程序中去
    3. 最后,也像我们上一节说到的,我们得定义个输出流对吧,得把我们应用程序处理好的数据进去,输出到我们要存放的B文件中

    实战演练之代码实现

    关注此公众号 java小杰要加油 ,后台回复“09IO” 即可获得此思维导图以及文中全套代码,重要的地方都有备注及注释

    老大现在发给了我们一个A.txt,让我们处理下,一个小时之后把处理好的文本B.txt发给他,所以我们现在有文件A,自己也可以创建个文件B.txt,如图所示

    我们来实现下

    • 输入流:
    //输入流 (读取数据到程序中)
        public static  List<String> read(String APath) throws IOException {
            //创建一个字节输入流  从A.txt里读取数据出来
            FileInputStream fileInputStream = new FileInputStream(APath);
            // 因为字节流的话,没有行的概念,需要转换成字符流
            InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8");
            //从转换的字符输入流中读取文本,这个时候就有行的概念了
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            //读出来的每一行的值
            String s = "";
            //用个list存储修改后读出来的每一行的值
            List<String> list = new ArrayList<String>();
            while ((s = bufferedReader.readLine()) != null) {
                //对读出来的这一行的值进行操作,这次我们是替换引号,以后就是根据业务来操作了
                s =s.replace(""","");
                //将修改后的值存储进list
                list.add(s);
            }
            //关掉资源
            bufferedReader.close();
            inputStreamReader.close();
            fileInputStream.close();
            //打印下list,看下我们的list存储的数据对不对(是不是去除引号后的数据)
            for (int i=0;i<list.size();i++){
                System.out.println(list.get(i));
            }
            return list;
        }

    运行后控制台的结果是

    • 输出流:
    //输出流(从程序中输出到B文本文件)
        public static void writer(String BPath, List<String> list) throws IOException{
             //字节输出流,true的意思是追加在文件末尾,默认是false不追加,替换
             FileOutputStream fileOutputStream = new FileOutputStream(BPath,true);
             //将字节流转换为字符流
             OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);
          //从转换的字符输出流中写入文本,这个时候就有行的概念了
           BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
            for (int i=0;i<list.size();i++){
                //将list内的内容写入到文件中
                bufferedWriter.write(list.get(i));
                //换行
                bufferedWriter.newLine();
            }
            //关闭流
            bufferedWriter.close();
            System.out.println("写入结束啦");
        }

    这时我们打开B文件会发现内容已经从无到有了 B文件

    我们代码中做了很多层转换,例如编写输入流时的代码

    //创建一个字节输入流  从A.txt里读取数据出来
            FileInputStream fileInputStream = new FileInputStream(APath);
            // 因为字节流的话,没有行的概念,需要转换成字符流
            InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8");
            //从转换的字符输入流中读取文本,这个时候就有行的概念了
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

    编写输出流时的代码

    //字节输出流,true的意思是追加在文件末尾,默认是false不追加,替换
             FileOutputStream fileOutputStream = new FileOutputStream(BPath,true);
             //将字节流转换为字符流
             OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);
          //从转换的字符输出流中写入文本,这个时候就有行的概念了
           BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);

    那我们可不可以简化下呢?当然是可以的啦

    我们可以通过FileReaderFileWriter来简化上面的字符流的读写操作。

    • FileReader
    FileReader fileReader = new FileReader(APath);
    BufferedReader bufferedReader = new BufferedReader(fileReader);
    • FileWriter
    FileWriter fileWriter = new FileWriter(BPath);
    BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);

    结果还是一样的,但是API变的更加精炼了起来。

    API关系梳理

    我们通过这个我实际遇到过的问题,来熟悉了解了下IO流的一些操作,我再梳理总结一下,如下图所示(若有错误请指出,谢谢大佬们指点

    • 备注:若A->B 构造方法参数,则代表
    A a = new A();
      B b = new B(a);
    • 其实我们只要静下心来好好看看这个图,然后动手操作几次应该就会有个清晰的认知,而不是一上来就去看各种各样的API,我反正是记不下来
    关注此公众号java小杰要加油 ,后台回复“09IO” 即可获得此思维导图以及文中全套代码,重要的地方都有备注及注释
    • 文中的这个例子,是我一个同事让我帮忙处理一个大文本数据而产生的,那个大文本数据好多好多行,足足有18M,这篇文章,仅仅是个开始,后期我还会好好打磨代码进行下效率对比进行下效率对比(缓冲区?多线程?),感兴趣的就请关注我吧,以后处理大文本再也不怕啦,做一名合格的工具人!
    更多精彩请关注微信公众号java小杰要加油,京东工程师和你一起成长

    往期精彩推荐

    查看原文

    赞 0 收藏 0 评论 0

    好懂事一男的 发布了文章 · 2020-11-08

    学会了volatile,你变心了,我看到了

    更多精彩文章,请关注xhJaver,京东工程师和你一起成长

    volatile 简介

    一般用来修饰共享变量,保证可见性和可以禁止指令重排

    • 多线程操作同一个变量的时候,某一个线程修改完,其他线程可以立即看到修改的值,保证了共享变量的可见性
    • 禁止指令重排,保证了代码执行的有序性
    • 不保证原子性,例如常见的i++

      (但是对单次读或者写保证原子性)

    可见性代码示例

    以下代码建议使用PC端来查看,复制黏贴直接运行,都有详细注释

    我们来写个代码测试一下,多线程修改共享变量时究竟需不需要用volatile修饰变量

    1. 首先,我们创建一个任务类
     public class Task implements Runnable{
     @Override
     public void run() {
     System.out.println("这是"+Thread.currentThread().getName()+"线程开始,flag是 "+Demo.flag);
     //当共享变量是true时,就一直卡在这里,不输出下面那句话
     // 当flag是false时,输出下面这句话
     while (Demo.flag){
     }
     System.out.println("这是"+Thread.currentThread().getName()+"线程结束,flag是 "+Demo.flag);
     }
    } 

    2.其次,我们创建个测试类

    class Demo {
     //共享变量,还没用volatile修饰
     public static   boolean flag = true ;
     public static void main(String[] args) throws InterruptedException {
     System.out.println("这是"+Thread.currentThread().getName()+"线程开始,flag是 "+flag);
     //开启刚才线程
     new Thread(new Task()).start();
     try {
     //沉睡一秒,确保刚才的线程已经跑到了while循环
     //要不然还没跑到while循环,主线程就将flag变为false
     Thread.sleep(1000L);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     //改变共享变量flag转为false
     flag = false;
     System.out.println("这是"+Thread.currentThread().getName()+"线程结束,flag是 "+flag);
     }
    }

    3.我们查看一下输出结果

    可见,程序并没有结束,他卡在了这里,为什么卡在了这里呢,就是因为我们在主线程修改了共享变量flag为false,但是另一个线程没有感知到,这个变量的修改对另一个线程不可见

    • 如果要是用volatile变量修饰的话,结果就变成了下面这个样子

    public static volatile boolean flag = true

    可见,这次主线程修改的变量被另一个线程所感知到了,保证了变量的可见性

    可见性原理分析

    那么,神奇的 volatile 底层到底做了什么呢,你的改变,逃不过他的法眼?为什么不用他修饰变量的话,变量的改变其他线程就看不见?

    回答此问题的时候首先,我们需要了解一下JMM(Java内存模型)

    注:本地内存是JMM的一种抽象,并不是真实存在的,本地内存它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化之后的一个数据存放位置
    • 由此我们可以分析出来,主线程修改了变量,但是其他线程不知道,有两种情况

      1. 主线程修改的变量还没有来得及刷新到主内存中,另一个线程读取的还是以前的变量
      2. 主线程修改的变量刷新到了主内存中,但是其他线程读取的还是本地的副本
    • 当我们用 volatile 关键字修饰共享变量时就可以做到以下两点

      1. 当线程修改变量时,会强制刷新到主内存中
      2. 当线程读取变量时,会强制从主内存读取变量并且刷新到工作内存中

    指令重排

    • 何为指令重排?

    为了提高程序运行效率,编译器和cpu会对代码执行的顺序进行重排列,可这有时候会带来很多问题

    我们来看下代码

    //指令重排测试
    public class Demo2 {
    private Integer number = 10;
    private boolean flag = false;
    private Integer result = 0;
    public void  write(){
    this.flag = true; // L1
    this.number = 20; // L2
    }
    public void  reader(){
    while (this.flag){ // L3
    this.result = this.number + 1; // L4
    }
    }
    }

    假如说我们有A、B两个线程 他们分别执行write()方法和 reader()方法,执行的顺序有可能如下图所示

    • 问题分析: 如图可见,A线程的L2和L1的执行顺序重排序了,如果要是这样执行的话,当A执行完L2时,B开始执行L3,可是这个时候flag还是为false,那么L4就执行不了了,所以result的值还是初始值0,没有被改变为21,导致程序执行错误

    这个时候,我们就可以用volatile关键字来解决这个问题,很简单,只需

    private volatile Integer number = 10;

    • 这个时候L1就一定在L2前面执行
    A线程在修改number变量为20的时候,就确保这句代码的前面的代码一定在此行代码之前执行,在number处插入了内存屏障 ,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排

    内存屏障

    内存屏障又是什么呢?一共有四种内存屏障类型,他们分别是

    1. LoadLoad屏障:

      • Load1 LoadLoad Load2 确保Load1的数据的装载先于Load2及所有后续装载指令的装载
    2. LoadStore屏障:

      • Load1 LoadStore Store2 确保Load1的数据的装载先于Store2及所有后续存储指令的存储
    3. StoreLoad屏障:

      • Store1 StoreLoad Load2 确保Store1的数据对其他处理器可见(刷新到内存)先于Load2及所有后续的装载指令的装载
    4. StoreStore屏障:

      • Store1 StoreStore Store2 确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储
    > StoreLoad 是一个全能型的屏障,同时具有其他3个屏障的效果。执行该屏障的花销比较昂贵,因为处理器通常要把当前的写缓冲区的内容全部刷新到内存中(Buffer Fully Flush)
    
    
    • 装载load 就是读 int a = load1 ( load1的装载)
    • 存储store就是写 store1 = 5 ( store1的存储)

    volatile与内存屏障

    那么volatile和这四种内存屏障又有什么关系呢,具体是怎么插入的呢?

    1. volatile写 (前后都插入屏障)

      • 前面插入一个StoreStore屏障
      • 后面插入一个StoreLoad屏障
    2. volatile读(只在后面插入屏障)

      • 后面插入一个LoadLoad屏障
      • 后面插入一个LoadStore屏障

    官方提供的表格是这样的

    我们此时回过头来在看我们的那个程序

    this.flag = true; // L1
    this.number = 20; // L2

    由于number被volatile修饰了,L2这句话是volatile写,那么加入屏障后就应该是这个样子

    this.flag = true; // L1
    //  StoreStore  确保flag数据对其他处理器可见(刷新到内存)先于number及所有后续存储指令的存储
    this.number = 20; // L2
    // StoreLoad  确保number数据对其他处理器可见(刷新到内存)先于所有后续存储指令的装载

    所以L1,L2的执行顺序不被重排序

    ps:总部四号楼真是越来越好了,奖励自己一杯奶茶

    更多精彩,请关注公众号xhJaver,京东工程师和你一起成长

    往期精彩

    查看原文

    赞 0 收藏 0 评论 0

    好懂事一男的 发布了文章 · 2020-11-01

    京东这道面试题你会吗?

    详解一道京东面试题

    • 跟多精彩请关注公众号“xhJaver”,京东java工程师和你一起成长
    多线程并发执行?线程之间通信? 这是我偶尔听到我同事做面试官时问的一道题,感觉很有意思,发出来大家和大家讨论下

    面试题目描述

    现在呢,我们有三个接口,就叫他A,B,C吧,这三个接口都是查询某个人征信信息的,必须同时返回true,我们才认为这个人的征信合格,如果其中某一个返回false的话,就表明这个人的征信不合格,如果是你,你会怎么设计怎么写这个代码呢?

    第一次思考

    首先,一定是并发执行,假如说A接口执行3秒,B接口执行5秒,C接口执行8秒的话

    • 串行执行: 3+5+8 = 16秒
    • 并发执行: 8=8秒 (时间最久的那个接口执行的时间就是这三个接口的执行总时间)

    熟悉的感觉,多线程执行任务,我在第二章文章实战!xhJaver竟然用线程池优化了。。。有提过怎么写,感兴趣的读者可以回去看一下,不过我在这里再写一下,话不多说来看下代码

    并发代码

    建议用PC端查看,所有代码都可直接复制运行,代码中重要的点都有详细注释
    1. 首先,我们先定义这三个接口
    public class DoService {
        //设置A B C接口的返回值,b接口设置的是false
        private static Boolean flagA = true;
        private static Boolean flagB = false;
        private static Boolean flagC = true;
        public static   boolean A(){
            long start = System.currentTimeMillis();
            try {
                Thread.sleep(3000L);
            } catch (InterruptedException e) {
                System.out.println("a被打断  耗时" + (System.currentTimeMillis() - start));
                   e.printStackTrace();
            }
            System.out.println("a耗时  "+(System.currentTimeMillis() - start));
            return flagA;
        }
        public static boolean B() {
            long start = System.currentTimeMillis();
            try {
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                System.out.println("b被打断  耗时" + (System.currentTimeMillis() - start));
                e.printStackTrace();
            }
            System.out.println("b耗时  "+(System.currentTimeMillis() - start));
            return flagB;
        }
        public static boolean C() {
            long start = System.currentTimeMillis();
            try {
                Thread.sleep(8000L);
            } catch (InterruptedException e) {
                System.out.println("c被打断  耗时" + (System.currentTimeMillis() - start));
                e.printStackTrace();
            }
            System.out.println("c耗时  "+(System.currentTimeMillis() - start));
            return flagC;
        }
    }

    2 其次 我们先创造一个Task 任务类

    public class Task implements Callable<Boolean> {
     
        private String taskName;
        private Integer i;
        public  Task(String taskName,int i){
            this.taskName =taskName;
            this.i = i;
        }
        
        @Override
        public Boolean call() throws Exception {
            // 标记 返回值,代表这个接口是否执行成功
            Boolean flag = false;
            //记录接口名字
            String serviceName = null;
            //根据i的值来判断调用哪个接口
            if (i==1){
                flag   =    DoService.A();
                serviceName="A";
            }
            if (i==2){
                flag   =    DoService.B();
                serviceName="B";
            }
            if (i==3){
                flag   =    DoService.C();
                serviceName="C";
            }
            System.out.println("当前线程是: "+Thread.currentThread().getName()+"正在处理的任务是: "+this.taskName+"调用的接口是: "+serviceName);
            return flag;
        }
    }

    3 最后,我们定义一个测试类

    class Test {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //创建一个包含三个线程的线程池
            ExecutorService executorService = Executors.newFixedThreadPool(3);
            //事先准备好储存结果的list集合
            List< Future<Boolean> > list = new ArrayList<>();
            //开始计时
            long start = System.currentTimeMillis();
            for (int i=1;i<4;i++){
                Task task = new Task("任务"+i,i);
                //将每个任务提交到线程池中,并且得到这个线程的执行结果
                Future<Boolean> result = executorService.submit(task);
                list.add(result);
            }
            //记得把线程池关闭
            executorService.shutdown();
            //定义一个变量 0
            int count = 0;
            System.out.println("等待处理结果。。。");
            for (int i=0;i<list.size();i++){
                Future<Boolean> result = list.get(i);
                //得到处理的结果 线程阻塞,如果线程没有处理完就一直阻塞
                Boolean flag = result.get();
                //如果这个接口返回true,那么count就++
                if (flag){
                    count++;
                }
            }
            System.out.println("线程池+结果处理时间:"+ (System.currentTimeMillis() - start));
            //如果count数量为3,那么三个就都为true,代表这个人征信没问题
            if (count==3){
                System.out.println("合格");
            }else {//否则,就是有问题
                System.out.println("不合格");
            }
        }
    }

    4.我们看下输出结果

    等待处理结果。。。
    a耗时  3000
    当前线程是: pool-1-thread-1正在处理的任务是: 任务1调用的接口是: A
    b耗时  5000
    当前线程是: pool-1-thread-2正在处理的任务是: 任务2调用的接口是: B
    c耗时  8000
    当前线程是: pool-1-thread-3正在处理的任务是: 任务3调用的接口是: C
    线程池+结果处理时间:8008
    不合格
    • 我们运行的时候会发现,它的输出结果的顺序如下1 2 3 4 5输出结果次序 我们图中的2,3,4是再线程池内开了三个线程执行的,他们之间相隔一段时间才出现的,因为每个接口都有执行时间

    程序运行后,“标记2”是3秒后出现,“标记三”是5秒后出现,“标记4”是8秒后出现

    其实4和5相差时间很短,几乎是同时出现的,因为4执行完了就是主线程继续执行了

    线程池+结果处理的时间一共是8秒,而每个接口分别执行的时间是3秒,5秒,8秒,达到了我们所说的,多线程处理多个接口,总共耗时时间是耗时最长的接口的时间

    和京东面试官探讨

    波哥说(我爱叫他波哥,东北人,说话则逗,幽默的人简直就是人间瑰宝,其实我也蛮有趣的,就是没人发现),你这程序不行啊,有个缺点,假如说,你这个A接口,耗时三秒,他返回了false,那么你另外两个线程也不用执行了,这个人的征信已经不合格了,你需要判断下,如果某一个线程执行的任务返回了false,那么就及时中断其他两个线程

    灵光乍现

    上一次的代码已经实现了多线程执行任务,可是这线程间通信怎么办呢?怎么才能根据一个线程的执行结果而打断其他线程呢?我想到了以下几点

    1. 共享变量public static volatile boolean end = true;

      • 这个共享变量就代表是否结束三个线程的执行 如果为true的话,代表结束,false的话代表不结束线程执行
    2. 计数器public static AtomicInteger count =new AtomicInteger(0);

      • 每当每个线程执行完的话,如果返回true,计数器就+1,当计数器变为3的时候,就代表这个人征信没问题
    3. 中断方法interrupt()

      • 我们会单独开个线程一直循环检测这个变量,当检测到为true的时候,就会调用中断方法中断这三个线程
    4. 阻塞线程countDownLatch

      • 我们程序往下执行需要获取结果,获取不到这个结果的话,就要一直等着。我们可以用这个线程阻塞的工具,一开始给他设置数量为1,当满足继续向下执行的条件时,调用countDownLatch.countDown();,在主线程那里countDownLatch.await();一下这样当检测到数量为0的时候,主线程那里就继续往下执行了,话不多说,来看代码

    代码优化

    建议用PC端查看,所有代码都可直接复制运行,代码中重要的点都有详细注释
    1. 首先,还是创建接口
    public class DoService {
        private static Boolean flagA = true;
        private static Boolean flagB = false;
        private static Boolean flagC = true;
        public static   boolean A(){
            long start = System.currentTimeMillis();
            try {
                Thread.sleep(3000L);
            } catch (InterruptedException e) {
                System.out.println("a被打断  耗时" + (System.currentTimeMillis() - start));
                   e.printStackTrace();
            }
            System.out.println("a耗时  "+(System.currentTimeMillis() - start));
            return flagA;
        }
        public static boolean B() {
            long start = System.currentTimeMillis();
            try {
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                System.out.println("b被打断  耗时" + (System.currentTimeMillis() - start));
                e.printStackTrace();
            }
            System.out.println("b耗时  "+(System.currentTimeMillis() - start));
            return flagB;
        }
        public static boolean C() {
            long start = System.currentTimeMillis();
            try {
                Thread.sleep(8000L);
            } catch (InterruptedException e) {
                System.out.println("c被打断  耗时" + (System.currentTimeMillis() - start));
                e.printStackTrace();
            }
            System.out.println("c耗时  "+(System.currentTimeMillis() - start));
            return flagC;
        }
    }

    2 创建任务

    public class Task implements Runnable {
        private String name ;
        public Task( String name){
            this.name = name;
        }
        
        @Override
        public void run() {
            boolean flag = false;
            String serviceName = null;
            if(this.name.equals("A")){
                serviceName = "A";
                 flag = DoService.A();
            }
            if(this.name.equals("B")){
                serviceName = "B";
                flag = DoService.B();
            }
            if(this.name.equals("C")){
               serviceName = "C";
               flag = DoService.C();
           }
            //如果有一个为false
            if (!flag){
                //就把共享标志位置为false
                Test.end = false;
            }else {
                //计数器加一,到三的话就是三个都为true
                Test.count.incrementAndGet();
           }
            System.out.println("当前线程是: "+Thread.currentThread().getName()+"正在处理的任务是: "+this.name+"调用的接口是: "+serviceName);
        }
    }

    3 创建测试类

    class Test {
        //设置countDownLatch 里面计数为1,
        // 只调用一次countDownLatch.countDown就可以继续执行 countDownLatch.await();
        //后面的代码了,接触阻塞
        public static CountDownLatch countDownLatch = new CountDownLatch(1);
        //默认都为true,有一个线程为false了,那么就变为false
        public static volatile boolean end = true;
        //计数器,数字变为3的时候代表三个接口都返回true,线程安全的原子类
        public static AtomicInteger count =new  AtomicInteger(0);
        public static void main(String[] args) throws InterruptedException {
            long start = System.currentTimeMillis();
            //创建三个任务,分被调用A B C 接口
            Task taskA = new Task("A");
            Task taskB = new Task("B");
            Task taskC = new Task("C");
            //创建三个线程
            Thread tA = new Thread(taskA);
            Thread tB = new Thread(taskB);
            Thread tC = new Thread(taskC);
            //开启三个线程
            tA.start();
            tB.start();
            tC.start();
            //在开启一个线程,这个线程就是单独循环扫描这个共享变量的
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //此线程一直循环判断这个结束变量,如果为false的话,就代表有一个接口返回false,跳出,重点其他线程
                    while (true){
                        if (!end ){
                            //当这个共享变量为false时i表示,其他线程可以中断了,所以就打断他们执行
                            tA.interrupt();
                            tB.interrupt();
                            tC.interrupt();
                            //如果某个线程被打断的话,就表明不合格
                            System.out.println("不合格");
                            //countDownLatch 计数器减一
                            countDownLatch.countDown();
                            break;
                        }
                        if (Test.count.get()==3){
                            System.out.println("合格");
                            //countDownLatch 计数器减一
                            countDownLatch.countDown();
                            break;
                        }
                    }
                }
            }).start();
            System.out.println(Thread.currentThread().getName()+"主线程开始挂起");
            //阻塞主线程继续执行,等待其他线程计算完结果在执行下去,countDownLatch中的计数为0时,就可以继续执行下去
            countDownLatch.await();
            System.out.println(Thread.currentThread().getName()+" 主线获得结果后继续执行"+(System.currentTimeMillis() - start));
        }
    }

    4 我们看下输出结果

    main主线程开始挂起
    a耗时  3024
    当前线程是: Thread-0正在处理的任务是: A调用的接口是: A
    b耗时  5000
    当前线程是: Thread-1正在处理的任务是: B调用的接口是: B
    c被打断  耗时5001
    不合格
    java.lang.InterruptedException: sleep interrupted
     at java.lang.Thread.sleep(Native Method)
     at com.xhj.concurrent.executor_05._02.DoService.C(DoService.java:41)
     at com.xhj.concurrent.executor_05._02.Task.run(Task.java:30)
     at java.lang.Thread.run(Thread.java:748)
    c耗时  5003
    当前线程是: Thread-2正在处理的任务是: C调用的接口是: C
    main 主线获得结果后继续执行5014
    • 我们运行的时候会发现 执行结果

    由图可见,我们首先就把主线程挂起,等待其他四个线程的处理结果,三个线程分别处理那三个接口,另外一个线程循环遍历那个共享变量,当检测到为false时,及时打断其他线程,这样的话,就解决了上面的那个问题

    • 跟多精彩请关注公众号“xhJaver”,京东java工程师和你一起成长

    往期推荐

    查看原文

    赞 0 收藏 0 评论 0

    好懂事一男的 发布了文章 · 2020-10-28

    近万字,就是为了和你聊聊线程池

    更多精彩请关注公众号xhJaver,京东java工程师和你一起成长

    我们知道,在计算机中创建一个线程和销毁一个线程都是十分耗费资源的操作,有一种思想叫做,池化思想,就是说我们创建个池子,把耗费资源的操作都提前做好,后面大家一起用创建好的东西,最后统一销毁。省去了用一次创建一次,销毁一次,这种耗费资源的操作。

    一、线程池工作原理

    线程池就是这种思想,他的基本工作流程如下图所示
    image

    那么他的核心线程,任务队列,这些又是什么呢?怎么设置呢?

    这些就要从代码入手了,我们先来看下线程池构造方法的代码

    二、线程池构造方法

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
    

    其实ThreadPoolExecutor有四种构造方法,不过底层都是用这个7个参数的构造方法,所以我们弄懂这一个就好了,以下是其他构造方法的底层实现

     public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  RejectedExecutionHandler handler) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 Executors.defaultThreadFactory(), handler);
        }
     public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 Executors.defaultThreadFactory(), defaultHandler);
        }
     public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 threadFactory, defaultHandler);
        }

    其中默认的拒绝策略是

    private static final RejectedExecutionHandler defaultHandler =
            new AbortPolicy();
    

    这些构造方法中的

     this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 threadFactory, defaultHandler);

    就是那七个参数的构造方法

    有点懵?没关系,接下来我们一个个的解析这七个参数的意思

    三、线程池参数介绍

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
    

    1. 第一个参数 corePoolSize 代表这个线程池的核心线程数

    2. 第二个参数 maximumPoolSize 代表这个线程池的最大线程数 (核心线程数 +非核心线程数)

    3. 第三个参数 keepAliveTime 代表这个线程池的非核心线程的空闲时的存活时间

    4. 第四个参数 unit 代表这个线程池的非核心线程的空闲存活时间的单位

    5. 第五个参数 workQueue 代表这个线程池的任务阻塞队列,jdk中有几种常见的阻塞队列

    • ArrayBlockingQueue:基于数组结构的有界阻塞队列
    • LinkedBlockingQueue:是一个基于链表结构的阻塞队列
    • SynchronousQueue :同步队列,只存储一个任务,插入任务时要等待(如果队列里有元素的话)取出任务时要等待(如果队列里没有元素的话)
    • PriorityBlockingQueue:优先级队列,进入队列的元素按照优先级会进行排序
    建议:建议使用有界队列,要是无界队列的话,任务太多的话可能会导致OOM

    6. 第六个参数 threadFactory(可以自定义) 代表这个线程池的创建线程的工厂,有两种

    • Executors.privilegedThreadFactory() 使用访问权限创建一个权限控制的线程。
    • Executors.defaultThreadFactory() 将创建一个同线程组且默认优先级的线程

    7. 第七个参数 handler(可以自定义) 代表这个线程池的拒绝处理任务的饱和策略,jdk默认提供了四种

    • new ThreadPoolExecutor.AbortPolicy(); 直接抛出异常
    • new ThreadPoolExecutor.CallerRunsPolicy(); 用当前调用者的线程中处理传过来的任务
    • new ThreadPoolExecutor.DiscardOldestPolicy(); 丢弃最老的一个任务,然后把传过来的任务加入到阻塞队列中
    • new ThreadPoolExecutor.DiscardPolicy(); 什么都不做,直接丢掉传过来的任务

    四、使用线程池例子

    基础概念也都看了,下面来看个使用线程池处理任务的小例子

    首先,我们先创建个任务类

    public class Task implements Runnable {
        private String taskName;
        
        public Task(String taskName) {
            this.taskName = taskName;
        }
        @Override
        public void run() {
            try {
                //模拟每个任务的耗时
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String name = Thread.currentThread().getName();
            System.out.println("这里是xhJaver,线程池系列 当前线程名字是 " + name+"  处理了  "+ taskName+"  任务");
        }
    }
    

    我们再来看测试类

    public class Demo1 {
        public static void main(String[] args) {
            //阻塞队列,设置阻塞任务最多为10个
            BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable> (10);
            //线程工厂
            ThreadFactory threadFactory = Executors.defaultThreadFactory();
            //拒绝策略 当线程池的最大工作线程跑满以及阻塞队列满了的话,会由拒绝策略处理剩下的任务
            ThreadPoolExecutor.AbortPolicy abortPolicy = new ThreadPoolExecutor.AbortPolicy();
            //创建线程池  核心线程数为5  最大线程数为10 非核心线程空闲存活时间为60s
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60L,
                            TimeUnit.SECONDS, blockingQueue, threadFactory, abortPolicy
            );
            for (int i=0;i<10;i++){
                //创建10个任务,如果要是创建>20个任务,则20以外的任务会交由拒绝策略处理
                Task task = new Task("task" + i);
                //让我们自定义的线程池去跑这些任务
                threadPoolExecutor.execute(task);
            }
             //记得要关闭线程池
            threadPoolExecutor.shutdown();
        }
    }
    

    输出结果是

    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task0  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-2  处理了  task1  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-3  处理了  task2  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-4  处理了  task3  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-5  处理了  task4  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task5  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-2  处理了  task6  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-5  处理了  task9  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-4  处理了  task8  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-3  处理了  task7  任务
    

    五、线程工厂是什么 ?

    文中一直说线程工厂线程工厂,这线程工场到底是干嘛的呢? 当然是创建线程的工厂啦,创建线程,线程当然得有个名字咯,就像刚才的小例子输出的一样,线程的名字是pool-1-thread-3等等,我现在不想叫这个名字了,那就叫thread-xhJaver吧,这是自定义的名字,那怎么自定义呢?

    首先,要实现ThreadFactory接口中的Thread newThread(Runnable r)方法, 传入一个任务,返回一个自定义线程,如下面的代码一样

    public class DIYThreadFactory implements ThreadFactory {
        private AtomicInteger atomicInteger;
        public  DIYThreadFactory( AtomicInteger atomicInteger){
             this.atomicInteger =  atomicInteger;
        }
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("xhJaver-thread-"+atomicInteger.getAndIncrement());
            return thread;
        }
    }
    

    然后在使用时传入这个自定义的线程工厂

    public static void main(String[] args) {
     //阻塞队列,设置阻塞任务最多为10个
    BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable> (10);
     //创建线程安全的计数器
     AtomicInteger atomicInteger = new AtomicInteger();
     //自定义线程工厂
     ThreadFactory threadFactory = new DIYThreadFactory(atomicInteger);
     //拒绝策略 当线程池的最大工作线程跑满以及阻塞队列满了的话,会由拒绝策略处理剩下的任务
    ThreadPoolExecutor.AbortPolicy abortPolicy = new ThreadPoolExecutor.AbortPolicy();
     //创建线程池  核心线程数为5  最大线程数为10 非核心线程空闲存活时间为60s
     ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60L,
       TimeUnit.SECONDS, blockingQueue, threadFactory, abortPolicy
     );
     for (int i=0;i<10;i++){
      //创建10个任务,如果要是创建>20个任务,则20以外的任务会交由拒绝策略处理
      Task task = new Task("task" + i);
      //让我们自定义的线程池去跑这些任务
      threadPoolExecutor.execute(task);
     }
     //记得要关闭线程池
     threadPoolExecutor.shutdown();
    }
    

    输出结果是

    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-0  处理了  task0  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-1  处理了  task1  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-4  处理了  task4  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-3  处理了  task3  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-2  处理了  task2  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-0  处理了  task5  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-1  处理了  task6  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-2  处理了  task9  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-3  处理了  task8  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-4  处理了  task7  任务
    

    我也学会了自定义线程工厂了,可自定义名字到底有用呢,当然是排查问题啊!把线程名字定义为和自己业务有关的名字,到时候报错的时候就方便排查了。

    六、 拒绝策略是什么

    线程工厂可以自定义,那拒绝策略可以自定义吗?当然可以啦 方法如下,首先也要实现一个RejectedExecutionHandler接口,重写rejectedExecution 这个方法

    public class DIYRejectedHandler implements RejectedExecutionHandler {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        //记录日志等操作
            System.out.println("这是xhJaver无法处理的任务  "+r.toString()+"  当前线程名字是 "+Thread.currentThread().getName());
        }
    }
    

    然后在使用时传入这个自定义的拒绝策略

    public static void main(String[] args) {
     //阻塞队列,设置阻塞任务最多为10个
     BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable> (10);
     //创建线程安全的计数器
     AtomicInteger atomicInteger = new AtomicInteger();
     //自定义线程工厂
     ThreadFactory threadFactory = new DIYThreadFactory(atomicInteger);
     //自定义拒绝策略 当线程池的最大工作线程跑满以及阻塞队列满了的话,会由拒绝策略处理剩下的任务
     DIYRejectedHandler diyRejectedHandler = new DIYRejectedHandler();
     //创建线程池  核心线程数为5  最大线程数为10 非核心线程空闲存活时间为60s
     ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60L,
       TimeUnit.SECONDS, blockingQueue, threadFactory, diyRejectedHandler
     );
     for (int i=0;i<30;i++){
      //创建10个任务,如果要是创建>20个任务,则20以外的任务会交由拒绝策略处理
      Task task = new Task("task" + i);
      //让我们自定义的线程池去跑这些任务
      threadPoolExecutor.execute(task);
     }
     //记得要关闭线程池
     threadPoolExecutor.shutdown();
    }
    

    输出结果是

    这是xhJaver无法处理的任务  Task{taskName='task20'}  当前线程名字是 main
    这是xhJaver无法处理的任务  Task{taskName='task21'}  当前线程名字是 main
    这是xhJaver无法处理的任务  Task{taskName='task22'}  当前线程名字是 main
    这是xhJaver无法处理的任务  Task{taskName='task23'}  当前线程名字是 main
    这是xhJaver无法处理的任务  Task{taskName='task24'}  当前线程名字是 main
    这是xhJaver无法处理的任务  Task{taskName='task25'}  当前线程名字是 main
    这是xhJaver无法处理的任务  Task{taskName='task26'}  当前线程名字是 main
    这是xhJaver无法处理的任务  Task{taskName='task27'}  当前线程名字是 main
    这是xhJaver无法处理的任务  Task{taskName='task28'}  当前线程名字是 main
    这是xhJaver无法处理的任务  Task{taskName='task29'}  当前线程名字是 main
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-5  处理了  task15  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-4  处理了  task4  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-3  处理了  task3  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-2  处理了  task2  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-1  处理了  task1  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-0  处理了  task0  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-9  处理了  task19  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-8  处理了  task18  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-7  处理了  task17  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-6  处理了  task16  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-4  处理了  task6  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-5  处理了  task5  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-3  处理了  task7  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-2  处理了  task8  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-1  处理了  task9  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-0  处理了  task10  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-9  处理了  task11  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-8  处理了  task12  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-7  处理了  task13  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-6  处理了  task14  任务
    

    七、常见的阻塞队列及注意点

    因为阻塞队列的知识太多了,后续我们会单独开篇来讲这个阻塞队列,先介绍几个常用的

    1.ArrayBlockingQueue 基于数组的有界队列

    2.LinkedBlockingQueue 基于链表的无界队列

    3.SynchronousQueue

    它内部只有一个元素,插入时如果发现内部有元素未被取走则阻塞,取元素时若队列没有元素则被阻 塞,直到有元素插入进来。

    搭配线程池使用如下 ,先创建任务类

    public class Task implements Runnable {
        private String taskName;
        public Task(String taskName) {
            this.taskName = taskName;
        }
        @Override
        public String toString() {
            return "Task{" +
                    "taskName='" + taskName + ''' +
                    '}';
        }
        @Override
        public void run() {
            try {
                //模拟每个任务的耗时
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String name = Thread.currentThread().getName();
            System.out.println("这里是xhJaver,线程池系列 当前线程名字是 " + name+"  处理了  "+ taskName+"  任务");
        }
    }
    

    再使用阻塞队列

    public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i=0;i<10;i++){
                //创建十个任务
                Task task = new Task("task" + i);
                //去跑任务
                executorService.execute(task);
            }
             //记得要关闭线程池
            executorService.shutdown();
        }
    

    其中newCachedThreadPool底层就使用的是SynchronousQueue

     public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }

    输出结果是

    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task0  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-2  处理了  task1  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-5  处理了  task4  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-4  处理了  task3  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-3  处理了  task2  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-6  处理了  task5  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-7  处理了  task6  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-10  处理了  task9  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-9  处理了  task8  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-8  处理了  task7  任务
    

    由此可见,线程池分别创建了十个线程来处理这十个任务,为什么呢? 这是因为,我每个任务的模拟处理时间是1s,当再来的任务发现阻塞队列中有任务还没被取走,就创建非核心线程处理刚来的这个任务,不断的来任务,不断的创建线程,所以用这个阻塞队列再搭配线程池的总线程数等参数设置可能会因为不断的创建线程而导致OOM。

    4.PriorityBlockingQueue 优先级队列 进入队列的元素会按照任务的优先级排序。并且必须实现Comparable接口。

    参数:priorityTask - 要比较的对象。 返回:负整数、零或正整数, 根据此对象是小于、等于还是大于指定对象(要比较的对象)。

    先创建一个带有优先级的任务

    public class PriorityTask implements Runnable , Comparable<PriorityTask>{
        private String taskName;
        // 优先级,根据这个数进行排序
        private Integer priority;
        public PriorityTask(Integer priority,String taskName) {
            this.priority = priority;
            this.taskName = taskName;
        }
        
        //这个compareTo方法的返回值如果是-1的话,则排序会认为传过来的任务比此任务的大,降序排列
        //这个compareTo方法的返回值如果是1的话,则排序会认为传过来的任务比此任务的小,升序排列
        @Override
        public int compareTo(PriorityTask priorityTask) {
            //Integer.compare返回 -1代表 传过来的任务的priority 比次任务的priority要小
            // Integer.compare 0   传过来的任务的priority 比次任务的priority一样大
            //Integer.compare  1   传过来的任务的priority 比次任务的priority要大
            return Integer.compare(priorityTask.priority,this.priority);
        }
        @Override
        public void run() {
            try {
                //模拟每个任务的耗时
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String name = Thread.currentThread().getName();
            System.out.println("这里是xhJaver,线程池系列 当前线程名字是 " + name+"  处理了  "+ taskName+"  任务");
        }
        @Override
        public String toString() {
            return "Task{" +
                    "taskName='" + taskName + ''' +
                    '}';
        }
    }
    

    Integer.compare 的 比较大小代码

    java
       public static int compare(int x, int y) {
            return (x < y) ? -1 : ((x == y) ? 0 : 1);
        }
    

    测试代码

     public static void main(String[] args) {
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1,
                    60L, TimeUnit.SECONDS,
                    new PriorityBlockingQueue());
            for (int i=0;i<5;i++){
                //创建十个任务
                PriorityTask priorityTask = new PriorityTask(i,"task" + i);
                //去跑任务
                threadPoolExecutor.execute(priorityTask);
            }
            for (int i=100;i>=95;i--){
                //创建十个任务
                PriorityTask priorityTask = new PriorityTask(i,"task" + i);
                //去跑任务
                threadPoolExecutor.execute(priorityTask);
            }
             //记得要关闭线程池
            threadPoolExecutor.shutdown();
        }

    输出结果是

    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task0  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task100  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task99  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task98  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task97  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task96  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task95  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task4  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task3  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task2  任务
    这里是xhJaver,线程池系列 当前线程名字是 pool-1-thread-1  处理了  task1  任务
    

    由输出结果可见,除了第一个以外,处理任务的顺序会按照优先级大小先处理

    八、几种常见的线程池及注意点

    他们分别是以下几种

    1.newFixedThreadPool

    • Executors.newFixedThreadPool(10) 它的构造方法是
     public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }

    有此可见,这个FixedThreadPool线程池的核心线程数和最大线程数一样,所以就没有非核心线程数,存活时间这个参数也就是无效的了,它底层用的是LinkedBlockingQueue这个阻塞队列,这个队列是个无界队列,可以点进去源码看它默认的容量是Integer.MAX_VALUE

     public LinkedBlockingQueue() {
            this(Integer.MAX_VALUE);
        }

    所以这会导致一个什么问题呢?就会导致,当核心线程都跑满的时候,再来新任务的话就会不断的添加至这个阻塞队列里面,一直加一直加,但是内存是有限的,所以有可能会出现 OOM(OutOfMemory) 的问题

    • Executors.newFixedThreadPool(10,Executors.defaultThreadFactory()); 这个构造方法可以传过来指定的创建线程的工厂
     public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>(),
                                          threadFactory);
        }

    2.newCachedThreadPool

    • Executors.newCachedThreadPool() 它的构造方法是
    public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
    

    由此可见,它的核心线程数默认是0,线程池总线程容量是Integer.MAX_VALUE,阻塞队列用的是SynchronousQueue同步队列,非核心线程数的空闲存活时间为60s,这会导致一个什么问题呢?只要来了一个任务,如果没有线程的话就创建一个非核心线程去跑这个任务,如果跑着的过程中又来了一个任务,就会继续创建线程去跑,以此类推,内存是有限的,不断的创建线程的话也会触发OOM问题

    • Executors.newCachedThreadPool(Executors.defaultThreadFactory()) 这个构造方法可以传过来指定的创建线程的工厂
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>(),
                                          threadFactory);
        }
    

    3.newSingleThreadExecutor

    • Executors.newSingleThreadExecutor() 它的构造方法是
    public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
    

    我们可以看出,它的核心线程数是一个,总线程数也是一个。底层用的是LinkedBlockingQueue阻塞队列 当来任务的时候线程池如果没有线程的话,则创建一个也是唯一一个线程来执行任务,剩下的任务都会被塞进无界阻塞队列里面,也是会有可能产生OOM问题。

    • Executors.newSingleThreadExecutor(Executors.defaultThreadFactory()) 这个构造方法可以传过来指定的创建线程的工厂
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>(),
                                        threadFactory));
        }
    

    九、拓展线程池

    什么?线程池还可以拓展?!是的,如果我想记录下每个任务的执行开始情况,结束情况,线程池关闭情况就要拓展啦,ThreadPoolExecutor它内部是提供了几个方法给我们拓展,其中beforeExecute、afterExecute、terminated,这三个分别对应任务开始,任务结束,线程池关闭的三种情况,所以我们就要重写他们啦,话不多说,看下代码

    public static void main(String[] args) {
     //阻塞队列,设置阻塞任务最多为10个
     BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable> (10);
     //创建线程安全的计数器
     AtomicInteger atomicInteger = new AtomicInteger();
     //自定义线程工厂
     ThreadFactory threadFactory = new DIYThreadFactory(atomicInteger);
     //自定义拒绝策略 当线程池的最大工作线程跑满以及阻塞队列满了的话,会由拒绝策略处理剩下的任务
     DIYRejectedHandler diyRejectedHandler = new DIYRejectedHandler();
     //创建线程池  核心线程数为5  最大线程数为10 非核心线程空闲存活时间为60s
     ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60L,
       TimeUnit.SECONDS, blockingQueue, threadFactory, diyRejectedHandler
     ){
      @Override
      protected void beforeExecute(Thread t, Runnable r) {
       System.out.println("xhJaver 当前线程是"+t.getName()+"开始处理任务:"+r.toString());
      }
      @Override
      protected void afterExecute(Runnable r, Throwable t) {
       if(t!=null){
        System.out.println("xhJaver 当前线程是"+Thread.currentThread().getName() +"处理任务结束:"+r.toString()+" 错误是 "+ t);
       }
       System.out.println("xhJaver 当前线程是"+Thread.currentThread().getName() +"处理任务结束:"+r.toString()+" 没有错误 ");
      }
      @Override
      protected void terminated() {
       System.out.println("xhJaver 当前线程是"+Thread.currentThread().getName() +"关闭线程池");
      }
     };
     for (int i=0;i<21;i++){
      //创建10个任务,如果要是创建>20个任务,则20以外的任务会交由拒绝策略处理
      Task task = new Task("task" + i);
      //让我们自定义的线程池去跑这些任务
      threadPoolExecutor.execute(task);
     }
     //记得要关闭线程池
     threadPoolExecutor.shutdown();
    }
    

    输出结果是

    这是xhJaver无法处理的任务  Task{taskName='task20'}  当前线程名字是 main
    xhJaver 当前线程是xhJaver-thread-7开始处理任务:Task{taskName='task17'}
    xhJaver 当前线程是xhJaver-thread-6开始处理任务:Task{taskName='task16'}
    xhJaver 当前线程是xhJaver-thread-9开始处理任务:Task{taskName='task19'}
    xhJaver 当前线程是xhJaver-thread-4开始处理任务:Task{taskName='task4'}
    xhJaver 当前线程是xhJaver-thread-8开始处理任务:Task{taskName='task18'}
    xhJaver 当前线程是xhJaver-thread-2开始处理任务:Task{taskName='task2'}
    xhJaver 当前线程是xhJaver-thread-3开始处理任务:Task{taskName='task3'}
    xhJaver 当前线程是xhJaver-thread-5开始处理任务:Task{taskName='task15'}
    xhJaver 当前线程是xhJaver-thread-0开始处理任务:Task{taskName='task0'}
    xhJaver 当前线程是xhJaver-thread-1开始处理任务:Task{taskName='task1'}
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-4  处理了  task4  任务
    xhJaver 当前线程是xhJaver-thread-4处理任务结束:Task{taskName='task4'} 没有错误 
    xhJaver 当前线程是xhJaver-thread-4开始处理任务:Task{taskName='task5'}
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-9  处理了  task19  任务
    xhJaver 当前线程是xhJaver-thread-9处理任务结束:Task{taskName='task19'} 没有错误 
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-6  处理了  task16  任务
    xhJaver 当前线程是xhJaver-thread-6处理任务结束:Task{taskName='task16'} 没有错误 
    xhJaver 当前线程是xhJaver-thread-9开始处理任务:Task{taskName='task6'}
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-7  处理了  task17  任务
    xhJaver 当前线程是xhJaver-thread-7处理任务结束:Task{taskName='task17'} 没有错误 
    xhJaver 当前线程是xhJaver-thread-7开始处理任务:Task{taskName='task8'}
    xhJaver 当前线程是xhJaver-thread-6开始处理任务:Task{taskName='task7'}
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-1  处理了  task1  任务
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-8  处理了  task18  任务
    xhJaver 当前线程是xhJaver-thread-8处理任务结束:Task{taskName='task18'} 没有错误 
    xhJaver 当前线程是xhJaver-thread-8开始处理任务:Task{taskName='task9'}
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-2  处理了  task2  任务
    xhJaver 当前线程是xhJaver-thread-2处理任务结束:Task{taskName='task2'} 没有错误 
    xhJaver 当前线程是xhJaver-thread-2开始处理任务:Task{taskName='task10'}
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-3  处理了  task3  任务
    xhJaver 当前线程是xhJaver-thread-3处理任务结束:Task{taskName='task3'} 没有错误 
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-5  处理了  task15  任务
    xhJaver 当前线程是xhJaver-thread-5处理任务结束:Task{taskName='task15'} 没有错误 
    xhJaver 当前线程是xhJaver-thread-5开始处理任务:Task{taskName='task12'}
    xhJaver 当前线程是xhJaver-thread-1处理任务结束:Task{taskName='task1'} 没有错误 
    xhJaver 当前线程是xhJaver-thread-1开始处理任务:Task{taskName='task13'}
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-0  处理了  task0  任务
    xhJaver 当前线程是xhJaver-thread-3开始处理任务:Task{taskName='task11'}
    xhJaver 当前线程是xhJaver-thread-0处理任务结束:Task{taskName='task0'} 没有错误 
    xhJaver 当前线程是xhJaver-thread-0开始处理任务:Task{taskName='task14'}
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-4  处理了  task5  任务
    xhJaver 当前线程是xhJaver-thread-4处理任务结束:Task{taskName='task5'} 没有错误 
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-6  处理了  task7  任务
    xhJaver 当前线程是xhJaver-thread-6处理任务结束:Task{taskName='task7'} 没有错误 
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-9  处理了  task6  任务
    xhJaver 当前线程是xhJaver-thread-9处理任务结束:Task{taskName='task6'} 没有错误 
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-7  处理了  task8  任务
    xhJaver 当前线程是xhJaver-thread-7处理任务结束:Task{taskName='task8'} 没有错误 
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-2  处理了  task10  任务
    xhJaver 当前线程是xhJaver-thread-2处理任务结束:Task{taskName='task10'} 没有错误 
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-8  处理了  task9  任务
    xhJaver 当前线程是xhJaver-thread-8处理任务结束:Task{taskName='task9'} 没有错误 
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-5  处理了  task12  任务
    xhJaver 当前线程是xhJaver-thread-5处理任务结束:Task{taskName='task12'} 没有错误 
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-0  处理了  task14  任务
    xhJaver 当前线程是xhJaver-thread-0处理任务结束:Task{taskName='task14'} 没有错误 
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-3  处理了  task11  任务
    xhJaver 当前线程是xhJaver-thread-3处理任务结束:Task{taskName='task11'} 没有错误 
    这里是xhJaver,线程池系列 当前线程名字是 xhJaver-thread-1  处理了  task13  任务
    xhJaver 当前线程是xhJaver-thread-1处理任务结束:Task{taskName='task13'} 没有错误 
    xhJaver 当前线程是xhJaver-thread-1关闭线程池
    
    更多精彩请关注公众号xhJaver,京东java工程师和你一起成长
    查看原文

    赞 0 收藏 0 评论 0

    认证与成就

    • 获得 3 次点赞
    • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    (゚∀゚ )
    暂时没有

    注册于 2020-08-25
    个人主页被 746 人浏览