diff --git a/79884.log b/79884.log index d09b8c77..3b791505 100644 --- a/79884.log +++ b/79884.log @@ -9,30 +9,30 @@ Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.131-b11 mixed mode): "Blocked2" #13 prio=5 os_prio=31 tid=0x00007ffd7b08b000 nid=0x5503 waiting for monitor entry [0x00007000083d1000] java.lang.Thread.State: BLOCKED (on object monitor) - at com.crossoverjie.concurrent.ThreadState$Blocked.run(ThreadState.java:59) - - waiting to lock <0x000000079576cd60> (a java.lang.Class for com.crossoverjie.concurrent.ThreadState$Blocked) + at com.crossoverjie.thread.ThreadState$Blocked.run(ThreadState.java:59) + - waiting to lock <0x000000079576cd60> (a java.lang.Class for com.crossoverjie.thread.ThreadState$Blocked) at java.lang.Thread.run(Thread.java:748) "Blocked1" #12 prio=5 os_prio=31 tid=0x00007ffd7b08a000 nid=0x5303 waiting on condition [0x00007000082ce000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) - at com.crossoverjie.concurrent.ThreadState$Blocked.run(ThreadState.java:59) - - locked <0x000000079576cd60> (a java.lang.Class for com.crossoverjie.concurrent.ThreadState$Blocked) + at com.crossoverjie.thread.ThreadState$Blocked.run(ThreadState.java:59) + - locked <0x000000079576cd60> (a java.lang.Class for com.crossoverjie.thread.ThreadState$Blocked) at java.lang.Thread.run(Thread.java:748) "Waiting" #11 prio=5 os_prio=31 tid=0x00007ffd7b089800 nid=0x5103 in Object.wait() [0x00007000081cb000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - - waiting on <0x0000000795768db0> (a java.lang.Class for com.crossoverjie.concurrent.ThreadState$Waiting) + - waiting on <0x0000000795768db0> (a java.lang.Class for com.crossoverjie.thread.ThreadState$Waiting) at java.lang.Object.wait(Object.java:502) - at com.crossoverjie.concurrent.ThreadState$Waiting.run(ThreadState.java:42) - - locked <0x0000000795768db0> (a java.lang.Class for com.crossoverjie.concurrent.ThreadState$Waiting) + at com.crossoverjie.thread.ThreadState$Waiting.run(ThreadState.java:42) + - locked <0x0000000795768db0> (a java.lang.Class for com.crossoverjie.thread.ThreadState$Waiting) at java.lang.Thread.run(Thread.java:748) "TimeWaiting" #10 prio=5 os_prio=31 tid=0x00007ffd7b82c800 nid=0x4f03 waiting on condition [0x00007000080c8000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) - at com.crossoverjie.concurrent.ThreadState$TimeWaiting.run(ThreadState.java:27) + at com.crossoverjie.thread.ThreadState$TimeWaiting.run(ThreadState.java:27) at java.lang.Thread.run(Thread.java:748) "Monitor Ctrl-Break" #9 daemon prio=5 os_prio=31 tid=0x00007ffd7a97e000 nid=0x4d03 runnable [0x0000700007fc5000] diff --git a/MD/GarbageCollection.md b/MD/GarbageCollection.md index b6253e3f..97038762 100644 --- a/MD/GarbageCollection.md +++ b/MD/GarbageCollection.md @@ -33,7 +33,7 @@ ### 标记-清除算法 标记清除算法分为两个步骤,标记和清除。 -首先将**不需要回收的对象**标记起来,然后再清除其余可回收对象。但是存在两个主要的问题: +首先将**需要回收的对象**标记起来,在标记完成后统一回收所有被标记的对象。但是存在两个主要的问题: - 标记和清除的效率都不高。 - 清除之后容易出现不连续内存,当需要分配一个较大内存时就不得不需要进行一次垃圾回收。 diff --git a/MD/HashMap.md b/MD/HashMap.md index e6cbd7c7..bcebd2d5 100644 --- a/MD/HashMap.md +++ b/MD/HashMap.md @@ -66,7 +66,7 @@ map.forEach((key,value)->{ > 所以 HashMap 只能在单线程中使用,并且尽量的预设容量,尽可能的减少扩容。 在 `JDK1.8` 中对 `HashMap` 进行了优化: -当 `hash` 碰撞之后写入链表的长度超过了阈值(默认为8),链表将会转换为**红黑树**。 +当 `hash` 碰撞之后写入链表的长度超过了阈值(默认为8)并且 `table` 的长度不小于64(否则扩容一次)时,链表将会转换为**红黑树**。 假设 `hash` 冲突非常严重,一个数组后面接了很长的链表,此时重新的时间复杂度就是 `O(n)` 。 diff --git a/MD/ThreadPoolExecutor.md b/MD/ThreadPoolExecutor.md index 17120a7f..616601b1 100644 --- a/MD/ThreadPoolExecutor.md +++ b/MD/ThreadPoolExecutor.md @@ -86,7 +86,7 @@ threadPool.execute(new Job()); 1. 获取当前线程池的状态。 2. 当前线程数量小于 coreSize 时创建一个新的线程运行。 3. 如果当前线程处于运行状态,并且写入阻塞队列成功。 -4. 双重检查,再次获取线程状态;如果线程状态变了(非运行状态)就需要从阻塞队列移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 +4. 双重检查,再次获取线程池状态;如果线程池状态变了(非运行状态)就需要从阻塞队列移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 5. 如果当前线程池为空就新创建一个线程并执行。 6. 如果在第三步的判断为非运行状态,尝试新建线程,如果失败则执行拒绝策略。 diff --git a/README.md b/README.md index 487cd48a..06e30f8d 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ - [ConcurrentHashMap 的实现原理](https://github.com/crossoverJie/JCSprout/blob/master/MD/ConcurrentHashMap.md) - [如何优雅的使用和理解线程池](https://github.com/crossoverJie/JCSprout/blob/master/MD/ThreadPoolExecutor.md) - [深入理解线程通信](https://github.com/crossoverJie/JCSprout/blob/master/MD/concurrent/thread-communication.md) +- [一个线程罢工的诡异事件](docs/thread/thread-gone.md) +- [线程池中你不容错过的一些细节](docs/thread/thread-gone2.md) +- [『并发包入坑指北』之阻塞队列](docs/thread/ArrayBlockingQueue.md) ### JVM - [Java 运行时内存划分](https://github.com/crossoverJie/JCSprout/blob/master/MD/MemoryAllocation.md) @@ -67,10 +70,8 @@ - [Spring AOP 的实现原理](https://github.com/crossoverJie/JCSprout/blob/master/MD/SpringAOP.md) - [Guava 源码分析(Cache 原理)](https://crossoverjie.top/2018/06/13/guava/guava-cache/) - [轻量级 HTTP 框架](https://github.com/crossoverJie/cicada) -- [Kakfa produce 源码分析](https://github.com/crossoverJie/JCSprout/blob/master/MD/kafka/kafka-product.md) +- [Kafka produce 源码分析](https://github.com/crossoverJie/JCSprout/blob/master/MD/kafka/kafka-product.md) - [Kafka 消费实践](https://github.com/crossoverJie/JCSprout/blob/master/docs/frame/kafka-consumer.md) -- SpringBoot 启动过程 -- Tomcat 类加载机制 ### 架构设计 @@ -83,6 +84,7 @@ - [MySQL 索引原理](https://github.com/crossoverJie/JCSprout/blob/master/MD/MySQL-Index.md) - [SQL 优化](https://github.com/crossoverJie/JCSprout/blob/master/MD/SQL-optimization.md) - [数据库水平垂直拆分](https://github.com/crossoverJie/JCSprout/blob/master/MD/DB-split.md) +- [一次分表踩坑实践的探讨](docs/db/sharding-db.md) ### 数据结构与算法 - [红包算法](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/red/RedPacket.java) @@ -90,7 +92,8 @@ - [是否为快乐数字](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/algorithm/HappyNum.java#L38-L55) - [链表是否有环](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/algorithm/LinkLoop.java#L32-L59) - [从一个数组中返回两个值相加等于目标值的下标](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/algorithm/TwoSum.java#L38-L59) -- [一致性 Hash 算法](https://github.com/crossoverJie/JCSprout/blob/master/MD/Consistent-Hash.md) +- [一致性 Hash 算法原理](https://github.com/crossoverJie/JCSprout/blob/master/MD/Consistent-Hash.md) +- [一致性 Hash 算法实践](https://github.com/crossoverJie/JCSprout/blob/master/docs/algorithm/consistent-hash-implement.md) - [限流算法](https://github.com/crossoverJie/JCSprout/blob/master/MD/Limiting.md) - [三种方式反向打印单向链表](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/algorithm/ReverseNode.java) - [合并两个排好序的链表](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/algorithm/MergeTwoSortedLists.java) diff --git a/docs/README.md b/docs/README.md index 3aabcbfb..ff56f8e0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -55,7 +55,7 @@ **欢迎我的关注公众号一起交流:** -![](https://ws3.sinaimg.cn/large/006tKfTcgy1fsuvb4ebtmj30760760t7.jpg) +![](https://crossoverjie.top/uploads/weixinfooter1.jpg) diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 13fa6f7c..50f96c93 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -16,6 +16,9 @@ - [ConcurrentHashMap 的实现原理](thread/ConcurrentHashMap.md) - [如何优雅的使用和理解线程池](thread/ThreadPoolExecutor.md) - [深入理解线程通信](thread/thread-communication.md) + - [一个线程罢工的诡异事件](thread/thread-gone.md) + - [线程池中你不容错过的一些细节](thread/thread-gone2.md) + - [『并发包入坑指北』之阻塞队列](thread/ArrayBlockingQueue.md) - JVM @@ -41,10 +44,9 @@ - [Spring Bean 生命周期](frame/spring-bean-lifecycle.md) - [Spring AOP 的实现原理](frame/SpringAOP.md) - [Guava 源码分析(Cache 原理)](frame/guava-cache.md) - - [Kakfa produce 源码分析](frame/kafka-product.md) + - [Kafka produce 源码分析](frame/kafka-product.md) - [Kafka 消费实践](frame/kafka-consumer.md) - - SpringBoot 启动过程 - - Tomcat 类加载机制 + - 架构设计 @@ -57,11 +59,13 @@ - [MySQL 索引原理](db/MySQL-Index.md) - [SQL 优化](db/SQL-optimization.md) - [数据库水平垂直拆分](db/DB-split.md) + - [一次分表踩坑实践的探讨](db/sharding-db.md) - 数据结构与算法 - [常见算法](algorithm/common-algorithm.md) - - [一致性 Hash 算法](algorithm/Consistent-Hash.md) + - [一致性 Hash 算法原理](algorithm/Consistent-Hash.md) + - [一致性 Hash 算法实践](algorithm/consistent-hash-implement.md) - [限流算法](algorithm/Limiting.md) - [动手实现一个 LRU cache](algorithm/LRU-cache.md) - [亿级数据中判断数据是否不存在](algorithm/guava-bloom-filter.md) diff --git a/docs/algorithm/Consistent-Hash.md b/docs/algorithm/Consistent-Hash.md index 4c778cfa..83c433f3 100644 --- a/docs/algorithm/Consistent-Hash.md +++ b/docs/algorithm/Consistent-Hash.md @@ -17,22 +17,22 @@ 一致 Hash 算法是将所有的哈希值构成了一个环,其范围在 `0 ~ 2^32-1`。如下图: -![](https://ws1.sinaimg.cn/large/006tNc79gy1fn8kbmd4ncj30ad08y3yn.jpg) +![](https://i.loli.net/2019/06/26/5d13931ace0d988790.jpg) 之后将各个节点散列到这个环上,可以用节点的 IP、hostname 这样的唯一性字段作为 Key 进行 `hash(key)`,散列之后如下: -![](https://ws3.sinaimg.cn/large/006tNc79gy1fn8kf72uwuj30a40a70t5.jpg) +![](https://i.loli.net/2019/06/26/5d13931b42d3941564.jpg) 之后需要将数据定位到对应的节点上,使用同样的 `hash 函数` 将 Key 也映射到这个环上。 -![](https://ws3.sinaimg.cn/large/006tNc79gy1fn8kj9kd4oj30ax0aomxq.jpg) +![](https://i.loli.net/2019/06/26/5d13931b811c782755.jpg) 这样按照顺时针方向就可以把 k1 定位到 `N1节点`,k2 定位到 `N3节点`,k3 定位到 `N2节点`。 ### 容错性 这时假设 N1 宕机了: -![](https://ws3.sinaimg.cn/large/006tNc79gy1fn8kl9pp06j30a409waaj.jpg) +![](https://i.loli.net/2019/06/26/5d13931ba4a0869451.jpg) 依然根据顺时针方向,k2 和 k3 保持不变,只有 k1 被重新映射到了 N3。这样就很好的保证了容错性,当一个节点宕机时只会影响到少少部分的数据。 @@ -40,7 +40,7 @@ 当新增一个节点时: -![](https://ws1.sinaimg.cn/large/006tNc79gy1fn8kp1fc9xj30ca0abt9c.jpg) +![](https://i.loli.net/2019/06/26/5d13931bc818391034.jpg) 在 N2 和 N3 之间新增了一个节点 N4 ,这时会发现受印象的数据只有 k3,其余数据也是保持不变,所以这样也很好的保证了拓展性。 @@ -49,14 +49,14 @@ 当节点较少时会出现数据分布不均匀的情况: -![](https://ws2.sinaimg.cn/large/006tNc79gy1fn8krttekbj30c10a5dg5.jpg) +![](https://i.loli.net/2019/06/26/5d13931c0392a99489.jpg) 这样会导致大部分数据都在 N1 节点,只有少量的数据在 N2 节点。 为了解决这个问题,一致哈希算法引入了虚拟节点。将每一个节点都进行多次 hash,生成多个节点放置在环上称为虚拟节点: -![](https://ws2.sinaimg.cn/large/006tNc79gy1fn8ktzuswkj30ae0abdgb.jpg) +![](https://i.loli.net/2019/06/26/5d13931c3e2f146589.jpg) 计算时可以在 IP 后加上编号来生成哈希值。 -这样只需要在原有的基础上多一步由虚拟节点映射到实际节点的步骤即可让少量节点也能满足均匀性。 \ No newline at end of file +这样只需要在原有的基础上多一步由虚拟节点映射到实际节点的步骤即可让少量节点也能满足均匀性。 diff --git a/docs/algorithm/LRU-cache.md b/docs/algorithm/LRU-cache.md index 97fa6089..ca512d10 100644 --- a/docs/algorithm/LRU-cache.md +++ b/docs/algorithm/LRU-cache.md @@ -1,4 +1,4 @@ -![](https://ws1.sinaimg.cn/large/006tNc79gy1fq3fey7n97j31340o8myw.jpg) +![](https://i.loli.net/2019/06/26/5d13931b1ef2443865.jpg) ## 前言 LRU 是 `Least Recently Used` 的简写,字面意思则是`最近最少使用`。 @@ -676,7 +676,7 @@ public class LRUMap { ### 初始化时 -![](https://ws1.sinaimg.cn/large/006tNc79gy1fq3h4xsf4cj30dh09hglr.jpg) +![](https://i.loli.net/2019/06/26/5d13931b9416744111.jpg) ### 写入数据时 @@ -685,24 +685,24 @@ LRUMap lruMap = new LRUMap(3) ; lruMap.put("1",1) ; ``` -![](https://ws4.sinaimg.cn/large/006tNc79gy1fq3h892nalj30ef09jdg2.jpg) +![](https://i.loli.net/2019/06/26/5d13931c136d238581.jpg) ```java lruMap.put("2",2) ; ``` -![](https://ws3.sinaimg.cn/large/006tNc79gy1fq3hayffy1j30jr0b6q3a.jpg) +![](https://i.loli.net/2019/06/26/5d1393217488285452.jpg) ```java lruMap.put("3",3) ; ``` -![](https://ws4.sinaimg.cn/large/006tNc79gy1fq3hcfq95pj30gp0bot93.jpg) +![](https://i.loli.net/2019/06/26/5d139321e34f996391.jpg) ```java lruMap.put("4",4) ; ``` -![](https://ws1.sinaimg.cn/large/006tNc79gy1fq3hfl5r8ij30kn0b374s.jpg) +![](https://i.loli.net/2019/06/26/5d139322609e214433.jpg) ### 获取数据时 @@ -713,7 +713,7 @@ lruMap.put("4",4) ; Integer integer = lruMap.get("2"); ``` -![](https://ws2.sinaimg.cn/large/006tNc79gy1fq3hjbou5pj30k70aj3yy.jpg) +![](https://i.loli.net/2019/06/26/5d139322ea89567527.jpg) 通过以上几张图应该是很好理解数据是如何存放的了。 diff --git a/docs/algorithm/consistent-hash-implement.md b/docs/algorithm/consistent-hash-implement.md new file mode 100644 index 00000000..fb669b4e --- /dev/null +++ b/docs/algorithm/consistent-hash-implement.md @@ -0,0 +1,307 @@ + +![](https://i.loli.net/2019/05/08/5cd1be999402c.jpg) + +# 前言 + +记得一年前分享过一篇[《一致性 Hash 算法分析》](https://crossoverjie.top/2018/01/08/Consistent-Hash/),当时只是分析了这个算法的实现原理、解决了什么问题等。 + +但没有实际实现一个这样的算法,毕竟要加深印象还得自己撸一遍,于是本次就当前的一个路由需求来着手实现一次。 + +# 背景 + +看过[《为自己搭建一个分布式 IM(即时通讯) 系统》](https://crossoverjie.top/2019/01/02/netty/cim01-started/)的朋友应该对其中的登录逻辑有所印象。 + + +> 先给新来的朋友简单介绍下 [cim](https://github.com/crossoverJie/cim) 是干啥的: + +![](https://i.loli.net/2019/05/08/5cd1be99f3bb2.jpg) + +其中有一个场景是在客户端登录成功后需要从可用的服务端列表中选择一台服务节点返回给客户端使用。 + +而这个选择的过程就是一个负载策略的过程;第一版本做的比较简单,默认只支持轮询的方式。 + +虽然够用,但不够优雅😏。 + +**因此我的规划是内置多种路由策略供使用者根据自己的场景选择,同时提供简单的 API 供用户自定义自己的路由策略。** + + +先来看看一致性 Hash 算法的一些特点: + +- 构造一个 `0 ~ 2^32-1` 大小的环。 +- 服务节点经过 hash 之后将自身存放到环中的下标中。 +- 客户端根据自身的某些数据 hash 之后也定位到这个环中。 +- 通过顺时针找到离他最近的一个节点,也就是这次路由的服务节点。 +- 考虑到服务节点的个数以及 hash 算法的问题导致环中的数据分布不均匀时引入了虚拟节点。 + +![](https://i.loli.net/2019/05/08/5cd1be9b0e4e3.jpg) + +# 自定义有序 Map + +根据这些客观条件我们很容易想到通过自定义一个**有序**数组来模拟这个环。 + +这样我们的流程如下: + +1. 初始化一个长度为 N 的数组。 +2. 将服务节点通过 hash 算法得到的正整数,同时将节点自身的数据(hashcode、ip、端口等)存放在这里。 +3. 完成节点存放后将整个数组进行排序(排序算法有多种)。 +4. 客户端获取路由节点时,将自身进行 hash 也得到一个正整数; +5. 遍历这个数组直到找到一个数据大于等于当前客户端的 hash 值,就将当前节点作为该客户端所路由的节点。 +6. 如果没有发现比客户端大的数据就返回第一个节点(满足环的特性)。 + +先不考虑排序所消耗的时间,单看这个路由的时间复杂度: +- 最好是第一次就找到,时间复杂度为`O(1)`。 +- 最差为遍历完数组后才找到,时间复杂度为`O(N)`。 + +理论讲完了来看看具体实践。 + +我自定义了一个类:`SortArrayMap` + +他的使用方法及结果如下: + +![](https://i.loli.net/2019/05/08/5cd1be9b8278e.jpg) + +![](https://i.loli.net/2019/05/08/5cd1be9bb786e.jpg) + +可见最终会按照 `key` 的大小进行排序,同时传入 `hashcode = 101` 时会按照顺时针找到 `hashcode = 1000` 这个节点进行返回。 + +---- +下面来看看具体的实现。 + +成员变量和构造函数如下: + +![](https://i.loli.net/2019/05/08/5cd1be9c182fe.jpg) + +其中最核心的就是一个 `Node` 数组,用它来存放服务节点的 `hashcode` 以及 `value` 值。 + +其中的内部类 `Node` 结构如下: + +![](https://i.loli.net/2019/05/08/5cd1be9c6be0b.jpg) + +---- + +写入数据的方法如下: + +![](https://i.loli.net/2019/05/08/5cd1bea38b4ab.jpg) + +相信看过 `ArrayList` 的源码应该有印象,这里的写入逻辑和它很像。 + +- 写入之前判断是否需要扩容,如果需要则复制原来大小的 1.5 倍数组来存放数据。 +- 之后就写入数组,同时数组大小 +1。 + +但是存放时是按照写入顺序存放的,遍历时自然不会有序;因此提供了一个 `Sort` 方法,可以把其中的数据按照 `key` 其实也就是 `hashcode` 进行排序。 + +![](https://i.loli.net/2019/05/08/5cd1bea416c01.jpg) + +排序也比较简单,使用了 `Arrays` 这个数组工具进行排序,它其实是使用了一个 `TimSort` 的排序算法,效率还是比较高的。 + +最后则需要按照一致性 Hash 的标准顺时针查找对应的节点: + +![](https://i.loli.net/2019/05/08/5cd1bea459788.jpg) + +代码还是比较简单清晰的;遍历数组如果找到比当前 key 大的就返回,没有查到就取第一个。 + +这样就基本实现了一致性 Hash 的要求。 + +> ps:这里并不包含具体的 hash 方法以及虚拟节点等功能(具体实现请看下文),这个可以由使用者来定,SortArrayMap 可作为一个底层的数据结构,提供有序 Map 的能力,使用场景也不局限于一致性 Hash 算法中。 + +# TreeMap 实现 + +`SortArrayMap` 虽说是实现了一致性 hash 的功能,但效率还不够高,主要体现在 `sort` 排序处。 + +下图是目前主流排序算法的时间复杂度: + +![](https://i.loli.net/2019/05/08/5cd1bea49b947.jpg) + +最好的也就是 `O(N)` 了。 + +这里完全可以换一个思路,不用对数据进行排序;而是在写入的时候就排好顺序,只是这样会降低写入的效率。 + +比如二叉查找树,这样的数据结构 `jdk` 里有现成的实现;比如 `TreeMap` 就是使用红黑树来实现的,默认情况下它会对 key 进行自然排序。 + +--- + +来看看使用 `TreeMap` 如何来达到同样的效果。 +![](https://i.loli.net/2019/05/08/5cd1bea4e6550.jpg) +运行结果: + +``` +127.0.0.1000 +``` + +效果和上文使用 `SortArrayMap` 是一致的。 + +只使用了 TreeMap 的一些 API: + +- 写入数据候,`TreeMap` 可以保证 key 的自然排序。 +- `tailMap` 可以获取比当前 key 大的部分数据。 +- 当这个方法有数据返回时取第一个就是顺时针中的第一个节点了。 +- 如果没有返回那就直接取整个 `Map` 的第一个节点,同样也实现了环形结构。 + +> ps:这里同样也没有 hash 方法以及虚拟节点(具体实现请看下文),因为 TreeMap 和 SortArrayMap 一样都是作为基础数据结构来使用的。 + +## 性能对比 + +为了方便大家选择哪一个数据结构,我用 `TreeMap` 和 `SortArrayMap` 分别写入了一百万条数据来对比。 + +先是 `SortArrayMap`: + +![](https://i.loli.net/2019/05/08/5cd1bea9f1177.jpg) + +**耗时 2237 毫秒。** + +TreeMap: + +![](https://i.loli.net/2019/05/08/5cd1beaa90503.jpg) + +**耗时 1316毫秒。** + +结果是快了将近一倍,所以还是推荐使用 `TreeMap` 来进行实现,毕竟它不需要额外的排序损耗。 + +# cim 中的实际应用 + +下面来看看在 `cim` 这个应用中是如何具体使用的,其中也包括上文提到的虚拟节点以及 hash 算法。 + +## 模板方法 + +在应用的时候考虑到就算是一致性 hash 算法都有多种实现,为了方便其使用者扩展自己的一致性 hash 算法因此我定义了一个抽象类;其中定义了一些模板方法,这样大家只需要在子类中进行不同的实现即可完成自己的算法。 + +AbstractConsistentHash,这个抽象类的主要方法如下: + +![](https://i.loli.net/2019/05/08/5cd1beab41c7a.jpg) + +- `add` 方法自然是写入数据的。 +- `sort` 方法用于排序,但子类也不一定需要重写,比如 `TreeMap` 这样自带排序的容器就不用。 +- `getFirstNodeValue` 获取节点。 +- `process` 则是面向客户端的,最终只需要调用这个方法即可返回一个节点。 + + +下面我们来看看利用 `SortArrayMap` 以及 `AbstractConsistentHash` 是如何实现的。 + +![](https://i.loli.net/2019/05/08/5cd1beab9a84f.jpg) + +就是实现了几个抽象方法,逻辑和上文是一样的,只是抽取到了不同的方法中。 + +只是在 add 方法中新增了几个虚拟节点,相信大家也看得明白。 + +> 把虚拟节点的控制放到子类而没有放到抽象类中也是为了灵活性考虑,可能不同的实现对虚拟节点的数量要求也不一样,所以不如自定义的好。 + +但是 `hash` 方法确是放到了抽象类中,子类不用重写;因为这是一个基本功能,只需要有一个公共算法可以保证他散列地足够均匀即可。 + +因此在 `AbstractConsistentHash` 中定义了 hash 方法。 + +![](https://i.loli.net/2019/05/08/5cd1beac476c2.jpg) + +> 这里的算法摘抄自 xxl_job,网上也有其他不同的实现,比如 `FNV1_32_HASH` 等;实现不同但是目的都一样。 + +--- + +这样对于使用者来说就非常简单了: + +![](https://i.loli.net/2019/05/08/5cd1beacc8e2c.jpg) + +他只需要构建一个服务列表,然后把当前的客户端信息传入 `process` 方法中即可获得一个一致性 hash 算法的返回。 + + + +--- + +同样的对于想通过 `TreeMap` 来实现也是一样的套路: + +![](https://i.loli.net/2019/05/08/5cd1bead5feca.jpg) + +他这里不需要重写 sort 方法,因为自身写入时已经排好序了。 + +而在使用时对于客户端来说只需求修改一个实现类,其他的啥都不用改就可以了。 + +![](https://i.loli.net/2019/05/08/5cd1beb27d748.jpg) + +运行的效果也是一样的。 + +这样大家想自定义自己的算法时只需要继承 `AbstractConsistentHash` 重写相关方法即可,**客户端代码无须改动。** + +## 路由算法扩展性 + +但其实对于 `cim` 来说真正的扩展性是对路由算法来说的,比如它需要支持轮询、hash、一致性hash、随机、LRU等。 + +只是一致性 hash 也有多种实现,他们的关系就如下图: + +![](https://i.loli.net/2019/05/08/5cd1beb2d6428.jpg) + +应用还需要满足对这一类路由策略的灵活支持,比如我也想自定义一个随机的策略。 + +因此定义了一个接口:`RouteHandle` + +```java +public interface RouteHandle { + + /** + * 再一批服务器里进行路由 + * @param values + * @param key + * @return + */ + String routeServer(List values,String key) ; +} +``` + +其中只有一个方法,也就是路由方法;入参分别是服务列表以及客户端信息即可。 + +而对于一致性 hash 算法来说也是只需要实现这个接口,同时在这个接口中选择使用 `SortArrayMapConsistentHash` 还是 `TreeMapConsistentHash` 即可。 + +![](https://i.loli.net/2019/05/08/5cd1beb35b595.jpg) + +这里还有一个 `setHash` 的方法,入参是 AbstractConsistentHash;这就是用于客户端指定需要使用具体的那种数据结构。 + +--- + +而对于之前就存在的轮询策略来说也是同样的实现 `RouteHandle` 接口。 + +![](https://i.loli.net/2019/05/08/5cd1beb3dbd86.jpg) + +这里我只是把之前的代码搬过来了而已。 + + +接下来看看客户端到底是如何使用以及如何选择使用哪种算法。 + +> 为了使客户端代码几乎不动,我将这个选择的过程放入了配置文件。 + +![](https://i.loli.net/2019/05/08/5cd1beb476ca8.jpg) + +1. 如果想使用原有的轮询策略,就配置实现了 `RouteHandle` 接口的轮询策略的全限定名。 +2. 如果想使用一致性 hash 的策略,也只需要配置实现了 `RouteHandle` 接口的一致性 hash 算法的全限定名。 +3. 当然目前的一致性 hash 也有多种实现,所以一旦配置为一致性 hash 后就需要再加一个配置用于决定使用 `SortArrayMapConsistentHash` 还是 `TreeMapConsistentHash` 或是自定义的其他方案。 +4. 同样的也是需要配置继承了 `AbstractConsistentHash` 的全限定名。 + + +不管这里的策略如何改变,在使用处依然保持不变。 + +只需要注入 `RouteHandle`,调用它的 `routeServer` 方法。 + +```java +@Autowired +private RouteHandle routeHandle ; +String server = routeHandle.routeServer(serverCache.getAll(),String.valueOf(loginReqVO.getUserId())); + +``` + +既然使用了注入,那其实这个策略切换的过程就在创建 `RouteHandle bean` 的时候完成的。 + +![](https://i.loli.net/2019/05/08/5cd1beb4d7cd2.jpg) + +也比较简单,需要读取之前的配置文件来动态生成具体的实现类,主要是利用反射完成的。 + +这样处理之后就比较灵活了,比如想新建一个随机的路由策略也是同样的套路;到时候只需要修改配置即可。 + +> 感兴趣的朋友也可提交 PR 来新增更多的路由策略。 + +# 总结 + +希望看到这里的朋友能对这个算法有所理解,同时对一些设计模式在实际的使用也能有所帮助。 + +相信在金三银四的面试过程中还是能让面试官眼前一亮的,毕竟根据我这段时间的面试过程来看听过这个名词的都在少数😂(可能也是和候选人都在 1~3 年这个层级有关)。 + +以上所有源码: + +[https://github.com/crossoverJie/cim](https://github.com/crossoverJie/cim) + +如果本文对你有所帮助还请不吝转发。 diff --git a/docs/algorithm/guava-bloom-filter.md b/docs/algorithm/guava-bloom-filter.md index 81091f12..bb705451 100755 --- a/docs/algorithm/guava-bloom-filter.md +++ b/docs/algorithm/guava-bloom-filter.md @@ -1,5 +1,5 @@ -![](https://ws2.sinaimg.cn/large/006tNbRwly1fxjmn1eyr6j31hc0qr114.jpg) +![](https://i.loli.net/2019/06/26/5d1393217483718447.jpg) # 前言 @@ -50,11 +50,11 @@ 还是在这个基础上,写入 1000W 数据试试: -![](https://ws1.sinaimg.cn/large/006tNbRwly1fxjn76zaoaj30fr07ejsh.jpg) +![](https://i.loli.net/2019/06/26/5d139321d4ee464729.jpg) 执行后马上就内存溢出。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxjn7zovu8j30my07rq4o.jpg) +![](https://i.loli.net/2019/06/26/5d139322c054a77994.jpg) 可见在内存有限的情况下我们不能使用这种方式。 @@ -83,7 +83,7 @@ 听起来比较绕,但是通过一个图就比较容易理解了。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxjo2ku62jj30ew0bzweu.jpg) +![](https://i.loli.net/2019/06/26/5d1393234976c40998.jpg) 如图所示: @@ -269,13 +269,13 @@ public class BloomFilters { 执行结果如下: -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxjow3df29j30le06d405.jpg) +![](https://i.loli.net/2019/06/26/5d139324062c317953.jpg) 只花了 3 秒钟就写入了 1000W 的数据同时做出来准确的判断。 --- -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxjoxad1kmj30p8072dhe.jpg) +![](https://i.loli.net/2019/06/26/5d139324c314174414.jpg) 当让我把数组长度缩小到了 100W 时就出现了一个误报,`400230340` 这个数明明没在集合里,却返回了存在。 @@ -286,7 +286,7 @@ public class BloomFilters { # Guava 实现 -![](https://ws2.sinaimg.cn/large/006tNbRwly1fxjp1vluy8j30lj04iab8.jpg) +![](https://i.loli.net/2019/06/26/5d13932a2cbfa10136.jpg) 刚才的方式虽然实现了功能,也满足了大量数据。但其实观察 `GC` 日志非常频繁,同时老年代也使用了 90%,接近崩溃的边缘。 @@ -325,7 +325,7 @@ public class BloomFilters { 也是同样写入了 1000W 的数据,执行没有问题。 -![](https://ws2.sinaimg.cn/large/006tNbRwly1fxjp57ga3oj30ma052gmt.jpg) +![](https://i.loli.net/2019/06/26/5d13932aa240376389.jpg) 观察 GC 日志会发现没有一次 `fullGC`,同时老年代的使用率很低。和刚才的一对比这里明显的要好上很多,也可以写入更多的数据。 @@ -336,7 +336,7 @@ public class BloomFilters { 构造方法中有两个比较重要的参数,一个是预计存放多少数据,一个是可以接受的误报率。 我这里的测试 demo 分别是 1000W 以及 0.01。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxjp9reomaj30yq0cqjv9.jpg) +![](https://i.loli.net/2019/06/26/5d13932b7b19733775.jpg) `Guava` 会通过你预计的数量以及误报率帮你计算出你应当会使用的数组大小 `numBits` 以及需要计算几次 Hash 函数 `numHashFunctions` 。 @@ -346,7 +346,7 @@ public class BloomFilters { 真正存放数据的 `put` 函数如下: -![](https://ws1.sinaimg.cn/large/006tNbRwly1fxjpg55hszj30so082abx.jpg) +![](https://i.loli.net/2019/06/26/5d13932bf409b70520.jpg) - 根据 `murmur3_128` 方法的到一个 128 位长度的 `byte[]`。 - 分别取高低 8 位的到两个 `hash` 值。 @@ -361,7 +361,7 @@ bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize); 重点是 `bits.set()` 方法。 -![](https://ws2.sinaimg.cn/large/006tNbRwly1fxjpl6uih0j30m30dxmz9.jpg) +![](https://i.loli.net/2019/06/26/5d13932c8cb9133569.jpg) 其实 set 方法是 `BitArray` 中的一个函数,`BitArray` 就是真正存放数据的底层数据结构。 @@ -369,7 +369,7 @@ bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize); 所以 `set()` 时候也是对这个 `data` 做处理。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxjpnobodvj30iz06vwf7.jpg) +![](https://i.loli.net/2019/06/26/5d13932d2faa229373.jpg) - 在 `set` 之前先通过 `get()` 判断这个数据是否存在于集合中,如果已经存在则直接返回告知客户端写入失败。 - 接下来就是通过位运算进行`位或赋值`。 @@ -377,7 +377,7 @@ bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize); ### mightContain 是否存在函数 -![](https://ws2.sinaimg.cn/large/006tNbRwly1fxjprkzulxj30o408wabk.jpg) +![](https://i.loli.net/2019/06/26/5d13932db4fcf97015.jpg) 前面几步的逻辑都是类似的,只是调用了刚才的 `get()` 方法判断元素是否存在而已。 @@ -398,4 +398,4 @@ bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize); -**你的点赞与分享是对我最大的支持** \ No newline at end of file +**你的点赞与分享是对我最大的支持** diff --git a/docs/architecture-design/Spike.md b/docs/architecture-design/Spike.md index b0c00db6..35142c69 100644 --- a/docs/architecture-design/Spike.md +++ b/docs/architecture-design/Spike.md @@ -9,7 +9,7 @@ 常用的系统分层结构: -

+

针对于浏览器端,可以使用 JS 进行请求过滤,比如五秒钟之类只能点一次抢购按钮,五秒钟只能允许请求一次后端服务。(APP 同理) diff --git a/docs/architecture-design/million-sms-push.md b/docs/architecture-design/million-sms-push.md index a3d366b7..73a5421b 100755 --- a/docs/architecture-design/million-sms-push.md +++ b/docs/architecture-design/million-sms-push.md @@ -33,7 +33,7 @@ 最终的架构图如下: -![](https://ws1.sinaimg.cn/mw690/72fbb941gy1fvjz1teappj20rg0humy1.jpg) +![](https://i.loli.net/2019/06/26/5d1393a70683166304.jpg) 现在看着蒙没关系,下文一一介绍。 @@ -80,7 +80,7 @@ 这点和之前 [SpringBoot 整合长连接心跳机制](http://t.cn/EPcNHFZ) 类似。 -![](https://ws2.sinaimg.cn/large/006tNbRwgy1fvkj6oe4rej30k104c0tg.jpg) +![](https://i.loli.net/2019/06/26/5d1393a5e41f832920.jpg) 同时为了可以通过 Channel 获取到客户端唯一标识(手机号码),还需要在 Channel 中设置对应的属性: @@ -127,7 +127,7 @@ log.info("客户端下线,TelNo=" + telNo); 我们都知道在 Netty 中处理消息一般是在 `channelRead()` 方法中。 -![](https://ws2.sinaimg.cn/large/006tNbRwgy1fvkkawymbkj30o6027mxf.jpg) +![](https://i.loli.net/2019/06/26/5d1393a6126d530691.jpg) 在这里可以解析消息,区分类型。 @@ -148,9 +148,9 @@ log.info("客户端下线,TelNo=" + telNo); 伪代码如下: -![](https://ws1.sinaimg.cn/large/006tNbRwgy1fvkkhd8961j30n602kglr.jpg) +![](https://i.loli.net/2019/06/26/5d1393a638fa183367.jpg) -![](https://ws2.sinaimg.cn/large/006tNbRwgy1fvkkhwsgkqj30nh0m0gpt.jpg) +![](https://i.loli.net/2019/06/26/5d1393a68a53a59900.jpg) 想要了解 cicada 的具体实现请点击这里: @@ -181,7 +181,7 @@ log.info("客户端下线,TelNo=" + telNo); 伪代码如下: -![](https://ws3.sinaimg.cn/large/006tNbRwgy1fvkkpefci7j30w408h768.jpg) +![](https://i.loli.net/2019/06/26/5d1393a6da88584453.jpg) 具体可以参考: @@ -207,7 +207,7 @@ log.info("客户端下线,TelNo=" + telNo); 在将具体实现之前首先得讲讲上文贴出的整体架构图。 -![](https://ws1.sinaimg.cn/mw690/72fbb941gy1fvjz1teappj20rg0humy1.jpg) +![](https://i.loli.net/2019/06/26/5d1393a70683166304.jpg) 先从左边开始。 @@ -231,19 +231,19 @@ log.info("客户端下线,TelNo=" + telNo); `注册鉴权` 模块会订阅 Zookeeper 中的节点,从而可以获取最新的服务列表。结构如下: -![](https://ws2.sinaimg.cn/large/006tNbRwgy1fundatqf6uj30el06f0su.jpg) +![](https://i.loli.net/2019/06/26/5d1393a7327b184532.jpg) 以下是一些伪代码: 应用启动注册 Zookeeper。 -![](https://ws2.sinaimg.cn/large/006tNbRwgy1fvkriuz7yrj30m304lq3r.jpg) +![](https://i.loli.net/2019/06/26/5d1393a7624a976369.jpg) -![](https://ws4.sinaimg.cn/large/006tNbRwgy1fvkrj927rsj30od08ejst.jpg) +![](https://i.loli.net/2019/06/26/5d1393c2d2a1b31176.jpg) 对于`注册鉴权`模块来说只需要订阅这个 Zookeeper 节点: -![](https://ws2.sinaimg.cn/large/006tNbRwgy1fvkrlfdgrkj30tb08j0uf.jpg) +![](https://i.loli.net/2019/06/26/5d1393ad257fe34873.jpg) ### 路由策略 @@ -288,7 +288,7 @@ log.info("客户端下线,TelNo=" + telNo); 伪代码如下: -![](https://ws1.sinaimg.cn/large/006tNbRwgy1fvkt2ytdxoj30r109u40n.jpg) +![](https://i.loli.net/2019/06/26/5d1393ad5e2e263573.jpg) 这里存放路由关系的时候会有并发问题,最好是换为一个 `lua` 脚本。 @@ -357,4 +357,4 @@ log.info("客户端下线,TelNo=" + telNo); **欢迎关注公众号一起交流:** -![](https://ws4.sinaimg.cn/large/006tNbRwgy1fvkwiw9pwaj30760760t7.jpg) \ No newline at end of file +![](https://i.loli.net/2019/06/26/5d1393ad8d38d78633.jpg) diff --git a/docs/architecture-design/seconds-kill.md b/docs/architecture-design/seconds-kill.md index 5803b057..aa8061d7 100644 --- a/docs/architecture-design/seconds-kill.md +++ b/docs/architecture-design/seconds-kill.md @@ -1,4 +1,4 @@ -![](https://ws2.sinaimg.cn/large/006tKfTcly1fr1z9k79lrj31kw11zwt8.jpg) +![](https://i.loli.net/2019/05/08/5cd1d713e19ed.jpg) ## 前言 @@ -37,7 +37,7 @@ 先看看实际项目的结构: -![](https://ws2.sinaimg.cn/large/006tKfTcly1fr38jkau5kj30jk07a754.jpg) +![](https://i.loli.net/2019/05/08/5cd1d71693bb0.jpg) 还是和以前一样: @@ -173,24 +173,24 @@ public class OrderServiceImpl implements OrderService { 手动调用下 `createWrongOrder/1` 接口发现: 库存表: -![](https://ws3.sinaimg.cn/large/006tKfTcly1fr38x4wqhcj30g404ajrg.jpg) +![](https://i.loli.net/2019/05/08/5cd1d7189c72f.jpg) 订单表: -![](https://ws1.sinaimg.cn/large/006tKfTcly1fr38xpcdn7j30f0040glq.jpg) +![](https://i.loli.net/2019/05/08/5cd1d721e9fd4.jpg) 一切看起来都没有问题,数据也正常。 但是当用 `JMeter` 并发测试时: -![](https://ws2.sinaimg.cn/large/006tKfTcly1fr391hontsj31ge0b8dgt.jpg) +![](https://i.loli.net/2019/05/08/5cd1d7243c657.jpg) 测试配置是:300个线程并发,测试两轮来看看数据库中的结果: -![](https://ws4.sinaimg.cn/large/006tKfTcly1fr393xxc0rj31ge0463z6.jpg) +![](https://i.loli.net/2019/05/08/5cd1d726cee79.jpg) -![](https://ws4.sinaimg.cn/large/006tKfTcly1fr3939yo1bj30c4062t8s.jpg) +![](https://i.loli.net/2019/05/08/5cd1d72816d67.jpg) -![](https://ws4.sinaimg.cn/large/006tKfTcly1fr393pxvf3j30j60d60v4.jpg) +![](https://i.loli.net/2019/05/08/5cd1d72b9f26a.jpg) 请求都响应成功,库存确实也扣完了,但是订单却生成了 **124** 条记录。 @@ -251,17 +251,17 @@ public class OrderServiceImpl implements OrderService { 同样的测试条件,我们再进行上面的测试 `/createOptimisticOrder/1`: -![](https://ws4.sinaimg.cn/large/006tKfTcly1fr39fxn691j31g603adgg.jpg) +![](https://i.loli.net/2019/05/08/5cd1d72dab853.jpg) -![](https://ws2.sinaimg.cn/large/006tKfTcly1fr39dlobs1j30ca042wej.jpg) +![](https://i.loli.net/2019/05/08/5cd1d730800b1.jpg) -![](https://ws2.sinaimg.cn/large/006tKfTcly1fr39dwfmrzj30f60gqgn7.jpg) +![](https://i.loli.net/2019/05/08/5cd1d73324dd2.jpg) 这次发现无论是库存订单都是 OK 的。 查看日志发现: -![](https://ws2.sinaimg.cn/large/006tKfTcly1fr39hxcbsgj31kw0jhu0y.jpg) +![](https://i.loli.net//2019//05//08//5cd1daafb70bc.jpg) 很多并发请求会响应错误,这就达到了效果。 @@ -272,9 +272,9 @@ public class OrderServiceImpl implements OrderService { - web 利用 Nginx 进行负载。 - Service 也是多台应用。 -![](https://ws3.sinaimg.cn/large/006tKfTcly1fr39lm8iyjj31kw0ad784.jpg) +![](https://i.loli.net/2019/05/08/5cd1d752909b9.jpg) -![](https://ws4.sinaimg.cn/large/006tKfTcly1fr39lvxnunj31kw0adaeh.jpg) +![](https://i.loli.net/2019/05/08/5cd1d758c7714.jpg) 再用 JMeter 测试时可以直观的看到效果。 @@ -418,11 +418,11 @@ echo "start $appname success" 通过 `Druid` 的监控来看看之前请求数据库的情况: 因为 Service 是两个应用。 -![](https://ws1.sinaimg.cn/large/006tKfTcly1fr3a1zpp5lj31kw0h277s.jpg) +![](https://i.loli.net/2019/05/08/5cd1d764221b5.jpg) -![](https://ws3.sinaimg.cn/large/006tKfTcly1fr3a2c0vvdj31kw0g4n0m.jpg) +![](https://i.loli.net/2019/05/08/5cd1d7676e1d2.jpg) -![](https://ws4.sinaimg.cn/large/006tKfTcly1fr3a3xwslqj319g10cthl.jpg) +![](https://i.loli.net//2019//05//08//5cd1daeb0c306.jpg) 数据库也有 20 多个连接。 @@ -554,15 +554,15 @@ Service 端就没什么更新了,依然是采用的乐观锁更新数据库。 再压测看下效果 `/createOptimisticLimitOrderByRedis/1`: -![](https://ws3.sinaimg.cn/large/006tKfTcly1fr3amu17zuj30e603ewej.jpg) +![](https://i.loli.net/2019/05/08/5cd1d776c39b7.jpg) -![](https://ws4.sinaimg.cn/large/006tKfTcly1fr3an1x3pqj30oy0fwq4p.jpg) +![](https://i.loli.net/2019/05/08/5cd1d77ba16d2.jpg) -![](https://ws2.sinaimg.cn/large/006tKfTcly1fr3aml0c8rj31ek0ssn3g.jpg) +![](https://i.loli.net/2019/05/08/5cd1d780d5aa2.jpg) -![](https://ws1.sinaimg.cn/large/006tKfTcly1fr3ank9otcj31kw0d4die.jpg) +![](https://i.loli.net/2019/05/08/5cd1d784644d5.jpg) -![](https://ws4.sinaimg.cn/large/006tKfTcly1fr3anxbb0hj31kw0cjtbb.jpg) +![](https://i.loli.net/2019/05/08/5cd1d787b3e49.jpg) 首先是看结果没有问题,再看数据库连接以及并发请求数都有**明显的下降**。 @@ -571,7 +571,7 @@ Service 端就没什么更新了,依然是采用的乐观锁更新数据库。 其实仔细观察 Druid 监控数据发现这个 SQL 被多次查询: -![](https://ws3.sinaimg.cn/large/006tKfTcly1fr3aq7shudj31kw0bomzp.jpg) +![](https://i.loli.net/2019/05/08/5cd1d78b3896a.jpg) 其实这是实时查询库存的 SQL,主要是为了在每次下单之前判断是否还有库存。 @@ -638,13 +638,13 @@ Service 端就没什么更新了,依然是采用的乐观锁更新数据库。 压测看看实际效果 `/createOptimisticLimitOrderByRedis/1`: -![](https://ws1.sinaimg.cn/large/006tKfTcly1fr3b419f2aj30by04g0ss.jpg) +![](https://i.loli.net/2019/05/08/5cd1d78d659b6.jpg) -![](https://ws2.sinaimg.cn/large/006tKfTcly1fr3b48vebkj30gk0cy0u3.jpg) +![](https://i.loli.net/2019/05/08/5cd1d790607a1.jpg) -![](https://ws2.sinaimg.cn/large/006tKfTcgy1fr3b55kyv6j31kw0dijtx.jpg) +![](https://i.loli.net/2019/05/08/5cd1d79307676.jpg) -![](https://ws3.sinaimg.cn/large/006tKfTcgy1fr3b5n1n21j31kw0c2acg.jpg) +![](https://i.loli.net/2019/05/08/5cd1d7973de43.jpg) 最后发现数据没问题,数据库的请求与并发也都下来了。 @@ -691,4 +691,4 @@ Service 端就没什么更新了,依然是采用的乐观锁更新数据库。 ### 号外 最近在总结一些 Java 相关的知识点,感兴趣的朋友可以一起维护。 -> 地址: [https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) \ No newline at end of file +> 地址: [https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) diff --git a/docs/collections/HashMap.md b/docs/collections/HashMap.md index e6cbd7c7..a038f075 100644 --- a/docs/collections/HashMap.md +++ b/docs/collections/HashMap.md @@ -4,7 +4,7 @@ > 以下基于 JDK1.7 分析。 -![](https://ws2.sinaimg.cn/large/006tNc79gy1fn84b0ftj4j30eb0560sv.jpg) +![](https://i.loli.net/2019/06/26/5d1391f87ca6355105.jpg) 如图所示,HashMap 底层是基于数组和链表实现的。其中有两个重要的参数: @@ -61,12 +61,12 @@ map.forEach((key,value)->{ 并发场景发生扩容,调用 `resize()` 方法里的 `rehash()` 时,容易出现环形链表。这样当获取一个不存在的 `key` 时,计算出的 `index` 正好是环形链表的下标时就会出现死循环。 -![](https://ws2.sinaimg.cn/large/006tNc79gy1fn85u0a0d9j30n20ii0tp.jpg) +![](https://i.loli.net/2019/06/26/5d1391f90375678382.jpg) > 所以 HashMap 只能在单线程中使用,并且尽量的预设容量,尽可能的减少扩容。 在 `JDK1.8` 中对 `HashMap` 进行了优化: -当 `hash` 碰撞之后写入链表的长度超过了阈值(默认为8),链表将会转换为**红黑树**。 +当 `hash` 碰撞之后写入链表的长度超过了阈值(默认为8)并且 `table` 的长度不小于64(否则扩容一次)时,链表将会转换为**红黑树**。 假设 `hash` 冲突非常严重,一个数组后面接了很长的链表,此时重新的时间复杂度就是 `O(n)` 。 diff --git a/docs/collections/LinkedList.md b/docs/collections/LinkedList.md index e4d25b3b..041f3221 100644 --- a/docs/collections/LinkedList.md +++ b/docs/collections/LinkedList.md @@ -1,6 +1,6 @@ # LinkedList 底层分析 -![](https://ws4.sinaimg.cn/large/006tKfTcly1fqzb66c00gj30p7056q38.jpg) +![](https://i.loli.net/2019/06/26/5d1391f88b05665203.jpg) 如图所示 `LinkedList` 底层是基于双向链表实现的,也是实现了 `List` 接口,所以也拥有 List 的一些特点(JDK1.7/8 之后取消了循环,修改为双向链表)。 diff --git a/docs/contactme.md b/docs/contactme.md index ee53d146..6908d045 100644 --- a/docs/contactme.md +++ b/docs/contactme.md @@ -29,5 +29,5 @@ **欢迎我的关注公众号一起交流:** -![](https://ws3.sinaimg.cn/large/006tKfTcgy1fsuvb4ebtmj30760760t7.jpg) +![](https://crossoverjie.top/uploads/weixinfooter1.jpg) diff --git a/docs/db/MySQL-Index.md b/docs/db/MySQL-Index.md index e5d17cb6..c41f5bff 100644 --- a/docs/db/MySQL-Index.md +++ b/docs/db/MySQL-Index.md @@ -5,7 +5,7 @@ ## B+ Tree 的数据结构 -![](https://ws2.sinaimg.cn/large/006tKfTcgy1fn10d6j9sij30hc08cab3.jpg) +![](https://i.loli.net/2019/06/26/5d139411b683d65706.jpg) 如图所示是 `B+ Tree` 的数据结构。是由一个一个的磁盘块组成的树形结构,每个磁盘块由数据项和指针组成。 diff --git a/docs/db/sharding-db.md b/docs/db/sharding-db.md new file mode 100644 index 00000000..82a8068a --- /dev/null +++ b/docs/db/sharding-db.md @@ -0,0 +1,193 @@ +![](https://i.loli.net/2019/06/26/5d1394119463b63622.jpg) + +# 前言 + +之前不少人问我“能否分享一些分库分表相关的实践”,其实不是我不分享,而是真的经验不多🤣;和大部分人一样都是停留在理论阶段。 + + +不过这次多少有些可以说道了。 + +先谈谈背景,我们生产数据库随着业务发展量也逐渐起来;好几张单表已经突破**亿级**数据,并且保持每天 200+W 的数据量增加。 + +而我们有些业务需要进行关联查询、或者是报表统计;在这样的背景下大表的问题更加突出(比如一个查询功能需要跑好几分钟)。 + + + +> 可能很多人会说:为啥单表都过亿了才想方案解决?其实不是不想,而是由于历史原因加上错误预估了数据增长才导致这个局面。总之原因比较复杂,也不是本次讨论的重点。 + + +# 临时方案 + +由于需求紧、人手缺的情况下,整个处理的过程分为几个阶段。 + +第一阶段应该是去年底,当时运维反应 `MySQL` 所在的主机内存占用很高,整体负载也居高不下,导致整个 MySQL 的吞吐量明显降低(写入、查询数据都明显减慢)。 + +为此我们找出了数据量最大的几张表,发现大部分数据量在7/8000W 左右,少数的已经突破一亿。 + +通过业务层面进行分析发现,这些数据多数都是用户产生的一些**日志型数据**,而且这些数据在业务上并不是强相关的,甚至两三个月前的数据其实已经不需要实时查询了。 + + +因为接近年底,尽可能的不想去动应用,考虑是否可以在运维层面缓解压力;主要的目的就是把单表的数据量降低。 + + +原本是想把两个月之前的数据直接迁移出来放到备份表中,但在准备实施的过程中发现一个大坑。 + +> 表中没有一个可以排序的索引,导致我们无法快速的筛选出一部分数据!这真是一个深坑,为后面的一些优化埋了个地雷;即便是加索引也需要花几个小时(具体多久没敢在生产测试)。 + + +如果我们强行按照时间进行筛选,可能查询出 4000W 的数据就得花上好几个小时;这显然是行不通的。 + + +于是我们便想到了一个大胆的想法:这部分数据是否可以直接不要了? + +这可能是最有效及最快的方式了,和产品沟通后得知这部分数据真的只是日志型的数据,即便是报表出不来今后补上也是可以的。 + +于是我们就简单粗暴的做了以下事情: + +- 修改原有表的表名,比如加上(`_190416bak`)。 +- 再新建一张和原有表名称相同的表。 + + +这样新的数据就写到了新表,同时业务上也是使用的这个数据量较小的新表。 + +虽说过程不太优雅,但至少是解决了问题同时也给我们做技术改造预留了时间。 + +# 分表方案 + +之前的方案虽说可以缓解压力,但不能根本解决问题。 + +有些业务必须得查询之前的数据,导致之前那招行不通了,所以正好我们就借助这个机会把表分了。 + + +我相信大部分人虽说没有做过实际做过分表,但也见过猪跑;网上一搜各种方案层出不穷。 + +我认为最重要的一点是要结合实际业务找出需要 sharding 的字段,同时还有上线阶段的数据迁移也非常重要。 + +## 时间 + +可能大家都会说用 hash 的方式分配得最均匀,但我认为这还是需要使用历史数据的场景才用哈希分表。 + + +而对于不需要历史数据的场景,比如业务上只查询近三个月的数据。 + +这类需求完成可以采取时间分表,按照月份进行划分,这样改动简单,同时对历史数据也比较好迁移。 + +于是我们首先将这类需求的表筛选出来,按照月份进行拆分,只是在查询的时候拼接好表名即可;也比较好理解。 + +## 哈希 + +刚才也提到了:需要根据业务需求进行分表策略。 + +而一旦所有的数据都有可能查询时,按照时间分表也就行不通了。(也能做,只是如果不是按照时间进行查询时需要遍历所有的表) + +因此我们计划采用 `hash` 的方式分表,这算是业界比较主流的方式就不再赘述。 + +采用哈希时需要将 `sharding` 字段选好,由于我们的业务比较单纯;是一个物联网应用,所有的数据都包含有物联网设备的唯一标识(IMEI),并且这个字段天然的就保持了唯一性;大多数的业务也都是根据这个字段来的,所以它非常适合来做这个 `sharding` 字段。 + +在做分表之前也调研过 `MyCAT` 及 `sharding-jdbc`(现已升级为 `shardingsphere`),最终考虑到对开发的友好性及不增加运维复杂度还是决定在 jdbc 层 sharding 的方式。 + +但由于历史原因我们并不太好集成 `sharding-jdbc`,但基于 `sharding` 的特点自己实现了一个分表策略。 + +这个简单也好理解: + +```java +int index = hash(sharding字段) % 分表数量 ; + +select xx from 'busy_'+index where sharding字段 = xxx; +``` + +其实就是算出了表名,然后路由过去查询即可。 + + +只是我们实现的非常简单:修改了所有的底层查询方法,每个方法都里都做了这样的一个判断。 + +并没有像 `sharding-jdbc` 一样,代理了数据库的查询方法;其中还要做 `SQL解析-->SQL路由-->执行SQL-->合并结果` 这一系列的流程。 + +如果自己再做一遍无异于重新造了一个轮子,并且并不专业,只是在现有的技术条件下选择了一个快速实现达成效果的方法。 + +不过这个过程中我们节省了将 sharding 字段哈希的过程,因为每一个 IMEI 号其实都是一个唯一的整型,直接用它做 mod 运算即可。 + + + +还有一个是需要一个统一的组件生成规则,分表后不能再依赖于单表的字段自增了;方法还是挺多的: + +- 比如时间戳+随机数可满足大部分业务。 +- UUID,生成简单,但没法做排序。 +- 雪花算法统一生成主键ID。 + +大家可以根据自己的实际情况做选择。 + +# 业务调整 + +因为我们并没有使用第三方的 sharding-jdbc 组件,所有没有办法做到对代码的低侵入性;每个涉及到分表的业务代码都需要做底层方法的改造(也就是路由到正确的表)。 + +考虑到后续业务的发展,我们决定将拆分的表分为 64 张;加上后续引入大数据平台足以应对几年的数据增长。 + +> 这里还有个小细节需要注意:分表的数量需要为 2∧N 次方,因为在取模的这种分表方式下,即便是今后再需要分表影响的数据也会尽量的小。 + + +再修改时只能将表名称进行全局搜索,然后加以修改,同时根据修改的方法倒推到表现的业务并记录下来,方便后续回归测试。 + +--- + +当然无法避免查询时利用非 sharding 字段导致的全表扫描,这是所有分片后都会遇到的问题。 + +因此我们在修改分表方法的底层查询时同时也会查看是否有走分片字段,如果不是,那是否可以调整业务。 + +比如对于一个上亿的数据是否还有必要存在按照分页查询、日期查询?这样的业务是否真的具有意义? + +我们尽可能的引导产品按照这样的方式来设计产品或者做出调整。 + +但对于报表这类的需求确实也没办法,比如统计表中某种类型的数据;这种我们也可以利用多线程的方式去并行查询然后汇总统计来提高查询效率。 + + +有时也有一些另类场景: + +> 比如一个千万表中有某一特殊类型的数据只占了很小一部分,比如说几千上万条。 + + +这时页面上需要对它进行分页查询是比较正常的(比如某种投诉消息,客户需要一条一条的单独处理),但如果我们按照 IMEI 号或者是主键进行分片后再分页查询那就比较蛋疼了。 + +所以这类型的数据建议单独新建一张表来维护,不要和其他数据混合在一起,这样不管是做分页还是 like 都比较简单和独立。 + +## 验证 + +代码改完,开发也单测完成后怎么来验证分表的业务是否正常也比较麻烦。 + +一个是测试麻烦,再一个是万一哪里改漏了还是查询的原表,但这样在测试环境并不会有异常,一旦上线产生了生产数据到新的 64 张表后想要再修复就比较麻烦了。 + +所以我们取了个巧,直接将原表的表名修改,比如加一个后缀;这样在测试过程中观察前后台有无报错就比较容易提前发现这个问题。 + +# 上线流程 + +测试验收通过后只是分表这个需求的80%,剩下如何上线也是比较头疼。 + +一旦应用上线后所有的查询、写入、删除都会先走路由然后到达新表;而老数据在原表里是不会发生改变的。 + +## 数据迁移 + +所以我们上线前的第一步自然是需要将原有的数据进行迁移,迁移的目的是要分片到新的 64 张表中,这样才会对原有的业务无影响。 + + +因此我们需要额外准备一个程序,它需要将老表里的数据按照分片规则复制到新表中; + +在我们这个场景下,生产数据有些已经上亿了,这个迁移过程我们在测试环境模拟发现耗时是非常久的。而且我们老表中对于 `create_time` 这样用于筛选数据的字段没有索引(以前的技术债),所以查询起来就更加慢了。 + +最后没办法,我们只能和产品协商告知用户对于之前产生的数据短期可能会查询不到,这个时间最坏可能会持续几天(我们只能在凌晨迁移,白天会影响到数据库负载)。 + + +# 总结 + +这便是我们这次的分表实践,虽说不少过程都不优雅,但受限于条件也只能折中处理。 + +但我们后续的计划是,修改我们底层的数据连接(目前是自己封装的一个 jar 包,导致集成 sharding-jdbc 比较麻烦)最终逐渐迁移到 `sharding-jdbc` . + +最后得出了几个结论: + +- 一个好的产品规划非常有必要,可以在合理的时间对数据处理(不管是分表还是切入归档)。 +- 每张表都需要一个可以用于排序查询的字段(自增ID、创建时间),整个过程由于没有这个字段导致耽搁了很长时间。 +- 分表字段需要谨慎,要全盘的考虑业务情况,尽量避免出现查询扫表的情况。 + +最后欢迎留言讨论。 + +**你的点赞与分享是对我最大的支持** diff --git a/docs/distributed/Distributed-Limit.md b/docs/distributed/Distributed-Limit.md index dd33f793..c69a06fb 100644 --- a/docs/distributed/Distributed-Limit.md +++ b/docs/distributed/Distributed-Limit.md @@ -1,4 +1,4 @@ -![](https://ws3.sinaimg.cn/large/006tKfTcly1fqrle104hwj31i6104aig.jpg) +![](https://i.loli.net/2019/06/26/5d1394364203855229.jpg) ## 前言 @@ -151,15 +151,15 @@ public void doSomething(){} > 为了验证分布式效果启动了两个 Order 应用。 -![](https://ws1.sinaimg.cn/large/006tKfTcly1fqrnxt2l8lj313x09rwfm.jpg) +![](https://i.loli.net/2019/06/26/5d139436e765a73996.jpg) 效果如下: -![](https://ws1.sinaimg.cn/large/006tKfTcly1fqrlvvj8cbj31kw0f1wws.jpg) +![](https://i.loli.net/2019/06/26/5d13943f1d81465048.jpg) -![](https://ws4.sinaimg.cn/large/006tKfTcly1fqrlznycdnj31kw0gbh0n.jpg) +![](https://i.loli.net/2019/06/26/5d139440e0b0e36306.jpg) -![](https://ws1.sinaimg.cn/large/006tKfTcly1fqrm0jpbjjj31kw04wgq9.jpg) +![](https://i.loli.net/2019/06/26/5d139441c7bb338785.jpg) ## 实现原理 diff --git a/docs/distributed/distributed-lock-redis.md b/docs/distributed/distributed-lock-redis.md index 8c7d8abb..b57d052c 100644 --- a/docs/distributed/distributed-lock-redis.md +++ b/docs/distributed/distributed-lock-redis.md @@ -1,4 +1,4 @@ -![](https://ws3.sinaimg.cn/large/006tKfTcgy1fpvathnbf6j31kw11xwl3.jpg) +![](https://i.loli.net/2019/06/26/5d139438c1cec87655.jpg) ## 前言 分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三。 @@ -265,7 +265,7 @@ public class RedisLockConfig { 它的原理其实也挺简单,debug 的话可以很直接的看出来: -![](https://ws2.sinaimg.cn/large/006tKfTcgy1fpxho866hbj311u0ej42f.jpg) +![](https://i.loli.net/2019/06/26/5d139439c08cc20580.jpg) 这里我们所依赖的 JedisCluster 其实是一个 `cglib 代理对象`。所以也不难想到它是如何工作的。 diff --git a/docs/frame/SpringAOP.md b/docs/frame/SpringAOP.md index 808cfa8f..3ebb7f58 100644 --- a/docs/frame/SpringAOP.md +++ b/docs/frame/SpringAOP.md @@ -118,7 +118,7 @@ public class CustomizeHandle implements InvocationHandler { 其实代理类是由 -![](https://ws3.sinaimg.cn/large/006tNc79gy1fms01lcml3j30ki09s75v.jpg) +![](https://i.loli.net/2019/06/26/5d13945f24cb855978.jpg) 这个方法动态创建出来的。将 proxyClassFile 输出到文件并进行反编译的话就可以的到代理类。 ```java diff --git a/docs/frame/guava-cache.md b/docs/frame/guava-cache.md index dfca253d..8c35b72d 100644 --- a/docs/frame/guava-cache.md +++ b/docs/frame/guava-cache.md @@ -141,7 +141,7 @@ Google 出的 [Guava](https://github.com/google/guava) 是 Java 核心增强的 2761 行,根据方法名称可以看出是判断当前的 Entry 是否过期,该 entry 就是通过 key 查询到的。 -![](https://ws2.sinaimg.cn/large/006tNc79gy1ft9l0mx77rj30zk0a1tat.jpg) +![](https://i.loli.net/2019/06/26/5d13945fe1cae45017.jpg) 这里就很明显的看出是根据根据构建时指定的过期方式来判断当前 key 是否过期了。 @@ -354,7 +354,7 @@ Guava 就是利用了上文的两个特性来实现了**引用回收**及**移 来自定义键和值的引用关系。 -![](https://ws2.sinaimg.cn/large/006tKfTcgy1ftatngp76aj30n20h6gpn.jpg) +![](https://i.loli.net/2019/06/26/5d139460a52cf85772.jpg) 在上文的分析中可以看出 Cache 中的 `ReferenceEntry` 是类似于 HashMap 的 Entry 存放数据的。 @@ -411,13 +411,13 @@ Guava 就是利用了上文的两个特性来实现了**引用回收**及**移 根据 `ValueReference getValueReference();` 的实现: -![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftatsg5jfvj30vg059wg9.jpg) +![](https://i.loli.net/2019/06/26/5d139461408fd93335.jpg) 具有强引用和弱引用的不同实现。 key 也是相同的道理: -![](https://ws2.sinaimg.cn/large/006tKfTcgy1ftattls2uzj30w005eq4t.jpg) +![](https://i.loli.net/2019/06/26/5d139461d363838006.jpg) 当使用这样的构造方式时,弱引用的 key 和 value 都会被垃圾回收。 @@ -479,19 +479,19 @@ loadingCache = CacheBuilder.newBuilder() 那么 Guava 是如何实现的呢? -![](https://ws3.sinaimg.cn/large/006tKfTcgy1ftau23uj5aj30mp08odh8.jpg) +![](https://i.loli.net/2019/06/26/5d13946796ed610501.jpg) 根据 LocalCache 中的 `getLiveValue()` 中判断缓存过期时,跟着这里的调用关系就会一直跟到: -![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftau4ed7dcj30rm0a5acd.jpg) +![](https://i.loli.net/2019/06/26/5d139468716d365202.jpg) `removeValueFromChain()` 中的: -![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftau5ywcojj30rs0750u9.jpg) +![](https://i.loli.net/2019/06/26/5d1394692e8d362414.jpg) `enqueueNotification()` 方法会将回收的缓存(包含了 key,value)以及回收原因包装成之前定义的事件接口加入到一个**本地队列**中。 -![](https://ws4.sinaimg.cn/large/006tKfTcgy1ftau7hpijrj30sl06wtaf.jpg) +![](https://i.loli.net/2019/06/26/5d139469c776a45831.jpg) 这样一看也没有回调我们初始化时候的事件啊。 @@ -499,11 +499,11 @@ loadingCache = CacheBuilder.newBuilder() 我们回到获取缓存的地方: -![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftau9rwgacj30ti0hswio.jpg) +![](https://i.loli.net/2019/06/26/5d13946c8960257603.jpg) 在 finally 中执行了 `postReadCleanup()` 方法;其实在这里面就是对刚才的队列进行了消费: -![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftaubaco48j30lw0513zi.jpg) +![](https://i.loli.net/2019/06/26/5d139471de1d710535.jpg) 一直跟进来就会发现这里消费了队列,将之前包装好的移除消息调用了我们自定义的事件,这样就完成了一次事件回调。 diff --git a/docs/frame/kafka-consumer.md b/docs/frame/kafka-consumer.md index d21a1ffd..97d8269d 100755 --- a/docs/frame/kafka-consumer.md +++ b/docs/frame/kafka-consumer.md @@ -1,6 +1,6 @@ -![](https://ws2.sinaimg.cn/large/006tNbRwly1fxdqk5h39cj31c00u0qbt.jpg) +![](https://i.loli.net/2019/06/26/5d1394729707f14762.jpg) # 前言 @@ -21,21 +21,21 @@ 先来谈谈最简单的单线程消费,如下图所示: -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxdsqd5ohgj30er08bglw.jpg) +![](https://i.loli.net/2019/06/26/5d1394735c50464148.jpg) 由于数据散列在三个不同分区,所以单个线程需要遍历三个分区将数据拉取下来。 单线程消费的示例代码: -![](https://ws1.sinaimg.cn/large/006tNbRwly1fxdsw65zjvj30si0d8goz.jpg) +![](https://i.loli.net/2019/06/26/5d139473f18c354807.jpg) 这段代码大家在官网也可以找到:将数据取出放到一个内存缓冲中最后写入数据库的过程。 > 先不讨论其中的 offset 的提交方式。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxdsy8m2bgj30u70b9wju.jpg) +![](https://i.loli.net/2019/06/26/5d139474d871a64091.jpg) -![](https://ws4.sinaimg.cn/large/006tNbRwly1fxdsyqby67j30t30dbjxm.jpg) +![](https://i.loli.net/2019/06/26/5d13947ae42a961451.jpg) 通过消费日志可以看出: @@ -60,13 +60,13 @@ 看一个简单示例即可知道它的用法: -![](https://ws4.sinaimg.cn/large/006tNbRwly1fxdukpv76lj30zb0cdacw.jpg) +![](https://i.loli.net/2019/06/26/5d13947b7cc9386064.jpg) > 值得注意的是:独立消费者可以不设置 group.id 属性。 也是发送100条消息,消费结果如下: -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxdulom1zcj30ax08raam.jpg) +![](https://i.loli.net/2019/06/26/5d13947c15efb51481.jpg) 通过 API 可以看出:我们可以手动指定需要消费哪些分区。 @@ -74,13 +74,13 @@ 同时它也支持多线程的方式,每个线程消费指定分区进行消费。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxdvncc15dj30ou0ddgo9.jpg) +![](https://i.loli.net/2019/06/26/5d13947cb8d4395802.jpg) -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxdv31z4hgj31cn0d6diw.jpg) +![](https://i.loli.net/2019/06/26/5d13947de7b6033298.jpg) 为了直观,只发送了 10 条数据。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxdv3oyjbrj30gz05qwfp.jpg) +![](https://i.loli.net/2019/06/26/5d139483f2cd338378.jpg) 根据消费结果可以看出: @@ -88,7 +88,7 @@ c1 线程只取 0 分区;c2 只取 1 分区;c3 只取 2 分区的数据。 甚至我们可以将消费者多进程部署,这样的消费方式如下: -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxdvhgd1rej30rw0ddwft.jpg) +![](https://i.loli.net/2019/06/26/5d139484a4f5012233.jpg) 假设 `Topic:data-push` 的分区数为 4 个,那我们就可以按照图中的方式创建两个进程。 @@ -112,7 +112,7 @@ c1 线程只取 0 分区;c2 只取 1 分区;c3 只取 2 分区的数据。 还是借助官方的示例图来更好的理解它。 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fxdvqdzduoj30d60700tc.jpg) +![](https://i.loli.net/2019/06/26/5d13948b443d263987.jpg) 某个 Topic 有四个分区 `p0 p1 p2 p3`,同时创建了两个消费组 `groupA,groupB`。 @@ -149,7 +149,7 @@ c1 线程只取 0 分区;c2 只取 1 分区;c3 只取 2 分区的数据。 当其中一个进程(其中有三个线程,每个线程对应一个消费实例)时,消费结果如下: -![](https://ws3.sinaimg.cn/large/006tNbRwly1fxdwin6y1gj30yf08sq7f.jpg) +![](https://i.loli.net/2019/06/26/5d13948bc771365298.jpg) 里边的 20 条数据都被这个进程的三个实例消费掉。 @@ -159,7 +159,7 @@ c1 线程只取 0 分区;c2 只取 1 分区;c3 只取 2 分区的数据。 进程1 只取到了分区 1 里的两条数据(之前是所有数据都是进程1里的线程获取的)。 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fxdwkziunkj30vm02kjsa.jpg) +![](https://i.loli.net/2019/06/26/5d13948d56c0b79122.jpg) --- @@ -171,7 +171,7 @@ c1 线程只取 0 分区;c2 只取 1 分区;c3 只取 2 分区的数据。 当我关掉进程2,再发送10条数据时会发现所有数据又被进程1里的三个线程消费了。 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fxdwndw0q6j312m066acx.jpg) +![](https://i.loli.net/2019/06/26/5d1395ff039d368487.jpg) 通过这些测试相信大家已经可以看到消费组的优势了。 @@ -201,4 +201,4 @@ c1 线程只取 0 分区;c2 只取 1 分区;c3 只取 2 分区的数据。 -**欢迎关注公众号一起交流:** \ No newline at end of file +**欢迎关注公众号一起交流:** diff --git a/docs/frame/kafka-product.md b/docs/frame/kafka-product.md index b3eecb4e..d50364a8 100755 --- a/docs/frame/kafka-product.md +++ b/docs/frame/kafka-product.md @@ -2,7 +2,7 @@ # 从源码分析如何优雅的使用 Kafka 生产者 -![](https://ws2.sinaimg.cn/large/006tNbRwgy1fw2g4pw7ooj31kw11xwjh.jpg) +![](https://i.loli.net/2019/06/26/5d13945f3952999092.jpg) # 前言 @@ -23,7 +23,7 @@ 首先创建一个 `org.apache.kafka.clients.producer.Producer` 的 bean。 -![](https://ws1.sinaimg.cn/large/006tNbRwgy1fw2hc2t8oij30n507g0u6.jpg) +![](https://i.loli.net/2019/06/26/5d13945fedfe948763.jpg) 主要关注 `bootstrap.servers`,它是必填参数。指的是 Kafka 集群中的 broker 地址,例如 `127.0.0.1:9094`。 @@ -31,7 +31,7 @@ 接着注入这个 bean 即可调用它的发送函数发送消息。 -![](https://ws4.sinaimg.cn/large/006tNbRwgy1fw2he841x7j30ou054751.jpg) +![](https://i.loli.net/2019/06/26/5d1394607779261290.jpg) 这里我给某一个 Topic 发送了 10W 条数据,运行程序消息正常发送。 @@ -43,11 +43,11 @@ 其实 `Producer` 的 `API` 已经帮我们考虑到了,发送之后只需要调用它的 `get()` 方法即可同步获取发送结果。 -![](https://ws4.sinaimg.cn/large/006tNbRwly1fw3fsyrkpbj3103065mya.jpg) +![](https://i.loli.net/2019/06/26/5d139460ec48465745.jpg) 发送结果: -![](https://ws2.sinaimg.cn/large/006tNbRwly1fw3ftq0w5lj312g053770.jpg) +![](https://i.loli.net/2019/06/26/5d1394624bd1b33069.jpg) 这样的发送效率其实是比较低下的,因为每次都需要同步等待消息发送的结果。 @@ -65,11 +65,11 @@ Future send(ProducerRecord producer, Callback callback); `Callback` 是一个回调接口,在消息发送完成之后可以回调我们自定义的实现。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fw3g4hce6aj30zv0b0dhp.jpg) +![](https://i.loli.net/2019/06/26/5d139467efab428313.jpg) 执行之后的结果: -![](https://ws2.sinaimg.cn/large/006tNbRwly1fw3g54ne3oj31do06t0wl.jpg) +![](https://i.loli.net/2019/06/26/5d139468a896053832.jpg) 同样的也能获取结果,同时发现回调的线程并不是上文同步时的`主线程`,这样也能证明是异步回调的。 @@ -82,7 +82,7 @@ Future send(ProducerRecord producer, Callback callback); 所以正确的写法应当是: -![](https://ws4.sinaimg.cn/large/006tNbRwly1fw3g9fst9kj30zy07jab0.jpg) +![](https://i.loli.net/2019/06/26/5d13946a0678117891.jpg) > 至于为什么会只有参数一个有值,在下文的源码分析中会一一解释。 @@ -97,7 +97,7 @@ Future send(ProducerRecord producer, Callback callback); 为了直观的了解发送的流程,简单的画了几个在发送过程中关键的步骤。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fw3j5x05izj30a40btmxt.jpg) +![](https://i.loli.net/2019/06/26/5d13946b61a1015175.jpg) 从上至下依次是: @@ -114,13 +114,14 @@ Future send(ProducerRecord producer, Callback callback); ### 初始化 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fw3jc9hvwbj30rc0273yn.jpg) +![](https://i.loli.net/2019/06/26/5d13946bef0f188816.jpg) 调用该构造方法进行初始化时,不止是简单的将基本参数写入 `KafkaProducer`。比较麻烦的是初始化 `Sender` 线程进行缓冲区消费。 初始化 IO 线程处: -![](https://ws2.sinaimg.cn/large/006tNbRwly1fw3jh4xtt2j31fo02pgms.jpg) +![kafka-product.md---006tNbRwly1fw3jh4xtt2j31fo02pgms.jpg](https://i.loli.net/2019/06/26/5d1395b88cb5d97051.jpg) + 可以看到 Sender 线程有需要成员变量,比如: @@ -134,11 +135,11 @@ acks,retries,requestTimeout 在调用 `send()` 函数后其实第一步就是序列化,毕竟我们的消息需要通过网络才能发送到 Kafka。 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fw3job8ejaj31fw05owg2.jpg) +![](https://i.loli.net/2019/06/26/5d139473088b949912.jpg) 其中的 `valueSerializer.serialize(record.topic(), record.value());` 是一个接口,我们需要在初始化时候指定序列化实现类。 -![](https://ws4.sinaimg.cn/large/006tNbRwly1fw3jq5h0nyj30p607oq4e.jpg) +![](https://i.loli.net/2019/06/26/5d139473ac2a494720.jpg) 我们也可以自己实现序列化,只需要实现 `org.apache.kafka.common.serialization.Serializer` 接口即可。 @@ -156,23 +157,23 @@ acks,retries,requestTimeout 可以在构建 `ProducerRecord` 为每条消息指定分区。 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fw3jxiet6mj30pj06smyb.jpg) +![](https://i.loli.net/2019/06/26/5d139474258e055711.jpg) 这样在路由时会判断是否有指定,有就直接使用该分区。 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fw3jybsavdj30zj077abj.jpg) +![](https://i.loli.net/2019/06/26/5d13947490e3c51457.jpg) 这种一般在特殊场景下会使用。 #### 自定义路由策略 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fw3k0giiy6j30zm079ta7.jpg) +![](https://i.loli.net/2019/06/26/5d13947582be674626.jpg) 如果没有指定分区,则会调用 `partitioner.partition` 接口执行自定义分区策略。 而我们也只需要自定义一个类实现 `org.apache.kafka.clients.producer.Partitioner` 接口,同时在创建 `KafkaProducer` 实例时配置 `partitioner.class` 参数。 -![](https://ws4.sinaimg.cn/large/006tNbRwly1fw3k5uqf68j30rm04pt94.jpg) +![](https://i.loli.net/2019/06/26/5d13947a8e98b39707.jpg) 通常需要自定义分区一般是在想尽量的保证消息的顺序性。 @@ -186,7 +187,7 @@ acks,retries,requestTimeout 来看看它的实现: -![](https://ws2.sinaimg.cn/large/006tNbRwly1fw3kajn4iyj30r20g2772.jpg) +![](https://i.loli.net/2019/06/26/5d13947b2fbb155310.jpg) 简单的来说分为以下几步: @@ -200,26 +201,26 @@ acks,retries,requestTimeout 在 `send()` 方法拿到分区后会调用一个 `append()` 函数: -![](https://ws3.sinaimg.cn/large/006tNbRwly1fw3khecuqej313704uwg9.jpg) +![](https://i.loli.net/2019/06/26/5d13947c189cf45913.jpg) 该函数中会调用一个 `getOrCreateDeque()` 写入到一个内部缓存中 `batches`。 -![](https://ws2.sinaimg.cn/large/006tNbRwly1fw3kih9wf1j30j005daaq.jpg) +![](https://i.loli.net/2019/06/26/5d13947c8cf8b64631.jpg) ### 消费缓存 在最开始初始化的 IO 线程其实是一个守护线程,它会一直消费这些数据。 -![](https://ws4.sinaimg.cn/large/006tNbRwly1fw3kntf8xlj30sn0ju42o.jpg) +![](https://i.loli.net/2019/06/26/5d13947e0f60822234.jpg) 通过图中的几个函数会获取到之前写入的数据。这块内容可以不必深究,但其中有个 `completeBatch` 方法却非常关键。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fw3kqrk5rnj312e0jbjve.jpg) +![](https://i.loli.net/2019/06/26/5d139483ba47613836.jpg) 调用该方法时候肯定已经是消息发送完毕了,所以会调用 `batch.done()` 来完成之前我们在 `send()` 方法中定义的回调接口。 -![](https://ws4.sinaimg.cn/large/006tNbRwly1fw3kuprn02j30zo09qgnr.jpg) +![](https://i.loli.net/2019/06/26/5d13948a61cbc31617.jpg) > 从这里也可以看出为什么之前说发送完成后元数据和异常信息只会出现一个。 @@ -231,7 +232,7 @@ acks,retries,requestTimeout `acks` 是一个影响消息吞吐量的一个关键参数。 -![](https://ws2.sinaimg.cn/large/006tNbRwly1fw3l52birsj30u607o0ta.jpg) +![](https://i.loli.net/2019/06/26/5d13948ae180b18955.jpg) 主要有 `[all、-1, 0, 1]` 这几个选项,默认为 1。 @@ -264,9 +265,9 @@ producer 不会等待副本的任何响应,这样最容易丢失消息但同 但也不能极端,调太大会浪费内存。小了也发挥不了作用,也是一个典型的时间和空间的权衡。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fw3l2ydx4tj311l0e9ae3.jpg) +![](https://i.loli.net/2019/06/26/5d13948bbdf5832883.jpg) -![](https://ws1.sinaimg.cn/large/006tNbRwly1fw3l3mh0pqj312409940u.jpg) +![](https://i.loli.net/2019/06/26/5d13948d0d86f22526.jpg) 上图是几个使用的体现。 @@ -299,7 +300,7 @@ producer 不会等待副本的任何响应,这样最容易丢失消息但同 最后则是 `Producer` 的关闭,Producer 在使用过程中消耗了不少资源(线程、内存、网络等)因此需要显式的关闭从而回收这些资源。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fw3mw4a00rj311x0kp434.jpg) +![](https://i.loli.net/2019/06/26/5d13948e08f3a58866.jpg) 默认的 `close()` 方法和带有超时时间的方法都是在一定的时间后强制关闭。 @@ -320,4 +321,4 @@ producer 不会等待副本的任何响应,这样最容易丢失消息但同 **欢迎关注公众号一起交流:** - \ No newline at end of file + diff --git a/docs/jvm/ClassLoad.md b/docs/jvm/ClassLoad.md index e2e79699..3578ad71 100644 --- a/docs/jvm/ClassLoad.md +++ b/docs/jvm/ClassLoad.md @@ -4,7 +4,7 @@ 模型如下图: -![](https://ws3.sinaimg.cn/large/006tNc79ly1fmjwua3iv4j30ic0f0mxq.jpg) +![](https://i.loli.net/2019/07/19/5d31384d5ecdf51413.jpg) 双亲委派模型中除了启动类加载器之外其余都需要有自己的父类加载器 @@ -14,4 +14,4 @@ 双亲委派的好处 : 由于每个类加载都会经过最顶层的启动类加载器,比如 `java.lang.Object`这样的类在各个类加载器下都是同一个类(只有当两个类是由同一个类加载器加载的才有意义,这两个类才相等。) -如果没有双亲委派模型,由各个类加载器自行加载的话。当用户自己编写了一个 `java.lang.Object`类,那样系统中就会出现多个 `Object`,这样 Java 程序中最基本的行为都无法保证,程序会变的非常混乱。 \ No newline at end of file +如果没有双亲委派模型,由各个类加载器自行加载的话。当用户自己编写了一个 `java.lang.Object`类,那样系统中就会出现多个 `Object`,这样 Java 程序中最基本的行为都无法保证,程序会变的非常混乱。 diff --git a/docs/jvm/GarbageCollection.md b/docs/jvm/GarbageCollection.md index b6253e3f..eeb3a43a 100644 --- a/docs/jvm/GarbageCollection.md +++ b/docs/jvm/GarbageCollection.md @@ -19,7 +19,7 @@ 可达性算法是通过一个称为 `GC Roots` 的对象向下搜索,整个搜索路径就称为引用链,当一个对象到 `GC Roots` 没有任何引用链 `JVM` 就认为该对象是可以被回收的。 -![](https://ws3.sinaimg.cn/large/006tNc79gy1fmwqi5mv1jj30e407kmxm.jpg) +![](https://i.loli.net/2019/07/19/5d313829b468683360.jpg) 如图:Object1、2、3、4 都是存活的对象,而 Object5、6、7都是可回收对象。 @@ -39,7 +39,7 @@ 标记清除过程如下: -![](https://ws3.sinaimg.cn/large/006tNc79gy1fmz99ai1n3j30fj08qdgc.jpg) +![](https://i.loli.net/2019/07/19/5d31382a842c844446.jpg) ### 复制算法 @@ -53,7 +53,7 @@ 复制算法过程: -![](https://ws3.sinaimg.cn/large/006tNc79gy1fmzavlf4enj30fj08qt9b.jpg) +![](https://i.loli.net/2019/07/19/5d31382aea89b37377.jpg) ### 标记整理算法 @@ -62,7 +62,7 @@ 所以老年代中使用的时候`标记整理算法`,它的原理和`标记清除算法`类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后再将边界之外的内存全部回收。 -![](https://ws3.sinaimg.cn/large/006tNc79gy1fmzbq55pfdj30fe08s3yx.jpg) +![](https://i.loli.net/2019/07/19/5d31382b3ca8f11151.jpg) ### 分代回收算法 现代多数的商用 `JVM` 的垃圾收集器都是采用的分代回收算法,和之前所提到的算法并没有新的内容。 diff --git a/docs/jvm/JVM-concurrent-HashSet-problem.md b/docs/jvm/JVM-concurrent-HashSet-problem.md index 76870790..f834b47a 100755 --- a/docs/jvm/JVM-concurrent-HashSet-problem.md +++ b/docs/jvm/JVM-concurrent-HashSet-problem.md @@ -1,6 +1,6 @@ -![](https://ws1.sinaimg.cn/large/006tNbRwly1fwztw0m8okj31hc0u0tbz.jpg) +![](https://i.loli.net/2019/07/19/5d3138498107383830.jpg) # 背景 @@ -47,7 +47,7 @@ ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize, -![](https://ws2.sinaimg.cn/large/006tNbRwly1fwzum85fz9j30n30hsadh.jpg) +![](https://i.loli.net/2019/07/19/5d3138538800334258.jpg) @@ -75,7 +75,7 @@ ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize, -![](https://ws1.sinaimg.cn/large/006tNbRwly1fwzwvpjxzpj30ob08q76a.jpg) +![](https://i.loli.net/2019/07/19/5d3138551b39c76261.jpg) 发现正好就是在处理上文提到的 `HashSet`,看这个堆栈是在查询 `key` 是否存在。通过查看 312 行的业务代码确实也是如此。 @@ -105,7 +105,7 @@ ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize, 通过之前的监控曲线图也可以看出: -![](https://ws3.sinaimg.cn/large/006tNbRwly1fwzx8rod8yj30f405cmyh.jpg) +![](https://i.loli.net/2019/07/19/5d313854e9fa082346.jpg) 操作系统在之前一直处于高负载中,直到我们早上看到报警重启之后才降低。 @@ -191,11 +191,11 @@ public class Worker extends Thread{ 不巧的是代码中也有查询操作(`contains()`),观察上文的堆栈情况: -![](https://ws1.sinaimg.cn/large/006tNbRwly1fwzwvpjxzpj30ob08q76a.jpg) +![](https://i.loli.net/2019/07/19/5d3138551b39c76261.jpg) 发现是运行在 `HashMap` 的 465 行,来看看 1.7 中那里具体在做什么: -![](https://ws2.sinaimg.cn/large/006tNbRwly1fwzy1tp1ftj30rd08ct9x.jpg) +![](https://i.loli.net/2019/07/19/5d313855acb5545919.jpg) 已经很明显了。这里在遍历链表,同时由于形成了环形链表导致这个 `e.next` 永远不为空,所以这个循环也不会退出了。 @@ -248,4 +248,4 @@ public class Worker extends Thread{ `HashMap` 的死循环问题在网上层出不穷,没想到还真被我遇到了。现在要满足这个条件还是挺少见的,比如 1.8 以下的 `JDK` 这一条可能大多数人就碰不到,正好又证实了一次墨菲定律。 -**你的点赞与分享是对我最大的支持** \ No newline at end of file +**你的点赞与分享是对我最大的支持** diff --git a/docs/jvm/MemoryAllocation.md b/docs/jvm/MemoryAllocation.md index f4dcbf1f..ce615e40 100644 --- a/docs/jvm/MemoryAllocation.md +++ b/docs/jvm/MemoryAllocation.md @@ -1,6 +1,6 @@ # Java 运行时的内存划分 -![](https://ws1.sinaimg.cn/large/006tNc79ly1fmk5v19cmvj30g20anq3y.jpg) +![](https://i.loli.net/2019/07/19/5d31384c568c531115.jpg) ## 程序计数器 @@ -73,7 +73,7 @@ ## 常用参数 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fxjcmnkuqyj30p009vjsn.jpg) +![](https://i.loli.net/2019/07/19/5d31384cbc79744624.jpg) 通过上图可以直观的查看各个区域的参数设置。 @@ -90,4 +90,4 @@ 新生代和老年代的默认比例为 `1:2`,也就是说新生代占用 `1/3`的堆内存,而老年代占用 `2/3` 的堆内存。 -可以通过参数 `-XX:NewRatio=2` 来设置老年代/新生代的比例。 \ No newline at end of file +可以通过参数 `-XX:NewRatio=2` 来设置老年代/新生代的比例。 diff --git a/docs/jvm/OOM-Disruptor.md b/docs/jvm/OOM-Disruptor.md index 8b88697e..c9f32f7b 100644 --- a/docs/jvm/OOM-Disruptor.md +++ b/docs/jvm/OOM-Disruptor.md @@ -1,4 +1,4 @@ -![](https://ws2.sinaimg.cn/large/0069RVTdgy1fupvtq0tf1j31kw11x1ab.jpg) +![](https://i.loli.net/2019/07/19/5d3138372d6e887188.jpg) # 前言 @@ -24,13 +24,13 @@ 于是我们想根据运维之前收集到的内存数据、GC 日志尝试判断哪里出现问题。 -![](https://ws1.sinaimg.cn/large/0069RVTdgy1fupwodz2tlj30rd0b1tcj.jpg) +![](https://i.loli.net/2019/07/19/5d313837e4bed39389.jpg) 结果发现老年代的内存使用就算是发生 GC 也一直居高不下,而且随着时间推移也越来越高。 结合 jstat 的日志发现就算是发生了 FGC 老年代也已经回收不了,内存已经到顶。 -![](https://ws4.sinaimg.cn/large/0069RVTdly1fupx2amu1lj30t90b17oe.jpg) +![](https://i.loli.net/2019/07/19/5d31383dd7f7267709.jpg) 甚至有几台应用 FGC 达到了上百次,时间也高的可怕。 @@ -53,7 +53,7 @@ 结果跑了 10 几分钟内存使用并没有什么问题。根据图中可以看出,每产生一次 GC 内存都能有效的回收,所以这样并没有复现问题。 -![](https://ws2.sinaimg.cn/large/0069RVTdly1fupxfovjhgj30vl0kywps.jpg) +![](https://i.loli.net/2019/07/19/5d31383e755bb33860.jpg) 没法复现问题就很难定位了。于是我们 review 代码,发现生产的逻辑和我们用 while 循环 Mock 数据还不太一样。 @@ -64,7 +64,7 @@ 果然不出意外只跑了一分多钟内存就顶不住了,观察左图发现 GC 的频次非常高,但是内存的回收却是相形见拙。 -![](https://ws4.sinaimg.cn/large/0069RVTdly1fupxcg3yh7j31kw0xi122.jpg) +![](https://i.loli.net/2019/07/19/5d3138428c8a826344.jpg) 同时后台也开始打印内存溢出了,这样便复现出问题。 @@ -74,7 +74,7 @@ 于是便想看看到底是什么对象占用了这么多的内存,利用 VisualVM 的 HeapDump 功能可以立即 dump 出当前应用的内存情况。 -![](https://ws2.sinaimg.cn/large/0069RVTdly1fupxqxqjdcj318c0q4kb3.jpg) +![](https://i.loli.net/2019/07/19/5d3138486fea240331.jpg) 结果发现 `com.lmax.disruptor.RingBuffer` 类型的对象占用了将近 50% 的内存。 @@ -90,7 +90,7 @@ 我也做了一个实验,证明确实如此。 -![](https://ws4.sinaimg.cn/large/0069RVTdly1fupy48es6lj30jd0b9dhu.jpg) +![](https://i.loli.net/2019/07/19/5d3138493076e20268.jpg) 我设置队列大小为 8 ,从 0~9 往里面写 10 条数据,当写到 8 的时候就会把之前 0 的位置覆盖掉,后面的以此类推(类似于 HashMap 的取模定位)。 @@ -104,7 +104,7 @@ 同样的 128M 内存,也是通过 Kafka 一直源源不断的取出数据。通过监控如下: -![](https://ws4.sinaimg.cn/large/0069RVTdly1fupyds04cij31kw0xial3.jpg) +![](https://i.loli.net/2019/07/19/5d31384bc3a3888930.jpg) 跑了 20 几分钟系统一切正常,每当一次 GC 都能回收大部分内存,最终呈现锯齿状。 @@ -122,4 +122,4 @@ [https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor](https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor) -**你的点赞与转发是最大的支持。** \ No newline at end of file +**你的点赞与转发是最大的支持。** diff --git a/docs/jvm/cpu-percent-100.md b/docs/jvm/cpu-percent-100.md index a960d2ca..4c327b5f 100755 --- a/docs/jvm/cpu-percent-100.md +++ b/docs/jvm/cpu-percent-100.md @@ -1,6 +1,6 @@ -![](https://ws3.sinaimg.cn/large/006tNbRwly1fy67gauqxyj31eg0u0gun.jpg) +![](https://i.loli.net/2019/07/19/5d31382a2d77079070.jpg) # 前言 @@ -17,7 +17,7 @@ 接着使用 `top -Hp pid` 将这个进程的线程显示出来。输入大写的 P 可以将线程按照 CPU 使用比例排序,于是得到以下结果。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fy7z1kg8s3j30s40ncn9w.jpg) +![](https://i.loli.net/2019/07/19/5d31382b1d3df15468.jpg) 果然某些线程的 CPU 使用率非常高。 @@ -28,7 +28,7 @@ > 因为线程快照中线程 ID 都是16进制存放。 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fy7z7vtcruj30q5056tar.jpg) +![](https://i.loli.net/2019/07/19/5d31382bb08a129414.jpg) 发现这是 `Disruptor` 的一个堆栈,前段时间正好解决过一个由于 Disruptor 队列引起的一次 [OOM]():[强如 Disruptor 也发生内存溢出?](https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/) @@ -38,7 +38,7 @@ [http://fastthread.io/](http://fastthread.io/) -![](https://ws2.sinaimg.cn/large/006tNbRwly1fy7zciqp2ij311q0q5jzl.jpg) +![](https://i.loli.net/2019/07/19/5d31382fbe13e22162.jpg) 其中有一项菜单展示了所有消耗 CPU 的线程,我仔细看了下发现几乎都是和上面的堆栈一样。 @@ -60,7 +60,7 @@ 代码如下: -![](https://ws3.sinaimg.cn/large/006tNbRwly1fy8yrlsituj30nv0nfq5d.jpg) +![](https://i.loli.net/2019/07/19/5d31383063b5729406.jpg) > 初步看来和这个等待策略有很大的关系。 @@ -68,44 +68,44 @@ 为了验证,我在本地创建了 15 个 `Disruptor` 队列同时结合监控观察 CPU 的使用情况。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fy8wd8puupj30s10bs0up.jpg) -![](https://ws1.sinaimg.cn/large/006tNbRwly1fy8weciz9jj30po03z0tk.jpg) +![](https://i.loli.net/2019/07/19/5d313830e683e59146.jpg) +![](https://i.loli.net/2019/07/19/5d3138364092a60230.jpg) 创建了 15 个 `Disruptor` 队列,同时每个队列都用线程池来往 `Disruptor队列` 里面发送 100W 条数据。 消费程序仅仅只是打印一下。 -![](https://ws2.sinaimg.cn/large/006tNbRwly1fy8whdcy5hj30e706tdg7.jpg) +![](https://i.loli.net/2019/07/19/5d313836ac8a448151.jpg) 跑了一段时间发现 CPU 使用率确实很高。 --- -![](https://ws4.sinaimg.cn/large/006tNbRwly1fy8wjq0xkwj310t0cln12.jpg) +![](https://i.loli.net/2019/07/19/5d31383d664cb51737.jpg) 同时 `dump` 线程发现和生产的现象也是一致的:消费线程都处于 `RUNNABLE` 状态,同时都在执行 `yield`。 通过查询 `Disruptor` 官方文档发现: -![](https://ws4.sinaimg.cn/large/006tNbRwly1fy8wx1x6z8j30l1069jsz.jpg) +![](https://i.loli.net/2019/07/19/5d31383e10f0327921.jpg) > YieldingWaitStrategy 是一种充分压榨 CPU 的策略,使用`自旋 + yield`的方式来提高性能。 > 当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。 --- -![](https://ws3.sinaimg.cn/large/006tNbRwly1fy8wym9wxlj30ln04sjsm.jpg) +![](https://i.loli.net/2019/07/19/5d31383fd2dc594576.jpg) 同时查阅到其他的等待策略 `BlockingWaitStrategy` (也是默认的策略),它使用的是锁的机制,对 CPU 的使用率不高。 于是在和之前同样的条件下将等待策略换为 `BlockingWaitStrategy`。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fy8x3b5xh7j30pl0brgnh.jpg) +![](https://i.loli.net/2019/07/19/5d31384097d6190496.jpg) --- -![](https://ws1.sinaimg.cn/large/006tNbRwly1fy8x6jytcoj30e605b3yt.jpg) -![](https://ws3.sinaimg.cn/large/006tNbRwly1fy8x79u64nj30t6076jty.jpg) +![](https://i.loli.net/2019/07/19/5d3138411411e73544.jpg) +![](https://i.loli.net/2019/07/19/5d313841d679b99195.jpg) 和刚才的 CPU 对比会发现到后面使用率的会有明显的降低;同时 dump 线程后会发现大部分线程都处于 waiting 状态。 @@ -119,9 +119,9 @@ 而现有的使用场景很明显消费线程数已经大大的超过了核心 CPU 数了,因为我的使用方式是一个 `Disruptor` 队列一个消费者,所以我将队列调整为只有 1 个再试试(策略依然是 `YieldingWaitStrategy`)。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fy8xlhzh05j30qo0aogng.jpg) +![](https://i.loli.net/2019/07/19/5d313842427b798742.jpg) -![](https://ws2.sinaimg.cn/large/006tNbRwly1fy8xn1ktk6j30e207g0t0.jpg) +![](https://i.loli.net/2019/07/19/5d3138669071113680.jpg) 跑了一分钟,发现 CPU 的使用率一直都比较平稳而且不高。 @@ -149,4 +149,4 @@ **你的点赞与分享是对我最大的支持** -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrjtr3ja2j30760760t7.jpg) \ No newline at end of file +![](https://i.loli.net/2019/07/19/5d313848b169269048.jpg) diff --git a/docs/jvm/newObject.md b/docs/jvm/newObject.md index 6c091b4e..4542db99 100644 --- a/docs/jvm/newObject.md +++ b/docs/jvm/newObject.md @@ -36,7 +36,7 @@ 如图: -![](https://ws2.sinaimg.cn/large/006tKfTcly1fnkmy0bvu3j30o60heaaq.jpg) +![](https://i.loli.net/2019/07/19/5d31384ddc06744280.jpg) 这样的好处就是:在 Java 里进行频繁的对象访问可以提升访问速度(相对于使用句柄池来说)。 @@ -74,4 +74,4 @@ JVM 是根据记录对象年龄的方式来判断该对象是否应该移动到 ## 总结 -虽说这些内容略显枯燥,但当应用发生不正常的 `GC` 时,可以方便更快的定位问题。 \ No newline at end of file +虽说这些内容略显枯燥,但当应用发生不正常的 `GC` 时,可以方便更快的定位问题。 diff --git a/docs/jvm/volatile.md b/docs/jvm/volatile.md index d836e7ec..3110e2c6 100644 --- a/docs/jvm/volatile.md +++ b/docs/jvm/volatile.md @@ -15,7 +15,7 @@ 如下图所示: -![](https://ws2.sinaimg.cn/large/006tKfTcly1fmouu3fpokj31ae0osjt1.jpg) +![](https://i.loli.net/2019/07/19/5d31384d22ac511765.jpg) 所以在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新之前的数据。 @@ -215,4 +215,4 @@ public class Singleton { `volatile` 在 `Java` 并发中用的很多,比如像 `Atomic` 包中的 `value`、以及 `AbstractQueuedLongSynchronizer` 中的 `state` 都是被定义为 `volatile` 来用于保证内存可见性。 -将这块理解透彻对我们编写并发程序时可以提供很大帮助。 \ No newline at end of file +将这块理解透彻对我们编写并发程序时可以提供很大帮助。 diff --git a/docs/netty/Netty(1)TCP-Heartbeat.md b/docs/netty/Netty(1)TCP-Heartbeat.md index 7d5cf2ca..e562ecb1 100644 --- a/docs/netty/Netty(1)TCP-Heartbeat.md +++ b/docs/netty/Netty(1)TCP-Heartbeat.md @@ -137,7 +137,7 @@ public class CustomerHandleInitializer extends ChannelInitializer { 所以当应用启动每隔 10 秒会检测是否发送过消息,不然就会发送心跳信息。 -![](https://ws3.sinaimg.cn/large/006tKfTcgy1frqd863hrhj31kw04taed.jpg) +![](https://i.loli.net/2019/07/19/5d313938a059249899.jpg) ## 服务端心跳 @@ -286,7 +286,7 @@ public class HeartbeatInitializer extends ChannelInitializer { 也是同样将IdleStateHandler 添加到 ChannelPipeline 中,也会有一个定时任务,每5秒校验一次是否有收到消息,否则就主动发送一次请求。 -![](https://ws1.sinaimg.cn/large/006tKfTcgy1frqe2hbxjfj31kw06rtej.jpg) +![](https://i.loli.net/2019/07/19/5d31393e0f8c660705.jpg) 因为测试是有两个客户端连上所以有两个日志。 @@ -474,10 +474,10 @@ public class HeartbeatDecoder extends ByteToMessageDecoder { 就开启了 SpringBoot 的 actuator 监控功能,他可以暴露出很多监控端点供我们使用。 如一些应用中的一些统计数据: -![](https://ws1.sinaimg.cn/large/006tKfTcgy1frqeyocotnj31kw0b8tiy.jpg) +![](https://i.loli.net/2019/07/19/5d31393fa017351196.jpg) 存在的 Beans: -![](https://ws3.sinaimg.cn/large/006tKfTcgy1frqez1kr3dj31kw0onawi.jpg) +![](https://i.loli.net/2019/07/19/5d31394d8fb6d16523.jpg) 更多信息请查看:[https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html) @@ -536,10 +536,10 @@ public class EndPointConfig { 这样我们就可以通过配置文件中的 `monitor.channel.map.key` 来访问了: 一个客户端连接时: -![](https://ws3.sinaimg.cn/large/006tKfTcgy1frqf7ic0wqj31kw07rq7a.jpg) +![](https://i.loli.net/2019/07/19/5d31394e55fe384723.jpg) 两个客户端连接时: -![](https://ws2.sinaimg.cn/large/006tKfTcgy1frqf8omlzkj31kw07xq7y.jpg) +![](https://i.loli.net/2019/07/19/5d31395522dda45659.jpg) ## 整合 SBA @@ -552,7 +552,7 @@ public class EndPointConfig { 简单来说我们可以利用该工具将 actuator 暴露出来的接口可视化并聚合的展示在页面中: -![](https://ws2.sinaimg.cn/large/006tKfTcgy1frqfbz359wj31kw0p513n.jpg) +![](https://i.loli.net/2019/07/19/5d313956b3aca18512.jpg) 接入也很简单,首先需要引入依赖: @@ -609,7 +609,7 @@ public class AdminApplication { 这样我们在 SpringBootAdmin 的页面中就可以查看很多应用信息了。 -![](https://ws4.sinaimg.cn/large/006tKfTcgy1frqfjuof2rj31kw10ldqk.jpg) +![](https://i.loli.net/2019/07/19/5d31395886b8e59039.jpg) 更多内容请参考官方指南: @@ -668,21 +668,21 @@ public class IndexController { 当我们调用该接口时: -![](https://ws4.sinaimg.cn/large/006tKfTcgy1frqfv3vx0ej31hk0g6dj5.jpg) +![](https://i.loli.net/2019/07/19/5d313960a3a9826851.jpg) -![](https://ws4.sinaimg.cn/large/006tKfTcgy1frqfvof9lpj31kw07l0z8.jpg) +![](https://i.loli.net/2019/07/19/5d3139634d9e553115.jpg) 在监控页面中可以查询刚才的调用情况: -![](https://ws4.sinaimg.cn/large/006tKfTcgy1frqfwembi6j31kw0o0dot.jpg) +![](https://i.loli.net/2019/07/19/5d313964b932568620.jpg) 服务端主动 push 消息也是类似,只是需要在发送时候根据客户端的 ID 查询到具体的 Channel 发送: -![](https://ws4.sinaimg.cn/large/006tKfTcgy1frqfy9dcu5j31hu0f277i.jpg) +![](https://i.loli.net/2019/07/19/5d31396c18f0c66961.jpg) -![](https://ws2.sinaimg.cn/large/006tKfTcgy1frqfz0aticj31kw05jgol.jpg) +![](https://i.loli.net/2019/07/19/5d313aacddff648185.jpg) -![](https://ws3.sinaimg.cn/large/006tKfTcgy1frqfztzxd8j31kw0iyn0t.jpg) +![](https://i.loli.net/2019/07/19/5d313aada4d3082044.jpg) # 总结 diff --git a/docs/netty/Netty(2)Thread-model.md b/docs/netty/Netty(2)Thread-model.md index bbabd473..a60ab5a6 100644 --- a/docs/netty/Netty(2)Thread-model.md +++ b/docs/netty/Netty(2)Thread-model.md @@ -1,4 +1,4 @@ -![](https://ws1.sinaimg.cn/large/006tNc79gy1fsx42fcwsxj312v0ocjve.jpg) +![](https://i.loli.net/2019/07/19/5d313935e4ef253589.jpg) ## 前言 @@ -49,7 +49,7 @@ while((request = in.readLine()) != null){ ### 单线程 -![](https://ws4.sinaimg.cn/large/006tNc79gy1fsx4by9581j30k60aygn7.jpg) +![](https://i.loli.net/2019/07/19/5d3139369e57d74023.jpg) 从图中可以看出: @@ -60,7 +60,7 @@ while((request = in.readLine()) != null){ ### 多线程 -![](https://ws2.sinaimg.cn/large/006tNc79gy1fsx4cctol0j30k70dq40n.jpg) +![](https://i.loli.net/2019/07/19/5d313937667e941981.jpg) 因此产生了多线程模型。 @@ -76,7 +76,7 @@ while((request = in.readLine()) != null){ ### 主从多线程 -![](https://ws1.sinaimg.cn/large/006tNc79gy1fsx4iv4kmxj30gb0c0dha.jpg) +![](https://i.loli.net/2019/07/19/5d313937f2dbd55910.jpg) 该模型将客户端连接那一块的线程也改为多线程,称为主线程。 @@ -179,4 +179,4 @@ ServerBootstrap bootstrap = new ServerBootstrap() [https://github.com/crossoverJie/netty-action](https://github.com/crossoverJie/netty-action) -**欢迎关注公众号一起交流:** \ No newline at end of file +**欢迎关注公众号一起交流:** diff --git a/docs/netty/cicada.md b/docs/netty/cicada.md index 2fca3fb3..4c02e9d8 100644 --- a/docs/netty/cicada.md +++ b/docs/netty/cicada.md @@ -1,7 +1,7 @@
- +
[![Build Status](https://travis-ci.org/crossoverJie/cicada.svg?branch=master)](https://travis-ci.org/crossoverJie/cicada) @@ -187,7 +187,7 @@ public class KafkaConfiguration extends AbstractCicadaConfiguration { } ``` -![](https://ws3.sinaimg.cn/large/0069RVTdgy1fv5mw7p5nvj31by0fo76t.jpg) +![](https://i.loli.net/2019/07/19/5d31392ece42e20923.jpg) ### 获取配置 @@ -247,7 +247,7 @@ public class ExecuteTimeInterceptor implements CicadaInterceptor { ## 性能测试 -![](https://ws4.sinaimg.cn/large/006tNbRwly1fv4luap7w0j31kw0iwdnu.jpg) +![](https://i.loli.net/2019/07/19/5d31392f5efa350999.jpg) > 测试条件:100 threads and 100 connections ;1G RAM/4 CPU。 @@ -283,9 +283,9 @@ public class ExecuteTimeInterceptor implements CicadaInterceptor { > crossoverJie#gmail.com - + ## 特别感谢 - [Netty](https://github.com/netty/netty) -- [blade](https://github.com/lets-blade/blade) \ No newline at end of file +- [blade](https://github.com/lets-blade/blade) diff --git a/docs/netty/cim.md b/docs/netty/cim.md index faf46284..54668249 100755 --- a/docs/netty/cim.md +++ b/docs/netty/cim.md @@ -1,6 +1,6 @@ -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyr320o1ymj31hc0u0qew.jpg) +![](https://i.loli.net/2019/07/19/5d31392e8a50b75976.jpg) # 前言 @@ -13,17 +13,17 @@ 目录结构: -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyrjwtbi0cj304i0jfgm3.jpg) -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyrjy5iu3sj305c03x0sm.jpg) +![](https://i.loli.net/2019/07/19/5d31392f0ad2d76578.jpg) +![](https://i.loli.net/2019/07/19/5d31392f6182225602.jpg) > 本文较长,高能预警;带好瓜子板凳。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fyr5yjibvqj30vk04paal.jpg) -![](https://ws3.sinaimg.cn/large/006tNbRwly1fyr60ahgr5j30te08nt9j.jpg) -![](https://ws3.sinaimg.cn/large/006tNbRwly1fyr60vrexlj30vs0d40up.jpg) +![](https://i.loli.net/2019/07/19/5d31392fe669471794.jpg) +![](https://i.loli.net/2019/07/19/5d31393061d9624009.jpg) +![](https://i.loli.net/2019/07/19/5d313930bd66213776.jpg) 于是在之前的基础上我完善了一些内容,先来看看这个项目的介绍吧: @@ -46,7 +46,7 @@ | YouTube | Bilibili| | :------:| :------: | | [群聊](https://youtu.be/_9a4lIkQ5_o) [私聊](https://youtu.be/kfEfQFPLBTQ) | [群聊](https://www.bilibili.com/video/av39405501) [私聊](https://www.bilibili.com/video/av39405821) | -| | +| | 也在公网部署了一套演示环境,想要试一试的可以联系我加入内测群获取账号一起尬聊😋。 @@ -55,7 +55,7 @@ 下面来看看具体的架构设计。 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fyldgiizhuj315o0r4n0k.jpg) +![](https://i.loli.net/2019/07/19/5d313936e89ff75333.jpg) - `CIM` 中的各个组件均采用 `SpringBoot` 构建。 - 采用 `Netty + Google Protocol Buffer` 构建底层通信。 @@ -83,7 +83,7 @@ 整体的流程也比较简单,流程图如下: -![](https://ws1.sinaimg.cn/large/006tNbRwly1fylfxevl2ij30it0etaau.jpg) +![](https://i.loli.net/2019/07/19/5d31393783d9878382.jpg) - 客户端向 `route` 发起登录。 - 登录成功从 `Zookeeper` 中选择可用 `IM-server` 返回给客户端,并保存登录、路由信息到 `Redis`。 @@ -110,8 +110,8 @@ 首先是服务启动: -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyrejaa9iaj30sg0ammz3.jpg) -![](https://ws1.sinaimg.cn/large/006tNbRwly1fyregwf1tnj30qq07qwg3.jpg) +![](https://i.loli.net/2019/07/19/5d31393808e5864405.jpg) +![](https://i.loli.net/2019/07/19/5d313938780ae12933.jpg) 由于是在 `SpringBoot` 中搭建的,所以在应用启动时需要启动 `Netty` 服务。 @@ -125,13 +125,13 @@ 所以在应用启动成功后需要将自身数据注册到 `Zookeeper` 中。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fyrerhadxkj30sc05ajs9.jpg) -![](https://ws1.sinaimg.cn/large/006tNbRwly1fyres6kmkaj30sd07e3zn.jpg) +![](https://i.loli.net/2019/07/19/5d313938d33fb47365.jpg) +![](https://i.loli.net/2019/07/19/5d31393ec08cc70862.jpg) 最主要的目的就是将当前应用的 `ip + cim-server-port+ http-port` 注册上去。 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fyretaeawnj30y906qjrl.jpg) +![](https://i.loli.net/2019/07/19/5d3139401722563891.jpg) 上图是我在演示环境中注册的两个 `cim-server` 实例(由于在一台服务器,所以只是端口不同)。 @@ -141,14 +141,14 @@ 当客户端请求 `cim-forward-route` 中的登录接口(详见下文)做完业务验证(就相当于日常登录其他网站一样)之后,客户端会向服务端发起一个长连接,如之前的流程所示: -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrg4laej8j30r30eu76o.jpg) +![](https://i.loli.net/2019/07/19/5d313940c94a652003.jpg) 这时客户端会发送一个特殊报文,表明当前是登录信息。 服务端收到后就需要将该客户端的 `userID` 和当前 `Channel` 通道关系保存起来。 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fyrg639a1wj30sw05zdhb.jpg) -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrg6w5anej30se056abe.jpg) +![](https://i.loli.net/2019/07/19/5d3139429a08032119.jpg) +![](https://i.loli.net/2019/07/19/5d313943a25c029466.jpg) 同时也缓存了用户的信息,也就是 `userID` 和 用户名。 @@ -157,7 +157,7 @@ 当客户端断线后也需要将刚才缓存的信息清除掉。 -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrgjiwu2lj30sk0in42x.jpg) +![](https://i.loli.net/2019/07/19/5d313944af8c328873.jpg) 同时也需要调用 `route` 接口清除相关信息(具体接口看下文)。 @@ -165,7 +165,7 @@ ## IM 路由 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fyreyfu8ooj314f0qads5.jpg) +![](https://i.loli.net/2019/07/19/5d313945a039126377.jpg) 从架构图中可以看出,路由层是非常重要的一环;它提供了一系列的 `HTTP` 服务承接了客户端和服务端。 @@ -173,8 +173,8 @@ ### 注册接口 -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrf29j3tmj30sn09zmze.jpg) -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrf2pkzwwj30sg089407.jpg) +![](https://i.loli.net/2019/07/19/5d313946d089382853.jpg) +![](https://i.loli.net/2019/07/19/5d31394c795cc10022.jpg) 由于每一个客户端都是需要登录才能使用的,所以第一步自然是注册。 @@ -187,7 +187,7 @@ 这里的登录和 `cim-server` 中的登录不一样,具有业务性质, -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrfnyin5uj30sp0clq6b.jpg) +![](https://i.loli.net/2019/07/19/5d31394d5915e56923.jpg) - 登录成功之后需要判断是否是重复登录(一个用户只能运行一个客户端)。 - 登录成功后需要从 `Zookeeper` 中获取服务列表(`cim-server`)并根据某种算法选择一台服务返回给客户端。 @@ -195,15 +195,15 @@ 为了实现只能一个用户登录,使用了 `Redis` 中的 `set` 来保存登录信息;利用 `userID` 作为 `key` ,重复的登录就会写入失败。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fyrflvo52lj30qu0attan.jpg) -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyrfmfrnfkj30sm05bgmd.jpg) +![](https://i.loli.net/2019/07/19/5d31394de19fe32033.jpg) +![](https://i.loli.net/2019/07/19/5d31394e5a7cf72944.jpg) > 类似于 Java 中的 HashSet,只能去重保存。 获取一台可用的路由实例也比较简单: -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrfp67qo3j30qz08xq41.jpg) +![](https://i.loli.net/2019/07/19/5d31394ed00d392001.jpg) - 先从 `Zookeeper` 获取所有的服务实例做一个内部缓存。 - 轮询选择一台服务器(目前只有这一种算法,后续会新增)。 @@ -212,9 +212,9 @@ 具体代码如下: -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyrfrnxk03j30qx04g3yy.jpg) -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyrfs42vwcj30qy06raaw.jpg) -![](https://ws1.sinaimg.cn/large/006tNbRwly1fyrfsuferjj30qt08xwfy.jpg) +![](https://i.loli.net/2019/07/19/5d31394f4609531937.jpg) +![](https://i.loli.net/2019/07/19/5d31394f9ad3c50783.jpg) +![](https://i.loli.net/2019/07/19/5d31395006b0b64086.jpg) 也是在应用启动之后监听 `Zookeeper` 中的路由节点,一旦发生变化就会更新内部缓存。 @@ -230,14 +230,14 @@ 因此就需要路由层来发挥作用了。 -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrgrd1fcgj30sm0f9djy.jpg) -![](https://ws1.sinaimg.cn/large/006tNbRwly1fyrgt3135cj30st0a340e.jpg) +![](https://i.loli.net/2019/07/19/5d313955b870762733.jpg) +![](https://i.loli.net/2019/07/19/5d31395784bf477598.jpg) 路由接口收到消息后首先遍历出所有的客户端和服务实例的关系。 路由关系在 `Redis` 中的存放如下: -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrgulywq5j30bc02ztaq.jpg) +![](https://i.loli.net/2019/07/19/5d313957c158a14876.jpg) 由于 `Redis` 单线程的特质,当数据量大时;一旦使用 keys 匹配所有 `cim-route:*` 数据,会导致 Redis 不能处理其他请求。 @@ -249,8 +249,8 @@ 在 `cim-server` 中的实现如下: -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyrgyjn184j30sg0b40v5.jpg) -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrgz0lm1dj30sx082ac3.jpg) +![](https://i.loli.net/2019/07/19/5d3139590e61247525.jpg) +![](https://i.loli.net/2019/07/19/5d31395979de822216.jpg) `cim-server` 收到消息后会在内部缓存中查询该 userID 的通道,接着只需要发消息即可。 @@ -259,8 +259,8 @@ 这是一个辅助接口,可以查询出当前在线用户信息。 -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrh2fp5qrj30sp05u75x.jpg) -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrh2w6fk5j30sh06nmyc.jpg) +![](https://i.loli.net/2019/07/19/5d313959c343668519.jpg) +![](https://i.loli.net/2019/07/19/5d31395a06ae762846.jpg) 实现也很简单,也就是查询之前保存 ”用户登录状态的那个去重 `set` “即可。 @@ -272,12 +272,12 @@ 类似于这样: -![](https://ws3.sinaimg.cn/large/006tNbRwly1fylh7bdlo6g30go01shdt.gif) +![](https://i.loli.net/2019/07/19/5d31396246da062612.jpg) 在我们这个场景中,私聊的前提就是需要获得在线用户的 `userID`。 -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyrh7nditgj30so0cegot.jpg) +![](https://i.loli.net/2019/07/19/5d3139637537e14521.jpg) 所以私聊接口在收到消息后需要查询到接收者所在的 `cim-server` 实例信息,后续的步骤就和群聊一致了。调用接收者所在实例的 `HTTP` 接口下发信息。 @@ -287,8 +287,8 @@ 一旦客户端下线,我们就需要将之前存放在 `Redis` 中的一些信息删除掉(路由信息、登录状态)。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fyrhcb5mehj30sp070gnl.jpg) -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyrhcjsznmj30sp048q3n.jpg) +![](https://i.loli.net/2019/07/19/5d313963e388b20088.jpg) +![](https://i.loli.net/2019/07/19/5d3139642e6b729312.jpg) @@ -301,21 +301,21 @@ 第一步也就是登录,需要在启动时调用 `route` 的登录接口,获得 `cim-server` 信息再创建连接。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fyrhj0mgeyj30si06pdgr.jpg) +![](https://i.loli.net/2019/07/19/5d3139645e63671036.jpg) -![image-20190102001525565](https://ws1.sinaimg.cn/large/006tNbRwly1fyrjgk4maej30su05u3zq.jpg) +![image-20190102001525565](https://i.loli.net/2019/07/19/5d313964a2d2194790.jpg) -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyrhjtv984j30sr0fnadw.jpg) +![](https://i.loli.net/2019/07/19/5d3139661c62598094.jpg) 登录过程中 `route` 接口会判断是否为重复登录,重复登录则会直接退出程序。 -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyrhl41i3cj30sm095jt2.jpg) +![](https://i.loli.net/2019/07/19/5d31396b6301828962.jpg) 接下来是利用 `route` 接口返回的 `cim-server` 实例信息(`ip+port`)创建连接。 最后一步就是发送一个登录标志的信息到服务端,让它保持客户端和 `Channel` 的关系。 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fyrhn66rmij30sn06j0u1.jpg) +![](https://i.loli.net/2019/07/19/5d31396ba9bfa44516.jpg) ### 自定义协议 @@ -323,7 +323,7 @@ 由于是使用 `Google Protocol Buffer` 编解码,所以先看看原始格式。 -![](https://ws1.sinaimg.cn/large/006tNbRwly1fyrhpupejtj30sj072my1.jpg) +![](https://i.loli.net/2019/07/19/5d31396be687915596.jpg) 其实这个协议中目前一共就三个字段: @@ -334,7 +334,7 @@ 目前主要是三种类型,分别对应不同的业务: -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrhsf53dzj30sf08mmy3.jpg) +![](https://i.loli.net/2019/07/19/5d313aac604fa88452.jpg) ### 心跳 @@ -342,12 +342,12 @@ 目前的策略是每隔一分钟就是发送一个心跳包到服务端: -![](https://ws3.sinaimg.cn/large/006tNbRwly1fyrhvs3z2gj30s209njtc.jpg) -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrhwdgorcj30sj08cabj.jpg) +![](https://i.loli.net/2019/07/19/5d313aad7641038034.jpg) +![](https://i.loli.net/2019/07/19/5d313aae3e8ee56138.jpg) 这样服务端每隔一分钟没有收到业务消息时就会收到 `ping` 的心跳包: -![](https://ws1.sinaimg.cn/large/006tNbRwly1fyrhuu1xd3j30s40c0h83.jpg) +![](https://i.loli.net/2019/07/19/5d313aaed8e5685298.jpg) ### 内置命令 @@ -361,17 +361,17 @@ | `:all` | 获取所有命令 | | `:` | 更多命令正在开发中。。 | -![](https://ws3.sinaimg.cn/large/006tNbRwly1fylh7bdlo6g30go01shdt.gif) +![](https://i.loli.net/2019/07/19/5d31396246da062612.jpg) 比如输入 `:q` 就会退出客户端,同时会关闭一些系统资源。 -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyri42lsgpj30sg0cewg3.jpg) -![](https://ws1.sinaimg.cn/large/006tNbRwly1fyri5mwdh5j30sm0djwgp.jpg) +![](https://i.loli.net/2019/07/19/5d313aaf6f05466906.jpg) +![](https://i.loli.net/2019/07/19/5d313aafe852815113.jpg) 当输入 `:olu`(`onlineUser` 的简写)就会去调用 `route` 的获取所有在线用户接口。 -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyri6wuz62j30ss08eq4b.jpg) -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyri7dvvubj312803u48b.jpg) +![](https://i.loli.net/2019/07/19/5d313ab06dd6c35435.jpg) +![](https://i.loli.net/2019/07/19/5d313ab0ea75d16268.jpg) ### 群聊 @@ -379,13 +379,13 @@ 这时会去调用 `route` 的群聊接口。 -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyri9ltxedj30su0bkjtn.jpg) +![](https://i.loli.net/2019/07/19/5d313ab63223a26868.jpg) ### 私聊 私聊也是同理,但前提是需要触发关键字;使用 `userId;;消息内容` 这样的格式才会给某个用户发送消息,所以一般都需要先使用 `:olu` 命令获取所以在线用户才方便使用。 -![](https://ws3.sinaimg.cn/large/006tNbRwly1fyrichyh40j30si0btq57.jpg) +![](https://i.loli.net/2019/07/19/5d313ab6ac1d016245.jpg) ### 消息回调 @@ -393,8 +393,8 @@ 所以在客户端收到消息之后会回调一个接口,在这个接口中可以自定义实现。 -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyriffgjt4j30sh0bitb6.jpg) -![](https://ws2.sinaimg.cn/large/006tNbRwly1fyrigbtcfrj30sq06nq3r.jpg) +![](https://i.loli.net/2019/07/19/5d313ab75333232387.jpg) +![](https://i.loli.net/2019/07/19/5d313ab7e6e3426627.jpg) 因此先创建了一个 `caller` 的 `bean`,这个 `bean` 中包含了一个 `CustomMsgHandleListener` 接口,需要自行处理只需要实现此接口即可。 @@ -410,10 +410,10 @@ 后续计划: -![](https://ws4.sinaimg.cn/large/006tNbRwly1fyrinluw2vj30r50arabu.jpg) +![](https://i.loli.net/2019/07/19/5d313ab870c5834835.jpg) 完整源码: [https://github.com/crossoverJie/cim](https://github.com/crossoverJie/cim) -如果这篇对你有所帮助还请不吝转发。 \ No newline at end of file +如果这篇对你有所帮助还请不吝转发。 diff --git a/docs/soft-skills/Interview-experience.md b/docs/soft-skills/Interview-experience.md index d5094691..926e4e85 100644 --- a/docs/soft-skills/Interview-experience.md +++ b/docs/soft-skills/Interview-experience.md @@ -1,4 +1,4 @@ -![](https://ws2.sinaimg.cn/large/006tNc79ly1fshrh2oexpj31kw0wkgsx.jpg) +![](https://i.loli.net/2019/07/19/5d313e9cab8b192420.jpg) ## 前言 @@ -334,7 +334,7 @@ long 类型的赋值是否是原子的? 看到这里的朋友应该都是老铁了,我也把上文提到的大多数面试题整理在了 GitHub: -![](https://ws1.sinaimg.cn/large/006tNc79gy1fsi40z9dulj30sl0p00yg.jpg) +![](https://i.loli.net/2019/07/19/5d313e9f5616854253.jpg) 厂库地址: @@ -356,4 +356,4 @@ long 类型的赋值是否是原子的? 我就是个例子,虽然最后没能去成阿里,现在在公司也是一个部门的技术负责人,在我们城市还有个窝,温馨的家,和女朋友一起为想要的生活努力奋斗。 -> 欢迎关注作者公众号于我交流🤗。 \ No newline at end of file +> 欢迎关注作者公众号于我交流🤗。 diff --git a/docs/soft-skills/TCP-IP.md b/docs/soft-skills/TCP-IP.md index 3d5f37ef..d37c58a6 100644 --- a/docs/soft-skills/TCP-IP.md +++ b/docs/soft-skills/TCP-IP.md @@ -6,7 +6,7 @@ - 滑动窗口。 ## 三次握手 -![](https://ws4.sinaimg.cn/large/006tNc79gy1fms9a563c3j30o309ogmc.jpg) +![](https://i.loli.net/2019/07/19/5d313e983e24378832.jpg) 如图类似: 1. 发送者问接收者我发消息了,你收到了嘛? @@ -21,4 +21,4 @@ 并且如果一次性发了三个包,只要最后一个包确认收到之后就默认前面两个也收到了。 ## 滑动窗口 -假设一次性发送包的大小为3,那么每次可以发3个包,而且可以边发边接收,这样就会增强效率。这里的 3 就是滑动窗口的大小,这样的发送方式也叫滑动窗口协议。 \ No newline at end of file +假设一次性发送包的大小为3,那么每次可以发3个包,而且可以边发边接收,这样就会增强效率。这里的 3 就是滑动窗口的大小,这样的发送方式也叫滑动窗口协议。 diff --git a/docs/soft-skills/how-to-be-developer.md b/docs/soft-skills/how-to-be-developer.md index 4bf400f8..7925ef12 100644 --- a/docs/soft-skills/how-to-be-developer.md +++ b/docs/soft-skills/how-to-be-developer.md @@ -1,4 +1,4 @@ -![](https://ws4.sinaimg.cn/large/0069RVTdgy1fu1lwclu7hj31kw11vqf0.jpg) +![](https://i.loli.net/2019/07/19/5d313ea44cb8b81194.jpg) ## 前言 @@ -326,7 +326,7 @@ Java 基础则是走向 Java 高级的必经之路。 ## 思维导图 -![](https://ws2.sinaimg.cn/large/0069RVTdgy1fu71j8bb1tj31kw1w1qlc.jpg) +![](https://i.loli.net/2019/07/19/5d313eafdee9c64439.jpg) 结合上文产出了一个思维导图更直观些。 @@ -340,6 +340,6 @@ Java 基础则是走向 Java 高级的必经之路。 上文大部分的知识点都有维护在 GitHub 上,感兴趣的朋友可以自行查阅: -![](https://ws1.sinaimg.cn/large/0069RVTdgy1fuc1ejsp0fj31kw1hx4qp.jpg) +![](https://i.loli.net/2019/07/19/5d313eb45ba5b49307.jpg) -[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) \ No newline at end of file +[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) diff --git a/docs/soft-skills/how-to-use-git-efficiently.md b/docs/soft-skills/how-to-use-git-efficiently.md index 188118b6..7a2ceaf0 100755 --- a/docs/soft-skills/how-to-use-git-efficiently.md +++ b/docs/soft-skills/how-to-use-git-efficiently.md @@ -1,6 +1,6 @@ **[原文链接](https://medium.freecodecamp.org/how-to-use-git-efficiently-54320a236369)** -![](https://ws1.sinaimg.cn/large/0069RVTdly1fuz415uvavj318g0tmh0f.jpg) +![](https://i.loli.net/2019/07/19/5d313e9893e8f53523.jpg) > 代码昨天还是运行好好的今天就不行了。 @@ -22,7 +22,7 @@ 这里我将介绍一种工作流,它在一个多人大型项目中将非常有用。 -![](https://ws1.sinaimg.cn/large/0069RVTdly1fuz4imimuuj313111zq6q.jpg) +![](https://i.loli.net/2019/07/19/5d313e9b120b999387.jpg) # 前言 @@ -70,11 +70,11 @@ Alice 能够按照如下 GitHub 方式提交 `pull request`。 -![](https://ws1.sinaimg.cn/large/0069RVTdgy1fv03386jcoj30ig05swet.jpg) +![](https://i.loli.net/2019/07/19/5d313e9c5e34a14226.jpg) 在分支名字的旁边有一个 “New pull request” 按钮,点击之后将会显示如下界面: -![](https://ws4.sinaimg.cn/large/0069RVTdgy1fv03etb1afj30no078gmn.jpg) +![](https://i.loli.net/2019/07/19/5d313e9ed71a587054.jpg) - 比较分支是 Alice 的功能分支 `feature/login`。 - base 分支则应该是发布分支 `release/fb`。 @@ -119,9 +119,9 @@ Alice 能够按照如下 GitHub 方式提交 `pull request`。 甚至尝试和作者交流,经过沟通原作者也会在原文中贴上我的翻译链接。大家互惠互利使好的文章转播的更广。 -![](https://ws3.sinaimg.cn/large/0069RVTdgy1fv0bxa6p94j30uq0rgjvc.jpg) +![](https://i.loli.net/2019/07/19/5d313ea5e16f824179.jpg) -![](https://ws3.sinaimg.cn/large/0069RVTdgy1fv0bxs6fp9j30us0qydkt.jpg) +![](https://i.loli.net/2019/07/19/5d313ea9407c511988.jpg) -**你的点赞与转发是最大的支持。** \ No newline at end of file +**你的点赞与转发是最大的支持。** diff --git a/docs/thread/ArrayBlockingQueue.md b/docs/thread/ArrayBlockingQueue.md new file mode 100644 index 00000000..3fd0f55a --- /dev/null +++ b/docs/thread/ArrayBlockingQueue.md @@ -0,0 +1,283 @@ +![](https://i.loli.net/2019/07/19/5d313f289d57811656.jpg) + + + +# 前言 + +较长一段时间以来我都发现不少开发者对 jdk 中的 `J.U.C`(java.util.concurrent)也就是 Java 并发包的使用甚少,更别谈对它的理解了;但这却也是我们进阶的必备关卡。 + +之前或多或少也分享过相关内容,但都不成体系;于是便想整理一套与并发包相关的系列文章。 + +其中的内容主要包含以下几个部分: + +- 根据定义自己实现一个并发工具。 +- JDK 的标准实现。 +- 实践案例。 + + +基于这三点我相信大家对这部分内容不至于一问三不知。 + +既然开了一个新坑,就不想做的太差;所以我打算将这个列表下的大部分类都讲到。 + +![](https://i.loli.net/2019/07/19/5d313f2c7f91450086.jpg) + + +所以本次重点讨论 `ArrayBlockingQueue`。 + +# 自己实现 + +在自己实现之前先搞清楚阻塞队列的几个特点: + +- 基本队列特性:先进先出。 +- 写入队列空间不可用时会阻塞。 +- 获取队列数据时当队列为空时将阻塞。 + + +实现队列的方式多种,总的来说就是数组和链表;其实我们只需要搞清楚其中一个即可,不同的特性主要表现为数组和链表的区别。 + +这里的 `ArrayBlockingQueue` 看名字很明显是由数组实现。 + +我们先根据它这三个特性尝试自己实现试试。 + +## 初始化队列 + +我这里自定义了一个类:`ArrayQueue`,它的构造函数如下: + +```java + public ArrayQueue(int size) { + items = new Object[size]; + } +``` + +很明显这里的 `items` 就是存放数据的数组;在初始化时需要根据大小创建数组。 + +![](https://i.loli.net/2019/07/19/5d313f2fb8fe622692.jpg) + +## 写入队列 + +写入队列比较简单,只需要依次把数据存放到这个数组中即可,如下图: + +![](https://i.loli.net/2019/07/19/5d313f32aa77680089.jpg) + +但还是有几个需要注意的点: + +- 队列满的时候,写入的线程需要被阻塞。 +- 写入过队列的数量大于队列大小时需要从第一个下标开始写。 + +先看第一个`队列满的时候,写入的线程需要被阻塞`,先来考虑下如何才能使一个线程被**阻塞**,看起来的表象线程卡住啥事也做不了。 + +有几种方案可以实现这个效果: + +- `Thread.sleep(timeout)`线程休眠。 +- `object.wait()` 让线程进入 `waiting` 状态。 + +> 当然还有一些 `join、LockSupport.part` 等不在本次的讨论范围。 + +阻塞队列还有一个非常重要的特性是:当队列空间可用时(取出队列),写入线程需要被唤醒让数据可以写入进去。 + +所以很明显`Thread.sleep(timeout)`不合适,它在到达超时时间之后便会继续运行;达不到**空间可用时**才唤醒继续运行这个特点。 + +其实这样的一个特点很容易让我们想到 Java 的等待通知机制来实现线程间通信;更多线程见通信的方案可以参考这里:[深入理解线程通信](https://crossoverjie.top/2018/03/16/java-senior/thread-communication/#%E7%AD%89%E5%BE%85%E9%80%9A%E7%9F%A5%E6%9C%BA%E5%88%B6) + +所以我这里的做法是,一旦队列满时就将写入线程调用 `object.wait()` 进入 `waiting` 状态,直到空间可用时再进行唤醒。 + +```java + /** + * 队列满时的阻塞锁 + */ + private Object full = new Object(); + + /** + * 队列空时的阻塞锁 + */ + private Object empty = new Object(); +``` + +![](https://i.loli.net/2019/07/19/5d313f35038c649523.jpg) + +所以这里声明了两个对象用于队列满、空情况下的互相通知作用。 + + +在写入数据成功后需要使用 `empty.notify()`,这样的目的是当获取队列为空时,一旦写入数据成功就可以把消费队列的线程唤醒。 + + +> 这里的 wait 和 notify 操作都需要对各自的对象使用 `synchronized` 方法块,这是因为 wait 和 notify 都需要获取到各自的锁。 + +## 消费队列 + +上文也提到了:当队列为空时,获取队列的线程需要被阻塞,直到队列中有数据时才被唤醒。 + +![](https://i.loli.net/2019/07/19/5d313f3ad811825796.jpg) + +代码和写入的非常类似,也很好理解;只是这里的等待、唤醒恰好是相反的,通过下面这张图可以很好理解: + +![](https://i.loli.net/2019/07/19/5d313f3d9cf3f67442.jpg) + +总的来说就是: + +- 写入队列满时会阻塞直到获取线程消费了队列数据后唤醒**写入线程**。 +- 消费队列空时会阻塞直到写入线程写入了队列数据后唤醒**消费线程**。 + + +## 测试 + +先来一个基本的测试:单线程的写入和消费。 + +![](https://i.loli.net/2019/07/19/5d313f405d0e291936.jpg) + +```log +3 +123 +1234 +12345 +``` + +通过结果来看没什么问题。 + +--- + +当写入的数据超过队列的大小时,就只能消费之后才能接着写入。 + +![](https://i.loli.net/2019/07/19/5d313f41cf91223286.jpg) + +```log +2019-04-09 16:24:41.040 [Thread-0] INFO c.c.concurrent.ArrayQueueTest - [Thread-0]123 +2019-04-09 16:24:41.040 [main] INFO c.c.concurrent.ArrayQueueTest - size=3 +2019-04-09 16:24:41.047 [main] INFO c.c.concurrent.ArrayQueueTest - 1234 +2019-04-09 16:24:41.048 [main] INFO c.c.concurrent.ArrayQueueTest - 12345 +2019-04-09 16:24:41.048 [main] INFO c.c.concurrent.ArrayQueueTest - 123456 +``` + +从运行结果也能看出只有当消费数据后才能接着往队列里写入数据。 + +--- + +![](https://i.loli.net/2019/07/19/5d313f4346e6458625.jpg) + +![](https://i.loli.net/2019/07/19/5d313f49e902d49687.jpg) + +而当没有消费时,再往队列里写数据则会导致写入线程被阻塞。 + + + +### 并发测试 + +![](https://i.loli.net/2019/07/19/5d313f4d00e9696823.jpg) + +三个线程并发写入300条数据,其中一个线程消费一条。 + +```log +=====0 +299 +``` + +最终的队列大小为 299,可见线程也是安全的。 + +> 由于不管是写入还是获取方法里的操作都需要获取锁才能操作,所以整个队列是线程安全的。 + + +# ArrayBlockingQueue + +下面来看看 JDK 标准的 `ArrayBlockingQueue` 的实现,有了上面的基础会更好理解。 + +## 初始化队列 + +![](https://i.loli.net/2019/07/19/5d313f5007ecc42909.jpg) + +看似要复杂些,但其实逐步拆分后也很好理解: + +第一步其实和我们自己写的一样,初始化一个队列大小的数组。 + + +第二步初始化了一个重入锁,这里其实就和我们之前使用的 `synchronized` 作用一致的; + +只是这里在初始化重入锁的时候默认是`非公平锁`,当然也可以指定为 `true` 使用公平锁;这样就会按照队列的顺序进行写入和消费。 + +> 更多关于 `ReentrantLock` 的使用和原理请参考这里:[ReentrantLock 实现原理](https://crossoverjie.top/2018/01/25/ReentrantLock/) + +三四两步则是创建了 `notEmpty notFull` 这两个条件,他的作用于用法和之前使用的 `object.wait/notify` 类似。 + +这就是整个初始化的内容,其实和我们自己实现的非常类似。 + + +## 写入队列 + +![](https://i.loli.net/2019/07/19/5d313f52b585671592.jpg) +![](https://i.loli.net/2019/07/19/5d313f5a1c28c84172.jpg) + +其实会发现阻塞写入的原理都是差不多的,只是这里使用的是 Lock 来显式获取和释放锁。 + +同时其中的 `notFull.await();notEmpty.signal();` 和我们之前使用的 `object.wait/notify` 的用法和作用也是一样的。 + + +当然它还是实现了超时阻塞的 `API`。 + +![](https://i.loli.net/2019/07/19/5d313f5b7a55b36447.jpg) + +也是比较简单,使用了一个具有超时时间的等待方法。 + +## 消费队列 + +再看消费队列: + +![](https://i.loli.net/2019/07/19/5d313f5e98db976041.jpg) +![](https://i.loli.net/2019/07/19/5d313f5fea44784743.jpg) + +也是差不多的,一看就懂。 + +而其中的超时 API 也是使用了 `notEmpty.awaitNanos(nanos)` 来实现超时返回的,就不具体说了。 + + +# 实际案例 + +说了这么多,来看一个队列的实际案例吧。 + +背景是这样的: + +> 有一个定时任务会按照一定的间隔时间从数据库中读取一批数据,需要对这些数据做校验同时调用一个远程接口。 + + +简单的做法就是由这个定时任务的线程去完成读取数据、消息校验、调用接口等整个全流程;但这样会有一个问题: + +假设调用外部接口出现了异常、网络不稳导致耗时增加就会造成整个任务的效率降低,因为他都是串行会互相影响。 + + +所以我们改进了方案: + +![](https://i.loli.net/2019/07/19/5d313f61e33f644196.jpg) + +其实就是一个典型的生产者消费者模型: + +- 生产线程从数据库中读取消息丢到队列里。 +- 消费线程从队列里获取数据做业务逻辑。 + +这样两个线程就可以通过这个队列来进行解耦,互相不影响,同时这个队列也能起到缓冲的作用。 + +但在使用过程中也有一些小细节值得注意。 + +因为这个外部接口是支持批量执行的,所以在消费线程取出数据后会在内存中做一个累加,一旦达到阈值或者是累计了一个时间段便将这批累计的数据处理掉。 + +但由于开发者的大意,在消费的时候使用的是 `queue.take()` 这个阻塞的 API;正常运行没啥问题。 + +可一旦原始的数据源,也就是 DB 中没数据了,导致队列里的数据也被消费完后这个消费线程便会被阻塞。 + +这样上一轮积累在内存中的数据便一直没机会使用,直到数据源又有数据了,一旦中间间隔较长时便可能会导致严重的业务异常。 + +所以我们最好是使用 `queue.poll(timeout)` 这样带超时时间的 api,除非业务上有明确的要求需要阻塞。 + +这个习惯同样适用于其他场景,比如调用 http、rpc 接口等都需要设置合理的超时时间。 + +# 总结 + +关于 `ArrayBlockingQueue` 的相关分享便到此结束,接着会继续更新其他并发容器及并发工具。 + +对本文有任何相关问题都可以留言讨论。 + + + +本文涉及到的所有源码: + +https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/concurrent/ArrayQueue.java + + +**你的点赞与分享是对我最大的支持** diff --git a/docs/thread/ConcurrentHashMap.md b/docs/thread/ConcurrentHashMap.md index 1e3220a4..adc905ad 100644 --- a/docs/thread/ConcurrentHashMap.md +++ b/docs/thread/ConcurrentHashMap.md @@ -9,7 +9,7 @@ ## JDK1.7 实现 ### 数据结构 -![](https://ws2.sinaimg.cn/large/006tNc79ly1fn2f5pgxinj30dw0730t7.jpg) +![](https://i.loli.net/2019/07/19/5d313f7215c4240040.jpg) 如图所示,是由 `Segment` 数组、`HashEntry` 数组组成,和 `HashMap` 一样,仍然是数组加链表组成。 @@ -58,13 +58,13 @@ ## JDK1.8 实现 -![](https://ws3.sinaimg.cn/large/006tNc79gy1fthpv4odbsj30lp0drmxr.jpg) +![](https://i.loli.net/2019/07/19/5d313f751f85f13539.jpg) 1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。 其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性。 -![](https://ws3.sinaimg.cn/large/006tNc79gy1fthq78e5gqj30nr09mmz9.jpg) +![](https://i.loli.net/2019/07/19/5d313f76c30a232619.jpg) 也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。 @@ -74,7 +74,7 @@ 重点来看看 put 函数: -![](https://ws3.sinaimg.cn/large/006tNc79gy1fthrz8jlo8j30oc0rbte3.jpg) +![](https://i.loli.net/2019/07/19/5d313f78c31dc41505.jpg) - 根据 key 计算出 hashcode 。 - 判断是否需要进行初始化。 @@ -85,7 +85,7 @@ ### get 方法 -![](https://ws1.sinaimg.cn/large/006tNc79gy1fthsnp2f35j30o409hwg7.jpg) +![](https://i.loli.net/2019/07/19/5d313f7aab95b41715.jpg) - 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 - 如果是红黑树那就按照树的方式获取值。 diff --git a/docs/thread/Synchronize.md b/docs/thread/Synchronize.md index cf00e90c..afbe5900 100644 --- a/docs/thread/Synchronize.md +++ b/docs/thread/Synchronize.md @@ -18,7 +18,7 @@ 流程图如下: -![](https://ws2.sinaimg.cn/large/006tNc79ly1fn27fkl07jj31e80hyn0n.jpg) +![](https://i.loli.net/2019/07/19/5d313f638492c49210.jpg) 通过一段代码来演示: diff --git a/docs/thread/ThreadPoolExecutor.md b/docs/thread/ThreadPoolExecutor.md index 17120a7f..54fe3a30 100644 --- a/docs/thread/ThreadPoolExecutor.md +++ b/docs/thread/ThreadPoolExecutor.md @@ -1,10 +1,10 @@ -![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftpwh3a2szj31kw11xh84.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2a867bea.jpg) ## 前言 平时接触过多线程开发的童鞋应该都或多或少了解过线程池,之前发布的《阿里巴巴 Java 手册》里也有一条: -![](https://ws2.sinaimg.cn/large/006tKfTcgy1ftpxf3x1epj30la03s0tl.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2a8dad64.jpg) 可见线程池的重要性。 @@ -26,6 +26,7 @@ - `Executors.newFixedThreadPool(nThreads)`:创建固定大小的线程池。 - `Executors.newSingleThreadExecutor()`:创建单个线程的线程池。 + 其实看这三种方式创建的源码就会发现: @@ -67,7 +68,7 @@ threadPool.execute(new Job()); 在具体分析之前先了解下线程池中所定义的状态,这些状态都和线程的执行密切相关: -![](https://ws3.sinaimg.cn/large/006tKfTcgy1ftq1ks5qywj30jn03i3za.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2a9bc566.jpg) - `RUNNING` 自然是运行状态,指可以接受任务执行队列里的任务 - `SHUTDOWN` 指调用了 `shutdown()` 方法,不再接受新任务了,但是队列里的任务得执行完毕。 @@ -77,11 +78,11 @@ threadPool.execute(new Job()); 用图表示为: -![](https://ws4.sinaimg.cn/large/006tKfTcgy1ftq2nxlwe5j30sp0ba0ts.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2aa81655.jpg) 然后看看 `execute()` 方法是如何处理的: -![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftq283zi91j30ky08mwgb.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2ab921db.jpg) 1. 获取当前线程池的状态。 2. 当前线程数量小于 coreSize 时创建一个新的线程运行。 @@ -92,7 +93,7 @@ threadPool.execute(new Job()); 这里借助《聊聊并发》的一张图来描述这个流程: -![](https://ws4.sinaimg.cn/large/006tKfTcgy1ftq2vzuv5rj30dw085q3i.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2ac0936c.jpg) ### 如何配置线程 @@ -206,16 +207,16 @@ public class TreadPoolConfig { 其实 ThreadPool 本身已经提供了不少 api 可以获取线程状态: -![](https://ws1.sinaimg.cn/large/006tKfTcgy1ftq3xsrbs6j30bg0bpgnb.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2accbbcf.jpg) 很多方法看名字就知道其含义,只需要将这些信息暴露到 SpringBoot 的监控端点中,我们就可以在可视化页面查看当前的线程池状态了。 甚至我们可以继承线程池扩展其中的几个函数来自定义监控逻辑: -![](https://ws2.sinaimg.cn/large/006tKfTcgy1ftq40lkw9jj30mq07rmyt.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2add4d31.jpg) -![](https://ws4.sinaimg.cn/large/006tKfTcgy1ftq41asf8rj30kq07cabd.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2aeea439.jpg) 看这些名称和定义都知道,这是让子类来实现的。 @@ -383,7 +384,7 @@ public class CommandUser extends HystrixCommand { 运行结果: -![](https://ws2.sinaimg.cn/large/006tKfTcgy1ftq4e0ukubj30ps04gtak.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2b06ef2d.jpg) 可以看到两个任务分成了两个线程池运行,他们之间互不干扰。 @@ -397,7 +398,7 @@ public class CommandUser extends HystrixCommand { 通过刚才的构造函数也能证明: -![](https://ws2.sinaimg.cn/large/006tKfTcgy1ftq4i6xy2qj30uo09adhp.jpg) +![](https://i.loli.net/2019/05/08/5cd1d2b69cd32.jpg) 还要注意的一点是: @@ -409,4 +410,4 @@ public class CommandUser extends HystrixCommand { 文末的 hystrix 源码: -[https://github.com/crossoverJie/Java-Interview/tree/master/src/main/java/com/crossoverjie/hystrix](https://github.com/crossoverJie/Java-Interview/tree/master/src/main/java/com/crossoverjie/hystrix) +[https://github.com/crossoverJie/Java-Interview/tree/master/src/main/java/com/crossoverjie/hystrix](https://github.com/crossoverJie/Java-Interview/tree/master/src/main/java/com/crossoverjie/hystrix) \ No newline at end of file diff --git a/docs/thread/Threadcore.md b/docs/thread/Threadcore.md index 65c394d2..5411303f 100644 --- a/docs/thread/Threadcore.md +++ b/docs/thread/Threadcore.md @@ -48,7 +48,7 @@ public final boolean compareAndSet(long expect, long update) { 现代计算机中,由于 `CPU` 直接从主内存中读取数据的效率不高,所以都会对应的 `CPU` 高速缓存,先将主内存中的数据读取到缓存中,线程修改数据之后首先更新到缓存,之后才会更新到主内存。如果此时还没有将数据更新到主内存其他的线程此时来读取就是修改之前的数据。 -![](https://ws2.sinaimg.cn/large/006tKfTcly1fmouu3fpokj31ae0osjt1.jpg) +![](https://i.loli.net/2019/07/19/5d313f69701ef45566.jpg) 如上图所示。 diff --git a/docs/thread/thread-gone.md b/docs/thread/thread-gone.md new file mode 100755 index 00000000..5e4df2ff --- /dev/null +++ b/docs/thread/thread-gone.md @@ -0,0 +1,189 @@ + +# 一个线程罢工的诡异事件 + + +![](https://i.loli.net/2019/07/19/5d313f4a3a31f18582.jpg) + +# 背景 + +事情(事故)是这样的,突然收到报警,线上某个应用里业务逻辑没有执行,导致的结果是数据库里的某些数据没有更新。 + +虽然是前人写的代码,但作为 `Bug maker&killer` 只能咬着牙上了。 + + + +因为之前没有接触过出问题这块的逻辑,所以简单理了下如图: + +![](https://i.loli.net/2019/07/19/5d313f4c9d69456679.jpg) + +1. 有一个生产线程一直源源不断的往队列写数据。 +2. 消费线程也一直不停的取出数据后写入后续的业务线程池。 +3. 业务线程池里的线程会对每个任务进行入库操作。 + +整个过程还是比较清晰的,就是一个典型的生产者消费者模型。 + +# 尝试定位 + +接下来便是尝试定位这个问题,首先例行检查了以下几项: +- 是否内存有内存溢出? +- 应用 GC 是否有异常? + +通过日志以及监控发现以上两项都是正常的。 + +紧接着便 dump 了线程快照查看业务线程池中的线程都在干啥。 + +![](https://i.loli.net/2019/07/19/5d313f4f2a5fc61091.jpg) + +结果发现所有业务线程池都处于 `waiting` 状态,队列也是空的。 + + +同时生产者使用的队列却已经满了,没有任何消费迹象。 + +结合上面的流程图不难发现应该是消费队列的 `Consumer` 出问题了,导致上游的队列不能消费,下有的业务线程池没事可做。 + +## review 代码 + +于是查看了消费代码的业务逻辑,同时也发现消费线程是一个**单线程**。 + +![](https://i.loli.net/2019/07/19/5d313f5162ec253903.jpg) + +结合之前的线程快照,我发现这个消费线程也是处于 waiting 状态,和后面的业务线程池一模一样。 + +他做的事情基本上就是对消息解析,之后丢到后面的业务线程池中,没有发现什么特别的地方。 + +> 但是由于里面的分支特别多(switch case),看着有点头疼;所以我与写这个业务代码的同学沟通后他告诉我确实也只是入口处解析了一下数据,后续所有的业务逻辑都是丢到线程池中处理的,于是我便带着这个前提去排查了(埋下了伏笔)。 + +因为这里消费的队列其实是一个 `disruptor` 队列;它和我们常用的 `BlockQueue` 不太一样,不是由开发者自定义一个消费逻辑进行处理的;而是在初始化队列时直接丢一个线程池进去,它会在内部使用这个线程池进行消费,同时回调一个方法,在这个方法里我们写自己的消费逻辑。 + + +所以对于开发者而言,这个消费逻辑其实是一个黑盒。 + +于是在我反复 `review` 了消费代码中的数据解析逻辑发现不太可能出现问题后,便开始疯狂怀疑是不是 `disruptor` 自身的问题导致这个消费线程罢工了。 + +再翻了一阵 `disruptor` 的源码后依旧没发现什么问题后我咨询对 `disruptor` 较熟的@咖啡拿铁,在他的帮助下在本地模拟出来和生产一样的情况。 + +# 本地模拟 + +![](https://i.loli.net/2019/07/19/5d313f52c634323563.jpg) +![](https://i.loli.net/2019/07/19/5d313f5420dc952988.jpg) + +本地也是创建了一个单线程的线程池,分别执行了两个任务。 + +- 第一个任务没啥好说的,就是简单的打印。 +- 第二个任务会对一个数进行累加,加到 10 之后就抛出一个未捕获的异常。 + +接着我们来运行一下。 + +![](https://i.loli.net/2019/07/19/5d313f5a2c02c31627.jpg) +![](https://i.loli.net/2019/07/19/5d313f5d8ffa965140.jpg) + +发现当任务中抛出一个没有捕获的异常时,线程池中的线程就会处于 `waiting` 状态,同时所有的堆栈都和生产相符。 + +> 细心的朋友会发现正常运行的线程名称和异常后处于 waiting 状态的线程名称是不一样的,这个后续分析。 + +## 解决问题 + +![](https://i.loli.net/2019/07/19/5d313f5ec672d88094.jpg) + +当加入异常捕获后又如何呢? + +![](https://i.loli.net/2019/07/19/5d313f6231de819950.jpg) + +程序肯定会正常运行。 + +> 同时会发现所有的任务都是由一个线程完成的。 + +虽说就是加了一行代码,但我们还是要搞清楚这里面的门门道道。 + +# 源码分析 + +于是只有直接 `debug` 线程池的源码最快了; + +--- + +![](https://i.loli.net/2019/07/19/5d313f6973b8619302.jpg) + +![](https://i.loli.net/2019/07/19/5d313f6f57e9d51378.jpg) + +通过刚才的异常堆栈我们进入到 `ThreadPoolExecutor.java:1142` 处。 + +- 发现线程池已经帮我们做了异常捕获,但依然会往上抛。 +- 在 `finally` 块中会执行 `processWorkerExit(w, completedAbruptly)` 方法。 + + +![](https://i.loli.net/2019/07/19/5d313f759363b25554.jpg) + +看过之前[《如何优雅的使用和理解线程池》](https://crossoverjie.top/2018/07/29/java-senior/ThreadPool/)的朋友应该还会有印象。 + +线程池中的任务都会被包装为一个内部 `Worker` 对象执行。 + +`processWorkerExit` 可以简单的理解为是把当前运行的线程销毁(`workers.remove(w)`)、同时新增(`addWorker()`)一个 `Worker` 对象接着处理; + +> 就像是哪个零件坏掉后重新换了一个新的接着工作,但是旧零件负责的任务就没有了。 + + +接下来看看 `addWorker()` 做了什么事情: + +![](https://i.loli.net/2019/07/19/5d313f77c421b49964.jpg) + +只看这次比较关心的部分;添加成功后会直接执行他的 `start()` 的方法。 + + +![](https://i.loli.net/2019/07/19/5d313f7994c8b72107.jpg) + +由于 `Worker` 实现了 `Runnable` 接口,所以本质上就是调用了 `runWorker()` 方法。 + +--- + + + +在 `runWorker()` 其实就是上文 `ThreadPoolExecutor` 抛出异常时的那个方法。 + +![](https://i.loli.net/2019/07/19/5d313f7e6beff17180.jpg) +![](https://i.loli.net/2019/07/19/5d313f843771a14962.jpg) + +它会从队列里一直不停的获取待执行的任务,也就是 `getTask()`;在 `getTask` 也能看出它会一直从内置的队列取出任务。 + +而一旦队列是空的,它就会 `waiting` 在 `workQueue.take()`,也就是我们从堆栈中发现的 1067 行代码。 + + + +## 线程名字的变化 + +![](https://i.loli.net/2019/07/19/5d313f8734b2d13880.jpg) +![](https://i.loli.net/2019/07/19/5d313f8a0386d77948.jpg) +![](https://i.loli.net/2019/07/19/5d313f8ced57345869.jpg) + +上文还提到了异常后的线程名称发生了改变,其实在 `addWorker()` 方法中可以看到 `new Worker()`时就会重新命名线程的名称,默认就是把后缀的计数+1。 + +这样一切都能解释得通了,真相只有一个: + + +> 在单个线程的线程池中一但抛出了未被捕获的异常时,线程池会回收当前的线程并创建一个新的 `Worker`; +> 它也会一直不断的从队列里获取任务来执行,但由于这是一个消费线程,根本没有生产者往里边丢任务,所以它会一直 waiting 在从队列里获取任务处,所以也就造成了线上的队列没有消费,业务线程池没有执行的问题。 + +# 总结 + +所以之后线上的那个问题加上异常捕获之后也变得正常了,但我还是有点纳闷的是: + +> 既然后续所有的任务都是在线程池中执行的,也就是纯异步了,那即便是出现异常也不会抛到消费线程中啊。 + +这不是把我之前储备的知识点推翻了嘛?不信邪!之后我让运维给了加上异常捕获后的线上错误日志。 + +结果发现在上文提到的众多 `switch case` 中,最后一个竟然是直接操作的数据库,导致一个非空字段报错了🤬!! + +这事也给我个教训,还是得眼见为实啊。 + +虽然这个问题改动很小解决了,但复盘整个过程还是有许多需要改进的: + +1. 消费队列的线程名称竟然和业务线程的前缀一样,导致我光找它就花了许多时间,命名必须得调整。 +2. 开发规范,防御式编程大家需要养成习惯。 +3. 未知的技术栈需要谨慎,比如 `disruptor`,之前的团队应该只是看了个高性能的介绍就直接使用,并没有深究其原理;导致出现问题后对它拿不准。 + +实例代码: + +[https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java](https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java) + + +**你的点赞与分享是对我最大的支持** + diff --git a/docs/thread/thread-gone2.md b/docs/thread/thread-gone2.md new file mode 100755 index 00000000..291fe90b --- /dev/null +++ b/docs/thread/thread-gone2.md @@ -0,0 +1,133 @@ +# 线程池中你不容错过的一些细节 + +![](https://i.loli.net/2019/07/19/5d313f2ad38b490450.jpg) + +# 背景 + +上周分享了一篇[《一个线程罢工的诡异事件》](docs/jvm/thread-gone.md),最近也在公司内部分享了这个案例。 + +无独有偶,在内部分享的时候也有小伙伴问了之前分享时所提出的一类问题: + + + +![](https://i.loli.net/2019/07/19/5d313f2da903922875.jpg) + +![](https://i.loli.net/2019/07/19/5d313f2fb8ab281501.jpg) + +![](https://i.loli.net/2019/07/19/5d313f31ae8dd83926.jpg) + +![](https://i.loli.net/2019/07/19/5d313f349d9f989541.jpg) + +这其实是一类共性问题,我认为主要还是两个原因: + +- 我自己确实也没讲清楚,之前画的那张图还需要再完善,有些误导。 +- 第二还是大家对线程池的理解不够深刻,比如今天要探讨的内容。 + + +# 线程池的工作原理 + +首先还是来复习下线程池的基本原理。 + +我认为线程池它就是一个**调度任务**的工具。 + +众所周知在初始化线程池会给定线程池的大小,假设现在我们有 1000 个线程任务需要运行,而线程池的大小为 10~20,在真正运行任务的过程中他肯定不会创建这1000个线程同时运行,而是充分利用线程池里这 10~20 个线程来调度这1000个任务。 + +而这里的 10~20 个线程最后会由线程池封装为 `ThreadPoolExecutor.Worker` 对象,而这个 `Worker` 是实现了 Runnable 接口的,所以他自己本身就是一个线程。 + +# 深入分析 + +![](https://i.loli.net/2019/07/19/5d313f3a4276e41232.jpg) + +这里我们来做一个模拟,创建了一个核心线程、最大线程数、阻塞队列都为2的线程池。 + +这里假设线程池已经完成了预热,也就是线程池内部已经创建好了两个线程 `Worker`。 + +当我们往一个线程池丢一个任务会发生什么事呢? + +![](https://i.loli.net/2019/07/19/5d313f3cd67dd15513.jpg) + +- 第一步是生产者,也就是任务提供者他执行了一个 execute() 方法,本质上就是往这个内部队列里放了一个任务。 +- 之前已经创建好了的 Worker 线程会执行一个 `while` 循环 ---> 不停的从这个`内部队列`里获取任务。(这一步是竞争的关系,都会抢着从队列里获取任务,由这个队列内部实现了线程安全。) +- 获取得到一个任务后,其实也就是拿到了一个 `Runnable` 对象(也就是 `execute(Runnable task)` 这里所提交的任务),接着执行这个 `Runnable` 的 **run() 方法,而不是 start()**,这点需要注意后文分析原因。 + +结合源码来看: + +![](https://i.loli.net/2019/07/19/5d313f3e7871333125.jpg) + +从图中其实就对应了刚才提到的二三两步: + +- `while` 循环,从 `getTask()` 方法中一直不停的获取任务。 +- 拿到任务后,执行它的 run() 方法。 + +这样一个线程就调度完毕,然后再次进入循环从队列里取任务并不断的进行调度。 + +# 再次解释之前的问题 + +接下来回顾一下我们上一篇文章所提到的,导致一个线程没有运行的根本原因是: + +> 在单个线程的线程池中一但抛出了未被捕获的异常时,线程池会回收当前的线程并创建一个新的 `Worker`; +> 它也会一直不断的从队列里获取任务来执行,但由于这是一个消费线程,**根本没有生产者往里边丢任务**,所以它会一直 waiting 在从队列里获取任务处,所以也就造成了线上的队列没有消费,业务线程池没有执行的问题。 + +结合之前的那张图来看: + +![](https://i.loli.net/2019/07/19/5d313f41461af62841.jpg) + +这里大家问的最多的一个点是,为什么会没有是`根本没有生产者往里边丢任务`,图中不是明明画的有一个 `product` 嘛? + +这里确实是有些不太清楚,再次强调一次: + +**图中的 product 是往内部队列里写消息的生产者,并不是往这个 Consumer 所在的线程池中写任务的生产者。** + +因为即便 `Consumer` 是一个单线程的线程池,它依然具有一个常规线程池所具备的所有条件: + +- Worker 调度线程,也就是线程池运行的线程;虽然只有一个。 +- 内部的阻塞队列;虽然长度只有1。 + +再次结合图来看: + +![](https://i.loli.net/2019/07/19/5d313f43d9b0242820.jpg) + +所以之前提到的【没有生产者往里边丢任务】是指右图放大后的那一块,也就是内部队列并没有其他线程往里边丢任务执行 `execute()` 方法。 + +而一旦发生未捕获的异常后,`Worker1` 被回收,顺带的它所调度的线程 `task1`(这个task1 也就是在执行一个 while 循环消费左图中的那个队列) 也会被回收掉。 + +新创建的 `Worker2` 会取代 `Worker1` 继续执行 `while` 循环从内部队列里获取任务,但此时这个队列就一直会是空的,所以也就是处于 `Waiting` 状态。 + + +> 我觉得这波解释应该还是讲清楚了,欢迎还没搞明白的朋友留言讨论。 + +# 为什是 run() 而不是 start() + +问题搞清楚后来想想为什么线程池在调度的时候执行的是 `Runnable` 的 `run()` 方法,而不是 `start()` 方法呢? + +我相信大部分没有看过源码的同学心中第一个印象就应该是执行的 `start()` 方法; + +因为不管是学校老师,还是网上大牛讲的都是只有执行了` start()` 方法后操作系统才会给我们创建一个独立的线程来运行,而 `run()` 方法只是一个普通的方法调用。 + +而在线程池这个场景中却恰好就是要利用它**只是一个普通方法调用**。 + +回到我在文初中所提到的:我认为线程池它就是一个**调度任务**的工具。 + +假设这里是调用的 `Runnable` 的 `start` 方法,那会发生什么事情。 + +如果我们往一个核心、最大线程数为 2 的线程池里丢了 1000 个任务,**那么它会额外的创建 1000 个线程,同时每个任务都是异步执行的,一下子就执行完毕了**。 + +从而没法做到由这两个 `Worker` 线程来调度这 1000 个任务,而只有当做一个同步阻塞的 `run()` 方法调用时才能满足这个要求。 + +> 这事也让我发现一个奇特的现象:就是网上几乎没人讲过为什么在线程池里是 run 而不是 start,不知道是大家都觉得这是基操还是没人仔细考虑过。 + +# 总结 + +针对之前线上事故的总结上次已经写得差不多了,感兴趣的可以翻回去看看。 + +这次呢可能更多是我自己的总结,比如写一篇技术博客时如果大部分人对某一个知识点讨论的比较热烈时,那一定是作者要么讲错了,要么没讲清楚。 + +这点确实是要把自己作为一个读者的角度来看,不然很容易出现之前的一些误解。 + +在这之外呢,我觉得对于线程池把这两篇都看完同时也理解后对于大家理解线程池,利用线程池完成工作也是有很大好处的。 + +如果有在面试中加分的记得回来点赞、分享啊。 + + +**你的点赞与分享是对我最大的支持** + diff --git a/pom.xml b/pom.xml index 011a3125..53c95e0e 100644 --- a/pom.xml +++ b/pom.xml @@ -122,7 +122,7 @@ org.apache.zookeeper zookeeper - 3.4.6 + 3.4.14 slf4j-log4j12 @@ -131,6 +131,20 @@ + + org.openjdk.jmh + jmh-core + 1.9.3 + + + + org.openjdk.jmh + jmh-generator-annprocess + 1.9.3 + + + + diff --git a/src/main/java/com/crossoverjie/actual/ThreadCommunication.java b/src/main/java/com/crossoverjie/actual/ThreadCommunication.java index ecbda200..7b5cc89c 100644 --- a/src/main/java/com/crossoverjie/actual/ThreadCommunication.java +++ b/src/main/java/com/crossoverjie/actual/ThreadCommunication.java @@ -4,7 +4,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import java.io.PipedInputStream; import java.io.PipedReader; import java.io.PipedWriter; import java.util.concurrent.*; @@ -22,9 +21,9 @@ public class ThreadCommunication { public static void main(String[] args) throws Exception { //join(); //executorService(); - //countDownLatch(); + countDownLatch(); //piped(); - cyclicBarrier(); + //cyclicBarrier(); } /** @@ -82,22 +81,19 @@ public void run() { } private static void countDownLatch() throws Exception { - int thread = 3; + int thread = 2; long start = System.currentTimeMillis(); final CountDownLatch countDown = new CountDownLatch(thread); for (int i = 0; i < thread; i++) { - new Thread(new Runnable() { - @Override - public void run() { - LOGGER.info("thread run"); - try { - Thread.sleep(2000); - countDown.countDown(); + new Thread(() -> { + LOGGER.info("thread run"); + try { + Thread.sleep(2000); + countDown.countDown(); - LOGGER.info("thread end"); - } catch (InterruptedException e) { - e.printStackTrace(); - } + LOGGER.info("thread end"); + } catch (InterruptedException e) { + e.printStackTrace(); } }).start(); } diff --git a/src/main/java/com/crossoverjie/basic/CollectionsTest.java b/src/main/java/com/crossoverjie/basic/CollectionsTest.java new file mode 100644 index 00000000..6c37fa26 --- /dev/null +++ b/src/main/java/com/crossoverjie/basic/CollectionsTest.java @@ -0,0 +1,74 @@ +package com.crossoverjie.basic; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Function: + * + * @author crossoverJie + * Date: 2019-06-27 00:11 + * @since JDK 1.8 + */ +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +public class CollectionsTest { + + private static final int TEN_MILLION = 10000000; + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void arrayList() { + + List array = new ArrayList<>(); + + for (int i = 0; i < TEN_MILLION; i++) { + array.add("123"); + } + + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void arrayListSize() { + List array = new ArrayList<>(TEN_MILLION); + + for (int i = 0; i < TEN_MILLION; i++) { + array.add("123"); + } + + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void linkedList() { + List array = new LinkedList<>(); + + for (int i = 0; i < TEN_MILLION; i++) { + array.add("123"); + } + + } + + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include(CollectionsTest.class.getSimpleName()) + .forks(1) + .build(); + + + new Runner(opt).run(); + } +} diff --git a/src/main/java/com/crossoverjie/concurrent/ArrayQueue.java b/src/main/java/com/crossoverjie/concurrent/ArrayQueue.java new file mode 100644 index 00000000..a8dbe3d2 --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/ArrayQueue.java @@ -0,0 +1,129 @@ +package com.crossoverjie.concurrent; + +/** + * Function: 数组实现的线程安全阻塞队列 + * + * @author crossoverJie + * Date: 2019-04-04 15:02 + * @since JDK 1.8 + */ +public final class ArrayQueue { + + /** + * 队列数量 + */ + private int count = 0; + + /** + * 最终的数据存储 + */ + private Object[] items; + + /** + * 队列满时的阻塞锁 + */ + private Object full = new Object(); + + /** + * 队列空时的阻塞锁 + */ + private Object empty = new Object(); + + + /** + * 写入数据时的下标 + */ + private int putIndex; + + /** + * 获取数据时的下标 + */ + private int getIndex; + + public ArrayQueue(int size) { + items = new Object[size]; + } + + /** + * 从队列尾写入数据 + * @param t + */ + public void put(T t) { + + synchronized (full) { + while (count == items.length) { + try { + full.wait(); + } catch (InterruptedException e) { + break; + } + } + } + + synchronized (empty) { + //写入 + items[putIndex] = t; + count++; + + putIndex++; + if (putIndex == items.length) { + //超过数组长度后需要从头开始 + putIndex = 0; + } + + empty.notify(); + } + + } + + /** + * 从队列头获取数据 + * @return + */ + public T get() { + + synchronized (empty) { + while (count == 0) { + try { + empty.wait(); + } catch (InterruptedException e) { + return null; + } + } + } + + synchronized (full) { + Object result = items[getIndex]; + items[getIndex] = null; + count--; + + getIndex++; + if (getIndex == items.length) { + getIndex = 0; + } + + full.notify(); + + return (T) result; + } + } + + /** + * 获取队列大小 + * @return + */ + public synchronized int size() { + return count; + } + + + /** + * 判断队列是否为空 + * @return + */ + public boolean isEmpty() { + return size() == 0; + } + + +} diff --git a/src/main/java/com/crossoverjie/concurrent/CustomThreadPool.java b/src/main/java/com/crossoverjie/concurrent/CustomThreadPool.java new file mode 100644 index 00000000..a9713a93 --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/CustomThreadPool.java @@ -0,0 +1,403 @@ +package com.crossoverjie.concurrent; + +import com.crossoverjie.concurrent.communication.Notify; +import com.crossoverjie.concurrent.future.Callable; +import com.crossoverjie.concurrent.future.Future; +import com.crossoverjie.concurrent.future.FutureTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Function:线程池 + * + * @author crossoverJie + * Date: 2019-05-14 10:51 + * @since JDK 1.8 + */ +public class CustomThreadPool { + + private final static Logger LOGGER = LoggerFactory.getLogger(CustomThreadPool.class); + private final ReentrantLock lock = new ReentrantLock(); + + /** + * 最小线程数,也叫核心线程数 + */ + private volatile int miniSize; + + /** + * 最大线程数 + */ + private volatile int maxSize; + + /** + * 线程需要被回收的时间 + */ + private long keepAliveTime; + private TimeUnit unit; + + /** + * 存放线程的阻塞队列 + */ + private BlockingQueue workQueue; + + /** + * 存放线程池 + */ + private volatile Set workers; + + /** + * 是否关闭线程池标志 + */ + private AtomicBoolean isShutDown = new AtomicBoolean(false); + + /** + * 提交到线程池中的任务总数 + */ + private AtomicInteger totalTask = new AtomicInteger(); + + /** + * 线程池任务全部执行完毕后的通知组件 + */ + private Object shutDownNotify = new Object(); + + private Notify notify; + + /** + * @param miniSize 最小线程数 + * @param maxSize 最大线程数 + * @param keepAliveTime 线程保活时间 + * @param unit + * @param workQueue 阻塞队列 + * @param notify 通知接口 + */ + public CustomThreadPool(int miniSize, int maxSize, long keepAliveTime, + TimeUnit unit, BlockingQueue workQueue, Notify notify) { + this.miniSize = miniSize; + this.maxSize = maxSize; + this.keepAliveTime = keepAliveTime; + this.unit = unit; + this.workQueue = workQueue; + this.notify = notify; + + workers = new ConcurrentHashSet<>(); + } + + + /** + * 有返回值 + * + * @param callable + * @param + * @return + */ + public Future submit(Callable callable) { + FutureTask future = new FutureTask(callable); + execute(future); + return future; + } + + + /** + * 执行任务 + * + * @param runnable 需要执行的任务 + */ + public void execute(Runnable runnable) { + if (runnable == null) { + throw new NullPointerException("runnable nullPointerException"); + } + if (isShutDown.get()) { + LOGGER.info("线程池已经关闭,不能再提交任务!"); + return; + } + + //提交的线程 计数 + totalTask.incrementAndGet(); + + //小于最小线程数时新建线程 + if (workers.size() < miniSize) { + addWorker(runnable); + return; + } + + + boolean offer = workQueue.offer(runnable); + //写入队列失败 + if (!offer) { + + //创建新的线程执行 + if (workers.size() < maxSize) { + addWorker(runnable); + return; + } else { + LOGGER.error("超过最大线程数"); + try { + //会阻塞 + workQueue.put(runnable); + } catch (InterruptedException e) { + + } + } + + } + + + } + + /** + * 添加任务,需要加锁 + * + * @param runnable 任务 + */ + private void addWorker(Runnable runnable) { + Worker worker = new Worker(runnable, true); + worker.startTask(); + workers.add(worker); + } + + + /** + * 工作线程 + */ + private final class Worker extends Thread { + + private Runnable task; + + private Thread thread; + /** + * true --> 创建新的线程执行 + * false --> 从队列里获取线程执行 + */ + private boolean isNewTask; + + public Worker(Runnable task, boolean isNewTask) { + this.task = task; + this.isNewTask = isNewTask; + thread = this; + } + + public void startTask() { + thread.start(); + } + + public void close() { + thread.interrupt(); + } + + @Override + public void run() { + + Runnable task = null; + + if (isNewTask) { + task = this.task; + } + + boolean compile = true ; + + try { + while ((task != null || (task = getTask()) != null)) { + try { + //执行任务 + task.run(); + } catch (Exception e) { + compile = false ; + throw e ; + } finally { + //任务执行完毕 + task = null; + int number = totalTask.decrementAndGet(); + //LOGGER.info("number={}",number); + if (number == 0) { + synchronized (shutDownNotify) { + shutDownNotify.notify(); + } + } + } + } + + } finally { + //释放线程 + boolean remove = workers.remove(this); + //LOGGER.info("remove={},size={}", remove, workers.size()); + + if (!compile){ + addWorker(null); + } + tryClose(true); + } + } + } + + + /** + * 从队列中获取任务 + * + * @return + */ + private Runnable getTask() { + //关闭标识及任务是否全部完成 + if (isShutDown.get() && totalTask.get() == 0) { + return null; + } + //while (true) { + // + // if (workers.size() > miniSize) { + // boolean value = number.compareAndSet(number.get(), number.get() - 1); + // if (value) { + // return null; + // } else { + // continue; + // } + // } + + lock.lock(); + + try { + Runnable task = null; + if (workers.size() > miniSize) { + //大于核心线程数时需要用保活时间获取任务 + task = workQueue.poll(keepAliveTime, unit); + } else { + task = workQueue.take(); + } + + if (task != null) { + return task; + } + } catch (InterruptedException e) { + return null; + } finally { + lock.unlock(); + } + + return null; + //} + } + + /** + * 任务执行完毕后关闭线程池 + */ + public void shutdown() { + isShutDown.set(true); + tryClose(true); + //中断所有线程 + //synchronized (shutDownNotify){ + // while (totalTask.get() > 0){ + // try { + // shutDownNotify.wait(); + // } catch (InterruptedException e) { + // e.printStackTrace(); + // } + // } + //} + } + + /** + * 立即关闭线程池,会造成任务丢失 + */ + public void shutDownNow() { + isShutDown.set(true); + tryClose(false); + + } + + /** + * 阻塞等到任务执行完毕 + */ + public void mainNotify() { + synchronized (shutDownNotify) { + while (totalTask.get() > 0) { + try { + shutDownNotify.wait(); + if (notify != null) { + notify.notifyListen(); + } + } catch (InterruptedException e) { + return; + } + } + } + } + + /** + * 关闭线程池 + * + * @param isTry true 尝试关闭 --> 会等待所有任务执行完毕 + * false 立即关闭线程池--> 任务有丢失的可能 + */ + private void tryClose(boolean isTry) { + if (!isTry) { + closeAllTask(); + } else { + if (isShutDown.get() && totalTask.get() == 0) { + closeAllTask(); + } + } + + } + + /** + * 关闭所有任务 + */ + private void closeAllTask() { + for (Worker worker : workers) { + //LOGGER.info("开始关闭"); + worker.close(); + } + } + + /** + * 获取工作线程数量 + * + * @return + */ + public int getWorkerCount() { + return workers.size(); + } + + /** + * 内部存放工作线程容器,并发安全。 + * + * @param + */ + private final class ConcurrentHashSet extends AbstractSet { + + private ConcurrentHashMap map = new ConcurrentHashMap<>(); + private final Object PRESENT = new Object(); + + private AtomicInteger count = new AtomicInteger(); + + @Override + public Iterator iterator() { + return map.keySet().iterator(); + } + + @Override + public boolean add(T t) { + count.incrementAndGet(); + return map.put(t, PRESENT) == null; + } + + @Override + public boolean remove(Object o) { + count.decrementAndGet(); + return map.remove(o) == PRESENT; + } + + @Override + public int size() { + return count.get(); + } + } +} diff --git a/src/main/java/com/crossoverjie/concurrent/communication/MultipleThreadCountDownKit.java b/src/main/java/com/crossoverjie/concurrent/communication/MultipleThreadCountDownKit.java new file mode 100644 index 00000000..fe9d5f2e --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/communication/MultipleThreadCountDownKit.java @@ -0,0 +1,82 @@ +package com.crossoverjie.concurrent.communication; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Function: + * + * @author crossoverJie + * Date: 2019-04-17 19:35 + * @since JDK 1.8 + */ +public final class MultipleThreadCountDownKit { + + /** + * 计数器 + */ + private AtomicInteger counter; + + /** + * 通知对象 + */ + private Object notify ; + + private Notify notifyListen ; + + public MultipleThreadCountDownKit(int number){ + if (number < 0) { + throw new IllegalArgumentException("counter < 0"); + } + counter = new AtomicInteger(number) ; + notify = new Object() ; + } + + /** + * 设置回调接口 + * @param notify + */ + public void setNotify(Notify notify){ + notifyListen = notify ; + } + + + /** + * 线程完成后计数 -1 + */ + public void countDown(){ + + if (counter.get() <= 0){ + return; + } + + int count = this.counter.decrementAndGet(); + if (count < 0){ + throw new RuntimeException("concurrent error") ; + } + + if (count == 0){ + synchronized (notify){ + notify.notify(); + } + } + + } + + /** + * 等待所有的线程完成 + * @throws InterruptedException + */ + public void await() throws InterruptedException { + synchronized (notify){ + while (counter.get() > 0){ + notify.wait(); + } + + if (notifyListen != null){ + notifyListen.notifyListen(); + } + + } + } + +} diff --git a/src/main/java/com/crossoverjie/concurrent/communication/Notify.java b/src/main/java/com/crossoverjie/concurrent/communication/Notify.java new file mode 100644 index 00000000..ff7ab086 --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/communication/Notify.java @@ -0,0 +1,16 @@ +package com.crossoverjie.concurrent.communication; + +/** + * Function: + * + * @author crossoverJie + * Date: 2019-04-17 20:26 + * @since JDK 1.8 + */ +public interface Notify { + + /** + * 回调 + */ + void notifyListen() ; +} diff --git a/src/main/java/com/crossoverjie/concurrent/future/Callable.java b/src/main/java/com/crossoverjie/concurrent/future/Callable.java new file mode 100644 index 00000000..b28f8944 --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/future/Callable.java @@ -0,0 +1,17 @@ +package com.crossoverjie.concurrent.future; + +/** + * Function: + * + * @author crossoverJie + * Date: 2019-06-03 23:54 + * @since JDK 1.8 + */ +public interface Callable { + + /** + * 执行任务 + * @return 执行结果 + */ + T call() ; +} diff --git a/src/main/java/com/crossoverjie/concurrent/future/Future.java b/src/main/java/com/crossoverjie/concurrent/future/Future.java new file mode 100644 index 00000000..acfbf83d --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/future/Future.java @@ -0,0 +1,18 @@ +package com.crossoverjie.concurrent.future; + +/** + * Function: + * + * @author crossoverJie + * Date: 2019-06-03 23:55 + * @since JDK 1.8 + */ +public interface Future { + + /** + * 获取 + * @return 结果 + * @throws InterruptedException + */ + T get() throws InterruptedException; +} diff --git a/src/main/java/com/crossoverjie/concurrent/future/FutureTask.java b/src/main/java/com/crossoverjie/concurrent/future/FutureTask.java new file mode 100644 index 00000000..15a6684a --- /dev/null +++ b/src/main/java/com/crossoverjie/concurrent/future/FutureTask.java @@ -0,0 +1,46 @@ +package com.crossoverjie.concurrent.future; + +/** + * Function: + * + * @author crossoverJie + * Date: 2019-06-03 23:56 + * @since JDK 1.8 + */ +public class FutureTask implements Runnable,Future { + + private Callable callable ; + + private T result; + + private Object notify ; + + public FutureTask(Callable callable) { + this.callable = callable; + notify = new Object() ; + } + + @Override + public T get() throws InterruptedException { + + synchronized (notify){ + while (result == null){ + notify.wait(); + } + + return result; + } + } + + @Override + public void run() { + + T call = callable.call(); + + this.result = call ; + + synchronized (notify){ + notify.notify(); + } + } +} diff --git a/src/main/java/com/crossoverjie/spring/LifeCycleConfig.java b/src/main/java/com/crossoverjie/spring/LifeCycleConfig.java index fd44aa6d..707a6cd1 100644 --- a/src/main/java/com/crossoverjie/spring/LifeCycleConfig.java +++ b/src/main/java/com/crossoverjie/spring/LifeCycleConfig.java @@ -1,8 +1,6 @@ package com.crossoverjie.spring; -import com.crossoverjie.concurrent.Singleton; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; /** diff --git a/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java b/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java new file mode 100644 index 00000000..5deddd1f --- /dev/null +++ b/src/main/java/com/crossoverjie/thread/ThreadExceptionTest.java @@ -0,0 +1,85 @@ +package com.crossoverjie.thread; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Function:线程池异常测试 + * + * @author crossoverJie + * Date: 2019-03-07 20:35 + * @since JDK 1.8 + */ +public class ThreadExceptionTest { + + private final static Logger LOGGER = LoggerFactory.getLogger(ThreadExceptionTest.class); + + + public static void main(String[] args) throws InterruptedException { + + ExecutorService execute = new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); + + execute.execute(new Runnable() { + @Override + public void run() { + LOGGER.info("=====11======="); + } + }); + + TimeUnit.SECONDS.sleep(5); + + + execute.execute(new Run1()); + + //TimeUnit.SECONDS.sleep(5); + // + //execute.execute(new Run2()); + //execute.shutdown(); + + } + + + private static class Run1 implements Runnable { + + @Override + public void run() { + int count = 0; + while (true) { + count++; + LOGGER.info("-------222-------------{}", count); + + if (count == 10) { + System.out.println(1 / 0); + try { + } catch (Exception e) { + LOGGER.error("Exception",e); + } + } + + if (count == 20) { + LOGGER.info("count={}", count); + break; + } + } + } + } + + private static class Run2 implements Runnable { + + public Run2() { + LOGGER.info("run2 构造函数"); + } + + @Override + public void run() { + LOGGER.info("run222222222"); + } + } +} diff --git a/src/test/java/com/crossoverjie/algorithm/LinkedListMergeSortTest.java b/src/test/java/com/crossoverjie/algorithm/LinkedListMergeSortTest.java new file mode 100644 index 00000000..b553b35c --- /dev/null +++ b/src/test/java/com/crossoverjie/algorithm/LinkedListMergeSortTest.java @@ -0,0 +1,164 @@ +package com.crossoverjie.algorithm; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; + + +public class LinkedListMergeSortTest { + + @Rule public Timeout globalTimeout = new Timeout(10000); + + @Test + public void constructorOutputVoid() { + + // Act, creating object to test constructor + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + + // Method returns void, testing that no exception is thrown + } + + // Test generated by Diffblue Cover. + @Test + public void mergeListNotNull() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node left = new LinkedListMergeSort.Node(-2_147_483_647, null); + final LinkedListMergeSort.Node right = new LinkedListMergeSort.Node(0, null); + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeList(left, right); + + // Assert result + Assert.assertNotNull(retval); + Assert.assertEquals(0, retval.e); + Assert.assertNotNull(retval.next); + Assert.assertEquals(-2_147_483_647, retval.next.e); + Assert.assertNull(retval.next.next); + } + + // Test generated by Diffblue Cover. + @Test + public void mergeListInputNotNullNotNullOutputNotNull2() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node left = new LinkedListMergeSort.Node(1, null); + final LinkedListMergeSort.Node right = new LinkedListMergeSort.Node(-2_147_483_648, null); + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeList(left, right); + + + // Assert result + Assert.assertNotNull(retval); + Assert.assertEquals(1, retval.e); + Assert.assertNotNull(retval.next); + Assert.assertEquals(-2_147_483_648, retval.next.e); + Assert.assertNull(retval.next.next); + } + + + @Test + public void mergeListInputRightNull() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node left = new LinkedListMergeSort.Node(-2_147_483_647,null); + final LinkedListMergeSort.Node right = null; + + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeList(left, right); + + // Assert result + Assert.assertNotNull(retval); + Assert.assertEquals(-2_147_483_647, retval.e); + Assert.assertNull(retval.next); + } + + + @Test + public void mergeListInputLeftNull() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node left = null; + final LinkedListMergeSort.Node right = new LinkedListMergeSort.Node(0, null); + + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeList(left, right); + + // Assert result + Assert.assertNotNull(retval); + Assert.assertEquals(0, retval.e); + Assert.assertNull(retval.next); + } + + + @Test + public void mergeListInputNull() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node left = null; + final LinkedListMergeSort.Node right = null; + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeList(left, right); + + // Assert result + Assert.assertNull(retval); + } + + @Test + public void mergeSortLength2() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node node = new LinkedListMergeSort.Node(-0, null); + final LinkedListMergeSort.Node first = new LinkedListMergeSort.Node(-2_147_483_647, node); + + final int length = 2; + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeSort(first, length); + + // Assert result + Assert.assertNotNull(retval); + Assert.assertEquals(0, retval.e); + Assert.assertNotNull(retval.next); + Assert.assertEquals(-2_147_483_647, retval.next.e); + Assert.assertNull(retval.next.next); + } + + @Test + public void mergeSortInputNull() { + + // Arrange + final LinkedListMergeSort objectUnderTest = new LinkedListMergeSort(); + final LinkedListMergeSort.Node first = null; + final int length = 1; + + // Act + final LinkedListMergeSort.Node retval = objectUnderTest.mergeSort(first, length); + + // Assert result + Assert.assertNull(retval); + } + @Test + public void mainInput0OutputVoid() throws Exception { + + // Arrange + final String[] args = {}; + + // Act + LinkedListMergeSort.main(args); + + // Method returns void, testing that no exception is thrown + } + +} diff --git a/src/test/java/com/crossoverjie/concurrent/ArrayQueueTest.java b/src/test/java/com/crossoverjie/concurrent/ArrayQueueTest.java new file mode 100644 index 00000000..011b982f --- /dev/null +++ b/src/test/java/com/crossoverjie/concurrent/ArrayQueueTest.java @@ -0,0 +1,232 @@ +package com.crossoverjie.concurrent; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.TimeUnit; + +public class ArrayQueueTest { + + private final static Logger LOGGER = LoggerFactory.getLogger(ArrayQueueTest.class) ; + + @Test + public void test() throws InterruptedException { + ArrayBlockingQueue queue = new ArrayBlockingQueue(3); + + new Thread(() -> { + try { + TimeUnit.SECONDS.sleep(2); + System.out.println("thread[" + Thread.currentThread().getName() + "]" + queue.take()); + } catch (Exception e) { + } + }).start(); + + queue.put("123"); + queue.put("1234"); + queue.put("12345"); + queue.put("123456"); + queue.size(); + + + } + + @Test + public void put() { + ArrayQueue queue = new ArrayQueue<>(3); + queue.put("123"); + queue.put("1234"); + queue.put("12345"); + System.out.println(queue.size()); + + + while (!queue.isEmpty()) { + System.out.println(queue.get()); + } + + } + + @Test + public void put2() { + final ArrayQueue queue = new ArrayQueue<>(3); + + new Thread(() -> { + try { + LOGGER.info("[" + Thread.currentThread().getName() + "]" + queue.get()); + } catch (Exception e) { + } + }).start(); + + + queue.put("123"); + queue.put("1234"); + queue.put("12345"); + queue.put("123456"); + LOGGER.info("size=" + queue.size()); + + + while (!queue.isEmpty()) { + LOGGER.info(queue.get()); + } + + } + + @Test + public void put3() { + final ArrayQueue queue = new ArrayQueue<>(3); + + queue.put("123"); + queue.put("1234"); + queue.put("12345"); + queue.put("123456"); + System.out.println(queue.size()); + + + while (!queue.isEmpty()) { + System.out.println(queue.get()); + } + + } + + @Test + public void put4() throws InterruptedException { + final ArrayQueue queue = new ArrayQueue<>(299); + + Thread t1 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + queue.put(i + ""); + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + queue.put(i + ""); + } + }); + + Thread t3 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + queue.put(i + ""); + } + }); + Thread t4 = new Thread(() -> { + System.out.println("=====" + queue.get()); + }); + + t1.start(); + t2.start(); + t3.start(); + t4.start(); + + t1.join(); + t2.join(); + t3.join(); + System.out.println(queue.size()); + + + } + + @Test + public void put5() throws InterruptedException { + final ArrayQueue queue = new ArrayQueue<>(1000000); + + long startTime = System.currentTimeMillis(); + Thread t1 = new Thread(() -> { + for (int i = 0; i < 500000; i++) { + queue.put(i + ""); + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 0; i < 500000; i++) { + queue.put(i + ""); + } + }); + + + t1.start(); + t2.start(); + + t1.join(); + t2.join(); + long end = System.currentTimeMillis(); + + System.out.println("cast = [" + (end - startTime) + "]" + queue.size()); + + } + + @Test + public void put6() throws InterruptedException { + final ArrayBlockingQueue queue = new ArrayBlockingQueue<>(1000000); + + long startTime = System.currentTimeMillis(); + Thread t1 = new Thread(() -> { + for (int i = 0; i < 500000; i++) { + try { + queue.put(i + ""); + } catch (InterruptedException e) { + } + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 0; i < 500000; i++) { + try { + queue.put(i + ""); + } catch (InterruptedException e) { + } + } + }); + + + t1.start(); + t2.start(); + + t1.join(); + t2.join(); + long end = System.currentTimeMillis(); + + System.out.println("cast = [" + (end - startTime) + "]" + queue.size()); + + } + + + @Test + public void get2() throws InterruptedException { + ArrayQueue queue = new ArrayQueue<>(100); + Thread t1 = new Thread(() -> { + for (int i = 0; i < 50; i++) { + try { + queue.put(i + ""); + } catch (Exception e) { + } + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 50; i < 100; i++) { + try { + queue.put(i + ""); + } catch (Exception e) { + } + } + }); + + Thread t3 = new Thread(() -> { + System.out.println("开始消费"); + while (true) { + System.out.println(queue.get()); + } + }); + + t3.start(); + t2.start(); + t1.start(); + + t3.join(); + t2.join(); + t1.join(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolExeceptionTest.java b/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolExeceptionTest.java new file mode 100644 index 00000000..4af12b33 --- /dev/null +++ b/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolExeceptionTest.java @@ -0,0 +1,64 @@ +package com.crossoverjie.concurrent; + +import com.crossoverjie.concurrent.communication.Notify; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +public class CustomThreadPoolExeceptionTest { + private final static Logger LOGGER = LoggerFactory.getLogger(CustomThreadPoolExeceptionTest.class); + @Test + public void execute() { + } + + + public static void main(String[] args) throws InterruptedException { + BlockingQueue queue = new ArrayBlockingQueue<>(1); + CustomThreadPool pool = new CustomThreadPool(1, 1, 1, TimeUnit.SECONDS, queue, new Notify() { + @Override + public void notifyListen() { + LOGGER.info("任务执行完毕"); + } + }) ; + + pool.execute(new Worker(0)); + LOGGER.info("++++++++++++++"); + pool.mainNotify(); + + } + + + + + private static class Worker implements Runnable { + + private int state ; + + public Worker(int state) { + this.state = state; + } + + @Override + public void run() { + try { + TimeUnit.SECONDS.sleep(1); + LOGGER.info("state={}",state); + + while (true){ + state ++ ; + + if (state == 1000){ + throw new NullPointerException("NullPointerException"); + } + } + + } catch (InterruptedException e) { + + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolFutureTest.java b/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolFutureTest.java new file mode 100644 index 00000000..176632ea --- /dev/null +++ b/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolFutureTest.java @@ -0,0 +1,75 @@ +package com.crossoverjie.concurrent; + +import com.crossoverjie.concurrent.communication.Notify; +import com.crossoverjie.concurrent.future.Callable; +import com.crossoverjie.concurrent.future.Future; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +public class CustomThreadPoolFutureTest { + private final static Logger LOGGER = LoggerFactory.getLogger(CustomThreadPoolFutureTest.class); + @Test + public void execute() { + } + + + public static void main(String[] args) throws InterruptedException { + BlockingQueue queue = new ArrayBlockingQueue<>(10); + CustomThreadPool pool = new CustomThreadPool(3, 5, 1, TimeUnit.SECONDS, queue, new Notify() { + @Override + public void notifyListen() { + LOGGER.info("任务执行完毕"); + } + }) ; + + List futures = new ArrayList<>() ; + for (int i = 0; i < 10; i++) { + Future future = pool.submit(new Worker(i)); + futures.add(future) ; + } + + pool.shutdown(); + LOGGER.info("++++++++++++++"); + pool.mainNotify(); + for (Future future : futures) { + Integer integer = future.get(); + LOGGER.info("future======{}" ,integer); + } + + + + + } + + + + + private static class Worker implements Callable { + + private int state ; + + public Worker(int state) { + this.state = state; + } + + @Override + public Integer call() { + try { + TimeUnit.SECONDS.sleep(1); + LOGGER.info("state={}",state); + return state + 1 ; + } catch (InterruptedException e) { + + } + + return 0 ; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolTest.java b/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolTest.java new file mode 100644 index 00000000..c4c5f20d --- /dev/null +++ b/src/test/java/com/crossoverjie/concurrent/CustomThreadPoolTest.java @@ -0,0 +1,70 @@ +package com.crossoverjie.concurrent; + +import com.crossoverjie.concurrent.communication.Notify; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +public class CustomThreadPoolTest { + private final static Logger LOGGER = LoggerFactory.getLogger(CustomThreadPoolTest.class); + @Test + public void execute() { + } + + + public static void main(String[] args) throws InterruptedException { + BlockingQueue queue = new ArrayBlockingQueue<>(10); + CustomThreadPool pool = new CustomThreadPool(3, 5, 1, TimeUnit.SECONDS, queue, new Notify() { + @Override + public void notifyListen() { + LOGGER.info("任务执行完毕"); + } + }) ; + for (int i = 0; i < 10; i++) { + pool.execute(new Worker(i)); + } + + + LOGGER.info("=======休眠前线程池活跃线程数={}======",pool.getWorkerCount()); + + TimeUnit.SECONDS.sleep(5); + LOGGER.info("=======休眠后线程池活跃线程数={}======",pool.getWorkerCount()); + + for (int i = 0; i < 3; i++) { + pool.execute(new Worker(i + 100)); + } + + pool.shutdown(); + //pool.shutDownNow(); + //pool.execute(new Worker(100)); + LOGGER.info("++++++++++++++"); + pool.mainNotify(); + + } + + + + + private static class Worker implements Runnable{ + + private int state ; + + public Worker(int state) { + this.state = state; + } + + @Override + public void run() { + try { + TimeUnit.SECONDS.sleep(1); + LOGGER.info("state={}",state); + } catch (InterruptedException e) { + + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/concurrent/MultipleThreadCountDownKitTest.java b/src/test/java/com/crossoverjie/concurrent/MultipleThreadCountDownKitTest.java new file mode 100644 index 00000000..35661a67 --- /dev/null +++ b/src/test/java/com/crossoverjie/concurrent/MultipleThreadCountDownKitTest.java @@ -0,0 +1,51 @@ +package com.crossoverjie.concurrent; + +import com.crossoverjie.concurrent.communication.MultipleThreadCountDownKit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class MultipleThreadCountDownKitTest { + + private final static Logger LOGGER = LoggerFactory.getLogger(MultipleThreadCountDownKitTest.class) ; + + + public static void main(String[] args) throws InterruptedException { + MultipleThreadCountDownKit multipleThreadKit = new MultipleThreadCountDownKit(3); + multipleThreadKit.setNotify(() -> LOGGER.info("三个线程完成了任务")); + + Thread t1= new Thread(() -> { + try { + //TimeUnit.SECONDS.sleep(5); + LOGGER.info("t1..."); + multipleThreadKit.countDown(); + } catch (Exception e) { + } + }); + Thread t2= new Thread(() -> { + try { + //TimeUnit.SECONDS.sleep(3); + LOGGER.info("t2..."); + multipleThreadKit.countDown(); + } catch (Exception e) { + } + }); + Thread t3= new Thread(() -> { + try { + TimeUnit.SECONDS.sleep(2); + LOGGER.info("t3..."); + multipleThreadKit.countDown(); + } catch (Exception e) { + } + }); + + t1.start(); + t2.start(); + t3.start(); + + multipleThreadKit.await(); + LOGGER.info("======================"); + } + +} \ No newline at end of file diff --git a/src/test/java/com/crossoverjie/concurrent/ThreadPoolTest.java b/src/test/java/com/crossoverjie/concurrent/ThreadPoolTest.java new file mode 100644 index 00000000..ebe47a0f --- /dev/null +++ b/src/test/java/com/crossoverjie/concurrent/ThreadPoolTest.java @@ -0,0 +1,61 @@ +package com.crossoverjie.concurrent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + + +public class ThreadPoolTest { + private final static Logger LOGGER = LoggerFactory.getLogger(ThreadPoolTest.class); + + + + public static void main(String[] args) throws Exception { + BlockingQueue queue = new ArrayBlockingQueue<>(10); + ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,1, TimeUnit.SECONDS,queue,new ThreadPoolExecutor.DiscardOldestPolicy()) ; + + List futures = new ArrayList<>() ; + for (int i = 0; i < 10; i++) { + Future future = pool.submit(new Worker(i)); + futures.add(future) ; + } + + pool.shutdown(); + + for (Future future : futures) { + LOGGER.info("执行结果={}",future.get()); + } + LOGGER.info("++++++++++++++"); + } + + + + + private static class Worker implements Callable{ + + private int state ; + + public Worker(int state) { + this.state = state; + } + + @Override + public Integer call() { + try { + TimeUnit.SECONDS.sleep(2); + LOGGER.info("state={}",state); + return state ; + } catch (InterruptedException e) { + + } + + return -1; + } + } + + + +} \ No newline at end of file