├── LICENSE ├── README.md └── resource └── markdown ├── algorithm ├── 0.array.maxProfit.md ├── 0.array.removeDuplicates.md ├── 0.array.rotate.md ├── BasicSorting.md ├── BinaryTreeDepth.md ├── BinaryTreeTraversal.md └── LRUCache.md ├── cache ├── BloomFilter.md ├── CachePenetration.md ├── HotKey.md ├── Persistence.md ├── RedisCluster.md ├── Replication.md ├── Sentinel.md └── SingleThreadModel.md ├── collection ├── ArrayList.md ├── BinaryTrees.md ├── ConcurrentHashMap1.7v.md ├── ConcurrentHashMap1.8v.md ├── HashConflictsAndResolve.md ├── HashMap1.7v.md ├── HashMap1.8v.md ├── HashMapHashtableConcurrentHashMap.md ├── JavaCollections.md ├── JavaMaps.md ├── LinkedList.md └── Map.Entry1.7v.md ├── database ├── ACIDAndIsolationLevel.md ├── DBSplit.md ├── InnoDB.md ├── MySQLLock.md ├── MySqlBasicInfo.md ├── Mycat1.x.md ├── NormalformAndConstraint.md ├── SQLOptimization.md ├── SeparationOfReadingAndWriting.md ├── ShardingSphere3.x.md └── Zebra2.9.x.md ├── design-pattern └── design-principles.md ├── distribution ├── 2PCand3PC.md ├── ByzantineGeneral.md ├── CAPandBASE.md ├── ConsistentHashing.md ├── DistributedCache.md ├── DistributedLock.md ├── DistributedTransaction.md ├── DuplicateConstruction.md ├── GlobalIDGeneration.md ├── GrabRedEnvelope.md ├── LockDesign.md ├── PaxosAlgorithm.md ├── RequestIdempotency.md ├── TokenDesign.md ├── TryConfirmCancel.md ├── WhatisDistributed.md └── XA.md ├── jvm ├── ClassFileStructure.md ├── Classloading.md ├── GarbageCollectionAlgorithm.md ├── GarbageCollector.md ├── JVMMemoryAnalysisCommand.md └── RuntimeDataAreas.md ├── microservice └── MicroserviceAndSOA.md ├── multithreads ├── AbstractQueuedSynchronizer.md ├── AtomicOperation.md ├── CLH.md ├── CompareAndSwap.md ├── ConcurrentHelperUtil.md ├── ForkJoinFramework.md ├── JavaMemoryModle.md ├── LockAndLock-free.md ├── ScheduledTask.md ├── SequentialConsistencyModel.md ├── ThreadAndProcess.md ├── ThreadLocal.md ├── ThreadPool.md ├── ThreadRule.md ├── ThreadStatus.md ├── synchronized.md └── volatile.md ├── mybatis └── MultitableQueries.md ├── networking ├── GetPost.md ├── HTTP1.1HTTP2.0HTTPS.md ├── IPAddressClassification.md ├── JavaNetServerSocket.md ├── JavaNetSocket.md ├── NetworkModel.md ├── RequestAndResponse.md ├── SlidingWindowProtocol.md ├── StateCode.md ├── TCPAndUDP.md └── TCPConnectAndDisconnect.md ├── spring ├── 08.md ├── AbstractApplicationContext.md ├── AbstractBeanFactory.md ├── AliasRegistry.md ├── ApplicationContext.md ├── ApplicationEvent.md ├── AspectOrientedProgramming.md ├── BeanDefinition.md ├── BeanFactory.md ├── BeanLifeCycle.md ├── BeanScope.md ├── CyclicDependence.md ├── DefaultListableBeanFactory.md ├── DefaultResourceLoader.md ├── DynamicProxy.md ├── GenericApplicationContext.md ├── InversionOfControl.md ├── Reflection.md ├── SingletonBeanRegistry.md ├── SpringBootStart-upProcedure.md ├── SpringThreadSafety.md └── TransactionPropagation.md └── tomcat8 └── conf-server.md /resource/markdown/algorithm/0.array.maxProfit.md: -------------------------------------------------------------------------------- 1 |

Leetcode:买卖股票的最佳时机 II

2 | 3 | 给定一个数组,它的第 *i* 个元素是一支给定股票第 *i* 天的价格。 4 | 5 | 设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 6 | 7 | **注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 8 | 9 | **示例 1:** 10 | 11 | ```java 12 | 输入: [7,1,5,3,6,4] 13 | 输出: 7 14 | 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 15 | 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 16 | ``` 17 | 18 | **示例 2:** 19 | 20 | ```java 21 | 输入: [1,2,3,4,5] 22 | 输出: 4 23 | 解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 24 | 注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。 25 | 因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 26 | ``` 27 | 28 | **示例 3:** 29 | 30 | ```java 31 | 输入: [7,6,4,3,1] 32 | 输出: 0 33 | 解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 34 | ``` 35 | 36 | 37 | 38 | **解题思路:** 39 | 40 | 只看涨,不看跌。不能同参与多个交易,只能根据时间分段买。 41 | 42 | ![过程说明图](https://i.loli.net/2019/01/24/5c49cf8b2385e.png) 43 | 44 | 45 | 46 | **Java代码如下:** 47 | 48 | ```java 49 | class Solution { 50 | public int maxProfit(int[] prices) { 51 | int profit = 0; 52 | for (int i = 0; i < prices.length - 1; i++) { 53 | // 涨钱,就获利润 54 | if (prices[i + 1] > prices[i]) { 55 | profit += prices[i + 1] - prices[i]; 56 | } 57 | } 58 | 59 | return profit; 60 | } 61 | } 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /resource/markdown/algorithm/0.array.removeDuplicates.md: -------------------------------------------------------------------------------- 1 |

Leetcode:从排序数组中删除重复项

2 | 3 | 给定一个排序数组,你需要在**原地**删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。 4 | 5 | 不要使用额外的数组空间,你必须在**原地修改输入数组**并在使用 O(1) 额外空间的条件下完成。 6 | 7 | **示例 1:** 8 | 9 | ``` 10 | 给定数组 nums = [1,1,2], 11 | 12 | 函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。 13 | 14 | 你不需要考虑数组中超出新长度后面的元素。 15 | ``` 16 | 17 | **示例 2:** 18 | 19 | ``` 20 | 给定 nums = [0,0,1,1,1,2,2,3,3,4], 21 | 22 | 函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。 23 | 24 | 你不需要考虑数组中超出新长度后面的元素。 25 | ``` 26 | 27 | **说明:** 28 | 29 | 为什么返回数值是整数,但输出的答案是数组呢? 30 | 31 | 请注意,输入数组是以**“引用”**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。 32 | 33 | 你可以想象内部操作如下: 34 | 35 | ```java 36 | // nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝 37 | int len = removeDuplicates(nums); 38 | 39 | // 在函数里修改输入数组对于调用者是可见的。 40 | // 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。 41 | for (int i = 0; i < len; i++) { 42 | print(nums[i]); 43 | } 44 | ``` 45 | 46 | 47 | 48 | **解题思路:** 49 | 50 | 题目要求必须在原数组修改,那么元素只能在原数组移动,时间复杂度 O(1),那么只能遍历一次数组。为了保证数据靠近头部,从头部开始遍历。用 index 表示当前元素前的元素都没有重复的元素,以此向后遍历,保证出现 **新** 的元素(也就是前面未出现的元素)那么把它 `nums[i]` 放到 `nums[++index]` 位置。 51 | 52 | 总体思路:把后面第一次出现的新元素,往前放。 53 | 54 | ![过程说明图](https://i.loli.net/2019/01/24/5c49c5307b3be.png) 55 | 56 | 57 | 58 | **Java代码如下:** 59 | 60 | ```java 61 | class Solution { 62 | public int removeDuplicates(int[] nums) { 63 | int index = 0; 64 | for (int i = 0; i < nums.length; i++) { 65 | if (nums[i] != nums[index]) { 66 | nums[++index] = nums[i]; 67 | } 68 | } 69 | 70 | return index + 1; 71 | } 72 | } 73 | ``` 74 | 75 | -------------------------------------------------------------------------------- /resource/markdown/algorithm/0.array.rotate.md: -------------------------------------------------------------------------------- 1 |

Leetcode:旋转数组

2 | 3 | 给定一个数组,将数组中的元素向右移动 *k* 个位置,其中 *k* 是非负数。 4 | 5 | **示例 1:** 6 | 7 | ```java 8 | 输入: [1,2,3,4,5,6,7] 和 k = 3 9 | 输出: [5,6,7,1,2,3,4] 10 | 解释: 11 | 向右旋转 1 步: [7,1,2,3,4,5,6] 12 | 向右旋转 2 步: [6,7,1,2,3,4,5] 13 | 向右旋转 3 步: [5,6,7,1,2,3,4] 14 | ``` 15 | 16 | **示例 2:** 17 | 18 | ```java 19 | 输入: [-1,-100,3,99] 和 k = 2 20 | 输出: [3,99,-1,-100] 21 | 解释: 22 | 向右旋转 1 步: [99,-1,-100,3] 23 | 向右旋转 2 步: [3,99,-1,-100] 24 | ``` 25 | 26 | **说明:** 27 | 28 | - 尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。 29 | - 要求使用空间复杂度为 O(1) 的原地算法。 30 | 31 | 32 | 33 | **解题思路:** 34 | 35 | 36 | 37 | ![过程说明图]() 38 | 39 | 40 | 41 | **Java代码如下:** 42 | 43 | ```java 44 | 45 | ``` 46 | 47 | -------------------------------------------------------------------------------- /resource/markdown/algorithm/BasicSorting.md: -------------------------------------------------------------------------------- 1 | ### 2常见基础排序算法 2 | 3 | 4 | 5 | #### 排序算法分类 6 | 7 | ![排序算法分类](http://ww2.sinaimg.cn/large/006y8mN6ly1g68uopou69j30ko0dzwgb.jpg) 8 | 9 | 10 | #### 时间复杂度 11 | 12 | | 排序算法 | 最好(时间复杂度) | 平均(时间复杂度) | 最坏(时间复杂度) | 稳定性 | 空间复杂度 | 13 | | ------------ | ------------------------- | ------------------------- | ------------------------- | ------ | -------------------------------- | 14 | | 冒泡排序 | **O**(n) | **O**(n2) | **O**(n2) | 稳定 | **O**(1) | 15 | | **快速排序** | **O**(n*log2n) | **O**(n*log2n) | **O**(n2) | 不稳定 | **O**(log2n)~**O**(n) | 16 | | 直接插入排序 | **O**(n) | **O**(n2) | **O**(n2) | 稳定 | **O**(1) | 17 | | **希尔排序** | **O**(n) | **O**(n1.3) | **O**(n2) | 不稳定 | **O**(1) | 18 | | 简单选择排序 | **O**(n) | **O**(n2) | **O**(n2) | 不稳定 | **O**(1) | 19 | | **堆排序** | **O**(n*log2n) | **O**(n*log2n) | **O**(n*log2n) | 不稳定 | **O**(1) | 20 | | **归并排序** | **O**(n*log2n) | **O**(n*log2n) | **O**(n*log2n) | 稳定 | **O**(n) | 21 | | 基数排序 | **O**(d*(r+n)) | **O**(d*(r+n)) | **O**(d*(r+n)) | 稳定 | **O**(r*d+n) | 22 | 23 | 24 | 25 | #### 各种复杂度效率比较图 26 | 27 | ```java 28 | O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(2^n) < O(n^3) < O(n^n) 29 | ``` 30 | 31 | ![各种时间复杂度效率比较图](https://i.loli.net/2019/06/11/5cff235ba9b9a93703.jpg) 32 | 33 | **说明:** n 越大,越能体现算法效率。当 n 比较小时,复杂度会有一波小交叉,上图不考虑 n 比较小的情况。 34 | 35 | 36 | 37 | ##### 1. 冒泡排序 ★☆☆☆☆ 38 | 39 | ```java 40 | public void bubbleSort(int[] array) { 41 | if (array == null) { 42 | return; 43 | } 44 | 45 | int temp; 46 | // 冒泡次数 47 | for (int i = array.length - 1; i > 0; i--) { 48 | // 冒泡排序 49 | for (int j = 0; j < i; j++) { 50 | // 将大值交换到后面 51 | if (array[j] > array[j + 1]) { 52 | temp = array[j]; 53 | array[j] = array[j + 1]; 54 | array[j + 1] = temp; 55 | } 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | 62 | 63 | ##### 2. 快速排序 ★★★★★ 64 | 65 | 基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。 66 | 67 | ```java 68 | public void quickSort(int[] array, int left, int right) { 69 | if (array == null) { 70 | return; 71 | } 72 | 73 | if (left < right) { 74 | int i = left; 75 | int j = right; 76 | int temp = array[i]; // 选取一端值为基准值 77 | 78 | while (i < j) { 79 | // 如果 j 处值大于等于基准值,那么不用交换数据,直接将 j 向前移动, 80 | // 直到 i 等于 j 或者 j 处值比基准值小 81 | while (i < j && array[j] >= temp) { 82 | j--; 83 | } 84 | // 如果 i < j,说明 j 处值比基准值小(根据上面循环判断) 85 | if (i < j) { 86 | // 交换 j 与 i 处的值,并将 i 向后移动 87 | array[i++] = array[j]; 88 | } 89 | 90 | 91 | // 如果 i 处值小于等于基准值,那么将i向后移动就可以了 92 | while (i < j && array[i] <= temp) { 93 | i++; 94 | } 95 | // 如果 i < j,说明 i 处值比基准值大(根据上面循环判断) 96 | if (i < j) { 97 | // 交换 i 与 j 处的值,并将 i 向前移动 98 | array[j--] = array[i]; 99 | } 100 | 101 | // 最后将临时基准值填充到 i 处 102 | array[i] = temp; 103 | // 对两段各自快速排序 104 | } 105 | 106 | quickSort(array, left, i - 1); 107 | quickSort(array, i + 1, right); 108 | } 109 | } 110 | ``` 111 | 112 | 113 | 114 | ##### 3. 直接插入排序 ★★★☆☆ 115 | 116 | ```java 117 | public void insertionSort(int[] array) { 118 | if (array == null) { 119 | return; 120 | } 121 | // 和冒泡排序有些类似,这里是遍历趟数 122 | for (int i = 0; i < array.length; i++) { 123 | // 精髓是从局部有序,到整体有序 124 | int temp = array[i]; // 当前基准元素 125 | int j; 126 | for (j = i; j > 0 && array[j - 1] > temp; j--) { 127 | array[j] = array[j - 1]; // 下一个元素比基准元素大,下一个元素向后移动 128 | } 129 | // 最后比较当前元素和基准元素大小 130 | if (array[j] > temp) { 131 | array[j] = temp; 132 | } 133 | } 134 | } 135 | ``` 136 | 137 | 138 | 139 | ##### 4. 希尔排序(缩写增量-直接插入排序) ★★★☆☆ 140 | 141 | ```java 142 | public void shellSort(int[] array) { 143 | if (array == null) { 144 | return; 145 | } 146 | // 计算增量 147 | for (int d = array.length / 2; d > 0; d /= 2) { 148 | // 分组 149 | for (int g = 0; g < d; g++) { 150 | // 插入排序(第 x 组的第 d 个增量元素起步)(直接插入排序的增量是 1,这里是 d,需注意下) 151 | for (int i = g + d; i < array.length; i += d) { 152 | int temp = array[i]; 153 | int j; 154 | for (j = i; j > d && array[j - d] > temp; j -= d) { 155 | array[j] = array[j - d]; 156 | } 157 | if (array[j] > temp) { 158 | array[j] = temp; 159 | } 160 | } 161 | } 162 | } 163 | } 164 | ``` 165 | 166 | 167 | 168 | ##### 5. 简单选择排序 ★★☆☆☆ 169 | 170 | ```java 171 | public void selectionSort(int[] array) { 172 | if (array == null) { 173 | return; 174 | } 175 | 176 | int index; 177 | int temp; 178 | // 做出的选择次数 179 | for (int i = array.length - 1; i > 0; i--) { 180 | index = 0; 181 | for (int j = 1; j < i; j++) { 182 | // 选择一个最大的值(记录索引) 183 | if (array[j] > array[index]) { 184 | index = j; 185 | } 186 | } 187 | // 将选出的最大值换到一端 188 | if (array[index] > array[i]) { 189 | temp = array[index]; 190 | array[index] = array[i]; 191 | array[i] = temp; 192 | } 193 | } 194 | } 195 | ``` 196 | 197 | 198 | 199 | ##### 6. 堆排序 ★★★★☆ 200 | 201 | ```java 202 | public void heapSort(int[] array) { 203 | if (array == null) { 204 | return; 205 | } 206 | 207 | for (int i = array.length / 2 - 1; i >= 0; i--) { 208 | // 先调整堆(选择一个最大值放到堆顶) 209 | adjustHeap(array, i, array.length); 210 | } 211 | 212 | for (int i = array.length - 1; i > 0; i--) { 213 | // 将堆顶的元素与其他元素比较并交换 214 | swap(array, 0, i); 215 | // 再调整堆 216 | adjustHeap(array, 0, i); 217 | } 218 | } 219 | 220 | // 调整堆,使得堆顶元素值大于等于其子节点值 221 | private void adjustHeap(int[] array, int top, int length) { 222 | int temp = array[top]; 223 | 224 | for (int i = top * 2 + 1; i < length; i = i * 2 + 1) { 225 | // (如果存在的化)从左右子节点找出值最大的子节点 226 | if (i + 1 < length && array[i + 1] > array[i]) { 227 | i++; 228 | } 229 | if (array[i] > temp) { 230 | array[top] = array[i]; 231 | top = i; 232 | } else { 233 | break; 234 | } 235 | } 236 | 237 | if (array[top] > temp) { 238 | array[top] = temp; 239 | } 240 | } 241 | 242 | private void swap(int[] array, int a, int b) { 243 | int temp = array[a]; 244 | array[a] = array[b]; 245 | array[b] = temp; 246 | } 247 | ``` 248 | 249 | 250 | 251 | ##### 7. 归并排序 ★★★★☆ 252 | 253 | ```java 254 | public void mergeSort(int[] array) { 255 | if (array == null) { 256 | return; 257 | } 258 | 259 | int[] aux = new int[array.length]; 260 | sort(array, 0, array.length - 1, aux); 261 | } 262 | 263 | private void sort(int[] array, int left, int right,int[] aux) { 264 | if (left < right) { 265 | int mid = (left + right) / 2; 266 | // 先分后合 267 | sort(array, left , mid, aux); 268 | sort(array, mid + 1, right, aux); 269 | merge(array, left, mid, right, aux); 270 | } 271 | } 272 | 273 | private void merge(int[] array, int left, int mid, int right, int[] aux){ 274 | int t = 0; 275 | int l = left; 276 | int m = mid + 1; 277 | 278 | // 判断元素值大小,按大小排序到辅助数组上 279 | while (l <= mid && m <= right) { 280 | if (array[l] <= array[m]) { 281 | aux[t++] = array[l++]; 282 | } else { 283 | aux[t++] = array[m++]; 284 | } 285 | } 286 | 287 | // 把剩余元素填充到辅助数组上 288 | while (l <= mid) { 289 | aux[t++] = array[l++]; 290 | } 291 | while (m <= right) { 292 | aux[t++] = array[m++]; 293 | } 294 | 295 | // 将辅助线数组上的元素复制到需要排序的数组上 296 | t = 0; 297 | while (left <= right) { 298 | array[left++] = aux[t++]; 299 | } 300 | } 301 | ``` 302 | 303 | -------------------------------------------------------------------------------- /resource/markdown/algorithm/BinaryTreeDepth.md: -------------------------------------------------------------------------------- 1 | ### 二叉树的最小深度 2 | 3 | 4 | 5 | > 给定一个二叉树,找出其最小深度。 6 | > 7 | > 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 8 | 9 | **说明:** 叶子节点是指没有子节点的节点。 10 | 11 | 12 | 13 | ##### 0. 定义一个二叉树节点 14 | 15 | ```java 16 | public class TreeNode { 17 | int val; 18 | TreeNode left; 19 | TreeNode right; 20 | TreeNode(int x) { 21 | val = x; 22 | } 23 | } 24 | ``` 25 | 26 | 27 | 28 | ##### 1. 递归 29 | 30 | ```java 31 | public class Solution { 32 | public int minDepth(TreeNode root) { 33 | if (root == null) { 34 | return 0; 35 | } 36 | 37 | // 递归计算 38 | int leftMinDepth = minDepth(root.left); 39 | int rightMinDepth = minDepth(root.right); 40 | 41 | // 有子节点为空的,则返回另一个节点的深度。否则返回两则最小的深度。 42 | return leftMinDepth == 0 || rightMinDepth == 0 43 | ? leftMinDepth + rightMinDepth + 1 44 | : Math.min(leftMinDepth, rightMinDepth) + 1; 45 | } 46 | } 47 | ``` 48 | 49 | 50 | 51 | ##### 2. 非递归(宽度优先搜索) 52 | 53 | > 出现第一个无子节点的节点,则该节点的深度为树的最小深度 54 | 55 | ```java 56 | public class Solution { 57 | public int minDepth(TreeNode root) { 58 | if (root == null) { 59 | return 0; 60 | } 61 | 62 | Queue queue = new LinkedList<>(); 63 | queue.offer(root); 64 | 65 | int minDepth = 0; 66 | while (!queue.isEmpty()) { 67 | ++minDepth; 68 | 69 | // 逐层遍历,判断一层是否存在没有子节点的节点,则该节点的深度为树的最小深度 70 | int size = queue.size(); 71 | for (int i = 0; i < size; i++) { 72 | root = queue.poll(); 73 | if (root.left == null && root.right == null) { 74 | return minDepth; 75 | } 76 | if (root.left != null) { 77 | queue.offer(root.left); 78 | } 79 | if (root.right != null) { 80 | queue.offer(root.right); 81 | } 82 | } 83 | } 84 | 85 | return minDepth; 86 | } 87 | } 88 | ``` 89 | 90 | -------------------------------------------------------------------------------- /resource/markdown/algorithm/BinaryTreeTraversal.md: -------------------------------------------------------------------------------- 1 | ### 二叉树的遍历 ★★★★★ 2 | 3 | 4 | 5 | ##### TreeNode 节点 6 | 7 | ```java 8 | /* Definition for a binary tree node. */ 9 | public class TreeNode { 10 | int val; 11 | TreeNode left; 12 | TreeNode right; 13 | 14 | TreeNode(int x) { 15 | val = x; 16 | } 17 | } 18 | ``` 19 | 20 | 21 | 22 | ##### 1. 递归调用 (深度优先遍历) ★☆☆☆☆ 23 | 24 | ```java 25 | public void preorderTraversal(TreeNode root) { 26 | if (root == null) { 27 | return; 28 | } 29 | // 可调整前、中、后序遍历 30 | System.out.print(root.val); 31 | preorderTraversal(root.left); 32 | preorderTraversal(root.right); 33 | } 34 | ``` 35 | 36 | 37 | 38 | ##### 2.1 非递归遍历 - 基于栈 (前序遍历、深度优先遍历) ★★★☆☆(不推荐) 39 | 40 | ```java 41 | public List preorderTraversalWithStack(TreeNode root) { 42 | 43 | LinkedList output = new LinkedList<>(); 44 | 45 | // 用于回溯的栈 46 | Stack stack = new Stack<>(); 47 | 48 | while (root != null || !stack.isEmpty()) { 49 | // 访问结点的数据,并且移动到左子结点,直到无左子结点 50 | while (root != null) { 51 | output.add(root.val); 52 | stack.push(root); // 将当前节点 push 到栈中,方便后续回溯 53 | root = root.left; // 遍历左子结点 54 | } 55 | // 回溯,遍历右子结点 56 | if (!stack.isEmpty()) { 57 | root = stack.pop().right; 58 | } 59 | } 60 | 61 | return output; 62 | } 63 | ``` 64 | 65 | 66 | 67 | ##### 2.2非递归遍历 - 基于栈改进版(基于队列的栈模式)(执行时间较上者提升1倍) ★★★★☆ 68 | 69 | ```java 70 | public List preorderTraversalWithStack(TreeNode root) { 71 | 72 | LinkedList output = new LinkedList<>(); 73 | 74 | // 用于回溯的栈(基于链表实现,不用担心栈的扩容问题) 75 | LinkedList stack = new LinkedList<>(); 76 | 77 | while (root != null || !stack.isEmpty()) { 78 | // 访问结点的数据,并且移动到左子结点,直到无左子结点 79 | while (root != null) { 80 | output.add(root.val); 81 | stack.add(root); // 将当前节点 add 到栈中,方便后续回溯 82 | root = root.left; // 遍历左子结点 83 | } 84 | // 回溯,遍历右子结点 85 | if (!stack.isEmpty()) { 86 | root = stack.pollLast().right; 87 | } 88 | } 89 | 90 | return output; 91 | } 92 | ``` 93 | 94 | 95 | 96 | 97 | 98 | ##### 3. 非递归遍历 - 基于队列 (层次遍历、广度优先遍历、O(n)) ★★★★☆ 99 | 100 | ```java 101 | public List levelorderTraversal(TreeNode root) { 102 | LinkedList output = new LinkedList<>(); 103 | if (root == null) { 104 | return output; 105 | } 106 | 107 | Queue queue = new LinkedList<>(); 108 | queue.add(root); 109 | 110 | // 遍历队列 111 | while (!queue.isEmpty()) { 112 | // 从队列头部取出一个结点 113 | root = queue.poll(); 114 | output.add(root.val); 115 | // 将左右子结点放入队列尾部 116 | if (root.left != null) { 117 | queue.add(root.left); 118 | } 119 | if (root.right != null) { 120 | queue.add(root.right); 121 | } 122 | } 123 | 124 | return output; 125 | } 126 | ``` 127 | 128 | 129 | 130 | ##### 4. Morris Traversal(莫里斯遍历、O(n)) ★★★★☆ 131 | 132 | ```java 133 | public List morrisTraversal(TreeNode root) { 134 | LinkedList output = new LinkedList<>(); 135 | 136 | TreeNode prev; 137 | TreeNode curr = root; 138 | while (curr != null) { 139 | // 向左子节点遍历 140 | if (curr.left != null) { 141 | prev = curr.left; 142 | while (prev.right != null && prev.right != curr) { 143 | prev = prev.right; 144 | } 145 | // 右子节点的回溯指针绑定 146 | if (prev.right == null) { 147 | prev.right = curr; 148 | curr = curr.left; 149 | } else { 150 | output.add(curr.val); 151 | prev.right = null; 152 | curr = curr.right; 153 | } 154 | // 向右子节点遍历 155 | } else { 156 | output.add(curr.val); 157 | curr = curr.right; 158 | } 159 | } 160 | 161 | return output; 162 | } 163 | ``` 164 | 165 | -------------------------------------------------------------------------------- /resource/markdown/algorithm/LRUCache.md: -------------------------------------------------------------------------------- 1 | ### LRUCache 缓存算法 2 | 3 | ##### 最少最近使用算法 4 | 5 | ```java 6 | public class LRUCache { 7 | /** 8 | * 默认容量 9 | */ 10 | private static final int DEFAULT_CAPACITY = 1024; 11 | /** 12 | * 缓存容量 13 | */ 14 | private int capacity; 15 | /** 16 | * 实际存储/使用的元素大小 17 | */ 18 | private int size = 0; 19 | /** 20 | * 高效访问的散列表 21 | */ 22 | private Map> map; 23 | 24 | private Node head, tail; 25 | 26 | /** 27 | * 自定义双向链表中的节点 28 | */ 29 | private static class Node { 30 | K key; 31 | V value; 32 | Node prev; 33 | Node next; 34 | 35 | Node(Node prev, K key, V value, Node next) { 36 | this.key = key; 37 | this.value = value; 38 | this.prev = prev; 39 | this.next = next; 40 | } 41 | } 42 | 43 | public LRUCache() { 44 | this.capacity = DEFAULT_CAPACITY; 45 | this.map = new HashMap<>(DEFAULT_CAPACITY, 0.75F); 46 | this.head = null; 47 | this.tail = null; 48 | } 49 | 50 | public LRUCache(int capacity) { 51 | if (capacity <= 0) { 52 | throw new IllegalArgumentException("Capacity must be positive integer"); 53 | } 54 | 55 | this.capacity = capacity; 56 | this.map = new HashMap<>(capacity, 0.75F); 57 | this.head = null; 58 | this.tail = null; 59 | } 60 | 61 | public V get(K key) { 62 | Node node = this.map.get(key); 63 | if (node != null) { 64 | this.moveToHead(node); 65 | return node.value; 66 | } else { 67 | return null; 68 | } 69 | } 70 | 71 | public V put(K key, V value) { 72 | Node node = this.map.get(key); 73 | if (node != null) { 74 | node.value = value; 75 | moveToHead(node); 76 | return value; 77 | } 78 | 79 | if (size == capacity) { 80 | node = removeLast(); 81 | map.remove(node.key); 82 | } 83 | 84 | node = addFirst(key, value); 85 | map.put(key, node); 86 | 87 | return value; 88 | } 89 | 90 | /** 91 | * 对于新添加的元素,应将新元素添加到链表的头部 92 | */ 93 | private Node addFirst(K key, V value) { 94 | final Node h = head; 95 | final Node newNode = new Node<>(null, key, value, h); 96 | head = newNode; 97 | if (h == null) { 98 | tail = newNode; 99 | } else { 100 | h.prev = newNode; 101 | } 102 | size++; 103 | 104 | return newNode; 105 | } 106 | 107 | /** 108 | * 对于被访问的元素,将该元素移动到头部 109 | */ 110 | private Node moveToHead(Node node) { 111 | final Node prev = node.prev; 112 | final Node next = node.next; 113 | 114 | if (prev == null) { // 如果是首节点,无需移动 115 | return node; 116 | } 117 | 118 | prev.next = next; 119 | if (next == null) { // 如果是尾节点,需要移动tail 120 | tail = prev; 121 | } else { 122 | next.prev = prev; 123 | } 124 | 125 | node.prev = null; 126 | node.next = head; 127 | head.prev = node; 128 | head = node; 129 | 130 | return node; 131 | } 132 | 133 | /** 134 | * 缓存满时,应删除(淘汰)最后一个节点 135 | */ 136 | private Node removeLast() { 137 | final Node t = tail; 138 | if (t == null) { 139 | return null; 140 | } 141 | 142 | t.value = null; // help GC 143 | Node prev = t.prev; 144 | t.prev = null; // help GC 145 | tail = prev; // 移动 tail 146 | if (prev == null) { // 如果尾节点的前一个节点也为空,说明尾节点也是首节点 147 | head = null; 148 | } else { 149 | prev.next = null; 150 | } 151 | size--; 152 | return t; 153 | } 154 | } 155 | ``` 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /resource/markdown/cache/BloomFilter.md: -------------------------------------------------------------------------------- 1 |

布隆过滤

2 | 3 | -------------------------------------------------------------------------------- /resource/markdown/cache/CachePenetration.md: -------------------------------------------------------------------------------- 1 |

缓存击穿(缓存穿透)

2 | 3 | ![缓存穿透](https://i.loli.net/2019/02/19/5c6b968d1e946.png) 4 | 5 | **缓存(Cache)** 是分布式、高并发的场景下,为了保护后端数据库,降低数据库的压力,引入的一种基于内存的数据访问机制,能加快数据的读取与写入,进而提高系统负载能力。 6 | 7 | **缓存击穿(缓存穿透)** 是访问不存在缓存中的数据,进而直接访问数据库。 8 | 9 | 10 | 11 | **缓存击穿** 和 **缓存穿透** 更为详细的分类: 12 | 13 | | 名称 | 缓存是否存在被访问数据 | 数据库是否存在被访问数据 | 14 | | -------- | ---------------------- | ------------------------ | 15 | | 缓存击穿 | 否 | 是 | 16 | | | 否 | 否 | 17 | 18 | 在缓存穿透这种情形下,如果缓存和数据库都没有需要被访问的数据,那么访问缓存时没有数据则直接返回空,避免重复访问数据库,造成不必要的后端数据库压力。 19 | 20 | 21 | 22 | 总之,避免缓存击穿也好,避免缓存穿透也罢,核心思路是想法设法不要让请求怼在数据库上。 23 | 24 | 25 | 26 | ##### 解决方案 27 | 28 | **1.缓存空值:** 如果第一次访问一个不存在的数据,那么将此key与value为空值的数据缓存起来,下次再有对应的key访问时,缓存直接返回空值,通常要设置key的过期时间,再次访问时更新过期时间; 29 | 30 | **2.布隆过滤:** 类似散列集合(hash set),判断key是否在这个集合中。实现机制在于比特位,一个key对应一个比特位,并且存储一个标识,如果key有对应的比特位,并且标识位表示存在,则表示有对应的数据。比如使用Redis 的 Bitmap实现。 31 | 32 | 33 | 34 | --- 35 | 36 |

缓存雪崩

37 | 38 | **缓存雪崩:** 大量的缓存击穿意味着大量的请求怼在数据库上,轻者造成数据库响应巨慢,严重者造成数据库宕机。比如缓存内的数据集体定时刷新、服务器重启等。 39 | 40 | 41 | 42 | ##### 解决方案 43 | 44 | 1. 降低缓存刷新频率; 45 | 2. 部分缓存刷新,刷新数据时按照一定规则分组刷新; 46 | 3. 设置key永远不过期,如果需要刷新数据,则定时刷新; 47 | 4. 分片缓存,在分布式缓存下,将需要缓存的数据 散列 分布到多个节点,尽量将热点数据均匀分布到多节点。 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /resource/markdown/cache/HotKey.md: -------------------------------------------------------------------------------- 1 |

Redis热点Key

2 | 3 | 在一定时间内,被频繁访问的key称为 **热点key** 。比如突发性新闻,微博上常见的热搜新闻,引起千千万万人短时间内浏览;在线商城大促活动,消费者比较关注的商品突然降价,引起成千上万消费者点击、购买。热点key会导致流量过于集中,缓存服务器的压力骤然上升,如果超出物理机器的承载能力,则缓存不可用,进而可能诱发缓存击穿、缓存雪崩的问题。 4 | 5 | ![缓存击穿](https://i.loli.net/2019/02/22/5c6f888d77c01.png) 6 | 7 | 8 | 9 | #### 解决方案 10 | 11 | --- 12 | 13 | ##### 一、读写分离 14 | 15 | ![读写分离](https://i.loli.net/2019/02/22/5c6fbae592c5d.png) 16 | 17 | 通过将数据的写入与读取分散去各个节点,通过数据复制到达各个节点数据一致性的目的。在写少的情形下,master节点写入数据,在读取请求压力大的情形下,配置多个slave节点,数据横向同步(拓扑结构)。结合Redis Sentinel (或其他高可用技术)实现缓存节点的高可用。 18 | 19 | 20 | 21 | ##### 二、阿里云云数据库 Redis 版解决方案 22 | 23 | ![阿里云云数据库 Redis 版解决方案](https://i.loli.net/2019/02/22/5c6f8da9c4e60.png) 24 | 25 | 在热点 `Key` 的处理上主要分为写入跟读取两种形式,在数据写入过程当 `SLB` 收到数据 `key1` 并将其通过某一个 `Proxy` 写入一个 `Redis`,完成数据的写入。假若经过后端热点模块计算发现 `key1` 成为热点 `key` 后, `Proxy` 会将该热点进行缓存,当下次客户端再进行访问 `key1` 时,可以不经 `Redis`。最后由于` Proxy` 是可以水平扩充的,因此可以任意增强热点数据的访问能力。 26 | 27 | 28 | 29 | ##### 三、热点key不过期 30 | 31 | 如果key存在,那么不要设置key过期时间,如果key对应的数据不可用(比如删除了),那么从缓存中删除key。从请求来说,如果在缓存找到对应的key,表明该key及其value就是用户需要的数据。如果缓存中不存在对应的key,表明无对应的数据,返回空值。比如微博发来爆料某明星文章,短时间内访问量直接上升,如果key不过期,那么请求永远命中缓存。只有当文章被删除的时候,才从缓存中删除对应的key,如果此时还有请求访问,在缓存中查无数据时,直接返回空值,表明文章被删除。当然也可以更新key对应的value值,返回想要表达的value。 32 | 33 | 34 | 35 | **参考资料:** 36 | 37 | [阿里云:热点 Key 问题的发现与解决](https://help.aliyun.com/document_detail/67252.html) -------------------------------------------------------------------------------- /resource/markdown/cache/Persistence.md: -------------------------------------------------------------------------------- 1 |

Redis持久化

2 | 3 | ![persistence](https://i.loli.net/2019/02/13/5c63bb6ce9d5f.jpeg) 4 | 5 | Redis 是一种基于内存的数据库,一旦断电、重启,Redis 中的数据将不复存在。Redis 提供了 RDB 和 AOF 两个持久化的方式。当 Redis 实例重启时,可以使用已持久化的数据(文件)来还原内存数据集。 6 | 7 | 8 | 9 | --- 10 | 11 |

RDB

12 | 13 | **RDB** 的全称是 *Redis DataBase* ,Redis 数据库。这种持久化方式是将当前Redis内存中的数据生成 **快照(Snapshot)** 文件保存到硬盘,简单粗暴。RDB也是Redis默认开启的持久化方式。 14 | 15 | 16 | 17 | ##### 触发持久化 18 | 19 | ![save](https://i.loli.net/2019/02/13/5c63c0174e045.png) 20 | 21 | 在之前已经废弃的 `save` 命令来实现RDB持久化数据,`save` 命令持久化时会一直阻塞,直到持久化完成。数据量越大,持久化过程带来的阻塞时间就越长。 22 | 23 | ![bgsave](https://i.loli.net/2019/02/13/5c63c02cc0753.png) 24 | 25 | 现在可以使用 `bgsave` 命令来代替 `save`。bg就是background,使用 `bgsave` 方式RDB持久化时,Redis工作进程会 fork 一个子进程,该子进程专门来负责耗时的RDB持久化,而工作进程会继续工作,阻塞时间只发生在短暂的 fork 阶段。 26 | 27 | 28 | 29 | 除了主动触发命令外,Redis还是提供了自动触发RDB持久化命令 `bgsave ` ,该命令表示在seconds秒内,Redis内存数据修改的次数达到changes次时,将触发RDB持久化。比如 `bgsave 7200 10` 表示在2个小时内,数据被更改10次时将自动触发RDB持久化。 30 | 31 | (注意changes计数并不包含查询语句,当然被操作的key一定存在,否则也不会进行计数) 32 | 33 | 作者现在测试使用的是 `4.0.9` 版本,Redis在此版本默认开启的参数是 `save 900 1` 、`save 300 10` 、`save 60 10000` ,更加实际情况可自定义更改。 34 | 35 | 36 | 37 | ##### 数据快照存储 38 | 39 | 首先,工作目录默认为当前Redis目录,那么持久化的RDB数据快照将存储在该目录,可通过 `dir ./` 参数进行修改。生成的数据存储快照名称默认为 `dump.rdb` ,可通过 `dbfilename dump.rdb` 参数进行修改。RDB持久化先生存一个临时快照,待数据持久化完成,这个临时快照会替换原快照,以防持久化过程出现问题。 40 | 41 | RDB存储快照默认是使用LZF算法压缩功能,压缩后的文件体积将大大减少。压缩文件但并不是十全十美,RDB快照压缩时也会消耗CPU,数据量越大,消耗CPU资源就越多,但官网还是极力推荐我们开始此功能。 42 | 43 | RDB持久化时可能出现权限问题、存储空间不够用等问题,Redis 默认开启`stop-writes-on-bgsave-error yes` 参数,意味着出现 error时,Redis将停止向快照文件写入数据。 44 | 45 | 在Redis保存/恢复数据时,并不是一股脑进行保存/恢复,默认会对RDB快照文件进行检查,性能会受到影响(大约10%),因此可以禁用它以获得最大的性能。(推荐开启) 46 | 47 | 48 | 49 | --- 50 | 51 |

AOF

52 | 53 | **AOF** 的全称是 *Append Only File* ,仅附加文件,类似于MySQL的binlog。这种持久化方式是通过保存写命令来记录操作日志,在数据恢复时,重新执行已记录的写命令,来达到数据恢复。 54 | 55 | 56 | 57 | ##### AOF原理 58 | 59 | ![AOF](https://i.loli.net/2019/02/13/5c63dd9d9f120.png) 60 | 61 | 默认情况下,AOF是关闭的,可以通过修改 `appendonly no` 为 `appendonly yes` 来开启 AOF 持久化。内存的Output的带宽远大于磁盘Input的带宽,如果内存瞬时将大量的数据写入磁盘,必然发生IO堵塞,对于单线程工作的Redis也将会在性能上大打折扣。所以所有的写命令并不会直接写入AOF文件,而是先将写命令存储至AOF Buffer缓冲区,最后在同步至AOF文件。 62 | 63 | 生成的AOF文件名,默认为 `appendonly.aof` ,可以修改 `appendfilename "appendonly.aof"` 参数来更改生成的AOF文件名。 64 | 65 | 66 | 67 | 从缓冲区同步至磁盘,Redis提供了三种同步方式: 68 | 69 | **always:** 写命令被追加到AOF缓存后,调用系统fsync函数将缓冲区的数据同步至磁盘,最后返回 70 | 71 | **everysec:** 写命令被追加到AOF缓存后,调用系统write函数,在write函数执行完成,然后返回,会触发系统将对缓冲区的数据进行同步至磁盘 72 | 73 | **no:** 写命令被追加到AOF缓存后,调用系统write函数,然后返回,后续的同步由系统负责 74 | 75 | 其中 `everysec` 不仅是默认的方式,也是推荐的方式。 76 | 77 | 78 | 79 | ##### AOF重写机制 80 | 81 | AOF持久化文件存储的是Redis写命令,这比仅仅是数据的存储文件要大了很多,而且像如下的命令就可以考虑做很多优化: 82 | 83 | ```shell 84 | 127.0.0.1:6379> set user:10001 Kevin 85 | OK 86 | 127.0.0.1:6379> del user:10001 87 | (integer) 1 88 | 127.0.0.1:6379> set user:10002 Jack 89 | OK 90 | 127.0.0.1:6379> del user:10002 91 | (integer) 1 92 | 127.0.0.1:6379> set user:10003 Tony 93 | OK 94 | 127.0.0.1:6379> del user:10003 95 | (integer) 1 96 | ``` 97 | 98 | 上述命令都是些命令,最终是空数据,如果都存储到AOF文件,不仅增大文件体积,而且在恢复数据时会有很多不必要的执行命令。AOF重写机制对持久化的命令做了优化。 99 | 100 | 在AOF重写机制中,首先遍历数据库,如果数据库为空库,则跳过该库。对于非空的数据库,则遍历所有的key,如果key过期则跳过该key,如果key有过期时间则设置key的过期时间。对于不同类型的数据,用不同的、最简单的写命令来保存,比如下面的命令: 101 | 102 | ```shell 103 | # 假设一开始 user 对应的 value 为空 104 | lpush user kevin 105 | 106 | lpush user jack 107 | 108 | lpush user tony 109 | 110 | rpop user 111 | 112 | lpop user 113 | ``` 114 | 115 | 可以重写成: 116 | 117 | ```shell 118 | lpush user jack 119 | ``` 120 | 121 | 这样大大减少了不必要的命令,在存储AOF文件时,文件的体积也会大大减少,而且恢复数据的效率也会大大提升。 122 | 123 | 124 | 125 | AOF的重写机制并不是随机触发的,默认是当前写入AOF文件大小较上次重写后文件大小增长了 `100%` 时就触发重写机制。比如上次重写后的文件大小是1G,经过一段时间,AOF文件体积增长了 `1G` ,那么增长率就是 `100%` ,就会触发AOF重写。可以通过设置 `auto-aof-rewrite-percentage 100` 来调节文件增长率的触发阈值。 126 | 127 | 如果AOF的文件较少,不足以影响太大性能,所以没必要重写AOF文件。可以通过 `auto-aof-rewrite-min-size 64mb` 来设置AOF文件重写的最小阈值。 128 | 129 | 130 | 131 | `aof-load-truncated yes` 参数表示,在Redis恢复数据时,最后一条命令可能不完整,开启则表示忽略最后一条不完整的命令。 132 | 133 | 134 | 135 | --- 136 | 137 |

RDB和AOF对比

138 | 139 | RDB开始压缩后,生成一个体量更小、更紧凑的二进制文件,占有空间小。RDB不仅有着较快的数据恢复能力,而且可以直接复制RDB文件至其他Redis实例进行回复数据,有着良好的容灾体验。RDB适用于定时复制、全量复制、快速恢复的场景,由于RDB文件生成时间长,数据有存储延迟,不适用于近实时持久化的场景。 140 | 141 | 142 | 143 | AOF以追加命令的方式,持续的、近实时的写入文件(秒级别)。而且,在一定程度时,AOF不断的重写持久化文件,不断的优化。特别是在数据完整性上要比RDB好很多,例如,使用默认的数据同步策略,Redis在发生服务器断电等重大事件时,可能只丢失一秒钟的写入时间,或者在Redis进程本身发生错误但操作系统仍在正常运行时丢失一次写入时间。所以AOF比较适合对数据实时性和数据完整性要求比较高的场景。AOF相较于RDB,缺点是文件相对较大,而且恢复数据时因为要执行全部的写命令,所以数据恢复比较慢,特别在是在被依赖的应用系统高负载的情况下,较长时间的数据恢复,对后端系统是不可容忍的。 144 | 145 | 146 | 147 | 但AOF和RDB持久性可以同时启用,不会出现问题。如果在启动时启用了AOF,redis将加载AOF,即具有更好的持久性保证的文件。而且在较新的版本还支持混合模式。 148 | 149 | 150 | 151 | #### RDB-AOF混合持久化 152 | 153 | Redis从 `4.0` 开始支持RDB-AOF混合持久化,默认是关闭状态,可以通过 `aof-use-rdb-preamble yes` 开启。AOF文件在重写之后,将生成一份记录已有数据的RDB快照文件,再生成一份记录最近写命令的AOF文件,作为对RDB快照的补充。这样的混合持久化模式,将兼具RDB和AOF的双优特性。 -------------------------------------------------------------------------------- /resource/markdown/cache/RedisCluster.md: -------------------------------------------------------------------------------- 1 |

Redis集群

2 | 3 | -------------------------------------------------------------------------------- /resource/markdown/cache/Replication.md: -------------------------------------------------------------------------------- 1 |

主从复制

2 | 3 | **数据复制的意义:** 1、读写分离,降低单节点的读写压力;2、容灾转移,单机出现问题,从节点接替。 4 | 5 | Redis 数据复制是单向的,而且一个节点只能从属一个master节点。 6 | 7 | 8 | 9 | 设置主从复制方式一: redis实例启动前,在配置文件中设置 `slaveof host port` 10 | 11 | 设置主从复制方式二: redis实例启动时,使用 `redis-server` 加上 `--slaveof host port` 12 | 13 | 设置主从复制方式三:redis实例启动后,在命令行设置 `slaveof host port` 14 | 15 | 断开主从复制:在从节点命令行执行 `slaveof no one` 16 | 17 | 18 | 19 | #### 一主一从: 20 | 21 | ![一主一从](https://i.loli.net/2019/02/11/5c612b5f58ffc.png) 22 | 23 | 常规的主从配置,一个主节点配置一个从节点。 24 | 25 | 作主备容灾转移时,主节点负责读写,从节点负责复制数据,一旦主节点宕机,从节点晋升为主节点,接替读写工作; 26 | 27 | 做读写分离时,主节点只写,从节点只读。 28 | 29 | 注意,节点最好要开启持久化,否则Redis实例宕机重启后,数据变空。 30 | 31 | 32 | 33 | #### 一主多从: 34 | 35 | ![一主多从](https://i.loli.net/2019/02/11/5c612ba97b585.png) 36 | 37 | 根据“二八原则”,大部分场景下都是在读操作,通常给主节点配置多个从节点,主节点只写,从节点只读,从节点间分摊压力。 38 | 39 | 40 | 41 | #### 树状主从: 42 | 43 | ![树状主从](https://i.loli.net/2019/02/11/5c612bbf22aea.png) 44 | 45 | 但是从节点过多,主节点的IO就越大(主要是IO中的O),这时就会出现IO堵塞。使用树状主从结构,每个主节点的从节点数量就变少,这样主节点的IO输出压力就会变小,非叶子节点的主节点层层分担压力,从而降低根节点的压力。 46 | 47 | (为了能够校正数据,不推荐将从节点设置为只读模式 ~~slave-read-only=yes~~) 48 | 49 | 50 | 51 | --- 52 | 53 |

复制原理

54 | 55 | ![Redis注册复制原理](https://i.loli.net/2019/02/12/5c62226abbf3d.png) 56 | 57 | 首先,通过上述配置主从复制,从节点保存主节点的信息,然后建立 socket 连接。Redis的ping命令是用于客户端检测服务端是否正常运作或消息延时的一种命令,是一种心跳机制,在这里从节点就是客户端,主节点就是服务端。从节点发送ping命令至主节点,如果主节点正常运作则返回pong,这样才能表示双方网络通达。紧接着,如果主节点开启 `requirepass foobared` 参数(foobared 可以认为是安全校验密码),主节点会对从节点进行权限校验。密码正确后,才进行数据同步,最后不断的、陆陆续续的、持续复制后面的数据,实现这一持续复制数据的实现方式就是复制其操作命令。 58 | 59 | 60 | 61 | --- 62 | 63 |

故障转移

64 | 65 | 对于单个节点而言,有可能出现故障(fail),为了保证该服务的可用性,需要使用其他冗余或备用节点来接替该节点工作,这种拯救方式就是 **故障转移(Failover)** 。 66 | 67 | 比如一个 **一主两从** 的高可用方案,主节点工作,从节点作备用: 68 | 69 | ![一主两从](https://i.loli.net/2019/02/16/5c67baed29b4a.png) 70 | 71 | 如果主节点(master节点)出现故障,那么服务不可用,从节点(slave节点)也无法从master节点持续复制数据,如何实现故障转移呢?下面是基于客户端的实现。 72 | 73 | 1. 客户端使用心跳机制,定时检测 master、slave节点活性,比如使用ping命令; 74 | 75 | 2. 如果master节点在一定时间内无回复,则认为master节点此时不可用; 76 | 77 | 3. 从slave节点中随机选择或选择一个ping-pong网络较好的一个节点晋升为master,比如6380节点; 78 | 79 | 4. 6380节点和6381节点先与6379断开复制关系 `slaveof no one`; 80 | 81 | 5. 然后以6380为master节点,6381为salve节点建立复制关系; 82 | 83 | ![建立新的主从复制](https://i.loli.net/2019/02/16/5c67c5a831a15.png) 84 | 85 | 6. 通过心跳检测6379节点故障恢复后,作为salve节点与master节点建立主从复制关系; 86 | 87 | ![屏幕快照 2019-02-16 下午4.10.30.png](https://i.loli.net/2019/02/16/5c67c61676337.png) 88 | 89 | 7. 故障转移完成。(其实到了第3步选取6380为主节点后,服务就可用了) 90 | 91 | 92 | 93 | 客户端为了高可用,也可以做成多节点,在Redis的master节点出现故障时,客户端多节点通过选举方式来产生新的master节点。从Redis 2.8 版本开始,新加入了 Redis Sentinel 来实现高可用。 -------------------------------------------------------------------------------- /resource/markdown/cache/Sentinel.md: -------------------------------------------------------------------------------- 1 |

Redis高可用之Sentinel

2 | 3 | -------------------------------------------------------------------------------- /resource/markdown/cache/SingleThreadModel.md: -------------------------------------------------------------------------------- 1 |

Redis单线程模型为啥这么快?

2 | 3 | ![Single](https://i.loli.net/2019/02/11/5c610bc32d64e.jpeg) 4 | 5 | **1.基于内存操作:** Redis将所有需要存储的数据都存放在内存中,基于内存的随机访问速度是磁盘的10万倍左右,即使是SSD也遥不可及,这是Redis操作快速的重要物理基础。 6 | 7 | 8 | 9 | **2.C语言实现:** 相同逻辑下的C语言程序,执行效率要比其他语言的高很多。C语言与当前主流的操作系统之间有着独特的关系,抛开开发难度来看,执行速度还是蛮快的。 10 | 11 | 12 | 13 | **3.简单的数据结构:** 内存数据库的另一个优点是,基于内存的数据结构要比基于硬盘的数据结构更加简单,对数据的操作也更加简单,因此Redis可以做很多事情,内部复杂性很小。Redis占用的内存空间也是比较少。 14 | 15 | 16 | 17 | **4.多路I/O复用模型:** **多路** 指的的是多个网络连接,**复用** 指的是重复使用一个线程。I/O多路复用技术可以使单个线程处理多个网络连接请求。 18 | 19 | 20 | 21 | **5.单线程模型:** 避免了多线程的启动、销毁、上下文切换、加锁、解锁等消耗资源的操作。单线程简单,不仅能避免上下文切换这种非常消耗资源的操作,而且可以避免死锁的情况。大大提升CPU的利用率。如果是多核服务器,可以通过启动多个Redis实例来利用多个CPU。 -------------------------------------------------------------------------------- /resource/markdown/collection/ArrayList.md: -------------------------------------------------------------------------------- 1 |

本文关注点:

2 | 3 | #### **ArrayList** 4 | #### **Vector** 和 **Stack** 5 | #### `★★★★★`本文重点:ArrayList自动扩容机制(**1.5倍或1.5倍-1** 扩容) 6 | 7 | *** 8 |

一、ArrayList

9 | 10 | > ArrayList是动态数组,其动态性体现在能够`动态扩容` 、`动态缩容`。(其原理是构建一个新Object数组,将原数组复制进去。借助 `Arrays.copyOf(T[] original, int newLength))` 。 11 | 12 | > ![ArrayList继承关系](https://i.loli.net/2018/12/09/5c0cbffb79b69.png) 13 | 14 | #### 重要的属性:ledger: 15 | ```java 16 | // 默认初始容量为 10(当ArrayList的容量低于9时才会用到) 17 | private static final int DEFAULT_CAPACITY = 10; 18 | // 存放数据的数组,数组的每个位置并不一定都填充数据,用transient修饰避免序列化、避免浪费资源 19 | transient Object[] elementData; 20 | // 记录实际存放的元素个数 21 | private int size; 22 | // 记录着ArrayList的修改次数,也就每次add或remove,它的值都会加1 23 | protected transient int modCount = 0; 24 | ``` 25 | 26 | #### 1.1、一般添加元素的方法 public boolean `add(E e)` 27 | ```java 28 | public boolean add(E e) { 29 | // modCount++,并且校验容量,不够用就扩容 30 | ensureCapacityInternal(size + 1); // 增加 modCount 31 | // 将新元素添加到新数组size位置,并将size加1(千万不要理解成将新元素添加到size+1的位置) 32 | elementData[size++] = e; 33 | return true; 34 | } 35 | 36 | // 确保容量够用 37 | private void ensureCapacityInternal(int minCapacity) { 38 | if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 39 | minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); 40 | } 41 | 42 | ensureExplicitCapacity(minCapacity); 43 | } 44 | 45 | private void ensureExplicitCapacity(int minCapacity) { 46 | modCount++; 47 | // 下标溢出表示容量不够用 48 | if (minCapacity - elementData.length > 0) 49 | grow(minCapacity); 50 | } 51 | 52 | ``` 53 | #### `本文重点方法★★★★★` ArrayList 扩容方法 grow(int minCapacity) 54 | ```java 55 | // 扩容方法 56 | private void grow(int minCapacity) { 57 | // overflow-conscious code 58 | int oldCapacity = elementData.length; 59 | // ArrayList 的扩容是 1.5 倍 或 1.5 倍 - 1 60 | // oldCapacity为偶数按1.5倍扩容,oldCapacity为奇数按1.5倍-1扩容 61 | int newCapacity = oldCapacity + (oldCapacity >> 1); 62 | if (newCapacity - minCapacity < 0) 63 | newCapacity = minCapacity; 64 | if (newCapacity - MAX_ARRAY_SIZE > 0) 65 | newCapacity = hugeCapacity(minCapacity); 66 | // 该拷贝为深拷贝,将原来的数组复制到新的数组并返回新的数组 67 | elementData = java.util.Arrays.copyOf(elementData, newCapacity); 68 | } 69 | ``` 70 | 71 | :bomb::bomb::bomb:虽然上面的代码不多,但是添加元素遇到经常扩容的现象要格外留心,因为频繁的扩容势必带来频繁的数组拷贝,这会大大牺牲性能,所以有必要在 `ArrayList` 初始化时指定容量。初始化的容量也不要太大于实际存储容量,不然会造成空间浪费,所以都要权衡利弊。 72 | 73 | #### 1.2、在指定位子添加方法 public void `add(int index, E element)` 74 | 75 | ```java 76 | // 注意:在指定 index 位置添加元素,并不会覆盖该处的元素,而是 index位置及其之后的元素后移 77 | public void add(int index, E element) { 78 | // 只要有index,必定会检查range 79 | rangeCheckForAdd(index); 80 | // 确保容量够用(否则就扩容) 81 | ensureCapacityInternal(size + 1); // Increments modCount!! 82 | // 这里是在指定位置index添加元素的关键:从index处,将数据后移 1 位,空出index处给新元素用 83 | System.arraycopy(elementData, index, elementData, index + 1, 84 | size - index); 85 | // 上一步已将index处的位置空出来,现在将新元素添加到index处 86 | elementData[index] = element; 87 | // 实际存储的元素数加1 88 | size++; 89 | } 90 | ``` 91 | 92 | #### 2.1、删除元素 public boolean `remove(Object o)` 93 | ```java 94 | public boolean remove(Object o) { 95 | // 两种情况,被删除的元素是否为 null 96 | // for 循环遍历、判断、删除(请注意:千万不要使用 foreach 进行删除!!!) 97 | if (o == null) { 98 | for (int index = 0; index < size; index++) 99 | if (elementData[index] == null) { 100 | fastRemove(index); 101 | return true; 102 | } 103 | } else { 104 | for (int index = 0; index < size; index++) 105 | if (o.equals(elementData[index])) { 106 | fastRemove(index); 107 | return true; 108 | } 109 | } 110 | // 如果删除了,返回为true,否则返回false。 111 | return false; 112 | } 113 | 114 | /* 115 | * Private remove method that skips bounds checking and does not 116 | * return the value removed. 117 | * 跳过边界检查的私有移除方法,并且不返回删除的值 118 | */ 119 | private void fastRemove(int index) { 120 | modCount++; 121 | int numMoved = size - index - 1; 122 | if (numMoved > 0) 123 | // 重点在这里,数据向前移动 124 | System.arraycopy(elementData, index+1, elementData, index, 125 | numMoved); 126 | // 清除 size-1 位置上的元素(设置空引用),让GC去收集该元素,同时 size 减 1 127 | elementData[--size] = null; 128 | } 129 | ``` 130 | 131 | :bomb::bomb::bomb:注意: 132 | 133 | 1. 如果存在多个对象o,仅仅删除距离 index = 0 处最近的一个元素; 134 | 2. 元素被删除后,该位置不会空出来,后面的元素会前移。 135 | 136 | 所以 `ArrayList` 适合读多删少场景。 137 | 138 | 139 | 140 | #### 3、重新设置指定index位置的元素值 public E `set(int index, E element)` 141 | 142 | ```java 143 | public E set(int index, E element) { 144 | // 有index,必检查 145 | rangeCheck(index); 146 | // 返回旧的(被重置的)元素 147 | E oldValue = elementData(index); 148 | // 直接把 index位置的元素覆盖掉行了 149 | elementData[index] = element; 150 | return oldValue; 151 | } 152 | ``` 153 | 154 | #### 4、获取指定index位置的元素 public E `get(int index)` 155 | ```java 156 | public E get(int index) { 157 | // 还是那句话,凡是遇到index索引,必检查 158 | rangeCheck(index); 159 | // 直接返回index的元素,数组查询还不快吗。 160 | return elementData(index); 161 | } 162 | 163 | E elementData(int index) { 164 | // 因为 ArrayList 本质上是个数组,直接通过index获取元素 165 | return (E) elementData[index]; 166 | } 167 | ``` 168 | 169 | #### 5、判断元素是否存储 public boolean `contains(Object o)` 判断是否存在某个元素 170 | ```java 171 | public boolean contains(Object o) { 172 | // 简单粗暴,直接遍历查看index是否大于0 173 | return indexOf(o) >= 0; 174 | } 175 | 176 | // 查找元素是否存在,如果存在返回下标,否则返回 -1 177 | public int indexOf(Object o) { 178 | // 遍历的时候分两种情况,是否为 null 179 | if (o == null) { 180 | for (int i = 0; i < size; i++) 181 | if (elementData[i]==null) 182 | return i; 183 | } else { 184 | for (int i = 0; i < size; i++) 185 | // 用 equals 判断值是否相等 186 | if (o.equals(elementData[i])) 187 | return i; 188 | } 189 | return -1; 190 | } 191 | ``` 192 | 193 | *** 194 |

二、ArrayList的亲兄弟 Vector

195 | 196 | > Vector 的大部分方法是被 `synchronized` 修饰的。 197 | 线程安全的ArrayList,几乎所有的方法都加 `synchronized` 修饰; 198 | 199 | *** 200 |

三、Stack

201 | 202 | > Stack 继承至 Vector,是一个`后进先出LIFO`的队列。 203 | 它实现了一个标准的 `后进先出LIFO` 的栈。 -------------------------------------------------------------------------------- /resource/markdown/collection/BinaryTrees.md: -------------------------------------------------------------------------------- 1 | ![tree](https://i.loli.net/2018/12/09/5c0cc0541fdb9.jpg) 2 | 3 |

一、树

4 | 5 | > 浩瀚宇宙,从 **点** 到**线**,**线**到**面**,**面**到**体**,组成了丰富多彩的世界。动物、植物和其他物体正是存储在 **宇宙** 这个数据结构中。 6 | > 7 | > 前几篇[文章](https://github.com/about-cloud/JavaCore)介绍了 **线性** 的数据结构: `ArrayList`、`LinkedList`及其组合`HashMap`。从 **线性** 数据结构到 **非线性** 数据结构,就像 **树干** 发叉一样: 8 | 9 | ![line to tree](https://i.loli.net/2018/12/09/5c0cc530aff25.png) 10 | 11 | > 从数据结构进化的角度来看,:palm_tree:**树(tree)** 的生成是 **链表** 进化而来。**链(Linked)** 每个节点最多只有一个 **前驱节点** ,那么可以称这个 **链** 为 🌴**树(tree)** 。数据结构中的树就像一颗倒挂的树。 12 | > 13 | > **链表** 只不过是特殊的树,就像树没有树枝只有树干一样。 14 | 15 | #### 树的基本术语 16 | 17 | 1. **节点(Node)**:树中的每个元素称为 节点(Node); 18 | 2. **根节点(Root Node)**:树最顶端的节点被称为根节点(树根),根节点无父节点; 19 | 3. **父节点(Parent Node)**:节点的(上面的)前驱节点在树中被称为父节点; 20 | 4. **子节点(Child Node)**:(下面的)后继节点被称为子节点; 21 | 5. **子树(Subtree)**:子节点及其下面的节点组成的树称为子树 ; 22 | 6. 一棵树最多只有一个根节点,树中的每个子节点最多只有一个父节点; 23 | 7. **节点的度**:一个节点含有的子树的个数称为该节点的度(叶子节点的度为0); 24 | 8. :leaves:**叶节点(Leaf Node)**(叶子节点或终端节点):度为0的节点称为叶节点(也就是没有子节点的节点称为叶子节点); 25 | 9. **非终端节点** 或分支节点:度不为0的节点(拥有子节点的节点); 26 | 10. **兄弟节点**:具有相同父节点的节点互称为兄弟节点; 27 | 11. **树的度**:一棵树中,节点中最大的度称为树的度; 28 | 12. **节点的层次**:从根开始定义起,**根为第1层**,根的子节点为第2层,以此类推; 29 | 13. **树的高度或深度**:树中节点的最大层次; 30 | 14. **堂兄弟节点**:双亲在同一层的节点互为堂兄弟; 31 | 15. **节点的祖先**:从该节点上溯至根节点,所经分支上的所有节点; 32 | 16. 子孙节点:该节点下面所有所有的节点称为该节点的子孙节点; 33 | 17. 空树:没有节点的树称为空树; 34 | 18. **森林**:由多棵互不相交(没有重叠节点)的树组成的集合称为森林(单棵树可以认为是特殊的森林); 35 | 36 | 37 | 38 |

二、二叉树

39 | 40 | > **二叉树**是每个节点最多只有两个子树的树。子树通常被称为**左子树(Left subtree)**和**右子树(Right subtree)**。 41 | 42 | ![BinaryTree](https://i.loli.net/2018/12/09/5c0ccab8c3956.png) 43 | 44 | #### 二叉树的特性 45 | 46 | 1. 在非空二叉树的 `i` 层上,至多有 **$$2^{i-1}$$**个节点`(i>=1)`; 47 | 2. 在深度为 `d` 的二叉树上最多有 `2d-1` 个结点`(d>=1)`; 48 | 3. 对于任何一棵非空的二叉树,如果叶节点个数为 `n0`,度数为 `2` 的节点个数为 `n2`,则有: `n0 = n2 + 1`。 49 | 50 | #### 二叉树的遍历 51 | 52 | > **二叉树遍历**:从树的根节点出发,按照某种 **次序** 依次访问二叉树中所有的节点,使得每个节点被访问仅且一次。 53 | 54 | 这里有两个关键词:**访问** 和 **次序**。**访问** 包括 **增删改查**, **次序** 包括 **前序**、**中序**、**后序**,`前中后`是以树的根节点作为参照物:triangular_flag_on_post:,又规定不管如何遍历 **左子树** 的访问顺序要排在 **右子树** 前面(左边)。 55 | 56 | #### 1、前序遍历(先序遍历) 57 | 58 | > 遍历步骤:先访问根节点,然后再先序遍历左子树,最后再先序遍历右子树即 **根—左—右**。 59 | 60 | ![先序遍历](https://i.loli.net/2018/12/09/5c0cd0ee63aa2.png) 61 | 62 | > 上图先序遍历的结果: 63 | 64 | #### 2、中序遍历 65 | 66 | > 遍历步骤:先中序遍历左子树,然后再访问根节点,最后再中序遍历右子树即 **左—根—右**。 67 | 68 | ![中序遍历]() 69 | 70 | > 上图中序遍历的结果: 71 | 72 | #### 3、后序遍历 73 | 74 | > 遍历步骤:先后序遍历左子树,再后序遍历右子树,最后再访问根节点,即 **左—右—根**。 75 | 76 | ![后序遍历]() 77 | 78 | > 上图后序遍历的结果: 79 | 80 | 81 | 82 |

三、最优二叉树(哈夫曼树)

83 | 84 | 85 | 86 | 87 | 88 |

三、平衡二叉树

89 | 90 | 平衡二叉树(Balanced Binary Tree)的 **平衡性** 体现在以下几点: 91 | 92 | 1. 树的左、右子树的 **高度差** 的绝对值不超过1; 93 | 2. 任意节点左、右子树也分别平衡二叉树; 94 | 95 | 一棵数频繁的删除和插入节点都有可能使数发生旋转。平衡二叉树中访问节点的操作(插入、查找、删除)的时间复杂度维持在一个“平衡”的水准,既最坏情况和最好情况的时间复杂度维持在 `O(logN)`。 96 | 97 | ![平衡二叉树]() 98 | 99 | 100 | 101 |

四、二叉查找树

102 | 103 | 二叉查找树(二叉搜索树 Binary Search Tree)的 **查找** 特性体现在以下几点: 104 | 105 | 1. 如果它的左子树不空,则左子树上所有节点的值均小于它的根节点的值; 106 | 2. 如果它的右子树不空,则右子树上所有节点的值均大于它的根节点的值; 107 | 3. 任意节点左、右子树也分别为二叉查找树。 108 | 109 | ![先序遍历]() 110 | 111 | #### 二叉查找树的查找过程 112 | 113 | 1. 从树的根节点开始查找,并沿着这棵树中的一条简单路径向下进行; 114 | 2. 若树为空树,则查找失败,返回 `null`; 115 | 3. 对于访问的每个节点 `x`,若指定的值 `k` 等于结点 `x` 的值,返回该节点`x` 并结束查找; 116 | 4. 若指定的值 `k` 小于节点 `x` 的值,则在节点 `x` 的左子树中查找(根据上面二叉查找树的性质判断); 117 | 5. 相反,若指定的值 `k` 大于节点 `x` 的值,则查找在节点 `x` 的右子树中继续; 118 | 6. 若查找至 **叶子节点** 后仍未匹配到相等值,则表示不存在指定值的节点,返回null。 119 | 120 | 121 | 122 |

五、AVL树(高度平衡树)

123 | 124 | **AVL树** 得名于它的两位发明者G. M. Adelson-Velsky和E. M. Landis的名字,树的特性是: 125 | 126 | 1. :wink:本身首先是一棵二叉查找树; 127 | 2. 每个节点的左右子树的高度之差的绝对值(平衡因子)最多为 `1` ; 128 | 129 | ![AVL]() 130 | 131 | ALV追求极限的平衡性,优点是在查找元素时,时间复杂度低,查找元素快。那么问题来了,在添加/删除元素时,AVL树为了追求“绝对的平衡性”,不断的通过循环来达到自平衡,增加时间复杂度。适所以AVL树适用于添加/删除元素少、查找操作多的环境下。 132 | 133 |

六、红黑树

134 | 135 | **红黑树(Red Black Tree)** 也是一种 **平衡二叉树**(而平衡二叉树又是一种二叉查找树,所以红黑树也是一种二叉查找树),但它不像 **AVL树** 那样追求绝对的平衡,它这种于查找和添加/删除之间。特性是: 136 | 137 | 1. 树中所有节点都被染成红色🔴或黑色⚫️(官方话语:节点是红色或黑色); 138 | 2. 根节点和叶子节点都是黑色⚫️ 139 | 3. 每个红色节点🔴的两个子节点都是黑色⚫️(从每个叶子到根的所有路径上**不能**有两个连续的红色节点🔴); 140 | 4. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点⚫️。 141 | 142 | ![红黑树]() 143 | 144 | 上述特性构成了一个最关键的约束:从根节点到叶子节点的最长路径不大于最短路径的两倍长。 145 | 146 | 所以红黑树可以不是那么绝对的高度平衡。对于在树中添加/删除元素,红黑树也不像AVL树那样,为了达到绝对的自平衡而“使劲费力”地旋转整颗树了。 147 | 148 | 149 | 150 | 最差平衡二叉树即树退化成链表(可以看成特殊的树、没有叉的树),这样的树添加/删除元素快,不需要自平衡,但是查找慢,最坏情况要遍历整棵树。而从增删改查的时间复杂度来讲,**红黑树** 介于 **二叉查找树** 和 **链表** 之间,这是在添加/删除和查找之间一个折中的选择。 151 | 152 | #### 总结: 153 | 154 | 链表:适用于添加/删除多、查找少的场景; 155 | 156 | 二叉查找树:适用于查找多、添加/删除少的场景; 157 | 158 | 红黑树:适用于查找、添加、删除都多的场景。 159 | 160 | 161 | 162 |

七、B-Tree、B+Tree、B*Tree

163 | 164 | > 这种和MySQL的[InnoDB索引](https://github.com/about-cloud/JavaCore)息息相关,单独拿出一个章节去讲解,详见: 165 | > 166 | > https://github.com/about-cloud/JavaCore -------------------------------------------------------------------------------- /resource/markdown/collection/HashConflictsAndResolve.md: -------------------------------------------------------------------------------- 1 |

一、Hash冲突(哈希碰撞💥)

2 | 3 | #### 1.1、什么是冲突? 4 | 5 | ![排椅](https://images.unsplash.com/photo-1474895652670-fc6493d963f1?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=aa18e019455dd04ef8b4ae30321c0ed0&auto=format&fit=crop&w=800&q=60) 6 | 7 | > 在一间教室里有一排座椅,这一排座椅线性排列对应的数据结构是 **数组**,数组的特点是可以根据下标(索引)快速🔜访问,通过下标(索引)访问的位置 `table[i]` 称为 **槽(slot)**,在伴随着 `哈希算法` 计算的槽称为 **哈希槽**。假设这排座椅共16个座位💺,编号从0,1,2…开始直到15。学校有很多学生,他们的编号也是从0,1,2…起始,有很多同学喜欢坐在一起,学校🏫为了让他们散列分开坐,采用 `模运算` 的方式。比如 1号同学的对座椅长度16模运算得1,那么你坐在1号位置,5号同学对座椅长度16模运算得5,那么他坐在5号位置,以此类推。1号座位已经被1号同学占用,新来的17号同学对16作模运算也得1,按照原来的算法,他也应该坐在1号,此时17号同学和1号同学的座位💺就 **冲突**。 8 | 9 | #### 1.2、哈希算法与哈希冲突 10 | 11 | > 实际上 **“编号”** 多种多样,如果它们共有某种特性、聚集密度增加,**冲突** 概率就有可能增大,为了防止这些 **项(元素)** 过于集中,而将它们按照某种算法 **散列** 分布,这种算法称为 **Hash算法**,翻译成**散列算法**,根据音译常翻译成**哈希算法**,通过 哈希算法 计算后的值称为**哈希码**或 **散列码**(HashCode)。假如 `1.1` 中学生的编号不是数字,而是其他字符组合,比如学生周星星的编号是 `ZXX9573` ,非 `Number` 类型的字符串是不能进行数学模运算的,即使能使用 `模运算` 也不能最大化的将他们散列开。所以使用 **哈希算法** 计算出 **哈希码**,然后根据模运算来决定它们该进入哪个 **槽**,这里的槽也称为 **哈希槽**。 12 | > 13 | > 这个通过 **散列** 方式存储元素的阵列容器称为 **散列表** (hashtable),不过大多数人喜欢叫它**哈希表**。 14 | > 15 | > 如果跟据 **哈希算法** 计算得到的 **哈希码** 有多个相同的,那么模运算后得到的 **哈希槽** 也一定相同,也就产生了新的冲突,这种冲突称为 **哈希冲突(哈希碰撞💥)**。 16 | 17 | ![哈希冲突](https://images.pexels.com/photos/162943/aviary-pigeons-birds-feather-162943.jpeg?auto=compress&cs=tinysrgb&h=350) 18 | 19 |

二、哈希冲突解决算法

20 | 21 | #### 2.1、开放定址法(open addressing) 22 | 23 | > 一个元素与另一个元素关于某个地址(或槽)发生了冲突,将开放其他地址给此元素使用,这种算法称**开放定址法** 或 **开放地址法**。如何寻找适合的开放地址给此元素使用,这种寻找的过程或者技术称为**探测技术**。常见的 **“寻找技术”** 有如下几种: 24 | 25 | ##### 线性探测 26 | 27 | > 接上面的故事,假设 1号座位已经有人,然后根据哈希算法计算出 `周星星` 应该落到 1号 座位,这时出现哈希冲突,然后按照线性方式探测下 1 个座位(哈希槽),如果2号座位(哈希槽)也有人,然后再按照线性方式探测下 1 个座位(哈希槽)... 以此类推。此种方式就是 **线性探测**。其中 “下 1 个” 指的是 **步长**(也称为**增量**),你也可以将步长定为2,那就是下2个。实际上根据步长计算出的一批**地址**,这批地址称为**地址序列**。 28 | 29 | ##### 二次探测(平方探测) 30 | 31 | >将步长(增量)改为 ![i = $$1^2$$, $$-1^2$$, $$2^2$$, $$-2^2$$, $$3^2$$, $$-3^2$$,…](https://i.loli.net/2018/12/11/5c0f5d1037952.gif) 这种增量序列。从 **增量i** 可以看出正负交替,特点是在探测时左右↔️跳跃式探测。 32 | 33 | ##### 伪随机探测 34 | 35 | > 原来线性探测的步长都是已知数,现在将每个步长改为随机获取。实际过程也是随机产生一批随机数,也就是 **随机序列**。 36 | 37 | #### 2.2、再哈希法 38 | 39 | >再哈希法是有多个哈希函数(算法),当第一次产生哈希冲突时,就使用第二个哈希函数,如果还产生哈希冲突就使用第三个,以此类推,再次哈希,直达无冲突,这样的做法会增加哈希计算时间。 40 | 41 | #### 2.3、:anchor:链地址法(链表法):heavy_check_mark: 42 | 43 | >当多个元素产生哈希冲突时,它们的哈希码是一定相同,落脚的哈希槽也相同。将它们通过地址引用一一相连,也就是形成 **链表** 的形式。这样它们既可以存储,又不会占用其他哈希槽的位置。这种通过**链表**形式解决**哈希冲突**的算法称为**链表法**。由不同元素、相同哈希码经过组织形成的数据结构称为 **哈希桶**Bucket,或者说这个容器是 **哈希桶**。**哈希槽**的位置也就是**哈希桶号** 。 44 | 45 | #### 2.4、建立公共溢出区 46 | 47 | >为所有产生哈希冲突的元素建立一个公共溢出区域来存放溢出的元素。在查找时,如果通过计算发现对应的哈希槽处没该元素,则进入**公共溢出区** 进行查找,溢出的元素相对于 **散列表** 来说是比较小的。原来的表称为**基本表**,公共溢出区又称**溢出表**。 -------------------------------------------------------------------------------- /resource/markdown/collection/HashMap1.7v.md: -------------------------------------------------------------------------------- 1 | > 本文是基于 `jdk1.7.0_79` 分析 2 | 3 |

一、继承关系

4 | 5 | ![HashMap1.7vExtend](https://i.loli.net/2019/02/20/5c6d2069aef34.png) 6 | 7 |

二、数据结构

8 | 9 | >`jdk1.7` **HashMap**的底层数据结构:**数组** + **单向线性链表**。 10 | > 11 | >HashMap的元素就是在 Map.Entry 的基础上实现的Entry项。 12 | > 13 | >上一节分析了 **哈希冲突** 和 解决哈希冲突的算法,**HashMap** 就是基于 **链表法** 来解决哈希冲突的。 14 | 15 | ![jdk1.7 HashMap图片](https://i.loli.net/2018/12/13/5c11b3dab0979.png) 16 | 17 | #### 重要属性(请记住这些常量和变量的含义,下面源码分析会用到) 18 | 19 | ```java 20 | /** 默认初始容量 16 */ 21 | static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 既 16 22 | /** 最大容量 10.7亿+(这里也用到效率高的位运算) */ 23 | static final int MAXIMUM_CAPACITY = 1 << 30; 24 | /** 默认负载因子 0.75 */ 25 | static final float DEFAULT_LOAD_FACTOR = 0.75f; 26 | /** 当 table 未填充的时候,共享这个空table */ 27 | static final Entry[] EMPTY_TABLE = {}; 28 | /** (这个表又称为基本表)该 table 根据需要而调整大小。长度必须始终是2的次幂。 */ 29 | transient Entry[] table = (Entry[]) EMPTY_TABLE; 30 | /** map 中 存储 key-value 映射的数量 */ 31 | transient int size; 32 | /** 下次调整大小时的阈值(容量 * 负载因子) */ 33 | int threshold; 34 | /** 哈希表的负载因子 */ 35 | final float loadFactor; 36 | /** 此 HashMap 修改的次数 */ 37 | transient int modCount; 38 | /** 默认容量的最大值:Integer.MAX_VALUE */ 39 | static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE; 40 | ``` 41 | 42 | `HashMap` 重点元素 **项Entry**:之前文章已讲解了 `Map.Entry` 接口,下面就来分析一下 `jdk1.7` `HashMap`实现`Map.Entry` 的 **Entry**: 43 | 44 | ```java 45 | static class Entry implements Map.Entry { 46 | // final 修饰的 key,防止被重复赋值 47 | final K key; 48 | // 可被重复设置值的value 49 | V value; 50 | // 此项的下一项(用于链表。并没有类四的 Entry prev,说名是单链表) 51 | Entry next; 52 | int hash; 53 | 54 | /** 55 | * 构造方法用于注入 entry项 的属性值(或引用) 56 | * 参数从左至右依次是:key的哈希码,key,value,指向的下一个entry 57 | */ 58 | Entry(int h, K k, V v, Entry n) { 59 | value = v; 60 | next = n; 61 | key = k; 62 | hash = h; 63 | } 64 | // getter & toString 方法 65 | public final K getKey() { 66 | return key; 67 | } 68 | public final V getValue() { 69 | return value; 70 | } 71 | public final String toString() { 72 | return getKey() + "=" + getValue(); 73 | } 74 | 75 | // 设置节点的新值value 76 | public final V setValue(V newValue) { 77 | V oldValue = value; 78 | value = newValue; 79 | return oldValue; 80 | } 81 | 82 | // 比较节点的方法 83 | public final boolean equals(Object o) { 84 | // 检查类型 85 | if (!(o instanceof Map.Entry)) 86 | return false; 87 | Map.Entry e = (Map.Entry)o; 88 | // 当前项的key 89 | Object k1 = getKey(); 90 | // 被比较对象的key 91 | Object k2 = e.getKey(); 92 | // 这里说明一下 Object,它的 equals方法很简单,就是用 == 来做判断的 93 | if (k1 == k2 || (k1 != null && k1.equals(k2))) { 94 | Object v1 = getValue(); 95 | Object v2 = e.getValue(); 96 | if (v1 == v2 || (v1 != null && v1.equals(v2))) 97 | return true; 98 | } 99 | return false; 100 | } 101 | 102 | // 返回节点的哈希码 103 | public final int hashCode() { 104 | return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); 105 | } 106 | 107 | /** 108 | * 当entry被访问时,都会调用此方法 109 | * 这里只不过是一个空方法 110 | */ 111 | void recordAccess(HashMap m) { 112 | } 113 | 114 | /** 115 | * 每当entry项从表格中删除时,都会调用这个空方法 116 | */ 117 | void recordRemoval(HashMap m) { 118 | } 119 | } 120 | ``` 121 | 122 | 123 | 124 |

三、添加元素项

125 | 126 | ```java 127 | public V put(K key, V value) { 128 | // 判断是否为空表 129 | if (table == EMPTY_TABLE) { 130 | inflateTable(threshold); 131 | } 132 | if (key == null) 133 | // 如果key为null的情况下,将将键值对放在table[0]处 134 | // 如果table[0]已存在元素,则将替换value 135 | return putForNullKey(value); 136 | // key的哈希值 137 | int hash = hash(key); 138 | // 可以的哈希码对表的长度模运算,计算并得到哈希槽的位置 139 | int i = indexFor(hash, table.length); 140 | // 对哈希桶(链表)进行遍历,靠指针地址移动 141 | // 查找是否包含key的项,如果包含就将value换掉 142 | for (Entry e = table[i]; e != null; e = e.next) { 143 | Object k; 144 | // 该元素的哈希码与新增项的key的哈希码项相等,并且 key 也相同 145 | // 那么就会替换 value(因为key具有唯一性,相同的key要替换value) 146 | if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 147 | // 被替换的旧值 148 | V oldValue = e.value; 149 | // 选用新的value 150 | e.value = value; 151 | // 调用上面的空方法 152 | e.recordAccess(this); 153 | // 返回旧值 154 | return oldValue; 155 | } 156 | } 157 | 158 | // 如果上面for循环没有查找到key的存在(或者说没有找到相同的key),那么就新添加一项 159 | modCount++; // modCount加1 160 | // 添加entry项 161 | addEntry(hash, key, value, i); 162 | return null; 163 | } 164 | 165 | /** 166 | * 将具有指定key、value、key的哈希码、桶号(也就是哈希槽的位置)的条目(元素)(项) 167 | * 添加到指定的桶(哈希桶)。 168 | * 此方法会在适当的情况下调整桶的大小 169 | * 子类也可以重写此方法来改变put添加的行为 170 | */ 171 | void addEntry(int hash, K key, V value, int bucketIndex) { 172 | // 如果已添加元素项的实际大小达到了HashMap所承载的阈值,并且哈希槽的位置不为空 173 | // 那么就进行扩容 174 | if ((size >= threshold) && (null != table[bucketIndex])) { 175 | // 扩容后的大小是2倍于原基本表的长度 176 | resize(2 * table.length); 177 | // 因为基本表已经扩容,那么对key重新计算哈希值 178 | // 如果 key 不为 null 那么计算key的哈希值,否则哈希值直接记为0 179 | hash = (null != key) ? hash(key) : 0; 180 | // 根据哈希码和基本表(数组)长度计算桶号 181 | // indexFor 方法并没有使用模运行,而是使用高性能的逻辑与&运算 182 | bucketIndex = indexFor(hash, table.length); 183 | } 184 | 185 | createEntry(hash, key, value, bucketIndex); 186 | } 187 | ``` 188 | 189 | #### :triangular_flag_on_post:创建元素项的方法: 190 | 191 | ```java 192 | /** 193 | * 此方法与 addEntry 不同,只有在创建 entry项的时候使用此方法 194 | * 作为构建(或伪构建) Map 的一部分。"伪构建" 是指 克隆、反序列。 195 | * 使用此方法是不必担心扩容问题。 196 | * 子类可以重新此方法改变方法的功能,比如将单向链表改成双向链表等等。 197 | */ 198 | void createEntry(int hash, K key, V value, int bucketIndex) { 199 | // 原位置的元素项要下沉,新的元素放在哈希槽(桶顶)的位置 200 | Entry e = table[bucketIndex]; 201 | // 构造新的元素项放在哈西槽(桶顶),同步把原有的元素项链入其后 202 | table[bucketIndex] = new Entry<>(hash, key, value, e); 203 | // 实际大小加1 204 | size++; 205 | } 206 | ``` 207 | 208 | > 链表的时候讲要添加的元素项放到**桶顶**,那么先进的元素项位于链表的尾部,后进的元素项位于链表的头部。(这只是**本版本** `HashMap` 的实现方式) 209 | 210 | ![HashMap解决哈希冲突](https://i.loli.net/2018/12/13/5c11b46a0d68a.png) 211 | 212 | 213 | 214 | #### :heavy_plus_sign::heavy_plus_sign::heavy_plus_sign:扩容机制 215 | 216 | ```java 217 | /** 218 | * 扩容方法就是给 map 换一个更大容量的新数组。 219 | * 前提前提条件是已添加元素项的实际大小达到了HashMap所承载的阈值。 220 | * 221 | * 如果当前容量达到最大容量, 此方法也能给map扩容。但是可以设置阈值为Integer类型的最大值。 222 | * 223 | * @param newCapacity 新的容量, 必须是2的次幂; 224 | * 必须大于当前容量,除非当前容量达到了最大值。 225 | * (在这种情况下值是无关紧要的) 226 | */ 227 | void resize(int newCapacity) { 228 | // 当前table数组 229 | Entry[] oldTable = table; 230 | // 当前table数组长度 231 | int oldCapacity = oldTable.length; 232 | // 如果当前数组达到了最大容量(1<<30),那么赋值阈值为Integer的最大值,并直接返回🔙 233 | if (oldCapacity == MAXIMUM_CAPACITY) { 234 | threshold = Integer.MAX_VALUE; 235 | return; 236 | } 237 | // new 新的数组 238 | Entry[] newTable = new Entry[newCapacity]; 239 | // 将当前的数组上索引元素项转移到新的数组上 240 | transfer(newTable, initHashSeedAsNeeded(newCapacity)); 241 | // 当前数组指向新数组,完成扩容 242 | table = newTable; 243 | // 计算阈值 244 | threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); 245 | } 246 | 247 | /** 248 | * 将当前的数组上索引元素项转移到新的数组上 249 | */ 250 | void transfer(Entry[] newTable, boolean rehash) { 251 | // 新数组的长度(容量) 252 | int newCapacity = newTable.length; 253 | // 首先横向遍历数组 254 | for (Entry e : table) { 255 | // 然后纵向遍历链表 256 | while(null != e) { 257 | // 链表中提前获取下一个元素项(节点) 258 | Entry next = e.next; 259 | if (rehash) { 260 | e.hash = null == e.key ? 0 : hash(e.key); 261 | } 262 | // 重新计算存放元素 263 | int i = indexFor(e.hash, newCapacity); 264 | e.next = newTable[i]; 265 | newTable[i] = e; 266 | e = next; 267 | } 268 | } 269 | } 270 | ``` 271 | 272 | :dart::dart::dart: 上面已经看出每次扩容时都伴随着所有元素项的重新 **选址** 存放,这不仅大大牺牲性能,而且耗时。所以在使用 `HashMap` 一定要评估存放元素项的数量,指定map的大小。 273 | 274 | 275 | 276 |

四、删除元素项

277 | 278 | > 删除元素项的的思路基本和添加操作相似,只不过一个是添加,一个是删除。先根据 `key` 计算 `哈希槽`(桶的位置),然后循环链表比较判断,这里参考:telescope:之前关于 `LinkedList` 文章的删除节点操作吧。 279 | 280 |

其他问题

281 | 282 | #### 为什么 HashMap 的容量必须是2的次幂呢:question: 283 | 284 | > `key`的哈希码对 基本表 做 **逻辑与** 运算 **h&(length-1)**,来确定此元素项的数组上的位置。 原因就出在 **二进制** 的 **与&** 操作上了。 285 | > 286 | > **与&** 运算规则:`0&0=0`; `0&1=0`; `1&0=0`; `1&1=1`; 287 | 288 | | (指数形式)length | (十进制)length | (十进制)length - 1 |(二进制)length - 1| 289 | | ---------------- | ------------------ | -------------------- |--| 290 | | [$$1^2$$](https://i.loli.net/2018/12/13/5c11b8a163aba.gif) | 2 | 1 | 1 | 291 | | [$$2^2$$](https://i.loli.net/2018/12/13/5c11b8e5ba8bf.gif) | 4 | 3 | 11 | 292 | | [$$3^2$$](https://i.loli.net/2018/12/13/5c11b90318c7a.gif) | 8 | 7 | 111 | 293 | | [$$4^2$$](https://i.loli.net/2018/12/13/5c11b92c4af9f.gif) | 16 | 15 | 1111 | 294 | | $$...^2$$ | ... | ... | ... | 295 | 296 | > 这种情况下,`length - 1` 二进制的 **最右位** 永远是 `1`。这样 `0 & 1 = 0`,`1 & 1 = 1` 的结果既有 `0` 又有`1`。对于 **哈希槽** 的二进制最右位为 `1` (十进制的奇数)的位置就有可能被填充,而不至于浪费存储空间。 297 | > 298 | > 如果 `HashMap` 的容量不是 `2` 的次幂,比如 容量length为 `19` ,`length - 1` 的二进制为 `10010`,任何常数与之作 **与&** 运算,二进制最右位都是 `0`,那么只能存放 (十进制)尾数为 `偶数` 的数组位置,这样会大大浪费空间。 -------------------------------------------------------------------------------- /resource/markdown/collection/HashMapHashtableConcurrentHashMap.md: -------------------------------------------------------------------------------- 1 | :oncoming_police_car::oncoming_police_car::oncoming_police_car: 2 | 3 |

一、非线程安全的HashMap

4 | 5 | > :clapper:前面的[章节](https://github.com/about-cloud/JavaCore/)已经从源码分析了 `HashMap` 的实现,这里简单回顾一下,如想详细了解,[请参考这里](https://github.com/about-cloud/JavaCore)。 6 | > 7 | > * `jdk1.7` 的 `HashMap` 的底层实现是 **数组** + **单向链表**,支持key为 `null`,value 为`null`。 8 | > * 它的初始容量是 `16`,负载因子默认值 `0.75`,当实际存放元素数量大于等于 **阈值**,(而且新元素被放入的哈希槽不为空),那么就会触发扩容,每次扩容时的容量都是翻倍增长(一定是 `2`的次幂),所以在实例存储元素项较多的情况下,一定要指定初始容量,避免每次扩容带来性能上的影响。 9 | > * **负载因子** 取值问题也是一个重要原因,取值越大**哈希冲突** 的概率就越高,取值越小空间浪费度就越高。而 `0.75` 是一个比较折中的选择。 10 | > * 其属性、方法、代码块没有被 `synchronize`修饰,也没有使用其他同步机制,在多线程环境下是 **非线程安全**的。 11 | 12 | 13 | 14 |

二、线程安全的Hashtable

15 | 16 | > `Hashtable` 是`HashMap` 线程安全的实现。它也起始于 **上古时期**,可追溯到` jdk1.0`。(:no_good:注意是 `Hashtable` 而非 ~~HashTable~~) 17 | 18 | ```java 19 | public class Hashtable 20 | extends Dictionary 21 | implements Map, Cloneable, java.io.Serializable 22 | ``` 23 | 24 | > `Hashtable` 扩展至 `Dictionary` 抽象类、实现至`Map` 接口。 25 | 26 | * `Hashtable` 线程安全的实现机制是 **几乎**在所有的方法都加:poop: `synchronized` 修饰; 27 | * `HashMap` 允许 key 和 value 都可以是 `null` 值,但 `Hashtable` 对 key 、value都不允许:sob::broken_heart:为 ~~null~~; 28 | * `Hashtable` 初始容量是`11` ,默认负载因子也是 `0.75`。扩容是 `2倍 + 1`; 29 | 30 | 处处加锁:lock:的`Hashtable` 虽然保证了同步性,但性能大大折扣,再并发的情况下有些不可取。 31 | 32 | `jdk1.5` 又引入了新的线程安全容器:sunny: `ConcurrentHashMap`,用于替代性能差的 `Hashtable`。`ConcurrentHashMap` 为什么能够取代 `Hashtable`呢?因为它在保证性能安全的情况下、性能还比`Hashtable`好。 33 | 34 | 35 | 36 | :suspension_railway::suspension_railway::suspension_railway: 37 | 38 |

三、线程安全的ConcurrentHashMap

39 | 40 | > 面对着 `Hashtable` 粗暴的大锁:lock:,`ConcurrentHashMap` 采用 **分段锁技术**,将一个大的Map分割成n个小的 **段segment**,对每段进行加锁,降低了容器加锁的粒子度,每段(segment)各自加锁,互不影响。分段锁使用的锁是 `ReentrantLock` 可重入锁。 41 | > 42 | > (:dart:分段锁使用ReentrantLock锁其实是无锁的一种实现,[可参关于 CAS和AQS的文章](https://github.com/about-cloud/JavaCore)) 43 | > 44 | > `ConcurrentHashMap` 也是不允许 key 、value😭💔为 ~~null~~; 45 | > 46 | > :fuelpump:[其他的更多实现和源码分析详见后面的文章](https://github.com/about-cloud/JavaCore) 。 -------------------------------------------------------------------------------- /resource/markdown/collection/JavaCollections.md: -------------------------------------------------------------------------------- 1 | > 什么是 **程序** ?**程序** 就是 **数据结构** + **算法**,而数据结构又是算法的基础。 2 | > 3 | > Java是强大的面向对象语言,大量的对象需要 **容器** 用来存储。集合框架就提供了用于存储对象的功能,Collection框架如下图: 4 | > 5 | > (如果您正在使用电脑💻浏览本文,建议将图片保存下来、逆时针🔄旋转90°阅览,如果您正在使用手机📱,建议将手机逆时针🔄旋转90°,便于阅览) 6 | 7 | ![Collection集合框架图](https://i.loli.net/2019/04/03/5ca4aea6d4244.png) -------------------------------------------------------------------------------- /resource/markdown/collection/JavaMaps.md: -------------------------------------------------------------------------------- 1 | > Java除了按 **单个元素** 存储的 **Collection**,还有如下按 **Key-Value** 存储的 **Map**: 2 | 3 | ![JavaMap框架图](https://i.loli.net/2019/04/03/5ca4b20dedfd4.png) 4 | 5 | -------------------------------------------------------------------------------- /resource/markdown/collection/LinkedList.md: -------------------------------------------------------------------------------- 1 | ### LinkedList 源码分析 2 | 3 | > 源码分析基于 `jdk-11.0.1` 4 | 5 |

本文重点

6 | 7 | #### :heavy_plus_sign:添加元素 8 | 9 | #### :heavy_minus_sign:删除元素 10 | 11 | --- 12 | 13 |

一、扩展关系

14 | 15 | ```java 16 | public class LinkedList 17 | extends AbstractSequentialList 18 | implements List, Deque, Cloneable, java.io.Serializable 19 | ``` 20 | 21 | `LinkedList` 继承至 `AbstractSequentialList`表现其具有连续性,同时实现了线性的 `List` 和双端的 `Deque`,**LinkedList** 底层实现就是双向链表。 22 | 23 | 24 | 25 |

二、源码分析

26 | 27 | #### 重点属性 28 | 29 | ```java 30 | // 记录元素数量 31 | transient int size = 0; 32 | // 记录链表更改的次数 33 | protected transient int modCount = 0; 34 | // 指向头节点 35 | transient Node first; 36 | // 指向尾节点 37 | transient Node last; 38 | 39 | // 链表中的每个节点 40 | private static class Node { 41 | // 节点中存放的元素 42 | E item; 43 | // 前驱节点 44 | Node next; 45 | // 后继节点 46 | Node prev; 47 | 48 | // 节点内唯一的方法(构造方法)来注入元素、前驱节点、后继节点 49 | Node(Node prev, E element, Node next) { 50 | this.item = element; 51 | this.next = next; 52 | this.prev = prev; 53 | } 54 | } 55 | ``` 56 | 57 | 58 | 59 | #### 2.1、添加元素 60 | 61 | #### 添加元素到链表尾部 62 | 63 | ```java 64 | public boolean add(E e) { 65 | linkLast(e); 66 | return true; 67 | } 68 | 69 | // 默认将元素添加到双向链表的尾部 70 | void linkLast(E e) { 71 | // 将原来的最后一个节点存储起来 72 | final Node l = last; 73 | // 实例化一个新的节点,也就是被链入的节点, 74 | // 并通过Node唯一的构造方法注入前驱节点和元素 75 | // (因为它被链入尾部,所以后面是没有节点的) 76 | final Node newNode = new Node<>(l, e, null); 77 | // 将新构建的节点链入尾部 78 | last = newNode; 79 | // 如果链表为空,那么它也是头节点 80 | if (l == null) 81 | first = newNode; 82 | else 83 | l.next = newNode; // 否则,刚才那个节点的后继节点就是这个新节点 84 | // 记录元素个数加 1 85 | size++; 86 | // 更改次数加 1 87 | modCount++; 88 | } 89 | ``` 90 | 91 | 添加元素到头部,操作与此类似。 92 | 93 | :star::star::star::star::star: 94 |

重点:添加元素到指定index位置

95 | 96 | ```java 97 | public void add(int index, E element) { 98 | // 和 ArrayList 相似,只要遇到 index ,必做下标越界检查 99 | // 这也符合异常处理规范:如有异常提前抛出 100 | checkPositionIndex(index); 101 | // 如果正好是链表的尾部之后,则链入尾部 102 | if (index == size) 103 | linkLast(element); 104 | else 105 | linkBefore(element, node(index)); // 否则链入 index 位置元素的前面 106 | } 107 | // 在正式链表前,还需要计算index处的节点(或者说是获取index处的节点) 108 | // 前面说过,LinkedList 实现至 List接口,它也有List线性的特性,可以进行迭代 109 | // 这个方法有个牛逼的地方,请看下方 110 | Node node(int index) { 111 | // assert isElementIndex(index); 112 | // 判断 index “距离”哪头近,就从哪头开始遍历,这样可以节省时间 113 | if (index < (size >> 1)) { 114 | Node x = first; 115 | for (int i = 0; i < index; i++) 116 | x = x.next; 117 | return x; 118 | } else { 119 | Node x = last; 120 | for (int i = size - 1; i > index; i--) 121 | x = x.prev; 122 | return x; 123 | } 124 | } 125 | 126 | /** 127 | * @param e 被链入的元素 128 | * @param succ index 位置的元素(下文成其为succ节点 -- 也就是新节点的后继节点) 129 | */ 130 | void linkBefore(E e, Node succ) { 131 | // assert succ != null; 132 | // 获取 succ节点的前驱节点引用 133 | final Node pred = succ.prev; 134 | // 构造新的、即将被链入的节点 135 | final Node newNode = new Node<>(pred, e, succ); 136 | // 将新节点设置为succ节点的前驱节点 137 | succ.prev = newNode; 138 | 139 | // 以下操作就是不要忘记头节点、尾节点、size、modCount 140 | // 如果succ节点没有前驱节点,那么新链入的节点就是头节点 141 | if (pred == null) 142 | first = newNode; 143 | else 144 | pred.next = newNode; // 否则,将新节点设置成之前succ的前驱节点的后继节点 145 | size++; 146 | modCount++; 147 | } 148 | ``` 149 | 150 | #### 图解双向链表的添加模式: 151 | 152 | ![链表添加元素前](https://i.loli.net/2018/12/13/5c11ddc17ae7d.png) 153 | 154 | **第一步:**通过 `Node node(int index)` 获取 `succ 节点`(假设index为3) 155 | 156 | ![使用node(index)方法获取succ节点](https://i.loli.net/2018/12/13/5c11dde93ead8.png) 157 | 158 | **第二步:**链表添加元素 159 | 160 | ```java 161 | // succ 前驱节点是2节点,pred节点实际指向2节点指向的地址 162 | ① final Node pred = succ.prev; 163 | // 构造新的节点,同时又设置前驱节点为 pred(也就是指向2节点)、后继节点为succ 164 | ② final Node newNode = new Node<>(pred, e, succ); 165 | // 设置succ的前驱节点为新节点 166 | ③ succ.prev = newNode; 167 | ④ if (pred == null) // 这里不介绍 pred 为空的情况 168 | first = newNode; 169 | else 170 | pred.next = newNode; // 设置 pred节点(也就是2节点)的后继节点为新节点 171 | ``` 172 | 173 | ![链入元素](https://i.loli.net/2018/12/13/5c11de21eb83f.png) 174 | 175 | 176 | 177 | :star::star::star::star::star: 178 |

重点:2.2 删除元素

179 | 180 | ```java 181 | public boolean remove(Object o) { 182 | // 分两种情况:被删除的元素为nul与不为空 183 | if (o == null) { 184 | // for 循环判断 185 | for (Node x = first; x != null; x = x.next) { 186 | if (x.item == null) { 187 | unlink(x); 188 | return true; 189 | } 190 | } 191 | } else { 192 | for (Node x = first; x != null; x = x.next) { 193 | if (o.equals(x.item)) { 194 | unlink(x); 195 | return true; 196 | } 197 | } 198 | } 199 | return false; 200 | } 201 | 202 | // 解链方法,删除节点方法 203 | E unlink(Node x) { 204 | // assert x != null; 205 | // 被删除节点的元素 206 | final E element = x.item; 207 | // 被删除节点的后继节点 208 | final Node next = x.next; 209 | // 被删除节点的前驱节点 210 | final Node prev = x.prev; 211 | 212 | if (prev == null) { 213 | first = next; 214 | } else { 215 | prev.next = next; 216 | x.prev = null; // 空引用,利于GC回收“无用”的对象 217 | } 218 | 219 | if (next == null) { 220 | last = prev; 221 | } else { 222 | next.prev = prev; 223 | x.next = null; // 空引用,利于GC回收“无用”的对象 224 | } 225 | 226 | x.item = null; // 空引用,利于GC回收“无用”的对象 227 | size--; 228 | modCount++; 229 | return element; 230 | } 231 | ``` 232 | 233 | #### 图解删除LinkedList中的节点 234 | 235 | > 假设删除是3节点(这里不介绍头结点和尾节点为空的情况) 236 | 237 | 首先获取 `被删除节点x(既节点3)` 的 `后继节点next(既节点4)` 和 `前驱节点prev(既节点2)`; 238 | 239 | 然后将prev节点(2节点)的next指向next节点(4节点); 240 | 241 | 然后将next节点(4节点)的prev指向prev节点(2节点); 242 | 243 | 此时就完成,删除操作要比添加操作更为简单。 244 | 245 | ![图解删除LinkedList中的节点](https://i.loli.net/2018/12/13/5c11de4ecaad8.png) -------------------------------------------------------------------------------- /resource/markdown/collection/Map.Entry1.7v.md: -------------------------------------------------------------------------------- 1 | > 本文是基于 `jdk1.7.0_71` 分析 2 | 3 |

一、Map接口

4 | 5 | > **Map接口**是整个 **Key-Value** 存储容器的核心。它 **抽**(qǔ)(xíng)**象** 定义了 **Key-Value** 结构的容器框架。**Map**不会像 **数组** 中元素那样可以单独存放,到像似 `LinkedList` 中的节点自定义,那么特殊的节点一定需要特殊的定义来描述。在 `jdk1.7` 及其之前的版本,Map的作者将其节点描述为 **Entry** ,Entry不是 ~~入口~~、~~大门~~,它在这里称为 **项目** 或 **条目** ,下文简称为 **项**。**Map** 中的每个节点称为 `一项`。 6 | 7 |

二、Map.Entry

8 | 9 | > **Entry** 是Map内部的接口,其中定义了一些方法: 10 | 11 | ```java 12 | // 返回此项对应的key 13 | K getKey(); 14 | // 返回此项对应的value 15 | V getValue(); 16 | // 设置此项中value值 17 | V setValue(V value); 18 | // 将指定的对象与此项进行比较 19 | boolean equals(Object o); 20 | // 返回此项的哈希码值 21 | int hashCode() 22 | ``` 23 | 24 | > Entry 项 的定义很简单,主要是记住它是 Key-Value 形式,并有一些get/set方法。 25 | 26 | 27 | 28 |

三、Map中的方法

29 | 30 | > **方法** 意味着 行为。下面Map的方法意味着整个Map框架具有最基本的、什么样的 **功能**。明白这些方法,就掌握了Map框架核心操作功能。 31 | 32 | ```java 33 | // 返回 map 中键值对的数量 34 | int size(); 35 | // 返回 true 表示map中不包含键值对 36 | boolean isEmpty(); 37 | // 判断比较map中是否含有指定对象相等的key,如果含有返回true 38 | boolean containsKey(Object key); 39 | // 判断比较map中是否含有指定对象相等的value,如果含有返回true 40 | boolean containsValue(Object value); 41 | // 通过key获取对应的value 42 | V get(Object key); 43 | // 放入 key-value键值对,并返回value 44 | V put(K key, V value); 45 | // 通过指定key的一项,并返回此项中的value 46 | V remove(Object key); 47 | // 放入另一个map 48 | void putAll(Map m); 49 | // 清空map 50 | void clear(); 51 | // 返回map中key的Set集合 52 | Set keySet(); 53 | // 将map中的value以Collection的形式返回 54 | Collection values(); 55 | // 返回map中多个项的Set集合(此方法适合既获取key又获取value的场景)(请记住这个方法,非常实用) 56 | Set> entrySet(); 57 | // 将指定的对象与map项进行比较 58 | boolean equals(Object o); 59 | // 返回map的哈希码 60 | int hashCode(); 61 | ``` 62 | 63 | 64 | 65 | > :herb:合抱之木,生于毫末;:mount_fuji:九层之台,起于累土;:camel:千里之行,始于足下。 66 | > 67 | > :sound:再次提醒,请熟记这次基础方法。 -------------------------------------------------------------------------------- /resource/markdown/database/ACIDAndIsolationLevel.md: -------------------------------------------------------------------------------- 1 |

数据库事务是什么?

2 | 3 | **数据库事务**(Database Transaction) ,是指作为 **单个逻辑工作单元** 执行的 *一系列操作*,要么完全地执行,要么完全地不执行。 4 | 5 | 6 | 7 |

一、事务的四大特性

8 | 9 | #### :new_moon: 原子性(Atomicity) 10 | 11 | > **原子性** 是指事务包含的所有 *一系列操作* 要么全部成功提交,要么全部失败回滚。它也是数据库事务最最本质的特性。 12 | > 13 | > 比如一个事务包含两个更新操作A和B,A操作更新成功,而B操作更新失败,则A操作会被回滚。绝不会出现一个成功、一个失败的场景,否则这不是一个事务。 14 | 15 | 16 | 17 | #### :four_leaf_clover:一致性(Consistency ) 18 | 19 | > **一致性** 是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。这里有个容易混淆点,容易和数据一致性混淆。这里更多的强调的是单机下的事务一致性,必须是一个事务内部。 20 | 21 | 保证数据库一致性的主要机制层面:数据库机制层面、业务层面。 22 | 23 | 24 | 25 | #### :umbrella:隔离性(Isolation ) 26 | 27 | 每个事务都有各自的资源单位,事务与事务之间是互相隔离的、不可见的,而事务的结果是对其他事务可见的。 28 | 29 | 一个数据库系统中存在多个事务,每个事务中的各个子操作是对其他事务不可见的,在提交后的结果是对其他事务可见的。可以理解为资源粒子度的划分与隔离。 30 | 31 | 32 | 33 | #### :memo:持久性(Durability ) 34 | 35 | 持久性确保的是一旦提交了事务,即使出现系统故障,该事务的更新也不会丢失。我们可以宽泛的认为,将数据持久化到磁盘。 36 | 37 | 38 | 39 |

三、事务的四种隔离级别

40 | 41 | > 隔离级别由低到高分别为 42 | 43 | 44 | 45 | #### 读未提交(READ_UNCOMMITTED) 46 | 47 | > 就是一个事务A读取另一个事务B *未提交* 的数据。(如果如果事务B出现 **回滚**,那么事务A就会出现 ~~*脏读*~~:x: 的问题)。 48 | 49 | 50 | 51 | #### 读已提交(READ_COMMITTED) 52 | 53 | > 一个事务A读取另一个事务B *已提交* 的数据,那么这样可以解决 ~~`脏读`~~ 的问题。保证读取的数据 **已提交**:white_check_mark: 而不能被 ~~回滚~~:back:。在事务B提交前的 **update**,事务A是读取不到的。只有事务B提交后,事务A才能读取到事务B的 `update改动`。出现的问题是 *一个事务范围内两个相同的查询却返回了不同的数据*,那么这就是`不可重复读`。 54 | 55 | 56 | 57 | #### 可重复读(REPETABLE_READ) 58 | 59 | > 事务开启时,不再允许其他事务 ~~修改(update)~~ 数据。这样就可以无限制的读取没有被 ~~修改(update)~~ 的数据了。出现的问题是,当有 **并行插入(insert)** 操作时就会出现 **幻读**。 60 | 61 | 62 | 63 | #### 可串行化(SERIALIZABLE) 64 | 65 | > 在可串行化的隔离级别下,将事务 **串行化** 顺序执行。那么事务不能进行并行操作,也就解决了 ~~*幻读*~~ 的问题。 66 | 67 | 68 | 69 | **总结:** 70 | 71 | | 隔离级别 | 脏读 | 可重复读 | 幻读 | 72 | | ------------------ | ---- | -------- | ---- | 73 | | READ_UNCOMMITTED | 允许 | 允许 | 允许 | 74 | | READ_COMMITTED | 阻止 | 允许 | 允许 | 75 | | **REPETABLE_READ** | 阻止 | 阻止 | 允许 | 76 | | SERIALIZABLE | 阻止 | 阻止 | 阻止 | 77 | 78 | 79 | 80 | ***InnoDB** 的默认事务隔离级别是:**可重复读(REPETABLE_READ)**。 -------------------------------------------------------------------------------- /resource/markdown/database/DBSplit.md: -------------------------------------------------------------------------------- 1 |

数据库拆分

2 | 3 | ![database](https://i.loli.net/2019/01/14/5c3c8c06cd185.jpeg) 4 | 5 | 数据库承载的数据以及请求负载较高时,我们就要考虑使用读写分离、数据缓存。随着业务的增长,数据库的压力达到了承载的阈值,就要考虑分库分表,分解、分摊单个数据库的压力。 6 | 7 | 8 | 9 | --- 10 | 11 |

垂直拆分

12 | 13 | **数据库的垂直拆分:** 通常,我们将所有的数据按照不同的业务建立并存储不同的表(table),垂直拆分是按照业务将一个数据库拆分多个数据库。原来每个业务对应一张表,垂直拆分后,是一个业务对应一个数据库(当然也有坑可能是多个业务对应一个数据库)。其核心是专库专用。达到的结果是将原来一个数据库系统的压力按照业务均摊到各个拆分后的数据库中。垂直拆分也是比较推荐的一直拆分方式。 14 | 15 | 垂直分片往往需要对架构和设计进行调整。在当前微服务化的进程中,对数据库的垂直拆分是非常友好的。 16 | 17 | 18 | 19 | **数据表的垂直拆分:** 单表的数据达到2GB或500万行记录就要考虑拆分数据表,垂直拆分表就将热点列和不经常使用的列表拆分开,降低单表的大小。 20 | 21 | 22 | 23 | --- 24 | 25 |

水平拆分

26 | 27 | 当一般垂直拆分遇到瓶颈时,会对数据表进行水平拆分。这种方式与垂直拆分不同的地方是,它不会更改表结构。水平分表是将一个表的拆分多结构相同的多个表;水平分库是将一个表拆分成多个结构相同的表,并且这些表分布在不同的数据库。 28 | 29 | 30 | 31 | #### 分布分表中间件有两种: 32 | 33 | 1、代理模式的分库分表中间件:MyCat; 34 | 35 | 2、客户端模式的分库分表中间件:ShardingJDBC; 36 | 37 | 3、支持事务的分布式数据库。 38 | 39 | (当然ShardingProxy也是代理分库分表中间件) 40 | 41 | 42 | 43 | #### 总结: 44 | 45 | 结合着微服务体系,一般会进行垂直拆分。当微服务中的数据库出现压力时,然后进行水平拆分。 46 | 47 | -------------------------------------------------------------------------------- /resource/markdown/database/InnoDB.md: -------------------------------------------------------------------------------- 1 |

InnoDB 索引

2 | 3 | 4 | 5 | #### InnoDB特征 6 | 7 | * 完全的事务支持 8 | * 基于行存储的行级锁 9 | * 多版本并发控制 10 | * 原子死锁检测 11 | * 原子崩溃恢复 12 | 13 | 14 | 15 | #### InnoDB架构 16 | 17 | ![InnoDB架构](https://i.loli.net/2019/01/14/5c3c064b9dcee.png) 18 | 19 | #### InnoDB 逻辑存储结构 20 | 21 | 在InnoDB 下,所有的数据都存储在一个 **表空间(tablespace)** 中。表空间又由段(segment)、区(extent)、页(page)、行(row)组成。页在一些文档中有时也称为块(block),1 extent = 64 pages,InnoDB存储引擎的逻辑存储结构大致如图所示。 22 | 23 | ![InnoDB表结构](https://images2015.cnblogs.com/blog/990532/201701/990532-20170116094754739-1789768872.png) 24 | 25 | 26 | 27 | #### B-tree 28 | 29 | 定义: 30 | 31 | B树(B-TREE)满足如下条件,即可称之为m阶B树: 32 | 33 | - 每个结点最多拥有m棵子树; 34 | - 根结点最少拥有2颗子树(存在子树的情况下); 35 | - 除了根结点以外,其余每个分支结点至少拥有 m/2 棵子树; 36 | - 所有的叶结点都在同一层上; 37 | - 有 k 棵子树的分支结点则存在 k-1 个关键码,关键码按照递增次序进行排列; 38 | - 关键字数量需要满足ceil(m/2)-1 <= n <= m-1; 39 | 40 | ![B-tree](https://i.loli.net/2019/01/14/5c3c2564e867a.png) 41 | 42 | B-tree的特点是每个结点不仅存放键值,而且存放数据。 43 | 44 | --- 45 | 46 | #### B+tree 47 | 48 | ![B+tree](https://i.loli.net/2019/01/14/5c3c24d11402a.png) 49 | 50 | 51 | 52 | **B+树特点:** 53 | 54 | * 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。 55 | * 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。 56 | 57 | 58 | 59 | **B+树的优点:** 60 | 61 | * 单一节点存储更多的元素(因为不含有对应的值,仅仅含有键),使得查询的IO次数更少。 62 | * 所有查询都要从根节点查找到叶子节点,查询性能稳定,相对于B树更加稳定,以为B+树只有叶子节点存储了对应的值信息。 63 | * 所有叶子节点形成有序双向链表,对于SQL的范围查询以及排序查询都很方便。 64 | * B/B+树的共同优点的每个节点有更多的孩子,插入不需要改变树的高度,从而减少重新平衡的次数,非常适合做数据库索引这种需要持久化在磁盘,同时需要大量查询和插入的应用。树中节点存储这指向页的信息,可以快速定位到磁盘对应的页上面。 65 | 66 | 67 | 68 | #### 存储引擎对比 69 | 70 | | 引擎 | 存储限制 | 支持事务 | 全文索引 | 哈希索引 | 数据缓存 | 支持外键 | 71 | | ---------- | -------- | :------: | :------: | :------: | :------: | :------: | 72 | | **InnoDB** | 64TB | ✅ | :x: | ✅ | ✅ | ✅ | 73 | | **MyISAM** | 256TB | :x: | ✅ | ✅ | :x: | :x: | 74 | | Memory | RAM | :x: | :x: | ✅ | N/A | :x: | 75 | | Archive | None | :x: | :x: | :x: | :x: | :x: | 76 | 77 | 78 | 79 | 参考资料: 80 | 81 | [InnoDB 存储引擎原理解析](https://zhuanlan.zhihu.com/p/35925589) 82 | 83 | [InnoDB索引最通俗的解释](https://blog.csdn.net/u010710458/article/details/80209909) -------------------------------------------------------------------------------- /resource/markdown/database/MySQLLock.md: -------------------------------------------------------------------------------- 1 |

MySQL 锁总结

2 | 3 | ![Lock](https://i.loli.net/2019/01/14/5c3c27dcc66ca.jpeg) 4 | 5 | **锁**不仅是资源占有的一种处理机制,更是多线程或并发编程下对数据一致性的一种保证。加锁和释放锁本身也会消耗资源。了解并合理利用锁机制,能大大提升数据库的性能。 6 | 7 | 8 | 9 | 锁的作用者是事务,也就是说,锁是针对事务使用而言。单个操作不显式的开启和提交/回滚事务,默认情况下每个操作会自动的开启一个事务。 10 | 11 | --- 12 | 13 | 14 | 15 | #### 共享锁(S): 16 | 17 | 一个事务对数据加共享锁,也可以允许其他事务对此交集数据加此锁。但阻止其他事务对此交集数据加排他锁。 18 | 19 | 加共享锁语句: 20 | 21 | ```mysql 22 | SELECT * FROM table_name WHERE ... 23 | LOCK IN SHARE MODE 24 | ``` 25 | 26 | 27 | 28 | #### 排他锁(X): 29 | 30 | 一个事务对数据加排他锁,会阻止其他事务对此交集数据加任何锁。 31 | 32 | 加排他锁锁语句: 33 | 34 | ```mysql 35 | SELECT * FROM table_name WHERE ... 36 | FOR UPDATE 37 | ``` 38 | 39 | 40 | 41 | --- 42 | 43 | #### 意向锁: 44 | 45 | 为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),在这里的两种意向锁都是表锁(绕脑)。 46 | 47 | 48 | 49 | #### 意向共享锁(IS): 50 | 51 | 事务打算给数据行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。 52 | 53 | 54 | 55 | #### 意向排他锁(IX): 56 | 57 | 事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。 58 | 59 | 60 | 61 | **锁的冲突与兼容:** 62 | 63 | | 锁的冲突与兼容 | X | IX | S | IS | 64 | | -------------- | ---- | ---- | ---- | ---- | 65 | | **X** | 冲突 | 冲突 | 冲突 | 冲突 | 66 | | **IX** | 冲突 | 兼容 | 冲突 | 兼容 | 67 | | **S** | 冲突 | 冲突 | 兼容 | 兼容 | 68 | | **IS** | 冲突 | 兼容 | 兼容 | 兼容 | 69 | 70 | **备注:**意向锁是 InnoDB 自动加的, 不需用户干预。 71 | 72 | 73 | 74 | --- 75 | 76 | #### 表级锁: 77 | 78 | 每个事务操作会锁住整张表,粒子度最大,简单粗暴。优点是加锁和释放锁次数会大大减少。缺点是锁冲突的概率会大大增加,高并发情况下不可取。 79 | 80 | 81 | 82 | #### 页级锁: 83 | 84 | 资源开销介于行级锁和表级锁,会出现死锁。 85 | 86 | 87 | 88 | #### 行级锁: 89 | 90 | 每个事务仅会锁住被影响的行,也就是说,涉及到哪些行记录,哪些行才会被锁住,会出现死锁。优点是锁冲突概率小,并发度高。缺点是由于锁离子度小,加锁和释放锁的次数会大大增加,资源开销大。 91 | 92 | **切记:**MySQL的行级锁通过索引上的索引项来实现的,InnoDB这种行锁实现特点意味者:只有通过索引条件检索数据,InnoDB才会使用行级锁,否则,InnoDB将使用表锁。 93 | 94 | 95 | 96 | --- 97 | 98 | #### 间隙锁(Next-Key锁) 99 | 100 | 当我们用范围条件而不是相等条件检索数据,并请求共享锁或排他锁时,InnoDB会给符合条件的已有数据的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做 **“间隙(GAP)”** ,InnoDB也会对这个“间隙”加锁,这种锁机制不是所谓的 **间隙锁(Next-Key锁)**。 101 | 102 | 举例来说,假如employee表中只有101条记录,其employee_id的值分别是1,2,...,100,101,下面的SQL: 103 | 104 | ```MySQL 105 | SELECT id, name, age, department 106 | FROM employee 107 | WHERE employee_id > 100 108 | FOR UPDATE 109 | ``` 110 | 111 | 是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。 112 | 113 | InnoDB使用间隙锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求,对于上面的例子,要是不使用间隙锁,如果其他事务插入了employee_id大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;另一方面,是为了满足其恢复和复制的需要。有关其恢复和复制对机制的影响,以及不同隔离级别下InnoDB使用间隙锁的情况。 114 | 115 | 很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。 116 | 117 | 118 | 119 | --- 120 | 121 | #### 死锁: 122 | 123 | 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。当事务试图以不同的顺序锁定资源时,就可能产生死锁。多个事务同时锁定同一个资源时也可能会产生死锁。 124 | 125 | #### InnoDB避免死锁: 126 | 127 | - 为了在单个InnoDB表上执行多个并发写入操作时避免死锁,可以在事务开始时通过为预期要修改的每个记录(行)使用SELECT ... FOR UPDATE语句来获取必要的锁,即使这些行的更改语句是在之后才执行的。 128 | - 在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁、更新时再申请排他锁,因为这时候当用户再申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁 129 | - 如果事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句。 在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会 130 | - 通过SELECT ... LOCK IN SHARE MODE获取行的读锁后,如果当前事务再需要对该记录进行更新操作,则很有可能造成死锁。 131 | - 改变事务隔离级别 132 | 133 | 如果出现死锁,可以用 SHOW INNODB STATUS 命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的 SQL 语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。 134 | 135 | 136 | 137 | --- 138 | 139 | #### 乐观锁: 140 | 141 | 加锁是为了占用资源,我们上面说过加锁和释放锁都会有资源开销。在有些不需要加锁就能获取资源岂不是更好?。乐观锁是乐观的认为在抢占资源时不用加锁就能获取资源(因为没有其他事务抢占资源或者发生的冲突概率小,稍稍尝试几次就能成功,美滋滋)。适用于冲突概率小的情景下。可以参考之前 [关于CAS的文章](https://github.com/about-cloud/JavaCore)。 142 | 143 | 144 | 145 | #### 悲观锁: 146 | 147 | 在冲突概率大的情况下,悲观的认为抢不到资源或者多次都抢不到资源。只能通过加锁的方式的抢占资源,然后再做处理,最后释放资源。 148 | 149 | 150 | 151 | --- 152 | 153 | 参考资料: 154 | 155 | [MySQL锁总结](https://zhuanlan.zhihu.com/p/29150809) -------------------------------------------------------------------------------- /resource/markdown/database/MySqlBasicInfo.md: -------------------------------------------------------------------------------- 1 | Linux平台MySQL的有关目录: 2 | 3 | | 目录 | 描述 | 4 | | ------------------ | -------------------------- | 5 | | /usr/bin | 客户端和脚本 | 6 | | /usr/sbin | MYSQLD服务器 | 7 | | /usr/lib/mysql | 库 | 8 | | /usr/share/info | 信息格式的手册 | 9 | | /usr/share/man | UNIX帮助页 | 10 | | /usr/share/mysql | 错误消息、字符集、示例配置 | 11 | | /usr/include/mysql | 头文件 | 12 | | **/var/lib/mysql** | 日志文件和数据库 | 13 | 14 | 15 | 16 | ```shell 17 | # 关于MySQL启动与关闭服务的命令分别表示:启动、停止、重启、查看状态 18 | $ service mysqld start | stop | restart | status 19 | ``` 20 | 21 | 查看数据库的引擎情况: 22 | 23 | ```mysql 24 | SHOW ENGINES; 25 | +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+ 26 | | Engine | Support | Comment | Transactions | XA | Savepoints | 27 | +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+ 28 | | InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES | 29 | | MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO | 30 | | MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO | 31 | | BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO | 32 | | MyISAM | YES | MyISAM storage engine | NO | NO | NO | 33 | | CSV | YES | CSV storage engine | NO | NO | NO | 34 | | ARCHIVE | YES | Archive storage engine | NO | NO | NO | 35 | | PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO | 36 | | FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL | 37 | +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+ 38 | 9 rows in set (0.00 sec) 39 | ``` 40 | 41 | -------------------------------------------------------------------------------- /resource/markdown/database/Mycat1.x.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hicf/JavaCore/1d7ce34f7a18462c566d33ca702a13d443c52c17/resource/markdown/database/Mycat1.x.md -------------------------------------------------------------------------------- /resource/markdown/database/NormalformAndConstraint.md: -------------------------------------------------------------------------------- 1 |

一、数据库三大范式

2 | 3 | > **范式** 英文 *Normal Form*,缩写 *NF*,翻译为 **规范化形式**,简称 **范式**。 4 | 5 | 6 | 7 | --- 8 | 9 | #### :turtle:第一范式1NF: 10 | 11 | > 数据表中的每一列(字段),必须是不可拆分的最小单元,也就是确保每一列的原子性,而不是集合。 12 | 13 | **反例**: 14 | 15 | | id | user_id | address | 16 | | ----------- | -------- | --------------------------------- | 17 | | 20079878899 | c6999001 | 浙江省舟山市普陀区朱家尖街道666号 | 18 | 19 | 其中 *address* 可以再分为省、市、地区(县)、街道、详细地址,违反了第一范式。 20 | 21 | **正例**: 22 | 23 | | id | user_id | province | city | district | street | detail_address | 24 | | ----------- | -------- | -------- | ------ | -------- | ---------- | -------------- | 25 | | 20079878899 | c6999001 | 浙江省 | 舟山市 | 普陀区 | 朱家尖街道 | 和平路666号 | 26 | 27 | *根据业务需求合理使用行政区域 28 | 29 | 30 | 31 | --- 32 | 33 | 34 | #### :snail:第二范式2NF: 35 | 36 | > 满足1NF的基础上,要求:表中的所有列,都必需依赖于主键,而不能有任何一列与主键没有关系(一个表只描述一件事情)。第二范式消除表的无关数据。 37 | 38 | *主键* 存在的意义就是唯一地标识表中的某一条记录。如果某一列和该行记录没关系,也就没必要存在。 39 | 40 | **反例**: 41 | 42 | | user_id | province | city | district | street | weather | 43 | | -------- | -------- | ------ | -------- | ---------- | ------- | 44 | | c6999001 | 浙江省 | 舟山市 | 普陀区 | 朱家尖街道 | 晴天 | 45 | 46 | 此表中,天气和用户没啥关系,也就不存在依赖关系,所不符合 *第二范式*。正确的做法应该删除此列,如有其他需要可单独存在一张表中。 47 | 48 | 49 | 50 | --- 51 | 52 | #### :octopus:第三范式3NF: 53 | 54 | > 满足2NF的基础上,任何非主属性不依赖于其它非主属性(在2NF基础上消除传递依赖)(也表明不允许数据存在冗余的现象) 55 | 56 | **反例**: 57 | 58 | | order_id | buyer_info_id | buyer_name | buyer_gender | buyer_age | order_status | 59 | | ------------ | ------------- | ---------- | ------------ | --------- | -------------- | 60 | | 201809050001 | c666899 | 李某人 | 男 | 18 | waitsellersend | 61 | 62 | 上面是一个订单表,字段从左至右以此是:订单id、买家id、买家名称、买家性别、买家年龄、订单状态。其中字段`buyer_name`、`buyer_gender`、`buyer_age` 是依赖于字段 `buyer_info_id`,违反 *第二范式*。 63 | 64 | **正例**: 65 | 66 | 订单表 67 | 68 | | order_id | buyer_info_id | order_status | 69 | | ------------ | ------------- | -------------- | 70 | | 201809050001 | c666899 | waitsellersend | 71 | 72 | 买家信息表 73 | 74 | | buyer_info_id | buyer_name | buyer_gender | buyer_age | 75 | | ------------- | ---------- | ------------ | --------- | 76 | | c666899 | 李某人 | 男 | 18 | 77 | 78 | 79 | 80 |

二、数据库五大约束

81 | 82 | #### 1、主键约束(Primay Key) 83 | 84 | > 唯一性,非空性 85 | 86 | #### 2、唯一约束 (Unique) 87 | 88 | > 唯一性,可以空,但只能有一个 89 | 90 | #### 3、检查约束 (Check) 91 | 92 | > 对该列数据的范围、格式的限制(如:年龄、性别等) 93 | 94 | #### 4、默认约束 (Default) 95 | 96 | > 该数据的默认值 97 | 98 | #### 5、外键约束 (Foreign Key) 99 | 100 | > 需要建立两表间的关系 -------------------------------------------------------------------------------- /resource/markdown/database/SQLOptimization.md: -------------------------------------------------------------------------------- 1 | > 目前阿里云推出的 *云数据库RDS* 的版本有 `5.7`、`5.6`、`5.5` 三个版本,以下测试是基于 `5.7`版本。全文也会随着版本流行度进行及时更新,原文更新地址:https://github.com/about-cloud/JavaCore 2 | 3 | --- 4 | 5 |

一、优化必备的 EXPLAIN 命令

6 | 7 | > `EXPLAIN` 是用来查询 SQL 的执行计划, 8 | 9 | 用法: 10 | 11 | ```mysql 12 | EXPLAIN SELECT [字段...] FROM TABLE; 13 | ``` 14 | 15 | 结果: 16 | 17 | ```mysql 18 | +----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ 19 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 20 | +----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ 21 | | 1 | SIMPLE | t0 | NULL | range | idx_trade_id | idx_trade_id | 8 | NULL | 2 | 100.00 | Using index condition | 22 | +----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ 23 | 1 row in set (0.02 sec) 24 | ``` 25 | 26 | **重要字段说明**: 27 | 28 | select_type:使用的SELECT查询类型,比如SIMPLE、PRIMARY、UNION、SUBQUERY等; 29 | 30 | table:关于访问哪张表,如果是多表,则按访问的先后顺序排列; 31 | 32 | **type**:非常非常重要的指标,表示MySQL在表中找到行记录的方式,又称 **访问类型**。访问类型的性能指标从差到好依次是 system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL,一般来说,得保证查询至少达到range级别,最好能达到ref,否则就可能会出现性能问题。; 33 | 34 | **possible_keys**:可能用到的索引,如果为 *NULL*,表示没有可能用到的索引; 35 | 36 | **key**:用到的索引,如果为 *NULL*,表示没有使用索引; 37 | 38 | **key_len**:按字节计算的索引长度,值越小,表示越快; 39 | 40 | ref:关联关系中另一个表的列名称; 41 | 42 | rows:查询数据返回的行数; 43 | 44 | Extra:与关联操作有关的信息。 45 | 46 | 47 | 48 | --- 49 | 50 |

二、索引优化

51 | 52 | #### 1、禁止无边界范围查询 `!=` 、`<` 、`>` 、`=<` 、`>=` ,否则不会命中索引 53 | 54 | *反例*::x: 55 | 56 | ```mysql 57 | SELECT t0.product_id 58 | FROM product t0 59 | WHERE t0.stock_quantity >= '5'; 60 | ``` 61 | 62 | :pushpin:**应指定上下边界** 63 | 64 | 正例::white_check_mark: 65 | 66 | ```mysql 67 | SELECT t0.product_id 68 | FROM product t0 69 | WHERE t0.stock_quantity >= '5' AND t0.stock_quantity <= '999999'; 70 | ``` 71 | 72 | 73 | 74 | #### 2、禁止无边界范围查询 `NOT IN` ,否则不会命中索引 75 | 76 | *反例*:❌ 77 | 78 | ```mysql 79 | SELECT t0.product_id 80 | FROM product t0 81 | WHERE t0.product_status NOT IN ('已下架', '已删除', '无库存'); 82 | ``` 83 | 84 | 📌**应指定范围** 85 | 86 | 正例:✅ 87 | 88 | ```mysql 89 | SELECT t0.product_id 90 | FROM product t0 91 | WHERE t0.product_status IN ('正常销售', '促销中', '可预订'); 92 | ``` 93 | 94 | *使用 `IN` 时也控制范围大小,不推荐超过 `1000`,否则影响性能,得不偿失。 95 | 96 | 97 | 98 | #### 3、禁止 ~~`左模糊`~~ 或 ~~`全模糊`~~ 查询,否则不会命中索引 99 | 100 | *反例*:❌ 101 | 102 | ```mysql 103 | SELECT t0.product_id 104 | FROM product t0 105 | WHERE t0.product_name LIKE '%化工品%'; 106 | -- 或 107 | SELECT t0.product_id 108 | FROM product t0 109 | WHERE t0.product_name LIKE '%化工品原料'; 110 | ``` 111 | 112 | 正例:✅ 113 | 114 | ```mysql 115 | SELECT t0.product_id 116 | FROM product t0 117 | WHERE t0.product_name LIKE '危险化工品%'; 118 | ``` 119 | 120 | 121 | 122 | #### 4、字段的默认值不要为 ~~`null`~~,否则不会命中索引: 123 | > 使用默认约束 (Default Counstraint) 填充数据的默认值 124 | 125 | 126 | 127 | #### 5、在 ~~`字段上计算`~~ 后,不会命中索引: 128 | 129 | *反例*:❌ 130 | 131 | ```sql 132 | SELECT t0.trade_id 133 | FROM trade_order t0 134 | WHERE DATE(t0.gmt_create) < CURDATE(); 135 | ``` 136 | 正例:✅ 137 | ```sql 138 | SELECT t0.trade_id 139 | FROM trade_order t0 140 | WHERE t0.gmt_create >= DATE(NOW()) 141 | ``` 142 | 143 | 144 | 145 | #### 6、`组合索引` 的 `最左前缀原则`: 146 | 147 | 148 | 149 | > 建立 `(id_sex_city)组合索引`会自动建立 `id`、`id_sex`和`id_sex_ciry`三个索引。所以下面三个语句都会 `命中索引`: 150 | 151 | ```sql 152 | -- statement1: 153 | SELECT t0.user_name 154 | FROM user t0 155 | WHERE t0.id = '1001' AND t0.sex = '1' AND t0.ciry = 'Shanghai'; 156 | 157 | -- statement2: 158 | SELECT t0.user_name 159 | FROM user t0 160 | WHERE t0.id = '1001' AND t0.sex = '1'; 161 | 162 | -- statement3: 163 | SELECT t0.user_name 164 | FROM user t0 165 | WHERE t0.id = '1001'; 166 | ``` 167 | 168 | > 但下列语句 ~~`不会命中索引`~~,因为根据`组合索引`的`最左前缀原则`只会生成靠左、逐渐递进的索引: 169 | 170 | ```sql 171 | -- statement4: 172 | SELECT t0.user_name 173 | FROM user t0 174 | WHERE t0.sex = '1' AND t0.ciry = 'Shanghai'; 175 | 176 | -- statement5: 177 | SELECT t0.user_name 178 | FROM user t0 179 | WHERE t0.id = '1001' AND t0.ciry = 'Shanghai'; 180 | 181 | -- statement6: 182 | SELECT t0.user_name 183 | FROM user t0 184 | WHERE t0.ciry = 'Shanghai'; 185 | 186 | -- 略... 187 | ``` 188 | 189 | > (组合索引、复合索引、联合索引都是一个意思)。 190 | 191 | 192 | 193 | #### 7、关于 NUMBER 类型的字段不加单引号也会走索引(亲测有效): 194 | 195 | ```sql 196 | SELECT t0.product_id 197 | FROM product t0 198 | WHERE t0.product_id = 201805010001; 199 | ``` 200 | 201 | (前提user_id字段加了索引)上面和下面的语句都会走索引: 202 | 203 | ```sql 204 | SELECT t0.product_id 205 | FROM product t0 206 | WHERE t0.product_id = '201805010001'; 207 | ``` 208 | 209 | 210 | 211 | #### 8、对于多表 `JOIN` 时的 `ON` 条件中`字段类型`一定要一致,否则也不会命中索引. 212 | 213 | 214 | 215 | #### 9、`varchar`查询性能比 ~~`bigint`~~ 好(亲测有效): 216 | 217 | > 因为 在 ~~`bigint`~~ 类型字段上会`全表扫描`,而在 `varchar` 上每个字符判断会走`索引`,这样尽量避免全表扫描。 218 | 219 | 220 | 221 | #### 10、小数类型使用 `decimal`,禁止使用 ~~`float`~~ 与 ~~`double`~~: 222 | > ~~`float`~~ 与 ~~`double`~~存储数据时,可能会损失精度,进而判断的时候导致结果不准,`强制` 使用 `decimal` 数据类型。 223 | 224 | 225 | 226 | #### 11、表达`是否`的概念时,字段使用`is_`开头,数据类型使用 `unsigned tinyint`类型,`1`表示`是`,`0`表示`否`。 227 | 228 | 229 | 230 | #### 12、`任何非负数`都必须声明为`unsigned`类型: 231 | > 比如年龄、状态码等等,这样最大容量正值会扩大一倍。 232 | 233 | 234 | 235 | #### 13、如果存储的`字符串长度几乎相等`,`必须`使用`char`定长字符串类型: 236 | > 比如中国大陆 11 位长度的手机号。 237 | 238 | 239 | 240 | #### 14、有时候是不需要建索引: 241 | > 性别字段、状态,这种不同值很少的字段是不需要建索引的。 242 | 243 | 244 | 245 | #### 15、单表行数超过 `500万行` 或者单表容量超过 `2G`,才推荐分表; 246 | 247 | 248 | 249 | #### 16、进行 `UPDATE` 或 `DELETE` 时,必先 `SELETE`,避免出现`误删`数据; 250 | 251 | 252 | 253 | :sparkles:更多内容请参考《阿里巴巴Java开发手册》和《唯品会Java开发手册》。 254 | 255 | ##### -------------------------------------------------------------------------------- /resource/markdown/database/SeparationOfReadingAndWriting.md: -------------------------------------------------------------------------------- 1 |

一、ArrayList

2 | 3 | -------------------------------------------------------------------------------- /resource/markdown/database/ShardingSphere3.x.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hicf/JavaCore/1d7ce34f7a18462c566d33ca702a13d443c52c17/resource/markdown/database/ShardingSphere3.x.md -------------------------------------------------------------------------------- /resource/markdown/database/Zebra2.9.x.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hicf/JavaCore/1d7ce34f7a18462c566d33ca702a13d443c52c17/resource/markdown/database/Zebra2.9.x.md -------------------------------------------------------------------------------- /resource/markdown/design-pattern/design-principles.md: -------------------------------------------------------------------------------- 1 |

6大设计原则

2 | 3 | 1.单一职能原则 4 | 5 | 6 | 7 | 2.开闭原则 8 | 9 | 10 | 11 | 3.里氏置换原则 12 | 13 | 14 | 15 | 4.依赖倒置原则 16 | 17 | 18 | 19 | 5.接口隔离原则 20 | 21 | 22 | 23 | 6.迪米特法则(最少知道原则) -------------------------------------------------------------------------------- /resource/markdown/distribution/2PCand3PC.md: -------------------------------------------------------------------------------- 1 |

一、2PC协议

2 | 3 | > 2PC 是 *Two-Phase Commit Protocol* 的缩写,中文名称 **两阶段提交协议**,它是为了解决 *分布式事务一致性* 问题而产生的算法。分布式一致性问题是分布式系统各个节点(分区)如何就一项决议达成一致的问题,构成的一组操作称为**事务** (又称 **分布式事务**,**全局事务**),各个节点的事务称为**分支事务**。**两阶段提交协议** 是一种原子提交协议、共识协议。 4 | 5 | **两阶段提交协议** 是指事务提交的过程分为两个阶段:**准备阶段** 和 **提交阶段**。其中事务的发起者称为 **事务协调者** ,事务的执行者称为 **参与者**。一般,在一个事务中有一个事务协调者和多个参与者。 6 | 7 | ![2PC](https://i.loli.net/2019/01/16/5c3f3a888ca74.png) 8 | 9 | #### 阶段一:准备阶段: 10 | 11 | 又称 投票阶段。首先,协调者询问全局事务中各个参与者是否可以执行操作。然后,全局事务中的各个参与者执行操作(占有资源)并做出回答 YES/NO 给协调者; 12 | 13 | #### 阶段二:提交阶段: 14 | 15 | 又称 完成阶段。最后,协调者检查事务中的各个参与者的反馈。 16 | 17 | **成功:** 在规定的时效内全部反馈 YES,则协调器向所有参与者发送事务提交消息。每个参与者完成操作,并释放事务期间持有的所有锁和资源。每个参与者向协调器发送一个确认。当事务协调器收到所有确认时,协调器完成事务。 18 | 19 | **失败:** 在规定的时效内有参与者未对阶段一做回复(超时)或有参与者回复 NO,则协调器向所有参与者发送事务回滚消息。每个参与者使用撤消日志撤消事务,并释放事务期间保留的资源和锁。每个参与者向协调器发送一个确认。当收到所有确认时,协调器将撤消事务。 20 | 21 | 22 | 23 | #### 下面通过一个案例来说明: 24 | 25 | **背景**:比如某用户想从杭州去巴黎,但杭州没有直达巴黎的机票,他想先从杭州转到北京,然后从北京飞往巴黎。 26 | 27 | 他想通过携程APP进行购买两张票,要么两张票都购买成功,要么两张票都购买失败。 28 | 29 | **过程**:首先用户先通过携程选择杭州到北京、北京到巴黎的机票;如果两个航班都有机票话,然后下单购买两个航班的机票,从杭州到北京的机票是首都航空公司提供,从北京到巴黎的机票是法国航空公司提供。 30 | 31 | **分析**:整个购买两张机票的过程就是一个事务,分别购买两个班次的行为就是两个操作。 32 | 33 | 准备阶段:选择航班机票时就是准备阶段,被选择的航空公司票务系统就是参与者,通过选择机票就是占有资源(占有资源是有时间限制的); 34 | 35 | ![准备阶段](https://i.loli.net/2019/01/16/5c3f3e5d530ae.png) 36 | 37 | 消息反馈: 38 | 39 | ![反馈](https://i.loli.net/2019/01/16/5c3f3e94d2bdd.png) 40 | 41 | 提交阶段:携程作为协调者会查看参与者(各航空公司票务系统)是否可以提供机票,如果都是YES,表示可以提交订单,否则有NO,只能放弃操作(事务回滚),也是放弃对资源的占有。 42 | 43 | ![提交阶段](https://i.loli.net/2019/01/16/5c3f40464dec3.png) 44 | 45 | 46 | 47 | #### 2PC缺点: 48 | 49 | **同步阻塞**:我们前面讲过,2PC协议就是为了解决分布式事务一致性问题的,而可用性与一致性往往存在矛盾。假设分区节点之间出现网络通信问题,那么消息就不能及时传递给响应的节点,为了到达一致性,整个分布式系统(准确地将是参与事务的机器组成的系统)将会陷入阻塞状态,所以2PC协议是一个典型的 **阻塞式协议**。 50 | 51 | **单点问题**:如果协调者出现故障,参与者将一直处在等待状态(等待协调者发送消息指令),分支事务占有资源也不能释放,直到收到提交/回滚命令为止。这就会协调者的单点问题。 52 | 53 | **脑裂问题**:在提交阶段中,只有部分参与者接收到协调者发送的 `Commit` 指令,则会出现数据不一致的问题。 54 | 55 | 56 | 57 | **2PC如何解决故障问题?** 58 | 59 | > 两阶段提交协议的参与者使用协议状态的日志记录。协议的恢复过程使用日志记录,这些记录通常生成速度很慢,但在失败后仍然存在。存在许多协议变体,它们主要在日志策略和恢复机制方面有所不同。尽管恢复过程通常很少使用,但由于协议要考虑和支持许多可能的故障场景,因此它构成了协议的一个重要部分。 60 | > 61 | > 协议的工作方式如下:一个节点是指定的协调器,它是主站点,网络中的其余节点被指定为参与者。协议假定每个节点上都有一个稳定的存储,有一个提前写入日志,不会永远崩溃,提前写入日志中的数据不会在崩溃中丢失或损坏,并且任何两个节点都可以相互通信。最后一个假设没有太多限制,因为网络通信通常可以被重新路由。前两个假设要强大得多;如果一个节点被完全破坏,那么数据可能会丢失。 62 | 63 | > 在到达事务的最后一步之后,协调器将启动该协议。然后,参与者根据是否在参与者处成功处理了事务,使用协议消息或中止消息进行响应。 64 | 65 | 66 | 67 | #### 2PC与XA事务 68 | 69 | X/Open 的XA事务就是使用的2PC协议,事务的协调者是TM(事务管理器),事务的参与者是TM(资源管理器)。为了解决2PC中事务协调者的单点问题,又出于性能和可靠性的原因,协调者的角色可以转移到另一个TM(事务管理器)。参与者之间不交换2PC信息,而是与各自的TM(事务管理器)交换信息。相关的TM(事务管理器)相互通信以执行上面的2PC协议模式,“代表”各自的参与者,以终止该事务。在这种体系结构中,协议是完全分布式的(不需要任何中央处理组件或数据结构),并且可以有效地随着网络节点的数量(网络大小)而扩展。 70 | 71 | 72 | 73 | #### 树型两阶段提交协议 74 | 75 | The TreeTwo-phase Commit Protocol (树形两阶段提交,T2PC,t2pc,Tree-2PC)也称为嵌套2PC或递归2PC,是2PC的常见变体协议,它更好地利用了底层通信基础设施。分布式事务中的参与者通常按照定义树结构的顺序调用,即调用树,其中参与者是节点,边缘是调用(通信链接)。通常使用同一个树通过2PC协议完成事务,但原则上也可以使用另一个通信树来完成事务。在 Tree-2PC 中,协调器被认为是通信树(倒挂树)的根(“top”),而参与者是其他节点。协调器可以是发起事务的节点(递归地(传递地)调用其他参与者),但同一树中的另一个节点可以代替协调器角色。来自协调器的2PC消息被“向下”传播到树上,而发送到协调器的消息则被一个参与者从树下的所有参与者“收集”到,然后在树上发送适当的消息“向上”(除中止消息外,该消息在接收到该消息后立即被“向上”传播,或者如果当前参与者启动了博尔特) 76 | 77 | 78 | 79 | #### 动态两阶段提交 80 | 81 | The Dynamic Two-phase Commit Protocol (动态两阶段提交,D2PC,d2pc,Dynamic-2PC)是没有预定协调器的 树形两阶段提交协议 的变体协议。协议消息(是投票)从所有叶开始传播,每个叶在代表事务完成其任务时(准备就绪)。中间(非叶)节点在协议消息发送到最后一个(单个)相邻节点(尚未从该节点接收到协议消息)时发送就绪。协调器是通过在事务树上的冲突位置对协议消息进行竞速来动态确定的。它们要么在事务树节点上发生冲突,要么作为协调器,要么在树边缘发生冲突。在后一种情况下,两个边缘的节点之一被选为协调器(任何节点)。D2PC是时间最优的(在特定事务树的所有实例和任何特定 Tree-2PC 协议实现中;所有实例都有相同的树;每个实例都有不同的节点作为协调器):通过选择最佳协调器,D2PC在尽可能短的时间内提交协调器和每个参与者,从而允许在每个事务参与者(树节点)中尽早释放锁定的资源。 82 | 83 | 84 | 85 | --- 86 | 87 |

二、3PC协议

88 | 89 | > 3PC 是 *Three-Phase Commit Protocol* 的缩写,中文名称 **三阶段提交协议**,与两阶段提交协议(2PC)不同,3PC是非阻塞协议。具体来说,3PC对事务提交或中止之前所需的时间量设置了一个上限。此属性确保如果给定的事务正试图通过3PC提交并持有一些资源锁,则它将在超时后释放这些锁。 90 | > 91 | > 在原来2PC协议的基础上,将2阶段扩展到3阶段:**询问提交阶段**、**预提交阶段**、**执行提交阶段**。 92 | 93 | ![3PC](https://i.loli.net/2019/01/16/5c3f0b5b4af27.png) 94 | 95 | #### 阶段一:询问提交阶段(CanCommit): 96 | 97 | 这个阶段(CanCommit)和2PC的准备阶段非常相似。事务协调者接受事务请求。如果出现协调者出现故障,则中止事务(即使在恢复时,它将认为事务已中止)。否则继续做以下操作: 98 | 99 | ​ **1.事务询问**:事务中的协调者向事务中的所有参与者发送消息,询问各个参与者是否可以提交事务; 100 | 101 | ​ **2.事务反馈**:各个参与者根据当前资源判断是否可以提交事务(一般会占有资源),并反馈给协调者。 102 | 103 | 104 | 105 | #### 阶段二:预提交阶段(PreCommit): 106 | 107 | 事务中的协调者在收到各个参与者反馈后,做出判断,出现以下两种情况: 108 | 109 | **1.** 如果事务中的所有参与者都表示可以提交事务,则做执行 **事务预提交** 110 | 111 | ​ **1.1.发送预提交请求**:事务中的协调者向各个参与者发送预提交(PreCommit)指令,并进入已准备(prepared)状态; 112 | 113 | ​ **1.2.事务预提交**:事务中的参与者接受到协调者的预提交(PreCommit)指令,会执行事务操作,并将undo和redo信息记录到事务日志中。(undo和redo分别表示撤销和恢复); 114 | 115 | ​ **1.3.反馈事务预提交的结果**:在参与者成功的执行了事务操作,并向协调者返回ACK响应,如果协调者出现故障,那么参与者将不断的重试发送ACK。之后开始等待最终事务提交指令。 116 | 117 | **2.** 如果其中之一的参与者表示不能提交事务或超时反馈,则做 **中断事务** 处理 118 | 119 | ​ **2.1.发送中断事务指令**:事务中的协调者向各个参与者发送中断指令(abort); 120 | 121 | ​ **2.2.中断事务**:各个参与者就收到中断指令或超时未收到任何指令时,会做中断事务处理。 122 | 123 | 124 | 125 | #### 阶段三:执行提交阶段(DoCommit): 126 | 127 | 事务中的协调者在先接收各个参与者的反馈,然后做出判断,也会出现以下两种情况: 128 | 129 | **1.** 如果事务中的参与者成功执行的预提交事务操作,并返回成功ACK,则 **执行事务提交**: 130 | 131 | ​ **1.1.发送执行事务提交指令**:事务协调者发送执行事务提交指令给各个参与者; 132 | 133 | ​ **1.2.事务提交**:各个参与者收到协调者发送的执行事务提交的指令,然后执行事务提交; 134 | 135 | ​ **1.3.反馈事务提交结果**:各个参与者执行完事务提交操作后,反馈结果给事务协调者; 136 | 137 | ​ **1.4.完成事务**:协调者接收到各个参与者执行提交的反馈,事务完成。 138 | 139 | 140 | 141 | **2.** 如果事务中的参与者执行预提交事务操作失败或超时反馈,则 **中断事务**: 142 | 143 | ​ **1.1.发送中断事务指令**:事务中的协调者向各个参与者发送中断指令(abort); 144 | 145 | ​ **1.2.事务回滚**:各个参与者收到协调者发送的执行事务回滚的指令,然后执行事务回滚; 146 | 147 | ​ **1.3.反馈事务回滚结果**:各个参与者执行完事务回滚操作后,反馈结果给事务协调者; 148 | 149 | ​ **1.4.中断事务**:协调者接收到各个参与者执行回滚的反馈,事务中断。 150 | 151 | 152 | 153 | **说明**:执行提交阶段(DoCommit)时,可能协调者或网络出现问题,都有可能导致事务的部分参与者无法接收到协调者的指令(包括执行提交和中断事务),在这种情况下,部分参与者会在超时等待之后继续进行事务提交。有一定小概率会指定数据不一致的情况,所以这也是3PC协议的一大缺点。 154 | 155 | 156 | 157 | 参考资料: 158 | 159 | [WIKIPEDIA:Two-phase commit protocol](https://en.wikipedia.org/wiki/Two-phase_commit_protocol) -------------------------------------------------------------------------------- /resource/markdown/distribution/ByzantineGeneral.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resource/markdown/distribution/CAPandBASE.md: -------------------------------------------------------------------------------- 1 |

一、CAP定理

2 | 3 | **CAP定理** (又称*CAP*原则) 是指一个分布式系统中,*Consistency*(一致性)、 *Availability*(可用性)、*Partition tolerance*(分区容错性),三者不可兼得。 4 | 5 | ![CAP原则]() 6 | 7 | #### 1、一致性(Consistency) 8 | 9 | ![一致性]() 10 | 11 | > 在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本) 12 | 13 | 14 | 15 | #### 2、可用性(Availability) 16 | 17 | ![可用性]() 18 | 19 | > 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性) 20 | 21 | 22 | 23 | #### 3、分区容错性(Partition tolerance) 24 | 25 | ![分区通信]() 26 | 27 | > 分布式系统分布在多个子网络,每个子网络就叫做一个 *分区(partition)*。分区出错(出现故障)的意思是,分区之间通信可能会出现故障,那么分区之间就可能无法通信。 28 | 29 | 在分布式系统中,分区通信是基础条件,更是必要条件。如果分布式系统中,区间通信出现故障,那消息传递是不可达的。所以P是分布式系统中,P是必选的,更据CAP定理,那么只能从C和A中选一个,就出现CP和AP的架构。 30 | 31 | 32 | 33 | #### 为什么三者不可兼得呢? 34 | 35 | > 原因还是因为 *分区容错性* ,如果出现网络故障将会出现以下两种情况: 36 | > 37 | > ①如果追求 *一致性(Consistency)* ,分区会等待、追求所有节点的最新数据,分布式系统将不可用的; 38 | > 39 | > ②如果追求 可用性(Availability),分区就无法去追求所有节点的最新数据,也就无法满足一致性; 40 | > 41 | > 所以一般只有CP和AP两种选型。 42 | 43 | 44 | 45 | --- 46 | 47 |

二、BASE理论

48 | 49 | **BASE** 基本可用(Basically Available)、软状态(Soft state)、最终一致(Eventually consistent)三个短语的缩写。*BASE 理论*是对 *CAP定理* 中 *一致性* 和 *可用性* 权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于 *CAP定理* 逐步演化而来的,其核心思想是即使无法做到 *强一致性(Strong consistency)*,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到 *最终一致性(Eventual consistency)*。 50 | 51 | #### 1、基本可用(Basically Available) 52 | 53 | > 在分布式系统分区出现故障时,允许损失部分可用性来用于权衡一致性,但系统会处于基本可用的状态; 54 | > 55 | > ① 牺牲响应延时:本来一次请求-响应的时间是0.5ms,为了到达一致性,可延时到3s,这样既达到了基本可用,也满足了一致性; 56 | > 57 | > ② 牺牲可用功能:本来一次正常的查找钱余额查找应该显示正确的余额,牺牲可用功能后,将用于请求引导到正在处理界面。 58 | 59 | 60 | 61 | #### 2、软状态(Soft state) 62 | 63 | > *软状态* 是指运行系统中的数据允许存在 *中间状态* ,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。 64 | 65 | ![软状态]() 66 | 67 | 68 | 69 | #### 3、最终一致(Eventually consistent) 70 | 71 | > *最终一致性* 强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。 -------------------------------------------------------------------------------- /resource/markdown/distribution/ConsistentHashing.md: -------------------------------------------------------------------------------- 1 |

一致性哈希:变化世界中的负载平衡

2 | 3 | Teradata在1986年发布的分布式数据库中使用了这种技术,但他没有提出这个术语。直到1997年,由David Karger等人在应用于分布式缓存问题中提出了 **“一致性哈希”** ,作为一种在不断变化的Web服务器集群中分配请求的方法。一致性哈希算法也是负载均衡算法中的一种。常用的负载均衡算法适用于集群,但不一定适用于分布式。 4 | 5 | 6 | 7 | #### 案例背景 8 | 9 | 在某大促活动中,我们需要将1亿用户数据缓存到1000个Redis实例中,每个Redis实例均摊10万用户数据。通常使用哈希算法(hash(K) mod N )来做映射,先计算用户id的哈希值,然后模运算计算出对应的服务器,最后把对应的数据缓存到对应的服务器即可。此种方法比较简单,但存在一个问题,它不适用于分布式下节点宕机、扩容和缩容的场景。 10 | 11 | ![未命名1550461043.png](https://i.loli.net/2019/02/18/5c6a287f117fc.png) 12 | 13 | 假如1号Redis服务宕机,对应10万用户将无法命中缓存。当然1号Redis服务可以做成高可用集群,但又不适用于分布式缓存下节点的扩容与缩容。试想,大促活动结束后,服务器的压力也会减少,没必要使用这么多服务器,可以将1000个Redis实例缩容为100个。随着节点的宕机、扩容和缩容,势必会带来数据的再哈希与重分配。如此海量的数据,将导致缓存服务不可用。 14 | 15 | 16 | 17 | #### 一致性哈希算法 18 | 19 | 为了解决上述问题,一致性哈希算法孕育而出 20 | 21 | ![图1](https://i.loli.net/2019/02/18/5c6a7ecbcab93.png) 22 | 23 | 首先将分配 ![2^32](https://i.loli.net/2019/02/18/5c6a8e51599c7.gif) 个槽,也就是从 0 到 ![CodeCogsEqn (1).gif](https://i.loli.net/2019/02/18/5c6a8ed1b3329.gif) ,如上图,虚拟出环形。通过哈希算法 `hash(ip, port)` 计算出Redis实例对应的哈希值,然后对 ![2^32](https://i.loli.net/2019/02/18/5c6a8e51599c7.gif)取模,得到映射到环上的值。对于请求来说,先计算出key的哈希值,同样对 ![2^32](https://i.loli.net/2019/02/18/5c6a8e51599c7.gif)取模,得到映射到环上的值。沿着顺时针匹配第一个Redis服务节点。 24 | 25 | 当添加Redis节点时,也是通过哈希算法 `hash(ip, port)` 计算出Redis实例对应的哈希值,然后对 ![2^32](https://i.loli.net/2019/02/18/5c6a8e51599c7.gif) 取模,得到映射到环上的值。假设如下图的X节点: 26 | 27 | ![图2](https://i.loli.net/2019/02/18/5c6a855193525.png) 28 | 29 | 在数据重分配上,仅仅需要将 B节点 和 C节点 之间的数据再哈希、重分配就可以了,B-X之间的数据映射到X节点,X-C之间的数据映射到C节点。这样就避免将全量的数据再哈希、重分配节点。 30 | 31 | 32 | 33 | ##### 数据倾斜 34 | 35 | ![图3](https://i.loli.net/2019/02/18/5c6a872f8a386.png) 36 | 37 | 如果节点较少,就会造成节点分布不均衡(数据倾斜)的问题,如上图,B-A之间映射的请求远多于A-B之间。就会造成A节点压力增加,而B节点负载较轻。 38 | 39 | 40 | 41 | #### 虚拟节点 42 | 43 | ![图4](https://i.loli.net/2019/02/18/5c6a8b15e7d81.png) 44 | 45 | 为了解决上述的数据倾斜问题,在一致性哈希算法的基础上引入虚拟节点。其思想是,预先分配N个虚拟节点,然后这些虚拟节点再在映射实际节点,能保证虚拟节点映射的数据,不会过于倾斜。图4中,虚拟节点A1和A2对应的服务器为A、虚拟节点B1和B2对应的服务器为B,以此类推。假如A服务器宕机后,虚拟节点B1和B2则无法映射实际服务器,那么可以将数据按照瞬时间就近映射。D2-B1之间的数据映射到节点C1,实际映射的服务器是C;A1-B2之间的数据映射到节点A2,实际映射的服务器是A,这样就避免数据过度倾斜。 46 | 47 | -------------------------------------------------------------------------------- /resource/markdown/distribution/DistributedCache.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resource/markdown/distribution/DistributedLock.md: -------------------------------------------------------------------------------- 1 | 比如某个应用需要定时跑一些任务,为了保证高可用,做了三个节点的集群,后来发现集群中每个节点都跑了重复的任务。 2 | 3 |

一、基于数据库实现分布式锁

4 | 5 | 首先创建一张表,用于存储锁标识的记录: 6 | 7 | ```mysql 8 | CREATE TABLE IF NOT EXISTS distribution_lock( 9 | id BIGINT(20) UNSIGNED NOT NULL COMMENT '分布式锁id', 10 | lock_name VARCHAR(50) DEFAULT '' COMMENT '分布式锁的名称', 11 | node_number TINYINT(1) NOT NULL COMMENT '集群下节点的编号,用于重入锁', 12 | gmt_create DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '行记录创建时间', 13 | PRIMARY KEY (id) 14 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='存储分布式锁的表'; 15 | ``` 16 | 17 | 18 | 19 | **获取锁:** 20 | 21 | ```mysql 22 | INSERT INTO distribution_lock(id, lock_name, node_number) VALUES(1001, 'order_task', 1); 23 | ``` 24 | 25 | 插入成功表示获取锁,否是未获取锁; 26 | 27 | 28 | 29 | **获取重入锁:** 30 | 31 | ```mysql 32 | SELECT 33 | t0.id, 34 | t0.lock_name, 35 | t0.gmt_create 36 | FROM distribution_lock t0 37 | WHERE t0.id = 1001 AND t0.node_number = 1; 38 | ``` 39 | 40 | 41 | 42 | **检查锁超时:** 43 | 44 | ```mysql 45 | SELECT 46 | t0.id, 47 | t0.lock_name, 48 | t0.gmt_create , 49 | TIMESTAMPDIFF(SECOND, t0.gmt_create, NOW()) 'duration_time' 50 | FROM distribution_lock t0 51 | WHERE t0.id = 1001 AND t0.node_number != 2; 52 | ``` 53 | 54 | 55 | 56 | **释放锁:** 57 | 58 | ```mysql 59 | DELETE FROM distribution_lock WHERE id = 1001 AND t0.node_number = 1; 60 | ``` 61 | 62 | 63 | 64 | > 上面是用 `INSERT` + `SELECT ` + `DELETE` 来实现的,当然也可以使用 `UPDATE` + `SELECT` 实现(注意初始化锁标识的行记录)。 65 | 66 | 因为 `id` 设置为 主键,所以具有唯一约束属性,同一时刻只能被一个线程获取锁🔐。 67 | 68 | 69 | 70 | **注意问题:** 71 | 72 | 1. 单点故障: 数据库存在单点故障的风险,建议做成高可用版本; 73 | 2. 重入性: 也就是重入锁,可以精确到线程级别; 74 | 3. 锁失效机制: 如果一台服务获得锁后,再没有主动释放锁的情况下,其他服务也就无法获取锁。解决方法是获取锁时,判断锁是否超时; 75 | 4. 非阻塞性: 比如一个定时任务每月跑一次,恰巧此时被其他节点服务占用锁,导致该节点获取锁失败,解决方法是循环多次获取锁; 76 | 5. 性能: 基于数据库的性能比较低,不适合高并发场景。 77 | 78 | 79 | 80 | --- 81 | 82 |

二、基于Redis实现分布式锁

83 | 84 | **获取锁** 85 | 86 | ```shell 87 | 127.0.0.1:6379> SETNX lock:1001 uuid 88 | ``` 89 | 90 | 巧用 `SETNX` 命令,其中 `value` 为唯一标识,比如 `ip+线程id+时间戳` 、 `UUID`诸如此类,如果返回 `1` 表示获取锁成功,否则获取锁失败; 91 | 92 | 93 | 94 | **设置锁超时时间** 95 | 96 | ```shell 97 | 127.0.0.1:6379> expire lock:1001 5 98 | ``` 99 | 100 | 如果返回 `1` 表示设置锁超时成功,否则失败; 101 | 102 | 103 | 104 | ##### 获取将直接使用以下命令代替上面两条命令: 105 | 106 | ```shell 107 | 127.0.0.1:6379> SET lock:1001 uuid EX 5 NX 108 | ``` 109 | 110 | 111 | 112 | **锁过期检查** 113 | 114 | ```shell 115 | 127.0.0.1:6379> GET lock:1001 116 | ``` 117 | 118 | 如果获得的 `value` 和之前设置的 `value` 一致,说明锁有效,业务提交;否则锁过期,业务回滚; 119 | 120 | 121 | 122 | **释放锁** Redis的事务不像MySQL的事务那样强大,所以释放锁时要防止误删 `key` ,使用Lua脚本来操作: 123 | 124 | ```java 125 | /** 126 | * 释放分布式锁 127 | * 128 | * @param key key 129 | * @param value value 130 | * @return 主动成功释放锁返回 true,否则返回false 131 | */ 132 | public boolean release(String key, String value) { 133 | // Null check 134 | Assert.notNull(key, "The key is not allowed to be null"); 135 | Assert.notNull(value, "The value is not allowed to be null"); 136 | 137 | // result 表示是否删除指定的 key,1表示是,0表示否 138 | long result = 0; 139 | 140 | try { 141 | String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then \n" + 142 | " return redis.call(\"del\",KEYS[1])\n" + 143 | " else \n" + 144 | " return 0\n" + 145 | " end"; 146 | 147 | result = (Long) jedis.evalsha(jedis.scriptLoad(lua), 148 | Collections.singletonList(key), 149 | Collections.singletonList(value)); 150 | 151 | } catch (Exception e) { 152 | e.printStackTrace(); 153 | } finally { 154 | if (jedis != null) { 155 | try { 156 | jedis.close(); 157 | } catch (Exception e) { 158 | e.printStackTrace(); 159 | } 160 | } 161 | } 162 | 163 | return result == 1; 164 | } 165 | ``` 166 | 167 | 168 | 169 | --- 170 | 171 |

三、基于Zookeeper实现分布式锁

172 | 173 | **获取锁** 174 | 175 | // TODO 176 | 177 | 178 | 179 | **释放锁** 180 | 181 | 182 | 183 | // TODO 184 | 185 | -------------------------------------------------------------------------------- /resource/markdown/distribution/DistributedTransaction.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resource/markdown/distribution/DuplicateConstruction.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resource/markdown/distribution/GlobalIDGeneration.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resource/markdown/distribution/GrabRedEnvelope.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resource/markdown/distribution/LockDesign.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resource/markdown/distribution/PaxosAlgorithm.md: -------------------------------------------------------------------------------- 1 |

Paxos算法(强一致性算法)

2 | 3 | **Paxos协议** 是一种就某个议题打成一致共识的协议(共识协议)。 4 | 5 | 6 | 7 | #### 什么是共识? 8 | 9 | 比如在一个部门内,部门老大问大家年度旅游去哪里玩,大家先是选取旅游地点,可能大家的旅游向往地不一致,有人想去新加坡,有人想去泰国,意见产生分歧,这就是没打成**共识**。为了打成共识,部门老大说“既然意见不一致,大家就旅游目的地进行投票表决吧,选取票最多的目的地”。大家在参与投票,以及认可投票结果,最后大家打成共识。 10 | 11 | 共识是一组参与者就一个结果达成一致的过程。如果参与者或他们的交流媒介可能遇到故障时,这个问题就变得困难了。比如上述投票过程是用的微信投票小程序(交流媒介),假设微信投票小程序挂了(仅仅是假设),那么打成共识的过程就会出现问题。 12 | 13 | --- 14 | 15 | 基于Paxos协议实现的算法,当前在分布式系统应用中被公认的最有效的强一致性算法。下面就来分析一下“强一致性”的Paxos算法的机制。 16 | 17 | 18 | 19 | #### 角色: 20 | 21 | **Client**:产生议题者,客户机向分布式系统发出请求,并等待响应。例如,分布式文件服务器中文件的写入请求。 22 | 23 | **Acceptor(Voters)**:决策者(投票者),接受者,接受方充当协议的容错“存储器”。接受者被收集到称为引言的组中。任何发送给承兑人的信息必须发送到承兑人的法定人数。除非从仲裁中的每个接受方收到副本,否则从接受方收到的任何消息都将被忽略。 24 | 25 | **Proposer** :提议者,提议者主张客户请求,试图说服接受方同意该请求,并在发生冲突时充当协调人,推动协议向前发展。 26 | 27 | **Learner**:(最终决策)学习者,学习者充当协议的复制因素。一旦接受方同意客户请求,学习者可以采取行动(即:执行请求并向客户发送响应)。为了提高处理的可用性,可以添加更多的学习者。 28 | 29 | **Leader**:领导者,Paxos需要一个杰出的提议者(称为领导者)来取得进展。许多进程可能认为它们是领导者,但协议只保证最终选择其中一个进程时才能取得进展。如果两个进程认为它们是领导者,它们可能会通过不断提出冲突的更新来拖延协议。然而,在这种情况下,安全性能仍然保持不变。 30 | 31 | 32 | 33 | Quorums通过确保至少有一些幸存的处理器保留对结果的了解,来表达Paxos的安全性(或一致性)属性。Quorums定义为一组接受者的子集,其中任意两个子集(即任意两个quorum)共享至少一个成员。通常,仲裁是参与的接受方的任何多数。例如,给定一组接受者{A,B,C,D},多数仲裁将是任意三个接受者:{A,B,C}, {A,C,D}, {A,B,D}, {B,C,D}, {B,C,D}。更一般地,可以给受体分配任意的正权值;在这种情况下,仲裁可以定义为接受方的任何子集,其汇总权重大于所有接受方总权重的一半。 34 | 35 | 36 | 37 | 38 | 39 | --- 40 | 41 | Paxos通常用于需要持久性的地方(例如,复制文件或数据库),其中持久性状态的数量可能很大。即使在一定数量的副本没有响应的情况下,协议也会尝试取得进展。还有一种机制可以删除永久失败的副本或添加新副本。 -------------------------------------------------------------------------------- /resource/markdown/distribution/RequestIdempotency.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resource/markdown/distribution/TokenDesign.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resource/markdown/distribution/TryConfirmCancel.md: -------------------------------------------------------------------------------- 1 |

TCC事务补偿机制(柔性事务方案)

2 | 3 | **TCC** 是 *Try Confirm-Cancel* 的缩写,其本质上还是2PC(两阶段提交协议)。它不像具有[ACID特性](https://github.com/about-cloud/JavaCore)的数据库事务那样刚性 ,TCC是 **柔性事务方案**。为什么称为柔性事务方案呢?因为它不直接依赖于资源管理(RM),回想一下[前面的XA事务](https://github.com/about-cloud/JavaCore),其中就直接依赖于资源管理器,而 TCC 是在业务层面处理的,业务依赖于何种资源管理器、依赖多少资源管理器,TCC 中是不需要担心,所以它的方式更为弹性。 4 | 5 | ![TCC](https://i.loli.net/2019/01/16/5c3f4b5b2e21d.png) 6 | 7 | 8 | 9 | 一个完整的TCC模式有以下组成部分: 10 | 11 | 1、业务应用:直接使用的应用; 12 | 13 | 2、被调用的服务:一般是多个,是主业务应用被调用的服务,以此来获得资源; 14 | 15 | 3、事务协调器:开启事务、注册事务,协调事务内的服务提交/回滚活动,从而达到全局事务的一致性。 16 | 17 | 18 | 19 | #### 第一阶段:Try阶段: 20 | 21 | 检查和预留资源阶段,和2PC的第一阶段相似 -- 占有资源。 22 | 23 | 24 | 25 | #### 第二阶段:Confirm/Cancel阶段: 26 | 27 | Confirm表示确认执行业务操作,Cancel表示取消执行业务操作。 28 | 29 | 30 | 31 | #### TCC 这种入侵业务而不接触资源管理器的优缺点如下: 32 | 33 | **优点:** 资源管理的粒子度可自控,在资源层面可以做到很好的控制; 34 | 35 | **缺点:** 入侵业务代码,耦合度增加,每个被调用的服务都要重复实现Try、Confirm、Cancel接口代码,改造成本高。 36 | 37 | 38 | 39 | 参考资料: 40 | 41 | 阿里技术 -------------------------------------------------------------------------------- /resource/markdown/distribution/WhatisDistributed.md: -------------------------------------------------------------------------------- 1 |

零、巨石系统

2 | 3 | > 巨石系统(Monolith System)又称单体系统,最直观的感受是把一个程序(系统)涉及的各个组件都打包成一个可部署、运行的包(如JAR包、WAR包)。 4 | 5 | 6 | 7 |

一、集群

8 | 9 | ![集群]() 10 | 11 | > 一个小型应用部署在一台性能不错的服务器上,如果访问量和计算量不大,基本上没啥压力。比如个人博客运行在低配置上的服务器,也是能够满足日常使用。随着时间推移,网站的访问量越来越大,业务也越来越复杂,应用程序和服务器的性能受到挑战,传统的做法是 *重构系统* 和升级服务器配置。但这并不是长久之计,因为单台服务器的性能还是有限的。解决办法是加入多台服务器构成 **多处理系统**,同一个应用部署在多台服务器上,每个服务器都会处理完整的任务,这就是 **集群**。每个请求具体交给哪台服务器去处理,通常是配合 *负载均衡/请求分配/集群调度* 来协调工作。这样将较多的访问量分流到多个服务器,每个服务器的压力就小了。 12 | 13 | 14 | 15 |

二、分布式

16 | 17 | ![分布式]() 18 | 19 | > *集群* 解决了单机压力问题,但是随着业务的增长,单应用变的越来越复杂,越来越巨大(也就是 *巨型应用* ),维护和开发成本也会变的更高,而且单机不一定有能力处理一次任务,比如要处理 TB级别 日志。将原来单个应用又拆分成多个小应用,然后部署在多个服务器上,这些小应用共同组成一个大应用,整个系统就是 **分布式系统** 。集群中的每个应用都是 “大应用”,每个 “大应用” 都能处理完整的业务。而分布式系统中的每个应用都是小应用,每个小应用只能处理特点的任务,但它更加灵活,比如一个简单任务可以仅需要较少的应用组成,如搭积木般灵活。 20 | 21 | ![block toy](https://images.unsplash.com/photo-1527689638836-411945a2b57c?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=35ac1f0be2205e93f13bf6d2d006c7f1&auto=format&fit=crop&w=500&q=60) 22 | 23 | 24 | 25 |

三、集群和分布式系统的区别

26 | 27 | -------------------------------------------------------------------------------- /resource/markdown/distribution/XA.md: -------------------------------------------------------------------------------- 1 |

X/Open DTP 与 XA 事务

2 | 3 | **X/Open** 是一个欧洲基金会的公司名称,中文名称国际联盟有限公司。 4 | 5 | 6 | 7 | **X/Open DTP** 全称 *X/Open Distributed Transaction Processing Reference Model* ,中文名称 **X/Open 分布式事务处理参考模型**,是X/Open 基金会规范的一个分布式事务处理模型,当然这个模型也是概念模型。 8 | 9 | ![X/Open DTP](https://i.loli.net/2019/01/16/5c3ea241598f8.png) 10 | 11 | **X/Open DTP** 中定义了三个组件: 12 | 13 | - **AP(Application Program):**应用程序,参与分布式事务中的应用程序,比如微服务程序; 14 | - **RM(Resource Manager):**资源管理器,比如RDBMS(Relational Database Management System)关系数据库管理系统,主要作用还是管理资源,那就意味着,事务想要获得资源,必须要经过RM管理。其中RM自身支持事务(比如InnoDB)。 15 | - **TM(Transaction Manager):**事务管理器,负责协调和管理AP和RM之间的分布式事务完整性。主要的工作是提供AP注册全局事务的接口,颁发全局事务标识(GTID、XID之类 的),存储/管理全局事务的内容和决策并指挥RM做commit/rollback。 16 | 17 | 18 | 19 | **分布式事务下几个概念:** 20 | 21 | - **事务:**一个事务是一个完整的工作单元,由多个独立的计算任务组成,这多个任务在逻辑上是原子的; 22 | - **全局事务:** 对于一次性操作多个资源管理器的事务,就是全局事务; 23 | - **分支事务:** 在全局事务中,某一个资源管理器有自己独立的任务,这些任务的集合作为这个资源管理器的分支任务; 24 | - **TX协议:** AP与TM之间交互协议(接口); 25 | - **XA协议:** TM与RM之间交互协议(接口)。 26 | 27 | 28 | 29 | --- 30 | 31 | #### XA事务 32 | 33 | 提起MySQL的事务,我们最熟悉的几个字 -- **XA事务**。那 XA到底是什么? 34 | 35 | ![2PC](https://i.loli.net/2019/01/16/5c3eaf76a1bd9.png) 36 | 37 | **XA** ,全称 *eXtended Architecture*,是 X/Open组织在1991年发布的分布式事务处理(DTP) **规范**。**XA** 的目标确保跨异构后端数据存储(如数据库、应用程序服务器、消息队列、事务缓存等)执行的“全局事务”中的原子性。为了保证原子性,XA使用 [两阶段提交(2PC)](https://github.com/about-cloud/JavaCore) 来确保所有资源都一致地提交或回滚任何特定事务(即,所有资源都以原子方式执行相同的操作)。 38 | 39 | 具体来说,**XA** 描述了全局事务管理器和特定应用程序之间的接口。希望使用**XA**的应用程序将使用库或单独的服务与XA事务管理器进行交互。然后事务管理器将跟踪事务中的参与者(即应用程序正在写入的各种其他数据存储),并与他们一起执行两阶段提交协议。换句话说,**XA**事务管理器通常位于应用程序与要向其写入事务的服务器的现有连接之上。它维护一个关于提交或中止的决定的**日志**(类似于数据库维护自己写的日志的方式),当数据库失败并需要重新启动时,它可以使用该日志进行恢复。 40 | 41 | 许多软件供应商继续支持**XA**(这意味着软件可以参与XA事务),包括各种关系数据库和消息代理。 42 | 43 | 44 | 45 | #### 优缺点 46 | 47 | > 由于XA使用两阶段提交(2PC),所以该协议的优点和缺点通常适用于XA。其主要优点是XA(使用2PC)允许跨多个异构技术进行原子事务(例如,单个**XA事务**可以包含来自不同供应商的多个数据库和消息代理),而传统的数据库事务仅限于单个数据库。(这句话很重要,也是描述分布式事务和传统数据库事务的最大区别) 48 | 49 | 50 | 51 | > 主要缺点是2PC是一个阻塞协议:其他服务器需要等待 事务管理器 发出关于是否提交或中止每个事务的决定。如果 事务管理器 在等待其最终决定时脱机,则它们将被卡住并保持数据库锁,直到 事务管理器 再次联机并发布其决定。这种对锁的扩展持有可能会破坏使用相同数据库的其他应用程序。解决方案是超时回滚,具体[关于2PC与3PC的文章参考这里](https://github.com/about-cloud/JavaCore)。 52 | 53 | 54 | 55 | > 此外,如果 事务管理器 崩溃并且无法恢复其决策记录(例如,由于如何记录决策的错误,或由于服务器上的数据损坏),则可能需要手动干预。许多XA实现为事务提供了一个“逃生门”,以便独立地决定是提交还是中止(不等待事务管理器的消息),但这有违反原子性保证的风险,因此保留用于紧急情况。 56 | 57 | 58 | 59 | **参考资料:** 60 | 61 | [XA 分布式事务原理](https://blog.csdn.net/wuzhiwei549/article/details/79925618) -------------------------------------------------------------------------------- /resource/markdown/jvm/ClassFileStructure.md: -------------------------------------------------------------------------------- 1 |

类文件结构

2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resource/markdown/jvm/Classloading.md: -------------------------------------------------------------------------------- 1 |

类加载机制

2 | 3 | 从写好的代码到被执行完成,这都与类加载器机制密不可分。就像我们吃食物一样,先用筷子/勺子/叉子将食物放入嘴中,然后通过舌头上的味觉器官验证食物好不好吃、有没有变馊。 4 | 5 | Java的 **“动态性”** 表现在依赖运行期动态加载和动态链接的特点实现。 6 | 7 | 首先, Java 编译器将Java源代码(.java 文件)编译成 Java字节码(.class 文件)。类加载器负责读取 .class 文件(不限于.class文件),并转换成 `java.lang.Class`类的一个实例。每个这样的实例用来表示一个 class类。所有的类加载器都是 `java.lang.ClassLoader`类的一个实例。 8 | 9 |

一、类加载过程

10 | 11 | ![类加载过程](https://upload-images.jianshu.io/upload_images/11476758-1bc7770bbd72e069.png) 12 | 13 | #### 1.1、加载 14 | 15 | ![加载](https://i.loli.net/2018/12/17/5c179c0abadff.jpeg) 16 | 17 | **"类加载"** 过程比较多,而加载是其中第一个步骤,负责将.class文件加载至内存,但又不仅可以从本地 `.class` 文件加载一个类或接口,也可以从JAR包、WAR包、远程网络资源获取并加载到虚拟机,通过动态代理技术可以在运行时动态生成。比如JSP文件也可以生成对应的Class类。 18 | 19 | 20 | 21 | #### 1.2、验证 22 | 23 | ![验证](https://i.loli.net/2018/12/17/5c179c6357781.jpeg) 24 | 25 | 对加载的Class信息首先要做验证,检测文件格式、元数据、字节码等信息是否合法,是否符合虚拟机的最低要求,校验文件是否对虚拟机有害。 26 | 27 | 28 | 29 | #### 1.3、准备 30 | 31 | 前面的文章讲过,变量所需的内存大小在编译期就已经确定,而类加载的准备阶段是为类变量分配内存并设置类变量的初始值(区别于用户指定值),这些类变量的使用的内存区域是线程共享的方法区(Method Area)。 32 | 33 | 34 | 35 | #### 1.4、解析 36 | 37 | ![解析](https://i.loli.net/2018/12/17/5c179d350dc8f.jpeg) 38 | 39 | 虚拟机将用于标识引用的符号替换为实际指向的引用的地址。符号或符号引用只不过是个标识(描述符),而实际地址才是真正的目的内存位置。 40 | 41 | 42 | 43 | #### 1.5、初始化 44 | 45 | 给类的静态变量初始化值,不同于准备阶段,此处是使用用于自定义的值进行赋值。 46 | 47 | 48 | 49 |

二、双亲委派模型

50 | 51 | ![双亲委派模型](https://i.loli.net/2018/12/17/5c17bc6c3f45c.png) 52 | 53 | 一个类唯一存在虚拟机的判定,是由加载它的类加载器和这个类本身共同决定。如果同一个Class类文件由不同的类加载器加载至虚拟机,那么会存在两个不同的类。 54 | 55 | 那么如何保证相同的Class类文件均有同一个类加载器加载,而且存在多样化的加载器呢? 56 | 57 | 如上图所示,顶层的 **Bootstrap ClassLoader** 是启动类加载器,所有的类最终都是有它来加载,那么同一个的Class类文件无论加载多少次,仅在虚拟机存在一个类。其他类加载器直接或间接是启动类加载的器的子类。这是一个典型的树形数据结构,从树的每个节点向上遍历总能找到根节点。那么其他类加载器仅需要将类加载的工作向上委托,最终都会委托给启动类加载(根类加载器),这样的模型称为类加载的 **双亲委派模型** 。 58 | -------------------------------------------------------------------------------- /resource/markdown/jvm/GarbageCollectionAlgorithm.md: -------------------------------------------------------------------------------- 1 |

零、垃圾收集算法

2 | 3 | > 标记-清除算法、复制算法、标记-整理算法(标记-压缩算法)、分代收集算法 4 | 5 |

一、标记-清除算法

6 | 7 | > 分为两个阶段:标记阶段和删除阶段。标记阶段是标记哪些对象要被清除,清除阶段阶段是回收对象实例。 8 | 9 | ![标记-清除算法]() 10 | 11 |

二、复制算法

12 | 13 | 14 | 15 |

三、标记-整理算法(标记-压缩算法)

16 | 17 | 18 | 19 |

四、分代收集算法

20 | 21 | 新生代:复制算法 22 | 老年代:标记-整理算法 -------------------------------------------------------------------------------- /resource/markdown/jvm/GarbageCollector.md: -------------------------------------------------------------------------------- 1 |

零、GC垃圾收集器

2 | 3 | ![GC垃圾收集器](https://upload-images.jianshu.io/upload_images/11476758-a1cf2fa524ae40cf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | * Serial 串行收集器(年轻代:Serial New复制,老年代:Serial Old标记-整理) 6 | 7 | - Parallel 并行收集器(年轻代:Parallel New复制、Parallel Scavenge(低延时)复制,老年代:Parallel Old标记-整理) 8 | - CMS(Concurrent Mark Sweep)并发-标记清除收集器(老年代:标记-清除,需要Serial Old) 9 | - G1(Garbage First)收集器(Region区域,收集大的垃圾) 10 | - Epsilon(No-op 无操作)垃圾收集器 11 | - ZGC 可扩展、低延时的垃圾收集器 12 | 13 |

一、Serial 串行垃圾收集器

14 | 15 | ![Serial 串行垃圾收集器]() 16 | 17 | > **特点**:单线程 18 | > **垃圾收集算法**:分代垃圾回收算法,新生代采用 **复制算法**,老年代采用 **标记-整理算法** 。 19 | > **缺点**:延迟大,收集垃圾时首先 **暂停所有工作线程Stop-The-World**(简称STW),然后单线程地进行收集垃圾。 20 | > **适用于**:对GC延迟不敏感的客户端。另外,如果很多个jvm运行在同一台机器上时,也适合使用这种算法。 21 | > **开启参数**:`-XX:+UseSerialGC` 22 | 23 | ##### Serial Old老年代收集器 24 | 25 | > 这也是一个单线程的垃圾收集器,作用于老年代。 26 | > 27 | > 作用:搭配Parallel Scavenge使用或作CMS老年代垃圾回收的备用方案,只是CMS失败时才用Serial Old。 28 | 29 | 30 | 31 |

二、Parallel 并行垃圾收集器

32 | 33 | ![Parallel 并行垃圾收集器]() 34 | 35 | > Parallel 并行垃圾收集器Parallel New、Parallel Scavenge、Parallel Old 36 | 37 | #### 2.1、Parallel New 38 | 39 | > 特点:Parallel New是多线程(进行年轻代垃圾收集,是Serial多线程版本。当主机有N个 CPU 时,默认会使用N个垃圾收集线程。使用`-XX:ParallelGCThreads=<线程数目>`控制线程数),ParOld默认单线程。 40 | > 垃圾收集算法:**标记-整理算法**(另外,使用`-XX:+UseParallelOldGC`命令行选项可以让新生代和老年代同时使用多线程并行垃圾收集。在老年代,ParallelOld是一种 标记-整理 算法,会在标记后将存活对象搬运到一起来防止出现内存碎片。) 41 | > 缺点:注意,在单核主机上,是不会使用Parallel GC的,在2核主机上,Parallel GC 可能和默认的 GC 收集器工作效率相同,而在大于2核的主机上,Parallel GC 一般会有更好的表现。当然,一切还是需要以实际测量为准。GC延迟大。 42 | > 适用于:Parallel GC适合于需要高吞吐量而对暂停时间不敏感的场合,比如批处理任务。 43 | > 44 | > 开启参数:`-XX:+UseParallelGC` 45 | 46 | #### 2.2、Parallel Scavenge 47 | 48 | > **Parallel Scavenge和Parallel New 都是作用于新生代的垃圾收集器**,区别在于,Parallel Scavenge牺牲垃圾回收的吞吐量,达到低延时的垃圾回收时,从而使得应用线程的等待状态大大降低。同时垃圾收集更加频繁。 49 | > **Parallel Scavenge** 的目的是尽可能缩短垃圾收集时用户线程的停顿时间。 50 | 51 | #### 2.3、Parallel Old 52 | 53 | > Parallel Old是Parallel Scavenge的老年代版本,多线程收集器。起始于jdk1.6。 54 | 55 | 56 | 57 |

三、CMS 并发标记-清除收集器

58 | 59 | > CMS全称ConcurrentMarkSweep。 60 | > 61 | > 产品定位:以 **最短回收停顿时间** 为目标的收集器。 62 | > 背景:目前基于B/S系统服务尤其注重 **响应速度**,希望系统停顿时间短。 63 | > 特点:最小化响应时间,GC线程在某些垃圾收集步骤中可以和应用线程并行进行。 64 | > 垃圾收集算法:TODO 65 | 66 | #### CMS垃圾收集的周期(3标记1清除): 67 | 68 | > - **初始标记(CMS-initial-mark)**:标记roots能直接引用到的对象; 69 | > - **并发标记(CMS-concurrent-mark)**:进行GC root跟踪 70 | > - **重新标记(CMS-remark)**:修正并发标记阶段因用户线程运行而导致的变动 71 | > - **并发清除(CMS-concurrent-sweep)**:进行并发清除工作 72 | 73 | #### CMS的优点: 74 | 75 | > 1)、初始标记和重新标记会导致 Stop-The-World。`由于最耗时的并发标记和并发清除都可以和用户程序同时进行,所以其实可以认为 GC 和用户程序是同时进行的。` 76 | > 2)、`CMS的一个劣势是对CPU资源比较敏感,不过现代的后端系统通常是重IO的,所以个人感觉影响并不会太大。`另外一个问题是,由于 GC 和用户程序同时进行,可能会有部分新产生的垃圾无法被直接回收,需要等到下一次 GC 时再回收。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此 **CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用** 。使用`-XX:CMSInitiatingOccupancyFraction`可以定义老年代使用了多少空间后就开始进行 GC,默认为68%。 77 | > 3)、 CMS的在新生代与 Parallel 一样使用 **并行复制收集器**,而在老年代使用 **标记-清除 算法**。`在老年代,由于不会进行整理操作,会导致空间碎片的出现,如果空间碎片过多,则需要使用Serial Old进行一次老年代垃圾回收,为了防止该情况出现,可以设置若干次CMS GC以后进行一次内存整理;同时,由于和应用线程并行的进行垃圾回收,所以需要在内存耗尽前就开始垃圾收集,否则可能导致应用无内存可用。` 78 | 79 | #### 启用参数 80 | 81 | > 启用CMS参数`-XX:+UseConcMarkSweepGC`。 82 | > 83 | > CMS默认回收线程数目是 `ParallelGCThreads + 3)/4` ,`ParallelGCThreads` 是年轻代并行收集线程数目,可以通过`-XX:ParallelCMSThreads=2` 来设置。 84 | > 85 | > 可以使用`-XX:+UseCMSCompactAtFullCollection` 开启CMS阶段整理内存。 86 | > 87 | > 可以使用`-XX:+CMSFullGCsBeforeCompaction=<次数>` 设置每多少次CMS后进行一次内存整理。 88 | > 89 | > 老年代进行CMS时的内存消耗百分比,默认为68,可以使用`-XX:CMSInitiatingOccupancyFraction=百分比`设置。 90 | 91 | 92 | 93 |

四、G1收集器(Garbage First)

94 | 95 | > 前言:G1是具有全新、划时代意义的、比较前沿的、流行广泛的垃圾收集器,可以替换JDK1.5的CMS。 96 | > 垃圾收集算法:分代收集 97 | > 优点: 98 | > - 并行与并发:G1收集器通过 **并发** 方式让Java工作程序继续运行。 99 | > - 分代收集:G1保留着分代收集的概念,但它不像CMS收集器那样需要Serial Old(在收集老年代时)配合使用,**G1可以独立不依赖其他收集器工作**。 100 | > - 空间整合:CMS老年代是标记-清除,**G1是标记-整理,从局部(两个Region之间)来看是基于复制算法**。不会像CMS那样产生碎片空间,收集后可提供规整的内存空间。 101 | > - 可预测停顿:CMS和G1都是追求垃圾收集时间的低延迟,相比而言,**G1还能预测停顿时间模型**。 102 | 103 | #### 特点: 104 | 105 | > 其他收集器的收集范围是整个年轻代或老年代,而G1则不是这样的了。使用G1时,Java堆的内存分布差别很大,它将整个Java堆划分为多个大大小小相等的独立 **区域(Region)**,虽然像其他收集器保留着年轻代、老年代的分代概念,但是新生代和老年代不再是物理隔离的了,它们都是逻辑上连续的多个Region的集合。 106 | > 优先回收策略:**优先回收最大的Region,保证了有限时间可以仅可能提供工作效率。** 107 | 108 | #### G1垃圾收集的周期(3标记1回收): 109 | 110 | > - **初始标记**(Initial Marking) 111 | > - **并发标记**(Concurrent Marking) 112 | > - **最终标记**(Final Marking) 113 | > - **筛选回收**(Live Data Counting and Evacuation) 114 | 115 | #### 区域Region: 116 | 117 | > 区域Region大小是 2 的次幂,范围是 1 MB 到 32 MB 之间。目的是根据最小的 Java 堆大小划分出约 1024/2048 个区域。 118 | 119 | 120 | 121 |

五、ZGC 收集器

122 | 123 | > 此款垃圾收集器是jdk11新添加的垃圾收集器。它是可扩展、低延时的垃圾的垃圾收集器,支持TB级的内存区域,最大堆内存4TB,延时不超过 10ms。 124 | 125 | #### 目标: 126 | 127 | > - 每次GC STW的时间不超过10ms 128 | > - 能够处理从几百M到几T的JAVA堆 129 | > - 与G1相比,吞吐量下降不超过15% 130 | > - 为未来的GC功能和优化利用 **有色对象指针(colored oops)** 和 **加载屏障(load barriers)** 奠定基础 131 | > - 初始支持Linux/x64 132 | 133 | #### 特点: 134 | 135 | > - 并发 136 | > - 基于Region 137 | > - 标记-整理 138 | > - NUMA感知 139 | > - 使用有色对象指针( 140 | > - 使用加载屏障 141 | > - 仅root扫描时出现延时STW(Stop-The-World),因此GC暂停时间不会随堆的大小而增加。 142 | 143 | #### 垃圾回收步骤: 144 | 145 | > - **递归标记** 146 | > - **迁移对象** (relocate) 147 | 148 | 149 | 150 |

六、Epsilon(No-op 无操作)垃圾收集器

151 | 152 | > Java 11还加入了一个比较特殊的垃圾回收器——Epsilon,该垃圾收集器被称为“no-op”收集器,将处理内存分配而不实施任何实际的内存回收机制。 也就是说,这是一款不做垃圾回收的垃圾回收器。它的作用就是与“做垃圾回收的收集器”做对比使用。 -------------------------------------------------------------------------------- /resource/markdown/jvm/RuntimeDataAreas.md: -------------------------------------------------------------------------------- 1 |

一、运行时数据区

2 | 3 | ![运行时数据区](https://i.loli.net/2018/12/13/5c11e3f47e659.png) 4 | 5 | #### :womans_hat:线程私有的内存区域: 6 | 7 | - 程序计数器、虚拟机栈、本地方法栈 8 | 9 | #### :closed_umbrella:线程共享的内存区域: 10 | 11 | * 方法区、Java堆 12 | 13 | 14 | 15 |

二、线程私有的内存区域

16 | 17 | #### :curry:2.1 程序计数器 18 | 19 | > **Program Counter**,简称 **PC**,用于存放 *下一条* 指令所在单元的地址,是`线程所执行的字节码的行号指示器`。因为JVM的多线程是通过轮流切换来分配CPU的执行时间(时间片轮询),当切换到下一条线程的时候,**线程要能知道当前要执行的字节码位置**,这就要求每条线程都要有一个自己的程序计数器,独立`存储`待执行的虚拟机字节码指令的地址。 20 | 21 | #### :custard:2.2 虚拟机栈 22 | 23 | ![虚拟机栈](https://i.loli.net/2018/12/13/5c1229db978ad.png) 24 | 25 | > **虚拟机栈**生命周期同线程,所以不必担心垃圾回收问题。**虚拟机栈(VM Stack)**这个内存区域对应的是每个线程class方法执行的内存模型(不仅Java,像Kotlin、Scala、Groovy等于都可以编译成class文件并允许在JVM上)。线程中的每个方法在执行的同时都会创建一个 **栈帧(Stack Frame)**。每个class方法从`被调用`到`执行完成`的过程,都对应着一个栈帧在虚拟机栈从`入栈`到`出栈`的过程。 26 | 27 | > **栈帧**是用来**存储** **(方法内的)局部变量表、(计算时的)操作数栈、(引用的)动态链接、(进入或退出方法时的)方法出入口**等。 28 | > * **局部变量表**:存放 **编译期** 可知的数据类型,包括 **基本数据类型**(boolean、byte、char、short、int、long、float、double)、**对象引用**(reference类型)、**returnAddress类型**(指向一条字节码指令的地址)。每个 **局部变量空间(slot)**的长度是 **32位**,而 **long**、**double**类型的每个变量占用 **2个局部变量空间**,也就是会占用64位空间。局部变量表所需的内存大小在编译期就已经确定,并记录在class文件中。 29 | > * **操作数栈**:栈是一个先进后出的线性表,操作数栈是逻辑计算的数据容器,比如下面的计算: 30 | > ```java 31 | > int a = 1; 32 | > int b = 2; 33 | > int c = 1 + 2; 34 | > retuen c; 35 | > ``` 36 | 37 | #### :melon:2.3本地方法栈 38 | 39 | >本地方法栈和虚拟机栈相似,只不过虚拟机栈是用于线程中执行Java方法(字节码,因为还有Scala、Kotlin、Groovy等其他运行在虚拟机上的编程语言),本地方法栈是用于执行本地的 **native方法** 。 40 | 41 | 42 | 43 |

三、线程共享的内存区域

44 | 45 | #### :tangerine:3.1 堆 46 | 47 | > 堆Heap又称 **Java堆**,这个内存区域是JVM线程共享的区域,也是JVM管理的最大一块内存区域。堆内存区域创建于JVM启动时。 48 | > 49 | > **堆**的 **唯一** 目的就是存放 **实例对象**,反之不成立,也就是说有少量的对象实例并不在Java堆中存放,也可以在栈上分配,**Java堆仅存放绝大量的对象实例**。 50 | 51 | > **Java堆** 不像 栈 那样,随线程而生、随线程而亡。Java堆中的实例对象可能被多个线程中的变量所引用,一个线程死亡,而其他线程还可能正使用此实例对象,所以 **Java堆** 成为垃圾收集器收集垃圾一个重要的区域(宏观来讲是垃圾收集器管理的主要区域)。 52 | 53 | > 现在主流的垃圾收集器采用 **分代收集算法**,堆内存又被细分为**新生代(Young Generation)** 和 **老年代(Old Generation)**(默认比: 1:2)。**新生代** 再细分为:1个**Eden空间**和**2个Survivor空间**(默认比例8:1:1),2个Survivor空间其中一个是**From Survivor空间**和一个**To Survivor 空间**。**分代分空间的唯一理由就是优化GC性能**。具体jvm查看命令可参考[jvm内存分析命令文章](https://github.com/about-cloud/JavaCore)) 54 | 55 | > 常量池 56 | > * **静态常量池**:即*.class文件中的常量池,class文件中的常量池不仅仅包含**字符串(数值)字面量**,还包含**类、方法的信息**,占用class文件绝大部分空间; 57 | > * **运行时常量池**:则是JVM虚拟机在完成 **类装载** 操作后,将class文件中的常量池载入到内存中,并保存在 **方法区** 中,我们常说的 **常量池,就是指方法区中的运行时常量池,存放在字面量和符号引用**。 58 | 59 | 60 | 61 | #### :strawberry:3.2方法区 62 | 63 | > 方法区用来`存储`类信息(类的版本、接口描述)、常量、静态变量、方法(数据与编译后代码)等数据,也有一些方法区的数据生命周期长,进而将方法区称为“永久代(Permanent Generation)”,永久存在不被GC回收,但本质上并不相等。方法区在逻辑上是和Java堆相连的,该区域一般GC很少参与,偶尔会存在对不使用的常量进行内存回收,对于一些卸载的类进行资源回收,因为这些数据占用内存本来就比较少,所以GC的回收效果也非常的一般。 64 | 65 | - JDK1.6的永久代(PermGen) 66 | - JDK1.7的永久代(PermGen)(过度阶段) 67 | - JDK1.8的元空间(Metaspace)(直接内存) 68 | 69 | > 用jdk1.6运行后会报错,永久代这个区域内存溢出会报: 70 | > Exception in thread “main” java.lang.OutOfMemoryError:PermGen space的内存溢出异常,表示永久代内存溢出。 71 | > 72 | > 在Java7之前,HotSpot虚拟机中将GC分代收集扩展到了方法区,使用永久代来实现了方法区。这个区域的内存回收目标主要是 **针对常量池的回收和对类型的卸载**。但是在之后的HotSpot虚拟机实现中,逐渐开始将方法区从永久代移除。Java7中已经将运行时常量池从永久代移除,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。而在Java8中,已经彻底没有了永久代,将方法区直接放在一个与堆不相连的本地内存区域,这个区域被叫做**元空间(MetaSpace)**。 73 | > 74 | > 使用jdk1.7后 75 | > 验证如下:执行代码和上面相同 76 | > 设置参数:-Xmx20m -Xms20m -XX:-UseGCOverheadLimit,这里的-XX:-UseGCOverheadLimit是关闭GC占用时间过长时会报的异常,然后限制堆的大小,运行程序,果然,一会后报异常: 77 | > 78 | > Exception in thread “main” java.lang.OutOfMemoryError: Java heap space 79 | > 从上面的异常可以知道我们测试增加的常量都放到了堆中,所以限制堆内存以后,不断增加常量,堆内存会溢出。 80 | 81 | > 总结:**jdk1.6常量池放在方法区,jdk1.7常量池放在堆内存,jdk1.8放在元空间里面,和堆相独立。所以导致String的intern()方法因为以上变化在不同版本会有不同表现。** -------------------------------------------------------------------------------- /resource/markdown/microservice/MicroserviceAndSOA.md: -------------------------------------------------------------------------------- 1 |

微服务

2 | 3 | ![微服务]() 4 | 5 | *微服务* 是一种架构风格,全称是 **微服务架构** ,英文名 *Microservice Architecture* 6 | 7 | 8 | 9 | 不一定是单机支撑不了整个应用,而更多强调的是业务上的拆分。 10 | 11 | 12 | 13 | 14 | 15 |

SOA

16 | 17 | **SOA** 是 *Service-oriented architecture* 简写,中文名称 **面向服务的架构** ,一种软件设计风格,通过网络通信协议,应用程序组件向其他组件提供服务。面向服务的体系结构的基本原则是独立于供应商、产品和技术的。服务是一个独立的功能单元,可以远程访问、独立操作和更新,例如在线检索信用卡对账单。 18 | 19 | 20 | 21 | 根据SOA的许多定义之一,服务有四个属性: 22 | 23 | 1. 它在逻辑上表示具有指定结果的业务活动; 24 | 2. 它是独立的; 25 | 3. 对于消费者来说,这是一个黑盒; 26 | 4. 它可能由其他基础服务组成。 27 | 28 | 可以结合使用不同的服务来提供大型软件应用程序的功能,[5]SOA与模块化编程共享的原则。面向服务的体系结构集成了分布式、单独维护和部署的软件组件。它是由技术和标准支持的,这些技术和标准促进了组件在网络上的通信和合作,特别是在IP网络上。 29 | 30 | 31 | 32 | 在SOA中,服务使用协议来描述如何使用描述元数据传递和解析消息。此元数据描述了服务的功能特性和服务质量特性。面向服务的体系结构旨在允许用户组合大量功能以形成纯粹从现有服务构建的应用程序,并以特殊方式组合它们。服务向请求者提供了一个简单的接口,它抽象出充当黑盒的底层复杂性。其他用户也可以在不了解其内部实现的情况下访问这些独立的服务。 33 | 34 | 35 | 36 | 参考资料: 37 | 38 | [Wikipedia-Service-oriented architecture](https://en.wikipedia.org/wiki/Service-oriented_architecture) -------------------------------------------------------------------------------- /resource/markdown/multithreads/AbstractQueuedSynchronizer.md: -------------------------------------------------------------------------------- 1 |

并发包的基石 -- 队列同步器 AQS

2 | 3 | #### 本文关注点 4 | * **AbstractOwnableSynchronizer** 抽象的、可拥有的同步器 5 | * `★★★★★`**AbstractQueuedSynchronizer**抽象的、队列(排队)的同步器 6 | * **Sync** 同步 7 | * **FairSync** 公平同步 8 | * **NonfairSync** 非公平同步 9 | 10 | --- 11 | ![AQS](https://upload-images.jianshu.io/upload_images/11476758-15ca6a3e1415a256.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 12 | 13 | 14 | #### 一、 `AbstractOwnableSynchronizer` 抽象的、可拥有的同步器 15 | ![AbstractOwnableSynchronizer的方法与属性](https://upload-images.jianshu.io/upload_images/11476758-f0950e50411f1845.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 16 | 17 | * **源码分析:** 18 | ```java 19 | package java.util.concurrent.locks; 20 | 21 | /** 22 | * 可由线程独占的同步器。 23 | * 该类为创建锁和相关的同步器提供了基础,可能需要拥有所有权的概念。 24 | * AbstractOwnableSynchronizer 类本身不管理或使用此信息。 25 | * 但是,子类和工具可以使用适当维护的值来帮助控制和监视访问并提供诊断。 26 | * 27 | * @since 1.6 28 | * @author Doug Lea 29 | */ 30 | public abstract class AbstractOwnableSynchronizer 31 | implements java.io.Serializable { 32 | 33 | /** 即使所有的字段都是短暂的(被禁止序列化的),也要使用 序列ID。*/ 34 | private static final long serialVersionUID = 3737899427754241961L; 35 | 36 | /** 37 | * 供子类使用的空的构造方法。 38 | */ 39 | protected AbstractOwnableSynchronizer() { } 40 | 41 | /** 42 | * 独占模式同步下的当前(线程)拥有者。(也就是当前独占锁的线程) 43 | */ 44 | private transient Thread exclusiveOwnerThread; 45 | 46 | /** 47 | * 设置 当前拥有独占访问的 线程。 48 | * 空参数指示没有线程拥有访问权限。 49 | * 此方法不另外施加任何同步化易失性 volatile 字段访问。 50 | * @param thread 所有者的线程(拥有当前独占访问权的线程) 51 | */ 52 | protected final void setExclusiveOwnerThread(Thread thread) { 53 | exclusiveOwnerThread = thread; 54 | } 55 | 56 | /** 57 | * 返回 setExclusiveOwnableThread(Thread thread) 方法最后设置的线程, 58 | * 如果从未设置,则返回 null。 59 | * 此方法不另外施加任何同步化易失性 volatile 字段访问。 60 | * @return 所有者的线程(拥有当前独占访问权的线程) 61 | */ 62 | protected final Thread getExclusiveOwnerThread() { 63 | return exclusiveOwnerThread; 64 | } 65 | } 66 | ``` 67 | > **小结:** 68 | > **独占、持有锁的线程成员变量:Thread exclusiveOwnerThread** 69 | > **setExclusiveOwnerThread(Thread thread)** 设置独占、持有锁的线程成员变量 70 | > **getExclusiveOwnerThread()** 获得独占、持有锁的线程成员变量 71 | 72 | 73 | #### 二、 `AbstractQueuedSynchronizer` 抽象、队列同步器 AQS 74 | ![AbstractQueueSynchronizer公有属性和公有方法.png](https://upload-images.jianshu.io/upload_images/11476758-10ff5c98c4f752a9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 75 | 76 | ![AbstractQueuedSynchronizer 私有方法.png](https://upload-images.jianshu.io/upload_images/11476758-b373d216213b6845.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 77 | 78 | ![AbstractQueuedSynchronizer final域与私有域.png](https://upload-images.jianshu.io/upload_images/11476758-4e2da27a6904ad4d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 79 | 80 | * **AbstractQueuedSynchronizer 简介** 81 | > 提供了一个基于 **FIFO队列**,可以用于构建锁或者其他相关同步装置的基础框架。该 **抽象队列同步器**(以下简称**队列同步器**)利用了一个 **private volatile int state**来表示 **同步状态**,期望它能够成为实现大部分同步需求的基础。使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似 **acquire获取** 和 **release释放** 的方式来 **操纵状态**。然而 **多线程环境中对状态的操纵必须确保原子性**,因此子类对于状态的把握,需要使用这个同步器提供的以下三个方法对状态进行操作: 82 | > * private volatile int **state**; 83 | > * java.util.concurrent.locks.AbstractQueuedSynchronizer.**getState()** 84 | > * java.util.concurrent.locks.AbstractQueuedSynchronizer.**setState(int newState)** 85 | > * java.util.concurrent.locks.AbstractQueuedSynchronizer.**compareAndSetState(int expect, int update)** 86 | * **2.1、源码分析:** 87 | ```java 88 | /** 89 | * 返回同步状态的当前值。 90 | * 此操作具有易失性 volatile 读取的内存语义。 91 | * @return 当前 state 的值 92 | */ 93 | protected final int getState() { 94 | return state; 95 | } 96 | 97 | // ------------------- 华丽的分割线 ------------------- 98 | /** 99 | * 设置同步状态 state 的值。 100 | * 此操作具有易失性 volatile 写的内存语义。 101 | * @param newState 状态 state 的新值 102 | */ 103 | protected final void setState(int newState) { 104 | state = newState; 105 | } 106 | 107 | // ------------------- 华丽的分割线 ------------------- 108 | /** 109 | * 如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。 110 | * 此操作具有易失性 volatile 读取和写入的内存语义。 111 | * 112 | * @param expect 期望的值 113 | * @param update 新值 114 | * @return 如果成功则为 true 。 错误返回表示实际值不等于预期值的指示。 115 | */ 116 | protected final boolean compareAndSetState(int expect, int update) { 117 | // See below for intrinsics setup to support this 118 | return unsafe.compareAndSwapInt(this, stateOffset, expect, update); 119 | } 120 | ``` 121 | 122 | * **2.2、FIFO 队列** (类似双向链表) 123 | > 同步器的开始提到了其实现依赖于一个 **FIFO 队列**,那么队列中的 **元素节点Node** 就是保存着 **线程引用** 和 **线程状态** 的 **容器**,**每个线程对同步器的访问,都可以看做是队列中的一个节点Node**。Node的主要包含以下成员变量: 124 | ```java 125 | static final class Node { 126 | /** 作为标记表示节点正在 共享模式 中等待 */ 127 | static final Node SHARED = new Node(); 128 | /** 作为标记表示节点正在 独占模式 下等待 */ 129 | static final Node EXCLUSIVE = null; 130 | 131 | int waitStatus; 132 | volatile Node prev; 133 | volatile Node next; 134 | volatile Thread thread; 135 | volatile Node nextWaiter; 136 | 137 | final boolean isShared() { 138 | return nextWaiter == SHARED; 139 | } 140 | } 141 | ``` 142 | * **2.3、API** 143 | 144 | | 方法名称 | 描述 | 145 | | ------ | ------- | 146 | | protected boolean tryAcquire(int arg) | 独占式获取锁,实现该方法需要查询当前状态并判断同步状态是否和预期值相同,然后使用 CAS 操作设置同步状态 | 147 | | protected boolean tryRelease(int arg) | 独占式释放锁,实际也是修改同步变量 | 148 | | protected int tryAcquireShared(int arg) | 共享式获取锁,返回大于等于 0 的值,表示获取锁成功,反之获取失败 | 149 | | protected boolean tryReleaseShared(int arg) | 共享式释放锁 | 150 | | protected boolean isHeldExclusively() | 判断调用该方法的线程是否持有互斥锁 | 151 | 152 | * **其他重要方法** 153 | > private Node **addWaiter(Node mode)** 154 | 155 | > 参考资料: 156 | > [AbstractQueuedSynchronizer的介绍和原理分析](http://ifeve.com/introduce-abstractqueuedsynchronizer/) 157 | 158 | --- 159 | ##### 三、 `Sync`抽象的静态内部类 160 | ![Sync 静态内部类的方法与属](https://upload-images.jianshu.io/upload_images/11476758-9430f3ace4f38a69.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 161 | 162 | 163 | ##### 四、 `FairSync` 公平锁(静态内部类) 164 | ![FairSync 公平同步器](https://upload-images.jianshu.io/upload_images/11476758-3feff7da83e0a35c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 165 | 166 | ##### 五、 `NonfairSync` 非公平锁(静态内部类) 167 | ![NonfairSync 非公平同步器](https://upload-images.jianshu.io/upload_images/11476758-453881e169960163.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -------------------------------------------------------------------------------- /resource/markdown/multithreads/AtomicOperation.md: -------------------------------------------------------------------------------- 1 |

原子操作

2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resource/markdown/multithreads/CLH.md: -------------------------------------------------------------------------------- 1 |

CLH同步队列锁

2 | 3 | > **CLH锁**,通过对前一个节点的 **自旋** 提供公平的竞争锁(**公平锁**)的机会,严格的 **先到先得(FIFO)**。 4 | 5 | -------------------------------------------------------------------------------- /resource/markdown/multithreads/CompareAndSwap.md: -------------------------------------------------------------------------------- 1 |

CAS实现原理

2 | 3 | > **CAS** 是 **CompareAndSwap** 的缩写,意思是 **比较** 并 **交换**。 它是无锁化的实现是经典的乐观锁。 4 | > 5 | > **CAS** 操作很简单,它包含三个操作数:**内存地址V**、**预期原值A**、**新值B**。先比较内存地址V处的值与预期原值A是否相等,如果相等就将内存地址V处更新为新值B。在配合循环使用时,若CAS操作失败,会循环执行或到达某个终止处。此操作配合 **循环** 使用时,又称为 **自旋锁** 的实现方式。 6 | 7 | 8 | 9 |

CAS存在的问题

10 | 11 | #### 1、ABA的问题 12 | 13 | > 比如一个线程操作时,先将内存地址V处的值A更新为值B,然后又将值B更新为值A。最后CAS判断内存地址V处的没有变化,认为操作更新未成功,但实质上是已经更新过了。这就是经典的 **ABA问题**。 14 | 15 | **解决方法**: 16 | 17 | ①加时间戳: 18 | 19 | **②加版本号**: 20 | 21 | 22 | 23 | #### 2、循环开销大 24 | 25 | > **CAS** 这种也是乐观锁,如果线程较多、资源抢占激烈、命中率低的情况下,不断的循环会不断的消耗资源。实现上,可以设置最大循环数,达到最大循环数还没有占有资源就自动放弃,避免无限的循环。 26 | 27 | 28 | 29 | #### 3、最多只能保证一个共享变量的操作 30 | 31 | > CAS 最多只能操作一个共享变量,单体效率低。 -------------------------------------------------------------------------------- /resource/markdown/multithreads/ConcurrentHelperUtil.md: -------------------------------------------------------------------------------- 1 |

并发辅助工具类(很好的玩的工具类)

2 | 3 | 4 | 5 |

一、Exchanger 交换器(两线程间的通信)

6 | 7 | > **使用场景:**用于 `有且仅有两个线程` 间的 `数据传输`,就就是线程间的 `通信` 。它是 `生产者/消费者` d的 ~~`wait() / notify()`~~ 的最佳替代工具。 8 | > **核心原理**:方法 `exchange()`阻塞特性:此方法被调用后等待其他线程来取数据,如果没有其他线程取得数据,则一直 **阻塞**。 9 | * **示例:交替打印奇偶数:** 10 | ```java 11 | public class Print { 12 | public static void main(String[] args) { 13 | // 交换器 14 | Exchanger exchanger = new Exchanger<>(); 15 | // 起始打印数值 16 | Integer startNumber = 1; 17 | // 终止的数值 18 | Integer endNumber= 100; 19 | 20 | // 线程1 21 | Thread t1 = new Thread(new Thread1(exchanger, startNumber, endNumber)); 22 | Thread t2 = new Thread(new Thread2(exchanger, startNumber, endNumber)); 23 | // 设置线程名称 24 | t1.setName("thread-no-1"); 25 | t2.setName("thread-no-2"); 26 | // 启动线程 27 | t1.start(); 28 | t2.start(); 29 | } 30 | } 31 | 32 | /** 33 | * 打印奇数的线程 34 | */ 35 | class Thread1 implements Runnable { 36 | private Exchanger exchanger; 37 | /** 被打印的数 */ 38 | private Integer number; 39 | private final Integer endNumber; 40 | 41 | public Thread1(Exchanger exchanger, Integer startNumber, Integer endNumber) { 42 | this.exchanger = exchanger; 43 | this.number = startNumber; 44 | this.endNumber = endNumber; 45 | } 46 | 47 | @Override 48 | public void run() { 49 | while (number <= endNumber) { 50 | if (number % 2 == 1) { 51 | System.out.println("线程:" + Thread.currentThread().getName() + " : " + number); 52 | } 53 | try { 54 | exchanger.exchange(number++); 55 | } catch (InterruptedException e) { 56 | e.printStackTrace(); 57 | } 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * 打印偶数的线程 64 | */ 65 | class Thread2 implements Runnable { 66 | private Exchanger exchanger; 67 | /** 被打印的数 */ 68 | private Integer number; 69 | private final Integer endNumber; 70 | 71 | public Thread2(Exchanger exchanger, Integer startNumber, Integer endNumber) { 72 | this.exchanger = exchanger; 73 | this.number = startNumber; 74 | this.endNumber = endNumber; 75 | } 76 | 77 | @Override 78 | public void run() { 79 | while (number <= endNumber) { 80 | if (number % 2 == 0) { 81 | System.out.println("线程:" + Thread.currentThread().getName() + " : " + number); 82 | } 83 | try { 84 | exchanger.exchange(number++); 85 | } catch (InterruptedException e) { 86 | e.printStackTrace(); 87 | } 88 | } 89 | } 90 | } 91 | ``` 92 | 93 |

二、Semaphore 信号灯

94 | 95 | ![信号灯限流](https://upload-images.jianshu.io/upload_images/11476758-b0f7445595d94324.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 96 | > **核心原理:** 通过发放设置最大 `许可数`,来限制线程的并发数。 97 | > 默认是 **非公平锁**,效率高。 98 | ```java 99 | public Semaphore(int permits) { 100 | sync = new NonfairSync(permits); 101 | } 102 | 103 | Semaphore semaphore = new Semaphore(5); 104 | try { 105 | semaphore.acquire(); // 获取许可 106 | // 逻辑 107 | 108 | } catch (InterruptedException e) { 109 | e.printStackTrace(); 110 | } finally { 111 | semaphore.release(); // 释放许可 112 | } 113 | ``` 114 | 115 | 116 | 117 |

三、CountDownLatch 倒计时闩(锁)

118 | 119 | ![CountDownLatch](https://upload-images.jianshu.io/upload_images/11476758-d32fe09a960417fb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 120 | 121 | 122 | > **核心原理**:线程以 **组团** 的方式进行任务。 123 | > `count` 作为 `stat 状态`。`await()` 方式将 `阻塞当前线程`,直到 `count 为 0`。 124 | ``` 125 | CountDownLatch countDownLatch = new CountDownLatch(5); 126 | countDownLatch.countDown(); // count - 1 127 | // 预处理 128 | try { 129 | countDownLatch.await(); // 阻塞当前线程 130 | // 大家一起处理的时候,我才处理 131 | } catch (InterruptedException e) { 132 | e.printStackTrace(); 133 | } 134 | ``` 135 | * **Sync同步器** 136 | ```java 137 | private static final class Sync extends AbstractQueuedSynchronizer { 138 | private static final long serialVersionUID = 4982264981922014374L; 139 | 140 | Sync(int count) { 141 | setState(count); 142 | } 143 | 144 | int getCount() { 145 | return getState(); 146 | } 147 | 148 | protected int tryAcquireShared(int acquires) { 149 | return (getState() == 0) ? 1 : -1; 150 | } 151 | 152 | protected boolean tryReleaseShared(int releases) { 153 | // 递减 count; 转换为零时发出信号 154 | for (;;) { 155 | int c = getState(); 156 | if (c == 0) 157 | return false; 158 | int nextc = c-1; 159 | if (compareAndSetState(c, nextc)) 160 | return nextc == 0; 161 | } 162 | } 163 | } 164 | ``` 165 | 166 | 167 | 168 |

四、CyclicBarrier 循环栅栏(循环锁)

169 | 170 | > **核心原理:** 基于 `ReentrantLock` 和 `Condition`。 171 | > `CyclicBarrier` 不仅具有 `CountDownLatch` 的功能,还有实现屏障等待的功能,也就是阶段性同步。 172 | 173 | * **CyclicBarrier与CountDownLatch比较** 174 | * 1)CountDownLatch:一个线程(或者多个),等待另外N个线程完成某个事情之后才能执行;CyclicBarrier:N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。 175 | * 2)CountDownLatch:一次性的;CyclicBarrier:可以重复使用。 176 | * 3)CountDownLatch基于AQS;CyclicBarrier基于锁和Condition。本质上都是依赖于volatile和CAS实现的。 177 | 178 | 179 | 180 |

五、Phaser 移相器

181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /resource/markdown/multithreads/ForkJoinFramework.md: -------------------------------------------------------------------------------- 1 |

java.util.concurrent之:Fork/Join框架

2 | 3 | #### Interfaces 4 | 5 | * ForkJoinPool.ForkJoinWorkerThreadFactory 6 | * ForkJoinPool.ManagedBlocker 7 | 8 | #### Classes 9 | 10 | * ForkJoinPool 11 | * ForkJoinTask 12 | * ForkJoinWorkerThread 13 | * RecursiveTask 14 | * RecursiveAction -------------------------------------------------------------------------------- /resource/markdown/multithreads/JavaMemoryModle.md: -------------------------------------------------------------------------------- 1 |

Java内存模型与原子性、可见性、有序性

2 | 3 | ![JMM](https://i.loli.net/2018/12/18/5c187731f169a.png) 4 | 5 | ### 一、Java内存模型 6 | 7 | *Java Memory Modle*,简称 *JMM*,中文名称 **Java内存模型**,它是一个抽象的概念,用来描述或者规范访问内存变量的方式。因为各中计算机的操作系统和硬件不同,方式机制也可能不同,Java内存模型用于屏蔽(适配)各种差异,以此来达到访问各个平台的一致的效果。这也是Java夸平台的重要原因之一。 8 | 9 | **主内存:** Java内存规定了所有变量都存储在主内存(Main Memory)中,各个线程又有自己的本地内存(工作内存),本地内存保存着主内存中部分变量。具体访问方式如下: 10 | 11 | ![JMM工作方式](https://i.loli.net/2018/12/19/5c19a23c8ac3a.png) 12 | 13 | **1、lock加锁:**为了保证访问主内存变量的线程安全性,在访问前一般会加锁处理; 14 | 15 | **2、read读:**从主内存中读取一个变量到工作内存; 16 | 17 | **3、load加载:**把read读到的变量加载到工作内存的变量副本中; 18 | 19 | **4、use使用:**此时线程可以使用其工作内存中的变量了; 20 | 21 | **5、assign赋值:**将处理后的变量赋值给工作内存中的变量; 22 | 23 | **6、store存储:**将工作内存中的变量存储到主内存中,以新建new 一个新变量的方式存储; 24 | 25 | **7、write写:**将store存在的新变量的引用赋值给被处理的变量; 26 | 27 | **8、unload解锁:**所有的工作做完,最后解锁释放资源。 28 | 29 | 30 | 31 | --- 32 | 33 | ### 二、Java内存模型的三大特性 34 | 35 | #### 1、原子性(Atomicity) 36 | 37 | 这里的原子性如同数据库事务中是原子性,一个或多个操作要么全执行成功要么全执行失败(全不执行)。 38 | 39 | ```java 40 | int a = 1; 41 | a++; 42 | double b = 1.5; 43 | ``` 44 | 45 | Java内存模型只保证单一的操作具有原子性,比如上面的 `int a = 1;` 是一个单子的操作,所以具有原子性。而 `a++` 操作在底层会分为三个操作:1)、读取a的值给临时变量;2)、临时变量a的值加1操作;3)、将加操作后的值赋值给a。每个操作都是原子的,但Java内存模型在多线程下并不能保证多操作具有整体原子性,因为它也不知道这个整体内有多少操作,用户想要达到多操作具有整体原子性,需要对响应的代码块做同步(synchronous)处理,比如使用 有锁的`synchronized` 或 无锁的`CAS`。 46 | 47 | 48 | 49 | #### 2、可见性(Visibility) 50 | 51 | 这里的可见性是内存可见性。 52 | 53 | ![多线程共访变量](https://i.loli.net/2018/12/19/5c19a77a8df9a.png) 54 | 55 | 如上图,线程1和线程2在未同步的情况下对共享内存(主内存)中的变量进行访问,比如两个线程的操作都是对变量a进行加1操作。假设线程1首先获取主内存中变量a的值,随后线程2又获取了主内存变量a的值,此时它们工作内存中a的值都是1,它们各自将a的值加1操作,然后assign至工作内存,工作内存中变量a的值都是2,然后两个线程又将值刷新到主内存,最后的结果是主内存中变量a的值是2。虽然整体对a的值加1操作做了两次操作,但由于线程间的操作是互相隔离的,默认情况下无法感知内存变量的值在随后的变化,也就无法访问内存中最新的变量值,这就是内存可行性的问题。 56 | 57 | 如何解决内存可见性的问题? 58 | 59 | 1)、对进入临界区的线程做同步处理(比如 synchronized),同一时刻仅有一个线程能够访问临界区的资源; 60 | 61 | 2)、使用 volatile 关键字保证内存可见性,它能保证访问临界区资源的所有线程总能看到共享资源的最新值; 62 | 63 | 3)、CAS无锁化。 64 | 65 | 66 | 67 | #### 3、有序性(Ordering) 68 | 69 | 线程内的所有操作都是有序的,既程序执行的顺序按照代码的先后顺序执行。比如下面的示例: 70 | 71 | ```java 72 | int a = 1; 73 | int b = 2; 74 | int c = a + b; 75 | ``` 76 | 77 | 线程内程序会先执行 `int a = 1;` ,然后执行 `int b = 2;` 最后执行`int c = a + b;`。 78 | 79 | 关于 **指令重排序** 和 **顺序一致性模型** 参见后面的章节。 -------------------------------------------------------------------------------- /resource/markdown/multithreads/LockAndLock-free.md: -------------------------------------------------------------------------------- 1 |

Java各种锁与无锁

2 | 3 | 4 | 5 | - 读锁与写锁 6 | - 互斥锁/读写锁 7 | - 独占锁与共享锁 8 | - 可重入锁 9 | - 公平锁/非公平锁 10 | - 乐观锁/悲观锁 11 | - 分段锁 12 | - 自旋锁 13 | - 偏向锁 14 | - 轻量级锁 15 | - 重量级锁 16 | - 锁消除 -------------------------------------------------------------------------------- /resource/markdown/multithreads/ScheduledTask.md: -------------------------------------------------------------------------------- 1 |

定时/计划任务

2 | 3 | #### Interfaces 4 | 5 | * Delayed 6 | * ScheduledExecutorService 7 | * ScheduledFuture 8 | * RunnableScheduledFuture 9 | 10 | #### Classes 11 | 12 | * FutureTask 13 | * ScheduledThreadPoolExecutor 14 | 15 | -------------------------------------------------------------------------------- /resource/markdown/multithreads/SequentialConsistencyModel.md: -------------------------------------------------------------------------------- 1 |

顺序一致性模型

2 | 3 | 4 | 5 | #### 一、竞态条件(Race Condition) 6 | > 计算的正确性取决于 `多个线程` 执行的 `时序` 时,就会发生 **竞态条件**。 7 | 8 | #### 二、顺序一致性模型 9 | > **对内存可见性的保证 10 | > 对多线程并发时的串行化保证** 11 | 12 | ##### 顺序一致性模型的两大特征: 13 | * 一个线程中的所有操作必须按照程序的顺序来执行。 14 | * (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,**每个操作都必须原子执行且立刻对所有线程可见。** 15 | 16 | ![顺序一致性模型](https://upload-images.jianshu.io/upload_images/11476758-f763b4a6a9fff64b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 17 | 18 | > 在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程。同时,每一个线程必须按程序的顺序来执行内存读/写操作。从上图我们可以看出,在任意时间点最多只能有一个线程可以连接到内存。**当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化**。 19 | 20 | * **为了更好的理解,下面我们通过两个示意图来对顺序一致性模型的特性做进一步的说明:** 21 | > 假设有两个线程A和B并发执行。其中A线程有三个操作,它们在程序中的顺序是:A1->A2->A3。B线程也有三个操作,它们在程序中的顺序是:B1->B2->B3。 22 | > 假设这两个线程使用监视器来正确同步:A线程的三个操作执行后释放监视器,随后B线程获取同一个监视器。那么程序在顺序一致性模型中的执行效果将如下图所示: 23 | > ![多线程并发执行多操作的模型](https://upload-images.jianshu.io/upload_images/11476758-16bbbed0a7328ca7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 24 | 25 | > 参考资料: 26 | > [JVM(十二)Java顺序一致性模型](https://blog.csdn.net/yjp198713/article/details/78850590) -------------------------------------------------------------------------------- /resource/markdown/multithreads/ThreadAndProcess.md: -------------------------------------------------------------------------------- 1 |

线程和进程的区别

2 | 3 | #### 1、资源调度单位 4 | 5 | 在计算机中,进程是程序运行所使用资源的基本单位。比如传统的一个Web应用,运行它需要多少资源,操作系统应该分配多少资源,都是进程为基本单位进行分配资源(资源包括内存、CPU、磁盘、I/O等)。我们知道CPU是一台计算机的运算核心和控制核心,它的功能解释并处理计算机指令,而进程这种量级的资源对于CPU来说实在太大了,无法调度,将进程划分为多个称为线程的实体,这些实体(线程)又能被CPU进行调度和分配,线程就是CPU调度和分配资源的基本单位。 6 | 7 | 8 | 9 | #### 2、资源边界 10 | 11 | 每个进程拥有独立的内存空间,进程与进程间的内存资源是相互隔离的,是进程私有的。而多个线程可以共享内存。 12 | 13 | 14 | 15 | #### 3、线程与进程的关系 16 | 17 | 一个进程包含至少一个或多个线程,而一个线程只能属于一个进程。 18 | 19 | -------------------------------------------------------------------------------- /resource/markdown/multithreads/ThreadLocal.md: -------------------------------------------------------------------------------- 1 |

ThreadLocal

2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resource/markdown/multithreads/ThreadPool.md: -------------------------------------------------------------------------------- 1 |

ThreadPoolExecutor线程池

2 | 3 | 线程的创建和销毁都会消耗大量资源,就好像公司每天上午9点工作时就招进一批员工,晚上6点干完活就辞退一批员工,这都会销毁公司大量资源。所以合理利用 “池” 中固定、稳定的线程是非常有必要的。 4 | 5 | ### 扩展关系 6 | 7 | ![ThreadPoolExecutor继承关系](https://i.loli.net/2018/12/12/5c1070e4ab3ca.png) 8 | 9 | #### ThreadPoolExecutor 构造方法 10 | 11 | ThreadPoolExecutor 共有四个构造方法: 12 | 13 | ```java 14 | ThreadPoolExecutor(int, int, long, TimeUnit, BlockingQueue) 15 | 16 | ThreadPoolExecutor(int, int, long, TimeUnit, BlockingQueue, ThreadFactory) 17 | 18 | ThreadPoolExecutor(int, int, long, TimeUnit, BlockingQueue, RejectedExecutionHandler) 19 | 20 | ThreadPoolExecutor(int, int, long, TimeUnit, BlockingQueue, ThreadFactory, RejectedExecutionHandler) 21 | ``` 22 | 23 | 以最多参数的构造方法为例进行分析: 24 | 25 | ```java 26 | public ThreadPoolExecutor(int corePoolSize, 27 | int maximumPoolSize, 28 | long keepAliveTime, 29 | TimeUnit unit, 30 | BlockingQueue workQueue, 31 | ThreadFactory threadFactory, 32 | RejectedExecutionHandler handler) { 33 | // 核心线程池不能小于0 34 | if (corePoolSize < 0 || 35 | // 最大池大小不能小于等于0 36 | maximumPoolSize <= 0 || 37 | // 最大池大小不能小于核心池大小 38 | maximumPoolSize < corePoolSize || 39 | // 存活时间不能小于0 40 | keepAliveTime < 0) 41 | // 否则将会抛出 IllegalArgumentException 非法参数异常 42 | throw new IllegalArgumentException(); 43 | // 工作队列、线程工厂、拒绝执行的处理策略都不能为空,否则将会排除NPE空指针异常 44 | if (workQueue == null || threadFactory == null || handler == null) 45 | throw new NullPointerException(); 46 | this.acc = System.getSecurityManager() == null ? 47 | null : 48 | AccessController.getContext(); 49 | this.corePoolSize = corePoolSize; 50 | this.maximumPoolSize = maximumPoolSize; 51 | this.workQueue = workQueue; 52 | this.keepAliveTime = unit.toNanos(keepAliveTime); 53 | this.threadFactory = threadFactory; 54 | this.handler = handler; 55 | } 56 | ``` 57 | 58 | #### ①参数 corePoolSize 核心线程池大小: 59 | 60 | > 线程池中一直会存活该大小的线程数,即使是没有工作(任务)需要执行。除非设置 `allowCoreThreadTimeOut` 为 `true` ,线程池中的核心线程会超时关闭。 61 | 62 | 63 | 64 | #### ②参数 maximumPoolSize 线程池最大大小: 65 | 66 | > 线程池最大允许同时存活的线程的大小。 67 | 68 | 69 | 70 | #### ③参数 keepAliveTime 线程空闲时间: 71 | 72 | > 当线程池中的线程空闲时间达到 `keepAliveTime` 时,线程会被销毁,仅保留 `corePoolSize` 大小线程,如果`allowCoreThreadTimeOut` 为 `true` ,则线程池中包含核心线程在内空闲线程都会被销毁。 73 | 74 | 75 | 76 | #### ④参数 unit 时间单位: 77 | 78 | > `keepAliveTime` 的时间单位(枚举类型)`TimeUnit` ,其可选单位有 `TimeUnit.DAYS` 天、`TimeUnit.HOURS` 小时、`TimeUnit.MINUTES` 分钟、`TimeUnit.SECONDS`秒、`TimeUnitMILLISECONDS.`毫秒、`TimeUnit.MICROSECONDS`微秒、`TimeUnit.NANOSECONDS`纳秒,常用的是秒。 79 | 80 | 81 | 82 | #### ⑤参数 workQueue 任务队列(工作队列、缓存队列): 83 | 84 | > 当任务所需的线程数达到核心线程数 `corePoolSize ` 时,新任务会放在工作队列中排队(缓存)等待执行。如果任务所需的线程数达到核心线程数 `corePoolSize ` 时,并且工作队列已满时,并且线程池最大大小 `maximumPoolSize ` 大于 核心线程池大小 `corePoolSize` 时,才会创建新的线程去处理任务。 85 | > 86 | > *经常有个错觉,~~认为当任务所需的线程数达到线程池最大线程数 `maximumPoolSize ` 时,新任务才会进入工作队列~~,这是不对的。 87 | 88 | 89 | 90 | #### ⑥参数 threadFactory 线程工厂: 91 | 92 | > 用于创建线程池中线程的工厂。创建线程时,经常会给这一批具有处理相同类型任务的线程命名和线程工厂命名(*线程工厂命名是指给poolName线程池命名,作为线程名称的前缀prefix),以此来标识线程的用处,在分析程序执行信息或排查程序异常问题时,非常有帮助。 93 | 94 | 95 | 96 | #### ⑦参数 handler 拒绝处理策略: 97 | 98 | > 线程池拒绝处理任务有一下两者情况: 99 | > 100 | > 1)、当线程池中的线程数达到核心池大小时,并且任务队列已满,会使用拒绝处理策略;(此种情况就像工作台上的工作量积累已满,又没有足够的人员去处理,那么我们就可以拒绝处理新任务。) 101 | > 102 | > 2)、当调用shutdown() 方法后,会等待线程池中正在执行的任务执行完毕,然后再关闭线程池,如果在调用 shutdown() 方法后(还未真正关闭时),紧接着又有新的任务提交时,会使用拒绝处理策略。(此种情况就像工作到了下班时间,工作人员还在忙着手头上剩余的工作,如果此时又新任务提交,那么我们就可以拒绝处理新任务。) 103 | > 104 | > (当然也可以不拒绝) 105 | 106 | **拒绝处理策略:** 107 | 108 | | 拒绝处理策略 | 是否默认 | 描述 | 109 | | ------------------- | -------- | ---------------------------------------------------- | 110 | | AbortPolicy | 是 | 丢弃任务,抛RejectedExecutionException异常; | 111 | | ~~DiscardPolicy~~ | | 直接忽视,也不会抛出异常,不推荐使用; | 112 | | DiscardOldestPolicy | | 从任务队列中踢出最先进入队列(最后一个执行)的任务; | 113 | | CallerRunsPolicy | | 执行新任务。 | 114 | 115 | -------------------------------------------------------------------------------- /resource/markdown/multithreads/ThreadRule.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

一、重排序

4 | 5 | ![排序](https://i.loli.net/2018/12/19/5c19f0bccee8e.jpeg) 6 | 7 | 前篇文章已经讲了Java内存模型和与其三个特性:原子性、可见性、有序性。但事实上,为了提升程序的执行性能,**编译器** 和 **处理器** 常常会对程序指令序列进行 **重排序**。 8 | 9 | ##### 重排序分为以下几种: 10 | 11 | - 编译器优化重排序 12 | - 处理器重排序 13 | - 指令级并行重排序 14 | - 内存系统重排序 15 | 16 | 17 | 18 |

二、屏障指令

19 | 20 | ![fence](https://i.loli.net/2018/12/19/5c19f01f8d3da.jpg) 21 | 22 | > 内存屏障(Memory Barrier,或称为内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则在一定程度地禁止重排序。 23 | 24 | 25 | 26 |

三、as-if-serial 语句

27 | 28 | > 重排序也不能毫无规则,否则语义就变得不可读, **as-if-serial语句** 给重排序戴上紧箍咒,起到约束作用。 29 | 30 | **as-if-serial语句** 规定重排序要满足以下两个规则: 31 | 32 | - 在单线程环境下不能改变程序执行的结果; 33 | - 存在数据依赖关系代码(指令)片段的不允许重排序。 34 | 35 | 比如下面的代码: 36 | 37 | ```java 38 | int a = 1; // ① 39 | int b = 2; // ② 40 | int c = a + b; // 依赖于 ① 和 ② 41 | return c; 42 | ``` 43 | 44 | 可能会被优化成: 45 | 46 | ```java 47 | int b = 2; // ② 48 | int a = 1; // ① 49 | int c = a + b; // 依赖于 ① 和 ② 50 | return c; 51 | ``` 52 | 53 | 上述的重排序既没有改变单线程下程序运行的结果,又没有对存在依赖关系的指令进行重排序。 54 | 55 | 56 | 57 |

四、happens-before 规则

58 | 59 | > 产生的背景是为了确保多线程操作下具有内存可见性。 60 | > 61 | > 如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。换句话说,操作1 happens-before 操作2,那么操作1的结果是对操作2可见的。 62 | > 63 | > 这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。 64 | 65 | 66 | 67 | #### 规则: 68 | 69 | - **1. 程序顺序规则**:一个线程中的每个操作,happens-before 于该线程中的任意后续操作 70 | - **2. 监视器锁规则**:对一个锁的解锁,happens-before 于随后对这个锁的加锁 71 | - **3. volatile变量规则**:对一个volatile域的写,happens-before 于任意后续对这个volatile域的读 72 | - **4. 传递性**:如果A happens-before B,且B happens-before C,那么A happens-before C 73 | - **5. start规则**:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作 74 | - **6. join规则**:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。 75 | 76 | 77 | 78 | ### 总结: 79 | 80 | **as-if-serial** 语句保证单线程环境下不能改变程序执行的结果,**happens-before** 规则保证多线程环境下不能改变程序执行的结果。 -------------------------------------------------------------------------------- /resource/markdown/multithreads/ThreadStatus.md: -------------------------------------------------------------------------------- 1 |

线程的状态

2 | 3 | ![线程状态图](https://i.loli.net/2018/12/18/5c18688584a91.png) 4 | 5 | #### 1、新建状态(New) 6 | 7 | > 万事万物都不是凭空出现的,线程也一样,它被创建后的状态称为 **新建** 状态。 8 | 9 | 比如: 10 | 11 | ```java 12 | Thread thread = new Thread(); 13 | ``` 14 | 15 | 16 | 17 | #### 2、可运行状态(Runable) 18 | 19 | > 线程被创建后是不能使用的,就是让用户在此期间设置一些属性, 20 | 21 | 比如: 22 | 23 | ```java 24 | // 设置类加载器 25 | thread.setContextClassLoader(System.class.getClassLoader()); 26 | // 设置线程名称 27 | thread.setName("商品服务-product-service"); 28 | // 是否为守护线程/用户线程 29 | thread.setDaemon(false); 30 | // 设置线程优先级 31 | thread.setPriority(5); 32 | ``` 33 | 34 | 通过 `thread.start()` 方法开启线程,开启后意味着该线程 **“能够”** 运行,并不意味着一定会运行,因为它要抢占资源,获取CPU的使用权后,才能运行。所以此状态称为 **可运行状态**。从上图中可以看出,不仅通过 `start()` 启动一个线程后可以进入 Runnable 状态,还可以通过其他方式到达 Runnable 状态。 35 | 36 | 37 | 38 | #### 3、运行状态(Running) 39 | 40 | > 线程通过努力,获得了CPU的使用权,就会进入执行程序,此时状态被称为 **运行状态**。 41 | 42 | 43 | 44 | #### 4、阻塞状态(BLOCKED) 45 | 46 | > 多线程抢占CPU资源,同一时刻仅有一个线程进入临界区,为保证对资源访问的线程安全,同一时刻仅有一个线程进入 synchronized 同步块,而其他未获得访问权的线程将进入 **阻塞状态** 。 47 | 48 | **等待阻塞**:通过调用线程的wait()方法,让线程等待某工作的完成。 49 | 50 | **同步阻塞**:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。 51 | 52 | **其他阻塞**:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 53 | 54 | 55 | 56 | #### 5、睡眠状态 TIMED_WAITING(sleeping) 57 | 58 | > 通过调用对象的wait(time)方法或调用线程的sleep(time)/join(time),等待/睡眠指定的时间,此时该线程会进入TIMED_WAITING(sleeping) 状态,直接时间已到,会进入Runnable状态,重新抢占CPU资源。 59 | 60 | 61 | 62 | #### 6、等待状态 WAITING 63 | 64 | > 通过调用对象的wait()方法,让抢占资源的线程等待某工作的完成,或主动join()其他线程,让当前线程释放资源等待被join的线程完成工作,而该线程将进入 **等待状态** 。 65 | 66 | 67 | 68 | #### 7、死亡状态(Dead) 69 | 70 | > 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。 71 | 72 | -------------------------------------------------------------------------------- /resource/markdown/multithreads/synchronized.md: -------------------------------------------------------------------------------- 1 |

Java内存模型之:synchronized

2 | 3 | 4 | 5 | #### 本文关注点 6 | * **Class文件与对象** 7 | * **synchronized 三种应用方式** 8 | * **synchronized 底层语义原理** 9 | * **JVM对synchronized的优化** 10 | * **synchronized关键点** 11 | 12 | --- 13 | #### 一、Class文件与对象 14 | > 对象头 15 | > ![32位JVM的对象头](https://upload-images.jianshu.io/upload_images/11476758-4907bdaeaac7f527.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 16 | 17 | --- 18 | #### 二、synchronized 三种应用方式 19 | > 用于实例对象方法(对象是锁) 20 | > 用于静态方法(类是锁) 21 | > 用于同步代码块(同步代码块是锁) 22 | 23 | --- 24 | #### 三、synchronized 底层语义原理 25 | > * 锁:就在对象头中 26 | > * Java 虚拟机中的同步(Synchronization)基于 **进入enter** 和 **退出exit** **监视器(Monitor)对象** 实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 **synchronized** 修饰的同步方法。**同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析**。下面先来了解一个概念Java对象头,这对深入理解synchronized实现原理非常关键。 27 | 28 | ##### 2.1、对象加锁: 29 | > 使用 **monitorenter** 和 **monitorexit** 指令分别获取控制权和释放控制权。 30 | 31 | ![monitor](https://upload-images.jianshu.io/upload_images/11476758-633049c76385f4d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 32 | 33 | 34 | ##### 2.1、方法加锁: 35 | > 方法级的同步是隐式,**即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中**。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 **ACC_SYNCHRONIZED** 访问标志区分一个方法是否同步方法。**当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是监视器一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor**。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。 36 | 37 | 38 | 39 | --- 40 | #### 四、JVM对synchronized的优化 41 | > 背景:**Java 6**之后,为了减少获得锁和释放锁所带来的性能消耗,引入了 **偏向锁** 和 **轻量级锁**。 42 | > 锁的状态:**无锁状态、偏向锁、轻量级锁、重量级锁**。 43 | > 锁从 **偏向锁 -> 轻量级锁 -> 重量级锁** 的锁升级是单向的、不可逆的,没有 ~~锁降级~~ 这一说。 44 | 45 | ##### 4.1、偏向锁 46 | > 因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。 47 | 48 | ##### 4.2、轻量级锁 49 | > 轻量级锁所适应的场景是 **线程交替执行同步块** 的场合,如果存在 **同一时刻访问同一锁** 的场合,就会导致轻量级锁膨胀为重量级锁。 50 | 51 | ##### 4.3、`★★★★★`自旋锁 52 | > 自旋锁是乐观的一种表现,乐观的认为很大概率是能够获得锁的。(根据阿里巴巴Java开发规范:如果每次访问冲突的概率小于 **20%**,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于**3次**。) 53 | 54 | ##### 4.4、重入锁 55 | > 从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界区的资源时,这种情况属于 ***重入锁**,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。 56 | 57 | ##### 4.5、`★★★★★`锁消除 58 | > 消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。 59 | 60 | 61 | 62 | --- 63 | #### 五、synchronized关键点 64 | > 中断与synchronized: 65 | > 事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,**如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。** 66 | 67 | 68 | > 参考资料: 69 | > [深入理解Java并发之synchronized实现原理](https://blog.csdn.net/javazejian/article/details/72828483) -------------------------------------------------------------------------------- /resource/markdown/multithreads/volatile.md: -------------------------------------------------------------------------------- 1 |

Java内存模型之:volatile

2 | 3 | 4 | 5 | #### 概念 6 | > 把对 **volatile变量**的单个读/写,看成是使用 **同一个监视器锁** 对这些单个读/写操作做了 **同步**。 7 | > 原理:插入内存屏蔽指令,禁止一定条件下的重排序。 8 | * **volatile** 是轻量级的同步机制 9 | 10 | 11 | 12 | * 举例说明: 13 | ``` 14 | public class Assignment { 15 | int value = 1; 16 | /** 17 | * 加法 18 | */ 19 | public void assign1() { 20 | value = 1; // 单操作 21 | } 22 | public void assign2() { 23 | value = 2; // 单操作 24 | } 25 | } 26 | ``` 27 | * volatile禁止指令重排序也有一些规则,简单列举一下**(重点是存在多操作)**: 28 | > * 1. 当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序 29 | > * 2.当地一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序 30 | > * 3.当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序 31 | 32 | 33 | > 参考资料: 34 | > [深入理解Java内存模型(四)——volatile](http://www.infoq.com/cn/articles/java-memory-model-4) -------------------------------------------------------------------------------- /resource/markdown/mybatis/MultitableQueries.md: -------------------------------------------------------------------------------- 1 | 一对多 collection 标签 2 | 3 | ```xml 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ``` 13 | 14 | 15 | 16 | 17 | 18 | 多对一 association 标签 19 | 20 | ```xml 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /resource/markdown/networking/GetPost.md: -------------------------------------------------------------------------------- 1 |

GET和POST方法区别

2 | 3 | #### 设计初衷: 4 | 5 | - GET -- **从** 指定的资源定位符处请求数据。 6 | - POST -- **向** 指定的资源定位符处提交要被处理的数据。 7 | 8 | #### 最直观的区别: 9 | 10 | - GET - 把参数包含在URL中。 11 | - POST - 把参数包含在请求体重。 12 | 13 | > 注意两个谬论: 14 | > 15 | > 1. GET 方法的参数最多2083字节?:这是早期浏览器地址栏的限制,不是GET请求的限制,不同的浏览器对地址栏url限制长度不一样; 16 | > 2. POST方法比GET方法安全?:只不过片面看待此问题,GET方法传递加密后的参数一样安全,POST方法未加密的请求体一样不安全。 17 | 18 | 19 | 20 | 对于GET方法请求,回将整个请求一并发送至目的地;而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。 -------------------------------------------------------------------------------- /resource/markdown/networking/HTTP1.1HTTP2.0HTTPS.md: -------------------------------------------------------------------------------- 1 |

HTTP协议

2 | 3 | > **HTTP** 协议是大家耳熟能详、也是计算机网络流行广泛的网络协议 -- *HTTP*,HyperText Transfer Protocol)**超文本传输协议**。它诞生于1991年(比我年纪还大),是为传输 HTML(超文本标记语言)格式的文本字符串。HTTP 是建立在TCP之上的协议。它还有一个特点 -- 无状态。 4 | 5 | #### 基本工作流程: 6 | 7 | > 客户端发起HTTP请求,请求里封装一些信息表明目的地和想要的资源。服务端收到请求后开始分析含义,得知客户端想要访问资源时,服务端就把资源响应给客户端。 8 | 9 | 10 | 11 | #### HTTP/0.9 12 | 13 | > 最早比较简单的HTTP/0.9版本,起初只能支持Get这种拉模式的请求,默认端口号80,只支持文本,也没有HTTP头。 14 | 15 | #### HTTP/1.0 16 | 17 | > 经过5年历练发展之后,到了1996年,又发布了新版本的HTTP/1.0协议:新增支持图片视频等格式的数据、新增HTTP头,头里添加了状态码status。关于状态码大全及含义参考:https://github.com/about-cloud/JavaCore。为了提高效率,HTTP 1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。这里有个问题,一个页面包含多个url(比如css、script、图片、音频、视频等),每个url都需要重新发起一次请求、建立单独连接,每次建立连接都要消耗资源,存在的问题就是 **不能复用连接**。 18 | 19 | 20 | 21 |

一、HTTP/1.1

22 | 23 | > 为了解决不能 **复用连接** 等其他问题,HTTP/1.1 支持持久连接(可复用连接)、管道化机制、多种缓存机制,而且还添加了 OPTIONS,PUT, DELETE, TRACE, CONNECT方法 等。 24 | 25 | 26 | 27 |

二、HTTPS

28 | 29 | > **HTTPS**,超文本传输安全协议(Hypertext Transfer Protocol Secure,HTTPS,HTTP over TLS,HTTP over SSL或HTTP Secure)。 30 | > 31 | > 传统的HTTP存在安全问题,HTTPS可以称为安全版本的HTTP,实现是HTTP + SSL/TLS,加密安全的任务就交给SSL/TLS。 32 | 33 | 34 | 35 |

三、HTTP/2.0

36 | 37 | 2015年发布的HTTP/2.0有很多新特点: 38 | 39 | ##### 1.多路复用技术 40 | 41 | ![多路复用技术]() 42 | 43 | ##### 2.服务端推送 44 | 45 | 46 | 47 | ##### 3.头部压缩技术 48 | 49 | 50 | 51 | ##### 4.增加二进制分帧层 52 | 53 | -------------------------------------------------------------------------------- /resource/markdown/networking/IPAddressClassification.md: -------------------------------------------------------------------------------- 1 |

IP地址的5种类型

2 | 3 | > 网络之间互连的协议(*IP*)是Internet Protocol的外语缩写,中文缩写为“网协”。 4 | > 5 | > 首先要明白ip地址的含义,ip地址是分配给英特网上每个主机的唯一标识地址,由 `32位` 二进制来表示,也就是有 `4` 个字节组成。二进制实在不容易记,为了方便使用,又将ip地址划分成十进制的形式,以 `"."` 点号划分成四个字节,表示形式为 `127.0.0.1`。IP地址的这种表示法叫做 **点分十进制表示法**。 6 | 7 | #### A类地址 8 | 9 | 以0开头,第一个字节范围:0~127(1.0.0.0 - 126.255.255.255); 10 | 11 | #### B类地址 12 | 13 | 以10开头,第一个字节范围:128~191(128.0.0.0 - 191.255.255.255); 14 | 15 | #### C类地址 16 | 17 | 以110开头,第一个字节范围:192~223(192.0.0.0 - 223.255.255.255); 18 | 19 | ![C类地址](https://imgsa.baidu.com/exp/pic/item/425773224f4a20a42112a2679a529822730ed0c7.jpg) 20 | 21 | 其中前三个字节表示 **网络号** ,第四个字节表示 **主机号**。 22 | 23 | #### D类地址 24 | 25 | 26 | 27 | #### E类地址 -------------------------------------------------------------------------------- /resource/markdown/networking/JavaNetServerSocket.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hicf/JavaCore/1d7ce34f7a18462c566d33ca702a13d443c52c17/resource/markdown/networking/JavaNetServerSocket.md -------------------------------------------------------------------------------- /resource/markdown/networking/JavaNetSocket.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hicf/JavaCore/1d7ce34f7a18462c566d33ca702a13d443c52c17/resource/markdown/networking/JavaNetSocket.md -------------------------------------------------------------------------------- /resource/markdown/networking/NetworkModel.md: -------------------------------------------------------------------------------- 1 |

ISO的OSI七层网络模型与TCP/IP的四层网络模型

2 | 3 | > 在巨大的 **计算机网络** 中,整个通信线路结构变得越来越复杂。从软件应用到硬件传输,各种问题聚在一块使得更加凌乱不堪。 “大事化小、小事化了” ,把复杂通信问题划分成多个小问题,然后针对每个小问题设计规范准则。流行最广泛的 **网络模型** 一般指 **ISO的OSI七层网络模型** 和 **TCP/IP四层网络模型**,此种常称为 **参考模型**。 4 | 5 | 6 | 7 |

一、ISO的OSI七层网络模型

8 | 9 | > *ISO* 的全称是 国际标准化组织(International Organization for Standardization,*ISO*), OSI(Open System Interconnect)即开放式系统互联。一般称此模型为 *OSI参考模型*。 10 | > 11 | > 七层模型的划分如下: 12 | 13 | ![ISO的OSI七层网络模型](https://i.loli.net/2019/01/10/5c3753e50b452.png) 14 | 15 | #### 7、应用层 16 | 17 | > 为应用程序提供服务,面向最终用户的网络服务。 18 | > 19 | > 数据格式:应用协议数据单元APDU 20 | > 21 | > 常见协议:HTTP、FTP、SMTP、DNS、TELNET、HTTPS、POP3、DHCP等 22 | 23 | #### 6、表示层 24 | 25 | > 由于应用层数据格式不同,比如有音频、视频、文本、图片等格式数据,都需要转义、转码成能被传输的数据。 表示层的作用就是将数据格式化、转义、转码、加密、解密等(有上行、下行之分)。 26 | > 27 | > 数据格式:表示协议数据单元PPDU 28 | > 29 | > 常见协议:JPEG、MPEG、ASII等 30 | 31 | #### 5、会话层 32 | 33 | > 负责客户端与服务端建立通道、管理、维护和断开会话。 34 | > 35 | > 数据格式:会话协议数据单元SPDU 36 | > 37 | > 常见协议:NFS、SQL、NETBIOS、RPC等 38 | 39 | #### 4、传输层 40 | 41 | > 负责建立、管理、维护、断开端对端的连接,它为上一层提供可端对端的传输数据,包括流量控制、差错处理等机制。它屏蔽了下层的数据通信的细节,强调端点应用数据的传输。 42 | > 43 | > 数据格式:段Segment 44 | > 45 | > 常见协议:TCP、UDP、SPX 46 | 47 | #### 3、网络层 48 | 49 | > 负责IP地址选择(寻址)与路由选择。为源端传输的数据提供合适的路由和交换节点。此层就是我们经常说的IP协议层。 50 | > 51 | > 数据格式:包PackeT 52 | > 53 | > 常见协议:IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGRP (路由器) 54 | 55 | #### 2、数据链路层 56 | 57 | > 提供介质管理与链路管理。负责将网络层的帧数据拆解成比特流数据给物理层运输,负责将物理层的比特流数据封装成帧数据转给网络层。 58 | > 59 | > 数据格式:帧Frame 60 | > 61 | > 常见协议:PPP、FR、HDLC、VLAN、MAC (网桥,交换机) 62 | 63 | #### 1、物理层 64 | 65 | > 实例传输的物理介质,线缆、光纤等(比如生成中的交通工具:公交车、汽车、火车等)。以比特流的方式在物理介质上传输。 66 | > 67 | > 数据格式:比特Bit 68 | > 69 | > 常见协议:RJ45、CLOCK、IEEE802.3 (中继器,集线器,网关) 70 | 71 | 72 | 73 |

二、TCP/IP四层网络模型

74 | 75 | > OSI七层网络模型 过于繁杂,而应用更为广泛的网络模型是 TCP/IP四层的参考模型,它们都很相似。 76 | 77 | ![ TCP/IP四层参考模型](https://i.loli.net/2019/01/10/5c37573d0cc07.png) 78 | 79 | #### 4、应用层 80 | 81 | > TCP/IP四层参考模型的应用层包含OSI七层模型的上三层:应用层、表示层、会话层,所以它的功能也就包含这三层的功能。负责面向直接应用的服务,提供表示应用服务的数据转义、解释等。 82 | 83 | #### 3、传输层 84 | 85 | > 功能和OSI模型的传输层一致,负责建立、管理、维护、断开端对端的连接,它为上一层提供可端对端的传输数据,包括流量控制、差错处理等机制。它屏蔽了下层的数据通信的细节,强调端点应用数据的传输。 86 | 87 | #### 2、网络层(网际层) 88 | 89 | > 功能和OSI模型的传输层一致,负责IP地址选择(寻址)与路由选择。为源端传输的数据提供合适的路由和交换节点。此层就是我们经常说的IP协议层。 90 | 91 | #### 1、数据链路层(网络接口层) 92 | 93 | > TCP/IP的数据链路层则将OSI的数据链路层和物理层合二为一,化繁为简。提供了更加简介的数据链路服务:物理寻址,将比特流与数据帧转换、传输。 94 | > 95 | > 96 | 97 | 98 | 99 |

三、五层网络模型

100 | 101 | > 还有少数资料将网络协议划分为五层:应用层、传输层、网络层、数据链路层、物理层。 102 | 103 | ![五层因特网协议栈](https://i.loli.net/2019/01/10/5c375848ea8f7.png) -------------------------------------------------------------------------------- /resource/markdown/networking/RequestAndResponse.md: -------------------------------------------------------------------------------- 1 |

类文件结构

2 | 3 | -------------------------------------------------------------------------------- /resource/markdown/networking/SlidingWindowProtocol.md: -------------------------------------------------------------------------------- 1 | ![滑动窗口](https://i.loli.net/2019/01/03/5c2d78ac2d805.jpeg) 2 | 3 |

什么是滑动窗口协议

4 | 5 | **滑动窗口协议**(*Sliding Window Protocol*),属于[TCP协议](https://baike.baidu.com/item/TCP%E5%8D%8F%E8%AE%AE)的一种应用,用于网络数据传输时的 **流量控制**,以 **避免拥塞** 的发生。该协议允许发送方在停止并等待确认前发送多个数据分组。由于发送方不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输,提高网络吞吐量。 6 | 7 | 8 | 9 |

TCP流量控制与滑动窗口协议

10 | 11 | ![滑动窗口协议的机制](https://i.loli.net/2019/01/13/5c3b5c7c55887.png) 12 | 13 | TCP滑动窗口从发送端和接收端分为发送窗口和接收窗口,各自维护着一个滑动窗口,以此来达到流量控制。 14 | 15 | 16 | 17 | #### 关于滑动窗口协议中的概念 18 | 19 | **发送端:** 20 | 21 | * 已发送并收到确认:已经发送数据段,并且这些数据被接收端已确认接收; 22 | * 已发送,未收到确认:已经发送数据段,但发送端未接收到由接收端发送的已接收的确认消息; 23 | * 未发送,可以发送:在流量控制中,数据量未达发送的到阈值,是可以由发送端发送的,但发送端还未来得及发送; 24 | * 未发送,不允许发送:发送端发送的数据过多,但很多数据未被确认接收,为达到流量控制的效果,这些数据段不允许被发送。 25 | 26 | **接收端:** 27 | 28 | * 已发送确认并已交付主机:接收端对已接收的数据交付给内部使用,并且发送确认已收到的消息给发送端,表示客户端已收到相应的数据,避免数据丢失; 29 | * 允许接受:在接受窗口中,未达到阈值的情况下,接收端允许接受数据的窗口; 30 | * 不允许接受:为了达到流量控制,接收到不能无限制的接受数据,达到接受窗口的阈值时,接受端将不允许接受数据。 31 | 32 | -------------------------------------------------------------------------------- /resource/markdown/networking/TCPAndUDP.md: -------------------------------------------------------------------------------- 1 |

传输层协议

2 | 3 | > TCP 和 UDP 都是传输层协议。 4 | 5 | 6 | 7 |

TCP

8 | 9 | > TCP, Transmission Control Protocol,传输控制协议,是面向 **连接** 的传输协议,既传输数据前先进行客户端和服务端建立 **可靠** 连接,连接成功后才进行传输数据。TCP每次连接都需要通过 **[”三次握手“](https://github.com/about-cloud/JavaCore)** 才能建立. 10 | 11 | 12 | 13 | **TCP的头部** 14 | 15 | ![TCP包首部格式](https://i.loli.net/2019/01/13/5c3b36cf5fc20.png) 16 | 17 | * **32位端口号:** 18 | 19 | 源端口和目的端口各占16位,2的16次方等于65536,查看看端口的命令:netstat。 20 | 21 | * **32位序列号:** 22 | 23 | 也称为顺序号(Sequence Number),简写为seq, 24 | 25 | * **32位确认序号:** 26 | 27 | 也称为应答号(Acknowledgment Number),简写为ACK。在握手阶段,确认序号将发送方的序号加1作为回答。 28 | 29 | * **4位首部长度:** 30 | 31 | 这个字段占4位,它的单位时32位(4个字节)。本例值为7,TCP的头长度为28字节,等于正常的长度2 0字节加上可选项8个字节。,TCP的头长度最长可为60字节(二进制1111换算为十进制为15,15*4字节=60字节)。 32 | 33 | * **6位标志字段:** 34 | 35 | ACK 置1时表示确认号为合法,为0的时候表示数据段不包含确认信息,确认号被忽略。 36 | 37 | RST 置1时重建连接。如果接收到RST位时候,通常发生了某些错误。 38 | 39 | SYN 置1时用来发起一个连接。 40 | 41 | FIN 置1时表示发端完成发送任务。用来释放连接,表明发送方已经没有数据发送了。 42 | 43 | URG 紧急指针,告诉接收TCP模块紧要指针域指着紧要数据。注:一般不使用。 44 | 45 | PSH 置1时请求的数据段在接收方得到后就可直接送到应用程序,而不必等到缓冲区满时才传送。注:一般不使用。 46 | 47 | * **16位窗口大小:** 48 | 49 | 又称接受窗口字段(Receive Window Field),该字段用于流量控制。用于指示接收方愿意接受的字节数量。 50 | 51 | * **16位检验和:** 52 | 53 | 又称因特网校验和,检验和覆盖了整个的TCP报文段: TCP首部和TCP数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。 54 | 55 | * **16位紧急指针:** 56 | 57 | **注:**一般不使用。 58 | 59 | 又称紧急数据指针,只有当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。 60 | 61 | * **可选与变长选项**: 62 | 63 | 通常为空,可根据首部长度推算。用于发送方与接收方协商最大报文段长度(MSS),或在高速网络环境下作窗口调节因子时使用。首部字段还定义了一个时间戳选项。 64 | 65 | 最常见的可选字段是最长报文大小,又称为MSS (Maximum Segment Size)。每个连接方通常都在握手的第一步中指明这个选项。它指明本端所能接收的最大长度的报文段。1460是以太网默认的大小。 66 | 67 | 68 | 69 |

UDP

70 | 71 | > UDP,User Data Protocol,用户数据报协议,面向 **非连接** 的传输协议,既传输数据前客户端和服务端 **无需** 建立连接,数据也可被传输,但 *被传输* 不带表一定传输成功。传世时直接将数据包丢给对方。 72 | 73 | 74 | 75 | **UPD的头部** 76 | 77 | ![UDP包首部格式](https://i.loli.net/2019/01/13/5c3b39228488e.png) 78 | 79 | * 16位源端口号 记录源端口号,在需要对方回信时选用。不需要时可用全0。 80 | * 16位目的端口号 记录目标端口号。这在终点交付报文时必须要使用到。 81 | * 长度 UDP数据报的长度(包括数据和首部),其最小值为8B(即仅有首部没有数据的情况)。 82 | * 校验和 检测UDP数据报在传输中是否有错,有错就丢弃。该字段时可选的,当源主机不想计算校验和,则直接令该字段为全0。当传输层从IP层收到UDP数据报时,就根据首部中的目的端口,把UDP数据报通过相应的端口,上交给进程。如果接收方UDP发现收到的报文中目的端口号不正确(即不存在对应端口号的应用进程),就丢弃该报文,并由ICMP发送“端口不可达”差错报文交给发送方。 83 | 84 | 85 | 86 |

对比

87 | 88 | | 对比维度\传输协议 | TCP | UDP | 89 | | :---------------- | ---------------------- | -------------------- | 90 | | 是否需要连接 | 需要 | 不需要 | 91 | | 是否是可靠传输 | 是 | 否 | 92 | | 速度 | 慢 | 快 | 93 | | 使用环境 | 传输数据量少、可靠性高 | 传输数量多、可靠性低 | 94 | 95 | 96 | 97 | > 参考资料: 98 | > 99 | > [TCP、UDP报文结构与区别](https://blog.csdn.net/cbjcry/article/details/84650730) 100 | > 101 | > [UDP协议的特点及UDP头部结构](https://blog.csdn.net/ASJBFJSB/article/details/80357111) -------------------------------------------------------------------------------- /resource/markdown/networking/TCPConnectAndDisconnect.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 前面的[章节](https://github.com/about-cloud/JavaCore)已经讲过,TCP是面向连接的传输协议。 4 | 5 |

一、3次握手过程

6 | 7 | ![TCP三次握手](https://i.loli.net/2019/01/10/5c3768734d0d2.png) 8 | 9 | #### 第一次握手 10 | 11 | > 客户端发起请求,发送 **SYN** (seq=i)到服务器,表示请求连接,之后客户端处于 **SYN_SNET** 状态; 12 | 13 | #### 第二次握手 14 | 15 | > 服务端接收到客户端的TCP连接请求,最起码能表示双方具有连接能力,然后响应 **ACK**(ack=i+1) + **SYN**(seq=j)表示已确认客户端的请求,并表示可以连接,之后服务端处于 **SYN_RCVD** 状态; 16 | 17 | #### 第三次握手 18 | 19 | > 客户端收到服务端消息,并回复 **ACK** (ack=j+1, seq=i+1,)确认已收到服务端的消息。之后双端处于 **ESTABLISHED** 状态表示已成功建立 **连接**。 20 | 21 | *SYN 是Synchronize Sequence Numbers,同步序列号的缩写; 22 | 23 | ACK是Acknowledgement,确认、答复的缩写。 24 | 25 | SENT 表示发送; 26 | 27 | RCVD 是received缩写,表示已收到; 28 | 29 | ESTABLISHED 表示已建立。 30 | 31 | 32 | 33 |

二、4次挥手过程

34 | 35 | ![TCP四次挥手](https://i.loli.net/2019/01/10/5c376853c0109.png) 36 | 37 | #### 第一次挥手 38 | 39 | >从双方建立连接的ESTABLISHED状态,客户端想要断开连接,向服务端发送 **FIN** 标识报文段,表示客户端要 **安全地** 断开连接。客户端进入 **FIN_WAIT_1** 状态。 40 | 41 | #### 第二次挥手 42 | 43 | >服务端收到客户端的终止连接的请求,并回复确认的 **ACK** 报文段,此后服务端进入 **CLOSE_WAIT** 状态,并且内部正在处理数据。 44 | 45 | #### 第三次挥手 46 | 47 | >待服务端处理好任务,才能发送 **FIN** 结束标识的状态,并发送 **ACK** 报文,确认FIN。 48 | 49 | #### 第四次挥手 50 | 51 | >客户端收到断开连接FIN标识后,向服务端发送**ACK**确认应答,此时服务端进入TIME-WAIT状态。该状态会持续**2MSL**时间,若该时间段内没有服务端的重发请求的话,就进入CLOSED状态,撤销TCB。当B收到确认应答后,也便进入CLOSED状态,撤销TCB。 52 | 53 | 54 | 55 |

三、为什么?

56 | 57 | **为什么连接建立需要三次握手,而不是两次握手?** 58 | 59 | > 为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误。 60 | 61 | 62 | 63 | **为什么A要先进入TIME-WAIT状态,等待2MSL时间后才进入CLOSED状态?** 64 | 65 | > 为了保证服务端能收到客户端的确认应答。 66 | > 若客户端发完确认应答后直接进入CLOSED状态,那么如果该应答丢失,服务端等待超时后就会重新发送连接释放请求,但此时客户端已经关闭了,不会作出任何响应,因此服务端永远无法正常关闭。 -------------------------------------------------------------------------------- /resource/markdown/spring/08.md: -------------------------------------------------------------------------------- 1 | #### 运行Application,创建、刷新一个新的 ApplicationContext 2 | 3 | ```java 4 | /** 5 | * Static helper that can be used to run a {@link SpringApplication} from the 6 | * specified sources using default settings and user supplied arguments. 7 | * @param primarySources the primary sources to load 8 | * @param args the application arguments (usually passed from a Java main method) 9 | * @return the running {@link ApplicationContext} 10 | */ 11 | public static ConfigurableApplicationContext run(Class[] primarySources, 12 | String[] args) { 13 | return new SpringApplication(primarySources).run(args); 14 | } 15 | // ----------------------------------------------------------- 16 | /** 17 | * Run the Spring application, creating and refreshing a new 18 | * {@link ApplicationContext}. 19 | * @param args the application arguments (usually passed from a Java main method) 20 | * @return a running {@link ApplicationContext} 21 | */ 22 | public ConfigurableApplicationContext run(String... args) { 23 | // 秒表工具(启动监控作用)(可以对多个任务计时,显示每个命名任务的总运行时间和运行时间)) 24 | StopWatch stopWatch = new StopWatch(); 25 | stopWatch.start(); 26 | // ApplicationContext子接口 27 | ConfigurableApplicationContext context = null; 28 | // 异常记录 29 | Collection exceptionReporters = new ArrayList<>(); 30 | // 配置java.awt.headless模式 31 | configureHeadlessProperty(); 32 | // 获取SpringApplication运行监听器 33 | SpringApplicationRunListeners listeners = getRunListeners(args); 34 | listeners.starting(); 35 | try { 36 | ApplicationArguments applicationArguments = new DefaultApplicationArguments( 37 | args); 38 | ConfigurableEnvironment environment = prepareEnvironment(listeners, 39 | applicationArguments); 40 | configureIgnoreBeanInfo(environment); 41 | Banner printedBanner = printBanner(environment); 42 | context = createApplicationContext(); 43 | exceptionReporters = getSpringFactoriesInstances( 44 | SpringBootExceptionReporter.class, 45 | new Class[] { ConfigurableApplicationContext.class }, context); 46 | prepareContext(context, environment, listeners, applicationArguments, 47 | printedBanner); 48 | refreshContext(context); 49 | afterRefresh(context, applicationArguments); 50 | stopWatch.stop(); 51 | if (this.logStartupInfo) { 52 | new StartupInfoLogger(this.mainApplicationClass) 53 | .logStarted(getApplicationLog(), stopWatch); 54 | } 55 | listeners.started(context); 56 | callRunners(context, applicationArguments); 57 | } 58 | catch (Throwable ex) { 59 | handleRunFailure(context, ex, exceptionReporters, listeners); 60 | throw new IllegalStateException(ex); 61 | } 62 | 63 | try { 64 | listeners.running(context); 65 | } 66 | catch (Throwable ex) { 67 | handleRunFailure(context, ex, exceptionReporters, null); 68 | throw new IllegalStateException(ex); 69 | } 70 | return context; 71 | } 72 | ``` 73 | 74 | 75 | 76 | ##### 1、 -------------------------------------------------------------------------------- /resource/markdown/spring/ApplicationEvent.md: -------------------------------------------------------------------------------- 1 | 完整的事件操作有三部门组成: 事件 、事件监听器、事件发布器。 2 | 3 |

应用事件

4 | 5 | *Spring* 上下文应用事件为bean 与 bean之间传递消息。一个bean处理完了希望其余一个接着处理,这时我们就需要其余的一个bean监听当前bean所发送的事件。*Spring* 框架定义了 *ApplicationEvent* 抽象类作为应用事件的半成品,由所有应用程序事件扩展的类。抽象的,因为直接发布通用事件是没有意义的。它属于上下文。 6 | 7 | ```java 8 | package org.springframework.context; 9 | 10 | import java.util.EventObject; 11 | 12 | public abstract class ApplicationEvent extends EventObject { 13 | private static final long serialVersionUID = 7099057708183571937L; 14 | 15 | /** 事件发生时的系统时间。 */ 16 | private final long timestamp; 17 | /** 18 | * 创建一个新的ApplicationEvent。 19 | * @param source 事件最初产生的事件对象(决不为null) 20 | */ 21 | public ApplicationEvent(Object source) { 22 | super(source); 23 | this.timestamp = System.currentTimeMillis(); 24 | } 25 | 26 | 27 | /** 在事件发生时以毫秒为单位返回系统时间。 */ 28 | public final long getTimestamp() { 29 | return this.timestamp; 30 | } 31 | } 32 | ``` 33 | 34 | 上下文应用事件功能很少,构造方法注入 *应用事件*、记录应用事件的创建时间、获取应用事件的创建时间。它还扩展至 *java.util.EventObject* 工具类。让我们一探究竟: 35 | 36 | ```java 37 | package java.util; 38 | 39 | /** 40 | * 此类是所有事件类的根类。(除其本身,所有的事件类均由其派生) 41 | * 所有事件都是通过引用对象来构造的,“源”在逻辑上被认为是事件最初发生的对象。 42 | */ 43 | 44 | public class EventObject implements java.io.Serializable { 45 | 46 | private static final long serialVersionUID = 5516075349620653480L; 47 | 48 | /** 49 | * 事件最初产生的对象。 50 | */ 51 | protected transient Object source; 52 | 53 | /** 54 | * 构建一个原型事件。 55 | * 56 | * @param source 事件最初产生的对象。 57 | * @exception IllegalArgumentException 如果源对象为null 58 | */ 59 | public EventObject(Object source) { 60 | if (source == null) 61 | throw new IllegalArgumentException("null source"); 62 | this.source = source; 63 | } 64 | 65 | /** 获取事件最初产生的对象 */ 66 | public Object getSource() { 67 | return source; 68 | } 69 | 70 | /** 返回这个时间对象的字符串表现形式 */ 71 | public String toString() { 72 | return getClass().getName() + "[source=" + source + "]"; 73 | } 74 | } 75 | ``` 76 | 77 | 78 | 79 |

应用事件监听器

80 | 81 | > 事件的触发是由 *事件发布器* 来完成,事件的获取是由 *事件监听器* 来完成。 82 | 83 | 所有事件侦听器接口都必须扩展至此 *java.util.EventListener* 接口: 84 | 85 | ```java 86 | package java.util; 87 | 88 | /** 89 | * 此接口的作用是标记为应用事件监听器。 90 | * @since JDK1.1 91 | */ 92 | public interface EventListener { 93 | } 94 | 95 | ``` 96 | 97 | 98 | 99 | *Spring* 中的应用监听器 *ApplicationListener*,它是使用观察者模式来完成事件监听任务: 100 | 101 | ```java 102 | package org.springframework.context; 103 | 104 | import java.util.EventListener; 105 | 106 | /** 107 | * @see org.springframework.context.event.ApplicationEventMulticaster 108 | */ 109 | @FunctionalInterface 110 | public interface ApplicationListener extends EventListener { 111 | 112 | /** 113 | * 处理应用程序事件。 114 | * @param event 要响应的事件 115 | */ 116 | void onApplicationEvent(E event); 117 | } 118 | ``` 119 | 120 | -------------------------------------------------------------------------------- /resource/markdown/spring/AspectOrientedProgramming.md: -------------------------------------------------------------------------------- 1 |

一、ArrayList

2 | 3 | -------------------------------------------------------------------------------- /resource/markdown/spring/BeanDefinition.md: -------------------------------------------------------------------------------- 1 | #### bean元数据元素 2 | > 何为元数据?用于描述数据的数据。 3 | 4 | ```java 5 | package org.springframework.beans; 6 | 7 | import org.springframework.lang.Nullable; 8 | 9 | /** 10 | * 由携带源配置对象的bean元数据元素实现此接口。 11 | * 12 | * @author Juergen Hoeller 13 | * @since 2.0 14 | */ 15 | public interface BeanMetadataElement { 16 | 17 | /** 18 | * 返回此元数据元素的配置源对象 19 | * (may be {@code null}). 20 | */ 21 | @Nullable 22 | Object getSource(); 23 | 24 | } 25 | 26 | ``` 27 | 28 | 29 | 30 | #### 属性访问器 31 | >用于访问bean的属性。 32 | 33 | ```java 34 | package org.springframework.core; 35 | 36 | import org.springframework.lang.Nullable; 37 | 38 | /** 39 | * 接口定义用于向任意对象增加和访问元数据的通用契约。 40 | * 41 | * @author Rob Harrop 42 | * @since 2.0 43 | */ 44 | public interface AttributeAccessor { 45 | 46 | /** 47 | * 通过名称设置属性值。 48 | * 通常,用户应该使用完全限定的名称(可能使用类或包名称作为前缀)来防止与其他元数据属性重叠。 49 | * @param name 唯一属性键 50 | * @param value 要附加的属性值 51 | */ 52 | void setAttribute(String name, @Nullable Object value); 53 | 54 | /** 55 | * 获取由名称标识的属性的值 56 | * 如果属性值不存在,则返回null 57 | * @param name 唯一属性键 58 | * @return 如果有的话,就返回属性的当前值 59 | */ 60 | @Nullable 61 | Object getAttribute(String name); 62 | 63 | /** 64 | * 删除由名称标识的属性并返回其值。 65 | * 如果未找到名称下的属性,则返回NULL。 66 | * @param name 唯一属性键 67 | * @return 如果有的话,返回最后一个属性值 68 | */ 69 | @Nullable 70 | Object removeAttribute(String name); 71 | 72 | /** 73 | * 判断指定属性名称对应的数值是否存在 74 | * @param name 唯一的属性键 75 | */ 76 | boolean hasAttribute(String name); 77 | 78 | /** 79 | * 返回所有属性的名称。 80 | */ 81 | String[] attributeNames(); 82 | } 83 | ``` 84 | 85 | 86 | 87 |

BeanDefinition接口

88 | 89 | > *BeanDefinition* 用于描述一个Bean的实例对象,它具有属性值、构造方法参数值和由它提供的进一步信息 90 | > 91 | > 具体的实现。这只是一个最小的接口:主要目的是允许 *BeanFactoryPostProcessor* 这样的*PropertyPlaceholderConfigurer* 来自行修改属性值和其他bean元数据。 92 | 93 | ```java 94 | package org.springframework.beans.factory.config; 95 | 96 | import org.springframework.beans.BeanMetadataElement; 97 | import org.springframework.beans.MutablePropertyValues; 98 | import org.springframework.core.AttributeAccessor; 99 | import org.springframework.lang.Nullable; 100 | 101 | /** 102 | * @author Juergen Hoeller 103 | * @author Rob Harrop 104 | * @since 19.03.2004 105 | * @see ConfigurableListableBeanFactory#getBeanDefinition 106 | * @see org.springframework.beans.factory.support.RootBeanDefinition 107 | * @see org.springframework.beans.factory.support.ChildBeanDefinition 108 | */ 109 | public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { 110 | 111 | /** 112 | * 标准单例范围的范围标识符: "singleton". 113 | * 请注意,扩展的bean工厂可能支持更多的范围。 114 | * @see #setScope 115 | */ 116 | String SCOPE_SINGLETON = ConfigurableBeanFactory.SCOPE_SINGLETON; 117 | 118 | /** 119 | * 标准原型范围的范围标识符: "prototype". 120 | * 请注意,扩展的bean工厂可能支持更多的范围。 121 | * @see #setScope 122 | */ 123 | String SCOPE_PROTOTYPE = ConfigurableBeanFactory.SCOPE_PROTOTYPE; 124 | 125 | 126 | /** 127 | * bean对象的角色表示,0表示是bean定义是应用程序的主要部分。通常对应于用户定义的bean。 128 | */ 129 | int ROLE_APPLICATION = 0; 130 | 131 | /** 表示角色是 较大的配置(通常是外部配置) */ 132 | int ROLE_SUPPORT = 1; 133 | 134 | /** 内部使用的角色 */ 135 | int ROLE_INFRASTRUCTURE = 2; 136 | 137 | 138 | // 可更改的属性 139 | 140 | /** 如果当前bean描述器存在父描述器名称的话,就设置父描述器的名称 */ 141 | void setParentName(@Nullable String parentName); 142 | 143 | /** 如果当前bean描述器存在父描述器名称的话,就设获取父描述器的名称 */ 144 | @Nullable 145 | String getParentName(); 146 | 147 | /** 148 | * 指定此bean定义的bean类名。 149 | * 类名可以在bean工厂的后置处理过程中修改,通常用经过解析的类名变体替换原来的类名。 150 | * @see #setParentName 151 | * @see #setFactoryBeanName 152 | * @see #setFactoryMethodName 153 | */ 154 | void setBeanClassName(@Nullable String beanClassName); 155 | 156 | /** 157 | * 返回此bean描述器类名。 158 | * 注意,如果子类定义覆盖/继承了父类的类名,那么它不必是运行时使用的实际类名。 159 | * 而且,这可能只是调用工厂方法的类,或者在调用方法的工厂bean引用的情况下,它甚至可能是空的。 160 | * 因此,而不是认为它是运行时的最终bean类型,而只是将其用于在单个bean定义级别进行解析。 161 | * @see #getParentName() 162 | * @see #getFactoryBeanName() 163 | * @see #getFactoryMethodName() 164 | */ 165 | @Nullable 166 | String getBeanClassName(); 167 | 168 | /** 169 | * 指定一个新的作用域,用以覆盖当前bean的作用域 170 | * @see #SCOPE_SINGLETON 171 | * @see #SCOPE_PROTOTYPE 172 | */ 173 | void setScope(@Nullable String scope); 174 | 175 | /** 176 | * 获取当前bean的作用域名称 177 | * 如果不存在的话,则返回null 178 | */ 179 | @Nullable 180 | String getScope(); 181 | 182 | /** 183 | * 设置是否应该延迟初始化此bean。 184 | * 如果设置为false,bean将在启动时由执行单例初始化的bean工厂进行实例化。 185 | */ 186 | void setLazyInit(boolean lazyInit); 187 | 188 | /** 返回是否延迟初始化此bean */ 189 | boolean isLazyInit(); 190 | 191 | /** 192 | * 设置此bean依赖于初始化的bean的名称。 193 | * bean工厂将保证首先初始化这些bean。 194 | */ 195 | void setDependsOn(@Nullable String... dependsOn); 196 | 197 | /** 返回此bean所依赖的bean名称 */ 198 | @Nullable 199 | String[] getDependsOn(); 200 | 201 | /** 202 | * 设置此bean是否是自动连接到其他bean的候选bean。 203 | * 注意,此标志仅用于影响基于类型的自动连接。 204 | * 它不影响按名称显式引用,即使指定的bean没有标记为自动连接候选,也会解析该引用。 205 | * 因此,如果名称匹配,按名称自动连接将注入bean。 206 | */ 207 | void setAutowireCandidate(boolean autowireCandidate); 208 | 209 | /** 210 | * 返回此bean是否是自动连接到其他bean的候选bean。 211 | */ 212 | boolean isAutowireCandidate(); 213 | 214 | /** 215 | * 设置此bean是否为主要自动连接候选bean。 216 | * 如果这个值是true,那么对于多个匹配候选bean中会选择其中一个bean,它将起到关键作用。 217 | */ 218 | void setPrimary(boolean primary); 219 | 220 | /** 返回此bean是否为主要自动连接候选对象 */ 221 | boolean isPrimary(); 222 | 223 | /** 224 | * 也可以通过此方式来设置指定的bean工厂。 225 | * 只需要传入被指的的bean工厂字符串名称即可 226 | * @see #setFactoryMethodName 227 | */ 228 | void setFactoryBeanName(@Nullable String factoryBeanName); 229 | 230 | /** 231 | * 如果该bean有bean工厂的话,可以通过此方法获取 232 | */ 233 | @Nullable 234 | String getFactoryBeanName(); 235 | 236 | /** 237 | * (如果有工厂的方法)可以指定工厂方法。 238 | * 将使用构造方法参数调用此方法,如果没有指定参数,则不使用参数。 239 | * (如果有的话)方法将在指定的工厂bean上调用,或者作为本地bean类上的静态方法调用。 240 | * @see #setFactoryBeanName 241 | * @see #setBeanClassName 242 | */ 243 | void setFactoryMethodName(@Nullable String factoryMethodName); 244 | 245 | /** 如果此bean有工厂方法的化话,此方法可以返回这个工厂方法 */ 246 | @Nullable 247 | String getFactoryMethodName(); 248 | 249 | /** 250 | * 返回此bean的构造方法参数值 251 | * 返回的实例可以在bean工厂后处理期间修改。 252 | * @return the ConstructorArgumentValues 对象(非null) 253 | */ 254 | ConstructorArgumentValues getConstructorArgumentValues(); 255 | 256 | /** 257 | * 如果此bean定义了构造函数参数值,则返回true。 258 | * @since 5.0.2 259 | */ 260 | default boolean hasConstructorArgumentValues() { 261 | return !getConstructorArgumentValues().isEmpty(); 262 | } 263 | 264 | /** 265 | * 返回要应用于bean的新实例的属性值。 266 | * 返回的实例可以在bean工厂后置处理期间修改。 267 | * @return the MutablePropertyValues 对象(非null) 268 | */ 269 | MutablePropertyValues getPropertyValues(); 270 | 271 | /** 272 | * 如果为该bean定义了属性值,则返回true。 273 | * @since 5.0.2 274 | */ 275 | default boolean hasPropertyValues() { 276 | return !getPropertyValues().isEmpty(); 277 | } 278 | 279 | 280 | // 只读的属性 281 | 282 | /** 283 | * 判断此bean的作用域是否是单例作用域 284 | * @see #SCOPE_SINGLETON 285 | */ 286 | boolean isSingleton(); 287 | 288 | /** 289 | * 判断此bean的作用域是否是原型作用域 290 | * @since 3.0 291 | * @see #SCOPE_PROTOTYPE 292 | */ 293 | boolean isPrototype(); 294 | 295 | /** 返回此bean是否“抽象”,即不打算实例化。 */ 296 | boolean isAbstract(); 297 | 298 | /** 299 | * 获取这个bean的角色 300 | * @see #ROLE_APPLICATION 301 | * @see #ROLE_SUPPORT 302 | * @see #ROLE_INFRASTRUCTURE 303 | */ 304 | int getRole(); 305 | 306 | /** 307 | * 返回此bean定义的可读描述。 308 | */ 309 | @Nullable 310 | String getDescription(); 311 | 312 | /** 313 | * 获取此bean定义的资源描述(以便在出现错误时显示在上下文)。 314 | */ 315 | @Nullable 316 | String getResourceDescription(); 317 | 318 | /** 319 | * 返回原始的BeanDefinition,如果没有,则返回null 320 | * 允许检索修饰后的bean定义 321 | */ 322 | @Nullable 323 | BeanDefinition getOriginatingBeanDefinition(); 324 | 325 | } 326 | ``` 327 | 328 | -------------------------------------------------------------------------------- /resource/markdown/spring/BeanFactory.md: -------------------------------------------------------------------------------- 1 | > :dizzy:本文基于 `Spring5.0.10.RELEASE` 版本 2 | 3 |

BeanFactory

4 | 5 | ![bean](https://images.unsplash.com/photo-1538391382455-5a95e0b549de?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=cc89611e38144bf02dcdb52f637d97cf&auto=format&fit=crop&w=500&q=60) 6 | 7 | 8 | 9 | > *Java* 名称源于 Java咖啡:coffee:,*bean* 意为 “豆” ,JavaBean 意为 “Java咖啡豆”。在Java编程世界了,万物皆对象,*bean* 就是对象的称呼,JavaBean就是java对象。 10 | > 11 | > *Spring* 框架最为核心的莫过于 *IoC容器*,这个容器在 *Spring* 框架中是通过 *Factory工厂* 来生成 *bean对象* 。IoC容器最顶层的对象工厂就是 **BeanFactory** 接口,它定义了生产对象的工厂对基本的行为功能: 12 | 13 | ```java 14 | package org.springframework.beans.factory; 15 | 16 | import org.springframework.beans.BeansException; 17 | import org.springframework.core.ResolvableType; 18 | import org.springframework.lang.Nullable; 19 | 20 | public interface BeanFactory { 21 | /** 标识符。如果以&开头表示获取FactoryBean,否则是FactoryBean创建的实例 */ 22 | String FACTORY_BEAN_PREFIX = "&"; 23 | 24 | // --------------------- 获取bean对象实例 --------------------- 25 | /** 根据bean名称获取bean对象实例 */ 26 | Object getBean(String name) throws BeansException; 27 | /** 根据bean名称和必须的bean类型获取bean对象实例 */ 28 | T getBean(String name, @Nullable Class requiredType) throws BeansException; 29 | /** 根据bean名称和参数来获取bean对象实例 */ 30 | Object getBean(String name, Object... args) throws BeansException; 31 | /** 根据类型获取对象实例 */ 32 | T getBean(Class requiredType) throws BeansException; 33 | /** 根据类型和参数获取对象实例 */ 34 | T getBean(Class requiredType, Object... args) throws BeansException; 35 | 36 | // ------------------ 判断是否存在bean对象实例 ----------------- 37 | /** 根据名称判断是否存在相同的bean对象 */ 38 | boolean containsBean(String name); 39 | 40 | // --------------- 判断是对象是单例模式还是原型模式 --------------- 41 | /** 判断创建对象的模式是否是单例模式 */ 42 | boolean isSingleton(String name) throws NoSuchBeanDefinitionException; 43 | /** 判断创建对象的模式是否是原型模式 */ 44 | boolean isPrototype(String name) throws NoSuchBeanDefinitionException; 45 | 46 | // ---------------------- 判断类型是否匹配 ---------------------- 47 | /** 48 | * 判断指定bean的名称和可解析类型是否匹配。 49 | * ResolvableType 类是为了解决泛型丢失问题而发明的一个类,它可以很好的解决泛化参数问题。 50 | */ 51 | boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException; 52 | /** 指定名称的bean的类型是否匹配 */ 53 | boolean isTypeMatch(String name, @Nullable Class typeToMatch) throws NoSuchBeanDefinitionException; 54 | 55 | // ---------------------------- 其他 ---------------------------- 56 | /** 根据bean名称获取其类型 */ 57 | @Nullable 58 | Class getType(String name) throws NoSuchBeanDefinitionException; 59 | /** 根据bean名称获取其别名 */ 60 | String[] getAliases(String name); 61 | } 62 | 63 | ``` 64 | 65 | 通过上面 *BeanFactory* 接口的源码可以看出,它定义了 **获取、判断** bean的基本行为,并未涉及创建bean的行为。 66 | 67 | 68 | 69 |

bean工厂体系结构

70 | 71 | BeanFactory接口和子接口、子实现类以及其他接口、类共同组成了一个对象工厂与应用上下文体系: 72 | 73 | ![BeanFactory]() 74 | 75 | 丰富的 *工厂与应用上下文体系* 不仅仅具有生产bean对象这么单一的工作,它还负责查看Bean的个数、获取Bean的配置、Bean的属性编辑、设置类装载器,而且提供了基于Xml、注解的等多种依赖关系映射处理,当前还有其它更多的功能。 76 | 77 | 78 | 79 |

HierarchicalBeanFactory接口

80 | 81 | *HierarchicalBeanFactory* 接口继承于 *BeanFactory* 接口,是bean工厂分层结构的一部分。可参见*org.springframework.beans.factory.config.ConfigurableBeanFactory#setParentBeanFactory* 设置父级bean工厂的方法。 82 | 83 | ```java 84 | package org.springframework.beans.factory; 85 | 86 | import org.springframework.lang.Nullable; 87 | 88 | public interface HierarchicalBeanFactory extends BeanFactory { 89 | /** 返回其父级bean工厂,如果不存在则返回null。 */ 90 | @Nullable 91 | BeanFactory getParentBeanFactory(); 92 | /** 93 | * 返回本地bean工厂是否包含给定名称的bean,忽略祖先上下文中定义的bean。 94 | * 这是一种替代bean的方法,忽略来自祖先bean工厂的给定名称的bean。 95 | * @param name 要查询的bean的名称 96 | * @return 具有指定名称的bean是否在本地工厂中定义 97 | * @see BeanFactory#containsBean 98 | */ 99 | boolean containsLocalBean(String name); 100 | } 101 | ``` 102 | 103 | 104 | 105 |

ListableBeanFactory接口

106 | 107 | > *ListableBeanFactory* 也是 *BeanFactory* 接口的子接口,它提供工厂容器内bean实例对象的可罗列功能,但不会列举父容器内的实例对象。 108 | 109 | ```java 110 | package org.springframework.beans.factory; 111 | 112 | import java.lang.annotation.Annotation; 113 | import java.util.Map; 114 | import org.springframework.beans.BeansException; 115 | import org.springframework.core.ResolvableType; 116 | import org.springframework.lang.Nullable; 117 | 118 | public interface ListableBeanFactory extends BeanFactory { 119 | /** 判断是否包含指定bean名称的bean */ 120 | boolean containsBeanDefinition(String beanName); 121 | /** 获取当前工厂中bean实例对象的数量 */ 122 | int getBeanDefinitionCount(); 123 | /** 获取当前工厂中bean的名称,以String数组的集合形式返回 */ 124 | String[] getBeanDefinitionNames(); 125 | /** 获取指定可解析类型的bean名称列表,以String数组的形式返回。 */ 126 | String[] getBeanNamesForType(ResolvableType type); 127 | /** 获取指定类型的bean名称列表,以String数组的形式返回。 */ 128 | String[] getBeanNamesForType(@Nullable Class type); 129 | /** 获取指定类型的、是否单例模式的、是否允许被初始化的bean名称列表,以String数组的形式返回。 */ 130 | String[] getBeanNamesForType(@Nullable Class type, boolean includeNonSingletons, boolean allowEagerInit); 131 | /** 获取指定类型的bean集合,以Map的形式返回 */ 132 | Map getBeansOfType(@Nullable Class type) throws BeansException; 133 | /** 获取指定类型的、是否单例模式下的、是否允许初始化的bean对象实例的集合,以Map的方式返回 */ 134 | Map getBeansOfType(@Nullable Class type, boolean includeNonSingletons, boolean allowEagerInit) 135 | throws BeansException; 136 | /** 根据指定的注解类型,获取bean名称的集合,以String数组的方式返回 */ 137 | String[] getBeanNamesForAnnotation(Class annotationType); 138 | /** 根据指定的注解类型,获取bean对象实例的集合,以Map的方式返回 */ 139 | Map getBeansWithAnnotation(Class annotationType) throws BeansException; 140 | /** 查找指定bean名称的、指定注解类型的注解 */ 141 | @Nullable 142 | A findAnnotationOnBean(String beanName, Class annotationType) 143 | throws NoSuchBeanDefinitionException; 144 | 145 | } 146 | ``` 147 | 148 | *ListableBeanFactory* 接口的意义在于罗列关于bean的集合。 -------------------------------------------------------------------------------- /resource/markdown/spring/BeanLifeCycle.md: -------------------------------------------------------------------------------- 1 |

SpringBean的生命周期

2 | 3 | #### 一、传统 Bean 的生命周期 4 | 1. new实例化; 5 | 2. 可使用了 6 | 3. 无引用时,GC回收♻️。 7 | 8 | #### 二、Servlet 的生命周期 9 | 1. 实例化Servlet对象; 10 | 2. init初始化对象; 11 | 3. 相应客户端请求service()(doGet()与doPost()); 12 | 4. destroy()终止/销毁。 13 | 14 | #### 三、Spring Bean的生命周期 15 | 1. 实例化对象; 16 | 2. 填充属性值及引用; 17 | 3. 调用 `BeanNameAware` 的 `setBeanName(String name)` 设置 bean 的 id; 18 | 4. 调用 `BeanFactoryAware` 的 `setBeanFactory(BeanFactory beanFactory)` 设置 `BeanFactory` Bean工厂; 19 | 5. 同上:`ApplicationContextAware` `setApplicationContext(ApplicationContext applicationContext)`; 20 | 6. 如果实现 `BeanPostProcessor`,则 调用 postProcessBeforeInitialization() 初始化前的后置处理方法 21 | 7. 如果实现了 `InitializingBean` 接口,则使用 `afterPropertiesSet()` 来初始化属性 22 | 8. 如果实现 `BeanPostProcessor`,则 调用 postProcessAfterInitialization() 初始化后的后置处理方法 23 | 9. 此时,bean 就可以使用了 24 | 10. `DisposableBean`接口 `destroy()` 销毁bean。不过在Spring5.0开始,DisposableBean.destroy() 已经是过时的方法了,可直接使用 close()。 -------------------------------------------------------------------------------- /resource/markdown/spring/BeanScope.md: -------------------------------------------------------------------------------- 1 |

SpringBean 的作用域

2 | 3 | 4 | 5 | | 作用域类型 | 模式 | 描述 | 6 | | :----------------- | :----------- | ------------------------------------------------------------ | 7 | | **singleton** | **单例模式** | bean对象以单例的模式存在IoC容器中,既在一个容器中,相同的对象只存在一个实例。 | 8 | | **prototype** | **原型模式** | 每次从容器中获取bean时,都会产生并返回新创建的对象实例。 | 9 | | request | 请求模式 | 每次HTTP请求时才会创建一个新的bean,该作用域仅适用于WebApplicationContext上下文的环境。 | 10 | | session | 会话模式 | 同一个HTTP session 共享一个bean,不同的HTTP session的bean对象实例不同。 | 11 | | globalSession | 全局会话模式 | 同一个全局 session 共享一个bean,一般用于 Portlet 应用环境。该作用域仅适用于 WebApplicationContext上下文的环境。 | 12 | | 自定义bean的作用域 | | | -------------------------------------------------------------------------------- /resource/markdown/spring/CyclicDependence.md: -------------------------------------------------------------------------------- 1 |

依赖注入的循环依赖问题

2 | 3 | ![循环依赖](https://img-blog.csdn.net/20170912082357749?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMDg1MzI2MQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 4 | 5 | #### 什么是循环依赖? 6 | 7 | > 在对象的引用中,两个及两个以上的类互相引用,只要形成环形就会产生 **循环依赖**。 8 | 9 | 10 | 11 |

依赖注入产生循环依赖的三种方式

12 | 13 | #### 1、构造方法注入产生的循环依赖 14 | 15 | 16 | 17 | #### 2、setter 方法单例模式注入产生的循环依赖 18 | 19 | 20 | 21 | #### 3、setter 方法原型模式注入产生的循环依赖 -------------------------------------------------------------------------------- /resource/markdown/spring/DefaultResourceLoader.md: -------------------------------------------------------------------------------- 1 | *AbstractApplicationContext* 抽象类依赖/扩展了很多其他的实现类,所以在分析 *AbstractApplicationContext* 源码之前是有必要分析其依赖/扩展类的源码。但有些依赖/扩展类在 [前面的文章](https://github.com/about-cloud/JavaCore) 中已经分析过,这里就不在重复。 2 | 3 | 4 | 5 |

默认资源加载器

6 | 7 | > *DefaultResourceLoader* ,默认资源加载器,是 *ResourceLoader* 接口的默认实现类,用于加载资源配置。*AbstractApplicationContext* 直接继承此类。 8 | 9 | ```java 10 | package org.springframework.core.io; 11 | 12 | import java.net.MalformedURLException; 13 | import java.net.URL; 14 | import java.util.Collection; 15 | import java.util.LinkedHashSet; 16 | import java.util.Map; 17 | import java.util.Set; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | import org.springframework.lang.Nullable; 20 | import org.springframework.util.Assert; 21 | import org.springframework.util.ClassUtils; 22 | import org.springframework.util.ResourceUtils; 23 | import org.springframework.util.StringUtils; 24 | 25 | /** 26 | * 通过ResourceEditor来使用,作为一个基础类来服务于 27 | * org.springframework.context.support.AbstractApplicationContext 28 | * 29 | * 如果location 的值是一个 URL,将返回一个UrlResource资源; 30 | * 如果location 的值非URL路径或是一个"classpath:"相对URL,则返回一个ClassPathResource. 31 | * 32 | * @author Juergen Hoeller 33 | * @since 10.03.2004 34 | * @see FileSystemResourceLoader 35 | * @see org.springframework.context.support.ClassPathXmlApplicationContext 36 | */ 37 | public class DefaultResourceLoader implements ResourceLoader { 38 | 39 | /** 40 | * 可为空的类加载器。 41 | * 默认情况下,类加载器将在这个ResourceLoader初始化时使用线程上下文类加载器进行访问。 42 | */ 43 | @Nullable 44 | private ClassLoader classLoader; 45 | /** 路径协议解析的Set集合 */ 46 | private final Set protocolResolvers = new LinkedHashSet<>(4); 47 | 48 | /** 用于资源缓存的Map的映射集 */ 49 | private final Map, Map> resourceCaches = new ConcurrentHashMap<>(4); 50 | 51 | 52 | /** 53 | * 无参构造方法创建一个新的DefaultResourceLoader对象,并设置类加载器. 54 | * @see java.lang.Thread#getContextClassLoader() 55 | */ 56 | public DefaultResourceLoader() { 57 | /** 58 | * 底层实现是按以下递进获取,有则返回: 59 | * 当前线程的类加载、ClassUtils.class 的加载器、系统类加载 60 | */ 61 | this.classLoader = ClassUtils.getDefaultClassLoader(); 62 | } 63 | 64 | /** 65 | * 有参构造方法,主动注入一个类加载 66 | * @param classLoader 用来加载类路径资源的类加载器,因为这个类加载器可以null,所以可传null值 67 | */ 68 | public DefaultResourceLoader(@Nullable ClassLoader classLoader) { 69 | this.classLoader = classLoader; 70 | } 71 | 72 | 73 | /** 74 | * setter 方法注入一个(可为null)的类加载器 75 | */ 76 | public void setClassLoader(@Nullable ClassLoader classLoader) { 77 | this.classLoader = classLoader; 78 | } 79 | 80 | /** 81 | * 返回用于加载类资源的类加载器。 82 | * 如果未null,则通过ClassUtils.getDefaultClassLoader()重新获取。 83 | * @see ClassPathResource 84 | */ 85 | @Override 86 | @Nullable 87 | public ClassLoader getClassLoader() { 88 | return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader()); 89 | } 90 | 91 | /** 92 | * 用这个资源加载器注册给定的解析器(前面的文章也讲说,解析器用于解析多个资源路径)。 93 | * 任何这样的解析器都会在这个加载程序的标准解析规则之前被调用。 94 | * 因此,它还可能覆盖任何默认规则。 95 | * @since 4.3 96 | * @see #getProtocolResolvers() 97 | */ 98 | public void addProtocolResolver(ProtocolResolver resolver) { 99 | Assert.notNull(resolver, "ProtocolResolver must not be null"); 100 | this.protocolResolvers.add(resolver); 101 | } 102 | 103 | /** 104 | * 返回当前注册的协议解析器集合,允许进行自行修改。 105 | * @since 4.3 106 | */ 107 | public Collection getProtocolResolvers() { 108 | return this.protocolResolvers; 109 | } 110 | 111 | /** 112 | * 获取由 Resource 键入的给定值类型的缓存。 113 | * @param valueType 值类型, 比如一个 ASM MetadataReader 114 | * @return Map缓存, 在ResourceLoader级别进行分享 115 | * @since 5.0 116 | */ 117 | @SuppressWarnings("unchecked") 118 | public Map getResourceCache(Class valueType) { 119 | return (Map) this.resourceCaches.computeIfAbsent(valueType, key -> new ConcurrentHashMap<>()); 120 | } 121 | 122 | /** 123 | * 清空此资源加载器中所有的资源缓存 124 | * @since 5.0 125 | * @see #getResourceCache 126 | */ 127 | public void clearResourceCaches() { 128 | this.resourceCaches.clear(); 129 | } 130 | 131 | /** 132 | * 从指定的路径获取资源,不能为null 133 | * 134 | * 获取资源的优先策略从高到底依次是: 135 | * 1.从协议解析器集合遍历每个协议器尝试解析指定位置的资源; 136 | * 2.从"/"开头的绝对路径尝试解析资源; 137 | * 3.从"classpath:"的类相对路径解析资源; 138 | * 4.最后从网络URL处解析资源。 139 | */ 140 | @Override 141 | public Resource getResource(String location) { 142 | Assert.notNull(location, "Location must not be null"); 143 | // 遍历协议解析器的集合 144 | for (ProtocolResolver protocolResolver : this.protocolResolvers) { 145 | // 然后用当前的协议解析来解析指定路径的资源 146 | Resource resource = protocolResolver.resolve(location, this); 147 | if (resource != null) { 148 | // 如果在当前协议解析器下解析的资源不为空,则返回该资源 149 | return resource; 150 | } 151 | } 152 | // 程序运行到这里,要么说明协议解析器集合为空, 153 | // 要么说明协议解析器集合中的所有协议解析器解析指定位置的资源都为null 154 | // 那么就对资源路径判断 155 | if (location.startsWith("/")) { 156 | // 此方法是本类中的方法,下面分析 157 | return getResourceByPath(location); 158 | } 159 | // 如果资源路径不以 "/" 为开始的绝对路径标识, 160 | // 则判断是否以默认方式 "classpath:" 开头 161 | else if (location.startsWith(CLASSPATH_URL_PREFIX)) { 162 | // 如果以默认的"classpath:"开头,则说明资源是类路径的相对资源 163 | return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); 164 | } 165 | // 在不然,就尝试从远处网络URL资源加载 166 | else { 167 | try { 168 | // 尝试将路径位置解析为URL… 169 | URL url = new URL(location); 170 | return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); 171 | } 172 | catch (MalformedURLException ex) { 173 | // 没有URL可以解析为资源路径 174 | return getResourceByPath(location); 175 | } 176 | } 177 | } 178 | 179 | /** 180 | * 返回给定路径上的资源地址引用。 181 | * 默认实现支持类路径位置。 这应该适用于独立实现,但可以被重写, 182 | * 比如:用于针对Servlet容器的实现。 183 | * @param path 资源的路径 184 | * @return 对应的资源引用 185 | * @see ClassPathResource 186 | * @see org.springframework.context.support.FileSystemXmlApplicationContext#getResourceByPath 187 | * @see org.springframework.web.context.support.XmlWebApplicationContext#getResourceByPath 188 | */ 189 | protected Resource getResourceByPath(String path) { 190 | return new ClassPathContextResource(path, getClassLoader()); 191 | } 192 | 193 | 194 | /** 195 | * 类路径库,通过实现上下文源接口显式地表示上下文相关路径。 196 | */ 197 | protected static class ClassPathContextResource extends ClassPathResource implements ContextResource { 198 | 199 | public ClassPathContextResource(String path, @Nullable ClassLoader classLoader) { 200 | super(path, classLoader); 201 | } 202 | 203 | @Override 204 | public String getPathWithinContext() { 205 | return getPath(); 206 | } 207 | 208 | @Override 209 | public Resource createRelative(String relativePath) { 210 | String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath); 211 | return new ClassPathContextResource(pathToUse, getClassLoader()); 212 | } 213 | } 214 | } 215 | ``` 216 | 217 | 218 | -------------------------------------------------------------------------------- /resource/markdown/spring/InversionOfControl.md: -------------------------------------------------------------------------------- 1 |

控制反转IoC

2 | 3 | > IoC,全称 *Inversion of Control* ,中文称 控制反转。 4 | 5 | ![Control](https://images.pexels.com/photos/935019/pexels-photo-935019.jpeg?auto=compress&cs=tinysrgb&h=350) 6 | 7 | 首先要知道,什么是 *控制(Control)* ?要控制谁?或者说谁要被控制?在面向对象编程中,对象A内部依赖对象B,那么就需要在对象A中实例化一个对象B,到底谁来实例化对象B,那么谁就是对象B的 **控制者**(controller)。我们通常的做法是在对象A中实例化一个对象B,那么 *控制权* 也就在对象A中,因为对象A也可以选择不去实例化,控制权在需求方。 8 | 9 | 10 | 11 | ![new](https://images.pexels.com/photos/12733/pexels-photo-12733.jpeg?auto=compress&cs=tinysrgb&h=350) 12 | 13 | 独木不成林。面向对象编程的特点就是以 **对象** 为基本单位。大家本着 *谁用谁创建* 的原则。比如:发布评论时,要先剔除敏感词,然后才能发布,发布功能依赖敏感词剔除功能: 14 | 15 | ```java 16 | package com.foovv.example; 17 | 18 | /** 19 | * 评论功能类 20 | * 21 | * @author 代码风水师 https://github.com/about-cloud/JavaCore 22 | * @version jdk1.8 23 | */ 24 | public class Comment { 25 | /** 26 | * 引入评论语句预处理工具类,并主动实例化对象 27 | */ 28 | private CommentPretreatmentUtils commentPretreatmentUtils = new CommentPretreatmentUtils(); 29 | 30 | /** 31 | * 发布评论 32 | * 33 | * @param comment 评语 34 | * @return 返回true表示评论成功,返回false表示评论失败 35 | */ 36 | public boolean uploadComment(String comment) { 37 | 38 | // 剔除敏感词 39 | this.commentPretreatmentUtils.removesensitiveWords(comment); 40 | ... 其他操作 ... 41 | } 42 | } 43 | 44 | ``` 45 | 46 | ```java 47 | package com.foovv.example; 48 | 49 | /** 50 | * 评论预处理工具类 51 | * 52 | * @author 代码风水师 https://github.com/about-cloud/JavaCore 53 | * @version jdk1.8 54 | */ 55 | public class CommentPretreatmentUtils { 56 | /** 57 | * 删除评语中的敏感词 58 | * 59 | * @param comment 评语 60 | * @return 删除明敏感词后的评论 61 | */ 62 | public String removesensitiveWords(String comment) { 63 | ... 其他操作 ... 64 | return ...; 65 | } 66 | 67 | } 68 | 69 | ``` 70 | 71 | *Comment* 类 **控制** (control) 着所依赖对象的实例化的工作,它们之间的耦合度大大增加,万一依赖的类发生了变化,对于被依赖类无疑是致命一击。对于 *Comment* 来说,它的工作就是发布或删除评论,至于要剔除敏感词或者增加某些语句与它无关,那么至于要注入什么对象实例,也不应该与它有关,它只需要使用被注入对象实例的方法罢了。 72 | 73 | ![集权](https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1542703412920&di=153ab0debf76c2d790da56b32a307df2&imgtype=0&src=http%3A%2F%2F07imgmini.eastday.com%2Fmobile%2F20181030%2F20181030075129_90828cc2c29cf13e1327e9b7165881fd_10.jpeg) 74 | 75 | 控制权的交接给统一的系统负责,该系统负责对象的实例化、传递引用、初始化值等。控制权就发生了变化,外国人称为 **Inversion of Control**,IoC,中文意思是 **控制反转**。 76 | 77 | 78 | 79 |

依赖注入DI

80 | 81 | > DI,全称 *Dependency Injection* ,中文称 依赖注入。 82 | 83 | 控制实例化对象的权利交给 *统一管理系统* 来负责了,如果我们需要其他被依赖的对象时怎么 ~~获取~~ 呢?错错错,压根不需要主动去获取,而是等待被注入所依赖的对象实例。 84 | 85 | 86 | 87 | #### 依赖注入的三种方式: 88 | 89 | > 接着上面评论功能的代码,被依赖的 *CommentPretreatmentUtils* 不变,修改 *Comment* 类。 90 | 91 | #### 1、构造方法注入 92 | 93 | ```java 94 | package com.foovv.example; 95 | 96 | /** 97 | * 评论功能类 98 | * 99 | * @author 代码风水师 https://github.com/about-cloud/JavaCore 100 | * @version jdk1.8 101 | */ 102 | public class Comment { 103 | /** 104 | * 引入评论语句预处理工具类,并主动实例化对象 105 | */ 106 | private CommentPretreatmentUtils commentPretreatmentUtils; 107 | 108 | /** 109 | * 构造方法注入依赖对象 110 | * 111 | * @param commentPretreatmentUtils 被依赖对象 112 | */ 113 | public Comment(CommentPretreatmentUtils commentPretreatmentUtils) { 114 | this.commentPretreatmentUtils = commentPretreatmentUtils; 115 | } 116 | 117 | /** 118 | * 发布评论 119 | * 120 | * @param comment 评语 121 | * @return 返回true表示评论成功,返回false表示评论失败 122 | */ 123 | public boolean uploadComment(String comment) { 124 | 125 | // 剔除敏感词 126 | this.commentPretreatmentUtils.removesensitiveWords(comment); 127 | ... 其他操作 ... 128 | } 129 | } 130 | ``` 131 | 132 | 使用时: 133 | 134 | ```java 135 | public class Client { 136 | public static void main(String[] args) { 137 | // 通过构造方法直接注入被依赖的对象实例 138 | Comment comment = new Comment(new CommentPretreatmentUtils()); 139 | comment.uploadComment("文章很不错,更多文章在:https://github.com/about-cloud/JavaCore"); 140 | 141 | } 142 | } 143 | ``` 144 | 145 | 146 | 147 | 这样还是有很强的依赖: 148 | 149 | ```java 150 | public class Comment { 151 | /** 152 | * Comment类还是依赖CommentPretreatmentUtils类, 153 | * 如果要切换其他类还是很麻烦 154 | */ 155 | private CommentPretreatmentUtils commentPretreatmentUtils; 156 | ... 其他操作 ... 157 | } 158 | ``` 159 | 160 | --- 161 | 162 | 根据 [迪米特法则](https://github.com/about-cloud/JavaCore) 现在要将被依赖的类以接口化编程实现,降低耦合度,只对外暴露方法服务,屏蔽实现细节,UML图如下: 163 | 164 | ![UML]() 165 | 166 | **评论预处理工具对外服务的接口** 167 | 168 | ```java 169 | package com.foovv.example; 170 | 171 | /** 172 | * 评论预处理工具对外服务类 173 | * 174 | * @author 代码风水师 https://github.com/about-cloud/JavaCore 175 | * @version jdk1.8 176 | */ 177 | public interface CommentPretreatmentService { 178 | /** 179 | * 删除评论中的敏感词 180 | * 181 | * @param comment 评语 182 | * @return 删除明敏感词后的评论 183 | */ 184 | String removesensitiveWords(String comment); 185 | } 186 | ``` 187 | 188 | **评论预处理工具的实现类** 189 | 190 | ```java 191 | package com.foovv.example; 192 | 193 | /** 194 | * @author 代码风水师 https://github.com/about-cloud/JavaCore 195 | * @version jdk1.8 196 | */ 197 | public class CommentPretreatmentUtils implements CommentPretreatmentService { 198 | 199 | /** 200 | * 删除评论中的敏感词 201 | * 202 | * @param comment 评语 203 | * @return 删除明敏感词后的评论 204 | */ 205 | @Override 206 | public String removesensitiveWords(String comment) { 207 | return comment; 208 | } 209 | 210 | } 211 | ``` 212 | 213 | **评论功能类:** 214 | 215 | ```java 216 | package com.foovv.example; 217 | 218 | /** 219 | * 评论功能类 220 | * 221 | * @author 代码风水师 https://github.com/about-cloud/JavaCore 222 | * @version jdk1.8 223 | */ 224 | public class Comment { 225 | /** 226 | * 评论预处理服务类 227 | */ 228 | private CommentPretreatmentService commentPretreatmentService; 229 | 230 | /** 231 | * 构造方法注入依赖对象 232 | * 233 | * @param commentPretreatmentService 被依赖对象 234 | */ 235 | public Comment(CommentPretreatmentService commentPretreatmentService) { 236 | this.commentPretreatmentService = commentPretreatmentService; 237 | } 238 | 239 | /** 240 | * 发布评论 241 | * 242 | * @param comment 评论 243 | * @return 返回true表示评论成功,返回false表示评论失败 244 | */ 245 | public boolean uploadComment(String comment) { 246 | 247 | // 剔除敏感词 248 | this.commentPretreatmentService.removesensitiveWords(comment); 249 | ... 其他操作 ... 250 | } 251 | } 252 | ``` 253 | 254 | 上面的 *Comment* 最多也就知道 *CommentPretreatmentService* 接口中的方法(这里不考虑静态变量和default语句),不会知道具体实现,而且切换具体实现类也很方法。 255 | 256 | 使用时: 257 | 258 | ```java 259 | public class Client { 260 | public static void main(String[] args) { 261 | // 通过构造方法直接注入被依赖的对象实例 262 | // 被注入的实例对象一定要实现于CommentPretreatmentService接口 263 | Comment comment = new Comment(new CommentPretreatmentUtils()); 264 | comment.uploadComment("文章很不错,更多文章在:https://github.com/about-cloud/JavaCore"); 265 | 266 | } 267 | } 268 | ``` 269 | 270 | 271 | 272 | #### 2、setter方法注入 273 | 274 | 接上面的构造方法注入,区别是将构造方法注入改成setter方法注入,客户端使用时通过调用setter方法注入即可: 275 | 276 | **评论功能类:** 277 | 278 | ```java 279 | package com.foovv.example; 280 | 281 | /** 282 | * 评论功能类 283 | * 284 | * @author 代码风水师 https://github.com/about-cloud/JavaCore 285 | * @version jdk1.8 286 | */ 287 | public class Comment { 288 | /** 289 | * 评论预处理服务类 290 | */ 291 | private CommentPretreatmentService commentPretreatmentService; 292 | 293 | /** 294 | * CommentPretreatmentService 的 setter 方法 295 | * 296 | * @param commentPretreatmentService set 注入实例对象 297 | */ 298 | public void setCommentPretreatmentService(CommentPretreatmentService commentPretreatmentService) { 299 | this.commentPretreatmentService = commentPretreatmentService; 300 | } 301 | 302 | /** 303 | * 发布评论 304 | * 305 | * @param comment 评论 306 | * @return 返回true表示评论成功,返回false表示评论失败 307 | */ 308 | public boolean uploadComment(String comment) { 309 | 310 | // 剔除敏感词 311 | this.commentPretreatmentService.removesensitiveWords(comment); 312 | ... 其他操作 ... 313 | } 314 | } 315 | ``` 316 | 317 | 使用时: 318 | 319 | ```java 320 | public class Client { 321 | public static void main(String[] args) { 322 | // 评论功能实例 323 | Comment comment = new Comment(); 324 | // setter 注入被依赖的实例对象 325 | comment.setCommentPretreatmentService(new CommentPretreatmentUtils()); 326 | comment.uploadComment("文章很不错,更多文章在:https://github.com/about-cloud/JavaCore"); 327 | 328 | } 329 | } 330 | ``` 331 | 332 | 333 | 334 | ##### 3、接口注入 335 | 336 | 337 | 338 |

手写简易版的IoC容器

339 | 340 | 首先在使用前通过文件或其他方式来映射 *依赖对象* 与 *被依赖对象* 之间的关系,也包括属性默认值的填充等等,最后*统一管理系统* 会去处理,然后 *统一管理系统* 生成大量的 **实例对象**,屏蔽其他细节,在外界看来它就像一个存放对象的容器,所以常称为 **IoC容器**。最后 *IoC容器* 这个统一管理系统会负责管理依赖关系。 341 | 342 | 343 | 344 | -------------------------------------------------------------------------------- /resource/markdown/spring/SpringBootStart-upProcedure.md: -------------------------------------------------------------------------------- 1 |

SpringBoot启动过程

2 | 3 | > 一个Application被注解为 `@SpringBootApplication`,通过 `main` 方法开始、SpringApplication.run(Object source, String... args)运行。 4 | > 5 | > 本文是基于 `SpringBoot 2.1.0.RELEASE` 进行解析 6 | 7 | 8 | 9 | ### 启动过程概要 10 | 11 | **一、初始化资源** 12 | 13 | 通过 `SpringApplication(ResourceLoader resourceLoader, Class... primarySources)` 构造方法初始化一些资源; 14 | 15 | 2、将基础参数资源添加到 `LinkedHashSet>` ; 16 | 17 | 3、分析Web应用的类型; 18 | 19 | 4、通过SpringFactoriesLoader#loadFactoryNames()加载工厂名称,然后使用createSpringFactoriesInstances()创建工厂的实例对象的集合; 20 | 21 | 5、设置监听器的集合; 22 | 23 | 6、通过main()方法判断并获得出所在的Application类; 24 | 25 | **二、运行Spring应用,创建并刷新一个新的 `ApplicationContext`** 26 | 27 | 初始化完了基本资源,调用 `org.springframework.boot.SpringApplication#run(String... args)` 然后运行该Spring 应用,创建并刷新一个新的 `ApplicationContext`; 28 | 29 | 1、创建并启动一个任务秒表StopWatch(此秒表注意用于在概念验证期间验证性能); 30 | 31 | 2、配置 `headless属性的模式` , 32 | 33 | 34 | 35 | ### Application初始化过程: 36 | 37 | 不同于 `1.5.6` 版本的使用 `initialize(sources)` 初始化方法进行初始化资源,`2.1.0` 使用构造方法进行初始化资源。 38 | 39 | ```java 40 | /** 41 | * 创建一个新的 SpringApplication 实例。此 application context 将从指定的基础资源 SpringApplication类级文件加载 beans 详情。实例可以在调用 run(String... args) 方法前定制化。 42 | * @param resourceLoader 使用的资源加载器 43 | * @param primarySources 基础 bean 资源 44 | * @see #run(Class, String[]) 45 | * @see #setSources(Set) 46 | */ 47 | @SuppressWarnings({ "unchecked", "rawtypes" }) 48 | public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) { 49 | this.resourceLoader = resourceLoader; 50 | // 如果为 primarySources == null,那么直接停止运行(run方法一定要填**Application.class) 51 | Assert.notNull(primarySources, "PrimarySources must not be null"); 52 | // 将 **Application.class 添加到 LinkedHashSet> (1.5.6是Object泛型) 53 | this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); 54 | // 推导web应用的类型 55 | this.webApplicationType = WebApplicationType.deduceFromClasspath(); 56 | // 设置初始化器 57 | setInitializers((Collection) getSpringFactoriesInstances( 58 | ApplicationContextInitializer.class)); 59 | // 设置监听器 60 | setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); 61 | // 推导主函数的类 62 | this.mainApplicationClass = deduceMainApplicationClass(); 63 | } 64 | ``` 65 | 66 | ##### 1、将 `Application` 添加到到 `Set` 容器 67 | 68 | 容器是 **LinkedHashSet>** 69 | 70 | ```java 71 | private Set> primarySources; 72 | this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); 73 | ``` 74 | 75 | ##### 2、根据类路径推导Web应用 76 | 77 | ```java 78 | static WebApplicationType deduceFromClasspath() { 79 | if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) 80 | && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) 81 | && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) { 82 | // Spring 5.x 新特性之一就是reactive响应式编程,此处推导出响应式reactive web server 83 | return WebApplicationType.REACTIVE; 84 | } 85 | for (String className : SERVLET_INDICATOR_CLASSES) { 86 | if (!ClassUtils.isPresent(className, null)) { 87 | /* 88 | * 如果存在Servlet或Spring ConfigurableWebApplicationContext 89 | * 推导出是一般的 web server 90 | */return WebApplicationType.NONE; 91 | } 92 | } 93 | // 否则就认为是 Servlet 服务 94 | return WebApplicationType.SERVLET; 95 | } 96 | ``` 97 | 98 | ##### 3、设置/添加初始化器(将初始化器添加到ArrayList) 99 | 100 | 设置/添加 `ApplicationContextInitializer.class` 为初始化器 101 | 102 | ```java 103 | setInitializers((Collection) getSpringFactoriesInstances( 104 | ApplicationContextInitializer.class)); 105 | // -------------------------------------------------- 106 | private Collection getSpringFactoriesInstances(Class type) { 107 | return getSpringFactoriesInstances(type, new Class[] {}); 108 | } 109 | // -------------------------------------------------- 110 | private Collection getSpringFactoriesInstances(Class type, 111 | Class[] parameterTypes, Object... args) { 112 | // 类加载器是当前线程的上下文类加载器 113 | ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 114 | // 使用名称并确保唯一性以防止重复 115 | Set names = new LinkedHashSet<>( 116 | SpringFactoriesLoader.loadFactoryNames(type, classLoader)); 117 | // 将创建后的实例放入 List 118 | List instances = createSpringFactoriesInstances(type, parameterTypes, 119 | classLoader, args, names); 120 | AnnotationAwareOrderComparator.sort(instances); 121 | return instances; 122 | } 123 | ``` 124 | 125 | ##### 4、设置/添加监听器 126 | 127 | 设置/添加 `ApplicationListener.class` 为监听器(同上) 128 | 129 | ```java 130 | setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); 131 | ``` 132 | 133 | ##### 5、推断main方法所在的Application应用类 134 | 135 | 虚拟机栈是方法的执行模型,从栈信息查找 `mian` 方法所在的类 136 | 137 | ```java 138 | private Class deduceMainApplicationClass() { 139 | try { 140 | StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); 141 | for (StackTraceElement stackTraceElement : stackTrace) { 142 | // 判断栈追踪元素的方法是否等于 main 方法 143 | if ("main".equals(stackTraceElement.getMethodName())) { 144 | // 则根据此实例的类名称获取类,并返回 145 | return Class.forName(stackTraceElement.getClassName()); 146 | } 147 | } 148 | } 149 | catch (ClassNotFoundException ex) { 150 | // 啥也不处理,并继续 151 | } 152 | return null; 153 | } 154 | ``` 155 | 156 | -------------------------------------------------------------------------------- /resource/markdown/spring/SpringThreadSafety.md: -------------------------------------------------------------------------------- 1 |

一、ArrayList

2 | 3 | -------------------------------------------------------------------------------- /resource/markdown/spring/TransactionPropagation.md: -------------------------------------------------------------------------------- 1 |

Spring事务传播机制

2 | 3 | **事务传播:** 事务方法与事务方法嵌套调用时,描述事务如何传播的行为。 4 | 5 | 6 | 7 | Spring 框架的 *TransactionDefinition* 接口提供了七种事务传播的行为类型(或等级) 8 | 9 | | 事务传播行为类型 | 描述 | 10 | | ------------------------- | ------------------------------------------------------------ | 11 | | **PROPAGATION_REQUIRED** | 如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,这个是常用的而且是默认的事务传播类型。 | 12 | | PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 | 13 | | PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 | 14 | | PROPAGATION_REQUIRES_NEW | 不管当前有没有事务,都会新建事务,如果当前存在事务,把当前事务挂起。 | 15 | | PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 | 16 | | PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 | 17 | | PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 | 18 | 19 | --------------------------------------------------------------------------------