diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/1.Java Based.md b/1.Java Based.md old mode 100755 new mode 100644 index b1d7ce6..c224e3c --- a/1.Java Based.md +++ b/1.Java Based.md @@ -27,36 +27,29 @@ JDK8后,接口中可以包含default方法,抽象类中不可以 ### 面向对象开发的六个基本原则,在项目中用过哪些原则 - 六个基本原则 - - - 单一原则 - - 一个类只做它该做的事情(高内聚)。在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一原则 - - - 开闭原则 - - 软件实体应当对扩展开发,对修改关闭,要做到开闭有两个要点: - - 1. 抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点 - 2. 封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变的复杂而繁乱 - - - 里氏替换原则 - - 任何时候都可以用子类型替换掉父类型,子类一定是增加父类的能力而不是减少父类的能力 - - - 依赖倒置原则 - - 面向接口编程。高层模块不应该依赖底层模块,两者都应该依赖其抽象,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代。 - - - 接口隔离原则 - - 类间的依赖关系应该建立在最小的接口上,不能大而全,接口表示能力,一个接口只应该描述一种能力,接口也应该高度内聚 - - - 迪米特法则 - - 由叫最少知识原则,一个对象应该对其他对象有尽可能少的了解 + + - 单一原则 + 一个类只做它该做的事情(高内聚)。在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一原则 + + - 开闭原则 + 软件实体应当对扩展开发,对修改关闭,要做到开闭有两个要点: + + 1. 抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点 + 2. 封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变的复杂而繁乱 + + - 里氏替换原则 + 任何时候都可以用子类型替换掉父类型,子类一定是增加父类的能力而不是减少父类的能力 + + - 依赖倒置原则 + 面向接口编程。高层模块不应该依赖底层模块,两者都应该依赖其抽象,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代。 + + - 接口隔离原则 + 类间的依赖关系应该建立在最小的接口上,不能大而全,接口表示能力,一个接口只应该描述一种能力,接口也应该高度内聚 + + - 迪米特法则 + 又叫最少知识原则,一个对象应该对其他对象有尽可能少的了解 - 根据自己的项目来说 - 详细的可以看这里:https://www.cnblogs.com/qifengshi/p/5709594.html > 关于网络方面的问题,面试官应该只会问一题,不会多问 @@ -73,37 +66,47 @@ JDK8后,接口中可以包含default方法,抽象类中不可以 ### TCP 三次握手,四次挥手 -可见该文:https://blog.csdn.net/qq_38950316/article/details/81087809 +* **SYN(Synchronize)**:同步标志位,用于发起连接、请求同步序列号。 +* **ACK(Acknowledgment)**:确认标志位,用于确认收到对方数据,`ACK=1`表示确认有效。 +* **序列号(Seq)**:随机生成的初始值(ISN,Initial Sequence Number),后续数据按 "Seq + 数据长度" 递增,确保数据有序。 +* **确认号(Ack)**:表示 "期望收到对方的下一个序列号",值为 "对方已发 Seq + 已接收数据长度"。 +- 三次握手详细流程(以 "客户端→服务器" 建立连接为例) + +| 步骤 | 发起方 | 标志位 | 关键字段 | 目的 | +| --- | --- | ---------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------- | +| 1 | 客户端 | SYN=1 | Seq = x(随机初始值) | 向服务器 "请求建立连接",并告知服务器:"我后续发送数据的序列号从 x 开始"。 | +| 2 | 服务器 | SYN=1
ACK=1 | Seq = y(服务器随机值)
Ack = x+1 | 1. 用`ACK=1`和`Ack=x+1`确认 "已收到客户端的连接请求";
2. 用`SYN=1`和`Seq=y`向客户端发起 "反向连接请求",告知客户端:"我后续发送数据的序列号从 y 开始"。 | +| 3 | 客户端 | ACK=1 | Seq = x+1
Ack = y+1 | 用`ACK=1`和`Ack=y+1`确认 "已收到服务器的反向连接请求",至此双方确认 "收发能力正常",连接建立。 | + +- 四次挥手详细流程(以 "客户端→服务器" 主动断开连接为例) + +| 步骤 | 发起方 | 标志位 | 关键字段 | 目的 | +| --- | --- | ---------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | +| 1 | 客户端 | FIN=1
ACK=1 | Seq = u(客户端当前 Seq)
Ack = v(服务器当前 Ack) | 客户端告知服务器:"我没有数据要发了,请求关闭我的发送通道(单向)",但仍能接收服务器的数据。 | +| 2 | 服务器 | ACK=1 | Seq = v
Ack = u+1 | 服务器确认 "已收到客户端关闭发送通道的请求",此时客户端→服务器的发送通道关闭,但服务器→客户端的发送通道仍可传输数据(服务器可能还有未发完的数据)。 | +| 3 | 服务器 | FIN=1
ACK=1 | Seq = w(服务器剩余数据后的 Seq)
Ack = u+1 | 服务器发送完所有剩余数据后,告知客户端:"我也没有数据要发了,请求关闭我的发送通道(单向)"。 | +| 4 | 客户端 | ACK=1 | Seq = u+1
Ack = w+1 | 客户端确认 "已收到服务器关闭发送通道的请求",此时服务器→客户端的发送通道关闭。客户端会等待**2MSL(Maximum Segment Lifetime,报文最大生存时间)** 后释放连接(确保服务器能收到该确认,避免服务器重发 FIN)。 | ### TCP 和 UDP区别 - 区别: - UDP是无连接的,即发送数据之前不需要建立连接 - UDP使用尽最大努力交付,即不保证可靠交付,同时也不使用拥塞控制 - UDP是面向报文的,没有拥塞控制,适合多媒体通信要求 - UDP支持一对一,一对多,多对一和多对多的交互通信 - UDP首部开销小,只有8个字节 - TCP是面向连接的运输层协议 - TCP只能一对一连接 - TCP提供可靠的交付服务,提供全双工通信 - TCP 面向字节流,头部最低20个字节 ### 从输入网址到获取页面的过程 - 查询DNS, 获取域名对应的IP地址 - - 浏览器搜索自身的DNS缓存 - - 搜索操作系统的DNS缓存 - - 读取本地的HOST文件 - - 发起一个DNS系统调用(宽带运营服务器查看本身缓存,运营服务器发起一个迭代DNS解析请求) + - 浏览器搜索自身的DNS缓存 + - 搜索操作系统的DNS缓存 + - 读取本地的HOST文件 + - 发起一个DNS系统调用(宽带运营服务器查看本身缓存,运营服务器发起一个迭代DNS解析请求) - 浏览器获得域名对应的IP地址后,发起HTTP三次握手 - TCP/IP建立连接后,浏览器可以向服务器发送HTTP请求了 - 服务器接收到请求后,根据路径参数,经过后端处理将页面返回给浏览器 @@ -117,28 +120,24 @@ Session: Cookie: -数据已文件形式存在用户浏览器端,用户可以通过浏览器禁用Cookie,用户可以对Cookie进行查看,修改,和删除 +数据以文件形式存在用户浏览器端,用户可以通过浏览器禁用Cookie,用户可以对Cookie进行查看,修改,和删除 ### 列出自己常用的JDK包 常用的包: -java.lang 包装类,线程等都在该包 +java.lang 包装类,线程等都在该包 -java.match 有BigDecimal 精确数字类型 +java.math 有BigDecimal 精确数字类型 java.util 并发,集合等都在该包内 ### equals与==的区别 1. equals 比较两个实体值是否相同,可以被覆盖,但需要遵循几个约定: - 自反性:对于任何非null的引用值x, x.equals(x)必须返回true - 对称性:对于任何非null的引用值x和y,当y.equals(x)返回true时,x.equlas(y)必须返回true - 传递性:对于任何非null的引用值x、y、z,如果x.equals(y)返回true,并且y.equals(x)也返回true,那么x.equals(z)也必须返回true - 一致性:对于任何非null的引用值x和y,只要比较对象中的所有信息没有被修改,多次调用equals一致返回true,或者false 2. == 比较两个实体的引用地址是否相等,不能覆盖,如果引用地址相等,那认为两个实体为同一个实体 @@ -217,9 +216,92 @@ wait(long timeout)导致当前的线程等待,直到其他线程调用此对 ### 创建线程的方式 -1. 继承Thread类创建线程,并重写run方法,调用实例对象的start()方法启动线程 -2. 实现Runnable接口,并实现run方法,将实现Runnable的类传入Thread构造函数中,并调用Thread实例对象的start方法启动线程 -3. 实现Callable接口,并实现call方法,创建Callable实现类的实例,使用FutureTask包装Callable对象,使用FutureTask对象传入Thread中,调用start方法启动线程,使用FutureTask对象的get方法获取线程的返回值 +- 继承Thread类创建线程,并重写run方法,调用实例对象的start()方法启动线程 + +```java +public class ThreadDemo extends Thread { + @Override + public void run() { + // 线程执行逻辑 + for (int i = 0; i < 5; i++) { + System.out.println(Thread.currentThread().getName() + ": " + i); + } + } + + public static void main(String[] args) { + ThreadDemo thread = new ThreadDemo(); + thread.setName("子线程1"); + thread.start(); // 启动线程,JVM会调用run() + + // 主线程逻辑 + for (int i = 0; i < 5; i++) { + System.out.println(Thread.currentThread().getName() + ": " + i); + } + } +} +``` + +- 实现Runnable接口,并实现run方法,将实现Runnable的类传入Thread构造函数中,并调用Thread实例对象的start方法启动线程 + +```java +public class RunnableDemo implements Runnable { + @Override + public void run() { + // 线程执行逻辑 + for (int i = 0; i < 5; i++) { + System.out.println(Thread.currentThread().getName() + ": " + i); + } + } + + public static void main(String[] args) { + // 创建任务对象 + RunnableDemo task = new RunnableDemo(); + + // 创建线程并关联任务 + Thread thread = new Thread(task, "子线程1"); + thread.start(); + + // 主线程逻辑 + for (int i = 0; i < 5; i++) { + System.out.println(Thread.currentThread().getName() + ": " + i); + } + } +} +``` + +- 实现Callable接口,并实现call方法,创建Callable实现类的实例,使用FutureTask包装Callable对象,使用FutureTask对象传入Thread中,调用start方法启动线程,使用FutureTask对象的get方法获取线程的返回值 + +```java +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class CallableDemo implements Callable { + @Override + public Integer call() throws Exception { + // 线程执行逻辑,返回计算结果 + int sum = 0; + for (int i = 1; i <= 10; i++) { + sum += i; + } + return sum; + } + + public static void main(String[] args) throws ExecutionException, InterruptedException { + // 创建任务对象 + CallableDemo task = new CallableDemo(); + + // 包装任务,用于获取结果 + FutureTask futureTask = new FutureTask(task); + + // 启动线程 + new Thread(futureTask, "计算线程").start(); + + // 获取结果(会阻塞,直到子线程执行完毕) + System.out.println("1-10的和为:" + futureTask.get()); + } +} +``` ### ArrayList 与 LinkedList 区别 @@ -230,67 +312,50 @@ LinkedList是一种链式存储的线性表,本质是一个双向链表,实 ### 自定义注解 1. 声明注解的保留期限类型 - @Retention(RetentionPolicy.RUNTIME)表示该注解可以在运行期保留 - 保留期限类型:java.lang.annotation.Retention - SOURCE: 注解信息仅保留在目标类源代码文件中,对应的字节码文件不会保留 - CLASS: 注解信息存在于源代码、字节码文件中,但运行期JVM不能获得该注解信息 - RUNTIME: 注解信息存在于源代码、字节码文件、运行期JVM中,能够通过反射机制获取注解类信息 2. 声明注解可以使用的目标类型 - @Target(ElementType.METHOD) 表示这个注解只能在方法上使用 - 目标类型:java.lang.annotation.ElementType - TYPE: 类、接口、注解类、Enum - FIELD: 类成员变量或常量 - METHOD: 方法 - PARAMETER: 参数 - CONSTRUCTOR: 构造器 - LOCAL_VARIABLE: 局部变量 - ANNOTATION_TYPE: 注解 - PACKAGE: 包 3. 使用@interface 修饰类 4. 声明注解成员 - 成员无入参、不能抛出异常; - 可以通过default成员指定默认值 - 成员类型只能使用基本数据类型、String、Class、enums、注解类型,及上述类型的数组类型。如ForumService value()是非法的 - 如果注解只有一个成员,则成员名必须取名为value(),再使用时可以忽略成员名和赋值号,如果注解类拥有多个成员时, - 对value成员赋值,可以省略value和赋值号,如果是多个成员赋值,必须使用赋值号 ### ArrayList扩容机制是怎么样的? 详细说一下。 在往ArrayList add元素的时候,如果ArrayList 已有元素数量+1 大于 ArrayList 存储元素的总长度,就会触发扩容。 -首先ArrayList会计算新数组的长度,长度为老数组的0.5倍,如果新数组长度还是小于插入元素需要的最小长度,那么新数组长度赋值为最小长度,如果超过ArrayList允许的最大长度Integer.MAX_VALUE(2ドル^{31} - 1$),那么新数组长度为Integer.MAX_VALUE,否则为Integer.MAX_VALUE - 8(为什么要-8?[Why the maximum array size of ArrayList is Integer.MAX_VALUE - 8?](https://stackoverflow.com/questions/35756277/why-the-maximum-array-size-of-arraylist-is-integer-max-value-8)) +首先ArrayList会计算新数组的长度,长度为老数组的1.5倍,如果新数组长度还是小于插入元素需要的最小长度,那么新数组长度赋值为最小长度,如果超过ArrayList允许的最大长度Integer.MAX_VALUE(2ドル^{31} - 1$),那么新数组长度为Integer.MAX_VALUE,否则为Integer.MAX_VALUE - 8(为什么要-8?[Why the maximum array size of ArrayList is Integer.MAX_VALUE - 8?](https://stackoverflow.com/questions/35756277/why-the-maximum-array-size-of-arraylist-is-integer-max-value-8)) 最后将原数组元素拷贝到新数组进行扩容 +> 为什么要预留8个字节 +> +> 数组除了存储元素的数组体外,还需要额外的元数据来记录数组的长度、类型信息等,本质是 **为数组元数据预留内存空间**,避免因数组容量达到理论最大值时,元数据无内存可用而导致的分配失败 + ### HashMap 1.7 和 1.8 的区别 - 1.7,在发生hash冲突的时候,数据结构只有链表; - 1.8,数据结构有链表和红黑树,使用红黑树是为了能够提高查询效率。在链表长度达到7时(bingCount>= TREEIFY_THRESHOLD - 1),并且hash tab[]数组长度大于等于64时,将链表转换成红黑树,如果数组长度小于64,只是对数组进行扩容 - https://blog.csdn.net/qq_21251983/article/details/90056067 ### HashMap中的key可以是任何对象或数据类型吗 @@ -326,16 +391,16 @@ cap -1 ,-1是为了在计算的时候能得到大于等于输入参数的值 ```java final Node[] resize() { - //原table数组赋值 + //原table数组赋值 Node[] oldTab = table; - //如果原数组为null,那么原数组长度为0 + //如果原数组为null,那么原数组长度为0 int oldCap = (oldTab == null) ? 0 : oldTab.length; - //赋值阈值 + //赋值阈值 int oldThr = threshold; - //newCap 新数组长度 - //newThr 下次扩容的阈值 + //newCap 新数组长度 + //newThr 下次扩容的阈值 int newCap, newThr = 0; - // 1. 如果原数组长度大于0 + // 1. 如果原数组长度大于0 if (oldCap> 0) { //如果大于最大长度1 << 30 = 1073741824,那么阈值赋值为Integer.MAX_VALUE后直接返回 if (oldCap>= MAXIMUM_CAPACITY) { @@ -347,7 +412,7 @@ final Node[] resize() { oldCap>= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } - // 3. 如果原数组长度等于0,但原阈值大于0,那么新的数组长度赋值为原阈值大小 + // 3. 如果原数组长度等于0,但原阈值大于0,那么新的数组长度赋值为原阈值大小 else if (oldThr> 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults @@ -355,7 +420,7 @@ final Node[] resize() { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } - // 5.如果新的阈值等于0 + // 5.如果新的阈值等于0 if (newThr == 0) { //计算临时阈值 float ft = (float)newCap * loadFactor; @@ -363,13 +428,13 @@ final Node[] resize() { newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } - //计算出来的新阈值赋值给对象的阈值 + //计算出来的新阈值赋值给对象的阈值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) - //用新计算的数组长度新建一个Node数组,并赋值给对象的table + //用新计算的数组长度新建一个Node数组,并赋值给对象的table Node[] newTab = (Node[])new Node[newCap]; table = newTab; - //后面是copy数组和链表数据逻辑 + //后面是copy数组和链表数据逻辑 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node e; @@ -467,13 +532,12 @@ if ((tab = table) == null || (n = tab.length) == 0) - HashMap并发下产生问题:由于在发生hash冲突,插入链表的时候,多线程会造成环链,再get的时候变成死循环,Map.size()不准确,数据丢失 https://www.iteye.com/blog/hwl-sz-1897468 - + - HashTable: 通过synchronized来修饰,效率低,多线程put的时候,只能有一个线程成功,其他线程都处于阻塞状态 - ConcurrentHashMap: 1.7 采用锁分段技术提高并发访问率 1.8 数据依旧是分段存储,但锁采用了synchronized,内部采用Node数组+链表+红黑树的结构存储,当单个链表存储数量达到红黑树阈值8时(此时链表已有元素7),并且数组长度大于64时,存储结构转换为红黑树来存储,否则只进行数组的扩容 - https://www.cnblogs.com/banjinbaijiu/p/9147434.html ### HashMap 为什么不用平衡树,而用红黑树 @@ -485,7 +549,7 @@ if ((tab = table) == null || (n = tab.length) == 0) 查找时间复杂度都维持在O(logN) > 关于HashMap的其他文章:https://blog.csdn.net/login_sonata/article/details/76598675 -> +> > 源码解析:https://www.jianshu.com/p/0a70ce2d3b67 ### 常见异常分为哪两种(Exception,Error),区别是什么,了解受检异常和非受检异常吗 @@ -496,7 +560,7 @@ Error: 表示程序发生错误,是程序无法处理的,不可恢复的, Exception: 表示程序可处理的异常,又分为CheckedException(受检异常)、UncheckedException(非受检异常),受检异常发生在编译期,必须要使用try...catch 或者 throws捕获或者抛出异常,否则编译不通过;非受检异常发生在运行期,具有不确定性,主要由程序的逻辑问题引起的,在程序设计的时候要认真考虑,尽量处理异常 -### 实现一个LRU +### 实现一个LRU(最近最少使用) 可以直接使用LinkedHashMap实现 @@ -521,30 +585,25 @@ public class LRUCache extends LinkedHashMap { } ``` -### 用过流没有,流怎么实现 - -Stream流是Java8中引入的新特性,Stream有几个特点: - -不存数据,都是通过管道将源数据元素传递给操作; - -对Stream的任何修改都不会修改数据源,都是新产生一个流 - -流的很多操作如filter、map都是延迟执行的,只有到终点才会将操作顺序执行 - -对于无限流可以通过"短路"操作访问到有限元素后就返回 +### 获取一个Class对象的方式 -流的元素只访问一次,如果需要重新访问,需要重新生成一个新的流 +1. 通过类对象的getClass方法 -Stream中BaseStream规定了流的基本接口,在Stream中使用Stage来描述一个完整的操作,将具有先后顺序的各个Stage连一起,就构成了整个流水线。 +```java +User user = new User(); +Class clazz = user.getClass(); +``` -AbstractPipeline是流水线的核心,定义了三个AbstractPipeline类型的变量:sourceStage(源阶段)、previousStage(上游pipeline,上一阶段),nexStage(下一阶段) +2. 通过类的静态成员,每个类都有隐含的静态成员class -ReferencePipeline 继承了AbstractPipeline +```java +Class clazz = User.class; +``` -Head、StatefulOp、StatelessOp继承了ReferencePipeline,分别表示源,无状态操作,有状态操作 +3. 通过Class类的静态方法forName方法获取 -![image-20200629003951004](https://tva1.sinaimg.cn/large/007S8ZIlly1gg8h9m9dooj310o0daabt.jpg) +```java +Class clazz = Class.forName("com.itsaysay.User"); +``` -比如Collection.stream()方法得到Head也就是stage0,紧接着调用一系列的中间操作,不断产生新的stage。这些stage对象以双向链表的形式组织在一起,构成整个流水线。 -由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就建立起对数据源的所有操作。 diff --git a/10.Spring.md b/10.Spring.md old mode 100755 new mode 100644 index 5aece9f..c3c7878 --- a/10.Spring.md +++ b/10.Spring.md @@ -6,17 +6,17 @@ ### BeanFactory 与ApplicationContext 是干什么的,两者的区别 -BeanFactory、ApplicationContext都代表容器,BeanFactory是一个基础接口,实现了容器基础的功能,ApplicationContext是容器的高级形态,增加了许多了特性,顶级父类是BeanFactory。 +BeanFactory、ApplicationContext都代表容器,BeanFactory是一个基础接口,实现了容器基础的功能,ApplicationContext是容器的高级形态,增加了许多特性,顶级父类是BeanFactory。 跟FactoryBean的区别是: -FactoryBean 是一个Bean,用于生产修饰其他的Bean实例,典型应用是AOP代理类,使用'&'获取FactoryBean本身 +FactoryBean 是一个Bean,用于创建或修饰其他的Bean实例,典型应用是AOP代理类,使用'&'获取FactoryBean本身,通过getObject来获取原来的Bean实例 -BeanFactory 是一个工厂,是容器的顶层接口 +BeanFactory 是一个工厂,是容器的顶层接口 ### BeanPostProcessor 的实现 -Bean的后置处理器,是一个监听器,可以监听容器触发的事件。将它向IOC容器注册后,容器中管理的Bean具备了接收IOC容器事件回调的能力。BeanPostProcessor是一个接口类,有两个接口方法,postProcessBeforeInitialization提供Bean初始化前的回调入口;postProcessAfterInitialization 提供Bean初始化后的回调入口`AbstractAutowireCapableBeanFactory#initializeBean` +Bean的后置处理器,是一个监听器,可以监听容器触发的事件。将它向IOC容器注册后,容器中管理的Bean具备了接收IOC容器事件回调的能力。BeanPostProcessor是一个接口类,有两个接口方法,postProcessBeforeInitialization提供Bean初始化前的回调入口;postProcessAfterInitialization 提供Bean初始化后的回调入口`AbstractAutowireCapableBeanFactory#initializeBean`,这个类可以对项目中的Bean进行修饰,所有Bean都会调用该实现。 ### BeanDefinition 的实现 @@ -32,7 +32,7 @@ Spring通过`refresh()`方法对容器进行初始化和资源的载入 第二个过程是BeanDefinition载入,把定义好的Bean表示成IOC容器的内部数据结构BeanDefinition,通过定义BeanDefinition来管理应用的各种对象及依赖关系,其是容器实现依赖反转功能的核心数据结构 -第三个过程是BeanDefinition注册,容器解析得到BeanDefinition后,需要在容器中注册,这由IOC实现BeanDefinitionRegistry接口来实现,注册过程是IOC容器内部维护了一个ConcurrentHasmap来保存得到的BeanDefinition。如果某些Bean设置了lazyinit属性,Bean的依赖注入会在这个过程预先完成,而不需要等到第一次使用Bean的时候才触发。 +第三个过程是BeanDefinition注册,容器解析得到BeanDefinition后,需要在容器中注册,这由IOC实现BeanDefinitionRegistry接口来实现,注册过程是IOC容器内部维护了一个ConcurrentHasmap来保存得到的BeanDefinition。如果某些Bean设置了lazyinit=false属性,Bean的依赖注入会在这个过程预先完成,而不需要等到第一次使用Bean的时候才触发。 ### Spring DI(依赖注入)的实现 @@ -51,7 +51,7 @@ getBean()方法定义在BeanFactory接口中,具体实现在子类AbstractBean 5. 通过populateBean注入Bean属性,并调用init-method初始化方法 6. 注册实例化的Bean -### Spring如何解决循环依赖问题 +### Spring如何解决循环依赖问题(三级缓存) 比如A依赖B, B依赖A. @@ -131,7 +131,7 @@ AOP面向切面编程,可以通过预编译和运行时动态代理,实现 ### Spring MVC运行流程 -image-20190910155238902 +image-20190910155238902 1. 客户端请求到DispatcherServlet 2. DispatcherServlet根据请求地址查询映射处理器HandleMapping,获取Handler @@ -193,6 +193,64 @@ TransactionTemplate 事务模版是对原始事务管理方式的封装,原始 事务模版主要通过execute(TransactionCallback action)来执行事务,TransactionCallback 有两种方式一种是有返回值TransactionCallback,一种是没有返回值TransactionCallbackWithoutResult。 +手动处理事务的方法: + +1. 使用TransactionTemplate + +```java +@Autowired +private TransactionTemplate transactionTemplate; + +public void manualTransaction() { + transactionTemplate.execute(status -> { + try { + // 业务逻辑代码 + // ... + + // 手动提交 + // 不需要显式调用,execute方法成功完成后会自动提交 + return result; // 返回执行结果 + } catch (Exception e) { + // 手动回滚 + status.setRollbackOnly(); + throw e; // 或者处理异常 + } + }); +} +``` + + + +2. 使用PlatformTransactionManager + +```java +@Autowired +private PlatformTransactionManager transactionManager; + +public void manualTransactionWithManager() { + // 定义事务属性 + DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + + // 开启事务 + TransactionStatus status = transactionManager.getTransaction(def); + + try { + // 业务逻辑代码 + // ... + + // 手动提交 + transactionManager.commit(status); + } catch (Exception e) { + // 手动回滚 + transactionManager.rollback(status); + throw e; // 或者处理异常 + } +} +``` + + + ### Spring 事务底层原理 1. 事务的准备 @@ -255,13 +313,13 @@ TTI:空闲期,即一个数据多久没被访问就从缓存中移除的时 ### Spring Cache 注解 -| 注解 | 用法 | -| ------------ | -------------------------------------------------- | -| @Cacheable | 先查询缓存,如果没有执行方法并缓存结果,用于取数据 | -| @CachePut | 先执行方法,然后将返回值放入缓存,用于更新数据 | -| @CacheEvict | 删除缓存,用于删除数据 | -| @Caching | 基于前3者的注解数组,多用于一个类有多种实现的情况 | -| @CacheConfig | 全局缓存注解,用于类上 | +| 注解 | 用法 | +| ------------ | ------------------------------------------------------ | +| @Cacheable | 先查询缓存,如果没有缓存执行方法并缓存结果,用于取数据 | +| @CachePut | 先执行方法,然后将返回值放入缓存,用于更新数据 | +| @CacheEvict | 删除缓存,用于删除数据 | +| @Caching | 基于前3者的注解数组,多用于一个类有多种实现的情况 | +| @CacheConfig | 全局缓存注解,用于类上 | 缓存管理器 @@ -270,6 +328,32 @@ TTI:空闲期,即一个数据多久没被访问就从缓存中移除的时 3. ConcurrentMapCacheManager 不用配置缓存列表,自动生成缓存ConcurrentMapCache 4. CompositeCacheManager 可以将不同的缓存管理器组合在一起,不同的缓存使用不同的缓存管理器,并且可以通过fallbackToNoOpCache属性回到NoOpCacheManager +手动处理Spring Cache缓存: + +```java +@Autowired +private CacheManager cacheManager; + +public void manualCacheOperations() { + // 获取指定缓存 + Cache cache = cacheManager.getCache("cacheName"); + + // 存入缓存 + cache.put("key", "value"); + + // 获取缓存值 + ValueWrapper wrapper = cache.get("key"); + if (wrapper != null) { + Object value = wrapper.get(); + // 使用缓存值 + } + + // 删除缓存 + cache.evict("key"); // 删除指定key + cache.clear(); // 清空整个缓存 +} +``` + ### Spring BeanUtils bean拷贝工具用过吗?它是浅拷贝还是深拷贝?怎么实现的?有没有什么坑?其他还有什么bean 拷贝的方法,是浅拷贝还是深拷贝?如何实现深拷贝? diff --git a/11.Spring Boot.md b/11.Spring Boot.md old mode 100755 new mode 100644 index f50a1eb..02af055 --- a/11.Spring Boot.md +++ b/11.Spring Boot.md @@ -19,42 +19,39 @@ Spring Boot是 Spring 的子项目,正如其名字,提供 Spring 的引导( - 判断webApplication是什么类型的 - 设置ApplicationContextInitializer,ApplicationListener,通过加载META-INF/spring.factories中配置的类 - 找到main方法找到启动主类 - 4. run方法中,做的工作 - StopWatch主要是监控启动过程,统计启动时间,检测应用是否已经启动或者停止。 - - 加载SpringApplicationRunListener(也是通过META-INF/spring.factories),默认加载的是EventPublishingRunListener - - - 调用RunListener.starting()方法。 - - - 根据args创建应用参数解析器ApplicationArguments; - + - 调用RunListener.starting()方法。 + - 根据args创建应用参数解析器ApplicationArguments; - 准备环境变量:获取环境变量environment,将应用参数放入到环境变量持有对象中,监听器监听环境变量对象的变化(listener.environmentPrepared) - - 打印Banner信息(SpringBootBanner) - - 创建SpringBoot的应用上下文(AnnotationConfigEmbeddedWebApplicationContext) - - prepareContext上下文之前的准备 - - refreshContext刷新上下文 - - afterRefresh(ApplicationRunner,CommandLineRunner接口实现类的启动) - - 返回上下文对象 - +基于SB3.x的启动流程图: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/a8ba0cec245d1e9c078b96ade98fa997.png) ### Spring Boot启动的时候会加载哪些包? 在web项目中,会在Maven中配置 spring-boot-starter-web 包,该包中包含了spring-core、spring-content、servlet、tomcat、jackson、HikariCP、junit、jdbc、slf4j 等 +### Spring Boot 的监听器 +Spring Boot 的监听器指的是SpringApplicationRunListener和ApplicationListener -### 如何重新加载 Spring Boot 上的更改,而无需重新启动服务器? +SpringApplicationRunListener 是Spring Boot 定义的监听器,用于监听 `SpringApplication` 的启动过程,并在**启动的不同阶段执行自定义逻辑**(只在启动阶段)。它允许开发者在应用启动的各个生命周期节点插入自己的代码,例如初始化资源、记录日志、监控启动过程等,通过`META-INF/spring.factories` 文件配置注册。 + +ApplicationListener是Spring 定义的监听器,能够监听 Spring 容器内发布的 `ApplicationEvent` 事件,覆盖应用的全生命周期,通过作为 Spring Bean 注册(如 `@Component`)、META-INF/spring.factories(org.springframework.context.ApplicationListener)、SpringApplication的addListener和配置文件context.istener.classes -一共有三种方式,可以实现效果: + + +### 如何重新加载 Spring Boot 上的更改,而无需重新启动服务器? - 【推荐】`spring-boot-devtools` 插件。注意,这个工具需要配置 IDEA 的自动编译。 @@ -64,6 +61,8 @@ Spring Boot是 Spring 的子项目,正如其名字,提供 Spring 的引导( - [JRebel](https://www.jianshu.com/p/bab43eaa4e14) 插件,需要付费。 +- 使用插件化开发,插件化代码使用手动注册Bean的方式 + 关于如何使用 `spring-boot-devtools` 和 Spring Loaded 插件,可以看看 [《Spring Boot 学习笔记:Spring Boot Developer Tools 与热部署》](https://segmentfault.com/a/1190000014488100) 。 @@ -71,8 +70,8 @@ Spring Boot是 Spring 的子项目,正如其名字,提供 Spring 的引导( ### 什么是 Spring Boot 自动配置? 1. Spring Boot 在启动时扫描项目所依赖的 jar 包,寻找包含`spring.factories` 文件的 jar 包。 -2. 根据 `spring.factories` 配置加载 AutoConfigure 类。 +2. 根据 `spring.factories` 配置加载 AutoConfigure 类。(Spring Boot 2.7 以后,从META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中加载配置的Jar包) 3. 根据 `@Conditional` 等条件注解的条件,进行自动配置并将 Bean 注入 Spring IoC 中。 -https://my.oschina.net/itsaysay/blog/3011826 +> 详细介绍:https://itsaysay.blog.csdn.net/article/details/131736129 diff --git a/12.Dubbo.md b/12.Dubbo.md old mode 100755 new mode 100644 diff --git a/13.Spring Cloud.md b/13.Spring Cloud.md old mode 100755 new mode 100644 index 13286a0..9ee37f4 --- a/13.Spring Cloud.md +++ b/13.Spring Cloud.md @@ -18,15 +18,15 @@ Spring Cloud 主要提供了如下核心的功能: 脑图如下: -![Spring Cloud的 组件](https://tva1.sinaimg.cn/large/006tNbRwly1gaf4laqglvj30hs0es3z4.jpg) +![Spring Cloud的 组件](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/68747470733a2f2f747661312e73696e61696d672e636e2f6c617267652f303036744e6252776c7931676166346c6171676c766a33306873306573337a342e6a7067.jpeg) 由于 [Spring Cloud Netflix](https://github.com/spring-cloud/spring-cloud-netflix) 要进入维护模式,下面是一些可以替代组件 -| | Netflix | 阿里 | 其它 | -| -------- | ------- | -------- | ------------------------------------------------------------ | -| 注册中心 | Eureka | Nacos | Zookeeper、Consul、Etcd | -| 熔断器 | Hystrix | Sentinel | Resilience4j | -| 网关 | Zuul | 暂无 | Spring Cloud Gateway | +| | Netflix | 阿里 | 其它 | +| ---- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------- | +| 注册中心 | Eureka | Nacos | Zookeeper、Consul、Etcd | +| 熔断器 | Hystrix | Sentinel | Resilience4j | +| 网关 | Zuul | 暂无 | Spring Cloud Gateway | | 负载均衡 | Ribbon | Dubbo | [`spring-cloud-loadbalancer`](https://github.com/spring-cloud/spring-cloud-commons/tree/master/spring-cloud-loadbalancer) | ### Spring Cloud 和 Spring Boot 的区别和关系? @@ -53,7 +53,7 @@ Spring Cloud 主要提供了如下核心的功能: ### SpringCloud的注册和发现流程,以Eureka为注册中心 -![img](https://tva1.sinaimg.cn/large/007S8ZIlly1gfzzc6yo1oj30ok0c1tcu.jpg) +img 1. 服务启动时会生成服务的基本信息对象InstanceInfo,然后再启动时注册到服务治理中心 2. 服务注册完成后,会从服务治理中心拉取所有的服务信息,缓存在本地 @@ -100,139 +100,92 @@ Spring Cloud 主要提供了如下核心的功能: 在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。 -### Ribbon 有哪些负载均衡算法? - -详细文章可见:[《Ribbon 负载均衡策略配置》](https://blog.csdn.net/rickiyeat/article/details/64918756) - -其中,默认的负载均衡算法是 Round Robin 算法,顺序向下轮询。 - -### Ribbon 是怎么和 Eureka 整合的? - - Ribbon 原理图: - -![Ribbon 原理](https://tva1.sinaimg.cn/large/006tNbRwly1gaf4lfk3twj30vc0u0afc.jpg) - -- 首先,Ribbon 会从 Eureka Client 里获取到对应的服务列表。 -- 然后,Ribbon 使用负载均衡算法获得使用的服务。 -- 最后,Ribbon 调用对应的服务。 - -另外,此处的 Eureka 仅仅是作为注册中心的举例,也是可以配合其它的注册中心使用,例如 Zookeeper 。可参考 [《以 Zookeeper 为注册中心搭建 Spring Cloud 环境》](https://www.jianshu.com/p/775c363d0fda) 文章。 - ### Feign 实现原理 **Feign的一个关键机制就是使用了动态代理**。咱们一起来看看下面的图,结合图来分析: - 首先,如果你对某个接口定义了 `@FeignClient` 注解,Feign 就会针对这个接口创建一个动态代理。 - 接着你要是调用那个接口,本质就是会调用 Feign 创建的动态代理,这是核心中的核心。 -- Feig n的动态代理会根据你在接口上的 `@RequestMapping` 等注解,来动态构造出你要请求的服务的地址。 +- Feign的动态代理会根据你在接口上的 `@RequestMapping` 等注解,来动态构造出你要请求的服务的地址。 - 最后针对这个地址,发起请求、解析响应。 -![Feign 原理](https://tva1.sinaimg.cn/large/006tNbRwly1gaf4ljm7hhj30ub09daah.jpg) +![Feign 原理](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/68747470733a2f2f747661312e73696e61696d672e636e2f6c617267652f303036744e6252776c7931676166346c6a6d3768686a333075623039646161682e6a7067.jpeg) ### Feign 和 Ribbon 的区别? Ribbon 和 Feign 都是使用于调用用其余服务的,不过方式不同。 - 启动类用的注解不同。 - - Ribbon 使用的是 `@RibbonClient` 。 - - Feign 使用的是 `@EnableFeignClients` 。 + - Ribbon 使用的是 `@RibbonClient` 。 + - Feign 使用的是 `@EnableFeignClients` 。 - 服务的指定位置不同。 - - Ribbon 是在 `@RibbonClient` 注解上设置。 - - Feign 则是在定义声明方法的接口中用 `@FeignClient` 注解上设置。 + - Ribbon 是在 `@RibbonClient` 注解上设置。 + - Feign 则是在定义声明方法的接口中用 `@FeignClient` 注解上设置。 - 调使用方式不同。 - - Ribbon 需要自己构建 Http 请求,模拟 Http 请求而后用 RestTemplate 发送给其余服务,步骤相当繁琐。 - - Feign 采使用接口的方式,将需要调使用的其余服务的方法定义成声明方法就可,不需要自己构建 Http 请求。不过要注意的是声明方法的注解、方法签名要和提供服务的方法完全一致。 - -### Feign 是怎么和 Ribbon、Eureka 整合的? - -![Feign + Ribbon + Eureka](https://tva1.sinaimg.cn/large/006tNbRwly1gaf4lp8pxuj30zk0fst9r.jpg) - -- 首先,用户调用 Feign 创建的动态代理。 - -- 然后,Feign 调用 Ribbon 发起调用流程。 - - - 首先,Ribbon 会从 Eureka Client 里获取到对应的服务列表。 - - - 然后,Ribbon 使用负载均衡算法获得使用的服务。 - - - 最后,Ribbon 调用 Feign ,而 Feign 调用 HTTP 库最终调用使用的服务。 - -> 因为 Feign 和 Ribbon 都存在使用 HTTP 库调用指定的服务,那么两者在集成之后,必然是只能保留一个。比较正常的理解,也是保留 Feign 的调用,而 Ribbon 更纯粹的只负责负载均衡的功能。 - -想要完全理解,建议胖友直接看如下两个类: - -- [LoadBalancerFeignClient](https://github.com/spring-cloud/spring-cloud-openfeign/blob/master/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/LoadBalancerFeignClient.java) ,Spring Cloud 实现 Feign Client 接口的二次封装,实现对 Ribbon 的调用。 - -- [FeignLoadBalancer](https://github.com/spring-cloud/spring-cloud-openfeign/blob/master/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/ribbon/FeignLoadBalancer.java) ,Ribbon 的集成。 - -> 集成的是 AbstractLoadBalancerAwareClient 抽象类,它会自动注入项目中所使用的负载均衡组件。 - -- LoadBalancerFeignClient =》调用=》 FeignLoadBalancer 。 - -### Hystrix 隔离策略? - -Hystrix 有两种隔离策略: - -- 线程池隔离 -- 信号量隔离 - -实际场景下,使用线程池隔离居多,因为支持超时功能。 - -详细的,可以看看 [《Hystrix 的资源隔离策略》](https://blog.csdn.net/liuchuanhong1/article/details/73718794) 文章。 - -#### 聊聊 Hystrix 缓存机制? + - Ribbon 需要自己构建 Http 请求,模拟 Http 请求而后用 RestTemplate 发送给其余服务,步骤相当繁琐。 + - Feign 采使用接口的方式,将需要调使用的其余服务的方法定义成声明方法就可,不需要自己构建 Http 请求。不过要注意的是声明方法的注解、方法签名要和提供服务的方法完全一致。 -Hystrix 提供缓存功能,作用是: - -- 减少重复的请求数。 -- 在同一个用户请求的上下文中,相同依赖服务的返回数据始终保持一致。 - -详细的,可以看看 [《Hystrix 缓存功能的使用》](https://blog.csdn.net/zhuchuangang/article/details/74566185) 文章。 - -### 什么是 Hystrix 断路器? - -Hystrix 断路器通过 HystrixCircuitBreaker 实现。 +### 为什么要网关服务? -HystrixCircuitBreaker 有三种状态 : +使用网关服务,我们实现统一的功能: -- `CLOSED` :关闭 -- `OPEN` :打开 -- `HALF_OPEN` :半开 +- 动态路由 +- 灰度发布 +- 健康检查 +- 限流 +- 熔断 +- 认证: 如数支持 HMAC, JWT, Basic, OAuth 2.0 等常用协议 +- 鉴权: 权限控制,IP 黑白名单,同样是 OpenResty 的特性 +- 可用性 +- 高性能 -其中,断路器处于 `OPEN` 状态时,链路处于**非健康**状态,命令执行时,直接调用**回退**逻辑,跳过**正常**逻辑。 +### 熔断和降级区别 -HystrixCircuitBreaker 状态变迁如下图 : +熔断是下层服务一旦产生故障就断掉;降级需要对服务进行分级,把产生故障的服务丢掉,换一个轻量级的方案。 -![HystrixCircuitBreaker 状态](https://tva1.sinaimg.cn/large/006tNbRwly1gaf4lttab6j31000l4gmm.jpg) -- **红线**:初始时,断路器处于``CLOSED``状态,链路处于健康状态。当满足如下条件,断路器从``CLOSED``变成``OPEN`` - 状态: +### Spring Cloud Gateway - - **周期**( 可配,`HystrixCommandProperties.default_metricsRollingStatisticalWindow = 10000 ms` )内,总请求数超过一定**量**( 可配,`HystrixCommandProperties.circuitBreakerRequestVolumeThreshold = 20` ) 。 -- **错误**请求占总请求数超过一定**比例**( 可配,`HystrixCommandProperties.circuitBreakerErrorThresholdPercentage = 50%` ) 。 - -- **绿线** :断路器处于 `OPEN` 状态,命令执行时,若当前时间超过断路器**开启**时间一定时间( `HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds = 5000 ms` ),断路器变成 `HALF_OPEN` 状态,**尝试**调用**正常**逻辑,根据执行是否成功,**打开或关闭**熔断器【**蓝线**】。 +#### 过滤器 -### 什么是 Hystrix 服务降级? +分类: -在 Hystrix 断路器熔断时,可以调用一个降级方法,返回相应的结果。当然,降级方法需要配置和编码,如果不需要,也可以不写,也就是不会有服务降级的功能。 +1、 GatewayFilter,网关过滤器,只应用在单个路由或者一个分组的路由上 -具体的使用方式,可以看看 [《通过 Hystrix 理解熔断和降级》](https://blog.csdn.net/jiaobuchong/article/details/78232920) 。 +- AddRequestHeader:用于在请求头中添加自定义键值对 +- AddRequestParameter:用于在请求中添加请求参数的键值对 +- AddResponseHeader:用于在响应头中添加键值对 +- Hystrix网关过滤工厂:用于将断路器引入网关路由中 +- PrefixPath:用于使用简单的Prefix参数 +- PreserveHostHeader:用于设置路由过滤器的请求属性,检查是否发送原始主机头或由HTTP客户端确定主机头 +- RequestRateLimiter:用于确定当前请求是否允许继续,如果不允许,返回提示"HTTP 429 - Too Many Requests" +- RedirectTo:用于接收请求的状态和URL参数,该状态是一个重定向的300系列的HTTP代码,如301,URL是Location的头部值 +- RemoveNonProxyHeaders:用于从转发的请求中删除请求头 +- RemoveRequestHeader:用于删除请求头,需要请求头名 +- RemoveResponseHeader:用于响应头,需要响应头名 +- RewritePath:用于使用Java正则表达式重写请求路径 +- SaveSession:用于在转发下游调用之前强制执行保存Session操作 +- SecureHeaders:用于为响应添加安全头 +- SetPath:允许通过路径的模版段来操作请求的路径,使用了Spring框架的URI模版,支持多种匹配 +- SetResponseHeader:用于设置响应头,需要有一个Key-Value对 +- SetStatus:用于设置请求响应状态,需要一个Status参数,该参数的值必须是有效的SpringHttpStatus, +- StripPrefix:用于剥离前缀,需要parts参数,表明在请求被发送到下游之前从请求路径中剥离的元素数量 +- Retry:用于重试 +- RequestSize:用于限制请求的大小,当请求超过限制时启用,限制请求到达下游服务,该过滤器将RequestSize作为参数 -### 为什么要网关服务? +2、 GlobalFilter,全局过滤器,应用在所有的路由上 -使用网关服务,我们实现统一的功能: +- Forward Routing Filter +- LoadBalancerClientFilter +- Netty Routing Filter +- Netty Write Response Filter +- RouteToRequestUrl Filter +- Websocket Routing Filter +- GateWay Metrics Filter 网关指标过滤器 +- Combined Global Filter and GateWayFilter 组合式全局过滤器和网关过滤器排序 +- Marking An Exchange As Routed 路由交换 -- 动态路由 -- 灰度发布 -- 健康检查 -- 限流 -- 熔断 -- 认证: 如数支持 HMAC, JWT, Basic, OAuth 2.0 等常用协议 -- 鉴权: 权限控制,IP 黑白名单,同样是 OpenResty 的特性 -- 可用性 -- 高性能 +> 详细见:[Spring Cloud Gateway 参考指南_cloud: gateway: metrics: enabled: true-CSDN博客](https://blog.csdn.net/weixin_40972073/article/details/125840118?ops_request_misc=%7B%22request%5Fid%22%3A%22171941508116800184151243%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fblog.%22%7D&request_id=171941508116800184151243&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-125840118-null-null.nonecase&utm_term=gateway&spm=1018.2226.3001.4450) -详细的,可以看看 [《为什么微服务需要 API 网关?》](http://dockone.io/article/2033) 。 diff --git a/14.Message Queue.md b/14.Message Queue.md old mode 100755 new mode 100644 index 2201513..a7777b5 --- a/14.Message Queue.md +++ b/14.Message Queue.md @@ -8,6 +8,18 @@ Kafka是一款高性能的消息中间件,包括Producer,Consumer,Broker,以及Zookeeper,Zookeeper用来负责集群元数据管理,控制器的选举等操作,Producer将消息发送到Broker,由Broker负责将收到的消息存储到磁盘中,Consumer负责从Broker订阅并消费消息。Kafka中的消息是以主题为单位,主题可以分布在不同的分区,分区可以分布于不同的Broker,分区有Leader 与副本follower,follower负责从leader同步数据,leader负责读写请求 +从4.0.0开始彻底去掉了Zookeeper,转为使用 KRaft 模式 + +| 特性 | Zookeeper 模式 | KRaft 模式 | +| ------------------ | ---------------------------------------- | --------------------------------------------- | +| **元数据存储** | 存储在 Zookeeper 中(内存 + 磁盘) | 存储在 Controller 节点的本地磁盘(Raft 日志) | +| **控制器选举** | 由 Zookeeper 协调选举 | 基于 Raft 协议自主选举(Leader 节点即控制器) | +| **元数据更新效率** | 需通过网络请求 Zookeeper,延迟较高 | 本地元数据直接更新,延迟低(毫秒级) | +| **集群扩展性** | Zookeeper 集群扩容复杂,易成瓶颈 | 支持动态添加 Controller 节点,扩展性更好 | +| **启动速度** | 需等待 Zookeeper 和 Kafka 双重初始化 | 仅需启动 Kafka 节点,启动更快 | +| **数据一致性保障** | 依赖 Zookeeper 的 Paxos 协议 | 基于 Raft 协议,强一致性更易理解和维护 | +| **安全性** | 需分别配置 Kafka 和 Zookeeper 的安全策略 | 统一的安全配置(如 SSL、SASL) | + #### 消息的幂等性处理思路 @@ -63,6 +75,16 @@ kafka保证消息可靠性,可以通过如下几个配置: +#### 怎么保证消息顺序消费 + +消息要顺序消费的场景,比如发送了一个用户新增的消息,随后用户修改了发送了一个修改的消息,最后又删除了发送了一个删除的消息,由于Kafka的多分区,多消费者,消费端势必会变成无序消费,但消费端业务需要顺序处理,如果先消费了删除消息,根本没数据,随后又消费了新增消息,最后消息没有删除,变成了脏数据。 + +解决方法是: + +- 生产者发送消息的时候,根据用户id指定分区key,指定后kafka会将消息发送到指定的分区中,这样保证了分区中消息的顺序。消费端,可以使用单线程从指定分区中消费,如果要保证性能,消费端定义多个内存队列,将相同用户id的消息发送到同一个内存队列中,然后开启多线程从来消费多个内存队列,一个线程处理一个内存队列 + +- 让消费者只消费一个指定的分区,速度会变慢 + #### kafka的分区策略 消费者客户端参数partition.assignment.strategy 来配置消费分区策略 @@ -70,3 +92,90 @@ kafka保证消息可靠性,可以通过如下几个配置: 2. RoundRobinAssignor 轮询分配策略 3. StickyAssignor 能够使分区的分配尽可能与上一次保持一致,避免过度重分配 4. 自定义分配,实现PartitionAssignor接口 + + + +#### kafka 集群如何搭建 + +- 安装zk集群,修改各个节点的kafka配置文件server.properties(broker.id、listeners、zookeeper.connect) +- 启动zk、启动kafka + +k8s 上创建:[K8s - 安装部署Kafka、Zookeeper集群教程(支持从K8s外部访问) - 蜂蜜log - 博客园 (cnblogs.com)](https://www.cnblogs.com/fengyuanfei/p/17789107.html) + + + +#### 什么是ISR + +- AR(Assigned Repllicas)一个partition的所有副本(即使replica,不区分leader或follower) +- ISR(In-Sync Replicas)能够和leader保持同步的follower+leader本身组成的集合 +- OSR(Out-Sync Relipcas)不能和leader 保持同步的follower集合 + +### RocketMQ + +#### RocketMQ的核心组件是什么 + +NameServer: 轻量级服务发现中心,管理Broker的地址路由信息;无状态,支持快速扩容 + +Broker: 消息存储和转发节点,负责接收生产者消息、持久化存储、投递给消费者;主从架构,支持同步/异步复制 + +Producer: 消息生产者,通过NameServer找到目标Broker发送消息,支持同步、异步、单向发送模式 + +Consumer: 消息消费者,从Broker拉取消息,支持集群消费和广播消费 + +#### RocketMQ 的消息模型有哪些 + +- 点对点(Queue模型): + +​ 消息通过队列存储,同一消费者组内竞争消费(每条消息仅被一个消费者处理) + +- 发布/订阅(Pub-Sub): + +- 延迟消息 +- 顺序消息 + +​ 通过MessageQueueSelector 保证同一业务键(如订单ID)的消息发送到同一队列,消费者按队列顺序消费 + +#### RocketMQ 如何保证消息不丢失 + +1. 生产者端:同步发送+重试机制;事务消息 + +2. Broker端:消息持久化,同步刷盘或异步刷盘;同步复制,保证Slave写入成功后才返回ACK + +3. 消费者端:手动提交消费偏移量;消费失败重试 + +#### RocketMQ 怎么实现顺序消息 + +生产者:通过MessageQueueSelector 将同一业务键(如订单ID)的消息发送到同一队列 + +Broker:单个队列内的消息天然有序 + +消费者:单线程消费队列(或锁保证并发安全),并且关闭异步提交消费偏移量 + +#### 如何解决消息堆积问题 + +1. 扩容消费者:增加消费者实例数(不超过队列数),提升并行消费能力 +2. 调整消费逻辑:优化消费代码(如批量处理,异步) +3. 跳过非关键消息:在业务允许时,重置消费偏移量到最新位置 + +#### 事务消息的实现原理 + +RokectMQ支持在分布式场景下保障消息生产和本地事务的最终一致性。 + +1. **第一阶段(发送半消息)**: + - 生产者发送"半消息"(对消费者不可见)到 Broker。 + - Broker 返回 ACK 确认半消息持久化成功。 +2. **第二阶段(执行本地事务)**: + - 生产者执行本地事务(如数据库操作),生成事务状态(提交/回滚)。 +3. **Broker 回调检查**: + - 若生产者未响应,Broker 定期回调查询本地事务状态。 +4. **最终提交/回滚**: + - 根据事务状态提交(投递消息)或回滚(丢弃消息)。 + +### RabbitMQ + +#### 说一下RabbitMQ + +RabbitMQ 是一个开源的消息代理软件,核心有Producer(生产者)、Consumer(消费者)、Queue(队列)、Exchange(交换机)、Binding(绑定)、Message(消息),交换机类型有:Fanout(广播到所有绑定的队列)、Direct(精确匹配路由键)、Topic(基于通配符的路由)、Headers(通过消息头属性匹配),另外有死信队列(DLX)可以处理失败或超时的消息,用来实现延时消息 + + + diff --git a/15.Mybatis.md b/15.Mybatis.md old mode 100755 new mode 100644 index 28cce8c..4c4d323 --- a/15.Mybatis.md +++ b/15.Mybatis.md @@ -62,7 +62,7 @@ -#####Mybatis 的 XML Mapper文件中,不同的 XML 映射文件,id 是否可以重复? +### Mybatis 的 XML Mapper文件中,不同的 XML 映射文件,id 是否可以重复? 不同的 XML Mapper 文件,如果配置了 `"namespace"` ,那么 id 可以重复;如果没有配置 `"namespace"` ,那么 id 不能重复。毕竟`"namespace"` 不是必须的,只是最佳实践而已。 diff --git a/16.Zookeeper.md b/16.Zookeeper.md old mode 100755 new mode 100644 index caef830..c16f716 --- a/16.Zookeeper.md +++ b/16.Zookeeper.md @@ -1,4 +1,6 @@ -# 16.Zookeeper +# 16.注册中心 + +## Zookeeper ### Zookeeper 是什么? @@ -200,4 +202,51 @@ Zookeeper 的选举算法有两种:一种是基于 basic paxos 实现的,另 FastLeaderElection 算法通过异步的通信方式来收集其它节点的选票,同时在分析选票时又根据投票者的当前状态来作不同的处理,以加快 Leader 的选举进程。 - 流程 \ No newline at end of file + 流程 + +## Nacos + +### Nacos 与 Eureka、Consul、Zookeeper 等注册中心有何区别? + +| 特性 | Nacos | Eureka | Consul | Zookeeper | +| :----------- | :------------------------- | :---------- | :------------ | :--------- | +| 一致性协议 | AP + CP 可切换 | AP | CP | CP | +| 健康检查 | TCP/HTTP/MYSQL/Client Beat | Client Beat | TCP/HTTP/gRPC | Keep Alive | +| 负载均衡 | 权重/metadata/Selector | Ribbon | Fabio | - | +| 配置中心 | 支持 | 不支持 | 支持 | 支持 | +| 雪崩保护 | 支持 | 支持 | 不支持 | 不支持 | +| 自动注销实例 | 支持 | 支持 | 支持 | 支持 | +| 访问协议 | HTTP/DNS | HTTP | HTTP/DNS | 客户端 | +| 监听支持 | 支持 | 支持 | 支持 | 支持 | +| 多数据中心 | 支持 | 支持 | 支持 | 不支持 | +| 跨注册中心 | 支持 | 不支持 | 支持 | 不支持 | +| 易用性 | 简单 | 简单 | 中等 | 复杂 | + +Nacos 的优势在于: + +- 同时支持服务发现和配置管理 +- 支持 AP 和 CP 两种模式切换 +- 提供更丰富的健康检查机制 +- 支持权重路由等更灵活的路由策略 +- 提供更友好的管理界面 + +### Nacos 如何实现配置的动态更新?其原理是什么? + +1. **客户端长轮询机制**: + - 客户端发起配置查询请求时,会携带配置的 MD5 值 + - 服务端比较客户端 MD5 和服务端 MD5: + - 如果相同,服务端会保持连接,直到配置发生变化或超时(默认 30s) + - 如果不同,立即返回最新配置 +2. **服务端配置变更处理**: + - 当管理员通过控制台或 API 更新配置时 + - 服务端更新配置并计算新 MD5 + - 服务端检查所有保持的长轮询连接 + - 向相关客户端发送配置变更通知 +3. **客户端处理更新**: + - 客户端收到变更通知后,立即拉取最新配置 + - 更新本地缓存和内存中的配置值 + - 触发配置变更回调(如果有注册监听器) +4. **监听器机制**: + - 应用可以注册配置监听器 + - 当配置变更时,监听器会被触发 + - 应用可以在监听器中实现自定义逻辑 \ No newline at end of file diff --git a/17.Maven.md b/17.Maven.md old mode 100755 new mode 100644 diff --git a/18.Open Question.md b/18.Open Question.md old mode 100755 new mode 100644 index 6b4198b..75a2ece --- a/18.Open Question.md +++ b/18.Open Question.md @@ -1,16 +1,12 @@ # 18.开放题 -##### 假设一个场景,要求stop the world时间非常短,你会怎么设计垃圾回收机制? +### 假设一个场景,要求stop the world时间非常短,你会怎么设计垃圾回收机制? STW时间短即要求应用响应时间快,应用的绝大多数对象都存在年轻代中,并且能够活到GC的对象很少,所以采用复制算法,只需要复制少量的对象就可以完成收集,同时将年轻代大小调大,通过-Xmn设置。由于年轻代分为Eden区和两个Survivor区,大部分新生对象都存在Eden区,因此还可以通过-XX:SurvivorRatio 调大Eden区的比例,比如-XX:SurvivorRatio=4,表示两个Survivor区与一个Eden区的比值为2:4 +### 现在有一个A类,其中有A、B、C方法,C方法中调用了A、B,定义了一个A、B方法的日志切面,请问能打印出日志吗? - -##### 现在有一个A类,其中有A、B、C方法,C方法中调用了A、B,定义了一个A、B方法的日志切面,请问能打印出日志吗? - - - -##### 分级代理问题 +### 分级代理问题 某公司销售一款智能硬件柜机,内含多种可消费的服务。 @@ -27,9 +23,17 @@ STW时间短即要求应用响应时间快,应用的绝大多数对象都存 2. 假设订单成功消息将触发onTransactionSuccess( ... )方法,请给出(...)部分的必要参数列表 3. 写出onTransactionSuccess方法的伪代码,调用#1列出的方法完成分成逻辑 +### 设计短链接问题 +https://blog.csdn.net/xlgen157387/article/details/80026452 + + + +### TCP连接如何保证安全 + +TCP 连接只要有IP+端口就可以,首先要保证连接的安全性,通过TLS对TCP的传输数据进行加密。 + +防止报文重放:TCP连接后,由平台生成一个随机秘钥下发给客户端,每次数据传输都对数据进行签名,接收方对签名进行校验,计算规则可以是:data+timeStamp+signKey,接收方首先会校验时间戳有效性(比如30秒内有效),接着校验数据签名,签名不一致表示报文数据可能有篡改。 -##### 设计短链接问题 -https://blog.csdn.net/xlgen157387/article/details/80026452 diff --git a/19.Distribute_MicroService.md b/19.Distribute_MicroService.md old mode 100755 new mode 100644 index 472a466..98922cc --- a/19.Distribute_MicroService.md +++ b/19.Distribute_MicroService.md @@ -30,7 +30,11 @@ Soft-state:在基于client-server模式的系统中,server端是否有状态 Eventually consistent:数据最终一致性 +### CAP 为什么只能满足两项 +由于分布式系统中存在网络的不稳定因素(网络故障、丢包等),因此在分布式系统中P(分区容错性)是首先要满足的。此时,如果要保证C(一致性),当A更新了数据,必须等待B同步更新数据才能响应客户端,如果中间出现网络问题,就不能响应客户端,这时就无法保证A(可用性); + +如果要保证A(可用性),就需要立即响应客户端,但A、B数据可能会不一致,无法保证C(一致性) ### 微服务与 SOA 的区别 @@ -80,7 +84,7 @@ SOA即面向服务架构,关注点是服务,现有的分布式服务化技 1. 2PC两阶段提交 - 分准备阶段、提交阶段,由事务管理协调器发起 + 分**准备阶段、提交阶段**,由事务管理协调器发起 准备阶段:事务管理器向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,则会写redo或者undo日志,然后锁定资源,执行操作,但并不提交。如果其中一个参与者返回准备准备失败,则协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源 @@ -140,6 +144,13 @@ SOA即面向服务架构,关注点是服务,现有的分布式服务化技 Paxos 算法运行在运行宕机故障的异步系统中,它不要求可靠的消息传递,也容忍消息丢失,延迟,乱序和重复,它利用大多数机制保证了"2F+1"的容错能力,即"2F+1"个节点的系统最多允许F个节点同时出现故障 +### Seata 支持事务模式 + +1. AT模式,基于2PC实现,适用高并发、对性能敏感的业务场景 +2. TCC模式,基于TCC实现,侵入业务,需要实现TCC的逻辑 +3. Saga模式,基于Saga实现,适用业务流程长的场景 +4. XA模式,基于XA协议(2PC实现),适用使用XA模式的老应用迁移到Seata平台,以及AT模式未适配的数据库应用 + ### 说说达到最终一致性的方案 1. 查询模式,通过查询了解调用服务的最终处理情况,决定下一步做什么 @@ -158,8 +169,29 @@ SOA即面向服务架构,关注点是服务,现有的分布式服务化技 如果实例服务是分布式部署,需要将同一请求路由到同一个实例,可以通过某个请求参数的hash路由,也可以通过Nginx的hash路由功能 - https://blog.csdn.net/dustin_cds/article/details/79595297 + https://blog.csdn.net/dustin_cds/article/details/79595297 + +### 架构设计原则 + +要保持模块模块大小适中,尽可能减少调用深度,多扇入少扇出,保持高内聚低耦合,单入口、单出口,模块的作用域要在模块内,模块功能是可预测的,另外面向对象设计要遵守如下原则: + +- 单一责任原则:一个类只做一种责任类型 + +比如,一个"用户类"不应同时负责用户信息管理和订单生成,这两项职责应拆分到"用户类"和"订单类"中 + +- 开放-封闭原则:支持扩展,不支持修改 + +比如,一个"支付接口"可以通过新增"支付宝实现类","微信支付实现类"来扩展支付方式,而不是修改原有的接口 + +- 里氏替换原则:子类可以替换父类 + +比如,一个支付接口,子类无论是"支付宝实现类"还是"微信支付实现类",都能正常实现支付 + +- 依赖倒置原则:细节依赖抽象 + +比如,订单服务调用物流服务时,依赖的是物流服务的抽象接口,而非具体的"顺丰物流"、"圆通物流" +- 接口分离原则:不强迫适用,依赖抽象,不依赖具体 +一个 "多功能设备接口" 不应包含 "打印""扫描""复印" 所有方法,而应拆分为 "打印接口""扫描接口" 等,让仅需打印功能的客户端只依赖 "打印接口" -### \ No newline at end of file diff --git a/2.Java Concurrent.md b/2.Java Concurrent.md old mode 100755 new mode 100644 index 9b138ec..d78dcfd --- a/2.Java Concurrent.md +++ b/2.Java Concurrent.md @@ -6,7 +6,7 @@ ### 运行中的线程能否强制杀死 -Jdk提供了stop()方法用于强制停止线程,但官方并不建议使用,因为强制停止线程会导致线程使用的资源,比如文件描述符、网络连接处于不正常的状态。建议使用标志位的方式来终止线程,如果线程中有使用无限期的阻塞方式,比如wait()没有设置超时时间,就只能使用interrupt()方法来终止线程 +Jdk提供了stop()方法用于强制停止线程,但官方并不建议使用,因为强制停止线程会导致线程使用的资源,比如文件描述符、网络连接处于不正常的状态。建议使用标志位的方式来终止线程,如果线程中有使用无限期的阻塞方式,比如wait()没有设置超时时间,就只能使用interrupt()方法来终止线程 ```java @SneakyThrows @@ -39,15 +39,24 @@ class Thread1 extends Thread{ ### ThreadLocal 子类及原理, OOM产生原因及防治 - InheritableThreadLocal - - 继承了ThreadLocal,并重写childValue、getMap、createMap,对该类的操作实际是对线程ThreadLocalMap的操作 - + 继承了ThreadLocal,并重写childValue、getMap、createMap,对该类的操作实际是对线程ThreadLocalMap的操作。 子线程能够读取父线程数据,实际原因是新建子线程的时候,会从父线程copy数据 + +> 使用场景:子线程能够访问父线程设置的信息 -- OOM原因及防治 +- ThreadLocal原理及OOM原因及防治 ThreadLocal只是一个工具类,具体存放变量的是线程的threadLocals变量,threadLocals是一个ThreadLocalMap类型的变量,内部是一个Entry数组,Entry继承自WeakReference,Entry内部的value用来存放通过ThreadLocal的set方法传递的值,key是ThreadLocal的弱引用,key虽然会被GC回收,但value不能被回收,这时候ThreadLocalMap中会存在key为null,value不为null的entry项,如果时间长了就会存在大量无用对象,造成OOM。虽然set,get也提供了一些对Entry项清理的时机,但不及时,`所以在使用完毕后需要及时调用remove` - - https://www.cnblogs.com/micrari/p/6790229.html + +> 源码分析:[ThreadLocal源码解读](源码分析/ThreadLocal.md) 源码分析/ThreadLocal.md + +### 分布式系统中使用ThreadLocal 要注意的问题 + +- 跨服务调用,ThreadLocal存储的数据丢失 + +通过协议头(HTTP Header、RPC元数据)携带ThreadLocal中存储的数据,接收方在将协议头中的数据重新设置到本地ThreadLocal中 + +- 线程池复用,导致数据污染 + 无论是否使用线程池,都在数据使用完毕后,调用ThreadLocal.remove(),避免复用污染 ### 有哪些并发队列 @@ -56,8 +65,11 @@ LinkedBlockingQueue: 有界阻塞队列,使用单向链表实现,通过Reent ArrayBlockingQueue: 有界数组方式实现的阻塞队列 , 通过ReentrantLock实现线程安全,阻塞通过Condition实现,出队和入队使用同一把锁 PriorityBlockingQueue: 带优先级的无界阻塞队列,内部使用平衡二叉树堆实现,遍历保证有序需要自定排序 DelayQueue: 无界阻塞延迟队列,队列中的每个元素都有个过期时间,当从队列获取元素时,只有过期元素才会出队列,队列头元素是最快要过期的元素 -SynchronousQueue: 任何一个写需要等待一个读的操作,读操作也必须等待一个写操作,相当于数据交换 https://www.cnblogs.com/dwlsxj/p/Thread.html -LinkedTransferQueue: 由链表组成的无界阻塞队列,多了tryTransfer 和 transfer方法。transfer方法,能够把生产者元素立刻传输给消费者,如果没有消费者在等待,那就会放入队列的tail节点,并阻塞等待元素被消费了返回,可以使用带超时的方法。tryTransfer方法,会在没有消费者等待接收元素的时候马上返回false +SynchronousQueue: 任何一个写需要等待一个读的操作,读操作也必须等待一个写操作,相当于数据交换 + +> [SynchronousQueue原理详解-公平模式](源码分析/SynchronousQueue.md) 源码分析/SynchronousQueue.md + +LinkedTransferQueue: 由链表组成的无界阻塞队列,多了tryTransfer 和 transfer方法。transfer方法,能够把生产者元素立刻传输给消费者,如果没有消费者在等待,那就会放入队列的tail节点,并**阻塞**等待元素被消费了返回,可以使用带超时的方法。tryTransfer方法,会在没有消费者等待接收元素的时候**马上返回false** LinkedBlockingDeque: 由链表组成的双向阻塞队列,可以从队列的两端插入和移除元素 @@ -65,27 +77,18 @@ LinkedBlockingDeque: 由链表组成的双向阻塞队列,可以从队列的 - 构造参数: corePoolSize: 线程池核心线程个数 - maximunPoolSize: 线程池最大线程数量 - keeyAliveTime: 空闲线程存活时间 - TimeUnit: 存活时间单位 - workQueue: 用于保存等待执行任务的阻塞队列 - ThreadFactory: 创建线程的工厂 RejectedExecutionHandler: 队列满,并且线程达到最大线程数量的时候,对新任务的处理策略,AbortPolicy(抛出异常)、CallerRunsPolicy(使用调用者所在线程执行)、DiscardOldestPolicy(调用poll丢弃一个任务,执行当前任务)、DiscardPolicy(默默丢弃、不抛异常) - + - 原理: 线程池主要是解决两个问题: - 一个是当执行大量异步任务时能够提供较好的性能,能复用线程处理任务; - 二是能够对线程池进行资源限制和管理。 - 一个任务提交的线程池,首先会判断核心线程池是否已满,未满就会创建worker线程执行任务,已满判断阻塞队列是否已满,阻塞队列未满加入阻塞队列,已满就判断线程池线程数量是否已经达到最大值,没有就新建线程执行任务,达到最大值的话执行拒绝策略。 - 拒绝策略有:直接抛出异常、使用调用者所在线程执行、丢弃一个旧任务,执行当前任务、直接丢弃什么都不做。 - 创建线程池的方式:直接new ThreadPoolExecutor 或者通过Executors工具类创建 @@ -97,7 +100,7 @@ LinkedBlockingDeque: 由链表组成的双向阻塞队列,可以从队列的 3. newCachedThreadPool 创建一个核心线程数为0,最大线程为Inter.MAX_VALUE的线程池,也就是说没有限制,线程池中的线程数量不确定,但如果有空闲线程可以复用,则优先使用,如果没有空闲线程,则创建新线程处理任务,处理完放入线程池 4. newSingleThreadScheduledExecutor 创建只有一个线程的可以定时执行的线程池 5. newScheduledThreadPool 创建一个没有最大线程数限制的可以定时执行线程池 -6. newWorkStealingPool 创建一个含有足够多线程的线程池,能够调用闲置的CPU去处理其他的任务,使用ForkJoinPool实现,jdk8新增 +6. newWorkStealingPool 创建一个含有足够多线程的线程池,能够调用闲置的CPU去处理其他的任务,使用ForkJoinPool实现,jdk8新增(线程数量根据当前系统的可用处理器核心数(逻辑核心),默认值等于系统的逻辑核心数量) ### 线程池的阻塞队列为什么都用LinkedBlockingQueue,而不用ArrayBlockingQueue @@ -113,7 +116,7 @@ ArrayBlockingQueue 使用数组实现,在声明的时候必须指定长度, 如CPU密集型的任务,基本线程池应该配置多大?IO密集型的任务,基本线程池应该配置多大?用有界队列好还是无界队列好?任务非常多的时候,使用什么阻塞队列能获取最好的吞吐量? -CPU密集型,为了充分使用CPU,减少上下文切换,线程数配置成CPU个数+1个即可 +CPU密集型,为了充分使用CPU,减少上下文切换,线程数配置成CPU个数+1个即可(线程数略多于核心数可避免因个别线程偶尔阻塞导致的CPU闲置,同时避免过多上下文切换) IO密集型,由于可能大部分线程在处理IO,IO都比较耗时,因此可以配置成 2*CPU个数的线程,去处理其他任务 @@ -206,83 +209,83 @@ public class CopyOnWriteArrayListTest { - CountDownLatch: 使用AQS实现,通过AQS的状态变量state来作为计数器值,当多个线程调用countdown方法时实际是原子性递减AQS的状态值,当线程调用await方法后当前线程会被放入AQS阻塞队列等待计数器为0再返回 - - ```java - public class CountDownLatchTest { - - public static final CountDownLatch countDownLatch = new CountDownLatch(2); - - ExecutorService executor = Executors.newFixedThreadPool(2); - - @Test - public void test1() throws InterruptedException { - executor.submit(() -> { - System.out.println(Thread.currentThread().getName() + " step1"); - countDownLatch.countDown(); - }); - executor.submit(() -> { - System.out.println(Thread.currentThread().getName() + " step2"); - countDownLatch.countDown(); - }); - - countDownLatch.await(); - System.out.println("thread end"); - } - } - ``` - - ``` - pool-1-thread-1 step1 - pool-1-thread-2 step2 - thread end - ``` - - + + ```java + public class CountDownLatchTest { + + public static final CountDownLatch countDownLatch = new CountDownLatch(2); + + ExecutorService executor = Executors.newFixedThreadPool(2); + + @Test + public void test1() throws InterruptedException { + executor.submit(() -> { + System.out.println(Thread.currentThread().getName() + " step1"); + countDownLatch.countDown(); + }); + executor.submit(() -> { + System.out.println(Thread.currentThread().getName() + " step2"); + countDownLatch.countDown(); + }); + + countDownLatch.await(); + System.out.println("thread end"); + } + } + ``` + + ``` + pool-1-thread-1 step1 + pool-1-thread-2 step2 + thread end + ``` + + - CyclicBarrier: 区别:CountDownLatch计数器是一次性的,变为0后就起不到线程同步的作用了。而CyclicBarrier(撒克里克巴瑞儿)在计数器变为0后重新开始,通过调用await方法,能在所有线程到达屏障点后统一执行某个任务,再执行完后继续执行子线程,通过ReentrantLock实现 - - ```java - public class CyclicBarrierTest { - - public static final CyclicBarrier cycle = new CyclicBarrier(3); - - ExecutorService executorService = Executors.newFixedThreadPool(2); - - @Test - public void test1() throws BrokenBarrierException, InterruptedException { - executorService.submit(() -> { - System.out.println(Thread.currentThread().getName()); - try { - cycle.await(); - System.out.println(Thread.currentThread().getName() + ",执行结束"); - } catch (InterruptedException | BrokenBarrierException e) { - e.printStackTrace(); - } - }); - - executorService.submit(() -> { - System.out.println(Thread.currentThread().getName()); - try { - Thread.sleep(3000); - cycle.await(); - System.out.println(Thread.currentThread().getName() + ",执行结束"); - } catch (InterruptedException | BrokenBarrierException e) { - e.printStackTrace(); - } - }); - cycle.await(); - } - } - ``` - - ``` - pool-1-thread-1 - pool-1-thread-2 - pool-1-thread-2,执行结束 - pool-1-thread-1,执行结束 - ``` - + + ```java + public class CyclicBarrierTest { + + public static final CyclicBarrier cycle = new CyclicBarrier(3); + + ExecutorService executorService = Executors.newFixedThreadPool(2); + + @Test + public void test1() throws BrokenBarrierException, InterruptedException { + executorService.submit(() -> { + System.out.println(Thread.currentThread().getName()); + try { + cycle.await(); + System.out.println(Thread.currentThread().getName() + ",执行结束"); + } catch (InterruptedException | BrokenBarrierException e) { + e.printStackTrace(); + } + }); + + executorService.submit(() -> { + System.out.println(Thread.currentThread().getName()); + try { + Thread.sleep(3000); + cycle.await(); + System.out.println(Thread.currentThread().getName() + ",执行结束"); + } catch (InterruptedException | BrokenBarrierException e) { + e.printStackTrace(); + } + }); + cycle.await(); + } + } + ``` + + ``` + pool-1-thread-1 + pool-1-thread-2 + pool-1-thread-2,执行结束 + pool-1-thread-1,执行结束 + # 等待所有线程都执行完成 + ``` ### Phaser 的实现 @@ -307,7 +310,7 @@ public class PhaserTest { executor.submit(() -> { System.out.println(Thread.currentThread().getName() + " step2"); - phaser.arrive(d); + phaser.arrive(); }); //等同await() @@ -401,39 +404,40 @@ Thread2:1 什么时候使用volatile? +- 有一个变量,这个变量需要被多线程检查某个条件,此时使用volatile确保每次都从主内存读取 + - 写入变量值不依赖变量的当前值。因为如果依赖当前值,将是获取-计算-写入三步操作,这三步操作不是原子性的,而volatile不保证原子性 - 读写变量值时没有加锁。因为加锁已经保证了内存可见性,没必要再使用volatile - volatile不能保证原子性 - - ```java - public class VolatileTest { - - public volatile int inc = 0; - - public void increase() { - inc++; - } - - public static void main(String[] args) { - final VolatileTest test = new VolatileTest(); - - for(int i = 0; i < 10; i++) { - new Thread(() -> { - for(int j = 0; j < 1000; j++) - test.increase(); - }).start(); - } - //保证前面的线程都执行完 - while (Thread.activeCount()> 1) - Thread.yield(); - - System.out.println(test.inc); - } - } - //输出:<=10000 - ``` + + ```java + public class VolatileTest { + + public volatile int inc = 0; + + public void increase() { + inc++; + } + + public static void main(String[] args) { + final VolatileTest test = new VolatileTest(); + + for(int i = 0; i < 10; i++) { + new Thread(() -> { + for(int j = 0; j < 1000; j++) + test.increase(); + }).start(); + } + //保证前面的线程都执行完 + while (Thread.activeCount()> 1) + Thread.yield(); + + System.out.println(test.inc); + } + } + //输出:<=10000 + ``` ### 伪共享 @@ -443,8 +447,6 @@ Thread2:1> @Contended注解只用于Java核心类,如果用户类路径下的类要使用这个注解,需要添加JVM参数:-XX:-RestrictContended。默认填充宽度为128,需要自定义宽度设置 -XX:ContendedPaddingWidth参数 -> CPU缓存行详细说明:https://mp.weixin.qq.com/s/yosnZr0bDdLrhmpnX8TY5A - ### 原子操作类 **AtomicBoolean** @@ -456,19 +458,15 @@ Thread2:1 整型的原子操作类,1.8后提供函数式操作的方法 - int getAndUpdate(IntUnaryOperator updateFunction) - 使用指定函数计算并更新,返回计算前结果 - int updateAndGet(IntUnaryOperator updateFunction) - 使用指定函数计算并更新,返回计算后的结果 - int getAndAccumulate(int x,IntBinaryOperator accumulatorFunction) - 使用指定的函数计算x值和当前值,返回计算前结果 - int accumulateAndGet(int x,IntBinaryOperator accumulatorFunction) - 使用指定的函数计算x值和当前值,返回结算后的结果 ```java @@ -510,15 +508,12 @@ public void atomicIntegerTest() { 提供了原子性更新整型数组元素的方式 - int getAndUpdate(int i, IntUnaryOperator updateFunction) - 使用指定函数计算i索引的值,返回计算前结果 - int updateAndGet(int i, IntUnaryOperator updateFunction) - 使用指定函数计算i索引的值,返回计算后结果 - int getAndAccumulate(int i, int x, IntBinaryOperator accumulatorFunction) - 使用指定的函数计算x值和i索引的值,返回计算前结果 - int accumulateAndGet(int i, int x, IntBinaryOperator accumulatorFunction) @@ -683,46 +678,53 @@ public void test1() { AtomicStampedReference解决ABA问题,通过维护一个版本号 ```java -@SneakyThrows -@Test -public void test2() { - AtomicStampedReference atomicStampedReference = new AtomicStampedReference(10,1); - - CountDownLatch countDownLatch = new CountDownLatch(2); + //AtomicStampedReference,通过维护一个版本号 + @SneakyThrows + @Test + public void test2() { + AtomicStampedReference atomicStampedReference = new AtomicStampedReference(10,1); - new Thread(() -> { - System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp()); - atomicStampedReference.compareAndSet(10, 11, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); - System.out.println(Thread.currentThread().getName() + " 第二次版本:" + atomicStampedReference.getStamp()); - atomicStampedReference.compareAndSet(11, 10, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); - System.out.println(Thread.currentThread().getName() + " 第三次版本:" + atomicStampedReference.getStamp()); - countDownLatch.countDown(); - }).start(); + CountDownLatch countDownLatch = new CountDownLatch(2); - new Thread(() -> { - System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp()); - try { - TimeUnit.SECONDS.sleep(2); - boolean isSuccess = atomicStampedReference.compareAndSet(10,12, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); - System.out.println(Thread.currentThread().getName() + " 修改是否成功:" + isSuccess + " 当前版本:" + atomicStampedReference.getStamp() + " 当前值:" + atomicStampedReference.getReference()); + new Thread(() -> { + //使用第一次获取的版本,因为不知道有其他线程偷摸改了 + int stamp = atomicStampedReference.getStamp(); + System.out.println(Thread.currentThread().getName() + " 第一次版本:" + stamp); + try { + //等待一下 + TimeUnit.SECONDS.sleep(2); + //这个线程打算修改10->12 + boolean isSuccess = atomicStampedReference.compareAndSet(10,12, stamp, stamp + 1); + System.out.println(Thread.currentThread().getName() + " 修改是否成功:" + isSuccess + " 当前版本:" + atomicStampedReference.getStamp() + " 当前值:" + atomicStampedReference.getReference()); + countDownLatch.countDown(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }).start(); + + new Thread(() -> { + //这个线程偷摸的把10->11->10 + System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp()); + atomicStampedReference.compareAndSet(10, 11, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); + System.out.println(Thread.currentThread().getName() + " 第二次版本:" + atomicStampedReference.getStamp()); + atomicStampedReference.compareAndSet(11, 10, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); + System.out.println(Thread.currentThread().getName() + " 第三次版本:" + atomicStampedReference.getStamp()); countDownLatch.countDown(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }).start(); + }).start(); - countDownLatch.await(); -} + + countDownLatch.await(); + } ``` ```java //输出 +Thread-1 第一次版本:1 Thread-0 第一次版本:1 -Thread-0 第二次版本:2 -Thread-0 第三次版本:3 -Thread-1 第一次版本:3 -Thread-1 修改是否成功:true 当前版本:4 当前值:12 +Thread-1 第二次版本:2 +Thread-1 第三次版本:3 +Thread-0 修改是否成功:false 当前版本:3 当前值:10 ``` AtomicMarkableReference 通过标志位,由于其标志位只有true和false,如果每次更新都变更标志位,在第三次的时候标志位还是跟第一次一样,并没有解决ABA问题 @@ -911,7 +913,7 @@ public void LongAccumulatorTest() { 3. BlockingQueue阻塞队列 put() 和take方法 4. Semaphore 基于计数的信号量 5. PipedInputStream / PipedOutputStream 管道输入输出流 -https://blog.csdn.net/ldx19980108/article/details/81707751 + https://blog.csdn.net/ldx19980108/article/details/81707751 ### 说说Random 与 ThreadLocalRandom @@ -931,3 +933,71 @@ https://www.jianshu.com/p/89dfe990295c https://blog.csdn.net/m0_37542889/article/details/92640903 +### Lambda表达式和Stream API 的底层实现原理 + +- Lambda表达式 + +Java8引入的函数式编程语法糖,依赖函数式接口(只有一个抽象方法的接口),本质是该接口抽象方法的实现,通过invokedynamic 指令和方法句柄(Method Handle)实现动态绑定,能够减少匿名内部类带来的类加载开销和字节码冗余 + +- Stream API + +底层通过Pipeline(流水线)、Spliterator(拆分器)实现,支持串行和并行处理。 + +Pipeline(流水线)由数据源(如List)、中间操作(如filter、map)和终端操作(如collect、forEach)组成,中间操作是"惰性的",仅记录操作逻辑,不立即执行,到达终点后才遍历数据源,依次执行中间操作 + +Spliterator 用于拆分数据源的迭代器(支持并行处理),定义了`trySplit()` 方法(拆分数据为子部分)和 `tryAdvance()` 方法(遍历元素) + +并行流:基于ForkJoin框架,通过 Spliterator 将数据源拆分为多个子任务,由多个线程并行处理,最后合并结果 + +### 用过流没有,流怎么实现 + +Stream流是Java8中引入的新特性,Stream有几个特点: + +不存数据,都是通过管道将源数据元素传递给操作; + +对Stream的任何修改都不会修改数据源,都是新产生一个流 + +流的很多操作如filter、map都是延迟执行的,只有到终点才会将操作顺序执行 + +对于无限流可以通过"短路"操作访问到有限元素后就返回 + +流的元素只访问一次,如果需要重新访问,需要重新生成一个新的流 + +Stream中BaseStream规定了流的基本接口,在Stream中使用Stage来描述一个完整的操作,将具有先后顺序的各个Stage连一起,就构成了整个流水线。 + +AbstractPipeline是流水线的核心,定义了三个AbstractPipeline类型的变量:sourceStage(源阶段)、previousStage(上游pipeline,上一阶段),nexStage(下一阶段) + +ReferencePipeline 继承了AbstractPipeline + +Head、StatefulOp、StatelessOp继承了ReferencePipeline,分别表示源,无状态操作,有状态操作 + +![image-20200629003951004](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/007S8ZIlly1gg8h9m9dooj310o0daabt-20240625214323108.jpg) + +比如Collection.stream()方法得到Head也就是stage0,紧接着调用一系列的中间操作,不断产生新的stage。这些stage对象以双向链表的形式组织在一起,构成整个流水线。 +由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就建立起对数据源的所有操作。 + +### parallelStream 怎么实现的并行处理 + +parallelStream 底层使用ForkJoinPool 实现并行处理,默认线程为可用的CPU数量。 + +### 解释一下ForkJoin框架 + +parallelStream的底层是基于ForkJoinPool的,ForkJoinPool实现了ExecutorService接口 + +Fork/Join框架主要采用分而治之的理念来处理问题,对于一个比较大的任务,首先将它拆分(fork)为多个小任务task1、task2等。再使用新的线程thread1去处理task1,thread2去处理task2。 + +如果thread1认为task1还是太大,则继续往下拆分成新的子任务task1.1与task1.2。thread2认为task2任务量不大,则立即进行处理,形成结果result2。 + +之后将task1.1和task1.2的处理结果合并(join)成result1,最后将result1与result2合并成最后的结果。 + +img + +### CompletableFuture 异步编程实现原理 + +CompletableFuture是Java8引入的异步编程工具,基于回调机制和事件驱动模型实现异步操作,核心原理是: + +1. 异步执行任务:通过线程池(默认使用ForkJoinPool)执行异步任务,任务执行期间主线程无需阻塞等待 +2. 回调触发机制:任务完成后,自动触发注册的回调函数(如thenApply、exceptionally等),回调函数在任务完成后由线程池中的线程执行 +3. 链式编程:内部实现CompletionStage接口,允许将多个异步操作串联或并联,形成流水线式的异步处理流程 + +传统Future 要get()阻塞等待获取结果,或轮询isDone();无法直接串联多个异步任务;难以合并多个独立异步任务的结果。 diff --git a/20.Java Performance.md b/20.Java Performance.md old mode 100755 new mode 100644 index 5da6730..4088448 --- a/20.Java Performance.md +++ b/20.Java Performance.md @@ -244,6 +244,8 @@ jstat -gcutil 31798 -XX:+UseConcMarkSweepGC: 设置CMS收集器 +-XX:+UseZGC: 使用ZGC + **垃圾回收统计信息** @@ -284,7 +286,7 @@ jstat -gcutil 31798 -XX:ParallelGCThreads=n: 设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。 --XX:+CMSInitiatingOccupancyFraction: 设置CMS收集器在老年代空间被使用多少后触发,默认68% +-XX:+CMSInitiatingOccupancyFraction: 设置CMS收集器在老年代空间被使用多少后触发,默认92% -XX:+UseCMSCompactAtFullCollection: 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理 @@ -294,24 +296,40 @@ jstat -gcutil 31798 -XX:+CMSParallelRemarkEnabled: 启用并行重标记 +-XX:+CMSParallelInitialMarkEnabled:在初始标记的时候使用多线程 + -XX:CMSInitiatingPermOccupancyFraction: 当永久区占用率达到这一百分比时,启动CMS回收(前提是开启-XX:+CMSClassUnloadingEnabled) --XX:UseCMSInitiatingOccupancyOnly: 表示只在到达阈值的时候,才进行CMS回收 +-XX:+UseCMSInitiatingOccupancyOnly: 表示只在到达阈值的时候(-XX:CMSInitiatingPermOccupancyFraction指定的值),才进行CMS回收,如果不指定,JVM仅在第一次使用设定值,后续会自动调整 + +-XX:+CMSScavengeBeforeRemark:在CMS GC 前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC 标记阶段的开销,一般CMS的GC耗时80%都在标记阶段 **G1回收器设置** --XX:+UseG1GC: 使用G1回收器 + -XX:+UseG1GC: 使用G1回收器 -XX:+UnlockExperimentalVMOptions: 允许使用实验性参数 --XX:MaxGCPauseMillis: 设置最大垃圾收集停顿时间 +-XX:MaxGCPauseMillis: 设置最大垃圾收集停顿时间(默认200ms) -XX:GCPauseIntervalMillis: 设置停顿间隔时间 -XX:+DisableExplicitGC: 禁用显示GC +-XX:ParallelGCThreads:指定GC工作的线程数量 + +-XX:G1HeapRegionSize: 指定分区大小(1MB-32MB,必须是2的N次幂),默认将整堆划分为2048个分区 + +-XX:G1NewSizePercent: 新生代内存初始空间(默认整堆5%,值配置整数,默认就是百分比) + +-XX:G1MaxNewSizePercent: 新生代内存最大空间 + +-XX:TargentSurvivorRatio: Survivor区的填充容量(默认50%),Survivor区域里的一批对象总和超过了Survivor区域的50%,此时会把年龄n(含)以上的对象都放入老年代 + +-XX:MaxTenuringThreshold:最大年龄阈值(默认15) + > 更多垃圾回收器参数,见 @@ -540,7 +558,55 @@ the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000) +### 如何根据业务场景选择合适的垃圾收集器 + +从延迟、吞吐量、内存大小来考虑。 + +- #### SerialGC + + - 适用场景: + - 堆内存较小(通常 < 1GB)的应用; + - 单核 CPU 或资源受限的环境(如嵌入式设备、简单客户端应用); + - 对延迟不敏感的场景(如本地工具类程序)。 + + - 不适用:服务端应用、多线程高并发场景。 + +- #### ParallelGC + + - 适用场景: + - 对吞吐量要求高,对延迟不敏感的应用(如后台批处理任务、数据分析、科学计算); + - 堆内存中等(1GB ~ 10GB),且 CPU 核心数较多(充分利用多线程加速 GC); + - 不需要低延迟的服务(如离线数据处理)。 + + - 不适用:对响应时间敏感的服务(如 Web 应用、实时交易系统)。 + +- #### CMS + + - 适用场景: + - 对延迟敏感的服务端应用(如 Web 服务器、电商网站、API 服务),需要快速响应用户请求; + - 堆内存中等(1GB ~ 10GB),且 CPU 资源充足(需预留线程处理并发 GC); + - 无法接受长时间 STW 的场景。 + - 不适用: + - 堆内存过大(>10GB)(并发阶段耗时太长,反而可能增加延迟); + - CPU 资源紧张的环境(并发阶段会抢占用户线程 CPU); + - JDK 9 及以上(CMS 已被标记为 deprecated,JDK 14 移除)。 +- #### G1 + - 适用场景: + - 堆内存较大(4GB ~ 数百 GB)的服务端应用(如企业级应用、中间件、微服务); + - 需要平衡吞吐量和延迟的场景(既不能接受过长停顿,也需要一定的吞吐量); + - 替代 CMS 的主流选择(JDK 9 后默认 GC)。 + - 不适用: + - 堆内存极小(<4gb)(region 管理的 overhead 不划算); + - 对延迟要求极高(如微秒级响应)的场景(STW 停顿通常在几十到几百毫秒)。 +- #### ZGC + - 适用场景: + - 对延迟要求极高的场景(如高频交易系统、实时数据分析、大型分布式服务); + - 堆内存超大(数十 GB 到 TB 级),且需要快速响应的应用; + - 希望在大内存下保持低延迟的现代服务(如云原生应用)。 + - 不适用: + - JDK 版本过低(需 JDK 11+,且 JDK 15 后才正式可用); + - 对吞吐量要求极致且可接受长停顿的场景(ZGC 为低延迟牺牲了部分吞吐量优化)。 diff --git a/21. Nginx.md b/21. Nginx.md old mode 100755 new mode 100644 diff --git a/22. ShardingJDBC.md b/22. ShardingJDBC.md old mode 100755 new mode 100644 index c09302b..8996851 --- a/22. ShardingJDBC.md +++ b/22. ShardingJDBC.md @@ -1,4 +1,4 @@ -# 22.Sharding-JDBC +# 22.分库分表 [toc] @@ -10,6 +10,8 @@> > 数据库分库分表思路:https://www.cnblogs.com/butterfly100/p/9034281.html +## Sharding-JDBC + ### 分库分表的方式 - 垂直分库 @@ -248,10 +250,12 @@ mycat:自己上官网,找一个官网最基本的例子,自己写一下, orderId 模 32 = 库 orderId / 32 模 32 = 表 -259 3 8 -1189 5 5 -352 0 11 -4593 17 15 +orderId 库 表 + +259 3 8 +1189 5 5 +352 0 11 +4593 17 15 1、设定好几台数据库服务器,每台服务器上几个库,每个库多少个表,推荐是32库 * 32表,对于大部分公司来说,可能几年都够了; @@ -263,4 +267,6 @@ orderId / 32 模 32 = 表 5、我们这边就是修改一下配置,调整迁移的库所在数据库服务器的地址; -6、重新发布系统,上线,原先的路由规则变都不用变,直接可以基于2倍的数据库服务器的资源,继续进行线上系统的提供服务。 \ No newline at end of file +6、重新发布系统,上线,原先的路由规则变都不用变,直接可以基于2倍的数据库服务器的资源,继续进行线上系统的提供服务。 + +![image-20240903130206635](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/image-20240903130206635.png) \ No newline at end of file diff --git a/23.ES.md b/23.ES.md new file mode 100644 index 0000000..45d63e7 --- /dev/null +++ b/23.ES.md @@ -0,0 +1,62 @@ +# 23.ES + +### ES的分布式架构原理 + +核心思想是在多台机器上启动多个ES进程实例,组成一个ES集群。ES中存储数据的基本单位是索引,用来存储具有共同特性的文档集合,一个索引差不多就相当于Mysql中的表。为了提高可伸缩性和容错性,ES会将索引划分为多个分片,每个分片都是一个独立的Lucene索引,可以部署在集群中的任何节点上,一个索引包含一个或多个主分片和零个或多个副本分片,主分片负责数据的写入,而副本分片则用于数据的容错和读请求的分流。 + +### ES 的核心概念 + +- 索引(Index) + +类似数据库中的"表",是文档的逻辑集合。每个索引有一个唯一的名称(如products) + +- 文档(Document) + +索引中的基本数据单元,以JSON格式存储;每个文档有一个唯一ID和类型 + +- 分片(Shard) + +索引被水平拆分的子集,每个分片是一个独立的Lucene索引; + +主分片:数据写入的目标分片,数量在索引创建时固定 + +副本分片:主分片的拷贝,提供高可用和读负载均衡 + +- 节点(Node) + +一个运行的ES实例,可以是数据节点、主节点或协调节点 + +### ES写入数据的流程 + +1. 客户端请求:文档发送到协调节点 +2. 路由与分片选择:协调节点根据文档ID的哈希值选择目标分片 +3. 写入主分片:主分片先写入内存缓冲区,同时记录到事务日志(用于崩溃恢复) +4. 刷新:默认1秒,内存缓冲区的内容生成一个新的段(Segment)并开放搜索 +5. 刷盘:定期将内存中的段持久化到磁盘 +6. 同步副本:主分片将写入操作同步到所有副本分片 + +### ES 如何实现全文搜索 + +- 倒排索引:核心数据结构,记录每个词项出现在哪些文档中 +- 分词:将文本转换为词项的过程 + +**查询流程**: + +1. 对查询字符串分词(使用相同的分析器)。 +2. 在倒排索引中匹配词项,计算相关性得分(如 TF-IDF、BM25)。 +3. 返回排序后的文档。 + +### ES在数据量很大的情况下如何提高性能 + +1. **索引设计**: + - 合理设置分片数(建议单个分片大小 10-50GB)。 + - 使用 `routing` 将相关文档存到同一分片,减少跨分片查询。 +2. **查询优化**: + - 避免 `wildcard` 查询(性能差),改用 `keyword` 类型 + 前缀搜索。 + - 使用 `filter` 替代 `query` 条件(不计算得分,结果可缓存)。 +3. **硬件与配置**: + - 数据节点使用 SSD,内存分配给文件系统缓存(建议不超过 50% 堆内存)。 + - 调整 `indices.query.bool.max_clause_count` 解决大量 `terms` 查询问题。 +4. **聚合优化**: + - 对高基数字段(如用户 ID)使用 `cardinality` 聚合时,开启 `precision_threshold`。 + diff --git a/24. AI.md b/24. AI.md new file mode 100644 index 0000000..4f136a5 --- /dev/null +++ b/24. AI.md @@ -0,0 +1,15 @@ +# 24. AI + +### 大模型聊天中有哪些配置,他们的作用 + +temperature:较高的温度生成的内容随机性更大,较低的温度会使大模型选择最可能的单词 + +top_p: 模型从累计概率大于或等于"p"的最小集合中随机选择一个 + +repetition_penalty: 重复惩罚 + +### 大模型中有哪几种role,他们的区别 + +1. system : 用于设定模型的行为规范、背景或任务指令,由开发者或系统设定,用户不可见,内容通常是对模型的隐性提示,如角色设定,回答格式等 +2. user:真实用户的输入,即用户向模型提出的问题或指令 +3. assistant:模型自身的回复,在多轮对话中起到记录历史响应,维持上下文连贯性的作用 \ No newline at end of file diff --git a/25.Netty.md b/25.Netty.md new file mode 100644 index 0000000..be839be --- /dev/null +++ b/25.Netty.md @@ -0,0 +1,173 @@ +# 25.Netty + +### TCP 的粘包和拆包 + +TCP 是以流的方式来处理数据的,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。 + +### BIO、NIO、AIO的区别 + +BIO:一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理,线程开销大。是面向流的,阻塞流,流是单向的。 + +NIO: 一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。面向缓存区,非阻塞,channe是双向的。 + +AIO:一个有效请求一个线程,客户端的IO请求都是由OS先完成了再通知服务器应用去启动线程进行处理 + +### NIO 的组成 + +Buffer:与Channel进行交互,数据从Channel读入缓冲区,从缓冲区写入Channel中 + +flip方法:反转缓冲区,将position给limit,然后将position置为0,就是读写切换 + +clear方法:清除此缓冲区,将position置为0,把capacity的值给limit + +rewind方法:重置此缓冲区,将position置为0 + +DirectByteBuffer:可减少一次系统空间到用户空间的拷贝 + +Channel:与数据源的连接,是双向的,只能与Buffer交互 + +Selector:允许单个线程管理多个Channel + +Pipe:两个线程之间的单向数据连接,数据会被写到sink通道,从source通道读取 + + + +NIO服务端建立过程: + +1. ServerSocketChannel.open 创建服务端Channel +2. bind 绑定服务端端口 +3. 配置非阻塞模式 +4. Selector.open 打开一个selector +5. 注册关注的事件到selector上 + +```java +// 服务端代码 +public class NioServer { + + public static void main(String[] args) { + try(ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) { + serverSocketChannel.socket().bind(new InetSocketAddress(3388)); + + Selector selector = Selector.open(); + serverSocketChannel.configureBlocking(false); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + + System.out.println("服务器准备就绪,开始监听,端口3388"); + + + while (true) { + int wait = selector.select(); + if (wait == 0) + continue; + + Set keys = selector.selectedKeys(); + Iterator iterator = keys.iterator(); + ByteBuffer byteBuffer = ByteBuffer.allocate(1024); + + while (iterator.hasNext()) { + + SelectionKey key = iterator.next(); + + if (key.isAcceptable()) { + ServerSocketChannel server = (ServerSocketChannel) key.channel(); + SocketChannel channel = server.accept(); + channel.configureBlocking(false); + channel.register(selector, SelectionKey.OP_READ); + } else if(key.isReadable()) { + SocketChannel server = (SocketChannel) key.channel(); + int len = server.read(byteBuffer); + if (len> 0) { + byteBuffer.flip(); + String content = new String(byteBuffer.array(), 0, len); + System.out.println(content); + + server.configureBlocking(false); + server.register(selector, SelectionKey.OP_WRITE); + } + byteBuffer.clear(); + } else if (key.isWritable()) { + SocketChannel server = (SocketChannel) key.channel(); + server.write(ByteBuffer.wrap("Hello Client!".getBytes())); + } + + iterator.remove(); + } + + } + + } catch (Exception e) { + e.printStackTrace(); + } + } +} + + +//客户端代码 +public class NIOClient { + + public static void main(String[] args) { + try(SocketChannel channel = SocketChannel.open()) { + channel.connect(new InetSocketAddress(3388)); + + //发送数据 + if (channel.isConnected()) { + channel.write(ByteBuffer.wrap("Hello Server!".getBytes())); + } + + ByteBuffer buffer = ByteBuffer.allocate(1024); + int len = channel.read(buffer); + String content = new String(buffer.array(), 0, len); + System.out.println(content); + }catch (Exception e) { + + } + + } +} +``` + +### Netty 的特点 + +一个高性能、异步事件驱动的NIO框架。 + +使用更高效的socket底层,处理了epoll空轮询引起的cpu占用飙升(Netty检测到空轮询的时候,主动重建Selector) + +采用decoder/encoder 支持,对TCP粘包/拆包进行自动化处理 + +可配置IO线程数、TCP参数,TCP 接收和发送缓冲区使用直接内存代替堆内存,通过内存池的方式循环利用 ByteBuf + +通过引用计数器及时申请释放不再引用的对象,降低了 GC 频率 + +使用单线程串行化的方式,高效的 Reactor 线程模型 + +大量使用了 volitale、使用了 CAS 和原子类、线程安全类的使用、读写锁的使用 + +### Netty 使用的线程模型 + +Netty 通过 Reactor 线程模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss 线程池和 work 线程池, + +其中 boss 线程池的线程负责处理请求的 accept 事件,当接收到 accept 事件的请求时,把对应的 socket 封装到一个, + +NioSocketChannel 中,并交给 work线程池,其中 work 线程池负责请求的 read 和 write 事件,由对应的 Handler 处理。 + +### Netty 的零拷贝实现 + +- Direct Buffer + + 使用堆外内存进行Socket读写,避免JVM堆内存与内核缓存之间的拷贝 + +- CompositeByteBuf + +​ 将多个缓存区"逻辑上"合并为一个缓冲区,避免物理上的数据拷贝 + +- 文件传输的零拷贝 FileRegion + +​ 利用操作系统的 `sendfile` 系统调用,直接在文件描述符和套接字之间传输数据,完全跳过用户态 + +- 缓存区包装 Wrapped Buffers + +​ 通过包装现有数据(如字节数组、`ByteBuffer`)创建 `ByteBuf`,避免数据贝,`Unpooled.wrappedBuffer(...)` 方法直接引用原始数据,而非复制 + +- 延迟缓冲区拷贝 Lazy Copy + +​ 在某些场景下(如缓冲区切片 `slice()`),仅记录原始缓冲区的引用和偏移量,不立即拷贝数据,直到必要时才执行拷贝。 \ No newline at end of file diff --git a/3.Java Lock.md b/3.Java Lock.md old mode 100755 new mode 100644 index a20ef6a..6d7e655 --- a/3.Java Lock.md +++ b/3.Java Lock.md @@ -17,7 +17,7 @@ 公平锁:根据线程请求锁的顺序来获取锁 非公平锁:抢占式获取锁 -### 什么是死锁 +### 什么是死锁,什么情况下产生死锁 具备以下4个条件就会产生死锁: @@ -125,9 +125,26 @@ yield: ### 什么是虚假唤醒?如何避免 -https://blog.csdn.net/LuckyBug007/article/details/70053669 +AB线程执行了wait()方法,C线程执行了notifyAll()方法唤醒了它们,AB线程就都开始执行,但其中只有一个线程能执行成功,另外一个线程会得到错误的结果。 -wait(),notify()源码分析:https://www.jianshu.com/p/f4454164c017 +避免方式是将wait()方法包裹在while(条件)中,进行循环判断 + +```java +synchronized (someObject) { + while (!condition) { + someObject.wait(); + } + // 现在 condition 为 true,执行你的操作 +} + +// 错误用法 +synchronized (someObject) { + if (!condition) { // 仅判断一次 + lock.wait(); // 若此处发生虚假唤醒,线程会直接执行下面的逻辑 + } + // 执行需要条件满足的操作(可能因条件未满足而出错) +} +``` ### Synchronized原理 @@ -138,7 +155,8 @@ Synchronized可以修饰普通方法、同步方法块、静态方法; 同步方法块锁是Synchonized配置的对象; 用的锁是存在对象头里的,根据mark word的锁状态来判断锁,如果锁只被同一个线程持有使用的是偏向锁,不同线程互相交替持有锁使用轻量级锁,多线程竞争使用重量级锁。锁会按偏向锁->轻量级锁->重量级锁 升级,称为锁膨胀 - https://github.com/farmerjohngit/myblog/issues/12 + +> 扩展:https://github.com/farmerjohngit/myblog/issues/12 ### synchronized和Lock的区别 @@ -146,7 +164,7 @@ Synchronized可以修饰普通方法、同步方法块、静态方法; 2. synchronized 无法显式的判断是否获取锁的状态,Lock可以判断是否获取到锁 3. synchronized 会自动释放锁,Lock需要在finally中手工释放锁 4. synchronized 不同线程获取锁只有一个线程能获取成功,其他线程会一直阻塞直到获取锁,Lock有阻塞锁,也有非阻塞锁,阻塞锁还有尝试设置,功能更强 -5. synchronized 可重入,不可中断,非公平,Lock锁可重入,可判断,有公平锁,非公平锁 +5. synchronized 可重入,不可中断,非公平,Lock锁可重入,可中断,有公平锁,非公平锁 6. Lock锁适合大量同步代码的同步问题,synchronized锁适合代码少量的同步问题 ### synchronized 可重入是怎么实现的 @@ -157,7 +175,7 @@ Synchronized可以修饰普通方法、同步方法块、静态方法; ### ReentrantLock可重入性怎么实现的? -由于ReentrantLock是通过AQS来实现的,其使用了AQS的state状态值来表示线程获取该锁的可重入次数,默认情况下state为0表示当前锁没有被任何线程持有,当一个线程获取该锁时会尝试使用CAS设置state值为1,如果CAS设置成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程,在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置2,这就是可以重入次数,在释放锁的时候,需要通过CAS将状态值减1,直到状态值为0,表示当前线程释放该锁 +由于ReentrantLock是通过AQS来实现的,其使用了AQS的state状态值来表示线程获取该锁的可重入次数,默认情况下state为0表示当前锁没有被任何线程持有,当一个线程获取该锁时会尝试使用**CAS设置state值为1**,如果CAS设置成功则当前线程获取了该锁,然后**记录该锁的持有者为当前线程**,在该线程没有释放锁的情况下第二次获取该锁后,**状态值被设置2**,这就是可以重入次数,在释放锁的时候,需要通过CAS将状态值减1,直到状态值为0,表示当前线程释放该锁 ### 非公平锁和公平锁在ReetrantLock里的实现过程是怎样的 @@ -178,15 +196,15 @@ Synchronized可以修饰普通方法、同步方法块、静态方法; ### AbstractQueuedSynchronizer的作用 -抽象同步队列简称AQS,是实现同步器的基础组件,并发包中的锁都是基于其实现的,关键是先进先出的队列,state状态,并且定义了 ConditionObject ,拥有两种线程模式,独占模式和共享模式 +抽象同步队列简称AQS,是实现同步器的基础组件,并发包中的锁都是基于其实现的,关键是**先进先出的队列,state状态**,并且定义了 ConditionObject ,拥有两种线程模式,**独占模式和共享模式** - AQS核心思想 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制使用CLH队列实现的,即将暂时获取不到锁的线程加入到队列中 -> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配, 并保持了上下节点,当前请求资源的线程 +> CLH(Craig,Landin,and Hagersten)(3个人名)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配, 并保持了上下节点,当前请求资源的线程 -AQS原理图 + @@ -197,3 +215,11 @@ StampedLock 提供了三种模式的读写控制,当调用获取锁的系列 悲观读锁readLock: 共享锁,在没有线程独占获取写锁的情况下,多个线程可以同时获取该锁,如果已经有其他线程持有写锁,则其他线程请求读锁会被阻塞 乐观读锁tryOptimisticRead: 在操作数据前并没有通过CAS设置锁的状态,仅通过位运算测试 +### CAS 实现的锁和synchronized 锁的性能差异 + +| 维度 | CAS(乐观锁) | synchronized(悲观锁,可升级) | +| ------------ | ------------------------------------ | -------------------------------------------- | +| **竞争低时** | 无锁,自旋少,性能最优 | 偏向锁 / 轻量级锁,开销接近但略高 | +| **竞争高时** | 自旋浪费 CPU,性能急剧下降 | 重量级锁阻塞线程,避免 CPU 空转,性能更稳定 | +| **资源开销** | 自旋消耗 CPU,无上下文切换 | 高竞争时上下文切换开销大,但无自旋浪费 | +| **适用场景** | 简单操作、低冲突(如原子类、计数器) | 复杂逻辑、高冲突(如多步更新、共享资源访问) | diff --git a/4.JVM.md b/4.JVM.md old mode 100755 new mode 100644 index 51baaa0..70ee478 --- a/4.JVM.md +++ b/4.JVM.md @@ -6,18 +6,24 @@ ### JVM运行时内存区域划分 -image-20190922235827314 +image-20190922235827314 -线程独享区域:程序计数器,本地方法栈,虚拟机栈 -线程共享区域:元空间(<=1.7方法区), 堆 +线程独享区域:程序计数器,本地方法栈,虚拟机栈 + +线程共享区域:元空间(<=1.7方法区), 堆 程序计数器:线程私有,是一块较小的内存空间,可以看做是当前线程执行的字节码指示器,也是唯一的没有定义OOM的区块 -本地方法栈: 用于执行Native 方法时使用 + +本地方法栈: 用于执行Native 方法时使用 虚拟机栈:用于存储局部变量,操作数栈,动态链接,方法出口等信息 + 元空间:存储已被虚拟机加载的类元信息,常量,静态变量,即时编译器编译后的代码等数据依旧存储在方法区中,方法区位于堆中 + 堆:存储对象实例 -示例: + + +**元空间OOM示例:** ```java /** @@ -47,21 +53,9 @@ public class JavaMetaSpaceOOM { } ``` - - -### OOM,及SOE的示例、原因,排查方法 +**虚拟机栈OOM** ```java -//OOM -Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError -public class OOMTest { - public static void main(String[] args) { - List objList = new ArrayList(); - while(true) { - objList.add(new Object()); - } - } -} - //SOE栈异常 -Xss125k public class SOETest() { static int count = 0; @@ -80,9 +74,55 @@ public class SOETest() { } ``` +**堆OOM** + +```java +//OOM -Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError +public class OOMTest { + public static void main(String[] args) { + List objList = new ArrayList(); + while(true) { + objList.add(new Object()); + } + } +} +``` + +**直接内存OOM** + +> 直接内存(Direct Memory)不属于 JVM 内存区域,是通过`sun.misc.Unsafe`分配的本地内存(如 NIO 的`DirectByteBuffer`),但可能因内存不足触发 OOM + +```java +// -XX:MaxDirectMemorySize=512k +public class DirectMemoryOOM { + private static final int BUFFER_SIZE = 1024 * 1024; // 1MB + public static void main(String[] args) { + List list = new ArrayList(); + while (true) { + ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE); + list.add(buffer); + } + } +} +``` + + + +### OOM,及SOE的示例、原因,排查方法 + - OOM排查:如果能看到日志,可以从打印的日志中获取到发送异常的代码行,再去代码中查找具体哪块的代码有问题。如果没有记录日志,通过设置的 -XX:+HeapDumpOnOutOfMemoryError 在发生OOM的时候生成.hprof文件,再导入JProfiler能够看到是由于哪个对象造成的OOM,再通过这个对象去代码中寻找 - SOE排查:栈的深度一般为1000-2000深度,超过了深度或者超过了栈大小就会导致SOE,通过打印的日志定位错误代码位置,检测是否有无限递归,发生了死循环等情况,修改代码 +### JMM 内存模型 + +Jave Memory Model(JMM)是一种抽象的规范,定义了多线程环境下线程与主内存之间的交互规则,目的是解决多线程并发时因共享内存访问导致的数据一致性问题,如原子性、可见性、有序性,核心是happens-before原则 + +原子性:指一个操作或一系列操作不可分割,实现方式:synchronized关键字、CAS操作 + +可见性:指当一个线程修改了共享变量的值后,其他线程能立即看到该修改,实现方式:volatile,synchronized、final(初始化后就不可修改) + +有序性:指程序执行的指令顺序与代码逻辑顺序一致,避免因编译器或CPU的指令重排序导致的并发问题,实现方式:volatile、synchronized + ### 如何判断对象可以回收或存活 判断是否可以回收,或者存活主要是看: @@ -106,60 +146,56 @@ public class SOETest() { ### 常见的JVM性能监测分析工具 - 1. jps - 能够查看正在运行的虚拟机进程,并显示虚拟机的执行主类及进程ID - - 2. jstat [option vmid [interval[s|ms] [count]] ] - 可以显示本地或者远程虚拟机中的类装载、内存、垃圾收集、JIT编译等运行数据 - - 3. jinfo - 实时查看和调整虚拟机的各项参数 - - 4. jmap - 生成堆转储快照 - - 5. jhat - - 生成页面分析导出的堆存储快照 +1. jps + 能够查看正在运行的虚拟机进程,并显示虚拟机的执行主类及进程ID - 6. jstack - 用于生成虚拟机当前时刻的线程快照 +2. jstat [option vmid [interval[s|ms] [count]] ] + 可以显示本地或者远程虚拟机中的类装载、内存、垃圾收集、JIT编译等运行数据 - 7. jstatd +3. jinfo + 实时查看和调整虚拟机的各项参数 - 启动RMI服务端程序,代理本地的Java进程,供远程计算机连接调式 +4. jmap + 生成堆转储快照 - 8. 查看当前JVM使用的垃圾收集器 +5. jhat + 生成页面分析导出的堆存储快照 - java -XX:+PrintFlagFinal -version 或者 java -XX:+PrintCommandLineFlags -version +6. jstack + 用于生成虚拟机当前时刻的线程快照 - 9. jconsole - - Java监视和管理控制台,能够监控内存,线程,类等 - - 10. jvisualvm - - 多合一监视工具 +7. jstatd + 启动RMI服务端程序,代理本地的Java进程,供远程计算机连接调式 + + 8. 查看当前JVM使用的垃圾收集器 + java -XX:+PrintFlagFinal -version 或者 java -XX:+PrintCommandLineFlags -version + + 9. jconsole + Java监视和管理控制台,能够监控内存,线程,类等 + + 10. jvisualvm + 多合一监视工具 > 更多资料请学习官网:https://docs.oracle.com/en/java/javase/11/tools/index.html ### JVM优化 - 1. 响应时间优先:年轻代设的大些,直到接近系统的最低响应时间限制。年轻代设大,可以减少到达年老代的对象。对于永久代的设置需要参考:永久代并发收集的次数、年轻代和永久代回收时间比例,调整达到一个合适的值 - 2. 吞吐量优先:年轻代设的大些,永久代较小 +1. 响应时间优先:年轻代设的大些,直到接近系统的最低响应时间限制。年轻代设大,可以减少到达年老代的对象。对于永久代的设置需要参考:永久代并发收集的次数、年轻代和永久代回收时间比例,调整达到一个合适的值 +2. 吞吐量优先:年轻代设的大些,永久代较小 ### 什么时候会触发FullGC 1. 永久代空间不足 2. 手动调用触发gc +3. 元空间不足 ### 类加载器有几种 - 1. Bootstrap ClassLoader - 负责加载JDK自带的rt.jar包中的类文件,它是所有类加载器的父加载器,Bootstrap ClassLoader没有任何父类加载器。 - 2. Extension ClassLoader负责加载Java的扩展类库,也就是从jre/lib/ext目录下或者java.ext.dirs系统属性指定的目录下加载类。 - 3. System ClassLoader负责从classpath环境变量中加载类文件,classpath环境变量通常由"-classpath" 或 "-cp" 命令行选项来定义,或是由 jar中 Mainfest文件的classpath属性指定,System ClassLoader是Extension ClassLoader的子加载器 - 4. 自定义加载器 +1. Bootstrap ClassLoader(C++实现) + 负责加载JDK自带的rt.jar包中的类文件,它是所有类加载器的父加载器,Bootstrap ClassLoader没有任何父类加载器。 +2. Extension ClassLoader(ExtClassLoader)负责加载Java的扩展类库,也就是从jre/lib/ext目录下或者java.ext.dirs系统属性指定的目录下加载类。 +3. System ClassLoader(AppClassLoader)负责从classpath环境变量中加载类文件,classpath环境变量通常由"-classpath" 或 "-cp" 命令行选项来定义,或是由 jar中 Mainfest文件的classpath属性指定,System ClassLoader是Extension ClassLoader的子加载器 +4. 自定义加载器 ### 什么是双亲委派模型?双亲委派模型的破坏 @@ -171,24 +207,24 @@ public class SOETest() { 类的生命周期一个有7个阶段:加载、验证、准备、解析、初始化、使用、卸载 - 加载: - 加载阶段,虚拟机需要完成以下3件事 + 加载阶段,虚拟机需要完成以下3件事 1. 通过类的全限定名来获取此类的二进制字节流 2. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构 3. 在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口 - - - 验证:分4个验证 - 1. 文件格式验证,验证是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理 - - 2. 元数据验证,对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求 - - 3. 字节码验证,通过数据流和控制流分析,确定程序语义是否合法、符合逻辑 - - 4. 符合引用验证,是对类自身以外的信息进行匹配性校验(常量池中各种符合引用) + + 1. 文件格式验证,验证是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理 + + 2. 元数据验证,对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求 + + 3. 字节码验证,通过数据流和控制流分析,确定程序语义是否合法、符合逻辑 + + 4. 符合引用验证,是对类自身以外的信息进行匹配性校验(常量池中各种符合引用) + + - - 准备:正式为类变量分配内存并设置初始值的阶段,这里设置初始值是数据类型的默认值 - 解析:虚拟机将常量池中的符号引用替换为直接引用的过程 - 初始化:执行类构造器的过程 @@ -203,52 +239,126 @@ public class SOETest() { ### 编译器会对指令做哪些优化? 编译器优化分编译期和运行期 -- 编译期: - 1.标注检查,检查变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配,对常量进行折叠 - 2.数据及控制流分析,检查诸如程序局部变量在使用前是否有赋值、是否所有的受检异常都被正确处理等问题 - 3.将语法糖还原为基础的语法结构 - 4.生成字节码 -- 运行期: - 即时编译器JIT会把运行频繁的代码编译成与本地平台相关的机器码,并进行各种层次的优化 - Client Compiler: 会进行局部性的优化,分三阶段:第一阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示HIR,HIR使用静态单分配(SSA)的形式来代表代码值,在字节码上做方法内联,常量传播等基础优化;第二阶段,从HIR中产生低级中间代码,在这之前会做空值检查消除,范围检查消除等。第三阶段,在LIR上分配寄存器,并在LIR上做窥孔优化,最后产生机器码 - Server Compiler: 会执行无用代码消除、循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排序、范围检测消除、空值检查消除,另外还能根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的激进优化,比如守护内联、分支频率预测等 + +- 编译期: + 1.标注检查,检查变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配,对常量进行折叠 + 2.数据及控制流分析,检查诸如程序局部变量在使用前是否有赋值、是否所有的受检异常都被正确处理等问题 + 3.将语法糖还原为基础的语法结构 + 4.生成字节码 + +- 运行期: + 即时编译器JIT会把运行频繁的代码编译成与本地平台相关的机器码,并进行各种层次的优化 + Client Compiler: 会进行局部性的优化,分三阶段:第一阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示HIR,HIR使用静态单分配(SSA)的形式来代表代码值,在字节码上做方法内联,常量传播等基础优化;第二阶段,从HIR中产生低级中间代码,在这之前会做空值检查消除,范围检查消除等。第三阶段,在LIR上分配寄存器,并在LIR上做窥孔优化,最后产生机器码 + Server Compiler: 会执行无用代码消除、循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排序、范围检测消除、空值检查消除,另外还能根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的激进优化,比如守护内联、分支频率预测等 - 几种经典的优化技术: 1. 公共子表达式消除 - 如果一个表达式E已经计算过,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为公共子表达式 + 如果一个表达式E已经计算过,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为公共子表达式 + 2. 数组范围检查消除 - 编译期就判断数组是否在合理的范围内,如果在,那就可以在循环中把数组的上下界检查消除。另外还有隐式异常处理,虚拟机会注册一个Segment Fault信号的异常处理器,但如果代码经常为空,消耗时间比判空慢,但虚拟机会根据运行期收集到的信息选择使用判空还是隐式异常处理 -3. 方法内联 - 一可以给"公共子表达式消除"等其他优化技术提供基础。虚方法即多态情况下,如果要确定是否能内联,虚拟机需要向"类型继承关系分析"器查询,如果只有一个版本,那可以进行内联,但还会留一个"逃生门",这种内联称为"守护内联",如果继承关系发生变化,虚拟机会通过"逃生门"退回到解释状态执行,或重新编译。 - 如果是多版本方法,虚拟机会通过"内联缓存",在第一次调用的时候将目标方法版本缓存起来,下次调用的时候检查版本是否一致,如果不一致就会取消内联,查找虚拟方法表进行方法分派 -4. 逃逸分析 - 分析对象动态作用域,对象是否作为调用参数传递到其他方法中(方法逃逸),是否有被其他线程访问(线程逃逸)。如果没有以上情况,虚拟机会做一些高效优化:栈上分配、同步消除(去掉同步措施)、标量替换(将对象成员变量恢复到原始类型) + 编译期就判断数组是否在合理的范围内,如果在,那就可以在循环中把数组的上下界检查消除。另外还有隐式异常处理,虚拟机会注册一个Segment Fault信号的异常处理器,但如果代码经常为空,消耗时间比判空慢,但虚拟机会根据运行期收集到的信息选择使用判空还是隐式异常处理 +3. 方法内联 + 就是把目标方法的代码复制到发起调用的方法之中,避免发生真是的方法调用 +4. 逃逸分析 + 分析对象动态作用域,对象是否作为调用参数传递到其他方法中(方法逃逸),是否有被其他线程访问(线程逃逸)。**如果没有以上情况,虚拟机会做一些高效优化:栈上分配、同步消除(去掉同步措施)、标量替换(将对象成员变量恢复到原始类型)** + + ```java + public void test() { + //obj 对象只有在这个方法内部使用,并没有做为参数传递到其他方法中 + Object obj = new Object(); + } + ``` + +5. 锁消除 + JIT检测到方法是在单线程环境中执行时,会将已有的锁消除,提高性能 + +6. 标量替换 + 将原本需要分配到堆上的对象拆解成若干个基础数据类型(标量),并将这些基础数据类型作为局部变量存储在栈上。标量通常指的是不可再分解的数据类型,如int、long、float、double以及引用类型(reference)等。这种优化技术特别适用于那些生命周期短、作用域小、且不会逃逸出当前方法或线程的对象 + + ### Serial、Parallel、CMS、G1收集器特点 -image-20191008191348602 +image-20191008191348602 -img +img -image-20191008202251538 +image-20191008202251538 - Serial - - 单线程收集器,在进行垃圾收集时,必须暂停所有的工作线程直到结束,该收集器停顿时间长,-XX:+UseSerialGC 使用串行垃圾收集器 + 单线程收集器,在进行垃圾收集时,必须暂停所有的工作线程直到结束,该收集器停顿时间长,-XX:+UseSerialGC 年轻代使用串行垃圾收集器,-XX:+UseSerialOldGC 老年代使用串行垃圾收集器 - Parallel + 采用多线程来扫描并压缩堆,停顿时间短,回收效率高,-XX:+UseParNewGC 使用并发标记扫描垃圾回收器;能够提高应用吞吐率 + +> JDK8 默认使用:年轻代UseParallelGC,老年代UseParallelOldGC + +- CMS 基于"标记-清除"算法,一共分初始标记、并发标记、重新标记、并发清除,并发重置5个阶段;能够降低STW,提高用户体验 + 在初始标记、重新标记阶段需要STW,并且CMS收集器占用CPU资源较多,无法处理浮动垃圾 + 并发重置阶段重新初始化CMS数据结构和数据,为下次垃圾回收做准备 + 在并发标记和并发清理阶段可能会出现垃圾回收还没执行完,垃圾回收又被触发的情况,此时会发生"concurrent mode failure",垃圾收集器进入STW,用serial old 进行回收。 + +> (-XX:CMSInitiatingOccupancyFraction 调整老年代占用多少触发回收;-XX:+UseCMSCompactAtFullCollection 默认开启,在即将触发FullGC前对内存碎片进行整理;-XX:CMSFullGCsBeforeCompaction设置执行多少次不压缩的FullGC后,来一次带压缩的的碎片整理) - 采用多线程来扫描并压缩堆,停顿时间短,回收效率高,-XX:+UseParNewGC 使用并发标记扫描垃圾回收器 +- G1 可以跟用户程序并发进行垃圾收集;分代收集,将堆划分成多个大小相等的独立Region区域;空间整合,默认就会进行内存整理;可预测的停顿,G1跟踪各个Region的回收获得的空间大小和回收所需要的经验值,维护一个优先列表; + + -- CMS 基于"标记-清除"算法,一共分初始标记、并发标记、重新标记、并发清除,并发重置5个阶段 +### G1 垃圾收集分类 - 在初始标记、重新标记阶段需要STW,并且CMS收集器占用CPU资源较多,无法处理浮动垃圾 +**YoungGC:** - 并发重置阶段重新初始化CMS数据结构和数据,为下次垃圾回收做准备 +YoungGC并不是在现有的Eden区放满了就马上触发,G1会计算现有的Eden区回收大概要多久时间,如果回收时间远小于参数-XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数-XX:MaxGCPauseMills设定的值,就会触发Young GC - (-XX:CMSInitiatingOccupancyFraction 调整老年代占用多少触发回收;-XX:+UseCMSCompactAtFullCollection 默认开启,在即将触发FullGC前对内存碎片进行整理;-XX:CMSFullGCsBeforeCompaction设置执行多少次不压缩的FullGC后,来一次带压缩的的碎片整理) +**MixedGC** -- G1 可以跟用户程序并发进行垃圾收集;分代收集,将堆划分成多个大小相等的独立Region区域;空间整合,默认就会进行内存整理;可预测的停顿,G1跟踪各个Region的回收获得的空间大小和回收所需要的经验值,维护一个优先列表; +不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中的存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC + +**Full GC** + +停止系统程序,然后采用单线程进行标记、清理、和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程非常耗时。(Shenandoah已经优化成多线程收集,Shenandoah可以认为是G1的升级版本) + + + +### ZGC + +ZGC 是JDK11 中加入的低延迟垃圾收集器,JDK15正式发布,支持16TB级别的堆,停顿时间不超过1ms,没有采用分代算法,清理过程大致分为:并发标记、并发预备重分配、并发重分配、并发重映射。 + +ZGC 堆空间分页模型(**无分代**):小页面、中页面、大页面 + +**一次ZGC流程:** + +标记阶段:初始标记、并发标记、再标记,初始标记和再标记会STW + +转移阶段:并发转移准备、初始转移、并发转移 + +**ZGC常见触发时机:** + +- 基于分配速率的自适应算法(主要):ZAllocationSpikeTolerance控制 +- 基于固定时间间隔:ZCollectionInterval参数控制 +- 主动触发规则:Zproactive控制 +- 启动预热:关键词warmup + +> 开启:-XX:+UseZGC + +### 什么是逃逸分析技术 + +是JVM的一种高级优化技术,用于分析对象的动态作用域,从而决定是否可以在栈上分配对象而不是堆上。 + +逃逸分析的主要优化: + +1. 栈上分配 + +对于未逃逸的对象,直接在栈帧中分配内存。适用于小对象,大对象还是要在堆上分配 + +优点:对象随方法调用结束自动销毁,减少GC压力;分配速度比堆上分配快。 + +2. 标量替换 + +将对象拆解为基本类型变量分配在栈上 + +3. 同步消除 +对于不会线程逃逸的对象,移除不必要的同步操作 diff --git a/5.Java Reflect_IO.md b/5.Java Reflect_IO.md old mode 100755 new mode 100644 index ca58eed..6ffb3c7 --- a/5.Java Reflect_IO.md +++ b/5.Java Reflect_IO.md @@ -23,7 +23,7 @@ Class clazz = Class.forName("类的全路径"); 1.使用Class对象的newInstance(),这种方法需要Class对象对应的类有默认的空构造器 2.调用Constructor对象的newInstance(),先通过Class对象获取构造器对象,再通过构造器对象的newInstance()创建 - +> 扩展:[【读码JDK】-带你详细了解lang.Class类(一)_java.lang.class类-CSDN博客](https://itsaysay.blog.csdn.net/article/details/125228566) ### 请说明如何通过反射获取和设置对象私有字段的值? @@ -36,8 +36,6 @@ BIO(Block IO): jkd1.4以前的IO模型,它是一种阻塞IO NIO(NoN-Block IO):JDK1.4以后才有的IO模型,提高了程序的性能,借鉴比较先进的设计思想,linux多路复用技术,轮询机制 AIO(Asynchronous IO):JDK1.7以后才有的IO模型,相当于NIO2,相当于NIO2,学习Linux epoll模式 -linux epoll介绍:https://mp.weixin.qq.com/s/YUMmIjJzhudrKb8WHcJ49Q - ### Java NIO的原理 1.多路复用技术:建立连接—发送数据—服务端处理—反馈 2.轮询机制(Select模式) @@ -82,4 +80,12 @@ try(ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) { } catch (Exception e) { e.printStackTrace(); } -``` \ No newline at end of file +``` + +### 什么是AIO + +AIO 是基于Proactor模型的,就是异步非阻塞模型。 + +每个连接发送过来的请求,都会绑定一个buffer,然后通知操作系统去异步完成读,此时你的程序是会去干别的事儿的,等操作系统完成数据读取之后,就会回调你的接口,给你操作系统异步读完的数据。 + +然后你对这个数据处理一下,接着将结果往回写。写的时候也给操作系统一个buffer,让操作系统自己获取数据去完成写操作,写完以后再回来通知你。 \ No newline at end of file diff --git a/6.Design Pattern.md b/6.Design Pattern.md old mode 100755 new mode 100644 index 9411b32..0ea0ac3 --- a/6.Design Pattern.md +++ b/6.Design Pattern.md @@ -8,7 +8,7 @@ > > https://www.runoob.com/design-pattern/design-pattern-tutorial.html -## 23种设计模式 +## 创建型设计模式 ### 抽象工厂模式 @@ -32,6 +32,8 @@ [「单例模式示例代码」](#单例模式代码) +## 结构型设计模式 + ### 适配器模式 是一种结构型设计模式,它能使接口不兼容的对象能够互相合作 @@ -101,6 +103,8 @@ [「代理模式示例代码」](#代理模式示例代码) +## 行为型设计模式 + ### 责任链模式 是一种行为型设计模式,允许你将请求沿着处理者链进行发送。收到请求后,每个处理者均可对请求进行处理,或将其传递给链上的下一个处理者 @@ -109,6 +113,10 @@ 是一种行为设计模式,它可将请求转换为一个包含在请求相关的所有信息的独立对象 +### 解释器模式 + +给定一种语言,定义它的文法表示,并定义一个解释器,该解释器根据文法表示来解释语言中的句子 + ### 迭代器模式 是一种行为设计模式,让你能在不暴露集合低层表现形式(列表、栈、树等)的情况下遍历集合中的所有元素 diff --git a/7.Data Structure.md b/7.Data Structure.md old mode 100755 new mode 100644 diff --git a/8.DataBase.md b/8.DataBase.md old mode 100755 new mode 100644 index 02c7b65..ba25b4b --- a/8.DataBase.md +++ b/8.DataBase.md @@ -4,9 +4,7 @@ ### 数据库三大范式、反模式 -1. 强调属性的原子性约束,要求属性具有原子性,不可再分解 -2. 强调记录的唯一性约束,表必须有一个主键,并且没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分 -3. 强调属性冗余性的约束,即非主键列必须直接依赖于主键 +![image-20250714080228920](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/image-20250714080228920.png) 反模式:如果完全按照三大范式来设计表结构,会导致业务涉及表增多,查询数据需要多表联合查询,导致sql复杂,性能变差,不利于维护,也不利于分库分表,比如会在表中冗余存储城市id对应的城市名称 @@ -136,6 +134,10 @@ B+Tree索引: 设置系统当前隔离级别:set global transaction isolation level repeatable read; +### 什么是回表,如何解决 + +"回表" 是指:通过非聚簇索引查询时,因索引未包含所需全部列,需用查到的主键到聚簇索引中再次查询完整数据的过程。解决核心是 **避免回表**,即利用 **覆盖索引**,覆盖索引是指 **索引包含查询所需的列**。 + ### 什么是MVCC, MySQL的MVCC原理 MVCC即多版本并发控制,它能在很多情况下避免加锁操作,降低开销,不同的存储引擎实现方式不同,有乐观并发控制和悲观并发控制 @@ -224,3 +226,18 @@ select * from a left join b where 条件 只返回where中匹配的数据 https://www.cnblogs.com/caowenhao/p/8003846.html +### Mysql 主从同步延迟问题 + +主从同步延迟产生的问题:插入新数据后,立马查询会查不到数据 + +1. 主从同步开启**并行复制** +2. 调整代码,不要插入后,先查询,再更新,如果要更新,插入后直接更新 +3. 拆库,降低库的并发量,在并发量小的时候(500/s),延迟可以忽略不计 +4. 这个查询操作直连主库 +4. 二次查询,从库查不到,从主库查询 +4. 容忍短暂的数据不一致 + + + + + diff --git a/9.Redis.md b/9.Redis.md old mode 100755 new mode 100644 index 50358ce..dca4777 --- a/9.Redis.md +++ b/9.Redis.md @@ -38,7 +38,11 @@ Redis 3.0 版本支持的策略 ### Redis 持久化机制 1.RDB持久化: 把当前进程数据生成快照保存到硬盘的过程,触发方式有手动触发和自动触发 + +redis-cli 进入redis命令行: + 手动触发命令:save和bgsave命令 + - save命令:阻塞当前Redis服务,直到RDB过程完成为止,不建议线上环境使用 - bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束,阻塞只发生在fork阶段,一般时间很短 自动触发: @@ -106,7 +110,7 @@ AOF文件错误,可以通过redis-check-aof-fix 修复 ### Redis 数据类型 > 字符串 -> key是字符串类型,字符串类型的值可以是字符串、数字、二进制(最大不能超过512MB),是动态字符串,内部通过预分配冗余空间的方式来减少内存的频繁分配 +> key是字符串类型,字符串类型的值可以是字符串、数字、二进制(最大不能超过512MB),是动态字符串,内部通过预分配冗余空间的方式来减少内存的频繁分配,内部使用简单动态字符串(SDS) | 命令 | 解释 | 备注 | | ----------- | ------------------ | ------------------------------------------------------------ | @@ -125,7 +129,7 @@ AOF文件错误,可以通过redis-check-aof-fix 修复 | getrange | 获取部分字符串 | key start end | > 哈希 -> 一个键值对结构, 内部结构同Java的 HashMap, 数组+链表的结构,值只能存储字符串,编码是ziplist或者hashtable。另外在rehash的时候,采用定时任务渐进式迁移内容 +> 一个键值对结构, 内部结构同Java的 HashMap, 数组+链表的结构,值只能存储字符串,编码是ziplist或者hashtable。另外在rehash的时候,采用定时任务渐进式迁移内容,根据哈希表的元素数量和大小自动切换,数量少且键值对都小时用压缩列表,否则切换为哈希表 | 命令 | 解释 | 备注 | | -------------------- | ------------------------ | ----------------------------------------------- | @@ -144,7 +148,7 @@ AOF文件错误,可以通过redis-check-aof-fix 修复 | hstrlen | 计算value字符串长度 | key field | > 列表 -> 用来存储多个有序的字符串,一个列表最多可以存储232 - 1 个元素,列表中的元素可以是重复的,相当于Java中的LinkedList, 是一个链表而不是数组, 底层是采用quicklist结构,在数据量少的时候会使用ziplist压缩列表,数据量多的时候才使用quicklist +> 用来存储多个有序的字符串,一个列表最多可以存储232 - 1 个元素,列表中的元素可以是重复的,相当于Java中的LinkedList, 是一个链表而不是数组, 底层是采用quicklist结构(3.2+默认),在数据量少的时候会使用ziplist压缩列表,数据量多的时候才使用quicklist | 命令 | 解释 | 备注 | | --------------- | -------------------------- | ------------------------------------------------------------ | @@ -210,7 +214,7 @@ AOF文件错误,可以通过redis-check-aof-fix 修复 > 查询编码:object encoding key -![e5c2bf73](https://tva1.sinaimg.cn/large/006y8mN6ly1g6lnmw7glhj31240u0aou.jpg) +![e5c2bf73](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/68747470733a2f2f747661312e73696e61696d672e636e2f6c617267652f30303679386d4e366c793167366c6e6d7737676c686a33313234307530616f752e6a7067.jpeg) > 键相关命令 @@ -285,12 +289,12 @@ object encoding key 查询key的内部编码 Pipeline(流水线) 机制能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按照顺序返回给客户端 原生批量命令与Pipeline对比: -* 原生批量命令是原子的,Pipeline是非原子的 +* **原生批量命令是原子的,Pipeline是非原子的** * 原生批量命令是一个命令对应多个key,Pipeline支持多个命令 * 原生批量命令是Redis服务端支持实现的,而Pipeline需要服务端和客户端共同实现 ### 事务 -redis 简单事务功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi命令代表事务开始,exec命令代表事务结束,他们之间的命令是原子顺序执行的。如果要停止事务的执行,用discard命令代替exec命令 +redis 简单事务功能,将一组需要一起执行的命令放到**multi和exec**两个命令之间。multi命令代表事务开始,exec命令代表事务结束,他们之间的命令是**原子顺序**执行的。如果要停止事务的执行,用discard命令代替exec命令 watch 命令 用来确保事务中的key没有被其他客户端修改过,才执行事务,否则不执行(类似乐观锁) @@ -316,7 +320,7 @@ evalsha 脚本sha1值 key个数 key列表 参数列表 > config set slowlog-max-len - 获取慢查询日志: 慢查询只记录命令执行时间,不包括命令排队和网络传输时间 - slowlog get [n] n指定条数 + slowlog get [n] n指定条数 - 日志数据结构 > 3 1) (integer) 3 (id) @@ -329,7 +333,7 @@ evalsha 脚本sha1值 key个数 key列表 参数列表 > slowlog reset ### Redis键过期删除策略 -键过期,内部保存在过期字典expires中,Redis采用惰性删除和定时任务删除机制; +键过期,内部保存在过期字典expires中,Redis采用**惰性删除**和**定时任务删除**机制; 惰性删除用于在客户端读取带有超时属性的键时,如果已经超过设置的过期时间,会执行删除并返回空,但这种方式如果键过期,而且一直没有被重新访问,键一直存在 定时任务删除:能够解决惰性删除问题,Redis内部维护一个定时任务,每秒运行10次。根据键的过期比例,使用快慢两种速率模式回收键,缺点是占用CPU时间,在过期键多的时候会影响服务器的响应时间和吞吐量 @@ -342,20 +346,20 @@ AOF写入的时候,如果某个键过期,会向AOF追加一条DEL命令;AO 复制的时候,主服务器发送删除通知,从服务器接到删除通知时才删除过期键 ### Redis高可用方案 -1. 主从模式:一主二从 +1. 主从模式:一主二从(达到支持10万+并发) 配置redis.conf , 从节点配置 slaveof 127.0.0.1 6379 确认主从关系: redis-cli -h 127.0.0.1 -p 6379 info replication - 2. 哨兵模式: 配置 redis-sentinel.conf ,sentinel monitor mymaster 127.0.0.1 6379 1 最后一位是选举master需要的票数 启动哨兵: redis-sentinel redis-sentinel.conf 或者 redis-server redis-sentinel.conf --sentinel - 3. redis-cluster: - 每个节点保存数据和整个集群状态,每个节点都和其他节点连接。采用哈希函数把数据映射到一个固定范围的整数集合中,整数定义为槽。所有键根据哈希函数映射到0~16383整数槽内,公式 slot = CRC16(key) & 16383 - + 每个节点保存数据和整个集群状态,每个节点都和其他节点连接。采用哈希函数把数据映射到一个固定范围的整数集合中,整数定义为槽。所有键根据哈希函数映射到0~16383整数槽内,公式 slot = CRC16(key) & 16383 + 修改redis.conf,开启集群模式:cluster-enabled yes,集群内部配置文件路径:cluster-config-file "nodes-6379.conf" + 启动节点 + 使用meet把节点加入集群中:cluster meet 127.0.0.1 6381 4. Codis https://juejin.im/post/5c132b076fb9a04a08218eef @@ -373,19 +377,55 @@ redis-sentinel redis-sentinel.conf 2. 从节点内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接 3. 连接建立成功后,从节点发送ping请求进行首次通信,目的是检测主从之间网络套接字是否可用,主节点当前是否接受处理命令 4. 如果主节点配置了密码验证,则从节点必须要配置相同的密码才能通过验证,进行复制同步 - 5. 通过验证后,主从可正常通信了,主节点会把数据持续发给从节点,同步方式有全量同步和部分同步,刚建立建立的时候,会进行全量同步,同步结束后,进行部分同步 + 5. 通过验证后,主从可正常通信了,主节点会把数据持续发给从节点,**同步方式有全量同步和部分同步**,刚建立建立的时候,会进行全量同步,同步结束后,进行部分同步 6. 当主节点与从节点同步完当前的数据后,主节点会把后续新增的命令持续发送给从节点进行同步 - 哨兵模式 最小配置 1主 2从 3哨兵,3个哨兵能监控每个master和salve +### 什么是脑裂 + +脑裂指的是在分布式系统中,由于网络分区(network partition)导致集群被分割成多个独立的部分,每个部分都认为自己是唯一存活的,从而导致数据不一致的情况。 + +Redis 中的脑裂表现为: + +1. 主从集群被分割成两部分 +2. 两部分都认为自己是主节点(或包含主节点) +3. 客户端可能连接到不同的部分,看到不同的数据 + +导致脑裂的原因: + +1. **网络问题**:主节点和从节点/哨兵之间的网络连接中断 +2. **资源问题**:主节点负载过高导致无法及时响应心跳 +3. **配置问题**:哨兵或集群配置不当 + +### 解决异步复制和脑裂导致的数据丢失 + +min-slaves-to-write 1 + +min-slaves-max-lag 10 + +要求至少有1个slave,数据复制和同步的延迟不能超过10秒 + +如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒,那么master就不会再接收任何请求了 + +这两个配置可以减少异步复制和脑裂导致的数据丢失 + +(1)减少异步复制的数据丢失 + +有了min-slaves-max-lag 这个配置,就是说一旦slave复制数据和ack延迟太长,就认为master可能宕机后损失数据太多,就拒绝写入,使得同步造成的数据丢失降到可控范围 + +(2)减少脑裂的数据丢失 + +如果一个master 出现了脑裂,跟其他slave丢了连接,那么上面的配置可以确保,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求,这样脑裂后的旧master不会接受client的新数据,也就避免了数据丢失 + ### 缓存问题 - 缓存穿透 - 缓存穿透是指,缓存中不存在该key的数据,于是就是去数据库中查询,数据库也不存在该数据,导致循环查询数据 + 缓存穿透是指,**缓存中不存在该key的数据**,于是就是去数据库中查询,**数据库也不存在该数据**,导致循环查询数 优化: - 1. 缓存空对象 + 1. 缓存空对象 对于不存在的数据,依旧将空值缓存起来。但这会造成内存空间的浪费,可以针对这类数据加一个过期时间。对于缓存和存储层数据的一致性,可以在过期的时候,请求存储层,或者通过消息系统更新缓存 @@ -401,7 +441,7 @@ redis-sentinel redis-sentinel.conf - 缓存击穿 - 缓存击穿指,某key突然变成了热点key,大量请求到该key,但key刚好又失效,导致从数据库中去查询数据 + 缓存击穿指,**某key突然变成了热点key,大量请求到该key,但key刚好又失效,导致从数据库中去查询数据** 优化: @@ -409,14 +449,23 @@ redis-sentinel redis-sentinel.conf - 缓存雪崩 - 指缓存由于大量请求,造成缓存挂掉,大量请求直接打到存储层,造成存储层挂机 + 指缓存由于**大量请求**,同时**缓存又大量失效**,导致大量请求直接打到存储层,造成存储层压力过大或者挂机 优化: + 事前 + 1. 使用主从,哨兵,集群模式保证缓存高可用 - 2. 依赖隔离组件为后端限流并降级 3. 提前演练,做好后备方案 + 事中: + + ​ 使用限流组件,限流并降级 + + 事后: + + ​ redis 持久化,重启后恢复数据 + - 缓存更新方式 同步更新,先写入数据库,写入成功后,再更新缓存。 @@ -448,10 +497,6 @@ PS: Redis锁可能会在业务逻辑还没执行完的时候就已经超时释 ZK锁 通过在服务端新建一个临时有序节点,哪个客户端成功创建了第一个临时有序节点,就代表该客户端获得了锁,后面节点的客户端会处于监听状态,当释放锁的时候,服务端就会删除第一个临时节点,此时第二个临时节点能监听到上一个节点的释放事件,这样第二个节点就变成第一个节点,此时客户端2就代表获得了锁。如果客户端的会话关闭,临时节点会被删除,也就释放了锁 -[《三种分布式锁的优缺点及解决方案》](https://mp.weixin.qq.com/s?__biz=MzA4NjA3MTAyMQ==&mid=2649219741&idx=1&sn=e543b333306af62d1ccb8753c3935f61&chksm=87dd011fb0aa8809ade0cb675f6963d606990aab2ac460ff3b27fb8fedb63a7701370518fee6&token=308357506&lang=zh_CN#rd) - - - ### 在某个时间段,redis某个key变成了热点key,此时请求又都打到了一台slave上,请问该怎么办? 由于之前没有做热点Key监控,不能进行对热点key 进行本地缓存,也没有预料到该key会变成热点key,但现在该key变成了热点key, 此时的办法可以新开redis实例,在新的实例上新建该热点key,将后续的请求分散到其他的实例上。 @@ -462,3 +507,89 @@ ZK锁 通过在服务端新建一个临时有序节点,哪个客户端成功 [《阿里云redis热点key问题的发现与解决》](https://blog.csdn.net/zhanglf88/article/details/103079306) +### Redis 如何排查大key + +**使用`--bigkeys`命令** + +- 这是Redis自带的一个命令,用于对整个Key空间进行扫描,统计string、list、set、zset、hash等常见数据类型中每种类型里的最大的key。 +- 使用方法:`redis-cli -h -p -a --bigkeys` +- 注意:这个命令是以scan延迟计算的方式扫描所有key,因此执行过程中不会阻塞Redis,但当实例存在大量的keys时,命令执行的时间会很长。建议在slave上扫描。 + +**使用`MEMORY USAGE`命令(仅支持Redis 4.0以后的版本)** + +- 这个方法可以查看指定key的内存使用情况。 +- 使用方法:`redis-cli -h -p -a MEMORY USAGE ` +- 该命令会返回key的内存使用情况(以字节为单位)。 + +**使用Rdbtools工具包** + +- Rdbtools是一个第三方开源工具,用于解析Redis的快照文件(rdb文件)。 +- 通过这个工具,可以分析rdb文件中的内容,包括各个key的大小。 + +### Redisson 如何实现分布式锁 + +Redisson 的分布式锁实现(`RLock`)基于Redis的Lua脚本和发布/订阅机制 + +```java +RLock lock = redisson.getLock("myLock"); +lock.lock(); +try { + // 业务代码 +} finally { + lock.unlock(); +} +``` + +**实现原理:** + +1. **加锁机制**: + - 使用Lua脚本保证原子性:`SET lock_name uuid NX PX 30000` + - 如果锁已被占用,通过发布/订阅机制等待锁释放通知 + - 支持可重入:通过计数器记录重入次数 +2. **看门狗机制**: + - 默认加锁时间30秒 + - 后台线程每10秒检查一次,如果线程还持有锁则延长锁时间 +3. **解锁机制**: + - 使用Lua脚本保证原子性操作 + - 发布锁释放消息通知其他等待客户端 + +### Redisson 如何实现延迟队列 + +Redisson 通过 `RDelayedQueue` 实现延迟队列: + +```java +RQueue queue = redisson.getQueue("myQueue"); +RDelayedQueue delayedQueue = redisson.getDelayedQueue(queue); +// 延迟10秒放入队列 +delayedQueue.offer("msg1", 10, TimeUnit.SECONDS); + +// 在其他地方消费 +String msg = queue.poll(); +``` + +**实现原理**: + +1. 使用Redis的ZSET结构存储延迟消息,score为执行时间戳 +2. 后台线程定期检查到期消息 +3. 到期后从ZSET移除并放入目标队列 + +### Redisson 如何实现限流器 + +Redisson 提供 `RRateLimiter` 实现分布式限流: + +```java +RRateLimiter limiter = redisson.getRateLimiter("myLimiter"); +// 每秒10个请求 +limiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS); + +if (limiter.tryAcquire()) { + // 通过限流 +} +``` + +**实现原理**: + +1. 使用Redis的Lua脚本保证原子性 +2. 基于令牌桶算法实现 +3. 支持全局(OVERALL)和单客户端(PER_CLIENT)两种模式 + diff --git "a/Bug345円210円206円344円272円253円/1.Lock Transactional.md" "b/Bug345円210円206円344円272円253円/1.Lock Transactional.md" new file mode 100644 index 0000000..be68d6f --- /dev/null +++ "b/Bug345円210円206円344円272円253円/1.Lock Transactional.md" @@ -0,0 +1,157 @@ +# 不当使用Redis锁产生的问题 + +# 前言 + +春节放假期间,一个项目上的积分接口被刷,而且不止一个人在刷,并且东西也被兑走,放假晚上被人叫起来排查问题,通过这个人的积分明细观察,基本一秒就能获取一次,远远超过了积分规则限定的次数,这肯定是用脚本了,虽然后期联系死活说自己是正常途径获取。由于是业主,我们还是决定自己来承担这个损失,被项目方从合同中扣除奖品费用1万余元。 + +# 问题原因 + +先说下接口的逻辑层次结构: + +--controller 积分获取接口,用PointController表示 + +--service 积分获取接口service,用PointService表示 + +我用伪代码来表示整个调用逻辑: + +*PointController* + +```java +@RestController +public class PointController { + + @Resource + private PointService pointService; + + @PostMapping(/addPoint) + public Response addPoint() { + //分布式锁,使用redis的NX命令 + RedisDistributedLock lock = new RedisDistributedLock(); + //创建一个3s过期,100ms休眠的锁 + if(lock.lock("POINT_KEY", 3000L, 100L)) { + try { + //调用 + pointService.addPoint(); + } catch (Exception ex) { + e.printStackTrace; + } finally { + //解锁 + lock.unlock("POINT_KEY"); + } + } + return Response.ok(getLastPoint()); + } +} +``` + +1. 创建一个分布式锁对象,该分布式锁使用redis的`NX`命令实现 +2. 随后创建一个3s过期的分布式锁,以便锁住该新增积分的请求 +3. 最后在新增积分执行完后,在finally中释放锁 +4. 最后返回该用户的最终积分 + +*PointService* + +```java +public class PointService { + + @Transactional(rollbackFor = Exception.class) + public void addPoint() { + //查询积分规则 + PointRule pointRule = getPointRule(); + //查询用户该积分项的积分获取记录总数 + Integer total = getPointRecords(); + //判断该用户的积分记录总数是否大于 积分规则限定的次数 + //大于则不处理,返回 + if(total - pointRule.getRuleTimes>= 0) { + return; + } + + //生成积分记录 + int insert = insertPointRecords(); + //更新用户总积分 + if(insert> 0) { + updateUserPoint(); + } + } +} +``` + +PointService 中的添加积分逻辑: + +1. 首先查询该项目积分规则,查询用户该积分项的积分获取记录总数 +2. 判断该用户的积分记录总数是否大于 积分规则限定的次数,大于则不处理,返回 +3. 生成积分记录 +4. 更新用户总积分 + + + +该添加积分的逻辑整体上看好像没什么问题,也确实在一切正常的情况下运行是不会有问题的。 + +如果PointService 中的添加积分逻辑在分布式锁有效期3s内执行完,是不会有问题的。 + +但如果PointService中的添加积分逻辑超过3s...那是不是后续请求又可以获取锁了,这也正是这次事故的原因。 + + + +`因为PointService中的添加积分逻辑超过了3s,并且上一个请求的事务还未提交,后续请求已经获取锁进入PointService,在查询积分记录后,判断还是满足规则,继续执行后续的逻辑,造成用户能够获取多次积分。` + + + +# 问题处理 + +原因总结一下: + +1. 添加积分逻辑处理时间过长 +2. 分布式锁超时 + + + +第一个问题:逻辑改动过大,需要时间调整,没有采用 + +第二个问题:换成Redisson,因为redisson在即使超时的情况下也会续锁,避免锁超时 + + + +# 你以为问题真的解决了吗 + +如果把上面的代码换成Redisson后,代码一般是这样的 + +*PointController* + +```java +@RestController +public class PointController { + + @Resource + private PointService pointService; + + @PostMapping(/addPoint) + public Response addPoint() { + RLock redissonClientLock = redissonClient.getLock("addPoint"); + try { + redissonClientLock.lock(); + //调用 + pointService.addPoint(); + } catch (Exception ex) { + e.printStackTrace; + } finally { + //解锁 + redissonClientLock.unlock(); + } + return Response.ok(getLastPoint()); + } +} +``` + +你觉得还会有问题吗?事实证明,这段代码确实没问题了。 + +但是如果你把锁加到pointService 的addPoint方法里面,你觉得会不会有问题? + +> 如果这样做,执行到finally中,释放了锁,但实际该方法还没彻底执行完,还没提交事务,此时下一个阻塞的请求获取了锁,还是会造成锁失效的现象,所以应该把锁加在有事务的方法外面。 + +# 总结 + +一方面因为忙于做项目,忽略了代码Review,另一方面测试的时候没有对接口进行并发测试,或者根本没有测出来,第三没有监控工具监控长事务,以及频繁请求。 + + + diff --git a/Other Interview.md b/Other Interview.md old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 9207da6..68cba31 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ https://github.com/jujunchen/ebook #### 交流平台 -![阿提说说](https://tva1.sinaimg.cn/large/007S8ZIlly1ggv0xy8u89j305805675g.jpg) +阿提说说:[阿提说说-CSDN博客](https://itsaysay.blog.csdn.net/) diff --git a/media/._IMG_082BD9A3B24F-1.jpeg b/media/._IMG_082BD9A3B24F-1.jpeg deleted file mode 100755 index 23a8c5e..0000000 Binary files a/media/._IMG_082BD9A3B24F-1.jpeg and /dev/null differ diff --git a/media/._dubbo-keep-connection.png b/media/._dubbo-keep-connection.png deleted file mode 100755 index e0a6d48..0000000 Binary files a/media/._dubbo-keep-connection.png and /dev/null differ diff --git a/media/._image-20191114195305300.png b/media/._image-20191114195305300.png deleted file mode 100755 index 24a4c4e..0000000 Binary files a/media/._image-20191114195305300.png and /dev/null differ diff --git a/media/._serialize-deserialize.png b/media/._serialize-deserialize.png deleted file mode 100755 index 69c1830..0000000 Binary files a/media/._serialize-deserialize.png and /dev/null differ diff --git a/media/006y8mN6ly1g8xdtvdrcyj31jy06adjs.jpg b/media/006y8mN6ly1g8xdtvdrcyj31jy06adjs.jpg old mode 100755 new mode 100644 diff --git a/media/006y8mN6ly1g8xdwn9fq8j31ki0r8nfb.jpg b/media/006y8mN6ly1g8xdwn9fq8j31ki0r8nfb.jpg old mode 100755 new mode 100644 diff --git a/media/006y8mN6ly1g8xe5miksnj30w201w74f.jpg b/media/006y8mN6ly1g8xe5miksnj30w201w74f.jpg old mode 100755 new mode 100644 diff --git a/media/006y8mN6ly1g8xetz5v5jj31so04sgmx.jpg b/media/006y8mN6ly1g8xetz5v5jj31so04sgmx.jpg old mode 100755 new mode 100644 diff --git a/media/006y8mN6ly1g8xf7p2kg1j30h004omxn.jpg b/media/006y8mN6ly1g8xf7p2kg1j30h004omxn.jpg old mode 100755 new mode 100644 diff --git a/media/006y8mN6ly1g8xfievgsdj31kk03awf9.jpg b/media/006y8mN6ly1g8xfievgsdj31kk03awf9.jpg old mode 100755 new mode 100644 diff --git a/media/006y8mN6ly1g8xfrevz58j30v602yaah.jpg b/media/006y8mN6ly1g8xfrevz58j30v602yaah.jpg old mode 100755 new mode 100644 diff --git a/media/01.png b/media/01.png old mode 100755 new mode 100644 diff --git a/media/252461fbb6d64d3dbc1914b7eadbfb86.jpeg b/media/252461fbb6d64d3dbc1914b7eadbfb86.jpeg old mode 100755 new mode 100644 diff --git a/media/36465fd7d91b3a4aeb3b28c3777649e6.jpeg b/media/36465fd7d91b3a4aeb3b28c3777649e6.jpeg old mode 100755 new mode 100644 diff --git a/media/4935fcc0a209fd1d4b70cade94986f59.jpeg b/media/4935fcc0a209fd1d4b70cade94986f59.jpeg old mode 100755 new mode 100644 diff --git a/media/6650aa32de0def76db0e4c5228619aef.jpeg b/media/6650aa32de0def76db0e4c5228619aef.jpeg old mode 100755 new mode 100644 diff --git a/media/IMG_082BD9A3B24F-1.jpeg b/media/IMG_082BD9A3B24F-1.jpeg old mode 100755 new mode 100644 diff --git a/media/distributed-system-request-sequence.png b/media/distributed-system-request-sequence.png old mode 100755 new mode 100644 diff --git a/media/dubbo-keep-connection.png b/media/dubbo-keep-connection.png old mode 100755 new mode 100644 diff --git a/media/dubbo-not-keep-connection.png b/media/dubbo-not-keep-connection.png old mode 100755 new mode 100644 diff --git a/media/dubbo-service-invoke-road.png b/media/dubbo-service-invoke-road.png old mode 100755 new mode 100644 diff --git a/media/image-20191114193034340-3731511.png b/media/image-20191114193034340-3731511.png old mode 100755 new mode 100644 diff --git a/media/image-20191114193034340.png b/media/image-20191114193034340.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194022209.png b/media/image-20191114194022209.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194055084.png b/media/image-20191114194055084.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194125306.png b/media/image-20191114194125306.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194139915.png b/media/image-20191114194139915.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194157527.png b/media/image-20191114194157527.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194224919.png b/media/image-20191114194224919.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194236837.png b/media/image-20191114194236837.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194247158.png b/media/image-20191114194247158.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194335314.png b/media/image-20191114194335314.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194356825.png b/media/image-20191114194356825.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194415039.png b/media/image-20191114194415039.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194430674.png b/media/image-20191114194430674.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194439833.png b/media/image-20191114194439833.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194449725.png b/media/image-20191114194449725.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194503521.png b/media/image-20191114194503521.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194740033.png b/media/image-20191114194740033.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194815842.png b/media/image-20191114194815842.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194918523.png b/media/image-20191114194918523.png old mode 100755 new mode 100644 diff --git a/media/image-20191114194952076.png b/media/image-20191114194952076.png old mode 100755 new mode 100644 diff --git a/media/image-20191114195007116.png b/media/image-20191114195007116.png old mode 100755 new mode 100644 diff --git a/media/image-20191114195010955.png b/media/image-20191114195010955.png old mode 100755 new mode 100644 diff --git a/media/image-20191114195023197.png b/media/image-20191114195023197.png old mode 100755 new mode 100644 diff --git a/media/image-20191114195045400.png b/media/image-20191114195045400.png old mode 100755 new mode 100644 diff --git a/media/image-20191114195113356.png b/media/image-20191114195113356.png old mode 100755 new mode 100644 diff --git a/media/image-20191114195209444.png b/media/image-20191114195209444.png old mode 100755 new mode 100644 diff --git a/media/image-20191114195228161.png b/media/image-20191114195228161.png old mode 100755 new mode 100644 diff --git a/media/image-20191114195305300.png b/media/image-20191114195305300.png old mode 100755 new mode 100644 diff --git a/media/image-20191114195334883.png b/media/image-20191114195334883.png old mode 100755 new mode 100644 diff --git a/media/image-20191114195346681.png b/media/image-20191114195346681.png old mode 100755 new mode 100644 diff --git a/media/image-20191114195421280.png b/media/image-20191114195421280.png old mode 100755 new mode 100644 diff --git a/media/image-20191114195509483.png b/media/image-20191114195509483.png old mode 100755 new mode 100644 diff --git a/media/image-20191114200036013.png b/media/image-20191114200036013.png old mode 100755 new mode 100644 diff --git a/media/image-20191114203012441.png b/media/image-20191114203012441.png old mode 100755 new mode 100644 diff --git a/media/image-20191114203913199.png b/media/image-20191114203913199.png old mode 100755 new mode 100644 diff --git a/media/image-20191114204200585.png b/media/image-20191114204200585.png old mode 100755 new mode 100644 diff --git a/media/serialize-deserialize.png b/media/serialize-deserialize.png old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp1.md" "b/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp1.md" old mode 100755 new mode 100644 index e5e8a66..a5b5169 --- "a/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp1.md" +++ "b/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp1.md" @@ -1,5 +1,5 @@ # "四面四杀"面经 -## 前言(经历)123123 +## 前言(经历) 毕业五年了,已经将近两年没有面试了,这次想在自己职业生涯5~8年的阶段在一个大厂的核心部门的某个细分领域沉下心来,总结出自己在复杂领域模型设计的方法论,为未来转型业务架构师打下基础,所以简历投的也不多,只有某里、某团、某滴(简历没过),下步准备再刷刷算法试试某条和拼夕夕,除了某团某个大数据部门方向确实不是很匹配(三面仅仅聊了半个小时,部门的主方向主要是一些某团大数据团队在做的一些事情,而我确实对大数据中间件不是很感兴趣,但在此也要谢谢大数据部门的三轮面试官大佬,他们确实问的问题更偏底层,深度也是这次经历面试最深的,面试收获也是不少),其他技术面试都已经通过,也算是比较顺利的面试经历。 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp2.md" "b/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp2.md" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp3.md" "b/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp3.md" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp4.md" "b/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp4.md" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp5.md" "b/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp5.md" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp6.md" "b/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp6.md" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp7.md" "b/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp7.md" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp8.md" "b/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp8.md" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp9.md" "b/345円244円247円345円216円202円351円235円242円347円273円217円/Interview Exp9.md" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/._.DS_Store" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/._.DS_Store" deleted file mode 100755 index 8e82ed9..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/._.DS_Store" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/._image-20191213092233778.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/._image-20191213092233778.png" deleted file mode 100755 index 15c5994..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/._image-20191213092233778.png" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/._image-20191213092446557.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/._image-20191213092446557.png" deleted file mode 100755 index 9a807c8..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/._image-20191213092446557.png" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/006tNbRwgy1g9ux8rjqfwj30g70fhdm4.jpg" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/006tNbRwgy1g9ux8rjqfwj30g70fhdm4.jpg" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/006tNbRwgy1g9ux8w8661j30hc08u403.jpg" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/006tNbRwgy1g9ux8w8661j30hc08u403.jpg" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/006tNbRwgy1g9ux90cmw4j30h803iab4.jpg" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/006tNbRwgy1g9ux90cmw4j30h803iab4.jpg" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/006tNbRwgy1g9ux96vbg7j30i407sgmz.jpg" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/006tNbRwgy1g9ux96vbg7j30i407sgmz.jpg" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/006tNbRwgy1g9ux9a6cfxj308b049gm0.jpg" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/006tNbRwgy1g9ux9a6cfxj308b049gm0.jpg" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/006tNbRwgy1g9ux9dyrn5j30ax0a43zm.jpg" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/006tNbRwgy1g9ux9dyrn5j30ax0a43zm.jpg" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310547" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310547" deleted file mode 100755 index 3e78f41..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310547" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310630" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310630" deleted file mode 100755 index 393d8dd..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310630" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310681" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310681" deleted file mode 100755 index 11561ce..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310681" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310684" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310684" deleted file mode 100755 index cf19ee5..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310684" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310692" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310692" deleted file mode 100755 index 8c1a170..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310692" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310736" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310736" deleted file mode 100755 index 3e78f41..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310736" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310747" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310747" deleted file mode 100755 index 8c1a170..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310747" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310749" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310749" deleted file mode 100755 index c587984..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310749" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310764" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310764" deleted file mode 100755 index c587984..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310764" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310784" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310784" deleted file mode 100755 index c338ed3..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310784" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310802" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310802" deleted file mode 100755 index 11561ce..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310802" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310803" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310803" deleted file mode 100755 index 393d8dd..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310803" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310828" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310828" deleted file mode 100755 index cf19ee5..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310828" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310899" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310899" deleted file mode 100755 index c338ed3..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213310899" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322270" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322270" deleted file mode 100755 index 8c1a170..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322270" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322273" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322273" deleted file mode 100755 index 3e78f41..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322273" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322276" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322276" deleted file mode 100755 index 11561ce..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322276" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322278" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322278" deleted file mode 100755 index c587984..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322278" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322285" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322285" deleted file mode 100755 index c338ed3..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322285" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322329" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322329" deleted file mode 100755 index cf19ee5..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322329" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322353" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322353" deleted file mode 100755 index 393d8dd..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213322353" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213327997" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213327997" deleted file mode 100755 index c338ed3..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213327997" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328002" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328002" deleted file mode 100755 index c587984..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328002" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328002-6244008." "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328002-6244008." deleted file mode 100755 index c587984..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328002-6244008." and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328004" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328004" deleted file mode 100755 index 11561ce..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328004" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328010" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328010" deleted file mode 100755 index cf19ee5..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328010" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328034" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328034" deleted file mode 100755 index 8c1a170..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328034" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328040" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328040" deleted file mode 100755 index 393d8dd..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213328040" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349268" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349268" deleted file mode 100755 index c338ed3..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349268" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349274" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349274" deleted file mode 100755 index cf19ee5..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349274" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349278" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349278" deleted file mode 100755 index c587984..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349278" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349286" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349286" deleted file mode 100755 index 11561ce..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349286" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349309" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349309" deleted file mode 100755 index 393d8dd..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349309" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349324" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349324" deleted file mode 100755 index 8c1a170..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349324" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349350" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349350" deleted file mode 100755 index 3e78f41..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213349350" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358869" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358869" deleted file mode 100755 index c338ed3..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358869" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358870" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358870" deleted file mode 100755 index cf19ee5..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358870" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358873" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358873" deleted file mode 100755 index c587984..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358873" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358875" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358875" deleted file mode 100755 index 11561ce..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358875" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358889" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358889" deleted file mode 100755 index 3e78f41..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358889" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358898" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358898" deleted file mode 100755 index 8c1a170..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358898" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358910" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358910" deleted file mode 100755 index 393d8dd..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213358910" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213554931" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213554931" deleted file mode 100755 index 3e78f41..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213554931" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213602270" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213602270" deleted file mode 100755 index 3e78f41..0000000 Binary files "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/640-20191213213602270" and /dev/null differ diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213092233778.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213092233778.png" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213092446557.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213092446557.png" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213093127572.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213093127572.png" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213093225461.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213093225461.png" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213093243414.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213093243414.png" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213093257647.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213093257647.png" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213213621574.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213213621574.png" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213213740733.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213213740733.png" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213213805148.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213213805148.png" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213213928996.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213213928996.png" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213214128396.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213214128396.png" old mode 100755 new mode 100644 diff --git "a/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213214535975.png" "b/345円244円247円345円216円202円351円235円242円347円273円217円/media/image-20191213214535975.png" old mode 100755 new mode 100644 diff --git "a/345円255円246円344円271円240円350円265円204円346円226円231円/Learning.md" "b/345円255円246円344円271円240円350円265円204円346円226円231円/Learning.md" old mode 100755 new mode 100644 index 08e9bfc..f0bcd9f --- "a/345円255円246円344円271円240円350円265円204円346円226円231円/Learning.md" +++ "b/345円255円246円344円271円240円350円265円204円346円226円231円/Learning.md" @@ -46,9 +46,6 @@ Java知识库:https://www.yuque.com/lexiangqizhong/java ![image-20200627142356190](https://tva1.sinaimg.cn/large/007S8ZIlly1gg6tugktvgj30as0cc0tf.jpg) -## 公众号 - -image-20191223213731596 diff --git "a/345円255円246円344円271円240円350円265円204円346円226円231円/media/006tNbRwly1ga6z6ig0vej30ck0ju75u.jpg" "b/345円255円246円344円271円240円350円265円204円346円226円231円/media/006tNbRwly1ga6z6ig0vej30ck0ju75u.jpg" old mode 100755 new mode 100644 diff --git "a/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212524703.png" "b/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212524703.png" old mode 100755 new mode 100644 diff --git "a/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212734977.png" "b/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212734977.png" old mode 100755 new mode 100644 diff --git "a/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212741417.png" "b/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212741417.png" old mode 100755 new mode 100644 diff --git "a/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212859627.png" "b/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212859627.png" old mode 100755 new mode 100644 diff --git "a/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212918623.png" "b/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212918623.png" old mode 100755 new mode 100644 diff --git "a/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212936257.png" "b/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212936257.png" old mode 100755 new mode 100644 diff --git "a/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212952306.png" "b/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223212952306.png" old mode 100755 new mode 100644 diff --git "a/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223213011426.png" "b/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223213011426.png" old mode 100755 new mode 100644 diff --git "a/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223213029704.png" "b/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223213029704.png" old mode 100755 new mode 100644 diff --git "a/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223213731596.png" "b/345円255円246円344円271円240円350円265円204円346円226円231円/media/image-20191223213731596.png" old mode 100755 new mode 100644 diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/ArrayBlockingQueue.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/ArrayBlockingQueue.md" deleted file mode 100755 index 555cf91..0000000 --- "a/346円272円220円347円240円201円345円210円206円346円236円220円/ArrayBlockingQueue.md" +++ /dev/null @@ -1,643 +0,0 @@ -# ArrayBlockingQueue源码分析 - -> 源码基于open jdk 11 - -![image-20200716182357663](https://tva1.sinaimg.cn/large/007S8ZIlly1ggszjzgkvwj30s00tmdhd.jpg) - -ArrayBlockingQueue是通过有界数组方式实现的阻塞队列 , 通过ReentrantLock实现线程安全,阻塞通过Condition实现,出队和入队使用同一把锁。 - -有两个内部类Itr 和 Itrs,Itrs内部又实现了Node类,像LinkedBlockingQueue和ConcurrentLinkedQueue都有Node类,但两个类有不同,该类的Node类定义在Itrs的内部类中 - -先来看下该类哪些属性 - -## 属性 - -```java -/** The queued items */ -final Object[] items; - -/** items index for next take, poll, peek or remove */ -int takeIndex; - -/** items index for next put, offer, or add */ -int putIndex; - -/** Number of elements in the queue */ -int count; - -/* - * Concurrency control uses the classic two-condition algorithm - * found in any textbook. - */ - -/** Main lock guarding all access */ -final ReentrantLock lock; - -/** Condition for waiting takes */ -private final Condition notEmpty; - -/** Condition for waiting puts */ -private final Condition notFull; - -/** - * Shared state for currently active iterators, or null if there - * are known not to be any. Allows queue operations to update - * iterator state. - */ -transient Itrs itrs; -``` - -## 内部类 - -### Itr - -```java -private class Itr implements Iterator { - /** Index to look for new nextItem; NONE at end */ - private int cursor; - - /** Element to be returned by next call to next(); null if none */ - private E nextItem; - - /** Index of nextItem; NONE if none, REMOVED if removed elsewhere */ - private int nextIndex; - - /** Last element returned; null if none or not detached. */ - private E lastItem; - - /** Index of lastItem, NONE if none, REMOVED if removed elsewhere */ - private int lastRet; - - /** Previous value of takeIndex, or DETACHED when detached */ - private int prevTakeIndex; - - /** Previous value of iters.cycles */ - private int prevCycles; - - /** Special index value indicating "not available" or "undefined" */ - private static final int NONE = -1; - - /** - * Special index value indicating "removed elsewhere", that is, - * removed by some operation other than a call to this.remove(). - */ - private static final int REMOVED = -2; - - /** Special value for prevTakeIndex indicating "detached mode" */ - private static final int DETACHED = -3; - - Itr() { - lastRet = NONE; - final ReentrantLock lock = ArrayBlockingQueue.this.lock; - lock.lock(); - try { - if (count == 0) { - // assert itrs == null; - cursor = NONE; - nextIndex = NONE; - prevTakeIndex = DETACHED; - } else { - final int takeIndex = ArrayBlockingQueue.this.takeIndex; - prevTakeIndex = takeIndex; - nextItem = itemAt(nextIndex = takeIndex); - cursor = incCursor(takeIndex); - if (itrs == null) { - itrs = new Itrs(this); - } else { - itrs.register(this); // in this order - itrs.doSomeSweeping(false); - } - prevCycles = itrs.cycles; - // assert takeIndex>= 0; - // assert prevTakeIndex == takeIndex; - // assert nextIndex>= 0; - // assert nextItem != null; - } - } finally { - lock.unlock(); - } - } - - boolean isDetached() { - // assert lock.isHeldByCurrentThread(); - return prevTakeIndex < 0; - } - - private int incCursor(int index) { - // assert lock.isHeldByCurrentThread(); - if (++index == items.length) index = 0; - if (index == putIndex) index = NONE; - return index; - } - - /** - * Returns true if index is invalidated by the given number of - * dequeues, starting from prevTakeIndex. - */ - private boolean invalidated(int index, int prevTakeIndex, - long dequeues, int length) { - if (index < 0) - return false; - int distance = index - prevTakeIndex; - if (distance < 0) - distance += length; - return dequeues> distance; - } - - /** - * Adjusts indices to incorporate all dequeues since the last - * operation on this iterator. Call only from iterating thread. - */ - private void incorporateDequeues() { - // assert lock.isHeldByCurrentThread(); - // assert itrs != null; - // assert !isDetached(); - // assert count> 0; - - final int cycles = itrs.cycles; - final int takeIndex = ArrayBlockingQueue.this.takeIndex; - final int prevCycles = this.prevCycles; - final int prevTakeIndex = this.prevTakeIndex; - - if (cycles != prevCycles || takeIndex != prevTakeIndex) { - final int len = items.length; - // how far takeIndex has advanced since the previous - // operation of this iterator - long dequeues = (long) (cycles - prevCycles) * len - + (takeIndex - prevTakeIndex); - - // Check indices for invalidation - if (invalidated(lastRet, prevTakeIndex, dequeues, len)) - lastRet = REMOVED; - if (invalidated(nextIndex, prevTakeIndex, dequeues, len)) - nextIndex = REMOVED; - if (invalidated(cursor, prevTakeIndex, dequeues, len)) - cursor = takeIndex; - - if (cursor < 0 && nextIndex < 0 && lastRet < 0) - detach(); - else { - this.prevCycles = cycles; - this.prevTakeIndex = takeIndex; - } - } - } - - /** - * Called when itrs should stop tracking this iterator, either - * because there are no more indices to update (cursor < 0 && - * nextIndex < 0 && lastRet < 0) or as a special exception, when - * lastRet>= 0, because hasNext() is about to return false for the - * first time. Call only from iterating thread. - */ - private void detach() { - // Switch to detached mode - // assert lock.isHeldByCurrentThread(); - // assert cursor == NONE; - // assert nextIndex < 0; - // assert lastRet < 0 || nextItem == null; - // assert lastRet < 0 ^ lastItem != null; - if (prevTakeIndex>= 0) { - // assert itrs != null; - prevTakeIndex = DETACHED; - // try to unlink from itrs (but not too hard) - itrs.doSomeSweeping(true); - } - } - - /** - * For performance reasons, we would like not to acquire a lock in - * hasNext in the common case. To allow for this, we only access - * fields (i.e. nextItem) that are not modified by update operations - * triggered by queue modifications. - */ - public boolean hasNext() { - if (nextItem != null) - return true; - noNext(); - return false; - } - - private void noNext() { - final ReentrantLock lock = ArrayBlockingQueue.this.lock; - lock.lock(); - try { - // assert cursor == NONE; - // assert nextIndex == NONE; - if (!isDetached()) { - // assert lastRet>= 0; - incorporateDequeues(); // might update lastRet - if (lastRet>= 0) { - lastItem = itemAt(lastRet); - // assert lastItem != null; - detach(); - } - } - // assert isDetached(); - // assert lastRet < 0 ^ lastItem != null; - } finally { - lock.unlock(); - } - } - - public E next() { - final E e = nextItem; - if (e == null) - throw new NoSuchElementException(); - final ReentrantLock lock = ArrayBlockingQueue.this.lock; - lock.lock(); - try { - if (!isDetached()) - incorporateDequeues(); - // assert nextIndex != NONE; - // assert lastItem == null; - lastRet = nextIndex; - final int cursor = this.cursor; - if (cursor>= 0) { - nextItem = itemAt(nextIndex = cursor); - // assert nextItem != null; - this.cursor = incCursor(cursor); - } else { - nextIndex = NONE; - nextItem = null; - if (lastRet == REMOVED) detach(); - } - } finally { - lock.unlock(); - } - return e; - } - - public void forEachRemaining(Consumer action) { - Objects.requireNonNull(action); - final ReentrantLock lock = ArrayBlockingQueue.this.lock; - lock.lock(); - try { - final E e = nextItem; - if (e == null) return; - if (!isDetached()) - incorporateDequeues(); - action.accept(e); - if (isDetached() || cursor < 0) return; - final Object[] items = ArrayBlockingQueue.this.items; - for (int i = cursor, end = putIndex, - to = (i < end) ? end : items.length; - ; i = 0, to = end) { - for (; i < to; i++) - action.accept(itemAt(items, i)); - if (to == end) break; - } - } finally { - // Calling forEachRemaining is a strong hint that this - // iteration is surely over; supporting remove() after - // forEachRemaining() is more trouble than it's worth - cursor = nextIndex = lastRet = NONE; - nextItem = lastItem = null; - detach(); - lock.unlock(); - } - } - - public void remove() { - final ReentrantLock lock = ArrayBlockingQueue.this.lock; - lock.lock(); - // assert lock.getHoldCount() == 1; - try { - if (!isDetached()) - incorporateDequeues(); // might update lastRet or detach - final int lastRet = this.lastRet; - this.lastRet = NONE; - if (lastRet>= 0) { - if (!isDetached()) - removeAt(lastRet); - else { - final E lastItem = this.lastItem; - // assert lastItem != null; - this.lastItem = null; - if (itemAt(lastRet) == lastItem) - removeAt(lastRet); - } - } else if (lastRet == NONE) - throw new IllegalStateException(); - // else lastRet == REMOVED and the last returned element was - // previously asynchronously removed via an operation other - // than this.remove(), so nothing to do. - - if (cursor < 0 && nextIndex < 0) - detach(); - } finally { - lock.unlock(); - // assert lastRet == NONE; - // assert lastItem == null; - } - } - - /** - * Called to notify the iterator that the queue is empty, or that it - * has fallen hopelessly behind, so that it should abandon any - * further iteration, except possibly to return one more element - * from next(), as promised by returning true from hasNext(). - */ - void shutdown() { - // assert lock.isHeldByCurrentThread(); - cursor = NONE; - if (nextIndex>= 0) - nextIndex = REMOVED; - if (lastRet>= 0) { - lastRet = REMOVED; - lastItem = null; - } - prevTakeIndex = DETACHED; - // Don't set nextItem to null because we must continue to be - // able to return it on next(). - // - // Caller will unlink from itrs when convenient. - } - - private int distance(int index, int prevTakeIndex, int length) { - int distance = index - prevTakeIndex; - if (distance < 0) - distance += length; - return distance; - } - - /** - * Called whenever an interior remove (not at takeIndex) occurred. - * - * @return true if this iterator should be unlinked from itrs - */ - boolean removedAt(int removedIndex) { - // assert lock.isHeldByCurrentThread(); - if (isDetached()) - return true; - - final int takeIndex = ArrayBlockingQueue.this.takeIndex; - final int prevTakeIndex = this.prevTakeIndex; - final int len = items.length; - // distance from prevTakeIndex to removedIndex - final int removedDistance = - len * (itrs.cycles - this.prevCycles - + ((removedIndex < takeIndex) ? 1 : 0)) - + (removedIndex - prevTakeIndex); - // assert itrs.cycles - this.prevCycles>= 0; - // assert itrs.cycles - this.prevCycles <= 1; - // assert removedDistance> 0; - // assert removedIndex != takeIndex; - int cursor = this.cursor; - if (cursor>= 0) { - int x = distance(cursor, prevTakeIndex, len); - if (x == removedDistance) { - if (cursor == putIndex) - this.cursor = cursor = NONE; - } - else if (x> removedDistance) { - // assert cursor != prevTakeIndex; - this.cursor = cursor = dec(cursor, len); - } - } - int lastRet = this.lastRet; - if (lastRet>= 0) { - int x = distance(lastRet, prevTakeIndex, len); - if (x == removedDistance) - this.lastRet = lastRet = REMOVED; - else if (x> removedDistance) - this.lastRet = lastRet = dec(lastRet, len); - } - int nextIndex = this.nextIndex; - if (nextIndex>= 0) { - int x = distance(nextIndex, prevTakeIndex, len); - if (x == removedDistance) - this.nextIndex = nextIndex = REMOVED; - else if (x> removedDistance) - this.nextIndex = nextIndex = dec(nextIndex, len); - } - if (cursor < 0 && nextIndex < 0 && lastRet < 0) { - this.prevTakeIndex = DETACHED; - return true; - } - return false; - } - - /** - * Called whenever takeIndex wraps around to zero. - * - * @return true if this iterator should be unlinked from itrs - */ - boolean takeIndexWrapped() { - // assert lock.isHeldByCurrentThread(); - if (isDetached()) - return true; - if (itrs.cycles - prevCycles> 1) { - // All the elements that existed at the time of the last - // operation are gone, so abandon further iteration. - shutdown(); - return true; - } - return false; - } - - // /** Uncomment for debugging. */ - // public String toString() { - // return ("cursor=" + cursor + " " + - // "nextIndex=" + nextIndex + " " + - // "lastRet=" + lastRet + " " + - // "nextItem=" + nextItem + " " + - // "lastItem=" + lastItem + " " + - // "prevCycles=" + prevCycles + " " + - // "prevTakeIndex=" + prevTakeIndex + " " + - // "size()=" + size() + " " + - // "remainingCapacity()=" + remainingCapacity()); - // } -} -``` - -### Itrs - -```java -class Itrs { - - /** - * Node in a linked list of weak iterator references. - */ - private class Node extends WeakReference { - Node next; - - Node(Itr iterator, Node next) { - super(iterator); - this.next = next; - } - } - - /** Incremented whenever takeIndex wraps around to 0 */ - int cycles; - - /** Linked list of weak iterator references */ - private Node head; - - /** Used to expunge stale iterators */ - private Node sweeper; - - private static final int SHORT_SWEEP_PROBES = 4; - private static final int LONG_SWEEP_PROBES = 16; - - Itrs(Itr initial) { - register(initial); - } - - /** - * Sweeps itrs, looking for and expunging stale iterators. - * If at least one was found, tries harder to find more. - * Called only from iterating thread. - * - * @param tryHarder whether to start in try-harder mode, because - * there is known to be at least one iterator to collect - */ - void doSomeSweeping(boolean tryHarder) { - // assert lock.isHeldByCurrentThread(); - // assert head != null; - int probes = tryHarder ? LONG_SWEEP_PROBES : SHORT_SWEEP_PROBES; - Node o, p; - final Node sweeper = this.sweeper; - boolean passedGo; // to limit search to one full sweep - - if (sweeper == null) { - o = null; - p = head; - passedGo = true; - } else { - o = sweeper; - p = o.next; - passedGo = false; - } - - for (; probes> 0; probes--) { - if (p == null) { - if (passedGo) - break; - o = null; - p = head; - passedGo = true; - } - final Itr it = p.get(); - final Node next = p.next; - if (it == null || it.isDetached()) { - // found a discarded/exhausted iterator - probes = LONG_SWEEP_PROBES; // "try harder" - // unlink p - p.clear(); - p.next = null; - if (o == null) { - head = next; - if (next == null) { - // We've run out of iterators to track; retire - itrs = null; - return; - } - } - else - o.next = next; - } else { - o = p; - } - p = next; - } - - this.sweeper = (p == null) ? null : o; - } - - /** - * Adds a new iterator to the linked list of tracked iterators. - */ - void register(Itr itr) { - // assert lock.isHeldByCurrentThread(); - head = new Node(itr, head); - } - - /** - * Called whenever takeIndex wraps around to 0. - * - * Notifies all iterators, and expunges any that are now stale. - */ - void takeIndexWrapped() { - // assert lock.isHeldByCurrentThread(); - cycles++; - for (Node o = null, p = head; p != null;) { - final Itr it = p.get(); - final Node next = p.next; - if (it == null || it.takeIndexWrapped()) { - // unlink p - // assert it == null || it.isDetached(); - p.clear(); - p.next = null; - if (o == null) - head = next; - else - o.next = next; - } else { - o = p; - } - p = next; - } - if (head == null) // no more iterators to track - itrs = null; - } - - /** - * Called whenever an interior remove (not at takeIndex) occurred. - * - * Notifies all iterators, and expunges any that are now stale. - */ - void removedAt(int removedIndex) { - for (Node o = null, p = head; p != null;) { - final Itr it = p.get(); - final Node next = p.next; - if (it == null || it.removedAt(removedIndex)) { - // unlink p - // assert it == null || it.isDetached(); - p.clear(); - p.next = null; - if (o == null) - head = next; - else - o.next = next; - } else { - o = p; - } - p = next; - } - if (head == null) // no more iterators to track - itrs = null; - } - - /** - * Called whenever the queue becomes empty. - * - * Notifies all active iterators that the queue is empty, - * clears all weak refs, and unlinks the itrs datastructure. - */ - void queueIsEmpty() { - // assert lock.isHeldByCurrentThread(); - for (Node p = head; p != null; p = p.next) { - Itr it = p.get(); - if (it != null) { - p.clear(); - it.shutdown(); - } - } - head = null; - itrs = null; - } - - /** - * Called whenever an element has been dequeued (at takeIndex). - */ - void elementDequeued() { - // assert lock.isHeldByCurrentThread(); - if (count == 0) - queueIsEmpty(); - else if (takeIndex == 0) - takeIndexWrapped(); - } -} -``` - diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/AtomicIntegerFieldUpdater.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/AtomicIntegerFieldUpdater.md" deleted file mode 100755 index ac2bec9..0000000 --- "a/346円272円220円347円240円201円345円210円206円346円236円220円/AtomicIntegerFieldUpdater.md" +++ /dev/null @@ -1,466 +0,0 @@ -# AtomicIntegerFieldUpdater源码分析 - -> 源码基于Java8 - -这个类是一个基于反射的使用的工具类,可以对指定类的指定的被volatile修饰的int型字段进行原子更新。 - -![image-20200708232748537](https://tva1.sinaimg.cn/large/007S8ZIlly1ggjzdqqif2j30ys0gkwfy.jpg) - -## 创建方法 - -```java -//创建并返回一个Updater, -//要求给定字段在目标对象中必须存在, -//Class类型的参数用于检查反射类型和泛型之间是否匹配 -//tclass 持有给定字段的目标对象的类类型 -//fieldName 要更新的字段的名字,字符串类型 -//泛型参数U代表了目标类型 - -//如果目标字段不是volatile描述的整形,会抛出IllegalArgumentException -//如果目标字段基于访问控制不允许访问, -//或者目标类型中不含有这个字段 -//或者类型不匹配,可能会出现反射的异常, -//会在捕获之后抛出RuntimeException -//此处有个CallerSensitive注解,这个注解具体的内容在对应的地方说 -//对于这个注解此处要明白它在这主要是配合Reflection.getCallerClass() -//作用是避免自己写Reflection.getCallerClass()的参数 -//增加这个特性主要还是为了修复一个jdk中的利用双重反射的越权漏洞 -@CallerSensitive -public static AtomicIntegerFieldUpdater newUpdater(Class tclass, - String fieldName) { - //这个实现类在下面,是个内部类 - //通过反射Reflection类实现对调用者的反射 - //Reflection属于sun包下的, - //Oracle曾多次表示sun包不安全, - //或者说要废弃, - //所以自己使用的时候要谨慎 - //关于sun包的事情后面单独再说 - return new AtomicIntegerFieldUpdaterImpl - (tclass, fieldName, Reflection.getCallerClass()); -} - -//构造方法protected, -//用于保证子类可以调用一个无任何作用的构造方法 -protected AtomicIntegerFieldUpdater() { -} - -``` - -## 抽象方法 - -这些抽象方法有子类AtomicIntegerFieldUpdaterImpl实现 - -```java - -//如果当前值==预期值, -//那么就原子的将当前Updater管理的给定的对象的字段设置为给定的更新值 -//这个方法只对compareAndSet和set提供原子保证 -//但是对于字段的其他修改不一定能够提供保证 -//obj 是要进行设置的目标对象 -//expect 是期待的目标对象 -//update 要更新设置的值 -//如果返回true说明设置成功了 -//如果obj不是构造方法里给出的类型的实例,这个方法可能会抛出ClassCastException -//注: 按照package中对于false的定义,在期待值和当前值不同时,返回false -public abstract boolean compareAndSet(T obj, int expect, int update); - -//方法的基本描述跟上面的一样 -//但是略有不同的地方是 -//这个方法可能会由于错误而失败, -//并且不提供顺序保证, -//因此很少用做compareAndSet的替换方法 -//参数以及返回和异常与上一个方法一样 -public abstract boolean weakCompareAndSet(T obj, int expect, int update); - -//将这个Updater所管理的给定对象的字段设置为给定的新元素 -//这个操作相对于compareAndSet提供了操作的保证 -public abstract void set(T obj, int newValue); - -//可以保证最终会把目标中的被Updater管理的元素更新为给定元素 -public abstract void lazySet(T obj, int newValue); - -//读取被当前Updater管理的字段的当前值 -public abstract int get(T obj); - -``` - -## 其他方法 - -```java -//原子的被当前Updater管理的目标对象中的字段更新为指定值 -//同时返回原来的元素值 -public int getAndSet(T obj, int newValue) { - int prev; - do { - prev = get(obj); - } while (!compareAndSet(obj, prev, newValue)); - return prev; -} - - -//用于将当前Updater管理的目标对象中的字段更新为+1后的值 -//返回原来的值 -public int getAndIncrement(T obj) { - int prev, next; - do { - prev = get(obj); - next = prev + 1; - } while (!compareAndSet(obj, prev, next)); - return prev; -} - -//跟上一个方法相似,区别在于这个方法适用于-1 -public int getAndDecrement(T obj) { - int prev, next; - do { - prev = get(obj); - next = prev - 1; - } while (!compareAndSet(obj, prev, next)); - return prev; -} - -//用于将当前Updater管理的目标对象中的字段更新为+delta后的值 -//返回原来的值 -public int getAndAdd(T obj, int delta) { - int prev, next; - do { - prev = get(obj); - next = prev + delta; - } while (!compareAndSet(obj, prev, next)); - return prev; -} - -//用于将当前Updater管理的目标对象中的字段更新为+1后的值 -//返回新值 -public int incrementAndGet(T obj) { - int prev, next; - do { - prev = get(obj); - next = prev + 1; - } while (!compareAndSet(obj, prev, next)); - return next; -} - -//跟上一个方法相似,区别在于这个方法-1 -public int decrementAndGet(T obj) { - int prev, next; - do { - prev = get(obj); - next = prev - 1; - } while (!compareAndSet(obj, prev, next)); - return next; -} - -//用于将当前Updater管理的目标对象中的字段更新为+delta后的值 -//返回新值 -public int addAndGet(T obj, int delta) { - int prev, next; - do { - prev = get(obj); - next = prev + delta; - } while (!compareAndSet(obj, prev, next)); - return next; -} -``` - -## Java8后增加的方法 - -```java -//返回旧元素 -public final int getAndUpdate(T obj, IntUnaryOperator updateFunction) { - int prev, next; - do { - prev = get(obj); - next = updateFunction.applyAsInt(prev); - } while (!compareAndSet(obj, prev, next)); - return prev; -} - -//返回新元素 -public final int updateAndGet(T obj, IntUnaryOperator updateFunction) { - int prev, next; - do { - prev = get(obj); - next = updateFunction.applyAsInt(prev); - } while (!compareAndSet(obj, prev, next)); - return next; -} - -//执行方法的时候传递的第一个参数是旧元素,第二个参数是x -//返回旧元素 -public final int getAndAccumulate(T obj, int x, - IntBinaryOperator accumulatorFunction) { - int prev, next; - do { - prev = get(obj); - next = accumulatorFunction.applyAsInt(prev, x); - } while (!compareAndSet(obj, prev, next)); - return prev; -} - -//跟上一个方法相似 -//返回新元素 -public final int accumulateAndGet(T obj, int x, - IntBinaryOperator accumulatorFunction) { - int prev, next; - do { - prev = get(obj); - next = accumulatorFunction.applyAsInt(prev, x); - } while (!compareAndSet(obj, prev, next)); - return next; -} - -``` - -## AtomicIntegerFieldUpdaterImpl - -该类为AtomicIntegerFieldUpdater的内部子类 - -```java -private static final class AtomicIntegerFieldUpdaterImpl - extends AtomicIntegerFieldUpdater { - //Unsafe实例 - private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe(); - //偏移量记录 - private final long offset; - //如果字段受保护,子类构造这个更新器,否则和tclass一样 - //cclass记录的是子类类名 - private final Class cclass; - //含有目标字段的类的类类型 - //cclass tclass两个字段主要用于校验是否能够进行更新 - private final Class tclass; - - //构造方法 - //tclass:含有目标字段的目标类类型 - //caller:调用者的类类型 - AtomicIntegerFieldUpdaterImpl(final Class tclass, - final String fieldName, - final Class caller) { - final Field field; - final int modifiers; - try { - //这里有个AccessController,这个属于Java的Security的一部分 - //包括AccessController,SecurityManager - //这部分相关内容在对应的部分说 - //在这doPrivileged方法主要是用于越过权限检查 - //在特权块内的代码会越过特权检查, - //这之后其他的类就可以越过检查执行这段代码 - field = AccessController.doPrivileged( - //PrivilegedExceptionAction接口中含有一个run方法 - //当做一个doPrivileged调用时, - //一个PrivilegedAction实现的实例被传递给它。 - //doPrivileged方法在使特权生效后, - //从PrivilegedAction实现中调用run方法, - //并返回run方法的返回值以作为doPrivileged的返回值 - //PrivilegedAction和PrivilegedExceptionAction区别在于 - //如果特权块中可能异常,就用PrivilegedExceptionActio - //否则使用PrivilegedAction - new PrivilegedExceptionAction() { - public Field run() throws NoSuchFieldException { - return tclass.getDeclaredField(fieldName); - } - }); - //读取字段修饰符(此处返回的是int类型) - // PUBLIC: 1 - // PRIVATE: 2 - // PROTECTED: 4 - // STATIC: 8 - // FINAL: 16 - // SYNCHRONIZED: 32 - // VOLATILE: 64 - // TRANSIENT: 128 - // NATIVE: 256 - // INTERFACE: 512 - // ABSTRACT: 1024 - // STRICT: 2048 - //返回值是各项修饰符的加和 - //自己做判断可能有点麻烦 - //可以通过Modifier.toString(int mod)方法来进行解析 - //或者通过Modifier里面的isXXX相关方法判断 - //Modifier.toString方法放到Modifier里面说 - modifiers = field.getModifiers(); - //这个方法也在sun包下面 - //这个方法主要用于校验给定的调用者(第一个参数) - //第二个参数是目标的类类型 - //第三个参数在静态的情况系可以是null - //第四个参数是访问修饰 - //判断是否可以访问目标对象中构造方法、字段、普通方法等 - //如果没有访问权限会抛出IllegalAccessException - sun.reflect.misc.ReflectUtil.ensureMemberAccess( - caller, tclass, null, modifiers); - //目标类的类加载器 - ClassLoader cl = tclass.getClassLoader(); - //调用者的类加载器 - ClassLoader ccl = caller.getClassLoader(); - //这个判断条件要求满足目标类和调用者不能使同一个类加载器 - //调用者不能是根加载器 - //目标类加载器是根加载器或者调用者类加载器和目标类加载器的关系不满足isAncestor(没有委托链关系) - if ((ccl != null) && (ccl != cl) && - ((cl == null) || !isAncestor(cl, ccl))) { - //sun包下面 - //校验目标类的包的可访问性 - sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass); - } - } catch (PrivilegedActionException pae) { - throw new RuntimeException(pae.getException()); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - - //类型校验 - if (field.getType() != int.class) - throw new IllegalArgumentException("Must be integer type"); - - //判断是否有volatile修饰 - if (!Modifier.isVolatile(modifiers)) - throw new IllegalArgumentException("Must be volatile type"); - - //访问protected字段需要是同包下或者有父子关系的情况 - //这个判断条件指出,如果是 - //在字段为Protected,目标类是调用类的父类,调用类和目标类不是同一个包下时 - //cclass存储调用者的类 - //上面条件任何一个不满足都指向tclass - //包括不是保护域,没有父子关系,在同一个包下面 - this.cclass = (Modifier.isProtected(modifiers) && - tclass.isAssignableFrom(caller) && - !isSamePackage(tclass, caller)) - ? caller : tclass; - - //记录目标class - this.tclass = tclass; - //记录偏移量 - this.offset = U.objectFieldOffset(field); - } - - - //如果第二个加载器可以再第一个加载器的委托链中找到返回True - //等价于调用 first.isAncestor(second). - private static boolean isAncestor(ClassLoader first, ClassLoader second) { - ClassLoader acl = first; - do { - acl = acl.getParent(); - if (second == acl) { - return true; - } - } while (acl != null); - return false; - } - - //如果两个类具有相同的类加载器和包限定符,则返回true - private static boolean isSamePackage(Class class1, Class class2) { - return class1.getClassLoader() == class2.getClassLoader() - //Objects是一个工具类,具体的放到对应的类讲 - && Objects.equals(getPackageName(class1), getPackageName(class2)); - } - - //获取给定类的包名 - private static String getPackageName(Class cls) { - //这个方法返回虚拟机中Class对象的表示 - //例如String[]类型的表示为: [Ljava.lang.String - String cn = cls.getName(); - //定位到最后一个点的位置 - int dot = cn.lastIndexOf('.'); - //返回类名的截取 - return (dot != -1) ? cn.substring(0, dot) : ""; - } - - //检查目标参数是否为cclass的实例。失败时,抛出异常原因 - private final void accessCheck(T obj) { - if (!cclass.isInstance(obj)) - throwAccessCheckException(obj); - } - - //如果由于受保护的访问而导致访问检查失败, - //则引发访问异常, - //否则将引发ClassCastException。 - //实际上当cclass和tclass如果相等,会抛出ClassCastException - //也就是说在上面初始化过程中出现了对于受保护字段的访问检查失败 - private final void throwAccessCheckException(T obj) { - if (cclass == tclass) - throw new ClassCastException(); - else - throw new RuntimeException( - new IllegalAccessException( - "Class " + - cclass.getName() + - " can not access a protected member of class " + - tclass.getName() + - " using an instance of " + - obj.getClass().getName())); - } - - //************************************************************** - - //下面终于到了这个类应该做的正经事了 - //由于上面有过叙述,就不进行冗余的叙述了 - //每个方法都是先进行对可访问性的校验 - - //CAS操作 - public final boolean compareAndSet(T obj, int expect, int update) { - accessCheck(obj); - return U.compareAndSwapInt(obj, offset, expect, update); - } - - //弱CAS操作 - public final boolean weakCompareAndSet(T obj, int expect, int update) { - accessCheck(obj); - return U.compareAndSwapInt(obj, offset, expect, update); - } - - //set - public final void set(T obj, int newValue) { - accessCheck(obj); - U.putIntVolatile(obj, offset, newValue); - } - - //lazySet - public final void lazySet(T obj, int newValue) { - accessCheck(obj); - U.putOrderedInt(obj, offset, newValue); - } - - //读取 - public final int get(T obj) { - accessCheck(obj); - return U.getIntVolatile(obj, offset); - } - - //设置并返回原来的值 - public final int getAndSet(T obj, int newValue) { - accessCheck(obj); - return U.getAndSetInt(obj, offset, newValue); - } - - //增加delta后返回原来的元素 - public final int getAndAdd(T obj, int delta) { - accessCheck(obj); - return U.getAndAddInt(obj, offset, delta); - } - - //+1然后返回原来的元素,实际调用的是getAndAdd - public final int getAndIncrement(T obj) { - return getAndAdd(obj, 1); - } - - //-1然后返回原来的元素,实际调用的是getAndAdd - public final int getAndDecrement(T obj) { - return getAndAdd(obj, -1); - } - - //+1然后返回新元素,实际调用的是getAndAdd - public final int incrementAndGet(T obj) { - return getAndAdd(obj, 1) + 1; - } - - //-1然后返回新元素,实际调用的是getAndAdd - public final int decrementAndGet(T obj) { - return getAndAdd(obj, -1) - 1; - } - - //加delta然后返回新元素,实际调用的是getAndAdd - public final int addAndGet(T obj, int delta) { - return getAndAdd(obj, delta) + delta; - } - -} -``` \ No newline at end of file diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/CompletableFuture.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/CompletableFuture.md" new file mode 100644 index 0000000..e946189 --- /dev/null +++ "b/346円272円220円347円240円201円345円210円206円346円236円220円/CompletableFuture.md" @@ -0,0 +1,27 @@ +# CompletableFuture 源码分析 + +> 源码基于open jdk 11 + +CompletableFuture 是 jdk8 引入的类,主要是对Future的补充。 + +CompletableFuture类的官方API文档解释: + +1. CompletableFuture是一个在完成时可以触发相关方法和操作的Future,并且它可以视作为CompletableStage。 +2. 除了直接操作状态和结果的这些方法和相关方法外(CompletableFuture API提供的方法),CompletableFuture还实现了以下的CompletionStage的相关策略: + - 非异步方法的完成,可以由当前CompletableFuture的线程提供,也可以由其他调用完方法的线程提供。 + - 所有没有显示使用Executor的异步方法,会使用ForkJoinPool.commonPool()(那些并行度小于2的任务会创建一个新线程来运行)。为了简化监视、调试和跟踪异步方法,所有异步任务都被标记为CompletableFuture.AsynchronouseCompletionTask。 + - 所有CompletionStage方法都是独立于其他公共方法实现的,因此一个方法的行为不受子类中其他方法的覆盖影响。 +3. CompletableFuture还实现了Future的以下策略 + - 不像FutureTask,因CompletableFuture无法直接控制计算任务的完成,所以CompletableFuture的取消会被视为异常完成。调用cancel()方法会和调用completeExceptionally()方法一样,具有同样的效果。isCompletedEceptionally()方法可以判断CompletableFuture是否是异常完成。 + - 在调用get()和get(long, TimeUnit)方法时以异常的形式完成,则会抛出ExecutionException,大多数情况下都会使用join()和getNow(T),它们会抛出CompletionException。 + +待回答的几个问题: + +1、如何实现并发执行任务? + +2、并发执行如何获取任务结果? + + + +https://blog.csdn.net/CoderBruis/article/details/103181520?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control + diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/ConcurrentLinkedQueue.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/ConcurrentLinkedQueue.md" old mode 100755 new mode 100644 diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/Flow_SubmissionPublisher.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/Flow_SubmissionPublisher.md" old mode 100755 new mode 100644 diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/Helpers.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/Helpers.md" old mode 100755 new mode 100644 diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/LinkedBlockingQueue.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/LinkedBlockingQueue.md" old mode 100755 new mode 100644 diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/LinkedHashMap.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/LinkedHashMap.md" old mode 100755 new mode 100644 diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/LongAdder.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/LongAdder.md" old mode 100755 new mode 100644 diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/Spring Boot/Spring Boot Reference 2.6.11 .pdf" "b/346円272円220円347円240円201円345円210円206円346円236円220円/Spring Boot/Spring Boot Reference 2.6.11 .pdf" new file mode 100644 index 0000000..afcd9d4 Binary files /dev/null and "b/346円272円220円347円240円201円345円210円206円346円236円220円/Spring Boot/Spring Boot Reference 2.6.11 .pdf" differ diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/StreamApi.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/StreamApi.md" new file mode 100644 index 0000000..705847b --- /dev/null +++ "b/346円272円220円347円240円201円345円210円206円346円236円220円/StreamApi.md" @@ -0,0 +1,3629 @@ +# Stream API源码分析 + +## 前提 + +`Stream`是`JDK1.8`中首次引入的,距今已经过去了接近`8`年时间(`JDK1.8`正式版是`2013`年底发布的)。`Stream`的引入一方面极大地简化了某些开发场景,另一方面也可能降低了编码的可读性(确实有不少人说到`Stream`会降低代码的可读性,但是在笔者看来,熟练使用之后反而觉得代码的可读性提高了)。这篇文章会花巨量篇幅,详细分析`Stream`的底层实现原理,参考的源码是`JDK11`的源码,其他版本`JDK`可能不适用于本文中的源码展示和相关例子。 + +> 这篇文章花费了极多时间和精力梳理和编写,希望能够帮助到本文的读者 + +## Stream是如何做到向前兼容的 + +`Stream`是`JDK1.8`引入的,如要需要`JDK1.7`或者以前的代码也能在`JDK1.8`或以上运行,那么`Stream`的引入必定不能在原来已经发布的接口方法进行修改,否则必定会因为兼容性问题导致老版本的接口实现无法在新版本中运行(方法签名出现异常),猜测是基于这个问题引入了接口默认方法,也就是`default`关键字。查看源码可以发现,`ArrayList`的超类`Collection`和`Iterable`分别添加了数个`default`方法: + +```java +// java.util.Collection部分源码 +public interface Collection extends Iterable { + + // 省略其他代码 + + @Override + default Spliterator spliterator() { + return Spliterators.spliterator(this, 0); + } + + default Stream stream() { + return StreamSupport.stream(spliterator(), false); + } + + default Stream parallelStream() { + return StreamSupport.stream(spliterator(), true); + } +} + +// java.lang.Iterable部分源码 +public interface Iterable { + + // 省略其他代码 + + default void forEach(Consumer action) { + Objects.requireNonNull(action); + for (T t : this) { + action.accept(t); + } + } + + default Spliterator spliterator() { + return Spliterators.spliteratorUnknownSize(iterator(), 0); + } +} +``` + +从直觉来看,这些新增的方法应该就是`Stream`实现的关键方法(后面会印证这不是直觉,而是查看源码的结果)。接口默认方法在使用上和实例方法一致,在实现上可以直接在接口方法中编写方法体,有点静态方法的意味,但是子类可以覆盖其实现(也就是接口默认方法在本接口中的实现有点像静态方法,可以被子类覆盖,使用上和实例方法一致)。这种实现方式,有可能是一种突破,也有可能是一种妥协,但是无论是妥协还是突破,都实现了向前兼容: + +```java +// JDK1.7中的java.lang.Iterable +public interface Iterable { + + Iterator iterator(); +} + +// JDK1.7中的Iterable实现 +public MyIterable implements Iterable{ + + public Iterator iterator(){ + .... + } +} +``` + +如上,`MyIterable`在`JDK1.7`中定义,如果该类在`JDK1.8`中运行,那么调用其实例中的`forEach()`和`spliterator()`方法,相当于直接调用`JDK1.8`中的`Iterable`中的接口默认方法`forEach()`和`spliterator()`。当然受限于`JDK`版本,这里只能确保编译通过,旧功能正常使用,而无法在`JDK1.7`中使用`Stream`相关功能或者使用`default`方法关键字。总结这么多,就是想说明为什么使用`JDK7`开发和编译的代码可以在`JDK8`环境下运行。 + +## 可拆分迭代器Spliterator + +`Stream`实现的基石是`Spliterator`,`Spliterator`是`splitable iterator`的缩写,意为"可拆分迭代器",用于遍历指定数据源(例如数组、集合或者`IO Channel`等)中的元素,在设计上充分考虑了串行和并行的场景。上一节提到了`Collection`存在接口默认方法`spliterator()`,此方法会生成一个`Spliterator`实例,意为着**所有的集合子类都具备创建`Spliterator`实例的能力**。`Stream`的实现在设计上和`Netty`中的`ChannelHandlerContext`十分相似,本质是一个链表,**而`Spliterator`就是这个链表的`Head`节点**(`Spliterator`实例就是一个流实例的头节点,后面分析具体的源码时候再具体展开)。 + +### Spliterator接口方法 + +接着看`Spliterator`接口定义的方法: + +```java +public interface Spliterator { + + // 暂时省略其他代码 + + boolean tryAdvance(Consumer action); + + default void forEachRemaining(Consumer action) { + do { } while (tryAdvance(action)); + } + + Spliterator trySplit(); + + long estimateSize(); + + default long getExactSizeIfKnown() { + return (characteristics() & SIZED) == 0 ? -1L : estimateSize(); + } + + int characteristics(); + + default boolean hasCharacteristics(int characteristics) { + return (characteristics() & characteristics) == characteristics; + } + + default Comparator getComparator() { + throw new IllegalStateException(); + } + + // 暂时省略其他代码 +} +``` + +**tryAdvance** + +- 方法签名:`boolean tryAdvance(Consumer action)` +- 功能:如果`Spliterator`中存在剩余元素,则对其中的某个元素执行传入的`action`回调,并且返回`true`,否则返回`false`。如果`Spliterator`启用了`ORDERED`特性,会按照顺序(这里的顺序值可以类比为`ArrayList`中容器数组元素的下标,`ArrayList`中添加新元素是天然有序的,下标由零开始递增)处理下一个元素 +- 例子: + +```java +public static void main(String[] args) throws Exception { + List list = new ArrayList(); + list.add(2); + list.add(1); + list.add(3); + Spliterator spliterator = list.stream().spliterator(); + final AtomicInteger round = new AtomicInteger(1); + final AtomicInteger loop = new AtomicInteger(1); + while (spliterator.tryAdvance(num -> System.out.printf("第%d轮回调Action,值:%d\n", round.getAndIncrement(), num))) { + System.out.printf("第%d轮循环\n", loop.getAndIncrement()); + } +} + +// 控制台输出 +第1轮回调Action,值:2 +第1轮循环 +第2轮回调Action,值:1 +第2轮循环 +第3轮回调Action,值:3 +第3轮循环 +``` + +**forEachRemaining** + +- 方法签名:`default void forEachRemaining(Consumer action)` +- 功能:如果`Spliterator`中存在剩余元素,则对其中的**所有剩余元素**在**当前线程中**执行传入的`action`回调。如果`Spliterator`启用了`ORDERED`特性,会按照顺序处理剩余所有元素。这是一个接口默认方法,方法体比较粗暴,直接是一个死循环包裹着`tryAdvance()`方法,直到`false`退出循环 +- 例子: + +```java +public static void main(String[] args) { + List list = new ArrayList(); + list.add(2); + list.add(1); + list.add(3); + Spliterator spliterator = list.stream().spliterator(); + final AtomicInteger round = new AtomicInteger(1); + spliterator.forEachRemaining(num -> System.out.printf("第%d轮回调Action,值:%d\n", round.getAndIncrement(), num)); +} + +// 控制台输出 +第1轮回调Action,值:2 +第2轮回调Action,值:1 +第3轮回调Action,值:3 +``` + +**trySplit** + +- 方法签名:`Spliterator trySplit()` +- 功能:如果当前的`Spliterator`是可分区(可分割)的,那么此方法将会返回一个全新的`Spliterator`实例,这个全新的`Spliterator`实例里面的元素不会被当前`Spliterator`实例中的元素覆盖(这里是直译了`API`注释,实际要表达的意思是:当前的`Spliterator`实例`X`是可分割的,`trySplit()`方法会分割`X`产生一个全新的`Spliterator`实例`Y`,原来的`X`所包含的元素(范围)也会收缩,类似于`X = [a,b,c,d] => X = [a,b], Y = [c,d]`;如果当前的`Spliterator`实例`X`是不可分割的,此方法会返回`NULL`),**具体的分割算法由实现类决定** +- 例子: + +```java +public static void main(String[] args) throws Exception { + List list = new ArrayList(); + list.add(2); + list.add(3); + list.add(4); + list.add(1); + Spliterator first = list.stream().spliterator(); + Spliterator second = first.trySplit(); + first.forEachRemaining(num -> { + System.out.printf("first spliterator item: %d\n", num); + }); + second.forEachRemaining(num -> { + System.out.printf("second spliterator item: %d\n", num); + }); +} + +// 控制台输出 +first spliterator item: 4 +first spliterator item: 1 +second spliterator item: 2 +second spliterator item: 3 +``` + +**estimateSize** + +- 方法签名:`long estimateSize()` +- 功能:返回`forEachRemaining()`方法需要遍历的元素总量的估计值,如果样本个数是无限、计算成本过高或者未知,会直接返回`Long.MAX_VALUE` +- 例子: + +```java +public static void main(String[] args) throws Exception { + List list = new ArrayList(); + list.add(2); + list.add(3); + list.add(4); + list.add(1); + Spliterator spliterator = list.stream().spliterator(); + System.out.println(spliterator.estimateSize()); +} + +// 控制台输出 +4 +``` + +**getExactSizeIfKnown** + +- 方法签名:`default long getExactSizeIfKnown()` +- 功能:如果当前的`Spliterator`具备`SIZED`特性(关于特性,下文再展开分析),那么直接调用`estimateSize()`方法,否则返回`-1` +- 例子: + +```java +public static void main(String[] args) throws Exception { + List list = new ArrayList(); + list.add(2); + list.add(3); + list.add(4); + list.add(1); + Spliterator spliterator = list.stream().spliterator(); + System.out.println(spliterator.getExactSizeIfKnown()); +} + +// 控制台输出 +4 +``` + +**int characteristics()** + +- 方法签名:`long estimateSize()` +- 功能:当前的`Spliterator`具备的特性(集合),采用位运算,存储在`32`位整数中(关于特性,下文再展开分析) + +**hasCharacteristics** + +- 方法签名:`default boolean hasCharacteristics(int characteristics)` +- 功能:判断当前的`Spliterator`是否具备传入的特性 + +**getComparator** + +- 方法签名:`default Comparator getComparator()` +- 功能:如果当前的`Spliterator`具备`SORTED`特性,则需要返回一个`Comparator`实例;如果`Spliterator`中的元素是天然有序(例如元素实现了`Comparable`接口),则返回`NULL`;其他情况直接抛出`IllegalStateException`异常 + +### Spliterator自分割 + +`Spliterator#trySplit()`可以把一个既有的`Spliterator`实例分割为两个`Spliterator`实例,笔者这里把这种方式称为`Spliterator`自分割,示意图如下: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-1.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-1.png) + +这里的分割在实现上可以采用两种方式: + +- 物理分割:对于`ArrayList`而言,把底层数组**拷贝**并且进行分割,用上面的例子来说相当于`X = [1,3,4,2] => X = [4,2], Y = [1,3]`,这样实现加上对于`ArrayList`中本身的元素容器数组,相当于多存了一份数据,显然不是十分合理 +- 逻辑分割:对于`ArrayList`而言,由于元素容器数组天然有序,可以采用数组的索引(下标)进行分割,用上面的例子来说相当于`X = 索引表[0,1,2,3] => X = 索引表[2,3], Y = 索引表[0,1]`,这种方式是共享底层容器数组,只对元素索引进行分割,实现上比较简单而且相对合理 + +参看`ArrayListSpliterator`的源码,可以分析其分割算法实现: + +```java +// ArrayList#spliterator() +public Spliterator spliterator() { + return new ArrayListSpliterator(0, -1, 0); +} + +// ArrayList中内部类ArrayListSpliterator +final class ArrayListSpliterator implements Spliterator { + + // 当前的处理的元素索引值,其实是剩余元素的下边界值(包含),在tryAdvance()或者trySplit()方法中被修改,一般初始值为0 + private int index; + // 栅栏,其实是元素索引值的上边界值(不包含),一般初始化的时候为-1,使用时具体值为元素索引值上边界加1 + private int fence; + // 预期的修改次数,一般初始化值等于modCount + private int expectedModCount; + + ArrayListSpliterator(int origin, int fence, int expectedModCount) { + this.index = origin; + this.fence = fence; + this.expectedModCount = expectedModCount; + } + + // 获取元素索引值的上边界值,如果小于0,则把hi和fence都赋值为(ArrayList中的)size,expectedModCount赋值为(ArrayList中的)modCount,返回上边界值 + // 这里注意if条件中有赋值语句hi = fence,也就是此方法调用过程中临时变量hi总是重新赋值为fence,fence是ArrayListSpliterator实例中的成员属性 + private int getFence() { + int hi; + if ((hi = fence) < 0) { + expectedModCount = modCount; + hi = fence = size; + } + return hi; + } + + // Spliterator自分割,这里采用了二分法 + public ArrayListSpliterator trySplit() { + // hi等于当前ArrayListSpliterator实例中的fence变量,相当于获取剩余元素的上边界值 + // lo等于当前ArrayListSpliterator实例中的index变量,相当于获取剩余元素的下边界值 + // mid = (lo + hi)>>> 1,这里的无符号右移动1位运算相当于(lo + hi)/2 + int hi = getFence(), lo = index, mid = (lo + hi)>>> 1; + // 当lo>= mid的时候为不可分割,返回NULL,否则,以index = lo,fence = mid和expectedModCount = expectedModCount创建一个新的ArrayListSpliterator + // 这里有个细节之处,在新的ArrayListSpliterator构造参数中,当前的index被重新赋值为index = mid,这一点容易看漏,老程序员都喜欢做这样的赋值简化 + // lo>= mid返回NULL的时候,不会创建新的ArrayListSpliterator,也不会修改当前ArrayListSpliterator中的参数 + return (lo>= mid) ? null : new ArrayListSpliterator(lo, index = mid, expectedModCount); + } + + // tryAdvance实现 + public boolean tryAdvance(Consumer action) { + if (action == null) + throw new NullPointerException(); + // 获取迭代的上下边界 + int hi = getFence(), i = index; + // 由于前面分析下边界是包含关系,上边界是非包含关系,所以这里要i < hi而不是i <= hi + if (i < hi) { + index = i + 1; + // 这里的elementData来自ArrayList中,也就是前文经常提到的元素数组容器,这里是直接通过元素索引访问容器中的数据 + @SuppressWarnings("unchecked") E e = (E)elementData[i]; + // 对传入的Action进行回调 + action.accept(e); + // 并发修改异常判断 + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + return true; + } + return false; + } + + // forEachRemaining实现,这里没有采用默认实现,而是完全覆盖实现一个新方法 + public void forEachRemaining(Consumer action) { + // 这里会新建所需的中间变量,i为index的中间变量,hi为fence的中间变量,mc为expectedModCount的中间变量 + int i, hi, mc; + Object[] a; + if (action == null) + throw new NullPointerException(); + // 判断容器数组存在性 + if ((a = elementData) != null) { + // hi、fence和mc初始化 + if ((hi = fence) < 0) { + mc = modCount; + hi = size; + } + else + mc = expectedModCount; + // 这里就是先做参数合法性校验,再遍历临时数组容器a中中[i,hi)的剩余元素对传入的Action进行回调 + // 这里注意有一处隐蔽的赋值(index = hi),下界被赋值为上界,意味着每个ArrayListSpliterator实例只能调用一次forEachRemaining()方法 + if ((i = index)>= 0 && (index = hi) <= a.length) { + for (; i < hi; ++i) { + @SuppressWarnings("unchecked") E e = (E) a[i]; + action.accept(e); + } + // 这里校验ArrayList的modCount和mc是否一致,理论上在forEachRemaining()遍历期间,不能对数组容器进行元素的新增或者移除,一旦发生modCount更变会抛出异常 + if (modCount == mc) + return; + } + } + throw new ConcurrentModificationException(); + } + + // 获取剩余元素估计值,就是用剩余元素索引上边界直接减去下边界 + public long estimateSize() { + return getFence() - index; + } + + // 具备ORDERED、SIZED和SUBSIZED特性 + public int characteristics() { + return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED; + } +} +``` + +在阅读源码的时候务必注意,老一辈的程序员有时候会采用比较**隐蔽**的赋值方式,笔者认为需要展开一下: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-2.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-2.png) + +第一处红圈位置在构建新的`ArrayListSpliterator`的时候,当前`ArrayListSpliterator`的`index`属性也被修改了,过程如下图: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-3.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-3.png) + +第二处红圈位置,在`forEachRemaining()`方法调用时候做参数校验,并且`if`分支里面把`index`(下边界值)赋值为`hi`(上边界值),**那么一个`ArrayListSpliterator`实例中的`forEachRemaining()`方法的遍历操作必定只会执行一次**。可以这样验证一下: + +```java +public static void main(String[] args) { + List list = new ArrayList(); + list.add(2); + list.add(1); + list.add(3); + Spliterator spliterator = list.stream().spliterator(); + final AtomicInteger round = new AtomicInteger(1); + spliterator.forEachRemaining(num -> System.out.printf("[第一次遍历forEachRemaining]第%d轮回调Action,值:%d\n", round.getAndIncrement(), num)); + round.set(1); + spliterator.forEachRemaining(num -> System.out.printf("[第二次遍历forEachRemaining]第%d轮回调Action,值:%d\n", round.getAndIncrement(), num)); +} + +// 控制台输出 +[第一次遍历forEachRemaining]第1轮回调Action,值:2 +[第一次遍历forEachRemaining]第2轮回调Action,值:1 +[第一次遍历forEachRemaining]第3轮回调Action,值:3 +``` + +对于`ArrayListSpliterator`的实现可以确认下面几点: + +- 一个新的`ArrayListSpliterator`实例中的`forEachRemaining()`方法只能调用一次 +- `ArrayListSpliterator`实例中的`forEachRemaining()`方法遍历元素的边界是`[index, fence)` +- `ArrayListSpliterator`自分割的时候,分割出来的新`ArrayListSpliterator`负责处理元素下标小的分段(类比`fork`的左分支),而原`ArrayListSpliterator`负责处理元素下标大的分段(类比`fork`的右分支) +- `ArrayListSpliterator`提供的`estimateSize()`方法得到的分段元素剩余数量是一个准确值 + +如果把上面的例子继续分割,可以得到下面的过程: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-4.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-4.png) + +**`Spliterator`自分割是并行流实现的基础**,并行流计算过程其实就是`fork-join`的处理过程,`trySplit()`方法的实现决定了`fork`任务的粒度,每个`fork`任务进行计算的时候是并发安全的,这一点由线程封闭(线程栈封闭)保证,每一个`fork`任务计算完成最后的结果再由单个线程进行`join`操作,才能得到正确的结果。下面的例子是求整数`1 ~ 100`的和: + +```java +public class ConcurrentSplitCalculateSum { + + private static class ForkTask extends Thread { + + private int result = 0; + + private final Spliterator spliterator; + private final CountDownLatch latch; + + public ForkTask(Spliterator spliterator, + CountDownLatch latch) { + this.spliterator = spliterator; + this.latch = latch; + } + + @Override + public void run() { + long start = System.currentTimeMillis(); + spliterator.forEachRemaining(num -> result = result + num); + long end = System.currentTimeMillis(); + System.out.printf("线程[%s]完成计算任务,当前段计算结果:%d,耗时:%d ms\n", + Thread.currentThread().getName(), result, end - start); + latch.countDown(); + } + + public int result() { + return result; + } + } + + private static int join(List tasks) { + int result = 0; + for (ForkTask task : tasks) { + result = result + task.result(); + } + return result; + } + + private static final int THREAD_NUM = 4; + + public static void main(String[] args) throws Exception { + List source = new ArrayList(); + for (int i = 1; i < 101; i++) { + source.add(i); + } + Spliterator root = source.stream().spliterator(); + List> spliteratorList = new ArrayList(); + Spliterator x = root.trySplit(); + Spliterator y = x.trySplit(); + Spliterator z = root.trySplit(); + spliteratorList.add(root); + spliteratorList.add(x); + spliteratorList.add(y); + spliteratorList.add(z); + List tasks = new ArrayList(); + CountDownLatch latch = new CountDownLatch(THREAD_NUM); + for (int i = 0; i < THREAD_NUM; i++) { + ForkTask task = new ForkTask(spliteratorList.get(i), latch); + task.setName("fork-task-" + (i + 1)); + tasks.add(task); + } + tasks.forEach(Thread::start); + latch.await(); + int result = join(tasks); + System.out.println("最终计算结果为:" + result); + } +} + +// 控制台输出结果 +线程[fork-task-4]完成计算任务,当前段计算结果:1575,耗时:0 ms +线程[fork-task-2]完成计算任务,当前段计算结果:950,耗时:1 ms +线程[fork-task-3]完成计算任务,当前段计算结果:325,耗时:1 ms +线程[fork-task-1]完成计算任务,当前段计算结果:2200,耗时:1 ms +最终计算结果为:5050 +``` + +当然,最终并行流的计算用到了`ForkJoinPool`,并不像这个例子中这么粗暴地进行异步执行。关于并行流的实现下文会详细分析。 + +### Spliterator支持的特性 + +某一个`Spliterator`实例支持的特性由方法`characteristics()`决定,这个方法返回的是一个`32`位数值,实际使用中会展开为`bit`数组,所有的特性分配在不同的位上,而`hasCharacteristics(int characteristics)`就是通过输入的具体特性值通过位运算判断该特性是否存在于`characteristics()`中。下面简化`characteristics`为`byte`分析一下这个技巧: + +```shell +假设:byte characteristics() => 也就是最多8个位用于表示特性集合,如果每个位只表示一种特性,那么可以总共表示8种特性 +特性X:0000 0001 +特性Y:0000 0010 +以此类推 +假设:characteristics = X | Y = 0000 0001 | 0000 0010 = 0000 0011 +那么:characteristics & X = 0000 0011 & 0000 0001 = 0000 0001 +判断characteristics是否包含X:(characteristics & X) == X +``` + +上面推断的过程就是`Spliterator`中特性判断方法的处理逻辑: + +```java +// 返回特性集合 +int characteristics(); + +// 基于位运算判断特性集合中是否存在输入的特性 +default boolean hasCharacteristics(int characteristics) { + return (characteristics() & characteristics) == characteristics; +} +``` + +这里可以验证一下: + +```java +public class CharacteristicsCheck { + + public static void main(String[] args) { + System.out.printf("是否存在ORDERED特性:%s\n", hasCharacteristics(Spliterator.ORDERED)); + System.out.printf("是否存在SIZED特性:%s\n", hasCharacteristics(Spliterator.SIZED)); + System.out.printf("是否存在DISTINCT特性:%s\n", hasCharacteristics(Spliterator.DISTINCT)); + } + + private static int characteristics() { + return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SORTED; + } + + private static boolean hasCharacteristics(int characteristics) { + return (characteristics() & characteristics) == characteristics; + } +} + +// 控制台输出 +是否存在ORDERED特性:true +是否存在SIZED特性:true +是否存在DISTINCT特性:false +``` + +目前`Spliterator`支持的特性一共有`8`个,如下: + +| 特性 | 十六进制值 | 二进制值 | 功能 | +| :----------: | :----------: | :-------------------: | :----------------------------------------------------------: | +| `DISTINCT` | `0x00000001` | `0000 0000 0000 0001` | 去重,例如对于每对要处理的元素`(x,y)`,使用`!x.equals(y)`比较,`Spliterator`中去重实际上基于`Set`处理 | +| `ORDERED` | `0x00000010` | `0000 0000 0001 0000` | (元素)顺序处理,可以理解为`trySplit()`、`tryAdvance()`和`forEachRemaining()`方法对所有元素处理都保证一个严格的前缀顺序 | +| `SORTED` | `0x00000004` | `0000 0000 0000 0100` | 排序,元素使用`getComparator()`方法提供的`Comparator`进行排序,如果定义了`SORTED`特性,则必须定义`ORDERED`特性 | +| `SIZED` | `0x00000040` | `0000 0000 0100 0000` | (元素)预估数量,启用此特性,那么`Spliterator`拆分或者迭代之前,`estimateSize()`返回的是元素的准确数量 | +| `NONNULL` | `0x00000040` | `0000 0001 0000 0000` | (元素)非`NULL`,数据源保证`Spliterator`需要处理的元素不能为`NULL`,最常用于并发容器中的集合、队列和`Map` | +| `IMMUTABLE` | `0x00000400` | `0000 0100 0000 0000` | (元素)不可变,数据源不可被修改,也就是处理过程中元素不能被添加、替换和移除(更新属性是允许的) | +| `CONCURRENT` | `0x00001000` | `0001 0000 0000 0000` | (元素源)的修改是并发安全的,意味着多线程在数据源中添加、替换或者移除元素在不需要额外的同步条件下是并发安全的 | +| `SUBSIZED` | `0x00004000` | `0100 0000 0000 0000` | (子`Spliterator`元素)预估数量,启用此特性,意味着通过`trySplit()`方法分割出来的所有子`Spliterator`(当前`Spliterator`分割后也属于子`Spliterator`)都启用`SIZED`特性 | + +> 细心点观察可以发现:所有特性采用32位的整数存储,使用了隔1位存储的策略,位下标和特性的映射是:(0 => DISTINCT)、(3 => SORTED)、(5 => ORDERED)、(7=> SIZED)、(9 => NONNULL)、(11 => IMMUTABLE)、(13 => CONCURRENT)、(15 => SUBSIZED) + +所有特性的功能这里只概括了核心的定义,还有一些小字或者特例描述限于篇幅没有完全加上,这一点可以参考具体的源码中的`API`注释。这些特性最终会转化为`StreamOpFlag`再提供给`Stream`中的操作判断使用,由于`StreamOpFlag`会更加复杂,下文再进行详细分析。 + +## 流的实现原理以及源码分析 + +由于流的实现是高度抽象的工程代码,所以在源码阅读上会有点困难。整个体系涉及到大量的接口、类和枚举,如下图: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-5.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-5.png) + +图中的顶层类结构图描述的就是流的流水线相关类继承关系,其中`IntStream`、`LongStream`和`DoubleStream`都是特化类型,分别针对于`Integer`、`Long`和`Double`三种类型,其他引用类型构建的`Pipeline`都是`ReferencePipeline`实例,因此笔者认为,`ReferencePipeline`(引用类型流水线)是流的核心数据结构,下面会基于`ReferencePipeline`的实现做深入分析。 + +### StreamOpFlag源码分析 + +> 注意,这一小节很烧脑,也有可能是笔者的位操作不怎么熟练,这篇文章大部分时间消耗在这一小节 + +`StreamOpFlag`是一个枚举,功能是存储`Stream`和操作的标志(`Flags corresponding to characteristics of streams and operations`,下称`Stream`标志),这些标志提供给`Stream`框架用于控制、定制化和优化计算。`Stream`标志可以用于描述与流相关联的若干不同实体的特征,这些实体包括:`Stream`的源、`Stream`的中间操作(`Op`)和`Stream`的终端操作(`Terminal Op`)。但是并非所有的`Stream`标志对所有的`Stream`实体都具备意义,目前这些实体和标志映射关系如下: + +| Type(Stream Entity Type) | DISTINCT | SORTED | ORDERED | SIZED | SHORT_CIRCUIT | +| :----------------------: | :------: | :----: | :-----: | :---: | :-----------: | +| `SPLITERATOR` | 01 | 01 | 01 | 01 | 00 | +| `STREAM` | 01 | 01 | 01 | 01 | 00 | +| `OP` | 11 | 11 | 11 | 10 | 01 | +| `TERMINAL_OP` | 00 | 00 | 10 | 00 | 01 | +| `UPSTREAM_TERMINAL_OP` | 00 | 00 | 10 | 00 | 00 | + +其中: + +- 01:表示设置/注入 +- 10:表示清除 +- 11:表示保留 +- 00:表示初始化值(默认填充值),这是一个关键点,`0`值表示绝对不会是某个类型的标志 + +`StreamOpFlag`的顶部注释中还有一个表格如下: + +| - | DISTINCT | SORTED | ORDERED | SIZED | SHORT_CIRCUIT | +| :--------------------------------: | :------: | :----: | :-----: | :---: | :-----------: | +| Stream source(`Stream`的源) | Y | Y | Y | Y | N | +| Intermediate operation(中间操作) | PCI | PCI | PCI | PC | PI | +| Terminal operation(终结操作) | N | N | PC | N | PI | + +标记 `->` 含义: + +- `Y`:允许 +- `N`:非法 +- `P`:保留 +- `C`:清除 +- `I`:注入 +- 组合`PCI`:可以保留、清除或者注入 +- 组合`PC`:可以保留或者清除 +- 组合`PI`:可以保留或者注入 + +两个表格其实是在描述同一个结论,可以相互对照和理解,但是**最终实现参照于第一个表的定义**。注意一点:这里的`preserved`(`P`)表示保留的意思,如果`Stream`实体某个标志被赋值为`preserved`,意味着该实体可以使用此标志代表的特性。例如此小节第一个表格中的`OP`的`DISTINCT`、`SORTED`和`ORDERED`都赋值为`11`(`preserved`),意味着`OP`类型的实体允许使用去重、自然排序和顺序处理特性。回到源码部分,先看`StreamOpFlag`的核心属性和构造器: + +```java +enum StreamOpFlag { + + // 暂时忽略其他代码 + + // 类型枚举,Stream相关实体类型 + enum Type { + + // SPLITERATOR类型,关联所有和Spliterator相关的特性 + SPLITERATOR, + + // STREAM类型,关联所有和Stream相关的标志 + STREAM, + + // STREAM类型,关联所有和Stream中间操作相关的标志 + OP, + + // TERMINAL_OP类型,关联所有和Stream终结操作相关的标志 + TERMINAL_OP, + + // UPSTREAM_TERMINAL_OP类型,关联所有在最后一个有状态操作边界上游传播的终止操作标志 + // 这个类型的意义直译有点拗口,不过实际上在JDK11源码中,这个类型没有被流相关功能引用,暂时可以忽略 + UPSTREAM_TERMINAL_OP + } + + // 设置/注入标志的bit模式,二进制数0001,十进制数1 + private static final int SET_BITS = 0b01; + + // 清除标志的bit模式,二进制数0010,十进制数2 + private static final int CLEAR_BITS = 0b10; + + // 保留标志的bit模式,二进制数0011,十进制数3 + private static final int PRESERVE_BITS = 0b11; + + // 掩码建造器工厂方法,注意这个方法用于实例化MaskBuilder + private static MaskBuilder set(Type t) { + return new MaskBuilder(new EnumMap(Type.class)).set(t); + } + + // 私有静态内部类,掩码建造器,里面的map由上面的set(Type t)方法得知是EnumMap实例 + private static class MaskBuilder { + // Type -> SET_BITS|CLEAR_BITS|PRESERVE_BITS|0 + final Map map; + + MaskBuilder(Map map) { + this.map = map; + } + + // 设置类型和对应的掩码 + MaskBuilder mask(Type t, Integer i) { + map.put(t, i); + return this; + } + + // 对类型添加/inject + MaskBuilder set(Type t) { + return mask(t, SET_BITS); + } + + MaskBuilder clear(Type t) { + return mask(t, CLEAR_BITS); + } + + MaskBuilder setAndClear(Type t) { + return mask(t, PRESERVE_BITS); + } + + // 这里的build方法对于类型中的NULL掩码填充为0,然后把map返回 + Map build() { + for (Type t : Type.values()) { + map.putIfAbsent(t, 0b00); + } + return map; + } + } + + // 类型->掩码映射 + private final Map maskTable; + + // bit的起始偏移量,控制下面set、clear和preserve的起始偏移量 + private final int bitPosition; + + // set/inject的bit set(map),其实准确来说应该是一个表示set/inject的bit map + private final int set; + + // clear的bit set(map),其实准确来说应该是一个表示clear的bit map + private final int clear; + + // preserve的bit set(map),其实准确来说应该是一个表示preserve的bit map + private final int preserve; + + private StreamOpFlag(int position, MaskBuilder maskBuilder) { + // 这里会基于MaskBuilder初始化内部的EnumMap + this.maskTable = maskBuilder.build(); + // Two bits per flag <= 这里会把入参position放大一倍 + position *= 2; + this.bitPosition = position; + this.set = SET_BITS << position; // 设置/注入标志的bit模式左移2倍position + this.clear = CLEAR_BITS << position; // 清除标志的bit模式左移2倍position + this.preserve = PRESERVE_BITS << position; // 保留标志的bit模式左移2倍position + } + + // 省略中间一些方法 + + // 下面这些静态变量就是直接返回标志对应的set/injec、清除和保留的bit map + /** + * The bit value to set or inject {@link #DISTINCT}. + */ + static final int IS_DISTINCT = DISTINCT.set; + + /** + * The bit value to clear {@link #DISTINCT}. + */ + static final int NOT_DISTINCT = DISTINCT.clear; + + /** + * The bit value to set or inject {@link #SORTED}. + */ + static final int IS_SORTED = SORTED.set; + + /** + * The bit value to clear {@link #SORTED}. + */ + static final int NOT_SORTED = SORTED.clear; + + /** + * The bit value to set or inject {@link #ORDERED}. + */ + static final int IS_ORDERED = ORDERED.set; + + /** + * The bit value to clear {@link #ORDERED}. + */ + static final int NOT_ORDERED = ORDERED.clear; + + /** + * The bit value to set {@link #SIZED}. + */ + static final int IS_SIZED = SIZED.set; + + /** + * The bit value to clear {@link #SIZED}. + */ + static final int NOT_SIZED = SIZED.clear; + + /** + * The bit value to inject {@link #SHORT_CIRCUIT}. + */ + static final int IS_SHORT_CIRCUIT = SHORT_CIRCUIT.set; +} +``` + +又因为`StreamOpFlag`是一个枚举,一个枚举成员是一个独立的标志,而一个标志会对多个`Stream`实体类型产生作用,所以它的一个成员描述的是上面实体和标志映射关系的一个列(竖着看): + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-7.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-7.png) + +```shell +// 纵向看 +DISTINCT Flag: +maskTable: { + SPLITERATOR: 0000 0001, + STREAM: 0000 0001, + OP: 0000 0011, + TERMINAL_OP: 0000 0000, + UPSTREAM_TERMINAL_OP: 0000 0000 +} +position(input): 0 +bitPosition: 0 +set: 1 => 0000 0000 0000 0000 0000 0000 0000 0001 +clear: 2 => 0000 0000 0000 0000 0000 0000 0000 0010 +preserve: 3 => 0000 0000 0000 0000 0000 0000 0000 0011 + +SORTED Flag: +maskTable: { + SPLITERATOR: 0000 0001, + STREAM: 0000 0001, + OP: 0000 0011, + TERMINAL_OP: 0000 0000, + UPSTREAM_TERMINAL_OP: 0000 0000 +} +position(input): 1 +bitPosition: 2 +set: 4 => 0000 0000 0000 0000 0000 0000 0000 0100 +clear: 8 => 0000 0000 0000 0000 0000 0000 0000 1000 +preserve: 12 => 0000 0000 0000 0000 0000 0000 0000 1100 + +ORDERED Flag: +maskTable: { + SPLITERATOR: 0000 0001, + STREAM: 0000 0001, + OP: 0000 0011, + TERMINAL_OP: 0000 0010, + UPSTREAM_TERMINAL_OP: 0000 0010 +} +position(input): 2 +bitPosition: 4 +set: 16 => 0000 0000 0000 0000 0000 0000 0001 0000 +clear: 32 => 0000 0000 0000 0000 0000 0000 0010 0000 +preserve: 48 => 0000 0000 0000 0000 0000 0000 0011 0000 + +SIZED Flag: +maskTable: { + SPLITERATOR: 0000 0001, + STREAM: 0000 0001, + OP: 0000 0010, + TERMINAL_OP: 0000 0000, + UPSTREAM_TERMINAL_OP: 0000 0000 +} +position(input): 3 +bitPosition: 6 +set: 64 => 0000 0000 0000 0000 0000 0000 0100 0000 +clear: 128 => 0000 0000 0000 0000 0000 0000 1000 0000 +preserve: 192 => 0000 0000 0000 0000 0000 0000 1100 0000 + +SHORT_CIRCUIT Flag: +maskTable: { + SPLITERATOR: 0000 0000, + STREAM: 0000 0000, + OP: 0000 0001, + TERMINAL_OP: 0000 0001, + UPSTREAM_TERMINAL_OP: 0000 0000 +} +position(input): 12 +bitPosition: 24 +set: 16777216 => 0000 0001 0000 0000 0000 0000 0000 0000 +clear: 33554432 => 0000 0010 0000 0000 0000 0000 0000 0000 +preserve: 50331648 => 0000 0011 0000 0000 0000 0000 0000 0000 +``` + +接着就用到按位与(`&`)和按位或(`|`)的操作,假设`A = 0001`、`B = 0010`、`C = 1000`,那么: + +- `A|B = A | B = 0001 | 0010 = 0011`(按位或,`1|0=1, 0|1=1,0|0 =0,1|1=1`) +- `A&B = A & B = 0001 | 0010 = 0000`(按位与,`1|0=0, 0|1=0,0|0 =0,1|1=1`) +- `MASK = A | B | C = 0001 | 0010 | 1000 = 1011` +- 那么判断`A|B`是否包含`A`的条件为:`A == (A|B & A)` +- 那么判断`MASK`是否包含`A`的条件为:`A == MASK & A` + +这里把`StreamOpFlag`中的枚举套用进去分析: + +```java +static int DISTINCT_SET = 0b0001; +static int SORTED_CLEAR = 0b1000; + +public static void main(String[] args) throws Exception { + // 支持DISTINCT标志和不支持SORTED标志 + int flags = DISTINCT_SET | SORTED_CLEAR; + System.out.println(Integer.toBinaryString(flags)); + System.out.printf("支持DISTINCT标志:%s\n", DISTINCT_SET == (DISTINCT_SET & flags)); + System.out.printf("不支持SORTED标志:%s\n", SORTED_CLEAR == (SORTED_CLEAR & flags)); +} + +// 控制台输出 +1001 +支持DISTINCT标志:true +不支持SORTED标志:true +``` + +由于`StreamOpFlag`的修饰符是默认,不能直接使用,可以把它的代码拷贝出来修改包名验证里面的功能: + +```java +public static void main(String[] args) { + int flags = StreamOpFlag.DISTINCT.set | StreamOpFlag.SORTED.clear; + System.out.println(StreamOpFlag.DISTINCT.set == (StreamOpFlag.DISTINCT.set & flags)); + System.out.println(StreamOpFlag.SORTED.clear == (StreamOpFlag.SORTED.clear & flags)); +} + +// 输出 + +true +true +``` + +下面这些方法就是基于这些运算特性而定义的: + +```java +enum StreamOpFlag { + + // 暂时忽略其他代码 + + // 返回当前StreamOpFlag的set/inject的bit map + int set() { + return set; + } + + // 返回当前StreamOpFlag的清除的bit map + int clear() { + return clear; + } + + // 这里判断当前StreamOpFlag类型->标记映射中Stream类型的标记,如果大于0说明不是初始化状态,那么当前StreamOpFlag就是Stream相关的标志 + boolean isStreamFlag() { + return maskTable.get(Type.STREAM)> 0; + } + + // 这里就用到按位与判断输入的flags中是否设置当前StreamOpFlag(StreamOpFlag.set) + boolean isKnown(int flags) { + return (flags & preserve) == set; + } + + // 这里就用到按位与判断输入的flags中是否清除当前StreamOpFlag(StreamOpFlag.clear) + boolean isCleared(int flags) { + return (flags & preserve) == clear; + } + + // 这里就用到按位与判断输入的flags中是否保留当前StreamOpFlag(StreamOpFlag.clear) + boolean isPreserved(int flags) { + return (flags & preserve) == preserve; + } + + // 判断当前的Stream实体类型是否可以设置本标志,要求Stream实体类型的标志位为set或者preserve,按位与要大于0 + boolean canSet(Type t) { + return (maskTable.get(t) & SET_BITS)> 0; + } + + // 暂时忽略其他代码 +} +``` + +这里有个特殊操作,位运算的时候采用了`(flags & preserve)`,理由是:同一个标志中的同一个`Stream`实体类型只可能存在`set/inject`、`clear`和`preserve`的其中一种,也就是同一个`flags`中不可能同时存在`StreamOpFlag.SORTED.set`和`StreamOpFlag.SORTED.clear`,从语义上已经矛盾,而`set/inject`、`clear`和`preserve`在`bit map`中的大小(为`2`位)和位置已经是固定的,`preserve`在设计的时候为`0b11`刚好`2`位取反,因此可以特化为(这个特化也让判断更加严谨): + +```shell +(flags & set) == set => (flags & preserve) == set +(flags & clear) == clear => (flags & preserve) == clear +(flags & preserve) == preserve => (flags & preserve) == preserve +``` + +分析这么多,总的来说,就是想通过一个`32`位整数,每`2`位分别表示`3`种状态,那么一个完整的`Flags`(标志集合)一共可以表示`16`种标志(`position=[0,15]`,可以查看`API`注释,`[4,11]`和`[13,15]`的位置是未需实现或者预留的,属于`gap`)。接着分析掩码`Mask`的计算过程例子: + +```shell +// 横向看(位移动运算符优先级高于与或,例如<<的优先级比|高) +SPLITERATOR_CHARACTERISTICS_MASK: +mask(init) = 0 +mask(DISTINCT,SPLITERATOR[DISTINCT]=01,bitPosition=0) = 0000 0000 | 0000 0001 << 0 = 0000 0000 | 0000 0001 = 0000 0001 +mask(SORTED,SPLITERATOR[SORTED]=01,bitPosition=2) = 0000 0001 | 0000 0001 << 2 = 0000 0001 | 0000 0100 = 0000 0101 +mask(ORDERED,SPLITERATOR[ORDERED]=01,bitPosition=4) = 0000 0101 | 0000 0001 << 4 = 0000 0101 | 0001 0000 = 0001 0101 +mask(SIZED,SPLITERATOR[SIZED]=01,bitPosition=6) = 0001 0101 | 0000 0001 << 6 = 0001 0101 | 0100 0000 = 0101 0101 +mask(SHORT_CIRCUIT,SPLITERATOR[SHORT_CIRCUIT]=00,bitPosition=24) = 0101 0101 | 0000 0000 << 24 = 0101 0101 | 0000 0000 = 0101 0101 +mask(final) = 0000 0000 0000 0000 0000 0000 0101 0101(二进制)、85(十进制) + +STREAM_MASK: +mask(init) = 0 +mask(DISTINCT,SPLITERATOR[DISTINCT]=01,bitPosition=0) = 0000 0000 | 0000 0001 << 0 = 0000 0000 | 0000 0001 = 0000 0001 +mask(SORTED,SPLITERATOR[SORTED]=01,bitPosition=2) = 0000 0001 | 0000 0001 << 2 = 0000 0001 | 0000 0100 = 0000 0101 +mask(ORDERED,SPLITERATOR[ORDERED]=01,bitPosition=4) = 0000 0101 | 0000 0001 << 4 = 0000 0101 | 0001 0000 = 0001 0101 +mask(SIZED,SPLITERATOR[SIZED]=01,bitPosition=6) = 0001 0101 | 0000 0001 << 6 = 0001 0101 | 0100 0000 = 0101 0101 +mask(SHORT_CIRCUIT,SPLITERATOR[SHORT_CIRCUIT]=00,bitPosition=24) = 0101 0101 | 0000 0000 << 24 = 0101 0101 | 0000 0000 = 0101 0101 +mask(final) = 0000 0000 0000 0000 0000 0000 0101 0101(二进制)、85(十进制) + +OP_MASK: +mask(init) = 0 +mask(DISTINCT,SPLITERATOR[DISTINCT]=11,bitPosition=0) = 0000 0000 | 0000 0011 << 0 = 0000 0000 | 0000 0011 = 0000 0011 +mask(SORTED,SPLITERATOR[SORTED]=11,bitPosition=2) = 0000 0011 | 0000 0011 << 2 = 0000 0011 | 0000 1100 = 0000 1111 +mask(ORDERED,SPLITERATOR[ORDERED]=11,bitPosition=4) = 0000 1111 | 0000 0011 << 4 = 0000 1111 | 0011 0000 = 0011 1111 +mask(SIZED,SPLITERATOR[SIZED]=10,bitPosition=6) = 0011 1111 | 0000 0010 << 6 = 0011 1111 | 1000 0000 = 1011 1111 +mask(SHORT_CIRCUIT,SPLITERATOR[SHORT_CIRCUIT]=01,bitPosition=24) = 1011 1111 | 0000 0001 << 24 = 1011 1111 | 0100 0000 0000 0000 0000 0000 0000 = 0100 0000 0000 0000 0000 1011 1111 +mask(final) = 0000 0000 1000 0000 0000 0000 1011 1111(二进制)、16777407(十进制) + +TERMINAL_OP_MASK: +mask(init) = 0 +mask(DISTINCT,SPLITERATOR[DISTINCT]=00,bitPosition=0) = 0000 0000 | 0000 0000 << 0 = 0000 0000 | 0000 0000 = 0000 0000 +mask(SORTED,SPLITERATOR[SORTED]=00,bitPosition=2) = 0000 0000 | 0000 0000 << 2 = 0000 0000 | 0000 0000 = 0000 0000 +mask(ORDERED,SPLITERATOR[ORDERED]=10,bitPosition=4) = 0000 0000 | 0000 0010 << 4 = 0000 0000 | 0010 0000 = 0010 0000 +mask(SIZED,SPLITERATOR[SIZED]=00,bitPosition=6) = 0010 0000 | 0000 0000 << 6 = 0010 0000 | 0000 0000 = 0010 0000 +mask(SHORT_CIRCUIT,SPLITERATOR[SHORT_CIRCUIT]=01,bitPosition=24) = 0010 0000 | 0000 0001 << 24 = 0010 0000 | 0001 0000 0000 0000 0000 0000 0000 = 0001 0000 0000 0000 0000 0010 0000 +mask(final) = 0000 0001 0000 0000 0000 0000 0010 0000(二进制)、16777248(十进制) + +UPSTREAM_TERMINAL_OP_MASK: +mask(init) = 0 +mask(DISTINCT,SPLITERATOR[DISTINCT]=00,bitPosition=0) = 0000 0000 | 0000 0000 << 0 = 0000 0000 | 0000 0000 = 0000 0000 +mask(SORTED,SPLITERATOR[SORTED]=00,bitPosition=2) = 0000 0000 | 0000 0000 << 2 = 0000 0000 | 0000 0000 = 0000 0000 +mask(ORDERED,SPLITERATOR[ORDERED]=10,bitPosition=4) = 0000 0000 | 0000 0010 << 4 = 0000 0000 | 0010 0000 = 0010 0000 +mask(SIZED,SPLITERATOR[SIZED]=00,bitPosition=6) = 0010 0000 | 0000 0000 << 6 = 0010 0000 | 0000 0000 = 0010 0000 +mask(SHORT_CIRCUIT,SPLITERATOR[SHORT_CIRCUIT]=00,bitPosition=24) = 0010 0000 | 0000 0000 << 24 = 0010 0000 | 0000 0000 = 0010 0000 +mask(final) = 0000 0000 0000 0000 0000 0000 0010 0000(二进制)、32(十进制) +``` + +相关的方法和属性如下: + +```java +enum StreamOpFlag { + + // SPLITERATOR类型的标志bit map + static final int SPLITERATOR_CHARACTERISTICS_MASK = createMask(Type.SPLITERATOR); + + // STREAM类型的标志bit map + static final int STREAM_MASK = createMask(Type.STREAM); + + // OP类型的标志bit map + static final int OP_MASK = createMask(Type.OP); + + // TERMINAL_OP类型的标志bit map + static final int TERMINAL_OP_MASK = createMask(Type.TERMINAL_OP); + + // UPSTREAM_TERMINAL_OP类型的标志bit map + static final int UPSTREAM_TERMINAL_OP_MASK = createMask(Type.UPSTREAM_TERMINAL_OP); + + // 基于Stream类型,创建对应类型填充所有标志的bit map + private static int createMask(Type t) { + int mask = 0; + for (StreamOpFlag flag : StreamOpFlag.values()) { + mask |= flag.maskTable.get(t) << flag.bitPosition; + } + return mask; + } + + // 构造一个标志本身的掩码,就是所有标志都采用保留位表示,目前作为flags == 0时候的初始值 + private static final int FLAG_MASK = createFlagMask(); + + // 构造一个包含全部标志中的preserve位的bit map,按照目前来看是暂时是一个固定值,二进制表示为0011 0000 0000 0000 0000 1111 1111 + private static int createFlagMask() { + int mask = 0; + for (StreamOpFlag flag : StreamOpFlag.values()) { + mask |= flag.preserve; + } + return mask; + } + + // 构造一个Stream类型包含全部标志中的set位的bit map,这里直接使用了STREAM_MASK,按照目前来看是暂时是一个固定值,二进制表示为0000 0000 0000 0000 0000 0000 0101 0101 + private static final int FLAG_MASK_IS = STREAM_MASK; + + // 构造一个Stream类型包含全部标志中的clear位的bit map,按照目前来看是暂时是一个固定值,二进制表示为0000 0000 0000 0000 0000 0000 1010 1010 + private static final int FLAG_MASK_NOT = STREAM_MASK << 1; + + // 初始化操作的标志bit map,目前来看就是Stream的头节点初始化时候需要合并在flags里面的初始化值,照目前来看是暂时是一个固定值,二进制表示为0000 0000 0000 0000 0000 0000 1111 1111 + static final int INITIAL_OPS_VALUE = FLAG_MASK_IS | FLAG_MASK_NOT; +} +``` + +`SPLITERATOR_CHARACTERISTICS_MASK`等`5`个成员(见上面的`Mask`计算例子)其实就是预先计算好对应的`Stream`实体类型的**所有`StreamOpFlag`标志**的`bit map`,也就是之前那个展示`Stream`的类型和标志的映射图的"横向"展示: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-8.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-8.png) + +前面的分析已经相对详细,过程非常复杂,但是更复杂的`Mask`应用还在后面的方法。`Mask`的初始化就是提供给标志的合并(`combine`)和转化(从`Spliterator`中的`characteristics`转化为`flags`)操作的,见下面的方法: + +```java +enum StreamOpFlag { + + // 这个方法完全没有注释,只使用在下面的combineOpFlags()方法中 + // 从源码来看 + // 入参flags == 0的时候,那么直接返回0011 0000 0000 0000 0000 1111 1111 + // 入参flags != 0的时候,那么会把当前flags的所有set/inject、clear和preserve所在位在bit map中全部置为0,然后其他位全部置为1 + private static int getMask(int flags) { + return (flags == 0) + ? FLAG_MASK + : ~(flags | ((FLAG_MASK_IS & flags) << 1) | ((FLAG_MASK_NOT & flags)>> 1)); + } + + // 合并新的flags和前一个flags,这里还是用到老套路先和Mask按位与,再进行一次按位或 + // 作为Stream的头节点的时候,prevCombOpFlags必须为INITIAL_OPS_VALUE + static int combineOpFlags(int newStreamOrOpFlags, int prevCombOpFlags) { + // 0x01 or 0x10 nibbles are transformed to 0x11 + // 0x00 nibbles remain unchanged + // Then all the bits are flipped + // Then the result is logically or'ed with the operation flags. + return (prevCombOpFlags & StreamOpFlag.getMask(newStreamOrOpFlags)) | newStreamOrOpFlags; + } + + // 通过合并后的flags,转换出Stream类型的flags + static int toStreamFlags(int combOpFlags) { + // By flipping the nibbles 0x11 become 0x00 and 0x01 become 0x10 + // Shift left 1 to restore set flags and mask off anything other than the set flags + return ((~combOpFlags)>> 1) & FLAG_MASK_IS & combOpFlags; + } + + // Stream的标志转换为Spliterator的characteristics + static int toCharacteristics(int streamFlags) { + return streamFlags & SPLITERATOR_CHARACTERISTICS_MASK; + } + + // Spliterator的characteristics转换为Stream的标志,入参是Spliterator实例 + static int fromCharacteristics(Spliterator spliterator) { + int characteristics = spliterator.characteristics(); + if ((characteristics & Spliterator.SORTED) != 0 && spliterator.getComparator() != null) { + // Do not propagate the SORTED characteristic if it does not correspond + // to a natural sort order + return characteristics & SPLITERATOR_CHARACTERISTICS_MASK & ~Spliterator.SORTED; + } + else { + return characteristics & SPLITERATOR_CHARACTERISTICS_MASK; + } + } + + // Spliterator的characteristics转换为Stream的标志,入参是Spliterator的characteristics + static int fromCharacteristics(int characteristics) { + return characteristics & SPLITERATOR_CHARACTERISTICS_MASK; + } +} +``` + +这里的位运算很复杂,只展示简单的计算结果和相关功能: + +- `combineOpFlags()`:用于合并新的`flags`和上一个`flags`,因为`Stream`的数据结构是一个`Pipeline`,后继节点需要合并前驱节点的`flags`,例如前驱节点`flags`是`ORDERED.set`,当前新加入`Pipeline`的节点(后继节点)的新`flags`为`SIZED.set`,那么在后继节点中应该合并前驱节点的标志,简单想象为`SIZED.set | ORDERED.set`,如果是头节点,那么初始化头节点时候的`flags`要合并`INITIAL_OPS_VALUE`,这里举个例子: + +```java +int left = ORDERED.set | DISTINCT.set; +int right = SIZED.clear | SORTED.clear; +System.out.println("left:" + Integer.toBinaryString(left)); +System.out.println("right:" + Integer.toBinaryString(right)); +System.out.println("right mask:" + Integer.toBinaryString(getMask(right))); +System.out.println("combine:" + Integer.toBinaryString(combineOpFlags(right, left))); + +// 输出结果 +left:1010001 +right:10001000 +right mask:11111111111111111111111100110011 +combine:10011001 +``` + +- `characteristics`的转化问题:`Spliterator`中的`characteristics`可以通过简单的按位与转换为`flags`的原因是`Spliterator`中的`characteristics`在设计时候本身就是和`StreamOpFlag`匹配的,准确来说就是`bit map`的位分布是匹配的,所以直接与`SPLITERATOR_CHARACTERISTICS_MASK`做按位与即可,见下面的例子: + +```shell +// 这里简单点只展示8 bit +SPLITERATOR_CHARACTERISTICS_MASK: 0101 0101 +Spliterator.ORDERED: 0001 0000 +StreamOpFlag.ORDERED.set: 0001 0000 +``` + +至此,已经分析完`StreamOpFlag`的完整实现,`Mask`相关的方法限于篇幅就不打算详细展开,下面会开始分析`Stream`中的"流水线"结构实现,因为习惯问题,下文的"标志"和"特性"两个词语会混用。 + +## ReferencePipeline源码分析 + +既然`Stream`具备流的特性,那么就需要一个链式数据结构,让元素能够从`Source`一直往下"流动"和传递到每一个链节点,实现这种场景的常用数据结构就是双向链表(考虑需要回溯,单向链表不太合适),目前比较著名的实现有`AQS`和`Netty`中的`ChannelHandlerContext`。例如`Netty`中的流水线`ChannelPipeline`设计如下: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-6.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-6.png) + +对于这个双向链表的数据结构,`Stream`中对应的类就是`AbstractPipeline`,核心实现类在`ReferencePipeline`和`ReferencePipeline`的内部类。 + +### 主要接口 + +先简单展示`AbstractPipeline`的核心父类方法定义,主要接父类是`Stream`、`BaseStream`和`PipelineHelper`: + +- `Stream`代表一个支持串行和并行聚合操作集合的元素序列,此顶层接口提供了流中间操作、终结操作和一些静态工厂方法的定义(由于方法太多,这里不全部列举),这个接口本质是一个建造器类型接口(对接中间操作来说),可以构成一个多中间操作,单终结操作的链,例如: + +```java +public interface Stream extends BaseStream> { + + // 忽略其他代码 + + // 过滤Op + Stream filter(Predicate predicate); + + // 映射Op + Stream map(Function mapper); + + // 终结操作 - 遍历 + void forEach(Consumer action); + + // 忽略其他代码 +} + +// init +Stream x = buildStream(); +// chain: head -> filter(Op) -> map(Op) -> forEach(Terminal Op) +x.filter().map().forEach() +``` + +- `BaseStream`:`Stream`的基础接口,定义流的迭代器、流的等效变体(并发处理变体、同步处理变体和不支持顺序处理元素变体)、并发和同步判断以及关闭相关方法 + +```java +// T是元素类型,S是BaseStream类型 +// 流的基础接口,这里的流指定的支持同步执行和异步执行的聚合操作的元素序列 +public interface BaseStream> extends AutoCloseable { + + // 返回一个当前Stream实例中所有元素的迭代器 + // 这是一个终结操作 + Iterator iterator(); + + // 返回一个当前Stream实例中所有元素的可拆分迭代器 + Spliterator spliterator(); + + // 当前的Stream实例是否支持并发 + boolean isParallel(); + + // 返回一个等效的同步处理的Stream实例 + S sequential(); + + // 返回一个等效的并发处理的Stream实例 + S parallel(); + + // 返回一个等效的不支持StreamOpFlag.ORDERED特性的Stream实例 + // 或者说支持StreamOpFlag.NOT_ORDERED的特性,也就返回的变体Stream在处理元素的时候不需要顺序处理 + S unordered(); + + // 返回一个添加了close处理器的Stream实例,close处理器会在下面的close方法中回调 + S onClose(Runnable closeHandler); + + // 关闭当前Stream实例,回调关联本Stream的所有close处理器 + @Override + void close(); +} +``` + +- `PipelineHelper`: + +```java +abstract class PipelineHelper { + + // 获取流的流水线的数据源的"形状",其实就是数据源元素的类型 + // 主要有四种类型:REFERENCE(除了int、long和double之外的引用类型)、INT_VALUE、LONG_VALUE和DOUBLE_VALUE + abstract StreamShape getSourceShape(); + + // 获取合并流和流操作的标志,合并的标志包括流的数据源标志、中间操作标志和终结操作标志 + // 从实现上看是当前流管道节点合并前面所有节点和自身节点标志的所有标志 + abstract int getStreamAndOpFlags(); + + // 如果当前的流管道节点的合并标志集合支持SIZED,则调用Spliterator.getExactSizeIfKnown()返回数据源中的准确元素数量,否则返回-1 + abstract long exactOutputSizeIfKnown(Spliterator spliterator); + + // 相当于调用下面的方法组合:copyInto(wrapSink(sink), spliterator) + abstract> S wrapAndCopyInto(S sink, Spliterator spliterator); + + // 发送所有来自Spliterator中的元素到Sink中,如果支持SHORT_CIRCUIT标志,则会调用copyIntoWithCancel + abstract void copyInto(Sink wrappedSink, Spliterator spliterator); + + // 发送所有来自Spliterator中的元素到Sink中,Sink处理完每个元素后会检查Sink#cancellationRequested()方法的状态去判断是否中断推送元素的操作 + abstract boolean copyIntoWithCancel(Sink wrappedSink, Spliterator spliterator); + + // 创建接收元素类型为P_IN的Sink实例,实现PipelineHelper中描述的所有中间操作,用这个Sink去包装传入的Sink实例(传入的Sink实例的元素类型为PipelineHelper的输出类型P_OUT) + abstract Sink wrapSink(Sink sink); + + // 包装传入的spliterator,从源码来看,在Stream链的头节点调用会直接返回传入的实例,如果在非头节点调用会委托到StreamSpliterators.WrappingSpliterator()方法进行包装 + // 这个方法在源码中没有API注释 + abstract Spliterator wrapSpliterator(Spliterator spliterator); + + // 构造一个兼容当前Stream元素"形状"的Node.Builder实例 + // 从源码来看直接委托到Nodes.builder()方法 + abstract Node.Builder makeNodeBuilder(long exactSizeIfKnown, + IntFunction generator); + + // Stream流水线所有阶段(节点)应用于数据源Spliterator,输出的元素作为结果收集起来转化为Node实例 + // 此方法应用于toArray()方法的计算,本质上是一个终结操作 + abstract Node evaluate(Spliterator spliterator, + boolean flatten, + IntFunction generator); +} +``` + +注意一点(重复`3`次): + +- 这里把同步流称为同步处理|执行的流,"并行流"称为并发处理|执行的流,因为并行流有歧义,**实际上只是并发执行,不是并行执行** +- 这里把同步流称为同步处理|执行的流,"并行流"称为并发处理|执行的流,因为并行流有歧义,**实际上只是并发执行,不是并行执行** +- 这里把同步流称为同步处理|执行的流,"并行流"称为并发处理|执行的流,因为并行流有歧义,**实际上只是并发执行,不是并行执行** + +### Sink和引用类型链 + +`PipelineHelper`的几个方法中存在`Sink`这个接口,上一节没有分析,这一小节会详细展开。`Stream`在构建的时候虽然是一个双向链表的结构,但是在最终应用终结操作的时候,会把所有操作转化为引用类型链(`ChainedReference`),记得之前也提到过这种类似于多层包装器的编程模式,简化一下模型如下: + +```java +public class WrapperApp { + + interface Wrapper { + + void doAction(); + } + + public static void main(String[] args) { + AtomicInteger counter = new AtomicInteger(0); + Wrapper first = () -> System.out.printf("wrapper [depth => %d] invoke\n", counter.incrementAndGet()); + Wrapper second = () -> { + first.doAction(); + System.out.printf("wrapper [depth => %d] invoke\n", counter.incrementAndGet()); + }; + second.doAction(); + } +} + +// 控制台输出 +wrapper [depth => 1] invoke +wrapper [depth => 2] invoke +``` + +上面的例子有点突兀,两个不同`Sink`的实现可以做到无感知融合,举另一个例子如下: + +```java +public interface Sink extends Consumer { + + default void begin(long size) { + + } + + default void end() { + + } + + abstract class ChainedReference implements Sink { + + protected final Sink downstream; + + public ChainedReference(Sink downstream) { + this.downstream = downstream; + } + } +} + +@SuppressWarnings({"unchecked", "rawtypes"}) +public class ReferenceChain { + + /** + * sink chain + */ + private final List>> sinkBuilders = new ArrayList(); + + /** + * current sink + */ + private final AtomicReference sinkReference = new AtomicReference(); + + public ReferenceChain filter(Predicate predicate) { + //filter + sinkBuilders.add(() -> { + Sink prevSink = (Sink) sinkReference.get(); + Sink.ChainedReference currentSink = new Sink.ChainedReference(prevSink) { + + @Override + public void accept(OUT out) { + if (predicate.test(out)) { + downstream.accept(out); + } + } + }; + sinkReference.set(currentSink); + return currentSink; + }); + return this; + } + + public ReferenceChain map(Function function) { + // map + sinkBuilders.add(() -> { + Sink prevSink = (Sink) sinkReference.get(); + Sink.ChainedReference currentSink = new Sink.ChainedReference(prevSink) { + + @Override + public void accept(OUT in) { + downstream.accept(function.apply(in)); + } + }; + sinkReference.set(currentSink); + return currentSink; + }); + return this; + } + + public void forEachPrint(Collection collection) { + forEachPrint(collection, false); + } + + public void forEachPrint(Collection collection, boolean reverse) { + Spliterator spliterator = collection.spliterator(); + // 这个是类似于terminal op + Sink sink = System.out::println; + sinkReference.set(sink); + Sink stage = sink; + // 反向包装 -> 正向遍历 + if (reverse) { + for (int i = 0; i <= sinkBuilders.size() - 1; i++) { + Supplier> supplier = sinkBuilders.get(i); + stage = (Sink) supplier.get(); + } + } else { + // 正向包装 -> 反向遍历 + for (int i = sinkBuilders.size() - 1; i>= 0; i--) { + Supplier> supplier = sinkBuilders.get(i); + stage = (Sink) supplier.get(); + } + } + Sink finalStage = stage; + spliterator.forEachRemaining(finalStage); + } + + public static void main(String[] args) { + List list = new ArrayList(); + list.add(1); + list.add(2); + list.add(3); + list.add(12); + ReferenceChain chain = new ReferenceChain(); + // filter -> map -> for each + chain.filter(item -> item> 10) + .map(item -> item * 2) + .forEachPrint(list); + } +} + +// 输出结果 +24 +``` + +执行的流程如下: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-9.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-9.png) + +多层包装器的编程模式的核心要领就是: + +- 绝大部分操作可以转换为`java.util.function.Consumer`的实现,也就是实现`accept(T t)`方法完成对传入的元素进行处理 +- 先处理的`Sink`总是以后处理的`Sink`为入参,在自身处理方法中判断和回调传入的`Sink`的处理方法回调,也就是构建引用链的时候,需要从后往前构建,这种方式的实现逻辑可以参考`AbstractPipeline#wrapSink()`,例如: + +```java +// 目标顺序:filter -> map +Sink mapSink = new Sink(inputSink){ + + private Function mapper; + + public void accept(E ele) { + inputSink.accept(mapper.apply(ele)) + } + +} +Sink filterSink = new Sink(mapSink){ + + private Predicate predicate; + + public void accept(E ele) { + if(predicate.test(ele)){ + mapSink.accept(ele); + } + } +} +``` + +- 由上一点得知,一般来说,最后的终结操作会应用在引用链的第一个`Sink`上 + +上面的代码并非笔者虚构出来,可见`java.util.stream.Sink`的源码: + +```java +// 继承自Consumer,主要是继承函数式接口方法void accept(T t) +interface Sink extends Consumer { + + // 重置当前Sink的状态(为了接收一个新的数据集),传入的size是推送到downstream的准确数据量,无法评估数据量则传入-1 + default void begin(long size) {} + + // + default void end() {} + + // 返回true的时候表示当前的Sink不会接收数据 + default boolean cancellationRequested() { + return false; + } + + // 特化方法,接受一个int类型的值 + default void accept(int value) { + throw new IllegalStateException("called wrong accept method"); + } + + // 特化方法,接受一个long类型的值 + default void accept(long value) { + throw new IllegalStateException("called wrong accept method"); + } + + // 特化方法,接受一个double类型的值 + default void accept(double value) { + throw new IllegalStateException("called wrong accept method"); + } + + // 引用类型链,准确来说是Sink链 + abstract static class ChainedReference implements Sink { + + // 下一个Sink + protected final Sink downstream; + + public ChainedReference(Sink downstream) { + this.downstream = Objects.requireNonNull(downstream); + } + + @Override + public void begin(long size) { + downstream.begin(size); + } + + @Override + public void end() { + downstream.end(); + } + + @Override + public boolean cancellationRequested() { + return downstream.cancellationRequested(); + } + } + // 暂时忽略Int、Long、Double的特化类型场景 +} +``` + +如果用过`RxJava`或者`Project-Reactor`,`Sink`更像是`Subscriber`,多个`Subscriber`组成了`ChainedReference`(`Sink Chain`,可以理解为一个复合的`Subscriber`),而`Terminal Op`则类似于`Publisher`,只有在`Subscriber`订阅`Publisher`的时候才会进行数据的处理,这里是应用了`Reactive`编程模式。 + +### AbstractPipeline和ReferencePipeline的实现 + +`AbstractPipeline`和`ReferencePipeline`都是抽象类,`AbstractPipeline`用于构建`Pipeline`的数据结构,提供一些`Shape`相关的抽象方法给`ReferencePipeline`实现,而`ReferencePipeline`就是`Stream`中`Pipeline`的基础类型,从源码上看,`Stream`链式(管道式)结构的头节点和操作节点都是`ReferencePipeline`的子类。先看`AbstractPipeline`的成员变量和构造函数: + +```java +abstract class AbstractPipeline> + extends PipelineHelper implements BaseStream { + + // 流管道链式结构的头节点(只有当前的AbstractPipeline引用是头节点,此变量才会被赋值,非头节点为NULL) + @SuppressWarnings("rawtypes") + private final AbstractPipeline sourceStage; + + // 流管道链式结构的upstream,也就是上一个节点,如果是头节点此引用为NULL + @SuppressWarnings("rawtypes") + private final AbstractPipeline previousStage; + + // 合并数据源的标志和操作标志的掩码 + protected final int sourceOrOpFlags; + + // 流管道链式结构的下一个节点,如果是头节点此引用为NULL + @SuppressWarnings("rawtypes") + private AbstractPipeline nextStage; + + // 流的深度 + // 串行执行的流中,表示当前流管道实例中中间操作节点的个数(除去头节点和终结操作) + // 并发执行的流中,表示当前流管道实例中中间操作节点和前一个有状态操作节点之间的节点个数 + private int depth; + + // 合并了所有数据源的标志、操作标志和当前的节点(AbstractPipeline)实例的标志,也就是当前的节点可以基于此属性得知所有支持的标志 + private int combinedFlags; + + // 数据源的Spliterator实例 + private Spliterator sourceSpliterator; + + // 数据源的Spliterator实例封装的Supplier实例 + private Supplier> sourceSupplier; + + // 标记当前的流节点是否被连接或者消费掉,不能重复连接或者重复消费 + private boolean linkedOrConsumed; + + // 标记当前的流管道链式结构中是否存在有状态的操作节点,这个属性只会在头节点中有效 + private boolean sourceAnyStateful; + + // 数据源关闭动作,这个属性只会在头节点中有效,由sourceStage持有 + private Runnable sourceCloseAction; + + // 标记当前流是否并发执行 + private boolean parallel; + + // 流管道结构头节点的父构造方法,使用数据源的Spliterator实例封装的Supplier实例 + AbstractPipeline(Supplier> source, + int sourceFlags, boolean parallel) { + // 头节点的前驱节点置为NULL + this.previousStage = null; + this.sourceSupplier = source; + this.sourceStage = this; + // 合并传入的源标志和流标志的掩码 + this.sourceOrOpFlags = sourceFlags & StreamOpFlag.STREAM_MASK; + // The following is an optimization of: + // StreamOpFlag.combineOpFlags(sourceOrOpFlags, StreamOpFlag.INITIAL_OPS_VALUE); + // 初始化合并标志集合为sourceOrOpFlags和所有流操作标志的初始化值 + this.combinedFlags = (~(sourceOrOpFlags << 1)) & StreamOpFlag.INITIAL_OPS_VALUE; + // 深度设置为0 + this.depth = 0; + this.parallel = parallel; + } + + // 流管道结构头节点的父构造方法,使用数据源的Spliterator实例 + AbstractPipeline(Spliterator source, + int sourceFlags, boolean parallel) { + // 头节点的前驱节点置为NULL + this.previousStage = null; + this.sourceSpliterator = source; + this.sourceStage = this; + // 合并传入的源标志和流标志的掩码 + this.sourceOrOpFlags = sourceFlags & StreamOpFlag.STREAM_MASK; + // The following is an optimization of: + // StreamOpFlag.combineOpFlags(sourceOrOpFlags, StreamOpFlag.INITIAL_OPS_VALUE); + // 初始化合并标志集合为sourceOrOpFlags和所有流操作标志的初始化值 + this.combinedFlags = (~(sourceOrOpFlags << 1)) & StreamOpFlag.INITIAL_OPS_VALUE; + this.depth = 0; + this.parallel = parallel; + } + + // 流管道结构中间操作节点的父构造方法 + AbstractPipeline(AbstractPipeline previousStage, int opFlags) { + if (previousStage.linkedOrConsumed) + throw new IllegalStateException(MSG_STREAM_LINKED); + previousStage.linkedOrConsumed = true; + // 设置前驱节点的后继节点引用为当前的AbstractPipeline实例 + previousStage.nextStage = this; + // 设置前驱节点引用为传入的前驱节点实例 + this.previousStage = previousStage; + // 合并传入的中间操作标志和流操作标志的掩码 + this.sourceOrOpFlags = opFlags & StreamOpFlag.OP_MASK; + // 合并标志集合为传入的标志和前驱节点的标志集合 + this.combinedFlags = StreamOpFlag.combineOpFlags(opFlags, previousStage.combinedFlags); + // 赋值sourceStage为前驱节点的sourceStage + this.sourceStage = previousStage.sourceStage; + if (opIsStateful()) + // 标记当前的流存在有状态操作 + sourceStage.sourceAnyStateful = true; + // 深度设置为前驱节点深度加1 + this.depth = previousStage.depth + 1; + } + + // 省略其他方法 +} +``` + +至此,可以看出流管道的数据结构: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-10.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-10.png) + +`Terminal Op`不参与管道链式结构的构建。接着看`AbstractPipeline`中的终结求值方法(`Terminal evaluation methods`): + +```java +abstract class AbstractPipeline> + extends PipelineHelper implements BaseStream { + + // 省略其他方法 + + // 基于终结操作进行求值,这个是Stream执行的常用核心方法,常用于collect()这类终结操作 + final R evaluate(TerminalOp terminalOp) { + assert getOutputShape() == terminalOp.inputShape(); + // 判断linkedOrConsumed,以防多次终结求值,也就是每个终结操作只能执行一次 + if (linkedOrConsumed) + throw new IllegalStateException(MSG_STREAM_LINKED); + linkedOrConsumed = true; + + // 如果当前流支持并发执行,则委托到TerminalOp.evaluateParallel(),如果当前流只支持同步执行,则委托到TerminalOp.evaluateSequential() + // 这里注意传入到TerminalOp中的方法参数分别是this(PipelineHelper类型)和数据源Spliterator + return isParallel() + ? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags())) + : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags())); + } + + // 基于当前的流实例转换为最终的Node实例,传入的IntFunction用于创建数组实例 + // 此终结方法一般用于toArray()这类终结操作 + @SuppressWarnings("unchecked") + final Node evaluateToArrayNode(IntFunction generator) { + if (linkedOrConsumed) + throw new IllegalStateException(MSG_STREAM_LINKED); + linkedOrConsumed = true; + + // If the last intermediate operation is stateful then + // evaluate directly to avoid an extra collection step + // 当前流支持并发执行,并且最后一个中间操作是有状态,则委托到opEvaluateParallel(),否则委托到evaluate(),这两个都是AbstractPipeline中的方法 + if (isParallel() && previousStage != null && opIsStateful()) { + // Set the depth of this, last, pipeline stage to zero to slice the + // pipeline such that this operation will not be included in the + // upstream slice and upstream operations will not be included + // in this slice + depth = 0; + return opEvaluateParallel(previousStage, previousStage.sourceSpliterator(0), generator); + } + else { + return evaluate(sourceSpliterator(0), true, generator); + } + } + + // 这个方法比较简单,就是获取当前流的数据源所在的Spliterator,并且确保流已经消费,一般用于forEach()这类终结操作 + final Spliterator sourceStageSpliterator() { + if (this != sourceStage) + throw new IllegalStateException(); + + if (linkedOrConsumed) + throw new IllegalStateException(MSG_STREAM_LINKED); + linkedOrConsumed = true; + + if (sourceStage.sourceSpliterator != null) { + @SuppressWarnings("unchecked") + Spliterator s = sourceStage.sourceSpliterator; + sourceStage.sourceSpliterator = null; + return s; + } + else if (sourceStage.sourceSupplier != null) { + @SuppressWarnings("unchecked") + Spliterator s = (Spliterator) sourceStage.sourceSupplier.get(); + sourceStage.sourceSupplier = null; + return s; + } + else { + throw new IllegalStateException(MSG_CONSUMED); + } + } + // 省略其他方法 +} +``` + +`AbstractPipeline`中实现了`BaseStream`的方法: + +```java +abstract class AbstractPipeline> + extends PipelineHelper implements BaseStream { + + // 省略其他方法 + + // 设置头节点的parallel属性为false,返回自身实例,表示当前的流是同步执行的 + @Override + @SuppressWarnings("unchecked") + public final S sequential() { + sourceStage.parallel = false; + return (S) this; + } + + // 设置头节点的parallel属性为true,返回自身实例,表示当前的流是并发执行的 + @Override + @SuppressWarnings("unchecked") + public final S parallel() { + sourceStage.parallel = true; + return (S) this; + } + + // 流关闭操作,设置linkedOrConsumed为true,数据源的Spliterator相关引用置为NULL,置空并且回调sourceCloseAction钩子实例 + @Override + public void close() { + linkedOrConsumed = true; + sourceSupplier = null; + sourceSpliterator = null; + if (sourceStage.sourceCloseAction != null) { + Runnable closeAction = sourceStage.sourceCloseAction; + sourceStage.sourceCloseAction = null; + closeAction.run(); + } + } + + // 返回一个添加了close处理器的Stream实例,close处理器会在下面的close方法中回调 + // 如果本来持有的引用sourceStage.sourceCloseAction非空,会使用传入的closeHandler与sourceStage.sourceCloseAction进行合并 + @Override + @SuppressWarnings("unchecked") + public S onClose(Runnable closeHandler) { + if (linkedOrConsumed) + throw new IllegalStateException(MSG_STREAM_LINKED); + Objects.requireNonNull(closeHandler); + Runnable existingHandler = sourceStage.sourceCloseAction; + sourceStage.sourceCloseAction = + (existingHandler == null) + ? closeHandler + : Streams.composeWithExceptions(existingHandler, closeHandler); + return (S) this; + } + + // Primitive specialization use co-variant overrides, hence is not final + // 返回当前流实例中所有元素的Spliterator实例 + @Override + @SuppressWarnings("unchecked") + public Spliterator spliterator() { + if (linkedOrConsumed) + throw new IllegalStateException(MSG_STREAM_LINKED); + // 标记当前节点被链接或者消费 + linkedOrConsumed = true; + // 如果当前节点为头节点,那么返回sourceStage.sourceSpliterator或者延时加载的sourceStage.sourceSupplier(延时加载封装由lazySpliterator实现) + if (this == sourceStage) { + if (sourceStage.sourceSpliterator != null) { + @SuppressWarnings("unchecked") + Spliterator s = (Spliterator) sourceStage.sourceSpliterator; + sourceStage.sourceSpliterator = null; + return s; + } + else if (sourceStage.sourceSupplier != null) { + @SuppressWarnings("unchecked") + Supplier> s = (Supplier>) sourceStage.sourceSupplier; + sourceStage.sourceSupplier = null; + return lazySpliterator(s); + } + else { + throw new IllegalStateException(MSG_CONSUMED); + } + } + else { + // 如果当前节点不是头节点,重新对sourceSpliterator进行包装,包装后的实例为WrappingSpliterator + return wrap(this, () -> sourceSpliterator(0), isParallel()); + } + } + + // 当前流实例是否并发执行,从头节点的parallel属性进行判断 + @Override + public final boolean isParallel() { + return sourceStage.parallel; + } + + // 从当前combinedFlags中获取数据源标志和所有流中间操作标志的集合 + final int getStreamFlags() { + return StreamOpFlag.toStreamFlags(combinedFlags); + } + + /** + * Get the source spliterator for this pipeline stage. For a sequential or + * stateless parallel pipeline, this is the source spliterator. For a + * stateful parallel pipeline, this is a spliterator describing the results + * of all computations up to and including the most recent stateful + * operation. + */ + @SuppressWarnings("unchecked") + private Spliterator sourceSpliterator(int terminalFlags) { + // 从sourceStage.sourceSpliterator或者sourceStage.sourceSupplier中获取当前流实例中的Spliterator实例,确保必定存在,否则抛出IllegalStateException + Spliterator spliterator = null; + if (sourceStage.sourceSpliterator != null) { + spliterator = sourceStage.sourceSpliterator; + sourceStage.sourceSpliterator = null; + } + else if (sourceStage.sourceSupplier != null) { + spliterator = (Spliterator) sourceStage.sourceSupplier.get(); + sourceStage.sourceSupplier = null; + } + else { + throw new IllegalStateException(MSG_CONSUMED); + } + + // 下面这段逻辑是对于并发执行并且存在有状态操作的节点,那么需要重新计算节点的深度和节点的合并标志集合 + // 这里只提一下计算过程,从头节点的后继节点开始遍历到当前节点,如果被遍历的节点时有状态的,那么对depth、combinedFlags和spliterator会进行重新计算 + // depth一旦出现有状态节点就会重置为0,然后从1重新开始增加 + // combinedFlags会重新合并sourceOrOpFlags、SHORT_CIRCUIT(如果sourceOrOpFlags支持)和Spliterator.SIZED + // spliterator简单来看就是从并发执行的toArray()=>Array数组=>Spliterator实例 + if (isParallel() && sourceStage.sourceAnyStateful) { + // Adapt the source spliterator, evaluating each stateful op + // in the pipeline up to and including this pipeline stage. + // The depth and flags of each pipeline stage are adjusted accordingly. + int depth = 1; + for (@SuppressWarnings("rawtypes") AbstractPipeline u = sourceStage, p = sourceStage.nextStage, e = this; + u != e; + u = p, p = p.nextStage) { + + int thisOpFlags = p.sourceOrOpFlags; + if (p.opIsStateful()) { + depth = 0; + + if (StreamOpFlag.SHORT_CIRCUIT.isKnown(thisOpFlags)) { + // Clear the short circuit flag for next pipeline stage + // This stage encapsulates short-circuiting, the next + // stage may not have any short-circuit operations, and + // if so spliterator.forEachRemaining should be used + // for traversal + thisOpFlags = thisOpFlags & ~StreamOpFlag.IS_SHORT_CIRCUIT; + } + + spliterator = p.opEvaluateParallelLazy(u, spliterator); + + // Inject or clear SIZED on the source pipeline stage + // based on the stage's spliterator + thisOpFlags = spliterator.hasCharacteristics(Spliterator.SIZED) + ? (thisOpFlags & ~StreamOpFlag.NOT_SIZED) | StreamOpFlag.IS_SIZED + : (thisOpFlags & ~StreamOpFlag.IS_SIZED) | StreamOpFlag.NOT_SIZED; + } + p.depth = depth++; + p.combinedFlags = StreamOpFlag.combineOpFlags(thisOpFlags, u.combinedFlags); + } + } + // 如果传入的terminalFlags标志不为0,则当前节点的combinedFlags会合并terminalFlags + if (terminalFlags != 0) { + // Apply flags from the terminal operation to last pipeline stage + combinedFlags = StreamOpFlag.combineOpFlags(terminalFlags, combinedFlags); + } + + return spliterator; + } + + // 省略其他方法 +} +``` + +`AbstractPipeline`中实现了`PipelineHelper`的方法: + +```java +abstract class AbstractPipeline> + extends PipelineHelper implements BaseStream { + + // 省略其他方法 + + // 获取数据源元素的类型,这里的类型包括引用、int、double和float + // 其实实现上就是获取depth<=0的第一个节点的输出类型 + @Override + final StreamShape getSourceShape() { + @SuppressWarnings("rawtypes") + AbstractPipeline p = AbstractPipeline.this; + while (p.depth> 0) { + p = p.previousStage; + } + return p.getOutputShape(); + } + + // 基于当前节点的标志集合判断和返回流中待处理的元素数量,无法获取则返回-1 + @Override + final long exactOutputSizeIfKnown(Spliterator spliterator) { + return StreamOpFlag.SIZED.isKnown(getStreamAndOpFlags()) ? spliterator.getExactSizeIfKnown() : -1; + } + + // 通过流管道链式结构构建元素引用链,再遍历元素引用链 + @Override + final > S wrapAndCopyInto(S sink, Spliterator spliterator) { + copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator); + return sink; + } + + // 遍历元素引用链 + @Override + final void copyInto(Sink wrappedSink, Spliterator spliterator) { + Objects.requireNonNull(wrappedSink); + // 当前节点不支持SHORT_CIRCUIT(短路)特性,则直接遍历元素引用链,不支持短路跳出 + if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) { + wrappedSink.begin(spliterator.getExactSizeIfKnown()); + spliterator.forEachRemaining(wrappedSink); + wrappedSink.end(); + } + else { + // 支持短路(中途取消)遍历元素引用链 + copyIntoWithCancel(wrappedSink, spliterator); + } + } + + // 支持短路(中途取消)遍历元素引用链 + @Override + @SuppressWarnings("unchecked") + final boolean copyIntoWithCancel(Sink wrappedSink, Spliterator spliterator) { + @SuppressWarnings({"rawtypes","unchecked"}) + AbstractPipeline p = AbstractPipeline.this; + // 基于当前节点,获取流管道链式结构中第最后一个depth=0的前驱节点 + while (p.depth> 0) { + p = p.previousStage; + } + wrappedSink.begin(spliterator.getExactSizeIfKnown()); + // 委托到forEachWithCancel()进行遍历 + boolean cancelled = p.forEachWithCancel(spliterator, wrappedSink); + wrappedSink.end(); + return cancelled; + } + + // 返回当前节点的标志集合 + @Override + final int getStreamAndOpFlags() { + return combinedFlags; + } + + // 当前节点标志集合中是否支持ORDERED + final boolean isOrdered() { + return StreamOpFlag.ORDERED.isKnown(combinedFlags); + } + + // 构建元素引用链,生成一个多重包装的Sink(WrapSink),这里的逻辑可以看前面的分析章节 + @Override + @SuppressWarnings("unchecked") + final Sink wrapSink(Sink sink) { + Objects.requireNonNull(sink); + // 这里遍历的时候,总是从当前节点向前驱节点遍历,也就是传入的sink实例总是包裹在最里面一层执行 + for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth> 0; p=p.previousStage) { + sink = p.opWrapSink(p.previousStage.combinedFlags, sink); + } + return (Sink) sink; + } + + // 包装数据源的Spliterator,如果depth=0,则直接返回sourceSpliterator,否则返回的是延迟加载的WrappingSpliterator + @Override + @SuppressWarnings("unchecked") + final Spliterator wrapSpliterator(Spliterator sourceSpliterator) { + if (depth == 0) { + return (Spliterator) sourceSpliterator; + } + else { + return wrap(this, () -> sourceSpliterator, isParallel()); + } + } + + // 计算Node实例,这个方法用于toArray()方法系列,是一个终结操作,下面会另开章节详细分析 + @Override + @SuppressWarnings("unchecked") + final Node evaluate(Spliterator spliterator, + boolean flatten, + IntFunction generator) { + if (isParallel()) { + // @@@ Optimize if op of this pipeline stage is a stateful op + return evaluateToNode(this, spliterator, flatten, generator); + } + else { + Node.Builder nb = makeNodeBuilder( + exactOutputSizeIfKnown(spliterator), generator); + return wrapAndCopyInto(nb, spliterator).build(); + } + } + + // 省略其他方法 +} +``` + +`AbstractPipeline`中剩余的待如`XXYYZZPipeline`等子类实现的抽象方法: + +```java +abstract class AbstractPipeline> + extends PipelineHelper implements BaseStream { + + // 省略其他方法 + + // 获取当前流的输出"形状",REFERENCE、INT_VALUE、LONG_VALUE或者DOUBLE_VALUE + abstract StreamShape getOutputShape(); + + // 收集当前流的所有输出元素,转化为一个适配当前流输出"形状"的Node实例 + abstract Node evaluateToNode(PipelineHelper helper, + Spliterator spliterator, + boolean flattenTree, + IntFunction generator); + + // 包装Spliterator为WrappingSpliterator实例 + abstract Spliterator wrap(PipelineHelper ph, + Supplier> supplier, + boolean isParallel); + + // 包装Spliterator为DelegatingSpliterator实例 + abstract Spliterator wrap(PipelineHelper ph, + Supplier> supplier, + boolean isParallel); + // 基于Sink遍历Spliterator中的元素,支持取消操作,简单理解就是支持cancel的tryAdvance方法 + abstract boolean forEachWithCancel(Spliterator spliterator, Sink sink); + + // 返回Node的建造器实例,用于toArray方法系列 + abstract Node.Builder makeNodeBuilder(long exactSizeIfKnown, + IntFunction generator); + + // 判断当前的操作(节点)是否有状态,如果是有状态的操作,必须覆盖opEvaluateParallel方法 + abstract boolean opIsStateful(); + + // 当前操作生成的结果会作为传入的Sink实例的入参,这是一个包装Sink的过程,通俗理解就是之前提到的元素引用链添加一个新的链节点,这个方法算是流执行的一个核心方法 + abstract Sink opWrapSink(int flags, Sink sink); + + // 并发执行的操作节点求值 + Node opEvaluateParallel(PipelineHelper helper, + Spliterator spliterator, + IntFunction generator) { + throw new UnsupportedOperationException("Parallel evaluation is not supported"); + } + + // 并发执行的操作节点惰性求值 + @SuppressWarnings("unchecked") + Spliterator opEvaluateParallelLazy(PipelineHelper helper, + Spliterator spliterator) { + return opEvaluateParallel(helper, spliterator, i -> (E_OUT[]) new Object[i]).spliterator(); + } + + // 省略其他方法 +} +``` + +这里提到的抽象方法`opWrapSink()`其实就是元素引用链的添加链节点的方法,它的实现逻辑见子类,这里只考虑非特化子类`ReferencePipeline`的部分源码: + +```java +abstract class ReferencePipeline + extends AbstractPipeline> + implements Stream { + + // 构造函数,用于头节点,传入基于Supplier封装的Spliterator实例作为数据源,数据源的标志集合和是否支持并发执行的判断标记 + ReferencePipeline(Supplier> source, + int sourceFlags, boolean parallel) { + super(source, sourceFlags, parallel); + } + + // 构造函数,用于头节点,传入Spliterator实例作为数据源,数据源的标志集合和是否支持并发执行的判断标记 + ReferencePipeline(Spliterator source, + int sourceFlags, boolean parallel) { + super(source, sourceFlags, parallel); + } + + // 构造函数,用于中间节点,传入上一个流管道节点的实例(前驱节点)和当前操作节点支持的标志集合 + ReferencePipeline(AbstractPipeline upstream, int opFlags) { + super(upstream, opFlags); + } + + // 这里流的输出"形状"固定为REFERENCE + @Override + final StreamShape getOutputShape() { + return StreamShape.REFERENCE; + } + + // 转换当前流实例为Node实例,应用于toArray方法,后面详细分析终结操作的时候再展开 + @Override + final Node evaluateToNode(PipelineHelper helper, + Spliterator spliterator, + boolean flattenTree, + IntFunction generator) { + return Nodes.collect(helper, spliterator, flattenTree, generator); + } + + // 包装Spliterator=>WrappingSpliterator + @Override + final Spliterator wrap(PipelineHelper ph, + Supplier> supplier, + boolean isParallel) { + return new StreamSpliterators.WrappingSpliterator(ph, supplier, isParallel); + } + + // 包装Spliterator=>DelegatingSpliterator,实现惰性加载 + @Override + final Spliterator lazySpliterator(Supplier> supplier) { + return new StreamSpliterators.DelegatingSpliterator(supplier); + } + + // 遍历Spliterator中的元素,基于传入的Sink实例进行处理,支持Cancel操作 + @Override + final boolean forEachWithCancel(Spliterator spliterator, Sink sink) { + boolean cancelled; + do { } while (!(cancelled = sink.cancellationRequested()) && spliterator.tryAdvance(sink)); + return cancelled; + } + + // 构造Node建造器实例 + @Override + final Node.Builder makeNodeBuilder(long exactSizeIfKnown, IntFunction generator) { + return Nodes.builder(exactSizeIfKnown, generator); + } + + // 基于当前流的Spliterator生成迭代器实例 + @Override + public final Iterator iterator() { + return Spliterators.iterator(spliterator()); + } + + // 省略其他OP的代码 + + // 流管道结构的头节点 + static class Head extends ReferencePipeline { + + // 构造函数,用于头节点,传入基于Supplier封装的Spliterator实例作为数据源,数据源的标志集合和是否支持并发执行的判断标记 + Head(Supplier> source, + int sourceFlags, boolean parallel) { + super(source, sourceFlags, parallel); + } + + // 构造函数,用于头节点,传入Spliterator实例作为数据源,数据源的标志集合和是否支持并发执行的判断标记 + Head(Spliterator source, + int sourceFlags, boolean parallel) { + super(source, sourceFlags, parallel); + } + + // 不支持判断是否状态操作 + @Override + final boolean opIsStateful() { + throw new UnsupportedOperationException(); + } + + // 不支持包装Sink实例 + @Override + final Sink opWrapSink(int flags, Sink sink) { + throw new UnsupportedOperationException(); + } + + // 区分同步异步执行forEach,同步则简单理解为调用Spliterator.forEachRemaining,异步则调用终结操作forEach + @Override + public void forEach(Consumer action) { + if (!isParallel()) { + sourceStageSpliterator().forEachRemaining(action); + } + else { + super.forEach(action); + } + } + + // 区分同步异步执行forEachOrdered,同步则简单理解为调用Spliterator.forEachRemaining,异步则调用终结操作forEachOrdered + @Override + public void forEachOrdered(Consumer action) { + if (!isParallel()) { + sourceStageSpliterator().forEachRemaining(action); + } + else { + super.forEachOrdered(action); + } + } + } + + // 无状态操作节点的父类 + abstract static class StatelessOp + extends ReferencePipeline { + + // 基于上一个节点引用、输入元素"形状"和当前节点支持的标志集合创建StatelessOp实例 + StatelessOp(AbstractPipeline upstream, + StreamShape inputShape, + int opFlags) { + super(upstream, opFlags); + assert upstream.getOutputShape() == inputShape; + } + + // 操作状态标记设置为无状态 + @Override + final boolean opIsStateful() { + return false; + } + } + + // 有状态操作节点的父类 + abstract static class StatefulOp + extends ReferencePipeline { + + // 基于上一个节点引用、输入元素"形状"和当前节点支持的标志集合创建StatefulOp实例 + StatefulOp(AbstractPipeline upstream, + StreamShape inputShape, + int opFlags) { + super(upstream, opFlags); + assert upstream.getOutputShape() == inputShape; + } + + // 操作状态标记设置为有状态 + @Override + final boolean opIsStateful() { + return true; + } + + // 前面也提到,节点操作异步求值的方法在无状态节点下必须覆盖,这里重新把这个方法抽象,子类必须实现 + @Override + abstract Node opEvaluateParallel(PipelineHelper helper, + Spliterator spliterator, + IntFunction generator); + } +} +``` + +这里重重重点分析一下`ReferencePipeline`中的`wrapSink`方法实现: + +```java +final Sink wrapSink(Sink sink) { + Objects.requireNonNull(sink); + + for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth> 0; p=p.previousStage) { + sink = p.opWrapSink(p.previousStage.combinedFlags, sink); + } + return (Sink) sink; +} +``` + +入参是一个`Sink`实例,返回值也是一个`Sink`实例,里面的`for`循环是基于当前的`AbstractPipeline`节点向前遍历,直到`depth`为`0`的节点跳出循环,而`depth`为`0`意味着该节点必定为头节点,也就是该循环是遍历当前节点到头节点的后继节点,`Sink`是"向前包装的",也就是处于链后面的节点`Sink`总是会作为其前驱节点的`opWrapSink()`方法的入参,在同步执行流求值计算的时候,前驱节点的`Sink`处理完元素后就会通过`downstream`引用(其实就是后驱节点的`Sink`)调用其`accept()`把元素或者处理完的元素结果传递进去,激活下一个`Sink`,以此类推。另外,`ReferencePipeline`的三个内部类`Head`、`StatelessOp`和`StatefulOp`就是流的节点类,其中只有`Head`是非抽象类,代表流管道结构(或者说双向链表结构)的头节点,`StatelessOp`(无状态操作)和`StatefulOp`(有状态操作)的子类构成了流管道结构的操作节点或者是终结操作。在忽略是否有状态操作的前提下看`ReferencePipeline`,它只是流数据结构的承载体,表面上看到的双向链表结构在流的求值计算过程中并不会进行直接遍历每个节点进行求值,而是先转化成一个多层包装的`Sink`,也就是前文笔者提到的元素引用链后者前一句分析的`Sink`元素处理以及传递,正确来说应该是一个`Sink`栈或者`Sink`包装器,它的实现可以类比为现实生活中的洋葱,或者编程模式中的`AOP`编程模式。形象一点的描述如下: + +```shell +Head(Spliterator) -> Op(filter) -> Op(map) -> Op(sorted) -> Terminal Op(forEach) + +↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ +forEach ele in Spliterator: + Sink[filter](ele){ + if filter process == true: + Sink[map](ele){ + ele = mapper(ele) + Sink[sorted](ele){ + + var array + + begin: + + accept(ele): + add ele to array + + end: + sort ele in array + } + } + } +``` + +终结操作`forEach`是目前分析源码中最简单的实现,下面会详细分析每种终结操作的实现细节。 + +## 流中间操作的源码实现 + +限于篇幅,这里只能挑选一部分的中间`Op`进行分析。流的中间操作基本都是由`BaseStream`接口定义,在`ReferencePipeline`中进行实现,这里挑选比较常用的`filter`、`map`和`sorted`进行分析。先看`filter`: + +```java +abstract class ReferencePipeline + extends AbstractPipeline> + implements Stream { + + // 暂时省略其他代码 + + // filter操作,泛型参数Predicate类型接受一个任意类型(这里考虑到泛型擦除)的元素,输出布尔值,它是一个无状态操作 + @Override + public final Stream filter(Predicate predicate) { + Objects.requireNonNull(predicate); + // 这里注意到,StatelessOp的第一个参数是指upstream,也就是理解为上一个节点,这里使用了this,意味着upstream为当前的ReferencePipeline实例,元素"形状"为引用类型,操作标志位不支持SIZED + // 在AbstractPipeline,previousStage指向了this,当前的节点就是StatelessOp[filter]实例,那么前驱节点this的后继节点引用nextStage就指向了StatelessOp[filter]实例 + // 也就是StatelessOp[filter].previousStage = this; this.nextStage = StatelessOp[filter]; ===> 也就是这个看起来简单的new StatelessOp()其实已经把自身加入到管道中 + return new StatelessOp(this, StreamShape.REFERENCE, + StreamOpFlag.NOT_SIZED) { + @Override + Sink opWrapSink(int flags, Sink sink) { + return new Sink.ChainedReference(sink) { + @Override + public void begin(long size) { + // 这里通知下一个节点的Sink.begin(),由于filter方法不感知元素数量,所以传值-1 + downstream.begin(-1); + } + + @Override + public void accept(P_OUT u) { + // 基于输入的Predicate实例判断当前处理元素是否符合判断,只有判断结果为true才会把元素原封不动直接传递到下一个Sink + if (predicate.test(u)) + downstream.accept(u); + } + }; + } + }; + } + + // 暂时省略其他代码 +} +``` + +接着是`map`: + +```java +abstract class ReferencePipeline + extends AbstractPipeline> + implements Stream { + + // 暂时省略其他代码 + + // map操作,基于传入的Function实例做映射转换(P_OUT->R),它是一个无状态操作 + @Override + @SuppressWarnings("unchecked") + public final Stream map(Function mapper) { + Objects.requireNonNull(mapper); + // upstream为当前的ReferencePipeline实例,元素"形状"为引用类型,操作标志位不支持SORTED和DISTINCT + return new StatelessOp(this, StreamShape.REFERENCE, + StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) { + @Override + Sink opWrapSink(int flags, Sink sink) { + return new Sink.ChainedReference(sink) { + @Override + public void accept(P_OUT u) { + // 基于传入的Function实例转换元素后把转换结果传递到下一个Sink + downstream.accept(mapper.apply(u)); + } + }; + } + }; + } + + // 暂时省略其他代码 +} +``` + +然后是`sorted`,`sorted`操作会相对复杂一点: + +```java +abstract class ReferencePipeline + extends AbstractPipeline> + implements Stream { + + // 暂时省略其他代码 + + // sorted操作,基于传入的Comparator实例对处理的元素进行排序,从源码中看,它是一个有状态操作 + @Override + public final Stream sorted(Comparator comparator) { + return SortedOps.makeRef(this, comparator); + } + + // 暂时省略其他代码 +} + +// SortedOps工具类 +final class SortedOps { + + // 暂时省略其他代码 + + // 构建排序操作的链节点 + static Stream makeRef(AbstractPipeline upstream, + Comparator comparator) { + return new OfRef(upstream, comparator); + } + + // 有状态的排序操作节点 + private static final class OfRef extends ReferencePipeline.StatefulOp { + + // 是否自然排序,不定义Comparator实例的时候为true,否则为false + private final boolean isNaturalSort; + // 用于排序的Comparator实例 + private final Comparator comparator; + + // 自然排序情况下的构造方法,元素"形状"为引用类型,操作标志位不支持ORDERED和SORTED + OfRef(AbstractPipeline upstream) { + super(upstream, StreamShape.REFERENCE, + StreamOpFlag.IS_ORDERED | StreamOpFlag.IS_SORTED); + this.isNaturalSort = true; + // Comparator实例赋值为Comparator.naturalOrder(),本质是基于Object中的equals或者子类覆盖Object中的equals方法进行元素排序 + @SuppressWarnings("unchecked") + Comparator comp = (Comparator) Comparator.naturalOrder(); + this.comparator = comp; + } + + // 非自然排序情况下的构造方法,需要传入Comparator实例,元素"形状"为引用类型,操作标志位不支持ORDERED和SORTED + OfRef(AbstractPipeline upstream, Comparator comparator) { + super(upstream, StreamShape.REFERENCE, + StreamOpFlag.IS_ORDERED | StreamOpFlag.NOT_SORTED); + this.isNaturalSort = false; + this.comparator = Objects.requireNonNull(comparator); + } + + @Override + public Sink opWrapSink(int flags, Sink sink) { + Objects.requireNonNull(sink); + + // If the input is already naturally sorted and this operation + // also naturally sorted then this is a no-op + // 流中的所有元素本身已经按照自然顺序排序,并且没有定义Comparator实例,则不需要进行排序,所以no op就行 + if (StreamOpFlag.SORTED.isKnown(flags) && isNaturalSort) + return sink; + else if (StreamOpFlag.SIZED.isKnown(flags)) + // 知道要处理的元素的确切数量,使用数组进行排序 + return new SizedRefSortingSink(sink, comparator); + else + // 不知道要处理的元素的确切数量,使用ArrayList进行排序 + return new RefSortingSink(sink, comparator); + } + + // 这里是并行执行流中toArray方法的实现,暂不分析 + @Override + public Node opEvaluateParallel(PipelineHelper helper, + Spliterator spliterator, + IntFunction generator) { + // If the input is already naturally sorted and this operation + // naturally sorts then collect the output + if (StreamOpFlag.SORTED.isKnown(helper.getStreamAndOpFlags()) && isNaturalSort) { + return helper.evaluate(spliterator, false, generator); + } + else { + // @@@ Weak two-pass parallel implementation; parallel collect, parallel sort + T[] flattenedData = helper.evaluate(spliterator, true, generator).asArray(generator); + Arrays.parallelSort(flattenedData, comparator); + return Nodes.node(flattenedData); + } + } + } + + // 这里考虑到篇幅太长,SizedRefSortingSink和RefSortingSink的源码不复杂,只展开RefSortingSink进行分析 + + // 无法确认待处理元素确切数量时候用于元素排序的Sink实现 + private static final class RefSortingSink extends AbstractRefSortingSink { + + // 临时ArrayList实例 + private ArrayList list; + + // 构造函数,需要的参数为下一个Sink引用和Comparator实例 + RefSortingSink(Sink sink, Comparator comparator) { + super(sink, comparator); + } + + @Override + public void begin(long size) { + if (size>= Nodes.MAX_ARRAY_SIZE) + throw new IllegalArgumentException(Nodes.BAD_SIZE); + // 基于传入的size是否大于0,大于等于0用于作为initialCapacity构建ArrayList,小于0则构建默认initialCapacity的ArrayList,赋值到临时变量list + list = (size>= 0) ? new ArrayList((int) size) : new ArrayList(); + } + + @Override + public void end() { + // 临时的ArrayList实例基于Comparator实例进行潘旭 + list.sort(comparator); + // 下一个Sink节点的激活,区分是否支持取消操作 + downstream.begin(list.size()); + if (!cancellationRequestedCalled) { + list.forEach(downstream::accept); + } + else { + for (T t : list) { + if (downstream.cancellationRequested()) break; + downstream.accept(t); + } + } + downstream.end(); + // 激活下一个Sink完成后,临时的ArrayList实例置为NULL,便于GC回收 + list = null; + } + + @Override + public void accept(T t) { + // 当前Sink处理元素直接添加到临时的ArrayList实例 + list.add(t); + } + } + + // 暂时省略其他代码 +} +``` + +`sorted`操作有个比较显著的特点,一般的`Sink`处理完自身的逻辑,会在`accept()`方法激活下一个`Sink`引用,但是它在`accept()`方法中只做元素的累积(**元素富集**),在`end()`方法进行最终的排序操作和模仿`Spliterator`的两个元素遍历方法向`downstream`推送待处理的元素。示意图如下: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-11.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-11.png) + +其他中间操作的实现逻辑是大致相同的。 + +## 同步执行流终结操作的源码实现 + +限于篇幅,这里只能挑选一部分的`Terminal Op`进行分析,**简单起见只分析同步执行的场景**,这里挑选最典型和最复杂的`froEach()`和`collect()`,还有比较独特的`toArray()`方法。先看`froEach()`方法的实现过程: + +```java +abstract class ReferencePipeline + extends AbstractPipeline> + implements Stream { + + // 暂时省略其他代码 + + // 遍历元素 + @Override + public void forEach(Consumer action) { + evaluate(ForEachOps.makeRef(action, false)); + } + + // 暂时省略其他代码 + + // 基于终结操作的求值方法 + final R evaluate(TerminalOp terminalOp) { + assert getOutputShape() == terminalOp.inputShape(); + // 确保只会执行一次,linkedOrConsumed是流管道结构最后一个节点的属性 + if (linkedOrConsumed) + throw new IllegalStateException(MSG_STREAM_LINKED); + linkedOrConsumed = true; + // 这里暂且只分析同步执行的流的终结操作,终结操作节点的标志会合并到流最后一个节点的combinedFlags中,执行的关键就是evaluateSequential方法 + return isParallel() + ? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags())) + : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags())); + } + + // 暂时省略其他代码 +} + +// ForEachOps类,TerminalOp接口的定义比较简单,这里不展开 +final class ForEachOps { + + // 暂时省略其他代码 + + // 构造变量元素的终结操作实例,传入的元素是T类型,结果是Void类型(返回NULL,或者说是没有返回值,毕竟是一个元素遍历过程) + // 参数为一个Consumer接口实例和一个标记是否顺序处理元素的布尔值 + public static TerminalOp makeRef(Consumer action, + boolean ordered) { + Objects.requireNonNull(action); + return new ForEachOp.OfRef(action, ordered); + } + + // 遍历元素操作的终结操作实现,同时它是一个适配器,适配TerminalSink(Sink)接口 + abstract static class ForEachOp + implements TerminalOp, TerminalSink { + + // 标记是否顺序处理元素 + private final boolean ordered; + + protected ForEachOp(boolean ordered) { + this.ordered = ordered; + } + + // TerminalOp + + // 终结操作节点的标志集合,如果ordered为true则返回0,否则返回StreamOpFlag.NOT_ORDERED,表示不支持顺序处理元素 + @Override + public int getOpFlags() { + return ordered ? 0 : StreamOpFlag.NOT_ORDERED; + } + + // 同步遍历和处理元素 + @Override + public Void evaluateSequential(PipelineHelper helper, + Spliterator spliterator) { + // 以当前的ForEachOp实例作为最后一个Sink添加到Sink链(也就是前面经常说的元素引用链),然后对Sink链进行遍历 + return helper.wrapAndCopyInto(this, spliterator).get(); + } + + // 并发遍历和处理元素,这里暂不分析 + @Override + public Void evaluateParallel(PipelineHelper helper, + Spliterator spliterator) { + if (ordered) + new ForEachOrderedTask(helper, spliterator, this).invoke(); + else + new ForEachTask(helper, spliterator, helper.wrapSink(this)).invoke(); + return null; + } + + // TerminalSink + + // 实现TerminalSink的方法,实际上TerminalSink继承接口Supplier,这里是实现了Supplier接口的get()方法,由于PipelineHelper.wrapAndCopyInto()方法会返回最后一个Sink的引用,这里其实就是evaluateSequential()中的返回值 + @Override + public Void get() { + return null; + } + + // ForEachOp的静态内部类,引用类型的ForEachOp的最终实现,依赖入参遍历元素处理的最后一步回调Consumer实例 + static final class OfRef extends ForEachOp { + + // 最后的遍历回调的Consumer句柄 + final Consumer consumer; + + OfRef(Consumer consumer, boolean ordered) { + super(ordered); + this.consumer = consumer; + } + + @Override + public void accept(T t) { + // 遍历元素回调操作 + consumer.accept(t); + } + } + } +} +``` + +`forEach`终结操作实现上,自身这个操作并不会构成流的链式结构的一部分,也就是它不是一个`AbstractPipeline`的子类实例,而是构建一个回调`Consumer`实例操作的一个`Sink`实例(准确来说是`TerminalSink`)实例,这里暂且叫`forEach terminal sink`,通过流最后一个操作节点的`wrapSink()`方法,把`forEach terminal sink`添加到`Sink`链的尾部,通过流最后一个操作节点的`copyInto()`方法进行元素遍历,按照`copyInto()`方法的套路,只要多层包装的`Sink`方法在回调其实现方法的时候总是激活`downstream`的前提下,执行的顺序就是流链式结构定义的操作节点顺序,而`forEach`最后添加的`Consumer`实例一定就是最后回调的。 + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-12.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-12.png) + +接着分析`collect()`方法的实现,先看`Collector`接口的定义: + +```java +// T:需要进行reduce操作的输入元素类型 +// A:reduce操作中可变累加对象的类型,可以简单理解为累加操作中,累加到Container中的可变对象类型 +// R:reduce操作结果类型 +public interface Collector { + + // 注释中称为Container,用于承载最终结果的可变容器,而此方法的Supplier实例持有的是创建Container实例的get()方法实现,后面称为Supplier + // 也就是一般使用如:Supplier supplier = () -> new Container(); + Supplier supplier(); + + // Accumulator,翻译为累加器,用于处理值并且把处理结果传递(累加)到Container中,后面称为Accumulator + BiConsumer accumulator(); + + // Combiner,翻译为合并器,真实泛型类型为BinaryOperator,BiFunction的子类,接收两个部分的结果并且合并为一个结果,后面称为Combiner + // 这个方法可以把一个参数的状态转移到另一个参数,然后返回更新状态后的参数,例如:(arg1, arg2) -> {arg2.state = arg1.state; return arg2;} + // 可以把一个参数的状态转移到另一个参数,然后返回一个新的容器,例如:(arg1, arg2) -> {arg2.state = arg1.state; return new Container(arg2);} + BinaryOperator combiner(); + + // Finisher,直接翻译感觉意义不合理,实际上就是做最后一步转换工作的处理器,后面称为Finisher + Function finisher(); + + // Collector支持的特性集合,见枚举Characteristics + Set characteristics(); + + // 这里忽略两个Collector的静态工厂方法,因为并不常用 + + enum Characteristics { + + // 标记Collector支持并发执行,一般和并发容器相关 + CONCURRENT, + + // 标记Collector处理元素时候无序 + UNORDERED, + + // 标记Collector的输入和输出元素是同类型,也就是Finisher在实现上R -> A可以等效于A -> R,unchecked cast会成功(也就是类型强转可以成功) + // 在这种场景下,对于Container来说其实类型强制转换也是等效的,也就是Supplier和Supplier得出的Container是同一种类型的Container + IDENTITY_FINISH + } +} + +// Collector的实现Collectors.CollectorImpl +public final class Collectors { + + // 这一大堆常量就是预设的多种特性组合,CH_NOID比较特殊,是空集合,也就是Collector三种特性都不支持 + static final Set CH_CONCURRENT_ID + = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT, + Collector.Characteristics.UNORDERED, + Collector.Characteristics.IDENTITY_FINISH)); + static final Set CH_CONCURRENT_NOID + = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT, + Collector.Characteristics.UNORDERED)); + static final Set CH_ID + = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); + static final Set CH_UNORDERED_ID + = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED, + Collector.Characteristics.IDENTITY_FINISH)); + static final Set CH_NOID = Collections.emptySet(); + static final Set CH_UNORDERED_NOID + = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED)); + + private Collectors() { } + + // 省略大量代码 + + // 静态类,Collector的实现,实现其实就是Supplier、Accumulator、Combiner、Finisher和Characteristics集合的成员属性承载 + static class CollectorImpl implements Collector { + private final Supplier supplier; + private final BiConsumer accumulator; + private final BinaryOperator combiner; + private final Function finisher; + private final Set characteristics; + + CollectorImpl(Supplier supplier, + BiConsumer accumulator, + BinaryOperator combiner, + Function finisher, + Set characteristics) { + this.supplier = supplier; + this.accumulator = accumulator; + this.combiner = combiner; + this.finisher = finisher; + this.characteristics = characteristics; + } + + CollectorImpl(Supplier supplier, + BiConsumer accumulator, + BinaryOperator combiner, + Set characteristics) { + this(supplier, accumulator, combiner, castingIdentity(), characteristics); + } + + @Override + public BiConsumer accumulator() { + return accumulator; + } + + @Override + public Supplier supplier() { + return supplier; + } + + @Override + public BinaryOperator combiner() { + return combiner; + } + + @Override + public Function finisher() { + return finisher; + } + + @Override + public Set characteristics() { + return characteristics; + } + } + + // 省略大量代码 + + // IDENTITY_FINISH特性下,Finisher的实现,也就是之前提到的A->R和R->A等效,可以强转 + private static Function castingIdentity() { + return i -> (R) i; + } + + // 省略大量代码 +} +``` + +`collect()`方法的求值执行入口在`ReferencePipeline`中: + +```java +// ReferencePipeline +abstract class ReferencePipeline + extends AbstractPipeline> + implements Stream { + + // 暂时省略其他代码 + + // 基于Collector实例进行求值 + public final R collect(Collector collector) { + A container; + // 并发求值场景暂不考虑 + if (isParallel() + && (collector.characteristics().contains(Collector.Characteristics.CONCURRENT)) + && (!isOrdered() || collector.characteristics().contains(Collector.Characteristics.UNORDERED))) { + container = collector.supplier().get(); + BiConsumer accumulator = collector.accumulator(); + forEach(u -> accumulator.accept(container, u)); + } + else { + // 这里就是同步执行场景下的求值过程,这里可以看出其实所有Collector的求值都是Reduce操作 + container = evaluate(ReduceOps.makeRef(collector)); + } + // 如果Collector的Finisher输入类型和输出类型相同,所以Supplier和Supplier得出的Container是同一种类型的Container,可以直接类型转换,否则就要调用Collector中的Finisher进行最后一步处理 + return collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH) + ? (R) container + : collector.finisher().apply(container); + } + + // 暂时省略其他代码 +} + +// ReduceOps +final class ReduceOps { + + private ReduceOps() { } + + // 暂时省略其他代码 + + // 引用类型Reduce操作创建TerminalOp实例 + public static TerminalOp + makeRef(Collector collector) { + // Supplier + Supplier supplier = Objects.requireNonNull(collector).supplier(); + // Accumulator + BiConsumer accumulator = collector.accumulator(); + // Combiner + BinaryOperator combiner = collector.combiner(); + + // 这里注意一点,ReducingSink是方法makeRef中的内部类,作用域只在方法内,它是封装为TerminalOp最终转化为Sink链中最后一个Sink实例的类型 + class ReducingSink extends Box + implements AccumulatingSink { + @Override + public void begin(long size) { + // 这里把从Supplier创建的新Container实例存放在父类Box的状态属性中 + state = supplier.get(); + } + + @Override + public void accept(T t) { + // 处理元素,Accumulator处理状态(容器实例)和元素,这里可以想象,如果state为一个ArrayList实例,这里的accept()实现可能为add(ele)操作 + accumulator.accept(state, t); + } + + @Override + public void combine(ReducingSink other) { + // Combiner合并两个状态(容器实例) + state = combiner.apply(state, other.state); + } + } + return new ReduceOp(StreamShape.REFERENCE) { + @Override + public ReducingSink makeSink() { + return new ReducingSink(); + } + + @Override + public int getOpFlags() { + return collector.characteristics().contains(Collector.Characteristics.UNORDERED) + ? StreamOpFlag.NOT_ORDERED + : 0; + } + }; + } + + // 暂时省略其他代码 + + // 继承自接口TerminalSink,主要添加了combine()抽象方法,用于合并元素 + private interface AccumulatingSink> + extends TerminalSink { + void combine(K other); + } + + // 状态盒,用于持有和获取状态,状态属性的修饰符为default,包内的类实例都能修改 + private abstract static class Box { + U state; + + Box() {} // Avoid creation of special accessor + + public U get() { + return state; + } + } + + // ReduceOp的最终实现,这个就是Reduce操作终结操作的实现 + private abstract static class ReduceOp> + implements TerminalOp { + + // 流输入元素"形状" + private final StreamShape inputShape; + + ReduceOp(StreamShape shape) { + inputShape = shape; + } + + // 抽象方法,让子类生成终结操作的Sink + public abstract S makeSink(); + + // 获取流输入元素"形状" + @Override + public StreamShape inputShape() { + return inputShape; + } + + // 同步执行求值,还是相似的思路,使用wrapAndCopyInto()进行Sink链构建和元素遍历 + @Override + public R evaluateSequential(PipelineHelper helper, + Spliterator spliterator) { + // 以当前的ReduceOp实例的makeSink()返回的Sink实例作为最后一个Sink添加到Sink链(也就是前面经常说的元素引用链),然后对Sink链进行遍历 + // 这里向上一步一步推演思考,最终get()方法执行完毕拿到的结果就是ReducingSink父类Box中的state变量,也就是容器实例 + return helper.wrapAndCopyInto(makeSink(), spliterator).get(); + } + + // 异步执行求值,暂时忽略 + @Override + public R evaluateParallel(PipelineHelper helper, + Spliterator spliterator) { + return new ReduceTask(this, helper, spliterator).invoke().get(); + } + } + + // 暂时省略其他代码 +} +``` + +接着就看`Collector`的静态工厂方法,看一些常用的`Collector`实例是如何构建的,例如看`Collectors.toList()`: + +```java +// Supplier => () -> new ArrayList(); // 初始化ArrayList +// Accumulator => (list,number) -> list.add(number); // 往ArrayList中添加元素 +// Combiner => (left, right) -> { left.addAll(right); return left;} // 合并ArrayList +// Finisher => X -> X; // 输入什么就返回什么,这里实际返回的是ArrayList +public static +Collector> toList() { + return new CollectorImpl((Supplier>) ArrayList::new, List::add, + (left, right) -> { left.addAll(right); return left; }, + CH_ID); +} +``` + +把过程画成流程图如下: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-13.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-13.png) + +甚至可以更通俗地用伪代码表示`Collector`这类`Terminal Op`的执行过程(还是以`Collectors.toList()`为例): + +```java +[begin] +Supplier supplier = () -> new ArrayList(); +Container container = supplier.get(); +Box.state = container; + +[accept] +Box.state.add(element); + +[end] +return supplier.get(); (=> return Box.state); + +↓↓↓↓↓↓↓↓↓甚至更加通俗的过程如下↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + +ArrayList container = new ArrayList(); +loop: + container.add(element) +return container; +``` + +也就是虽然工程化的代码看起来很复杂,最终的实现就是简单的:初始化`ArrayList`实例由`state`属性持有,遍历处理元素的时候把元素添加到`state`中,最终返回`state`。最后看`toArray()`的方法实现(下面的方法代码没有按照实际的位置贴出,笔者把零散的代码块放在一起方便分析): + +```java +abstract class ReferencePipeline + extends AbstractPipeline> + implements Stream { + + // 暂时省略其他代码 + + // 流的所有元素转换为数组,这里的IntFunction有一种比较特殊的用法,就是用于创建数组实例 + // 例如IntFunction f = String::new; String[] arry = f.apply(2); 相当于String[] arry = new String[2]; + @Override + @SuppressWarnings("unchecked") + public final A[] toArray(IntFunction generator) { + + // 这里主动擦除了IntFunction的类型,只要保证求值的过程是正确,最终可以做类型强转 + @SuppressWarnings("rawtypes") + IntFunction rawGenerator = (IntFunction) generator; + // 委托到evaluateToArrayNode()方法进行计算 + return (A[]) Nodes.flatten(evaluateToArrayNode(rawGenerator), rawGenerator) + .asArray(rawGenerator); + } + + // 流的所有元素转换为Object数组 + @Override + public final Object[] toArray() { + return toArray(Object[]::new); + } + + // 流元素求值转换为ArrayNode + final Node evaluateToArrayNode(IntFunction generator) { + // 确保不会处理多次 + if (linkedOrConsumed) + throw new IllegalStateException(MSG_STREAM_LINKED); + linkedOrConsumed = true; + // 并发执行暂时跳过 + if (isParallel() && previousStage != null && opIsStateful()) { + depth = 0; + return opEvaluateParallel(previousStage, previousStage.sourceSpliterator(0), generator); + } + else { + return evaluate(sourceSpliterator(0), true, generator); + } + } + + // 最终的转换Node的方法 + final Node evaluate(Spliterator spliterator, + boolean flatten, + IntFunction generator) { + // 并发执行暂时跳过 + if (isParallel()) { + // @@@ Optimize if op of this pipeline stage is a stateful op + return evaluateToNode(this, spliterator, flatten, generator); + } + else { + // 兜兜转换还是回到了wrapAndCopyInto()方法,遍历Sink链,所以基本可以得知Node.Builder是Sink的一个实现 + Node.Builder nb = makeNodeBuilder( + exactOutputSizeIfKnown(spliterator), generator); + return wrapAndCopyInto(nb, spliterator).build(); + } + } + + // 获取Node的建造器实例 + final Node.Builder makeNodeBuilder(long exactSizeIfKnown, IntFunction generator) { + return Nodes.builder(exactSizeIfKnown, generator); + } + + // 暂时省略其他代码 +} + +// Node接口定义 +interface Node { + + // 获取待处理的元素封装成的Spliterator实例 + Spliterator spliterator(); + + // 遍历当前Node实例中所有待处理的元素,回调到Consumer实例中 + void forEach(Consumer consumer); + + // 获取当前Node实例的所有子Node的个数 + default int getChildCount() { + return 0; + } + + // 获取当前Node实例的子Node实例,入参i是子Node的索引 + default Node getChild(int i) { + throw new IndexOutOfBoundsException(); + } + + // 分割当前Node实例的一个部分,生成一个新的sub Node,类似于ArrayList中的subList方法 + default Node truncate(long from, long to, IntFunction generator) { + if (from == 0 && to == count()) + return this; + Spliterator spliterator = spliterator(); + long size = to - from; + Node.Builder nodeBuilder = Nodes.builder(size, generator); + nodeBuilder.begin(size); + for (int i = 0; i < from && spliterator.tryAdvance(e -> { }); i++) { } + if (to == count()) { + spliterator.forEachRemaining(nodeBuilder); + } else { + for (int i = 0; i < size && spliterator.tryAdvance(nodeBuilder); i++) { } + } + nodeBuilder.end(); + return nodeBuilder.build(); + } + + // 创建一个包含当前Node实例所有元素的元素数组视图 + T[] asArray(IntFunction generator); + + // + void copyInto(T[] array, int offset); + + // 返回Node实例基于Stream的元素"形状" + default StreamShape getShape() { + return StreamShape.REFERENCE; + } + + // 获取当前Node实例包含的元素个数 + long count(); + +// Node建造器,注意这个Node.Builder接口是继承自Sink,那么其子类实现都可以添加到Sink链中作为一个节点(终结节点) +interface Builder extends Sink { + + // 创建Node实例 + Node build(); + + // 基于Integer元素类型的特化类型Node.Builder + interface OfInt extends Node.Builder, Sink.OfInt { + @Override + Node.OfInt build(); + } + + // 基于Long元素类型的特化类型Node.Builder + interface OfLong extends Node.Builder, Sink.OfLong { + @Override + Node.OfLong build(); + } + + // 基于Double元素类型的特化类型Node.Builder + interface OfDouble extends Node.Builder, Sink.OfDouble { + @Override + Node.OfDouble build(); + } + } + + // 暂时省略其他代码 +} + + +// 这里下面的方法来源于Nodes类 +final class Nodes { + + // 暂时省略其他代码 + + // Node扁平化处理,如果传入的Node实例存在子Node实例,则使用fork-join对Node进行分割和并发计算,结果添加到IntFunction生成的数组中,如果不存在子Node,直接返回传入的Node实例 + // 关于并发计算部分暂时不分析 + public static Node flatten(Node node, IntFunction generator) { + if (node.getChildCount()> 0) { + long size = node.count(); + if (size>= MAX_ARRAY_SIZE) + throw new IllegalArgumentException(BAD_SIZE); + T[] array = generator.apply((int) size); + new ToArrayTask.OfRef(node, array, 0).invoke(); + return node(array); + } else { + return node; + } + } + + // 创建Node的建造器实例 + static Node.Builder builder(long exactSizeIfKnown, IntFunction generator) { + // 当知道待处理元素的准确数量并且小于允许创建的数组的最大长度MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8),使用FixedNodeBuilder(固定长度数组Node建造器),否则使用SpinedNodeBuilder实例 + return (exactSizeIfKnown>= 0 && exactSizeIfKnown < MAX_ARRAY_SIZE) + ? new FixedNodeBuilder(exactSizeIfKnown, generator) + : builder(); + } + + // 创建Node的建造器实例,使用SpinedNodeBuilder的实例,此SpinedNode支持元素添加,但是不支持元素移除 + static Node.Builder builder() { + return new SpinedNodeBuilder(); + } + + // 固定长度固定长度数组Node实现(也就是最终的Node实现是一个ArrayNode,最终的容器为一个T类型元素的数组T[]) + private static final class FixedNodeBuilder + extends ArrayNode + implements Node.Builder { + + // 基于size(元素个数,或者说创建数组的长度)和数组创建方法IntFunction构建FixedNodeBuilder实例 + FixedNodeBuilder(long size, IntFunction generator) { + super(size, generator); + assert size < MAX_ARRAY_SIZE; + } + + // 返回当前FixedNodeBuilder实例,判断数组元素计数值curSize必须大于等于实际数组容器中元素的个数 + @Override + public Node build() { + if (curSize < array.length) + throw new IllegalStateException(String.format("Current size %d is less than fixed size %d", + curSize, array.length)); + return this; + } + + // Sink的begin方法回调,传入的size必须和数组长度相等,因为后面的accept()方法会执行size此 + @Override + public void begin(long size) { + if (size != array.length) + throw new IllegalStateException(String.format("Begin size %d is not equal to fixed size %d", + size, array.length)); + // 重置数组元素计数值为0 + curSize = 0; + } + + // Sink的accept方法回调,当数组元素计数值小于数组长度,直接向数组下标curSize++添加传入的元素 + @Override + public void accept(T t) { + if (curSize < array.length) { + array[curSize++] = t; + } else { + throw new IllegalStateException(String.format("Accept exceeded fixed size of %d", + array.length)); + } + } + + // Sink的end方法回调,再次判断数组元素计数值curSize必须大于等于实际数组容器中元素的个数 + @Override + public void end() { + if (curSize < array.length) + throw new IllegalStateException(String.format("End size %d is less than fixed size %d", + curSize, array.length)); + } + + // 返回FixedNodeBuilder当前信息,当前处理的下标和当前数组中所有的元素 + @Override + public String toString() { + return String.format("FixedNodeBuilder[%d][%s]", + array.length - curSize, Arrays.toString(array)); + } + } + + // Node实现,容器为一个固定长度的数组 + private static class ArrayNode implements Node { + + // 数组容器 + final T[] array; + + // 数组容器中当前元素的个数,这个值是一个固定值,或者在FixedNodeBuilder的accept()方法回调中递增 + int curSize; + + // 基于size和数组创建的工厂IntFunction构建ArrayNode实例 + @SuppressWarnings("unchecked") + ArrayNode(long size, IntFunction generator) { + if (size>= MAX_ARRAY_SIZE) + throw new IllegalArgumentException(BAD_SIZE); + // 创建szie长度的数组容器 + this.array = generator.apply((int) size); + this.curSize = 0; + } + + // 这个方法是基于一个现成的数组创建ArrayNode实例,直接改变数组的引用为array,元素个数curSize置为输入参数长度 + ArrayNode(T[] array) { + this.array = array; + this.curSize = array.length; + } + + // Node - 接下来是Node接口的实现 + + // 基于数组实例,起始索引0和结束索引curSize构造一个全新的Spliterator实例 + @Override + public Spliterator spliterator() { + return Arrays.spliterator(array, 0, curSize); + } + + // 拷贝array中的元素到外部传入的dest数组中 + @Override + public void copyInto(T[] dest, int destOffset) { + System.arraycopy(array, 0, dest, destOffset, curSize); + } + + // 返回元素数组视图,这里直接返回array引用 + @Override + public T[] asArray(IntFunction generator) { + if (array.length == curSize) { + return array; + } else { + throw new IllegalStateException(); + } + } + + // 获取array中的元素个数 + @Override + public long count() { + return curSize; + } + + // 遍历array,每个元素回调Consumer实例 + @Override + public void forEach(Consumer consumer) { + for (int i = 0; i < curSize; i++) { + consumer.accept(array[i]); + } + } + + // 返回ArrayNode当前信息,当前处理的下标和当前数组中所有的元素 + @Override + public String toString() { + return String.format("ArrayNode[%d][%s]", + array.length - curSize, Arrays.toString(array)); + } + } + + // 暂时省略其他代码 +} +``` + +很多集合容器的`Spliterator`其实并不支持`SIZED`特性,其实`Node`的最终实现很多情况下都是`Nodes.SpinedNodeBuilder`,因为`SpinedNodeBuilder`重实现实现了数组扩容和`Spliterator`基于数组进行分割的方法,源码相对复杂(特别是`spliterator()`方法),这里挑部分进行分析,由于`SpinedNodeBuilder`绝大部分方法都是使用父类`SpinedBuffer`中的实现,这里可以直接分析`SpinedBuffer`: + +```java +// SpinedBuffer的当前数组在超过了元素数量阈值之后,会拆分为多个数组块,存储到spine中,而curChunk引用指向的是当前处理的数组块 +class SpinedBuffer + extends AbstractSpinedBuffer + implements Consumer, Iterable { + + // 暂时省略其他代码 + + // 当前的数组块 + protected E[] curChunk; + + // 所有数组块 + protected E[][] spine; + + // 构造函数,指定初始化容量 + SpinedBuffer(int initialCapacity) { + super(initialCapacity); + curChunk = (E[]) new Object[1 << initialChunkPower]; + } + + // 构造函数,指定默认初始化容量 + @SuppressWarnings("unchecked") + SpinedBuffer() { + super(); + curChunk = (E[]) new Object[1 << initialChunkPower]; + } + + // 拷贝当前SpinedBuffer中的数组元素到传入的数组实例 + public void copyInto(E[] array, int offset) { + // 计算最终的offset,区分单个chunk和多个chunk的情况 + long finalOffset = offset + count(); + if (finalOffset> array.length || finalOffset < offset) { + throw new IndexOutOfBoundsException("does not fit"); + } + // 单个chunk的情况,由curChunk最接拷贝 + if (spineIndex == 0) + System.arraycopy(curChunk, 0, array, offset, elementIndex); + else { + // 多个chunk的情况,由遍历spine并且对每个chunk进行拷贝 + // full chunks + for (int i=0; i < spineIndex; i++) { + System.arraycopy(spine[i], 0, array, offset, spine[i].length); + offset += spine[i].length; + } + if (elementIndex> 0) + System.arraycopy(curChunk, 0, array, offset, elementIndex); + } + } + + // 返回数组元素视图,基于IntFunction构建数组实例,使用copyInto()方法进行元素拷贝 + public E[] asArray(IntFunction arrayFactory) { + long size = count(); + if (size>= Nodes.MAX_ARRAY_SIZE) + throw new IllegalArgumentException(Nodes.BAD_SIZE); + E[] result = arrayFactory.apply((int) size); + copyInto(result, 0); + return result; + } + + // 清空SpinedBuffer,清空分块元素和所有引用 + @Override + public void clear() { + if (spine != null) { + curChunk = spine[0]; + for (int i=0; i consumer) { + // completed chunks, if any + for (int j = 0; j < spineIndex; j++) + for (E t : spine[j]) + consumer.accept(t); + + // current chunk + for (int i=0; i= spine.length || spine[spineIndex+1] == null) + increaseCapacity(); + elementIndex = 0; + ++spineIndex; + // 当前的chunk更新为最新的chunk,就是spine中的最新一个chunk + curChunk = spine[spineIndex]; + } + // 当前的curChunk添加元素 + curChunk[elementIndex++] = e; + } + + // 暂时省略其他代码 +} +``` + +源码已经基本分析完毕,下面还是用一个例子转化为流程图: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-14.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-14.png) + +## 流并发执行的源码实现 + +如果流实例调用了`parallel()`,注释中提到会返回一个异步执行流的变体,实际上并没有构造变体,只是把`sourceStage.parallel`标记为`true`,异步求值的基本过程是:构建流管道结构的时候和同步求值的过程一致,构建完`Sink`链之后,`Spliterator`会使用特定算法基于`trySplit()`进行自分割,自分割算法由具体的子类决定,例如`ArrayList`采用的就是二分法,分割完成后每个`Spliterator`持有所有元素中的一小部分,然后把每个`Spliterator`作为`sourceSpliterator`在`fork-join`线程池中执行`Sink`链,得到多个部分的结果在当前调用线程中聚合,得到最终结果。这里用到的技巧就是:线程封闭和`fork-join`。因为不同`Terminal Op`的并发求值过程大同小异,这里只分析`forEach`并发执行的实现。首先展示一个使用`fork-join`线程池的简单例子: + +```java +public class MapReduceApp { + + public static void main(String[] args) { + // 数组中每个元素*2,再求和 + Integer result = new MapReducer(new Integer[]{1, 2, 3, 4}, x -> x * 2, Integer::sum).invoke(); + System.out.println(result); + } + + interface Mapper { + + T apply(S source); + } + + interface Reducer { + + T apply(S first, S second); + } + + public static class MapReducer extends CountedCompleter { + + final T[] array; + + final Mapper mapper; + + final Reducer reducer; + + final int lo, hi; + + MapReducer sibling; + + T result; + + public MapReducer(T[] array, + Mapper mapper, + Reducer reducer) { + this.array = array; + this.mapper = mapper; + this.reducer = reducer; + this.lo = 0; + this.hi = array.length; + } + + public MapReducer(CountedCompleter p, + T[] array, + Mapper mapper, + Reducer reducer, + int lo, + int hi) { + super(p); + this.array = array; + this.mapper = mapper; + this.reducer = reducer; + this.lo = lo; + this.hi = hi; + } + + @Override + public void compute() { + if (hi - lo>= 2) { + int mid = (lo + hi)>> 1; + MapReducer left = new MapReducer(this, array, mapper, reducer, lo, mid); + MapReducer right = new MapReducer(this, array, mapper, reducer, mid, hi); + left.sibling = right; + right.sibling = left; + // 创建子任务父任务的pending计数器加1 + setPendingCount(1); + // 提交右子任务 + right.fork(); + // 在当前线程计算左子任务 + left.compute(); + } else { + if (hi> lo) { + result = mapper.apply(array[lo]); + } + // 叶子节点完成,尝试合并其他兄弟节点的结果,会调用onCompletion方法 + tryComplete(); + } + } + + @Override + public T getRawResult() { + return result; + } + + @SuppressWarnings("unchecked") + @Override + public void onCompletion(CountedCompleter caller) { + if (caller != this) { + MapReducer child = (MapReducer) caller; + MapReducer sib = child.sibling; + // 合并子任务结果,只有两个子任务 + if (Objects.isNull(sib) || Objects.isNull(sib.result)) { + result = child.result; + } else { + result = reducer.apply(child.result, sib.result); + } + } + } + } +} +``` + +这里简单使用了`fork-join`编写了一个简易的`MapReduce`应用,`main`方法中运行的是数组`[1,2,3,4]`中的所有元素先映射为`i -> i * 2`,再进行`reduce`(求和)的过程,代码中也是简单使用二分法对原始的`array`进行分割,当最终的任务只包含一个元素,也就是`lo < hi`且`hi - lo == 1`的时候,会基于单个元素调用`Mapper`的方法进行完成通知`tryComplete()`,任务完成会最终通知`onCompletion()`方法,`Reducer`就是在此方法中进行结果的聚合操作。对于流的并发求值来说,过程是类似的,`ForEachOp`中最终调用`ForEachOrderedTask`或者`ForEachTask`,这里挑选`ForEachTask`进行分析: + +```java +abstract static class ForEachOp + implements TerminalOp, TerminalSink { + + // 暂时省略其他代码 + @Override + public Void evaluateParallel(PipelineHelper helper, + Spliterator spliterator) { + if (ordered) + new ForEachOrderedTask(helper, spliterator, this).invoke(); + else + // 最终是调用ForEachTask的invoke方法,invoke会阻塞到所有fork任务执行完,获取最终的结果 + new ForEachTask(helper, spliterator, helper.wrapSink(this)).invoke(); + return null; + } + // 暂时省略其他代码 +} + +// ForEachOps类 +final class ForEachOps { + + private ForEachOps() { } + + // forEach的fork-join任务实现,没有覆盖getRawResult()方法,最终只会返回NULL + static final class ForEachTask extends CountedCompleter { + + // Spliterator实例,如果是父任务则代表所有待处理的元素,如果是子任务则是一个分割后的新Spliterator实例 + private Spliterator spliterator; + + // Sink链实例 + private final Sink sink; + + // 流管道引用 + private final PipelineHelper helper; + + // 目标数量,其实是每个任务处理元素数量的建议值 + private long targetSize; + + // 这个构造器是提供给父(根)任务 + ForEachTask(PipelineHelper helper, + Spliterator spliterator, + Sink sink) { + super(null); + this.sink = sink; + this.helper = helper; + this.spliterator = spliterator; + this.targetSize = 0L; + } + + // 这个构造器是提供给子任务,所以需要父任务的引用和一个分割后的新Spliterator实例作为参数 + ForEachTask(ForEachTask parent, Spliterator spliterator) { + super(parent); + this.spliterator = spliterator; + this.sink = parent.sink; + this.targetSize = parent.targetSize; + this.helper = parent.helper; + } + + // Similar to AbstractTask but doesn't need to track child tasks + // 实现compute方法,用于分割Spliterator成多个子任务,这里不需要跟踪所有子任务 + public void compute() { + // 神奇的赋值,相当于Spliterator rightSplit = spliterator; Spliterator leftSplit; + // rightSplit总是指向当前的spliterator实例 + Spliterator rightSplit = spliterator, leftSplit; + // 这里也是神奇的赋值,相当于long sizeEstimate = rightSplit.estimateSize(); long sizeThreshold; + long sizeEstimate = rightSplit.estimateSize(), sizeThreshold; + // sizeThreshold赋值为targetSize + if ((sizeThreshold = targetSize) == 0L) + // 基于Spliterator分割后的右分支实例的元素数量重新赋值sizeThreshold和targetSize + // 计算方式是待处理元素数量/(fork-join线程池并行度<<2)或者1(当前一个计算方式结果为0的时候) + targetSize = sizeThreshold = AbstractTask.suggestTargetSize(sizeEstimate); + // 当前的流是否支持SHORT_CIRCUIT,也就是短路特性 + boolean isShortCircuit = StreamOpFlag.SHORT_CIRCUIT.isKnown(helper.getStreamAndOpFlags()); + // 当前的任务是否fork右分支 + boolean forkRight = false; + // taskSink作为Sink的临时变量 + Sink taskSink = sink; + // 当前任务的临时变量 + ForEachTask task = this; + // Spliterator分割和创建新的fork任务ForEachTask,前提是不支持短路或者Sink不支持取消 + while (!isShortCircuit || !taskSink.cancellationRequested()) { + // 当前的任务中的Spliterator(rightSplit)中的待处理元素小于等于每个任务应该处理的元素阈值或者再分割后得到NULL,则不需要再分割,直接基于rightSplit和Sink链执行循环处理元素 + if (sizeEstimate <= sizeThreshold || (leftSplit = rightSplit.trySplit()) == null) { + // 这里就是遍历rightSplit元素回调Sink链的操作 + task.helper.copyInto(taskSink, rightSplit); + break; + } + // rightSplit还能分割,则基于分割后的leftSplit和以当前任务作为父任务创建一个新的fork任务 + ForEachTask leftTask = new ForEachTask(task, leftSplit); + // 待处理子任务加1 + task.addToPendingCount(1); + // 需要fork的任务实例临时变量 + ForEachTask taskToFork; + // 因为rightSplit总是分割Spliterator后对应原来的Spliterator引用,而leftSplit总是trySplit()后生成的新的Spliterator + // 所以这里leftSplit也需要作为rightSplit进行分割,通俗来说就是周星驰007那把梅花间足发射的枪 + if (forkRight) { + // 这里交换leftSplit为rightSplit,所以forkRight设置为false,下一轮循环相当于fork left + forkRight = false; + rightSplit = leftSplit; + taskToFork = task; + // 赋值下一轮的父Task为当前的fork task + task = leftTask; + } + else { + forkRight = true; + taskToFork = leftTask; + } + // 添加fork任务到任务队列中 + taskToFork.fork(); + // 其实这里是更新剩余待分割的Spliterator中的所有元素数量到sizeEstimate + sizeEstimate = rightSplit.estimateSize(); + } + // 置空spliterator实例并且传播任务完成状态,等待所有任务执行完成 + task.spliterator = null; + task.propagateCompletion(); + } + } +} +``` + +上面的源码分析看起来可能比较难理解,这里举个简单的例子: + +```java +public static void main(String[] args) throws Exception { + List list = new ArrayList(); + list.add(1); + list.add(2); + list.add(3); + list.add(4); + list.stream().parallel().forEach(System.out::println); +} +``` + +这段代码中最终转换成`ForEachTask`中评估后得到的`targetSize = sizeThreshold == 1`,当前调用线程会参与计算,会执行`3`次`fork`,也就是一共有`4`个处理流程实例(也就是原始的`Spliterator`实例最终会分割出`3`个全新的`Spliterator`实例,加上自身一个`4`个`Spliterator`实例),每个处理流程实例只处理`1`个元素,对应的流程图如下: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-16.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-16.png) + +最终的计算结果是调用`CountedCompleter.invoke()`方法获取的,此方法会阻塞直到所有子任务处理完成,当然`forEach`终结操作不需要返回值,所以没有实现`getRawResult()`方法,这里只是为了阻塞到所有任务执行完毕才解除调用线程的阻塞状态。 + +## 状态操作与短路操作 + +`Stream`中按照**中间操作**是否有状态可以把这些操作分为**无状态操作**和**有状态操作**。`Stream`中按照**终结操作**是否支持短路特性可以把这些操作分为**非短路操作**和**短路操作**。理解如下: + +- 无状态操作:当前操作节点处理元素完成后,在满足前提条件下直接把结果传递到下一个操作节点,也就是操作内部不存在状态也不需要保存状态,例如`filter`、`map`等操作 +- 有状态操作:处理元素的时候,依赖于节点的内部状态对元素进行累积,当处理一个新的元素的时候,其实可以感知到所有处理过的元素的历史状态,这个"状态"其实更像是缓冲区的概念,例如`sort`、`limit`等操作,以`sort`操作为例,一般是把所有待处理的元素全部添加到一个容器如`ArrayList`,再进行所有元素的排序,然后再重新模拟`Spliterator`把元素推送到后一个节点 +- 非短路(终结)操作:终结操作在处理元素时候不能基于短路条件提前中断处理并且返回,也就是必须处理所有的元素,如`forEach` +- 短路(终结)操作:终结操作在处理元素时候允许基于短路条件提前中断处理并且返回,但是最终实现中是有可能遍历完所有的元素中,只是在处理方法中基于前置的短路条件跳过了实际的处理过程,如`anyMatch`(实际上`anyMatch`会遍历完所有的元素,不过在命中了短路条件下,元素回调`Sink.accept()`方法时候会基于`stop`短路标记跳过具体的处理流程) + +这里不展开源码进行分析,仅仅展示一个经常见到的`Stream`操作汇总表如下: + +[![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/stream-source-15.png)](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202110/stream-source-15.png) + +这里还有两点要注意: + +- 从源码上看部分中间操作也是支持短路的,例如`slice`和`while`相关操作 +- 从源码上看`find`相关终结操作中`findFirst`、`findAny`均支持和判断`StreamOpFlag.SHORT_CIRCUIT`,而`match`相关终结操作是通过内部的临时状态`stop`和`value`进行短路控制 + +## 小结 + +前前后后写了十多万字,其实也仅仅相对浅层次介绍了`Stream`的基本实现,笔者认为很多没分析到的中间操作实现和终结操作实现,特别是并发执行的终结操作实现是十分复杂的,多线程环境下需要进行一些想象和多处`DEBUG`定位执行位置和推演执行的过程。简单总结一下: + +- `JDK`中`Stream`的实现是精炼的高度工程化代码 +- `Stream`的载体虽然是`AbstractPipeline`,管道结构,但是只用其形,实际求值操作之前会转化为一个多层包裹的`Sink`结构,也就是前文一直说的`Sink`链,从编程模式来看,应用的是`Reactor`编程模式 +- `Stream`目前支持的固有求值执行结构一定是`Head(Source Spliterator) -> Op -> Op ... -> Terminal Op`的形式,这算是一个局限性,没有办法做到像`LINQ`那样可以灵活实现类似内存视图的功能 +- `Stream`目前支持并发求值方案是针对`Source Spliterator`进行分割,封装`Terminal Op`和固定`Sink`链构造的`ForkJoinTask`进行并发计算,调用线程和`fork-join`线程池中的工作线程都可以参与求值过程,笔者认为这部分是`Stream`中除了那些标志集合位运算外最复杂的实现 +- `Stream`实现的功能是一个突破,也有人说过此功能是一个"早产儿",在此希望`JDK`能够在矛盾螺旋中前进和发展 + +>作者:throwable +> +>出处:https://www.cnblogs.com/throwable/p/15371609.html + diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/SynchronousQueue.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/SynchronousQueue.md" new file mode 100644 index 0000000..72797f2 --- /dev/null +++ "b/346円272円220円347円240円201円345円210円206円346円236円220円/SynchronousQueue.md" @@ -0,0 +1,880 @@ +# SynchronousQueue原理详解-公平模式 + +## 一、介绍 + +SynchronousQueue是一个双栈双队列算法,无空间的队列或栈,任何一个对SynchronousQueue写需要等到一个对SynchronousQueue的读操作,反之亦然。一个读操作需要等待一个写操作,相当于是交换通道,提供者和消费者是需要组队完成工作,缺少一个将会阻塞线程,知道等到配对为止。 + +SynchronousQueue是一个队列和栈算法实现,在SynchronousQueue中双队列FIFO提供公平模式,而双栈LIFO提供的则是非公平模式。 + +对于SynchronousQueue来说,他的put方法和take方法都被抽象成统一方法来进行操作,通过抽象出内部类Transferer,来实现不同的操作。 + +> 注意事项:本文分析主要是针对jdk1.8的版本进行分析,下面的代码中的线程执行顺序可能并不能完全保证顺序性,执行时间比较短,所以暂且认定有序执行。 +> +> 约定:图片中以Reference-开头的代表对象的引用地址,通过箭头方式进行引用对象。 + +Transferer.transfer方法主要介绍如下所示: + +```java +abstract static class Transferer { + /** + * 执行put和take方法. + * + * @param e 非空时,表示这个元素要传递给消费者(提供者-put); + * 为空时, 则表示当前操作要请求消费一个数据(消费者-take)。 + * offered by producer. + * @param timed 决定是否存在timeout时间。 + * @param nanos 超时时长。 + * @return 如果返回非空, 代表数据已经被消费或者正常提供; 如果为空, + * 则表示由于超时或中断导致失败。可通过Thread.interrupted来检查是那种。 + */ + abstract E transfer(E e, boolean timed, long nanos); +} +``` + +接下来看一下SynchronousQueue的字段信息: + +```java +/** CPU数量 */ +static final int NCPUS = Runtime.getRuntime().availableProcessors(); + +/** + * 自旋次数,如果transfer指定了timeout时间,则使用maxTimeSpins,如果CPU数量小于2则自旋次数为0,否则为32 + * 此值为经验值,不随CPU数量增加而变化,这里只是个常量。 + */ +static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32; + +/** + * 自旋次数,如果没有指定时间设置,则使用maxUntimedSpins。如果NCPUS数量大于等于2则设定为为32*16,否则为0; + */ +static final int maxUntimedSpins = maxTimedSpins * 16; + +/** + * The number of nanoseconds for which it is faster to spin + * rather than to use timed park. A rough estimate suffices. + */ +static final long spinForTimeoutThreshold = 1000L; +``` + +- NCPUS:代表CPU的数量 +- maxTimedSpins:自旋次数,如果transfer指定了timeout时间,则使用maxTimeSpins,如果CPU数量小于2则自旋次数为0,否则为32,此值为经验值,不随CPU数量增加而变化,这里只是个常量。 +- maxUntimedSpins:自旋次数,如果没有指定时间设置,则使用maxUntimedSpins。如果NCPUS数量大于等于2则设定为为32*16,否则为0; +- spinForTimeoutThreshold:为了防止自定义的时间限过长,而设置的,如果设置的时间限长于这个值则取这个spinForTimeoutThreshold 为时间限。这是为了优化而考虑的。这个的单位为纳秒。 + +## 公平模式-TransferQueue + +TransferQueue内部是如何进行工作的,这里先大致讲解下,队列采用了互补模式进行等待,QNode中有一个字段是isData,如果模式相同或空队列时进行等待操作,互补的情况下就进行消费操作。 + +入队操作相同模式 +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511194700294-1968231289.png) + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511194800943-86356229.png) + +不同模式时进行出队列操作: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511194811557-2047294276.png) + +这时候来了一个isData=false的互补模式,队列就会变成如下状态: +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511194824692-1695599974.png) + +TransferQueue继承自Transferer抽象类,并且实现了transfer方法,它主要包含以下内容: + +### QNode + +代表队列中的节点元素,它内部包含以下字段信息: + +1. 字段信息描述 + +| 字段 | 描述 | 类型 | +| ------ | -------------- | ------- | +| next | 下一个节点 | QNode | +| item | 元素信息 | Object | +| waiter | 当前等待的线程 | Thread | +| isData | 是否是数据 | boolean | + +1. 方法信息描述 + +| 方法 | 描述 | +| ----------- | ------------------------------------------------------------ | +| casNext | 替换当前节点的next节点 | +| casItem | 替换当前节点的item数据 | +| tryCancel | 取消当前操作,将当前item赋值为this(当前QNode节点) | +| isCancelled | 如果item是this(当前QNode节点)的话就返回true,反之返回false | +| isOffList | 如果已知此节点离队列,判断next节点是不是为this,则返回true,因为由于* advanceHead操作而忘记了其下一个指针。 | + +```java +E transfer(E e, boolean timed, long nanos) { + /* Basic algorithm is to loop trying to take either of + * two actions: + * + * 1. If queue apparently empty or holding same-mode nodes, + * try to add node to queue of waiters, wait to be + * fulfilled (or cancelled) and return matching item. + * + * 2. If queue apparently contains waiting items, and this + * call is of complementary mode, try to fulfill by CAS'ing + * item field of waiting node and dequeuing it, and then + * returning matching item. + * + * In each case, along the way, check for and try to help + * advance head and tail on behalf of other stalled/slow + * threads. + * + * The loop starts off with a null check guarding against + * seeing uninitialized head or tail values. This never + * happens in current SynchronousQueue, but could if + * callers held non-volatile/final ref to the + * transferer. The check is here anyway because it places + * null checks at top of loop, which is usually faster + * than having them implicitly interspersed. + */ + + QNode s = null; // constructed/reused as needed + // 分为两种状态1.有数据=true 2.无数据=false + boolean isData = (e != null); + // 循环内容 + for (;;) { + // 尾部节点。 + QNode t = tail; + // 头部节点。 + QNode h = head; + // 判断头部和尾部如果有一个为null则自旋转。 + if (t == null || h == null) // 还未进行初始化的值。 + continue; // 自旋 + // 头结点和尾节点相同或者尾节点的模式和当前节点模式相同。 + if (h == t || t.isData == isData) { // 空或同模式。 + // tn为尾节点的下一个节点信息。 + QNode tn = t.next; + // 这里我认为是阅读不一致,原因是当前线程还没有阻塞的时候其他线程已经修改了尾节点tail会导致当前线程的tail节点不一致。 + if (t != tail) // inconsistent read + continue; + if (tn != null) { // lagging tail + advanceTail(t, tn); + continue; + } + if (timed && nanos <= 0) // 这里如果指定timed判断时间小于等于0直接返回。 + return null; + // 判断新增节点是否为null,为null直接构建新节点。 + if (s == null) + s = new QNode(e, isData); + if (!t.casNext(null, s)) // 如果next节点不为null说明已经有其他线程进行tail操作 + continue; + // 将t节点替换为s节点 + advanceTail(t, s); + // 等待有消费者消费线程。 + Object x = awaitFulfill(s, e, timed, nanos); + // 如果返回的x,指的是s.item,如果s.item指向自己的话清除操作。 + if (x == s) { + clean(t, s); + return null; + } + // 如果没有取消联系 + if (!s.isOffList()) { + // 将当前节点替换头结点 + advanceHead(t, s); // unlink if head + if (x != null) // 取消item值,这里是take方法时会进行item赋值为this + s.item = s; + // 将等待线程设置为null + s.waiter = null; + } + return (x != null) ? (E)x : e; + + } else { // complementary-mode + // 获取头结点下一个节点 + QNode m = h.next; // node to fulfill + // 如果当前线程尾节点和全局尾节点不一致,重新开始 + // 头结点的next节点为空,代表无下一个节点,则重新开始, + // 当前线程头结点和全局头结点不相等,则重新开始 + if (t != tail || m == null || h != head) + continue; // inconsistent read + + Object x = m.item; + if (isData == (x != null) || // m already fulfilled + x == m || // m cancelled + !m.casItem(x, e)) { // lost CAS + advanceHead(h, m); // dequeue and retry + continue; + } + + advanceHead(h, m); // successfully fulfilled + LockSupport.unpark(m.waiter); + return (x != null) ? (E)x : e; + } + } +} +``` + +我们来看一下awaitFulfill方法内容: + +```java +Object awaitFulfill(QNode s, E e, boolean timed, long nanos) { + // 如果指定了timed则为System.nanoTime() + nanos,反之为0。 + final long deadline = timed ? System.nanoTime() + nanos : 0L; + // 获取当前线程。 + Thread w = Thread.currentThread(); + // 如果头节点下一个节点是当前s节点(以防止其他线程已经修改了head节点) + // 则运算(timed ? maxTimedSpins : maxUntimedSpins),否则直接返回。 + // 指定了timed则使用maxTimedSpins,反之使用maxUntimedSpins + int spins = ((head.next == s) ? + (timed ? maxTimedSpins : maxUntimedSpins) : 0); + // 自旋 + for (;;) { + // 判断是否已经被中断。 + if (w.isInterrupted()) + //尝试取消,将当前节点的item修改为当前节点(this)。 + s.tryCancel(e); + // 获取当前节点内容。 + Object x = s.item; + // 判断当前值和节点值不相同是返回,因为弹出时会将item值赋值为null。 + if (x != e) + return x; + if (timed) { + nanos = deadline - System.nanoTime(); + if (nanos <= 0L) { + s.tryCancel(e); + continue; + } + } + if (spins> 0)![](https://img2018.cnblogs.com/blog/458325/201905/458325-20190511194850882-1013581623.png) + + --spins; + else if (s.waiter == null) + s.waiter = w; + else if (!timed) + LockSupport.park(this); + else if (nanos> spinForTimeoutThreshold) + LockSupport.parkNanos(this, nanos); + } +} +``` + +1. 首先先判断有没有被中断,如果被中断则取消本次操作,将当前节点的item内容赋值为当前节点。 +2. 判断当前节点和节点值不相同是返回 +3. 将当前线程赋值给当前节点 +4. 自旋,如果指定了timed则使用`LockSupport.parkNanos(this, nanos);`,如果没有指定则使用`LockSupport.park(this);`。 +5. 中断相应是在下次才能被执行。 + +通过上面源码分析我们这里做出简单的示例代码演示一下put操作和take操作是如何进行运作的,首先看一下示例代码,如下所示: + +```java +/** + * SynchronousQueue进行put和take操作。 + * + * @author battleheart + */ +public class SynchronousQueueDemo { + public static void main(String[] args) throws Exception { + ExecutorService executorService = Executors.newFixedThreadPool(3); + SynchronousQueue queue = new SynchronousQueue(true); + Thread thread1 = new Thread(() -> { + try { + queue.put(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread1.start(); + Thread.sleep(2000); + Thread thread2 = new Thread(() -> { + try { + queue.put(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread2.start(); + Thread.sleep(10000); + Thread thread3 = new Thread(() -> { + try { + System.out.println(queue.take()); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread3.start(); + } +} +``` + +首先上来之后进行的是两次put操作,然后再take操作,默认队列上来会进行初始化,初始化的内容如下代码所示: + +```java +TransferQueue() { + QNode h = new QNode(null, false); // initialize to dummy node. + head = h; + tail = h; +} +``` + +初始化后队列的状态如下图所示: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195104140-1116474850.png) + +当线程1执行put操作时,来分析下代码: + +```java +QNode t = tail; +QNode h = head; +if (t == null || h == null) // saw uninitialized value + continue; +``` + +首先执行局部变量t代表队尾指针,h代表队头指针,判断队头和队尾不为空则进行下面的操作,接下来是if...else语句这里是分水岭,当相同模式操作的时候执行if语句,当进行不同模式操作时执行的是else语句,程序是如何控制这样的操作的呢?接下来我们慢慢分析一下: + +```java +if (h == t || t.isData == isData) { // 队列为空或者模式相同时进行if语句 + QNode tn = t.next; + if (t != tail) // 判断t是否是队尾,不是则重新循环。 + continue; + if (tn != null) { // tn是队尾的下个节点,如果tn有内容则将队尾更换为tn,并且重新循环操作。 + advanceTail(t, tn); + continue; + } + if (timed && nanos <= 0) // 如果指定了timed并且延时时间用尽则直接返回空,这里操作主要是offer操作时,因为队列无存储空间的当offer时不允许插入。 + return null; + if (s == null) // 这里是新节点生成。 + s = new QNode(e, isData); + if (!t.casNext(null, s)) // 将尾节点的next节点修改为当前节点。 + continue; + + advanceTail(t, s); // 队尾移动 + Object x = awaitFulfill(s, e, timed, nanos); //自旋并且设置线程。 + if (x == s) { // wait was cancelled + clean(t, s); + return null; + } + + if (!s.isOffList()) { // not already unlinked + advanceHead(t, s); // unlink if head + if (x != null) // and forget fields + s.item = s; + s.waiter = null; + } + return (x != null) ? (E)x : e; + +} +``` + +上面代码是if语句中的内容,进入到if语句中的判断是如果头结点和尾节点相等代表队列为空,并没有元素所有要进行插入队列的操作,或者是队尾的节点的isData标志和当前操作的节点的类型一样时,会进行入队操作,isData标识当前元素是否是数据,如果为true代表是数据,如果为false则代表不是数据,换句话说只有模式相同的时候才会往队列中存放,如果不是模式相同的时候则代表互补模式,就不走if语句了,而是走了else语句,上面代码中做有注释讲解,下面看一下这里: + +```java +if (s == null) // 这里是新节点生成。 + s = new QNode(e, isData); +if (!t.casNext(null, s)) // 将尾节点的next节点修改为当前节点。 + continue +``` + +当执行上面代码后,队列的情况如下图所示:(这里视为`插入第一个元素`图,方便下面的引用) + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195115791-1877855953.png) + +接下来执行这段代码: + +```java + advanceTail(t, s); // 队尾移动 +``` + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195122314-69757535.png) + +修改了tail节点后,这时候就需要进行自旋操作,并且设置QNode的waiter等待线程,并且将线程等待,等到唤醒线程进行唤醒操作 + +```java + Object x = awaitFulfill(s, e, timed, nanos); //自旋并且设置线程。 +``` + +方法内部分析局部内容,上面已经全部内容的分析: + +```java +if (spins> 0) + --spins; +else if (s.waiter == null) + s.waiter = w; +else if (!timed) + LockSupport.park(this); +else if (nanos> spinForTimeoutThreshold) + LockSupport.parkNanos(this, nanos); +``` + +如果自旋时间spins还有则进行循环递减操作,接下来判断如果当前节点的waiter是空则价格当前线程赋值给waiter,上图中显然是为空的所以会把当前线程进行赋值给我waiter,接下来就是等待操作了。 + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195128586-1381252368.png) + +上面线程则处于等待状态,接下来是线程二进行操作,这里不进行重复进行,插入第二个元素队列的状况,此时线程二也处于等待状态。 + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195133984-1507606809.png) + +上面的主要是put了两次操作后队列的情况,接下来分析一下take操作时又是如何进行操作的,当take操作时,isData为false,而队尾的isData为true两个不相等,所以不会进入到if语句,而是进入到了else语句 + +```java +} else { // 互补模式 + QNode m = h.next; // 获取头结点的下一个节点,进行互补操作。 + if (t != tail || m == null || h != head) + continue; // 这里就是为了防止阅读不一致的问题 + + Object x = m.item; + if (isData == (x != null) || // 如果x=null说明已经被读取了。 + x == m || // x节点和m节点相等说明被中断操作,被取消操作了。 + !m.casItem(x, e)) { // 这里是将item值设置为null + advanceHead(h, m); // 移动头结点到头结点的下一个节点 + continue; + } + + advanceHead(h, m); // successfully fulfilled + LockSupport.unpark(m.waiter); + return (x != null) ? (E)x : e; +} +``` + +首先获取头结点的下一个节点用于互补操作,也就是take操作,接下来进行阅读不一致的判断,防止其他线程进行了阅读操作,接下来获取需要弹出内容x=1,首先进行判断节点内容是不是已经被消费了,节点内容为null时则代表被消费了,接下来判断节点的item值是不是和本身相等如果相等话说明节点被取消了或者被中断了,然后移动头结点到下一个节点上,然后将`refenrence-715`的item值修改为null,`至于为什么修改为null这里留下一个悬念,这里还是比较重要的,大家看到这里的时候需要注意下`,显然这些都不会成立,所以if语句中内容不会被执行,接下来的队列的状态是是这个样子的: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195139214-877626357.png) + +OK,接下来就开始移动队头head了,将head移动到m节点上,执行代码如下所示: + +```java +advanceHead(h, m); +``` + +此时队列的状态是这个样子的: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195144711-804621885.png) + +```java +LockSupport.unpark(m.waiter); +return (x != null) ? (E)x : e; +``` + +接下来将执行唤醒被等待的线程,也就是thread-0,然后返回获取item值1,take方法结束,但是这里并没有结束,因为唤醒了put的线程,此时会切换到put方法中,这时候线程唤醒后会执行`awaitFulfill`方法,此时循环时,有与item值修改为null则直接返回内容。 + +```java +Object x = s.item; +if (x != e) + return x; +``` + +这里的代码我们可以对照`插入第一个元素`图,s节点也就是当前m节点,获取值得时候已经修改为null,但是当时插入的值时1,所以两个不想等了,则直接返回null值。 + +```java +Object x = awaitFulfill(s, e, timed, nanos); +if (x == s) { // wait was cancelled + clean(t, s); + return null; +} + +if (!s.isOffList()) { // not already unlinked + advanceHead(t, s); // unlink if head + if (x != null) // and forget fields + s.item = s; + s.waiter = null; +} +return (x != null) ? (E)x : e; +``` + +又返回到了transfer方法的if语句中,此时x和s并不相等所以不用进行clean操作,首先判断s节点是否已经离队了,显然并没有进行离队操作,`advanceHead(t, s);`操作不会被执行因为上面已近将头节点修改了,但是第一次插入的时候头结点还是`reference-716`,此时已经是`reference-715`,而t节点的引用地址是`reference-716`,所以不会操作,接下来就是将waiter设置为null,也就是忘记掉等待的线程。 + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195151368-1904763115.png) + +分析了正常的take和put操作,接下来分析下中断操作,由于中断相应后,会被执行`if(w.isInterrupted())`这段代码,它会执行`s.tryCancel(e)`方法,这个方法的作用的是将QNode节点的item节点赋值为当前QNode,这时候x和e值就不相等了(`if (x != e)`),x的值是s.item,则为当前QNode,而e的值是用户指定的值,这时候返回x(s.item)。返回到函数调用地方`transfer`中,这时候要执行下面语句: + +```java +if (x == s) { + clean(t, s); + return null; +} +``` + +进入到clean方法执行清理当前节点,下面是方法clean代码: + +```java +/** + * Gets rid of cancelled node s with original predecessor pred. + */ +void clean(QNode pred, QNode s) { + s.waiter = null; // forget thread + /* + * At any given time, exactly one node on list cannot be + * deleted -- the last inserted node. To accommodate this, + * if we cannot delete s, we save its predecessor as + * "cleanMe", deleting the previously saved version + * first. At least one of node s or the node previously + * saved can always be deleted, so this always terminates. + */ + while (pred.next == s) { // Return early if already unlinked + QNode h = head; + QNode hn = h.next; // Absorb cancelled first node as head + if (hn != null && hn.isCancelled()) { + advanceHead(h, hn); + continue; + } + QNode t = tail; // Ensure consistent read for tail + if (t == h) + return; + QNode tn = t.next; + // 判断现在的t是不是末尾节点,可能其他线程插入了内容导致不是最后的节点。 + if (t != tail) + continue; + // 如果不是最后节点的话将其现在t.next节点作为tail尾节点。 + if (tn != null) { + advanceTail(t, tn); + continue; + } + // 如果当前节点不是尾节点进入到这里面。 + if (s != t) { // If not tail, try to unsplice + // 获取当前节点(被取消的节点)的下一个节点。 + QNode sn = s.next; + // 修改上一个节点的next(下一个)元素为下下个节点。 + if (sn == s || pred.casNext(s, sn)) + //返回。 + return; + } + QNode dp = cleanMe; + if (dp != null) { // 尝试清除上一个标记为清除的节点。 + QNode d = dp.next; //1.获取要被清除的节点 + QNode dn; + if (d == null || // 被清除节点不为空 + d == dp || // 被清除节点已经离队 + !d.isCancelled() || // 被清除节点是标记为Cancel状态的。 + (d != t && // 被清除节点不是尾节点 + (dn = d.next) != null && // 被清除节点下一个节点不为null + dn != d && // that is on list + dp.casNext(d, dn))) // 将被清除的节点的前一个节点的下一个节点修改为被清除节点的下一个节点。 + casCleanMe(dp, null); // 清空cleanMe节点。 + if (dp == pred) + return; // s is already saved node + } else if (casCleanMe(null, pred)) // 这里将上一个节点标记为被清除操作,但是其实要操作的是下一个节点。 + return; // Postpone cleaning s + } +} +``` + +1. 如果节点中取消的头结点的下一个节点,只需要移动当前head节点到下一个节点即可。 +2. 如果取消的是中间的节点,则将当前节点next节点修改为下下个节点。 +3. 如果修改为末尾的节点,则将当前节点放入到QNode的clearMe中,等待有内容进来之后下一次进行清除操作。 + +**实例一**:清除头结点下一个节点,下面是实例代码进行讲解: + +```java +/** + * 清除头结点的下一个节点实例代码。 + * + * @author battleheart + */ +public class SynchronousQueueDemo { + public static void main(String[] args) throws Exception { + ExecutorService executorService = Executors.newFixedThreadPool(3); + SynchronousQueue queue = new SynchronousQueue(true); + AtomicInteger atomicInteger = new AtomicInteger(0); + + Thread thread1 = new Thread(() -> { + try { + queue.put(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread1.start(); + Thread.sleep(200); + + Thread thread2 = new Thread(() -> { + try { + queue.put(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread2.start(); + Thread.sleep(2000); + thread1.interrupt(); + + } +} +``` + +上面例子说明我们启动了两个线程,分别向SynchronousQueue队列中添加了元素1和元素2,添加成功之后的,让主线程休眠一会,然后将第一个线程进行中断操作,添加两个元素后节点所处在的状态为下图所示: +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511194913685-639275710.png) + +当我们调用`thread1.interrupt`时,此时线程1等待的消费操作将被终止,会相应上面`awaitFulfill`方法,该方法会运行下面代码: + +```java +if (w.isInterrupted()) + //尝试取消,将当前节点的item修改为当前节点(this)。 + s.tryCancel(e); +// 获取当前节点内容。 +Object x = s.item; +// 判断当前值和节点值不相同是返回,因为弹出时会将item值赋值为null。 +if (x != e) + return x; +``` + +首先上来现将s节点(上图中的Reference-715引用对象)的item节点设置为当前节点引用(Reference-715引用对象),所以s节点和e=1不相等则直接返回,此时节点的状态变化如下所示: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511194923006-1133504150.png) + +退出`awaitFulfill`并且返回的是s节点内容(实际上返回的就是s节点),接下来返回到调用`awaitFulfill`的方法`transfer`方法中 + +```java +Object x = awaitFulfill(s, e, timed, nanos); +if (x == s) { // 是否是被取消了 + clean(t, s); + return null; +} +``` + +首先判断的事x节点和s节点是否相等,上面我们也说了明显是相等的所以这里会进入到clean方法中,`clean(QNode pred, QNode s)`clean方法一个是前节点,一个是当前被取消的节点,也就是当前s节点的前节点是head节点,接下来我们一步一步的分析代码: + +```java +s.waiter = null; // 删除等待的线程。 +``` + +进入到方法体之后首先先进行的是将当前节点的等待线程删除,如下图所示: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511194930509-319081924.png) + +接下来进入while循环,循环内容时`pred.next == s`如果不是则表示已经移除了节点,反之还在队列中,则进行下面的操作: + +```java +QNode h = head; +QNode hn = h.next; // 如果取消的是第一个节点则进入下面语句 +if (hn != null && hn.isCancelled()) { + advanceHead(h, hn); + continue; +} +``` + +可以看到首先h节点为head节点,hn为头结点的下一个节点,在进行判断头结点的下一个节点不为空并且头结点下一个节点是被中断的节点(取消的节点),则进入到if语句中,if语句其实也很简单就是将头结点修改为头结点的下一个节点(s节点,别取消节点,并且将前节点的next节点修改为自己,也就是移除了之前的节点,我们看下advanceHead方法: + +```java +void advanceHead(QNode h, QNode nh) { + if (h == head && + UNSAFE.compareAndSwapObject(this, headOffset, h, nh)) + h.next = h; // forget old next +} +``` + +首先上来先进行CAS移动头结点,再讲原来头结点h的next节点修改为自己(h),为什么这样做呢?因为上面进行`advanceHead`之后并没有退出循环,是进行continue操作,也就是它并没有跳出while循环,他还会循环一次prev.next此时已经不能等于s所以退出循环,如下图所示: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511194937772-952186611.png) + +**实例二**:清除中间的节点 + +```java +/** + * SynchronousQueue实例二,清除中间的节点。 + * + * @author battleheart + */ +public class SynchronousQueueDemo { + public static void main(String[] args) throws Exception { + ExecutorService executorService = Executors.newFixedThreadPool(3); + SynchronousQueue queue = new SynchronousQueue(true); + AtomicInteger atomicInteger = new AtomicInteger(0); + + Thread thread1 = new Thread(() -> { + try { + queue.put(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread1.start(); + //休眠一会。 + Thread.sleep(200); + Thread thread2 = new Thread(() -> { + try { + queue.put(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread2.start(); + //休眠一会。 + Thread.sleep(200); + Thread thread3 = new Thread(() -> { + try { + queue.put(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread3.start(); + //休眠一会。 + Thread.sleep(10000); + thread2.interrupt(); + + + } +} +``` + +看上面例子,首先先进行put操作三次,也就是入队3条数据,分别是整型值1,整型值2,整型值3,然后将当前线程休眠一下,对中间线程进行中断操作,通过让主线程休眠一会保证线程执行顺序性(当然上面线程不一定能保证执行顺序,因为put操作一下子就执行完了所以这点时间是可以的),此时队列所处的状态来看一下下图: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511194945160-1193138044.png) + +当休眠一会之后,进入到threa2进行中断操作,目前上图中表示`Reference-723`被中断操作,此时也会进入到`awaitFulfill`方法中,将`Reference-723`的item节点修改为当前节点,如下图所示: +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511194951281-1862925421.png) + +进入到clear方法中此时的prev节点为`Reference-715`,s节点是被清除节点,还是首先进入clear方法中先将waiter设置为null,取消当前线程内容,如下图所示: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511194955786-1500616605.png) + +接下来进入到循环中,进行下面处理 + +```java +QNode h = head; +QNode hn = h.next; // Absorb cancelled first node as head +if (hn != null && hn.isCancelled()) { + advanceHead(h, hn); + continue; +} +QNode t = tail; // Ensure consistent read for tail +if (t == h) + return; +QNode tn = t.next; +if (t != tail) + continue; +if (tn != null) { + advanceTail(t, tn); + continue; +} +if (s != t) { // If not tail, try to unsplice + QNode sn = s.next; + if (sn == s || pred.casNext(s, sn)) + return; +} +``` + +第一个if语句已经分析过了所以说这里不会进入到里面去,接下来是进行尾节点t是否是等于head节点如果相等则代表没有元素,在判断当前方法的t尾节点是不是真正的尾节点tail如果不是则进行修改尾节点,先来看一下现在的状态: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195001272-912303463.png) + +`tn != null`判断如果tn不是尾节点,则将tn作为尾节点处理,如果处理之后还不是尾节点还会进行处理直到tail是尾节点未知,我们现在这个是尾节点所以跳过这段代码。`s != t`通过上图可以看到s节点是被清除节点,并不是尾节点所以进入到循环中: + +```java +if (s != t) { // If not tail, try to unsplice + QNode sn = s.next; + if (sn == s || pred.casNext(s, sn)) + return; +} +``` + +首先获取的s节点的下一个节点,上图中表示`Reference-725`节点,判断sn是都等于当前节点显然这一条不成立,pred节点为`Reference-715`节点,将715节点的next节点变成`Reference-725`节点,这里就将原来的节点清理出去了,现在的状态如下所示: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195006304-825474069.png) + +**实例三**:删除的节点是尾节点 + +```java +/** + * SynchronousQueue实例三,删除的节点为尾节点 + * + * @author battleheart + */ +public class SynchronousQueueDemo { + public static void main(String[] args) throws Exception { + ExecutorService executorService = Executors.newFixedThreadPool(3); + SynchronousQueue queue = new SynchronousQueue(true); + AtomicInteger atomicInteger = new AtomicInteger(0); + + Thread thread1 = new Thread(() -> { + try { + queue.put(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread1.start(); + + Thread thread2 = new Thread(() -> { + try { + queue.put(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread2.start(); + + Thread.sleep(10000); + thread2.interrupt(); + + Thread.sleep(10000); + + Thread thread3 = new Thread(() -> { + try { + queue.put(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + thread3.start(); + + Thread.sleep(10000); + thread3.interrupt(); + } +} +``` + +该例子主要说明一个问题就是删除的节点如果是末尾节点的话,`clear`方法又是如何处理的,首先启动了三个线程其中主线程休眠了一会,为了能让插入的顺序保持线程1,线程2,线程3这样子,启动第二个线程后,又将第二个线程中断,这是第二个线程插入的节点为尾节点,然后再启动第三个节点插入值,再中断了第三个节点末尾节点,说一下为啥这样操作,因为当清除尾节点时,并不是直接移除当前节点,而是将被清除的节点的前节点设置到QNode的CleanMe中,等待下次clear方法时进行清除上次保存在CleanMe的节点,然后再处理当前被中断节点,将新的被清理的节点prev设置为cleanMe当中,等待下次进行处理,接下来一步一步分析,首先我们先来看一下第二个线程启动后节点的状态。 + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195014952-2096559474.png) + +此时运行`thread2.interrupt();`将第二个线程中断,这时候会进入到clear方法中,前面的代码都不会被返回,会执行下面的语句: + +```java +QNode dp = cleanMe; +if (dp != null) { // Try unlinking previous cancelled node + QNode d = dp.next; + QNode dn; + if (d == null || // d is gone or + d == dp || // d is off list or + !d.isCancelled() || // d not cancelled or + (d != t && // d not tail and + (dn = d.next) != null && // has successor + dn != d && // that is on list + dp.casNext(d, dn))) // d unspliced + casCleanMe(dp, null); + if (dp == pred) + return; // s is already saved node +} else if (casCleanMe(null, pred)) + return; +``` + +首先获得TransferQueue当中cleanMe节点,此时获取的为null,当判断dp!=null时就会被跳过,直接执行 + +`casCleanMe(null, pred)`此时pred传入的值时t节点指向的内容,也就是当前节点的上一个节点,它会被标记为清除操作节点(其实并不清楚它而是清除它下一个节点,也就是说item=this的节点),此时看一下节点状态为下图所示: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195020658-676114858.png) + +接下来第三个线程启动了这时候又往队列中添加了元素3,此时队列的状况如下图所示: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195025439-1111265737.png) + +此时thread3也被中断操作了,这时候还是运行上面的代码,但是这次不同的点在于cleanMe已经不是空值,是有内容的,首先获取的是cleanMe的下一个节点(d),然我来把变量标记在图上然后看起来好分析一些,如下图所示: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195029976-1603958797.png) + +dp表示d节点的前一个pred节点,dn表示d节点的next节点,主要逻辑在这里: + +```java +if (d == null || // d is gone or + d == dp || // d is off list or + !d.isCancelled() || // d not cancelled or + (d != t && // d not tail and + (dn = d.next) != null && // has successor + dn != d && // that is on list + dp.casNext(d, dn))) // d unspliced + casCleanMe(dp, null); +if (dp == pred) + return; // s +``` + +首先判断d节点是不是为null,如果d节点为null代表已经清除掉了,如果cleanMe节点的下一个节点和自己相等,说明需要清除的节点已经离队了,判断下个节点是不是需要被清除的节点,目前看d节点是被清除的节点,然后就将被清除的节点的下一个节点赋值给dn并且判断d节点是不是末尾节点,如果不是末尾节点则进行`dp.casNext`方法,这个地方是关键点,它将被清除节点d的前节点的next节点修改为被清除节点d的后面节点dn,然后调用caseCleanMe将TransferQueue中的cleanMe节点清空,此时节点的内容如下所示: +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195037209-1474107924.png) + +可以看出将上一次标记为清除的节点清除了队列中,清除完了就完事儿?那这次的怎么弄呢?因为现在运行的是thread3的中断程序,所以上面并没有退出,而是再次进入循环,循环之后发现dp为null则会运行`casCleanMe(null, pred)`,此时当前节点s的前一个节点已经被清除队列,但是并不影响后续的清除操作,因为前节点的next节点还在维护中,也是前节点的next指向还是`reference-725`,如下图所示: + +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/458325-20190511195042216-2002082895.png) + +就此分析完毕如果有不正确的地方请指正。 + + + +> 原文:[图解SynchronousQueue原理-公平模式 - BattleHeart - 博客园 (cnblogs.com)](https://www.cnblogs.com/dwlsxj/p/Thread.html) \ No newline at end of file diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/ThreadLocal.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/ThreadLocal.md" new file mode 100644 index 0000000..77acf7e --- /dev/null +++ "b/346円272円220円347円240円201円345円210円206円346円236円220円/ThreadLocal.md" @@ -0,0 +1,573 @@ +# ThreadLocal源码解读 + +## 1. 背景 + +ThreadLocal源码解读,网上面早已经泛滥了,大多比较浅,甚至有的连基本原理都说的很有问题,包括百度搜索出来的第一篇高访问量博文,说ThreadLocal内部有个map,键为线程对象,太误导人了。 + +ThreadLocal非常适合对Java多线程编程感兴趣的程序员作为入门类阅读,原因两方面: + +1. 加上注释源码也不过七八百行。 +2. 结构清晰,代码简洁。 + +本文重点导读ThreadLocal中的嵌套内部类ThreadLocalMap,对ThreadLocal本身API的介绍简略带过。 +**读ThreadLocal源码,不读ThreadLocalMap的实现,和没看过没多大差别**。 + +## 2. 两个问题 + +先回答两个问题: + +1. 什么是ThreadLocal? + ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。 +2. 它大致的实现思路是怎样的? + Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。 + +## 3. ThreadLocal的API + +ThreadLocal的API其实没多少好介绍的,这些API介绍网上早已烂大街了。 +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/584724-20170430200137319-1696376569.png) + +## 4. ThreadLocalMap的源码实现 + +ThreadLocalMap的源码实现,才是我们读ThreadLocal源码真正要领悟的。看看大师Doug Lea和Joshua Bloch的鬼斧神工之作。 +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/584724-20170430200358928-1369102130.png) + +ThreadLocalMap提供了一种为ThreadLocal定制的高效实现,并且自带一种基于弱引用的垃圾清理机制。 +下面从基本结构开始一点点解读。 + +### 4.1 存储结构 + +既然是个map(注意不要与java.util.map混为一谈,这里指的是概念上的map),当然得要有自己的key和value,上面回答的问题2中也已经提及,我们可以将其简单视作key为ThreadLocal,value为实际放入的值。之所以说是**简单视作**,因为实际上ThreadLocal中存放的是ThreadLocal的弱引用。我们来看看ThreadLocalMap里的节点是如何定义的。 + +```scala +static class Entry extends WeakReference> { + // 往ThreadLocal里实际塞入的值 + Object value; + + Entry(java.lang.ThreadLocal k, Object v) { + super(k); + value = v; + } +} +``` + +Entry便是ThreadLocalMap里定义的节点,它继承了WeakReference类,定义了一个类型为Object的value,用于存放塞到ThreadLocal里的值。 + +### 4.2 为什么要弱引用 + +读到这里,如果不问不答为什么是这样的定义形式,为什么要用弱引用,等于没读懂源码。 +因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。 + +### 4.3 类成员变量与相应方法 + +```cpp +/** + * 初始容量,必须为2的幂 + */ +private static final int INITIAL_CAPACITY = 16; + +/** + * Entry表,大小必须为2的幂 + */ +private Entry[] table; + +/** + * 表里entry的个数 + */ +private int size = 0; + +/** + * 重新分配表大小的阈值,默认为0 + */ +private int threshold; +``` + +可以看到,ThreadLocalMap维护了一个Entry表或者说Entry数组,并且要求表的大小必须为2的幂,同时记录表里面entry的个数以及下一次需要扩容的阈值。 +显然这里会产生一个问题,**为什么必须是2的幂?**很好,但是目前还无法回答,带着问题接着往下读。 + +```cpp +/** + * 设置resize阈值以维持最坏2/3的装载因子 + */ +private void setThreshold(int len) { + threshold = len * 2 / 3; +} + +/** + * 环形意义的下一个索引 + */ +private static int nextIndex(int i, int len) { + return ((i + 1 < len) ? i + 1 : 0); +} + +/** + * 环形意义的上一个索引 + */ +private static int prevIndex(int i, int len) { + return ((i - 1>= 0) ? i - 1 : len - 1); +} +``` + +ThreadLocal需要维持一个最坏2/3的负载因子,对于负载因子相信应该不会陌生,在HashMap中就有这个概念。 +ThreadLocal有两个方法用于得到上一个/下一个索引,注意这里实际上是环形意义下的上一个与下一个。 + +**由于ThreadLocalMap使用线性探测法来解决散列冲突,所以实际上Entry[]数组在程序逻辑上是作为一个环形存在的。** +关于开放寻址、线性探测等内容,可以参考网上资料或者TAOCP(《计算机程序设计艺术》)第三卷的6.4章节。 + +至此,我们已经可以大致勾勒出ThreadLocalMap的内部存储结构。下面是我绘制的示意图。虚线表示弱引用,实线表示强引用。 +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/584724-20170501020337211-761293878.png) + +ThreadLocalMap维护了Entry环形数组,数组中元素Entry的逻辑上的key为某个ThreadLocal对象(实际上是指向该ThreadLocal对象的弱引用),value为代码中该线程往该ThreadLoacl变量实际塞入的值。 + +### 4.4 构造函数 + +好的,接下来再来看看ThreadLocalMap的一个构造函数 + +```verilog +/** + * 构造一个包含firstKey和firstValue的map。 + * ThreadLocalMap是惰性构造的,所以只有当至少要往里面放一个元素的时候才会构建它。 + */ +ThreadLocalMap(java.lang.ThreadLocal firstKey, Object firstValue) { + // 初始化table数组 + table = new Entry[INITIAL_CAPACITY]; + // 用firstKey的threadLocalHashCode与初始大小16取模得到哈希值 + int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); + // 初始化该节点 + table[i] = new Entry(firstKey, firstValue); + // 设置节点表大小为1 + size = 1; + // 设定扩容阈值 + setThreshold(INITIAL_CAPACITY); +} +``` + +这个构造函数在set和get的时候都可能会被间接调用以初始化线程的ThreadLocalMap。 + +### 4.5 哈希函数 + +重点看一下上面构造函数中的`int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);`这一行代码。 + +ThreadLocal类中有一个被final修饰的类型为int的threadLocalHashCode,它在该ThreadLocal被构造的时候就会生成,相当于一个ThreadLocal的ID,而它的值来源于 + +```cpp +/* + * 生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。 + */ +private static final int HASH_INCREMENT = 0x61c88647; + +private static int nextHashCode() { + return nextHashCode.getAndAdd(HASH_INCREMENT); +} +``` + +可以看出,它是在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说 +`(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))`得到的结果就是1640531527也就是0x61c88647。通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。 +ThreadLocalMap使用的是**线性探测法**,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。这就回答了上文抛出的为什么大小要为2的幂的问题。为了优化效率。 + +对于`& (INITIAL_CAPACITY - 1)`,相信有过算法竞赛经验或是阅读源码较多的程序员,一看就明白,对于2的幂作为模数取模,可以用&(2n-1)来替代%2n,位运算比取模效率高很多。至于为什么,因为对2^n取模,只要不是低n位对结果的贡献显然都是0,会影响结果的只能是低n位。 + +**可以说在ThreadLocalMap中,形如`key.threadLocalHashCode & (table.length - 1)`(其中key为一个ThreadLocal实例)这样的代码片段实质上就是在求一个ThreadLocal实例的哈希值,只是在源码实现中没有将其抽为一个公用函数。** + +### 4.6 getEntry方法 + +这个方法会被ThreadLocal的get方法直接调用,用于获取map中某个ThreadLocal存放的值。 + +```csharp +private Entry getEntry(ThreadLocal key) { + // 根据key这个ThreadLocal的ID来获取索引,也即哈希值 + int i = key.threadLocalHashCode & (table.length - 1); + Entry e = table[i]; + // 对应的entry存在且未失效且弱引用指向的ThreadLocal就是key,则命中返回 + if (e != null && e.get() == key) { + return e; + } else { + // 因为用的是线性探测,所以往后找还是有可能能够找到目标Entry的。 + return getEntryAfterMiss(key, i, e); + } +} + +/* + * 调用getEntry未直接命中的时候调用此方法 + */ +private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { + Entry[] tab = table; + int len = tab.length; + + + // 基于线性探测法不断向后探测直到遇到空entry。 + while (e != null) { + ThreadLocal k = e.get(); + // 找到目标 + if (k == key) { + return e; + } + if (k == null) { + // 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry + expungeStaleEntry(i); + } else { + // 环形意义下往后面走 + i = nextIndex(i, len); + } + e = tab[i]; + } + return null; +} + +/** + * 这个函数是ThreadLocal中核心清理函数,它做的事情很简单: + * 就是从staleSlot开始遍历,将无效(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。 + * 另外,在过程中还会对非空的entry作rehash。 + * 可以说这个函数的作用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等) + */ +private int expungeStaleEntry(int staleSlot) { + Entry[] tab = table; + int len = tab.length; + + // 因为entry对应的ThreadLocal已经被回收,value设为null,显式断开强引用 + tab[staleSlot].value = null; + // 显式设置该entry为null,以便垃圾回收 + tab[staleSlot] = null; + size--; + + Entry e; + int i; + for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { + ThreadLocal k = e.get(); + // 清理对应ThreadLocal已经被回收的entry + if (k == null) { + e.value = null; + tab[i] = null; + size--; + } else { + /* + * 对于还没有被回收的情况,需要做一次rehash。 + * + * 如果对应的ThreadLocal的ID对len取模出来的索引h不为当前位置i, + * 则从h向后线性探测到第一个空的slot,把当前的entry给挪过去。 + */ + int h = k.threadLocalHashCode & (len - 1); + if (h != i) { + tab[i] = null; + + /* + * 在原代码的这里有句注释值得一提,原注释如下: + * + * Unlike Knuth 6.4 Algorithm R, we must scan until + * null because multiple entries could have been stale. + * + * 这段话提及了Knuth高德纳的著作TAOCP(《计算机程序设计艺术》)的6.4章节(散列) + * 中的R算法。R算法描述了如何从使用线性探测的散列表中删除一个元素。 + * R算法维护了一个上次删除元素的index,当在非空连续段中扫到某个entry的哈希值取模后的索引 + * 还没有遍历到时,会将该entry挪到index那个位置,并更新当前位置为新的index, + * 继续向后扫描直到遇到空的entry。 + * + * ThreadLocalMap因为使用了弱引用,所以其实每个slot的状态有三种也即 + * 有效(value未回收),无效(value已回收),空(entry==null)。 + * 正是因为ThreadLocalMap的entry有三种状态,所以不能完全套高德纳原书的R算法。 + * + * 因为expungeStaleEntry函数在扫描过程中还会对无效slot清理将之转为空slot, + * 如果直接套用R算法,可能会出现具有相同哈希值的entry之间断开(中间有空entry)。 + */ + while (tab[h] != null) { + h = nextIndex(h, len); + } + tab[h] = e; + } + } + } + // 返回staleSlot之后第一个空的slot索引 + return i; +} +``` + +我们来回顾一下从ThreadLocal读一个值可能遇到的情况: +根据入参threadLocal的threadLocalHashCode对表容量取模得到index + +- 如果index对应的slot就是要读的threadLocal,则直接返回结果 +- 调用getEntryAfterMiss线性探测,过程中每碰到无效slot,调用expungeStaleEntry进行段清理;如果找到了key,则返回结果entry +- 没有找到key,返回null + +### 4.7 set方法 + +```java +private void set(ThreadLocal key, Object value) { + + Entry[] tab = table; + int len = tab.length; + int i = key.threadLocalHashCode & (len - 1); + // 线性探测 + for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { + ThreadLocal k = e.get(); + // 找到对应的entry + if (k == key) { + e.value = value; + return; + } + // 替换失效的entry + if (k == null) { + replaceStaleEntry(key, value, i); + return; + } + } + + tab[i] = new Entry(key, value); + int sz = ++size; + if (!cleanSomeSlots(i, sz) && sz>= threshold) { + rehash(); + } +} + +private void replaceStaleEntry(ThreadLocal key, Object value, + int staleSlot) { + Entry[] tab = table; + int len = tab.length; + Entry e; + + // 向前扫描,查找最前的一个无效slot + int slotToExpunge = staleSlot; + for (int i = prevIndex(staleSlot, len); + (e = tab[i]) != null; + i = prevIndex(i, len)) { + if (e.get() == null) { + slotToExpunge = i; + } + } + + // 向后遍历table + for (int i = nextIndex(staleSlot, len); + (e = tab[i]) != null; + i = nextIndex(i, len)) { + ThreadLocal k = e.get(); + + // 找到了key,将其与无效的slot交换 + if (k == key) { + // 更新对应slot的value值 + e.value = value; + + tab[i] = tab[staleSlot]; + tab[staleSlot] = e; + + /* + * 如果在整个扫描过程中(包括函数一开始的向前扫描与i之前的向后扫描) + * 找到了之前的无效slot则以那个位置作为清理的起点, + * 否则则以当前的i作为清理起点 + */ + if (slotToExpunge == staleSlot) { + slotToExpunge = i; + } + // 从slotToExpunge开始做一次连续段的清理,再做一次启发式清理 + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); + return; + } + + // 如果当前的slot已经无效,并且向前扫描过程中没有无效slot,则更新slotToExpunge为当前位置 + if (k == null && slotToExpunge == staleSlot) { + slotToExpunge = i; + } + } + + // 如果key在table中不存在,则在原地放一个即可 + tab[staleSlot].value = null; + tab[staleSlot] = new Entry(key, value); + + // 在探测过程中如果发现任何无效slot,则做一次清理(连续段清理+启发式清理) + if (slotToExpunge != staleSlot) { + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); + } +} + +/** + * 启发式地清理slot, + * i对应entry是非无效(指向的ThreadLocal没被回收,或者entry本身为空) + * n是用于控制控制扫描次数的 + * 正常情况下如果log n次扫描没有发现无效slot,函数就结束了 + * 但是如果发现了无效的slot,将n置为table的长度len,做一次连续段的清理 + * 再从下一个空的slot开始继续扫描 + * + * 这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效slot的时候可能会被调用, + * 区别是前者传入的n为元素个数,后者为table的容量 + */ +private boolean cleanSomeSlots(int i, int n) { + boolean removed = false; + Entry[] tab = table; + int len = tab.length; + do { + // i在任何情况下自己都不会是一个无效slot,所以从下一个开始判断 + i = nextIndex(i, len); + Entry e = tab[i]; + if (e != null && e.get() == null) { + // 扩大扫描控制因子 + n = len; + removed = true; + // 清理一个连续段 + i = expungeStaleEntry(i); + } + } while ((n>>>= 1) != 0); + return removed; +} + + +private void rehash() { + // 做一次全量清理 + expungeStaleEntries(); + + /* + * 因为做了一次清理,所以size很可能会变小。 + * ThreadLocalMap这里的实现是调低阈值来判断是否需要扩容, + * threshold默认为len*2/3,所以这里的threshold - threshold / 4相当于len/2 + */ + if (size>= threshold - threshold / 4) { + resize(); + } +} + +/* + * 做一次全量清理 + */ +private void expungeStaleEntries() { + Entry[] tab = table; + int len = tab.length; + for (int j = 0; j < len; j++) { + Entry e = tab[j]; + if (e != null && e.get() == null) { + /* + * 个人觉得这里可以取返回值,如果大于j的话取了用,这样也是可行的。 + * 因为expungeStaleEntry执行过程中是把连续段内所有无效slot都清理了一遍了。 + */ + expungeStaleEntry(j); + } + } +} + +/** + * 扩容,因为需要保证table的容量len为2的幂,所以扩容即扩大2倍 + */ +private void resize() { + Entry[] oldTab = table; + int oldLen = oldTab.length; + int newLen = oldLen * 2; + Entry[] newTab = new Entry[newLen]; + int count = 0; + + for (int j = 0; j < oldLen; ++j) { + Entry e = oldTab[j]; + if (e != null) { + ThreadLocal k = e.get(); + if (k == null) { + e.value = null; + } else { + // 线性探测来存放Entry + int h = k.threadLocalHashCode & (newLen - 1); + while (newTab[h] != null) { + h = nextIndex(h, newLen); + } + newTab[h] = e; + count++; + } + } + } + + setThreshold(newLen); + size = count; + table = newTab; +} +``` + +我们来回顾一下ThreadLocal的set方法可能会有的情况 + +- 探测过程中slot都不无效,并且顺利找到key所在的slot,直接替换即可 +- 探测过程中发现有无效slot,调用replaceStaleEntry,效果是最终一定会把key和value放在这个slot,并且会尽可能清理无效slot + - 在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值 + - 在replaceStaleEntry过程中,没有找到key,直接在无效slot原地放entry +- 探测没有发现key,则在连续段末尾的后一个空位置放上entry,这也是线性探测法的一部分。放完后,做一次启发式清理,如果没清理出去key,并且当前table大小已经超过阈值了,则做一次rehash,rehash函数会调用一次全量清理slot方法也即expungeStaleEntries,如果完了之后table大小超过了threshold - threshold / 4,则进行扩容2倍 + +### 4.8 remove方法 + +```csharp +/** + * 从map中删除ThreadLocal + */ +private void remove(ThreadLocal key) { + Entry[] tab = table; + int len = tab.length; + int i = key.threadLocalHashCode & (len - 1); + for (Entry e = tab[i]; + e != null; + e = tab[i = nextIndex(i, len)]) { + if (e.get() == key) { + // 显式断开弱引用 + e.clear(); + // 进行段清理 + expungeStaleEntry(i); + return; + } + } +} +``` + +remove方法相对于getEntry和set方法比较简单,直接在table中找key,如果找到了,把弱引用断了做一次段清理。 + +## 5. ThreadLocal与内存泄漏 + +关于ThreadLocal是否会引起内存泄漏也是一个比较有争议性的问题,其实就是要看对内存泄漏的准确定义是什么。 +认为ThreadLocal会引起内存泄漏的说法是因为如果一个ThreadLocal对象被回收了,我们往里面放的value对于**【当前线程->当前线程的threadLocals(ThreadLocal.ThreadLocalMap对象)->Entry数组->某个entry.value】**这样一条强引用链是可达的,因此value不会被回收。 +认为ThreadLocal不会引起内存泄漏的说法是因为ThreadLocal.ThreadLocalMap源码实现中自带一套自我清理的机制。 + +之所以有关于内存泄露的讨论是因为在有线程复用如线程池的场景中,一个线程的寿命很长,大对象长期不被回收影响系统运行效率与安全。如果线程不会复用,用完即销毁了也不会有ThreadLocal引发内存泄露的问题。《Effective Java》一书中的第6条对这种内存泄露称为`unintentional object retention`(无意识的对象保留)。 + +当我们仔细读过ThreadLocalMap的源码,我们可以推断,如果在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。 +那么如果没有显式地进行remove呢?只能说如果对应线程之后调用ThreadLocal的get和set方法都有**很高的概率**会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。 + +但无论如何,我们应该考虑到何时调用ThreadLocal的remove方法。一个比较熟悉的场景就是对于一个请求一个线程的server如tomcat,在代码中对web api作一个切面,存放一些如用户名等用户信息,在连接点方法结束后,再显式调用remove。 + +## 6. InheritableThreadLocal原理 + +对于InheritableThreadLocal,本文不作过多介绍,只是简单略过。 +ThreadLocal本身是线程隔离的,InheritableThreadLocal提供了一种父子线程之间的数据共享机制。 + +它的具体实现是在Thread类中除了threadLocals外还有一个`inheritableThreadLocals`对象。 +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/584724-20170520151532228-109069867.png) + +在线程对象初始化的时候,会调用ThreadLocal的`createInheritedMap`从父线程的`inheritableThreadLocals`中把有效的entry都拷过来 +![img](https://itsaysay-1313174343.cos.ap-shanghai.myqcloud.com/blog/584724-20170520151800494-707226209.png) + +可以看一下其中的具体实现 + +```java +private ThreadLocalMap(ThreadLocalMap parentMap) { + Entry[] parentTable = parentMap.table; + int len = parentTable.length; + setThreshold(len); + table = new Entry[len]; + + for (int j = 0; j < len; j++) { + Entry e = parentTable[j]; + if (e != null) { + @SuppressWarnings("unchecked") + ThreadLocal key = (ThreadLocal) e.get(); + if (key != null) { + // 这里的childValue方法在InheritableThreadLocal中默认实现为返回本身值,可以被重写 + Object value = key.childValue(e.value); + Entry c = new Entry(key, value); + int h = key.threadLocalHashCode & (len - 1); + while (table[h] != null) + h = nextIndex(h, len); + table[h] = c; + size++; + } + } + } +} +``` + +还是比较简单的,做的事情就是以父线程的`inheritableThreadLocalMap`为数据源,过滤出有效的entry,初始化到自己的`inheritableThreadLocalMap`中。其中childValue可以被重写。 + +需要注意的地方是`InheritableThreadLocal`只是在子线程创建的时候会去拷一份父线程的`inheritableThreadLocals`。如果父线程是在子线程创建后再set某个InheritableThreadLocal对象的值,对子线程是不可见的。 + +## 7. 总结 + +本博文重点介绍了ThreadLocal中ThreadLocalMap的大致实现原理以及ThreadLocal内存泄露的问题以及简略介绍InheritableThreadLocal。作为Josh Bloch和Doug Lea两位大师之作,ThreadLocal本身实现的算法与技巧还是很优雅的。在开发过程中,ThreadLocal用到恰到好处的话,可以消除一些代码的重复。但也要注意过度使用ThreadLocal很容易加大类之间的耦合度与依赖关系(开发过程可能会不得不过度考虑某个ThreadLocal在调用时是否已有值,存放的是哪个类放的什么值)。 + + + +> 原文:[ThreadLocal源码解读 - 活在夢裡 - 博客园 (cnblogs.com)](https://www.cnblogs.com/micrari/p/6790229.html) \ No newline at end of file diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/Unsafe.md" "b/346円272円220円347円240円201円345円210円206円346円236円220円/Unsafe.md" old mode 100755 new mode 100644 diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/media/5cd1ba2adf7c0-20191214003715601.jpg" "b/346円272円220円347円240円201円345円210円206円346円236円220円/media/5cd1ba2adf7c0-20191214003715601.jpg" old mode 100755 new mode 100644 diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/media/5cd1ba2adf7c0.jpg" "b/346円272円220円347円240円201円345円210円206円346円236円220円/media/5cd1ba2adf7c0.jpg" old mode 100755 new mode 100644 diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/media/5cd1ba2d418b6-20191214003733877.jpg" "b/346円272円220円347円240円201円345円210円206円346円236円220円/media/5cd1ba2d418b6-20191214003733877.jpg" old mode 100755 new mode 100644 diff --git "a/346円272円220円347円240円201円345円210円206円346円236円220円/media/5cd1ba2d418b6.jpg" "b/346円272円220円347円240円201円345円210円206円346円236円220円/media/5cd1ba2d418b6.jpg" old mode 100755 new mode 100644

AltStyle によって変換されたページ (->オリジナル) /