From 3b25ca482efe629c824c38af0a6ee14beb5d58b5 Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Tue, 21 Apr 2020 09:20:58 +0800 Subject: [PATCH 01/38] update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef8d5cf..07bde7a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 是本人在备战春招及这几年学习的知识沉淀,这里面有很多都是自己的原创文章,同时,也有很多是本在备战春招的过程中觉得对面试特别有帮助的文章,**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 不一定可以帮助你进入到 BAT 等大厂,但是,如果你认真研究,仔细思考,我相信你也可以跟我一样幸运的进入到腾讯等大厂实实习。 +**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 是本人在备战春招及这几年学习的知识沉淀,这里面有很多都是自己的原创文章,同时,也有很多是本在备战春招的过程中觉得对面试特别有帮助的文章,**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 不一定可以帮助你进入到 BAT 等大厂,但是,如果你认真研究,仔细思考,我相信你也可以跟我一样幸运的进入到腾讯等大厂。 本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,春招目前拿到了腾讯等大厂offer。 From d8d41eaf717e431a23c0d55ed84466b19a7e95be Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Wed, 22 Apr 2020 09:24:03 +0800 Subject: [PATCH 02/38] update --- .../Backtracking-NQueens.md | 145 --- ...54\345\217\270\347\234\237\351\242\230.md" | 254 ----- ...62\347\256\227\346\263\225\351\242\230.md" | 467 --------- ...50\347\256\227\346\263\225\351\242\230.md" | 421 -------- ...06\347\274\226\347\250\213\351\242\230.md" | 686 ------------- ...60\346\215\256\347\273\223\346\236\204.md" | 192 ---- ...04\346\272\220\346\216\250\350\215\220.md" | 52 - .../2019alipay-pinduoduo-toutiao.md | 294 ------ ...\207\214,\347\273\210\350\216\267offer.md" | 96 -- ...\213\277\345\217\243\345\244\264offer).md" | 251 ----- .../JavaInterviewLibrary.md | 89 -- .../JavaProgrammerNeedKnow.md | 81 -- .../PreparingForInterview/interviewPrepare.md | 88 -- ...72\347\241\200\351\227\256\351\242\230.md" | 743 -------------- ...00\345\216\206\344\271\213\351\201\223.md" | 121 --- ...56\351\242\230\346\200\273\347\273\223.md" | 925 ----------------- ...30\350\246\201\351\227\256\346\210\221.md" | 64 -- .../alibaba-1.md | 222 ----- ...17\347\232\204\347\256\200\345\216\206.md" | 93 -- ...00\345\216\206\346\250\241\346\235\277.md" | 79 -- ...16\346\202\262\350\247\202\351\224\201.md" | 115 --- docs/java/BIO-NIO-AIO.md | 346 ------- .../Basis/Arrays,CollectionsCommonMethods.md | 383 -------- ...tatic\343\200\201this\343\200\201super.md" | 342 ------- ...3\344\271\210\347\256\200\345\215\225!.md" | 561 ----------- ...72\347\241\200\347\237\245\350\257\206.md" | 301 ------ "docs/java/Java IO\344\270\216NIO.md" | 200 ---- ...72\347\241\200\347\237\245\350\257\206.md" | 556 ----------- ...va\347\226\221\351\232\276\347\202\271.md" | 373 ------- ...17\350\256\276\350\256\241\351\242\230.md" | 125 --- ...26\347\250\213\350\247\204\350\214\203.md" | 30 - docs/java/Multithread/AQS.md | 713 -------------- docs/java/Multithread/Atomic.md | 556 ----------- ...urrencyAdvancedCommonInterviewQuestions.md | 929 ------------------ ...cyBasicsCommonInterviewQuestionsSummary.md | 312 ------ docs/java/Multithread/ThreadLocal.md | 170 ---- ...46\344\271\240\346\200\273\347\273\223.md" | 781 --------------- docs/java/Multithread/synchronized.md | 169 ---- ...71\345\231\250\346\200\273\347\273\223.md" | 229 ----- ...72\347\241\200\347\237\245\350\257\206.md" | 407 -------- docs/java/collection/ArrayList-Grow.md | 358 ------- docs/java/collection/ArrayList.md | 737 -------------- docs/java/collection/HashMap.md | 542 ---------- ...01\351\235\242\350\257\225\351\242\230.md" | 456 --------- docs/java/collection/LinkedList.md | 515 ---------- ...77\347\250\213\347\263\273\345\210\227.md" | 69 -- .../JWT-advantages-and-disadvantages.md | 93 -- .../basis-of-authority-certification.md | 154 --- ...07\345\260\261\345\244\237\344\272\206.md" | 321 ------ ...07\345\260\261\345\244\237\344\272\206.md" | 281 ------ .../data-communication/RocketMQ-Questions.md | 214 ---- .../data-communication/RocketMQ.md | 454 --------- .../system-design/data-communication/dubbo.md | 282 ------ .../data-communication/message-queue.md | 157 --- .../data-communication/rabbitmq.md | 324 ------ .../data-communication/summary.md | 100 -- .../data-communication/why-use-rpc.md | 76 -- .../system-design/framework/ZooKeeper-plus.md | 375 ------- docs/system-design/framework/ZooKeeper.md | 185 ---- ...70\350\247\201\345\221\275\344\273\244.md" | 200 ---- .../spring/Spring-Design-Patterns.md | 363 ------- docs/system-design/framework/spring/Spring.md | 82 -- .../framework/spring/SpringBean.md | 451 --------- .../spring/SpringInterviewQuestions.md | 358 ------- .../framework/spring/SpringMVC-Principle.md | 269 ----- .../API\347\275\221\345\205\263.md" | 190 ---- ...71\346\241\210\346\200\273\347\273\223.md" | 186 ---- ...00\346\234\257\346\236\266\346\236\204.md" | 47 - ...10\344\270\252\351\227\256\351\242\230.md" | 190 ---- .../\345\210\206\345\270\203\345\274\217.md" | 37 - ...33\345\234\260\346\226\271\357\274\237.md" | 70 -- ...76\350\256\241\346\250\241\345\274\217.md" | 84 -- 72 files changed, 21181 deletions(-) delete mode 100644 docs/dataStructures-algorithms/Backtracking-NQueens.md delete mode 100644 "docs/dataStructures-algorithms/\345\205\254\345\217\270\347\234\237\351\242\230.md" delete mode 100644 "docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\220\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" delete mode 100644 "docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" delete mode 100644 "docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" delete mode 100644 "docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" delete mode 100644 "docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" delete mode 100644 docs/essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md delete mode 100644 "docs/essential-content-for-interview/BATJrealInterviewExperience/5\351\235\242\351\230\277\351\207\214,\347\273\210\350\216\267offer.md" delete mode 100644 "docs/essential-content-for-interview/BATJrealInterviewExperience/\350\232\202\350\232\201\351\207\221\346\234\215\345\256\236\344\271\240\347\224\237\351\235\242\347\273\217\346\200\273\347\273\223(\345\267\262\346\213\277\345\217\243\345\244\264offer).md" delete mode 100644 docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md delete mode 100644 docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md delete mode 100644 docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md delete mode 100644 "docs/essential-content-for-interview/PreparingForInterview/\345\272\224\345\261\212\347\224\237\351\235\242\350\257\225\346\234\200\347\210\261\351\227\256\347\232\204\345\207\240\351\201\223Java\345\237\272\347\241\200\351\227\256\351\242\230.md" delete mode 100644 "docs/essential-content-for-interview/PreparingForInterview/\347\250\213\345\272\217\345\221\230\347\232\204\347\256\200\345\216\206\344\271\213\351\201\223.md" delete mode 100644 "docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" delete mode 100644 "docs/essential-content-for-interview/PreparingForInterview/\351\235\242\350\257\225\345\256\230-\344\275\240\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221.md" delete mode 100644 docs/essential-content-for-interview/real-interview-experience-analysis/alibaba-1.md delete mode 100644 "docs/essential-content-for-interview/\346\211\213\346\212\212\346\211\213\346\225\231\344\275\240\347\224\250Markdown\345\206\231\344\270\200\344\273\275\351\253\230\350\264\250\351\207\217\347\232\204\347\256\200\345\216\206.md" delete mode 100644 "docs/essential-content-for-interview/\347\256\200\345\216\206\346\250\241\346\235\277.md" delete mode 100644 "docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" delete mode 100644 docs/java/BIO-NIO-AIO.md delete mode 100644 docs/java/Basis/Arrays,CollectionsCommonMethods.md delete mode 100644 "docs/java/Basis/final\343\200\201static\343\200\201this\343\200\201super.md" delete mode 100644 "docs/java/Basis/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276,\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225!.md" delete mode 100644 "docs/java/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md" delete mode 100644 "docs/java/Java IO\344\270\216NIO.md" delete mode 100644 "docs/java/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" delete mode 100644 "docs/java/Java\347\226\221\351\232\276\347\202\271.md" delete mode 100644 "docs/java/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" delete mode 100644 "docs/java/Java\347\274\226\347\250\213\350\247\204\350\214\203.md" delete mode 100644 docs/java/Multithread/AQS.md delete mode 100644 docs/java/Multithread/Atomic.md delete mode 100644 docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md delete mode 100644 docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md delete mode 100644 docs/java/Multithread/ThreadLocal.md delete mode 100644 "docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" delete mode 100644 docs/java/Multithread/synchronized.md delete mode 100644 "docs/java/Multithread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" delete mode 100644 "docs/java/Multithread/\345\271\266\345\217\221\347\274\226\347\250\213\345\237\272\347\241\200\347\237\245\350\257\206.md" delete mode 100644 docs/java/collection/ArrayList-Grow.md delete mode 100644 docs/java/collection/ArrayList.md delete mode 100644 docs/java/collection/HashMap.md delete mode 100644 "docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" delete mode 100644 docs/java/collection/LinkedList.md delete mode 100644 "docs/java/\345\244\232\347\272\277\347\250\213\347\263\273\345\210\227.md" delete mode 100644 docs/system-design/authority-certification/JWT-advantages-and-disadvantages.md delete mode 100644 docs/system-design/authority-certification/basis-of-authority-certification.md delete mode 100644 "docs/system-design/data-communication/Kafka\345\205\245\351\227\250\347\234\213\350\277\231\344\270\200\347\257\207\345\260\261\345\244\237\344\272\206.md" delete mode 100644 "docs/system-design/data-communication/Kafka\347\263\273\347\273\237\350\256\276\350\256\241\345\274\200\347\257\207-\351\235\242\350\257\225\347\234\213\350\277\231\347\257\207\345\260\261\345\244\237\344\272\206.md" delete mode 100644 docs/system-design/data-communication/RocketMQ-Questions.md delete mode 100644 docs/system-design/data-communication/RocketMQ.md delete mode 100644 docs/system-design/data-communication/dubbo.md delete mode 100644 docs/system-design/data-communication/message-queue.md delete mode 100644 docs/system-design/data-communication/rabbitmq.md delete mode 100644 docs/system-design/data-communication/summary.md delete mode 100644 docs/system-design/data-communication/why-use-rpc.md delete mode 100644 docs/system-design/framework/ZooKeeper-plus.md delete mode 100644 docs/system-design/framework/ZooKeeper.md delete mode 100644 "docs/system-design/framework/ZooKeeper\346\225\260\346\215\256\346\250\241\345\236\213\345\222\214\345\270\270\350\247\201\345\221\275\344\273\244.md" delete mode 100644 docs/system-design/framework/spring/Spring-Design-Patterns.md delete mode 100644 docs/system-design/framework/spring/Spring.md delete mode 100644 docs/system-design/framework/spring/SpringBean.md delete mode 100644 docs/system-design/framework/spring/SpringInterviewQuestions.md delete mode 100644 docs/system-design/framework/spring/SpringMVC-Principle.md delete mode 100644 "docs/system-design/micro-service/API\347\275\221\345\205\263.md" delete mode 100644 "docs/system-design/micro-service/\345\210\206\345\270\203\345\274\217id\347\224\237\346\210\220\346\226\271\346\241\210\346\200\273\347\273\223.md" delete mode 100644 "docs/system-design/website-architecture/8 \345\274\240\345\233\276\350\257\273\346\207\202\345\244\247\345\236\213\347\275\221\347\253\231\346\212\200\346\234\257\346\236\266\346\236\204.md" delete mode 100644 "docs/system-design/website-architecture/\345\205\263\344\272\216\345\244\247\345\236\213\347\275\221\347\253\231\347\263\273\347\273\237\346\236\266\346\236\204\344\275\240\344\270\215\345\276\227\344\270\215\346\207\202\347\232\20410\344\270\252\351\227\256\351\242\230.md" delete mode 100644 "docs/system-design/website-architecture/\345\210\206\345\270\203\345\274\217.md" delete mode 100644 "docs/system-design/website-architecture/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\357\274\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271\357\274\237.md" delete mode 100644 "docs/system-design/\350\256\276\350\256\241\346\250\241\345\274\217.md" diff --git a/docs/dataStructures-algorithms/Backtracking-NQueens.md b/docs/dataStructures-algorithms/Backtracking-NQueens.md deleted file mode 100644 index bac262d..0000000 --- a/docs/dataStructures-algorithms/Backtracking-NQueens.md +++ /dev/null @@ -1,145 +0,0 @@ -# N皇后 -[51. N皇后](https://leetcode-cn.com/problems/n-queens/) -### 题目描述 -> n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 -> -![ANUzjA.png](https://s2.ax1x.com/2019/03/26/ANUzjA.png) -> -上图为 8 皇后问题的一种解法。 -> -给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。 -> -每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。 - -示例: - -``` -输入: 4 -输出: [ - [".Q..", // 解法 1 - "...Q", - "Q...", - "..Q."], - - ["..Q.", // 解法 2 - "Q...", - "...Q", - ".Q.."] -] -解释: 4 皇后问题存在两个不同的解法。 -``` - -### 问题分析 -约束条件为每个棋子所在的行、列、对角线都不能有另一个棋子。 - -使用一维数组表示一种解法,下标(index)表示行,值(value)表示该行的Q(皇后)在哪一列。 -每行只存储一个元素,然后递归到下一行,这样就不用判断行了,只需要判断列和对角线。 -### Solution1 -当result[row] = column时,即row行的棋子在column列。 - -对于[0, row-1]的任意一行(i 行),若 row 行的棋子和 i 行的棋子在同一列,则有result[i] == column; -若 row 行的棋子和 i 行的棋子在同一对角线,等腰直角三角形两直角边相等,即 row - i == Math.abs(result[i] - column) - -布尔类型变量 isValid 的作用是剪枝,减少不必要的递归。 -``` -public List> solveNQueens(int n) { - // 下标代表行,值代表列。如result[0] = 3 表示第1行的Q在第3列 - int[] result = new int[n]; - List> resultList = new LinkedList<>(); - dfs(resultList, result, 0, n); - return resultList; -} - -void dfs(List> resultList, int[] result, int row, int n) { - // 递归终止条件 - if (row == n) { - List list = new LinkedList<>(); - for (int x = 0; x < n; ++x) { - StringBuilder sb = new StringBuilder(); - for (int y = 0; y < n; ++y) - sb.append(result[x] == y ? "Q" : "."); - list.add(sb.toString()); - } - resultList.add(list); - return; - } - for (int column = 0; column < n; ++column) { - boolean isValid = true; - result[row] = column; - /* - * 逐行往下考察每一行。同列,result[i] == column - * 同对角线,row - i == Math.abs(result[i] - column) - */ - for (int i = row - 1; i >= 0; --i) { - if (result[i] == column || row - i == Math.abs(result[i] - column)) { - isValid = false; - break; - } - } - if (isValid) dfs(resultList, result, row + 1, n); - } -} -``` -### Solution2 -使用LinkedList表示一种解法,下标(index)表示行,值(value)表示该行的Q(皇后)在哪一列。 - -解法二和解法一的不同在于,相同列以及相同对角线的校验。 -将对角线抽象成【一次函数】这个简单的数学模型,根据一次函数的截距是常量这一特性进行校验。 - -这里,我将右上-左下对角线,简称为“\”对角线;左上-右下对角线简称为“/”对角线。 - -“/”对角线斜率为1,对应方程为y = x + b,其中b为截距。 -对于线上任意一点,均有y - x = b,即row - i = b; -定义一个布尔类型数组anti_diag,将b作为下标,当anti_diag[b] = true时,表示相应对角线上已经放置棋子。 -但row - i有可能为负数,负数不能作为数组下标,row - i 的最小值为-n(当row = 0,i = n时),可以加上n作为数组下标,即将row -i + n 作为数组下标。 -row - i + n 的最大值为 2n(当row = n,i = 0时),故anti_diag的容量设置为 2n 即可。 - -![ANXG79.png](https://s2.ax1x.com/2019/03/26/ANXG79.png) - -“\”对角线斜率为-1,对应方程为y = -x + b,其中b为截距。 -对于线上任意一点,均有y + x = b,即row + i = b; -同理,定义数组main_diag,将b作为下标,当main_diag[row + i] = true时,表示相应对角线上已经放置棋子。 - -有了两个校验对角线的数组,再来定义一个用于校验列的数组cols,这个太简单啦,不解释。 - -**解法二时间复杂度为O(n!),在校验相同列和相同对角线时,引入三个布尔类型数组进行判断。相比解法一,少了一层循环,用空间换时间。** - -``` -List> resultList = new LinkedList<>(); - -public List> solveNQueens(int n) { - boolean[] cols = new boolean[n]; - boolean[] main_diag = new boolean[2 * n]; - boolean[] anti_diag = new boolean[2 * n]; - LinkedList result = new LinkedList<>(); - dfs(result, 0, cols, main_diag, anti_diag, n); - return resultList; -} - -void dfs(LinkedList result, int row, boolean[] cols, boolean[] main_diag, boolean[] anti_diag, int n) { - if (row == n) { - List list = new LinkedList<>(); - for (int x = 0; x < n; ++x) { - StringBuilder sb = new StringBuilder(); - for (int y = 0; y < n; ++y) - sb.append(result.get(x) == y ? "Q" : "."); - list.add(sb.toString()); - } - resultList.add(list); - return; - } - for (int i = 0; i < n; ++i) { - if (cols[i] || main_diag[row + i] || anti_diag[row - i + n]) - continue; - result.add(i); - cols[i] = true; - main_diag[row + i] = true; - anti_diag[row - i + n] = true; - dfs(result, row + 1, cols, main_diag, anti_diag, n); - result.removeLast(); - cols[i] = false; - main_diag[row + i] = false; - anti_diag[row - i + n] = false; - } -} -``` \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\345\205\254\345\217\270\347\234\237\351\242\230.md" "b/docs/dataStructures-algorithms/\345\205\254\345\217\270\347\234\237\351\242\230.md" deleted file mode 100644 index c78ed8f..0000000 --- "a/docs/dataStructures-algorithms/\345\205\254\345\217\270\347\234\237\351\242\230.md" +++ /dev/null @@ -1,254 +0,0 @@ -# 网易 2018 - -下面三道编程题来自网易2018校招编程题,这三道应该来说是非常简单的编程题了,这些题目大家稍微有点编程和数学基础的话应该没什么问题。看答案之前一定要自己先想一下如果是自己做的话会怎么去做,然后再对照这我的答案看看,和你自己想的有什么区别?那一种方法更好? - -## 问题 - -### 一 获得特定数量硬币问题 - -小易准备去魔法王国采购魔法神器,购买魔法神器需要使用魔法币,但是小易现在一枚魔法币都没有,但是小易有两台魔法机器可以通过投入x(x可以为0)个魔法币产生更多的魔法币。 - -魔法机器1:如果投入x个魔法币,魔法机器会将其变为2x+1个魔法币 - -魔法机器2:如果投入x个魔法币,魔法机器会将其变为2x+2个魔法币 - -小易采购魔法神器总共需要n个魔法币,所以小易只能通过两台魔法机器产生恰好n个魔法币,小易需要你帮他设计一个投入方案使他最后恰好拥有n个魔法币。 - -**输入描述:** 输入包括一行,包括一个正整数n(1 ≤ n ≤ 10^9),表示小易需要的魔法币数量。 - -**输出描述:** 输出一个字符串,每个字符表示该次小易选取投入的魔法机器。其中只包含字符'1'和'2'。 - -**输入例子1:** 10 - -**输出例子1:** 122 - -### 二 求“相反数”问题 - -为了得到一个数的"相反数",我们将这个数的数字顺序颠倒,然后再加上原先的数得到"相反数"。例如,为了得到1325的"相反数",首先我们将该数的数字顺序颠倒,我们得到5231,之后再加上原先的数,我们得到5231+1325=6556.如果颠倒之后的数字有前缀零,前缀零将会被忽略。例如n = 100, 颠倒之后是1. - -**输入描述:** 输入包括一个整数n,(1 ≤ n ≤ 10^5) - -**输出描述:** 输出一个整数,表示n的相反数 - -**输入例子1:** 1325 - -**输出例子1:** 6556 - -### 三 字符串碎片的平均长度 - -一个由小写字母组成的字符串可以看成一些同一字母的最大碎片组成的。例如,"aaabbaaac"是由下面碎片组成的:'aaa','bb','c'。牛牛现在给定一个字符串,请你帮助计算这个字符串的所有碎片的平均长度是多少。 - -**输入描述:** 输入包括一个字符串s,字符串s的长度length(1 ≤ length ≤ 50),s只含小写字母('a'-'z') - -**输出描述:** 输出一个整数,表示所有碎片的平均长度,四舍五入保留两位小数。 - -**如样例所示:** s = "aaabbaaac" -所有碎片的平均长度 = (3 + 2 + 3 + 1) / 4 = 2.25 - -**输入例子1:** aaabbaaac - -**输出例子1:** 2.25 - -## 答案 - -### 一 获得特定数量硬币问题 - -#### 分析: - -作为该试卷的第一题,这道题应该只要思路正确就很简单了。 - -解题关键:明确魔法机器1只能产生奇数,魔法机器2只能产生偶数即可。我们从后往前一步一步推回去即可。 - -#### 示例代码 - -注意:由于用户的输入不确定性,一般是为了程序高可用性使需要将捕获用户输入异常然后友好提示用户输入类型错误并重新输入的。所以下面我给了两个版本,这两个版本都是正确的。这里只是给大家演示如何捕获输入类型异常,后面的题目中我给的代码没有异常处理的部分,参照下面两个示例代码,应该很容易添加。(PS:企业面试中没有明确就不用添加异常处理,当然你有的话也更好) - -**不带输入异常处理判断的版本:** - -```java -import java.util.Scanner; - -public class Main2 { - // 解题关键:明确魔法机器1只能产生奇数,魔法机器2只能产生偶数即可。我们从后往前一步一步推回去即可。 - - public static void main(String[] args) { - System.out.println("请输入要获得的硬币数量:"); - Scanner scanner = new Scanner(System.in); - int coincount = scanner.nextInt(); - StringBuilder sb = new StringBuilder(); - while (coincount >= 1) { - // 偶数的情况 - if (coincount % 2 == 0) { - coincount = (coincount - 2) / 2; - sb.append("2"); - // 奇数的情况 - } else { - coincount = (coincount - 1) / 2; - sb.append("1"); - } - } - // 输出反转后的字符串 - System.out.println(sb.reverse()); - - } -} -``` - -**带输入异常处理判断的版本(当输入的不是整数的时候会提示重新输入):** - -```java -import java.util.InputMismatchException; -import java.util.Scanner; - - -public class Main { - // 解题关键:明确魔法机器1只能产生奇数,魔法机器2只能产生偶数即可。我们从后往前一步一步推回去即可。 - - public static void main(String[] args) { - System.out.println("请输入要获得的硬币数量:"); - Scanner scanner = new Scanner(System.in); - boolean flag = true; - while (flag) { - try { - int coincount = scanner.nextInt(); - StringBuilder sb = new StringBuilder(); - while (coincount >= 1) { - // 偶数的情况 - if (coincount % 2 == 0) { - coincount = (coincount - 2) / 2; - sb.append("2"); - // 奇数的情况 - } else { - coincount = (coincount - 1) / 2; - sb.append("1"); - } - } - // 输出反转后的字符串 - System.out.println(sb.reverse()); - flag=false;//程序结束 - } catch (InputMismatchException e) { - System.out.println("输入数据类型不匹配,请您重新输入:"); - scanner.nextLine(); - continue; - } - } - - } -} - -``` - -### 二 求“相反数”问题 - -#### 分析: - -解决本道题有几种不同的方法,但是最快速的方法就是利用reverse()方法反转字符串然后再将字符串转换成int类型的整数,这个方法是快速解决本题关键。我们先来回顾一下下面两个知识点: - -**1)String转int;** - -在 Java 中要将 String 类型转化为 int 类型时,需要使用 Integer 类中的 parseInt() 方法或者 valueOf() 方法进行转换. - -```java - String str = "123"; - int a = Integer.parseInt(str); -``` - - 或 - -```java - String str = "123"; - int a = Integer.valueOf(str).intValue(); -``` - -**2)next()和nextLine()的区别** - -在Java中输入字符串有两种方法,就是next()和nextLine().两者的区别就是:nextLine()的输入是碰到回车就终止输入,而next()方法是碰到空格,回车,Tab键都会被视为终止符。所以next()不会得到带空格的字符串,而nextLine()可以得到带空格的字符串。 - -#### 示例代码: - -```java -import java.util.Scanner; - -/** - * 本题关键:①String转int;②next()和nextLine()的区别 - */ -public class Main { - - public static void main(String[] args) { - - System.out.println("请输入一个整数:"); - Scanner scanner = new Scanner(System.in); - String s=scanner.next(); - //将字符串转换成数字 - int number1=Integer.parseInt(s); - //将字符串倒序后转换成数字 - //因为Integer.parseInt()的参数类型必须是字符串所以必须加上toString() - int number2=Integer.parseInt(new StringBuilder(s).reverse().toString()); - System.out.println(number1+number2); - - } -} -``` - -### 三 字符串碎片的平均长度 - -#### 分析: - -这道题的意思也就是要求:(字符串的总长度)/(相同字母团构成的字符串的个数)。 - -这样就很简单了,就变成了字符串的字符之间的比较。如果需要比较字符串的字符的话,我们可以利用charAt(i)方法:取出特定位置的字符与后一个字符比较,或者利用toCharArray()方法将字符串转换成字符数组采用同样的方法做比较。 - -#### 示例代码 - -**利用charAt(i)方法:** - -```java -import java.util.Scanner; - -public class Main { - - public static void main(String[] args) { - - Scanner sc = new Scanner(System.in); - while (sc.hasNext()) { - String s = sc.next(); - //个数至少为一个 - float count = 1; - for (int i = 0; i < s.length() - 1; i++) { - if (s.charAt(i) != s.charAt(i + 1)) { - count++; - } - } - System.out.println(s.length() / count); - } - } - -} -``` - -**利用toCharArray()方法:** - -```java -import java.util.Scanner; - -public class Main2 { - - public static void main(String[] args) { - - Scanner sc = new Scanner(System.in); - while (sc.hasNext()) { - String s = sc.next(); - //个数至少为一个 - float count = 1; - char [] stringArr = s.toCharArray(); - for (int i = 0; i < stringArr.length - 1; i++) { - if (stringArr[i] != stringArr[i + 1]) { - count++; - } - } - System.out.println(s.length() / count); - } - } - -} -``` \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\220\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" "b/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\220\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" deleted file mode 100644 index 45ea008..0000000 --- "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\220\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" +++ /dev/null @@ -1,467 +0,0 @@ -[toc] - - -## 说明 - -- 本文作者:wwwxmu -- 原文地址:https://www.weiweiblog.cn/13string/ -- 作者的博客站点:https://www.weiweiblog.cn/ (推荐哦!) - -考虑到篇幅问题,我会分两次更新这个内容。本篇文章只是原文的一部分,我在原文的基础上增加了部分内容以及修改了部分代码和注释。另外,我增加了爱奇艺 2018 秋招 Java:`求给定合法括号序列的深度` 这道题。所有代码均编译成功,并带有注释,欢迎各位享用! - -## 1. KMP 算法 - -谈到字符串问题,不得不提的就是 KMP 算法,它是用来解决字符串查找的问题,可以在一个字符串(S)中查找一个子串(W)出现的位置。KMP 算法把字符匹配的时间复杂度缩小到 O(m+n) ,而空间复杂度也只有O(m)。因为“暴力搜索”的方法会反复回溯主串,导致效率低下,而KMP算法可以利用已经部分匹配这个有效信息,保持主串上的指针不回溯,通过修改子串的指针,让模式串尽量地移动到有效的位置。 - -具体算法细节请参考: - -- **字符串匹配的KMP算法:** http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html -- **从头到尾彻底理解KMP:** https://blog.csdn.net/v_july_v/article/details/7041827 -- **如何更好的理解和掌握 KMP 算法?:** https://www.zhihu.com/question/21923021 -- **KMP 算法详细解析:** https://blog.sengxian.com/algorithms/kmp -- **图解 KMP 算法:** http://blog.jobbole.com/76611/ -- **汪都能听懂的KMP字符串匹配算法【双语字幕】:** https://www.bilibili.com/video/av3246487/?from=search&seid=17173603269940723925 -- **KMP字符串匹配算法1:** https://www.bilibili.com/video/av11866460?from=search&seid=12730654434238709250 - -**除此之外,再来了解一下BM算法!** - -> BM算法也是一种精确字符串匹配算法,它采用从右向左比较的方法,同时应用到了两种启发式规则,即坏字符规则 和好后缀规则 ,来决定向右跳跃的距离。基本思路就是从右往左进行字符匹配,遇到不匹配的字符后从坏字符表和好后缀表找一个最大的右移值,将模式串右移继续匹配。 -《字符串匹配的KMP算法》:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html - - -## 2. 替换空格 - -> 剑指offer:请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。 - -这里我提供了两种方法:①常规方法;②利用 API 解决。 - -```java -//https://www.weiweiblog.cn/replacespace/ -public class Solution { - - /** - * 第一种方法:常规方法。利用String.charAt(i)以及String.valueOf(char).equals(" " - * )遍历字符串并判断元素是否为空格。是则替换为"%20",否则不替换 - */ - public static String replaceSpace(StringBuffer str) { - - int length = str.length(); - // System.out.println("length=" + length); - StringBuffer result = new StringBuffer(); - for (int i = 0; i < length; i++) { - char b = str.charAt(i); - if (String.valueOf(b).equals(" ")) { - result.append("%20"); - } else { - result.append(b); - } - } - return result.toString(); - - } - - /** - * 第二种方法:利用API替换掉所用空格,一行代码解决问题 - */ - public static String replaceSpace2(StringBuffer str) { - - return str.toString().replaceAll("\\s", "%20"); - } -} - -``` - -## 3. 最长公共前缀 - -> Leetcode: 编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""。 - -示例 1: - -``` -输入: ["flower","flow","flight"] -输出: "fl" -``` - -示例 2: - -``` -输入: ["dog","racecar","car"] -输出: "" -解释: 输入不存在公共前缀。 -``` - - -思路很简单!先利用Arrays.sort(strs)为数组排序,再将数组第一个元素和最后一个元素的字符从前往后对比即可! - -```java -public class Main { - public static String replaceSpace(String[] strs) { - - // 如果检查值不合法及就返回空串 - if (!checkStrs(strs)) { - return ""; - } - // 数组长度 - int len = strs.length; - // 用于保存结果 - StringBuilder res = new StringBuilder(); - // 给字符串数组的元素按照升序排序(包含数字的话,数字会排在前面) - Arrays.sort(strs); - int m = strs[0].length(); - int n = strs[len - 1].length(); - int num = Math.min(m, n); - for (int i = 0; i < num; i++) { - if (strs[0].charAt(i) == strs[len - 1].charAt(i)) { - res.append(strs[0].charAt(i)); - } else - break; - - } - return res.toString(); - - } - - private static boolean chechStrs(String[] strs) { - boolean flag = false; - if (strs != null) { - // 遍历strs检查元素值 - for (int i = 0; i < strs.length; i++) { - if (strs[i] != null && strs[i].length() != 0) { - flag = true; - } else { - flag = false; - break; - } - } - } - return flag; - } - - // 测试 - public static void main(String[] args) { - String[] strs = { "customer", "car", "cat" }; - // String[] strs = { "customer", "car", null };//空串 - // String[] strs = {};//空串 - // String[] strs = null;//空串 - System.out.println(Main.replaceSpace(strs));// c - } -} - -``` - -## 4. 回文串 - -### 4.1. 最长回文串 - -> LeetCode: 给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。在构造过程中,请注意区分大小写。比如`"Aa"`不能当做一个回文字符串。注 -意:假设字符串的长度不会超过 1010。 - - - -> 回文串:“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。——百度百科 地址:https://baike.baidu.com/item/%E5%9B%9E%E6%96%87%E4%B8%B2/1274921?fr=aladdin - -示例 1: - -``` -输入: -"abccccdd" - -输出: -7 - -解释: -我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。 -``` - -我们上面已经知道了什么是回文串?现在我们考虑一下可以构成回文串的两种情况: - -- 字符出现次数为双数的组合 -- 字符出现次数为双数的组合+一个只出现一次的字符 - -统计字符出现的次数即可,双数才能构成回文。因为允许中间一个数单独出现,比如“abcba”,所以如果最后有字母落单,总长度可以加 1。首先将字符串转变为字符数组。然后遍历该数组,判断对应字符是否在hashset中,如果不在就加进去,如果在就让count++,然后移除该字符!这样就能找到出现次数为双数的字符个数。 - -```java -//https://leetcode-cn.com/problems/longest-palindrome/description/ -class Solution { - public int longestPalindrome(String s) { - if (s.length() == 0) - return 0; - // 用于存放字符 - HashSet hashset = new HashSet(); - char[] chars = s.toCharArray(); - int count = 0; - for (int i = 0; i < chars.length; i++) { - if (!hashset.contains(chars[i])) {// 如果hashset没有该字符就保存进去 - hashset.add(chars[i]); - } else {// 如果有,就让count++(说明找到了一个成对的字符),然后把该字符移除 - hashset.remove(chars[i]); - count++; - } - } - return hashset.isEmpty() ? count * 2 : count * 2 + 1; - } -} -``` - - -### 4.2. 验证回文串 - -> LeetCode: 给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。 说明:本题中,我们将空字符串定义为有效的回文串。 - -示例 1: - -``` -输入: "A man, a plan, a canal: Panama" -输出: true -``` - -示例 2: - -``` -输入: "race a car" -输出: false -``` - -```java -//https://leetcode-cn.com/problems/valid-palindrome/description/ -class Solution { - public boolean isPalindrome(String s) { - if (s.length() == 0) - return true; - int l = 0, r = s.length() - 1; - while (l < r) { - // 从头和尾开始向中间遍历 - if (!Character.isLetterOrDigit(s.charAt(l))) {// 字符不是字母和数字的情况 - l++; - } else if (!Character.isLetterOrDigit(s.charAt(r))) {// 字符不是字母和数字的情况 - r--; - } else { - // 判断二者是否相等 - if (Character.toLowerCase(s.charAt(l)) != Character.toLowerCase(s.charAt(r))) - return false; - l++; - r--; - } - } - return true; - } -} -``` - - -### 4.3. 最长回文子串 - -> Leetcode: LeetCode: 最长回文子串 给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。 - -示例 1: - -``` -输入: "babad" -输出: "bab" -注意: "aba"也是一个有效答案。 -``` - -示例 2: - -``` -输入: "cbbd" -输出: "bb" -``` - -以某个元素为中心,分别计算偶数长度的回文最大长度和奇数长度的回文最大长度。给大家大致花了个草图,不要嫌弃! - - -![](https://user-gold-cdn.xitu.io/2018/9/9/165bc32f6f1833ff?w=723&h=371&f=png&s=9305) - -```java -//https://leetcode-cn.com/problems/longest-palindromic-substring/description/ -class Solution { - private int index, len; - - public String longestPalindrome(String s) { - if (s.length() < 2) - return s; - for (int i = 0; i < s.length() - 1; i++) { - PalindromeHelper(s, i, i); - PalindromeHelper(s, i, i + 1); - } - return s.substring(index, index + len); - } - - public void PalindromeHelper(String s, int l, int r) { - while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { - l--; - r++; - } - if (len < r - l - 1) { - index = l + 1; - len = r - l - 1; - } - } -} -``` - -### 4.4. 最长回文子序列 - -> LeetCode: 最长回文子序列 -给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000。 -**最长回文子序列和上一题最长回文子串的区别是,子串是字符串中连续的一个序列,而子序列是字符串中保持相对位置的字符序列,例如,"bbbb"可以是字符串"bbbab"的子序列但不是子串。** - -给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000。 - -示例 1: - -``` -输入: -"bbbab" -输出: -4 -``` -一个可能的最长回文子序列为 "bbbb"。 - -示例 2: - -``` -输入: -"cbbd" -输出: -2 -``` - -一个可能的最长回文子序列为 "bb"。 - -**动态规划:** dp[i][j] = dp[i+1][j-1] + 2 if s.charAt(i) == s.charAt(j) otherwise, dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) - -```java -class Solution { - public int longestPalindromeSubseq(String s) { - int len = s.length(); - int [][] dp = new int[len][len]; - for(int i = len - 1; i>=0; i--){ - dp[i][i] = 1; - for(int j = i+1; j < len; j++){ - if(s.charAt(i) == s.charAt(j)) - dp[i][j] = dp[i+1][j-1] + 2; - else - dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]); - } - } - return dp[0][len-1]; - } -} -``` - -## 5. 括号匹配深度 - -> 爱奇艺 2018 秋招 Java: ->一个合法的括号匹配序列有以下定义: ->1. 空串""是一个合法的括号匹配序列 ->2. 如果"X"和"Y"都是合法的括号匹配序列,"XY"也是一个合法的括号匹配序列 ->3. 如果"X"是一个合法的括号匹配序列,那么"(X)"也是一个合法的括号匹配序列 ->4. 每个合法的括号序列都可以由以上规则生成。 - -> 例如: "","()","()()","((()))"都是合法的括号序列 ->对于一个合法的括号序列我们又有以下定义它的深度: ->1. 空串""的深度是0 ->2. 如果字符串"X"的深度是x,字符串"Y"的深度是y,那么字符串"XY"的深度为max(x,y) ->3. 如果"X"的深度是x,那么字符串"(X)"的深度是x+1 - -> 例如: "()()()"的深度是1,"((()))"的深度是3。牛牛现在给你一个合法的括号序列,需要你计算出其深度。 - -``` -输入描述: -输入包括一个合法的括号序列s,s长度length(2 ≤ length ≤ 50),序列中只包含'('和')'。 - -输出描述: -输出一个正整数,即这个序列的深度。 -``` - -示例: - -``` -输入: -(()) -输出: -2 -``` - -思路草图: - - -![](https://user-gold-cdn.xitu.io/2018/9/9/165bc6fca94ef278?w=792&h=324&f=png&s=15868) - -代码如下: - -```java -import java.util.Scanner; - -/** - * https://www.nowcoder.com/test/8246651/summary - * - * @author Snailclimb - * @date 2018年9月6日 - * @Description: TODO 求给定合法括号序列的深度 - */ -public class Main { - public static void main(String[] args) { - Scanner sc = new Scanner(System.in); - String s = sc.nextLine(); - int cnt = 0, max = 0, i; - for (i = 0; i < s.length(); ++i) { - if (s.charAt(i) == '(') - cnt++; - else - cnt--; - max = Math.max(max, cnt); - } - sc.close(); - System.out.println(max); - } -} - -``` - -## 6. 把字符串转换成整数 - -> 剑指offer: 将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。 - -```java -//https://www.weiweiblog.cn/strtoint/ -public class Main { - - public static int StrToInt(String str) { - if (str.length() == 0) - return 0; - char[] chars = str.toCharArray(); - // 判断是否存在符号位 - int flag = 0; - if (chars[0] == '+') - flag = 1; - else if (chars[0] == '-') - flag = 2; - int start = flag > 0 ? 1 : 0; - int res = 0;// 保存结果 - for (int i = start; i < chars.length; i++) { - if (Character.isDigit(chars[i])) {// 调用Character.isDigit(char)方法判断是否是数字,是返回True,否则False - int temp = chars[i] - '0'; - res = res * 10 + temp; - } else { - return 0; - } - } - return flag != 2 ? res : -res; - - } - - public static void main(String[] args) { - // TODO Auto-generated method stub - String s = "-12312312"; - System.out.println("使用库函数转换:" + Integer.valueOf(s)); - int res = Main.StrToInt(s); - System.out.println("使用自己写的方法转换:" + res); - - } - -} - -``` diff --git "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" "b/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" deleted file mode 100644 index 85e2934..0000000 --- "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" +++ /dev/null @@ -1,421 +0,0 @@ - - -- [1. 两数相加](#1-两数相加) - - [题目描述](#题目描述) - - [问题分析](#问题分析) - - [Solution](#solution) -- [2. 翻转链表](#2-翻转链表) - - [题目描述](#题目描述-1) - - [问题分析](#问题分析-1) - - [Solution](#solution-1) -- [3. 链表中倒数第k个节点](#3-链表中倒数第k个节点) - - [题目描述](#题目描述-2) - - [问题分析](#问题分析-2) - - [Solution](#solution-2) -- [4. 删除链表的倒数第N个节点](#4-删除链表的倒数第n个节点) - - [问题分析](#问题分析-3) - - [Solution](#solution-3) -- [5. 合并两个排序的链表](#5-合并两个排序的链表) - - [题目描述](#题目描述-3) - - [问题分析](#问题分析-4) - - [Solution](#solution-4) - - - - -# 1. 两数相加 - -### 题目描述 - -> Leetcode:给定两个非空链表来表示两个非负整数。位数按照逆序方式存储,它们的每个节点只存储单个数字。将两数相加返回一个新的链表。 -> ->你可以假设除了数字 0 之外,这两个数字都不会以零开头。 - -示例: - -``` -输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) -输出:7 -> 0 -> 8 -原因:342 + 465 = 807 -``` - -### 问题分析 - -Leetcode官方详细解答地址: - - https://leetcode-cn.com/problems/add-two-numbers/solution/ - -> 要对头结点进行操作时,考虑创建哑节点dummy,使用dummy->next表示真正的头节点。这样可以避免处理头节点为空的边界问题。 - -我们使用变量来跟踪进位,并从包含最低有效位的表头开始模拟逐 -位相加的过程。 - -![图1,对两数相加方法的可视化: 342 + 465 = 807342+465=807, 每个结点都包含一个数字,并且数字按位逆序存储。](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/34910956.jpg) - -### Solution - -**我们首先从最低有效位也就是列表 l1和 l2 的表头开始相加。注意需要考虑到进位的情况!** - -```java -/** - * Definition for singly-linked list. - * public class ListNode { - * int val; - * ListNode next; - * ListNode(int x) { val = x; } - * } - */ - //https://leetcode-cn.com/problems/add-two-numbers/description/ -class Solution { -public ListNode addTwoNumbers(ListNode l1, ListNode l2) { - ListNode dummyHead = new ListNode(0); - ListNode p = l1, q = l2, curr = dummyHead; - //carry 表示进位数 - int carry = 0; - while (p != null || q != null) { - int x = (p != null) ? p.val : 0; - int y = (q != null) ? q.val : 0; - int sum = carry + x + y; - //进位数 - carry = sum / 10; - //新节点的数值为sum % 10 - curr.next = new ListNode(sum % 10); - curr = curr.next; - if (p != null) p = p.next; - if (q != null) q = q.next; - } - if (carry > 0) { - curr.next = new ListNode(carry); - } - return dummyHead.next; -} -} -``` - -# 2. 翻转链表 - - -### 题目描述 -> 剑指 offer:输入一个链表,反转链表后,输出链表的所有元素。 - -![翻转链表](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/81431871.jpg) - -### 问题分析 - -这道算法题,说直白点就是:如何让后一个节点指向前一个节点!在下面的代码中定义了一个 next 节点,该节点主要是保存要反转到头的那个节点,防止链表 “断裂”。 - -### Solution - - -```java -public class ListNode { - int val; - ListNode next = null; - - ListNode(int val) { - this.val = val; - } -} -``` - -```java -/** - * - * @author Snailclimb - * @date 2018年9月19日 - * @Description: TODO - */ -public class Solution { - - public ListNode ReverseList(ListNode head) { - - ListNode next = null; - ListNode pre = null; - - while (head != null) { - // 保存要反转到头的那个节点 - next = head.next; - // 要反转的那个节点指向已经反转的上一个节点(备注:第一次反转的时候会指向null) - head.next = pre; - // 上一个已经反转到头部的节点 - pre = head; - // 一直向链表尾走 - head = next; - } - return pre; - } - -} -``` - -测试方法: - -```java - public static void main(String[] args) { - - ListNode a = new ListNode(1); - ListNode b = new ListNode(2); - ListNode c = new ListNode(3); - ListNode d = new ListNode(4); - ListNode e = new ListNode(5); - a.next = b; - b.next = c; - c.next = d; - d.next = e; - new Solution().ReverseList(a); - while (e != null) { - System.out.println(e.val); - e = e.next; - } - } -``` - -输出: - -``` -5 -4 -3 -2 -1 -``` - -# 3. 链表中倒数第k个节点 - -### 题目描述 - -> 剑指offer: 输入一个链表,输出该链表中倒数第k个结点。 - -### 问题分析 - -> **链表中倒数第k个节点也就是正数第(L-K+1)个节点,知道了只一点,这一题基本就没问题!** - -首先两个节点/指针,一个节点 node1 先开始跑,指针 node1 跑到 k-1 个节点后,另一个节点 node2 开始跑,当 node1 跑到最后时,node2 所指的节点就是倒数第k个节点也就是正数第(L-K+1)个节点。 - - -### Solution - -```java -/* -public class ListNode { - int val; - ListNode next = null; - - ListNode(int val) { - this.val = val; - } -}*/ - -// 时间复杂度O(n),一次遍历即可 -// https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&tqId=11167&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking -public class Solution { - public ListNode FindKthToTail(ListNode head, int k) { - // 如果链表为空或者k小于等于0 - if (head == null || k <= 0) { - return null; - } - // 声明两个指向头结点的节点 - ListNode node1 = head, node2 = head; - // 记录节点的个数 - int count = 0; - // 记录k值,后面要使用 - int index = k; - // p指针先跑,并且记录节点数,当node1节点跑了k-1个节点后,node2节点开始跑, - // 当node1节点跑到最后时,node2节点所指的节点就是倒数第k个节点 - while (node1 != null) { - node1 = node1.next; - count++; - if (k < 1) { - node2 = node2.next; - } - k--; - } - // 如果节点个数小于所求的倒数第k个节点,则返回空 - if (count < index) - return null; - return node2; - - } -} -``` - - -# 4. 删除链表的倒数第N个节点 - - -> Leetcode:给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。 - -**示例:** - -``` -给定一个链表: 1->2->3->4->5, 和 n = 2. - -当删除了倒数第二个节点后,链表变为 1->2->3->5. - -``` - -**说明:** - -给定的 n 保证是有效的。 - -**进阶:** - -你能尝试使用一趟扫描实现吗? - -该题在 leetcode 上有详细解答,具体可参考 Leetcode. - -### 问题分析 - - -我们注意到这个问题可以容易地简化成另一个问题:删除从列表开头数起的第 (L - n + 1)个结点,其中 L是列表的长度。只要我们找到列表的长度 L,这个问题就很容易解决。 - -![图 1. 删除列表中的第 L - n + 1 个元素](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/94354387.jpg) - -### Solution - -**两次遍历法** - -首先我们将添加一个 **哑结点** 作为辅助,该结点位于列表头部。哑结点用来简化某些极端情况,例如列表中只含有一个结点,或需要删除列表的头部。在第一次遍历中,我们找出列表的长度 L。然后设置一个指向哑结点的指针,并移动它遍历列表,直至它到达第 (L - n) 个结点那里。**我们把第 (L - n)个结点的 next 指针重新链接至第 (L - n + 2)个结点,完成这个算法。** - -```java -/** - * Definition for singly-linked list. - * public class ListNode { - * int val; - * ListNode next; - * ListNode(int x) { val = x; } - * } - */ -// https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/description/ -public class Solution { - public ListNode removeNthFromEnd(ListNode head, int n) { - // 哑结点,哑结点用来简化某些极端情况,例如列表中只含有一个结点,或需要删除列表的头部 - ListNode dummy = new ListNode(0); - // 哑结点指向头结点 - dummy.next = head; - // 保存链表长度 - int length = 0; - ListNode len = head; - while (len != null) { - length++; - len = len.next; - } - length = length - n; - ListNode target = dummy; - // 找到 L-n 位置的节点 - while (length > 0) { - target = target.next; - length--; - } - // 把第 (L - n)个结点的 next 指针重新链接至第 (L - n + 2)个结点 - target.next = target.next.next; - return dummy.next; - } -} -``` - -**复杂度分析:** - -- **时间复杂度 O(L)** :该算法对列表进行了两次遍历,首先计算了列表的长度 LL 其次找到第 (L - n)(L−n) 个结点。 操作执行了 2L-n2L−n 步,时间复杂度为 O(L)O(L)。 -- **空间复杂度 O(1)** :我们只用了常量级的额外空间。 - - - -**进阶——一次遍历法:** - - -> **链表中倒数第N个节点也就是正数第(L-N+1)个节点。 - -其实这种方法就和我们上面第四题找“链表中倒数第k个节点”所用的思想是一样的。**基本思路就是:** 定义两个节点 node1、node2;node1 节点先跑,node1节点 跑到第 n+1 个节点的时候,node2 节点开始跑.当node1 节点跑到最后一个节点时,node2 节点所在的位置就是第 (L-n ) 个节点(L代表总链表长度,也就是倒数第 n+1 个节点) - -```java -/** - * Definition for singly-linked list. - * public class ListNode { - * int val; - * ListNode next; - * ListNode(int x) { val = x; } - * } - */ -public class Solution { - public ListNode removeNthFromEnd(ListNode head, int n) { - - ListNode dummy = new ListNode(0); - dummy.next = head; - // 声明两个指向头结点的节点 - ListNode node1 = dummy, node2 = dummy; - - // node1 节点先跑,node1节点 跑到第 n 个节点的时候,node2 节点开始跑 - // 当node1 节点跑到最后一个节点时,node2 节点所在的位置就是第 (L-n ) 个节点,也就是倒数第 n+1(L代表总链表长度) - while (node1 != null) { - node1 = node1.next; - if (n < 1 && node1 != null) { - node2 = node2.next; - } - n--; - } - - node2.next = node2.next.next; - - return dummy.next; - - } -} -``` - - - - - -# 5. 合并两个排序的链表 - -### 题目描述 - -> 剑指offer:输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。 - -### 问题分析 - -我们可以这样分析: - -1. 假设我们有两个链表 A,B; -2. A的头节点A1的值与B的头结点B1的值比较,假设A1小,则A1为头节点; -3. A2再和B1比较,假设B1小,则,A1指向B1; -4. A2再和B2比较 -就这样循环往复就行了,应该还算好理解。 - -考虑通过递归的方式实现! - -### Solution - -**递归版本:** - -```java -/* -public class ListNode { - int val; - ListNode next = null; - - ListNode(int val) { - this.val = val; - } -}*/ -//https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=13&tqId=11169&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking -public class Solution { -public ListNode Merge(ListNode list1,ListNode list2) { - if(list1 == null){ - return list2; - } - if(list2 == null){ - return list1; - } - if(list1.val <= list2.val){ - list1.next = Merge(list1.next, list2); - return list1; - }else{ - list2.next = Merge(list1, list2.next); - return list2; - } - } -} -``` - diff --git "a/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" "b/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" deleted file mode 100644 index 51de35e..0000000 --- "a/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" +++ /dev/null @@ -1,686 +0,0 @@ -### 一 斐波那契数列 - -#### **题目描述:** - -大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项。 -n<=39 - -#### **问题分析:** - -可以肯定的是这一题通过递归的方式是肯定能做出来,但是这样会有一个很大的问题,那就是递归大量的重复计算会导致内存溢出。另外可以使用迭代法,用fn1和fn2保存计算过程中的结果,并复用起来。下面我会把两个方法示例代码都给出来并给出两个方法的运行时间对比。 - -#### **示例代码:** - -**采用迭代法:** - -```java - int Fibonacci(int number) { - if (number <= 0) { - return 0; - } - if (number == 1 || number == 2) { - return 1; - } - int first = 1, second = 1, third = 0; - for (int i = 3; i <= number; i++) { - third = first + second; - first = second; - second = third; - } - return third; - } -``` - -**采用递归:** - -```java - public int Fibonacci(int n) { - - if (n <= 0) { - return 0; - } - if (n == 1||n==2) { - return 1; - } - - return Fibonacci(n - 2) + Fibonacci(n - 1); - - } -``` - -#### **运行时间对比:** - -假设n为40我们分别使用迭代法和递归法计算,计算结果如下: - -1. 迭代法 - ![迭代法](https://ws1.sinaimg.cn/large/006rNwoDgy1fpydt5as85j308a025dfl.jpg) -2. 递归法 - ![递归法](https://ws1.sinaimg.cn/large/006rNwoDgy1fpydt2d1k3j30ed02kt8i.jpg) - -### 二 跳台阶问题 - -#### **题目描述:** - -一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 - -#### **问题分析:** - -**正常分析法:** -a.如果两种跳法,1阶或者2阶,那么假定第一次跳的是一阶,那么剩下的是n-1个台阶,跳法是f(n-1); -b.假定第一次跳的是2阶,那么剩下的是n-2个台阶,跳法是f(n-2) -c.由a,b假设可以得出总跳法为: f(n) = f(n-1) + f(n-2) -d.然后通过实际的情况可以得出:只有一阶的时候 f(1) = 1 ,只有两阶的时候可以有 f(2) = 2 -**找规律分析法:** -f(1) = 1, f(2) = 2, f(3) = 3, f(4) = 5, 可以总结出f(n) = f(n-1) + f(n-2)的规律。 -但是为什么会出现这样的规律呢?假设现在6个台阶,我们可以从第5跳一步到6,这样的话有多少种方案跳到5就有多少种方案跳到6,另外我们也可以从4跳两步跳到6,跳到4有多少种方案的话,就有多少种方案跳到6,其他的不能从3跳到6什么的啦,所以最后就是f(6) = f(5) + f(4);这样子也很好理解变态跳台阶的问题了。 - -**所以这道题其实就是斐波那契数列的问题。** -代码只需要在上一题的代码稍做修改即可。和上一题唯一不同的就是这一题的初始元素变为 1 2 3 5 8.....而上一题为1 1 2 3 5 .......。另外这一题也可以用递归做,但是递归效率太低,所以我这里只给出了迭代方式的代码。 - -#### **示例代码:** - -```java - int jumpFloor(int number) { - if (number <= 0) { - return 0; - } - if (number == 1) { - return 1; - } - if (number == 2) { - return 2; - } - int first = 1, second = 2, third = 0; - for (int i = 3; i <= number; i++) { - third = first + second; - first = second; - second = third; - } - return third; - } -``` - -### 三 变态跳台阶问题 - -#### **题目描述:** - -一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 - -#### **问题分析:** - -假设n>=2,第一步有n种跳法:跳1级、跳2级、到跳n级 -跳1级,剩下n-1级,则剩下跳法是f(n-1) -跳2级,剩下n-2级,则剩下跳法是f(n-2) -...... -跳n-1级,剩下1级,则剩下跳法是f(1) -跳n级,剩下0级,则剩下跳法是f(0) -所以在n>=2的情况下: -f(n)=f(n-1)+f(n-2)+...+f(1) -因为f(n-1)=f(n-2)+f(n-3)+...+f(1) -所以f(n)=2*f(n-1) 又f(1)=1,所以可得**f(n)=2^(number-1)** - -#### **示例代码:** - -```java - int JumpFloorII(int number) { - return 1 << --number;//2^(number-1)用位移操作进行,更快 - } -``` - -#### **补充:** - -**java中有三种移位运算符:** - -1. “<<” : **左移运算符**,等同于乘2的n次方 -2. “>>”: **右移运算符**,等同于除2的n次方 -3. “>>>” **无符号右移运算符**,不管移动前最高位是0还是1,右移后左侧产生的空位部分都以0来填充。与>>类似。 - 例: - int a = 16; - int b = a << 2;//左移2,等同于16 * 2的2次方,也就是16 * 4 - int c = a >> 2;//右移2,等同于16 / 2的2次方,也就是16 / 4 - -### 四 二维数组查找 - -#### **题目描述:** - -在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 - -#### **问题解析:** - -这一道题还是比较简单的,我们需要考虑的是如何做,效率最快。这里有一种很好理解的思路: - -> 矩阵是有序的,从左下角来看,向上数字递减,向右数字递增, -> 因此从左下角开始查找,当要查找数字比左下角数字大时。右移 -> 要查找数字比左下角数字小时,上移。这样找的速度最快。 - -#### **示例代码:** - -```java - public boolean Find(int target, int [][] array) { - //基本思路从左下角开始找,这样速度最快 - int row = array.length-1;//行 - int column = 0;//列 - //当行数大于0,当前列数小于总列数时循环条件成立 - while((row >= 0)&& (column< array[0].length)){ - if(array[row][column] > target){ - row--; - }else if(array[row][column] < target){ - column++; - }else{ - return true; - } - } - return false; - } -``` - -### 五 替换空格 - -#### **题目描述:** - -请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。 - -#### **问题分析:** - -这道题不难,我们可以通过循环判断字符串的字符是否为空格,是的话就利用append()方法添加追加“%20”,否则还是追加原字符。 - -或者最简单的方法就是利用: replaceAll(String regex,String replacement)方法了,一行代码就可以解决。 - -#### **示例代码:** - -**常规做法:** - -```java - public String replaceSpace(StringBuffer str) { - StringBuffer out=new StringBuffer(); - for (int i = 0; i < str.toString().length(); i++) { - char b=str.charAt(i); - if(String.valueOf(b).equals(" ")){ - out.append("%20"); - }else{ - out.append(b); - } - } - return out.toString(); - } -``` - -**一行代码解决:** - -```java - public String replaceSpace(StringBuffer str) { - //return str.toString().replaceAll(" ", "%20"); - //public String replaceAll(String regex,String replacement) - //用给定的替换替换与给定的regular expression匹配的此字符串的每个子字符串。 - //\ 转义字符. 如果你要使用 "\" 本身, 则应该使用 "\\". String类型中的空格用“\s”表示,所以我这里猜测"\\s"就是代表空格的意思 - return str.toString().replaceAll("\\s", "%20"); - } - -``` - -### 六 数值的整数次方 - -#### **题目描述:** - -给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。 - -#### **问题解析:** - -这道题算是比较麻烦和难一点的一个了。我这里采用的是**二分幂**思想,当然也可以采用**快速幂**。 -更具剑指offer书中细节,该题的解题思路如下: -1.当底数为0且指数<0时,会出现对0求倒数的情况,需进行错误处理,设置一个全局变量; -2.判断底数是否等于0,由于base为double型,所以不能直接用==判断 -3.优化求幂函数(二分幂)。 -当n为偶数,a^n =(a^n/2)*(a^n/2); -当n为奇数,a^n = a^[(n-1)/2] * a^[(n-1)/2] * a。时间复杂度O(logn) - -**时间复杂度**:O(logn) - -#### **示例代码:** - -```java -public class Solution { - boolean invalidInput=false; - public double Power(double base, int exponent) { - //如果底数等于0并且指数小于0 - //由于base为double型,不能直接用==判断 - if(equal(base,0.0)&&exponent<0){ - invalidInput=true; - return 0.0; - } - int absexponent=exponent; - //如果指数小于0,将指数转正 - if(exponent<0) - absexponent=-exponent; - //getPower方法求出base的exponent次方。 - double res=getPower(base,absexponent); - //如果指数小于0,所得结果为上面求的结果的倒数 - if(exponent<0) - res=1.0/res; - return res; - } - //比较两个double型变量是否相等的方法 - boolean equal(double num1,double num2){ - if(num1-num2>-0.000001&&num1-num2<0.000001) - return true; - else - return false; - } - //求出b的e次方的方法 - double getPower(double b,int e){ - //如果指数为0,返回1 - if(e==0) - return 1.0; - //如果指数为1,返回b - if(e==1) - return b; - //e>>1相等于e/2,这里就是求a^n =(a^n/2)*(a^n/2) - double result=getPower(b,e>>1); - result*=result; - //如果指数n为奇数,则要再乘一次底数base - if((e&1)==1) - result*=b; - return result; - } -} -``` - -当然这一题也可以采用笨方法:累乘。不过这种方法的时间复杂度为O(n),这样没有前一种方法效率高。 - -```java - // 使用累乘 - public double powerAnother(double base, int exponent) { - double result = 1.0; - for (int i = 0; i < Math.abs(exponent); i++) { - result *= base; - } - if (exponent >= 0) - return result; - else - return 1 / result; - } -``` - -### 七 调整数组顺序使奇数位于偶数前面 - -#### **题目描述:** - -输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。 - -#### **问题解析:** - -这道题有挺多种解法的,给大家介绍一种我觉得挺好理解的方法: -我们首先统计奇数的个数假设为n,然后新建一个等长数组,然后通过循环判断原数组中的元素为偶数还是奇数。如果是则从数组下标0的元素开始,把该奇数添加到新数组;如果是偶数则从数组下标为n的元素开始把该偶数添加到新数组中。 - -#### **示例代码:** - -时间复杂度为O(n),空间复杂度为O(n)的算法 - -```java -public class Solution { - public void reOrderArray(int [] array) { - //如果数组长度等于0或者等于1,什么都不做直接返回 - if(array.length==0||array.length==1) - return; - //oddCount:保存奇数个数 - //oddBegin:奇数从数组头部开始添加 - int oddCount=0,oddBegin=0; - //新建一个数组 - int[] newArray=new int[array.length]; - //计算出(数组中的奇数个数)开始添加元素 - for(int i=0;i stack1 = new Stack(); - Stack stack2 = new Stack(); - - //当执行push操作时,将元素添加到stack1 - public void push(int node) { - stack1.push(node); - } - - public int pop() { - //如果两个队列都为空则抛出异常,说明用户没有push进任何元素 - if(stack1.empty()&&stack2.empty()){ - throw new RuntimeException("Queue is empty!"); - } - //如果stack2不为空直接对stack2执行pop操作, - if(stack2.empty()){ - while(!stack1.empty()){ - //将stack1的元素按后进先出push进stack2里面 - stack2.push(stack1.pop()); - } - } - return stack2.pop(); - } -} -``` - -### 十二 栈的压入,弹出序列 - -#### **题目描述:** - -输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的) - -#### **题目分析:** - -这道题想了半天没有思路,参考了Alias的答案,他的思路写的也很详细应该很容易看懂。 -作者:Alias -https://www.nowcoder.com/questionTerminal/d77d11405cc7470d82554cb392585106 -来源:牛客网 - -【思路】借用一个辅助的栈,遍历压栈顺序,先讲第一个放入栈中,这里是1,然后判断栈顶元素是不是出栈顺序的第一个元素,这里是4,很显然1≠4,所以我们继续压栈,直到相等以后开始出栈,出栈一个元素,则将出栈顺序向后移动一位,直到不相等,这样循环等压栈顺序遍历完成,如果辅助栈还不为空,说明弹出序列不是该栈的弹出顺序。 - -举例: - -入栈1,2,3,4,5 - -出栈4,5,3,2,1 - -首先1入辅助栈,此时栈顶1≠4,继续入栈2 - -此时栈顶2≠4,继续入栈3 - -此时栈顶3≠4,继续入栈4 - -此时栈顶4=4,出栈4,弹出序列向后一位,此时为5,,辅助栈里面是1,2,3 - -此时栈顶3≠5,继续入栈5 - -此时栈顶5=5,出栈5,弹出序列向后一位,此时为3,,辅助栈里面是1,2,3 - -…. -依次执行,最后辅助栈为空。如果不为空说明弹出序列不是该栈的弹出顺序。 - - - -#### **考察内容:** - -栈 - -#### **示例代码:** - -```java -import java.util.ArrayList; -import java.util.Stack; -//这道题没想出来,参考了Alias同学的答案:https://www.nowcoder.com/questionTerminal/d77d11405cc7470d82554cb392585106 -public class Solution { - public boolean IsPopOrder(int [] pushA,int [] popA) { - if(pushA.length == 0 || popA.length == 0) - return false; - Stack s = new Stack(); - //用于标识弹出序列的位置 - int popIndex = 0; - for(int i = 0; i< pushA.length;i++){ - s.push(pushA[i]); - //如果栈不为空,且栈顶元素等于弹出序列 - while(!s.empty() &&s.peek() == popA[popIndex]){ - //出栈 - s.pop(); - //弹出序列向后一位 - popIndex++; - } - } - return s.empty(); - } -} -``` \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" deleted file mode 100644 index dfb5bc1..0000000 --- "a/docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ /dev/null @@ -1,192 +0,0 @@ -下面只是简单地总结,给了一些参考文章,后面会对这部分内容进行重构。 - - -- [Queue](#queue) - - [什么是队列](#什么是队列) - - [队列的种类](#队列的种类) - - [Java 集合框架中的队列 Queue](#java-集合框架中的队列-queue) - - [推荐文章](#推荐文章) -- [Set](#set) - - [什么是 Set](#什么是-set) - - [补充:有序集合与无序集合说明](#补充:有序集合与无序集合说明) - - [HashSet 和 TreeSet 底层数据结构](#hashset-和-treeset-底层数据结构) - - [推荐文章](#推荐文章-1) -- [List](#list) - - [什么是List](#什么是list) - - [List的常见实现类](#list的常见实现类) - - [ArrayList 和 LinkedList 源码学习](#arraylist-和-linkedlist-源码学习) - - [推荐阅读](#推荐阅读) -- [Map](#map) -- [树](#树) - - - - -## Queue - -### 什么是队列 -队列是数据结构中比较重要的一种类型,它支持 FIFO,尾部添加、头部删除(先进队列的元素先出队列),跟我们生活中的排队类似。 - -### 队列的种类 - -- **单队列**(单队列就是常见的队列, 每次添加元素时,都是添加到队尾,存在“假溢出”的问题也就是明明有位置却不能添加的情况) -- **循环队列**(避免了“假溢出”的问题) - -### Java 集合框架中的队列 Queue - -Java 集合中的 Queue 继承自 Collection 接口 ,Deque, LinkedList, PriorityQueue, BlockingQueue 等类都实现了它。 -Queue 用来存放 等待处理元素 的集合,这种场景一般用于缓冲、并发访问。 -除了继承 Collection 接口的一些方法,Queue 还添加了额外的 添加、删除、查询操作。 - -### 推荐文章 - -- [Java 集合深入理解(9):Queue 队列](https://blog.csdn.net/u011240877/article/details/52860924) - -## Set - -### 什么是 Set -Set 继承于 Collection 接口,是一个不允许出现重复元素,并且无序的集合,主要 HashSet 和 TreeSet 两大实现类。 - -在判断重复元素的时候,HashSet 集合会调用 hashCode()和 equal()方法来实现;TreeSet 集合会调用compareTo方法来实现。 - -### 补充:有序集合与无序集合说明 -- 有序集合:集合里的元素可以根据 key 或 index 访问 (List、Map) -- 无序集合:集合里的元素只能遍历。(Set) - - -### HashSet 和 TreeSet 底层数据结构 - -**HashSet** 是哈希表结构,主要利用 HashMap 的 key 来存储元素,计算插入元素的 hashCode 来获取元素在集合中的位置; - -**TreeSet** 是红黑树结构,每一个元素都是树中的一个节点,插入的元素都会进行排序; - - -### 推荐文章 - -- [Java集合--Set(基础)](https://www.jianshu.com/p/b48c47a42916) - -## List - -### 什么是List - -在 List 中,用户可以精确控制列表中每个元素的插入位置,另外用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。 与 Set 不同,List 通常允许重复的元素。 另外 List 是有序集合而 Set 是无序集合。 - -### List的常见实现类 - -**ArrayList** 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。 - -**LinkedList** 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。 - -**Vector** 是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。 - -**Stack** 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。相关阅读:[java数据结构与算法之栈(Stack)设计与实现](https://blog.csdn.net/javazejian/article/details/53362993) - -### ArrayList 和 LinkedList 源码学习 - -- [ArrayList 源码学习](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList.md) -- [LinkedList 源码学习](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/LinkedList.md) - -### 推荐阅读 - -- [java 数据结构与算法之顺序表与链表深入分析](https://blog.csdn.net/javazejian/article/details/52953190) - - -## Map - - -- [集合框架源码学习之 HashMap(JDK1.8)](https://juejin.im/post/5ab0568b5188255580020e56) -- [ConcurrentHashMap 实现原理及源码分析](https://link.juejin.im/?target=http%3A%2F%2Fwww.cnblogs.com%2Fchengxiao%2Fp%2F6842045.html) - -## 树 - * ### 1 二叉树 - - [二叉树](https://baike.baidu.com/item/%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科) - - (1)[完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91)——若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。 - - (2)[满二叉树](https://baike.baidu.com/item/%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91)——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。 - - (3)[平衡二叉树](https://baike.baidu.com/item/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/10421057)——平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 - - * ### 2 完全二叉树 - - [完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科) - - 完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。 - * ### 3 满二叉树 - - [满二叉树](https://baike.baidu.com/item/%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科,国内外的定义不同) - - 国内教程定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。 - * ### 堆 - - [数据结构之堆的定义](https://blog.csdn.net/qq_33186366/article/details/51876191) - - 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。 - * ### 4 二叉查找树(BST) - - [浅谈算法和数据结构: 七 二叉查找树](http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html) - - 二叉查找树的特点: - - 1. 若任意节点的左子树不空,则左子树上所有结点的 值均小于它的根结点的值; - 2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值; - 3. 任意节点的左、右子树也分别为二叉查找树; - 4. 没有键值相等的节点(no duplicate nodes)。 - - * ### 5 平衡二叉树(Self-balancing binary search tree) - - [ 平衡二叉树](https://baike.baidu.com/item/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科,平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等) - * ### 6 红黑树 - - - 红黑树特点: - 1. 每个节点非红即黑; - 2. 根节点总是黑色的; - 3. 每个叶子节点都是黑色的空节点(NIL节点); - 4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定); - 5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。 - - - 红黑树的应用: - - TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。 - - - 为什么要用红黑树 - - 简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。详细了解可以查看 [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐) - - - 推荐文章: - - [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐) - - [寻找红黑树的操作手册](http://dandanlove.com/2018/03/18/red-black-tree/)(文章排版以及思路真的不错) - - [红黑树深入剖析及Java实现](https://zhuanlan.zhihu.com/p/24367771)(美团点评技术团队) - * ### 7 B-,B+,B*树 - - [二叉树学习笔记之B树、B+树、B*树 ](https://yq.aliyun.com/articles/38345) - - [《B-树,B+树,B*树详解》](https://blog.csdn.net/aqzwss/article/details/53074186) - - [《B-树,B+树与B*树的优缺点比较》](https://blog.csdn.net/bigtree_3721/article/details/73632405) - - B-树(或B树)是一种平衡的多路查找(又称排序)树,在文件系统中有所应用。主要用作文件的索引。其中的B就表示平衡(Balance) - 1. B+ 树的叶子节点链表结构相比于 B- 树便于扫库,和范围检索。 - 2. B+树支持range-query(区间查询)非常方便,而B树不支持。这是数据库选用B+树的最主要原因。 - 3. B\*树 是B+树的变体,B\*树分配新结点的概率比B+树要低,空间使用率更高; - * ### 8 LSM 树 - - [[HBase] LSM树 VS B+树](https://blog.csdn.net/dbanote/article/details/8897599) - - B+树最大的性能问题是会产生大量的随机IO - - 为了克服B+树的弱点,HBase引入了LSM树的概念,即Log-Structured Merge-Trees。 - - [LSM树由来、设计思想以及应用到HBase的索引](http://www.cnblogs.com/yanghuahui/p/3483754.html) - - -## 图 - - - - -## BFS及DFS - -- [《使用BFS及DFS遍历树和图的思路及实现》](https://blog.csdn.net/Gene1994/article/details/85097507) - diff --git "a/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" "b/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" deleted file mode 100644 index 4c5df56..0000000 --- "a/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" +++ /dev/null @@ -1,52 +0,0 @@ -我比较推荐大家可以刷一下 Leetcode ,我自己平时没事也会刷一下,我觉得刷 Leetcode 不仅是为了能让你更从容地面对面试中的手撕算法问题,更可以提高你的编程思维能力、解决问题的能力以及你对某门编程语言 API 的熟练度。当然牛客网也有一些算法题,我下面也整理了一些。 - -## LeetCode - -- [LeetCode(中国)官网](https://leetcode-cn.com/) - -- [如何高效地使用 LeetCode](https://leetcode-cn.com/articles/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%9C%B0%E4%BD%BF%E7%94%A8-leetcode/) - - -## 牛客网 - -- [牛客网官网](https://www.nowcoder.com) -- [剑指offer编程题](https://www.nowcoder.com/ta/coding-interviews) - -- [2017校招真题](https://www.nowcoder.com/ta/2017test) -- [华为机试题](https://www.nowcoder.com/ta/huawei) - - -## 公司真题 - -- [ 网易2018校园招聘编程题真题集合](https://www.nowcoder.com/test/6910869/summary) -- [ 网易2018校招内推编程题集合](https://www.nowcoder.com/test/6291726/summary) -- [2017年校招全国统一模拟笔试(第五场)编程题集合](https://www.nowcoder.com/test/5986669/summary) -- [2017年校招全国统一模拟笔试(第四场)编程题集合](https://www.nowcoder.com/test/5507925/summary) -- [2017年校招全国统一模拟笔试(第三场)编程题集合](https://www.nowcoder.com/test/5217106/summary) -- [2017年校招全国统一模拟笔试(第二场)编程题集合](https://www.nowcoder.com/test/4546329/summary) -- [ 2017年校招全国统一模拟笔试(第一场)编程题集合](https://www.nowcoder.com/test/4236887/summary) -- [百度2017春招笔试真题编程题集合](https://www.nowcoder.com/test/4998655/summary) -- [网易2017春招笔试真题编程题集合](https://www.nowcoder.com/test/4575457/summary) -- [网易2017秋招编程题集合](https://www.nowcoder.com/test/2811407/summary) -- [网易有道2017内推编程题](https://www.nowcoder.com/test/2385858/summary) -- [ 滴滴出行2017秋招笔试真题-编程题汇总](https://www.nowcoder.com/test/3701760/summary) -- [腾讯2017暑期实习生编程题](https://www.nowcoder.com/test/1725829/summary) -- [今日头条2017客户端工程师实习生笔试题](https://www.nowcoder.com/test/1649301/summary) -- [今日头条2017后端工程师实习生笔试题](https://www.nowcoder.com/test/1649268/summary) - - - - - - - - - - - - - - - - - diff --git a/docs/essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md b/docs/essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md deleted file mode 100644 index 183a185..0000000 --- a/docs/essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md +++ /dev/null @@ -1,294 +0,0 @@ -作者: rhwayfun,原文地址:https://mp.weixin.qq.com/s/msYty4vjjC0PvrwasRH5Bw ,JavaGuide 已经获得作者授权并对原文进行了重新排版。 - - -- [写在2019年后的蚂蚁、头条、拼多多的面试总结](#写在2019年后的蚂蚁头条拼多多的面试总结) - - [准备过程](#准备过程) - - [蚂蚁金服](#蚂蚁金服) - - [一面](#一面) - - [二面](#二面) - - [三面](#三面) - - [四面](#四面) - - [五面](#五面) - - [小结](#小结) - - [拼多多](#拼多多) - - [面试前](#面试前) - - [一面](#一面-1) - - [二面](#二面-1) - - [三面](#三面-1) - - [小结](#小结-1) - - [字节跳动](#字节跳动) - - [面试前](#面试前-1) - - [一面](#一面-2) - - [二面](#二面-2) - - [小结](#小结-2) - - [总结](#总结) - - - -# 2019年蚂蚁金服、头条、拼多多的面试总结 - -文章有点长,请耐心看完,绝对有收获!不想听我BB直接进入面试分享: - -- 准备过程 -- 蚂蚁金服面试分享 -- 拼多多面试分享 -- 字节跳动面试分享 -- 总结 - -说起来开始进行面试是年前倒数第二周,上午9点,我还在去公司的公交上,突然收到蚂蚁的面试电话,其实算不上真正的面试。面试官只是和我聊了下他们在做的事情(主要是做双十一这里大促的稳定性保障,偏中间件吧),说的很详细,然后和我沟通了下是否有兴趣,我表示有兴趣,后面就收到正式面试的通知,最后没选择去蚂蚁表示抱歉。 - -当时我自己也准备出去看看机会,顺便看看自己的实力。当时我其实挺纠结的,一方面现在部门也正需要我,还是可以有一番作为的,另一方面觉得近一年来进步缓慢,没有以前飞速进步的成就感了,而且业务和技术偏于稳定,加上自己也属于那种比较懒散的人,骨子里还是希望能够突破现状,持续在技术上有所精进。 - -在开始正式的总结之前,还是希望各位同仁能否听我继续发泄一会,抱拳! - -我翻开自己2018年初立的flag,觉得甚是惭愧。其中就有一条是保持一周写一篇博客,奈何中间因为各种原因没能坚持下去。细细想来,主要是自己没能真正静下来心认真投入到技术的研究和学习,那么为什么会这样?说白了还是因为没有确定目标或者目标不明确,没有目标或者目标不明确都可能导致行动的失败。 - -那么问题来了,目标是啥?就我而言,短期目标是深入研究某一项技术,比如最近在研究mysql,那么深入研究一定要动手实践并且有所产出,这就够了么?还需要我们能够举一反三,结合实际开发场景想一想日常开发要注意什么,这中间有没有什么坑?可以看出,要进步真的不是一件简单的事,这种反人类的行为需要我们克服自我的弱点,逐渐形成习惯。真正牛逼的人,从不觉得认真学习是一件多么难的事,因为这已经形成了他的习惯,就喝早上起床刷牙洗脸那么自然简单。 - -扯了那么多,开始进入正题,先后进行了蚂蚁、拼多多和字节跳动的面试。 - -## 准备过程 - -先说说我自己的情况,我2016先在蚂蚁实习了将近三个月,然后去了我现在的老东家,2.5年工作经验,可以说毕业后就一直老老实实在老东家打怪升级,虽说有蚂蚁的实习经历,但是因为时间太短,还是有点虚的。所以面试官看到我简历第一个问题绝对是这样的。 - -“哇,你在蚂蚁待过,不错啊”,面试官笑嘻嘻地问到。“是的,还好”,我说。“为啥才三个月?”,面试官脸色一沉问到。“哗啦啦解释一通。。。”,我解释道。“哦,原来如此,那我们开始面试吧”,面试官一本正经说到。 - -尼玛,早知道不写蚂蚁的实习经历了,后面仔细一想,当初写上蚂蚁不就给简历加点料嘛。 - -言归正传,准备过程其实很早开始了(当然这不是说我工作时老想着跳槽,因为我明白现在的老东家并不是终点,我还需要不断提升),具体可追溯到从蚂蚁离职的时候,当时出来也面了很多公司,没啥大公司,面了大概5家公司,都拿到offer了。 - -工作之余常常会去额外研究自己感兴趣的技术以及工作用到的技术,力求把原理搞明白,并且会自己实践一把。此外,买了N多书,基本有时间就会去看,补补基础,什么操作系统、数据结构与算法、MySQL、JDK之类的源码,基本都好好温习了(文末会列一下自己看过的书和一些好的资料)。**我深知基础就像“木桶效应”的短板,决定了能装多少水。** - -此外,在正式决定看机会之前,我给自己列了一个提纲,主要包括Java要掌握的核心要点,有不懂的就查资料搞懂。我给自己定位还是Java工程师,所以Java体系是一定要做到心中有数的,很多东西没有常年的积累面试的时候很容易露馅,学习要对得起自己,不要骗人。 - -剩下的就是找平台和内推了,除了蚂蚁,头条和拼多多都是找人内推的,感谢蚂蚁面试官对我的欣赏,以后说不定会去蚂蚁咯😄。 - -平台:脉脉、GitHub、v2 - -## 蚂蚁金服 - -![img](https://mmbiz.qpic.cn/mmbiz_jpg/zsXjkGNcic53JMPc0FUw1lBXl5iaibrEXvt9qal7lJSgfGJ8mq00yE1J4UQ9H1oo9t6RAL4T3whhx17TYlj1mjlXA/?wx_fmt=jpeg) - -- 一面 -- 二面 -- 三面 -- 四面 -- 五面 -- 小结 - -### 一面 - -一面就做了一道算法题,要求两小时内完成,给了长度为N的有重复元素的数组,要求输出第10大的数。典型的TopK问题,快排算法搞定。 - -算法题要注意的是合法性校验、边界条件以及异常的处理。另外,如果要写测试用例,一定要保证测试覆盖场景尽可能全。加上平时刷刷算法题,这种考核应该没问题的。 - -### 二面 - -- 自我介绍下呗 -- 开源项目贡献过代码么?(Dubbo提过一个打印accesslog的bug算么) -- 目前在部门做什么,业务简单介绍下,内部有哪些系统,作用和交互过程说下 -- Dubbo踩过哪些坑,分别是怎么解决的?(说了异常处理时业务异常捕获的问题,自定义了一个异常拦截器) -- 开始进入正题,说下你对线程安全的理解(多线程访问同一个对象,如果不需要考虑额外的同步,调用对象的行为就可以获得正确的结果就是线程安全) -- 事务有哪些特性?(ACID) -- 怎么理解原子性?(同一个事务下,多个操作要么成功要么失败,不存在部分成功或者部分失败的情况) -- 乐观锁和悲观锁的区别?(悲观锁假定会发生冲突,访问的时候都要先获得锁,保证同一个时刻只有线程获得锁,读读也会阻塞;乐观锁假设不会发生冲突,只有在提交操作的时候检查是否有冲突)这两种锁在Java和MySQL分别是怎么实现的?(Java乐观锁通过CAS实现,悲观锁通过synchronize实现。mysql乐观锁通过MVCC,也就是版本实现,悲观锁可以通过select... for update加上排它锁) -- HashMap为什么不是线程安全的?(多线程操作无并发控制,顺便说了在扩容的时候多线程访问时会造成死锁,会形成一个环,不过扩容时多线程操作形成环的问题再JDK1.8已经解决,但多线程下使用HashMap还会有一些其他问题比如数据丢失,所以多线程下不应该使用HashMap,而应该使用ConcurrentHashMap)怎么让HashMap变得线程安全?(Collections的synchronize方法包装一个线程安全的Map,或者直接用ConcurrentHashMap)两者的区别是什么?(前者直接在put和get方法加了synchronize同步,后者采用了分段锁以及CAS支持更高的并发) -- jdk1.8对ConcurrentHashMap做了哪些优化?(插入的时候如果数组元素使用了红黑树,取消了分段锁设计,synchronize替代了Lock锁)为什么这样优化?(避免冲突严重时链表多长,提高查询效率,时间复杂度从O(N)提高到O(logN)) -- redis主从机制了解么?怎么实现的? -- 有过GC调优的经历么?(有点虚,答得不是很好) -- 有什么想问的么? - -### 三面 - -- 简单自我介绍下 -- 监控系统怎么做的,分为哪些模块,模块之间怎么交互的?用的什么数据库?(MySQL)使用什么存储引擎,为什么使用InnnoDB?(支持事务、聚簇索引、MVCC) -- 订单表有做拆分么,怎么拆的?(垂直拆分和水平拆分) -- 水平拆分后查询过程描述下 -- 如果落到某个分片的数据很大怎么办?(按照某种规则,比如哈希取模、range,将单张表拆分为多张表) -- 哈希取模会有什么问题么?(有的,数据分布不均,扩容缩容相对复杂 ) -- 分库分表后怎么解决读写压力?(一主多从、多主多从) -- 拆分后主键怎么保证惟一?(UUID、Snowflake算法) -- Snowflake生成的ID是全局递增唯一么?(不是,只是全局唯一,单机递增) -- 怎么实现全局递增的唯一ID?(讲了TDDL的一次取一批ID,然后再本地慢慢分配的做法) -- Mysql的索引结构说下(说了B+树,B+树可以对叶子结点顺序查找,因为叶子结点存放了数据结点且有序) -- 主键索引和普通索引的区别(主键索引的叶子结点存放了整行记录,普通索引的叶子结点存放了主键ID,查询的时候需要做一次回表查询)一定要回表查询么?(不一定,当查询的字段刚好是索引的字段或者索引的一部分,就可以不用回表,这也是索引覆盖的原理) -- 你们系统目前的瓶颈在哪里? -- 你打算怎么优化?简要说下你的优化思路 -- 有什么想问我么? - -### 四面 - -- 介绍下自己 -- 为什么要做逆向? -- 怎么理解微服务? -- 服务治理怎么实现的?(说了限流、压测、监控等模块的实现) -- 这个不是中间件做的事么,为什么你们部门做?(当时没有单独的中间件团队,微服务刚搞不久,需要进行监控和性能优化) -- 说说Spring的生命周期吧 -- 说说GC的过程(说了young gc和full gc的触发条件和回收过程以及对象创建的过程) -- CMS GC有什么问题?(并发清除算法,浮动垃圾,短暂停顿) -- 怎么避免产生浮动垃圾?(记得有个VM参数设置可以让扫描新生代之前进行一次young gc,但是因为gc是虚拟机自动调度的,所以不保证一定执行。但是还有参数可以让虚拟机强制执行一次young gc) -- 强制young gc会有什么问题?(STW停顿时间变长) -- 知道G1么?(了解一点 ) -- 回收过程是怎么样的?(young gc、并发阶段、混合阶段、full gc,说了Remember Set) -- 你提到的Remember Set底层是怎么实现的? -- 有什么想问的么? - -### 五面 - -五面是HRBP面的,和我提前预约了时间,主要聊了之前在蚂蚁的实习经历、部门在做的事情、职业发展、福利待遇等。阿里面试官确实是具有一票否决权的,很看重你的价值观是否match,一般都比较喜欢皮实的候选人。HR面一定要诚实,不要说谎,只要你说谎HR都会去证实,直接cut了。 - -- 之前蚂蚁实习三个月怎么不留下来? -- 实习的时候主管是谁? -- 实习做了哪些事情?(尼玛这种也问?) -- 你对技术怎么看?平时使用什么技术栈?(阿里HR真的是既当爹又当妈,😂) -- 最近有在研究什么东西么 -- 你对SRE怎么看 -- 对待遇有什么预期么 - -最后HR还对我说目前稳定性保障部挺缺人的,希望我尽快回复。 - -### 小结 - -蚂蚁面试比较重视基础,所以Java那些基本功一定要扎实。蚂蚁的工作环境还是挺赞的,因为我面的是稳定性保障部门,还有许多单独的小组,什么三年1班,很有青春的感觉。面试官基本水平都比较高,基本都P7以上,除了基础还问了不少架构设计方面的问题,收获还是挺大的。 - -## 拼多多 - -![img](https://mmbiz.qpic.cn/mmbiz_jpg/zsXjkGNcic53JMPc0FUw1lBXl5iaibrEXvtsmoh9TdJcV0hwnrjtbWPdOacyj2uYe2qaI5jvlGIQHwYtknwnGTibbQ/?wx_fmt=jpeg) - -- 面试前 -- 一面 -- 二面 -- 三面 -- 小结 - -### 面试前 - -面完蚂蚁后,早就听闻拼多多这个独角兽,决定也去面一把。首先我在脉脉找了一个拼多多的HR,加了微信聊了下,发了简历便开始我的拼多多面试之旅。这里要非常感谢拼多多HR小姐姐,从面试内推到offer确认一直都在帮我,人真的很nice。 - -### 一面 - -- 为啥蚂蚁只待了三个月?没转正?(转正了,解释了一通。。。) -- Java中的HashMap、TreeMap解释下?(TreeMap红黑树,有序,HashMap无序,数组+链表) -- TreeMap查询写入的时间复杂度多少?(O(logN)) -- HashMap多线程有什么问题?(线程安全,死锁)怎么解决?( jdk1.8用了synchronize + CAS,扩容的时候通过CAS检查是否有修改,是则重试)重试会有什么问题么?(CAS(Compare And Swap)是比较和交换,不会导致线程阻塞,但是因为重试是通过自旋实现的,所以仍然会占用CPU时间,还有ABA的问题)怎么解决?(超时,限定自旋的次数,ABA可以通过原理变量AtomicStampedReference解决,原理利用版本号进行比较)超过重试次数如果仍然失败怎么办?(synchronize互斥锁) -- CAS和synchronize有什么区别?都用synchronize不行么?(CAS是乐观锁,不需要阻塞,硬件级别实现的原子性;synchronize会阻塞,JVM级别实现的原子性。使用场景不同,线程冲突严重时CAS会造成CPU压力过大,导致吞吐量下降,synchronize的原理是先自旋然后阻塞,线程冲突严重仍然有较高的吞吐量,因为线程都被阻塞了,不会占用CPU -) -- 如果要保证线程安全怎么办?(ConcurrentHashMap) -- ConcurrentHashMap怎么实现线程安全的?(分段锁) -- get需要加锁么,为什么?(不用,volatile关键字) -- volatile的作用是什么?(保证内存可见性) -- 底层怎么实现的?(说了主内存和工作内存,读写内存屏障,happen-before,并在纸上画了线程交互图) -- 在多核CPU下,可见性怎么保证?(思考了一会,总线嗅探技术) -- 聊项目,系统之间是怎么交互的? -- 系统并发多少,怎么优化? -- 给我一张纸,画了一个九方格,都填了数字,给一个M*N矩阵,从1开始逆时针打印这M*N个数,要求时间复杂度尽可能低(内心OS:之前貌似碰到过这题,最优解是怎么实现来着)思考中。。。 -- 可以先说下你的思路(想起来了,说了什么时候要变换方向的条件,向右、向下、向左、向上,依此循环) -- 有什么想问我的? - -### 二面 - -- 自我介绍下 -- 手上还有其他offer么?(拿了蚂蚁的offer) -- 部门组织结构是怎样的?(这轮不是技术面么,不过还是老老实实说了) -- 系统有哪些模块,每个模块用了哪些技术,数据怎么流转的?(面试官有点秃顶,一看级别就很高)给了我一张纸,我在上面简单画了下系统之间的流转情况 -- 链路追踪的信息是怎么传递的?(RpcContext的attachment,说了Span的结构:parentSpanId + curSpanId) -- SpanId怎么保证唯一性?(UUID,说了下内部的定制改动) -- RpcContext是在什么维度传递的?(线程) -- Dubbo的远程调用怎么实现的?(讲了读取配置、拼装url、创建Invoker、服务导出、服务注册以及消费者通过动态代理、filter、获取Invoker列表、负载均衡等过程(哗啦啦讲了10多分钟),我可以喝口水么) -- Spring的单例是怎么实现的?(单例注册表) -- 为什么要单独实现一个服务治理框架?(说了下内部刚搞微服务不久,主要对服务进行一些监控和性能优化) -- 谁主导的?内部还在使用么? -- 逆向有想过怎么做成通用么? -- 有什么想问的么? - -### 三面 - -二面老大面完后就直接HR面了,主要问了些职业发展、是否有其他offer、以及入职意向等问题,顺便说了下公司的福利待遇等,都比较常规啦。不过要说的是手上有其他offer或者大厂经历会有一定加分。 - -### 小结 - -拼多多的面试流程就简单许多,毕竟是一个成立三年多的公司。面试难度中规中矩,只要基础扎实应该不是问题。但不得不说工作强度很大,开始面试前HR就提前和我确认能否接受这样强度的工作,想来的老铁还是要做好准备 - -## 字节跳动 - -![img](https://mmbiz.qpic.cn/mmbiz_jpg/zsXjkGNcic53JMPc0FUw1lBXl5iaibrEXvtRoTSCMeUWramk7M4CekxE9ssH5DFGBxmDcw0x9hjzmbIGHVWenDK8w/?wx_fmt=jpeg) - -- 面试前 -- 一面 -- 二面 -- 小结 - -### 面试前 - -头条的面试是三家里最专业的,每次面试前有专门的HR和你约时间,确定OK后再进行面试。每次都是通过视频面试,因为都是之前都是电话面或现场面,所以视频面试还是有点不自然。也有人觉得视频面试体验很赞,当然萝卜青菜各有所爱。最坑的二面的时候对方面试官的网络老是掉线,最后很冤枉的挂了(当然有一些点答得不好也是原因之一)。所以还是有点遗憾的。 - -### 一面 - -- 先自我介绍下 -- 聊项目,逆向系统是什么意思 -- 聊项目,逆向系统用了哪些技术 -- 线程池的线程数怎么确定? -- 如果是IO操作为主怎么确定? -- 如果计算型操作又怎么确定? -- Redis熟悉么,了解哪些数据结构?(说了zset) zset底层怎么实现的?(跳表) -- 跳表的查询过程是怎么样的,查询和插入的时间复杂度?(说了先从第一层查找,不满足就下沉到第二层找,因为每一层都是有序的,写入和插入的时间复杂度都是O(logN)) -- 红黑树了解么,时间复杂度?(说了是N叉平衡树,O(logN)) -- 既然两个数据结构时间复杂度都是O(logN),zset为什么不用红黑树(跳表实现简单,踩坑成本低,红黑树每次插入都要通过旋转以维持平衡,实现复杂) -- 点了点头,说下Dubbo的原理?(说了服务注册与发布以及消费者调用的过程)踩过什么坑没有?(说了dubbo异常处理的和打印accesslog的问题) -- CAS了解么?(说了CAS的实现)还了解其他同步机制么?(说了synchronize以及两者的区别,一个乐观锁,一个悲观锁) -- 那我们做一道题吧,数组A,2*n个元素,n个奇数、n个偶数,设计一个算法,使得数组奇数下标位置放置的都是奇数,偶数下标位置放置的都是偶数 -- 先说下你的思路(从0下标开始遍历,如果是奇数下标判断该元素是否奇数,是则跳过,否则从该位置寻找下一个奇数) -- 下一个奇数?怎么找?(有点懵逼,思考中。。) -- 有思路么?(仍然是先遍历一次数组,并对下标进行判断,如果下标属性和该位置元素不匹配从当前下标的下一个遍历数组元素,然后替换) -- 你这样时间复杂度有点高,如果要求O(N)要怎么做(思考一会,答道“定义两个指针,分别从下标0和1开始遍历,遇见奇数位是是偶数和偶数位是奇数就停下,交换内容”) -- 时间差不多了,先到这吧。你有什么想问我的? - -### 二面 - -- 面试官和蔼很多,你先介绍下自己吧 -- 你对服务治理怎么理解的? -- 项目中的限流怎么实现的?(Guava ratelimiter,令牌桶算法) -- 具体怎么实现的?(要点是固定速率且令牌数有限) -- 如果突然很多线程同时请求令牌,有什么问题?(导致很多请求积压,线程阻塞) -- 怎么解决呢?(可以把积压的请求放到消息队列,然后异步处理) -- 如果不用消息队列怎么解决?(说了RateLimiter预消费的策略) -- 分布式追踪的上下文是怎么存储和传递的?(ThreadLocal + spanId,当前节点的spanId作为下个节点的父spanId) -- Dubbo的RpcContext是怎么传递的?(ThreadLocal)主线程的ThreadLocal怎么传递到线程池?(说了先在主线程通过ThreadLocal的get方法拿到上下文信息,在线程池创建新的ThreadLocal并把之前获取的上下文信息设置到ThreadLocal中。这里要注意的线程池创建的ThreadLocal要在finally中手动remove,不然会有内存泄漏的问题) -- 你说的内存泄漏具体是怎么产生的?(说了ThreadLocal的结构,主要分两种场景:主线程仍然对ThreadLocal有引用和主线程不存在对ThreadLocal的引用。第一种场景因为主线程仍然在运行,所以还是有对ThreadLocal的引用,那么ThreadLocal变量的引用和value是不会被回收的。第二种场景虽然主线程不存在对ThreadLocal的引用,且该引用是弱引用,所以会在gc的时候被回收,但是对用的value不是弱引用,不会被内存回收,仍然会造成内存泄漏) -- 线程池的线程是不是必须手动remove才可以回收value?(是的,因为线程池的核心线程是一直存在的,如果不清理,那么核心线程的threadLocals变量会一直持有ThreadLocal变量) -- 那你说的内存泄漏是指主线程还是线程池?(主线程 ) -- 可是主线程不是都退出了,引用的对象不应该会主动回收么?(面试官和内存泄漏杠上了),沉默了一会。。。 -- 那你说下SpringMVC不同用户登录的信息怎么保证线程安全的?(刚才解释的有点懵逼,一下没反应过来,居然回答成锁了。大脑有点晕了,此时已经一个小时过去了,感觉情况不妙。。。) -- 这个直接用ThreadLocal不就可以么,你见过SpringMVC有锁实现的代码么?(有点晕菜。。。) -- 我们聊聊mysql吧,说下索引结构(说了B+树) -- 为什么使用B+树?( 说了查询效率高,O(logN),可以充分利用磁盘预读的特性,多叉树,深度小,叶子结点有序且存储数据) -- 什么是索引覆盖?(忘记了。。。 ) -- Java为什么要设计双亲委派模型? -- 什么时候需要自定义类加载器? -- 我们做一道题吧,手写一个对象池 -- 有什么想问我的么?(感觉我很多点都没答好,是不是挂了(结果真的是) ) - -### 小结 - -头条的面试确实很专业,每次面试官会提前给你发一个视频链接,然后准点开始面试,而且考察的点都比较全。 - -面试官都有一个特点,会抓住一个值得深入的点或者你没说清楚的点深入下去直到你把这个点讲清楚,不然面试官会觉得你并没有真正理解。二面面试官给了我一点建议,研究技术的时候一定要去研究产生的背景,弄明白在什么场景解决什么特定的问题,其实很多技术内部都是相通的。很诚恳,还是很感谢这位面试官大大。 - -## 总结 - -从年前开始面试到头条面完大概一个多月的时间,真的有点身心俱疲的感觉。最后拿到了拼多多、蚂蚁的offer,还是蛮幸运的。头条的面试对我帮助很大,再次感谢面试官对我的诚恳建议,以及拼多多的HR对我的啰嗦的问题详细解答。 - -这里要说的是面试前要做好两件事:简历和自我介绍,简历要好好回顾下自己做的一些项目,然后挑几个亮点项目。自我介绍基本每轮面试都有,所以最好提前自己练习下,想好要讲哪些东西,分别怎么讲。此外,简历提到的技术一定是自己深入研究过的,没有深入研究也最好找点资料预热下,不打无准备的仗。 - -**这些年看过的书**: - -《Effective Java》、《现代操作系统》、《TCP/IP详解:卷一》、《代码整洁之道》、《重构》、《Java程序性能优化》、《Spring实战》、《Zookeeper》、《高性能MySQL》、《亿级网站架构核心技术》、《可伸缩服务架构》、《Java编程思想》 - -说实话这些书很多只看了一部分,我通常会带着问题看书,不然看着看着就睡着了,简直是催眠良药😅。 - - -最后,附一张自己面试前准备的脑图: - -链接:https://pan.baidu.com/s/1o2l1tuRakBEP0InKEh4Hzw 密码:300d - -全文完。 diff --git "a/docs/essential-content-for-interview/BATJrealInterviewExperience/5\351\235\242\351\230\277\351\207\214,\347\273\210\350\216\267offer.md" "b/docs/essential-content-for-interview/BATJrealInterviewExperience/5\351\235\242\351\230\277\351\207\214,\347\273\210\350\216\267offer.md" deleted file mode 100644 index 9efac14..0000000 --- "a/docs/essential-content-for-interview/BATJrealInterviewExperience/5\351\235\242\351\230\277\351\207\214,\347\273\210\350\216\267offer.md" +++ /dev/null @@ -1,96 +0,0 @@ -> 作者:ppxyn。本文来自读者投稿,同时也欢迎各位投稿,**对于不错的原创文章我根据你的选择给予现金(50-200)、付费专栏或者任选书籍进行奖励!所以,快提 pr 或者邮件的方式(邮件地址在主页)给我投稿吧!** 当然,我觉得奖励是次要的,最重要的是你可以从自己整理知识点的过程中学习到很多知识。 - -**目录** - - - -- [前言](#前言) -- [一面\(技术面\)](#一面技术面) -- [二面\(技术面\)](#二面技术面) -- [三面\(技术面\)](#三面技术面) -- [四面\(半个技术面\)](#四面半个技术面) -- [五面\(HR面\)](#五面hr面) -- [总结](#总结) - - - -### 前言 - -在接触 Java 之前我接触的比较多的是硬件方面,用的比较多的语言就是C和C++。到了大三我才正式选择 Java 方向,到目前为止使用Java到现在大概有一年多的时间,所以Java算不上很好。刚开始投递的时候,实习刚辞职,也没准备笔试面试,很多东西都忘记了。所以,刚开始我并没有直接就投递阿里,毕竟心里还是有一点点小害怕的。于是,我就先投递了几个不算大的公司来练手,就是想着刷刷经验而已或者说是练练手(ps:还是挺对不起那些公司的)。面了一个月其他公司后,我找了我实验室的学长内推我,后面就有了这5次面试。 - -下面简单的说一下我的这5次面试:4次技术面+1次HR面,希望我的经历能对你有所帮助。 - -### 一面(技术面) - -1. 自我介绍(主要讲自己会的技术细节,项目经验,经历那些就一语带过,后面面试官会问你的)。 -2. 聊聊项目(就是一个很普通的分布式商城,自己做了一些改进),让我画了整个项目的架构图,然后针对项目抛了一系列的提高性能的问题,还问了我做项目的过程中遇到了那些问题,如何解决的,差不读就这些吧。 -3. 可能是我前面说了我会数据库优化,然后面试官就开始问索引、事务隔离级别、悲观锁和乐观锁、索引、ACID、MVVC这些问题。 -4. 浏览器输入URL发生了什么? TCP和UDP区别? TCP如何保证传输可靠性? -5. 讲下跳表怎么实现的?哈夫曼编码是怎么回事?非递归且不用额外空间(不用栈),如何遍历二叉树 -6. 后面又问了很多JVM方面的问题,比如Java内存模型、常见的垃圾回收器、双亲委派模型这些 -7. 你有什么问题要问吗? - -### 二面(技术面) - -1. 自我介绍(主要讲自己会的技术细节,项目经验,经历那些就一语带过,后面面试官会问你的)。 -2. 操作系统的内存管理机制 -3. 进程和线程的区别 -4. 说下你对线程安全的理解 -5. volatile 有什么作用 ,sychronized和lock有什么区别 -6. ReentrantLock实现原理 -7. 用过CountDownLatch么?什么场景下用的? -8. AQS底层原理。 -9. 造成死锁的原因有哪些,如何预防? -10. 加锁会带来哪些性能问题。如何解决? -11. HashMap、ConcurrentHashMap源码。HashMap是线程安全的吗?Hashtable呢?ConcurrentHashMap有了解吗? -12. 是否可以实习? -13. 你有什么问题要问吗? - -### 三面(技术面) - -1. 有没有参加过 ACM 或者他竞赛,有没有拿过什么奖?( 我说我没参加过ACM,本科参加过数学建模竞赛,名次并不好,没拿过什么奖。面试官好像有点失望,然后我又赶紧补充说我和老师一起做过一个项目,目前已经投入使用。面试官还比较感兴趣,后面又和他聊了一下这个项目。) -2. 研究生期间,做过什么项目,发过论文吗?有什么成果吗? -3. 你觉得你有什么优点和缺点?你觉得你相比于那些比你更优秀的人欠缺什么? -4. 有读过什么源码吗?(我说我读过 Java 集合框架和 Netty 的,面试官说 Java 集合前几面一定问的差不多,就不问了,然后就问我 Netty的,我当时很慌啊!) -5. 介绍一下自己对 Netty 的认识,为什么要用。说说业务中,Netty 的使用场景。什么是TCP 粘包/拆包,解决办法。Netty线程模型。Dubbo 在使用 Netty 作为网络通讯时候是如何避免粘包与半包问题?讲讲Netty的零拷贝?巴拉巴拉问了好多,我记得有好几个我都没回答上来,心里想着凉凉了啊。 -6. 用到了那些开源技术、在开源领域做过贡献吗? -7. 常见的排序算法及其复杂度,现场写了快排。 -8. 红黑树,B树的一些问题。 -9. 讲讲算法及数据结构在实习项目中的用处。 -10. 自己的未来规划(就简单描述了一下自己未来的设想啊,说的还挺诚恳,面试官好像还挺满意的) -11. 你有什么问题要问吗? - -### 四面(半个技术面) - -三面面完当天,晚上9点接到面试电话,感觉像是部门或者项目主管。 这个和之前的面试不大相同,感觉面试官主要考察的是你解决问题的能力、学习能力和团队协作能力。 - -1. 让我讲一个自己觉得最不错的项目。然后就巴拉巴拉的聊,我记得主要是问了项目是如何进行协作的、遇到问题是如何解决的、与他人发生冲突是如何解决的这些。感觉聊了挺久。 -2. 出现 OOM 后你会怎么排查问题? -3. 自己平时是如何学习新技术的?除了 Java 还回去了解其他技术吗? -4. 上一段实习经历的收获。 -5. NginX如何做负载均衡、常见的负载均衡算法有哪些、一致性哈希的一致性是什么意思、一致性哈希是如何做哈希的 -6. 你有什么问题问我吗? -7. 还有一些其他的,想不起来了,感觉这一面不是偏向技术来问。 - -## 五面(HR面) - -1. 自我介绍(主要讲能突出自己的经历,会的编程技术一语带过)。 -2. 你觉得你有什么优点和缺点?如何克服这些缺点? -3. 说一件大学里你自己比较有成就感的一件事情,为此付出了那些努力。 -4. 你前面跟其他面试官讲过一些你做的项目吧?可以给我讲讲吗?你要考虑到我不是一个做技术的人,怎么让我也听得懂。项目中有什么问题,你怎么解决的?你最大的收获是什么? -5. 你目前有面试过其他公司吗?如果让你选,这些公司和阿里,你选哪个?(送分题,回答不好可能送命) -6. 你期望的工作地点是哪里? -7. 你有什么问题吗? - -### 总结 - -1. 可以看出面试官问我的很多问题都是比较常见的问题,所以记得一定要提前准备,还要深入准备,不要回答的太皮毛。很多时候一个问题可能会牵扯出很多问题,遇到不会的问题不要慌,冷静分析,如果你真的回答不上来,也不要担心自己是不是就要挂了,很可能这个问题本身就比较难。 -2. 表达能力和沟通能力太重要了,一定要提前练一下,我自身就是一个不太会说话的人,所以,面试前我对于自我介绍、项目介绍和一些常见问题都在脑子里练了好久,确保面试的时候能够很清晰和简洁的说出来。 -3. 等待面试的过程和面试的过程真的好熬人,那段时间我压力也比较大,好在我私下找到学长聊了很多,心情也好了很多。 -4. 面试之后及时总结,面的好的话,不要得意,尽快准备下一场面试吧! - -我觉得我还算是比较幸运的,最后也祝大家都能获得心仪的Offer。 - - - - diff --git "a/docs/essential-content-for-interview/BATJrealInterviewExperience/\350\232\202\350\232\201\351\207\221\346\234\215\345\256\236\344\271\240\347\224\237\351\235\242\347\273\217\346\200\273\347\273\223(\345\267\262\346\213\277\345\217\243\345\244\264offer).md" "b/docs/essential-content-for-interview/BATJrealInterviewExperience/\350\232\202\350\232\201\351\207\221\346\234\215\345\256\236\344\271\240\347\224\237\351\235\242\347\273\217\346\200\273\347\273\223(\345\267\262\346\213\277\345\217\243\345\244\264offer).md" deleted file mode 100644 index 2e2df23..0000000 --- "a/docs/essential-content-for-interview/BATJrealInterviewExperience/\350\232\202\350\232\201\351\207\221\346\234\215\345\256\236\344\271\240\347\224\237\351\235\242\347\273\217\346\200\273\347\273\223(\345\267\262\346\213\277\345\217\243\345\244\264offer).md" +++ /dev/null @@ -1,251 +0,0 @@ -本文来自 Anonymous 的投稿 ,JavaGuide 对原文进行了重新排版和一点完善。 - - - -- [一面 (37 分钟左右)](#一面-37-分钟左右) -- [二面 (33 分钟左右)](#二面-33-分钟左右) -- [三面 (46 分钟)](#三面-46-分钟) -- [HR 面](#hr-面) - - - -### 一面 (37 分钟左右) - -一面是上海的小哥打来的,3.12 号中午确认的内推,下午就打来约时间了,也是唯一一个约时间的面试官。约的晚上八点。紧张的一比,人生第一次面试就献给了阿里。 - -幸运的是一面的小哥特温柔。好像是个海归?口语中夹杂着英文。废话不多说,上干货: - -**面试官:** 先自我介绍下吧! - -**我:** 巴拉巴拉...。 - -> 关于自我介绍:从 HR 面、技术面到高管面/部门主管面,面试官一般会让你先自我介绍一下,所以好好准备自己的自我介绍真的非常重要。网上一般建议的是准备好两份自我介绍:一份对 HR 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。 - -**面试官:** 我看你简历上写你做了个秒杀系统?我们就从这个项目开始吧,先介绍下你的项目。 - -> 关于项目介绍:如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑: -> -> 1. 对项目整体设计的一个感受(面试官可能会让你画系统的架构图) -> 2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -> 3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -> 4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用 redis 做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 - -**我:** 我说了我是如何考虑它的需求(秒杀地址隐藏,记录订单,减库存),一开始简单的用 synchronized 锁住方法,出现了问题,后来乐观锁改进,又有瓶颈,再上缓存,出现了缓存雪崩,于是缓存预热,错开缓存失效时间。最后,发现先记录订单再减库存会减少行级锁等待时间。 - -> 一面面试官很耐心地听,并给了我一些指导,问了我乐观锁是怎么实现的,我说是基于 sql 语句,在减库存操作的 where 条件里加剩余库存数>0,他说这应该不算是一种乐观锁,应该先查库存,在减库存的时候判断当前库存是否与读到的库存一样(可这样不是多一次查询操作吗?不是很理解,不过我没有反驳,只是说理解您的意思。事实证明千万别怼面试官,即使你觉得他说的不对) - -**面试官:** 我缓存雪崩什么情况下会发生?如何避免? - -**我:** 当多个商品缓存同时失效时会雪崩,导致大量查询数据库。还有就是秒杀刚开始的时候缓存里没有数据。解决方案:缓存预热,错开缓存失效时间 - -**面试官:** 问我更新数据库的同时为什么不马上更新缓存,而是删除缓存? - -**我:** 因为考虑到更新数据库后更新缓存可能会因为多线程下导致写入脏数据(比如线程 A 先更新数据库成功,接下来要取更新缓存,接着线程 B 更新数据库,但 B 又更新了缓存,接着 B 的时间片用完了,线程 A 更新了缓存) - -逼逼了将近 30 分钟,面试官居然用周杰伦的语气对我说: - -![not bad](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3not-bad.jpg) - -我突然受宠若惊,连忙说谢谢,也正是因为第一次面试得到了面试官的肯定,才让我信心大增,二三面稳定发挥。 - -**面试官又曰:** 我看你还懂数据库是吧,答:略懂略懂。。。那我问个简单的吧! - -**我:** 因为这个问题太简单了,所以我忘记它是什么了。 - -**面试官:** 你还会啥数据库知识? - -**我:** 我一听,问的这么随意的吗。。。都让我选题了,我就说我了解索引,慢查询优化,巴拉巴拉 - -**面试官:** 等等,你说索引是吧,那你能说下索引的存储数据结构吗? - -**我:** 我心想这简单啊,我就说 B+树,还说了为什么用 B+树 - -**面试官:** 你简历上写的这个 J.U.C 包是什么啊?(他居然不知道 JUC) - -**我:** 就是 java 多线程的那个包啊。。。 - -**面试官:** 那你都了解里面的哪些东西呢? - -**我:** 哈哈哈!这可是我的强项,从 ConcurrentHashMap,ConcurrentLinkedQueue 说到 CountDownLatch,CyclicBarrier,又说到线程池,分别说了底层实现和项目中的应用。 - -**面试官:** 我觉得差不多了,那我再问个与技术无关的问题哈,虽然这个问题可能不应该我问,就是你是如何考虑你的项目架构的呢? - -**我:** 先用最简单的方式实现它,再去发掘系统的问题和瓶颈,于是查资料改进架构。。。 - -**面试官:** 好,那我给你介绍下我这边的情况吧 - -![chat-end](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3chat-end.jpg) - -**总结:** 一面可能是简历面吧,问的比较简单,我在讲项目中说出了我做项目时的学习历程和思考,赢得了面试官的好感,感觉他应该给我的评价很好。 - -### 二面 (33 分钟左右) - -然而开心了没一会,内推人问我面的怎么样啊?看我流程已经到大大 boss 那了。我一听二面不是主管吗???怎么直接跳了一面。于是瞬间慌了,赶紧(下床)学习准备二面。 - -隔了一天,3.14 的早上 10:56 分,杭州的大大 boss 给我打来了电话,卧槽我当时在上毛概课,万恶的毛概课每节课都点名,我还在最后一排不敢跑出去。于是接起电话来怂怂地说不好意思我在上课,晚上可以面试吗?大大 boss 看来很忙啊,跟我说晚上没时间啊,再说吧! - -于是又隔了一天,3.16 中午我收到了北京的电话,当时心里小失望,我的大大 boss 呢???接起电话来,就是一番狂轰乱炸。。。 - -第一步还是先自我介绍,这个就不多说了,提前准备好要说的重点就没问题! - -**面试官:** 我们还是从你的项目开始吧,说说你的秒杀系统。 - -**我:** 一面时的套路。。。我考虑到秒杀地址在开始前不应暴露给用户。。。 - -**面试官:** 等下啊,为什么要这样呢?暴露给用户会怎么样? - -**我:** 用户提前知道秒杀地址就可以写脚本来抢购了,这样不公平 - -**面试官:** 那比如说啊,我现在是个黑客,我在秒杀开始时写好了脚本,运行一万个线程获取秒杀地址,这样是不是也不公平呢? - -**我:** 我考虑到了这方面,于是我自己写了个 LRU 缓存(划重点,这么多好用的缓存我为啥不用偏要自己写?就是为了让面试官上钩问我是怎么写的,这样我就可以逼逼准备好的内容了!),用这个缓存存储请求的 ip 和用户名,一个 ip 和用户名只能同时透过 3 个请求。 - -**面试官:** 那我可不可以创建一个 ip 代理池和很多用户来抢购呢?假设我有很多手机号的账户。 - -**我:** 这就是在为难我胖虎啊,我说这种情况跟真实用户操作太像了。。。我没法区别,不过我觉得可以通过地理位置信息或者机器学习算法来做吧。。。 - -**面试官:** 好的这个问题就到这吧,你接着说 - -**我:** 我把生成订单和减库存两条 sql 语句放在一个事务里,都操作成功了则认为秒杀成功。 - -**面试官:** 等等,你这个订单表和商品库存表是在一个数据库的吧,那如果在不同的数据库中呢? - -**我:** 这面试官好变态啊,我只是个本科生?!?!我觉得应该要用分布式锁来实现吧。。。 - -**面试官:** 有没有更轻量级的做法? - -**我:** 不知道了。后来查资料发现可以用消息队列来实现。使用消息队列主要能带来两个好处:(1) 通过异步处理提高系统性能(削峰、减少响应所需时间);(2) 降低系统耦合性。关于消息队列的更多内容可以查看这篇文章: - -后来发现消息队列作用好大,于是现在在学手写一个消息队列。 - -**面试官:** 好的你接着说项目吧。 - -**我:** 我考虑到了缓存雪崩问题,于是。。。 - -**面试官:** 等等,你有没有考虑到一种情况,假如说你的缓存刚刚失效,大量流量就来查缓存,你的数据库会不会炸? - -**我:** 我不知道数据库会不会炸,反正我快炸了。当时说没考虑这么高的并发量,后来发现也是可以用消息队列来解决,对流量削峰填谷。 - -**面试官:** 好项目聊(怼)完了,我们来说说别的,操作系统了解吧,你能说说 NIO 吗? - -**我:** NIO 是。。。 - -**面试官:** 那你知道 NIO 的系统调用有哪些吗,具体是怎么实现的? - -**我:** 当时复习 NIO 的时候就知道是咋回事,不知道咋实现。最近在补这方面的知识,可见 NIO 还是很重要的! - -**面试官:** 说说进程切换时操作系统都会发生什么? - -**我:** 不如杀了我,我最讨厌操作系统了。简单说了下,可能不对,需要答案自行百度。 - -**面试官:** 说说线程池? - -**答:** 卧槽这我熟啊,把 Java 并发编程的艺术里讲的都说出来了,说了得有十分钟,自夸一波,毕竟这本书我看了五遍😂 - -**面试官:** 好问问计网吧如果设计一个聊天系统,应该用 TCP 还是 UDP?为什么 - -**我:** 当然是 TCP!原因如下: - -![TCP VS UDP](https://user-gold-cdn.xitu.io/2018/4/19/162db5e97e9a9e01?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -**面试官:** 好的,你有什么要问我的吗? - -**我:** 我还有下一次面试吗? - -**面试官:** 应该。应该有的,一周内吧。还告诉我居然转正前要实习三个月?wtf,一个大三满课的本科生让我如何在八月底前实习三个月? - -**我:** 面试官再见 - -![saygoodbye-smile](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3saygoodbye-smile.jpg) - -### 三面 (46 分钟) - -3.18 号,三面来了,这次又是那个大大 boss! - -第一步还是先自我介绍,这个就不多说了,提前准备好要说的重点就没问题! - -**面试官:** 聊聊你的项目? - -**我:** 经过二面的教训,我迅速学习了一下分布式的理论知识,并应用到了我的项目(吹牛逼)中。 - -**面试官:** 看你用到了 Spring 的事务机制,你能说下 Spring 的事务传播吗? - -**我:** 完了这个问题好像没准备,虽然之前刷知乎看到过。。。我就只说出来一条,面试官说其实这个有很多机制的,比如事务嵌套,内事务回滚外事务回滚都会有不同情况,你可以回去看看。 - -**面试官:** 说说你的分布式事务解决方案? - -**我:** 我叭叭的照着资料查到的解决方案说了一通,面试官怎么好像没大听懂??? - -> 阿里巴巴之前开源了一个分布式 Fescar(一种易于使用,高性能,基于 Java 的开源分布式事务解决方案),后来,Ant Financial 加入 Fescar,使其成为一个更加中立和开放的分布式交易社区,Fescar 重命名为 Seata。Github 地址: - -**面试官:** 好,我们聊聊其他项目,说说你这个 MapReduce 项目?MapReduce 原理了解过吗? - -**我:** 我叭叭地说了一通,面试官好像觉得这个项目太简单了。要不是没项目,我会把我的实验写上吗??? - -**面试官:** 你这个手写 BP 神经网络是干了啥? - -**我:** 这是我选修机器学习课程时的一个作业,我又对它进行了扩展。 - -**面试官:** 你能说说为什么调整权值时要沿着梯度下降的方向? - -**我:** 老大,你太厉害了,怎么什么都懂。我压根没准备这个项目。。。没想到会问,做过去好几个月了,加上当时一紧张就忘了,后来想起来大概是....。 - -**面试官:** 好我们问问基础知识吧,说说什么叫 xisuo? - -**我:**???xisuo,您说什么,不好意思我没听清。(这面试官有点口音。。。)就是 xisuo 啊!xisuo 你不知道吗?。。。尴尬了十几秒后我终于意识到,他在说死锁!!! - -**面试官:** 假如 A 账户给 B 账户转钱,会发生 xisuo 吗?能具体说说吗? - -**我:** 当时答的不好,后来发现面试官又是想问分布式,具体答案参考这个: - -**面试官:** 为什么不考研? - -**我:** 不喜欢学术氛围,巴拉巴拉。 - -**面试官:** 你有什么问题吗? - -**我:** 我还有下一面吗。。。面试官说让我等,一周内答复。 - ------- - -等了十天,一度以为我凉了,内推人说我流程到 HR 了,让我等着吧可能 HR 太忙了,3.28 号 HR 打来了电话,当时在教室,我直接飞了出去。 - -### HR 面 - -**面试官:** 你好啊,先自我介绍下吧 - -**我:** 巴拉巴拉....HR 面的技术面试和技术面的还是有所区别的! - -面试官人特别好,一听就是很会说话的小姐姐!说我这里给你悄悄透露下,你的评级是 A 哦! - -![panghu-knowledge](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3panghu-knowledge.jpg) - -接下来就是几个经典 HR 面挂人的问题,什么难给我来什么,我看别人的 HR 面怎么都是聊聊天。。。 - -**面试官:** 你为什么选择支付宝呢,你怎么看待支付宝? - -**我:** 我从个人情怀,公司理念,环境氛围,市场价值,趋势导向分析了一波(说白了就是疯狂夸支付宝,不过说实话我说的那些一点都没撒谎,阿里确实做到了。比如我举了个雷军和格力打赌 5 年 2000 亿销售额,大部分企业家关注的是利益,而马云更关注的是真的为人类为世界做一些事情,利益不是第一位的。) - -**面试官:** 明白了解,那你的优点我们都很明了了,你能说说你的缺点吗? - -> 缺点肯定不能是目标岗位需要的关键能力!!! -> -> 总之,记住一点,面试官问你这个问题的话,你可以说一些不影响你这个职位工作需要的一些缺点。比如你面试后端工程师,面试官问你的缺点是什么的话,你可以这样说:自己比较内向,平时不太爱与人交流,但是考虑到以后可能要和客户沟通,自己正在努力改。 - -**我:** 据说这是 HR 面最难的一个问题。。。我当时翻了好几天的知乎才找到一个合适的,也符合我的答案:我有时候会表现的不太自信,比如阿里的内推二月份就开始了,其实我当时已经复习了很久了,但是老是觉得自己还不行,不敢投简历,于是又把书看了一遍才投的,当时也是舍友怂恿一波才投的,面了之后发现其实自己也没有很差。(划重点,一定要把自己的缺点圆回来)。 - -**面试官:** HR 好像不太满意我的答案,继续问我还有缺点吗? - -**我:** 我说比较容易紧张吧,举了自己大一面实验室因为紧张没进去的例子,后来不断调整心态,现在已经好很多了。 - -接下来又是个好难的问题。 - -**面试官:** BAT 都给你 offer 了,你怎么选? - -其实我当时好想说,BT 是什么?不好意思我只知道阿里。 - -**我 :** 哈哈哈哈开玩笑,就说了阿里的文化,支付宝给我们带来很多便利,想加入支付宝为人类做贡献! - -最后 HR 问了我实习时间,现在大几之类的问题,说肯定会给我发 offer 的,让我等着就好了,希望过两天能收到好的结果。 - -![mengbi](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3mengbi.jpg) diff --git a/docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md b/docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md deleted file mode 100644 index 835b6a5..0000000 --- a/docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md +++ /dev/null @@ -1,89 +0,0 @@ -昨天我整理了公众号历史所有和面试相关的我觉得还不错的文章:[整理了一些有助于你拿Offer的文章]() 。今天分享一下最近逛Github看到了一些我觉得对于Java面试以及学习有帮助的仓库,这些仓库涉及Java核心知识点整理、Java常见面试题、算法、基础知识点比如网络和操作系统等等。 - -## 知识点相关 - -### 1.JavaGuide - -- Github地址: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- star: 64.0k -- 介绍: 【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。 - -### 2.CS-Notes - -- Github 地址: -- Star: 68.3k -- 介绍: 技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。 - -### 3. advanced-java - -- Github地址:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- star: 23.4k -- 介绍: 互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务等领域知识,后端同学必看,前端同学也可学习。 - -### 4.JCSprout - -- Github地址:[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) -- star: 21.2k -- 介绍: Java Core Sprout:处于萌芽阶段的 Java 核心知识库。 - -### 5.toBeTopJavaer - -- Github地址:[https://github.com/hollischuang/toBeTopJavaer](https://github.com/hollischuang/toBeTopJavaer) -- star: 4.0 k -- 介绍: Java工程师成神之路。 - -### 6.architect-awesome - -- Github地址:[https://github.com/xingshaocheng/architect-awesome](https://github.com/xingshaocheng/architect-awesome) -- star: 34.4 k -- 介绍:后端架构师技术图谱。 - -### 7.technology-talk - -- Github地址: [https://github.com/aalansehaiyang/technology-talk](https://github.com/aalansehaiyang/technology-talk) -- star: 6.1k -- 介绍: 汇总java生态圈常用技术框架、开源中间件,系统架构、项目管理、经典架构案例、数据库、常用三方库、线上运维等知识。 - -### 8.fullstack-tutorial - -- Github地址: [https://github.com/frank-lam/fullstack-tutorial](https://github.com/frank-lam/fullstack-tutorial) -- star: 4.0k -- 介绍: fullstack tutorial 2019,后台技术栈/架构师之路/全栈开发社区,春招/秋招/校招/面试。 - -### 9.3y - -- Github地址:[https://github.com/ZhongFuCheng3y/3y](https://github.com/ZhongFuCheng3y/3y) -- star: 1.9 k -- 介绍: Java 知识整合。 - -### 10.java-bible - -- Github地址:[https://github.com/biezhi/java-bible](https://github.com/biezhi/java-bible) -- star: 2.3k -- 介绍: 这里记录了一些技术摘要,部分文章来自网络,本项目的目的力求分享精品技术干货,以Java为主。 - -### 11.interviews - -- Github地址: [https://github.com/kdn251/interviews/blob/master/README-zh-cn.md](https://github.com/kdn251/interviews/blob/master/README-zh-cn.md) -- star: 35.3k -- 介绍: 软件工程技术面试个人指南(国外的一个项目,虽然有翻译版,但是不太推荐,因为很多内容并不适用于国内)。 - -## 算法相关 - -### 1.LeetCodeAnimation - -- Github 地址: -- Star: 33.4k -- 介绍: Demonstrate all the questions on LeetCode in the form of animation.(用动画的形式呈现解LeetCode题目的思路)。 - -### 2.awesome-java-leetcode - -- Github地址:[https://github.com/Blankj/awesome-java-leetcode](https://github.com/Blankj/awesome-java-leetcode) -- star: 6.1k -- 介绍: LeetCode 上 Facebook 的面试题目。 - -### 3.leetcode - -- Github地址:[https://github.com/azl397985856/leetcode](https://github.com/azl397985856/leetcode) -- star: 12.0k -- 介绍: LeetCode Solutions: A Record of My Problem Solving Journey.( leetcode题解,记录自己的leetcode解题之路。) \ No newline at end of file diff --git a/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md b/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md deleted file mode 100644 index d515693..0000000 --- a/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md +++ /dev/null @@ -1,81 +0,0 @@ -  身边的朋友或者公众号的粉丝很多人都向我询问过:“我是双非/三本/专科学校的,我有机会进入大厂吗?”、“非计算机专业的学生能学好吗?”、“如何学习Java?”、“Java学习该学哪些东西?”、“我该如何准备Java面试?”......这些方面的问题。我会根据自己的一点经验对大部分人关心的这些问题进行答疑解惑。现在又刚好赶上考研结束,这篇文章也算是给考研结束准备往Java后端方向发展的朋友们指明一条学习之路。道理懂了如果没有实际行动,那这篇文章对你或许没有任何意义。 - -### Question1:我是双非/三本/专科学校的,我有机会进入大厂吗? - -  我自己也是非985非211学校的,结合自己的经历以及一些朋友的经历,我觉得让我回答这个问题再好不过。 - -  首先,我觉得学校歧视很正常,真的太正常了,如果要抱怨的话,你只能抱怨自己没有进入名校。但是,千万不要动不动说自己学校差,动不动拿自己学校当做自己进不了大厂的借口,学历只是筛选简历的很多标准中的一个而已,如果你够优秀,简历够丰富,你也一样可以和名校同学一起同台竞争。 - -  企业HR肯定是更喜欢高学历的人,毕竟985、211优秀人才比例肯定比普通学校高很多,HR团队肯定会优先在这些学校里选。这就好比相亲,你是愿意在很多优秀的人中选一个优秀的,还是愿意在很多普通的人中选一个优秀的呢? -   -  双非本科甚至是二本、三本甚至是专科的同学也有很多进入大厂的,不过比率相比于名校的低很多而已。从大厂招聘的结果上看,高学历人才的数量占据大头,那些成功进入BAT、美团,京东,网易等大厂的双非本科甚至是二本、三本甚至是专科的同学往往是因为具备丰富的项目经历或者在某个含金量比较高的竞赛比如ACM中取得了不错的成绩。**一部分学历不突出但能力出众的面试者能够进入大厂并不是说明学历不重要,而是学历的软肋能够通过其他的优势来弥补。** 所以,如果你的学校不够好而你自己又想去大厂的话,建议你可以从这几点来做:**①尽量在面试前最好有一个可以拿的出手的项目;②有实习条件的话,尽早出去实习,实习经历也会是你的简历的一个亮点(有能力在大厂实习最佳!);③参加一些含金量比较高的比赛,拿不拿得到名次没关系,重在锻炼。** - - -### Question2:非计算机专业的学生能学好Java后台吗?我能进大厂吗? - -  当然可以!现在非科班的程序员很多,很大一部分原因是互联网行业的工资比较高。我们学校外面的培训班里面90%都是非科班,我觉得他们很多人学的都还不错。另外,我的一个朋友本科是机械专业,大一开始自学安卓,技术贼溜,在我看来他比大部分本科是计算机的同学学的还要好。参考Question1的回答,即使你是非科班程序员,如果你想进入大厂的话,你也可以通过自己的其他优势来弥补。 - -  我觉得我们不应该因为自己的专业给自己划界限或者贴标签,说实话,很多科班的同学可能并不如你,你以为科班的同学就会认真听讲吗?还不是几乎全靠自己课下自学!不过如果你是非科班的话,你想要学好,那么注定就要舍弃自己本专业的一些学习时间,这是无可厚非的。 - -  建议非科班的同学,首先要打好计算机基础知识基础:①计算机网络、②操作系统、③数据机构与算法,我个人觉得这3个对你最重要。这些东西就像是内功,对你以后的长远发展非常有用。当然,如果你想要进大厂的话,这些知识也是一定会被问到的。另外,“一定学好数据结构与算法!一定学好数据结构与算法!一定学好数据结构与算法!”,重要的东西说3遍。 - - - -### Question3: 我没有实习经历的话找工作是不是特别艰难? - -  没有实习经历没关系,只要你有拿得出手的项目或者大赛经历的话,你依然有可能拿到大厂的 offer 。笔主当时找工作的时候就没有实习经历以及大赛获奖经历,单纯就是凭借自己的项目经验撑起了整个面试。 - -  如果你既没有实习经历,又没有拿得出手的项目或者大赛经历的话,我觉得在简历关,除非你有其他特别的亮点,不然,你应该就会被刷。 - -### Question4: 我该如何准备面试呢?面试的注意事项有哪些呢? - -下面是我总结的一些准备面试的Tips以及面试必备的注意事项: - -1. **准备一份自己的自我介绍,面试的时候根据面试对象适当进行修改**(突出重点,突出自己的优势在哪里,切忌流水账); -2. **注意随身带上自己的成绩单和简历复印件;** (有的公司在面试前都会让你交一份成绩单和简历当做面试中的参考。) -3. **如果需要笔试就提前刷一些笔试题,大部分在线笔试的类型是选择题+编程题,有的还会有简答题。**(平时空闲时间多的可以刷一下笔试题目(牛客网上有很多),但是不要只刷面试题,不动手code,程序员不是为了考试而存在的。)另外,注意抓重点,因为题目太多了,但是有很多题目几乎次次遇到,像这样的题目一定要搞定。 -4. **提前准备技术面试。** 搞清楚自己面试中可能涉及哪些知识点、哪些知识点是重点。面试中哪些问题会被经常问到、自己该如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!) -5. **面试之前做好定向复习。** 也就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你要面试的公司的面经。 -6. **准备好自己的项目介绍。** 如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑:①对项目整体设计的一个感受(面试官可能会让你画系统的架构图);②在这个项目中你负责了什么、做了什么、担任了什么角色;③ 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用;④项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用 redis 做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 -7. **面试之后记得复盘。** 面试遭遇失败是很正常的事情,所以善于总结自己的失败原因才是最重要的。如果失败,不要灰心;如果通过,切勿狂喜。 - - -**一些还算不错的 Java面试/学习相关的仓库,相信对大家准备面试一定有帮助:**[盘点一下Github上开源的Java面试/学习相关的仓库,看完弄懂薪资至少增加10k](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484817&idx=1&sn=12f0c254a240c40c2ccab8314653216b&chksm=fd9853f0caefdae6d191e6bf085d44ab9c73f165e3323aa0362d830e420ccbfad93aa5901021&token=766994974&lang=zh_CN#rd) - -### Question5: 我该自学还是报培训班呢? - -  我本人更加赞同自学(你要知道去了公司可没人手把手教你了,而且几乎所有的公司都对培训班出生的有偏见。为什么有偏见,你学个东西还要去培训班,说明什么,同等水平下,你的自学能力以及自律能力一定是比不上自学的人的)。但是如果,你连每天在寝室坚持学上8个小时以上都坚持不了,或者总是容易半途而废的话,我还是推荐你去培训班。观望身边同学去培训班的,大多是非计算机专业或者是没有自律能力以及自学能力非常差的人。 - -  另外,如果自律能力不行,你也可以通过结伴学习、参加老师的项目等方式来督促自己学习。 - -  总结:去不去培训班主要还是看自己,如果自己能坚持自学就自学,坚持不下来就去培训班。 - -### Question6: 没有项目经历/博客/Github开源项目怎么办? - -  从现在开始做! - -  网上有很多非常不错的项目视频,你就跟着一步一步做,不光要做,还要改进,改善。另外,如果你的老师有相关 Java 后台项目的话,你也可以主动申请参与进来。 - -  如果有自己的博客,也算是简历上的一个亮点。建议可以在掘金、Segmentfault、CSDN等技术交流社区写博客,当然,你也可以自己搭建一个博客(采用 Hexo+Githu Pages 搭建非常简单)。写一些什么?学习笔记、实战内容、读书笔记等等都可以。 - -  多用 Github,用好 Github,上传自己不错的项目,写好 readme 文档,在其他技术社区做好宣传。相信你也会收获一个不错的开源项目! - - -### Question7: 大厂到底青睐什么样的应届生? - -  从阿里、腾讯等大厂招聘官网对于Java后端方向/后端方向的应届实习生的要求,我们大概可以总结归纳出下面这 4 点能给简历增加很多分数: - -- 参加过竞赛(含金量超高的是ACM); -- 对数据结构与算法非常熟练; -- 参与过实际项目(比如学校网站); -- 参与过某个知名的开源项目或者自己的某个开源项目很不错; - -  除了我上面说的这三点,在面试Java工程师的时候,下面几点也提升你的个人竞争力: - -- 熟悉Python、Shell、Perl等脚本语言; -- 熟悉 Java 优化,JVM调优; -- 熟悉 SOA 模式; -- 熟悉自己所用框架的底层知识比如Spring; -- 了解分布式一些常见的理论; -- 具备高并发开发经验;大数据开发经验等等。 - diff --git a/docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md b/docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md deleted file mode 100644 index 1ae36a3..0000000 --- a/docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md +++ /dev/null @@ -1,88 +0,0 @@ -不论是校招还是社招都避免不了各种面试、笔试,如何去准备这些东西就显得格外重要。不论是笔试还是面试都是有章可循的,我这个“有章可循”说的意思只是说应对技术面试是可以提前准备。 我其实特别不喜欢那种临近考试就提前背啊记啊各种题的行为,非常反对!我觉得这种方法特别极端,而且在稍有一点经验的面试官面前是根本没有用的。建议大家还是一步一个脚印踏踏实实地走。 - - - -- [1 如何获取大厂面试机会?](#1-如何获取大厂面试机会) -- [2 面试前的准备](#2--面试前的准备) - - [2.1 准备自己的自我介绍](#21-准备自己的自我介绍) - - [2.2 关于着装](#22-关于着装) - - [2.3 随身带上自己的成绩单和简历](#23-随身带上自己的成绩单和简历) - - [2.4 如果需要笔试就提前刷一些笔试题](#24-如果需要笔试就提前刷一些笔试题) - - [2.5 花时间一些逻辑题](#25-花时间一些逻辑题) - - [2.6 准备好自己的项目介绍](#26-准备好自己的项目介绍) - - [2.7 提前准备技术面试](#27-提前准备技术面试) - - [2.7 面试之前做好定向复习](#27-面试之前做好定向复习) -- [3 面试之后复盘](#3-面试之后复盘) - - - -## 1 如何获取大厂面试机会? - -**在讲如何获取大厂面试机会之前,先来给大家科普/对比一下两个校招非常常见的概念——春招和秋招。** - -1. **招聘人数** :秋招多于春招 ; -2. **招聘时间** : 秋招一般7月左右开始,大概一直持续到10月底。但是大厂(如BAT)都会早开始早结束,所以一定要把握好时间。春招最佳时间为3月,次佳时间为4月,进入5月基本就不会再有春招了(金三银四)。 -3. **应聘难度** :秋招略大于春招; -4. **招聘公司:** 秋招数量多,而春招数量较少,一般为秋招的补充。 - -**综上,一般来说,秋招的含金量明显是高于春招的。** - -**下面我就说一下我自己知道的一些方法,不过应该也涵盖了大部分获取面试机会的方法。** - -1. **关注大厂官网,随时投递简历(走流程的网申);** -2. **线下参加宣讲会,直接投递简历;** -3. **找到师兄师姐/认识的人,帮忙内推(能够让你避开网申简历筛选,笔试筛选,还是挺不错的,不过也还是需要你的简历够棒);** -4. **博客发文被看中/Github优秀开源项目作者,大厂内部人员邀请你面试;** -5. **求职类网站投递简历(不是太推荐,适合海投);** - - -除了这些方法,我也遇到过这样的经历:有些大公司的一些部门可能暂时没招够人,然后如果你的亲戚或者朋友刚好在这个公司,而你正好又在寻求offer,那么面试机会基本上是有了,而且这种面试的难度好像一般还普遍比其他正规面试低很多。 - -## 2 面试前的准备 - -### 2.1 准备自己的自我介绍 - -从HR面、技术面到高管面/部门主管面,面试官一般会让你先自我介绍一下,所以好好准备自己的自我介绍真的非常重要。网上一般建议的是准备好两份自我介绍:一份对hr说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。 - -我这里简单分享一下我自己的自我介绍的一个简单的模板吧: - -> 面试官,您好!我叫某某。大学时间我主要利用课外时间学习某某。在校期间参与过一个某某系统的开发,另外,自己学习过程中也写过很多系统比如某某系统。在学习之余,我比较喜欢通过博客整理分享自己所学知识。我现在是某某社区的认证作者,写过某某很不错的文章。另外,我获得过某某奖,我的Github上开源的某个项目已经有多少Star了。 - -### 2.2 关于着装 - -穿西装、打领带、小皮鞋?NO!NO!NO!这是互联网公司面试又不是去走红毯,所以你只需要穿的简单大方就好,不需要太正式。 - -### 2.3 随身带上自己的成绩单和简历 - -有的公司在面试前都会让你交一份成绩单和简历当做面试中的参考。 - -### 2.4 如果需要笔试就提前刷一些笔试题 - -平时空闲时间多的可以刷一下笔试题目(牛客网上有很多)。但是不要只刷面试题,不动手code,程序员不是为了考试而存在的。 - -### 2.5 花时间一些逻辑题 - -面试中发现有些公司都有逻辑题测试环节,并且都把逻辑笔试成绩作为很重要的一个参考。 - -### 2.6 准备好自己的项目介绍 - -如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑: - -1. 对项目整体设计的一个感受(面试官可能会让你画系统的架构图) -2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 - -### 2.7 提前准备技术面试 - -搞清楚自己面试中可能涉及哪些知识点、哪些知识点是重点。面试中哪些问题会被经常问到、自己该如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!) - -### 2.7 面试之前做好定向复习 - -所谓定向复习就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你要面试的公司的面经。 - -举个栗子:在我面试 ThoughtWorks 的前几天我就在网上找了一些关于 ThoughtWorks 的技术面的一些文章。然后知道了 ThoughtWorks 的技术面会让我们在之前做的作业的基础上增加一个或两个功能,所以我提前一天就把我之前做的程序重新重构了一下。然后在技术面的时候,简单的改了几行代码之后写个测试就完事了。如果没有提前准备,我觉得 20 分钟我很大几率会完不成这项任务。 - -## 3 面试之后复盘 - -如果失败,不要灰心;如果通过,切勿狂喜。面试和工作实际上是两回事,可能很多面试未通过的人,工作能力比你强的多,反之亦然。我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油! diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\345\272\224\345\261\212\347\224\237\351\235\242\350\257\225\346\234\200\347\210\261\351\227\256\347\232\204\345\207\240\351\201\223Java\345\237\272\347\241\200\351\227\256\351\242\230.md" "b/docs/essential-content-for-interview/PreparingForInterview/\345\272\224\345\261\212\347\224\237\351\235\242\350\257\225\346\234\200\347\210\261\351\227\256\347\232\204\345\207\240\351\201\223Java\345\237\272\347\241\200\351\227\256\351\242\230.md" deleted file mode 100644 index 2e93113..0000000 --- "a/docs/essential-content-for-interview/PreparingForInterview/\345\272\224\345\261\212\347\224\237\351\235\242\350\257\225\346\234\200\347\210\261\351\227\256\347\232\204\345\207\240\351\201\223Java\345\237\272\347\241\200\351\227\256\351\242\230.md" +++ /dev/null @@ -1,743 +0,0 @@ - - -- [一 为什么 Java 中只有值传递?](#一-为什么-java-中只有值传递) -- [二 ==与 equals(重要)](#二-与-equals重要) -- [三 hashCode 与 equals(重要)](#三-hashcode-与-equals重要) - - [3.1 hashCode()介绍](#31-hashcode介绍) - - [3.2 为什么要有 hashCode](#32-为什么要有-hashcode) - - [3.3 hashCode()与 equals()的相关规定](#33-hashcode与-equals的相关规定) - - [3.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?](#34-为什么两个对象有相同的-hashcode-值它们也不一定是相等的) -- [四 String 和 StringBuffer、StringBuilder 的区别是什么?String 为什么是不可变的?](#四-string-和-stringbufferstringbuilder-的区别是什么string-为什么是不可变的) - - [String 为什么是不可变的吗?](#string-为什么是不可变的吗) - - [String 真的是不可变的吗?](#string-真的是不可变的吗) -- [五 什么是反射机制?反射机制的应用场景有哪些?](#五-什么是反射机制反射机制的应用场景有哪些) - - [5.1 反射机制介绍](#51-反射机制介绍) - - [5.2 静态编译和动态编译](#52-静态编译和动态编译) - - [5.3 反射机制优缺点](#53-反射机制优缺点) - - [5.4 反射的应用场景](#54-反射的应用场景) -- [六 什么是 JDK?什么是 JRE?什么是 JVM?三者之间的联系与区别](#六-什么是-jdk什么是-jre什么是-jvm三者之间的联系与区别) - - [6.1 JVM](#61-jvm) - - [6.2 JDK 和 JRE](#62-jdk-和-jre) -- [七 什么是字节码?采用字节码的最大好处是什么?](#七-什么是字节码采用字节码的最大好处是什么) -- [八 接口和抽象类的区别是什么?](#八-接口和抽象类的区别是什么) -- [九 重载和重写的区别](#九-重载和重写的区别) - - [重载](#重载) - - [重写](#重写) -- [十. Java 面向对象编程三大特性: 封装 继承 多态](#十-java-面向对象编程三大特性-封装-继承-多态) - - [封装](#封装) - - [继承](#继承) - - [多态](#多态) -- [十一. 什么是线程和进程?](#十一-什么是线程和进程) - - [11.1 何为进程?](#111-何为进程) - - [11.2 何为线程?](#112-何为线程) -- [十二. 请简要描述线程与进程的关系,区别及优缺点?](#十二-请简要描述线程与进程的关系区别及优缺点) - - [12.1 图解进程和线程的关系](#121-图解进程和线程的关系) - - [12.2 程序计数器为什么是私有的?](#122-程序计数器为什么是私有的) - - [12.3 虚拟机栈和本地方法栈为什么是私有的?](#123-虚拟机栈和本地方法栈为什么是私有的) - - [12.4 一句话简单了解堆和方法区](#124-一句话简单了解堆和方法区) -- [十三. 说说并发与并行的区别?](#十三-说说并发与并行的区别) -- [十四. 什么是上下文切换?](#十四-什么是上下文切换) -- [十五. 什么是线程死锁?如何避免死锁?](#十五-什么是线程死锁如何避免死锁) - - [15.1. 认识线程死锁](#151-认识线程死锁) - - [15.2 如何避免线程死锁?](#152-如何避免线程死锁) -- [十六. 说说 sleep() 方法和 wait() 方法区别和共同点?](#十六-说说-sleep-方法和-wait-方法区别和共同点) -- [十七. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?](#十七-为什么我们调用-start-方法时会执行-run-方法为什么我们不能直接调用-run-方法) -- [参考](#参考) - - - -## 一 为什么 Java 中只有值传递? - -首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。**按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。** 它用来描述各种程序设计语言(不只是 Java)中方法参数传递方式。 - -**Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。** - -**下面通过 3 个例子来给大家说明** - -> **example 1** - -```java -public static void main(String[] args) { - int num1 = 10; - int num2 = 20; - - swap(num1, num2); - - System.out.println("num1 = " + num1); - System.out.println("num2 = " + num2); -} - -public static void swap(int a, int b) { - int temp = a; - a = b; - b = temp; - - System.out.println("a = " + a); - System.out.println("b = " + b); -} -``` - -**结果:** - -``` -a = 20 -b = 10 -num1 = 10 -num2 = 20 -``` - -**解析:** - -![example 1 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/22191348.jpg) - -在 swap 方法中,a、b 的值进行交换,并不会影响到 num1、num2。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。 - -**通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example2.** - -> **example 2** - -```java - public static void main(String[] args) { - int[] arr = { 1, 2, 3, 4, 5 }; - System.out.println(arr[0]); - change(arr); - System.out.println(arr[0]); - } - - public static void change(int[] array) { - // 将数组的第一个元素变为0 - array[0] = 0; - } -``` - -**结果:** - -``` -1 -0 -``` - -**解析:** - -![example 2](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/3825204.jpg) - -array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的是同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。 - -**通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。** - -**很多程序设计语言(特别是,C++和 Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。** - -> **example 3** - -```java -public class Test { - - public static void main(String[] args) { - // TODO Auto-generated method stub - Student s1 = new Student("小张"); - Student s2 = new Student("小李"); - Test.swap(s1, s2); - System.out.println("s1:" + s1.getName()); - System.out.println("s2:" + s2.getName()); - } - - public static void swap(Student x, Student y) { - Student temp = x; - x = y; - y = temp; - System.out.println("x:" + x.getName()); - System.out.println("y:" + y.getName()); - } -} -``` - -**结果:** - -``` -x:小李 -y:小张 -s1:小张 -s2:小李 -``` - -**解析:** - -交换之前: - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/88729818.jpg) - -交换之后: - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/34384414.jpg) - -通过上面两张图可以很清晰的看出: **方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝** - -> **总结** - -Java 程序设计语言对对象采用的不是引用调用,实际上,对象引用是按 -值传递的。 - -下面再总结一下 Java 中方法参数的使用情况: - -- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。 -- 一个方法可以改变一个对象参数的状态。 -- 一个方法不能让对象参数引用一个新的对象。 - -**参考:** - -《Java 核心技术卷 Ⅰ》基础知识第十版第四章 4.5 小节 - -## 二 ==与 equals(重要) - -**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址) - -**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: - -- 情况 1:类没有覆盖 equals()方法。则通过 equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。 -- 情况 2:类覆盖了 equals()方法。一般,我们都覆盖 equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。 - -**举个例子:** - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -**说明:** - -- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -## 三 hashCode 与 equals(重要) - -面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?” - -### 3.1 hashCode()介绍 - -hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。 - -```java - /** - * Returns a hash code value for the object. This method is - * supported for the benefit of hash tables such as those provided by - * {@link java.util.HashMap}. - *

- * As much as is reasonably practical, the hashCode method defined by - * class {@code Object} does return distinct integers for distinct - * objects. (This is typically implemented by converting the internal - * address of the object into an integer, but this implementation - * technique is not required by the - * Java™ programming language.) - * - * @return a hash code value for this object. - * @see java.lang.Object#equals(java.lang.Object) - * @see java.lang.System#identityHashCode - */ - public native int hashCode(); -``` - -散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - -### 3.2 为什么要有 hashCode - -**我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** - -当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head fist java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -### 3.3 hashCode()与 equals()的相关规定 - -1. 如果两个对象相等,则 hashcode 一定也是相同的 -2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true -3. 两个对象有相同的 hashcode 值,它们也不一定是相等的 -4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** -5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) - -### 3.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的? - -在这里解释一位小伙伴的问题。以下内容摘自《Head Fisrt Java》。 - -因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。 - -我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。 - -## 四 String 和 StringBuffer、StringBuilder 的区别是什么?String 为什么是不可变的? - -**可变性** - -简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。 - -StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。 - -AbstractStringBuilder.java - -```java -abstract class AbstractStringBuilder implements Appendable, CharSequence { - char[] value; - int count; - AbstractStringBuilder() { - } - AbstractStringBuilder(int capacity) { - value = new char[capacity]; - } -``` - -**线程安全性** - -String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。 - -**性能** - -每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 - -**对于三者使用的总结:** - -1. 操作少量的数据: 适用 String -2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder -3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer - -#### String 为什么是不可变的吗? - -简单来说就是 String 类利用了 final 修饰的 char 类型数组存储字符,源码如下图所以: - -```java - /** The value is used for character storage. */ - private final char value[]; -``` - -#### String 真的是不可变的吗? - -我觉得如果别人问这个问题的话,回答不可变就可以了。 -下面只是给大家看两个有代表性的例子: - -**1) String 不可变但不代表引用不可以变** - -```java - String str = "Hello"; - str = str + " World"; - System.out.println("str=" + str); -``` - -结果: - -``` -str=Hello World -``` - -解析: - -实际上,原来 String 的内容是不变的,只是 str 由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。 - -**2) 通过反射是可以修改所谓的“不可变”对象** - -```java - // 创建字符串"Hello World", 并赋给引用s - String s = "Hello World"; - - System.out.println("s = " + s); // Hello World - - // 获取String类中的value字段 - Field valueFieldOfString = String.class.getDeclaredField("value"); - - // 改变value属性的访问权限 - valueFieldOfString.setAccessible(true); - - // 获取s对象上的value属性的值 - char[] value = (char[]) valueFieldOfString.get(s); - - // 改变value所引用的数组中的第5个字符 - value[5] = '_'; - - System.out.println("s = " + s); // Hello_World -``` - -结果: - -``` -s = Hello World -s = Hello_World -``` - -解析: - -用反射可以访问私有成员, 然后反射出 String 对象中的 value 属性, 进而改变通过获得的 value 引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。 - -## 五 什么是反射机制?反射机制的应用场景有哪些? - -### 5.1 反射机制介绍 - -JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。 - -### 5.2 静态编译和动态编译 - -- **静态编译:**在编译时确定类型,绑定对象 -- **动态编译:**运行时确定类型,绑定对象 - -### 5.3 反射机制优缺点 - -- **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。 -- **缺点:** 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。 - -### 5.4 反射的应用场景 - -**反射是框架设计的灵魂。** - -在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。 - -举例:① 我们在使用 JDBC 连接数据库时使用 `Class.forName()`通过反射加载数据库的驱动程序;②Spring 框架也用到很多反射机制,最经典的就是 xml 的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; -2)Java 类里面解析 xml 或 properties 里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的 Class 实例; 4)动态配置实例的属性 - -**推荐阅读:** - -- [Reflection:Java 反射机制的应用场景](https://segmentfault.com/a/1190000010162647?utm_source=tuicool&utm_medium=referral "Reflection:Java反射机制的应用场景") -- [Java 基础之—反射(非常重要)](https://blog.csdn.net/sinat_38259539/article/details/71799078 "Java基础之—反射(非常重要)") - -## 六 什么是 JDK?什么是 JRE?什么是 JVM?三者之间的联系与区别 - -### 6.1 JVM - -Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 - -**什么是字节码?采用字节码的好处是什么?** - -> 在 Java 中,JVM 可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 - -**Java 程序从源代码到运行一般有下面 3 步:** - -![Java程序运行过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E8%BF%87%E7%A8%8B.png) - -我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。 - -> HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。 - -**总结:** - -Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 - -### 6.2 JDK 和 JRE - -JDK 是 Java Development Kit,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。 - -JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。 - -如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。 - -## 七 什么是字节码?采用字节码的最大好处是什么? - -**先看下 java 中的编译器和解释器:** - -Java 中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在 Java 中,这种供虚拟机理解的代码叫做`字节码`(即扩展名为`.class`的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java 源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了 Java 的编译与解释并存的特点。 - -Java 源代码---->编译器---->jvm 可执行的 Java 字节码(即虚拟指令)---->jvm---->jvm 中解释器----->机器可执行的二进制机器码---->程序运行。 - -**采用字节码的好处:** - -Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同的计算机上运行。 - -## 八 接口和抽象类的区别是什么? - -1. 接口的方法默认是 public,所有方法在接口中不能有实现,抽象类可以有非抽象的方法 -2. 接口中的实例变量默认是 final 类型的,而抽象类中则不一定 -3. 一个类可以实现多个接口,但最多只能实现一个抽象类 -4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定 -5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。 - -注意:Java8 后接口可以有默认实现( default )。 - -## 九 重载和重写的区别 - -### 重载 - -发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 - -下面是《Java 核心技术》对重载这个概念的介绍: - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktopjava核心技术-重载.jpg) - -### 重写 - -重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。** - -## 十. Java 面向对象编程三大特性: 封装 继承 多态 - -### 封装 - -封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 - -### 继承 - -继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 - -**关于继承如下 3 点请记住:** - -1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 -2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 -3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 - -### 多态 - -所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 - -在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 - -## 十一. 什么是线程和进程? - -### 11.1 何为进程? - -进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 - -在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。 - -如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。 - -![进程示例图片-Windows](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/进程示例图片-Windows.png) - -### 11.2 何为线程? - -线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。 - -```java -public class MultiThread { - public static void main(String[] args) { - // 获取 Java 线程管理 MXBean - ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); - // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 - ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); - // 遍历线程信息,仅打印线程 ID 和线程名称信息 - for (ThreadInfo threadInfo : threadInfos) { - System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); - } - } -} -``` - -上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可): - -``` -[5] Attach Listener //添加事件 -[4] Signal Dispatcher // 分发处理给 JVM 信号的线程 -[3] Finalizer //调用对象 finalize 方法的线程 -[2] Reference Handler //清除 reference 线程 -[1] main //main 线程,程序入口 -``` - -从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。 - -## 十二. 请简要描述线程与进程的关系,区别及优缺点? - -**从 JVM 角度说进程和线程之间的关系** - -### 12.1 图解进程和线程的关系 - -下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md "《可能是把 Java 内存区域讲的最清楚的一篇文章》") - -

- -
- -从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 - -**总结:** 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反 - -下面是该知识点的扩展内容! - -下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢? - -### 12.2 程序计数器为什么是私有的? - -程序计数器主要有下面两个作用: - -1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 - -需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。 - -所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 - -### 12.3 虚拟机栈和本地方法栈为什么是私有的? - -- **虚拟机栈:** 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 -- **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 - -所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 - -### 12.4 一句话简单了解堆和方法区 - -堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 - -## 十三. 说说并发与并行的区别? - -- **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行); -- **并行:** 单位时间内,多个任务同时执行。 - -## 十四. 什么是上下文切换? - -多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 - -概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 - -上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 - -Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 - -## 十五. 什么是线程死锁?如何避免死锁? - -### 15.1. 认识线程死锁 - -多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 - -如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 - -![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4%E6%AD%BB%E9%94%811.png) - -下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》): - -```java -public class DeadLockDemo { - private static Object resource1 = new Object();//资源 1 - private static Object resource2 = new Object();//资源 2 - - public static void main(String[] args) { - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 1").start(); - - new Thread(() -> { - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource1"); - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - } - } - }, "线程 2").start(); - } -} -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 2,5,main]get resource2 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 2,5,main]waiting get resource1 -``` - -线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。 - -学过操作系统的朋友都知道产生死锁必须具备以下四个条件: - -1. 互斥条件:该资源任意一个时刻只由一个线程占用。 -2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 -3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 -4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 - -### 15.2 如何避免线程死锁? - -我们只要破坏产生死锁的四个条件中的其中一个就可以了。 - -**破坏互斥条件** - -这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 - -**破坏请求与保持条件** - -一次性申请所有的资源。 - -**破坏不剥夺条件** - -占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 - -**破坏循环等待条件** - -靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 - -我们对线程 2 的代码修改成下面这样就不会产生死锁了。 - -```java - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 2").start(); -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 1,5,main]get resource2 -Thread[线程 2,5,main]get resource1 -Thread[线程 2,5,main]waiting get resource2 -Thread[线程 2,5,main]get resource2 - -Process finished with exit code 0 -``` - -我们分析一下上面的代码为什么避免了死锁的发生? - -线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 - -## 十六. 说说 sleep() 方法和 wait() 方法区别和共同点? - -- 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁** 。 -- 两者都可以暂停线程的执行。 -- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 -- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。 - -## 十七. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? - -这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! - -new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 - -**总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。** - -## 参考 - -- [https://blog.csdn.net/zhzhao999/article/details/53449504](https://blog.csdn.net/zhzhao999/article/details/53449504 "https://blog.csdn.net/zhzhao999/article/details/53449504") -- [https://www.cnblogs.com/skywang12345/p/3324958.html](https://www.cnblogs.com/skywang12345/p/3324958.html "https://www.cnblogs.com/skywang12345/p/3324958.html") -- [https://www.cnblogs.com/Eason-S/p/5524837.html](https://www.cnblogs.com/Eason-S/p/5524837.html "https://www.cnblogs.com/Eason-S/p/5524837.html") diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\347\250\213\345\272\217\345\221\230\347\232\204\347\256\200\345\216\206\344\271\213\351\201\223.md" "b/docs/essential-content-for-interview/PreparingForInterview/\347\250\213\345\272\217\345\221\230\347\232\204\347\256\200\345\216\206\344\271\213\351\201\223.md" deleted file mode 100644 index 7feead7..0000000 --- "a/docs/essential-content-for-interview/PreparingForInterview/\347\250\213\345\272\217\345\221\230\347\232\204\347\256\200\345\216\206\344\271\213\351\201\223.md" +++ /dev/null @@ -1,121 +0,0 @@ - - -- [程序员简历就该这样写](#程序员简历就该这样写) - - [为什么说简历很重要?](#为什么说简历很重要) - - [先从面试前来说](#先从面试前来说) - - [再从面试中来说](#再从面试中来说) - - [下面这几点你必须知道](#下面这几点你必须知道) - - [必须了解的两大法则](#必须了解的两大法则) - - [STAR法则(Situation Task Action Result)](#star法则situation-task-action-result) - - [FAB 法则(Feature Advantage Benefit)](#fab-法则feature-advantage-benefit) - - [项目经历怎么写?](#项目经历怎么写) - - [专业技能该怎么写?](#专业技能该怎么写) - - [排版注意事项](#排版注意事项) - - [其他的一些小tips](#其他的一些小tips) - - [推荐的工具/网站](#推荐的工具网站) - - - -# 程序员简历就该这样写 - -本篇文章除了教大家用Markdown如何写一份程序员专属的简历,后面还会给大家推荐一些不错的用来写Markdown简历的软件或者网站,以及如何优雅的将Markdown格式转变为PDF格式或者其他格式。 - -推荐大家使用Markdown语法写简历,然后再将Markdown格式转换为PDF格式后进行简历投递。 - -如果你对Markdown语法不太了解的话,可以花半个小时简单看一下Markdown语法说明: http://www.markdown.cn 。 - -## 为什么说简历很重要? - -一份好的简历可以在整个申请面试以及面试过程中起到非常好的作用。 在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。为什么说简历很重要呢? - -### 先从面试前来说 - -- 假如你是网申,你的简历必然会经过HR的筛选,一张简历HR可能也就花费10秒钟看一下,然后HR就会决定你这一关是Fail还是Pass。 -- 假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。 - -另外,就算你通过了筛选,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。 - -所以,简历就像是我们的一个门面一样,它在很大程度上决定了你能否进入到下一轮的面试中。 - -### 再从面试中来说 - -我发现大家比较喜欢看面经 ,这点无可厚非,但是大部分面经都没告诉你很多问题都是在特定条件下才问的。举个简单的例子:一般情况下你的简历上注明你会的东西才会被问到(Java、数据结构、网络、算法这些基础是每个人必问的),比如写了你会 redis,那面试官就很大概率会问你 redis 的一些问题。比如:redis的常见数据类型及应用场景、redis是单线程为什么还这么快、 redis 和 memcached 的区别、redis 内存淘汰机制等等。 - -所以,首先,你要明确的一点是:**你不会的东西就不要写在简历上**。另外,**你要考虑你该如何才能让你的亮点在简历中凸显出来**,比如:你在某某项目做了什么事情解决了什么问题(只要有项目就一定有要解决的问题)、你的某一个项目里使用了什么技术后整体性能和并发量提升了很多等等。 - -面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是两回事,但是你要想要获得自己满意的 offer ,你自身的实力必须要强。 - -## 下面这几点你必须知道 - -1. 大部分公司的HR都说我们不看重学历(骗你的!),但是如果你的学校不出众的话,很难在一堆简历中脱颖而出,除非你的简历上有特别的亮点,比如:某某大厂的实习经历、获得了某某大赛的奖等等。 -2. **大部分应届生找工作的硬伤是没有工作经验或实习经历,所以如果你是应届生就不要错过秋招和春招。一旦错过,你后面就极大可能会面临社招,这个时候没有工作经验的你可能就会面临各种碰壁,导致找不到一个好的工作** -3. **写在简历上的东西一定要慎重,这是面试官大量提问的地方;** -4. **将自己的项目经历完美的展示出来非常重要。** - -## 必须了解的两大法则 - -### STAR法则(Situation Task Action Result) - -- **Situation:** 事情是在什么情况下发生; -- **Task::** 你是如何明确你的任务的; -- **Action:** 针对这样的情况分析,你采用了什么行动方式; -- **Result:** 结果怎样,在这样的情况下你学习到了什么。 - -简而言之,STAR法则,就是一种讲述自己故事的方式,或者说,是一个清晰、条理的作文模板。不管是什么,合理熟练运用此法则,可以轻松的对面试官描述事物的逻辑方式,表现出自己分析阐述问题的清晰性、条理性和逻辑性。 - -### FAB 法则(Feature Advantage Benefit) - -- **Feature:** 是什么; -- **Advantage:** 比别人好在哪些地方; -- **Benefit:** 如果雇佣你,招聘方会得到什么好处。 - -简单来说,这个法则主要是让你的面试官知道你的优势、招了你之后对公司有什么帮助。 - -## 项目经历怎么写? - -简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑从如下几点来写: - -1. 对项目整体设计的一个感受 -2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 - -## 专业技能该怎么写? - -先问一下你自己会什么,然后看看你意向的公司需要什么。一般HR可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。比如你可以这样写(下面这部分内容摘自我的简历,大家可以根据自己的情况做一些修改和完善): - -- 计算机网络、数据结构、算法、操作系统等课内基础知识:掌握 -- Java 基础知识:掌握 -- JVM 虚拟机(Java内存区域、虚拟机垃圾算法、虚拟垃圾收集器、JVM内存管理):掌握 -- 高并发、高可用、高性能系统开发:掌握 -- Struts2、Spring、Hibernate、Ajax、Mybatis、JQuery :掌握 -- SSH 整合、SSM 整合、 SOA 架构:掌握 -- Dubbo: 掌握 -- Zookeeper: 掌握 -- 常见消息队列: 掌握 -- Linux:掌握 -- MySQL常见优化手段:掌握 -- Spring Boot +Spring Cloud +Docker:了解 -- Hadoop 生态相关技术中的 HDFS、Storm、MapReduce、Hive、Hbase :了解 -- Python 基础、一些常见第三方库比如OpenCV、wxpy、wordcloud、matplotlib:熟悉 - -## 排版注意事项 - -1. 尽量简洁,不要太花里胡哨; -2. 一些技术名词不要弄错了大小写比如MySQL不要写成mysql,Java不要写成java。这个在我看来还是比较忌讳的,所以一定要注意这个细节; -3. 中文和数字英文之间加上空格的话看起来会舒服一点; - -## 其他的一些小tips - -1. 尽量避免主观表述,少一点语义模糊的形容词,尽量要简洁明了,逻辑结构清晰。 -2. 如果自己有博客或者个人技术栈点的话,写上去会为你加分很多。 -3. 如果自己的Github比较活跃的话,写上去也会为你加分很多。 -4. 注意简历真实性,一定不要写自己不会的东西,或者带有欺骗性的内容 -5. 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。 -6. 如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。 -7. 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你会很有礼貌。 - -## 推荐的工具/网站 - -- 冷熊简历(MarkDown在线简历工具,可在线预览、编辑和生成PDF): -- Typora+[Java程序员简历模板](https://github.com/geekcompany/ResumeSample/blob/master/java.md) diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" "b/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" deleted file mode 100644 index fcbc75d..0000000 --- "a/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" +++ /dev/null @@ -1,925 +0,0 @@ - - -- [一 基础篇](#一-基础篇) - - [1. `System.out.println(3|9)`输出什么?](#1-systemoutprintln39输出什么) - - [2. 说一下转发(Forward)和重定向(Redirect)的区别](#2-说一下转发forward和重定向redirect的区别) - - [3. 在浏览器中输入 url 地址到显示主页的过程,整个过程会使用哪些协议](#3-在浏览器中输入-url-地址到显示主页的过程整个过程会使用哪些协议) - - [4. TCP 三次握手和四次挥手](#4-tcp-三次握手和四次挥手) - - [为什么要三次握手](#为什么要三次握手) - - [为什么要传回 SYN](#为什么要传回-syn) - - [传了 SYN,为啥还要传 ACK](#传了-syn为啥还要传-ack) - - [为什么要四次挥手](#为什么要四次挥手) - - [5. IP 地址与 MAC 地址的区别](#5-ip-地址与-mac-地址的区别) - - [6. HTTP 请求,响应报文格式](#6-http-请求响应报文格式) - - [7. 为什么要使用索引?索引这么多优点,为什么不对表中的每一个列创建一个索引呢?索引是如何提高查询速度的?说一下使用索引的注意事项?Mysql 索引主要使用的两种数据结构?什么是覆盖索引?](#7-为什么要使用索引索引这么多优点为什么不对表中的每一个列创建一个索引呢索引是如何提高查询速度的说一下使用索引的注意事项mysql-索引主要使用的两种数据结构什么是覆盖索引) - - [8. 进程与线程的区别是什么?进程间的几种通信方式说一下?线程间的几种通信方式知道不?](#8-进程与线程的区别是什么进程间的几种通信方式说一下线程间的几种通信方式知道不) - - [9. 为什么要用单例模式?手写几种线程安全的单例模式?](#9-为什么要用单例模式手写几种线程安全的单例模式) - - [10. 简单介绍一下 bean;知道 Spring 的 bean 的作用域与生命周期吗?](#10-简单介绍一下-bean知道-spring-的-bean-的作用域与生命周期吗) - - [11. Spring 中的事务传播行为了解吗?TransactionDefinition 接口中哪五个表示隔离级别的常量?](#11-spring-中的事务传播行为了解吗transactiondefinition-接口中哪五个表示隔离级别的常量) - - [事务传播行为](#事务传播行为) - - [隔离级别](#隔离级别) - - [12. SpringMVC 原理了解吗?](#12-springmvc-原理了解吗) - - [13. Spring AOP IOC 实现原理](#13-spring-aop-ioc-实现原理) -- [二 进阶篇](#二-进阶篇) - - [1 消息队列 MQ 的套路](#1-消息队列-mq-的套路) - - [1.1 介绍一下消息队列 MQ 的应用场景/使用消息队列的好处](#11-介绍一下消息队列-mq-的应用场景使用消息队列的好处) - - [1)通过异步处理提高系统性能](#1通过异步处理提高系统性能) - - [2)降低系统耦合性](#2降低系统耦合性) - - [1.2 那么使用消息队列会带来什么问题?考虑过这些问题吗?](#12-那么使用消息队列会带来什么问题考虑过这些问题吗) - - [1.3 介绍一下你知道哪几种消息队列,该如何选择呢?](#13-介绍一下你知道哪几种消息队列该如何选择呢) - - [1.4 关于消息队列其他一些常见的问题展望](#14-关于消息队列其他一些常见的问题展望) - - [2 谈谈 InnoDB 和 MyIsam 两者的区别](#2-谈谈-innodb-和-myisam-两者的区别) - - [2.1 两者的对比](#21-两者的对比) - - [2.2 关于两者的总结](#22-关于两者的总结) - - [3 聊聊 Java 中的集合吧!](#3-聊聊-java-中的集合吧) - - [3.1 Arraylist 与 LinkedList 有什么不同?(注意加上从数据结构分析的内容)](#31-arraylist-与-linkedlist-有什么不同注意加上从数据结构分析的内容) - - [3.2 HashMap 的底层实现](#32-hashmap-的底层实现) - - [1)JDK1.8 之前](#1jdk18-之前) - - [2)JDK1.8 之后](#2jdk18-之后) - - [3.3 既然谈到了红黑树,你给我手绘一个出来吧,然后简单讲一下自己对于红黑树的理解](#33-既然谈到了红黑树你给我手绘一个出来吧然后简单讲一下自己对于红黑树的理解) - - [3.4 红黑树这么优秀,为何不直接使用红黑树得了?](#34-红黑树这么优秀为何不直接使用红黑树得了) - - [3.5 HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别](#35-hashmap-和-hashtable-的区别hashset-和-hashmap-区别) -- [三 终结篇](#三-终结篇) - - [1. Object 类有哪些方法?](#1-object-类有哪些方法) - - [1.1 Object 类的常见方法总结](#11-object-类的常见方法总结) - - [1.2 hashCode 与 equals](#12-hashcode-与-equals) - - [1.2.1 hashCode()介绍](#121-hashcode介绍) - - [1.2.2 为什么要有 hashCode](#122-为什么要有-hashcode) - - [1.2.3 hashCode()与 equals()的相关规定](#123-hashcode与-equals的相关规定) - - [1.2.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?](#124-为什么两个对象有相同的-hashcode-值它们也不一定是相等的) - - [1.3 ==与 equals](#13-与-equals) - - [2 ConcurrentHashMap 相关问题](#2-concurrenthashmap-相关问题) - - [2.1 ConcurrentHashMap 和 Hashtable 的区别](#21-concurrenthashmap-和-hashtable-的区别) - - [2.2 ConcurrentHashMap 线程安全的具体实现方式/底层具体实现](#22-concurrenthashmap-线程安全的具体实现方式底层具体实现) - - [JDK1.7(上面有示意图)](#jdk17上面有示意图) - - [JDK1.8(上面有示意图)](#jdk18上面有示意图) - - [3 谈谈 synchronized 和 ReentrantLock 的区别](#3-谈谈-synchronized-和-reentrantlock-的区别) - - [4 线程池了解吗?](#4-线程池了解吗) - - [4.1 为什么要用线程池?](#41-为什么要用线程池) - - [4.2 Java 提供了哪几种线程池?他们各自的使用场景是什么?](#42-java-提供了哪几种线程池他们各自的使用场景是什么) - - [Java 主要提供了下面 4 种线程池](#java-主要提供了下面-4-种线程池) - - [各种线程池的适用场景介绍](#各种线程池的适用场景介绍) - - [4.3 创建的线程池的方式](#43-创建的线程池的方式) - - [5 Nginx](#5-nginx) - - [5.1 简单介绍一下 Nginx](#51-简单介绍一下-nginx) - - [反向代理](#反向代理) - - [负载均衡](#负载均衡) - - [动静分离](#动静分离) - - [5.2 为什么要用 Nginx?](#52-为什么要用-nginx) - - [5.3 Nginx 的四个主要组成部分了解吗?](#53-nginx-的四个主要组成部分了解吗) - - - -这些问题是 2018 年去美团面试的同学被问到的一些常见的问题,希望对你有帮助! - -# 一 基础篇 - -## 1. `System.out.println(3|9)`输出什么? - -正确答案:11。 - -**考察知识点:&和&&;|和||** - -**&和&&:** - -共同点:两者都可做逻辑运算符。它们都表示运算符的两边都是 true 时,结果为 true; - -不同点: &也是位运算符。& 表示在运算时两边都会计算,然后再判断;&&表示先运算符号左边的东西,然后判断是否为 true,是 true 就继续运算右边的然后判断并输出,是 false 就停下来直接输出不会再运行后面的东西。 - -**|和||:** - -共同点:两者都可做逻辑运算符。它们都表示运算符的两边任意一边为 true,结果为 true,两边都不是 true,结果就为 false; - -不同点:|也是位运算符。| 表示两边都会运算,然后再判断结果;|| 表示先运算符号左边的东西,然后判断是否为 true,是 true 就停下来直接输出不会再运行后面的东西,是 false 就继续运算右边的然后判断并输出。 - -**回到本题:** - -3 | 9=0011(二进制) | 1001(二进制)=1011(二进制)=11(十进制) - -## 2. 说一下转发(Forward)和重定向(Redirect)的区别 - -**转发是服务器行为,重定向是客户端行为。** - -**转发(Forword)** 通过 RequestDispatcher 对象的`forward(HttpServletRequest request,HttpServletResponse response)`方法实现的。`RequestDispatcher` 可以通过`HttpServletRequest` 的 `getRequestDispatcher()`方法获得。例如下面的代码就是跳转到 login_success.jsp 页面。 - -```java -request.getRequestDispatcher("login_success.jsp").forward(request, response); -``` - -**重定向(Redirect)** 是利用服务器返回的状态码来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过 HttpServletRequestResponse 的 setStatus(int status)方法设置状态码。如果服务器返回 301 或者 302,则浏览器会到新的网址重新请求该资源。 - -1. **从地址栏显示来说**:forward 是服务器请求资源,服务器直接访问目标地址的 URL,把那个 URL 的响应内容读取过来,然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址。redirect 是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址。所以地址栏显示的是新的 URL。 -2. **从数据共享来说**:forward:转发页面和转发到的页面可以共享 request 里面的数据。redirect:不能共享数据。 -3. **从运用地方来说**:forward:一般用于用户登陆的时候,根据角色转发到相应的模块。redirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等。 -4. **从效率来说**:forward:高。redirect:低。 - -## 3. 在浏览器中输入 url 地址到显示主页的过程,整个过程会使用哪些协议 - -图片来源:《图解 HTTP》: - -![各种网络请求用到的协议](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/各种网络请求用到的协议.jpg) - -总体来说分为以下几个过程: - -1. DNS 解析 -2. TCP 连接 -3. 发送 HTTP 请求 -4. 服务器处理请求并返回 HTTP 报文 -5. 浏览器解析渲染页面 -6. 连接结束 - -具体可以参考下面这篇文章: - -- [https://segmentfault.com/a/1190000006879700](https://segmentfault.com/a/1190000006879700 "https://segmentfault.com/a/1190000006879700") - -> 修正 [issue-568](https://github.com/Snailclimb/JavaGuide/issues/568 "issue-568"):上图中 IP 数据包在路由器之间使用的协议为 OPSF 协议错误,应该为 OSPF 协议 。 -> -> IP 数据包在路由器之间传播大致分为 IGP 和 BGP 协议,而 IGP 目前主流为 OSPF 协议,思科,华为和 H3C 等主流厂商都有各自实现并使用;BGP 协议为不同 AS(自治系统号)间路由传输,也分为 I-BGP 和 E-BGP,详细资料请查看《TCP/IP 卷一》 - -## 4. TCP 三次握手和四次挥手 - -为了准确无误地把数据送达目标处,TCP 协议采用了三次握手策略。 - -**漫画图解:** - -图片来源:《图解 HTTP》 - - -**简单示意图:** - - -- 客户端–发送带有 SYN 标志的数据包–一次握手–服务端 -- 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端 -- 客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端 - -#### 为什么要三次握手 - -**三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。** - -第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常。 - -第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发送正常 - -第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常 - -所以三次握手就能确认双发收发功能都正常,缺一不可。 - -#### 为什么要传回 SYN - -接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。 - -> SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务器之间传递。 - -#### 传了 SYN,为啥还要传 ACK - -双方通信无误必须是两者互相发送信息都无误。传了 SYN,证明发送方(主动关闭方)到接收方(被动关闭方)的通道没有问题,但是接收方到发送方的通道还需要 ACK 信号来进行验证。 - -断开一个 TCP 连接则需要“四次挥手”: - -- 客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送 -- 服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加 1 。和 SYN 一样,一个 FIN 将占用一个序号 -- 服务器-关闭与客户端的连接,发送一个 FIN 给客户端 -- 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加 1 - -#### 为什么要四次挥手 - -任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。 - -举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B 回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。 - -上面讲的比较概括,推荐一篇讲的比较细致的文章:[https://blog.csdn.net/qzcsu/article/details/72861891](https://blog.csdn.net/qzcsu/article/details/72861891 "https://blog.csdn.net/qzcsu/article/details/72861891") - -## 5. IP 地址与 MAC 地址的区别 - -参考:[https://blog.csdn.net/guoweimelon/article/details/50858597](https://blog.csdn.net/guoweimelon/article/details/50858597 "https://blog.csdn.net/guoweimelon/article/details/50858597") - -IP 地址是指互联网协议地址(Internet Protocol Address)IP Address 的缩写。IP 地址是 IP 协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 - -MAC 地址又称为物理地址、硬件地址,用来定义网络设备的位置。网卡的物理地址通常是由网卡生产厂家写入网卡的,具有全球唯一性。MAC 地址用于在网络中唯一标示一个网卡,一台电脑会有一或多个网卡,每个网卡都需要有一个唯一的 MAC 地址。 - -## 6. HTTP 请求,响应报文格式 - -HTTP 请求报文主要由请求行、请求头部、请求正文 3 部分组成 - -HTTP 响应报文主要由状态行、响应头部、响应正文 3 部分组成 - -详细内容可以参考:[https://blog.csdn.net/a19881029/article/details/14002273](https://blog.csdn.net/a19881029/article/details/14002273 "https://blog.csdn.net/a19881029/article/details/14002273") - -## 7. 为什么要使用索引?索引这么多优点,为什么不对表中的每一个列创建一个索引呢?索引是如何提高查询速度的?说一下使用索引的注意事项?Mysql 索引主要使用的两种数据结构?什么是覆盖索引? - -**为什么要使用索引?** - -1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 -2. 可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。 -3. 帮助服务器避免排序和临时表 -4. 将随机 IO 变为顺序 IO -5. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。 - -**索引这么多优点,为什么不对表中的每一个列创建一个索引呢?** - -1. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。 -2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。 -3. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。 - -**索引是如何提高查询速度的?** - -将无序的数据变成相对有序的数据(就像查目录一样) - -**说一下使用索引的注意事项** - -1. 避免 where 子句中对字段施加函数,这会造成无法命中索引。 -2. 在使用 InnoDB 时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。 -3. 将打算加索引的列设置为 NOT NULL ,否则将导致引擎放弃使用索引而进行全表扫描 -4. 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用 -5. 在使用 limit offset 查询缓慢时,可以借助索引来提高性能 - -**Mysql 索引主要使用的哪两种数据结构?** - -- 哈希索引:对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择 BTree 索引。 -- BTree 索引:Mysql 的 BTree 索引使用的是 B 树中的 B+Tree。但对于主要的两种存储引擎(MyISAM 和 InnoDB)的实现方式是不同的。 - -更多关于索引的内容可以查看我的这篇文章:[【思维导图-索引篇】搞定数据库索引就是这么简单](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484486&idx=1&sn=215450f11e042bca8a58eac9f4a97686&chksm=fd985227caefdb3117b8375f150676f5824aa20d1ebfdbcfb93ff06e23e26efbafae6cf6b48e&token=1990180468&lang=zh_CN#rd) - -**什么是覆盖索引?** - -如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称 -之为“覆盖索引”。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! - -## 8. 进程与线程的区别是什么?进程间的几种通信方式说一下?线程间的几种通信方式知道不? - -**进程与线程的区别是什么?** - -线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。另外,也正是因为共享资源,所以线程中执行时一般都要进行同步和互斥。总的来说,进程和线程的主要差别在于它们是不同的操作系统资源管理方式。 - -**进程间的几种通信方式说一下?** - -1. **管道(pipe)**:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。管道分为 pipe(无名管道)和 fifo(命名管道)两种,有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间通信。 -2. **信号量(semophore)**:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 -3. **消息队列(message queue)**:消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。 -4. **信号(signal)**:信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。 -5. **共享内存(shared memory)**:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问,共享内存是最快的 IPC 方式,它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。 -6. **套接字(socket)**:socket,即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。 - -**线程间的几种通信方式知道不?** - -1、锁机制 - -- 互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。 -- 读写锁:允许多个线程同时读共享数据,而对写操作互斥。 -- 条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。 - -2、信号量机制:包括无名线程信号量与有名线程信号量 - -3、信号机制:类似于进程间的信号处理。 - -线程间通信的主要目的是用于线程同步,所以线程没有象进程通信中用于数据交换的通信机制。 - -## 9. 为什么要用单例模式?手写几种线程安全的单例模式? - -**简单来说使用单例模式可以带来下面几个好处:** - -- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销; -- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。 - -**懒汉式(双重检查加锁版本)** - -```java -public class Singleton { - - //volatile保证,当uniqueInstance变量被初始化成Singleton实例时,多个线程可以正确处理uniqueInstance变量 - private volatile static Singleton uniqueInstance; - private Singleton() { - } - public static Singleton getInstance() { - //检查实例,如果不存在,就进入同步代码块 - if (uniqueInstance == null) { - //只有第一次才彻底执行这里的代码 - synchronized(Singleton.class) { - //进入同步代码块后,再检查一次,如果仍是null,才创建实例 - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} -``` - -**静态内部类方式** - -静态内部实现的单例是懒加载的且线程安全。 - -只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance(只有第一次使用这个单例的实例的时候才加载,同时不会有线程安全问题)。 - -```java -public class Singleton { - private static class SingletonHolder { - private static final Singleton INSTANCE = new Singleton(); - } - private Singleton (){} - public static final Singleton getInstance() { - return SingletonHolder.INSTANCE; - } -} -``` - -## 10. 简单介绍一下 bean;知道 Spring 的 bean 的作用域与生命周期吗? - -在 Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 bean。简单地讲,bean 就是由 IOC 容器初始化、装配及管理的对象,除此之外,bean 就与应用程序中的其他对象没有什么区别了。而 bean 的定义以及 bean 相互间的依赖关系将通过配置元数据来描述。 - -Spring 中的 bean 默认都是单例的,这些单例 Bean 在多线程程序下如何保证线程安全呢? 例如对于 Web 应用来说,Web 容器对于每个用户请求都创建一个单独的 Sevlet 线程来处理请求,引入 Spring 框架之后,每个 Action 都是单例的,那么对于 Spring 托管的单例 Service Bean,如何保证其安全呢? Spring 的单例是基于 BeanFactory 也就是 Spring 容器的,单例 Bean 在此容器内只有一个,Java 的单例是基于 JVM,每个 JVM 内只有一个实例。 - -![pring的bean的作用域](https://user-gold-cdn.xitu.io/2018/11/10/166fd45773d5dd2e?w=563&h=299&f=webp&s=27930) - -Spring 的 bean 的生命周期以及更多内容可以查看:[一文轻松搞懂 Spring 中 bean 的作用域与生命周期](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484400&idx=2&sn=7201eb365102fce017f89cb3527fb0bc&chksm=fd985591caefdc872a2fac897288119f94c345e4e12150774f960bf5f816b79e4b9b46be3d7f&token=1990180468&lang=zh_CN#rd) - -## 11. Spring 中的事务传播行为了解吗?TransactionDefinition 接口中哪五个表示隔离级别的常量? - -#### 事务传播行为 - -事务传播行为(为了解决业务层方法之间互相调用的事务问题): -当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。在 TransactionDefinition 定义中包括了如下几个表示传播行为的常量: - -**支持当前事务的情况:** - -- TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 -- TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 -- TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性) - -**不支持当前事务的情况:** - -- TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 -- TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 -- TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 - -**其他情况:** - -- TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。 - -#### 隔离级别 - -TransactionDefinition 接口中定义了五个表示隔离级别的常量: - -- **TransactionDefinition.ISOLATION_DEFAULT:** 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别. -- **TransactionDefinition.ISOLATION_READ_UNCOMMITTED:** 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 -- **TransactionDefinition.ISOLATION_READ_COMMITTED:** 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 -- **TransactionDefinition.ISOLATION_REPEATABLE_READ:** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 -- **TransactionDefinition.ISOLATION_SERIALIZABLE:** 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 - -## 12. SpringMVC 原理了解吗? - -![SpringMVC 原理](https://user-gold-cdn.xitu.io/2018/11/10/166fd45787394192?w=1015&h=466&f=webp&s=35352) - -客户端发送请求-> 前端控制器 DispatcherServlet 接受客户端请求 -> 找到处理器映射 HandlerMapping 解析请求对应的 Handler-> HandlerAdapter 会根据 Handler 来调用真正的处理器处理请求,并处理相应的业务逻辑 -> 处理器返回一个模型视图 ModelAndView -> 视图解析器进行解析 -> 返回一个视图对象->前端控制器 DispatcherServlet 渲染数据(Model)->将得到视图对象返回给用户 - -关于 SpringMVC 原理更多内容可以查看我的这篇文章:[SpringMVC 工作原理详解](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484496&idx=1&sn=5472ffa687fe4a05f8900d8ee6726de4&chksm=fd985231caefdb27fc75b44ecf76b6f43e4617e0b01b3c040f8b8fab32e51dfa5118eed1d6ad&token=1990180468&lang=zh_CN#rd) - -## 13. Spring AOP IOC 实现原理 - -过了秋招挺长一段时间了,说实话我自己也忘了如何简要概括 Spring AOP IOC 实现原理,就在网上找了一个较为简洁的答案,下面分享给各位。 - -**IOC:** 控制反转也叫依赖注入。IOC 利用 java 反射机制,AOP 利用代理模式。IOC 概念看似很抽象,但是很容易理解。说简单点就是将对象交给容器管理,你只需要在 spring 配置文件中配置对应的 bean 以及设置相关的属性,让 spring 容器来生成类的实例对象以及管理对象。在 spring 容器启动的时候,spring 会把你在配置文件中配置的 bean 都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些 bean 分配给你需要调用这些 bean 的类。 - -**AOP:** 面向切面编程。(Aspect-Oriented Programming) 。AOP 可以说是对 OOP 的补充和完善。OOP 引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。实现 AOP 的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码,属于静态代理。 - -# 二 进阶篇 - -## 1 消息队列 MQ 的套路 - -消息队列/消息中间件应该是 Java 程序员必备的一个技能了,如果你之前没接触过消息队列的话,建议先去百度一下某某消息队列入门,然后花 2 个小时就差不多可以学会任何一种消息队列的使用了。如果说仅仅学会使用是万万不够的,在实际生产环境还要考虑消息丢失等等情况。关于消息队列面试相关的问题,推荐大家也可以看一下视频《Java 工程师面试突击第 1 季-中华石杉老师》,如果大家没有资源的话,可以在我的公众号“Java 面试通关手册”后台回复关键字“1”即可! - -### 1.1 介绍一下消息队列 MQ 的应用场景/使用消息队列的好处 - -面试官一般会先问你这个问题,预热一下,看你知道消息队列不,一般在第一面的时候面试官可能只会问消息队列 MQ 的应用场景/使用消息队列的好处、使用消息队列会带来什么问题、消息队列的技术选型这几个问题,不会太深究下去,在后面的第二轮/第三轮技术面试中可能会深入问一下。 - -**《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。** - -#### 1)通过异步处理提高系统性能 - -![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/通过异步处理提高系统性能.jpg) -如上图,**在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。** - -通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: -![合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击.jpg) -因为**用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败**。因此使用消息队列进行异步处理之后,需要**适当修改业务流程进行配合**,比如**用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 - -#### 2)降低系统耦合性 - -我们知道模块分布式部署以后聚合方式通常有两种:1.**分布式消息队列**和 2.**分布式服务**。 - -> **先来简单说一下分布式服务:** - -目前使用比较多的用来构建**SOA(Service Oriented Architecture 面向服务体系结构)**的**分布式服务框架**是阿里巴巴开源的**Dubbo**。如果想深入了解 Dubbo 的可以看我写的关于 Dubbo 的这一篇文章:**《高性能优秀的服务框架-dubbo 介绍》**:[https://juejin.im/post/5acadeb1f265da2375072f9c](https://juejin.im/post/5acadeb1f265da2375072f9c "https://juejin.im/post/5acadeb1f265da2375072f9c") - -> **再来谈我们的分布式消息队列:** - -我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。 - -我们最常见的**事件驱动架构**类似生产者消费者模式,在大型网站中通常用利用消息队列实现事件驱动结构。如下图所示: - -![利用消息队列实现事件驱动结构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/利用消息队列实现事件驱动结构.jpg) -**消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 - -消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。 - -**另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。** - -**备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的,**比如在我们的 ActiveMQ 消息队列中还有点对点工作模式**,具体的会在后面的文章给大家详细介绍,这一篇文章主要还是让大家对消息队列有一个更透彻的了解。 - -> 这个问题一般会在上一个问题问完之后,紧接着被问到。“使用消息队列会带来什么问题?”这个问题要引起重视,一般我们都会考虑使用消息队列会带来的好处而忽略它带来的问题! - -### 1.2 那么使用消息队列会带来什么问题?考虑过这些问题吗? - -- **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了! -- **系统复杂性提高:** 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! -- **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! - -> 了解下面这个问题是为了我们更好的进行技术选型!该部分摘自:《Java 工程师面试突击第 1 季-中华石杉老师》,如果大家没有资源的话,可以在我的公众号“Java 面试通关手册”后台回复关键字“1”即可! - -### 1.3 介绍一下你知道哪几种消息队列,该如何选择呢? - -| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | -| :----------------------- | -----------------------------------------------------------: | -----------------------------------------------------------: | -----------------------------------------------------------: | -----------------------------------------------------------: | -| 单机吞吐量 | 万级,吞吐量比 RocketMQ 和 Kafka 要低了一个数量级 | 万级,吞吐量比 RocketMQ 和 Kafka 要低了一个数量级 | 10 万级,RocketMQ 也是可以支撑高吞吐的一种 MQ | 10 万级别,这是 kafka 最大的优点,就是吞吐量高。一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | -| topic 数量对吞吐量的影响 | | | topic 可以达到几百,几千个的级别,吞吐量会有较小幅度的下降这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka 尽量保证 topic 数量不要过多。如果要支撑大规模 topic,需要增加更多的机器资源 | -| 可用性 | 高,基于主从架构实现高可用性 | 高,基于主从架构实现高可用性 | 非常高,分布式架构 | 非常高,kafka 是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | -| 消息可靠性 | 有较低的概率丢失数据 | | 经过参数优化配置,可以做到 0 丢失 | 经过参数优化配置,消息可以做到 0 丢失 | -| 时效性 | ms 级 | 微秒级,这是 rabbitmq 的一大特点,延迟是最低的 | ms 级 | 延迟在 ms 级以内 | -| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,所以并发能力很强,性能极其好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 | -| 优劣势总结 | 非常成熟,功能强大,在业内大量的公司以及项目中都有应用。偶尔会有较低概率丢失消息,而且现在社区以及国内应用都越来越少,官方社区现在对 ActiveMQ 5.x 维护越来越少,几个月才发布一个版本而且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用 | erlang 语言开发,性能极其好,延时很低;吞吐量到万级,MQ 功能比较完备而且开源提供的管理界面非常棒,用起来很好用。社区相对比较活跃,几乎每个月都发布几个版本分在国内一些互联网公司近几年用 rabbitmq 也比较多一些但是问题也是显而易见的,RabbitMQ 确实吞吐量会低一些,这是因为他做的实现机制比较重。而且 erlang 开发,国内有几个公司有实力做 erlang 源码级别的研究和定制?如果说你没这个实力的话,确实偶尔会有一些问题,你很难去看懂源码,你公司对这个东西的掌控很弱,基本职能依赖于开源社区的快速维护和修复 bug。而且 rabbitmq 集群动态扩展会很麻烦,不过这个我觉得还好。其实主要是 erlang 语言本身带来的问题。很难读源码,很难定制和掌控。 | 接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障。日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是 ok 的,还可以支撑大规模的 topic 数量,支持复杂 MQ 业务场景。而且一个很大的优势在于,阿里出品都是 java 系的,我们可以自己阅读源码,定制自己公司的 MQ,可以掌控。社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用 RocketMQ 挺好的 | kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。而且 kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 | - -> 这部分内容,我这里不给出答案,大家可以自行根据自己学习的消息队列查阅相关内容,我可能会在后面的文章中介绍到这部分内容。另外,下面这些问题在视频《Java 工程师面试突击第 1 季-中华石杉老师》中都有提到,如果大家没有资源的话,可以在我的公众号“Java 面试通关手册”后台回复关键字“1”即可! - -### 1.4 关于消息队列其他一些常见的问题展望 - -1. 引入消息队列之后如何保证高可用性? -2. 如何保证消息不被重复消费呢? -3. 如何保证消息的可靠性传输(如何处理消息丢失的问题)? -4. 我该怎么保证从消息队列里拿到的数据按顺序执行? -5. 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决? -6. 如果让你来开发一个消息队列中间件,你会怎么设计架构? - -## 2 谈谈 InnoDB 和 MyIsam 两者的区别 - -### 2.1 两者的对比 - -1. **count 运算上的区别:** 因为 MyISAM 缓存有表 meta-data(行数等),因此在做 COUNT(\*)时对于一个结构很好的查询是不需要消耗多少资源的。而对于 InnoDB 来说,则没有这种缓存 -2. **是否支持事务和崩溃后的安全恢复:** MyISAM 强调的是性能,每次查询具有原子性,其执行速度比 InnoDB 类型更快,但是不提供事务支持。但是 InnoDB 提供事务支持,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。 -3. **是否支持外键:** MyISAM 不支持,而 InnoDB 支持。 - -### 2.2 关于两者的总结 - -MyISAM 更适合读密集的表,而 InnoDB 更适合写密集的表。 在数据库做主从分离的情况下,经常选择 MyISAM 作为主库的存储引擎。 - -一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM 的表锁的粒度太大,所以当该表写并发量较高时,要等待的查询就会很多了),InnoDB 是不错的选择。如果你的数据量很大(MyISAM 支持压缩特性可以减少磁盘的空间占用),而且不需要支持事务时,MyISAM 是最好的选择。 - -## 3 聊聊 Java 中的集合吧! - -### 3.1 Arraylist 与 LinkedList 有什么不同?(注意加上从数据结构分析的内容) - -- **1. 是否保证线程安全:** ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全; -- **2. 底层数据结构:** Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是双向链表数据结构(注意双向链表和双向循环链表的区别:); -- **3. 插入和删除是否受元素位置的影响:** ① **ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e)`方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1) 而数组为近似 O(n) 。** -- **4. 是否支持快速随机访问:** LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 -- **5. 内存空间占用:** ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。 - -**补充内容:RandomAccess 接口** - -```java -public interface RandomAccess { -} -``` - -查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。 - -在 binarySearch() 方法中,它要判断传入的 list 是否 RamdomAccess 的实例,如果是,调用 indexedBinarySearch() 方法,如果不是,那么调用 iteratorBinarySearch() 方法 - -```java - public static - int binarySearch(List> list, T key) { - if (list instanceof RandomAccess || list.size() Java 中的集合这类问题几乎是面试必问的,问到这类问题的时候,HashMap 又是几乎必问的问题,所以大家一定要引起重视! - -### 3.2 HashMap 的底层实现 - -#### 1)JDK1.8 之前 - -JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的时数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。** - -**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。** - -**JDK 1.8 HashMap 的 hash 方法源码:** - -JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 - -```java - static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^ :按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } -``` - -对比一下 JDK1.7 的 HashMap 的 hash 方法源码. - -```java -static int hash(int h) { - // This function ensures that hashCodes that differ only by - // constant multiples at each bit position have a bounded - // number of collisions (approximately 8 at default load factor). - - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); -} -``` - -相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 - -所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 - -![jdk1.8之前的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/jdk1.8之前的内部结构-HashMap.jpg) - -#### 2)JDK1.8 之后 - -相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。 - -![jdk1.8之后的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8之后的HashMap底层数据结构.jpg) - -TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 - -> 问完 HashMap 的底层原理之后,面试官可能就会紧接着问你 HashMap 底层数据结构相关的问题! - -### 3.3 既然谈到了红黑树,你给我手绘一个出来吧,然后简单讲一下自己对于红黑树的理解 - -![红黑树](https://user-gold-cdn.xitu.io/2018/11/14/16711ac29c138cba?w=851&h=614&f=jpeg&s=34458) - -**红黑树特点:** - -1. 每个节点非红即黑; -2. 根节点总是黑色的; -3. 每个叶子节点都是黑色的空节点(NIL 节点); -4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定); -5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度) - -**红黑树的应用:** - -TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。 - -**为什么要用红黑树** - -简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 - -### 3.4 红黑树这么优秀,为何不直接使用红黑树得了? - -说一下自己对于这个问题的看法:我们知道红黑树属于(自)平衡二叉树,但是为了保持“平衡”是需要付出代价的,红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,这费事啊。你说说我们引入红黑树就是为了查找数据快,如果链表长度很短的话,根本不需要引入红黑树的,你引入之后还要付出代价维持它的平衡。但是链表过长就不一样了。至于为什么选 8 这个值呢?通过概率统计所得,这个值是综合查询成本和新增元素成本得出的最好的一个值。 - -### 3.5 HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别 - -**HashMap 和 Hashtable 的区别** - -1. **线程是否安全:** HashMap 是非线程安全的,Hashtable 是线程安全的;Hashtable 内部的方法基本都经过 `synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); -2. **效率:** 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它; -3. **对 Null key 和 Null value 的支持:** HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 Hashtable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。 -4. **初始容量大小和每次扩充容量大小的不同 :** ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 -5. **底层数据结构:** JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 - -**HashSet 和 HashMap 区别** - -如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。) - -![HashSet 和 HashMap 区别](https://user-gold-cdn.xitu.io/2018/3/2/161e717d734f3b23?w=896&h=363&f=jpeg&s=205536) - -# 三 终结篇 - -## 1. Object 类有哪些方法? - -这个问题,面试中经常出现。我觉得不论是出于应付面试还是说更好地掌握 Java 这门编程语言,大家都要掌握! - -### 1.1 Object 类的常见方法总结 - -Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法: - -```java - -public final native Class getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。 - -public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。 -public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。 - -protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。 - -public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。 - -public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。 - -public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。 - -public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。 - -public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。 - -public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念 - -protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作 - -``` - -> 问完上面这个问题之后,面试官很可能紧接着就会问你“hashCode 与 equals”相关的问题。 - -### 1.2 hashCode 与 equals - -面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?” - -#### 1.2.1 hashCode()介绍 - -hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。 - -```java - public native int hashCode(); -``` - -散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - -#### 1.2.2 为什么要有 hashCode - -**我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** - -当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head fist java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -#### 1.2.3 hashCode()与 equals()的相关规定 - -1. 如果两个对象相等,则 hashcode 一定也是相同的 -2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true -3. 两个对象有相同的 hashcode 值,它们也不一定是相等的 -4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** -5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) - -#### 1.2.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的? - -在这里解释一位小伙伴的问题。以下内容摘自《Head Fisrt Java》。 - -因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。 - -我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。 - -> ==与 equals 的对比也是比较常问的基础问题之一! - -### 1.3 ==与 equals - -**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址) - -**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: - -- 情况 1:类没有覆盖 equals()方法。则通过 equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。 -- 情况 2:类覆盖了 equals()方法。一般,我们都覆盖 equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。 - -**举个例子:** - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -**说明:** - -- String 中的 equals()方法是被重写过的,因为 Object 的 equals()方法是比较的对象的内存地址,而 String 的 equals()方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -> 在[【备战春招/秋招系列 5】美团面经总结进阶篇 (附详解答案)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484625&idx=1&sn=9c4fa1f7d4291a5fbd7daa44bac2b012&chksm=fd9852b0caefdba6edcf9a827aa4a17ddc97bf6ad2e5ee6f7e1aa1b443b54444d05d2b76732b&token=723699735&lang=zh_CN#rd) 这篇文章中,我们已经提到了一下关于 HashMap 在面试中常见的问题:HashMap 的底层实现、简单讲一下自己对于红黑树的理解、红黑树这么优秀,为何不直接使用红黑树得了、HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别。HashMap 和 ConcurrentHashMap 这俩兄弟在一般只要面试中问到集合相关的问题就一定会被问到,所以各位务必引起重视! - -## 2 ConcurrentHashMap 相关问题 - -### 2.1 ConcurrentHashMap 和 Hashtable 的区别 - -ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 - -- **底层数据结构:** JDK1.7 的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; -- **实现线程安全的方式(重要):** ① **在 JDK1.7 的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配 16 个 Segment,比 Hashtable 效率提高 16 倍。) **到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)**:使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 - -**两者的对比图:** - -图片来源:http://www.cnblogs.com/chengxiao/p/6842045.html - -Hashtable: -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/50656681.jpg) - -JDK1.7 的 ConcurrentHashMap: -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/33120488.jpg) -JDK1.8 的 ConcurrentHashMap(TreeBin: 红黑二叉树节点 -Node: 链表节点): -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/97739220.jpg) - -### 2.2 ConcurrentHashMap 线程安全的具体实现方式/底层具体实现 - -#### JDK1.7(上面有示意图) - -首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 - -**ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成**。 - -Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。 - -```java -static class Segment extends ReentrantLock implements Serializable { -} -``` - -一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。 - -#### JDK1.8(上面有示意图) - -ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。 - -synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。 - -## 3 谈谈 synchronized 和 ReentrantLock 的区别 - -**① 两者都是可重入锁** - -两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。 - -**② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API** - -synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - -**③ ReentrantLock 比 synchronized 增加了一些高级功能** - -相比 synchronized,ReentrantLock 增加了一些高级功能。主要来说主要有三点:**① 等待可中断;② 可实现公平锁;③ 可实现选择性通知(锁可以绑定多个条件)** - -- **ReentrantLock 提供了一种能够中断等待锁的线程的机制**,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -- **ReentrantLock 可以指定是公平锁还是非公平锁。而 synchronized 只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReentrantLock 默认情况是非公平的,可以通过 ReentrantLock 类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 -- synchronized 关键字与 wait()和 notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock 类当然也可以实现,但是需要借助于 Condition 接口与 newCondition() 方法。Condition 是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个 Lock 对象中可以创建多个 Condition 实例(即对象监视器),**线程对象可以注册在指定的 Condition 中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用 notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用 ReentrantLock 类结合 Condition 实例可以实现“选择性通知”** ,这个功能非常重要,而且是 Condition 接口默认提供的。而 synchronized 关键字就相当于整个 Lock 对象中只有一个 Condition 实例,所有的线程都注册在它一个身上。如果执行 notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而 Condition 实例的 signalAll()方法 只会唤醒注册在该 Condition 实例中的所有等待线程。 - -如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。 - -**④ 两者的性能已经相差无几** - -在 JDK1.6 之前,synchronized 的性能是比 ReentrantLock 差很多。具体表示为:synchronized 关键字吞吐量岁线程数的增加,下降得非常严重。而 ReentrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReentrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReentrantLock 的文章都是错的!JDK1.6 之后,性能已经不是选择 synchronized 和 ReentrantLock 的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的 synchronized,所以还是提倡在 synchronized 能满足你的需求的情况下,优先考虑使用 synchronized 关键字来进行同步!优化后的 synchronized 和 ReentrantLock 一样,在很多地方都是用到了 CAS 操作。 - -## 4 线程池了解吗? - -### 4.1 为什么要用线程池? - -线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。 - -这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处: - -- **降低资源消耗。** 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度。** 当任务到达时,任务可以不需要的等到线程创建就能立即执行。 -- **提高线程的可管理性。** 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - -### 4.2 Java 提供了哪几种线程池?他们各自的使用场景是什么? - -#### Java 主要提供了下面 4 种线程池 - -- **FixedThreadPool:** 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 -- **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 -- **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 -- **ScheduledThreadPoolExecutor:** 主要用来在给定的延迟后运行任务,或者定期执行任务。ScheduledThreadPoolExecutor 又分为:ScheduledThreadPoolExecutor(包含多个线程)和 SingleThreadScheduledExecutor (只包含一个线程)两种。 - -#### 各种线程池的适用场景介绍 - -- **FixedThreadPool:** 适用于为了满足资源管理需求,而需要限制当前线程数量的应用场景。它适用于负载比较重的服务器; -- **SingleThreadExecutor:** 适用于需要保证顺序地执行各个任务并且在任意时间点,不会有多个线程是活动的应用场景; -- **CachedThreadPool:** 适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器; -- **ScheduledThreadPoolExecutor:** 适用于需要多个后台执行周期任务,同时为了满足资源管理需求而需要限制后台线程的数量的应用场景; -- **SingleThreadScheduledExecutor:** 适用于需要单个后台线程执行周期任务,同时保证顺序地执行各个任务的应用场景。 - -### 4.3 创建的线程池的方式 - -**(1) 使用 Executors 创建** - -我们上面刚刚提到了 Java 提供的几种线程池,通过 Executors 工具类我们可以很轻松的创建我们上面说的几种线程池。但是实际上我们一般都不是直接使用 Java 提供好的线程池,另外在《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 - -```java -Executors 返回线程池对象的弊端如下: - -FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。 -CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。 - -``` - -**(2) ThreadPoolExecutor 的构造函数创建** - -我们可以自己直接调用 ThreadPoolExecutor 的构造函数来自己创建线程池。在创建的同时,给 BlockQueue 指定容量就可以了。示例如下: - -```java -private static ExecutorService executor = new ThreadPoolExecutor(13, 13, - 60L, TimeUnit.SECONDS, - new ArrayBlockingQueue(13)); -``` - -这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出 java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。 - -**(3) 使用开源类库** - -Hollis 大佬之前在他的文章中也提到了:“除了自己定义 ThreadPoolExecutor 外。还有其他方法。这个时候第一时间就应该想到开源类库,如 apache 和 guava 等。”他推荐使用 guava 提供的 ThreadFactoryBuilder 来创建线程池。下面是参考他的代码示例: - -```java -public class ExecutorsDemo { - - private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() - .setNameFormat("demo-pool-%d").build(); - - private static ExecutorService pool = new ThreadPoolExecutor(5, 200, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy()); - - public static void main(String[] args) { - - for (int i = 0; i < Integer.MAX_VALUE; i++) { - pool.execute(new SubThread()); - } - } -} -``` - -通过上述方式创建线程时,不仅可以避免 OOM 的问题,还可以自定义线程名称,更加方便的出错的时候溯源。 - -## 5 Nginx - -### 5.1 简单介绍一下 Nginx - -Nginx 是一款轻量级的 Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。 Nginx 主要提供反向代理、负载均衡、动静分离(静态资源服务)等服务。下面我简单地介绍一下这些名词。 - -#### 反向代理 - -谈到反向代理,就不得不提一下正向代理。无论是正向代理,还是反向代理,说到底,就是代理模式的衍生版本罢了 - -- **正向代理:**某些情况下,代理我们用户去访问服务器,需要用户手动的设置代理服务器的 ip 和端口号。正向代理比较常见的一个例子就是 VPN 了。 -- **反向代理:** 是用来代理服务器的,代理我们要访问的目标服务器。代理服务器接受请求,然后将请求转发给内部网络的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个服务器。 - -通过下面两幅图,大家应该更好理解(图源:http://blog.720ui.com/2016/nginx_action_05_proxy/): - -![正向代理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-15/60925795.jpg) - -![反向代理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-15/62563930.jpg) - -所以,简单的理解,就是正向代理是为客户端做代理,代替客户端去访问服务器,而反向代理是为服务器做代理,代替服务器接受客户端请求。 - -#### 负载均衡 - -在高并发情况下需要使用,其原理就是将并发请求分摊到多个服务器执行,减轻每台服务器的压力,多台服务器(集群)共同完成工作任务,从而提高了数据的吞吐量。 - -Nginx 支持的 weight 轮询(默认)、ip_hash、fair、url_hash 这四种负载均衡调度算法,感兴趣的可以自行查阅。 - -负载均衡相比于反向代理更侧重的是将请求分担到多台服务器上去,所以谈论负载均衡只有在提供某服务的服务器大于两台时才有意义。 - -#### 动静分离 - -动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。 - -### 5.2 为什么要用 Nginx? - -> 这部分内容参考极客时间—[Nginx 核心知识 100 讲的内容](https://time.geekbang.org/course/intro/138?code=AycjiiQk6uQRxnVJzBupFkrGkvZlmYELPRsZbWzaAHE= "Nginx核心知识100讲的内容")。 - -如果面试官问你这个问题,就一定想看你知道 Nginx 服务器的一些优点吗。 - -Nginx 有以下 5 个优点: - -1. 高并发、高性能(这是其他 web 服务器不具有的) -2. 可扩展性好(模块化设计,第三方插件生态圈丰富) -3. 高可靠性(可以在服务器行持续不间断的运行数年) -4. 热部署(这个功能对于 Nginx 来说特别重要,热部署指可以在不停止 Nginx 服务的情况下升级 Nginx) -5. BSD 许可证(意味着我们可以将源代码下载下来进行修改然后使用自己的版本) - -### 5.3 Nginx 的四个主要组成部分了解吗? - -> 这部分内容参考极客时间—[Nginx 核心知识 100 讲的内容](https://time.geekbang.org/course/intro/138?code=AycjiiQk6uQRxnVJzBupFkrGkvZlmYELPRsZbWzaAHE= "Nginx核心知识100讲的内容")。 - -- Nginx 二进制可执行文件:由各模块源码编译出一个文件 -- nginx.conf 配置文件:控制 Nginx 行为 -- acess.log 访问日志: 记录每一条 HTTP 请求信息 -- error.log 错误日志:定位问题 \ No newline at end of file diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\351\235\242\350\257\225\345\256\230-\344\275\240\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221.md" "b/docs/essential-content-for-interview/PreparingForInterview/\351\235\242\350\257\225\345\256\230-\344\275\240\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221.md" deleted file mode 100644 index 7a55d53..0000000 --- "a/docs/essential-content-for-interview/PreparingForInterview/\351\235\242\350\257\225\345\256\230-\344\275\240\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221.md" +++ /dev/null @@ -1,64 +0,0 @@ -我还记得当时我去参加面试的时候,几乎每一场面试,特别是HR面和高管面的时候,面试官总是会在结尾问我:“问了你这么多问题了,你有什么问题问我吗?”。这个时候很多人内心就会陷入短暂的纠结中:我该问吗?不问的话面试官会不会对我影响不好?问什么问题?问这个问题会不会让面试官对我的影响不好啊? - -![无奈](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/无奈.jpg) - -### 这个问题对最终面试结果的影响到底大不大? - -就技术面试而言,回答这个问题的时候,只要你不是触碰到你所面试的公司的雷区,那么我觉得这对你能不能拿到最终offer来说影响确实是不大的。我说这些并不代表你就可以直接对面试官说:“我没问题了。”,笔主当时面试的时候确实也说过挺多次“没问题要问了。”,最终也没有导致笔主被pass掉(可能是前面表现比较好,哈哈,自恋一下)。我现在回想起来,觉得自己当时做法其实挺不对的。面试本身就是一个双向选择的过程,你对这个问题的回答也会侧面反映出你对这次面试的上心程度,你的问题是否有价值,也影响了你最终的选择与公司是否选择你。 - -面试官在技术面试中主要考察的还是你这样个人到底有没有胜任这个工作的能力以及你是否适合公司未来的发展需要,很多公司还需要你认同它的文化,我觉得你只要不是太笨,应该不会栽在这里。除非你和另外一个人在能力上相同,但是只能在你们两个人中选一个,那么这个问题才对你能不能拿到offer至关重要。有准备总比没准备好,给面试官留一个好的影响总归是没错的。 - -但是,就非技术面试来说,我觉得好好回答这个问题对你最终的结果还是比较重要的。 - -总的来说不管是技术面试还是非技术面试,如果你想赢得公司的青睐和尊重,我觉得我们都应该重视这个问题。 - -### 真诚一点,不要问太 Low 的问题 - -回答这个问题很重要的一点就是你没有必要放低自己的姿态问一些很虚或者故意讨好面试官的问题,也不要把自己从面经上学到的东西照搬下来使用。面试官也不是傻子,特别是那种特别有经验的面试官,你是真心诚意的问问题,还是从别处照搬问题来讨好面试官,人家可能一听就听出来了。总的来说,还是要真诚。除此之外,不要问太 Low 的问题,会显得你整个人格局比较小或者说你根本没有准备(侧面反映你对这家公司不上心,既然你不上心,为什么要要你呢)。举例几个比较 Low 的问题,大家看看自己有没有问过其中的问题: - -- 贵公司的主要业务是什么?(面试之前自己不知道提前网上查一下吗?) -- 贵公司的男女比例如何?(考虑脱单?记住你是来工作的!) -- 贵公司一年搞几次外出旅游?(你是来工作的,这些娱乐活动先别放在心上!) -- ...... - -### 有哪些有价值的问题值得问? - -针对这个问题。笔主专门找了几个专门做HR工作的小哥哥小姐姐们询问并且查阅了挺多前辈们的回答,然后结合自己的实际经历,我概括了下面几个比较适合问的问题。 - -#### 面对HR或者其他Level比较低的面试官时 - -1. **能不能谈谈你作为一个公司老员工对公司的感受?** (这个问题比较容易回答,不会让面试官陷入无话可说的尴尬境地。另外,从面试官的回答中你可以加深对这个公司的了解,让你更加清楚这个公司到底是不是你想的那样或者说你是否能适应这个公司的文化。除此之外,这样的问题在某种程度上还可以拉进你与面试官的距离。) -2. **能不能问一下,你当时因为什么原因选择加入这家公司的呢或者说这家公司有哪些地方吸引你?有什么地方你觉得还不太好或者可以继续完善吗?** (类似第一个问题,都是问面试官个人对于公司的看法。) -3. **我觉得我这次表现的不是太好,你有什么建议或者评价给我吗?**(这个是我常问的。我觉得说自己表现不好只是这个语境需要这样来说,这样可以显的你比较谦虚好学上进。) -4. **接下来我会有一段空档期,有什么值得注意或者建议学习的吗?** (体现出你对工作比较上心,自助学习意识比较强。) -5. **这个岗位为什么还在招人?** (岗位真实性和价值咨询) -6. **大概什么时候能给我回复呢?** (终面的时候,如果面试官没有说的话,可以问一下) -7. ...... - - - -#### 面对部门领导 - -1. **部门的主要人员分配以及对应的主要工作能简单介绍一下吗?** -2. **未来如果我要加入这个团队,你对我的期望是什么?** (部门领导一般情况下是你的直属上级了,你以后和他打交道的机会应该是最多的。你问这个问题,会让他感觉你是一个对他的部门比较上心,比较有团体意识,并且愿意倾听的候选人。) -3. **公司对新入职的员工的培养机制是什么样的呢?** (正规的公司一般都有培养机制,提前问一下是对你自己的负责也会显的你比较上心) -4. **以您来看,这个岗位未来在公司内部的发展如何?** (在我看来,问这个问题也是对你自己的负责吧,谁不想发展前景更好的岗位呢?) -5. **团队现在面临的最大挑战是什么?** (这样的问题不会暴露你对公司的不了解,并且也能让你对未来工作的挑战或困难有一个提前的预期。) - - - -#### 面对Level比较高的(比如总裁,老板) - -1. **贵公司的发展目标和方向是什么?** (看下公司的发展是否满足自己的期望) -2. **与同行业的竞争者相比,贵公司的核心竞争优势在什么地方?** (充分了解自己的优势和劣势) -3. **公司现在面临的最大挑战是什么?** - -### 来个补充,顺便送个祝福给大家 - -薪酬待遇和相关福利问题一般在终面的时候(最好不要在前面几面的时候就问到这个问题),面试官会提出来或者在面试完之后以邮件的形式告知你。一般来说,如果面试官很愿意为你回答问题,对你的问题也比较上心的话,那他肯定是觉得你就是他们要招的人。 - -大家在面试的时候,可以根据自己对于公司或者岗位的了解程度,对上面提到的问题进行适当修饰或者修改。上面提到的一些问题只是给没有经验的朋友一个参考,如果你还有其他比较好的问题的话,那当然也更好啦! - -金三银四。过了二月就到了面试高峰期或者说是黄金期。几份惊喜几份愁,愿各位能始终不忘初心!每个人都有每个人的难处。引用一句《阿甘正传》里面的台词:“生活就像一盒巧克力,你永远不知道下一块是什么味道“。 - -![加油!彩虹就要来了](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/生活就像一盒巧克力你永远不知道下一块是什么味道.JPEG) \ No newline at end of file diff --git a/docs/essential-content-for-interview/real-interview-experience-analysis/alibaba-1.md b/docs/essential-content-for-interview/real-interview-experience-analysis/alibaba-1.md deleted file mode 100644 index a1f6101..0000000 --- a/docs/essential-content-for-interview/real-interview-experience-analysis/alibaba-1.md +++ /dev/null @@ -1,222 +0,0 @@ -本文的内容都是根据读者投稿的真实面试经历改编而来,首次尝试这种风格的文章,花了几天晚上才总算写完,希望对你有帮助。 - -本文主要涵盖下面的内容: - -1. 分布式商城系统:架构图讲解; -2. 消息队列相关:削峰和解耦; -3. Redis 相关:缓存穿透问题的解决; -4. 一些基础问题: - - 网络相关:1.浏览器输入 URL 发生了什么? 2.TCP 和 UDP 区别? 3.TCP 如何保证传输可靠性? - - Java 基础:1. 既然有了字节流,为什么还要有字符流? 2.深拷贝 和 浅拷贝有啥区别呢? - -下面是正文! - -面试开始,坐在我前面的就是这次我的面试官吗?这发量看着根本不像程序员啊?我心里正嘀咕着,只听见面试官说:“小伙,下午好,我今天就是你的面试官,咱们开始面试吧!”。 - -### 第一面开始 - -**面试官:** 我也不用多说了,你先自我介绍一下吧,简历上有的就不要再说了哈。 - -**我:** 内心 os:"果然如我所料,就知道会让我先自我介绍一下,还好我看了 [JavaGuide](https://github.com/Snailclimb/JavaGuide "JavaGuide") ,学到了一些套路。套路总结起来就是:**最好准备好两份自我介绍,一份对 hr 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。** 所以,我按照这个套路准备了一个还算通用的模板,毕竟我懒嘛!不想多准备一个自我介绍,整个通用的多好! - -> 面试官,您好!我叫小李子。大学时间我主要利用课外时间学习 Java 相关的知识。在校期间参与过一个某某系统的开发,主要负责数据库设计和后端系统开发.,期间解决了什么问题,巴拉巴拉。另外,我自己在学习过程中也参照网上的教程写过一个电商系统的网站,写这个电商网站主要是为了能让自己接触到分布式系统的开发。在学习之余,我比较喜欢通过博客整理分享自己所学知识。我现在已经是某社区的认证作者,写过一系列关于 线程池使用以及源码分析的文章深受好评。另外,我获得过省级编程比赛二等奖,我将这个获奖项目开源到 Github 还收获了 2k 的 Star 呢? - -**面试官:** 你刚刚说参考网上的教程做了一个电商系统?你能画画这个电商系统的架构图吗? - -**我:** 内心 os: "这可难不倒我!早知道写在简历上的项目要重视了,提前都把这个系统的架构图画了好多遍了呢!" - - - -做过分布式电商系统的一定很熟悉上面的架构图(目前比较流行的是微服务架构,但是如果你有分布式开发经验也是非常加分的!)。 - -**面试官:** 简单介绍一下你做的这个系统吧! - -**我:** 我一本正经的对着我刚刚画的商城架构图开始了满嘴造火箭的讲起来: - -> 本系统主要分为展示层、服务层和持久层这三层。表现层顾名思义主要就是为了用来展示,比如我们的后台管理系统的页面、商城首页的页面、搜索系统的页面等等,这一层都只是作为展示,并没有提供任何服务。 -> -> 展示层和服务层一般是部署在不同的机器上来提高并发量和扩展性,那么展示层和服务层怎样才能交互呢?在本系统中我们使用 Dubbo 来进行服务治理。Dubbo 是一款高性能、轻量级的开源 Java RPC 框架。Dubbo 在本系统的主要作用就是提供远程 RPC 调用。在本系统中服务层的信息通过 Dubbo 注册给 ZooKeeper,表现层通过 Dubbo 去 ZooKeeper 中获取服务的相关信息。Zookeeper 的作用仅仅是存放提供服务的服务器的地址和一些服务的相关信息,实现 RPC 远程调用功能的还是 Dubbo。如果需要引用到某个服务的时候,我们只需要在配置文件中配置相关信息就可以在代码中直接使用了,就像调用本地方法一样。假如说某个服务的使用量增加时,我们只用为这单个服务增加服务器,而不需要为整个系统添加服务。 -> -> 另外,本系统的数据库使用的是常用的 MySQL,并且用到了数据库中间件 MyCat。另外,本系统还用到 redis 内存数据库来作为缓存来提高系统的反应速度。假如用户第一次访问数据库中的某些数据,这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。 -> -> 系统还用到了 Elasticsearch 来提供搜索功能。使用 Elasticsearch 我们可以非常方便的为我们的商城系统添加必备的搜索功能,并且使用 Elasticsearch 还能提供其它非常实用的功能,并且很容易扩展。 - -**面试官:** 我看你的系统里面还用到了消息队列,能说说为什么要用它吗? - -**我:** - -> 使用消息队列主要是为了: -> -> 1. 减少响应所需时间和削峰。 -> 2. 降低系统耦合性(解耦/提升系统可扩展性)。 - -**面试官:** 你这说的太简单了!能不能稍微详细一点,最好能画图给我解释一下。 - -**我:** 内心 os:"都 2019 年了,大部分面试者都能对消息队列的为系统带来的这两个好处倒背如流了,如果你想走的更远就要别别人懂的更深一点!" - -> 当我们不使用消息队列的时候,所有的用户的请求会直接落到服务器,然后通过数据库或者缓存响应。假如在高并发的场景下,如果没有缓存或者数据库承受不了这么大的压力的话,就会造成响应速度缓慢,甚至造成数据库宕机。但是,在使用消息队列之后,用户的请求数据发送给了消息队列之后就可以立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库,不过要确保消息不被重复消费还要考虑到消息丢失问题。由于消息队列服务器处理速度快于数据库,因此响应速度得到大幅改善。 -> -> 文字 is too 空洞,直接上图吧!下图展示了使用消息前后系统处理用户请求的对比(ps:我自己都被我画的这个图美到了,如果你也觉得这张图好看的话麻烦来个素质三连!)。 -> -> ![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/Asynchronous-message-queue.png) -> -> 通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: -> -> ![削峰](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/削峰-消息队列.png) -> -> 使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧: -> -> ![解耦](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/消息队列-解耦.png) -> -> 生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。 - -**面试官:** 你觉得它有什么缺点吗?或者说怎么考虑用不用消息队列? - -**我:** 内心 os: "面试官真鸡贼!这不是勾引我上钩么?还好我准备充分。" - -> 我觉得可以从下面几个方面来说: -> -> 1. **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了! -> 2. **系统复杂性提高:** 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! -> 3. **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! - -**面试官**:做项目的过程中遇到了什么问题吗?解决了吗?如果解决的话是如何解决的呢? - -**我** : 内心 os: "做的过程中好像也没有遇到什么问题啊!怎么办?怎么办?突然想到可以说我在使用 Redis 过程中遇到的问题,毕竟我对 Redis 还算熟悉嘛,**把面试官往这个方向吸引**,准没错。" - -> 我在使用 Redis 对常用数据进行缓冲的过程中出现了缓存穿透问题。然后,我通过谷歌搜索相关的解决方案来解决的。 - -**面试官:** 你还知道缓存穿透啊?不错啊!来说说什么是缓存穿透以及你最后的解决办法。 - -**我:** 我先来谈谈什么是缓存穿透吧! - -> 缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。 -> -> 总结一下就是: -> -> 1. 缓存层不命中。 -> 2. 存储层不命中,不将空结果写回缓存。 -> 3. 返回空结果给客户端。 -> -> 一般 MySQL 默认的最大连接数在 150 左右,这个可以通过 `show variables like '%max_connections%';`命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等物理条件都是其运行指标,这些指标都会限制其并发能力!所以,一般 3000 的并发请求就能打死大部分数据库了。 - -**面试官:** 小伙子不错啊!还准备问你:“为什么 3000 的并发能把支持最大连接数 4000 数据库压死?”想不到你自己就提前回答了!不错! - -**我:** 别夸了!别夸了!我再来说说我知道的一些解决办法以及我最后采用的方案吧!您帮忙看看有没有问题。 - -> 最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。 -> -> 参数校验通过的情况还是会出现缓存穿透,我们还可以通过以下几个方案来解决这个问题: -> -> **1)缓存无效 key** : 如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如何黑客恶意攻击,每次构建的不同的请求 key,会导致 redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。 -> -> 另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值`。 -> -> **2)布隆过滤器:** 布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。 - -**面试官:** 不错不错!你还知道布隆过滤器啊!来给我谈一谈。 - -**我:** 内心 os:“如果你准备过海量数据处理的面试题,你一定对:“如何确定一个数字是否在于包含大量数字的数字集中(数字集很大,5 亿以上!)?”这个题目很了解了!解决这道题目就要用到布隆过滤器。” - -> 布隆过滤器在针对海量数据去重或者验证数据合法性的时候非常有用。**布隆过滤器的本质实际上是 “位(bit)数组”,也就是说每一个存入布隆过滤器的数据都只占一位。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。** -> -> **当一个元素加入布隆过滤器中的时候,会进行如下操作:** -> -> 1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。 -> 2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。 -> -> **当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:** -> -> 1. 对给定元素再次进行相同的哈希计算; -> 2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 -> -> 举个简单的例子: -> -> ![布隆过滤器hash计算](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-hash运算.png) -> -> 如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为 0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。 -> -> 如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 -> -> **不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。** -> -> 综上,我们可以得出:**布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。** - -**面试官:** 看来你对布隆过滤器了解的还挺不错的嘛!那你快说说你最后是怎么利用它来解决缓存穿透的。 - -**我:** 知道了布隆过滤器的原理就之后就很容易做了。我是利用 Redis 布隆过滤器来做的。我把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。总结一下就是下面这张图(这张图片不是我画的,为了省事直接在网上找的): - - - -更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md "《不了解布隆过滤器?一文给你整的明明白白!》") ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。 - -**面试官:** 好了好了。项目就暂时问到这里吧!下面有一些比较基础的问题我简单地问一下你。内心 os: 难不成这家伙满口高并发,连最基础的东西都不会吧! - -**我:** 好的好的!没问题! - -**面试官:** 浏览器输入 URL 发生了什么? - -**我:** 内心 os:“很常问的一个问题,建议拿小本本记好了!另外,百度好像最喜欢问这个问题,去百度面试可要提前备好这道题的功课哦!相似问题:打开一个网页,整个过程会使用哪些协议?”。 - -> 图解(图片来源:《图解 HTTP》): -> -> -> -> 总体来说分为以下几个过程: -> -> 1. DNS 解析 -> 2. TCP 连接 -> 3. 发送 HTTP 请求 -> 4. 服务器处理请求并返回 HTTP 报文 -> 5. 浏览器解析渲染页面 -> 6. 连接结束 -> -> 具体可以参考下面这篇文章: -> -> - [https://segmentfault.com/a/1190000006879700](https://segmentfault.com/a/1190000006879700 "https://segmentfault.com/a/1190000006879700") - -**面试官:** TCP 和 UDP 区别? - -**我:** - -> ![TCP、UDP协议的区别](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/tcp-vs-udp.jpg) -> -> UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等 -> -> TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP 的可靠体现在 TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。 - -**面试官:** TCP 如何保证传输可靠性? - -**我:** - -> 1. 应用数据被分割成 TCP 认为最适合发送的数据块。 -> 2. TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。 -> 3. **校验和:** TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。 -> 4. TCP 的接收端会丢弃重复的数据。 -> 5. **流量控制:** TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制) -> 6. **拥塞控制:** 当网络拥塞时,减少数据的发送。 -> 7. **ARQ 协议:** 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。 -> 8. **超时重传:** 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。 - -**面试官:** 我再来问你一些 Java 基础的问题吧!小伙子。 - -**我:** 好的。(内心 os:“你尽管来!”) - -**面试官:** 既然有了字节流,为什么还要有字符流? - -我:内心 os :“问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?**” - -> 字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 - -**面试官**:深拷贝 和 浅拷贝有啥区别呢? - -**我:** - -> 1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。 -> 2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 -> -> ![deep and shallow copy](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/java-deep-and-shallow-copy.jpg) - -**面试官:** 好的!面试结束。小伙子可以的!回家等通知吧! - -**我:** 好的好的!辛苦您了! diff --git "a/docs/essential-content-for-interview/\346\211\213\346\212\212\346\211\213\346\225\231\344\275\240\347\224\250Markdown\345\206\231\344\270\200\344\273\275\351\253\230\350\264\250\351\207\217\347\232\204\347\256\200\345\216\206.md" "b/docs/essential-content-for-interview/\346\211\213\346\212\212\346\211\213\346\225\231\344\275\240\347\224\250Markdown\345\206\231\344\270\200\344\273\275\351\253\230\350\264\250\351\207\217\347\232\204\347\256\200\345\216\206.md" deleted file mode 100644 index 9cb2811..0000000 --- "a/docs/essential-content-for-interview/\346\211\213\346\212\212\346\211\213\346\225\231\344\275\240\347\224\250Markdown\345\206\231\344\270\200\344\273\275\351\253\230\350\264\250\351\207\217\347\232\204\347\256\200\345\216\206.md" +++ /dev/null @@ -1,93 +0,0 @@ -## Markdown 简历模板样式一览 -![](https://user-gold-cdn.xitu.io/2018/9/3/1659f91e4843bd67?w=800&h=1737&f=png&s=97357) -**可以看到我把联系方式放在第一位,因为公司一般会与你联系,所以把联系方式放在第一位也是为了方便联系考虑。** - -## 为什么要用 Markdown 写简历? - -Markdown 语法简单,易于上手。使用正确的 Markdown 语言写出来的简历不论是在排版还是格式上都比较干净,易于阅读。另外,使用 Markdown 写简历也会给面试官一种你比较专业的感觉。 - -除了这些,我觉得使用 Markdown 写简历可以很方便将其与PDF、HTML、PNG格式之间转换。后面我会介绍到转换方法,只需要一条命令你就可以实现 Markdown 到 PDF、HTML 与 PNG之间的无缝切换。 - -> 下面的一些内容我在之前的一篇文章中已经提到过,这里再说一遍,最后会分享如何实现Markdown 到 PDF、HTML、PNG格式之间转换。 - -## 为什么说简历很重要? - -假如你是网申,你的简历必然会经过HR的筛选,一张简历HR可能也就花费10秒钟看一下,然后HR就会决定你这一关是Fail还是Pass。 - -假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。 - -另外,就算你通过了筛选,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。 - -## 写简历的两大法则 - -目前写简历的方式有两种普遍被认可,一种是 STAR, 一种是 FAB。 - -**STAR法则(Situation Task Action Result):** - -- **Situation:** 事情是在什么情况下发生; -- **Task::** 你是如何明确你的任务的; -- **Action:** 针对这样的情况分析,你采用了什么行动方式; -- **Result:** 结果怎样,在这样的情况下你学习到了什么。 - -**FAB 法则(Feature Advantage Benefit):** - -- **Feature:** 是什么; -- **Advantage:** 比别人好在哪些地方; -- **Benefit:** 如果雇佣你,招聘方会得到什么好处。 - -## 项目经历怎么写? -简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑从如下几点来写: - -1. 对项目整体设计的一个感受 -2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的。 - -## 专业技能该怎么写? -先问一下你自己会什么,然后看看你意向的公司需要什么。一般HR可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。比如你可以这样写: - -- Dubbo:精通 -- Spring:精通 -- Docker:掌握 -- SOA分布式开发 :掌握 -- Spring Cloud:了解 - -## 简历模板分享 - -**开源程序员简历模板**: [https://github.com/geekcompany/ResumeSample](https://github.com/geekcompany/ResumeSample)(包括PHP程序员简历模板、iOS程序员简历模板、Android程序员简历模板、Web前端程序员简历模板、Java程序员简历模板、C/C++程序员简历模板、NodeJS程序员简历模板、架构师简历模板以及通用程序员简历模板) - -**上述简历模板的改进版本:** [https://github.com/Snailclimb/Java-Guide/blob/master/面试必备/简历模板.md](https://github.com/Snailclimb/Java-Guide/blob/master/面试必备/简历模板.md) - -## 其他的一些小tips - -1. 尽量避免主观表述,少一点语义模糊的形容词,尽量要简洁明了,逻辑结构清晰。 -2. 注意排版(不需要花花绿绿的),尽量使用Markdown语法。 -3. 如果自己有博客或者个人技术栈点的话,写上去会为你加分很多。 -4. 如果自己的Github比较活跃的话,写上去也会为你加分很多。 -5. 注意简历真实性,一定不要写自己不会的东西,或者带有欺骗性的内容 -6. 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。 -7. 如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。 -8. 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你会很有礼貌。 - - -> 我们刚刚讲了很多关于如何写简历的内容并且分享了一份 Markdown 格式的简历文档。下面我们来看看如何实现 Markdown 到 HTML格式、PNG格式之间转换。 -## Markdown 到 HTML格式、PNG格式之间转换 - -网上很难找到一个比较方便并且效果好的转换方法,最后我是通过 Visual Studio Code 的 Markdown PDF 插件完美解决了这个问题! - -### 安装 Markdown PDF 插件 - -**① 打开Visual Studio Code ,按快捷键 F1,选择安装扩展选项** - -![① 打开Visual Studio Code ,按快捷键 F1,选择安装扩展选项](https://user-gold-cdn.xitu.io/2018/9/3/1659f9a44103e551?w=1366&h=688&f=png&s=104435) - -**② 搜索 “Markdown PDF” 插件并安装 ,然后重启** - -![② 搜索 “Markdown PDF” 插件并安装 ,然后重启](https://user-gold-cdn.xitu.io/2018/9/3/1659f9dbef0d06fb?w=1280&h=420&f=png&s=70510) - -### 使用方法 - -随便打开一份 Markdown 文件 点击F1,然后输入export即可! - -![](https://user-gold-cdn.xitu.io/2018/9/3/1659fa0292906150?w=1289&h=468&f=png&s=72178) - diff --git "a/docs/essential-content-for-interview/\347\256\200\345\216\206\346\250\241\346\235\277.md" "b/docs/essential-content-for-interview/\347\256\200\345\216\206\346\250\241\346\235\277.md" deleted file mode 100644 index 2a7a043..0000000 --- "a/docs/essential-content-for-interview/\347\256\200\345\216\206\346\250\241\346\235\277.md" +++ /dev/null @@ -1,79 +0,0 @@ -# 联系方式 - -- 手机: -- Email: -- 微信: - -# 个人信息 - - - 姓名/性别/出生日期 - - 本科/xxx计算机系xxx专业/英语六级 - - 技术博客:[http://snailclimb.top/](http://snailclimb.top/) - - 荣誉奖励:获得了什么奖(获奖时间) - - Github:[https://github.com/Snailclimb ](https://github.com/Snailclimb) - - Github Resume: [http://resume.github.io/?Snailclimb](http://resume.github.io/?Snailclimb) - - 期望职位:Java 研发程序员/大数据工程师(Java后台开发为首选) - - 期望城市:xxx城市 - - -# 项目经历 - -## xxx项目 - -### 项目描述 - -介绍该项目是做什么的、使用到了什么技术以及你对项目整体设计的一个感受 - -### 责任描述 - -主要可以从下面三点来写: - -1. 在这个项目中你负责了什么、做了什么、担任了什么角色 -2. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -3. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的。 - -# 开源项目和技术文章 - -## 开源项目 - -- [Java-Guide](https://github.com/Snailclimb/Java-Guide) :一份涵盖大部分Java程序员所需要掌握的核心知识。Star:3.9K; Fork:0.9k。 - - -## 技术文章推荐 - -- [可能是把Java内存区域讲的最清楚的一篇文章](https://juejin.im/post/5b7d69e4e51d4538ca5730cb) -- [搞定JVM垃圾回收就是这么简单](https://juejin.im/post/5b85ea54e51d4538dd08f601) -- [前端&后端程序员必备的Linux基础知识](https://juejin.im/post/5b3b19856fb9a04fa42f8c71) -- [可能是把Docker的概念讲的最清楚的一篇文章](https://juejin.im/post/5b260ec26fb9a00e8e4b031a) - - -# 校园经历(可选) - -## 2016-2017 - -担任学校社团-致深社副会长,主要负责团队每周活动的组建以及每周例会的主持。 - -## 2017-2018 - 担任学校传媒组织:“长江大学在线信息传媒”的副站长以及安卓组成员。主要负责每周例会主持、活动策划以及学校校园通APP的研发工作。 - - -# 技能清单 - -以下均为我熟练使用的技能 - -- Web开发:PHP/Hack/Node -- Web框架:ThinkPHP/Yaf/Yii/Lavarel/LazyPHP -- 前端框架:Bootstrap/AngularJS/EmberJS/HTML5/Cocos2dJS/ionic -- 前端工具:Bower/Gulp/SaSS/LeSS/PhoneGap -- 数据库相关:MySQL/PgSQL/PDO/SQLite -- 版本管理、文档和自动化部署工具:Svn/Git/PHPDoc/Phing/Composer -- 单元测试:PHPUnit/SimpleTest/Qunit -- 云和开放平台:SAE/BAE/AWS/微博开放平台/微信应用开发 - -# 自我评价(可选) - -自我发挥。切记不要过度自夸!!! - - -### 感谢您花时间阅读我的简历,期待能有机会和您共事。 - diff --git "a/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" "b/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" deleted file mode 100644 index 00aaecd..0000000 --- "a/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" +++ /dev/null @@ -1,115 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [何谓悲观锁与乐观锁](#何谓悲观锁与乐观锁) - - [悲观锁](#悲观锁) - - [乐观锁](#乐观锁) - - [两种锁的使用场景](#两种锁的使用场景) -- [乐观锁常见的两种实现方式](#乐观锁常见的两种实现方式) - - [1. 版本号机制](#1-版本号机制) - - [2. CAS算法](#2-cas算法) -- [乐观锁的缺点](#乐观锁的缺点) - - [1 ABA 问题](#1-aba-问题) - - [2 循环时间长开销大](#2-循环时间长开销大) - - [3 只能保证一个共享变量的原子操作](#3-只能保证一个共享变量的原子操作) -- [CAS与synchronized的使用情景](#cas与synchronized的使用情景) - - - -### 何谓悲观锁与乐观锁 - -> 乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。 - -#### 悲观锁 - -总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中`synchronized`和`ReentrantLock`等独占锁就是悲观锁思想的实现。 - - -#### 乐观锁 - -总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。**乐观锁适用于多读的应用类型,这样可以提高吞吐量**,像数据库提供的类似于**write_condition机制**,其实都是提供的乐观锁。在Java中`java.util.concurrent.atomic`包下面的原子变量类就是使用了乐观锁的一种实现方式**CAS**实现的。 - -#### 两种锁的使用场景 - -从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像**乐观锁适用于写比较少的情况下(多读场景)**,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以**一般多写的场景下用悲观锁就比较合适。** - - -### 乐观锁常见的两种实现方式 - -> **乐观锁一般会使用版本号机制或CAS算法实现。** - -#### 1. 版本号机制 - -一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。 - -**举一个简单的例子:** -假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。 - -1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。 -2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。 -3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。 -4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 - -这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。 - -#### 2. CAS算法 - -即**compare and swap(比较与交换)**,是一种有名的**无锁算法**。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。**CAS算法**涉及到三个操作数 - -- 需要读写的内存值 V -- 进行比较的值 A -- 拟写入的新值 B - -当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个**自旋操作**,即**不断的重试**。 - -关于自旋锁,大家可以看一下这篇文章,非常不错:[《 -面试必备之深入理解自旋锁》](https://blog.csdn.net/qq_34337272/article/details/81252853) - -### 乐观锁的缺点 - -> ABA 问题是乐观锁一个常见的问题 - -#### 1 ABA 问题 - -如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 **"ABA"问题。** - -JDK 1.5 以后的 `AtomicStampedReference 类`就提供了此种能力,其中的 `compareAndSet 方法`就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 - -#### 2 循环时间长开销大 -**自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。** 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。 - -#### 3 只能保证一个共享变量的原子操作 - -CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了`AtomicReference类`来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference类`把多个共享变量合并成一个共享变量来操作。 - -### CAS与synchronized的使用情景 - -> **简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)** - -1. 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。 -2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。 - -补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 **“重量级锁”** 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 **偏向锁** 和 **轻量级锁** 以及其它**各种优化**之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 **Lock-Free** 的队列,基本思路是 **自旋后阻塞**,**竞争切换后继续竞争锁**,**稍微牺牲了公平性,但获得了高吞吐量**。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) - - - - - - - - - - - - diff --git a/docs/java/BIO-NIO-AIO.md b/docs/java/BIO-NIO-AIO.md deleted file mode 100644 index 36aac43..0000000 --- a/docs/java/BIO-NIO-AIO.md +++ /dev/null @@ -1,346 +0,0 @@ -熟练掌握 BIO,NIO,AIO 的基本概念以及一些常见问题是你准备面试的过程中不可或缺的一部分,另外这些知识点也是你学习 Netty 的基础。 - - - -- [BIO,NIO,AIO 总结](#bionioaio-总结) - - [1. BIO \(Blocking I/O\)](#1-bio-blocking-io) - - [1.1 传统 BIO](#11-传统-bio) - - [1.2 伪异步 IO](#12-伪异步-io) - - [1.3 代码示例](#13-代码示例) - - [1.4 总结](#14-总结) - - [2. NIO \(New I/O\)](#2-nio-new-io) - - [2.1 NIO 简介](#21-nio-简介) - - [2.2 NIO的特性/NIO与IO区别](#22-nio的特性nio与io区别) - - [1)Non-blocking IO(非阻塞IO)](#1non-blocking-io(非阻塞io)) - - [2)Buffer\(缓冲区\)](#2buffer缓冲区) - - [3)Channel \(通道\)](#3channel-通道) - - [4)Selectors\(选择器\)](#4selectors选择器) - - [2.3 NIO 读数据和写数据方式](#23-nio-读数据和写数据方式) - - [2.4 NIO核心组件简单介绍](#24-nio核心组件简单介绍) - - [2.5 代码示例](#25-代码示例) - - [3. AIO \(Asynchronous I/O\)](#3-aio-asynchronous-io) - - [参考](#参考) - - - - -# BIO,NIO,AIO 总结 - - Java 中的 BIO、NIO和 AIO 理解为是 Java 语言对操作系统的各种 IO 模型的封装。程序员在使用这些 API 的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。只需要使用Java的API就可以了。 - -在讲 BIO,NIO,AIO 之前先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。 - -**同步与异步** - -- **同步:** 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。 -- **异步:** 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。 - -同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。 - -**阻塞和非阻塞** - -- **阻塞:** 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。 -- **非阻塞:** 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。 - -举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在那里傻等着水开(**同步阻塞**)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(**同步非阻塞**)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(**异步非阻塞**)。 - - -## 1. BIO (Blocking I/O) - -同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。 - -### 1.1 传统 BIO - -BIO通信(一请求一应答)模型图如下(图源网络,原出处不明): - -![传统BIO通信模型图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2.png) - -采用 **BIO 通信模型** 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在`while(true)` 循环中服务端会调用 `accept()` 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示。 - -如果要让 **BIO 通信模型** 能够同时处理多个客户端请求,就必须使用多线程(主要原因是`socket.accept()`、`socket.read()`、`socket.write()` 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 **一请求一应答通信模型** 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过 **线程池机制** 改善,线程池还可以让线程的创建和回收成本相对较低。使用`FixedThreadPool` 可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M),下面一节"伪异步 BIO"中会详细介绍到。 - -**我们再设想一下当客户端并发访问量增加后这种模型会出现什么问题?** - -在 Java 虚拟机中,线程是宝贵的资源,线程的创建和销毁成本很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。 - -### 1.2 伪异步 IO - -为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。 - -伪异步IO模型图(图源网络,原出处不明): - -![伪异步IO模型图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/3.png) - -采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。 - -伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层仍然是同步阻塞的BIO模型,因此无法从根本上解决问题。 - -### 1.3 代码示例 - -下面代码中演示了BIO通信(一请求一应答)模型。我们会在客户端创建多个线程依次连接服务端并向其发送"当前时间+:hello world",服务端会为每个客户端线程创建一个线程来处理。代码示例出自闪电侠的博客,原地址如下: - -[https://www.jianshu.com/p/a4e03835921a](https://www.jianshu.com/p/a4e03835921a) - -**客户端** - -```java -/** - * - * @author 闪电侠 - * @date 2018年10月14日 - * @Description:客户端 - */ -public class IOClient { - - public static void main(String[] args) { - // TODO 创建多个线程,模拟多个客户端连接服务端 - new Thread(() -> { - try { - Socket socket = new Socket("127.0.0.1", 3333); - while (true) { - try { - socket.getOutputStream().write((new Date() + ": hello world").getBytes()); - Thread.sleep(2000); - } catch (Exception e) { - } - } - } catch (IOException e) { - } - }).start(); - - } - -} - -``` - -**服务端** - -```java -/** - * @author 闪电侠 - * @date 2018年10月14日 - * @Description: 服务端 - */ -public class IOServer { - - public static void main(String[] args) throws IOException { - // TODO 服务端处理客户端连接请求 - ServerSocket serverSocket = new ServerSocket(3333); - - // 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理 - new Thread(() -> { - while (true) { - try { - // 阻塞方法获取新的连接 - Socket socket = serverSocket.accept(); - - // 每一个新的连接都创建一个线程,负责读取数据 - new Thread(() -> { - try { - int len; - byte[] data = new byte[1024]; - InputStream inputStream = socket.getInputStream(); - // 按字节流方式读取数据 - while ((len = inputStream.read(data)) != -1) { - System.out.println(new String(data, 0, len)); - } - } catch (IOException e) { - } - }).start(); - - } catch (IOException e) { - } - - } - }).start(); - - } - -} -``` - -### 1.4 总结 - -在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 - - - -## 2. NIO (New I/O) - -### 2.1 NIO 简介 - - NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。 - -NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。 - -### 2.2 NIO的特性/NIO与IO区别 - -如果是在面试中回答这个问题,我觉得首先肯定要从 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 说起。然后,可以从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。如果,你把这些都回答上了我觉得你对于 NIO 就有了更为深入一点的认识,面试官问到你这个问题,你也能很轻松的回答上来了。 - -#### 1)Non-blocking IO(非阻塞IO) - -**IO流是阻塞的,NIO流是不阻塞的。** - -Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 - -Java IO的各种流是阻塞的。这意味着,当一个线程调用 `read()` 或 `write()` 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了 - -#### 2)Buffer(缓冲区) - -**IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。** - -Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。 - -在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。 - -最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。 - -#### 3)Channel (通道) - -NIO 通过Channel(通道) 进行读写。 - -通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。 - -#### 4)Selector (选择器) - -NIO有选择器,而IO没有。 - -选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。 - -![一个单线程中Selector维护3个Channel的示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/Slector.png) - -### 2.3 NIO 读数据和写数据方式 -通常来说NIO中的所有IO都是从 Channel(通道) 开始的。 - -- 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。 -- 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。 - -数据读取和写入操作图示: - -![NIO读写数据的方式](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/NIO读写数据的方式.png) - - -### 2.4 NIO核心组件简单介绍 - -NIO 包含下面几个核心的组件: - -- Channel(通道) -- Buffer(缓冲区) -- Selector(选择器) - -整个NIO体系包含的类远远不止这三个,只能说这三个是NIO体系的“核心API”。我们上面已经对这三个概念进行了基本的阐述,这里就不多做解释了。 - -### 2.5 代码示例 - -代码示例出自闪电侠的博客,原地址如下: - -[https://www.jianshu.com/p/a4e03835921a](https://www.jianshu.com/p/a4e03835921a) - -客户端 IOClient.java 的代码不变,我们对服务端使用 NIO 进行改造。以下代码较多而且逻辑比较复杂,大家看看就好。 - -```java -/** - * - * @author 闪电侠 - * @date 2019年2月21日 - * @Description: NIO 改造后的服务端 - */ -public class NIOServer { - public static void main(String[] args) throws IOException { - // 1. serverSelector负责轮询是否有新的连接,服务端监测到新的连接之后,不再创建一个新的线程, - // 而是直接将新连接绑定到clientSelector上,这样就不用 IO 模型中 1w 个 while 循环在死等 - Selector serverSelector = Selector.open(); - // 2. clientSelector负责轮询连接是否有数据可读 - Selector clientSelector = Selector.open(); - - new Thread(() -> { - try { - // 对应IO编程中服务端启动 - ServerSocketChannel listenerChannel = ServerSocketChannel.open(); - listenerChannel.socket().bind(new InetSocketAddress(3333)); - listenerChannel.configureBlocking(false); - listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT); - - while (true) { - // 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms - if (serverSelector.select(1) > 0) { - Set set = serverSelector.selectedKeys(); - Iterator keyIterator = set.iterator(); - - while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); - - if (key.isAcceptable()) { - try { - // (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector - SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept(); - clientChannel.configureBlocking(false); - clientChannel.register(clientSelector, SelectionKey.OP_READ); - } finally { - keyIterator.remove(); - } - } - - } - } - } - } catch (IOException ignored) { - } - }).start(); - new Thread(() -> { - try { - while (true) { - // (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms - if (clientSelector.select(1) > 0) { - Set set = clientSelector.selectedKeys(); - Iterator keyIterator = set.iterator(); - - while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); - - if (key.isReadable()) { - try { - SocketChannel clientChannel = (SocketChannel) key.channel(); - ByteBuffer byteBuffer = ByteBuffer.allocate(1024); - // (3) 面向 Buffer - clientChannel.read(byteBuffer); - byteBuffer.flip(); - System.out.println( - Charset.defaultCharset().newDecoder().decode(byteBuffer).toString()); - } finally { - keyIterator.remove(); - key.interestOps(SelectionKey.OP_READ); - } - } - - } - } - } - } catch (IOException ignored) { - } - }).start(); - - } -} -``` - -为什么大家都不愿意用 JDK 原生 NIO 进行开发呢?从上面的代码中大家都可以看出来,是真的难用!除了编程复杂、编程模型难之外,它还有以下让人诟病的问题: - -- JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100% -- 项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高,上面这一坨代码我都不能保证没有 bug - -Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题。 - -### 3. AIO (Asynchronous I/O) - -AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。 - -AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。(除了 AIO 其他的 IO 类型都是同步的,这一点可以从底层IO线程模型解释,推荐一篇文章:[《漫话:如何给女朋友解释什么是Linux的五种IO模型?》](https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41#wechat_redirect) ) - -查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 - -## 参考 - -- 《Netty 权威指南》第二版 -- https://zhuanlan.zhihu.com/p/23488863 (美团技术团队) diff --git a/docs/java/Basis/Arrays,CollectionsCommonMethods.md b/docs/java/Basis/Arrays,CollectionsCommonMethods.md deleted file mode 100644 index 0710de4..0000000 --- a/docs/java/Basis/Arrays,CollectionsCommonMethods.md +++ /dev/null @@ -1,383 +0,0 @@ - - -- [Collections 工具类和 Arrays 工具类常见方法](#collections-工具类和-arrays-工具类常见方法) - - [Collections](#collections) - - [排序操作](#排序操作) - - [查找,替换操作](#查找替换操作) - - [同步控制](#同步控制) - - [Arrays类的常见操作](#arrays类的常见操作) - - [排序 : `sort()`](#排序--sort) - - [查找 : `binarySearch()`](#查找--binarysearch) - - [比较: `equals()`](#比较-equals) - - [填充 : `fill()`](#填充--fill) - - [转列表 `asList()`](#转列表-aslist) - - [转字符串 `toString()`](#转字符串-tostring) - - [复制 `copyOf()`](#复制-copyof) - - -# Collections 工具类和 Arrays 工具类常见方法 - -## Collections - -Collections 工具类常用方法: - -1. 排序 -2. 查找,替换操作 -3. 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合) - -### 排序操作 - -```java -void reverse(List list)//反转 -void shuffle(List list)//随机排序 -void sort(List list)//按自然排序的升序排序 -void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑 -void swap(List list, int i , int j)//交换两个索引位置的元素 -void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。 -``` - -**示例代码:** - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - System.out.println("原始数组:"); - System.out.println(arrayList); - // void reverse(List list):反转 - Collections.reverse(arrayList); - System.out.println("Collections.reverse(arrayList):"); - System.out.println(arrayList); - - - Collections.rotate(arrayList, 4); - System.out.println("Collections.rotate(arrayList, 4):"); - System.out.println(arrayList); - - // void sort(List list),按自然排序的升序排序 - Collections.sort(arrayList); - System.out.println("Collections.sort(arrayList):"); - System.out.println(arrayList); - - // void shuffle(List list),随机排序 - Collections.shuffle(arrayList); - System.out.println("Collections.shuffle(arrayList):"); - System.out.println(arrayList); - - // void swap(List list, int i , int j),交换两个索引位置的元素 - Collections.swap(arrayList, 2, 5); - System.out.println("Collections.swap(arrayList, 2, 5):"); - System.out.println(arrayList); - - // 定制排序的用法 - Collections.sort(arrayList, new Comparator() { - - @Override - public int compare(Integer o1, Integer o2) { - return o2.compareTo(o1); - } - }); - System.out.println("定制排序后:"); - System.out.println(arrayList); -``` - -### 查找,替换操作 - -```java -int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的 -int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll) -int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c) -void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素。 -int frequency(Collection c, Object o)//统计元素出现次数 -int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target). -boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素 -``` - -**示例代码:** - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - ArrayList arrayList2 = new ArrayList(); - arrayList2.add(-3); - arrayList2.add(-5); - arrayList2.add(7); - System.out.println("原始数组:"); - System.out.println(arrayList); - - System.out.println("Collections.max(arrayList):"); - System.out.println(Collections.max(arrayList)); - - System.out.println("Collections.min(arrayList):"); - System.out.println(Collections.min(arrayList)); - - System.out.println("Collections.replaceAll(arrayList, 3, -3):"); - Collections.replaceAll(arrayList, 3, -3); - System.out.println(arrayList); - - System.out.println("Collections.frequency(arrayList, -3):"); - System.out.println(Collections.frequency(arrayList, -3)); - - System.out.println("Collections.indexOfSubList(arrayList, arrayList2):"); - System.out.println(Collections.indexOfSubList(arrayList, arrayList2)); - - System.out.println("Collections.binarySearch(arrayList, 7):"); - // 对List进行二分查找,返回索引,List必须是有序的 - Collections.sort(arrayList); - System.out.println(Collections.binarySearch(arrayList, 7)); -``` - -### 同步控制 - -Collections提供了多个`synchronizedXxx()`方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。 - -我们知道 HashSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。Collections提供了多个静态方法可以把他们包装成线程同步的集合。 - -**最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。** - -方法如下: - -```java -synchronizedCollection(Collection c) //返回指定 collection 支持的同步(线程安全的)collection。 -synchronizedList(List list)//返回指定列表支持的同步(线程安全的)List。 -synchronizedMap(Map m) //返回由指定映射支持的同步(线程安全的)Map。 -synchronizedSet(Set s) //返回指定 set 支持的同步(线程安全的)set。 -``` - -### Collections还可以设置不可变集合,提供了如下三类方法: - -```java -emptyXxx(): 返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是Set,还可以是Map。 -singletonXxx(): 返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以是:List,Set,Map。 -unmodifiableXxx(): 返回指定集合对象的不可变视图,此处的集合可以是:List,Set,Map。 -上面三类方法的参数是原有的集合对象,返回值是该集合的”只读“版本。 -``` - -**示例代码:** - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - HashSet integers1 = new HashSet<>(); - integers1.add(1); - integers1.add(3); - integers1.add(2); - Map scores = new HashMap(); - scores.put("语文" , 80); - scores.put("Java" , 82); - - //Collections.emptyXXX();创建一个空的、不可改变的XXX对象 - List list = Collections.emptyList(); - System.out.println(list);//[] - Set objects = Collections.emptySet(); - System.out.println(objects);//[] - Map objectObjectMap = Collections.emptyMap(); - System.out.println(objectObjectMap);//{} - - //Collections.singletonXXX(); - List> arrayLists = Collections.singletonList(arrayList); - System.out.println(arrayLists);//[[-1, 3, 3, -5, 7, 4, -9, -7]] - //创建一个只有一个元素,且不可改变的Set对象 - Set> singleton = Collections.singleton(arrayList); - System.out.println(singleton);//[[-1, 3, 3, -5, 7, 4, -9, -7]] - Map nihao = Collections.singletonMap("1", "nihao"); - System.out.println(nihao);//{1=nihao} - - //unmodifiableXXX();创建普通XXX对象对应的不可变版本 - List integers = Collections.unmodifiableList(arrayList); - System.out.println(integers);//[-1, 3, 3, -5, 7, 4, -9, -7] - Set integers2 = Collections.unmodifiableSet(integers1); - System.out.println(integers2);//[1, 2, 3] - Map objectObjectMap2 = Collections.unmodifiableMap(scores); - System.out.println(objectObjectMap2);//{Java=82, 语文=80} - - //添加出现异常:java.lang.UnsupportedOperationException -// list.add(1); -// arrayLists.add(arrayList); -// integers.add(1); -``` - -## Arrays类的常见操作 -1. 排序 : `sort()` -2. 查找 : `binarySearch()` -3. 比较: `equals()` -4. 填充 : `fill()` -5. 转列表: `asList()` -6. 转字符串 : `toString()` -7. 复制: `copyOf()` - - -### 排序 : `sort()` - -```java - // *************排序 sort**************** - int a[] = { 1, 3, 2, 7, 6, 5, 4, 9 }; - // sort(int[] a)方法按照数字顺序排列指定的数组。 - Arrays.sort(a); - System.out.println("Arrays.sort(a):"); - for (int i : a) { - System.out.print(i); - } - // 换行 - System.out.println(); - - // sort(int[] a,int fromIndex,int toIndex)按升序排列数组的指定范围 - int b[] = { 1, 3, 2, 7, 6, 5, 4, 9 }; - Arrays.sort(b, 2, 6); - System.out.println("Arrays.sort(b, 2, 6):"); - for (int i : b) { - System.out.print(i); - } - // 换行 - System.out.println(); - - int c[] = { 1, 3, 2, 7, 6, 5, 4, 9 }; - // parallelSort(int[] a) 按照数字顺序排列指定的数组(并行的)。同sort方法一样也有按范围的排序 - Arrays.parallelSort(c); - System.out.println("Arrays.parallelSort(c):"); - for (int i : c) { - System.out.print(i); - } - // 换行 - System.out.println(); - - // parallelSort给字符数组排序,sort也可以 - char d[] = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - Arrays.parallelSort(d); - System.out.println("Arrays.parallelSort(d):"); - for (char d2 : d) { - System.out.print(d2); - } - // 换行 - System.out.println(); - -``` - -在做算法面试题的时候,我们还可能会经常遇到对字符串排序的情况,`Arrays.sort()` 对每个字符串的特定位置进行比较,然后按照升序排序。 - -```java -String[] strs = { "abcdehg", "abcdefg", "abcdeag" }; -Arrays.sort(strs); -System.out.println(Arrays.toString(strs));//[abcdeag, abcdefg, abcdehg] -``` - -### 查找 : `binarySearch()` - -```java - // *************查找 binarySearch()**************** - char[] e = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - // 排序后再进行二分查找,否则找不到 - Arrays.sort(e); - System.out.println("Arrays.sort(e)" + Arrays.toString(e)); - System.out.println("Arrays.binarySearch(e, 'c'):"); - int s = Arrays.binarySearch(e, 'c'); - System.out.println("字符c在数组的位置:" + s); -``` - -### 比较: `equals()` - -```java - // *************比较 equals**************** - char[] e = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - char[] f = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - /* - * 元素数量相同,并且相同位置的元素相同。 另外,如果两个数组引用都是null,则它们被认为是相等的 。 - */ - // 输出true - System.out.println("Arrays.equals(e, f):" + Arrays.equals(e, f)); -``` - -### 填充 : `fill()` - -```java - // *************填充fill(批量初始化)**************** - int[] g = { 1, 2, 3, 3, 3, 3, 6, 6, 6 }; - // 数组中所有元素重新分配值 - Arrays.fill(g, 3); - System.out.println("Arrays.fill(g, 3):"); - // 输出结果:333333333 - for (int i : g) { - System.out.print(i); - } - // 换行 - System.out.println(); - - int[] h = { 1, 2, 3, 3, 3, 3, 6, 6, 6, }; - // 数组中指定范围元素重新分配值 - Arrays.fill(h, 0, 2, 9); - System.out.println("Arrays.fill(h, 0, 2, 9);:"); - // 输出结果:993333666 - for (int i : h) { - System.out.print(i); - } -``` - -### 转列表 `asList()` - -```java - // *************转列表 asList()**************** - /* - * 返回由指定数组支持的固定大小的列表。 - * (将返回的列表更改为“写入数组”。)该方法作为基于数组和基于集合的API之间的桥梁,与Collection.toArray()相结合 。 - * 返回的列表是可序列化的,并实现RandomAccess 。 - * 此方法还提供了一种方便的方式来创建一个初始化为包含几个元素的固定大小的列表如下: - */ - List stooges = Arrays.asList("Larry", "Moe", "Curly"); - System.out.println(stooges); -``` - -### 转字符串 `toString()` - -```java - // *************转字符串 toString()**************** - /* - * 返回指定数组的内容的字符串表示形式。 - */ - char[] k = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - System.out.println(Arrays.toString(k));// [a, f, b, c, e, A, C, B] -``` - -### 复制 `copyOf()` - -```java - // *************复制 copy**************** - // copyOf 方法实现数组复制,h为数组,6为复制的长度 - int[] h = { 1, 2, 3, 3, 3, 3, 6, 6, 6, }; - int i[] = Arrays.copyOf(h, 6); - System.out.println("Arrays.copyOf(h, 6);:"); - // 输出结果:123333 - for (int j : i) { - System.out.print(j); - } - // 换行 - System.out.println(); - // copyOfRange将指定数组的指定范围复制到新数组中 - int j[] = Arrays.copyOfRange(h, 6, 11); - System.out.println("Arrays.copyOfRange(h, 6, 11):"); - // 输出结果66600(h数组只有9个元素这里是从索引6到索引11复制所以不足的就为0) - for (int j2 : j) { - System.out.print(j2); - } - // 换行 - System.out.println(); -``` diff --git "a/docs/java/Basis/final\343\200\201static\343\200\201this\343\200\201super.md" "b/docs/java/Basis/final\343\200\201static\343\200\201this\343\200\201super.md" deleted file mode 100644 index 77e8b09..0000000 --- "a/docs/java/Basis/final\343\200\201static\343\200\201this\343\200\201super.md" +++ /dev/null @@ -1,342 +0,0 @@ - - -- [final,static,this,super 关键字总结](#finalstaticthissuper-关键字总结) - - [final 关键字](#final-关键字) - - [static 关键字](#static-关键字) - - [this 关键字](#this-关键字) - - [super 关键字](#super-关键字) - - [参考](#参考) -- [static 关键字详解](#static-关键字详解) - - [static 关键字主要有以下四种使用场景](#static-关键字主要有以下四种使用场景) - - [修饰成员变量和成员方法\(常用\)](#修饰成员变量和成员方法常用) - - [静态代码块](#静态代码块) - - [静态内部类](#静态内部类) - - [静态导包](#静态导包) - - [补充内容](#补充内容) - - [静态方法与非静态方法](#静态方法与非静态方法) - - [static{}静态代码块与{}非静态代码块\(构造代码块\)](#static静态代码块与非静态代码块构造代码块) - - [参考](#参考-1) - - - -# final,static,this,super 关键字总结 - -## final 关键字 - -**final关键字主要用在三个地方:变量、方法、类。** - -1. **对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。** - -2. **当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。** - -3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 - -## static 关键字 - -**static 关键字主要有以下四种使用场景:** - -1. **修饰成员变量和成员方法:** 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:`类名.静态变量名` `类名.静态方法名()` -2. **静态代码块:** 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次. -3. **静态内部类(static修饰类的话只能修饰内部类):** 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。 -4. **静态导包(用来导入类中的静态资源,1.5之后的新特性):** 格式为:`import static` 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。 - -## this 关键字 - -this关键字用于引用类的当前实例。 例如: - -```java -class Manager { - Employees[] employees; - - void manageEmployees() { - int totalEmp = this.employees.length; - System.out.println("Total employees: " + totalEmp); - this.report(); - } - - void report() { } -} -``` - -在上面的示例中,this关键字用于两个地方: - -- this.employees.length:访问类Manager的当前实例的变量。 -- this.report():调用类Manager的当前实例的方法。 - -此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂。 - -## super 关键字 - -super关键字用于从子类访问父类的变量和方法。 例如: - -```java -public class Super { - protected int number; - - protected showNumber() { - System.out.println("number = " + number); - } -} - -public class Sub extends Super { - void bar() { - super.number = 10; - super.showNumber(); - } -} -``` - -在上面的例子中,Sub 类访问父类成员变量 number 并调用其其父类 Super 的 `showNumber()` 方法。 - -**使用 this 和 super 要注意的问题:** - -- 在构造器中使用 `super()` 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 -- this、super不能用在static方法中。 - -**简单解释一下:** - -被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, **this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西**。 - -## 参考 - -- https://www.codejava.net/java-core/the-java-language/java-keywords -- https://blog.csdn.net/u013393958/article/details/79881037 - -# static 关键字详解 - -## static 关键字主要有以下四种使用场景 - -1. 修饰成员变量和成员方法 -2. 静态代码块 -3. 修饰类(只能修饰内部类) -4. 静态导包(用来导入类中的静态资源,1.5之后的新特性) - -### 修饰成员变量和成员方法(常用) - -被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。 - -方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。 - - HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。 - -调用格式: - -- 类名.静态变量名 -- 类名.静态方法名() - -如果变量或者方法被 private 则代表该属性或者该方法只能在类的内部被访问而不能在类的外部被访问。 - -测试方法: - -```java -public class StaticBean { - - String name; - 静态变量 - static int age; - - public StaticBean(String name) { - this.name = name; - } - 静态方法 - static void SayHello() { - System.out.println(Hello i am java); - } - @Override - public String toString() { - return StaticBean{ + - name=' + name + ''' + age + age + - '}'; - } -} -``` - -```java -public class StaticDemo { - - public static void main(String[] args) { - StaticBean staticBean = new StaticBean(1); - StaticBean staticBean2 = new StaticBean(2); - StaticBean staticBean3 = new StaticBean(3); - StaticBean staticBean4 = new StaticBean(4); - StaticBean.age = 33; - StaticBean{name='1'age33} StaticBean{name='2'age33} StaticBean{name='3'age33} StaticBean{name='4'age33} - System.out.println(staticBean+ +staticBean2+ +staticBean3+ +staticBean4); - StaticBean.SayHello();Hello i am java - } - -} -``` - - -### 静态代码块 - -静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。 该类不管创建多少对象,静态代码块只执行一次. - -静态代码块的格式是 - -``` -static { -语句体; -} -``` - - -一个类中的静态代码块可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。 - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-14/88531075.jpg) - -静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问. - - -### 静态内部类 - -静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着: - -1. 它的创建是不需要依赖外围类的创建。 -2. 它不能使用任何外围类的非static成员变量和方法。 - - -Example(静态内部类实现单例模式) - -```java -public class Singleton { - - 声明为 private 避免调用默认构造方法创建对象 - private Singleton() { - } - - 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问 - private static class SingletonHolder { - private static final Singleton INSTANCE = new Singleton(); - } - - public static Singleton getUniqueInstance() { - return SingletonHolder.INSTANCE; - } -} -``` - -当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 `getUniqueInstance() `方法从而触发 `SingletonHolder.INSTANCE` 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。 - -这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。 - -### 静态导包 - -格式为:import static - -这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法 - -```java - - - Math. --- 将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用 - 如果只想导入单一某个静态方法,只需要将换成对应的方法名即可 - -import static java.lang.Math.; - - 换成import static java.lang.Math.max;具有一样的效果 - -public class Demo { - public static void main(String[] args) { - - int max = max(1,2); - System.out.println(max); - } -} - -``` - - -## 补充内容 - -### 静态方法与非静态方法 - -静态方法属于类本身,非静态方法属于从该类生成的每个对象。 如果您的方法执行的操作不依赖于其类的各个变量和方法,请将其设置为静态(这将使程序的占用空间更小)。 否则,它应该是非静态的。 - -Example - -```java -class Foo { - int i; - public Foo(int i) { - this.i = i; - } - - public static String method1() { - return An example string that doesn't depend on i (an instance variable); - - } - - public int method2() { - return this.i + 1; Depends on i - } - -} -``` -你可以像这样调用静态方法:`Foo.method1()`。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行:`Foo bar = new Foo(1);bar.method2();` - -总结: - -- 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 -- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制 - -### static{}静态代码块与{}非静态代码块(构造代码块) - -相同点: 都是在JVM加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些static变量进行赋值。 - -不同点: 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。 - -一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:Arrays类,Character类,String类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的. - -Example - -```java -public class Test { - public Test() { - System.out.print(默认构造方法!--); - } - - 非静态代码块 - { - System.out.print(非静态代码块!--); - } - 静态代码块 - static { - System.out.print(静态代码块!--); - } - - public static void test() { - System.out.print(静态方法中的内容! --); - { - System.out.print(静态方法中的代码块!--); - } - - } - public static void main(String[] args) { - - Test test = new Test(); - Test.test();静态代码块!--静态方法中的内容! --静态方法中的代码块!-- - } -``` - -当执行 `Test.test();` 时输出: - -``` -静态代码块!--静态方法中的内容! --静态方法中的代码块!-- -``` - -当执行 `Test test = new Test();` 时输出: - -``` -静态代码块!--非静态代码块!--默认构造方法!-- -``` - - -非静态代码块与构造函数的区别是: 非静态代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。 - -### 参考 - -- httpsblog.csdn.netchen13579867831articledetails78995480 -- httpwww.cnblogs.comchenssyp3388487.html -- httpwww.cnblogs.comQian123p5713440.html diff --git "a/docs/java/Basis/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276,\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225!.md" "b/docs/java/Basis/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276,\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225!.md" deleted file mode 100644 index 20b0229..0000000 --- "a/docs/java/Basis/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276,\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225!.md" +++ /dev/null @@ -1,561 +0,0 @@ -> 最近重看 Java 枚举,看到这篇觉得还不错的文章,于是简单翻译和完善了一些内容,分享给大家,希望你们也能有所收获。另外,不要忘了文末还有补充哦! -> -> ps: 这里发一篇枚举的文章,也是因为后面要发一篇非常实用的关于 SpringBoot 全局异常处理的比较好的实践,里面就用到了枚举。 -> -> 这篇文章由 JavaGuide 翻译,公众号: JavaGuide,原文地址:https://www.baeldung.com/a-guide-to-java-enums 。 -> -> 转载请注明上面这段文字。 - -## 1.概览 - -在本文中,我们将看到什么是 Java 枚举,它们解决了哪些问题以及如何在实践中使用 Java 枚举实现一些设计模式。 - -enum关键字在 java5 中引入,表示一种特殊类型的类,其总是继承java.lang.Enum类,更多内容可以自行查看其[官方文档](https://docs.oracle.com/javase/6/docs/api/java/lang/Enum.html)。 - -枚举在很多时候会和常量拿来对比,可能因为本身我们大量实际使用枚举的地方就是为了替代常量。那么这种方式由什么优势呢? - -**以这种方式定义的常量使代码更具可读性,允许进行编译时检查,预先记录可接受值的列表,并避免由于传入无效值而引起的意外行为。** - -下面示例定义一个简单的枚举类型 pizza 订单的状态,共有三种 ORDERED, READY, DELIVERED状态: - -```java -package shuang.kou.enumdemo.enumtest; - -public enum PizzaStatus { - ORDERED, - READY, - DELIVERED; -} -``` - -**简单来说,我们通过上面的代码避免了定义常量,我们将所有和 pizza 订单的状态的常量都统一放到了一个枚举类型里面。** - -```java -System.out.println(PizzaStatus.ORDERED.name());//ORDERED -System.out.println(PizzaStatus.ORDERED);//ORDERED -System.out.println(PizzaStatus.ORDERED.name().getClass());//class java.lang.String -System.out.println(PizzaStatus.ORDERED.getClass());//class shuang.kou.enumdemo.enumtest.PizzaStatus -``` - -## 2.自定义枚举方法 - -现在我们对枚举是什么以及如何使用它们有了基本的了解,让我们通过在枚举上定义一些额外的API方法,将上一个示例提升到一个新的水平: - -```java -public class Pizza { - private PizzaStatus status; - public enum PizzaStatus { - ORDERED, - READY, - DELIVERED; - } - - public boolean isDeliverable() { - if (getStatus() == PizzaStatus.READY) { - return true; - } - return false; - } - - // Methods that set and get the status variable. -} -``` - -## 3.使用 == 比较枚举类型 - -由于枚举类型确保JVM中仅存在一个常量实例,因此我们可以安全地使用“ ==”运算符比较两个变量,如上例所示;此外,“ ==”运算符可提供编译时和运行时的安全性。 - -首先,让我们看一下以下代码段中的运行时安全性,其中“ ==”运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException: - -```java -if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); -if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED); -``` - -对于编译时安全性,我们看另一个示例,两个不同枚举类型进行比较,使用equal方法比较结果确定为true,因为getStatus方法的枚举值与另一个类型枚举值一致,但逻辑上应该为false。这个问题可以使用==操作符避免。因为编译器会表示类型不兼容错误: - -```java -if(testPz.getStatus().equals(TestColor.GREEN)); -if(testPz.getStatus() == TestColor.GREEN); -``` - -## 4.在 switch 语句中使用枚举类型 - -```java -public int getDeliveryTimeInDays() { - switch (status) { - case ORDERED: return 5; - case READY: return 2; - case DELIVERED: return 0; - } - return 0; -} -``` - -## 5.枚举类型的属性,方法和构造函数 - -> 文末有我(JavaGuide)的补充。 - -你可以通过在枚举类型中定义属性,方法和构造函数让它变得更加强大。 - -下面,让我们扩展上面的示例,实现从比萨的一个阶段到另一个阶段的过渡,并了解如何摆脱之前使用的if语句和switch语句: - -```java -public class Pizza { - - private PizzaStatus status; - public enum PizzaStatus { - ORDERED (5){ - @Override - public boolean isOrdered() { - return true; - } - }, - READY (2){ - @Override - public boolean isReady() { - return true; - } - }, - DELIVERED (0){ - @Override - public boolean isDelivered() { - return true; - } - }; - - private int timeToDelivery; - - public boolean isOrdered() {return false;} - - public boolean isReady() {return false;} - - public boolean isDelivered(){return false;} - - public int getTimeToDelivery() { - return timeToDelivery; - } - - PizzaStatus (int timeToDelivery) { - this.timeToDelivery = timeToDelivery; - } - } - - public boolean isDeliverable() { - return this.status.isReady(); - } - - public void printTimeToDeliver() { - System.out.println("Time to delivery is " + - this.getStatus().getTimeToDelivery()); - } - - // Methods that set and get the status variable. -} -``` - -下面这段代码展示它是如何 work 的: - -```java -@Test -public void givenPizaOrder_whenReady_thenDeliverable() { - Pizza testPz = new Pizza(); - testPz.setStatus(Pizza.PizzaStatus.READY); - assertTrue(testPz.isDeliverable()); -} -``` - -## 6.EnumSet and EnumMap - -### 6.1. EnumSet - -`EnumSet` 是一种专门为枚举类型所设计的 `Set` 类型。 - -与`HashSet`相比,由于使用了内部位向量表示,因此它是特定 `Enum` 常量集的非常有效且紧凑的表示形式。 - -它提供了类型安全的替代方法,以替代传统的基于int的“位标志”,使我们能够编写更易读和易于维护的简洁代码。 - -`EnumSet` 是抽象类,其有两个实现:`RegularEnumSet` 、`JumboEnumSet`,选择哪一个取决于实例化时枚举中常量的数量。 - -在很多场景中的枚举常量集合操作(如:取子集、增加、删除、`containsAll`和`removeAll`批操作)使用`EnumSet`非常合适;如果需要迭代所有可能的常量则使用`Enum.values()`。 - -```java -public class Pizza { - - private static EnumSet undeliveredPizzaStatuses = - EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY); - - private PizzaStatus status; - - public enum PizzaStatus { - ... - } - - public boolean isDeliverable() { - return this.status.isReady(); - } - - public void printTimeToDeliver() { - System.out.println("Time to delivery is " + - this.getStatus().getTimeToDelivery() + " days"); - } - - public static List getAllUndeliveredPizzas(List input) { - return input.stream().filter( - (s) -> undeliveredPizzaStatuses.contains(s.getStatus())) - .collect(Collectors.toList()); - } - - public void deliver() { - if (isDeliverable()) { - PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy() - .deliver(this); - this.setStatus(PizzaStatus.DELIVERED); - } - } - - // Methods that set and get the status variable. -} -``` - - 下面的测试演示了展示了 `EnumSet` 在某些场景下的强大功能: - -```java -@Test -public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() { - List pzList = new ArrayList<>(); - Pizza pz1 = new Pizza(); - pz1.setStatus(Pizza.PizzaStatus.DELIVERED); - - Pizza pz2 = new Pizza(); - pz2.setStatus(Pizza.PizzaStatus.ORDERED); - - Pizza pz3 = new Pizza(); - pz3.setStatus(Pizza.PizzaStatus.ORDERED); - - Pizza pz4 = new Pizza(); - pz4.setStatus(Pizza.PizzaStatus.READY); - - pzList.add(pz1); - pzList.add(pz2); - pzList.add(pz3); - pzList.add(pz4); - - List undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList); - assertTrue(undeliveredPzs.size() == 3); -} -``` - -### 6.2. EnumMap - -`EnumMap`是一个专门化的映射实现,用于将枚举常量用作键。与对应的 `HashMap` 相比,它是一个高效紧凑的实现,并且在内部表示为一个数组: - -```java -EnumMap map; -``` - -让我们快速看一个真实的示例,该示例演示如何在实践中使用它: - -```java -public static EnumMap> - groupPizzaByStatus(List pizzaList) { - EnumMap> pzByStatus = - new EnumMap>(PizzaStatus.class); - - for (Pizza pz : pizzaList) { - PizzaStatus status = pz.getStatus(); - if (pzByStatus.containsKey(status)) { - pzByStatus.get(status).add(pz); - } else { - List newPzList = new ArrayList(); - newPzList.add(pz); - pzByStatus.put(status, newPzList); - } - } - return pzByStatus; -} -``` - - 下面的测试演示了展示了 `EnumMap` 在某些场景下的强大功能: - -```java -@Test -public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() { - List pzList = new ArrayList<>(); - Pizza pz1 = new Pizza(); - pz1.setStatus(Pizza.PizzaStatus.DELIVERED); - - Pizza pz2 = new Pizza(); - pz2.setStatus(Pizza.PizzaStatus.ORDERED); - - Pizza pz3 = new Pizza(); - pz3.setStatus(Pizza.PizzaStatus.ORDERED); - - Pizza pz4 = new Pizza(); - pz4.setStatus(Pizza.PizzaStatus.READY); - - pzList.add(pz1); - pzList.add(pz2); - pzList.add(pz3); - pzList.add(pz4); - - EnumMap> map = Pizza.groupPizzaByStatus(pzList); - assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1); - assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2); - assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1); -} -``` - -## 7. 通过枚举实现一些设计模式 - -### 7.1 单例模式 - -通常,使用类实现 Singleton 模式并非易事,枚举提供了一种实现单例的简便方法。 - -《Effective Java 》和《Java与模式》都非常推荐这种方式,使用这种方式方式实现枚举可以有什么好处呢? - -《Effective Java》 - -> 这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton的最佳方法。 —-《Effective Java 中文版 第二版》 - -《Java与模式》 - -> 《Java与模式》中,作者这样写道,使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。 - -下面的代码段显示了如何使用枚举实现单例模式: - -```java -public enum PizzaDeliverySystemConfiguration { - INSTANCE; - PizzaDeliverySystemConfiguration() { - // Initialization configuration which involves - // overriding defaults like delivery strategy - } - - private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL; - - public static PizzaDeliverySystemConfiguration getInstance() { - return INSTANCE; - } - - public PizzaDeliveryStrategy getDeliveryStrategy() { - return deliveryStrategy; - } -} -``` - -如何使用呢?请看下面的代码: - -```java -PizzaDeliveryStrategy deliveryStrategy = PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy(); -``` - -通过 `PizzaDeliverySystemConfiguration.getInstance()` 获取的就是单例的 `PizzaDeliverySystemConfiguration` - -### 7.2 策略模式 - -通常,策略模式由不同类实现同一个接口来实现的。 - - 这也就意味着添加新策略意味着添加新的实现类。使用枚举,可以轻松完成此任务,添加新的实现意味着只定义具有某个实现的另一个实例。 - -下面的代码段显示了如何使用枚举实现策略模式: - -```java -public enum PizzaDeliveryStrategy { - EXPRESS { - @Override - public void deliver(Pizza pz) { - System.out.println("Pizza will be delivered in express mode"); - } - }, - NORMAL { - @Override - public void deliver(Pizza pz) { - System.out.println("Pizza will be delivered in normal mode"); - } - }; - - public abstract void deliver(Pizza pz); -} -``` - -给 `Pizza `增加下面的方法: - -```java -public void deliver() { - if (isDeliverable()) { - PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy() - .deliver(this); - this.setStatus(PizzaStatus.DELIVERED); - } -} -``` - -如何使用呢?请看下面的代码: - -```java -@Test -public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() { - Pizza pz = new Pizza(); - pz.setStatus(Pizza.PizzaStatus.READY); - pz.deliver(); - assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED); -} -``` - -## 8. Java 8 与枚举 - -Pizza 类可以用Java 8重写,您可以看到方法 lambda 和Stream API如何使 `getAllUndeliveredPizzas()`和`groupPizzaByStatus()`方法变得如此简洁: - -`getAllUndeliveredPizzas()`: - -```java -public static List getAllUndeliveredPizzas(List input) { - return input.stream().filter( - (s) -> !deliveredPizzaStatuses.contains(s.getStatus())) - .collect(Collectors.toList()); -} -``` - -`groupPizzaByStatus()` : - -```java -public static EnumMap> - groupPizzaByStatus(List pzList) { - EnumMap> map = pzList.stream().collect( - Collectors.groupingBy(Pizza::getStatus, - () -> new EnumMap<>(PizzaStatus.class), Collectors.toList())); - return map; -} -``` - -## 9. Enum 类型的 JSON 表现形式 - -使用Jackson库,可以将枚举类型的JSON表示为POJO。下面的代码段显示了可以用于同一目的的Jackson批注: - -```java -@JsonFormat(shape = JsonFormat.Shape.OBJECT) -public enum PizzaStatus { - ORDERED (5){ - @Override - public boolean isOrdered() { - return true; - } - }, - READY (2){ - @Override - public boolean isReady() { - return true; - } - }, - DELIVERED (0){ - @Override - public boolean isDelivered() { - return true; - } - }; - - private int timeToDelivery; - - public boolean isOrdered() {return false;} - - public boolean isReady() {return false;} - - public boolean isDelivered(){return false;} - - @JsonProperty("timeToDelivery") - public int getTimeToDelivery() { - return timeToDelivery; - } - - private PizzaStatus (int timeToDelivery) { - this.timeToDelivery = timeToDelivery; - } -} -``` - -我们可以按如下方式使用 `Pizza` 和 `PizzaStatus`: - -```java -Pizza pz = new Pizza(); -pz.setStatus(Pizza.PizzaStatus.READY); -System.out.println(Pizza.getJsonString(pz)); -``` - -生成 Pizza 状态以以下JSON展示: - -```json -{ - "status" : { - "timeToDelivery" : 2, - "ready" : true, - "ordered" : false, - "delivered" : false - }, - "deliverable" : true -} -``` - -有关枚举类型的JSON序列化/反序列化(包括自定义)的更多信息,请参阅[Jackson-将枚举序列化为JSON对象。](https://www.baeldung.com/jackson-serialize-enums) - -## 10.总结 - -本文我们讨论了Java枚举类型,从基础知识到高级应用以及实际应用场景,让我们感受到枚举的强大功能。 - -## 11. 补充 - -我们在上面讲到了,我们可以通过在枚举类型中定义属性,方法和构造函数让它变得更加强大。 - -下面我通过一个实际的例子展示一下,当我们调用短信验证码的时候可能有几种不同的用途,我们在下面这样定义: - -```java - -public enum PinType { - - REGISTER(100000, "注册使用"), - FORGET_PASSWORD(100001, "忘记密码使用"), - UPDATE_PHONE_NUMBER(100002, "更新手机号码使用"); - - private final int code; - private final String message; - - PinType(int code, String message) { - this.code = code; - this.message = message; - } - - public int getCode() { - return code; - } - - public String getMessage() { - return message; - } - - @Override - public String toString() { - return "PinType{" + - "code=" + code + - ", message='" + message + '\'' + - '}'; - } -} -``` - -实际使用: - - ```java -System.out.println(PinType.FORGET_PASSWORD.getCode()); -System.out.println(PinType.FORGET_PASSWORD.getMessage()); -System.out.println(PinType.FORGET_PASSWORD.toString()); - ``` - -Output: - -```java -100001 -忘记密码使用 -PinType{code=100001, message='忘记密码使用'} -``` - -这样的话,在实际使用起来就会非常灵活方便! \ No newline at end of file diff --git "a/docs/java/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/java/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md" deleted file mode 100644 index 22ce691..0000000 --- "a/docs/java/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ /dev/null @@ -1,301 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [Servlet总结](#servlet总结) -- [阐述Servlet和CGI的区别?](#阐述servlet和cgi的区别) - - [CGI的不足之处:](#cgi的不足之处) - - [Servlet的优点:](#servlet的优点) -- [Servlet接口中有哪些方法及Servlet生命周期探秘](#servlet接口中有哪些方法及servlet生命周期探秘) -- [get和post请求的区别](#get和post请求的区别) -- [什么情况下调用doGet\(\)和doPost\(\)](#什么情况下调用doget和dopost) -- [转发(Forward)和重定向(Redirect)的区别](#转发forward和重定向redirect的区别) -- [自动刷新\(Refresh\)](#自动刷新refresh) -- [Servlet与线程安全](#servlet与线程安全) -- [JSP和Servlet是什么关系](#jsp和servlet是什么关系) -- [JSP工作原理](#jsp工作原理) -- [JSP有哪些内置对象、作用分别是什么](#jsp有哪些内置对象、作用分别是什么) -- [Request对象的主要方法有哪些](#request对象的主要方法有哪些) -- [request.getAttribute\(\)和 request.getParameter\(\)有何区别](#requestgetattribute和-requestgetparameter有何区别) -- [include指令include的行为的区别](#include指令include的行为的区别) -- [JSP九大内置对象,七大动作,三大指令](#jsp九大内置对象,七大动作,三大指令) -- [讲解JSP中的四种作用域](#讲解jsp中的四种作用域) -- [如何实现JSP或Servlet的单线程模式](#如何实现jsp或servlet的单线程模式) -- [实现会话跟踪的技术有哪些](#实现会话跟踪的技术有哪些) -- [Cookie和Session的的区别](#cookie和session的的区别) - - - -## Servlet总结 - -在Java Web程序中,**Servlet**主要负责接收用户请求 `HttpServletRequest`,在`doGet()`,`doPost()`中做相应的处理,并将回应`HttpServletResponse`反馈给用户。**Servlet** 可以设置初始化参数,供Servlet内部使用。一个Servlet类只会有一个实例,在它初始化时调用`init()`方法,销毁时调用`destroy()`方法**。**Servlet需要在web.xml中配置(MyEclipse中创建Servlet会自动配置),**一个Servlet可以设置多个URL访问**。**Servlet不是线程安全**,因此要谨慎使用类变量。 - -## 阐述Servlet和CGI的区别? - -### CGI的不足之处: - -1,需要为每个请求启动一个操作CGI程序的系统进程。如果请求频繁,这将会带来很大的开销。 - -2,需要为每个请求加载和运行一个CGI程序,这将带来很大的开销 - -3,需要重复编写处理网络协议的代码以及编码,这些工作都是非常耗时的。 - -### Servlet的优点: - -1,只需要启动一个操作系统进程以及加载一个JVM,大大降低了系统的开销 - -2,如果多个请求需要做同样处理的时候,这时候只需要加载一个类,这也大大降低了开销 - -3,所有动态加载的类可以实现对网络协议以及请求解码的共享,大大降低了工作量。 - -4,Servlet能直接和Web服务器交互,而普通的CGI程序不能。Servlet还能在各个程序之间共享数据,使数据库连接池之类的功能很容易实现。 - -补充:Sun Microsystems公司在1996年发布Servlet技术就是为了和CGI进行竞争,Servlet是一个特殊的Java程序,一个基于Java的Web应用通常包含一个或多个Servlet类。Servlet不能够自行创建并执行,它是在Servlet容器中运行的,容器将用户的请求传递给Servlet程序,并将Servlet的响应回传给用户。通常一个Servlet会关联一个或多个JSP页面。以前CGI经常因为性能开销上的问题被诟病,然而Fast CGI早就已经解决了CGI效率上的问题,所以面试的时候大可不必信口开河的诟病CGI,事实上有很多你熟悉的网站都使用了CGI技术。 - -参考:《javaweb整合开发王者归来》P7 - -## Servlet接口中有哪些方法及Servlet生命周期探秘 -Servlet接口定义了5个方法,其中**前三个方法与Servlet生命周期相关**: - -- `void init(ServletConfig config) throws ServletException` -- `void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException` -- `void destroy()` -- `java.lang.String getServletInfo()` -- `ServletConfig getServletConfig()` - -**生命周期:** **Web容器加载Servlet并将其实例化后,Servlet生命周期开始**,容器运行其**init()方法**进行Servlet的初始化;请求到达时调用Servlet的**service()方法**,service()方法会根据需要调用与请求对应的**doGet或doPost**等方法;当服务器关闭或项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的**destroy()方法**。**init方法和destroy方法只会执行一次,service方法客户端每次请求Servlet都会执行**。Servlet中有时会用到一些需要初始化与销毁的资源,因此可以把初始化资源的代码放入init方法中,销毁资源的代码放入destroy方法中,这样就不需要每次处理客户端的请求都要初始化与销毁资源。 - -参考:《javaweb整合开发王者归来》P81 - -## get和post请求的区别 - -get和post请求实际上是没有区别,大家可以自行查询相关文章(参考文章:[https://www.cnblogs.com/logsharing/p/8448446.html](https://www.cnblogs.com/logsharing/p/8448446.html),知乎对应的问题链接:[get和post区别?](https://www.zhihu.com/question/28586791))! - -可以把 get 和 post 当作两个不同的行为,两者并没有什么本质区别,底层都是 TCP 连接。 get请求用来从服务器上获得资源,而post是用来向服务器提交数据。比如你要获取人员列表可以用 get 请求,你需要创建一个人员可以用 post 。这也是 Restful API 最基本的一个要求。 - -推荐阅读: - -- https://www.zhihu.com/question/28586791 -- https://mp.weixin.qq.com/s?__biz=MzI3NzIzMzg3Mw==&mid=100000054&idx=1&sn=71f6c214f3833d9ca20b9f7dcd9d33e4#rd - -## 什么情况下调用doGet()和doPost() -Form标签里的method的属性为get时调用doGet(),为post时调用doPost()。 - -## 转发(Forward)和重定向(Redirect)的区别 - -**转发是服务器行为,重定向是客户端行为。** - -**转发(Forward)** -通过RequestDispatcher对象的forward(HttpServletRequest request,HttpServletResponse response)方法实现的。RequestDispatcher可以通过HttpServletRequest 的getRequestDispatcher()方法获得。例如下面的代码就是跳转到login_success.jsp页面。 -```java - request.getRequestDispatcher("login_success.jsp").forward(request, response); -``` -**重定向(Redirect)** 是利用服务器返回的状态码来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过 `HttpServletResponse` 的 `setStatus(int status)` 方法设置状态码。如果服务器返回301或者302,则浏览器会到新的网址重新请求该资源。 - -1. **从地址栏显示来说** - -forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器.浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址. -redirect是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址.所以地址栏显示的是新的URL. - -2. **从数据共享来说** - -forward:转发页面和转发到的页面可以共享request里面的数据. -redirect:不能共享数据. - -3. **从运用地方来说** - -forward:一般用于用户登陆的时候,根据角色转发到相应的模块. -redirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等 - -4. 从效率来说 - -forward:高. -redirect:低. - -## 自动刷新(Refresh) -自动刷新不仅可以实现一段时间之后自动跳转到另一个页面,还可以实现一段时间之后自动刷新本页面。Servlet中通过HttpServletResponse对象设置Header属性实现自动刷新例如: -```java -Response.setHeader("Refresh","5;URL=http://localhost:8080/servlet/example.htm"); -``` -其中5为时间,单位为秒。URL指定就是要跳转的页面(如果设置自己的路径,就会实现每过5秒自动刷新本页面一次) - - -## Servlet与线程安全 -**Servlet不是线程安全的,多线程并发的读写会导致数据不同步的问题。** 解决的办法是尽量不要定义name属性,而是要把name变量分别定义在doGet()和doPost()方法内。虽然使用synchronized(name){}语句块可以解决问题,但是会造成线程的等待,不是很科学的办法。 -注意:多线程的并发的读写Servlet类属性会导致数据不同步。但是如果只是并发地读取属性而不写入,则不存在数据不同步的问题。因此Servlet里的只读属性最好定义为final类型的。 - -参考:《javaweb整合开发王者归来》P92 - - - -## JSP和Servlet是什么关系 -其实这个问题在上面已经阐述过了,Servlet是一个特殊的Java程序,它运行于服务器的JVM中,能够依靠服务器的支持向浏览器提供显示内容。JSP本质上是Servlet的一种简易形式,JSP会被服务器处理成一个类似于Servlet的Java程序,可以简化页面内容的生成。Servlet和JSP最主要的不同点在于,Servlet的应用逻辑是在Java文件中,并且完全从表示层中的HTML分离开来。而JSP的情况是Java和HTML可以组合成一个扩展名为.jsp的文件。有人说,Servlet就是在Java中写HTML,而JSP就是在HTML中写Java代码,当然这个说法是很片面且不够准确的。JSP侧重于视图,Servlet更侧重于控制逻辑,在MVC架构模式中,JSP适合充当视图(view)而Servlet适合充当控制器(controller)。 - -## JSP工作原理 -JSP是一种Servlet,但是与HttpServlet的工作方式不太一样。HttpServlet是先由源代码编译为class文件后部署到服务器下,为先编译后部署。而JSP则是先部署后编译。JSP会在客户端第一次请求JSP文件时被编译为HttpJspPage类(接口Servlet的一个子类)。该类会被服务器临时存放在服务器工作目录里面。下面通过实例给大家介绍。 -工程JspLoginDemo下有一个名为login.jsp的Jsp文件,把工程第一次部署到服务器上后访问这个Jsp文件,我们发现这个目录下多了下图这两个东东。 -.class文件便是JSP对应的Servlet。编译完毕后再运行class文件来响应客户端请求。以后客户端访问login.jsp的时候,Tomcat将不再重新编译JSP文件,而是直接调用class文件来响应客户端请求。 -![JSP工作原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/1.png) -由于JSP只会在客户端第一次请求的时候被编译 ,因此第一次请求JSP时会感觉比较慢,之后就会感觉快很多。如果把服务器保存的class文件删除,服务器也会重新编译JSP。 - -开发Web程序时经常需要修改JSP。Tomcat能够自动检测到JSP程序的改动。如果检测到JSP源代码发生了改动。Tomcat会在下次客户端请求JSP时重新编译JSP,而不需要重启Tomcat。这种自动检测功能是默认开启的,检测改动会消耗少量的时间,在部署Web应用的时候可以在web.xml中将它关掉。 - -参考:《javaweb整合开发王者归来》P97 - -## JSP有哪些内置对象、作用分别是什么 -[JSP内置对象 - CSDN博客 ](http://blog.csdn.net/qq_34337272/article/details/64310849 ) - -JSP有9个内置对象: -- request:封装客户端的请求,其中包含来自GET或POST请求的参数; -- response:封装服务器对客户端的响应; -- pageContext:通过该对象可以获取其他对象; -- session:封装用户会话的对象; -- application:封装服务器运行环境的对象; -- out:输出服务器响应的输出流对象; -- config:Web应用的配置对象; -- page:JSP页面本身(相当于Java程序中的this); -- exception:封装页面抛出异常的对象。 - - -## Request对象的主要方法有哪些 -- setAttribute(String name,Object):设置名字为name的request 的参数值 -- getAttribute(String name):返回由name指定的属性值 -- getAttributeNames():返回request 对象所有属性的名字集合,结果是一个枚举的实例 -- getCookies():返回客户端的所有 Cookie 对象,结果是一个Cookie 数组 -- getCharacterEncoding() :返回请求中的字符编码方式 = getContentLength() :返回请求的 Body的长度 -- getHeader(String name) :获得HTTP协议定义的文件头信息 -- getHeaders(String name) :返回指定名字的request Header 的所有值,结果是一个枚举的实例 -- getHeaderNames() :返回所以request Header 的名字,结果是一个枚举的实例 -- getInputStream() :返回请求的输入流,用于获得请求中的数据 -- getMethod() :获得客户端向服务器端传送数据的方法 -- getParameter(String name) :获得客户端传送给服务器端的有 name指定的参数值 -- getParameterNames() :获得客户端传送给服务器端的所有参数的名字,结果是一个枚举的实例 -- getParameterValues(String name):获得有name指定的参数的所有值 -- getProtocol():获取客户端向服务器端传送数据所依据的协议名称 -- getQueryString() :获得查询字符串 -- getRequestURI() :获取发出请求字符串的客户端地址 -- getRemoteAddr():获取客户端的 IP 地址 -- getRemoteHost() :获取客户端的名字 -- getSession([Boolean create]) :返回和请求相关 Session -- getServerName() :获取服务器的名字 -- getServletPath():获取客户端所请求的脚本文件的路径 -- getServerPort():获取服务器的端口号 -- removeAttribute(String name):删除请求中的一个属性 - -## request.getAttribute()和 request.getParameter()有何区别 -**从获取方向来看:** - -`getParameter()`是获取 POST/GET 传递的参数值; - -`getAttribute()`是获取对象容器中的数据值; - -**从用途来看:** - -`getParameter()`用于客户端重定向时,即点击了链接或提交按扭时传值用,即用于在用表单或url重定向传值时接收数据用。 - -`getAttribute()` 用于服务器端重定向时,即在 sevlet 中使用了 forward 函数,或 struts 中使用了 -mapping.findForward。 getAttribute 只能收到程序用 setAttribute 传过来的值。 - -另外,可以用 `setAttribute()`,`getAttribute()` 发送接收对象.而 `getParameter()` 显然只能传字符串。 -`setAttribute()` 是应用服务器把这个对象放在该页面所对应的一块内存中去,当你的页面服务器重定向到另一个页面时,应用服务器会把这块内存拷贝另一个页面所对应的内存中。这样`getAttribute()`就能取得你所设下的值,当然这种方法可以传对象。session也一样,只是对象在内存中的生命周期不一样而已。`getParameter()`只是应用服务器在分析你送上来的 request页面的文本时,取得你设在表单或 url 重定向时的值。 - -**总结:** - -`getParameter()`返回的是String,用于读取提交的表单中的值;(获取之后会根据实际需要转换为自己需要的相应类型,比如整型,日期类型啊等等) - -`getAttribute()`返回的是Object,需进行转换,可用`setAttribute()`设置成任意对象,使用很灵活,可随时用 - -## include指令include的行为的区别 -**include指令:** JSP可以通过include指令来包含其他文件。被包含的文件可以是JSP文件、HTML文件或文本文件。包含的文件就好像是该JSP文件的一部分,会被同时编译执行。 语法格式如下: -<%@ include file="文件相对 url 地址" %> - -i**nclude动作:** ``动作元素用来包含静态和动态的文件。该动作把指定文件插入正在生成的页面。语法格式如下: - - -## JSP九大内置对象,七大动作,三大指令 -[JSP九大内置对象,七大动作,三大指令总结](http://blog.csdn.net/qq_34337272/article/details/64310849) - -## 讲解JSP中的四种作用域 -JSP中的四种作用域包括page、request、session和application,具体来说: -- **page**代表与一个页面相关的对象和属性。 -- **request**代表与Web客户机发出的一个请求相关的对象和属性。一个请求可能跨越多个页面,涉及多个Web组件;需要在页面显示的临时数据可以置于此作用域。 -- **session**代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的session中。 -- **application**代表与整个Web应用程序相关的对象和属性,它实质上是跨越整个Web应用程序,包括多个页面、请求和会话的一个全局作用域。 - -## 如何实现JSP或Servlet的单线程模式 -对于JSP页面,可以通过page指令进行设置。 -`<%@page isThreadSafe="false"%>` - -对于Servlet,可以让自定义的Servlet实现SingleThreadModel标识接口。 - -说明:如果将JSP或Servlet设置成单线程工作模式,会导致每个请求创建一个Servlet实例,这种实践将导致严重的性能问题(服务器的内存压力很大,还会导致频繁的垃圾回收),所以通常情况下并不会这么做。 - -## 实现会话跟踪的技术有哪些 -1. **使用Cookie** - -向客户端发送Cookie -```java -Cookie c =new Cookie("name","value"); //创建Cookie -c.setMaxAge(60*60*24); //设置最大时效,此处设置的最大时效为一天 -response.addCookie(c); //把Cookie放入到HTTP响应中 -``` -从客户端读取Cookie -```java -String name ="name"; -Cookie[]cookies =request.getCookies(); -if(cookies !=null){ - for(int i= 0;i -``` - -**优点:** Cookie被禁时可以使用 - -**缺点:** 所有页面必须是表单提交之后的结果。 - -4. HttpSession - - - 在所有会话跟踪技术中,HttpSession对象是最强大也是功能最多的。当一个用户第一次访问某个网站时会自动创建 HttpSession,每个用户可以访问他自己的HttpSession。可以通过HttpServletRequest对象的getSession方 法获得HttpSession,通过HttpSession的setAttribute方法可以将一个值放在HttpSession中,通过调用 HttpSession对象的getAttribute方法,同时传入属性名就可以获取保存在HttpSession中的对象。与上面三种方式不同的 是,HttpSession放在服务器的内存中,因此不要将过大的对象放在里面,即使目前的Servlet容器可以在内存将满时将HttpSession 中的对象移到其他存储设备中,但是这样势必影响性能。添加到HttpSession中的值可以是任意Java对象,这个对象最好实现了 Serializable接口,这样Servlet容器在必要的时候可以将其序列化到文件中,否则在序列化时就会出现异常。 -## Cookie和Session的的区别 - -Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。 - - **Cookie 一般用来保存用户信息** 比如①我们在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了;②一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了,这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写);③登录一次网站后访问网站其他页面不需要重新登录。**Session 的主要作用就是通过服务端记录用户的状态。** 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。 - -Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。 - -Cookie 存储在客户端中,而Session存储在服务器上,相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/Java IO\344\270\216NIO.md" "b/docs/java/Java IO\344\270\216NIO.md" deleted file mode 100644 index 74bd850..0000000 --- "a/docs/java/Java IO\344\270\216NIO.md" +++ /dev/null @@ -1,200 +0,0 @@ - - -- [IO流学习总结](#io流学习总结) - - [一 Java IO,硬骨头也能变软](#一-java-io,硬骨头也能变软) - - [二 java IO体系的学习总结](#二-java-io体系的学习总结) - - [三 Java IO面试题](#三-java-io面试题) -- [NIO与AIO学习总结](#nio与aio学习总结) - - [一 Java NIO 概览](#一-java-nio-概览) - - [二 Java NIO 之 Buffer\(缓冲区\)](#二-java-nio-之-buffer缓冲区) - - [三 Java NIO 之 Channel(通道)](#三-java-nio-之-channel(通道)) - - [四 Java NIO之Selector(选择器)](#四-java-nio之selector(选择器)) - - [五 Java NIO之拥抱Path和Files](#五-java-nio之拥抱path和files) - - [六 NIO学习总结以及NIO新特性介绍](#六-nio学习总结以及nio新特性介绍) - - [七 Java NIO AsynchronousFileChannel异步文件通](#七-java-nio-asynchronousfilechannel异步文件通) - - [八 高并发Java(8):NIO和AIO](#八-高并发java(8):nio和aio) -- [推荐阅读](#推荐阅读) - - [在 Java 7 中体会 NIO.2 异步执行的快乐](#在-java-7-中体会-nio2-异步执行的快乐) - - [Java AIO总结与示例](#java-aio总结与示例) - - - - - -## IO流学习总结 - -### [一 Java IO,硬骨头也能变软](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483981&idx=1&sn=6e5c682d76972c8d2cf271a85dcf09e2&chksm=fd98542ccaefdd3a70428e9549bc33e8165836855edaa748928d16c1ebde9648579d3acaac10#rd) - -**(1) 按操作方式分类结构图:** - -![IO-操作方式分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作方式分类.png) - - -**(2)按操作对象分类结构图** - -![IO-操作对象分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作对象分类.png) - -### [二 java IO体系的学习总结](https://blog.csdn.net/nightcurtis/article/details/51324105) -1. **IO流的分类:** - - 按照流的流向分,可以分为输入流和输出流; - - 按照操作单元划分,可以划分为字节流和字符流; - - 按照流的角色划分为节点流和处理流。 -2. **流的原理浅析:** - - java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java Io流的40多个类都是从如下4个抽象类基类中派生出来的。 - - - **InputStream/Reader**: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 - - **OutputStream/Writer**: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 -3. **常用的io流的用法** - -### [三 Java IO面试题](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483985&idx=1&sn=38531c2cee7b87f125df7aef41637014&chksm=fd985430caefdd26b0506aa84fc26251877eccba24fac73169a4d6bd1eb5e3fbdf3c3b940261#rd) - -## NIO与AIO学习总结 - - -### [一 Java NIO 概览](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483956&idx=1&sn=57692bc5b7c2c6dfb812489baadc29c9&chksm=fd985455caefdd4331d828d8e89b22f19b304aa87d6da73c5d8c66fcef16e4c0b448b1a6f791#rd) - -1. **NIO简介**: - - Java NIO 是 java 1.4, 之后新出的一套IO接口NIO中的N可以理解为Non-blocking,不单纯是New。 - -2. **NIO的特性/NIO与IO区别:** - - 1)IO是面向流的,NIO是面向缓冲区的; - - 2)IO流是阻塞的,NIO流是不阻塞的; - - 3)NIO有选择器,而IO没有。 -3. **读数据和写数据方式:** - - 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。 - - - 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。 - -4. **NIO核心组件简单介绍** - - **Channels** - - **Buffers** - - **Selectors** - - -### [二 Java NIO 之 Buffer(缓冲区)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483961&idx=1&sn=f67bef4c279e78043ff649b6b03fdcbc&chksm=fd985458caefdd4e3317ccbdb2d0a5a70a5024d3255eebf38183919ed9c25ade536017c0a6ba#rd) - -1. **Buffer(缓冲区)介绍:** - - Java NIO Buffers用于和NIO Channel交互。 我们从Channel中读取数据到buffers里,从Buffer把数据写入到Channels; - - Buffer本质上就是一块内存区; - - 一个Buffer有三个属性是必须掌握的,分别是:capacity容量、position位置、limit限制。 -2. **Buffer的常见方法** - - Buffer clear() - - Buffer flip() - - Buffer rewind() - - Buffer position(int newPosition) -3. **Buffer的使用方式/方法介绍:** - - 分配缓冲区(Allocating a Buffer): - ```java - ByteBuffer buf = ByteBuffer.allocate(28);//以ByteBuffer为例子 - ``` - - 写入数据到缓冲区(Writing Data to a Buffer) - - **写数据到Buffer有两种方法:** - - 1.从Channel中写数据到Buffer - ```java - int bytesRead = inChannel.read(buf); //read into buffer. - ``` - 2.通过put写数据: - ```java - buf.put(127); - ``` - -4. **Buffer常用方法测试** - - 说实话,NIO编程真的难,通过后面这个测试例子,你可能才能勉强理解前面说的Buffer方法的作用。 - - -### [三 Java NIO 之 Channel(通道)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483966&idx=1&sn=d5cf18c69f5f9ec2aff149270422731f&chksm=fd98545fcaefdd49296e2c78000ce5da277435b90ba3c03b92b7cf54c6ccc71d61d13efbce63#rd) - - -1. **Channel(通道)介绍** - - 通常来说NIO中的所有IO都是从 Channel(通道) 开始的。 - - NIO Channel通道和流的区别: -2. **FileChannel的使用** -3. **SocketChannel和ServerSocketChannel的使用** -4. **️DatagramChannel的使用** -5. **Scatter / Gather** - - Scatter: 从一个Channel读取的信息分散到N个缓冲区中(Buufer). - - Gather: 将N个Buffer里面内容按照顺序发送到一个Channel. -6. **通道之间的数据传输** - - 在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel。 - - transferFrom() :transferFrom方法把数据从通道源传输到FileChannel - - transferTo() :transferTo方法把FileChannel数据传输到另一个channel - - -### [四 Java NIO之Selector(选择器)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483970&idx=1&sn=d5e2b133313b1d0f32872d54fbdf0aa7&chksm=fd985423caefdd354b587e57ce6cf5f5a7bec48b9ab7554f39a8d13af47660cae793956e0f46#rd) - - -1. **Selector(选择器)介绍** - - Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。 - - 使用Selector的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销。 -2. **Selector(选择器)的使用方法介绍** - - Selector的创建 - ```java - Selector selector = Selector.open(); - ``` - - 注册Channel到Selector(Channel必须是非阻塞的) - ```java - channel.configureBlocking(false); - SelectionKey key = channel.register(selector, Selectionkey.OP_READ); - ``` - - SelectionKey介绍 - - 一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。 - - 从Selector中选择channel(Selecting Channels via a Selector) - - 选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中. - - 停止选择的方法 - - wakeup()方法 和close()方法。 -3. **模板代码** - - 有了模板代码我们在编写程序时,大多数时间都是在模板代码中添加相应的业务代码。 -4. **客户端与服务端简单交互实例** - - - -### [五 Java NIO之拥抱Path和Files](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483976&idx=1&sn=2296c05fc1b840a64679e2ad7794c96d&chksm=fd985429caefdd3f48e2ee6fdd7b0f6fc419df90b3de46832b484d6d1ca4e74e7837689c8146&token=537240785&lang=zh_CN#rd) - -**一 文件I/O基石:Path:** -- 创建一个Path -- File和Path之间的转换,File和URI之间的转换 -- 获取Path的相关信息 -- 移除Path中的冗余项 - -**二 拥抱Files类:** -- Files.exists() 检测文件路径是否存在 -- Files.createFile() 创建文件 -- Files.createDirectories()和Files.createDirectory()创建文件夹 -- Files.delete()方法 可以删除一个文件或目录 -- Files.copy()方法可以吧一个文件从一个地址复制到另一个位置 -- 获取文件属性 -- 遍历一个文件夹 -- Files.walkFileTree()遍历整个目录 - -### [六 NIO学习总结以及NIO新特性介绍](https://blog.csdn.net/a953713428/article/details/64907250) - -- **内存映射:** - -这个功能主要是为了提高大文件的读写速度而设计的。内存映射文件(memory-mappedfile)能让你创建和修改那些大到无法读入内存的文件。有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问了。将文件的一段区域映射到内存中,比传统的文件处理速度要快很多。内存映射文件它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。 - -### [七 Java NIO AsynchronousFileChannel异步文件通](http://wiki.jikexueyuan.com/project/java-nio-zh/java-nio-asynchronousfilechannel.html) - -Java7中新增了AsynchronousFileChannel作为nio的一部分。AsynchronousFileChannel使得数据可以进行异步读写。 - -### [八 高并发Java(8):NIO和AIO](http://www.importnew.com/21341.html) - - - -## 推荐阅读 - -### [在 Java 7 中体会 NIO.2 异步执行的快乐](https://www.ibm.com/developerworks/cn/java/j-lo-nio2/index.html) - -### [Java AIO总结与示例](https://blog.csdn.net/x_i_y_u_e/article/details/52223406) -AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。 - - -**欢迎关注我的微信公众号:"Java面试通关手册"(一个有温度的微信公众号,期待与你共同进步~~~坚持原创,分享美文,分享各种Java学习资源):** diff --git "a/docs/java/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/java/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" deleted file mode 100644 index 2f214d0..0000000 --- "a/docs/java/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ /dev/null @@ -1,556 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [1. 面向对象和面向过程的区别](#1-面向对象和面向过程的区别) -- [2. Java 语言有哪些特点?](#2-java-语言有哪些特点) -- [3. 关于 JVM JDK 和 JRE 最详细通俗的解答](#3-关于-jvm-jdk-和-jre-最详细通俗的解答) - - [JVM](#jvm) - - [JDK 和 JRE](#jdk-和-jre) -- [4. Oracle JDK 和 OpenJDK 的对比](#4-oracle-jdk-和-openjdk-的对比) -- [5. Java和C++的区别?](#5-java和c的区别) -- [6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?](#6-什么是-java-程序的主类-应用程序和小程序的主类有何不同) -- [7. Java 应用程序与小程序之间有哪些差别?](#7-java-应用程序与小程序之间有哪些差别) -- [8. 字符型常量和字符串常量的区别?](#8-字符型常量和字符串常量的区别) -- [9. 构造器 Constructor 是否可被 override?](#9-构造器-constructor-是否可被-override) -- [10. 重载和重写的区别](#10-重载和重写的区别) -- [11. Java 面向对象编程三大特性: 封装 继承 多态](#11-java-面向对象编程三大特性-封装-继承-多态) - - [封装](#封装) - - [继承](#继承) - - [多态](#多态) -- [12. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?](#12-string-stringbuffer-和-stringbuilder-的区别是什么-string-为什么是不可变的) -- [13. 自动装箱与拆箱](#13-自动装箱与拆箱) -- [14. 在一个静态方法内调用一个非静态成员为什么是非法的?](#14-在一个静态方法内调用一个非静态成员为什么是非法的) -- [15. 在 Java 中定义一个不做事且没有参数的构造方法的作用](#15-在-java-中定义一个不做事且没有参数的构造方法的作用) -- [16. import java和javax有什么区别?](#16-import-java和javax有什么区别) -- [17. 接口和抽象类的区别是什么?](#17-接口和抽象类的区别是什么) -- [18. 成员变量与局部变量的区别有哪些?](#18-成员变量与局部变量的区别有哪些) -- [19. 创建一个对象用什么运算符?对象实体与对象引用有何不同?](#19-创建一个对象用什么运算符对象实体与对象引用有何不同) -- [20. 什么是方法的返回值?返回值在类的方法里的作用是什么?](#20-什么是方法的返回值返回值在类的方法里的作用是什么) -- [21. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?](#21-一个类的构造方法的作用是什么-若一个类没有声明构造方法该程序能正确执行吗-为什么) -- [22. 构造方法有哪些特性?](#22-构造方法有哪些特性) -- [23. 静态方法和实例方法有何不同](#23-静态方法和实例方法有何不同) -- [24. 对象的相等与指向他们的引用相等,两者有什么不同?](#24-对象的相等与指向他们的引用相等两者有什么不同) -- [25. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?](#25-在调用子类构造方法之前会先调用父类没有参数的构造方法其目的是) -- [26. == 与 equals(重要)](#26--与-equals重要) -- [27. hashCode 与 equals (重要)](#27-hashcode-与-equals-重要) - - [hashCode()介绍](#hashcode介绍) - - [为什么要有 hashCode](#为什么要有-hashcode) - - [hashCode()与equals()的相关规定](#hashcode与equals的相关规定) -- [28. 为什么Java中只有值传递?](#28-为什么java中只有值传递) -- [29. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?](#29-简述线程程序进程的基本概念以及他们之间关系是什么) -- [30. 线程有哪些基本状态?](#30-线程有哪些基本状态) -- [31 关于 final 关键字的一些总结](#31-关于-final-关键字的一些总结) -- [32 Java 中的异常处理](#32-java-中的异常处理) - - [Java异常类层次结构图](#java异常类层次结构图) - - [Throwable类常用方法](#throwable类常用方法) - - [异常处理总结](#异常处理总结) -- [33 Java序列化中如果有些字段不想进行序列化,怎么办?](#33-java序列化中如果有些字段不想进行序列化怎么办) -- [34 获取用键盘输入常用的两种方法](#34-获取用键盘输入常用的两种方法) -- [35 Java 中 IO 流](#35-java-中-io-流) - - [Java 中 IO 流分为几种?](#java-中-io-流分为几种) - - [既然有了字节流,为什么还要有字符流?](#既然有了字节流为什么还要有字符流) - - [BIO,NIO,AIO 有什么区别?](#bionioaio-有什么区别) -- [36. 常见关键字总结:static,final,this,super](#36-常见关键字总结staticfinalthissuper) -- [37. Collections 工具类和 Arrays 工具类常见方法总结](#37-collections-工具类和-arrays-工具类常见方法总结) -- [参考](#参考) -- [公众号](#公众号) - - - -## 1. 面向对象和面向过程的区别 - -- **面向过程** :**面向过程性能比面向对象高。** 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发。但是,**面向过程没有面向对象易维护、易复用、易扩展。** -- **面向对象** :**面向对象易维护、易复用、易扩展。** 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,**面向对象性能比面向过程低**。 - -参见 issue : [面向过程 :面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) - -> 这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java性能差的主要原因并不是因为它是面向对象语言,而是Java是半编译语言,最终的执行代码并不是可以直接被CPU执行的二进制机械码。 -> -> 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比Java好。 - -## 2. Java 语言有哪些特点? - -1. 简单易学; -2. 面向对象(封装,继承,多态); -3. 平台无关性( Java 虚拟机实现平台无关性); -4. 可靠性; -5. 安全性; -6. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持); -7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便); -8. 编译与解释并存; - -> 修正(参见: [issue#544](https://github.com/Snailclimb/JavaGuide/issues/544)):C++11开始(2011年的时候),C++就引入了多线程库,在windows、linux、macos都可以使用`std::thread`和`std::async`来创建线程。参考链接:http://www.cplusplus.com/reference/thread/thread/?kw=thread - -## 3. 关于 JVM JDK 和 JRE 最详细通俗的解答 - -### JVM - -Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 - -**什么是字节码?采用字节码的好处是什么?** - -> 在 Java 中,JVM可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。 - -**Java 程序从源代码到运行一般有下面3步:** - -![Java程序运行过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E8%BF%87%E7%A8%8B.png) - -我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。 - -> HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销。JDK支持分层编译和AOT协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。 - -**总结:** - -Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 - -### JDK 和 JRE - -JDK是Java Development Kit,它是功能齐全的Java SDK。它拥有JRE所拥有的一切,还有编译器(javac)和工具(如javadoc和jdb)。它能够创建和编译程序。 - -JRE 是 Java运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java虚拟机(JVM),Java类库,java命令和其他的一些基础构件。但是,它不能用于创建新程序。 - -如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装JDK了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何Java开发,仍然需要安装JDK。例如,如果要使用JSP部署Web应用程序,那么从技术上讲,您只是在应用程序服务器中运行Java程序。那你为什么需要JDK呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。 - -## 4. Oracle JDK 和 OpenJDK 的对比 - -可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么Oracle和OpenJDK之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。 - -对于Java 7,没什么关键的地方。OpenJDK项目主要基于Sun捐赠的HotSpot源代码。此外,OpenJDK被选为Java 7的参考实现,由Oracle工程师维护。关于JVM,JDK,JRE和OpenJDK之间的区别,Oracle博客帖子在2012年有一个更详细的答案: - -> 问:OpenJDK存储库中的源代码与用于构建Oracle JDK的代码之间有什么区别? -> -> 答:非常接近 - 我们的Oracle JDK版本构建过程基于OpenJDK 7构建,只添加了几个部分,例如部署代码,其中包括Oracle的Java插件和Java WebStart的实现,以及一些封闭的源代码派对组件,如图形光栅化器,一些开源的第三方组件,如Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源Oracle JDK的所有部分,除了我们考虑商业功能的部分。 - -**总结:** - -1. Oracle JDK大概每6个月发一次主要版本,而OpenJDK版本大概每三个月发布一次。但这不是固定的,我觉得了解这个没啥用处。详情参见:https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence。 -2. OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全开源的; -3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到Oracle JDK就可以解决问题; -4. 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能; -5. Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本; -6. Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 - -## 5. Java和C++的区别? - -我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过C++,也要记下来! - -- 都是面向对象的语言,都支持封装、继承和多态 -- Java 不提供指针来直接访问内存,程序内存更加安全 -- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。 -- Java 有自动内存管理机制,不需要程序员手动释放无用内存 -- **在 C 语言中,字符串或字符数组最后都会有一个额外的字符‘\0’来表示结束。但是,Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189]( https://blog.csdn.net/sszgg2006/article/details/49148189) - - -## 6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同? - -一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 main()方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。 - -## 7. Java 应用程序与小程序之间有哪些差别? - -简单说应用程序是从主线程启动(也就是 `main()` 方法)。applet 小程序没有 `main()` 方法,主要是嵌在浏览器页面上运行(调用`init()`或者`run()`来启动),嵌入浏览器这点跟 flash 的小游戏类似。 - -## 8. 字符型常量和字符串常量的区别? - -1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符 -2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置) -3. 占内存大小 字符常量只占2个字节; 字符串常量占若干个字节 (**注意: char在Java中占两个字节**) - -> java编程思想第四版:2.2.2节 -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) - -## 9. 构造器 Constructor 是否可被 override? - -Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。 - -## 10. 重载和重写的区别 - -#### 重载 - -发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 - -下面是《Java核心技术》对重载这个概念的介绍: - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktopjava核心技术-重载.jpg)  - -#### 重写 - - 重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。** - -## 11. Java 面向对象编程三大特性: 封装 继承 多态 - -### 封装 - -封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 - - -### 继承 -继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 - -**关于继承如下 3 点请记住:** - -1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 -2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 -3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 - -### 多态 - -所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 - -在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 - -## 12. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? - -**可变性** - -简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。 - -StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。 - -AbstractStringBuilder.java - -```java -abstract class AbstractStringBuilder implements Appendable, CharSequence { - char[] value; - int count; - AbstractStringBuilder() { - } - AbstractStringBuilder(int capacity) { - value = new char[capacity]; - } -``` - - -**线程安全性** - -String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。  - -**性能** - -每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 - -**对于三者使用的总结:** - -1. 操作少量的数据: 适用String -2. 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder -3. 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer - -## 13. 自动装箱与拆箱 - -- **装箱**:将基本类型用它们对应的引用类型包装起来; -- **拆箱**:将包装类型转换为基本数据类型; - -## 14. 在一个静态方法内调用一个非静态成员为什么是非法的? - -由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。 - -## 15. 在 Java 中定义一个不做事且没有参数的构造方法的作用 - -Java 程序在执行子类的构造方法之前,如果没有用 `super() `来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 `super() `来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。 -  -## 16. import java和javax有什么区别? - -刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。 - -所以,实际上java和javax没有区别。这都是一个名字。 - -## 17. 接口和抽象类的区别是什么? - -1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 -2. 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。 -3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。 -4. 接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。 -5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。 - -备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。(详见issue:[https://github.com/Snailclimb/JavaGuide/issues/146](https://github.com/Snailclimb/JavaGuide/issues/146)) - -## 18. 成员变量与局部变量的区别有哪些? - -1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。 -2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 -3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 -4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 - -## 19. 创建一个对象用什么运算符?对象实体与对象引用有何不同? - -new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)。 - -## 20. 什么是方法的返回值?返回值在类的方法里的作用是什么? - -方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作! - -## 21. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么? - -主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。 - -## 22. 构造方法有哪些特性? - -1. 名字与类名相同。 -2. 没有返回值,但不能用void声明构造函数。 -3. 生成类的对象时自动执行,无需调用。 - -## 23. 静态方法和实例方法有何不同 - -1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 - -2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。 - -## 24. 对象的相等与指向他们的引用相等,两者有什么不同? - -对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。 - -## 25. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是? - -帮助子类做初始化工作。 - -## 26. == 与 equals(重要) - -**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。 - -**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: -- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 -- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 - - -**举个例子:** - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -**说明:** - -- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -## 27. hashCode 与 equals (重要) - -面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?” - -### hashCode()介绍 -hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。 - -散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - -### 为什么要有 hashCode - -**我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 `equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -通过我们可以看出:`hashCode()` 的作用就是**获取哈希码**,也称为散列码;它实际上是返回一个int整数。这个**哈希码的作用**是确定该对象在哈希表中的索引位置。**`hashCode() `在散列表中才有用,在其它情况下没用**。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。 - -### hashCode()与equals()的相关规定 - -1. 如果两个对象相等,则hashcode一定也是相同的 -2. 两个对象相等,对两个对象分别调用equals方法都返回true -3. 两个对象有相同的hashcode值,它们也不一定是相等的 -4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** -5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) - -推荐阅读:[Java hashCode() 和 equals()的若干问题解答](https://www.cnblogs.com/skywang12345/p/3324958.html) - - -## 28. 为什么Java中只有值传递? - -[为什么Java中只有值传递?](https://juejin.im/post/5e18879e6fb9a02fc63602e2) - - -## 29. 简述线程、程序、进程的基本概念。以及他们之间关系是什么? - -**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。 - -**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 -线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。 - -## 30. 线程有哪些基本状态? - -Java 线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4节)。 - -![Java线程的状态](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81.png) - -线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4节): - -![Java线程状态变迁](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%20%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E5%8F%98%E8%BF%81.png) - - - -由上图可以看出: - -线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。 - -> 操作系统隐藏 Java虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。 - -![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png) - -当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的` run() `方法之后将会进入到 **TERMINATED(终止)** 状态。 - -## 31 关于 final 关键字的一些总结 - -final关键字主要用在三个地方:变量、方法、类。 - -1. 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。 -2. 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。 -3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 - -## 32 Java 中的异常处理 - -### Java异常类层次结构图 - -![Java异常类层次结构图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/Exception.png) - - - -在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 **Throwable类**。Throwable: 有两个重要的子类:**Exception(异常)** 和 **Error(错误)** ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。 - -**Error(错误):是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。 - -这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。 - -**Exception(异常):是程序本身可以处理的异常**。Exception 类有一个重要的子类 **RuntimeException**。RuntimeException 异常由Java虚拟机抛出。**NullPointerException**(要访问的变量没有引用任何对象时,抛出该异常)、**ArithmeticException**(算术运算异常,一个整数除以0时,抛出该异常)和 **ArrayIndexOutOfBoundsException** (下标越界异常)。 - -**注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。** - -### Throwable类常用方法 - -- **public string getMessage()**:返回异常发生时的简要描述 -- **public string toString()**:返回异常发生时的详细信息 -- **public string getLocalizedMessage()**:返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同 -- **public void printStackTrace()**:在控制台上打印Throwable对象封装的异常信息 - -### 异常处理总结 - -- **try 块:** 用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。 -- **catch 块:** 用于处理try捕获到的异常。 -- **finally 块:** 无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return -语句时,finally语句块将在方法返回之前被执行。 - -**在以下4种特殊情况下,finally块不会被执行:** - -1. 在finally语句块第一行发生了异常。 因为在其他行,finally块还是会得到执行 -2. 在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数 ;若该语句在异常语句之后,finally会执行 -3. 程序所在的线程死亡。 -4. 关闭CPU。 - -下面这部分内容来自issue:。 - -**注意:** 当try语句和finally语句中都有return语句时,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值将会覆盖原始的返回值。如下: - -```java - public static int f(int value) { - try { - return value * value; - } finally { - if (value == 2) { - return 0; - } - } - } -``` - -如果调用 `f(2)`,返回值将是0,因为finally语句的返回值覆盖了try语句块的返回值。 - -## 33 Java序列化中如果有些字段不想进行序列化,怎么办? - -对于不想进行序列化的变量,使用transient关键字修饰。 - -transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。 - -## 34 获取用键盘输入常用的两种方法 - -方法1:通过 Scanner - -```java -Scanner input = new Scanner(System.in); -String s = input.nextLine(); -input.close(); -``` - -方法2:通过 BufferedReader - -```java -BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); -String s = input.readLine(); -``` - -## 35 Java 中 IO 流 - -### Java 中 IO 流分为几种? - - - 按照流的流向分,可以分为输入流和输出流; - - 按照操作单元划分,可以划分为字节流和字符流; - - 按照流的角色划分为节点流和处理流。 - -Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。 - - - InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 - - OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 - -按操作方式分类结构图: - -![IO-操作方式分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作方式分类.png) - - -按操作对象分类结构图: - -![IO-操作对象分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作对象分类.png) - -### 既然有了字节流,为什么还要有字符流? - -问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** - -回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 - -### BIO,NIO,AIO 有什么区别? - -- **BIO (Blocking I/O):** 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 -- **NIO (New I/O):** NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 -- **AIO (Asynchronous I/O):** AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 - -## 36. 常见关键字总结:static,final,this,super - -详见笔主的这篇文章: - -## 37. Collections 工具类和 Arrays 工具类常见方法总结 - -详见笔主的这篇文章: - -### 38. 深拷贝 vs 浅拷贝 - -1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。 -2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 - -![deep and shallow copy](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/java-deep-and-shallow-copy.jpg) - -## 参考 - -- https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre -- https://www.educba.com/oracle-vs-openjdk/ -- https://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-openjdk?answertab=active#tab-top - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) - diff --git "a/docs/java/Java\347\226\221\351\232\276\347\202\271.md" "b/docs/java/Java\347\226\221\351\232\276\347\202\271.md" deleted file mode 100644 index 1a10e95..0000000 --- "a/docs/java/Java\347\226\221\351\232\276\347\202\271.md" +++ /dev/null @@ -1,373 +0,0 @@ - - -- [1. 基础](#1-基础) - - [1.1. 正确使用 equals 方法](#11-正确使用-equals-方法) - - [1.2. 整型包装类值的比较](#12-整型包装类值的比较) - - [1.3. BigDecimal](#13-bigdecimal) - - [1.3.1. BigDecimal 的用处](#131-bigdecimal-的用处) - - [1.3.2. BigDecimal 的大小比较](#132-bigdecimal-的大小比较) - - [1.3.3. BigDecimal 保留几位小数](#133-bigdecimal-保留几位小数) - - [1.3.4. BigDecimal 的使用注意事项](#134-bigdecimal-的使用注意事项) - - [1.3.5. 总结](#135-总结) - - [1.4. 基本数据类型与包装数据类型的使用标准](#14-基本数据类型与包装数据类型的使用标准) -- [2. 集合](#2-集合) - - [2.1. Arrays.asList()使用指南](#21-arraysaslist使用指南) - - [2.1.1. 简介](#211-简介) - - [2.1.2. 《阿里巴巴Java 开发手册》对其的描述](#212-阿里巴巴java-开发手册对其的描述) - - [2.1.3. 使用时的注意事项总结](#213-使用时的注意事项总结) - - [2.1.4. 如何正确的将数组转换为ArrayList?](#214-如何正确的将数组转换为arraylist) - - [2.2. Collection.toArray()方法使用的坑&如何反转数组](#22-collectiontoarray方法使用的坑如何反转数组) - - [2.3. 不要在 foreach 循环里进行元素的 remove/add 操作](#23-不要在-foreach-循环里进行元素的-removeadd-操作) - - - -# 1. 基础 - -## 1.1. 正确使用 equals 方法 - -Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。 - -举个例子: - -```java -// 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常 -String str = null; -if (str.equals("SnailClimb")) { - ... -} else { - .. -} -``` - -运行上面的程序会抛出空指针异常,但是我们把第二行的条件判断语句改为下面这样的话,就不会抛出空指针异常,else 语句块得到执行。: - -```java -"SnailClimb".equals(str);// false -``` -不过更推荐使用 `java.util.Objects#equals`(JDK7 引入的工具类)。 - -```java -Objects.equals(null,"SnailClimb");// false -``` -我们看一下`java.util.Objects#equals`的源码就知道原因了。 -```java -public static boolean equals(Object a, Object b) { - // 可以避免空指针异常。如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。 - return (a == b) || (a != null && a.equals(b)); - } -``` - -**注意:** - -Reference:[Java中equals方法造成空指针异常的原因及解决方案](https://blog.csdn.net/tick_tock97/article/details/72824894) - -- 每种原始类型都有默认值一样,如int默认值为 0,boolean 的默认值为 false,null 是任何引用类型的默认值,不严格的说是所有 Object 类型的默认值。 -- 可以使用 == 或者 != 操作来比较null值,但是不能使用其他算法或者逻辑操作。在Java中`null == null`将返回true。 -- 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常 - -## 1.2. 整型包装类值的比较 - -所有整型包装类对象值的比较必须使用equals方法。 - -先看下面这个例子: - -```java -Integer x = 3; -Integer y = 3; -System.out.println(x == y);// true -Integer a = new Integer(3); -Integer b = new Integer(3); -System.out.println(a == b);//false -System.out.println(a.equals(b));//true -``` - -当使用自动装箱方式创建一个Integer对象时,当数值在-128 ~127时,会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的Integer对象。所以上述代码中,x和y引用的是相同的Integer对象。 - -**注意:**如果你的IDE(IDEA/Eclipse)上安装了阿里巴巴的p3c插件,这个插件如果检测到你用 ==的话会报错提示,推荐安装一个这个插件,很不错。 - -## 1.3. BigDecimal - -### 1.3.1. BigDecimal 的用处 - -《阿里巴巴Java开发手册》中提到:**浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。** 具体原理和浮点数的编码方式有关,这里就不多提了,我们下面直接上实例: - -```java -float a = 1.0f - 0.9f; -float b = 0.9f - 0.8f; -System.out.println(a);// 0.100000024 -System.out.println(b);// 0.099999964 -System.out.println(a == b);// false -``` -具有基本数学知识的我们很清楚的知道输出并不是我们想要的结果(**精度丢失**),我们如何解决这个问题呢?一种很常用的方法是:**使用使用 BigDecimal 来定义浮点数的值,再进行浮点数的运算操作。** - -```java -BigDecimal a = new BigDecimal("1.0"); -BigDecimal b = new BigDecimal("0.9"); -BigDecimal c = new BigDecimal("0.8"); -BigDecimal x = a.subtract(b);// 0.1 -BigDecimal y = b.subtract(c);// 0.1 -System.out.println(x.equals(y));// true -``` - -### 1.3.2. BigDecimal 的大小比较 - -`a.compareTo(b)` : 返回 -1 表示小于,0 表示 等于, 1表示 大于。 - -```java -BigDecimal a = new BigDecimal("1.0"); -BigDecimal b = new BigDecimal("0.9"); -System.out.println(a.compareTo(b));// 1 -``` -### 1.3.3. BigDecimal 保留几位小数 - -通过 `setScale`方法设置保留几位小数以及保留规则。保留规则有挺多种,不需要记,IDEA会提示。 - -```java -BigDecimal m = new BigDecimal("1.255433"); -BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN); -System.out.println(n);// 1.255 -``` - -### 1.3.4. BigDecimal 的使用注意事项 - -注意:我们在使用BigDecimal时,为了防止精度丢失,推荐使用它的 **BigDecimal(String)** 构造方法来创建对象。《阿里巴巴Java开发手册》对这部分内容也有提到如下图所示。 - -![《阿里巴巴Java开发手册》对这部分BigDecimal的描述](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/BigDecimal.png) - -### 1.3.5. 总结 - -BigDecimal 主要用来操作(大)浮点数,BigInteger 主要用来操作大整数(超过 long 类型)。 - -BigDecimal 的实现利用到了 BigInteger, 所不同的是 BigDecimal 加入了小数位的概念 - -## 1.4. 基本数据类型与包装数据类型的使用标准 - -Reference:《阿里巴巴Java开发手册》 - -- 【强制】所有的 POJO 类属性必须使用包装数据类型。 -- 【强制】RPC 方法的返回值和参数必须使用包装数据类型。 -- 【推荐】所有的局部变量使用基本数据类型。 - -比如我们如果自定义了一个Student类,其中有一个属性是成绩score,如果用Integer而不用int定义,一次考试,学生可能没考,值是null,也可能考了,但考了0分,值是0,这两个表达的状态明显不一样. - -**说明** :POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。 - -**正例** : 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。 - -**反例** : 比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。 - -# 2. 集合 - -## 2.1. Arrays.asList()使用指南 - -最近使用`Arrays.asList()`遇到了一些坑,然后在网上看到这篇文章:[Java Array to List Examples](http://javadevnotes.com/java-array-to-list-examples) 感觉挺不错的,但是还不是特别全面。所以,自己对于这块小知识点进行了简单的总结。 - -### 2.1.1. 简介 - -`Arrays.asList()`在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个List集合。 - -```java -String[] myArray = { "Apple", "Banana", "Orange" }; -List myList = Arrays.asList(myArray); -//上面两个语句等价于下面一条语句 -List myList = Arrays.asList("Apple","Banana", "Orange"); -``` - -JDK 源码对于这个方法的说明: - -```java -/** - *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁,与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。 - */ -public static List asList(T... a) { - return new ArrayList<>(a); -} -``` - -### 2.1.2. 《阿里巴巴Java 开发手册》对其的描述 - -`Arrays.asList()`将数组转换为集合后,底层其实还是数组,《阿里巴巴Java 开发手册》对于这个方法有如下描述: - -![阿里巴巴Java开发手-Arrays.asList()方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/阿里巴巴Java开发手-Arrays.asList()方法.png) - -### 2.1.3. 使用时的注意事项总结 - -**传递的数组必须是对象数组,而不是基本类型。** - -`Arrays.asList()`是泛型方法,传入的对象必须是对象数组。 - -```java -int[] myArray = { 1, 2, 3 }; -List myList = Arrays.asList(myArray); -System.out.println(myList.size());//1 -System.out.println(myList.get(0));//数组地址值 -System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException -int [] array=(int[]) myList.get(0); -System.out.println(array[0]);//1 -``` -当传入一个原生数据类型数组时,`Arrays.asList()` 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时List 的唯一元素就是这个数组,这也就解释了上面的代码。 - -我们使用包装类型数组就可以解决这个问题。 - -```java -Integer[] myArray = { 1, 2, 3 }; -``` - -**使用集合的修改方法:`add()`、`remove()`、`clear()`会抛出异常。** - -```java -List myList = Arrays.asList(1, 2, 3); -myList.add(4);//运行时报错:UnsupportedOperationException -myList.remove(1);//运行时报错:UnsupportedOperationException -myList.clear();//运行时报错:UnsupportedOperationException -``` - -`Arrays.asList()` 方法返回的并不是 `java.util.ArrayList` ,而是 `java.util.Arrays` 的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。 - -```java -List myList = Arrays.asList(1, 2, 3); -System.out.println(myList.getClass());//class java.util.Arrays$ArrayList -``` - -下图是`java.util.Arrays$ArrayList`的简易源码,我们可以看到这个类重写的方法有哪些。 - -```java - private static class ArrayList extends AbstractList - implements RandomAccess, java.io.Serializable - { - ... - - @Override - public E get(int index) { - ... - } - - @Override - public E set(int index, E element) { - ... - } - - @Override - public int indexOf(Object o) { - ... - } - - @Override - public boolean contains(Object o) { - ... - } - - @Override - public void forEach(Consumer action) { - ... - } - - @Override - public void replaceAll(UnaryOperator operator) { - ... - } - - @Override - public void sort(Comparator c) { - ... - } - } -``` - -我们再看一下`java.util.AbstractList`的`remove()`方法,这样我们就明白为啥会抛出`UnsupportedOperationException`。 - -```java -public E remove(int index) { - throw new UnsupportedOperationException(); -} -``` - -### 2.1.4. 如何正确的将数组转换为ArrayList? - -stackoverflow:https://dwz.cn/vcBkTiTW - -**1. 自己动手实现(教育目的)** - -```java -//JDK1.5+ -static List arrayToList(final T[] array) { - final List l = new ArrayList(array.length); - - for (final T s : array) { - l.add(s); - } - return (l); -} -``` - -```java -Integer [] myArray = { 1, 2, 3 }; -System.out.println(arrayToList(myArray).getClass());//class java.util.ArrayList -``` - -**2. 最简便的方法(推荐)** - -```java -List list = new ArrayList<>(Arrays.asList("a", "b", "c")) -``` - -**3. 使用 Java8 的Stream(推荐)** - -```java -Integer [] myArray = { 1, 2, 3 }; -List myList = Arrays.stream(myArray).collect(Collectors.toList()); -//基本类型也可以实现转换(依赖boxed的装箱操作) -int [] myArray2 = { 1, 2, 3 }; -List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList()); -``` - -**4. 使用 Guava(推荐)** - -对于不可变集合,你可以使用[`ImmutableList`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java)类及其[`of()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L101)与[`copyOf()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L225)工厂方法:(参数不能为空) - -```java -List il = ImmutableList.of("string", "elements"); // from varargs -List il = ImmutableList.copyOf(aStringArray); // from array -``` -对于可变集合,你可以使用[`Lists`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java)类及其[`newArrayList()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java#L87)工厂方法: - -```java -List l1 = Lists.newArrayList(anotherListOrCollection); // from collection -List l2 = Lists.newArrayList(aStringArray); // from array -List l3 = Lists.newArrayList("or", "string", "elements"); // from varargs -``` - -**5. 使用 Apache Commons Collections** - -```java -List list = new ArrayList(); -CollectionUtils.addAll(list, str); -``` - -## 2.2. Collection.toArray()方法使用的坑&如何反转数组 - -该方法是一个泛型方法:` T[] toArray(T[] a);` 如果`toArray`方法中没有传递任何参数的话返回的是`Object`类型数组。 - -```java -String [] s= new String[]{ - "dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A" -}; -List list = Arrays.asList(s); -Collections.reverse(list); -s=list.toArray(new String[0]);//没有指定类型的话会报错 -``` - -由于JVM优化,`new String[0]`作为`Collection.toArray()`方法的参数现在使用更好,`new String[0]`就是起一个模板的作用,指定了返回数组的类型,0是为了节省空间,因为它只是为了说明返回的类型。详见: - -## 2.3. 不要在 foreach 循环里进行元素的 remove/add 操作 - -如果要进行`remove`操作,可以调用迭代器的 `remove `方法而不是集合类的 remove 方法。因为如果列表在任何时间从结构上修改创建迭代器之后,以任何方式除非通过迭代器自身`remove/add`方法,迭代器都将抛出一个`ConcurrentModificationException`,这就是单线程状态下产生的 **fail-fast 机制**。 - -> **fail-fast 机制** :多个线程对 fail-fast 集合进行修改的时,可能会抛出ConcurrentModificationException,单线程下也会出现这种情况,上面已经提到过。 - -`java.util`包下面的所有的集合类都是fail-fast的,而`java.util.concurrent`包下面的所有的类都是fail-safe的。 - -![不要在 foreach 循环里进行元素的 remove/add 操作](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/foreach-remove:add.png) - - - diff --git "a/docs/java/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" "b/docs/java/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" deleted file mode 100644 index 46c9c16..0000000 --- "a/docs/java/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" +++ /dev/null @@ -1,125 +0,0 @@ -## 泛型的实际应用 - -### 实现最小值函数 - -自己设计一个泛型的获取数组最小值的函数.并且这个方法只能接受Number的子类并且实现了Comparable接口。 - -```java -//注意:Number并没有实现Comparable -private static > T min(T[] values) { - if (values == null || values.length == 0) return null; - T min = values[0]; - for (int i = 1; i < values.length; i++) { - if (min.compareTo(values[i]) > 0) min = values[i]; - } - return min; -} -``` - -测试: - -```java -int minInteger = min(new Integer[]{1, 2, 3});//result:1 -double minDouble = min(new Double[]{1.2, 2.2, -1d});//result:-1d -String typeError = min(new String[]{"1","3"});//报错 -``` - -## 数据结构 - -### 使用数组实现栈 - -**自己实现一个栈,要求这个栈具有`push()`、`pop()`(返回栈顶元素并出栈)、`peek()` (返回栈顶元素不出栈)、`isEmpty()`、`size()`这些基本的方法。** - -提示:每次入栈之前先判断栈的容量是否够用,如果不够用就用`Arrays.copyOf()`进行扩容; - -```java -public class MyStack { - private int[] storage;//存放栈中元素的数组 - private int capacity;//栈的容量 - private int count;//栈中元素数量 - private static final int GROW_FACTOR = 2; - - //TODO:不带初始容量的构造方法。默认容量为8 - public MyStack() { - this.capacity = 8; - this.storage=new int[8]; - this.count = 0; - } - - //TODO:带初始容量的构造方法 - public MyStack(int initialCapacity) { - if (initialCapacity < 1) - throw new IllegalArgumentException("Capacity too small."); - - this.capacity = initialCapacity; - this.storage = new int[initialCapacity]; - this.count = 0; - } - - //TODO:入栈 - public void push(int value) { - if (count == capacity) { - ensureCapacity(); - } - storage[count++] = value; - } - - //TODO:确保容量大小 - private void ensureCapacity() { - int newCapacity = capacity * GROW_FACTOR; - storage = Arrays.copyOf(storage, newCapacity); - capacity = newCapacity; - } - - //TODO:返回栈顶元素并出栈 - private int pop() { - count--; - if (count == -1) - throw new IllegalArgumentException("Stack is empty."); - - return storage[count]; - } - - //TODO:返回栈顶元素不出栈 - private int peek() { - if (count == 0){ - throw new IllegalArgumentException("Stack is empty."); - }else { - return storage[count-1]; - } - } - - //TODO:判断栈是否为空 - private boolean isEmpty() { - return count == 0; - } - - //TODO:返回栈中元素的个数 - private int size() { - return count; - } - -} - -``` - -验证 - -```java -MyStack myStack = new MyStack(3); -myStack.push(1); -myStack.push(2); -myStack.push(3); -myStack.push(4); -myStack.push(5); -myStack.push(6); -myStack.push(7); -myStack.push(8); -System.out.println(myStack.peek());//8 -System.out.println(myStack.size());//8 -for (int i = 0; i < 8; i++) { - System.out.println(myStack.pop()); -} -System.out.println(myStack.isEmpty());//true -myStack.pop();//报错:java.lang.IllegalArgumentException: Stack is empty. -``` \ No newline at end of file diff --git "a/docs/java/Java\347\274\226\347\250\213\350\247\204\350\214\203.md" "b/docs/java/Java\347\274\226\347\250\213\350\247\204\350\214\203.md" deleted file mode 100644 index 6b4731e..0000000 --- "a/docs/java/Java\347\274\226\347\250\213\350\247\204\350\214\203.md" +++ /dev/null @@ -1,30 +0,0 @@ -讲真的,下面推荐的文章或者资源建议阅读 3 遍以上。 - -### 团队 - -- **阿里巴巴Java开发手册(详尽版)** -- **Google Java编程风格指南:** - -### 个人 - -- **程序员你为什么这么累:** - -### 如何写出优雅的 Java 代码 - -1. 使用 IntelliJ IDEA 作为您的集成开发环境 (IDE) -1. 使用 JDK 8 或更高版本 -1. 使用 Maven/Gradle -1. 使用 Lombok -1. 编写单元测试 -1. 重构:常见,但也很慢 -1. 注意代码规范 -1. 定期联络客户,以获取他们的反馈 - -上述建议的详细内容:[八点建议助您写出优雅的Java代码](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485140&idx=1&sn=ecaeace613474f1859aaeed0282ae680&chksm=cea2491ff9d5c00982ffaece847ce1aead89fdb3fe190752d9837c075c79fc95db5940992c56&token=1328169465&lang=zh_CN&scene=21#wechat_redirect)。 - -更多代码优化相关内容推荐: - -- [业务复杂=if else?刚来的大神竟然用策略+工厂彻底干掉了他们!](https://juejin.im/post/5dad23685188251d2c4ea2b6) -- [一些不错的 Java 实践!推荐阅读3遍以上!](http://lrwinx.github.io/2017/03/04/%E7%BB%86%E6%80%9D%E6%9E%81%E6%81%90-%E4%BD%A0%E7%9C%9F%E7%9A%84%E4%BC%9A%E5%86%99java%E5%90%97/) -- [[解锁新姿势] 兄dei,你代码需要优化了](https://juejin.im/post/5dafbc02e51d4524a0060bdd) -- [消灭 Java 代码的“坏味道”](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485599&idx=1&sn=d83ff4e6b1ee951a0a33508a10980ea3&chksm=cea24754f9d5ce426d18b435a8c373ddc580c06c7d6a45cc51377361729c31c7301f1bbc3b78&token=1328169465&lang=zh_CN#rd) \ No newline at end of file diff --git a/docs/java/Multithread/AQS.md b/docs/java/Multithread/AQS.md deleted file mode 100644 index ab39908..0000000 --- a/docs/java/Multithread/AQS.md +++ /dev/null @@ -1,713 +0,0 @@ -点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 - - - -- [1 AQS 简单介绍](#1-aqs-简单介绍) -- [2 AQS 原理](#2-aqs-原理) - - [2.1 AQS 原理概览](#21-aqs-原理概览) - - [2.2 AQS 对资源的共享方式](#22-aqs-对资源的共享方式) - - [2.3 AQS 底层使用了模板方法模式](#23-aqs-底层使用了模板方法模式) -- [3 Semaphore(信号量)-允许多个线程同时访问](#3-semaphore信号量-允许多个线程同时访问) -- [4 CountDownLatch (倒计时器)](#4-countdownlatch-倒计时器) - - [4.1 CountDownLatch 的三种典型用法](#41-countdownlatch-的三种典型用法) - - [4.2 CountDownLatch 的使用示例](#42-countdownlatch-的使用示例) - - [4.3 CountDownLatch 的不足](#43-countdownlatch-的不足) - - [4.4 CountDownLatch 常见面试题](#44-countdownlatch-相常见面试题) -- [5 CyclicBarrier(循环栅栏)](#5-cyclicbarrier循环栅栏) - - [5.1 CyclicBarrier 的应用场景](#51-cyclicbarrier-的应用场景) - - [5.2 CyclicBarrier 的使用示例](#52-cyclicbarrier-的使用示例) - - [5.3 `CyclicBarrier`源码分析](#53-cyclicbarrier源码分析) - - [5.4 CyclicBarrier 和 CountDownLatch 的区别](#54-cyclicbarrier-和-countdownlatch-的区别) -- [6 ReentrantLock 和 ReentrantReadWriteLock](#6-reentrantlock-和-reentrantreadwritelock) -- [参考](#参考) -- [公众号](#公众号) - - - -> 常见问题:AQS 原理?;CountDownLatch 和 CyclicBarrier 了解吗,两者的区别是什么?用过 Semaphore 吗? - -### 1 AQS 简单介绍 - -AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。 - -![enter image description here](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/AQS.png) - -AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。 - -### 2 AQS 原理 - -> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参考,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 - -下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。 - -#### 2.1 AQS 原理概览 - -**AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。** - -> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。 - -看个 AQS(AbstractQueuedSynchronizer)原理图: - -![enter image description here](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/CLH.png) - -AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。 - -```java -private volatile int state;//共享变量,使用volatile修饰保证线程可见性 -``` - -状态信息通过 protected 类型的`getState`,`setState`,`compareAndSetState`进行操作 - -```java -//返回同步状态的当前值 -protected final int getState() { - return state; -} - // 设置同步状态的值 -protected final void setState(int newState) { - state = newState; -} -//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) -protected final boolean compareAndSetState(int expect, int update) { - return unsafe.compareAndSwapInt(this, stateOffset, expect, update); -} -``` - -#### 2.2 AQS 对资源的共享方式 - -**AQS 定义两种资源共享方式** - -**1)Exclusive**(独占) - -只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁,下面以 ReentrantLock 对这两种锁的定义做介绍: - -- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 -- 非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。 - -> 说明:下面这部分关于 `ReentrantLock` 源代码内容节选自:https://www.javadoop.com/post/AbstractQueuedSynchronizer-2,这是一篇很不错文章,推荐阅读。 - -**下面来看 ReentrantLock 中相关的源代码:** - -ReentrantLock 默认采用非公平锁,因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁)。 - -```java -/** Synchronizer providing all implementation mechanics */ -private final Sync sync; -public ReentrantLock() { - // 默认非公平锁 - sync = new NonfairSync(); -} -public ReentrantLock(boolean fair) { - sync = fair ? new FairSync() : new NonfairSync(); -} -``` - -ReentrantLock 中公平锁的 `lock` 方法 - -```java -static final class FairSync extends Sync { - final void lock() { - acquire(1); - } - // AbstractQueuedSynchronizer.acquire(int arg) - public final void acquire(int arg) { - if (!tryAcquire(arg) && - acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); - } - protected final boolean tryAcquire(int acquires) { - final Thread current = Thread.currentThread(); - int c = getState(); - if (c == 0) { - // 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待 - if (!hasQueuedPredecessors() && - compareAndSetState(0, acquires)) { - setExclusiveOwnerThread(current); - return true; - } - } - else if (current == getExclusiveOwnerThread()) { - int nextc = c + acquires; - if (nextc < 0) - throw new Error("Maximum lock count exceeded"); - setState(nextc); - return true; - } - return false; - } -} -``` - -非公平锁的 lock 方法: - -```java -static final class NonfairSync extends Sync { - final void lock() { - // 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了 - if (compareAndSetState(0, 1)) - setExclusiveOwnerThread(Thread.currentThread()); - else - acquire(1); - } - // AbstractQueuedSynchronizer.acquire(int arg) - public final void acquire(int arg) { - if (!tryAcquire(arg) && - acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); - } - protected final boolean tryAcquire(int acquires) { - return nonfairTryAcquire(acquires); - } -} -/** - * Performs non-fair tryLock. tryAcquire is implemented in - * subclasses, but both need nonfair try for trylock method. - */ -final boolean nonfairTryAcquire(int acquires) { - final Thread current = Thread.currentThread(); - int c = getState(); - if (c == 0) { - // 这里没有对阻塞队列进行判断 - if (compareAndSetState(0, acquires)) { - setExclusiveOwnerThread(current); - return true; - } - } - else if (current == getExclusiveOwnerThread()) { - int nextc = c + acquires; - if (nextc < 0) // overflow - throw new Error("Maximum lock count exceeded"); - setState(nextc); - return true; - } - return false; -} -``` - -总结:公平锁和非公平锁只有两处不同: - -1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。 -2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。 - -公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。 - -相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。 - -**2)Share**(共享) - -多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。 - -ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。 - -不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在上层已经帮我们实现好了。 - -#### 2.3 AQS 底层使用了模板方法模式 - -同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): - -1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放) -2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 - -这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用,下面简单的给大家介绍一下模板方法模式,模板方法模式是一个很容易理解的设计模式之一。 - -> 模板方法模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码。举个很简单的例子假如我们要去一个地方的步骤是:购票`buyTicket()`->安检`securityCheck()`->乘坐某某工具回家`ride()`->到达目的地`arrive()`。我们可能乘坐不同的交通工具回家比如飞机或者火车,所以除了`ride()`方法,其他方法的实现几乎相同。我们可以定义一个包含了这些方法的抽象类,然后用户根据自己的需要继承该抽象类然后修改 `ride()`方法。 - -**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:** - -```java -isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 -tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 -tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 -tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 -tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 - -``` - -默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 - -以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。 - -再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。 - -一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 - -推荐两篇 AQS 原理和相关源码分析的文章: - -- http://www.cnblogs.com/waterystone/p/4920797.html -- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html - -### 3 Semaphore(信号量)-允许多个线程同时访问 - -**synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。** 示例代码如下: - -```java -/** - * - * @author Snailclimb - * @date 2018年9月30日 - * @Description: 需要一次性拿一个许可的情况 - */ -public class SemaphoreExample1 { - // 请求的数量 - private static final int threadCount = 550; - - public static void main(String[] args) throws InterruptedException { - // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) - ExecutorService threadPool = Executors.newFixedThreadPool(300); - // 一次只能允许执行的线程数量。 - final Semaphore semaphore = new Semaphore(20); - - for (int i = 0; i < threadCount; i++) { - final int threadnum = i; - threadPool.execute(() -> {// Lambda 表达式的运用 - try { - semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20 - test(threadnum); - semaphore.release();// 释放一个许可 - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - }); - } - threadPool.shutdown(); - System.out.println("finish"); - } - - public static void test(int threadnum) throws InterruptedException { - Thread.sleep(1000);// 模拟请求的耗时操作 - System.out.println("threadnum:" + threadnum); - Thread.sleep(1000);// 模拟请求的耗时操作 - } -} -``` - -执行 `acquire` 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 `release` 方法增加一个许可证,这可能会释放一个阻塞的 acquire 方法。然而,其实并没有实际的许可证这个对象,Semaphore 只是维持了一个可获得许可证的数量。 Semaphore 经常用于限制获取某种资源的线程数量。 - -当然一次也可以一次拿取和释放多个许可,不过一般没有必要这样做: - -```java - semaphore.acquire(5);// 获取5个许可,所以可运行线程数量为20/5=4 - test(threadnum); - semaphore.release(5);// 获取5个许可,所以可运行线程数量为20/5=4 -``` - -除了 `acquire`方法之外,另一个比较常用的与之对应的方法是`tryAcquire`方法,该方法如果获取不到许可就立即返回 false。 - -Semaphore 有两种模式,公平模式和非公平模式。 - -- **公平模式:** 调用 acquire 的顺序就是获取许可证的顺序,遵循 FIFO; -- **非公平模式:** 抢占式的。 - -**Semaphore 对应的两个构造方法如下:** - -```java - public Semaphore(int permits) { - sync = new NonfairSync(permits); - } - - public Semaphore(int permits, boolean fair) { - sync = fair ? new FairSync(permits) : new NonfairSync(permits); - } -``` - -**这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。** - -由于篇幅问题,如果对 Semaphore 源码感兴趣的朋友可以看下面这篇文章: - -- https://blog.csdn.net/qq_19431333/article/details/70212663 - -### 4 CountDownLatch (倒计时器) - -CountDownLatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。在 Java 并发中,countdownlatch 的概念是一个常见的面试题,所以一定要确保你很好的理解了它。 - -#### 4.1 CountDownLatch 的三种典型用法 - -① 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :`new CountDownLatch(n)`,每当一个任务线程执行完毕,就将计数器减 1 `countdownlatch.countDown()`,当计数器的值变为 0 时,在`CountDownLatch上 await()` 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 - -② 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 `CountDownLatch` 对象,将其计数器初始化为 1 :`new CountDownLatch(1)`,多个线程在开始执行任务前首先 `coundownlatch.await()`,当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。 - -③ 死锁检测:一个非常方便的使用场景是,你可以使用 n 个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。 - -#### 4.2 CountDownLatch 的使用示例 - -```java -/** - * - * @author SnailClimb - * @date 2018年10月1日 - * @Description: CountDownLatch 使用方法示例 - */ -public class CountDownLatchExample1 { - // 请求的数量 - private static final int threadCount = 550; - - public static void main(String[] args) throws InterruptedException { - // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) - ExecutorService threadPool = Executors.newFixedThreadPool(300); - final CountDownLatch countDownLatch = new CountDownLatch(threadCount); - for (int i = 0; i < threadCount; i++) { - final int threadnum = i; - threadPool.execute(() -> {// Lambda 表达式的运用 - try { - test(threadnum); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } finally { - countDownLatch.countDown();// 表示一个请求已经被完成 - } - - }); - } - countDownLatch.await(); - threadPool.shutdown(); - System.out.println("finish"); - } - - public static void test(int threadnum) throws InterruptedException { - Thread.sleep(1000);// 模拟请求的耗时操作 - System.out.println("threadnum:" + threadnum); - Thread.sleep(1000);// 模拟请求的耗时操作 - } -} - -``` - -上面的代码中,我们定义了请求的数量为 550,当这 550 个请求被处理完成之后,才会执行`System.out.println("finish");`。 - -与 CountDownLatch 的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用 CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。 - -其他 N 个线程必须引用闭锁对象,因为他们需要通知 CountDownLatch 对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的 count 值就减 1。所以当 N 个线程都调 用了这个方法,count 的值等于 0,然后主线程就能通过 await()方法,恢复执行自己的任务。 - -#### 4.3 CountDownLatch 的不足 - -CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。 - -#### 4.4 CountDownLatch 相常见面试题: - -解释一下 CountDownLatch 概念? - -CountDownLatch 和 CyclicBarrier 的不同之处? - -给出一些 CountDownLatch 使用的例子? - -CountDownLatch 类中主要的方法? - -### 5 CyclicBarrier(循环栅栏) - -CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。 - -CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用`await`方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 - -再来看一下它的构造函数: - -```java -public CyclicBarrier(int parties) { - this(parties, null); -} - -public CyclicBarrier(int parties, Runnable barrierAction) { - if (parties <= 0) throw new IllegalArgumentException(); - this.parties = parties; - this.count = parties; - this.barrierCommand = barrierAction; -} -``` - -其中,parties 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。 - -#### 5.1 CyclicBarrier 的应用场景 - -CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。 - -#### 5.2 CyclicBarrier 的使用示例 - -示例 1: - -```java -/** - * - * @author Snailclimb - * @date 2018年10月1日 - * @Description: 测试 CyclicBarrier 类中带参数的 await() 方法 - */ -public class CyclicBarrierExample2 { - // 请求的数量 - private static final int threadCount = 550; - // 需要同步的线程数量 - private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5); - - public static void main(String[] args) throws InterruptedException { - // 创建线程池 - ExecutorService threadPool = Executors.newFixedThreadPool(10); - - for (int i = 0; i < threadCount; i++) { - final int threadNum = i; - Thread.sleep(1000); - threadPool.execute(() -> { - try { - test(threadNum); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (BrokenBarrierException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - }); - } - threadPool.shutdown(); - } - - public static void test(int threadnum) throws InterruptedException, BrokenBarrierException { - System.out.println("threadnum:" + threadnum + "is ready"); - try { - /**等待60秒,保证子线程完全执行结束*/ - cyclicBarrier.await(60, TimeUnit.SECONDS); - } catch (Exception e) { - System.out.println("-----CyclicBarrierException------"); - } - System.out.println("threadnum:" + threadnum + "is finish"); - } - -} -``` - -运行结果,如下: - -``` -threadnum:0is ready -threadnum:1is ready -threadnum:2is ready -threadnum:3is ready -threadnum:4is ready -threadnum:4is finish -threadnum:0is finish -threadnum:1is finish -threadnum:2is finish -threadnum:3is finish -threadnum:5is ready -threadnum:6is ready -threadnum:7is ready -threadnum:8is ready -threadnum:9is ready -threadnum:9is finish -threadnum:5is finish -threadnum:8is finish -threadnum:7is finish -threadnum:6is finish -...... -``` - -可以看到当线程数量也就是请求数量达到我们定义的 5 个的时候, `await`方法之后的方法才被执行。 - -另外,CyclicBarrier 还提供一个更高级的构造函数`CyclicBarrier(int parties, Runnable barrierAction)`,用于在线程到达屏障时,优先执行`barrierAction`,方便处理更复杂的业务场景。示例代码如下: - -```java -/** - * - * @author SnailClimb - * @date 2018年10月1日 - * @Description: 新建 CyclicBarrier 的时候指定一个 Runnable - */ -public class CyclicBarrierExample3 { - // 请求的数量 - private static final int threadCount = 550; - // 需要同步的线程数量 - private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> { - System.out.println("------当线程数达到之后,优先执行------"); - }); - - public static void main(String[] args) throws InterruptedException { - // 创建线程池 - ExecutorService threadPool = Executors.newFixedThreadPool(10); - - for (int i = 0; i < threadCount; i++) { - final int threadNum = i; - Thread.sleep(1000); - threadPool.execute(() -> { - try { - test(threadNum); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (BrokenBarrierException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - }); - } - threadPool.shutdown(); - } - - public static void test(int threadnum) throws InterruptedException, BrokenBarrierException { - System.out.println("threadnum:" + threadnum + "is ready"); - cyclicBarrier.await(); - System.out.println("threadnum:" + threadnum + "is finish"); - } - -} -``` - -运行结果,如下: - -``` -threadnum:0is ready -threadnum:1is ready -threadnum:2is ready -threadnum:3is ready -threadnum:4is ready -------当线程数达到之后,优先执行------ -threadnum:4is finish -threadnum:0is finish -threadnum:2is finish -threadnum:1is finish -threadnum:3is finish -threadnum:5is ready -threadnum:6is ready -threadnum:7is ready -threadnum:8is ready -threadnum:9is ready -------当线程数达到之后,优先执行------ -threadnum:9is finish -threadnum:5is finish -threadnum:6is finish -threadnum:8is finish -threadnum:7is finish -...... -``` - -#### 5.3 `CyclicBarrier`源码分析 - -当调用 `CyclicBarrier` 对象调用 `await()` 方法时,实际上调用的是`dowait(false, 0L)`方法。 `await()` 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 parties 的值时,栅栏才会打开,线程才得以通过执行。 - -```java - public int await() throws InterruptedException, BrokenBarrierException { - try { - return dowait(false, 0L); - } catch (TimeoutException toe) { - throw new Error(toe); // cannot happen - } - } -``` - -`dowait(false, 0L)`: - -```java - // 当线程数量或者请求数量达到 count 时 await 之后的方法才会被执行。上面的示例中 count 的值就为 5。 - private int count; - /** - * Main barrier code, covering the various policies. - */ - private int dowait(boolean timed, long nanos) - throws InterruptedException, BrokenBarrierException, - TimeoutException { - final ReentrantLock lock = this.lock; - // 锁住 - lock.lock(); - try { - final Generation g = generation; - - if (g.broken) - throw new BrokenBarrierException(); - - // 如果线程中断了,抛出异常 - if (Thread.interrupted()) { - breakBarrier(); - throw new InterruptedException(); - } - // cout减1 - int index = --count; - // 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行await 方法之后的条件 - if (index == 0) { // tripped - boolean ranAction = false; - try { - final Runnable command = barrierCommand; - if (command != null) - command.run(); - ranAction = true; - // 将 count 重置为 parties 属性的初始化值 - // 唤醒之前等待的线程 - // 下一波执行开始 - nextGeneration(); - return 0; - } finally { - if (!ranAction) - breakBarrier(); - } - } - - // loop until tripped, broken, interrupted, or timed out - for (;;) { - try { - if (!timed) - trip.await(); - else if (nanos > 0L) - nanos = trip.awaitNanos(nanos); - } catch (InterruptedException ie) { - if (g == generation && ! g.broken) { - breakBarrier(); - throw ie; - } else { - // We're about to finish waiting even if we had not - // been interrupted, so this interrupt is deemed to - // "belong" to subsequent execution. - Thread.currentThread().interrupt(); - } - } - - if (g.broken) - throw new BrokenBarrierException(); - - if (g != generation) - return index; - - if (timed && nanos <= 0L) { - breakBarrier(); - throw new TimeoutException(); - } - } - } finally { - lock.unlock(); - } - } - -``` - -总结:`CyclicBarrier` 内部通过一个 count 变量作为计数器,cout 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。 - -#### 5.4 CyclicBarrier 和 CountDownLatch 的区别 - -**下面这个是国外一个大佬的回答:** - -CountDownLatch 是计数器,只能使用一次,而 CyclicBarrier 的计数器提供 reset 功能,可以多次使用。但是我不那么认为它们之间的区别仅仅就是这么简单的一点。我们来从 jdk 作者设计的目的来看,javadoc 是这么描述它们的: - -> CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.(CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;) -> CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。) - -对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。 - -CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。 - -### 6 ReentrantLock 和 ReentrantReadWriteLock - -ReentrantLock 和 synchronized 的区别在上面已经讲过了这里就不多做讲解。另外,需要注意的是:读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。 - -### 参考 - -- https://juejin.im/post/5ae755256fb9a07ac3634067 -- https://blog.csdn.net/u010185262/article/details/54692886 -- https://blog.csdn.net/tolcf/article/details/50925145?utm_source=blogxgwz0 - -### 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取! - -**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) - diff --git a/docs/java/Multithread/Atomic.md b/docs/java/Multithread/Atomic.md deleted file mode 100644 index 627ea3d..0000000 --- a/docs/java/Multithread/Atomic.md +++ /dev/null @@ -1,556 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - -> 个人觉得这一节掌握基本的使用即可! - - - -- [1 Atomic 原子类介绍](#1-atomic-原子类介绍) -- [2 基本类型原子类](#2-基本类型原子类) - - [2.1 基本类型原子类介绍](#21-基本类型原子类介绍) - - [2.2 AtomicInteger 常见方法使用](#22-atomicinteger-常见方法使用) - - [2.3 基本数据类型原子类的优势](#23-基本数据类型原子类的优势) - - [2.4 AtomicInteger 线程安全原理简单分析](#24-atomicinteger-线程安全原理简单分析) -- [3 数组类型原子类](#3-数组类型原子类) - - [3.1 数组类型原子类介绍](#31-数组类型原子类介绍) - - [3.2 AtomicIntegerArray 常见方法使用](#32-atomicintegerarray-常见方法使用) -- [4 引用类型原子类](#4-引用类型原子类) - - [4.1 引用类型原子类介绍](#41--引用类型原子类介绍) - - [4.2 AtomicReference 类使用示例](#42-atomicreference-类使用示例) - - [4.3 AtomicStampedReference 类使用示例](#43-atomicstampedreference-类使用示例) - - [4.4 AtomicMarkableReference 类使用示例](#44-atomicmarkablereference-类使用示例) -- [5 对象的属性修改类型原子类](#5-对象的属性修改类型原子类) - - [5.1 对象的属性修改类型原子类介绍](#51-对象的属性修改类型原子类介绍) - - [5.2 AtomicIntegerFieldUpdater 类使用示例](#52-atomicintegerfieldupdater-类使用示例) - - - -### 1 Atomic 原子类介绍 - -Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 - -所以,所谓原子类说简单点就是具有原子/原子操作特征的类。 - -并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。 - -![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) - -根据操作的数据类型,可以将JUC包中的原子类分为4类 - -**基本类型** - -使用原子的方式更新基本类型 - -- AtomicInteger:整型原子类 -- AtomicLong:长整型原子类 -- AtomicBoolean :布尔型原子类 - -**数组类型** - -使用原子的方式更新数组里的某个元素 - - -- AtomicIntegerArray:整型数组原子类 -- AtomicLongArray:长整型数组原子类 -- AtomicReferenceArray :引用类型数组原子类 - -**引用类型** - -- AtomicReference:引用类型原子类 -- AtomicReferenceFieldUpdater:原子更新引用类型里的字段 -- AtomicMarkableReference :原子更新带有标记位的引用类型 - -**对象的属性修改类型** - -- AtomicIntegerFieldUpdater:原子更新整型字段的更新器 -- AtomicLongFieldUpdater:原子更新长整型字段的更新器 -- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 -- AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - -**CAS ABA 问题** -- 描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功。 -- 例子描述(可能不太合适,但好理解): 年初,现金为零,然后通过正常劳动赚了三百万,之后正常消费了(比如买房子)三百万。年末,虽然现金零收入(可能变成其他形式了),但是赚了钱是事实,还是得交税的! -- 代码例子(以``` AtomicInteger ```为例) - -```java -import java.util.concurrent.atomic.AtomicInteger; - -public class AtomicIntegerDefectDemo { - public static void main(String[] args) { - defectOfABA(); - } - - static void defectOfABA() { - final AtomicInteger atomicInteger = new AtomicInteger(1); - - Thread coreThread = new Thread( - () -> { - final int currentValue = atomicInteger.get(); - System.out.println(Thread.currentThread().getName() + " ------ currentValue=" + currentValue); - - // 这段目的:模拟处理其他业务花费的时间 - try { - Thread.sleep(300); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - boolean casResult = atomicInteger.compareAndSet(1, 2); - System.out.println(Thread.currentThread().getName() - + " ------ currentValue=" + currentValue - + ", finalValue=" + atomicInteger.get() - + ", compareAndSet Result=" + casResult); - } - ); - coreThread.start(); - - // 这段目的:为了让 coreThread 线程先跑起来 - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - Thread amateurThread = new Thread( - () -> { - int currentValue = atomicInteger.get(); - boolean casResult = atomicInteger.compareAndSet(1, 2); - System.out.println(Thread.currentThread().getName() - + " ------ currentValue=" + currentValue - + ", finalValue=" + atomicInteger.get() - + ", compareAndSet Result=" + casResult); - - currentValue = atomicInteger.get(); - casResult = atomicInteger.compareAndSet(2, 1); - System.out.println(Thread.currentThread().getName() - + " ------ currentValue=" + currentValue - + ", finalValue=" + atomicInteger.get() - + ", compareAndSet Result=" + casResult); - } - ); - amateurThread.start(); - } -} -``` - -输出内容如下: - -``` -Thread-0 ------ currentValue=1 -Thread-1 ------ currentValue=1, finalValue=2, compareAndSet Result=true -Thread-1 ------ currentValue=2, finalValue=1, compareAndSet Result=true -Thread-0 ------ currentValue=1, finalValue=2, compareAndSet Result=true -``` - -下面我们来详细介绍一下这些原子类。 - -### 2 基本类型原子类 - -#### 2.1 基本类型原子类介绍 - -使用原子的方式更新基本类型 - -- AtomicInteger:整型原子类 -- AtomicLong:长整型原子类 -- AtomicBoolean :布尔型原子类 - -上面三个类提供的方法几乎相同,所以我们这里以 AtomicInteger 为例子来介绍。 - - **AtomicInteger 类常用方法** - -```java -public final int get() //获取当前的值 -public final int getAndSet(int newValue)//获取当前的值,并设置新的值 -public final int getAndIncrement()//获取当前的值,并自增 -public final int getAndDecrement() //获取当前的值,并自减 -public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 -boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) -public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 -``` - -#### 2.2 AtomicInteger 常见方法使用 - -```java -import java.util.concurrent.atomic.AtomicInteger; - -public class AtomicIntegerTest { - - public static void main(String[] args) { - // TODO Auto-generated method stub - int temvalue = 0; - AtomicInteger i = new AtomicInteger(0); - temvalue = i.getAndSet(3); - System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:0; i:3 - temvalue = i.getAndIncrement(); - System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:3; i:4 - temvalue = i.getAndAdd(5); - System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:4; i:9 - } - -} -``` - -#### 2.3 基本数据类型原子类的优势 - -通过一个简单例子带大家看一下基本数据类型原子类的优势 - -**①多线程环境不使用原子类保证线程安全(基本数据类型)** - -```java -class Test { - private volatile int count = 0; - //若要线程安全执行执行count++,需要加锁 - public synchronized void increment() { - count++; - } - - public int getCount() { - return count; - } -} -``` -**②多线程环境使用原子类保证线程安全(基本数据类型)** - -```java -class Test2 { - private AtomicInteger count = new AtomicInteger(); - - public void increment() { - count.incrementAndGet(); - } - //使用AtomicInteger之后,不需要加锁,也可以实现线程安全。 - public int getCount() { - return count.get(); - } -} - -``` -#### 2.4 AtomicInteger 线程安全原理简单分析 - -AtomicInteger 类的部分源码: - -```java - // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) - private static final Unsafe unsafe = Unsafe.getUnsafe(); - private static final long valueOffset; - - static { - try { - valueOffset = unsafe.objectFieldOffset - (AtomicInteger.class.getDeclaredField("value")); - } catch (Exception ex) { throw new Error(ex); } - } - - private volatile int value; -``` - -AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 - -CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 - - -### 3 数组类型原子类 - -#### 3.1 数组类型原子类介绍 - -使用原子的方式更新数组里的某个元素 - - -- AtomicIntegerArray:整形数组原子类 -- AtomicLongArray:长整形数组原子类 -- AtomicReferenceArray :引用类型数组原子类 - -上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。 - -**AtomicIntegerArray 类常用方法** - -```java -public final int get(int i) //获取 index=i 位置元素的值 -public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue -public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增 -public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减 -public final int getAndAdd(int delta) //获取 index=i 位置元素的值,并加上预期的值 -boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update) -public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 -``` -#### 3.2 AtomicIntegerArray 常见方法使用 - -```java - -import java.util.concurrent.atomic.AtomicIntegerArray; - -public class AtomicIntegerArrayTest { - - public static void main(String[] args) { - // TODO Auto-generated method stub - int temvalue = 0; - int[] nums = { 1, 2, 3, 4, 5, 6 }; - AtomicIntegerArray i = new AtomicIntegerArray(nums); - for (int j = 0; j < nums.length; j++) { - System.out.println(i.get(j)); - } - temvalue = i.getAndSet(0, 2); - System.out.println("temvalue:" + temvalue + "; i:" + i); - temvalue = i.getAndIncrement(0); - System.out.println("temvalue:" + temvalue + "; i:" + i); - temvalue = i.getAndAdd(0, 5); - System.out.println("temvalue:" + temvalue + "; i:" + i); - } - -} -``` - -### 4 引用类型原子类 - -#### 4.1 引用类型原子类介绍 - -基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。 - -- AtomicReference:引用类型原子类 -- AtomicStampedReference:原子更新引用类型里的字段原子类 -- AtomicMarkableReference :原子更新带有标记位的引用类型 - -上面三个类提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。 - -#### 4.2 AtomicReference 类使用示例 - -```java -import java.util.concurrent.atomic.AtomicReference; - -public class AtomicReferenceTest { - - public static void main(String[] args) { - AtomicReference ar = new AtomicReference(); - Person person = new Person("SnailClimb", 22); - ar.set(person); - Person updatePerson = new Person("Daisy", 20); - ar.compareAndSet(person, updatePerson); - - System.out.println(ar.get().getName()); - System.out.println(ar.get().getAge()); - } -} - -class Person { - private String name; - private int age; - - public Person(String name, int age) { - super(); - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - -} -``` -上述代码首先创建了一个 Person 对象,然后把 Person 对象设置进 AtomicReference 对象中,然后调用 compareAndSet 方法,该方法就是通过 CAS 操作设置 ar。如果 ar 的值为 person 的话,则将其设置为 updatePerson。实现原理与 AtomicInteger 类中的 compareAndSet 方法相同。运行上面的代码后的输出结果如下: - -``` -Daisy -20 -``` -#### 4.3 AtomicStampedReference 类使用示例 - -```java -import java.util.concurrent.atomic.AtomicStampedReference; - -public class AtomicStampedReferenceDemo { - public static void main(String[] args) { - // 实例化、取当前值和 stamp 值 - final Integer initialRef = 0, initialStamp = 0; - final AtomicStampedReference asr = new AtomicStampedReference<>(initialRef, initialStamp); - System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp()); - - // compare and set - final Integer newReference = 666, newStamp = 999; - final boolean casResult = asr.compareAndSet(initialRef, newReference, initialStamp, newStamp); - System.out.println("currentValue=" + asr.getReference() - + ", currentStamp=" + asr.getStamp() - + ", casResult=" + casResult); - - // 获取当前的值和当前的 stamp 值 - int[] arr = new int[1]; - final Integer currentValue = asr.get(arr); - final int currentStamp = arr[0]; - System.out.println("currentValue=" + currentValue + ", currentStamp=" + currentStamp); - - // 单独设置 stamp 值 - final boolean attemptStampResult = asr.attemptStamp(newReference, 88); - System.out.println("currentValue=" + asr.getReference() - + ", currentStamp=" + asr.getStamp() - + ", attemptStampResult=" + attemptStampResult); - - // 重新设置当前值和 stamp 值 - asr.set(initialRef, initialStamp); - System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp()); - - // [不推荐使用,除非搞清楚注释的意思了] weak compare and set - // 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191] - // 但是注释上写着 "May fail spuriously and does not provide ordering guarantees, - // so is only rarely an appropriate alternative to compareAndSet." - // todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发 - final boolean wCasResult = asr.weakCompareAndSet(initialRef, newReference, initialStamp, newStamp); - System.out.println("currentValue=" + asr.getReference() - + ", currentStamp=" + asr.getStamp() - + ", wCasResult=" + wCasResult); - } -} -``` - -输出结果如下: -``` -currentValue=0, currentStamp=0 -currentValue=666, currentStamp=999, casResult=true -currentValue=666, currentStamp=999 -currentValue=666, currentStamp=88, attemptStampResult=true -currentValue=0, currentStamp=0 -currentValue=666, currentStamp=999, wCasResult=true -``` - -#### 4.4 AtomicMarkableReference 类使用示例 - -``` java -import java.util.concurrent.atomic.AtomicMarkableReference; - -public class AtomicMarkableReferenceDemo { - public static void main(String[] args) { - // 实例化、取当前值和 mark 值 - final Boolean initialRef = null, initialMark = false; - final AtomicMarkableReference amr = new AtomicMarkableReference<>(initialRef, initialMark); - System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked()); - - // compare and set - final Boolean newReference1 = true, newMark1 = true; - final boolean casResult = amr.compareAndSet(initialRef, newReference1, initialMark, newMark1); - System.out.println("currentValue=" + amr.getReference() - + ", currentMark=" + amr.isMarked() - + ", casResult=" + casResult); - - // 获取当前的值和当前的 mark 值 - boolean[] arr = new boolean[1]; - final Boolean currentValue = amr.get(arr); - final boolean currentMark = arr[0]; - System.out.println("currentValue=" + currentValue + ", currentMark=" + currentMark); - - // 单独设置 mark 值 - final boolean attemptMarkResult = amr.attemptMark(newReference1, false); - System.out.println("currentValue=" + amr.getReference() - + ", currentMark=" + amr.isMarked() - + ", attemptMarkResult=" + attemptMarkResult); - - // 重新设置当前值和 mark 值 - amr.set(initialRef, initialMark); - System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked()); - - // [不推荐使用,除非搞清楚注释的意思了] weak compare and set - // 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191] - // 但是注释上写着 "May fail spuriously and does not provide ordering guarantees, - // so is only rarely an appropriate alternative to compareAndSet." - // todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发 - final boolean wCasResult = amr.weakCompareAndSet(initialRef, newReference1, initialMark, newMark1); - System.out.println("currentValue=" + amr.getReference() - + ", currentMark=" + amr.isMarked() - + ", wCasResult=" + wCasResult); - } -} -``` - -输出结果如下: -``` -currentValue=null, currentMark=false -currentValue=true, currentMark=true, casResult=true -currentValue=true, currentMark=true -currentValue=true, currentMark=false, attemptMarkResult=true -currentValue=null, currentMark=false -currentValue=true, currentMark=true, wCasResult=true -``` - -### 5 对象的属性修改类型原子类 - -#### 5.1 对象的属性修改类型原子类介绍 - -如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类。 - -- AtomicIntegerFieldUpdater:原子更新整形字段的更新器 -- AtomicLongFieldUpdater:原子更新长整形字段的更新器 -- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - -要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符。 - -上面三个类提供的方法几乎相同,所以我们这里以 `AtomicIntegerFieldUpdater`为例子来介绍。 - -#### 5.2 AtomicIntegerFieldUpdater 类使用示例 - -```java -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; - -public class AtomicIntegerFieldUpdaterTest { - public static void main(String[] args) { - AtomicIntegerFieldUpdater a = AtomicIntegerFieldUpdater.newUpdater(User.class, "age"); - - User user = new User("Java", 22); - System.out.println(a.getAndIncrement(user));// 22 - System.out.println(a.get(user));// 23 - } -} - -class User { - private String name; - public volatile int age; - - public User(String name, int age) { - super(); - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - -} -``` - -输出结果: - -``` -22 -23 -``` - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md b/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md deleted file mode 100644 index 51b21fd..0000000 --- a/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md +++ /dev/null @@ -1,929 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [Java 并发进阶常见面试题总结](#java-并发进阶常见面试题总结) - - [1. synchronized 关键字](#1-synchronized-关键字) - - [1.1. 说一说自己对于 synchronized 关键字的了解](#11-说一说自己对于-synchronized-关键字的了解) - - [1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗](#12-说说自己是怎么使用-synchronized-关键字在项目中用到了吗) - - [1.3. 讲一下 synchronized 关键字的底层原理](#13-讲一下-synchronized-关键字的底层原理) - - [1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗](#14-说说-jdk16-之后的synchronized-关键字底层做了哪些优化可以详细介绍一下这些优化吗) - - [1.5. 谈谈 synchronized和ReentrantLock 的区别](#15-谈谈-synchronized和reentrantlock-的区别) - - [2. volatile关键字](#2-volatile关键字) - - [2.1. 讲一下Java内存模型](#21-讲一下java内存模型) - - [2.2. 说说 synchronized 关键字和 volatile 关键字的区别](#22-说说-synchronized-关键字和-volatile-关键字的区别) - - [3. ThreadLocal](#3-threadlocal) - - [3.1. ThreadLocal简介](#31-threadlocal简介) - - [3.2. ThreadLocal示例](#32-threadlocal示例) - - [3.3. ThreadLocal原理](#33-threadlocal原理) - - [3.4. ThreadLocal 内存泄露问题](#34-threadlocal-内存泄露问题) - - [4. 线程池](#4-线程池) - - [4.1. 为什么要用线程池?](#41-为什么要用线程池) - - [4.2. 实现Runnable接口和Callable接口的区别](#42-实现runnable接口和callable接口的区别) - - [4.3. 执行execute()方法和submit()方法的区别是什么呢?](#43-执行execute方法和submit方法的区别是什么呢) - - [4.4. 如何创建线程池](#44-如何创建线程池) - - [5. Atomic 原子类](#5-atomic-原子类) - - [5.1. 介绍一下Atomic 原子类](#51-介绍一下atomic-原子类) - - [5.2. JUC 包中的原子类是哪4类?](#52-juc-包中的原子类是哪4类) - - [5.3. 讲讲 AtomicInteger 的使用](#53-讲讲-atomicinteger-的使用) - - [5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理](#54-能不能给我简单介绍一下-atomicinteger-类的原理) - - [6. AQS](#6-aqs) - - [6.1. AQS 介绍](#61-aqs-介绍) - - [6.2. AQS 原理分析](#62-aqs-原理分析) - - [6.2.1. AQS 原理概览](#621-aqs-原理概览) - - [6.2.2. AQS 对资源的共享方式](#622-aqs-对资源的共享方式) - - [6.2.3. AQS底层使用了模板方法模式](#623-aqs底层使用了模板方法模式) - - [6.3. AQS 组件总结](#63-aqs-组件总结) - - [7 Reference](#7-reference) - - - -# Java 并发进阶常见面试题总结 - -## 1. synchronized 关键字 - -### 1.1. 说一说自己对于 synchronized 关键字的了解 - -synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 - -另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 - - -### 1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 - -**synchronized关键字最主要的三种使用方式:** - -- **修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 -- **修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。 -- **修饰代码块:** 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 - -**总结:** synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能! - -下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。 - -面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” - -**双重校验锁实现对象单例(线程安全)** - -```java -public class Singleton { - - private volatile static Singleton uniqueInstance; - - private Singleton() { - } - - public static Singleton getUniqueInstance() { - //先判断对象是否已经实例过,没有实例化过才进入加锁代码 - if (uniqueInstance == null) { - //类对象加锁 - synchronized (Singleton.class) { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} -``` -另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。 - -uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: - -1. 为 uniqueInstance 分配内存空间 -2. 初始化 uniqueInstance -3. 将 uniqueInstance 指向分配的内存地址 - -但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 - -使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 - -### 1.3. 讲一下 synchronized 关键字的底层原理 - -**synchronized 关键字底层原理属于 JVM 层面。** - -**① synchronized 同步语句块的情况** - -```java -public class SynchronizedDemo { - public void method() { - synchronized (this) { - System.out.println("synchronized 代码块"); - } - } -} - -``` - -通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 - -![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理.png) - -从上面我们可以看出: - -**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 - -**② synchronized 修饰方法的的情况** - -```java -public class SynchronizedDemo2 { - public synchronized void method() { - System.out.println("synchronized 方法"); - } -} - -``` - -![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理2.png) - -synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 - - -### 1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗 - -JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 - -锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 - -关于这几种优化的详细信息可以查看笔主的这篇文章: - -### 1.5. 谈谈 synchronized和ReentrantLock 的区别 - - -**① 两者都是可重入锁** - -两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 - -**② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API** - -synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - -**③ ReentrantLock 比 synchronized 增加了一些高级功能** - -相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)** - -- **ReentrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -- **ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 -- synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),**线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”** ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。 - -如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。 - -**④ 性能已不是选择标准** - -## 2. volatile关键字 - -### 2.1. 讲一下Java内存模型 - - -在 JDK1.2 之前,Java的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。 - -![数据不一致](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/数据不一致.png) - -要解决这个问题,就需要把变量声明为**volatile**,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。 - -说白了, **volatile** 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。 - -![volatile关键字的可见性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/volatile关键字的可见性.png) - - -### 2.2. 说说 synchronized 关键字和 volatile 关键字的区别 - - synchronized关键字和volatile关键字比较 - -- **volatile关键字**是线程同步的**轻量级实现**,所以**volatile性能肯定比synchronized关键字要好**。但是**volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块**。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,**实际开发中使用 synchronized 关键字的场景还是更多一些**。 -- **多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞** -- **volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。** -- **volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。** - -## 3. ThreadLocal - -### 3.1. ThreadLocal简介 - -通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** - -**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** - -再举个简单的例子: - -比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来避免这两个线程竞争的。 - -### 3.2. ThreadLocal示例 - -相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 - -```java -import java.text.SimpleDateFormat; -import java.util.Random; - -public class ThreadLocalExample implements Runnable{ - - // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 - private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); - - public static void main(String[] args) throws InterruptedException { - ThreadLocalExample obj = new ThreadLocalExample(); - for(int i=0 ; i<10; i++){ - Thread t = new Thread(obj, ""+i); - Thread.sleep(new Random().nextInt(1000)); - t.start(); - } - } - - @Override - public void run() { - System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); - try { - Thread.sleep(new Random().nextInt(1000)); - } catch (InterruptedException e) { - e.printStackTrace(); - } - //formatter pattern is changed here by thread, but it won't reflect to other threads - formatter.set(new SimpleDateFormat()); - - System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); - } - -} - -``` - -Output: - -``` -Thread Name= 0 default Formatter = yyyyMMdd HHmm -Thread Name= 0 formatter = yy-M-d ah:mm -Thread Name= 1 default Formatter = yyyyMMdd HHmm -Thread Name= 2 default Formatter = yyyyMMdd HHmm -Thread Name= 1 formatter = yy-M-d ah:mm -Thread Name= 3 default Formatter = yyyyMMdd HHmm -Thread Name= 2 formatter = yy-M-d ah:mm -Thread Name= 4 default Formatter = yyyyMMdd HHmm -Thread Name= 3 formatter = yy-M-d ah:mm -Thread Name= 4 formatter = yy-M-d ah:mm -Thread Name= 5 default Formatter = yyyyMMdd HHmm -Thread Name= 5 formatter = yy-M-d ah:mm -Thread Name= 6 default Formatter = yyyyMMdd HHmm -Thread Name= 6 formatter = yy-M-d ah:mm -Thread Name= 7 default Formatter = yyyyMMdd HHmm -Thread Name= 7 formatter = yy-M-d ah:mm -Thread Name= 8 default Formatter = yyyyMMdd HHmm -Thread Name= 9 default Formatter = yyyyMMdd HHmm -Thread Name= 8 formatter = yy-M-d ah:mm -Thread Name= 9 formatter = yy-M-d ah:mm -``` - -从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同,其他线程也一样。 - -上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA会提示你转换为Java8的格式(IDEA真的不错!)。因为ThreadLocal类在Java 8中扩展,使用一个新的方法`withInitial()`,将Supplier功能接口作为参数。 - -```java - private static final ThreadLocal formatter = new ThreadLocal(){ - @Override - protected SimpleDateFormat initialValue() - { - return new SimpleDateFormat("yyyyMMdd HHmm"); - } - }; -``` - -### 3.3. ThreadLocal原理 - -从 `Thread`类源代码入手。 - -```java -public class Thread implements Runnable { - ...... -//与此线程有关的ThreadLocal值。由ThreadLocal类维护 -ThreadLocal.ThreadLocalMap threadLocals = null; - -//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 -ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; - ...... -} -``` - -从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。 - -`ThreadLocal`类的`set()`方法 - -```java - public void set(T value) { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) - map.set(this, value); - else - createMap(t, value); - } - ThreadLocalMap getMap(Thread t) { - return t.threadLocals; - } -``` - -通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。 - -**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。 - -`ThreadLocal` 内部维护的是一个类似 `Map` 的`ThreadLocalMap` 数据结构,`key` 为当前对象的 `Thread` 对象,值为 Object 对象。 - -![ThreadLocal数据结构](https://upload-images.jianshu.io/upload_images/7432604-ad2ff581127ba8cc.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/806) - -`ThreadLocalMap`是`ThreadLocal`的静态内部类。 - -![ThreadLocal内部类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadLocal内部类.png) - -### 3.4. ThreadLocal 内存泄露问题 - -`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 - -```java - static class Entry extends WeakReference> { - /** The value associated with this ThreadLocal. */ - Object value; - - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } - } -``` - -**弱引用介绍:** - -> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 -> -> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 - -## 4. 线程池 - -### 4.1. 为什么要用线程池? - -> **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。** - -**线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。 - -这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**: - -- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 -- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - -### 4.2. 实现Runnable接口和Callable接口的区别 - -`Runnable`自Java 1.0以来一直存在,但`Callable`仅在Java 1.5中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。 - -工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callable(Runnable task`)或 `Executors.callable(Runnable task,Object resule)`)。 - -`Runnable.java` - -```java -@FunctionalInterface -public interface Runnable { - /** - * 被线程执行,没有返回值也无法抛出异常 - */ - public abstract void run(); -} -``` - -`Callable.java` - -```java -@FunctionalInterface -public interface Callable { - /** - * 计算结果,或在无法这样做时抛出异常。 - * @return 计算得出的结果 - * @throws 如果无法计算结果,则抛出异常 - */ - V call() throws Exception; -} -``` - -### 4.3. 执行execute()方法和submit()方法的区别是什么呢? - -1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** -2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 - -我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码: - -```java - public Future submit(Runnable task) { - if (task == null) throw new NullPointerException(); - RunnableFuture ftask = newTaskFor(task, null); - execute(ftask); - return ftask; - } -``` - -上面方法调用的 `newTaskFor` 方法返回了一个 `FutureTask` 对象。 - -```java - protected RunnableFuture newTaskFor(Runnable runnable, T value) { - return new FutureTask(runnable, value); - } -``` - -我们再来看看`execute()`方法: - -```java - public void execute(Runnable command) { - ... - } -``` - -### 4.4. 如何创建线程池 - -《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 - -> Executors 返回线程池对象的弊端如下: -> -> - **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。 -> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。 - -**方式一:通过构造方法实现** -![ThreadPoolExecutor构造方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadPoolExecutor构造方法.png) -**方式二:通过Executor 框架的工具类Executors来实现** -我们可以创建三种类型的ThreadPoolExecutor: - -- **FixedThreadPool** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 -- **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 -- **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 - -对应Executors工具类中的方法如图所示: -![Executor框架的工具类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Executor框架的工具类.png) - -### 4.5 ThreadPoolExecutor 类分析 - -`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。 - -```java - /** - * 用给定的初始参数创建一个新的ThreadPoolExecutor。 - */ - public ThreadPoolExecutor(int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - ThreadFactory threadFactory, - RejectedExecutionHandler handler) { - if (corePoolSize < 0 || - maximumPoolSize <= 0 || - maximumPoolSize < corePoolSize || - keepAliveTime < 0) - throw new IllegalArgumentException(); - if (workQueue == null || threadFactory == null || handler == null) - throw new NullPointerException(); - this.corePoolSize = corePoolSize; - this.maximumPoolSize = maximumPoolSize; - this.workQueue = workQueue; - this.keepAliveTime = unit.toNanos(keepAliveTime); - this.threadFactory = threadFactory; - this.handler = handler; - } -``` - -**下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。** - -#### 4.5.1 `ThreadPoolExecutor`构造函数重要参数分析 - -**`ThreadPoolExecutor` 3 个最重要的参数:** - -- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 -- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 - -`ThreadPoolExecutor`其他常见参数: - -1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁; -2. **`unit`** : `keepAliveTime` 参数的时间单位。 -3. **`threadFactory`** :executor 创建新线程的时候会用到。 -4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。 - -#### 4.5.2 `ThreadPoolExecutor` 饱和策略 - -**`ThreadPoolExecutor` 饱和策略定义:** - -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略: - -- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 -- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 - -举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了) - -### 4.6 一个简单的线程池Demo:`Runnable`+`ThreadPoolExecutor` - -为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。 - -首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。) - -`MyRunnable.java` - -```java -import java.util.Date; - -/** - * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 - * @author shuang.kou - */ -public class MyRunnable implements Runnable { - - private String command; - - public MyRunnable(String s) { - this.command = s; - } - - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); - processCommand(); - System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); - } - - private void processCommand() { - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - @Override - public String toString() { - return this.command; - } -} - -``` - -编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。 - -`ThreadPoolExecutorDemo.java` - -```java -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -public class ThreadPoolExecutorDemo { - - private static final int CORE_POOL_SIZE = 5; - private static final int MAX_POOL_SIZE = 10; - private static final int QUEUE_CAPACITY = 100; - private static final Long KEEP_ALIVE_TIME = 1L; - public static void main(String[] args) { - - //使用阿里巴巴推荐的创建线程池的方式 - //通过ThreadPoolExecutor构造函数自定义参数创建 - ThreadPoolExecutor executor = new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(QUEUE_CAPACITY), - new ThreadPoolExecutor.CallerRunsPolicy()); - - for (int i = 0; i < 10; i++) { - //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) - Runnable worker = new MyRunnable("" + i); - //执行Runnable - executor.execute(worker); - } - //终止线程池 - executor.shutdown(); - while (!executor.isTerminated()) { - } - System.out.println("Finished all threads"); - } -} - -``` - -可以看到我们上面的代码指定了: - -1. `corePoolSize`: 核心线程数为 5。 -2. `maximumPoolSize` :最大线程数 10 -3. `keepAliveTime` : 等待时间为 1L。 -4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。 -5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100; -6. `handler`:饱和策略为 `CallerRunsPolicy`。 - -**Output:** - -``` -pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019 - -``` - -### 4.7 线程池原理分析 - -承接 4.6 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) - -现在,我们就分析上面的输出内容来简单分析一下线程池原理。 - -**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.6 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码: - -```java - // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) - private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); - - private static int workerCountOf(int c) { - return c & CAPACITY; - } - - private final BlockingQueue workQueue; - - public void execute(Runnable command) { - // 如果任务为null,则抛出异常。 - if (command == null) - throw new NullPointerException(); - // ctl 中保存的线程池当前的一些状态信息 - int c = ctl.get(); - - // 下面会涉及到 3 步 操作 - // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize - // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - if (workerCountOf(c) < corePoolSize) { - if (addWorker(command, true)) - return; - c = ctl.get(); - } - // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里 - // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去 - if (isRunning(c) && workQueue.offer(command)) { - int recheck = ctl.get(); - // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 - if (!isRunning(recheck) && remove(command)) - reject(command); - // 如果当前线程池为空就新创建一个线程并执行。 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - } - //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。 - else if (!addWorker(command, false)) - reject(command); - } -``` - -通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。 - -![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png) - -现在,让我们在回到 4.6 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢? - -没搞懂的话,也没关系,可以看看我的分析: - -> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。 - -## 5. Atomic 原子类 - -### 5.1. 介绍一下Atomic 原子类 - -Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 - -所以,所谓原子类说简单点就是具有原子/原子操作特征的类。 - - -并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。 - -![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) - -### 5.2. JUC 包中的原子类是哪4类? - -**基本类型** - -使用原子的方式更新基本类型 - -- AtomicInteger:整形原子类 -- AtomicLong:长整型原子类 -- AtomicBoolean:布尔型原子类 - -**数组类型** - -使用原子的方式更新数组里的某个元素 - - -- AtomicIntegerArray:整形数组原子类 -- AtomicLongArray:长整形数组原子类 -- AtomicReferenceArray:引用类型数组原子类 - -**引用类型** - -- AtomicReference:引用类型原子类 -- AtomicStampedReference:原子更新引用类型里的字段原子类 -- AtomicMarkableReference :原子更新带有标记位的引用类型 - -**对象的属性修改类型** - -- AtomicIntegerFieldUpdater:原子更新整形字段的更新器 -- AtomicLongFieldUpdater:原子更新长整形字段的更新器 -- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - - -### 5.3. 讲讲 AtomicInteger 的使用 - - **AtomicInteger 类常用方法** - -```java -public final int get() //获取当前的值 -public final int getAndSet(int newValue)//获取当前的值,并设置新的值 -public final int getAndIncrement()//获取当前的值,并自增 -public final int getAndDecrement() //获取当前的值,并自减 -public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 -boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) -public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 -``` - - **AtomicInteger 类的使用示例** - -使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。 -```java -class AtomicIntegerTest { - private AtomicInteger count = new AtomicInteger(); - //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。 - public void increment() { - count.incrementAndGet(); - } - - public int getCount() { - return count.get(); - } -} - -``` - -### 5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理 - -AtomicInteger 线程安全原理简单分析 - -AtomicInteger 类的部分源码: - -```java - // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) - private static final Unsafe unsafe = Unsafe.getUnsafe(); - private static final long valueOffset; - - static { - try { - valueOffset = unsafe.objectFieldOffset - (AtomicInteger.class.getDeclaredField("value")); - } catch (Exception ex) { throw new Error(ex); } - } - - private volatile int value; -``` - -AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 - -CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 - -关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:[JUC 中的 Atomic 原子类总结](https://mp.weixin.qq.com/s/joa-yOiTrYF67bElj8xqvg) - -## 6. AQS - -### 6.1. AQS 介绍 - -AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。 - -![AQS类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS类.png) - -AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。 - -### 6.2. AQS 原理分析 - -AQS 原理这部分参考了部分博客,在5.2节末尾放了链接。 - -> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 - -下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。 - -#### 6.2.1. AQS 原理概览 - -**AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。** - -> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。 - -看个AQS(AbstractQueuedSynchronizer)原理图: - - -![AQS原理图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS原理图.png) - -AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。 - -```java -private volatile int state;//共享变量,使用volatile修饰保证线程可见性 -``` - -状态信息通过protected类型的getState,setState,compareAndSetState进行操作 - -```java - -//返回同步状态的当前值 -protected final int getState() { - return state; -} - // 设置同步状态的值 -protected final void setState(int newState) { - state = newState; -} -//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) -protected final boolean compareAndSetState(int expect, int update) { - return unsafe.compareAndSwapInt(this, stateOffset, expect, update); -} -``` - -#### 6.2.2. AQS 对资源的共享方式 - -**AQS定义两种资源共享方式** - -- **Exclusive**(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁: - - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 - - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 -- **Share**(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。 - -ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。 - -不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。 - -#### 6.2.3. AQS底层使用了模板方法模式 - -同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): - -1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放) -2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 - -这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。 - -**AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:** - -```java -isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 -tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 -tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 -tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 -tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 - -``` - -默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 - -以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。 - -再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。 - -一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 - -推荐两篇 AQS 原理和相关源码分析的文章: - -- http://www.cnblogs.com/waterystone/p/4920797.html -- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html - -### 6.3. AQS 组件总结 - -- **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。 -- **CountDownLatch (倒计时器):** CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 -- **CyclicBarrier(循环栅栏):** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 - -## 7 Reference - -- 《深入理解 Java 虚拟机》 -- 《实战 Java 高并发程序设计》 -- 《Java并发编程的艺术》 -- http://www.cnblogs.com/waterystone/p/4920797.html -- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html -- - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md b/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md deleted file mode 100644 index ca2518e..0000000 --- a/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md +++ /dev/null @@ -1,312 +0,0 @@ -点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 - - -- [Java 并发基础常见面试题总结](#java-并发基础常见面试题总结) - - [1. 什么是线程和进程?](#1-什么是线程和进程) - - [1.1. 何为进程?](#11-何为进程) - - [1.2. 何为线程?](#12-何为线程) - - [2. 请简要描述线程与进程的关系,区别及优缺点?](#2-请简要描述线程与进程的关系区别及优缺点) - - [2.1. 图解进程和线程的关系](#21-图解进程和线程的关系) - - [2.2. 程序计数器为什么是私有的?](#22-程序计数器为什么是私有的) - - [2.3. 虚拟机栈和本地方法栈为什么是私有的?](#23-虚拟机栈和本地方法栈为什么是私有的) - - [2.4. 一句话简单了解堆和方法区](#24-一句话简单了解堆和方法区) - - [3. 说说并发与并行的区别?](#3-说说并发与并行的区别) - - [4. 为什么要使用多线程呢?](#4-为什么要使用多线程呢) - - [5. 使用多线程可能带来什么问题?](#5-使用多线程可能带来什么问题) - - [6. 说说线程的生命周期和状态?](#6-说说线程的生命周期和状态) - - [7. 什么是上下文切换?](#7-什么是上下文切换) - - [8. 什么是线程死锁?如何避免死锁?](#8-什么是线程死锁如何避免死锁) - - [8.1. 认识线程死锁](#81-认识线程死锁) - - [8.2. 如何避免线程死锁?](#82-如何避免线程死锁) - - [9. 说说 sleep() 方法和 wait() 方法区别和共同点?](#9-说说-sleep-方法和-wait-方法区别和共同点) - - [10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?](#10-为什么我们调用-start-方法时会执行-run-方法为什么我们不能直接调用-run-方法) - - [公众号](#公众号) - - -# Java 并发基础常见面试题总结 - -## 1. 什么是线程和进程? - -### 1.1. 何为进程? - -进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 - -在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。 - -如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。 - -![进程示例图片-Windows](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/进程示例图片-Windows.png) - -### 1.2. 何为线程? - -线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。 - -```java -public class MultiThread { - public static void main(String[] args) { - // 获取 Java 线程管理 MXBean - ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); - // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 - ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); - // 遍历线程信息,仅打印线程 ID 和线程名称信息 - for (ThreadInfo threadInfo : threadInfos) { - System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); - } - } -} -``` - -上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可): - -``` -[5] Attach Listener //添加事件 -[4] Signal Dispatcher // 分发处理给 JVM 信号的线程 -[3] Finalizer //调用对象 finalize 方法的线程 -[2] Reference Handler //清除 reference 线程 -[1] main //main 线程,程序入口 -``` - -从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。 - -## 2. 请简要描述线程与进程的关系,区别及优缺点? - -**从 JVM 角度说进程和线程之间的关系** - -### 2.1. 图解进程和线程的关系 - -下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md "《可能是把 Java 内存区域讲的最清楚的一篇文章》") - -
- -
- -从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 - -**总结:** 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反 - -下面是该知识点的扩展内容! - -下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢? - -### 2.2. 程序计数器为什么是私有的? - -程序计数器主要有下面两个作用: - -1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 - -需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。 - -所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 - -### 2.3. 虚拟机栈和本地方法栈为什么是私有的? - -- **虚拟机栈:** 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 -- **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 - -所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 - -### 2.4. 一句话简单了解堆和方法区 - -堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 - -## 3. 说说并发与并行的区别? - -- **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行); -- **并行:** 单位时间内,多个任务同时执行。 - -## 4. 为什么要使用多线程呢? - -先从总体上来说: - -- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 -- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 - -再深入到计算机底层来探讨: - -- **单核时代:** 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。 -- **多核时代:** 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。 - -## 5. 使用多线程可能带来什么问题? - -并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。 - -## 6. 说说线程的生命周期和状态? - -Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。 - -![Java 线程的状态 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81.png) - -线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节): - -![Java 线程状态变迁 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java+%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E5%8F%98%E8%BF%81.png) - -由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。 - -> 操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/ "HowToDoInJava"):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/ "Java Thread Life Cycle and Thread States")),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。 - -![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png) - -当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)** 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的`run()`方法之后将会进入到 **TERMINATED(终止)** 状态。 - -## 7. 什么是上下文切换? - -多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 - -概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 - -上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 - -Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 - -## 8. 什么是线程死锁?如何避免死锁? - -### 8.1. 认识线程死锁 - -多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 - -如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 - -![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4%E6%AD%BB%E9%94%811.png) - -下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》): - -```java -public class DeadLockDemo { - private static Object resource1 = new Object();//资源 1 - private static Object resource2 = new Object();//资源 2 - - public static void main(String[] args) { - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 1").start(); - - new Thread(() -> { - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource1"); - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - } - } - }, "线程 2").start(); - } -} -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 2,5,main]get resource2 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 2,5,main]waiting get resource1 -``` - -线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。 - -学过操作系统的朋友都知道产生死锁必须具备以下四个条件: - -1. 互斥条件:该资源任意一个时刻只由一个线程占用。 -2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 -3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 -4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 - -### 8.2. 如何避免线程死锁? - -我们只要破坏产生死锁的四个条件中的其中一个就可以了。 - -**破坏互斥条件** - -这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 - -**破坏请求与保持条件** - -一次性申请所有的资源。 - -**破坏不剥夺条件** - -占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 - -**破坏循环等待条件** - -靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 - -我们对线程 2 的代码修改成下面这样就不会产生死锁了。 - -```java - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 2").start(); -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 1,5,main]get resource2 -Thread[线程 2,5,main]get resource1 -Thread[线程 2,5,main]waiting get resource2 -Thread[线程 2,5,main]get resource2 - -Process finished with exit code 0 -``` - -我们分析一下上面的代码为什么避免了死锁的发生? - -线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 - -## 9. 说说 sleep() 方法和 wait() 方法区别和共同点? - -- 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁** 。 -- 两者都可以暂停线程的执行。 -- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 -- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。 - -## 10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? - -这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! - -new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 - -**总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。** - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取! - -**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/java/Multithread/ThreadLocal.md b/docs/java/Multithread/ThreadLocal.md deleted file mode 100644 index 06cdbea..0000000 --- a/docs/java/Multithread/ThreadLocal.md +++ /dev/null @@ -1,170 +0,0 @@ -[ThreadLocal造成OOM内存溢出案例演示与原理分析](https://blog.csdn.net/xlgen157387/article/details/78298840) - -[深入理解 Java 之 ThreadLocal 工作原理]() - -## ThreadLocal - -### ThreadLocal简介 - -通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** - -**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** - -再举个简单的例子: - -比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来这两个线程竞争的。 - -### ThreadLocal示例 - -相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 - -```java -import java.text.SimpleDateFormat; -import java.util.Random; - -public class ThreadLocalExample implements Runnable{ - - // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 - private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); - - public static void main(String[] args) throws InterruptedException { - ThreadLocalExample obj = new ThreadLocalExample(); - for(int i=0 ; i<10; i++){ - Thread t = new Thread(obj, ""+i); - Thread.sleep(new Random().nextInt(1000)); - t.start(); - } - } - - @Override - public void run() { - System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); - try { - Thread.sleep(new Random().nextInt(1000)); - } catch (InterruptedException e) { - e.printStackTrace(); - } - //formatter pattern is changed here by thread, but it won't reflect to other threads - formatter.set(new SimpleDateFormat()); - - System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); - } - -} - -``` - -Output: - -``` -Thread Name= 0 default Formatter = yyyyMMdd HHmm -Thread Name= 0 formatter = yy-M-d ah:mm -Thread Name= 1 default Formatter = yyyyMMdd HHmm -Thread Name= 2 default Formatter = yyyyMMdd HHmm -Thread Name= 1 formatter = yy-M-d ah:mm -Thread Name= 3 default Formatter = yyyyMMdd HHmm -Thread Name= 2 formatter = yy-M-d ah:mm -Thread Name= 4 default Formatter = yyyyMMdd HHmm -Thread Name= 3 formatter = yy-M-d ah:mm -Thread Name= 4 formatter = yy-M-d ah:mm -Thread Name= 5 default Formatter = yyyyMMdd HHmm -Thread Name= 5 formatter = yy-M-d ah:mm -Thread Name= 6 default Formatter = yyyyMMdd HHmm -Thread Name= 6 formatter = yy-M-d ah:mm -Thread Name= 7 default Formatter = yyyyMMdd HHmm -Thread Name= 7 formatter = yy-M-d ah:mm -Thread Name= 8 default Formatter = yyyyMMdd HHmm -Thread Name= 9 default Formatter = yyyyMMdd HHmm -Thread Name= 8 formatter = yy-M-d ah:mm -Thread Name= 9 formatter = yy-M-d ah:mm -``` - -从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同,其他线程也一样。 - -上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA会提示你转换为Java8的格式(IDEA真的不错!)。因为ThreadLocal类在Java 8中扩展,使用一个新的方法`withInitial()`,将Supplier功能接口作为参数。 - -```java - private static final ThreadLocal formatter = new ThreadLocal(){ - @Override - protected SimpleDateFormat initialValue() - { - return new SimpleDateFormat("yyyyMMdd HHmm"); - } - }; -``` - -### ThreadLocal原理 - -从 `Thread`类源代码入手。 - -```java -public class Thread implements Runnable { - ...... -//与此线程有关的ThreadLocal值。由ThreadLocal类维护 -ThreadLocal.ThreadLocalMap threadLocals = null; - -//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 -ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; - ...... -} -``` - -从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。 - -`ThreadLocal`类的`set()`方法 - -```java - public void set(T value) { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) - map.set(this, value); - else - createMap(t, value); - } - ThreadLocalMap getMap(Thread t) { - return t.threadLocals; - } -``` - -通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。** - -**每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。`ThreadLocal` 是 map结构是为了让每个线程可以关联多个 `ThreadLocal`变量。这也就解释了ThreadLocal声明的变量为什么在每一个线程都有自己的专属本地变量。 - -```java -public class Thread implements Runnable { - ...... -//与此线程有关的ThreadLocal值。由ThreadLocal类维护 -ThreadLocal.ThreadLocalMap threadLocals = null; - -//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 -ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; - ...... -} -``` - -`ThreadLocalMap`是`ThreadLocal`的静态内部类。 - -![ThreadLocal内部类](https://ws1.sinaimg.cn/large/006rNwoDgy1g2f47u9li2j30ka08cq43.jpg) - -### ThreadLocal 内存泄露问题 - -`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候会 key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 - -```java - static class Entry extends WeakReference> { - /** The value associated with this ThreadLocal. */ - Object value; - - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } - } -``` - -**弱引用介绍:** - -> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 -> -> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 diff --git "a/docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" "b/docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" deleted file mode 100644 index bfbe99d..0000000 --- "a/docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" +++ /dev/null @@ -1,781 +0,0 @@ - - -- [一 使用线程池的好处](#一-使用线程池的好处) -- [二 Executor 框架](#二-executor-框架) - - [2.1 简介](#21-简介) - - [2.2 Executor 框架结构(主要由三大部分组成)](#22-executor-框架结构主要由三大部分组成) - - [1) 任务(`Runnable` /`Callable`)](#1-任务runnable-callable) - - [2) 任务的执行(`Executor`)](#2-任务的执行executor) - - [3) 异步计算的结果(`Future`)](#3-异步计算的结果future) - - [2.3 Executor 框架的使用示意图](#23-executor-框架的使用示意图) -- [三 (重要)ThreadPoolExecutor 类简单介绍](#三-重要threadpoolexecutor-类简单介绍) - - [3.1 ThreadPoolExecutor 类分析](#31-threadpoolexecutor-类分析) - - [3.2 推荐使用 `ThreadPoolExecutor` 构造函数创建线程池](#32-推荐使用-threadpoolexecutor-构造函数创建线程池) -- [四 (重要)ThreadPoolExecutor 使用示例](#四-重要threadpoolexecutor-使用示例) - - [4.1 示例代码:`Runnable`+`ThreadPoolExecutor`](#41-示例代码runnablethreadpoolexecutor) - - [4.2 线程池原理分析](#42-线程池原理分析) - - [4.3 几个常见的对比](#43-几个常见的对比) - - [4.3.1 `Runnable` vs `Callable`](#431-runnable-vs-callable) - - [4.3.2 `execute()` vs `submit()`](#432-execute-vs-submit) - - [4.3.3 `shutdown()`VS`shutdownNow()`](#433-shutdownvsshutdownnow) - - [4.3.2 `isTerminated()` VS `isShutdown()`](#432-isterminated-vs-isshutdown) - - [4.4 加餐:`Callable`+`ThreadPoolExecutor`示例代码](#44-加餐callablethreadpoolexecutor示例代码) -- [五 几种常见的线程池详解](#五-几种常见的线程池详解) - - [5.1 FixedThreadPool](#51-fixedthreadpool) - - [5.1.1 介绍](#511-介绍) - - [5.1.2 执行任务过程介绍](#512-执行任务过程介绍) - - [5.1.3 为什么不推荐使用`FixedThreadPool`?](#513-为什么不推荐使用fixedthreadpool) - - [5.2 SingleThreadExecutor 详解](#52-singlethreadexecutor-详解) - - [5.2.1 介绍](#521-介绍) - - [5.2.2 执行任务过程介绍](#522-执行任务过程介绍) - - [5.2.3 为什么不推荐使用`FixedThreadPool`?](#523-为什么不推荐使用fixedthreadpool) - - [5.3 CachedThreadPool 详解](#53-cachedthreadpool-详解) - - [5.3.1 介绍](#531-介绍) - - [5.3.2 执行任务过程介绍](#532-执行任务过程介绍) - - [5.3.3 为什么不推荐使用`CachedThreadPool`?](#533-为什么不推荐使用cachedthreadpool) -- [六 ScheduledThreadPoolExecutor 详解](#六-scheduledthreadpoolexecutor-详解) - - [6.1 简介](#61-简介) - - [6.2 运行机制](#62-运行机制) - - [6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤](#63-scheduledthreadpoolexecutor-执行周期任务的步骤) -- [七 线程池大小确定](#七-线程池大小确定) -- [八 参考](#八-参考) -- [九 其他推荐阅读](#九-其他推荐阅读) - - - -## 一 使用线程池的好处 - -> **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。** - -**线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。 - -这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**: - -- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 -- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - -## 二 Executor 框架 - -### 2.1 简介 - -Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。 - -> 补充:this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。 - -Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。 - -### 2.2 Executor 框架结构(主要由三大部分组成) - -#### 1) 任务(`Runnable` /`Callable`) - -执行任务需要实现的 **`Runnable` 接口** 或 **`Callable`接口**。**`Runnable` 接口**或 **`Callable` 接口** 实现类都可以被 **`ThreadPoolExecutor`** 或 **`ScheduledThreadPoolExecutor`** 执行。 - -#### 2) 任务的执行(`Executor`) - -如下图所示,包括任务执行机制的核心接口 **`Executor`** ,以及继承自 `Executor` 接口的 **`ExecutorService` 接口。`ThreadPoolExecutor`** 和 **`ScheduledThreadPoolExecutor`** 这两个关键类实现了 **ExecutorService 接口**。 - -**这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 `ThreadPoolExecutor` 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。** - -> **注意:** 通过查看 `ScheduledThreadPoolExecutor` 源代码我们发现 `ScheduledThreadPoolExecutor` 实际上是继承了 `ThreadPoolExecutor` 并实现了 ScheduledExecutorService ,而 `ScheduledExecutorService` 又实现了 `ExecutorService`,正如我们下面给出的类关系图显示的一样。 - -**`ThreadPoolExecutor` 类描述:** - -```java -//AbstractExecutorService实现了ExecutorService接口 -public class ThreadPoolExecutor extends AbstractExecutorService -``` - -**`ScheduledThreadPoolExecutor` 类描述:** - -```java -//ScheduledExecutorService实现了ExecutorService接口 -public class ScheduledThreadPoolExecutor - extends ThreadPoolExecutor - implements ScheduledExecutorService -``` - -![任务的执行相关接口](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/任务的执行相关接口.png) - -#### 3) 异步计算的结果(`Future`) - -**`Future`** 接口以及 `Future` 接口的实现类 **`FutureTask`** 类都可以代表异步计算的结果。 - -当我们把 **`Runnable`接口** 或 **`Callable` 接口** 的实现类提交给 **`ThreadPoolExecutor`** 或 **`ScheduledThreadPoolExecutor`** 执行。(调用 `submit()` 方法时会返回一个 **`FutureTask`** 对象) - -### 2.3 Executor 框架的使用示意图 - -![Executor 框架的使用示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC01LTMwLzg0ODIzMzMwLmpwZw?x-oss-process=image/format,png) - -1. **主线程首先要创建实现 `Runnable` 或者 `Callable` 接口的任务对象。** -2. **把创建完成的实现 `Runnable`/`Callable`接口的 对象直接交给 `ExecutorService` 执行**: `ExecutorService.execute(Runnable command)`)或者也可以把 `Runnable` 对象或`Callable` 对象提交给 `ExecutorService` 执行(`ExecutorService.submit(Runnable task)`或 `ExecutorService.submit(Callable task)`)。 -3. **如果执行 `ExecutorService.submit(…)`,`ExecutorService` 将返回一个实现`Future`接口的对象**(我们刚刚也提到过了执行 `execute()`方法和 `submit()`方法的区别,`submit()`会返回一个 `FutureTask 对象)。由于 FutureTask` 实现了 `Runnable`,我们也可以创建 `FutureTask`,然后直接交给 `ExecutorService` 执行。 -4. **最后,主线程可以执行 `FutureTask.get()`方法来等待任务执行完成。主线程也可以执行 `FutureTask.cancel(boolean mayInterruptIfRunning)`来取消此任务的执行。** - -## 三 (重要)ThreadPoolExecutor 类简单介绍 - -**线程池实现类 `ThreadPoolExecutor` 是 `Executor` 框架最核心的类。** - -### 3.1 ThreadPoolExecutor 类分析 - -`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。 - -```java - /** - * 用给定的初始参数创建一个新的ThreadPoolExecutor。 - */ - public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 - int maximumPoolSize,//线程池的最大线程数 - long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间 - TimeUnit unit,//时间单位 - BlockingQueue workQueue,//任务队列,用来储存等待执行任务的队列 - ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可 - RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 - ) { - if (corePoolSize < 0 || - maximumPoolSize <= 0 || - maximumPoolSize < corePoolSize || - keepAliveTime < 0) - throw new IllegalArgumentException(); - if (workQueue == null || threadFactory == null || handler == null) - throw new NullPointerException(); - this.corePoolSize = corePoolSize; - this.maximumPoolSize = maximumPoolSize; - this.workQueue = workQueue; - this.keepAliveTime = unit.toNanos(keepAliveTime); - this.threadFactory = threadFactory; - this.handler = handler; - } -``` - -**下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。** - -**`ThreadPoolExecutor` 3 个最重要的参数:** - -- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 -- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。 - -`ThreadPoolExecutor`其他常见参数: - -1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁; -2. **`unit`** : `keepAliveTime` 参数的时间单位。 -3. **`threadFactory`** :executor 创建新线程的时候会用到。 -4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。 - -下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java性能调优实战》): - -![线程池各个参数的关系](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/线程池各个参数的关系.jpg) - -**`ThreadPoolExecutor` 饱和策略定义:** - -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略: - -- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 -- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 - -举个例子: - -> Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。) - -### 3.2 推荐使用 `ThreadPoolExecutor` 构造函数创建线程池 - -**在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。** - -**为什么呢?** - -> **使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。** - -**另外《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险** - -> Executors 返回线程池对象的弊端如下: -> -> - **`FixedThreadPool` 和 `SingleThreadExecutor`** : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。 -> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 - -**方式一:通过`ThreadPoolExecutor`构造函数实现(推荐)** -![通过构造方法实现](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzE3ODU4MjMwLmpwZw?x-oss-process=image/format,png) -**方式二:通过 Executor 框架的工具类 Executors 来实现** -我们可以创建三种类型的 ThreadPoolExecutor: - -- **FixedThreadPool** -- **SingleThreadExecutor** -- **CachedThreadPool** - -对应 Executors 工具类中的方法如图所示: -![通过Executor 框架的工具类Executors来实现](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzEzMjk2OTAxLmpwZw?x-oss-process=image/format,png) - -## 四 (重要)ThreadPoolExecutor 使用示例 - -我们上面讲解了 `Executor`框架以及 `ThreadPoolExecutor` 类,下面让我们实战一下,来通过写一个 `ThreadPoolExecutor` 的小 Demo 来回顾上面的内容。 - -### 4.1 示例代码:`Runnable`+`ThreadPoolExecutor` - -首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。) - -`MyRunnable.java` - -```java -import java.util.Date; - -/** - * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 - * @author shuang.kou - */ -public class MyRunnable implements Runnable { - - private String command; - - public MyRunnable(String s) { - this.command = s; - } - - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); - processCommand(); - System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); - } - - private void processCommand() { - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - @Override - public String toString() { - return this.command; - } -} - -``` - -编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。 - -`ThreadPoolExecutorDemo.java` - -```java -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -public class ThreadPoolExecutorDemo { - - private static final int CORE_POOL_SIZE = 5; - private static final int MAX_POOL_SIZE = 10; - private static final int QUEUE_CAPACITY = 100; - private static final Long KEEP_ALIVE_TIME = 1L; - public static void main(String[] args) { - - //使用阿里巴巴推荐的创建线程池的方式 - //通过ThreadPoolExecutor构造函数自定义参数创建 - ThreadPoolExecutor executor = new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(QUEUE_CAPACITY), - new ThreadPoolExecutor.CallerRunsPolicy()); - - for (int i = 0; i < 10; i++) { - //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) - Runnable worker = new MyRunnable("" + i); - //执行Runnable - executor.execute(worker); - } - //终止线程池 - executor.shutdown(); - while (!executor.isTerminated()) { - } - System.out.println("Finished all threads"); - } -} - -``` - -可以看到我们上面的代码指定了: - -1. `corePoolSize`: 核心线程数为 5。 -2. `maximumPoolSize` :最大线程数 10 -3. `keepAliveTime` : 等待时间为 1L。 -4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。 -5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100; -6. `handler`:饱和策略为 `CallerRunsPolicy`。 - -**Output:** - -``` -pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019 - -``` - -### 4.2 线程池原理分析 - -承接 4.1 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) - -现在,我们就分析上面的输出内容来简单分析一下线程池原理。 - -**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.1 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码: - -```java - // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) - private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); - - private static int workerCountOf(int c) { - return c & CAPACITY; - } - - private final BlockingQueue workQueue; - - public void execute(Runnable command) { - // 如果任务为null,则抛出异常。 - if (command == null) - throw new NullPointerException(); - // ctl 中保存的线程池当前的一些状态信息 - int c = ctl.get(); - - // 下面会涉及到 3 步 操作 - // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize - // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - if (workerCountOf(c) < corePoolSize) { - if (addWorker(command, true)) - return; - c = ctl.get(); - } - // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里 - // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去 - if (isRunning(c) && workQueue.offer(command)) { - int recheck = ctl.get(); - // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 - if (!isRunning(recheck) && remove(command)) - reject(command); - // 如果当前线程池为空就新创建一个线程并执行。 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - } - //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。 - else if (!addWorker(command, false)) - reject(command); - } -``` - -通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。 - -![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png) - -现在,让我们在回到 4.1 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢? - -没搞懂的话,也没关系,可以看看我的分析: - -> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。 - -### 4.3 几个常见的对比 - -#### 4.3.1 `Runnable` vs `Callable` - -`Runnable`自 Java 1.0 以来一直存在,但`Callable`仅在 Java 1.5 中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。 - -工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callable(Runnable task`)或 `Executors.callable(Runnable task,Object resule)`)。 - -`Runnable.java` - -```java -@FunctionalInterface -public interface Runnable { - /** - * 被线程执行,没有返回值也无法抛出异常 - */ - public abstract void run(); -} -``` - -`Callable.java` - -```java -@FunctionalInterface -public interface Callable { - /** - * 计算结果,或在无法这样做时抛出异常。 - * @return 计算得出的结果 - * @throws 如果无法计算结果,则抛出异常 - */ - V call() throws Exception; -} - -``` - -#### 4.3.2 `execute()` vs `submit()` - -1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** -2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 - -我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码: - -```java - public Future submit(Runnable task) { - if (task == null) throw new NullPointerException(); - RunnableFuture ftask = newTaskFor(task, null); - execute(ftask); - return ftask; - } -``` - -上面方法调用的 `newTaskFor` 方法返回了一个 `FutureTask` 对象。 - -```java - protected RunnableFuture newTaskFor(Runnable runnable, T value) { - return new FutureTask(runnable, value); - } -``` - -我们再来看看`execute()`方法: - -```java - public void execute(Runnable command) { - ... - } -``` - -#### 4.3.3 `shutdown()`VS`shutdownNow()` - -- **`shutdown()`** :关闭线程池,线程池的状态变为 `SHUTDOWN`。线程池不再接受新任务了,但是队列里的任务得执行完毕。 -- **`shutdownNow()`** :关闭线程池,线程的状态变为 `STOP`。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。 - -#### 4.3.2 `isTerminated()` VS `isShutdown()` - -- **`isShutDown`** 当调用 `shutdown()` 方法后返回为 true。 -- **`isTerminated`** 当调用 `shutdown()` 方法后,并且所有提交的任务完成后返回为 true - -### 4.4 加餐:`Callable`+`ThreadPoolExecutor`示例代码 - -`MyCallable.java` - -```java - -import java.util.concurrent.Callable; - -public class MyCallable implements Callable { - @Override - public String call() throws Exception { - Thread.sleep(1000); - //返回执行当前 Callable 的线程名字 - return Thread.currentThread().getName(); - } -} -``` - -`CallableDemo.java` - -```java - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -public class CallableDemo { - - private static final int CORE_POOL_SIZE = 5; - private static final int MAX_POOL_SIZE = 10; - private static final int QUEUE_CAPACITY = 100; - private static final Long KEEP_ALIVE_TIME = 1L; - - public static void main(String[] args) { - - //使用阿里巴巴推荐的创建线程池的方式 - //通过ThreadPoolExecutor构造函数自定义参数创建 - ThreadPoolExecutor executor = new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(QUEUE_CAPACITY), - new ThreadPoolExecutor.CallerRunsPolicy()); - - List> futureList = new ArrayList<>(); - Callable callable = new MyCallable(); - for (int i = 0; i < 10; i++) { - //提交任务到线程池 - Future future = executor.submit(callable); - //将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值 - futureList.add(future); - } - for (Future fut : futureList) { - try { - System.out.println(new Date() + "::" + fut.get()); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - } - //关闭线程池 - executor.shutdown(); - } -} -``` - -Output: - -``` -Wed Nov 13 13:40:41 CST 2019::pool-1-thread-1 -Wed Nov 13 13:40:42 CST 2019::pool-1-thread-2 -Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3 -Wed Nov 13 13:40:42 CST 2019::pool-1-thread-4 -Wed Nov 13 13:40:42 CST 2019::pool-1-thread-5 -Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3 -Wed Nov 13 13:40:43 CST 2019::pool-1-thread-2 -Wed Nov 13 13:40:43 CST 2019::pool-1-thread-1 -Wed Nov 13 13:40:43 CST 2019::pool-1-thread-4 -Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 -``` - -## 五 几种常见的线程池详解 - -### 5.1 FixedThreadPool - -#### 5.1.1 介绍 - -`FixedThreadPool` 被称为可重用固定线程数的线程池。通过 Executors 类中的相关源代码来看一下相关实现: - -```java - /** - * 创建一个可重用固定数量线程的线程池 - */ - public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { - return new ThreadPoolExecutor(nThreads, nThreads, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(), - threadFactory); - } -``` - -另外还有一个 `FixedThreadPool` 的实现方法,和上面的类似,所以这里不多做阐述: - -```java - public static ExecutorService newFixedThreadPool(int nThreads) { - return new ThreadPoolExecutor(nThreads, nThreads, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue()); - } -``` - -**从上面源代码可以看出新创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的。** - -#### 5.1.2 执行任务过程介绍 - -`FixedThreadPool` 的 `execute()` 方法运行示意图(该图片来源:《Java 并发编程的艺术》): - -![FixedThreadPool的execute()方法运行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzcxMzc1OTYzLmpwZw?x-oss-process=image/format,png) - -**上图说明:** - -1. 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务; -2. 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 `LinkedBlockingQueue`; -3. 线程池中的线程执行完 手头的任务后,会在循环中反复从 `LinkedBlockingQueue` 中获取任务来执行; - -#### 5.1.3 为什么不推荐使用`FixedThreadPool`? - -**`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :** - -1. 当线程池中的线程数达到 `corePoolSize` 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize; -2. 由于使用无界队列时 `maximumPoolSize` 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 `FixedThreadPool`的源码可以看出创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 被设置为同一个值。 -3. 由于 1 和 2,使用无界队列时 `keepAliveTime` 将是一个无效参数; -4. 运行中的 `FixedThreadPool`(未执行 `shutdown()`或 `shutdownNow()`)不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。 - -### 5.2 SingleThreadExecutor 详解 - -#### 5.2.1 介绍 - -`SingleThreadExecutor` 是只有一个线程的线程池。下面看看**SingleThreadExecutor 的实现:** - -```java - /** - *返回只有一个线程的线程池 - */ - public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { - return new FinalizableDelegatedExecutorService - (new ThreadPoolExecutor(1, 1, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(), - threadFactory)); - } -``` - -```java - public static ExecutorService newSingleThreadExecutor() { - return new FinalizableDelegatedExecutorService - (new ThreadPoolExecutor(1, 1, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue())); - } -``` - -从上面源代码可以看出新创建的 `SingleThreadExecutor` 的 `corePoolSize` 和 `maximumPoolSize` 都被设置为 1.其他参数和 `FixedThreadPool` 相同。 - -#### 5.2.2 执行任务过程介绍 - -**`SingleThreadExecutor` 的运行示意图(该图片来源:《Java 并发编程的艺术》):** -![SingleThreadExecutor的运行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzgyMjc2NDU4LmpwZw?x-oss-process=image/format,png) - -**上图说明;** - -1. 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务; -2. 当前线程池中有一个运行的线程后,将任务加入 `LinkedBlockingQueue` -3. 线程执行完当前的任务后,会在循环中反复从` LinkedBlockingQueue` 中获取任务来执行; - -#### 5.2.3 为什么不推荐使用`SingleThreadExecutor`? - -`SingleThreadExecutor` 使用无界队列 `LinkedBlockingQueue` 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。`SingleThreadExecutor` 使用无界队列作为线程池的工作队列会对线程池带来的影响与 `FixedThreadPool` 相同。说简单点就是可能会导致 OOM, - -### 5.3 CachedThreadPool 详解 - -#### 5.3.1 介绍 - -`CachedThreadPool` 是一个会根据需要创建新线程的线程池。下面通过源码来看看 `CachedThreadPool` 的实现: - -```java - /** - * 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。 - */ - public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { - return new ThreadPoolExecutor(0, Integer.MAX_VALUE, - 60L, TimeUnit.SECONDS, - new SynchronousQueue(), - threadFactory); - } - -``` - -```java - public static ExecutorService newCachedThreadPool() { - return new ThreadPoolExecutor(0, Integer.MAX_VALUE, - 60L, TimeUnit.SECONDS, - new SynchronousQueue()); - } -``` - -`CachedThreadPool` 的` corePoolSize` 被设置为空(0),`maximumPoolSize `被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 `maximumPool` 中线程处理任务的速度时,`CachedThreadPool` 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。 - -#### 5.3.2 执行任务过程介绍 - -**CachedThreadPool 的 execute()方法的执行示意图(该图片来源:《Java 并发编程的艺术》):** -![CachedThreadPool的execute()方法的执行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzE4NjExNzY3LmpwZw?x-oss-process=image/format,png) - -**上图说明:** - -1. 首先执行 `SynchronousQueue.offer(Runnable task)` 提交任务到任务队列。如果当前 `maximumPool` 中有闲线程正在执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`,那么主线程执行 offer 操作与空闲线程执行的 `poll` 操作配对成功,主线程把任务交给空闲线程执行,`execute()`方法执行完成,否则执行下面的步骤 2; -2. 当初始 `maximumPool` 为空,或者 `maximumPool` 中没有空闲线程时,将没有线程执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`。这种情况下,步骤 1 将失败,此时 `CachedThreadPool` 会创建新线程执行任务,execute 方法执行完成; - -#### 5.3.3 为什么不推荐使用`CachedThreadPool`? - -`CachedThreadPool`允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 - -## 六 ScheduledThreadPoolExecutor 详解 - -**`ScheduledThreadPoolExecutor` 主要用来在给定的延迟后运行任务,或者定期执行任务。** 这个在实际项目中基本不会被用到,所以对这部分大家只需要简单了解一下它的思想。关于如何在Spring Boot 中 实现定时任务,可以查看这篇文章[《5分钟搞懂如何在Spring Boot中Schedule Tasks》](https://github.com/Snailclimb/springboot-guide/blob/master/docs/advanced/SpringBoot-ScheduleTasks.md)。 - -### 6.1 简介 - -**`ScheduledThreadPoolExecutor` 使用的任务队列 `DelayQueue` 封装了一个 `PriorityQueue`,`PriorityQueue` 会对队列中的任务进行排序,执行所需时间短的放在前面先被执行(`ScheduledFutureTask` 的 `time` 变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行(`ScheduledFutureTask` 的 `squenceNumber` 变量小的先执行)。** - -**`ScheduledThreadPoolExecutor` 和 `Timer` 的比较:** - -- `Timer` 对系统时钟的变化敏感,`ScheduledThreadPoolExecutor`不是; -- `Timer` 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。 `ScheduledThreadPoolExecutor` 可以配置任意数量的线程。 此外,如果你想(通过提供 ThreadFactory),你可以完全控制创建的线程; -- 在`TimerTask` 中抛出的运行时异常会杀死一个线程,从而导致 `Timer` 死机:-( ...即计划任务将不再运行。`ScheduledThreadExecutor` 不仅捕获运行时异常,还允许您在需要时处理它们(通过重写 `afterExecute` 方法`ThreadPoolExecutor`)。抛出异常的任务将被取消,但其他任务将继续运行。 - -**综上,在 JDK1.5 之后,你没有理由再使用 Timer 进行任务调度了。** - -> **备注:** Quartz 是一个由 java 编写的任务调度库,由 OpenSymphony 组织开源出来。在实际项目开发中使用 Quartz 的还是居多,比较推荐使用 Quartz。因为 Quartz 理论上能够同时对上万个任务进行调度,拥有丰富的功能特性,包括任务调度、任务持久化、可集群化、插件等等。 - -### 6.2 运行机制 - -![ScheduledThreadPoolExecutor运行机制](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzkyNTk0Njk4LmpwZw?x-oss-process=image/format,png) - -**`ScheduledThreadPoolExecutor` 的执行主要分为两大部分:** - -1. 当调用 `ScheduledThreadPoolExecutor` 的 **`scheduleAtFixedRate()`** 方法或者**`scheduleWirhFixedDelay()`** 方法时,会向 `ScheduledThreadPoolExecutor` 的 **`DelayQueue`** 添加一个实现了 **`RunnableScheduledFuture`** 接口的 **`ScheduledFutureTask`** 。 -2. 线程池中的线程从 `DelayQueue` 中获取 `ScheduledFutureTask`,然后执行任务。 - -**`ScheduledThreadPoolExecutor` 为了实现周期性的执行任务,对 `ThreadPoolExecutor `做了如下修改:** - -- 使用 **`DelayQueue`** 作为任务队列; -- 获取任务的方不同 -- 执行周期任务后,增加了额外的处理 - -### 6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤 - -![ScheduledThreadPoolExecutor执行周期任务的步骤](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC01LTMwLzU5OTE2Mzg5LmpwZw?x-oss-process=image/format,png) - -1. 线程 1 从 `DelayQueue` 中获取已到期的 `ScheduledFutureTask(DelayQueue.take())`。到期任务是指 `ScheduledFutureTask `的 time 大于等于当前系统的时间; -2. 线程 1 执行这个 `ScheduledFutureTask`; -3. 线程 1 修改 `ScheduledFutureTask` 的 time 变量为下次将要被执行的时间; -4. 线程 1 把这个修改 time 之后的 `ScheduledFutureTask` 放回 `DelayQueue` 中(`DelayQueue.add()`)。 - -## 七 线程池大小确定 - -**线程池数量的确定一直是困扰着程序员的一个难题,大部分程序员在设定线程池大小的时候就是随心而定。我们并没有考虑过这样大小的配置是否会带来什么问题,我自己就是这大部分程序员中的一个代表。** - -由于笔主对如何确定线程池大小也没有什么实际经验,所以,这部分内容参考了网上很多文章/书籍。 - -**首先,可以肯定的一点是线程池大小设置过大或者过小都会有问题。合适的才是最好,貌似在 95 % 的场景下都是合适的。** - -如果阅读过我的上一篇关于线程池的文章的话,你一定知道: - -**如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。** - -**但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。** - -> 上下文切换: -> -> 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 -> -> 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 -> -> Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 - -有一个简单并且适用面比较广的公式: - -- **CPU 密集型任务(N+1):** 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 -- **I/O 密集型任务(2N):** 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。 - -## 八 参考 - -- 《Java 并发编程的艺术》 -- [Java Scheduler ScheduledExecutorService ScheduledThreadPoolExecutor Example](https://www.journaldev.com/2340/java-scheduler-scheduledexecutorservice-scheduledthreadpoolexecutor-example "Java Scheduler ScheduledExecutorService ScheduledThreadPoolExecutor Example") -- [java.util.concurrent.ScheduledThreadPoolExecutor Example](https://examples.javacodegeeks.com/core-java/util/concurrent/scheduledthreadpoolexecutor/java-util-concurrent-scheduledthreadpoolexecutor-example/ "java.util.concurrent.ScheduledThreadPoolExecutor Example") -- [ThreadPoolExecutor – Java Thread Pool Example](https://www.journaldev.com/1069/threadpoolexecutor-java-thread-pool-example-executorservice "ThreadPoolExecutor – Java Thread Pool Example") - -## 九 其他推荐阅读 - -- [Java 并发(三)线程池原理](https://www.cnblogs.com/warehouse/p/10720781.html "Java并发(三)线程池原理") -- [如何优雅的使用和理解线程池](https://github.com/crossoverJie/JCSprout/blob/master/MD/ThreadPoolExecutor.md "如何优雅的使用和理解线程池") diff --git a/docs/java/Multithread/synchronized.md b/docs/java/Multithread/synchronized.md deleted file mode 100644 index 0a1f4f2..0000000 --- a/docs/java/Multithread/synchronized.md +++ /dev/null @@ -1,169 +0,0 @@ - - -![Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/%E4%BA%8C%20%20Synchronized%20%E5%85%B3%E9%94%AE%E5%AD%97%E4%BD%BF%E7%94%A8%E3%80%81%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E3%80%81JDK1.6%20%E4%B9%8B%E5%90%8E%E7%9A%84%E5%BA%95%E5%B1%82%E4%BC%98%E5%8C%96%E4%BB%A5%E5%8F%8A%20%E5%92%8CReenTrantLock%20%E7%9A%84%E5%AF%B9%E6%AF%94.png) - -### synchronized关键字最主要的三种使用方式的总结 - -- **修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁** -- **修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁** 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。 -- **修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。** 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能! - -下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。 - -面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” - - - -**双重校验锁实现对象单例(线程安全)** - -```java -public class Singleton { - - private volatile static Singleton uniqueInstance; - - private Singleton() { - } - - public static Singleton getUniqueInstance() { - //先判断对象是否已经实例过,没有实例化过才进入加锁代码 - if (uniqueInstance == null) { - //类对象加锁 - synchronized (Singleton.class) { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} -``` -另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。 - -uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: - -1. 为 uniqueInstance 分配内存空间 -2. 初始化 uniqueInstance -3. 将 uniqueInstance 指向分配的内存地址 - -但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 - -使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 - - -###synchronized 关键字底层原理总结 - - - -**synchronized 关键字底层原理属于 JVM 层面。** - -**① synchronized 同步语句块的情况** - -```java -public class SynchronizedDemo { - public void method() { - synchronized (this) { - System.out.println("synchronized 代码块"); - } - } -} - -``` - -通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 - -![synchronized 关键字原理](https://images.gitbook.cn/abc37c80-d21d-11e8-aab3-09d30029e0d5) - -从上面我们可以看出: - -**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 - -**② synchronized 修饰方法的的情况** - -```java -public class SynchronizedDemo2 { - public synchronized void method() { - System.out.println("synchronized 方法"); - } -} - -``` - -![synchronized 关键字原理](https://images.gitbook.cn/7d407bf0-d21e-11e8-b2d6-1188c7e0dd7e) - -synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 - - -在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 - - -### JDK1.6 之后的底层优化 - -JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 - -锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 - -**①偏向锁** - -**引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。 - -偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。 - -但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。 - -**② 轻量级锁** - -倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。** 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。 - -**轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!** - -**③ 自旋锁和自适应自旋** - -轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。 - -互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。 - -**一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。 - -百度百科对自旋锁的解释: - -> 何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。 - -自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过`--XX:+UseSpinning`参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。**自旋次数的默认值是10次,用户可以修改`--XX:PreBlockSpin`来更改**。 - -另外,**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。 - -**④ 锁消除** - -锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。 - -**⑤ 锁粗化** - -原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。 - -大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。 - -### Synchronized 和 ReenTrantLock 的对比 - - -**① 两者都是可重入锁** - -两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 - -**② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API** - -synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - -**③ ReenTrantLock 比 synchronized 增加了一些高级功能** - -相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)** - -- **ReenTrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -- **ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 -- synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),**线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”** ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。 - -如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。 - -**④ 性能已不是选择标准** - -在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。**JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作**。 diff --git "a/docs/java/Multithread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" "b/docs/java/Multithread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" deleted file mode 100644 index ed60634..0000000 --- "a/docs/java/Multithread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" +++ /dev/null @@ -1,229 +0,0 @@ -点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 - - - -- [一 JDK 提供的并发容器总结](#一-jdk-提供的并发容器总结 "一 JDK 提供的并发容器总结") -- [二 ConcurrentHashMap](#二-concurrenthashmap "二 ConcurrentHashMap") -- [三 CopyOnWriteArrayList](#三-copyonwritearraylist "三 CopyOnWriteArrayList") - - [3.1 CopyOnWriteArrayList 简介](#31-copyonwritearraylist-简介 "3.1 CopyOnWriteArrayList 简介") - - [3.2 CopyOnWriteArrayList 是如何做到的?](#32-copyonwritearraylist-是如何做到的? "3.2 CopyOnWriteArrayList 是如何做到的?") - - [3.3 CopyOnWriteArrayList 读取和写入源码简单分析](#33-copyonwritearraylist-读取和写入源码简单分析 "3.3 CopyOnWriteArrayList 读取和写入源码简单分析") - - [3.3.1 CopyOnWriteArrayList 读取操作的实现](#331-copyonwritearraylist-读取操作的实现 "3.3.1 CopyOnWriteArrayList 读取操作的实现") - - [3.3.2 CopyOnWriteArrayList 写入操作的实现](#332-copyonwritearraylist-写入操作的实现 "3.3.2 CopyOnWriteArrayList 写入操作的实现") -- [四 ConcurrentLinkedQueue](#四-concurrentlinkedqueue "四 ConcurrentLinkedQueue") -- [五 BlockingQueue](#五-blockingqueue "五 BlockingQueue") - - [5.1 BlockingQueue 简单介绍](#51-blockingqueue-简单介绍 "5.1 BlockingQueue 简单介绍") - - [5.2 ArrayBlockingQueue](#52-arrayblockingqueue "5.2 ArrayBlockingQueue") - - [5.3 LinkedBlockingQueue](#53-linkedblockingqueue "5.3 LinkedBlockingQueue") - - [5.4 PriorityBlockingQueue](#54-priorityblockingqueue "5.4 PriorityBlockingQueue") -- [六 ConcurrentSkipListMap](#六-concurrentskiplistmap "六 ConcurrentSkipListMap") -- [七 参考](#七-参考 "七 参考") - - - -## 一 JDK 提供的并发容器总结 - -JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。 - -- **ConcurrentHashMap:** 线程安全的 HashMap -- **CopyOnWriteArrayList:** 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector. -- **ConcurrentLinkedQueue:** 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。 -- **BlockingQueue:** 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。 -- **ConcurrentSkipListMap:** 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。 - -## 二 ConcurrentHashMap - -我们知道 HashMap 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 `Collections.synchronizedMap()` 方法来包装我们的 HashMap。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。 - -所以就有了 HashMap 的线程安全版本—— ConcurrentHashMap 的诞生。在 ConcurrentHashMap 中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。 - -关于 ConcurrentHashMap 相关问题,我在 [Java 集合框架常见面试题](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md "Java集合框架常见面试题") 这篇文章中已经提到过。下面梳理一下关于 ConcurrentHashMap 比较重要的问题: - -- [ConcurrentHashMap 和 Hashtable 的区别](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md#concurrenthashmap-%E5%92%8C-hashtable-%E7%9A%84%E5%8C%BA%E5%88%AB "ConcurrentHashMap 和 Hashtable 的区别") -- [ConcurrentHashMap 线程安全的具体实现方式/底层具体实现](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md#concurrenthashmap%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F%E5%BA%95%E5%B1%82%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0 "ConcurrentHashMap线程安全的具体实现方式/底层具体实现") - -## 三 CopyOnWriteArrayList - -### 3.1 CopyOnWriteArrayList 简介 - -```java -public class CopyOnWriteArrayList -extends Object -implements List, RandomAccess, Cloneable, Serializable -``` - -在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问 List 的内部数据,毕竟读取操作是安全的。 - -这和我们之前在多线程章节讲过 `ReentrantReadWriteLock` 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 `CopyOnWriteArrayList` 类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,`CopyOnWriteArrayList` 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。**那它是怎么做的呢?** - -### 3.2 CopyOnWriteArrayList 是如何做到的? - -`CopyOnWriteArrayList` 类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。 - -从 `CopyOnWriteArrayList` 的名字就能看出`CopyOnWriteArrayList` 是满足`CopyOnWrite` 的 ArrayList,所谓`CopyOnWrite` 也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了。 - -### 3.3 CopyOnWriteArrayList 读取和写入源码简单分析 - -#### 3.3.1 CopyOnWriteArrayList 读取操作的实现 - -读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。 - -```java - /** The array, accessed only via getArray/setArray. */ - private transient volatile Object[] array; - public E get(int index) { - return get(getArray(), index); - } - @SuppressWarnings("unchecked") - private E get(Object[] a, int index) { - return (E) a[index]; - } - final Object[] getArray() { - return array; - } - -``` - -#### 3.3.2 CopyOnWriteArrayList 写入操作的实现 - -CopyOnWriteArrayList 写入操作 add() 方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。 - -```java - /** - * Appends the specified element to the end of this list. - * - * @param e element to be appended to this list - * @return {@code true} (as specified by {@link Collection#add}) - */ - public boolean add(E e) { - final ReentrantLock lock = this.lock; - lock.lock();//加锁 - try { - Object[] elements = getArray(); - int len = elements.length; - Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组 - newElements[len] = e; - setArray(newElements); - return true; - } finally { - lock.unlock();//释放锁 - } - } -``` - -## 四 ConcurrentLinkedQueue - -Java 提供的线程安全的 Queue 可以分为**阻塞队列**和**非阻塞队列**,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 **阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。** - -从名字可以看出,`ConcurrentLinkedQueue`这个队列使用链表作为其数据结构.ConcurrentLinkedQueue 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。 - -ConcurrentLinkedQueue 内部代码我们就不分析了,大家知道 ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法来实现线程安全就好了。 - -ConcurrentLinkedQueue 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。 - -## 五 BlockingQueue - -### 5.1 BlockingQueue 简单介绍 - -上面我们己经提到了 ConcurrentLinkedQueue 作为高性能的非阻塞队列。下面我们要讲到的是阻塞队列——BlockingQueue。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。 - -BlockingQueue 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口。下面是 BlockingQueue 的相关实现类: - -![BlockingQueue 的实现类](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/51622268.jpg) - -**下面主要介绍一下:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,这三个 BlockingQueue 的实现类。** - -### 5.2 ArrayBlockingQueue - -**ArrayBlockingQueue** 是 BlockingQueue 接口的有界队列实现类,底层采用**数组**来实现。ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。 - -ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码: - -```java -private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10,true); -``` - -### 5.3 LinkedBlockingQueue - -**LinkedBlockingQueue** 底层基于**单向链表**实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE。 - -**相关构造方法:** - -```java - /** - *某种意义上的无界队列 - * Creates a {@code LinkedBlockingQueue} with a capacity of - * {@link Integer#MAX_VALUE}. - */ - public LinkedBlockingQueue() { - this(Integer.MAX_VALUE); - } - - /** - *有界队列 - * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. - * - * @param capacity the capacity of this queue - * @throws IllegalArgumentException if {@code capacity} is not greater - * than zero - */ - public LinkedBlockingQueue(int capacity) { - if (capacity <= 0) throw new IllegalArgumentException(); - this.capacity = capacity; - last = head = new Node(null); - } -``` - -### 5.4 PriorityBlockingQueue - -**PriorityBlockingQueue** 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 `compareTo()` 方法来指定元素排序规则,或者初始化时通过构造器参数 `Comparator` 来指定排序规则。 - -PriorityBlockingQueue 并发控制采用的是 **ReentrantLock**,队列为无界队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 也可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,**如果空间不够的话会自动扩容**)。 - -简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。 - -**推荐文章:** - -《解读 Java 并发队列 BlockingQueue》 - -[https://javadoop.com/post/java-concurrent-queue](https://javadoop.com/post/java-concurrent-queue "https://javadoop.com/post/java-concurrent-queue") - -## 六 ConcurrentSkipListMap - -下面这部分内容参考了极客时间专栏[《数据结构与算法之美》](https://time.geekbang.org/column/intro/126?code=zl3GYeAsRI4rEJIBNu5B/km7LSZsPDlGWQEpAYw5Vu0=&utm_term=SPoster "《数据结构与算法之美》")以及《实战 Java 高并发程序设计》。 - -**为了引出 ConcurrentSkipListMap,先带着大家简单理解一下跳表。** - -对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 **O(logn)** 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。 - -跳表的本质是同时维护了多个链表,并且链表是分层的, - -![2级索引跳表](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/93666217.jpg) - -最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。 - -跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。 - -![在跳表中查找元素18](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/32005738.jpg) - -查找 18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。 - -从上面很容易看出,**跳表是一种利用空间换时间的算法。** - -使用跳表实现 Map 和使用哈希算法实现 Map 的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是 ConcurrentSkipListMap。 - -## 七 参考 - -- 《实战 Java 高并发程序设计》 -- https://javadoop.com/post/java-concurrent-queue -- https://juejin.im/post/5aeebd02518825672f19c546 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取! - -**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/Multithread/\345\271\266\345\217\221\347\274\226\347\250\213\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/java/Multithread/\345\271\266\345\217\221\347\274\226\347\250\213\345\237\272\347\241\200\347\237\245\350\257\206.md" deleted file mode 100644 index 68509cd..0000000 --- "a/docs/java/Multithread/\345\271\266\345\217\221\347\274\226\347\250\213\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ /dev/null @@ -1,407 +0,0 @@ -# Java 并发基础知识 - -Java 并发的基础知识,可能会在笔试中遇到,技术面试中也可能以并发知识环节提问的第一个问题出现。比如面试官可能会问你:“谈谈自己对于进程和线程的理解,两者的区别是什么?” - -**本节思维导图:** - -## 一 进程和线程 - -进程和线程的对比这一知识点由于过于基础,所以在面试中很少碰到,但是极有可能会在笔试题中碰到。 - -常见的提问形式是这样的:**“什么是线程和进程?,请简要描述线程与进程的关系、区别及优缺点? ”**。 - -### 1.1. 何为进程? - -进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 - -在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。 - -如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。 - -![进程 ](https://images.gitbook.cn/a0929b60-d133-11e8-88a4-5328c5b70145) - -### 1.2 何为线程? - -线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。 - -```java -public class MultiThread { - public static void main(String[] args) { - // 获取 Java 线程管理 MXBean - ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); - // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 - ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); - // 遍历线程信息,仅打印线程 ID 和线程名称信息 - for (ThreadInfo threadInfo : threadInfos) { - System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); - } - } -} -``` - -上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可): - -``` -[5] Attach Listener //添加事件 -[4] Signal Dispatcher // 分发处理给 JVM 信号的线程 -[3] Finalizer //调用对象 finalize 方法的线程 -[2] Reference Handler //清除 reference 线程 -[1] main //main 线程,程序入口 -``` - -从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。 - -### 1.3 从 JVM 角度说进程和线程之间的关系(重要) - -#### 1.3.1 图解进程和线程的关系 - -下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下我的这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》]() - -
- -
- - -从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 - -下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢? - -#### 1.3.2 程序计数器为什么是私有的? - -程序计数器主要有下面两个作用: - -1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 - -需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。 - -所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 - -#### 1.3.3 虚拟机栈和本地方法栈为什么是私有的? - -- **虚拟机栈:**每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 -- **本地方法栈:**和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 - -所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 - -#### 1.3.4 一句话简单了解堆和方法区 - -堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 - -## 二 多线程并发编程 - -### 2.1 并发与并行概念解读 - -- **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行); -- **并行:**单位时间内,多个任务同时执行。 - -### 2.2 为什么要使用多线程? - -先从总体上来说: - -- **从计算机底层来说:**线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 -- **从当代互联网发展趋势来说:**现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 - -再深入到计算机底层来探讨: - -- **单核时代:** 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。 -- **多核时代:** 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。 - -### 2.3 使用多线程可能带来的问题 - -并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。 - -## 三 线程的创建与运行 - -前两种实际上很少使用,一般都是用线程池的方式比较多一点。 - -### 3.1 继承 Thread 类的方式 - - -```java -public class MyThread extends Thread { - @Override - public void run() { - super.run(); - System.out.println("MyThread"); - } -} -``` -Run.java - -```java -public class Run { - - public static void main(String[] args) { - MyThread mythread = new MyThread(); - mythread.start(); - System.out.println("运行结束"); - } - -} - -``` -运行结果: -![结果 ](https://user-gold-cdn.xitu.io/2018/3/20/16243e80f22a2d54?w=161&h=54&f=jpeg&s=7380) - -从上面的运行结果可以看出:线程是一个子任务,CPU 以不确定的方式,或者说是以随机的时间来调用线程中的 run 方法。 - -### 3.2 实现 Runnable 接口的方式 - -推荐实现 Runnable 接口方式开发多线程,因为 Java 单继承但是可以实现多个接口。 - -MyRunnable.java - -```java -public class MyRunnable implements Runnable { - @Override - public void run() { - System.out.println("MyRunnable"); - } -} -``` - -Run.java - -```java -public class Run { - - public static void main(String[] args) { - Runnable runnable=new MyRunnable(); - Thread thread=new Thread(runnable); - thread.start(); - System.out.println("运行结束!"); - } - -} -``` -运行结果: -![运行结果 ](https://user-gold-cdn.xitu.io/2018/3/20/16243f4373c6141a?w=137&h=46&f=jpeg&s=7316) - -### 3.3 使用线程池的方式 - -使用线程池的方式也是最推荐的一种方式,另外,《阿里巴巴 Java 开发手册》在第一章第六节并发处理这一部分也强调到“线程资源必须通过线程池提供,不允许在应用中自行显示创建线程”。这里就不给大家演示代码了,线程池这一节会详细介绍到这部分内容。 - -## 四 线程的生命周期和状态 - -Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。 - -![Java 线程的状态 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81.png) - -线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节): - -![Java 线程状态变迁 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%20%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E5%8F%98%E8%BF%81.png) - - - -由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。 - -> 操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。 - -![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png) - -当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的` run() `方法之后将会进入到 **TERMINATED(终止)** 状态。 - -## 五 线程优先级 - -**理论上**来说系统会根据优先级来决定首先使哪个线程进入运行状态。当 CPU 比较闲的时候,设置线程优先级几乎不会有任何作用,而且很多操作系统压根不会不会理会你设置的线程优先级,所以不要让业务过度依赖于线程的优先级。 - -另外,**线程优先级具有继承特性**比如 A 线程启动 B 线程,则 B 线程的优先级和 A 是一样的。**线程优先级还具有随机性** 也就是说线程优先级高的不一定每一次都先执行完。 - -Thread 类中包含的成员变量代表了线程的某些优先级。如**Thread.MIN_PRIORITY(常数 1)**,**Thread.NORM_PRIORITY(常数 5)**,**Thread.MAX_PRIORITY(常数 10)**。其中每个线程的优先级都在**1** 到**10** 之间,在默认情况下优先级都是**Thread.NORM_PRIORITY(常数 5)**。 - -**一般情况下,不会对线程设定优先级别,更不会让某些业务严重地依赖线程的优先级别,比如权重,借助优先级设定某个任务的权重,这种方式是不可取的,一般定义线程的时候使用默认的优先级就好了。** - -**相关方法:** - -```java -public final void setPriority(int newPriority) //为线程设定优先级 -public final int getPriority() //获取线程的优先级 -``` -**设置线程优先级方法源码:** - -```java - public final void setPriority(int newPriority) { - ThreadGroup g; - checkAccess(); - //线程游戏优先级不能小于 1 也不能大于 10,否则会抛出异常 - if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { - throw new IllegalArgumentException(); - } - //如果指定的线程优先级大于该线程所在线程组的最大优先级,那么该线程的优先级将设为线程组的最大优先级 - if((g = getThreadGroup()) != null) { - if (newPriority > g.getMaxPriority()) { - newPriority = g.getMaxPriority(); - } - setPriority0(priority = newPriority); - } - } - -``` - -## 六 守护线程和用户线程 - -**守护线程和用户线程简介:** - -- **用户 (User) 线程:**运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程 -- **守护 (Daemon) 线程:**运行在后台,为其他前台线程服务.也可以说守护线程是 JVM 中非守护线程的 **“佣人”**。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作. - -main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。 - -**那么守护线程和用户线程有什么区别呢?** - -比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。 - -**注意事项:** - -1. `setDaemon(true)`必须在`start()`方法前执行,否则会抛出 `IllegalThreadStateException` 异常 -2. 在守护线程中产生的新线程也是守护线程 -3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑 -4. 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。 - -## 七 上下文切换 - -多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 - -概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 - -上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 - -Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 - -## 八 线程死锁 - -### 认识线程死锁 - -多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 - -如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 - -![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4死锁1.png) - -下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》): - -```java -public class DeadLockDemo { - private static Object resource1 = new Object();//资源 1 - private static Object resource2 = new Object();//资源 2 - - public static void main(String[] args) { - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 1").start(); - - new Thread(() -> { - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource1"); - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - } - } - }, "线程 2").start(); - } -} -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 2,5,main]get resource2 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 2,5,main]waiting get resource1 -``` - -线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过` Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。 - -学过操作系统的朋友都知道产生死锁必须具备以下四个条件: - -1. 互斥条件:该资源任意一个时刻只由一个线程占用。 -1. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 -1. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 -1. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 - -### 如何预防线程死锁? - -我们只要破坏产生死锁的四个条件中的其中一个就可以了。 - -**破坏互斥条件** - -这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 - -**破坏请求与保持条件** - -一次性申请所有的资源。 - -**破坏不剥夺条件** - -占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 - -**破坏循环等待条件** - -靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 - -我们对线程 2 的代码修改成下面这样就不会产生死锁了。 - -```java - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 2").start(); -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 1,5,main]get resource2 -Thread[线程 2,5,main]get resource1 -Thread[线程 2,5,main]waiting get resource2 -Thread[线程 2,5,main]get resource2 - -Process finished with exit code 0 -``` - -我们分析一下上面的代码为什么避免了死锁的发生? - -线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 - -## 参考 - -- 《Java 并发编程之美》 - -- 《Java 并发编程的艺术》 - -- https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/ - - \ No newline at end of file diff --git a/docs/java/collection/ArrayList-Grow.md b/docs/java/collection/ArrayList-Grow.md deleted file mode 100644 index ab56b80..0000000 --- a/docs/java/collection/ArrayList-Grow.md +++ /dev/null @@ -1,358 +0,0 @@ - -## 一 先从 ArrayList 的构造函数说起 - -**ArrayList有三种方式来初始化,构造方法源码如下:** - -```java - /** - * 默认初始容量大小 - */ - private static final int DEFAULT_CAPACITY = 10; - - - private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; - - /** - *默认构造函数,使用初始容量10构造一个空列表(无参数构造) - */ - public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; - } - - /** - * 带初始容量参数的构造函数。(用户自己指定容量) - */ - public ArrayList(int initialCapacity) { - if (initialCapacity > 0) {//初始容量大于0 - //创建initialCapacity大小的数组 - this.elementData = new Object[initialCapacity]; - } else if (initialCapacity == 0) {//初始容量等于0 - //创建空数组 - this.elementData = EMPTY_ELEMENTDATA; - } else {//初始容量小于0,抛出异常 - throw new IllegalArgumentException("Illegal Capacity: "+ - initialCapacity); - } - } - - - /** - *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回 - *如果指定的集合为null,throws NullPointerException。 - */ - public ArrayList(Collection c) { - elementData = c.toArray(); - if ((size = elementData.length) != 0) { - // c.toArray might (incorrectly) not return Object[] (see 6260652) - if (elementData.getClass() != Object[].class) - elementData = Arrays.copyOf(elementData, size, Object[].class); - } else { - // replace with empty array. - this.elementData = EMPTY_ELEMENTDATA; - } - } - -``` - -细心的同学一定会发现 :**以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。** 下面在我们分析 ArrayList 扩容时会讲到这一点内容! - -## 二 一步一步分析 ArrayList 扩容机制 - -这里以无参构造函数创建的 ArrayList 为例分析 - -### 1. 先来看 `add` 方法 - -```java - /** - * 将指定的元素追加到此列表的末尾。 - */ - public boolean add(E e) { - //添加元素之前,先调用ensureCapacityInternal方法 - ensureCapacityInternal(size + 1); // Increments modCount!! - //这里看到ArrayList添加元素的实质就相当于为数组赋值 - elementData[size++] = e; - return true; - } -``` -### 2. 再来看看 `ensureCapacityInternal()` 方法 - -可以看到 `add` 方法 首先调用了`ensureCapacityInternal(size + 1)` - -```java - //得到最小扩容量 - private void ensureCapacityInternal(int minCapacity) { - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - // 获取默认的容量和传入参数的较大值 - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); - } - - ensureExplicitCapacity(minCapacity); - } -``` -**当 要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10。** - -### 3. `ensureExplicitCapacity()` 方法 - -如果调用 `ensureCapacityInternal()` 方法就一定会进过(执行)这个方法,下面我们来研究一下这个方法的源码! - -```java - //判断是否需要扩容 - private void ensureExplicitCapacity(int minCapacity) { - modCount++; - - // overflow-conscious code - if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容,调用此方法代表已经开始扩容了 - grow(minCapacity); - } - -``` - -我们来仔细分析一下: - -- 当我们要 add 进第1个元素到 ArrayList 时,elementData.length 为0 (因为还是一个空的 list),因为执行了 `ensureCapacityInternal()` 方法 ,所以 minCapacity 此时为10。此时,`minCapacity - elementData.length > 0 `成立,所以会进入 `grow(minCapacity)` 方法。 -- 当add第2个元素时,minCapacity 为2,此时e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,`minCapacity - elementData.length > 0 ` 不成立,所以不会进入 (执行)`grow(minCapacity)` 方法。 -- 添加第3、4···到第10个元素时,依然不会执行grow方法,数组容量都为10。 - -直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大。进入grow方法进行扩容。 - -### 4. `grow()` 方法 - -```java - /** - * 要分配的最大数组大小 - */ - private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - - /** - * ArrayList扩容的核心方法。 - */ - private void grow(int minCapacity) { - // oldCapacity为旧容量,newCapacity为新容量 - int oldCapacity = elementData.length; - //将oldCapacity 右移一位,其效果相当于oldCapacity /2, - //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, - int newCapacity = oldCapacity + (oldCapacity >> 1); - //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE, - //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); - } -``` - -**int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍!(JDK1.6版本以后)** JDk1.6版本时,扩容之后容量为 1.5 倍+1!详情请参考源码 - -> ">>"(移位运算符):>>1 右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源   - -**我们再来通过例子探究一下`grow()` 方法 :** - -- 当add第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入 `hugeCapacity` 方法。数组容量为10,add方法中 return true,size增为1。 -- 当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。 -- 以此类推······ - -**这里补充一点比较重要,但是容易被忽视掉的知识点:** - -- java 中的 `length `属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. -- java 中的 `length()` 方法是针对字符串说的,如果想看这个字符串的长度则用到 `length()` 这个方法. -- java 中的 `size()` 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! - -### 5. `hugeCapacity()` 方法。 - -从上面 `grow()` 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 - - -```java - private static int hugeCapacity(int minCapacity) { - if (minCapacity < 0) // overflow - throw new OutOfMemoryError(); - //对minCapacity和MAX_ARRAY_SIZE进行比较 - //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小 - //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小 - //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - return (minCapacity > MAX_ARRAY_SIZE) ? - Integer.MAX_VALUE : - MAX_ARRAY_SIZE; - } -``` - - - -## 三 `System.arraycopy()` 和 `Arrays.copyOf()`方法 - - -阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及`add(int index, E element)`、`toArray()` 等方法中都用到了该方法! - - -### 3.1 `System.arraycopy()` 方法 - -```java - /** - * 在此列表中的指定位置插入指定的元素。 - *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 - */ - public void add(int index, E element) { - rangeCheckForAdd(index); - - ensureCapacityInternal(size + 1); // Increments modCount!! - //arraycopy()方法实现数组自己复制自己 - //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; - System.arraycopy(elementData, index, elementData, index + 1, size - index); - elementData[index] = element; - size++; - } -``` - -我们写一个简单的方法测试以下: - -```java -public class ArraycopyTest { - - public static void main(String[] args) { - // TODO Auto-generated method stub - int[] a = new int[10]; - a[0] = 0; - a[1] = 1; - a[2] = 2; - a[3] = 3; - System.arraycopy(a, 2, a, 3, 3); - a[2]=99; - for (int i = 0; i < a.length; i++) { - System.out.println(a[i]); - } - } - -} -``` - -结果: - -``` -0 1 99 2 3 0 0 0 0 0 -``` - -### 3.2 `Arrays.copyOf()`方法 - -```java - /** - 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。 - */ - public Object[] toArray() { - //elementData:要复制的数组;size:要复制的长度 - return Arrays.copyOf(elementData, size); - } -``` - -个人觉得使用 `Arrays.copyOf()`方法主要是为了给原有数组扩容,测试代码如下: - -```java -public class ArrayscopyOfTest { - - public static void main(String[] args) { - int[] a = new int[3]; - a[0] = 0; - a[1] = 1; - a[2] = 2; - int[] b = Arrays.copyOf(a, 10); - System.out.println("b.length"+b.length); - } -} -``` - -结果: - -``` -10 -``` - -### 3.3 两者联系和区别 - -**联系:** - -看两者源代码可以发现 copyOf() 内部实际调用了 `System.arraycopy()` 方法 - -**区别:** - -`arraycopy()` 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 `copyOf()` 是系统自动在内部新建一个数组,并返回该数组。 - -## 四 `ensureCapacity`方法 - -ArrayList 源码中有一个 `ensureCapacity` 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢? - -```java - /** - 如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。 - * - * @param minCapacity 所需的最小容量 - */ - public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } - } - -``` - -**最好在 add 大量元素之前用 `ensureCapacity` 方法,以减少增量重新分配的次数** - -我们通过下面的代码实际测试以下这个方法的效果: - -```java -public class EnsureCapacityTest { - public static void main(String[] args) { - ArrayList list = new ArrayList(); - final int N = 10000000; - long startTime = System.currentTimeMillis(); - for (int i = 0; i < N; i++) { - list.add(i); - } - long endTime = System.currentTimeMillis(); - System.out.println("使用ensureCapacity方法前:"+(endTime - startTime)); - - } -} -``` - -运行结果: - -``` -使用ensureCapacity方法前:2158 -``` - -```java -public class EnsureCapacityTest { - public static void main(String[] args) { - ArrayList list = new ArrayList(); - final int N = 10000000; - list = new ArrayList(); - long startTime1 = System.currentTimeMillis(); - list.ensureCapacity(N); - for (int i = 0; i < N; i++) { - list.add(i); - } - long endTime1 = System.currentTimeMillis(); - System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1)); - } -} -``` - -运行结果: - -``` - -使用ensureCapacity方法前:1773 -``` - -通过运行结果,我们可以看出向 ArrayList 添加大量元素之前最好先使用`ensureCapacity` 方法,以减少增量重新分配的次数。 diff --git a/docs/java/collection/ArrayList.md b/docs/java/collection/ArrayList.md deleted file mode 100644 index f6578a7..0000000 --- a/docs/java/collection/ArrayList.md +++ /dev/null @@ -1,737 +0,0 @@ - - -- [ArrayList简介](#arraylist简介) -- [ArrayList核心源码](#arraylist核心源码) -- [ArrayList源码分析](#arraylist源码分析) - - [System.arraycopy\(\)和Arrays.copyOf\(\)方法](#systemarraycopy和arrayscopyof方法) - - [两者联系与区别](#两者联系与区别) - - [ArrayList核心扩容技术](#arraylist核心扩容技术) - - [内部类](#内部类) -- [ArrayList经典Demo](#arraylist经典demo) - - - - -### ArrayList简介 -  ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用`ensureCapacity`操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。 - - 它继承于 **AbstractList**,实现了 **List**, **RandomAccess**, **Cloneable**, **java.io.Serializable** 这些接口。 - - 在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为**O(n)**,求表长以及增加元素,取第 i 元素的时间复杂度为**O(1)** - -  ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。 - -  ArrayList 实现了**RandomAccess 接口**, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 - -  ArrayList 实现了**Cloneable 接口**,即覆盖了函数 clone(),**能被克隆**。 - -  ArrayList 实现**java.io.Serializable 接口**,这意味着ArrayList**支持序列化**,**能通过序列化去传输**。 - -  和 Vector 不同,**ArrayList 中的操作不是线程安全的**!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。 -### ArrayList核心源码 - -```java -package java.util; - -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.function.UnaryOperator; - - -public class ArrayList extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable -{ - private static final long serialVersionUID = 8683452581122892189L; - - /** - * 默认初始容量大小 - */ - private static final int DEFAULT_CAPACITY = 10; - - /** - * 空数组(用于空实例)。 - */ - private static final Object[] EMPTY_ELEMENTDATA = {}; - - //用于默认大小空实例的共享空数组实例。 - //我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。 - private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; - - /** - * 保存ArrayList数据的数组 - */ - transient Object[] elementData; // non-private to simplify nested class access - - /** - * ArrayList 所包含的元素个数 - */ - private int size; - - /** - * 带初始容量参数的构造函数。(用户自己指定容量) - */ - public ArrayList(int initialCapacity) { - if (initialCapacity > 0) { - //创建initialCapacity大小的数组 - this.elementData = new Object[initialCapacity]; - } else if (initialCapacity == 0) { - //创建空数组 - this.elementData = EMPTY_ELEMENTDATA; - } else { - throw new IllegalArgumentException("Illegal Capacity: "+ - initialCapacity); - } - } - - /** - *默认构造函数,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10 - */ - public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; - } - - /** - * 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。 - */ - public ArrayList(Collection c) { - // - elementData = c.toArray(); - //如果指定集合元素个数不为0 - if ((size = elementData.length) != 0) { - // c.toArray 可能返回的不是Object类型的数组所以加上下面的语句用于判断, - //这里用到了反射里面的getClass()方法 - if (elementData.getClass() != Object[].class) - elementData = Arrays.copyOf(elementData, size, Object[].class); - } else { - // 用空数组代替 - this.elementData = EMPTY_ELEMENTDATA; - } - } - - /** - * 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。 - */ - public void trimToSize() { - modCount++; - if (size < elementData.length) { - elementData = (size == 0) - ? EMPTY_ELEMENTDATA - : Arrays.copyOf(elementData, size); - } - } -//下面是ArrayList的扩容机制 -//ArrayList的扩容机制提高了性能,如果每次只扩充一个, -//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。 - /** - * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量 - * @param minCapacity 所需的最小容量 - */ - public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } - } - //得到最小扩容量 - private void ensureCapacityInternal(int minCapacity) { - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - // 获取默认的容量和传入参数的较大值 - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); - } - - ensureExplicitCapacity(minCapacity); - } - //判断是否需要扩容 - private void ensureExplicitCapacity(int minCapacity) { - modCount++; - - // overflow-conscious code - if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容,调用此方法代表已经开始扩容了 - grow(minCapacity); - } - - /** - * 要分配的最大数组大小 - */ - private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - - /** - * ArrayList扩容的核心方法。 - */ - private void grow(int minCapacity) { - // oldCapacity为旧容量,newCapacity为新容量 - int oldCapacity = elementData.length; - //将oldCapacity 右移一位,其效果相当于oldCapacity /2, - //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, - int newCapacity = oldCapacity + (oldCapacity >> 1); - //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - //再检查新容量是否超出了ArrayList所定义的最大容量, - //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, - //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); - } - //比较minCapacity和 MAX_ARRAY_SIZE - private static int hugeCapacity(int minCapacity) { - if (minCapacity < 0) // overflow - throw new OutOfMemoryError(); - return (minCapacity > MAX_ARRAY_SIZE) ? - Integer.MAX_VALUE : - MAX_ARRAY_SIZE; - } - - /** - *返回此列表中的元素数。 - */ - public int size() { - return size; - } - - /** - * 如果此列表不包含元素,则返回 true 。 - */ - public boolean isEmpty() { - //注意=和==的区别 - return size == 0; - } - - /** - * 如果此列表包含指定的元素,则返回true 。 - */ - public boolean contains(Object o) { - //indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 - return indexOf(o) >= 0; - } - - /** - *返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 - */ - public int indexOf(Object o) { - if (o == null) { - for (int i = 0; i < size; i++) - if (elementData[i]==null) - return i; - } else { - for (int i = 0; i < size; i++) - //equals()方法比较 - if (o.equals(elementData[i])) - return i; - } - return -1; - } - - /** - * 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。. - */ - public int lastIndexOf(Object o) { - if (o == null) { - for (int i = size-1; i >= 0; i--) - if (elementData[i]==null) - return i; - } else { - for (int i = size-1; i >= 0; i--) - if (o.equals(elementData[i])) - return i; - } - return -1; - } - - /** - * 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。) - */ - public Object clone() { - try { - ArrayList v = (ArrayList) super.clone(); - //Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度 - v.elementData = Arrays.copyOf(elementData, size); - v.modCount = 0; - return v; - } catch (CloneNotSupportedException e) { - // 这不应该发生,因为我们是可以克隆的 - throw new InternalError(e); - } - } - - /** - *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 - *返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。 - *因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。 - */ - public Object[] toArray() { - return Arrays.copyOf(elementData, size); - } - - /** - * 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); - *返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。 - *否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。 - *如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。 - *(这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。) - */ - @SuppressWarnings("unchecked") - public T[] toArray(T[] a) { - if (a.length < size) - // 新建一个运行时类型的数组,但是ArrayList数组的内容 - return (T[]) Arrays.copyOf(elementData, size, a.getClass()); - //调用System提供的arraycopy()方法实现数组之间的复制 - System.arraycopy(elementData, 0, a, 0, size); - if (a.length > size) - a[size] = null; - return a; - } - - // Positional Access Operations - - @SuppressWarnings("unchecked") - E elementData(int index) { - return (E) elementData[index]; - } - - /** - * 返回此列表中指定位置的元素。 - */ - public E get(int index) { - rangeCheck(index); - - return elementData(index); - } - - /** - * 用指定的元素替换此列表中指定位置的元素。 - */ - public E set(int index, E element) { - //对index进行界限检查 - rangeCheck(index); - - E oldValue = elementData(index); - elementData[index] = element; - //返回原来在这个位置的元素 - return oldValue; - } - - /** - * 将指定的元素追加到此列表的末尾。 - */ - public boolean add(E e) { - ensureCapacityInternal(size + 1); // Increments modCount!! - //这里看到ArrayList添加元素的实质就相当于为数组赋值 - elementData[size++] = e; - return true; - } - - /** - * 在此列表中的指定位置插入指定的元素。 - *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 - */ - public void add(int index, E element) { - rangeCheckForAdd(index); - - ensureCapacityInternal(size + 1); // Increments modCount!! - //arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己 - System.arraycopy(elementData, index, elementData, index + 1, - size - index); - elementData[index] = element; - size++; - } - - /** - * 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。 - */ - public E remove(int index) { - rangeCheck(index); - - modCount++; - E oldValue = elementData(index); - - int numMoved = size - index - 1; - if (numMoved > 0) - System.arraycopy(elementData, index+1, elementData, index, - numMoved); - elementData[--size] = null; // clear to let GC do its work - //从列表中删除的元素 - return oldValue; - } - - /** - * 从列表中删除指定元素的第一个出现(如果存在)。 如果列表不包含该元素,则它不会更改。 - *返回true,如果此列表包含指定的元素 - */ - public boolean remove(Object o) { - if (o == null) { - for (int index = 0; index < size; index++) - if (elementData[index] == null) { - fastRemove(index); - return true; - } - } else { - for (int index = 0; index < size; index++) - if (o.equals(elementData[index])) { - fastRemove(index); - return true; - } - } - return false; - } - - /* - * Private remove method that skips bounds checking and does not - * return the value removed. - */ - private void fastRemove(int index) { - modCount++; - int numMoved = size - index - 1; - if (numMoved > 0) - System.arraycopy(elementData, index+1, elementData, index, - numMoved); - elementData[--size] = null; // clear to let GC do its work - } - - /** - * 从列表中删除所有元素。 - */ - public void clear() { - modCount++; - - // 把数组中所有的元素的值设为null - for (int i = 0; i < size; i++) - elementData[i] = null; - - size = 0; - } - - /** - * 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。 - */ - public boolean addAll(Collection c) { - Object[] a = c.toArray(); - int numNew = a.length; - ensureCapacityInternal(size + numNew); // Increments modCount - System.arraycopy(a, 0, elementData, size, numNew); - size += numNew; - return numNew != 0; - } - - /** - * 将指定集合中的所有元素插入到此列表中,从指定的位置开始。 - */ - public boolean addAll(int index, Collection c) { - rangeCheckForAdd(index); - - Object[] a = c.toArray(); - int numNew = a.length; - ensureCapacityInternal(size + numNew); // Increments modCount - - int numMoved = size - index; - if (numMoved > 0) - System.arraycopy(elementData, index, elementData, index + numNew, - numMoved); - - System.arraycopy(a, 0, elementData, index, numNew); - size += numNew; - return numNew != 0; - } - - /** - * 从此列表中删除所有索引为fromIndex (含)和toIndex之间的元素。 - *将任何后续元素移动到左侧(减少其索引)。 - */ - protected void removeRange(int fromIndex, int toIndex) { - modCount++; - int numMoved = size - toIndex; - System.arraycopy(elementData, toIndex, elementData, fromIndex, - numMoved); - - // clear to let GC do its work - int newSize = size - (toIndex-fromIndex); - for (int i = newSize; i < size; i++) { - elementData[i] = null; - } - size = newSize; - } - - /** - * 检查给定的索引是否在范围内。 - */ - private void rangeCheck(int index) { - if (index >= size) - throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); - } - - /** - * add和addAll使用的rangeCheck的一个版本 - */ - private void rangeCheckForAdd(int index) { - if (index > size || index < 0) - throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); - } - - /** - * 返回IndexOutOfBoundsException细节信息 - */ - private String outOfBoundsMsg(int index) { - return "Index: "+index+", Size: "+size; - } - - /** - * 从此列表中删除指定集合中包含的所有元素。 - */ - public boolean removeAll(Collection c) { - Objects.requireNonNull(c); - //如果此列表被修改则返回true - return batchRemove(c, false); - } - - /** - * 仅保留此列表中包含在指定集合中的元素。 - *换句话说,从此列表中删除其中不包含在指定集合中的所有元素。 - */ - public boolean retainAll(Collection c) { - Objects.requireNonNull(c); - return batchRemove(c, true); - } - - - /** - * 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。 - *指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。 - *返回的列表迭代器是fail-fast 。 - */ - public ListIterator listIterator(int index) { - if (index < 0 || index > size) - throw new IndexOutOfBoundsException("Index: "+index); - return new ListItr(index); - } - - /** - *返回列表中的列表迭代器(按适当的顺序)。 - *返回的列表迭代器是fail-fast 。 - */ - public ListIterator listIterator() { - return new ListItr(0); - } - - /** - *以正确的顺序返回该列表中的元素的迭代器。 - *返回的迭代器是fail-fast 。 - */ - public Iterator iterator() { - return new Itr(); - } - - -``` -### ArrayList源码分析 -#### System.arraycopy()和Arrays.copyOf()方法 -  通过上面源码我们发现这两个实现数组复制的方法被广泛使用而且很多地方都特别巧妙。比如下面add(int index, E element)方法就很巧妙的用到了arraycopy()方法让数组自己复制自己实现让index开始之后的所有成员后移一个位置: -```java - /** - * 在此列表中的指定位置插入指定的元素。 - *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 - */ - public void add(int index, E element) { - rangeCheckForAdd(index); - - ensureCapacityInternal(size + 1); // Increments modCount!! - //arraycopy()方法实现数组自己复制自己 - //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; - System.arraycopy(elementData, index, elementData, index + 1, size - index); - elementData[index] = element; - size++; - } -``` -又如toArray()方法中用到了copyOf()方法 -```java - - /** - *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 - *返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。 - *因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。 - */ - public Object[] toArray() { - //elementData:要复制的数组;size:要复制的长度 - return Arrays.copyOf(elementData, size); - } -``` -##### 两者联系与区别 -**联系:** -看两者源代码可以发现`copyOf()`内部调用了`System.arraycopy()`方法 -**区别:** -1. arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位置 -2. copyOf()是系统自动在内部新建一个数组,并返回该数组。 -#### ArrayList 核心扩容技术 -```java -//下面是ArrayList的扩容机制 -//ArrayList的扩容机制提高了性能,如果每次只扩充一个, -//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。 - /** - * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量 - * @param minCapacity 所需的最小容量 - */ - public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } - } - //得到最小扩容量 - private void ensureCapacityInternal(int minCapacity) { - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - // 获取默认的容量和传入参数的较大值 - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); - } - - ensureExplicitCapacity(minCapacity); - } - //判断是否需要扩容,上面两个方法都要调用 - private void ensureExplicitCapacity(int minCapacity) { - modCount++; - - // 如果说minCapacity也就是所需的最小容量大于保存ArrayList数据的数组的长度的话,就需要调用grow(minCapacity)方法扩容。 - //这个minCapacity到底为多少呢?举个例子在添加元素(add)方法中这个minCapacity的大小就为现在数组的长度加1 - if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容,调用此方法代表已经开始扩容了 - grow(minCapacity); - } - -``` -```java - /** - * ArrayList扩容的核心方法。 - */ - private void grow(int minCapacity) { - //elementData为保存ArrayList数据的数组 - ///elementData.length求数组长度elementData.size是求数组中的元素个数 - // oldCapacity为旧容量,newCapacity为新容量 - int oldCapacity = elementData.length; - //将oldCapacity 右移一位,其效果相当于oldCapacity /2, - //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, - int newCapacity = oldCapacity + (oldCapacity >> 1); - //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - //再检查新容量是否超出了ArrayList所定义的最大容量, - //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, - //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); - } - -``` -  扩容机制代码已经做了详细的解释。另外值得注意的是大家很容易忽略的一个运算符:**移位运算符** -  **简介**:移位运算符就是在二进制的基础上对数字进行平移。按照平移的方向和填充数字的规则分为三种:<<(左移)>>(带符号右移)>>>(无符号右移)。 -  **作用**:**对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源** -  比如这里:int newCapacity = oldCapacity + (oldCapacity >> 1); -右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。 - -**另外需要注意的是:** - -1. java 中的**length 属性**是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. - -2. java 中的**length()方法**是针对字 符串String说的,如果想看这个字符串的长度则用到 length()这个方法. - -3. .java 中的**size()方法**是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! - - -#### 内部类 -```java - (1)private class Itr implements Iterator - (2)private class ListItr extends Itr implements ListIterator - (3)private class SubList extends AbstractList implements RandomAccess - (4)static final class ArrayListSpliterator implements Spliterator -``` -  ArrayList有四个内部类,其中的**Itr是实现了Iterator接口**,同时重写了里面的**hasNext()**, **next()**, **remove()** 等方法;其中的**ListItr** 继承 **Itr**,实现了**ListIterator接口**,同时重写了**hasPrevious()**, **nextIndex()**, **previousIndex()**, **previous()**, **set(E e)**, **add(E e)** 等方法,所以这也可以看出了 **Iterator和ListIterator的区别:** ListIterator在Iterator的基础上增加了添加对象,修改对象,逆向遍历等方法,这些是Iterator不能实现的。 -### ArrayList经典Demo - -```java -package list; -import java.util.ArrayList; -import java.util.Iterator; - -public class ArrayListDemo { - - public static void main(String[] srgs){ - ArrayList arrayList = new ArrayList(); - - System.out.printf("Before add:arrayList.size() = %d\n",arrayList.size()); - - arrayList.add(1); - arrayList.add(3); - arrayList.add(5); - arrayList.add(7); - arrayList.add(9); - System.out.printf("After add:arrayList.size() = %d\n",arrayList.size()); - - System.out.println("Printing elements of arrayList"); - // 三种遍历方式打印元素 - // 第一种:通过迭代器遍历 - System.out.print("通过迭代器遍历:"); - Iterator it = arrayList.iterator(); - while(it.hasNext()){ - System.out.print(it.next() + " "); - } - System.out.println(); - - // 第二种:通过索引值遍历 - System.out.print("通过索引值遍历:"); - for(int i = 0; i < arrayList.size(); i++){ - System.out.print(arrayList.get(i) + " "); - } - System.out.println(); - - // 第三种:for循环遍历 - System.out.print("for循环遍历:"); - for(Integer number : arrayList){ - System.out.print(number + " "); - } - - // toArray用法 - // 第一种方式(最常用) - Integer[] integer = arrayList.toArray(new Integer[0]); - - // 第二种方式(容易理解) - Integer[] integer1 = new Integer[arrayList.size()]; - arrayList.toArray(integer1); - - // 抛出异常,java不支持向下转型 - //Integer[] integer2 = new Integer[arrayList.size()]; - //integer2 = arrayList.toArray(); - System.out.println(); - - // 在指定位置添加元素 - arrayList.add(2,2); - // 删除指定位置上的元素 - arrayList.remove(2); - // 删除指定元素 - arrayList.remove((Object)3); - // 判断arrayList是否包含5 - System.out.println("ArrayList contains 5 is: " + arrayList.contains(5)); - - // 清空ArrayList - arrayList.clear(); - // 判断ArrayList是否为空 - System.out.println("ArrayList is empty: " + arrayList.isEmpty()); - } -} -``` - diff --git a/docs/java/collection/HashMap.md b/docs/java/collection/HashMap.md deleted file mode 100644 index c850cd5..0000000 --- a/docs/java/collection/HashMap.md +++ /dev/null @@ -1,542 +0,0 @@ - - -- [HashMap 简介](#hashmap-简介) -- [底层数据结构分析](#底层数据结构分析) - - [JDK1.8之前](#jdk18之前) - - [JDK1.8之后](#jdk18之后) -- [HashMap源码分析](#hashmap源码分析) - - [构造方法](#构造方法) - - [put方法](#put方法) - - [get方法](#get方法) - - [resize方法](#resize方法) -- [HashMap常用方法测试](#hashmap常用方法测试) - - - -> 感谢 [changfubai](https://github.com/changfubai) 对本文的改进做出的贡献! - -## HashMap 简介 -HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。 - -JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间,具体可以参考 `treeifyBin`方法。 - -## 底层数据结构分析 -### JDK1.8之前 -JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。** - -**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。** - -**JDK 1.8 HashMap 的 hash 方法源码:** - -JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 - - ```java - static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^ :按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } - ``` -对比一下 JDK1.7的 HashMap 的 hash 方法源码. - -```java -static int hash(int h) { - // This function ensures that hashCodes that differ only by - // constant multiples at each bit position have a bounded - // number of collisions (approximately 8 at default load factor). - - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); -} -``` - -相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 - -所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 - -![jdk1.8之前的内部结构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/jdk1.8之前的内部结构.png) - -### JDK1.8之后 -相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 - -![JDK1.8之后的HashMap底层数据结构](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/67233764.jpg) - -**类的属性:** -```java -public class HashMap extends AbstractMap implements Map, Cloneable, Serializable { - // 序列号 - private static final long serialVersionUID = 362498820763181265L; - // 默认的初始容量是16 - static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; - // 最大容量 - static final int MAXIMUM_CAPACITY = 1 << 30; - // 默认的填充因子 - static final float DEFAULT_LOAD_FACTOR = 0.75f; - // 当桶(bucket)上的结点数大于这个值时会转成红黑树 - static final int TREEIFY_THRESHOLD = 8; - // 当桶(bucket)上的结点数小于这个值时树转链表 - static final int UNTREEIFY_THRESHOLD = 6; - // 桶中结构转化为红黑树对应的table的最小大小 - static final int MIN_TREEIFY_CAPACITY = 64; - // 存储元素的数组,总是2的幂次倍 - transient Node[] table; - // 存放具体元素的集 - transient Set> entrySet; - // 存放元素的个数,注意这个不等于数组的长度。 - transient int size; - // 每次扩容和更改map结构的计数器 - transient int modCount; - // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容 - int threshold; - // 加载因子 - final float loadFactor; -} -``` -- **loadFactor加载因子** - - loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。 - - **loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值**。 - - 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。 - -- **threshold** - - **threshold = capacity * loadFactor**,**当Size>=threshold**的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 **衡量数组是否需要扩增的一个标准**。 - -**Node节点类源码:** - -```java -// 继承自 Map.Entry -static class Node implements Map.Entry { - final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较 - final K key;//键 - V value;//值 - // 指向下一个节点 - Node next; - Node(int hash, K key, V value, Node next) { - this.hash = hash; - this.key = key; - this.value = value; - this.next = next; - } - public final K getKey() { return key; } - public final V getValue() { return value; } - public final String toString() { return key + "=" + value; } - // 重写hashCode()方法 - public final int hashCode() { - return Objects.hashCode(key) ^ Objects.hashCode(value); - } - - public final V setValue(V newValue) { - V oldValue = value; - value = newValue; - return oldValue; - } - // 重写 equals() 方法 - public final boolean equals(Object o) { - if (o == this) - return true; - if (o instanceof Map.Entry) { - Map.Entry e = (Map.Entry)o; - if (Objects.equals(key, e.getKey()) && - Objects.equals(value, e.getValue())) - return true; - } - return false; - } -} -``` -**树节点类源码:** -```java -static final class TreeNode extends LinkedHashMap.Entry { - TreeNode parent; // 父 - TreeNode left; // 左 - TreeNode right; // 右 - TreeNode prev; // needed to unlink next upon deletion - boolean red; // 判断颜色 - TreeNode(int hash, K key, V val, Node next) { - super(hash, key, val, next); - } - // 返回根节点 - final TreeNode root() { - for (TreeNode r = this, p;;) { - if ((p = r.parent) == null) - return r; - r = p; - } -``` -## HashMap源码分析 -### 构造方法 - -HashMap 中有四个构造方法,它们分别如下: - -```java - // 默认构造函数。 - public HashMap() { - this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted - } - - // 包含另一个“Map”的构造函数 - public HashMap(Map m) { - this.loadFactor = DEFAULT_LOAD_FACTOR; - putMapEntries(m, false);//下面会分析到这个方法 - } - - // 指定“容量大小”的构造函数 - public HashMap(int initialCapacity) { - this(initialCapacity, DEFAULT_LOAD_FACTOR); - } - - // 指定“容量大小”和“加载因子”的构造函数 - public HashMap(int initialCapacity, float loadFactor) { - if (initialCapacity < 0) - throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); - if (initialCapacity > MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - if (loadFactor <= 0 || Float.isNaN(loadFactor)) - throw new IllegalArgumentException("Illegal load factor: " + loadFactor); - this.loadFactor = loadFactor; - this.threshold = tableSizeFor(initialCapacity); - } -``` - -**putMapEntries方法:** - -```java -final void putMapEntries(Map m, boolean evict) { - int s = m.size(); - if (s > 0) { - // 判断table是否已经初始化 - if (table == null) { // pre-size - // 未初始化,s为m的实际元素个数 - float ft = ((float)s / loadFactor) + 1.0F; - int t = ((ft < (float)MAXIMUM_CAPACITY) ? - (int)ft : MAXIMUM_CAPACITY); - // 计算得到的t大于阈值,则初始化阈值 - if (t > threshold) - threshold = tableSizeFor(t); - } - // 已初始化,并且m元素个数大于阈值,进行扩容处理 - else if (s > threshold) - resize(); - // 将m中的所有元素添加至HashMap中 - for (Map.Entry e : m.entrySet()) { - K key = e.getKey(); - V value = e.getValue(); - putVal(hash(key), key, value, false, evict); - } - } -} -``` -### put方法 -HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。 - -**对putVal方法添加元素的分析如下:** - -- ①如果定位到的数组位置没有元素 就直接插入。 -- ②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用`e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value)`将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。 - -ps:下图有一个小问题,来自 [issue#608](https://github.com/Snailclimb/JavaGuide/issues/608)指出:直接覆盖之后应该就会 return,不会有后续操作。参考 JDK8 HashMap.java 658 行。 - -![put方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/put方法.png) - -```java -public V put(K key, V value) { - return putVal(hash(key), key, value, false, true); -} - -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, - boolean evict) { - Node[] tab; Node p; int n, i; - // table未初始化或者长度为0,进行扩容 - if ((tab = table) == null || (n = tab.length) == 0) - n = (tab = resize()).length; - // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) - if ((p = tab[i = (n - 1) & hash]) == null) - tab[i] = newNode(hash, key, value, null); - // 桶中已经存在元素 - else { - Node e; K k; - // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 - if (p.hash == hash && - ((k = p.key) == key || (key != null && key.equals(k)))) - // 将第一个元素赋值给e,用e来记录 - e = p; - // hash值不相等,即key不相等;为红黑树结点 - else if (p instanceof TreeNode) - // 放入树中 - e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); - // 为链表结点 - else { - // 在链表最末插入结点 - for (int binCount = 0; ; ++binCount) { - // 到达链表的尾部 - if ((e = p.next) == null) { - // 在尾部插入新结点 - p.next = newNode(hash, key, value, null); - // 结点数量达到阈值,转化为红黑树 - if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st - treeifyBin(tab, hash); - // 跳出循环 - break; - } - // 判断链表中结点的key值与插入的元素的key值是否相等 - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - // 相等,跳出循环 - break; - // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 - p = e; - } - } - // 表示在桶中找到key值、hash值与插入元素相等的结点 - if (e != null) { - // 记录e的value - V oldValue = e.value; - // onlyIfAbsent为false或者旧值为null - if (!onlyIfAbsent || oldValue == null) - //用新值替换旧值 - e.value = value; - // 访问后回调 - afterNodeAccess(e); - // 返回旧值 - return oldValue; - } - } - // 结构性修改 - ++modCount; - // 实际大小大于阈值则扩容 - if (++size > threshold) - resize(); - // 插入后回调 - afterNodeInsertion(evict); - return null; -} -``` - -**我们再来对比一下 JDK1.7 put方法的代码** - -**对于put方法的分析如下:** - -- ①如果定位到的数组位置没有元素 就直接插入。 -- ②如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较,如果key相同就直接覆盖,不同就采用头插法插入元素。 - -```java -public V put(K key, V value) - if (table == EMPTY_TABLE) { - inflateTable(threshold); -} - if (key == null) - return putForNullKey(value); - int hash = hash(key); - int i = indexFor(hash, table.length); - for (Entry e = table[i]; e != null; e = e.next) { // 先遍历 - Object k; - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { - V oldValue = e.value; - e.value = value; - e.recordAccess(this); - return oldValue; - } - } - - modCount++; - addEntry(hash, key, value, i); // 再插入 - return null; -} -``` - - - -### get方法 -```java -public V get(Object key) { - Node e; - return (e = getNode(hash(key), key)) == null ? null : e.value; -} - -final Node getNode(int hash, Object key) { - Node[] tab; Node first, e; int n; K k; - if ((tab = table) != null && (n = tab.length) > 0 && - (first = tab[(n - 1) & hash]) != null) { - // 数组元素相等 - if (first.hash == hash && // always check first node - ((k = first.key) == key || (key != null && key.equals(k)))) - return first; - // 桶中不止一个节点 - if ((e = first.next) != null) { - // 在树中get - if (first instanceof TreeNode) - return ((TreeNode)first).getTreeNode(hash, key); - // 在链表中get - do { - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } while ((e = e.next) != null); - } - } - return null; -} -``` -### resize方法 -进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。 -```java -final Node[] resize() { - Node[] oldTab = table; - int oldCap = (oldTab == null) ? 0 : oldTab.length; - int oldThr = threshold; - int newCap, newThr = 0; - if (oldCap > 0) { - // 超过最大值就不再扩充了,就只好随你碰撞去吧 - if (oldCap >= MAXIMUM_CAPACITY) { - threshold = Integer.MAX_VALUE; - return oldTab; - } - // 没超过最大值,就扩充为原来的2倍 - else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) - newThr = oldThr << 1; // double threshold - } - else if (oldThr > 0) // initial capacity was placed in threshold - newCap = oldThr; - else { - // signifies using defaults - newCap = DEFAULT_INITIAL_CAPACITY; - newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); - } - // 计算新的resize上限 - if (newThr == 0) { - float ft = (float)newCap * loadFactor; - newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); - } - threshold = newThr; - @SuppressWarnings({"rawtypes","unchecked"}) - Node[] newTab = (Node[])new Node[newCap]; - table = newTab; - if (oldTab != null) { - // 把每个bucket都移动到新的buckets中 - for (int j = 0; j < oldCap; ++j) { - Node e; - if ((e = oldTab[j]) != null) { - oldTab[j] = null; - if (e.next == null) - newTab[e.hash & (newCap - 1)] = e; - else if (e instanceof TreeNode) - ((TreeNode)e).split(this, newTab, j, oldCap); - else { - Node loHead = null, loTail = null; - Node hiHead = null, hiTail = null; - Node next; - do { - next = e.next; - // 原索引 - if ((e.hash & oldCap) == 0) { - if (loTail == null) - loHead = e; - else - loTail.next = e; - loTail = e; - } - // 原索引+oldCap - else { - if (hiTail == null) - hiHead = e; - else - hiTail.next = e; - hiTail = e; - } - } while ((e = next) != null); - // 原索引放到bucket里 - if (loTail != null) { - loTail.next = null; - newTab[j] = loHead; - } - // 原索引+oldCap放到bucket里 - if (hiTail != null) { - hiTail.next = null; - newTab[j + oldCap] = hiHead; - } - } - } - } - } - return newTab; -} -``` -## HashMap常用方法测试 -```java -package map; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Set; - -public class HashMapDemo { - - public static void main(String[] args) { - HashMap map = new HashMap(); - // 键不能重复,值可以重复 - map.put("san", "张三"); - map.put("si", "李四"); - map.put("wu", "王五"); - map.put("wang", "老王"); - map.put("wang", "老王2");// 老王被覆盖 - map.put("lao", "老王"); - System.out.println("-------直接输出hashmap:-------"); - System.out.println(map); - /** - * 遍历HashMap - */ - // 1.获取Map中的所有键 - System.out.println("-------foreach获取Map中所有的键:------"); - Set keys = map.keySet(); - for (String key : keys) { - System.out.print(key+" "); - } - System.out.println();//换行 - // 2.获取Map中所有值 - System.out.println("-------foreach获取Map中所有的值:------"); - Collection values = map.values(); - for (String value : values) { - System.out.print(value+" "); - } - System.out.println();//换行 - // 3.得到key的值的同时得到key所对应的值 - System.out.println("-------得到key的值的同时得到key所对应的值:-------"); - Set keys2 = map.keySet(); - for (String key : keys2) { - System.out.print(key + ":" + map.get(key)+" "); - - } - /** - * 另外一种不常用的遍历方式 - */ - // 当我调用put(key,value)方法的时候,首先会把key和value封装到 - // Entry这个静态内部类对象中,把Entry对象再添加到数组中,所以我们想获取 - // map中的所有键值对,我们只要获取数组中的所有Entry对象,接下来 - // 调用Entry对象中的getKey()和getValue()方法就能获取键值对了 - Set> entrys = map.entrySet(); - for (java.util.Map.Entry entry : entrys) { - System.out.println(entry.getKey() + "--" + entry.getValue()); - } - - /** - * HashMap其他常用方法 - */ - System.out.println("after map.size():"+map.size()); - System.out.println("after map.isEmpty():"+map.isEmpty()); - System.out.println(map.remove("san")); - System.out.println("after map.remove():"+map); - System.out.println("after map.get(si):"+map.get("si")); - System.out.println("after map.containsKey(si):"+map.containsKey("si")); - System.out.println("after containsValue(李四):"+map.containsValue("李四")); - System.out.println(map.replace("si", "李四2")); - System.out.println("after map.replace(si, 李四2):"+map); - } - -} - -``` diff --git "a/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" "b/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" deleted file mode 100644 index c5280d5..0000000 --- "a/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" +++ /dev/null @@ -1,456 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [剖析面试最常见问题之Java集合框架](#剖析面试最常见问题之java集合框架) - - [说说List,Set,Map三者的区别?](#说说listsetmap三者的区别) - - [Arraylist 与 LinkedList 区别?](#arraylist-与-linkedlist-区别) - - [补充内容:RandomAccess接口](#补充内容randomaccess接口) - - [补充内容:双向链表和双向循环链表](#补充内容双向链表和双向循环链表) - - [ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?](#arraylist-与-vector-区别呢为什么要用arraylist取代vector呢) - - [说一说 ArrayList 的扩容机制吧](#说一说-arraylist-的扩容机制吧) - - [HashMap 和 Hashtable 的区别](#hashmap-和-hashtable-的区别) - - [HashMap 和 HashSet区别](#hashmap-和-hashset区别) - - [HashSet如何检查重复](#hashset如何检查重复) - - [HashMap的底层实现](#hashmap的底层实现) - - [JDK1.8之前](#jdk18之前) - - [JDK1.8之后](#jdk18之后) - - [HashMap 的长度为什么是2的幂次方](#hashmap-的长度为什么是2的幂次方) - - [HashMap 多线程操作导致死循环问题](#hashmap-多线程操作导致死循环问题) - - [ConcurrentHashMap 和 Hashtable 的区别](#concurrenthashmap-和-hashtable-的区别) - - [ConcurrentHashMap线程安全的具体实现方式/底层具体实现](#concurrenthashmap线程安全的具体实现方式底层具体实现) - - [JDK1.7(上面有示意图)](#jdk17上面有示意图) - - [JDK1.8 (上面有示意图)](#jdk18-上面有示意图) - - [comparable 和 Comparator的区别](#comparable-和-comparator的区别) - - [Comparator定制排序](#comparator定制排序) - - [重写compareTo方法实现按年龄来排序](#重写compareto方法实现按年龄来排序) - - [集合框架底层数据结构总结](#集合框架底层数据结构总结) - - [Collection](#collection) - - [1. List](#1-list) - - [2. Set](#2-set) - - [Map](#map) - - [如何选用集合?](#如何选用集合) - - - -# 剖析面试最常见问题之Java集合框架 - -## 说说List,Set,Map三者的区别? - -- **List(对付顺序的好帮手):** List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象 -- **Set(注重独一无二的性质):** 不允许重复的集合。不会有多个元素引用相同的对象。 -- **Map(用Key来搜索的专家):** 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。 - -## Arraylist 与 LinkedList 区别? - -- **1. 是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; - -- **2. 底层数据结构:** `Arraylist` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) - -- **3. 插入和删除是否受元素位置的影响:** ① **`ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e) `方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element) `)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **`LinkedList` 采用链表存储,所以对于`add(E e)`方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置`i`插入和删除元素的话(`(add(int index, E element)`) 时间复杂度近似为`o(n))`因为需要先移动到指定位置再插入。** - -- **4. 是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList` 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index) `方法)。 - -- **5. 内存空间占用:** ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。 - -### **补充内容:RandomAccess接口** - -```java -public interface RandomAccess { -} -``` - -查看源码我们发现实际上 `RandomAccess` 接口中什么都没有定义。所以,在我看来 `RandomAccess` 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。 - -在 `binarySearch(`)方法中,它要判断传入的list 是否 `RamdomAccess` 的实例,如果是,调用`indexedBinarySearch()`方法,如果不是,那么调用`iteratorBinarySearch()`方法 - -```java - public static - int binarySearch(List> list, T key) { - if (list instanceof RandomAccess || list.size() MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - if (loadFactor <= 0 || Float.isNaN(loadFactor)) - throw new IllegalArgumentException("Illegal load factor: " + - loadFactor); - this.loadFactor = loadFactor; - this.threshold = tableSizeFor(initialCapacity); - } - public HashMap(int initialCapacity) { - this(initialCapacity, DEFAULT_LOAD_FACTOR); - } -``` - -下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。 - -```java - /** - * Returns a power of two size for the given target capacity. - */ - static final int tableSizeFor(int cap) { - int n = cap - 1; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; - } -``` - -## HashMap 和 HashSet区别 - -如果你看过 `HashSet` 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 `clone() `、`writeObject()`、`readObject()`是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。 - -| HashMap | HashSet | -| :------------------------------: | :----------------------------------------------------------: | -| 实现了Map接口 | 实现Set接口 | -| 存储键值对 | 仅存储对象 | -| 调用 `put()`向map中添加元素 | 调用 `add()`方法向Set中添加元素 | -| HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性, | - -## HashSet如何检查重复 - -当你把对象加入`HashSet`时,HashSet会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用`equals()`方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。(摘自我的Java启蒙书《Head fist java》第二版) - -**hashCode()与equals()的相关规定:** - -1. 如果两个对象相等,则hashcode一定也是相同的 -2. 两个对象相等,对两个equals方法返回true -3. 两个对象有相同的hashcode值,它们也不一定是相等的 -4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖 -5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 - -**==与equals的区别** - -1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同 -2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较 -3. ==指引用是否相同 equals()指的是值是否相同 - -## HashMap的底层实现 - -### JDK1.8之前 - -JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。** - -**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。** - -**JDK 1.8 HashMap 的 hash 方法源码:** - -JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 - -```java - static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^ :按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } -``` - -对比一下 JDK1.7的 HashMap 的 hash 方法源码. - -```java -static int hash(int h) { - // This function ensures that hashCodes that differ only by - // constant multiples at each bit position have a bounded - // number of collisions (approximately 8 at default load factor). - - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); -} -``` - -相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 - -所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 - -![jdk1.8之前的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/jdk1.8之前的内部结构-HashMap.jpg) - -### JDK1.8之后 - -相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 - -![jdk1.8之后的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8之后的HashMap底层数据结构.jpg) - -> TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 - -**推荐阅读:** - -- 《Java 8系列之重新认识HashMap》 : - -## HashMap 的长度为什么是2的幂次方 - -为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash`”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。 - -**这个算法应该如何设计呢?** - -我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。** - -## HashMap 多线程操作导致死循环问题 - -主要原因在于 并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。 - -详情请查看: - -## ConcurrentHashMap 和 Hashtable 的区别 - -ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 - -- **底层数据结构:** JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; -- **实现线程安全的方式(重要):** ① **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 - -**两者的对比图:** - -图片来源: - -**HashTable:** - -![HashTable全表锁](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/HashTable全表锁.png) - -**JDK1.7的ConcurrentHashMap:** - -![JDK1.7的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ConcurrentHashMap分段锁.jpg) - -**JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):** - -![JDK1.8的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8-ConcurrentHashMap-Structure.jpg) - -## ConcurrentHashMap线程安全的具体实现方式/底层具体实现 - -### JDK1.7(上面有示意图) - -首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 - -**ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成**。 - -Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。 - -```java -static class Segment extends ReentrantLock implements Serializable { -} -``` - -一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 - -### JDK1.8 (上面有示意图) - -ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N))) - -synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。 - -## comparable 和 Comparator的区别 - -- comparable接口实际上是出自java.lang包 它有一个 `compareTo(Object obj)`方法用来排序 -- comparator接口实际上是出自 java.util 包它有一个`compare(Object obj1, Object obj2)`方法用来排序 - -一般我们需要对一个集合使用自定义排序时,我们就要重写`compareTo()`方法或`compare()`方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写`compareTo()`方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 `Collections.sort()`. - -### Comparator定制排序 - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - System.out.println("原始数组:"); - System.out.println(arrayList); - // void reverse(List list):反转 - Collections.reverse(arrayList); - System.out.println("Collections.reverse(arrayList):"); - System.out.println(arrayList); - - // void sort(List list),按自然排序的升序排序 - Collections.sort(arrayList); - System.out.println("Collections.sort(arrayList):"); - System.out.println(arrayList); - // 定制排序的用法 - Collections.sort(arrayList, new Comparator() { - - @Override - public int compare(Integer o1, Integer o2) { - return o2.compareTo(o1); - } - }); - System.out.println("定制排序后:"); - System.out.println(arrayList); -``` - -Output: - -``` -原始数组: -[-1, 3, 3, -5, 7, 4, -9, -7] -Collections.reverse(arrayList): -[-7, -9, 4, 7, -5, 3, 3, -1] -Collections.sort(arrayList): -[-9, -7, -5, -1, 3, 3, 4, 7] -定制排序后: -[7, 4, 3, 3, -1, -5, -7, -9] -``` - -### 重写compareTo方法实现按年龄来排序 - -```java -// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列 -// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他 -// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了 - -public class Person implements Comparable { - private String name; - private int age; - - public Person(String name, int age) { - super(); - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - /** - * TODO重写compareTo方法实现按年龄来排序 - */ - @Override - public int compareTo(Person o) { - // TODO Auto-generated method stub - if (this.age > o.getAge()) { - return 1; - } else if (this.age < o.getAge()) { - return -1; - } - return age; - } -} - -``` - -```java - public static void main(String[] args) { - TreeMap pdata = new TreeMap(); - pdata.put(new Person("张三", 30), "zhangsan"); - pdata.put(new Person("李四", 20), "lisi"); - pdata.put(new Person("王五", 10), "wangwu"); - pdata.put(new Person("小红", 5), "xiaohong"); - // 得到key的值的同时得到key所对应的值 - Set keys = pdata.keySet(); - for (Person key : keys) { - System.out.println(key.getAge() + "-" + key.getName()); - - } - } -``` - -Output: - -``` -5-小红 -10-王五 -20-李四 -30-张三 -``` - -## 集合框架底层数据结构总结 - -### Collection - -#### 1. List - -- **Arraylist:** Object数组 -- **Vector:** Object数组 -- **LinkedList:** 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环) - -#### 2. Set - -- **HashSet(无序,唯一):** 基于 HashMap 实现的,底层采用 HashMap 来保存元素 -- **LinkedHashSet:** LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的 -- **TreeSet(有序,唯一):** 红黑树(自平衡的排序二叉树) - -### Map - -- **HashMap:** JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间 -- **LinkedHashMap:** LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931) -- **Hashtable:** 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 -- **TreeMap:** 红黑树(自平衡的排序二叉树) - -## 如何选用集合? - -主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/java/collection/LinkedList.md b/docs/java/collection/LinkedList.md deleted file mode 100644 index d26bc75..0000000 --- a/docs/java/collection/LinkedList.md +++ /dev/null @@ -1,515 +0,0 @@ - - - -- [简介](#简介) -- [内部结构分析](#内部结构分析) -- [LinkedList源码分析](#linkedlist源码分析) - - [构造方法](#构造方法) - - [添加(add)方法](#add方法) - - [根据位置取数据的方法](#根据位置取数据的方法) - - [根据对象得到索引的方法](#根据对象得到索引的方法) - - [检查链表是否包含某对象的方法:](#检查链表是否包含某对象的方法:) - - [删除(remove/pop)方法](#删除方法) -- [LinkedList类常用方法测试:](#linkedlist类常用方法测试) - - - -## 简介 -LinkedList是一个实现了List接口Deque接口双端链表。 -LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; -LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法: -```java -List list=Collections.synchronizedList(new LinkedList(...)); -``` -## 内部结构分析 -**如下图所示:** -![LinkedList内部结构](https://user-gold-cdn.xitu.io/2018/3/19/1623e363fe0450b0?w=600&h=481&f=jpeg&s=18502) -看完了图之后,我们再看LinkedList类中的一个**内部私有类Node**就很好理解了: -```java -private static class Node { - E item;//节点值 - Node next;//后继节点 - Node prev;//前驱节点 - - Node(Node prev, E element, Node next) { - this.item = element; - this.next = next; - this.prev = prev; - } - } -``` -这个类就代表双端链表的节点Node。这个类有三个属性,分别是前驱节点,本节点的值,后继结点。 - -## LinkedList源码分析 -### 构造方法 -**空构造方法:** -```java - public LinkedList() { - } -``` -**用已有的集合创建链表的构造方法:** -```java - public LinkedList(Collection c) { - this(); - addAll(c); - } -``` -### add方法 -**add(E e)** 方法:将元素添加到链表尾部 -```java -public boolean add(E e) { - linkLast(e);//这里就只调用了这一个方法 - return true; - } -``` - -```java - /** - * 链接使e作为最后一个元素。 - */ - void linkLast(E e) { - final Node l = last; - final Node newNode = new Node<>(l, e, null); - last = newNode;//新建节点 - if (l == null) - first = newNode; - else - l.next = newNode;//指向后继元素也就是指向下一个元素 - size++; - modCount++; - } -``` -**add(int index,E e)**:在指定位置添加元素 -```java -public void add(int index, E element) { - checkPositionIndex(index); //检查索引是否处于[0-size]之间 - - if (index == size)//添加在链表尾部 - linkLast(element); - else//添加在链表中间 - linkBefore(element, node(index)); - } -``` -linkBefore方法需要给定两个参数,一个插入节点的值,一个指定的node,所以我们又调用了Node(index)去找到index对应的node - -**addAll(Collection c ):将集合插入到链表尾部** - -```java -public boolean addAll(Collection c) { - return addAll(size, c); - } -``` -**addAll(int index, Collection c):** 将集合从指定位置开始插入 -```java -public boolean addAll(int index, Collection c) { - //1:检查index范围是否在size之内 - checkPositionIndex(index); - - //2:toArray()方法把集合的数据存到对象数组中 - Object[] a = c.toArray(); - int numNew = a.length; - if (numNew == 0) - return false; - - //3:得到插入位置的前驱节点和后继节点 - Node pred, succ; - //如果插入位置为尾部,前驱节点为last,后继节点为null - if (index == size) { - succ = null; - pred = last; - } - //否则,调用node()方法得到后继节点,再得到前驱节点 - else { - succ = node(index); - pred = succ.prev; - } - - // 4:遍历数据将数据插入 - for (Object o : a) { - @SuppressWarnings("unchecked") E e = (E) o; - //创建新节点 - Node newNode = new Node<>(pred, e, null); - //如果插入位置在链表头部 - if (pred == null) - first = newNode; - else - pred.next = newNode; - pred = newNode; - } - - //如果插入位置在尾部,重置last节点 - if (succ == null) { - last = pred; - } - //否则,将插入的链表与先前链表连接起来 - else { - pred.next = succ; - succ.prev = pred; - } - - size += numNew; - modCount++; - return true; - } -``` -上面可以看出addAll方法通常包括下面四个步骤: -1. 检查index范围是否在size之内 -2. toArray()方法把集合的数据存到对象数组中 -3. 得到插入位置的前驱和后继节点 -4. 遍历数据,将数据插入到指定位置 - -**addFirst(E e):** 将元素添加到链表头部 -```java - public void addFirst(E e) { - linkFirst(e); - } -``` -```java -private void linkFirst(E e) { - final Node f = first; - final Node newNode = new Node<>(null, e, f);//新建节点,以头节点为后继节点 - first = newNode; - //如果链表为空,last节点也指向该节点 - if (f == null) - last = newNode; - //否则,将头节点的前驱指针指向新节点,也就是指向前一个元素 - else - f.prev = newNode; - size++; - modCount++; - } -``` -**addLast(E e):** 将元素添加到链表尾部,与 **add(E e)** 方法一样 -```java -public void addLast(E e) { - linkLast(e); - } -``` -### 根据位置取数据的方法 -**get(int index):** 根据指定索引返回数据 -```java -public E get(int index) { - //检查index范围是否在size之内 - checkElementIndex(index); - //调用Node(index)去找到index对应的node然后返回它的值 - return node(index).item; - } -``` -**获取头节点(index=0)数据方法:** -```java -public E getFirst() { - final Node f = first; - if (f == null) - throw new NoSuchElementException(); - return f.item; - } -public E element() { - return getFirst(); - } -public E peek() { - final Node f = first; - return (f == null) ? null : f.item; - } - -public E peekFirst() { - final Node f = first; - return (f == null) ? null : f.item; - } -``` -**区别:** -getFirst(),element(),peek(),peekFirst() -这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中**getFirst()** 和**element()** 方法将会在链表为空时,抛出异常 - -element()方法的内部就是使用getFirst()实现的。它们会在链表为空时,抛出NoSuchElementException -**获取尾节点(index=-1)数据方法:** -```java - public E getLast() { - final Node l = last; - if (l == null) - throw new NoSuchElementException(); - return l.item; - } - public E peekLast() { - final Node l = last; - return (l == null) ? null : l.item; - } -``` -**两者区别:** -**getLast()** 方法在链表为空时,会抛出**NoSuchElementException**,而**peekLast()** 则不会,只是会返回 **null**。 -### 根据对象得到索引的方法 -**int indexOf(Object o):** 从头遍历找 -```java -public int indexOf(Object o) { - int index = 0; - if (o == null) { - //从头遍历 - for (Node x = first; x != null; x = x.next) { - if (x.item == null) - return index; - index++; - } - } else { - //从头遍历 - for (Node x = first; x != null; x = x.next) { - if (o.equals(x.item)) - return index; - index++; - } - } - return -1; - } -``` -**int lastIndexOf(Object o):** 从尾遍历找 -```java -public int lastIndexOf(Object o) { - int index = size; - if (o == null) { - //从尾遍历 - for (Node x = last; x != null; x = x.prev) { - index--; - if (x.item == null) - return index; - } - } else { - //从尾遍历 - for (Node x = last; x != null; x = x.prev) { - index--; - if (o.equals(x.item)) - return index; - } - } - return -1; - } -``` -### 检查链表是否包含某对象的方法: -**contains(Object o):** 检查对象o是否存在于链表中 -```java - public boolean contains(Object o) { - return indexOf(o) != -1; - } -``` -### 删除方法 -**remove()** ,**removeFirst(),pop():** 删除头节点 -``` -public E pop() { - return removeFirst(); - } -public E remove() { - return removeFirst(); - } -public E removeFirst() { - final Node f = first; - if (f == null) - throw new NoSuchElementException(); - return unlinkFirst(f); - } -``` -**removeLast(),pollLast():** 删除尾节点 -```java -public E removeLast() { - final Node l = last; - if (l == null) - throw new NoSuchElementException(); - return unlinkLast(l); - } -public E pollLast() { - final Node l = last; - return (l == null) ? null : unlinkLast(l); - } -``` -**区别:** removeLast()在链表为空时将抛出NoSuchElementException,而pollLast()方法返回null。 - -**remove(Object o):** 删除指定元素 -```java -public boolean remove(Object o) { - //如果删除对象为null - if (o == null) { - //从头开始遍历 - for (Node x = first; x != null; x = x.next) { - //找到元素 - if (x.item == null) { - //从链表中移除找到的元素 - unlink(x); - return true; - } - } - } else { - //从头开始遍历 - for (Node x = first; x != null; x = x.next) { - //找到元素 - if (o.equals(x.item)) { - //从链表中移除找到的元素 - unlink(x); - return true; - } - } - } - return false; - } -``` -当删除指定对象时,只需调用remove(Object o)即可,不过该方法一次只会删除一个匹配的对象,如果删除了匹配对象,返回true,否则false。 - -unlink(Node x) 方法: -```java -E unlink(Node x) { - // assert x != null; - final E element = x.item; - final Node next = x.next;//得到后继节点 - final Node prev = x.prev;//得到前驱节点 - - //删除前驱指针 - if (prev == null) { - first = next;//如果删除的节点是头节点,令头节点指向该节点的后继节点 - } else { - prev.next = next;//将前驱节点的后继节点指向后继节点 - x.prev = null; - } - - //删除后继指针 - if (next == null) { - last = prev;//如果删除的节点是尾节点,令尾节点指向该节点的前驱节点 - } else { - next.prev = prev; - x.next = null; - } - - x.item = null; - size--; - modCount++; - return element; - } -``` -**remove(int index)**:删除指定位置的元素 -```java -public E remove(int index) { - //检查index范围 - checkElementIndex(index); - //将节点删除 - return unlink(node(index)); - } -``` -## LinkedList类常用方法测试 - -```java -package list; - -import java.util.Iterator; -import java.util.LinkedList; - -public class LinkedListDemo { - public static void main(String[] srgs) { - //创建存放int类型的linkedList - LinkedList linkedList = new LinkedList<>(); - /************************** linkedList的基本操作 ************************/ - linkedList.addFirst(0); // 添加元素到列表开头 - linkedList.add(1); // 在列表结尾添加元素 - linkedList.add(2, 2); // 在指定位置添加元素 - linkedList.addLast(3); // 添加元素到列表结尾 - - System.out.println("LinkedList(直接输出的): " + linkedList); - - System.out.println("getFirst()获得第一个元素: " + linkedList.getFirst()); // 返回此列表的第一个元素 - System.out.println("getLast()获得第最后一个元素: " + linkedList.getLast()); // 返回此列表的最后一个元素 - System.out.println("removeFirst()删除第一个元素并返回: " + linkedList.removeFirst()); // 移除并返回此列表的第一个元素 - System.out.println("removeLast()删除最后一个元素并返回: " + linkedList.removeLast()); // 移除并返回此列表的最后一个元素 - System.out.println("After remove:" + linkedList); - System.out.println("contains()方法判断列表是否包含1这个元素:" + linkedList.contains(1)); // 判断此列表包含指定元素,如果是,则返回true - System.out.println("该linkedList的大小 : " + linkedList.size()); // 返回此列表的元素个数 - - /************************** 位置访问操作 ************************/ - System.out.println("-----------------------------------------"); - linkedList.set(1, 3); // 将此列表中指定位置的元素替换为指定的元素 - System.out.println("After set(1, 3):" + linkedList); - System.out.println("get(1)获得指定位置(这里为1)的元素: " + linkedList.get(1)); // 返回此列表中指定位置处的元素 - - /************************** Search操作 ************************/ - System.out.println("-----------------------------------------"); - linkedList.add(3); - System.out.println("indexOf(3): " + linkedList.indexOf(3)); // 返回此列表中首次出现的指定元素的索引 - System.out.println("lastIndexOf(3): " + linkedList.lastIndexOf(3));// 返回此列表中最后出现的指定元素的索引 - - /************************** Queue操作 ************************/ - System.out.println("-----------------------------------------"); - System.out.println("peek(): " + linkedList.peek()); // 获取但不移除此列表的头 - System.out.println("element(): " + linkedList.element()); // 获取但不移除此列表的头 - linkedList.poll(); // 获取并移除此列表的头 - System.out.println("After poll():" + linkedList); - linkedList.remove(); - System.out.println("After remove():" + linkedList); // 获取并移除此列表的头 - linkedList.offer(4); - System.out.println("After offer(4):" + linkedList); // 将指定元素添加到此列表的末尾 - - /************************** Deque操作 ************************/ - System.out.println("-----------------------------------------"); - linkedList.offerFirst(2); // 在此列表的开头插入指定的元素 - System.out.println("After offerFirst(2):" + linkedList); - linkedList.offerLast(5); // 在此列表末尾插入指定的元素 - System.out.println("After offerLast(5):" + linkedList); - System.out.println("peekFirst(): " + linkedList.peekFirst()); // 获取但不移除此列表的第一个元素 - System.out.println("peekLast(): " + linkedList.peekLast()); // 获取但不移除此列表的第一个元素 - linkedList.pollFirst(); // 获取并移除此列表的第一个元素 - System.out.println("After pollFirst():" + linkedList); - linkedList.pollLast(); // 获取并移除此列表的最后一个元素 - System.out.println("After pollLast():" + linkedList); - linkedList.push(2); // 将元素推入此列表所表示的堆栈(插入到列表的头) - System.out.println("After push(2):" + linkedList); - linkedList.pop(); // 从此列表所表示的堆栈处弹出一个元素(获取并移除列表第一个元素) - System.out.println("After pop():" + linkedList); - linkedList.add(3); - linkedList.removeFirstOccurrence(3); // 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表) - System.out.println("After removeFirstOccurrence(3):" + linkedList); - linkedList.removeLastOccurrence(3); // 从此列表中移除最后一次出现的指定元素(从尾部到头部遍历列表) - System.out.println("After removeFirstOccurrence(3):" + linkedList); - - /************************** 遍历操作 ************************/ - System.out.println("-----------------------------------------"); - linkedList.clear(); - for (int i = 0; i < 100000; i++) { - linkedList.add(i); - } - // 迭代器遍历 - long start = System.currentTimeMillis(); - Iterator iterator = linkedList.iterator(); - while (iterator.hasNext()) { - iterator.next(); - } - long end = System.currentTimeMillis(); - System.out.println("Iterator:" + (end - start) + " ms"); - - // 顺序遍历(随机遍历) - start = System.currentTimeMillis(); - for (int i = 0; i < linkedList.size(); i++) { - linkedList.get(i); - } - end = System.currentTimeMillis(); - System.out.println("for:" + (end - start) + " ms"); - - // 另一种for循环遍历 - start = System.currentTimeMillis(); - for (Integer i : linkedList) - ; - end = System.currentTimeMillis(); - System.out.println("for2:" + (end - start) + " ms"); - - // 通过pollFirst()或pollLast()来遍历LinkedList - LinkedList temp1 = new LinkedList<>(); - temp1.addAll(linkedList); - start = System.currentTimeMillis(); - while (temp1.size() != 0) { - temp1.pollFirst(); - } - end = System.currentTimeMillis(); - System.out.println("pollFirst()或pollLast():" + (end - start) + " ms"); - - // 通过removeFirst()或removeLast()来遍历LinkedList - LinkedList temp2 = new LinkedList<>(); - temp2.addAll(linkedList); - start = System.currentTimeMillis(); - while (temp2.size() != 0) { - temp2.removeFirst(); - } - end = System.currentTimeMillis(); - System.out.println("removeFirst()或removeLast():" + (end - start) + " ms"); - } -} -``` diff --git "a/docs/java/\345\244\232\347\272\277\347\250\213\347\263\273\345\210\227.md" "b/docs/java/\345\244\232\347\272\277\347\250\213\347\263\273\345\210\227.md" deleted file mode 100644 index 6ed7bda..0000000 --- "a/docs/java/\345\244\232\347\272\277\347\250\213\347\263\273\345\210\227.md" +++ /dev/null @@ -1,69 +0,0 @@ -> ## 多线程系列文章 -下列文章,我都更新在了我的博客专栏:[Java并发编程指南](https://blog.csdn.net/column/details/20860.html)。 - -1. [Java多线程学习(一)Java多线程入门](http://blog.csdn.net/qq_34337272/article/details/79640870) -2. [Java多线程学习(二)synchronized关键字(1)](http://blog.csdn.net/qq_34337272/article/details/79655194) -3. [Java多线程学习(二)synchronized关键字(2)](http://blog.csdn.net/qq_34337272/article/details/79670775) -4. [Java多线程学习(三)volatile关键字](http://blog.csdn.net/qq_34337272/article/details/79680771) -5. [Java多线程学习(四)等待/通知(wait/notify)机制](http://blog.csdn.net/qq_34337272/article/details/79690279) - -6. [Java多线程学习(五)线程间通信知识点补充](http://blog.csdn.net/qq_34337272/article/details/79694226) -7. [Java多线程学习(六)Lock锁的使用](http://blog.csdn.net/qq_34337272/article/details/79714196) -8. [Java多线程学习(七)并发编程中一些问题](https://blog.csdn.net/qq_34337272/article/details/79844051) -9. [Java多线程学习(八)线程池与Executor 框架](https://blog.csdn.net/qq_34337272/article/details/79959271) - - -> ## 多线程系列文章重要知识点与思维导图 - -### Java多线程学习(一)Java多线程入门 - -![](https://user-gold-cdn.xitu.io/2018/8/4/16504e0cb6bac32e?w=758&h=772&f=jpeg&s=247210) - -### Java多线程学习(二)synchronized关键字(1) -![](https://user-gold-cdn.xitu.io/2018/8/4/16504e245ceb3ea9?w=1028&h=490&f=jpeg&s=203811) - -注意:**可重入锁的概念**。 - - 另外要注意:**synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁。** 如果多个线程访问的是同一个对象,哪个线程先执行带synchronized关键字的方法,则哪个线程就持有该方法,那么其他线程只能呈等待状态。如果多个线程访问的是多个对象则不一定,因为多个对象会产生多个锁。 - -### Java多线程学习(二)synchronized关键字(2) - -![思维导图](https://user-gold-cdn.xitu.io/2018/8/4/16504e3d98213324?w=1448&h=439&f=jpeg&s=245012) - - **注意:** - - - 其他线程执行对象中**synchronized同步方法**(上一节我们介绍过,需要回顾的可以看上一节的文章)和**synchronized(this)代码块**时呈现同步效果; - - **如果两个线程使用了同一个“对象监视器”(synchronized(object)),运行结果同步,否则不同步**. - - **synchronized关键字加到static静态方法**和**synchronized(class)代码块**上都是是给**Class类**上锁,而**synchronized关键字加到非static静态方法**上是给**对象**上锁。 - - 数据类型String的常量池属性:**在Jvm中具有String常量池缓存的功能** - -### Java多线程学习(三)volatile关键字 - -![volatile关键字](https://user-gold-cdn.xitu.io/2018/8/4/16504e4ab69d8d58) - **注意:** - - **synchronized关键字**和**volatile关键字**比较 - -### Java多线程学习(四)等待/通知(wait/notify)机制 - -![本节思维导图](https://user-gold-cdn.xitu.io/2018/3/25/1625d2a9188ec021?w=1254&h=452&f=jpeg&s=229471) - -### Java多线程学习(五)线程间通信知识点补充 - -![本节思维导图](https://user-gold-cdn.xitu.io/2018/8/4/16504e618d6886c5?w=1146&h=427&f=jpeg&s=220573) - **注意:** ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。 - -### Java多线程学习(六)Lock锁的使用 - - ![本节思维导图](https://user-gold-cdn.xitu.io/2018/3/27/1626755a8e9a8774?w=1197&h=571&f=jpeg&s=258439) - -### Java多线程学习(七)并发编程中一些问题 - -![思维导图](https://user-gold-cdn.xitu.io/2018/4/7/162a01b71ebc4842?w=1067&h=517&f=png&s=36857) - -### Java多线程学习(八)线程池与Executor 框架 - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-14/86510659.jpg) - diff --git a/docs/system-design/authority-certification/JWT-advantages-and-disadvantages.md b/docs/system-design/authority-certification/JWT-advantages-and-disadvantages.md deleted file mode 100644 index 4d16c41..0000000 --- a/docs/system-design/authority-certification/JWT-advantages-and-disadvantages.md +++ /dev/null @@ -1,93 +0,0 @@ -# JWT 身份认证优缺点分析以及常见问题解决方案 - -之前分享了一个使用 Spring Security 实现 JWT 身份认证的 Demo,文章地址:[适合初学者入门 Spring Security With JWT 的 Demo](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485622&idx=1&sn=e9750ed63c47457ba1896db8dfceac6a&chksm=cea2477df9d5ce6b7af20e582c6c60b7408a6459b05b849394c45f04664d1651510bdee029f7&token=684071313&lang=zh_CN&scene=21#wechat_redirect)。 Demo 非常简单,没有介绍到 JWT 存在的一些问题。所以,单独抽了一篇文章出来介绍。为了完成这篇文章,我查阅了很多资料和文献,我觉得应该对大家有帮助。 - -相关阅读: - -- [《一问带你区分清楚Authentication,Authorization以及Cookie、Session、Token》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485626&idx=1&sn=3247aa9000693dd692de8a04ccffeec1&chksm=cea24771f9d5ce675ea0203633a95b68bfe412dc6a9d05f22d221161147b76161d1b470d54b3&token=684071313&lang=zh_CN&scene=21#wechat_redirect) -- [适合初学者入门 Spring Security With JWT 的 Demo](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485622&idx=1&sn=e9750ed63c47457ba1896db8dfceac6a&chksm=cea2477df9d5ce6b7af20e582c6c60b7408a6459b05b849394c45f04664d1651510bdee029f7&token=684071313&lang=zh_CN&scene=21#wechat_redirect) -- [Spring Boot 使用 JWT 进行身份和权限验证](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485640&idx=1&sn=0ff147808318d53b371f16bb730c96ef&chksm=cea24703f9d5ce156ba67662f6f3f482330e8e6ebd9d44c61bf623083e9b941d8a180db6b0ea&token=1533246333&lang=zh_CN#rd) - -## Token 认证的优势 - - 相比于 Session 认证的方式来说,使用 token 进行身份认证主要有下面三个优势: - -### 1.无状态 - -token 自身包含了身份验证所需要的所有信息,使得我们的服务器不需要存储 Session 信息,这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。但是,也正是由于 token 的无状态,也导致了它最大的缺点:当后端在token 有效期内废弃一个 token 或者更改它的权限的话,不会立即生效,一般需要等到有效期过后才可以。另外,当用户 Logout 的话,token 也还有效。除非,我们在后端增加额外的处理逻辑。 - -### 2.有效避免了CSRF 攻击 - -**CSRF(Cross Site Request Forgery)**一般被翻译为 **跨站请求伪造**,属于网络攻击领域范围。相比于 SQL 脚本注入、XSS等等安全攻击方式,CSRF 的知名度并没有它们高。但是,它的确是每个系统都要考虑的安全隐患,就连技术帝国 Google 的 Gmail 在早些年也被曝出过存在 CSRF 漏洞,这给 Gmail 的用户造成了很大的损失。 - -那么究竟什么是 **跨站请求伪造** 呢?说简单用你的身份去发送一些对你不友好的请求。举个简单的例子: - -小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了10000元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。 - -```html -科学理财,年盈利率过万 -``` - -导致这个问题很大的原因就是: Session 认证中 Cookie 中的 session_id 是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。 - -**那为什么 token 不会存在这种问题呢?** - -我是这样理解的:一般情况下我们使用 JWT 的话,在我们登录成功获得 token 之后,一般会选择存放在 local storage 中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 token,这样就不会出现 CSRF 漏洞的问题。因为,即使有个你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 token 的,所以这个请求将是非法的。 - -但是这样会存在 XSS 攻击中被盗的风险,为了避免 XSS 攻击,你可以选择将 token 存储在标记为`httpOnly` 的cookie 中。但是,这样又导致了你必须自己提供CSRF保护。 - -具体采用上面哪两种方式存储 token 呢,大部分情况下存放在 local storage 下都是最好的选择,某些情况下可能需要存放在标记为`httpOnly` 的cookie 中会更好。 - -### 3.适合移动端应用 - -使用 Session 进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到 Cookie(需要 Cookie 保存 SessionId),所以不适合移动端。 - -但是,使用 token 进行身份认证就不会存在这种问题,因为只要 token 可以被客户端存储就能够使用,而且 token 还可以跨语言使用。 - -### 4.单点登录友好 - -使用 Session 进行身份认证的话,实现单点登录,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 token 进行认证的话, token 被保存在客户端,不会存在这些问题。 - -## Token 认证常见问题以及解决办法 - -### 1.注销登录等场景下 token 还有效 - -与之类似的具体相关场景有: - -1. 退出登录; -2. 修改密码; -3. 服务端修改了某个用户具有的权限或者角色; -4. 用户的帐户被删除/暂停。 -5. 用户由管理员注销; - -这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。但是,使用 token 认证的方式就不好解决了。我们也说过了,token 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。那么,我们如何解决这个问题呢?查阅了很多资料,总结了下面几种方案: - -- **将 token 存入内存数据库**:将 token 存入 DB 中,redis 内存数据库在这里是是不错的选择。如果需要让某个 token 失效就直接从 redis 中删除这个 token 即可。但是,这样会导致每次使用 token 发送请求都要先从 DB 中查询 token 是否存在的步骤,而且违背了 JWT 的无状态原则。 -- **黑名单机制**:和上面的方式类似,使用内存数据库比如 redis 维护一个黑名单,如果想让某个 token 失效的话就直接将这个 token 加入到 **黑名单** 即可。然后,每次使用 token 进行请求的话都会先判断这个 token 是否存在于黑名单中。 -- **修改密钥 (Secret)** : 我们为每个用户都创建一个专属密钥,如果我们想让某个 token 失效,我们直接修改对应用户的密钥即可。但是,这样相比于前两种引入内存数据库带来了危害更大,比如:1⃣️如果服务是分布式的,则每次发出新的 token 时都必须在多台机器同步密钥。为此,你需要将必须将机密存储在数据库或其他外部服务中,这样和 Session 认证就没太大区别了。2⃣️如果用户同时在两个浏览器打开系统,或者在手机端也打开了系统,如果它从一个地方将账号退出,那么其他地方都要重新进行登录,这是不可取的。 -- **保持令牌的有效期限短并经常轮换** :很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。 - -对于修改密码后 token 还有效问题的解决还是比较容易的,说一种我觉得比较好的方式:**使用用户的密码的哈希值对 token 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。** - -### 2.token 的续签问题 - -token 有效期一般都建议设置的不太长,那么 token 过期后如何认证,如何实现动态刷新 token,避免用户经常需要重新登录? - -我们先来看看在 Session 认证中一般的做法:**假如 session 的有效期30分钟,如果 30 分钟内用户有访问,就把 session 有效期被延长30分钟。** - -1. **类似于 Session 认证中的做法**:这种方案满足于大部分场景。假设服务端给的 token 有效期设置为30分钟,服务端每次进行校验时,如果发现 token 的有效期马上快过期了,服务端就重新生成 token 给客户端。客户端每次请求都检查新旧token,如果不一致,则更新本地的token。这种做法的问题是仅仅在快过期的时候请求才会更新 token ,对客户端不是很友好。 -2. **每次请求都返回新 token** :这种方案的的思路很简单,但是,很明显,开销会比较大。 -3. **token 有效期设置到半夜** :这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。 -4. **用户登录返回两个 token** :第一个是 acessToken ,它的过期时间 token 本身的过期时间比如半个小时,另外一个是 refreshToken 它的过期时间更长一点比如为1天。客户端登录后,将 accessToken和refreshToken 保存在本地,每次访问将 accessToken 传给服务端。服务端校验 accessToken 的有效性,如果过期的话,就将 refreshToken 传给服务端。如果有效,服务端就生成新的 accessToken 给客户端。否则,客户端就重新登录即可。该方案的不足是:1⃣️需要客户端来配合;2⃣️用户注销的时候需要同时保证两个 token 都无效;3⃣️重新请求获取 token 的过程中会有短暂 token 不可用的情况(可以通过在客户端设置定时器,当accessToken 快过期的时候,提前去通过 refreshToken 获取新的accessToken)。 - -## 总结 - -JWT 最适合的场景是不需要服务端保存用户状态的场景,比如如果考虑到 token 注销和 token 续签的场景话,没有特别好的解决方案,大部分解决方案都给 token 加上了状态,这就有点类似 Session 认证了。 - -## Reference - -- [JWT 超详细分析](https://learnku.com/articles/17883?order_by=vote_count&) -- https://medium.com/devgorilla/how-to-log-out-when-using-jwt-a8c7823e8a6 -- https://medium.com/@agungsantoso/csrf-protection-with-json-web-tokens-83e0f2fcbcc -- [Invalidating JSON Web Tokens](https://stackoverflow.com/questions/21978658/invalidating-json-web-tokens) - diff --git a/docs/system-design/authority-certification/basis-of-authority-certification.md b/docs/system-design/authority-certification/basis-of-authority-certification.md deleted file mode 100644 index 896353f..0000000 --- a/docs/system-design/authority-certification/basis-of-authority-certification.md +++ /dev/null @@ -1,154 +0,0 @@ -## 1. 认证 (Authentication) 和授权 (Authorization)的区别是什么? - -这是一个绝大多数人都会混淆的问题。首先先从读音上来认识这两个名词,很多人都会把它俩的读音搞混,所以我建议你先先去查一查这两个单词到底该怎么读,他们的具体含义是什么。 - -说简单点就是: - -- **认证 (Authentication):** 你是谁。 -- **授权 (Authorization):** 你有权限干什么。 - -稍微正式点(啰嗦点)的说法就是: - -- **Authentication(认证)** 是验证您的身份的凭据(例如用户名/用户ID和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。 -- **Authorization(授权)** 发生在 **Authentication(认证)** 之后。授权嘛,光看意思大家应该就明白,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。 - -这两个一般在我们的系统中被结合在一起使用,目的就是为了保护我们系统的安全性。 - -## 2. 什么是Cookie ? Cookie的作用是什么?如何在服务端使用 Cookie ? - -### 2.1 什么是Cookie ? Cookie的作用是什么? - -Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。 - -维基百科是这样定义 Cookie 的:Cookies是某些网站为了辨别用户身份而储存在用户本地终端上的数据(通常经过加密)。简单来说: **Cookie 存放在客户端,一般用来保存用户信息**。 - -下面是 Cookie 的一些应用案例: - -1. 我们在 Cookie 中保存已经登录过的用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了。除此之外,Cookie 还能保存用户首选项,主题和其他设置信息。 -2. 使用Cookie 保存 session 或者 token ,向后端发送请求的时候带上 Cookie,这样后端就能取到session或者token了。这样就能记录用户当前的状态了,因为 HTTP 协议是无状态的。 -3. Cookie 还可以用来记录和分析用户行为。举个简单的例子你在网上购物的时候,因为HTTP协议是没有状态的,如果服务器想要获取你在某个页面的停留状态或者看了哪些商品,一种常用的实现方式就是将这些信息存放在Cookie - -### 2.2 如何能在 服务端使用 Cookie 呢? - -这部分内容参考:https://attacomsian.com/blog/cookies-spring-boot,更多如何在Spring Boot中使用Cookie 的内容可以查看这篇文章。 - -**1)设置cookie返回给客户端** - -```java -@GetMapping("/change-username") -public String setCookie(HttpServletResponse response) { - // 创建一个 cookie - Cookie cookie = new Cookie("username", "Jovan"); - //设置 cookie过期时间 - cookie.setMaxAge(7 * 24 * 60 * 60); // expires in 7 days - //添加到 response 中 - response.addCookie(cookie); - - return "Username is changed!"; -} -``` - -**2) 使用Spring框架提供的`@CookieValue`注解获取特定的 cookie的值** - -```java -@GetMapping("/") -public String readCookie(@CookieValue(value = "username", defaultValue = "Atta") String username) { - return "Hey! My username is " + username; -} -``` - -**3) 读取所有的 Cookie 值** - -```java -@GetMapping("/all-cookies") -public String readAllCookies(HttpServletRequest request) { - - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - return Arrays.stream(cookies) - .map(c -> c.getName() + "=" + c.getValue()).collect(Collectors.joining(", ")); - } - - return "No cookies"; -} -``` - -## 3. Cookie 和 Session 有什么区别?如何使用Session进行身份验证? - -**Session 的主要作用就是通过服务端记录用户的状态。** 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。 - -**Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。** - -**那么,如何使用Session进行身份验证?** - -很多时候我们都是通过 SessionID 来实现特定的用户,SessionID 一般会选择存放在 Redis 中。举个例子:用户成功登陆系统,然后返回给客户端具有 SessionID 的 Cookie,当用户向后端发起请求的时候会把 SessionID 带上,这样后端就知道你的身份状态了。关于这种认证方式更详细的过程如下: - -![Session Based Authentication flow](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/Session-Based-Authentication-flow.png) - -1. 用户向服务器发送用户名和密码用于登陆系统。 -2. 服务器验证通过后,服务器为用户创建一个 Session,并将 Session信息存储 起来。 -3. 服务器向用户返回一个 SessionID,写入用户的 Cookie。 -4. 当用户保持登录状态时,Cookie 将与每个后续请求一起被发送出去。 -5. 服务器可以将存储在 Cookie 上的 Session ID 与存储在内存中或者数据库中的 Session 信息进行比较,以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态。 - -另外,Spring Session提供了一种跨多个应用程序或实例管理用户会话信息的机制。如果想详细了解可以查看下面几篇很不错的文章: - -- [Getting Started with Spring Session](https://codeboje.de/spring-session-tutorial/) -- [Guide to Spring Session](https://www.baeldung.com/spring-session) -- [Sticky Sessions with Spring Session & Redis](https://medium.com/@gvnix/sticky-sessions-with-spring-session-redis-bdc6f7438cc3) - -## 4. 什么是 Token?什么是 JWT?如何基于Token进行身份验证? - -我们在上一个问题中探讨了使用 Session 来鉴别用户的身份,并且给出了几个 Spring Session 的案例分享。 我们知道 Session 信息需要保存一份在服务器端。这种方式会带来一些麻烦,比如需要我们保证保存 Session 信息服务器的可用性、不适合移动端(依赖Cookie)等等。 - -有没有一种不需要自己存放 Session 信息就能实现身份验证的方式呢?使用 Token 即可!JWT (JSON Web Token) 就是这种方式的实现,通过这种方式服务器端就不需要保存 Session 数据了,只用在客户端保存服务端返回给客户的 Token 就可以了,扩展性得到提升。 - -**JWT 本质上就一段签名的 JSON 格式的数据。由于它是带有签名的,因此接收者便可以验证它的真实性。** - -下面是 [RFC 7519](https://tools.ietf.org/html/rfc7519) 对 JWT 做的较为正式的定义。 - -> JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted. ——[JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) - -JWT 由 3 部分构成: - -1. Header :描述 JWT 的元数据。定义了生成签名的算法以及 Token 的类型。 -2. Payload(负载):用来存放实际需要传递的数据 -3. Signature(签名):服务器通过`Payload`、`Header`和一个密钥(`secret`)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。 - -在基于 Token 进行身份验证的的应用程序中,服务器通过`Payload`、`Header`和一个密钥(`secret`)创建令牌(`Token`)并将 `Token` 发送给客户端,客户端将 `Token` 保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP Header 的 Authorization字段中:` Authorization: Bearer Token`。 - -![Token Based Authentication flow](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/Token-Based-Authentication.png) - -1. 用户向服务器发送用户名和密码用于登陆系统。 -2. 身份验证服务响应并返回了签名的 JWT,上面包含了用户是谁的内容。 -3. 用户以后每次向后端发请求都在Header中带上 JWT。 -4. 服务端检查 JWT 并从中获取用户相关信息。 - - -推荐阅读: - -- [JWT (JSON Web Tokens) Are Better Than Session Cookies](https://dzone.com/articles/jwtjson-web-tokens-are-better-than-session-cookies) -- [JSON Web Tokens (JWT) 与 Sessions](https://juejin.im/entry/577b7b56a3413100618c2938) -- [JSON Web Token 入门教程](https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html) -- [彻底理解Cookie,Session,Token](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485603&idx=1&sn=c8d324f44d6102e7b44554733da10bb7&chksm=cea24768f9d5ce7efe7291ddabce02b68db34073c7e7d9a7dc9a7f01c5a80cebe33ac75248df&token=844918801&lang=zh_CN#rd) - -## 5 什么是OAuth 2.0? - -OAuth 是一个行业的标准授权协议,主要用来授权第三方应用获取有限的权限。而 OAuth 2.0是对 OAuth 1.0 的完全重新设计,OAuth 2.0更快,更容易实现,OAuth 1.0 已经被废弃。详情请见:[rfc6749](https://tools.ietf.org/html/rfc6749)。 - -实际上它就是一种授权机制,它的最终目的是为第三方应用颁发一个有时效性的令牌 token,使得第三方应用能够通过该令牌获取相关的资源。 - -OAuth 2.0 比较常用的场景就是第三方登录,当你的网站接入了第三方登录的时候一般就是使用的 OAuth 2.0 协议。 - -**推荐阅读:** - -- [OAuth 2.0 的一个简单解释](http://www.ruanyifeng.com/blog/2019/04/oauth_design.html) -- [10 分钟理解什么是 OAuth 2.0 协议](https://deepzz.com/post/what-is-oauth2-protocol.html) -- [OAuth 2.0 的四种方式](http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html) -- [GitHub OAuth 第三方登录示例教程](http://www.ruanyifeng.com/blog/2019/04/github-oauth.html) - -## 参考 - -- https://medium.com/@sherryhsu/session-vs-token-based-authentication-11a6c5ac45e4 -- https://www.varonis.com/blog/what-is-oauth/ -- https://tools.ietf.org/html/rfc6749 diff --git "a/docs/system-design/data-communication/Kafka\345\205\245\351\227\250\347\234\213\350\277\231\344\270\200\347\257\207\345\260\261\345\244\237\344\272\206.md" "b/docs/system-design/data-communication/Kafka\345\205\245\351\227\250\347\234\213\350\277\231\344\270\200\347\257\207\345\260\261\345\244\237\344\272\206.md" deleted file mode 100644 index cd45e06..0000000 --- "a/docs/system-design/data-communication/Kafka\345\205\245\351\227\250\347\234\213\350\277\231\344\270\200\347\257\207\345\260\261\345\244\237\344\272\206.md" +++ /dev/null @@ -1,321 +0,0 @@ -> 本文由 JavaGuide 读者推荐,JavaGuide 对文章进行了整理排版!原文地址:https://www.wmyskxz.com/2019/07/17/kafka-ru-men-jiu-zhe-yi-pian/ , 作者:我没有三颗心脏。 - -# 一、Kafka 简介 - ------- - -## Kafka 创建背景 - -**Kafka** 是一个消息系统,原本开发自 LinkedIn,用作 LinkedIn 的活动流(Activity Stream)和运营数据处理管道(Pipeline)的基础。现在它已被[多家不同类型的公司](https://cwiki.apache.org/confluence/display/KAFKA/Powered+By) 作为多种类型的数据管道和消息系统使用。 - -**活动流数据**是几乎所有站点在对其网站使用情况做报表时都要用到的数据中最常规的部分。活动数据包括页面访问量(Page View)、被查看内容方面的信息以及搜索情况等内容。这种数据通常的处理方式是先把各种活动以日志的形式写入某种文件,然后周期性地对这些文件进行统计分析。**运营数据**指的是服务器的性能数据(CPU、IO 使用率、请求时间、服务日志等等数据)。运营数据的统计方法种类繁多。 - -近年来,活动和运营数据处理已经成为了网站软件产品特性中一个至关重要的组成部分,这就需要一套稍微更加复杂的基础设施对其提供支持。 - -## Kafka 简介 - -**Kafka 是一种分布式的,基于发布 / 订阅的消息系统。** - -主要设计目标如下: - -- 以时间复杂度为 O(1) 的方式提供消息持久化能力,即使对 TB 级以上数据也能保证常数时间复杂度的访问性能。 -- 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒 100K 条以上消息的传输。 -- 支持 Kafka Server 间的消息分区,及分布式消费,同时保证每个 Partition 内的消息顺序传输。 -- 同时支持离线数据处理和实时数据处理。 -- Scale out:支持在线水平扩展。 - -## Kafka 基础概念 - -### 概念一:生产者与消费者 - -![生产者与消费者](./../../../media/pictures/kafka/生产者和消费者.png) - -对于 Kafka 来说客户端有两种基本类型: - -1. **生产者(Producer)** -2. **消费者(Consumer)**。 - -除此之外,还有用来做数据集成的 Kafka Connect API 和流式处理的 Kafka Streams 等高阶客户端,但这些高阶客户端底层仍然是生产者和消费者API,它们只不过是在上层做了封装。 - -这很容易理解,生产者(也称为发布者)创建消息,而消费者(也称为订阅者)负责消费or读取消息。 - -### 概念二:主题(Topic)与分区(Partition) - -![主题(Topic)与分区(Partition)](./../../../media/pictures/kafka/主题与分区.png) - -在 Kafka 中,消息以**主题(Topic)**来分类,每一个主题都对应一个 **「消息队列」**,这有点儿类似于数据库中的表。但是如果我们把所有同类的消息都塞入到一个“中心”队列中,势必缺少可伸缩性,无论是生产者/消费者数目的增加,还是消息数量的增加,都可能耗尽系统的性能或存储。 - -我们使用一个生活中的例子来说明:现在 A 城市生产的某商品需要运输到 B 城市,走的是公路,那么单通道的高速公路不论是在「A 城市商品增多」还是「现在 C 城市也要往 B 城市运输东西」这样的情况下都会出现「吞吐量不足」的问题。所以我们现在引入**分区(Partition)**的概念,类似“允许多修几条道”的方式对我们的主题完成了水平扩展。 - -### 概念三:Broker 和集群(Cluster) - -一个 Kafka 服务器也称为 Broker,它接受生产者发送的消息并存入磁盘;Broker 同时服务消费者拉取分区消息的请求,返回目前已经提交的消息。使用特定的机器硬件,一个 Broker 每秒可以处理成千上万的分区和百万量级的消息。(现在动不动就百万量级..我特地去查了一把,好像确实集群的情况下吞吐量挺高的..嗯..) - -若干个 Broker 组成一个集群(Cluster),其中集群内某个 Broker 会成为集群控制器(Cluster Controller),它负责管理集群,包括分配分区到 Broker、监控 Broker 故障等。在集群内,一个分区由一个 Broker 负责,这个 Broker 也称为这个分区的 Leader;当然一个分区可以被复制到多个 Broker 上来实现冗余,这样当存在 Broker 故障时可以将其分区重新分配到其他 Broker 来负责。下图是一个样例: - -![Broker和集群](./../../../media/pictures/kafka/Broker和集群.png) - -Kafka 的一个关键性质是日志保留(retention),我们可以配置主题的消息保留策略,譬如只保留一段时间的日志或者只保留特定大小的日志。当超过这些限制时,老的消息会被删除。我们也可以针对某个主题单独设置消息过期策略,这样对于不同应用可以实现个性化。 - -### 概念四:多集群 - -随着业务发展,我们往往需要多集群,通常处于下面几个原因: - -- 基于数据的隔离; -- 基于安全的隔离; -- 多数据中心(容灾) - -当构建多个数据中心时,往往需要实现消息互通。举个例子,假如用户修改了个人资料,那么后续的请求无论被哪个数据中心处理,这个更新需要反映出来。又或者,多个数据中心的数据需要汇总到一个总控中心来做数据分析。 - -上面说的分区复制冗余机制只适用于同一个 Kafka 集群内部,对于多个 Kafka 集群消息同步可以使用 Kafka 提供的 MirrorMaker 工具。本质上来说,MirrorMaker 只是一个 Kafka 消费者和生产者,并使用一个队列连接起来而已。它从一个集群中消费消息,然后往另一个集群生产消息。 - - -# 二、Kafka 的设计与实现 - ------- - -上面我们知道了 Kafka 中的一些基本概念,但作为一个成熟的「消息队列」中间件,其中有许多有意思的设计值得我们思考,下面我们简单列举一些。 - -## 讨论一:Kafka 存储在文件系统上 - -是的,**您首先应该知道 Kafka 的消息是存在于文件系统之上的**。Kafka 高度依赖文件系统来存储和缓存消息,一般的人认为 “磁盘是缓慢的”,所以对这样的设计持有怀疑态度。实际上,磁盘比人们预想的快很多也慢很多,这取决于它们如何被使用;一个好的磁盘结构设计可以使之跟网络速度一样快。 - -现代的操作系统针对磁盘的读写已经做了一些优化方案来加快磁盘的访问速度。比如,**预读**会提前将一个比较大的磁盘快读入内存。**后写**会将很多小的逻辑写操作合并起来组合成一个大的物理写操作。并且,操作系统还会将主内存剩余的所有空闲内存空间都用作**磁盘缓存**,所有的磁盘读写操作都会经过统一的磁盘缓存(除了直接 I/O 会绕过磁盘缓存)。综合这几点优化特点,**如果是针对磁盘的顺序访问,某些情况下它可能比随机的内存访问都要快,甚至可以和网络的速度相差无几。** - -**上述的 Topic 其实是逻辑上的概念,面相消费者和生产者,物理上存储的其实是 Partition**,每一个 Partition 最终对应一个目录,里面存储所有的消息和索引文件。默认情况下,每一个 Topic 在创建时如果不指定 Partition 数量时只会创建 1 个 Partition。比如,我创建了一个 Topic 名字为 test ,没有指定 Partition 的数量,那么会默认创建一个 test-0 的文件夹,这里的命名规则是:`-`。 - -![主题(Topic)与分区(Partition)](./../../../media/pictures/kafka/kafka存在文件系统上.png) - -任何发布到 Partition 的消息都会被追加到 Partition 数据文件的尾部,这样的顺序写磁盘操作让 Kafka 的效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是 Kafka 高吞吐率的一个很重要的保证)。 - -每一条消息被发送到 Broker 中,会根据 Partition 规则选择被存储到哪一个 Partition。如果 Partition 规则设置的合理,所有消息可以均匀分布到不同的 Partition中。 - -## 讨论二:Kafka 中的底层存储设计 - -假设我们现在 Kafka 集群只有一个 Broker,我们创建 2 个 Topic 名称分别为:「topic1」和「topic2」,Partition 数量分别为 1、2,那么我们的根目录下就会创建如下三个文件夹: - -```shell - | --topic1-0 - | --topic2-0 - | --topic2-1 -``` - -在 Kafka 的文件存储中,同一个 Topic 下有多个不同的 Partition,每个 Partition 都为一个目录,而每一个目录又被平均分配成多个大小相等的 **Segment File** 中,Segment File 又由 index file 和 data file 组成,他们总是成对出现,后缀 “.index” 和 “.log” 分表表示 Segment 索引文件和数据文件。 - -现在假设我们设置每个 Segment 大小为 500 MB,并启动生产者向 topic1 中写入大量数据,topic1-0 文件夹中就会产生类似如下的一些文件: - -```shell - | --topic1-0 - | --00000000000000000000.index - | --00000000000000000000.log - | --00000000000000368769.index - | --00000000000000368769.log - | --00000000000000737337.index - | --00000000000000737337.log - | --00000000000001105814.index | --00000000000001105814.log - | --topic2-0 - | --topic2-1 - -``` - -**Segment 是 Kafka 文件存储的最小单位。**Segment 文件命名规则:Partition 全局的第一个 Segment 从 0 开始,后续每个 Segment 文件名为上一个 Segment 文件最后一条消息的 offset 值。数值最大为 64 位 long 大小,19 位数字字符长度,没有数字用0填充。如 00000000000000368769.index 和 00000000000000368769.log。 - -以上面的一对 Segment File 为例,说明一下索引文件和数据文件对应关系: - -![索引文件和数据文件](./../../../media/pictures/kafka/segment是kafka文件存储的最小单位.png) - - - -其中以索引文件中元数据 `<3, 497>` 为例,依次在数据文件中表示第 3 个 message(在全局 Partition 表示第 368769 + 3 = 368772 个 message)以及该消息的物理偏移地址为 497。 - -注意该 index 文件并不是从0开始,也不是每次递增1的,这是因为 Kafka 采取稀疏索引存储的方式,每隔一定字节的数据建立一条索引,它减少了索引文件大小,使得能够把 index 映射到内存,降低了查询时的磁盘 IO 开销,同时也并没有给查询带来太多的时间消耗。 - -因为其文件名为上一个 Segment 最后一条消息的 offset ,所以当需要查找一个指定 offset 的 message 时,通过在所有 segment 的文件名中进行二分查找就能找到它归属的 segment ,再在其 index 文件中找到其对应到文件上的物理位置,就能拿出该 message 。 - -由于消息在 Partition 的 Segment 数据文件中是顺序读写的,且消息消费后不会删除(删除策略是针对过期的 Segment 文件),这种顺序磁盘 IO 存储设计师 Kafka 高性能很重要的原因。 - -> Kafka 是如何准确的知道 message 的偏移的呢?这是因为在 Kafka 定义了标准的数据存储结构,在 Partition 中的每一条 message 都包含了以下三个属性: -> -> - offset:表示 message 在当前 Partition 中的偏移量,是一个逻辑上的值,唯一确定了 Partition 中的一条 message,可以简单的认为是一个 id; -> - MessageSize:表示 message 内容 data 的大小; -> - data:message 的具体内容 - -## 讨论三:生产者设计概要 - -当我们发送消息之前,先问几个问题:每条消息都是很关键且不能容忍丢失么?偶尔重复消息可以么?我们关注的是消息延迟还是写入消息的吞吐量? - -举个例子,有一个信用卡交易处理系统,当交易发生时会发送一条消息到 Kafka,另一个服务来读取消息并根据规则引擎来检查交易是否通过,将结果通过 Kafka 返回。对于这样的业务,消息既不能丢失也不能重复,由于交易量大因此吞吐量需要尽可能大,延迟可以稍微高一点。 - -再举个例子,假如我们需要收集用户在网页上的点击数据,对于这样的场景,少量消息丢失或者重复是可以容忍的,延迟多大都不重要只要不影响用户体验,吞吐则根据实时用户数来决定。 - -不同的业务需要使用不同的写入方式和配置。具体的方式我们在这里不做讨论,现在先看下生产者写消息的基本流程: - -![生产者设计概要](./../../../media/pictures/kafka/生产者设计概要.png) - -图片来源:[http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/12/kafka-producer.html](http://www.dengshenyu.com/分布式系统/2017/11/12/kafka-producer.html) - -流程如下: - -1. 首先,我们需要创建一个ProducerRecord,这个对象需要包含消息的主题(topic)和值(value),可以选择性指定一个键值(key)或者分区(partition)。 -2. 发送消息时,生产者会对键值和值序列化成字节数组,然后发送到分配器(partitioner)。 -3. 如果我们指定了分区,那么分配器返回该分区即可;否则,分配器将会基于键值来选择一个分区并返回。 -4. 选择完分区后,生产者知道了消息所属的主题和分区,它将这条记录添加到相同主题和分区的批量消息中,另一个线程负责发送这些批量消息到对应的Kafka broker。 -5. 当broker接收到消息后,如果成功写入则返回一个包含消息的主题、分区及位移的RecordMetadata对象,否则返回异常。 -6. 生产者接收到结果后,对于异常可能会进行重试。 - - - -## 讨论四:消费者设计概要 - -### 消费者与消费组 - -假设这么个场景:我们从Kafka中读取消息,并且进行检查,最后产生结果数据。我们可以创建一个消费者实例去做这件事情,但如果生产者写入消息的速度比消费者读取的速度快怎么办呢?这样随着时间增长,消息堆积越来越严重。对于这种场景,我们需要增加多个消费者来进行水平扩展。 - -Kafka消费者是**消费组**的一部分,当多个消费者形成一个消费组来消费主题时,每个消费者会收到不同分区的消息。假设有一个T1主题,该主题有4个分区;同时我们有一个消费组G1,这个消费组只有一个消费者C1。那么消费者C1将会收到这4个分区的消息,如下所示: - -![生产者设计概要](./../../../media/pictures/kafka/消费者设计概要1.png) -如果我们增加新的消费者C2到消费组G1,那么每个消费者将会分别收到两个分区的消息,如下所示: - -![生产者设计概要](./../../../media/pictures/kafka/消费者设计概要2.png) - -如果增加到4个消费者,那么每个消费者将会分别收到一个分区的消息,如下所示: - -![生产者设计概要](./../../../media/pictures/kafka/消费者设计概要3.png) - -但如果我们继续增加消费者到这个消费组,剩余的消费者将会空闲,不会收到任何消息: - -![生产者设计概要](./../../../media/pictures/kafka/消费者设计概要4.png) - -总而言之,我们可以通过增加消费组的消费者来进行水平扩展提升消费能力。这也是为什么建议创建主题时使用比较多的分区数,这样可以在消费负载高的情况下增加消费者来提升性能。另外,消费者的数量不应该比分区数多,因为多出来的消费者是空闲的,没有任何帮助。 - -**Kafka一个很重要的特性就是,只需写入一次消息,可以支持任意多的应用读取这个消息。**换句话说,每个应用都可以读到全量的消息。为了使得每个应用都能读到全量消息,应用需要有不同的消费组。对于上面的例子,假如我们新增了一个新的消费组G2,而这个消费组有两个消费者,那么会是这样的: - -![生产者设计概要](./../../../media/pictures/kafka/消费者设计概要5.png) - -在这个场景中,消费组G1和消费组G2都能收到T1主题的全量消息,在逻辑意义上来说它们属于不同的应用。 - -最后,总结起来就是:如果应用需要读取全量消息,那么请为该应用设置一个消费组;如果该应用消费能力不足,那么可以考虑在这个消费组里增加消费者。 - -### 消费组与分区重平衡 - -可以看到,当新的消费者加入消费组,它会消费一个或多个分区,而这些分区之前是由其他消费者负责的;另外,当消费者离开消费组(比如重启、宕机等)时,它所消费的分区会分配给其他分区。这种现象称为**重平衡(rebalance)**。重平衡是 Kafka 一个很重要的性质,这个性质保证了高可用和水平扩展。**不过也需要注意到,在重平衡期间,所有消费者都不能消费消息,因此会造成整个消费组短暂的不可用。**而且,将分区进行重平衡也会导致原来的消费者状态过期,从而导致消费者需要重新更新状态,这段期间也会降低消费性能。后面我们会讨论如何安全的进行重平衡以及如何尽可能避免。 - -消费者通过定期发送心跳(hearbeat)到一个作为组协调者(group coordinator)的 broker 来保持在消费组内存活。这个 broker 不是固定的,每个消费组都可能不同。当消费者拉取消息或者提交时,便会发送心跳。 - -如果消费者超过一定时间没有发送心跳,那么它的会话(session)就会过期,组协调者会认为该消费者已经宕机,然后触发重平衡。可以看到,从消费者宕机到会话过期是有一定时间的,这段时间内该消费者的分区都不能进行消息消费;通常情况下,我们可以进行优雅关闭,这样消费者会发送离开的消息到组协调者,这样组协调者可以立即进行重平衡而不需要等待会话过期。 - -在 0.10.1 版本,Kafka 对心跳机制进行了修改,将发送心跳与拉取消息进行分离,这样使得发送心跳的频率不受拉取的频率影响。另外更高版本的 Kafka 支持配置一个消费者多长时间不拉取消息但仍然保持存活,这个配置可以避免活锁(livelock)。活锁,是指应用没有故障但是由于某些原因不能进一步消费。 - -### Partition 与消费模型 - -上面提到,Kafka 中一个 topic 中的消息是被打散分配在多个 Partition(分区) 中存储的, Consumer Group 在消费时需要从不同的 Partition 获取消息,那最终如何重建出 Topic 中消息的顺序呢? - -答案是:没有办法。Kafka 只会保证在 Partition 内消息是有序的,而不管全局的情况。 - -下一个问题是:Partition 中的消息可以被(不同的 Consumer Group)多次消费,那 Partition中被消费的消息是何时删除的? Partition 又是如何知道一个 Consumer Group 当前消费的位置呢? - -无论消息是否被消费,除非消息到期 Partition 从不删除消息。例如设置保留时间为 2 天,则消息发布 2 天内任何 Group 都可以消费,2 天后,消息自动被删除。 -Partition 会为每个 Consumer Group 保存一个偏移量,记录 Group 消费到的位置。 如下图: -![生产者设计概要](./../../../media/pictures/kafka/Partition与消费模型.png) - - - - -### 为什么 Kafka 是 pull 模型 - -消费者应该向 Broker 要数据(pull)还是 Broker 向消费者推送数据(push)?作为一个消息系统,Kafka 遵循了传统的方式,选择由 Producer 向 broker push 消息并由 Consumer 从 broker pull 消息。一些 logging-centric system,比如 Facebook 的[Scribe](https://github.com/facebookarchive/scribe)和 Cloudera 的[Flume](https://flume.apache.org/),采用 push 模式。事实上,push 模式和 pull 模式各有优劣。 - -**push 模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。**push 模式的目标是尽可能以最快速度传递消息,但是这样很容易造成 Consumer 来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。**而 pull 模式则可以根据 Consumer 的消费能力以适当的速率消费消息。** - -**对于 Kafka 而言,pull 模式更合适。**pull 模式可简化 broker 的设计,Consumer 可自主控制消费消息的速率,同时 Consumer 可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。 - -## 讨论五:Kafka 如何保证可靠性 - -当我们讨论**可靠性**的时候,我们总会提到*保证**这个词语。可靠性保证是基础,我们基于这些基础之上构建我们的应用。比如关系型数据库的可靠性保证是ACID,也就是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。 - -Kafka 中的可靠性保证有如下四点: - -- 对于一个分区来说,它的消息是有序的。如果一个生产者向一个分区先写入消息A,然后写入消息B,那么消费者会先读取消息A再读取消息B。 -- 当消息写入所有in-sync状态的副本后,消息才会认为**已提交(committed)**。这里的写入有可能只是写入到文件系统的缓存,不一定刷新到磁盘。生产者可以等待不同时机的确认,比如等待分区主副本写入即返回,后者等待所有in-sync状态副本写入才返回。 -- 一旦消息已提交,那么只要有一个副本存活,数据不会丢失。 -- 消费者只能读取到已提交的消息。 - -使用这些基础保证,我们构建一个可靠的系统,这时候需要考虑一个问题:究竟我们的应用需要多大程度的可靠性?可靠性不是无偿的,它与系统可用性、吞吐量、延迟和硬件价格息息相关,得此失彼。因此,我们往往需要做权衡,一味的追求可靠性并不实际。 - -> 想了解更多戳这里:http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/21/kafka-data-delivery.html - -# 三、动手搭一个 Kafka - -通过上面的描述,我们已经大致了解到了「Kafka」是何方神圣了,现在我们开始尝试自己动手本地搭一个来实际体验一把。 - -## 第一步:下载 Kafka - -这里以 Mac OS 为例,在安装了 Homebrew 的情况下执行下列代码: - -```shell -brew install kafka -``` - -由于 Kafka 依赖了 Zookeeper,所以在下载的时候会自动下载。 - -## 第二步:启动服务 - -我们在启动之前首先需要修改 Kafka 的监听地址和端口为 `localhost:9092`: - -```shell -vi /usr/local/etc/kafka/server.properties -``` - - -然后修改成下图的样子: - -![启动服务](./../../../media/pictures/kafka/启动服务.png) -依次启动 Zookeeper 和 Kafka: - -```shell -brew services start zookeeper -brew services start kafka -``` - -然后执行下列语句来创建一个名字为 “test” 的 Topic: - -```shell -kafka-topics --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test -``` - -我们可以通过下列的命令查看我们的 Topic 列表: - -```shell -kafka-topics --list --zookeeper localhost:2181 -``` - -## 第三步:发送消息 - -然后我们新建一个控制台,运行下列命令创建一个消费者关注刚才创建的 Topic: - -```shell -kafka-console-consumer --bootstrap-server localhost:9092 --topic test --from-beginning -``` - -用控制台往刚才创建的 Topic 中添加消息,并观察刚才创建的消费者窗口: - -```shel -kafka-console-producer --broker-list localhost:9092 --topic test -``` - -能通过消费者窗口观察到正确的消息: - -![发送消息](./../../../media/pictures/kafka/发送消息.png) - -# 参考资料 - ------- - -1. https://www.infoq.cn/article/kafka-analysis-part-1 - Kafka 设计解析(一):Kafka 背景及架构介绍 -2. [http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/06/kafka-Meet-Kafka.html](http://www.dengshenyu.com/分布式系统/2017/11/06/kafka-Meet-Kafka.html) - Kafka系列(一)初识Kafka -3. https://lotabout.me/2018/kafka-introduction/ - Kafka 入门介绍 -4. https://www.zhihu.com/question/28925721 - Kafka 中的 Topic 为什么要进行分区? - 知乎 -5. https://blog.joway.io/posts/kafka-design-practice/ - Kafka 的设计与实践思考 -6. [http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/21/kafka-data-delivery.html](http://www.dengshenyu.com/分布式系统/2017/11/21/kafka-data-delivery.html) - Kafka系列(六)可靠的数据传输 - - diff --git "a/docs/system-design/data-communication/Kafka\347\263\273\347\273\237\350\256\276\350\256\241\345\274\200\347\257\207-\351\235\242\350\257\225\347\234\213\350\277\231\347\257\207\345\260\261\345\244\237\344\272\206.md" "b/docs/system-design/data-communication/Kafka\347\263\273\347\273\237\350\256\276\350\256\241\345\274\200\347\257\207-\351\235\242\350\257\225\347\234\213\350\277\231\347\257\207\345\260\261\345\244\237\344\272\206.md" deleted file mode 100644 index d21fad3..0000000 --- "a/docs/system-design/data-communication/Kafka\347\263\273\347\273\237\350\256\276\350\256\241\345\274\200\347\257\207-\351\235\242\350\257\225\347\234\213\350\277\231\347\257\207\345\260\261\345\244\237\344\272\206.md" +++ /dev/null @@ -1,281 +0,0 @@ -> 原文链接:https://mp.weixin.qq.com/s/zxPz_aFEMrshApZQ727h4g - -## 引言 - -MQ(消息队列)是跨进程通信的方式之一,可理解为异步rpc,上游系统对调用结果的态度往往是重要不紧急。使用消息队列有以下好处:业务解耦、流量削峰、灵活扩展。接下来介绍消息中间件Kafka。 - -## Kafka是什么? - -Kafka是一个分布式的消息引擎。具有以下特征 - -能够发布和订阅消息流(类似于消息队列) -以容错的、持久的方式存储消息流 -多分区概念,提高了并行能力 - -## Kafka架构总览 - -![Kafka系统架构](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/kafka%E6%9E%B6%E6%9E%84.png) - -## Topic - -消息的主题、队列,每一个消息都有它的topic,Kafka通过topic对消息进行归类。Kafka中可以将Topic从物理上划分成一个或多个分区(Partition),每个分区在物理上对应一个文件夹,以”topicName_partitionIndex”的命名方式命名,该dir包含了这个分区的所有消息(.log)和索引文件(.index),这使得Kafka的吞吐率可以水平扩展。 - -## Partition - -每个分区都是一个 顺序的、不可变的消息队列, 并且可以持续的添加;分区中的消息都被分了一个序列号,称之为偏移量(offset),在每个分区中此偏移量都是唯一的。 -producer在发布消息的时候,可以为每条消息指定Key,这样消息被发送到broker时,会根据分区算法把消息存储到对应的分区中(一个分区存储多个消息),如果分区规则设置的合理,那么所有的消息将会被均匀的分布到不同的分区中,这样就实现了负载均衡。 -![partition_info](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/partition.jpg) - -## Broker - -Kafka server,用来存储消息,Kafka集群中的每一个服务器都是一个Broker,消费者将从broker拉取订阅的消息 -Producer -向Kafka发送消息,生产者会根据topic分发消息。生产者也负责把消息关联到Topic上的哪一个分区。最简单的方式从分区列表中轮流选择。也可以根据某种算法依照权重选择分区。算法可由开发者定义。 - -## Cousumer - -Consermer实例可以是独立的进程,负责订阅和消费消息。消费者用consumerGroup来标识自己。同一个消费组可以并发地消费多个分区的消息,同一个partition也可以由多个consumerGroup并发消费,但是在consumerGroup中一个partition只能由一个consumer消费 - -## CousumerGroup - -Consumer Group:同一个Consumer Group中的Consumers,Kafka将相应Topic中的每个消息只发送给其中一个Consumer - -# Kafka producer 设计原理 - -## 发送消息的流程 - -![partition_info](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/sendMsg.jpg) -**1.序列化消息&&.计算partition** -根据key和value的配置对消息进行序列化,然后计算partition: -ProducerRecord对象中如果指定了partition,就使用这个partition。否则根据key和topic的partition数目取余,如果key也没有的话就随机生成一个counter,使用这个counter来和partition数目取余。这个counter每次使用的时候递增。 - -**2发送到batch&&唤醒Sender 线程** -根据topic-partition获取对应的batchs(Deque),然后将消息append到batch中.如果有batch满了则唤醒Sender 线程。队列的操作是加锁执行,所以batch内消息时有序的。后续的Sender操作当前方法异步操作。 -![send_msg](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/send2Batch1.png)![send_msg2](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/send2Batch2.png) - - - -**3.Sender把消息有序发到 broker(tp replia leader)** -**3.1 确定tp relica leader 所在的broker** - -Kafka中 每台broker都保存了kafka集群的metadata信息,metadata信息里包括了每个topic的所有partition的信息: leader, leader_epoch, controller_epoch, isr, replicas等;Kafka客户端从任一broker都可以获取到需要的metadata信息;sender线程通过metadata信息可以知道tp leader的brokerId -producer也保存了metada信息,同时根据metadata更新策略(定期更新metadata.max.age.ms、失效检测,强制更新:检查到metadata失效以后,调用metadata.requestUpdate()强制更新 - -``` -public class PartitionInfo { - private final String topic; private final int partition; - private final Node leader; private final Node[] replicas; - private final Node[] inSyncReplicas; private final Node[] offlineReplicas; -} -``` - -**3.2 幂等性发送** - -为实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number。对于每个PID,该Producer发送消息的每个都对应一个单调递增的Sequence Number。同样,Broker端也会为每个维护一个序号,并且每Commit一条消息时将其对应序号递增。对于接收的每条消息,如果其序号比Broker维护的序号)大一,则Broker会接受它,否则将其丢弃: - -如果消息序号比Broker维护的序号差值比一大,说明中间有数据尚未写入,即乱序,此时Broker拒绝该消息,Producer抛出InvalidSequenceNumber -如果消息序号小于等于Broker维护的序号,说明该消息已被保存,即为重复消息,Broker直接丢弃该消息,Producer抛出DuplicateSequenceNumber -Sender发送失败后会重试,这样可以保证每个消息都被发送到broker - -**4. Sender处理broker发来的produce response** -一旦broker处理完Sender的produce请求,就会发送produce response给Sender,此时producer将执行我们为send()设置的回调函数。至此producer的send执行完毕。 - -## 吞吐性&&延时: - -buffer.memory:buffer设置大了有助于提升吞吐性,但是batch太大会增大延迟,可搭配linger_ms参数使用 -linger_ms:如果batch太大,或者producer qps不高,batch添加的会很慢,我们可以强制在linger_ms时间后发送batch数据 -ack:producer收到多少broker的答复才算真的发送成功 -0表示producer无需等待leader的确认(吞吐最高、数据可靠性最差) -1代表需要leader确认写入它的本地log并立即确认 --1/all 代表所有的ISR都完成后确认(吞吐最低、数据可靠性最高) - -## Sender线程和长连接 - -每初始化一个producer实例,都会初始化一个Sender实例,新增到broker的长连接。 -代码角度:每初始化一次KafkaProducer,都赋一个空的client - -``` -public KafkaProducer(final Map configs) { - this(configs, null, null, null, null, null, Time.SYSTEM); -} -``` - -![Sender_io](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/SenderIO.jpg) - -终端查看TCP连接数: -lsof -p portNum -np | grep TCP - -# Consumer设计原理 - -## poll消息 - -![consumer-pool](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/consumerPoll.jpg) - -- 消费者通过fetch线程拉消息(单线程) -- 消费者通过心跳线程来与broker发送心跳。超时会认为挂掉 -- 每个consumer - group在broker上都有一个coordnator来管理,消费者加入和退出,以及消费消息的位移都由coordnator处理。 - -## 位移管理 - -consumer的消息位移代表了当前group对topic-partition的消费进度,consumer宕机重启后可以继续从该offset开始消费。 -在kafka0.8之前,位移信息存放在zookeeper上,由于zookeeper不适合高并发的读写,新版本Kafka把位移信息当成消息,发往__consumers_offsets 这个topic所在的broker,__consumers_offsets默认有50个分区。 -消息的key 是groupId+topic_partition,value 是offset. - -![consumerOffsetDat](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/consumerOffsetData.jpg)![consumerOffsetView](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/consumerOffsetView.jpg) - - - -## Kafka Group 状态 - -![groupState](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/groupState.jpg) - -- Empty:初始状态,Group 没有任何成员,如果所有的 offsets 都过期的话就会变成 Dead -- PreparingRebalance:Group 正在准备进行 Rebalance -- AwaitingSync:Group 正在等待来 group leader 的 分配方案 -- Stable:稳定的状态(Group is stable); -- Dead:Group 内已经没有成员,并且它的 Metadata 已经被移除 - -## 重平衡reblance - -当一些原因导致consumer对partition消费不再均匀时,kafka会自动执行reblance,使得consumer对partition的消费再次平衡。 -什么时候发生rebalance?: - -- 组订阅topic数变更 -- topic partition数变更 -- consumer成员变更 -- consumer 加入群组或者离开群组的时候 -- consumer被检测为崩溃的时候 - -## reblance过程 - -举例1 consumer被检测为崩溃引起的reblance -比如心跳线程在timeout时间内没和broker发送心跳,此时coordnator认为该group应该进行reblance。接下来其他consumer发来fetch请求后,coordnator将回复他们进行reblance通知。当consumer成员收到请求后,只有leader会根据分配策略进行分配,然后把各自的分配结果返回给coordnator。这个时候只有consumer leader返回的是实质数据,其他返回的都为空。收到分配方法后,consumer将会把分配策略同步给各consumer - -举例2 consumer加入引起的reblance - -使用join协议,表示有consumer 要加入到group中 -使用sync 协议,根据分配规则进行分配 -![reblance-join](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/reblance-join.jpg)![reblance-sync](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/reblance-sync.jpg) - -(上图图片摘自网络) - -## 引申:以上reblance机制存在的问题 - -在大型系统中,一个topic可能对应数百个consumer实例。这些consumer陆续加入到一个空消费组将导致多次的rebalance;此外consumer 实例启动的时间不可控,很有可能超出coordinator确定的rebalance timeout(即max.poll.interval.ms),将会再次触发rebalance,而每次rebalance的代价又相当地大,因为很多状态都需要在rebalance前被持久化,而在rebalance后被重新初始化。 - -## 新版本改进 - -**通过延迟进入PreparingRebalance状态减少reblance次数** - -![groupStateOfNewVersion](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/groupStateOfNewVersion.jpg) - -新版本新增了group.initial.rebalance.delay.ms参数。空消费组接受到成员加入请求时,不立即转化到PreparingRebalance状态来开启reblance。当时间超过group.initial.rebalance.delay.ms后,再把group状态改为PreparingRebalance(开启reblance)。实现机制是在coordinator底层新增一个group状态:InitialReblance。假设此时有多个consumer陆续启动,那么group状态先转化为InitialReblance,待group.initial.rebalance.delay.ms时间后,再转换为PreparingRebalance(开启reblance) - - - -# Broker设计原理 - -Broker 是Kafka 集群中的节点。负责处理生产者发送过来的消息,消费者消费的请求。以及集群节点的管理等。由于涉及内容较多,先简单介绍,后续专门抽出一篇文章分享 - -## broker zk注册 - -![brokersInZk](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/brokersInZk.jpg) - -## broker消息存储 - -Kafka的消息以二进制的方式紧凑地存储,节省了很大空间 -此外消息存在ByteBuffer而不是堆,这样broker进程挂掉时,数据不会丢失,同时避免了gc问题 -通过零拷贝和顺序寻址,让消息存储和读取速度都非常快 -处理fetch请求的时候通过zero-copy 加快速度 - -## broker状态数据 - -broker设计中,每台机器都保存了相同的状态数据。主要包括以下: - -controller所在的broker ID,即保存了当前集群中controller是哪台broker -集群中所有broker的信息:比如每台broker的ID、机架信息以及配置的若干组连接信息 -集群中所有节点的信息:严格来说,它和上一个有些重复,不过此项是按照broker ID和***类型进行分组的。对于超大集群来说,使用这一项缓存可以快速地定位和查找给定节点信息,而无需遍历上一项中的内容,算是一个优化吧 -集群中所有分区的信息:所谓分区信息指的是分区的leader、ISR和AR信息以及当前处于offline状态的副本集合。这部分数据按照topic-partitionID进行分组,可以快速地查找到每个分区的当前状态。(注:AR表示assigned replicas,即创建topic时为该分区分配的副本集合) - -## broker负载均衡 - -**分区数量负载**:各台broker的partition数量应该均匀 -partition Replica分配算法如下: - -将所有Broker(假设共n个Broker)和待分配的Partition排序 -将第i个Partition分配到第(i mod n)个Broker上 -将第i个Partition的第j个Replica分配到第((i + j) mod n)个Broker上 - -**容量大小负载:**每台broker的硬盘占用大小应该均匀 -在kafka1.1之前,Kafka能够保证各台broker上partition数量均匀,但由于每个partition内的消息数不同,可能存在不同硬盘之间内存占用差异大的情况。在Kafka1.1中增加了副本跨路径迁移功能kafka-reassign-partitions.sh,我们可以结合它和监控系统,实现自动化的负载均衡 - -# Kafka高可用 - -在介绍kafka高可用之前先介绍几个概念 - -同步复制:要求所有能工作的Follower都复制完,这条消息才会被认为commit,这种复制方式极大的影响了吞吐率 -异步复制:Follower异步的从Leader pull数据,data只要被Leader写入log认为已经commit,这种情况下如果Follower落后于Leader的比较多,如果Leader突然宕机,会丢失数据 - -## Isr - -Kafka结合同步复制和异步复制,使用ISR(与Partition Leader保持同步的Replica列表)的方式在确保数据不丢失和吞吐率之间做了平衡。Producer只需把消息发送到Partition Leader,Leader将消息写入本地Log。Follower则从Leader pull数据。Follower在收到该消息向Leader发送ACK。一旦Leader收到了ISR中所有Replica的ACK,该消息就被认为已经commit了,Leader将增加HW并且向Producer发送ACK。这样如果leader挂了,只要Isr中有一个replica存活,就不会丢数据。 - -## Isr动态更新 - -Leader会跟踪ISR,如果ISR中一个Follower宕机,或者落后太多,Leader将把它从ISR中移除。这里所描述的“落后太多”指Follower复制的消息落后于Leader后的条数超过预定值(replica.lag.max.messages)或者Follower超过一定时间(replica.lag.time.max.ms)未向Leader发送fetch请求。 - -broker Nodes In Zookeeper -/brokers/topics/[topic]/partitions/[partition]/state 保存了topic-partition的leader和Isr等信息 - -![partitionStateInZk](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/partitionStateInZk.jpg) - -## Controller负责broker故障检查&&故障转移(fail/recover) - -1. Controller在Zookeeper上注册Watch,一旦有Broker宕机,其在Zookeeper对应的znode会自动被删除,Zookeeper会触发 - Controller注册的watch,Controller读取最新的Broker信息 -2. Controller确定set_p,该集合包含了宕机的所有Broker上的所有Partition -3. 对set_p中的每一个Partition,选举出新的leader、Isr,并更新结果。 - -3.1 从/brokers/topics/[topic]/partitions/[partition]/state读取该Partition当前的ISR - -3.2 决定该Partition的新Leader和Isr。如果当前ISR中有至少一个Replica还幸存,则选择其中一个作为新Leader,新的ISR则包含当前ISR中所有幸存的Replica。否则选择该Partition中任意一个幸存的Replica作为新的Leader以及ISR(该场景下可能会有潜在的数据丢失) - -![electLeader](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/electLeader.jpg) -3.3 更新Leader、ISR、leader_epoch、controller_epoch:写入/brokers/topics/[topic]/partitions/[partition]/state - -4. 直接通过RPC向set_p相关的Broker发送LeaderAndISRRequest命令。Controller可以在一个RPC操作中发送多个命令从而提高效率。 - -## Controller挂掉 - -每个 broker 都会在 zookeeper 的临时节点 "/controller" 注册 watcher,当 controller 宕机时 "/controller" 会消失,触发broker的watch,每个 broker 都尝试创建新的 controller path,只有一个竞选成功并当选为 controller。 - -# 使用Kafka如何保证幂等性 - -不丢消息 - -首先kafka保证了对已提交消息的at least保证 -Sender有重试机制 -producer业务方在使用producer发送消息时,注册回调函数。在onError方法中重发消息 -consumer 拉取到消息后,处理完毕再commit,保证commit的消息一定被处理完毕 - -不重复 - -consumer拉取到消息先保存,commit成功后删除缓存数据 - -# Kafka高性能 - -partition提升了并发 -zero-copy -顺序写入 -消息聚集batch -页缓存 -业务方对 Kafka producer的优化 - -增大producer数量 -ack配置 -batch - -如果喜欢我的文章,欢迎扫码关注 - -![wechat](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/qrcode_for_gh_2f3803598393_258.jpg) \ No newline at end of file diff --git a/docs/system-design/data-communication/RocketMQ-Questions.md b/docs/system-design/data-communication/RocketMQ-Questions.md deleted file mode 100644 index a41a403..0000000 --- a/docs/system-design/data-communication/RocketMQ-Questions.md +++ /dev/null @@ -1,214 +0,0 @@ -本文来自读者 [PR](https://github.com/Snailclimb/JavaGuide/pull/291)。 - - -- [1 单机版消息中心](#1-%E5%8D%95%E6%9C%BA%E7%89%88%E6%B6%88%E6%81%AF%E4%B8%AD%E5%BF%83) -- [2 分布式消息中心](#2-%E5%88%86%E5%B8%83%E5%BC%8F%E6%B6%88%E6%81%AF%E4%B8%AD%E5%BF%83) - - [2.1 问题与解决](#21-%E9%97%AE%E9%A2%98%E4%B8%8E%E8%A7%A3%E5%86%B3) - - [2.1.1 消息丢失的问题](#211-%E6%B6%88%E6%81%AF%E4%B8%A2%E5%A4%B1%E7%9A%84%E9%97%AE%E9%A2%98) - - [2.1.2 同步落盘怎么才能快](#212-%E5%90%8C%E6%AD%A5%E8%90%BD%E7%9B%98%E6%80%8E%E4%B9%88%E6%89%8D%E8%83%BD%E5%BF%AB) - - [2.1.3 消息堆积的问题](#213-%E6%B6%88%E6%81%AF%E5%A0%86%E7%A7%AF%E7%9A%84%E9%97%AE%E9%A2%98) - - [2.1.4 定时消息的实现](#214-%E5%AE%9A%E6%97%B6%E6%B6%88%E6%81%AF%E7%9A%84%E5%AE%9E%E7%8E%B0) - - [2.1.5 顺序消息的实现](#215-%E9%A1%BA%E5%BA%8F%E6%B6%88%E6%81%AF%E7%9A%84%E5%AE%9E%E7%8E%B0) - - [2.1.6 分布式消息的实现](#216-%E5%88%86%E5%B8%83%E5%BC%8F%E6%B6%88%E6%81%AF%E7%9A%84%E5%AE%9E%E7%8E%B0) - - [2.1.7 消息的 push 实现](#217-%E6%B6%88%E6%81%AF%E7%9A%84-push-%E5%AE%9E%E7%8E%B0) - - [2.1.8 消息重复发送的避免](#218-%E6%B6%88%E6%81%AF%E9%87%8D%E5%A4%8D%E5%8F%91%E9%80%81%E7%9A%84%E9%81%BF%E5%85%8D) - - [2.1.9 广播消费与集群消费](#219-%E5%B9%BF%E6%92%AD%E6%B6%88%E8%B4%B9%E4%B8%8E%E9%9B%86%E7%BE%A4%E6%B6%88%E8%B4%B9) - - [2.1.10 RocketMQ 不使用 ZooKeeper 作为注册中心的原因,以及自制的 NameServer 优缺点?](#2110-rocketmq-%E4%B8%8D%E4%BD%BF%E7%94%A8-zookeeper-%E4%BD%9C%E4%B8%BA%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83%E7%9A%84%E5%8E%9F%E5%9B%A0%E4%BB%A5%E5%8F%8A%E8%87%AA%E5%88%B6%E7%9A%84-nameserver-%E4%BC%98%E7%BC%BA%E7%82%B9) - - [2.1.11 其它](#2111-%E5%85%B6%E5%AE%83) -- [3 参考](#3-%E5%8F%82%E8%80%83) - - - -# 1 单机版消息中心 - -一个消息中心,最基本的需要支持多生产者、多消费者,例如下: - -```java -class Scratch { - - public static void main(String[] args) { - // 实际中会有 nameserver 服务来找到 broker 具体位置以及 broker 主从信息 - Broker broker = new Broker(); - Producer producer1 = new Producer(); - producer1.connectBroker(broker); - Producer producer2 = new Producer(); - producer2.connectBroker(broker); - - Consumer consumer1 = new Consumer(); - consumer1.connectBroker(broker); - Consumer consumer2 = new Consumer(); - consumer2.connectBroker(broker); - - for (int i = 0; i < 2; i++) { - producer1.asyncSendMsg("producer1 send msg" + i); - producer2.asyncSendMsg("producer2 send msg" + i); - } - System.out.println("broker has msg:" + broker.getAllMagByDisk()); - - for (int i = 0; i < 1; i++) { - System.out.println("consumer1 consume msg:" + consumer1.syncPullMsg()); - } - for (int i = 0; i < 3; i++) { - System.out.println("consumer2 consume msg:" + consumer2.syncPullMsg()); - } - } - -} - -class Producer { - - private Broker broker; - - public void connectBroker(Broker broker) { - this.broker = broker; - } - - public void asyncSendMsg(String msg) { - if (broker == null) { - throw new RuntimeException("please connect broker first"); - } - new Thread(() -> { - broker.sendMsg(msg); - }).start(); - } -} - -class Consumer { - private Broker broker; - - public void connectBroker(Broker broker) { - this.broker = broker; - } - - public String syncPullMsg() { - return broker.getMsg(); - } - -} - -class Broker { - - // 对应 RocketMQ 中 MessageQueue,默认情况下 1 个 Topic 包含 4 个 MessageQueue - private LinkedBlockingQueue messageQueue = new LinkedBlockingQueue(Integer.MAX_VALUE); - - // 实际发送消息到 broker 服务器使用 Netty 发送 - public void sendMsg(String msg) { - try { - messageQueue.put(msg); - // 实际会同步或异步落盘,异步落盘使用的定时任务定时扫描落盘 - } catch (InterruptedException e) { - - } - } - - public String getMsg() { - try { - return messageQueue.take(); - } catch (InterruptedException e) { - - } - return null; - } - - public String getAllMagByDisk() { - StringBuilder sb = new StringBuilder("\n"); - messageQueue.iterator().forEachRemaining((msg) -> { - sb.append(msg + "\n"); - }); - return sb.toString(); - } -} -``` - -问题: -1. 没有实现真正执行消息存储落盘 -2. 没有实现 NameServer 去作为注册中心,定位服务 -3. 使用 LinkedBlockingQueue 作为消息队列,注意,参数是无限大,在真正 RocketMQ 也是如此是无限大,理论上不会出现对进来的数据进行抛弃,但是会有内存泄漏问题(阿里巴巴开发手册也因为这个问题,建议我们使用自制线程池) -4. 没有使用多个队列(即多个 LinkedBlockingQueue),RocketMQ 的顺序消息是通过生产者和消费者同时使用同一个 MessageQueue 来实现,但是如果我们只有一个 MessageQueue,那我们天然就支持顺序消息 -5. 没有使用 MappedByteBuffer 来实现文件映射从而使消息数据落盘非常的快(实际 RocketMQ 使用的是 FileChannel+DirectBuffer) - -# 2 分布式消息中心 - -## 2.1 问题与解决 - -### 2.1.1 消息丢失的问题 - -1. 当你系统需要保证百分百消息不丢失,你可以使用生产者每发送一个消息,Broker 同步返回一个消息发送成功的反馈消息 -2. 即每发送一个消息,同步落盘后才返回生产者消息发送成功,这样只要生产者得到了消息发送生成的返回,事后除了硬盘损坏,都可以保证不会消息丢失 -3. 但是这同时引入了一个问题,同步落盘怎么才能快? - -### 2.1.2 同步落盘怎么才能快 - -1. 使用 FileChannel + DirectBuffer 池,使用堆外内存,加快内存拷贝 -2. 使用数据和索引分离,当消息需要写入时,使用 commitlog 文件顺序写,当需要定位某个消息时,查询index 文件来定位,从而减少文件IO随机读写的性能损耗 - -### 2.1.3 消息堆积的问题 - -1. 后台定时任务每隔72小时,删除旧的没有使用过的消息信息 -2. 根据不同的业务实现不同的丢弃任务,具体参考线程池的 AbortPolicy,例如FIFO/LRU等(RocketMQ没有此策略) -3. 消息定时转移,或者对某些重要的 TAG 型(支付型)消息真正落库 - -### 2.1.4 定时消息的实现 - -1. 实际 RocketMQ 没有实现任意精度的定时消息,它只支持某些特定的时间精度的定时消息 -2. 实现定时消息的原理是:创建特定时间精度的 MessageQueue,例如生产者需要定时1s之后被消费者消费,你只需要将此消息发送到特定的 Topic,例如:MessageQueue-1 表示这个 MessageQueue 里面的消息都会延迟一秒被消费,然后 Broker 会在 1s 后发送到消费者消费此消息,使用 newSingleThreadScheduledExecutor 实现 - -### 2.1.5 顺序消息的实现 - -1. 与定时消息同原理,生产者生产消息时指定特定的 MessageQueue ,消费者消费消息时,消费特定的 MessageQueue,其实单机版的消息中心在一个 MessageQueue 就天然支持了顺序消息 -2. 注意:同一个 MessageQueue 保证里面的消息是顺序消费的前提是:消费者是串行的消费该 MessageQueue,因为就算 MessageQueue 是顺序的,但是当并行消费时,还是会有顺序问题,但是串行消费也同时引入了两个问题: ->1. 引入锁来实现串行 ->2. 前一个消费阻塞时后面都会被阻塞 - -### 2.1.6 分布式消息的实现 - -1. 需要前置知识:2PC -2. RocketMQ4.3 起支持,原理为2PC,即两阶段提交,prepared->commit/rollback -3. 生产者发送事务消息,假设该事务消息 Topic 为 Topic1-Trans,Broker 得到后首先更改该消息的 Topic 为 Topic1-Prepared,该 Topic1-Prepared 对消费者不可见。然后定时回调生产者的本地事务A执行状态,根据本地事务A执行状态,来是否将该消息修改为 Topic1-Commit 或 Topic1-Rollback,消费者就可以正常找到该事务消息或者不执行等 - ->注意,就算是事务消息最后回滚了也不会物理删除,只会逻辑删除该消息 - -### 2.1.7 消息的 push 实现 - -1. 注意,RocketMQ 已经说了自己会有低延迟问题,其中就包括这个消息的 push 延迟问题 -2. 因为这并不是真正的将消息主动的推送到消费者,而是 Broker 定时任务每5s将消息推送到消费者 - -### 2.1.8 消息重复发送的避免 - -1. RocketMQ 会出现消息重复发送的问题,因为在网络延迟的情况下,这种问题不可避免的发生,如果非要实现消息不可重复发送,那基本太难,因为网络环境无法预知,还会使程序复杂度加大,因此默认允许消息重复发送 -2. RocketMQ 让使用者在消费者端去解决该问题,即需要消费者端在消费消息时支持幂等性的去消费消息 -3. 最简单的解决方案是每条消费记录有个消费状态字段,根据这个消费状态字段来是否消费或者使用一个集中式的表,来存储所有消息的消费状态,从而避免重复消费 -4. 具体实现可以查询关于消息幂等消费的解决方案 - -### 2.1.9 广播消费与集群消费 - -1. 消息消费区别:广播消费,订阅该 Topic 的消息者们都会消费**每个**消息。集群消费,订阅该 Topic 的消息者们只会有一个去消费**某个**消息 -2. 消息落盘区别:具体表现在消息消费进度的保存上。广播消费,由于每个消费者都独立的去消费每个消息,因此每个消费者各自保存自己的消息消费进度。而集群消费下,订阅了某个 Topic,而旗下又有多个 MessageQueue,每个消费者都可能会去消费不同的 MessageQueue,因此总体的消费进度保存在 Broker 上集中的管理 - -### 2.1.10 RocketMQ 不使用 ZooKeeper 作为注册中心的原因,以及自制的 NameServer 优缺点? - -1. ZooKeeper 作为支持顺序一致性的中间件,在某些情况下,它为了满足一致性,会丢失一定时间内的可用性,RocketMQ 需要注册中心只是为了发现组件地址,在某些情况下,RocketMQ 的注册中心可以出现数据不一致性,这同时也是 NameServer 的缺点,因为 NameServer 集群间互不通信,它们之间的注册信息可能会不一致 -2. 另外,当有新的服务器加入时,NameServer 并不会立马通知到 Produer,而是由 Produer 定时去请求 NameServer 获取最新的 Broker/Consumer 信息(这种情况是通过 Producer 发送消息时,负载均衡解决) - -### 2.1.11 其它 - -![][1] - -加分项咯 -1. 包括组件通信间使用 Netty 的自定义协议 -2. 消息重试负载均衡策略(具体参考 Dubbo 负载均衡策略) -3. 消息过滤器(Producer 发送消息到 Broker,Broker 存储消息信息,Consumer 消费时请求 Broker 端从磁盘文件查询消息文件时,在 Broker 端就使用过滤服务器进行过滤) -4. Broker 同步双写和异步双写中 Master 和 Slave 的交互 -5. Broker 在 4.5.0 版本更新中引入了基于 Raft 协议的多副本选举,之前这是商业版才有的特性 [ISSUE-1046][2] - -# 3 参考 - -1. 《RocketMQ技术内幕》:https://blog.csdn.net/prestigeding/article/details/85233529 -2. 关于 RocketMQ 对 MappedByteBuffer 的一点优化:https://lishoubo.github.io/2017/09/27/MappedByteBuffer%E7%9A%84%E4%B8%80%E7%82%B9%E4%BC%98%E5%8C%96/ -3. 阿里中间件团队博客-十分钟入门RocketMQ:http://jm.taobao.org/2017/01/12/rocketmq-quick-start-in-10-minutes/ -4. 分布式事务的种类以及 RocketMQ 支持的分布式消息:https://www.infoq.cn/article/2018/08/rocketmq-4.3-release -5. 滴滴出行基于RocketMQ构建企业级消息队列服务的实践:https://yq.aliyun.com/articles/664608 -6. 基于《RocketMQ技术内幕》源码注释:https://github.com/LiWenGu/awesome-rocketmq - -[1]: https://leran2deeplearnjavawebtech.oss-cn-beijing.aliyuncs.com/somephoto/RocketMQ%E6%B5%81%E7%A8%8B.png -[2]: http://rocketmq.apache.org/release_notes/release-notes-4.5.0/ diff --git a/docs/system-design/data-communication/RocketMQ.md b/docs/system-design/data-communication/RocketMQ.md deleted file mode 100644 index c74fab6..0000000 --- a/docs/system-design/data-communication/RocketMQ.md +++ /dev/null @@ -1,454 +0,0 @@ -> 文章很长,点赞再看,养成好习惯😋😋😋 -> -> [本文由 FrancisQ 老哥投稿!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485969&idx=1&sn=6bd53abde30d42a778d5a35ec104428c&chksm=cea245daf9d5cccce631f93115f0c2c4a7634e55f5bef9009fd03f5a0ffa55b745b5ef4f0530&token=294077121&lang=zh_CN#rd) - -## 消息队列扫盲 - -消息队列顾名思义就是存放消息的队列,队列我就不解释了,别告诉我你连队列都不知道似啥吧? - -所以问题并不是消息队列是什么,而是 **消息队列为什么会出现?消息队列能用来干什么?用它来干这些事会带来什么好处?消息队列会带来副作用吗?** - -### 消息队列为什么会出现? - -消息队列算是作为后端程序员的一个必备技能吧,因为**分布式应用必定涉及到各个系统之间的通信问题**,这个时候消息队列也应运而生了。可以说分布式的产生是消息队列的基础,而分布式怕是一个很古老的概念了吧,所以消息队列也是一个很古老的中间件了。 - -### 消息队列能用来干什么? - -#### 异步 - -你可能会反驳我,应用之间的通信又不是只能由消息队列解决,好好的通信为什么中间非要插一个消息队列呢?我不能直接进行通信吗? - -很好👍,你又提出了一个概念,**同步通信**。就比如现在业界使用比较多的 `Dubbo` 就是一个适用于各个系统之间同步通信的 `RPC` 框架。 - -我来举个🌰吧,比如我们有一个购票系统,需求是用户在购买完之后能接收到购买完成的短信。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef37fee7e09230.jpg) - -我们省略中间的网络通信时间消耗,假如购票系统处理需要 150ms ,短信系统处理需要 200ms ,那么整个处理流程的时间消耗就是 150ms + 200ms = 350ms。 - -当然,乍看没什么问题。可是仔细一想你就感觉有点问题,我用户购票在购票系统的时候其实就已经完成了购买,而我现在通过同步调用非要让整个请求拉长时间,而短息系统这玩意又不是很有必要,它仅仅是一个辅助功能增强用户体验感而已。我现在整个调用流程就有点 **头重脚轻** 的感觉了,购票是一个不太耗时的流程,而我现在因为同步调用,非要等待发送短信这个比较耗时的操作才返回结果。那我如果再加一个发送邮件呢? - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef380429cf373e.jpg) - -这样整个系统的调用链又变长了,整个时间就变成了550ms。 - -当我们在学生时代需要在食堂排队的时候,我们和食堂大妈就是一个同步的模型。 - -我们需要告诉食堂大妈:“姐姐,给我加个鸡腿,再加个酸辣土豆丝,帮我浇点汁上去,多打点饭哦😋😋😋” 咦~~~ 为了多吃点,真恶心。 - -然后大妈帮我们打饭配菜,我们看着大妈那颤抖的手和掉落的土豆丝不禁咽了咽口水。 - -最终我们从大妈手中接过饭菜然后去寻找座位了... - -回想一下,我们在给大妈发送需要的信息之后我们是 **同步等待大妈给我配好饭菜** 的,上面我们只是加了鸡腿和土豆丝,万一我再加一个番茄牛腩,韭菜鸡蛋,这样是不是大妈打饭配菜的流程就会变长,我们等待的时间也会相应的变长。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/006APoFYly1fvd9cwjlfrj30as0b03ym.jpg) - -那后来,我们工作赚钱了有钱去饭店吃饭了,我们告诉服务员来一碗牛肉面加个荷包蛋 **(传达一个消息)** ,然后我们就可以在饭桌上安心的玩手机了 **(干自己其他事情)** ,等到我们的牛肉面上了我们就可以吃了。这其中我们也就传达了一个消息,然后我们又转过头干其他事情了。这其中虽然做面的时间没有变短,但是我们只需要传达一个消息就可以看其他事情了,这是一个 **异步** 的概念。 - -所以,为了解决这一个问题,聪明的程序员在中间也加了个类似于服务员的中间件——消息队列。这个时候我们就可以把模型给改造了。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38124f55eaea.jpg) - -这样,我们在将消息存入消息队列之后我们就可以直接返回了(我们告诉服务员我们要吃什么然后玩手机),所以整个耗时只是 150ms + 10ms = 160ms。 - -> 但是你需要注意的是,整个流程的时长是没变的,就像你仅仅告诉服务员要吃什么是不会影响到做面的速度的。 - -#### 解耦 - -回到最初同步调用的过程,我们写个伪代码简单概括一下。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef381a505d3e1f.jpg) - -那么第二步,我们又添加了一个发送邮件,我们就得重新去修改代码,如果我们又加一个需求:用户购买完还需要给他加积分,这个时候我们是不是又得改代码? - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef381c4e1b1ac7.jpg) - -如果你觉得还行,那么我这个时候不要发邮件这个服务了呢,我是不是又得改代码,又得重启应用? - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef381f273a66bd.jpg) - -这样改来改去是不是很麻烦,那么 **此时我们就用一个消息队列在中间进行解耦** 。你需要注意的是,我们后面的发送短信、发送邮件、添加积分等一些操作都依赖于上面的 `result` ,这东西抽象出来就是购票的处理结果呀,比如订单号,用户账号等等,也就是说我们后面的一系列服务都是需要同样的消息来进行处理。既然这样,我们是不是可以通过 **“广播消息”** 来实现。 - -我上面所讲的“广播”并不是真正的广播,而是接下来的系统作为消费者去 **订阅** 特定的主题。比如我们这里的主题就可以叫做 `订票` ,我们购买系统作为一个生产者去生产这条消息放入消息队列,然后消费者订阅了这个主题,会从消息队列中拉取消息并消费。就比如我们刚刚画的那张图,你会发现,在生产者这边我们只需要关注 **生产消息到指定主题中** ,而 **消费者只需要关注从指定主题中拉取消息** 就行了。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef382674b66892.jpg) - -> 如果没有消息队列,每当一个新的业务接入,我们都要在主系统调用新接口、或者当我们取消某些业务,我们也得在主系统删除某些接口调用。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,接下来收到消息如何处理,是下游的事情,无疑极大地减少了开发和联调的工作量。 - -#### 削峰 - -我们再次回到一开始我们使用同步调用系统的情况,并且思考一下,如果此时有大量用户请求购票整个系统会变成什么样? - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef382a9756bb1c.jpg) - -如果,此时有一万的请求进入购票系统,我们知道运行我们主业务的服务器配置一般会比较好,所以这里我们假设购票系统能承受这一万的用户请求,那么也就意味着我们同时也会出现一万调用发短信服务的请求。而对于短信系统来说并不是我们的主要业务,所以我们配备的硬件资源并不会太高,那么你觉得现在这个短信系统能承受这一万的峰值么,且不说能不能承受,系统会不会 **直接崩溃** 了? - -短信业务又不是我们的主业务,我们能不能 **折中处理** 呢?如果我们把购买完成的信息发送到消息队列中,而短信系统 **尽自己所能地去消息队列中取消息和消费消息** ,即使处理速度慢一点也无所谓,只要我们的系统没有崩溃就行了。 - -留得江山在,还怕没柴烧?你敢说每次发送验证码的时候是一发你就收到了的么? - -#### 消息队列能带来什么好处? - -其实上面我已经说了。**异步、解耦、削峰。** 哪怕你上面的都没看懂也千万要记住这六个字,因为他不仅是消息队列的精华,更是编程和架构的精华。 - -#### 消息队列会带来副作用吗? - -没有哪一门技术是“银弹”,消息队列也有它的副作用。 - -比如,本来好好的两个系统之间的调用,我中间加了个消息队列,如果消息队列挂了怎么办呢?是不是 **降低了系统的可用性** ? - -那这样是不是要保证HA(高可用)?是不是要搞集群?那么我 **整个系统的复杂度是不是上升了** ? - -抛开上面的问题不讲,万一我发送方发送失败了,然后执行重试,这样就可能产生重复的消息。 - -或者我消费端处理失败了,请求重发,这样也会产生重复的消息。 - -对于一些微服务来说,消费重复消息会带来更大的麻烦,比如增加积分,这个时候我加了多次是不是对其他用户不公平? - -那么,又 **如何解决重复消费消息的问题** 呢? - -如果我们此时的消息需要保证严格的顺序性怎么办呢?比如生产者生产了一系列的有序消息(对一个id为1的记录进行删除增加修改),但是我们知道在发布订阅模型中,对于主题是无顺序的,那么这个时候就会导致对于消费者消费消息的时候没有按照生产者的发送顺序消费,比如这个时候我们消费的顺序为修改删除增加,如果该记录涉及到金额的话是不是会出大事情? - -那么,又 **如何解决消息的顺序消费问题** 呢? - -就拿我们上面所讲的分布式系统来说,用户购票完成之后是不是需要增加账户积分?在同一个系统中我们一般会使用事务来进行解决,如果用 `Spring` 的话我们在上面伪代码中加入 `@Transactional` 注解就好了。但是在不同系统中如何保证事务呢?总不能这个系统我扣钱成功了你那积分系统积分没加吧?或者说我这扣钱明明失败了,你那积分系统给我加了积分。 - -那么,又如何 **解决分布式事务问题** 呢? - -我们刚刚说了,消息队列可以进行削峰操作,那如果我的消费者如果消费很慢或者生产者生产消息很快,这样是不是会将消息堆积在消息队列中? - -那么,又如何 **解决消息堆积的问题** 呢? - -可用性降低,复杂度上升,又带来一系列的重复消费,顺序消费,分布式事务,消息堆积的问题,这消息队列还怎么用啊😵? - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef382d709abc9d.png) - -别急,办法总是有的。 - -## RocketMQ是什么? - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef383014430799.jpg) - -哇,你个混蛋!上面给我抛出那么多问题,你现在又讲 `RocketMQ` ,还让不让人活了?!🤬 - -别急别急,话说你现在清楚 `MQ` 的构造吗,我还没讲呢,我们先搞明白 `MQ` 的内部构造,再来看看如何解决上面的一系列问题吧,不过你最好带着问题去阅读和了解喔。 - -`RocketMQ` 是一个 **队列模型** 的消息中间件,具有**高性能、高可靠、高实时、分布式** 的特点。它是一个采用 `Java` 语言开发的分布式的消息系统,由阿里巴巴团队开发,在2016年底贡献给 `Apache`,成为了 `Apache` 的一个顶级项目。 在阿里内部,`RocketMQ` 很好地服务了集团大大小小上千个应用,在每年的双十一当天,更有不可思议的万亿级消息通过 `RocketMQ` 流转。 - -废话不多说,想要了解 `RocketMQ` 历史的同学可以自己去搜寻资料。听完上面的介绍,你只要知道 `RocketMQ` 很快、很牛、而且经历过双十一的实践就行了! - -## 队列模型和主题模型 - -在谈 `RocketMQ` 的技术架构之前,我们先来了解一下两个名词概念——**队列模型** 和 **主题模型** 。 - -首先我问一个问题,消息队列为什么要叫消息队列? - -你可能觉得很弱智,这玩意不就是存放消息的队列嘛?不叫消息队列叫什么? - -的确,早期的消息中间件是通过 **队列** 这一模型来实现的,可能是历史原因,我们都习惯把消息中间件成为消息队列。 - -但是,如今例如 `RocketMQ` 、`Kafka` 这些优秀的消息中间件不仅仅是通过一个 **队列** 来实现消息存储的。 - -### 队列模型 - -就像我们理解队列一样,消息中间件的队列模型就真的只是一个队列。。。我画一张图给大家理解。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3834ae653469.jpg) - -在一开始我跟你提到了一个 **“广播”** 的概念,也就是说如果我们此时我们需要将一个消息发送给多个消费者(比如此时我需要将信息发送给短信系统和邮件系统),这个时候单个队列即不能满足需求了。 - -当然你可以让 `Producer` 生产消息放入多个队列中,然后每个队列去对应每一个消费者。问题是可以解决,创建多个队列并且复制多份消息是会很影响资源和性能的。而且,这样子就会导致生产者需要知道具体消费者个数然后去复制对应数量的消息队列,这就违背我们消息中间件的 **解耦** 这一原则。 - -### 主题模型 - -那么有没有好的方法去解决这一个问题呢?有,那就是 **主题模型** 或者可以称为 **发布订阅模型** 。 - -> 感兴趣的同学可以去了解一下设计模式里面的观察者模式并且手动实现一下,我相信你会有所收获的。 - -在主题模型中,消息的生产者称为 **发布者(Publisher)** ,消息的消费者称为 **订阅者(Subscriber)** ,存放消息的容器称为 **主题(Topic)** 。 - -其中,发布者将消息发送到指定主题中,订阅者需要 **提前订阅主题** 才能接受特定主题的消息。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3837887d9a54sds.jpg) - -### RocketMQ中的消息模型 - -`RockerMQ` 中的消息模型就是按照 **主题模型** 所实现的。你可能会好奇这个 **主题** 到底是怎么实现的呢?你上面也没有讲到呀! - -其实对于主题模型的实现来说每个消息中间件的底层设计都是不一样的,就比如 `Kafka` 中的 **分区** ,`RocketMQ` 中的 **队列** ,`RabbitMQ` 中的 `Exchange` 。我们可以理解为 **主题模型/发布订阅模型** 就是一个标准,那些中间件只不过照着这个标准去实现而已。 - -所以,`RocketMQ` 中的 **主题模型** 到底是如何实现的呢?首先我画一张图,大家尝试着去理解一下。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef383d3e8c9788.jpg) - -我们可以看到在整个图中有 `Producer Group` 、`Topic` 、`Consumer Group` 三个角色,我来分别介绍一下他们。 - -- `Producer Group` 生产者组: 代表某一类的生产者,比如我们有多个秒杀系统作为生产者,这多个合在一起就是一个 `Producer Group` 生产者组,它们一般生产相同的消息。 -- `Consumer Group` 消费者组: 代表某一类的消费者,比如我们有多个短信系统作为消费者,这多个合在一起就是一个 `Consumer Group` 消费者组,它们一般消费相同的消息。 -- `Topic` 主题: 代表一类消息,比如订单消息,物流消息等等。 - -你可以看到图中生产者组中的生产者会向主题发送消息,而 **主题中存在多个队列**,生产者每次生产消息之后是指定主题中的某个队列发送消息的。 - -每个主题中都有多个队列(这里还不涉及到 `Broker`),集群消费模式下,一个消费者集群多台机器共同消费一个 `topic` 的多个队列,**一个队列只会被一个消费者消费**。如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。就像上图中 `Consumer1` 和 `Consumer2` 分别对应着两个队列,而 `Consuer3` 是没有队列对应的,所以一般来讲要控制 **消费者组中的消费者个数和主题中队列个数相同** 。 - -当然也可以消费者个数小于队列个数,只不过不太建议。如下图。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3850c808d707.jpg) - -**每个消费组在每个队列上维护一个消费位置** ,为什么呢? - -因为我们刚刚画的仅仅是一个消费者组,我们知道在发布订阅模式中一般会涉及到多个消费者组,而每个消费者组在每个队列中的消费位置都是不同的。如果此时有多个消费者组,那么消息被一个消费者组消费完之后是不会删除的(因为其它消费者组也需要呀),它仅仅是为每个消费者组维护一个 **消费位移(offset)** ,每次消费者组消费完会返回一个成功的响应,然后队列再把维护的消费位移加一,这样就不会出现刚刚消费过的消息再一次被消费了。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3857fefaa079.jpg) - -可能你还有一个问题,**为什么一个主题中需要维护多个队列** ? - -答案是 **提高并发能力** 。的确,每个主题中只存在一个队列也是可行的。你想一下,如果每个主题中只存在一个队列,这个队列中也维护着每个消费者组的消费位置,这样也可以做到 **发布订阅模式** 。如下图。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38600cdb6d4b.jpg) - -但是,这样我生产者是不是只能向一个队列发送消息?又因为需要维护消费位置所以一个队列只能对应一个消费者组中的消费者,这样是不是其他的 `Consumer` 就没有用武之地了?从这两个角度来讲,并发度一下子就小了很多。 - -所以总结来说,`RocketMQ` 通过**使用在一个 `Topic` 中配置多个队列并且每个队列维护每个消费者组的消费位置** 实现了 **主题模式/发布订阅模式** 。 - -## RocketMQ的架构图 - -讲完了消息模型,我们理解起 `RocketMQ` 的技术架构起来就容易多了。 - -`RocketMQ` 技术架构中有四大角色 `NameServer` 、`Broker` 、`Producer` 、`Consumer` 。我来向大家分别解释一下这四个角色是干啥的。 - -- `Broker`: 主要负责消息的存储、投递和查询以及服务高可用保证。说白了就是消息队列服务器嘛,生产者生产消息到 `Broker` ,消费者从 `Broker` 拉取消息并消费。 - - 这里,我还得普及一下关于 `Broker` 、`Topic` 和 队列的关系。上面我讲解了 `Topic` 和队列的关系——一个 `Topic` 中存在多个队列,那么这个 `Topic` 和队列存放在哪呢? - - **一个 `Topic` 分布在多个 `Broker`上,一个 `Broker` 可以配置多个 `Topic` ,它们是多对多的关系**。 - - 如果某个 `Topic` 消息量很大,应该给它多配置几个队列(上文中提到了提高并发能力),并且 **尽量多分布在不同 `Broker` 上,以减轻某个 `Broker` 的压力** 。 - - `Topic` 消息量都比较均匀的情况下,如果某个 `broker` 上的队列越多,则该 `broker` 压力越大。 - - ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38687488a5a4.jpg) - - > 所以说我们需要配置多个Broker。 - -- `NameServer`: 不知道你们有没有接触过 `ZooKeeper` 和 `Spring Cloud` 中的 `Eureka` ,它其实也是一个 **注册中心** ,主要提供两个功能:**Broker管理** 和 **路由信息管理** 。说白了就是 `Broker` 会将自己的信息注册到 `NameServer` 中,此时 `NameServer` 就存放了很多 `Broker` 的信息(Broker的路由表),消费者和生产者就从 `NameServer` 中获取路由表然后照着路由表的信息和对应的 `Broker` 进行通信(生产者和消费者定期会向 `NameServer` 去查询相关的 `Broker` 的信息)。 - -- `Producer`: 消息发布的角色,支持分布式集群方式部署。说白了就是生产者。 - -- `Consumer`: 消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制。说白了就是消费者。 - -听完了上面的解释你可能会觉得,这玩意好简单。不就是这样的么? - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef386c6d1e8bdb.jpg) - -嗯?你可能会发现一个问题,这老家伙 `NameServer` 干啥用的,这不多余吗?直接 `Producer` 、`Consumer` 和 `Broker` 直接进行生产消息,消费消息不就好了么? - -但是,我们上文提到过 `Broker` 是需要保证高可用的,如果整个系统仅仅靠着一个 `Broker` 来维持的话,那么这个 `Broker` 的压力会不会很大?所以我们需要使用多个 `Broker` 来保证 **负载均衡** 。 - -如果说,我们的消费者和生产者直接和多个 `Broker` 相连,那么当 `Broker` 修改的时候必定会牵连着每个生产者和消费者,这样就会产生耦合问题,而 `NameServer` 注册中心就是用来解决这个问题的。 - -> 如果还不是很理解的话,可以去看我介绍 `Spring Cloud` 的那篇文章,其中介绍了 `Eureka` 注册中心。 - -当然,`RocketMQ` 中的技术架构肯定不止前面那么简单,因为上面图中的四个角色都是需要做集群的。我给出一张官网的架构图,大家尝试理解一下。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef386fa3be1e53.jpg) - -其实和我们最开始画的那张乞丐版的架构图也没什么区别,主要是一些细节上的差别。听我细细道来🤨。 - -第一、我们的 `Broker` **做了集群并且还进行了主从部署** ,由于消息分布在各个 `Broker` 上,一旦某个 `Broker` 宕机,则该`Broker` 上的消息读写都会受到影响。所以 `Rocketmq` 提供了 `master/slave` 的结构,` salve` 定时从 `master` 同步数据(同步刷盘或者异步刷盘),如果 `master` 宕机,**则 `slave` 提供消费服务,但是不能写入消息** (后面我还会提到哦)。 - -第二、为了保证 `HA` ,我们的 `NameServer` 也做了集群部署,但是请注意它是 **去中心化** 的。也就意味着它没有主节点,你可以很明显地看出 `NameServer` 的所有节点是没有进行 `Info Replicate` 的,在 `RocketMQ` 中是通过 **单个Broker和所有NameServer保持长连接** ,并且在每隔30秒 `Broker` 会向所有 `Nameserver` 发送心跳,心跳包含了自身的 `Topic` 配置信息,这个步骤就对应这上面的 `Routing Info` 。 - -第三、在生产者需要向 `Broker` 发送消息的时候,**需要先从 `NameServer` 获取关于 `Broker` 的路由信息**,然后通过 **轮询** 的方法去向每个队列中生产数据以达到 **负载均衡** 的效果。 - -第四、消费者通过 `NameServer` 获取所有 `Broker` 的路由信息后,向 `Broker` 发送 `Pull` 请求来获取消息数据。`Consumer` 可以以两种模式启动—— **广播(Broadcast)和集群(Cluster)**。广播模式下,一条消息会发送给 **同一个消费组中的所有消费者** ,集群模式下消息只会发送给一个消费者。 - -## 如何解决 顺序消费、重复消费 - -其实,这些东西都是我在介绍消息队列带来的一些副作用的时候提到的,也就是说,这些问题不仅仅挂钩于 `RocketMQ` ,而是应该每个消息中间件都需要去解决的。 - -在上面我介绍 `RocketMQ` 的技术架构的时候我已经向你展示了 **它是如何保证高可用的** ,这里不涉及运维方面的搭建,如果你感兴趣可以自己去官网上照着例子搭建属于你自己的 `RocketMQ` 集群。 - -> 其实 `Kafka` 的架构基本和 `RocketMQ` 类似,只是它注册中心使用了 `Zookeeper` 、它的 **分区** 就相当于 `RocketMQ` 中的 **队列** 。还有一些小细节不同会在后面提到。 - -### 顺序消费 - -在上面的技术架构介绍中,我们已经知道了 **`RocketMQ` 在主题上是无序的、它只有在队列层面才是保证有序** 的。 - -这又扯到两个概念——**普通顺序** 和 **严格顺序** 。 - -所谓普通顺序是指 消费者通过 **同一个消费队列收到的消息是有顺序的** ,不同消息队列收到的消息则可能是无顺序的。普通顺序消息在 `Broker` **重启情况下不会保证消息顺序性** (短暂时间) 。 - -所谓严格顺序是指 消费者收到的 **所有消息** 均是有顺序的。严格顺序消息 **即使在异常情况下也会保证消息的顺序性** 。 - -但是,严格顺序看起来虽好,实现它可会付出巨大的代价。如果你使用严格顺序模式,`Broker` 集群中只要有一台机器不可用,则整个集群都不可用。你还用啥?现在主要场景也就在 `binlog` 同步。 - -一般而言,我们的 `MQ` 都是能容忍短暂的乱序,所以推荐使用普通顺序模式。 - -那么,我们现在使用了 **普通顺序模式** ,我们从上面学习知道了在 `Producer` 生产消息的时候会进行轮询(取决你的负载均衡策略)来向同一主题的不同消息队列发送消息。那么如果此时我有几个消息分别是同一个订单的创建、支付、发货,在轮询的策略下这 **三个消息会被发送到不同队列** ,因为在不同的队列此时就无法使用 `RocketMQ` 带来的队列有序特性来保证消息有序性了。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3874585e096e.jpg) - -那么,怎么解决呢? - -其实很简单,我们需要处理的仅仅是将同一语义下的消息放入同一个队列(比如这里是同一个订单),那我们就可以使用 **Hash取模法** 来保证同一个订单在同一个队列中就行了。 - -### 重复消费 - -emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。比如说,这个时候我们有一个订单的处理积分的系统,每当来一个消息的时候它就负责为创建这个订单的用户的积分加上相应的数值。可是有一次,消息队列发送给订单系统 FrancisQ 的订单信息,其要求是给 FrancisQ 的积分加上 500。但是积分系统在收到 FrancisQ 的订单信息处理完成之后返回给消息队列处理成功的信息的时候出现了网络波动(当然还有很多种情况,比如Broker意外重启等等),这条回应没有发送成功。 - -那么,消息队列没收到积分系统的回应会不会尝试重发这个消息?问题就来了,我再发这个消息,万一它又给 FrancisQ 的账户加上 500 积分怎么办呢? - -所以我们需要给我们的消费者实现 **幂等** ,也就是对同一个消息的处理结果,执行多少次都不变。 - -那么如何给业务实现幂等呢?这个还是需要结合具体的业务的。你可以使用 **写入 `Redis`** 来保证,因为 `Redis` 的 `key` 和 `value` 就是天然支持幂等的。当然还有使用 **数据库插入法** ,基于数据库的唯一键来保证重复数据不会被插入多条。 - -不过最主要的还是需要 **根据特定场景使用特定的解决方案** ,你要知道你的消息消费是否是完全不可重复消费还是可以忍受重复消费的,然后再选择强校验和弱校验的方式。毕竟在 CS 领域还是很少有技术银弹的说法。 - -而在整个互联网领域,幂等不仅仅适用于消息队列的重复消费问题,这些实现幂等的方法,也同样适用于,**在其他场景中来解决重复请求或者重复调用的问题** 。比如将HTTP服务设计成幂等的,**解决前端或者APP重复提交表单数据的问题** ,也可以将一个微服务设计成幂等的,解决 `RPC` 框架自动重试导致的 **重复调用问题** 。 - -## 分布式事务 - -如何解释分布式事务呢?事务大家都知道吧?**要么都执行要么都不执行** 。在同一个系统中我们可以轻松地实现事务,但是在分布式架构中,我们有很多服务是部署在不同系统之间的,而不同服务之间又需要进行调用。比如此时我下订单然后增加积分,如果保证不了分布式事务的话,就会出现A系统下了订单,但是B系统增加积分失败或者A系统没有下订单,B系统却增加了积分。前者对用户不友好,后者对运营商不利,这是我们都不愿意见到的。 - -那么,如何去解决这个问题呢? - -如今比较常见的分布式事务实现有 2PC、TCC 和事务消息(half 半消息机制)。每一种实现都有其特定的使用场景,但是也有各自的问题,**都不是完美的解决方案**。 - -在 `RocketMQ` 中使用的是 **事务消息加上事务反查机制** 来解决分布式事务问题的。我画了张图,大家可以对照着图进行理解。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38798d7a987f.png) - -在第一步发送的 half 消息 ,它的意思是 **在事务提交之前,对于消费者来说,这个消息是不可见的** 。 - -> 那么,如何做到写入消息但是对用户不可见呢?RocketMQ事务消息的做法是:如果消息是half消息,将备份原消息的主题与消息消费队列,然后 **改变主题** 为RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费half类型的消息,**然后RocketMQ会开启一个定时任务,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取消息进行消费**,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。 - -你可以试想一下,如果没有从第5步开始的 **事务反查机制** ,如果出现网路波动第4步没有发送成功,这样就会产生 MQ 不知道是不是需要给消费者消费的问题,他就像一个无头苍蝇一样。在 `RocketMQ` 中就是使用的上述的事务反查来解决的,而在 `Kafka` 中通常是直接抛出一个异常让用户来自行解决。 - -你还需要注意的是,在 `MQ Server` 指向系统B的操作已经和系统A不相关了,也就是说在消息队列中的分布式事务是——**本地事务和存储消息到消息队列才是同一个事务**。这样也就产生了事务的**最终一致性**,因为整个过程是异步的,**每个系统只要保证它自己那一部分的事务就行了**。 - -## 消息堆积问题 - -在上面我们提到了消息队列一个很重要的功能——**削峰** 。那么如果这个峰值太大了导致消息堆积在队列中怎么办呢? - -其实这个问题可以将它广义化,因为产生消息堆积的根源其实就只有两个——生产者生产太快或者消费者消费太慢。 - -我们可以从多个角度去思考解决这个问题,当流量到峰值的时候是因为生产者生产太快,我们可以使用一些 **限流降级** 的方法,当然你也可以增加多个消费者实例去水平扩展增加消费能力来匹配生产的激增。如果消费者消费过慢的话,我们可以先检查 **是否是消费者出现了大量的消费错误** ,或者打印一下日志查看是否是哪一个线程卡死,出现了锁资源不释放等等的问题。 - -> 当然,最快速解决消息堆积问题的方法还是增加消费者实例,不过 **同时你还需要增加每个主题的队列数量** 。 -> -> 别忘了在 `RocketMQ` 中,**一个队列只会被一个消费者消费** ,如果你仅仅是增加消费者实例就会出现我一开始给你画架构图的那种情况。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef387d939ab66d.jpg) - -## 回溯消费 - -回溯消费是指 `Consumer` 已经消费成功的消息,由于业务上需求需要重新消费,在`RocketMQ` 中, `Broker` 在向`Consumer` 投递成功消息后,**消息仍然需要保留** 。并且重新消费一般是按照时间维度,例如由于 `Consumer` 系统故障,恢复后需要重新消费1小时前的数据,那么 `Broker` 要提供一种机制,可以按照时间维度来回退消费进度。`RocketMQ` 支持按照时间回溯消费,时间维度精确到毫秒。 - -这是官方文档的解释,我直接照搬过来就当科普了😁😁😁。 - -## RocketMQ 的刷盘机制 - -上面我讲了那么多的 `RocketMQ` 的架构和设计原理,你有没有好奇 - -在 `Topic` 中的 **队列是以什么样的形式存在的?** - -**队列中的消息又是如何进行存储持久化的呢?** - -我在上文中提到的 **同步刷盘** 和 **异步刷盘** 又是什么呢?它们会给持久化带来什么样的影响呢? - -下面我将给你们一一解释。 - -### 同步刷盘和异步刷盘 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef387fba311cda.jpg) - -如上图所示,在同步刷盘中需要等待一个刷盘成功的 `ACK` ,同步刷盘对 `MQ` 消息可靠性来说是一种不错的保障,但是 **性能上会有较大影响** ,一般地适用于金融等特定业务场景。 - -而异步刷盘往往是开启一个线程去异步地执行刷盘操作。消息刷盘采用后台异步线程提交的方式进行, **降低了读写延迟** ,提高了 `MQ` 的性能和吞吐量,一般适用于如发验证码等对于消息保证要求不太高的业务场景。 - -一般地,**异步刷盘只有在 `Broker` 意外宕机的时候会丢失部分数据**,你可以设置 `Broker` 的参数 `FlushDiskType` 来调整你的刷盘策略(ASYNC_FLUSH 或者 SYNC_FLUSH)。 - -### 同步复制和异步复制 - -上面的同步刷盘和异步刷盘是在单个结点层面的,而同步复制和异步复制主要是指的 `Borker` 主从模式下,主节点返回消息给客户端的时候是否需要同步从节点。 - -- 同步复制: 也叫 “同步双写”,也就是说,**只有消息同步双写到主从结点上时才返回写入成功** 。 -- 异步复制: **消息写入主节点之后就直接返回写入成功** 。 - -然而,很多事情是没有完美的方案的,就比如我们进行消息写入的节点越多就更能保证消息的可靠性,但是随之的性能也会下降,所以需要程序员根据特定业务场景去选择适应的主从复制方案。 - -那么,**异步复制会不会也像异步刷盘那样影响消息的可靠性呢?** - -答案是不会的,因为两者就是不同的概念,对于消息可靠性是通过不同的刷盘策略保证的,而像异步同步复制策略仅仅是影响到了 **可用性** 。为什么呢?其主要原因**是 `RocketMQ` 是不支持自动主从切换的,当主节点挂掉之后,生产者就不能再给这个主节点生产消息了**。 - -比如这个时候采用异步复制的方式,在主节点还未发送完需要同步的消息的时候主节点挂掉了,这个时候从节点就少了一部分消息。但是此时生产者无法再给主节点生产消息了,**消费者可以自动切换到从节点进行消费**(仅仅是消费),所以在主节点挂掉的时间只会产生主从结点短暂的消息不一致的情况,降低了可用性,而当主节点重启之后,从节点那部分未来得及复制的消息还会继续复制。 - -在单主从架构中,如果一个主节点挂掉了,那么也就意味着整个系统不能再生产了。那么这个可用性的问题能否解决呢?**一个主从不行那就多个主从的呗**,别忘了在我们最初的架构图中,每个 `Topic` 是分布在不同 `Broker` 中的。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38687488a5a4.jpg) - -但是这种复制方式同样也会带来一个问题,那就是无法保证 **严格顺序** 。在上文中我们提到了如何保证的消息顺序性是通过将一个语义的消息发送在同一个队列中,使用 `Topic` 下的队列来保证顺序性的。如果此时我们主节点A负责的是订单A的一系列语义消息,然后它挂了,这样其他节点是无法代替主节点A的,如果我们任意节点都可以存入任何消息,那就没有顺序性可言了。 - -而在 `RocketMQ` 中采用了 `Dledger` 解决这个问题。他要求在写入消息的时候,要求**至少消息复制到半数以上的节点之后**,才给客⼾端返回写⼊成功,并且它是⽀持通过选举来动态切换主节点的。这里我就不展开说明了,读者可以自己去了解。 - -> 也不是说 `Dledger` 是个完美的方案,至少在 `Dledger` 选举过程中是无法提供服务的,而且他必须要使用三个节点或以上,如果多数节点同时挂掉他也是无法保证可用性的,而且要求消息复制板书以上节点的效率和直接异步复制还是有一定的差距的。 - -### 存储机制 - -还记得上面我们一开始的三个问题吗?到这里第三个问题已经解决了。 - -但是,在 `Topic` 中的 **队列是以什么样的形式存在的?队列中的消息又是如何进行存储持久化的呢?** 还未解决,其实这里涉及到了 `RocketMQ` 是如何设计它的存储结构了。我首先想大家介绍 `RocketMQ` 消息存储架构中的三大角色——`CommitLog` 、`ConsumeQueue` 和 `IndexFile` 。 - -- `CommitLog`: **消息主体以及元数据的存储主体**,存储 `Producer` 端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是**顺序写入日志文件**,当文件满了,写入下一个文件。 -- `ConsumeQueue`: 消息消费队列,**引入的目的主要是提高消息消费的性能**(我们再前面也讲了),由于`RocketMQ` 是基于主题 `Topic` 的订阅模式,消息消费是针对主题进行的,如果要遍历 `commitlog` 文件中根据 `Topic` 检索消息是非常低效的。`Consumer` 即可根据 `ConsumeQueue` 来查找待消费的消息。其中,`ConsumeQueue`(逻辑消费队列)**作为消费消息的索引**,保存了指定 `Topic` 下的队列消息在 `CommitLog` 中的**起始物理偏移量 `offset` **,消息大小 `size` 和消息 `Tag` 的 `HashCode` 值。**`consumequeue` 文件可以看成是基于 `topic` 的 `commitlog` 索引文件**,故 `consumequeue` 文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样 `consumequeue` 文件采取定长设计,每一个条目共20个字节,分别为8字节的 `commitlog` 物理偏移量、4字节的消息长度、8字节tag `hashcode`,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个 `ConsumeQueue`文件大小约5.72M; -- `IndexFile`: `IndexFile`(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。这里只做科普不做详细介绍。 - -总结来说,整个消息存储的结构,最主要的就是 `CommitLoq` 和 `ConsumeQueue` 。而 `ConsumeQueue` 你可以大概理解为 `Topic` 中的队列。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3884c02acc72.png) - -`RocketMQ` 采用的是 **混合型的存储结构** ,即为 `Broker` 单个实例下所有的队列共用一个日志数据文件来存储消息。有意思的是在同样高并发的 `Kafka` 中会为每个 `Topic` 分配一个存储文件。这就有点类似于我们有一大堆书需要装上书架,`RockeMQ` 是不分书的种类直接成批的塞上去的,而 `Kafka` 是将书本放入指定的分类区域的。 - -而 `RocketMQ` 为什么要这么做呢?原因是 **提高数据的写入效率** ,不分 `Topic` 意味着我们有更大的几率获取 **成批** 的消息进行数据写入,但也会带来一个麻烦就是读取消息的时候需要遍历整个大文件,这是非常耗时的。 - -所以,在 `RocketMQ` 中又使用了 `ConsumeQueue` 作为每个队列的索引文件来 **提升读取消息的效率**。我们可以直接根据队列的消息序号,计算出索引的全局位置(索引序号*索引固定⻓度20),然后直接读取这条索引,再根据索引中记录的消息的全局位置,找到消息。 - -讲到这里,你可能对 `RockeMQ` 的存储架构还有些模糊,没事,我们结合着图来理解一下。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef388763c25c62.jpg) - -emmm,是不是有一点复杂🤣,看英文图片和英文文档的时候就不要怂,硬着头皮往下看就行。 - -> 如果上面没看懂的读者一定要认真看下面的流程分析! - -首先,在最上面的那一块就是我刚刚讲的你现在可以直接 **把 `ConsumerQueue` 理解为 `Queue`**。 - -在图中最左边说明了 红色方块 代表被写入的消息,虚线方块代表等待被写入的。左边的生产者发送消息会指定 `Topic` 、`QueueId` 和具体消息内容,而在 `Broker` 中管你是哪门子消息,他直接 **全部顺序存储到了 CommitLog **。而根据生产者指定的 `Topic` 和 `QueueId` 将这条消息本身在 `CommitLog` 的偏移(offset),消息本身大小,和tag的hash值存入对应的 `ConsumeQueue` 索引文件中。而在每个队列中都保存了 `ConsumeOffset` 即每个消费者组的消费位置(我在架构那里提到了,忘了的同学可以回去看一下),而消费者拉取消息进行消费的时候只需要根据 `ConsumeOffset` 获取下一个未被消费的消息就行了。 - -上述就是我对于整个消息存储架构的大概理解(这里不涉及到一些细节讨论,比如稀疏索引等等问题),希望对你有帮助。 - -因为有一个知识点因为写嗨了忘讲了,想想在哪里加也不好,所以我留给大家去思考🤔🤔一下吧。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/e314ee45gy1g05zgr67bbj20gp0b3aba.jpg) - -为什么 `CommitLog` 文件要设计成固定大小的长度呢?提醒:**内存映射机制**。 - -## 总结 - -总算把这篇博客写完了。我讲的你们还记得吗😅? - -这篇文章中我主要想大家介绍了 - -1. 消息队列出现的原因 -2. 消息队列的作用(异步,解耦,削峰) -3. 消息队列带来的一系列问题(消息堆积、重复消费、顺序消费、分布式事务等等) -4. 消息队列的两种消息模型——队列和主题模式 -5. 分析了 `RocketMQ` 的技术架构(`NameServer` 、`Broker` 、`Producer` 、`Comsumer`) -6. 结合 `RocketMQ` 回答了消息队列副作用的解决方案 -7. 介绍了 `RocketMQ` 的存储机制和刷盘策略。 - -等等。。。 - -> 如果喜欢可以点赞哟👍👍👍。 \ No newline at end of file diff --git a/docs/system-design/data-communication/dubbo.md b/docs/system-design/data-communication/dubbo.md deleted file mode 100644 index f2e0ac4..0000000 --- a/docs/system-design/data-communication/dubbo.md +++ /dev/null @@ -1,282 +0,0 @@ -本文是作者根据官方文档以及自己平时的使用情况,对 Dubbo 所做的一个总结。如果不懂 Dubbo 的使用的话,可以参考我的这篇文章[《超详细,新手都能看懂 !使用SpringBoot+Dubbo 搭建一个简单的分布式服务》](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484706&idx=1&sn=d413fc17023482f67ca17cb6756b9ff8&chksm=fd985343caefda555969568fdf4734536e0a1745f9de337d434a7dbd04e893bd2d75f3641aab&token=1902169190&lang=zh_CN#rd) - -Dubbo 官网:http://dubbo.apache.org/zh-cn/index.html - -Dubbo 中文文档: http://dubbo.apache.org/zh-cn/index.html - - - -- [一 重要的概念](#一-重要的概念) - - [1.1 什么是 Dubbo?](#11-什么是-dubbo) - - [1.2 什么是 RPC?RPC原理是什么?](#12-什么是-rpcrpc原理是什么) - - [1.3 为什么要用 Dubbo?](#13-为什么要用-dubbo) - - [1.4 什么是分布式?](#14-什么是分布式) - - [1.5 为什么要分布式?](#15-为什么要分布式) -- [二 Dubbo 的架构](#二-dubbo-的架构) - - [2.1 Dubbo 的架构图解](#21-dubbo-的架构图解) - - [2.2 Dubbo 工作原理](#22-dubbo-工作原理) -- [三 Dubbo 的负载均衡策略](#三-dubbo-的负载均衡策略) - - [3.1 先来解释一下什么是负载均衡](#31-先来解释一下什么是负载均衡) - - [3.2 再来看看 Dubbo 提供的负载均衡策略](#32-再来看看-dubbo-提供的负载均衡策略) - - [3.2.1 Random LoadBalance\(默认,基于权重的随机负载均衡机制\)](#321-random-loadbalance默认基于权重的随机负载均衡机制) - - [3.2.2 RoundRobin LoadBalance\(不推荐,基于权重的轮询负载均衡机制\)](#322-roundrobin-loadbalance不推荐基于权重的轮询负载均衡机制) - - [3.2.3 LeastActive LoadBalance](#323-leastactive-loadbalance) - - [3.2.4 ConsistentHash LoadBalance](#324-consistenthash-loadbalance) - - [3.3 配置方式](#33-配置方式) -- [四 zookeeper宕机与dubbo直连的情况](#四-zookeeper宕机与dubbo直连的情况) - - - - -## 一 重要的概念 - -### 1.1 什么是 Dubbo? - -Apache Dubbo (incubating) |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。简单来说 Dubbo 是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。 - -Dubbo 目前已经有接近 23k 的 Star ,Dubbo的Github 地址:[https://github.com/apache/incubator-dubbo](https://github.com/apache/incubator-dubbo) 。 另外,在开源中国举行的2018年度最受欢迎中国开源软件这个活动的评选中,Dubbo 更是凭借其超高人气仅次于 vue.js 和 ECharts 获得第三名的好成绩。 - -Dubbo 是由阿里开源,后来加入了 Apache 。正式由于 Dubbo 的出现,才使得越来越多的公司开始使用以及接受分布式架构。 - -**我们上面说了 Dubbo 实际上是 RPC 框架,那么什么是 RPC呢?** - -### 1.2 什么是 RPC?RPC原理是什么? - -**什么是 RPC?** - -RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务 A、B 部署在两台不同的机器上,那么服务 A 如果想要调用服务 B 中的某个方法该怎么办呢?使用 HTTP请求 当然可以,但是可能会比较慢而且一些优化做的并不好。 RPC 的出现就是为了解决这个问题。 - -**RPC原理是什么?** - -我这里这是简单的提一下。详细内容可以查看下面这篇文章: - -[http://www.importnew.com/22003.html](http://www.importnew.com/22003.html) - -![RPC原理图](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-6/37345851.jpg) - - -1. 服务消费方(client)调用以本地调用方式调用服务; -2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体; -3. client stub找到服务地址,并将消息发送到服务端; -4. server stub收到消息后进行解码; -5. server stub根据解码结果调用本地的服务; -6. 本地服务执行并将结果返回给server stub; -7. server stub将返回结果打包成消息并发送至消费方; -8. client stub接收到消息,并进行解码; -9. 服务消费方得到最终结果。 - -下面再贴一个网上的时序图: - -![RPC原理时序图](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-6/32527396.jpg) - -**说了这么多,我们为什么要用 Dubbo 呢?** - -### 1.3 为什么要用 Dubbo? - -Dubbo 的诞生和 SOA 分布式架构的流行有着莫大的关系。SOA 面向服务的架构(Service Oriented Architecture),也就是把工程按照业务逻辑拆分成服务层、表现层两个工程。服务层中包含业务逻辑,只需要对外提供服务即可。表现层只需要处理和页面的交互,业务逻辑都是调用服务层的服务来实现。SOA架构中有两个主要角色:服务提供者(Provider)和服务使用者(Consumer)。 - -![为什么要用 Dubbo](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-26/43050183.jpg) - -**如果你要开发分布式程序,你也可以直接基于 HTTP 接口进行通信,但是为什么要用 Dubbo呢?** - -我觉得主要可以从 Dubbo 提供的下面四点特性来说为什么要用 Dubbo: - -1. **负载均衡**——同一个服务部署在不同的机器时该调用那一台机器上的服务。 -2. **服务调用链路生成**——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如何调用的。 -3. **服务访问压力以及时长统计、资源调度和治理**——基于访问压力实时管理集群容量,提高集群利用率。 -4. **服务降级**——某个服务挂掉之后调用备用服务。 - -另外,Dubbo 除了能够应用在分布式系统中,也可以应用在现在比较火的微服务系统中。不过,由于 Spring Cloud 在微服务中应用更加广泛,所以,我觉得一般我们提 Dubbo 的话,大部分是分布式系统的情况。 - -**我们刚刚提到了分布式这个概念,下面再给大家介绍一下什么是分布式?为什么要分布式?** - -### 1.4 什么是分布式? - -分布式或者说 SOA 分布式重要的就是面向服务,说简单的分布式就是我们把整个系统拆分成不同的服务然后将这些服务放在不同的服务器上减轻单体服务的压力提高并发量和性能。比如电商系统可以简单地拆分成订单系统、商品系统、登录系统等等,拆分之后的每个服务可以部署在不同的机器上,如果某一个服务的访问量比较大的话也可以将这个服务同时部署在多台机器上。 - -### 1.5 为什么要分布式? - -从开发角度来讲单体应用的代码都集中在一起,而分布式系统的代码根据业务被拆分。所以,每个团队可以负责一个服务的开发,这样提升了开发效率。另外,代码根据业务拆分之后更加便于维护和扩展。 - -另外,我觉得将系统拆分成分布式之后不光便于系统扩展和维护,更能提高整个系统的性能。你想一想嘛?把整个系统拆分成不同的服务/系统,然后每个服务/系统 单独部署在一台服务器上,是不是很大程度上提高了系统性能呢? - -## 二 Dubbo 的架构 - -### 2.1 Dubbo 的架构图解 - -![Dubbo 架构](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-26/46816446.jpg) - -**上述节点简单说明:** - -- **Provider:** 暴露服务的服务提供方 -- **Consumer:** 调用远程服务的服务消费方 -- **Registry:** 服务注册与发现的注册中心 -- **Monitor:** 统计服务的调用次数和调用时间的监控中心 -- **Container:** 服务运行容器 - -**调用关系说明:** - -1. 服务容器负责启动,加载,运行服务提供者。 -2. 服务提供者在启动时,向注册中心注册自己提供的服务。 -3. 服务消费者在启动时,向注册中心订阅自己所需的服务。 -4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 -5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 -6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 - -**重要知识点总结:** - -- **注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小** -- **监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示** -- **注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外** -- **注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者** -- **注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表** -- **注册中心和监控中心都是可选的,服务消费者可以直连服务提供者** -- **服务提供者无状态,任意一台宕掉后,不影响使用** -- **服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复** - - - - - -### 2.2 Dubbo 工作原理 - - -![Dubbo 工作原理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-26/64702923.jpg) - -图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。 - -**各层说明**: - -- 第一层:**service层**,接口层,给服务提供者和消费者来实现的 -- 第二层:**config层**,配置层,主要是对dubbo进行各种配置的 -- 第三层:**proxy层**,服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton -- 第四层:**registry层**,服务注册层,负责服务的注册与发现 -- 第五层:**cluster层**,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务 -- 第六层:**monitor层**,监控层,对rpc接口的调用次数和调用时间进行监控 -- 第七层:**protocol层**,远程调用层,封装rpc调用 -- 第八层:**exchange层**,信息交换层,封装请求响应模式,同步转异步 -- 第九层:**transport层**,网络传输层,抽象mina和netty为统一接口 -- 第十层:**serialize层**,数据序列化层,网络传输需要 - - -## 三 Dubbo 的负载均衡策略 - -### 3.1 先来解释一下什么是负载均衡 - -**先来个官方的解释。** - -> 维基百科对负载均衡的定义:负载均衡改善了跨多个计算资源(例如计算机,计算机集群,网络链接,中央处理单元或磁盘驱动的的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间,并避免任何单个资源的过载。使用具有负载平衡而不是单个组件的多个组件可以通过冗余提高可靠性和可用性。负载平衡通常涉及专用软件或硬件。 - -**上面讲的大家可能不太好理解,再用通俗的话给大家说一下。** - -比如我们的系统中的某个服务的访问量特别大,我们将这个服务部署在了多台服务器上,当客户端发起请求的时候,多台服务器都可以处理这个请求。那么,如何正确选择处理该请求的服务器就很关键。假如,你就要一台服务器来处理该服务的请求,那该服务部署在多台服务器的意义就不复存在了。负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题,我们从负载均衡的这四个字就能明显感受到它的意义。 - -### 3.2 再来看看 Dubbo 提供的负载均衡策略 - -在集群负载均衡时,Dubbo 提供了多种均衡策略,默认为 `random` 随机调用。可以自行扩展负载均衡策略,参见:[负载均衡扩展](https://dubbo.gitbooks.io/dubbo-dev-book/content/impls/load-balance.html)。 - -备注:下面的图片来自于:尚硅谷2018Dubbo 视频。 - - -#### 3.2.1 Random LoadBalance(默认,基于权重的随机负载均衡机制) - -- **随机,按权重设置随机概率。** -- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。 - -![基于权重的随机负载均衡机制](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-7/77722327.jpg) - - - -#### 3.2.2 RoundRobin LoadBalance(不推荐,基于权重的轮询负载均衡机制) - -- 轮循,按公约后的权重设置轮循比率。 -- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。 - -![基于权重的轮询负载均衡机制](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-7/97933247.jpg) - -#### 3.2.3 LeastActive LoadBalance - -- 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。 -- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。 - -#### 3.2.4 ConsistentHash LoadBalance - -- **一致性 Hash,相同参数的请求总是发到同一提供者。(如果你需要的不是随机负载均衡,是要一类请求都到一个节点,那就走这个一致性hash策略。)** -- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 -- 算法参见:http://en.wikipedia.org/wiki/Consistent_hashing -- 缺省只对第一个参数 Hash,如果要修改,请配置 `` -- 缺省用 160 份虚拟节点,如果要修改,请配置 `` - -### 3.3 配置方式 - -**xml 配置方式** - -服务端服务级别 - -```java - -``` -客户端服务级别 - -```java - -``` - -服务端方法级别 - -```java - - - -``` - -客户端方法级别 - -```java - - - -``` - -**注解配置方式:** - -消费方基于基于注解的服务级别配置方式: - -```java -@Reference(loadbalance = "roundrobin") -HelloService helloService; -``` - -## 四 zookeeper宕机与dubbo直连的情况 - -zookeeper宕机与dubbo直连的情况在面试中可能会被经常问到,所以要引起重视。 - -在实际生产中,假如zookeeper注册中心宕掉,一段时间内服务消费方还是能够调用提供方的服务的,实际上它使用的本地缓存进行通讯,这只是dubbo健壮性的一种体现。 - -**dubbo的健壮性表现:** - -1. 监控中心宕掉不影响使用,只是丢失部分采样数据 -2. 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务 -3. 注册中心对等集群,任意一台宕掉后,将自动切换到另一台 -4. 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯 -5. 服务提供者无状态,任意一台宕掉后,不影响使用 -5. 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复 - -我们前面提到过:注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。所以,我们可以完全可以绕过注册中心——采用 **dubbo 直连** ,即在服务消费方配置服务提供方的位置信息。 - - -**xml配置方式:** - -```xml - -``` - -**注解方式:** - -```java - @Reference(url = "127.0.0.1:20880") - HelloService helloService; -``` - - - diff --git a/docs/system-design/data-communication/message-queue.md b/docs/system-design/data-communication/message-queue.md deleted file mode 100644 index 3f99c07..0000000 --- a/docs/system-design/data-communication/message-queue.md +++ /dev/null @@ -1,157 +0,0 @@ - - -- [消息队列其实很简单](#消息队列其实很简单) - - [一 什么是消息队列](#一-什么是消息队列) - - [二 为什么要用消息队列](#二-为什么要用消息队列) - - [\(1\) 通过异步处理提高系统性能(削峰、减少响应所需时间)](#1-通过异步处理提高系统性能削峰减少响应所需时间) - - [\(2\) 降低系统耦合性](#2-降低系统耦合性) - - [三 使用消息队列带来的一些问题](#三-使用消息队列带来的一些问题) - - [四 JMS VS AMQP](#四-jms-vs-amqp) - - [4.1 JMS](#41-jms) - - [4.1.1 JMS 简介](#411-jms-简介) - - [4.1.2 JMS两种消息模型](#412-jms两种消息模型) - - [4.1.3 JMS 五种不同的消息正文格式](#413-jms-五种不同的消息正文格式) - - [4.2 AMQP](#42-amqp) - - [4.3 JMS vs AMQP](#43-jms-vs-amqp) - - [五 常见的消息队列对比](#五-常见的消息队列对比) - - - - -# 消息队列其实很简单 - -  “RabbitMQ?”“Kafka?”“RocketMQ?”...在日常学习与开发过程中,我们常常听到消息队列这个关键词。我也在我的多篇文章中提到了这个概念。可能你是熟练使用消息队列的老手,又或者你是不懂消息队列的新手,不论你了不了解消息队列,本文都将带你搞懂消息队列的一些基本理论。如果你是老手,你可能从本文学到你之前不曾注意的一些关于消息队列的重要概念,如果你是新手,相信本文将是你打开消息队列大门的一板砖。 - -## 一 什么是消息队列 - -  我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。目前使用较多的消息队列有ActiveMQ,RabbitMQ,Kafka,RocketMQ,我们后面会一一对比这些消息队列。 - -  另外,我们知道队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。比如生产者发送消息1,2,3...对于消费者就会按照1,2,3...的顺序来消费。但是偶尔也会出现消息被消费的顺序不对的情况,比如某个消息消费失败又或者一个 queue 多个consumer 也会导致消息被消费的顺序不对,我们一定要保证消息被消费的顺序正确。 - -  除了上面说的消息消费顺序的问题,使用消息队列,我们还要考虑如何保证消息不被重复消费?如何保证消息的可靠性传输(如何处理消息丢失的问题)?......等等问题。所以说使用消息队列也不是十全十美的,使用它也会让系统可用性降低、复杂度提高,另外需要我们保障一致性等问题。 - -## 二 为什么要用消息队列 - -  我觉得使用消息队列主要有两点好处:1.通过异步处理提高系统性能(削峰、减少响应所需时间);2.降低系统耦合性。如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,这个时候推荐你结合你自己的项目来回答。 - - -  《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。 - -### (1) 通过异步处理提高系统性能(削峰、减少响应所需时间) - -![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/Asynchronous-message-queue.png) -  如上图,**在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。** - -  通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: - -![削峰](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/削峰-消息队列.png) - -  因为**用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败**。因此使用消息队列进行异步处理之后,需要**适当修改业务流程进行配合**,比如**用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 - -### (2) 降低系统耦合性 - -   使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧: - -![解耦](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/消息队列-解耦.png) - -  生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。 - -  **消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 - -  消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。 - -  **另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。** - -**备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的。**除了发布-订阅模式,还有点对点订阅模式(一个消息只有一个消费者),我们比较常用的是发布-订阅模式。** 另外,这两种消息模型是 JMS 提供的,AMQP 协议还提供了 5 种消息模型。 - -## 三 使用消息队列带来的一些问题 - -- **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入MQ之前,你不用考虑消息丢失或者说MQ挂掉等等的情况,但是,引入MQ之后你就需要去考虑了! -- **系统复杂性提高:** 加入MQ之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! -- **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! - -## 四 JMS VS AMQP - -### 4.1 JMS - -#### 4.1.1 JMS 简介 - -  JMS(JAVA Message Service,java消息服务)是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。**JMS(JAVA Message Service,Java消息服务)API是一个消息服务的标准或者说是规范**,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 - -**ActiveMQ 就是基于 JMS 规范实现的。** - -#### 4.1.2 JMS两种消息模型 - -①点到点(P2P)模型 - -![点到点(P2P)模型](https://user-gold-cdn.xitu.io/2018/4/21/162e7185572ca37d?w=575&h=135&f=gif&s=8530) -   - -使用**队列(Queue)**作为消息通信载体;满足**生产者与消费者模式**,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。比如:我们生产者发送100条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) - -② 发布/订阅(Pub/Sub)模型 - - ![发布/订阅(Pub/Sub)模型](https://user-gold-cdn.xitu.io/2018/4/21/162e7187c268eaa5?w=402&h=164&f=gif&s=15492) -   - -发布订阅模型(Pub/Sub) 使用**主题(Topic)**作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者,**在一条消息广播之后才订阅的用户则是收不到该条消息的**。 - -#### 4.1.3 JMS 五种不同的消息正文格式 - -  JMS定义了五种不同的消息正文格式,以及调用的消息类型,允许你发送并接收以一些不同形式的数据,提供现有消息格式的一些级别的兼容性。 - -- StreamMessage -- Java原始值的数据流 -- MapMessage--一套名称-值对 -- TextMessage--一个字符串对象 -- ObjectMessage--一个序列化的 Java对象 -- BytesMessage--一个字节的数据流 - - -### 4.2 AMQP - -  ​ AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 **高级消息队列协议**(二进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。 - -**RabbitMQ 就是基于 AMQP 协议实现的。** - - - -### 4.3 JMS vs AMQP - - -|对比方向| JMS | AMQP | -| :-------- | --------:| :--: | -| 定义| Java API | 协议 | -| 跨语言 | 否 | 是 | -| 跨平台 | 否 | 是 | -| 支持消息类型 | 提供两种消息模型:①Peer-2-Peer;②Pub/sub| 提供了五种消息模型:①direct exchange;②fanout exchange;③topic change;④headers exchange;⑤system exchange。本质来讲,后四种和JMS的pub/sub模型没有太大差别,仅是在路由机制上做了更详细的划分;| -|支持消息类型| 支持多种消息类型 ,我们在上面提到过| byte[](二进制)| - -**总结:** - -- AMQP 为消息定义了线路层(wire-level protocol)的协议,而JMS所定义的是API规范。在 Java 体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而AMQP天然具有跨平台、跨语言特性。 -- JMS 支持TextMessage、MapMessage 等复杂的消息类型;而 AMQP 仅支持 byte[] 消息类型(复杂的类型可序列化后发送)。 -- 由于Exchange 提供的路由算法,AMQP可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队列 和 主题/订阅 方式两种。 - - -## 五 常见的消息队列对比 - - - -对比方向 |概要 --------- | --- - 吞吐量| 万级的 ActiveMQ 和 RabbitMQ 的吞吐量(ActiveMQ 的性能最差)要比 十万级甚至是百万级的 RocketMQ 和 Kafka 低一个数量级。 -可用性| 都可以实现高可用。ActiveMQ 和 RabbitMQ 都是基于主从架构实现高可用性。RocketMQ 基于分布式架构。 kafka 也是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 -时效性| RabbitMQ 基于erlang开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。其他三个都是 ms 级。 -功能支持| 除了 Kafka,其他三个功能都较为完备。 Kafka 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 -消息丢失| ActiveMQ 和 RabbitMQ 丢失的可能性非常低, RocketMQ 和 Kafka 理论上不会丢失。 - - -**总结:** - -- ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用。 -- RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做erlang源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 -- RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ 挺好的 -- kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 - - -参考:《Java工程师面试突击第1季-中华石杉老师》 diff --git a/docs/system-design/data-communication/rabbitmq.md b/docs/system-design/data-communication/rabbitmq.md deleted file mode 100644 index 28407cc..0000000 --- a/docs/system-design/data-communication/rabbitmq.md +++ /dev/null @@ -1,324 +0,0 @@ - - -- [一文搞懂 RabbitMQ 的重要概念以及安装](#一文搞懂-rabbitmq-的重要概念以及安装) - - [一 RabbitMQ 介绍](#一-rabbitmq-介绍) - - [1.1 RabbitMQ 简介](#11-rabbitmq-简介) - - [1.2 RabbitMQ 核心概念](#12-rabbitmq-核心概念) - - [1.2.1 Producer(生产者) 和 Consumer(消费者)](#121-producer生产者-和-consumer消费者) - - [1.2.2 Exchange(交换器)](#122-exchange交换器) - - [1.2.3 Queue(消息队列)](#123-queue消息队列) - - [1.2.4 Broker(消息中间件的服务节点)](#124-broker消息中间件的服务节点) - - [1.2.5 Exchange Types(交换器类型)](#125-exchange-types交换器类型) - - [① fanout](#①-fanout) - - [② direct](#②-direct) - - [③ topic](#③-topic) - - [④ headers(不推荐)](#④-headers不推荐) - - [二 安装 RabbitMq](#二-安装-rabbitmq) - - [2.1 安装 erlang](#21-安装-erlang) - - [2.2 安装 RabbitMQ](#22-安装-rabbitmq) - - - -# 一文搞懂 RabbitMQ 的重要概念以及安装 - -## 一 RabbitMQ 介绍 - -这部分参考了 《RabbitMQ实战指南》这本书的第 1 章和第 2 章。 - -### 1.1 RabbitMQ 简介 - -RabbitMQ 是采用 Erlang 语言实现 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的消息中间件,它最初起源于金融系统,用于在分布式系统中存储转发消息。 - -RabbitMQ 发展到今天,被越来越多的人认可,这和它在易用性、扩展性、可靠性和高可用性等方面的卓著表现是分不开的。RabbitMQ 的具体特点可以概括为以下几点: - -- **可靠性:** RabbitMQ使用一些机制来保证消息的可靠性,如持久化、传输确认及发布确认等。 -- **灵活的路由:** 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。这个后面会在我们将 RabbitMQ 核心概念的时候详细介绍到。 -- **扩展性:** 多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。 -- **高可用性:** 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。 -- **支持多种协议:** RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP、MQTT 等多种消息中间件协议。 -- **多语言客户端:** RabbitMQ几乎支持所有常用语言,比如 Java、Python、Ruby、PHP、C#、JavaScript等。 -- **易用的管理界面:** RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。在安装 RabbitMQ 的时候会介绍到,安装好 RabbitMQ 就自带管理界面。 -- **插件机制:** RabbitMQ 提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。感觉这个有点类似 Dubbo 的 SPI机制。 - -### 1.2 RabbitMQ 核心概念 - -RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。可以把消息传递的过程想象成:当你将一个包裹送到邮局,邮局会暂存并最终将邮件通过邮递员送到收件人的手上,RabbitMQ就好比由邮局、邮箱和邮递员组成的一个系统。从计算机术语层面来说,RabbitMQ 模型更像是一种交换机模型。 - -下面再来看看图1—— RabbitMQ 的整体模型架构。 - -![图1-RabbitMQ 的整体模型架构](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/96388546.jpg) - -下面我会一一介绍上图中的一些概念。 - -#### 1.2.1 Producer(生产者) 和 Consumer(消费者) - -- **Producer(生产者)** :生产消息的一方(邮件投递者) -- **Consumer(消费者)** :消费消息的一方(邮件收件人) - -消息一般由 2 部分组成:**消息头**(或者说是标签 Label)和 **消息体**。消息体也可以称为 payLoad ,消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。生产者把消息交由 RabbitMQ 后,RabbitMQ 会根据消息头把消息发送给感兴趣的 Consumer(消费者)。 - -#### 1.2.2 Exchange(交换器) - -在 RabbitMQ 中,消息并不是直接被投递到 **Queue(消息队列)** 中的,中间还必须经过 **Exchange(交换器)** 这一层,**Exchange(交换器)** 会把我们的消息分配到对应的 **Queue(消息队列)** 中。 - -**Exchange(交换器)** 用来接收生产者发送的消息并将这些消息路由给服务器中的队列中,如果路由不到,或许会返回给 **Producer(生产者)** ,或许会被直接丢弃掉 。这里可以将RabbitMQ中的交换器看作一个简单的实体。 - -**RabbitMQ 的 Exchange(交换器) 有4种类型,不同的类型对应着不同的路由策略**:**direct(默认)**,**fanout**, **topic**, 和 **headers**,不同类型的Exchange转发消息的策略有所区别。这个会在介绍 **Exchange Types(交换器类型)** 的时候介绍到。 - -Exchange(交换器) 示意图如下: - -![Exchange(交换器) 示意图](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/24007899.jpg) - -生产者将消息发给交换器的时候,一般会指定一个 **RoutingKey(路由键)**,用来指定这个消息的路由规则,而这个 **RoutingKey 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效**。 - -RabbitMQ 中通过 **Binding(绑定)** 将 **Exchange(交换器)** 与 **Queue(消息队列)** 关联起来,在绑定的时候一般会指定一个 **BindingKey(绑定建)** ,这样 RabbitMQ 就知道如何正确将消息路由到队列了,如下图所示。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和 Queue 的绑定可以是多对多的关系。 - -Binding(绑定) 示意图: - -![Binding(绑定) 示意图](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/70553134.jpg) - -生产者将消息发送给交换器时,需要一个RoutingKey,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如fanout类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。 - -#### 1.2.3 Queue(消息队列) - -**Queue(消息队列)** 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。 - -**RabbitMQ** 中消息只能存储在 **队列** 中,这一点和 **Kafka** 这种消息中间件相反。Kafka 将消息存储在 **topic(主题)** 这个逻辑层面,而相对应的队列逻辑只是topic实际存储文件中的位移标识。 RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。 - -**多个消费者可以订阅同一个队列**,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,这样避免的消息被重复消费。 - -**RabbitMQ** 不支持队列层面的广播消费,如果有广播消费的需求,需要在其上进行二次开发,这样会很麻烦,不建议这样做。 - -#### 1.2.4 Broker(消息中间件的服务节点) - -对于 RabbitMQ 来说,一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点,或者RabbitMQ服务实例。大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。 - -下图展示了生产者将消息存入 RabbitMQ Broker,以及消费者从Broker中消费数据的整个流程。 - -![消息队列的运转过程](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/67952922.jpg) - -这样图1中的一些关于 RabbitMQ 的基本概念我们就介绍完毕了,下面再来介绍一下 **Exchange Types(交换器类型)** 。 - -#### 1.2.5 Exchange Types(交换器类型) - -RabbitMQ 常用的 Exchange Type 有 **fanout**、**direct**、**topic**、**headers** 这四种(AMQP规范里还提到两种 Exchange Type,分别为 system 与 自定义,这里不予以描述)。 - -##### ① fanout - -fanout 类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中,不需要做任何判断操作,所以 fanout 类型是所有的交换机类型里面速度最快的。fanout 类型常用来广播消息。 - -##### ② direct - -direct 类型的Exchange路由规则也很简单,它会把消息路由到那些 Bindingkey 与 RoutingKey 完全匹配的 Queue 中。 - -![direct 类型交换器](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/37008021.jpg) - -以上图为例,如果发送消息的时候设置路由键为“warning”,那么消息会路由到 Queue1 和 Queue2。如果在发送消息的时候设置路由键为"Info”或者"debug”,消息只会路由到Queue2。如果以其他的路由键发送消息,则消息不会路由到这两个队列中。 - -direct 类型常用在处理有优先级的任务,根据任务的优先级把消息发送到对应的队列,这样可以指派更多的资源去处理高优先级的队列。 - -##### ③ topic - -前面讲到direct类型的交换器路由规则是完全匹配 BindingKey 和 RoutingKey ,但是这种严格的匹配方式在很多情况下不能满足实际业务的需求。topic类型的交换器在匹配规则上进行了扩展,它与 direct 类型的交换器相似,也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同,它约定: - -- RoutingKey 为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),如 “com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”; -- BindingKey 和 RoutingKey 一样也是点号“.”分隔的字符串; -- BindingKey 中可以存在两种特殊字符串“*”和“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。 - -![topic 类型交换器](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/73843.jpg) - -以上图为例: - -- 路由键为 “com.rabbitmq.client” 的消息会同时路由到 Queuel 和 Queue2; -- 路由键为 “com.hidden.client” 的消息只会路由到 Queue2 中; -- 路由键为 “com.hidden.demo” 的消息只会路由到 Queue2 中; -- 路由键为 “java.rabbitmq.demo” 的消息只会路由到Queuel中; -- 路由键为 “java.util.concurrent” 的消息将会被丢弃或者返回给生产者(需要设置 mandatory 参数),因为它没有匹配任何路由键。 - -##### ④ headers(不推荐) - -headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定队列和交换器时制定一组键值对,当发送消息到交换器时,RabbitMQ会获取到该消息的 headers(也是一个键值对的形式)'对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。 - -## 二 安装 RabbitMq - -通过 Docker 安装非常方便,只需要几条命令就好了,我这里是只说一下常规安装方法。 - -前面提到了 RabbitMQ 是由 Erlang语言编写的,也正因如此,在安装RabbitMQ 之前需要安装 Erlang。 - -注意:在安装 RabbitMQ 的时候需要注意 RabbitMQ 和 Erlang 的版本关系,如果不注意的话会导致出错,两者对应关系如下: - -![RabbitMQ 和 Erlang 的版本关系](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RabbitMQ-Erlang.png) - -### 2.1 安装 erlang - -**1 下载 erlang 安装包** - -在官网下载然后上传到 Linux 上或者直接使用下面的命令下载对应的版本。 - -```shell -[root@SnailClimb local]#wget http://erlang.org/download/otp_src_19.3.tar.gz -``` - -erlang 官网下载:[http://www.erlang.org/downloads](http://www.erlang.org/downloads) - - **2 解压 erlang 安装包** - -```shell -[root@SnailClimb local]#tar -xvzf otp_src_19.3.tar.gz -``` - -**3 删除 erlang 安装包** - -```shell -[root@SnailClimb local]#rm -rf otp_src_19.3.tar.gz -``` - -**4 安装 erlang 的依赖工具** - -```shell -[root@SnailClimb local]#yum -y install make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel unixODBC-devel -``` - -**5 进入erlang 安装包解压文件对 erlang 进行安装环境的配置** - -新建一个文件夹 - -```shell -[root@SnailClimb local]# mkdir erlang -``` - -对 erlang 进行安装环境的配置 - -```shell -[root@SnailClimb otp_src_19.3]# -./configure --prefix=/usr/local/erlang --without-javac -``` - -**6 编译安装** - -```shell -[root@SnailClimb otp_src_19.3]# -make && make install -``` - -**7 验证一下 erlang 是否安装成功了** - -```shell -[root@SnailClimb otp_src_19.3]# ./bin/erl -``` -运行下面的语句输出“hello world” - -```erlang - io:format("hello world~n", []). -``` -![输出“hello world”](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-12/49570541.jpg) - -大功告成,我们的 erlang 已经安装完成。 - -**8 配置 erlang 环境变量** - -```shell -[root@SnailClimb etc]# vim profile -``` - -追加下列环境变量到文件末尾 - -```shell -#erlang -ERL_HOME=/usr/local/erlang -PATH=$ERL_HOME/bin:$PATH -export ERL_HOME PATH -``` - -运行下列命令使配置文件`profile`生效 - -```shell -[root@SnailClimb etc]# source /etc/profile -``` - -输入 erl 查看 erlang 环境变量是否配置正确 - -```shell -[root@SnailClimb etc]# erl -``` - -![输入 erl 查看 erlang 环境变量是否配置正确](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-12/62504246.jpg) - -### 2.2 安装 RabbitMQ - -**1. 下载rpm** - -```shell -wget https://www.rabbitmq.com/releases/rabbitmq-server/v3.6.8/rabbitmq-server-3.6.8-1.el7.noarch.rpm -``` -或者直接在官网下载 - -https://www.rabbitmq.com/install-rpm.html[enter link description here](https://www.rabbitmq.com/install-rpm.html) - -**2. 安装rpm** - -```shell -rpm --import https://www.rabbitmq.com/rabbitmq-release-signing-key.asc -``` -紧接着执行: - -```shell -yum install rabbitmq-server-3.6.8-1.el7.noarch.rpm -``` -中途需要你输入"y"才能继续安装。 - -**3 开启 web 管理插件** - -```shell -rabbitmq-plugins enable rabbitmq_management -``` - -**4 设置开机启动** - -```shell -chkconfig rabbitmq-server on -``` - -**4. 启动服务** - -```shell -service rabbitmq-server start -``` - -**5. 查看服务状态** - -```shell -service rabbitmq-server status -``` - -**6. 访问 RabbitMQ 控制台** - -浏览器访问:http://你的ip地址:15672/ - -默认用户名和密码: guest/guest;但是需要注意的是:guestuest用户只是被容许从localhost访问。官网文档描述如下: - -```shell -“guest” user can only connect via localhost -``` - -**解决远程访问 RabbitMQ 远程访问密码错误** - -新建用户并授权 - -```shell -[root@SnailClimb rabbitmq]# rabbitmqctl add_user root root -Creating user "root" ... -[root@SnailClimb rabbitmq]# rabbitmqctl set_user_tags root administrator - -Setting tags for user "root" to [administrator] ... -[root@SnailClimb rabbitmq]# -[root@SnailClimb rabbitmq]# rabbitmqctl set_permissions -p / root ".*" ".*" ".*" -Setting permissions for user "root" in vhost "/" ... - -``` - -再次访问:http://你的ip地址:15672/ ,输入用户名和密码:root root - -![RabbitMQ控制台](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-12/45835332.jpg) - - diff --git a/docs/system-design/data-communication/summary.md b/docs/system-design/data-communication/summary.md deleted file mode 100644 index 209df6b..0000000 --- a/docs/system-design/data-communication/summary.md +++ /dev/null @@ -1,100 +0,0 @@ -> ## RPC - -**RPC(Remote Procedure Call)—远程过程调用** ,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发分布式程序就像开发本地程序一样简单。 - -**RPC采用客户端(服务调用方)/服务器端(服务提供方)模式,** 都运行在自己的JVM中。客户端只需要引入要使用的接口,接口的实现和运行都在服务器端。RPC主要依赖的技术包括序列化、反序列化和数据传输协议,这是一种定义与实现相分离的设计。 - -**目前Java使用比较多的RPC方案主要有RMI(JDK自带)、Hessian、Dubbo以及Thrift等。** - -**注意: RPC主要指内部服务之间的调用,RESTful也可以用于内部服务之间的调用,但其主要用途还在于外部系统提供服务,因此没有将其包含在本知识点内。** - -### 常见RPC框架: - -- **RMI(JDK自带):** JDK自带的RPC - - 详细内容可以参考:[从懵逼到恍然大悟之Java中RMI的使用](https://blog.csdn.net/lmy86263/article/details/72594760) - -- **Dubbo:** Dubbo是 阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。 - - 详细内容可以参考: - - - [ 高性能优秀的服务框架-dubbo介绍](https://blog.csdn.net/qq_34337272/article/details/79862899) - - - [Dubbo是什么?能做什么?](https://blog.csdn.net/houshaolin/article/details/76408399) - - -- **Hessian:** Hessian是一个轻量级的remotingonhttp工具,使用简单的方法提供了RMI的功能。 相比WebService,Hessian更简单、快捷。采用的是二进制RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。 - - 详细内容可以参考: [Hessian的使用以及理解](https://blog.csdn.net/sunwei_pyw/article/details/74002351) - -- **Thrift:** Apache Thrift是Facebook开源的跨语言的RPC通信框架,目前已经捐献给Apache基金会管理,由于其跨语言特性和出色的性能,在很多互联网公司得到应用,有能力的公司甚至会基于thrift研发一套分布式服务框架,增加诸如服务注册、服务发现等功能。 - - - 详细内容可以参考: [【Java】分布式RPC通信框架Apache Thrift 使用总结](https://www.cnblogs.com/zeze/p/8628585.html) - -### 如何进行选择: - -- **是否允许代码侵入:** 即需要依赖相应的代码生成器生成代码,比如Thrift。 -- **是否需要长连接获取高性能:** 如果对于性能需求较高的haul,那么可以果断选择基于TCP的Thrift、Dubbo。 -- **是否需要跨越网段、跨越防火墙:** 这种情况一般选择基于HTTP协议的Hessian和Thrift的HTTP Transport。 - -此外,Google推出的基于HTTP2.0的gRPC框架也开始得到应用,其序列化协议基于Protobuf,网络框架使用的是Netty4,但是其需要生成代码,可扩展性也比较差。 - -> ## 消息中间件 - -**消息中间件,也可以叫做中央消息队列或者是消息队列(区别于本地消息队列,本地消息队列指的是JVM内的队列实现)**,是一种独立的队列系统,消息中间件经常用来解决内部服务之间的 **异步调用问题** 。请求服务方把请求队列放到队列中即可返回,然后等待服务提供方去队列中获取请求进行处理,之后通过回调等机制把结果返回给请求服务方。 - -异步调用只是消息中间件一个非常常见的应用场景。此外,常用的消息队列应用场景还有如下几个: -- **解耦 :** 一个业务的非核心流程需要依赖其他系统,但结果并不重要,有通知即可。 -- **最终一致性 :** 指的是两个系统的状态保持一致,可以有一定的延迟,只要最终达到一致性即可。经常用在解决分布式事务上。 -- **广播 :** 消息队列最基本的功能。生产者只负责生产消息,订阅者接收消息。 -- **错峰和流控** - - -具体可以参考: - -[《消息队列深入解析》](https://blog.csdn.net/qq_34337272/article/details/80029918) - -当前使用较多的消息队列有ActiveMQ(性能差,不推荐使用)、RabbitMQ、RocketMQ、Kafka等等,我们之前提到的redis数据库也可以实现消息队列,不过不推荐,redis本身设计就不是用来做消息队列的。 - -- **ActiveMQ:** ActiveMQ是Apache出品,最流行的,能力强劲的开源消息总线。ActiveMQ是一个完全支持JMS1.1和J2EE 1.4规范的JMSProvider实现,尽管JMS规范出台已经是很久的事情了,但是JMS在当今的J2EE应用中间仍然扮演着特殊的地位。 - - 具体可以参考: - - [《消息队列ActiveMQ的使用详解》](https://blog.csdn.net/qq_34337272/article/details/80031702) - -- **RabbitMQ:** RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗 - > AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。 - - - 具体可以参考: - - [《消息队列之 RabbitMQ》](https://www.jianshu.com/p/79ca08116d57) - -- **RocketMQ:** - - 具体可以参考: - - [《RocketMQ 实战之快速入门》](https://www.jianshu.com/p/824066d70da8) - - [《十分钟入门RocketMQ》](http://jm.taobao.org/2017/01/12/rocketmq-quick-start-in-10-minutes/) (阿里中间件团队博客) - - -- **Kafka**:Kafka是一个分布式的、可分区的、可复制的、基于发布/订阅的消息系统(现在官方的描述是“一个分布式流平台”),Kafka主要用于大数据领域,当然在分布式系统中也有应用。目前市面上流行的消息队列RocketMQ就是阿里借鉴Kafka的原理、用Java开发而得。 - - 具体可以参考: - - [《Kafka应用场景》](http://book.51cto.com/art/201801/565244.htm) - - [《初谈Kafka》](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484106&idx=1&sn=aa1999895d009d91eb3692a3e6429d18&chksm=fd9854abcaefddbd1101ca5dc2c7c783d7171320d6300d9b2d8e68b7ef8abd2b02ea03e03600#rd) - -**推荐阅读:** - -[《Kafka、RabbitMQ、RocketMQ等消息中间件的对比 —— 消息发送性能和区别》](https://mp.weixin.qq.com/s?__biz=MzU5OTMyODAyNg==&mid=2247484721&idx=1&sn=11e4e29886e581dd328311d308ccc068&chksm=feb7d144c9c058529465b02a4e26a25ef76b60be8984ace9e4a0f5d3d98ca52e014ecb73b061&scene=21#wechat_redirect) - - - - - - - diff --git a/docs/system-design/data-communication/why-use-rpc.md b/docs/system-design/data-communication/why-use-rpc.md deleted file mode 100644 index f295e13..0000000 --- a/docs/system-design/data-communication/why-use-rpc.md +++ /dev/null @@ -1,76 +0,0 @@ -## 什么是 RPC?RPC原理是什么? - -### **什么是 RPC?** - -RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务 A、B 部署在两台不同的机器上,那么服务 A 如果想要调用服务 B 中的某个方法该怎么办呢?使用 HTTP请求 当然可以,但是可能会比较慢而且一些优化做的并不好。 RPC 的出现就是为了解决这个问题。 - -### **RPC原理是什么?** - -我这里这是简单的提一下,详细内容可以查看下面这篇文章: - -http://www.importnew.com/22003.html - -![RPC原理图](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-6/37345851.jpg) - -1. 服务消费方(client)调用以本地调用方式调用服务; -2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体; -3. client stub找到服务地址,并将消息发送到服务端; -4. server stub收到消息后进行解码; -5. server stub根据解码结果调用本地的服务; -6. 本地服务执行并将结果返回给server stub; -7. server stub将返回结果打包成消息并发送至消费方; -8. client stub接收到消息,并进行解码; -9. 服务消费方得到最终结果。 - -下面再贴一个网上的时序图: - -![RPC原理时序图](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-6/32527396.jpg) - -### RPC 解决了什么问题? - -从上面对 RPC 介绍的内容中,概括来讲RPC 主要解决了:**让分布式或者微服务系统中不同服务之间的调用像本地调用一样简单。** - -### 常见的 RPC 框架总结? - -- **RMI(JDK自带):** JDK自带的RPC,有很多局限性,不推荐使用。 -- **Dubbo:** Dubbo是 阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。目前 Dubbo 已经成为 Spring Cloud Alibaba 中的官方组件。 -- **gRPC** :gRPC是可以在任何环境中运行的现代开源高性能RPC框架。它可以通过可插拔的支持来有效地连接数据中心内和跨数据中心的服务,以实现负载平衡,跟踪,运行状况检查和身份验证。它也适用于分布式计算的最后一英里,以将设备,移动应用程序和浏览器连接到后端服务。 - -- **Hessian:** Hessian是一个轻量级的remotingonhttp工具,使用简单的方法提供了RMI的功能。 相比WebService,Hessian更简单、快捷。采用的是二进制RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。 -- **Thrift:** Apache Thrift是Facebook开源的跨语言的RPC通信框架,目前已经捐献给Apache基金会管理,由于其跨语言特性和出色的性能,在很多互联网公司得到应用,有能力的公司甚至会基于thrift研发一套分布式服务框架,增加诸如服务注册、服务发现等功能。 - -## 既有 HTTP ,为啥用 RPC 进行服务调用? - -###RPC 只是一种设计而已 - -RPC 只是一种概念、一种设计,就是为了解决 **不同服务之间的调用问题**, 它一般会包含有 **传输协议** 和 **序列化协议** 这两个。 - -但是,HTTP 是一种协议,RPC框架可以使用 HTTP协议作为传输协议或者直接使用TCP作为传输协议,使用不同的协议一般也是为了适应不同的场景。 - -### HTTP 和 TCP - -**可能现在很多对计算机网络不太熟悉的朋友已经被搞蒙了,要想真正搞懂,还需要来简单复习一下计算机网络基础知识:** - -> 我们通常谈计算机网络的五层协议的体系结构是指:应用层、传输层、网络层、数据链路层、物理层。 -> -> **应用层(application-layer)的任务是通过应用进程间的交互来完成特定网络应用。**HTTP 属于应用层协议,它会基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。HTTP协议工作于客户端-服务端架构为上。浏览器作为HTTP客户端通过 URL 向HTTP服务端即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息。HTTP协议建立在 TCP 协议之上。 -> -> **运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务**。TCP是传输层协议,主要解决数据如何在网络中传输。相比于UDP,**TCP** 提供的是**面向连接**的,**可靠的**数据传输服务。 - -### RPC框架功能更齐全 - -成熟的 RPC框架还提供好了“服务自动注册与发现”、"智能负载均衡"、“可视化的服务治理和运维”、“运行期流量调度”等等功能,这些也算是选择 -RPC 进行服务注册和发现的一方面原因吧! - -**相关阅读:** - -- http://www.ruanyifeng.com/blog/2016/08/http.html (HTTP 协议入门- 阮一峰) - -### 一个常见的错误观点 - -很多文章中还会提到说 HTTP 协议相较于自定义 TCP 报文协议,增加的开销在于连接的建立与断开,但是这个观点已经被否认,下面截取自知乎中一个回答,原回答地址:https://www.zhihu.com/question/41609070/answer/191965937。 - ->首先要否认一点 HTTP 协议相较于自定义 TCP 报文协议,增加的开销在于连接的建立与断开。HTTP 协议是支持连接池复用的,也就是建立一定数量的连接不断开,并不会频繁的创建和销毁连接。二一要说的是 HTTP 也可以使用 Protobuf 这种二进制编码协议对内容进行编码,因此二者最大的区别还是在传输协议上。 - - - diff --git a/docs/system-design/framework/ZooKeeper-plus.md b/docs/system-design/framework/ZooKeeper-plus.md deleted file mode 100644 index d68a4c4..0000000 --- a/docs/system-design/framework/ZooKeeper-plus.md +++ /dev/null @@ -1,375 +0,0 @@ -[FrancisQ](https://juejin.im/user/5c33853851882525ea106810) 投稿。 - -# ZooKeeper - -## 好久不见 - -离上一篇文章的发布也快一个月了,想想已经快一个月没写东西了,其中可能有期末考试、课程设计和驾照考试,但这都不是借口! - -一到冬天就懒的不行,望广大掘友督促我🙄🙄✍️✍️。 - -> 文章很长,先赞后看,养成习惯。❤️ 🧡 💛 💚 💙 💜 - -## 什么是ZooKeeper - -`ZooKeeper` 由 `Yahoo` 开发,后来捐赠给了 `Apache` ,现已成为 `Apache` 顶级项目。`ZooKeeper` 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 `Paxos` 算法的 `ZAB` 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理、分布式事务等。 - -![zookeeper](http://img.francisqiang.top/img/Zookeeper.jpg) - -简单来说, `ZooKeeper` 是一个 **分布式协调服务框架** 。分布式?协调服务?这啥玩意?🤔🤔 - -其实解释到分布式这个概念的时候,我发现有些同学并不是能把 **分布式和集群 **这两个概念很好的理解透。前段时间有同学和我探讨起分布式的东西,他说分布式不就是加机器吗?一台机器不够用再加一台抗压呗。当然加机器这种说法也无可厚非,你一个分布式系统必定涉及到多个机器,但是你别忘了,计算机学科中还有一个相似的概念—— `Cluster` ,集群不也是加机器吗?但是 集群 和 分布式 其实就是两个完全不同的概念。 - -比如,我现在有一个秒杀服务,并发量太大单机系统承受不住,那我加几台服务器也 **一样** 提供秒杀服务,这个时候就是 **`Cluster` 集群** 。 - -![cluster](http://img.francisqiang.top/img/cluster.jpg) - -但是,我现在换一种方式,我将一个秒杀服务 **拆分成多个子服务** ,比如创建订单服务,增加积分服务,扣优惠券服务等等,**然后我将这些子服务都部署在不同的服务器上** ,这个时候就是 **`Distributed` 分布式** 。 - -![distributed](http://img.francisqiang.top/img/distributed.jpg) - -而我为什么反驳同学所说的分布式就是加机器呢?因为我认为加机器更加适用于构建集群,因为它真是只有加机器。而对于分布式来说,你首先需要将业务进行拆分,然后再加机器(不仅仅是加机器那么简单),同时你还要去解决分布式带来的一系列问题。 - -![](http://img.francisqiang.top/img/miao.jpg) - -比如各个分布式组件如何协调起来,如何减少各个系统之间的耦合度,分布式事务的处理,如何去配置整个分布式系统等等。`ZooKeeper` 主要就是解决这些问题的。 - -## 一致性问题 - -设计一个分布式系统必定会遇到一个问题—— **因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡** 。这就是著名的 `CAP` 定理。 - -理解起来其实很简单,比如说把一个班级作为整个系统,而学生是系统中的一个个独立的子系统。这个时候班里的小红小明偷偷谈恋爱被班里的大嘴巴小花发现了,小花欣喜若狂告诉了周围的人,然后小红小明谈恋爱的消息在班级里传播起来了。当在消息的传播(散布)过程中,你抓到一个同学问他们的情况,如果回答你不知道,那么说明整个班级系统出现了数据不一致的问题(因为小花已经知道这个消息了)。而如果他直接不回答你,因为整个班级有消息在进行传播(为了保证一致性,需要所有人都知道才可提供服务),这个时候就出现了系统的可用性问题。 - -![](http://img.francisqiang.top/img/垃圾例子.jpg) - -而上述前者就是 `Eureka` 的处理方式,它保证了AP(可用性),后者就是我们今天所要将的 `ZooKeeper` 的处理方式,它保证了CP(数据一致性)。 - -## 一致性协议和算法 - -而为了解决数据一致性问题,在科学家和程序员的不断探索中,就出现了很多的一致性协议和算法。比如 2PC(两阶段提交),3PC(三阶段提交),Paxos算法等等。 - -这时候请你思考一个问题,同学之间如果采用传纸条的方式去传播消息,那么就会出现一个问题——我咋知道我的小纸条有没有传到我想要传递的那个人手中呢?万一被哪个小家伙给劫持篡改了呢,对吧? - -![](http://img.francisqiang.top/img/neigui.jpg) - -这个时候就引申出一个概念—— **拜占庭将军问题** 。它意指 **在不可靠信道上试图通过消息传递的方式达到一致性是不可能的**, 所以所有的一致性算法的 **必要前提** 就是安全可靠的消息通道。 - -而为什么要去解决数据一致性的问题?你想想,如果一个秒杀系统将服务拆分成了下订单和加积分服务,这两个服务部署在不同的机器上了,万一在消息的传播过程中积分系统宕机了,总不能你这边下了订单却没加积分吧?你总得保证两边的数据需要一致吧? - -### 2PC(两阶段提交) - -两阶段提交是一种保证分布式系统数据一致性的协议,现在很多数据库都是采用的两阶段提交协议来完成 **分布式事务** 的处理。 - -在介绍2PC之前,我们先来想想分布式事务到底有什么问题呢? - -还拿秒杀系统的下订单和加积分两个系统来举例吧(我想你们可能都吐了🤮🤮🤮),我们此时下完订单会发个消息给积分系统告诉它下面该增加积分了。如果我们仅仅是发送一个消息也不收回复,那么我们的订单系统怎么能知道积分系统的收到消息的情况呢?如果我们增加一个收回复的过程,那么当积分系统收到消息后返回给订单系统一个 `Response` ,但在中间出现了网络波动,那个回复消息没有发送成功,订单系统是不是以为积分系统消息接收失败了?它是不是会回滚事务?但此时积分系统是成功收到消息的,它就会去处理消息然后给用户增加积分,这个时候就会出现积分加了但是订单没下成功。 - -所以我们所需要解决的是在分布式系统中,整个调用链中,我们所有服务的数据处理要么都成功要么都失败,即所有服务的 **原子性问题** 。 - -在两阶段提交中,主要涉及到两个角色,分别是协调者和参与者。 - -第一阶段:当要执行一个分布式事务的时候,事务发起者首先向协调者发起事务请求,然后协调者会给所有参与者发送 `prepare` 请求(其中包括事务内容)告诉参与者你们需要执行事务了,如果能执行我发的事务内容那么就先执行但不提交,执行后请给我回复。然后参与者收到 `prepare` 消息后,他们会开始执行事务(但不提交),并将 `Undo` 和 `Redo` 信息记入事务日志中,之后参与者就向协调者反馈是否准备好了。 - -第二阶段:第二阶段主要是协调者根据参与者反馈的情况来决定接下来是否可以进行事务的提交操作,即提交事务或者回滚事务。 - -比如这个时候 **所有的参与者** 都返回了准备好了的消息,这个时候就进行事务的提交,协调者此时会给所有的参与者发送 **`Commit` 请求** ,当参与者收到 `Commit` 请求的时候会执行前面执行的事务的 **提交操作** ,提交完毕之后将给协调者发送提交成功的响应。 - -而如果在第一阶段并不是所有参与者都返回了准备好了的消息,那么此时协调者将会给所有参与者发送 **回滚事务的 `rollback` 请求**,参与者收到之后将会 **回滚它在第一阶段所做的事务处理** ,然后再将处理情况返回给协调者,最终协调者收到响应后便给事务发起者返回处理失败的结果。 - -![2PC流程](http://img.francisqiang.top/img/2PC.jpg) - -个人觉得 2PC 实现得还是比较鸡肋的,因为事实上它只解决了各个事务的原子性问题,随之也带来了很多的问题。 - -![](http://img.francisqiang.top/img/laji.jpg) - -* **单点故障问题**,如果协调者挂了那么整个系统都处于不可用的状态了。 -* **阻塞问题**,即当协调者发送 `prepare` 请求,参与者收到之后如果能处理那么它将会进行事务的处理但并不提交,这个时候会一直占用着资源不释放,如果此时协调者挂了,那么这些资源都不会再释放了,这会极大影响性能。 -* **数据不一致问题**,比如当第二阶段,协调者只发送了一部分的 `commit` 请求就挂了,那么也就意味着,收到消息的参与者会进行事务的提交,而后面没收到的则不会进行事务提交,那么这时候就会产生数据不一致性问题。 - -### 3PC(三阶段提交) - -因为2PC存在的一系列问题,比如单点,容错机制缺陷等等,从而产生了 **3PC(三阶段提交)** 。那么这三阶段又分别是什么呢? - -> 千万不要吧PC理解成个人电脑了,其实他们是 phase-commit 的缩写,即阶段提交。 - -1. **CanCommit阶段**:协调者向所有参与者发送 `CanCommit` 请求,参与者收到请求后会根据自身情况查看是否能执行事务,如果可以则返回 YES 响应并进入预备状态,否则返回 NO 。 -2. **PreCommit阶段**:协调者根据参与者返回的响应来决定是否可以进行下面的 `PreCommit` 操作。如果上面参与者返回的都是 YES,那么协调者将向所有参与者发送 `PreCommit` 预提交请求,**参与者收到预提交请求后,会进行事务的执行操作,并将 `Undo` 和 `Redo` 信息写入事务日志中** ,最后如果参与者顺利执行了事务则给协调者返回成功的响应。如果在第一阶段协调者收到了 **任何一个 NO** 的信息,或者 **在一定时间内** 并没有收到全部的参与者的响应,那么就会中断事务,它会向所有参与者发送中断请求(abort),参与者收到中断请求之后会立即中断事务,或者在一定时间内没有收到协调者的请求,它也会中断事务。 -3. **DoCommit阶段**:这个阶段其实和 `2PC` 的第二阶段差不多,如果协调者收到了所有参与者在 `PreCommit` 阶段的 YES 响应,那么协调者将会给所有参与者发送 `DoCommit` 请求,**参与者收到 `DoCommit` 请求后则会进行事务的提交工作**,完成后则会给协调者返回响应,协调者收到所有参与者返回的事务提交成功的响应之后则完成事务。若协调者在 `PreCommit` 阶段 **收到了任何一个 NO 或者在一定时间内没有收到所有参与者的响应** ,那么就会进行中断请求的发送,参与者收到中断请求后则会 **通过上面记录的回滚日志** 来进行事务的回滚操作,并向协调者反馈回滚状况,协调者收到参与者返回的消息后,中断事务。 - -![3PC流程](http://img.francisqiang.top/img/3PC.jpg) - -> 这里是 `3PC` 在成功的环境下的流程图,你可以看到 `3PC` 在很多地方进行了超时中断的处理,比如协调者在指定时间内为收到全部的确认消息则进行事务中断的处理,这样能 **减少同步阻塞的时间** 。还有需要注意的是,**`3PC` 在 `DoCommit` 阶段参与者如未收到协调者发送的提交事务的请求,它会在一定时间内进行事务的提交**。为什么这么做呢?是因为这个时候我们肯定**保证了在第一阶段所有的协调者全部返回了可以执行事务的响应**,这个时候我们有理由**相信其他系统都能进行事务的执行和提交**,所以**不管**协调者有没有发消息给参与者,进入第三阶段参与者都会进行事务的提交操作。 - -总之,`3PC` 通过一系列的超时机制很好的缓解了阻塞问题,但是最重要的一致性并没有得到根本的解决,比如在 `PreCommit` 阶段,当一个参与者收到了请求之后其他参与者和协调者挂了或者出现了网络分区,这个时候收到消息的参与者都会进行事务提交,这就会出现数据不一致性问题。 - -所以,要解决一致性问题还需要靠 `Paxos` 算法⭐️ ⭐️ ⭐️ 。 - -### `Paxos` 算法 - -`Paxos` 算法是基于**消息传递且具有高度容错特性的一致性算法**,是目前公认的解决分布式一致性问题最有效的算法之一,**其解决的问题就是在分布式系统中如何就某个值(决议)达成一致** 。 - -在 `Paxos` 中主要有三个角色,分别为 `Proposer提案者`、`Acceptor表决者`、`Learner学习者`。`Paxos` 算法和 `2PC` 一样,也有两个阶段,分别为 `Prepare` 和 `accept` 阶段。 - -#### prepare 阶段 - -* `Proposer提案者`:负责提出 `proposal`,每个提案者在提出提案时都会首先获取到一个 **具有全局唯一性的、递增的提案编号N**,即在整个集群中是唯一的编号 N,然后将该编号赋予其要提出的提案,在**第一阶段是只将提案编号发送给所有的表决者**。 -* `Acceptor表决者`:每个表决者在 `accept` 某提案后,会将该提案编号N记录在本地,这样每个表决者中保存的已经被 accept 的提案中会存在一个**编号最大的提案**,其编号假设为 `maxN`。每个表决者仅会 `accept` 编号大于自己本地 `maxN` 的提案,在批准提案时表决者会将以前接受过的最大编号的提案作为响应反馈给 `Proposer` 。 - -> 下面是 `prepare` 阶段的流程图,你可以对照着参考一下。 - -![paxos第一阶段](http://img.francisqiang.top/img/paxos1.jpg) - -#### accept 阶段 - -当一个提案被 `Proposer` 提出后,如果 `Proposer` 收到了超过半数的 `Acceptor` 的批准(`Proposer` 本身同意),那么此时 `Proposer` 会给所有的 `Acceptor` 发送真正的提案(你可以理解为第一阶段为试探),这个时候 `Proposer` 就会发送提案的内容和提案编号。 - -表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号,如果该提案编号 **大于等于** 已经批准过的最大提案编号,那么就 `accept` 该提案(此时执行提案内容但不提交),随后将情况返回给 `Proposer` 。如果不满足则不回应或者返回 NO 。 - -![paxos第二阶段1](http://img.francisqiang.top/img/paxos2.jpg) - -当 `Proposer` 收到超过半数的 `accept` ,那么它这个时候会向所有的 `acceptor` 发送提案的提交请求。需要注意的是,因为上述仅仅是超过半数的 `acceptor` 批准执行了该提案内容,其他没有批准的并没有执行该提案内容,所以这个时候需要**向未批准的 `acceptor` 发送提案内容和提案编号并让它无条件执行和提交**,而对于前面已经批准过该提案的 `acceptor` 来说 **仅仅需要发送该提案的编号** ,让 `acceptor` 执行提交就行了。 - -![paxos第二阶段2](http://img.francisqiang.top/img/paxos3.jpg) - -而如果 `Proposer` 如果没有收到超过半数的 `accept` 那么它将会将 **递增** 该 `Proposal` 的编号,然后 **重新进入 `Prepare` 阶段** 。 - -> 对于 `Learner` 来说如何去学习 `Acceptor` 批准的提案内容,这有很多方式,读者可以自己去了解一下,这里不做过多解释。 - -#### `paxos` 算法的死循环问题 - -其实就有点类似于两个人吵架,小明说我是对的,小红说我才是对的,两个人据理力争的谁也不让谁🤬🤬。 - -比如说,此时提案者 P1 提出一个方案 M1,完成了 `Prepare` 阶段的工作,这个时候 `acceptor` 则批准了 M1,但是此时提案者 P2 同时也提出了一个方案 M2,它也完成了 `Prepare` 阶段的工作。然后 P1 的方案已经不能在第二阶段被批准了(因为 `acceptor` 已经批准了比 M1 更大的 M2),所以 P1 自增方案变为 M3 重新进入 `Prepare` 阶段,然后 `acceptor` ,又批准了新的 M3 方案,它又不能批准 M2 了,这个时候 M2 又自增进入 `Prepare` 阶段。。。 - -就这样无休无止的永远提案下去,这就是 `paxos` 算法的死循环问题。 - -![](http://img.francisqiang.top/img/chaojia.jpg) - -那么如何解决呢?很简单,人多了容易吵架,我现在 **就允许一个能提案** 就行了。 - -## 引出 `ZAB` - -### `Zookeeper` 架构 - -作为一个优秀高效且可靠的分布式协调框架,`ZooKeeper` 在解决分布式数据一致性问题时并没有直接使用 `Paxos` ,而是专门定制了一致性协议叫做 `ZAB(ZooKeeper Automic Broadcast)` 原子广播协议,该协议能够很好地支持 **崩溃恢复** 。 - -![Zookeeper架构](http://img.francisqiang.top/img/Zookeeper架构.jpg) - -### `ZAB` 中的三个角色 - -和介绍 `Paxos` 一样,在介绍 `ZAB` 协议之前,我们首先来了解一下在 `ZAB` 中三个主要的角色,`Leader 领导者`、`Follower跟随者`、`Observer观察者` 。 - -* `Leader` :集群中 **唯一的写请求处理者** ,能够发起投票(投票也是为了进行写请求)。 -* `Follower`:能够接收客户端的请求,如果是读请求则可以自己处理,**如果是写请求则要转发给 `Leader`** 。在选举过程中会参与投票,**有选举权和被选举权** 。 -* `Observer` :就是没有选举权和被选举权的 `Follower` 。 - -在 `ZAB` 协议中对 `zkServer`(即上面我们说的三个角色的总称) 还有两种模式的定义,分别是 **消息广播** 和 **崩溃恢复** 。 - -### 消息广播模式 - -说白了就是 `ZAB` 协议是如何处理写请求的,上面我们不是说只有 `Leader` 能处理写请求嘛?那么我们的 `Follower` 和 `Observer` 是不是也需要 **同步更新数据** 呢?总不能数据只在 `Leader` 中更新了,其他角色都没有得到更新吧? - -不就是 **在整个集群中保持数据的一致性** 嘛?如果是你,你会怎么做呢? - -![](http://img.francisqiang.top/img/zenmezhidao.jpg) - -废话,第一步肯定需要 `Leader` 将写请求 **广播** 出去呀,让 `Leader` 问问 `Followers` 是否同意更新,如果超过半数以上的同意那么就进行 `Follower` 和 `Observer` 的更新(和 `Paxos` 一样)。当然这么说有点虚,画张图理解一下。 - -![消息广播](http://img.francisqiang.top/img/消息广播1.jpg) - -嗯。。。看起来很简单,貌似懂了🤥🤥🤥。这两个 `Queue` 哪冒出来的?答案是 **`ZAB` 需要让 `Follower` 和 `Observer` 保证顺序性** 。何为顺序性,比如我现在有一个写请求A,此时 `Leader` 将请求A广播出去,因为只需要半数同意就行,所以可能这个时候有一个 `Follower` F1因为网络原因没有收到,而 `Leader` 又广播了一个请求B,因为网络原因,F1竟然先收到了请求B然后才收到了请求A,这个时候请求处理的顺序不同就会导致数据的不同,从而 **产生数据不一致问题** 。 - -所以在 `Leader` 这端,它为每个其他的 `zkServer` 准备了一个 **队列** ,采用先进先出的方式发送消息。由于协议是 **通过 `TCP` **来进行网络通信的,保证了消息的发送顺序性,接受顺序性也得到了保证。 - -除此之外,在 `ZAB` 中还定义了一个 **全局单调递增的事务ID `ZXID`** ,它是一个64位long型,其中高32位表示 `epoch` 年代,低32位表示事务id。`epoch` 是会根据 `Leader` 的变化而变化的,当一个 `Leader` 挂了,新的 `Leader` 上位的时候,年代(`epoch`)就变了。而低32位可以简单理解为递增的事务id。 - -定义这个的原因也是为了顺序性,每个 `proposal` 在 `Leader` 中生成后需要 **通过其 `ZXID` 来进行排序** ,才能得到处理。 - -### 崩溃恢复模式 - -说到崩溃恢复我们首先要提到 `ZAB` 中的 `Leader` 选举算法,当系统出现崩溃影响最大应该是 `Leader` 的崩溃,因为我们只有一个 `Leader` ,所以当 `Leader` 出现问题的时候我们势必需要重新选举 `Leader` 。 - -`Leader` 选举可以分为两个不同的阶段,第一个是我们提到的 `Leader` 宕机需要重新选举,第二则是当 `Zookeeper` 启动时需要进行系统的 `Leader` 初始化选举。下面我先来介绍一下 `ZAB` 是如何进行初始化选举的。 - -假设我们集群中有3台机器,那也就意味着我们需要两台以上同意(超过半数)。比如这个时候我们启动了 `server1` ,它会首先 **投票给自己** ,投票内容为服务器的 `myid` 和 `ZXID` ,因为初始化所以 `ZXID` 都为0,此时 `server1` 发出的投票为 (1,0)。但此时 `server1` 的投票仅为1,所以不能作为 `Leader` ,此时还在选举阶段所以整个集群处于 **`Looking` 状态**。 - -接着 `server2` 启动了,它首先也会将投票选给自己(2,0),并将投票信息广播出去(`server1`也会,只是它那时没有其他的服务器了),`server1` 在收到 `server2` 的投票信息后会将投票信息与自己的作比较。**首先它会比较 `ZXID` ,`ZXID` 大的优先为 `Leader`,如果相同则比较 `myid`,`myid` 大的优先作为 `Leader`**。所以此时`server1` 发现 `server2` 更适合做 `Leader`,它就会将自己的投票信息更改为(2,0)然后再广播出去,之后`server2` 收到之后发现和自己的一样无需做更改,并且自己的 **投票已经超过半数** ,则 **确定 `server2` 为 `Leader`**,`server1` 也会将自己服务器设置为 `Following` 变为 `Follower`。整个服务器就从 `Looking` 变为了正常状态。 - -当 `server3` 启动发现集群没有处于 `Looking` 状态时,它会直接以 `Follower` 的身份加入集群。 - -还是前面三个 `server` 的例子,如果在整个集群运行的过程中 `server2` 挂了,那么整个集群会如何重新选举 `Leader` 呢?其实和初始化选举差不多。 - -首先毫无疑问的是剩下的两个 `Follower` 会将自己的状态 **从 `Following` 变为 `Looking` 状态** ,然后每个 `server` 会向初始化投票一样首先给自己投票(这不过这里的 `zxid` 可能不是0了,这里为了方便随便取个数字)。 - -假设 `server1` 给自己投票为(1,99),然后广播给其他 `server`,`server3` 首先也会给自己投票(3,95),然后也广播给其他 `server`。`server1` 和 `server3` 此时会收到彼此的投票信息,和一开始选举一样,他们也会比较自己的投票和收到的投票(`zxid` 大的优先,如果相同那么就 `myid` 大的优先)。这个时候 `server1` 收到了 `server3` 的投票发现没自己的合适故不变,`server3` 收到 `server1` 的投票结果后发现比自己的合适于是更改投票为(1,99)然后广播出去,最后 `server1` 收到了发现自己的投票已经超过半数就把自己设为 `Leader`,`server3` 也随之变为 `Follower`。 - -> 请注意 `ZooKeeper` 为什么要设置奇数个结点?比如这里我们是三个,挂了一个我们还能正常工作,挂了两个我们就不能正常工作了(已经没有超过半数的节点数了,所以无法进行投票等操作了)。而假设我们现在有四个,挂了一个也能工作,**但是挂了两个也不能正常工作了**,这是和三个一样的,而三个比四个还少一个,带来的效益是一样的,所以 `Zookeeper` 推荐奇数个 `server` 。 - -那么说完了 `ZAB` 中的 `Leader` 选举方式之后我们再来了解一下 **崩溃恢复** 是什么玩意? - -其实主要就是 **当集群中有机器挂了,我们整个集群如何保证数据一致性?** - -如果只是 `Follower` 挂了,而且挂的没超过半数的时候,因为我们一开始讲了在 `Leader` 中会维护队列,所以不用担心后面的数据没接收到导致数据不一致性。 - -如果 `Leader` 挂了那就麻烦了,我们肯定需要先暂停服务变为 `Looking` 状态然后进行 `Leader` 的重新选举(上面我讲过了),但这个就要分为两种情况了,分别是 **确保已经被Leader提交的提案最终能够被所有的Follower提交** 和 **跳过那些已经被丢弃的提案** 。 - -确保已经被Leader提交的提案最终能够被所有的Follower提交是什么意思呢? - -假设 `Leader (server2)` 发送 `commit` 请求(忘了请看上面的消息广播模式),他发送给了 `server3`,然后要发给 `server1` 的时候突然挂了。这个时候重新选举的时候我们如果把 `server1` 作为 `Leader` 的话,那么肯定会产生数据不一致性,因为 `server3` 肯定会提交刚刚 `server2` 发送的 `commit` 请求的提案,而 `server1` 根本没收到所以会丢弃。 - -![崩溃恢复](http://img.francisqiang.top/img/崩溃恢复1.jpg) - -那怎么解决呢? - -聪明的同学肯定会质疑,**这个时候 `server1` 已经不可能成为 `Leader` 了,因为 `server1` 和 `server3` 进行投票选举的时候会比较 `ZXID` ,而此时 `server3` 的 `ZXID` 肯定比 `server1` 的大了**。(不理解可以看前面的选举算法) - -那么跳过那些已经被丢弃的提案又是什么意思呢? - -假设 `Leader (server2)` 此时同意了提案N1,自身提交了这个事务并且要发送给所有 `Follower` 要 `commit` 的请求,却在这个时候挂了,此时肯定要重新进行 `Leader` 的选举,比如说此时选 `server1` 为 `Leader` (这无所谓)。但是过了一会,这个 **挂掉的 `Leader` 又重新恢复了** ,此时它肯定会作为 `Follower` 的身份进入集群中,需要注意的是刚刚 `server2` 已经同意提交了提案N1,但其他 `server` 并没有收到它的 `commit` 信息,所以其他 `server` 不可能再提交这个提案N1了,这样就会出现数据不一致性问题了,所以 **该提案N1最终需要被抛弃掉** 。 - -![崩溃恢复](http://img.francisqiang.top/img/崩溃恢复2.jpg) - -## Zookeeper的几个理论知识 - -了解了 `ZAB` 协议还不够,它仅仅是 `Zookeeper` 内部实现的一种方式,而我们如何通过 `Zookeeper` 去做一些典型的应用场景呢?比如说集群管理,分布式锁,`Master` 选举等等。 - -这就涉及到如何使用 `Zookeeper` 了,但在使用之前我们还需要掌握几个概念。比如 `Zookeeper` 的 **数据模型** 、**会话机制**、**ACL**、**Watcher机制** 等等。 - -### 数据模型 - -`zookeeper` 数据存储结构与标准的 `Unix` 文件系统非常相似,都是在根节点下挂很多子节点(树型)。但是 `zookeeper` 中没有文件系统中目录与文件的概念,而是 **使用了 `znode` 作为数据节点** 。`znode` 是 `zookeeper` 中的最小数据单元,每个 `znode` 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。 - -![zk数据模型](http://img.francisqiang.top/img/zk数据模型.jpg) - -每个 `znode` 都有自己所属的 **节点类型** 和 **节点状态**。 - -其中节点类型可以分为 **持久节点**、**持久顺序节点**、**临时节点** 和 **临时顺序节点**。 - -* 持久节点:一旦创建就一直存在,直到将其删除。 -* 持久顺序节点:一个父节点可以为其子节点 **维护一个创建的先后顺序** ,这个顺序体现在 **节点名称** 上,是节点名称后自动添加一个由 10 位数字组成的数字串,从 0 开始计数。 -* 临时节点:临时节点的生命周期是与 **客户端会话** 绑定的,**会话消失则节点消失** 。临时节点 **只能做叶子节点** ,不能创建子节点。 -* 临时顺序节点:父节点可以创建一个维持了顺序的临时节点(和前面的持久顺序性节点一样)。 - -节点状态中包含了很多节点的属性比如 `czxid` 、`mzxid` 等等,在 `zookeeper` 中是使用 `Stat` 这个类来维护的。下面我列举一些属性解释。 - -* `czxid`:`Created ZXID`,该数据节点被 **创建** 时的事务ID。 -* `mzxid`:`Modified ZXID`,节点 **最后一次被更新时** 的事务ID。 -* `ctime`:`Created Time`,该节点被创建的时间。 -* `mtime`: `Modified Time`,该节点最后一次被修改的时间。 -* `version`:节点的版本号。 -* `cversion`:**子节点** 的版本号。 -* `aversion`:节点的 `ACL` 版本号。 -* `ephemeralOwner`:创建该节点的会话的 `sessionID` ,如果该节点为持久节点,该值为0。 -* `dataLength`:节点数据内容的长度。 -* `numChildre`:该节点的子节点个数,如果为临时节点为0。 -* `pzxid`:该节点子节点列表最后一次被修改时的事务ID,注意是子节点的 **列表** ,不是内容。 - -### 会话 - -我想这个对于后端开发的朋友肯定不陌生,不就是 `session` 吗?只不过 `zk` 客户端和服务端是通过 **`TCP` 长连接** 维持的会话机制,其实对于会话来说你可以理解为 **保持连接状态** 。 - -在 `zookeeper` 中,会话还有对应的事件,比如 `CONNECTION_LOSS 连接丢失事件` 、`SESSION_MOVED 会话转移事件` 、`SESSION_EXPIRED 会话超时失效事件` 。 - -### ACL - -`ACL` 为 `Access Control Lists` ,它是一种权限控制。在 `zookeeper` 中定义了5种权限,它们分别为: - -* `CREATE` :创建子节点的权限。 -* `READ`:获取节点数据和子节点列表的权限。 -* `WRITE`:更新节点数据的权限。 -* `DELETE`:删除子节点的权限。 -* `ADMIN`:设置节点 ACL 的权限。 - -### Watcher机制 - -`Watcher` 为事件监听器,是 `zk` 非常重要的一个特性,很多功能都依赖于它,它有点类似于订阅的方式,即客户端向服务端 **注册** 指定的 `watcher` ,当服务端符合了 `watcher` 的某些事件或要求则会 **向客户端发送事件通知** ,客户端收到通知后找到自己定义的 `Watcher` 然后 **执行相应的回调方法** 。 - -![watcher机制](http://img.francisqiang.top/img/watcher机制.jpg) - -## Zookeeper的几个典型应用场景 - -前面说了这么多的理论知识,你可能听得一头雾水,这些玩意有啥用?能干啥事?别急,听我慢慢道来。 - -![](http://img.francisqiang.top/img/feijie.jpg) - -### 选主 - -还记得上面我们的所说的临时节点吗?因为 `Zookeeper` 的强一致性,能够很好地在保证 **在高并发的情况下保证节点创建的全局唯一性** (即无法重复创建同样的节点)。 - -利用这个特性,我们可以 **让多个客户端创建一个指定的节点** ,创建成功的就是 `master`。 - -但是,如果这个 `master` 挂了怎么办??? - -你想想为什么我们要创建临时节点?还记得临时节点的生命周期吗?`master` 挂了是不是代表会话断了?会话断了是不是意味着这个节点没了?还记得 `watcher` 吗?我们是不是可以 **让其他不是 `master` 的节点监听节点的状态** ,比如说我们监听这个临时节点的父节点,如果子节点个数变了就代表 `master` 挂了,这个时候我们 **触发回调函数进行重新选举** ,或者我们直接监听节点的状态,我们可以通过节点是否已经失去连接来判断 `master` 是否挂了等等。 - -![选主](http://img.francisqiang.top/img/选主.jpg) - -总的来说,我们可以完全 **利用 临时节点、节点状态 和 `watcher` 来实现选主的功能**,临时节点主要用来选举,节点状态和`watcher` 可以用来判断 `master` 的活性和进行重新选举。 - -### 分布式锁 - -分布式锁的实现方式有很多种,比如 `Redis` 、数据库 、`zookeeper` 等。个人认为 `zookeeper` 在实现分布式锁这方面是非常非常简单的。 - -上面我们已经提到过了 **zk在高并发的情况下保证节点创建的全局唯一性**,这玩意一看就知道能干啥了。实现互斥锁呗,又因为能在分布式的情况下,所以能实现分布式锁呗。 - -如何实现呢?这玩意其实跟选主基本一样,我们也可以利用临时节点的创建来实现。 - -首先肯定是如何获取锁,因为创建节点的唯一性,我们可以让多个客户端同时创建一个临时节点,**创建成功的就说明获取到了锁** 。然后没有获取到锁的客户端也像上面选主的非主节点创建一个 `watcher` 进行节点状态的监听,如果这个互斥锁被释放了(可能获取锁的客户端宕机了,或者那个客户端主动释放了锁)可以调用回调函数重新获得锁。 - -> `zk` 中不需要向 `redis` 那样考虑锁得不到释放的问题了,因为当客户端挂了,节点也挂了,锁也释放了。是不是很简答? - -那能不能使用 `zookeeper` 同时实现 **共享锁和独占锁** 呢?答案是可以的,不过稍微有点复杂而已。 - -还记得 **有序的节点** 吗? - -这个时候我规定所有创建节点必须有序,当你是读请求(要获取共享锁)的话,如果 **没有比自己更小的节点,或比自己小的节点都是读请求** ,则可以获取到读锁,然后就可以开始读了。**若比自己小的节点中有写请求** ,则当前客户端无法获取到读锁,只能等待前面的写请求完成。 - -如果你是写请求(获取独占锁),若 **没有比自己更小的节点** ,则表示当前客户端可以直接获取到写锁,对数据进行修改。若发现 **有比自己更小的节点,无论是读操作还是写操作,当前客户端都无法获取到写锁** ,等待所有前面的操作完成。 - -这就很好地同时实现了共享锁和独占锁,当然还有优化的地方,比如当一个锁得到释放它会通知所有等待的客户端从而造成 **羊群效应** 。此时你可以通过让等待的节点只监听他们前面的节点。 - -具体怎么做呢?其实也很简单,你可以让 **读请求监听比自己小的最后一个写请求节点,写请求只监听比自己小的最后一个节点** ,感兴趣的小伙伴可以自己去研究一下。 - -### 命名服务 - -如何给一个对象设置ID,大家可能都会想到 `UUID`,但是 `UUID` 最大的问题就在于它太长了。。。(太长不一定是好事,嘿嘿嘿)。那么在条件允许的情况下,我们能不能使用 `zookeeper` 来实现呢? - -我们之前提到过 `zookeeper` 是通过 **树形结构** 来存储数据节点的,那也就是说,对于每个节点的 **全路径**,它必定是唯一的,我们可以使用节点的全路径作为命名方式了。而且更重要的是,路径是我们可以自己定义的,这对于我们对有些有语意的对象的ID设置可以更加便于理解。 - -### 集群管理和注册中心 - -看到这里是不是觉得 `zookeeper` 实在是太强大了,它怎么能这么能干! - -别急,它能干的事情还很多呢。可能我们会有这样的需求,我们需要了解整个集群中有多少机器在工作,我们想对及群众的每台机器的运行时状态进行数据采集,对集群中机器进行上下线操作等等。 - -而 `zookeeper` 天然支持的 `watcher` 和 临时节点能很好的实现这些需求。我们可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的 `watcher` 进行状态监控和回调。 - -![集群管理](http://img.francisqiang.top/img/集群管理.jpg) - -至于注册中心也很简单,我们同样也是让 **服务提供者** 在 `zookeeper` 中创建一个临时节点并且将自己的 `ip、port、调用方式` 写入节点,当 **服务消费者** 需要进行调用的时候会 **通过注册中心找到相应的服务的地址列表(IP端口什么的)** ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。 - -当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听,我记得 `Eureka` 会先试错,然后再更新)。 - -![注册中心](http://img.francisqiang.top/img/注册中心.jpg) - -## 总结 - -看到这里的同学实在是太有耐心了👍👍👍,如果觉得我写得不错的话点个赞哈。 - -不知道大家是否还记得我讲了什么😒。 - -![](http://img.francisqiang.top/img/masmmei.jpg) - -这篇文章中我带大家入门了 `zookeeper` 这个强大的分布式协调框架。现在我们来简单梳理一下整篇文章的内容。 - -* 分布式与集群的区别 - -* `2PC` 、`3PC` 以及 `paxos` 算法这些一致性框架的原理和实现。 - -* `zookeeper` 专门的一致性算法 `ZAB` 原子广播协议的内容(`Leader` 选举、崩溃恢复、消息广播)。 - -* `zookeeper` 中的一些基本概念,比如 `ACL`,数据节点,会话,`watcher`机制等等。 - -* `zookeeper` 的典型应用场景,比如选主,注册中心等等。 - - 如果忘了可以回去看看再次理解一下,如果有疑问和建议欢迎提出🤝🤝🤝。 \ No newline at end of file diff --git a/docs/system-design/framework/ZooKeeper.md b/docs/system-design/framework/ZooKeeper.md deleted file mode 100644 index 4b10ca0..0000000 --- a/docs/system-design/framework/ZooKeeper.md +++ /dev/null @@ -1,185 +0,0 @@ -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/56385654.jpg) -## 前言 - -相信大家对 ZooKeeper 应该不算陌生。但是你真的了解 ZooKeeper 是个什么东西吗?如果别人/面试官让你给他讲讲 ZooKeeper 是个什么东西,你能回答到什么地步呢? - -我本人曾经使用过 ZooKeeper 作为 Dubbo 的注册中心,另外在搭建 solr 集群的时候,我使用到了 ZooKeeper 作为 solr 集群的管理工具。前几天,总结项目经验的时候,我突然问自己 ZooKeeper 到底是个什么东西?想了半天,脑海中只是简单的能浮现出几句话:“①Zookeeper 可以被用作注册中心。 ②Zookeeper 是 Hadoop 生态系统的一员;③构建 Zookeeper 集群的时候,使用的服务器最好是奇数台。” 可见,我对于 Zookeeper 的理解仅仅是停留在了表面。 - -所以,**通过本文,希望带大家稍微详细的了解一下 ZooKeeper 。如果没有学过 ZooKeeper ,那么本文将会是你进入 ZooKeeper 大门的垫脚砖。如果你已经接触过 ZooKeeper ,那么本文将带你回顾一下 ZooKeeper 的一些基础概念。** - -最后,**本文只涉及 ZooKeeper 的一些概念,并不涉及 ZooKeeper 的使用以及 ZooKeeper 集群的搭建。** 网上有介绍 ZooKeeper 的使用以及搭建 ZooKeeper 集群的文章,大家有需要可以自行查阅。 - -## 一 什么是 ZooKeeper - -### ZooKeeper 的由来 - -**下面这段内容摘自《从Paxos到Zookeeper 》第四章第一节的某段内容,推荐大家阅读以下:** - -> Zookeeper最早起源于雅虎研究院的一个研究小组。在当时,研究人员发现,在雅虎内部很多大型系统基本都需要依赖一个类似的系统来进行分布式协调,但是这些系统往往都存在分布式单点问题。所以,**雅虎的开发人员就试图开发一个通用的无单点问题的分布式协调框架,以便让开发人员将精力集中在处理业务逻辑上。** -> ->关于“ZooKeeper”这个项目的名字,其实也有一段趣闻。在立项初期,考虑到之前内部很多项目都是使用动物的名字来命名的(例如著名的Pig项目),雅虎的工程师希望给这个项目也取一个动物的名字。时任研究院的首席科学家RaghuRamakrishnan开玩笑地说:“在这样下去,我们这儿就变成动物园了!”此话一出,大家纷纷表示就叫动物园管理员吧一一一因为各个以动物命名的分布式组件放在一起,**雅虎的整个分布式系统看上去就像一个大型的动物园了,而Zookeeper正好要用来进行分布式环境的协调一一于是,Zookeeper的名字也就由此诞生了。** - - -### 1.1 ZooKeeper 概览 - -ZooKeeper 是一个开源的分布式协调服务,ZooKeeper框架最初是在“Yahoo!"上构建的,用于以简单而稳健的方式访问他们的应用程序。 后来,Apache ZooKeeper成为Hadoop,HBase和其他分布式框架使用的有组织服务的标准。 例如,Apache HBase使用ZooKeeper跟踪分布式数据的状态。**ZooKeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。** - -> **原语:** 操作系统或计算机网络用语范畴。是由若干条指令组成的,用于完成一定功能的一个过程。具有不可分割性·即原语的执行必须是连续的,在执行过程中不允许被中断。 - -**ZooKeeper 是一个典型的分布式数据一致性解决方案,分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。** - -**Zookeeper 一个最常用的使用场景就是用于担任服务生产者和服务消费者的注册中心(提供发布订阅服务)。** 服务生产者将自己提供的服务注册到Zookeeper中心,服务的消费者在进行服务调用的时候先到Zookeeper中查找服务,获取到服务生产者的详细信息之后,再去调用服务生产者的内容与数据。如下图所示,在 Dubbo架构中 Zookeeper 就担任了注册中心这一角色。 - -![Dubbo](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/35571782.jpg) - -### 1.2 结合个人使用情况的讲一下 ZooKeeper - -在我自己做过的项目中,主要使用到了 ZooKeeper 作为 Dubbo 的注册中心(Dubbo 官方推荐使用 ZooKeeper注册中心)。另外在搭建 solr 集群的时候,我使用 ZooKeeper 作为 solr 集群的管理工具。这时,ZooKeeper 主要提供下面几个功能:1、集群管理:容错、负载均衡。2、配置文件的集中管理3、集群的入口。 - - -我个人觉得在使用 ZooKeeper 的时候,最好是使用 集群版的 ZooKeeper 而不是单机版的。官网给出的架构图就描述的是一个集群版的 ZooKeeper 。通常 3 台服务器就可以构成一个 ZooKeeper 集群了。 - -**为什么最好使用奇数台服务器构成 ZooKeeper 集群?** - -所谓的zookeeper容错是指,当宕掉几个zookeeper服务器之后,剩下的个数必须大于宕掉的个数的话整个zookeeper才依然可用。假如我们的集群中有n台zookeeper服务器,那么也就是剩下的服务数必须大于n/2。先说一下结论,2n和2n-1的容忍度是一样的,都是n-1,大家可以先自己仔细想一想,这应该是一个很简单的数学问题了。 -比如假如我们有3台,那么最大允许宕掉1台zookeeper服务器,如果我们有4台的的时候也同样只允许宕掉1台。 -假如我们有5台,那么最大允许宕掉2台zookeeper服务器,如果我们有6台的的时候也同样只允许宕掉2台。 - -综上,何必增加那一个不必要的zookeeper呢? - -## 二 关于 ZooKeeper 的一些重要概念 - -### 2.1 重要概念总结 - -- **ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。** -- **为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。** -- **ZooKeeper 将数据保存在内存中,这也就保证了 高吞吐量和低延迟**(但是内存限制了能够存储的容量不太大,此限制也是保持znode中存储的数据量较小的进一步原因)。 -- **ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。**(“读”多于“写”是协调服务的典型场景。) -- **ZooKeeper有临时节点的概念。 当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。而当会话终结时,瞬时节点被删除。持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上。** -- ZooKeeper 底层其实只提供了两个功能:①管理(存储、读取)用户程序提交的数据;②为用户程序提供数据节点监听服务。 - -**下面关于会话(Session)、 Znode、版本、Watcher、ACL概念的总结都在《从Paxos到Zookeeper 》第四章第一节以及第七章第八节有提到,感兴趣的可以看看!** - -### 2.2 会话(Session) - -Session 指的是 ZooKeeper 服务器与客户端会话。**在 ZooKeeper 中,一个客户端连接是指客户端和服务器之间的一个 TCP 长连接**。客户端启动的时候,首先会与服务器建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也开始了。**通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch事件通知。** Session的`sessionTimeout`值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,**只要在`sessionTimeout`规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。** - -**在为客户端创建会话之前,服务端首先会为每个客户端都分配一个sessionID。由于 sessionID 是 Zookeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的,因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证全局唯一。** - -### 2.3 Znode - -**在谈到分布式的时候,我们通常说的“节点"是指组成集群的每一台机器。然而,在Zookeeper中,“节点"分为两类,第一类同样是指构成集群的机器,我们称之为机器节点;第二类则是指数据模型中的数据单元,我们称之为数据节点一一ZNode。** - -Zookeeper将所有数据存储在内存中,数据模型是一棵树(Znode Tree),由斜杠(/)的进行分割的路径,就是一个Znode,例如/foo/path1。每个上都会保存自己的数据内容,同时还会保存一系列属性信息。 - -**在Zookeeper中,node可以分为持久节点和临时节点两类。所谓持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上。而临时节点就不一样了,它的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。** 另外,ZooKeeper还允许用户为每个节点添加一个特殊的属性:**SEQUENTIAL**.一旦节点被标记上这个属性,那么在这个节点被创建的时候,Zookeeper会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。 - -### 2.4 版本 - -在前面我们已经提到,Zookeeper 的每个 ZNode 上都会存储数据,对应于每个ZNode,Zookeeper 都会为其维护一个叫作 **Stat** 的数据结构,Stat 中记录了这个 ZNode 的三个数据版本,分别是version(当前ZNode的版本)、cversion(当前ZNode子节点的版本)和 aversion(当前ZNode的ACL版本)。 - - -### 2.5 Watcher - -**Watcher(事件监听器),是Zookeeper中的一个很重要的特性。Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是Zookeeper实现分布式协调服务的重要特性。** - -### 2.6 ACL - -Zookeeper采用ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。Zookeeper 定义了如下5种权限。 - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/27473480.jpg) - -其中尤其需要注意的是,CREATE和DELETE这两种权限都是针对子节点的权限控制。 - -## 三 ZooKeeper 特点 - -- **顺序一致性:** 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。 -- **原子性:** 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。 -- **单一系统映像 :** 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。 -- **可靠性:** 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。 - -## 四 ZooKeeper 设计目标 - -### 4.1 简单的数据模型 - -ZooKeeper 允许分布式进程通过共享的层次结构命名空间进行相互协调,这与标准文件系统类似。 名称空间由 ZooKeeper 中的数据寄存器组成 - 称为znode,这些类似于文件和目录。 与为存储设计的典型文件系统不同,ZooKeeper数据保存在内存中,这意味着ZooKeeper可以实现高吞吐量和低延迟。 - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/94251757.jpg) - -### 4.2 可构建集群 - -**为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么zookeeper本身仍然是可用的。** 客户端在使用 ZooKeeper 时,需要知道集群机器列表,通过与集群中的某一台机器建立 TCP 连接来使用服务,客户端使用这个TCP链接来发送请求、获取结果、获取监听事件以及发送心跳包。如果这个连接异常断开了,客户端可以连接到另外的机器上。 - -**ZooKeeper 官方提供的架构图:** - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/68900686.jpg) - -上图中每一个Server代表一个安装Zookeeper服务的服务器。组成 ZooKeeper 服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信。集群间通过 Zab 协议(Zookeeper Atomic Broadcast)来保持数据的一致性。 - -### 4.3 顺序访问 - -**对于来自客户端的每个更新请求,ZooKeeper 都会分配一个全局唯一的递增编号,这个编号反应了所有事务操作的先后顺序,应用程序可以使用 ZooKeeper 这个特性来实现更高层次的同步原语。** **这个编号也叫做时间戳——zxid(Zookeeper Transaction Id)** - -### 4.4 高性能 - -**ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。)** - -## 五 ZooKeeper 集群角色介绍 - -**最典型集群模式: Master/Slave 模式(主备模式)**。在这种模式中,通常 Master服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。 - -但是,**在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了Leader、Follower 和 Observer 三种角色**。如下图所示 - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/89602762.jpg) - - **ZooKeeper 集群中的所有机器通过一个 Leader 选举过程来选定一台称为 “Leader” 的机器,Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,Follower 和 Observer 都只能提供读服务。Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。** - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-13/91622395.jpg) - -**当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进人恢复模式并选举产生新的Leader服务器。这个过程大致是这样的:** - -1. Leader election(选举阶段):节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。 -2. Discovery(发现阶段):在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。 -3. Synchronization(同步阶段):同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后 -准 leader 才会成为真正的 leader。 -4. Broadcast(广播阶段) -到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。 - -## 六 ZooKeeper &ZAB 协议&Paxos算法 - -### 6.1 ZAB 协议&Paxos算法 - -Paxos 算法应该可以说是 ZooKeeper 的灵魂了。但是,ZooKeeper 并没有完全采用 Paxos算法 ,而是使用 ZAB 协议作为其保证数据一致性的核心算法。另外,在ZooKeeper的官方文档中也指出,ZAB协议并不像 Paxos 算法那样,是一种通用的分布式一致性算法,它是一种特别为Zookeeper设计的崩溃可恢复的原子消息广播算法。 - -### 6.2 ZAB 协议介绍 - -**ZAB(ZooKeeper Atomic Broadcast 原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。** - -### 6.3 ZAB 协议两种基本的模式:崩溃恢复和消息广播 - -ZAB协议包括两种基本的模式,分别是 **崩溃恢复和消息广播**。当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进人恢复模式并选举产生新的Leader服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式。其中,**所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和Leader服务器的数据状态保持一致**。 - -**当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进人消息广播模式了。** 当一台同样遵守ZAB协议的服务器启动后加人到集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加人的服务器就会自觉地进人数据恢复模式:找到Leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。正如上文介绍中所说的,ZooKeeper设计成只允许唯一的一个Leader服务器来进行事务请求的处理。Leader服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;而如果集群中的其他机器接收到客户端的事务请求,那么这些非Leader服务器会首先将这个事务请求转发给Leader服务器。 - -关于 **ZAB 协议&Paxos算法** 需要讲和理解的东西太多了,说实话,笔主到现在不太清楚这俩兄弟的具体原理和实现过程。推荐阅读下面两篇文章: - -- [图解 Paxos 一致性协议](http://codemacro.com/2014/10/15/explain-poxos/) -- [Zookeeper ZAB 协议分析](https://dbaplus.cn/news-141-1875-1.html) - -关于如何使用 zookeeper 实现分布式锁,可以查看下面这篇文章: - -- -[10分钟看懂!基于Zookeeper的分布式锁](https://blog.csdn.net/qiangcuo6087/article/details/79067136) - -## 六 总结 - -通过阅读本文,想必大家已从 **①ZooKeeper的由来。** -> **②ZooKeeper 到底是什么 。**-> **③ ZooKeeper 的一些重要概念**(会话(Session)、 Znode、版本、Watcher、ACL)-> **④ZooKeeper 的特点。** -> **⑤ZooKeeper 的设计目标。**-> **⑥ ZooKeeper 集群角色介绍** (Leader、Follower 和 Observer 三种角色)-> **⑦ZooKeeper &ZAB 协议&Paxos算法。** 这七点了解了 ZooKeeper 。 - -## 参考 - -- 《从Paxos到Zookeeper 》 -- https://cwiki.apache.org/confluence/display/ZOOKEEPER/ProjectDescription -- https://cwiki.apache.org/confluence/display/ZOOKEEPER/Index -- https://www.cnblogs.com/raphael5200/p/5285583.html -- https://zhuanlan.zhihu.com/p/30024403 - diff --git "a/docs/system-design/framework/ZooKeeper\346\225\260\346\215\256\346\250\241\345\236\213\345\222\214\345\270\270\350\247\201\345\221\275\344\273\244.md" "b/docs/system-design/framework/ZooKeeper\346\225\260\346\215\256\346\250\241\345\236\213\345\222\214\345\270\270\350\247\201\345\221\275\344\273\244.md" deleted file mode 100644 index eceaf22..0000000 --- "a/docs/system-design/framework/ZooKeeper\346\225\260\346\215\256\346\250\241\345\236\213\345\222\214\345\270\270\350\247\201\345\221\275\344\273\244.md" +++ /dev/null @@ -1,200 +0,0 @@ - - -- [ZooKeeper 数据模型](#zookeeper-数据模型) -- [ZNode\(数据节点\)的结构](#znode数据节点的结构) -- [测试 ZooKeeper 中的常见操作](#测试-zookeeper-中的常见操作) - - [连接 ZooKeeper 服务](#连接-zookeeper-服务) - - [查看常用命令\(help 命令\)](#查看常用命令help-命令) - - [创建节点\(create 命令\)](#创建节点create-命令) - - [更新节点数据内容\(set 命令\)](#更新节点数据内容set-命令) - - [获取节点的数据\(get 命令\)](#获取节点的数据get-命令) - - [查看某个目录下的子节点\(ls 命令\)](#查看某个目录下的子节点ls-命令) - - [查看节点状态\(stat 命令\)](#查看节点状态stat-命令) - - [查看节点信息和状态\(ls2 命令\)](#查看节点信息和状态ls2-命令) - - [删除节点\(delete 命令\)](#删除节点delete-命令) -- [参考](#参考) - - - -> 看本文之前如果你没有安装 ZooKeeper 的话,可以参考这篇文章:[《使用 SpringBoot+Dubbo 搭建一个简单分布式服务》](https://github.com/Snailclimb/springboot-integration-examples/blob/master/md/springboot-dubbo.md) 的 “开始实战 1 :zookeeper 环境安装搭建” 这部分进行安装(Centos7.4 环境下)。如果你想对 ZooKeeper 有一个整体了解的话,可以参考这篇文章:[《可能是把 ZooKeeper 概念讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/master/%E4%B8%BB%E6%B5%81%E6%A1%86%E6%9E%B6/ZooKeeper.md) - -### ZooKeeper 数据模型 - -ZNode(数据节点)是 ZooKeeper 中数据的最小单元,每个ZNode上都可以保存数据,同时还是可以有子节点(这就像树结构一样,如下图所示)。可以看出,节点路径标识方式和Unix文件 -系统路径非常相似,都是由一系列使用斜杠"/"进行分割的路径表示,开发人员可以向这个节点中写人数据,也可以在节点下面创建子节点。这些操作我们后面都会介绍到。 -![ZooKeeper 数据模型](https://images.gitbook.cn/95a192b0-1c56-11e9-9a8e-f3b01b1ea9aa) - -提到 ZooKeeper 数据模型,还有一个不得不得提的东西就是 **事务 ID** 。事务的ACID(Atomic:原子性;Consistency:一致性;Isolation:隔离性;Durability:持久性)四大特性我在这里就不多说了,相信大家也已经挺腻了。 - -在Zookeeper中,事务是指能够改变 ZooKeeper 服务器状态的操作,我们也称之为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新和客户端会话创建与失效等操作。**对于每一个事务请求,ZooKeeper 都会为其分配一个全局唯一的事务ID,用 ZXID 来表示**,通常是一个64位的数字。每一个ZXID对应一次更新操作,**从这些 ZXID 中可以间接地识别出Zookeeper处理这些更新操作请求的全局顺序**。 - -### ZNode(数据节点)的结构 - -每个 ZNode 由2部分组成: - -- stat:状态信息 -- data:数据内容 - -如下所示,我通过 get 命令来获取 根目录下的 dubbo 节点的内容。(get 命令在下面会介绍到) - -```shell -[zk: 127.0.0.1:2181(CONNECTED) 6] get /dubbo -# 该数据节点关联的数据内容为空 -null -# 下面是该数据节点的一些状态信息,其实就是 Stat 对象的格式化输出 -cZxid = 0x2 -ctime = Tue Nov 27 11:05:34 CST 2018 -mZxid = 0x2 -mtime = Tue Nov 27 11:05:34 CST 2018 -pZxid = 0x3 -cversion = 1 -dataVersion = 0 -aclVersion = 0 -ephemeralOwner = 0x0 -dataLength = 0 -numChildren = 1 - -``` -这些状态信息其实就是 Stat 对象的格式化输出。Stat 类中包含了一个数据节点的所有状态信息的字段,包括事务ID、版本信息和子节点个数等,如下图所示(图源:《从Paxos到Zookeeper 分布式一致性原理与实践》,下面会介绍通过 stat 命令查看数据节点的状态)。 - -**Stat 类:** - -![Stat 类](https://images.gitbook.cn/a841e740-1c55-11e9-b5b7-abf0ec0c666a) - -关于数据节点的状态信息说明(也就是对Stat 类中的各字段进行说明),可以参考下图(图源:《从Paxos到Zookeeper 分布式一致性原理与实践》)。 - -![数据节点的状态信息说明](https://images.gitbook.cn/f44d8630-1c55-11e9-b5b7-abf0ec0c666a) - -### 测试 ZooKeeper 中的常见操作 - - -#### 连接 ZooKeeper 服务 - -进入安装 ZooKeeper文件夹的 bin 目录下执行下面的命令连接 ZooKeeper 服务(Linux环境下)(连接之前首选要确定你的 ZooKeeper 服务已经启动成功)。 - -```shell -./zkCli.sh -server 127.0.0.1:2181 -``` -![连接 ZooKeeper 服务](https://images.gitbook.cn/153b84c0-1c59-11e9-9a8e-f3b01b1ea9aa) - -从上图可以看出控制台打印出了很多信息,包括我们的主机名称、JDK 版本、操作系统等等。如果你成功看到这些信息,说明你成功连接到 ZooKeeper 服务。 - -#### 查看常用命令(help 命令) - -help 命令查看 zookeeper 常用命令 - -![help 命令](https://images.gitbook.cn/091db640-1c59-11e9-b5b7-abf0ec0c666a) - -#### 创建节点(create 命令) - -通过 create 命令在根目录创建了node1节点,与它关联的字符串是"node1" - -```shell -[zk: 127.0.0.1:2181(CONNECTED) 34] create /node1 “node1” -``` -通过 create 命令在根目录创建了node1节点,与它关联的内容是数字 123 - -```shell -[zk: 127.0.0.1:2181(CONNECTED) 1] create /node1/node1.1 123 -Created /node1/node1.1 -``` - -#### 更新节点数据内容(set 命令) - -```shell -[zk: 127.0.0.1:2181(CONNECTED) 11] set /node1 "set node1" -``` - -#### 获取节点的数据(get 命令) - -get 命令可以获取指定节点的数据内容和节点的状态,可以看出我们通过set 命令已经将节点数据内容改为 "set node1"。 - -```shell -set node1 -cZxid = 0x47 -ctime = Sun Jan 20 10:22:59 CST 2019 -mZxid = 0x4b -mtime = Sun Jan 20 10:41:10 CST 2019 -pZxid = 0x4a -cversion = 1 -dataVersion = 1 -aclVersion = 0 -ephemeralOwner = 0x0 -dataLength = 9 -numChildren = 1 - -``` - -#### 查看某个目录下的子节点(ls 命令) - -通过 ls 命令查看根目录下的节点 - -```shell -[zk: 127.0.0.1:2181(CONNECTED) 37] ls / -[dubbo, zookeeper, node1] -``` -通过 ls 命令查看 node1 目录下的节点 - -```shell -[zk: 127.0.0.1:2181(CONNECTED) 5] ls /node1 -[node1.1] -``` -zookeeper 中的 ls 命令和 linux 命令中的 ls 类似, 这个命令将列出绝对路径path下的所有子节点信息(列出1级,并不递归) - -#### 查看节点状态(stat 命令) - -通过 stat 命令查看节点状态 - -```shell -[zk: 127.0.0.1:2181(CONNECTED) 10] stat /node1 -cZxid = 0x47 -ctime = Sun Jan 20 10:22:59 CST 2019 -mZxid = 0x47 -mtime = Sun Jan 20 10:22:59 CST 2019 -pZxid = 0x4a -cversion = 1 -dataVersion = 0 -aclVersion = 0 -ephemeralOwner = 0x0 -dataLength = 11 -numChildren = 1 -``` -上面显示的一些信息比如cversion、aclVersion、numChildren等等,我在上面 “ZNode(数据节点)的结构” 这部分已经介绍到。 - -#### 查看节点信息和状态(ls2 命令) - - -ls2 命令更像是 ls 命令和 stat 命令的结合。ls2 命令返回的信息包括2部分:子节点列表 + 当前节点的stat信息。 - -```shell -[zk: 127.0.0.1:2181(CONNECTED) 7] ls2 /node1 -[node1.1] -cZxid = 0x47 -ctime = Sun Jan 20 10:22:59 CST 2019 -mZxid = 0x47 -mtime = Sun Jan 20 10:22:59 CST 2019 -pZxid = 0x4a -cversion = 1 -dataVersion = 0 -aclVersion = 0 -ephemeralOwner = 0x0 -dataLength = 11 -numChildren = 1 - -``` - -#### 删除节点(delete 命令) - -这个命令很简单,但是需要注意的一点是如果你要删除某一个节点,那么这个节点必须无子节点才行。 - -```shell -[zk: 127.0.0.1:2181(CONNECTED) 3] delete /node1/node1.1 -``` - -在后面我会介绍到 Java 客户端 API的使用以及开源 Zookeeper 客户端 ZkClient 和 Curator 的使用。 - - -### 参考 - -- 《从Paxos到Zookeeper 分布式一致性原理与实践》 - diff --git a/docs/system-design/framework/spring/Spring-Design-Patterns.md b/docs/system-design/framework/spring/Spring-Design-Patterns.md deleted file mode 100644 index 968937f..0000000 --- a/docs/system-design/framework/spring/Spring-Design-Patterns.md +++ /dev/null @@ -1,363 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [控制反转(IoC)和依赖注入(DI)](#控制反转ioc和依赖注入di) -- [工厂设计模式](#工厂设计模式) -- [单例设计模式](#单例设计模式) -- [代理设计模式](#代理设计模式) - - [代理模式在 AOP 中的应用](#代理模式在-aop-中的应用) - - [Spring AOP 和 AspectJ AOP 有什么区别?](#spring-aop-和-aspectj-aop-有什么区别) -- [模板方法](#模板方法) -- [观察者模式](#观察者模式) - - [Spring 事件驱动模型中的三种角色](#spring-事件驱动模型中的三种角色) - - [事件角色](#事件角色) - - [事件监听者角色](#事件监听者角色) - - [事件发布者角色](#事件发布者角色) - - [Spring 的事件流程总结](#spring-的事件流程总结) -- [适配器模式](#适配器模式) - - [spring AOP中的适配器模式](#spring-aop中的适配器模式) - - [spring MVC中的适配器模式](#spring-mvc中的适配器模式) -- [装饰者模式](#装饰者模式) -- [总结](#总结) -- [参考](#参考) - - - -JDK 中用到了那些设计模式?Spring 中用到了那些设计模式?这两个问题,在面试中比较常见。我在网上搜索了一下关于 Spring 中设计模式的讲解几乎都是千篇一律,而且大部分都年代久远。所以,花了几天时间自己总结了一下,由于我的个人能力有限,文中如有任何错误各位都可以指出。另外,文章篇幅有限,对于设计模式以及一些源码的解读我只是一笔带过,这篇文章的主要目的是回顾一下 Spring 中的设计模式。 - -Design Patterns(设计模式) 表示面向对象软件开发中最好的计算机编程实践。 Spring 框架中广泛使用了不同类型的设计模式,下面我们来看看到底有哪些设计模式? - -## 控制反转(IoC)和依赖注入(DI) - -**IoC(Inversion of Control,控制翻转)** 是Spring 中一个非常非常重要的概念,它不是什么技术,而是一种解耦的设计思想。它的主要目的是借助于“第三方”(Spring 中的 IOC 容器) 实现具有依赖关系的对象之间的解耦(IOC容易管理对象,你只管使用即可),从而降低代码之间的耦合度。**IOC 是一个原则,而不是一个模式,以下模式(但不限于)实现了IoC原则。** - -![ioc-patterns](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ioc-patterns.png) - -**Spring IOC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。** IOC 容器负责创建对象,将对象连接在一起,配置这些对象,并从创建中处理这些对象的整个生命周期,直到它们被完全销毁。 - -在实际项目中一个 Service 类如果有几百甚至上千个类作为它的底层,我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IOC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。关于Spring IOC 的理解,推荐看这一下知乎的一个回答: ,非常不错。 - -**控制翻转怎么理解呢?** 举个例子:"对象a 依赖了对象 b,当对象 a 需要使用 对象 b的时候必须自己去创建。但是当系统引入了 IOC 容器后, 对象a 和对象 b 之前就失去了直接的联系。这个时候,当对象 a 需要使用 对象 b的时候, 我们可以指定 IOC 容器去创建一个对象b注入到对象 a 中"。 对象 a 获得依赖对象 b 的过程,由主动行为变为了被动行为,控制权翻转,这就是控制反转名字的由来。 - -**DI(Dependecy Inject,依赖注入)是实现控制反转的一种设计模式,依赖注入就是将实例变量传入到一个对象中去。** - -## 工厂设计模式 - -Spring使用工厂模式可以通过 `BeanFactory` 或 `ApplicationContext` 创建 bean 对象。 - -**两者对比:** - -- `BeanFactory` :延迟注入(使用到某个 bean 的时候才会注入),相比于`ApplicationContext` 来说会占用更少的内存,程序启动速度更快。 -- `ApplicationContext` :容器启动的时候,不管你用没用到,一次性创建所有 bean 。`BeanFactory` 仅提供了最基本的依赖注入支持,` ApplicationContext` 扩展了 `BeanFactory` ,除了有`BeanFactory`的功能还有额外更多功能,所以一般开发人员使用` ApplicationContext`会更多。 - -ApplicationContext的三个实现类: - -1. `ClassPathXmlApplication`:把上下文文件当成类路径资源。 -2. `FileSystemXmlApplication`:从文件系统中的 XML 文件载入上下文定义信息。 -3. `XmlWebApplicationContext`:从Web系统中的XML文件载入上下文定义信息。 - -Example: - -```java -import org.springframework.context.ApplicationContext; -import org.springframework.context.support.FileSystemXmlApplicationContext; - -public class App { - public static void main(String[] args) { - ApplicationContext context = new FileSystemXmlApplicationContext( - "C:/work/IOC Containers/springframework.applicationcontext/src/main/resources/bean-factory-config.xml"); - - HelloApplicationContext obj = (HelloApplicationContext) context.getBean("helloApplicationContext"); - obj.getMsg(); - } -} -``` - -## 单例设计模式 - -在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。 - -**使用单例模式的好处:** - -- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销; -- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。 - -**Spring 中 bean 的默认作用域就是 singleton(单例)的。** 除了 singleton 作用域,Spring 中 bean 还有下面几种作用域: - -- prototype : 每次请求都会创建一个新的 bean 实例。 -- request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 -- session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。 -- global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话 - -**Spring 实现单例的方式:** - -- xml : `` -- 注解:`@Scope(value = "singleton")` - -**Spring 通过 `ConcurrentHashMap` 实现单例注册表的特殊方式实现单例模式。Spring 实现单例的核心代码如下** - -```java -// 通过 ConcurrentHashMap(线程安全) 实现单例注册表 -private final Map singletonObjects = new ConcurrentHashMap(64); - -public Object getSingleton(String beanName, ObjectFactory singletonFactory) { - Assert.notNull(beanName, "'beanName' must not be null"); - synchronized (this.singletonObjects) { - // 检查缓存中是否存在实例 - Object singletonObject = this.singletonObjects.get(beanName); - if (singletonObject == null) { - //...省略了很多代码 - try { - singletonObject = singletonFactory.getObject(); - } - //...省略了很多代码 - // 如果实例对象在不存在,我们注册到单例注册表中。 - addSingleton(beanName, singletonObject); - } - return (singletonObject != NULL_OBJECT ? singletonObject : null); - } - } - //将对象添加到单例注册表 - protected void addSingleton(String beanName, Object singletonObject) { - synchronized (this.singletonObjects) { - this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT)); - - } - } -} -``` - -## 代理设计模式 - -### 代理模式在 AOP 中的应用 - -AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,**却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来**,便于**减少系统的重复代码**,**降低模块间的耦合度**,并**有利于未来的可拓展性和可维护性**。 - -**Spring AOP 就是基于动态代理的**,如果要代理的对象,实现了某个接口,那么Spring AOP会使用**JDK Proxy**,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用**Cglib** ,这时候Spring AOP会使用 **Cglib** 生成一个被代理对象的子类来作为代理,如下图所示: - -![SpringAOPProcess](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/SpringAOPProcess.jpg) - -当然你也可以使用 AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。 - -使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。 - -### Spring AOP 和 AspectJ AOP 有什么区别? - -**Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。 - - Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单, - -如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。 - -## 模板方法 - -模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。 - -![模板方法UML图](https://ws1.sinaimg.cn/large/006rNwoDgy1g3a73vdbojj30vo0iwdgc.jpg) - -```java -public abstract class Template { - //这是我们的模板方法 - public final void TemplateMethod(){ - PrimitiveOperation1(); - PrimitiveOperation2(); - PrimitiveOperation3(); - } - - protected void PrimitiveOperation1(){ - //当前类实现 - } - - //被子类实现的方法 - protected abstract void PrimitiveOperation2(); - protected abstract void PrimitiveOperation3(); - -} -public class TemplateImpl extends Template { - - @Override - public void PrimitiveOperation2() { - //当前类实现 - } - - @Override - public void PrimitiveOperation3() { - //当前类实现 - } -} - -``` - -Spring 中 `jdbcTemplate`、`hibernateTemplate` 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。 - -## 观察者模式 - -观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。 - -### Spring 事件驱动模型中的三种角色 - -#### 事件角色 - - `ApplicationEvent` (`org.springframework.context`包下)充当事件的角色,这是一个抽象类,它继承了`java.util.EventObject`并实现了 `java.io.Serializable`接口。 - -Spring 中默认存在以下事件,他们都是对 `ApplicationContextEvent` 的实现(继承自`ApplicationContextEvent`): - -- `ContextStartedEvent`:`ApplicationContext` 启动后触发的事件; -- `ContextStoppedEvent`:`ApplicationContext` 停止后触发的事件; -- `ContextRefreshedEvent`:`ApplicationContext` 初始化或刷新完成后触发的事件; -- `ContextClosedEvent`:`ApplicationContext` 关闭后触发的事件。 - -![ApplicationEvent-Subclass](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ApplicationEvent-Subclass.png) - -#### 事件监听者角色 - -`ApplicationListener` 充当了事件监听者角色,它是一个接口,里面只定义了一个 `onApplicationEvent()`方法来处理`ApplicationEvent`。`ApplicationListener`接口类源码如下,可以看出接口定义看出接口中的事件只要实现了 `ApplicationEvent`就可以了。所以,在 Spring中我们只要实现 `ApplicationListener` 接口实现 `onApplicationEvent()` 方法即可完成监听事件 - -```java -package org.springframework.context; -import java.util.EventListener; -@FunctionalInterface -public interface ApplicationListener extends EventListener { - void onApplicationEvent(E var1); -} -``` - -#### 事件发布者角色 - -`ApplicationEventPublisher` 充当了事件的发布者,它也是一个接口。 - -```java -@FunctionalInterface -public interface ApplicationEventPublisher { - default void publishEvent(ApplicationEvent event) { - this.publishEvent((Object)event); - } - - void publishEvent(Object var1); -} - -``` - -`ApplicationEventPublisher` 接口的`publishEvent()`这个方法在`AbstractApplicationContext`类中被实现,阅读这个方法的实现,你会发现实际上事件真正是通过`ApplicationEventMulticaster`来广播出去的。具体内容过多,就不在这里分析了,后面可能会单独写一篇文章提到。 - -### Spring 的事件流程总结 - -1. 定义一个事件: 实现一个继承自 `ApplicationEvent`,并且写相应的构造函数; -2. 定义一个事件监听者:实现 `ApplicationListener` 接口,重写 `onApplicationEvent()` 方法; -3. 使用事件发布者发布消息: 可以通过 `ApplicationEventPublisher ` 的 `publishEvent()` 方法发布消息。 - -Example: - -```java -// 定义一个事件,继承自ApplicationEvent并且写相应的构造函数 -public class DemoEvent extends ApplicationEvent{ - private static final long serialVersionUID = 1L; - - private String message; - - public DemoEvent(Object source,String message){ - super(source); - this.message = message; - } - - public String getMessage() { - return message; - } - - -// 定义一个事件监听者,实现ApplicationListener接口,重写 onApplicationEvent() 方法; -@Component -public class DemoListener implements ApplicationListener{ - - //使用onApplicationEvent接收消息 - @Override - public void onApplicationEvent(DemoEvent event) { - String msg = event.getMessage(); - System.out.println("接收到的信息是:"+msg); - } - -} -// 发布事件,可以通过ApplicationEventPublisher 的 publishEvent() 方法发布消息。 -@Component -public class DemoPublisher { - - @Autowired - ApplicationContext applicationContext; - - public void publish(String message){ - //发布事件 - applicationContext.publishEvent(new DemoEvent(this, message)); - } -} - -``` - -当调用 `DemoPublisher ` 的 `publish()` 方法的时候,比如 `demoPublisher.publish("你好")` ,控制台就会打印出:`接收到的信息是:你好` 。 - -## 适配器模式 - -适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。 - -### spring AOP中的适配器模式 - -我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是`AdvisorAdapter ` 。Advice 常用的类型有:`BeforeAdvice`(目标方法调用前,前置通知)、`AfterAdvice`(目标方法调用后,后置通知)、`AfterReturningAdvice`(目标方法执行结束后,return之前)等等。每个类型Advice(通知)都有对应的拦截器:`MethodBeforeAdviceInterceptor`、`AfterReturningAdviceAdapter`、`AfterReturningAdviceInterceptor`。Spring预定义的通知要通过对应的适配器,适配成 `MethodInterceptor`接口(方法拦截器)类型的对象(如:`MethodBeforeAdviceInterceptor` 负责适配 `MethodBeforeAdvice`)。 - -### spring MVC中的适配器模式 - -在Spring MVC中,`DispatcherServlet` 根据请求信息调用 `HandlerMapping`,解析请求对应的 `Handler`。解析到对应的 `Handler`(也就是我们平常说的 `Controller` 控制器)后,开始由`HandlerAdapter` 适配器处理。`HandlerAdapter` 作为期望接口,具体的适配器实现类用于对目标类进行适配,`Controller` 作为需要适配的类。 - -**为什么要在 Spring MVC 中使用适配器模式?** Spring MVC 中的 `Controller` 种类众多,不同类型的 `Controller` 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,`DispatcherServlet` 直接获取对应类型的 `Controller`,需要的自行来判断,像下面这段代码一样: - -```java -if(mappedHandler.getHandler() instanceof MultiActionController){ - ((MultiActionController)mappedHandler.getHandler()).xxx -}else if(mappedHandler.getHandler() instanceof XXX){ - ... -}else if(...){ - ... -} -``` - -假如我们再增加一个 `Controller`类型就要在上面代码中再加入一行 判断语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。 - -## 装饰者模式 - -装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个Decorator套在原有代码外面。其实在 JDK 中就有很多地方用到了装饰者模式,比如 `InputStream`家族,`InputStream` 类下有 `FileInputStream` (读取文件)、`BufferedInputStream` (增加缓存,使读取文件速度大大提升)等子类都在不修改`InputStream` 代码的情况下扩展了它的功能。 - -![装饰者模式示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Decorator.jpg) - -Spring 中配置 DataSource 的时候,DataSource 可能是不同的数据库和数据源。我们能否根据客户的需求在少修改原有类的代码下动态切换不同的数据源?这个时候就要用到装饰者模式(这一点我自己还没太理解具体原理)。Spring 中用到的包装器模式在类名上含有 `Wrapper`或者 `Decorator`。这些类基本上都是动态地给一个对象添加一些额外的职责 - -## 总结 - -Spring 框架中用到了哪些设计模式? - -- **工厂设计模式** : Spring使用工厂模式通过 `BeanFactory`、`ApplicationContext` 创建 bean 对象。 -- **代理设计模式** : Spring AOP 功能的实现。 -- **单例设计模式** : Spring 中的 Bean 默认都是单例的。 -- **模板方法模式** : Spring 中 `jdbcTemplate`、`hibernateTemplate` 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 -- **包装器设计模式** : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 -- **观察者模式:** Spring 事件驱动模型就是观察者模式很经典的一个应用。 -- **适配器模式** :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。 -- ...... - -## 参考 - -- 《Spring技术内幕》 -- -- -- -- -- -- - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/system-design/framework/spring/Spring.md b/docs/system-design/framework/spring/Spring.md deleted file mode 100644 index 52f195a..0000000 --- a/docs/system-design/framework/spring/Spring.md +++ /dev/null @@ -1,82 +0,0 @@ - - -## Spring相关教程/资料 - -### 官网相关 - -- [Spring官网](https://spring.io/)、[Spring系列主要项目](https://spring.io/projects)、[Spring官网指南](https://spring.io/guides)、[官方文档](https://spring.io/docs/reference) -- [spring-framework-reference](https://docs.spring.io/spring/docs/5.0.14.RELEASE/spring-framework-reference/index.html) -- [Spring Framework 4.3.17.RELEASE API](https://docs.spring.io/spring/docs/4.3.17.RELEASE/javadoc-api/) - -## 系统学习教程 - -### 文档 - -- [极客学院Spring Wiki](http://wiki.jikexueyuan.com/project/spring/transaction-management.html) -- [Spring W3Cschool教程 ](https://www.w3cschool.cn/wkspring/f6pk1ic8.html) - -### 视频 - -- [网易云课堂——58集精通java教程Spring框架开发](http://study.163.com/course/courseMain.htm?courseId=1004475015#/courseDetail?tab=1&35) -- [慕课网相关视频](https://www.imooc.com/) - -- **黑马视频和尚硅谷视频(非常推荐):** 微信公众号:“**JavaGuide**”后台回复关键字 “**1**” 免费领取。 - - -## 面试必备知识点 - -### SpringAOP,IOC实现原理 - -AOP实现原理、动态代理和静态代理、Spring IOC的初始化过程、IOC原理、自己实现怎么实现一个IOC容器?这些东西都是经常会被问到的。 - -推荐阅读: - -- [自己动手实现的 Spring IOC 和 AOP - 上篇](http://www.coolblog.xyz/2018/01/18/自己动手实现的-Spring-IOC-和-AOP-上篇/) - -- [自己动手实现的 Spring IOC 和 AOP - 下篇](http://www.coolblog.xyz/2018/01/18/自己动手实现的-Spring-IOC-和-AOP-下篇/) - -### AOP - -AOP思想的实现一般都是基于 **代理模式** ,在JAVA中一般采用JDK动态代理模式,但是我们都知道,**JDK动态代理模式只能代理接口而不能代理类**。因此,Spring AOP 会这样子来进行切换,因为Spring AOP 同时支持 CGLIB、ASPECTJ、JDK动态代理。 - -- 如果目标对象的实现类实现了接口,Spring AOP 将会采用 JDK 动态代理来生成 AOP 代理类; -- 如果目标对象的实现类没有实现接口,Spring AOP 将会采用 CGLIB 来生成 AOP 代理类——不过这个选择过程对开发者完全透明、开发者也无需关心。 - -推荐阅读: - -- [静态代理、JDK动态代理、CGLIB动态代理讲解](http://www.cnblogs.com/puyangsky/p/6218925.html) :我们知道AOP思想的实现一般都是基于 **代理模式** ,所以在看下面的文章之前建议先了解一下静态代理以及JDK动态代理、CGLIB动态代理的实现方式。 -- [Spring AOP 入门](https://juejin.im/post/5aa7818af265da23844040c6) :带你入门的一篇文章。这篇文章主要介绍了AOP中的基本概念:5种类型的通知(Before,After,After-returning,After-throwing,Around);Spring中对AOP的支持:AOP思想的实现一般都是基于代理模式,在Java中一般采用JDK动态代理模式,Spring AOP 同时支持 CGLIB、ASPECTJ、JDK动态代理, -- [Spring AOP 基于AspectJ注解如何实现AOP](https://juejin.im/post/5a55af9e518825734d14813f) : **AspectJ是一个AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器)**,可以这样说AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ与java程序完全兼容,几乎是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易。Spring注意到AspectJ在AOP的实现方式上依赖于特殊编译器(ajc编译器),因此Spring很机智回避了这点,转向采用动态代理技术的实现原理来构建Spring AOP的内部机制(动态织入),这是与AspectJ(静态织入)最根本的区别。**Spring 只是使用了与 AspectJ 5 一样的注解,但仍然没有使用 AspectJ 的编译器,底层依是动态代理技术的实现,因此并不依赖于 AspectJ 的编译器**。 Spring AOP虽然是使用了那一套注解,其实实现AOP的底层是使用了动态代理(JDK或者CGLib)来动态植入。至于AspectJ的静态植入,不是本文重点,所以只提一提。 -- [探秘Spring AOP(慕课网视频,很不错)](https://www.imooc.com/learn/869):慕课网视频,讲解的很不错,详细且深入 -- [spring源码剖析(六)AOP实现原理剖析](https://blog.csdn.net/fighterandknight/article/details/51209822) :通过源码分析Spring AOP的原理 - -### IOC - -Spring IOC的初始化过程: -![Spring IOC的初始化过程](https://user-gold-cdn.xitu.io/2018/5/22/16387903ee72c831?w=709&h=56&f=png&s=4673) - -- [[Spring框架]Spring IOC的原理及详解。](https://www.cnblogs.com/wang-meng/p/5597490.html) - -- [Spring IOC核心源码学习](https://yikun.github.io/2015/05/29/Spring-IOC核心源码学习/) :比较简短,推荐阅读。 -- [Spring IOC 容器源码分析](https://javadoop.com/post/spring-ioc) :强烈推荐,内容详尽,而且便于阅读。 - -## Spring事务管理 - -- [可能是最漂亮的Spring事务管理详解](https://juejin.im/post/5b00c52ef265da0b95276091) -- [Spring编程式和声明式事务实例讲解](https://juejin.im/post/5b010f27518825426539ba38) - -### Spring单例与线程安全 - -- [Spring框架中的单例模式(源码解读)](http://www.cnblogs.com/chengxuyuanzhilu/p/6404991.html):单例模式是一种常用的软件设计模式。通过单例模式可以保证系统中一个类只有一个实例。spring依赖注入时,使用了 多重判断加锁 的单例模式。 - -### Spring源码阅读 - -阅读源码不仅可以加深我们对Spring设计思想的理解,提高自己的编码水平,还可以让自己在面试中如鱼得水。下面的是Github上的一个开源的Spring源码阅读,大家有时间可以看一下,当然你如果有时间也可以自己慢慢研究源码。 - - - [spring-core](https://github.com/seaswalker/Spring/blob/master/note/Spring.md) -- [spring-aop](https://github.com/seaswalker/Spring/blob/master/note/spring-aop.md) -- [spring-context](https://github.com/seaswalker/Spring/blob/master/note/spring-context.md) -- [spring-task](https://github.com/seaswalker/Spring/blob/master/note/spring-task.md) -- [spring-transaction](https://github.com/seaswalker/Spring/blob/master/note/spring-transaction.md) -- [spring-mvc](https://github.com/seaswalker/Spring/blob/master/note/spring-mvc.md) -- [guava-cache](https://github.com/seaswalker/Spring/blob/master/note/guava-cache.md) diff --git a/docs/system-design/framework/spring/SpringBean.md b/docs/system-design/framework/spring/SpringBean.md deleted file mode 100644 index 4e8279e..0000000 --- a/docs/system-design/framework/spring/SpringBean.md +++ /dev/null @@ -1,451 +0,0 @@ - - -- [前言](#前言) -- [一 bean的作用域](#一-bean的作用域) - - [1. singleton——唯一 bean 实例](#1-singleton——唯一-bean-实例) - - [2. prototype——每次请求都会创建一个新的 bean 实例](#2-prototype——每次请求都会创建一个新的-bean-实例) - - [3. request——每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效](#3-request——每一次http请求都会产生一个新的bean,该bean仅在当前http-request内有效) - - [4. session——每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效](#4-session——每一次http请求都会产生一个新的-bean,该bean仅在当前-http-session-内有效) - - [5. globalSession](#5-globalsession) -- [二 bean的生命周期](#二-bean的生命周期) - - [initialization 和 destroy](#initialization-和-destroy) - - [实现*Aware接口 在Bean中使用Spring框架的一些对象](#实现aware接口-在bean中使用spring框架的一些对象) - - [BeanPostProcessor](#beanpostprocessor) - - [总结](#总结) - - [单例管理的对象](#单例管理的对象) - - [非单例管理的对象](#非单例管理的对象) -- [三 说明](#三-说明) - - - -# 前言 -在 Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 bean。简单地讲,bean 就是由 IOC 容器初始化、装配及管理的对象,除此之外,bean 就与应用程序中的其他对象没有什么区别了。而 bean 的定义以及 bean 相互间的依赖关系将通过配置元数据来描述。 - -**Spring中的bean默认都是单例的,这些单例Bean在多线程程序下如何保证线程安全呢?** 例如对于Web应用来说,Web容器对于每个用户请求都创建一个单独的Sevlet线程来处理请求,引入Spring框架之后,每个Action都是单例的,那么对于Spring托管的单例Service Bean,如何保证其安全呢? **Spring的单例是基于BeanFactory也就是Spring容器的,单例Bean在此容器内只有一个,Java的单例是基于 JVM,每个 JVM 内只有一个实例。** - -在大多数情况下。单例 bean 是很理想的方案。不过,有时候你可能会发现你所使用的类是易变的,它们会保持一些状态,因此重用是不安全的。在这种情况下,将 class 声明为单例的就不是那么明智了。因为对象会被污染,稍后重用的时候会出现意想不到的问题。所以 Spring 定义了多种作用域的bean。 - -# 一 bean的作用域 - -创建一个bean定义,其实质是用该bean定义对应的类来创建真正实例的“配方”。把bean定义看成一个配方很有意义,它与class很类似,只根据一张“处方”就可以创建多个实例。不仅可以控制注入到对象中的各种依赖和配置值,还可以控制该对象的作用域。这样可以灵活选择所建对象的作用域,而不必在Java Class级定义作用域。Spring Framework支持五种作用域,分别阐述如下表。 - - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-17/1188352.jpg) - -五种作用域中,**request、session** 和 **global session** 三种作用域仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架),只能用在基于 web 的 Spring ApplicationContext 环境。 - - - -### 1. singleton——唯一 bean 实例 - -**当一个 bean 的作用域为 singleton,那么Spring IoC容器中只会存在一个共享的 bean 实例,并且所有对 bean 的请求,只要 id 与该 bean 定义相匹配,则只会返回bean的同一实例。** singleton 是单例类型(对应于单例模式),就是在创建起容器时就同时自动创建了一个bean的对象,不管你是否使用,但我们可以指定Bean节点的 `lazy-init=”true”` 来延迟初始化bean,这时候,只有在第一次获取bean时才会初始化bean,即第一次请求该bean时才初始化。 每次获取到的对象都是同一个对象。注意,singleton 作用域是Spring中的缺省作用域。要在XML中将 bean 定义成 singleton ,可以这样配置: - -```xml - -``` - -也可以通过 `@Scope` 注解(它可以显示指定bean的作用范围。)的方式 - -```java -@Service -@Scope("singleton") -public class ServiceImpl{ - -} -``` - -### 2. prototype——每次请求都会创建一个新的 bean 实例 - -**当一个bean的作用域为 prototype,表示一个 bean 定义对应多个对象实例。** **prototype 作用域的 bean 会导致在每次对该 bean 请求**(将其注入到另一个 bean 中,或者以程序的方式调用容器的 getBean() 方法**)时都会创建一个新的 bean 实例。prototype 是原型类型,它在我们创建容器的时候并没有实例化,而是当我们获取bean的时候才会去创建一个对象,而且我们每次获取到的对象都不是同一个对象。根据经验,对有状态的 bean 应该使用 prototype 作用域,而对无状态的 bean 则应该使用 singleton 作用域。** 在 XML 中将 bean 定义成 prototype ,可以这样配置: - -```java - - 或者 - -``` -通过 `@Scope` 注解的方式实现就不做演示了。 - -### 3. request——每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效 - -**request只适用于Web程序,每一次 HTTP 请求都会产生一个新的bean,同时该bean仅在当前HTTP request内有效,当请求结束后,该对象的生命周期即告结束。** 在 XML 中将 bean 定义成 request ,可以这样配置: - -```java - -``` - -### 4. session——每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效 - -**session只适用于Web程序,session 作用域表示该针对每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效.与request作用域一样,可以根据需要放心的更改所创建实例的内部状态,而别的 HTTP session 中根据 userPreferences 创建的实例,将不会看到这些特定于某个 HTTP session 的状态变化。当HTTP session最终被废弃的时候,在该HTTP session作用域内的bean也会被废弃掉。** - -```xml - -``` - -### 5. globalSession - -global session 作用域类似于标准的 HTTP session 作用域,不过仅仅在基于 portlet 的 web 应用中才有意义。Portlet 规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种不同的 portle t所共享。在global session 作用域中定义的 bean 被限定于全局portlet Session的生命周期范围内。 - -```xml - -``` - -# 二 bean的生命周期 - -Spring Bean是Spring应用中最最重要的部分了。所以来看看Spring容器在初始化一个bean的时候会做那些事情,顺序是怎样的,在容器关闭的时候,又会做哪些事情。 - -> spring版本:4.2.3.RELEASE -鉴于Spring源码是用gradle构建的,我也决定舍弃我大maven,尝试下洪菊推荐过的gradle。运行beanLifeCycle模块下的junit test即可在控制台看到如下输出,可以清楚了解Spring容器在创建,初始化和销毁Bean的时候依次做了那些事情。 - -``` -Spring容器初始化 -===================================== -调用GiraffeService无参构造函数 -GiraffeService中利用set方法设置属性值 -调用setBeanName:: Bean Name defined in context=giraffeService -调用setBeanClassLoader,ClassLoader Name = sun.misc.Launcher$AppClassLoader -调用setBeanFactory,setBeanFactory:: giraffe bean singleton=true -调用setEnvironment -调用setResourceLoader:: Resource File Name=spring-beans.xml -调用setApplicationEventPublisher -调用setApplicationContext:: Bean Definition Names=[giraffeService, org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#0, com.giraffe.spring.service.GiraffeServicePostProcessor#0] -执行BeanPostProcessor的postProcessBeforeInitialization方法,beanName=giraffeService -调用PostConstruct注解标注的方法 -执行InitializingBean接口的afterPropertiesSet方法 -执行配置的init-method -执行BeanPostProcessor的postProcessAfterInitialization方法,beanName=giraffeService -Spring容器初始化完毕 -===================================== -从容器中获取Bean -giraffe Name=李光洙 -===================================== -调用preDestroy注解标注的方法 -执行DisposableBean接口的destroy方法 -执行配置的destroy-method -Spring容器关闭 -``` - -先来看看,Spring在Bean从创建到销毁的生命周期中可能做得事情。 - - -### initialization 和 destroy - -有时我们需要在Bean属性值set好之后和Bean销毁之前做一些事情,比如检查Bean中某个属性是否被正常的设置好值了。Spring框架提供了多种方法让我们可以在Spring Bean的生命周期中执行initialization和pre-destroy方法。 - -**1.实现InitializingBean和DisposableBean接口** - -这两个接口都只包含一个方法。通过实现InitializingBean接口的afterPropertiesSet()方法可以在Bean属性值设置好之后做一些操作,实现DisposableBean接口的destroy()方法可以在销毁Bean之前做一些操作。 - -例子如下: - -```java -public class GiraffeService implements InitializingBean,DisposableBean { - @Override - public void afterPropertiesSet() throws Exception { - System.out.println("执行InitializingBean接口的afterPropertiesSet方法"); - } - @Override - public void destroy() throws Exception { - System.out.println("执行DisposableBean接口的destroy方法"); - } -} -``` -这种方法比较简单,但是不建议使用。因为这样会将Bean的实现和Spring框架耦合在一起。 - -**2.在bean的配置文件中指定init-method和destroy-method方法** - -Spring允许我们创建自己的 init 方法和 destroy 方法,只要在 Bean 的配置文件中指定 init-method 和 destroy-method 的值就可以在 Bean 初始化时和销毁之前执行一些操作。 - -例子如下: - -```java -public class GiraffeService { - //通过的destroy-method属性指定的销毁方法 - public void destroyMethod() throws Exception { - System.out.println("执行配置的destroy-method"); - } - //通过的init-method属性指定的初始化方法 - public void initMethod() throws Exception { - System.out.println("执行配置的init-method"); - } -} -``` - -配置文件中的配置: - -``` - - -``` - -需要注意的是自定义的init-method和post-method方法可以抛异常但是不能有参数。 - -这种方式比较推荐,因为可以自己创建方法,无需将Bean的实现直接依赖于spring的框架。 - -**3.使用@PostConstruct和@PreDestroy注解** - -除了xml配置的方式,Spring 也支持用 `@PostConstruct`和 `@PreDestroy`注解来指定 `init` 和 `destroy` 方法。这两个注解均在`javax.annotation` 包中。为了注解可以生效,需要在配置文件中定义org.springframework.context.annotation.CommonAnnotationBeanPostProcessor或context:annotation-config - -例子如下: - -```java -public class GiraffeService { - @PostConstruct - public void initPostConstruct(){ - System.out.println("执行PostConstruct注解标注的方法"); - } - @PreDestroy - public void preDestroy(){ - System.out.println("执行preDestroy注解标注的方法"); - } -} -``` - -配置文件: - -```xml - - - -``` - -### 实现*Aware接口 在Bean中使用Spring框架的一些对象 - -有些时候我们需要在 Bean 的初始化中使用 Spring 框架自身的一些对象来执行一些操作,比如获取 ServletContext 的一些参数,获取 ApplicaitionContext 中的 BeanDefinition 的名字,获取 Bean 在容器中的名字等等。为了让 Bean 可以获取到框架自身的一些对象,Spring 提供了一组名为*Aware的接口。 - -这些接口均继承于`org.springframework.beans.factory.Aware`标记接口,并提供一个将由 Bean 实现的set*方法,Spring通过基于setter的依赖注入方式使相应的对象可以被Bean使用。 -网上说,这些接口是利用观察者模式实现的,类似于servlet listeners,目前还不明白,不过这也不在本文的讨论范围内。 -介绍一些重要的Aware接口: - -- **ApplicationContextAware**: 获得ApplicationContext对象,可以用来获取所有Bean definition的名字。 -- **BeanFactoryAware**:获得BeanFactory对象,可以用来检测Bean的作用域。 -- **BeanNameAware**:获得Bean在配置文件中定义的名字。 -- **ResourceLoaderAware**:获得ResourceLoader对象,可以获得classpath中某个文件。 -- **ServletContextAware**:在一个MVC应用中可以获取ServletContext对象,可以读取context中的参数。 -- **ServletConfigAware**: 在一个MVC应用中可以获取ServletConfig对象,可以读取config中的参数。 - -```java -public class GiraffeService implements ApplicationContextAware, - ApplicationEventPublisherAware, BeanClassLoaderAware, BeanFactoryAware, - BeanNameAware, EnvironmentAware, ImportAware, ResourceLoaderAware{ - @Override - public void setBeanClassLoader(ClassLoader classLoader) { - System.out.println("执行setBeanClassLoader,ClassLoader Name = " + classLoader.getClass().getName()); - } - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - System.out.println("执行setBeanFactory,setBeanFactory:: giraffe bean singleton=" + beanFactory.isSingleton("giraffeService")); - } - @Override - public void setBeanName(String s) { - System.out.println("执行setBeanName:: Bean Name defined in context=" - + s); - } - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - System.out.println("执行setApplicationContext:: Bean Definition Names=" - + Arrays.toString(applicationContext.getBeanDefinitionNames())); - } - @Override - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { - System.out.println("执行setApplicationEventPublisher"); - } - @Override - public void setEnvironment(Environment environment) { - System.out.println("执行setEnvironment"); - } - @Override - public void setResourceLoader(ResourceLoader resourceLoader) { - Resource resource = resourceLoader.getResource("classpath:spring-beans.xml"); - System.out.println("执行setResourceLoader:: Resource File Name=" - + resource.getFilename()); - } - @Override - public void setImportMetadata(AnnotationMetadata annotationMetadata) { - System.out.println("执行setImportMetadata"); - } -} -``` - -### BeanPostProcessor - -上面的*Aware接口是针对某个实现这些接口的Bean定制初始化的过程, -Spring同样可以针对容器中的所有Bean,或者某些Bean定制初始化过程,只需提供一个实现BeanPostProcessor接口的类即可。 该接口中包含两个方法,postProcessBeforeInitialization和postProcessAfterInitialization。 postProcessBeforeInitialization方法会在容器中的Bean初始化之前执行, postProcessAfterInitialization方法在容器中的Bean初始化之后执行。 - -例子如下: - -```java -public class CustomerBeanPostProcessor implements BeanPostProcessor { - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - System.out.println("执行BeanPostProcessor的postProcessBeforeInitialization方法,beanName=" + beanName); - return bean; - } - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - System.out.println("执行BeanPostProcessor的postProcessAfterInitialization方法,beanName=" + beanName); - return bean; - } -} -``` - -要将BeanPostProcessor的Bean像其他Bean一样定义在配置文件中 - -```xml - -``` - -### 总结 - -所以。。。结合第一节控制台输出的内容,Spring Bean的生命周期是这样纸的: - -- Bean容器找到配置文件中 Spring Bean 的定义。 -- Bean容器利用Java Reflection API创建一个Bean的实例。 -- 如果涉及到一些属性值 利用set方法设置一些属性值。 -- 如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字。 -- 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 -- 如果Bean实现了BeanFactoryAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 -- 与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 -- 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessBeforeInitialization()方法 -- 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。 -- 如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 -- 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessAfterInitialization()方法 -- 当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy()方法。 -- 当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 - -用图表示一下(图来源:http://www.jianshu.com/p/d00539babca5): - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-17/48376272.jpg) - -与之比较类似的中文版本: - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-17/5496407.jpg) - - -**其实很多时候我们并不会真的去实现上面说描述的那些接口,那么下面我们就除去那些接口,针对bean的单例和非单例来描述下bean的生命周期:** - -### 单例管理的对象 - -当scope=”singleton”,即默认情况下,会在启动容器时(即实例化容器时)时实例化。但我们可以指定Bean节点的lazy-init=”true”来延迟初始化bean,这时候,只有在第一次获取bean时才会初始化bean,即第一次请求该bean时才初始化。如下配置: - -```xml - -``` - -如果想对所有的默认单例bean都应用延迟初始化,可以在根节点beans设置default-lazy-init属性为true,如下所示: - -```xml - -``` - -默认情况下,Spring 在读取 xml 文件的时候,就会创建对象。在创建对象的时候先调用构造器,然后调用 init-method 属性值中所指定的方法。对象在被销毁的时候,会调用 destroy-method 属性值中所指定的方法(例如调用Container.destroy()方法的时候)。写一个测试类,代码如下: - -```java -public class LifeBean { - private String name; - - public LifeBean(){ - System.out.println("LifeBean()构造函数"); - } - public String getName() { - return name; - } - - public void setName(String name) { - System.out.println("setName()"); - this.name = name; - } - - public void init(){ - System.out.println("this is init of lifeBean"); - } - - public void destory(){ - System.out.println("this is destory of lifeBean " + this); - } -} -``` - life.xml配置如下: - -```xml - -``` - -测试代码: - -```java -public class LifeTest { - @Test - public void test() { - AbstractApplicationContext container = - new ClassPathXmlApplicationContext("life.xml"); - LifeBean life1 = (LifeBean)container.getBean("life"); - System.out.println(life1); - container.close(); - } -} -``` - -运行结果: - -``` -LifeBean()构造函数 -this is init of lifeBean -com.bean.LifeBean@573f2bb1 -…… -this is destory of lifeBean com.bean.LifeBean@573f2bb1 -``` - -### 非单例管理的对象 - -当`scope=”prototype”`时,容器也会延迟初始化 bean,Spring 读取xml 文件的时候,并不会立刻创建对象,而是在第一次请求该 bean 时才初始化(如调用getBean方法时)。在第一次请求每一个 prototype 的bean 时,Spring容器都会调用其构造器创建这个对象,然后调用`init-method`属性值中所指定的方法。对象销毁的时候,Spring 容器不会帮我们调用任何方法,因为是非单例,这个类型的对象有很多个,Spring容器一旦把这个对象交给你之后,就不再管理这个对象了。 - -为了测试prototype bean的生命周期life.xml配置如下: - -```xml - -``` - -测试程序: - -```java -public class LifeTest { - @Test - public void test() { - AbstractApplicationContext container = new ClassPathXmlApplicationContext("life.xml"); - LifeBean life1 = (LifeBean)container.getBean("life_singleton"); - System.out.println(life1); - - LifeBean life3 = (LifeBean)container.getBean("life_prototype"); - System.out.println(life3); - container.close(); - } -} -``` - -运行结果: - -``` -LifeBean()构造函数 -this is init of lifeBean -com.bean.LifeBean@573f2bb1 -LifeBean()构造函数 -this is init of lifeBean -com.bean.LifeBean@5ae9a829 -…… -this is destory of lifeBean com.bean.LifeBean@573f2bb1 -``` - -可以发现,对于作用域为 prototype 的 bean ,其`destroy`方法并没有被调用。**如果 bean 的 scope 设为prototype时,当容器关闭时,`destroy` 方法不会被调用。对于 prototype 作用域的 bean,有一点非常重要,那就是 Spring不能对一个 prototype bean 的整个生命周期负责:容器在初始化、配置、装饰或者是装配完一个prototype实例后,将它交给客户端,随后就对该prototype实例不闻不问了。** 不管何种作用域,容器都会调用所有对象的初始化生命周期回调方法。但对prototype而言,任何配置好的析构生命周期回调方法都将不会被调用。**清除prototype作用域的对象并释放任何prototype bean所持有的昂贵资源,都是客户端代码的职责**(让Spring容器释放被prototype作用域bean占用资源的一种可行方式是,通过使用bean的后置处理器,该处理器持有要被清除的bean的引用)。谈及prototype作用域的bean时,在某些方面你可以将Spring容器的角色看作是Java new操作的替代者,任何迟于该时间点的生命周期事宜都得交由客户端来处理。 - -**Spring 容器可以管理 singleton 作用域下 bean 的生命周期,在此作用域下,Spring 能够精确地知道bean何时被创建,何时初始化完成,以及何时被销毁。而对于 prototype 作用域的bean,Spring只负责创建,当容器创建了 bean 的实例后,bean 的实例就交给了客户端的代码管理,Spring容器将不再跟踪其生命周期,并且不会管理那些被配置成prototype作用域的bean的生命周期。** - - -# 三 说明 - -本文的完成结合了下面两篇文章,并做了相应修改: - -- https://blog.csdn.net/fuzhongmin05/article/details/73389779 -- https://yemengying.com/2016/07/14/spring-bean-life-cycle/ - -由于本文非本人独立原创,所以未声明为原创!在此说明! diff --git a/docs/system-design/framework/spring/SpringInterviewQuestions.md b/docs/system-design/framework/spring/SpringInterviewQuestions.md deleted file mode 100644 index ff49cbb..0000000 --- a/docs/system-design/framework/spring/SpringInterviewQuestions.md +++ /dev/null @@ -1,358 +0,0 @@ -这篇文章主要是想通过一些问题,加深大家对于 Spring 的理解,所以不会涉及太多的代码!这篇文章整理了挺长时间,下面的很多问题我自己在使用 Spring 的过程中也并没有注意,自己也是临时查阅了很多资料和书籍补上的。网上也有一些很多关于 Spring 常见问题/面试题整理的文章,我感觉大部分都是互相 copy,而且很多问题也不是很好,有些回答也存在问题。所以,自己花了一周的业余时间整理了一下,希望对大家有帮助。 - -## 1. 什么是 Spring 框架? - -Spring 是一种轻量级开发框架,旨在提高开发人员的开发效率以及系统的可维护性。Spring 官网:。 - -我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是:核心容器、数据访问/集成,、Web、AOP(面向切面编程)、工具、消息和测试模块。比如:Core Container 中的 Core 组件是Spring 所有组件的核心,Beans 组件和 Context 组件是实现IOC和依赖注入的基础,AOP组件用来实现面向切面编程。 - -Spring 官网列出的 Spring 的 6 个特征: - -- **核心技术** :依赖注入(DI),AOP,事件(events),资源,i18n,验证,数据绑定,类型转换,SpEL。 -- **测试** :模拟对象,TestContext框架,Spring MVC 测试,WebTestClient。 -- **数据访问** :事务,DAO支持,JDBC,ORM,编组XML。 -- **Web支持** : Spring MVC和Spring WebFlux Web框架。 -- **集成** :远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 -- **语言** :Kotlin,Groovy,动态语言。 - -## 2. 列举一些重要的Spring模块? - -下图对应的是 Spring4.x 版本。目前最新的5.x版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。 - -![Spring主要模块](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Spring主要模块.png) - -- **Spring Core:** 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 IoC 依赖注入功能。 -- **Spring Aspects** : 该模块为与AspectJ的集成提供支持。 -- **Spring AOP** :提供了面向切面的编程实现。 -- **Spring JDBC** : Java数据库连接。 -- **Spring JMS** :Java消息服务。 -- **Spring ORM** : 用于支持Hibernate等ORM工具。 -- **Spring Web** : 为创建Web应用程序提供支持。 -- **Spring Test** : 提供了对 JUnit 和 TestNG 测试的支持。 - -## 3. @RestController vs @Controller - -**`Controller` 返回一个页面** - -单独使用 `@Controller` 不加 `@ResponseBody`的话一般使用在要返回一个视图的情况,这种情况属于比较传统的Spring MVC 的应用,对应于前后端不分离的情况。 - -![SpringMVC 传统工作流程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/SpringMVC传统工作流程.png) - -**`@RestController` 返回JSON 或 XML 形式数据** - -但`@RestController`只返回对象,对象数据直接以 JSON 或 XML 形式写入 HTTP 响应(Response)中,这种情况属于 RESTful Web服务,这也是目前日常开发所接触的最常用的情况(前后端分离)。 - -![SpringMVC+RestController](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/SpringMVCRestController.png) - -**`@Controller +@ResponseBody` 返回JSON 或 XML 形式数据** - -如果你需要在Spring4之前开发 RESTful Web服务的话,你需要使用`@Controller` 并结合`@ResponseBody`注解,也就是说`@Controller` +`@ResponseBody`= `@RestController`(Spring 4 之后新加的注解)。 - -> `@ResponseBody` 注解的作用是将 `Controller` 的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到HTTP 响应(Response)对象的 body 中,通常用来返回 JSON 或者 XML 数据,返回 JSON 数据的情况比较多。 - -![Spring3.xMVC RESTfulWeb服务工作流程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/Spring3.xMVCRESTfulWeb服务工作流程.png) - -Reference: - -- https://dzone.com/articles/spring-framework-restcontroller-vs-controller(图片来源) -- https://javarevisited.blogspot.com/2017/08/difference-between-restcontroller-and-controller-annotations-spring-mvc-rest.html?m=1 - -## 4. Spring IOC & AOP - -### 4.1 谈谈自己对于 Spring IoC 和 AOP 的理解 - -#### IoC - -IoC(Inverse of Control:控制反转)是一种**设计思想**,就是 **将原本在程序中手动创建对象的控制权,交由Spring框架来管理。** IoC 在其他语言中也有应用,并非 Spirng 特有。 **IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。** - -将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 **IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。** 在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。 - -Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。 - -推荐阅读:https://www.zhihu.com/question/23277575/answer/169698662 - -**Spring IoC的初始化过程:** - -![Spring IoC的初始化过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/SpringIOC初始化过程.png) - -IoC源码阅读 - -- https://javadoop.com/post/spring-ioc - -#### AOP - -AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,**却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来**,便于**减少系统的重复代码**,**降低模块间的耦合度**,并**有利于未来的可拓展性和可维护性**。 - -**Spring AOP就是基于动态代理的**,如果要代理的对象,实现了某个接口,那么Spring AOP会使用**JDK Proxy**,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用**Cglib** ,这时候Spring AOP会使用 **Cglib** 生成一个被代理对象的子类来作为代理,如下图所示: - -![SpringAOPProcess](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/SpringAOPProcess.jpg) - -当然你也可以使用 AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。 - -使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。 - -### 4.2 Spring AOP 和 AspectJ AOP 有什么区别? - -**Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。 - - Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单, - -如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。 - -## 5. Spring bean - -### 5.1 Spring 中的 bean 的作用域有哪些? - -- singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。 -- prototype : 每次请求都会创建一个新的 bean 实例。 -- request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 -- session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。 -- global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话 - -### 5.2 Spring 中的单例 bean 的线程安全问题了解吗? - -大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 - -常见的有两种解决办法: - -1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。 - -2. 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。 - - -### 5.3 @Component 和 @Bean 的区别是什么? - -1. 作用对象不同: `@Component` 注解作用于类,而`@Bean`注解作用于方法。 -2. `@Component`通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用 `@ComponentScan` 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。`@Bean` 注解通常是我们在标有该注解的方法中定义产生这个 bean,`@Bean`告诉了Spring这是某个类的示例,当我需要用它的时候还给我。 -3. `@Bean` 注解比 `Component` 注解的自定义性更强,而且很多地方我们只能通过 `@Bean` 注解来注册bean。比如当我们引用第三方库中的类需要装配到 `Spring`容器时,则只能通过 `@Bean`来实现。 - -`@Bean`注解使用示例: - -```java -@Configuration -public class AppConfig { - @Bean - public TransferService transferService() { - return new TransferServiceImpl(); - } - -} -``` - - 上面的代码相当于下面的 xml 配置 - -```xml - - - -``` - -下面这个例子是通过 `@Component` 无法实现的。 - -```java -@Bean -public OneService getService(status) { - case (status) { - when 1: - return new serviceImpl1(); - when 2: - return new serviceImpl2(); - when 3: - return new serviceImpl3(); - } -} -``` - -### 5.4 将一个类声明为Spring的 bean 的注解有哪些? - -我们一般使用 `@Autowired` 注解自动装配 bean,要想把类标识成可用于 `@Autowired` 注解自动装配的 bean 的类,采用以下注解可实现: - -- `@Component` :通用的注解,可标注任意类为 `Spring` 组件。如果一个Bean不知道属于哪个层,可以使用`@Component` 注解标注。 -- `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 -- `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。 -- `@Controller` : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。 - -### 5.5 Spring 中的 bean 生命周期? - -这部分网上有很多文章都讲到了,下面的内容整理自: ,除了这篇文章,再推荐一篇很不错的文章 : 。 - -- Bean 容器找到配置文件中 Spring Bean 的定义。 -- Bean 容器利用 Java Reflection API 创建一个Bean的实例。 -- 如果涉及到一些属性值 利用 `set()`方法设置一些属性值。 -- 如果 Bean 实现了 `BeanNameAware` 接口,调用 `setBeanName()`方法,传入Bean的名字。 -- 如果 Bean 实现了 `BeanClassLoaderAware` 接口,调用 `setBeanClassLoader()`方法,传入 `ClassLoader`对象的实例。 -- 与上面的类似,如果实现了其他 `*.Aware`接口,就调用相应的方法。 -- 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessBeforeInitialization()` 方法 -- 如果Bean实现了`InitializingBean`接口,执行`afterPropertiesSet()`方法。 -- 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。 -- 如果有和加载这个 Bean的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessAfterInitialization()` 方法 -- 当要销毁 Bean 的时候,如果 Bean 实现了 `DisposableBean` 接口,执行 `destroy()` 方法。 -- 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。 - -图示: - -![Spring Bean 生命周期](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-17/48376272.jpg) - -与之比较类似的中文版本: - -![Spring Bean 生命周期](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-17/5496407.jpg) - -## 6. Spring MVC - -### 6.1 说说自己对于 Spring MVC 了解? - -谈到这个问题,我们不得不提提之前 Model1 和 Model2 这两个没有 Spring MVC 的时代。 - -- **Model1 时代** : 很多学 Java 后端比较晚的朋友可能并没有接触过 Model1 模式下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。这个模式下 JSP 即是控制层又是表现层。显而易见,这种模式存在很多问题。比如①将控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;②前端和后端相互依赖,难以进行测试并且开发效率极低; -- **Model2 时代** :学过 Servlet 并做过相关 Demo 的朋友应该了解“Java Bean(Model)+ JSP(View,)+Servlet(Controller) ”这种开发模式,这就是早期的 JavaWeb MVC 开发模式。Model:系统涉及的数据,也就是 dao 和 bean。View:展示模型中的数据,只是用来展示。Controller:处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。 - -Model2 模式下还存在很多问题,Model2的抽象和封装程度还远远不够,使用Model2进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。于是很多JavaWeb开发相关的 MVC 框架应运而生比如Struts2,但是 Struts2 比较笨重。随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。 - -MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的Web层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service层(处理业务)、Dao层(数据库操作)、Entity层(实体类)、Controller层(控制层,返回数据给前台页面)。 - -**Spring MVC 的简单原理图如下:** - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/60679444.jpg) - -### 6.2 SpringMVC 工作原理了解吗? - -**原理如下图所示:** -![SpringMVC运行原理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/49790288.jpg) - -上图的一个笔误的小问题:Spring MVC 的入口函数也就是前端控制器 `DispatcherServlet` 的作用是接收请求,响应结果。 - -**流程说明(重要):** - -1. 客户端(浏览器)发送请求,直接请求到 `DispatcherServlet`。 -2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping`,解析请求对应的 `Handler`。 -3. 解析到对应的 `Handler`(也就是我们平常说的 `Controller` 控制器)后,开始由 `HandlerAdapter` 适配器处理。 -4. `HandlerAdapter` 会根据 `Handler `来调用真正的处理器开处理请求,并处理相应的业务逻辑。 -5. 处理器处理完业务后,会返回一个 `ModelAndView` 对象,`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 -6. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 -7. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 -8. 把 `View` 返回给请求者(浏览器) - -## 7. Spring 框架中用到了哪些设计模式? - -关于下面一些设计模式的详细介绍,可以看笔主前段时间的原创文章[《面试官:“谈谈Spring中都用到了那些设计模式?”。》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485303&idx=1&sn=9e4626a1e3f001f9b0d84a6fa0cff04a&chksm=cea248bcf9d5c1aaf48b67cc52bac74eb29d6037848d6cf213b0e5466f2d1fda970db700ba41&token=255050878&lang=zh_CN#rd) 。 - -- **工厂设计模式** : Spring使用工厂模式通过 `BeanFactory`、`ApplicationContext` 创建 bean 对象。 -- **代理设计模式** : Spring AOP 功能的实现。 -- **单例设计模式** : Spring 中的 Bean 默认都是单例的。 -- **模板方法模式** : Spring 中 `jdbcTemplate`、`hibernateTemplate` 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 -- **包装器设计模式** : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 -- **观察者模式:** Spring 事件驱动模型就是观察者模式很经典的一个应用。 -- **适配器模式** :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。 -- ...... - -## 8. Spring 事务 - -### 8.1 Spring 管理事务的方式有几种? - -1. 编程式事务,在代码中硬编码。(不推荐使用) -2. 声明式事务,在配置文件中配置(推荐使用) - -**声明式事务又分为两种:** - -1. 基于XML的声明式事务 -2. 基于注解的声明式事务 - -### 8.2 Spring 事务中的隔离级别有哪几种? - -**TransactionDefinition 接口中定义了五个表示隔离级别的常量:** - -- **TransactionDefinition.ISOLATION_DEFAULT:** 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别. -- **TransactionDefinition.ISOLATION_READ_UNCOMMITTED:** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读** -- **TransactionDefinition.ISOLATION_READ_COMMITTED:** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生** -- **TransactionDefinition.ISOLATION_REPEATABLE_READ:** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生。** -- **TransactionDefinition.ISOLATION_SERIALIZABLE:** 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 - -### 8.3 Spring 事务中哪几种事务传播行为? - -**支持当前事务的情况:** - -- **TransactionDefinition.PROPAGATION_REQUIRED:** 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 -- **TransactionDefinition.PROPAGATION_SUPPORTS:** 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 -- **TransactionDefinition.PROPAGATION_MANDATORY:** 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性) - -**不支持当前事务的情况:** - -- **TransactionDefinition.PROPAGATION_REQUIRES_NEW:** 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 -- **TransactionDefinition.PROPAGATION_NOT_SUPPORTED:** 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 -- **TransactionDefinition.PROPAGATION_NEVER:** 以非事务方式运行,如果当前存在事务,则抛出异常。 - -**其他情况:** - -- **TransactionDefinition.PROPAGATION_NESTED:** 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。 - -### 8.4 @Transactional(rollbackFor = Exception.class)注解了解吗? - -我们知道:Exception分为运行时异常RuntimeException和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。 - -当`@Transactional`注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。 - -在`@Transactional`注解中如果不配置`rollbackFor`属性,那么事物只会在遇到`RuntimeException`的时候才会回滚,加上`rollbackFor=Exception.class`,可以让事物在遇到非运行时异常时也回滚。 - -关于 `@Transactional ` 注解推荐阅读的文章: - -- [透彻的掌握 Spring 中@transactional 的使用](https://www.ibm.com/developerworks/cn/java/j-master-spring-transactional-use/index.html) - -## 9. JPA - -### 9.1 如何使用JPA在数据库中非持久化一个字段? - -假如我们有有下面一个类: - -```java -Entity(name="USER") -public class User { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - @Column(name = "ID") - private Long id; - - @Column(name="USER_NAME") - private String userName; - - @Column(name="PASSWORD") - private String password; - - private String secrect; - -} -``` - -如果我们想让`secrect` 这个字段不被持久化,也就是不被数据库存储怎么办?我们可以采用下面几种方法: - -```java -static String transient1; // not persistent because of static -final String transient2 = “Satish”; // not persistent because of final -transient String transient3; // not persistent because of transient -@Transient -String transient4; // not persistent because of @Transient -``` - -一般使用后面两种方式比较多,我个人使用注解的方式比较多。 - - -## 参考 - -- 《Spring 技术内幕》 -- -- -- -- https://www.cnblogs.com/clwydjgs/p/9317849.html -- -- -- - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/javaguide1.jpg) diff --git a/docs/system-design/framework/spring/SpringMVC-Principle.md b/docs/system-design/framework/spring/SpringMVC-Principle.md deleted file mode 100644 index 0efcd3f..0000000 --- a/docs/system-design/framework/spring/SpringMVC-Principle.md +++ /dev/null @@ -1,269 +0,0 @@ -> 本文整理自网络,原文出处暂不知,对原文做了较大的改动,在此说明! - -### 先来看一下什么是 MVC 模式 - -MVC 是一种设计模式. - -**MVC 的原理图如下:** - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/60679444.jpg) - - - -### SpringMVC 简单介绍 - -SpringMVC 框架是以请求为驱动,围绕 Servlet 设计,将请求发给控制器,然后通过模型对象,分派器来展示请求结果视图。其中核心类是 DispatcherServlet,它是一个 Servlet,顶层是实现的Servlet接口。 - -### SpringMVC 使用 - -需要在 web.xml 中配置 DispatcherServlet 。并且需要配置 Spring 监听器ContextLoaderListener - -```xml - - - org.springframework.web.context.ContextLoaderListener - - - - springmvc - org.springframework.web.servlet.DispatcherServlet - - - - contextConfigLocation - classpath:spring/springmvc-servlet.xml - - 1 - - - springmvc - / - - -``` - -### SpringMVC 工作原理(重要) - -**简单来说:** - -客户端发送请求-> 前端控制器 DispatcherServlet 接受客户端请求 -> 找到处理器映射 HandlerMapping 解析请求对应的 Handler-> HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑 -> 处理器返回一个模型视图 ModelAndView -> 视图解析器进行解析 -> 返回一个视图对象->前端控制器 DispatcherServlet 渲染数据(Moder)->将得到视图对象返回给用户 - - - -**如下图所示:** -![SpringMVC运行原理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/49790288.jpg) - -上图的一个笔误的小问题:Spring MVC 的入口函数也就是前端控制器 DispatcherServlet 的作用是接收请求,响应结果。 - -**流程说明(重要):** - -(1)客户端(浏览器)发送请求,直接请求到 DispatcherServlet。 - -(2)DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。 - -(3)解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。 - -(4)HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑。 - -(5)处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View。 - -(6)ViewResolver 会根据逻辑 View 查找实际的 View。 - -(7)DispaterServlet 把返回的 Model 传给 View(视图渲染)。 - -(8)把 View 返回给请求者(浏览器) - - - -### SpringMVC 重要组件说明 - - -**1、前端控制器DispatcherServlet(不需要工程师开发),由框架提供(重要)** - -作用:**Spring MVC 的入口函数。接收请求,响应结果,相当于转发器,中央处理器。有了 DispatcherServlet 减少了其它组件之间的耦合度。用户请求到达前端控制器,它就相当于mvc模式中的c,DispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,DispatcherServlet的存在降低了组件之间的耦合性。** - -**2、处理器映射器HandlerMapping(不需要工程师开发),由框架提供** - -作用:根据请求的url查找Handler。HandlerMapping负责根据用户请求找到Handler即处理器(Controller),SpringMVC提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。 - -**3、处理器适配器HandlerAdapter** - -作用:按照特定规则(HandlerAdapter要求的规则)去执行Handler -通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。 - -**4、处理器Handler(需要工程师开发)** - -注意:编写Handler时按照HandlerAdapter的要求去做,这样适配器才可以去正确执行Handler -Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。 -由于Handler涉及到具体的用户业务请求,所以一般情况需要工程师根据业务需求开发Handler。 - -**5、视图解析器View resolver(不需要工程师开发),由框架提供** - -作用:进行视图解析,根据逻辑视图名解析成真正的视图(view) -View Resolver负责将处理结果生成View视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。 springmvc框架提供了很多的View视图类型,包括:jstlView、freemarkerView、pdfView等。 -一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由工程师根据业务需求开发具体的页面。 - -**6、视图View(需要工程师开发)** - -View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf...) - -**注意:处理器Handler(也就是我们平常说的Controller控制器)以及视图层view都是需要我们自己手动开发的。其他的一些组件比如:前端控制器DispatcherServlet、处理器映射器HandlerMapping、处理器适配器HandlerAdapter等等都是框架提供给我们的,不需要自己手动开发。** - -### DispatcherServlet详细解析 - -首先看下源码: - -```java -package org.springframework.web.servlet; - -@SuppressWarnings("serial") -public class DispatcherServlet extends FrameworkServlet { - - public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver"; - public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver"; - public static final String THEME_RESOLVER_BEAN_NAME = "themeResolver"; - public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping"; - public static final String HANDLER_ADAPTER_BEAN_NAME = "handlerAdapter"; - public static final String HANDLER_EXCEPTION_RESOLVER_BEAN_NAME = "handlerExceptionResolver"; - public static final String REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME = "viewNameTranslator"; - public static final String VIEW_RESOLVER_BEAN_NAME = "viewResolver"; - public static final String FLASH_MAP_MANAGER_BEAN_NAME = "flashMapManager"; - public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.class.getName() + ".CONTEXT"; - public static final String LOCALE_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".LOCALE_RESOLVER"; - public static final String THEME_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_RESOLVER"; - public static final String THEME_SOURCE_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_SOURCE"; - public static final String INPUT_FLASH_MAP_ATTRIBUTE = DispatcherServlet.class.getName() + ".INPUT_FLASH_MAP"; - public static final String OUTPUT_FLASH_MAP_ATTRIBUTE = DispatcherServlet.class.getName() + ".OUTPUT_FLASH_MAP"; - public static final String FLASH_MAP_MANAGER_ATTRIBUTE = DispatcherServlet.class.getName() + ".FLASH_MAP_MANAGER"; - public static final String EXCEPTION_ATTRIBUTE = DispatcherServlet.class.getName() + ".EXCEPTION"; - public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound"; - private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties"; - protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); - private static final Properties defaultStrategies; - static { - try { - ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class); - defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); - } - catch (IOException ex) { - throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage()); - } - } - - /** Detect all HandlerMappings or just expect "handlerMapping" bean? */ - private boolean detectAllHandlerMappings = true; - - /** Detect all HandlerAdapters or just expect "handlerAdapter" bean? */ - private boolean detectAllHandlerAdapters = true; - - /** Detect all HandlerExceptionResolvers or just expect "handlerExceptionResolver" bean? */ - private boolean detectAllHandlerExceptionResolvers = true; - - /** Detect all ViewResolvers or just expect "viewResolver" bean? */ - private boolean detectAllViewResolvers = true; - - /** Throw a NoHandlerFoundException if no Handler was found to process this request? **/ - private boolean throwExceptionIfNoHandlerFound = false; - - /** Perform cleanup of request attributes after include request? */ - private boolean cleanupAfterInclude = true; - - /** MultipartResolver used by this servlet */ - private MultipartResolver multipartResolver; - - /** LocaleResolver used by this servlet */ - private LocaleResolver localeResolver; - - /** ThemeResolver used by this servlet */ - private ThemeResolver themeResolver; - - /** List of HandlerMappings used by this servlet */ - private List handlerMappings; - - /** List of HandlerAdapters used by this servlet */ - private List handlerAdapters; - - /** List of HandlerExceptionResolvers used by this servlet */ - private List handlerExceptionResolvers; - - /** RequestToViewNameTranslator used by this servlet */ - private RequestToViewNameTranslator viewNameTranslator; - - private FlashMapManager flashMapManager; - - /** List of ViewResolvers used by this servlet */ - private List viewResolvers; - - public DispatcherServlet() { - super(); - } - - public DispatcherServlet(WebApplicationContext webApplicationContext) { - super(webApplicationContext); - } - @Override - protected void onRefresh(ApplicationContext context) { - initStrategies(context); - } - - protected void initStrategies(ApplicationContext context) { - initMultipartResolver(context); - initLocaleResolver(context); - initThemeResolver(context); - initHandlerMappings(context); - initHandlerAdapters(context); - initHandlerExceptionResolvers(context); - initRequestToViewNameTranslator(context); - initViewResolvers(context); - initFlashMapManager(context); - } -} - -``` - -DispatcherServlet类中的属性beans: - -- HandlerMapping:用于handlers映射请求和一系列的对于拦截器的前处理和后处理,大部分用@Controller注解。 -- HandlerAdapter:帮助DispatcherServlet处理映射请求处理程序的适配器,而不用考虑实际调用的是 哪个处理程序。- - - -- ViewResolver:根据实际配置解析实际的View类型。 -- ThemeResolver:解决Web应用程序可以使用的主题,例如提供个性化布局。 -- MultipartResolver:解析多部分请求,以支持从HTML表单上传文件。- -- FlashMapManager:存储并检索可用于将一个请求属性传递到另一个请求的input和output的FlashMap,通常用于重定向。 - -在Web MVC框架中,每个DispatcherServlet都拥自己的WebApplicationContext,它继承了ApplicationContext。WebApplicationContext包含了其上下文和Servlet实例之间共享的所有的基础框架beans。 - -**HandlerMapping** - -![HandlerMapping](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/96666164.jpg) - -HandlerMapping接口处理请求的映射HandlerMapping接口的实现类: - -- SimpleUrlHandlerMapping类通过配置文件把URL映射到Controller类。 -- DefaultAnnotationHandlerMapping类通过注解把URL映射到Controller类。 - -**HandlerAdapter** - - -![HandlerAdapter](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/91433100.jpg) - -HandlerAdapter接口-处理请求映射 - -AnnotationMethodHandlerAdapter:通过注解,把请求URL映射到Controller类的方法上。 - -**HandlerExceptionResolver** - - -![HandlerExceptionResolver](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/50343885.jpg) - -HandlerExceptionResolver接口-异常处理接口 - -- SimpleMappingExceptionResolver通过配置文件进行异常处理。 -- AnnotationMethodHandlerExceptionResolver:通过注解进行异常处理。 - -**ViewResolver** - -![ViewResolver](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/49497279.jpg) - -ViewResolver接口解析View视图。 - -UrlBasedViewResolver类 通过配置文件,把一个视图名交给到一个View来处理。 diff --git "a/docs/system-design/micro-service/API\347\275\221\345\205\263.md" "b/docs/system-design/micro-service/API\347\275\221\345\205\263.md" deleted file mode 100644 index fe96bc0..0000000 --- "a/docs/system-design/micro-service/API\347\275\221\345\205\263.md" +++ /dev/null @@ -1,190 +0,0 @@ -> 点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 -> -> 本文授权转载自:[https://github.com/javagrowing/JGrowing/blob/master/服务端开发/浅析如何设计一个亿级网关.md](https://github.com/javagrowing/JGrowing/blob/master/服务端开发/浅析如何设计一个亿级网关.md)。 - -## 1.背景 - -### 1.1 什么是API网关 - -API网关可以看做系统与外界联通的入口,我们可以在网关进行处理一些非业务逻辑的逻辑,比如权限验证,监控,缓存,请求路由等等。 - -### 1.2 为什么需要API网关 - -- RPC协议转成HTTP。 - -由于在内部开发中我们都是以RPC协议(thrift or dubbo)去做开发,暴露给内部服务,当外部服务需要使用这个接口的时候往往需要将RPC协议转换成HTTP协议。 - -- 请求路由 - -在我们的系统中由于同一个接口新老两套系统都在使用,我们需要根据请求上下文将请求路由到对应的接口。 - -- 统一鉴权 - -对于鉴权操作不涉及到业务逻辑,那么可以在网关层进行处理,不用下层到业务逻辑。 - -- 统一监控 - -由于网关是外部服务的入口,所以我们可以在这里监控我们想要的数据,比如入参出参,链路时间。 - -- 流量控制,熔断降级 - -对于流量控制,熔断降级非业务逻辑可以统一放到网关层。 - -有很多业务都会自己去实现一层网关层,用来接入自己的服务,但是对于整个公司来说这还不够。 - -### 1.3 统一API网关 - -统一的API网关不仅有API网关的所有的特点,还有下面几个好处: - -- 统一技术组件升级 - -在公司中如果有某个技术组件需要升级,那么是需要和每个业务线沟通,通常几个月都搞不定。举个例子如果对于入口的安全鉴权有重大安全隐患需要升级,如果速度还是这么慢肯定是不行,那么有了统一的网关升级是很快的。 - -- 统一服务接入 - -对于某个服务的接入也比较困难,比如公司已经研发出了比较稳定的服务组件,正在公司大力推广,这个周期肯定也特别漫长,由于有了统一网关,那么只需要统一网关统一接入。 - -- 节约资源 - -不同业务不同部门如果按照我们上面的做法应该会都自己搞一个网关层,用来做这个事,可以想象如果一个公司有100个这种业务,每个业务配备4台机器,那么就需要400台机器。并且每个业务的开发RD都需要去开发这个网关层,去随时去维护,增加人力。如果有了统一网关层,那么也许只需要50台机器就可以做这100个业务的网关层的事,并且业务RD不需要随时关注开发,上线的步骤。 - -## 2.统一网关的设计 - -### 2.1 异步化请求 - -对于我们自己实现的网关层,由于只有我们自己使用,对于吞吐量的要求并不高所以,我们一般同步请求调用即可。 - -对于我们统一的网关层,如何用少量的机器接入更多的服务,这就需要我们的异步,用来提高更多的吞吐量。对于异步化一般有下面两种策略: - -- Tomcat/Jetty+NIO+servlet3 - -这种策略使用的比较普遍,京东,有赞,Zuul,都选取的是这个策略,这种策略比较适合HTTP。在Servlet3中可以开启异步。 - -- Netty+NIO - -Netty为高并发而生,目前唯品会的网关使用这个策略,在唯品会的技术文章中在相同的情况下Netty是每秒30w+的吞吐量,Tomcat是13w+,可以看出是有一定的差距的,但是Netty需要自己处理HTTP协议,这一块比较麻烦。 - -对于网关是HTTP请求场景比较多的情况可以采用Servlet,毕竟有更加成熟的处理HTTP协议。如果更加重视吞吐量那么可以采用Netty。 - -#### 2.1.1 全链路异步 - -对于来的请求我们已经使用异步了,为了达到全链路异步所以我们需要对去的请求也进行异步处理,对于去的请求我们可以利用我们rpc的异步支持进行异步请求所以基本可以达到下图: - -[![img](https://camo.githubusercontent.com/ea78c61029cee6487f3aa0cecf5443f18d102173/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633837373331356535353761663f773d3133303026683d34383026663d706e6726733d3639323735)](https://camo.githubusercontent.com/ea78c61029cee6487f3aa0cecf5443f18d102173/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633837373331356535353761663f773d3133303026683d34383026663d706e6726733d3639323735) - -由在web容器中开启servlet异步,然后进入到网关的业务线程池中进行业务处理,然后进行rpc的异步调用并注册需要回调的业务,最后在回调线程池中进行回调处理。 - -### 2.2 链式处理 - -在设计模式中有一个模式叫责任链模式,他的作用是避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。通过这种模式将请求的发送者和请求的处理者解耦了。在我们的各个框架中对此模式都有实现,比如servlet里面的filter,springmvc里面的Interceptor。 - -在Netflix Zuul中也应用了这种模式,如下图所示: - -[![img](https://camo.githubusercontent.com/22d9288d6e9137b98aef563ff7a5a76a090a4609/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633837383234303735333733353f773d39363026683d37323026663d706e6726733d3439333134)](https://camo.githubusercontent.com/22d9288d6e9137b98aef563ff7a5a76a090a4609/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633837383234303735333733353f773d39363026683d37323026663d706e6726733d3439333134) - -这种模式在网关的设计中我们可以借鉴到自己的网关设计: - -- preFilters:前置过滤器,用来处理一些公共的业务,比如统一鉴权,统一限流,熔断降级,缓存处理等,并且提供业务方扩展。 -- routingFilters: 用来处理一些泛化调用,主要是做协议的转换,请求的路由工作。 -- postFilters: 后置过滤器,主要用来做结果的处理,日志打点,记录时间等等。 -- errorFilters: 错误过滤器,用来处理调用异常的情况。 - -这种设计在有赞的网关也有应用。 - -### 2.3 业务隔离 - -上面在全链路异步的情况下不同业务之间的影响很小,但是如果在提供的自定义FiIlter中进行了某些同步调用,一旦超时频繁那么就会对其他业务产生影响。所以我们需要采用隔离之术,降低业务之间的互相影响。 - -#### 2.3.1 信号量隔离 - -信号量隔离只是限制了总的并发数,服务还是主线程进行同步调用。这个隔离如果远程调用超时依然会影响主线程,从而会影响其他业务。因此,如果只是想限制某个服务的总并发调用量或者调用的服务不涉及远程调用的话,可以使用轻量级的信号量来实现。有赞的网关由于没有自定义filter所以选取的是信号量隔离。 - -#### 2.3.2 线程池隔离 - -最简单的就是不同业务之间通过不同的线程池进行隔离,就算业务接口出现了问题由于线程池已经进行了隔离那么也不会影响其他业务。在京东的网关实现之中就是采用的线程池隔离,比较重要的业务比如商品或者订单 都是单独的通过线程池去处理。但是由于是统一网关平台,如果业务线众多,大家都觉得自己的业务比较重要需要单独的线程池隔离,如果使用的是Java语言开发的话那么,在Java中线程是比较重的资源比较受限,如果需要隔离的线程池过多不是很适用。如果使用一些其他语言比如Golang进行开发网关的话,线程是比较轻的资源,所以比较适合使用线程池隔离。 - -#### 2.3.3 集群隔离 - -如果有某些业务就需要使用隔离但是统一网关又没有线程池隔离那么应该怎么办呢?那么可以使用集群隔离,如果你的某些业务真的很重要那么可以为这一系列业务单独申请一个集群或者多个集群,通过机器之间进行隔离。 - -### 2.4 请求限流 - -流量控制可以采用很多开源的实现,比如阿里最近开源的Sentinel和比较成熟的Hystrix。 - -一般限流分为集群限流和单机限流: - -- 利用统一存储保存当前流量的情况,一般可以采用Redis,这个一般会有一些性能损耗。 -- 单机限流:限流每台机器我们可以直接利用Guava的令牌桶去做,由于没有远程调用性能消耗较小。 - -### 2.5 熔断降级 - -这一块也可以参照开源的实现Sentinel和Hystrix,这里不是重点就不多提了。 - -### 2.6 泛化调用 - -泛化调用指的是一些通信协议的转换,比如将HTTP转换成Thrift。在一些开源的网关中比如Zuul是没有实现的,因为各个公司的内部服务通信协议都不同。比如在唯品会中支持HTTP1,HTTP2,以及二进制的协议,然后转化成内部的协议,淘宝的支持HTTPS,HTTP1,HTTP2这些协议都可以转换成,HTTP,HSF,Dubbo等协议。 - -#### 2.6.1泛化调用 - -如何去实现泛化调用呢?由于协议很难自动转换,那么其实每个协议对应的接口需要提供一种映射。简单来说就是把两个协议都能转换成共同语言,从而互相转换。 - -[![img](https://camo.githubusercontent.com/d680a88d0dd9063fe53df26705836c4b7a19c121/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633838306163386264623537353f773d3133333826683d39363326663d706e6726733d3830313730)](https://camo.githubusercontent.com/d680a88d0dd9063fe53df26705836c4b7a19c121/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633838306163386264623537353f773d3133333826683d39363326663d706e6726733d3830313730)一般来说共同语言有三种方式指定: - -- json:json数据格式比较简单,解析速度快,较轻量级。在Dubbo的生态中有一个HTTP转Dubbo的项目是用JsonRpc做的,将HTTP转化成JsonRpc再转化成Dubbo。 - -比如可以将一个 [www.baidu.com/id](http://www.baidu.com/id) = 1 GET 可以映射为json: - -代码块 - -``` - -``` - -- xml:xml数据比较重,解析比较困难,这里不过多讨论。 -- 自定义描述语言:一般来说这个成本比较高需要自己定义语言来进行描述并进行解析,但是其扩展性,自定义个性化性都是最高。例:spring自定义了一套自己的SPEL表达式语言 - -对于泛化调用如果要自己设计的话JSON基本可以满足,如果对于个性化的需要特别多的话倒是可以自己定义一套语言。 - -### 2.7 管理平台 - -上面介绍的都是如何实现一个网关的技术关键。这里需要介绍网关的一个业务关键。有了网关之后,需要一个管理平台如何去对我们上面所描述的技术关键进行配置,包括但不限于下面这些配置: - -- 限流 -- 熔断 -- 缓存 -- 日志 -- 自定义filter -- 泛化调用 - -## 3.总结 - -最后一个合理的标准网关应该按照如下去实现: - -[![img](https://camo.githubusercontent.com/16eef64bd42ee7b2eb08c36039316fadb4e4d6b3/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633838343136633463633232373f773d3230313326683d3130303726663d706e6726733d313532363930)](https://camo.githubusercontent.com/16eef64bd42ee7b2eb08c36039316fadb4e4d6b3/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633838343136633463633232373f773d3230313326683d3130303726663d706e6726733d313532363930) - -| --- | 京东 | 唯品会 | 有赞 | 阿里 | Zuul | -| -------- | ------------------------------ | ----------------------------- | ----------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 实现关键 | servlet3.0 | netty | servlet3.0 | servlet3.0 | servlet3.0 | -| 异步情况 | servlet异步,rpc是否异步不清楚 | 全链路异步 | 全链路异步 | 全链路异步 | Zuul1同步阻塞,Zuul2异步非阻塞 | -| 限流 | --- | --- | 平滑限流。最初是codis,后续换到每个单机的令牌桶限流。 | 1.基本流控:基于API的QPS做限流。2.运营流控:支持APP流量包,APP+API+USER的流控33.大促流控:APP访问API的权重流控。阿里开源:Sentinel | 提供了jar包:spring-cloud-zuul-ratelimit。1.对请求的目标URL进行限流(例如:某个URL每分钟只允许调用多少次)。2.对客户端的访问IP进行限流(例如:某个IP每分钟只允许请求多少次)3.对某些特定用户或者用户组进行限流(例如:非VIP用户限制每分钟只允许调用100次某个API等)4.多维度混合的限流。此时,就需要实现一些限流规则的编排机制。与、或、非等关系。支持四种存储方式ConcurrentHashMap,Consul,Redis,数据库。 | -| 熔断降级 | --- | --- | Hystrix | --- | 只支持服务级别熔断,不支持URL级别。 | -| 隔离 | 线程池隔离 | --- | 信号量隔离 | --- | 线程池隔离,信号量隔离 | -| 缓存 | redis | --- | 二级缓存,本地缓存+Codis | HDCC 本地缓存,远程缓存,数据库 | 需要自己开发 | -| 泛化调用 | --- | http,https,http1,http2,二进制 | dubbo,http,nova | hsf,dubbo,http,https,http2,http1 | 只支持http | - -## 4.参考 - -- 京东:http://www.yunweipai.com/archives/23653.html -- 有赞网关:https://tech.youzan.com/api-gateway-in-practice/ -- 唯品会:https://mp.weixin.qq.com/s/gREMe-G7nqNJJLzbZ3ed3A -- Zuul:http://www.scienjus.com/api-gateway-and-netflix-zuul/ - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) \ No newline at end of file diff --git "a/docs/system-design/micro-service/\345\210\206\345\270\203\345\274\217id\347\224\237\346\210\220\346\226\271\346\241\210\346\200\273\347\273\223.md" "b/docs/system-design/micro-service/\345\210\206\345\270\203\345\274\217id\347\224\237\346\210\220\346\226\271\346\241\210\346\200\273\347\273\223.md" deleted file mode 100644 index bf27804..0000000 --- "a/docs/system-design/micro-service/\345\210\206\345\270\203\345\274\217id\347\224\237\346\210\220\346\226\271\346\241\210\346\200\273\347\273\223.md" +++ /dev/null @@ -1,186 +0,0 @@ -> 点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 -> -> 本文授权转载自:https://juejin.im/post/5d6fc8eff265da03ef7a324b ,作者:1点25。 - -ID是数据的唯一标识,传统的做法是利用UUID和数据库的自增ID,在互联网企业中,大部分公司使用的都是Mysql,并且因为需要事务支持,所以通常会使用Innodb存储引擎,UUID太长以及无序,所以并不适合在Innodb中来作为主键,自增ID比较合适,但是随着公司的业务发展,数据量将越来越大,需要对数据进行分表,而分表后,每个表中的数据都会按自己的节奏进行自增,很有可能出现ID冲突。这时就需要一个单独的机制来负责生成唯一ID,生成出来的ID也可以叫做**分布式ID**,或**全局ID**。下面来分析各个生成分布式ID的机制。 - -![常用分布式id方案](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/分布式id方案.jpeg) - -这篇文章并不会分析的特别详细,主要是做一些总结,以后再出一些详细某个方案的文章。 - - - -## 数据库自增ID - -第一种方案仍然还是基于数据库的自增ID,需要单独使用一个数据库实例,在这个实例中新建一个单独的表: - -表结构如下: - -```sql -CREATE DATABASE `SEQID`; - -CREATE TABLE SEQID.SEQUENCE_ID ( - id bigint(20) unsigned NOT NULL auto_increment, - stub char(10) NOT NULL default '', - PRIMARY KEY (id), - UNIQUE KEY stub (stub) -) ENGINE=MyISAM; -``` - -可以使用下面的语句生成并获取到一个自增ID - -```sql -begin; -replace into SEQUENCE_ID (stub) VALUES ('anyword'); -select last_insert_id(); -commit; -``` - -stub字段在这里并没有什么特殊的意义,只是为了方便的去插入数据,只有能插入数据才能产生自增id。而对于插入我们用的是replace,replace会先看是否存在stub指定值一样的数据,如果存在则先delete再insert,如果不存在则直接insert。 - -这种生成分布式ID的机制,需要一个单独的Mysql实例,虽然可行,但是基于性能与可靠性来考虑的话都不够,**业务系统每次需要一个ID时,都需要请求数据库获取,性能低,并且如果此数据库实例下线了,那么将影响所有的业务系统。** - -为了解决数据库可靠性问题,我们可以使用第二种分布式ID生成方案。 - -## 数据库多主模式 - -如果我们两个数据库组成一个**主从模式**集群,正常情况下可以解决数据库可靠性问题,但是如果主库挂掉后,数据没有及时同步到从库,这个时候会出现ID重复的现象。我们可以使用**双主模式**集群,也就是两个Mysql实例都能单独的生产自增ID,这样能够提高效率,但是如果不经过其他改造的话,这两个Mysql实例很可能会生成同样的ID。需要单独给每个Mysql实例配置不同的起始值和自增步长。 - -第一台Mysql实例配置: - -```sql -set @@auto_increment_offset = 1; -- 起始值 -set @@auto_increment_increment = 2; -- 步长 -``` - -第二台Mysql实例配置: - -```sql -set @@auto_increment_offset = 2; -- 起始值 -set @@auto_increment_increment = 2; -- 步长 -``` - -经过上面的配置后,这两个Mysql实例生成的id序列如下: mysql1,起始值为1,步长为2,ID生成的序列为:1,3,5,7,9,... mysql2,起始值为2,步长为2,ID生成的序列为:2,4,6,8,10,... - -对于这种生成分布式ID的方案,需要单独新增一个生成分布式ID应用,比如DistributIdService,该应用提供一个接口供业务应用获取ID,业务应用需要一个ID时,通过rpc的方式请求DistributIdService,DistributIdService随机去上面的两个Mysql实例中去获取ID。 - -实行这种方案后,就算其中某一台Mysql实例下线了,也不会影响DistributIdService,DistributIdService仍然可以利用另外一台Mysql来生成ID。 - -但是这种方案的扩展性不太好,如果两台Mysql实例不够用,需要新增Mysql实例来提高性能时,这时就会比较麻烦。 - -现在如果要新增一个实例mysql3,要怎么操作呢? 第一,mysql1、mysql2的步长肯定都要修改为3,而且只能是人工去修改,这是需要时间的。 第二,因为mysql1和mysql2是不停在自增的,对于mysql3的起始值我们可能要定得大一点,以给充分的时间去修改mysql1,mysql2的步长。 第三,在修改步长的时候很可能会出现重复ID,要解决这个问题,可能需要停机才行。 - -为了解决上面的问题,以及能够进一步提高DistributIdService的性能,如果使用第三种生成分布式ID机制。 - -## 号段模式 - -我们可以使用号段的方式来获取自增ID,号段可以理解成批量获取,比如DistributIdService从数据库获取ID时,如果能批量获取多个ID并缓存在本地的话,那样将大大提供业务应用获取ID的效率。 - -比如DistributIdService每次从数据库获取ID时,就获取一个号段,比如(1,1000],这个范围表示了1000个ID,业务应用在请求DistributIdService提供ID时,DistributIdService只需要在本地从1开始自增并返回即可,而不需要每次都请求数据库,一直到本地自增到1000时,也就是当前号段已经被用完时,才去数据库重新获取下一号段。 - -所以,我们需要对数据库表进行改动,如下: - -```sql -CREATE TABLE id_generator ( - id int(10) NOT NULL, - current_max_id bigint(20) NOT NULL COMMENT '当前最大id', - increment_step int(10) NOT NULL COMMENT '号段的长度', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -``` - -这个数据库表用来记录自增步长以及当前自增ID的最大值(也就是当前已经被申请的号段的最后一个值),因为自增逻辑被移到DistributIdService中去了,所以数据库不需要这部分逻辑了。 - -这种方案不再强依赖数据库,就算数据库不可用,那么DistributIdService也能继续支撑一段时间。但是如果DistributIdService重启,会丢失一段ID,导致ID空洞。 - -为了提高DistributIdService的高可用,需要做一个集群,业务在请求DistributIdService集群获取ID时,会随机的选择某一个DistributIdService节点进行获取,对每一个DistributIdService节点来说,数据库连接的是同一个数据库,那么可能会产生多个DistributIdService节点同时请求数据库获取号段,那么这个时候需要利用乐观锁来进行控制,比如在数据库表中增加一个version字段,在获取号段时使用如下SQL: - -```sql -update id_generator set current_max_id=#{newMaxId}, version=version+1 where version = #{version} -``` - -因为newMaxId是DistributIdService中根据oldMaxId+步长算出来的,只要上面的update更新成功了就表示号段获取成功了。 - -为了提供数据库层的高可用,需要对数据库使用多主模式进行部署,对于每个数据库来说要保证生成的号段不重复,这就需要利用最开始的思路,再在刚刚的数据库表中增加起始值和步长,比如如果现在是两台Mysql,那么 mysql1将生成号段(1,1001],自增的时候序列为1,3,4,5,7.... mysql1将生成号段(2,1002],自增的时候序列为2,4,6,8,10... - -更详细的可以参考滴滴开源的TinyId:[github.com/didi/tinyid…](https://github.com/didi/tinyid/wiki/tinyid原理介绍) - -在TinyId中还增加了一步来提高效率,在上面的实现中,ID自增的逻辑是在DistributIdService中实现的,而实际上可以把自增的逻辑转移到业务应用本地,这样对于业务应用来说只需要获取号段,每次自增时不再需要请求调用DistributIdService了。 - -## 雪花算法 - -上面的三种方法总的来说是基于自增思想的,而接下来就介绍比较著名的雪花算法-snowflake。 - -我们可以换个角度来对分布式ID进行思考,只要能让负责生成分布式ID的每台机器在每毫秒内生成不一样的ID就行了。 - -snowflake是twitter开源的分布式ID生成算法,是一种算法,所以它和上面的三种生成分布式ID机制不太一样,它不依赖数据库。 - -核心思想是:分布式ID固定是一个long型的数字,一个long型占8个字节,也就是64个bit,原始snowflake算法中对于bit的分配如下图: - -![雪花算法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/雪花算法.png) - -- 第一个bit位是标识部分,在java中由于long的最高位是符号位,正数是0,负数是1,一般生成的ID为正数,所以固定为0。 -- 时间戳部分占41bit,这个是毫秒级的时间,一般实现上不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可以使产生的ID从更小值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年 -- 工作机器id占10bit,这里比较灵活,比如,可以使用前5位作为数据中心机房标识,后5位作为单机房机器标识,可以部署1024个节点。 -- 序列号部分占12bit,支持同一毫秒内同一个节点可以生成4096个ID - -根据这个算法的逻辑,只需要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式ID,只需保证每个业务应用有自己的工作机器id即可,而不需要单独去搭建一个获取分布式ID的应用。 - -snowflake算法实现起来并不难,提供一个github上用java实现的:[github.com/beyondfengy…](https://github.com/beyondfengyu/SnowFlake) - -在大厂里,其实并没有直接使用snowflake,而是进行了改造,因为snowflake算法中最难实践的就是工作机器id,原始的snowflake算法需要人工去为每台机器去指定一个机器id,并配置在某个地方从而让snowflake从此处获取机器id。 - -但是在大厂里,机器是很多的,人力成本太大且容易出错,所以大厂对snowflake进行了改造。 - -### 百度(uid-generator) - -github地址:[uid-generator](https://github.com/baidu/uid-generator) - -uid-generator使用的就是snowflake,只是在生产机器id,也叫做workId时有所不同。 - -uid-generator中的workId是由uid-generator自动生成的,并且考虑到了应用部署在docker上的情况,在uid-generator中用户可以自己去定义workId的生成策略,默认提供的策略是:应用启动时由数据库分配。说的简单一点就是:应用在启动时会往数据库表(uid-generator需要新增一个WORKER_NODE表)中去插入一条数据,数据插入成功后返回的该数据对应的自增唯一id就是该机器的workId,而数据由host,port组成。 - -对于uid-generator中的workId,占用了22个bit位,时间占用了28个bit位,序列化占用了13个bit位,需要注意的是,和原始的snowflake不太一样,时间的单位是秒,而不是毫秒,workId也不一样,同一个应用每重启一次就会消费一个workId。 - -具体可参考[github.com/baidu/uid-g…](https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md) - -### 美团(Leaf) - -github地址:[Leaf](https://github.com/Meituan-Dianping/Leaf) - -美团的Leaf也是一个分布式ID生成框架。它非常全面,即支持号段模式,也支持snowflake模式。号段模式这里就不介绍了,和上面的分析类似。 - -Leaf中的snowflake模式和原始snowflake算法的不同点,也主要在workId的生成,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,在启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。 - -### 总结 - -总得来说,上面两种都是自动生成workId,以让系统更加稳定以及减少人工成功。 - -## Redis - -这里额外再介绍一下使用Redis来生成分布式ID,其实和利用Mysql自增ID类似,可以利用Redis中的incr命令来实现原子性的自增与返回,比如: - -```shell -127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1 -OK -127.0.0.1:6379> incr seq_id // 增加1,并返回 -(integer) 2 -127.0.0.1:6379> incr seq_id // 增加1,并返回 -(integer) 3 -``` - -使用redis的效率是非常高的,但是要考虑持久化的问题。Redis支持RDB和AOF两种持久化的方式。 - -RDB持久化相当于定时打一个快照进行持久化,如果打完快照后,连续自增了几次,还没来得及做下一次快照持久化,这个时候Redis挂掉了,重启Redis后会出现ID重复。 - -AOF持久化相当于对每条写命令进行持久化,如果Redis挂掉了,不会出现ID重复的现象,但是会由于incr命令过得,导致重启恢复数据时间过长。 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) \ No newline at end of file diff --git "a/docs/system-design/website-architecture/8 \345\274\240\345\233\276\350\257\273\346\207\202\345\244\247\345\236\213\347\275\221\347\253\231\346\212\200\346\234\257\346\236\266\346\236\204.md" "b/docs/system-design/website-architecture/8 \345\274\240\345\233\276\350\257\273\346\207\202\345\244\247\345\236\213\347\275\221\347\253\231\346\212\200\346\234\257\346\236\266\346\236\204.md" deleted file mode 100644 index f975930..0000000 --- "a/docs/system-design/website-architecture/8 \345\274\240\345\233\276\350\257\273\346\207\202\345\244\247\345\236\213\347\275\221\347\253\231\346\212\200\346\234\257\346\236\266\346\236\204.md" +++ /dev/null @@ -1,47 +0,0 @@ -> 本文是作者读 《大型网站技术架构》所做的思维导图,在这里分享给各位,公众号(JavaGuide)后台回复:“架构”。即可获得下面图片的源文件以及思维导图源文件! - - - -- [1. 大型网站架构演化](#1-大型网站架构演化) -- [2. 大型架构模式](#2-大型架构模式) -- [3. 大型网站核心架构要素](#3-大型网站核心架构要素) -- [4. 瞬时响应:网站的高性能架构](#4-瞬时响应网站的高性能架构) -- [5. 万无一失:网站的高可用架构](#5-万无一失网站的高可用架构) -- [6. 永无止境:网站的伸缩性架构](#6-永无止境网站的伸缩性架构) -- [7. 随机应变:网站的可扩展性架构](#7-随机应变网站的可扩展性架构) -- [8. 固若金汤:网站的安全机构](#8-固若金汤网站的安全机构) - - - - -### 1. 大型网站架构演化 - -![1. 大型网站架构演化](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/1%20%E5%A4%A7%E5%9E%8B%E7%BD%91%E7%AB%99%E6%9E%B6%E6%9E%84%E6%BC%94%E5%8C%96.png) - -### 2. 大型架构模式 - -![2. 大型架构模式](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2%20%E5%A4%A7%E5%9E%8B%E6%9E%B6%E6%9E%84%E6%A8%A1%E5%BC%8F.png) - -### 3. 大型网站核心架构要素 - -![3. 大型网站核心架构要素](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/3%20%E5%A4%A7%E5%9E%8B%E7%BD%91%E7%AB%99%E6%A0%B8%E5%BF%83%E6%9E%B6%E6%9E%84%E8%A6%81%E7%B4%A0.png) - -### 4. 瞬时响应:网站的高性能架构 - -![4. 瞬时响应:网站的高性能架构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/4%20%E7%9E%AC%E6%97%B6%E5%93%8D%E5%BA%94%EF%BC%9A%E7%BD%91%E7%AB%99%E7%9A%84%E9%AB%98%E6%80%A7%E8%83%BD%E6%9E%B6%E6%9E%84.png) - -### 5. 万无一失:网站的高可用架构 - -![5. 万无一失:网站的高可用架构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/5%20%E4%B8%87%E6%97%A0%E4%B8%80%E5%A4%B1%EF%BC%9A%E7%BD%91%E7%AB%99%E7%9A%84%E9%AB%98%E5%8F%AF%E7%94%A8%E6%9E%B6%E6%9E%84.png) - -### 6. 永无止境:网站的伸缩性架构 - -![6. 永无止境:网站的伸缩性架构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/6%20%E6%B0%B8%E6%97%A0%E6%AD%A2%E5%A2%83%EF%BC%9A%E7%BD%91%E7%AB%99%E7%9A%84%E4%BC%B8%E7%BC%A9%E6%80%A7%E6%9E%B6%E6%9E%84.png) - -### 7. 随机应变:网站的可扩展性架构 - -![7. 随机应变:网站的可扩展性架构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/7%20%E9%9A%8F%E6%9C%BA%E5%BA%94%E5%8F%98%EF%BC%9A%E7%BD%91%E7%AB%99%E7%9A%84%E5%8F%AF%E6%89%A9%E5%B1%95%E6%9E%B6%E6%9E%84.png) - -### 8. 固若金汤:网站的安全机构 - -![enter image description here](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/8%20%E5%9B%BA%E8%8B%A5%E9%87%91%E6%B1%A4%EF%BC%9A%E7%BD%91%E7%AB%99%E7%9A%84%E5%AE%89%E5%85%A8%E6%9E%B6%E6%9E%84.png) diff --git "a/docs/system-design/website-architecture/\345\205\263\344\272\216\345\244\247\345\236\213\347\275\221\347\253\231\347\263\273\347\273\237\346\236\266\346\236\204\344\275\240\344\270\215\345\276\227\344\270\215\346\207\202\347\232\20410\344\270\252\351\227\256\351\242\230.md" "b/docs/system-design/website-architecture/\345\205\263\344\272\216\345\244\247\345\236\213\347\275\221\347\253\231\347\263\273\347\273\237\346\236\266\346\236\204\344\275\240\344\270\215\345\276\227\344\270\215\346\207\202\347\232\20410\344\270\252\351\227\256\351\242\230.md" deleted file mode 100644 index c5d585c..0000000 --- "a/docs/system-design/website-architecture/\345\205\263\344\272\216\345\244\247\345\236\213\347\275\221\347\253\231\347\263\273\347\273\237\346\236\266\346\236\204\344\275\240\344\270\215\345\276\227\344\270\215\346\207\202\347\232\20410\344\270\252\351\227\256\351\242\230.md" +++ /dev/null @@ -1,190 +0,0 @@ -下面这些问题都是一线大厂的真实面试问题,不论是对你面试还是说拓宽知识面都很有帮助。之前发过一篇[8 张图读懂大型网站技术架构](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484416&idx=1&sn=6ced00d65491ef8fd33151bdfa8895c9&chksm=fd985261caefdb779412974a6a7207c93d0c2da5b28489afb74acd2fee28505daebbadb018ff&token=177958022&lang=zh_CN#rd) 可以作为不太了解大型网站系统技术架构朋友的入门文章。 - - - -- [1. 你使用过哪些组件或者方法来提升网站性能,可用性以及并发量](#1-你使用过哪些组件或者方法来提升网站性能可用性以及并发量) -- [2. 设计高可用系统的常用手段](#2-设计高可用系统的常用手段) -- [3. 现代互联网应用系统通常具有哪些特点?](#3-现代互联网应用系统通常具有哪些特点) -- [4. 谈谈你对微服务领域的了解和认识](#4-谈谈你对微服务领域的了解和认识) -- [5. 谈谈你对 Dubbo 和 Spring Cloud 的认识\(两者关系\)](#5-谈谈你对-dubbo-和-spring-cloud-的认识两者关系) -- [6. 性能测试了解吗?说说你知道的性能测试工具?](#6-性能测试了解吗说说你知道的性能测试工具) -- [7. 对于一个单体应用系统,随着产品使用的用户越来越多,网站的流量会增加,最终单台服务器无法处理那么大的流量怎么办?](#7-对于一个单体应用系统随着产品使用的用户越来越多网站的流量会增加最终单台服务器无法处理那么大的流量怎么办) -- [8. 大表优化的常见手段](#8-大表优化的常见手段) -- [9. 在系统中使用消息队列能带来什么好处?](#9-在系统中使用消息队列能带来什么好处) - - [1) 通过异步处理提高系统性能](#1-通过异步处理提高系统性能) -- [2) 降低系统耦合性](#2-降低系统耦合性) -- [10. 说说自己对 CAP 定理,BASE 理论的了解](#10-说说自己对-cap-定理base-理论的了解) - - [CAP 定理](#cap-定理) - - [BASE 理论](#base-理论) -- [参考](#参考) - - - - -### 1. 你使用过哪些组件或者方法来提升网站性能,可用性以及并发量 - -1. **提高硬件能力、增加系统服务器**。(当服务器增加到某个程度的时候系统所能提供的并发访问量几乎不变,所以不能根本解决问题) -2. **使用缓存**(本地缓存:本地可以使用JDK自带的 Map、Guava Cache.分布式缓存:Redis、Memcache.本地缓存不适用于提高系统并发量,一般是用处用在程序中。比如Spring是如何实现单例的呢?大家如果看过源码的话,应该知道,S把已经初始过的变量放在一个Map中,下次再要使用这个变量的时候,先判断Map中有没有,这也就是系统中常见的单例模式的实现。) -3. **消息队列** (解耦+削峰+异步) -4. **采用分布式开发** (不同的服务部署在不同的机器节点上,并且一个服务也可以部署在多台机器上,然后利用 Nginx 负载均衡访问。这样就解决了单点部署(All In)的缺点,大大提高的系统并发量) -5. **数据库分库(读写分离)、分表(水平分表、垂直分表)** -6. **采用集群** (多台机器提供相同的服务) -7. **CDN 加速** (将一些静态资源比如图片、视频等等缓存到离用户最近的网络节点) -8. **浏览器缓存** -9. **使用合适的连接池**(数据库连接池、线程池等等) -10. **适当使用多线程进行开发。** - - -### 2. 设计高可用系统的常用手段 - -1. **降级:** 服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好; -2. **限流:** 防止恶意请求流量、恶意攻击,或者防止流量超出系统峰值; -3. **缓存:** 避免大量请求直接落到数据库,将数据库击垮; -4. **超时和重试机制:** 避免请求堆积造成雪崩; -5. **回滚机制:** 快速修复错误版本。 - - -### 3. 现代互联网应用系统通常具有哪些特点? - -1. 高并发,大流量; -2. 高可用:系统7×24小时不间断服务; -3. 海量数据:需要存储、管理海量数据,需要使用大量服务器; -4. 用户分布广泛,网络情况复杂:许多大型互联网都是为全球用户提供服务的,用户分布范围广,各地网络情况千差万别; -5. 安全环境恶劣:由于互联网的开放性,使得互联网更容易受到攻击,大型网站几乎每天都会被黑客攻击; -6. 需求快速变更,发布频繁:和传统软件的版本发布频率不同,互联网产品为快速适应市场,满足用户需求,其产品发布频率是极高的; -7. 渐进式发展:与传统软件产品或企业应用系统一开始就规划好全部的功能和非功能需求不同,几乎所有的大型互联网网站都是从一个小网站开始,渐进地发展起来。 - -### 4. 谈谈你对微服务领域的了解和认识 - -现在大公司都在用并且未来的趋势都是 Spring Cloud,而阿里开源的 Spring Cloud Alibaba 也是 Spring Cloud 规范的实现 。 - -我们通常把 Spring Cloud 理解为一系列开源组件的集合,但是 Spring Cloud并不是等同于 Spring Cloud Netflix 的 Ribbon、Feign、Eureka(停止更新)、Hystrix 这一套组件,而是抽象了一套通用的开发模式。它的目的是通过抽象出这套通用的模式,让开发者更快更好地开发业务。但是这套开发模式运行时的实际载体,还是依赖于 RPC、网关、服务发现、配置管理、限流熔断、分布式链路跟踪等组件的具体实现。 - -Spring Cloud Alibaba 是官方认证的新一套 Spring Cloud 规范的实现,Spring Cloud Alibaba 是一套国产开源产品集合,后续还会有中文 reference 和一些原理分析文章,所以,这对于国内的开发者是非常棒的一件事。阿里的这一举动势必会推动国内微服务技术的发展,因为在没有 Spring Cloud Alibaba 之前,我们的第一选择是 Spring Cloud Netflix,但是它们的文档都是英文的,出问题后排查也比较困难, 在国内并不是有特别多的人精通。Spring Cloud Alibaba 由阿里开源组件和阿里云产品组件两部分组成,其致力于提供微服务一站式解决方案,方便开发者通过 Spring Cloud 编程模型轻松开发微服务应用。 - -另外,Apache Dubbo Ecosystem 是围绕 Apache Dubbo 打造的微服务生态,是经过生产验证的微服务的最佳实践组合。在阿里巴巴的微服务解决方案中,Dubbo、Nacos 和 Sentinel,以及后续将开源的微服务组件,都是 Dubbo EcoSystem 的一部分。阿里后续也会将 Dubbo EcoSystem 集成到 Spring Cloud 的生态中。 - -### 5. 谈谈你对 Dubbo 和 Spring Cloud 的认识(两者关系) - -具体可以看公众号-阿里巴巴中间件的这篇文章:[独家解读:Dubbo Ecosystem - 从微服务框架到微服务生态](https://mp.weixin.qq.com/s/iNVctXw7tUGHhnF0hV84ww) - -Dubbo 与 Spring Cloud 并不是竞争关系,Dubbo 作为成熟的 RPC 框架,其易用性、扩展性和健壮性已得到业界的认可。未来 Dubbo 将会作为 Spring Cloud Alibaba 的 RPC 组件,并与 Spring Cloud 原生的 Feign 以及 RestTemplate 进行无缝整合,实现“零”成本迁移。 - -在阿里巴巴的微服务解决方案中,Dubbo、Nacos 和 Sentinel,以及后续将开源的微服务组件,都是 Dubbo EcoSystem 的一部分。我们后续也会将 Dubbo EcoSystem 集成到 Spring Cloud 的生态中。 - -### 6. 性能测试了解吗?说说你知道的性能测试工具? - -性能测试指通过自动化的测试工具模拟多种正常、峰值以及异常负载条件来对系统的各项性能指标进行测试。性能测试是总称,通常细分为: - -1. **基准测试:** 在给系统施加较低压力时,查看系统的运行状况并记录相关数做为基础参考 -2. **负载测试:** 是指对系统不断地增加压力或增加一定压力下的持续时间,直到系统的某项或多项性能指标达到安全临界值,例如某种资源已经达到饱和状态等 。此时继续加压,系统处理能力会下降。 -3. **压力测试:** 超过安全负载情况下,不断施加压力(增加并发请求),直到系统崩溃或无法处理任何请求,依此获得系统最大压力承受能力。 -4. **稳定性测试:** 被测试系统在特定硬件、软件、网络环境下,加载一定业务压力(模拟生产环境不同时间点、不均匀请求,呈波浪特性)运行一段较长时间,以此检测系统是否稳定。 - -后端程序员或者测试平常比较常用的测试工具是 JMeter(官网:[https://jmeter.apache.org/](https://jmeter.apache.org/))。Apache JMeter 是一款基于Java的压力测试工具(100%纯Java应用程序),旨在加载测试功能行为和测量性能。它最初被设计用于 Web 应用测试但后来扩展到其他测试领域。 - -### 7. 对于一个单体应用系统,随着产品使用的用户越来越多,网站的流量会增加,最终单台服务器无法处理那么大的流量怎么办? - -这个时候就要考虑扩容了。《亿级流量网站架构核心技术》这本书上面介绍到我们可以考虑下面几步来解决这个问题: - -- 第一步,可以考虑简单的扩容来解决问题。比如增加系统的服务器,提高硬件能力等等。 -- 第二步,如果简单扩容搞不定,就需要水平拆分和垂直拆分数据/应用来提升系统的伸缩性,即通过扩容提升系统负载能力。 -- 第三步,如果通过水平拆分/垂直拆分还是搞不定,那就需要根据现有系统特性,架构层面进行重构甚至是重新设计,即推倒重来。 - -对于系统设计,理想的情况下应支持线性扩容和弹性扩容,即在系统瓶颈时,只需要增加机器就可以解决系统瓶颈,如降低延迟提升吞吐量,从而实现扩容需求。 - -如果你想扩容,则支持水平/垂直伸缩是前提。在进行拆分时,一定要清楚知道自己的目的是什么,拆分后带来的问题如何解决,拆分后如果没有得到任何收益就不要为了 -拆而拆,即不要过度拆分,要适合自己的业务。 - -### 8. 大表优化的常见手段 - - 当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下: - -1. **限定数据的范围:** 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内; -2. **读/写分离:** 经典的数据库拆分方案,主库负责写,从库负责读; -3. **垂直分区:** **根据数据库里面数据表的相关性进行拆分。** 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。**简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。** 如下图所示,这样来说大家应该就更容易理解了。![](https://user-gold-cdn.xitu.io/2018/6/16/164084354ba2e0fd?w=950&h=279&f=jpeg&s=26015)**垂直拆分的优点:** 可以使得行数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。**垂直拆分的缺点:** 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂; -4. **水平分区:** **保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。** 水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。![数据库水平拆分](https://user-gold-cdn.xitu.io/2018/6/16/164084b7e9e423e3?w=690&h=271&f=jpeg&s=23119)水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 **水平拆分最好分库** 。水平拆分能够 **支持非常大的数据量存储,应用端改造也少**,但 **分片事务难以解决** ,跨界点Join性能较差,逻辑复杂。《Java工程师修炼之道》的作者推荐 **尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度** ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。 - -**下面补充一下数据库分片的两种常见方案:** - -- **客户端代理:** **分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。** 当当网的 **Sharding-JDBC** 、阿里的TDDL是两种比较常用的实现。 -- **中间件代理:** **在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。** 我们现在谈的 **Mycat** 、360的Atlas、网易的DDB等等都是这种架构的实现。 - -### 9. 在系统中使用消息队列能带来什么好处? - -**《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。** - -#### 1) 通过异步处理提高系统性能 -![通过异步处理提高系统性能](https://user-gold-cdn.xitu.io/2018/4/21/162e63a8e34ba534?w=910&h=350&f=jpeg&s=29123) -如上图,**在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。** - -通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: -![合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击](https://user-gold-cdn.xitu.io/2018/4/21/162e64583dd3ed01?w=780&h=384&f=jpeg&s=13550) -因为**用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败**。因此使用消息队列进行异步处理之后,需要**适当修改业务流程进行配合**,比如**用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 - -### 2) 降低系统耦合性 -我们知道模块分布式部署以后聚合方式通常有两种:1.**分布式消息队列**和2.**分布式服务**。 - -> **先来简单说一下分布式服务:** - -目前使用比较多的用来构建**SOA(Service Oriented Architecture面向服务体系结构)**的**分布式服务框架**是阿里巴巴开源的**Dubbo**.如果想深入了解Dubbo的可以看我写的关于Dubbo的这一篇文章:**《高性能优秀的服务框架-dubbo介绍》**:[https://juejin.im/post/5acadeb1f265da2375072f9c](https://juejin.im/post/5acadeb1f265da2375072f9c) - -> **再来谈我们的分布式消息队列:** - -我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。 - -我们最常见的**事件驱动架构**类似生产者消费者模式,在大型网站中通常用利用消息队列实现事件驱动结构。如下图所示: -![利用消息队列实现事件驱动结构](https://user-gold-cdn.xitu.io/2018/4/21/162e6665fa394b3b?w=790&h=290&f=jpeg&s=14946) -**消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 - -消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。 - -**另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。** - -**备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的,**比如在我们的ActiveMQ消息队列中还有点对点工作模式**,具体的会在后面的文章给大家详细介绍,这一篇文章主要还是让大家对消息队列有一个更透彻的了解。 - -> 这个问题一般会在上一个问题问完之后,紧接着被问到。“使用消息队列会带来什么问题?”这个问题要引起重视,一般我们都会考虑使用消息队列会带来的好处而忽略它带来的问题! - -### 10. 说说自己对 CAP 定理,BASE 理论的了解 - -#### CAP 定理 - -![CAP定理](https://user-gold-cdn.xitu.io/2018/5/24/163912e973ecb93c?w=624&h=471&f=png&s=32984) -在理论计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer's theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点: - -- **一致性(Consistence)** :所有节点访问同一份最新的数据副本 -- **可用性(Availability)**:每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据 -- **分区容错性(Partition tolerance)** : 分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。 - -CAP仅适用于原子读写的NOSQL场景中,并不适合数据库系统。现在的分布式系统具有更多特性比如扩展性、可用性等等,在进行系统设计和开发时,我们不应该仅仅局限在CAP问题上。 - -**注意:不是所谓的3选2(不要被网上大多数文章误导了):** - -大部分人解释这一定律时,常常简单的表述为:“一致性、可用性、分区容忍性三者你只能同时达到其中两个,不可能同时达到”。实际上这是一个非常具有误导性质的说法,而且在CAP理论诞生12年之后,CAP之父也在2012年重写了之前的论文。 - -**当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能2选1。也就是说当网络分区之后P是前提,决定了P之后才有C和A的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。** - -我在网上找了很多文章想看一下有没有文章提到这个不是所谓的3选2,用百度半天没找到了一篇,用谷歌搜索找到一篇比较不错的,如果想深入学习一下CAP就看这篇文章把,我这里就不多BB了:**《分布式系统之CAP理论》 :** [http://www.cnblogs.com/hxsyl/p/4381980.html](http://www.cnblogs.com/hxsyl/p/4381980.html) - - -#### BASE 理论 - -**BASE** 是 **Basically Available(基本可用)** 、**Soft-state(软状态)** 和 **Eventually Consistent(最终一致性)** 三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的,它大大降低了我们对系统的要求。 - -**BASE理论的核心思想:** 即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。也就是牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。 - - -**BASE理论三要素:** - -![BASE理论三要素](https://user-gold-cdn.xitu.io/2018/5/24/163914806d9e15c6?w=612&h=461&f=png&s=39129) - -1. **基本可用:** 基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。 比如: **①响应时间上的损失**:正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒;**②系统功能上的损失**:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面; -2. **软状态:** 软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时; -3. **最终一致性:** 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。 - -### 参考 - -- 《大型网站技术架构》 -- 《亿级流量网站架构核心技术》 -- 《Java工程师修炼之道》 -- https://www.cnblogs.com/puresoul/p/5456855.html diff --git "a/docs/system-design/website-architecture/\345\210\206\345\270\203\345\274\217.md" "b/docs/system-design/website-architecture/\345\210\206\345\270\203\345\274\217.md" deleted file mode 100644 index d5632f5..0000000 --- "a/docs/system-design/website-architecture/\345\210\206\345\270\203\345\274\217.md" +++ /dev/null @@ -1,37 +0,0 @@ - - ### 一 分布式系统的经典基础理论 - - [分布式系统的经典基础理论](https://blog.csdn.net/qq_34337272/article/details/80444032) - - 本文主要是简单的介绍了三个常见的概念: **分布式系统设计理念** 、 **CAP定理** 、 **BASE理论** ,关于分布式系统的还有很多很多东西。 - ![分布式系统的经典基础理论总结](https://user-gold-cdn.xitu.io/2018/5/24/1639234237ec9805?w=791&h=466&f=png&s=55908) - - - ### 二 分布式事务 - 分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。 - * [深入理解分布式事务](http://www.codeceo.com/article/distributed-transaction.html) - * [分布式事务?No, 最终一致性](https://zhuanlan.zhihu.com/p/25933039) - * [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html) - - - - ### 三 分布式系统一致性 - [分布式服务化系统一致性的“最佳实干”](https://www.jianshu.com/p/1156151e20c8) - - - ### 四 一致性协议/算法 - 早在1898年就诞生了著名的 **Paxos经典算法** (**Zookeeper就采用了Paxos算法的近亲兄弟Zab算法**),但由于Paxos算法非常难以理解、实现、排错。所以不断有人尝试简化这一算法,直到2013年才有了重大突破:斯坦福的Diego Ongaro、John Ousterhout以易懂性为目标设计了新的一致性算法—— **Raft算法** ,并发布了对应的论文《In Search of an Understandable Consensus Algorithm》,到现在有十多种语言实现的Raft算法框架,较为出名的有以Go语言实现的Etcd,它的功能类似于Zookeeper,但采用了更为主流的Rest接口。 - * [图解 Paxos 一致性协议](https://mp.weixin.qq.com/s?__biz=MzI0NDI0MTgyOA==&mid=2652037784&idx=1&sn=d8c4f31a9cfb49ee91d05bb374e5cdd5&chksm=f2868653c5f10f45fc4a64d15a5f4163c3e66c00ed2ad334fa93edb46671f42db6752001f6c0#rd) - * [图解分布式协议-RAFT](http://ifeve.com/raft/) - * [Zookeeper ZAB 协议分析](https://dbaplus.cn/news-141-1875-1.html) - -- ### 五 分布式存储 - - **分布式存储系统将数据分散存储在多台独立的设备上**。传统的网络存储系统采用集中的存储服务器存放所有数据,存储服务器成为系统性能的瓶颈,也是可靠性和安全性的焦点,不能满足大规模存储应用的需要。分布式网络存储系统采用可扩展的系统结构,利用多台存储服务器分担存储负荷,利用位置服务器定位存储信息,它不但提高了系统的可靠性、可用性和存取效率,还易于扩展。 - - * [分布式存储系统概要](http://witchiman.top/2017/05/05/distributed-system/) - -- ### 六 分布式计算 - - **所谓分布式计算是一门计算机科学,它研究如何把一个需要非常巨大的计算能力才能解决的问题分成许多小的部分,然后把这些部分分配给许多计算机进行处理,最后把这些计算结果综合起来得到最终的结果。** - 分布式网络存储技术是将数据分散的存储于多台独立的机器设备上。分布式网络存储系统采用可扩展的系统结构,利用多台存储服务器分担存储负荷,利用位置服务器定位存储信息,不但解决了传统集中式存储系统中单存储服务器的瓶颈问题,还提高了系统的可靠性、可用性和扩展性。 - - * [关于分布式计算的一些概念](https://blog.csdn.net/qq_34337272/article/details/80549020) - - \ No newline at end of file diff --git "a/docs/system-design/website-architecture/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\357\274\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271\357\274\237.md" "b/docs/system-design/website-architecture/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\357\274\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271\357\274\237.md" deleted file mode 100644 index a25736a..0000000 --- "a/docs/system-design/website-architecture/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\357\274\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271\357\274\237.md" +++ /dev/null @@ -1,70 +0,0 @@ -一篇短小的文章,面试经常遇到的这个问题。本文主要包括下面这些内容: - -1. 高可用的定义 -2. 哪些情况可能会导致系统不可用? -3. 有些提高系统可用性的方法?只是简单的提一嘴,更具体内容在后续的文章中介绍,就拿限流来说,你需要搞懂:何为限流?如何限流?为什么要限流?如何做呢?说一下原理?。 - -## 什么是高可用?可用性的判断标准是啥? - -**高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的。** - -**一般情况下,我们使用多少个 9 来评判一个系统的可用性,比如 99.9999% 就是代表该系统在所有的运行时间中只有 0.0001% 的时间是不可用的,这样的系统就是非常非常高可用的了!当然,也会有系统如果可用性不太好的话,可能连 9 都上不了。** - -## 哪些情况会导致系统不可用? - -1. 黑客攻击; -2. 硬件故障,比如服务器坏掉。 -3. 并发量/用户请求量激增导致整个服务宕掉或者部分服务不可用。 -4. 代码中的坏味道导致内存泄漏或者其他问题导致程序挂掉。 -5. 网站架构某个重要的角色比如 Nginx 或者数据库突然不可用。 -6. 自然灾害或者人为破坏。 -7. ...... - -## 有哪些提高系统可用性的方法? - -### 1. 注重代码质量,测试严格把关 - -我觉得这个是最最最重要的,代码质量有问题比如比较常见的内存泄漏、循环依赖都是对系统可用性极大的损害。大家都喜欢谈限流、降级、熔断,但是我觉得从代码质量这个源头把关是首先要做好的一件很重要的事情。如何提高代码质量?比较实际可用的就是 CodeReview,不要在乎每天多花的那 1 个小时左右的时间,作用可大着呢! - -另外,安利这个对提高代码质量有实际效果的宝贝: - -1. sonarqube :保证你写出更安全更干净的代码!(ps: 目前所在的项目基本都会用到这个插件)。 -2. Alibaba 开源的 Java 诊断工具 Arthas 也是很不错的选择。 -3. IDEA 自带的代码分析等工具进行代码扫描也是非常非常棒的。 - -### 2.使用集群,减少单点故障 - -先拿常用的 Redis 举个例子!我们如何保证我们的 Redis 缓存高可用呢?答案就是使用集群,避免单点故障。当我们使用一个 Redis 实例作为缓存的时候,这个 Redis 实例挂了之后,整个缓存服务可能就挂了。使用了集群之后,即使一台 Redis 实例,不到一秒就会有另外一台 Redis 实例顶上。 - -### 3.限流 - -流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。——来自 alibaba-[Sentinel](https://github.com/alibaba/Sentinel "Sentinel") 的 wiki。 - -### 4.超时和重试机制设置 - -一旦用户请求超过某个时间的得不到响应,就抛出异常。这个是非常重要的,很多线上系统故障都是因为没有进行超时设置或者超时设置的方式不对导致的。我们在读取第三方服务的时候,尤其适合设置超时和重试机制。一般我们使用一些 RPC 框架的时候,这些框架都自带的超时重试的配置。如果不进行超时设置可能会导致请求响应速度慢,甚至导致请求堆积进而让系统无法在处理请求。重试的次数一般设为 3 次,再多次的重试没有好处,反而会加重服务器压力(部分场景使用失败重试机制会不太适合)。 - -### 5.熔断机制 - -超时和重试机制设置之外,熔断机制也是很重要的。 熔断机制说的是系统自动收集所依赖服务的资源使用情况和性能指标,当所依赖的服务恶化或者调用失败次数达到某个阈值的时候就迅速失败,让当前系统立即切换依赖其他备用服务。 比较常用的是流量控制和熔断降级框架是 Netflix 的 Hystrix 和 alibaba 的 Sentinel。 - -### 6.异步调用 - -异步调用的话我们不需要关心最后的结果,这样我们就可以用户请求完成之后就立即返回结果,具体处理我们可以后续再做,秒杀场景用这个还是蛮多的。但是,使用异步之后我们可能需要 **适当修改业务流程进行配合**,比如**用户在提交订单之后,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**。除了可以在程序中实现异步之外,我们常常还使用消息队列,消息队列可以通过异步处理提高系统性能(削峰、减少响应所需时间)并且可以降低系统耦合性。 - -### 7.使用缓存 - -如果我们的系统属于并发量比较高的话,如果我们单纯使用数据库的话,当大量请求直接落到数据库可能数据库就会直接挂掉。使用缓存缓存热点数据,因为缓存存储在内存中,所以速度相当地快! - -### 8.其他 - -1. **核心应用和服务优先使用更好的硬件** -2. **监控系统资源使用情况增加报警设置。** -3. **注意备份,必要时候回滚。** -4. **灰度发布:** 将服务器集群分成若干部分,每天只发布一部分机器,观察运行稳定没有故障,第二天继续发布一部分机器,持续几天才把整个集群全部发布完毕,期间如果发现问题,只需要回滚已发布的一部分服务器即可 -5. **定期检查/更换硬件:** 如果不是购买的云服务的话,定期还是需要对硬件进行一波检查的,对于一些需要更换或者升级的硬件,要及时更换或者升级。 -6. .....(想起来再补充!也欢迎各位欢迎补充!) - -## 总结 - -![如何设计高可用系统?](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/如何设计高可用的系统?.png) \ No newline at end of file diff --git "a/docs/system-design/\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/docs/system-design/\350\256\276\350\256\241\346\250\241\345\274\217.md" deleted file mode 100644 index 6af52e4..0000000 --- "a/docs/system-design/\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ /dev/null @@ -1,84 +0,0 @@ -# Java 设计模式 - -下面是自己学习设计模式的时候做的总结,有些是自己的原创文章,有些是网上写的比较好的文章,保存下来细细消化吧! - -**系列文章推荐:** - -## 创建型模式 - -### 创建型模式概述 - -- 创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。 -- 创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。 - -![创建型模式](https://user-gold-cdn.xitu.io/2018/6/16/1640641afcb7559b?w=491&h=241&f=png&s=51443) - -### 常见创建型模式详解 - -- **单例模式:** [深入理解单例模式——只有一个实例](https://blog.csdn.net/qq_34337272/article/details/80455972) -- **工厂模式:** [深入理解工厂模式——由对象工厂生成对象](https://blog.csdn.net/qq_34337272/article/details/80472071) -- **建造者模式:** [深入理解建造者模式 ——组装复杂的实例](http://blog.csdn.net/qq_34337272/article/details/80540059) -- **原型模式:** [深入理解原型模式 ——通过复制生成实例](https://blog.csdn.net/qq_34337272/article/details/80706444) - -## 结构型模式 - -### 结构型模式概述 - -- **结构型模式(Structural Pattern):** 描述如何将类或者对象结合在一起形成更大的结构,就像搭积木,可以通过简单积木的组合形成复杂的、功能更为强大的结构 -![结构型模式(Structural Pattern)](https://user-gold-cdn.xitu.io/2018/6/16/164064d6b3c205e3?w=719&h=233&f=png&s=270293) -- **结构型模式可以分为类结构型模式和对象结构型模式:** - - 类结构型模式关心类的组合,由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系。 - - 对象结构型模式关心类与对象的组合,通过关联关系使得在一个类中定义另一个类的实例对象,然后通过该对象调用其方法。根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系,因此大部分结构型模式都是对象结构型模式。 - -![结构型模式](https://user-gold-cdn.xitu.io/2018/6/16/1640655459d766d2?w=378&h=266&f=png&s=59652) - -### 常见结构型模式详解 - -- **适配器模式:** - - [深入理解适配器模式——加个“适配器”以便于复用](https://segmentfault.com/a/1190000011856448) - - [适配器模式原理及实例介绍-IBM](https://www.ibm.com/developerworks/cn/java/j-lo-adapter-pattern/index.html) -- **桥接模式:** [设计模式笔记16:桥接模式(Bridge Pattern)](https://blog.csdn.net/yangzl2008/article/details/7670996) -- **组合模式:** [大话设计模式—组合模式](https://blog.csdn.net/lmb55/article/details/51039781) -- **装饰模式:** [java模式—装饰者模式](https://www.cnblogs.com/chenxing818/p/4705919.html)、[Java设计模式-装饰者模式](https://blog.csdn.net/cauchyweierstrass/article/details/48240147) -- **外观模式:** [java设计模式之外观模式(门面模式)](https://www.cnblogs.com/lthIU/p/5860607.html) -- **享元模式:** [享元模式](http://www.jasongj.com/design_pattern/flyweight/) -- **代理模式:** - - [代理模式原理及实例讲解 (IBM出品,很不错)](https://www.ibm.com/developerworks/cn/java/j-lo-proxy-pattern/index.html) - - [轻松学,Java 中的代理模式及动态代理](https://blog.csdn.net/briblue/article/details/73928350) - - [Java代理模式及其应用](https://blog.csdn.net/justloveyou_/article/details/74203025) - - -## 行为型模式 - -### 行为型模式概述 - -- 行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化。 -- 行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。 -- 通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象之间的交互。在系统运行时,对象并不是孤立的,它们可以通过相互通信与协作完成某些复杂功能,一个对象在运行时也将影响到其他对象的运行。 - -**行为型模式分为类行为型模式和对象行为型模式两种:** - -- **类行为型模式:** 类的行为型模式使用继承关系在几个类之间分配行为,类行为型模式主要通过多态等方式来分配父类与子类的职责。 -- **对象行为型模式:** 对象的行为型模式则使用对象的聚合关联关系来分配行为,对象行为型模式主要是通过对象关联等方式来分配两个或多个类的职责。根据“合成复用原则”,系统中要尽量使用关联关系来取代继承关系,因此大部分行为型设计模式都属于对象行为型设计模式。 - -![行为型模式](https://user-gold-cdn.xitu.io/2018/6/28/164467dd92c6172c?w=453&h=269&f=png&s=63270) - -- **职责链模式:** -- [Java设计模式之责任链模式、职责链模式](https://blog.csdn.net/jason0539/article/details/45091639) -- [责任链模式实现的三种方式](https://www.cnblogs.com/lizo/p/7503862.html) -- **命令模式:** 在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。 -- **解释器模式:** -- **迭代器模式:** -- **中介者模式:** -- **备忘录模式:** -- **观察者模式:** -- **状态模式:** -- **策略模式:** - -策略模式作为设计原则中开闭原则最典型的体现,也是经常使用的。下面这篇博客介绍了策略模式一般的组成部分和概念,并用了一个小demo去说明了策略模式的应用。 - -[java设计模式之策略模式](https://blog.csdn.net/zlj1217/article/details/81230077) - -- **模板方法模式:** -- **访问者模式:** - From 9528c62268111ead60aba8abf7e69af50bddb7ae Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Wed, 22 Apr 2020 09:31:54 +0800 Subject: [PATCH 03/38] update --- README.md | 76 +++++++++++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 07bde7a..b44b2eb 100644 --- a/README.md +++ b/README.md @@ -19,58 +19,58 @@ ### 目录(ctrl + f 查找更香) -- [项目准备](#----) -- [面试知识点](#-----) -- [公司面经](#----) +- [项目准备](#项目准备) +- [面试知识点](#面试知识点) +- [公司面经](#公司面经) - [Java](#java) - * [基础](#--) - * [容器(包括juc)](#-----juc-) - * [并发](#--) + * [基础](#基础) + * [容器(包括juc)](#容器包括juc) + * [并发](#并发) * [JVM](#jvm) * [Java8](#java8) -- [计算机网络](#-----) -- [计算机操作系统](#-------) +- [计算机网络](#计算机网络) +- [计算机操作系统](#计算机操作系统) - [Linux](#linux) -- [数据结构与算法](#-------) - * [数据结构](#----) - * [算法](#--) -- [数据库](#---) +- [数据结构与算法](#数据结构与算法) + * [数据结构](#数据结构) + * [算法](#算法) +- [数据库](#数据库) * [MySQL](#mysql) - + [mysql(优化思路)](#mysql------) -- [系统设计](#----) - * [秒杀系统相关](#------) - * [前后端分离](#-----) - * [单点登录](#----) - * [常用框架](#----) - + [Spring](#spring) + + [mysql(优化思路)](#mysql优化思路) +- [系统设计](#系统设计) + * [秒杀系统相关](#秒杀系统相关) + * [前后端分离](#前后端分离) + * [单点登录](#单点登录) + * [常用框架](#常用框架) + + [Spring](#Spring) + [SpringBoot](#springboot) -- [分布式](#---) +- [分布式](#分布式) * [dubbo](#dubbo) * [zookeeper](#zookeeper) * [RocketMQ](#rocketmq) * [RabbitMQ](#rabbitmq) * [kafka](#kafka) - * [消息中间件](#-----) + * [消息中间件](#消息中间件) * [redis](#redis) - * [分布式系统](#-----) -- [线上问题调优(虚拟机,tomcat)](#-----------tomcat-) -- [面试指南](#----) -- [工具](#--) + * [分布式系统](#分布式系统) +- [线上问题调优(虚拟机,tomcat)](#线上问题调优虚拟机,tomcat) +- [面试指南](#面试指南) +- [工具](#工具) * [Git](#git) * [Docker](#docker) -- [其他](#--) - * [权限控制(设计、shiro)](#--------shiro-) -- [Java学习资源](#--) -- [Java书籍推荐](#java----) -- [实战项目推荐](#------) -- [说明](#--) - * [JavaInterview介绍](#javainterview--) - * [关于转载](#----) - * [如何对该开源文档进行贡献](#------------) - * [为什么要做这个开源文档?](#------------) - * [投稿](#--) - * [联系我](#---) - * [公众号](#---) +- [其他](#其他) + * [权限控制(设计、shiro)](#权限控制设计、shiro) +- [Java学习资源](#Java学习资源) +- [Java书籍推荐](#Java书籍推荐) +- [实战项目推荐](#实战项目推荐) +- [说明](#说明) + * [JavaInterview介绍](#JavaInterview介绍) + * [关于转载](#关于转载) + * [如何对该开源文档进行贡献](#如何对该开源文档进行贡献) + * [为什么要做这个开源文档?](#为什么要做这个开源文档?) + * [投稿](#投稿) + * [联系我](#联系我) + * [公众号](#公众号) ## 项目准备 From 39989103ed85c9216bff1cbc7354f254f0bd6fbc Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Fri, 8 May 2020 17:38:17 +0800 Subject: [PATCH 04/38] update --- ...54\345\217\270\351\235\242\347\273\217.md" | 40 ++++++ ...70\350\247\201\347\237\245\350\257\206.md" | 90 -------------- ...06\347\261\273\346\261\207\346\200\273.md" | 117 ++++++++---------- ...54\345\217\270\346\203\205\345\206\265.md" | 6 +- ...71\347\233\256\344\273\213\347\273\215.md" | 22 ---- 5 files changed, 92 insertions(+), 183 deletions(-) delete mode 100644 "docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\347\237\245\350\257\206.md" diff --git "a/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" "b/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" index 03ebf3e..b45e662 100644 --- "a/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" +++ "b/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" @@ -1,3 +1,23 @@ +# 京东 + +### 京东一面 + +1、Java中的乐观锁悲观锁 + +https://segmentfault.com/a/1190000016611415 + +2、单点登录 +3、集合的问题 +4、反转链表 +5、二分查找 +6、堆排序 +7、JUC +8、Java中如何实现线程安全 +9、ArrayList和linkedList区别 +10、数组和链表区别 +11、volitale + + # 阿里 - [阿里社招四面(分布式)](https://www.nowcoder.com/discuss/349542) @@ -19,6 +39,18 @@ 8、maven:如何查找重复的jar包问题 9、单点登录如何做的,如果保证安全性 +**缺点**:linux实战的不懂 + +### 钉钉二面 + +1、项目有什么难点:应该不管是什么项目都是数据库优化跟JVM问题排查 +2、操作系统内存交换方式 +3、tcp7层模型 +4、平时学习方法 +5、自己博客写的好的进行介绍 + +**缺点**:重点不突出、操作系统不清楚。 + # 腾讯 @@ -133,6 +165,14 @@ https://www.cnblogs.com/JackPn/p/9386182.html # 快手 +1、项目 +2、两数之和等于target +3、http介绍 +4、输入网址的过程 +5、dubbo原理 +6、http和dubbo协议的区别 + +**缺点**:写算法还不熟练 #### Tip:本来有很多我准备的资料的,但是都是外链,或者不合适的分享方式,所以大家去公众号回复【资料】好了。 diff --git "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\347\237\245\350\257\206.md" "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\347\237\245\350\257\206.md" deleted file mode 100644 index d5c41a0..0000000 --- "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\347\237\245\350\257\206.md" +++ /dev/null @@ -1,90 +0,0 @@ -# 阿里 - -linux进程结构 -linux文件系统 -linux统计文件数量 -spingioc,springaop中的cglib动态 -shell脚本 -webLogic知道不? 与tomcat区别? -webservice -HashMap链表转红黑树是怎么实现的 -进程通信 -散列表(哈希表) -Java中pv操作实现时用的类 -说一下TCP/IP的状态转移 -说一下NIO -线程通信 -写过异步的代码吗 -拜占庭算法的理解? -红黑树么,在插入上有什么优化? -resetful风格 -分布式rpc调度过程中要注意的问题 -hashmap并发读写死循环问题 -快排时间复杂度?最好什么情况,最坏什么情况?有什么改进方案? -HashMap get和put源码,为什么红黑而非平衡树? -分布式系统CAP理论,重点解释分区容错性的意义 -对虚拟内存的理解 -三个线程如何实现交替打印ABC -线程池中LinkedBlockingQueue满了的话,线程会怎么样 -HashMap和ConcurrentHashMap哪个效率更高?为什么? -mybatis如何进行类型转换 -myql间歇锁的实现原理 -future的底层实现异步原理 -rpc原理 -多个服务端上下线怎么感知 -降级处理hystrix了解过么 -redis的热点key问题 -一致性哈希 -ClassLoader原理和应用 -注解的原理 - - - -# 腾讯 - - - -# 百度 - - - -# 滴滴 - - - -# 头条 - - - -# 拼多多 - - - -# 美团 - - - -# 小米 - - - -# 网易 - - - -# 华为 - - -#### Tip:本来有很多我准备的资料的,但是都是外链,或者不合适的分享方式,所以大家去公众号回复【资料】好了。 - -![](http://image.ouyangsihai.cn/FszE5cIon6eHHexBEgOSBGBWeoyP) - -现在免费送给大家,在我的公众号 **好好学java** 回复 **资料** 即可获取。 - -有收获?希望老铁们来个三连击,给更多的人看到这篇文章 - -1、老铁们,关注我的原创微信公众号「**好好学java**」,专注于Java、数据结构和算法、微服务、中间件等技术分享,保证你看完有所收获。 - -![](http://image.ouyangsihai.cn/FgUUPlQOlQtjbbdOs1RZK9gWxitV) - -2、给俺一个 **star** 呗,可以让更多的人看到这篇文章,顺便激励下我继续写作,嘻嘻。 \ No newline at end of file diff --git "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" index 7857062..4e06c95 100644 --- "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" +++ "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" @@ -1,5 +1,9 @@ # Java基础 +#### 创建对象的方式 + +- https://blog.csdn.net/w410589502/article/details/56489294 + #### 反射在jvm层面的实现 https://www.jianshu.com/p/b6cb4c694951 @@ -16,6 +20,10 @@ https://blog.csdn.net/cnq2328/article/details/50436175 https://www.jianshu.com/p/10584345b10a +#### oom出现的原因 + +https://www.jianshu.com/p/2fdee831ed03 + #### Class.forName和ClassLoader的区别 https://blog.csdn.net/qq_27093465/article/details/52262340 @@ -50,6 +58,8 @@ https://blog.csdn.net/xlgen157387/article/details/63683882 #### 红黑树 + +- https://www.jianshu.com/p/e136ec79235c - https://zhuanlan.zhihu.com/p/31805309 #### hashmap的jdk1.7和jdk1.8区别 @@ -109,10 +119,22 @@ https://www.cnblogs.com/peizhe123/p/5790252.html 更加详细的解释 https://blog.csdn.net/yanshuanche3765/article/details/78917507 +#### ArrayList、LinkedList、Vector + +https://blog.csdn.net/zhangqiluGrubby/article/details/72870493 + +#### sleep和wait的区别 + +https://blog.csdn.net/u012050154/article/details/50903326 + #### java 地址和值传递的例子 https://www.cnblogs.com/zhangyu317/p/11226105.html +#### Java序列化 + +https://juejin.im/post/5ce3cdc8e51d45777b1a3cdf + #### java NIO,java 多线程、线程池,java 网络编程解决并发量 - Java Nio使用:https://blog.csdn.net/forezp/article/details/88414741 @@ -237,8 +259,11 @@ HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分 #### 讲讲多线程,多线程的同步方法 -1、synchronized -2、reetrantlock +- synchronized原理 + +https://www.jianshu.com/p/d53bf830fa09 + +- reetrantlock #### list,map,set 之间的区别 @@ -405,6 +430,14 @@ https://www.cnblogs.com/kismetv/p/7806063.html https://mp.weixin.qq.com/s/2Y5X11TycreHgO0R3agK2A https://mp.weixin.qq.com/s/IdjCxumDleLqdU8MgQnrLQ +#### spring中的设计模式 + +https://juejin.im/post/5ce69379e51d455d877e0ca0 + +#### aspect的种类 + +https://blog.csdn.net/StubbornAccepted/article/details/70767014 + #### ioc aop总结(概述性) https://juejin.im/post/5b040cf66fb9a07ab7748c8b @@ -523,6 +556,10 @@ https://blog.csdn.net/itmyhome1990/article/details/54863822 ### Redis +#### redis为什么快? + +https://zhuanlan.zhihu.com/p/57089960 + - Redis 数据结构 - Redis 持久化机制 @@ -759,6 +796,10 @@ https://cloud.tencent.com/developer/article/1104098 ### 计算机网络 +#### http相关问题 + +https://mp.weixin.qq.com/s/xSGD3rWdboeeQvaS8jEchw + #### TCP三次握手第三次握手时ACK丢失怎么办 https://www.cnblogs.com/wuyepeng/p/9801470.html @@ -875,76 +916,16 @@ https://juejin.im/post/5da44c5de51d45783a772a22 - https://blog.csdn.net/wjw_77/article/details/99696757 -### 项目及规划 - -1. 对你来说影响最大的一个项目(该面试中有关项目问题都针对该项目展开)? - -2. 项目哪一部分最难攻克?如何攻克? - -个人建议:大家一定要选自己印象最深的项目回答,首先按模块,然后组成 人员,最后你在项目中的角色和发挥 的作用。全程组织好语言,最好不要有停顿,面试官可以 看出你对项目的熟悉程度 - -3. 你觉得你在项目运行过程中作为组长是否最大限度发挥了组员的优势?具体事例? - -4. 职业规划,今天想发展的工作方向 - -5. 项目里我遇到过的最大的困难是什么 - -6. 实验室的新来的研一,你会给他们什么学习上的建议,例如对于内核源码的枯 燥如何克服 - -7. 如何协调团队中多人的工作 - -8. 当团队中有某人的任务没有完成的很好,如何处理 - -9. 平时看些什么书,技术 综合 - -10. 项目解决的什么问题 用到了哪些技术 - -11. 怎么预防 bug 日志 jvm 异常信息 如何找问题的根源(统计表格) - -12. 你是怎么学习的,说完会让举个例子 - -13. 实习投了哪几个公司?为什么,原因 - -14. 最得意的项目是什么?为什么?(回答因为项目对实际作用大,并得到认可) - -15. 最得意的项目内容,讲了会 - -16. 你简历上写的是最想去的部门不是我们部门,来我们部门的话对你有影响麽? - -17. 你除了在学校还有哪些方式去获取知识和技术? - -18. 你了解阿里文化和阿里开源吗? - -19. 遇到困难解决问题的思路? - -20. 我觉得最成功的一件事了 - -我说能说几件吗,说了我大学明白明白了 自己想干什么,选择了自己喜欢的事,大学里学会了和自己相处,自己一个人的 时候也不会感觉无聊,精神世界比较丰富,坚持锻炼,健身,有个很不错的身体, 然后顿了顿笑着说,说,有一个对我很好的女朋友算吗? - -21. 压力大的时候怎么调整?多个任务冲突了你怎么协调的? - -22. 家里有几个孩子,父母对你来北京有什么看法? - -23. 职业生涯规划 - -24. 你在什么情况下可能会离职 - -25. 对你影响最大的人 - -26. 1. 优点 3 个,以及缺点 2. 说说你应聘这个岗位的优势 3. 说说家庭 4. 为什么 想来网易,用过网易的哪些产品,对比下有什么好的地方 5. 投递了哪些公司,对第一份工 作怎么看待 -27. 为什么要选择互联网(楼主偏底层的) +#### 负载均衡 -28. 为什么来网易(看你如何夸) +https://juejin.im/post/5b39eea0e51d4558c1010e36 -29. 在校期间怎样学习 -30. 经常逛的技术性网站有哪些? +#### 分布式锁的实现方式及优缺点 -31. 举出你在开发过程中遇到的原先不知道的 bug, 通过各种方式定位 bug 并最终 成功解决的例子 +https://zhuanlan.zhihu.com/p/62158041 -32. 举出一个例子说明你的自学能力 7 次面试记录,除了京东基本上也都走到了很后面的阶段。硬要说经验可能有三点: +#### CAP -- 不会就不会。我比较爽快,如果遇到的不会的甚至是不确定的,都直接说:“对不起, 我答不上来”之类的。 -- 一技之长。中间件和架构相关的实习经历,让我基本上和面试官都可以聊的很多, 也可以看到,我整个过程没有多少算法题。是因为面试官和你聊完项目就知道你能 做事了。其实,面试官很不愿意出算法题的(BAT 那个档次除外),你能和他扯技 术他当然高兴了。关键很多人只会算法(逃)。 -- 基础非常重要。面试官只要问 Java 相关的基础,我都有自信让一般的面试官感觉 惊讶,甚至学到新知识 \ No newline at end of file +https://www.jianshu.com/p/8025e3346734 \ No newline at end of file diff --git "a/docs/interview/\345\267\262\346\212\225\345\205\254\345\217\270\346\203\205\345\206\265.md" "b/docs/interview/\345\267\262\346\212\225\345\205\254\345\217\270\346\203\205\345\206\265.md" index c1bb8fe..3ca1b9c 100644 --- "a/docs/interview/\345\267\262\346\212\225\345\205\254\345\217\270\346\203\205\345\206\265.md" +++ "b/docs/interview/\345\267\262\346\212\225\345\205\254\345\217\270\346\203\205\345\206\265.md" @@ -5,7 +5,7 @@ |蘑菇街|http://job.mogujie.com/#/candidate/perfectInfo | | offer | |虎牙| | | 不匹配 | |远景|https://campus.envisioncn.com/ | | 笔试 | -|阿里钉钉| https://campus.alibaba.com/myJobApply.htm?saveResume=yes&t=1584782560963 |https://www.nowcoder.com/discuss/368915?type=0&order=0&pos=25&page=3| 一面 | +|阿里钉钉| https://campus.alibaba.com/myJobApply.htm?saveResume=yes&t=1584782560963 |https://www.nowcoder.com/discuss/368915?type=0&order=0&pos=25&page=3| 二面 | |阿里新零售| |https://www.nowcoder.com/discuss/374171?type=0&order=0&pos=35&page=1 https://www.nowcoder.com/discuss/372118?type=0&order=0&pos=80&page=2| | |深信服| |https://www.nowcoder.com/discuss/369399?type=0&order=0&pos=40&page=6| | |CVTE| |https://www.nowcoder.com/discuss/368463?type=0&order=0&pos=87&page=3| 已投 | @@ -15,13 +15,13 @@ |拼多多| https://pinduoduo.zhiye.com/Portal/Apply/Index | https://www.nowcoder.com/discuss/393350?type=post&order=time&pos=&page=8 | 已投 | |腾讯| https://join.qq.com/center.php |https://www.nowcoder.com/discuss/377813?type=post&order=time&pos=&page=1| offer | |猿辅导| https://app.mokahr.com/m/candidate/applications/deliver-query/fenbi |https://www.nowcoder.com/discuss/375610?type=0&order=0&pos=95&page=2| 已投 | -|斗鱼| https://app.mokahr.com/m/candidate/applications/deliver-query/douyu |https://www.nowcoder.com/discuss/375180?type=0&order=0&pos=158&page=1| 笔试 | +|斗鱼| https://app.mokahr.com/m/candidate/applications/deliver-query/douyu |https://www.nowcoder.com/discuss/375180?type=0&order=0&pos=158&page=1| 一面挂 | |淘宝技术部| |https://www.nowcoder.com/discuss/374655?type=0&order=0&pos=165&page=6| | |字节跳动| https://job.bytedance.com/user https://job.bytedance.com/referral/pc/position/application?lightning=1&token=MzsxNTg0MTU2NDIxMDIzOzY2ODgyMjg1NzI1Mjk3MjI4ODM7MA/profile/ |https://www.nowcoder.com/discuss/381888?type=post&order=time&pos=&page=2| 已投 | |陌陌| | 来自内推军 |已投| |网易| http://gzgame.campus.163.com/applyPosition.do?&lan=zh |https://www.nowcoder.com/discuss/373132?type=post&order=create&pos=&page=1 一姐| 已投| |百度| https://talent.baidu.com/external/baidu/index.html#/individualCenter |https://www.nowcoder.com/discuss/376515?type=post&order=time&pos=&page=1| 已投 | -|京东| http://campus.jd.com/web/resume/resume_index?fxType=0 |https://www.nowcoder.com/discuss/372978?type=post&order=time&pos=&page=4| 已投 | +|京东| http://campus.jd.com/web/resume/resume_index?fxType=0 |https://www.nowcoder.com/discuss/372978?type=post&order=time&pos=&page=4| 二面挂 | |爱奇艺| | | | |科大讯飞| | | | |度小满| | https://www.nowcoder.com/discuss/387950?type=post&order=time&pos=&page=13 | 已投 | \ No newline at end of file diff --git "a/docs/interview/\350\207\252\346\210\221\344\273\213\347\273\215\345\222\214\351\241\271\347\233\256\344\273\213\347\273\215.md" "b/docs/interview/\350\207\252\346\210\221\344\273\213\347\273\215\345\222\214\351\241\271\347\233\256\344\273\213\347\273\215.md" index 374650b..8391e79 100644 --- "a/docs/interview/\350\207\252\346\210\221\344\273\213\347\273\215\345\222\214\351\241\271\347\233\256\344\273\213\347\273\215.md" +++ "b/docs/interview/\350\207\252\346\210\221\344\273\213\347\273\215\345\222\214\351\241\271\347\233\256\344\273\213\347\273\215.md" @@ -35,28 +35,6 @@ 4、对数据库性能进行调优 5、开发抢购活动功能模块(业务需求) -### 北京项目介绍 - -这个项目的背景是,现在深空探索的研究越来越热,这个项目就是研究及开发脉冲星导航的相关问题,而脉冲星导航的能够解决深空探索航天器的位置问题,这个就是北京项目的研究目标。 - -脉冲星是一颗稳定的中子星,能够发出稳定的脉冲波,通过接收脉冲星发出来的脉冲波,可以确定航天器在太空中的位置。 - -而我在北京的工作是: -1、开发一个软件 -2、实现相关算法对航天器上的部件精度进行控制 -3、航天器相关的数据进行数据显示 -4、近地卫星轨道5星编队仿真实现 - - -2019年5月–2019年9月,在####实习,参加了脉冲星导航的项目开发工作。项目的背景是,现在深空探索的研究越来越热,这个项目就是研究及开发脉冲星导航的相关问题,而脉冲星导航的能够解决深空探索航天器的位置问题,这个就是项目的研究目标。 -负责工作: -1、开发一个脉冲星导航仿真软件 -2、实现相关算法对航天器上的部件精度进行控制 -3、航天器相关的数据进行数据显示 -4、近地卫星轨道5星编队仿真实现 -个人收获: -通过这个项目的开发,学习到了许多以前没有接触到的航天知识,对于这方面的知识,个人有了很大的提高,同时,由于时间关系,开发的时间非常紧张,对于自己的有了很大的锻炼,不管是从抗压能力还是学习能力,都是得到了一定得锻炼的。 - #### Tip:本来有很多我准备的资料的,但是都是外链,或者不合适的分享方式,所以大家去公众号回复【资料】好了。 ![](http://image.ouyangsihai.cn/FszE5cIon6eHHexBEgOSBGBWeoyP) From 28a0c5cfdc0012eb148f68cdae0ed84d0a1e0e7e Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Sat, 23 May 2020 10:31:42 +0800 Subject: [PATCH 05/38] update readme --- README.md | 26 +++++++++++++++---- ...06\347\261\273\346\261\207\346\200\273.md" | 5 ++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b44b2eb..701debf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 是本人在备战春招及这几年学习的知识沉淀,这里面有很多都是自己的原创文章,同时,也有很多是本在备战春招的过程中觉得对面试特别有帮助的文章,**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 不一定可以帮助你进入到 BAT 等大厂,但是,如果你认真研究,仔细思考,我相信你也可以跟我一样幸运的进入到腾讯等大厂。 +**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 是本人在备战春招及这几年学习的知识沉淀,这里面有很多都是自己的原创文章,同时,也有很多是本在备战春招的过程中觉得对面试特别有帮助的文章,**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 不一定可以帮助你进入到 BAT 等大厂,但是,如果你认真研究,仔细思考,我相信你也可以跟我一样幸运的进入到大厂。 -本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,春招目前拿到了腾讯等大厂offer。 +本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,春招目前拿到了大厂offer。 如果觉得有帮助,给个 **star** 好不好,哈哈(目前还不是很完善,后面会一一补充)。 @@ -8,6 +8,10 @@ **一起冲!!!** +如果大家想要实时关注我更新的文章以及分享的干货的话,请及时关注我的公众号 **程序员的技术圈子**。 + +![](http://image.ouyangsihai.cn/FlL0VJf1Q4gCfrc8RhL-SL-xiiXo) +
@@ -133,7 +137,8 @@ ### 算法 - [2020年最新算法面试真题汇总](docs/dataStructures-algorithms/算法面试真题汇总.md) -- [2020年最新算法题型难点总结](docs/dataStructures-algorithms/算法题目难点题目总结.md) +- [2020年最新算法题型难点总结](docs/dataStructures-algorithms/算法题目难点题目总结.md) + ## 数据库 @@ -284,6 +289,7 @@ ## Java书籍推荐 + ## 实战项目推荐 >小心翼翼的告诉你,上面的资源当中就有很多**企业级项目**,没有项目一点不用怕,因为你看到了这个。 @@ -291,6 +297,15 @@ - [找工作,没有上的了台面的项目怎么办?](https://mp.weixin.qq.com/s/0oK43_z99pVY9dYVXyIeiw) +## 程序人生 + +- [我想是时候跟大学告别了](https://blog.ouyangsihai.cn/wo-xiang-shi-shi-hou-he-da-xue-gao-bie-liao.html) +- [坚持,这两个字非常重要!](https://blog.ouyangsihai.cn/jian-chi-zhe-liang-ge-zi-fei-chang-chong-yao.html) +- [2018年年终总结,你的呢?](https://blog.ouyangsihai.cn/zhe-shi-wo-de-2018-nian-zhong-zong-jie-ni-de-ni.html) +- [多去了解了解自己](https://blog.ouyangsihai.cn/duo-wen-wen-zi-ji-xiang-cheng-wei-shi-me-yang-de-ren.html) +- [关于考研,这是我给大家的经验](https://blog.ouyangsihai.cn/guan-yu-zhe-jian-shi-wo-you-hua-yao-shuo.html) +- [从普通二本到研究生再到自媒体的年轻人,这是我的故事](https://blog.ouyangsihai.cn/cong-pu-ben-dao-zha-shuo-cong-da-xue-sheng-dao-zi-mei-ti-de-nian-qing-ren-wo-fen-xiang-wo-de-coding-sheng-huo.html) + ## 说明 ### JavaInterview介绍 @@ -324,7 +339,8 @@ ### 公众号 -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 +如果大家想要实时关注我更新的文章以及分享的干货的话,关注我的公众号 **程序员的技术圈子**。 + +![](http://image.ouyangsihai.cn/FlL0VJf1Q4gCfrc8RhL-SL-xiiXo) -![](http://image.ouyangsihai.cn/FgUUPlQOlQtjbbdOs1RZK9gWxitV) diff --git "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" index 4e06c95..29465ef 100644 --- "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" +++ "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" @@ -56,6 +56,11 @@ https://blog.csdn.net/xlgen157387/article/details/63683882 - https://segmentfault.com/a/1190000011291179 +#### 二叉树相关 + +- https://www.jianshu.com/p/655d83f9ba7b +- https://www.jianshu.com/p/ff4b93b088eb + #### 红黑树 From 0bd8d4c41b8019521a023343dc32c3c0a2a0a2c8 Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Sat, 23 May 2020 11:10:07 +0800 Subject: [PATCH 06/38] update readme --- README.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 701debf..fc37677 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ **一起冲!!!** -如果大家想要实时关注我更新的文章以及分享的干货的话,请及时关注我的公众号 **程序员的技术圈子**。 +如果大家想要实时关注我更新的文章以及分享的干货的话,请及时关注我的公众号 **程序员的技术圈子**,微信扫描下面二维码,绿色通道关注福利,等你拿。 ![](http://image.ouyangsihai.cn/FlL0VJf1Q4gCfrc8RhL-SL-xiiXo) @@ -21,7 +21,7 @@ [![微信群](https://camo.githubusercontent.com/59d7f19ba1af85247e016858a63045f8fe9a8c19/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7765436861742de5beaee4bfa1e7bea42d626c75652e737667)](https://github.com/OUYANGSIHAI/JavaInterview#%E8%81%94%E7%B3%BB%E6%88%91) [![公众号](https://img.shields.io/badge/%E5%85%AC%E4%BC%97%E5%8F%B7-%E5%A5%BD%E5%A5%BD%E5%AD%A6java-orange)](https://github.com/OUYANGSIHAI/JavaInterview#%E5%85%AC%E4%BC%97%E5%8F%B7) [![公众号](https://camo.githubusercontent.com/6d206aa03f27a851cf994123ef7be1a8d3192d54/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6a75656a696e2de68e98e987912d626c75652e737667)](https://juejin.im/user/5a672822f265da3e55380f0b) [![投稿](https://camo.githubusercontent.com/85a04ac4953a80940570b5c86ce73a1d34ff1542/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6373646e2d4353444e2d7265642e737667)](https://blog.csdn.net/sihai12345) [![投稿](https://camo.githubusercontent.com/6efc9c83ef8e85b19ce2853b5f69d68255f0c037/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f62696c6962696c692de59394e593a9e59394e593a92d637269746963616c)](https://space.bilibili.com/441147490) -### 目录(ctrl + f 查找更香) +### 目录(ctrl + f 查找更香:不能点击的,还在写) - [项目准备](#项目准备) - [面试知识点](#面试知识点) @@ -67,6 +67,7 @@ - [Java学习资源](#Java学习资源) - [Java书籍推荐](#Java书籍推荐) - [实战项目推荐](#实战项目推荐) +- [程序人生](#程序人生) - [说明](#说明) * [JavaInterview介绍](#JavaInterview介绍) * [关于转载](#关于转载) @@ -101,25 +102,59 @@ ### 容器(包括juc) +#### 基础容器 + +- ArrayList源码分析及真实大厂面试题精讲 +- LinkedList源码分析及真实大厂面试题精讲 +- HashMap源码分析及真实大厂面试题精讲 +- TreeMap源码分析及真实大厂面试题精讲 +- TreeSet源码分析及真实大厂面试题精讲 +- LinkedHashMap源码分析及真实大厂面试题精讲 + +#### 阻塞容器 + +- ConcurrentHashMap源码分析及真实大厂面试题精讲 +- ArrayBlockingQueue源码分析及真实大厂面试题精讲 +- LinkedBlockingQueue源码分析及真实大厂面试题精讲 +- PriorityBlockingQueue源码分析及真实大厂面试题精讲 + ### 并发 +- Synchronized关键字精讲及真实大厂面试题解析 +- Volitale关键字精讲及真实大厂面试题解析 +- 关于LRU的实现 +- ThreadLocal面试中会怎么提问呢? +- 线程池的面试题,这篇文章帮你搞定它! ### JVM - [深入理解Java虚拟机系列](https://mp.weixin.qq.com/s/SZ87s3fmKL3Kc_tAMcOFQw) - [深入理解Java虚拟机系列--完全解决面试问题](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-xi-lie-jiao-cheng.html) +- [深入理解Java虚拟机-Java内存区域透彻分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-java-nei-cun-qu-yu-tou-che-fen-xi.html) +- [深入理解Java虚拟机-JVM内存分配与回收策略原理,从此告别JVM内存分配文盲](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-jvm-nei-cun-fen-pei-yu-hui-shou-ce-lue-yuan-li-cong-ci-gao-bie-jvm-nei-cun-fen-pei-wen-mang.html) +- [深入理解Java虚拟机-常用vm参数分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-yong-vm-can-shu-fen-xi.html) +- [深入理解Java虚拟机-如何利用JDK自带的命令行工具监控上百万的高并发的虚拟机性能](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-jdk-zi-dai-de-ming-ling-xing-gong-ju-jian-kong-shang-bai-wan-de-gao-bing-fa-de-xu-ni-ji-xing-neng.html) +- [深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html) +- [深入理解Java虚拟机-你了解GC算法原理吗](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ni-liao-jie-gc-suan-fa-yuan-li-ma.html) +- [几个面试官常问的垃圾回收器,下次面试就拿这篇文章怼回去!](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-jian-de-la-ji-hui-shou-qi.html) +- [面试官100%会严刑拷打的 CMS 垃圾回收器,下次面试就拿这篇文章怼回去!](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-cms-la-ji-hui-shou-qi.html) ### Java8 - [Java8快速学习教程](https://blog.ouyangsihai.cn/java8-zui-xin-jiao-cheng-bu-yi-yang-de-java8.html) - [Java11的最新特性](https://blog.ouyangsihai.cn/java11-zheng-shi-fa-bu-liao-wo-men-gai-zen-me-ban.html) +- [Java8 之 lambda 表达式、方法引用、函数式接口、默认方式、静态方法](https://blog.ouyangsihai.cn/java8-zhi-lambda-biao-da-shi-fang-fa-yin-yong-han-shu-shi-jie-kou-mo-ren-fang-shi-jing-tai-fang-fa.html) +- [Java8之Consumer、Supplier、Predicate和Function攻略](https://blog.ouyangsihai.cn/java8-zhi-consumer-supplier-predicate-he-function-gong-lue.html) +- [Java8 的 Stream 流式操作之王者归来](https://blog.ouyangsihai.cn/java8-de-stream-liu-shi-cao-zuo-zhi-wang-zhe-gui-lai.html) ## 计算机网络 - [http面试问题全解析](docs/network/http面试问题全解析.md) +- 关于tcp、udp网络模型的问题,这篇文章告诉你 +- http、https还不了解,别慌! ## 计算机操作系统 @@ -128,25 +163,45 @@ ## Linux - [java工程师linux命令,这篇文章就够了](https://blog.ouyangsihai.cn/java-gong-cheng-shi-linux-ming-ling-zhe-pian-wen-zhang-jiu-gou-liao.html) +- 常问的几个Linux面试题,通通解决它 ## 数据结构与算法 ### 数据结构 +- 跳表这种数据结构,你真的清楚吗,面试官可能会问这些问题! +- 红黑树你了解多少,不会肯定会被面试官怼坏 +- [B树,B+树,你了解多少,面试官问那些问题?](https://blog.ouyangsihai.cn/mian-shi-guan-wen-ni-b-shu-he-b-shu-jiu-ba-zhe-pian-wen-zhang-diu-gei-ta.html) ### 算法 - [2020年最新算法面试真题汇总](docs/dataStructures-algorithms/算法面试真题汇总.md) - [2020年最新算法题型难点总结](docs/dataStructures-algorithms/算法题目难点题目总结.md) - +- 关于贪心算法的leetcode题目,这篇文章可以帮你解决80% +- 回溯算法不会,这篇文章一定得看 +- 动态规划你了解多少,我来帮你入个们 +- 链表的题目真的不难,看了这篇文章你就知道有多简单了 +- 还在怕二叉树的题目吗? += 栈和队列的题目可以这样出题型,你掌握了吗 +- 数组中常用的几种leetcode解题技巧! ## 数据库 ### MySQL - [MySQL深入理解教程-解决面试中的各种问题](https://blog.ouyangsihai.cn/mysql-shen-ru-li-jie-jiao-cheng-mysql-de-yi-zhu-shi-jie.html) - -#### mysql(优化思路) +- [InnoDB与MyISAM等存储引擎对比](https://blog.ouyangsihai.cn/innodb-yu-myisam-deng-cun-chu-yin-qing-dui-bi.html) +- [ 面试官问你B树和B+树,就把这篇文章丢给他](https://blog.ouyangsihai.cn/mian-shi-guan-wen-ni-b-shu-he-b-shu-jiu-ba-zhe-pian-wen-zhang-diu-gei-ta.html) +- [MySQL的B+树索引的概念、使用、优化及使用场景](https://blog.ouyangsihai.cn/mysql-de-b-shu-suo-yin.html) +- [ MySQL全文索引最强教程](https://blog.ouyangsihai.cn/mysql-quan-wen-suo-yin.html) +- [ MySQL的又一神器-锁,MySQL面试必备](https://blog.ouyangsihai.cn/mysql-de-you-yi-shen-qi-suo.html) +- [ MySQL事务,这篇文章就够了](https://blog.ouyangsihai.cn/mysql-shi-wu-zhe-pian-wen-zhang-jiu-gou-liao.html) +- [ mysqldump工具命令参数大全](https://blog.ouyangsihai.cn/mysqldump-gong-ju-ming-ling-can-shu-da-quan.html) +- [看完这篇MySQL备份的文章,再也不用担心删库跑路了](https://blog.ouyangsihai.cn/kan-wan-zhe-pian-mysql-bei-fen-de-wen-zhang-zai-ye-bu-yong-dan-xin-shan-ku-pao-lu-liao.html) +- 关于MySQL索引,面试中面试官会怎么为难你,一定得注意 +- MySQL中的乐观锁、悲观锁,JDK中的乐观锁、悲观锁? + +#### MySQL(优化思路) - [MySQL高频面试题](https://mp.weixin.qq.com/s/KFCkvfF84l6Eu43CH_TmXA) - [MySQL查询优化过程](https://mp.weixin.qq.com/s/jtuLb8uAIHJNvNpwcIZfpA) @@ -180,14 +235,14 @@ #### SpringBoot -- [springboot史上最全教程](https://blog.csdn.net/sihai12345/category_7779682.html) +- [springboot史上最全教程,11篇文章全解析](https://blog.ouyangsihai.cn/categories/springboot2-0%E6%9C%80%E6%96%B0%E6%95%99%E7%A8%8B/) - [微服务面试相关资料](docs/microservice/微服务相关资料.md) ## 分布式 ### dubbo -- [dubbo教程](https://blog.ouyangsihai.cn/dubbo-yi-pian-wen-zhang-jiu-gou-liao-dubbo-yu-dao-chu-lian.html) +- [dubbo入门实战教程,这篇文章真的再好不过了](https://blog.ouyangsihai.cn/dubbo-yi-pian-wen-zhang-jiu-gou-liao-dubbo-yu-dao-chu-lian.html) - [dubbo源码分析](http://cmsblogs.com/?p=5324) - [dubbo面试题](https://mp.weixin.qq.com/s/PdWRHgm83XwPYP08KnkIsw) - [dubbo面试题2](https://mp.weixin.qq.com/s/Kz0s9K3J9Lpvh37oP_CtCA) From 8cc08169aeb2e0b1b3e426dc821dd234068bdd0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AC=A7=E9=98=B3=E6=80=9D=E6=B5=B7?= <1446037005@qq.com> Date: Sat, 23 May 2020 16:23:14 +0800 Subject: [PATCH 07/38] Create CNAME --- CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 CNAME diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..19f244e --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +www.ouyangsihai.cn \ No newline at end of file From a295ee452f4faade51050401eda2a60955aef5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AC=A7=E9=98=B3=E6=80=9D=E6=B5=B7?= <1446037005@qq.com> Date: Sat, 23 May 2020 16:28:27 +0800 Subject: [PATCH 08/38] Update CNAME --- CNAME | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CNAME b/CNAME index 19f244e..6efb3fd 100644 --- a/CNAME +++ b/CNAME @@ -1 +1 @@ -www.ouyangsihai.cn \ No newline at end of file +github.ouyangsihai.cn \ No newline at end of file From cea9fdf29ac28164f05c99d7641f6f4953ed644a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AC=A7=E9=98=B3=E6=80=9D=E6=B5=B7?= <1446037005@qq.com> Date: Sat, 23 May 2020 16:37:21 +0800 Subject: [PATCH 09/38] Set theme jekyll-theme-cayman --- _config.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 _config.yml diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..c419263 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file From 33245906c7ce97410b8d00a63ec5c4a5cc211026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AC=A7=E9=98=B3=E6=80=9D=E6=B5=B7?= <1446037005@qq.com> Date: Sat, 23 May 2020 16:45:04 +0800 Subject: [PATCH 10/38] Set theme jekyll-theme-dinky --- _config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_config.yml b/_config.yml index c419263..9da9a02 100644 --- a/_config.yml +++ b/_config.yml @@ -1 +1 @@ -theme: jekyll-theme-cayman \ No newline at end of file +theme: jekyll-theme-dinky \ No newline at end of file From a370549308b0f3031dd6a54d3c55cb3e90026ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AC=A7=E9=98=B3=E6=80=9D=E6=B5=B7?= <1446037005@qq.com> Date: Sat, 23 May 2020 16:51:44 +0800 Subject: [PATCH 11/38] Set theme jekyll-theme-hacker --- _config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_config.yml b/_config.yml index 9da9a02..fc24e7a 100644 --- a/_config.yml +++ b/_config.yml @@ -1 +1 @@ -theme: jekyll-theme-dinky \ No newline at end of file +theme: jekyll-theme-hacker \ No newline at end of file From 6751fd79f7c690b0418c148822dbbc44b19ca821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AC=A7=E9=98=B3=E6=80=9D=E6=B5=B7?= <1446037005@qq.com> Date: Sat, 23 May 2020 16:56:02 +0800 Subject: [PATCH 12/38] Set theme jekyll-theme-cayman --- _config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_config.yml b/_config.yml index fc24e7a..c419263 100644 --- a/_config.yml +++ b/_config.yml @@ -1 +1 @@ -theme: jekyll-theme-hacker \ No newline at end of file +theme: jekyll-theme-cayman \ No newline at end of file From 720a6a70dbea2082d3928f1518a73cc79371a250 Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Tue, 9 Jun 2020 23:09:36 +0800 Subject: [PATCH 13/38] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Java=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E6=80=9D=E7=BB=B4=E5=AF=BC=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fc37677..d5d8684 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,15 @@ **一起冲!!!** -如果大家想要实时关注我更新的文章以及分享的干货的话,请及时关注我的公众号 **程序员的技术圈子**,微信扫描下面二维码,绿色通道关注福利,等你拿。 +#### Java面试思维导图预警 + +这里再分享一些我总结的**Java面试思维导图**,我靠这些导图拿到了一线互联网公司的offer,先来瞧瞧。 + +![](http://image.ouyangsihai.cn/FtJ3PbBRdNSa1NaUr96JmUq24_yS) +![](http://image.ouyangsihai.cn/FsTv2aMsGcv8M9Rsyx6POBOGKY1g) + + +**划重点**:想要`导图源文件`,更多`Java面试思维导图`,请关注我的公众号 **程序员的技术圈子**,`微信扫描下面二维码`,回复:**思维导图**,获取思维导图,绿色通道关注福利,等你拿。 ![](http://image.ouyangsihai.cn/FlL0VJf1Q4gCfrc8RhL-SL-xiiXo) @@ -177,7 +185,7 @@ - [2020年最新算法面试真题汇总](docs/dataStructures-algorithms/算法面试真题汇总.md) - [2020年最新算法题型难点总结](docs/dataStructures-algorithms/算法题目难点题目总结.md) -- 关于贪心算法的leetcode题目,这篇文章可以帮你解决80% +- [关于贪心算法的leetcode题目,这篇文章可以帮你解决80%](https://blog.ouyangsihai.cn/jie-shao-yi-xia-guan-yu-leetcode-de-tan-xin-suan-fa-de-jie-ti-fang-fa.html) - 回溯算法不会,这篇文章一定得看 - 动态规划你了解多少,我来帮你入个们 - 链表的题目真的不难,看了这篇文章你就知道有多简单了 @@ -317,7 +325,7 @@ ### Git -- [实际开发中的git命令大全](https://www.jianshu.com/p/53a00fafbe99) +- [实际开发中的git命令大全](https://blog.ouyangsihai.cn/wo-zai-shi-ji-gong-zuo-zhong-yong-de-zui-duo-de-git-ming-ling.html) ### Docker @@ -343,7 +351,7 @@ ## Java书籍推荐 - +- [从入门到拿大厂offer,必须看的数据结构与算法书籍推荐](https://blog.ouyangsihai.cn/cong-ru-men-dao-na-da-han-offer-bi-xu-kan-de-suan-fa-shu-ji-tui-jian-bu-hao-bu-tui-jian.html) ## 实战项目推荐 From ef038da523b6d3590f2de65ff137e27eb1123897 Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Fri, 12 Jun 2020 18:13:26 +0800 Subject: [PATCH 14/38] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E6=80=9D=E7=BB=B4?= =?UTF-8?q?=E5=AF=BC=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...51\241\271\347\233\256\346\200\273\347\273\223.md" | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git "a/docs/project/\347\247\222\346\235\200\351\241\271\347\233\256\346\200\273\347\273\223.md" "b/docs/project/\347\247\222\346\235\200\351\241\271\347\233\256\346\200\273\347\273\223.md" index a477e75..d5bd289 100644 --- "a/docs/project/\347\247\222\346\235\200\351\241\271\347\233\256\346\200\273\347\273\223.md" +++ "b/docs/project/\347\247\222\346\235\200\351\241\271\347\233\256\346\200\273\347\273\223.md" @@ -276,6 +276,17 @@ and desc like ' %' - [一次线上JVM调优实践,FullGC40次/天到10天一次的优化过程](https://blog.csdn.net/cml_blog/article/details/81057966) - [JVM调优工具](https://www.jianshu.com/p/e36fac926539) +#### 性能优化 + +- [记一次接口压力测试与性能调优](https://zhuanlan.zhihu.com/p/45067134) +- [下单接口调优实战,性能提高10倍](https://blog.csdn.net/linsongbin1/article/details/82656887) +- [接口性能优化怎么做?](https://blog.csdn.net/justnow_/article/details/105905560) +- [记一次接口性能优化实践总结:优化接口性能的八个建议](https://www.cnblogs.com/jay-huaxiao/p/12995510.html) + +#### 架构设计 + +- [架构演变](https://segmentfault.com/a/1190000018626163) + #### 并发问题 #### Tip:本来有很多我准备的资料的,但是都是外链,或者不合适的分享方式,所以大家去公众号回复【资料】好了。 From b5de28678fefb431f47cbf2890123c7f562f7983 Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Sun, 6 Sep 2020 23:22:14 +0800 Subject: [PATCH 15/38] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E9=9A=BE=E7=82=B9=E9=A2=98=E7=9B=AE=E6=80=BB=E7=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...30\347\233\256\346\200\273\347\273\223.md" | 1429 +++++++++++++++++ ...54\345\217\270\351\235\242\347\273\217.md" | 30 + ...71\347\233\256\344\273\213\347\273\215.md" | 14 +- 3 files changed, 1472 insertions(+), 1 deletion(-) create mode 100644 "docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" diff --git "a/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" "b/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" new file mode 100644 index 0000000..ee85e29 --- /dev/null +++ "b/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" @@ -0,0 +1,1429 @@ +Ctrl+Shift+P(MacOS:cmd+shift+p)呼出命令面板,输入Markdown Preview Enhanced: Create Toc会生成一段类似,保存生成目录。 + + +### 翻转链表 + +```java +public class Solution { + public ListNode ReverseList(ListNode head) { + ListNode pre = null; + ListNode next = null; + while(head != null){ + next = head.next; + head.next = pre; + pre = head; + head = next; + } + return pre; + } +} +``` + +### 实现二叉树先序,中序和后序遍历 + +```java +import java.util.*; + +/* + * public class TreeNode { + * int val = 0; + * TreeNode left = null; + * TreeNode right = null; + * } + */ + +public class Solution { + /** + * + * @param root TreeNode类 the root of binary tree + * @return int整型二维数组 + */ + public int[][] threeOrders (TreeNode root) { + // write code here + ArrayList list1 = new ArrayList<>(); + ArrayList list2 = new ArrayList<>(); + ArrayList list3 = new ArrayList<>(); + front(root,list1,list2,list3); + int[][] ints = new int[3][list1.size()]; + for (int i = 0; i < list1.size(); i++) { + ints[0][i] = list1.get(i); + ints[1][i] = list2.get(i); + ints[2][i] = list3.get(i); + } + return ints; + } + + public void front(TreeNode root,ArrayList list1,ArrayList list2,ArrayList list3){ + if(root == null){ + return; + } + + list1.add(root.val); + front(root.left,list1,list2,list3); + list2.add(root.val); + front(root.right,list1,list2,list3); + list3.add(root.val); + } +} +``` + +### 设计LRU缓存结构 + +```java +import java.util.*; + + +public class Solution { + /** + * lru design + * @param operators int整型二维数组 the ops + * @param k int整型 the k + * @return int整型一维数组 + */ + public int[] LRU (int[][] operators, int k) { + // write code here + ArrayList list = new ArrayList(); + LRUCache cache = new LRUCache(k); + for(int[] op : operators){ + if(op[0]==1){ + cache.put(op[1],op[2]); + }else{ + int val = cache.get(op[1]); + list.add(val); + } + } + int[] ans = new int[list.size()]; + for(int i=0;i map; + public LinkedList list; + public int capacity; + + public LRUCache(int capacity){ + this.capacity = capacity; + map = new HashMap<>(); + list = new LinkedList<>(); + } + + public int get(int key){ + if(!map.containsKey(key)){ + return -1; + } + Node temp = map.get(key); + put(key,temp.value); + return temp.value; + } + + public void put(int key, int value){ + Node node = new Node(key,value); + if(map.containsKey(key)){ + Node temp = map.get(key); + list.remove(temp); + list.addFirst(node); + map.put(key,node); + } else { + if(map.size() == capacity){ + Node last = list.removeLast(); + map.remove(last.key); + } + list.addFirst(node); + map.put(key,node); + } + } + +} +``` + +### 两个链表的第一个公共结点 + +```java +/* +public class ListNode { + int val; + ListNode next = null; + + ListNode(int val) { + this.val = val; + } +}*/ +public class Solution { + public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + if(pHead1 == null || pHead2 == null){ + return null; + } + + ListNode p1 = pHead1; + ListNode p2 = pHead2; + + while(p1 != p2){ + p1 = p1.next; + p2 = p2.next; + if(p1 != p2){ + if(p1 == null) p1 = pHead2; + if(p2 == null) p2 = pHead1; + } + } + + return p1; + + } +} +``` + +### 求平方根 + +```java +import java.util.*; + + +public class Solution { + /** + * + * @param x int整型 + * @return int整型 + */ + public int sqrt (int x) { + // write code here + if(x < 2){ + return x; + } + int left = 1; + int right = x / 2; + while(left <= right){ + int mid = left + (right - left) / 2; + if(x / mid == mid){ + return mid; + } else if(x / mid < mid){ + right = mid - 1; + } else if(x / mid > mid){ + left = mid + 1; + } + } + + return right; + } +} +``` + +### 寻找第K大 + +```java +import java.util.*; + +public class Finder { + public int findKth(int[] a, int n, int K) { + // write code here + return find(a, 0, n-1, K); + } + + public int find(int[] a, int low, int high, int K){ + int pivot = partition(a, low, high); + + if(pivot + 1 < K){ + return find(a, pivot + 1, high, K); + } else if(pivot + 1 > K){ + return find(a, low, pivot - 1, K); + } else { + return a[pivot]; + } + } + + int partition(int arr[], int startIndex, int endIndex){ + int small = startIndex - 1; + for (int i = startIndex; i < endIndex; ++i) { + if(arr[i] > arr[endIndex]) { + swap(arr,++small, i); + } + } + swap(arr,++small,endIndex); + return small; + } + + public void swap(int[] arr, int i, int j){ + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } +} + +``` + +### 判断链表中是否有环 + +```java +/** + * Definition for singly-linked list. + * class ListNode { + * int val; + * ListNode next; + * ListNode(int x) { + * val = x; + * next = null; + * } + * } + */ +public class Solution { + public boolean hasCycle(ListNode head) { + if(head == null){ + return false; + } + ListNode p1 = head, p2 = head; + while(p1.next != null && p1.next.next != null){ + p1 = p1.next.next; + p2 = p2.next; + if(p1 == p2){ + return true; + } + } + return false; + } +} +``` + +### 合并有序链表 + +```java +import java.util.*; + +/* + * public class ListNode { + * int val; + * ListNode next = null; + * } + */ + +public class Solution { + /** + * + * @param l1 ListNode类 + * @param l2 ListNode类 + * @return ListNode类 + */ + public ListNode mergeTwoLists (ListNode l1, ListNode l2) { + ListNode node = new ListNode(0); + ListNode res = node; + while(l1 != null && l2 != null){ + if(l1.val > l2.val){ + node.next = l2; + l2 = l2.next; + } else { + node.next = l1; + l1 = l1.next; + } + node = node.next; + } + + if(l1 != null){ + node.next = l1; + } + + if(l2 != null){ + node.next = l2; + } + + return res.next; + } +} + +``` + +### 合并k个已排序的链表 + +```java +import java.util.*; +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode(int x) { + * val = x; + * next = null; + * } + * } + */ +public class Solution { + public ListNode mergeKLists(ArrayList lists) { + if(lists == null || lists.size() == 0){ + return null; + } + + return mergeList(lists,0,lists.size()-1); + } + + public ListNode mergeList(ArrayList lists, int low, int high){ + if(low >= high){ + return lists.get(low); + } + + int mid = low + (high - low)/2; + ListNode left = mergeList(lists,low,mid); + ListNode right = mergeList(lists,mid+1,high); + return merge(left,right); + } + + public ListNode merge(ListNode left, ListNode right){ + ListNode h = new ListNode(-1); + ListNode tmp = h; + while(left != null && right != null){ + if(left.val < right.val){ + tmp.next = left; + left = left.next; + } else { + tmp.next = right; + right = right.next; + } + tmp = tmp.next; + } + + if(left != null){ + tmp.next = left; + } + + if(right != null){ + tmp.next = right; + } + + return h.next; + } +} +``` + +### 数组中相加和为0的三元组 + +```java +import java.util.*; + +public class Solution { + public ArrayList> threeSum(int[] num) { + ArrayList> list = new ArrayList<>(); + Arrays.sort(num); + int left,right,sum; + for(int i = 0; i < num.length - 2; i++){ + if(i > 0 && num[i] == num[i-1]) continue; + left = i + 1; + right = num.length - 1; + while(left < right){ + sum = num[i] + num[left] + num[right]; + if(sum == 0){ + ArrayList temp = new ArrayList<>(); + temp.add(num[i]); + temp.add(num[left]); + temp.add(num[right]); + list.add(temp); + right--; + left++; + while(left < right && num[left] == num[left-1]){ + left++; + } + while(left < right && num[right] == num[right+1]){ + right--; + } + } else if(sum < 0){ + left++; + } else { + right--; + } + } + } + return list; + } +} + +``` + +### 删除链表的倒数第n个节点 + +```java +import java.util.*; + +/* + * public class ListNode { + * int val; + * ListNode next = null; + * } + */ + +public class Solution { + /** + * + * @param head ListNode类 + * @param n int整型 + * @return ListNode类 + */ + public ListNode removeNthFromEnd (ListNode head, int n) { + // write code here + ListNode dummyNode = new ListNode(0); + dummyNode.next = head; + ListNode fast = dummyNode; + ListNode slow = dummyNode; + for(int i = 0; i <= n; i++){ + fast = fast.next; + } + + while(fast != null){ + fast = fast.next; + slow = slow.next; + } + + slow.next = slow.next.next; + + return dummyNode.next; + } +} +``` + +### 二分查找 + +```java +import java.util.*; + + +public class Solution { + /** + * 二分查找 + * @param n int整型 数组长度 + * @param v int整型 查找值 + * @param a int整型一维数组 有序数组 + * @return int整型 + */ + public int upper_bound_ (int n, int v, int[] a) { + // write code here + int left = 0, right = n; + while(left < right){ + int mid = left + (right - left) / 2; + if(a[mid] == v){ + right = mid; + } else if(a[mid] > v){ + right = mid; + } else { + left = mid + 1; + } + } + return left+1; + } +} +``` + +### 两个链表生成相加链表 + +```java +import java.util.*; + +/* + * public class ListNode { + * int val; + * ListNode next = null; + * } + */ + +public class Solution { + /** + * + * @param head1 ListNode类 + * @param head2 ListNode类 + * @return ListNode类 + */ + public ListNode addInList (ListNode head1, ListNode head2) { + // write code here + if(head1==null) return head2; + if(head2==null) return head1; + ListNode l1=reverse(head1); + ListNode l2=reverse(head2); + ListNode result=new ListNode(0); + int c=0; + while(l1!=null||l2!=null||c!=0) + { + int v1=l1!=null?l1.val:0; + int v2=l2!=null?l2.val:0; + int val=v1+v2+c; + c=val/10; + ListNode cur=new ListNode(val%10); + cur.next=result.next; + result.next=cur; + if(l1!=null) + l1=l1.next; + if(l2!=null) + l2=l2.next; + } + return result.next; + } + + public ListNode reverse(ListNode node) + { + if(node==null) return node; + ListNode pre=null,next=null; + while(node!=null) + { + next=node.next; + node.next=pre; + pre=node; + node=next; + } + return pre; + } +} +``` + +### 二叉树的之字形层序遍历 + +```java +import java.util.*; + +/* + * public class TreeNode { + * int val = 0; + * TreeNode left = null; + * TreeNode right = null; + * } + */ + +public class Solution { + /** + * + * @param root TreeNode类 + * @return int整型ArrayList> + */ + public ArrayList> zigzagLevelOrder (TreeNode root) { + // write code here + Queue queue = new LinkedList<>(); + ArrayList> list = new ArrayList<>(); + if(root != null) queue.add(root); + while(!queue.isEmpty()){ + ArrayList temp = new ArrayList<>(); + for(int i = queue.size(); i > 0; i--){ + TreeNode node = queue.poll(); + temp.add(node.val); + if(node.left != null){ + queue.add(node.left); + } + if(node.right != null){ + queue.add(node.right); + } + } + if(list.size() % 2 == 1){ + Collections.reverse(temp); + } + list.add(temp); + } + return list; + } +} + +``` + +### 链表内指定区间反转 + +```java +public class Solution { + /** + * + * @param head ListNode类 + * @param m int整型 + * @param n int整型 + * @return ListNode类 + */ + public ListNode reverseBetween (ListNode head, int m, int n) { + // write code here + if(head == null || n == m){ + return head; + } + + ListNode dummy = new ListNode(0); + dummy.next = head; + + ListNode cur = dummy; + for(int i = 1; i < m; i++){ + cur = cur.next; + } + + // t1代表头,t2代表尾 + ListNode t1 = cur; + ListNode t2 = cur.next; + cur = t2; + + ListNode pre = null; + // 翻转链表 + for(int i = 0; i <= n - m; i++){ + ListNode temp = cur.next; + cur.next = pre; + pre = cur; + cur = temp; + } + + t1.next = pre; + t2.next = cur; + + return dummy.next; + } +} + +``` + +### 二叉树的镜像 + +```java +public class Solution { + public void Mirror(TreeNode root) { + if(root == null){ + return; + } + if(root.left == null && root.right == null){ + return; + } + Stack stack = new Stack<>(); + stack.push(root); + while(!stack.isEmpty()){ + TreeNode node = stack.pop(); + + if(node.left != null || node.right != null){ + TreeNode temp = node.left; + node.left = node.right; + node.right = temp; + } + + if(node.left != null){ + stack.push(node.left); + } + + if(node.right != null){ + stack.push(node.right); + } + } + } +} + +/* +public class Solution { + public void Mirror(TreeNode root) { + if(root == null){ + return; + } + if(root.left == null && root.right == null){ + return; + } + + TreeNode temp = root.left; + root.left = root.right; + root.right = temp; + + if(root.left != null){ + Mirror(root.left); + } + + if(root.right != null){ + Mirror(root.right); + } + } +} +*/ + +``` + +### 数组中只出现一次的数字 + +```java +public class Solution { + + public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) { + int num = 0; + for(int i = 0; i < array.length; i++){ + num^=array[i]; + } + + int count = 0; + // 标志位,记录num中的第一个1出现的位置 + for(;count < array.length; count++){ + if((num&(1< set = new HashSet<>(); + for(int i = 0; i < array.length; i++){ + if(!set.add(array[i])){ + set.remove(array[i]); + } + } + + Object[] temp = set.toArray(); + num1[0] = (int)temp[0]; + num2[0] = (int)temp[1]; + }*/ +} + +``` + +### 最长的括号子串 + +```java +public class Solution { + /** + * + * @param s string字符串 + * @return int整型 + */ + public int longestValidParentheses (String s) { + // write code here + if(s == null || s.length() <= 0){ + return 0; + } + + Stack stack = new Stack<>(); + int last = -1; + int maxLen = 0; + for(int i = 0; i < s.length(); i++){ + if(s.charAt(i) == '('){ + stack.push(i); + } else { + if(stack.isEmpty()){ + last = i; + } else { + stack.pop(); + if(stack.isEmpty()){ + maxLen = Math.max(maxLen, i - last); + } else { + maxLen = Math.max(maxLen, i - stack.peek()); + } + } + } + } + + return maxLen; + } +} + +// 动态规划 +public int longestValidParentheses2(String s) { + if (s == null || s.length() == 0) + return 0; + int[] dp = new int[s.length()]; + int ans = 0; + for (int i = 1; i < s.length(); i++) { + // 如果是'('直接跳过,默认为0 + if (s.charAt(i) == ')') { + if (s.charAt(i - 1) == '(') + dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; + // 说明s.charAt(i - 1)==')' + else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') { + dp[i] = (i - dp[i - 1] > 1 ? dp[i - dp[i - 1] - 2] : 0) + dp[i - 1] + 2; + // 因为加了一个左括号和一个右括号,所以是加2 + } + } + ans = Math.max(ans, dp[i]); + } + return ans; +} +``` + +### 把二叉树打印成多行 + +```java +import java.util.*; + + +/* +public class TreeNode { + int val = 0; + TreeNode left = null; + TreeNode right = null; + + public TreeNode(int val) { + this.val = val; + + } + +} +*/ +public class Solution { + ArrayList > Print(TreeNode pRoot) { + if(pRoot == null){ + return new ArrayList>(); + } + ArrayList> list = new ArrayList<>(); + + Queue queue = new LinkedList<>(); + queue.add(pRoot); + while(!queue.isEmpty()){ + ArrayList temp = new ArrayList<>(); + for(int i = queue.size(); i > 0; i--){ + TreeNode node = queue.poll(); + temp.add(node.val); + if(node.left != null){ + queue.add(node.left); + } + if(node.right != null){ + queue.add(node.right); + } + } + list.add(temp); + } + + return list; + } + +} +``` + +### 合并两个有序的数组 + +```java +public class Solution { + public void merge(int A[], int m, int B[], int n) { + int i = m-1, j = n-1, k = m+n-1; + while(i >= 0 && j >= 0){ + if(A[i] > B[j]){ + A[k--] = A[i--]; + } else { + A[k--] = B[j--]; + } + } + + while(j >= 0){ + A[k--] = B[j--]; + } + } +} +``` + +### 二叉树的最大路径和 + +```java +public class Solution { + int max = Integer.MIN_VALUE; + /** + * + * @param root TreeNode类 + * @return int整型 + */ + public int maxPathSum (TreeNode root) { + // write code here + maxSum(root); + return max; + } + + public int maxSum(TreeNode root){ + if(root == null){ + return 0; + } + + //三种情况:1.包含一个子树和顶点,2.仅包含顶点,3.包含左子树和右子树以及顶点。 + int left = Math.max(maxSum(root.left),0); + int right = Math.max(maxSum(root.right),0); + + max = Math.max(max,left+right+root.val); + + //对于每一个子树,返回包含该子树顶点的深度方向的路径和的最大值。 + return root.val + Math.max(left,right); + } +} +``` + +### 买卖股票的最佳时机 + +base case: +dp[-1][k][0] = dp[i][0][0] = 0 +dp[-1][k][1] = dp[i][0][1] = -infinity + +状态转移⽅程: +dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) +dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) + +```java +public class Solution { + /** + * + * @param prices int整型一维数组 + * @return int整型 + */ + public int maxProfit (int[] prices) { + if(prices.length == 0) return 0; + // write code here + int n = prices.length; + int[][] dp = new int[n][2]; + for(int i = 0; i < n; i++){ + if(i - 1 == -1){ + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]); + dp[i][1] = Math.max(dp[i-1][1],-prices[i]); + } + + return dp[n-1][0]; + } +} +``` + +### 二叉树中是否存在节点和为指定值的路径 + +```java +public class Solution { + /** + * + * @param root TreeNode类 + * @param sum int整型 + * @return bool布尔型 + */ + public boolean hasPathSum (TreeNode root, int sum) { + // write code here + if(root == null){ + return false; + } + + if(root.left == null && root.right == null){ + return sum - root.val == 0; + } + + return hasPathSum(root.left,sum - root.val) || hasPathSum(root.right,sum - root.val); + } +} +``` + +### 设计getMin功能的栈 + +```java +public class Solution { + /** + * return a array which include all ans for op3 + * @param op int整型二维数组 operator + * @return int整型一维数组 + */ + public int[] getMinStack (int[][] op) { + // write code here + LinkedList stack = new LinkedList<>(); + LinkedList minStack = new LinkedList<>(); + ArrayList ans = new ArrayList<>(); + + for(int i = 0; i < op.length; i++){ + int type = op[i][0]; + if(type == 1){ + if(minStack.size() == 0){ + minStack.push(op[i][1]); + } else if(op[i][1] <= minStack.peek()){ + minStack.push(op[i][1]); + } + stack.push(op[i][1]); + } else if(type == 2) { + if(stack.peek().equals(minStack.peek())){ + minStack.pop(); + } + stack.pop(); + } else { + ans.add(minStack.peek()); + } + } + + int[] res = new int[ans.size()]; + for(int i = 0; i < ans.size(); i++){ + res[i] = ans.get(i); + } + return res; + } +} +``` + +### LFU缓存结构设计 + +```java +// 解法一 + +public class Solution { + /** + * lfu design + * @param operators int整型二维数组 ops + * @param k int整型 the k + * @return int整型一维数组 + */ + public int[] LFU (int[][] operators, int k) { + // write code here + if(operators == null) return new int[]{-1}; + HashMap map = new HashMap<>();// key -> value + HashMap count = new HashMap<>(); // key -> count + List list = new ArrayList<>(); + for(int[] ops : operators){ + int type = ops[0]; + int key = ops[1]; + if(type == 1){ + // set操作 + if(map.containsKey(key)){ + map.put(key,ops[2]); + count.put(key,count.get(key)+1); + } else { + if(map.size() == k){ + int minKey = getMinKey(count); + map.remove(minKey); + count.remove(minKey); + } + map.put(key,ops[2]); + if(count.containsKey(key)){ + count.put(key,count.get(key)+1); + } else { + count.put(key,1); + } + } + } else if(type == 2) { + if(map.containsKey(key)){ + int value = map.get(key); + count.put(key,count.get(key)+1); + list.add(value); + } else { + list.add(-1); + } + } + } + + int[] ans = new int[list.size()]; + for(int i = 0; i < list.size(); i++){ + ans[i] = list.get(i); + } + return ans; + } + + public int getMinKey(HashMap map){ + int minCount = Integer.MAX_VALUE; + int key = 0; + for(Entry entry : map.entrySet()){ + if(entry.getValue() < minCount){ + minCount = entry.getValue(); + key = entry.getKey(); + } + } + return key; + } +} + +// 解法二 + +import java.util.*; + + +public +class Solution { + + public class LFUCache { + + private class Node { + int k; + int v; + int count; //调用次数 + + public Node(int k, int v) { + this.k = k; + this.v = v; + count = 1; + } + } + + private int size; + private int maxSize; + private Map key2node; + private Map> count2list; //<调用次数,对于调用次数的链表> + + public LFUCache(int maxSize) { + this.maxSize = maxSize; + size = 0; + key2node = new HashMap<>(); + count2list = new HashMap<>(); + } + + public void set(int k, int v) { + if (key2node.containsKey(k)) { //存在 + key2node.get(k).v = v; + get(k); //get 一次用于改变调用次数 + return; + } + //不存在:建一个新节点 + Node node = new Node(k, v); + //如果调用次数map中不存在,就添加一个新链表 + if (!count2list.containsKey(node.count)) count2list.put(node.count, new LinkedList<>()); + LinkedList list = count2list.get(node.count); + list.addFirst(node);//插入到该链表的头部,表示该调用次数中,是最近调用的,链表尾才是最久没有调动的 + key2node.put(k, node);//同时加入核心map + size++; + if (size > maxSize) {//超过容量了,删除一个 + key2node.remove(list.getLast().k); + list.removeLast(); + size--; + } + } + + public int get(int k) { + if (!key2node.containsKey(k)) return -1; + Node node = key2node.get(k);//获取之后 + //还需要更新调用次数: + LinkedList oldList = count2list.get(node.count); + oldList.remove(node);//原来的链表中删除 + if (oldList.isEmpty()) count2list.remove(node.count); + node.count++; + //建立并加入新链表 + if (!count2list.containsKey(node.count)) count2list.put(node.count, new LinkedList<>()); + LinkedList list = count2list.get(node.count); + list.addFirst(node); + return node.v; + } + } + + public int[] LFU(int[][] operators, int k) { + LFUCache cache = new LFUCache(k); + List list = new ArrayList<>(); + for (int[] e : operators) { + if (e[0] == 1) cache.set(e[1], e[2]); + else list.add(cache.get(e[1])); + } + int[] res = new int[list.size()]; + for (int i = 0; i < res.length; i++) res[i] = list.get(i); + return res; + } +} +``` + +### N皇后问题 + +```java +public class Solution { + /** + * + * @param n int整型 the n + * @return int整型 + */ + public int Nqueen (int n) { + // write code here + List res=new ArrayList<>(); + char[][] chess=new char[n][n]; + for(int i=0;i res){ + if(row==chess.length){ + res.add(1); + return; + } + for(int col=0;col=0&&j=0&&i>=0;i--,j--){ + if(chess[i][j]=='Q'){ + return false; + } + } + return true; + } +} +``` + +### 带权值的最小路径和 + +```java +public class Solution { + /** + * + * @param grid int整型二维数组 + * @return int整型 + */ + public int minPathSum (int[][] grid) { + // write code here + int row = grid.length; + int col = grid[0].length; + int[][] dp = new int[row][col]; + dp[0][0] = grid[0][0]; + for(int i = 1; i < row; i++){ + dp[i][0] = dp[i-1][0] + grid[i][0]; + } + for(int j = 1; j < col; j++){ + dp[0][j] = dp[0][j-1] + grid[0][j]; + } + + for(int i = 1; i < row; i++){ + for(int j = 1; j < col; j++){ + dp[i][j] = Math.min(dp[i][j-1],dp[i-1][j]) + grid[i][j]; + } + } + + return dp[row-1][col-1]; + } +} +``` + +### 反转数字 + +```java +public class Solution { + /** + * + * @param x int整型 + * @return int整型 + */ + public int reverse (int x) { + // write code here + int res = 0; + while(x != 0){ + // 获取最后一位 + int tail = x % 10; + int newRes = res * 10 + tail; + // 如果不等于,说明溢出 + if((newRes - tail) / 10 != res){ + return 0; + } + res = newRes; + x /= 10; + } + + return res; + } +} +``` + +### 二叉搜索树的第k个结点 + +```java +public class Solution { + int index = 0; + TreeNode target = null; + TreeNode KthNode(TreeNode pRoot, int k){ + getKthNode(pRoot,k); + return target; + } + + public void getKthNode(TreeNode pRoot, int k){ + if(pRoot == null){ + return; + } + getKthNode(pRoot.left,k); + index++; + if(index == k){ + target = pRoot; + return; + } + getKthNode(pRoot.right,k); + } + + +} +``` + +### 子数组最大乘积 + +```java +public class Solution { + public double maxProduct(double[] arr) { + if(arr.length == 0 || arr == null){ + return 0.0; + } + double[] max = new double[arr.length]; + double[] min = new double[arr.length]; + max[0] = min[0] = arr[0]; + for(int i = 1; i < arr.length; i++){ + max[i] = Math.max(Math.max(max[i-1]*arr[i],min[i-1]*arr[i]),arr[i]); + min[i] = Math.min(Math.min(max[i-1]*arr[i],min[i-1]*arr[i]),arr[i]); + } + + double ans = max[0]; + for(int i = 0; i < max.length; i++){ + if(max[i] > ans){ + ans = max[i]; + } + } + return ans; + } +} +``` + +### + +```java + +``` + +### + +```java + +``` + +### + +```java + +``` + +### + +```java + +``` + +### + +```java + +``` + +### + +```java + +``` + +### + +```java + +``` \ No newline at end of file diff --git "a/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" "b/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" index b45e662..b1f5e0c 100644 --- "a/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" +++ "b/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" @@ -109,6 +109,36 @@ https://segmentfault.com/a/1190000016611415 # 头条 +### 一面 + +1、多线程调度原理 +https://zhuanlan.zhihu.com/p/58846439 +2、select、poll、epoll +3、多线程原理与操作系统 +https://www.zhihu.com/question/25527028 +4、redis的单线程模型:单核会有多线程切换吗 +https://blog.csdn.net/qq_27185561/article/details/82900426 +5、算法、算法、算法、算法。 +6、线程池用到的数据结构 +7、最长上升子序列的个数 +8、为什么myisam快:非聚集索引,B+树,为什么用B+树和B树的区别。为什么B+树IO次数少 +9、MyISAM与InnoDB 的区别 +https://www.cnblogs.com/fwqblogs/p/6645274.html +10、事务隔离级别:可重复读会发生幻读吗?会 + +### 二面 + +1、hashmap解决冲突方法 +2、hashmap扩容机制 +3、jvm的jstack作用 +4、最长不重复子串 +5、https + + + + + + # 拼多多 diff --git "a/docs/interview/\350\207\252\346\210\221\344\273\213\347\273\215\345\222\214\351\241\271\347\233\256\344\273\213\347\273\215.md" "b/docs/interview/\350\207\252\346\210\221\344\273\213\347\273\215\345\222\214\351\241\271\347\233\256\344\273\213\347\273\215.md" index 8391e79..3825b52 100644 --- "a/docs/interview/\350\207\252\346\210\221\344\273\213\347\273\215\345\222\214\351\241\271\347\233\256\344\273\213\347\273\215.md" +++ "b/docs/interview/\350\207\252\346\210\221\344\273\213\347\273\215\345\222\214\351\241\271\347\233\256\344\273\213\347\273\215.md" @@ -35,6 +35,16 @@ 4、对数据库性能进行调优 5、开发抢购活动功能模块(业务需求) + +2019年5月–2019年9月,在中国空间技术研究院钱学森空间技术实验室实习,参加了脉冲星导航的项目开发工作。项目的背景是,现在深空探索的研究越来越热,这个项目就是研究及开发脉冲星导航的相关问题,而脉冲星导航的能够解决深空探索航天器的位置问题,这个就是项目的研究目标。 +负责工作: +1、开发一个脉冲星导航仿真软件 +2、实现相关算法对航天器上的部件精度进行控制 +3、航天器相关的数据进行数据显示 +4、近地卫星轨道5星编队仿真实现 +个人收获: +通过这个项目的开发,学习到了许多以前没有接触到的航天知识,对于这方面的知识,个人有了很大的提高,同时,由于时间关系,开发的时间非常紧张,对于自己的有了很大的锻炼,不管是从抗压能力还是学习能力,都是得到了一定得锻炼的。 + #### Tip:本来有很多我准备的资料的,但是都是外链,或者不合适的分享方式,所以大家去公众号回复【资料】好了。 ![](http://image.ouyangsihai.cn/FszE5cIon6eHHexBEgOSBGBWeoyP) @@ -47,4 +57,6 @@ ![](http://image.ouyangsihai.cn/FgUUPlQOlQtjbbdOs1RZK9gWxitV) -2、给俺一个 **star** 呗,可以让更多的人看到这篇文章,顺便激励下我继续写作,嘻嘻。 \ No newline at end of file +2、给俺一个 **star** 呗,可以让更多的人看到这篇文章,顺便激励下我继续写作,嘻嘻。 + + From 11d185fec0c94c8dcc15387d66ff269fa5b130c7 Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Mon, 7 Sep 2020 11:33:08 +0800 Subject: [PATCH 16/38] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E9=9A=BE=E7=82=B9=E9=A2=98=E7=9B=AE=E6=80=BB=E7=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...30\347\233\256\346\200\273\347\273\223.md" | 226 ++++++++++++++++-- 1 file changed, 207 insertions(+), 19 deletions(-) diff --git "a/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" "b/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" index ee85e29..16f6907 100644 --- "a/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" +++ "b/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" @@ -1,6 +1,50 @@ Ctrl+Shift+P(MacOS:cmd+shift+p)呼出命令面板,输入Markdown Preview Enhanced: Create Toc会生成一段类似,保存生成目录。 + + + + +- [翻转链表](#翻转链表) +- [实现二叉树先序,中序和后序遍历](#实现二叉树先序中序和后序遍历) +- [设计LRU缓存结构](#设计lru缓存结构) +- [两个链表的第一个公共结点](#两个链表的第一个公共结点) +- [求平方根](#求平方根) +- [寻找第K大](#寻找第k大) +- [判断链表中是否有环](#判断链表中是否有环) +- [合并有序链表](#合并有序链表) +- [合并k个已排序的链表](#合并k个已排序的链表) +- [数组中相加和为0的三元组](#数组中相加和为0的三元组) +- [删除链表的倒数第n个节点](#删除链表的倒数第n个节点) +- [二分查找](#二分查找) +- [两个链表生成相加链表](#两个链表生成相加链表) +- [二叉树的之字形层序遍历](#二叉树的之字形层序遍历) +- [链表内指定区间反转](#链表内指定区间反转) +- [二叉树的镜像](#二叉树的镜像) +- [数组中只出现一次的数字](#数组中只出现一次的数字) +- [最长的括号子串](#最长的括号子串) +- [把二叉树打印成多行](#把二叉树打印成多行) +- [合并两个有序的数组](#合并两个有序的数组) +- [二叉树的最大路径和](#二叉树的最大路径和) +- [买卖股票的最佳时机](#买卖股票的最佳时机) +- [二叉树中是否存在节点和为指定值的路径](#二叉树中是否存在节点和为指定值的路径) +- [设计getMin功能的栈](#设计getmin功能的栈) +- [LFU缓存结构设计](#lfu缓存结构设计) +- [N皇后问题](#n皇后问题) +- [带权值的最小路径和](#带权值的最小路径和) +- [反转数字](#反转数字) +- [二叉搜索树的第k个结点](#二叉搜索树的第k个结点) +- [子数组最大乘积](#子数组最大乘积) +- [最长递增子序列](#最长递增子序列) +- [在两个长度相等的排序数组中找到上中位数](#在两个长度相等的排序数组中找到上中位数) +- [判断t1树中是否有与t2树拓扑结构完全相同的子树](#判断t1树中是否有与t2树拓扑结构完全相同的子树) +- [反转字符串](#反转字符串) +- [最大正方形](#最大正方形) + + + + + ### 翻转链表 ```java @@ -1386,44 +1430,188 @@ public class Solution { } ``` -### - -```java - -``` - -### +### 最长递增子序列 ```java +public int[] LIS (int[] arr) { + // write code here + if(arr == null || arr.length <= 0){ + return null; + } -``` - -### - -```java + int len = arr.length; + int[] count = new int[len]; // 存长度 + int[] end = new int[len]; // 存最长递增子序列 + + //init + int index = 0; // end 数组下标 + end[index] = arr[0]; + count[0] = 1; + + for(int i = 0; i < len; i++){ + if(end[index] < arr[i]){ + end[++index] = arr[i]; + count[i] = index; + } + else{ + int left = 0, right = index; + while(left <= right){ + int mid = (left + right) >> 1; + if(end[mid] >= arr[i]){ + right = mid - 1; + } + else{ + left = mid + 1; + } + } + end[left] = arr[i]; + count[i] = left; + } + } + //因为返回的数组要求是字典序,所以从后向前遍历 + int[] res = new int[index + 1]; + for(int i = len - 1; i >= 0; i--){ + if(count[i] == index){ + res[index--] = arr[i]; + } + } + return res; +} ``` -### +### 在两个长度相等的排序数组中找到上中位数 ```java - +public int findMedianinTwoSortedAray (int[] arr1, int[] arr2) { + // write code here + int n = arr1.length; + if(n==0){ + return 0; + } + //arr1左右两端 + int l1=0,r1=n-1; + //arr2左右两端 + int l2=0,r2=n-1; + int mid1,mid2; + //终止条件为l1=r1,即两个数组都只有一个元素,此时的上中位数为两数的最小值 + while(l1< r1){ + //arr1中位数 + mid1 = l1+((r1-l1)>>1); + //arr2中位数 + mid2 = l2+((r2-l2)>>1); + int k = r1-l1+1; + if(arr1[mid1] == arr2[mid2]){ //若两数组中位数相等,整体中位数也是这个 + return arr1[mid1]; + } + else if(arr1[mid1] > arr2[mid2]){ + if(k%2 == 0){//区间元素个数为偶数 + r1 = mid1; //整体中位数在arr1左区间,包括mid1 + l2 = mid2+1; //整体中位数在arr2右区间,不包括mid2 + } + else if(k%2 == 1){ //区间元素个数为奇数 + r1 = mid1; //整体中位数在arr1左区间,包括mid1 + l2 = mid2; //整体中位数在arr2右区间,包括mid2 + } + } + else if (arr1[mid1] < arr2[mid2]){ + if(k%2 == 0){//区间元素个数为偶数 + r2 = mid2; //整体中位数在arr2左区间,包括mid2 + l1 = mid1+1; //整体中位数在arr1右区间,不包括mid1 + } + else if(k%2 == 1){ //区间元素个数为奇数 + r2 = mid2; //整体中位数在arr2左区间,包括mid2 + l1 = mid1; //整体中位数在arr1右区间,包括mid1 + } + } + } + //当区间内只有一个元素时,两个区间中最小值即为整体中位数 + return Math.min(arr1[l1],arr2[l2]); +} ``` -### +### 判断t1树中是否有与t2树拓扑结构完全相同的子树 ```java +/** + * + * @param root1 TreeNode类 + * @param root2 TreeNode类 + * @return bool布尔型 + */ +public boolean isContains (TreeNode root1, TreeNode root2) { + // write code here + if(root1 == null || root2 == null){ + return false; + } + return recur(root1, root2) || isContains(root1.left,root2) || isContains(root1.right,root2); +} +public boolean recur(TreeNode root1, TreeNode root2){ + if(root2 == null){ + return true; + } + if(root1 == null || root1.val != root2.val){ + return false; + } + return recur(root1.left,root2.left) && recur(root1.right,root2.right); +} ``` -### +### 反转字符串 ```java - +/** +* 反转字符串 +* @param str string字符串 +* @return string字符串 +*/ +public String solve (String str) { + // write code here + if(str == null){ + return null; + } + char[] c = new char[str.length()]; + int left = 0, right = str.length() - 1; + + while(left <= right){ + c[left] = str.charAt(right); + c[right] = str.charAt(left); + left++; + right--; + } + return new String(c); +} ``` -### +### 最大正方形 ```java - +/** +* 最大正方形 +* @param matrix char字符型二维数组 +* @return int整型 +*/ +public int solve (char[][] matrix) { + // write code here + int m = matrix.length; + int n = matrix[0].length; + int dp[][] = new int [m][n]; + for(int i = 0; i < m; i++){ + dp[i][0] = matrix[i][0] - '0'; + } + for(int j = 0; j < n; j++){ + dp[0][j] = matrix[0][j] - '0'; + } + int max = 0; + for(int i = 1; i < m; i++){ + for(int j = 1; j < n; j++){ + if(matrix[i][j] == '1'){ + dp[i][j] = Math.min(Math.min(dp[i-1][j-1],dp[i][j-1]),dp[i-1][j]) + 1; + max = Math.max(max,dp[i][j]); + } + } + } + return max*max; +} ``` \ No newline at end of file From fcc9465f3abb80d2015afa4094a3e38893c85d54 Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Sun, 27 Sep 2020 17:05:18 +0800 Subject: [PATCH 17/38] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E5=B8=B8=E8=A7=81=E9=97=AE=E9=A2=98=E5=88=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...76\347\202\271\346\200\273\347\273\223.md" | 788 +++++++++++++++++ ...30\347\233\256\346\200\273\347\273\223.md" | 807 +++++++++++++++++- ...0\347\233\256\346\200\273\347\273\223.pdf" | Bin 0 -> 754952 bytes ...54\345\217\270\351\235\242\347\273\217.md" | 26 + ...06\347\261\273\346\261\207\346\200\273.md" | 144 +++- ...71\347\233\256\346\200\273\347\273\223.md" | 6 + 6 files changed, 1738 insertions(+), 33 deletions(-) create mode 100644 "docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\232\276\347\202\271\346\200\273\347\273\223.md" create mode 100644 "docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.pdf" diff --git "a/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\232\276\347\202\271\346\200\273\347\273\223.md" "b/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\232\276\347\202\271\346\200\273\347\273\223.md" new file mode 100644 index 0000000..9c32dde --- /dev/null +++ "b/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\232\276\347\202\271\346\200\273\347\273\223.md" @@ -0,0 +1,788 @@ + + + + + +- [重构二叉树](#重构二叉树httpswwwnowcodercompractice8a19cbe657394eeaac2f6ea9b0f6fcf6tpid13tqid11157rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [旋转数组的最小数字](#旋转数组的最小数字httpswwwnowcodercompractice9f3231a991af4f55b95579b44b7a01batpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [跳N级台阶](#跳n级台阶httpswwwnowcodercompractice22243d016f6b47f2a6928b4313c85387tpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [栈的压入、弹出序列](#栈的压入-弹出序列httpswwwnowcodercompracticed77d11405cc7470d82554cb392585106tpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [调整数组顺序使奇数位于偶数前面](#调整数组顺序使奇数位于偶数前面httpswwwnowcodercompracticebeb5aa231adc45b2a5dcc5b62c93f593tpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [树的子结构](#树的子结构httpswwwnowcodercompractice6e196c44c7004d15b1610b9afca8bd88tpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [二叉搜索树的后序遍历序列](#二叉搜索树的后序遍历序列httpswwwnowcodercompracticea861533d45854474ac791d90e447bafdtpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [二叉树中和为某一值的路径](#二叉树中和为某一值的路径httpswwwnowcodercompracticeb736e784e3e34731af99065031301bcatpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [二叉搜索树与双向链表](#二叉搜索树与双向链表httpswwwnowcodercompractice947f6eb80d944a84850b0538bf0ec3a5tpid13tqid11179rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [最小的k个数(partation)](#最小的k个数partationhttpswwwnowcodercompractice6a296eb82cf844ca8539b57c23e6e9bftpid13tqid11182rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [连续子数组的最大和(sum < 0置为0)](#连续子数组的最大和sum-0置为0httpswwwnowcodercompractice459bd355da1549fa8a49e350bf3df484tpid13tqid11183rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [整数中1出现的次数(从1到n整数中1出现的次数)](#整数中1出现的次数从1到n整数中1出现的次数httpswwwnowcodercompracticebd7f978302044eee894445e244c7eee6tpid13tqid11184rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [数组中的逆序对](#数组中的逆序对httpswwwnowcodercompractice96bd6684e04a44eb80e6a68efc0ec6c5tpid13tqid11188rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [两个链表的第一个公共结点](#两个链表的第一个公共结点httpswwwnowcodercompractice6ab1d9a29e88450685099d45c9e31e46tpid13tqid11189rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [数字在排序数组中出现的次数](#数字在排序数组中出现的次数httpswwwnowcodercompractice70610bf967994b22bb1c26f9ae901fa2tpid13tqid11190rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [和为S的连续正数序列](#和为s的连续正数序列httpswwwnowcodercompracticec451a3fd84b64cb19485dad758a55ebetpid13tqid11194rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [孩子们的游戏(圆圈中剩下的数)](#孩子们的游戏圆圈中剩下的数httpswwwnowcodercompracticef78a359491e64a50bce2d89cff857eb6tpid13tqid11199rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [不用加减乘除做加法](#不用加减乘除做加法httpswwwnowcodercompractice59ac416b4b944300b617d4f7f111b215tpid13tqid11201rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [数组中重复的数字](#数组中重复的数字httpswwwnowcodercompractice623a5ac0ea5b4e5f95552655361ae0a8tpid13tqid11203rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [正则表达式匹配](#正则表达式匹配) +- [表示数值的字符串](#表示数值的字符串) +- [删除链表中重复的结点](#删除链表中重复的结点) +- [二叉树的下一个结点](#二叉树的下一个结点) +- [对称的二叉树(序列化)](#对称的二叉树序列化) + + + + +### [重构二叉树](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&&tqId=11157&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +/** + * Definition for binary tree + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode(int x) { val = x; } + * } + */ +import java.util.*; +public class Solution { + public TreeNode reConstructBinaryTree(int [] pre,int [] in) { + if(pre.length == 0||in.length == 0){ + return null; + } + + // 前序遍历的第一个节点是根结点 + TreeNode node = new TreeNode(pre[0]); + for(int i = 0; i < in.length; i++){ + // 如果找到中序遍历和根结点一样, + // 那么中序遍历的左边是左子树,右边是右子树 + if(pre[0] == in[i]){ + node.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i+1), + Arrays.copyOfRange(in, 0, i)); + node.right = reConstructBinaryTree(Arrays.copyOfRange(pre, + i+1, pre.length), Arrays.copyOfRange(in, i+1,in.length)); + } + } + return node; + } +} +``` + +### [旋转数组的最小数字](https://www.nowcoder.com/practice/9f3231a991af4f55b95579b44b7a01ba?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +import java.util.ArrayList; + +public class Solution { + public int minNumberInRotateArray(int [] array) { + int low = 0 ; int high = array.length - 1; + while(low < high){ + int mid = low + (high - low) / 2; + // 出现这种情况的array类似[3,4,5,6,0,1,2], + // 此时最小数字一定在mid的右边。 + if(array[mid] > array[high]){ + low = mid + 1; + // 出现这种情况的array类似 [1,0,1,1,1] 或者[1,1,1,0,1], + // 此时最小数字不好判断在mid左边还是右边,这时只好一个一个试 + }else if(array[mid] == array[high]){ + high = high - 1; + // 出现这种情况的array类似[2,2,3,4,5,6,6], + // 此时最小数字一定就是array[mid]或者在mid的左边 + }else{ + high = mid; + } + } + return array[low]; + } +} +``` + +### [跳N级台阶](https://www.nowcoder.com/practice/22243d016f6b47f2a6928b4313c85387?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +// 每个台阶都有跳与不跳两种情况(除了最后一个台阶), +// 最后一个台阶必须跳。所以共用2^(n-1)中情况 +public int JumpFloorII(int target) { + return 1 << (target - 1); +} + +public int JumpFloorII(int target) { + if(target <= 0){ + return 0; + } + + int arr[] = new int[target + 1]; + arr[0] = 1; + int preSum = arr[0]; + for(int i = 1 ; i < arr.length ; i++){ + arr[i] = preSum; + preSum += arr[i]; + } + + return arr[target]; +} +``` + + +### [栈的压入、弹出序列](https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public boolean IsPopOrder(int [] arr1,int [] arr2) { + //input check + if(arr1 == null || arr2 == null + || arr1.length != arr2.length + || arr1.length == 0){ + return false; + } + Stack stack = new Stack(); + int length = arr1.length; + int i = 0, j = 0; + while(i < length && j < length){ + if(arr1[i] != arr2[j]){ + // 不相等,压入栈中 + stack.push(arr1[i++]); + }else{ + // 相等,同时弹出 + i++; + j++; + } + } + + // 栈中为空时,才满足条件 + while(j < length){ + if(arr2[j] != stack.peek()){ + return false; + }else{ + stack.pop(); + j++; + } + } + + return stack.empty() && j == length; +} + +``` + +### [调整数组顺序使奇数位于偶数前面](https://www.nowcoder.com/practice/beb5aa231adc45b2a5dcc5b62c93f593?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public void reOrderArray(int [] array) { + //相对位置不变,稳定性 + //插入排序的思想 + int m = array.length; + int k = 0;//记录已经摆好位置的奇数的个数 + for (int i = 0; i < m; i++) { + if (array[i] % 2 == 1) { + int j = i; + while (j > k) {//j >= k+1 + int tmp = array[j]; + array[j] = array[j-1]; + array[j-1] = tmp; + j--; + } + k++; + } + } +} + +// 空间换时间 +public class Solution { + public void reOrderArray(int [] array) { + if (array != null) { + int[] even = new int[array.length]; + int indexOdd = 0; + int indexEven = 0; + for (int num : array) { + if ((num & 1) == 1) { + array[indexOdd++] = num; + } else { + even[indexEven++] = num; + } + } + + for (int i = 0; i < indexEven; i++) { + array[indexOdd + i] = even[i]; + } + } + } +} + +``` + +### [树的子结构](https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java + +// 解法一 +public boolean HasSubtree(TreeNode root1,TreeNode root2) { + if(root1==null || root2==null) return false; + return doesTree1HasTree2(root1, root2)|| HasSubtree(root1.left, root2) + ||HasSubtree(root1.right, root2); +} + +private boolean doesTree1HasTree2(TreeNode root1,TreeNode root2) { + if(root2==null) return true; + if(root1==null) return false; + return root1.val==root2.val && doesTree1HasTree2(root1.left, root2.left) + && doesTree1HasTree2(root1.right, root2.right); +} + +// 解法二:先序遍历判断是否是子串 +public boolean HasSubtree(TreeNode root1,TreeNode root2) { + if(root2 == null){ + return false; + } + String str1 = pre(root1); + String str2 = pre(root2); + return str1.indexOf(str2) != -1; +} + +public String pre(TreeNode root){ + if(root == null){ + return ""; + } + String str = root.val + ""; + str += pre(root.left); + str += pre(root.right); + return str; +} +``` + +### [二叉搜索树的后序遍历序列](https://www.nowcoder.com/practice/a861533d45854474ac791d90e447bafd?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +BST的后序序列的合法序列是,对于一个序列S,最后一个元素是x (也就是根),如果去掉最后一个元素的序列为T,那么T满足:T可以分成两段,前一段(左子树)小于x,后一段(右子树)大于x,且这两段(子树)都是合法的后序序列。 +```java +public class Solution { + public boolean VerifySquenceOfBST(int [] sequence) { + if(sequence.length==0) + return false; + if(sequence.length==1) + return true; + return ju(sequence, 0, sequence.length-1); + + } + + public boolean ju(int[] a,int star,int root){ + if(star>=root) + return true; + int i = root; + //从后面开始找 + while(i>star&&a[i-1]>a[root]) + i--;//找到比根小的坐标 + //从前面开始找 star到i-1应该比根小 + for(int j = star;ja[root]) + return false;; + return ju(a,star,i-1)&&ju(a, i, root-1); + } +} +``` + +### [二叉树中和为某一值的路径](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public class Solution { + private ArrayList> listAll = + new ArrayList>(); + private ArrayList list = new ArrayList(); + public ArrayList> FindPath(TreeNode root,int target) { + if(root == null) return listAll; + list.add(root.val); + target -= root.val; + if(target == 0 && root.left == null + && root.right == null) + listAll.add(new ArrayList(list)); + FindPath(root.left, target); + FindPath(root.right, target); + list.remove(list.size()-1); + return listAll; + } +} +``` + +### [二叉搜索树与双向链表](https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&&tqId=11179&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +// 直接使用中序遍历 +public class Solution { + public TreeNode head = null; + public TreeNode dummy = null; + public TreeNode Convert(TreeNode pRootOfTree) { + ConvertSub(pRootOfTree); + return dummy; + } + + public void ConvertSub(TreeNode root){ + if(root == null) return; + ConvertSub(root.left); + + if(head == null){ + head = root; + dummy = root; + } else { + head.right = root; + root.left = head; + head = root; + } + + ConvertSub(root.right); + } +} +``` + +### [最小的k个数(partation)](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&&tqId=11182&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +// 方法一:最大堆 +public class Solution { + public ArrayList GetLeastNumbers_Solution(int[] input, int k) { + ArrayList result = new ArrayList(); + int length = input.length; + if(k > length || k == 0){ + return result; + } + PriorityQueue maxHeap = + new PriorityQueue(k, new Comparator() { + + @Override + public int compare(Integer o1, Integer o2) { + return o2.compareTo(o1); + } + }); + for (int i = 0; i < length; i++) { + if (maxHeap.size() != k) { + maxHeap.offer(input[i]); + } else if (maxHeap.peek() > input[i]) { + Integer temp = maxHeap.poll(); + temp = null; + maxHeap.offer(input[i]); + } + } + for (Integer integer : maxHeap) { + result.add(integer); + } + return result; + } +} + +// 全排序 +public class Solution { + public ArrayList GetLeastNumbers_Solution(int [] input, int k) { + ArrayList list = new ArrayList<>(); + if(k > input.length){ + return list; + } + Arrays.sort(input); + for(int i = 0; i < k; i++){ + list.add(input[i]); + } + return list; + } +} +``` + +### [连续子数组的最大和(sum < 0置为0)](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&&tqId=11183&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java + +// dp +public int FindGreatestSumOfSubArray(int[] array) { + if(array.length == 0){ + return 0; + } + + int max = Integer.MIN_VALUE; + int[] dp = new int[array.length]; + for(int i = 0; i < array.length; i++){ + dp[i] = array[i]; + } + for(int i = 1; i < array.length; i++){ + dp[i] = Math.max(dp[i-1] + array[i],dp[i]); + } + + for(int i = 0; i < dp.length; i++){ + if(dp[i] > max){ + max = dp[i]; + } + } + return max; +} + +// sum < 0置为0 +public int FindGreatestSumOfSubArray(int[] array) { + if(array.length == 0){ + return 0; + } + + int max = Integer.MIN_VALUE; + int cur = 0; + for(int i = 0; i < array.length; i++){ + cur += array[i]; + max = Math.max(max,cur); + cur = cur < 0 ? 0 : cur; + } + return max; +} + +``` + +### [整数中1出现的次数(从1到n整数中1出现的次数)](https://www.nowcoder.com/practice/bd7f978302044eee894445e244c7eee6?tpId=13&&tqId=11184&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +public int NumberOf1Between1AndN_Solution(int n) { + int count=0; + StringBuffer s=new StringBuffer(); + for(int i=1;i= end) + return; + //计算中间值,注意溢出 + int mid = start + (end - start)/2; + + //递归分 + divide(arr,start,mid); + divide(arr,mid+1,end); + + //治 + merge(arr,start,mid,end); + } + + private void merge(int[] arr,int start,int mid,int end){ + int[] temp = new int[end-start+1]; + + //存一下变量 + int i=start,j=mid+1,k=0; + //下面就开始两两进行比较,若前面的数大于后面的数,就构成逆序对 + while(i<=mid && j<=end){ + //若前面小于后面,直接存进去,并且移动前面数所在的数组的指针即可 + if(arr[i] <= arr[j]){ + temp[k++] = arr[i++]; + }else{ + temp[k++] = arr[j++]; + //a[i]>a[j]了,那么这一次,从a[i]开始到a[mid]必定都是大于这个a[j]的,因为此时分治的两边已经是各自有序了 + cnt = (cnt+mid-i+1)%1000000007; + } + } + //各自还有剩余的没比完,直接赋值即可 + while(i<=mid) + temp[k++] = arr[i++]; + while(j<=end) + temp[k++] = arr[j++]; + //覆盖原数组 + for (k = 0; k < temp.length; k++) + arr[start + k] = temp[k]; + } +} +``` + +### [两个链表的第一个公共结点](https://www.nowcoder.com/practice/6ab1d9a29e88450685099d45c9e31e46?tpId=13&&tqId=11189&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + if(pHead1 == null || pHead2 == null){ + return null; + } + + ListNode p1 = pHead1; + ListNode p2 = pHead2; + + while(p1 != p2){ + p1 = p1.next; + p2 = p2.next; + if(p1 != p2){ + if(p1 == null) p1 = pHead2; + if(p2 == null) p2 = pHead1; + } + } + + return p1; + +} +``` + +### [数字在排序数组中出现的次数](https://www.nowcoder.com/practice/70610bf967994b22bb1c26f9ae901fa2?tpId=13&&tqId=11190&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +public class Solution { + public static int GetNumberOfK(int [] array , int k) { + int length = array.length; + if(length == 0){ + return 0; + } + int lastK = getLastK(array,k); + int firstK = getFirstK(array,k); + if(firstK != -1 && lastK != -1){ + return lastK - firstK + 1; + } + return 0; + } + + public static int getLastK(int [] array, int k){ + int start = 0, end = array.length - 1; + int mid = start + (end - start) / 2; + while(start <= end){ + if(array[mid] > k){ + end = mid - 1; + } else if(array[mid] < k){ + start = mid + 1; + } else if(mid + 1 < array.length && array[mid+1] == k){ + start = mid + 1; + }else { + return mid; + } + mid = start + (end - start) / 2; + } + return -1; + } + + public static int getFirstK(int [] array, int k){ + int start = 0, end = array.length - 1; + int mid = start + (end - start) / 2; + while(start <= end){ + if(array[mid] > k){ + end = mid - 1; + } else if(array[mid] < k){ + start = mid + 1; + } else if(mid - 1 >= 0 && array[mid-1] == k){ + end = mid - 1; + }else { + return mid; + } + mid = start + (end - start) / 2; + } + + return -1; + } +} +``` + +### [和为S的连续正数序列](https://www.nowcoder.com/practice/c451a3fd84b64cb19485dad758a55ebe?tpId=13&&tqId=11194&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +import java.util.ArrayList; +public class Solution { + public ArrayList > FindContinuousSequence(int sum) { + ArrayList> list = new ArrayList<>(); + int left = 1; + int right = 2; + int res = 0; + while(left < right){ + ArrayList temp = new ArrayList<>(); + res = 0; + int index = left; + while(index <= right){ + //收集临时值 + temp.add(index); + res+=index; + index++; + } + + if(res < sum){ + right++; + } else if(res > sum){ + left++; + } else { + //收集结果 + list.add(temp); + left++;//如果找到一个结果,left右移,继续找其他结果 + } + + } + return list; + } +} +``` + +### [孩子们的游戏(圆圈中剩下的数)](https://www.nowcoder.com/practice/f78a359491e64a50bce2d89cff857eb6?tpId=13&&tqId=11199&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +import java.util.*; +public class Solution { + public int LastRemaining_Solution(int n, int m) { + LinkedList list = new LinkedList<>(); + for(int i = 0; i < n; i++){ + list.add(i); + } + int bt = 0; + while(list.size() > 1){ + bt = (bt + m - 1) % list.size(); + list.remove(bt); + } + + return list.size() == 1 ? list.get(0) : -1; + } +} +``` + +### [不用加减乘除做加法](https://www.nowcoder.com/practice/59ac416b4b944300b617d4f7f111b215?tpId=13&&tqId=11201&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +public class Solution { + public int Add(int num1,int num2) { + while(num2 != 0){ + int sum = num1 ^ num2;//不进位求值 + int carry = (num1 & num2) << 1;//求进位值 + num1 = sum; + num2 = carry; + } + return num1; + } +} +``` + +### [数组中重复的数字](https://www.nowcoder.com/practice/623a5ac0ea5b4e5f95552655361ae0a8?tpId=13&&tqId=11203&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +//boolean只占一位,所以还是比较省的 +public boolean duplicate(int numbers[], int length, int[] duplication) { + boolean[] k = new boolean[length]; + for (int i = 0; i < k.length; i++) { + if (k[numbers[i]] == true) { + duplication[0] = numbers[i]; + return true; + } + k[numbers[i]] = true; + } + return false; +} +``` + +### 正则表达式匹配 + +```java + +``` + +### 表示数值的字符串 + +```java +// 统一回复 .2 在 Java、Python 中都是数字 +public class Solution { + public boolean isNumeric(char[] str) { + String string = String.valueOf(str); + return string.matches("[\\+-]?[0-9]*(\\.[0-9]*)?([eE][\\+-]?[0-9]+)?"); + } +} +``` + +### 删除链表中重复的结点 + +```java +public ListNode deleteDuplication(ListNode pHead){ + if(pHead == null){ + return null; + } + ListNode node = new ListNode(Integer.MIN_VALUE); + node.next = pHead; + ListNode pre = node, p = pHead; + boolean deletedMode = false; + while(p != null){ + if(p.next != null && p.next.val == p.val){ + p.next = p.next.next; + deletedMode = true; + }else if(deletedMode){ + pre.next = p.next; + p = pre.next; + deletedMode = false; + }else{ + pre = p; + p = p.next; + } + } + + return node.next; +} + +``` + +### 二叉树的下一个结点 + +```java +public TreeLinkNode GetNext(TreeLinkNode pNode){ + if(pNode == null){ + return null; + } + //如果有右子树,后继结点是右子树上最左的结点 + if(pNode.right != null){ + TreeLinkNode p = pNode.right; + while(p.left != null){ + p = p.left; + } + return p; + }else{ + //如果没有右子树,向上查找第一个当前结点是父结点的左孩子的结点 + TreeLinkNode p = pNode.next; + while(p != null && pNode != p.left){ + pNode = p; + p = p.next; + } + + if(p != null && pNode == p.left){ + return p; + } + return null; + } +} + +``` + +### 对称的二叉树(序列化) + +```java +boolean isSymmetrical(TreeNode pRoot){ + if(pRoot == null){ + return true; + } + StringBuffer str1 = new StringBuffer(""); + StringBuffer str2 = new StringBuffer(""); + preOrder(pRoot, str1); + preOrder2(pRoot, str2); + return str1.toString().equals(str2.toString()); +} + +public void preOrder(TreeNode root, StringBuffer str){ + if(root == null){ + str.append("#"); + return; + } + str.append(String.valueOf(root.val)); + preOrder(root.left, str); + preOrder(root.right, str); +} + +public void preOrder2(TreeNode root, StringBuffer str){ + if(root == null){ + str.append("#"); + return; + } + str.append(String.valueOf(root.val)); + preOrder2(root.right, str); + preOrder2(root.left, str); +} + +``` \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" "b/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" index 16f6907..1d1c4bc 100644 --- "a/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" +++ "b/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" @@ -40,6 +40,27 @@ Ctrl+Shift+P(MacOS:cmd+shift+p)呼出命令面板,输入Markdown Preview - [判断t1树中是否有与t2树拓扑结构完全相同的子树](#判断t1树中是否有与t2树拓扑结构完全相同的子树) - [反转字符串](#反转字符串) - [最大正方形](#最大正方形) +- [链表中的节点每K个一组翻转](#链表中的节点每k个一组翻转) +- [数组中的最长无重复子串的长度](#数组中的最长无重复子串的长度) +- [判断链表是否为回文结构](#判断链表是否为回文结构) +- [岛屿的数量](#岛屿的数量) +- [在二叉树中找到两个节点的最近公共祖先](#在二叉树中找到两个节点的最近公共祖先) +- [重复项数字的所有排列](#重复项数字的所有排列) +- [最长回文子串的长度](#最长回文子串的长度) +- [最长公共子序列](#最长公共子序列) +- [最小编辑代价](#最小编辑代价) +- [矩阵的最小路径和](#矩阵的最小路径和) +- [顺时针旋转数组](#顺时针旋转数组) +- [判断一棵树是否是搜索二叉树和完全二叉树](#判断一棵树是否是搜索二叉树和完全二叉树) +- [连续子数组的最大和(sum < 0置为0)](#连续子数组的最大和sum-0置为0httpswwwnowcodercompractice459bd355da1549fa8a49e350bf3df484tpid13tqid11183rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [两数之和](#两数之和) +- [删除有序链表中重复出现的元素](#删除有序链表中重复出现的元素) +- [在转动过的有序数组中寻找目标值](#在转动过的有序数组中寻找目标值) +- [数组中未出现的最小正整数](#数组中未出现的最小正整数) +- [数组中最长连续子序列](#数组中最长连续子序列) +- [判断二叉树是否对称](#判断二叉树是否对称) +- [没有重复项数字的所有排列](#没有重复项数字的所有排列) +- [集合的所有子集](#集合的所有子集) @@ -48,6 +69,7 @@ Ctrl+Shift+P(MacOS:cmd+shift+p)呼出命令面板,输入Markdown Preview ### 翻转链表 ```java +// 非递归 public class Solution { public ListNode ReverseList(ListNode head) { ListNode pre = null; @@ -61,6 +83,15 @@ public class Solution { return pre; } } + +// 递归 +ListNode reverse(ListNode head) { + if (head.next == null) return head; + ListNode last = reverse(head.next); + head.next.next = head; + head.next = null; + return last; +} ``` ### 实现二叉树先序,中序和后序遍历 @@ -97,7 +128,8 @@ public class Solution { return ints; } - public void front(TreeNode root,ArrayList list1,ArrayList list2,ArrayList list3){ + public void front(TreeNode root,ArrayList list1, + ArrayList list2,ArrayList list3){ if(root == null){ return; } @@ -111,6 +143,63 @@ public class Solution { } ``` +- 非递归遍历 + +- 前序遍历 + +用栈来保存信息,但是遍历的时候,是:**先输出根节点信息,然后压入右节点信息,然后再压入左节点信息。** + +```java +public void pre(Node head){ + if(head == null){ + return; + } + Stack stack = new Stack<>(); + stack.push(head); + while(!stack.isEmpty()){ + head = stack.poll(); + System.out.println(head.value + " "); + if(head.right != null){ + stack.push(head.right); + } + if(head.left != null){ + stack.push(head.left); + } + } + System.out.println(); +} +``` + +- 中序遍历 + +中序遍历的顺序是**左中右**,先一直左节点遍历,并压入栈中,当做节点为空时,输出当前节点,往右节点遍历。 + +```java +public void inorder(Node head){ + if(head == null){ + return; + } + Stack stack = new Stack<>(); + stack.push(head); + while(!stack.isEmpty() || head != null){ + if(head != null){ + stack.push(head); + head = head.left + } else { + head = stack.poll(); + System.out.println(head.value + " "); + head = head.right; + } + } + System.out.println(); +} +``` + +- 后序遍历 + +用两个栈来实现,压入栈1的时候为**先左后右**,栈1弹出来就是**中右左**,栈2收集起来就是**左右中**。 + + ### 设计LRU缓存结构 ```java @@ -208,7 +297,8 @@ public class ListNode { } }*/ public class Solution { - public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + public ListNode FindFirstCommonNode(ListNode pHead1, + ListNode pHead2) { if(pHead1 == null || pHead2 == null){ return null; } @@ -880,8 +970,10 @@ public int longestValidParentheses2(String s) { if (s.charAt(i - 1) == '(') dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; // 说明s.charAt(i - 1)==')' - else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') { - dp[i] = (i - dp[i - 1] > 1 ? dp[i - dp[i - 1] - 2] : 0) + dp[i - 1] + 2; + else if (i - dp[i - 1] > 0 && + s.charAt(i - dp[i - 1] - 1) == '(') { + dp[i] = (i - dp[i - 1] > 1 ? + dp[i - dp[i - 1] - 2] : 0) + dp[i - 1] + 2; // 因为加了一个左括号和一个右括号,所以是加2 } } @@ -1022,7 +1114,8 @@ public class Solution { dp[i][1] = -prices[i]; continue; } - dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]); + dp[i][0] = Math.max(dp[i-1][0], + dp[i-1][1] + prices[i]); dp[i][1] = Math.max(dp[i-1][1],-prices[i]); } @@ -1051,7 +1144,8 @@ public class Solution { return sum - root.val == 0; } - return hasPathSum(root.left,sum - root.val) || hasPathSum(root.right,sum - root.val); + return hasPathSum(root.left,sum - root.val) || + hasPathSum(root.right,sum - root.val); } } ``` @@ -1212,7 +1306,8 @@ class Solution { //不存在:建一个新节点 Node node = new Node(k, v); //如果调用次数map中不存在,就添加一个新链表 - if (!count2list.containsKey(node.count)) count2list.put(node.count, new LinkedList<>()); + if (!count2list.containsKey(node.count)) + count2list.put(node.count, new LinkedList<>()); LinkedList list = count2list.get(node.count); list.addFirst(node);//插入到该链表的头部,表示该调用次数中,是最近调用的,链表尾才是最久没有调动的 key2node.put(k, node);//同时加入核心map @@ -1233,7 +1328,8 @@ class Solution { if (oldList.isEmpty()) count2list.remove(node.count); node.count++; //建立并加入新链表 - if (!count2list.containsKey(node.count)) count2list.put(node.count, new LinkedList<>()); + if (!count2list.containsKey(node.count)) + count2list.put(node.count, new LinkedList<>()); LinkedList list = count2list.get(node.count); list.addFirst(node); return node.v; @@ -1415,8 +1511,10 @@ public class Solution { double[] min = new double[arr.length]; max[0] = min[0] = arr[0]; for(int i = 1; i < arr.length; i++){ - max[i] = Math.max(Math.max(max[i-1]*arr[i],min[i-1]*arr[i]),arr[i]); - min[i] = Math.min(Math.min(max[i-1]*arr[i],min[i-1]*arr[i]),arr[i]); + max[i] = Math.max(Math.max(max[i-1]*arr[i], + min[i-1]*arr[i]),arr[i]); + min[i] = Math.min(Math.min(max[i-1]*arr[i], + min[i-1]*arr[i]),arr[i]); } double ans = max[0]; @@ -1544,7 +1642,8 @@ public boolean isContains (TreeNode root1, TreeNode root2) { if(root1 == null || root2 == null){ return false; } - return recur(root1, root2) || isContains(root1.left,root2) || isContains(root1.right,root2); + return recur(root1, root2) || isContains(root1.left,root2) + || isContains(root1.right,root2); } public boolean recur(TreeNode root1, TreeNode root2){ @@ -1554,7 +1653,8 @@ public boolean recur(TreeNode root1, TreeNode root2){ if(root1 == null || root1.val != root2.val){ return false; } - return recur(root1.left,root2.left) && recur(root1.right,root2.right); + return recur(root1.left,root2.left) && + recur(root1.right,root2.right); } ``` @@ -1607,11 +1707,692 @@ public int solve (char[][] matrix) { for(int i = 1; i < m; i++){ for(int j = 1; j < n; j++){ if(matrix[i][j] == '1'){ - dp[i][j] = Math.min(Math.min(dp[i-1][j-1],dp[i][j-1]),dp[i-1][j]) + 1; + dp[i][j] = Math.min(Math.min(dp[i-1][j-1], + dp[i][j-1]),dp[i-1][j]) + 1; max = Math.max(max,dp[i][j]); } } } return max*max; } +``` + +### 链表中的节点每K个一组翻转 + +```java +//明显递归解决,翻转第一组之后,以第二组的开头为头节点,继续翻转,转翻到最后,返回。 +public ListNode reverseKGroup(ListNode head, int k) { + if(head==null||head.next==null) + return head; + ListNode h=new ListNode(0); + h.next=head; + ListNode next=null,tmp=head,cur=head; + for(int i=1;i set = new HashSet<>(); + int res = 1; + while(right < arr.length){ + if(!set.contains(arr[right])){ + set.add(arr[right]); + right++; + }else{ + set.remove(arr[left]); + left++; + } + res = Math.max(res, set.size()); + } + return res; +} +``` + +### 判断链表是否为回文结构 + +```java +import java.util.*; + +public class Solution { + /** + * + * @param head ListNode类 the head + * @return bool布尔型 + */ + public boolean isPail (ListNode head) { + ListNode slow = head; + ListNode fast = head; + while(fast != null && fast.next != null){ + fast = fast.next.next; + slow = slow.next; + } + + Stack stack = new Stack<>(); + while(slow != null){ + stack.add(slow.val); + slow = slow.next; + } + + while(!stack.isEmpty()){ + if(stack.pop() != head.val){ + return false; + } + + head = head.next; + } + + return true; + } +} +``` + +### 岛屿的数量 + +```java +class Solution { + public int numIslands(char[][] grid) { + int count = 0; + for(int i = 0; i < grid.length; i++) { + for(int j = 0; j < grid[0].length; j++) { + if(grid[i][j] == '1'){ + bfs(grid, i, j); + count++; + } + } + } + return count; + } + + public void bfs(char[][] grid, int i, int j){ + Queue list = new LinkedList<>(); + list.add(new int[] { i, j }); + while(!list.isEmpty()){ + int[] cur = list.remove(); + i = cur[0]; j = cur[1]; + if(inArea(i,j,grid) && grid[i][j] == '1') { + grid[i][j] = '0'; + list.add(new int[] { i + 1, j }); + list.add(new int[] { i - 1, j }); + list.add(new int[] { i, j + 1 }); + list.add(new int[] { i, j - 1 }); + } + } + } + + public boolean inArea(int i, int j, char[][] grid){ + return i >=0 && j >= 0 && i < grid.length + && j < grid[0].length; + } +} +``` + +### 在二叉树中找到两个节点的最近公共祖先 + +```java +public class Solution { + /** + * + * @param root TreeNode类 + * @param o1 int整型 + * @param o2 int整型 + * @return int整型 + */ + public int lowestCommonAncestor (TreeNode root, int o1, int o2) { + if (root == null) { + return 0; + } + if (root.val == o1 || root.val == o2) { + return root.val; + } + int left = lowestCommonAncestor(root.left, o1, o2); + int right = lowestCommonAncestor(root.right, o1, o2); + if (left != 0 && right != 0) { + return root.val; + } + if (left == 0) { + return right; + } + if (right == 0) { + return left; + } + return 0; + } +} +``` + +### 重复项数字的所有排列 + +```java +public class Solution { + private ArrayList> res; + private boolean[] visited; + + public ArrayList> permute(int[] nums) { + + res = new ArrayList<>(); + visited = new boolean[nums.length]; + List list = new ArrayList<>(); + backtrace(nums, list); + + return res; + } + + private void backtrace(int[] nums, List list) { + + if (list.size() == nums.length) { + res.add(new ArrayList<>(list)); + } + + for (int i = 0; i < nums.length; i++) { + + if (visited[i]) continue; + + visited[i] = true; + list.add(nums[i]); + + backtrace(nums, list); + + visited[i] = false; + list.remove(list.size() - 1); + + } + } +} +``` + +### 最长回文子串的长度 + +```java +public class Palindrome { + public int getLongestPalindrome(String A, int n) { + // write code here + int max=0; + for (int i=1;i=0&&right=0&&right=max){ + return false; + } + return isBST(root.left,min,root.val) && isBST(root.right,root.val,max); + } + + + private boolean isComplete(TreeNode root){ + Queue queue = new LinkedList<>(); + queue.offer(root); + + while(!queue.isEmpty()){ + TreeNode tmp = queue.poll(); + if(tmp == null){ + break; + } + queue.offer(tmp.left); + queue.offer(tmp.right); + } + while(!queue.isEmpty()){ + if(queue.poll() != null){ + return false; + } + } + return true; + } +} +``` + +### [连续子数组的最大和(sum < 0置为0)](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&&tqId=11183&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java + +// dp +public int FindGreatestSumOfSubArray(int[] array) { + if(array.length == 0){ + return 0; + } + + int max = Integer.MIN_VALUE; + int[] dp = new int[array.length]; + for(int i = 0; i < array.length; i++){ + dp[i] = array[i]; + } + for(int i = 1; i < array.length; i++){ + dp[i] = Math.max(dp[i-1] + array[i],dp[i]); + } + + for(int i = 0; i < dp.length; i++){ + if(dp[i] > max){ + max = dp[i]; + } + } + return max; +} + +// sum < 0置为0 +public int FindGreatestSumOfSubArray(int[] array) { + if(array.length == 0){ + return 0; + } + + int max = Integer.MIN_VALUE; + int cur = 0; + for(int i = 0; i < array.length; i++){ + cur += array[i]; + max = Math.max(max,cur); + cur = cur < 0 ? 0 : cur; + } + return max; +} + +``` + +### 两数之和 + +```java +import java.util.HashMap; +public class Solution { + public int[] twoSum(int[] nums, int target) { + HashMap map = new HashMap<>(); + for (int i = 0; i < nums.length; i++) { + if (map.containsKey(nums[i])){ + return new int[]{map.get(nums[i])+1,i+1}; + } + map.put(target - nums[i],i); + } + return null; + } +} + +``` + +### 删除有序链表中重复出现的元素 + +```java +public ListNode deleteDuplicates (ListNode head) { + ListNode dummy=new ListNode(0); + dummy.next=head; + ListNode pre=dummy; + ListNode p=head; + while(p!=null&&p.next!=null){ + if(p.val==p.next.val){ + while(p.next!=null&&p.val==p.next.val){ + p=p.next; + } + pre.next=p.next; + p=p.next; + } + else{ + pre=p; + p=p.next; + } + } + return dummy.next; +} +``` + +### 在转动过的有序数组中寻找目标值 + +```java +public int search (int[] a, int target) { + // write code here + if(a==null||a.length<=0){ + return -1; + } + int low = 0; + int high = a.length-1; + while(low<=high){ + int mid = low+(high-low)/2; + if(a[mid]==target){ + return mid; + }else if(a[mid]=1&&arr[i]<=n&&arr[arr[i]-1]!=arr[i]){ + swap(arr,arr[i]-1,i); + } + } + for(int i=0;i> res; + + public ArrayList> permute(int[] nums) { + res = new ArrayList>(); + if (nums == null || nums.length < 1) + return res; + //对数组元素进行从小到大排序 + Arrays.sort(nums); + ArrayList list = new ArrayList(); + + solve(list, nums); + + return res; + } + + private void solve(ArrayList list, int[] nums) { + if (list.size() == nums.length) { + res.add(new ArrayList(list)); + return; + } + for (int i = 0; i < nums.length; i++) { + if (!list.contains(nums[i])) { + list.add(nums[i]); + solve(list, nums); + list.remove(list.size() - 1); + } + } + } +} +``` + +### 集合的所有子集 + +```java +import java.util.*; +public class Solution { + public ArrayList> subsets(int[] S) { + ArrayList> res=new ArrayList<>(); + Arrays.sort(S); + LinkedList list=new LinkedList<>(); + dfs(res,list,0,S); + return res; + } + public void dfs(ArrayList> res, + LinkedList list, int k, int[] S){ + res.add(new ArrayList<>(list)); + for(int i=k;iL&pn>|HSX6sulwdxmz5VmA+Yp(-Lup0=uvPa+{w(A z{@5|m6E2pfZcZ+6ffMIkoE&W29R%q`)m@w{+|4b4ms)lis9h^2nZCBfI4OhE6he`F}?z4u?SD&}af2g~K2)cr*@!h2!xA1RjsYVt&IX1R;b#2%j5OX_UQ- zJFsLDr-0am7y<%K8U%v~Ar0aqkcCLCAci5r5U^yRVYiY~Fn9!pfWugQL@^xUH^?AffZzd3Wb_32<3F%C*b0J0g!l*=3s#|>LE0W0GZQwM4*C!@^9Eay zI&V~EAg22RVPKU2@{tUY3}LvfL>Nj8fky#Sfm|ryF(?EIgA~L5hB1(O&?=!7k+>M- zT^UdmK)Z(sb@_cW{kC%a7Oos%F$4jJ62rk!C@cbl7Q+$%E=HjNhQ<(xzpWfZvh=1n z16K)cd6N7hBcB}z3Dg@TqO=$p(#5uN=>#MKNx%X)#{<$AjUf^-aA1od0NISh{bS{X zO)iBt5cq)h*nh*N8+=IW42^}Rf&>@{B_xxi!3GlfzE?5NlF1E3OQI17j9B-Lu|-E(g73)5V;^F0cjc1U`QZA zv<3T-$*2Db60o4v@XIv%e^xL!GzK9?z+%yG6cLXgB2jo09ETA@pow@K>Noy|m=dBH zBr72RL4830!6y^SkYDlDnfb${18dnZ#W)gCc_`g0B8QSlgKE)mF6G@ zaPa|04Ud7N01FC*L}O86a17w%1Gd9o{1Aw6B>!rIm{7ptPn8F41W+1~xmzGdL$(x| zgn)3eA%A7iWMFQkd5FOQ1}*_D2DsjvrauO7y)i`WKN*3fZXsg+uh<;|dP7P;P(rZ< zQWOMg1sQ>)DInT{(?jInjOGwdY~ld7mDV8!@X=8y91;5)4ge<|(CdG3fBtSG1M4L- zP{8GrRUwQ)EDQ-g5ECTn56KHizJr_N-}xQdL?G9Y2P%LN+SVC27{KqwV2Q}zd_W8! z^ay|p`@4<&zastL;DV3CM8#W5iIgABCk7t2VKAz>@6MYI?Yoj1*gGrJ{kD1QJWY0p`R?XBE_l|i(CsPexI?1E$e9XVu#+e*6;4Fnu; zD8T0eVu65PEe7l-Jn&nA81&P}rtu6>WfSik97$3!Ao(^eV8}>@s*t-tB&W(&nucg3 z79oZeBmSlc&_E;rjYt0B8iD)-g+9nab|5PepqRyf-ba6=J5k`(&=)yIeg#vtiGzyCW z133gN;MpQkU);wUz10F8;?>Dr7)FROcBH14l+*muw zCix2+Lvb*YJp*AFvU@=ZL_!NV0eKJ`2t{ro^#P|HPe9_4zqu^{I{?3_0^0m{&i!NU z&^jSjg$z=#3OO{S*c^lvP~DP#$ovCI(6CKo7OawDKV-jH0K$C>0Yw4fD-=cyCk97@ zVL8Bj1zZ@wF~J^C&^m~90&^=fRRKTNIW9Qxul3M zL`z8Gf!O-fTmbR=?=kWJ%vYPMf^;)975EL3cHh7++0V$3Hitm?V=PpK7P&DM5dsUj zsr&#pck5^a#1>FkJW&h-$DoLS+CTw;Y7`J$zymR}KjMuWfRKX65Kh2{w5%Hx1%S%n zHYEK7{s&}GcSyHy&xW}t7-Vun`g!x`btrgGX6|n?4G1$y!T-(11Y7@*KED=8My;*1F@ZP-hJZxkfjA$a zzw(=$% zKp+DJWE1@H3kJkD5NB*EWsp5cLK|#_C?E})F;HjlAp@}y2qh`zNEVU-khgW)2ZKWp zuy{bU!2#C`h|*yI0~rWLV1VC){&2*>1ws5sX7hvF=1(~X?R!XcK_VIimE@s9)xY^r z5Dp+p%l}P*Z|)@sfg78Hq!XbkIQSpQI%F(NX0poL*t-FFLZb5aKsJ-CrMZ)ZCB3MM zrK7dmIXLJZNJzk4-GJ;U2l@*MPZB$W$HX14FH8!~h)dZzXgm-YflGO+eLl-ikUD4* z{uW+C+c5FNzt2*MxAkd7PI`EvE5hnSkw|El@l#2ye7gr8Yp$nD*MBbA{yc41@qM~L z^5@#iisi`9l3&e!#tDe%9eThZBCMA!V*GP;LGtGc7h=7H^TcbM&S|0Hww(26lLLt2 zv#aYr$L~guie6r8zAO1X6mj`$64%-iE`vc-u-W&Ty_JJXTIgmF>cpwD>V6(%XIDb*X=v4B=}#Kd<@L@dciY5 zt>G?*H?^(cS!wT%VivqD-{e6ZpWr%N*cA0l?xn6*l9u^_bO+RXo%!u;GCnb5J~6bR zOx?qV2c?@8!1(mGqZHeLU4Z zQA%FUuy$P4bB%lSKKSk2Vsm1MMC`qbZ+(1bggq5Ho#VV)Z}s74U~Pu&?S&l08t?6e zXXoOLmSYBOX6n_$PHC>CiwBt7KD)@y)%3CazUJ)r9`OisW%Zm%jMA5uHT#;LqVc&b zt9-4f3wY)C8R?9g2i_^W?5d7xPOn|@{^(k^L&mNj@x+M-p%OiJ#7E@hkoQxE@U*4X zQ$-HV*FHb}VIvvG*O;z4_^Qc9?MG>zz0yFnwOr}DhwJ@id_(C5{bh5Pk9;L~4?p!P zbC~sQS#^*2;&HEZesI;*$zizEK`(dk(NO7JUPuvFYguX9Aphr8$4Qm(sjF{F=Pq|u zJ5<^a-W7Nq$){H)X3STn*?5o7_~ej9+1J_e*6*(zkONT;F4b&3@4cl4_0B2FRu{On z8oKEo*i+GE@cOs_dsQo4*{Qh~H)8V{w9}jH6M@u+A!YWRvmLc`Q>zSp1;pOovw#b{8{F7>*`uCBIa*zPTf-O>l=V zjZ#?Xv-Lhli>T(34=2Wj*79zDeO(@_)O`BmvP-{r#jPRngi!Xl{K`RRSFHo$q3IK; zukW*bc1$hR&0o_CVhQm%sUEU2QK`1O$cdW0$cc%&$T3PVv!QxdLqoNfWUf^T&ssg4 zcTGN-ceXVtF?Fo8^IT(v75`z6b#0c5>4N+}vt14P_O^XBUqffaEqsoNz{|Q$44AYj z%~MIP)EpMP@206%#u^2DCdwV-CvC8LN6K)uRrSoPG&%QvF-os~v28y6VzmB1`5I6T z>c_8gh_(i`INV@jam{7Q8WkVYt}j)*k!4fKWb;~$^XK8iRu6>vvz89wi=BeZ%vty` zg*zKt9|`F?zG}Vk!TSVXd34b|qn+0dl%qM@s)%bu3tM&$ZwA^MU9S(?&2SRz@gsRX zch|VImi7?2vaZiy?xz-a50uH?PaVziF^$)~Vr4g6we#UAg%-v8&re$%&767&ukubz zeV%8Z@`6!^^19t{d0ConK{&PWYlqRH+OA1vuWrlFIjXxK8vV=`e?qrkbZ{T*j@JuW zqU?uQ53wk-3SXIMP8Yaup{Z6Hu0%;|&o@%Ddgmg{b``BuSuk?AUx1H-tJLYudR&p7 zGNr2haCwsP^P%A4c?ULAE@h>f+j$WRQBOE4=a5u1`-_|t!+DAw&?i_gpog_ERDMT_ z-(fyBT_0$X9%;3wv^@L;JZoh3rJP#&%-PBugSa;; zZU=MmJ~SlXr)kLfWnpEKUo1R+ zwB3uAbPr*&lmvo$!awkahPqmead@#MdWJPvPwI!b3cuCB72FsmZ__P$a-s_L3Yk(80^QF=ekh;4kmI%M0KCOkf16d;%P({pv2U|Y zyxCbF)^=C7@<-O4lXc5V08>_W_*xoydHDw>LDE z&ljJ$PuSRPQ~Dt8T^=8PXZC#4OZdG1>9LY@EmTNJ5~bj*<(7qJ7AXDyzZKoI=t?gyMqW;3=fs$c4`*=)wWVtBu46ge$D8QX&>qyT{(O%IW3{KnN3tx0v;bC#7}6@P}?V|BPCFjmmDE-EqV zws6F)q=4gw=qc>Lr^= z*{!^1PW8b#&)pQ2;Q|AR)aP}%Ej+v6Qo5NyO}5@=>R~F-NURK3g*SiDy$|$!L*rg# z)9t2dyU*?0?!}{Y@Z4R;GqeM~2P-L32BqB7%g|2rC8W*7q?R-6O+1xu!zZ9~1FM=v z+rFeMSG&Y}Gnn5!&TY34@c$00NYxf0qLn2R&iv(>y4yul7asLm^~KG{Qrt^XwW(^k zEvT4*-NrhmY$e3im38yeeb?;hy+3wHx);-)|NcpCHd!fLv{B)UdTg1`HwyrqbV_4o z#_v6~$jts^%(4~?kru2bd{2e(#~v!l-p)G*Y#D#_z+`mO#fjNI+DSEG{dwrL42{^L zeLY#}r@Gl1Yt&I_ZFbgPN?4?QjLums7v0z$Z-Yc-)S_0EY~)Pl^WjLoVBhSN@ZQx& zQj)#?w{^16KW=4f`+H=osHNH*;1x_%)O6u(BW~`$6?Lw1yP2@TmAMCe1Bfeo!1_6LVQ~#%mp!*& zFI@I@YS0Qql^Wa-3D7^xY{e}*IL>W%_;rgk&Ev4xrkC0KLk?@)(3j+APP(b}@yD7( zo<5hVz2jH5rko*VLT7U{{osMpe4)Y|<4460D&IQyRa?I&Cjc5wD?uvWP6?i66_?gNVXz zr}62AYy(#pMY3&4mtrS;_n4JzC%kLK%I&??Sa~e5hjE_;^C{PsFY&E-9#*0(3q5Al zx~_#6Rfy}hvATc7zENdD^eTpZ#J=I#89%Ktx@elY*h}v}rSFkkSxheTIAuCFU>)Vg zA(uguZcvXJyl1sD=}1;GY$4G9uEhw?#jM?4UcmosUlgxPg!$jSea*Dz^nlVqwm!w3 z3sck!+iNO)>4m8%t7nOwi5A90!q@LnLd9y1g%AYQz z{uB&_(qvG$5x_k+SRl9;zTTHYm4K{38*uHp_1%LsKOpQWoyahNO~fPJ0Wwd-hN z?-k*FQZ5#u95lQNl)}-l=HA-Q86DXN- zhKKXijHwj2W?qY8kRk#rHcB4fZSuIzAm2Kf;}K8FvDsv@>@Ugp60bqe4VS?#*`nPyVMkL@P^gRyC*O&UfyS; z%vV`*K6lu{L0c^x1d+W?^TY+BOP`yJU+?P zRdTF|5w)!=g2JZB4{NCyhlZbG3rS~G(xOcj#4H*y<1GE+iWE+L4TwWaonlK)2kZf@ zCzky)dw_nS>5Pg%zs75)X=p>z?Ul5cQU$4?sY0;hq^ZKvIcDq7uv7g}=d)BOIwH26 zYvS*S;5^p^!&>U|M^T(>DpVCd(H22r-QZB2Xiu2dI~6@N@#IDs zi!DWiwJnQ@_Fj`)fPD5ZRfJ@`Nu)lljv1SFkR^a4=}7(6V?s$6v}unCT@@;1%z^Ni ztSp=&gkoV$?54C2l``b5*&9?MN1 znqKg%OEQ}Mhq4$$A}RyftcXM76ugm#!kS>fVE!nekgO^^C|>xz$FQ6^yeU!XxZWX@ zWzfgdMu5*$yXG!&(fodO-=56Vm%R7SACinN-kq#NVYj!L-R=W}@Mj^IK_qT(|Gx5b zg|KrvuWU3X0)-#G$pOmZkuz7HCL~ApA0N;4b19&_A89j?@R&yG$m{+?w1QjOvbuBPdPxBMk4%@Eo&H9WUEfZtpY`HoLupU-9j$W$ z_gnSEF~uz%KVdpidpjl;b-2{Zu~oPyMmRR>lFH@n)9n{*bOq>qmajVsX)@f5@V&mX zmETXvr~lI1tD5;MyCUhq$+T+>>YjD*za<2T$OxDSn)#pH)l6}2Z>3oU^1KqG(QYY~ zYfny!A39uYD0kEU9ag7JYbL~Br_Es|1VcT(H3jdU zm@?_HntfQp*+ap!UyI^);|tK33KKRL8aBG6e^CDxVWCVV>zVm13s=>}i}}<~sQ2r@ zZVORXv79N=uL*m>U30YdeI})e_H^CL>8{%AH!rW9B?@%iQ{l{f-`Y(l5BMr4z1e&D zE=nh!0R(*jOrBMn!}7s{ZZ6@|!si3S4cx2MqiX~E9o^an5+AAMvZL;UVa9VMu zQ@0b3j)+0ZX?k4mEKb>!p?!7e;N#}en%Mn&6i_27j6@cywmLryYD|TZ02V_qz}pW* zmL#Y~)({9b0DVq?-3vv6-vd*eKTMktq&Ie--~two1}-`Ek2Zj>uO|diSii50{Zbd( z=M1(TvNo%U1-iXH&N%{1I--IovT~8z_96vkB3&0*79&AU@H} z&~~MvStTEx_ep(}Ky}FsmbAZy|N3IkPCxEde&qf=L}z~V*+|$$vztGI=#ICT!;<=* z^Gnq7(=G;4Tyojh6D!v#iAq*0EpS!7x|^TnXz%vz;0&dUK~$ICLq%XR9%$ZY;GmrY zqw}--p`y)1cO}0EIFH%(Bo)^dliTw_hc;ILyrxR}!uEw)PH_FE*SFs2voAj9W;6~A zr%!sDxWDCL$l?-t{$SiqFUU!O>!FWvrj4HkBb<#&v7R~187iJotlwr@BpF)kT+8*m z%pE0V@nIKR5;Gf>Y&|UWF+=vjI?o!rH63GSxH+tW07GB*yG;Q%X8?=y4KHSS1?Lxix0${x1qCo88qE@X6+k zQ~OVdTe+y4XImUzetr4#m7|;&r#wZ!-Dth&@~wGb^lnT=QjD8q*2RSzb_?qji*Joz z9v2CcZ?3)=Zh5QU<1yMdX4HB|eaVyK?=^>q4(@8HS=hgLl=ge@&U~z|v;*UQidB|9 z;f3%uxBsrg$OD*j0I@-;FVfSAT zfI3JmoUv~mqx{+3b=3FzkTmMUr}=QM_33s*inzq`pyZG5S#y%lhh~_Y7JjbY^^vue zTW2BWP%P`fzS=4%>$V96Tw1BoR&BF2p0ys&m1)qU7|XGH=*LPTEwDhmPLJY!4)ap% z#FRcDfHDCAw3K%r`O!qar6qm`C@JO>CBjB@V2eVO7gmyu^N}f(etL&$a)5U`7iZ|8 z7T5(=$|`!;z3D4$vOq_m3OjbdqL;pAA$!~tw^+&k)rjVM{_bTp+;S!T7o%(6^C^Ax zq?hzb1)KElwTziBU>D=DFOjD)_3yjeH@Z5dp58 zZGL9fx0$qcbPF_nZ0@rP-mRaZyQAY*|4BGoW7S2x`;n`cNKhF6G&oyfX_ z#Y|@(ZGJ;hA$y&Ce~S^lg#H8+J!?yiGe9Y{+!0QtSiUYXgW<<)4X@8s?hs9?h~efea@k{ zz{+=I?(=A-+vkH3sl9=rV#sm=H)pkiPmwFy5(i6y07Gxd4?r8i2dZ%0B#Q&%ybWrhkS0@C}HVNlUb;QqO`I&%S{sQ|_P zrUZG`?PfsXSEC771coe{T`1hIOnE&l@IJMPfWM{1Zd3$}k&pQVU5z?rQ$xUQJIxye_9GSl_ z&T5YDIJcy~DBZ|O$}eH?YFW1xM3uHl{2WpjXLDH|r3g=eg~Mtr zR8yACMKi;QIL%ZY#mf(XE}e+oSmD15_yC;jji?W;C7tJpm7|2gh{$ zdqZIEE!bwoPBU4!vXZf-BopFP;iCq`lsiI}ChY1$LYP=zNbS8F`eR1&8!+ub)KMq!U_+DOP(60um|7z$1 z8~pNC1d={!b%#m~WLo!5XUMM6WB+!aZ~h`pKj4Qx5YGT zZpy1qmA7`jmvNgL8%#+&^KmAA<+SzS^F*WOsCN~m?A|#=>mMEHznHVy+NMp)(EsQP zTO0N;@L2x%Gn#XG+B*B&*VoDnCPfs;{+b7`X=^R0Zk;`#|7idoV{i9qe1~5n_{yZn z3)!y~R8S*4+TQQi7~r*4xy8lnC%J|D zUlnluyw3k*zOmU({fB~(vH>i32ka&7%n&XyLxw-=fM(*rbtf%!dr~1{{6EgLow_*C2LV2NocA1s4IYW7&+?zg_qdl`&Y2bka zHFtKructk4PE}qg4ZX$|PyeuWEia^a(+797hl%0+4jibvvttD8bQOBd=x9G&e`oIY zfm$sFAEn&tDr3&!s=Jcw0S)_}?N&FV5-6mI=woVfuh{44$A_=keZVXz?`nB3fbF0i zMRm0LW*>zM6ZOrggbOL6`rs$+{rKV#QwgLh?4+B9?r9jZ@N`TcrHVc5FdoKoARtKO zSsks3*;QmAMNA(|#eUJqdPhwi-o)%^ZQX>485OeANLGQ(F9>)^YYJ4sZg6Nc1IwTm za@UU;g<0fMvGI>DAGX!7(n&muGK!p8`O$o!a|myEPR4j&-0hT7inc)?s{?jBBqTni z4TQ?f6t%64-t!;K|JFWud7UArk!}&ZndH|G|G(}i0Wu-BF3$oupAY1X0cQ)~Xf$vD zL`;l8go^v6jd`1AP zn>TMjiBV)#FqZ?$9wN`E0p9)jT_`sMTJuJ36m+Ccn(Pt}AjM=0(Fvp;0$E>3AdhI{ zNSPRrYzd{&{)SF)RsYvJ1R#X{Ys%LKDM+UW!4E;%dZe61Xe{`EPz)gq%24@7O4nwZ z3UDFK7GelU=Mh7q!F=4!3?(rjXA#KP{Yz4@43upN=8cgK!ho4Q|9YhCS4JP$8KgXk ziO5b50w;dA4iG#MNSwvs$Pb7B_e6jhwtt+ahX?@W0Ymw(5P6`SBPjC?;sYqj2+FJi zDMOZzw?RwNwu0ye_WREnkfc1q&9P8IFlw}MNp6t8`xZnOb_dvE`!Lbn5|G0#Q z?D!rC$L4J*&^aH{DIf?y=s}jO59FzCAtiw9Jro{D7b82_jR%tVz#QP;_S_~@K*?a> zqR1o**b1FgBIUS28wG-DGd~WJJ0Pn56f_%zAqhBWERUAEVh;T?|2{$ z5XgB4E@UD*!i5Kt;=yd;-*K|JR0vFPy`*ilAu<15ct8l;N?AtxRqQ zZxRAd-GMhv0NjiMl3IaVX@K)lr2N+3_BABDKy*t(+YRDjNMwN!{%xz0iMBtG01X4@ z`^V`va4N9#uTyoK>Jd2Kv~{u!2b?>^U;s@>entcMCMg^CH>g1{N%9Jk?+|ez8baiQ zkN_%Ai2F!KaKKj5B@7Ul;LasUaA@oXCN_-%@`JX3iMWMe;*kg(UJST~XY=3!c+C(} zjEMW^xrKkXlQwkRrrd@2QwE|u2o{+#g`fl>-=yuInev;hP-k#+K)gai3HUYA$sg#u z|5GJS#x=kM|4p9!|9ECH2HZ*u7KcS4fJ}HC;1J*eB?4S?fdj5e!U4BJ0Y_(k<3EUp zz+Fc&EFso|1jMFt-P8^L&S4-*z_I^seSr9b^fW0ooeZ6>?hYc5+JZg~@iTN@aDy#? zYlhCbxt(|Q^z?Le@&uErUCf;v&byeJyV;mqVsS(>3*eBLg()Cnh*qWqQ!LREgF~8G zVJxhGi_6^3+gPA5ZqC3HFjg@x=TR>1Zl=I2HjdUJHjY5Pwugpbd-?{@AHv+d) ziny3M+5rQ|=(UyhGy59)4ffbT{_yrMqxM{?0Njh{a_4BDiigdm-e z_yhOgcOk({Ixqo_g$PP=bs+3Q(goV!U@IiqNvnd^4DyBy+4UGeBxCCo7I5esA%?*K zZtLb1mf-DCXeE!f6Y62nY@V$ESZ?Wbo^TN#H&{;0`X-ANQbcEQU-}Z%~vZwV;)6 zIwYVX{_AaA5PCtx{MG6KKSbJf&;|xs4AQbPWMT?%Ew&H|;BhVt8aVVuwvhlGMg$n< zHz@_K8;bsrZV!RL0Kp{z`JW{<*!usUD-YZox^?)%0AVIP9tVW3$h=rA5NHBowSS!1 z2BC(Wb?BlolCuJe29gKx7wG|^a?^4qc|{N(Kx_d@4^otsY-TcXLFN$rr_*b|b!iy# z(`&s6Zym=89`Xx6_DqV`By|4V#Yb@mW~p~EA0uFL&?GDtF14*;?1#T%&#uQx`7WHE zMvR*Cv>zNaxF0tzTrwC~HgafQckKSTa^vC`=l)NtuZPe6oRs5z+r8B8bk}L0v6HR% zTRlu=f5fXwk@(@COI)ItzeW#UUU2w;sL1UOTlSh&dw1q?heOmFcU*9D!`zo|ACnv> z#n!*9HFK?7ls#QvXzuiNev+p2IcHw)%V4vS)8yr~cjIeMuH~0G6t7)co0E&acf%i;hot#%4TOxM-hP`Y4U#BXKT~$ey92_txdS(puR||BtCf-wwtvT`X z@oft0)~DP1ezxE3bMAf9{}o}glJ>P^SZ}!f+!EF|>8;z_0fuk)mM!BZ`f!y_7q3rP zZTB!%>CI)!dFx}SOQ&V#87{XR5fM9B{Pa|umPSV3ycSw4=&^c3>~YNsF&kf>+h6y# zKX@79V~5D_YH&GQ;HL8VPzLVq^h`Ae&%H^j7?(?x?cR=O4$B7PPu_cvc6Lcvi%E68 zS@!YvMNx~R83g{(W6M(oLuOrLP3Z~!!i`aeXPDA#^a)?N#giB^gbUwrE*YM=nKo$J zdY$6>3!`*y`zvU3)3SiaJJzx-OxeOqqO^ZbRtIGa`bhMQRv#QBe1GIL_S#AG@}=RI zeABCQPL)^-hp>3HvJ-LDKUSBU^}fCyEL$JA8#bG1y!Udzt0U-s_YRk9-^CTvk6@kd zcIXbIDWtl+i~hVq-^d#F`bb1mR`pqHwdap*3nmsm+kUtb;o-~l^|ce%bdpMu2W8BO zCzSKG4k0xhZ}z?ZaA*6(Vqx^_Y!Je0P|s<;&XZM(X^B=cCbAot>`r z2d|xHxBpj$shOe zU6U5ft``={Tkm{&9DzN_?}NDWsdvKpG@Y{XobH)i%C5<=J)?DXtqaqrrl*XTO~#VUEL!cP{mw^7dtTGJ1>p6!`bz1YTd*x`}&p}~@n zIiYSA{3`M8WIclWe4Cen_*~XkjiywiEXNZ5yyW9S_Id{D7rW55YEAQI3#J@y%9-Fh zWuV0Yd}j6K_YCIu4CD_S6V9nC&G9%Q{7Sa;2ov7kUmWFJ9Km+`fSvz=-k8{HF6FOy zB|VEP?ym}GEO`%l+gWHiB~92Y9y6X>^L*rS?QDTZ^GM;$+u7NROGe{Jk@=Qpkx$VJ zM|j^LPiiP8t*L(5x4t5Nc{;~;Zf9{>Myh+VHxa8mD_`npZn*87)6Oe-Jmt|TDUTd2 zWH-_LlQ&v9H+ISS;MIBu`&oREyP^3k?LhPi zVAzXeb}c`XigbptN=A`iXZy!f#1kLp&2%=s0nl_Tt> zjRJuM&!^{_lbgQX?rIENkgbtzqT1nrI>cSu(K8mb=c87)uy~xJv^PK7g+{zW8jHxm zJcN>|H$U*Q^Odaf&V^?bI@2?;*u&Li%n0F1 zQyGYyk)H-izXf+C*Hr&@b<6;*Z| z6Fy?)dEd~qaCfC0?SWFM#F+Baes6KboMP|LLp9eq@^OvrZ);Rj-IkL*xsGRC2rl^1 zeNed}a6I5)0&^5gy^D;#Dy*5#jCe?xKVUqdFaZ`7&eW`Q7i@?9YTw>WXG)~_)y^Ch zPTSnLIi6Bq6=*jn60>xV*q*(l$&5>Pk2(QM=>tZfvpIqrpeK?_;ys}b;Ddhc_MQe%Y=u_8&$tS zI+vq|9Qo_!0v;#A%EE&OmD<5}SeYGyy;OV7r0*io1lAco?$_pi*R2V3-cc4FI@s8F z=@{<4>>JgXLAvutl`I|u|2u5{7f=zP z5e=IOV{%uz3%0|4wQqN)yN{sw)y_NuZ%Ohg@;47&3GGKEFSCL zR!4u~N2Zwp^~_51NGAP^(A?0yE+prnsC!f9tC^ud&c9&I`8OPb6jxI*@Gf(IgTo6MHoJ}G{5|aj!1Ncri|X9 z%N~5?8VJ@!4P{H~rYU6#Bf-c-!IUFFr-1y3&dA*rU03oSDd-J60AJk($L+4rl-C1# zD^Dr2fxWMRY^f%=PvU3Kw#VKVxv)0uvu$nxpBB)BoMq|=bzftK?>vd@g%P`>G@ogD zbB9T1s3vqx_cR){nGJ>wgm>f)2=*$uKH4SpUbE?pvj0h&muZhCHI>1lpsOGsLPudY z-igEQ?WDW!6?ji9Ch43OMB{me^<_#4_6KP*&m05 zN9?BOQ#{JZSLk8%A#}^n38gDY_~_e=*C& zbn2l!HSydRv?!gFM^3R-3Hpi6)x@tt?S7>8h?@A_7qno%+Wo~GYT|+MSthE$6j#M) zz$qeX!7h|eP2e~i3a5dqVh+$NT5uH9k8h)2ce;tHB+byn>=RrgdArxSFO8Q_U)a8Q zd*7wi!;fEurmj@RogUT}5Ucaj{#l#M%US5O?z?7bY-7B-uJ&X&@q0z<%gTiDXzPW# z`Nn96_-4_AL!O>e3YSEjkBN=F!^in#zGHugCjO6Y}3_W!avW@W1 zrd01`%Cy}H_MueQq0PR3Ra$md{mRI0bsbo?vXl;6rVxdWgI{*Aln$3uQmOtGzigMe zWSO~S&AH@zx1E*WX0$Z&O!i!AVg^S80~pkM4`$jjY<$!d=4^DMwq68Qa#gwHYODRr zQ`@C*W9Rdfd}p+-kB`-A4ZkdUvb>~Gck265E=tJ|&I4?j8i$hTVhl$=pFdUeW*}%S zy`H9zj&`zy^80A>mFRJs{2x^ zA}_AFET67+%9yp|w8XNWImRE)6NZxk>`%$q_?CO;J4(V^S$Gk5Z12?-YPGVcGG$Mj zyl$z@?^jarnb&3s$#qwJ<;`msV6h{_-nH{#C+7!){mLPC2_Let?$@vIMrJY2uv#A% zFlr6aM9e9B9El6CxMNFm$Bt$P@C?aCf!~r>e6_!k{m|(n&{QJet9r#(+rd}n;M8}r zY3}6E0M9(>qE$BD=7P@ttBuunMDMYh9_jkH;y`GS49VpmUlpuDHaS?bx*lR5jlWo8 zSW=PcHaj;Hb?Pn6W!^NNv$$tD@)vsd7qGw6p{lYC)Vs5oz3VKlK8N-~uXvU1aj+dI zLhU?NwgGxOezgOG*sE-z@lg#vhdq+$-dJCmT=Llc@@M^PRq_6zFTjlE;E22to!O=A zk_)}t3)n|>c&gZGnP0hc*!NM=-FtZhFrRL{wdVfZKcchNv(|87(r#zfH>F)io@oV6 zE#Ly%S^0W1Y+QsLs zkAv+%5o+g|x32ra_^TZl#6E8gj2BA9dRzAa9nMPv9a?_erWd?Zo|K53KPDX_`QBHz zJZa@ky8$>f?&J$TMTsz={W18B_rCRSL@liDgW_0{=kn8qyW1o_MpItx|NbT=C+YI# z7Ybcdvh0c3B8fR7z>^dBg9_jqVv1HtTD0xSglq?W_Ancg7~ud#`4mI`MSM{{ST7PsZB~<<#UJm1?LZM~XDN0y^I-bb z4h-5+{1F)c4W8L&xS+$;A6aGWmn`+6-pkPx&NXsakUm4IdwrXDYARbkuT`T_<8uAk zZT=pqhpgM)TgH#V;OTKEnen@xE@+M#_A|kIGr9&a<&DL+-

QQP^%K3LLY&#q_SeTb zq`ccjChuq17VwnOsS@9Y2hbf<9P*mIaudt;biWDHl9iwQMWOSJh&nvBEImVdmiAnS zVTYgQ1399f;i5nc(}Rq|ffvGtr&Bo<2ZpcSNMuKFjq+*LKCUi7_|o{{O5R6U$W)pH zSKQL(Q9`YLjY$+HDA*pEyxK5+e7NI(hWez&H#9xQ;XG;Ap#|84rC!JqruG`Eg2} zEl@VNl(#PH&KvO_Bgf`5kKQb&%FpwqE)^BXU0!!Pn@CNh@c1&EzW$Egb|OJ><>SQ9 z#Z|=2cEOOBcjxDyN5tDWb%SinQSMI*<9AV1ig)l%dcayUDe?9D9M5T0IO|n78&;TVoakR!57S~?SDiKY$!Cal#nmL- zk>NUo-~RT&Q-tXm+om)0Dk@1bag^ekSCmx09OlMX`9t1X?yroIO^2MM` z;O?!s*-qNga{E-3S7ZBjK7CG!wY!;kr=;+JmRpFKxNybX+cX-jYtE@RmV;)30#@zK z!d5R?_S#j)O&qBk8=uJOcGO@@Sxzp)G6|X$D{(U@?5)P$uw=8k*`FPx5|iaBk#p>N z*7g@UYUcI9CtlD{+kK3Q)=PT%YMHggJTS;>_!v$gKY5SO)cdg$3}F^6@dlFWH#wd2 zC8xdOKFKy8q?S>7$VzKM$d0dX|3-|iS|al0muvavUY--i9jFYooii_^89Cy5Z{&Tp z>yU(4zWW=qW1}`{VJ*I1-wm5B68Yx}17*#R>{bsnm~gI>t(uLmKGcC>9Z-EqR7^%y^9KMY?pp3g{I5w1)G7KK*AIH*XXki9|n(|jhXHUq0_$5 zNr)tk zv?-W<(dqn^*<$H?`wIp3wQC*pEKY=;7mPPFwfUJ1V2N4k3nL*urN=l=drgF2!{qEh{ zw9MS&Veu~vRuXDWW{a&R0CcUO6?Y+oi*{i1RYuaj4_#F?qdrSV)gM8=^Pdim&~ z0g>;8hb?C23QMuom*mWPM4ye)(VA7ilKWyxt#*^K`QXPPQRAI#!?pS@h8C` z8@^rtGy?X5Bwe8zWMSGs#{ZjX`Zn3YjwuJjsF!n`g z`Xcecbm(flOq}$PVe08rrPRlv6VI!R^L-m`6}@5i{Ct)F|6}YOgG7hAZPB)E+qP}n zwr$(J+qUiAZQHhX+qQB0ydSsDdG*z)`(srlsbD56m5ds5jxlkmPQJ1=L$}0!fODt# z4F3Zf`t`5=zv;OD4GppUHsAaoXh>HxZi@}c??%7i1R%i^U!XpKyyYP~H1IQEouhvM zF8=TbSQ~{@?aV;r{M5K*H2PAeLUKhHAHG4G(D!AL;q}E=`nySwrEZ7LUyp;m29m$> z%>U_DLZV-byu^ltxi$q2>+9*_DD5VbKR?E%y*^fR@hRFrp{M6%s$EUfY>=S~^5^?Y z8QK5sqII$}&*WiqCv9$R0I~jimZu53zB89 zn?b}g(9D)!O369ji!evT+lAy+amYrk(qef|NL1RtdzT!z0J*y#jvH%?etfQ& z-^HY{CFjQ4kju>yGTKff)0eN6|wZm`rj+uc#ZKy&cH_hc*O^%09HQqeuuMV8LY<^l`QsB}X>JGW2c)w8OSJ z3FdVV>OSuP2`JV9P7Cx3k{9XZi3lZl3`~#m&6+@XmxES(fZ9wO98MYxIMcr7)_5dj zziT;6nhF#Fl*Ej>q7PV6`N{?N(PZgp75W)KWsFh@3@of5pmbACGLn)2uxurh!tziL zBu%{H`w zRqL(vNN1xoMQmMxaIC54usRFrI^f>Wa^|JTN@VByW5>IMIv13EtXu|LAO*8q1x(V9%aAg0(2#K{<_# zkvq_kO?&9)st`!q+D&pBUjS?1S;GjQNwFdodFjsy+UC*8RI^^}+rg&;ZHUakum#H1 z_j)Pxz%h@k??x5qQJGzgl!kRxflfw)&84uVF|ecw&-Lg+X^S+g*Sf#-AV!p*sj?xs zBl?Dr7)rcy2;)D0Q5kk-qd-xj;6z4d3bHhus1#b)Io7jAHC-ftoi@wVtWfav>NMZ@ zXoM8$a9@I@kk|@S_Dv#w54M`4jtRaoCsJogVr?!Jwa#|%#MKqr6j{@D#(O=bOmvZS zCYm>^&@5`#uB=u5G2-ZILZ|ncD%X-mwKh*Tm;_*nM9<9A&Ii7!?Sfn)6;%GTtv%DUj??1ag zd~&?xTef2_gEcTsJtOtcc0R3#xbMiDTvv?_gkQ+GBQGq0SU5Q%OT73SM zB@5<|UAr^ruQ%@sn{Wym4hF$v9z`ja;#w1YAx$^B!D&Mvb)+O2ny8i9C{CPR^@)~z z8j*cZmzO)y9g#$k5dwr{H(3PyV6h_2*|T46KdGb zE?vt6FJW@mbeV8&ZwsocE7FzU!m8Kkj!2t5TrW(wRg6>l`$4vTnChzlTaE9`$IQWa zQau+Pdio5byMz3)86+sNj742d&tR|T&kA*|7bwQ$hFsl@ z@_OIY!GcZLjA_e3+9c!Aoe;6JsrDI1oA%CX_?XJ6u*?TU_`q&BEad&7^n10=N43e> z-@h(AUzgsweB4RK@9XegUHVl}#DW28Iy#W)pu(a!XHTLp+9UJVM~NY6=80+)5q@Z!5U_nGgycSX|kn z>nsi%Pn%2*JkgCdn}aLXB%;rzrL~lgB?0BJywj(B0Rij47MVCho z-q53F+n?nR12Jrg6HSfv7P$O|^c-kMHoYX$7@rMI4XfC=4B5RoTn2YmW*jhGFd$S6 zA^JOy9Vg)8bm?5nofZp{Zv8C1iJ7kFTXc3`VGcbiOZXk-JFXlKw>qX(b%dP{iw;3X zXz)h`B!*~Dw*@W=!`MQQu0)k-0qoVVCq!Z@@<{jEe>xFegUZTrzRw(>VFXe&cH5Fd zJZTbns~V9OxQW#JU`z|;6}{|yAwGiEYJN$lw>crranEN+WSPCsDR|ILYZ!ZSJ$Im! z#52G5kU^QU492aYJdq*jxDkW>5}E^4Y#@;OydTWd|Xe z0J)i?Sx}Dh)7|_@CqoZwC5rOFz^4b$T@G8 zQ^Nl%^tYGs3wV-jcLM5eZm~a_=iOhHbT*#RMQD(#1V1H;ao}YU%eAfa$^l|+xn8Ob z4j^E<>KwWg20Z4?qD?L{=M#k)LSUx(^4X2Ak8!)gW^ysni(VlFi%mKS^Z$IinBCz| z{wRydbF~i#uDjTwuT$`MaCP~~h*0pgM}-;=_W2sFG1zb&Ywt#Z>zOsVajz7ZH!OBe zP#j!Gs=dHH$e4%Ez%Opja7@9PXedzYLVJxM zWpbT#%FDrnC$WR@Y5JDWvRkGAj&_q{h?icKUmnnd$c7h6`k2{|k5v+SC!Ak1Z?4E! zDc;Ed&nhR4q?}Z+bCBkik%d+9$#*ALK;5Mqz1%7N@a{1#N&1C#-vdn`df1p{Lh*=g z^x)Kzi`a=MvkTM?a;t;#Wt-l>Jf@I+jjj1T9*Jb%(s~ZhD8)D+a-TTS4+7F9exKKM zwKM;PtB)URdI7Tr2_$XYM&1=*5M6TQ*~KtGhjUW*Ag&IjG2Jj~62n3_jsXm|ji9Yi z?KM)z(+;;Q(qwyiiEBsrZd>JnT(DJi4spS@Vi4lmZ(feKlfJ_z$WefYJ+ph*eJ#n2 zY8{z337_u0n*(?2=`DZJCR{Up>%cB71T~DFhlkU_8VC7jv)duexER1?J`Wwf?oVN~ zwn8}u`VvaL?}g&Hjw~Rml|clj=pSYAvalIoJ0~&>K!hv4jsS0Y5dovq>ncK{((KxV zB3#g#VW@=r(V@6+#9uq?N(n51>J6MF~uifjzKF883udSsWQnJ)#KGBVScEjIl zsm*w!)gN7MUgF&8&R|$#RrYVdSZV&fn~O|D?(+?>+RhF(*z%-vEv)0dj+R{L>vNn9 zh1I$Ldww&&2Q5$CsyOg!>(cK+`riu{bab>J{Dwcx`>Cp1f;}R+uwOTOXQUCCXH}uo zpV=U;e!J)WQl8}92H_=s6bp_6E#resklY8zr14D~4$P4bslNCA*;U26(vCp~Ax1aR z%QpD}=}9ISdV3F6_q7tic2pcHa{s|r8wZG>lrWy}n?{wvmIgS7p*DFOZ z?dgzwqCNK4n6^9{+0OYCz7GMoYkCsrme-WX>MnXc3_bK*_o~5qX2TMT-j`7cSRso` zpLgtU+!x$#^YKWy>e8MH2i13m>`6Wu<;kB;M=pinbN{S%6$@q)aFpg(qinp_YVz>T zjdjuTpg(yEF^fXi%wc~Oj-~)cl)LTuqKL~Y;0;WDdYRqm8Ff0cB^6?7{NCiDnV)<1 z2e-L=JWm@3sQQxqd-~W( zDKI;C0x#s5r*yuvTVEm^8{c@cTK=BdzHl3#eF7>9Th>aQz(m*#(;t^y_@zGTr`N#v z_hdOu|4A?PD}M#wLBDbam{9JyNhk6?4n>8d!T&(;dMf}+Zu(Xjl#xb-TCsm*F^PFE zv0KEp09W?PvpNgmfw7sm-3 zqv*61+YI#v216yY#zJ{w~(>P5-#nR^YHk&<>lVX|)Q|FM~K<88Ps zAet8*;`k?v$Z%h|Spp+#8wQddv$Bl_@q1Q$KxFnwB0WMgCd6E_h-fF`B#luvf#LkH zKc0cnj1*}kwN+$bY#5A0hd{_rpSDx~s}Y+Bo+1sS?7I^xLV6ojB$mQG+T!eKj5=D) zSI8{b!Q=X9VgOb65bp?SJqplCsGUS>lF-S2^{)C-MTO>RZKu|mI7H_&5l1bl_!Jq@ z3G1K*LUY-Fd{*J_l5Fo$neZ5N7?t>#)l*3+Fk)ln_cnS^yz0|S4}nlFSxl6V@eKEi z?)L(jlVVq028e^#%A@(~%Sov7V&-j8=C}F{sD)1biX+b+I zBor&$zopnSxNAhFnEMYmPQwd9};BJPKal8Bk7 zsn}(gr|nib_Zw0Rumm)up$`XlD%AN%%(9$GHQ-+t2_TKlNCw3?lT_MAH3uq`NR2&5 zij=|keI2=xGdLpCNQtE?iORtdpo8Q6^>b#QIEQd{VP6pFhiXNLfnY(SfXLQ+)S#px zKnVmK5sXeGRCCpbd~9-3fK)7ru&TKbb)=ZwMu}LtHlqA#(UM0RtnbkudJ6X)^(Cfb+ z0-HKG?Q(dCPO;w{^bB^&XTRWo^!wmQ*b%J1m2C+y0gIM8$Khfd2D&HG1%OnZ6uA_* z-pX+&Vq4_<^GTu$s)>UMX!qAFB610@T z2OP_*U~v@FP#!`AO^WU4jvFkrZbSjbY(Ee?9#e?GEKoLD;tFa zSbu=2eBAT?1LgCpI{1I2eAxa+V~s0aO?#47M87a5`+e%F!X@004?9r&DARx{`C`_Z#r@)}|E z+tHlSI7~cDYn8O>X7ehca~qkr&l7(6-tVJNd<$c4TO%dL$FF<)!aDq>c|Mo%s(^!2 z=I@UW`jfrEyxu&&&*5DEpB+ft3U9Q(! zzA`wmC9cjuGZu#&F5|?|QHj3{ZE`2{bpivjQ!)u{JJjd&4M?>=By_xQT>Ie!RWpdo2ji1Zq19klN6Bs(oYj$klOZX%_#fkWYyIJj zYzRD70QAH0-fb)k0t2H1xAej)?}Ka)JiaT!OJTDpFB2m6CKG)oIn{;^ePKB?s$VzA z$c?znU3U|Je!aK_+%o)pd?LxtFRL$d;J;LK5_sk{i}6h+HLy=@Ox*md#KEQEi6gEw zjpH~#fRbU;#^~3Z{mlx(%moYf*Li#<2ou5#bBfm%aY|q@DG$;u0ZP{l?l`vz!YrTi zb9g$ay^QX6(Eis5ZYKvC6a!;MVod+|f$!h)PwwABUNbGn-qYs6`yjX@)k%3pm!l;? zy_V-fG!&{t1|&_U5sI546iS|25ogeHg)wV-q)88~hcYXs4d&$hi#R2r3&y;V+MkW% zr(%dVCE+T?{ksi{YZ@iC31gWjJs~}2oGINKtHah_>o$Y2@NSW(@GNV$6)2=0uBSysps#>tbewj7n47)|2Mmzbj;5C*&AnjLOR(-bY>44S)!Mo@3@>k z(Za=~#62?Uk?G%0%i*Bhcd%%=s|5)nu*;>SSmg0`Rdp(wQSTVhTsDq;`lEvN$&(%l zrSkJV;Ij-D;V3rF`bt`8QJ5?VmPG2LTFp5AUL;y&?S(AD7UIRn4Vsba%R)mZEz7`< zkK)K~1dHCVSrI;=x?$hBAo^K&gzePHqKLKEE=TzOn+!O#lg5FO1G(Q+kxKd~m&U$y~T zVMtct$SLgkBGJh4F_@9H&ha$7D1{ub6!;%1*Fxq(aFFBit;8z~8~uHf+NYD5Tbgmw zs1hadOt3BF8)-?76K+(4=B0k3R4A5^(N={(v~G~pPFUa)?a>U}60v_4J01kJ`+4SM zPK=s_CL)hRh2B?)Rm*QKBDnzq1!GYRXZ8{TEVzMPyA5*#z(9*b&x{b94e-hfCi}y_ zvx?#cLdqBiwc?)`5ta2t6YfDUr^Tbtd?B(T_Cf&7t0AqS8AFUAB3rwmPg?YM+6JzI z+~2k=Z4cW20&)ygIEo&o*yHEK*RsmIK>2UcNU+L1G5k;&AQG!m@()>^vn&BXssW#W zzTunu#6!Vn)AM@J@_NwmLMQ{h9xv&ql9NzIH!kTmZ_~jb05kEfYS3B!`hDToCF@s( zFkZ4Z)%WY)3CC7oxWcZH{qNWQnUxJ1=cN zyhp8NJ>u8+(V08T!oSS$Gtk3Uk@-w@mfR*zYt+l7Qk}p|^xXP~yCb^JVDIi#Yg+_0 z7zbYqVPD?Ug5#(j$dRzX(38F&)j}zEQMYG20AdXbj3OJ zG3Yer&9k<+n0RriL!M?81q)6S>NU?CZS2vl?2^dQvC+(NoccY#t5vVU?ci0J;}Q{_ zZQJo%tkLH*)XVtwHGFUbC@qtmHL>EBj*sY+Y2 zc`9ojp~GIb6Dm-5N|_m|Iwt{F^&k3qMtw#s~?QN^q-=LskT ztn%Yte6dv`sW^r2LR!NJPe3FK@h<243TZOLZ9$A&1oPTpk zy=#Fshly+V0{(3HG>HnoskQBP1B&0MiiajBOHLzP(>5Xz&X}YmRz6AE_)WyGU@xp+RUppNX>`gdnzWf^6s~9efTS|L zKjfF*PhwTjy5Q{OTzOSUwYvCxHzx3k!-W;zpGmDJ%d9qyN92NXD^1tg%#t| z;MVwLQ|0BP&naf|#A#rhbJtsoZ!h2xEPqTer$)1Vp_2a1chejUCc|mME=!l~=_pC4 zb=&Q47zux*Xs(q{LrZ`7F@<2Br3sI&8o>E=`zP9V?dbSo%0(qOdT$)Z6u z8LTrIUBN}wPk+c-vO;Hc-Os&)WE>3KoL{IMjW*v?O|zOvIB)%Xaj#USu98n}dIuuT zd8%nz$~SoVGP<2WDNZ^-2NL?Zf(GnAl|=`d68{hq7<1TD=&}gaUj0~X2&q4E6y=xC zm>m1pe4e+0#xtRd`o(h6h*Vj|qQwBQUOD+~*k51ru@QnD+e!E1h`(h$EkDB3cw8oU zU3YA5?k7AgA7iicV2{zj^-~YiIjaY2jxqqZY#E;hO5^W}m7NMYX|fUzzUXK_v!!V& zpV(8-sBcxF*RO`_KHL7?O>i3d_!S z-_z&N@JrO-Dq@$SCL){}>!F`c0wTIz9R*nE@P^A)p%@Oob6XpiI#{LTOF_ts?+&Cs z@){jJD&#<-S$< z^`527Ljuw&pC+>+T9OT|9axiz;71u-g9>poD|7>$Fammy^O}+WLNpzMJrBj z1;Tu*x@^qejGFKjJW_#xjleJsZ-I;OE5}rU%y^w_UhoRt=lKag0nS|j%2{!joCZW3 z?R&FM7_wVtOdIqX#+8|p=A21J%L+8;s*!T=W|?zfU~^Nj;QB&Z6t$J7@-h>ozlL1g z8E6|*dBMB9)?0C+{ZBq3(LwOQOx9?JaA!263=}On(N?>EXM~N_qWZ-&wxNzNPjM2a z!B|fk#B-eRmbS1>KFMx3u@hj;$4brWDL1#W&FvVQQ{jdVB-FDw66rV{>2lIclkNB{ zg~JAmSui<@bukT-F!#gSk+P5hi9SA)QpcR*ZL_D1CooHaAh#_bvG19n4w4Vlj0q4b zS-oKzARi^xyl3lnPJJLRUX{QcNkaG;Il)r{4Sz5yYJQEHKn$4OG#a4WL<=RWrAnoK2h$dD%wSu55OE*@=N(-7k|Xt$z^0VvNb$a2Wj(&;RQ}{R2bGg9F;&^K8D9@ zEUIL?kK2Tohy_5>cZ>-cL-jHV8A0RV-^J)iH4+aUB&Je`uc;7h@gEpoT%d&yJ=KJn z5hCMja3f5lf0{Xmcs~ITui1Ar$nk-Yl7>BjNscG@v2>`n2Ir}r=lvc#67vOLA2r15 zrf!7uhv%08jz0t>Wbh6Lg^i#TwFw!ib-oXTjG(%&KqK21PHM9khw1T--Qv@;3a{ZK z-wY&U2^d@d>*om)`4$#&tsjZbW*I(&5|i~CXwy}Ex^}J#r|IBdcbd?fb2@t2vg%-9 zLPpQ{&W;IgmB|6<4MN8DJKNuV5vHjX_kgab=T%`?L-P3G_|GE#(Hy@t5HN=3#sv?d z1Yd$jQnL8Tk0g794 zW$!iamf{A#^mI1$Ezf#nZM%)<-neV&rx`59yO*rV>Ft=vyDe-%(bqOzyNQ=ik}LhP zTN%Hz_k#Xv?LDK#@STzwE(zkr!?fH;({BGYSH8;}+$Lo}>eeJ&n)@^hjjwqR$%9US z{aQ7n{=vmM>u*&P`FFhoh!qLmUe%52LodbQ{Nb&*_1aP=2~Ak}ckD5<$sypJX5i?9 z36;ep;dSj+u+{UnoD}Q?o9V-^EMI2;Bo^R~%D4Qe}`dCiQ zijP#ITaACYG}fQ|f%%+G-_o%}43>&1Yzv2`#a9yGs8*N>Lj4);vIak@Q>5*H10Xvdc>H+aKFkPXW?$kYQuB=!G2W+ z+|wxe(GYw(0`N2iSMMrGv&nOm_JyEY!}#Ey}6i0ytNu({{g+Xw9?GDFFkO-&JEr^y?^` z&}r)ZXi3c1do7Ob9`AhVw4b^Ou6rEmHcvf)tuNYo^;nyy6@w4JMJ4k6&6xK(pee5U zb&GIh*@WWClGPwB<@CprX~5DOh$l3K;L6U|{w}nH;fkIkxv_}02H}ck_7C=9 zUQ#Hf)fY)<55y9|zNHVw60s7yp%W`SlZfHU0yAI)tKU0|;g;B8xTwUAwq;CV9#}Vm z{28J&B^{GfgCh2ByvG1ism$mXS|>0YbZ}$3gmFQUl%AVMd4h65vE0Vw7snN?Y|tx= zCDV}yw?Ap55;=5nUQ#TkQ~V7^1Xpwe%N3ouI4gAlzY*Cx6D` z#*qI&4l(~XJA?nV9*FaQ$kv-xH?6i95PVS`KPH|!`BW!*8${bJB>a0L^^s4kxjT{U;K#nU)dLza=$EN!D zL`Jb-w{t7&@ORCl48x1(tuY?x?5lL#HISEM$Ib)64Keib3^TjBRbJv?=zFUO1EBr6(tOM4NnF{Y8La=J?D09ONPpMI;FPXtC1zhJ1@_sm?}c8hi81V7$Y) z=_bR3!8qj|(r)>&(zyF!sJ6C%F#MW1e=?nTMLL#MKDo-+DOgxqN9*h`N7 z^^nr~)24gqi&h$~HF`161kjASoYG3(|5-c63@-m-XXrkSIba(|bjkvUhmyc^qnpml zak8TSK{Vfz{IfgBH@Sh%4Oxn9wf4S%cbr&Fk?Wl*#g6gCU**OMF11>0r1MYb1k216 zHI?r;t6FH&vC@Wfy*9ZPW^iFTrpU3w8Xnw#gZYx4Uf$iiCAo|?zjzigbKGff8nl3m ztho*9j7`QPV_d1JzQXh)I*M;~^|jD#d}(NbMvuu%^lH}f6L1AQuwkS| z7&tlqM-dtBdhB7#U3ah0EFeuZJ0u9;^W)!O(9i$k6B)1xW`2MvrHG@i-qFmJ*Dk+|H+V0A*cVEi*-WEBweZSn^ZbBLVS^p4s9ulMboYo)|A_up0Go-rv ze?Q!B>UaF~;mkAJT^c-k=zmhT_sdT?k$drS^ukY8>451(lKCfulX5Sib{16&8KA_`nxnPBAb~!u1Q-ar& zGPZSwFltOoT9+~g0F(U%+;v)LutWej#{B~rFg1qAE#G+dqfak3g|GiPs>fkm+Ufc& zf1uvKiP7&(7NdOT<)^@yGH?`5@o&hofTPU2+rPlkG= z$a7IFG&PdjALCnM8SN2`>@7+8hoc27RaF^iku9qqC&C4nSj~}y9XEoj14wMQygG(QE-~q1-bfH<4U`+?-P#JaTbl|@U=065ZpZnpc0^ms5h1cWOf@+Smd{Ch7s|Jqa z8g)(KppF58+tS7&QFa(Al(r+-Q-cZd^ypoHFhB}YuTO(g3}6Grf>J~>3t9-y`72K~LK7M8w~|Cy!2M;jT~Z_xw#>j@)mnij2YY&QWn?m;ly% z-`w7)b$)JJuP1v2b^BQ~CQ*EDCi%JA^24@36FU2x(N(U4FzJ2Xoz6VjzfSI;^8jmb zeFC^4>4N(2iq}SA*p!avTz2vtaen1NE0NZ(7*%qh z_F@>lTB7SW4=%v#*~R0t<`o40l&TXTyq!~?q`X)?UnO#?`@?kVJ|el6|D=l9PSUH! z3@dP@WChs0ZMD&)=M7HUO_gYKQUw^Gn=wvfRb_upWH)3EH;L4{!akrmS_&|**gK|R zo+5qzb)$Euc1>llh;J!Zp1yQkjvi(ridLTaFg;V74>{JQ=a#kv7j#Ko>P-5V!lV>K z9FE{XnRcWG^~|NNyt>5cV^~~voNu9Xai`CBwrk~(H-OM6m&!OW>9mF8Q~cGU6e*twnB=3g-O3-#?QD{j_vP3FG>a0cJP6M(Z| zpqjO2x;kuHV>4q{RgJsHG2Uof)zvA%SxG0ro!!~_Cn=N{H*43iPAEoT4Xm6AH&KLs?rR^{+-SX>14iG{e*x$>JL?p^zH1ZI zS%|1RFg)sD*^ROswH|CMB@eDa8YSP%@b&xo zCgj~M)yb}zOT0>QW_R#gXoiVKX>MR?GUrt`^bUNlg*;_MVn3Dv9H?p8lFI@h7=Fed z>8eDBwZ}e7nJ-$Lxg52hGSjDu&q7TNj^#P+2w!?WB3GTZR(JlnZ@ua^9vlmxa+{&A zPAKGyr|vZ4O>Z6^w8zwA>KRLsBVlVS(g@ec?ELnX+x5L%qQLG`wVCg=;^RzKPc)su zXf0}kst2Kj3DPmem`s2M0lPOzOSr3)TuG1WGxdVhp=eKB!={L3u%iqOw4Q5p4XgH?s8n;*B&owcLS2iBMmPWM*?3Lf<>*i@ z?MtCRx`(HqLIql=m_j7$;i&cyurp&R!v;2Up`peoXD`4Y7B^&=SLJMAS0=^ju%wDk zVbPM=QMRDEZy}PR)fr>eTi1E?anFnCm7)XMI=H}dw?{KxqaEo0J(r^y;h?7Uu>W*z z(=@KR7H7AoC9NBZv0Z)iQgo5Jf^=|e&Yq^!SwBm|p0^Stj9$0JH}zpE;K>bf5} zCzpBu<08<%q*SUE5Sw76a}0(EdaTeIUsF;d+42;=yh!3Etfc(4jFK?y`6xp~=H#eY znKvk6mo4q)BI)HOTwfkL(`5bVM=P#AsV5?iZb{@EKKl#>j4D@BQf;XKp9LRSJhuJY zK#_C>LQK$ih1N7(FL$_jss|`}wNxv4u|W=t&Q2yeOAFhiwAVzf~;0yn_sKphh`q8@&Us2 zFG%oWE%CAFZRs)(xS23gZEj1r*0Z{Maq9pk-^X^y^O9K|Mzk8~H;y@5emBO_r4jN* z-aj;G{+T#M{6!(ir*TZIJTxf;N_prS9K}F*s)$-a!eo>k}^Al`%rK_lF38p z!1cvPO?mf@2t;DnkT{%#PzfK>c+ke`>jRVZIKP6*sc;k=rWZ8Lp|&#Ov9;}oAt;w- z(Q&HFOoXI|VK$i0=W%EC22Uh5(J}0yV+?}=lr(m$Y&+-CddPJVI=$griWy#fVz~nO zF2imBL}6l+i}hU|wFM-yMBpI}NofgrI0V)k^tO0o5$37p+!QmkS5;?xa@ifac-+y9 z9xkhW#Nk3oHexc_eI`%v1luI;oP~&OB-eoKd4W0%a{O3WsYF}|D$bOsaTpUS&NMD^ z+Elf|zP>iMvZlNvp>Z;nMCGKa<+f&>`=A-I{;+Vf1HRG%e_%#Z<+ZzYjOua~0fXV z4IERnS^P(^tdvcRNnDge2gPA=6xuKdNh!7n0%FGjK#C+M2p=Ix7c@U~$O9Y1%0{+5Qv^Cmz{y`5`07_g)Xa6p$F~3=Gb6lR&D$ z;?RTQkHFcJXBYU^0fBf8P=^@?GQ3c)n-XIrL#lknRYdkPHZ8dN$u%|-Zk8n&%K z+va&)q z{#a)vU+A*O)anhzFSyi5Pci{cdT4upS&bM4JmVFuts-iCg(Z?np%wBXiDE1HBNjDc zxBkFHz0p&y$^(&*26Ruk6+a=^ch-SL~q?Y1%o%5yr1)v@fv zH*;@=pdt`hvpl+XtHXU@OTpGWx);8?%o!1mOAJJ;-FO&~uL3&-9fI4hCc7c14uEx$ z?qX#KR@@_R2$ud#|H%ZXW3O$BS5u-Ik<`RuE*Ys(vTZ^Z-VDj*+F{p!AbY4~9E9&a zGFIc?I?Ab4PL8wdN$H=VbGX0+cT^1{Wzbh31v{WxDx?R`O4)$1C+hA3KM zS-37g_p&wbfu-T}#hN@KkvPtSL$Z~6!B%`BpPp>|!WG{F`wWtMSSLv2anQZB)83%G z+MCQ~>Jy&Ip5G}{+Cl6Be$U-~_L6&Wr4fPoLKyXeu8>Mn0?3zu`)t>K_b%RaY)0CuZq1P-yXg4Q6S>^B*u-J|v zD5TZfz$hvX$(?<}YeWDdz1Pgk#h}?^dP)8ob~_5$IdoSKb2AN!R3%3V%S4f8DdM4Q zF)b~}3u~oaR(oMU+$O(ckbY;sXK>5_zA2khvx!C)*EDvJx1SzDgDBR5c0Y19ZX3|m zT-nuAusXc~aidP?taBEbzkRqz&brH-2*Ja zr3rk+EPrX$w;o>UjLUh9d&edXK>?zgpq%&O6Q;6_=PDX<(tO$C4Ew4 z_u4(~dWiLXOLykpSJQi2_ZJq5EE-)K*eXcEEr({Rb7iy)wT{6j%SPrqWR=T8&+Q$l z3}>8^5o@HeOvf&;~^R@ncBL@VcQ-);8ew@s4p zvcQotDEv$;m|A(X<9D?)Y1a~sD`i)&^aO-&^h7DfXv-O$y9WS{zoB}6z?o+~(EbCL z!1CXGR{u9Hf$7)P|9@}^x|?>y>`1<=>W>dVgL_{u5)eke z{{nS7)dJ3gSj66@W%9NY9$DqH)F8L4!^Bx62@J*{^G%nlplD_czXb-){vbe-oC*eM zIDoUobWMf@Lnj!P1N25`B$!j5LBC*aesFpOK|M|Y7!XvM_*LO}=}&J?cBX#MaGDJ0 zN$GmnYf-e016COc@&>5#eY6O%4CSv-|F(nrSE%pxE7ULj|AhKfHb|CgAePXJqM6Rj zX*P!vuu7h)ZN|m@X>?o1$+=xv;iuz^_T)b1BqGbx0nD*1CqeWcz|-Qu=@~C1M-r?; zn}E%jmU@8FOV}2>8sk^L$X1A0)fq9&W-P%3v<(%OZ$Thx+%(Bp3`?R4%;ld*v?4a| zMbj=L$+5b984<#54hAC!ahO^^rZIx(kcOx%L#1?%^J|f#LrOlPBQL3QobloS^=BeF z#k;-U$d24k80#1OYpFm?mcD2?V6i5A1Y^@kX*{?^e>J>No1ll?ZZi-8WqF>-Tp}gr zK9jVI0>c998a4^qE!bpCvRUzfk^+(dG@dm>#ys)zUjtViHUn}p+j_rZ>=~W<`pP}n z4Pc;^QdC_$rcKQ707up|*4(v5)s%DKjS+Xv`VVBrOQD zYK0Osfn*u(GO1AH`Q#N$^7QfbWP9>#fCvP9#Lk4ITZFz1>^LSsNHY|_1?NOu3oN4) zanHn90p>s2RbVXQg)we3Ey7lh1})5T3T+^qsNd8gR6a3{=!St(dQsIFoBTVe2pKq~ z{_)5U)-utvb!D^kyP$w$BL``Cdj6p_T&0c*WOUZd8aKnY?_ZCg+xdPNdOyI?$N>Q5 zVB0V6bp?~bb#<#l%mA7~dWkphYsFf-U0GHHpnt`}10f5x`jSCSqGL4>`-xXwLhIwr z?=!L752)drDtJ9AcwZZy`^;de5#6T0aTJaczg}1}15{C$II}9H#Fp^dYC41j6_iz_ z5aJ-gF;gkwIPJ2%BuQ>D9E}Ukm8ZR%A=YavL1lPWpFI6rI}nZG-KlLHC|(PC**f>c ziK50eFmki9_*kk}!rr8CVcrzRq-2y6&?iRR<*D4GuJxE*?UK0e!@A?quXYS0iSCK8 zyH%a`j>{*VvGkaeYHMhnvWW`K+=N^JuIqK+ot?S~I=c`Lu`Zo~<5z}Kumf&J5GWO{ zgglZIVRh#bJ9Bh~(Q&(Z^$RF`Z31AYjBpbY_&Lbv3*3Qksek7Bek{g84VkgGRosEp zsRqOTiKT=iVxxP~F{K4ARg6v9fXLgt_WfZAxQ~rn$orB>-Uze&F>Ryqmxb$p==_^I&(V(RVW(Mp9{t&n={W!yB$v?jB=N5i~e2QgtNIY=T zfJ@xV5iX#pKNdX51oSe?nd};W{*MMMh^o6*6j$h>LF_83$WME}0wMvi5IT$JmCduut+4GCK|%slpO63-*^GzM*Ht~9V7cjtI)%9I z0#be@)>>ml>ZxnF^q(raQhe*gnUsHxVDP;dA4}=KQBQFyr!uS{Y zd7Xw9|1|2J1shg`+Pp!PnELi1dDjP1=1X7yR!WbS8*_%N!baR$SLf|%9XN}wF>@A_ zfU^x>L^bMwE@tUv?;TO97*i>nn}#=lu>JZ@s>9@cb0KA6Vai@8#e3YUg;d(XO3o*+ z?>SM0aOAQ9vKJ410Y^>W&~2(0?`?DxtZR4udN%F6khFFcUj=yz`47@A>4|G!O2;y2 zP-Zf$Kp)LOPX_pFP0T}|@WMpsV3$3Gk*%*I5tYlZ>~!!Xcu@v&Jq^I2Y>HR$(K9CU zLfH=Y?9^v+{pLeiGPvvY2Tt4!qB=U*JWGA8weO;Xhi7gZM{9?@@0weY(PD!qPlx%s z$c3X9* zwnOl*weG=D9*bVJN;TUym9e=T-IXE%Ha=9`jA_^$jAao8+KQ^I%V~1-h#iaz6AH;P zrchS6(^UZlG1M_q3qD^;pm3ew8Pf+OQY?TGKDV1seMAGB(c=ps1E#F^>$Z*+D|i!B z@n1yr9H85JS`(klogB3+xWh5)ea++Y6Aj75u=ecdKy!5o3%l0^L=78ph~J6-nGSw) zXLvKfipzhg>%iHhSsWpJA8FHpIM~2DJ5}#=KRtb;!CBB8*H@v#`SuLvSe@SE|^cdzVx80g|uGj6Fu0r^D=kv0aRy8Sg}F_APWyWxB$@ zlF#m+w=r0_NzEP%X}LO2^d_NT-A;y+FC?af=7HrE27RO%FT!b`TwHNb-j z$d%4yJWT(;VVj0*`k{7A+Py_fJ~E;APi>Xcuta0Wc-K~RBrE@fU!HdrW9x#$wQeZT zk3r&$$J?hn-#7{)pqzrB!*ruw!Y<9=UwIes;o!i#j2S#jyh=UaQRQ4ypGW1~iK*x0G`HQX$eD@1H;l`Cj`jERK`g+7ag647B} zqpBG!1}L95c#oaMK$Tm17Y$7OemvQ}ye#UZ6Z`~`rRKk}ht;rHC~n{^R;?9nDfNS` z4FA0deNjhofA+83-^@kc*@NVQf9*bgaxd!V>XUJNeeNHA_R#N9Ca4gNpJbrWLzr!3 zyAe;5Z#ZYduKT@F>Bn2$pO$gj58V+dc26|LTU=ML?EL_yASmtqC;a;VXsE#S+cW0> zhhJkif9<7K)lb_6Fq}NVgMg+@e-o&Ietj*HiZ)OzU;g>RQu}j>kV&bR5?PmE*q$Vb z%;t7oe{8`PM)PtHZm3hcoABPt=>PTRAcSSy7QJ60kvJR|CwCu?Fk0e1>i_!k{8apa zZ%UB$aoNdU{vXEPAw09F>(-8K+jdfMDt0QW*tTukwr$(CZQD<5oO=JibG~!Ejjz2L zySeVU_L%ob)iK8J0x2dlU3g`RIvWJo5`OF-T%O)MiLd*9*TP;`2kh9%Geq5%J9aCo zyy)HzaMQn^&NTR2<3nZbHx{_UL81**`a^&m+r#xqc15!uW3#h=DTZ$G*l@mpDs!FR z{YRx>K?IGD%X1m^#4>jP8ytNo6Pt~obRB&yOekev2$|6YWrjCRYYK@2Bz3zKK9+WG zkmpco%?JTpedmQ^nx z6y7}zR>x8%?Z<=6CXWtD|4SZZny86Fzm#Np!#;_s6-(1IPFD8aj)4#Rv4o{t{a=4` z2!)0GlcTsCgbI_8ycw@s8D=G zo4hv&OG1<#*Iv8{Jz)p|8)`T~-%C-aKSnaqKx7^XvktFoI&l%?xLz4QOSP9Ci;zKl zR5BTHCQ{guJlEEdN4h~mWCDn|xX;D6Q0+ImuVp|qeyvblzgnYJ-+X!M z{CFvZ6w_fBQVV1Z2+s`pNJ11{A5%?N!e3}od{Hz%j6&^Ot$ZNoLf~5zFi>Mw1M(yT z7{Nt<%pqN>a^f;2Y={wD%(E1IOre881U53E&q*vy4I;NZxgz%q0sHZ`QO>mZTu^mV zSF^$PtB67+vuak1gx`&Oa_b=EV2c#DLM%kcSU3Sw!`WfnqZBSo3DD!GF&;oSOnsin zp7^0c8h#9i(A2;!XLsSQWDrwUUN%X{=4QG59?%E{#8c2b1rXpAS>ajcMmXa~7nyDe ze?28v!{4T8B*q$3(4#(HE*(f5z*|OpDZYm-r(*w*98a{yZeK#i@)|T9?u$Rg<}x*p zY{i(fwRu2Igk2n_-oT%N_}mcwi20&pfo9FUZXFWx?(6{$qY-0M-;*->B~!IjTBNu1 zFt9T$)c3?X2jGkUa1cmw>?GTyp`el1M9@zJRwnor*Fkb5c= z*CIR2<#RsUB<}%OgXcL=TeK~1Op2(<-NsU zN_7Ml`|MZv&TWrZQtzL)8}F85%x#@9F+=52mRs{CEfxHDlJN7;Gur zPiu3AZ6~rQ=w-6>v*BsuJxga+`Vb7+hH??=Y@>^zvRqjfTUgK_ zk}*Z32v74$QH7ZBMwIJc?%TtS3g4Zfmcv=Dik<~mZE~#%DU1bfUtWXn?ZB&YxLNq6 zHmA3>+3Z04k4^h#iiu7TLVcL7eG|)E#Y~EPSyVR-)qECd_pe)V?4tD6876htR1kCX zkaw!STJ)@mrr!1a(Z&2txVkB<)8~mr%cds;CGN9$#Snf^yno7Ff`9jOjOGFJd0uJc z(zj+OkW)(cw8~H-?@+6lV+@;rXk%8I150vic2gA}AHblwIEx5O)|379#*oI1hvmc> zi0U<+UlAOT9yt1d|CKsjfM;^k=1|Hc%)QApRVARWiu;LXk|44gl+K0sqz_?w6^ign zQyKyji?xq(Ie))%_%FDAfiE?etRu3!dw(Z)QwhnWbo%lsfkJ~~yesqQqK(_A417@8~po9SIWF-Zy(_*Fe}!LT63Qj$2+#vZDdXaNLet zJAL1)gMz|pm<4KLVBbNQ5_|5OP1rQ5->;G2 z(z^w1REg&-9CVs`(?u@@phIR^g8k>XQJ<~4f8M_k61StDBR#TkYF#Mt249h zjIIbzMTD1n8V$GO?oiR9%zR8mfC*cwbm5J<=xb2Z&Qx0_R<(fyX`??BRi%M-P|)Vw zm2Xs)>6i^4w_sPgP=@bS)npb6?*ud8ESVwbzNxzh$tAR?QZUL}0IlJ7o>+t5gYDu4 zdc30XyU@?lNBo6K4D97FUR$q`zk?7TF82xwsr;EbvIptiYwC~LUlUd4kn}n9Y{Xn+ z?W?ePcQ?XpOXD++YAci5Nd$txkg@FbF$@H}kM~v!1Oe>QM6-6z?-r>DA#6uS+pq~e zK^+RHx#sy^AG0#d0E7z{*J{tQCj_-n!+%K^7a5zDqdxdWJ9j+!SJtq*ivPD;^_4YifV zYEa;`{jH{${Qw}HCstq#{+S>bd%Q!>3I~)?rhimbLFq5Xgy*<| zeM!C$G|I7Zs7J`)k=0acVQY11k5Jq;&nu$=h6ZV7G_syU5wy&S!^v`z9GVk>7l~m$ zMhvD0g3NsR<7-}pr_&RXD(zwUalUbjfvhslssaNs;lZoUB+qu1)F|Lk=9>9WQ8kAR z#S#AxtS~hw%D^&!8KpsV^3*qLg_s~KFc-!}i1Fcjl?pg%m1|d-M4mc)tj`ktk&IU9 zr`P7K=HRQNS(Rc~Jk&Z*8wxF&MpKH;%@Fz}j-Fwzalt}?n+Wn_DmYcG!5T=h>!=U# zr*U=}dCN*jJ(OJkOkA30qY!mLSN7jtG)vME`^3?OoRDrxVN%eNVT@}?qBLA8oty!I zA-qBROAsUv$n>OAQM^-8+ua_ep6H&f_>KY+%nU)DiadR0a}bLD6tB9)McF zYw;hX)Zr+o)v-pBRavlhB*c*g^M=hZe?un;Y?~3^77G}PwIkEYM^J;wKbC zTgL1u&-#9k5F4RS^B^xkY8SXt*ufReI6`$wz{8Mbjoy_D9dMUFqr@mEi|;5_h6&YZ zs4(5=5^kcU&rh_%32dZD1}5%9w398EyghDxz78Kou>>g)bbY+JZfSN-bb=H9-~`da7yM1R9zf#R8cZawg8cICO>1WaiowZn3|_Pb#6@pDKwef1LUzTymT z#ErHoVKA6?=rzL8L6B0kyMA>!9z27rfR&Vnc~3|bD#=R!TqDO$BH~#~%f5kf;sqm# z2}rBV-)wSgBeR>DUrg$tCFN$a`PZ>wbSGt&TYV~?>jerLb=;gTbX!lZKmJ=2CkWS9m~IlNJmaIxpayTEVYqhUcPrMA1*8UN+ceSV z6#cp0mYOLA0H=T`Qg%Xs^%VNWr731&JaO%#zJ&mn|5lCpOmfW}IV04~wI-KU&&mli z1$647j$hn$w3z?LiJ)h0-@NXMyx7|pVdFq}pp+tZdbB#ggV}7%zaVK8O_NZgAy`#321~5tq zh^Yy^xH5w9O2VxtKmnH1wDOfuBRA$c!zi7%|KP7WxmZ9{S?IE?Hl-7dMGryM49~M6 z)3dIM{+g2tbF0s{Q&)3T@#=?0Bv4fVtZ*X3jMD&b{dpEYCgKw;B@IBBdekH?MYVY2 z@!D@x+9V7hrEP%PC3Jkbq^E|QXmy;)7~WX&+InHca;drQBW+_kSsLOp$9gRDFP#PkvhqsnssT;sH)gRD($kRX@*`L`TpUsp>)9s~{2cKIvRrrc1RL!HoN z*9T{zyq)VHs>@izY$}5eV3OTcD^(Dt-JQC@n62z`D+=WuB{~#U!~7oysB5%tyG?mz=HUA=QB^}%m-8LBAt(~ zDK0!R;^sjukzSJp5SKamUmaNgD`EpjNsuHo~e-Jd3p9+VkrTHvjb?AQ2WL|d$B z8lX4_y6>@1a15VRzn_FC&2A4yDuQ*cYBaoPpZuIlDkUpG84GbA~@F(ikCN)|ufJ3 zM7Y{!B;-p2k~D`3Wg9=2DDMpIjrYO_?XC4a7UaXTrR6n?)8a=Jy0)n-)#8WRUoLeP z9{V1Y6ZPyL+&F>PqgcW|u4uNtI zJ!b$Vg$t3~?KE?l-|W^0)C)FZ77_(VZ|-(07>trvxhcs(zLmkJ|EV0Ng6eA!4?ud4 z9rMukjg(=+wMr0Z_{qTRoX~@7Z&s?<7Za3objkL@WNhy?QzaA!g}-F#wmvuKRU(mL zp+_1C=q*~Zeguy>)qoqWP{1YaK^o?A`!$j~`cM1^e=q!t;(uXCw*Tc%<$ta4|MOAq zKLK_2o9>>0S>Tbv51;_>l2ebF0{=I^9V3j$93aVWLbdTw2|}sAEy}7M9`V&Be^40z zl8JWwM{-)LzyD7P<lzVoQoM8 z`~#GLHY**umg~C}WFn2hHghdpm7}dn0*W?AV2Yqb{Tisl>cuHm#!|l?!w}|O;bc@A zFGWw<>I*?F$a}5Js$VOUP_RRqmxFqQ1RjJG*ZCW`o3Biv3R7nU6xMrXh?U?*S=ahP zYWQNKa=;hvi11jE0@_3Zc9q=sYI+(!Ka7;Lp|Q1hhP2(yUrsWgY;|h+=2d9)sw>umP=MAhd;ZZFZ;{q)N>B)h zS;-ZtmUsma`n@@OmH1=|wYX`GkQ-D4YJ4E1m{9~P0Osp{?mk?ia!Bs;-vxXiho&Qt zJ5Ny;{SnN|WFgN?gY+ieh)p9eP;f#Q`8rU$et3|2lt?>YB?SRdgt~GPjC-AV0%i(x z&ExNRDNw3&x*}+0??J%kibOj7CxTkR-t6%}R+4npO|J1;qrBe^@3w1qy+MQ##0`U` zW?=#N8VF$*%}rW-@bX=3GmbX6`9(0Z0S%vhdJNK&NvaE?{&2SvB~V$go5XoD%1egy zBCk|#v>@m4hEmNmb+Y1G0Yq&H8X|8ri!g`Lk#?IQ)By*M#{p$+tOTSpppI$i9zxTT zl2jC$CxOu-d#g|@z@24lVJ^c~mjxiD%^+i~DzDTULc!jZGDF~IPy+KbmgE$!U=~xE zHUYqJSzf&TaKIKCwnjtdS~PtD>>AN=C^WZe%(R|Do=2c{n_>98pz**Mk6M=q?6-zN zqKn{Dpk_&W2CUM4A|SNHZ~9qrb-mN40%CXJ%1_!qCyam36dGg^{m1mw3mgWvV{PUgW{9v-vBaZahut4B%pL+sU6t)1J^9GJ0Gn|3 zBOA)!gchCsr4o}#fb#7ce8heCn5DrhsneQSiEck_rdJZe->k7C|8e>q4mdZJ`HjXS*<#_B?Fy+mBfpizO>mul$%MgZvHjs9-xQA2Kb$ z6Jiqrxy6U6)ulNB7(RmJEi|)K_0r+hfLR(s4=55ev zgW5Wz`=5t=R^pW1-eF>?@7r;RKrWEU!@|!4Ib7lDtFW(}0Rw(Ct75%lfHy6^GeL26 zlAD996Szf=9EXYHq*tj29PpUXkTVF9+Q}=>Q5Y+@!62%8BAhvl2=4M-Fe|u5+J&cc z{Y=J|+M-2%pc~yKC-Yl3*?|9e`<7*JTMhuXBaty6Vn$8+_MZmfTd^+)Yar&}8B0FD z{beT=${M288n*RAuc|`OuMR$C{raqg2tHrd*(vy8Fc=sGlq>JKMSFQB0F|A}cvy*) z*&j*`%YskfW8QC?nu=(rZL$-SylDE4mS}ptQ;Hb0CpKP4=eAcC20Nu)2gW^l35EbK z&LfimuWtYKi1F296^+wj4h84&%GP1X56s~n6R-ZkB|7H|`~}8@+>>_PyP|zDUf5Z= zpAs>&?>lRLdX>>TkK&m`%PJAmD6M{I!$qEm=%{)X3^)>E2t7XXXEM5l)363u#j0mL)tz2%l zQ(eY*rP$(ANtSNug^hnuWZ6B&409eR9!`}RA~gn=0e^^?uLG`+&(0ca>>lNJz3nva zSlr)?A}?o@I0zjk6Y=Ah{kzWrN9!^{0=&W>TI)up3Eex);Oy#YSI-DJMCQMA`p~yT zMDD@Om9z%MFKoXkU*iEYqmy30GqM!%Y_C_XLEo^@-)my$@z;46JzhM6p~>NtE^4sV zMiC{Y$8C9ir9o^-fbXO69F!N%LLVeXVq%~ppF`mGPN&d)e`GM9T^Fu80UdF!jRE)e zR|K{elw$y!^YacJRb>9F3TtVM1Lx=6W_a}>==-C|7UpQq_1wpM<(@jYW0&o*&X&s; z7ndk_*0cv#9gal0W36O_=^>C3H!VOVy?J}8GzFcYvcQgQjQsJ?|GD4O*6hmyT{ zuTZ6HrhK25^tn{QDnM9vk1&n*Xd~bmU;a^}Mu5|Uw^nF1T8BeEv|u#@0%5q_JpGFt zV7UGc=I*e!qa7uUR^4Xjoo#6Z7+SXc-4%U?XD}{8(|!ac`s~Puuer@EmS>(^WtcEq zXWW_jlH&ZLOxWzsl;SPjCeL^#&Rnhed>hZp;@X1vtb=HCo9f5kIct3+UQM;Q^S*Lq zTHOA!+8TpSn!M~w$Iw3+Mrw<<630(>O9-IY(yumpof$yqNX+MfRo(h01Y1F``75__ z>$pmJ`8D#@_XZjMfmxwMeG}?z!+IkgMHTyj?2LQh)8u5oCC3Dg&L}H>*%b^@Hvoo1 zFby7&?44%U(+Wq*mR4(f-W?8WjFHj9Z_B)bVY85%gi&VEFAoB|!c~>-9pGip@gahB zg!@KJUpKWJ3Njl{e`-bTrAOLat*y5WBMsx{ih=W9uWsl@Qz} zRTa`B1bdc@Fd}l15@lo#2;>jyay646#{ z6X(D&462=*V(JNX3APm<5ip+}{HGv)nd~GW3xuKXEab3P4i+W=(=WA|1D9dNqH>1_ zL+j#pu=|Ddi=z!Y47}L8Cmr9@tq_p>1b!n0b&=o;aCYe>G_@Kc-w~Fnaq}-)guo9x zQc%VhHZQBZSX%$;NfCJ-Pp# zcK{_NiZsz_88dQW!bnz>Pw~1kY!{~xl2GrtFe zeEVu@Ofl6l(4Kaa3KfwxTssmU9FrQf{w++q&51PjRhp$Px=i24@oo)0%TdM8|c$%J%xD34}`x4Fl z>b0dx2J+w)h30ScD}~$rH;H zl0@n9893}%VY*{;kqQWWNy*!t%%~SFeg(&NC8}Qdmp8WjymTEfTdmogsAT)uyl6P@ zW~&Q;lE01rI3nWRHNI=S`JWf#J|kptGFqSYEiTaE0RGP)_UUeip3Q!LK0YauHs$$A z;e?f~`$`pY)Jw=Dp;03)0 z^T>M^m$QK*h}2bg#}P<8iP4rzKzu2y0K}1z%k$Su=j9{OKN(vBE!JNI7vg9X$%qez z8b+!_a0QN}afVl=0Ym4P;;~UoCJ;rNiBdm$Xd%)MG_yY?^K6H2M|s3Xu`kr}+T|KMU6?c9?7s}4yZ!c)^ z@rjjjgxf@c-!Qc_AM?h$&TsrZ_n`w{a!TLDF4U}-A+C2smO2_u%@n^IHj8#D$}#&% zn2tN5e}U~E(Hb_^n=`ymcI2~dagEIa$A3gdj~Wnz99o=kb{63E`QIQ4+)WFC|55NV zv%4c?8!0>oA005$7-I zwP~QYpx2Q=3<_Ep2|uN^(F`Z8aVi1osG}VF@;MO|O3QGB9W@%ERmP+COT`)z)94l$ z-sAiai9e>Q6BN(;1d|&m;OJiJF7@_dH2j}0^dCN-6~>W3X-ch2m}oKeGL+rcmfG^O z^@cLDSlM2tv2AnT;%4k+8*PaE(#uRG{Dwg=O z|CfvZEG!)Vb41p%w#DUu`@Ye&&xMpuEOX7ns%L5xZOkmUf{ z60aQbS|tPaq}5U0Zbi|^HT`1VOygcZs)-oAgZ95l2l*k3v+T}C;YP&?webW3(F84d z&CUbn_=ff$2t#*!u)jz6G^lO`JyJ)jYsb`fEv}o;RWewkaTWlz{D`BWvxJDdUsa*8 zKLiTCwt99b(h|SCIKlA50txtvDbEYBa}ad!uv^m$f)gj;1^2R~_HkoGoUK;BEcbWW>CDu5Rz8#|O{{-!)2BNHnJ zX+d$^J(PEmH}rb;IV4Y8J5*FDKlDirw5&*@GE_`cG4xdi5mt#}tz25;k=^z4wJ#S1 z-n$2o3Y&;!6?wa(4yKBd_hzPdW|C?{N*3Ca|GOrv2`7*QCo56Os_~xkW7I_<3*(KP8G~5PE08P10P>= zhHkN-FWM1Tt}r336i6z!E*Rbc`?ACmkyg2q4ENdkprUAm{OR|j{T(~o*1!<%ob2uP z+v|nlAdGsUgz(pIpFUA8Q*bPSgw@hId?pxvAMUPrh5nbpL*27hm_tE%jNRAQ4?p<( z(jQy5k2bcdKdDFU&md!m*!#y^Mbs1)hWbF;6#nGz%U+_|F@3kZ3 zEWdR2~mOoTPKdqI*zL@9AvPa-d zHGySCY=A1>eheSzs;rw~3hsNEMlz+a+q3V}X=KVveF&PF#yBs5-#v`F^ypQX ztF7Fr{Ty7Fd_9%aC&Y^c?Z2kd4jt~c2rx_qmBVbnvjD_mDHnem0-t!9ExbwY@Kqfq z#%c%OBm7u4-5s8l4uzNlHFht$v(lnxsx}(SxhQXRZ+HQ(7}vJIJevSGUB|zV&#KJ- zFfnb4PB`K|I}KBlpoRc=JofL2R@mw+|4kAi-Ykf)pa8P%n#o-Fy7^lJaVJ@z$TJ(} zS}R4Wf-AmWo@Y>8J`U-6U9MAMcy5Mi3p$r8X!bOwYD43Uk1F`=*k;*B=T4pUXtp66 zUp#UPPKHLYHSkdWmS>l~(f;Zl9fs*TckpFChN#OZ@FnZqrLLn8Ue!$VQ*nWP%(=Er z5Dm85xeDF}Iu#YymuypD?esP#(Uv8UdxfP*5>Jbl&Iu8Wb(Y!hAa@~xV)JT9By>%_6zZhh6n8qNEF-=47HjH3?z!Mik#bL z90NgjAf*IRf;&7Toz{yL>@ZZin+(+^`tTwBDm7@BV>R_e;8-uoB9@q?q0LwueuMNb zm2rG04gmQ#_iflK289)Gl&Ju;cLm48a$~ z)ypq*z=&9`FLSb`R2NzWwpWvW{ zqZYqRj+K@)9!{*H1FDwP>aVF&A=w=`f~Hn|IfBZrviuxqEW`x_33NawV9nZQ*y<~` zd3t>lBKu%9yFc%t?ezlCX}M^LGzr)5-kpu>crU3^D^}|)W02oH2=U$&aB%kM%0-47 z-$-D&@-)l}#S@=zY?)ef1XTXcw$V5Bl|vf({_}6vIuK|Hwm$@$P0e9S?d%g6P48Wv zx!NN(P;4jP{p$HKLI1aU9l6>Gx&BS;Fd*h>mMbzR#qiFe%jp>n7%JJFE-xC{(!ZGq zAH2J)`@EAC_sGTFeSxsbPY-8b(J}!nGA;a;F5-kgb;iE-C%GluCOzc)Z=m$Q4ft~Y z;{1Q}C|CdTD7*h@Ow%9R|93qMNoJj5K-`0Fk%{gr=AHj5kX;JSN>0I`wzsbC<~tQ6 zpdU}SWxzuiX9GR|$c;Jd1z^7|dz|U?WPf|23^1YkC~^GOu=RpO`A;N8_CJx78CTyg zpZDv|8~!yh7T|x8ly{#++oD=PalIDHsS%c#)BY~M&vi&>@AvPo+sdo&&t2-KRv7L< z!FHwrLdsR>InJ%tTe7OU?3s~=H8LsddZg3cZePL5BUzp?%PbzY7CaWw!4Yb)?p zhnA+ge0pp1uL|ZfWS+yz2q+#`kM}8A{Cdw|- zxCnnOEaR(IDjX1|Wy|Ql4EN%c2SMo|-F_CqRT5JJy5>62e;gYavyoeo0EFrB&P$$B zLrI|Rt49&V_&pg72VbBx@J>UymIURP(Gy~&JuWNqY6yutaU*#Shkdu^`7-nN?ppF7`wr!Mv~VSM8M zXhZ$|B8C6!;srwtL#m`L4GWJJaPV707m~s)X%Pk#i5QkC%90o|IOq+1Udv*=0#~3O{f%@X)YmU>cF3K} ze*kK-8$`+n-YEL&i6Jv+^d=ow%LfO}ybR{{G|C?(z?_sU86><9t>ZtyWDy~|+bWO)q>m~fP>I`jJ}c6J*zu+{v#ltIe#6EhHvg%~814H(pgo1)Xc?p|3`|5j{kQ zpkMG8*Hjd2k!4d5_7iZ^=1J0K91vTDubo0QRz4Ow} z96X~DsA`z-LZe=x7}z*RBohVyCaN}hX^?%tSTs~y&{h_cj)hi_61@pBrt2a1A&~m= z44VbY2~1oun+47ZzG5eSWJ_CFSD0L5vt>rXA}0&2vJ1up@v0quR&9P*w_2!!4Z+p& z%J&v|PS1W4p%3RLu-#&`g!#%$O~YS{>p!g4Wt2H0u26WIi3s@NibhlnZS9@Z~Ce zpD5u=!`h%pBw{+RVsqY`LJ0p}(6W@?uEr$eUs!U*R4};jLTd)0=wF>w_r##6wg6ik z2x9WuG)C{|>a6lFDGqGwmK|wdCu%~!Xp{6duEVE`W=&N)J}s?^|5n>fo|1KLuW8tD zN6cK;js-m(#Q}(l);2$YG1sckT5BuUfIMDe5oUDa;z3 zOr*FV%VN!hzFC;$R~f@s31L>KINJBa_|jO@zsi5^qFAz0=sE0kv{6;}PffS;-GTaq zeY&YGcg6#_*wVGZjw{=Be*3B!V7+O7olp{G*i47W5LFoVpf)veHHlP{9}+zd2yozm zz1i2cEsVOkOKz#$KuyTRKTW9tPg+TIZ{g*i&&}`1Brk!q0$!0-}il+425Lp^m2o)a%C3 zy2{i2rmn*peZ1Ii5#Z(XzKc6}@H@Ln>-X>r=7}kLgKuYm?0m~N`>k7UYLm5F{gsDv z`xyNdFRkpQ!(~mUFR6ZqT9c@z6U26HZNObrhNh=ERmm??gJw7NV>gDZl^_G(sKK2V zOmVfd#3KF;QTHse;P|z7e1IzZ^tS<6M8egqmZXu!O9#YXrwD1ueO4h9t1gGvHm+utoXR(JHmZzq@&LBZi>B#Cx zM-P-vb~t=|Ny0;wiLc>$hj$}PO^5fT_Z7!?GReuTp1#Xx)rOLPUJeIg*01BP_$^QV447n!!6eVXxlEgV`55yPC;wq(}6L#2@dv;zxGF z>drq|Bc6^{L4W$3Y%rY6e8D=ss+A>7lZ?}Yz-jRA?K3K>k@X5X00RUqNpM` zD4R{gL(ShI=F-8sOHMdsoe@kl1QF48kS5!V!_yt;%+&{=_HESio;`ctG4%a-NX%H8?Ue2nIvuB#w&%1V$th3de;k#XZDsA9W3Y@6ne?ZnA(LY@EOpuOzVn79bl8&lrbp%J*>R< z*fhGd8md*7wMrt~3L`Po++_c@XIO9pb#YNYp7=C==J=U=cisHnV*4Up37AAMiHGE3 zJ>-~J+rN|>IGuWJhvby=3_7xX%-i~{9CO{~U6|b;HEsczTKhbwW?_MUxh0Zoz! zsty(KzTOY??H7Gblm`E=oyK3@B3zliTtzheBBjK1l*yxP|L=>WbG6giw=&81r+lHV zw*2y{ZR(Fsk<~@XmubbNIA(@Cb+yQK^GY&CbZzrP{`*7gWUX)cH|pt`2F+=(Wry=V zN7D)-H)pBEveV{R#n;>D?DErKuCJEQr@E@|CtzX5RZKZrex#M>)8{UEX@`Be^*Pbu zZgF#Xc{Om;W@sfkPUiTstu(3sF;L2b5Hk6mn_(3`V?%RZNtY2Bj1h-eh~KJ<{m$0f zZX?}CdLs-~*YlJV!HJ{n(3WB7E#DjGVBo6`enpv>!7$3qihoU?dWiloz3UQE`O{ZR zAi}LjFW@TlaQ+ZkG)P3!v?gwZ_hZyPU~%xY_1SUzpi#Hc!Z#L+!;QXFnLZl!*=5&d zP0G`8$Yn}g#SROrRFhlk*cR9}G+JK8kgPc{8m^*$%?dIbw?!(=HZ*WS$B@Q3@GiIi z(P`rm%+@qL2Y!d)EOs+KEIiD<$%^AFYxNb%eooYjwS`~Ecgto?$~(9fvI_Tc13W5- z^X$-+*puuT>M|r6sRU%o#W325L*xuX8?^=!>ufn1qF5-{%mW{8TkSE#F{wRW*Js%aY3GskZ_WyMG+gQM?O)*bWo zF)`$w6yvA}rv|BJ6r#(rIV%pFW72||7Sr&cxM-N3;#%_p7r1EKQPStGbJnu(pj(B} z!-B*=w4@-(-N2`pqS(m6{31``A-fy!~&52l$jXV*a%(!-;c=mU$OtwH}q%7u={m5t>D?aMl@N$L01HkG*? z$&g=eKyTCFxMQ7fkcI4@=;_k&`9#qWjHR^sNxyh{F-zj9C8CK_x!`HO_Ii51zF&QX z^!?F%dIwOovRr^NT7M3%`l{Udqc4HV@X9ZN{^7OT4xFt`)lgpj(EtjaMXmw&!|?@; znyb^XM7KOIC@F6a``n@bO`x?y%o3HgcSQ+8U`Qn}_+nZ$>W4N@ayAcKOcxWXk}2Wb z8U3_ddI{Ht)|$?!d8aoJ)Cvs$C$SJ1Z5oRE4%O8q>2Y4`4ZF`=DM=B%+0prHtDlX) z2q?{n!(!A2Q)&-vIL|6A%7$(K+J#7atML8oYI|y$i-t`&eoC~ykhnNj$%n;LjD2$P zB4Hy-1R7eeM~dR5PKJxj=e-9bO+8{mSV1L1%VOA`-}D#WFdQrRoqeX+muYMy;nv*Y zi9abp-Ew}+OyE2L~qM+aqcspgn@ij&MZkt0&qr6(NbJyA|F^Y+MAzB{@Z|{ zjck2K$x!%`Q^Y`Mz0&i0ztL`&!&;ih2j&9u8-6SCimq~XU21*^#U$rY(XgUPV&m{j^~FU`o&CjrwK?Q!2l|ObEo>7R*ao` zgTGT_+^`z1A;-L|Dwl}#}V6-PRy2DB(ckmqb@ce$wlqw#?JaqS9BD@JaRxp$yt{j z=J`&eYB=(^MKO3tH!GT4JOL@jw8mO6|$@*BEwmf zil!-W7QMXRSWaE&#Vgcwu%a4mBZ00uuw{+> zQ(WlDb6m$KI-Z8lYAu%fZwfCRiNE$!r-mTp@wL3|opQ(rj3G+By9plv5eGl_3Nin{ zQr!*|6WmwqmA#-=bjpKruOY=5A;^VSKMKfhs8LLm2qq+^v{2`oZtXq6p(lCI=OxK% zVBQKq9AxG%=+iKt*2ztTmg8N`Ayfubjy{gr#*u!6wR%u$L^@9>SmvUjGi6%tHk;oN zBdkq#2R1Cq#vLH3qAOb`M^_z+wBAnxvX%$;(t!pqs>Pofj^9<{m_;Z7DXr~3DxQpm z#9767;+5(*UGC42qStgqd8jAnSX?~we@a8ffxI0HT5TtC8x;O_5%WVKxspMGljke~ zzbPx(*_zaiVVLx8tvMY*ev;6DBx`rW(X2Nx=djq(4LhLDP!k>z*#!jQfzjAofZd>M zfGWt^z%|vFhHEwD#bv}W?+|GG<&?L;k%lG#;h+(`W1Qks*;-Fy4kcggW)=`gMpyH8 z3<7daYA1atyb4@6!~r7pN|po8lgA$ah))1QYORUm3i~sEgeI60c}%Pzx`YyJWEy5P zzo5yeRS4yZ!2GANI4cZ)6Q4s$de|novM@Ch=f!bV!4?WHaQ7TDw*k)Lq z2*MASkO`8rb!*fOj1YB=2>&&^2}*{lPB9Yc>cOhnHOYA$bG`gMDD81DAK=~hEGp{| zl+BCd12K!U_EorTdf`uTbv1vJIKe$z=@@qi!KugbNi=G`&PPZS$0Cz4-~8?J^TCF>wDUFXmG(cgjO(0zcyf30#GBI=`@H zS?#4WgoYyH1e?w;D4wANe!2kh0L+to6uCe zb15=Dd+#ACMeX@()*`MJYEAJg)4m}pf`q#c>Wm)T238Vm%8mJEJhd2J-FhG8Adld= zESKln>0n>13~K@KM+Yca9@B>!1Yf!t7{R8|04*wKRHN0eNztS6G}5{R?=778!D|#X z?+5g_eU)(-ZX#Wf`gPC@&^ewlBrL1aku;Af8L!fj3St?SQo427=nVkQq=Vh;(Y|l4V zO?vj)4PsDm`dSV0e;9kGSX;t2YkS$Yy_fC1Y}>YN+qP}nwr$(CwZHXt=TH7l`k)W$ zV9r$LOjW8<_funxYs8(WArrKOuzYGKO}3FkjH=no+W`r}eNFqgp11seH%+gu<_#@6 z=zibrQNdmi5T`aukarI4JRmygt1xStq22=18{!(!r+K1VdzWy-vg~AJT$Y+jSFGKL z+&4freXmblW!^sX$f5%hB^15exf~BehjduX%nV1-W{}u)LnEA>s9lV3yGPgH;1WF$$RBR--yhJ5Lx#<|a;JAroM$)j#10xjr7M9=Q8O5xe{fCg>YD^Yn4ZQezmxpie5(6G;K_y(R=`|Cg0#xs30=5x8gi z@1H2dTpcqS&VfQwwvaLja$-~Doti*zHsdkI4c0sn5~rvfnl9J&=tY4+L3zsk`s}2o-Lt2>0PLM2F4r0n&s^fEA!#|rC>tKzo(cEC5U>2 zf5&lep#q<2+RpKyOomt6p7OD!Vs6}bJMRM?-4Msr*g^T!*iSiTad4@iGjy&wSp8J zX=GTHGsWB1knHaa8VQ&I?%A}wkRPd~ya^_=$xY5eeF#p<0)Ch}a}jej!UAE~u~Tmn zUQ{4_IR?_C!6R(%9ri=pL3JsvthjgyU^{zPXUR1P058A2)w0{E+$yYEAu zJ0zuYzOMS<&{B)PX`!Y>mB$N?+ucyXlRc{67Tzi1lghOoNycGvHI^#IKA7u&ph9sO zVz6p)t=04;?&3W3k^>a4j2Mj}e2V1sIA(5!L*~`IRro>XIM`LqZliYszC-vMk{l7t z<#dvnrH&ZzP2l!u!N}_kr$*)%<}^7a!cYhd`E2(IMmP_=ayGLM@`zp-3(fx#0Qp?z zD7S>+Wkj&R$4!p$IiWF%wwG2{< z%<)StL5ul1Sj7Q`N2+5Af&`8GN}xxV{^OBApN0P}=`I%sC^|3ceDp@|fw zG`^+cvY+DPMr2(dPT9Tx=4iea>EAxJC4HK7n0tM?zh6h%&86Q8Gj0l>N7w|QRyZh( ziPP#PgS;Eb8}I5}8l+go5)o?ShWzt$SX%OVcZ%P7>zgo* z|F1Ln<-gN3XZx?2mTgWO%({bQ`umHinbw{DmtynP`diF3zi8Um3Nol09E4yyBpvkd z3g?XUlU3}xy)^x)SEJ*ifLjaOVrA}V%eZjMY-a*s%y1;12E2{WyQRTM&xV%`Ry>_T z6s8U`5eoM+q>&>lS(H1vE%M7RTfuFELX(zy?dUgH!-)s;5TBzSR0^Y^@fR0!#`n8PjsS~wAV3UdE2<74tS`d>nL5nq7-Tp=>~JIc_Z!x> zAq*%YXDHBk_Tb;W2GST%gcut=U_kw`@(Fy-em6XR{vw1$iQ57dv5!AsnIU2{oD729 z0PiJmi9$l!RO1O9`ed%W`~f1_#vn~jbJY|KPg)K1HgnAlVV$hM$;Fv>J&Q@&vMsfZg? zTm~s50fw|6eXXRgKXLjp&U*7lsC*!*FDWiy%)1)aii z3*mHUIAWoC8$-Osn-2YR2etCVWo?vFy+2q zUY|dS@P0UbNx|yhkGBZ^qbLA%MC@*SCjcb5y*oaBwqw0)x&L5kp(vf<(hR3heOXrn zi@`Te@anb#ZTqvP_>YMDtDoU%> z>$ypb=wFKC4YHL>2{14kE!6Zi;rnwDBONN@8Hz}2sSj}n$bhn-@&)|h4lY&E_Tm9D zrZ9sOz?-j>;hXdJxHn8U4yZN^5RlD7nHrh~$5X>zSuNSL$dkudRjw$WffP9+9J z&OzpljB05FU&7`ya(T|`KTY!8j`M`6Zz-+S8Fe15*(U zqS2~&D2SK~kJS;96|a|~#+h*ZIcjs$1GNo?3)VKMhb`m;u=1R|e!sWKn2HL^(2#Y{W# zmlrr|<%pekw5zP={Pi!KlhL*aZVFUa1n;_ecEFF5rtAZ@d8M_jsHIe%( z@~zvthG@*N>hTwo;52j+(5hqBygt)QE*)C%as92d+d(XTKLKNcSkOQUUqTbRFF|~E zxIZg8u8_$@>uNS-wFMShmU|L9I<^wJn9zeYO&X2Q;iae{e6G|&kSUZ;#^3}b9&t>3 z`)RNyo)0b3p?1MjSdiX8oF|k&eoinL<}+Ts#-UL`#Q}Uk!$)E(TiXz>o#d(jC!x*A z-Wh42G2LF}Fd6DHmdqqCwEZBiG6Arc)#EoaW(6I8Yb>io;yasR7a#?&T0ujx%OejhLF{R396swW(h6Z-rKp zU^QuEeoI++R-7nViR+S$KLHU+)Od!*bD|u(_@hZwQ*&mDe*~OU74cd#LfQY3#(2r8bvv4|ojjc)wDPK!}sLDa8$WerTbv2C^ms&`D zfh>kz^$$huFoSxYEU5C9+8y(CO`x4ORJTc}YdO_e-Log@Zufn9 zlaEQBDa_6~GiN03Cfe?`5nC07iOD7Lc8*9l`rAy=p>byLP6R|F9=TXh_yygz*e~YT zN*iYj4zZ+cXQ#Ya;*URQB~6U%@}%u87a@{>{DtTtU6&DmMRVKY184s#Z z_ylg*>MAgvt>$E3IDBz(cU{eNwOD_6rJ4KXkEYAOhEISdlKZI+8WV805wtR;PRYX! zU-}pjQAL~m>MC>RwUZ4Ev|X-`c8g$+?ySjfLDI{4o6Yi2(?x37?zrM^O-#h;)pl;C zB3zX*?Pqc9j+^K-Io!M>ONsWsy*|P(Q6FlpNHGSR+N;D=N28pa>inaGx3mO_&kr7^ zsUj$zejdJEK=m;Z-@a3rbEDk_4;}}m!|sOcpbU?V2hIu#yxMgY3w^PTk8p|JRPZy9 zt#S|VM`QR(5kwlayp`!d4FT~vgcMTr_5cR2Bb$U7nV3I-_UIzcZ!h2esu3qCAzlNp z2u-E zW@f#A5LNh7>e!{3*(IsC%Bp?rzL18t4~Kp=OVkb& zvYO2W?Lq^jlKrt4(t2%on~{s3@SqU)^XpDiBrUr?blY`%9Yb~uBjon_QqO^$v5IAD)sF>NzEIv%aMj34RXD_9q)S*1!d@SSLc4M9S8=K{UNvrYp z*pEoP!(!HN!Ar3e_N3#N7AU%%mPbKL!6FG}Ov9jy%&0{zbXf9=Te4kOy6D62dfSA# z7R2%uP5W(;l3qr5e5T}Z%80k6M3w!#qqJY16X2PzN!#F~_WOkDbAYM>hCdxPsit79 zB(PGz6t$o5nOt?&?(4Z#?0T06_J-WX*EVz-yUyDJMg&3E2TnFjFzCpoO42VrFN(k&7Ki zw*PqV2r1n~?`Q>l)bX2{C*Jn9&U|!dJTyy8CH$i>n033J# zsI69uIj|t&d*4Wn(SmoWX2Ib(UbX_{-{jnXs%NaeMwg@|{v=ipk}72iB4pjz1tkBejr0RJeue7GhjVa*mWhlROZ?I}O@ zxEr9)vbr_Gkoo393z=VT>9e!&#>bG^whquwdLu?7hGahAi(u%M+>}=zwu`aM8)}HW z=c@LfA!rAixwhQRa^!Z?XK#J%cT{7K1SDkc4cfKi&1~;7p5FE|+5&mg&+e;$rK=UKX*689$x9 zQi@Dl$442pN0rZWVS`%_virKP6034Nl;5#tr$S&-1jK8pwvgC_;GY4Wdv?T|!9o9| zT**t<)$DV5P*K+qYg;3X;n0vG%!k!pPt4@1@HLHc~9k*N%tXWy|NOR92zc6z%!P z=e_HaNi_rkS#~<@o7n_C-Q#t}aFwyreYY3I9axw1x6mEi%T&3aCh> zD-N7@6}&9PUVdng<`3BV?~4!E7Vqy5*aru_?-BsDp8XHl{U5yNOeFo?VxK_Z@|s-) z9QTrF-6D2Dubwu5cnMl2R9ubqeoNE3Tqa3QpZ+;~J@+D!#{P#pzT-lx+qMb#6!|cb z1cDb8pAbgqbP5-ApSrJx{@S`Pfi;?%FZRE$DGw8|=$NjwG3~?7IL{t-CxPg~3=OG> z-bX{zQY0W~&%Hf(g4^Z1t9t$&GeSMd;4G+wO_4OH`RU5bYDMia&fJ56K*`0Cfbyot zD*>uKv&=qb`DSgh2%h8UA`Zn#SMdSgY`deo{~Rko#!v%=z)H3E#GtiGe%90|?TTAu zef$l3%K{I#b%%)T`vdf^eyoYDPMSY$&4&ijjmDNk>B1J+aC+2`RPL(|YXnVxua?AG zg;MG5A&J@DrRZ$y^(Ah{GAPrfD-OtK7>6_?K2gMf*6LCYz!XpC;sJG7DiXPp74k9 zic}VZwYKJCE4L1rB@d{*SJFF28!6WF?Mc@83HbKPu;ngDK&zf6SITR>`={4r zyw4Ceqz|Kpv@C()sHS1BT?t!6h3 zTn*#2JDA+y_UzMOwWs-t(hqZ5;s_J^OBjxa7+@y>Vh`Vb7DD0wyV#u_0D`0W{$IA^ zn*X2exPlL?;N@-Hz%iDsd(=m?;$^}G&-1@+$IW=*GQUM8F+u>yBj`Bmj}wvcQ`I7H zf1!W|DB0xXy$^>s$0yvr@oLhaR${ze_L(j;lYd>uFuWe#JT?TD`Lk*;%%L%QlKNFJ zDkyt1M{r8R{F4g8{$@eoiWiIs)wrS;&o|R+{@~|E4e`$3u7NDd=F(KlW;b&Z(K2^F zls!1)We`ek^VYIWXmrqMabXAKT{vnO>(}ENWBgn4bvb87wP{7bB$Qnz221;8wK-L7 za@s0ZvP=Wn>v?1vli*+l?H(227*gyRP6Qu0E51r|v3M)D6uoH1eVZ;Oz+NQOfE zh~i?ZkYf3@YWzlrn5WOV`L2#=&!2yOwoh_J-9Lq&1MMrPutNs)4Dnd zUqs?BNo|kuCbh`kAA<=89$ z;0s}gInDqOS1cd_0Fgj2KJ>YM?P55gC*3F6E)Z)>RUn`ssNe{OUnbuQOxr2HHQXTo zSYNpc4A+x=^ta~Ors&2dBuZ+O=afwP2VNQ0yTH+yQjz+}fG`%gc#is=Db9a+ppE*u z7W^o%yXeI+%{%18b~zLhUv^}LJ)>Ad2#~&*qYLfaQ2J3|^bJN3lmt`b?UxVj@bScZ z5K^#=_Q=eM=H+_TEyJ;89aC@sL71HL%0FVhk z(wWmx5Is(Yw7Q=9!_8&J;s)+^-J>G(4tVej)&SG*pn0DePSg1EoCQw9PM2WCw*f9f zN^I6J?qE9hdkOTqV(uiMjkeP1wN8EjK#d28f|$m;u{m;(Qbp@X?YQUHRRwYoGw2{L zcx~!+fCD&Bj)V9KT~>iRm^&`t=?&qCuRZv+CN`HQRk+D{AFr!Iibuk{x1@>=buBfq zgA&Hab}cqH@a&oQN1E@i+N`XhStGp484QRCy7b0({^5fir6W>ZC8&XfP2}4ZXdEiQ zL(O{E$fYBlT8sP8ZV?d1b@2yB6x7*Ag{5FHR$nF1ob`(b6?PEW#{Gx7{IrLHAU>Zl zgc5F^WNuDZE({Lw*c3Ulad5_dzP3HTX*)MUEX{A8rE(>ZVQu&10RldK6o+Y=VN;JZ zqj%dU;rq2#2u9YqFx&Y~CdcB;+36%eNYl2y%!DJI-8?+sy}3L83rYqi3p1Q&GA`r? ziCye$CW;!K{SZ(cSEHX0^ z6CKrb*PlAE9j8v+EjYOja?FU}x^DOa zInoKo(WZwh3n)z+R!UCIubW>8O&QGtrSCM@z|PpXx7L#Hn#ZdCEKabyQV?krh||&) zePc>*7KUd*-Si$MbjvMe=$cJ8-S12F&8@HcSI$97aq^EsV_$RmydJlAWb}Piw8f`> z)eM_HKV+=+d!;<}og5_5nvQ#SnXGyNIN@nYv5%5KP~&OtW3o32_Vc)|qN%b?LPxeb zJx@aul|$r2ZWNHVc2PGQ2h^`>@ygIcg~WDq5wV-5($rld@okXOjr^sCs8)8~u)ZJzb)adLrq#>sN zC{*4r*6GeNUeBf6w8(OrcKBuYnDwtKRWDbLVqG%*1&%v8pv6(`3B`KDQz^^F+NPok z*2*tQtq~9PvdGCzAZ*z!;cdr!!6bv*Ky)ov(SWD52lvH8q?rdfd7E!iJdQ7PRg{C{y%;#%opqrg)p?cgHg2@?Gw}-ei zuWXA^)af9@^Lsp`NCy`IDMwvY{BvZEf13st&C!e#VtO7ii`pG}`aJWA=0G)?GAmfs z!I!c!D^UI+m+j<&M?jU(un;OU;rpgE|9~Q)KmB2gU2yMWjA*pjj`heTT5}5bZ^rA~t0ta=>SuLFus&{d%H(#Qe~% zroQRkB#ol4Qou4c<#6JIy}T6fsLR++zL|#n^yaYb&^upCVFI{ttHCg?V8L{7T$;zC z_{oDj)w+vpg2wVXU!Jzy6m&vbdDG9K@Afe2kvl70oIEvIAcabju72%M8n)Oz=N10t zO-`EP=JeJOu2!J@SvaBJGEYB?!nB7K*ucKYg!KC5PpwxTscVn>G!-v`(u>CTnq#iQ z%~S&bBP|yik$kzkC)yPBBWgt1P9+^0kO~GVEyz-)i7IrF@r%KiZ4$WGriEV`MPz>k zx^;VwsMT|SkQeoZLYpd?d0VU~S&Z{xnIx>cJKLtW=YjaR)q3iSs@CLVE@m}1Cw7Pndybk}UP}%gnPJO&o_+it9eFR-_gDQa=0Ek>+N#sUZP%*P zRPXbZXRiZ0Y3^LC-L{-rfvWP;QV|OPXo?u`s1H`Qm=567exglp-nz28;&0Sd5VZ6` zRK0lYT{LPaLKVN^DKQ#7!dUOFM*W9eVQq%@-1Gk&#Mt*8)id|0U=?XYEcvfTP_Vu0wLwW7pO6Jj2*7L@+u9ct8;+M6#Z7{|m-bu7^SZ8D~ z$ns~PyQ}?)DNWc#tapTIZW6Py!0!rx{RQq)d8M#X;by;DAG^;>%c6$<;SiT!%ACZ~ zR2qxI>$t_d2%+$qd}@kU13>7Z5>RaVlWs(jG(Gx4Tpo@8U0w44{H5kju=1EL@mQC7 zDlZ7;;{LkOL9h0UdzfyAEF=fq(q)Hh~L``!fE4lQL4Jk~b*8ppDz4lt^;a1x)?X z5$cN4QkyPDxxgJD_ImZzJSm(QveEpwCCi6F*CI zRVOcw#znySKzBk=pUU>9b(jO`sGiLb0eBpK94~kHMygE*Vt33I{DKR+)JRPxNR%=| zm$F_>SBf?gNsFWvNe5T8Q^PDRzzu#%jSywPm?wu(K+!-f8a5NjcPuWC@InDE?;22% z5O_1Qk}Y*&b)znB#O}(P8jzEtOSG&$h&aT4AO;B zAwWQZo`5;AD8t-uW~j%HkgpqI8Q3T-i0@JUCheYsq3lJEDnGBY>~sJEKrRE?IY(B( zao7#uL+ButoOQ1IY+cN~Ea&~KznUQq^u(y`Ds4%C-Y|GT!%vR!I%@z5J-`2~2r8n@ z;N3xhg}pY7c?-6T;uKa1nt6faxXX~y%yWhL6)y@*#@ukdq8K{MKAS@c@b9D&NQb{_ z(0l;wsRmAFQA^-;s7Fw=_Gip@0j2wLYk}uKk4-fw)H8Z|!X)gphK*r+XG$x>tHH(d z(a~RVWvuaTesJ-Yo}@Gj;EZ_0i^Q62`0nZW37PBe{&fFbHcSK1YA@IyJoksY4|dR# zP3bn|YMZ_rn|Ax{%gMlj{a-9pishW82;n7N(tgD<`lt{ojvbOBP8YW_7urngp4HVe zM58ROche08R*&|93kb+7b(uVP@ET6&*dGBMR>QB>YY*Y=oYwQvX7)4s`)ja4Eq&m^ z6kPLBUU^B)y#)Em+;U^>0-QyALkBL)o~9}%<>TXE%|X&Cvo{HSj^p@F2ywHQRa(bq z9QD^Us?SR#_|{B2ZDkYiF@)7+6NlDRg1JBaI?5(A%wbtI`;D035`#-^xXa5GY&b6Q zn{l$AhV^QZEJboX@Gn;9c9;oD1xazJGFgvcJpn@x1y?BfnYJWS)j@8FapCZ%mmoK4 z?6Hg_4e7*|p~AC8U8yl~qVya#XDIY7-|_`^>)r-*KuDw z***l-*znI%&ZG-Fwx?mb^Mrg+&Xku)b^fA&EPoAPpsjv6*$>H9uvf=9%6z`r6)r;3 z9Rj5)e(RN30s#NczYzs~zq!x9pjk7sBhTTsyc5>$%v0PI<&P-lEQtlkF_LmDAKXd0 z#7UluKWuQvjqkdgNsg6``CPcI)QnFT&6Pk?uV*wkbZQMujvdfIp-3jJMkBi6AjP6?L_^u25|NnE=q z2(_^L=bFZNe@^Y}M-uGKXA|t@^Q2627(A%~S;lUo-8;A%-0F*axfZu~+07_@TN>u* ztDgrpFVZ@r{_!(dD`i>9Zd>|Wf)T=9XSlncyaYG5KrqQWLrO*5cJwtbYdC7}S>X2E z9=reM_iX&7U0F6jUwj+8=eME6?_D&|VE$Zh2uN!X+%i z+Fc@()AW?M_bEnGdDR+VThp|%$Q1@t9B)kZ5>lc$Q8q}(cu9QK=`p2I<00lp>V)?20d~39CwzK}l}Qpvv!9 z;(qv48ndl#W}8}yUFazVvxzuTQgmdQPbG>?*2kair6+=wHuE>iiBj#LKA&IX$tfMO&rcV%`&%8v$Kad+37cJv#DK@ECPH zi4TDV*$TH*2IAg80~+kN!T|}-P^7fvDdw14aj|<|m)kEU&Nou3l{QV0E6-D}0N=h= zTs)X7j`H$xJ!4NEphJ<-79=S7s|%^)w;l3`{Ioy z@=79t&DrIn;O`B-bDFi}zACQ6;Gb9e`_E<1?}<*T`?}>Rj@p?+xXff+o646ql?MT5 z_2%ASP7MX(*n6OXc-yO_C8eX!FDw-gwH!%(s>D)#@@sSMKe^=8ws3_5N842`S0NI+ zS!(hD`6j~BiVu~1~U6O=FrnLabla*kVaoHZvAoI!2oAc zMwGZ{+{dae2Nf-?u*Cj+mPbF8YH#PlpZzEeUMN3w!L~7hIU`~X*69v}H{_R~eXDu4 zU6H+`G7Q-@GymNcf|RBClAvBwDh7i$=;!_hqoZARIojB5FN>6UB$1M(p6ns6NzPLL zkSoQSelZ#yKb5S_2;d@kH|)ZD@;UrRQeXAcVs7yFcDPokyqUO_B*l;hZK!^4)GhU5 zj4g#E1$I^wX)>JUMT}aLI8=(dR-f;;QrrB&Z4GU~#=HY8;po=iX$;%7!!?rh&vYy6 z+6x@BDfde}0J9U3cFX2k4nUI+72LG6#hd4t+>TpU5x7)- zB`(_uf{_(9198XKTPlRv5+dbs0`T-*$gG1V5W`m=1viUms0l=$Lc{M7mj1EMton7s zZ`5-I@Ra6S&!(VGl^zd*p>u`GWG-;_QeneR8px}<-M^dfHc7j4eA{`kc*39*Q$<#% zw)R)|rvZ2Pvnxw*x#xl z^ArX82)IQ$!r(u>Cg2=Ne`eGei4_X0&fl&efpX^wSza;WLYWgOpZ;;c@$ND_R6;j= zK3qTSMk1uFzsb6;#?r3MiU0ZgC{ZqBf0^#U}x zDZz3u3;mv@Ad=VjUwfy|+u`fvD{tk^>p$%Sw^C!%_9dR4{np6apvXMi`^4rPS6#Xo zZA3Q21E5B&wHYS2QUrc@K8h~zyd<4|C+<0>+Q91`EkBT1np1rMOFEXXYDk&C+f}=x+g7`rByxsDEcyFumELzAnI|E^I%AjW?fu!0x<2j6T1= zdSm_w6j?8-(U z7^MXm!B1>XqGcxwDU=}QPwtGtO%Kffm^LR$V z`CYgAg#h4$&vgXGqDOAQ&x_$dY{%EaF^@&(Lf^K4#h^HV`2^7}&2@-6}_ z8Nvqe47o!h2IfKgL*(+zX5GK9(~^DLzo4yt(BIk*BR*o6P}}zvorL$oFT7N7{%dgE z=O}KD1bWyrgHRKjY(u0J+Q#0G7lD)j9uk2CM51O>0OD4lx6>*KwJ7noBs~BELQnw; zv_>`!`&XCtzb023#Jg1pRzIC3Ljf+SS+6BE zFU&q5N5(B8(q@DywS00^fTZ7+WAc9DIk}rBWEj1p<5lRHyr%s=?|E7JE+{=c9vw^} zD89aSGkNY})hj>uFX`GZ?%^Nq`$4ia8(NJF%*^BQF)F8B8%|3bi2Mlhe&%*ZeOLHv%UOx*M+=p z(Pp>v{G5EBziCa~eR!WQ&DZx`y=EXTe{i<`&e?xwd;a_F;FSMiQm1VJukyaVy$=|< ze$yeb`+#+zK{Wh?VAR<0Kn5}H_kl7F^Y4C!m-ZdAv18Zg++CHzaBG}G!20qSaD)G6 zeuWGWU_KRmA{E%_8F}MPy~K5J+$iC-J`RLq+7@A@hGj))z6#>_Q9L(;{xN(wi7htN zzIBiiiH^Mf4WHF7{7JZl0Xrq{@8*MB|EH_NJqx2okQ!8QLhR_^Q;>Ez0RtoJSfiH? z*=pGMw&fn@?7t5FS8@LDF1wmgH0-*DQzP*M-KB%%S@TZO}TDFx~6k0zHe+2t}!75y;M)8X1gRSX$i*w8FEcwi$Llm1g_sh@q=KMy~QaJ2urB)E~+* z;ndC-{>fw7k<_5O1qatbjx>y)Bwo}mSb?jN%?$8jrB^uT5GUCF2>h_UjIa8Rn>g2t zb%=Ca`RuL>t&^=%c(lvqR341fz( zj8B4&@8aMOnH|HbOlZYl2&xhWgTUxFpwoCe+w%3Y#f5?C6>6U7h@FVKI>Sw{qKXWP zWh7uorR@>lY8)CHRS`6>${scB!_Cfx#kr;NHM!Gz zzGXT$CfdTCF=1CS`@|az7u$Ub=6Zs(OigT4GzB9WO+4K!%^KVJEPqOlRCV+VpX`o< zk>2Dexf^l>l<+e^nlE!Pu=61))_aK6H`th7*b~J!TS=|gx8Pb6=%TmVxVc-mQWW9` zrmXfMglAxP9JD%VIJ*RoJtzmrDzvr$ON%fv$Zskn?c?e$xW%RdgbXH#13)D7q~rLM z@GCYvLBfS3CJEw6x%J}s#%m#w6eqVS#0Y?QMdH+4+(s+@>c@$t)j}b;E)}zjy?tx! z;I+`&D(MXp#HT%29J3rWq@oh#YaY zaSXHWFc8DqG4Ch=Bkz9c(Em*lwsXmEDX4m*=_sy`a1ugviYQEOVLDVptXg7#8^f`{ z2S{)Dn21ALHBW{JZGvW#Twhc-4gmPLie@EGGH6w_W$nAnc14a!-pvWPcb~Yy;$$3U z2XLh3l#m*K!97mJgMMwgjxR?X7PUvSor#Xf4&iA6iO`5DJ4G{p-ZU3V@U$vLwa!Gu zX~4W%f#pcBM+>X>ZYN>!Yfd5>r^=-iIe(WDbwx3*0LdfS1eercDnnEslw$2}Q6`Oa zsv0*c^{*oh>oCNr-b4_J6f`7iZd>ILGYKi4Pm*Qfg8R&#^rV8%06V!4ARKr%t)di* z>8b);=Xk?Qmz-cg(sAi98o;upY+kf4l;>je9py=eUShyC}OoUEY+`Gz5gs!^UFQUTAFuAEDdgJ%_ zhxe6(^cD9w3+n@l6$kIbX%!pW4}Wig`313x-+mg~A6wUHd^c#Gi}Mw>wgK8pS&j5dNBqU3bVsaojqo*gbsd18?j(Gd*tUrr@zY_@F%Ht zba;sQlX1SXckW?@8t zrG2zv8_8`BB--ee-3^nytaO(uXhOt-V1o3!YC%Z~E6&zW=VHdm9!B3_ccaZ^15mD-MORoT~*ZJ(nn^S^Q*Df+Eawa4S^jx97 zrbxYyu^YB3VWg+lToHm9T4~Rs%kDBu!H&q-nnLJI4VP%uxl^97Cl`FKB_Kta_@#Vf zUBsD=Ztlb?Tf2=1kg{E+VSib5f+*}1kbe(!O(Q}E7VRu5wzJ+22Se?A2VcMrfJqM- zS^x~*Moa|bpa|b3?R*(8W$ge9-9+eV|M~s9TT}0t_q}|yGzY-mQ9$f0XU|s3X;80c z^Jqf!yGU?DyaedC3qd&M-`#D3mo0s61mRFi$>WtYvDoiWj-!6>ij>ovV(W8VIjnGs z(;h|-saB{UuLLNj4&|cqg=(aIp2cdUFV(i?bRkQNW-744-nq}`AlkzCooRUZPk1x?Co-J^`%D4;%4Z$_)XKjBZ z1+{>Ltp!IhX-U8GLI+c8Xd0$*WHZyTb>b@Vgsdd%y2OTPW+Sr$a(+~YJ_`FSXyqD~ z8G{LG?GB_imqQO?dH9-%#7Jtodqla-@pV?|s))Fp>W28YdoyHoe`_0R*4^$3CB$eh zf4e!Ps0T5n_TQ%MumtjnoOk7p+^AZ~9%AP?$UoH1bEd^=dQYmEZ9Yf(#$CnjM25nt ziDukrG*SF5_k4Ha30N8XP)$6mGVCNWULk|y7w)%^^xze>R*zmF>tJX6f|Q_6Wb)zb zi&z;F=?@gs6cz5U(TvJr6DS=+oRe>h`oN$bTt=NFo5?72Q*V(TG1Ofp-D{?xp7NE! zKHa_3e-rYtL;u?%_5bKOVrHcO`#<#+_1L3d&(W>!{45~#BM$(vpX>E=V<6}kFwQ1_ z9o#RD1(>4YkIFfDy zyeA%=PxHf-3ldKJmfp()!7`r+wZ2~t@8`Cw|M;-C#g7B;XnE295zT%~Xi!L#X0o2mBPHm)^)_CdpW;mvIHo3tU>g?UH_S`Q+J@z8mpKuLed zwE!!>s)R^Fh^){-Ddm?BJ*OD&NhDevSi&(O^v0l+sgtR*U7ma%y*C-`#V*f>a3hhE zVZP(Z0gwmCWOCMvZ&{+@j%_ikR04`i{>ca2m|UN#Lsu@f%%CM-kbYED+%Vh3@86xk zNJHuT$D?ch;n3)3l9U*ewpXjnJZTn*m~xinCBz$fk~)xtRLhNUM5axomHdG4MsY}t zD;gq;{Z9p0cWas5-9s+jzm9h=JB=-`9dP>4_*vj#7vj)al(<VMm7Uhc2GO2ua>d;l z=G=4&ZD8pDkgK&;rez6XZ_dR8EXYkhMaSJ4B|$DuWG$28FwRBNoNROC*`9f@*^pkR z+x2&ctM%i?A&Uv7aBuzHA%sbS@cx%9gh5iALU5FM1W^hi5X ztD}ifEYokK^=#B;xAipbyw%jzjBqTZC;gx(ldaaVsgTO*Sili2xnSPCtu=FjH;Is8 zRp1sU%~g7MU?6VTM2Gd2JpVQTEKWDk0CUTb+suv8JT9@mWHI--MGAbdc0K*w_PxQ5 zWM-P{Sp=5;^f!@u-^@#%MHl=KzwH(aAJ1@F$xwJvLTN_%)u673vi6%1~64oU?;;3 zqZi2Vx0B(3NTZCp&vHUDL7nPi~hPTy-6z}{~cz&&giC~>QK61vC1LXVW1y*Sw7<|acMULZnL88whGs)$fxf^c6vThW`h-Rv`VEq7+p+O=@aWz6a2Pw%aa-T~banO2Ui>c}%qhBOLgohqv}*Ajsh`A0 zUV;|R2pKYA3v+_wQhHlSO(LV=HJ$niD-vdT`Cb}o(S*|e@*5SS33a#+qF9L98r$BRUor!tAwlhGPAwt&tYXJ{dV9^u#57&iQI)6d*IWsOWE zn+3Q%4VP3_Nrtw}h1aK24q#L%3QHOF3hvhbkFj@%5-i%bZNs*0+qP|G*tTukwv7zi zwr$%+zBvDv?t7I~lF^qHP?im{e?`Vp}@nM&VnnU$XGKF;tJ&_3>F$UaDtY$^&GfIho8XdHUJ#T~J+Er*4-nug?0Td`GxQe3oLPJtUHv_(VpA+b%h z)5OtG+s+de(;(5Vex}pQYosISt{DbtM9J#wF0ORJ5~ncUOL_L*=lYa?B1qr)mBA;1 zec{O!2e@f{k!Ae5)K=>qN_Xcp#{W(=;stM};cT)~WCMIRp*tmQ!)h-=S7VIha=q<<~1s)pVofYYKQWW7mR}tF_w>IMMPo zZ=fjj=r=dNsf^z}y*-_!r6CsFL8m&Db*I6SivTZYecNM0w)hU#42I$9V(q*Lg7%C4 z-DcJb=w_ZfT(rjk>^8%X5Rf^e|f^LU9F5 z(;LzDcwB0)QWwd=hfABS_Y~z?|5j`G!s@J;!>6P;Y>~&=!>r>Sw8>kO_bM#X5Nzr- ziPOpWYEwSfFwkULT~?N}?O&_v?`IgcHJ2ijV_Cj1YBWK$Msv_AZ1#yx%8BznRH&ml zT~~al%N!FLqgvoJ5%T}&kZlHkjVAB-)Kxr>KMZj%K>`2bU(lsvg;-^6Id89r;xH z(y^ZkJ)m!?t!*cY6uDPl)rQpcra>nzF?Me3w!u3ittV4>O9e#yMcwd6G1pBF*7F~(8DEc_y z3~zrpCj<{vQMlc^@s9D=v)gu`C!ObuU>7s&Py8S;eB;vCbpiq*D5}XBIl({=h-!!V zKhKg!S8oC|fn(J}L8eoqOG}<`CqAfNEQUR_`HW@ODHDH5J*Rcy9gZHAoxgk0t`0YW z%r8!HJ_05K#I_3E7}Zbl1^SnVX2YdTzLHic$VB@Fm*`VV)gq&SUC0sYb%F31+y;$Y z+J+~5&>pn=O8l>_V!dhDr%MWoo#Z#RcrWWn%9T6Y(I#rvK56kmq6#rF0~{|r(J|Zg_d$PH_VtyA4U+Ga z7D0<3UW=oL!?5A#qz~_>V%!W|#5#w7h6 zDco)y!xDZJw9aG=T~34SQe4ph`%7viLWj54$vLZ$L!`+^pEiVJhm{*EOWJUxZs6lK zMk>jLk-4ipR~9t(#|IDdWM+=98RLG-j?#VGJ#L&t^EhLi5|WePdMzB1^uzDyr2S4*hV^XBmW_E=0hHiBz2(P)Nc$B_2K zh36GOHuaF0V%AgkpcBD#!+gHj3m4OKQ3lWDMs0TPYWC<8{SN}II{AZpd3f|wG;!!N zDk#nv=4bGPE6LSKL2P1j;t-gPQrlH#YdEMIr#9g zc~&R<7`*$pipAjt-%C-OtPePfzm&-d3~%xlP;>oj3@^*6xf|vdi$LLc=%KpwKxOXc z;N{sE@fOGF8HA;Jv)}Z*R6t`v@}S=+k`|->soYC2i&pL0r%$8v4CMn)QFL&l!nx^S zS~I&JBGJDcNVN5<9D9qcApjMhF~CDIs0+BPMM+-pj5VcM!79tTQ<+0I;j7jIm!*R- z7~JD_QsF#{BGIIaX6UZ(5gn#wCibG483~n!(6H|<14STYQ^UgLV^E%ypqg96Gf7jK zHS?M1h>@z@D^C13Q1IrJqwYtqLXyf6~Q@eNIIvZUV~;l7<%=eGm0-i!qYOx4sSQk z4C3z-6kTdk%`^3q&r<-8J0@_Cq?_27dcRrbAz zho@%$G9b)k&Ke$f0Zk)1m;#Z>VwA;$JSz>MW@nOUAa)7pqVEEYw(#yRD-s<^5twpI zGY+u;&n!lwi$IEb=N$|{ktF4V@FPUbf?*1uZHy7#9s##6Df@mHrsvG$AN}+DbAUum zl)~l*S-dI@g)jT$UzY=vv_z=Mg2NCN;FN$zsy9;*2xXXz#7zW9Z3WZxk1;kX-N09!p zK-o2)mJjkIiyGP}Z=HBmz z4bk><=oTnKk#%7yEddM}rYTIK$hjC2&7y%H>vQp*Z{+ba8K-Q(PmcH+Bq*rr-YD#*!rHtIaf66U>KcT<}r= z5Reyiq9Er*guzAu9CT4NRKXzlxr2&z-#+NsxZa{tAB2pS0xuedSvArAlEOSI@CWvG zGp?x&I1QyM1(-aqd^Q)Vr!UZejA{<1K|jy}hakY#!HtEO4wG1lIe-kFMXvas3aK5( z0t#@U6|VxZ`xMyH4i*FbHib6U2-cm@;6Auou^())t3M2=_IoaYx1Oh-`1F|t2+;z- z4v1E%dkLv0A(|9Xd_YNBc>pNZX|xg{gQrS%N3(ZU>mou8fZ0GMJr;sChW#}=UC4h2 z*uPAlfat<6mKX4`XR?kdbYllsp%N^q5hQ$t36!1hr|c2kdOuL51RA~sT2Bi7$^16h zZIFL<@6E&6!X&hb*O2Tz&hv;&jA3K;1EZ1Q8V$r4@>ogTl9DAqIA6aq7(SRCDxaZ# z4N=e_@~yITp&97z(UAhW%}CqVYDWfj)z+3f;QVUuryM6&5=rv z68bu)lK#P!LK9fZT8I~kD9oZ}%P9|hg^f8>r{>&0kcGu9FWI6^8py$Aw=2#bR+$EV zqO$ZCHJtN8YSg}9S{x-g@>Vc8KB$MTnfHrw-a=5mXYRLyIxZ}LmsQ2SkC~S`K{J|b z2fde9sLn0L=B>-?1V5LT)}?N=T$Arsi|d78YDRYsRJ*8-*8%Hk!+WskT3+JT_ZQYFhdWPlZ_r%@4{B%GqRHx5;=#`KM;2^_Clv zng>JU9GxygP6cc}9iI>STDjB~pC3n=S}3b!hCr=LsCo{Z-&YhZEYqk+cuOaLuOE2$8*g~ z8A?oD0%5!cpuVCBGf35f?7ro$q3*aHI_h@;yg?Cj%h^hf^vgO>6VGTIzmzsLAC%Vq z%&*~lnoU|=VnSCTd? z`R;Tc>`hN@t6a4c7CbGiFiYmMAxE$$TU4%c z7i%Fqlj)R3b|vSXI8$C|hM}IR+^Qc#Ub`G}95WPA6$t8IURu=|-cx4Kq3 zT|1VF)13@a?bfG(k{PqxFE$TZE)q(yO~4Se<(-aKReuvT%V}6OG%>^$HHW6jXOoes zzR)DUcuW5ov$yQmuqnE^2HPVBkQX!uoU<-kvQUIGC`ooxI)#|fnshJYl?X9JDO1#q zmnzVG7lJ&M7AZ5Im%ys$fRK$Er!20L%Z)mNKj0W%>%lSh)}5$rGOBip-EOu=dt7({7!kH67{oRFaC32;xRy<8l+-=FZ*I` z4l2D+x6>FySr^^u`RST$Q$+Bbx*35Jj0P~wHi6UHlp~Pe)eJytOKyo$vk2IlM^G;U(Yr;y?=|nmWNY8D~^g(t$H; z*zijnZ5?9Fie2yam0Y}AiO|%ck-I~xr6M_S{HFOn6t+YCC;xQu=8hKj7RBN zIs6_|Y^kGX!`3_WzS^uRR(z_b%JwBySwK-hS~(jnDNh2MvFc&G6)MyQDHid34M)%TUQ;~*tH(p|LXX>l?$0E8&F%sO6*kh4f#+_!hmflUdrzh5-o`u+A`HnIfTD67Eq^vg1^;d9X zp~uCw+uQeNUpm>wzcJf4^K4fF2DnJ_;4_eAG@b~T?OA@fUBAnpK(;^zTN>65d~zRQ zA0LGk>K>$8FHcVIFR~8&f5B2p6WqQBRbMcJRl*|P?zxfnyW}b6j>SHx#IV_h`5~_( zzTOOn2zfWRM#`)UZ375j_}Q7H7yqRm;$Zw=)`Vb<;4UNJbD6>s;N@QN7=#st5@ic zv!L4TF5SOw2!jk+{tipN4t-(Ll%px?5~|@Evx#?w@9)p!)!WK{Lwu2t=R9sLzI0z9 zxzF9G3-)Ni@`K@2-#?e9mG5`2d1?5*YRh^5I*WhgCMO&#e6aT8qUwxs7F2c*$Puem zmPdyhOb<#+CD8tP9RI;l3eVTdL?K)#exhn7%hbPTyiMcq`Rz6?ARP(dVeY3a(~4wZ zOYC|~@K9YOdYl>VA#4djz+YFAtyc|~pUc!*Tu6paIFZecbfcA>Skk&8(nur${YIMTv`s)e6M<3u00miGI*cC!a++)ln)Pv}wuO)W*2+ri zxEsCA(^x>tqkkvThzPV9yEFB-0E>`%ue5-a6KFT$L}Mu*tqH6kv@)>HlK?vr6^w9a zF8euV4Qrldh` zp0q;Gfg*yoz+@K^RezgbOuCI%v>|F7XLDZoLu|72RDL=ynUPtU5nGts(%?;XNOK! zb<#Ve|H}E`5<++hK4rhmUx(37P0W#H$~#n@!v+eNBh2|q;Y7_c{uaW*>20W>PlQ58 zhahu*276H!7Xdmrb~ZJ7SWcs!7^$rW8&hPdGOw@C`$emg@7% zZc|GTsU5qps(9Sdw*f$dUj7rEcV^!Recodn;X zwCv}b_56!xwY2CJQh>sxLrDX&P$fK9>EY<nm+Q1`??h49qtXL`o@|;ewPmuBz`usA^5!HTJO?MH%}-{p$C5MqU0sK!qEG zJpxD?2>bk3ybQpJS_2{>lvg-p+9~>rbq0dYP5W5}ShjQjwCe*FlzbT&ci9$#WI@jt ziGa)(>*xMd<{6K@aOSq;d>u5oXmC}@JBQ^b6mV;$Yc^E@M2GfCppoSt)Zwjl+1b*q2 z73L-iD{N{a(TE)WCM4y*ExF62e>O45p+@>~aSxZ_5(Kb#Dc+F+t|;YdLy7ca@1NC_ zLk_^w+ue0=@e5F}`ya>U)-sc1_ViN4DmkV*Y#rVmvzD80=&FjEJc~J6PirU|F@ZZS zmU#2rC#>@`_1644SgB4+^%PInbS8%sxm;=R%cF171~Kv7Yg$vRL2Mf=4!>YTlsf>+ zwDpZvb$p3p9a;4|-t;>s`%Fak$4)^BWtLg%(Zrp{J{knk93B zzk&+KWK%W99qXv^IiCT)^ZNp%Q01uHeLYCn^W4jd?VHmud@{8B?zYyR#6f*tX1 zzd&&GU2dfD-~8da|GY9OzWghUX16<~P#{ex?eA60D*w^K8gP^*`gUlcp+!Izv*93S=QZSzO0o#HZr+x7cn2O5@;0iRelLCIb1^2X@~ zddyDaeAPsi;P}7?A({8NdsQ#c0-NbuWCHq|U;v;i?$246n~6Yfdy3~WhS3#?nh7*h z3lQ&S!5sYGDp>;o2gJSjZa!aX^)v5seaN-J8$}c$b}#3SpgEpD1$rNq@exrwGlXYP zyfVoD8UeL*D7TG1fNh2m0uGq?u|M*0IH>6!ooo%^p6uzvjdra#r3cww{|R>F*$_j< zc0#f6^j;GQ_NQE^B{WSvsT==o(3ebGM@rfZ3=(IFVZ3KPpA~U;yP6=_&5!1 z1lHMRNW>mkJwGx{8?jwoS$TD999ca-M+uE?jrGAbnmxF(Z`skHtsQ=F0!}AbKIByV z_&W{Y4lA!4?WT7q*Xnwg1G}T?O$vc~AX_MqoX7dlU(Sn07qf_UXh<=!W!HRtV~o~d zyw}8Ofh&>Ww;t?t;H-Yo>EeAZ40>SWRl@w8y{sRxR{F#T6Xsc9t#Q4z(Pthm`54mya`Lh+aS^9@ z45WE=^*Pmnqu4M?4WJp@ZRGLK_nwWJZI_LlT%OH>z9enHkoQF5CWeh9xo$Ll!;`#% zSwNwDH=DkbldSGlpS}~AkWSS2hrl}MPV{@>x3VD^-u;Cu6+w84(;2JkaZziRTCt1Q zPFq}-tIZE_UO}!7|8m^(NJR1c705pcj^DVin4~qAKAotAaX^@)Wy}nns5P|JGk%>- z5S64gG%UOvA#e~`tl~SK2=73so-xIt63LwUbsnuQ;{`ZNYH=%qg(6vLvh9mAsddPOEf7b7w)=G(T08HmHe!;II!^~!DXnd_PXr(-ae3&KjG>D$)qnc?siE> z?2glq4s*2#@K5u)X~LTVU-pWo7GA*rW(8!Np9EeT?XVP40G|wMa`LCzsQ@l#%+2nq=#t1e1J1~&*vzRJq!>a|IEB=)8Y5mnVVNx>8hbUB z3=OGKxShy(K|RD|<^39@I6|(ht$!aun{m@(- zWcVMhOmpgHED9k)=fWZd-{BAvDIp<>kOGQ z_;ROy_tXtM^t<+l$tpEhx7W?I7>ZA8d1s$;R`t<=esn|!B***0l8(CD7Iyh9vnxdT zYEpAs$J0%sODge#i}uP2LbJTp)>`-;-?iMG$j>{Q#&4|k|88^xZ_&)a*|31hEk{z?IF{cgs}8Z*psw);c{Rh#ruj%bDUr$Fe=TxE*7v; z)Kt4)u~+CQzrs|VkZ>e^QNIwinj~GxnZa~Zw!b#mBp) zA+3^&gv(1j`H)B|gjclYT&QAR#G-jdLGZ@qUTFA1Oq=eD6$_aB50Md#@-fe$nS_fv z(}8vUUz9&c3mCa=Byyy&LmRmq3qCt18)ADttJ@~7ruXR+F($zT@YmZDa>xx?JI!E=x$%kx;*L(pLjhDlp4%98vd%Q4##BPNg!h@Ilp0pdAidd`f@RFGOW+V$KO z`Va^AcYT{b9=-3KpNMD&S0i?|y%>kJUzjsqkM9;)c{{=-v{IB%3AY`U!U`o¨9V2~$5Ab)t;<$ilZUU|I_HqLK0$6w64o&glQ z&$Zzn9d4E(5VwG2Ajl2M0m;11K8{)TY%v~*5lS7p|3jh_YJ(f(yo`Q!^oM+#6w-sY z*%^!{R`FW&z!0P~eS%0@0iLoD7+yyt6G_it?wg>V6{iTo1O$6?sG4bl+*^CwpB$e! ze{8*LPutmW7^3@vrBO4dpcb7?dHyh509e*Gx&j&t*rP2Rm>^)G+XC$dtW2LnavM!^9+eOqh9G2}_^(`CY`Wt3&lkXdX5BHzC!)+l5q7jI})q(`H zGw>?+J@09damdfb%?7Ee44xE}&- zygfZSnuI~i26pjO&cwyTQ zg#&&D0ESA-fC7ltRi~uyL6$SRg#wD%9~`$TV*%z@o=(94M=JwPJtYnrA|wFC10Q2L zU^Rf}-*#h@<&~qT1!x1Vk%r1JpmN~0r{xQVCM;S2kR-DpClZW6MsOR%u@EP;m+J`j z`|UQ-y!^&e1Sm>vJ}*#Ur;g#Ir%t}+$=l`iw{X01qxq`5zRPRqge~K$t?1wMQ)8QU z&hEwu7iBdRUU4@{kgmdPcLbYwA;@j3)`_Z{kbItOY?y9}rGlcfQI}_VHEf80?^tia#oTu^|U%dpVflYbHkTx{O zt2TVtvbw8S{ka(-?B2UZ&a(-NpuvLQ$ylI3*wvD67Pd)&S`n|mMMX%5DlUF4hq+P% zx*2^F1wp?Z+N`8@N}x?5-Br-c)^U<@ry8?>O-pWL8pGwgW+Ue&Jv&nbGs6*>dl)<3 zWuJv*iI`2<0v=Hk(V9M}>2lqi)d{S>y!x_vu70+`8sq)oHgG=X+|0nX zWcz^%1lO==F)BThcH9k|3!Q9fJ}CP$-Aj{$!Sqzy2q?tQ9G!m7Qk#!tai$WW2jVgF zsWNOFs+T$5tv;XOUN$5v%+gN*ru&#J592Rkt`%rM9=y)EBcF?a>~M#;wJ+i&RckN0 zh_2m__wzrUA6K1ahR)ZW z7j1`AL^Cq8F-tS->yp`|$&Jz3x_@_|sE{*Jz-`C8!@{xc=9Gqo}5>P zV1Becsd5z0b@CI*@n+?o+sEUHT(Ce++D%=R_|P)+S0S)TwZ^xwfcLDg*UcK(I}3d1 z$-wzvn^}D~Hp5;QuLoPlk~i^63HFo8AQllNFSL8E$^u=60@Kh43iat4%IG7kM zZC!?SYBcIQfG3y?ae*>Jp~hWL35}K-L>A>P1<*C(FK%RoCjfS-{3zXR>yIx#?2%0) zl!vW4cy4@;tOjEaLYwl%&J)#z&vn$FRXLm41xBjWhq{;w@+h5S7|%xO>U!gt)lpi+ ze9`jUi=Jf2Vz4AP!s_zqWRFOd*ppF>y({+{{4AEWTHB_=s)tXTg}qvV$1)GjK4*2x zatiBeIR?0g+dpucKt?Py(F$k@cQ2@A3C95=BAZ!i!DW zccjX@tBgLwaHMx9pE-UFi56+KlU= ziE~~w*db|CCt9keMjJ+ry%S$1Hon(trDym5v1qmrZ423hN028j0R5T@>H3?D19KH? zRcmo_BozM?4%jhI*9+h}WuR z!-^sm4Eu0g7gUZ5llTNy3kLH5W(@*^J`xr);+KP|=c(qa!F=>o9ec5-p4Z%iqes~? zA#@JFup*XPR$0J2=`>sd&P>QzHq{hJq9?KiB!Cn56B5;I#{cGcK17XnNzvt{$9E;J zZnMg{^@Y;Id)FlMEZ24i3+92MjtrPN+Qaf<-p*nypYS+}u1G{_M^U)7v}p$As_x~L z-5r5})=OeAwKGMk@$3q^)cFPq+3?1|yjip$V3FJX%ls68ZcEei)KD79ITtL@Hccv* za3OB5fP2(1h(XPQcs`8OQx>xJS#PVVTyruZ8tC8<4{|iX_AJw0OW^X~ImtOeTq3-^ zG3M>~Y7kl3sa#7|lBVkJq(Cd#U<-GUY96i~{=@au@&M~gA7XLSa2wsT6`F-&QSw+_ zj(%N9uV9QKZfCD~LJvk0=bJvLtS*!L z>e8bWC{`2?1>G`#XLw#gyNyj27gn-M7pWsGw3NI^c>qH4buMZC-5cJCiA&sj-`M#j zHZKpSdcFSl`R%&li6YcrHUls$adzuUew#@)J z6}z7>iy``GKdT;TuxD0wt?&ZXZh#@lirqk~iNa=}UP;|<08O=iEi7xaC&I%mi$U3E z8`*B)WT8Bv*z4*u_t?!6vEtdy(uq&?F1_)PZnCC`(Fc-Ms3`sC&1IVtmh}nR)d#nV z$o4;HmWQ^3-Bhi?Vlc;M(^Matqd*5~obvtL9@G6PUhj8L*pBJmhc3g1)mzLmrBK>| zjhtQPSZIFw`TVaU_<8XCjyBOtJ0g;`4koZb1W4alY$x3F^U#G=|mc>0)vZah7;B zx-ro#?T)cNe=I4rFr?zuA9?QxZNb_l3F4F6nF@jD!Q%Lh~w={0RJUhSDQ2zQ^PM#78pYU9f4 z_L51C%Pb%hqd+WvK4Um>4^3V)j3ytNGY|N%YYir!Z#_zp%Z*7S$O0=k@%m^{YVM!M zvmT7z-a_BD0NJ(@rNhpYVRQqO1c|szfl*UKFN|opF{QRywoMf6NPp9t=b$xXeJAujCSl?2!yqeN()aZ}z5C+ZgzI`nvm(qC-(J?6NgsO1jjY6GV zOz!Q!-6fxqwHt`Lm=nLts&-SFkYhUZJjb~8{c`yDPP?H$Ble=JHd=vsEbg5Hu#1MZ z!(pubeE5EPNxJI!Vcr=AMj5W2;3nYc!4fHJ-D$XQ*UQMzUy!SJrj^fte0g#RWaR9p ziK-8|PY-8^S%2_jT;_-PZ^b(6|4w;hW#IUKiuI$G`|jStJYWqp`UH?b0PdF6Sva&i zaBD;GelCIRf8atDC6!o`O_AzuR@=-`qVUT+7knDUToqqWPS=CC?6~iXo*v&{o5#KA zgrT+=eorah$x<&l%2GP2{B3f7Dr$TGJw80@)&7I@Q6VpO>BcQ}xTk08V{chCJ^K)h zSq#5?`+UW(YlZLq=lT93?(+TYm)6j`{J{03)aekaM@J`v$ED|o%`eFevr3P?xs7I9 zina)=z8V4X37<_J_9>f*J0#8oOFu zISa~feQq_(Y$uHHR1LZG(Ez08vwzBl%6l=aVn8W7XR{c`D~hjr!ixT61ya}teTBTb zr@s_v;^db?rAC|A8=c2}T$3Z-73&9%W5txL`WJb_^U-#pVHrYLot>>+z(YE}-m*9| z{syskmt2)q+&T7JD8b9IZHWK2zMj&{xSM5Ccko34-ryXsGvsV zb%o8ys!&SBw6q6laA)(ogC^rQE93CP{ba}kkarpbY_#N>Rj!uI6l63+6$uP?kPt?e ze>4(68=)i6Z{~(*y{riQc_0wflNQMPNNNBJ8H1%cYR-~pJYN&o z4ZH;Np%T!`aJig#;zdDH5dPMhE%%`(;uw7sGzgOAo%8-cxIeGOj^N`vkqdtC2Dqq1 zW^eSgpn109t^R8Fwf3GG(;xH49Nbk2^j`#74rXwnLa^Ang%v8Oy9Emh_acB3^TF?+ zvp7Ue0Ol5jJcB9#NBtcH6b%&%=zhYW&6vk?0b_V8f+b%i**hhMNk?;UcN3P5`*<@67AiHy^~Lq>Q;+lhS1GuMzEIs zGPg&!-M!8t;IGl$Ygw|YNrbSs`aVLPqhdJ0q{WGANVy5AK)v@ANS);8#O{zbKg2BJ ziD5;K1yDm>I*4EdZK|j~mI=@sJ&YSKP;I)nwy#6%O%qIFVbUc$uaf#wW%=pMM!$qx zOr*wkFjhcWHjxZk-X_g{BzN<+J*UyM!rM+Cs3noCGo5J6D&<{6x3?=|)r@!qK;FIYGiZbu}nih)vGh8v_oD zteUV!o}5mm&S1svP>%&D6Pe+!+{hSw#3NH4M|9LF(AD)=P4LmWG|fY-eS+H5DL5(u6hx%FWmvOU;)`qi z1SB3*ut74~?Qa^m{F5#4azswAxkytwBYZKGq7;=LKvjDaD=MX<2-Q#D#nvYsQC`H; z^~Zw;6;DliN z{%PDT2H0K;nYH}^Ny8g4{KJDD_fyDAM5ixHfR@T*SRTHQ{C*(w#n7cz1Yt! zNmA`GM%wB{6pjOw^V6)uQ@kkXG;|wT<4_x81#ly~o|FLlc$w*&=#TXH*c|)p<|{Gz z;Ck;tGiSQfG1xndQf3bZT+YwN-!Uo--Zm5B03ow zwkR**`9auSR3@0*m5DtJB;16Z@p(ltJtSO2U6WC@c(5JwEfgEx?9Z-&OTqD=q|{i& zZLlw`T;#yHa}6@bLL!V5{MGn!^X^g9>7kvT1e{J0)-bMjq8K z=gFBXegC|I(VefT^!Xa;JWoVb)Uj`*W&Z1+(@5pMF{D~+cQ=p>Qam9M=}nb_6#->w zlXzU=Eu{D+>PwXkDLZ6y1JrG0~J>xbISP5i$@f#{8=Bl z)91Z){4WbRB*j&@;z}Ob5V~DWm8uOWiKrwG6|VCQR7GT5SZHf~7Bd?-W#dLWz;gSJ zoP}(gsS~9WECv;n{=<+#+*uUMn-~E1QRdO&?Z@!2l@2X>nPf{)noAg&RgrO={ z?7z6gi&5=N5eLzi7?|^Jh)cyBVuS5*#65T_(@REY(BA#COTq;Qsa2IDr>)_brS7FH zyrti+41IJeB;D^Bo-i$oWnwOSy5QEj@VX94#~kD1U=9nKDq6#m46}_RlC}F8HJoyR zlz2Gg?MKXN800B~cjX+A1v3#zv9cRM2P8+EnGOq#ZSu{M0zvtyfieT6D8UrMB03Xi zK?_CeVw1F;P9a+u4TCpV97}I>owxd@mhw`3Z1*+m|A(=63KAvgwsqUKx!SgE+qP}n zwr$(C-M!kjZDZa3_dPHBoc*vLvZ5j@qADM<5;MpA282-OBIU=bKquc+P%;MEJ0V%< zw!I3J*yPVhg4`W`0c*lv>LOMLse2S#g}NpYRPqj-1){fU_Tsh@Q7Dp`jyYVMpib>| zQGOXexj{5J3ij61+E3Ymd#oiC=69#nt(E8wgaZHMq`r|8*OfG(BPWp0rQ3&z=fXDJ z09Zz(Vv$JKmcs0s?scOPlRFx8|9odlJqz?#P%g8**(>XpB~N(TSwn)(X&?9-pd<`t zH%^Yyoxs%jNA=y^gM)g*zCVQG#qfMRS$!Wld3}^urk&4j*P9R9*9$AV-+$Fs;~-k* z`o4w4SS&xV52W2!#0<_B9yGM9&$KMe%UQ&;7QdwWIn0Q z8F#CH^J_xUw^>8lB#S2dM*EZ=2g-3$b_wI`yf;>j$9j4#)>NafIxY2RvRW{af^}60 zcX{i{RDs5l(zb6;q)tHC(K|(}FxM0_xsibg@?3eXu9nfgV3gUtJvfiMUmJ7`Q0-GF z=A9p(r-#}^g};ZhiwHHjl^wEz33c4rb>4zaqR4jq93g->wex+4N3Z^AIz7#*R z4cDecbZ6nj?D--gskf5^GdZ=27}xv6E`1z!+nO7h^@3u05eygzSE=IX->SnIyKRH-(8h4Mj3m0B{n>#@>%?3z*teUX4!4gQx-WN3!Qb6V2Yi{76+9rA#PyGf+_eMsGmzh5q|M*;8g!sygPA^BwZV8r&2b#r^WHX0LjsG*6r@)2 z>7<9D*>r=n^+#CJW7`BHo@2Z7W?@(QTOGdx(?|#Z-0rK>0$rO|m(COdQBpm+9zuQK z63(vN-dY*t__QP?D18izm%1F`00I^3@5%DK+ioh>l72yKDC$)GMZNa20A@AcSM9=- zTbz_BA36UJfjQODBL!XeWp(o)QC(VS+(MM>ETvD8y<&38Qkk0SavLxr1uWf_j-)p$ zioQHNw1tG-i&j29=g{oeB>Z%f{w-QvLN*g^FiM&45W~v;kvvRXi?w=^R<%&6Nmn<< z_Z5RhPb1=*U%v%bY7Di&JgxNdj!kr%eS$~f3ojNidf$$FE!HFfpRKL&89B^yc&Z0lk6i~XIxd1wHLn$Mf51a@8LRL*I!ERX2I~dBN$68kipbn$= z@Aq5thb4=XqK&sOs@Dfc5xxA4=~_+fwGML;P)u_rIw&<9Elx0DkZ9S*@UQ(h^P^GA zI)pGUX{B4f5EBE5A5o6*8kK^4{@hE*GKlR5Nzr%PlKRYoi;;N25C%;E6E;zmkC&po zoP?=kY#W?LPZhFmY52Lpl##D$6Z39qun?3e0tu`REs*ZPE&$E5fuL7FsB-G@u$+_71$q?UGd{e3=en=r($lp za7w%iGZMURuE|lot$BLOOvD68$k`TO>5gCGZIFJK={-IWuWYQ@3aGH{$bxr&ZMz_t zxV`inn7E6bMt^vK7tj%>!;0M941P1QCMEZ!CzE|W~y)d zH-@X#iczc~TkugFU?~-$w869)x(-0wf;W($TZ8R0U5{m_Y-ef3TPkzLf8a7sf0kwI z*AoSN8#5Zis9s7TT~GAPqO(Xbm5QpWX1}qD>Rts#G^vOkWouAhX3JX$bC@h^etTHo z3RGFShE+?OeeYq-LR|%6>}#gBn_IU5APerT5s%91y(~KBfji%CbKO^o09?A=)H`zz zW29=&-OH%**6(dA+{zDQCd=5}B4XWu1rf)(2uu3!Z6j?D3W++Urr;xla%(RnXjXJ@ z^9FDK0S3JY`u_SY zm?)Wl{42;a)>v8N!j$1oibtAjT+1tQ>Jx88m6{$azxOK1l*04y{$T%NLf|-FFc>{a z4rzfRWQ=qbkoe2&`(fn%y7EIc;FWexs|FjOrb~@&LQKrcT)Jy#H+Fus7`Fec zg~|8DRs6nlcgCC*&)$s(lUm#^Z;afr7xbvWNZS68j_p7v8c$sg+64hMYHkKiih(%*RS2R- zf5M!_TpUTND9{wr9O!*CQ=HXre0*ea$C$@g=0r08Dd-@*THjzH?qMJ@D(2PCT##x_ zr4tCmeTz-gOp+XbbmC*J0o+9IPt_e2joHF%q3%4P9{<-8cDm%zu+cCtI@5u{5De#i zWdoDyC9@~7VVs*aHmg2;O|f@UfFX|;-iCUqLFTV=ri?r4PI6Eam|;pI+ z%=&WfF`O&Wkk#vQ4uJD80X&J6wlbS++ZXi(xDLHg78kR&6Z3Gx`}*o5Zs zvcfasZlhrgpt+c+#CXkhhlUYnSYWjx8F5D~mKzWjUUhOdI4cwL?k@375W(e(5p~J^ zubZij`~!y%A+qXIDW+}@99=$q2Z&|qvZ3g>+j%ahN6}sBj%=i9!N{?=A?)b*ys@#q z_=GzH2xEr3``7!in17G3hjCl=i_xEBNdYK_0w@!|;91d|1x~P;>r)m0jpOPFq6ct~ z(%}N#5|B0j@n9t&{3x1-Pkw-fl(~d2P7#)t``O_QJD+Ob2cPo-ej zY<}UAbvd07bz&x%Rt(8U((iknL7q>Rxb&uF!-1h8y^Rk+H_ipbU*(rAuie1 zuBqX@v3bISt*!CfRJ5s}dN<@&t;;7^xsKhne9moZ4%J16nvYW)Q$}SCa5U>wj(3}b z<#t{MljbxsFXt@wv$ji@v2?KIrz zQz+I+tf1y@{}={rp_SY;m=t2E-DuCrs?I}zn%PvS&t>*I((9;Q)w6(_yFk}1lr|Hu zW>~0BLL3)+rt3aMtWEr`(EGk)aD-wLgs!xI=9D~b`WF>MNNu~Nyo3MEV^^sY9@zS3 zuZWg|mLhZ&NK+yvup~T9V}`*o9qOGloHy*8j_ri_c6xKgr6f7ul4V)ng@t8=?qc`} z6+JkWjqS`B39+-RT&9_HwWKZGHsjpfRDQUeBO3O>+(%pR0BeXyzziVNp7Cj79b!|@{2w|&tZ;Am+CoIkeNoB854+&7wI z8?v=|v`QG=X3MGA?MBlu^>bKozRTKGV{YVio{M>G?Jknn*|QPz(3=Nwf31R>d%S65 zu)2NhKUNW8>S#k|t|P{xa%sxd<6yQ}ta5&|+%Fki+zzhRu7PynZp&}H&>lXs9b}!g zqhaODF6ukAy722LGhS9y(YJ~tF=B56Tq|OVy{~U>3%9KC^ zyKO!6_K8j66PQfTI)68zY&rRak5Ur8w*Nxi4(|5xxs9>$_2TaMrEa(N!4-6>6f#MZqhq~&?{OBC7;V4a$y)4pt_EGl zeXjtIyp%feG>>(t5sBEtG81G6i@`E4a=9N(0dn4^8%)P3<9F(MQPp5kXosEtm(Vod z;?20AHjMFhKZ8i+S7_=Us5pI=Kw1Nyx%d~|Oe_%4Q}3A$ee!zVO*o)leR{k92Ttd49m3+K1X~viMBds@n{IwKASu$ z@E0UwfN48A*gZ6{l@Sp@^Pfvkf*fKbO!GTM30u`lh020ZER%0f{9PE7z3;V$x8VS^ zzxXo1F~9(()?QbK{L9)d!D#^}#YNh{GQqDz#zeo^As2`PetKXk$m;;s1Jn}6zugG9 zh+7FKLQURp#6gO~ZFS(4(C1yk77I)fo-sC$0vt(%u|3VW3Tp|dg=@hkIDnAYjIv0f z#=?lr6Kg^pdk``YQAF>@cX}ABeS>;2Ee(fV`ZoP*#;E>A8ChgGy>;$(*{1-Z^|{Zw zn)D>WlfRf4pua=(QOONHbBy#G{N#FsPn z(El2ZxAnM21LJFR#KWhJF8Vsn0CE?|rF(Edj2O>xV+acR!WzSF1EZE!Q<-{!PfB6| zXWrVJ5nZRt;|!a(7i$9`E)8l*V8#l|%wztcPee|x1NR0%Fo751P_J1-`s-|Wte-r} z3kb<+*}=u=dBu%TP$nJHnIN%`K_06QY%@%>5L%GbFJy^e0EC6dZyZR22)MwDXkm04 zVThll)QOt>c`1@)XxIFpGsnSQk|w%NFK{B9O`X?^v%@#kexQG)?p|>-bxYp{=fw3y z3-dfcY_;+$p?(9WeOiTSuMa<6d!DjAU3nlN6 z8o}yTm*?>)=AcW`i0U4=+>+a|^KDO!F-V9@^lY{A>_STo(`e`t0h0n1xz#5%GXRdy+IDHXp z8TIz}BPH(Qq@}Zu8g+9HiPD>`7CR;a$T(qbnrl+8sQWlY5(rNz7_ zGLG^*$-LF=c)r*9It^|p7|u`EuU%@LPnezys|_V!uQSbTCj2?@uj8A}v=ORIvo2Hx zY7a-4@)E5v^q3@CZE2&N?+&Pp*JhOBRFy@bF=uU>5tn1n^l|osHm!Pvbm@>$sELYy zb0mc`&|e+XV%jlH$W$vwFHZ%u8ISoIO0=~}gW~8pnTzy+%5ocY+T>NEzLK{L z1%Yt)<_HB=ii1U|*)j^5#=*^^RI=(I^d%HPjS<9T&sR5F^?^iH4Vsj}e8lw+fe)Gh zA-k6F6NVLUa6Gt!=+ovDzb4u@D`8MuhN{9~j@JCebZ8=JTg-Dy%Ehm}_(qGW<}fu^ zg$^z(mT#rdasv)nfl_Jv#(Tn5ZBCwts#0`lwQ60ZCt}VbOc`2qN}pS_8k0GB49KLB z&i3ibE zuadDWyjw(BMO-)n&5W(YB@^g&;p6Nms-g8MlzsHaPoQ1 zX6V4%FnKu(N@kPxrWFwYY_4wU&@v)_yXSHA=>`VB6e@aQS##?KAB&gmL3hQTKl|G$ zelx(?l@4%wOi&(wf=JO)cOZ|NQA&;E+9PZmC8GO}0o6aFANG5_KA#ZdkW;bj9l1+a zhsOgBG@va-yf|y)NdTRe%k+BL8SR0wqJVLft|}5624ew@mM#que_ocXkOlQ#l2bX) z?X>FK?A>wW+Og+#J?C!)s>-Im;exS^<@Md<2hq|h%`SPvY(f6{88yP`?UYuFY{8NDW=`ITe0`#*+W~A^U2gF2(rTA2OEJ#& zkOo=sCULH7+B9;Yd0#qX2C#3Fg1?J`W5u1bDl4Xf>s2y042GTMmXXorm8w~n7`I1g zZNISwm(`Vz9Y!?^Jwkz>gv>m84agjt1tS6uVbD9)RpAPQUm`5B!F>IUR=6wk23Z_U zDyR`u$Y7JZ1XB&z>3GeC$AoNVyJ#g9E<={7ON;VqJM@DAqmwng0Fwoy)hn^CA-=sF zA*!E?EY+1nG3q57(W5vm<-I!^kslxwr=wYPA$cyOO9XW`C)4Qzw63?Qzl)Lgwn+7%_d~~soUp)n36JdOA37wvt=%>JZIuvr?VDm9I#6Bj3P>5M>@*lc?fv4H zl8p*%HFmO;%8fFoSD)j4Eeds(`JhvG87Hu?A5zQrjC#A18i$8LY&%mn+m8l ztp|t-rG(Dj05SH`{H-e$ylXW)+Fm*!AHY@bILMqwoktU7<5{u0D%?K)pV}A`~-?WaB7UC^`4icL>W=0TOE2h8^^0Q0G~g zF_=;5Ol^+fq4##z9fjKQ(vIS%jMsW$l14RnGzw9IYSsQVq9Ju=tTL&H+Uyo|&GAHr z_b#{_%UeHr=mOa;2G(W5Cw_)($)P7gU*4wIL5HGgh=x_WZ=UJGO;w(N@#!XMx{_{- zkSv@rqRl+2=N93_C zhdP2`r^7>&EqORuFXTmsAeDGO--efwC-gs5Se)~R0sDcagWU~}bFl$z2-5FRvnhu3 zCpB(u*&vBs?{;p?59R01furBX21*zk_Pw`L$Ue=S9$I9dWx6T1%*edni1H~!LRg={vkUkMPjXd%@;mR67T*wEjTdQdB?D8_)TtU%bRP; zq#Y7a-Si_O7^?s^`X|~Z!V2gPCkX7H*wh7PKZByKXa;gT>!)ws9bF+fu9lI?uph%y z-MYX&WpdSW*0*MxH0I5A5Z9k%tBfsweaM}2|97yja}dY^+36J5+F*7KW?66lb`JQo zlx@F+^FdV{a5gTvF=?8e9!$`c&zH;lyW$i0LOh)PKQhGYJ+cRXw>{cG9oPTteBA$O zxbFUNx~4L%M=)6d<0>D{0k0dml6cQW?|$Mt7p{wY`Fi@wDx+3>P}I?g6i+C7Ig138 z8rt!CADw$~FH{%mf8&)?!WjL(TRTSj|D`06nU$X9|IQ!$FG=G6pFapd)IWO@org@l z2fsE97q|`J@Pj9=K+_-zu0|4ZF_i0q@T44NyiJWJwOq5%@VN@{b{qDa=w4*m>)rMB zN?2m0^I?YdpkP6=k>6t5Y+tY}Fn;ptr|bJJ^@xX2q*dXKu;cQ0pl2M2=j|-p%DB-S zZN1?4>wnte`}W;76~}jZ()Leh!QD>z@Yh(T=I)Y&c@hjY%Wj*D%JA{|V3X>gH7Iz6 zr62_K*H$Ks{2Z`U`7gZ+_@j+KMV~h3RgS+$t^6Sox5a-jzq`Pm_FD1~H23S=HyppZYHPE1jL`$(OKjKt} znNevJs47Ms8mmi7V1$`dtie1f{zzFZjbz7{Kz)&fRIx5jSLdI@v?K!tt`H%ps<<{O zK!C;#W>bGio9e4|y^c=l(Ge>kbHxqGTyqJOzUGAL&|ih*Y`6qTbd~l+;jd^z?vj;< zhE{YXO@Hwneg#pQl)d5<0Ig<6A_1s#&EbXlEpgYJ0^vZgN(v%FA`zl+YYHv6<@r#t zPl{3)>_RlA`uHt0#w;y^0_01Yb%k9Uj!4#at2ymrt~vNhT{rr^-K_mc(1UBWT;(aD z8!q6`obsK3rKh3W0u=^s{t*Ul_5Ug5Ff7*o8C42yd9|Y3J~-Ge;Di^6r-YHqbrfT0 zISoXW8%g}inemaGc^-n&#iu!iqf1U6h708I%(K*O1L1X~-)wJj$O3OaN|E^Z{^_s) zkl`-@?WPr~0`HPhM=`WevE7zNu1 z1)YWZ^^n(qJoCGg-IG}7&&~}2fOj+Pi^D01OssO2lA!=7>-Fj=27v!6-Z&$}>`hPA zp)&@7yxO!}(~nzC7{k$r6qpB0Xy9?pkbsy&uT7@c3reyG6GE}C%GVm>-(}s=2PKh1 z4l#KXXLoOjC#x+9vauj?Ckmv1Q0Oi-7hm;VL3?YN$yjTuud>y+(qn=QIN^g0)8*0e z^#&A3(EBF*WSl(KA@I2Yau3fIIGrKz2Hd`XT|XotsQznt|FWyshtwqyhlcn45liZY z2%(Fn@wr$1I50YgUiG>EcweYI`REhgyTd%wBp44tWDq=kU(43g80ZRqPB-J(&|=q@ z)TWg&iEAY70IIN#;6u39(hmunDO?6R7`1(&h;N8)tjVn{ic+qq$)>6-c-CsEX>U&v z^T0K@rC{=MamL9yqH@ZY6sxXqd`O#_?T@@);|O#0Q&>b97~^6Qee38+3kLAg0Ag7u z-Cx6bHpazf(=tzh{UOKh)M=Uyx2&BU{^g`A6jq+OMM6F8e=03I(2eJ(ePlYi2z&fm zNTuw$O@2zKAoy=oq~zs8Q;Ikqsw8tncEvggEn1twHC!lz8Wo5bfK$_Fs%y`?($V_W-u`t%o<4}t+Z6%rc9SFpjiz^a_#MH?o;Npo)Wx_u;?&!t&bFt+gKwsd z369$@PGb?Ub2d5&5MUa>UxfnY1)ruv-)|}b! z5#c{_7Fy7FFE;Bs7svdVPTy#A<$RiBQ2L~eXV*t}X^iJ@P;{6ae>ig;7f!@_ev}mF ztTP++m5ZtHhApYldj6>>(>=;=)ag3o2r1C5V^glfH|5%KxQ24L;kG^RX|9Lv-FwE@ zIBHHJ;~8K*>nQJac`7UL3@va;=OUlr#lui@6#n&2K-ej2(P&Q1FpQ$;OqH2CVgZ$ zh_Omh^7TmvXYEAHtmVef28-q7?gH#WoS@+2Q=Fj0<1?M1C!C(OlK-ylTz_TF7ODl^ zePHV*`&PDdlWQY2k_mL=?GeU(p&D8fEslBHeW3$&0s>s?^hOEsDv+I=LrF>zhM9#~ z6eg1peSDHJQOd?tQ|X!9CNTIIDS{C-uC|zdlbVe0`(zkkoyXM-%7h`jm)9P2-{}(8 zd9%-lTev0Ce?;O|Vbdc~^?d_M#bW?s)u&G4d5VF&Lr z3YO;+w%MKi-<7<9;}1c)E#E)tb3`v-rWYSztzGT6vdn3@u@~d?nkKYk|qL5{Z!j!ar(JVBBW^}xmb`# z&bA1UgzA`ty``VoNDu{NO3^^6@jOIG!iO*=I1Z$(+F@oSVGqAbI@V|qyZn*=)C~UC zNV^FKLVFJH^auLx_FIYuX5@7d4pcC+iZkkf^E45!O3a`?ic@AyD8TO6kCxGJt3FhC zc>BKx2PlzR8{})y)cT&$$yP84lyYn3Kq8*5a3bS)PI>u6UpstbFJ(+G^;+gY&gwSI z2Io)r3Jm+_mNS1wFM>WvWgBv#e{R*`aBmB6i1CcSAA*}4=sZ+^1wHKPJp5&vop67S zyUcc%9bEz>h{Dw|#ba)XO;Q9GMu$_1-7$xJ_e6g@YeRT15vspDrbj5$*-OtWO zMr`|gkj=4HFhuOcFTd@3+E*-cH8aBJ6T-~(XQuduLs#a#Ib`gv^MO$1cv~BBdW);jt%C2J__XF-Y~r`W;@U_|n`9A(iQTdZU#PQk0yygW3rZv;;7oTQH;Fm~Zs2%0f+RY??l+$)dl}T#UNM7rttW^RoiSNO3>+L>a zzY-anc&fQ>!FrXb%la=;oVNc}RSBg7wz;oG*Hc4kpwv0H)1@QGaURd1?SziCBkT3; zA2bZB~~YP%$d$MD{r=OLQpXAh*#xT^#Uh(x7CI^|EnRXRM`3FCQb1 zDmPEhN~tM7KUUJ6pO(DPC_Wt5C|K2+Ncr`HTKT`zPjRLXjN<+@E13&))|JPqkXd#T* zqRY+v(7k894r!~e6>ufVb0ey!*I(7>Kao}_P0=IUM|ZGpwe5N+@Q=RPu{FCbKz0ig zoO0LVj7O!mC<3;ae_kecio%4ztt~&kbe9clb+}#>|7~r1SeojD z7QzfB8?KaO$5LFlQW$BpejVhtap-8mjkfI~sAzYqTl9Q~@x*dcZXuVVHH>5)II&Sx zbbJ@En3OI>TiN0{9l0ZfO!ML=lEZg6gw4QNiBVu)6+RY-Cc~JT-Q9l}+md2SOHZEK zmf4SL(F9Y(#Y1!28p06VzL0z|+6IE@P0jb43R}-R`dCr`{A%dT>#LY-aH_pUhtNK* zE6`o6Kg!Il2?p}q@LVNRp%8z9j4Mo$*eRq!F1Hw@TEwp)H7E;7=5S{W*ag+y1aB9V zUju{a@9bj4gQqj{-i){^(y!yPenU);r@SJ$Ee|0e?ZmZWh zSWxtObw4?rRm*^ z4g3NLJ=+QVnc4#3Td-BhHhY7YDu5@bQFcQ(zAA!)CXnkbW(}5(Ik7*#4q`ESK2Ifn z$`Q+k(*FYHa{FoIawp|1(vHd+xHVD>(<&03?d%@W8n&^=JZP=nOCNkfE`A&xE9FT9 z+%MMx`^{EC1Lkhff>%nPC!q)JG6|h!Rq9us;CfLeA8Sx4gsf;Zuy{W|5;Um>S7{LP zblUg{WxBd^KZnOW@NAuk()aHM%Z4VMP}L(;oS)~35r-2?^v$I5QF8lgi#=oEGaOg( z+)p9QY6OfKxI9j$b#FpN!>iKQE=2HFKy%aXrSvH2x)m|&I3oqG~ zXn#x$?tFy6I63t|aK0;^%r_z3;!LTO?8w-l`P+|5WtEr$oSbq*Bi?75cx-{5{o=Ap z9grE1dmWI4*f*sPr+fq?|9qDME+c&4Uylt@z&M-Y)L_XUcR_2P6dY9GoZ|bM1uMCK zMqF+YbkxDbHy)}qgMB$kDkk<*j7LBHSY^} zA%S&?IICt|&6Z}i>|a%PH7fFlVrx*Iwa87dStMw)dlt8_#tM}Uk&R6oU!#{3#9q&c zq`kLc>;wfjmX~Bk@zxF1QAt4Z?ozko)T)M!I3F~cokuqAn)oPHh(dwIs&J)Wz_82E#k~Z@;llMKK4Z@^kls9s7(I z@;1h4^l$CA=48yvaq22896{p@`E(pk#|rXLRl>qt?XUGvH%H_ z*3_mMo#^|W&ai&QVY{>~i8?P$6yAtCD?p{CsX0Jg_3`Jta2;@Cw!XmT*S@%tX`hSpVrBY@-uxE+CZ+yQ zQKbL1K!E+fG3Bmt*KLpf4~44mju*fPaI@jDI|}dtuqlfgEf*l``#(lM6P2mMh!sRP zxea6x8zKqSpM=LZAx}ecV|%-CJnzfD6sjM`_uCMs#jLv`&IbkY0Z+1$D`KYFG}ZB& zIX@lW?}AU@lkr^W%XF5Z&Npn0Ju$NCyPj5B>!Gf@Tf9E!!!SO-zOSdKE8l;+B-3q@ z&Y_UUgK&L1+TctX*S*1VqrYky9cwlBW2-WO8&W$8k5{i+I&XLIRM*q$FjD6YcdhzC5sJU?bkZ}2 zZFz{Wa8=vv%0Cvs5#L*?yq*yv7TQh3&YzOBnK&DgeyE2&H~`SpW`o3$6QCzv7AXOS z{!KyHS+$aUgI-J|pigJK2@vQgE4G@WA4q8Y1i-03sM$-{$={N<5Od;}L=_)w?;qgb z5vLtRjg?R2=CUz7w2;c^?XPhtS|Kt4!%rtH7R)vX7W#OHZ6?65FxCdW9QH-6kljtH zz|hE{Fd9v&upthuU@OR^DPRi6kUA}Fs1m$A{3B0qUZ&wck;|v|1G_Qe)gB#+t?Pk$0^wikY-2~ED*;Bd`B#C#( zI>y@26FKiKZ23v7G3z9C;SU+ z@=p^qFj0*FD-JuwgTHU^K!W*Q)1m@t7-GHbU%9{e-CITm9hpAM5%(s3OwDeSSQp3R`)nGhI?VvdA{<$MwGOS6Xfey>44@t|=U{I44PmF* zjCUZB(E%gIKp8NS3QzkZb9s{aU-B|IEb%Y6Hxh757j{It7Y;y6;-oNVkti1}`8`)H z!moPxeO%CpGeD=H?>%P3?U=b-H)&~wz%J_Zm?Pr&;Rzan%Jbvob2QJFyq$IwI2gBv z@`&{Q?F*4%;-4Of+{#=;joQ=WR5uC-;C-PiKQUAY@y(J{0D&+An4wA#wR=tXqoDNM z2E@?N>Df$Afb!}?qW!}$j~K8Wvy zvjW)6wP5{?T>_+T9u-q0#6z;)vpvBS4EoFH7y@ss9&zR;+!F=ks z?b^BuAG4Pm_Tx!MumZPzhlw!v6*@dV6p4_b@z!zk`Dm}ofI}sLzDP_{q~K(hbBHn7 zLIp#vgXK;G%bf-$-S}MB`Z)!H=_VrW9AnU9Ia_n3FVp{#X@+=>23L-hCM9*=Qw@j< z0=h`@0|6-fyoL4FXS_EXwp`X9X#TOPX6YN_SmH1qnfA!PlZ_*iQn z>qOdQ#6E_7Y4-D`sagK@^A<-KD?AK03(ys;Wz+sYL*@}o`<$u0t(bPv^fci~-=B|u zPOtENx%b6EE&E%u`&N_u@mS4B2(D7%minPfFa0rZev~0&i~OsHsQV`Npq4_d*o%=F zHN&St{&}V}QDjOvwK;Jc zgCw-FlN7NOcaA#Y;M*?Blj!FuB=$s`?2etO30|-4QB_M4_F$xju#^rVBQrs320AJ; zN4XHdbsXl&l4d*CGcOVR_M5Uy`D;31a}8P8cplu^Iznv{UpE`njbdNO1A;0l-y+h^ zTJ>vXsuL{lMNnapT8ecRj=5!Ioml&8EUJ`@3NM#MQOlP6o9lt)hpce{TLOQsl;MO7 z#T8LMb$by=WwLx?B_ymyhvkd%C<3p5m5cl=yR>aV2%p_W44BRg zijl?i2L>627gQZjCwXe{fW(;;EyL^6OmJJ>NDULarh~i}kPKxh8Jfa@OzuZFJDISu z`tjsf^AdeuO@i!})Q5tZ(zPRjW>+%F;u0mHD<9+YAr&CvGEM<9HOO$V9sWq&08Z1m z;JcOBNoi0`oD6%sSnHE({J|V5ANsCuM}MUg;U;JSy9I)pA|=mtfO6-tgiU1>F6=2 zN*OG}k*2ej@w+G_#b5XZ9 z6blHkRjQ|!Mhz}ni6sVD5}rJjyGjB!KI|jnJG0(c6ICSk3lRVUdnLr@X+22z^B8Sa zyhIc8>~l4I^`!Ie1GQB}4IMFAcWvDEiZIjJeD})S%QfW37eWq5B&!TOK}zn;F%Wm# z`}ARJ6$f|-FcRe=(aXBg48yef-jdtduN?Q>+0LwU#8Jun(2~t<{X`1i>;Xo~Vx!7p zDBY(1zZ_HTp}&3jNf+@4wud%_aRDnghC?v~D2_hJDeYt2TFaKK4ngc4osd%5n?MI% z-#_XOMNrE~~>Ff5@qwGuzKnJuL$EB-5@TPR zKQ>#i>09h@)nC$oIWCF3&A939j6fuQ%;$|Fb}+ESqiHRf^lUJC<|Z?4QSv`a3m&?~>MYGn$}}xnGsvHgv98srChIHSvPNpN?+Gevv3aadu{j>F8?QY-KBk==vR8Q@ ztL%YcuFq%=V|2bp*5M7p5w@}O^Y-->PHqCja&AiSDQLkJED1((98ntBAuLFofZIhp zyo`vr`>4;$4ll8R2T$gfm=^aJ;CqIQnpg1M>D3gRflZ^nlT@`TA<5R#L8aX_xy8xv z((PMGa)#Pci1*<3v*p$+#;?O9`Ms~gq&&C#(=)(l=YuNKT?JUd}YTTlF&5>w{D105#8r1zfPPzfgsfp9h@I9al1 zYBxkha}f?*>x;wfJVOM(ca7vUC!?Bq^Vq&L|15tOCfFuktaI<%|E`lz6&I@(gO@Ip zTrVP>Q|Zu?X-Dp#Qh@Z>3t<|96r)ut-S ziMvn3q|%c!+UE=~iBk)V@3&JJ&~n->hj{Gt;>UJI*;A8)*LuQY-REq9YmRbp1u8z; z3AdYaF{Pr|T26bk+hRpOWZ?ouzn5F31J#;3TJ$Knyv3?9`jlV7Y<71f`jVX9GXF+I z!L>MjS$3@TL(EOQZ3T&@OVWR7W_RxZCq16NJ$m!JW*Sh>$;<`760bGM&4nyE21ktj zu9*j}4!z^ddi?c#Wivp$d~y#7Jw`{4d6ak94$G8%X%An6+mCp-%NYOhX(x|3{tr7m zhW{mJn30Kz^}p!^v^QfnSrL3z)y_KjF&*8(BZ1a!Sm$Wa#=))a$XP)*e)#@kYM_WE zVkR|SwFJ2b_5aB@n$DmSYX&)E@Ai!F+l79a7dzAAb^GTm$+3TC;`xHcERNW`(vwOH zfm?j>@4M#nN#`G1?7UzA{6d$(&R2xYoliZlCt^*hiL#tupZ68K-OqQC*Q>%WMP+r~ z-7en^Nje|Z_f{~j33S!(&kknY$0{5dmac2Q`2GjJy+BDJCPhB?0zlwsMfgb}1pBcR zoBeHZG|@6hzum@J3mcKdx_|M1l9b??KaL#eb)pc-a4XQziz%WLW@DLSc85o(8Iixl zf%E4h-y%sX6FBvQx1^3%f`JhQuy?zib<1Aso^;R7zS)iB@(@jiN*}O-8F@F>3x!)I z9dE)>|o_F2wl1kxv@l!*o`Lt*B#Blj@;g&%oq67xwdL2#LsCi#cbGL%nSFj1w5 zrps?uWBrgR$a^l|qlF)L=2qYah(KJmxf~>vtKhUrEy6`Tj~v4gKsNqDXbOgxbB5*u zN)VS_!8jDgq|0OyXo8$|Jb-P`=V0;9J5u)eT${ec_fEl*%}o3r|7L9(8YXU9Cx&Rb zrDk>r1crG@1txr|fMt8_6=R*0NrX(~>2>nWclnL>W#%7_xuH1IpHze8PkZ8);7bEX zBhdOKP&n`bW_s;1&$!pIf~9ODb-N%yu?H-2-zaot6L-u{=Z~CM4)ZSSRo>8`JhA zlru4Fv|=n2-VGC(!9*(#a+Y+Uh=Uk(uWIiJH{;|O1PagU_qtj`1?hvR+U#IT$A z2p@p8{qNoBH7Hs2-x}Y)5ZY>bYLIBmM!%nyGUx*PfwhH@vW1Yb1yDBax4SLKGx&ev z{ZD_6ISJySnGA|^PpU+3hM{&4SDmIsTeT4ANs^_M)}0Cp8E>%RjGkh&yG1e$x)jL)tH4YwQ7Ek#Z^uXaT(r|2Y5L+XLTNgj`{oZI~Zp_OqA&J zhk3EPM9m*V0F}a)$$xG`|LyNZQXN<%GmLVjGgV7crttZ^4_df49@gDi44_H%NEvK8 z7C?yA2*bpdh|}1sd%)XQ)A2LNq|zn}`7-#SiH)neMp-2r>E8=d%%Y*MmLQr;M74;X z2dd6smZsAUPDiFBwwS`uE9V$wOOC|c0Bk^<4ve|BfrN3%+BmbHXwzitM|{mN)a|8@ zOqQ#kGS?~4`bWwhqSNME!isv36gH^FIDI?oN1pf}<7ld1K#U*J$-!gSpg@yeaHrG! z1bVUbpB2Qddqa~Ec-#Ce;ABT-6j)D{SxbT;J{#tI7fDOqYf*E}-l(xp2Z{*0S;4*+6m{}H6=-ZIP84rabszl6A6Dr| zaw*i^ti`FPbyZFZ1XSMwtMQ%+l`oqdC3#`m>DPb$-Q#zZzZi5N>P_74}NpsFSh3UqEZWc zRpS2sOq+6OYp*KO#k}PFNQ>=cAqdqmoBm>VEK_7hS0c|~yw~7fudr1#Ztw4FK04=X zzO$ejhSSy6$MQxoJ*92#43-R#%SYATI>Pgq_uF9?KQENdg{q_F?tIP?rye%3&lI4%QQOa0pX^Gh4m=mb*a-fiTdhs*xbmCsPoNeo zO@=r}A9ojNu7A}sHQ>sw$Tv_?rnU{&>@Ic`5bq$F2`r@|EGcM7s{1lyH`mve2cNg2 zJ?f6uct^*+rAvr?qigkhhXj>S?qxS(pwnEt^!z@~@wI~mTD&x#us9V?mhVE9N6*n! zG1B|uXVe?AT;bA_%Z9X$AXft{`H`=f|U{5=9c+L z8^ubdP9Bv3#hEzZd8fs;4iD~y6-%zf!&O&n-@6u|ywGVGoBc^=p-HQ{*Yxai+T|Ud z7*7NMXj9(EB1q5E0tj~^4K9)|<#KVb*K|XpZa>N3T?+@r+E(*ddKb1GnM;m!8$w;D z1M+)x!cQl`!Q7Je3v>V2DQ%i@GVi*1kqnWog>tR^r69-d zSmm<+eI=kWg1XmBXFbcbDEU~)-1OSb=;A5e7mQPz)e{#!++hHR+2D$@cN;&hK}EyL zG@7cB0SaeDedQxNIOeRajbt=V-SuW+)2MR35e`*;85H&&(7BPCflUAoknJ>*b#Do- zfK`JHgQ^$V*n|rE4G`6e^cOnBQJCN4e0g6Q+R+SLaIwe6+vTq%hug<=fhS0L_Lv#f z#NL~xBK^rw-;31th3mG)lHS`fIz|2Fp{4HGqMc$J!pd%SNG^WhuCzveJ?-kVUApKO8j~xfYJMoCvb?BV3G zV|D(cVVKSrk_=}dd9^a_XFz<>lB#84VZNjgNcjdDO|Q%{iE48$ODa}D4P~a7iM+^Z ztWV|eYfaLq8TUf0B$$lZi8h!&>15iPdB&>JaHQkghp`6C2;P*@ zmAY=n1v%>M-~=`$^i!LNr9+nJCmb(fkVk^3j+j)W7J9fGD1%i*mI*de2+0g(dC*c2 ze0q_c^1f`+U!cZ6%%5~+Bt9>(1=yIm3JCS0!?W@21)`Wn1bRwAEK863q(AwOQH2gY z9K%*ACFW$HNQHo2_2OT9%Yd8#vQQAerwPn50p-{;c!ufH0-zUDrZP*h*o%~Cx?-oD z4FVDekxU>Il9r!gJ>+Bo4^#W{Hy1^*Q#}!ekm6Tos#XRbL_?`Aq+SGq>SAV!=pK3F zVo+tV0UoXjN;Cy1r@fw1F!|6_r<3)+8Zq6UKKb|}i;D8gZlRh-k^SrZGNrgX-pOoVH&8N6J0Ov@ZjL}(KG zg(jqc?~-Zq=b0ego05=zLE{nfe;o88`ftw%M;{3J>c}=Z68@zhuQ=^1RrZi=nJQma z34(R}ejT3#A_s4I7y8GUKAC#zX-SQ)G z_(F85{X9q*W`VQ!aJKeYe;3QSc5duo?OwoM`xL8bTO(OTuS)7cypn(lJ@0Fn9_(C% z^~Fml_!h;+#fKk@bV}vXZ|bm>RVkc}judTA_-mTmDZLt8d)0eiLs4)yC$j9LEi+@F z+<&Fl*}~HZwLGpckFuoN96r5FFTFo;D9_}eb%La zMai&O$9p`CcLcI#wap-Fe7q=*Bc|F&!Hd^;*gl{3E9|m2&<)Z^A9&-ENNkc_6i%<( z?^HoH(!sPN$LOhrW2-vdEmgA6@oWe~w0CDGXbqCpYl_oty+jorm2m%hZ2*l|itzJ< zcs9vGEr@@w&7&2oP0@mf*fT@yWD#Qx8M=`~9Bv^dKkI^Ev^^c}3BY!v)Vum%f$X?A zNkph=yi>3v{98A_W1oy+I)Le|m)Y7fu_B9Ps`PPnJyDWrdZO zQdtsuHQi%Nd97J?e_WfAJ#Ym6%f@zDaW2pCi??KdbYTL0=rDK5$Z$n&>mzdiw)fe{ z?(2dv2<019;T%pIsy`+uK*q2Q@wF%Pe3!N_#)$Tp^)%)1h zj*acakmgbZwyo0jU5h38TF82GMqy5tm)Qnl zkYPIy?E#pbteV4f25=|z@h#~Bq(Qz|+qX+hG!J9#MrlrC&k94vk*OgRj0WqJnoM+w zVz;xXVGd7e$Frcp_E%O-x-f|mKEee!BGXcyme$`t3ocxOgOeV&h>(;v!9u5569ByI zm#ZI2v*>`H2{zjSEp4iq*=it4OwxxZbTupZB6`IZyrtpVm1Q$^K6*b5tXUUa(~>rM zl%@hzRAA~0^FN)qrl1_6!H|8UdNfx0sbPi54DzK+!I~-5r$o+Pb0xXP{>V*E5M|TD zFy~t-q?85Cw}cfZ8P>f9SMX!fcvAv$aq*Bp-I>oX(DoScb{M`-E+#M($ek~73fP5!yac!#8i zy#l`o$nMd`h`9d}HUM!)K7b-);V+u^0RX0@o zSIQ8YXdOb6grh}=00HL5Fjrr&Uvt(-GW^FyJv*O6RJ6fPI%+R5gBpVT{5K@y?B_0l z#2?Oo1Ah#3L0*##FUK@f^v8uaQ&xv+%!t|!qWibd9c;TmiR4&b)N_q;bB=n7Rj+<0 zu9s-;dqfymuHhhIK23ByO(W8HUqDzf#)(0w@JMs&v7uC8GusU<#`MKt&1Rp(R}a^a znez;ZnsuN~D+=dX^H=hYwy2YdI!eNN1Kzxc*ZTwZw&d43S+wEPo0ToV!*y(~BZ~wn zE8JONr>I&Do_~*q{oBF6tgL^7qnNo4RAYinQb>-PCV|F2T93~_RE4}|YQS7wvt5k( zD{x53?Uh=#DfoR=7KP}wEDZ+;_&M_6uakTEnZ3Ipgc%C(KR9QM7j|bsG;y|$|Imtq zxltdM6%@W>4OROQZ4US2AhT)hsx-_#j&2d&`8Yg7)~)asY>eIehPV?sg6;UVK;O(c z@HZ^;7KGnDY@kN*!XdYPaqwNkzrII9MtI-4W3Zzx;Z60`^)&{hzl93gKS`ZJC}_0R z9+i7r5OCa3y}nbUjMaiHDEE>hnP+b7F7F%fc-?B@C4;5y+sx?PfeWUuab?{ZJHa{8 z`>;`Y57Sy7wb2QUFf1ng&Icwx>^Hn{z?KnejQUdhMRwH5ZBtB8JXVu>Q-qlBFxwl& z0#v=;*ig@KSz{=n^L0GZCQb-#W=X(doJzbLSR0k#B=_L1i&p2|x(ah%hN?So_eIDn zc}%)e^Z zs~=2Cu`?>hRiBq6#uZVhF3ttz$o+G>oGJmiQXAv)Ckj3xu0-$#{fuCs)3~@ObeD2L zu(*_PLGZL9-ernMIaYl!-gj%l+EW4L>@TNvq|209x0qJ7r_>}RzR%#~kyKppVYPa4 zI;g@hV*3D{95_WO0rGstJ&?*w9Kq0)XF6CGZM|E+~P$?xAd`_bPwZSEXItC}}8 zT+EE5qsQcATNxX#yK|;Ornif)Iq1X@I@U_4Ii~LIGC2=JP7jviF)cdjXkk5N!xlC# zL(1l|veUBbGYIq0{c2y+FB;X&ZyhKqOjqqoE-Wy25x8rpr!~U zoON1k=eULvTpBX{bVHo2S%y7XI=tAJ^Cq~=Mmb~VIp+$)K0{I<7B%&N_#Vv5u?*q| zbz6vvyJyXif&Ww0-I<3GBW74^8WPuss~)pi#WA-)ps!PUMikml#y5D~0e|v-{v$U5 zIE>QZxjB#MHjoh}iYhd^JOrcWmr`q+h(b>c`<(eK{5%D7c0)mDgAtLYw{ExdhGDaI zP~~;p%J~t~=-mb;>fHKsL2tt+z;&s@nR8KyIt1X6nMwX6MIB|F7z!#}*ozP*tvV1- zOpzvcB|*LJoyk29nug@Tx7+aHurgQRakKm7ES>2>g)AA7{g5svv*lTN*k)tymHEq| zGpEg)#E>_Q&10`Sc7=Ofs-Qz5x94(p6op0H zPyp`WiJiYMxt=ZGggQL~)wB2Rk2IVfgicJ+5Q39vqtb$tk|Pe(7SeA--nmVNNz8Kh zWz;>7Wksk8hc8OH*9%*!;x1a#HFa#~JE-*-WUFdKOK6@y_8O906NlWQaYsv*7tabbDjvajJ<;~ zNQ^$IE6!N-}Dy84}LAit;5TX))Bd94W)byBL)#;w7mJkg>cCyc7ktfv-iN zX3xxJ#IH3;jCXkkn4h2jRyc>gi7ZcW^49rv`p!_D{aIThpEE`B`qpyAKiJ1luhh9+ z!oQdhFGXG7uVt-a&;b-w-82sqB^7|1{8F@|_=F zH-^njv_97=kYeTc)JKnhu`InbEJrPLjx)SQdFR`5C9U34_za$kV&V-ezuhd*Df ztO$~QArvZj_Rx$o4CotBo1`KKc;+|1f)WvDQ=$dDsACDQ_-I&9xK7ky#@;4}Fsn}b z-pTel{|@V^y6x-NB!1h!ZOn_znXfVK_c3t$EIlYk4BJ)qLVLj1tpB#(*W?$!TZ{$f zs&7@-UdJtt*I)NAEJB>=^sKP0t-QW}KKT5fZy#45dBa~X|F$+gXy1CQJ1)22`nLSu zR=^Tjb?q@1{pbL0My4Qk9^U^3{SaU3u2)B9hMV_aM)(r$)TqpGIGGpLM%l7o3XKc< zno8Ac8@l{EIKkEo2iPX`CRPGBDT+eXYKFB3lNo!Ot!=%w+}AbocNf!ezCp0Tk&x7**=RGmRNn%JNDWY zU##N1XWC+-`-#Up+jb%>x=pn(w0RwswIIUqwNjByk$1w(6@xZE*lMX;(n2~(FY-T3oFTI`EPujoI2MG6atG8 z#iBbNRG=J_PIl4VjnIGBY^19+MeBLOh`+gQvo%_*QqJK+k*ktohSqrF$cd8M&Zf$y zcut8^Lxwmb>2DlFqqT>5=XT45nC6|;51ZHCa!|ya;ZTM#nza@$)i^ZBMsyKdT6F1i znN>UYdP`0uy6w=;tLV<8b(&?pKLYq56%^ZaJWSmXhgRHz_BKekXkZNsZ#N4#P2c_& zY%t}k7_4hhrB#+o0Sm}9t5XbndL$)oZbM9((=P1hO?92kcuz$*OFY8CV>}s7LH_ z^WYA!@pql*5=>aLDXgI<5TRw`q3Y$js2n6)j+0U%qoJhPJi~(HO8c+eyaA_Cj!;^5%i7Qs6#+H&fSCFt;2F&>nLYL=3e_l7vl5~MDwyL*xVF6u>Rk~V9nVDcktrBEM@tzGM6728Fwu&nvS8SoWYPf5C6$*a6#OQk&0>7c_1 zHjK??7Wts8!)6}`<<0CcDpv8t=|yeIiruxg{#duxrx8h+skdDaaPL=gBpdUQRhtue z{-K*F)vWH=9oyC3kHF!Z8T38>3ypxZ8ea9P8GrRj*$tiMm8?abXwyblTz#|H@VLW> zIH)9t?=n~ZkB-LzdWm#tJ6=J%@p$vi8{)ekqg4}|7W(C@PMC9RQ>!zX8{+igKSK4x zx&(7yG##$?6xb$N_g}c+mHyV=bREHsU+v8&X(}ZZUk;}$*p^epu8H1eN8Slcq>wdk z3Z?Noy6UC3x{XXn11w*N1X;G=?_>+_OIZ;qVo1TpT-p@4qR%+2JzqUWHrpGJ4*J|g zmepYz0^hxao2`udhN;LIt*qa}HCq7!h=&}h^;hw~skF~E##1xASJ%wopoTO(q7po) zw20o7KCL(bm@d$WLP-{v2p>%a+&5f_pq$RB)H_2Vo4DonMuK!}E-&-uDYg$181K!E z_@LKd9p57ZBw+d)&3d{ZPJ9WYT0<-7fEmra`g33$F^d&M&4^3nNIm}Yz-RGufud5Y z0Gi!Rz}JDoJ!O5@pORbN2G7-5O6o;xsfc>}M|A6?v$Vle}r3NU^)v zH|I%Qk~t*rO7;WD6L;ktz%KuWm{8vj|Eh#wfG>(d(wo8z0|A^c4{z|n4>HErICVr> z0=LFlfLLBM*;8BMGM$BC!^WE~vdwFt*OHj*Y>3?VNxmd#LVF7eT!olaYs+=MtlBjq z!96IzDlp|Nk=D9MX6<>LOsY`_+Ztag?c6uKQyRV(RFHG&1s4daah19`!;YwNgS!(W z;>T?=YVROofR5g8eG|JcwS}|w3)A~G2XXT;4#umQhD5~0U^fF7Ei18YdQd{lEz4tP z47EzxIc#_`%iB8?Gq?joaUdzo*X`S4K^&2inwU47!ej;WA~dB@zRQ?dBov9O4Q34P zDq)I8o7wTD{@8&2Dt^Xdy2sV!UQVtfNx2E4<5;C4bqYgs!(HA{{BI%%NwOEN@bD}u zV;2Z6NH4=A(s5qmG&1aE2bSn)&*ImLt0;pUItw^}V|-^+$)%_bM5K5`xHx$+N7#*~ zqzW@MB}lbP8`*cXl-p!<&W5Rgwg;d@MZyUw4I$d7?5A>`%r-v#))Nv)r`}RaEVW%e1a`qy+g$1jf9jKUOrWt^pGKv%YLA;^pgSnlRCD24k?N0^78hxD z&{b&(YeC&QDxB>UHN)jN!7+)%<{6<(N9)R7#3RPC(-{?cd>0f(m^J$L;m;5s7-SO7 zgWbcBYLnn@)+m13uUJ!-tzSgbsyzK7DIJCM>*KO!^zn=tM;W(v4H<}U#iWweAAJYogqzfz*hYZ`05^x6{*`{iLLwqv!&06_!$oa-1ch5nl5VR7(urdkZ4u zf)H@4)Pz3fiy;8Rk-rR9Kj-x7M-TzXU@09y`w38ah=^3>y z9UDY;9}6)jiLqUVIm}cB0%0*S%(Q%j+5=Rx+~e>F4P5|QDxrQis{D4^GY=_@7V(U` zP@UOS6eSVs#-~DFt?2VF_f~g#AH$$2*0~im?2OCKT%cFE0LAr#_hMZU!dAuf^;9YU&Vbf%gA)?KMFpbind`R&wdf!&;oj&c}eX{jMbk` zG*K~SV)GqK8}n*e53Tht99@%Lc-T?$ zu*X4is|(CW(lMqSLo%UHz${)8mZuyBU_h;@GX7`;|0SWux*;1|cBjYdw<2)1Zq!Pi z2?+T#`P_He`s30SxGJ=8rQA+yHbo@Rueo*Q3;r!d&oZueEYZCV%cy$iYIsnCElr_` zduG2ZF!zUg-v_We#gU(dwq?CzeyhfHde$kxc<<}NsOv6vrE6Tu7l(Bsk;X-8|E0m{ zO2KWcqs0p-i8DS6ck5T&W~kZ8{<2;r)x(9%(R&^A-pz|D!>}`c0<`eeH|(!PB{b|x z4|Q0*QZtf(Q&0F!*Gl!AReoZ>VMa`ltHFsp0bDhAGi6KxwP^9%k!R$gnXr(I!|pyI z8Y`VPrG1-&;Klcjuro*wI&K11fix^f#p};Xa_Xa$Xa)>D0pjr<_G$`HAvuXFz5oY% zJ}bXprUQ(i1g7i>JcAFljD-%n0UlP?e=N2WdYw)^AL$AHD)_|C$;WAjDj*A;XAHy> z_6DVj{kjOT&M=_itu7iOVn#t@?y5wqi_64iPL< z``Q~Ofd6a5FlrV3WWE4cQLweVC<~<9ucY4@+Hq{kc6OX_zG8NqAS~={ zzYX7^JB}DOt~-t*=-nr2#Z{)pX~hr6_$GuW$9N~Q#{D{17}UWpywA!L%t1bt*=ND- zEZuUUgYI;$8`LpNjSOjDcS$AMVXVpT+467*Ei7{tHE}kLlJp62w)WwTM;8T>^tEy1 zLCfAFs>iw9mRIy}Z|K`$eDrDCVU&OGC55sGHTRG60X#S$h^?bGcew~S#@nv7SAl^& z$@rrC0hK=Jk7z;zz@{34(6+nH3QdowYSFI3P;)S*8hoG-vW?nUYGZ!{WmI*-N`{E| zR>DFtG6@yN#<{*j&6pR1?V2O1nm>A6<`57AsP7RF&3?C3t=jWCrtiv#>h(?;TRttN z)5nyodU|5`4K@n`l~+MMoO^`pNgsZA=}BkrE7Xaa_8GH!&^jI}ccZiV6T6?i#XMLA z%#=Xr@lXinAM1UD{T=Upwk4}P6VlTk?!GA@9-N5QY3QHGazf3)koCUXhvqtM;{zJL zxA#p~%$g$a?2(dm%P;VE1vW^M8@#34$0BumIt_cl{flaTfA(2JOAE_T;Dh{P)a|X*#q--cYmlTeJ(VBlsxz?q_)IWX$y5o z>u`7UwA3b@wH(tgWiY$ISl#nH7CqngY2yJ2$Mh%Nz?k~yg0kGri*NWPbx&DczWGzp z0BG|C*H>>)W0yXQvc{tfFzajp!{q<1WuB@V-Dz>y9hHCi#k&QVCr(}IDp52r369&$fAHPts*B9+v}6NB0vEau@nnFnO#dMjZrz8z5M--Px*)Qb z%zqh<0_@c9RRa%SZX~#a&9K=^zf3Or)9yh}mKpF0BazJ|y+_BP%sSo}$N~yw#ro(8 zH1HfRQNH{-0dwZ+z(Lw@a01UoK~3B#S|*;WZR)tYSX`&jbOH^ZU};z_?(o0kAY~Xv zD;kO6v~_q{e?c2J>039NYV~oxsovcCamTTU1w2?`6EjQ(jzF!O%wq!RQ}L(JL*^)2 z+{Y^g5$K)(`<%c+KXhGqknmirgCbUDM{$hg`$||YVev3D;C`??SHZ=ez9+OkUn+XcwsBE|;0BawcWQ?wtxuQE=W@B2&Cq zIPo$9<47Guw%mBAK&U@_P%ywNEs!rMmxuTfmj7Co3*jbrU3=&DW z7Rz|>qd;N#gcMvW`suk{iiW*H;Bja@v@L+e+uOFKfhnxo+xqhaeLM?av`4VbhUyQU zRcG$%FG<$njo7k9i4#r9=jm)^3u`gzazwO+SH+XZmVjlrm_Rkc^}F&>ck0GR&-Be?8ce`_kN$e_ceVc%_`fu^|7R=%GaCo< zf0>{Eqp`JL`SuRXg73FnBP*bex#lT@tszKsX`@O8+Wo_)IBF1$vmH*RKe#iQ(fpZm zA-3l}R-IAr3D!QV$fDu>zgtqEeU4--hYyZ5js{DT4A$@k&Qfp0E z7l-Q8=lcg0fZp;X2&LYW&>yi5ePa$ZMv!&R?W3^V_G%7aAHv*NX(&R7V++P2h2#=o zXieGEDD)<^v^^xI?b{;)A;1nEyKcQDl5CBV?NkcfyF3uG+k&WimD5J2AQQc-geIuIGt>76?YDN-ZgzBc=8#SB5pT z+@6W2jZhzWY^?!mCLJ5+UPx9rU#XxYll-7<|#u`c!V) zqH8R-Bf|{{V`aNG+l1GudanNFnt+-b1&39(U1OVQf6>A&gdMsugipMC_)+0s%3~s! z5hO@BO2NpbCPj1v!HFXT_=+tQqZJ_Tg_aS!oq+eJ`??iGA2R^?s&CXY^weT`upJS& za4?zgd4Sk33_bw#cz+Ixq$C8oM88acPkxI+i&zN!6IG?ddn~| z^^aW&1`F7hq&;*-==(Z&s8}W7*nET;N-2jO3kvsj;LhXRq#e37{B;&Y&_QAX3s5tU zGelC0`xc#(xWX@lO`Z3cA7L+Tl0MIX4hDQ}7+=@-4fnV4#k*k` zXbC|YklX3x%cTBZ$Gi5^(GCUyY-tDN;+%^9w<7O*aJ)WU3+sF@)SMgx54s657Lyg& z%vAjlKx?ahC>_q>iWwnhl6UNRte=4hDIMuCJEUK_0j&BdUNPOnzWi&$#;a;JO>MbG6ZdL*OA~qq6 zk6id*!6v_eDYVns*YJ#EsjIq*o!V^YH(ovRB?!yV0dJf0COUCH2s_b=nn(=ucNC5a z=bhHgR_zOvd>oV@19B#YtX|1vj_*tsmS%SdZL7GqNV49uGSi(Rn{_Bkn~=B@fQ1XL zAa5z+*%{l<(#BV~Y&X-_(iLB*YzD9-a#{Q{$*(QTowW84HJ6%ql3)Db{EKv0gekZW z?+kOD-PV0OCOy$t!xkx17YNobcQLgYa(1J!#7DZm(rIfxeia6t%gBy;;*}8-cG*)BX7)1wxKM6kipff1AKFFg-{|P8K^2 zZJS-DU6HYzPe9EWpmW{f5j&#shwLG{qPSyz4rMYY=Zom-)$tXG!%4VLDbmFCRja?C zt$40D0mbC%f7^jMm=v{VMw59+y&f%TM;V~9+iFiXcg#b0eQ#Ciyi84R)|AX!)m7x+ z;B7jT!InKahsAvyW&loUV?i`WN$<7W3bL>-Z&-HSsxi=S&_b5W;5&z zDqWP~-I@yv`--61_Nm9*o7Ig6T_na8GjCjuB&oLrys2TVGizxXxP5iV({EW0M)km_ ztc~+k5QSW@gM~HjMk}&?7|#EN@! z6zS$bHOhM+)=0*G#7R=f-}PE;GMYj$hydU5dj```++=mRkiFENVc(HiYSqG`@2zkB z>g%3qRwO3xnFofZ+{oCF9p}ST0J~#C(dzc>50aeQQuxjk2qW~t{8eCuQWs<4^Ri`E z1HTg+-fG_4#ij%4O`^&UJg~o%gUo#$O-?{%Yh&WR=H%`4#+<->q|CqWrZ?uap`hhf ze%?yE(DwU7q0`^k%+05@oLczvSt9+(kyL2>W7I}ao{g}RjXB9mt}Oj89WSxhW8l`? zISurtlR0Ne-=?JvI(<9Q>DRQ(Dd?6nezD!O48^bYc@LS#t!#kSqhQ)h>Y0hAx{u0A z^^s*K7VzEnpdUM`K&v0+Mdw7@_T-iRw!K|*VfsEpml7CvQ&P(~9zF1;b(5A46VtY! zYv~0U;*PWS`1Q{)vewQsk-bI}HuGt982YnA_!spQ z%==lvt2^g%xWbH~t1^UZBJW1d7dmRtgqcp0;jtY|BHTLImrw9<^yHedl^gKxc45Y` z>ad>#8aLB-#Nga`G2>w!z&a5XYp?usI@2!CJVYUoZc_d3jIopEq^M(BpXz110)l=^qaC#dd42ua)bo}{-Y}|!3oMooII4ua)lkrJK z9?SJxtvQ;=-1JoJ!$q|)UDDMVFz?>@VMn8v@m4!pU6uZE9p2)dy%5HQiT2Q?thArW z($#)E{_4ESle3CVHj?MP!{T{ZKxEq~BKsx=_8@VgVpQ`7{)SCG*F~0Ai1TmC{zRrB z&zkgao&`yf^YS$`l)7F)60XKaqbSd0fz-; zB;zf>=bOVoU26eDNhfWGLUt&zNu95S|d_hsSEtO&*IyBkEaW;yR=w|k?e zw-sLLapXC#!V|cjr(|=ekL^)1jMG*rw>#20 zogMSt;oH`+!skc(G}w_wTgKelvMm|K>7KICLH4`%L&N?WMTzjV@pOlvs zgfoVKp<_b<#t9wUrtwt>59b>o^8#Lj7|FiVL}HQw?4e1wGk94tWSJXpN01^MZ^tk_ zu8BNk!5SQ@X1ES5J=6bEnQ;icp>1iuA{|^HNm`+fX})VE>_ydV>@~a+{>J?;Vh=dR z6!JPAfNWEASHmkv2XNV976bU0L*6aZiQsEYdbF4ex1p+!FZBeB?n7>@hHs5>6oE?{ z>FdvI<9gv+F1#DLO`-1Y8o9m~%)(q1(WPX#pcYSf>w-`-gvU4}{ zEZ*g&)k8@q)=|(8W|X{qX2ledn>`Dj!yu6Y$VI$@n4oMxxAX#)jJXM%!qziy06#t9 zglM&*NYz2+E%272z)YXXBQP~tN?5*)&mg72VA5dV4??}W!Wol@^7_Lah;Ys)M6f?y zQ<6)yr%L-Cua0Zt*|?WlC&Fi`Cn3|B@a|3Fl(b6BARD>M%n&6DCpE$0wC>u8GFk0y zMRmz>bZg&fT)R(tK-^5;sF4Jx?$mG$`^ssEnc2M|`@)=>zkcykQ)`g^AH6CQ0|5hp zy^$3R4-dVVrHzZJ69K)LjiHOFh^eu?i75=djH#Wuiv@p2 zG^-l!I216KM>e_2G;{d%wn|`GAhQ4X6+B0XJZY>&$5-yAM$Drhe@~x^DIJnWC397b zY|OU2{PQ~<%d<;frSH@K_C^|Tb9(DlDneV2P2Fys1DbXnK;p}+KW@o0WR4P$=T@<0m%uqO5?KrO=aZOTaw zZWMx7pNBW2dc4x-Vtwq*ybtx7>&y$=%$TP%_mXduOLzR;jJRs&5bRo{de}yf@eZ!^ zLkT+;&f|!q$6(^(I%J5qH0$}dK2)?lJO9tWOzed6#`41drFbz_%~i==sB6Ca+?47+ zz3X5`6a+o9$&wuQEIeVjI!d;bD}8KXb2>~7W=V;14onWpNO$3l3YQcs#XzKX#=m)! z${BXIo7wHR_a~v=V^}+szQp5FCiW1!nslil7H?+v3-R*9o+T(=Jx8iGCVpROva89% z$EtK_x1yOE1<0T+OAqP?TV<=AiPG_%3|8i2wR3Uny0LcejIsk2wl7v$ukWJ^@Uy@j z;Z8$u3Bz?}Lx^9A_3-Awze^XzGzCBchX`;Ln<<7XfM%=Dpbw6h-;eHZvWQ+YO7b>8 zH;VJRy`rd2{WANN3b0=J&QVE2&fn)H>*)2Wu=m2L!XjS_y3^X6W*hv{Q5T0$iAy?C zGH1rNquW<<)Z}n{saaxegxgAfA%#7bHw-=lHoAb<`{`M)zUTQB~gbxi{I9~ay z6O;Vu)sOCT#?mdQ*Fde$7CVE0Fmf3ENiP-i61fdoH$fQMDSX7tif0ue^_R&M)`Sy~HRCKfdaFf>ePG8=q%m2>}t6QSo@OGl0&+q%> z>2tSja`Hsle7bm;XQvyWs@8?GTqGp_O|$itnN$Z=$4}C^O6-!i>srffU$NqGsXeu< z((6M>aMO{}Ge&oQp0@NmdvRC+WLLj^pBA zm6v^*{iQbg@f!vClQo8tv9x(sseB?F*OOor=NX^UU`k7(Vjs6JS@Y#BEYnQb1)hu;R+q&wWFZJywOO>(RlBZj@ z4iIH(gKq*jT@Jq{Jnnb?oD^e=D>?t2s6Ufx$>PJ`W%$Q8eruIuycRX8hy61jxTBUQ z_;P5M%Ti?W{Gc~mXM$a0``1c=i5Lys)hxb&Ze^0@v1^Q|fG zg!Z5rUIU-YX?W|?sEn*_2$$L4X2}J1kqi;sTtvD}Eqf$cCoLJ*3HL$V+;ZzFdJ6#z z7|sxxCEP+!S_W=u&w7eXOFD>Gli~+GNqO6zh|H^LKFVWB)$m3Fsvy%ou0Q5#+3`AV zs6K1QT4C4xFr9W-8T8U}Hp&ptoT&>n{p|3EM_TH{0HmoPt8|pE>m`t{2I_reYorFX zl2>9AstJ-SW0xLm>Frq!0pm-U(Ix2^)riEiXo+chrFA)f7N0je7dh7ooj7rFH~?e7 zquhiPCLD;lSFkcgS*BG9)VD{x0s$5l(hu5uY&y7`_gzIqPWS%6Z=1b*v z1>0>SD!|$P0XZ=D6#Ninf^v=8;Rw6CEdWNJ-!K+xJ>X6wbZr6yq`xO#x9)W9aQEDg zyq8!~0!f9-V%vnNse1lmkL@)>*q_;e=KzJ+O=4)=6*nwPI%o78hS5 zF`7<9mWb2~rzYc=$A1$_bMXt%eq2A@u6Ya6hI@KzDQg1k3eMN4^sISr46HAO0}%A4 z^KSQ$_&H?p)(R!UAZ|hBI|df>hY$8LP*^$FBCiArkY~Z)W7@~ykU3D3|rI} z%Gq)X(=g$FQG(Sm!zrCY(Sg|d{f7dS4_7?+iZn>zv}Z@% zr*tAFb_A!y_{7eL{n)d>(&GUHgx83{%qGT_EJJ-mf;x)TG7-zzO@m)R1-znrT%^jA zVb2n(Q&Hl-h>ef(Q$wuwFqI12F(F9!6y6Ou4@*l$K>8|W<8WWMcmQ`@>4jN|ZcGq4 z#0Y+A5vI`*mG&ZKd+xY%{bqgK*W)wAx=%K{U9w^83P(+tCJ~LuUT~$OwlXfU7%HggJ-WS z)4bD;S?D3rvB;h^Fb;5z5WCGT2N?=bk=X&w^lsO<_cx12)&02p5NWclI*`}2cfU!> z3uC;?PAEwi;{_bc$I(Aqfh@henbuArEJ$tJS?Vynhv1uK@DM#D#Gcd_t|U7ux@75$ zNtbHD`($TlA9O7SohBQ1WDSOLZ;3-Ws95_6?{4(qDYrgv*+s)>Nx0?9ZfxXrDiaWt zbqUkwE5aAdYF55R;wmWj_K(Fg)6yesgOdtqtN_Hg%&q@n>>Ywc3$`u5vTfV8ZQHhO z+qP}HZrQeN-&?lP^?K0pf5#jA$%!1~Xh)utd#|%_hZBLiF$JW|{a9YaamK*dYSr-aVHPcWt47;l08nYsqgR#097;vSU3c0)Mt%>67d8rJ1cJmx z%q?PQyI#7>ichl>iRm|Lk;sQCtT6lRi$*agwe1?5yP|;D^NxC$@ww%V4?1k~$+Fvm zvs!17QXru0^Y%x}b@|!s9TaK@L<=Rnv7(o$olIwXwtPHMn%xzQ0ZwE$Wyh!4)0n|G z6gStpt^;q5dQxZJect>DlH^FX_7dormqHoXL{%8vk@!`pj^q->=P<(VUy7?)_YzBy z_PTHwtdeM3!Pyuy(_;byAc@ZdLtw-hsUc8_(~$O$1pUX(yST(>58hEfAoOhO2&bGv zyTtqhRNwg$5C?{)wpPdFlJ7yH4)ggyPhiQ?;#hh0`5>>jwU$NSRMaVUVngzoUprKs zMwSvRym7>P0IZe&RSb)sbo)2kr@H?`^Qq*Lwko&TQnJ`axcEWXFqSh%F)8hAg#nz_ z<`)_+$tUs7D$s2xA^O~>3XGinSh*1f%ZHtzHb<@yR*QhGLtN+%&VE{e1M;8)+csd0 zNSVU@J=;|Zy!*R^2sQ}?U=>jh1F)?6u7Pwb)<~vb|Ks&%b9;hb3OodA)?VEfjw|N| z=#F-4kr-4>2bVI5#;VWFi+#eaPGo#=k#ejUP(7hPAGRV-2ny$@D;V6;OH-x$gQMVv zaP-_0ko?3)nzoUUT|BQCbeq9UHuLg31_*oz|INU0pdEq6k~c$Rw)m!|AsA0$_MtZe zW;8EvTM18hE=bP=T)cq|$%jmb5z!?z zYi!XIT!*Z4Z?{jZaz~YeC+9)Gux?qUOGwJPyQLNC6vGFXEODo5sV=~^j(I2^SF2(Zbs*>$;`ytx%QOveC~PY+P33vzj-}_%|{|hfr?XNeRnL9-#gKaAbQ6)N@)&& zdw6R+=p*qhw{td}iz~S}R}3{Y6+x{PbM7+mj&TGB+1%npc1bA6m&bFTqR4fwWohr;2LY7(6Z2f4hJ8ni<_Jg9LUEGa=!uuvChm6*x@mAhmXrTy1^MY-oW?m4O?ObDM7gYAzMbs zm>}5dNY~nD(y4_V)ImrWWEUq{0m+A(1n1@g_5Ho=6aJ!56ZMS6%QgOgm%T7t>LC+} zG$@@m6&xR3^Oxt1VyN^*M8mp+P$-RcA`Gy(+PdsJ4RIxUk_A2HP&$duy;r{5jk16R zq$aM-&C2D`P*wPA-SsXz)AxNkPGnAFu#-@OLx~y`OA^nG9VZDK0OZd$J55;j0A`Sj zL;|4=pksgy1qh;Zh~_2vj$*Ch*kvm2@@7&6D6}^J3n=YdX4n~sL)l5ZkcbKuLBmxx zVumx25C3hyVgtGTu+KO4Oo73D9hbmX+!avE%kr{-A;yh!pBm$&UFzqDR-2!jg(16m zx0++T{}3a^bF&n}j~H3M-!%9$j5Uj0gL!~mlHJZRoPcd_GwbnP@&p?MN(lK694Z9} z&;VX*jPXYvT6G< zgh_W^ak;RBQDw)h%-@Ft53NjG|L8#-C_DY+ZO5i}0VyqhNN6#GJk(RlKF4(m2?}80 z8S-LB_LpU=&~3XUzct-~*mSN9r$&=cJCj#kRfYn{1IW5Sxu12LYa0l25JhMV9a#0_ zl$|_GM{rID&M_a+;foFMj|B(=F@ZjFj{20$X>i+jK8VKv10&*~9xLZsBWq0z{M#W0XV)Vc z{#xnSZ?nUnufe?h7aLDDWu2SEWU5W1G5gr;0d^OucHVTjQnzmUTJGxW3B%FrrVVZ$ zQ?G73f*D6rC+lubQ+VX%wxxiN3nJlNn>}j4Q9;n#1t;Z#o}-pr*)accXiJ$DfOvoI zZW@sb%pd4rsT_#%7dsEXIRG7>8X=5SiYOgT%T#LfeQ)QKv_e;5Q9Gkd#N-gUbLj({?s{L>$hGkR}+gvhC?0yl0+ zGh0SHMlCHn#lD-@BX8UNrpeB+bYGJlAq5JeCZE6B^5F@$cpRY&#W6QvcpFa%^h~5| z&=s|x5=MxO%9%!Ky#*9BtH+Tu?pBWMaX@r_$#uV9Kcoj2VJ!f!y)+S2e>D#pZpeTN z4L7JejuO|N#S9>bBc^PQoj6MpAvgPMTwRNMC`j~2^q{Z=kAJW^epQOIj@XA99$r_w zMCZZ&ed&{ahJXHe-XSi6mGl(<;t$LjK?)Z`PB7un#6c^=K?I6{!PWuzeh_Nqx|bOw z9=VOrN9D<}KG9VpB8^b|n344@M1rUY>i&%JZ4*f!}d6%*%kE*=1{0wxKf!X2%d zq`MjyP|x&!x-qJ&aV=PG-@-#Rm8uA&@aW(N*7*T92Wd+O#)BQnxs-q#^)SZT&hJ%#E+lKKDI0FntN%G}iaXFL3tLmJ1>`CxJAWWT5 z_)x7F9#Eg%#eI6LTpOW^O_DH21XP&PV`9WJd;I{*UxqmMn{jK^TW@JP&&W8n@4V-P zfr^iOdg|z!E$Q&(x*}jKP=w$d+unkzdcBR|uD*RbE(C#iVq|uOBNnzU33+lVM)a?Z zV((f3t@nG#{3+{-ds%N1(pW~;hAFAsX8EW|TKZAqW2j-c8~Ym|21ES)ztw}~|5gtc zRwj=BS&*y$C9u39Q9x-h&gUs-NPu_9MhVp|`1b>hYe_nbJQ9OSlUTMPdR;scYs}}zkPX8R+lkfZf z_et*|4lBgP&&53#-IoaIhfthW98UVw62~F$|MULvc0YvQm+$wfnd|>EXPMP+>G`L4 z*-z`REt#LXcI@uc_g#|mXm(G|u3fwuqF+icid(fW9>q&S$dHHI7pDaH-6-V2&N1h+JkTp}P zvJ=84=!4aQ9FiGSz~OO?1KO5rq%nP@^9f&-SbovtbA6U%?- zU<{^s!TebabCf*{?q1^bG(T=cKW^311gbl{T1QUD2q-5QQ>46=r%=YW^`f~q% zydThSCzp6_zbqH{io9dce$CR!$zlKc5d+_hd6mf_VqlgZx$w}Ip^Ct3v%^6qI0`68 zCF7c;+hVYwI58>UVxt5tZA=3dc&rFEd;NmGjFgLE6<8tr;HRZ_>==Ad1rZB)8l*`u zsCEXR3i26ho8CN<@nHtA`6!duEfyO99t4x9F1($6L6Zq11~f4C0SUllEdYG-2?CA& zf*iNzB8d9nFeURm157b3Kkfq2K-B;)`^uLDlaC)!* zc2K1;oPqy_dD;+;bFo!QmEgt6@x-KzLP97zkPZq$Asey_$&&ytbW`aj`}}BGl7d^i za17O{T=P&=@0c(y3AW_n_vQHcWU_!(^f;8O%dfWru>^vj@Z|Me7eWrLDoPp=}<$!tiezFs`f5Uj}2#mk0 zkH=8Wj+Jf4qUPil_{lbdULryL#YuHm&g?14eC|si6UTRefk_w)jKo-mC^N|OT4*ZX6{1M0eH0cv;LdS+WC8c45$zJJZI2|rGL zA2#ftgLCHP5@?krQb(@HfFa0t%tfE?n+B!JpnN$o$2!A!dMhoyGrW`8Ia)M^)^0!H zWDvQZ?N3SqAYAQ@&5fzMQsYvBJF}BDj%&1TJ55V4a@Xyu+m4YT7je~_PI`oTqy?9+ z#e&Xuj2C#K#vE-GsR`8i)x`2WgX!Qo88KD7F95282K+gfyOt81ymR|Bp085$HO|3! z@z$kEr7+P0RkZ4={iKw_T`-vVZ~`ILPlEXF_qxtt3B!`y?6wXTdzbX5-GNbgm^lWlRM$Y>Z~23 z4Q7cuMPlsw>F^^gIz1ng&Xd&}MLMJQUScV?WTg1MY^)8-ZP`SMrgg}vv3~b1>IG|| z*I{XIdr;&35V+Pq&BMAAmD$?f*5HyoIz!OMBEwM7Gg`H($Dh|ePz+M-Bq@g=51Ma z^%ombkK-_plvHg+%dm|zGLipi$dtS!JK>NKRKQZdw z6GdftU}F5S?R=6H#YTCCl*HSl2PX_M?XE6U2jN;h#p2AUL8=3_B@fzlsi?oSV{LDz z%CVS{rrC1Xd1cIuxrr29DPpS6+7i=j2&XOapvclOe|K58`V&OIl)bqy@K@wO0A|)a z@M2gT_?(VnHmmnAo3*)ZPSLFBCvxJ>q0CJ`m^|9|?t5y50}KH`?oH{>(b zxHzyqK*U^b_H7WRJhk_q%AZ{b)EI&0XE)k4#yIZmB@<_q(FY;77_$W5JLsVJavvGT&sk(wrivT8K z2zfActZsScnj|#JsqBjA9y7fXY3ucJXT@%!9qeltRM;Ztsu$6pFun)GeFg~5dMF5Z zX}O(3BAY?7OL0`jFQ_iYPclA6S2RW9m@NBgbEVF&;}!D;LBDRN<^t#jHS`G=3m;hs zJRotCNc92|rGHF$3v!n8?NP4y+~JzL#mpfmMiNG;-FfD8nfl}jOxFSrzVaZ35p%XL z?@)`w@ASN0=V1wa6Q=iTZ6XPn1YZdug476IsN_v$it89=Ri$y_bH=iR*k7r5dv-_+f21A z5+n5(bGdfofF0xU4?Jk#xwmnLWuOqmpBE44hZcjBBEv70hgP{MqUPY##6Ia+s?Xbw zlM_`ccw}Kp2cIr(Dy0UtEvc2>J2eJlrHbs&Hq}APg?GAqv)rx3JH%}+hkRHVskS=gLa!iBx+h+AJqK)^@hRR8wENgFFXtFzi~g?Z9GcQ_05^ZS z1c0Zd{BvtgX2O?%>0$byaY(GbRV=R(2(w$O%VlDfCYE=JRhpi63C6v9g~N;st#wh& z*2|a`XBqa9#E$3YxQCL%+N6jU;$YMoi$^~1AlUYT2yycVYHZ7po@p=tqv8H-yXwid za91N87>kahWRxS>T`V;m&_N@$(z^(FaMwvWO=ub|9MCx6v#-@~PrUujoyI0E&#H&X zI*LR|b@F87CbNh{$g6@`a>^Y+NRU6IkVmZ6D#C7iOqIaeUgvyM$$i2&D|eKnj!TNQ z8;Y}JD+yJt0It^^4F{$FJacP6AkeXF{IQS2O$ST{pq=`J*r z6(kQ4q*AA(u2uTnvKWPx~pXTsj(p~meTita)b3fGU6e-Qqol~>M#-^n>f|y zdE#zV9cPi2qE7mJWyt9Do%Ku>eo6wx%as%P^uLW0x%nRJbZ%iZ>T4$({J^qv+Mw+^ zRoI?iXPMhL59Ydd6YD#(?U|ykwqqd42ZWR?);7k59_dIzwK>F^H}r{^$X)*!;8)UG zCBiO}S|z>1e-ELTyZ_dYTYFzcj$n9)loCPf z`T2C+Q6}Juf;P4TS@yeaN=yv1iaGX#5#W8)y&Hp6&ZA5G)f|LgZ;{ta;p0eICHo2+ zm;L^NwHCnKyRx{F><3;u+LPu1=b=gL6{%Q|F6{@NPWbP}=YJvBPa4alo+%WgT2MHX+Ys-;OT4d%Vx5G=R$zUYazhjwbOUb)4Gw?BpGo@J4T zV;qI3H_LH2HIr9(+NbmwoH*(8v^%}H5tLXsQQ2!?!^ffzpNccZ?j1w}nu}|h$7c;i z>1&{!zl-=)G9`2i2&Q15RirBbaj{jvU$ z;y))lK#a1YyQl8ZHLb~&RJ4wepUwLFbYDOr_d6e;6zLly5W^}T!mDD)TWEkZuM$XF zHMGPT6KwBBZ{_FK0dcVaIB6~8%Y~6$K~`N3C|E=7FlUCar>`*j8>iyx58qbDYQg zE)5^epo(8a`=5(}9;R!mMNO&UIQf&sy1-C{<|E;|N_Hs@O1y5oY<*B61zLKbNsGUq zfYU~3Y8>}(YaicvBI-rIhu?1GYXlJ}4uzVafM5cwu4X(+mHv(Wt9vFbL)@p9U?@# zQX&hdWmRd~krYnwi4}`lV$s(3hTi#`yLapo=|KX*O)BNkSI;JOqAXhU4uul zGKVEx$G0#U$$D9SwqWnJP?t4L)*f_vbrZSfNW%qQ*00dw-R4Qe>0iL|!!?-PL#9wO zxq<#A!XTMlBlKrY>kdNLoyK)*Bz9B-WZ_sj{FC2KpHMu3+MnrL5>xYBV!;_LRQ!A6 zSqT~%oEtHe98Ucc1j;FI)9M{*3NaV2fpZ{OE~9gN-8DXJYuai{MmPIyk=d^BEeL~j z*de4+cMl-_q=E;_>?kppC8IMKizHW-eT5qCr;rdStU}G?2$?HoVUvf8O36OW$VCO= z>}7Tc(dj-j%xKbRPX-5Qksdk38J`v@JdS04_-ixF>?p!D`TH2|GCRbQQs9|JT8B@I zydV&?CaWujIq=Mw*2;^#pceLUB4c_?%j6Ks&dcOzisd#xL~358M1CpzjM}sBo2{4f zs=c~I{Rn2Md?9^(*GyKqaFJlE zoFC?=;XG?M&tCUUl9hP-o*)}Mi84VBe9V|218E@JNMQ)dwjrWdBgv);AoJs<(@nJL z1oQYXL9TPkH$g^x?;^{Fz&V*9^Xd5AYw?0Kg^*{fFYYl5wC=(9O+ozfJKByR`>YJ?Mp0^5kfa2kh`}y3_IW)z+mipm@C)`n9uIY$ z&f=i!B?j(3oQ8t7&xC)tCH(wYgCBZ4jMulX_fHtA_um}^P>UR3DAdU~OwYzHQHp`p zAEc&i#pT<(V;_uwQJeFF=W0wR1jHZD8x?%&V!X}vFKI62|LY^Z6T!5p3S^JMLma{ zIuIiO4A-HW>?3&hZ=^gs_2FXf;EAHA)pD4y8(fr{DJ9kDc+#sUgGGULEu~Aq$-qy# zq=6|T4!SLYT>%wY3uy)<>p%~qs9&JjEzeZVwZMxomqaX*T5Lzcxp%tGITJlKAko`0`G`q(=1=9n=uJ#iwE-;6DC&&)C+9JzxL zr71{a*hDmGorsEDF=BMUVn!^d_u_beU|awBnCd+^xl}XbAWu*C6qX zmZUj&*0z{Oi4~nih`1I52_lY#74|`5XUwgHmZ(QbEin(0IwIe~DMYwCm#BwNN6f7c ztz#|^nIjVur{mZ(nd24>tfK=FyO@v~LS%Z41d^u62?^K&2S)WqgtLr1;y3YU^#q-b z7itr<1RWcDz_e3#97MXkf)C3qI49{#aT}J#o6tmkh~_i z6*(M&M8))cdyXIwWou(V7zZj$pXH!6LS?0Z6OVbv6qiFisQLJyYVZ^#D1NCc(-P9q z>6Qf9g69ORUr~N6FhcRZ%y3W&xsly*o(EWz=UP`#@Q(`vBHjqY7kGz*U+llzuK2b! z{J}whY8+^PtJha(lQ~9za1=*I$z&-iexFY#Z(#Lte>nUfXhA45SGZLpHlNRz+e`fJ z9}nzUFhYaCBsYvwz;ZaM4Fo}UZYf8?Azdobi^0c9RKEE?kAin`i#We2kIkG z?wSr)&4AK+NQBLX8qE+5qxCnnqQ`hoe7dt7%njZ2!fYmCS+X>8qp)k0E<4!ONU|ao znu~**2^LbeH!FNg>oUw_@nhzjU2SijnSJbe=2Vr1H{synm1^0r#M!_I|PJ zgPGWbk?Gg!aGZTckqM@vlNm6`MMbb5%y{)@3_T2MuRqDDAsp&9QF{K;_iw1jgZYg0 z({h1cxo2LE6Lnpd5?O9Us}#izXrdRu-GUcEB$LKZN&h7BL%T7ky{u}RNk(VvY?)0X zr>E=xtY5p|Nc0%5y5NcAW)p&>_r<#K=0r!;sTqT)b$SiHwC#Kz>hh`G?ow;=q220w zwxzTA<5W}smA4k39o6*gfE*T&mQu1gq{qR&31XYQZ+kEt)u+ABFSvE_FUYBhNgd6~ z<~oKGFO}6Y83!;%NZsc`kGAgLn{t=gq+^_UIY{SP5nIKCgPUdAdbPonCK1zTleG>W z+xv}HLHQ{)vG@t0WluF&R@&3@iw*x@0C5c zH{N4w6$;O9$%``*5*(j(&vvGxt;VX{{ybpq1EpHc62y}T3b6Z!C-%lKR-`AkHY!q$ zsVdius7q0Py}F~Ji&Xk??ayU4V-wO0YISLD=hev4&R|8ZA}u1xopyWg)}qQU!h{tm z;wEtkV|XJ%M8(^l}?!EKfHJXr(h7_tKhXl+se~_e*W|IKTsXP%zi%Of^&L zq%}i>@Y+&LPa8nEJ*yJvTs>XrA*{@#Lq=47&WX7L^!Q^cc3aR8vj5KqIm*sKSgOibcE&GPkq@|AVeW5ml znCB;+JhZ3GiP%5Rr+V_(hdiXD_a7F${lT}=p`;s07Z3ZRV27OZRI8eFRy-4@Rw(;- z8E+mX7cL^}qu=gj{8%qeYXJw<2ODN3NooR(T@9GSmTb@QTMMA=vUA8i{>{MNvtD7g z`zUhqUm72>$LNB5`5eR+$6v(tUZH)E3*J)Ec77r|ItcdOJyhaGkzvnS;BLm^ikL0& zl-v@o?*4Ry8*VRT!584&ly!%s{!-)0_QG_(DuFw~tdaQaswm{t zA|Ge5^0_)mki~h9uXr_t1Yt*f1q0;Ajh7LkV08gGu<+?M&;9`}S7O`nV}iA64i0s5 z&C&Ce7_FVKh}b3aW||QSlz&5-B;)I?^jE2=hcQrBqeSAV=d`OU3>pdAoTF#(H`w_$0Lv57Y>j)hQ6w(f82#BGN}9=y^t^ha-p;pqeL^S6%HT;-PC4@~ zc^2TSuJ270tNTbMqj4aud0YRk^NtZm&8;|3NqIWV!NxjU&cTlO+RWMRp|5w$3t(+sYyx>1NMOBU)=4sJyII58@hxOGv>+?Dw))QxNA$F!1O=I_ zKM^>A0c2kNpqxN#ELX(&6XX>gH@Et(A5KB+TUU@S_We@}_Wofx%PHU(`2y(WwO826 z4z}i50cTuAbyRY`ZQ#H8UN?8U7ttI4;s`feUq>#*`((dDup~mWF^{|Zh#a!%vpW! zvoF^U_RXomJLBE^DIWOnyxeSoWM1yLI{zTiwAF9!h(NZD-a~8INM&6m&CkfDC}oo# zwPaJ!{@D;}&88TF-yC}7zL2%8E3Dz|-K8jRyO4EVDD3`(uDN*@=8dsm%U(K3JGE(^ z&T>#=Os5O>Vi-uDFS)H6E2SQL0KBsfzZ>8wi`_HN6U~C1g zRns0G#5NTR^$Dh5_QSB9s+UdS;5aT;!0R%=?ArITK!Do5wH6{Yxp$s z={=I2LE0-m)Ni2 zcAr}+S(Hfr0|39UNaMtL><|RRVbA%nqPqNTUy^R&VJ#r zkYC{_G@qpWjX|_D;9blSGt0TMRMNVn<+wjVjdCt=%0aXoFKjrc(C@G9^RS~CzDReN z>DS(hUk|&1Xdx2TOpu|tc$z2)tO_mL22B!v(k2QGE#M;DiR=!nYF`#efMg-=W&!z~ zn%QQN3gJ}f2LKhN#E0UECYuYqPQv)A)Ze6qZ$zOc_g-$sU|6^_aBVRKBf7=;fK{E> zlstiE0(lZ~o@PQK8bo7&o;citm=a=96|3{J`xnOl zg!!{JjyTTM4T*)1Hq8zMAstl zQOb{a*p?Uxbc{DlOwb3?B=)wLQ1KX9c$qhwFp+qe2E(H?R)QRGVSpt9MKX0TF8~rR zD*NjKEaC+AP)aC_5r?S1RWQtxDF_yNXYrazAB>(utRimd4H@(I(ElOu;7BgyAYrsx%+3hR8fdLNCD|p)&;D3<9gLpY>u z`(w(Xq7tR=TdyG&p5eRj*XaQ?=j+;r+$DvOVl^V5F@+LN2LqW7K#oz^rgoV$tVzPY zc0V-l+m6fgt`6QwMdfb1fZ}J+G{)5PF)k=)A^fVGBLjHxx2l&$u-7hQtbIgGH|;9n0D#(i5r7gRQzpkh9Bfu+PgjbDOW3;+#!gAA^+FRr58l}Gf{RI}ED?nN6bQ-2dGaqrXbKK0$heLE#yRm&# zML_g2(s#~Q5YV8vU7(9cT!k|qY-^~%%$0JTkM^3X=_PQUAv1sG!cUjryEU5~H zj>v|iYfF1MD__8Ti$?ybFsJl+@W>?&NU&~sNo~@j}0QQr%}@o=vbtq z{qumFN!vh1t%2m^iRGZPL8b5!mZ<;%z6m38Qhfy$Q=*%9mtZ^1(@C+hbf^+xW1sY| z${q9LYQ~vEMO>>n-kHqXZk=M0?6@AsvYfVHT-@`bRK;Yl&fRfbI2&f_a6JcqVcDR^ zK0u;>vWbTVlme`YkcS%N@e_wUzSmH|o$q=*7uk%L*JCQInTPHew_R?yIQZ3G)&Fz)>B&}Rr1VD2K?%b&kkVGbLb zm?7+Cw|pk(UlE=OFFakWb@`ib#aLiDEHwM3CuXjJvgNiU-sfTI#%2o-46e7B0)r3! z8S~3_gDhHZB0W95pJmfFzZ`*9ogY%-!)3JBM`GYtM2AP3UQK$m`s*dD6HbyLGTd37 z2Nq;om6?;AEKBoajx8UvYusC@sle_49S`K?{ydu=qnSqx1psD})i%S(T! z+;#7Z^1y3%Md#otXCZa2)~&kmJ$&e4Q5{Z)`3@{SY|nfwEJ|WIvsl`EFWge15MzQ;^mCru}G<6^O7hp8FNanMH`~@Fh zg%jp7*m(hWOA);%$z(O1irXQZ2=+ml=-T@tj*kJYV)%Sa^4o)WUI4w!?{)cc15jIk zE%ttnfGYhZqv}asgx({)*ZaHS`=9?5@<{=3(dXih(&n!N*aM`1k*2sh;{83kxxs(m z8Csj$*@Cb7`P4@Tcfj(dR{Y=?DW82=o~Zbqx-BWW%;zR|%fT)(fEWbX^A>al0^wam z2(XCITQC5gfv$J`S%wADpd6A|nO(>x<2ZSg016hlpVdZR7I1!?P&gB?C7drHUYQRO zw7}BTVqQe)!3<#^j&Uc3RaCT37*?ML6ss!1j1K(SWqT3`#iD%_J4X ztprX`fAiRj@n=%bQt(o-KmID7%;EZ{Q;_uid&@{X&B>a+jv+%Z$l<-itORrGCif(w zwMvq{jzI*l3!ybcskGW>7RHoBTmH2na$dbgNl!tJ1oWAEVxJev!3>jcgf1C7vok{a zr)$S;DPw19>SPxmDQnjziDh>nYIh0+NpQ^wDRHe0>U15d$vLf^jFha-@9Liz|0C8% z+c*|0&-kqHSJQkP2csYdWJs%R*>IMDJI*Qf$3k^#{ndfp2kQfx;yTG=QxBuE1tLCm1jYenupx z9IjPG2n;GZ<`ZY4L`$B)>7bQl4mU_5FUCT!ef&3(oi%`Afh7Y)WccrF6m(8RSTQq9 z!Z_yCSzN3^3!+$NU@%Ymf*c2y5c8jyGK&46;M|5{fUx-jKR*NlNh=sAQ7F@l7!r&_ zYr%$uVj}2mOOUz@CRBA|^Jwaxhxxgl=$QdcN<4F))40 z`Lfkp1#1s|>jvSWFJtnCy)RL~CFTiN0fGziL5b8cYq9Hom|3y9@{CxUdO?9Tg*sSM zxlpoEKmp|;@Wc?LeF}95Qc7}|;<-rJC4$nFH=Zgg)H=~RtFY19)<5URU~(f|3Wt@! zz76gfoyWe$;w2Fs471Oc&I|=0eE+&c{{9W4)lD>N9ibd@)g5Y^IrY$O4sgI|d;0uW zrKyg4aP33|pv53BD(+cz10(6oU%*mz87ipd4ox1ONS?1P1`kaQ?n*Tkrq<=G9HLea zDt!^3)jq>#eWK~>R{f~p!^T`=g=SX;9eu7;vRY7etuHG8{%Myi;-g{tRHLRVsG|5` z>Pk{6>lY_+=xB&pK8BPQex_kR`AKJdMtQzbX4SEhdN>q`VL&4wgJ=OK+aio2cm>a3 z9h3~sj+VnT#P8scMSZ2y6~Y6*TMrl1S~{yLs70h5_-N-Xp;_}`ID;OmyQ#|Cv5KVv zoBN*DxS8;R4z8iTI2^PT^Ax_io2d7)Rg07>q=B^oZSy;m8wIMmF0Eu;r-(A*Kl)nh zlRH;U_hh?YFP~r=wsFoW6>zBM^@t3m9n4>t5MZXjqWAAvS50a!7)iRaI)9PIs1J-%@v3AFQ8#C{}gR&@3S&(_iru9)Wgx_`9vQAUP7z3?`)Z z#{h626pCvgLJXJPzVlGGSro!{$JH|BpG>-G#A)S1bjRUZy2?@qR65 zTg29m02Bt$Oi(#%2DbZE#@%m%>VW%BLHl^*k)$||ajKqrD`U-7z)-<})mc;AewwOI zv&uTBs*xr$%1K?=veMHBj_MJau+!FvZXJ=z=OqsO#X(QRqt#(??_OwE z`Xz^f=)3H?hDlzwvSclp)R*$GRwY!rrB?a?r&@PIerub#y>HUwnQA1^6lfLF46wKg zDV7GW>Yo^;DkRbIXcbZ{pCV4~#vyAq8*8^sT)p(TvCo@zp)Ayefyu32ihCy5HHX?m zCOR#jGL6Z9c^MaOu&3_2)Nd+k)G_!EbC}SLYrkGyH%*f-iw=23I)dNal#{4|ag>v^ zFpjZ^7Y{oeolQkLh))qk+d$>47+Vjzk)0bwI)Epw6l-|oQKYzvDbzjn)Tw7hI)=9y zML5XyS*TUs(^asmm&Ll>jk}{RYdTfbPi&`jSES4xFalR2ojUf$T9$cSe$Jb_^KE1{ z?+-hN8jhHM8XSjB_d{W*kK^~I99;ByzF{UTI}qBRE=K;mS_yx{$`F{wH^T_gcySJP zcCkp`ShDuMT&qj4fJUCW6iMb<9F?xDuaRawz46MlEc`w4;Ok=tJoRU9?mPQ${m1`N zAjHhT`u}&Bvfp7t@>{E)cMhcJ*8fc?Z+E@l4h{SSu+JVDq?~{JRm9*)=5QuO2uqdN zdfJ{Hc(_Q+eqqOnm?A9d`+0%&;e*|mKNb0Z?tZN}+JPbasmSv|QLgWo{Az|qVHcq` zeqMV%-j#pIV-kVUR~fD89uDKGv)AA~&v_8tew1_MT9cY68;5+w1(Efloy6vaDGF*zRG!V-D)6;(=$!fu~mil9w zk!2yEi#`9tiXf*7MhyhRajM>y0BM!Q$K$3xo0C6yAn0jbit&BLeykZ+QHu(szIa+G z3ask*555C@@r&=I4mjzwQ+oodG8U39$P{2+hY~`;uvnj|nyG?Up)X1rr9i{_p)ls9 zS~1yS6q6hWoY(0XebYVVddi)fwsSm< zWy+qnHM1-aDcT`HH3wM?Pe+wBhEkO)V#Ep;xw0MNtiUKwix5645k8OJ6iIb#qZ#qbp0u(_U&{Xy_D=J^DiAvbnF9uCAY;UGWk#fL3$jEU8%U_I z&kD*0|38eqQ;;oDmxWulZQHhO+qQAawr$(?DciPB*|u-}-4QpU@9l?v$$Z$cSFSyC zX6#&JuJO%DMPa0w@?~?xa=-lQO+7T6uM56roM^S4u+!vL{9;8mf|JZ}3eT50pHCbn zuws5RUzML*aQ|!2CqReMExH<$M!(mcuc`FZpL6_g@cBtXVh)LgYQI-`e7g1_&=O^I zzB0Pc#J>h~=-2pv1s@N33I2Uz9{weacT^u>z&x2;`E|QgL+a>X*cf$+08^D5EvdYh zTM1$P6DoTI1`PyL)$orLWCGZqyp%Q?VFE%h?TD3;B9&rIa}l~(s?`#!oDv1bJeBsT-|DYNcA%~*!C|?&keprahz^!LrfT*Z z$F&vVD%!z<_L@@7ofO|O6f?JJp%;kP;6xMC@BrYI@K8``mi>!J=;c6`p9ueY(qEphKTN89c(y7Zv2Q~W2 z4+&)&YMdNW_cNEFGm%L+1-hr0Ospfy?|}?{ywI2*pWZzw5=tQ2`(w3@+cdl1J+Ns= zD9Qc1juer0%Qpr8D@@f{Y~5l4#(gf(j3X$ToJLHc<15&|lF(RG$Z)xYI=udiYX1|J zDQ_0R(ua0)vOsz6;7n~7m{#k8GtsW`V#ERCt8D5|b7|RcTTad`Rm1O^ zyn}{X^+r)SzMbuf=zOarPs;UteaU54-xevw>2^VK9Z|@$?t}c7T*&tDolKo?x0i|y zKFcQVmQF1B$bpozWTnKZQf-6;a@2LfYl$S-x3V?Fii&n!s5{{lb!ODp*8|{S1Y&u< zUQn`?#LEpl9rNdpQ3Y){U>^(+U#pJC-+kZ?0+>pn`tpV*P9% zyXDDedj*M5TKK=*S*v-DC;UW`{HT4j8xDAWBq_$*R(di_n}C^zY1zXI4z>f?>6B(v z0YpT0xtV1{8Ij^)&880u;=&%fNu@sF1UP;L6!v$_1vZd*%wvVdt;m=G%;R8Z8?kY) zt}W>@SohX6HJ=&D4Ajf|4;6+je3ymSHAGFCj|5WhO9=5BcPLNl2h&w*wVc0t9$q$CcTqqS{dnyExLL0xXc{&#YtJ$42@8HwNL(|TJ1A7VMcEdQWP{uN_7vtYc{)|cC$X+# zN2tVRXf%eFnJhbrRh;iT-?hyhxpN(o3h!$mjbPhUL>eCS(hM?wx}W-Qq6Di2#4#t5 zwqN!byBQ?HR$ti*LOXEGU^11U)`_Gzwhbg=eX|8bzhe62yt!gnvirS|4PQG@`*_A4 z@)$y)jU`>h)+uijhVy$-EF3~FwzK#q1>X@u2^T_h4`bsI>e>1G?q zL>cwp@1*<1rv8INC!y`9#h-C^tmRdq$?RZg_u0cKD z8JV;^RxZR8lN*$|w~#$&y^Qf%sNMO4ppnk^4_CR;F$`^{6?FLvDnYV2PfN_XXLADO=6>d0 zMYk(m_1tdc6<)pjtwX43Guk19(|w?`r*=G9t3IUdg*|ss8zGT`z`A?e>yYtmIW)mN z!@hr-dO~UJO0Z%tKv@oysx=)E5`JI!=_3g;omQZ-Y%jJICmlX0Kld-oi^-|)TGxBN zHijY5nrEv8k$!sTeot9Mdt`E^;C$;Q(W4Vx&*K3SIj-`h6g4s4?!$EQZ-xyN5%F7F zr14#yi=|K*CaAc%TLME-`98YUTy*1-ucR2`Id2h8Pn@7tP5JC)mgpgCB(-(2#zM|V zykY+~(eJIm?MTUma>2>uBwuy>%B7|{I{aB)Xbu@?a2h0Hy_SoQ`Ao&VmQxc-cJHuw zvDvp}+xKIMwK*(Xpa|63ZTf-r{W<4g4}#>cB*zayQJ9jT^cs%_9z|>Xx$}B?D*Kkl zASUe49j1*q?q%b!tl|;1kEcHQBq5T11VyEi*-MPY!l zAnykpXO{`f$_YLJ2r20Il*{aWVZbA@O1 z2a}M+VD#nw@CetIA@Nz=fUyhZ&BbI+f~+c#1t-m|sen-k6y?by1BEj3r%_q&Nnh18 zThZ-n2_!VlXwVC1Jl{ea@O!wQ@Un2FS@FLra+ajeJ0}Fg?>WQPb8x3Y5*9F-1Ei|w z&X&M%1v3G}Yah*o2(-TKJ&aEry-B;{m9+h_bA;#(n2BUDEy3t$)a$)qNw%t`bSW4E ze4!;WG&R6Mzs7AXQ2AXs6i8DrZ$S%>p-)uja7P5gmFP*3LrRx2y($O0s#RvR6vSp? zejkbSz>@lZ>-=mTtC;P+z?ni>)c5fXa;8`vg9Xm292e)O3uoYQwlOc8A<34U^q@=0 ziH3Se-B(P_NYHd;K=HZ(8}4>YWf>aLe=-TOq>j>)Qz71I6LB@j5}6b`6U8FR5Vas; z6YYZ7s{~6B6w)Wei!?!uN5By|E2tAAqvw2kzr$O+&feAy-LJh-YTz;ONdCI!OG~BN zH3aYGIq+{$;Gz>)Mglbnhjr2uC<(gYY|r+b zUb;PFzimn?1INE03&Lv#)br;38v9*7VDNpRtE2{DI`}hS^L=?i(CZwBEOemTcc9}t z(@#C_KP_6K48HjZICy|{u^}*B<1)#NJV#Qt%?Ggpp1-h6?iYDnhipes{FW*Gg?i|0 z?N(fi($YBU7d8xp13Ao2X+T(boS=Qpl3`S=2QC`x0R#cBPaE+Qk|+`m{I{qTb>2k8 z5w>e445ZRKNLD8IgvMdh>ozBg!O^|O8=Wd1dt?bf+rvaiHD{tJQ-t}=M~=}!{U!3w zQ9m@aus@ab>RPF+ldPiXNV^#jx>==O_XSCkw2SS186%1j^Wlly=N=wNveH5}g`g1o zH3<<7=$N+6^-Ta*@U2Ry>&n>FiBG~GnCzW=4iQwq|9mTLs^qHKwT0YU7itk7BfLMO zJQVQ&MS6g6;JdyN7t#r3qzP3T(TL{Tw9Kj#QbXn-nDjd-LT*ymVStbZx>0B`0Ql`n z@=1-s0XNQnZ|YIe%**Oytywm^35Mb*d9o{E(4ej*=PE)+pFNe-{OjU4-a9A(GY2KK zzI7fbZMmo1#rJl*oho9tx;gqkcqj%TucX-OpHBu z+M(A)W2DOyA-?}AeXOd1GJIvMi30LkiU!(Gk$V@*h$>jH^yJ>a(CKgZ^x*gAZxfWf z%INDT3zo2J|2cO|LUPrp7Sw%cE1L?7ADiWS8g<2T;HivD8rmQzc>n_5U$C5^Qggt9 z5n_r9UX|(vPo0_|B@cf)-OJ%r0R;wjP(s5}r%FrIXY1hyG&JsCSXI){bPqK3+z6}C z5I;S!sVZaj17cL}x+B97s`ud$2 zFvSBI5`GD9>@Yz|C|`y_^T#=g1B|@>y%4+XuU(}J$!dwnUhb}>V76Ju+udKojVx1&n zM1E_}7r`Ga9$ci+v^BfoKJ?K)T5YQ>S(+hk1#}qbV*VE`B|8V>99ATZ==m*8LFu!G0XYHRu>HN`kwS4pC`LTQ@tAnv)X)r?G@^q5Tunil%Q8Va;8_pTdGYHoI&@_o?PgV=O z)*K3w0d6ysqE>T0fBiMZo%_T{IMPcspYgz3HEm{)k+@YCZ3p{WNk*d|%xxW2#xG>f z(PIg~!;3&eM}*~H#~OviQ8_92K|(kyY^wH5qk+QSAocDj!drbMWVm^5(YlwWhHA1zx~iJ2u+{pFNsw=m++=pdE4B5XH0E8@FujJe&8MB2V`QmF9zl_F zgo_E`0&NhKBR6a7x|)7Q;E=(?Yy@(iWalmxDpA^e^?BRI;~1yt>4CX$2CYTbLi%>% z$aj?uMYP$Yx7gNcaYOOasEbh#P%@O&ro$71nWM-8scH3Lgf{E{r>Em4Cz7|i5C zYnF+iAM5;KY{Ltn8_xQ%RAPT2Jfy#0u?8b)V;A6U9jTCj5SI53m>mG=3zT7FWP2~S z!gAsT3r$cdxrr;C!Tf&iq8uTS_o(rP6GFy=k68Ejb1frQQ6vYP-3GhNxHPs>{oj`Z zKMRjo+mMl7Zepbk`8t23E3foUz{Y4wQ=6xeUazdVql81>Se94r+8)2zXrFthaj~v>0s3|tB=)z}%8rAh*d%b@2 zu&~LfCayt?%kIL%zJdw`i{up!;&z{>WC`E8N_W5r6QPhA9;8I#B`C*rHZ0}jx~$|O z_K!|Ax;nNf9l0Uaj8ujQ-|bzx7WQ${;T*ENU-5CnVJ&k;!KluaB$W;n4T4FCl0@pJ zDaV} zDU2MuDiP&-De}U-XEJpoA7v;Gfx1P`Bg*@lVO zfdywOKK$ekoY?^VLzr2opK7k^0%vNDK&1UtBz@I^K^XWV4V9vL!^w6la|s1o1EVh#7~Jhze!bkz~UUHCk=iXRvC><~g<5<2`a zDu+MW{&(K}7k}YT5vUm5`y3@l##PmoY2(0(5XAlB zIVc1wJ8D7gExFq==GNJm-W@og%Z8+*|9tTC>X}sQq1^zr^$@J8?MS=LXHTsQgs|IX z?|!p&67}-<22b7Hq!>xuCJO&Y>rUuqm&UYJlf}qpX!AkYjBP;Z?nJj_(!$WZhS8?gW#L8Jp{I#P z7~0hKZKyL(4_1G9??O=i9D;xiGfm!2GH%Kt7zZ!Vy-@4ZK)^-3`CS1> z%67*X$7lwUMXzJ-F&AZfC#j$R(vNmrDO!&z1PD>O=-Mq#Lv5-N^sM0ngCmGaF95eq+s za1#OVQCzN$h<{vw` z)adAJmBucVc_psyozu|#{t^KXhpO^EAYACP%=)gF)FJLHnb##utg5ELNuq(I`R*a& z3J{!C)(uFYR?hlDEX*2y5K2W0{!}q&4ducC(;f9#Va)ZtaS%%kH$PKFt2R)!?j1KU z$4{oUKvX_?FjD4NSzNx8vZkI4BQTsl7%AaU-JVqIU#q5h7=yI;Yo2`S`%)yf_4(v-}1(oM+~=|M!sw#bVX#o80~=$~+lvB^3Uhhfd|Q+fS246u^-B`}LI>ER5M z8tknv1N&~Cl!Gpm#RFB^*0a4P1sNHi{KU7YmH(TR@PBk>vv9Egf2D-jt=4Ow9;007 zrR6Um5b!awPM>Q*e_sK)&7h!Sfz2QASe{nVSkvKT+G+R3ER^9|oB6JYViBdY;Kww6 zbF}a8wF3Pb^pjbAdA}SE_MEeya^`nMiGD8f5*}$Y9rBv^lU_gWulK>XSqAYxIvT&y zN?*t>fA5Hpk`___opG?6p|9I_Y~u_YIqO4_zBoyOAg$q6oUmWB zLahJUd5Pkhp4|O~38@_aMBf0;ctz^*$yWv(?e^{hICIdV&O=~^;kZ~J_{F&NU@KMz zn}a19ZWj-e69$tGaWst4dWxV`kYzCc3ap3-CR*m1K^Ryj z7}Mbl$N&dS-MrP8%oH7hIq`B! z77AnhrSfl^Wo44y36qRCpie#ik%3QQ$)9rpioj8^$#eVLwaD!SPeA zLx91b@5emnpFUGOiX5-oM6X-Imp&al9zQPEeV)%%WR5SKQ!T==c50*a=-aVu?YBTy z-j!9J$%S8$Az)jQ!n``mFd!V2i07gmc9v^A&{yAo7MxPwF(=L0%BMo3$wq!u33IBV=yYwo;c{o;^3*lt)CUd^*>S2r*MRCGFdz9Q zc023Pu1*k)ntH%hXYojcQ$3x~N`zCAYA~8H$$OhF5}0|O|FTZw$&AVeb7>k#@W+w{ zk4Z9QTa37d_M6;esSs#YA=H`}K{*p)2p=P5*!bH|D-jI~!ub;Me0wOayd8AC24@U( zDi95Gor~AOHG?0nc`wkKpxf6pPtb)OArj&$C-}crpr~f4+t@vyeI-?)mvorcRi>>w zBFm5cZ*g@%Z89OeCkyBOc^$Dq-631mI}8>@@}Kbp;nevQjb|#7TCxIhij~)EeE&F_ zvukQDX0(XPi!6aQRp2>Q2BGMb^QYc|}-8;vY2@S_0WA3om{@rq8+r;^>7&DC=a=1SB36 zxi=j-M}gZeF)^9ano!kOu0QN_8Si^7D^J)dj-je89#Qy?)O=k);!pyR)3y7i5)>2!`(EH%4?_OZ1UNQDqqY(m;iYCnP-o)mbHTxp3^>@s z5Yb%n4o&U6SG}`$Qmn4F)5;*&`)$9kKe{uBcg*imvZ5@4_&i?>0QE#(d$#@@Ki38;R-0{uB6n6o-J zl3@?4d1e~Xv%)r07%zxIj<};}OOD(ez2EY`_2GSSB?{c{5TEMxo6R{@iQtoZ(8$F` z=&3p(yQRJ9aP=|f@v!Tw3ONBgMdf8TIs%)8ZGvnq3OUi+f3=&9DYrWBND!#Igz_PeIuyn|J!~Fx|zq(Nrt-A z2;G%TO!OFnn0BrsMtU*8r*Q{N(1kWXVv)p5wbYZ>XSJCY58qdfJt)+#CM~8i~Um`ZK^SBf?wXN1c+ZXVz{SfB=W&_yY--vnALqL95p9 zhBqfRO_n41)=5{4D>LX($MCy0;vQzk{7AQx{dRfdj~dlkDvoVL(lfl;Ur-41NvMZm z*L8R65+n1@?5_06NyM@WRNQ)xm#P*~TDby3q}bgPdTz`>;zQT%D$Sh`K5@Ex0Y#Qm zT@Msq@?-{dTO0PN+*G%48@wK&Vzq31Y7u|RunEqx9p z0qm31rCJfwBV%;d?R}I3wxd3S3PG}uIHu7?vcPov^NYR(;;GyhByJA?kfh7ceqfsn z+&DJFds*hSv{Il~w%scBavjWkBo-6c>Ud@U(Du&)E`AZe^WY`OAVY>8yAPrXz)MZz%~D1K-bhi`qY= zRMs1+u6(`{$;zc8go??M-SHN>Q|z?30>6@|8a)Y6y0%auY!e|#WRkrW7DKSe-KMH= zAw)_(At)iDLx-sZi1b-1AtFSfnM89Rqm={*ge_#m@kr57Z~GtBjW(!=$`?dHHF=NLh))l_duz3V7rS`1S^YsYPgS>QlLV{;tHCcfRY+T}{{hM}G z6OyIm{81z#uoHXlJVSu@H4Y9wJB6*QTs_#evqAC-lW3qXB=M5dg%Ks9q_*@iRGSb_ zF{tu@y^31F69-UQK78(6^aWH2{`P`jCtj^nd4IjNM}E^?c~zH&i$QPw+pF<1JUE6iU2?1}((FF&2oCA-V+g+`_~5 zNDyxcz{xo{kcWMw89A~h+lHpf|L97yGXWmPSdpZ7V?!s~l6h0@H7Lf-9Dt32B~ZRW zw>p;@a=DqGd`mO*Lr;aQPy~d(&RxHPwQD8@=h_v1~APEJr23a5pd>gcr-a82tMISUh=$ z0hEW782RNIi@}~d0B}{xuXjpj)8|%32bhq1mu(mD{CS2pQI)hM!W_#v-H)lKwv!XW zHcWdaQ{^@pafFVSvyGS6j(l)PIxu6yWq+3uCgnnnmYtH08CbokWrr#v9$FUL+wR`F zC|kQZYj)I(9{lrl_49;HiIbM`B=)rXb?^1CEACz2>d`6N4k!o0V%l35-YFIMzU?sx zfw3?PQ3h))q^o)YfGrkG{PvtYByzMzHnZpNK_@u%rdZ5g#&5&qlaH^^JNq;W9hgwv zG!Xd@AKZcTg(B7KmA303HB4X@3Qq0(U8oFn^qtnCr%9(t5>emAWwuIk_arVW0-^fn zC7Ly;$~6Wqnk&=nx@@@g5dm#M3|z!)>n4s`8?-eVZLCo z&y-guxsd|l`BM^+H2Fl9(80PB(cel(xI=5iMAZFkkZbBFXQcPd5zeOX1^aBd;=qhi z&I8;zRHMv9ta_GWf9lZ{d5X+nD#)tfxg7LYv_3X5=dAvD9oHRLb#q!wSf}#BHSO}o zPAOm~$Ub?e3^1uO@&$^7;_sdJDtuBukZgVxMhTe{lFa$?eyqC!T|C&~8Bg&SmVp*Q zGFT=Kh;gfmW@#{RfSYGhil9XmRf1LEJj=CBq`^YIK7MeO@pAtX9LFSL+tsliQEv{0?Zy2kcMa*jKcwDgkgq_`yDg3k@~ zfUmyzICPNa7cJkyDh;*1FLDFx_ty`n5+_S7QUidou1OBSTk3x}Qs(^nNVX`_iOKiG zP^d}w$Sm0924M4i@=3M=hY7=gz8mT~|9hwt+aLu4-LGpWOh715qEKw108ZB$AwYdk zS^!3r+etUzu1p1k{2vOBN)?9DG39MW{FHi;+a9@Pr}*WBd&P+}I)M%yoO%*4Ti zr_*{wQQ3teN#h+n5wE&m`7N9JvR#vJ|4>$l#TLB&8s{&lG!Sxv<*k3+ zxEB;-_9yR@RC%3G?&NEo-nX;8i{707Q4aJ73Keu_OGNBw!p>|$gdf9UG3sGpF=9t@1RJvrv6%XM7zc~l2I$r^c58IU$^jg3vs+l}7Ew!SBp?CqK-N`3 z3!bC3zYHC>#qLBrQ^oESZ?|V9wPqy+x4YESvN!p;+QsVp<#o_@^|%s2vXmGRMdbUs z$&mnt9z_%f{}qcM2{v99S%7$1CQB51x6hOyT27V;rolOoX^*Qcz4mH_<7zh3JJr$v! zyF`a11GwzqEQRW%;HWvftYB>Z502mbj}nFh-O@IGVf;_}pZo>`dFf`xtqb7)P1x1a z2Cv``Phmi}pbR+{Dt^sNp0drUHs-6eC5uzhSpBe1D*o1O_@zf6JAx|4! zZ9|2K+xEb!2vfNWyrON`tktbknHC0OXh7V%{6nt&QQ$1tHZwDJ-SRvU$Gk!-{SQbv z*j!h`Z|^73gB>6bUa4n4bQ=0QcIsNKQF{jGU};g0O@?+ELvZGT*{Hg|Ln(h>chk2* z-dtDRiJmsIKk3FXU_yn@sYK;4sp@R&A~G^1Xe`k79aZDLgu=LgG(|(NJgF=&PR)tX z96bl0s;?E&ti2Wv7o|6m7XNT^3rq|v%csAtdIDK`7gg#AhS^Q-(1P>3r>Ix-SGt!^0Ukywu}ON1vZK) z*g!MC`R58q5{)IR!&*%yv$jE?y(|))oy}Num@9;S79C$-LJQjH#;zAv2l>GXX`1fD z-A+qX5}E6g&r#ZPDQ87ruHpvm`Ca@5afX9ka=AC_w08gh?q)J~Mpyk2$EN($A`dsNtR&<^O83P}ehEagw-|z(S{y7K2U#$7N2&%&!Qf6t5~7I*hml7xCpCBn>AHvkLeJp-g&`r6(&G(Lorvd~ zH^)Nwqh>XirEIl{sdIHKq-=F-1XlIG5IYmd2*EWyB;p2B)Kp{asR6;(%;0pJ_r*Q= z`dj^f_`?tFcdCy(|J==x{moe2R2NPlwHPjhiLGGKoF;aEp&4?*Q}=1M-O106g<2(mnrCKjb%CF#CfZ6UAaMR)DT`qe3YI3NVMO+rL#5aO9xB%AEHX#!4`+JuruAPzdwix}WiKGIohEj{nf@Gt@@o^id}e(c_2 z$<1=sXBWfEhqcAc+l!!M`?$9$Y+DsJG2c(`vI>8 z`Ax3xje-Bq=MOM`uH03rs>#{iA00$;bd~-R+-coDi{SvOWtE{CeWAZ026Izi2t4@l zEr80{bAQ(&7ibSlEvHsC4Yk2h%uD6h8nRv|G;2V}sU8qY zg&eqRNCh)b3sOy2C($))7lY`-0G8fMk1YE~?4K_)+)Oc8dwGwBa#r3Ahi0s3jxv~! zEC)gkRlQiIPjjg*yC~-{KE+#2`Nvzu zOE!CxGS7(nUpFRc(VFRLCP5XgZ|gH-w$ss9}q12F5*2gml=qm?Y!9%@E4*I-0SJH*70ww zook#lDleK@ObY8=b|!9BUQ!2qRg_v~Uts2?a~tDkMxfGG@x+9T0=ulgymbVVr{E}F zMN>`o*>53InDe{RSZlreES=CQ;@M1abVu17Ty?bp*%=nyd72Twyb08NU*|=8fNpH4 zHIl~5wJ^0AFrV?Eu@OGh~q?`Hk+S?9GiT5EA&7g zLaIuO=yZRIeZ{f$@U!SN^T3k|sm|C6p(&OwB30!lJ75 z#O6%h$=Ko(*0^3ONj?U?P1>&&AWU2zaS zW>&)qyj)-%y~Qj@%+y0%<^_T4EBD^Z`HxcN=4!rBN!sD&b3p~@G5R=%hYocFbd@UM zvUMcoaka9`R7NJ&ds+((xg2b!FJ&yPQ})dkINL;Gl;E2!VuzVj(t4|mybl%Ij$p^9 z;!ltRI!U+z9np&MOa{IXDv(Bd#Y5aiFA=Pko~Qw-W)pv##rHtGcW048MNhttX`HnlT0n9^xr-N0HCPjg`GZ=QpH6Hn5vS2C z2O7M1xSYqgf|sF=kF!Xev5*j8A|7j^BtDW7A`WMwz@u1B49=uAql43}kRJ5A|Ndm0 zmF>*Q6K&?Eo84#!q}!LiL9s&j9rO_i-H7dqly0-@R&QU_*KSTaI{B6^GWrb?#Axmv zx8qb(bp6EZB2{F$H-l}b+(GhsjCPMgC}%4HdvDUE4vRUhYiuIUP{bvoc50P z_p`*T1MS1W3c^G-r&=!aAqMci(2%jJN9-9D*DPOQ zlE1i5k_F{u^#SJ=4m8T;MQ*miP%q-W!YRa?k3ctLBS-Ari~@XV%vk0w!qXNO`MdX5o(${}22cao;tao6 zN0iC<478U)A*w^_pZdKt1XzS!8sb!|4MhXo{vfNJ3tx zJW~7E)yCQl8#R@T2d3t{%vhC~sOb}TCyx>W|H=K>f2A;cH;_U%(;imzpX#W4;kG!4wXO*08=O}1J2^(@^Rz|&%6|(tpUu)DmG?-_SL?9RU@XW- zY`w@Mp2Kow{<_bQ@Hz;Xv-wt2a5@@8K0P`QCx!=dq~xS?2h)f+op@<r{4`v|#FK>-3|5qFZ6U+a=QEcmI+HbKT{I?yHfhQCl0VQ0SdzKdF5xiYC za)9o??4VX2(Z!}BH6&i_*2MF%B2KaPa5GNR#rK0%C$A5&32iHr-)AKsZO8)*$a-^0 z(iJ4;a+J%|b~wthQQw<++c*4AKmRa>gPUWunWwaO5+HXV&#G5su#i^}eO}+Loc;d3 zKDT)NXg=xxTnvDqnb^03pr^hrbl?RibopV{lB|9?o3BSW^D6jT-gu5W$P)61WTIeM zDshD*!LkhF2>b^}fidG#9N6+&{H&%KPEtUc_#Zwt>l{hXBFEYR?~UVv#BzJSxj(aW z5wWah97|aOra7^!R8$eDcn^;|Vg-)62$mBCP4;0@d7Jy)l4eS6-XN>;&IX4u{!US0 zfQ)I12~hQFktnb#v}^}7N!W3l2vk+z*_;Bg2QX^;QltghT=eY%0w`E!i)BjjxxkYW zr)V_O+%4!Y9b1sa4g+}pa2D4GjgrC-2mj=k^QE~h5g;YeMgGhAf?ibq<$T3}vMYsN zl(!TY3aA2SX(k1j1Y=e3%-IOIi`yWF>$T@*+ULLZMPuw#0K~YwZN(rYG^|<RJT6CwY?0Dw6|lM|j02 zf8vZ`tqT(g9za1 zPw{&V&T$j@OLf^r+*#0LJ-yHP9Ow>bzYca!hk<~D1{J11M)^Iv`u*O%V?IVpmIXjl zV$%*P9UTl&Ww5l5EO;s$YF*Wqc$_SP@UzJl8Dv7Tr;imdNbZJpwbX~jLvkb;y`JfQ zYm0f^le1vEn(69omB_f_a!VaMDCv3|GK{^1*6c1=!8OH5qzSSWNwI2!_}u9gx9>Rmtu$e z`cX!ajMCkuboBZ&`<^t+&$+tJgmNG&GLV$;Yd8a3zd@{3yO*hUsfwF_d_TAI@t`Zz z_{wrc;Rj_WRNCA=F3<+!vo>y94I{9PkEuO(84FmKaM5HrTs^)kT$>M?FgEq>Ic8r` z_}+V5o7<|-SEe%nhZ3*pdl>zTT(P_R>e0q$nFg1?oPCUSLsmz>cuK*>tV%SFpjJj_ zanXCVb?@kw#;}c=UrzG>f*ptALp^j;wE{g%Ido^s4Qa(>k?s%*1%QrtJo5OfU4%9%H6%G!fF z?OWQ)c3{R1ZHs;KP-aUp4EZFuv`rxk*|Q4JqQ^t$(`?6?;1oiAR-umXHt{6WriKDJ zsa}p(4Z%h9mO@nG*wot?YUf0L&TrRTsc0jrTsL;UpiyNQmC_Epb^V)q$cFlf&XI=V zd7L5M>MW&SQB5w%pZ~8?J8Z~o@?CRv7sK| zYx$|Vx3;u|G|d{V)V04m{idCPJ-M5)_hh>p-ZLqv<_Qk#seL1_@R6N=&IWG2Lv!ex z;a16>o%kXn9n(K~Z@4l4;rhllOShyJ+CQs%Sg_pPEM0R4mS1hZsCHn@+S@}GC^54I zFRjhR_I&h(d4IB(%FW3CDNIK}BYY=UL5~SBcA1Q$#lPN|b;yDi!cM(o|7+g2BPH?)%i{5$0m>zCJi?I+^%&=!oM5fm>GZ z%S?JH3;GKid5kmTIxrjjOuEnTFk&u|5;9V;o+}qcB0ffa=bms{=E4~E@+BuZ0wkSy zWC)j)lVE4x&1~c)$FRy>3UZu)z^{S)`UVlA%-*_j_ul!z1-`=SWnZI6k`zj@m4DI? z_%Ql)S?_VL#dT{UOEakUc4o@2$wwRgC)Tf;LA=ge6>F$9djlh&qfcf`)d{<}o;r>{ zJx+$NBl4HWu!BoL;6BR!6xXn^xg{jda%pRmF%)G6=-opxHE zF)~kLR+!0lISH1S>&1S(em3v$`uur2zDv_5e)5(;_9_R#OoX&|tCTEa9=4hE+vJ!! z51#k7aqU+Gtu2f!1tF{@(PsXXmVl)ya}1i`%(niL`f~n$2y)>Z0x>VQr7?P5%>Koy z08o6tjvMer+x@{w%7G>f!v#Pq!~V5QgJ_38{+9TFXfy=Kp8(1egBFwkbryv<`9NwQ z7zPyGXJ?CjZF_^0;}>@i*6&;Ar(X=lNxtA>*i0#_l_?|QUab~^T9%Z5AW;O~a~1Yn zk#bb_qIW=8g*0V3k__a;jv#=Mr#>805|$ycQs6QnN`m_12nVEru0yg$(ghFe$lvJ% z{J#x98O#IU#?SS%4F=lZ!_5P=&akB914En#y8oO#^tB|2LRNCKFq0#XN&1Z3gFMuh z5l)f;7Ae7^;#+7fMzQ$-wP5h~TefZ6<}KT{ZQHhO+qP}n z_S8Qy57Qm}&~N#2A|oR+Pn@;)Uf+6Ubz%DwJ-7Y5C3nNJWvk#)bl(|W4k9DIWk8z? z2k8H?2UD&_UZ&WcvIqtaL*z;ZOaVocEa0P2CWrpFMo|IN%3#L#=4Zl^2Xv`|$DVX( zrdgrsh~MXB5dcmKT?qszenl{(p$<8z@En<%lE45Y#o?0T3Q; z0Z7)YJ><}s7+2pUP7MJIpg88wOzZ-pKAvIi|0k3E&7FWTaFFDt9dfvck8}gUpGXXJ zHwL`YT>$*B&!DmPpXiL3A)RkGa0}lneJ>(Upc{HRQE24!-+yt3Nu{4)nU179NY2CA z=fWfp`PRf&-;MMPK+(Gmdq345yq>{i?BM!?o~}GyUg31aQuy;RX4yaf02{)ty&R6- zn`FcS51&VmG7x3o3tYdy0ZJk`M8oa=11>BV0&e%$?wy+%bHQKEnUufj>}YbJKp}O) zk<+9}yT2K}Kqr8LH;Py!4Rsk%BzFzs^16YzxkWxJL3>)Dv{ap#J24ObE;_C+c7ynB zVKzM&ASqe8bmsljPx*AwGEou{%M#jkni-yDL(*mk4?&lefKS-$dT|?@ykL z3yR1w*G;=%6<}4fB|t+L=Q+HrTQ00{GR?d)Xw!vMt+a;z6fsw7T4@OjQcE-EW<;4* z%My!7?x`Cj))oBmnyr8hz}MbLqLGmn>eS}O3sCI1!)CVyszbvUZhFunW*(-;J$-3(I%3H_=b#AbPp4fmoo*&_k3BO zqnw#FW5ZD)?VUfx&p0;rmm>d~NMR86gz`RTtNkKmx04x+g#>k&-9x~1b*ohnXkZmq z*c1fn2jr26pq#?a%0fpx6zbpUry^zcNJY)gQ)!iwz7IjA8{?Alc9E$luHT8dAx_1I z$XjH2()d2=2dlCJIcDrpcCBH_u&!}ohF0vwZAGbpMsfurVwXx*c8WYi6zHUURx+~^ z^7r-p2mKKH_;tL!%R#ad76oS21Jle_M$v>n-PHwZ#~bpjM`)r6cjf1%Xp7rv1!G8h z_yp<86wpnLsMs~r-X^yZZ24RriylP?i<%E>9++~C{4ydB+!i^*yY(-7im))*@srPG z%T4Ya?g1)KchjBbBCqY*PH8qn@qP70iC#w9QCpF@Jb%=NSwzo7mo{q~>(!W9sjBdo zS_k*aa%@d}u==_iK z%sz@%{Wp3__yh=`PJFA7VY0=pk(eA|A2l}&JD*3i?kCzlMlg@NQnwjv2Bf<26{bu1m*RYMYZ)sC2xn7)d|qs@<_OGpG9OsrlnJTu`N=1|Y1 zl{+t*&^KJ)D41W4ge2F)<9xF%ryftuayNBZDH~xZYwHx1$6kreAHPq#H-n9-;*J~% zu{6NzS1}FTWdYmM_~L;m2SxOVS{QG;7??$D@Z_!1=3qJ=Mz4b6L`Y?n4XcW6)tAX4 z-N|W-*Tgh2LWN{c>84+cS41sa-+(}u-!hValYZ_jtMhv++!!$NS)zfswn6}jzVMG- z#7Z$ZSurrr4yn(;V`A>nUA`nco6<5N2)V8!bT~(TCwe?wd*Phy?D9>xr}Wy)=nFyjx+-*u1VWWnz-ARN>vET-9Y_ zGOujOz+z_6mChfIQx;U`C9CZc5!B#GiLF^G=p{j@whv+fJsZV?U$YADp0W3nzMRCP z*=Fag_3>4*6A9Sjt)*wQmKcC~ui|>HSAO~iB=B4^h=u943N?k+$+DYNgC#QZ=^Y`m z7|w-KJBkh5Tl`RvkeVK?{ETW>Whh`KDZRNQK>uBYg0r{bN6jU0siffwL;Y4wnkJ&L z_+&lxM^WvO>ePg+#-0?@#%8<1Jd@Jv_q^$~M?>f<*MpcYwMp zh~LdzKjB1`)!yDLHFc+?@lsGb|=M#dL1s-bWr#xnpq`s5M6`sHDw)xrp^%1FJ975BVPaZ zEG4T$>to-dGZlI8E1!-F>-+jJyP=Jt_q{;+jn<+wg>s+75+InGN(&_N{$1Tu4cAJx zuDpxrU|jvHeu^jf+|R1A#G@xB4Al`(jQR7{3qqqH1BdF)#R-QUd;6DWGo(hC22zog z+}Ub#(h)wEqb=NCtL|e}ri-$pcCPOP1(aiKD~{reIydd8PRsj!LWprh%cr8BEN%n2 zlx*$g+7b){xwy~Wg?84|1wYh5FrD7my2Ub0l^K2?ys{`hhTfg6IKWB z%;rLxm>|2gpnwM4&AM%OaK_`27x&Y%w%*f0rV2j1t;ttp#JPv4Wnk{|9+r`XA=Qf+ zW2WAkzms!9BDo5ep%JdYWW=d_2zHWW?9gKXYz(_a%&>DB`7rSj0GTk%R2X{#@)Qh~Fzgs5?@;I=e(T}y6m_V}*#TGw z-q{`%X?eLAyB^WR<(4aP*dg)ZOZiEvsj{LB<>4l59qV!x5+iFP{)^JfmoOafbpU3M zL2SleDL-=e6B+B6iMv{Z#Zt?HU6_XX=20_qzKc)`%Eof*v~ko%PnE>rRCi5S)D;I& zEhXw;QWit}IB`sMEMt5*NRpe8lK*S%f{DDvuBr2WbkiVMrudYvq{9M5B|1y4v^kg} zfcH?G1sIbB%iL;C_aVw!-Zs{YmjLfLFc{~99rYpUhdybW5NpCPhB$}AO1vfsGj4#W zJgeu&a-^(CGhKe&-t%o_F!Ezb{VCu3g^rd`pv_*5>rI;ny{8+rAW#QG^ZE-Y zWH;{fJwQx4VjWJ(#*v_KE5?sAe={1fN%1@8=f??Ak%iVfz4XhR%UZ%b)Hrq(b72UL zmLYw@9%&A+w^MZ(`ikM9RWOO#9oXO-z+aR65&Y;O;H! zoPiGyg7(6I#m(XXK)gQIyY(!GuO9;Sci}*c7$0+-VMdclYdE0ID5M}*-8U}PpF`vS zN%8$}sU{38%>PaCRc|%leR&SK(I5nU0D*vBH+em5_Ve-dTFE2H_!a-aJGv50M-~c3 zEZQeutn%A(?K5zO9GWhYW7GOf1ATwb*7>}bPV0E{e!UPCU1+`=OsC{%gW1eTP|kr? zJ*h#Xa=_;De!ION(VqP9p>ByE1>9aUP<=(fJ_Rh22rWUL%`@F+et&C@lNo)uNzs{qdss=haUZ+_-RHm@V0WY|Nt}bBBPQ-m zgqqdR(rtzw&ipl~{NQCG*!g1I93^+zzxUWx zHJQ&b^g0F3B%yg+GNOL-$4-U04+2}CpkqhiSl*ySku?N@UY#DUAnc}oyE%d zAwx&}YZt2Fzv>_C{_Ci}1Ig56bFzf|A@dLVZ?r2E-PJ+r+pjp5h2i2i0fXz^?p=tn z+&z0)?fn~QF-Ny3wqYw@oebSs&`hNH~Fx##T>n_%wLlA+F)3QWf@L&e2k1wjZcTbsKSYQHN^T9l*e>VFQMJvt6zFCXKY zt-IY;7e=a^sAss3ik8lHs=#vh=%ByfM!oVI8o9!do_S#)l5k;F9BjchHzkG77^)#< zOw>ZH&)y<{1lO=kHE6li74-xAJuMjVO$tx_c}qEjf}qYcL{Qchmjz89M=m{^C5_~3 zuxES3p;vW2MAz#lQpRVM=|>0JP7w?D*aVQ2RX+}JHlI5EJJ8n;3X;p`J?3BJ=QJgZ zin!^*;u>0_>x>Y$NMk=(l+?>@B)aIyP7ZvnB2>WkY5}Ct3x|}pU>$e^R7y1kn!D-#It}mDhAb}s%8ks@L2uPG)2@OO07nH07 zzL?Y*4*;nv0{S%_Kict z-k3+*C0RhUkh9f=;%fzKjFj|uI;JGEtS{${TlyBK2rIAQ=Y4pT;i%969dpW^@-{q2 z9F|E#E~}4s<(O+yBI$5p7BtHWRPljky*QQ~U)nksbnutl^T0Z;xkhE8RocPwKb=Wihucgu4zn@ zWSn~}66V!48;U6zyPC^)5jv2|#xlxm@J@tLpBA@w$~{J`iv-NAkc=%D z_jjnMd|6NelY?FfO9NxM+e#7Z4E8<1fLYgU4b{6kTz>gExj92#E>W9(nR3qRWyQcq zfOV}fp^l@IB`~r(BUZD^)=cjtyj}l^?z(pA00sV8u}`;t3Qs}#vQ$FP){Rtt zb8)n4TI!-&e)PrGZgYhoxSqu`p8v>KUqUi=O6p8Qg}wD+4yobdMp+{%8ZG5-gB;~hTf;bzY*%X} z*Sm{CV_?7pw`?QC(Yh~)Ahkg0auQ|D@o`;$3b@07>hR(7(HF+Hy4?5tq=35^Irh zE6H=gK8k?7L$B{0%$~M1TUKPH9%i;H&O|fWeQ=VRAi0XD=lZBDj_91@QVE>voN!i! z&2G|sYrWJ}w~-P3ec8>w>eg0Kh_F@_5jY!O*<3Glxb~n;Fww9FrJQd%(XcG|HOgfV z9D@9HD|#P>rbHA)-M58N-i78j}aBAbr1BHychBm0?ZTyoQu8LnquaQ!_!!e4Ust>9N;8kKL3S~sZ zS(8R%!#f15EprDMyU1p<>9~+SQa}#}Lh<-I;d?mOo;D^P~K~wGmXoef_|ur{$P6P`h>>RU9Cz zbB~mWYkUS0Dvalf)(sTFKCE)y4}C`!+>Rqk5fLG$KtYu- zMwi9?@k_h3j2E|Cgp?7>5gDxQyTvH5E?Gp`%re<77lVtIt=OS9?pR%!K^BvZCvz3eE?5hwuCs*2oO70BvPW#kT0Tu{#5wl#bn zC<1Am!Cl+DliMM$_a=Ik?~-X#HGk%5{C=Z^1;lMwa4MIVY@p}}&UF|=*|!U~m(=oA zQT(Sm^Qk}{$d}5VU%PuJUz%U7Tr|T+@KdA$Zxc*JbTeD2{NA_Vt?|tknMV{O$6mTT zj0_h|v`v=a9s+|ejPBsy_TgwTXrw7>c;H9@1Wi7CFAQc56?>K7u2hu_@)#`t#V$^p zq~fJ z1IXn|zv*$JlPcf}1&U1yg!i2v;q5%mAqT({S|`DHP6F~(+s$FVBm_l27TF=gJ3aji z$^?)AGBsE@Y^RS%a+yYfkfzdPwlD%&jygRY^S3qBjCttqd`VfX)F2ju{j7x()0CFB zD<)7-#o%=_d_jRlF@8}(+P-yr>uZW66w*Pc{-NBaDl#-kr*#q_iGj{vfiG^P3opK@ zf{CalEvF>nas-_A?{Ntk<}F23_`;}5DIw~2jmQ^^1>7~x8<@CrP3+CF>VHhl2}UfN zoe5e6`)*Em_ha@bkb9h|&{cHEo*EK?o%gw~6z*ekICnC@R}^8RSFA7tb$gLEoSlBs zDX2{E$L?B=O5^{us=S56}{pR7sm--+*_RaeUccLLR8vPb5!P6>dW=*NLeC78F?8uN$6Eqw5BSh zb|1pC(x^)7@^m7WGcGYJsSOphC6g%WCvd;eJbs-5=a!j1x)f=Bift`aR$^f-Dp)6Q zlaP%Ni|*=g0?Qz+1!1(m##d2c%J$acPpqyfHCuGy6|^^wsT+OaPpdd7n_iR7(0{sb zoGsF9D6LT?MO;oZ_Jzise`cD^6~L})YsNKLu-s?Hm4El=apjfLDi(ep%e0j*X@o!C z^({>3B^f*OhC)3JTl5S1g?1iWOwPNtl{?_s#rJ$GEu@YwfYi>GyaJqui22?XGMzO% ze8ugzQYRI%kiqS@jCn0=gRh%m2Ob!tR#ON1W0POzy!4P*wi80AL6+H5lifHXEiQ*1cJ1#U zlE)t65jLncOq+M~#>?Ba_tNf`)n8*QXOtWb|GC8Cu$(WMkROOgIrN~!a8E#`w%P9y zX9@ZB{}`W1$L|NgRuFa-3@X;S{|y|YL>9W-6*vn`Ij(rgYgPYuGGd>1c!oX z(Xixp>Buh9LoUY=ArR2CW7g7`x=>nOZ6zSJbjKf!R z2j#F*h<|pBAB}3|15>T{Pg%%A@tFZ=cXhU(;4Bs+-)?m_tZ%mya9&U{H={{|-{- z?Qp9-AAz-`8yev22Yk~P6nBe z4yM5LD??3;LsZHW^5Vg41hEFnsikpYRo#JQE>3joJ6HNy#1W=408WZ z6_5WVe1(CP@xR4CY(#Ii{_(o`jeqbxiXY;vh1jj*{>bd@_3kC=;HQI&o%#VLFDF(y z>$j3mxlUeT49=rrrgbWJ6;qbO0OV-qjT+p+1^GB7WU|qr{plh*3jFPJo6&YbZL{R^ zhvzm~H#+j4l4(7B@Y42t7QMkI&k6X!&v!IyzYm_j^PAyj4aQS66{J6YyyRqD4Bhl- zd4H<0{+2xADQ>|qNc#d82WwJh(GKcXSDq#EUcdH-nogRNT=fi0{d-xG!iy99>dAm1 zFeojIWIznkTc9Zr3KJNdM7es2h&VZlntWD1#>HIYd$rEYPj4v-?jI6%dUjxyl6#9 zzen<6DkrVr{d+MLjUp*iN`9dU14`dvr0$d{RzMJWCn<~5WfmkE%M9N|^A$h#+L3^F ziWiyXXDn!B_@tk7)y|l1EsH#p`u5)25r#b9NxC_gVz_xKb$T=c;diQf`I3^S6dZUG zug}#hWM9`HTN+q4NGx1w1}94x9b@a~8tV+G*mFN1(o@Xad$ur|V^;ZiPpy^zI{Gj= zMEr7TyLoi7r!5$6<9*&b^xm)-;LdzB7R&<=jY-83<7tMl=Yax0sGVEKtixThN%7f3 z`@#I4ZaZ-wYa4M<`5n-6KOdz)rjuwzNXYh$B#7zk1**#NXRHzu#v zZ;e3m0ZXS~XN+JTq89TKnwI`P-RYmG`B-_l*;AQG5Y#}z!ba3&6X-l#a%N9cSoUx< zaWt|b^w+Gj1b4yn4cTPfxmxrGhB~hxt;@38v+FZZ=k;~vc%sK{H2)&dcQTUodu|t& z(t*|S8tl+|brW`H`}OmPAh+#X66$%SbOkm@6OEjZc?rC<^xs&ZfaK$H^M2W_GxRnj z^T4Ho2(E#B_%JU`9S$IIK&bmXf06SDzFnBV9V5$Uofhiilv{?|R(L=d;|>X$HF7hX zQCpbvy@Huph#`a7n}eC!j(zl0tKFR|oE;BH_*g+BaxZ#MgJ&|ZJ&m3{)~s6hu*xP* zCI2`U0D2u5^MX?cm>*TvVR^kqUY=fe=!~@`P%CI!b3udp5lJ$YJtu>Lx3YppU39C^ zRDf55BNqIa{5YVd?o?(&w*7pwy`ppU>^UZNF&t*^kJbmTX2`yjE2fd=U+uIkj!Juy zq_(gwR~lVlpKw{&=3(q$tEnjYs+vHKE61qcEajO3TVcpinZPzoH~E?A*fqp9V)y2b zV`3LVPMZ@9R(6sp110W-lan^h19hbt{55p^$WB#qwew+sD;_3CrLtCgHeZ(3Uy`nk zK}OC>>k#4nasE(Nz!A%9l$=rlW?Q+)G-LZ7H2$3LhN+dKDnS^AkX^}7)E}v2n(`H% zw52 zt(BDdpp^Q<2?d|ZTO;Zxg%A6dPW2wTV&O%pC5%)6IIZ04CKW7{(#1Uoo0_CV{WYKt z5n{4i;j7CT1`n#F<{P9Bhy{ZD4y$o;lp%yF?ToYYq#HXqegMOZvr~#Ay=Rm;}L9=%*CB zFpBT)9;F&mpb-~fat%+`J9{=)N3i+f6UB^=siH>7qc#TQ<{B6vHroHfw9(icqb9dy z!{!5r#%VHe(i^2wT)1TjLJQjNih;P=`mT?$mqltM=})?6V87r}fo|e5gf5@b|r%aDk7X+=pVAf0nKt zV*Y1;6L{jqZ9tRvsS#>eaz@wFq;nn9Bx()uAa3i$pc6=E9k-QYxB#bGmy6<4b=W|n z5(086Qc)1)ogHr58Uiv2%w0^06ndgKgr}pAd8iD`-~-FV>&EX+@%N+nbjVcoEa!so zi}z6_$_O6?t7=I6N@H5X$STaPSlPNeJ4juGFl^voyLya5n(mLX^nD$6o7V!0ky_aw z`CPZv@9Qn!>8f(B?x2czy%ejpnr2`pL`gGWsrEP(q*E&s*;OYO>uBZd&3?`%xB~OMha31yhjo2_A~ydB4&} z)btsf2$vNjOS%vGTAdzL;cUN3pl3o=F7b*%;HzTOG=<)16RO$y{sXeiV;bTyT3CQb z_Esb2@#S;8QWMJx52t8ecD!J*&%iAaFz%@JvRKUKs8z3{p+Egn3$4=8t+plNeR6d_ zcSK8-GsFl0?Fuz^S1>!_a$#EXTPs`2SldIhR@$O%x}_1$3&z)}ku8S9Fgr!F!4)yk zA?Nc$+4_=W4sIqo!6|32$xWjk+-^*zPGAiJ8l{AZc0)omry?7zWc?y%dlVe1LE&rJ zi=yhI?7%|6Mlu&AA%y)UZuta)yUyv(*Tl{021Dcb9+vKt zQJmfTZ@HJKYhFEFZ#TD?mDJwjbhb|qP*onnX z9LfTYe9p~7muU1Z)_g~G? zS{gB%rHI})I(sKc`?kz@!lL6=_no3~_rS-O$ET+C^Tt0tj=ff;<{Or%x-BIpCh;LS z?!N7EPjWQ2w1t>H9LGOhXLB{fVb^yfXET3K1bG8-m!fcCTSQz0pq4erjY!vYJsrc2u_$K2FT9ydBb^{0DN#NqeYDEvvtCT=!fQSoI2ap!eIxh0E@CvssDtkDV5ekC zjLSb6XY-;rGhNa&I2TBG8fWbdjN`Y{wng0xRvrpW7Ng0t+LeJ3^URw{WfMnoG<6bC z4^w1le!~q?G$~A1#C$Im-;*W%Z0R0q;C;#&hOrtRjaZ{T(FXHO=blGb{{q22CT#@e z)}N++K|C~$S4WaFO(M(^kIysPy%#vln}SK$&bJmQIA|U?$K2Q~?Z0c8W7;1nSqKsy z>=`QlJ2EH&mSErzX{cWjwW<37);%W>3eU01}rv?JzRhkxvg4OUlO7Ov~fL{glu71MfLFll@8QXmNAU+Deou zj;82Z?2e;2D|u|?faSIA>XqK7J&D)w87iMPFSMisk{7?S&us9VkC*~-vH7UhA=mD@b_^rYR4p^@8L6|> zo2-8@`7ASiJXVAF{Q_+rpj1D6shkZ_Ls}b8C98JR&!7`pxyb?p&Tg+XeZo8f*|T&< zXVBcb8Zu=i2V%(EFj%?Eg9I(p5{{3T4t-9vgbYoX99)9d)M5l}B9*h=o;{T$nzNqf zUHzJ=qIC14eNQ~SfN+*zf5}yFOW4%HVY9ZfHQh9=POSpU&Nks2NNEzhYPn@y_lBZl zdB#;4UM?lTP7s-A>#oJFc9Sexu$Jl=RFrY*3qNp3jdGcTAYH^H^YxIndDh?k6`PVciB z|F4xB;3q!{bU$6eqa+H{9(4`djQ(*HnkXatgRf9iLfNFlbbya_m--6H3KIb^#{v4t zp#V^FK$!sEEx>S-AbXZoZ28fCJENQ5zP%rdOUEmg&N*BB_ z3Gp&?3wRc#A9n>-+p9f~JJU~qpd8&C(m4JlyI~!!31sCw%jY z3s*?eL(z70b1@8M&>CTUloTy029Z8ym2B{Hn~+A8EL65H&nubvvx|Tl9~`o(ej@0? zHh8^IX_03a$)bREwMO;$Klu9F=nj5Kgop?bal`_r@T7otQ=l@Zk+H~+B)n@< zqvigVXY<7v#@|9$RhFB?O0iMCE1SSpzXm)c$%IhLB|*mW5T->8$w~5S@cm79KIT16WSW3~MM1Pe@p=b=n;66KR!|waF$zfkAPezB;LQp} zH`cjB!b&(dnl|LPFTD8e9-Mq8vT0OkXI=3=M_?+0Up?WWT)MPSfei1PakzUUbDeos zAy+Dl9bRyFf$1b>h5=;#ruLw&uvp`vV9?f~;A>YEX(gPg5soHQi{}x7shbuEC4}!Z zk<>J#kDvG|)yHOq+Ueq#Afm%xZX0!a(Gx-t_e_xSV5W$|x3RNuVv7E|x0*w6V}(vc zO`Iv(?*QE)h90<7`Si((blij+#p48Hb7|kr3-oExNpgVvabSQ_{pZ>9UZ^##4Ae(^ zXn7<6%$Zds8X!{XA^58%fPB0PU zuMB~^# zMFAmf46KgAVC-V6;%K@L=(p``mv;166mVC5rOX4z!YOD zLkED=(ako&O7PZ^X6<^8L6?-dbZ&XCXoTM?Z3eEGTs)@oWxU02Ar9y6+BJTyto;Ya zflw<+Lra;jt;AAo%!oFsEcbj|-4p-|@;UKC$MM^0sw#G-2fv}Tlt{E|%-6A;NR%sK z4@N+qa#<>v@x3U?-RfpnGF!oqi+yLaK{qk}c&r$qe!@E-FSA7%>eMCd1Mw~z1dBdKif3$x%^}tDHKzr%?)Mq zl;9wkn@r)+xzpJF@|#x!MCI(c{fy~M=lFbJRQpOg8Z}LL_iNSG9Svb2?O`Ew$F@j;TqAVV>PSB*BApo(C^oNCtFX+$ zWYYzLZ^bw00_^2E44H`*Hq|dgLwit#GVEDV)yNgIO*v0MW0MZTrknT7{LzQu|5=Le zhOh(1&)8xGX2S&Axz`s*OTy6~sSO;(50HT1MM~Pb&o4}EUZ3TgYu-}@7FP8J2M!7` z8YNI~StuPZ@8?q>sq7b#L(?*?A|e|5;Bm0;!NiUIjCScCv;|WSrdool;m5yC+?oud z1T80y$;)UkB)hH*jYe@ft|yD&q@!k!$;mFxEgLi0*fW!uS|q3Hn-pE_lyM^(41<5-9) z1aD1Ton)XirCoR4@VOtl>W2t7Msr%`0dgyEV}F?nfa6GH^NP?*Ns0l;mD&!rtAvmZ zup8tL9yLP59c0?KUZL zw2Oqnd^3NxFBJN=gv0G zEZLL;6;nNDcMFnW0n<#~tZDc8i@(To?@J?rY2s+9zeukTJCph!M?j|fBU%&?Q^&0a zT+pcAmPQf<1Sa*k$KY8Av28dEz#-<@(A@2-T)Fg0D2%mz6$b)S!-yUK@I1MvD>Hw|0Ka&n*OzM^IS-F4auj6JRMoaxfLc{b;8%az# zm;{#P;i4e2v$R|3llKb#jMZTQ{0Yj&RDL1}^gms{mEN)lw-Gikg92qgG0p{BB4=*q z;3qv*2hN%uJeq|Z%Ln36x`mk)-+a-sqkV}8$Wy3~$jVblKT$m;N?xJ30?HbUYl#4K zOs{$G%1vOqZRQ8b61p=AK;sBIatbg!Z6r7aIwcz4sf3t4Yd4_{lA+UwbtIT|fN%to zUCBHUm^Bl=+u$d~bCWz()T{WzQBN77OL(I_pp_OQ#75R9KfLE~3Ob?9%^$O3^ z-CnbLQ+bq3&n%7(;f;S|qGqZqS(o?H(95Mpe`vEk+TD6I?qy7=V;G~zq7#Tsd#9ar zYK$q>S_dHhSS_2JFHMBF1(&g;TDwEg`rL(ii4Qqlb9^*wx_X}?cykUnyZrDEyH`0r zALyQb(2i0K->WfcQ|a?xsc+#=4GS^ZTQRC}A9FBY+9*NY zA-TFuhQKJD^oIAAqPul{?0qpqzzKXkHtN8@PQ?2ZhQC^{` zal#Ke_K?FdTM5VwlPwVR@Us=**%}=#SyPxJ#8HWthZpi;9UVm0AsjM&((&{yukck6 z$}oYzr8GPY+(l)N_v?QCOvq9nFa#QE_F}(*XwqYOZJ;Sa zybU|(EB+yYd|+SVj{1Gn6=FV5Ki^z-Jdc8}a$B!+k;Vjtw;3GdMLGrn*k2bjJ2E@o zXWzG4g*&cj6O(i@z&&|++qOe#qBz6%|B>idzjcqF3#qD|p0}w9Bj*7H(-20kH`nJ` z8(hzyvzu-4GoCQ zTCM)YCxAFmaaQ+D4r!#g-MKxb!{&rDr_Uqq)f!X!rR!w+A$d0b_(5G#2ZEym0w zkEw)o{WTX=k~_K>@8=_ykB{!5>_wi>*Y?*oq+aT8{3I?-m96p%o*yG=(Iqi!v^de% z-Ocq$&r{DH!Wf9^NELeVluullH-dznP1-C872*5s$4t)q!VqqcbB=fXSIvL$9LoGQ zY-GQ9jv|LHhT-+^4_ji8n;nh`naB&iz3l(T=jg;R!(*BU$Q&F4OE-Wc^}34n8G$|V zE~LoZ+QU*-XO0g9Bl=HLyM3-2%H!?2TjvS442OKv4YvZ^5TiR0=#}d+)BXOX7;r3` zs;%J!C9Md>2xwd|3{=HtSg1KBtu`m+!IMU-@w^7-^lC_t+)TKX5iwm}<%&rJSUF0% zJ*pU7luZMIGS~p5t;sxC`F<|(oMbNgULV2}XnK=fTy#nQYz@+@hfEBV6B>v)qI}shqOdNS*3C*_=j3NuJip zV4rTNSRn&T@i1uR;+X2|$IW0!4q{cKL?Y?G*1y0BKXG3P3;aYG#b^`c zEzYk%XxWZ$iVd^|{LZBQCqRe&=|Ac#1W`v-)*4q@W*rPkz;1S5MabwHt%q-@i${Ib zrjKX~JSyd8JxhOOHa^;U(r4sOkUtXX|7#5;v5IWOuP|OBHm}uDc1ZWdFw_iLkG>Uz z&0;Izi4)R%tk_-St=Ys*n;U=8v)^DeON8Y-%1m{cDoMFsa2WlQdh7Zs_ccM=ItJIl z1$7l{8I6t-@OI?!LU^M7-R1K}J50$D?U0)6_H$puvuPWIDUo8$m16BlzC||>WkZV{ z@cl>&fmS0|4Go0uLIRUw)@?qerP_x?>^cRH{ zoP>S&p6(-2wGPIVq~u~WUvaj)Eg#c-&-bY5pGG= zk#3=84L>hU3N1CDkdNTVD`Z#&!(oKkt7_C zN39b%3r3pal(OrwHPOa`%Sp0^q3e}ql_CBdw*@FeXbX6{(t`w>Ns$+CLDGcH1js(* z0eS%QQ?r+qQ!ijsLO($=7?9!8T6_3Pvhy zsL0cRqNg^-d~fSNhTB z22F;-A?X3klp+$LTkd%vkmul6K@d+FMYhuWvY?~}*EL5;ys{@JJ4R0%1@|Z}voG7> z(FF3RV%#lj07Ye4eSGcJGf#hHz319#4HJot<>;o(xsu7}IHcnWOrk7pWLuB%-!SQ^ zyxt^qQ#{m@JNt&Yg>*q%s!gs+A8V%c*9TnjTpn0kfSF^JdsB|^RLXkYu=VKn`Z;@k zrPpC`R_4i}^DT9oXDH)1A%JH|Y9?6`O!t=vtL(0au^-fhk(jiI3a-tl9@ZJFXxpoWmr(BGp65pLhv2C=f~Gyqq)3<(t- z78t9@zf+1Xm&UZets2hxrPI!%09 zj!Kz2Vl~*O(aB}iFsN_21lAlNwEx zLEg@r%VXm(2e{dop9R3x@i(z$lAAkTc;D3lus!q;zQqidD^{v=a-M zL(4LB?lNgrKO+g3&rv$VoH1v(3Q(s)*L0)`;FtkoJk!71;Jytf!O4j~+#De|ttDN@ zSB?c|uvN;e6b{B;HL+;4B#AIIECnGS8BaG65WkL`es&GL(S)@hd$KMm1VksfM$*iS ziiM3~sFA;zN{)rJKpiAnv=33(lACRo$>aoWYDdR#_Db1xG(R5tW7s9eBxj!^yiGJc z9vVQ~O|IKVDjFvzUjypgpAkLC`K(TsB!s7`Wp$XV>%Vf{EwT;`2B(Klb_&LX2CE{d z4^;foK(FF%r6?gp!M|9_)>NQ`xvMxY4%viiNPMV}i}j3j0mFd?B`(@T2i0PcdD~U5 zoI!QjDG~J*g&ZYXd%09fHFH#PP0WTt1b^of#XUV`=pM6HgN?QiV~Od8<{$tdrI&|~ z8NRm&g849MEY{Z5FEGc8=kT;(cynG%|ExD@8RgfQt#Q^ya`0V$nynzW4nmJMc7~3_JM6$De zn$&@qth!zI(U(IjXAj@aYO7_MJ?wK(Pvm4fB zeGidMxA6_(Z5)xDxC{ToqRMP6n_uk;!n^SM4x8Bi4`c5XWJ|Pd?UrrZwyjmRU9}3U zY}>YN+qP}nw(VQ{#Et)9$9Xt!Idja2Idf!2X3sWy``Ysbu^Q(eqoU1S5Y`5P1<3G7 zYyLloobW#W5@Em~gQ|7^P^{jeo}ySSjarwU^-nU2u6pp$3* zoN4I!X7jxid}Emfi>;^FoH;PfcZZ7~oH_7JkuP{2D3vlS3#ZQ*sA#^Jbq9ZD@|6%@ zG0P`?&bq%H#^E7UU4DaS9*}&p|CtCuw0|t?jup0-C?S~i|AmhO>fROsw4h45 z1X!Q~l)f;%|LZ$MLd5fU2l6O(RwNCK;;Yb#B3%nC%n6^1kqdFJktiyj~p^p^$ z&-ZYKz(fiUH4=^B^pFm1O1=cAh%SJLf**m-e=J>zOXoHtPu1Kc)R1YiqL6o-K0puy z9TPMwPa?xKRMH*eQ$4?3?=p8i(f~Y&KkL*55a_ZanvZ!vkO947z$CU${9W8Qn5&3; zTsGK9L~&TC_RT$ZogyEGmC{)_9`jJ86~Qgo&)vN#s(h@r&`GRmWk69!{V9j3&b-r; z>o3OO>tA!u1Lm@*q^2^)XkoM)Y~ktb1$hz25wJ zJ?3pH25BSK)8Rf~fbu}+bxm8nWhiJthcuMf9%=F)k3)lC@V$QkY893su#^YL zMOvJo{R`K>X$}d^^AKshgh;naVBEQIm8tdCMhJ+Ps4GD7NPkMC>Kj!gIci{z0DU!A zD;fK*Asm(2zJS3PB=64xQ^!-~;_KBW@AWS=B+J^z!5FNTr_GXAf0sy7MgyvsXdu+f23A+I@5}@q=90oCGP6 zx%4|>IaR0qFxdI|GPixEt%0<&C;6K_UXlAP@lX7E8@NMQ1yJFC{h(53lRfPrKYiJt z2Vn}FuloZ&?ViM{*6L~GwR%ht-t` zo$^X)u!b$-y~~B#9*G-N>78Fm&a7{yu+osI1OgDm`us0)IY5{brsR{3xJpNoEt1vd z!E}w9T26K)&Er@|sp{y@DwmFJr2LGCtQ-?*cK0c?)4iusn{9!rJF5=V{vI#y@w@FW zXuPKC!^T1~yJOWB4fV>HwzD^n4C`7{L;m&?MMDNes!B+}Dk1Vxuz{SleJg#~;! zHGvP$K1#ilU4OE6OX$u4Ku;-5vgOs*RH(c9`lH~hFaq*dA{JBCiNNkP`*&C^N7GAm z!eTD%)2q?|ORVQr)?Dcb851Kxr^2xv@@!FAdnVypWw$hXX+eGf0A0#6c}XRKtTl=? z<(3|nj%nJj1Ip{SGm%5O)4LCsY+KU8-Hkm|>ygbPH&=RfVbJ zL0RNn;wV!n8C&=+>k&!NDIh8nt$1o71MwRho9bK1ZpkFL6&8t|$=F>$oV)2~4r=>R zh$#`uFYbR=@tc`0_D{pn)3#_ypLC>ApORzY+VUj|Iz-O&WZhvmhTrgZkL3$f1Qvhd zB6tnwL6WdZ#J#0mjoBF`fqlkI*S6Q+SB|T6?M*J+i2P-kudHJr1|?*zjMerd5@;$m ziAeNbcGayv4CJ-oKXbv0G zUa&vRuK12T-$t4}h8%>PR|k(9KttKrk7p>xF*X zg;?tHn!Pbm5-Gghy&|uq&$-Wo4P?VvOkXfvi<^6(caFq=iZ*{_j-;Qt!-H0)tOva^ zIZIFYBUx_}DUvAh{msX!VQGiubrFkx-NoJOZt6g_&|frR?;5Ex0{WV^JU35IQ_5h$+1LOM@?Z|JgT$rjbb}R5SgaUqR*l-hR#Z5s5 z$8wOs51gg1&PeF+mdj&z`8A#|_!v41=t^F=AT7HyP+lq;KW=ht~PBi`y z?DEeL6LH%=Gim9CcLkA(YY*kS*n0FJ#hJDF<4q@5c#~ql;Ae&#($z~g%`s!h9QM`5 z)FvX_aOL+8ld9I~aJLd&_=|PhAJ!y)3|lU0vD*y&Ka48Jr<#+hjWjmkaYtw} z_g1Z$1Ovuy;+Gl{bf=fS6qs4ObUON_C7Rp&mG&q87~V9L*w|j1L?zG>E_XJ+&7*$} z1CrmC)`L%Z##;4^1^`ymDA`CWx2zstBQ!^cQC{z)_i|!uAEP;7kW|aKY*zikt%4UFWKzamhI1~ETJ!{G?Ijf1#9=4221(Znk%ehd*Nz}!<+q|fluLQU z;4*G)%~aG&P4!cKkkFjvZ0n4ZaLq*cB6w{R*JxYhk4yUr1V$ zyl*S>gV(;xP|!`au4DAm_S&W(v&R_SeI~ntvs5S8__PKmi#w&iF5E&qaUPW`VE8b= zzRPY7rzD`$<<3aSVZ&m~?vGnq9ixJitqJuw*k#GD_Yg9)b(iVIWiI<1K~X(OKQM07 znTRqd_LaW}&xVI3j$o!`e`#*QKx-hl(Zt?vylpoP3-XST?R)hG6qp zHIl{(#ap8`u&*@~`XFU9GR|aJ{d56PjZ|wyhf@(+cMlA)B2EP4Uq9%bO`p=~_e;32 z`?4#js(BgC@(@w9%KujT%-0b%{@rFpviz=jlaXTUs8AwCEU0Ywvc)1E1G>Yew!<4&S3adrE`UG0n#AtL-O2I*>1W9wHsv{Z{4r)nWJs2d zW@55}pK`)K|8`8_vk8Uy)RQkLddP~()Ms50Yr^Is9~60UsZL0b*rOXHZMgo)=kO($ zjQ3)F7E4C7<~b7vd?H0;^wHzGL(I?r?<|swyV-HrNfpLQ&7t~S;*ZmS^|sMq(a;+X z%8t{}#Q@aI9k(ak5`URT*thPig$m1_cHUIH?eKFR2+p7EwyD1(EaPR!bC1gq(y|4~ zp*tAms;Pu#RRaxisF_~1Lm1In?_g1 z2Mj&4BJU?WV`12SM*XFjH+Ob-cF2o5Z{J3ozl=^_;@Pmu;SGL?c=#Tmp8v1%CTI_& z2)IeyIRghDLK^fk2(S+^8?LIlW?aC>_*F(q= zTMd~xc&WtH?`cOQJ86Y-UW1_CVLoybs_*Hnj_>xAQ+WHrzf&=z{NIH4|05HGiS56H zcigq`!~f){?49&dwZp3eaZgB(1entXk&dXWrOTzoeHgYmRvh9H(_ z1<0lECN2<*V?YwzbUOVJNrk0>+HZnpKm(%T5`-mykscV|Oh+u%21sQWh;$*AmIQAA z1#kC<`xj{63&D^p&v6AW5WDeuyUxkV+cRVm+v#u+?)8ZcQZ=S1D?!!>eGFk1h^0^A zgn0PpE82EXSvf2AJ9GzPNgGRq1X*7d#4HSec(M&q3dt!DtgMqr5>xNCAnZPKzQFzf zhqwGcsyiseE&nwBN+iR{BBHYaE5x73RdO!kMcW-EZ=y+Rx52i$U`WO=t9XS#JX>-F z(OA@T@ngi}N?r(U_Nok3JGK!unz5D7165!o&V$`IF!!ojf$-46cV5ud;vqf1Vn`PGBr5)2Q5$3@Tz zjz7@GUZI{)7N8G6jUPu4G_(i9;WRsm6H=;{vk?7LL~4nz*1OWfKsLn z4X28W7g33(k$(`dmRnb6dB{;ClQNEim88EZRka?0iTE4PwRR%k#fy@BKkGk^9eZ(Eqcg1Xu93=IX?oJgE>paeoM)I%gC-5 z;&+k~J@$R8`}+(5>z-yX{6b{bE_w%j3TdL<6w4Ba94 zl$k**aOxoDO_v1&xV>6wlo*U~n}5(IMOlSsde$3@0XIq1_k#NXa&p-SrWr0{JL8|e zSLPym&}+o0i7c+d&cR8PKnZE$9}x^HAkm1;?Hd2$>iY&KO?r5=JdK}+gF z-9pF6uo|DFH!{Dz_j9ke554PlaqnK~tw2GP0$dO5W-)2BimK0o9@j9&C8gp#lQ!kO zCfpA6rsOmim$Y=8rNpDddxprmZ8*5)p;Qs*x1AVhAH|h($v#a@uDfOgubG_=9es_3 zG!`%C>N*q2BHzHl2XS!vTRjc*s|T%9867C;468^rq}C>nC191?@s8kEx7c=(px~Cf za2DrXOeHDht~ySOliloEx=xG#26}S@S-Wk=K8-xvi}R(6WcDhp@v3C=jO|At2;GNWlqHwM*NtmktP9bO7cHcTxve)Sc zq|c`1usUGyj=+!a);)}Dr8QbY^e%?Z0T;oG059|23soz zITAme47B|~7xD9tY&z{r6jNZm_d@Tc>|#)&LH8GjND)MFwVR0qBj=3%D9U)oUL)dH zBjd*(HiT@v5pP%OW|yg^XDZTK7Vjs9PkK>hMR4n7ZgtaGXja6!#)&7lneJRx`VJ=q zsy4F!i{vSt%5)s@L(7ofXH1b&mpM8fm=9^rF$ZCPsoH0TooB8au24dodyW&a*2}$; zkyJNt8Jy61@+nG;o3*h<%PS+Pq7wF4U$~>nA7iI5Y4CjBra}KUbRJ%Y1r7FO37!X$ zZLSx_@gTW%2;@j}78xCe6GP3W*A_dr7CSpGjC<1;H9g^CQMx?iTK)S{)hc^lt+mHh zXaZ-V+x*VCB0@UIB!lAOqc62yzUFOxd^DxI+SE*;V-AI1dnvZ(5CPmW7bSQeWk|^l z;;Cpu!cwjSuy)KcFWgn_E^hK7z5~}5aP5PfEfZYpHW7aJ>qqYQm}8SpM5h+FGy`Vc znj@-vY2#K4$HzWhRx&8^ln9v^Fo#H{8T|@_2MEIS0LCzc?rFziDmofTYV zm9=>rIQwBgg0y=td7CIxm!$FwX6^VLE@a>hxNoJ2CLTm7L?{V%3z+wzYQJPG_fX4K@;Ym z`u*Uk+PGJBcb+KHe_#}&g|S)!8)N!{W#t;$ZG??i-EM)6m)~}yLj?0?3Pb(-b&l$d zV6oNJ2H2R@Yu5E!tai956pwbe<~=X!>V)-M)mFGRw2?LT^FcF)hOIZnhYyv-J}~GSv}pFRX1~CDrJ%^D-`z{eeN1M+LH5@>jx`)Qquz6h z-%>iGYklqZ#-BuaK%_UVPqJ#-;-YRB@OZSesik%HO}auiZcOtlc7&Kmc`S8$um7&k z=#jQgq$1B+Xrb_Wj_+4pe1*BaS3)>1;ddR9V{$xZu0Rfq{mGHIXYMK9((Ky5 zDDtac6gj-+g)B6>J8a=iPj>g`>=&GjBmI9+QEv$#1pnNvd!0=6L#Kx8hQiw=g>9g)si2$bZ)s z{eDs8vugqgUcRz9vJC%&q$|X_*~?&aznT({R|TYe-Su1qhFt|O&XGmD9)ie zHAvwL$b~)oMbb$f6m(&*C{#5%I)|YMd}Q11IVt|4$o+SO|AQhYNih9Ik%s~xPkopr zN-T!`2SrX{l>3V!AAeYn9G9`0EWJNL{-Trj{i*v}HOY6Yp%-ldd}Xl5@k%$z*L2JN z7?jH=l~J6wV(3atSS7&sPj|VlY?a^)!!SB6{RY#NCZ5<+if_!M z%aKDTWAsW{pI#(yV#3K>KVXqDZ$V~X?2ohAhf44*9g+4g><%I-(Me4b>U2`vKU|V4 z`SFkW0qP-r%%rD$e7&R6hZm4TRxP^0Uh6^&NDo6*RKNF<1yi?r9f4W zVn&iy!U7$ylhnu^*U>QYbug2*5in(w0X?h{@CxHL)2{i&k$|G z`L96KT!J#oksAeGi-5?J9G(a_GjP`$jJG3p!|T;?pvC^XQR&P~W>S1+TLaw#45fOj zFTRu&mNH^?WQc1Z$&o7rhbIHP;Nzf@mL+Ku z^R$kcvu`9ZxcYYM>i$Ey2jZJ8&R^{D2`hMoaM33?;Ct#@fBSRLvJeBb`>eZl@Y8G3 z=PC$binev&)9y&jz#e&I?YVF5xoh3JH#{<;RMY;@Gq~#r>q*OS*bmAqc=BCN+QQ)P z(v>-3nduw>0z+<8P2HiwGWJzepXo_JTT1{15179K^yzl$C0mIhx~4NTGDo9Uu3}$_ zskvirb-Al*oy58;xf|5A%B9nOI5Hj#)mzUn{jSf9AyC+DswHXq6!oVVPEr1K2ll6; zff{Ylh!VJy+lf&5NC?M=E(dH4Xc>oWBN@ArqZO@Fh|YB`soHzry0&|2 zAN=Hz2Y*W5P~;)XIIec#h|>mc#8Rzub&X;OZrXVWYoNc9N*?~6)eg+RzeAHpsp8{N zdT1T)0=QCVO*z`C?@gdXM9Ml6b-J$)XoetzgeG^@P=8)6BNe6=D2O#B=iME{-Jefz z4!p8rfk`9qt^@w9!|d*HWe3%{2%Z8pWL+!@YPIqr50!{+ICNT9%|X-hFYf1lu#vTP z>05r#q2{MDrErx{a2{u3V`=X=JBGUg#y*%2FM}gn9d>Zu1IUh^DKX4cUZDu6PA412 zKdiztJ_VZ%A)*dskdxXOOmZOq%b>hIk8?Mn^QL{4a?yi|sv`$w*phxUv!|%pIJ({lYvkWMh6YUGrZ2 zICvRpNF8*RJM2b&Zh0u}fR-qy+f%|JQoiP;{mo5h;q!Hm;2K^u!-14*X&Y+Ll3X9b z@t~5%d7TWBbkrtgARZ8s$7WaI@u-v5I=Tds$8&I^5-de(0r91!&WVG~7V=0*%|N6l z&rAOCd#w!tFA?z?6!ektZg}m>RJV%^`htsW!wtO_!Px?iL%kq0DX{T>vFo{k!~$Xb z1Kq|q3Pj}>_(rthI%KnzlO8yDC>j{L+vcgP_UyrHr3k-C|7Hj{cX-T|vz$v`($kn2 zThrX5h^%NC5&pgXxBIp4AhM#9WF39c+oU*j@5X&Ud;bQNb(sIU8S%Uriq;&MzLl7d z{P(I7xylfdVM%gMQ(LyAUA0PRJF*>!qDdkSt;O#22j9MclApqHAz#f2TWpx!O6amN zeqxLc>Im)mq586_!K8830Pivn);Vvzb3>2T-0))9J4mCN17?KsA@tK+6NH#NgIT|x zgYK~5V$(r3c%W05EV67)#o1+JcxG%qW2Yx{Ct=Dp|8(7cp%GIqeOWbQp72W#O%+_v0UKUwQ@IH;=pT`Z(VJkl*@J598}D!N3y zH?yFB#rcYRpB+%?hw`IuVzLYPEl^leVur2fQ#oahfmRnWhx&g`({zDWi7!u(7D!2>;!p@lrcV&(jI>-#c-z*nEQg2K=f*5STf(R%cpLe9OmEBmHf z&~GGc2X!h2Bu-5ySD~7PCNM`XC~Cb_+OuF`;N4@)ko>HH{B$X`92fL-9Lh4H^H6CW_sD;SLul z)a)W6*>i?l?2u#yKjsPF&lX?JOy52858rJVP;w@b$AbiWz1_6{4D4)}g#&jq0W7NX zsTO-Ccm@Y45I*zD0PHK3ADlRc#l9MM)!122?Y}F3kTFofR8l+4xNA+3>fCC5!j>9w zteALP3qPMZiW3yKwkii^MB}QpbIQs@VNsRlL1(c&Tu0xY@BayRGZ5r>Suda>cFVj1 zjipr32Ftusm3U}G#hPAQK^@>Jq=_F{qg9PBgyY`18$E@3z?GuDde6l?K+V~ZBTi++ z%SEt|oL#F%EP-MuUk2~K-k0a;LpO-kXH$C{#mu5k*eS21T-cUVVgVk}QOmq)>$kG1 zH#zj`IXJi)PR6c)FJ&WjFf{53$A)` zT~e9Oie5TOG(uF_c5x#sfu~NCm=kqA8MM;pTT2ZtzkK24t=JbFzm=$;PG0?-lDjpLMGp+qe$!9V{Dj3eZN#*$ zgdkT|n`ci*1TD3=Mt0q}jQ>t(kUyE`T)ID5vyQ?nNt7B2%Zjuom1%t3f3_yM(OIBt zcuRA`4lY$^JGq{=;tk^u3XI#T^!Tv(E|+);!^4ny{;{*KFxz1~lX`XHur)*!iRZtZ zli;gCoYG_M{zutwe?aEt82Z)|gvfi9Hv;s{lMN9_}uj!;A1zkngC{D>b$=pvYcH0b+C?;qhJ6N#3UQ~B9@vnD(wLMqQ1poAp57aLa>~Cq^D$?^Qv8)S ziNQm*vzjiNtqi#HN-lyInWLSx30k_r|VEl2_+Y%K`@?GKG9`DZXKN>E&KRh$4 z{Wwc7`1--tpVULop9pI2*%&=91rEqE6JgJPB3W`IsC;0Tm6R4vX^mcgy4xWs{1apNsB@oOJp1#LzSHuDfMi7oPTsduqbNpD8PU2UFyJ2@Fe1>FNOzQ16uY%mO`o8=+ar>HQKk=YqMPQlw| zzC&wmG7ykioWQkUpsUhVFl2#j0I^!EdEk$!IsGqH+I&8DIUbzq+%cSFh;zG z<8bfhiocZM;@LMc@X40Yz@%(D?N(|_uZn+yQiA9^#;*i0ggG`1&S{2(jW)P}j`6jI zmI3%8>Lx|$8W$g>X?wbKj~%l>rx(%DXB(ACY7GGdz_718A3!BGOOsA0_&djit^GKy zTMpJH<+LRCRrZ0*^7HJNbT#7F8@-T z-GavpBENQhHh{vyw+NeM0NhbVk-v-^+_CDJ(CNY3;d@v#c^s#mDbW7FEYa;Wlo~E~2)Q5>yt7uRx0!k&Y;(}iwnWjhVEdc}90|Z1 zzLS2=tLK;)r;$^E#2m=__<&)xSo-9Oe&W}X$thPmP+tZ%jaXc>;$|H>SVmnnn{4?z zo^P&iX8|opx!03FrPl}eyT)St@b_n;9&oK@3tpe@Z7XX9@)v@|WBgnp{GqkBaI!v+4cDqgo zcQDJ+Kn@}eE{L9q;e=eC9b%lI91dRuY5?{+S#aCQwsyQx&O67yUBirKO8C?Be_*lK zM451$mUrtWp}3#vnpyz0>eJu6%yqfbac~h2484MXB0l^xwoB#e_aw4F-Dwq(oA4R! z6d6AlrDUQr)3DIMdaNyPkRd#5D!to7Ydk%$k)CPrn%>qP?`QsEZ_Q0yz z)fO*doe-qRu$0l)-*Q~1GaRP}(}FpWg1Hyc_SQka*U=4je|5w3x0F@zOb5`H#}8D2 z`cV)MX1C87Hl6XG58V51_>>yF<)vRVPwT%26?nXhJF>V?#gI-@15bPP-C(tWC&OUs z=iLuR?F4zg+P!o@Pgd;{n3eTc!&(7=4;@%Fj{zrTigHHfMd!+V*J-zMZChfGiYmkb zo>gv47=I2X2z(u=qB(S2uvFGQ0u%&d&HeCY1Gr>VNhN0U;%dbP9E(If%2Zu4C++Jp z4>*TFFILnR22=(7m)zoi@ex`X;cm+?N{B@kjq1p>?k6eM2E2GH%9TsJA)Mo#>ET+1 zLUWvWi^|gWb{0yEGwIe;`;Jjm-ig&~&?a}Rtt8m?NuR>Bf0yp5*Q79mF!-K~PH ze^>*g(IQ16oc08ZA$tsXhnohv2UKrYrJS;%kB+@WIb=M4W@J_XB+K|#bx6{W-z^3q z>k1qr4hRvv#jmQQdk`bui+k0!pVswxTC`vPUSnT44~VoHY*Q7n$#Q8hB%)1_H3K;m zz|dcW`o)^Iz+OG|T?B~1*M4PheS<9HRK}9Tu7rB?YB5>RK<}9-EwAn&HU zVh}sZZr8~gM1GbrD$B>zRlG-Tlr~Xsmyu?|3x{2Y$JnIANS57rMD3eK zf`|#-FMTN;icQdk5RU-vIlyEjN_=vy&L{iU!#;y-2r7dMcR7DgoHgdO6L&8Gru%fdvDDBAJel6E;W|To z^+psu=qCI`d?RQmG35p?jN<0&e!$Q zBI=L1NMReq9V8zbWJ9!Rl9B8}(Gz4g9U1=SmY6w*IJSsF6}@Ta5vu}eO^Yj^}|Y;we`veP8ooHl4gvo-?n(yN)rdZ zf07~!^M1#B?Sg*7a?e5tk9`i|ntC+TaT3kP*!ADn5#{jjpKXswUwt%868d4wrgV;m`)QnJdWW$<_AVcj zj>xQ!a9n+9!y_rKEYfh#^7H;7k0yOoEfWmEmJvTWR8DP8#O}QyD$g$vmjE5>9WEwvk^~b;x!R107xS5pj?TJ!7pV+L4KrlCpDyFx?Dg1}G1tDMNE zXi9<5BQH*Yz<2)aJXq5Q%Nl68L`6AByMJ0@LFr(YSN2Z3tLS{WXmRmaGQf!pQW6BGX-s`M|K$*;&vSk8y#)x zmBgueQ4oXk6zQ+2{*Rgd)Qf5nD9HWe$NmUy8OUwNrIb2Rt>^v%L?!8y1W2&tQnh0( zh#c9^COc%9;7w#dV8mM;4*qHO$KgxEo5sEy(9zAFY1oO~hENf#^*X`ncHbOQjUCPH z`Cl(CgqYoC@`)L)^)co^znPM=!us8M_@DT3K&CD}88ccyKb{MtxdFnmuLJ z!77#_elCK=GG}kL!$Z0s8t$<%p&LW72=onWOR?Yr*2}h;dLMfDbyxO7>&|zUOs9WR zTq44hZ(bs&0Fb5ZU4^7;W(2-xLw_AXv1JWOSPN|efGCRjIbuS)UXDK9nUlk9ChrW< zTht$cf{`O+Yduodxz!(eqw$@F18#&77iMZ2F3jX=fcC-~k+}B98hD4wTqtZf3q-|L zuf*}P^#|r=a>k%tic>~I8?=t3OvZCRQVrD(Alf3k0Oe3luxDJ;NhNEGG}jgtLp%=H zo!>z&U2{BTj>uhx&>_|z`O<-6$|h?MLqON_DrsBP$IoyhlFZ+h2o9>hb8Cz}N2Uw= zt2j4S6Z~led0v)?cP0WvHH3Gqzas_<*x9qX`SHJ=pcewXv$l5NR-P?h5~mVU*8ix|7=U*bRKsl zdGM5prPs!{3Z>5bMpZHpU7qNoOB4BkusJ*-aFg|zVbBjr_mX*C#euxMnFkPlL^rE> zzAUxk&guFG=6k@&M|Jlm-t(hnn}zEx=~kn^vwiEVO>lDUpoNP3$sLwQzj|J(Nu3Ok@#2 zN>|qkgdJm_O!UM9e{3J9?=_A~>WNQp*d;XqcxZE51$f`fuFAJl5CSIC)w;av$F(`u z#Vz3&6mMh|$gQcfhO9z}wvWJ2sJEPbP=9_q?QK2||CCyPnu^&WBgG^9+Q&(RWeO0! zzCMmx@Z0(GB0!8aM=CK(`g%s?`R%Pd|Cv{w@_BWAK1N*f{V@NzkSS5u5ci!e8{6oV z{s*zcZ27%C(@;6v%|BzIJqyM$W91HEl~=>)kfG;k=HZ6iZIayV7sA%o*tL|X`UB2Q zB5d`)s0wWVM`t|?JKKM$3K|oKzcJ4~BK-7&b6x`KsmwDPVE4e?Q+@YSg7rW6&4yLYs|m-aGy1_Ih% z?JYmc%D;}0;3t9TtW0r-4NAQW}LX{)WvBUtILuF#(bz`O}9gn^;A zJgh)e0lWx(MuIpF5x^e~F(b(e$=;}tFse)c_}3TUOX;I*2B4u~u1TRcFw|p^S%B3+ zO;XMmR6=pQX>RXjuz9oqAc;6TED6xCwd=sm7m%%BGv5+uG9sfj|5Y><9{Ax-m#kFu z^Ub|&A@r3_C2N&jrJjXtX*iT*XgDTw~PMJV1hFP_+P}K&D6g zJCkP9f?K0C&e?cRG(G571>fwCTFiB}Hr@l|YPbbR*J_P3AL<8I{@@0N&u#MqU!Cs- zcGTYm=)`n=MTF(~>}J0OAe2Pu$R7_5K&pydRXEBD&DD%vRP+e-%yULw6QhN}l(AwA z4_voI*_tX2FfH%`_uJWE7eIT))pPiGUa$J?Dww|F?#)DTN%VldKXNwS0d&Xawqx>z zd2peDXW-VPj*|g!zrKd?3?v192^Il{=Qjbv+bJq%?l%VGgX3er4JB1s7l%=?A<0<% za|q9xGAd@SK5A#~FTpmbTmVxbu>2=$h}D?o^z%Pe}Qmo5pw--Ljb>xrOA0ZJLJ7yy)g<_lS?>w7Ec zdJLt!?Ro&T3Ae5xUfgO=KtLB&V(KvE=OVZ>cW;jvKIZ2}O7Y6-+h@l;`%kj%pR;Xl z4F{U90A*m0m=wh{xK(Q*3xGU`RC3}b#9|`50XQNK=9@bqZfh^;D8`sTZd`zg-fn0- z@U^;`wX$q#z=kgv-7oyb(R|U=jdU_quTKPNn$#2l-=5P+;3!Tszh5Y^G#B|=nVcxf zLKAiIePwvlg+cR;8iP2JT^v|oG$2J_GHl1Y{qvRa72MXD=2Kxsu15%p_xM&I8=IHY zP;T~l;bcPYw!gzihyM#a5_NHy({}9g{cK0gHCN->Z;}_G4V>$xHyWPLvmIvN{evC> z!w2{;HVCGR7)-+IZx1=Rb>9PQykBuN?u_3Cu{!fD=*+gssRET)>5qMcGo0-)7`+LE z{dN|7UyQ9Cf26Z+@&!~Ir{}1}H(xq#-m>-Yce0t^2Xj#JeTa1K-`QxPH9Y#`ACw&bsNwb@=P4n{$nZ|iF$e}Ux zQKbu~9{h_{3XZuq!~G&eExZ2YtB)P?5U6d&M6AqqJ`mBQ1Cdc%VJ=5>?jHTC(tuWK zA@0ydI=F^&qN4CBBY9acZhDR%?gTgyHkkwbJsm$be*>-km=D^PWyB{2r%SpR`-REQ za(>25xXrYR=n^nC?Go?xRBqad@%hCUY}sr`>I)$_?m6+K%#F8a#&(j{jb!q+k=9N3 z-q{u??t{zDLzQ<`bfd%o-J``v?z1uh{zVD^8jJJ-ivQq8oYvWo5mcAkBF45@E}%%C12G)~8{s)sZ9a z5g%1vZCtz46#78%qUoFU<093!g8Um&txw@*Y#+~8JpceOO z(wuvBDXNMZ^|-ETPNscoZ4D=@%7H3^6*3q_f2L$AFrip%+mJ*XPqfUV)9stk9LVr) z&x#^xYmLoeUQFrahRcTbe0y6Ojt;fPot(a^$0ILEr@EM7zyAbUYzo6C z55wV4ZjLrH^MkTnOl>#5BDuLNxlfJpriPMxm=i`ee!1i_&C|!TTpO+E@UeM-eo$$o zXj1}Xi4l>a&QgfwY2F?cx^ma2JYldWmCXt*7m2%Hr|7qlmc$)E}f|xe^SxM}7twjrEW!MzUqmZp65MAXB zl5g{Rv;>G}qPYt)cUMYlGY3f0c}L$4RjHjt$^s&YGS2S&GECaeor%23UnU;6#qhrI z4Ub9Wh)vznO7cuGM-!riER8to!^f65q6o49PkvJBP1}#iU0V#kVxNq&OLy-3bLW5u27ppNYR z?rxEv?LCy<9+ox;qHMc7WpDNW>{25>Ha_>2m@%*8dQa78%#yxWk>2FVcl?l0)wX6Qokbn~xPLy5n**Epr@> ztC4A!6psJ=5hAno7>p+A54BHeyJo!{gOv|8b(A`SreK-x*1tC=Rna8HJgr_$s&(sZW*Bm8GI1=i=vGYEB^BmSxdADVB9y{>r}tmP#9q z6#0KZ>bMktkF}im_gG7#U*ElMNyw{R)<|BSp0M{Mt~U^9|6%36wz>4@&@S~dL}mI2 zSNnMwqhrbE@>0IW4}%|KwXOs*TmZI-LUn^K$Mf!fhPnpEAu(>~Vn`fivo5W6=mSUa ztbSZ;6N4Jho7hzj0DM~RXXP^xn!7ank>8>qXnoUyxp20K6Nv8Gf8#g61x5O(x7$^4 z#l?j?z*ds7wpv!A>L1)|16v5v&Ji79-EW=*dUn81D{OU7Q7A-=!~YQ3DNaWlmA!rb z!9t%I5bv}Q;2C1YX?qlR#9G0+mDM6ZSKVM8++#_Kbl7CaNBUMBrOHlWV-ck zR?4(^S%7$Hzfz#FE`L#Ac(6NiSndP2r8s(9Piu0GZf#Z7up|{lnO2&{#5eO0HK9FUpD+eX;$tcAUZGPQRD#yD5q;u*_FZ|vXlAbG`y-xIcs$t&zYj&O zoEqVQ6A8Dm64atn+fpWYs_t;oDSBlZ%p=~C3=!&< zK3Um-Lx@aW?VK|Cg4rNSq#jQafvQg_=!8gEMmb^3W{Fmunb1q^j_+dahNZfGw0%ib?4i%2 z2Q>Hyi*qTMNq~+#N{noJ{N~lekIa7kAtLAF7$__4ewK8!d>jCl{q|sh{q}Yx3sis0 zJ3{Gh0dP}6yP=-e}i4ak(zH1ua0o}5`A4rARkzHSa%DF ze1N8)$JA(G0)F)a@@NowM9*UZ-FQzCzVIwy^6He}Nr8BBK0PvP z6@$sPt7Bi9BG`2nph}zWD_h4}Y+cW-t5I^z>-t!W#j%~IT}-bQ%zo#tws+oY9i);? z&HAEjoVoV2LQEy7d0a|Fl7^pf+bA5}Tb`)EksBy?StygOb+4!=W`Z*)*hJC${gH=apa{2th61{uV8gN;EZ7zyf)Tuprw@#oKpCA=$?m> zC}WA6Qu40~@2||>Kj7cJZgGjbidWUux(Tu{G(1emS3)vFV^6Q+wm24Ela!5Bgxlg>54Xs9C+R6y54nigpMPdeoAY1433A0JvX7%)v)>vzd7hO67 zt#$G&pW2H`2JI%Hl0#>pHs`9QvZ%vmJhkNKlseL?AC0D=4Ii00TWk3S;>v!nDmQW5 zTsYN9)~Zi+{^v#W@+9R8FAjg@q<#4(*r|VjAzgeuOm{jgW1y7|uO2ieSpyMp!K%fO zYRGW!6JBn#kouy9BWmc$gzmN0nlvKz+VykCMwHQ29dFJ)#~yf2yw#ax&b-yQZ&1dy z9VZ<|&ASk|RN1MkCW8Op(-vW5kt9(|dts=SESh)ae#6mO@6egG2Z)&J@t09t88Mc< z08s*;Ba$P2ytuwHjr<$%OHE>U7{rql$>?|(?rZl*{F5d=V#NICW3q@y;PJmB!?HoD z7mZwT{dsExEvU?~M%dpxShQFvl6qOeP- z*Nf7GXrHRz76b$CA)!0?(BW56MVOF1)R{})+FCfpX9K<>kFi=7$-PJ{7?AIh^8_T? z1grWx#sTWkhetpSV0I#C;VlF?vyVifi}udfulrv(@KCBo2Z9RF1uMozr`u{cykQLD zx;bwfj-7KtSgeW8XT2qbh0Bq}Y}q%D*N+k251I&C1@gH$eg&qlc4Onoc1ILNK!p6p zn&B(D!}SN)=LuZe%qv>^3Pxdv?WDO|=EXe@;bPwh^tu(ITlUivKY&pB0yFQQ>ajz5J7D$<_PKF`|NQbooNw(g;}#IT~>mP88S85ZgLa>P|RA zG2$)?#X@sIgflskMW0FvIeAxm;&PqucdvWTudkNR?Y|=b=8cxVx$igfYr9ozSok)x z1iKWeX>)bdP!jKW5e{6)IVuKWTXZZAdso8gbuy9^IEQ;{r#~Iy6BcwVl)21F5_@UB z8O5D-vjm%Uu$dvgEhpOnZ$KL2d=xS_R$9VldiF#yI1uB=MydL7GEREFpPY2(8m$XLAQ>o#aXeDRE7d z+t$p)D7S%a*sGn4*on$0NJRB-6LpKFt9UHknb;dUadc3ug!=lX8- zF&Ngzn)j2lYOR=yfW#7Y`pBQDYyhfKPyr>XqFy=pikio#r@5m2a|waNPu__J;dl^= zQBc620%21k2e59L@YHhauG!8bw3%%@&=d#4J1QSMdfTQx2S_P|{cIelcrzOYCsW*s zI~saB=K>m67(cMdBPo;}z-L*}lu>Fk$Gm(7Raw(zBQrck>{ZYD*R2-zZ z{lO|ZV-pmsP+v2H{Kc! zLv#v|dJ+zUb&A^atNRei7|b-6w?o7_+3Lv^OIAVjiV^9=E#`9`Ua&p+drJv0do{Rp z>m*w5qxUM$dX6g)(G;F;M)DF1dqnv|(u;=v0_^n<(7!4UY97MwTT2ZMj%p3Zl8doi z(@!uqf$NP%MXKTU7vkk^@{FTAKa+WY2^r(7%h}52E=Hm?X-f7h8x+40^)WgV9ZnqO zH=Adv@i7BQEBbHbrOUadgwB?~Fxn5rq}fp`I|rFH3RCQmv7OU_Bd}sSPu5Q+07^X< z;<<#j6iKw@r=sCZuETijCEaw=b7_4oY^-#y8Z~NjdbDi@m$zL#2n}i6)MA~hbY-~n zd|yw#-*)K8k3RRliyiwwGo|}JWlwJ>OKyFWzkGWP=kqiO(&b}0DrP%hF52eQI*|H1IY?+J(FwS61-VA*nqT#-)0hEeR~;hmXAuI_Y-#h54!paru2mgD0D~<%yeE+r&Z6*V)(>Zw7 zTBiny;@MD}m|R}s{8QDy$&dVZ);=mxX z;vmb~wSqSIaq=*Jj?h4kc*u^MKKIS_n7Q}lN$==rrPt_eimP@XVbXJ)|r z9z4`zQ5ZE@m}P2lhh{I^CRp7wvYrYjga`~bK9w0Sc94Q!Q&*PF5p&!tZ5fAyT!h(s z1J`%|IKD|0?a&|la5O$*7!l&ch;Aku@iD0XyukYwuO|!63B0##KtjCsfV;!Y-p35Z zN)-8^vwHMXC~9NEm~0($xrY7V%4plf1B99Jv$`LJOM%CB`5>DjmZV>W+xp~Wway*a zPECqwj6MM6p(c6;QkAn?{rtO@g)j;6xardguPZy4Sgc;y=A8sbVXRj!wt55;v)*Bf zf0Q@>Pc6sz!BjTChXB3sfO3i}$6rAJfU8PZRdeI@D{%sVapYruFc1KSA?_EX*s&dj z-~7Q5HVUSSD*_{BOtP|HpT^;p?Q6oLyUzn$+5_|vPC`G#1H9(;V73U^Djnhn^3ZyN zkjm8$+G$OOE^Hs6WBF~%X5k-1NQ00XIGk>X#W@>bBC`ii%Z78vktN*ajYBLi*oA5} zrZbXHs%Vg*FN29F<^HKk(8E8(uvqB(ejmYN+pDQidp!i1)|X!Wf-_byf8SPas5l#A z|2&XvKP3JN`I6rsedjseuI+d?<3^3waLTR$`iI^>kcI?K(k68_)UrLT)HoCbE+N1w zcNL*yH?(C{$)R4g*>t_}ZoGHkKjXANt_#4pZ$;CL! z_u8fB?NzTF9~=x2BZv*GWZ)anO(fT@QZf;Yao@)MzCBK@KuZdE!#?QcsEr_IFHkLt zDAcE@elmrB4L6}sX3&>zXo|{LBgj)`%r_hW{w8N=LR#x`*GP);Fi4+N=N(d@|IKMC z!5w%p$ywV2RB6pZ#XDpIkTx1I@ed4z`p9t)p8^dL642dc*aM6A)%_a|cBoIX#!y zwT2Xi!I!A=49_wT^Pvn2y{xIrNWCbz<2_=(9d0(C=0|A{S}jnIV*$#I2SaGToasX^ z8{*n|QeQVRu+qozm!OfrNqcgWo-`B51JTozqyXP|Sl{+jAB8uG z-?`7dEAYY#x8W3tLKNsT2BTOUAFBML>(vlT&5$o;z|9P%T&#v#yjm}j!#(sA4@0RR zS;Gfg43CC>sNB`=A)bBfb~YD2W*&cii+{>Z6@CHVvSGp#3W?XSCa8)Q-9)r6$yCTy zD_Qp8vrggcerGSM^Uf|VZ}l?D!jWy28E6!toYGTf;?^Lrjn&L{xNPpz+K-Pz19;}w zK92^l-}thSuMorkXV|)*22}Iqa|0jF56{9%I6c|Sw|_NMH#EfI+@|awitA;G$P+ez zc<>dq>G#}Ae&`i59><{2ms#HZvGJ8{vj`WvaRKS zQMZnU%puV5BWo-8K!9*iYb!hN7r~nRF~(WVF=WK zt2Mz9OoImn1$w?h@3^g8jC<#A>K@E{e^zQ=gsT2-tnTKrIe;^~H{c3tymHDnkrWA$EOQ)r@xbOeF8OBOZ2NO zAy3f7mCqOGpogpZEK&%tH4Y3Sj17BVc1^rsHH}V`3ve-y71CF8_@qA)56w_*231=#L?Bl#LbD!;s3UV-oeGh=|5LV>u6wW^}81( z-|y!?_WNH}WB;|le_Fp|60@-WJsttQn6-hkiHOO6E*y$p+Qin(*_?odftBO`R+dHc z$mg#QxYh8O9TMQ}&pu&j051OU(~m|Op~7`eMrx^<(n|ep{TJV?6qdB;fbY@$ZF5@7LAyPr*`IU-Brqp1bB~jE2$On( zegHw+6#?D-kGF%RnaPXCv%8KofqxuE=)T}=)J!QUzYJARUM&^@)-{wa07n9^C=qqc zEOA6iH~plN%07y~_j4Q^T{MlOM#cv++b5sWViA};C`icJQw7@!|-;yqXd z0zVx7X-osxUao$QJAF-`VP+v#e0%(h2nKS{5*+=0LEGQTHJZqIgX@SSMC0@_nm7il zVS+XVHpKIe%kVd_baovS`Y$vUqmQ?DF;*g~bTaBy*d>&~x?zY*<3Z z@b#&F@zZjX7@9Q)`$j<&xJ^r>L*u19+aFw790*JrZN|D&G&qEg<~s;2b&h@kW62p> z9-%WdJV9t^a11>y_h3tP`%xu-T4|m^RWiH+DP;Km*f=?IGd4y?Q&VnYU^CTJb>5~i1q$Xa+z|H77PX1dpz$*{xcHP9@bnhP z*3P8>NNlXi}kF-V!pHe^=ip#)n)DCD5>5$WjS))1O9)%0NDYM0+(t^~#Wfc&g> zjNB!hqPc#dBtnnVRwgFg`HXlSc@ro(?g>)>N%zX%Q6Heb@xxKQa}ERy1E}>o>;tF` zSXdG|2W!sb)HA`wp)$Y@qvzMYYJw`*g9gmF9}#H>EjbYoK?o8M(Z$5c>mwsGG{LFT zgi;d8gMmSg7{iwmQoK0-@p!&w9%j5jgeZUHU{1-AwN!@`g zCv!LkAjEWidkThb8O zn2ZBKz2m(gV40+|M&vE zf9_YYYrMg4BsOc)#5TqgQ4!po!YFOU?J1R-XKCuRPXk&zz&8*(SXtyqOoM&oCJ{Q= zLSU2{YuA8;eko%Hn;W;FN`G8*9jT?fQipMr#Qx#s?}!EE5RdQ4HSM;**IOkV=`=X} zrWZNv`~l|K6&Dg3s+UAUF>+;f<;KfXxCAeOaeB5@T)1xwDQd#m{;^9ef%%XM$ zB;n@=(FPsaOV)%CLueDA&kyrMfD|Qyw3Y>hCShfUTzfCky@!K0Nm|!~8OHtSV#LAB ziHi3S7QZ@|qy+m?OOo`sjPh_UUurMQtTcY#&#Hc5u322z$3=SvV^EKhie(;(E}l&b~&lQ!LD z@IYE`j9T7tVu*WSHIiu}thzRmwW-ak^$^`5CrX=Kmn(g(%*0L91vVv<@<>8O9``ZQ zR7%q*J-sTcA!t(O={8Sh>>m}P&~0-AXuQs|e|SJAAatj*SxQ4oSfUy)K@&ki+h2@` zd81n6RgR!XBs#w1ms3y4Bj8P?3Qc{e$1?r_bN&!!~5rOK*44vf( z407N-xt~=GGoH~FqD>kBVCX@r6ZX|{2F>QE%Y}jD)`62Fciy-_gZ*P9Jg z3WiOndkoa156&9z)n}4Dwv`W5*`E^Y=z*X!@QmVyaH3d(BL99tY8CaJ55b+3m0LXu zEhO`8qg0S(LxoWKMazb|DrDPHAz;dHEu;OOqMK?5VxGp72pbqhI~WCh1%kj5=?2s+ zSMiqk>oi z>P}2}I2>9bPmbuk9c!fMSVVD^47JJi9?f&&EFDc>TPM4RA$%Ds& z?OlOo&Jv4;-U%S%#@k-BR4~L7*8g0ol<6D;(M}M#hyKY6G#0va-zFt=kg$%zOV-=i zx<^BCO&G5p;Vnn;q9@Z*bR<;4-yGfrQl!UHO+fV;r0Ps4gRXkclo_;auw>^o2l$1x zUcYi)dicp#-BSQ+)F^k1D*)l>beAN>B3XM(9Uc#DEG5Q`@lbGps(T8-WGHD<%!lWQ zRei*mF}1&0^H+qXDe{s&m5FAC{`-XTr~nWQI)QV+3^eR1oeyl*Rk0_=0#9QHbs+YX znv>*>Gd`VeENe~|WO235sWC4v(A(w(aII;CNAxw{`19s!xbY|4$9zLApEQo*6QL%x z*H6@Xsu7<2tWsy=k7*6vXDyjlv~h4Hn~5G|g<9la^%q3dD~iu>Y>(DPyD(Fu4Yk_M z>nXm*7wBXk;}%>K{McIh&(>}ehlhe$?zyig6MQB!xs9+Qx8K!wzxCXtD@sCsn#C69 zdqO=M2AI-7^3Bg?NjF|=J>KGBrHK5qTTKBVZ_Tqe%$l^Zg2)wVVibx#NJgC2UqT-( zfN6H{MxFHr-!szjWN76S6Hsxv9_*F|`(el?jvwY>WnA2$q(LxS;gSW$LJAYn%Qn;d}Eww*4VlQO})QW8-!`eH+ea`pfHm5)X|y z`^GO7{Nb=6k%=14Th^4!SwkTsIWq?dVc72*Bzn^7`Emlm2Bke}^F18V zVEj!SQg7}CPRU<;IyDPO;Vs~ZHfySiQyXfr{c(6mH8UKqUYS{_nn1H67gEVOHHWMi zgT|@Of&d($ow;~cF#M2Pm>J2g^DA*;1JlLPdg=m9D$(LcH{ z(xVMCE@fhiH7y=4dl^1NA73`_vxgApj5}pWby$BS4jW6?r%%Yr+9slQHFWHxxAm0{ zj_x;y5u@4aB@vT;=aXZxFTaEaN~h*937cegQKj=&f4=eK1kdt!d5vc{F9o)S(ad*y zl8Y`W8L0%T)3gy1RHnlVU|w)p%XqD>$?{jiuLhbK~5=Wo=O^=_cfWF{I;9c zpMg1-TuVknbu)zTBJQM9)jcSS2m!uEU6-nb{Zq0AhhA5>1S@!bgFUNfsp%tX;R>63 z*?9bCTC|^&`Xm-QOU#(AJ3D9U1m^&rV*p;tZL+Td?W>-`tk-N+!;X!@+)t;eb*XfC zZ@<%&x=xgrGO${K53_uPow{XQf@;Ct+4W?Q%I1tbEJm0LEzMgx}J>4Cn!<(;u>YG1_%`^-4N3CJ$kMjPx1lSb-QnH)& zT6EFn?-d!UR{ZJ}Y=xc=Coh+Xl=67oT3CNn2X7y$%Wn=1Z(!@LNt5k3=scngjGz(9 z_Cz$YY!YK}bB$1AS)Qw@y$z*U{YJbP%Gf@)%3QqDL7Te2d-_?HH^k1{K65>AlE9e8XB zi_1fFn9BqzIbW8Uor1ZM8+BEC@*^~EL7R~@Ze!;Y{8JiVi@BBR@q_x|~@s9Af`S^$t zK9|&jJbTWEtiLudO;X!;dLZ)_oDY;B?+cWlk!DfZ}+T4P1M$@8Y~?Y&z@ z_Skz0$(wYJIWxO`yo7E;a>C)aw2-^?jvn`b3%=dR>WPcu<`BI!V$DKG;6j#tpyg>5 z&(Q)sb{Dr6dKKGloTwbF?0bEk^h0fLeTQk^(9CwW3;k5f@F5@dk;l~D(#tgNU^Ty7S}U0QmX#t1E4Cb_IVRN0}%4a<}5 zzm?7)(^X~b6D=N9x^<>m}@LT0(Q`U^+^Irls^OqFbuV?)9ecvUY@XZT7%6t&F zp}rUNt7~$LLv?>^q=v@nzW;r%-F*D76pEFd-xP}Cm)-JNNE0EfgIB5ZY86RWy``iG zF4sBqJw3!WV3H@cP`W3E;^YP6^h7j$Nl@4vScW}tqt4$FD}D%ak!krOpR|v=%`BK- z1xt$mQm~wUj)2#rL)`sUurw_X0W`A9H#@KnwZI2Nqai@{4y8OPXhHE;V^J_I_eTT- z!;rMAeIji=38P1^8#_CDf{A|RdtEQ-3z!RaIcfP@a=#!8CL>QxJ4P1(B)-Q?)h(Nt zL?wMLBTt;X63q;GYOEZ~$S}~2{|8eeCtu+rKSLf2;|qg~r%ndV*3zhPJdggW2@>e( z>}8D)JUuTKu6{Te@iqDkpoPdeIu{VF`7jw6Cii5#b-D=96W7c51i+EVF&CcUc3H(< zg9DgBa*6R>$G{=};7;u@1qVNmUYqo;N!qPmuGGZZzBDeJY)Pv))PkcZOBxV!m<9}U z!kW5C8!l!6N`k|pK^Z#B(6M-jZdJAjnChd z4UU0eb*dr;ULISU{eZHc#oKYv#~%>>ccs7?Tdsf3wA~+U{9N+iZadaVE!jtCkmJbo zy37;1661n%v6~@s1~c;31WdL%W}~NJoCXDQ*UaFQD=zJ(JJS2|C}3m#J;a&B{TgEKp5E#QHFKpYZpo`YDBwM2mc z*IphizfnyU^UiiLeHUQMS_q6JU%+?5za&8XvJe{bW?+`5Sa}g7K4F>8ak?a>pU;H+ z*t>&**qOdi0p}~bAFUpk_jDK=Z~>JibreYcswYyZIXMC8fwqDZ7C?C-m6hBLL~}N) zAP7;q+Pk-&PGs@$7dTr#0MZ>Rq^B_B4r$?8XXC79$P5Tn-bgpTj6;bkfJV_K+fJk= zrY{tAAKx#LIBfqnXeu=HDDk-5p#vFH`%n1cR%UARcbkob!z}^uUjhiL{}Sp%Iwp-z zJSqE?Na820OCa+8VB9&|BW%M<^djJep~Pu+YyA9ret`Dg|JLi8`vF4+lYs(sWL)t1 z_XnK5nxbdE1ikWUFje>MU}S7AP1j}3FQWe*^IVf~JOq+aNXZWh;<^OfZ`}%TlG~XF zyf$uMhaR2hQ$Bwz9jV}ci?-ECATU>yS6F^>=sjLLN_ok^`mI&lE?#Sq?a_iBUt0V8 z1=G47;c7b?{%?f_zIKIZ1+@;-*aLq@;7Y|`no@wVc4chD5i`Flw>g@7^;p`rle7o2nNl}x3s%zY%4{ zrNt#R9!Gy+CMX)7CT1`bQJJz+344a4h=;Tp)YN9uA97?TDKGV#C*{JNP#%$I7i)B@ z242|Rp}o|F25kPiry2^~1J^HCm84Q>TpTJRJoDA2i&SsyM@hrCBzhptVm7dYu4--@ z&K86lb@q=^QkiHZ?Ov0vRQzkD!m-HQjLJ6l`#~HopRZEiw>toxfbk4LKE$!JFZq8H{zSc=?rg$f9yktI0PeW_(qp&4Hsh~LZ z7y&&ztS)BjeGG2WiE43(%5N+k4lA8XFI~LDmnwa5C_}QsPa7y>LtY%97nXF{o}#v` zw)k#DuWh%1YEPHYc%9E!E;6yASrVL0bHhK{WpLez(~|5k;g4;rb>XKO8W_z=L!$~c z@_>$Y8}%TaG#2-q0Viam5li7{CiI{!sFH+h#aY|ERhH|mU8yyqX-_H^@eINvXc z2wtMKD`Lavp!$M8vb6pwmp!7@>3N&XMrSk9hpxm-uA86MTGWNrn`R>En^LDE*CB(Z%4VX8z z6l~^OE5h-+;6pQqyOw>1bL9_jsa)W0IV%iY~V%>iEHT3n_ ze_ra7HgG>&*L;V!3*J_wp^Y4~$qcKOV9$YyC0m)|{Fz&$A(b zBy4R}!8>wU_vIx=4bEum(Rnye_KfT%h(@Wnn_adbf%#Tp-qlIL<~tMlw@r^`Fi3C( zX{kz$}5lmBK5a)DS}PU=hJ6w;Rb#>jBsFb}`JdZ>JCwpy$>8WdA(nAMc198ard^;}4la#_TzC`+wqvE&5}Ec+K@V9!U{km6vh3a?}a z8Zj_FDN$Jxx?ec7UrG_tUNJGU|Mv@!(n#600q=d*3MRwBX-h>y)TB&h;(>0mm2n*1 z#7K?_YQ=#sfu~*Q87M_S7zBvK0}ujW%qoR$98>_o%u!0!7$|+d5{69FA}j+}#i+>q zBt_+Az@!Ndq%=J-iBAj6N<@#jDtW{dc9l!i)%-@ykC~;nHDw0E6fc9}YF-8Zt8eG_!2_VVIIAsp5lg~S>>yP76I7c9ca(5{APYcKrM@JA-zgRY-l+N@)P zy4UeH_x$>quvGJV9|IWL(BnDoreATX5QrahhtLz9OCjny*P5%%9H)1G37fWe!v=}f z%c_TY&U>Nb*6l>S@b;0?BagDR*#--dH}KXAmx6{up0F^r#QuZ>RIeU4_n4V6XLa`+ zsnP+I7@pho*#uAjQgI0Msi#zvj)xxT-v~VEQ1sHu8OfM&I-*ZtgaRZ%sF6h1r5fgW zF|+VbbP?(EefokLRSSvh-VS+HFKMe~Rq5z+5_x5X(!hfnqmSfXZ5vDc&g2-}MsBtD z=bACb&I&Y6`{`i$mOY{U!M7Gsm@l_C2NNhfK9Z6w4P2l?0Es-l!uXoerRQ1j1OaQU zn=B39{5RU2$ZSqiZ50lmp`%Ab@`*uV@R3J{h}&QbDD3nc)vD@<3B<&Djq4aoQhcL^X2fr zw6d^Tb`F*-PnWVD6W@qM@Z;`Il(A53*&G`}d7{NRY)gh3_Fb#@H0?T<_F)MzUs#Hm zt_RT!-1KP%=re5L{&bl(@c`604vhzOghD0Et?>y`W;jKEp2y15eN^-IvEmdMwX5Rv z6?h{>#TFB1+8=x2Zm&mj(aox*8lyo@eD|NGMtC!zT6YsG=I38?d=U{>oRHYQJ()*u zuSG?>2Med5@7vz)@7DD;pSg?&PBX{nXhY1=7R}>}QRBX2@Z4fj*>$V&eJl2M$TpV8 zD72wUM(e!TWE4^ucJoN)#juWqTO44Cye_dPJ(!sPf|6tmOZ1Ql(^~muUZ*T+m#yje5n?(MG z67j~(mZix^Ni>{UTPgfv`8saV50~!8pu;Jz-D;=yCf&C&6$a}(P2&Hgb&P8*lXjyH zdvAD*W3rZuM}lWR#6Yi<&oP)^BxtUIpE*Q06ePn!T@M?r&3)o6=_O7WT+q98kbak} zVlV2kJ?Sjzi4BK7z0SzqB%L&PH9{jcw{t$j0F z3|f)mm-picE&G>>Dl?#LA04-jAA3)d`o`HdeES`)M@7nw%Pw{O<8eg=`QP~Q?2P|2 zKc0z=^Z)l(|8eWR&VNW0r5^bGME+**{Nxz@_zVrWAWeXWU;Od^0Z~W^vrs1Fs+yUi z1>GoC-9*t;rb(cd<|Pc>!bkri$71RC!hT?c)J>1#@XFF;-Oa=*NX&F8S*dJ!l>k1D z)(+D8qJKiQ{ud4{7;O(xnIEDIa6B+Vc^Koj4&u?><9_URD2~7C{`Cj$-nZ<;yknV9 z@y+J0s6jLA-pku0@c8I*e?7zwnUIJ9`Rg1r9);)}lPm-tO${$8Yp(3VYsBF=sgWJ86m>LN(b zf}V0mAdQ0ZS@QIz7sK`q4rBxtCZtateL_XFC2mX~Bae43`84r7(%vL{BzofE2!1lQ zh&?ik@G#^KMwG-r@nI6t#C0Mna)t2G0dpBK?ZOX}H}c`Py?wVV4?22_Q-{Z@bWegu z<&w%b57^E-!bnO(rbboFacY$R;-T43tbu?dhJe)l^3bfHXa_j{!$TV=PQW5T*JOs3 zWKKYx?*n<15nD->z^;z74`;q5*n)y(JPK(Hc%bbE+Lmx4ot`3KRf`4}j|BO`8USkt z<}WR@f^)#eDF7v?4}@8>mY@Y;UZ4+$0Z3*{MMZT3#H!4K8~~Ed^u~n%J44-HNhlMU z^Z+)mA(7^U*MQo7AUdD!e%!CozfOR-05a3wlE@duhXnH!J0Uf1CZOU9sE|M+wd~6v z=gxqS$!Rb1O&IoGYAba6X{-}rvLfIiAW6Z_Xxtoe>;N^q@b<*>{X$y;Ip)FhdwToA zR^F^w3<~<7ED%lvo#FMv=X0=K)*CzX6C4k`b%)Q$h@1PtI0DV<$d|bpY}*c*>wtFc zynFW0ckKR(0-~!Eti=Y>@FRs$gVbv{wUxI|jcA!|e85QP+J}Rp;xMD83$K?ygdIO2 z*kB$ZWFtSe(uYKy3)J)qpV6SdB;+a!t-dt2;K5bN4zM&fk#%XiX^kgSedS|)Wek5$ zciY{Y5NbCR^r2Nw3NG%6LDKbXW3Z;>ohvEXXoIRBJ+%sX(z@?4zy`-ngs!&Bhv@Fw zDG35C9`m=l^ofL z)MDHAF>qvprj~rOD(ibV{Knq5X^Vq_x>QNGJ-7dPk3@kO03ix+0TVpzkq!!^~ zZlN4{9VCpi&}EmKu}3*_PvcjfLxJ3jv}A`$J~a?!7FH8WG)LxWYH|mg7=GiBRi zmN{w*2{N&xk1{=pF!Ac*_z=u@s3!E$tmA=~t6B&zvcXVlD=-)g(Qy{j@;E%6Yw1lQ z!xtPkM1>E{f>x`}cx^zVRR`XM5C@qm)(BP~dwxkq)PbrXtm8as4AOEKHn?;qij@PH zX5wMiB3f8}13C1-@ZD$VzD>`G8&D(l(v_+XoJ!m}i9vEO4_>y^8_Ic&drSyoe0?f? zgW`J`-LrXAH4Ae-m1S$XW~x2P`$DUIU=q`9sG)K$W1<#J|87yjT>4LrJ= zKtP$wIG3^2An$qHPTa~3xWZ%?&air{crwP=FT|va zVn|iv_e%xQVMugQ^~_dtv*6lYV!u|a(?vdJ0}ULPlI9=jXZwDG$T&gyDj(ZAnmKj@ z_PI7}mR>&VA-VLTREXFb2edrPeKM5;7ol>Bko?0q#df-o@?>A)ljRj8=w!Fy3QZ){ zDD(vjYB)POz9g|s-EJ>8-dc@&WC@Q?&5#q+3zX^kS1f!j^TH9OYyjdam&1;cra6-q z*s(<$LbA`4h=DMh~SChIkkVv`?44^)&(skz4eOGz4`c z*J_h^7rPHPQBSZJH~%QXLacAUSujbt4$md{aFnm=5o{d(+62?WDf))#Xj(jTWk%-V z44`sH#2%v2*9=8Op;5!HZP=Y;9xfigRY)yGcy$-Q$4+=rqL`l?F2@L?3|f4@VcOBn z?G#%UcX^%Ou$U7jx~g*?(a|q*X7`;926=YIwK#nKSuRHo)9%NEO;?7-Q_Z6q+;bg- z+hSi*viCxQ-~(+V4=OI(r?_x4r%i_5vew(wrnp_S1k&kyV;t09&DE~#dD{WYTJ9@q zxjWN`ht#Mn_O1A=!Y3ufcT}3e9+umCGS3G`|AG)hWUWRJ%8!y}=8Cs=< z^`hzRKuD8uZMD3i_}VS{hp#!CERn);ErKMmqv~HsdsGd-2%jCkU3bao(pQIEe4bms zf95T}K7r~kqdr0QDm=#BDK9XXQ0Lx&?)Cz~aVH!`ylh{O4pW?%W8DA4-haST`Nsd_ zcv8_Y8bp#=p^S4J94b4NBvCR#_9iQtMMko-B1&asW;c`-vdKtRDrJ<+Qc?Nc_jR9~ zLB4u_zn}X2ACLdz;T-2a_kCa2>w3QS>$>ji6lf{#+4*VQeUf4;mqeyw#3pr#ePU)W zLglOO=pNuJJw}dFD^^dm-OJ1OvTA$9B;}jBij6B{56~X-|BPwm5dI;#pa1692f?o- zM!YAjz2mKgI@fTYc|Oy*M#{&YFM3iBU% z?)o=l()GU`cy6jL7bgBiVUsWn%APN#cQ5-bzpRmN5WCUh6SP0ena#a^eip-D73%`m z(M_1!XXWxEnyX#aScdXuZgq!quAX?J6YgIfcAD3!J4`?ABkR|X zvq72APpKZ79p$@87e4jj(PqZj;_5H86=ARJ<<$7Ib5A_?TjXYLf zKfU^f)D|lb)5jIY(w^h8Pxu7l&aIZ%{(`+Lng6|i{NzU8r~LA72QZ4bMU~5v0R8RF`{{3l`XmLbXNkG@XDAX38fl+zpb@32MaKhjqKNM zUA?jG5GSj*9Mia@O^`=Bz^GhJ^$EsHZ0ct zNALE$d-4iCtKXGT7N6=);Qb`>gI=7m@$0A0dIP(9oo{BTyNEns^m*Hz7O?|M5kspe zZ7gZ`vQaT*sCMNG$2x)jlc_lyic%g&sAh0xt_+EFsC0hNRHn0Ki?8uZndDt3W>!02 z2#S8Ti<^t0vbn@Rch?b0+V5rLn%Yi9%8ctMF{eW`-(|`+bxyyot*H6>&HU@$!`2@= z`!@YtCA6zKB0{gf#@S8KB;G8O+tK6g=@H&Fx`tOp___o0JW{VVT^ZcowC+n8?dO#- z`8FYU1|>gBiycZbxLGQ(KdC`JU(wR!_Vd-V&TPF@n`m{8CNz2*Ss$R-a)$e(*mTW+ zbX(mK+3Vi@3Gr7nU%#k(pp{>cYI0NiX;93mgFa5F6W&IF_ZzyzMAkl={9vEa;+W_p z$lJ2^{K^MT-@ksA=7rX=UR8V_#=yuJdsSaAOwIC&&kK=ICXbL!vSU{^9SYfhTv6as z*hRG?VGO7r?@EJcyrl6>4dQp)wF72CE;Ys;f(c9lmpiqPtCL66g&Y<+n~N$+at9<$v*HtJUXWh@38uJFFxTeA~_R+4)CvW8fe7TD&uuA9vikO}NtGJch2dOH_9sI{7 zyY#xn?p++0>^Z$RWL=bq7!Ag}j<=HB#vkfbz$z}7frdqdA-FfOAh*fues+%GnL>|q zZ?D8yZt~fJy?i8u_OlF`UEA%49?wtoxss1#L{_BRP;=b5OFPT-Jt~>LJs6Xp^&vS# zQ_}2NxW5;)1)$9L)q1wopLTC*XS0j9*ELGYSJN{~xEOHiLK{OxDdxcj0r-a#j%S(h zZo1SE5kqHkghKoD@)Zg?0!Ep2y8}e{nxyO+q{S;DE^@zVK39XK*e$T@Qy@l%PZ}jE zNMlOFcAmkvWhL^XIK?Vc8UG8y0Q(&F_oU(%(Cdz zi4WfPY#n-gjfU8%ca&a=R=%>ah;y$4J?H&3g=<(vSz|=A*0ZO)H51kg6@1TmMY8Qq z@xgU=mr{d{i|xd&i<7F^5NB<0qjQz~Lu=jAs`KKJYZ~P)CQ}$lFN;e&CY(?qx?bfi zi-b3J)%a`)k&maKzLz z9n1Bc5~@7Ayl&Ipvhq5eAIKKIhwtn_N`>;_{(SK}r)4kZTU4ALHP>W1Hf8^%Q~L7N znY(5meG}go{TvagY+7Sl&D_J{-n3C5gfGJ`oWXs$_Q7+H@7{|Dysk0E9&NToCo4=^ zH%qQ9=pxhA;oaZlRyCMwK6&H##0(Xx*tH0DfRJyCFQMs1OUK)p1%~{!!6m=^(C;6hF=ud9Dj*S+kc{qEQFLM?auGcV8 zV^k}+bav8l|L8OGgv^!d- zv7v$7Oom_VxMg`S`xf!Yy|12zO>e3jxgXOBZB`cN6!LznGG+9J%e*6+?Ed;s*lRhq z1=tK8;(i!@GJQyX-y=s3B!!e`B#x(^%or>RY1ha)_Qg9x z{XC6V5FZtX{Ig=6tDk-v$!1QEy3p$Qs;btEJg3-sYS*}RaF)W&?nf0$`MPT?ZC9vX zGwS@b#Wc#^WnF#3+c3qu3f_~VO%%J+yzgm72{i1e&g|e&P?nG4$aMEV8ofI<&>(X7 z67y+#$MVi`j<_Bvo%583?uuaFJ^t>9HLiP>qS%?H5wzL&!~WHg`f(8oAM?zQH4n>9 z*hFTmlQ3$n`MR~ZO*JFEeq6I~|J~Sz+s2>EWSvB05+Gkom%NDLo{9NwA$d-~p9 z+X8R%r5^E1XDDbW7C)m_*>jj8Xy#U>B@cUQO8weqZiCEZdS>$JRof1n*oIy&ocgZa z{d2~Hb-WYilgdwRyj~k`dwT3~;GUq{W#W_A%u0EmZBN&)a%OoNnz?7&(`VY(g8L>+ zS?snRh#q#>dSH6*nXQfoEm1Pe=>_4n%9y>cutC)Au3HZz-`=;r!KQbE`m0%;y)KNe zn!=Q8e9evTo$WiqBSzjCZb&$~&y`JKtJlk6+-xys@wm_W8I| z`RK#jVLx|XJf4cNNv2u$tR?B?(YrEH`ul6F^7*%4$L41`CJp-C`MF&L zB1}Ki+4D-R-i|HtlhocPt>2%Za!c$)S8V=+~3D*H5nnir`A9B%?Ea4 ze$zL6T$jacpDAqWeDqFJ+vvv2ZTr`KG`-nnDl8S|685y?e);yxgT>s}(P~@YFvkU!U#X~FZP<`P{u1n7_%xPrFPTpuzQn z%Kq%pQdvi-d4a#ON6g7YOj00PHnqSb{FQ!}+1ipHEY0UiZt-Qd*o0F@P&YF&y>|P~ z*yX{h~BWitr?DUjsTShT{}gPi6Im$G@)sQZ(g%FzC+FBZoiQY}a*iG}kYQ-_(9?`gM$C z*_X&GzOP**)aXnVWINX+NUDSujL?wPXMNh2+idY((XGyRY*g9NNu@y{;ZUfFSo2Fi ziRXh=Ik8#c#|1^2EmCdGR{uEnu_B2lIdaS)Y1s0FBCoDrzxPQIA?Eyb)-tFMA}2)i z)#Ri3+L*Fc6X-t_nkS?h-0Io%PNDT$OswB!I$j&rbhks%?1t!L%axTjdT*}~IVPC5 zW9R$O$hRv}$=YwNOR3_P7P7Wp#|`~|ZTTTC;WO5Qa!q7FWZ#M{EA$Q5r95Rc{h7qN zQ`wrwRuA%%XE@O=;XBc7Rr|8nY_e<5d3jo>A+dYpvDVbdl*&V1jOu}5>n6+1VrR;x z?p7YE3{7O+EkDWIsA#V6@QYUl&DVs_7pOjmUzcd-JF`#GDBW$JqSqa5{Z~fr!bgL9 zPCVUp&nxt~V`C5lQ%B>@ZTZuwhdUD`^aiRUW^6>Xd@L@qKk2g?*Y5LdOe=ow;_`OF z>)n)20-MgQ!Kk$LvBJvP?Kx>Y{p1x{=cn^2KV@rju-MmM9lXgIA$&w>y^TZ;X{|L^ep=3+onfPkPzNvAl2RS!Yi75z146l!BQUwMMNI0XO_~Z7}Hk z-L#|MBb9vXO*UPc95K7C+uU7T=$33zed+N@&)s3YJ(&lT%=V&n**uhJNf>EBB^D#k34FqdQOQu1`swfZRP|LKsqyYL_?nVGcIoA+=Vcsa;`#L2-VvJ^w+`)-El#^z z4kfT3-$Ng3T*_T8O&!jucCoNb|Df=C+I#HW^lsq{bIM5?QJd}3c{lEFFe=Wr>^t{H zzP07!X}Vsy?T>F{HpK>+(@6Smt)*UhRIFtCHdD3nicfu9_Id9&A0D?nX`wZ1lwz>* zo@QLd9f=Qo{nZyAYC4Xis-OI*=H|1O+d<=(ky$O{oki2ZCo4zYy{ld!`NN7=mF&+{ zi_1)YCiP9;^prg>lx4A_dSl49mf}9|D2vsTG+!ADIHr6hu4S2D$QP|@;1qhxaA#)P z{z62V(qT$LM}Ga{AqstIHRT#|d5SPbtdiK(EVJpty1b~MCwpJ1$_0d*)?IIVPkE)Wz?7%b&V?7EsC zHf3wKzN_2H*N51%uhSUQZEB>-ZWL5Yk(l8?=f(XnG_m&Mo60|rn!0whfMsLw8wYxf z@Acx{k&NoT`TR+53VMZ3ZwRuO+4su)MMr4cq z$@7Dehsw7$B(-sS7CmyW3Z&ALOwN68FL9ej`+d8j?4>Do!{~b!<9W{Aab9fv6s%A9 zf9g5yMWtT#@DOGQHV*Nh+ERD(*0AuAbIMfx#~(4sunG*m6{2&Bde4=tQ7_?Iu2+?^ zYEnZ@u&MbArde65pXN$&WATvqb`72f=`CLp{4q~ba(>e6n|w01P+@}LVdTXgt3^Xk z`;rtUg1WF9+`gO~bh@Bh^@f>}{@~6br=s))JUmOT8NNMd_k+rW-J&_k zg%?Zs{G!Ah_*JaP#HJ+T%6nN>I(^`bwrp*wP%{KT8f2cIzWSeJ!fxjs>nrTaEVXKfdC?G6l@2?+G;Y zlkZX#N4%O_#-7r?cCuXcItgV&`>o5P`x;l(tiVsK@zKe1JJp#@S4lB^5g*P?)?gGX zJ9>2O7OsJVxzgMcCBmm$Rf5ffm8Lk8F%^%Jod$wDnY>yy-d@8T+8au*)n2=~H<8WE z_1oEap3u?l55opHHRR}bPG7xu=##LW3?_C=OM#7CcJ~f)=-DGl#q=!7WHod8HzsX& zeI=SIXZ?!pp4zz{Vh(1}-rjgID7jiCaVBsBUwL_wt>Wr%8uY|RFKWeu=41{Nye**z zR^AbA;A%Z$xGYhhY`5~UL|%rA+I0-Nv=rB-JeMyE*J(B~j9s=_>!3aVZVh^k`>VOw z6SWeUPO7pwpFVouY2SrVjjBu9p&`8+7`HPig>sL|Ij>hTtkpPpxlCqn4?Tl-WYngM zV;5u(R;F_oay(PEm|iVs>oM_BGD$L<^Vsdrp%qGMl-Fe{Gk7E?Bm!m z`Lx+*kH0h3Z_buVwivIB(s^jlCa^i+9p7HN&Ej^OPo01Lp;k)L@p}}*RviwV%?!4$ zy%Ld*`{yN=uNd8exs`8?@(UyW!rht z>kOhCE=;Xz_&odM*(T%iTCUsvj%Oa~R`?xv|Mp>R)N3{$iHhWvngJ@Bx?Q#&-nSz; zmOl+oq{qfSq`>xXzCab$z4GO|-0?7GHqCprnWZz5Q_Ba8J1T@kJO^Uyh3>D_5IVph ztnIatS5^LbDoXN(y^k&r-R2l_lZ#G!zrL`Fyr910dA(SyU}j|8if<-99qAV7Nt2?K z=0>s3Ij&ji4uuue500yv=zny_OaM;Z?#fk<3I3l5Brt$l}TYoN8Q|0 z+YAj?20Yx`_LN?$($%c-;a*PO+Lj|5{FBq4KiQP9=30`Q`H9o|od<+nd-tbZQI3r& ziOJ$-lx+=jc>V>=TN+z$otar2yRn4*p+?Gw8R};%jumviH9K8|HA;RnAu(0v$?$Yb zmC@N{4U~!|Q9jPJR{fRNRX2+!&|r4dc3V0ZDk*egQEk&q^xL8$q$niv;s>cE+Jl15 zrqCxTeo|PQRw$Ap(jfaLk}HT!m!`U5+ZNf#Wbdi1&(zUiyUP>-UYV?vM4Q|9QJ(nB7nr(Fd@con> z{ab%$mYt8LLc;nZkCAx?hmFf8@_*9{m9*?o%q`hF?Njt+N3)W!K}(Io@+c;cN2=R6 zX<|FeQ!Q4~s$`yfmhB!_-*V$_IUBS71G06?oQKbbIIu&HRcSB{V((qu=XLekRpsg^ z>(d7l8SkYmH&xQFHNN2^JMFaM(tE2{FB&=OPJCH@sKH%sMO%tT&r{QZeL8AF=z!oy zv~NehbEI{bMyGCQ;NGKKs+aQCP-3)trr?~`E3?elb5GAWgt1G!lonR&Y22y%b=}#v z9mDUmu8kS09&Wg9P<*H~&ZfUpd5+d) z<>NbQyUQPJQ{DOW!F4v548@?@2X~@fHDe3AMzY3lpL)S1+E67SZ@c$IqC;5U-kHbS zpE~vVU+EK!SM56cVuba_%PW0#@2|W)cUgtJQp3uzdG@8NW~3b1y9TYUv8-N&DyDtI zC!ae>Op6T)6YHhibM^0trXNjwbVAM7pQ5Ap$#iC!7)D~9B-gKdmedQjzhYUsFzcQ32b-CKTfv0-q>cUb6lIfcju(5SfdDru0>6x`PQ`m$55e9 z$*#RsBTtV?ddx~}vzZQFDEM3APhAV5(Sk(5A9IHl=e=>aqFXnp?$^T)OsP)QG_GFp zl|1}~R6G^!z^un6mzeaJk))&-PXiJTnlo@rg{05uWw#*@Ock@ZOr!;;y8LvXZajp0 zVCr^MX_unu_UZWik-RfE+mGEJX*?eb|Jt=k9qxfChjEdd=fUVfhEx2~opNV~dVAeJ zHn9Bs-uJ2H?!%eB;$%!OQ=8~bg@b#(yRV50tSldKncn2t(o5kYH2Kj71N!^<^I05OUT>Al5MVAfW?0LkRHwV{On9^zho8?eDg_yNY8gdY`3c>+Jr4a{ zRo8T(N2k_L#XY_CVbxTbwdv(L79GXo>@|*lgW*DcA))ntSb71!R(gv|t{Wd6TffHR zQqd;q4fX7tJH;;5Nonya)QSCYo!)8kRp)hY_gJii@{_)&N2|v!^qA%zH}xdb^WsVz zHORXetY{*z%U;dHJ4e++p(xy@hzOFTJB>TI6tvs9_q zzRkErF65xGA^uh_tM$}6l~v+mH1X4zd#sEb`KhY-Z$8CFzG5rib=&Xa!NHtmhKD-* zCW1?3qqX9bZI3mb3eVttS)4H$@;Eo1<^bMWOl%!JbGk)VPs^8<&1~sE4}bZ@-=rQAM_YpN9~m0ea;D{>3CP%YE`8tE z2U8_fG)4R3oZI4N4^#FxUTCNDI(1)CZZK!tHQSSC$1Me4qho;{@prvJ@ zUw!j1SK*`4SI4AlbP_wQFanTzh`4AM>;kkM?FI#THv3{0PAt!hVz~e8W}DGSZAD{u zhuq^0Nm|!p<*%uf<*cQCUYX*pv;LHrlc>mvgWie*+GS@t?7j$jj<4_K^AP+25xsw&GYzt5!9w-XYNtYN7OD)pV0(g8M&MCYXO$P&Qt-cnEt!=f)VhiG&sWsae`cP8d z`Dc#pnN=@Su$jU}cd{;rx13;Al<&FAuK&}2cQ0Lg(T5{Ht=`+X{U{V0YiADD^N4)? z{v^vp&QBi7HOE@oi~KpSZFHa38u)7Pbz1Ft{>gSK*MgzD(FWu1PqxVk-U=M?wqe_C zs_akiiLz=c>j`??XE9qC=~wVYB}r^osa#EuUy>I~Ew#Z7=loQo*7t9_?|!&^nNDPq z*NLuTgxbcn`UcmAZq{|V>^y5vHO4%iIJHX?)A?}5%)VnbcWh6Z7w!A4&#KVZFDPwu zt*ha06D*IoW_kPi5eLKIhFo)7`>^nb$2qv%3L;-*$)l3HGfRT0&<0U^Vm05`)T0ab zu(Z2wX%ICXOL(XDM7_a4yy#syUxZL(u6>G9#4EXTFA}AVbD0b(`RI674^v0k`&!q$ zqPrQP-;C?1y8nBuO!=d^7!gDr3uWr!O1N7TDSW-ke~)0AjXyieb`{e^Q_q*=X|->3`=kN-U9`R>d_5WR;o$oUfyoKQdmy8^}yDW zKGe1m74M>;QGVZ*&d&{#3p}ss8*hoL;Tt>`K~-^ucI1WqZllW%rw=&sVd~E~Gev5B z2875x0k}V;a4MnanlHt~TFH@$v}!LepOLdOh3okGm4fV-!r=Ug-u2*l`}= z3bAZ_h2mDD4GcUfX_}vy&i7gS`TD3+`pmryj)%9e%bOxGBd>jTD67^xK);#pvrgKNT6tulrV2q|~P$ZmA}Vt{+jV z`Wc{fblYaiNfg7La)YHoKVQ2V6S3aL!Pg}rPL+4s{iEd}Ifp8b)%^xcD zcjc0%jtV&r-TraS%-&x};6tyKYwe8ox!sp&i~YA5nr&B@+#{4vzj@>6dL_N^i(_^( zLZ;z@5qbBH)v~U!=61};fGVOQ8W%6Cks2e-%0rYoYoPY)$P%_ zmULc5!?QbW_2t!N;|}pfk7$(#$6a(>-9`1#XZx;lk7i={f-5FG59&@_t@u1qq_d)E zEIZ&Eg?!X3*$JO3Zx_gKuuJ^WU9>P7O_bkgbp^dGSnt+;a!~Yw=}Ue|+LFTSpY1%! zzLRb2k`iEIZGd)!kY`Wh6zWNg%!|AJsZQ^imYMeZ)(C^_114cUH?+$}9X%gW9r|Le zQ+VD{-s$yZ@JHV$sft(5mU)|=F|;rZ-4>9X{t-1YAn|$DbB7!k^R0F6gY>8OUb894iTt-@fNro~>ky*cKCsro2l{Lz)yV!Oe%GccfC0i4Q3Ylqzz*!-VKv z66X|b3a=7)yPis7hDmgFv(+j^i^#hZ4K+nL4_lovNE#hrdLllJX@*1^I5;}*$rP^d~Yyw zmHcdVAGAyGK3z2!9f?wE8SLA8AT=!efV-RqCg#j_(=iS2)%ojhg^C63V-vmd2)$w= zl>QORJK-85&K-0vn|fsri?{eVMq8iIE^?IR7|XpQ_F^XwBlFg_^~ru3{@wBJDn)5* ztdb`Dl$;v$-din0D3d#R)?~lP8HIq|VB zqnc<@Pkp}jV_0zXHZOm&*o>U@vIzl4?cdZGay+-Htk+D=z)0^H^Yi+Cy_(vH;ALFG=Zkn1*Nj@cFGU|2^d6V=y82*;-uLgHI^AZsyQ~#(i9T;JJH5-R zi>>=fUf-+s(ss|}b3ZzNFp7_TZSXmHyECm1 z1^?Z+rXOGV1=Y04bxtx&*i;5!UfftE=e_3DD(}$vJejA5-+W&0x!KISBbdrP-4Ye^ z^M1ugN-oCzX#dqb4p{1rM6P`-l@$_^A6A~#>@+GKt|(24u;N(PzGcs)Q|_g4-$T1P zFUqI~d{^$!y(Z9g_V%u?BD~q#8M<=HSI_cpnDl*NW$5!_Cr_YY~w$|mwln51?ktK`B z^qdWI$(5k{ET7gI)#rU-+Z%4Le2z)gSia@VlFf&MldsjK*4rcx*}A{z)Y~xm@xm64 z0Snt)HGRF+-1qW)49A;WVqZO5x7(_RXA>sh&W_9EY#Y7WVeYfZHmWHOSAW`{v&h$o zY+D=DGPUAlfIdax^UUt-*Znfjhw{VnuXJ^$<nEprksPFClzl_hry9iul~o@$)g_os z7}rR4VL!Ujvbi=X9DV58pYN4&atddgRk_Z>5(uHPD}YvptL z5&hbPYc(zX?|mO{2);RdKjX9nrdCmL?~fO4dq+H3`UcFq0)zU^xqck&elDc*q@Y_N zwBNi6v!_eN_T3fhgH!45J6zHX?t0puB=0$|ryFo}(yo&6?U|yYl8lesML%rv))cI= zZ=E{&l-z^qhQrmK!^a-h`-KfSUma)lx-{!^F(ktG#T%_h&dyO0jw$iov>w+wp4IlZ zK8y&M<$t&G{#06gTy`1fw&mWFC}{QSNhv<;yCix0e5anAdpBlnJ`LXS_7Adsm+;~P zJ+n&B^qq{+Y}?Yde&ubxe5a}&ej0l8)qjhrnUQ^|+jt{)fhNyBH_7_b{ATv2oV9*V z-5WR8scG0#Wm>V)+bEg&n0QOjiOyqErdxt7>10QA{6em*-b8(pC3|8G(`d(!&a7Ld z)wUn$&9|+7V|;G=PusW4H|A_s+@o}xl|sGYrL|^c>&u9awC#Pi@5=0hyQ+4qy(ZQ$ zaw106%FxgvIQp*B{ltW?yYgcidE;Kv`>rmLc=%*54@%ZoUFtz;JEnGXCg)Ur`p^Zf z_z%+JEh*0o1rm+1Hni?^stTVkfWNw(QM)=ApdiuP$#NRhZYm*k|1vO+C3+ zZTC>D3p|SMWtJm&37@Jn^aBo*xuynJje^etYnK=9W+0hmD`MU+#$BtYMV%?J66m zj!AU(gKyGnYgfM1A87I7Y%ME$;c=6mT>JEBQ(5&*h7fAo;954$tAYmA;uYM+k|KxV zW69!nFN=}rKCwz=oIP&UtfwNkb}Xv2C|;1BU-*J}P1@&v$e=FjVD+J|5P0m+OjKj#wS@ zWwk-^j%FT3`9~iF8jr-RoiQ-c>kiS%wObLXr6OlLvbLrn{Y{Me@IK{+6k7Aj6(U>O zCQ`dJg8lZc31q4?VLx9w@*;G1hi{O<&RAv@rOtu1CwX4ea1}(a9`O3=@p?nYkEzud zs;G8{XYS(Ld~Mf9DlRiWWNm1^SKx+6;kT05f_|2J`the~J}bSDr+Q{_#n@FvA%-fu zxHGwxwthzg^*NP{rgc3=Rc1z7^?Nh&BOs~Lqv+c3?q1razzO5`o2n_A=K7u!!0B4wPzL7X6@1Wq7u7IRgh{kX{3VLcv;=Gk2`WpRF zK}q7`sn`$-)nG5X^lhdCJrwV=%|^{ePx?ezzj(0jgUx~N`;ESR!#eAa>>@`yoEQrdgcn_F2g!Q+&68X2KB#Y3OqD){ z(d(T)Onb3Z!m|-8dE4)upXk+?=0D!5Z?-@fihuM2*^FID*BIKbT-ngh+QC-e&<@_E zUCGv3U&RpG*_~fWR*sebxZyc_R{lel(1p^t|1!A$hoFt!_n)&rpkfd0eGcDxK#&!F zxI`J+E1h4?+R7gOP7cEg|D>EU^oBO2rImqI(!z(3QqyRpG2Vr?8qmS z04wx|`;1~m!B1nuSD?_W&>!xzAS(*=3V)Abh5m5g3$dbp^@kE>h5m5gi?E`8^@+l= zLVvjLMOjh5`b7({LVw8jXcQ~-hx?3XMgQs-EyxP};l9VPqJQ;^7Gj0|aNi5FLfhfv zK8vuTfAx#TvO<5j??qYBzxovvV1@pm@4BF1! z+d+Hp1G-oR0ZFWafEsA?eYgQuX#0Ke4|LBe2zq7}1f8-9g8rcW>A@u!q!kR(3I=Hf zgS3J{TEQT#V31ZYNGlkm6(OV*A*2-{q!l5g6(OV*A*2-{q!l5g6(OV*A*2;yq!nSL z6=9?mVWbsdq!nSL74Vw{$`A@Nkc1gpK{yEGU{$fVbMG=7(MOqOBtq{ttC}dPD*ZCgczbcA*456uxB+eb|lS{V3!`QHUp9DvG~+EisCMi=YTR^4~`Be<<$w z)0pCS@d80yK+HL)1ANp1#Vja%+yWbHeB=VfEvUKJ1qDSe@C&CmkU2-q#V;r*dVycR z&OscXi(ybu1OvZ*orCmkE{;J#Q4IY0bq=BhiDi~5DZio}Vo8C1ROoju8UBxdBJeIVAT0N08M*QV?(iSuG?JSt^D|!a^!xB8y-Ma+|-6Au(1&x(UTB z1aS+La-op43&sP=yx`X#XdIHHk>Y_Rzyp|ieBy%w`P{$7gUE70vB+=Z5fXbuBs)+- zib9f7fE*-cNN&1hNm?-e6I~3se-ZyJO&cK$|L-H}KbJlv@h6o&6k*;?Dt@3y@VCWp zX&^3^ddw;9QpF3Yg3N=4Fs+*h4XpY9JE0-cSSUXu%+cncf%Oodhrvh0nFonB6ZwVz zAbSg^&4dZuJb?d2;ymXHmMl5AAe%T$KoRCt^9BZIzJFVC<^~4%4*9X)X7P)~3cras z;WnYDfgp1D4-&GVg+~#l9P@zu-)rKD0VWP976F)81^>X=5`hneYzu~MFq`m<0Y^k` z8LH``kh(5>4l3=!uRjQHkSg#zD@vGAkg^a|jQ_SSEXBbPRmzB}W5nM3{|Phkl^mc+ zbGZsQB4RCZ90e8HQAn{JFbtvvskwtAh_y)R9UMWeAZ7QZ8a7aKhibSahK(=*!o|M7 zs`MyO4*uKBBrvjK|;G${+eaOMV@n?)h>v)~+L zZjfnOa0K0l%+`Vv?SYCl&Z#>lz?wBsQ{3 z*JkL-h30i(`G#gKa5ES15pnzv)lhMDRQTLtEQK-wf^2};wL_YP)2=162DgTQ*qNgV zb9GSfAt+>nr3U{}gWw_ z%&oLRL#u4y*V4rf{c9x-iN+FU^Yg^+Ul`McCiH=e!vR6G@a=aCGzcc-2^Xdj8)qnN zAXwz|KQ6q)W}G-WUbJ2Z8E*W<8NMb52@|QnK+=KEX1 zN(iZlK)U<)V{Af}60WfzW-qaMCrVqEZgCcy1Ht>yr2-7eG0&=Y$ryq{Sz zbPKL`5hSc*%!BSG zSUv>_>lpJuhUdlprF3i?Q>+RvCyRHR9N8E*gU! zFSM=`e*Hmg_G|4YiBJ&MBYVF%A#mogWQ;E9TsB(>$adKcU#K2rLR)_cKqD}scX^*mwt7wcAt3>V4#4{`QOBn%|8 zIuLaD1;IpG1s5X!Y*8orSKLpcM}!$CDRf|k`~Oy>Mg$zmnrkAXhKCABHKaGlvYVwk z%Y|3&3le6M^ML!;&T>g%BvkMsT$o0bwiA~PNsJT(At**b^^3)qFOX!P&|)6sp$Ygt ze7*^e7BNyJl22TtnPVqlE+iC?*}{db(DIz$F40-cfDz{_B%uYNIU~qPpx)tn5UwV) zWYNOWN}`j58T35Sg1`9NI!S2GmL^&dTmDnQk5~vHp_nVO{rAirqUD$IgT!vhLb#~d zEm_b=s@z1La#2Ae&Ycz!yFbr?1aS*Zh{X;=SbYFj5ad80y8GK=x77HS*p3lJyCe}U zQPMyh3gMZtWbIur7{L(M7v{nEuY(b2W^nE~&Z${+~C1#C{h_oRNpl0AnFq@DE@S&4$2! zmB6pR7z;Ebd**^!?YsjKn8G!UJjl zuW)P0q1d9q7t|(x=_5(-wU|#Nt`ZW@;LZhRaG3Vz{&L|b2{DBApm`$rFTylJ5hM)R zNLC;&CK$wXrNr#UoAxE^E`Wt}@fU`$b~6vSe^I;neV{>Xj)_8xMR(vJGTg+V5{Dg- zxg|8Y#L3OvLm{x-{&{+VY(+E=e!^D%-I5uw zU_LEGm`~4Rz&~+U1aka4*F~6T6G!^Q@(ei@$aVd4RExN+#U!7&luGQA*sI2(Aa>`hVS}STUUKnkr%@71PM9|WS4|LagD1QHwk0Pr zKP0p4|B*S1vV*7|if7K^nliVoCK{5rg&ZVdJ~$87f3hT*Aih})V&XV`F|$SNIEeyL z62KQ(@_>(ImJB%T6QN0J5;-Q!8|MKIyOzH#$ArdbF~AowDkQ!DVh#Qgw?*we#0>ls z4|wYjNB4NZ;Um18hK&kT@rPd!yh~=r!s!)ZmO76au$%bX%phdI@1uAqZYP-EUPSgu z7z3Flg2EKB%OV% zzzDNT(t&~92Ik419lW`^1GIlXWJeC08bCYEgJ_)<*~5hud6E_q4;U3?h}}R-pe z$GDB?QOG1bwA=OECa>TE0-p*t*Iq*RDBK?P$o_>;m$<#>QMg^_QMmo)QMjGvQMf(k zQONG{i;}QlRxuv|6mBzmWLFx91jN@t|A7gH9>W94A@m=?3WeJ$9EEKCy(kF_TI2;t zfFuF{j?8aEOL34c(MXLFS|^S>Mhqi#46+F5Ke%sE5*ADz=97R%HXVk31p#<^=sz-g zG_qANbPVEN=szM0Xvq1&B%=|TSd;{q$R)@E8u!ZvXvpXzP2;-4$p{(>Tfq0Y-!4ES z^LNk>G=S?CC1F8pyZ{O~u|wm;4viB#G*0Z$xF0n@Bl}%JzX5?G3E56*Q4(Mc{XGg$ zfQnN{oGzk4SPB6Qc~~%N$W|i%ap)kM+ArEAjLRZjLgfuypSUgY(Kr!D<2J=d<3t?| zRa8LVP-PPNkD~$EYe9=Z@xcxYxIQ`1Li8H@A_9EE7-4v8259K;O+xVX@a>|Y4Y)%r z{7G5>dfShIKS{ygfezt|u`oGUxaOxZQMhG53+csh=UBK)>|9geI%t5JI3r9lZj1n$5887FUeJjfH=qOz6pVZB zPzq&X>i{*4Ko5@^APh*5g1Zw1g9D6#d4imUkpKWfEZ6*H@JVP8K@0O?goWXUR&X=` zbV1MXCjx|E#24W%d=@4^6mSOI3m*cZKqOKdlmk`Sl=mKZ2NV-6a z2=ZZ}bxZgzzyR>N0XGN45>W;MK+NGT!PPJSn zU^Dy%B;o(Sp*bEP7lV62Q$QVHdVqvW!6y(zU}-}%3xEy_rj#5^Eo_ODNkNA;li-8= z2mE_61k@s01NTD!`a)g61Q4hv%!uFFl3(EjDO?yKK4_nOI2rgq5&(FBM`TV6rGd1; z;sqc8Y7FcELOl?0fERonfxItHF5*^5$b}Yw@}aOO0=xiA01iM)04cCeV_`xG)Dc)2 zKmdGJdJbGHOdc?RfLa3kH8)ae&>yn*cAosfH=V;+YQvDFt_iU=8$fKitJ{_xNz;01U9zexeSe$NuP zM-l|Ukl|JkW-q`AoHet8Z=S6BwSy0(0JcCofUCtHf_}lkK`Xy;#J>^&lCcP5QG6J*5L`UQG2-`t{Gtbl zFi;RgaDkniqZWx?fM5Wrg3kiOhS+ELI%#;!U}#A60u&nNJuaAnkjJGfB-H*!Z2lwi zV5BgOqIeaBH69Ba`GpCNDg95h%LS9mn0bdM6mLO_@hXz~- zff$3PfRFxV%!Jni6^+15&*a^`H z_z>rrp?VFjG(ZBa5Jav3^Z+E3m;D!N@!$f(0>Fh?ji-ko(gVc9*CB}nAP*jz6nqC5 z3nGA|t_?m*Fc$c5u3yC50TBZz0a!psFb;@_!G|ClkcKZIRW6_^3t|X^hUBdzpg|iY zK`s>w7c~h%g9Qo^dC(&mDNGqMGotEBok&2#2nz6tKx80#VPWI|-2>VIT=~OP6TbKNqkw5ZiXS92;1UP8JPAPo z#)(HC{)EUGY)6sw8yGeC1aP&$t%B$P(tXWe6)%!L=EDu3gcTG?+JV5q97U`-U?F@7 zUUcT%$*&?BNf&4|mQO@P01laO90m6SK=5J#cL7+27-V=*3o5{z3?Uiw94R1R_#+d8 zEBTR7D=aF+Cyd3yN&!2RTYOONf)3wz(n8^;6uPzd~S(IFs$&TV*$6}y&-&vME6i= z^Q&-6LeIkZ0M;Sq4U7?pCg2eM5bg&FRPof#5rEVNU@S!O`2>Ilh&4Q_g^bYev*^Xy zgr^E_o-j#-59e?oC=J3RA_$eh0KvVWDL~|0Q=(v40x*UP=I2m3Lr_!@{D2;TJ}+E- zu?Hb3JQ{-f;{xOZAG@F%#>)#paqeF1etY0~N!o@FLGX#M_kmmsuYVv2$DhSj1AhSr zEGNFU36dEC$A{-G3=+_L@TNBn&H3W+o9$04o&W%xMbX<8KWyd=_VU z;SPbY0aF6^MG$d7VfC+>29ge;cp8fR1PFSBZw0!5x2*)~4+a5Ci4@+q!{12*#sHxK zXF-n(Rw{^{DoiJQE8qozZ-+&OR2hMAl3+Y1(@5zOK8rJtaLa&NfS%k!wiPc&z%0Ty z6J%%L5Waa{IUhoE;ku_F6fj_g(eOg}|D#sG^esMezzLwGz%0%Q8)9hYh)3c{a2EIp zU?su5@EaB$29S(}WgZ^Of*KC}{3f5U2=s%Q_>vN66!3TVva?lhQ z8)zA(6sf?$X93~(IRhYr1c$INAlcbmztVG2I6-Vm(h=wqR#7Qv`r$Vt$3UqjlwRP+ z4Q_%P2ZkN!H7J+=HuJ?Usgu%es8G+c4IwW9NVC_bXI}mGl zbcmG096{{c93BWCfDpjfA=(9x1(<0P$FN{wZE@{_`9**<;2}&OVmRP?0YD`4O#}r2 zfa_en0^}E9q5&BoLMaRyG6P3qxgh7xCjdp8cwJjuYT&aVnL$hyi7dmNkyuiap#cwq z3=KchIs8eyI6eefcsf835-}lB0ujSE2~-Pi7ZD=7>i<@`@w9_`=lK8U`hc4fUI=Mu z=XsK{7M|4$P>c9XytLw3O+qa=0YrOlq!>Li~il(*t@Etnoe_F<%f!4L(gz+#7ZA=tbD;PGdXk~5$O_;kQc61@Sf zz;MBHsSu5gjMp~68)*P0xDIdaz`ZbyzYiSo zXTinbUN9CwJlq?Sr-6Q#YGq+s2^jVL{Ja6au7<4OAT0xmp!gsjfprl$F9cW*OCf%_0SF=xHi04I-7vrwPXK{nKn*NG z1;8+n920H@N3ETMB`>gQpgkalAikL!Jv3)$VsC%O&e74)%Gy!i+Q86O-`etwt**Yk zslFjbNL0^2P)Nu?7bS!dHPXfEVnhuEg#`4B1PzQZSd9G{Qv;Nsy$$pZO^FNIoAm|7Y0np)W#+MYEvbhNWs_*(}#znQq5qux;e%KLdVs+F7Ny0-kpv~U zUtm9wP#?Y*K#zn>00-b9=0aeSqI2#AL zm_r4hC@nZc2QCI8hgVy0FCcGDT$f@l!EFGCxuN0Xo`s2rj2Ryk@WKzX5D*WdJfupG zCl6M8g46@Hn?O*3kwtVLm>W3w{=4xoZfPG0U*?B&FnJ)|LY73pa3Pfz80CfB$?xNH z_+B97NWMtWGAt(xCKt%!A|$yWT=54%PrS-dM%P}~!rB;K9&BuA$BKp}3x1YC7IF>H z;jbTI`t@^=Xu=;&QZTeKwl{&EzZDUHACi|dwSfE$E5DqDuDzkGA?(W5@FUki&*4F9 zVZ_B*?d)w0buHJNi~Z=j)>Z$c)4+o(pCb*c3%nQ7^S&dNJH@xl7um%+j7E)5TQzZK1Xi5qnt(C)Y-NqOjX8r9AGcnwG4 z*=y~z?Bz->juU#8>HJ>etJ?S9AE`~~6T(#ff9$_iB~=mn_tcEHX}4lI$m(&yK6%lgtO} z4+FY9UmjoEeSoCi=qGhXg~88s+Mz?+5qoY-t5j`lRl(xBZ%UAiJupXxn@^4eu-ZJ( z2cIX3JPUdsf%lo5tOM=)RGPtXi__%jEc&oDzg8JFYhBQ)}2X`QsiU z{lIm4`w`~4tA}sm-5l55fcfpCAO(R49#sG~3W>z+1kW{!2wlGssV_(ck`oR--dFo| zoZ@?xV87jYJRD;K;ed{X!L6Fk&}F2rxU3Dq#*XA|Zg7#Bcx2%m*u6Z4tgFzEGN9lX zTfLf4^efR&4A;H8yFN~=(Sd7VkjP;1+^CaLXQJ6(v%enL>$bWv*24!mE6VB~ZVkB7 z=fuQ6OzT*=9=+9pPR)17o%WqOhydr2?-RpF((fAoG7yCz_Bdby_n_Awr3`SwfjT6q z-q|z|6^FqHjxE^sV_c8;m2Hvtj0go?)3Xw7&uhrV)?G zlWay>fN0~a6CSeQ!}!SEh=N-?;j8R%fTZvypyj&-3F3DIHAXU1`F%g7v6SxgIA>0+D zRDwN4UIEo5ibIl+*mCNOi1*Cd2wg;U7PS52wy6 z=G!Xac?~~(DcrHj%?MedBo;cF|BS{o zpc@(geHI0lP#XEp>U2ha)N{QDua02Q16YATiY;@cgm=PN*KzQGE%_d*aDBjev%k}1 zi_`D$tpi=vi+d18KlJNFyDT2!at9FD z=5W-7{s2VHJ^p7QUd;Mm+fUsv{@LGxF^zn&aO26nZ-quGp{b^=;t8@(t%@;dEm8Rx zo5L%Cu0Oh6n+_ESxvay0-Qj5h?~i?U_{g911h&A0L$A*C9k;L&s2N4W>`v;-pn9O4 z$vTW-xFM~f;D|rQ)s$Fr+EL7Ai^zVRokpg#ZrF93Cc|f;;4%%0B^b>vJ_91J$MpkP zf>-R$4X^YZ6(-7Jgs0%*hNgRvby`2*vk&LsgaYT_?ULUZH~4A3Z1KZb)eK+;sxfy0 z4Gv<|=DgB(Ztyof1eftX1vjQ~IlQ)X0>+{k#Mpf1ZqV(c>S(u)X~=+WK%@do2w`x3 zPHYf3b7tJ~ir5+WB3lzYaZWJ6;=ZGQ2y2M(s!I#hAt=5KbPJBi+PT46&!|Lr32vd| z+oN?1+gz+}0QA_?xzXo?Wteg1{0``5Vx7(bqAL9{O>^kJk ze>v!z3^uzHVfvlD(AQ>Y|N0_;g$kHP&2JnTgtm=M#?1j|2R%2ZM~L^KEEj~W_Z|hX z^8p82>#+=rZ+X!HoUK9{NXnJE!ofG+DWSJcrfB;s4LXcP`{H{VGyG1Er)WPa#m4@; zY8>fbb?i|V9ysPoSuAHBGP_M7MfxB%#1O0JT*45`J?MNfyW2?p2nT;C`{8EM3@v77 zlNSxU%a@0z*+5RRU?@-lw|GWkrUeZg1{W?2J`)6~89JM*{L{CQ&a&(o_oL(D_T_NS*pQ8pi*Ltdd5|nGuvr` z9*5H#JuOhb@xI7b@wI$g}8#fDZJqYM9Nnme}33AQi{Im z;{b~GGEGcqAB5K)y}e%?F$wP)l7K=P>BjSt!an|ZZ+G}Lp>ra&vCxOeFZg9t-x{^4 zY^iZcU;x^gV4rXN#<6SM2W~+=37bSb)VC9 zQ@r0!7Gs--q*smuE73DhO2b_qTX`Cd^bUzYAXw{k5uw%QW3 zECPjr(H@BjgKUG3!|}pCN3_E~*d?sPK5QothCGw)+_G+PIRF`egfo75B|B%IP^OUx zWUks^8=Ky-8j9RyG<^f-Do>4_bLeo|KLetL7vdFd2P4XEjXH-#9>oO4F=5v3GkbNb zpk`0SW1TQckC>`5sqFr~AWR7nrc1mqas$y3bpG>;*WfK2^W$+H$34*IMy32U7!4^i zk2m@zrdfI@R&wBI+ZtJ{N0zZx3G@keyhIz>!kv^R--GlP7&4!NBUwkS3G8mIr)5pr z*JN-xtK3S_b<*H|=^B3IRv8^8ABBRGqhqyI{ zYq>YHz%Mel*YB@FT_}^)VE<$Cl1L2`r-=9{Nnc#jC~oue1GQ9(W37BOu2r&!tY#GS zq}LskZjLSJOA|E(nVkbMSgq6QPNBUn1rb-sNObf_$Y;$gS5qUq;R7A4#76cJS(|3L z&)8gU)(;1hlT*7YB@pIGo4dP?%4KksyqJ=bnAu0fy6*IGvGlF;qJb`0-3q|vB8A=a zq=pp{-!Q;vc7v3i$Pqp9)2PX8nUJv)S+0uHY?C!K4bru+kbk-_Z|n)=-tgzLNBHB5 zsTkOGZkSYDQ0+hqsm_Y2f_;p-MM4M8ClPv20)0Rxq5%;T4=jcHCnCQIMW8XFWvApS zUY-?rpetcgUyVj0iY+i=bwAZniDE5~IYxK}rP0+O{5IuJS<0WDnZToT=+droSjFM4 zD*PRic9JEw(J%-Xm~CCdY7AO~LyFen=p(OQCOh{?7e(JxTBpXLZ_ZOb6cGHH(#kJU zAH1P4Yw}&&Q9e??37MI1tC55czE`Z_s+ZLB-d(XZ*`v7QvCdAetN~tUqBLV7&7n$* z^!PN=W0-Oqo;n z3eA)&jM<~p9=2EJ5m2Q!P?brLOgg~o{tRZLTSe^OKGJ1f*D+__*Rf~c(f!DO9-va{ ziXO`(;8l2-ENM6x+Z}@&gv|-UY7b_<0yWxAQH%)pb1S_PbFeJhBu(@~Or`(fM6@>`#XF5KtS>=~-wEMIhvd`PJb z!Gv8zh|NWU@Y!BiipQ5r#s|&_un*fzy~8`t@HUq^M0EQ@p(!a=3ZH~CUCxI|!mADf z8T2DE$nge|j7RcMKud8_p*jMUkXY7RczUdl^-1G8{qOYB?`3yaJy5$OV@_T%5{(Cy zda>}>yg$7r3eCtV&G52K^|+4^5nmx2Ec?;Y^V>j2nZ(-w_qBY)26u@|T&Hblk|CY(5Du;r5vv%|Tt5#8L4W$x^~O62LF;&h z>iRl8_Vy#nFs_|wO2=we9l^zQ<5wngJd$~-k{@nTyXZ3%qKhyPBn$Xv?6;4Vx-kt; zKw~|TSve6`7Fz1Z=*30^e&Bl_Fz+cfhs*2t;`yvXVe#R1JBpSSiFFQ5l6 zpr3l_5zBECd+PoBUn3X%qrhb*DSV*x;0(y9J)Xuh!wfgLD#hAcyyI#F`W*JG2?oTI z?1`}tcS$=Pvny*EQjB||-hNbF%^vRSIlN9J9$mQOl!|+&kk>Wr6ha=p-Dh?+UMfvh z0DD6V=F&Oy(pgA<{7XBY;b+kIB*OO*U@vYWM#!Drz;?WZGyoAPPB$ksMgv<;Ez}}e zDK;ClhYOO=WMDL=$gTP)NJw7Vkn}){jZTi1pyii=*2S6s{tD}WbBok0RMpM36|S%n zlOJWI42_yZ3?E_;pGl3ie{PF44OX=3{&w3{Z~ zAAZ#bS29g*$!d1Gyp^WoBQ-f`(tU9(Cfi21?O%% zWU*VHekskzMLJv)Ed;z4`(zTe5la&96FZ3dAm$9k1qcF?ll7@gTq9nczEp(VPC_^fhG<2%Cv^2xHWQ^(%$A+zBI{P4ba< zvH$tOH{=QKEy^M3ZJ{oIyAL)3)9Z8Ge1b)@@X>7+W0GV03NhEeUMNCL{A+YL98Goe z2vC-h@zsgup=*cKSxn zzdYixyy({qc|1)}QYO+rU|dWqpBUMEpf?r!^1a0B0&*%%f;YyGE3{eYZj9qL!j|!k z^h-%{%@b0sDUzVpWW@RDy^|@}{L%-)6!W}U*dp3e%u<&sPE1%*TlXQu_-CxfWx|cd zp^aPfLzrn1#!7F8H@>i@5tWZ!T><8sFzPOEavJAO;9XkG;3?q;@Hc2mI2}Pn-f&|x z?36+sD)l5}{raEl3HO}6qLOWwq-S`vm1Vdt`M;V$`=o5k3}lE8ls)typOiTbP>&_} zK1N0Jx)C_UJ@^gMa- z6GhElA*G1jl`3(Z^>n=(NF*L-|Mrpc;0sPNqQ|8Vx+D*Cxw_B|)uIlea-L*Wt`|GP zT9(YINxK1IK@*3NtA3P+=~!#WcLR{!*S+Jjh@b0SoKH`Ou3)v$wYbh`&IFenJ59p= zj63n7{&1Jco)iAX(ptbN=++dS*hUzb$F8lDaCIRz5=xKAw>=<`lRrN4Yjo3vsV9+h zvs9n*5bz7|IrFu)V>V2rG z(@3i{Cu83!ZvI2fOe~%HkcMD*6viNUn#o(W$p9C31a{JM;*aRzMCw? zomaD?-=s5Kf9*VPS#7PftC8pENE7m6c*yOa>Nmz<+a}92IT<@Sq)K-aw008^I8=o@ zBf+tftS^fOyHog=F)L5_7cS79@ymDbYEeyqV7k;${WxsETqt+s3lxn<(3H3{&NCW} zfZN8OwflwyDH#?KG&8bJm96{5h8=8+cu9Y+P3Oeyj)T`T{HACX(?_!H-le|5Q+37# z0fL)jF&wGJb5%`qB-6&M361pQE%Nc;mvir{j99F3V|M&0`BPcu?&Azd%Yfw$LQVNC z+?v;+EQG?e56{#0#;onh+ru?dzP5J`n*1~27tA5GoMAj6 zlGwd3-6%=fjg9)5W|qI|K8Naa778%ujf=~%R`p+OQ!mJYNnnKd2=qR}EbeF@WzPHN zhqX9#P4Ri0o#fUWHzyx+BqDe0v5$OyX;3uM)C3DTVIK{_S}_tKR!l0J?Ju#)UM*ty;$UqEy?32w~T; zNI6(vk4{JRQDCET^pF65{R|?zZk_D&tu1)IxOO0Ilfu{|`vBgu(gXV`JpRLw>l8t% z?c-vFuRga_LPZ8DZL&kDpTj&CaiTmvFN=w)l-b6wkJVm#d)GD>UVYsA#~;S}Kg$Zz z9S|I4^K(%9ZTp~6L>T{QWa2Tek&|ncgK4M939sJofrTRqVEwhqDh0iLRWm_oI3E!d zLMb@*f*q&OZ+%?(@HDafh02(!O*QZVVsqJW@t;erY}DOWjoDZ+nY&4A?u1jAtJ* zyg&65uUPcek4COdzV5t{jp0~yuDRE`mW;44eu8Jk&F*_TXH~jy=;&S4-(=CVJRN$p zpVWKIq~E`VG4Tl-aRr9Eg;;y5C#AMq@Jz`#@{Es>$CN0xf&2#9`K`+%XqPav1lvxL zRp+kiXzX(F1|xgB88RyRsc5e^mJ(Reh)do*2Ddo8#2c*eXtuOALh+W6zi2iYR5_>E zHD+LI<(b;(kc)?x1ULI+-7mC;u8l|1;wks~5qqZ4)?b|!Rg1sI0Q;WdX>%X8@^+-y zPuK*L+{w{J5R zil6yhyls!KGm9i4RuKS^BpcL;sthZ0NeEW71Z21-X{<5~Ud-kw{%4vf>aQl`& z4m`#UHeCwm$~b-SIGdQ56k936?%ps?Eda+ZluY<9U_5lJaghW z7F2cECDsXPy)}*u?J*vt1cGAF<5+pq^)rPM-%8#@eU) z7CJaHTS>J|DiJ7-4-?iN?DC(zug1XFB>M{a5Zc^qopTQrlM_FhYVzBwyWh!jX<_WY zoEiLlDUepnF$|+WJ|-Hat%n4c7etIUCX55>A8k{=z&EbggX`15#CZ8tpw&i*c`1^}^*Z6m(%PP`()L`*C0ArHq;l3ZmJk;7}p5t_?AS7oV>j+VGU@{Kjr04idu6 zQ?K|;IX@Pk(-_@ zQoavf*Hj?IV2>t8Y2Js9B4zhil(mTbs*hU*8Jt~tVp1Lk_rEkE*Unxx2}KNE>FF!W z2wZUl(SQ1UA?{P8HKm_m-DwtCQ|@^d3iM%l#InSCBMS^MIF_S@Ck34Mu(>ag{HXrVn1I!bUXvQjcVc_Q6N0j1?w%bSY)Wl-9XPRy*Lhn_}FG!=Xit)$gm|!EQRnmfgr$4F7|`!MCA%BYcf;+Okce*eSBN81 z^Wk82fy6mR1WhoR&r#{Y3F{0f(1}V^NX+<5z@0$KxH%INzV5t)-p==U0tWSpdvl*e zX}pT?MCc*{-y4Ow3de-HW=jzvM~6v~p?uWYLD~o*MKbhjgw1q@y5Cw5Ll8)E1G4$7 z&dnwDeY4?5k5!`RVoHJN3WEsI2r+kPf+aVY9X`mxFnX`rlR$=sMjS)Nc?}+(W@Zf@ zNus3zLNOI^b>PGaNro6mk5HRbuWL70D2bGABGi#e)Y)ECGf7@(&&C{osMH#@Na1zrV3Sfo#`jyjRG6KkJ~0Oy5OS3w zz9_;E1Z^q-eQ!^EXwKy6h6s&wdd5Wud5?*mD*A&-kvD|m)Ol7Kv$gQ$@(aW4DtR(1 z;BzR0OA?~l%}21$L9<78hbqM9e;>FcBo1yWOMrRTPy>*gQNYsApTo04;+T2|;;XLN z?JlKFigDgM*&X!G-&={GTY0>s260c1kAoBB7pYH@rKi3Ie4o6pIj+Implky_iP$}` zUs5D21o&WeDiAX*`mx zHbdIkT<#ZwRwk&5ZI>=zTgOtwo4a91fLaTAAd)5OB6NNhq)EFYO5eV$B9=(2KAnm+ zFWH4^Xhl0uele~ITU*mFmWX+LI_!nQ$wg&3#4PGkA zw7R?yPj-2$|DYL<*YPNcn3;1ZUfNLv^SqGPu8UF>G@1qfOqjX(K`;buTMfn4C)aoESn0n(c5`gIwxGhKn31?wp zk%7a_Ulx7$3-lCaQQr)D)xGGfPB6W`eE@lNTM zxab#%i%LT5StyQ;0X~y-QIAT{Hc)!C4azi#Zp6bI?_zTxdip7FSeC7>Pr{VfM;Jpa zJZ>w7R%;@H-ct<1IBW-PiN!v84w@nbSACCO5#|IPj}Z_>y@5AMvgmZUBcdA-F%qOI ztf8q{&25v=r~*2%MhoTGKj|qj0n^&}f~ycY`5Pqtb!cqywxuqo`D`V_fj%fV-1Ryg z`&$IoTQcCglCxdE&WC^w?KnmxK_Q=3VX=uWd$8cxS+J-1c*hKMVVq_`sb`uc%(xN7uJcg?uc(~xpdE2j$SW`tV+O=QsJbu^xnH;M!4W4T)4z}#2%QC2ipRa? zK?_J61k7`a3eHJBT|vEVcQ&dzv5uth&zV1IWh*srl}}aWEM97R!8DLL^lAb{KQz=HGRrkpK9!d%%QtALs%@yK=uv$w z2UokvFLxJqVdc-eW0$6O*q=(HSNK3CbgfSOtB+5WTe%I5pt5sf%-6}mBzC*&@anHgw?2eEmYJ3 zmtp4Mc0QLYX`pXwsWIo#^m+gj{p>GAYuZo=Z?B}$SN$ZkrTiFNo+ABKM(ciuC{3*> zxV*jv((t+6nl*1puBRkwb0st`E9-o?jH8KDZ8xc0*NjP^tEg2;S#Po5(16N96?3B% zfv_9DR23CnjU7G*Z>PLGvj|;TRn4e^##cyq@(^Sm8Aurv%0^4AG8gbg=(shzTBW~! zgPIbsGUiozv`$vJH(w=)Bkpd6HEumH&f?FdI_4#(ssfh-w3n~IXe*@<$G~UhBMxxd zQJG1cXD;{d?ZP(Szg#7_cm1^1gi=aXT`bE}w}s83ZCn~yEG|nwUq1OF@e^-&F8`ZE zMJL9kZMF8W6q5yXg;fFekp#cPNE`cj__`Tw$`x(PBVGn2>UeDMS`xcMKP)@D8S~zj z_KdqXWrkElGPsL-kNt3xPN%IqrER4?Q^bn?Lfpd9k|U=t{2h2(gHglDVp9iBpZbdJ z1veXbb<1hXMjd8Hqm&8#&Uf$%o+kFK@^Q09ew&LLGxprPRI1prEizc1uSKVt4c>hE z(P6sKI3fD7tM!d`9!sFRAci%Dt4#BJsh0|5nqBGhXd2Qk^HSn84XWQhJ1xsux2+RK zK|Y%nV>ReB)fq_^V8kS#LmpOC=y0yOIQ=k{(^ekL<@n{C39Ic9$ERq|qx{WK6d$_Z z^;`IQJw$Ejv07|wxq7A7-MlrGwzRE9Hbyjai~ZHobd_!p-CiT9@LSl8F85ib1_%GE zbCN3r3$`S)DdOzNsc&9;K6L2GGB_o6TvfGQ*0#CDTmn=YJ#C|=bu8Qg-s1rVdMR&Z zVUI1sTr@^RkE8@%51JMukISyitSq?^qOMaPjptpMWpW4ZUfq5kHduvttxRmO((H)fnGyPOgO8lwIjHiA0tW^ zqez>Snz)ShbL4U1EjrBGVrfe7QcGD^G;6@y!@#sWg!QmIC!TP$E!vGnd43Halza79 zBha(a;hHY)RyUKT)(&N}w=SI6PX=Y>1G%QLeuBgJk>^Tk?IDh{nL?TiZLd9_ZQiF| zoV!YP^GVHOyengkZaLZ8n!MrAu0lNK17n6xB%1T?T%h>T;@tHVvvJ zR#KLE4rS6!HLneDsSe0j+9$VfIO6J!#=NE!di9|p$R}a-4FJ!IV%yq;D>A)-{I3bi zF!qo6)R?=jcBY)4Q-+f$%7iqV65x4;#b))%`;4qd6mVQG;8j)=X{!cGFO}ueYQaI- zF}2vT_!7fMwrUQNv$(tbmeVd&g02qbeMUElW`re0w@1~Ypie}4HC)9&QyNc6T%bK_ znzx%c8@E-*g&$6SCVVxFLOU5wAlNe#&VmXn6XTOp_wzEKuVSto>_f|#N5hlWaHf<) zPZPNXPvh4B6*tu=RUS=PV@u`puCu8;ZOA%;{Ux<(<8%P~WiE*yXJi@o^d#k{kq(qr zcb!}&i{%TIBdt64ZmnU+2J{9}*{Yu&$2(DTemy4i;qA+N zcFH<>E@!GLN9uPEjsn)MLHyObFgQ&kbx0)18E*39_|p&hMzW_{+NI{MXU$EdvA@bC zZQyubhUID1z$rJFbS_U5s*S4dIYwi1t1M-7VmbqhQ;APvQPh64YPc+I@qkSUTm`RT z(m=2v`wYluf*5sq7!`dPtJ($m;M0$P#aZvzAUjU-NB^#cgKE( z%rV&1`^IeDiQO#>+!h=}^#wfKxl!wRT_zh?f~=pPOK?nowtk@Z%v)W~eOB#Pti+%t zX4PVTg8B6690AQa%()w5?#X$$+j=;Q*aMEX5kKN*hHT}c%V0gdP%mFa|7ulOh1oI- z=10%^$nZ4h2u!&isWFs#T+%gfYj9i3Rb@s874CDFyxKkt=LmY@vGAfvO_$K9o`!s< z;fzX2`XKGGV%bBMy@Oy`4YHX{8M0(n+h(_^gn=AA645zs111}%{TGg!h5>dP;_bCt z@4&`+W2es^c3AuKbaBy}Nt9dVef+$8p0x>=y-Bro#V)!v_P8?yF62f{E1llg?*P`Zzhmos7hbWT1%ifY52sc9IM$~%$Pd=BEfji2dhPNV#-zQVVA{@Q8#f^ zhWTui_0ntYXhdm9%uh^%Xwh%?E_Q%SlP$9>bWcrj6Gjq8Y2X>8Em_Xgm0La z#CAQ%&}|yY45BO@M(5-=H_=Y?pX$-%VXZXeEGi-CE~FT;-PKT;S=61HzgMi9H~y?M zU)SzIaydpTN0w#5Ik&ky+N+>6r*`1QolBZ9U*NtfTxw8ZAaArWY9dvVew1$*EuP19 zFM6g8%V#8@$z)+T0heq{y5O$Gu6dG>GHJzf7b{EYw3InifgML`)Ttck(t;wtE|VdX z8Wc%YrI;#p0H?0San7_&l+Pi}8y;M+#nI%YWtTb13+7JJb{kOd(O@h-F`I7E)2xr6 z&6E;5lGU!wWN&leMTa2N%#}M-Y9yHVbR7L14lfSJo2ZE(xEennp z8#UfxL?s@@ZBb+S4N|tunB}tzFTGj;dj)-Q^s88*DxFW6cTd?^OE7XYC3ZS|%SaCk5>8(V)zh8x_mb^tn*$uoT9}SweD0xL9m zv|Yu}Y;Z(us=Z`)(!)vZ@#;n8+mtoeL^w>6@z_${!)vtc6mmSuVR{?s<&Wsr&=!t8 z`xTsx%89m7wJNk1b}r$w_rAjNl_<32H&wAq?uL|mG6(RO#JAsyIN*Db%*wc3=IkuK zIGY})x+*8bwEpTuI;=fXHZ_U-L0`&Rr5fa{#Q=6=Ze3|F7eP7@E7gxm3K~?Xr=t_4 zSvx#xF4vU$mEAbgn+ByAV`kY*Pr|`bytG@LUciKp%l+p z&<_P7Qz}SYc!N=6Eyx!NWv{-0ny;C<^z1CydUn+KP8EwDD~B~4ZUnAPr=?l!3K8T; zu$-wNJSKC5)UDmOAT@FGW4Keo#g5sS0M4TVh3C79o+F;~Q=__bXC@B_qka!2QYP?zM~s=OImm+HhF-OwyhOfHRTb*1q=u+LaV zNtNAxf@dYG7?yb-q+$eTdUpX#q$qu0GrvAkq@~6jI#i`fBC0-(lwcYu+L}JrkRg-o zYB;*5m9eSykaSyH(bTR_s_3SL4SmWR1>=Xc1D6c4BM&&Q zS5-Qcux64Is^mDN{_PfHn&hjD&zr{m`mRIyMpqcvR$Sl1IWeJp*bc+1(`U)cZt^LA zEwJOHkgAWd{kU+J*&vE1 zMS(wXm1jG?8M@1+6_de|Lf&HT<{50tk4+Iht~zcQ*Idi7)6%DYNQa>adF zIFp_iAXa4zOU9~mX2YfZWb)M^F4)eU=4FI%SJyWT|Q-dCel=d9z!o`gM-A{ z22aza(8bm=;#?Ff`_EX@*cfQjrhl)|w}EMan~e&aUg}GQC6=tHx7IpWzWqT_6x0#E zn$E2At41H3j+iEs2v)+T|AX9vOqqA9>h*H%tL5zcBxF4SA+~N!*k@`B9*vkJ_*yi3 z%Rc5GA}zhJ!znpG24#2clFG4zi$vBMGHS&*Z3e{3zL~H;JxeFS6knt3aoMnuyCGB; zBx=iGoD909<)#0uuq|E)${qa%nLl5UY+lj7wtGmRdiDnUCz3kx%79 z)3Lqkgr;1@Rz@E&igVX0&;Z}*PP=01DZ&dId0<|F#)uYjVRxck1IFyZr4ebtKA<%2 zDu6l-GWh94txgB=%7L_SK|Q;rH(34%!4u?D<@aLVy(zI72D=P6Yodtqd2~N(P-7#_ zmgp>`JFyd0AP(Jz+-SPOzE% zg~VdYW+8ppvc(O1Y97KiF}y*xlgja?BOlfkOc=zxe>nxrXDl%*mpM*9J1%wLCVj0VH{Pacx)^IJA*jR9f6H0B zZdPtN@rS&N3~0vSxm&I~+m55AUhsLXd4s>}D6qKcTyNF!&0JnXe;tFYiYR_&80TE= z6r5^5VkFReEi2)?xduG!e_3`C|8{WMwQ|yo@*KVXcEs-gLbzhkrNP%B%zs_ElqD5= z;-c6d)^(HUAdhqD7jT!i{+1kY+IEK9^N*-Pv$HAWbla_T)Si8stM8eX{dLbQDF9J`enT%+UIIOu=!H7>oEh-&Msi~{$4Mh z^6kfE>~(a2_ghKMXNGE*feb3@GqqjLZ2n^=Oo`0waKD=-myo9(v90WXuI0#H) z`4TUqE(CO(O+yOY-bk)F&G7k-r@UUQkLF}O95;2o=EeQ28w%J5Z6aB3zxjkHWxD=2 zNSfAp7h<(Q8L%5M)xapY>sdf@*Ov2m5OA2)>hnz7^>CVhP~agQ58nvu*+AIZb)$Kz z7|^cY-h44v8SpUbz;}PGvxCTY@kLVcXzig{u;?-GrGF}s(8JC@!ELSaX+3~OLp{_W zPR*+Wdd&j=D!~6Gf=2%w)w-JK{JV#LfvCMx*UNp$HECje&Qz1%@daV7;xyug#CdWz z2e61;vQ%%1*0t;pAejQXB@uBnNz4TFPanYTM5z%QxK%|>EFs9Jkh!)A$8a}w8Vu$e zQf}zYP*b)nQH#=XP-j|UR5^`8ablE$oU+)VvEbu7;1?8BgKaQVekid*RI;lUm6;Lk z295Bg*f=`;Lb#6W&9*vF32h{sP0Pi&N{m>d$&Aouo$)3z_|(~ZV#Q@pNIfwl=m;gu zyT>ekH}JXtu#Gv#DmO|}iurR&Ea{D?Aj2c z6IijDUs4O8+PCKEUiiap&ttu4Ca2YOc9shhT^lXE6csDmjSkD}*Y3QHc$VQD7Hd(S zG#iWq&3ZlTm@mt)@Ky6P5{qjzbn?%JXHuP-El|8u?=GyibPG=4TvGGM+q>)MtxjFo zyc;n3r%Q#e!ah__zc5Vl+rWW1S=wFtB{s$wK;+AhJPA&SY`@yF63feKDb7guyvW;z z2o=e==;oU_vby^KAEa6FS;q_zj>dN9wIF*AtQf9wuRhYd4q=AeI{-mF$75=iTEyL( z9QdQq=W(;ibhQy=iUz>Zf9X@xI5a3k*fQATe4E%sC+mMp1MU)gT5pPnID7XZ`nSG19WnH#{h z4B%&G;Q|1>fMFDNF);%GWpKY^IKDT1&j7;Fy@PO=*{Kt80U*IdjU1)StSl^Di8z@7 z0B+I%uQw|bVS5W(Ga>-y$oqSUh&Xr>>Cgi@A);pk1lb1!oBR!f#sTnEd@l#Mx0wOJ zzyCmz__I&ld$`a4#=#$L{=o;J;qNFlFpMHbE@toj{WpG8+>Bkl907caN{hW$Pz7WF zJOFa44wBMh@c5%+h$?Q5j<#lY?;>IXaKWfSqyum_1w>E+aKQv{<>S&L0t8+IfK>qe z6#+OJfY|shU zX=LYQ(<1_46YFpQfZE<$aJ>Ve0g5>RC^H1^7y-6zB|JI?Nj}Cj*YBr04e^%tx+#&H7bM& zDeTNIO4pq+O8VIfmZUy481t#G^$935M$aggcnNmhe>=v`2t3SzWq&6|S~F#T+7%U6 za3z-}U2_@x#1PM30oZ0^DMLuAn3ugW)L?Fb1FEAra|rrzx?VKFEG8_+-|h3tD2F6J z+c`?;nL^0X=qJ0*=pQa_b+?$_OsLz!Us&NQJ-jPVbXGxOKQz)e7@QS*9kEZuRsRG@ z{vHfi7Wyo0z^gbkpjb|GKAT~_?EquFIPjCJ&6MXX8F}oqai{SaNWl~yNV$qdkrrx_ zkW=QTAH=BAxpF%N`Ba?-uXFmCx8=$>F1f}V3l9e4kv~M^qa&hO#x{i)jhE&c3lFe{ z$=6^dT8J?)dr3sJAr#2sm@0}TDhCaiW`EkD9!5O|HFLy`}BFAH-I_)Kh868`#<6%{9`uJOa0fn zVQK`J=zzKRj<&=7{zl3G)lhX%v$uM8X9Y~+-|-RtJp13HmHtgn{>#^u>mM2cKyUhE zV*T-I$_(@0`C;LHcj5gHbPa%%^^cxCKa0mRBY-kPf%$Y93p%10GM#^k&PPjoYXWYQ z;=HU4F(Lu!k#-CNQwEcZ#EfV9#W~~wMz7mRv0`Ii)H??o>V&v2lBJ#$Da(!^H-dm$ zr0o;cr@-nlUwhf42i_4sOe@8G9Rn4Nl3$ocCQ?CmvrS`RGF!XRxER#pzF68?Q)2tW zis8_4M15Xo_=ra0Y&mW1tsv(D`|JV~7nQy1ulRglE&dZe+5aR#&(Hkv!tZ>7o_U1{ z)}SCs_4+iHdoRAeX(vl{Xf48Nih~7bG^f&QQmCRZ+R0k8UD#QtxKOd7El7oo=Wg8F zQ^e7>_N2hwIf)T2(^=FgWt0jM9~yuh)>G$l zcF}Pci?>U4waAbWkFVpB8=;$*lKiGsUNC-=f7f+6BXF|n|1!n)@vpdi_n`hyxa9mN zhiG;sUIbBQsAU%zgAM8Q=kH$nYHpTiUGs&9NHt7`sF~PsrW^UVzN$)RJ-t}vPMa!R z2&NXn5cA}Qacz^7RpOHaV;3WS_7bI5KJK1^qU;$Rk}~)-5wzvaENyUV;#Q@p+>{Gq zS#2(2jX#vY7R55~O4#jLBOxvruz!+wPAhk>5|@6WXVOhW(f4Tel_j4JyesDIXFF}{ zFAPvl8F+|zTVL5#`?3pruF0G6-`QsUt>^y%+bqnSe^z4lzbkPt;^+S035zbu4=EIH z4xrJim9|*!x#ju6+`BkRPOdG1)DCQI>;!JZ)fqbCq6p>DhY3geG}YngymxiMwIne< z5wAOx1w+sZM#u6e7t-1W6gzj=IbN)yzHt^^^2;h>jx*5EnSD_PL7cP_@tINP4a1Fv zQ$!E0JYTAB>8+0Z3t4N6A5`YX9uqk{Ta4{|S0V5r+u>; zOFnK;xFt#S>}YI^MNNgM{EW$(OJ&0$%TX9&EfPFIJkc;D`YQqb$@l(mD+L$pKSc4j zl>!iMi~cQ2>pV$nJNF6@cbs3&Z_h@lVP3a@)Vxw1u&RG5?FD z1`rnDKU7X8=6?*$^Rrs4G)y2964=>Q)T@voPxSD!WWVV1+ngL)NK<}Uep$KFPPmdw z=%-NIMe3Mg$}MlluVQKHClL(SyYAd!#Zbdh5vV3kSpANRbnQOIrTxthtbS}-qUk^s_@*GER99l+*+jQF( z2~PfMY=7E?{@vIB0P+7M$OPfI_ci;hJztP1sR!%a#~l5{==N=a4ORRn9-U0_D%(6W z1cdK473-|J>XzgH-J{F}W#;iXi*Ay>LFsZr@(;K|gxR%>LB<|s6`5+s=0}_-ndkf#ywz||k(GHQ9WFN67Iws0=Wp`7QJsa5 z-RKDe(NT?^aO`Ao8iAksu`Gv9bYRoZVB;}9+udQiX0$2l9za1P4$JX;=ral(PBr_X zBYQ)8t_c^zMVf2=2}LgWS0()8&}rs4vD|c1hb)3NHYvFtzbK3f0Y@4dXB*ErMYb)I zy%SV~?qM8mlC;AyBu=F0c$O=osWja$bPw;YE&JDr8e z;D)`30DoCcG|JEMpYIg~E}n_ipZjV%5~EKm4NxXfC-!E$JYnvdRy+Kf2;o};A^uAG ze>!*kTm55W`?LOu$MGWcF~Ny?c~La9Q*0}Lo9 z%Vx#%O@7`jIR)2xS*O$^wP$2BSsEqCVCnA7IffEQmT97eB&-l5c0J5mVI(xKVnnMH zglCvx3!N+c9LR)M2ut^AH|hC`Y*~o2d9Brm*7S9Lohz>vsg@5i2$Y-O7MD{3q+Va9|goDt*Cr(UTq^AA9E{9T4muc z^!b)GZ+G1Y(TO99dv0^5=n?JEmTD50rsVvtr5T={}SemCspjQ*AeL_++duRqLuK=k5w_#1A3!AZ}>!UbsdKbYD7IQ##hn#zCl z@(*bdu`<2O6F}FnGIg~C7!Ln(=Ku5g|Np%7Z(;mhB7aZ(wIKdqivPON{~y$U8rbj5 zpD2Gy{`FY?RN*}dP`1CDpn!XuKa~H!C;&s$%G}({+05Pq5RM6OHN$A}fB*ik4E|LI z-Y+N_E!Y6iNB@uw8{jtWAGv>R@$d3%fa|jVwU+;`*Z*D1*u=#DLRxI%?=oZ)XZx2j zHgT?hq5OZ53!4PzzqDqP5P8RNWqjY^l$;$*T;A_~0c*tj#pv&C2%r^!kZiy2lmMI3 zZyjOz12^e+4gjdm{kIrz7Vn??ABOwyngG67ncl6`-#Gw3f0&DZ^*6Nge{#pb^3T?50*;FBXN4BQLyVCANayY)A1i0C8fNE->B|e!tK0AhM}Z0Po=2$9@1hImb1QD7t~7( z8JXrCPN-E>@*(zpKd6i;%j524bB-3+m)g^&>O;@op+N15Vc``o(fE8#Fu>oOc5(8v z95cypFXIe{+OJ&x58d6}(k0#92nb4-(p^&0Al(g; zN`sUFDj^^p-;IwxN1nrZF87>!@9*{dYwy`Jdt$9Md(T?yJ!>u*93!R_b=Qr$_wka# zn($9iR9;GJTxrn1+W9oEADeh4(iGwsNf=x!CveO1?#H?BcOVjoFSi z9lfP3<(t5;$A)vmlQTP7O2I)Wi1>$6Axt1(r|Q2>*L*+yyD&`icP(D)E5+w_=HtViD~a&B_lD77ys0tcKZk7NhP>v_o8UF{ z!bD#JL+gl}-$njP!b;MPNrz4jM`k^*h@B(3m=(*?M-H>jb-&GFeZ~-z>yrkS*fZhq zImsXudrJ5Cx|Eir0ws=CK&LL%sDCLrsF+O^bnif6^-on z+ntfC%&3jsXNA#6Y@9mN?{+(gn7X$b(!hbbiSxLbSZ7Bj`8`M71G@STqC7NP8ix2q zMo}nr8S$hpE@FlYd;VAlGhe_z)hbaC+IK9+?IOpowG1n`!?1h=C#aA+h8}4z>%+hD zI={ackxqXb1Mg^~VPWR_T?JB$7P5sTMi4nlCBYO7l_FZ^Z9V;LHmkV4Jiz!E3+jk~2a?&m)CDnF!SBJtt(Aq!6w3I8LYH@xS%P(_|kmk|3jwtsDk|VM( zV5I?`)-cHFMOM*{&mMoKn8U}-+}&=zUdU#xZ+j=ub{RV{GxAvnAILepQ5lkLJcQnu zbAANBL^N+Q2c4sZ>VuyKhoWtV5oJdATYIr@|nQGTeHU zxl&7;CaX#2ys9(Qz>|qcHK^Bhiu9F2%AF4_LJlSQm&}Dv@sD(``di(@^&O(j!EU4K zU2R%iH|+D*f(P-#rWJrJdf&RO$t>F(IqpjTjg1}q;$Q(Q;zER)sAiqZ=oQbN>5`s4(9@gv2`^fX)JLw#JR9b3R&RyAJeeKDq?b*K=hAS(5VK>0qWl8?~@Wr>!Oy(Y%+?~Wk-)>aZxfKetn0=GODq8uZ6uF29z9Hu>|RNBf+{jlp|(5Dw1b zRVwarx+bo{OcO7LTww?l(g}Pd$DtL3_obfkM$4o;CrJ|+$cZzz-*Gm^U35phI?YCDk1@A`FtmA-f*E{ZnYZg# z7lq1=h(ho+`$#9q3CDfjVuvoaX@TkJq=hF9YX3gvj<6XLGBIWm?Ajid;EE%aK0TS( z{lZyHsx2K5-}&j`doBUL7=cw|2;-#6h!ZcD>JpwSgXD)Lw7wUg9tntSHutz|czt=? zO2SCf62%C~q?33XCGTiYV`jg(8u-ml2$QK`-M=oSWMaS58ZZ6CmqMw7RO}%NjJQEq zRw92Es=N8biw0P|H_q6a^jrEEldqd=fE38f#6GZ>-|x?T8fIAWpAtb4Vwcm9sDI6_6H{Z>vG2 z%_4zc`n{?%ZbG)-+p%b~su_%LhKJ;~;r?dZ`nyTX%yq*oDi&Y*kwFXqy|Jbw)`poklUpvpvexMz;_p-#>#Z3`b7d!xLs z12$PI7@Rz(BoT&E6;-j{Kv|^xGm-8_Tb)Tz$62GXZ%mX{C;`9yVo{M_gB;(RyqtGO z#AwZ>cB+lb&(gQvT!2BF%4*$4iQm@Se@}_Ot|I<$Q22`Z8IT3GCmxwR{J3;+<&skI zLJo;^U@M=QwAsyBb0suS64UoMCZA6l^_;*NXhsL?$Af`cMb<#ZCqxR^I zxn2L>T}vEZgjU>VE3xI32wpi1I)nCZF*!Z*ytA_4`=8Lw4U|niSTm~HEHs|Im!i^k zEip*9B7r0TpMU&WmqrfG2-w<&T0_zEO25E(ubNYlKd57k4I;4n$G z(G-TT6hf5J5>uFRK@{RM7!e$?RE*FW2CKM#5+4v$r+?3{_TVkdqXWNAk3b^J;aP|wf;!EL{ z=d?KR>S8X#)q#?NQ4Lt@dk$%!r6;pI@o%-~Ys>RXQ(Bl2Ru4^$!}MjMn%6tH6!xQ1 zPIVIl0VIZL;1eOLr#k}bT#@+ATo{sI?20{43dS;;|6Zm6Algm2;8CrvsoPRu4v zKJ!uo<05?fYOT&uxlP&E=BcZi8n0JqlPSo1gq88yYl>n|ZOg6OrtoA*Qc?Rv0;dNMm**$hN84dkD} z4v5u1O$w2UD}~8Y%}YHXrO%O1ykr;pJ{NuwNFykdwJn_>Y#iPy>sig(5KOMB%Uqmh zA-XV3MqlqCSZiPY^4l(>v-cAy|CBf}=q|DpC+Id(ZzG%Vpl93c(D|oYLZ44shT$hP zYQG_CS%4=YmlRiW?y}cSwY`UINl|~Ed?M_jZP$=eYyexxJg7weu7S8dEUCcuJ4ak` ziyjY+-g~t{XIU#MG5_|XV?KjRYsPFs4cObLbH}AFVD$f8IMiZg9RHw>FlHi+pm4@M z;JoZ;xnf^Vc=u@o^>Jllop&?!l03q=>6b< z*LiaFtTF`jN1HT-1XO}*a;gnTpY?pBTk7Rp4L1V z<93n3CRI~ELi@Vq7Fh3gfKu*?ESxDKAS2xNJ$aa@=A5ty${wDdsR1SKdAZbMs%?XC z&Bl7iVLQcjUc{%g5J=*(Zp%ySuY-W=3OjMJhbP^?OdZz=+caUm>YbgJ)+@rwZu3 zBD1NH);`+CGP4-MCA}@tQAYU6glY_+WNX!%?scc>v+)`v z+INpA0`|dW?D=Lb5qjZyW-A#b-8sd-a^~1-B~eFfUrYveu+50+KXt3tO4tV%>xr;H zKZB)yr_P(g!w@m@_btKV3}?^({s?In?ACLIf5T6^KgG(m$GfjCfLMoFqM{x#twFN7=%jkE(tt1X@(v zh@fD!*z+G;IioAg1zD*3X?%EaR2CJhW-%`AtQtcCE$=mS#Tiy0k{pHV#JboU+vcD3 z)j@DBUc}@buK31hbh#NM{sI!)mve}Bz*gLHN` z#U33BIvY9RH0+^d4JZxposIh7^>6Teq*f@mQSpw;Vi5BU1>@Sy48S|ZjxOOCC&apF zAxMaG4FbBdkzXEwKj+Jh53N9Ww0ndg>Q7AE*EfLs8h+st!c)R7)4G1@Hd`e;kC6T&Nm;>%0_6{ zSe=KE;@#4%)|%JjUe_PFBtvlRpHftLS2w{s4c3^M)_?OB9_H&kY^8Ha1x>xX_&%1C z;Pv4>tk_Gp$H;jjGg8wwd=f);Jxk6D(hHhff_VY!lizb+52owO%cLp%GZ%i7fCm_KClB!T3DDQZ@tgi~z}$UT5AbiEz}sDg*H8b`LOibMv)7AOF1hGkvqz@2NW!`Mvmm|NchF+ z1rE#ki%aLf;}H5)mIJ`F|K~0{4i@(RW-T0`UoJ-u(Er3ZevRkXl>@*v-W(SPtLR^z z{3?6bD+k+MYTy8{ApdBYgZ;1l_*boQaQyB(ibu3|J`#A&i}NA zzpMR!JC8&7+I7hx{IABqAuM*QBl8bW;k7IAdZzs0^ZStkBp&p`x%DFjNG@=E`%f3- zwN!$bf4L{GB@@K_%RPB5QP(>_{%}@aOA~N-?XMId86f7LUv;x<2>^chzw!VH00DbT ze!d5A8T7-5@S}Br%OI9tuF`8xgMREq`H=^34Fv3y`S~6|1rW=heF3Ne0yfb6$^#s( zz&L=^4Ob};#_La8#lPz+1(3sk*70|m{$IU-aWY7xKh;0M`&V2F;yHBOM<3*|DWuHM z_>yQEnbNp=GKI3=IpNSFCyQ*3SZ@pK+Kd%@aJnevv?-aTp@$B<%axd8o7Mp*sbo?X zTQD2da%NLd4HCBBM&~N*4Im=kNK6|3sL95!s2_>+Sf`UHk97zxD`zU!1Yzw7g9eES z7xe6G@o|c9eiR zT6UaRqMiD5hE&-e$KbHg1U>fnvS-!Hpd|Lx0cYxor3R-&VdBoyC*KLIzmL;&y(IcX zhFv4ow6lu4CQKtz2%p`Qyy7+Oj?XIEJ`jAKS=9{5A&UnlalV9N4+JN!h2-E3d>qcR z9VNrtI{GTm^)n-W=RQpKC3K3rnp-h+lcBekjc0A^79r(U&TT7H(DmQ!4hAdsoA6pO zRjJ4|CWOZIho^$~WG0PTUqMH3$lN2iwP#bPnZPaPC?&EK#;$>}|y zH+vU9g1Eluh@Z6&&-aiix(VY+@N`e!R&<8(?7h@>&h8qq5W9dh)b^)}U1HcoD@NpD zbg<%b~ z+Vf!Eq07e%kt9da#0}(dxxD0YqHRz?JQ_+^$yt4M?L{rCOt`O!bh2FfZ5cM6bVS2Z zWln}q8jL@AGEha9Id2kUAA~7B=ENS_?I-)>!*&1`q#Ek1zX4)jA}2$Rva;g#yY*I& zhuw;bR0S?H2y~~pXDzv2#?oBmfiy2!mHI2R+~pq88xKr2q}WKav^z3Gm1qtq z7MwbjFqziV&03d7oHVo6(N!nWA7ziBx9)`u>fz8K~Q3!Ugr~&)iLEcD&BGE(!XrVlVKJRtXpRVTA_bh)!}9ncY(4 zQJ+r5gzfvA_r&c^8h;bp$B*?bk8$ZNcH4BONzXOa{A$^t`bH&tb@3~FUciSZjW3KR z()WYioaY=xw!Nl4MsJAHXINJ%(|py8QRu84vA%$&gul198uPpyQe-f5o@n~q2y6Y3 zc&~FWeE`mk?923r$nk?6QAl~S^KWP%+BtfS-7TUgcD^Ai!X8fj^D6#&6T+P&m4)>t zDkDb7wwnnCurwJl81z4hc<41h6zCh-n-4Esyel%gU~r zN?$o^m+Yo*B)Kw#ocF7Er6MHOwA~UBjJ#^KJFnV)Qiz#djSEf)~jk9Gv%KCx6w)`?u5s5TYmkJ& zXWZn(>R<<&-eDR+78VZ&QoBi&I<<3X#z8n|ju^t}n`XVNK$#)UQW`m?c48A>ZIzL2 ztb8E+vt$gNoz3;;@g#j`XLX2e&#$0(?cfb)RZEobRE&vtHsr(kuT@>BrKV^ZyV(ut(8?liRPBh5@uPa_idl;F14+qzJ)hH7`WS(XaY!x4h+TXxF#q=7(d5C+( zrMj3>*5ks2YHoSeL2Gulk-2OH`{Rz_@MoEmR+3_j*bgxsz5%otPtS{^illry_{Tf~ zefZ*g;R(uI+aZy)^_FclQEEYNLk}(W50K)0DOv4mVgt#>FnWmu)vzZ zoumhjiRV|z$|6uWT6Gv|Qo~%FyKT0x^X7d@-u*1i-aJ7c@}xM)iOHwakrb@M0;g~e zYf0T1)hKE1tBeQeG@`4+dk{7roj!U5t!>q`ADxZjt-xrVMdk$sC+!qY6~$hiP1Gs=EmR8Z?-4lyHZo?SJ`nEn z$Ui*|JG`9h7!{9OkXgYprXYFX+1Z(ASQNPWsUJFS#jwO3(^ahr$ziTs3I3}mrG=D= zO{XVe32yw$kqX1e3pm@2um1T>hOk;0xD9=bD~K}X>|Xd+dBcJ`9nub5AIeb~w@&*e zR&&DphGdtPHj3QQXvo5PLzu*3WUfuGqDS12n2HaH ze7U1k3h7eM>?Snhl3u1qm?y^rmTOqtx5`3O9J=GaCsCO7?>=x_C47G)9gD4ZMaK>*!wruVzFaR57X^yh0 zt|c2Vls5ai2u-6dvPE>Xo|p*zJ%3l-Z6CSA#_>Z?76O-|<_^x*N&A^WaO`7ek^5`U zbwrL|Pu0x@oA@l%yzKT8+DGb09O5=4vxWB?;vw(!8Va&k@6D?#GD@@C$zt-4zz|iJ zfzRi)Y>0Vx<)yo7<{c&QuK7{iZKV56rTIHyVrIE%^dFT&>LP-HTxx!`bYD)eyv>Iq z?ve9$>uYI0lOu9uL}X!6K_b=ZaJ(VNruJu{H0>0Povo~sODL?B@-tk_P?q0CU(QO& zq0LFlVku)RMHWxg+|djBq)d^D1^LMkLwrX6Bp$7l zTafX#Vcb=#V!7c(=2O<;nM!uC4m#_?~(yVU)9_Dt)JXpzcU4qHVc=9{@ z^(XJ8#5o6su*!8zkbS{~{goUjPF}6*=?~%@Ven?dUEoN4T6)=Og7r9zCYd!_mK-}a zJ3i@w)@!E44JAo`(YRE-N(cv#vyxK6qE|m~8@6$Ol}GY`5@%^^VFB0E)ixZHkq#1$Mt(&qPKd!E=>$2PNm_ zdB3@=@!YZdD8*6umI1+l?zg|^q3XEt%3g<>w`4Dl;| zjEa>`C8M@FTE($|c7$9e6PAR?AT1W#PWZ~_D^4{b%Qj>mt?}mV_Yj}u%htId>ZPv@ zBE_w6qWUmfo3+mo4d_gQl9tbLcDcK?Gp3+SA6W4Slk;d%EiyQ(;BAvoZwJT~>GsUWgLyvYgEVXYx{jz_>QFI4Vdr7Fyxnq zhh5g*d7~jruG|lK+!5LhCWsjDA1M$ud(Yvf$sVEYjA=ZOkEx9|O2SzTsTS)deA4Cl z%x7cwRJo4mh|d@7O2Ga$^aSf2m-ryI8*;A}D|P+l6TANM*}qJL;8u9mP-*|c4_DB! zSMOW6Kvnq>5`J#yG8L0Uhx2heJ4L%;?(U=Kx=Ysir)}t|SF=?Y>}CPSm#l6IiRMh6 zNr6l6;qbV+N{OHOkPqK%JG$#%r{V?<3 z43m^hT5cCh5E>@n(3%c4{G3ts_0n^5JSeO@+Qavft=MQD+BkwMZAj>SGgyyTV@ycw z^I>w9#W@n|sbVV(F_?&RsSu=K&|JyW4@C1j<`c!#2D!M)VN~OIAIp)X#=$(#kKh;M z>ZBKir!CxT%CVJ!!YP(lNTrg65SzVMk*u~-!aHtb{7`j~=ba)%I00r3%ZrYux)ud| zOvtVb?TByBlTU+X6mc7tQQu|R)j>?f^{p$dZXd~=Y7Cb3&}k`?|DA{Om74EEKK2ct970zj+4sWLhpwgr zzG%z^y&nlYlS1d9>-s)FI7ZBY&OcS)mB}I9Vy*mr7|?ba;>k1>^o zJKF=h*hNA^JlgS=sojK8n~Ilqn7AG?)!A4l(ftL0jfk68>&ZnG zwdgNV3U-t=iAXF6=AQiEI?B+zRaZXgh%15Sw5#v`ED2J3S-LETng-qEHSGdH!I@kl z|1uBF<5x&H2+U7{UKw^cJezcklN)Z_su6#u)oB$)r|p=)9|N{d86P6hOGslFX@1-E zxT7`%?2q|ZKMe!H7soNftJxbJfI0+nyyzzxm0?{&C^UbqR#Z2@o;+AhttHn|Ez0c& zI-y_ItsNpBLmid#Mw&5Vp9y>KyP|ZesHd>W4|Y7s0~d@0#2d`vcDCU>TW;u?DSK0Q z>d`n$Z<%Xx67q$AucyRWT+_EUoW>$NYWdJCHs|~WKV^QG;wK>{6aUoSo<#V0bfchU z_?mI4^%VB^5Xq;|42ngJIt{7D{xMG#s;;26!ngNeK9W`?~@=PgA^ox8_DmgezD&qIe_&55aoaZ z3W;eRBJhb9U+j_WcB|a-7+I3s{<{|rN*JaT6lM*z5H%RElsnyJ!7AC`3U%DSAo>rXx6QU$4e+p4>Zh~gW7})!&o}B%56ify;*UO5|t>5 zPV=8`r?yTZ`9;jG%F>#PI0j7nHbo8v7tBgU!1-^SGsOFM9m0?kEBEXi+xq-GIu*B(>yBC*=w_q@H6UjXVeO-VM8xrOrg!~7%EkVZXT^?7k}d`^OAwXQUXkI+kWL_d4#qflMk6wE8@+WH>!UnQl|Z*<2ON{jLa+ zSwbj*N{*pBt~9f#%b7#6;;WY#mUo$7Cy6&y_o(UDWAH*7PG;E9m@5OQWsLJ6ei$D^ zhDw?3ZdLuk`DVrIJlM2%+#5>C|&oUy{qfXlEWyJ(^d|dh~o>HoxMK{P8jY zr=Z*_bOi5_K>o@ATKydVwlErlwt+*fu!xCLovqX50Lcc}I(@|cGUpQRo=SVIlg<%h z;y~vC{%El5uIICUlh#Q6V$)EF+xT-w)er=_VQ0{d9RZ*?2t7NzVv!43B)N*APouFA zLOL(a7YLqE9HtEc)NFT>c%9X1Y?2dgkZ(kHf3Cv5D?i6ZgMzi!W4I8w!A%znIA&&l~H zZ$0$5-vNi1a?wjV8nj(UY!{CL?<>j>v`R(p$`-WLrAKOHH1&#e^de`yPP|mt$DbX? zHHGFh&0$VLPHw8janf2kBL{Jec2R@TZgOREL8Z}UA{!dEA-p#&GLVw{)_C}Uio*Q-P+V$9{wovmx6yu-qxo(6#46Ge{JbH z{l5w3`74Nm9oXjlpMgI9TH~)YJG1!zV9o4*!g&6yAM^{j^6SapUD*MP0HYZ@5b^Vm z(XzAt6%2G8*Yn?_1UnGK^WS^(?|S_IWga`H&>cp^ey1o5>|7%MqAqr!Yi_U${SHU^ zebnqi|EX-U3;(Az{NA72?%73u$d%|Xx%yRxU6lQHu+R^z%r)%hdgfin%liXbb3I!? zKOhu8@~+`QKit%Rq^_ga{(=r&OY(J~*q`qK5)A_WK)+G|kO&AB^eY8`cYs;)GX+Q~ z2nbO7GX-!M1o{OXy5=OH;Qz`4I0l4?{gnc^1o~l5`_UJGTOgp#pS1uS0{yTc{Kx~S z4J=B3sI^o8|DUv2=20OZr3p80{pAFecKPGpVhmr*JxmOr<@@d@c5S#fIW`~8)LqZ+6bsR=;0xfHPBb9v z@Qm)Fc4Wd#KbR%Sl6_B-l5YIgo2hhs5viw2Z_b=Fl5I<}H8E>QV41PZ6+qgO=&v1qdy42lkn4- z;H=2BJrv?2B+;RsNQIoT`i%EMh}66h2py9@^IEr|MdWqf-ZtF38Ut^7T*Q>6Y`Xyb z5U@pF@Sar#Su16C=CgyVDP<`$Ph*4_lES`JG&HLfD(ZLX(?N{b%8{gRR?ME;c08^- zw&XOTR1Isx$MJqN=YWJZ9P~X^9raUiL#$P3)FM|)Z)KYy7xlIjF^JNFOWy@@GG7s` z180MbnlgyYlfg5uh=iiP0g|v8!z~6Wj((8|YlFn3OB2aX`8g^bcdOJx* z6m2rPrAaEG@?e(8oQDr3XPH%!-hR`P*f~1lM%d(?D-?eodgLB`1nM)BmF ztL?O{F~^$tG<~+fZWo&TU6RVQjt)Kztv00^mmjg0ieGX20;vglboXk#d2-}b&+mLx zYw|W4-qC=^%5*azXsk@+>QAqaf#CV`7sGKqnV9Kkc8$`tF(xrf?YW`dQP;-Rr+6v} zCCs9YgHg5E49`3UuGqiy)T2J2kl`^%Y?@{99gO}6srg`8hA?D_4x(9Fp2)I}F{%C@ z_dU3c?xuDFjryX}Sc@+K8ng|h>8}rZbhOEAVK5r)kv_?RD?=gT`#0sGEzO}f!ko9i z*tbeT_mv>>2onvLu*3S2?~~KWXU;`lr4LRM2CuhslD72*ku0CbR0>Bbt8IusW-H?@ z+9ziQW>ube_!2y6p5j4!6RhZf_zVO zONrhWb*Oo)%kN)IDW`e#z>|0~mv}~GvhJYpVoZv(zWubg)CR{cK89_2DwQ_Z&|&Ai zE|&C7^Q~c_y9W&(v0E_4-pm2JMf=v)1k((vy0a_s{dvbLVH}#P_MwuX)vVeW|98?0y-Ic0ySSQR7f%#L&{$%DtA{t?DTs!INgfr9zU z98t4Afo4__@vrm(37?_Du<7 zHtrajk{r%UX4L`t_KCZZw0VFkdX&jn=n8Ah?$_W(Hf?@S^bseoE@Xr`A|+e*JTmBp zpe|c%bwuTvz-?%`Y|S(9DiiZY(aaKitR-X@M?T^zID+zKl0j6r`QqBI+*b8ub!hYS zVDLFo2)P{W-p;WFOt}yx^iB*6FQ3om(e5$?cl2yFiLNFsJ;J~@M#}MLJIqxnk6){3 zL|n4Ngb{6-@?3hvGbm8L+D~cG8`hSAPAu1Fy>rFbaI%2UfLoQF=m4<*qxre};7Nlq$T+fGy&kR(i8CvAatJe~Ru5B@0lwvBthg6B>@ z(3|^d)dq!-o&e~FF}_!c@T^(%qlf%Gb!}&ML0CE4dcg{hO_{2*5C}9`lyGXL)mSMz zJti7DEIwyoAJ1{)=koEc&=WJ$S0JyfDa4(~V;YEHKe$bHeluA7-5TXDP8T`l!Pj^| zIG9E_HUmMzFRPyHFpkq(S2&Is9(@*^?BvSXKp0*%nWJ9OO+>M zdr+z~k4b0f8{f7Xd>ydL>%k0oM$!0P`g8d`UK!SJU?Ys~>#>+?X^Qcj5DLTKO6X2x zfo&oz4x9L@o3wKSxtgOika3}5;wf;O%tOS10^WL7l^+SPIVC>oxKo}r7^&7{Sy9q5 zB|<`)Cf~2-kb6ZON34j;*d9;hl6IArSeT1=M$JyYhxnzi({+?)F{rZO#jYUX`kBF_ z5cV@w0oQvf)TdP^N2gUKecja=&^X-934~Y3{$4Q5N2SK77~vFT_ftL_vMwrgh!eal zQbq_Qj=3M@T34LgNf{yYlms5?=~v(Irlq1|5~xDoNst95p@cxz>Oo_|O2=luCTnkt z*^=&Q$KKg43iE*9#q{~nDSX0&x5#a*xubQDh3y7rMop<-4npX`nH&1cHc4GK4cv=WEkianu!K2AN?%#Xo z^^jUAxRI{XO$MPpW^b}_YTYp~{LsG+U${Uhwd{Q?*NU?7{k`T{Ih}9K@*&b5gO8zm zAlnn{D9K9l&`hDGjNzEhj|0*XTPFR?pOwD23Q}7umS(9Taf#)_<&Jqq@tg%A9Dk8~ znyR7Bz>&eV42St~hp5{+Lp#GWV;gtuDn}_o1WnwaUEdm&vg#8IbJ6>u;oo%Xd&yqe zO`7j6k1C(+b?uY`7QQPnPm&MDKH$_lI#jn2@wavS-^n)+Iiv_$NXWVg~k~UZDU^6!>Iqb~Q3s9-saPZJYfEc$!j2iV2uQYKu%S zO-sbIQcR^|mQ(5jOS5EVk0r5uQNp?~s)I+79zfE!u{hUB2wBz(kZI(!;!GW5_dmC9 z;)RYEj5$G0%{UrtB;MOzT7H0UZshJJ5nC4ridB`j(s4kP$*p9k4DGToOr(a(Jde>| zZDViNze3X1HCin&ql@I9jyB+&eu?f|6^PQ|v$6R4O>IYLGpzVyI55ebRLR}@6CYj6 zr3)2Jv}&ZpqRcqCtFXleAk_FW@n}BBECR;zvnU=h#!aBGai%%`>;DfugdfYu56H$ z*%4coT^xNK-|**>_ZAIqY8d&hlz^e6L$gD&N{UVXnT@q3IspE0BN6@HkgOeM+Am2XX#rG1o2 zlgB_@7f&W#gPYadmd>22I*=Mn)zMCwAFcp~ViD2Bjs%=jzkJn}0T%0%^P_;QUhtSq#n=iV0 z%AdZO>HYS=l<qp7+n}BOBibj<1AlXmztl7m|de;f%d2 zXSalbA%Prl$lZGc!l6)Iwvfg%4WoTb3fXRu<~{HK$f`O!Sd2%|DbT_BB01#%ztQc< ziWk@6&J*m^MTwbq#&N}^K2IH@S9lX3J9Y`A3^UD$Ik>0ICloy+-@$`#!tW1L!9W)oo$aVs3?jwq2$tcL@4kR@eIqk-osUv7PA$>; zWYK|R7K&>iLw>{PgN-o=#b8HMi#DBBrMqzGKvI)s3Wq0xh;{9|c~?PN&9jGnVg^#* zBOI$x&A}1T1%~=?XmOT$f{D&>$?`q(kh{HQUfo83yQ*D(QTl-?{{xNzIH1eB1P-sa zCaf*^&9%RmnOh)h;{d<*Kf@=>q`ATYpU%F~`HPVaHq|84Pb3a`*c2vG6Ji@U{tTd3 zUSA>AgaR1R$I>8ULQCU2LK$iRuQ-3%ffGUKameV6s#L`VhbYkGRGXvSj81?G17EA_mWm z%^_-A?6(-$F&T*`;s-z%4O$$w>DoUQHR#E58EF$7eqAEwXvsmpz{J`=MjvnX(fZ?w zQpD4Og8>rb7=$zf)=yoCBcnPjP$Q|8>6ogxiyM;pU7?R1xif_Hl0K_krpTF_-A0`| zsxiPBS2w2KcGEFqPMXjuA3AXIQ?En^6lfX@BsEe6e`!{`Y7Yxz zI0S1$WWj6=@_RC)XOBz>N3tMWpPsv9dt`4v)-O;OO#2oT8!>z;?nFenLMfi5rH?czB$cO zMPel)mJRg@yY3+~-!6i?`@$}-`0`PtPu;~;u+M18Mk}km;y(Nln$zK1Dx$oM}32Z?S@9E79$n;^!g|Ot{}z-)U~U`>xJrJz;2- zi5`EZ78(q4mpxtXVJ}o5bWZIPF}EW*%(RUOdf<{F9~i0C;Jm^vO7WWm-Jam;=gsovs3U zeJA7Wtlb&7w}{YbK7tV}oQsG?L#%KQVg+$ISoOtilF*+O9U?5Z@ax{ODe z?Hf8wtzq>MgRXa?fxB%E97pVb=UEnEK#BO-|j9W9*eJYcu&D@lEfg zKV>|~d+Pd(vfXlFx#cUFca}VnLV{IjU`5zlbaGtU1UiZpHsuat*&t!=4nm=k!#CLE zaj`adoq{`JcyP+d`0SEYGW)s+R4+{M7p+x zC=96$Vx=ZI-GD6%_<9!eUb#^M7_o=F_jJ)&jFF$<=PNrq2M>}V)G#l4WVL!2(_?R| zR&;T!&bANpazF?Af<(r z^h0CZ!6_ESbIP-&)viVl!h1>)RK_ESslzZ<$&SQ#WcrwWdjjgPHT_yg8+`IE@>=*8 zUoM!AcgT#nW*ydz`)R(mvhp=B`ov7Pf|mCo^)f_b&Puz?H4%h4)EHF`O@MsB(NU(E zn3;qg&HB)Fo@s!Qm*GW;?XqXKL=8hFVxcr`c!?A93z(xX_{$9j1D+FZ!r7n_qxK2e9xQg?C9X&>4*9U(1p7WiF=7zB~WKO#%r*GP3ZY2 zQvm2<1pr-?n0^9X*d^FpcF}(VU2OPA5PROfO|-)jVx)r`i2VU{vC^Bp2D;2?T?1X{ z{{XsRG$x+?0J^+}nEOl%A%_04_mG(yESt<`6|&IkPC)fo=L`vnN{{N} zbxBQsPTSiE?&xiwD%J;!j~!+nm3qQWBErvEwxmiqf=6j)q@kaiJED+Y_$+S94A$`> zSIg+pz;|kzNIJ=?FwXDPEF<-}hx`w((!RAs`9x+nXTZRFCvaFfeAzBq zXs{Ry4DS74Kq125>3fe0H~W^YDZfph{|l@O>pumd0sNLfgV1o?3F`ucCHUbjyffD2 zCye3-n&L*{A0UeV`0>A};2$mgZ`}Q8^k268FEf61<-aWOPX+(J!C%F1yWY92+g|?r zXMa}x=gr@h|Fh8b&3}TBVHN*%8p8EatN)DKU;|DD_~)m$m%J;&<@(6iJJtyV;`sZL zY%Ie6MTy(``v13QZ0y(g95(j5^5S}YY#jf|DA_pf%FVm9!Ui0>{0}~|0mrZc{R2+C z_{X!m)bj6?VH5hF9LK-Yga7V3Y@)wQG~2DGmHfBs6#JbDY~n1pqj7!!POh;l*Ixx- zGuR)0DzhJlF#o_`T<2YX5kPFeFfVLO08$4yy7> z1pw}19a+7J?!5_ z;{Zo!|5?Z1dID|+rB_pwu@wcpq`;o+dm+(;TqCME40=1QuMJOUH{rrKYmKpu4Tzvl zQN=tm5eUbHM{O`#kdzs9Y=SQWIV5Gb`#V_M5qO8~5~?MgQVayF|zTk7pgT7&w52XFZEh@x1>rw_O142o6m>u zm7akzpL~{Zy-~K+OAlu%T`f8_m13t<+=&LiWI?LTdRc481Rv(0H!K?&BedU7U3Swk z^*P_P`DS(ob)+L#ddu1xI4=B-bh)v%R*MEq`@{%=KhDe;AcRd_SIc=cdS>^9DNX(Z zO-2A>&krT&^j&5*5c#_$_amy7{0AFx3_2tn2k`_c4W1XqW47C?!OZZlW)SK9s4YU{ z7zV;|G{Ww^bC50TJv=*ENJq@+eIN=33m+dKOUV7|OhJP{q>G=^kef>VtdA$&z_0o- zg1E03H4{lk`-SkEM?I+Loe4`b?-ZVai}zcaLLL^uIg>|Bghe~lkOb(@^>2Td5h+OT z$lWM5yT5p(TX#O@doS^g63cBwxvTN>W{^v@7@^2tp+cWSBtG#_P|x9Vzw%q4NdHPk znIH^=&`L~7bR1=8rFtY;HxX4mEI(%}-wF>sVa-pV`td|<_|TR?DkbqHz4fwAfyjVn z04_tjmsG=6!dS3gEN;AkZ_Q(brwTzf$9haG7DxL+PTaFn%V0H0yfIb@Sto6_guD0S0ZL^A( z!H-5iUm3pD=)?u>rC<3s5?d}Lj<&xh=d!o@f9$;lSRPB(EgU?!ySw{~yE_RM++BkO zhv4q+5L|+Lkl+M&hadq0gg}tsg#RTuXC^troMGh7cmI3uljjLtNYoC$}RLiSxvBK5%B(bHg zyydUqch0S2!Qu)Ki{m``U-Af0kevaB8Wt$U5)lsl7wp%$|Y%n zt8`>SyGp(q-pFZTQTROj6><7`{e9v6Ky|%;<*`(W6}pRX`yMt}X~d+*`J}CcVd3hz zz?^*QUgj<~5o^y34nWV}qI-47yR3^aksnfd17T%i{6mUQrO4;U>)tRDhag?6N|N+1 zw<(_Xc+V#uQ@wV7XG+2j7rW1~FiPT-qnOPEham_i0XOtQb`91ipeayN`xQcvxf(3r z$f6;IVvWNne%MPQGH z<1s>-HVXeP*9Uo>pkKw3D{3Z-=7ZR1E7n(IN}PQ01eG@dbcn{e%9|@PIr_4uZ8{2s zF+6=utb@;AjhMbhRZUc~?ING+g>%AH?L}3s{85s1#Z_HJRl&H#KA(DTYre9n@(qO6 zE(L#ABMYhvF}1v>u-j7QMzj7*(9#Zhi7u^0q)AdRXT2zPtJf z2>P>`6*Jc_GYIWbAs|%>^mD#bD2ek1-F006)FcDr1fzH7Y$zB<`l9@Uy@}u?%HX+G ziO^gWQ4Vuq4v?NTqNr+2fd(xELBOYYhiQ}4b&(=(uCht0X+^MyZPPZ|5%FjZpDo7Q{>qsbDs@HZPoB${wAN?Zw$05LG9TCbfX5m}ZB$)V5Zpk0I*UFeDy_ z)`oP%E#-48M}%VlTs<+pI`yc_D$u5hCu#4{0nxhgGy6oaksAq5f$9DEo5l@ng&$o& z@1bDVtI}a+LKpD)ESFH%OTxskWp)=_J3%~$()?w9#QjfiSykFAQ+=>N_hA&8NC(%l zL+n;bV&TEc1VgeLspDzO#+P;?_Gw_rJy&amFZij{DNk=6v$)_JTVX-obULcCc7#jZ zK*`SGQ{^c-^yaxN&K9F(z#-PQebu>@iJy!-#|PWM_U5HmT~re;==XOrV+BNSHR}{zk2kcab=&>nID~ib$quP{f3&#lzD5Jo%-TgaGj*RXLNTqglm)d9KYh;|Jv+33 zC2Y>w{T!7kp7m_OWhWYgYnx-$6m7c7;AFEJ1@tS@g@~!{uUJ&vV|twHc!795mNvW` z77hj;InE;~TZD~Z1zzh67_IzR&Gf_m(Fl!>1jd(T4u(vdnIT{-AB>syTC3=Dd+m`z zEV0CP#9EPUNU)1cvcub0c!Rm(?dy1wL_4M?6_prJ+_WGhmGDCnz-;4P4U@IX`0 zA!}8@XfrdG-PkeV zWp0xVHpN?OOUf}>yK%M+OE_0H6)44cd5?x|fJX);)Y?aj!4v2jFpM>LQv%^`Fq#&v z;Tg)34NDQ`oJh_WL`%6htZKI1_#aNgYFXLg#|MB>!3D+*|CNI#hr6WdQ$rfydne zNdalZjXX(P?HxmONANLkUfIFoEjlqH^vA>2HTnCic5r1d-GDTV8=3LzAy+Sg%@~g) zw7yEBg90IisT{yF$@z4uJ{GmTjEd{HM+c69bJF9JRg~*a@4CnN38|@y|7HlAJ%>;v zj4a$UOL*wKEnKNyu)mk$8^%;oOFU*u@}ad}?%q3-?01k!NX>6Y2RnDmMOD?+k8FAk z;S4BkI7f5B@>S+xpR~+ce(8Z@PLG4Fvy+sRm|etUJ-4|9c_up8ecxz!=(>{Y7u_mt zp#KOkPe1@rL62@yOq2*38bz1{oDAbe!5s0?X|x7SFUyGUqO+A5%?qEe5g+;7T@XSq zHE7L=U06wbF^1@;E5*JDt!-;NCQ11yPU&CcSJ`(8j#Tnnri4o1CYifRz z2&4;ct%9}OcBuwDxl)^V6Uz*KwIwWp($}BJ!*V}es9H*5$Fa~C1#<2dDVE(tH z@T|bd<2yEn>n`&6VQdUA1M_zb=)r8v4^aLu@cToGKaTm&{r`uPKcwM*q00Tu{>K`+ zulc_;{+A#39re#_``*yM*xFsSe}`H58KY$u5&L_T2XNQV*zB*h#Qzz<|L3az)4!QT z{~>ep?_MIaDC^Ha`M=xTzgwSKl;OFpG+a+)s{t$N%pLg6|JA>vt&sw<3@}_>N%zC;}f^R$#h;AKb$C2Z8lFQ2BlD z-JoUV_??INK4@6K<0U`#0y_gJ>Hn9yz|LR=W+(W)DWEY}IezCwz8i)0`%>daT@hfM zGB9^M@Zeu~3(lWl!Y?}sr6O8C-BmxJ2D zVR2-*PzgROam5~q95voW_%)Eck$-_DHGBaMD+oLK(-L@*ONz*%$~%))b_eb>%{AHW zmab*Ke%t<6y(c3)$U4??<VX#tkXZ`p+CW6=F7sd)OkwwJDb(pc;b5y%y@C)FGby-l&iO#tlZE3C zw&J-96%}>Q0P3lK4|DuLG}qbmGVN8sFn2=l#)o9Woz`4KI6N-=L8zI|B!F=`ao1>~ zjZf(?|7|~LIih==s*DT?Z7GxSpygmp*byXwJy=DA|FE-uNlQ!+-Rfnf%69+$3kX=5 zXSn=$LD}4Vpyhi{z)frPCTH}~rGhBl_GD0N*T&1LsV;?P!eK5+vnz@taZ@owuhk*=dKPOZl2^ zU8P}h)Q@i;Iq!FCpkDFOs$-rS5>wb}Omm>=;7?nAM~KLmlqeGkOF%bzBoC%9DGV(Lo-_3?Jraq`2QgT2sh zL8H2+U~X+y4@7l(?U_DB&gzK6ZqDswx_1yWAkkK22VckgiCWPY3LKa+l#v8j{I!ODkAu) zs93Vjp6RyzadZt0p#kloN>h3TZRlxBr&{gcl^{%{rn|t~b(YMg#7iGEyoN;a48LOq zELioEDmllSkh?9tF-82jikt1heIczh3>{!kIz}~pDOw_Oxz%E!6m6F@a2(+3eR{P7 z@#Wq?W#^yt=)*ZyK^-vJpI|moo|0Q^sF7G}Sh5*$(lN9NQavTcV(o601>nS}ejVy95;E5FKfX8k&67WTbcHt&)r=aLs%#8{wi-a7|rNa01;R6v!7Qio0h+K@!9kC^P zC$bpaJ*qb{SvF3`0?clLOu3Voa}KRK|MbqUA9JCQI{^_z`F-`g-hwCDjrC5`7d10} zpTbqxq*Z4r$H+x0^@a^tOOORbE&s7&3+$4_vn@?c%4sh1Fa#xjLq5tH&qw!>rx z!Aq_CJ`yL-T0T~&VQq;h2E=@06&W4E|LEjC2XBhpI*EEVvDF@syZU+8jW+9uLBO9( zcJjV^|7nT&r_kE3V+WY|^XDM)kSW(Ctf5Ya+t13>=8iZZ>nvh%JrX;>SYwyLX^m>t z;1P0NZj@KXG`Eoh0N23b^F$3ARke`a*ule(nhcZlXGPRWfh>czW?MPNL-tkb=LqWr z@jUAFr%{ca>MJQ97kj{_d1YHx{jkp-^DsNvVj#e+B_f3&(8Vgvuf6-gQE(fJ_j&n? z2>X^xWYv=k>2t`V@V8wKQn7MoplX%7zG886LJXV`gaM=Od9SKTGP|u>bvn=1iH%^06a17qJ9j?wi4dmAIU3;^SLOYE$kEF7N{MP!i)zhY2z}?pVKpLv+tWHEhRi12?#} zJP}37p-14^Pt`jd5fXM6ax5j;D=?lK@DDW=kGheW#&e(DwhCzClg1%NqY{t5Xv^zx@Eu!a z0@9Mn$4Ywa>#(l%wzc7-+o>^4Y%=fm=3T#q)#k`lIAWmDkBz?0o4fFthB=Z) z))cQi7<$^1!l~{)`l>ZBPn2mJvr`q37OP!Jo3xdlTI{3#b!2^2K|`*!DrLSUEUU3~ z6rV>|B|*g{^a0Z1KI*f#?2Ig*mr0IkS3)XRK47M*IQ!pc@CSmPe~4Pq22Rg@PluZ= zElfD(N|7*NV1H|kQ(2WUJOrk(jPjTjHI93lSR7m2H-nLSIH81c@;S0cDb%F425UAG z8)C_pxOMikSB;?>eLASy$`lAa%#P(!eFroGVh-8Cs9PmZr6JjTSTfMwdY3?5B*#?6 zN}@udWDH#~w1o!Pv|w&c@xUSSA}nu&5W}-C7mX4Yi}CfcOlFg`eJ`uxmInq}sjjki)XY^1$pVbg zZTD>Qb55qwwn*nC(6yIyM~`(H{kr(gB0S!I?j`j~5{N&sCcDp+4@3-sGyI=j3hv%7 zOLrm_{NJU?gY!lvJBY%7taWj$Y5K?jL~7Q0uHy)`60dP>;-2jI&XG(7I?IICH@v#U z!K;Z@f)obQ*P<%MjSPpe`l&~6s*Q^{YOGl1>?b@)JPnnH0sztg58cG8EUY#0euXxX zB`Q!7=64HEu_v)|5p{dKXvzWL6@~r68!yh9Z`{sa#+G^_UPr^<9vJQ^y@wVMd_N0x z_R0AOnPJe^ThiME+{Y6%EA?y;($nclgds@vjr>zOaR)p0x+a(28e!JO_!}*r({ZhBHz=wwX|< zmaRWDCTfX06I`CxEv!Wjm7Zp;1$#TB>^7PDp>~O4VBXt)-fqN`-)JUqsK@UlcEL>P zyoxKw*Ysu4F?hhlD-}*5jAdU5Ze8qcviBc|{Yjz-)rM!c=Ng#wu=E7vbhgi5+&~OP z$8p}*c|R?6{_Nub_|;=Z4k$vQ2?YN?p|yCMh#r<6=S}W(Hr=v&>%_Y0<0cBCJj%DE zS2v)p`TB0Ha5E}$t|K>b6xnicNcxbIg;a4bUN$N*YR!fPo~>fddeoe%+%U;MSBe3u z;)eGN#2m|+o%u`&gSi7nbYPXtZeDhaVE1SrRqJg{PqLA&p6sY?eXL9aXkfDkcpU15q8#yK<$D+%*hN#18v-PH?>A8B2~wuaS-)@;mzZK^n+wO|U?=ZTSB zC4UtOZ5>RCz`~3oEuAHp_)KX6X^5MMOG-ABKnGAx)h3HIYA=mdJ|rJyz#3po906g& znD^YOYOJZ4CvuKd{Tg1cq#1h`$9JG)3!;k6gqTICsdOB(bn8s+%+8~f;5dZ^ixfJ-WgOZZ zJ`XA*z@uVRo3RT7JJLr@es*dAoAP2K)(uy(Q z*8T!zrENNN1N$EO7Ti^5MHRE_Dg3V92C~_SYu5}o+`uuIxI+=!D`9Z1M9}gwN}m!7 zJV_9HnfUiCVs>p?02!_qcqCx83_9jC?y43v;3L^CkPGQkgqod-E}9K83+kisT2Ez> zy;|J0szXg_fY3{!iBtwl4f(shYVt6RW8QJ>deWx;fF)=W77Q4@u}&S_tN_Micg&7c zq}=7P$#PVQPrJ$m#OS^81(-2%Q5k5jNLY> ze&%nWLxbc-uZ(&+pV}t+oMa@K;o@wjM91p2yf(DOn;3jnYis}L1F=aD2GQXQHa&*sf1uK@GWP{IUCd=}})MASX)17^>}m{lKnKq2{g8Qoio=360YpEtR$; zsc~`188Tw>DHO9?*{j}c;bAfKZ2=F&=$G0rK{noYr{6bX9=J{h{4pWKU)RaNWCU0q znc?C!+ikZ_CF#%NbvDVVy&x8VR5p9w^mcVY==xw3L5)mW!O?rP$;}sLGe9@lCWsB1 zVI)fmYqNe=!Lg}O8nJbsEF4J#mq}30Ro>vR5@bX(ehg+x(1C7l{YU9dPfvMYtJP3C znm`Yc8@_65?oTjMXgJ)R_3$E=xI**LqqaOCAasG=l6%=#n~^h#zZi1SdV?Y6;$^%Xgw`RNe48=;|X1@GH- z<~sWn>2db7pj531JjpU0B=WN@looao=XdQ&j-ov|AJtUp7I^2odPSvVJ9ZJ%N8TH_ zUkQ0c2$DF?;Nc$ERd8@^ZQK}m5z?v3*i@J;0W&aGA5@{-*Ut}J&;LRH2#BuQdCnke#R4s9^MXsVO6g{aVCHw29r|CnH zFrHd$fGU`qRL*A3B0SOYY_S9z7fLeR$bhWX{Zoqo`zB`5rz&D2AKH#=ncrClpx1N#riavpZ#Uum z%al))QS!@Cmo8G5x{6amh?fzm_jS!f;RfbkmIFWsf*(Pj7wB9zK`@~7gebxmJJFnb zB8FEW0A~D&#|n~&*H>JDFD4K|2>QIrIGmoceH2HGgnI52OV9cKcXe|{TYLa2vZ|k> zrf35mPZua!!{DBtm7_pa&ML1<-`?I{Ct;+;n|4xIoib3KZQ7UTuQkFKzl)nY@fsQT zqo2&}G_f)+KjtNxFIPw=T&fls*6ryt@RmRen~q3F8!Cv???zEWdgbu>7oL4tP*}C)aQM%)jSR z@Q+CDf05JvN6kKL|8FP%(`&eA=TDn{`uX=x`KO2cw)0Pq`FjmNZ2O-y{e9Qp?Ejk$ ze?8CLuK$!LfvEVus>}V4SW_{k|DBlMe{{9~sr_Pr-|25LU~2h)J|0nw?H_4_ivi=) zer;C_7pRnbcLv~@S%FcJzqJWG7b{S|{I}Ab#{kzI zR`R_b&?2nB*xTPrz$*hrO#W5^T7s49FV}LXR?hYPT7LE=002Y8|AOrQi@pQ^rU!!6 zfIlQ7RN}GOX1a@k)F$%&mN+_@<09gA`&GedqE<$b6GKd)7MO-reO$c+Fe$0Ft%UF* zpY_Yd`!R~i7s0JeDe_T7#ocm-iXBqY6MbunxH=rK14|fdg+7iEy-<#lReYM!gF3Bv zuFldtxa|&BUHv9oa-Bj5(`!3D%jpOf21_IZ27N!yK-l9&(3;@3*K$|uQ}NgJR56Yy z$lkUrQ=t;h*2Hb+#H~bSWA-?o-JF)pIW^zD`UaH_%J@h#NDu9!nIZ+^KEuKuyoxJ4 zgjWFR%(Fqd1v0FUhR4>+<(HV{H+!&*d)hh&8WkT_*05}`&po@0HIH2M1TV3_khJ@; z6ciN*i)K$5bXkftm_$&#dFA)6a$&m_7Z(Do$oWN+wF~}qAtxQ3Qg?QH0<-$v z8G|pVVxi2aI`Lw9!#Qi|t6xk+=n@uk{4s%7b3frF{w;{aaGUJk&g}q955(I4sB8qp z+QEKHFIH*9Uw`Pz0!*L|Anc(nMCd_TyYLcprazbc#^~^=nU(;~k?9x)=1fSC$=(_x0{W!FaA;Sbmjwx!6y4 zVJN`h%vG8bKk$$JQ>6{I`)@OB_Bb=qbst-V*q~UKNyRbE2Ne&=j+-3irp?Eqc`X_{oO@eTj zx>Uf>aY{?=a`L12%q;ay3(~(vm%a1^KupgU$kLQCXl0biycjK)<-Twyv(~@^*#J6V@VbWt0(Y%)(@G zU!TV7bU-kIUvQy*4t6mlz_9%R9&sN z8Ha~K)}{iht-B)XKq?WQ(PzjV_q_j60sDCa#<%XK{TN!!HXo=Nh;s?-w%7XggQ>#& z-omDw%4TbD^+*Rs*Q!dVw+%O-N2-bE=oRB>rsfm$0~0{ z$(y@SuC!wDgEA&!fyHeskwAYb-PGzyQHg;h_PUU{nJzK7tT#-s?QdMbKDSwMAr*az$b2^Wo0;BjLQb3xXtAX`dYo2sVwtA(pcp;2l+L z1mF}>9AG!nWyld#%|H-z$OACPG&WL|5vhr4lWkACNQP# zBj>bx1WbWS{(qe>{23t!)OYx`C#7sQ1mT)zX-`1F7G&I_{D^`^ZYy5V@Fkxs*FKA_ z27ckdIpo22Qpt!$Y}MG|4H_&CvVk zXRJ*bQ0)~|DA;<^2Fly0g-1kKwtk%;xQSgQdznz-Dp257LhPIc&~36Qu{Da(*7=_r znUwWIp+z5AM%8f-S&q(2Ntmh@$E+K-)lH0~!g|W@ED>Rf`Yx#BO))EywdWPz%*%4ZN`dfuB!Gb*+K2TQ;8(ayMM=sYn3%iy6Z*BcNw*xy$_M#EsI%Ky zU;8D_Akj()KO(9_EI-I$A--;EPh`YgMjJXGXTP0>vb?sXZL2+^5iK%9(Bv2_@50G( zvfTG_U%K;Mgu*l0=yr7X%>=9?@{p(s#eHh>d0;T3*A>Z7X#=RM*3uHh_iD38;!I5F zCDKSBTC8|3zJyuVOLs$h<-zJ?djbMh7^DZxidvpKXv`R@JR+O6KzJQqM`LAF`g-^Y zfCj1K7WJseGxNvwEa0+1|Z$8y(?W%-;WtC z8xwvZNxpHQXW~y18o45U6h<2dR%pL`Xr*?MQ6~IGUo^vkbo33~pt=2WvBIk+JN%&G zxCI0}buVqWzKl7NK6e1+*2(g>N8K6aT=%_RA4n^(vHi*jD#=un{tzL_F8zWy*7_*um*I>a$s2&?}mxbevR=&(L`Ei0Hi8Kv^veK3X@YIweD;$~fD(Tlxyd{~- zH>6zwnx9_trR5*uy?eIGFtX$rby^{}!~k}DMB-8p;r1E4w?|hdE8bwz`*dv>zk#HD4XShdYGZcyT`bpsl^bV2%Do<$`Jb1O5 zer|Gn;vp*Hit=b#wM*-QP2XZPVYXSl#*`G+8!Sfo7BB6!n}E1Nv>80 z5>djVbDiJF6CU~`PX7cZ_Y0COG-}9ss1B-6!oVYV|7-A(JygJ~AWQ|=i==2J9Jp{6J*4w685mtC!#+v962I-*oZb`u?f9bcmqK8c zK+Z7q>Ci^VkwkJ90Faia71Fm9`( zLvEwfP_x*uD;TYlaC&c=g=BhzUM7ZeCzeZ(2hu=s#^YoY{LUkC>~1HA1%+GTC_WT5u#zgQ)vbJ~N= zY-$-jT3GZZklg_MA#Zd1ZyvjD-#vCkclZ!|@py0F7Dx@RH+7>IOL1T# zp1m)Qqs3S9PlU4L04l*w*v+I6&?iUQ=9X50^rd!NJ{pojTOHi7V#zX9jVlwW=7rTf zk$0|ti{Ux0Y^1}DT3){t0t>ED*p%!t1|Uc1?ymN)^oa+|4D`(h+WMM0MmzUs*Q9`i zqe1#Q=Fz9q=s2|pR*!)M$X*k+rgv%4#kRxa4Ki^jZpg(7IkMdr=-dddId{Vf*~ZOd zsSr>>@rk`0?S-qUeB9%$izT@FN}8l5(J|OpZ9I$KIxwPmD~Diksg0E4&eh=O(hba`eReH%CMTc&>S+u6&54bt(&OM*Tdml+iO>Tg@v{%H06VvMy2uws z*^aN{CfO>Z?lX;Z96GonEVe@~hGYhpq;F%?=4c2nWS+|St8`*&q7E-dnggoKK!#UM zQ9PcheKllI84smuAs3~fV$^xfT(#hF?Rqupd1HNBWwrYJzM=C# z_JxK0mnDRD^x$3irr6oHQ1ybaU6yXvgNxI9Y=KSq*V0Fq?gUJvtbr(g8rvl1Y zQ{DO6P^YygrSyp}nL@vs>7&KsYE4;Sb$Z7|lS%o&njEM!&daieq&KZ#ggH=3hz+0+ zT{P=Un-5D1U*$nW zd|36Otk(JB7}oUt%X@6f{1f`}Um#SNS=oQ2b^R?wlk+!1h5f}uHe-#Ds&%nFCAImJv zbcf&r|NOngn!g|UpZd!z%<@O{9k}Tq(ExX6XBGx#s{H41`~TF1|F{I8Sl@r=5}1YA zMeoJRf56Chi1hcd^aBz8UcBqeAF$wWMG;`}cQE_A-2qI$quJkW{$qvt<6xj|e=J~r z6oK~%VEP@nc%Y9nq%TI6oNMzeAM7TjO_54k5UQgVcA)$(T+r1K> zw){|j_SAiEW5=D=1!tal_yw`RMTxk6M6_YyE-U0hb;CLH$6phk+AU3~(}y7QOBO0* z3FZ4NMrKBo0?JsVDa^%?@)RltsLD{!tilf4t6ld3aUnf&i9do(!su^`U!A3F2Wsv< zhlhH`k_TSdG1{w^K+nSPy2KEb0Xiu7d5CrqGbr3tOnF{r@dtUtpC+Ob0R zK!6zh;0lI6n}!%f_`r@P!4DWpU=pqLeq;S}!kj+V>sIF-0}AW1czlKhZT=JKmUXNC zGvi4Fe#A(9L5XF2)hBx2x|E(8aPS)Fs=7(yXc$rGe~L!0mXI#uEw^T1H@=D$cUD^1 z=N!eKP)=}jm#m>rc|kTsNT*KoRAwb-D$|3vybdOF_DD!AF!Jl-l*8DFZ_*ywNH)=TY+Zc)s!V+zYk<6By&wzA%k{_f-B8h6*E-0Md!4pK7r-R=W1Nfzc` z7R_?e3U~TaqPKhym~*0P0f)ScfUEKMw+&RK?97KDtW;fGrUS~!2&_ntEGSz!p|c#( zAUu^6V6VYLY59rIyO5To?2q5f0i57+OGLgFV(=zmm9ft_ps=dbcBj0~d9KQWZ5n_q z8P@PM$4%yw7Z_AF!lU@m@Xtt%_R;XreN(Xc@Vqy-ujdKv^3R-`D`+pQKQKH!kX;g# zu&nKE^>7-JZ5wqV&GbP=bW)tVSa#Usj=(3(-AouH3|x&2cE6G0*{vq8j*wZ5I5+{T zOVPV0+y}t?K#-3G@Jr5=yIDFgI9Tk=BOqzAC)j_dHtn%FL1?@fp~V6s(Q2L~s-QpJ z(HTUnd_F^^)v|lIwVQc)#5l-yb3=8{qgM8T4@Rbp$lQ3f#N=T4#&acjn^btOkZqEv zSLc%7$CiL*6Dnl*>gqWw)^|Y(ue1nP3_ohFkiq@rCAmcKM`asjafS=Rd=G!{mv7X%bgJOo$6p*VM<-cm43#H5a|Ob z_;;R9{&sBr{FeJ~OvyPof1Z*9Gg~tQVOlO=cso5O^WDr)L`qcoxrCIcoW3m)qm+@c zjib4vD-5IZa}^tN!{8yn+5eeizE7h6#hLW~%oX2v*57X6-<{h3 zCnuW!x3tB~!g}Xn0{rv$WL*AlvBdx6eKNCf{qE<(%p$~e|FYxzT>tyx;I7XA->2=r z6~%$Y-(6_Fw*_Q4e!vdEo)uyO?)|>(_{AB9`6sgEUtT&q5Q_)=ipBFO$xSRXJqF6v zX%VqP4n>W|$rpyrUOlIs_go=C?a?C!8RsgLRhP+RPskwVmh;*va%bN>c(;6{obO@# z2BbAF^-+>02uo56=`zFhBYFR6vIy55Or{HbWV^MGox;<>8vzhwy8POQn>=IMh@$Aj zflrwj(EWu|5ZFAT6{i>=-$-;lx_s}|7a&ElatXH6+rAEut%3UpQtd0gJo_}tlgw9q z`*MQ(s;y%s&&NDa$GpjD#|o{XKm&wYg3bqyAv;D;)t?=8_fmbsbnSyF^x`CodUYvG znq_=t>Hoz+e@{;!ofH?KdT*mT?5x|?c_n}?1*mTw&dDQH@|O&&=NNMb{}@_ z(c4Fujmumhj`n^<*q*kNegGF}RdH3Jb8oYri~MTg4R9pXZ;XSZ3(bh(}H>`9SID+NOGF%GiF zT2oBSCfCFU;qvI_Hy{4R^~5(-s@ln5)LXqd@sY5F!a2dElS50o)6Ym$3;S!igDc6B zA_6#^b<{P{QG0yEu9(qZQLEQB(X?B%e{i}HUf;hPVHlH!_EB1UmbTz zE};r=?O@;AlE5B@NJJmXQ%-w%i8Kg-^R(d&)E5J|!xsuC6@C48k&0wK`QrI!A4}_d z#vcop$0fNy|S-1ln_iCEd-mN!4KE`L8z2e$wzNs}LYu>?gUn^O}yYRi0ilmx7 zVHqCT#vH&ZSz1SIwvyd7sXO2^CgX#YODA)9<{qH3=QT}_y7C!u8P}-m9s{!6O*bAA z%l`*G&FJ5ie{vXsUOZrq1ci#~oM$WID6FXN`9Wr)rNw8F2+Vk4aOJQJ=Q#zuwp6HI zX|jF9`$(_6o7!bShwSQwaYIX~`Rszh6ITPn6x){x8zFj~7jkzm0~OEWL8hNMK+?&X=FGWi)c|i5 z8{G{5eiI)yNv{y&Cj2ny*pVior{(H-7Wi>&e@-dHTr7QfB(qU)z{y1HXBwW3 zr1}MNKEP(_h0X+@vd(Z=bK?5ugu?w1`b9Jk(KMK*S$&#}_p3M#O3g(7Qv$^|7w#Ib;L06{#ViU}H_SCFI@x&~)2z4i zB*g6C-IURUQXqI>)2W^-!XTT`C^$lC(s0HWb5;61GkohFZnRR^1h+Tblt+QD^a!R* zsyFBBAbu)zfz4X1gO*6wXOp((EHl>dM-E6sfLymq1ll45&Np!Qh&pox*92EO`=3^p zS{4YGYTuKzrP2%MVJ%kTuCo9>OMHt?wI9P#}l!mMq@j1P!r zAGIQE+FcOxs_OHoO%wfU*suYSb;NH*R*UfNx0W#?yVxXVI^T^)lI7z`?{p z0@sD{NITM}V7E4+@g&y2UH2uZr#-8Dj7!}o4mMjg^ZsDxGYkC_o&#n*SMN5;-Og4- z{@H|Bj~y;q6p^tm*MlAjRLb(AxZV%LRj?O>XWrM&SCFPN{TBC)ori*8f6(3ndP#m) z%eezujQH7YGYKNxVt`SAFg&|b&s>>};UMnRoY}V@6U38J`*}4E^Y0{cZP$z^?^v;) z_>gnn)|t9D>?Cc}*kuK6fH_B2)H9SsULtEvNf$j0<7toUhS|zi)#NN#i{15DRfb^b zfcKMW=LdQtlJ3_km3@I%!4!T$#beypK#NTfZ=?)BMlL2>!VVT!i;<@@H%6k;nXOTM zhG39xL0PXu*zJlz^cI|$V!8}!^Nrto%*UoeFW3z|_2xH2>HHeZ`OFuo#0NF%+%`Vh zs}m%DUE4-WgnG`)F#=0h1nD0N-*rSo{WP;eN}Q5Inr=i-+PK6vH9K}8e<1jaAeWVj zQM_{tL5R0GHJZ*K8?_1c7C8=^G7KDA-*Pbm)XbFSCnAT1<^}-`)Hx$syRVL2%ahGF z^#Wef5tjG$#{+>cR;FK)PTVO({Ln({#1!n*UFGG&`AXt>`#JtqE)#90ff9r2D$r@Q zTKq-jIDpbdT0qOiF;zB;gaUd3w};L>dk4(M{Oy{}7rTi>jZH8Ww;VAI6RXGv!xj=Q-BR1w`|>VTF~wv!1JkcjJOG2(Qx8FjHA> zoxP4ZmFSu5P)n)d;8+v>+W}E!-D6mX=1=N?h7l=r#)BFyGHU@Bj1QI@-6}7WR*s;e z==@IZv-$(^H&*6flzf3!|0#@}tS4$DF6SLd>utSAzj;E$4N+te&ct_7^I~!yIR}Gu zkHgJ@Dc%4t_EcFn?bL5RT`)#F;Sx47 z)`LS`;sqd2&jo7oMK(nxCT214lC|Dd^4P&z*xc5ZJ-ZN02)G)^FzHG0 zfmVHuQ@OU@sgM(C4paZmP)^#MD;UBER^3MPQ6JHXiIoF=s4GZlj=!!5R?s^(%NJ+y zma8kXjKi$4_f1h*9*Fb-Llu7ci~&0VjQOwmR*vqp1Y{51(wkEz8?iAmi^;^-0W;Bd zZYtnZ3yU@PH`MQuN4?ViwqT2g_#}lCWYYM>H7osQ=`>Md|Fns0b`YHASXrzg3)q{8 zd>HNX%Xa@;RKtR9Zx#}AYrJ;)U8ss@#ZaZ(MA{ZrdUV@~D3$~Exy(MT$rDEQk$R3XI3af4q*cB!>OJb&j> zQ9vv3BMSv2l~vIa2;u^{~m@ zKY88kAaE2EbJ-4%ffJLjpDd&?i!DO_ZIhunhw$9lR4sL*{D?ZTGdw>j_&nxZ+9Q%T zi6;wBS7bf!$b-Ub1V0JFZpX5uwT%XE*3kD&UN8 ztmc&Ftrtkoo=?r+TJ}41@Y|gEzX0g6u(A9M=&}NUfG#Ug2Al29$AbAmfG&{G{^xuw z?gJtJ&)xP<Xo zuEBY6*WeZ)xVuAu;O@cQ9TFh8OK=Eo0Rq7tLU4iw!uP`7``pcLPB*7-pT6fl-~GWu zt&&w$OX^*7j5)^?5&zE+`rm(tOz%bxfcX#aox7g?4IjOO>+gCK08BLcTj1(lj{@#d zs{3QW9s#iY5gL2f69AS!aQ(YC2LNU1f4>g+1^|{noQZ%>1T5W$#y-IKS^k92|FalB z%M*d!zho%{rbGLWJdW7?GuUb8-!0gJhi=Q~1Y_-OC?*6yBYx2+ie>tU$;hIzI&eBz zKw~ih=_#e@ud{&bWaFb)^mYs=`j<8FY7&y508ZgszRt%t&>O&Hcc4%fW5Z{H#$Z8u zT|H=hlt?nZkr@u?njaNQ+_SpFg)#!?)eX;A;MAK+QnBnwEZ<Nz$EJZlRfn@#Uj|Jhe6Vsdy5Nebi#yOjtW)?i`C6)fdEoR0E_(92 zoJ{hMh?VD-bkdU&uF!{F+$g^W>%cDL``fh|faQq@F)QGKV~3cMnfqjS?_r)avXZ!6BdZ)n;PUJ4yil~S! zz5Q&?__SKvbBNmkPR8C!$WC_n`Ff{rjn@2YccW}MQQsq9ZFyan*X^=`6 zW1kj9{rDW4(r`2<_YLC^I!7i0<*ex2||Ol^OPS#kqS*m zZwRO)be-LPDNL(_p%Sm6fP1Mzovu~~P0rJ@rr)VmDT-PiCl0Ufj`Dhf)^y*iW38dT z;V`1}z+$g!zcl)*S0J9@NI{hyPcDf`g6YcbXtZ|bG32QYW8`Dc{6wr5=-BudBiRA} z&Peuz)zK>!4HL*PRVt(7Jzs*1<|Z&TY*Tzm{!Ho!bo7s0c16$R}oEbe_JGCg@9`p2#F;jbyb74pcH^ze?DvH^;A-p{>fRtT13 z{E?{ii*bdmC*d5U<&)~xYLodT{kHF`muvXnpVe2-qMGozus{V`Yc)qUVeDDiql2ky z$|_;o7vIRPQccHoBk>m9w6-j##|d!Yj&@D^E9H52?!JNy~>tik@+(?I^rNNnTuYz(z8G|&@ zIC;-EhODoxipRVxet) zH+7N%rBy{c$NI-C?3ZsWr}YsyjbC@;yD;Lh%y<~?{ZP4%(&~wb{Guf*h$veL4&G;t z(uM??E0#Ko)I?xa#<|%C-2%PbaiIWIm37{1TNKo*;i^?R36xBALewwr)HA z)!^2JcSBCE5{cCtFxnzMCAka=AsF0jn=$HBugDey^**@b%8ga9K*$tEqJB1^HN1I@ zUE!s?bZzm0xAcdI&VpWh@v$suybwxjFNhK&bR1TsoY$JV(+&kP1?t!YUl5HucR zIW*YqeB3SqZ3kFhu_T!ftLWPOtN{Nk090fo1*jbfD?p6&$Cu@%lr~E92baG~-lnYo zaW<}hUXXjii^1cj+Ql|iYYlzNH?)<~;lc^)8>C_gsiknqDpDXYUvr3CnR}3<%OA_S z6AO7DkgymX-nE1`C$zX($C4#!sFO9Z`D<9j2!<|SsNuSkgoP#D zuDaXa)Gj#aM7&EPA}_Ks_$uKlaNqkA(UVY zF~c-0_wCM2eF1M=>lZOS;T*FtmV=MxFPTbF3nMSL{maoca-^1^T(S8W2XCM(D6_et zb7eY(-g40>2=1<}4(EeL&V*o_b5dV;UlnjjKsmIE&Gzw1aHO2#M@jEKt0ko&COe{e z{gQ|gnZNz=8AsmOYbKZ145gqX;D}(+d>{O72K7=`6&HVmqvebmAlgML5(S zO=_}ZQTB4B#^`YcEGG#_c*|vm%$d?X7+z2oe&!;rTw;?aOe889d^2yOi*r8y6Uvl; z%$g%TqVzrA)KM~10{1g7gYLJZfric&64{Wsj5!sg{tl~AhTrFJV)-kKmP&phwOaQ@ zv@%5YXA?vPHtPBdFz&lThR9MXV&u%8$8ruSBhV_}QGob1{iPgwQU2Kphb>st8#EIqXV%n{KRX zMvX}Bxs|}r*_s_4?o!?{c~j9SLh*uvuAUEa#PFOnvd5tLsK0tA7)@+o5LUf3c}Fq` z5%l4GyaK=uDVLno7n2ufUpey!aB#?e$C-%ln%e+PR z3of3@(G6%@uv^;SHs-yr%fAu70x&9f+xp|k` zz_dF`;v4V2<06p%J_B)%EqvaE4&mo>mzF}cf4s@~MVMS1Bf+5JTf|Eo96Y8(WjQX7 zU`KYJl61pS1KAB`3&yhoBbs*SMu|k8E1QOy`r%*glaMXA{Cc`f6SwWnIag>4%9B6* z7AJKK5U&nt#A5hzMA$&eo2N&L-`eh{(pF}Yj=F!9eIqn_qjUyhK4y50hwr>C)liE& z=q{nprnOY9PeXM8eU!hMD<69yKx;(bQ2aipC+pB&1a6J|vu+4A{29X*E*d5jt1qV1 z2%b{a!lz11#$=|px9j*nOv=t1vgzeH)y2YfunXCHB&)bO%^coS=k~hM%XP}l)^vNo zLAvW1%P4dAoj?bLNKH`CyUsav@^qb>ncd(>U0F!6iqs04y_E-tnqQT$%YsgpC)xS< ze2C_KeNdmUvMqlsR`;r@Z_8nOl;3QlkTMz!vBOw&<+gn<22%a74NYs*F$Eu^dYUIA zf8j5_Utsl`8E}8QxB{^L2~zxfv@3w)A&DL*Tei*w9dMuh%IFt7|0rIB$vDt~M49VG zqn|d*03JQwDV#*4YQqYzs#7$0kcdxNdnAnF0B#HSnA>24Diwt7#Fy)YV>zf2R&7m) zU#NBUt&M{lVs4=JA^GK2*UqYsd=5$iHmDm8dnJ7Xf(U5mOpKhy6&A{f6@K%6jSOl( z0(KL=vvs!LTl(S@ICDe0GS|i_*ff56(aj{{+Q*MA z$0PK7LwBUfj)(ChkxNPq8IcQ)8`QjgS{uoFqz84D4_;lF?#a?2>_uz&mm{RIcPZIB zp4hmY%vuGq(x{BSwEE2^rueQ30`MR|1??T6mcWPD<3`$FyL6Gqf7+C8q%a!a8xG{{ z3x(-Hz5wTT5P&MmuZg!!B+9>bzlJKv?X!PubUYC(20SF?RpO*=$e4loXMpIDpw9)0 zWX2~UraN_(A|;+61nh-Ti_k1usjYMt3R!W>L1(NCT3xrIg7D+SCA?}GuQG|-_9?iK zPk23!(Pi>l&WQB4B1`i=3X%C_A9|{cFH~K%dOa5zHnmDnS<93U^RN^+{MDYxQYidb zZtIhk_t=|I$Te$)}XL`eNW$xGLqxldw(g}ss8kvR&_IjTjX zX&v7U`_zU?EXMV?N#HfT%=gyP)`BZEmtca=RIWCyQIMKVx1hlLVW`HQ(B7D8w>aj} zk*WXgsy6if0kw6Q&W#U&(0kgi19M36FhVb=rv|z;@Kxe|jGs*XB#f)wHd4Hq`E){I z@4nyHZr0O(n89oJmXA~_5%uhdOm}UnlAnpvewS+TWizLOtqYRNYo^8Mq?N`M1U$0S z7I<41TbG5Ltlg!A@fxEQVTu~u{&t|3SqG4-XdXfew=oei@LTFc; zNYdQ{6KAx{OYSs#E< zDnP*cPNV53#+464jB!GIw=?z)WUN$#Q8>v1FwguD@h zQwvzw!fu}Tc+kXfpY!OUp$H;Gpodl~6(l|V?9%Y}W<_6pmEm-t{=pV76L1AS1-^|K5$0gxe~lH_mZnL_8}gfMh`(KT-&_dN21(cEH& z!Z0%UCO#57NRY#+(=kfV=3z{(EYmn@Pm}hCugbNY^~7nF%?25%vazH`YL+B@cc)E) zA&+E5kK3=4tTV+@w^P{^2_=&?$PC0OYtM-33Y{CY<~IJ&>Z~P zlkLL5Td4f@)3EG{?N2bIqqmR9!~=jqbWg&mS=k;4mHdZy8aD(ZFz4rX*GUa^6;SGM z4?csNMz0+Sl?xv+9~h|2A`@v%3)2^GYn{WXq{W|RnPk{xw%VuN+Z{3RDosRuU=|R} z$v=ruB+j9=glg&zsO~|T{EVQbEylNj&1KB@{{GzG;B^o`>6t?MDvmcAvar9aE>a|I2Z@tg@#VF8UVVtVqS|-% z6|I$)sn?G$&Z%0rdGz@0kSa?Lj}@hECY7pp>lpr4oDvZ>lXuni<6HeJJ zBk(b<{%mmLS3IHc9M8L{(PeH)eGIw-JQ1O1<9rC+DNC2(0nI#< zKVM2zo@hy$Law7zL}zQ7j-{|)U#wf7#MGu}J3ZELC@H@F_(6MLZk)X_k3!n!xvN$J zDv55=>V!tLD{d5S)v()-PXu#&+*REijst$rqbPaF!_)JqYI&)d)6Df&iOc2I-{ z+jM|+U{Z{E47CCDP>BQ8G{=w39)+oxWOhx{5_a?(c9zJ6Z!O2j;2zkfzT<}P(*TSz<(pc&Shv4 zpa&nJfrov`*hD#P1x3*gVGW|ixBA&?60O|1UF(o4e^LwtsTZ53WXZTG0%f-v&>+>H-lfO=VANi;Q zp2#e)KZGw;l%*r@GMBXhNnJ=fb)uiVG+_+Dl&FdEpK%;9damLoKLg{yj-o54Hr7#l z1{pm)R_UkX%Prj!Te2#MUM7XtB5czBT$)5oz8XFKXsm{RBpf>&5lNlDbL=*c{HE|j zXF0w4XJc#5#CPdEj%j|qI-xPjP1R+h64aTK1h!D($pPajQl^|TVx7J*G@?%JJ$2TdSB2|!$fbmK_h4#wf{)2Yru>O5iQxVqp)#glA> z?QQKQNrTo#oFnhY_RO$FYeiABLn~NMq-Hyg+?AkF2;elK3j3mOL*Qdj-?#fh;9zU? zWi?q&!MYAqUvZ4c_gCaL<>LM2mtGbp2?atsoEJCBE;(OwD(b4VM6~p1ndYfOELP<4 z#*q4OT%jdmQBv`~5sDR+M_OL}np%7R*5$eLFE@BnY$HJVev6pQ4|J6yFLaQ2$~E-T z6xf(Huds?-+ekzQJIUVATsGMZ-c(}9z}HIx_8 zQ_>j2N;>=6ZE`ral5<6w!(vG?A|9~>0Zuc{l!-q|v7dAJOgZE|eL5Rb?vL(Y*E(90 zP+OJx5`qrijzb}RY)C&3d06@)Vl4d0Qzc%@O`p*9eWgbI-j(0;yf+GwbK6-bo`K9jC8`pN5wJ#=Tg#z>_)VwS?}8Y&g$m9m*nq<#OamFP6Sc`VOi zOW9o9@6P&a_C?qk4UgBjaXm>ny>`=JrFn947?)I{vax*k0pX{!X7x3arZELH0>}}? zE*XCI)Y0n3cjJL1B@gjnL!*4GpYylk!_}jS)A6+NR)H$3fK%6^HpO z5xrJhr`u?S0MQ{lPhy1)Qo$;DzpnH_m!CW6i=%IWJkB6 z)9@=;+47Kp!VPFcI*xJmPDfx8wj;zU6Z|PYQ6RdzQhJTGP`O;s863-si-Wpadde|B zSJo`mjTDy{-=WL}pICYzMW^lltFxkG->lC5v=QPdzx=Mo+dvu4OfAf5{@cYl+9j%U zI7HcFe;9td<2^sy2|ci1(y3m!2$^@KRq#aY%FTaUNilPg!gXtvB)XFQ+b`gsKv(soo9&Q!4FzG zi9(FR5mxxBH8$F>Z=32m0{BE}glc!9!$W+C!Cy6_G!fSz3UJlJC!q(GqwTIrSbY_4 z#dr={O>GD1&OqrWKX8-DiC+zo|NV{S)jqeL|G~VPj+%Wx6gy8}Q?8#QM5Yse5mvZK z>-tEGv5Ng(4@dn635VjL;_u2;2DjfEgODeJ;=#tj(+`E!XAZFJ>!e?s4sd z6Y=Wu2iz5l-ybD|r=>Nue{9Xh)U9;Xn+`kop!wSIqhbQ;_F!>Ei`|aqR*Y`bE^IH+vtbltv z`S&7_mIM6Z&3*T1z~a3d$OG;T@F(8vpXJ^FPh@PE*&hbVMFYK1nSe5cUOisKsE}Zr ze5JXB6Sul>_Ql09R;=Q1&$mUv{g~O$eWL}bGe4&2#qGE-ev8bD;QAH4lPibK-VsEi zx-#B9+E$Spxs@REI$km%g<(vY;yt%j;Dk!c2VomdxTNJ^GR|>8g*iVzstk0TX{2~6 zAC_B+bF)5ddA-o1KkG0klv3M4a2bwL~cWKCf&tPN*bC;o)P^@|zn1?(t zqb64Ua@M1`y>qBLKQ8inMj;-|^Bak`*<{^2jb_Q8r)o~{nt2FDB#1H)Whq5I1W(MO z<}iS%Phf1ztilKiniPn9vkL4~8t$bc=v6n^w!_A?{Mabpkgoa5>gCCvkv`YQ)F1YF ziQaH5&qE|Ped3j-Yf|gM*?d3@r784OtoJFmKKhs3uxp=?{FDsOexnjtb?K-!?=?MlXNW(6Wc5>}_wA(_R3Q@+lak70qhlq&fzmA8nkS*S7z@z2eN`M`mgBxNQE1@>EPUMWD;@Clv zPcYgd!EMU&6_u?)-$$ogs;=*A3#IrsZPYj^G8!>JkucX}mWV#*A@)~JPVnqzLh+_b z6_+EpD(^T}2v&Wpn6IC>V8codp6BW74<`RuB|ea)mzc^o_Ko+emF9R!pl;@O-n~o6 zuTp2O2+>mNSs~?NP~G>c>WcHl2eSt8Zcmi5i|Q*+vwDZm(`^8q3RCYJn;ec~{?=!o z-976b8QpA8#K~DW*&cYZ1M?Dt-;ZuFZ9VdKpCec9Fq84ux7K0%qH*d+0hcQ3V&I^! zcn2_@Ipb3p2`J&Bm^yaFk-E5e&!H^X#a?z~jK?ct6MRZmr_)XDe4kqp&!Fv5-IG3y zFef+DU)*gd{$tHbt8<64H&J`6KWN=$*F8!R!elgua9MmIjvg{iyZlRhkFt!6C4KUT z3Zq5o7&LQVmKwVtLo33U^wux4&-TC7MjnGFlo1d_#vu4A_lVcHdX7R+dMXXygqC5VjMH?J-5Q;Y4PfpQCGWve^gaWa$b1*v)F|z04rHl* zc1qckleFB!-$z=%%N_#h~o!OZ1bc*gc!wCfqdS4Q2w@{LbgHws;r<@9!=4d;snl--3iq&Zi1F z#pm5c9J%t6ym-6|ZuDi;0`5KsB)-q;+4H`JEX+>mP;_AwCp*}h?X@<`oG)r&)G|{A zpJe&@ljl^w$ZX;z9 z!s=^`@t(7k6M`##JIWCNS)aR^UoP*a7S$GV&S7aSdvDF(l(0bDMYyR}17buqHMP zC~s`pD^m!Wht?Ves_Ca!7f_k&V5yNoC)URwjN+)xFE^FE_7J>s(}uIPGr#@m=KrOW zV9Uvas94jLWKQ%@IWmDD;N6S_7r{H#$_d~x$Catwd*zLs-@e+V=ezw?C&p;G||9cR}!EvM0zKdS%0qp zr@VL`o!fp}#M^zNPS8tV*rY+=uj{(xi_L6vonWtgCspoTxEt+l{%xzG#vCzpsf$Ru zK>trcDKJ8tkrPY9t;oGwxA3O!bu4`G%t-H_*Q`|L%*aCM3wtVrEirG(DGQfAjJPB;>7qD{I-R{*#`P!@DJJ4D2u3EK);gz5 z8b3P9^Q9oEui)A-(G>g|z%TL}d`)~dw1SIq6!V$DPZDwLDWRuH_)T_^Npl7b;#N1N zp1-)oWWc~Wt2Xdk3nH3@IILd!8J1Y|{RsZp>zQ1o%%BgYU84iKai(9ntcE?b57 zzQk0EMRy+2kK;Mz@Fl|T`V>XAU%#|#=JgDlDqFyfGkbTsSlFyB^wK2m%U(n2vkX5n zz)>hqiYJ~t{*ge-V@>d<75(4CGXZQ5*;}BN7SMm{J_ttza_H06PSLCC?Kii35^~;@ zX`;sd(a&~)`Qa{NtDXA8;x~?8M2*n!ZJcixfJ>_k7^_Pp^F{`uTdJd_jC9ky3tBBq z0!bV|v+GlrZW57uy<@>eWhe+Cj43n)N#?~V4*v}G1@fd{`VF7cvRB@iV0V>Mu(gl6 zy??1B=M`kR&$A5#n=1Stfv{8NsA$H|jpY&~PtY)%7sojVz)NM5KDZVD=_!(xEnOv` zLj}^>!O{MYlM8on7g(+~ybUeQj3qs5IjS4DRrlCQsGkcF{Z=Ok4s)AO!)fDCn?ePq zzT?>@alHHqMZ!D0Rm6rOuTpEVIg%NDSb{L@Fu)x5eac7`hMuuBVo^t|!PB9B9_xq8 zj(Zj<3h)&$=I`m@o`sLY=O_Cj>B>GF|YmK*yPRuCQbSNLvE#5EBPkr58 z|C!RRe}sy~iXF+|HA9%q0C{GofR$x6wXEu-lec!lg~n&oIso8LVROMuSBc|&wWh)4 zMYoz3wJz{UV85yR^35+Y!73xq5mvGB zK=)f~8u{`3vNtKQ8JHH-cLyA>gzggNd9f~ROriA8pg#5r zG5RuWNV42Y1gv7m&uH7GA?a7%c9!e#AOkA-6^N<_!|cH=3vh;Sc`8wm3&qyj%w z4BZ&c@nGkMbSWuchG&&6QD2~IA%pRLaK%p;b8T|{JO;&vNR@sUG7FVR0wCsVUIrhT zC1AAxNpR#lBw#w2?QHe`DYvAO%)?9hel=T^3gMba-m0Uu*7_P>;Hur8?Xi}9qQ%I{ z@?bTg4P5RS-7WVH(6!o8Z42VR`l}-|_i$#@K?&p5; z$>h#9Q|DAlwO>1rz;O?lxh44=#xfA$Nc~LpmEeIrMKRrym^yEJd^ERD{!OlExv zs>itcIXG_xniVY$iaXHwBHSsWo@#p33Ph=tclp`UFd_z3T}Vr zxyeO>zvN0dB!2!CyFEU1g-Kej&xnd*Vb6*{gQu#~oHDLd4&=2npuVp$uBF$#rI(1M zTkNqcz3fl4@t8Rt#LcPPt+9T$@vYD{12SyHEM8{{4WckS3>w^hwNGtQ z7&eo801GiHGz*FH2>+*eWg5%(pwAg*u^d_jqI>UJnSb?xrot&QO;bn3G~dJXIs=m^C;o2b0w$C*9&z67zF70FsDFEpL!^SH{whon5_WSnA}&A@zF|w-M0kjKN_bkayxlx;i8~f6P2fY;!nUzdH#q(K~A^3Sq^zwpKh1AmaW{DY- z&3^B-Ad*`yNk0p1PM2JHsjH=Xwr`TPWvDCLu;%2oik1mj{3WNxe27q|nFdJR6bCUc zJnKLSEm`(Pn^uh``4>5ldQ54c^d+rxAG9f#g9kTeUvuBE2Deu(kJ`{M1K-AR zsy|Ok|5-UV@H&3ghF)&Gd?acVLC)O8<_mmp(flPueMbt|-}b?urL+Gnv?<`vv?(w| z11K2qyFwQ0(^4L=-whfd@cds)dGLU(yDvQ0^Izxg{^s&O?fBE?f4${%BqS#Ec!1(PdwEB9-pzWzJr8+T zx|{6)z#lZ~-K+-y{$Nq>W+LF8IJrL)IP-wHBYr1b?`9oPo8*rYa0>v;lJ^@KdpFVn zz`s9A!27ZN?kZ$@H&Ov?e`s^vjY1%}^2eFLQ3GK6k7ogn1OVHAJj-1`Ml$j&lk&}-*jzR}*rQm^| zg3M6WW6&LeA$36A6}&2|0bHPeDG`Hv)8MEHKckCPV!&Weu`#2rLG}Tk27emO87|Rb zZ>N3DHte(c9E_wsQ_JQJpQ;Lrmi96@Gdb@ZNkt8}_N>lxwh5Rls#LQ59jxxlBc`k( z^)9EF=Qzf{Q#Qv34d4DNh2p!aVVcmSaK=#!{B zX0C@(N>M_nUBtg7Uf?8y_1iLk7boU+Yb}IpsLH$1|AMYS5M)2PRu#LO7}{1Q0fyyO zkS!{%uPL!Tb9!eG3BI@HjsE;|U8#e;)JlEm7o>xA6k~)+tmp)SW_}fdc!(;?2?{ia z@K)d6+t^@(hSK3LykOH4C5mi6TlwJZwlLco(i20kUx3t%J!wN+GJjR9{xMJ4Sy*Qe z;dv392+_nq zCA55&mm{QNNZ^kv=8DKqzi8R$tOB^`7n0bMhyr?wuYRe5z42pt}`kSnF0 z%UAyP6b=QO3hs#G225DTc>A#idMZ@L`cUpGhUX3{6Lrt>!$NM45{=oCM9x>(UkZw+ z)12e`uoV7DOITD8+23HYqX;yookLp=zJSBOtz^2+PKG=`n|(?)7OC_Y4!y^)uEjCw zdPD*xd|xvBMlOTeC5f@=<@|2sl=vJUQ{YjPd_~M&Hn+{scJ$s|ljy0uGz!1(d_l>6 z-KnHU8M*ds>;dH^2D)t#((#}om==~_Xzh_~4J^|$=b&h}uw+&Q$q>lvk+g(&sY+=b zJQYqv=Y5ZlQO!ez3+HoCvQuBvz`|B{PEy>Kd5DO?(K9=j(<2S}uRD2_d8=Ys*FV-^ zPes6(AAG8Td2V8Xh=a(Thlw4A-t`J^O6oxFYao8V=V)5xUIzg>tE{5@HNMG8(m4U+ z*sv!^is0KZ?Y`w2NI*qBPzvvW%V&F~w{m6- zhyI#}bZr|*Z6AWpAL#~QzP~4ZpjjT$D8Q_Z76DA?LFcYv)#(gDoTNB#q(d{X3~o7f zNXT({2$SU94TpPa-TJWF@8q5%oi5;v=LIX8@7OVyph1JlNyWWHRD$uY zeyEpJMEmRtCHL(cJbZ=;_zS`9w7Bi~%;m$W(~kXDmcY#Fdn9B|2&kVFb^_;K@so{_ z^Ek6~CoDw@og^JKst54SzVpP@Cuc~6bJUWTD5fjPv6xE*3MLs?y@JoX*o;&bLtz>H zK=-ZPYU{NgCYP-`+GXsu^^pFTjKTIVEXS9Z%s(1c!U@(?_De#h8Y{%OwQ&Uy%K(ez zmJt!f7hWJG3T}@isl68|{r8C_F>^kIGB|z{`vxxffF9s1Px1i2?|I;Y?7s+Q{JZSu z$L{mHP5p>z{n*)mZ{MG)xZnI!`R`r!f9BZT!~LspgTLAw|9>en0~Z>7F z|EzVHS^p;eI&jw=Is*LnXSd=1tb+fy|Dh?(%qqn8xb1vzR^Hj2_agvsZz29(1ZIZ6 zw@-d6-X)4>|HHVw?+pO^AJ*-CuK8&pdHpaoSZMX52ULoJ$h+4k7C0}%6<2CiZwcKmcvF{lgKOf}G zf}yC||9Cl#*!y*HL**C9_l7&y=VZ>cd8yX2}@+Jv4WFQLS5s3IDsiN0r+&O$8? zp7FH4{vNs2zu90&poVorhoL1qM@^q2n5k!8_(HEZ%+!t6+`u5=RIBiiN1om~{kYd{ z5e23z;at?kx&e(sw?s}zv7hj*0%be{q%pw;2U#V%6es_i5;=%G&q-8*g#?b_wl72= zw9@pCz5Wxy4wk>Lma#&2KH8%9sn?C#dpCK@TAL&e+V*79mrf5d?XZ(cG%OQnKlU@z zQ_P{4YPZ(7I87H`qo0s`>Wi+PQ6O$@$|TdChbgNH+X4?-<3Uh{ zmk-h1o}Ljs;>aB)>wYmXKG|&w5nzuPv#>?x*s4-`uF1v&OkwW3dqg+83U=PN>+jv5 zQXPo|&)Gh+V~amP3(t~Z*uKBKkJea*nlcB_C|kdZJ~VPr$^C|@4l%pvkk2=g)sHp7 z!<-y^z{?k;wg^%NDqRmY;cRv?`ayrN3(!60ysgYksH2(tC4RSrTAF4YnQtJ>Rl`-O zt1087nr;avt8f)(FrJu#g~zG@%A4ukMi``1+O`2G=f{M>Ii84M{1sA)6^f+;W_-U( z9jP1uO`87BOJ*o{>(^ZWWYMpWG`cjvkB1twVziR(7PfMJCd1+3FHY2Sw zTs>IJ(me>0!}1d}d2nI!ba^+s1ocHfo1>Jzm><$Qag#8uD`7)_6ZPmN&Vt%a;m*}pLVE`^QzRzC{_)MX z{ESGhfC;|_9#4C`5l46=x0?d=_1kZ^MZiyG9z*yh092wJ>kf^blfOyWp>67NFJ?1{d3LWb%Jmf;qW{fZ~TB*@xzOh>U zc%6Me4t6XAWF2gtmt#`amu#zap!YDNZ6Z27Lv~}=WKLF^BJEhwUK8!8ycX#wkvt6g z5=2_!d;v8d5dRBgc$JbjyUVobXFytZ@;+MAQ1cO(O(*K54`K7sg-@l%%n%1WHo}Kb z_|{~V)+KKJ$7u?^2rZv@jeM(Uk&JHkV`Yn!%j60up2^~)0>BG$k7U&RT< z0#UsC9IN@TqN1a5MMXMB`)wIf`8;O>SU1F8cfLaE?+lgm%;N&^ZX{wh9czkCUn$cc zNjY-p5$Td+C^viNkQ}K5wf08?^=%AA_o=NFoC?7-YYkNjtCi8dDfhtighr^1kQVQz zQK=N@BX=|SlVvHt>yL&TmRhC9T>rMKRe*v@BWmwYD^(@+RUu!nO3=wzS+0eUqwJU# z;hbOrewyyXbZ4M~Ewu|B{Oem`bmuH@3C?SWMH=~cmvSj41h&bqJ|lb~@GADi)pdqxtV)E!N-Yq4k*yY2j=m#^ z#%1G#wIav;{8y(kV@J?Idpx`+)Yt2VkNVJMFYtUz-bY^>QxuI{C6bqTF+r3Y;P&=~ z;1TFt!7oLCje~aCeQ@P)aX2N^ah7>r=(1ySjhL*u%j%T~-Efj2@k{d;hz4x&%g4Iy ziNF=}LjXV}su2MQT!Ft!o9oABOJz-qlLqvM&5rr?7q!i_UwEU!7o<3M8)4x5rvKW5NE+3l&mB&GPYc zpONfAg=rpp$tQwM%v=w@8Nl?l_wFsg2oePhVlVEfmt}Ol=Szi0rmM4jLZ54pO(-d2 zvemVe3u-dFj6xVv>jDXFm2(Wy=OCugsQaLRau!_A}WwJ1S0L-n5V%;*z8>1c6CO1xu39 zo2%MJEBFF;UGom!4c7qS?-jDe)=^22^^JO7D{h~w+uk)BzK;{;$@7zI!y7tSLlRh% zXMLnCIPZ+uCq&b*a6J^MQo7HD-0~BapGgH2_Bd0rz#;kiHr3`$?66i1hIy*^-bd-A zZHM)5O8lbDgTd^wUD}BrOA5hS^elu->@~jvUVl6A_qLPaC;ozWNt@051x$l!pKN1{ zi8+Isn*yDLOH=63h#tzn_(HU$@Vgeo@MUgj+Nc&}gGYbG$Bf}Y7<@$}d$7&##Ti3GtxQC_NsJLGTEEBu|50Z z{RVT&0eUfvu_g%dOz$J}g~o6)&d?<(dmXWrD8nHnZ4!bzsx`L;=d0Y>-4WMdxK4NZ zZc9AbwAXzoF5m$RJaZFYtG@})2AhY`TMW#rah0NczaOw3CtmVlydEU zti}Ii75u$*DhtPhb*i=^7tpy1e)k5}Q}3B{UD>%2N`CTnoA$tnTP=_T_EUWeLI8DwFx@}skgsDvR`x6cCUL87n62056SM@QCbI0 zWQRsb?^wXeydA0O39YUdoD!p|*Bev~SW&DEO3$%apvD%4JPGATtcTs6&|I~I=Ht_Z zrhb{AMQhw``Y|`5J6grVfK8d!B?J9A?P==NN%*R^8$~83q)1(Nty}W?N4G%xQ!2%} z5>WbuP!?$n>D^&H?FQIpN2$|AFOUwk*PxFz>{CspzsM!U0;lzTWPmVMHtfv`uUtV@ zGB7Xpj+uxxcwNm>w#7&$th$JzjXQmu= zCh!YU^z)n(A5R0Q)&Z2t_AcwTPwuj{)dy!#LeH#iA8X7fT2m~4q3RSRZGfZnH$lPv z=__DJ!CiE#G}ktOD{RcuBimFe9Eap@Qkr`dySVh=o#>vW z1f9ulJIWy+>9r5*KZqpxwe^G$Pg4qgb{s1{g%!&?aGvl$4J5rt^e=C$BOap`pBtVY zFXe?TD0(xh8KYKmL|iJ@8n-Jrje}wi{WjMVo-Vd^Ik9fCf_u@}_SwkQ#4xxmoCY`i z`?;`)pn)Idd18FMjAXPJLV z?4-XebQzqf%YWUkdO7i&R*m)&BAsQLAr#;(vV0$YY77DhMhRcM3MJJ#2P`1P*rVVQ_GMsCf|mhHCq+zUz<}Wfzk@ z?tvj!EKI3hq|t;cz5(RGd8Kg|@ukQce|J3f>@6(9N}DuiX1omzmZ5|y=93g)eyS8z z{ZwDhHf8g!z>aOal(^co3=c#j`310x$WDN1im+=KMxzPkBbqZA#l!I`HENrSTF1<)_ z`!~VHLE$m>z!jHPVx%UMCKs5<&Db?*F>t_3JC1D@FKaIh!(xw_nY8p;W?{nMup0<4 z7U49#-vuq5&{)?~J`49=Zf}Hhne~xnxqyo_cdnsUXeW5A`oZJPlsKlouW~nquz(tI zs}eb11u{W~*UkuTR)Zo1);c=HAbN7JcmYj9qzq!@gFHip)RdTGn(3t@ia-h|wC?yC z=D=7ITw4(ja%3IE85$5U)=$iUzSd}lU@dms}fpgj|T0ocIfS-R2mWe z*Zkpk*ZkpX0SIqUp}_FGA`#qA>802FVLwKG)Da+m7`P2`3go!_YxQ#2`g=|$gMCg% ztEiW)N3L{Jt(>#TpspTUTFS&V_|fy!pYu!0soPd;6S#syHpt0H*M>QSpYNexn!g`~ zz*jlmWUOgbKW|^l6{>Gkxj=Y`IjF0os4fr|lge>164LTeY?=vs+3T5Y{87%CGPOCM zZJBK*8AACEYR!PUM>lIxO~F>6$MBU7E5vJc$CD3i(W$bDDMFe6O1ULsgycfpFS&%= zT70$A{$Z`p!keUeTb?XY3vhRI)Iv*~7+C{U$R6!QNRwLPzk{)KAsZO?tsaP+=dMl& z1PctbbP+P{VM5P_kE&pHrK#(7pMEW+?^520y@dLi0T>OB4mi~HO zKmQ-7#Q#jI_<@mW{`=qQpJ~B=S6O}_CiTZR;b*?4@bce;TjFQ_doi2%fv)X8-g}pp z@w0v}6a2u*?aw{uE_MAUl;P+9S0*t2cfBFMFvE`_(;F1?8Wg?$_8D*R$eSbJH_OQK zjVOHm-2$@@{ECQPe~*keP{~b6;P=G{qzC_kmR`#!Bg;1^^;$L=S-v5u*D}h;@{K5b zEvt+-{;8X%0H4Rm`i-=FEpv>l-$={XGRFu!^J@*j@nO8Nsc!BQzAkw~TD}F&vVP~F z{vx*RPhB~;H;r-0h{Cq>!q*Qz`r_dpr!pTU=eTiI9J4J0UD_E$ea%?w3soNz z(BZ%*qc!4=W{Wek+JZ2F;?>eJzL#Rj0v8vg8+v&l*b9ry%^ZxalJX)vnUsWytBotz z?uZMx%WfE1xd!w+UYvadCj>2!=RR|p6a=olU<9#GB*W{gotYi^xzLd*sudhxLTXgR zH7Qs<;W%@;jyz>2kMJk6kD+w3K1;@s;655BBHSULXQj#$e)cd-E16k6Ywd~+>wau$ z-|99k8%Y}P;p>HWpgRrINVfIzIX_a0@91cyr~h4ytp1lBgs48J+;26cFubG;6vKHF zv*sx^Jn6P=9*r%qt?l=rXFq7_K4fxp$zH122Z*=cGb(Zu1(vKVi(i!$wtG0ytOR%*MCoO1A5P`c**x>|6=V$o|_iel}M!|1Lv`0w2mU9d!K~nsXXM zr&h`QXi5rK#Z_~WcT`1VM;;tK=;V}Y6j|(Hd|p|fp5%F^pMF>SldcQ8SMF1*HWS!~ zB_BF8KtQ#KKh*UcVTxhGS)}?O87!sLj;(%l!oLdTM(!ST5BltcB;yHh{(%P|NTci}=Pc?L=oVly4Kzjd zC{&zAlVy4Cv^8qx?@VXUBBsPx7BFY_Ow?sCJ_2OY$fEsf=}zvq`pU08O#Wc>M9f{a zkw}|Z`&jajlddi*v}=uOseuS9<^4)I+uItYm!4U)dDPrT;h~n=I5C=+AaAWOnkQjZ z(e&Bf77!OsadvyudBTfcgA=T)TGxaiLOZAWFV6+j@92mt)4Zm60N2bo?vYfiMYX22 zt@Fvl=c_j=>o)oVuGHoV?pgbVOBpZ>{k2uylg;QD-4A_kH z%w8Rf!PNNb5=g0$Y+=pKUjbbtU+nmTJcKtE(zR$+ocQa0+o*KyGk7T6R!o#1I!0e^s;GHg$GbbN<7OGszV&2NOO9VbCAv zXhya>q6>_4x4m*e=Ev->{2G}UUiTpfS1KkrAoIgo{F7eU=RNVq>ySo!8g;>u%=x`N zs5>;x$0Gp@gQRXnfU(w#g_fiXXxD}N&Um819qles&Lr~7g$H0^3viLq2JLg99Z$cy z=fcCaf~f`Zyz1Q#yaIblWI&_4KcP5gn!e@11~VtW5Y59*L{h=Fq4D8TKyA7tBL4v8K};>fub zwaFzqib1TIkt&Mj4D^NsWjD)Pn3Ohy>2{2qe@v_~`S{=i8}~{#L<;tEsdub!qGz74 z4_t!G4L#6wk$eJiMr)fx0Hu8JwGb7%C5ui91Gc6o5)e659w(LloZ;F|Su<6KnJMQy zzT-hnbOUdfpj9=Y=vpxLyZ~o)pSHX%wGbRTSnLTWogV7WUZGgJHy1arr04bjXmIa{ zb}-$}+bk(BY6*0?-k=@N1p|`iqNK?Ej<3c+qf=IfU?XbX2}v4H$C3;<wQ26XyjPRCS3&Ob-Z-0iUoS}3VrWkqWYtaFOU5l09N*xZU28aVl05is= zV-mogGt^`L;l4|t2UlyIyAJw2A;tM8gnU7M7_XNOd`I5BJ@R4>Sc{0_Eca5&P;OP{ zzc=h?o*J-ew2+C&m@1()PR8}9s0$eyS!0VdCg@_ksUqYBviA@ zL2?-FdyDg;J0exthSS!;6s4BI9#8AuS0xE{lKb2$qwXa;B*I|UoTy}AB5}rgX3fPM zP>qXZc;xg2EH!A^;74M0M_`AZ@m3rS4Vmtjz$kHzTaP}*<9lvbSc4FW;DK{nrzD4&H1^eoF)LGSzc~u6?0FlGTqC%Nzg6nu^Q4Es3VI z!z9rso!v8qR-L&g0$Tj{_K|efl9eX#Ox6#4532P5Y&>nd`5Vx7;=w$u>4P!=%GM=b zQCjw88q#`i9+Zpf_0kVO#pb=b29MAglL~QJ)^t%POLH%SEPFU)4;PXA<;P zOG&&Bj7AdU5+~c;GjuHoW$KrfO!$zir~Y1Ts_LLJTSYFzca{3}lv0mFgC>n{O>NaF zT!Nt$;~zT?xLfr%UEweA=mKL5f3Fb(b1?#d@x1+Kw*;opfjCX($B3sw=0{l*rCRlI zxiUG%!f8mv&wu66RX7HV=G*c_%}W%#ZGTz}2mF->UQq`#PO?u;^ZnrxMHiw`E+B_b zZ~f>Icf2>8<;XiviLGspRRCZPKtaTDXn~@O<VyJGo*bI**2wG6Qd@LkmbD0^Aw+P$=pKLddPyM=Y zFI2m}WD(wXNVvwA=thOc=xH8W1d1H6Q8GM!VPm&>J= z8<+WYK#Umf1Q=44nx%F-@_^b46G$I@DxAcjxGk-ZanAz>Xeszejk*ux7XgCl4elZ@!Ic8Mb@@-WaL=S1~ zBSj%igrx}#kVQ)cf+s#CFGI2$rJf$N?HDu7nEQqAL)k7Y_ssd7VO=Son)&^x^X}+N zW%^y`{mp+u$iV!)|0Ic-OGWoVxcAiDwg1F&fLq^3>|OsW*I>ZPd+o!il0nkJhZ=5a z*h9u!p`VVhFTo}^slM{}YI$@bS*ty5MF>4M+D6#*ERi9gC8N?6q59}&=YZSW z={pEy39YHI|{US7?2QPL}v-h%X(x08hsEbzjwGMQd6`1duA}AlivWu};s_NSE zf_t5c1Y<7zF#YoY529#<0)4&|*<4~687%Q3_H5Y4V!QL$$lbgJK3!hi;l^EF} z-0vgvWI8)vhy~S;n+n0aONR`?%8SSFLK117s!yfU$Gmo*tNS7yd4u|KEy*?pY;5q`j zz}#UkC|U9+sF`1jpj<+UwPjI{1|ldhW#21?9?YfO%&aW|!F! z)&t`u+WVfSEyH7pj2jvQWsz7K6|cXH>MD072&_KWjqb6rzer)lI$r_9OWaV9@_IgmXolH|GhBVb8Ut&;AAF(rpXwqRu zZ0=lBgd3A>WZa_1_|kg*K=Y8FOow0U=4ox`7zhRjJ-)8v-?;l4x6lL64sMo8s+En zIBF-cW!4te4U;1fxGsvXc93lKm4a9Jm^ZmyGv+GLII;t~EmhFWR1YWGysdn|zt~Yj z869(VPCcoizkIY1Pseh(f*G>Ja z_)Fg#Xm|Y$;1~WzngIyhgjBrioqw+!KmhQMqQCvUTmSF;zlU4|5McS=?y&f)MGGJx zNdIF%n(c$W@i)EWPr6T-fK;g8p7B%Nh4FU231Aka4qo6sa2HsbA(SEK#3NShn=Djq_-qM2 zZ=)#F(tr`$yW>B8%&;WWeoc#&Eydt6JQz|MGazeBFyVP5?G(CK`f{{_3U%%c&XxQa z)EEv4C6ysSNZC)<+tMrN!EuqOp&L94DaGLZdeij)_Zhy3rxfe#P2@2<(gpYzn8wwC zVg*f$N&vfT%z%@o5-t8wE*fcyNnezWPxwNK?!4!!A_k^isixyZLoYvnG~MAs5%M%E zLZyecIo0%t7Mqs>$NS*fkzdX)_xN2%=rDzSftBGQPhviHv9_5pi10cy~Wwa##R0L(Z_l=$=^wgkZ>nM9tx#O8+R#SN3rsxsG+pvUHSX zB9Uy4k2VePoGRA@r8VN`1=1pgV1$aja%{qk%UKz?u42+s_DI$l#~(!#K{~TT(!^&2 zQ``+jW5WnUlrVV{4|ruwv`(3KU`bN4-4jW1K`dzGVLP26x88w}^{llKozp~T=SJLX z4EEH%TA%SNhB*m=w2nz@Ce zGq$EM9?4FRA4DBwqWjKm_%oUP{kGRIHekj=cyOmiPXf9Y@vP%ia6aI^5|h{_EjzH6 zmAJ6LsP-I>1<3kkP;4E=%ef6VB9Ets8%L-3*YJGMqK#SDQ=jjH5qCd6OgfgHvwe56>m-hD1a;10?2+XfL{Ve92}AR+FY)76uMuK$A=sc zjw-km=)z;65H)^^)ex}G`ZAm??@+Zes}PWD$yE5*W$FFQJrIh9g4Bh`)+DciHD^y+pS-E#2NKebdrprKYqzKse#UJUZ8x9-Y7+Rwi^Tn z@ftb3OqnEKrLWgyX*J~H4KTm_JI3bOhH31uQYvhLXAo#92h!*{olBOl7-|sJO$^!l}gbA76_VuzsF$I_Hcd;wRYiCs?|JQ5EmZx>Y%L15sSfl zC@f<%qBv@10bcimknxZgU#gX+yhJt^r!`UHeojpYN8+O%?*&W50DEim&x|SpuHo2H%%%vm^H0N}v7ujsbSCQ1_fa2MQ z3JG!J%snsE8x=6KQX&E}40(@T+J`pXrj|cgq`nt|B+H8=%Wuk7k+t#O`kYHPlkHt+ zB;KZxWN{jKWI7O?T-E$kNe%iv^DByR35bxl7if(~-7#~t{^~ZRf9yW`@0P>Az=i~7 z&A4SlUT>da0&btVj#US^)AkuApv`&1^!=CFkl&khf5(veJEy4x3k zQ+NSvz(f#tGcy2e|7XeFv8(=GZ~rHr2e1J-v3HYz|7!CAYSbMm~buSV^5Dzc*1p?BYr%{b*qby`bUs5Ti&eqJVHXMbb_YaL*T9onfJT~+)7B7OsKD{i5e7iOakECI*NulC5%$&Ao+`H^sm*?fe}>KR z9~WOH!1XF|hxjWDfLqZ(fGBb(5QhifYiJ|DgwfSq0EW{37=C_bGi^(cgLoh9y$Op} zQKj7LxecbyjMu_OBS>+kO$-}R9Vr9EE~bV{T_z@Ps9V^kd@1Q-5XvXyX79TUnt7Ht zh!b$Ryvpd`Me*1GvP$LmY05K#h+D{U*fkq9?6Ma;C~MfW0aGikT6qHdEz_7RQNo%> zb4U_4(D?Q^4s?xzu{d}*b71L$EY_{(#WtDG%<>^$J;Cdf;Aw)b%3*?AFas}Y{m;66SI)PW3t z;U*1ADwm?deHc?2S@imu`H=$e^T?z?C`_3AOT=r+JA++p@Qc5D^S8X*<3N zJhiS8s;&J|sE(Ehx+-N_`rG(3j^NM_e5Z!~Y#bOFZ%r+V^5R7^ zz+??;sCDijTSn)c#T0d2!0?SD2`t4L;AF-D85BZm$@FhEgp+z`oYh=6Vje$iU$v1b z2)`K7AW$Z1HwxU0)~w+n9~N)=$d8D}T*xe%txt6_AJ z5!vpa1!S3RBaj8YF^RtSIKD_cxgQdrBHOEi9ES7@DN zPtu>e%97%ZWlszN0u;?5>AOo_bWFHrNvvwnhg9)ws$sLyvz5aGl3Jw zJZI!S1e*ahhBaM6$tux#hpGvDS^9wg~lwbrl3QZJr*z9%o95L#su zQi0^|7Z^kspL!a(#lG+HNG%86rr*}PI431%KTjm{jOuN#c%88&t~q%1y}_ua7f1AE z4xJzjL(CfzGB}QG-mMPS_R|QJapIIhsuG_g?HQgiJUEB6M0z9k)mIW)7Z%WAtFAiS zcdC4>-)mo?t?}^weplqd2P{K$t)=y56ajXth#~({ibtNUFxw_VZc~Zx`;^ISVe2dA z!jI^VWMsbxoiS^l!oKc14*23L*&wLnmHqLfKI@?6CDbMy63i}72_ZerUT#t|DKgeX z!xwX%o*9NxhWD-863FWE*yzRX8?#5ck$PI^?7}09YSgu&Y5?>5HDRXL0ETM z+>!j-!P{kd7jdU(=!^8twVDI3^Y{`>Pn$ExO!a2A7taJz^K z!p?Ov)$p)hb21OLH#Q}n^y2y-m_Aoo1$^`vH5uCBARMo#qg&~L*L$e#Z;ZGk`*J3< zQx&hajY3m+JO8E4k&v&AOq`0|)wl%Cw78c2?%&r<4mXN1yo#1=6s-vcdO!!R*VU1Zm&oG;%7+_%2%vlwO zQ&NyW&4q|&>cj5&jZ$hlNHnS%GqJ10Ie5RFQWC(ot9UsLSJWl?W1zquJ=cs6ZH(9vl|bUlb6Y6^`&b}Z{B7xb-ulGI@Dt2Ph?RCE&!*{%N z!|RrM?kJ^HjS$IuO{lVnva*fCCY_RKI>ww_#bsD^5_l9do_+wwwDKO)U1`SyQkk-G zxgy%o({4OnQs0(yU0%o!z9bo-FSs*eG`daWU0{#L%{}bGRru9sYKU`HqCMBLZO}#n z!Y5Y`fs?X(V$DWxo`Niph!MWo>7As#AIWhDsE8;7mOOn5l;fwR`3Hr(oGQ{A<+ut0 zfkhP`An_uL3X}G$wBi}8+b_tiMLH!4FL?*9PU-{453V#JVfPxh2k;&`w(sr@4631< zS{Cd&wa=O(tby&$a+|5VVXDpZXxptMI`wyeI@E#PY$@nxJzuFR3=iS0K=$w6!q?$j zz)XH?>NzR%X}4M{bhYJ+2UsF~IJq!m>tsU9WQ}kk+H|R9w_Rw-28t0V@sMRsp3J5d zf{hI0Qckx~!7;$D^#xr;DLp|sbBDSzK`5aGRe{t?b3IQFcdX(i{1e;6%4GJrx2BeO zmGMuTA#zVDmp=KP$t|F!>*1mtgLqEAsIcd-F$g8)j#cikrpd1>6K2}tI9f&4abfGo z07~%Zy)%%$vzDbFO`YGF;XjoHz%AC3qP$uta1yuxYB(PEVhj9SQdQtT>3+Oy7M~KW z0#S{&pa{rPG$|B~GH3Ivm5?4bIDCTCAI;FWVkZzr$>W#PtR)Q=d?v_z&TiDCz2%;F z>AAT>_Qmg%7F^%=II;udRq?}*j-iGl0-$pB2g-1DPx17O6|&(J-$WM@=)-v$O&FWi z_&jbSS0~H9fYWq!YFY?2IR4}rd_e5ncHb`emHGYDW4F*|zYxTPo_soimKfG5s&$Kv zz%=#|Luj~94X>3whK$l+&BDhMBvWag83(Kkbik;7hM0Vy^I#Vqkm)-MLag#0Dg}OE z7d0_)q_JGf{b@Z6xVz z+tIQ@wNrz6QsH{4R+3!%JPayQEk2QZ!&>AJiGFHjAT&e%BPyyAwDyjWPPVkvWIbNh zSC6NhK1=apOk`mks$aP;LFf-_Hhwh5cXgAn-a>~pA|%bc36XvGz67$FBH{2<*^GC@ zzfpUD^i^c`@}Bo_qymXQ)Rha!4)X@cAv^;uLj$;?l}Xk$gb`3_n1&H)l$;~P;Ef1H zB#H`#(YGBjKPiF+Q&6Mv+)MZ2MSva_@4j|<`S2rLSZOSvakvGdW;9-6ebgf(&qmnYM(0;h zWaSxo)vLiQG+s`dCyM^eWR=ZD%rY`XZm|2UrRAY|TZmz_h=Y0u*|U)#>PS*@V4G3dP}yOu@`kp{Q$#Ax~2 z6EG>HVC6q{GyQkA!e0Pf(9;2K0T+N@cZsoGliyhGvP+EVCeQ=WN%xlk7k>|a_=z+B zCzt--(|&&b=7GOH^>+{d-Qmxl^X=-Ny6M}^fA;@(m45g0`=?!>{!eDG{a1a|-|h!6 z0wwsLbBhBQ>Hln8sc*IZEB^x+8Lu<-0{?wWa|`_WjqUgQiUHTjPye~s|NSHGatvU+ z3DtF%qW_c202ujxkFfwx-8E-0fRX>lDSm#zIc|`UYiVTyZom9BP0zI?{^D={y z=={GN@?YP3!&18i$b7%c`Og9}-xI)prcKf_--Zu>JTzdK=IgwPw8*?=+_8gE8C^P- zS0=gekp@cH5Ss~+ih_jZ?+O`9+tZiMh8Hz8;5enRM$O?h93te&^i?!gUO>-cBoIZb zjS0~dRxgTwYJcbEtIOHh<4Vf{)?ZZ3DYY>i8@DK)>I>y%$sVGUXrwZ&~|5Q*R$q~M)mh7^k4)M3~uiT=!IWC|D#61^gS2iXWhv_cdI)kWko}Q zJJfCrHMxm)C&%}R0w-6+x>TDZGt@1ekeWur#JY(|DOAarF$DN1Lh?-O40`F0wL6yX zttSoXSvevoJg2FHoaUGG)br@#*ib@#F2PjL5v{WcqTKC%I>R#|UVOlzj5F+AZH?XB>S zra%@H2^E9<&_8}M(ZMk>D4+f6h zMR$!(bn*$Ft6R#vd7c6x?eu~js} z`>zQ+-g}OLTx5QP@BDl{kx6JE2|f$Va=TL(YVc79R66fZKb7N$O2Sq)F&(ScY&6^+JUmFNXg#l^diK-Wz(z0WA zZQ)wp(+e+1(mI_z7CkkBewsWi^d>%9%UmK#Q_)_Y0hD$kouRwhf`jBE?Lj4kcisAO zVaO-QxHQH{%Lmti5(KBkdzuAje89HLtf^FT>=pzKIiUH?`}*AuYj& zW3gz}7{zGzT7R3f38j4^`SUNJEj;EwNWn}0o&WQ%(0!Tczt?>kzv;eA*Q6chJL$eR z+B$GP{I}}9-z#nZS-<~|X8E7h$bWrn{(sS=|0gSM|E{(J0KgoJ|9bWN-}QR`SMS}& z761_9|1oLmMybA0yRW5%=|(^PjZ5D#mNl@k**H%oe2 z)ZAlgar%T|b59;^P;{R1$-={>Tn(6ljo{KzuFh2r-1vvItr$jJ=&zLXqdgIfc4CXb zjD+M%v>$i9Gf{S{avRl&wEU5tct_SgBf~Afs$$gNwvA}~ugkv2i!DRLh_&%CxT2d>cWZPeG z5zrw3%6zL-Fs_eP$f8WuT?8i)>;@mC7sc~=F*fUF@`Z5hPLc%HEG8~9pDmPJbzo3e z^32lKAdL0V)_8rz)%}Rywng!>1Q;Ez|A`oXjXaSf>l!|8mvMub%2xXQLxN=wcWjz{ zS3C{!blsQ|A5L6I&h(^*qH9!`28OtG7Im5TL;}uHp5Uk_oTw8`6MclWwFH|xn90p} z+{lgTqMzOhAVfi|z}rp}-hV+bS( zX0&YgqAeB^QDE}lm~TV8tU$(;?3Wr^dxV@aU&8O~sXOmCI|$hWdXdD+usSSRF=A=CtiFaQ38CrjVVTH+-S zgx(@SNN3#4#Thn8HWu;`X3inZp8K195w=jWazQKEs7wxoI7ms^P%0ASg21=EBCN2} zDDj*RLFOKq;`(iFZ~$nWqo!ns(mur=r^!*lv&P$WB-x4F|4P`1@Pyih5K!<|(NdtD zrn|S0N=ynfV6lFPywHQm!Z|01+;PiXoU{_tyT>E&QQq_$p;s~r$15eO2bs+p^Dr88 zQ5-!su4(+cDkW{fQn>a@s(d|$fk@9AI|i-0(g&5vnp&Pq?jeSZCd((A^@sao@IBnL zbC)=>at2eqa@zaR$9P8|i}`kDVo3#2Qxaf?sPRX~9-swpBfqBNgIleCxnj>set(<} zLKaIIm?8cSlb%FjS6{$3cAf4#^XUNT>5P0+G1FlSOIkM`TNZkeeLSjbwa0=D8q#dG z9)g03SZ7DSIWD*J-WDxY^-LnpN~4agkcR# zTtNbTIzArSQA1Hm6a(KLSv@tB1DXQTr^nIMC398T)L&pqDX2JLE#|kL8n^eQg&#s$ ze!&*E)Q;r85y%pHZUtVC+5c>Ex6_YFkK`c^`+`G~;lf$lkcQmY5l$d9;=2?)^B5vx*I%R!Z9X%*$uc2(itv!q2fY?JaTJad{AGScIIe!yZ*rpjettp{*O5MlTYytXeAy=R2aZux{dTecN1dn4geHx|? zd1l=KAZ^^|Q`Cc}vODch;?A_<5B7u^HZ-7?c^~sn6wcU66u`y_6h6c24-K8>zG8gH zD=psCtZ6AAjlINq={`IU&NB!Dg0<3#L3ZX6u$bbxJI!|3v;>XJep>BA;_Oh|ZpSD+ zbqT^YC*6)z6Rl=qL9mbdC6V8&smM*-4de$&E}7`>2+sk-&415c(TEbihG1`Y9|D&V z(ExU7NH5K-z2+1uDvM;5^H!<@wGkj5UmDF&-+f3rxe|t--_1t6THQ6e>Tp~mvj&aO`sAwcSPW{@tv$8dpbi1?eV}gI;5b>+#5mVFqk*fP09D6 ztW9~}nIwVRP4eZ%hbWd09{3pV#q9gaQ$lAqEE~ryc&U&+eaJH7xlb$C9LiHO+Vtcr zrz_%9W8yx#(;i;u@d*mbR)p$>=kKO5J``X1s5L%t-FLBAa0=}XnzUR#iiZOWjA=tp zgTQ2|4u;!h67j|EUQVUQ#X$_Vp;*=?4b!FI1mIpdhB$S{Y`6ARBIUdjE-c9hU&cE0 zjn&1D)}CQUj3^3TBJ;W|pVTL2k)T&rnteMq3iHO{g-V29FIJXgZ-vR^c2uW!6g*0y zr#11@Xm~v62)>p=!M3O8y;Dv~KBJx|!)dg*$32Gp!)%N1^Vn>peRnrfc9l7IX=LH} zH&TN#w?DA6_y9(PX8aKXTHW5zks-)^2=3g-Owq1P3^%Gtwr)%ceR85!gWxYr%W8wJ zqrfur>B{+eE!y6er_0DV)Bu-G5XTG2tWdLwtr+qY+jrE7#X>&Zn!LgkW2CIAh&RVo(o`&)6PZ z5L6F%rpTU*VsJwVNzIt7J1#O>6M%gL*L`enY%Tfj12t+&MfwZ`i}|8GR)1lKHniVP zTC|K98#<-@gIQ2fUnJw%=;+l@MasR{BrHZc^0!70Q_Jf+BG7zZF1tLRP(K9E_UJeJ z1eQNNOZ>%$dNk@ZMc*-UgXko7l)|Vg=xGzz zO*)Ow12e7z=tiavFgAhlU%5akF*tZ^z~7}-}+C5**e zA2JtYTfW2V$2>N|OnA8jlKL7JFJ&q@1cnw}YAz#VX+qnk?H&3-sr@_OX{#@jZf&m5 z?7hUTil?p?qV_|5upEdydpv?UiQKgk~UalH;gr)CyG%UK#2w3@I4<;`_FpF8Aj5+ag>O!^J z22I}A#MzWdL$f<}n%O>u)Eyd*b>!Qvs8X<$&Z@On_b+v6`m|k|nG(%%`F8I0O8SZ6 z$Hi7(m`O6rOmSZVdkYF%9&i!}6w{Z&v5?G5lFTjDH7OQf4ytLRfV09qXxr2uWX@z_ zxFh%acPUAMZf{HAZua_9;8I@nGS>GaS5#t8>-iN^Ta>6}sXjP=SJ_w_KkG+Xp>yAY z5G1Tpz(d$)v>jDO@fQS)eRv>NMB!lNUK>u!fIbmb>UeYB(e&rqVx*$0p_Fxm1=8Xj zt;{HPCRlH6WTn!v6joq&V9?*-j<9D*SCqBny(vqQxbnczj{Nw^xHrSs*y%}~Bpa3I z8w4sSj*5`Cd5|us9L#9Vjq9HEU^L8?NY;07%x&6Vwb$V7pZlihP-SMOIMC*>Ty``KIqdZXj_Kht*tKx< zq3S+s5X5i3I>Uao%+>^Q-lRkRqtD`wj%{FwgYQ=wAgMi+6aYOvPFcg7wGX$*X~vi+ zbn>;@BuBM)t#C=K1ykTUwc}ZA>Eo=Y{sZAWj)nU4I=RkVGV;4duY2fX&}vE}SBcs+ zG{+MuAF7q93NQERg)UOYF;J2`1u+JFf5dd&#o2}vuHQrYa*>8qh6s$W6rpUp!&pWf z8)n}>ctjoy!`F}IMVFj|0vcmBs<%mu#+tnbUeZF7V?_Kt$y}lgaDJA<7@z!*G|Qm8 zXph8dR=?f;h7E$DNGjaq`@Ex5i=A9lPYi~4JHUcG4=#z0ZW zwiGrFgV7FrcdW>8dX6qQ12U3_OIJu@a88zd>psI8#QJIRRNj2d(MiL9Pm` z#L}9mlMR0kGY|VZB*kpG?GZJ>#df}_DaF_{>%1peHji`hsmOr+QhdjJp$pf(t5ym^ z|7V|xYFU6jo*W(Zv0tE&oCF;EmoL=LsZ{rK82s1>MRnEBCWTE^P_H!Jz7k687+33u z8@be+9}9WM@*}zV&b|LrK!oKsNi0$p2*WlXdyq9fI;Nza+!0l(BH}5 zv8H>VCU8oR<|l+SU*fQ(DiB2LMz$k{vr zttgo~8nfS*vj5u#3+BG}DP*wwsLHUMwwovg3)bqVsT>9= zOjO%xQ~Sb~Zh?d(Kxe@-!D+>eSzZX#Xt(;q!c4aXkq`*7^VIgsV@SV1S}^X21r4kB z{8Hv;c}_T|EU#V7gSXxfpq*Xl5-8-LNxZk%VEup*r~Nt&Kc%X;@|>PL;Vj zn*N*IgU@6Ai@yJ+#lq_H>MrM|4N*2?dU@3-QLHz$vSn4l_ zh5qsi?}91)(~bJ|hTH#sUej-N{PyE-<^OQ%`UFtCgiLkK^j?@gC*evpt8Yc9_WzS_Uy%MAmFj=vTMIHX{vQVQ{2z_1Am6`I1_TAZje(%Rw`7xo z0(TAB@UylH3Vx3;3JP9x1Akgx=vzyER7&vgf>hs#WY^yc)6JrJjZt0878CuqU}x8T z2+TM0D+BHNotfxwNMzR~f$#l`8|&si;JY*3_$qFWfbYP>@Qv4Y{SLp_cQ@C7?PX&4 z#%cq$4|sIbmRrO$U>307R`EXzi2;*n{Pv8WX|RmHi|_^Xu>tqB3!bw1&?3WJwIt^} z$-Hr~m5GidNFIZ)W*9?PkG+Y2*&z~^kFKHox^EC=dn){j7%p8`H_WYz0z?d~44K*1 zR3f`VI(>XB!FDm)+L$fstc8#7C1P%Nwu~zFi*_2nh-1dR6hU+5hi{D@sXx!Ja*GFq zk=kqRryO~RYgHdN)~ZrD+9m=r%amK}k42d|coULKM|2H7ro)B#Y;N!jZ$60AT5!^- zsEFiiwMuvgo41e4gxyC#ie(neC$MFl-fEGuqs;TQ!N?TJGKf0f`t*=rSGM|XaHnIo z1Sh&}E+?h^0!o90$yX}IopHxSxwr+7r|Fs1vlh*-n0N?O+Xrg%bv#9$m&wly|-_h;Qa655;q#T0~W@>)wKJ_ zl&Bvm-@PoQ_=I@kjYQQ7PSJ^Vao@45N93b}@sE(4@sv&7$qqfH=%V^CwE*|Bb6B~2 z4Jz$DbLvC#NO-A&hvA+Mf%DP4l6+vNpdTRt;ZKM*(K%&_U?VimyvOUa_WEU3a8=eQ zKS{)7Parj&C!H-Xm0W=%jP!`0ICU$%&$znYXt-{~6S(bAX?_Dma{0XM?S?mH6?Xdd zY<=_;X}(sR!jPwStw@VX&#GlKp-s5P$oWd?e8ml>#UBJZT?i@8;6O{7$au{8rr`I+ zxh`lnF)(}@;g40?>eT}Od>X+l5pgO$GX#kC9VKNRTvs?9<>|EYPT%pEZ@Hk&ZTsvh zWyGQUiK%C$WxUIkv2}}dtd{UZ$*{8RF)f@|@&Nn3P1>FY^CAJK_nKtuDxgmt?yiz8LA2dAZr*>fA zyqO%$_Q)nLk*VY{QMj%SxDE)X?~A$+NKiaR*qC^BtbNu;LEeG2Vu*hFHOJnfD-c1j z3w?t-w2U3b{cWNuu<@;~syz#H&99Wl-i5BhVdsMWx#KM(g+S<4Fm)Mw{#y&x{TJfBlAbu35%_WT)vt_ zA1VbS@56Or+RZg*pU8^2Z&G#-AzUw^;nxW}w~jZ`g;`}8#Sx>eI3vDSN`YHQ&V5EB zxg(h3q^b}d!ayF7BW!H$U{qOI$v0_MY%dKh9u23=aM+*Qcjgv`JDM~j8iLGjAp!5w zQFP)R>)L_LoZOUmO-M6}J9@k-7Me2*>QbzoZdKnidQRYJh{yT#M|b^`@Gw2&cfQ-- zFtp830bm4P>+feGe?0{^Fxi3A6YD!{_tX~qJ42hqsOF@}n)fSKQrBa;r_b?ej2ysp z+r&(wFo|OKo#I{r>?Dlo&K6c}c8`r=^g)gp@vGbk00>!S__m zN?Ef`BO)81g7igQ*G|JmVHoF5QxO^t7r32o$y=C1WXaM#&cl$}lxt*sa^>%+D294p z!^HRe&SY&E5V{)3(J}B;k|2CKhkScXP{UBFjdm1vJ@54!P)rNC+@?kb^4=Z7*zT_y z#>vt#raeC5pFRb@pWO)F!?eqAu=xG_J?I3#`XT-Tvl>|a>bq!9x?d>TnC~gtelpVu z7$N!)IsZS@tp0~-_oGk0GxvUU(l5t;bLw}^{}_pXtUUinW&W9CY`{JAU)wVq(?3(= z*_il#@Lg<7!at5ad(fQk747>mGCkY_9F2VaEPu8&z8%}AGITibho*d8n$WA-HcqdsB>l*1* zh>5&bKynx3nGi-||B`UrNS8XoJls+i{PJi++km~tz!sU^6^)e_nk$KegdN`H13J8I zchGX+s$Y;K8izcjWk3q#8>61RN-fNQC}DHnNN?r{OSnX;7aMAtU({AC8&`wSRN&gV zi%^ml}pxP)_d$erM0~@e(9D;oGqs3B?J{+c~XU2 zXsTJ+w1P1kfm>KYd<6XTc??&1SKYe((G^SMGDh(-yk)!Tp^v&wx}S<$4Nt?KCCTqM zT_(n#1>t{Jz1*^s2$+??*-bDh;MYk7Z9pZC-XD0IZ%GK!InP&!S=9sG>fxmq$KSFAsqS=Z3xLL7JNcmZvaF zcWCW74`Yz`5&ormIW2ih()ZQN7fE~yFOL^`paLZrG3Lhn_*tajFCEPwqpsF>y6EN` zbu*`*P>g;pHg)8Au9&zj*IP1FFNWW$O!>yGqZhgR5xmlSWDr_Q^{uJPT_W(hB z2Rbl*<2v}$``tId#UFtV3{2leA!tTPS$6+gy6CmRD}-~-f-+qX%Qe~2pIH^coAxVuvDo;iA<(28Pd|7?6-tAf?doqo^L)(+H?rR|nO&fLfqRmHoXp!dwOtg3KgusGr(r@kCC<=9!wyv`zeiW;s4=crkV+sK<18@M{2IvPc5 zeqDJ35>b}+qex!HpM{6$zjwraAA)rs$;(FyKYCNEAe6_k`2ylqcpu4&7>7_|f%g%W zUm{Y^EiSTpmDv~+ZkgZF)rXrLVUCc5fk}L2DLcN4ynA#i>K0mL)OBlq5p1NGp0Y$!HsgJ1p>aX%Cx1zDNeU3NGU4T$-6oon@^qp0_+jg)BfBLXqeAMfv&r`!NhRXZ z;|Fq_YtzF(Ka5(AVYQ)nm`5 zTBtc?cel==F}o|!elH-H7=IEnqG$Ndn|+_&2Q2jDf5zRdIr0pOVNMdBWQf*aoG|GW zG;}GOKg(7h!{w?FJ9o@~btPrbw&{9N1Lq~_zSChtH%zlbfb}7R36Kn9YqkYS-=V3< zc1sSj2X&D~p{v}c`6ZB^`^{(RNQ_y$us!R+b6jk)EvQ-d)P?K?Ha|kVU$XnkWTLec zos~#|*?lDdV0Pc!6JT~c#pr_4vu9`eW2l z2XSsqBUhO4pi*R?NWA(%!`01&&tHO3Bg$=>IYVZs)(M zw*O5c^`Dz(|2zNlofY}7xrP7BTF1ZI8vIXx|61!Bz{m2h@Xh@@mj%GbNB2(@*I(m9 zUOwCf6XV~!bN5#OH@*k=-1mCyH^}>+)nl3NU;3YjI|eX(&w@I_ha^A@12M*xw+u#3 z6l|TIYXdAT{4Rg9mRj2&piR)v&oV)c$sUEV8`F2}x=` z`fLm-rP_XKuH2$HF*|3Vuijd|SQpfGhuby|O>3Q8imkgZ49qSNk~?cy2a3uE%}RAA zf4`4R6+#GI(lGyGN(PDo%q5nE^;&K2NAvRy%JWAS%MX|j{-zp9WBMlY-tqboPx4~i zWSS7RZAb|lD_RbbSZO;1P}yfXv#vMd(ONYqteZpJ@@bu6=~j(AqIvZsjBeQgpJPg= zI9x@^lB=lA+2_h4h}FsRSP};0HgWOE1XN?z0r(Q&RvN0m0oL)ORJwg@z`id-0}OgJ z3PQNaL=l{osqix7f)n`#v*lIS^^=wwFIh~Fx~lP|Wt13Uc}srsLUA8s_5QI$Pc0AP z0J(H@fQ`Ar;qA&XNuqJu(N+1Yr;ALH6rZ)13O8dZtDhE8I@mtm+OK)HERcJ60omnf zUG@7Z`!;#wFMu0<%lgIm3lubgOJ6^7T*Q~M%{FBlrjDPcv+>eFnK^?jW4}s?cTMw?q?{qGvZ~Pp8TIc#% zw4e1m4kD>2z7k--lE7IC31)r_1WJbs`{iZG$W1M3t-oWpf*Gp*BO~(M+$#U4h`40M z$C0b6=j6vy=2a(HpGjc9e8z)SGoh`X^Y8Vy!ICT$1Jv5ZlVTnpYGeTJ6Ru^?0G*3S3+X(0pbgv;o zm&2RQ&y;sLCvj9Q>kW{H$8y?!Clit<sBTW? z1-`t=rbuAnQ@CEh(Bzi+qPEqQ$NtUfBy$p}*bZoRpc*JwhIYmQs3~MZKSpO`qqIEr zGiPbJwDL0}(n@Ssb7WaFZ`qKEXV1i9MHM$wDuScnmn?#%+@}L)`9pFP4#*qY#92J} z2?8{2ET6WBy=l$RHs(sQQ(agUwGGPD!LUmVmfifo^&}o5R~XL(z8DPtmFXHQ;w(OK^Ng6Oa)k9+f`~O-L1x>68CV8HIc!x5y0=P#vO3YaE9B z>I$S*5&K6K? zc8!nusX$yLc2Qh>JLfJzwf(MjLbi>4I-M3MK9mw)uZE&!qDX;b^x97*Bkj2gM?i8o zGGEYG_I6!U08s<$rwzAe`c2(WjD3qu_%jO&K^>Zz!TPyfH~^tvL@H$+lA}s@>S^X~ z7!_P9MDQ!=^T-Xa<%xNAVF-E9fSwdBCuniYALQQx8KC~IZu|vOl>WPHG+>+o09bR6 z4LFGBtaJ=NCq)1Kj+FlmLJ2rVfq$}T{vCe>o&wx7e~-Tcb(X)c`Jdf1fsu}X@8ge9 zIcCQ1@mEEOicfSfi@-uHUPWCpqU-%rQ_rqk zaOIPBe2T737l5M1O^0T^8P3g^v<|`BP}(Wi#->a`@oE&*RbqanL--?EDZcd)`cT>$ zi5HkKw3I1x`s-w^DH=3!<8>l&Eb3qmhAhL6>szWJuo0^K%rQ5^wMhEI>Vc_V1JNOl z4fa;_n?7{%{k{Y!Di$LUem2mb(H&`J3(gf5oU=*h<>64T{h_pLm-62{H&fnuOM#mg z4LWPuja(ullsVy2Eu1sHMVs)R0!NEs)yk`4oTlmqG_OAZ4=h_dl>sh1A4HNQCce(= z!)Mjkj33!bPbI%>ovX;nP##o{>%mVSp{|^|ZquA&K~ksl8}@7P$i7%|oI#~nL(1m1 zyhRrE-LkksQVxf#b%4C^TDUCF(Oj${qV_(&@4#*I;cn!qdv7A=9nDpJ%- z%&vKQhZ*K_uoDY0^!hR*U5Atoe5xw=0ku9ByrNvjmP|xu8(X$wfoI`~tEmGh+6i{H zkF#r>ZF3Fd7p89dJE}N1( z5?<8qr4c2xw{t(XNcIG(VkL439iAn3IENFCgv@5jhu2C2@`r6m(Aqk^)?IOWlO4hG+XN->ATulp+ zqwlmuDxyocUl4V2hu1hIq$}#}10Q0&m&jrmzT& zqLQ+oOP2J3sb!hGiLYH`@6w*kojn}d0edWuyg6!gYhz_=p}{!5geqE2VY~(|pC{j( z*mtAvgficKWhoz}utitfdbL$JTJm*wnWQk0%(%3A)D{(fq3qo=G>_1x<$!2S>6t@Pth;3mKYW)1nAa?ui5y0$T&QM4s6 zCk$y>r_c5hL!Y}{gHNUw{fHod`6vAbz$CG6i6x{I|5ioD-5aDvD{f@6TaVjl_pUK} zaT|>ig(S`}IJ@yniaydio4i@dH|0j*JX>njz0cWuM{{zZUlG=IfHp3Jr}>mByrnKH zZ$)j9V7%EpfNfmfirIEWf^da9=5sSH@y^3cE0w4Su(r9#SD=Mfl?0>4CBA8x=?E9D zadGa3Ry)$Y2to2MrB>nvi{()f#!q-jfbXAW^-gdPYK8Yw?ORlDKG+Ek1M>&}|mQsD`ndxt@J zt>H0Y>yOkAI1mH@Mof^Rn>i(F_Vf)T$$93Ni&IybqOhikVwAT^N?LnxH!GaBj(X_^}U4h_gl*e`yq+zsw3 zv?#g8(COMXLp~!;%MD~7JIte;y($>wCj>{+cDX9sc5%3l{{(4qt91wBt^PUaM_a}>P}(1fK6=LQ1G8lN z@2R+(x2V4)5cq6Y*@F^`;zMxTXIk>`#`TnB#Q8lVygtLHCiW*F`*MJ>gkUwVu%!ug zo944g*SXvz#Txho+mAqG{e=g}mSjuDVX~M=PygaOyPkp*0Ucnh4zk{Qit;SJ#l}Iv zL9GFRjprVrfpL-uukwn~vbTJN^~PLisqSiQ+U-BeJJ%rX$Xvc$?12ce zn}UiyCB0|j_LGmwti7vSsLUc9s+E?U3XVY*=aL}b-7|hw9L_W>Bb7K^V4yLjK3{WC zo%=%mZNS>IoO$H|mnJQmfV%pcGx$VfJH{VP+&B2)KXgq&Lpyz26Ki`b+h6b%XB(f!%K|K$$; z>=*y&y8nj3+96=NNt>^_6O#$#s*EFFp9O z97!qj>T{7on@eV!+UAWxg9*Fr^S4eSq$kj)O=h*UQy)eZSHHf+S8zx|xQ*CG{936M zJ0d4g-G3rii6Jh!?MIueZEvD2Xj#bKjh-5eEG>s0-TxTziiiz~4jwy2C&^Z@*siCY zez)%ycZ)l38Qx{^%IeO#=q-k_xi>z6;3Xii3!$bSqB-zToV#kD#@^fy;|yA+ zCKJWleXb)dCf8p@y#aoY8S|(u{{_H2GtUfqfF29i6!l8;>m!a@u_d0Ws8;o-q+Y;+ z*0h6odv$h0IC`9$aL336<-~T7+O*UulUH%oRGdo89=KCl7BffZp_H87S+vlgBS%fo z*Fafrlt7@VGX-BG)AJKXg`@-M&YbUTYCO|u%hS%L4)~T3WDIe%c?>90v?S(IYd(6W z7<`#NOi5(9o4EyPylO%Dk*xnoP?hofGBH3N>VM=_p`#Ejb0#JYbkyHDHgxN76g7Dn zS98BlsWhO{4A&)}%rho9%<3=hA2*E~?MTub$$qCjOED&850<0^!1sEw7HfLZYnLel zu2v1N9+$Q7C`-=?)6M`p;?FXJq%}gxbK_29F4~5|;|3k#uc) zyoX)5D?N$9M>yh;MU2*0j-zo#gVs9;o(Q*rtAH?IBvd@c_QvAQexaq0nNTN+2%b1R z7Nj7awI$SD4?|PcZgy*`$l9_Y>*&sovas0IJv#%4#Sb?h(*WvOo zbH~8Y4jZG@7{3`g(^{Xc7$tdbC&EeBF#HCB(6^2hDWp-mr6%~r96j^bS67163;i8V zmr|Z*_U-XMTH2pPRvFm7(|?r|z9F(StG-U73MQDi)0SZ0A>tZJh%iYc!M`W66lGfo zC}tC6S`$D%;g!;Cd1K2)L5Uemq%1Se1hUv=-k)f3$nI{oy%}Vb(Lx96flVyT&nWleH z$FXcLHuj7ua8kMh0pPDtH+#rDRTDGT7**d4fWa>oAXv0Fh*WT|L9>8iOq9@*b|w#M zN_s0H^+*)}=wz zrD5h3P;{C*KI42LYF!}!7ub+ND_t_Ii0CEBvcZcpjTyHN_(@^io-X}6?vy|h=+3Ne z?ntpCll_xrYOl@EeBs6Ftv)dVwe0byW-e!0vFW|Y7rUuHD!KeE0{ll?^=~tRE$vP{e`N-ba07r7<$9tCJU&quxD zQ*P#3+q24d%@YviMi2NDoy6-oBWo$JMnkUmbe;n)0CQc+ba!)6P)0prF)l^Yq+RzJ zS>wBmgUWNr+prYqr|`R8uBDHwI@ZX762HD2z(3zQ2bmEIw*SG4_pROXM=Qlh_gilN zs87JqfU}ghG#M~op}Soe#{uxeH?`40+ciyPEbSJKGIPgZ5d5=vOhLT6& zTG@@@jY@ratOMOesq7a{@VS~2;x;|sbr4*TdqS55@J5_0?OE^? zU@z66ii%n45<{t50570#mtmZ((YIf)@toH%h&W@Z@`+lzJrs(-`}iTn#e$wH$YB;F zMf+-<3Cy>Jq#QgQMTM;7z?(zA7^)FmVHNx%AwCPublacX%{ms zeUX<;%-+$4R@AnGSFWa>de`y|9rSEhRl~x@-hdzG^amB?Te&dHx0;zJYuuuYn>#n$gjw?TaF zj{9`fET^jM$r)3zgnA;qDQNzHV*Hna!@8<0%la(KlAt&OdX8{?hl??;l2(;tlnBn! z39`CUwWk(k)o4TcgFfV7ydI3wkLSTbv- z*ho9w$HE(0$6_cXJsW7GiDQerTB$et1?2r#^c4~MReF4pK2?XN@J!!FE$7qm^qUTK z4j2GQ*RN>sDOfiS3H~Yhx{q9t;=^~>O;cX^pjm3*a#3})_L(XVpZd?k;U_3i&dPI+ z;~Q3U;2A2V8bf#D7;Pfx0gh( z!+T}K+)Hf3o>bRPmake=+k=R# zfpSeedyHuOAI1km!QfM;RgZjK-*HxW>h4z~4|p1T=2oVwexz7;dCil#)yKRiyoavg zeubdJecGPaB7t7mP3ver^OZ}d^(J6OlVEhi6|VeiTvV}|sEQ>OVV~Dlkf9!xe&1+Z zWmat;+}s4=B4}-Rsw(Q<)$@Y4XYR-?sL`#%k57o+-Evl?&9l%iyXqyLpPeI{uB>ML z=n48+^z-|2nwk-#mcYW@J;0QIyxKrS)7p|`tZ3nxE`hu38e}gzwJ11q;qC zuj_zBs{kwG4@{|1NE?mQO(M`)iXE$ORR)2D2(w8{p!jW_LRN=g8o`{bzEZ~#%Tn#$kliRhCMm?+xmdcjHaTN>;EtTw{ z!@l^bBdRcsbZ{Oa%hfA~^`dpo{U>4!qe71_rn(YqcXeH7$39?azdH$E#mS1*;eXm> zHE)cnxN;2!QS5&6`{`u)X3zgCph$-AphzIG9ROIO^%p3T;phDCEdPI@$bT7%_oqAh z=E842{sVFR;lY3W`)5A=$u90M`>()_06wArNqJ)ce!8D6PW)TM?$5LV{6N;H@4-*t zp&zl){I+)hKak<^`zwEY%D=k};0H#x{WU`X@G}ek7%F^#`X0c*`v-}M`R}14_Y27c zB#D0TF1&o0XeQ=gBX91Tz-YV&yYb=L`^jW_Fiw7H0&k4zVPpHH$qXdvWO+#N`7R=z zjto{yGDXld%;b%ok<{Z&a|JEFNbot3wCR0~6T>A=Z!+mtA;;(GN@SF@nmE}Wz+_N`oDr|#fK3Rax z$%{t;A6?jKgfLM8tcBY}&9ipwddoCL>re$=*inSrP{T^qy()R)s*u~16iNnCxhmZ0C@<*X61l9Vmz#lo6xWA{98)^b4;X^ zc3HodWhf=X=8Sx`X6>r&vMu9cv*YPNih-U-yD_vCv~xR^*fmCHoTs*LVH-5H!#`+m zyqj;%-g6ONr{_0!ecmp{2xu& zPePmYOy8wJC`Iz!Q>Z*(+poJon7?qgUVbHMzN^tWW9i@R4-Euu8|osH?nRSDAl=WP zt;)=2$xRPS^&c_9Lay?cuqK|3(%2F9T*EqSv#5q(DXv+MT{i!SNZKeR*y9a_Ka$FS zq{7-SIf`{`%FCS?`#75diQIVYm?$CMGR`tHj){aI9|c`tm#i@T5Ya*Fjh_xXk>7-R z%_A>zd1CBJXcMFi{2$BTUD}ln-fJjPDv>cHvt}&@S2;bCyd~ zx7UpoDG(+$D6{(d*#(lW=j)UoEN*Op>0Q3z3R`3sE3NR&4*%p*=*bV zqiOs}81XmeuBM_`)V;ZTg?e^a91a&YiQC>sQX?oJ_%fT zab%pmYA=8>Ce8sJlNzv@efN1Qft$tMLbUiX66}=8IxN=z}3?c!%_R9Wps;^>c zH4igNrSMpbqaPFDNTvmnMYpxSan;dC9I32pJE|G9>+{Yyx25?TtGd)iwIlXhf|b_< z@3Lglo{@@ep_YwCfqa}3coJ39hWlu2n7~}M#sDvT(D8#x3oRKT;$cFPFh~r8IC~|n z07^HtDNB<`GuwgHAeb5sId;=588*PZ99M$8(dh;ems&H5@DZCgT^oA}Of#Tv;eJ zA>2o7^Ov2h?X%^=DHRy9;fT8vo-S7+)Uoe@B_!|)?H}{A>IEInWXL3CiR1dbQ=(+c z8imL47+ae}{Lqt>M?v2jPDr;KPds1(5?=yJ3TVQwTGWBNG)xdX-)wJwJA5rDxf@^0 zw0~}7{g|Sm?Q;-TBeqA_24XF1qrgD^~~qqGOM1@Cel7#UDLm-$Xk8k$hlc`p(?djFGT?C}YMB zNwfTPRm5j~d`b~x;HGv`Y9N>)q^tB9m^q```};y>Q7ZNQcr=AHp2sjME@224OS5kM z4hCDureLIG&q3J9@FSFL3C8U|Igs9J_J&@EZSk6L)~Wl;A|$c2En=qi`juVn`lC7F z3+%K9e3c{jKpL79ca=iQ%^&ILkl;4FL`s6 zUjk8V2Bq8aW|iSJfW}ml3YElW3vat;p-6`~DWfbZO9I`#Et-E`B$(R5UMY|m|(A*%^|>0Jb~CJ%RMSjT95HS8oR7={+h7qxlzeebQd8Sd5HJ${jq zUDEW8Tg%#mtuY;}F#iGPHvubHzAyC9U`0j*5<_8_<+GOjTtYRD(bhuHea+^L&EJuF zW-e%~T$3HGA3zIpYCDb-FiOR=8wOV`vj`X@YP9M%FLk-3aU6cend?Xez;Ui!2F$iC zw!eNh&%&;7<*mL5{C+p2y(7$A`sYcvL|VlvPRb86C`kh>}=Xm<7vkRu|`On&gpvEJJYKfz4!8=XW`R>};RuikV;y=3F2 zy66Yx$OqPV;e~g}H+c(^MO!eIk7d`_TAE}C&qc{FlDtk=_4TVXHS+NX?D4EXIg?Fm zKoKgC6Hh$)8FYbPYo&ciqB_5C{YQ*9c6lNKSl}6ie40%Q%hs_*Z4~?;Qv~ylfuwka z$W7YsZrZV{15v3YB3J{YXct=!vAxL`%A@&IUU5K;e(YK|dVJVb$rVP2f;kAuUP3{( zZU5A}+Cofr&{M6PBS6mGPDIraK03a*aL{WDADVD5`E~l}x)<$?-vaqEc{8R^tmDVp zE^R&3H|Kf|G-NUkIemcQiF4yA99+$VYq0EmXW`$^;e)pL*U|gXGkh174rJ>C02$cW zfR^raR)(KT?Rr>rpt}1TdY^w?fX=T5=13@v2JRlywU?RX* zkLdxtcxc|ge!tk^zchib5fjVb!I%3bVgm9*{ffl^y9YKOV!Xb?Q&_)&lK(88!upf$ zA$rE|@f10}`&a;=P3BI-0EG~6RyDavd~;VV-|#__M%c!WtZanMFt`yfN1dJDKG*%L zs+#S#`Nf5ABGtNaDza#Yi{0n}S5rKmZg3C$;W}Hz4e0Y*IniB=6kS#xVSW~}Wp69r zz!my9(pdwUY51Kg_#I184Iw^54>pH$aQbN%XahC%kSvM`Q#&{ag-=b?-C}~Qe&R4Y z{un!Lh7GDuRCHAd5YBSX)OVsRAuA~@TE?{%7SD-ayyaS8*aYVubn_AweVj0@g& zE1{eCu_t=U)G~Gx}*=|6XHZV*N?C2*YoD^YRjw zki-ZJqg1w_WQU|HoTY^g2}_>p(Cjiz)6TC^&7pUj6)2cYsZw(pY-BE1&B&|i@;>?p zNHx$_o_nQ1^>K4&7;^ie27@7SMT*Bd97Bsso309{NQUJ_v9}lZudzNVpfgMjlSr81v zic8FjP+;A~p7{O++>1WPqXzR9E?2K!*+)ZUzP4}%)^i(IE}H=VIrG8ZbG}4 zurti43#=tx-WcDP;sxJS$%PdzcPwQqE~B9yb~vX9%}EeIR}`_1^%Ae`Ms* z(|t#2s}#Y3)JcaR;FQjf36t^W`HOWiIeXE)*0=%W%~PUDG^z&z@4+eQO}-8>S`%jW z=bfP~(2i2`Ng>>Fth|}mz$!)&Mbn#e=`W0b-2IClplN!Pe$8niv z@=$g@YCdje^@_tdU!3fSn!Yz{l-e+YfDN(!IqXx@DL)E(j&P5I*dnBTOCC%JIqDzH z$xi~K3~b+ndh%l7i*yLUpq^_lP#e^OGkdxOfwQ~Pf~Zeow$!Kw7IF74a;*ke?Y6j_ z!$pq3AgkF0Lh4hM?iiL_g_7y#Mxb6C>NF~r(KFWT=8&i6`lVA{?x4j~z^VdB}oo}~g3CYvU*(BTd|G#Q~C$^m2#9r;%c=q1H#Hm-R@b)3V6&N?e+VfnvwFLUThFtq@2J`qE42A2_?^u~2D7l{G7I%N~Rg_rg8icbX?iT1s$AuTKMh}H9k^J&R zk~4&Ni25KYrIdyuwU_fQ&8=WLRJs3XE7P@2Eu@;D?t(s;@vcqd$Qv|vU{YHANDd{V zV*|IvDxp8xsElW9E5O3V?Bga4GS^+HBKui-%@JFj~4W2QCi0DVt;;99z-D+ zxkD$9>3j^@;R-ak$`PWP%-~|Vbx-{7TfUjPz7g zfLSOum0uSLx%TvP?wWNOmPP9Wvrc5>XRsVutL9S=ybsPY09YKID_4vtge-fW)_k~` zI#S>`6ymP2w^B4PoD1Q;NoP*!iWdZqOu3v+hM2BXw%~{lu=Ny34TtjIPv3*n@~<-}0l(pkW%$*gWcvAx4`6Kej~LPZbq3|Pw%tFm zs{YC0Z?F8n^Z8HS?EiEt@1JSE05)La{Xgu0e|FmaaRA$Y%(|bPsy~{>|7rgNu(7iI zMYjcDn}DCfQX)fnfbn0lZm;#p)JvUHlV$spy53$H!w%f(DJ3dF%f{7h3&hT$`4I0 z?zr6-bljDdMU#P_Nn20|0VCeA8x;oB(B%da+@B>Xo(z(Ei^f^ZRXiJva?hcHHG!$8 zLlcD=W0u7c6~D;k^^N9*VWW(YM+8#i%nb^B><7qv6^Ktzs zd3t+fXoUM{h+&kik+}mVQ?Lr^@s!HP{3$;zw;T38Y8uSX7<+Maa~pft`Ecmn2@EVc43&iY zQrnn?)%r`~>@yOwT#p^gy)K0V79z6cw3(|6zGDO>?=gD9snR1z5`icdcqd*D_|O-V zsOEMQuegsuj#b)k^g+*L(|Kvei4-r9rtH9X9@jSRUx7?{@>1qx(_)2dsD)Sx_O+jk z?rd4@UG2W1p&7ggecy6EiQUhtbtONY|Jebd{f~1%l(Gdt=(UQWd^5NJr zcZBU3)I2s(?gT;A{EFQd;6At5Z%*V*zS(4z%mvu{jJoY>TJJ2YGt1VtVJY-tvBRBF zwS2_l_GVcHmtBH1a|e4T@KG1`f|Mp;g4p@J&^q51rE7$zZOk>gP=DnH;xWA|p}1!^ z6xir0i!8w{Ooqj^4Vo>CsmMxGFdhj_iZA%a<7)_B4M=^c@#kn7R2;moV!0vTmA*x^ zUJN7PyrF*6i**a&zw*OYuA_$5BH@mbc@ z0lPhpzlqgb8pZSpK^AfLEUE59=kDf2`8OP~JMvx2zzOc4m@K5hH zEktSt9WCXTGGK#^JUeh@PbVPLek%tT8JmyW+>?Tlogt^oje~B#tn2#3vv~E32;v0w z0m1Q;w&MuGj5O#3uD(&M%seU%h2E+56poA?zFSTl4tRqNC0o#qSD4+a;`97*?Ws!k zhz-bG2r?s(14L+K>{wi{l>zGd(DUHEU`Et)Q2Q>XI?t=oxI`V6hrM=OGK?Z(vh=X@ zF^-tj-lRh{MOrh#*=fLbZ&B3S2FKHBi?8EShNeG;!%C9HqOvt~EAV-`w{spH@FV83Lve8v2Jz??)AwA$%mL0QPs3+bOC~J)G0U`t-a^r zaR7Id8?2}4)r3Kh>MzLao-2R;lGHeGwi+uNNtv;7$w>)hvcPET(sNp?{cJh#L2>Ov zj9LFleX|#fr$6pavRVwCudoVcO0tHFn}m7=4d#NnTHW|3R#ryjmkRK_8)<%H!vY{6 zsAE)Gy2BqNzA)CAmo%FVRG!Ag)f%j=ttFiiSX%FJX{5!@On^0`(CEX>OlxjNvr>SK zz|IKpVfccPJsqA8QDe3Uo<2M}Et#vLGD98Guiy?q(w_DjpGM4yM$~^DC<-^E?<@uG zX(shb3u7iCJw6@UxhAG_*4ENw$jd})5G-6Dj{FVzF~e>f2PM1%gXkKYyu|xx?P$Ou zJDb2=3U?nF!_8#4HlUP&2WA2y1X2b@1(pXrEO-g6HzYpTWA$d0knF4U0+oSw8$Q%Agu!H?C9p^ZiE zAEj8M*pa?ubuqGaJ4EeYmv2xY5Od*SqCM%x4tcqqdQe)`Z6alYe26Cl3tv0Dv{BDy z)6W(|x+=8dOeQd>`H}l}|9uE91)ZJfXRpGIijtg&Hy?zr5=3|d;pIjuU-uBniFzrV z>OQVS+V(q~O@28gdio`iWi&r(&E?RC(DOGBU3;C4zB#~k6$V*vMW{iqE zBWW4GS(S1}z0frvWd^7CE82)dXAHYBV(@1GWGe)@2%!o987nOIJeDQG5PD2GpUIO@ z($PTd&x8GYJVr9^&myEbMP2F$nhAsD*O+@~ zM{ksgJ;n%KK~Mo%foco=8MLpKZwbl;^Rq_;kS_}3^EtXOln0W(T|@yobXzD!21y#d zjdu3IqJZ3lCEDqd#7qDoD`--ePNQ*|s|JY;*;935rOPPoc?UeFdpQ zll=uFEfuW<8ttH3LW~;8)$&mJfqn&MYn;b(7Ld|D#6>nLL^#uI z_k+qK?bef*b=^#D!;HRgRmrk(`^c19Ep&Iw^l0ivAqR-6D%o9XLh9>D9h#6t!6^vj|y$bOPLaf@l6}gw#oRHezD5{^Ol$ddj-5_0c=MLjc6MDYX zc=@7fetTCvTl>`~31p2{jmTJ9(qszhg4zq)=Nw;EST#Bx`#EaUKc6gR(3F7Aol#2}?&0Doozi2Z5CcLW-(m zK>s8a;v8LL9-T5*{1Houm$;Oe_+YxgW^h!n*wLlK*89E1SEF1Dkl^X%A+6^rn#bKU z=Nr9*EBY@%iq^cQ7pcqN^1TC5PT>{jBg#<#qk{B~@p<@66e0gelnN#r4MHC7`@_7C z%TT0sb5bK?lK5edPQ)5W;AqzY^RRWWP$Oj$+?RfuwqLByyJdJfX>CWwP_nZKGtNla zOha_L_mdFFVdb%rU8B9tR>jaJ6Jf=}uMxyS(pJgolu)#2#AxDIY2yu|pR_$EfQQ$q zPN;{s29VASz;8g$+Flp?H=D#X_xq&l4?a6zk-Mhbygp4Q#hmj$KM5hdyLOxwx@K?O zCkeDdJ`Y#lywaT*%>6XD5+vQTAD5Ey83}41awcw%cRAn@yR0>O`sX?*N%*`g2N&C~ zWGu(}cu$XSid|UDdZ3*?`Ieb7ZEJnyScL4<_`-cmdUkM1DkJUJe-1)(F(}bo7jlzU zt=kmeS?&8Yn?=s5<2-1w(m2>b>8k-&gKx|3D)dxxV#WNR7Qo{1h`Hz&bLyzY4pB8st7@Uut&x|tU-Qjzs zZShG=(|JOFeUkO;SZ+vf#4l*bN3ueWd>e5iar$S<8ua^U>;QOpZY}x@eO*s zVv&Tkoa@mhp^qc_|NT23!dRuiRSTQjX)*_MUfIlxTJsMnq1w>%Wu)8htV5R<_GMP;b~-$qiQnHD z-3ZQGZN$&c%&F9ryuEE8*ziPg2Zy&qVdeH5C~XIfEZ0eQEtDi&d3!Ci3Fz5Fs)esx zgK2dwg+pt#UmnCePPXvixg2&hHmb9<w^il-FSUk2Be3Bs{jmszbztrY;v9LP!_E&U4$Pm;SES8L&Inb&ud>bX3Vbws}I zUNA&G7VGazBON5Kw3F{*I|9|_Js)Q9!n58?90gfns3;xv#b?N;ds${eH*4oG$7(bL zU%o3+^a|UAq~VJ1bZ?#g*RH)n?!9*WB(Zri%9?<&Wp_?ObNyj{rJxa>wi{dCVY4!}6M zq@GEzA?l_7PiH=fEZ1v|EQ{ywf%xZ3^g2&{3S{%TZZ6MX^*Zih(}cBjplgIPx}eXx zq3ykD!PO6R?R*Oaml-q}8SfIuw-y%!_iS;fy!Ei|sx`b(?Q|E{AeL(U1f391DrHQ7jjufQR%oiG`O}$Ot5h zG?a93bd+Xn*nmVWGAh{4AO*gm-%@%#-d;T&hgm-`v5SNrITjFUA|oo;UQ~*!uv_1{ z-8InO526a2nm{|lc*2X)9g!oSv>`Ym1Pt>kz=o@lW8~aHHVIH8;Y&6se=zA#>BHeMAP!x3lJrnyK~b#XPG6#sqG264qy#vF2Y=@-uRT zPnh5}qk3;V8x#J#pvYO?eU13g8d?#vpR{akLn+et;_pXRyUoP`P5$ryx?<}QeCiR- z+nGRVuLz(ip)-F0)^hm7gHB!osA2Jp0GYS~{IxRww{|Qw44xVL^cJ-F@BA7NX6Y^Y z7fg2mVn1iO&VUT;>M3`D&IQ6FJyWw7f#xaIG%9J`&l3L>-v=;kF6IGUA|DWI({2gf9eD> zxAy!Acx&Bd_-ljn>DW0wZv=~@x7_fqX2Qn`dCHkiMdJL(1$_U3yJnAjH#ZaD{>-+r z5MlrNNi4h&03YphiBDEQBrq@pe;9bMz-Lrsk>JmWKGwVhL<0UR7g}~LiGYEK=PP1* zLZnf?VdDoZzoIAlCkQ0%l7pR_0;)C;38%Q#jJQb&P%22qVts}%0{n6iY- z^UL1R{CbblHc-Y}rPbQnkLtZ8?;51T40vyqx|>d+Ly-EM3E0b1w+rY~?fg-flRWr} zZ|y4(PSTKWWI&V@W1}u$oUHonA>L%CqDxK}Ox5`Gs_;RxpA?A-zOf1)R|m)=0@UpF ztGLmnZ)w0&MQ6{|L2nFtbz}97oR^|wmzS+?b9fyu+Af@utvx@upr7`%xePo%R8UeH zN>^7@dmBl|+o9RxmsWb(1?MhxJ9>%Eu0*KGu;IrVF&8( zep9w6RZwEnq$#3AmPVs(LKuFc<&|*@%g|KV#6a+pvzYKNvsc@GqX(Q8XXzn+?kpv@ zgq_ZS9<*f7KhB#V)88PEpQ%M{^jPP(E;_$LS!|?-^d?q3ldb`fGl5f*j*Sco(2MNe zCe0{OvXkSpLmk90e$r$TrVTEmrpA0CvgUz!OSy^JJG$HlSqTBWrN2d?>Z`GGYwfDv z+ua_>@UANLd~LaZRh(CA#RP3KoITJkDbv&T6ZA;pOp6IRTkd<1qd5~xFTbI!;DFjk zulRnjNc!X3%FGr63y|`op9+hg>yH;Z4!AHWLa_)NT5CMiRH@+Fd&%-};=%`FDT!9d z-Xs8{zr{C&tQ z`SlS4(kEdH6z)*SW9)DH$kt0WL0Vx%d)IW(j_#)Eg*+6Wf|OJM%1eW@zVJ$mf5rp|(3xEGuANTqQI(sxvKMpU8^GX_$qfP%}t)D~CT?X%ffs z4KEStlei3D`HY4TE(yXoEG3>|E}sl;YZL=L)i?1)ipv;SIsyqGL$KN|HQMn3zH}zP zw?q~Z)`@D=JNwUr#0Pi6L$>z=sYhNma^e?GhTJ*ErFcx=3jEORtYrxMbQF;k^(4W;v@0c2 zjL6ag83*FLDWm9+a_mq)-q!QoHvERX_7+*{kQbnn_xG1i#y`n| z&*ymzoY~iX7Annk9Ha^aT`7~lgNe^vIH7(LOeTm4CPAZ3zK3HSMlzRY0m1wcKsj8q zI?mkQUCSfGhun>Ev9GM|b@5ZF9)#NyQ2sbCOuMa{gjlP^_~o(1+4J!==#BFNWzSS# z>FoYtm=~Xta!hcE&Q)*TDSEgB&$XUI6Dc8hI!5R6Kpreae$&p&UfSt9vG2DlBgIjR zu3t<1mE;oeTB6Wq3W=o6=bXYp;VIh7R4Lg~NC68z+dc3;m_{4=&I(wU(Pstnq3-lx zv8=$cqJfFEEw!Il(eYprV3KKo86+h>q2ZIN)ReUIno{NzMTbOmphkDQ=I@gWt~ZI&nf zq9vM;3>jfwO3e&h5n_=@273l!y|FTKkufor(IPeWCaI4K!#M{lzwSw|N!r?UQalD) zEkYQG#e;VZP~m1t`FbqoI1-@*Dl-6`uY_)df!?qiR9Sxs+76533Gh+m21jbMEMwytfe<_dm4<1 zjYj4WOwNx-=)jnK@dfDyJ;55jVy}?T+bUYMd`mgt^SBFX>|ND0zLL?>TO|hxGnIB; zNT<8PGb3%X=Af#?)4zx&45N>~i@e%ypU_sRcbUrWW5(E{q3zT-1(Nt&nD4+vm5H-T z0po)$CHZ*NnqkEU;g#$|4JAvPXP}_Ll8Ur~)8xA*@Rn#dsBVO8qs2Hc$K`QFI`0-y zm%iMCphdEyMXY~L59=A+U_c$O)&~qn`WcWfDz-&Jl`+^I{N4mm9>N(n7D1HOA3-h# z;TRM0R*E8$k4Ky~Sc^g?yAN)B*OTcr>6t9W8PRMZ`_X__0EJvkd<#`Ad3?0*YS6dv3EQ?Oi6{Za#6fBpmGL8~2p*9Unz8Vn)A|sL6BlX0-9lTs? zt)&#!`p6L;b3w-`eQH)&|?JZi3xhgpq5R&ACkl0n5<}-&woN>P@&^Y}Awv(9L z)vx7Q4zV!or(5-TLKL)@yL*y`J=U}67`CAvd99`j>?;$Nv%sy5Ra~nKRO!RNNR0=dR0@e;Y?yG?8YHgKo3XY+jyy=7U|Y=Q(cpKIBBxe z$_y3ikvfC%OpXE6!J9>vmNzW>tN2o(#yLC9g}6?>TK@6;dzvZxjI+`4sD#QlYS4*3 zw(*nH&pmeExT6Za%}+gj$h?;^68_B8R$8s zBYz;bnR}@-&{n=^Ehg|lkRxva-WS{1E#uUKHgZ`9t&4*g65!M z1G`56_(dOC%|LHu|E!C;oNDXRkA$~^$JGn0Eov*GGZdS~$%N?s4$qNp|3-SBoQ_{37lS6*3^8@pD0^tXGs>9Ep(RV(oY=G z0EDXFsUXCLR-+@U1#9s)VZ3~Dr+x+~4aU4<)@n8aXGkHBWA7^nj_j=hKJ6hO~l9DxiuE|tU9j^T=+Vd6(4Wxk76d~_4Cq)i$tVL_2=(Wi8@B6$bjUvT{@p<1l3 zMf(rL8bLss!qx`V)n}Wl)*Fvr)~lKg72X;GmZpdvuW})qsF`tq#M>+y!>@+!26yXP zUTr+~sr(c%Y7$L0^NYuH(M4ADYTBV_<$54Fz| z*v5(lTo`YTSerG5(TnTBOGYEjiuAe*S^1h|JU~Nl&l?);UoG_ z8y`nF)KOL}kL~DbRfz6rO1#z|gAs%rJQ2YWM52PRiDJ3HUrD|u?tb50tjbM^^R~(j?&TmiV0dnK&G~M449Px1j6REMjbKu1 z5{QK@T{Bc3Y%%bKO#9>U(VBh=RwHGDlkScG2EQ2=%L6# zD{@Sq1Fb#`QdqHQT&3V7-gOLBv-l(-1`+>grH%^ARdM6(DDLyYfCaD-w=O3-^jEol zwMWRsp|gsAXjyhIrdDz}RXhH{IEC#Z`bN)oP+J-ugvGO=q`Y} zc<=Z*AgsM?2Sg|dHKEJXNeN|JS^k*!B&>Ac6nenc=oRP4Ja*LR{k|hLsn-bkt}uR2 zclLo772fW7`k=2){G;xRm5y76-wbN2l02hcz)}MeQZAZAL4A8n@XZVC_j%X70l!l{ zQ3t^}8(@yH(1qP;k#+uP+7uS|BotCsGvd%3E>rmcfI; zfiMAhjy)N(8L-fCRS?3wA9n`5PWQwXK`jkb@C6{Hi9}6pSh&wL zhSwHNU?5+0>A*uHHI!+)$6vrFXvD_B?$B^Xbv4N4@Dzy*=Jl zh1cskd0C4|&kQ*7GFsW(!`CF}@^wcV?zc#(h52eBNy42~&k_E&I8b^`PzPk%j?Z5e z67q>@vIVN4hb~~JxI#q8@_k4RMZeCxMA924U~D@;5(lP&(n+2S6+u1IxX(yVEpWM7 zLhl9BbQt{jE_M7IvUjRUTo{*gA1G?g7}kD3RpMU4G}4gI&3b{JS#NqPl9K}p~! zmW?M4CjF5si#P60X$!m`+N-Efhsj*PjlH_AMXF$d@_6w+zx)SLC*gpkK2;X;fkYyN z_+xn4PBKFFB@yVT2@3c`hzQ!QqW@fUUX}}ndqbcvtt7cLZRt_|JWS2+N=Hu(v>O5k zbww#hOF72N>Vgy}YBY^HDq$N>AfF7+3_8|zRm3{#(dD7rE&^+udHtPN2gXcLd}}nD ztV&P~a}S}ZcWgSo5vn-MVNz3~p;42KC!B^qJ0tih4;+pVRRL-UFq1VJLljqWaFu7R zlOjq%OpRbo0eL&7G}{Wd=bG~R%N6_(Wwb+PibhI66ZUw**0ZOu^Xt}T&eI~edxo1G zHH=Lpog@YxI`wHT8`0^WQXL3M6BY@nV2e0;7dR|fCuml{YCslkfG8U9^=3O&Y;Fi) zVv7hNkGd_I%sc_?bRtq$sC*1zu^7QmjyV180V2-0iPs5cEPoEpnyw94r=L`t09Z(1 z{wOw4xnf9Dg3(ILq_ABO?{648ngAg9HhMuW7EPVLuX&^WsOsY0d~l&^v)i(U*S`mL zLRA#atMT%nW|qfR`B6tRLr_~Yi;)ozGf@>D3pZfPmB2yym7~2@0CkJXPSu0YLr;y5 zuT?Z2;|E-?y1($Rufoy4s(W3mGAKo372To|5pNI?`5|VshWi|8@7- zVuXD0F{cqk8KbX#|LPbz?C|ik;{ZjI*$aBJV~wrL^MJ>_bH*udoFJ7Tc$#FqVl^lt z()0Tl4l|8_&qiMhWQm!<8fJhWi2He5vxr(nLW9a4q zhux05b9s=Sk4&vt^<4)!|6tnaN?&77jjKOxC=g0pg_^;0Lb?ginfJb*hL`qS8T3AQ zXw;d9UfnxHX@jPp)ME5o;^?<~yO`pr46F>c#d^C zG1R%jH%N|T<89X0DcUI^UR80VZwN^52rqfm%^6y5)kcIHA_MTVOPhzUs}=zrU+MJ% zDS1A?9YJ%=O_EP83Z@f$lK@Uiyo%OenUJCF6XxVx*#ZH=oi|A4R7TSkhh3ZWtZx3Nc5*@ zMHd%e`lQp#C8}t*Dy-79zCryqfta)e4+$=yRLIvEIg9TqthBm+9i(bk=}d>3x>NnH zA{h}50k&dDiC+F|lbw7tte8}wyx8Tth4hP={$jkW)>s4pZr5hIzJ8IWW=FSxCzJaR zJ;+2-zD<5dt(u7=W$IU;v@CX1Lp~*Sa6DrCs+p1WO-2YLm5x6DT`knVShdz>SHj;& z>NlPAewAXY7dURF^C>rI#Y7UZqh}oteat3wJ6ASCJ_-2S`aHIB_+Ju9f7r3gjtV2D zY^K>pI;%vzLeyl+@!`v_y@B6tTz8bO_Qzy0OV6%f#!`^kHIwx8dxx(!6D`u-R~+oO z8Rvn)Vbo}TQ%J?e@#q@Nf#*Yjd+rKEkVRFEYiHZ-JcHdyM(;7pd}-Vbg@7k=(?HHcVR(=8kh1Pv z51Vmas06%D;k9M%*&AV#jC7$`qLp8P9Y~pD8wwiit2JOF;+T07yqljMacHsvJL55L z9d0^FLEJz@TFv5wTDp z0xgtl9i6mjtKEIj*H$1|k7ZqQCK)gkYTdI#op_3a2FM4q=CNdJ`L}gsrlz{Km*#Tz zzu4_N3E;lVu@)*-kzFG^g^H!%AS8^C1_YLY?&bIF5Ae8?%SOC6w}z_V0J@Ce#r(_> z2?WOdbIUrYnL78}ne^E!paaya4UC)P6xx~Rmh{;#6wA3Fdmv~3TMhP?*^#M2Vz3UJ z9bLf*AH1I+(RFv^pD<5Cd5E7qBK<;rv|;zt2#f)UR*s-Z-8e(FsGqwrzPV@FTT?%K zgszj+Lfn!00t(V_h(5?DOkvAh-`R;zVjs&~^LxU0RiXiq9v{g2uWau5VJP{tKNud$ z`sgfMNZuqy-F27BFjx)UVAfgS~nsdLH2}7wD7#yI#)Q(mgc7Y7lgU}3=!hMZd~~X~ z9VD+uX;p^-nenqHo8hFHDKxX1JKk-+Oiam47CM-E>U?N(RCOdH-Ky1b$TGQtX1tZM z->_M+v^hw^ zfVMWZ6uNBsx#qb3C^~-Km=22R3QA^1MFrs(OCoxoDK&{Q#=wZooO1l*;>r2@(4)Du zI$e9+i^{2;uki^fLAAdy(+t-LJ>xI8qW5*AB{Q_tl@>VRW{WtD4Eb$&tTFaO4@*38 zf3!3d*UWQG?@lK;&Mt?IC?p?lo=1-7V=|#w)(N@0mJK1yD=wlxGu8)YWslcW&7239 zyL9eamr_?DspfD}y=C?r7dZjH%8m# zat|+>Dk&{KqEAu7i<_I1gv!Y&U09kfo_1hQm2zxrqcvNAV6|YB5-)7;MTfd^SQ&lH z%vqe$GB~KIUvUe4bn}Ss{#NaySwarp;t|K2SKlzis*+@iSGHdQVV(wA534AgQeT2$&~HY+Qu;XDUnpHsZp#Any6aRiwhJbZEoClSIAiTx zEOBB<<(xaQwxYdpRAyOZ3eB?iC?Y7VS=T_8X;yu2_H?X>|#T321xD}!Cj zKBr0e0;s54WtZ8)qA;tCZdp{v@G!z3$FE!AI4Hk{SsTH?=pPn{)(npM;^k-@S6yG( z%eE+MK|;l_;+R&Se;ByT;LRoUO7JvcYO^h7>0*<`U)tX)|OR1zCNwB zYFWbUT)P+;R5uTj(UI?AQn@jCV8g`lFejMapY0Wtr(w=8kGza;7S~29Go__RUyO;= zQer`3a8}CTE<8NKZb9W42>@^}HxVTtgMn*m5SgRTeioxGgUa4qIhemNzA0Nu6>}Z)(#;?poV{%ih z2FBj=O6MNew@r#}^^PGE=6>j&9EJ|m6_sDFR*};?xwuQy0Ig7Ji^AZ zZ5;yJ^iXO--GYZ*l}kD$W+k+&%hx?GZv4jLjv*_|C<=&ku`3{Ry5{E6>KzNYMd+jp z-y7K^PwII5>c4G9XU@xM&1tYqVlpBm&mpFDA&GmY)VcIBere7bDMUkyP+AU0i+q3G z6}(b@HMjD-ynG2Z$I~XeBoB8of0$$Gm+UrIshbO_z*nY3&oHfOY<_tF62|`v#G9uA zq8@QsZ1vYvvP|BbGQtJ=;i%?tgjq#Rx2h$I>2X<=^1O49xP>LYq+|apY)n<1-XKOH z#T5i1DiFUU;{19&k@BF-#%`$z%`8+QzN~Q7z>G|`Ts(R><)cZ%#H22`iYH|{UCNXy z>vUN$>MeSS8JVP5UbQ6lNy!5Hn3sGIlUX%rT-?+nY(<~?nss5l1A;IRWq4hEX1*%? zGx(y&asz6*n;)7qM{PJLSeCE#d?s8p;c~`jU`xG zLCwQMr=YoMQ*s8zn}o#g_l>>_H!I=Id^I!qXqUokxAR`&m0i2ePIdDtqqjU8BDQ-xILEWf%RpI?WqOr!1{ib= z2$11E5ur>%E6M!x^HkP(){CfU4(hY{PzkE^7}NfILM=@#Q`z9|bBJA$Bl9X;rNv(r zpNf|;G_#0SM67e>CJ!Czkpnm&)5JV=MH}YXESQmb+1+{04pBpFX0@${W#7Z2$f*xZtzN)_YrgmVjX@+p=p z8WtECeGjOqO23*p2=Xe**Vm-S zZd_i)Z!?qx$tkIx#4al$uZE0wW@jur2{(EX(avj_M=2($PWcvtS*AE&-fyFzyccXz zGB;(FfQwOV4qmd_b`n>wra8EzR&J&}T?$C9jT_Y@S*tl!68!kTGL=JICk@!rA9mm)EBadte{`Fv6Z9 zWKqs%FJOLRRed{){tObdf>8us?8zT+)6+NB%w?S0*tD^(Lq+ZYfmmmzPX1I!RNyuE z8NV34XZ{h-nZI&n$HWro>(SBO>>P5!9bg+HZ%Tm_Cf#x|h93>H<96m6?WDQP(i-OL z=4tRuE)~Xig=Ml&SxhBuqa_q%H4pfNyd4vZMy6mXAbhfwIX=SXk}_Y(CiNhlU7EY2 z(hoDsR}Rdq=C9*ei-nib&;rF$=hCgh@~W4nSLbL{wrcMV)ze=4%ekcGqa0;&ekd$F zVH9uL5G+s0XI~zyx`mCEP#=?_9$FmB2T{m6XAoK3EFeDyfwJ)n&hZ8{Xl=-Bpw2-( zEt5Tl@`W>b=5i}+6c$e|U#CauX`5FTH5b$uWm{KPwRs2_mmi&0CKjiRDtTA6m1PfM zRxZw#IpPXin#YkcOczcG=JaEgl<8JhbMMqQ^2cXo4k)V3Z`Ls&B0gFoX{(w{W>r6( zq>?O4MKWdNZ} z7VF!~dUX7o;c>1XS!3%n947K{cZx~q>?@p{_>zErirVXQsB<@Q z!mBs@>xKLq3alEiN|1<4kLnu3>q-@CiqOq@YZahL5g>;-l3rmy6sHI;~8x>Y)y2%iC@0Bn?pyez37UDtLbL5?7@utY=KZ zkkYSEgg0dh0{MPB9#Kg=y<~8kwCom+l;c&{opYP~G&c^S@0?ISg?Jfo!&btty6rqG z(2Vieo}b_KZZb@|YqgWZR~dswk)nBK<@;og$9b|np2ew!<6EX;$+GcvB8IEm&MI?lbn#0 zjzp1x)%7}UrkUP@nF?+t!a|4SL2f{fnqr!UF?OArOUu*PhY&tNiU)-zwzXDIW7Jj- z8qH4~AlufGulajj6}#Tidfp@V(N7CtT?f6F+FIIPrvif02U+T;4DE96-qVucXKwdT z(#9Muncnl^XNzy*=f$R$A)b+JbuSqO5n>&kFwPi=PF}Is4p-HsM^4x-iVu2w^bdY%8Lz|m+K)T-DTF<55a?Ws!OdQr zPG{>4j!~L(`%CS_$7kh_nXf>X$+`dq_|x7c>x+~<=K zF`EPFPbE$?7sxx*tmf-+9)xz7XQ_@G>f9?e9>2ft*Yqh)6 zSikId5?t?_7ws>k3zWm*Kg^*UXhTAr0^J}Pe>KI#hJH?q+>v&~xnm=Z5|E6$1hdTp*j_E@n!be4=XQ^3V! z0d1UqNOJo4SfO;4hW>W(WQgdVLDF(n&TV`%7PqD`|1x&bdz<4*-cuvY49+#2shI4U zrny&Q?P(=E;9g|SaoK5>VY)EZeGD~|2ik9JbT-!Je!;#owS;Xv`||5$t-`uP`MED} zJ7qVOeP_^a!E+-u&%;<*uy1I;{sD4)JXhw_-Tl+VS$g zdPY81CVO~$fnRZow{uHma4&SHZt#s)WKE={^`x5P?ui}LyklCn%hSCz^R&xHdspwM z$9v@9Y8OO$Ti_c5^wCqW(mNd}91uR9A?}n=w~Z>V3?^t@GRHsPgi-8PP(0v;d5lL- zJnzWIKi~0Q4h^7=5+e@|pzh>Y?od-aL=s}RW(<3DM&HQ~xvvbioaNG;Rqv#g$?c{o zT;7^p&g0pkkrg0EF{@toAHnTH# z1`twL5K&j{{Gf1+A{;pTVH{DlLn0ngxdR-J0pb!QKX_%kGieW+#82URx`a4t4?1LT zzHbbTL^Dy2GZ8-x3O)!Jf4m{oFBCpXPq74=;E6nl4KP1L;W{jyejU9*9N8wi)1h+t zS)Gm7Yan_+^>T%vI*70ZTU^JlUcqku=Pm12aEBDHIFbmKXC zqtiNub)4LWD!fSbz2bPiNbx%&BSBR!0U#~`#7}<^v!ALD9_tKwf)6>Nct0^!pRD_h zGR3cgw>qBj+MS_2rn7@5x+_JxiygG6UfRd60^hViMYDqslECQCik$i^tw-_Iiwz-} zEN&5|d^1S_A0%R38jN01t^P*XP?kX7P-W1T`uv^nNX4#XwRd${WWL&945sN~KNrEZb?_wJ{__hRgiBQB*Hl%A4(JoHS5TQ1NBeEkn^1;uKT;5YQnMX(rnm143~t zqan%nmCgTL-7Oy9Az76AO@|T`$WWCVMd%d2s1ySdKU)yV z7C*ZMEI}!51_gJ-A*UHPu7DgQ2}MzjEMKx;pr3#^pB9XFOeMJ!1wOJ%CT>t+@heL$NBNwBpy`awweQ4oY&l!v>+H0^rD$ z;tZ3rJ#dVxO2%-CM6!IMNm59m#x&s2(ZHbwM!aCEG(xsLWCnIZQ4$Ck%`5xeN8pKk z@fy&a71TIaMS`Oq@uT`f8u6o|1iDXzblE$K2SAiwg&7xa;c2q=} zzNd~wt5PI=txX$>rcjA1g@_)ChKSFtA%MimRUQ033p@$|FDe;DTLdl=`kjEkV+Y}# zQjNGu0E#{#B^W{|awvpCsHNARg@`tapx-0`hFTYHON8&c(*;k9%hwRlWzP2lL=H_{tG~@51|AYE)A>QHTYI~;L|65QbmOtSh{$x-2lOXwbJl5X{r6momEUYQz42>NvbnV_7ej|mZ zeP?;*wbwVaa=>AwW1*s>qx&F|{+QsSijJOfqa^d z^_>g)o$c^n2&zBOVEOp0-|3?%85voqXlWQ3m~klSnHi`UY2SIOahPeDshFAR8Qy7F zXc%az7(XU?hYbA~_6}EJ_n%Xm2uSH zsjr!Em}uyzXc%djS>DwZQ2wp$Z{*yJtRI*{{|$aJ>px%+YRZmS`O(0<<0s!HV&Or2 zjurbtN>J9D1s!(_)TvdNr$@1kasQOeD4DBPtxV6ZU*Vi%?^#r52h+A9%CMiQtfrP) zP=fdc6H2Ux1+d6}1tv|;0y3<%N!2Jwzw9Xl5t!$Cv!4woG^;!FWERi8?0^ zd80NV!h}~GMBQ_Ov~S5#pR>8Lqnaz@-cl98HG+)bx@6p&>#1}u!8U4ue@p`Q*FIMq zdc19M9%HbBkU@ghXf8Px1pzD<9?`;|aDzZu96OgTvIHUuy+xMJv+}>=m5f*ZCd3D;2<&OTNlT=5C^>2k z%fJ^uW)X?gp>BpQ2##@U$p}Uu*h<1le`8+O+mnH_k|i)Ps>De_jAC1qrb#Sn|k^XU7u1noi&U+pARfAjGsX6G$hOab)gwq z|4VpEI@*7v+`}LikGuSl(;V(3##{@!GR3i*5qeC2>TVCuBWzT4D1cANaTQvSj4AvYC!S){ckX zplXYnMV$GP;B2bNK1&%pz8($ds9+aV(2M`17EofDgJOVijKy3yECqly*pG03ViTw! zJXeeX1Z;f7G1$EkhpAORaOM^Ti zA(s1$n~AJJmBj5j4`hHPj9H&%nBk5F6A31}V5$Ew;Hq-p(st zO|T$b+Dcp%9m+{VXf}0j{AHwuPbRHft}EFC;8ABHiWBhXfm7znDAJsnNM#4L zQGVDr*g-EesaK_e$MRB>v9iib0X;w{u6xKwSk%SXxA4>PVg0EkyAZOu^{jJ0qU~GW9FbvDtYF z&CJliihw2m8R;o60@28ugVzL6=|F?0YO{r;)C~7a$rdhlb0An+M{MsZyVF2Jt_Y=*d|D=_ z@ns61n8@NXR^(Q`^ySn^rork+bT)gyz5L#Z5hiIZmgNqdcGSN(e{+1o_6mg;)~2Mb zSKSNQG}J$@N0HN@LNHI-ShXppR*GaE-%Jl=k+(!PUt~Sno>ZTx%s|~_TkAp;MjN)k z-M7*240ja}n`W`ics`aiNFJqpokAXEoZX8ZjTwWl#(st|C2c{pyhh8KO%KjLov6O8a=3uG@2gwJL=X=|q_lh^0l5Gm5emlYrkxEarGq{nYC zurQxD<@bGxA^cIL#3-1?A-tq!K2atKm=CknVq2>eQH}Y`NiimoA;D>*( z4-SNI86=xB4uXpu+(i!98H`dR+$M#Jc;^6!iTL&kV*v2G# zjd`l!OOz{XSGe~DEG}89Vfd@)LM}h<^)iWR2?+`!igPZjN)iPCY@_}hX0eR492ylv z>0!_4b#Vyr!l2w2TTEwY2(A?4IzNw?5t|*WERb}|ta6Tq9I??a$nd+LCm_X)UG;RC^sEY{~v`ii_y-hYMS|jJ^lKUi{MF zgw2qjusvp_JN!NJYUwoJXrn|<0E*| zb9i^aai~8k-y_uzZ(354G8XU@huK5TgP3`O*_)T=}KBSY- z{o6Gi&EMwwJv3#Yd5=EdNkHGp|0r4LnBRx}FJi#I`tHBM&;KsbpXPdx>OLfSk2;l2 z4IE6~9shpKo(*7qnQ(AsO*8fdtQCfZ>#{bPB|4D-XKef>c(Em@es{T)! zj#l8EC-lEVA^v|V7OeowU#<9F{QsKxgjRr8@ZS@K{FVuMe~cRcb|W$VmelyY_KiHNO{mKZ?I)3I9qX!t_o> z@<&eS@9?;PapL~zNn=0@_V3Qs|1t~1!1xcSyNdGcR{Jy`Sr|OV$%bg za_CyjrGdrNwY+uyk@FV~bug3igE<@i9H&z5Zk*PvC{~odMz~Q9)$km=k$R z|HPG-eM?pHdvR5Fi-Sr==Z>#W%{G%^ZS`q;rY!~e;kTw#$AZ}i*k|z(P6jczK+ZSc zl}w1IZ~9!f<5W+O%JJ~B&6~Hppb!U>koi?~#A#L6UNp@t3+BGMML5_lVIfz0aUANW zao(73fWl+#68~-|{FiwrdX|46V0`~#&u_FS_MsX8A^sje|4xgNQY3U%MuW?}vX8KiKJ{Y^bpxRc=SZo%C>kl>Ku76|zdnVCD2x!ifV_WtksR@Mq#r@O1WYwxbE z{XEaEdJ=J~7JB6)1nuVik@s0XJT7i^J_U?ie(0n5FlWM%Fg}6>Au`;Q&-R8y8_%Jj zmox2=9aO9Ykrb2c5!X{?C{fj3f}`kC`s0jgt|vc3r6s9Ov^f=+n5@q{kLRQtD8GH` zs7j1m2Y_qtLW<#sx3;0xVO}Oalus-rKD2VeSjiOUM~j)xG-qL_w+53x5yTGFumK!` zH{ovt&jk*ju{5{Okg8k^KjpjP{U&Ssl?_MR$3E=?X{K47y2TS>WWC1%)EPVJmDo-E z-)J_RuOY#lQ;hDj=%^)b=y%F_$ST^`^ac1+V4=b4jeO`hhBDwh-N&(e+HbV5F>DO5+{ahqrp- zHV`M~``8L|sK(~82R4RQUbhwi?;T3HF;zr_*lxSF<#Pcv0G)WA{h=6Q;->(WQ<$o+ zZ3}ADx3>(AC%TBcq%v9h!l*0M{wJ@N(89IE|l)xex(}2T3 zFtQpSOOx=Veqrnd_w(r?;gd%}jAD4X`_*ke8Pe4iw{Zt1`t2ce6aMDsbvh~an{*L{ zA|wsu6l4w6l3$puw>Ha*%`u@c61&V;rs=F`w&!WPA*c#bmxQ+(S}jHl$F|2rlx@2Z z96yKksx1V8T^ck#DljHWN}3JdpH1H?hpOx78ACu?=onHsc0^Vbf1&EV)m{*}Ab*kE)f(95xP$BV0DhPkLy(?Ws#Bp} zUu-C9g&=6BjhKU!mpJjo)EJHTA0_t%FF;&6xb*6B$M_HABSyVhJ6(B=2|oS_4?9&% zVpGrnL%=|VLII+cC-Ad9CKo5ZYYWXx1mu4QigZv0JF||P*vkE4< z8Lx_^L#X}04L`q@?{*)qydf^C5aqBW-i)ZtnyA9zL=t9*smLtU9f>o!7s^NEi6+>) z)vT9D1Pb(WCJM&E-e<@^00@5t4zaWU1P-ah%Ah<3fL0{;L|__=>G0S;k;O6wG<*l5 zUc8L4(CiaZR2k#xi6Z4**KIVG2u68B>?YUAYu2PzLnH#$-mPfGxW*Gr*j?zgBo!qoZ+HXbX-wU=J!NVvmcC`%ky&%G2y;ZcW_ z$9h5k0v%6-1{yW`Xsgw;Zo;04N`b&)?x+9&x7@qhiwrGE2#p0fg! zc?&nC#{1k*EP!9L_Ur4~C6 z*Ua;AE}vw2>3%fVJBT%XCUz>jgSc=*D?&Y z8zJGd!=A0?o{I5>wboZaox@B%)spgT7%!?Ix7j4a!)y=h2IEUBrNXP>M5ktYgwxHn zGZ?etpmsewBZ&ZRZ-(Vk;~=pHp;Up@-z{w*ZPBM;+AWW`&u98##;%4|sa>Wv7FBTk zMUkY%szhTZ^pcwL_UG)}?dmt@jPf#W4EdNbW*o4pJ(`bzl`0dP)cD3htKZ>Wr?{Uk zJ0Mx=BpvRk1T^fIc;RJdO5w`#NKIXYxJ9)(n^7FfW|rB{zg@+lo_>SYF(36=KpC1Hf$qxQ6Q3RU3(ivpe(YO#gIi=|6x^7mEKDs*<{~^5?n6h> zAkl$pPIlU+B&<)nNRwFMjw`W~b_+S3H!G(;^kj)yd&52rCn;C$1L{Y?=eGE-I!pGU zZaO9nBW}EndkGJ{eN2~)i4UQ;(z`HWddu`sT_9E>+VsCiluif+e#z%*u}soM-3`#a zKvFZJ#_MsdJbF?pm~HsY>O1%)eC@$~)_x!+%)tTpBPI;8b|v{Q*3K+CL>+Qv<=_Zi zzg>)8&5Nxw;~>_E2|r)SlWEk>lTno)Cpwl$(KIY6YF>a!PCB;0mnp4~E`I7Q2`F8c zc4gPvE9OthJ9^xwy2>=NXoi@)MWM)0mFfmL ziE#->h@@0dzM>x#crQ5YCrz0fw6h}N1(TinKA#qmZ_nDC9FxANY0Jm?R$NZ0UIQLp z$g6vkSI-+3@-)`q6d~sj%?2Jb_%cKn(bHO(y!kx8oSKkmn><#@C*?>`A{mS@&chUt3?WQk$ZC}ZY}V%Vp9*Zp<&VD-hBT)f z%y#NH$!nr~j6l==wgQIEjtAJDX`_CjX@v%7Vtu^1MA0M`+aNN^@VJZX5w8#PwgyIy zc1_|<)O5ceo>z6Aw&=)UKB97~59(W+;xM-+gfNt1?^A*Ze1;5SuIK@Fe?$d2F6L@1 zYvaW=Q98;k2LZ~ybQj$ERTr3VQ6vl6)~%`6;g#|IxbKV@QaM;~3%o8_xR72l0ZFNd zA;>~%YOqG8Qx>kMP|keBnJ;kpKd^~K3bHwh1Ua*N+iuxrY`jmy?ET`nO3L80kC@)j zJbL``!#Qy-;(EwE?gqF67at(ZVdwt&D-$PW)y;wgb>Idqs4ukiWQQNc_c7o1wzpEl z9~-(K6-aU7pkAyL#L^Qg`z65Ia(-RwnXsKnB4SL8u*oT^0PjuiHU}G$!de~Nv0%+G zQHw1Rt>i=0I+b^>ZNv5)fA(CL2eY(xDHst-^Ib!t%NRQ)N_TgqU%5{l`^-S^e1MIw zi9?ige`Zw@co-IpuR|_lDxI!VdqCwFxWDWTaZ75KHbYE1^5a1ko`iL|7-0vF?{9U# z39UJIBMRHZf|PzlG_d?)uSR>2s>9J`|{UrA6FxWj(SSfBlfxI)JjdPq?C592R|3B zBW_r?ZSNT61T^2Qut3jBr1%&MQbvAuEpn!$oImrlLA-V1;4Sw!IMGoraBd*>fKK@A zLn;X55liNy` zdxgcS^?k1M2T%FR zJC>fz5)M-gIWx^S({fm^ysBTmoyr2`%fC<}f1@pS4Rwx27i{)wm`)jA5~#(M1H>+f-%!ZVZVie7K#QZ(l6X+q^d06&@+SC6aZ? zSXWHuN`_q*jgISiYc zAq1974Ij#l81=$9fktK=QT(QOrm zpDO#vHyS>o68(`up9#mh-?`5zj5A|^dr{5Dih&Eq+ckx6g_lh%GtF%N>iN!-W?&}) z%Pg$N7ZY6KISj@X8v?!y-w^6+p64_h#aH69i(kj8Zs@;*M@MCUzR!*i1oqgte};2n zWUN3-tv2|dgY4^ zGMHf&=p$iTC%nQG!UX$l^YU@OChS!6-MO6>0pZq&t&Zl2T}j8acH=V9rPeX`nGR;w zB6ua|G#UswgKkQ7bd_&144=42CdO43Eg-fY(KSO=bcKc?ltsDgb$t}ql}4)2E3f@B z*8we8;>5(gk{_QN@QGs$W>1*MwaD{vgyOqH|HZ_4*uf;x`ZwWgj@&&{2)6iLnkD$2 zH61<|hKI9{-8E;YqhKktlNgthl8*V!jM#Bsr3df9Ii0fy&zPz zkIK03yG?hwX@f&MPdF%L`41(7Y^FRc`sA^3tRhx&%Jy3Kv@;P^w)O| zc9&%FYxjLu6b2Rl#uVQz}yA7?22& z1%!GEK;r~GZ|~XUD{XC*JZSqYoBHAOAl(KxkyO(>p01@Iz(~1>Rie{c?czA|_6F;O zw6`y+YEFTqwK1JsX9>QvGGq%X;1e(EDvJQD{u1(()dl!PVaD^nuOIT_vIs=*r8?m_S@)@z8w z!R4vb)xFFj42gaY2WbN_U(|JyMiDw!qWbXN7=UGm;&k-l(7!w``EwOGczC{6y*-Y9hz#vyNQU#T2*!XLVL?(wFe~W^m%@POBD9j9)|W`BM9u z7dzIA!$v=0!j|c#ql(+&x~NtR=XzRn@TT#=bg688vN=5>A~*Ru?FDXr?ALLhK^_S9 zv$6dggDJ__f!u!a8cUmmt>FjY_{xc`}8Kv>&@|a0JL9zbX zG;m)uz5~#W2L!*)PauJ&+NFNx&bdKaq+I1?10#W2V`;upv5>zp-lDm5j-_&n^$d-a!e4KcTU)zsJV2;#iC5iF9`;gG;_o~$GGzp9dZ;v3ooMNOfF=$`7gtKBQm%VvJR%KUt@u7 ztb>(D(T}8N^09rD`Lqqnxfo;jwtZS2-s+`5>2qfbt#ZY@7=L~R4_#k^<9!wN44QLv zi#V_0R#U>jZOkof3vJ^qs~2CeWo$$j&zWyD4kiVP%jY-NJI~>pjo?>&b~boz<~8e^ zwI6XZfkS8n;~4;9piP~+!mADBfIE(TNcq(L?PIT{c0X+Yg5g0&+Ni6a zxpe#c?u?ca&V!AJ@APsjAd~v1>&iF8O1w|j>jSVQ2Q!+eyiJ-Oo8O}`O9kIP7g^`A zrviBEzw+IVZ6YC?b++TGu>Z0gm0+*TKS{|koXuT&NTPb$za0Q;wl+f|cJ8BR8m9kd z#+*-lW@}^$-5Yjkr#SM|wA8Hjb?t2b5>mn49AIHPuyy#Eq~*ZQc6 zVEWwmXd-JR$OIA#x_;zyc&U)A9R$^4Y4a>@jZy{}E7de{B7zeI_}#-%IzRwIPI0=s zY0roA;MWk^yU-cEie~}l;k1`sju8Tv0Rqe?O&=9?#kBNd?B&X*Fx65w;cpd<^4O;@ z-#`Q3T*DaeRyBjCd8w@2(hqpwFPy@FsvG ztY2f2#2vhy5lZ0-2wU}=4Sn$ipimL~=L(G_B_u_a0!S7DCSDHCnyNc#%4Fo_qp87p z|5y3zYCOzCHh`{}fd6H-3l8KO! zNp1DIaV2ptKbohMVT9lr6sGBPxP6Y$1l>FfShHy9h14{4Hw;wUG*|{Qf{=rqzhsP7d~y6!RhhR zeUTf2^(x=h!oa*X?6y#8zUFNwq`3mAYt;8+ZQZfJ*MQ}6>7k~laf_3vwh$t; z_dVnTQAEI>P{b&Z?%!JRw=JjJlqxwg+TU$In;o1XFJEqwd>e>?_cmun*aqc;lrc2ni=uMYH^R$I{1Vt_~|LzeQohz z^zG-bhLW%y6e(QuHG%+(3IxJ&x>TNB;?4JDE9aPD?dnIS$>N_|WZrDst?w!cJZTyX z1j=>kBzSyK2-Id|C1L?q-}>3_9=v{KBP&2MOK?S(37Cb{WcfnAF~$N&qv4^%BIVW+ zK77sy`?~P*N%Px%ZRp{v?BJA9ZJ2tG{_>Wz;X(Mvim0}b!6*2zpBqn3#WB6m&sNqQRHWnL>(Dh5ZlhlWC3hd8K`A5H}PZMe-ZCDTzuxIAI%)| z0y&N)KO$Fn3@_~*ONy$;0auAC3|h)6KCq)oYin?+(yiScNKquHE;w_#Z-#zv3cT zIe+TWbriWHAOT2w-w-_Y!DMPnFP@|4Cf>B^42;Ofe1=QOrilR|*rq5vzVQ=DtUG}z z@PuE4N5ktU=jTolP{qEcSQC(%WwuO%D*|&8EPzsnusxym(HxGSUki>#IbNI2xZ9*9 zJH9(g#lw)}8J$ZS7Cqf*^7KjAM;$lHbS@at+U^>+#Pt?8f7?@P#o8hW#^qpEnMX2v z!}>b)2u%*srz>9I9qRTF_ciQ8|N>lx+?4zc&t ze8?9gTbD#cmn=ZIAh0wug~G`t%4f}BPGcMcK>ZiY`IB`t@4B*qP|I^DL-^QRlF( zSoDo=+xsAr87kpPhq1y_1hJe0rwI=Xf3nMj*JTa0Bx5wd(dtD< zBqj2#v3o)@c<82Zq1m2D3^A>%3AIz@JS)c5U?X2AhM}wxHF=Qo7em4W2F}WI#Q1xd z$>|*L8*GB2`v-l4*;sxi)MAArK;4GeT~w9`=DY3c#=0Xm`mGL9NhhHRXD>7ZRScB0 z+Nl7GDVqjLS`{@u1+Q`RYkQIhlfgPIU;QP~H+*KSqKJsA@3G^U5ADlIl>L@h(5OSy z$)}ia(y%PB5>)oPUv6XYMahZkpFGLdDvy5Y*Ipu^cvX8yDbmXGAx>}0&Qm!-BZD~c zX@nFtlRNEPV#yNbb)0j$goA(p@lpK8&M>CW7P1|V0J~B>}OKzr))NSA}-ocwEPwyvNAFHzO_+K{pvU} zvE3;9m(t=P*s;X?)#R!redgIB&V-x1X&eN$&$yS@vSD!L%q;E1kzF6p5YEv#2INCJ zZl2SjwB(5fY@1>$n8NErJqrtG%8(}N#q{#eLy0pEI7Lx*%w=~j2ySW|M=ca0KP2En z!ZJ=PG{{E@ z=&}6%rgeV+mbM-vrn!TBMjli3lVWt(^oXo6Dg%CE0v<*z5NQuT~50kCDb zk#zhou=H$P)cf0=)R;6Va2_Fi`Lv@0LI27yP*7m0{KF?yN$U8p`%Lq|`0LMk+@S6U zG<5o<`x#s)zR0C<>$~}`$!ZC^EfqE_=HO65DN}4rW9Z1tSIEg=clxBEz~XnU_GR z!svdNH93B5M`)LD`+Bnt4*f+(izol5uk;R)Wm({XV4pd#j7iL06LgUUzFHhQu5}|r zh#eG+nL*+Lms6YrYpt2_)r_vnkv|zmdav>CvOw;C>VG4}?J{)L$GcOqR1{x#DbT+e z^lDM((24$*{B_+UeHD>)b#p3g6kUlxs7aBG0eM`H^ls~PQ@V~$|NJ`QVZmO`S1pEE z_ciGQgP*^L8h%&rQ>=HnzAbD_1VIg-O|NrHF`yT_VRX#|E*9A+aSkt2ECN8^aKwBxpaTEs zAa~T6^#bgw8vc;qW1|h5cd4tke~DC+-Bvc$J%17|;>_=Ps6V23D+*IyVZtSPB;`(d zI>W3aGVr4B8PK&BObM637%{aW&sSUoAJm!*5VcJNLUpqn#QLZZ``W}qyMCX=Rw z;&RUVe;zSZRpip{!|0cnDDP=8wDqaOjVH)4xfP+vJou`#Gu%UWTD}wlwy$7Tzo4evYb^!sh59|GZp3Or?Ny-jH!#C|cx)FNuBqU|! zll(i_t?#oCwv+7q@NXs2yo*7?T@0$aT3TKu%?;7OEfJ}@CFaCsyNF88+$XbVh;BTY zUp-!%!|K0l=_n#qZRrTLSQNaRbLVI#p-gA4*}Ci$h);W>3~IQ&!%w7Qfp$i)Sd^BTybaPj>};!C zq^Q*iB1QsNju9&U_R&{cLSVfvnb0O%EOS?gmfuO(nQoZIns{6vKP5+^N9t$XZf|?t z-O&zv5nyS5Iq@;a0{-1le_nyKUXO|8CX_uoRIm>Xg!A-;W5A^dW6^utR}@6e%!{<-387YKfdx>y zcFzuq#x@o*6)@ZjahsaYvfxfvR$(n=U+3zp$*?y)vrQCMmzJV{PwO-%k0C$VoDrSy zqou&N2=DLO^!DM-R)dv>Ek|ARA;^+i%t;XqK35q=bXi9 z({=cEMM7V!MJU0Y_W2g^%pWbGOSW%grubxn4c6$g^%fFRr84Tim-2y8EgS1k?w}Lh ze>ZPcz=0P6@uX;X3|i20QgkH>6rE=iuq5YEWRHyS>{dVX8cjai1wS1=jx>QE_^Q3< zwb{``Xbj1A;8wyQ`)OHvRjvLkSqz}olBzcNZDR}{JF{NnEna}8GdZU3$tq;dqy>lFh*bxIT91B-XMYu|Ay{dW0;jJfV2xXbGEMNNa|j5myCx2#N| z;6%K<$IE?+9nUr4Yo|H&$&l=s3h7KTr`y!yT>5Kb^-7G%QxceMpL_lu-oAlmv0f2imDnr^0|>vdpB_2U9g2&F`FG4{4waUT2svB`VHkY zcf(2{+eTcRJzQ5Ga*Y%5w~@+`v=@nSM4r?~$vZ~X@QpJ@?~kn*t!C4vO5(8>%w4pX z!vdb2eghi}NgMimV{rfB!}Y(Rv|s_{`2Rs^!S##M0+fyk%BbglNJc%7?N{dR!Agt& zgb?6Q0)U^Z{L7tphyITT{Db;_J@219{g0LXIQ94ce&6+=I{$RgPsN`Kzt{WA)~`GN z!7=~f=Ra3+SH3&+-%(oprzq!tw;mC|ZxV-prX~?q&VPgY7h(Ie(&C@F_Fo+j>+9EkTSK-2u?n9pt(dS_7k>kybi_Fi{%&i5MKJ%r!L41>(* z{1)SK$|s?yBtCB^kZF9$3Yy+(7UrgU*&6-F;z9d9X^{;*F0h3Y7<6e?UgN?$JZCe5H&+NM^L|-}gij^HR5EoVm-ZQ@$5G191 zQ2fQp`ZH)B#d`-eJaJF+#bF`2EjY0#m#k=bm6ByaE}utj%Y`4amCD{9AEA_I9O^>n zd8AI~kW29mDH2hz%M*fp4E<%2EYn1bNSQ%B(R+wATM;$kw=ObISx+X{IkK&qsEQzL zcLkA58S>Lz5S*%E({(?HLM1IhM04VIenc%L;KxCuJg8*KMHtG-N^4cD)LBmNPI0>O z+2u5lq}QD4K@``FP-ZNU)S&$;hZwrC+=9Nu<21|_L)TvL2hIs)S;LkGH zCy7WHo$lLHJm(vCMbN7>6?uFH$aWACM|41RC9l>JN}^GGNkR3(H0HQ|lbBKEKm|Bc zXrdfsRn598yNjY8%058JTloISc6dRdSei9T7`&V8)l4L{UzI24Jd7tc`iTw=f z)b~N*1S$DVN1|c0^4PaA2DEdXX{FvrMK6}BH{Y&`m0nL*CD7K3=e>uW(cGeRk*J4Y zVj$-+T@$W;jeV=6M!4ND-c8T4iOK+8s6+I;eg@D_KOSOpja9G#)UC?Mq^nW8h7N$W z*SW8W9|%ga{wXmhTFMFv00JdJLMWPkS(p)J#W=Bz+eHuU<&vo$K7&=Z!_-)+q_ODi zXEMR_+o2M)9;BgOflVte$aOvM*RPx6wCnBtM+~Y4R~rJd`p^~g_rW+>&zkzUXWZVH zS301<3ff2+N%I||%$0oWYML2E6Spt*N#Mjk#E*V@8lEvH-^~};NKz)1x~7su{JgrK zmSP%=<7hVv5Gr{=)*WFJ7W$2HiO)Uy7-OM8L^Br#-08_n9|-p~vq(c_C&ctFwdNYm z!e%%!#Q5mO&c#&nwW5fjC1Q?P447$~A$zQ3wwz9Q6cp`x0&gnTH`T}cOB@$`JDz9( z@86t~m{t(T@4-ngZ$1X(fK%_MQGMMR-mz@)w;m!MP-Q)%ncr$2?u2 zxsHtuEbkisj`;?%Jrr!@`jcq>SFr~VG_$_9UNGO&5E8_T+eMTTY+D4V5lBfC6B>gT zhOBwyM+F5G`=^wm^V@KUUstwkw;vOIx6#dQdk&ur&;L&Jc}yu(+?dfzIi4whSzLt( zCI>BTu}7;6FD0=DnkBIy6FJGHPT`6X%_d#+o1Rf@_@lOg6thZCV=JYgE%}TAOhT-?{{U@!0zPeLnq%2=1>~IySbS2@X)E z35ZM*y%6wcV(~t!@kqxK!=&+~*S?Q6 z7#gaO%_k=(R&nsS_6F3Dy8@4tonvDag10` zj~UXZ4$-?>Gx$1}Q~530E!Qp_R6qJ{X@8W2E|a2=5#jPc;ka!mJE@=^FT_l@JpPE@ ziafG`?7|8|Df71bcD?9jb9(jqCk?gZO>3MboBdtNHLUj{7swsGE^GN1Ra;}z)|H*5 zZ#X`zTh?cPi{;!i_lO`a)e3^U$10$?;vUqQv$6anPXGarzhms?HQzu+L7fCqd{o0E zpDD9y6ml&y=wNS)ppW4ZKVY7sT%`6zl!SARqKFcI?)<|)P=SdEIXf`f^>Mq zqK1N#V&I=m1fxNqLnOmcj+5e$)0A%(gzwYnT?niEGD2Ul*->=1Awu!vr~NGJt$;^y zy3Tf_5YQ5c9?@(e4le*J7Z7SRyRhy3=m@x76p{%nxKj#AvKV&n)cyGtufXhh8b~c( ztWMaj)wg3Uf(cN+WWcp=_8q1}axdF$U%^0e6QUy$1OJ+R1ZC~4?I2$m(5R%D++))I zexAEwJ55)*K|giHgW|h|pgXtQ(S1$-K(vt+@KcYYayRAXZ)#U4m@w|Ag=I_=*Veq3 z->Z+PT!VYMkg!4gsu~Yj(nQR}j*O;BmQnurD8X`EDK-Yd!oCn*W7Y_KY8q!)5+&RR z8~vr;{a{>=ZzlB%&i1cUxQ?+LX+MuOWcAA#vNx($6>rTiRkgaMQKoR@+#>Mo@tpeT zGs8KYs+U3DnrNL8#bXofc2c?+&Jk_~%h)&<=n|R>@>qd~CUmox@Sn#uqAL+ZvF&!n7!RLFY;xTQ8_7a#Ac z)|2(gQ?iEdwSz+Vw*wF*QlNf#A~UDq<(n~-uYGoE3*r|DCE$v(E2CVgJKCsb@{I}a zJUaGqOk=MZ`ZY{a2amz2Qc%z*SckeroFm82H1+nJ`(d~TqD9Gi=OtbI*re$9?wICjvPd|bIEHp{%wPx~va@iK<>i?Z($ z?V3Qy1STog2`g3Axn@avTokW9S@Y<4GJV|(-$#;Ohgb;vyoQRn+kIfFoVg1AE2MLD zb(8jU5O$OqEivFTPhW03ck-SFEDxMvO<9V;v#QoPiWY(*RHqIe89 zqBx@J~4Aqydpi-xl}jN(H4LeTx=>y5VL z!3)$>pFk86G2&uU_9^zQ*E5sv&2>HZdGs2Ivwom$1_>@w0-1`8Fp31Ty&gIuC2ohVL|F7WL|3TSJMr1)!_c02t~ny;}8w;sq(vT3ufM`dWaDUI1@|cz%ZSe zO-=f$zn!!r%`L$ZV3wVwZN2ml&&|)Z2*aO=+qMe;R#qX=a1Fv|_x3G#lkixE)qoq? z!9sSZOWJI>#}I@bOtPNZAQs~G6{vAoF=cf+!%UqIDcY>ta&0cdmEkBMl=#B3NW=Lu zvXzC7boM9{yQlbDR6hf1wCzf@>{?DGZ%l=tV)=Ih`mzeC-jcItJuR@Q&u96<&XnF} zVF6>sVR>zbJz;d>#co~yp>xz=lG2zcc839i{(aNVqEp=wVt9*j*op0orNu&-6#**V zm)J$NU7Z7;j~{x82_gb0?=!~(V>8fNB|nKU@TcwcpQYSE|T_9^LoM4-A(6q@2-- z{^LZ-(9iP-F+{SfB_E$l7`cNbCo)aIDN3R?e@kuWF%5ho*!y9NOa2+hJ8^p0@f1R_ zP!zYky!WQf8a_51MH38bB=I)pSWWl1Ha$m>OK*lG*u&O(bae&2|hw%d8HW+K%{O?RRvM0#|m)8u0{ zg0@NeC?aw2g*oA?+x4=iMTFENC_boDvbq60Dw}6#BPRu$S^YIB?$&JwTro7dRtWyuiK*#NDF~13})u4;}wEC>eIpsy)9` zG9VZY00Q~Ae^4@P><^-3Kx_5mZP*!_@O`tKfdcZ2^aa)(>|J_ueIG)F75i7+VH z@1FLD&+{|(ihF9fGVOZXsg2hc(?kgA15DvYj`Wo|mZn(HG=p8v*$Dhm(qMH-Y~dJv1F!RWn(daZ zx;KJ@iyc1js4ix)KC*o*VBk}fxYnQ`Rx^Y)9KG3gZ$oOej1plD^{x+8XodpwjbD8b!g9G{njRo{({mUZ;&n~`~*YTytcstO{Mbyi^ zTbD3z3~aRT8T4}8eexcVvc>kN3AtlrA}H=MZa@ryWWV>-B_D^`^xgbR@P`gh-#&}U z_-sP~_1<1SIagTC;w_={kjN_+idwFeQ~rd$j!|KB=;S%b7U<}we7?Y@HGq3$b*8LG zwmf)BMMm{>U%0MO%Gg4+=XhQ|%2WGKe2}mV#xXu2OSJr18tP4*7#)wvq`9;Yr;9iI z8&$y(P6gmI#=dFQJQ5>A3Ez6PwDDNJ)gXTW3ln;0#!YEk^U+ltYyudbrH3qQ->MC~ zVI5sijV?hg4!*{Ob1uz0_c0<#>PU9a*esP7b^+z&cKlhFg_1V=0ra}M&ag#jQH*xa z!}P!^n!S$86mi`&`@S?u31@V~Zy>wIJNESLDl;qh%mQquz zGRImq+1_=EcbLf^^PiIBDy--^u|50NK0W74&^Q~%e&*v77nQ!eqwOri$)|e0_nk=Z z6En}j8~OXJ^gxIY@TZliqU7!t7!tn|^|P<(@r|;}-9g@=luoJf%!l2m@aW*eP(;EA zU099nvG`gEvXBCD%Lt<)0QuY~g356W;82H&eQW0EOgVC)F`;uI`kHN*Tj>-cz2=j) ztUdM40O87POLYiTO`1dk*NEsiqBKZ9Hq+5D38Mje&oOIzxu>{6m`H>aPbuZYH<1id zl1)@Py-jNg*S@z%xYwy&L)=12-=|^)^-2$NN>=utW|Y=Z7&PkwE%dl|x6q@nMPN0K zk)#rmz^U-Emp4O+jxb|zjmO~!H6=ep{+9;qY)xHn@yHqyv zZ8fY%v?Cb?E5cwh)TYc(e6`x6zEF@08Qw?4U#G_aseI|N&T=i z==<_dMpFOXar(bj`;YMB|9lMcKRw%jGDi7tj+g#lsvE${_G9WFRF} z;Dex6o;@l2pmIr7M0aY~9AZX5W=@dg$|4>6gh@ri;gx``iGcA-f|D{ut!>EbrG>$G z0+a**U%ne(v_H`{+unN@me3B_-dz8rTa%VcT0w9efziMJMgch<2=oIufBLqojLD#Y z7S)7Vy<00$4y>cslPZ>J;o4e4jK|?Z%HEKS|I1SRlF0`wjzQ3v`gtK4}5Ez3Pl~?0YW`qme=y(KI-ENAbNsf4Rm7QmKA=pweVofEM!PP)*m&5%627umuKlczm>ZG}F*U$}=D($Tr1{(x`4Et;;3w>=6(a54)Jo%r+1{9A!J6@Yt?U4Pa1qZKjS}P!xfWQ& z?M59^;#m;3r)u3%B3$J{jrxkS3@c`@GSFf$LGCgl+7Q6Ctd3Q$nn^;HM?vIs=S;ty<7W^Dv0G+3O2gPLsr}|=v zxn7$NxNswc=E36fqP^8Crs2D2WAq$IJ@UHI_1k^Mcp%8n`txEX{Lw$DN9sn|gvZ4| z#^?vcsKA9pr*rdc#nH~&F`Ca;Jb5=LBXRmx?U<7fXPv;i+zoVSvFv=Idm$LHBDwQujV$~RW z5qlQ>?y7s8oX|YQnQFm&)l+bn->Y}@61D|K59hwN{6j?lSA7yY%g=pMtV|5~ud_AS z_<f$e;NmDMN4Skc>i8Cotc zv=sHt`|U0C+9l`T>67DeZ3OX2z#6X5;@AMpCbGcPcH+T|Vs*o^Osi7%VruKxxaoF# z%%^yDHj9^;gNE&4i(IX$a>tn!%c-6K8V-F1&*LPvb7?*V0T}&u14y2!F#6QuzL0B14k>2W#9B+43^CrK4P2TxDyz^O#y`5xN=quMn%TfSObMi z8Fx75yXcyYXXdmKXpWVr1mDpnB;56nRSWo#fljFWa|UApd_8M^BR!I#JhJi)mIz!f z^ec1UsO{=!RfyFkgs7-!{p;Yf{QWtJdUHk;_+y_kN2W2%6JUzUI|jKed9TrOa}xWs zjN4-zJ7uyAe{Qeys%r0sK`DJpe0fRoAvgB6+tKFO8;1fN2}aIq^d!ChrF>@d z1j`%Ko5h^6 zE)^NSFT>Fuj{%NpDI?%P!7OG1o2JL219&zCPqQx0NJi0N^Hm<{Y-!t(bW#2h&nwYh zLm;9T91mrDX1(0%g|@vl!UABPhM*ZQ%#aanwub?U>ltZV9Dz=sg3snh95fQWIfJ(X zteIiaDu`-XGd-%pBgV79i6tM_czKg`V_%yhCE53x3TL{|NDUb!OfJ?sG<@JJ+C7te zq>$!c2G9(`%9&v^;`D-+rmnnGBmK+YDH1<9YHx&zmOoxnz~8E1?i&36sQc=;td_NH zX{5VTTDqQx?(QxD>F$ycX^@l_B$Ni}lI{*^q#Kn60SQ6)9@yJ`#KYN#v-kU*@BRMZ z$C@>>X3d(J^{koey6@{gW%Er{E2~w-66zGKDri}ymiK7WUl5Pn;P2vJSO~bywgYfS z_~sWA+E76_?!*>Wqw9@@-4X*6A;+wo|$-@5;Om7JHwg{?#J3hFZU z#C?fpZmAZPCW?-i+ai6Y!h-2xGJdVpT z8h7a_H?H*Z{SrBWIVyi52HWv2O0VWAnE<8m@Mnl9HqnJ_rvybj4|T|La~jLzc>l?1 zwP<{@`Q!ni;j&DO5KJX@CF)3M!Z{zH6)9{}v1J)*O~1e1*WpO=UHesGIRU0f`88ba zW=Qgvh@~9;^blonEUu&JW(TD-Ddg=2838KvVe~V4QjifB=?(Z!)LagADk1U+kr+=k zzAOnoPHy=?e7rF6x^EwlX9pLU9Z>Q4JVXtk|D995uai}c7JL8@smHC@mABr@hhsI9 zns24_2D*J_AKzD90<*4|Y3%}eA6yj-!=abh$Wan4TVlv!#3xN^9>Vi0!GnQvtarZK z7hbO}0Bc@{)EA!LA(E>+32%9^0e1yhZlZpoikw&0dRt%No(XunhVI&H#&Fel9On5iLs3F+06SZ}C_L!^aY!x~=t zNay>Nh8AfuES$Rg?C&egm)PQFs_{6bo`Y>=%7{&E zwSH=dLYO-{C>zyYlsPVWVoYJ!ZuS*ApR*)Nw5U2#vBjBx-;!W5`*?FfpD4tS8N5~M zwV%B28RdN(&7+QQvt5N%^Q2rbaRiS^E?%~?+z-wW|1^w+X^2v2qczn#r&=Xp=@;!` zB7+LcBU@CZ0z4=3a!1sCcUuqsw5I>FZyyWbhEJy|$j~1WWQFSSS_ow5ule}3sV_;# z>V-8mBU9ZvY*H){MZb&;t$94dG%C6FA%$VhQPR+^h7}uGjFI>xJ=S1dYYi%K-L zU4fD*`Wrb)XxP={OBOT)cJ#-{?7S|0h`6sZ5u@uRxs!~$`ACjWaBI^QUu{SXj^|fi z^Z;hbdWnOZ5@G!_1hbWKMxgdAt6SBp+j!3l5GlJzO?>A^`lj_yuVE{mmRFP!PwDarM*7pzL+Vx;)+G;5P z9~Ig%B>LH@vf`%}F3Z*Rs8~pgvl*77tlOxaP?ECs&% zKp41X5+U=BbjGe?06$y6bcQKNHU_cngi6B>@C~0a!IyTouKwIxA<4v5e1!IkF98|O znGjjC(QKq`&UabP_DBFi+iN{RK4wH)nU*+KA?0I4uF@bR@~}-x&$Nd*?-Dc0?upyC z!5ap@enp5hVILcu7CJY!o$E%sOi@v?SCwHFE6iF3xYXV1q*>xwjP@2u+jI7)OB6aa-{5*CDC+3kwiH=L0(DE<=MqnFiV zzGzOW`&D$JSkq0{X)nKP?-fQSfZ&% zSzg3ueZo?+r$JI_GOzyly`6K~B#s@SJ3qR_r@H&VB@a7%Zb^;ml#FqMt%*SR*ZYjC z@Nb_*@Y}i;G2S-NcibzoaNhV?xsu#(BS8^8ehSs+ggVtJmn;#VRUcWuChYtT3@J=G z6OT*=>*5S4hB~hHUW)!Fq&5mpwyw@s&7iPF$SM-Ft4P zF8o=;ciANs93gyLTun8Z>UBbj@9R^8oG$qKlGQoOy-wZJ<;@|3+4;`+_L-_L}GQbq7eCw9i95SJA&%_|XB{jYE>VXai&>x;N=lUOTMr z&nw4w&v+VXw$~kvDaV(JiRoq~zVRWB=@`yvke45O?@{4*;r;YE5)S;JtwToW+v;av zZdAdy2sJQYt0MpYqw`+?%vpXhJp%r2dc*<(%vo9PWO@XO#r~g}9{r9;-vFj>B>sU2 z|LxQNqJn?i;s3_bpWO6Mvi?d-{!(>+>7u{Vx_^DmRTKV>-4UDE4;Ni_kgeW7+xM`8 z^uqu7?CmA*ig2@oTxRcBC&)?m&r7oZueLI}t)KqNxhw0#KktWMYh?%idggyi`YJg2 zfA%bYrRD5`cb$!YrS|_Hu3;AyyWQ&OnzFbeHLl)$z%}`BmAZQWe?#zpOM#ry*?<2a zf%2}%kRMh@SMM)?{fFZ@JLnw+u>Y_+x_Tc0*T}_nEueV|xJC=EQ=mBsxZa*#r$F-n z018Xzc1?UL|C?SL+3X8I=rW>aV~F7TRbT*sHpt6+&Urg*FK>~N?i zlM~J>$Q|Y=dN2)MDj!X{Rl&YHX)1C;(PhLmW*Sm744>X7#hF-HYb0PidR!D839j$OHuiV}T^PCZK% z5nUmX!zA8Fg6DT(M|Z|rqy)L0NtbM*Ev zs6TyDtK1t=^~Uv$ow5dxnHW|{>}6VJJ*s$KW7Oiw6g-lZGCh{l=2tJ-z#NW5D<2Sa0}eCe7OJ;<|jS4w9Ko9|q9wwVLlymRe7%p7(h zYVtCLvXp16zguI?QK`W1u!zYO-)q386GQ@vSFUP7q8UM!OYp&kDmr`xuWexD;dxF= zXw~)Eg`n?JE(CAl&Zd48rzS zHnW*ha_GYcC^8+T@%pBEwolg;bmo*m8ovxJ>`vSAH4x%zEixLptz~xp*aJ(;96Xq* zvz3^zWC4=MZ6+_}P=FC}#OOy3hIp5!V)Uihj}MEegg9UY(Ux>M#G(7PyJd~=66ENB za`AFzX(BTk_Kh*yDS>LbV_wyI)&>K?S!`KV6*aFGsGQl5gSPh+OoTL6hI$mFyh*IG zo)92Ddzmuvm3WT*SzsJ*a6>1YXHXS6clpswdMJ+r%=msr+H8Vm&~n^f&*zpIrYWp= zwzUUdr%kwWBcgi`M}h105-)isMMd1=mC}8%Sax_Cc;UCZ=z*pSvC(B7ni0sleqJvg zgFP~Pb6Z#66^gv+f+(xV4f6j*nDK$y05|}|(V(sWJKld-j z5{C_#>v=v6YlLgC^qlw5GFd@kjp(D15-qRWvck&Y_$TtmIa2BGDu99=E5~*x0uy|N z@6TY!{D*K~MOI}sjSCWX$-JL`x5naI&qO5msbnCDts&iQ9*fBAgV@U|{LOmp+*T8q z;pB+^!+HiC9ifKllTY=!-*vrI7gj7!f_H`|>lk$vfErHSvU?_FV8zEuN4D$snT=S_ zL>V4Fiy($*(^|Z(AMa{n@r#jVL>=N)2ulxHdw85hag01S0!W(J?=z7si{x{Pu20-$ zL`XCi?RFF9TC7~{40GI->x-$COSRU#==40N#&zF!*ssOXN%3u07jXc}d|tbPX=Y9X z2A3s*gmbZ5gWLTFRq$cU*^H8{U4&nt1?QqdA`;Bv9uFnvODP4#MI1r0HLK}@C3Q!Y zmr6#&N3PN%#3jQ84On$w?99`Qo1WbZiRCuF?Q!pjDKh_}4H7A61Hl9WTH!O33&^*M|8@XccI zQr>tjF7{vtb-5#ytt%ArNC(bnHGT(E`wV6KV+~)z=TaOG_bnc!oTOvMb(GR6nuZ@e zbf;gjL#6KTYm8IlmFIsQ*1M`-$!s{D+-Rw$n$(K*j#OxA4!0)FcW&5aPK)&Ll*Lq+ zhi#&kn?UMPgq5b^RPOlH>&ry|@i&Nsei_5ty5p|M+b_zgk$u*kObC9vsY38LLv7I? z-IUI|N#`!L9wNO;f9W0BBd_8QGA&PWUoOv%Dj9uQfvQz5xbR*~;YC&^-575GhM!N% zFdD-n8FlphG!{uY*q}LY!u1ks8l;6VFBTZ15ETh(VSq^wwG+K&xk+Ddmp8YC7>*R_ zW_;b+7njEADJO#+G-m-f-o!vs!pUWhI(Z#fw>Vf+xJLoHY^pB%uB4(e!rex`)CW-$ z^a1%v>7VlVAnZuh&O^Sd#j5j09z+}}ZavB?p!%vFaoSwPHIwuGUXZ2gt>4O@7S?}8 zc`*M%dAtAxJh{>tNcM#n0rxsdrc#Xy@VK;YQNuSis~?}8G)$?UjGm?8vy%n>$xsMyt7(kxu7%sp2shuP3x?giPfDh5&1lDdxv8umA%_PbW3={oZ8 zP9TKXnN2{l4!upttWq$}6pwZ*2;(ew7J`(?Liw1j)Y&g6q}RhK6V~A7!oNdSrt( zaK}adFY8CysJ`&4-^1OvzKA#^9bUXh;KcRcAHR%6@2Y~aFkwpvF*1UbRzyg7=&-oD zY|$P@dsW>+iv-IDp#+dgV$G}60#G!GC9Qn0=zw^-DU3oIxRQ98eu2!dR#}g_C%Fnk zUz_pZzlW!lI0MjDW$PjyMLE6HoHQP+JK~H$)iDqJfG=^->PSlJ%sC2Uo|OC@W9uuo-=juLelt=NGjfj=Qe0e^L4cnG=C5Bz}m|;Cc?kyjgny3hgvD zDVIFwCo6>!bnKav!e&N}9Y;Q%No7kRM>e}=o0JUGh$ZGHd>+`sP2FL$3cf~a%OQTQ z8{fVbuwy@EJLx+iUKb|8X^_BE&Gv+uX1UFBL^j(p;a5>-pf@P(k`_8L-hKDBI< zk19Lo;6lft7XRGY%-69W|9hkdGbiw8FM(@+Yj%*Az;&=;z@11Bj%%CRzu_ftClLPc z7P`~*Kcn zUBCVZf_^dn(O%#mUeG|`-;FH|FsKrQ5G0tmNBtJnrpvN|DaMN(z#s);h@ihl!q;sHa$s0Nch#) z(v%002rTb&kq!uTxb0wVCdnTIRERmC9$qx#0w93}rLY&><^e(uZ|b#NupEj|qVtw4 z6#(b-AkaTxe~GUPo8Qtj_n#LJ0FFC?B)^J@MF~c0O<828P?)3KGW{o7>zBe~s=C|Q`9==vw{`9vAsNv`GapzqPqhvd0BDURrB0y)*r2x)!hH z3yL5p={FrIIZA2&p;?$^&Yvm1Rug?l%7Sh|AA^UI`9nYlAkQBESHa%F}Cj#Rb zTjc)wiShV@;WXeCTRo$2yP@ld45#RmxT? ze(7;_RHbc})kD+#+0O_oD=4EQ?J-OuVU0rcDfGq^72#?gGae01e>>$7Xy`mUfsu_h zxy{9$UtB=XW2<{6{4(Nx(fQG@O9>3Qz1$&<^u#;AUQ;Im6 zI{4cw{)ud3_DDN_mYDFy1rwJHM_eVBHKo3XG9tcT%lZ-vUQ#|6PaRp5Nmv}&Eu^vc zkky76Fvc%pFd%rhv|$YgARv6)R9k#xF?a;#YA9=W+swHm-o(mwQ#c&tYVk*iM9Wh8 z1}f2E;cu6gl7nm#-na=vQi>zYfe7|-sU}J@BJ8WuGYIQ_Rp&$?A-r<)BJ5-aUOJH= z$tOw6uJ5IZtZmof;L{b#EoFz<7v~;onSGRa#wH|sX38p7uW-yFFwxOXPt@&PvL8oX z2415tN|os9`#20k{D7z=<$+?R!(s{6nzd3F^x#Y&AmBp&izNJt~RB2RFp z%X)sF=85K^dh8qP_r9N$`s6t$v5hA+*v;M!;E(0iNbZJtlFS9iti4IV;OCHKVCdA> zKkV$A`}RIq$DxdzNB?txIHP~7p)TJ%;=oG%vs(1+Wn4)3dclXz!f&xbAe@wc`7jT* z55#&8qJkwsU*#JZm#=dd_dZ>Oy5BZNcLc;(nQpi=fP_{ZuD^ZmP{ekauqY!Q$xLtO^YDr=(!v~FbbwX!Io$@B$FOg9?=VVMP=zrcD+ca|B0`Hh~QM^4}=o-(3t zN8$8mAydD7O)eiFxcHwL{ad}xK=?Dr^gc-*W?Rzw&{2cNx zw90cEFI}wWl?NYT82i{0pew5?exYzIrsxuu(YctE;GI=I#VxJspQb^*H$~ZKkJIqc zQskWs|HHxO5IFYaIxXd~tNOm@?0`(hr}q?XQYbtG2e)7?2h~RGEE!?~4J5?%zvEd) zcsX}RYvo!`7C8BIvuY{J`C5U)=nN2NvcN`+1QGbfb$s{3R29rd4w`_sz(#UBhUX)j zk}Hn&ZQTNA`jm-r8vYsAOPu3uD4<}35M?{op7GwIz1AE6<%livQ884UNnO&M6}_(0 zksqRwH_ATr$2Wrgp`0K3*c=N4GLgvQI_9O`Hb1f-wGU~QCGy3LZh;IFD@$YnXH-dy zaD`h4dqj%5DD(9!eHRY3;0>w1%w^-6?L?TK{R4uGFRTQfvdA)V3o{J56_Ljy27>*= zto57>QmKwMT165B(@jh%y8^vbbhgdXC*Jt2gG}|aR8LWvsEOAojK4o>_BBRrP@D%d zGjrSTnMa5FFrxn1WweuH7LJGjMo8Hs>GXvSdH-jRa|n^_Oqp9=FHqo$JB>68%MD>~ z*+}Ut%}Efy(0Uam0wS4eI2U{MV#Z#O>;2AsT}*(sRGQFotcNNd!x(%6=NFV-i#R@f z&mozVQ*!OWuH<#8LWJHef>JK4m067`@RxRrSVe^rQQRO!!;kGKpH?1B;(yFeKwsUk zf2eC)z)`6f{Ag7c|2|UsR$O4`;;bI7iy6_&C^p*6Kvk$|q3wjx_+t|P#CjK%Cl*`^ z=$GA%`72QxO!#CY`OT~rVa?R5Z#{1yuTZhkYKcHI2pbaA@&)e;B6u zCZH?rMY!g*PAt=6n08w~{j}xzGaiPO<;EgIRf+j3#@`Icd8heFrbN_;$H=C$-9-~t zWKw4$LLbRJpDV{Ibxsp?O-+MW`^N7r7W?CQ7!&jz-i2}Fr3t9n;&>0k3pu#T+s9;h z^kiVl6>v*JN6T`EE^Xd>FOhrOtG; zzQ`OEWEGVFHy^03r?j(IJ3~23V#%=(p1hsY0T9SG4Q(EGr@J5EtKaWOgH2Y`m--xv zk04Mu(~lz>Ga%-0Bz;fDMGvLswJNB$DrqUY50%5ZH@H~@!V?=yls@1oS52rJ04u9qYV1^E~+AttHe zo*YCh>Mz(2s5k2{hH$!GeugSPf^zm0KpOzQ^jp}*c2#{Ie%nyoap%bPOJHMVX&Y^3 zgqbGgHE?KhP5#%WA?cxq7cx+Gh4X^%0lY1ijn?>k9OY*#lef-xX<0 zNev@A`bHGNM>bfJl_WPRws?K*jy;63kIAvDL#rI57#$_TC%wx|hWTv5 z($bRHZ#5*xYw9PIWx&ZEHbTnIDwm+DSes9FE|44xAGOqi5zry{BS&lG*zp-|1*6K$ zRU7S1v&6Kpp8NVo2D?uhNyRYNueT%OnN26OynN4LSuKVISK~Uv-&?<-%i?WaF(HiK zF_K1XMstAYBCb5S;v)$iW*XWA$^Q-g-acb?6DHV`M&qR?<>SlANJu5Gla-l2mUyZ| z3;DRv?(zu)#>m%wPpNAR%6rO4P*t2bQ|)jD{nZor7C8BSfHS#u8E{s#ND%bC(qS%e z^TTAYzIb>jaRYX{7ysqG4#5tc+vdz2chGFV*cK{F7tQ_%4*8vK=ChmVaL4BNDvCI_ z_5KI2`om~6J=t&5mnjTAdh^gmV;{T75~#+XTeBT>9~&DZhebw}8rkCdG%?~v3{6yO z2L{As!cI>afGs+=KNK8tSBanXBfFx168`R}Iw|jgCkOkz@!bZ@`HKiTwq6yrI+K^_ zExx-Hl`6Sa5i3J>mMI59b!r)Y#T8y{iD|<++X^4?>x}_Ri1QIzgZTCLU!=KorI`@a zuD~ky6tT%=zKKTztO}4L!0AW~D_>rN(<*!7- zQQ_9^Q>-R!5`l&_l+(N#_i-ap_Emaw=UasG6l2ivirySAqBXQ^JYmYagc^RyY$`y6 zSWG{a7bpLKvrdHR%vt~QYm-vPreZJXrqbI2adX~vGk{>pBKLL! z_0+f;na&MtWUD1-)XL$52T^6tiQtnLgJ4$bdgsN3T$e=AO(TK>m?{EYPDkZ0ciOFV zr(Hz>T%=#PA|WFeHmfjpmYlx`>%1cvsm`KG)GF^Z;BG&=zkt#3h|b;Hf>_P*d>G08 z>Ez~WiVx!6>#`>v{3nspZo7$ItISW)r<`6*$9BeWYt%MoC2b)u@$cjCy`XS)+;;D) zqS&BXp*dx{z`q20x*M@{+iQ5ot@wPB4NV84r+QCZTt3#tBp2*28cnB4Pk1UI z)wY89-l)FUt0~ks#{8c2^0l)iOvE4oTCddH&8jk)C1FY{`W|?U$6fC=6~9TTQYw-_0!?6mESG>w=4M9xA~K*ue1Is|DR6$qwcG$|0%js zMC|_-=_JbZ!(Hot3k~((Xoo1<5BXuyzeHn-vj4l7lA<8Dlz$MASI;C0^2WS*=D+cz zKkgyQ@jpRJ|ETT1@n2NvKaCJ7Ds)9Ci-O3%f8Ecb!vASMh>9@Z?)G%eCSDPmS8pfa znisrEUA`u;bX@@lpLK=C+#Pl09;;2O%lE(w}301%D&dmd=E062aS znpdOu8@P2{64Wz*YXIc86sS(lALQ!Q(*rnvkgHcu3*h`gu7aKfl)7%q4JRnhpV0Kb zN{e#d5t3%-xZwmPtMbQ2Md041EkHV)!X?DDu%00V)>u#C0Ut8tO<1Rp*{X^MKgW=%@^Ypuls7&Oq=; zD@9`=UKZinm*o65RnH!mY4NV{WI3lt!5vnnw2MR@5crQ^##&N)?`}*-GO(kuiau5z zTcFP{3JmepIP+sFbB<)}!~bYtI@MBm6_GGs0y4i~F)T$% zeWBIlXv)1C5Mft7F%G@f+MSr;hTtLWIPU(eGjks7?Rr;qz%~Osl%SE|0Fz=u3mS z#cvH_I-iD9-HCN>k@yDw9AD+ndhM6EHmcErHakqP{#O#;C6JRi9P+7e87}6%W?JOl z*2|t4A-j6RvZ9X?j0s1NMEm<)NGhXme3In1p5{8?3wvMc5-Wf^a^9?qtrB#auKF8Q2s($@ub`3&pdPCC8(fP1SkmD+gI@|Sb?C@@> z3uaHzzdh>ppN-R8vS0>0)p}zCDGKeT5E28mgZ@F9MPI;@M?8#q3Z8l`bVp!ei0bpZb&3#_sJ_qrGx0!|!?cz13q?`l78orTI9V_5`(_ ziD}g%hLHzc*2C__yuOFPm?s0?$JvZm3Wc3+0wCu}=Tu{c!&U8!V&2e~?=@KpO-ylU z;wKPSPS4tkj^-Mt86TIs-{N2a{8^3p&-(pWC(<8gC0BTMS@m?@1dfEtP6Qn;;D{0?Y9^u{?{!}q@s+$(>56$Litw$)b2gr6R{a2T z>y3f_-sEtrnuf1r9OYT{bOGo4H+gLZ0y(~#>;OcklcEEz_w(Du_U~y6Ux{xIk|-7H zPi20Zar(TWnXeKf(WUqhf^l=&jWEXHX%+HusYc5`zsTFy@KD#sn-cmhn7@E=%P|f~kzM5x zf;WBTBJO0lOz2il#q7qbURcu`1l-&G3KOKK(s^6w-4S_aVfrON7bv7Q2wn1c?F+8~ zj&PAor5O7F6ll7II^X%&Do|el6m_mYPtI9uUea|$IpmE(9m6+Du03U^TLYU=KLw)`)xO27`O1*(4{YI2r_;^2u(DA-)*uF5 z^vYcUcCm4c)hEht^pZW3-Nj)9;RRm zWoy6Y=%6pH@y3uC_?&^l3LIU`uq^kl zi9OyY^$mC)IRsDC$-YyvQs@0HBq(7KSu4g!9?wx>wUQZL!zrV4I1SdeAVaT$9)sI) zLN$(Ot-jki5NI%lB=)uCdGc1v7JpmZN>;5lYRdoHTO?y84pIiA$ZhMfs zVtF?`okc;$6Tf@$dV_73&HwP?m1J%p_oq-Kw+03JEKB#y%pH}eUYCw{11;Bd`}XFt zg4hCFOd_SgpAT1(C%Qly+T5d^O51i*uvE~1nv;%LLc#nBEg+bKqie>h-fSatw1-`j zreW4_;XJ!(s+l&rMgQ;~d$LTL98(@EJwSK^5GYadRQRNYkEoRS0!|xGa1jUrA2tIf zDIeOjF4S#kqIk?TIAUph24O2O87J5KsuIC_&!urj&(e>|82n9l@TJM6iNZ`4N_jS{E#Gv7IR+I|@?06?G3u29Q*4-&nus3MqDRE8{CmuM zJr2I_qnzyWi!r;s)a9N$s7)bjZQfvVH+hkA|LtdH4xWY0MT$j+B&oxiX59ulDE4kf?!)&z{W%)lUoNk*Z7g0XZhZ*(jW!_ssk3=36hE6i+kh zI7%TSOfYC^SFMjXP7A$i=$kDF0Z#?wuJ3iZkzFU+?%o9 zjF16Z(?x@P@gR3WrYI6|D6nbAu9mbgyO+h>Bz^Ws_vt;LQ4%Z*Rq{9+l2fv&?X3^n zI%e0{Mb>Od58jlAeF-P!;GZt950%a_FF8#HQ^wPLzqc$VF1g>owEUR*T~9auex!Qm zrD9-*`I}+EPcdw%%;G+dFT*yt*0WlviOCaDiUFd?b}vHU#H`U?@xiH+vUy$5ZtfFhna`M`x#?z(DMUs^SVFD}i{9fNkT5yv=$P`1GcVJpl<13pkjb?8kb;4@ zeS}JioePh;uE_514>BUg4et{si3{)zmLnx|oA0(W zQ+BA}EJ#no?OEa;lfHrm%n^`qI{46PGNgqj~E{$cXCo9uBl3t}#MTM)~ z{lwp3uh~);gR2i*+0|L-=wvMgMcytq&N{LU5DQOV9KobQ`x4(W z63kc7?Hz1~SlNCV2~bd3CXja?X8;3=KpBN57f8YQnx97+87TOKDB>y6b#1UkXSr@Z zBF(q`+=_GN<7ZezjI=arTUYqEkJpJR`xrUU zjZzc>C-s;+s0P~DH63VqjfY1th=qu=srdtoR?Z}xiXJAA8Pm>;5m(^YX-N0$aRy53 zi|6Y1gL6JG$T-&Zd93DuPZwc;e=Ps_p3z2VSwf`S(o<1=qk|xles4r=_&?8?cKf{Tx z=f`E>Bi^b`Ej+{2|DtWkPd4mAWEla>T>Lgs_GR+~>R9&$*=?h9S9>48O%a(WSsM{j z1i$0t2iQ0?{E_|#6>n=@LB5lK;*I`-6A_tZ9FS#KFwF~>I6`_8X+CR>d>*tX{6H<) zn!o@EM=6+rN^-0L&3$IHIP)Z3HHKvDE`KR%$*Ff>98?4g6qS20#~za5gC z$A(%>`kX;}tMGFlz4|6jh(fn#0O!Oqwjnj^@C@>csP&{C1?lQrwXueee5Ki|nwL$$ zWGzPO$PeuNV|0!^ARWl+jvUok1;sTym4?9~d6z`-A?)&EipCTd8)Jz{5~+hSS@DrH zPMC8-Q_3cWccXf*|~Q*9>0q1l1In42RNDY<-=@D>fEpT6p{P*^?(wM!_mVAq)sCC zLpfoHiar<$80T;jW;zZk^JrS$S9`6g9}A$<3<5-gv?5E1Pcu0CnqrLOW1@`}uv5LK z^ZF4=xg$(T{h^afp7kk6l5N%p66n4*^FcCnL~F&R^y{x7l4)nS_d3+K2b(2$+$+n^ zu0$W>fmQB%=0@<>WFRAdodu8aFR9zeWklMy*c}6z@3@g-Wxru{rg{~f;m4Zg+&lTR zs_Io}ElXDUjK&TGERGhyFv##WII_`NLNw%U-^+K~BNJ?=yk>GQ^F=?vte$>lDn1%D zkb2>ug6c-&;v5b$9c`ngM5$9MS7Ntsk-O_5O&K4%XfVq#+|&9#sRPE02o1t>=xAI> zP&SS!*RJ5ieTf7!mPZL5V-Ag4VGMf8sJY~!@%!XhioRwxs>!WhKA^u1j_4G`QV)qc zcadWERK082ZZ^hj)mw6-kFCU>)N&sbn5$D+BDh85!1@xuFMWuuOor!rPfo2NX%dK_ zcRb#kcArLlkuu-e@4m5Z!@+YNyEZ5St*6aNk;43o+uJA?ZBKABg8bWAopor;>GZ`) zBUmP#p%a8pX6I7uPhdj`)|NVx4MSHp10g^Z+=3d*&5+lc`GVtzJLOT(2PXIn4T zMh>j0*_la3q6ntaD(W)7w46@Jv2Fl9EPS0R-Z)u&Hz5VPYClLw9^2JuIDd2z+VLau zYvWLRkMbjYF(&vInyHS6ylU<{Thc)A7xmT?X8p*0wZen1mkleMvtEdD@e*CKMBDxAIA?qhMwYpDI>*fG=b~j)3=pp)WA>iyX+tsuzvdx^ za=<&x!8j%GD1u*REe%8N4mhNXXm8ApWwsyc_OCvw&pGa$h^5XFd>7GSdpT1ujw-5} z>rwg5I9VuBjr0u4e5F%)#>2E#3{xAeqNMKBxZ`_MYhm zdw->t`onSfZfdDOP+;f3!ruRb@%<}De?lPNtnR0RKONpI!$k6X)=wpVI{dZpRr=p# z-*(AgJ$LJ^?sD?F%r6ChN&eL~{e0c8x4BCHPZ9Y44Z`$a-2=e^_$NQ(t5~xC znsB|XRacQ>f4kx;F6_-~0iXa{H_zNw!)@oT)FuDgmABRQzu_E!{v4+DU!G5HM#yUqiR1CZ&50l?LG0GWOm09=g$@Op=OT@v)XK;|Fb)mP6A z1jW+)&2Rwp%%Ie@0l?2B|CKDkAK&A@Nb&=j?+Bf;{NnpOB7GHC?%q&C&K`Io7>3V5 z#prrEXcOiu>6f%BE5e5d=?xc`MkJ*WUizToTRV$0?ALFIh|Aw-a^FQgdQDD%mFZBG%pgg$8Gx~>yu zvRuUTLf&j0{ej{Fg{Pr%aIGPgh_}7TfiX*sJ<4+BM@rw&&+q5CUK-EaWmii3I=gbd zUuyadJxPEm-T2n z{fKhu4Wp(t2gP?j>Z;Mw*m_)D1$zeG z4}(hLZJs9Zlp+(eSz@S^nafTG5=w(#UJiREu4J?D`mvd$eP~w{&DrJKm!kW^yYC=M znu{Zg)oWyC@d^`(vr%QvRlyA3{Z)lyt5N;aZzbm*-@ziXrGsxv?L`@5Y5^JPh(~g^OA5H#+v$fr6U#$dSWxqGJz9YcDwLkPRr$KD2t^hxAxZ z9sMcCY?zTVmx;_sL|1U{ly7K}Y#STY<&YTZSfCjAgs>;LUcm=_R`=mbNj4>>u5d%J zM9KZc36fI(o*^l5w8JIq2W$Zx;)XqX$qp{QiFAH}DwIlc=L!&|E=p#4p}?vQw|9ws zxktfKi_2bnXE=WRB?eo_wi=tT@yG@5IXgYmjGgKCgWHR|1v-QsAH3i~rkn;QKBpLR z2jY$fpyml7GBwqmQA#AH* zZ<_&M^x_DD;L_RXwhq2CaCpPuMm4(68U$Be3vSVoT;#bEKyq4rYciqcgvMfeW17f% zp7=SLO)^fGA~c@j3*EQnrBqM#L+*au?wI*D@Ti&uI>JbAixSZZy~Zz|L#rg@=`xsw zB$dyKJO@X)(Jffr2L#f##-GBZ$~3OF9QDxcd*@~kkG8Wv8-mtFP(4S| zsjFX$ASkUx>4Sz9Xcl`CKH`SA03(`zAn`1Hj-1K9)sPyliya~0E4PI{>DMMt2o($JD856uljQ7U0=Og_0GZ%Ykn zXZAZVK*%U}QhJ(8e|64#irm)v^64O&N5;Ym@;g5NhFeC1igN*1n-IQMVa70q7Rb!rdy6fGL5yab3D=rPxj(X+0H?HKi69)Yt&Th@FHZc zCjcyWhM=~9C9Kb<@$HV^@CM;ZfVWbY^P!m6l}ovj&fuO`HB>Mv-|#1T*Ejj9&ia>j z@X+s$L?*sSo;TBLi1H0>%bhoyTVhq?V{2gr>Zvn{zV@Iat(?$L&}34-(5zL#^NDp_ zb|K#-Dejz{!n9rTu0x>+eW)t@97##nqNOlbh|~Rjc&5xq%u%NLj>np1*7*1NX1u|R zrqV7`9{Kt(kk8c@;TX%?#^tWCA@Bym1@glEgZcoC3)nL6{B&>2>7_FaO;URm18bNm zAx4Yhg=^BI1S_;Kzq6*XJZ2_h@Pn187Gq6%5vSg_o&{k zj=lxkrX@o*+`TKfL*s+lijP)=!Y6PAQTkm8Ls}aNUbtsG@C3|dfnWy*D^>keNADp& z#n6Vx!v*R&Q5IK?D@yYzh+6$0_TDI*M*O%^lyTAM1@BT%ts#V3Z+BN4GbB!5SJBg)fA?m%! zAyYCblJ|W@WBqk533^q(pN({es|STxhh`C~eD`s!RgH6y{Nh!4g=_cM01DHg z{8vohs1iGLuyF>hr13lG9iNuQP*cN&I~R#e(Qy(?;x<0veV^$^yaqfO>uhc{*<^r1 z;PJ<>gehL7Vbs<#ARolWF)YPX-g2)k=Plue%WR9=c_jLhBMFI+TOTfbKU#k;pOOPb z`ORXJU%bvo?p}G5mn&3@m0D=0NbW9Kh*pkAFJu`+Kk>l2lDzod>8^F(U_TIcWaGL6 zJKjxke;JH{6GoN?I2(p>{SNk_!VRjD()7n-QyJweOs}XUpZ@K^QQ3=8`Yqq*nge_; ztWs2rgI?u?*`V;87;{oe_fy)R&y0hV6n7`4m&S`lVvE7EG8=jM>RWdiT_P0&8g=}h z6r;IS%WkaL0oGw?)R56tCP^LkWy<;+%HjMZXBx2@nZMMXrZq*BeI_*pC{)|lU~?06 zj<9i(7sccS6Q%UqVEb4d#A+nf7re0Im4+}4bx@1qc01H&Oe{=pwwbL91zEc}e{ z&@4%XqpN>qBgwv3EX`8`Wvnfbxa!RKojc|b?mI{fm8F$Y$<$`}J)F&%XH9<{Pr$7K z{%;huvH|}KtrGo*A)r71 z|NooIq9DPt|N4xIa^DYD{N}mexcxUP=Qkhxwgj4S;H{VWTL}a+fX@5|0^H0d5R_Eq z_Y!D!fVURvZB0;Xfoy+(2cZ87D&2wyci;lHU-*-M7A^p(cRt7{2HZ`bEfM)g*r74A zA)IFa20wuW9oMNngj;kc{tv)V&}&%8kIjg1d9X?7EW~6bdxQ&1yGeH78l4qt+&C5Z zjjsZYOBrNhlIqR^tj*dYRJt;j7supSs?65xy*r*q7Ky;`9EI&SufbNL%Y;|tJ=a~E zYqWx?lu)d(X^SX^a=w;V9Lrlu(4y29^f?}N;retfE40EszXBuS$$9_K$Q|2OhSa?XK+hN*T1Q9@bOf9TuKb={E0X{YA(#!>6$1UvE-N4*$HKSyVugK?L*XV8^u}zVoDq(t>81C&w~FzW@Gv_UyZ5Bg z=zg+7DQ=&8iZ=A24L(;k(7&iIq@e1Hb-#ll%*y@%sz$D{}MuX9fgk zGjV`kH69QK$OPgRL52=I2Pfzp=w-CMJz@s|aW?Qgpe#wC>wi1Exo-2fnl|tj@aFLM zHR6Fz|DRO&!&~5*SrH+wGg=WK$&KF(3g#az?Oxl8(~+UHk)^X*z0>}z+qb0G)qpHO zCd}&OBe*@!LDkBY7u6?U3pG-Si~p(}dN{~liY&1&JNk#ceMA`_ASI^#NIAUr@*yC|WcLA!(m3d#(!?=lG}2>{(H%hY=}7KVlF%J=VyA~7GEXG0pu zwQ@1Fg-&@F6W7pD3Po8{aBFMre??N&T{U9&|F}r@K3TNnTuqk0BC-+oq1&znF(iVSF2tYn+1W`=fn-t#Vb1U>-}fKLWO+`-RZ2J z`&VEa1Ygc0@oc?g)I^-*6(n!>dW&Ndt2 zdeQH(*r+<=Vfa>WzDr=%Ks92g^Yb~|dxqoVI_8R@r>~U}s;x5I;huTB=kElzMqt2J2X3a_D}U@IpV$EbC#sz z?Y;G!=I7MrS+sYh9@+(ib^{lB-5V2^m*rVl6{&IpOmJuZ@XVw2H*Eld077LlkmJor*9b? zFTx@}ljda$wp+F{Is(##XSdL_GAN-|UG-SU4-NJ(v93rPdHW8x36rF?-Z#j=z2STf zmlzPKu7Xxd0-+E|Jc(xDf4Uy?)5~t?DGj;V=?KqvJCuMDM(qrsCFKR&6}J6I$7gLB zq9;Vl=UFl4=Dg7wZ;RI^kw2^<9?6_ z%l`jIbj82Iq5O9~^VX{Pn+N;1gMVie{;U7%PNVPk=K<)zFL&P@{=0ni|C_zh|84u< z_r*Ct2_itLNT=F|21R5A%34S1Bb*79>yW@=RhD1i9Zmne`Y>7 zB>#+OaY%~Z8w>)xg*tAKk(-qPyag?OD}ll)Ze9D|N;jy(*q#r zoA~=RP)~rj?$2#aP%nV&f1oWltpbAZ-`{IWih@di4b@=(1vmL;u@?~J^B_;1{jMGZ zC{o$!8%Q|r8)k#=)AmTIul&0%^W>kdPr%$_3K-fjq5OSa2I*9v^o-P*Jq}TbWg+TQ z4n_WfheyDgpr*j{?3D|bZ%LZTkg@z4z>4K0-;}P|tyU_5@7%6ts(RpO^Eh+^zM!E2 z>*#fJefBxV7q#(Y|M_u!W90LFI*Dk3Y%xx-viga^;!ykD6#DWE@*(dTd3TYaE0q&4 z%VCpC0z$VR{T((aSZ0)p$#d4Vm9Op8!r}@~=G;n#U93{rB2*C) zW^P%^Arm%9k!e@>H*mwh`De8@9vX}l`mbr*^$o!a@I_LA_@)> zgqx4}T<@syTxs{vXBs17#<_-(iG5hJA^6ZL{;H2eKuvEjglZ3A1OJfMc#tLqjPtu6 z&v)QrP&vHjiin^{<8r8-lNU=ly>LPj1F70`WYwds3}Xsd3+#7~pixFndvwa$qOL2R z9ApGU83MkErs2@z2529&CgnidQY)hvD@kW1;g3wk%_w~F$yNlXe@sW_m|C;8Y%OcN ztVZ>$$4*EMZl3vU?e&nI1$I!Wx7voA4B-^oUd@U;mBxk z&<1S?>vt>U(0h<@>j>R!v;0Guijxs6O&m(uYrDDH9lKafN7;WSf1Brf41*SNsDNZw z+x57eRm`(n5LbaugkjBKu+D*xs62c8$qoMUPe*=ocy+Du=9s#1gR3;XbA zjb)PuD5Y@;V8@6y;UK*(Z?XU3y=_(w%$b)SZZ!iX292S0f|V9y6=Q z?->EsdQrd7;Y1KMB{-q598m3LXB$%w@omYZEnIWYuW(JWBEzq@kbc!1t!;-DC5#q79*drg*NQL1Us#d1I>h_b#9eRm<0Ip zt&ShtLz?vvK_NLmqin{HrSuQN#5K677g{i!QDHa~Mt)z(V)d>VYjya|c61_t&hb7ACI->!V+e%ydCD`p3P8Gf?NP6I{wOJleDR2b@*!wLePd})i)F~irO z*0KW}n#=lowDT1bz>*@2 zR=0FI$n*&(TUvxpN#eFQeIguX1UqKzZk2S^QyeV^^>wk=6!OLyfq13O>CtVkXZvh< z&E7V;>Gm@6r7ld*wGNd`S6~p+yycrX_e~~s=tHtn!v=CAWeikymSH$h8cAHfgqv^B zAwR3JkIt?Ken+5QI&%=JH`Q(BJTrbFQg+=nAirdP0fBN5nsU$2I>(J;^?-;W&btZ( z5+G$)0+1T;1!kQu*f;AR{B*>1APA0WYc;brE1-fM zbE;*jfKmE8reC}{mkyyeaOAR15%v93$h~zO;X<&Moq|o{EW6sBI$7|&52z8fv4S6( zQ~mp=HLq8i!VyIPhkim!oV&3uw6$ohdohj!JJyc@1!vc15ZKIzyqOy7Tkb!!42gfr zK>FD(!09dxoxyB$3@zWcx*mvca{%wirO2zwexLxr&YsfM_?959n+(PlY1l{CUb|7c zQ}>W#g(QaA(i4%@u0`rEsRmW|E#OonY{Vzq*F2hpe;sZfN|((f#<1p> zfW#f^8ZtQ}i{9QglULwx%KH^LOjPUhrKEMa`%s;Z8*a1qD0_2IUDxMbkAgRLVxhK) zwn^putJGWTVP1XqjX#6-Pw_!pLiwXc8yEHVBi1~x^h;4y>!Z<(RAjo&eajya)(QHq z4Jp!Cx{1g@<<9%TVxa9pZ4^0_%F$v;y5-Q#S>Sp`Y%&eFuEB(@Khp09R78(5uJTxP`%SRjFa?aUt(HE6F-h+FU@IonbRX7L|pfJ+itMBOW zppU}+ez2mrmHas8nK>>L*h?sG`Y`r1=vl<8ywz}kI&lUu14p84n7QlNP+gy8 zFsg_^S%*&Ls?7&O#}uD2A?Ab@oNKoWrSZqizHkYd}-56}>u ziyzqACZ7yU37oRig#@+HhQ!aPRgB7*oOKg1RF13jRjcFJdQ?qEhuTgh#h0Jn>g_6w zaMk8g$=We^>c*p!86+-`%4ImRv9vi}4<5K))!Nu%j!3pjkM~I+1zH!`Ko;Q7S1cd! zsC?Ipk*j)xKMy07lW{P75*m$Doa-V^E|=WWqWrGqdkrC>=Aj+MTuuPl`eUc>eW}o+ zymI%8^7a@my=`{d9(NnYv^pAtavmY=ne%-8NRpUPdwTK{(aZHm)r1p+t6`ZA#YZ1d z&P+KXp9_C@0d?uCGxXv_Xpw#K3kTMI2G5umge96}CNK|$6SGv7cOo!ucR<3H&j%OCQ`t|e(|koJ>>D3!%xO9C-P`TS=C=x5ej zSyDct(5QnHv0LGmAzxXKS9I0@U&4s6{a0Oz-wdi1QcblVxyzyVlA|5VfYL6JNQbAC zK;m2${a{#l*`vsh0};X8n@*Yd&EYdEpA-D0`q@)I5<0no^+lAIHElGSV~80>KjwcV zU7jAJjQ>3HyKhN8kO|?q8!(`*D(l2}v%MA`lo08s5%*Aoj2QqFM2mjpa?YfZTVbY? z5PmdoU6xTPA72-w-jeu2#YV!mT}WcG(AFY;+M4~HAYM@!y4JxDJyY#Ut)9L*40+T> z*@ixMWycdk0qKgq2>&4a@(&6>PlOf7I}#^a4C0W)`DrNNDeKckBgINr!G&2m44j{; zCi|}YEk_mcuttGR2|Hw`e0>{hphOCY>Wz3LeI$TLc5dy@gpgZanc|p-GzmU*Zg{1S z<&)Zqne7oMQkElG(oBk@jdLk7G*Zpt0QS6-*?+G2tk*%f%IR>khun$-B9JGR1#vsQ zsyzc^1!{Z4)K-Z_b~UGe)`pj)Gf;R|QTnWU=|Y%O;R76s2dqX;o)0>l&}*lSFPj01 zAlmF58RQ-kGT@Eih_u$%>lTVNrRzoIN;s6wQj%5(Xl7_g=5??x?AY7|hh@~qHr-}4 z*a>IV(XnoxXY?khZoR~@ON(2EtA0BL`uPXMSts;FBEc|d{2}cSp|uKV`yP)}xdZIs z7Wcy(A9)C6Zy0;n61shJ))jYdpaO!w>9{Cz_!3~P?_JX}ao}?zfm6yvoO;qpyg%jQ zoP4JK9Bgr<>z>5YKoFz;AUlbT^RD}QlSA&79cv{CHVs2s68NaS)_i$gSJe_AAWkPz zxfK-_>`Mx%QIA$fT7fLYQ;C>}8B~VxZCT1@Mzj%&0sJGa1GG0YwZCHTRR%ZVN2t7Q zckA^E*j^r227>{Al@`r zHr8x(ehE9GEY`pNmIcz`8y5K~-51iFkD);xD!wW#1n5sE??=n+UCJnI_4q=>SVNE? zX_Of};mva)wPrxuE z67tAJbk9zp(&S>9Nf&y4yx~8b0%BkK;NXzj2rb4|A*rq>`;fa> zKkVsa;F~>3PS|3i;i%;GMTd!eO>9bs}l_NAjY1AHUXe>>qW%BjL>wp_jU(;j&C&#~I(=&jR^Ur(80i0a_ia6%~Y@h&~puE0+9ZS&ZzsKN#F8_b+ ze!lN%|9kGQBYZO^08Vl7`#H2*%Ib#ix_v!?x9rnzMbOg!jl%e?2wJYdTg>{mB4}Ct zhED!g1T87xEim?55j536jz74w+bITuc47ZUp50CmgT2M9@tsO^dQl_!)m=nu;>VHsKj5oA*aSryi(O)^2FD zD?6F&Mx5&}0Z2b8@l6dkX;uvV-oj45zZuad>`=!*j*8^Xua{Vu5#-(bttd)JDpRX} zcF|p+L@oejd)fz{D=jaALxEAGI=xs;-yRo^2cBoySIRHZUy?<2PFXI94nHi5xF3958V5VVc{Z*%qcELG0C zdQ>3)?-xj-Z0CC(ZC%pb3!TyDfI}`g4B0qw5ma4lUWP1!= z5EaxzSgLD-XfZOVj}#syOs@IZoY~-dc$G>YveZC$MM6BKQIE~;C!uRQQ1tZwh%?*W zQ$uRQ{6#8O-YP7JO`LV^CE2UsiyYeiZQl%gh5vcW2U9HQXD!)ag`ckd`3&j2hYPfa z42HkJZJ}r|a&kj71kn)0#TONXWlp^`lCC3ODwZ(p7-07JTJl;{InMm;2zl|h(`H4f zFOXKPKxygB$ZnLQ!Cg!%CgN++Ow78?lg;;zdysBDv-HO%Ds(~vfr8Ezy0H*GKN;w-0iCOLG?UIm~pS;QBwK2BI7M$6iILfEZv5mvF7Q9HvQMYAYnDE1yL z!hc$j?jkyNrqr4G^TqPcT<`O+N8b$GS*H{ZP;1NL-wyLHh3Pm?`$;}YBei$Oqqx{Q z#x$&KntX9wy%Koe;(90v0=!Ft#QvcN55Z4sxSh7Q7CbNXFcS=vY5zWkxXZr~&Rp9-C~p(wxctRfsm4tN>~+aP4Y9NcP7# zzG4WJme(^IyN*=J;9*)owaUmN@ivtAtRWIZroL&K-~&iynH1U^<+8ng_^Uw?g9=qO zt_|*{#qKL89+7pTim1EEGG9ee5>KcH>@Co)B{arAJ)a>m`>Ny6veT>kJio0m#IuVR zie_-8?I@u~+Im^9xAplx&A|Ip8i=w3_Ws&%I>=V>8GJH5ffvwQsjjbfo^%o9t4s*X;zkUS7@&){a9I%EMfrT`rklY916!vC zWjZU?%z9Gg?uwHRhy#B_(^YogZd1-1tM~zA3HzS{-(&S6zOljvE_{OLQtI(-F&AL3 zt3%oPX|UlN%N1RG9_w>Pb0a6vwpr<+RHF=e<-~5}bztgM4Ir9ImV`khGh@BtAPSw^ zvS>?-lY{w`Rhd6~^u|-@fHo|dbq!(CgbeAr805VtUi3Eog9EC%WLqInO7K8@g3 z=CP8WfKYXKR%vNL(bd$1k4F$0-e&DZSCgF~yT;iUqFK`h`3=H7DUu5bB+{|meyR8> zkWUWI&O_pI^5-M-_*S%gUP;aevJ~uWKtVxLXBQ_kBU^ZnG^bDJ~8iMNl{o1=_n0LJ$ti2 zEN`o_6`kD;K8-spmOl_#qvLTI(x#-~B>&5K__E`Vz2Bp)k00*_vAUKndWeC^nZpzL z4zR*bM2z(K=`PlW+pB&!*km>PXsOfuF@U7<(U3M+7n(ui$SdS#-rbG>w%{GOkl>|9 zgo&Cj6D1m#FuZ3(x%}i}>sU|}YA`kzqkO7644a-n*NHsNpTe=yTl`E-&?*IhvfRvi$YW4HEysf~+J~XZej}+#!aR?;%H1nY0VvP&zB0 zAKo_|4`c+m?rsZ^SCh7*1R&N=G93AVzwL;*${>bx-n_i#%}X0U$_ArOpbN>7?ZRcF zRNFNZb4%D{{gJCNNOd}=+E~VU*bL0-A>zryF7-&ld|vIh=!%8%rN9(PO-rV;eZYZG z@Y&us7W(SB6oS>{h(7chgmJpWmvu6V5xo{a z{sdh#23uK)(pb(9&rv5F?7B2b+h5Kl8v!7PF2IjN)OuflSvCr zO(U<72^@;v5s|Wq*qpoY6Ub${K#}%mkbF13m_D8r)g`b#%5y-^`&!+#_wASA_D#qy z1z*$~z@48MnQ^V0!6GFzB}hKS&-HL3Y$FsxssTz|*GE}3yf@}AQm-u&b^DCRl}2C0 z&Rq}SlOw}1a)u|5mem?pRG0FX5qcoK`D zp}f=fIPuId>0nQieM=8!MVLu+>fM~1+*|lWv3CYU0}&Ckg4Y}r!iutuP5Mq^iulX? zKYd3QAccnD!0=W(aj4IHUN5El@6PfZHZH@W@}5=)QaqxSXd6^>mva$4q>&BV$^tlZ>!5rkqf6vmW(&8rCgQa^96DV?6) zF-_}73;lu4=tK%fc^rFv&mqK}&=0&Vu<_k&dFU<(>nY!Vk=YEfxW0O$PoSyY-LglfhbA7Al5N7k?*?`J*j6@E53 zMN8L$RK57LYZm8o*>#}KCyeL&K8}mt5j~;PPAlJ&5FkS0Td}jCaDmlf2)mq8LHIo@ z8Ek|E$YHLuD@Ih2Cbawj!fUT^@1FSWmi|hVH(iqN-j_hG;2j1fnBd15&G8~fmxe7< zzx?@e+Ke11$ENw@OUJ|*T%o6>R`jbrygaYI3ft-J*iNHZMD%z5F60_XO$zmZq>a`FZ;Q`47@VF`l?QW&-YVz+gD8gTC9`q|T3ti5mk zKBJQ`g965(e3tTg5%yEx;%U;|x)@iguHQpykZ|lStdIl{u8ZHdP#(x8aQsQ+SydKf zNc<6XVKOZUS|`;~drdxhspcwK5>uKv5-OiT3&k;!_fwAyhAo|t_BwonwV1xyTQXPE z#54?2Ij=)`4xZkiQfitk>CZN!BI;sP(2eO5<{z+SyT8T^${f8h`0{v~RacqQ{Dr73 zGKaU|uxrBypB7}EjnuDT&Q1+}%Jfunw1v4$T4~M!D7dc%R|N_fW# zXI7(-a84q_2S%kJ6;CHC2&&U?K2`fkcCI!;1kNRnw%u{s6NpDQ8M@Rs98r$NMy;+W z*F2>BUi0O*F4wNFdkD#bxW&xS4zW>=vRr1{5w>D*iRRX3dix1}d1+6#Bd!v^x>z^2 z)h?n98TSsj`kqx^!f>H}Lb`7SJeUo+qX;9fEbY`T3|oH+`pA`9B?PSt#HQRR!f>|6 zN;{RCL5g;Nm}nK{URRd*5%e}31+N@H0dK(4US*whLBi2b1m;W@1@ZCamiG#vUrL)X z$w7E5`>Bx(mGoK!T^;FLMcMWiE_8QWybf1og={>vRrnn^>_Wm(-aN&M56#7eA5!J6 z{cwz8)=zBua)NBUAJ@tA(EAr6(Zldngm)A}`(W~Mz51**kIgMHFhCGuqCzWXvxT!3 zwiehA{%PIzjiyjelCGo2gj&!nnBvBy$yqN>!~A>s`gXqguO8QJHAMSAg3^uYb92)@ zS>6DDwl@xY?)8UEm=dzPH}mylniw>Q6E|I-;b*S!G=lwTG^V+4V1EHx{+{#B!E!paO-QOw!c|=j|gb@!NBxNE}~P+rH5*+jzGu(~K*&m8M#7mQc==rx83DCFJ$a zjLo>{nShe&u62GdYdls>dCVGFo1Xq;3LS;c2W`ofe$$90xPK92ZxQSLf{0wVz%s7@l?a&D8LM`jn~`o&?8D+ zZ1jpwdL&;&mIEmYJd6)43D0s0Espa`spfvQJJ7I2G8{v%*!&`jYko4~nHVN6m~8cj z&*063K`@_p0^~~vYK$`r8%J=^b@81fZ9@21(+_huV4f8>JoYfSW{wJja&96HAX`Wb zdeQvemGl)tu4|5kF`ssh7Hj@=(dahz%16<9y82|oHy~HQ=Z7Pl6`?bpCZk3~X0H#| z7p^d@gPr|OiWp297_=ZB};ue35Sl3Z+;*xAue%yKXI zLlg)-D?61M^jJ=6UwG#uu>agJ9ckb^aH?7vU`C|E%6RlDL_$Rlf%o!~@mU5v+8g#) zJfzYlx@Xgpc3PO%dL5Y(DeaRw0|=u(^%o|>yMXttn+JnycV5W<$gLcrRS+8NJ}L$K zS~yDcD0}qzqw-hdDu4t4!p}w%jC>WGszLr;i&7#r+b{H%WFA0_z2R2Ucvf!3&?Q)n z2I-C*+rBOLXDd!}kBwpr_xP`=Er`;e89 z)IP;O8~xf!xkkDa;(8aUFE&OSrH77LGb1RYFN~Ydt5mO%pIf0M4M|LYXnpBTJ9760?+|HC)@=a2mFt?>^w@E@xFd*y%p zo_p@NIrTr0rTm}3Ac?cxq{F?hCE}n>NB3X3ul7HBPMqVP5g__U|I7cY^~FK@!~dGO z5f}M0hx^|#hT>v>(DCA8{|YKvT>K_gx48I!#@~udaNJK#-V%T}eB#af0KBF0ZcCtd z;5WkQwgg(UK#o7Q@ZHP-@D_8vy#^XL;4M^nTLSeL$oU5gdDCYg=N}~GO4b$|5*5X^2B+ zfuqr0)4Rg0Y6b(-pl0!a0J|h@P8W2J4xE?6m&L|s)24mg!k;by;43}In^{_bHF9Me zS7)1q0_Cvg4x-J)c1Q74iPLvzAJ_~g5CUoB&RXz0{7Dp=%0zn|2V?5e1_v0Q9zS^n zdyHpr7q-szJ^ z;y7#?IC>#AweSek>;+&Q!OjvI@EklOx_;PbtVbS9`3Scv23ej$HJB|nZiE>hm6LD* zz=rsO*jR+vLY40DGu|j(IW8wguZ=ZpyaFzkw)`%~V@W_rddBGcG*RB%Cr@1lCUUsl zn_07u=O2-&XPPN&+n&Pq5qN(L&U27U+s4T{#MQ^yMIiImgC1g7JrGzWE-SCmC~LGb zD6|B=d#&O0oXqR(>(F@z86tcnrf+{%R1@2(1vNd+{g?&BZ7RN{L!?F;6P=Yt<@CaswHvbylD+gr~mqPX{ zrno^Dti3A|USZFd6OY<15k7v#w||d}D7BYleA*mTdJyd6h{R7^cBtcF`Ux75#z{Rc zKRg_HhhkEEixp+G&@;Y=J|&zX{k;uB8NnM7_UEh;VquKK$Z3@>+g7{4uTwO#|*3 z59GeozApw6lw18_>*`PBA4mtbofKB$G{YYu4;~Y8D7L6npYn78S&H{HfzL8bqf{^~ zERhjACazo-{0lY7m-3aZ$pO%hv&j%bAl-a0B6-WUBoQbfjbFYyc!Xk)gmof1XH+tO zNgGf8RTk@X)JAwmBm7f^RF4xz3AB>|0Lu7-p?0c2&N@S0HP+K|$Fb`1!6NRD&LK4B zm2@6P@QwM-gqM|1$uC?YbH=d|TMjHJqPyQn^=2dmTh7dLf84AtlAB!dt3mDbZc-WW zkg)%b!H@g0iMNpzs=EHYRXdrb-RI>!2wU;~L^%2OCi@&CO09UP*)(TzjrX`RB}6WU zt@>)rYJ_0WpI$XY8@RP?d-L11&b+yVkfgsi%?>DF<6+~!%Pr zKl9l}Rpm*e*~dc*?HlW;#r=J<1eRFL3Q|X{av&)f2SeLFIQ(P9`X=rm<>v^BQ{~~Y zD3Nd2Y2Grk3*w*whv0+6;v)whPQfI&A|IDhT)Sz zgq!QfzrMDBTh;!5qhrQ-*D>P&IcA_FIiPPMD0-ChL5|t&goD0=|3e+K|6VWXfAZ7* z887O${@mk6{VO{9zpmN8#*F{(Fxvn38UOur!!7cM;t2P@JYf#E*o}tG{|mFoEy;er z+ji?0-8ft~;|jd>aDFR+T(es*;kE=C2O!rU5u%`Hfl9Zo*&P>)>lf1RpLMag9!MMg zIsN$%02%To{rSAAgP**x^1fi~0QTV3ELKw3jKJ{=2~lxjQqwbw!7ROvoHmKqI^_cX zd{aq=l&xM-?Q~A8I2lJXBxF@1d1_|e(Gg{7)@T(orZy7y-Z6to-q~3f~OKeQN8>$*sj&A=FE!Q zm_{wMnF#-x)~q8k#$e!a+7tBkpt>0AkjQzS#;y;|Mm%)8(q!y3mOKWoP!k187_Io5 zlyo$KlwQnU`NibawRKP=pRnDdkz<+WS#dYXP1`k*?Nx%&8F`zeu~6!WISBAn8;!i( zMG;b10ZGc5Tf2lQ)m?_7Uq`ypHM>kFh`vCT7{4qiA!uWo5Xfk|kTz-4V!j$96xz){ zqWt#hIw&FJD}V1&3T3eSx7y4~X?yx^ysYh>ga(oQVk^z)sH*y;_iokQ+FE)6h6FLk z+b40!NHRo>CFfnk`l>ygtrTjmTZoP?-v|vU`pvsS`grviEaeZOaZkQibiOj!TJF(b z!C{5;%)D_g7 zJ&+uE;vjq$UbXF&bfAeRRAat;c|SNGx?hO`Dod>^YRKagz*{R9sutVA0Y9TurN<`u z(C0Ryy45dTzL|Yzvh<*gxb9KY=K8WlQ5W5_{v#cSD7Lbz6|V$ux=X1v>1yTqYrtv< zqOsd+ssX{GkZ>ttwNtr6={BAVH3sRhhWJE-}(}vnYGA5|fmrVvSAr!ov}D--WD1 zIPX*SUQx{*j>_bwHrD)A-7!lGjs!}Tm1d+OE1(kNMkQ-oz_pdiTO$?puzqlOM zp15ZWfj36$L!hER{g8n&khBT^CSv=3{1TsAQF4Tl?Q}uG4?|Y#SQTejZMtD1M?%}B z0QU2f5DB4Yn%V|{E4|J7O;_0VF~^HeRxqc}IOk4TBGp?~qs5UnR_+@ z+{BN^o4-ksbgLX@Ez8$sCsLQI)EXn3xE92Iif4rBJfhYwlEC90iDLB#af{9_q~%eP z9pPQkc7617pU}`IL~mo8)0C@|7q8?>Bm;5o3L%|I6bmYtax1Oj z38n$&A#`vb^PB^WFlhfDDsY02hws9$zWl6pd^hk*#o(hL5)7t!^35m!t!e*w#E)o^&a+k){vvJ4YnmOWCaYM4FAc*GS*iowQFyPI$Fm8_ve%ZV!+be$aI5b|O@F>99O{ zBj;uyM)9-9aDh|qYl07%K|}^1KJ+D74b?d`pVxPP*CQq+IL(g?V|_8iQD96SG-h-u zZ-%5Ld3@9`M|d{V_ggBAhz2fD=4usW7k$VVD2s4tWA_>Kv$XW6J$9kx+XV7xo<|C; z(d*r}CkB+Q;bA{>oOi+_B%=91iS!V~E_iN28(&OsOp{8L*S;X=)yV^xCNJ+>s)>_2 zw9Qy?t8>E`oNG>0KsIH5aA_ldp95*`zozdY;HCLhP|3O&f7fBD&5f`l6+A!7h_e-O zieoMef2jR~RKPYG9(h5Or(6c1h1Xo^55KWvkTe>b2PgnFkqh+|oP}p-&(HHN4tG1m1n87RMq%8U(Z)) z5~~gmI}@j39%0)M#XCt?q!qrYNtNFtf*9l$SlYq#nusN4FZ=NW?;VE~bVG;Ir=MW2 zPA+~x8cCPWAusypixw)uR za^%$|sB}+i`s8uOD4J~a*r%r{GUHW!M-HLYrp+j(nd%3c47Yo&^E+|;Us2Z|%Y1$R zh+mfD0BnfGVkn(hc-)WIbUkP;>WtNDajj_>Sd*Zj%u~QCfbwt1V#$;USXvTDUDL1AlZx_? zF6g+J4w1eUwYd$`&j)@ejN2C`*|;#&dd+pACgk==mG1oGkJIyyZ@W7xGGXv}KP8e} zqxgBl0!~X!(6Pg)DZx{}8ga}kwn`Gm7pow?B#Q=*bgL=JYoiI54IoE=4%mJk*05N7 zMh;!{e1hE)hvczP_R3Lx;&SU&k0wV~s;2({qHxv1elUP0a%h zbT+O#nF2uZcsD;4H>Qv;nCyqj1{#UswpEZV6w9?~$&F4I^5Jdeds1tY0>U^3nFFde za#j;6{kDK%F^BL^$m<8~$n9 zP4dik$Am(uu=sBefAgErm#@ZYbU##Plc=sXCdoDWmv4Tl@kA@m=@kAfmh4Mu0RC z^>Ki-#*_G-y!GUl{j3$%!Z^>C-Lmo$CC*+uAu#t7O*AY$ldGoL6?wJTnQM87VWMj! zRO8_L@fw8~Jk)im^-bGBOP$0jc~`+j1@Cxet@g^BD~M(qgVy^->VbwhJNq4-ERam9 z9TX^NaRXDA6qHqt@Au3t*5>m^m8tFUoxX5ol@`(X1E#q%5)A=@9uBkvJJJg{ru&0r zvp=&eqmg1Q&!4DZr}&9|26Gw%P}bzTe}^ggkG6w`5XW zL<@@}%~_JvazNdwYuM&-#Tpq7?38mRhD|%cmoJH{FK`o;sqBMed^C*+22qBsiIDc* zB~J-W1;Dt)Y$t|oGWj$lx)_^`^wV~A5-ykpRThmHkYZoH#rDU8)1y5%iK?#@u{C&` z)4i#iZ}90Sa^?z5L_)yjJ<|f(`1`Pr-k)syXnDJiKNfI1hz3?s*IWJaVz%Y8?CclH zW*`K%IW;vxifpV?G|Wsq~n{CcYmt`y20VD0LBk7R-@! zsOXT=gw_z|%B#l%g%k)Mw-({kWYXReS)FcUtP&fv849wa7e$1r+uEKey0r14e_i`@`Wj{00@4GgF-m^^<|Ia+N><^fKn*J`a+L zsB2C;6DW@#0uIa9x-bI!m~*uxXK)KUD7u-}UiS^+13d^fz+LbRWQ^S;-StlQ$6;+2 z%KvVIn%6zwG@#VU>vl?tT(qPF8pQssq^}jvqTi1sB4&EXB6~%?_gvnSGy==-4o3z+ zG3%p@zi2^v=Yg>8w|j{;w}0v8DNz1lKuXtQH~~{4ks*-$|FHKK099sN`?z$6gmept zD0S#=q#Hz9y1TnUx>x&~FVk)%!Ma?D6B6aY@WAU#kd@%c_$V|d#?}On^Sk&C zvlHY_%NF{La3fS#gQs;gs(KEND*8{>(A5Zh^EnvrnGviQD9iV@K?iL#35?`R9-}cQ z?N2Hb;aU0z%R{?|y(rw+g5xbyHN2{WesUoLV7xS&Yex53`Y|B4Z*Yr5jAubYzEBdp z7srYb^yTd$u{Tw&5Iz~=ZqsO=Oo7Y@#ID_mM}%IGgPrMaS=nZ*OnE@SXFru2^GlYL zWwc|kL~W`00(6nUFLT4mq{4P}y+~o<>v@^CaFO^Jx#%4Y9!p58IwQ0=6*i$BwXm#}r?T#c@Swi379Icd3B1v!vP3L=Uz|vG5s`n{86{|ZpR)@>O zFEjW_47Ok#gz;y3oP`P7wK*pv`IJm%o*l)IR1O(-o4Fdn2#O99(NtGna=VQlXqz?m zPh`~D3*@%lUk>p&c0QXG#JYG$FVy+X*a^DtN~JV3O3I~;4PLj2aJR;fR_Kdgk=CCr z-b**z-|Rp#fqs#@U_LkgfUs~h=P|XkZU3f5RWfqT;xP<8Zqk)Wi7q^azac3R$W(kO z-j@;NR^|XDB?29R#m>a`Q*LE;@E!jvZsn`opTBc#UMOA=uXc4_S>b~6n|MKUbvr++ z{jQU<1cq|vCZGhEZ-fC$-?x@eX#stjP37R`8&!ipqRzW{d2L*0t%}C zE}e#^93x=4{tNYo9>!&tO)sE3od9uvnSy%h1AR1S zZ*UAK;d`N&cQO8cr{ap3%k$!V79*So9PG+I^H%t`Gkx&Uo=6Qb-h-kk_kB;rkE1Cq zl7CE^9K6g-JvD1e?z4~cPT8;9*t$WmRi|H)F{Th+0eVnrQgf}|g$2na{gbgf=8<}r z|5&`*lqn;t`-4nNXc1T!xks@u>zE%TnRNNhn1Y##nb>MoEe{`MCmB03k&l_IZqE(S zOy-8lDRn4N5$%g)%y&Old4FxA8Y9K7oS^gemln5K$*@7=f<9<$Q8G{@KbYiIiwrV?apdEE#(PK>``MXHZ;PeNf<(zIXrGH z*&BZ~La$xndezaMpmShAQ*Db%I4OhOyZk? z3I+gf`~9522vII#!F6Bkx-GI%AA{N~mHQ7-F>j$dWMW&rd*$c5QeSwx9`j9*1F+6& zHkneEHfOk!I&nd=O8^p-TEgAx~hIGy~IE|A=%I$$L6Sxwqyh4_l*mR(c~=TSw)MUyTH6K6{f! zRJVQKqGIj3lDw`Ersck!7tDL>kgGc8Crw4ak89``0G(Y6@;vb5O+va7_#RdCb8~R&YV#^zYy3=3&14Q?XNpDlR!7yHZso>JAHIba zzOt+Ske{^VF#SFfRocv-0jqvNwSH!=Ot{#h3LVsyl!})vQtoED_QE$mDoasXC`y68N-&2@ItwCQ7&IMa2Q@Yka^8g}Z;_!`aW9JH_K zKG1P~zkdCHTQUe6*y-7tSle6KekJ%ax)uhc^l~PaMxwfwMv6jr6%8y*9$J~}Bhri8 z>zbSB30NAL8;~-9w)p8<-!m{VGPVa#goyNt_68Ozq>OA#i1hcr=|KrcL5-U08rhM8 zMsn~ift|UGj2FhC-);odg2O|57i-YFgOz}4AI=&aYSCI+ub8DAxQ|9kN zV%g5S6f}AMTM1P!E(89${2hn?rKvx>@JofOe*dKxUDePZ>%edE@2oMdVsZc5bgZB( zi+|3?fBCCfL5UQ9zVz|~y;oZN!BFu32@&S~m-3DmD_=j7?U z1p52g*ni>IpteAzbAEk^Cv#k|#r{>E%<+?^NEW6`d<}GbL;2`=CKJ#BZ^H67b9Si}G;oONpTSy_?WV3TT*dkZrO zTraq023#qNC(t5*eOU7J@h59i8aD|NBvPr`SYbtdTH@x80x|_AMSEy(6#2(>z!hAl zIh!iQ&uALLpq=o&Q&bbcG!zVB=}wy?sA=L^>JBLIVM<@53;g=S84ihmz1pffM ze7&RPvnU|mEi8SYhu>ijC~Y1b6ra;|75jVDx5Ra{uR-pJz85%ldALT99}UE@%+SnO z>{hlv!qgRFv$e4g3`0e+wyr5#YM9NR9}1HBs7yN>Tk(N>_mJk$8@c+`bEhY|*b^ZM zPP>>NO2(c`b(at0V<5pMCqCD18yaGZF~LN7bHUSYDZfVGS^Z^*f)ifb}HiHH0>E>s4Cs4U-%aT_SfD1OmABJpP_8DqM^_aGi zVg>B;zSfk%@k_RUd*h~@A-w)ul9WA&u!-!J+1qS}(N(1*3;}dl5ex3FCEH_~oVM3z zx!eZy{NF|!XqNa&ovk&T|h1A`yOLjnKYCh*r_ziN1{GK{nzWm{m{!F<2SSUw&4fhAX;LtoH1PjFMzD?>J^$77$NeH^@k9XR6RnkG(w;vAHMI z93(aO84Bu=S}+90AsNj@ z3!<`k_IrFD0UojTR4NEK2JB$Rs9tRXqVjD7S`Q$+ntk%!BYZ}iyLJkkrY%ZOltQ(h z#+bb0dDPJJBDX(YJQPXv?tPWj`OInWTY1Y^4r_067X1#1-=GZ2?LNuHj~mawvzcR1 zg`4AOflb*yJVTZ}3p@t4*`Rf-$eMgEF3UsOepEyyD(Fv9i5qAlK!%m8FW8A>twnLe z-Jlxnjtj)Q)#khbbK4JY#4qy>1e_xKAk0;DPD+b8&xm6h-wE_$EAe;O?^PtOx0!2= zIE%1yl5KHDK}UUBslr;Ha@Z2mtR#F_okN}cprZXDSKBedC#(kbJFecQBr0|%Z&7XB zhgW9Pd{8zs%AMT!4x%JoHj=$&8Qn2I+NX@fwa0SZuYHyCcol7mZyT34lG533&Gk(c z)3YfK#q6ci8KH^v>zEPom>+U_BkCaiZl{qF} z-fzWkGe4E;COdm&+IquP&iU2;62V+fL**svj%2O6;6XbAjUeSR{vFh% z+{dW&u7aQ;^aW@LEeQe-p&z`hZyz0uwmXN~^~0(S1PdxFqZF8!`skmz5WGd3m#mN* zqM{_pAWoNb`p11PM@6wndAcVIeo6bY~3 zfavFz4T-E1s&yB+mP~+=F-`;Pfww)f7$lL`JP>(*G{$<}7+KKl+F|rKjmOj&?`eVS^-@Z+z_c7i5!0 zI&w6Fi8)?aYhxIx)6E*Br0!cTGIiv;tCYMd-%-+CBTm$)ax2oUr339XI+F4Sv}^R1 zXy5k}BjbgB`WyJtGqN%M!Io)` zU+j+HDxe|nJHsRU`5ABD_RRgoA&;Hmg7NZ)-&xknA=A;#pu@!Bg*Of#9s1n!hM3t{ zG945;IvXOwc=*z4St5QSWLD4fM=qpu(k+>^xQ5K_ZD*Jn}Xf}C&o%X608i` zMv2Ot4)8*=KuQhHCcb&7Bhu{l${EB&MC;f4SKie$tZ5%6Mrr zsECqy#4Knt=mPc;XftTlCq=(z!h$m|B89;;UaTkV7?jcWOAjOyh7;V>-X|@-nawpIO{;<W$S%o#J5bs=6zy#@22-& zC!|au>_K$Ntq=?;XO$1_SvQukj+%3$ki;>JFnE#+AGef5mkM=@4K<4WzNf~W=sA%` zyO|A7C29(FEsEUKVHbrT8t^k&o&<%3o_#3T^Z7yyqQPyv{mqN7r_fI z4eiI!8PjSoC~?QJLyz?y&h>5(8E2lT8MQ=?g~Q4&@);&a-EcjtT|zOVaWkPT%3&{PdC_ov>vCJkikQ!#~Kd@2->wIwMr3?%kW& zM!o?^GlyeJb+vICdJeZxl{<2e_(jv>2q#n+#s^d`dd|;Xd@|Arn@kp0vog+vnX3tS zARZwX@xvSqFRno*J61C)c)Yzv;TjgwKHDrQIV}k1pEy+qHTtFD6w3b7uFX}S)t|Kz zU*0bPwi358fF^9$M?6$Pq@zB8zUb975F|%J!ln|OzfnLjLaZg z$NA*_Q^C0GpvnFpTgM+;48Y_9)ItBb|Nl3$m4BAH|K3s;6c6*?$_M+Ocr>7u3B#4k z@2^zz+#>@13%;_(uSHN?%va8HX*tdgI@a~qaq=&7)1P#Z{63nk_t$;&laOf;kofBZ zOZwnUsQd~Ja|An$hyKCdI?$!DqwEO5&=X3+1)r1;s0wfIKCFLaNbv!omCY}H_fsy-}D|d7N@giX5wKGF#vs6cRK=P?k!vPH+ z1P*WC_mc)Y$i??-XZ(<4GhbR5D@O^GfU@+Q=kw$*fBt-m`>>!qbN`d4Yz*rXm8&mX z>#dgVw)eivayd&fEq8yVejvV6vAbiy;^seD{I;XgiQ|K}$Ee_Hyv9{6`j(r>$t{uA@wKW+Sb zwC11w`@h$JEW#pxKox)S)PTRE?B^leV5SJF4=SDe-7iK@vV-EYf8|DhnJ8W`sQ-u~ z09Y?^1Z4$|m{tblmCuNtO#t%Ok{g}9GKdRjYrYDr`n1i=@Q9h!`!Z?m*%U$j_IaJV zt>Ljt@OG{iw@mFT;|}0Lty%TzvapaT2%vOs+_G?VWsYYFY*uXESQYI;)N*`Br#`!t z>``fiWWx#jUQ1emmHU%*!h0(BW)Wx0NA2UrNvFB)&E%;h>*I^vs!!cCR;_^UES3l6 z8V>0VOf#rJaRU+RvFn1qEaHd|kBLNLv}F8*qaek!!>aDlaJF2y#U!^cc!zR2B3rXF6u?5)jYp+qas{GNBD`rJ_lBq zrdkM6_lX}4(^CktF_$(@;>6@_1LTs)=7tp1dR&=ev1n|3QQXAnn*$FdRp<#x#%MVn z4|>-<9%L!=R$qfy^6wC;&K3%tbjVqKARB2LZ~ZV%d2C<>4%e)Vl0CN`C^Gd)IHv59!$4W7D)h=*RTEGyLjhr<2V% z^*sGn1FXN^+aNK5nY+CL1p3kxh|0<=&vNr&V!kX`2=gUr_Xx6V(6|)HM6Nofwmfj{$2j}599d1$2k6Z=KnXJx!@lp75_>d!Q2v51H>)CB=E8k z^5RK}f0bKaJk;Yy+>(*)(q3a0IROmNmfE`GNA5aC@Z`&GBTKver)MLRGH4RW5WdU+ zTw?;9pw=p4PFdFVTM-AW^RLLB@Y6%ZbToXwHpO_H_B< zUguzFW^o7|lYD!Zanj!22D<^s&+;GykFqBOUs)yl8N{xe{%7?A!huxHv@WL>A zDM0;qrngu{3Np#~!h5{T;nRCi3`LsRt^#K6o1fsG#Y^ybGk0?*iLkDzOVat)|DL4H zAY4h(+iS|7tFKb%mMjsb;Hb0V*O(Pu%8iIB81kqLP87)a*5Nbw zu-t)4T4>0Umh9hm6C=}wZu%QI8YAG6#SNUC2V}M|CR*4(Y>FwRrQo^LbCPQ9^xMnG5n`q((W>6UcFY$WcXYo4$V|41Vw>e6FRvNW0U&B zoVxV9>^CLq%%0$b7f4vsX!}NNL;<4I|bR>0lOd9i^=$$ah@EmXEcE6L zlb|T;w~1k0*(Re)PQ$fZxO9R8LYfp@m= z@ZWU}UpsD@X(@t)rQ#l53gE~>k-UZcP@Htl^h*e59{cP=+xAf=io&`L7N+#tT(i^w zo@W6?w6VLT*c?hS9|Mh>9ZJZ20$x9_b`3?MT79}?&4Oq&q~a-O|3$*BHRp5Aggwdf zLYP$5EA80zYQ+rrF9%MJ%WfwSTozLAuIh@PbiFaMU&@at9aV<{GONM3NpIa?AU(}- z!GNYq{W2P*0GY=n*CD3x-c2lsAws~%1bNwqnnk5K%hb8b#AD{=TV{;J%^*vFd=O=Zs!+ST6Ww#L&%S;jfbEC{RK(cRdNQ|Vlgg7VH_0>g3wYq=ySOW?cD-` zI-ML+VGS;heCre?j+NJGf}OQ)=HIQqp+GOR17*3*dB@jk*ec$3fNfUeZqR$HyVJ@g zC0=pb2e}muBXF-u#jn^M%6@Tc(~mw4phL1R=F!oV>jZlr8+Nemq|mO*jIDV`rDP|# zFFvUSnW^+}@?n|V;m$mKgh3I_#_1DJ*jgjk@*P+qy{JpM!Wixw!5cM3i7wi_h*;ax zDT=^R^5n^Ha%MW;12#T+?tOf2myOLT+rIDIdO(18b47I6ov6+qWTzrY?H75L6sKc- zPn`aCeF1_G&S)h|NR`bvRbR%m9SZ!huhRnS2tlgfyL(;XKw)KZbZH7ufK+5*Uxa7> zr$cZhXwtZ z1+f8s$+Y+1<_-Vt_QARDe*Cxo>>Yrz)BOR%`Nd}h?ltyvDgx#^;CKEyA)l8(x$W4` zx9k6o{ao<={MA+d#jwsFjq|^o>cBgHpvjP7_EV=duXXa7+Yvvx725lyiqW1r_G2bS zy8$2Qw3PmkbcrOtXn!HKZa&w_{wChWGmib*!K2LRtWnaW_pE+IhO$Kt zZ#M43eF^T?#-;A^H*gROC|6S~XD(doO}2%lQmsK9m-NSf|2d(k5v^fF_1K*6k%Cz3 z6|?=tz>yzy0Ta_D;&XAe4|2(1=JSA7`?2?UZx%0uSNlZYR{J;?SNr)Om&~h*pL5AX z&h|#D4mUjbG!q+TWgblw4m$g_7VMI-N{u2{dQ#Fn9c5X&Y&~$hc0U={C!6;GF@QG zf5X(q1OR2+0>{RD+wKewzXPST0?%xKpW42|e*R=Z-=Loku=(@3=${(@|LTE%X6ygV z4ED1#-9PpI>F>Y$Gaw*xbzWok^Zlk^1_i1?3flAeouA}ll)+zSP#3J=Kco+gm;59= za&Ig^XRp-PA>x;{c{j9wHpiNaIW5(lq`_&0+);m4@QlAy${eSWiXomJd9*9JbAusM zJr!t3sbl9$x#3;Y4G*#H$){;k-PudZJzPkdMM#ZckEG^m#bRkS-)`)!v?f z46!cF>*X&59k|f!_-3$3EjUy~gFFMW4Nm#`xjZ>Fm8z)=Du9HYdiy~4M^7+vv`nQx zX{xhn_YOEittNOR!jMfZ-!D|C8FWgwEu0t%mSo28cje;?*L-9nZ8K3tgb~*~OWT?O zAGX7DdX3av@yqwb0hHkBXZGJ;QVRAvm&r4O|D5#0XcgnL60#piMJnCLTc7EA$ ze2BZ2sGHRn$>E|>kpJ<^3ps;wH=;VG1uBP=2U*m0{V^sKv|za{*<2!VLpGd|F1Hvf9C$H=OO@r_3G8=`EUpxFF`dx{86)#TCHU@sg1gz{1SV4&vA*=Jp1*q~KJz_6EWRdRF=dp!~E3mPYo*q>KO#j!T1I zng^V-o>BbX-FMy4P@Pff;+7@c9@)YwiMa;mA62UKB`dp>K2L<+tw5r==?@|JCa^by zUl2tX1JeISOZlooV!xyAlEu;;=oUf9hqzY5s`W%KDH3~oY2vf0N#w)RLcos7^H;i4(kcSRnjw1jp)Yl( zI1(Gp1kpz#AG+nm$u7L|n2n|B)()y^fS#;R9#At+X>(W4pHP42E`O_N_0eIjMB`{| zX~UFVQWb-C0ZWBRb3kIhC*?fB@={Jhwy_OwT83`PhnZ3Fe(%aP}ljCHQeMbbL1Pyi+vB zHT7`SZ3~C=llP<@-DxqYhVR`(ZnSc2-<1Y^#PVx2f(fMbKJ^gW#PGZ;G4z73kPgux z*l^47m=K@yTdyVd3R#aQ(g|7y)Dv%80>#MK_i{e@oWR;XK&Pv__QWrV%<>*`OweR4 zof{+c{$9D0059Yr_v~ntwop2T|C_#`XXu#Ft~K0ZGLs^_bhb@axBOpM3eN z63>*;OPuLuO@X-jgiLTg>m`cfdcn%AxB>opwwL7#s`i^q4A$y-!4TTey8jeSi^@@4)f5P(7T&I zLP#gH3nfZbXad#+ecJ@s$nS}G7)z?*K@9Ea@RAr(yN(l@1J*>aB&Z&7TLpMbc6RNM zbM_P28A65sT0x@F9{-+zFZw~!h<80Yj)x`ML>F`psadUUUC0`%A(0tYS3wd1iLR#Tt{GeEd0_d1!PAv^DO!Yu2Qe{ z4>&HGaP7VzaveeCu?R}96p|gy%DYOKY+Su0=N4D%5?;SXU#j^kze*owV*6l zR$f~FfXt!^_3jHy*AWDsHUD(StJJ>fjsmiEnR%M1@CSN0Agq6;+w8asa1-RKuwK6* zkbb@W%Sia~OM-nfwupw~yHWcB#6IqL+d{w%7+tgjbF^wRG-sVZhy6>qq2dmQ6`OHa zjVj{H;`7hdmOLfukt-i&YR|<{?-ka`T9j&dsMUdM$IMNIr?1 z9qz6Vy{y(An)cKT1s4*p@4sp6G(l2K}|ayhP`JH;Gt&ISX8a8GMxCeq^*_I?0$rWN1YN~EnS=J?Xx5fF7ybD-ry^wz zo?h0TSD@QWL^_#+^r~sn{;{|Q<8=iBG}l4+-K7AM4ZnW{K_nYL4H9pXKL$OKHwYIh z_>++QG_>LPUZHxr6K&V6G}R2bkcr=e*#>3(C$$P3gFA$V=2&sy^X8_457GyUM6IE7 zwLyvVcSRWhCCPx%Xbm`!XME7#KoWEo*?QJ7bvCnkW^-`He&R)V%7=P(g?b~c$%fjx z7qfZ-L1P2D#0heN+h;~*vKo_p9Xf*RqPSmcqrn!j(rz=(UP92}FjH+wI21nfopQ7H zrn3=7Jpf@M8G1v?*NLi`D|RwX{7N;^>TM|>r`#qk#mO`p@F}52i1h%NiDbx)E7hCS0S?1jI z^>(VCB%*ijK$_U{y(qd*5|KD}z)fuVIQ@y}=X5ej#OK_BHnH_Xk^0&Mg0~aoWZE_7 zKM_ftso^IOe4TtJ(|#-pPNtzdQ$tT6_&EJoOorI#_I5IC;sQ*j;cfOJI~&2)1E40Z zQe(ny_&0OuZpz@+|A`1)j|s6M+{AT#Q|8BF#blb=X78O7>l>czNPMX<8{NK6hRs~x z)XqjX>H$a-$>-|%CN8_pDUQuvLT4k?dH@_rI@CtDj}urunf9A{A{lB!c$8|125!Lk{^m^+$Ubdr+jE<89`@#t@#U6XFHo` zE(e#@XXk3KRjNBpXE>8*NSkLc2WOBcUJ$2^*Uq%P&*-mEgT9-fFDCm5=qvdrBE*?& z(;4RG8T1L?kHwoOdLhz({r_1omQfKC2 z_77a8X5NhpOn=v}i0>*PBx|xrgTj;3JYceEkI$V3E1UP5Xw)=-w`mW{odzNMO-xOM z@X~K0!P^vrJ&C>`!g%R7lc;Tq-8hNUqe6%5yQadY-(uE?5s|>y&)87QT+Ir6ja$<)Ub>ES!34dMvaomV7lE znv14cMxrbhB1R+smge)#S$0#}|*ahe8i zv9ulFPaB|ADz<{!0idJX)a4xx!3Bj1n~GeGJN%}J z7j_R|?d{`-tkKa#2HTF8q|?;XbQ4MuSqd}%3PKJoG4W7=egl4aO5f=$<-q=$ZHG!UL=hrVj_=+m|e>_mGL*MZQG!7UQ~ zNjK!Nj;1>J{^H|)I6P-+s54*_Tl%XJqcj-N_&lFbmHp~76MnUEihbrXGwk~v0sGty zK`z(_O#@A*gH@?#%nTkkqDZFtoY|mAT#oKW55Cb(eEx+C%Io1ICi2+prUvNXQqQBZ z{6WP}O;#>wP$ULxrCbACc=w$SGagQ^xIZLp6G5F`4nWpuYl=tKc->@xtZ};g1z4Ou z?8S@1+zUC%*OBL$(ifMkJ+~`q!163ry&zScmxhUSOZ~$j3dsYclSs5$wTxO4N07RU|N|qrfmCnyxLHSwrUTYO=zcG`oJ<)#QZd z{K)(5!WQvaS5pw4v$xMaVABScx4o%#)s>J;B*3Q-PbUMDV*m$)!{|ve}iy93iB9G=0Cg1lG zUOx*S8^2ytM`7zEHa!t;tZfG%G&3YL6+wMD{fgyyLmMyy8v#Tij3!_wXR{Wz>&@*&$VGq8*d=?rxTypQN_ijO94!Bs|ea9bgJFNq(C3O|74poMvEX zluR%@4YNW44^dx6YAdEb8&L@ix3?j}vny4bohSe{C`Z@5eM;SHf5+0@Q1o#Q;IZ~z zcNnV`Z^dDRk>Lw9FTco`3j zj|F0<0_TE&Ja?kqQpueJL@Zq&jm==y6jInmD$XY40;Ofa7xvjjEKMGb?PAsNV%G4{ z*w%^9u0I8S5sw~?CaT*=ie*TG3D!2O+gfLMj0xHm5DWsEwEUd7t>I9ma8L zTW1Lv%9$@p2AY=2+CUgLK~aoo(UqBE$YU_LO*oH*YKJ)FjM%{wFq7V5BRh3lZi@dI zgZl061jLG*7KbNb_{dE~Wm;3_{~j_=twDGp&z>h=9is%our}5nPJo#-7yuYBkfgw9 zP|z(^8m7hGllYNXeNUPF8f_{E4H(Td8g;ipNG+oQL8JNhuNYHOs?L9A9bO2=gJ;(7 z@~m?Tt!;BaKQ}4h5&3*Gt?w574x7sXErmFUs&Bx^>jYz{`}bg@!nTyUof=6yCZJNi z{_nz>Okzq<<2Ss%7Jvg4hr?ve1I5N*O##KmxJeFcX*hWUR;#tj`Gg3{iDB~rtX4gcMHn3fDBfJgDLWWjDGn8JmLV!hXILdSS8We1_m5yI_?1UETAk)$ccU&@WoB0yCElG5x^7D zZ3<#?%s`+&8Ix}C3Nh=X5wEc43pss>>&XhjxmeL8Sm=kkT+uwL`hD@$P~8DWRf!4X12n{bdt6M_U2BJ{K^T5h^a;2TbaK*EFlMPf0<5Jl$@GbAuFxMB}# z03n5FA+7aouLEDKLkQ5ZW=AqKy$%EwzoFB=4TR883?m#c7EmuCkpiRCM4~>I#-^G4 zH`vl`rR26zhJ*1Y5`&&kqJHGomg&>B!f*^#0WeC%BI*h(;iogTW_|yNRg}{$;%&F_kIq$0cecAuq^n(3CA{3&&Jc4moh+GyLRhwu)?L)aKeN zx_BPOyS3e3NWIOc`DA;5@-|!epo6wdvq?c02-14si~GUK>l+2B%V3OsQm$z%oKX_( z$PbGE;1E&Y=9E#gPYqm5ce4x|N^3N1uknrG0&uPeFIH1LnZe@kxUSvNtk&`Ten*yS zM;bg)hdhzk4Wf$72sOEmJI`ir%XeU>M~KUIWasI<8(>^oO4_xTa^=+8d&h{hoYZXJ zEz3DiO|%mmaVXw#lC8IgH&^5oSLgM#=!?9}wG54Yf`wHLa{8v=`g-#6%kt`ZniLwB{S=q<`HZZ?KI2ohlr#4pjT`POz4<4fNC}2g z2S*m%TkQR*dKv|HI*d=1Q_j-o+MDE$bz;(*XFGO**lAp>v7usqz zhF{Hp8GX$HFzJ!f0;ummPw%lLx9%Z*=%&*E)EW1zzGJC)^P#)4D0#egUd2vNZ-2%t z1-qUUdU6bp9xdMtHD_A^-P_Kno&ls$oOyxJR0H7MTTIc5+!n+J>&;<6HQB_jrE<-D zxaR%x(`n`2=-UsTEF&WuBI?IE#%7JzEY_+rioYyZy!>QZx%a01gXiFfk&TZJj$g_w zXhEHE9@LB;I5qc;w>QFjuCt!r-L$LPD)r8&3->y6vl#W5i@a5I8aUon{q@TIJa!QyuOH@DF==&wmZ0UUdnsyVAXG7^*)AU-TC3tbFO_Z7Yidx50wrEO zi(HG;Rw9s;(6@T)(MXWWDO7HdKT34lmnY8Z+hkk2J!lBAsvh@;Nr4@anmbXX_(~21 z+%6K_J|X(o%EW@A&E@iBJ4f>tFw|q0#Xo1$J1L~8NG^@h^y~rno|AbonJCF3ycNbf zYsaWn42n@t$D5DsY&an7=~^)Eb?@!G$(vPvgb)wN@J2hJb$o{pxlH3Cc9ZcjyTg~+ z1jQh#Sao|&hvoMa(QWx+R8@;H;;S#Gk^!t-aXy7m&-^b+jplr63131jA=T5wR_ zluhU68ZF>*_|h4xE;G%kvVevViB5`$O!xrCnp%ZdeuDRNl(WZdckW8na%Isqbq93^ z_P*Jh5gzGe=+aVCQVyMFD$I8CUJTq%iSEfBj%pL}d7aHn&pqyawgyVK{^jtq=?t5! z`!YSrNBh=XG~L=MyuxVz7DXpxM9_f{b_6hJbUne3p+DL~>>l3IH}Gr@daia1*Uj)Q z6|ZyHbD}CB@?q^m6#Oq@TZ%2zX1jX1NNg*tRznsau$f!5Yqt{`PiFJESb6tlBOO>* zwO$sYJWg*3v8bL2k>%Oie{9L#5vba?ym~fz5Z1%H(%l%;o4NrjK&#<6zwpt5sa+Lu z{Q z4s^ZlB^fn8z(zLC%FGlfYB}e~j33xurfd8`)k><)YVkcvT6-7KGBZ+OFZt=T%oNYK z)SLdE87qW$EcT&BmbyLt#car$C$H~Xl#VHAXrk>uQDxapq1~^l8fKj|g)?Z){KRl4 zcPB8%@U8c8<(St9w909z;#7PKL4ZfgSeG>oE!fyVNs<^fGo>D-A=x>@eq?awQW9wRG+h7HA(Pdg?)qxJAu zv8!)aQIDiOC}Eucp!zP|exz#FqJ6x^Fb^<#uP+P-@J3~bH0gPJm}H6mO@frwk#N@) z1t3RKISx5*1!HL`OzwC?_lx|8E3(;s>$>B7Z=W43Cj=PQYXRVD*JSGDiqJaD(p|aL zi5Dqa_p)t<8S1(waw2%l)w)om5=yF;?J9e64g|{v3Y|f3MVZKZ-`sw;SEWRUa2_+= zyO2q>cUE)B-nmEvH6Z*;^=oGJ&f}oZDwJh$=4@1i4Jsb>@Auf3*Q@~}rVFEM&J9MT z^dH8<1LiccbZn44C>vdB(;aRWk4phn!;M0FYM(C?a2}tQ*Ib8Ja&uIquaUzqrA)lx zI#wk^O{HKz zugdS+FT$+UZyPUptcjw|&Zy_O2PrOTQS_GPF3g(*m(aTC_2Jbl=N=*x=fEs|PPrwR zwK?~GUU^&R1XEtc0>i4RH$5C3nJn^sRBv)k-wax|@kqA6?|=M;BPPOkV#<*UGFR{k zLz%${>eEuaWVLjX5k=9&imd%Qu7N$zkLukHt|}hZWc`}vc|6fJ15$S|gHcyQiTa-O z;i*@~V`k-8wyW5QXhT;5!lXu5h#NlQh8%oGSa^7oPpwbYcW+(a zokq#T5z;TU0>?*JrRauw=Ej@X(=vH%6-sonDrqRO6xCylMsOd(Se{R=s!N@2Hdz*o z28NP7o#16iS$$iUzr5Yv9!?b65M}&6ChcL~f;u8@wRCMNL%t8-7`b5gaV5sBqDE>eUaJ`|d2MA4R-nUd;|%&wrT5yYvDNhd^|p z$ryEZro^~oKGIb)A<8Jfzv8`SuYTt;z8N!7Pecy7qe^YNe<6D3`^OxzpEhh;N+j}K zncsK$6Cw2|Ri>9bR`}prWO)~Epqc-LSqVx(&3zyTCX&p4bu=#dN8=#3dhmOD0I;(`jLkLqw?ytQ?ourj<(4Y@K^ft*trwBUm3*+LwF^ za|zU~8W_pgt9sn3)VffPqn@6x&z+_T+&%7W>b7piBHG}BY!9R!l1MsEb|-zf`&npn zThyLWA&FHCiL9ZRS3N(zW824_PBAh?A$M1h3i+`#irbfY>nFw;HoK$p+BpO-Syt9` zD)mZ{YtsxGxMG$&Fr`B$qF>g`==Jc|aBhm&RJJbJa<9|8-uYPMQax;4om;54QmowsJ!r^3~sTdZuIR2v{sR%5<`N7N&p_}YTtO`V>PapSvmGvx;cVg<^0 zHwnHxq}8j+ZI}~$lTMQ4zO*DdU?E-Q=4bBuoN*P!l*-JjNbdh5?j3_Ad$RrS?t?zI zZQHid$F^aVPr z(=JZ_agXS-NtZ!E#Ca`OyP;yobkA&zi;{LW#?dMftGA>FfkGZTl1va)Y zV6LATafza*%getWFtSD|i??rFkofknQKo#9EZOzr${M0nq2 zqkhcHs^+*%ncpasH2385Li(<`Xky5Nwf+&++Sdwa#|Ltia2OMUe!Uj%_{+5m=d`i? zTQ?p=h7bPo$=wJ>?hf?xcR&=(hh4gu$l(h<#)ZgsW0H{k^#yig@}-SjzBgx0wo7Lz z5e;T5iB~BkF36h8HQCK+Zh~R4$HwdRh-M9+9jLsJO9dbaf!huge`h z8Bs0>z3Z$YM??P%%r=+R9#XG4mm#qdW90yz&8>|C?ORt|#BJcbQ%83@l&ns$rdiP( z4yunSH(u%Wo##P{B4+rS}9uw)BX&2pIRkwQS$&oMd; z%1aE`(jMAk(ifSp1l%fcEY>kM`Cz`KJ(G1O?@I&2qNI&Jv zQ0CsfX@wXT(std*fS6eB1#YZ@ophuh|FG*;t!u@%)Ml{I7&9dQuo>h4b&W7wQ!1L4 z_0J-V?^yEF-n3A;t1fJ|il({e*d5JDac8x7We=3p$7Gv|v)!U59$(;{gImNf4nK$? z0VkO}Z4EEr<$+@?ObR&{A0jOIGAnlhC_c`((<=g(#OR^j((J-mn`!kWqY$hz*Re?rhbXjsQd9xXS^num zKAjw<^L-7Ld;{I8!(h@+GKu~r`>}a|`2GTqUQsfD-ixXGfYFrTs|Zss6`$Bzuu5ZfLi1 zE)BoxjMVl0b@~fv8X_8QYMQmt8X?iS<9}W@2J^quCFdDZvZ6CxdhirK5=UI{kFSBH zCyj_Tw(DTtqHyL^POsF9BqR9pp_e~q$DW!ZGp?kGSsb!8w>!d=YqjBP3(;iY{3jDS0 zW7}|yk>A_zMyUO(-rx>6c;4Fa)qFy5K2w$X$H=eHmOT9%XN+-Srv~~Y+ed%@r8XK2 zi02=UQ}WfiQFiu#z9o>4S#gpkLFHbuu90w)_APDCU*=;JQ`LIY~2X=GI4$L4)qWFdw zjCP9Bxh)wLtN<@P{9PDT`EIr2`z8f-Dbag1XzrU#n0=J*Jqh(ZjcYJ!Hof!^zCC)T z(^i$K{HdTfA!BlNS~MpdSnV8V+9PZ?A2yvUl0SBChi%H!=#5@8yF7}NGfUh~MWbL?Vg`6^CURlql4T{ zcZbOxZ}&WI>yEL`I9%^nWfPqarxb~|UQfN@;@)p#={&aw{U<`3Dl69&!JSWgUafW; zOC>7}(ia}RmiH8wDlfBJl`tO%Or6c=h6F*gi|seHD;C$$(Hb`Rw?2<$mu&BYPTsO^ zpASh7@4o(3P1dDFANO&WTu<-HXU-eH^FAM*q){A_+jW9R-#=9JJ_lgNJ`bF|$F<$w zDj}+x4z5SBU(bzkKJR%zoY#~;=B(wfdAROwv^lu#hL?EEDmUDhH#3~yyD#5++)szA zK1Pw6K_0w6L6i}h`#Y~^Ea==%?&f=~+f=8#?caIidYP_$bd_R1XB4?CVpSiGJmv>~a> zSUVptt+6KBJ}R|w*so0wZ?m4(Ceo?j9)Q=qpVkwzUi>jX->+|tL^|%Dj4z(w2Tng7 z)?qe1ds{C*_M`UXJEc3H3kVK3oi7u+W2Y~lKJDB}TJMXrN8Vg#-$Fm1cKu3D4baBeq@7OJA=d<(Ws9VR0SsIP zw{5n4au^9pu0`M{HAP0yqm;1+D&>U{vRyJprv~%Fwhv%sPm?G-H*CSz7cjU(U+{_A;%edEzzH>_eOth_*ADHLW3fl z-xyjIu-zuoqK+DiHPcQ@lYG|ql--7+-wF9^gpbX~RuWn_Bj%9qBQB%w3G7*xZ^~q! zB+%HD*@RiAO|};9!zsroo;J5x)E@3CoCyUYol)6@`g@H~E%4BM8U=90K(;THY2~y= zt*fAY#lcFM2;(d|0c`Yuzo!wCR>UEryka)h zeV0qyaoaQbg^Vt4#h<{hbBs)9@{4oT(gL$x+%$8HzMJLE*iYe1jXIyn=2rbr%;T1* zUw6H~TwKzukCa_}C$GYZW1FYLm?PXnryHWS#~>`w?|; zipo>?Og$iN;uU=zpGIQ4)@kzLx657dOc67iHVdFoi4*`=Q5^VbmQ|KW%dPGHaR7G? zyaG{)J#DpMHSpxRa;WQ(^S~cLGoux7-MPz+3^P z-?|W0EpXbkRN|<2Hq%18QMMj_&3(=t@o4_2@4@T-ZP$Or|8xHQvon@|N#lPfYO?-y zqNbpsy}q5Pjf1t_-%CzN=~^1%(g;{P+L;>uX|m8Yw#WU-x#Y9gH?(rVWnp5VW}{`M zX9B1BGxARpJtH$UEgJ*NSChPhp`{Wo9VQ++-wV+%uE+P}~G_i7nb%uI~bEUZjlRo$ofgX`a# z(c;Hnn%KbHyd)`}#T?X z^3OWba&v>zDC4Tr;;J*#(ta)JpMpBHnqQU*D*vtXpT_-*2*ZEMV5F!2x8i?awO_u3 zAR&V;y}+?n1#D;SK_im*pw$OwxP9~~Qgve^cQ9`r64$4?fso%8K2tYm4j*RXq_aC& zm!J_Kzl57HksCm{qB0d60S+x!6po$fWleos9i>?78G#fRzrCmz+V3DaSySINDZh+- z2{(UL(``mrr=t7V2}sx~PWyogeKQjrnq|a41t;!D;N8r-4n5h?% zI&%13h}&BHe}_BW|5ph7)31LV`W1eE;d&vfuT0QCp8BVQsYL%P-VJoWqV6jk>~Yoq z9%XW04pOjIv@-pZJ^B@j|4{z@?@;_-1&`?dlz^rG6Z!us{gjrG@!v}KoO`&sU@SUb z0*l4~JqjU`$>xj7=97Mhr&R$HOA0jn0RaY!s`De9V6_X9S&xKQ&oqIxUY0N!B&@TkBAwhWU3kJAp;0cLxynr}zHPxPa@{R&qZ!a9F^<*sgJAQK z-T7iHmE$qfed3bRxeE;}6qeu9#zeRsLM!m3119UmF+k8g``*i{fKkiK;P7$#<=ln~ zBRojLr0sli`{mZgGIYPj!$SBOQsWba)>WMnXR_`3WF7((>d9wYl2bGCC-;Ld0DrG* zKnu3mRqDuHRn{9M|Li!Z*ed)q?Pt&C+ilU-2j$f)Q8wR==h8T+=IH@Je9G zsu{5CC-R={o_+sJMKm4~|AXxsPbcEI_o4j)$@UAOMyHZ$*Z?g$C|gu-GfJq?COl6d zA@3&>A|bjuPuWx%XsE0@!dg6l(^Lqv<*m~q0J_NempGbvBXst3VO372b}LCzNLGI> z!8LRO-=(nev`E14ft4dc=$G;<#gFe!+3?~y!BcE^QV!Uo4x_#;eeiQo`mX`aY3OHy zJxBv05o)}I0gNzPh1|Z}(2R9q*^Q!pg7~t#Yv9hnp|6LphtuyV9rwKJ7wsgimeY6Q zbqRYtyOl?Fi0dz(76Pp3Q?^D}L0J&M0e1^jn9%8VrXR+-fL_O|Bi&lyMe%FthrTi7 zHm#665Xo9suwA=Klfl~Fu!*miz%SarM|&T>tu(BcHZsn8-m?0)%UkzTTR(0)DP~W@ zU38+mzfW^}PSO#&?YCC(tT{VR2FI$HxaST=rfInz+y}q9=RoDio39D&6dq(1G~`xN zm4*{h>4}-g=NBkCG@ z9xTAIqHaJ<3x~()sBP!1%DwAts#`yMGka}6r*Y0W|1DZ2iu6k!%Q(GgMdKW0#mcFD z%C_NF=e1o%+p1H!^oW(Bedaym4tz>k2U`0UF+$|0@hUeVmuA(tv>1{lOZo(VdfBnX zBFU6NbdcJo%#Jxb#r5_z?Q44qLuMJE%X*1U6oB$rl`M0e zX($t^!eY^F8P_JFoA$XEvVw}2PZ zY3kR3S~D)Ze($;&SHJTcSMOT51D!dvdx}hoz5DP&Cz{u5bfoy0%1Kn0+s6S<%=6#xePtftF`1^6;doLwA21_^XC?WR{H7 z;wUkVNz#Xm8?gKLCAifivq^+5`*n!?B9=DZ8|vXk4y<(;@hTlgX4h`f(~vD-Tt(yR zONU4MC5YeF&XbBfR5bQt-DoKFhT`Am?xf-S+@9rnIz!YT-dWpy=Y--k|=ERbYJ zSd0K~DMo61v)6}FrQwmLewu2~pmjXdn0hPS)8wH;@lp6C9|M8CmiaX-MoV=PSYP_Eh>QN_?Ec?uC_bJx)K5?7KTwGx}7uw20S?rVl!>XD<>LlU4SY; zlNqKOp+zLrAIGT-1jf7wIfiySaiC1-yz*;BreSS_?8nGfzcMmVyP@I{;t_B~rU2Rs zZrH4SfW(M@6P`Eng;A#Co-aX~l7uYc@vht>^R2ua;hww7$2@GH&2YywqX?{c8;g>7 z17x25zv$HQT-MO;b6ILYw398vMRJdNYFo&LmYdz}D{_8DuX z>v3Oz<7TsivlM>vwe-nt#JrjQ86)5{{UN#U>L78~g*Ecbkx+KJ&vYHaO72yzm3GHJ z{F!jGUr58!i7;ZEG&30HNLTE|3)-jS$MfUxKESv z3Gvwu%k{ZO@^N@dRpw4~n4X-IrSN3#@V@AXV+QXj;(@kMuH?Yd^b2w(1i+4Xu0d;v zVlkUMGa%}n$8TYSM3zD3D153DL~QE!S@_fxU`CLkI6vbw3*8jUG>Mzby?gGqyAsmQ zt5WP!Cd=6l&-WkxjUNJ>gkH!{cxW$W5~9!ym_g*hszJI*x}2v_yP#&@tvGU|b{k=3 z$ciZy-k5cOPpWv|ap$Di)p!?4-Kkzsk^R=6h9 zp#j!Ine&%{k~x?0mx-4tli7s0Z_M@rKZ%#A;y-5(q|W=!7?$=e(|2R^=v#K+F<`s6 z9~6TW8>4KAk}&+bVSs|*@YPr6AbxY4T1mo zA+%f(|h_bBPG{xa{jRlVAY3l5zdic9D$Ocbt; z!E98f;z30Qgm(96bphx3hf~B6{aZCAE)HV*0XhH?t9E>D!U1dZA8-RO$QH1bz-?+k zJ-*WBME>yS2~SgIwMX~!Fd<&`PiG_Z1tL0xU`tqlXZfPvu$Xv*jYG_ft-$84;OD~G zn7>LzEtE;GgS7DCO@^Hf~(CLUE3lalY5h8$)ym+Gok;_9dXPQjN$j4BWfuhw}J6h5;-&)YwCHE)iTSF_1nxlJlq-3KO>?d|M zqtALdf+N_D!-HOVA4V$VYJq%6Pse=FEL}S+H5{TFR zZm!j|d>QiNT8M=cqI`7QCuPGZX*v6AKcG&s=aCiUVAKyx+Hs~KuHA3kR;VC&Zs^TL zMb2f{PG3J~3{p6!fxgvC9i5Pw$PC@FzZ*1~*lpGw#a@Q73qxPd7Jtgtt+gFqgdD4+ zq-E9!UVyiGo;jR?Enmmy=5fuw1{ryuyCf;%)ZB_h-!=eV$oJMDsLULzJBPyf2c@(1I9Pc|5!d3DIvUah+cY zUF>AOU)_J}7+(3?0e*RWu#fp<{fn#dZd(JozS{x17RtAxURn_3eOes`fNS4lxbC9D z1y3{76_J1^HqeTtR%$#fT3G-m`6WAkb{vFV5AHb@Hda@`IyB+?Eh*ZoJKy1I;x?}^ zXvrZF_7-77{dkcFhldEP=ZW5(b*cIZ4vrRyLyl6Npg#Qf>*Kt0<8*_8z~ikQ3vJ72 zjZR|)cN>?+HQEjlE(T1*cL-OPmzetmardnIEnk1+cFW6%w~N&8M8^Xuz4##=03RV) zUU%v3ur*Y32j-3~1-3~WysY{bTEw<*l2JQT&m+zDdPc6x?*ry~U=NNAk~;7=u`eVC z*W=ID>dZKDEXO43uVaUIhtzNH4Bl5Ftmm9L@0CL@1*5snJ8Zlk#4Jx^x(?{+pcU!G zjd1GiywSPq0FlbtK=sE1hP2WsCrFS1>PRf;@Qwml-$RYi$?OYb=|QUIk3AQ!rkEKC zL@BIg?kSs%)+`C36O)uIj*}L|&Zq4;9;NCo)6d|t%&{`iG1AYhxf?}Bq1-_4 zd$6ljZU)TppX>CJ9ZX)XrcN3KcYDK1ab&a~R8!Ar_-u+lt7OrIluLwDceaZ1DR zVzQn}BbrCU{ffLg@AIP4fmW35#MYW^z+yZ%@AJTcMDIg07Qn%p2*)6oG;=&;6q#o@ ze1Tqu3SN#S=9|d`zPX4mV-X}tfOu&Kfm!{n5MwV`<*)D~p_!61g=LsJuX*7J^=iIf5;89UkLO_}9R**k%Z3MlGJ_ zVy`8M)PcF~k7mzvy8G(jetHIXm5CJS3pP88%cbdFcemgN2dPPBt~HOVzMdR`i+GT$ zNY_GdgIwdQuB9#;Hwxo5>r=`B^woXsu!;kF%m7CR0&&`uXXi{g1{-3JV zE-sy*>%s3JO2Kf|`4>Frumvt74;!n7{oiSei?yw_2fMe4loC%ZidjDEjvZOX}9ULKh&KUJFCF3il-NHtm&tf;M?(zMB}KJZQD zphaQvcP1Su%{}ivH8xo7w(yCmb6gD)&kV4Tq=SIHsmlh_+M3&AoSax7b*w#yzO6RM z%`{9ruzx-*!Gg3i^5o*E7v_Aur^69elH6B6D_cdxoCF>7h_@Jcy~EMTinJ52 zaB5+eZ7N*)38K}~Kf4Sl3Xs23YKy0>*1q=B(n3=cL!-4(&U2mPHTG&;=d*!ty(f#J zWjcGRs;mMd^6aF8OA-jr-EHA`qhc;tJE;ra~QUwg7ng8y^2m7ZNBlm*4JL*nFr?*;K>6&lh1JoM!feQxanvB<>l9|C_ z2b>4|Lxn#t85t%?fEP!LQ?9_Qe`7G-Mo>`D!m*}$Nwwyz`!!fj`|1y<-kMeDLDQT~ znMrDgy{V~l-H^uhR_Cf&_XRS(tvdtiysvYRUn1|K@v!Ld1$Qi0mzI7kURTWd+Uottf=ydyHlZP% zhZ5mGYBbit#F~%Vz&-#dx`wKZKTY^Hl;f#}#*Ig~%4`DA;?yfkaJSm3k3+v%Z(Zwc zZ6Pm@F*0dUkig(v5_9EQX||ee`ZPQPGuR5fQ}*5qGM!=g@<59i!0BM}i@Qx~_?nNT z5p+|EfU(sw1`_MLuHg`VM9Q)!s_PeL9qY2{v^Ny%=q4ZufD?saPx56T!s-vQSag2D z(1Sp)U=gr8=1aIUrABMBdbeK-)&-JoRlr%WH5M58z>z^)M@TPlyKa4(c`Ij|7^`Pz zK{tboCHhY~98S@ISpg`n>db&G5CKft?g3sc!}>j&wWpir5>{;+HEKy2n=gti7Da+Q zD?{12zmBNZ`|KD&=Y1!wOTE8gJs-DcXMucHn5eHV!iBD;Z-5-8t6%QApWgknNYC;{ z$nrt-nasFabbL#EnHFwE%SxZf&chA9Xs~UaDn9a>!(Z!FVh!X5J!S@s#=K_%(|;Ot zxP|1DX5J74#rTNoOqDj?g<6pk3p%jDhXO~X$m zyywA05-psxKV65b3>}V4HJmpkHvz^^JbR-8J&LX#q?S0?d&5c-U9^9#r;4pzIlN18 zBiKY)3;>C7(uaXgWyI=A4(TqO>1Qf0n~ebMN=_zSuvfx(rs@hFR1ij24CY!&DIL6f z;~^#|jGVDet7ib7^+Z>CRLys_Mkq!Uz^4PR#DP%Y<~ z5Tr3OyHV|LlD%a!rOeSG`~DvTfrig=!W{|yIyC!wH$+X;z0gg7$$Eq|G{U;lyi_CM zw%xd;GE*e(71;50WW)i=QHVW{*ePj2i|l{1 zB0@=4?nc+~UAak%29rjE$+Bi*91SA|UHF-C&|LxQ@j5C$&7axt=*#-g0GK z+AajL^n_tODq1p`oK(l#2rsCN!yt?bzejIR>fJRpm@26ChU9hAbb)i6zCeY&ZBtI3q@|%7>7D zT_f%nDjP7PC5O&nx38qTizcNA`p~+CbCn z@o~#cBO(tQl=7Mym#BqO0f8xw2`d5^PM~KH#APPPB0!J=F7UP>6E~ZLI6mDICzBT% z{mfxgj;t6jwDanL(3h+m(RVbepQbfItECiy9JyRc7LCP{iM^jnX(La&6M+5n1JLzf*< z3}M7tsa6DqP=)G{<#$Gv z#p6@*p*~@2%4}?1Xf9xQX`lk>`R_4e$U@OD<8xw>37kT1^%YN9N=(VIXvlt= zmPIk!EL`P|$3w805K9O@kc4`5Hspc!2L!L8WHnCh1@ms1QqW(Zo?vfbw-S zc(P_eU*VJzJ*&*94sC}*NQ7Q=d@xeVI904VfgqLvEa8duj)^-nu#;W>oZQ2`scx41DRi;e)q23NO+7r31Ck% zM4Ks-3$$P~tSY3!OqB{MC+jRJ|7ENI?M{1M?1DCnZ){H~C}$eZqjNJ8nyWvt10l07 zRImR%ZW|!A$gCU4bg$;Z%tq*v>XZtsvQOC*gtCSXrQdLTAGe}{HMeaLDVMS>QtBE= zCnhXl*DO=#-m9V5PK7^w;~NZd!B|dO%N9%Z59Z(0JS^F4zV^YPG0?;%@ zncvJJcIAs`c~3fHakr?mh-mW3Dg(j^SV+b|H7GcxuqP&rO**yk>_etOev0j)L0D4E zlC8J)8*@GUj99ps9#M{}dEr6?y-ht4?A4;RBgKh@#+t}28Y%WZ;^;*B5{tQJ!7M3w zKaCv4x@A`!x)0>26a?spXv~CvJ@&V0^yNa1yUqe({meIqFbtzMfmfXnu60mLbv7~_ zmF*Nk&_9Its-jArEc9#;Oq)cpV-`|OC!2fwy<;-V=3-=Px|h-q6uurdY{w`2dnd;$e=kmkX%S(Fq;c^azeo}&XTEH1LXQl%ang`6)~_gcbd+a=J-0QO`6Su&uR8_XhQ~6I!w05?U9-=KD7xe^@sc|3SP~qb^_U{V z8j!t#`0VGyJzn1J#g(oEA- zLZ9a;h|O6C;fOu6b>z~L2`HGA)WKDk%^(Go!D;3I&!cJ@*KYpSk)^E4F9f4iYCwenL3PW7 z32y4qkUEun4xf%-NrxfqnJAteGeF=p-mmb&k`KZm9})5s4*~!yTTO-*0ZY57{gGNB z=OpLl7mj>nQnw|i`ZH}mqjF~mNs>8DWlV)C)F*hz*e+lc0g#2t0BhpgRe>TkO(I_6 z?~t+zNb8haIHD-gsy#Ho!IpC*tyJ75%Kjg@Ya; zm5-9chuX$bJ?zD1CVr6VB8xY6`p zBJ9c!d*wibKYA)H%HyEBtZ`|wv>HSb+aiBbDgNAVBPw5jx9C>T3UPnK6UWE{8yU$D z7eDQ>^8+UaRWcR_*~u&}>jLp$78hC7ko6Z!4=ucb-AUxl-no9MHH~VZCIs{{w#}q> zd56S;C_x~zOO>&02wShxrz(#emHmznPqgivNtMq;s3A+Uz0(C2$;J{N{zH5_J1bXj z&Z0Xu7l~7B(ycruu+;QYE@{z?)P4kTlJWQ6o?iHH011|inPeg)0_^~RAouAteayv7 zTkuwygnp{>w6= zj81)Kv|p$+Eu<$}GC214Glkknp`?2XWqKOJlV~_ky#Ya(+fO3U&fwIrUUhI*r3kVb z_ErN;QiOcz)C`tbaTqwJy^w?zqgs-3vKBLj4aY?k@)ep!>G$YVZ*LK^nr!##A!-k! zt(VZfBlr64KBkLWYY4BaAX1Bi~TkG+xln?O^9f|qsathb^n?(P+yry-axv|D*~LYw1ihwJj?sk^EZ zRa7{c`|}sivge-O*)tD&)P(2e_0)W`x0l+syKDA7!DPqVrR99^#M^0}Gnbc=bN%UT zmdis~Rp;vf#cQYR8dk@FYs=MB(fs)L-Wv&br^xgC{?6DJJ!8tp)O{K$9u@Z>+Ky9y zk&nv3l;`M2DaKHt&NqzD;-*Js_K!33MQFl%U!R-F+)o2G_gk^is!b2h`!0R)-to>3 zE3DbTjpr?njn66w5&lo7N6qEVw+otIX0Hb`pU-ZO#+}y<5WyYqiOiqbqU zJ?=gtdYd2nYp*4yeZC)Tp6}fk=JC92E^57XUk#C@C2zjVOm_bCFx<+F4&gqP_P%wF z8mV;pkbTI!eE-HJ-f<^^C6dw0q5U?}+W8p#7&sNVtd)kY^LfkQ{`s^r?kp)(={(Jo zCUL^kUn%Kfb8#-ciSv={%;~T=TII1z5f?mwjF7g_W!-T(AAI!*g)glCMB_g$7m)V2 zOCtfKM%jkNkb@~sfStOXA);6p5JwsXUmP$n4Y@PL$BHoIW3)}GU8I<9a!$;%oXv- zW>2VbJ%G<*T&e*d1VU0~Bbx3f+oM!KY*dCdh+Vmjz7lQ;t*IKqBcFXV6j%vxw|qV& z4;E_7!C2rZmBNH`Q$2zJ}+p?oj z?|V=fh&jq?TfZ05;06eRCzn>@{WF^#Jx*AYfp~i3`hlR5(Va*A?!dmc&k*&uk)6`xnn%}Fk|h zMDU5b6z4XfD~0=QdVfXo{;?o^Gy48R`zX2mucUh!{^%V2L3h8<**{?BmnO#-Wck-{ zvZ$ejlc9sDzOIy^qahW)wS~bKO8GZVneGeF{RgPb_J>FQC!$REg&qF`PGV^w#aXuPB2T?SOf{LD%kq?|NHhC6Zw^|tcI_TC? z0GC^(P!e*Lb$1lo>d+O2val?-d%DFxd1`XEIA0*=hg(yvqs-2>LWM4+d}j|vSdvS@ z*4E13eZNN>(oslMX2Zm5slP5jcM&95nj@X8Os-}>pFX8gT_LjJvDhilE-U$&k0YtT z1uNd{5S7RI%6^`J$T_yZSz$SaPhkz3YEUP@hlU+RB%H_eeJ7I+CC|kqBUq`pldKaN z(a!~x6?fowE{i_9IJt%!#$JVImyhA#Q;UtSK!6&%5VpBUWsa^BYk1J(iWs0Np=^8% z(fG>Ds0Fou{cVdJ6%+m%0BJNcCTD(mxRUKT!Gqg;f7lO#ff$>c8!z z|3O#(&H4Yf-Tp<$?7xJ2SpQqx`;VH^-y-nu&jxhh|Jy9qFBRMWQ$#Vcd?EU{Ur@iH zt|hojhGP7%B_(TJ^Y&;_2l>Ekv6z=dk%k;8~opJ9jq z;^>RUjzpGOwv~*ddO&^2>d6H1?5kM=JT(!<9TX*&e-Jlq1=I^Pg0wmcMD*7mfv4PO z@E7(@w^s6Nrd;b)_BZRG#JAEuN#*(fFjpJSNfoS)`LzR*La7o`El$A8T&;PQHj>5( zY}h;R+9*EbOCYup3W9=E{2K*_Q!2QG0N+R4*saQ!ZPv^(lxZR`M|tI!zB%V$${T?6 zS*vDS7Dy(BwLi&&w~9JXjQ8Q-mhb4s=eFiJI;#t;y}%Id zMQ_BA(Y?Qlj(-u5{ZHRFj4bs39vu=^1ay#skKDjCQ{O%lCU*H~!}mT`OXLPqQMHbm zg=7to3`@z$6YZ#+mrL~2ZU(V~Iu~_zPY0|LCq)k0bkBj$-qZE0VU$X{j!Vk4;r{OFri zK3xLS@+Hc%3xdl{-#@zVlhD2VITHuX$P+lMYZ;C%!Jpi_nvO>Rf1}rEfvJaa5w`RZ zpSFWoJbU%BBD75Mz7M)vmF+-X!fDrT#7u5;Y@tv$F4{i@I(JP{ym-6?b_}Ku#G8>P zE4Yzj4LDBGpQx4@%I{%aU9k=t*#j0*lj$ zqJwForG+D}jytzQ)<#p=8r;PJnCW%=tN8dA{k#7Z9}M*WrY81NRwAk&7jo4;OygVV z+cqawp8@8gys<@c(sX*jcn(v2#|H(bygPdkBoJuD+@ z%|pV|34E(EaP!M`N)(=AkjY>^iMTjhV$U*5@+MBbr2gaBAsEdj3FqjBURxi=cLuRdiBidqh# z9%-$Yhn*1fBAHa9pD1gVEVsvT&vmIUPu2A`uuf%~lm5+NkK#cf2J@l!p*t2=njFwfO!w&|Y)i1be1sn-KauKqPvYBnb^-tH2*d3R%S<_KP_>$Zfr6aI4@JhJCeapSz zt%S#H>wAOuyK35BX>Bt6A2ZsR82?RcQ}O??_tpVbZe8L(-6bjAod*tZNNH)1l5P~F z8>G8C1W73gr5ou6r5ouM5dje-e+O=i+{=51d1v1HoB7W8hdld<{p@T#O^)$4i zq`%LY2;#q33elBm(G#XiNFJQR zZup#0GQ}!NfP}c)M~z@Mw6}FG=aBe9Q-YmzI`_~m1Lkwf5N0nMs4_BZC`XdgJa@Za zIaIF3y-!27S$G>fPv83RnVZZArSsN9^1g(Mojf4ZB(Cr?_r;xji+UG^-iO<1&Bq(q zmdA-~n3yzJrM<7`kvqNgydZ}~H%V^$Y4~X^-e3JRfMd(u_-Rm)DF%O;jZ^?ZvyL^cG<2kT-wZw_hD#`{CIp88?|wm|>Ir z(G%*`(a8ht4~n8zd<=;_HS>mHde8bNO{Qx%SR%`vYeO<3e968#{;;bvrxyb6pi5Fl zLHaNn^xhG{uy0758y`xj;yw(z2GgVWLPy1R0JY6@*-8&)JB=lbN;S0j0T-&135hm>kC#$)LYX~H zKL~^_=V~cp*i=0Az<_U0DGS#c3Zy995QCZNxh?(m3A8y)CQ)ZTlhb;ENoKLNcGfBb;Vh9E@1Mb2>wUWA&$^Ak4bqk=pSZpXp6vo5%2Us@yAPHbLb zTW?O0PT*H~Xa_qKhU3Un8@4D_x;RGUu4}z}3ys864o$#vVsP#%s5_Za^mxysRawc% z;*7aC*6>mWPe5Vvn`@A1R3San#4B0TA3=Mt@8kenW0y@YCoC!xw^JL17_EiEKwL1;Uaz&X7_WWsRO?ZKD2Rg#N ztC9R94*Kktkqo?2Xzri|gO&K3BUw0hl?|DgRXQvK5vm$|7p7 z7$Y-qpZ;KenJNTW8NqEU3OB#$ICE)pAJK1~ z>$3_6hNeO#fxYzW3ZSkT+ZX?{ijIjXYMRt=$*plGEdP|9FE#pp5+Y9vgJVmqDGCK+ zXnsU=5#%oWEcUYTb_!G#A2nj`*XcWKv%-)ZpwoG@t_m@DwK(>4pZIROEaL4!JT-c5 z+_VwLbFi^jcpmScSA_aXhmnf5a-|9LOGGmhZnEx&lGp_qp7INe%|pPRFIB1@3-Svb zy(oQ+>CZdQ9#v1WeycgwsqNT3-!UqyX<~hc9A{m~cPWUALVA?)ytQeu-DGKDAN`rd zT*GZ+=O;wpgLkszjAGy(H z!v6@6c#_Lnxh4;}Gh~*E!1gvonJStZb5uG5p}fOKnMwV7Fq2ysG_%bE4Los+iik#| z1IKDfc%-g;5|9myV8|?P(PhP+piK^?%_3&lTZd3FEfk_H1ftrG=5OdFB>pG2wc8!Z z6Z=h0pcCD1<x_2s8{b*L?o3-FvA2@X3)Jmh9s%Q$eE5$c@2OG^^y)m zr(I2Gp?MbZl^E|*l50#2qNVR^uaDdKlRPX?$M%cJIgfGTI;U}xq+rp3a>c13os{#|M3Y-G%T zJaa{9&Ft(QL`@Csu7XU+8vJ_9e#L*U*zzwSC%`}n003i4FqdQoGg|FUQBHPuakhz$+65c-puczu*u2)fo8ez__p22NgyfnZz8 zPsU5&e}Odxct>jrfa`{IDINttg9aeLxHTI-u))-4`h5h2P2C_ktd%xVzfC8#j z$|e9t2}U*qFN*CGcegVPvvw2BM{}#(wsB;CTl5W)6t&n88BR3Wel)^dO}Ef?c@F1^ z^8Fee*v6<{MoFG;;RwE%SvfDLMBY#xt}r|`8Y^F{p7N=4n#!*)eQnWa#5R0?<&vFY0Z2r8H1_u7Ze%8PQqJ*#N3BBtY@SqeEL$b}S zy72kb#ieGPWTVCuin=(AZ*pxSvpS6mCaa~)GvJ=(vv_+tAoirB-$3T#u@y}uV|{Bp z%8{)Y=`52O%5HqIBbRF5N8pNpopy^GgZ*IC^OsM8;@-1L`iOUT!uRT^f$Z$HoW!Cm z6CIu=ON&QUfMolbCL|x0X%uD;>|`A^?~e-Z%z2&<1A}j?w*M9vmQDVsjlOMDdL#VkU16~*1ww)=&)OUfWLV z4MViW;zOMu_WC`nB=XJNoLnA=awa!ElqO~fVl1p>Z$MEMH_K1tp`8>U_5dr6AxP88SQIhLM@`_d)E7p4#C%__Ue!Q9RA1HFVhATZ{WnnT1)t zJZl7swhndP^wz)zP7=r7l4LD$QMAIat>|rChLS)WzM~4?a*A-bpo>-7>~2JPgU@n1 zds3QuG^w?UhKzQzPyv)vtpWX&hrET2~)0 zeh3}*Q=Se_8BYWfP-Vz>bL2kQLEm|-Pj=h5_=(v5YIg~A@@B(;**yro5Yp!j>pgqeA$oQ>E{p9~lU0yaP^h>XsA%t>;BPTSAKpv5{M@EU z?1kD+5X-kUC!T;jgx=5~EF;^LvuqyE;-D37u|c?1UK&Pij3=kO#iVx7+;CJxx3n0z z-ghc2=*EYn`au}UTL=ZZ#Z4~;9igDQ>Xh^z`u1f>eXzO${)k9C6@s&DLCBMkq_Tpf zu6(sr^n}f2SU|Z(t9^MFgVz3&z>CfIN3U$&A6nP-lQ(F;nHzU3y76RIKG!}1}G%G#j9}=7ZGI$Ddlc8ud2Z+#}or<@dc+aBFK6 znX};os;jJEzLT4(y%>q2Wm|nE6-g?*!uRa9TK_az{}smJ0Nj{p zbz+6>t|r>u=dh$x3_=JwN1%lL){9F{4o&ONtCczkA5cu?RK-o=iBwq_MJD1|&5g

<^o+rK2Bh0fq*h~oakeAWpaF>O*!x_ zIMJP9q18<&oKz@}y%Rd_n|AMLAt&u3N8zSg9^pGAHz;UtBEZG;%JM#HH}LOGH0jfl zIb{5*juH-{%zTo7DeL>`E$T`4mzM+NvLiK(GH9Ckl+-!`qDaalTraS>8+Zq3(k$4l zY!QOAw;~B+rK~^06NHEkq&T1(OEG=Iaem&sZ1pWnnuZH5x2>|t@kuD*9QLkUk|wJ+ ztbQAM*vf`FHf?HD>al|0-Wj>tS$lb7OxTyVdYFS)gX?3>E=Wf;bB!J~q^R}YaJRMo zUB#T6CM)qsKJX{=>zLEmxID(3G8lBUR@GiPSXH%g@9g(`<(#m5cZ()!wH z!acC`GO9LeV}-9fNK_|NU(zp;%zC$99|={qq76_EsEUuVjK@V~MzZ#)7?LfHT51eY zk5r*}L-gT+EKL+~SDLj5`+ZvlmUj{<(Ka z)rlDdyH}xf?Ybh7Hi*KC>%e1*7-CbQoo1H_LdcW|e}obCV0OVE z_&6f^_!uK^siH;vh)AwP!q}AvFPdAfRWZb?PFYbK{$iXk@pW=Zc#-egblirhls0XL zl&D0BXa@$CW^lC1I}xwzK>^kr{y@CIVM?!5i2tU<>t6$Q?w;$l4D>s0`u*Vc4E~+Vw?FiE zoV`)7>%_nP#|93_d&9r(wgUZh%hlokL9zb>eg9c*bLEfC`V^#;YgtPRT~1uf!F{4MihbK-2cgz1xoxe2cU$=?HKKvU0uM{ufpM`Wm=qAmDZQ=Br%5H3D5LnAg|9H3MC1aKEI$xqwyL zU(nlCEkR&+^YN-?*?cC`_ccd4F0oj%ba&)6ddd~v|l+TDLb$P z+Hi_<=KVx0H0(ItfmeX)vKhB@8%Wyz_&->CaVe`ewt7kI!7v&dzeo zZ^4SsDf;GBzNAJ4`8a)4d){bgt8!9!AEp58v#grlm7b;@Z)vGZ7$az=>SiB8Z!g~m z=Y!OWKu?{a=X0OXCEE@%PH~IVYS@&lzr#~4{TR$;BOoot4m4a!u)qokNR}sn+?N~+ zsyAA)i{#+p&%U4Nd9KU2x^v|1%G#i;FuR*{Cfi8mSJoBCn_Mr94V=LxG6YOFb`lyq z#D~guwfy?dpz&liO*P4GbV&yjBO(~ey2Q10qm0Kl0eL4zyI;*oaRl4)Lyl>fUz}|! z`@~)*25PlK(lDp(xmyNgJn)CiOztOs{UC8j0vXJ-7hPPILbW{{_nWGmMZ&jFk6}a8 z18##w*XtPW;EMs^xvAO@@t{GIqreWOD<4BqPP?q}hh)*mp4I?B$+>R6gWW>G2naI< zVJvLM-smn$+mWU8i0&^r@D{`Ya>XqHI8%&<(tuy#(%@DCKQ8SET8SZ{RRngji31bwChI!_+ft07$d5W_GZBu zNm|ofUY}JiH0BxYa9khw(((;j6S&YGX>cwV&fwNn6iV)NId0E9P#xVvcXO*^txeQ5 zy-h2+E0MVwnqCeJ5=6-nV1^oBFG4*qXv+L9C^s6=cv&q}Q13R34*_<(jFxyzDk4hg z126C>Ftl0q1Y7db7OwS!gSh}zBuf-(X_9-N8k+0FHKnTQSA;ziQl}Y=b(-T&Pvjg- z&?g*K_L~Rf2MxnGC_ILs?VmOtGw8+8axdOe1FkC><_?|7&T)e!sK)&2J<0=%SK2Yz zwXro}iVOB0g_rD>FWJW~3`HUrgMkq?9YBdAMu2~}mGtpdgy7Py;vVv#hzK$}KKcj| z?>3wWYG1ruM)X2C@UVv^ONw+7%gB>u&8{qJHqTI|0RX!(!IF(Q*ClwY_J9qX$cvfF z40-*C?Ii4;7*^QyrEB=v0{Fyl7dKdW_A(5?;h<;s*|FnLeFjA`$UM(h-nV{{yBx70q9hj8`Pl<^0{}@&cCF zjUPN0kk+DB$hWoF9pMqjA1GXmoD~=m`(OLp8egCYDikqp|f=E%dsXQiX%xEZWf(F(-ngJL}lQv1=Fhyfp|jDkESD62(`RpElWrJDFe^K zg?YIgkp`j9fL7T562s61IrJ4~R4gHOEMYJNLr70PimA&=f2GR%{K-lNo5F#pKGqP4 z!v)-3_}Yxm---;EPx^h*Rj@9!&7BxokH|%p4_EW! zdp=22hF7RjRXo&Cm;k?WUp16ApzkfF9ln-&L17C3!tIbV_h`JS%ibatwOIF)eyyl?~)ci4j|9s~;^u*DAT}af2qSFAnbE~4P)m6Vq;jOTmMd+wS9?vdP z>Q&D#w>A15krzAXPgEAHrbiE2`IBLE?ecdL)dZmymp#KW8gVpVe&wY!3omq^xfcm$ zZ*dmSrb6zy_nF|6rltO&yaoqp-b_-p0}|qJCB(2WUFL`PZDK=Hl`vu(QbM^ilF7$X zPAzRg&d4imJScT}$zQMVl4CoJ-b04CCNgptGA zLLzh6>9c={Obv14M#+jf2ZGtyYXQ_rD7 zNe#SBOT5du8(G{)+~w)N^Pa21{hy-ypHr#;SN5P^vH&0c|E;0{IKVRfpDP4D{j|B?iE2Z-}`|Awn>09}i2zg!gt zD`;GQ_HO`l&) z?qOx^HM`baY=)e)a>h|p5p91@P#K|#ZLU4JD0iHT*Q1Fxp*2M(T24rnu6n3wk%#OW z^(4F{LPa^e5NtjAeQE>_&q~$tTnN{Y0?27=I6s0kSJ9e>UwsDYP!u;rk6{W>yg&*Zf**^Ge8H5dS>c~j^Gw#?Y_yYa)>V|R1d5RnBp z>Fr8ptWthy-;l*Uh1!q^F^-tyKI|cW%U}A`ykFFkbYUY8A-$mCvfZR1J_=dk zdsT98u7?4_dUj*Pc%O;Ic6hGmRSI>*`UHAd)`|AdQ!$9^j%EiAHm)0{+}MT~ure!V zujT`16?pu}SfHuSmh32l1h>=zu+LF>%-q`O{ApG8LAR3Td`QC(D>t6MzpnT_{*;$i zp!ZN3F5nH-T;HI|u$@hnhBBw{u`F`7PN#qHP@6AG?p38I+gGw_$K;=lF?x?5GL3Qk zWhWaxZ!wVC#raK#h4Nm9((A>*$A-MhA+b)z{AFYGwMz*-l!f{2`(pUe>svDzz6V9( zhM}$oon&e%)vbh~%>{7*oo|c{k6nQ}$eBSH9A@{q?-7%{j{aew7Sk`epO1y*ii`Hv zD}^}$6)^cDnN82X-EG~+7IpshA?}>%eh$Ki?9n!3#?SZM(<{`HJQfqSlvg0z8CohyPA|9pfCz7v!JVVaL`5Stgw{XMtwJHRfU4SsPaPFPE5b8<$ppEe`!W!zp1@~ zm&;ulvW^4;STI^nreX&<{Dkqg&TB+@O;fBwC;hXi7-rmFuvD+o5tp-zkFP$~Dj>2? z;!l~8c$ZAfCg*_jYW1}xkx}pT*JTL+QqH#Du>cY7j3}B6R!~~8x6HB>tT>!fu>VoOsn>gHGh( zzEm}f`_X3NyMx2a%vCjP}u&P}lb%;Xq4TKx=jZYRD$a!dd%IKksh-{MdKgJ?9MSKUj8S2@{F_QSTZ!GW2FxV`_GU&!#FiyvB^H>A-LbGf8 zW;fxBy>>`39sCh)>x?@x9U$MITBxd3PBuU#A)l`RHBO?^6Nh)#`jP*8fPP`$_D^`!jG66xKF7<*O@_fI@w1FzcLPvOB%kD)F ztCbstEw4uD=Fu1h^c|TLSsypOLEOO1pr@R~<%YFMn8eZKH~$J1AEVV-v*MfQGOh#j zZa(8Vd?KyPQ$d?0r8UeVL`78ZCIQr%gn$_+{s{ZoS3iQ8_p%EmIYiQOs2$g^ce4A| z07vx~y8S0B4m;gu!zgLEL2(#LpA(Ot>kzwUG?oZ9*GSo8$h-hHE_BvBkVk*F^A-o4 zDR(qh+;3c2jW(@Zrh0S>iWSebi7KLRiTXYgwfBq4TzOr>w=duDdB=AXza@AQuz*$+ zY9rBsDgLG17H*)_aK#r!XY4grA&v^RU0KmSoOu1*jy!lYV0)@*T2jz*OphZO zqWbW9Od6GxXe{D}AM$CuQLNtN1G_3cJBacc@p2s6%DG_}j+%TJS^!Ety$uvo_D-SsF6Ee|_@YHhg`O%sF?5!j}J+L;^_} zSSi#qzdVijcT|l>r~FFh**yW=R5Tn&F<RU zz3#S0t6b6#(lgyd#O6pubbbn?fKk?9E?5r(axR1}%oQaMYfoEJfG>g5{Zmd|@=}X1 zL8<6Tj|0mdWS^JitoOP}nKVI@4HrMR1hSg3rC zTe#4N8B6I&K%2IQ-iIDyhlpi(Cc{cDtr}@;BNUXJV_!IGk7vd!7R4q`ym1bxd_+H5 zH=%OS@v#e*#>cgV#V4AQ3Ve}jJWLHB5|H zSy`|jkCp3NgL2VM;93-MB1_4A6TKyeermn^dYDrS<)eQPh_j1 zLopUaGcKw**LP9ssD?^(4nb54Gi^pk57Y7kxeA%+w<#6-4pOT)Nxo(mA%7_qu&8fr z9kn`!_Kek#+(AoT*02fcZ=#!jY&5`63OW{ycf+zR0UU8bKP*R(@~VO2BW&h1IG1Q0Sk4@$qD}?l^Pn7lbD$c2>;<& z6{=wPOAp8$t}V#@K<-^sYtwOUg11CN+#3ruM_^G>t2-~7Cln*l-uj4 zR=UT*LfN0EBnKI5NE+8mR*X0i%-+Q9a2v*g2w8^g1jr0tD?`31M)uXbSQ$vk+U~@w z@z2FuaLvf5@GYI?U;f;>QjSt9+wP`eUAtc!DzK(Q(&+qRb<&*oAQh&;b_y8-H$8z|Ro_{swf%&IaDh_}|bScq?T9 zfE~PQjQeWU*i}fde}B~&_pkQCKlsr6y{pFlf|~usrGE~cU1#`Tp6fqXmjCEeKNb6@ z!=H1xI{m)^@qr`W@!SHw{)%7$fZ$MZe`aNW^~m3O4FCj+f{O?KBlh38lz(T7{Hxyu z01E%njRSn@t}D9$K#_j{3$JnBHSoI{q@dr{--5dqJOF<|O21qMcR1*3Gxc9qPF;08 zi0k*2Q&)upasB4^aa}MFc-#5kZWaM&buFRYTq?!=lL79ZMTy)$;mE&I_1L*?qL-Lf zFnU1{+x-#x!$;B^a%Fex*|_M*m?RZ2R48^@rxsuao+^ug!^)awV|Z zXXl^JFK(NuU`FM$XhqTg3E)_az4brYc ztZlTFds5HXw%;dy6M82nD5OUq7!My)Fc*g$(+aP#E5ueBhovzTkS~D-XNUNNmEV<8 zawBiOC1|8){$uep;r6&)w1oV{s2~g{l!ljYDm2ecE~!B zHCOyD1%=+Ke1gm|SZ`(8A4?^)ySJLqXa*43?O~VqeBw8>v?%%cyM9_o5kmvMBL;ix z4!bxJ;ipDLBq2{~=g@a8>!uz-tRkM};!ePp^^Gx_xdHNzwql`*A@y)!9`#!~LFfV$ zJ-(KNDeibLEDbL(7i5H{@ET7ojox+AoBF*m6M9ux2oCGzVJ(bgc@wL=KlDJzOUw-n%dY2e>z`ebMSS$+bq zVg!I=fbxKeJu?Wvc{lm#+KcK>KFEKs{PeQ~@ZanBe~Ucwub!*^U(6o>;NrR6+;%;S zUC)u=Tfp<&wcp|mPetyZ3^V`qJa<a zNTfwC)rs0pzly35?2Lf_+E1DqC$;0)ls9UBlAwwg)-FOSFpZTr{cRr}CFK*Ejr);r zY~Y@}>#M}2T&!I&ls&>_$>MxrzxjFL5NpzdXFD=EK*2+QwOa3(Cs^4!??+&jI9uqWziW3Vd?RTS*61E<$5fH2I0hjAD zRK~KFwEVmBKg8<027@($)iJD+tFNh{7~sY%`yG_=*YgN?d4&bbJ{k%$Kh)S( zEEHW7M1Xxv@R-ac0_Mrfve?xNTY7JU6Q*{jvPdYiGT`@cLb`mpb`2V2W?%N?d)`dy zv&SEyp|vkHbP@8{xas#LR9CZW3oC#OjC?{cCiq?KpEE1zG)ou9T|!87+oXRCl>tSy z$F>(IKnm8BepGKZ1c)I%jsG&U>4;h&vfSYJH#Qq^xQSa^b8FlvotUPED z4s(^Y=e9E6m0@$;5Wd8frD8w+>W^I1p*rTqo6y&KaA^)vx5I-!BLwB|7)~zAzh=R$ zF`kvZ8bzW;{xqO>u*_lpYyHy4AvwKG|FvcH^a?D02!T9zv@nXg<#eYNm)d+(w#axJ zo>AmUl`@>U&4?j)FISp6mO1ppP|-_ zZVi2D+5)XWn-pBkYL_gbd+OQU1f3R2oNHDkJ>|*?V}K~{SfGBUPbdgw+@_S^ukIvS z#;Z=l$Hmaj=c2TB8pd9$0L(VYeZMu*!0)dnwf+6=5;BWtWOZyy?2O9hx3{LQUO1LH z(Kf_82Y4c{W7)p?<8pTk7k#bOh30;zvy;RsJCzabN&KWa`WvS#6ORG3i`_S#3u4H( zDTf&9vgfqWhQT^bK+A z(Mi!}q49wmsleU{iI9;#sqj8Ly%=B{ZX=f+<8$4Kcj)Sb6s zZ8X3X3RP0;)E@A%rVL?saZ&DDWE&lLBVD~+%0>f&izDfaCGw8h&cV*K_IvvQjz@_{ z40XITg0!s{LCYdD^)BDPW0>a~d+}Geu2Jc5a6I1@AiMB5hYIcXyv@W2ULbL&{$S_4 z;p42M*ng!ay*t2hlSRK*Ju4RCAB|a)=)NiHcmR2Sf<*}x0jwo831xw`Bw#=Pedf2b z8xxxl-`cgN4ZMX3*KDi9ULXiW&P10(<(kyW{YZ?n@t3f#8Gw8}fs)7uC-i`l55Bo= zJ;x#n-M`5#KsV(c*Ngfv`x?-ZAJ$6<*d-!tG{BZ>EjNIC$YQpdVW0QS?B)+;_qS-T z`s`gTDc`nDBZlZE-9uzVLVIHd63)J`Z79knU9I`*uWF+51^q)LmJ^7^JyOA+SOI^R zlv1$SSi75l!r0?nW$m+;^a-`)vaTM>eD}JmPMsY?5$Zhmr#CXi4xW%vt}Zx#9@SvT z@Ncd7cOYos4FnAi6?3gZ{Svs0{Z1;>b-#n9y}t!P{{wFN-%D%ykCo<5Rl3tncYE)D zVOD?r!mAhkPlVYLU?a?bU(o#jqAK|xt&;?|@P9!MNr3JOi~pnL`)BW$5dO~ub^B-E z`mcIULY(~{@PRAs|B4S>eQ6-@4nDu8z`gp5+IW2p+&dud-vIhm%Ye9l@O|9;dhsoWyp*ELNAeIWml`-fz?0=uJgJ2xEQhy&?Dx61 zOz9F`%2%LA+7~<-_x;(fM7AF)`ao(+`j{7!6>DIL8e3=QyD?i-j8KSbA>QYL6qD3W zn)etI8@yP#gAM9>wQCkO@padIX0i*hEow*Ny7)_Br1=t5S*+<|Q{1;@T!a#3h}r9E z4#!n?xtN17f;~u2@4beU`KEY$d9HZ)WfY@pa~l`EKC7}+-Y7(*q+!`qOYVy>;+yPb zc#nZ0Z1gw&^i!oz+Fgj%?c;q>&DSkb)en-v2fW`}olniYxK?8-|19HxKzEFC@<(U1 zxNCBAz4?j05LIW?z-ZPc1$aH5C=%iu@l^O^qCJjv037OwxU=2RzBoUw#{s9`78mh1 z=j6eNl86Dg&)&38UM(#~wi*Za=+nx;=MPPqC>;qkD|yGrRt&Ty1EJXPmI*jNi3E-L zhbWMYg^ zj$G_B8fGbfzJV9;)FXDUFf)bln!n~W%t{>=lLKC*kk>tv^o5T_cBXCG@A&e3kC!b= z8l?)_LsdvRb!ryz1N0FM-=fdx#OX5plh=oWPEN-rT;bs zP#CR=YB*=k6haO<5G>~{%#7;7gW1-{Ulpqp((ZkDIhQa`_J}W9)fL$)mMbjAaI3~{ zw$Nr7R$IBGA2Kb|r82nd;oy^QIl-Z)hf|Q~vQuXP>_1wF7x#0R$4f}_HN#d}>OPL+ zAQRP}dqXDZ1t!90rDk^=P9sQ6LuHwCyyaXdEM+Z2{%!u1S&EC}hQo?#yo?nU0PGR$fuxfOvAwe3%6|VnMct(n ziO!Ztxzt!Wbqu!7yk^A{p3j;w)vr@(OuWuGpHvhLTI!Y5t)M;7vuAF2hLZB}ot;C9 zIEMX9x4Dw52|8hz_FDBYcS0SAh!W9J@^HMsoyPs$MPt|cRIhuZ1nuBpaoN^Hc5;zd z25dcA_#*>i4DV(ngi)3Op;Bb7qNI-&4Lv9$ZWhR|t+?~VY(@QFe?T=Peb!X(0^!cg zc__{{e_Sm&kCbX-1CM?BP%?XqP*S|*+g7E9Tz;T~#=Vc*$ii^Gk|9N=iiBT#wU1Hn zS$&#(orChF6+uosxK9l8#6&tNFuv{mKFJ}KNhTgOGJU(M&W9{BBn**Q6F$qcNI?bc zbTPDj=ishyMC+2yuJTwT%x|>`T4o=IM8L)_Bc=7=hTFb!%pKLGNp}SA?XRZMAh^ybV-pt+7*bFTmQ{1LSac-V6YF*MXh~q4P+D0L53=pV z%~%b>?0iVj$z18|m6M5d+%Z?1(@J6kEl06@_o=;a^JzZMCC@1fipqVazi_<2q7ZpNK+-IL^g zcqpa5?_C2X>jRCWIT>qSL2Vz;o#~!0KzsQ!0#Ch9@6F@QXGvx@mt5+jLwn!$+jU3k z{Ksv*Vx_Je+hKM+VISxVVd6d7?bT-sTD_dy$}0!a;;pVv7+b%U!Xma?SJbeImGL}@ zaS5?;#h!MkBQ<)D_kFRbo=LC@LzRb9)T&!nks~xotx84#o@{m~R-p%}iX%DlFkk{H znuRj0B?s0KurQ03bZ&dv8zNSfK-fxG2#vY*m>+!MbC2;vQ)_7X!w5Qn$}Z5^KT z1|Ecv5aU}1Rb(DT-+$u^v!pukmE$WKHyj0z9`{~X42GX1hx|iPLmNzS%$J2d%QOsxx6PVnDLCr`VStXp9-d?a?^;XEyvAX(Q@ zJ(PHAYmoPybxtk}pMv1uT%w+$M{T2;D153{Dv2vtz$2>i=?r_fOyM-((TVwPV^OiW z`$3e-{B5h2L3f1S>^GsefFe&Mm`QD)BYC!X%lAJYWb$Q>y{wTLq}f7{;upiYhv05x z<@}8ypV!mD*q|84WsdH}B9H6|vNAvv%L;k)yM&AWGfVAu9X72@Ih`+!t^MW>BDq%I zyOaicRYj3@RT5}r3JfDdp_n!0Gb=U|*eIPj7IG$Mq++8RBlNosCJpQ#-J?y zCO?b22;6Acko@GCiIvEC5Umm)QY8;?@-&>+@c8m>S~8b1ye^C-#=3EPcGmt3?eg0p zLD-M^bw*RH&s5ywX?aJNxN^@o1jc^9wK3EqT^D?BH_AxOi}^xm@k8{dgT2S*@neh4k4Cs}F`P z>hy+BQ6*T&`j5p@o12r4wiitL+4K>Uq1%ZsUl-GUgR@Qy;qysUPeke7aGJKlPwcJp zF5Je}8(4`qqe?RP($Y!T{tZhmyCx}T(VFTTzlh-@7x-FN|I^i(QqD)BtAMK2U>Kx2 zfo~+~_A{hC?#+0fEo*)G-7|!OaJp5c*^*<^xtny+=3`EXNkp&x8H{ULXLlb!BjVl!G^kNU#6K8xr zH4xxw$}=3hMzDD;iua^~kcU5TOl88lPV(DcBzm-ahO1mfX<)mZk-7Sb8U6i4(y7v!zB=MHp=LKzMhG zz2SDlKa?Eu%`;I}eYD5CLBPI_8m`MAF%L3p4jn zwEatCx`}9_kg8Q)OpT|O$;c{CXgKg%h(IYgcJ*?}0RnoE-uW@1?dqg1(90z4k5)SZ z=$FWi;)J0c0Z&da*c2=XamVZezY|MJNjchz>wKqLSE7#-LF$=I*Df~7ru06Y(lNpG zIj;2C>4h`?QK9>A2VS)stsr=yRVv#q&-~mw9l8|y!f)(AFmV?7En$*U3cFB#76PSY zGv+5Wa04p!@hSsL29u1r9q?Xd$O&K3t&-Z_MRowWr-?W}2MpN4 zjT55>Fj{qhg1aAmIR~!B+GsAyyc; zhp!SfPLzeh(o$=W%N|Qve(@L2{1Tr9>voSb9#7Ish=%?5bGMD7yE6GdY%>|#U zX3}1@6%Kb%PetG<5z(7d1%Y^r4Az+%Y%h*jHV#1DD5s2RK+M-1bo{JoKwgO-PuQ2G zH@o@oVzxq4jggbdQG;Dro`;W#9?|p{SN&*H7m{MTab*;166mQqo-&P#>`h}|iLMbE}SH}}Ym zW4P6(XCt?@>rbHJuap^f?i(xWbYj4pwvb^59`Sc-$m3z{a&Q7@)XbN&UW|Q*#E4AH zp%B(S{tlZe8^=PnPD+?Ti#Ve}B-n$9c(gQNrI1ckZp}Xjp&c`{ni*00q9la>0ByND(?VOx8SE!TVwV+YjI0V*UXA>_G;O zCH>33E#Sb?;0XG6GJo(~4TZnU{PEYo{kE%r8}0w?<$wDNe|zzN-QQoUaP!Y!Xa9Sx ze?R!Q`~P(6>csy(?o8 zEkW1B^_LVEIQ5&E<*K%zYfy204P0sv=r=RVRjEOs-^?smB?MioBG-3{gAFn~*Xzx0 z_;P~7UH-1C{If}7EqRn)BQo{9jr>~x z&}&R;mUpP?`eo>7IYgWdzqUVUroK#{VJRt2JQ}s~o*%)1K-O1$+_<(~kc7P89bhC= zAnE-bxeLm1>$L4CV2&JbQ6%u=T8xZ2-+wKO7a_ zYOy={VJa&Em*hQ55Q7pm)Ew8L_%Km2W%ROGUZbc zBz6@?X#>6z2Qf+n-5>i2K7@K!)Ix4@F(K`l4Zq5SPg2@Uvq}AnKetNSdv^)rMq-=))pOwMgI+W!c^jD(>|oQZ7-u zk8dU;MldC=r#N$>pw7`^+Ffm#!rKTa{5 z)0+p64oJTwmC5~C4g~Ss(OSTMb3mxZ2w!cteT7*`gJIxMJO|&X*-Kt{&2`BG`GO>I z8ldNh4W1Z&ii`X#VNZvsa1!$*(ZRu>p6fYPn%w$v8vw3U0YQS@jF-68@4YSl#MU@4 zI}q7n2IsZen7M(AVX|O-00XCj`X*C|pYk9lMmu1@V2l2)k>FhlhK{PNM)p zp0=D0cRFFhIpQ9A&EgzfB%0q`QNV%=3pSS`%P#udq9J)c{MxdO_C5Ja|LOx1F`4}J-8f*1Do2w)=a4eWn->A zds$^n%R&Z*;T(Bx4UbT>7p#ntCIRn4(P{m;%- zO|B<&j{NdQ0@?_=nNlsQgC%ReEGeG)gT1rZ{5HG-IxEmoncH!Z%-88}1PlpK-E0+oZF^SQ7G8*$B7BXlpW8Mc3IR3xvh2+X9dSbUI%a_HIZ z{vu-}x;uTfYB+xQdjfhPfY1hLbV}}C@rlDk*cT3GqG%TqG)v?kT!E?}|@G(kh5<)!;oIn)0Hz7D3GCWTk$P|DWFv;xHzfaXA zL|4`l>ej12vPY$$cU+B+s2W6)q#NzWlc7FA+~@ysT#ObYSuM0pBH@js<~j0@XA5%n zIVOP}$a8jZ-O36oO^8Y)O5fjAKemT~Wb00m=3;X+ra*Z5x~*+ca4{VNL|Oci_bHwD zMwR86&(`M??*EUyw+^dnUEhV3ZjkPfMrzU%knV2jF6j~k3F+?c?oR0rLAs<{LXj2_ z6wXA~+DF!6uOBZS%nWu{d7iM57MGZ2p?fCRKh&oYYR2pY8fn=f`TWh7zP>z$>LZ_I{Hz`xbIAG z8IghM$PPz6Qa*xGO6iT6(GY3S9?d&)nLdSI#)|VT(aNC?Y~Vu;<+hISp*%`ap7zWX zQx$u_621CLoWnHb4VGz%GnW^m)MGrpM;342Ya`Im7ci*Rvp5DoBQX`qC=V8v&W7Qq z+ z!?B3`j%}6R+5N0@yxvKyw22xA^jfRG3?15-;DLn zUaGOj!UPwwDTwpfOJo$U4H8QT2G=ddxTI859yW_pYRI_@P;R0ggjm4j*tz!{X@n{o z`KB*A^Lp&nV1-<{xB~E`8;?T#l;z<2kwbdEa^ZjXfz4iKUj?sY;(xva-3l;r zl?G*fQ#o8y`!t#EvzgfWtv968HHX;76<<`hv3DwC8+EP;@)j|+=(v{Yd~|zMN!Z^E zRTeCqcEgwx7<}3VEW@PXQS_ZDW^hWXzEtFOWr(gT*`{M-|CW@f&eb0tPp;llI?<`B;&yp8Pkb`ZffBC%*TRZBQ<#mADU;{m z{^T~WgxH#3r{CJ?Y+j8c)us^@d`fMQOEq51%6Vb0%(6n0QT^e;YUtPmiCGO;0Ac~# zMe1v8TU)cfK$DOjY9F8Gtacx%knin!A;AzIv&<|8UmPw=ku{)uPR_{_Nn&i*Koz5o zE;5&Snuw+~sFX~wrQtQ`_@E;CW}+q+e|ZLv1F34*&O|*{4o|)*-z8Sersr6K#1lA& zT;~GkTJHS_qc!^(X7+(PrlP7co1`+Ugo4{!K~B3&HdQ?Avo1M45@|`i(M^A;Alg(x+Ic#&NbR1HV>T zEv`*F=J19Tq|t1woEyAa=?w=V(s~E%FZd6f8(1^7?Z|B zl#en}CRS6`r#)ol1!ZDJgaMSL2$GE?L%&Fo&>_Ac+>w+zYH5493G%bi#rg_E?&2znsB8MqLwInAHO==`lq zTZ0?SI+VZ8Mq< z)G4vQl61;U;XM-?5!ugQFS$Ivcxdk%xqDYH+!G6C2Hxrgt!T*`p&cPm92%C5&pBLb zJ|(BwYU{NlYQNPhW5q8Ld~m23H;WP{L4S1K6I-Ga|76M0M{eV?@OXU6xZutJPHAIK zC`z|lq5eoY;Cw}(qAc8}2LScQ;2q_q@l?R%z-w=$@9>E1*yu5px{${qTk`QL*xr*wO@OrY7S)L<6yJ zMunb~yRu7ZM+%xOn{lp9+piB?BAc^s$-LCFD}AlEfafHD_-!;!eHH7YNNHT9SIx(n zLWFVcI>^UW+OP&Wf(NhbwF7k%a*5SLb@boRI;~-oy=8TMjO0|o(wD4tylzS>Ix@dr z^~!EB9jps+#BsGD;5YMYm3fIBHgYL>L@|)jz3|os1-d{9? zf9-dx2w!gWO+Q_{0^5M8pu4MAf3a%+8sWdSPx*oH|B{w{Cnn4%0{c&C*(r>FOUq8t zDkH~Yl%37LI;Uyx-AXnknk@vW>_x$K;nvy(uOaj|uN$XyJRIevE7 z@Z2Y1&2-;y1Pp7@mL(N8GFLBwU;RCfAoZv8>afm`-1va=$a4wT zFz$G+bbKAbwVWeY3in}xcu3nF^P-kk8vOST?r>-yKb#ir56JFW#h3ST?t%H{?A8p??no{r~!L z!T+N<*ah#Mx=N7a_oxFw5WM^E>nlP2-=X$jds2}9SD5_QZ~hxT3kv-^Y9T26ue76} z2;<#x6F-pl8z}x}}`8|=F?|wMCe}(>mpd`%yD)bLzyeHDj47{ZipnNMB zU`;d!giyZiya@)NxiWmT-IIh9S+k&FJ}KsNx{?9AJ+zZ=ZB{eSc=2R>)xkpRD}0lX zJ|3UrkzXxz;O5+REh0Ddm)GhkGx*2|aI%sTaETZ+J5=70y*!mq@odu;-T3;PaH62O z8Q*cot$2?0scnJBuf(;oS1VN*=Hs@j1bM12_)b3qMp zXf5+-j-ICRR|V5a+B;4zDc7`1 z?Mbe@qAVw~7QO)?<}N}Ofqr1UnZ>@vasu&eKUo<2G1o`IM-MmY#d6%%*6gk7tadFW zbxZ8j^w!|`^}%x$N@t2JRChMMhM`heeZ$?Y$fpaG>w^0ru;Hm^M#_@zHNwOKUhBPS zaB)|2RE7{&->7FJi$QG17z!tt#HG6ds|JVt}t6c;ms+%z~<@ z)C#Xk$t4>N>+0~~fh0PwUOq{eaQ9D*QB%p$12goEvF5V z^?f_D{cg$nJfz0A)iIEa5uCiIJ+KUAs)0>$<*r`ffDXsM)VCvO0+)-K( zj|Np^enBOSEJ#(K0*vfVIYR8zSNBV zx_=jkx9e+}B#y2@z!N?@1B>jQN6S7X_MbCeNFvR=+KH+@7w$CXfX|f&muDTE?Z6YO zt^eqYX)zFnT=HVEHf9_E#!#PzbY0&UI&|5%9$-zDYDhbaorJ6O%y-W3nNS$tR_{(~ zbr5*B775bD#}hmOETuOL(ctyg=od&`;Tl~8`dZo%Zu%}H)%%CgBOb5N?wUFGM8*N^ zx0EiFBP4J13_zZm8$BD3*_(9Z0MI&k0wF%S=uH(Eqa;LZ)2C@XQ=9Sed^|EVk?y>O zno5(@p=I<5;}H8Gd^H-O5Vv2Doo^bZ0tq*O6-&W%O>#E6MQvTK_z_q#wNl7f!YtSsm=?>t*1gE+a5DlLYx@uJ+rFc zBvVL7KK5NQyg$cTUPlf@;=aQ}1q4}Re|w4irKNp)#Q^CBn04K7rv|_X z1{fS|nCQbLuZ4Z3^nRz)! z_Gv_9BUGS)n9GOL&=DAx+0#^n#-n+jmsb?6Y{7D5Y0Gmkq_&ls8S5?romC}J({-%; zbl1;63;{yc{JA;@Xru_krgABEM};(vq}v%rV7KxE!XIK;%I7sVF;nz?CW`I(sA-xk z6JyrvC9%F9G(EE&w2x(<@xt=Y;|9R;%WVD+Q04%PxARRg|D?=;qz>;(nfuw}pk2m) zL^0qWxBUM4ivQ8%``?%U;nZ8j|7RHf`wWd=x9mWcUrYav&p=S7)w}BoWW6g49mpzj z=l1>QZuw?&eN)Gu``#ZV(EWdIn|_w~L8Y51{j($jD*f27-`aQsZU)(J7}`I{xdDQt z690COzg&0!5Muy(-$0O{2;O+oOam{voGEaP#z66yQp~ARm|~dRnvUZ81O$;Beh7m~ zV(hNs!JVsw=M6Cps}>)R(;XUOr3##u_>>>LWC_if0xN;QI7m7WLhz*WgxMM;TQOJ> z^DZ9k(ETJFJ(zpi&iFOfDCKPdFfqBZPt*UKlyQVi9#kUrTXpT!lASBrAi$DJye8|& z(@%m(g-wh83+*i_)oyQ!)P3v}ds;=GSCSzz`NW=M8%V#P;La+y*`dWZr&CMDlaXK8 zS|71aX*D7(Hp?Dd!X;ORksJLn z#jjzQ(GdV+v>TnQu&DS|9F`r9(cG`{$vrk#% zno2a5HWr$A{?o4UE6{INW+!u1R0F*-!}x4jjl{CJsQvorLs;}aSYS7rln#h|VZ`U` zkf?vqw!ZGv`K|mrI18#g?p>{NUrZcuJA^kvx(F2e2+GL`O6pTV`>AhB+}UAu$>UOl zOD8q#b#%G0us3>#uRxDoPM}Nx~NDX){^4R+vg`927?*J zU-6MCMtbr~XaZEeQdXj53D;0Dih`N`W{Npq zgRka>DCA4ls?};Ljd@Z0u!Pk3HfRv?@brnzf&EZX7fRBb&&QLT)xXkV=J z`;S(MzHn|wOO39%5hc3rg>3B%trNM`b%n-gl zakOad;q=r|eKYEZplqbm*@u2j&H;8^XR?AvX6BuBYLqe_ z)fF3*iKnQK^3DJaNf~+a3#aQlS3Y~aVT^eTN>jv#%pO)@jh?KQ+S59t>g+scQ9jz=qIYhyo8Pi#7`4 zqdwzcR)tNG=PP-Q*OG(IV0;9tk?QMsX_YA&hY!Ocn^wRtnj{gC#`XLAv28>*7p_x- zDS#PX9rEpSZj_Mp@{P8o7?FnA#TjwYi?I(7TJ#=R@I7tKt(l;04%@Wcb^S(3vd%&y ztl#$R*k~KpJkx~)&fkBNNuFRK5My$V%V=p@EO;X0%TV_aY^xUYo(%RIv_%=%87Im@ zE2oY}gEZ#|HDnn0s=^eZp__JXi3O)y7tn9WdhUqk0RrxeS=`Q(1Ddo}y~Ln(4fG}q z@X^5@jxPFmBS&E%rf{`L$xYaWT~@0=&dS-ygjn8PJAs8Y#2hEnCY+UN(XF-3NdAly zH<5tW2!xXX)GPL~5Xv&$eBCbcdOYS2O}3D&HhIku zc++2mTi+2aU}sm}HD`Y@ZT@mK1>BZ0zaf_V?plw1a5(17{X>i8Gs`%cE!(s`4(p=s z4Z#7CQ=e%FyPNi;E0k`>M%l$`gW7>L4oNDrggK#x2RJe7I_@bjnKK?4og;(k%L#6g zWtv%^B~@IAo#gazMcVa&VTP673sEiDWyZtW^A2TF1XoxtaqRJUooP)G z$Bmc|unYTU{jSz)EA=yCKVso>F$%ZZU`cJ%uVcd3u@5lqp|8!0nJ#Y*v!&|rV5vlW ziY^2b;S18+o6U+hLnO8NASZ#wlrQwuo}mRreIf}1Q&P(Nn(sJ%0Jfhs$`j)$`#{MP zbDmCbi%zO=GUoGy57qiarfg=3Ane5!Sl{ZB%!Bs`Qbnfbcd!iD|fZM1<6n~f?2?&))d5m?ANde@ zVftl$5zS{tW>mdcsgnA=QY->mDG9H7Qj%s@F*kBviT)Jt>-bM)8Xw9N*xA-d*LFER zyDd~Oyw~az?OL)H%PS!-^!FK&Y0c1E{6F`<&%&!Xp~fx~;b?Hv4g?fI3JbRtg% zFp3k86mPhT7p3K~rk4mfbKug$xzU;}`1kyrYCX)YqpYv1Lm9XsXOG}zt1G|gY>PUc z=S0$zegvFR1?n}sS{X*XFwt+fF@53VL1?kVg|Wbv0GrLT;e|IbP{y<45P)Z&;{dZ) zUxwzrNdZO#LF1Y1H^!I?r18tN&a-&W2_7Me<|M?a_NK69CkJxAc!_kT@^FL6_0ddw ze8;m@zewyR>l8hjE}y{_*{+vMT7;%wHLf0#pZj6m)d%-PNLW~I_d&FD)%wl)1lo74 z(D+=fa3?0Yz_(pm=i@O2wt%F9163qV&83r0BC3t!6Dj8MzKvy1e!xF^<%8rU{p~%f zee8skcHiSE`uD49$h$+HPgcSVAXn13)X5o%YQkS_gNu@?Mcc4a$^&=yA3ng ziV#cO5zt7A8tgD)@(X|AyQ^0$yfKilKpFQ7eZm|CZa%llD9Rr2iwvLpbj=a!A(<)Yt_VG!o0Gyl#zKA|iKm42U^t73k z6*~ue7S-OT{CEYN(cVvRhTn`EqW6wIlNRHyt<4oX(+_2}-R;ZAHA(4dpwgKV@HRJ1 zJVLPV*R`lOG_m|Z`mWYibq8jTjcvf+lH6vZVbOlp5BETK=%GUJyRzXanm6C!%v)&R zmJPbGBLylsxb=+rfV3srJ9?+@7zIqf*nEGr7&39(S`4)!rLDag5&ZVQV(>Wn2MK8K zkonZeM{w*48eBeP0nm=a$iqNieNTybF^Oc zLG!|&tnua5I4ZIpcs^Bj**lSBe+;oyXASWoSL28Wi)GyN1EvYvSsR+L?hi>llHq;l(K2> z66y!=^VL4?FBJK=Iayl9VgwG%J&CqZT(T>Ze=~Rqb*_(qxX-?q?|2N#-7Y#7?MUzB z{=hgtu_U5vG7wrTfDE#+4_MoqR zrw;rPX#)s~{5Pj-`eSu}Edzv@?#jyk>nZ>0XMhm%f0OnkK!{cN?_iw&>XZKe@p^!e zzzzQc^zT0L*@lVkAw;~8y{(*D;EP|#h5bzt(5ma^16aWHl zz^otN-V9eD;08hYSp*H2pH}nFBIt2}fZs9eACCVPop{~g^{RRRhp5*`zFxwfQ!HAhj*Ekq_Qo{*T3Qn;n7t|+FuBbX#2 zmmYP}BCon_#@rWrdTzFQ5e}p90?zwY4j6)Nur?h+BMCnM5Tl8%{@MXS6pTP{42(o) zy}h_$=!}=z8@Xx&tTgk%VLK!aey)TeKYDI2*3Mos>ij8+M9pWlTvVeb8@XYx{uq_9 zfOeaBqG;5@nc@c`(ROh29iLVj+Lvd`PNxPi%sBKSi`Vu8Tr)CREPzBWA?UC8syW?4 z4%g4;u+)TGg z&Jja5lI=eG+&?l~bx}hbQcGa;`(G7U5k2Ud<1fOh>4k1{^HeXK1Q+WXgsy&Tezp)P zOS}x6Aw&yF!(T^Lse=KRN$VE(JZAe`wCBhs{}$nFKFVg#6hXlJ!Pm$|@u)x+Ir7JnH9W5YUnudkEIzXT9a`2Rd#0@b}wrPn2+Pn7an^RU7XDK zLitOwV9t*E2(U}TUpSgWMqO@w?q-~8(#xD8ryTiMV&=$8rR>+o8*m9dmZP#7i|Hl8vzn^wDNb(f-;;a zP>blx+Dv`i&?6-sG8rNa06V1Ot_H|ZZ#KNN&!;<8GqXZs4U6I5&pU~_9&6(ju@@ePklD*_T$ELm^vNf0=@h>cdUQAx zp#FLN0D?06-fx{{xy^Bm2>>GhYmR_DBtKCING$%xl;$~SU^FQv^Y5+ z2y5uQT^;2)%*?8OpSE_{A=X3J^!Qp2a>1kWjcdPM&7`- zp=se*YyxM643-8gv9}rwpL}$ecXOgN1Q0B&STqlP?ju`HVt9eW{ZFH>b?>P-( zNr@JCtLO{U(LPsi_uHbGaoqtnshp7a+p|USBmg)B=%A}(g6T|!p1;SzbYu*&Vo$A& zc0-_oR~0~ODp#iu4Dr%|b$*vOqb$s5y7!uO6qqX9EL8uRlh4xhl3;AXk8WwNuex zD+KB8XWex0BJGnY2tgFTokiY{J+Z5Yo6MQP-cN_N2Z ze93x7J&zxJv^RY0DAw}qE&7_->g@bzq~@f=C6l_@`fDSrE#}a_xx(- zyDq0%mK~_62)PfdIA_iSpV2g2_tkz=JZN_fcc(mWFK$=w2z6MWVbVJZD&xwlvNCbB zscu8pu97i5q@f8yiVmtWn%pH$++H0MH*@U%P=Q2~lIh-BV(%JWeh6Qi=Zte#)87;L z1+d=oWIhdVLImf75xh+9giq$hl8LCoJ#<4mEnTr%iwr+-6ACrXus zTf6P(y-{H=PaUvf#LR8)S+h%kjFvF8cG58D%-avh6*WF(bVx}qVocK%TEAXeZyx#d z?SZHn;Er%&AV~4+elwHpmXO=8p{q8rWZj}$;xw$m=U4M%eH|~)EE@B(J__T(7#=qwb4Q~L*26pYq(-@{G=MAmu$KUa>Owr>ki09j_HOPYxcyTa zs|0xu-yJwEwBoW*cB%F8MgVR;fN{#U5wYBk_6?wnJk+z0FbG-d%XnGQ_{pKkE9TJb=W9>vko7-7cdN3+9zQE;E0Fh|AG(S-mViVY!+! zZvt)M&*I|8{WFtD!Pskrz8J?wYjXX3N1 zgaqg>c?T*^$H<+QcFVq*2tYa2ecaDqJ1%woqKq5_!8QUk1{ zMJ@V{`NsUq9`mo^-P_~}kece>(plxHTt1}^B8=_ds7Fiwa|J#hgkdmnb+!zK;2n(_n@=Ky_P z2~X&00JXBrsZ~(C?nK%XfFx(fsF@5!Xoe)st6U3?^o|Z04CtO<=LDIcXip!*`4g+C zZ?W56Cc_w?z#S3KNMCrg`?^jaAZtUsdgo5>DC&-1tF4@NpdR&7@H5oZ2D)<(q*v|2 zlE~*EcNw~D^EcW{k1|YrsHx&`Q$AWCUZBn)3UfG{Nf7p{Zji&jnqAy3bsec#IDu*t zY`{Lj<_@qvH1_P{Ce2pWj4Aj+@O;W2XLIREx=8Bzz$as+9IXxG)e1NXM!)cV=xm`q8KPQXecD> zz6qhNXw<7JXfJ9Ln`}QqEtddRsz<$p8Hv(t8Rm~OPkl?w70g`dm%|0v;P_w#K%Su! z!Gxj5-g*~Ztg~II19q)vRL6Vx`P59FM+&RDh6H&58Nu%jQ77*?ineqP6*a!Y1(N+0 z<_k@GBpAGS%JOG~>*|zGz#G5S%Wv9AvmSA9VG(_xb5guS2t96B&?~^qRDfkXL@RZm zNULS0T7@DYduvBUw1azM@8L6W=yVBbb#5bgS6koHzyvVew$DeUk-CXtAXi%QSHPtC zD%#yBqSYMs+Daun4A01s5s?Li_=uHeLhuJ6n>+l2XgVmGy4qN#S01rc$;<))P-b6* zV&}xA(PqE9*V%1~h|fTcsc`BXdbdu?i+DmaGnF%xA&Vra?rR5cDo~_gLvHF}ip=VK zjYBKrDc16Q{EzlGBuno z9vVoA$S&K>r;t>PV_PsTceG6ALOTzQCR$}oX0m|o^LC!OI8Z6t zrb%LnkRr#4%8E-wQ7>g&+Ljmd6pBvKu7a>-me7QPsB-0&7v0+MJ631?fVKofB1xR@ zR~vdXc(KfBLFEy>J-~GxjJc#-xUF${=71u*%XQ&KAN`3E<%-ZcVW`NLHnR}!-cacec}Zgkvcb| zWNsbHLgM~df>B*qgT#$mmc)5ELd04|7k>U>lkI*i9)=v?(;+a7QgBr31xY4ztq~^D zfErl#OuCZ;dH}i2Nd=-#524@t=b6?z8V{16d%=Rq`REEx&SZ5v`x_adPL|Jt>0>8^ zN#VYCS#4ub=U(f5E&nolBiwA3>J*(SM1ip4_0B`GfG}}^;Ajf=0mUlJ!5BuK?FNq& zn#?Vn{=_a32e?i+pKwMVQm_Z<`;PVJo;&ue5w+6X;{M{Vx+VP{8xh9)r8#IxGw*2{ zct4H;hsCwe*`Qpe)x!Dsc{g~OQQaJjOurVVh1qu7TazAD%d>B9W|)ASJlFQ?6)nP> zzDMXPnUznGsE=7D&Njxodo|_i@Qf=%?fWn^usi(p0?0q{=gVQ@7hJ7fzWxSUu~VPs z65Q48_cfn@xBcHgGyUEuEHDuBeN)`Bm3LTO@R-jW&v`pA68Cl$VaS|L-B7$b;ko^o z%Eg+z#mnrzf+)Ws{=F^0><+Gf5Y}S$AdlRK@FFBc95%N0%8#9q{YH} z@w-pmMW`mESO;w)p1m3R66m&i)i&=-y|UXVSlk9DqyzJLr!D|dm)6iXarGPS0cVd! z`gCaxQhMC3*)%{JqE2dtTB-TK$&tV4o9WInMeYudv#*CpbBYxeUY(Y6vE{ zW_HJ~jiZsi8r8Jk?Er&??@V0XhWTOuLwI`jv>=}woWL<=QBJNUPjc^aq>uwi+x9x)qg?i$%r|u80d2Jf*YVZ46U97j3 zJLM?xn_Zmn4@HaE1PD&q{KhH}`xTGRu21_ggtxll42dA8Yn6)8w$tIfgN>p?FXuol zKx@S!@1h+&<$A9AJDZ8u`4x+kY=SAHOQQG66dXRVyX>6rM7m(b0wu9|gr(eA1hyAp zK~DsG=(MFd0d5s1U?q&>YFrPK5G0 z7ZQY(-vK!*V3eY{jieu^M8gE+h4KgkyXb`ApA;T7XIn`^;TFrtrcg;i2+uvJOj2Ge z;U2d%z))J^nv{bGA;ijNis|&xdS3995!sQx199kE(uJR-9A4up>SU&MJ;eLyw_Eb- zduNf7GeJUKwx8Gip6L>JKu$-iUfo3eA(pp0U4rAg=Mvu4OZPO;m~SW4(TbA30Tq1C zfRI=fh6pp~+@(eIM|*9b5uC>q8;`J()Aa&QmvSW?o)?m6DAx7AAp{7{X&!O9GCT)# zi%L|Z!@Gu4>+}gPZg6*Atv=R%NK~`X9(maQME49@x$x6h?~ggHw$x@c)R|fE3K=Yv zL{AMH>x*X4kyc-}_yfb2!OJZ(M?Pf{4z8JEeN>QwhvkHNkVZ!DY8j&@lVtIF0y1v} z-vU{DX;zo&Q3GbdPT5-j!Sl(7)yawpM>2Zfs5B%D>5c7zU{1Rq(~yjI4l#BZGH6h7 zKN%)zfJ|{P&`fw4W2UYjJ)_*4gDUjAzeL{8PRcMep}>!Gdf9Q5zU6a$#k3usD4P9& zj;DVjq{ga5c{Vi-x?$;)MZ$th=>(otE*hhJBwPf*Yu|jmPFw$JyJ+d*rq8t^lj>dO z;j|id@jQ_awYDpnmB8D3`Ck?+P-^n?$l>*@?VFfZX1nkGk4Gh0 zHV_I;OO=c22iTI{)ljQTx7G-8dN6;bTh*!?BpE{;m3Bp%)n}Uxo^oH4zmU_G-Qjt7 zKFtN^hqSFen$788$#J>b*gISPrvAdYX^ze;=lR#bPdrDxB@QC0?sXwF&qKmjPfen- zzs2xS=5@<$@-rHGru6kDz%QWd`>w**j*D+4vrR!HT|m<_jH7#Fc6~u0e6=Gs#~D9G z^F92@qpnoC$~Vhs^CwLoywUN)Pw6+Nui@#+A*^#&Ue?eK0u0Bkb<1CSrs@-Lvk5Oi z5~3e}UMj#Ul*mHA4hW&iC57*LNpEHaAsIvPdJf0&z6YfV6*99 z?;PwljJ}BDClTq4CL9;=xkyRAnwxQ7YqCLZ$9MELG;3OrD~vhsBtIzfvn?@{3oysEc2;x5q|Tb|F>Sr4Dj{PgbC` zZ#_mns;x--@HDB>W{IZk;62>YA<9s$ED_v9{+XrP#hJx*h?3@A%u0|m;a<(f%yJ7w zR+g8v11%o&Tj-j^%$p{I&T~5skxL#L{m$u*7{ZvuK4AbuF(hDoUQO~g%-6|me&v{N z7A)rdFNEfJ-=2NSZ4TQfRUW2Nv1EBm1Bs0N+BlHpK@mxhetfur%!Am#s@bRx{tW(| zy+|r0+a4};e@uLK2t7V3q&{{jFd3QDm;Gb9b+h91>YXcd8AqKD-asZ6dz*h;kk^|T zq)fA{=V+;p;+!l(1c>s-Q%TeJM3-e0bvv+&Rqnnr!FDb8=z8o5)iY|mdk$V`$pL^3 zioDi^TE+b4%LC)4M_(nm*TbTV!-nlex=XiZS0>iHj}Qc3nMOTd{VA#Z;gea6_i77D znQ8&YQhAl9P#jmf)7Oodi z)B0CZ1Taq=#zLLhRfi?&>yv0mOq~>|Z1G~Xmhp}$UY9d*OoK;uWe!2^z6ZC&u-0(f6x!?Vtv_?M7ifc&gp~cVA=q+OFHfTfW3h*AvxT0r3I`M zfYX7$;6}$ph{9QqRFbi>NZ}u3#KcZ3QZFSF)pHpf25g;U$ZmJOOVjJuGz9;E!FQwn z}7dS(ApWwz4C&gsNH-^0{`oq6q{ zIFWs03~*J>Aqm`>>EuUz3^u74fxuYMe(CM9J`QVO%F7vcc**j-*ckHMG5=z#>D?2# zcVEC{$y}IG2TRIM4yF+339Zzx+rb7DM2dnW5*FgMMZR;=^N})eosLhLG!dn4map5t z=W7UGXAD&(w{St%6?p-vz@#!(^@#>d;;OhuI6{-+@F}iBZ=m^*>Xl|B6LrKo3%?Z@ zR@lS>U|;n!hol=%pDU(I(4g(`({%Y{N>6}?ej3+gn#|jj)i$&AEr95zr@bLB_!dIL zaSCxdcz)?R+sR~tb#gn0tiQc6?7MGW`wT+xHqyw}Jww7PnA#I-+?&{S?-TFiy#3{7zkIxX<=_3{=CnJ$yyMXA8{RJcIQ<{I=8ikuK6FRL ze*1|({L&wacU=&K;lGQ;_s_V|*%<#Db1|@irseH;TsG$4 zmHhAXlx(2A&+Y5)ch&#IkYi*0M{04O-~9a?Hny8w0&HymH+qAORXb z(cf=VHsJ5~U<3ZWtO#uUf9%!Y?|t*1?Ozs+ViWj#B-USl=U@HICiKUrU=#X3H7f3^ zE1L-5?{HIor}Ev*X&}?@Oun1B^pl-%!&|wTM?j|Ed3-nX1jzI|jgO55MEU#SE&XvO zXsrKq_WdY<#wYNH|LSK6^b4T-{^SbYj2|HL@4UjBaROxiTU|io0?7Qgx`4(85LAQT zt^PsN=4@TiltwC*m6H))$b)fdXp-TN$5&=B}%a6L;;vTa6 zvak4Oi8w6x1f!XlZaK(7s_=hH6mApiGwOfL!5Vv$C|sQ)jRfQ)-?Ji91Cb9Ew#D?4 z!ty8LSrGF`Me@_*+{Tp2(28fEL?{F4nbDqFJkYo5o_TnLo3aZsfT?~5!Bzc#xFPq2KIHM;^23J=N}{nP4U^B zpvm_1Oou?rkUTR)D0v?gXx`%(r6^iK<$Ok4K2#g-gzrc_$(YSJH{|^|JOe&`B?<*0 zSX7+hiIo(I={NmYCX}Wq;~41bVuhYB>ikNzD`AT4dt{+fsi%Beyhoin-z{~BV@xpa zYN~sJuT1QJ7*CSi-qZE>^kvJ>Q;vt^vey}p+SGCV1B_BU2W;A+d{w^~32#U}Sygbxr$VsoLIntkY z<)T7)yMONmB)p^w9uK~M2Io_-vRzpSlWN_GX*~>ae~PxARBEl77%A0fbAg>XKIIaq zK$ar-*fmpHZEep-x}+x2R`sHhjEwpEkOI(_#!GGWsd^hzsG-OdZ+9(9z2XvF+-Uj^rV4Ebbyw@&6EtOFy3O~H7mor5Ie$TRbw=?61MlVN z!a{uR>o%xa@jmM^tA_Bk|vg3cc8$l5^_ zTxOCb2fGE->SItCd|LAOEU!R(vK=4FW<0!}Wu)M9X%gLchFh{W-FncfzNGX!w55Hp z^0fm_uJL=?ruem+-mlnBBdZIuLMO;KH&PN!PpFnOtY&SQ#mPF+RfV$@Lvo6}9h*AT zUFZu0ZQNxd*}5|08=zt5I>ZjKEOIm_J_o%pAEcU>5@ORT3S?C|UeF*q$948YXHFa;=;8>G(MQ3Mml^n21o{Hy zqXl3k)y>`*6L;}ZEJ8d`gre?kw_9Yjl0H_k@<^S9riOJ%b^TJM3^uA$%B@CLi}?p@Cn zexEq;rGDvFgJVJ|)8C{Y=VBmUw*oJDU=e-_k5}e-mn;G6&5P@|fV00u8aQsZbo7rT zy1(TUU#Mv|@gj!ax_<94u|8vO$Nw>}UQNS%oGgCVAgM%b4D%>g!@9~J@hja?>&6Vr zC3KQ^FHR*CTqn8-C3h_SuA4kndALp;)aQ<8?bsL^TbO{*y!HhLrLrL8r}~sx|K} z`QXAdUh%fAF{L%cU3h|Y1LH(ey^rOw;nhwmr5(Np-W=(TEa1sj_|P8E7uZ5`BXQU< zqTxl+9GcCk8U?Ljv!IS^k5K2?tI0e#J@LA#9#X?acK~&9!##r+jq6! zeZfxPt;kkog`Z&O)(@}~A>qs-f1>O3`n`mB>1CPNOQqk=xgx(~OQqJ#))ujXw@EgX;)YD(uB?cuuJ65VS0Y@c7X% zRks;7H|p0Ni05PtEF7ZLf_QYE%DBoR02}g4oTkd$ir~iwF&_e$^3uXzFQl`@(2=zy zh%yK1r!jid4##A7i#Lt-?^Q<4%V76ubuzy05+i-H8vKo*zi&wMfZ&~ko$RnhAm@uw zM<{i$rVI@j=-0uKm*@hj!K)YU9juu(QP%|LW>~HOAnm~+QY4q%6?meb&|;#?;g+Pr zVnk?Q%327F6X5f}M_bM3{6+DKb5U4C@4q;Id+-)6?bTgfdQajH6YDJxm=id?iQVws_mSs$d{r;WC)M1j-;a3~Y++o$0bw$BJa+boikfMWGzN=0vbT?G9# zA+j<`3=Il3b?}nDHOSYh8EbQr3jvuMX>lWv1d(mAV1(E@CJUmk*l?wrWCvC(dz(qa z9!|5L6fl)0gLfJ7ksB#%%IsiIRmCK;W4kLdAkkPdtwF-Bmsw%c6%)N!M@cJu`ZDqH z9Gu~G_xl8E4#ttzFsB8uRgak?80~uRE*{fR*QL+Br0(&&F(;N}cQxfNw#r|LCWbi$Qzm90?)5;p6ZGKcNEv%{bkx+`;qs zmz#39y$NJfKgg|(A#ERsDATR)V)U}!+bp|H7P;BF{FbaM0OIQ5mN>dm$4z*IDW~C- ztYgD&mgI0E6ZJ&4ZJt$Z(P%N*umE!C#d2p6i^97PWHo)yfa8?Pfi97Lm5fOom`tx$ z(DCab(j~_~62{EcZScYLJ`3&$i^jGmK0{RwmBWG)WeJ|Xj@1VZ&~W)OJ&eK3YOHh(b>-Jlqe|b)GTCvP0rnfW5%rNFT(`ft1NInZE1?1 zw|*R@4UcS*yT6X4>GhrGcprWIWx@I64z{E8y^fRS$|v5HU-FfLSpyW5FLETWWGRBq?Pv`hfbGCf#L6VOqc) z3FJ>Iuhgcte|>sL6h=?i+y&8BrsyictaW4?8kLu+^gK0-LiwqY#(7o$z?u;_Xd3g{`P$^B+|MduFWvE@N-d>_xC@8k>(Rai{ zBV@gOaLWF)qd@O&|Sdk3-!J7WbbN>Pc?{y=G{S41>9}J0FDJk_R=5L# ziyk4H@?Zie!3MfH^&X@&$enVRs8&-t7{d;d8)UXN^~V~@zd8K)-qoE%^b*}wm)d-v zkmx1srnE<2O={Kz!F=G?mJ__b^9Vn53#RzNjy~rcYq7=65h(<|PjLflnhl$f82klr ziFHJw|BJn^j;kZt)`bAUB|vZu?rePH65KVoyGyVj!QCx51PugtcXtUvLU1QQkRVA& z-iDdn8|E-`ICJOBdH4O^Uwc<|cU5=Q>aJDax8xAFM4blYWmGy1ih9jDy1(oFVu`?H zG(fvdZPU_M#i`Ilw*bSmnnR6D-I;!#L(2Z_BY}sT?RdaqPq$S4ahjNQH@weAI4eI& zn$5#m~7J*I=KmF=Bms;NK`W^za#>fo&;wYa5{2 zK1pJnEhR8na{^DSSQ~}xYy-84O~|>gP4VgD?yODKC$w|ivzZ)O7yf5V_)@{5Cfr);n%KS#FwRbU$+UPEGceK;Fl7oF5o$NStcNl>PZp}v~v(+uGST;F-I zcJ9;G*X9+ibVC)eCM!gfmF)ud)rN5^x!^h$0;wdE#0ihW6ilc)zP>77JN_J5KVBbw z>D~|LBy)0}Aj9GuyeH=C@c_MiD>+g~qa*engH<}Dj>woOQyy?Vf$GD%9P`J-^oQ{K z{LttCiw{sq4f%ccNH`FzAg~J@&utcco78N>czlpvVrEW&P%cl8JE)hPa~L*en*ZD5 z&Xli2R2|OZROv=JO78}PdL6S`c(BDM$A(tu9)5GiD}eCoQL4tQ<@La{aX17sSC}Cq zZ#T+RT|2BqrZ)+p!E@2(>8ZEGqxgnY;}+zOnqtQ^PB}k-M35%wfPwgSUh^eY43L@6 zx3l?T!{5?!9qAK5#>`f1LTYF$IMPcXBAXjQ9IF!+Q9R0~4chd(md?6a!P#x^^c{E5 z%r|qQT`|ZY*IMB-cc{Uq4=&=*iPaKtxDuMSgdD$sBZVpE;E+gReEWj5N*Uh(Hp1ut zT?gqn{Zt)D(DY92Wa<1>RaMy#JB^plQXxyIK7G9HS&%URF#w8kf4aN<&9QZAhQtiV!+OURY@W#*TF8IW1!bDO;dJbEy*YdfAGjG)B!_ zL?X;ruu|geq*Bi>4Fu;2xG~e2xe+g_?7UMIr4vNbk#Py5;dhdjI|k6w;?piYrRTVZ zqHrfRqNmwRR!H*$=WRY@tXVyGA+vs)C4Iua7U?Wv=Hi}_6#nGm5Z+Wd{PZ8k9l-X} z^8a@*#!O7VWsE@)@PVMa05QhEJ2A$rzXI96@zwojjPb3k;;ofG$i?3r{7v!4S%2;C zZ#wz4_?z0lqRZ+StsKmVzEVGzA@`%?(B{0QA5%<`XLWdD^B5e7w)0?ifszng;; z=J-8n_*d@suR2%wI{eDN(weZ~KbuKmq2H5-+xiiHe0vHX;5G4h#gbmVo4{)d@v3z7 z4g#+eQy8E_K`|7kml!jE2nJUaR4<5d_|=D66t(8tIpw&&4RagT(H1`T%N zwx~n;N(m5qg*M|_4R%z|8{~o|%km;ybf#S*noUcCbecP*1Mq=K9@S-exlVd~5qVDxmI%*Y|1%Q&(h;fpFC_j4{A<`IQ zCDqe?w1)NR6@xt~SV$+$YOc4oogW-}>XwtKptv@^>a7P@JfWJ2a`W(bGUS20q1=)= z(%~qv%G^f7%8QR2hT6oHT}wSi{NyX69hm(g8DU>VRn)@bX7xiE8J+i4_fL0DY;4kt^bI*W;aTi8UCX zasfU=-F1Yhz*1^NsZKK{HtpCTuiDCoFj!>bB6DPDhN%3)E!$KxeB+qhA6p#3Tfj-S zDAP(uF)0sfqBt?j%#b-`y$~AdO7Y1<3LRkg(co}Hj@*-(f%Ku)#o%P^3q5fk)e8g~ zz{x^Uq(bmC%VHWtn*p^m`u-=AB3T*c9;^^?pP+rI3WBX;c}X5$5Kt(?3l)Xcu*m6+vMI6jb-MziHRxvaCvpt&EbcMI5;Z;+V;7cHU;v zDw&95ePQZ+F_Fi!mdv@f$-hhHe2n*<2=*+{t=8n&I&+PFi1Q=w7>}x02cZkhDBI!3 zTt#_~EC_QacnQ*p$gGbgw{^(n^ zFwFpCFd{I?+{-r`IedvkVUFcQY2yD7214rrcF!;?LifET(;x#+8MFxAYKI~;SU+)H z@o0>(*&MO^L2aR5eE7)g-p3+kXksa)LOs)c(ejRg+$72Dwwb&u7|eD9SHChkxh7_> zk`2e7!-7&iYpR7Ev`S|ubsg@D`{bD&rpA&NjFZe3a0lg&VIogv1&U4V3hD@MF1Q-cT@hr1!3YrII$?@YaTEoEu|7EL|Ka z-YItv)U#>9DHi?28I{P|T?cm^q+&=K(|H$dN+UO@3;yn%Msbt(Z9HePuauj~krIz_ z9=I>pM@O#%C)NnxkFJ07=6$!4&`|zFD-nWfqbDma0zO|bE9mv)(7eu+%A+Uw{Z*Vn zWuz}62o)pCCFhD z5O(Lsr4=5o_C`Otpts8g@i-g<_mDf)0>W?u%vV&tl{rX}wk~71e4VU0()3VJu5mxV$myb$R6D!L-&DLe7OK= zZyw92t#%-A@HO`!^mr_{-fd5MN643z@#Y$@e{7DRq2z(-)fz z$m5v)tk+O8F?3-d&yOJglZlu;YOb{xHFIP>+S#0+QxWu~cU#H|qTsI=P|KhW zFA`8`skmtF0VSVLQZUO6USfP+ah-lr{=*4@{}$pF(R;bHo$? z%Y0P~THV477xU{8J`C6AkmDg-0#Pbb>ip@5*qPwp*cSxW^3a9?iUg zMp(5?^+Hx104qL6UH~J^*iuMaMK;~k47YA`LCPtw$)Pey@U3LUW{IdKZWviAz`OtX zM3bqSosN1u)yHi5;a6habk-(F2odmr6+t|b_S84<8*5Q`qATCN_QtEFsFcCrQuT^J zOqai1IN07}qLbx10O-1UwNUJIG$Zp)$H#yo_eh}%jdi*nbc>=Z- zOWHv{)%|dN;0?FGCtI@AGp*!r-koqSmiYJX;h9s6K&tZTQW80S?W7*A&0As%Dj}>( zk;WygW2k|I{*34U;7u0AI{yrc-#TPiPH#M{)~Pv8K;E(p8$63yoOqA``$x-t&FDN# zoTOvIZL^crpFCcX(}X94Z30_K(>#T*`D7iFt87^?C1l|o z^4^RnqT|$#4zKz12mAYb$nNx&d3iQ`mAOLek3}b@G4K*K`^vcN;LWYSR~4&V@W=aq z8AvnkPV$C*QiM~sywV|`6=4Qf(`O(pVFnC>GK5#N)&T1zIx)6%pbK#Y56SY=5yA@W zW3_NRi?di-oMsqij=9Y`43uW;PUa#^Y&X@ks8Mr2MwFfd*ASpJa;SXKpdgC6W4q9I zxp&Y({iYDCumA%0FXXdE@I0hn1CnfvTl$k*_NtGfnz=~spD>@#Es8tJD0_i9}tS2Zjf^3nsB(Y3IzoY&k<_Mxn<^* z2&|xUHrsv~!1SZ-k7!14T|YAz`>O1|kMUB>&hxzpR`Bcfd-&7Ls3K;FAM6WY%`X_7 z8>oB4-5o?#1$t$))*hAK%Q)^L3wLj^$30IXN6Phcv|1?YvPBZQ|;nG^g%2v4Y1!^|Yz4RZY8o>6`7xC}F3z=_vn6Q96DnQ#i z(B9|H9wyf!0NSYii+JJfsMXzSe;2K~+vPvb`iq+R(cE2r@7(1rC*D%^$6fx*7yNN^ z|Ju>3&i^Xl@HR-}@2C?3MUA~3bo)Dc|G5t#<{vH#KiB@h^k0bmx2_8Rlc)RV*~W!9 z{u#vfKkEP1T;unyo7q>(>%!bb;3^<@I$}xxv}4A;Fs<;7^q2p9KNguReNrFo*q3k_+h= zfs!twU((IIii1S$aOM2`IdtT*7OmFbsaw$k&EP73T3(euH6ku)(Mj~m$~pOov}M&N zthGn58*6y5>Shepv;MvQ);!~C!I&EAC){wY(@xA+U=sLgXVk1krf3!^3ZFD(DmqYdyVpY?xzH* zmdysJqZ4@@zfTiVpU7vAt>iv!yAVV6W^0Lrvle_-Xq7XFXgp+N(-IlfmXUhDgL{mOfUb(Gpst{^`lg@=d-OTRQ@Kd=?Q}3b5_v+;- zOY&1qaP3^?1?u9bVV|CO62bY5{G-kTWWTGuD$@-(NGq!Sikp6X;C>zUIW}(xO05u2 z_~@myaxI~k-rl2bc=c7^?=AerN;}{=dK>Mu6=tJ!^)N?i&D)F}OMOfuo_fAxt6O4t z5xQx?Db$TgPf?iK-*bwls^c`t!SQ>NI!82h?00`AAoOdh5O!ke*nhnpv!8%s%|R^u z>amt@H}U(E!p@X*6vOt@V(FI9U0%TR;89j0yine+HGqW`yefgWELn2O%DxSn=H7d$cvuKaLdO zTFU>vbRYl#+s|w=$Z-k42zp0B3^7PMWCE@3|2*>Z_1ylw+Tp*Zr~LhA|G%Ia{cl)} z{%^iV0ick;?<|!+xU{QNK&f}G(b zJ>claP{-Vpk2!FRKrzfxItYMC{@t-unVJ4c!TpIF8(H?`dbtePc{0o%r%B)aVPZaK9_Qg?@`mN=0o75LOK|@U!>LM>{DIH+_!4PDVNUpwW6O z*lS)O`U&M0XA4FAo@^1p7c9%#5Q(CeWfg#P9#-(DmNH$zNgjGx%Gm(a?Nb8bWH0^; zbunY>zI5wp;xY5j8`gU~sKP!rCFo2rj%V85aT1F%koZ$RU9T$z73g}yBgQFRiX2Rj z_>y1alLZYg{yh@ za*E@yW6T%$$b_^QNFg1Q6+KwMV!Av!&G}Xdhd9=gy@7@Y*|GkmEBK3J{CrIx_8loA zB|_3UqXJiAO$0WIfbjAOF}L-s-Uq?6<3i|J3Z_jKU^dZEkHgWDwZ}uNgJQ8VTwa71 zJZn&uXQZkSivd2P*)hg*A8pL$2} z1#JvYUZjKOFdx0|OO%iiFUDgAw}QVoNv#BYd4vXG?V|k|qsfjhindjcFYkFf^hU)r z#6=rj-NMrGIj|*?PrAoj;2#&it1MLiCHH>>i?eXt^h8t?k01dcG)~i;c;BN{3%yKd zx$n^T^*d+Iym$ixxO@U-h|<$AVj<0S?FJ*0pa?3RXJ#_>Rw-XCUJ0$c&VCKv2V&@9 zP+RmtPFrm-UA)RYsx!uJ8TM|#_LFz%;m}Z=StNaxf@G>;{1Ml4j zWBPJ@Ff6M?2U&(UYL=(g09i0GPNo-!hixjFYpV$UIZH~s8>Zceu_4mExSSit!lg(@ zK1jX>&PE}E$X{0p%wN53me02=@XGGLUsQlH;T}ozsapzNHCChtH>T%B=zWj&kcEwL z#QQ>doBn`Wy&{E*PdNu;1k*ZifXWY1EfcxMSDPoHWj1AaLvs28teHqO!L@ZolT*zD zbg8sMmpR)!v7hUXy&H4soCJ`nR2WIz2b&>yGRE`)odXjsBQa6B3nH5ioViSzleZ52 z6OaaO@kLQRq$ckBlp343Qx95dfLb@X|<{DRQX8emUKt37Z+vMdZDD98rg_-rYFX>NCgSxz@u1q*a(V&dtaA=}w!jiIyKKGK{4&I2zmjsCq zrq;=RUN&YH5_m35dt%<%gV}&UB>BDLK`N|+`xwM4-+*;yh=4IN#jWu-A=<(ph}4eS zz5EX8;c$xt9x)E4&W+2vyqw?J4rLir3 zOu{ckp~3Q`gk+a)cn4)f&oIg!NsyjKRF;@ld;YKs25^dHFohzQlI{v;q3yzv=y-pV z7!NErFIRIhd-y_ar$@cIa|C~vR&pJeL^?wQl(##Hm>bbqc?fP$V9ud;h2^7OuUj?N zxqc!RMm_Eas*|DBeG>xN&ZJGA1K%$h_aiL>o|~sUW{+NVHb|O>WiRqGNvV6EOXBa= z&~=u%!NVSXRp;D`y3h7-QFWGUIY6}T`4;L%)xmkszyd5gcgNx>vQuZe@@>;}R~(HSoN+CRc75`Yg(N{`_Tm&a1CkI3ZqtZuC&|5T^5+3E!DHAYD zdm``9mm1>`GB$yUE<}&ct$PY+DH0@0vhe%0z+fw;Tag(@hYG_3ppB#fK`x18>P@)c zictuc6me1UEz9LXiAt$qns&l$MN@<^C2eQzPt+9R^Hk*(vApo@i0oRQTD!Qb4kNqJ z2A0SMJuL}fFKQ`qvHr-Snba~T`?{lU$lHr#w{6P#%ZDaEuZ3=xf8%jSw@gZ2GDHRc?qIvBe;5Fhs`3k0~dfhDd8V+c0N zb|QPYfG(FWmx^XbCE_Lncy<$CYK(;3>&|gt+ju+$5bJ$RzmO_Amz^sIABRez2xWn% zaR)4MeA7bKtB;MB;0L%bZEu^#J2C-mH&fJW#etH7UWI=Ix#)_;gL&s18=CLXfnu%a z<95p|6nzyY^3)iAo6NFJ9ce~R)9{MzB3rbG9yvu?yI1a=i}jntHkm*MP0v}I2^E%B zGmR%-v+(nVbm|o8r**b?=mQx(@RpwF)ZtIeL8B!qPMC>6!5)YP_8tcbJdQXHdYULi z*3`q8%K%xf?b-C&VVnVFl!f#@IB>t7Co+SCXDzz#NYEH^)G~$>tS<&puL6psx61d=ouMCJfy%1~tzbbaHn?LZ;+{lTibacP<2#lcP$; z2TdtVKwo~+bGxo_>AZ;`76TRW`>RVzJ41t5ePd3|j1{H4_e~ZtA8g2*(8unfM0u<2 zTB2-3)rU1t)Wsb#uIE}{sxkUt`#_MS<2rMlg~=jlTG&vYI$~96x>#5BbeqpTugjrN z+Q2O$nZ|#WW#2dV?qPggwv2T*aW(EjPek*#4)(-QgW&X)&U06Sh^Zs?1g~<=&&7IN zBN)euxls6{=woO!>7M}mX!11U{fieIVY6&&EBfFwjwjaG%a2mgtCvvyPu2W@+6e4syNql^%49q2VSlhI`;WJ zB4_j%4Vbz%=b}1qpalN48m~*zGk=d~w2a z$^C454L+*l#vjeOz0abWZ=eihm`6GYZTZl!`i15W3rgdOOFyV{OgHTnq(M>bLD39A{`Q|i5u0llLFu`KHoi~CU=!^L z1Q+=~BGaw`EgO9Y}-jrX)7^Z48uSpH(3ZH*2;@TdDdkFjE4>wG1SiJtZO5hVlF9) zigpQoIV=59NWv~hzzB8E>DA6BijO#aLgEIc0qVJZBjI%l#-%9mU2q*?(ds!%iIG_1 zGuCf&6B$kuxLp{$V$}q@znnheQ0PC(NQn%5{~p9e!&lV9bW6v)X0hDvETv*#(I{OaaWLx{RRnm<+$}@Goc&& zywjB&w3n#=&gM^n-A$7|Cx=c35Av<#!tWMN6HeKdR56(#ZrrLa^hGktMeB1>&ZW*K zNiolEm@rVL=7oy4QknyO$!W|*7;%w=bH@m(oe?u4hA?i4Xg*JOeHYfmE{7!UpSK6FK!h)_a15=UcGc zHnewy#(wLfqW}aY2SC`pg!9k^+pSnTvqZ^B{Mw4RjYuw5t{Oonk8YOcFc2qKKc{?Z zKTAf)*;J#+D%rAV>)+2~KMHL=7@v+BysL^!iiAR3`XJWiRcI2VqWf`I%8L-@QsEeE zKaGm^jlGJuk94)HA(UeMdIp&U1TzD>#xiG#)ZkDn<0MXd5@hn(gtekc%ir=8j-_oV zcsH1`V97k9*im8e&oewk4ZyRq=}!7cLF6Ec*p1~rf%(Q322G+|09m`y z@6VH>X{F##^-RG$C?G9>{)36Fxr4k-$<#@*{3TBQyXc8vWjtd&EG0)fJRKp}XPDfg z)JHJx0>y%JQ!X%zLf8)t8UopB{aJf@Yjq>__Jsr6cQ?a^PD>;K`Sl!b=VBud6}BPe zQ?Wn+D$j{DvZRCWC!p>qZt0ky5Wu?I^U^{t6)lGKB~{ahIGM{SVa?Y?dd1s|%(;_weA`_Aw6Fi8Zxb`{rZJ3yq&3u)F-(Z2F6q5B=k$WY+C)$c)D%~n zLhCl4F1u9I1FZ(ycoI=DVaz?nma1WUU{lK1d0WH&Da)?{P;am+RF>!r&HH!kYs_FV zjHC#Co`ed%DpcK0^JE)6XSCv2Y?Bgvu@>`j#j)loS)KY@Qc6VF*krV!bB5h}`0Eg1 zAzW%!-!oqnAKv@0Wg_;rLs5*}rb0Am<$bt<16gS$R1VdYKPc)*=)v*WS}TvtTK;SH z9E-lnZ~R~J_fh!dSVSbUhDnx@;DN(uX+G0lX)mRMsF3VZ*1qEQO=(-9Nv}RlXkB&` zO3~;$M?Ua+qH)5xjEV|96g-jkik92x;jU4LG#iuY3PCciG>dPEms<@kY<%DWWPOF_ zeww_IS2>NelfQUabugGcx7Hq~#(dqXRL_T@Zf;(8Qk_0Pv;SbZZB)az&d)?+=058E zsAoCVQmJlisjW9TCOC85!8R4bk*S#=g%VNp;!Eq+?R%LR zx6S4q5f*0Vo4eH*kP{^l$hj`H12f3)Io3$*_$$mLkUE4Q&Z+j;)u+Shp>1(IHTL9N zi)>;EO@Gfi4mLJIk>?bJlkzxU;1MYz_1|m1;wg74`a_dBs+^G{gY}E`5P~7MmZXjb zjF*^%+6%?j)bbk_B32SGJHzJ0jqT$-bt~P<(N<5{XLS8)Uk*p4WnR4Mih>Fo=@k!Q z=o~Ubp{aGnzEDzy4*1j*FX9uFV5u--Kd+d3xp;(3l9ps~+sxb%ykWZep8a<))eyD3 z&>+!?I58Ml7%|$)zFKe9wy#9UK3_>#Hxrzw8Wu~(0o!IU2LWggYFwp8BM;h=mO!jvev#>J#Ojm#+vR#Ku1wp8w?B#$v(G?uO z5{`e1G0X4RXZ%j}?-LdO@&&)&)IU3NqpKT*-@o#Xr~PQ)cTfM_13#|(D}Mh;C;#N| zFTq*=H=4ZwfQ)ywbpc$ZY5w<_#Q?6tI{y2b|4*<&03hpa#wLI(+lRmANdZ@s#9!0Y zZwCkfyi>^4AD01uz<vQ22Ke`d$-%R~9eVFDsD! zhpEi<3;7Et^~+Jv3-k+H@=FmUd_a)9-7mH?*Fpzm|KUM*EjU2-AMSkDlMQ75!9iY6 zCh&TH|4UP#K>>37FyFa;P$0(-m%ghT2OYh>s~d(mAWNSAB5wXHVaahvFrVc&j2kG; z@U=P(^k+bU{oaQ{}sCvam+B z52a0fdUhmeAYa=Z*7Gb+iW|eJfSbx?3AM@2>XMpe50rjO;l+~olb6}G@9kPMSosLm zJ4fnY>-&hARLXA;J*$qj;NW@JACs^*(~$m-;T`s;fV>WORiF$4<(K3L@a2bUqk&9E zQ%O@qB!D;(Vg4n41rGadnN}%+=u#9y(q2b&0{@H5V_=_elSsgbjjG%BQSRiL7TG6~ zbz}t#wWD08xf+e;%0p$MiP8}0yrpyV%8#J5L49)djsK`40y*vo|AUmt8%%T**A>Dc z`(Nh^{iC8!l z=A-_+zB9LQ+|lG|u~1}AD@l0Qj@O^OZiL58ecMyt5iMr|-sImExIoF05Z-yfX%TsSs&-FD7Ym?On2kg|UDwN))XGu0ttdEqJb8ZzP3fyyb z@-@(4;%8ew8pYww_NEe}+dt&q532RHhMqKvT1`qm>@3+#I&Clb)y6b~P zqFJBfBDVJDgWo>T!{Wrq`bfu5{n*)$6K)R*SN3Vxrr0L-@I{Z=GQ4d*f~Pi5$nyxB z(=XO3w+;ARf#2V-Owr=jkADrr#b42mws|4{1|AguZQ!en^D}RkBUPxYbUflb-KN{5 z0&9}VFp9_zKi7{NOjw)S80K5km=@i0i~)f8Zkw+NY7NV?V`L@!ZmdYq6kvlzzgANz zh4rTd3stD+MC%VzF7Jx^2-_u>VPX-p@-(A*h{G$&x@_#dkL8dCc>wLDqI((bp7ASB z;^ccV1A>ujxTroQd8XWTHesnbfI>YUaG{bji@j4*F-QN3457(-grReOY+0Y?wj_yk zh3Tnzq&nJUi05ceEjf|ZHvu#0DC$v7eXE)(5f{~t0^`qVmA~YcR!OW#i#+x?7I^Km zge>^1l_pZybUSGa$7!;~tj}y!mit2Vi*>7xs}t=lLkzsqzwcrzc~hpP6$D~HDUU#j zOs{gpAg~>5%j5^{e=ns0iq{58v3Nl$WL*<^R!I_$+>)Lrs>&02(ic3BdSDlEMegNz zcw>MK?ht;!Amd#^bxHHX9(zw|u&~C!v6`^3<(TNGJtM%pM=L9@IaeY0l*}uT8!27l zWPvh9EZ=h}aY7`S@)LJHt!1TJrn&A=TQ!mv9hxM98CG&*002Kg0gjVbT(EgdUb1^P zr_I|!7iqY6xxTqyxBsKajKuI(j{!eH{c$hMf;s&Na#nxkeTM4lwoX`xITG-!ROyb% zg9Ud1oRJ;V2I`*fi_m7^i77!lh7ye#;Zdr`h3nn@j`m2o5u`-e69(Vh|u4EdFTw(LNK1l ziS>dUsPd-$oCyLlwxp?9sC#IS&Xk#@Uc#?yj&^qDzSu9A9Dwj}7CG6{Q&Q*D9NK}r zLNV6je3d$M@}Gt#X$bn(FJeSGyUt^s`JQx*0R=?qS0{vYzV1!q2S+R!r)(W!nzWc(b?H%Mv=&~~ zRzh-v&o%400_3kfZ=j9TrbZFPuH&DwxtdX~{eA;~sZz<$l@{dvr_}QxDZ@?%3EG$1`6recq0LCjr=PQhY ziRDfh#r3p<)Um$>qxdWK)IYcJPdCs%pM2Z@TN=2f>Q7`zxoYKq&@8>EP z!oO!{dY5*AtRnxU_bbghM52StUr4WNDIE*8uWVWbhY)m8b#psCg{2Zda7SE z=j#&a34k0wws2Q>3gq~)gS$Elc)c>;G$IB5L^l1|JsBu@#+?>?P};1YRd=l@0VL3Z z4~iAu?MZ}sA7X>2IE!H7yAHfvVPUi-vj{xR7RXZ_z;@;x#ZQ^uoTMAO?ZU7VksS{F z7PXxvgT>Y!K&-Mn-Zk1xGx;z+t_S;CK?;VxH86MOOU1snb(%g|B>( z|F}#5fp;Yhm~WDEQNLz41(~3Jhgk+Ku};GfOJyyU!jsKCrcj-^N|28T-hu zY#9oxDb?RsnBCv!!6i6~KXQ?QCin%9FNj+o_O{XbX~+LZR3;Pqjj#})aLfN~dIyUD ziXciLT)72`dh4H76{B`B>t5IfQnzzrBR-_%;lk2fiiKv9bZyt0CQeRO9pg4~;g5(B zrXfj_3l#-U%%Z-ehfte%xFxj$%g=9|E3{+j-=Q$vLxJC;roUy2g=5iNFI$(Y@y$~4 zc+XG|*qr>yE<52lJQGcj_=Z=U!rQ9kT1Zwea05v)U1h6%^37)7*OzQs$Hd>}Db?16 zzQ1gRVeAs~=bpA>W7l#HzKX=j^r z;M42ODTK4eE@Gs684S)ol6Y236^&I;fSL=oZ(nuEzv+*vX)<{MyP4`Wcm_QrB@-^| z;$SpHkrUOSst#u9^*mnmi<$hZjZ8y|6DxhJ54?gzFH>Y$O31Wczv`2JvzNu9$Jy0q zLP;1d7nn8tmP=wou105x_jU7v9DC#YS?Q*Y4Q1jN%*EYJJ{~e;!mcIsXw5wQu{Ac# zy@S)Rv2kPY7z~+}1dkT3WP}7q>5&MYK z*h8?AFNdBvp>9BFebY{fOu#I-+R$HBFSRn4!b}@el>{cCHtt{)TXlY^;kAQ1|L12~ z@HVz)-@iQbo$kQjbaW>y)Nmz!_xPhyL_EG<{)`w9KVKzr#@?kIJfP(9$US}&J?`vb zl&H204)1{R%sKS@2<&Yct2A<6-#~KjA&UEvBrzd9mU@~RLhkbKUp_#oT)+v(Hq)bp za1LDdp?0cXe)HbAIUDX#sa@yOvgO`1*Um$Z_9ia!(rox#?=50yW#p5D6rcO&vi$Rn znTai6=s-^pk^^y6(zXaX;IX3Ro{v8;oAK`zi)Bj+ zK6!!HU>ot`oqnB&<)w+oH%?(G2#Ah~b-b3`@J0c4OGO`p*XDhn0-Jk05-XJG^`Nz? zwC}B->Xk1kA+DQASKz*SZKBDfGmqrMPD4I?fw;RiMONw4FX@{8b%s@i)$h`Ytt(YH zWYz{|@ePh*kAdWAnhy!fVK7&c2d*sMA>XaScW0pE#-c18{oe~!nW1OcGIwcVLti5( zY?&FZBbqFNJEJNnrZ1_6v6{Gs@){$?$1|LPnwM4ic%1yp zI2%Z@y$|-F$hZCKi)&{d#Mlu+9zK3h9CfI(bD@lb$_3TdwqjjUt8gyDuBpdU<7(9p zGLD}P{X*A*@%DniI0}Os|7gADrz(`Fjkvqa4-9im5X{3uPKVT*A1$e>$nr)w&WP^~5!?rn) z!hj!25JO<f~5Id}b_I#A6n3J|o`t*Keen z1qPmZK2#`SV0pg`Z*Nz%eCz{o^2Pc{*mQ4ca)VXxvmc4HC2kF7*2d|8uxJWM;q;no*1l=H6z%()3l%m66`36H_VY`UFQ)AF+Jz7sxxy4s6E> z@Xd4MD{^lg2ukKw2G3w7*m}-aUbZ}hMGuICABBGpKf8fhSQ0L%73J$Hk4!#PI63YD zv(9#}0y~&;sNU4MaA$S03Kf#MyYO2w)UphKa>5j9&5(peMqD4EQi5Wm^Fh0X4S10={6mj-h)0 zZ0Z|$b&zkutyA#RmgbL?PQY)X%0&%^U*!)tDD*+ZT6%)Md-$lY{oQxRK&MB#3@ej? z*~?ccKA`a>;|DNv$0Ng(ILQ+h168+iFm^-LW03}ls5B)X0+Gu2dYL%kWJ%GoWN_-D za}5W!^mJq-i;|26x zy5o+FiTS1_F37D9@;XnJuvS~l#Y~&)GjyYIkSXq1Ym(Zb;ft4WlUNWC6=P}GFA;FU zFsu8$dGl8e9zX0^;5ZQxw|ascWI+WBLI|VvEM8*nV?KL1a*+w!a46`wRfFfS7L2xO z5ijrg>S>(p5IHHW0F3^V*y7pA*>t-{P9&vFxM)g5`>z^9%2-GBXp^Z6@hrhZ=6%Y% ztJ}DZ{3{#s!W*5`{Ciav#OnKUJ|Z`iq;0_mZ;6gP4RXMLYcUW~D1gsq9%?>4=cG(g%+B#&>I`cVhHqBtSYlZ~5UpEM zrpD+dBCWMpZ*|{df$y6iiS2$Vf7w@oKy;q7Z#ayQf(g+67z}kFk{P`Nmr`1h+t6%w z=?g&(Pdw;=GBYVtn&!EC->>amO?YNY)o}v%^N{1B2l_gqgDmdo9NQ7u+ixDKVFUmT z9n+YVR&);PIOuu4?;}v1<%I3JWK`0M2Tx{kpyuap&K-JAxp+E#AEYGaB~5pZUbSBi zsXKBnoKLUbq@KS}d#jlJ1>wvN9@T#^cw0}Ip2ry+^N0l7lQ}@}kYRb!7AWQ)**lT# zL|^L(&JGdLd&?_&O+x>D?(0myu?J%YVXYvB`j=qn9CyK5f0d4Z9c=woMfe54ye{0R zxeeI-chvpMo&L@RzgGXw(W^VZ<F)OQ<00oETgA;9{th8h=O`)9G71pd-kNdJzJ z`u}bIv7879-&K-+ZzKgom~N*8u95p|*dO#Y0A6G8SEZ}3z%MBEFC|c%7LXy{FE(FS zZw(Om!vgF|%7MTi24Gi`3d}FTuJJ$pSbHrL1GCi{xJW#dPY$3hxr%i zhOdhMVEzTF`w0#HlUxI+kvn3ZpcLXilMTvB0;nMW(kttNOy(Zlvc<`?G@h#OttEc3 zRn1+6@J#n+=hS1#6OIxKD~1fEFR}xT`Z`7XzA$EZ&8<7m;2f1rb!ma$(|t76FmQ|| zfdy}u9&~KHl2-qAeZ!65dju~^|EQY~qHs(P!6H*BSk8})74_;fNj$dm3v zEZ~07dV7kP^t=b)@75VD2sRuPatMw*$ys*6u4#v12O-x)U($$5&22GTQRu}`r(xS` z{r)_9PJ(?2YyQAWQx0h(N2r6vb2%^+$n zyfzmwQlFy8Df8U$X#r!=sP7Z96fHX958(z~o9PrbonWv^O_6Qhv06d2QPM_3&1Jo+ zc37kMscq~<=C2NMlgnKV#?5r`MpE81atvpQTZBar7btwwRYXqAEMf3<3; zjk*O-?_t~Ic;*)J)=UFII>lCgJ2_EyuUO?dy>Dibf4!zGVEYo2O~$~!NN3FF%<0Ii zJRm`KfzVl;3tqHzOPO^iIUE&@42yh1-YELS@C;sM`El@0j8ESB+juOkOOFsLtyH_G zyn88(28od z?jl0*b4%ABl(g$;AxEr;)AU<=#1|3^-B#ulwE|Uaw_J;ivim3(%q;Oj7sGWgDN!`zug`K z)YV_6O8(xm^H_}b>h$P8E)%*P;>^rp{!fhQ0)LL0qLqV?b$ zs;47DS{0=upC%?<5b&wVb_Nia|K6vc_+8MGQ`2CIC9<7+lng3PmO287s=fBavjKAn zQG9-kH3hF<2GX#j(7aASw2NJSD@bXcJyuRL_l#Xn0Om50GcuTapGSW4)tsmYA)k;R zDCw(_wV}gPdn3JT$E&ON1;7Zn+6(>q>iiJ`#>9N%69WPvk$!-XQa*4{2h2%N^$uF` zz18;yUVQe1Vj+1%WE&8v@7)8F^>LWW!C%%2?ul+59MAZnNW4J(mQ3~V`#?CPrRo8| zTak*Tg=MH{m6vH>(HFJQ*D0$p)wewy$0w5>+fka+3ClTt%Z|cd?g$`Gb)F`m%^f<3 z&}%GPMhKWVLQwG}uYC#YQI8P@j<6mg(Gl@kGq?~lz&4MT4YvvnKdGNw|E%Kb1YSJ0 z+~9Z#ycr1O8V!5BPLvT1GpqalM%1{;V=s1jB0OE2>IZ|xE~&QQty=l=ay_?^(mx5c*H56x zk3ERgpTfv|@jM){m(fwa;%F^e$93S&k9epYbAQ8Mpn|Z}4zwC*Iy)s_O?2)tU1{H* zp}S9cU|C^(=!BEbhM4UA$BS$6_v#aE&tQC_v z6$jS{rCGD-a)6YyNXpY_hR3{S5zzzq3Cr4p-#TW4IN2@w7z}dtVJaLT0ydTJ2_UX| zSW2X;Z>}zRHK<9k6d;TB%I!4HgpS*VZ+=!#}k2 zd5u7Yg%ir_0SaHXyZaZ3(8g(-Q9b)70cdCxzM(ZL=}}W1p$6E=NaV#WfQdIG>T`1FG3|~h3jL~>d)o$N)uVKT(VsrfjT<@Qg>$D_8Tq~MFf!p-dQ2+Nn(r~tJFra*)YQD$s2Ai~rXKOE z8WNnbt}xB?1Bk)SDatJnxbW2XM$X&L-NvxO(w0BI5BKXG{z9lZCPJw?>D3b)qn-Oa z7XIF7&1z;ayB%N5rtD$0n;-S+(VPe{{Phbs-zsJ%d33~}78@+}%Sg8-&XL%LzP87+ zF%>)SHUl=$h8<_4W#4Q!d+zAi_eiHrOhg{APGQ_*9kS@3QlK(8Yylp)+vgs`r$RaK z-!gL7&MAMNLI7aBVWA1q2tZ2_2sLE7QVH&e8iE|;u6JJlCY1nmL%*P2|MllT34;Bn z`o4PjzY1CVE7hK>wg7@6Kf^)*p=*rpukPjlxA(^=%zt`(07C4-zo!8IKwPdR1tiKp zFqdml`Bjy>R**q$fWAP0Yt-eYf(-bH?fcUTGANthopeTKrW>Yu(hAb{B_L0vs}wv* zGlx1O$IaVM1rAz^A^m#;Oi|UM(XV1EsvFthR#*<;9Lcg1(z`VbjvDhaxL79m4R#kA zG3e-L9`LC1rcj;Y5Pt0Gu&dgF1Dk*O@I{}AMdP|xMVWb1TM4|W%=4Gx^6JmpGCIy! zCtx!ul1cWrF}p4f88ZshRtZ7}Qoi_*;(f2jL%8qEif|vUlXrH!&tl8J#Hmw&?Z}+b z<^ns!DRhKRfr-OHlAsy6>jjmsbkJrQJb}hh`r~ME*@qz-G1ABgP;Psbui6|wsFAhL z2BBtv5x{d(jM{;vb2{hyVY);Cg|o;B1AAZ&Igu;^@P_5!BEvsB*Fjswkn`lVIp`t7 zXQ`u*?&v~~J^6;6g!Et|UreI?d970Vpd%H3DY~I5#CB0xE4tWWmlRFWqS9CYtBs)qHC2X|6CEJu67E{Ddbf0`)ZazRdrka^%deJr!iw@kAi6k7N zur`VN{C;L5VAIDT_~8Gr_tgPiZQHs6(j5{K(%sz>(%m4Sbax{mjdXViNDD|xcXuj{ zlprl3Ao&*T3@B9O6uHRg1%{Av-bB^(iF}{IRl~{o4evg;WtyAJe z0R!j$<2DTJ-np7uCG!d_>?}~;o-E?7%;JR{QeR37{iPRMKBw|$zDN}a) zlV3Eo`ZNzfyws^7@PrS^TW72}4>Y}rO;Z{!i_N?7?Z@|4lEj&5tRJKz5exTV(b{$2 z*V)CL4TvHKZ;J(=OQ(=K#nII$%xr{y5T53t_uZ|LDUR4mcfvvS-qf(-LhdU|`Bx#z-!UTm^+-X&4;d(IEDDFWb8py;R09 zza=3iFsFWkL;l{fzPkqZ!QR?Z5P?XVdR7b76gwIFw`-c(t)SX)LDWw~d z*8@== zw*4G{{#V3!(ces}#18D=fZqX--}bIa*#9=#!2bVF(jfjBGW-8f8M)>1>r+>M{3HQ^Xi^gzh6W2zevwNA;W)KdS<-(knV!GGTej@!vX3``rjPR zk(y3aV+W#ihJ;>Rn&;Wkr%bjXvWm$?aFCHIFHC5Y!;ZDZ!5xK2o&~eK)#gw-XUosNOfDjM-#nuOT2aD!v|V!B1}e$;dHg|_inRZByHp=1v-s1ILn$hCZ22lh za7GlglsA3D1A04x%leSC&}4r5f>6KmVA`KwDewhsc1ztgG?nK4hSqgF5It<=mDJN5s3y zDB~~Q=u=~R#7m4pC-)X(DN!3`p_=kbd+-XNt(!ENIMU&$1)XK)26}Q6lA(f1@Cw2; zMO2IqeT5-;+5j0xV(|HCbSJknq2fN8vKI7GfXjOnoG-ctQh^b;y4gmNu%^pMP84{p7zd`!pd4{>Wp-w|B|=wEJ_$7QO`0UC`yC2(w{ zrss-0rZ_lV=Q()Wlcwz0k^6G-3GsDCv1nVOHaUn``pl@K;~^Q50~0-$TAv1?!XO{M zmxvT7R)_6ysoc*nWj`#NXtK3U){XKnzgMSW8MPtCyJX8g_LLlCeyV!mWtxm(X3=xS zGzoMX5yV!D2WQ@}?}Owh=D3MJtuC8+4xM|mZjqI=+N)6E?B3S4Q}a+7($0XwGPgQfgmL+BH`U%M_$zVi7YWz!f9zawPnY@R7i3Ht(bOU0c*B zZ2ciT^YCJj7evOB)f~9kDpr(x6F9F>gkfNO*v(itEI!+w3c{mkqUjzWnE0)N7>9_# zrE9`@_@od(BTH76^`Wih#z5%^=2i67*5Dcy@-i~9DfSauWPWM1)s5uq)+08d#V1#! zRU@V_jN_9vA>XR5aq;tgVoT}ng3qBS?%dh*#rGQ=JadjmK-YjAGIPEITwV{|x1(_5 zjv+gP1P0xOYO6FDav2S1)-6wDj(1rY08h4%!4hPV56VT$2RQWXNx!3U-(=sECGTnq z>7lZO#r0>MZxz(QIp`AEP0^TZwc}1;>!#TdioTtJsn9>Y?2sE;0#mU%23tBY5aFgo zW8Skh^OR#&g4lRyI2NMY?2SImj{hjdGiS;Ar)DgiYH`j!?NiEvc#h>J z)38OYvdAl+Uzz1F#p9~>k>+D$nx~ z({de-rMDo=X8%TGDBBZwz!226+Qd-B-;R6i#YIc_N*n3;vp8=WhMlXwX66Ichj8H4 zS~IUe%pJVf?xUJ|vV30BSpYdLYM*rFUw`r1iw={=Li71oG;W74{ZZcudIrC9nhlka zmC9xIrE5;*C7e$O>FgM6CA{IUm%!wz?QZQ*yC{DN=TUyEF_ZC*FbFftO@|)^{zxFk z=E~s*U#SC=ey4lvBi@UPm$=>1hA*U*E8!#guK47wqih}G9+FH+HXLA1e#T+?>>@}N zGoHQ+%d$L%>7l&k386|+zf>M=C6)>kujW1n7Mf}p=q*8xbVSr ziA}d-Xz?0~DJbEPO-{b-x-GoDEc0Scw`suvgJZKXeR&DvqcYYu4b?}X_|}vK(k51V zUY;IU?-udCIV4sjbI!F}*x**gt2Bb=eXCeJ^gEWHM@!ekK{){Nnl>(X;<7ggCE<-# zZL_0&TT?2`vca|g8A->Dqb&oN!Y4b&AbrP8i?=QVcLY?J8Ge}z>Y>0ga5_j~=Xo!- zz~3!Z4en5P<)TqyO6)zz(`ak*fJ`OxLa`_-4*zONnPG!H5ybvzroLrI!7z%){yB|W zGGM{SPguWmnl@-}c;sJrt#6T@`5)2U*Y`V!Y{Pi{j%ejYsL_ZZsC@0NDr}7kuAZ4< zHmp)^bm1d?STEBFGs`Mpniet*((F^%XYMWy^PvU@hhD+Egsv^fj=`_3kW&v{hBo?# zASCp>r59|9VSi8g#d<9;jdR2Z3ihxrOVxA8G!4cwd^|xamG-$=!16#F^wSgYNmL!w zZ8(NEezPEiYGaToa0A_C-Sb@iS8jXPAw0FW+<$n zp(#UYZwcvS%Scbw;=VukRN?krj2CAi4dZ%x&U*pExL37y>mc716Jos)Ri_y~XyHu{ z2PBh*c~mP1o~22Z2?hNnoP9H_0z(x-T7gMfBYYD{PS5 zCd8P(q^Uf8nw--Pe_O1r2qqKBugmP@);)7x+7nNXZ4vk^xO~ceVYyq2aGA4avgNmZiz7y*1Nq`3|w@# z+ia&l)8_pJ$Oj|+O>e3zk5GUykcIUM`Cz+S#0BGD*}>i=Kxi2r_5 z|DaKSaQgQ9|5W*>!=LM3o&HybZy1>A|113Bu29JTt$Ga1O#kYy@KYs3o!vjEE<;b*^Fm#=0spdep;q1SlY)zkpWKYaSH`v55a zaO%H)(Z4Cwztsm?2$X-v->#Ymlz+$HfHqu}Z*k{j{KNIDPz3%FHH@?73P)~w_+x=F$(~eK^;=_!f0IDDNWu3@w!E` z?j!wC1YX2YZ9b7%TooCui&h0$9X4*A=PEBmA3oJ1)!qnyP%0vx%Uxtn%c_4GDeR~) zwaq?=JE9o#!bP-=CsZrL4rKjImT#?xG5q)bTxjRlm@0(_>CD54|6hoZx=@ z=JR#uC-4O_a>Q4$E5~ zOqm-1=`-Ix6p2rXs+eJn%Bu1@ZIMdn$P0*+Q*Tvm#j3PDZW(p)JLjKrP4&VSwn2{K zwXDMLxLimXsA!;q4I8tB3trwo0XZ>`Y`;x@W4fAEcLGCic(_T0%OQXfKs~wS0mqnl zqUyWLJl>7EH$Jv;PkJ2Y@N1v%KW`+-!v1jnsN zGh#OTH51GZaBuPjzvZBF$6*$-ezZklQln~1D9Er>VZ<`@MHUUJ{Fd=TV#6H_G7aHg zbYSonQk6|OJap$6Y!*EC`DMZ6BkQaq$J%#P-_4h3Wp^ZJ_(V+0JL+8>`Xn0$orqFB zkr5r_C%(_xZgPg;K6>*xruUKGeCR8eb1|;~>``8Cjk*r^k`FI`do*Z}VCq5s5|HECsTrZLzd>SyfaztxI~KxM85*-;&$ECmdKZS7&GonHr!g(Y1PcOP~GB4Ez_!t_*DKKSu&EU)dS1m>F5{Y1r8A z==TR0C;pj!e?QAr|NP@W5a|B-5B^6!;a^?+{3`^Xf5tNy82>GE{u3m`|L#yRF!Iy? zflPQUk6p`jS3?EJY`@Ehz%@YmdN;p88VQ{H$;$MnWjG+X>TY5J;|(!E31EB&V?Yvg zNda7c=r1D%%KR2|+%8!a1KBOmH4wh7ydebjG|_cnb6^+$I<)YfK>~G0o4T){ji(Nm z={-SwqPCaa3OD%pE4_i9`J7xXtrGI%YCrQAz1>h^iQA$SE8pXSrKgihOx~0WcWUdU zuMH{{6mlcBc6})f&P(TLVYUh4T@_t)tdHq4(B@dY z{w-ix;QJ}qRZ@LgC{cFr&4;Ppzwa-i!u`q@^_4xV01SXhUFRyP+;90qH&gJ~)7h@2u%wB#o)_AR0g7+U2_}gTyqBg!k?3 z<$+N5sjPf3Or&zj8|=m+c_q%^07|=6c&Qa|w$MDU#Tr;DZC~5y^!y#SJ>E1S>(6VM zU)y;)!Rw#y?tO=}e7zGfDV=`siGZ|(@*zACP z^*R|xi}Xu_c7?tia>71I$qu_Jt}J^L*Y6S81$L5a2Qw2fYbLsVmeE`e>Dt19LoU8$ zY9^jiS*dzWyc=aw;_n#u;iIy&4)IqX5QnQ;$pm&^7AvOnAFvq^l*<$us@${VL}V8K z5HcRZ>q+<>k)=bRlbBg7T^Ej#y6uCmtZ@C#w{)aM;^0hDm90tiZ4SY$5LI^yO5NBF zg39gvrQHvB{bldlh%vWocBgXHgx~ql%|M5x=mAm zR^P|hhWxR)2C{!jlf^ia_AM?X4dl?5vvM_cA2Y+$e(f;=Dg)V`sVLxI#K--C3JdL1$#mLfqh}*L?1G)V*WSkw+ZdmJ} z$CE+?1|B_Njqu<&(gNM7L@kb>R1-yWhT5;yWc0qdv(r1WfM}ephoVC9L=@4I(t>!b z#N+rLJUyTt7r{;ne@I^sn3-(>wx^5QTeR{AiBXsrZm*Y9o!pfi+sxS zQhs?lHITgiC7e>YMZS5Z$msDIUgK7VOeiZsJh|k?1*YU#aD&CIo_toLc2DHkyn|B| zsg}ij(unCUStwb_4}*0>t0Ov^$<0<3GDXEsGLV}ij7;xqn^z`b_#K#JeSKLKhQbMt zjJKI_q~UA(&}qhahdQ!umj38_16K;fJ}B9afFS}R0Y*Ob@*XDNq7C_T8e$>PyeSOw zEe$4~vy;OQ9FJc{JYLehubWUB^4-P0ynyRcEAc@AmHYXM+GD}B`VJ=*mks?!LORNZ zFgkF0jrd!;@~$uq(~Y14C3$`u!2h{+ALgY8RxktWj#$p9;Nj)Cc;>7gT>ymMWaO3U z7K@fer#n%w-2VH>B2lIZobv;f77l%4Pr9#U{Bdv0FsGKOEe=6)5L>DUHbkTovgP4l z8+|FL#%IPgG!0k{pOUIefNS@CGYAtsh^IqgTjC^YW%A)Zi4(IVilF1f5JAwYsYCOw zTBHvz$z(hNQAh}+DD=6>dSq@M*iz9P`<^m>Fz?(U9=*-yyQqT0`wrIXcBYI#3(Odlt83 zN)mwoRu|Uu)mv~~7U2)<53}HRbapzlztGt5S<8reztc%cVLvAxqD^r~IFMuA93Hla zF_Rq+p2>>igz_>Pl6ax6dPl(VlCDnv)8=)F`i!=5)g(ONwy^<-#P1}2F)`l^ z5(e05f7=sJn(jR{-{4U|;~~CmjP1)5vnA3C%pgz74-}8Z;Zl47SVrlvO|x+n6t;CI zqu-eHnir5hu~+Gh_&gH$@yTZ#O`n+gijJtbp1J6_ISzMWto&*0MJxm(Sg8c0=jK*m zyDT8FARdmPSr|1uL(~~v-#~Oxv%f*sK~VU)YBcoYqY7S-O|z zT}V|bq+6ag&vaL&)<74vPd|w*<-R=W>QGrX^X_7^ibm6E!t{6LTK*}WR(jPV*b|*b zqQ=KnTK!hp^HMZOIzx{96gXX#s+OKdXwP%ENi;WodaFqe(dj5Y;}5*XTjbX2y8qx@ zs-(jWPuonw*sg`+^rM0Jrh2?V8ubONpy+hSeuyA+b>~(+%F({Ng_q@#)v2n9w9jf_ zymfx-eBE&~9Nu6=QGxFhIlPKEsIXlgcM!4f!C1Er&>f*7hMQ(krBE&)WF1b>5pa!% zN|Q0`UKNwmv)XBl758u_m1`jqdVmZitQC|V&zp|oWHcIG1O2Sh4oj70>p6OH!={O0 zkQ+*X^c(y7&o!UpeKnfG>O9Lrn9V+>bZB#BeN3`?xcbCIgs?l`4#)h3N5~{mq#0qh zu7abh4T4{dFpaYLAvIt6n|)yyuavNC3l(y8MiJbiZ)L4fqmtdOHu-C1^&mWxT!gp2 z?@vm_-&=PXIBrZxsc?WS93PN$lL;~Sdg7X#IUm{&&%NYANRxJyX?F1PO$g|Q_?9Q! z8|4$?p4l(E&gwhY?}gZqNF#|$Ii86}xeS8&m3wNB{kSF`=0M{SwWGK^-6DFEg-lwcQN8!c)8HQL8DV~^l5$De!iB{8Qry)7ohar9M)Ia09*Rs6MYdZ@Ua)u_ zU-L^-m7Z_QX|1k%MA*|nJYK80{QQ?SQaqJeZ^n)YQ0t2<6>As#QvotU5J&^br?+;+ z9aSSBFWU{FABY>dhPT~?UTP70H^(38J@`C-ercan5-o*5)U%b%KveH&r@9!JC5qvG z9Fffz&aWm9xdE3-|Ha7SQaG14 zv>LrujuCG zh-ngJL+yWG8Auo&(`rv_=~v)g3qM}gYGM4{w}Z{f;4;(p{8`;aOW8SykGy2{t^ILF zCFmFS2Bk1{7{HNF=u*=c8_Fp1L{-K={n6UROA%B~bN;VZ^$qc_!iV64^4};3l6D|^ zTZ>?DHX z6sCEPDX!Rl^_lm@*+oeW3~)<_2D(AdCBo`gTR3F)!;((8H+K41jnm6y-&4@wz)r_T z^n?JwJjH*bBKV@e~Q z&B1iX2PpHSv2jDNzqJq-97JRWS}KRQ=S{DuQgg6l)T#0q6=~__<64zJZ5@(YlPok! zUQ$&lT%<}QCFu~5$7zxpU^bq==ZnxzK{(9RlVSk-9go1;$A(0yPbv6gyWy&PtEh@e zg>HI>cj?|@JlYu;RJ#ZXSSYGRftn)TRu@Yp&f+kFF@?)`(jr%bU6$nuSR|tu%B_Qa zN3G(Q_g4ZY{}aG`aFm=us|DI4!an%g?!^L-G$3v5Y%PRaDn^;25647++emN8i)Mw@MV9@X#iA7XOYPVLC>q?CO{`BY0=w}F zaj>PKqy`~1DT@S{B+<1rA~^>?yPqgcVw{7t9gE7``u=wn?|&hcj*z$#nt=Bgz(|8R zU*ML`srqGV>uBu&7PAnyt%O3;?zk(fUw!4q^F3&^`1R7X~ zyXW!BK3`!=>9-jh!eR^wm`rowU%^ui*cMZsDR@tG*%qp_HuI)np@qKV@FYn0L0Jh#3bO4 zWFJ5g54o^b#I&wlj`wxVh1gnGWNF{`&dbkzB1%Q0_E8`Hd#P@Mc(3Z7ZSXU)2G(p8 zy{MJ|P{w3yV}T9uc-vj39)RcImrh56Trx3`4PH zp6iTk@=V{f=9`Wc)EmPV{<4lHHHpBROK1@@4ga3ZD$V0^VEa7}gsHM`k7@_g0L~np zCZVSLd$9wM*?`*pCJ)Eu@NsW8$*deQZd8d32?;YI#sssyEh~!%YYv9h$WY&T&Z>gg zFJG*+Qdu2kJ2XhC&7}lVu=)~JAXLD1zEu0I?!xjAnIhlXjdzve8E&8_|G|t4LpLha zc8Uy5K!QY!?v|VRh;NfXh_Zumz^ie`=@meXt8N(Y(%BcRQeppAxVIjWDF}i3kuV87 z_5Li>yh>F_AUHwXo8qun77X7KosG6XwzrJsYa0kPS04pTwkO8ygxWN;!0yk5+-J(i z{^X{F+cB%T@hF~8ZM&>=83m%?U}~e%9dtXw{F8WJn8WOwRT*nFmapN9TBq0y5~_A| zO$X}`1z*o2F{Bh2W%r)bin8_EBYmppUxUsPL&KF&_JOxXClG;Lj`XOq zIU$~_a!5uVmws(F$0;Js)<3y6npkE1jp^l5AA?)id5?A3E*3|Q;}Sj}?~}F#=xXs- z`+*N#?G~TfZ#WIe&V)4Y4i6YvSm?a*T7CMhAf-3u`E*U>Ekynd*9m?8LfQ+Cn?Y+= zu3G>T7VDMk7SojnkD%BSMN1K}CsNv0_;g}=`WAM^cFr(#ik3%s5kpKD(o-}=MvCx1Wq^|GHX{k_ib2e(!Jaqdo) zepLSR|F0hN&-CVE1K7C#@qRV`U8~8@_1OSB$=zDS23SY_>zZu;BL-VG=AXG^*_eM$ zE6&F9qup$G=CQrHg6-Diccu*Y5Li`PY9mIB2eZdq4T5h(uf%DS3! zK=Ils>*`uy5&^}(`Pl-+Cs)NQnd$mgz%&AiKdgaQa|u}4ua=D8e!Z#;I7j@g2+Rtg z_`_Z7YFYrr-&+NYH&Fb&RluMF#ot>6j5<*Ky;Z>Q0ma{2C2&Q1!hGGTn}M4^`+f(o z{`&&%9}yhVV(3sQfl&tLoW1qfnG?j zY<=(pMNbjEm#qTL2#mDGoTv8^i}CbA<UfQ6TJi_vdWDjD2?Y z{8Z|Am*HPv)OA1W`PTT_^b8Iniik{C4aCxOXk5$Ju8yFJ9Y;(^y>EbUx|9XWcpT)Z z#>Tc=2$##@W^}_PlsRHELsC+y+pxzPyN=HEzev_-%1+t!Uyfc#7}o9^kpa8qLVuxynoLC-dNJcR!~PO z(GZ+!71GVH>a`ItX;yuJO7yC^VQ$E@{=Q|rtMD53(502@ zU;R{cx%R*~?%xluFJ6#sFH_pBV>g%Dd=s<*bAidNKClc9Rs&>Rg6(&^Ky+XGa&-Ls z!O}a z!QxiOd!sfiL`_tN8LqVvB5v#KtYJdG*A?zG(Sj{Nmv#kj5+y0;4++Lin=-D5ViX+9Hm#zS#U2I#z`r(6Zpj-o z^BA^GF?~Xlj_?30_t*hbobNSRVfi}8yIEatyU{IkRF6I>mVr7j!{{pSoRxF`LMf%t z?q!z$X}F4G?;zxQ_-~>}y}}=kmT}v%QW~U0bW$c46W+wz&z-aJE7G#ZY~7y%AXSInM5VzC3oT z+zKhj666`HD9#i^oTm6pEzB}CY@4zOwFL5{;B3aQK$~sacv6aOLA6pYbWVZTVTW%h zx|s#Jy9?3MTXHkltqLbA z-2d20x;h#U>eVQgbk?U>>0DceUD7~Ag{P{~m2T}3 ztCcF6i;ZXgPEb^mYY@JorFYEg1a7@?FRA5|e?a-C&X>7eeV=cgJ9oq$nVD}|^cDCK zh#0PeEwwW5fkYnJRjg~DTy9JSOj*#y!~o{cw^zB_DaXeMs1%gr9#4%yq}Fu5jpvsWM|o-R1j^nBn#|WjcDVq+emJ z>-dGGg7FB$3+6Q!sK#aZ)(k}=;#Ql_e<*H!y9W9uHsD0ZQ&9{p-!8gOj8?oOx==*~ z!@s2$e{q+jrgGNn?H)}aEwH(JHI$U1SRV~!)NQ!4+crJM*pyMU6<3 z`(nw%Fo@}!eN$fVnQuXGWsMlHVwrNdo^TvFpA0}b;J9^5cHnlZ!FfKYe@5&m z@wCV2L??FQ9@k@lpM3fPq;Rit@75`BNBEV6{-#^j-y*HAmXCWIrl;Ik+mXXY zmIP|v(}5x+SNi^AWaV%OkX0#*P2sKTk zs8x`yj!jar4iWnh++G@D{(IB2A?9}YDFNylJB#g^!tG z4RpT+e+RG3{&E2uwa(u@RRl8R7&cxqzg@S9JwjrY_!583l(`#03U_CjsHM z9Pb_N9`TWmkoh5dQ*1t8vle+=6Zv%rnoMG?W_X>l18YWL zhEHSD25}mEWjA$e!%9>Vw=3QW1DsdH*F!N&EYyInkG0hcG?%kxn?0LXk60NgIeBYC zcGW0fj}T-^Tc)(8Wl>JA#YhGW84S%mp%qKwGT{U~QTPH7G_kIJfJyDo{9KTa?Kr*@)A$W`+=R%|r16RVg?v;Wpla18oo8t}&gOl3_{9T3?;|vfP%&AW z35aHxe}Z`wXEEzC_`GA>1Zgj~%=}>y_<)~vxnzi_Q6f5DQZ{~WTjPYW?E%=OLF`a# z&>(lr2V4FxyGYf%E)5?&-;(ZYtYAOqVe4%AMuO-%=d@w_4wKXGEw}PQP=$MtQw_U2 zrUs>}#<&rx#=<%1XU`YkZt2SF)ZKp_?#IY{!|ff=8GmzoXTJ*BWV##f2hjIitAl?U z_xtmJ)PH^DA0T6YaQeT#-@kf|pI-c@!++*m{q&ft!+$1z_^*WW{M^=S_v``}OC4m+#h%|0?~&K+pd(lk`=y8R!M>f;9gtydeX<(64|$aO#g};I5Is zD-`nja{>0LA28(gHvyD?;E>my29$pQk=H#2lz$+R*N+9tKcL8~XI|}l*I48)XyZ?| zbsnYH>#YdkIa`!unTq)A{ppilNAL>!T zrBETEAEy&5E~)#H0bYgesZRt)c|C^3iiUz28G`EEKKQAB5m;~<$T5sIznqB* z!v8dx1VSVkO1-qs#tKC=5Zaanu_4K=IN6`!9S)6m$g(&xy6h`|Z;z|>Y{ z*)q1eo(NNN!xX`qHtYg6MkZ8@(Zgc)tfXglP9I>ss3)o`yB)0`ldaJ0Rzv#KeCf5s zUZN@D$JUc#f>Ba}&n+^&A9=ot+s2;o)*yZRk$mZP>Qp3;G*b*&?FR;ITNC*Xx(9np zp+L5zW`D`g-@stGELd*oDUP2y>aPfrWV}J}0SJD;r-;u1ApU@~$b2_x@p^25ulRqZ zwD`}IgYI(ge+x+eZ*AZIs7;tbh(+KJ0{H-j)8CA7S9Pv-v1`-a&0UQ3Cv(@I-o=0v zD0fny>@WdKD=U0o% zVdyGxoZO#3@h92DX>3F}xiu2)>{TxFQGVz!w8gDMJk)pX@<8MUaWax8;+iv;&)y)y zGm0uHdz{Olbo9LmN?zpRb4HN8UjRfAv%iKA*)%34r}f9ErhtwH#bkJ0Xr%^Cf`qPh z9nvDG=5C$mQ%S)joK~}P&BG(>eV)h+Id(>{o+pW3J}OuZ#PZ}1A0}v*o~nI>c2oo7zurHq8kBm4@Xc_E@_q;uY=B?M@vfOX4|}Pu+FM7X z0Oz;OdegAT2c0oVQzy@O-y>pyaViVuc}#q$`~U)$FXK;EaPON}N6DFQ(X%dyh_;dg zu^eLo1cwZ+qB0BV?)NcXnucPIzWak#^WD~P+aMtVlx+CWluXY(Tw4(gtTq^Q@&J)DblsuAYsX8JEF#hn{_*oOQXZw*Rhy{ql$X6U9SFawsZY)8CTR#UY0 zuz@IkBG|$6N(G@Dt~Is$?x9_TYSH#U!ibOIWh>z+$L8PPbSiT&DO|7?(pQHX?_&%Q zhJZ>9Gje?c=;SFQW1EM=G0Ha1W4g}4enl3Rp)20AFs{vmCexzqi#YsQqs38~%#bh#R?5L%O- zDUT);jPccC5CblMoZdhv;*(A$A0ytEeK{B^@Gzs;x3fcaWqKa2Rp@xsIRHRhOE^FGCb8M9v13sqTIJ z?$brs)UV@9-mb0t5I%5jG2{$kL_ud)7@zU04LY}Tow=C|Do9C{&T87iR}&&GE1tT3 zcviV8hmi4z_*1w|iqj%3etzUfIF!M!O28_HsK*+pxMjCDjv+DHOVMr-E$CVAYEWYP z6%mg7On(&+rO~9+;o-SomzP)R$v zd3*NZJJEv`E0;sB%2|idW@&X3$h+-&H#oZvTVx9%S$Czkydp0X|+7Z5D z9HA1QsKESJQYD%qfn(^vfLt-EpnGy}8cQmzxBbJGyB{z_gWA|3$k5nt5_YG8H}{7n z1}fk0-qpgqADyBv&OscS`<(5QxsD_E!byV{(RlQt1N7FU+UW@bbc z{vKR7r==bP^iOD%nCq3)Q{oh~jV(f@gacY7$b#vpKAPkzla?)hHYAwobEFN0x##R9 zWk48$m<{*$8aNl;_rtMt*+GphdDKV&7~HkflR=9xY#Vx^3&(kR#!HqLpIto9cD^Cb zQ8WtQIzR4cG-A9N<)tZmjrrDZP_4w{c_sVs!3czBK|HFFt$c(pp(`RQp+o)X>?kHW zTmTVe-wD>z?{$*IlG*(W{%4h#>6HUW((a@oLkn8C(`L=8xI4LLFBCm@^(^8j7ALCa zn1GCChB`T-umzzVj>m1sTv)^#5UOskr+ub5d>-Kp3+2W~YZc3gy`!wlL-xd&u3-qM zv{oYR>?Yro$NIt+5_M9L`e+Eo(79^KWusyNAumgz<8%Qvd;@P6B4A- zos5lR&b%?zb1}iSoH?IE9l2}0miF00Juwk2NbU^1KKU z!?npcXCB)CzG>q17w-2gjq~X}basA+ONhBHVCxp~8Orx1l=oZ&T&eNNf>@G^L8-vZ zjKS>8qwDLPG%hv4W2Mx4*rn3Y;Fz*Nv+QIbNpj6WLZqY9mB>y4&167rCPyxIk1~pC zpLSbqzSRNir^u$99yTz>;m`BcYy+*wJY75n`6PL;j=#X%H;cwd22Fqr4`scALLF3C zpigu!T40tjhUIHJ;%i7nMDai|SlqW3Pf;R66D-QeW!9t-Z(!}8MY3V z>Z*fhvSB!vME}nI)p|a0h9Th-Y)iryQsSt6XmPHl9$0^q?h*>$*^~-GejyTTy~l(NkTHwsyu7y|si8F4p9^5H z*W#V*&Tkm3!ZJ9k%tyzn2YVqjrEfr3pRcm6IUp=RiU1gFu4(Z^&NT*01Yod5B>)CH zNpgk3-uDq4KK;bnp`Pr;+LZ`_mY;7TJ8nu7zWLaaq-M_56n))Eyt{L?SFpP%C!r^5 zb|XH4gN0%#4(7YHh|^{IykPcT4G3h61;s6W#qpD(^cR4zEQ~jZ>aKjOfwc9&S_CAh zWw|2;`!~$!e-Q@zZ{Y>lnuLJ?UZSlFeuVflvtNGz4e^uwIeC>1b z%hvOgndwh&J%D0xH9X*ROlu>l+RW2lw!TUuzu$i5td-Q1s|lti&BP^l4%O&-*%5|4^hP`j(FH5O9~2udrGfD?Woug{ zKb0`5c95g4k6?4U&|Grn4kgD-nX^uuBzs;m(iUzBchL8Y(K*voElMI#j=S2;Q;~H7 zIqBlsq?x5r&H?;f@o1!1E?qVXU-Q4I+~F5g%hw-5iyA#Ob<@%#Ut1 zJ+WL{Wrn&RS49J&{rq5ixWg$(VF*HP)I$I;yREwxfdgi@!YAWNkNO4bapm{E9b9x> zulPJaulUDjzpeOSXuKBk|GB3Cp8Nm!NdAHqpMjnA=OrI-00AuCfDR9Eliu0vc0IQL z+3d#hn+yv>r(&#UXJm`7{ts14|L2PB|1Wp=_xAU1-TgMD_21{NHTFMZF$Dq+|7QIB zO+&wyUVdRMW&6o)`RB!=JDR>2S$|1bEOAAfC3t1}LgFjsjU9~2=+dzPGCjgY8YpLj zuO~z*J;Ar?EMzWeOj@1*fSY9Vtb3G5Us--1oO?-p7 zC|Qlg^Sm*mUSkY}5$us9)633BjKU^{|9JxiG?t%qroS(0+(=lc8GcjLkWhqH6Gs)= zOE*`neB`0Mh2IYQbjkaYm#+|DM9KrmzdBv3Y> z1|rCM7f6HFXzuzRD$0Zi$A~D{RCnE-L;wF#@fsyD+bfb2Uj~tb>`_2r}imI{slmq z9J474RnieeJ*lXq*cYntQBa2qkCe`oH<}~P(sl#f9Fk8<%j{l?4I6A+kZ$P3?vq{^ zBwp6whiWOt;mIV#EIchd!H)YpsNmVdNUBw!-{U1XN3cU41U^P)EtLkxae5L#=o`tD zb-6XOr7#ydGDW$J?T1f|hOjCPqbD3^Mcf#wLGD`!glX)T)LySW;PJq8f_KBS` zU-&sum{u~YM&{}z2NrT{N#Ey9Iu7DAzP+sZPSCBoX+(!wg`8VAuRFr?jEpx3rmoym z+wjq@+*38BF}$S>l*0KEwDE&3hRPAO4oDv)zMY8|nrkp=MXQlzPu*Xu?vZfZ>LLrb z#4!tl7M2OMhNe>Y0U?uF9P`*`-OAr~V3q5FJDQHP+BSj{FunIZb(Ulo!k-KS!WC&R z2DdZ^8sT*u?}mHGF1BIjeerS{cAR~wLDGhJEPmz9iR#VMa$KhUxZ?>z0fldCl_Bs5 zaYodo>CnE)`QUD~6`%;E+e3%{yrQ3f~||?hNYW<0r*H581R|c8S!a=99VbUy8gGvG6An4 zWC0K^8Cw`$frGzw>yI-qbWe;84D@aEEp+t(UH*?2ng7?$GcmFL(VF?g#UH40wPfDn z;SaEq{60zl{F3<-^!oRfOeUtA){;nmAQ3#2&=qQm2@N-~!kw2-%XsuD){-r?DhP{5 z?a>9~6)F1q!TJcrjF{@01cK>O;joKNi1fJj8KRP>xd?RHVX5wJo7Jo})C(Hy0fUH3 zY=k9>V=W8`Gqtle3ac`BhM{i>Tb%AITm_#{V?J;1HYJ!F%dwlI`GRTo(m2ltHwp#(>EtQ%R%^u=E}fFs6)GhNrdFtqSPjc#H1HlY@$|{kdF7Q7Jzu^h{ghtHT_?~q zCi@E~V~CrP!3$$d3f=Hv?xbba)y>sVbCWW7(`IYGdC&cPl=lm0PA(&6Bqeafoz4k1 zp-KGVPi+;l@J0GTt`yKIY zrkh@?Lh@o)oT_!F4_uofLCTJKcpml>e805M=ajQC9%dc$vo9o(sMCL+mn=D4@mZwP zA`Xk}Gslb7x30mgu~Q=w`rEVe6?!jnG`_SZtj?-5Zx9(`LCk!{{E8Aa?i7RdCFmQC zeQ$w1v6H483PN2+H;V%frN)4bUnRl5HVUL@=Z?|2qZ~j-=t;Gay&tqk?4h} z_~aE|FC6FRc*zumd9(~1Pn*=?z6g0@ep9BdNPH1XjPm-KBtz>O;t)v9|7q{LfHAPRcL9ue${irBCKDvF9xkzxfED~bgb3t~lyJ$4~>O^i`g z1f$qdQDgs}JxOk6)*}9V^WHb_{q7t8@CS>1&faI2wVw5?wfFXUGIi0Gt^0~3*I#d4 za$9lRpPFikA{@Oz!gCm9cM+oXf@WuAX-K?AeX2z8^W*GihYoaR>MN zO}i~`x?QRLz+0EbO!XKwFU;wSb7f+N-)XSm~kZ&_he6UY7 z6KCd`9n5>q-R7uU(L#q@w&oV8?tXdhPtCe4Jc<*|*DXseR>xyUEo<*Cb>q9I7Om;B zKil_6@9qC6vvs~li5{V)M_;Tz`0=E}*}Kzs=SClSGW5~W$D8L~ofK*BcQ$cHkjrYv zK??`p`gm@x&x&V-^8%b=@@=$<_5D<%B|V`mIs_aSksMspux-HbL&ggO*1ozq_Jsf2 z1$KRpJee&rbr-cgSK6y;nR?Z(E?SqjyxvXyiP%kNN{*Vpr}^GJLpt2c?B-H#^s3hD zn+ENe@876c;rWrCIXmk4#_tO%IH|(PS-s*LZ5`YCuEz|!PF+ryIMcX+wZ1`;&5yyI z_iV0H{%PZJH3s?wMs|BUG%>uDcR=Glr5&#oyI;n>&9xYxBBSH2c2(Pbe{sJ-Zl!8l zT3S6y?-a2q(W&_$w{_oL$;Fc^#f7HKU|9)PR-11Ww8=fi?+u2WyIMB(j>*VJn=X7*FT=L_G^U3F47HXv( zn(y``+>DvA4qon0UC#}R9`y9hhij>2Yk93zEN828dbV5ia9&EI z0T){?PM=eCQMxFP{v4i)_o7grAqy#1p7>nUliA2)Q*Jnes> zWGU}5bxTxPdn-FHBkkf6mm$Sda~5rvhJyG9iK=z98Y+~ILb@vx{aeuaB`zSguzY?iSzx6=02PZIhzjn&+XE&OcVu-u#D z{HtcD279gOzwpM~*+*~MUvs~4CskV6#;?!Hi+4VpX)&N!!?o_a-yLz3r1+qeVxnwag=-Yk3*`4~*r4=G<4$#;XoyC)#ftmVqWKjZ!Om|q3z8g<14y4oqAtnXzf+b zMOU{!;y%kz&(rNecGj7-1!JyEujsGT{dw}6M1AWbqYk$GF|A2!eOzAUIqe=;?5bYR zp~lmCqrIy+m1=tEwS|7o`O&IzqZ@B7=xAxvGWKF@SHH{Gx0Un?wJjDeIoxP@Ph->2 z!?DW7FX|o`v~yK#=|`*Eo^MpC*|6`bI5yeZ{MFv-?F+v|7y?KM{_fMCo7CKu@>VIt3xk$gULtC#ZyxJ*YTlMf!S#$gz zX1tGWx^2MCO&@Fzj!26>*f=JARh2oBd-{eqb9y|!@bnI~JJ}`t_$pR8l`_6rlaw|c zD=%qVuAO?!o9{#2-r*X`Nfwh^%HpD&a(0G><}2?Q0l<*26uM54)6Wwa&j?s z5C0BL@@njQU!eG!S{vO;*?xFi@al+*g>IJ&yy17FX7c#q3xenF?{TJAu&Q;fhG!>i zubgnQQ~lUM4mCqU?E;&QiK^KrV@t}>jh(`4FDO{^$%^I6eQpKW?LFxdyT4~x*QC-m zE0bT0*;ny-blLhv>Ub@32+IBP-171FYDd)kHYPfEfAE6~!#+4S?HWG!xbyXOw|Zs{ zcaBN*u2E)!^Fp@=duwhu*6gvbH_oEC-!Q@dvCDJoO>LVywLH{i(S+WX@!t==Gx9~b zn{lt!jH#q@8GIo$q|Id4S5Ibb3ap>B;oB##&PH1oicGv7`|NnnYEpo_&U(+WNI&C4 zjYWR{?_UwF{ft`w71fBWntz|uC_IqE^*1Z}pF&J{=uOY*{WD+Xp@=tlDB@&XkAJI^ zZ9UxolWvpuXes>>b@bc5TCp@i9sL$*7Gn!P@7GWk2FLiT@M8rxFGdW1{Vj1^3>|(o zGWFuG=sWx>EUB?@OXD%?Mvlsy$2YzF_)n8krbozs3Yn@U^Xp{Jy4$pHENXe))p+cL z!?=~}M*8ByxD6Y>Y0x*T#G!vgMD$Vj7&AQiR)rP;F>Q|Lrlc<#Ii=HUKg$zmY;ru? zs0WTn>3;Rrl?83*H^|Mj2_DirYxE692^@2SD$Ba8#!Lp7#J6}xqne>`u9+BuWw_WT}748OcwyNN{(84jZ$%?jTs^ipB zZKsFIzHajSPR}cb7jENe+oi4ju^RWLEnC{@hoADk?zHGaO57Z$&V}pUjo$Atcw)bg zy$(L!*elC2RNce(b{!6LullH3UhUqt`E`)# zIS`*3YRNonlSCxq+&;QRI9_rq|31OL+M%2d9z!|@oUR_&cT7~h;Dm9?-F=mv0{42~ zHEzaxRnd3n*;0)s)rwYS4yfYtUBBd*Ze0RgHys)`_3elmN=QIn`=S;f;xcMoEWB^c z(?U1YAHFP{zPH%en;ljLe-o7(uGw4EZhn~(G5!}f6%QQhk!f`;r1on$;O((@wGO6E z@vQLG?wuEIWR_Xr;T6!lc2e7^Lwcp3kDJ`*E61o#EA7-HhkHN!a4WBR+>jN|-)^5& zKSMXK&TZT;es5vjUE+T6QKcW`rfwEl+pn#TjSbq#`%J>B{!8(!$_&pM59~G|t}gcB zX+ASJVH{0VL=78eCDId;Ny$@Pzo)i0aI-m%$_^@u#tu;WHx!lFlrLqDyR>0Gwcj+q|Y)J6tGKGX7+z zyb2#)-CS3x;0gN&KX$frT{7ydy45|kE@1nze|+2MgQu!;X0Pa*o)czm%<43uz>;u_ zwI>=(zj!Qk^+td-SGuHkK@1ON;ZzVQ8 zZ}n=;bq_lqj}c#O>{!m{#Eg}0-5d9jW@}$Q96$W0RRemQs6KIO)W@k4ZA!R#Bt>rz zx~!JI_I#D#_f5oiofoWG`P!~q?&eb!V^6NB({239v>OY1*L|RWSKxe&(0klUt5v4k zU5tr>)qhD8JotN};12(DjrJ6Mw*0_7)sf6J>Fgxm z@blR@tt-bMQE*xPeR07^6uj=-D!yl>{UTAYW=PEbBae^2sah>!zep53@_5s|*vE@X z1t^X!H~hmV{J7)wyriScR+JiW_~qsf2WLHKs-8UKoX(;0tfuc$)i<&#?{vScUG3SW zOZeX2L&FAq9B+TY_D-)9&vWY+OiZe}EcUwbEU+Kc*1snTe)#n)u#fWsYvfbt3oUa(RQ8JdE`#m(f~)_LH&2M zX*J#3W@4LW7d>p8?<~pNr&@kiK67x}%)!rd`VMeDF<_Lf^MrDWTM^f#F4di_$6vRz z9(8<1=CrJc8STrtcv%HR#aqo8bmB=$p9)d29>d+{g~@XRipSOKk<-)XPS0Bb`?6+~ z>sI`5(@kX#%yyXF$zs}uFP*yuw6FNWS82C@(<V_4_i*Cudp*93J{?YOj7P zBf5pAHrx9ozoc!9%jZU+bY@W~(S4TUHq#YTtt&cKvpaXGY?T8W_Iw;$rqQs6*S#gj zuy*x2w~pR1I?=cNnv1q|Dy(mK;9}s#4Tbx(2~9{IKA@$=uKD#QC09If@%;AOy`{4n z^hhaERNDC(dDOi_|2SlxXtx$^Sce*f33sP?C(~jUksZWoKa(! z%0KaRqvR@47aaOO__@WXTXnPgu3K89b6DE6E#0#U40H>8W~&(*e=qE9>y`d#NzcdD zcoT4L#@=2|Z`MXN^s(Hss%wpn#}aLl8@;=FaAdKby>ky;t=_@u=5EVE!{RHB4m=!o zHg#E*-Ik+s!`i7@RNZYEnsVjY@(&hAu7*tE1`VBP;-Y`b_UlyU84TX;F&U~7`|jGk zakFT*qu~SBdgWb*f1KQ?UdVdQw3-o(ytNy1Dz=C^y&!a1yT}=<&^adM^2 zC0-v)zEh`i;hbTUk2LNcuyULAsN{}UtaK-f47fSjDlll-`?&`Wd!LWKnOSehyujRY z$=#;iT-~t?Rz4<9@LF%%pJ$9FOwsUhUti3*Iz}zE8kC%}$>Wf`MH#yJg&L75KrzW!Z82H8-|L zY;yN{)xoY@#Q~0`QqI`+oLb&(@zj5mn=^2q%F?o&lsLq*-@{ewn)SQpk(s;Fx!;4V z9p2VUDmLEmW>ec<_DdK49DH@--R74nMz49fVtde(9#h9%oUtVMxn0vbzSVoL&0Z1_ z_1JZ9Pp7o-nX{)hJ=$~Y?K#hT&%YYyv#NC7sD^fXw(eZ>aF3UNjVon4jo9dWH`PkN zD(mgl%Y|yBZ?j98cl67_sePc=5;Ovt5L8I5lA4Qg(m zP`&w-6OOeGwEbASj%Cl`*SglZacs=8CX;7w{l3PwsUbhe-}=WN=v;Aam3g^IWe%Mi z?0)|KE!u_L|j?%exsbkwy6_a>0Tft7T-rIT&nFw&ly<4cmV# zWcRpV9cA>>xmKkI*v}07`i!g%-Bb66BZnqsr==EH?YsYn#IQ+^9PO9atNPA$n|i?0 z(>}?y%D-z>{6vjLGg}>W@961Mq*~?9>;1yJ1^Qiz4a%J5+9z*HlL}uK4q9?HE?RS? zz@k>IbrTj@EI3ix&Mxwcc_Zi4a~&ABdc|_geP6H0xt`G4_i5w38|~uPB(^Yo zVNoT1tncVkGt?`VxPNSYb^HhCHD}`OTQ0ls?U0-pt4lS%`?6Nd=@$V>DUV`9ORRk# zmg^i}*xL7-nA7j8j`*VJF!{;N<`wfEotZcIajAQ+Z^zXNd;TQ1Y4xL?A-7+8xA>Um zme%Z|OX9eJRojPD3G01e&cv$e*6wM~yJfjt+40(@RZO)D6RUll{Nm}T+pk`Xd0*tG zh*kMzQw!VvPjvCAk>%T`1T2eq(!_Yo$nq8))4XoijkSH>DWj=H)UH{borXl)PI_yx z%d1|IxlwUAW@JRjJ)A7H@?nQLr9E)6)YAs`*gn;2bd}yEb*^u9e3v!5Ttb-+h031{zO>Bd$pnwN zRxV@Z*Np>C9(z{@vC5$Odp``r*56uU>u+;x{Vg!&fLt)W>3A3Q>^rSeJjX1H-TG)x z;~(E^y~popRV77QUwhy?FUMw&i&kq^Zem*JJ|RWg2Gq$1g_&;V`ztc;%x{9iAtP$} z*F#3spLe$ZFC+Uu%jf<701(i|7JsB(jR6OtK2Zie{L7YTQ}sD+)WAW-k(`E`6^bK4 z?bm+}vQCG~xdu7>Q-))pjDK?o_(dj32Cc}^6#ssc;YYQ&^~6*rDY(BG(Ft>zMk_&) zo5>7l0$OV-)2LLEM4r{C3~F&uqFFgLP9`MvYH$v)p46ey;67*oce7_TXbUuixy+za zfl)D&X*CLy>9EC%KC8nSIj|*W&&rZcTybqKGw8(K6lOBLqyc6lGM!r7T5Vp=prZYU zeTTA08Zs-V;5+E|8q_$5my|OoGJT&`CF$r`V!$PFWwvP@T9vG(@6#gZh&?NI5R-Ak zd(?tqG<_d_*3fZ-Oi$+!WCmpEibqZ6pIQ?4juV-d&LLDQ)42wjB98Yke-=>=GOsWt zHR4inQcmXeN(S1e_%t>D4qW<8`V{44-Uk|kxHa9pUM*X1D%^lB?m#EyWTD-O%pk68 zH<#(uf=MJYt+;WI$nDuN(~3+z({hqVrKal=3Q;FmbJKDdNeykY zR#M5d@38_gNaR^bQL}lf*6SrYmTIUl+Rti5BW|`bZ3~uyPTX!qWCp5-uzDrB_94@X zZFc6*8u;QZ77Uq6P3Mp->*%*> z_0T!AoL<%eY?;>qvn6gYGpkoo6fKztT7#^neX4_9pnafI!A6ny>C`|2bZ$s`gMsuP zte`}71SY*AZe%lUpH6}rqIGC+cqRE>okr0zTcLq6CjE?h(W|5mEsnn+ZPsZu2KpU3 z?7q{Id8O0pjfc>fwOMQ6b?DT3ks4xFPN$V0B z4HWCiDvd(t0lL6I+bk>;vnjY_Tx{GiZL_4&NK`k;Kw*M>FfAubzYr9rGL=@3)|i*W z`*Z?%o0bFMg3=)}ELAp7p*83_!uu4urg0si@rFXvI)GHv6nEkpLIbr6D914paI84Q zk-SgOa0EJ4cxX zWrg`9vR;zJNxi0R0dUk)J&8vZs-I;A_>`<^>>S8v%=O|h3$r>@iY!z6D{1s_8EH9# zLa{f>8Pud-WJNX}esA_2Smo4rk%8c-egR4}o=$A~tR5?Y+Eu+uuU9B$Fkqhrkx8%> zbiU}}yHL9TavdHp`CdRD+Ge~DAc>YUz>1p7;I3-u90I{1Q=5WvOr}=@a*;98!)(#D z0Bb4J@5K~ix(-*isA)M&W>{+THjAY|{RKVr8l4Bwk_yGtcpp4g^Y=mfab7^HW@D*_ znWl3RK~){q_c)WsK*tT`Fu&|HRR^;GZT(?Cej za$q>fvnVH1KUC6cP>cDqQ0$U8e$sT_qnu3r03cvBod*)sJ@tRpTA&N^y(kAIW?qL{ ztJhOZh;oXCwoeBwLHZd8Q%iNCS_kyVz5_OZtPiyS2ts1C^m16fb=r+QPoW0Odr=;ce6(l;EdOTn9n{OT_$Hl!FICWZ-kyeAaP% z1s74G+=!%8YbgE$8q$axm(A7(&?&1|m-NuW1)nA>eWm~D8QY(oWlD+5l8fq{!{qeRp4vT zHK-_H&FQlS#=S}kVj`>#1H)YKV2vjd8)w&V@f7&e%&!1GWo?G%!p0mj>Th8raWgJy zvmu`~4dlbdTu^v)EaBTz-WwVWEDEhd$LB6658gg389EODZK*wy3((tn`gG!(x;1_p~hD>L3l zBc?s;7dX_cPvO$Bz7h(H)*+MxkqN3L9~s3Fpk!pqb3ulQK;I{U&Sf&0`V|Q9sj2?~ z89*U@7Pg!A6)YZqAJ#9c7k()11Gq;t76Z>frtuHR^c33y)Pabj?F01TGKI~1MEEFn z1UO}S7cy8@Qb#`V01bd$KIO#DX0vq;Vu|@0km;!%1k=YjH^@LW(D$Ji==XwfV)GR6 zkM$K~6o&!m=_rQ`Sw7|Tj6a1p2m?af2acPLISdlD6<8cVUF2EBsF)7JM4&#C23^2( z6FQaZV$i2Lit8W)f}!<7MbL56Xc2Uv$=)oKtL zqR&D-(=h_lqjnz@h4HXuQ@aXTKIOoe&^3dY2>%WR0Uz2HMbG8|rU9EnVAd&r3bq}r z5qX~o7SZp8jIC)v8eRuxK3!XIH7FLvl!Vtw>jey>Rlw!}m}VO5 zz_W}W2L+F43~dWYMlQqRV&$}y%Ycf|GyM#eO?h+;{7bgZWu3zNK*l1Z^$LfT$&fKc zW$=5rjO77fM&>g{P}!^w1&WQ<0T+k%m4MlF9ib@%)o52 zP<3?v!D`XCA503!J~Bq|`(R^f#Ye5(XK?3@12x-#w23C}_aU->BZuB3wrGfsj#K zD?;ydT|%a#I2JMlMa)!E1bVDUFVOaMUoKE%OT9Xcdy&~ku2tQ?>tS^HWM6k+{>Vyq7kCt+;?568X@ zz9gCV(0+RA69X2*g(GdoE<>tgk@(AUj}Z3ba|r$*8xN89MeG}_7UMUNug7AkT4aY& z{{u3XqXZel)Cesi(T#o^a>nQ!0<>l84P+X{-T*!T_~dnc6ioeE$Plfh&uST-#4=*BF~}JIhv+`Gy3zM3EY}4~5G#q5Ln;cD zAr+C!;Fhp*01ZTj$T8E;SmsPeK-)7N0hW;2OC+MO`3!%7a=r+&8#nry^|MGOp<|BO zOfei(4&!qmW4<$F2>j9S1q7!3q6e43>eU;VZG;~{`C_Ow=37C=_LxA1=ooDa{4F}K zuo9@>2m*llACR%!GRU~Sg#SVLA*fvh6G&SS?O|9-#1ZLyQIHTq+lkA3;8^Y#l^k@nQnA#!BwOAFIu1>{;VDuCbVCG|0vFZMWOtfU@RHd zGXQwf`G-gc(}@T(aT)Sd>6#YX4QTreNKvA5*MKZ$UXF1_B1MGF1NcJB=7FkbxDzR< z40i%Kvbh1aiE@WfE*$TI`e!yCGF~s@ICQ>NyZJZ1nw>^2cn-ni}(?lS3nGaPV`w|epUx^VTm5n z;nOGu1F2*tdZ;qvZrmNWTq!6pa%jRS0Rqq+X<3 z(6tYc$nshs!@-F3SvBA{lj$f|fM*qo*Ran4V1+)*_g4TUvOI1fWAjC%HqyC^X~oNN z9|DOu*x*Ck0+)&CNn;um>p$=>>^q={==fq*QN0VG$2b`wqu2)#W+*JuK7`6CR)O`v zVxZ-~)39fe2+GO{i$tFVAZO1ac1QaPke~YJFj{==BhtaY14(kUuf)DtRxc77=$aO( zp|qR``qTCy)<$g`UY?+nC^s(?Wcnbjy!!Im=(HyMUE?LGY~%2Ltq{# z&cO_4xf*bz7-tR{cCyg6$jmndvCeV=MRpkV2f=n4PiZy9qL8sXWRXh8zgKK-CT)Qy z&UBa{&gi%avWh>;ct;&bBZ`|K1Jgk3MHRH4VWyd_K(+w&lL3?wwW03=GeFlq;2*CS z7M-p)u;`ROK$L{V0}#PQ+6k=}W`@g82uo7#5Hc3a$BO3P27ilkst7^Ch?4JszeVvM$}zqPlZEBU zK*r)OkQvzg1D2;gHZq=Aj)TbjVEv5a3MfWL(g+Y1`3?iJqUjh3j8649@HoS<$l+r+ z5gshlyP*FV#zcI9VIS-q$)_CSs^B!TJZ?}%DyD}-iX7D?2AFvEy-4R`-zywA+E+-0 zpzBDasIxu=MN4$7jCck0Q!vGG+JSi;kTEU;G8QX>jP2otGDW&QeIFwC#D-!SA&f!G zK`WBAk8%trf^uUya*#2dh|s&Z$xN8Hf1Z;d!tpfM0Q)7`UQ5W>nijMk#Waw~)K*|y zAe<{wuQ;B8j3q+;EXPLd7@#%(y$w3^~VS{(-n=xvP-Dais6l zW0y9SVUE)=$E>HA3!}z-UC0>M2N|Nhqz*Rq0~2p`Cu^&yo~M35;8U(NdBchn4pH)+DA$P z^?y+gi-ELRBoxwq5y^wR9OF*`SlO8pprjdh0~w!J@cgK6Am_7R7c!QYCL;_%c|FLG z_&~o6{yF;&xNmgaV0xGigXyMzH%1a4N9#bGj_Nf)B#Mhc!Lt~t$O@-6R8aLy4@ z1IQTXfv^k9TN0rdrnBKn^0vSx(|&=KW^)63=2<^Otx@bPvfC*ZMRb+z0fda@LO_Ot zFlb+4TP5p1er5wQ{V*WpS&?5y*Bgu@YcrTlin(OSCRiL4GLL-G-m z8Q5BdNv8SK*s#a;9?EDS<$@sNd;bv*W$O(?L}QD{X5e!JX*bMn=~aMxxe zQ#%Oef#z%@AqHBJwpk=TP#Mq}omU9g^7la@u=ZiWv1b7ZnGBo<9bb{d$J!z$5p5s% z9BL~N8)N@(A?RxU9Y{Z5SROJq z51>g{KBkb-zK0CqURs9;*fJTI8Y<(Q5jIb=JvG9^rt=;q0E9bvpP+5Y7@-`C1tJ*0 z>;gKJ<#|BHI2jmowif_0F!;1Bn7d@&qxaZZ43M#NIYmYhjm^T8F{~oY8`W!&G2b5P zAV}mP?SrpN`Eis3HNeV25m6arkS$=iRzz+VoFb@B=f40bSb!{T<~HUWbI9Wb{Ya63@((ZRl>f{iE3;#El9Encfu I%Z?uZ55`2bp8x;= literal 0 HcmV?d00001 diff --git "a/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" "b/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" index b1f5e0c..1c0fbd0 100644 --- "a/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" +++ "b/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" @@ -17,6 +17,32 @@ https://segmentfault.com/a/1190000016611415 10、数组和链表区别 11、volitale +### 秋招一面 + +1、数据库死锁及解决方案 + +https://blog.csdn.net/cbjcry/article/details/84920174 + + +2、数据库分库分表后的分页查询及相关操作怎么解决 + +https://crossoverjie.top/2019/07/24/framework-design/sharding-db-03/ +https://juejin.im/entry/6844903478171533320 +https://tech.meituan.com/2016/11/18/dianping-order-db-sharding.html + + +### 秋招二面 + +1、UML +https://www.jianshu.com/p/28200121a33d +2、超时确认,快速重传原理,快速重传重传几次,3次 +https://blog.csdn.net/u010710458/article/details/79968648 +https://www.cnblogs.com/postw/p/9678454.html +3、singleThreadThreadPool相对于ThreadPoolExecutor的优势 +4、可重复读机制 +https://www.pianshen.com/article/11361872925/ + + # 阿里 diff --git "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" index 29465ef..a1bef8a 100644 --- "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" +++ "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" @@ -1,5 +1,30 @@ # Java基础 +#### git底层数据结构 + +https://blog.csdn.net/leo187/article/details/106233706 + +#### volitale使用场景 + +https://blog.csdn.net/vking_wang/article/details/9982709 + +#### 一致性hash算法 + +https://blog.csdn.net/qq_40551994/article/details/100991581 + +#### sleep和wait + +https://blog.csdn.net/qq_20009015/article/details/89980966 +https://blog.csdn.net/lengyue309/article/details/79639245 + +#### 强软弱虚引用 + +https://blog.csdn.net/junjunba2689/article/details/80601729 + +#### Arrays.sort原理 + +https://www.cnblogs.com/baichunyu/p/11935995.html + #### 创建对象的方式 - https://blog.csdn.net/w410589502/article/details/56489294 @@ -20,9 +45,13 @@ https://blog.csdn.net/cnq2328/article/details/50436175 https://www.jianshu.com/p/10584345b10a +#### JDK1.8 JVM方法区变成了什么,为什么这样做 + +https://blog.csdn.net/u011665991/article/details/107141348/ + #### oom出现的原因 -https://www.jianshu.com/p/2fdee831ed03 +https://blog.csdn.net/iteye_9584/article/details/82583093 #### Class.forName和ClassLoader的区别 @@ -389,10 +418,19 @@ https://segmentfault.com/a/1190000013504078?utm_source=tag-newest ### Java多线程 +#### Java多线程通信方式 + +https://blog.csdn.net/u011514810/article/details/77131296 +https://blog.csdn.net/xiaokang123456kao/article/details/77331878 + #### CountDownLatch、CyclicBarrier、Semaphore 用法总结 - https://segmentfault.com/a/1190000012234469 +#### juc下的内容 + +https://blog.csdn.net/sixabs/article/details/98471709 + #### AOS等并发相关面试题 - https://cloud.tencent.com/developer/article/1471770 @@ -429,6 +467,42 @@ https://juejin.im/post/5d1882b1f265da1ba84aa676#heading-14 https://www.cnblogs.com/kismetv/p/7806063.html +### hibernate + +#### Hibernate 的生成策略 + +主要说了 native 、uuid + +https://blog.csdn.net/itmyhome1990/article/details/54863822 + +#### Hibernate 与 Mybatis 区别 + +- https://blog.csdn.net/wangpeng047/article/details/17038659 + +#### Mybatis原理 + +- https://www.javazhiyin.com/34438.html + +#### mybatis执行select的过程 + +https://www.jianshu.com/p/ae2bda8f9d84 +https://blog.csdn.net/qwesxd/article/details/90049863 + +#### mybatis有哪些executors + +https://blog.csdn.net/weixin_42495773/article/details/106799280 +https://blog.csdn.net/weixin_34025051/article/details/92405286 + +#### mybatis插件原理 + +https://www.cnblogs.com/qdhxhz/p/11390778.html + + +#### mybatis二级缓存 + +https://blog.csdn.net/csdnliuxin123524/article/details/78874261 + + ### spring&springmvc 面试题: @@ -439,10 +513,30 @@ https://mp.weixin.qq.com/s/IdjCxumDleLqdU8MgQnrLQ https://juejin.im/post/5ce69379e51d455d877e0ca0 +#### spring中bean的作用域 + +https://blog.csdn.net/weidaoyouwen/article/details/80503575 + +#### BeanFactory和FactoryBean区别 + +https://blog.csdn.net/weixin_38361347/article/details/92852611 + #### aspect的种类 https://blog.csdn.net/StubbornAccepted/article/details/70767014 +#### spring aop的实际应用 + +https://blog.csdn.net/zzh_spring/article/details/107207025 + +#### spring实现多线程安全 + +https://www.cnblogs.com/tiancai/p/9627109.html + +#### spring的bean的高并发安全问题 + +https://blog.csdn.net/songzehao/article/details/103365494/ + #### ioc aop总结(概述性) https://juejin.im/post/5b040cf66fb9a07ab7748c8b @@ -453,6 +547,7 @@ https://juejin.im/post/5b06bf2df265da0de2574ee1 spring ioc系列文章:http://cmsblogs.com/?p=2806 - 加载流程(概述):https://www.jianshu.com/p/5fd1922ccab1 - 循环依赖问题:https://blog.csdn.net/u010853261/article/details/77940767 +- https://blog.csdn.net/a15119273009/article/details/108007864 #### Spring 事务源码,IOC 源码,AOP 源码 @@ -467,6 +562,10 @@ http://www.tianxiaobo.com/2018/05/30/Spring-IOC-%E5%AE%B9%E5%99%A8%E6%BA%90%E7%A https://www.jianshu.com/p/e7d59ebf41a3 +#### spring事务失效情况 + +https://blog.csdn.net/luo4105/article/details/79733338 + #### Spring 的 annotation 如何实现 https://segmentfault.com/a/1190000013258647 @@ -542,30 +641,15 @@ https://juejin.im/post/5a75ab4b6fb9a063592ba9db https://blog.csdn.net/shxz130/article/details/39735373 - -### hibernate - -#### Hibernate 的生成策略 - -主要说了 native 、uuid - -https://blog.csdn.net/itmyhome1990/article/details/54863822 - -#### Hibernate 与 Mybatis 区别 - -- https://blog.csdn.net/wangpeng047/article/details/17038659 - -#### Mybatis原理 - -- https://www.javazhiyin.com/34438.html - ### Redis #### redis为什么快? https://zhuanlan.zhihu.com/p/57089960 -- Redis 数据结构 +#### Redis 数据结构原理 + +https://blog.csdn.net/jackFXX/article/details/82318080 - Redis 持久化机制 @@ -663,6 +747,20 @@ https://www.ruanyifeng.com/blog/2011/09/restful.html ### 数据库 +#### 数据库死锁问题 + +https://blog.csdn.net/cbjcry/article/details/84920174 + +#### hash索引和B+树索引的区别 + +https://www.cnblogs.com/heiming/p/5865101.html + +#### 可重复的原理MVCC + +https://www.cnblogs.com/wade-luffy/p/8686883.html +https://blog.csdn.net/weixin_42041027/article/details/100587435 +https://www.jianshu.com/p/8845ddca3b23 + #### count(1)、count(*)、count(列名) - https://blog.csdn.net/iFuMI/article/details/77920767 @@ -933,4 +1031,10 @@ https://zhuanlan.zhihu.com/p/62158041 #### CAP -https://www.jianshu.com/p/8025e3346734 \ No newline at end of file +https://www.jianshu.com/p/8025e3346734 + +### 操作系统 + +#### 进程和线程调度方法 + +https://www.jianshu.com/p/91c8600cb2ae \ No newline at end of file diff --git "a/docs/project/\347\247\222\346\235\200\351\241\271\347\233\256\346\200\273\347\273\223.md" "b/docs/project/\347\247\222\346\235\200\351\241\271\347\233\256\346\200\273\347\273\223.md" index d5bd289..1a0a15d 100644 --- "a/docs/project/\347\247\222\346\235\200\351\241\271\347\233\256\346\200\273\347\273\223.md" +++ "b/docs/project/\347\247\222\346\235\200\351\241\271\347\233\256\346\200\273\347\273\223.md" @@ -83,11 +83,13 @@ Redis缓存服务 #### Redis +- [redis和数据库一致性](https://blog.csdn.net/gly1256288307/article/details/88739612) - [Redis设计与实现总结文章](https://blog.csdn.net/qq_41594698/category_9067680.html) - [Redis面试题必备:基础,面试题](https://mp.weixin.qq.com/s/3Fmv7h5p2QDtLxc9n1dp5A) - [Redis面试相关:其中包含redis知识](https://blog.csdn.net/qq_35190492/article/details/103105780) - [Redis源码分析](http://cmsblogs.com/?p=4570) - [redis其他数据结构](https://blog.csdn.net/c_royi/article/details/82011208) +- [redis面试题](https://github.com/AobingJava/JavaFamily/tree/master/docs/redis) 其他 @@ -183,11 +185,15 @@ Redis缓存服务 - [微服务架构如何保障双11狂欢下的99.99%高可用](https://mp.weixin.qq.com/s/lBeQSSPX7OeWO6SmWYf1Mg) ##### 分布式session + +https://blog.csdn.net/qq_35620501/article/details/95047642 + ##### 分库分表 ##### 读写分离 ### 亿级流量架构设计方案 +- [分布式主键算法](https://juejin.im/post/6844904065747402759) - [分布式系统的唯一id生成算法你了解吗?](https://mp.weixin.qq.com/s/dhQ8BCPKfQqMMm9QxpFwow) - [用大白话给你讲小白都能看懂的分布式系统容错架构](https://mp.weixin.qq.com/s/DKf63ZDJQKoEiOmGqn3NxQ) - [亿级流量系统架构之如何支撑百亿级数据的存储与计算](https://mp.weixin.qq.com/s/eqtR9QAMIm3F4QnGut1vrA) From 4c78196585abef00416eb9d69f24ea1d3762755e Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Tue, 6 Oct 2020 15:37:22 +0800 Subject: [PATCH 18/38] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E5=B8=B8=E8=A7=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...06\347\261\273\346\261\207\346\200\273.md" | 749 ++++++++---------- 1 file changed, 327 insertions(+), 422 deletions(-) diff --git "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" index a1bef8a..6694ca1 100644 --- "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" +++ "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" @@ -1,12 +1,6 @@ -# Java基础 +[TOC] -#### git底层数据结构 - -https://blog.csdn.net/leo187/article/details/106233706 - -#### volitale使用场景 - -https://blog.csdn.net/vking_wang/article/details/9982709 +# 一 Java基础 #### 一致性hash算法 @@ -27,114 +21,126 @@ https://www.cnblogs.com/baichunyu/p/11935995.html #### 创建对象的方式 -- https://blog.csdn.net/w410589502/article/details/56489294 +https://blog.csdn.net/w410589502/article/details/56489294 -#### 反射在jvm层面的实现 +#### 若hashcode方法永远返回1会产生什么结果 -https://www.jianshu.com/p/b6cb4c694951 +https://blog.csdn.net/cnq2328/article/details/50436175 -#### mysql语句分别会加什么锁 +#### 解决hash冲突的三种方法 -https://blog.csdn.net/iceman1952/article/details/85504278 +https://blog.csdn.net/qq_32595453/article/details/80660676 -#### 若hashcode方法永远返回1会产生什么结果 +#### 为什么要重写hashCode()方法和equals()方法以及如何进行重写 -https://blog.csdn.net/cnq2328/article/details/50436175 +https://blog.csdn.net/xlgen157387/article/details/63683882 -#### jvm的方法区存什么? +#### 动态代理 -https://www.jianshu.com/p/10584345b10a +https://segmentfault.com/a/1190000011291179 -#### JDK1.8 JVM方法区变成了什么,为什么这样做 +#### sleep和wait的区别 -https://blog.csdn.net/u011665991/article/details/107141348/ +https://blog.csdn.net/u012050154/article/details/50903326 -#### oom出现的原因 +#### java 地址和值传递的例子 -https://blog.csdn.net/iteye_9584/article/details/82583093 +https://www.cnblogs.com/zhangyu317/p/11226105.html -#### Class.forName和ClassLoader的区别 +#### Java序列化 -https://blog.csdn.net/qq_27093465/article/details/52262340 +https://juejin.im/post/5ce3cdc8e51d45777b1a3cdf -#### java对象信息分配 +#### java NIO,java 多线程、线程池,java 网络编程解决并发量 -https://blog.csdn.net/u014520047/article/details/81940447 +Java Nio使用:https://blog.csdn.net/forezp/article/details/88414741 +Java Nio原理:https://www.cnblogs.com/crazymakercircle/p/10225159.html +线程池:http://cmsblogs.com/?p=2448 +为什么nio快:https://blog.csdn.net/yaogao000/article/details/47972143 -#### java虚拟机ZGC详解 +#### JDBC 连接的过程 ,手写 jdbc 连接过程 -https://vimsky.com/article/4162.html +https://blog.csdn.net/qq_44971038/article/details/103204217 -#### java虚拟机CMS详解 +#### 说出三个遇到过的程序报异常的情况 -https://juejin.im/post/5c7262a15188252f30484351 +https://www.cnblogs.com/winnie-man/p/10471338.html -#### java虚拟机G1详解 +#### socket 是靠什么协议支持的 -https://zhuanlan.zhihu.com/p/59861022 +TCP/IP,协议。socket用于 通信,在实际应用中有im等,因此需要可靠的网络协议,UDP则是不可靠的协议,且服务端与客户端不链接,UDP用于广播,视频流等 -#### 解决hash冲突的三种方法 +#### java io 用到什么设计模式 -https://blog.csdn.net/qq_32595453/article/details/80660676 +装饰模式和适配器模式 -#### 为什么要重写hashCode()方法和equals()方法以及如何进行重写 +#### serviable 的序列化,其中 uuid 的作用 -https://blog.csdn.net/xlgen157387/article/details/63683882 +相当于快递的打包和拆包,里面的东西要保持一致,不能人为的去改变他,不然就交易不成功。序列化与反序列化也是一样,而版本号的存在就是要是里面内容要是不一致,不然就报错。像一个防伪码一样。 -#### 动态代理 +#### 什么情景下会用到反射 -- https://segmentfault.com/a/1190000011291179 +注解、Spring 配置文件、动态代理、jdbc -#### 二叉树相关 +#### 浅克隆与深克隆有什么区别,如何实现深克隆 -- https://www.jianshu.com/p/655d83f9ba7b -- https://www.jianshu.com/p/ff4b93b088eb +浅拷贝:仅仅克隆基本类型变量,而不克隆引用类型的变量 +深克隆:既克隆基本类型变量,也克隆引用类型变量 -#### 红黑树 +1.浅克隆:只复制基本类型的数据,引用类型的数据只复制了引用的地址,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。直接使用clone方法,再嵌套的还是浅克隆,因为有些引用类型不能直接克隆。 +2.深克隆:是在引用类型的类中也实现了clone,是clone的嵌套,并且在clone方法中又对没有clone方法的引用类型又做差异化复制,克隆后的对象与原对象之间完全不会影响,但是内容完全相同。 +#### 反射能够使用私有的方法属性吗和底层原理? -- https://www.jianshu.com/p/e136ec79235c -- https://zhuanlan.zhihu.com/p/31805309 +https://blog.51cto.com/4247649/2109128 -#### hashmap的jdk1.7和jdk1.8区别 +#### 处理器指令优化有些什么考虑? -- https://juejin.im/post/5aa5d8d26fb9a028d2079264 +禁止重排序 -- https://blog.csdn.net/qq_36520235/article/details/82417949 +#### object 对象的常用方法 -#### concurrenthashmap的jdk1.7和jdk1.8区别 +#### Stack 和 ArrayList 的区别 -- [面试题](https://juejin.im/post/5df8d7346fb9a015ff64eaf9) +#### statement 和 prestatement 的区别 +1、Statement用于执行静态SQL语句,在执行时,必须指定一个事先准备好的SQL语句。 +2、PrepareStatement是预编译的SQL语句对象,sql语句被预编译并保存在对象中。被封装的sql语句代表某一类操作,语句中可以包含动态参数“?”,在执行时可以为“?”动态设置参数值。 +3、使用PrepareStatement对象执行sql时,sql被数据库进行解析和编译,然后被放到命令缓冲区,每当执行同一个PrepareStatement对象时,它就会被解析一次,但不会被再次编译。在缓冲区可以发现预编译的命令,并且可以重用。 +4、PrepareStatement可以减少编译次数提高数据库性能。 -#### Java I/O 底层细节,注意是底层细节,而不是怎么用 +#### 手写模拟实现一个阻塞队列 -可以从Java IO底层、JavaIO模型(阻塞、异步等) +https://www.cnblogs.com/keeya/p/9713686.html -https://www.cnblogs.com/crazymakercircle/p/10225159.html +#### util 包下有哪几种接口 -#### 如何实现分布式缓存 +#### 很常见的 Nullpointerexception ,你是怎么排查的,怎么解决的; -redis如何实现分布式缓存 -https://stor.51cto.com/art/201912/607229.htm +#### 静态内部类和非静态内部类的区别是什么? -#### 浏览器的缓存机制 +#### 怎么创建静态内部类和非静态内部类? -说明计算机网络的知识还没有记住 +https://blog.csdn.net/qq_38366777/article/details/78088386 -https://www.cnblogs.com/yangyangxxb/p/10218871.html +#### Xml 解析方式,原理优缺点 -#### JVM tomcat 容器启动,jvm 加载情况描述 +https://segmentfault.com/a/1190000013504078?utm_source=tag-newest -- tomcat请求流程:http://objcoding.com/2017/06/12/Tomcat-structure-and-processing-request-process/ +#### 静态变量和全局变量的区别 -其实就是jvm的类加载情况,非常相似 -- https://blog.csdn.net/lduzhenlin/article/details/83013143 -- https://blog.csdn.net/xlgen157387/article/details/53521928 -#### 当获取第一个获取锁之后,条件不满足需要释放锁应当怎么做? +# 二 Java集合 -https://www.jianshu.com/p/eb112b25b848 +#### hashmap的jdk1.7和jdk1.8区别 + +https://juejin.im/post/5aa5d8d26fb9a028d2079264 + +https://blog.csdn.net/qq_36520235/article/details/82417949 + +#### concurrenthashmap的jdk1.7和jdk1.8区别 + +https://juejin.im/post/5df8d7346fb9a015ff64eaf9 #### HashMap 实现原理,扩容因子过大过小的缺点,扩容过程 采用什么方法能保证每个 bucket 中的数据更均匀 解决冲突的方式,还有没有其他方式(全域哈希) @@ -157,57 +163,31 @@ https://blog.csdn.net/yanshuanche3765/article/details/78917507 https://blog.csdn.net/zhangqiluGrubby/article/details/72870493 -#### sleep和wait的区别 - -https://blog.csdn.net/u012050154/article/details/50903326 - -#### java 地址和值传递的例子 - -https://www.cnblogs.com/zhangyu317/p/11226105.html - -#### Java序列化 - -https://juejin.im/post/5ce3cdc8e51d45777b1a3cdf - -#### java NIO,java 多线程、线程池,java 网络编程解决并发量 - -- Java Nio使用:https://blog.csdn.net/forezp/article/details/88414741 -- Java Nio原理:https://www.cnblogs.com/crazymakercircle/p/10225159.html -- 线程池:http://cmsblogs.com/?p=2448 -- 为什么nio快:https://blog.csdn.net/yaogao000/article/details/47972143 - -#### 手写一个线程安全的生产者与消费者。 +##### 还了解除 util 其他包下的 List 吗? -- https://www.cnblogs.com/jun-ma/p/11843394.html -- https://blog.csdn.net/Virgil_K2017/article/details/89283946 +##### CopyOnWriteArrayList +(1)CopyOnWriteArrayList使用ReentrantLock重入锁加锁,保证线程安全; +(2)CopyOnWriteArrayList的写操作都要先拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下; +(3)CopyOnWriteArrayList的读操作支持随机访问,时间复杂度为O(1); +(4)CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用于读多写少的场合; +(5)CopyOnWriteArrayList只保证最终一致性,不保证实时一致性; #### ConcurrentHashMap 和 LinkedHashMap 差异和适用情形 哈希表的原理:https://blog.csdn.net/yyyljw/article/details/80903391 可以以下方面进行回答 - (1)使用的数据结构? - (2)添加元素、删除元素的基本逻辑? - (3)是否是fail-fast的? - (4)是否需要扩容?扩容规则? - (5)是否有序?是按插入顺序还是自然顺序还是访问顺序? - (6)是否线程安全? - (7)使用的锁? - (8)优点?缺点? - (9)适用的场景? - (10)时间复杂度? - (11)空间复杂度? #### ConcurrentHashMap分段锁是如何实现,ConcurrentHashmap jdk1.8 访问的时候是怎么加锁的,插入的时候是怎么加锁的 访问不加 锁插入的时候对头结点加锁 @@ -220,18 +200,6 @@ jdk1.8;https://blog.csdn.net/weixin_42130471/article/details/89813248 2、线程不是安全的 3、可以用来实现栈 -#### JDBC 连接的过程 ,手写 jdbc 连接过程 - -https://blog.csdn.net/qq_44971038/article/details/103204217 - -#### 可重入锁,实现原理 - -ReetrantLock:https://www.jianshu.com/p/f8f6ac49830e - -#### Java IO 模型(BIO,NIO 等) ,Tomcat 用的哪一种模型 - -tomcat支持:https://blog.csdn.net/fd2025/article/details/80007435 - #### ArrayBlockingQueue 源码 http://cmsblogs.com/?p=4755 @@ -241,16 +209,6 @@ http://cmsblogs.com/?p=4755 (3)入队和出队各定义了四组方法为满足不同的用途; (4)利用重入锁和两个条件保证并发安全:lock、notEmpty、notFull -#### 多进程和多线程的区别 - -#### 说出三个遇到过的程序报异常的情况 - -https://www.cnblogs.com/winnie-man/p/10471338.html - -#### Java 无锁原理 - -https://blog.csdn.net/qq_39291929/article/details/81501829 - #### hashmap 和 treemap 的区别 http://cmsblogs.com/?p=4743 @@ -279,144 +237,92 @@ http://cmsblogs.com/?p=4743 https://www.jianshu.com/p/dde9b12343c1 -#### 网络编程的 accept 和 connect - #### HashMap 的负载因子,为什么容量为2^n HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法; 这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1), hash%length==hash&(length-1)的前提是length是2的n次方; 为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1; 例如长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了; 例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞; 其实就是按位“与”的时候,每一位都能 &1 ,也就是和1111……1111111进行与运算 -#### try catch finally 可不可以没有 catch(try return,finally return) - -#### mapreduce 流程,如何保证 reduce 接受的数据没有丢失,数据如何去重,mapreduce 原理,partion 发生在什么阶段 - -#### 直接写一个 java 程序,统计 IP 地址的次数 - -#### 讲讲多线程,多线程的同步方法 - -- synchronized原理 - -https://www.jianshu.com/p/d53bf830fa09 - -- reetrantlock - #### list,map,set 之间的区别 https://blog.csdn.net/u012102104/article/details/79235938 -#### socket 是靠什么协议支持的 - -TCP/IP,协议。socket用于 通信,在实际应用中有im等,因此需要可靠的网络协议,UDP则是不可靠的协议,且服务端与客户端不链接,UDP用于广播,视频流等 - -#### java io 用到什么设计模式 - -装饰模式和适配器模式 - -#### serviable 的序列化,其中 uuid 的作用 - -相当于快递的打包和拆包,里面的东西要保持一致,不能人为的去改变他,不然就交易不成功。序列化与反序列化也是一样,而版本号的存在就是要是里面内容要是不一致,不然就报错。像一个防伪码一样。 - #### 什么时候会用到 HashMap -#### 什么情景下会用到反射 +#### 常见的线程安全的集合类 -注解、Spring 配置文件、动态代理、jdbc -#### 浅克隆与深克隆有什么区别,如何实现深克隆 +# 三 JVM -浅拷贝:仅仅克隆基本类型变量,而不克隆引用类型的变量 -深克隆:既克隆基本类型变量,也克隆引用类型变量 +#### 反射在jvm层面的实现 -1.浅克隆:只复制基本类型的数据,引用类型的数据只复制了引用的地址,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。直接使用clone方法,再嵌套的还是浅克隆,因为有些引用类型不能直接克隆。 -2.深克隆:是在引用类型的类中也实现了clone,是clone的嵌套,并且在clone方法中又对没有clone方法的引用类型又做差异化复制,克隆后的对象与原对象之间完全不会影响,但是内容完全相同。 +https://www.jianshu.com/p/b6cb4c694951 +#### jvm的方法区存什么? -##### 常见的线程安全的集合类 +https://www.jianshu.com/p/10584345b10a -##### Java 8 函数式编程 回调函数 +#### JDK1.8 JVM方法区变成了什么,为什么这样做 -#### 函数式编程,面向对象之间区别 +https://blog.csdn.net/u011665991/article/details/107141348/ -#### Java 8 中 stream 迭代的优势和区别? +#### oom出现的原因 -#### 同步等于可见性吗? +https://blog.csdn.net/iteye_9584/article/details/82583093 -保证了可见性不等于正确同步,因为还有原子性没考虑。 +#### Class.forName和ClassLoader的区别 -#### 还了解除 util 其他包下的 List 吗? +https://blog.csdn.net/qq_27093465/article/details/52262340 -##### CopyOnWriteArrayList +#### java对象信息分配 -(1)CopyOnWriteArrayList使用ReentrantLock重入锁加锁,保证线程安全; -(2)CopyOnWriteArrayList的写操作都要先拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下; -(3)CopyOnWriteArrayList的读操作支持随机访问,时间复杂度为O(1); -(4)CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用于读多写少的场合; -(5)CopyOnWriteArrayList只保证最终一致性,不保证实时一致性; +https://blog.csdn.net/u014520047/article/details/81940447 -#### 反射能够使用私有的方法属性吗和底层原理? +#### java虚拟机ZGC详解 -https://blog.51cto.com/4247649/2109128 +https://vimsky.com/article/4162.html -#### 处理器指令优化有些什么考虑? +#### java虚拟机CMS详解 -禁止重排序 +https://juejin.im/post/5c7262a15188252f30484351 -#### object 对象的常用方法 +#### java虚拟机G1详解 -#### Stack 和 ArrayList 的区别 +https://zhuanlan.zhihu.com/p/59861022 -#### statement 和 prestatement 的区别 +#### JVM tomcat 容器启动,jvm 加载情况描述 -1、Statement用于执行静态SQL语句,在执行时,必须指定一个事先准备好的SQL语句。 -2、PrepareStatement是预编译的SQL语句对象,sql语句被预编译并保存在对象中。被封装的sql语句代表某一类操作,语句中可以包含动态参数“?”,在执行时可以为“?”动态设置参数值。 -3、使用PrepareStatement对象执行sql时,sql被数据库进行解析和编译,然后被放到命令缓冲区,每当执行同一个PrepareStatement对象时,它就会被解析一次,但不会被再次编译。在缓冲区可以发现预编译的命令,并且可以重用。 -4、PrepareStatement可以减少编译次数提高数据库性能。 +tomcat请求流程:http://objcoding.com/2017/06/12/Tomcat-structure-and-processing-request-process/ -#### 手写模拟实现一个阻塞队列 +其实就是jvm的类加载情况,非常相似 +https://blog.csdn.net/lduzhenlin/article/details/83013143 +https://blog.csdn.net/xlgen157387/article/details/53521928 -https://www.cnblogs.com/keeya/p/9713686.html +# 四 多线程并发 -#### 怎么使用父类的方法 +#### volitale使用场景 -#### util 包下有哪几种接口 +https://blog.csdn.net/vking_wang/article/details/9982709 -#### cookie 禁用怎么办 +#### 可重入锁,实现原理 -https://segmentfault.com/q/1010000007715137 +ReetrantLock:https://www.jianshu.com/p/f8f6ac49830e -- Netty new 实例化过程 +#### Java 无锁原理 -#### socket 实现过程,具体用的方法;怎么实现异步 socket. +https://blog.csdn.net/qq_39291929/article/details/81501829 -https://blog.csdn.net/charjay_lin/article/details/81810922 +#### 讲讲多线程,多线程的同步方法 + +#### synchronized原理 -- 很常见的 Nullpointerexception ,你是怎么排查的,怎么解决的; +https://www.jianshu.com/p/d53bf830fa09 -#### Binder 的原理 +#### reetrantlock #### java 线程安全都体现在哪些方面 #### 如果维护线程安全 如果想实现一个线程安全的队列,可以怎么实现? -JUC 包里的 ArrayBlockingQueue 还有 LinkedBlockingQueue 啥的又结合源码说了一 通。 - -#### 静态内部类和非静态内部类的区别是什么? - -#### 怎么创建静态内部类和非静态内部类? - -https://blog.csdn.net/qq_38366777/article/details/78088386 - -#### 断点续传的原理 - -#### Xml 解析方式,原理优缺点 - -#### - -https://segmentfault.com/a/1190000013504078?utm_source=tag-newest - -#### 静态变量和全局变量的区别 - - -### Java多线程 +JUC 包里的 ArrayBlockingQueue 还有 LinkedBlockingQueue 啥的又结合源码说了一通。 #### Java多线程通信方式 @@ -425,7 +331,7 @@ https://blog.csdn.net/xiaokang123456kao/article/details/77331878 #### CountDownLatch、CyclicBarrier、Semaphore 用法总结 -- https://segmentfault.com/a/1190000012234469 +https://segmentfault.com/a/1190000012234469 #### juc下的内容 @@ -433,9 +339,9 @@ https://blog.csdn.net/sixabs/article/details/98471709 #### AOS等并发相关面试题 -- https://cloud.tencent.com/developer/article/1471770 -- https://zhuanlan.zhihu.com/p/96544118 -- https://zhuanlan.zhihu.com/p/48295486 +https://cloud.tencent.com/developer/article/1471770 +https://zhuanlan.zhihu.com/p/96544118 +https://zhuanlan.zhihu.com/p/48295486 #### threadlocal https://juejin.im/post/5ac2eb52518825555e5e06ee @@ -467,6 +373,8 @@ https://juejin.im/post/5d1882b1f265da1ba84aa676#heading-14 https://www.cnblogs.com/kismetv/p/7806063.html +# 五 Java框架(ssm) + ### hibernate #### Hibernate 的生成策略 @@ -477,11 +385,11 @@ https://blog.csdn.net/itmyhome1990/article/details/54863822 #### Hibernate 与 Mybatis 区别 -- https://blog.csdn.net/wangpeng047/article/details/17038659 +https://blog.csdn.net/wangpeng047/article/details/17038659 #### Mybatis原理 -- https://www.javazhiyin.com/34438.html +https://www.javazhiyin.com/34438.html #### mybatis执行select的过程 @@ -545,9 +453,9 @@ https://juejin.im/post/5b06bf2df265da0de2574ee1 #### Spring 的加载流程,Spring 的源码中 Bean 的构造的流程 spring ioc系列文章:http://cmsblogs.com/?p=2806 -- 加载流程(概述):https://www.jianshu.com/p/5fd1922ccab1 -- 循环依赖问题:https://blog.csdn.net/u010853261/article/details/77940767 -- https://blog.csdn.net/a15119273009/article/details/108007864 +加载流程(概述):https://www.jianshu.com/p/5fd1922ccab1 +循环依赖问题:https://blog.csdn.net/u010853261/article/details/77940767 +https://blog.csdn.net/a15119273009/article/details/108007864 #### Spring 事务源码,IOC 源码,AOP 源码 @@ -582,8 +490,8 @@ https://blog.csdn.net/chenleixing/article/details/44570681 #### 项目中 Spring 的 IOC 和 AOP 具体怎么使用的 -- https://www.cnblogs.com/xdp-gacl/p/4249939.html -- https://juejin.im/post/5b06bf2df265da0de2574ee1 +https://www.cnblogs.com/xdp-gacl/p/4249939.html +https://juejin.im/post/5b06bf2df265da0de2574ee1 #### spring mvc 底层实现原理 @@ -595,157 +503,36 @@ https://juejin.im/post/5a3284a75188252970793195 #### 如果使用 spring mvc,那 post 请求跟 put 请求有什么区别啊; 然后开始问 springmvc:描述从 tomcat 开始到 springmvc 返回到前端显示的整个流程,接着问 springmvc 中的 handlerMapping 的内部实现,然后又问 spring 中从载入 xml 文件到 getbean 整个流程,描述一遍 -### springboot & springcloud + +# 六 微服务(springboot等) #### springboot -- [springboot面试题](https://mp.weixin.qq.com/s/id0Ga1OC4D3Hu6lkzc9hRg) -- [springboot面试题2](https://mp.weixin.qq.com/s/XGIErbCx2i6Y8vBgw1gq_Q) +[springboot面试题](https://mp.weixin.qq.com/s/id0Ga1OC4D3Hu6lkzc9hRg) +[springboot面试题2](https://mp.weixin.qq.com/s/XGIErbCx2i6Y8vBgw1gq_Q) #### springcloud -- [springcloudm面试题](https://mp.weixin.qq.com/s/CYfLA9s9zhwcIwJjMFXhQQ) - - - -### servlet - -#### Servlet 知道是做什么的吗?和 JSP 有什么联系? - -jsp就是在html里面写java代码,servlet就是在java里面写html代码…其实jsp经过容器解释之后就是servlet.只是我们自己写代码的时候尽量能让它们各司其职,jsp更注重前端显示,servlet更注重模型和业务逻辑。不要写出万能的jsp或servlet来即可。 - -作者:知乎用户 -链接:https://www.zhihu.com/question/37962386/answer/74906895 - - -#### JSP 的运行原理? - -jsp/servlet原理:https://www.jianshu.com/p/93736c3b448b - -#### JSP 属于 Java 中 的吗? - -#### Servlet 是线程安全 - -https://blog.csdn.net/qq_24145735/article/details/52433096 -https://www.cnblogs.com/chanshuyi/p/5052426.html - -#### servlet 是单例 - -#### servlet 和 filter 的区别。 - -https://blog.csdn.net/weixin_42669555/article/details/81049423 - -#### servlet jsp tomcat常见面试题 - -https://juejin.im/post/5a75ab4b6fb9a063592ba9db -https://blog.csdn.net/shxz130/article/details/39735373 - - -### Redis - -#### redis为什么快? - -https://zhuanlan.zhihu.com/p/57089960 - -#### Redis 数据结构原理 - -https://blog.csdn.net/jackFXX/article/details/82318080 - -- Redis 持久化机制 - -- Redis 的一致性哈希算法 - -- redis了解多少 - -- redis五种数据类型,当散列类型的 value 值非常大的时候怎么进行压缩 - -#### 用redis怎么实现摇一摇与附近的人功能 - -https://blog.csdn.net/smartwu_sir/article/details/80254733 - -- redis 主从复制过程 - -- Redis 如何解决 key 冲突 - -- redis 是怎么存储数据的 - -- redis 使用场景 - -### 框架其他 - -#### Servlet 的 Filter 用的什么设计模式 - -https://www.jianshu.com/p/e4197a54828d - -- zookeeper 的常用功能,自己用它来做什么 - - -#### ibatis 是怎么实现映射的,它的映射原理是什么 - -mybatis面试题:https://zhuanlan.zhihu.com/p/44464109 - -#### redis 的操作是不是原子操作 - -https://juejin.im/entry/58f9e22044d9040069d40dca - -#### 秒杀业务场景设计 - -- WebSocket 长连接问题 - -- 如何设计淘宝秒杀系统(重点关注架构,比如数据一致性,数据库集群一致性哈希,缓存, 分库分表等等) - -- List 接口去实例化一个它的实现类(ArrayList)以及直接用 ArrayList 去 new 一个该类的对 象,这两种方式有什么区别 - -#### Tomcat 关注哪些参数 (tomcat调优) - -https://juejin.im/post/5ac034f351882548fe4a4383 - -https://testerhome.com/topics/16082 - - -- 对后台的优化有了解吗?比如负载均衡 - -我给面试官说了 Ngix+Tomcat 负载均 衡,异步处理(消息缓冲服务器),缓存(Redis, Memcache), NoSQL,数据库优化,存储索引优化 - +[springcloud面试题](https://mp.weixin.qq.com/s/CYfLA9s9zhwcIwJjMFXhQQ) -#### 对 Restful 了解 Restful 的认识,优点,以及和 soap 的区别 - -https://www.ruanyifeng.com/blog/2011/09/restful.html - -- lrucache 的基本原理 - - -### 设计模式 - -#### Java常见设计模式 - -- https://www.jianshu.com/p/61b67ca754a3 - -- 单例模式(双检锁模式)、简单工厂、观察者模式、适配器模式、职责链模式等等 - -- 享元模式模式 选两个画下 UML 图 - -- 手写单例 - -写的是静态内部类的单例,然后他问我这个地方为什么用 private,这儿为啥用 static, 这就考察你的基本功啦 - -- 静态类与单例模式的区别 -- 单例模式 double check 单例模式都有什么,都是否线程安全,怎么改进(从 synchronized 到 双重检验锁 到 枚举 Enum) +# 七 数据结构 -- 基本的设计模式及其核心思想 +#### 二叉树相关 -- 来,我们写一个单例模式的实现 +https://www.jianshu.com/p/655d83f9ba7b +https://www.jianshu.com/p/ff4b93b088eb -这里有一个深坑,详情请见《 JVM 》 第 370 页 +#### 红黑树 +https://www.jianshu.com/p/e136ec79235c +https://zhuanlan.zhihu.com/p/31805309 -- 基本的设计原则 -如果有人问你接口里的属性为什么都是 final static 的,记得和他聊一聊设计原则。 +# 八 数据库 -### 数据库 +### MySQL #### 数据库死锁问题 @@ -763,39 +550,39 @@ https://www.jianshu.com/p/8845ddca3b23 #### count(1)、count(*)、count(列名) -- https://blog.csdn.net/iFuMI/article/details/77920767 +https://blog.csdn.net/iFuMI/article/details/77920767 #### mysql的undo、redo、binlog的区别 -- https://mp.weixin.qq.com/s/0z6GmUp0Lb1hDUo0EyYiUg +https://mp.weixin.qq.com/s/0z6GmUp0Lb1hDUo0EyYiUg #### explain解释 -- https://segmentfault.com/a/1190000010293791 +https://segmentfault.com/a/1190000010293791 #### mysql分页查询优化 -- https://blog.csdn.net/hanchao5272/article/details/102790490 +https://blog.csdn.net/hanchao5272/article/details/102790490 #### sql注入 -- https://blog.csdn.net/github_36032947/article/details/78442189 +https://blog.csdn.net/github_36032947/article/details/78442189 #### 为什么用B+树 -- https://blog.csdn.net/xlgen157387/article/details/79450295 +https://blog.csdn.net/xlgen157387/article/details/79450295 #### sql执行流程 -- https://juejin.im/post/5b7036de6fb9a009c40997eb +https://juejin.im/post/5b7036de6fb9a009c40997eb #### 聚集索引与非聚集索引 -- https://juejin.im/post/5cdd701ee51d453a36384939 +https://juejin.im/post/5cdd701ee51d453a36384939 #### 覆盖索引 -- https://www.jianshu.com/p/77eaad62f974 +https://www.jianshu.com/p/77eaad62f974 #### sql总结 @@ -809,62 +596,55 @@ https://blog.csdn.net/yixuandong9010/article/details/72286029 https://juejin.im/post/5cbdbb455188250ab224802d - #### 500万数字排序,内存只能容纳5万个,如何排序,如何优化? 参考文章:https://juejin.im/entry/5a27cb796fb9a045104a5e8c -- 平时怎么写数据库的模糊查询(由字典树扯到模糊查询,前缀查询,例如“abc%”,还是索引策略的问题) +#### 平时怎么写数据库的模糊查询(由字典树扯到模糊查询,前缀查询,例如“abc%”,还是索引策略的问题) -- 数据库里有 10000000 条用户信息,需要给每位用户发送信息(必须发送成功),要求节省内存 +#### 数据库里有 10000000 条用户信息,需要给每位用户发送信息(必须发送成功),要求节省内存 -- 项目中如何实现事务 +#### 项目中如何实现事务 #### 数据库设计一般设计成第几范式 https://blog.csdn.net/hsd2012/article/details/51018631 -- mysql 用的什么版本 5.7 跟 5.6 有啥区别 +#### mysql 用的什么版本 5.7 跟 5.6 有啥区别 #### 提升 MySQL 安全性 https://blog.csdn.net/listen_for/article/details/53907270 -- 问了一个这样的表(三个字段:姓名,id,分数)要求查出平均分大于 80 的 id 然后分数降序排序,然后经过提示用聚合函数 avg。 +#### 问了一个这样的表(三个字段:姓名,id,分数)要求查出平均分大于 80 的 id 然后分数降序排序,然后经过提示用聚合函数 avg。 select id from table group by id having avg(score) > 80 order by avg(score) desc。 -- 为什么 mysql 事务能保证失败回滚 - - -#### 一道算法题,在一个整形数组中,找出第三大的数,注意时间效率 (使用堆) +#### 为什么 mysql 事务能保证失败回滚 - - -- 主键索引底层的实现原理 +#### 主键索引底层的实现原理 B+树 -- 经典的01索引问题? +#### 经典的01索引问题? -- 如何在长文本中快捷的筛选出你的名字? +#### 如何在长文本中快捷的筛选出你的名字? 全文索引 -- 多列索引及最左前缀原则和其他使用场景 +#### 多列索引及最左前缀原则和其他使用场景 -- 事务隔离级别 +#### 事务隔离级别 -- 索引的最左前缀原则 +#### 索引的最左前缀原则 -- 数据库悲观锁怎么实现的 +#### 数据库悲观锁怎么实现的 https://www.jianshu.com/p/f5ff017db62a -- 建表的原则 - -- 索引的内涵和用法 +#### 建表的原则 +#### 索引的内涵和用法 -- 给了两条 SQL 语句,让根据这两条语句建索引(个人想法:主要考虑复合索引只能匹配前缀列的特点) +#### 给了两条 SQL 语句,让根据这两条语句建索引(个人想法:主要考虑复合索引只能匹配前缀列的特点) #### 那么我们来聊一下数据库。A 和 B 两个表做等值连接(Inner join) 怎么优化 @@ -875,29 +655,76 @@ https://blog.csdn.net/hguisu/article/details/5731880 #### 数据库连接池的理解和优化 -- Sql 语句 分组排序 +#### Sql语句分组排序 -- SQL 语句的 5 个连接概念 +#### SQL语句的5个连接概念 -- 数据库优化和架构(主要是主从分离和分库分表相关) +#### 数据库优化和架构(主要是主从分离和分库分表相关) -分库分表 +#### 分库分表 -- 跨库join实现 +#### 跨库join实现 -- 探讨主从分离和分库分表相关 +#### 探讨主从分离和分库分表相关 -- 数据库中间件 +#### 数据库中间件 -- 读写分离在中间件的实现 +#### 读写分离在中间件的实现 -- 限流 and 熔断 +#### 限流 and 熔断 #### 行锁适用场景 https://cloud.tencent.com/developer/article/1104098 -### 计算机网络 + +### Redis + +#### redis为什么快? + +https://zhuanlan.zhihu.com/p/57089960 + +#### Redis 数据结构原理 + +https://blog.csdn.net/jackFXX/article/details/82318080 + +#### Redis 持久化机制 + +#### Redis 的一致性哈希算法 + +#### redis了解多少 + +#### redis五种数据类型,当散列类型的 value 值非常大的时候怎么进行压缩 + +#### 用redis怎么实现摇一摇与附近的人功能 + +https://blog.csdn.net/smartwu_sir/article/details/80254733 + +#### redis 主从复制过程 + +#### Redis 如何解决 key 冲突 + +#### redis 是怎么存储数据的 + +#### redis 使用场景 + +# 九 计算机网络 + +#### cookie 禁用怎么办 + +https://segmentfault.com/q/1010000007715137 + +#### Netty new 实例化过程 + +#### socket 实现过程,具体用的方法;怎么实现异步 socket. + +https://blog.csdn.net/charjay_lin/article/details/81810922 + +#### 浏览器的缓存机制 + +说明计算机网络的知识还没有记住 + +https://www.cnblogs.com/yangyangxxb/p/10218871.html #### http相关问题 @@ -909,13 +736,13 @@ https://www.cnblogs.com/wuyepeng/p/9801470.html #### dns属于udp还是tcp,原因 -- https://www.zhihu.com/question/310145373 +https://www.zhihu.com/question/310145373 #### http的幂等性 -- https://www.cnblogs.com/weidagang2046/archive/2011/06/04/idempotence.html +https://www.cnblogs.com/weidagang2046/archive/2011/06/04/idempotence.html -- 建立连接的过程客户端跟服务端会交换什么信息(参考 TCP 报文结构) +#### 建立连接的过程客户端跟服务端会交换什么信息(参考 TCP 报文结构) #### 丢包如何解决重传的消耗 @@ -927,7 +754,7 @@ https://zhuanlan.zhihu.com/p/36811672 #### IO多路复用 -- https://sanyuesha.com/python-server-tutorial/book/ch05.html +https://sanyuesha.com/python-server-tutorial/book/ch05.html #### select 和 poll 区别? @@ -937,7 +764,7 @@ https://zhuanlan.zhihu.com/p/36811672 服务器推送:https://juejin.im/post/5c20e5766fb9a049b13e387b -可以使用客户端定时刷新请求或者和 TCP 保持心跳连接实现。 +#### 可以使用客户端定时刷新请求或者和 TCP 保持心跳连接实现。 #### 查看磁盘读写吞吐量? @@ -951,27 +778,51 @@ https://www.cnblogs.com/ggjucheng/archive/2013/01/13/2858810.html https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Redirections -- controller 怎么处理的请求:路由 +#### controller 怎么处理的请求:路由 #### IP 地址分为几类,每类都代表什么,私网是哪些 https://zhuanlan.zhihu.com/p/54593244 + +# 十 操作系统 + +#### Java I/O 底层细节,注意是底层细节,而不是怎么用 + +可以从Java IO底层、JavaIO模型(阻塞、异步等) +https://www.cnblogs.com/crazymakercircle/p/10225159.html + +#### Java IO 模型(BIO,NIO 等) ,Tomcat 用的哪一种模型 + +tomcat支持:https://blog.csdn.net/fd2025/article/details/80007435 + +#### 当获取第一个获取锁之后,条件不满足需要释放锁应当怎么做? + +https://www.jianshu.com/p/eb112b25b848 + +#### 手写一个线程安全的生产者与消费者。 + +https://blog.csdn.net/u010983881/article/details/78554671 + +#### 进程和线程调度方法 + +https://www.jianshu.com/p/91c8600cb2ae + ### linux #### linux查找命令 -- https://blog.51cto.com/whylinux/2043871 +https://blog.51cto.com/whylinux/2043871 #### 项目部署常见linux命令 -- https://blog.csdn.net/u010938610/article/details/79625988 +https://blog.csdn.net/u010938610/article/details/79625988 -- 进程文件里有哪些信息 +#### 进程文件里有哪些信息 #### sed 和 awk 的区别 -- awk用法:https://www.cnblogs.com/isykw/p/6258781.html +awk用法:https://www.cnblogs.com/isykw/p/6258781.html 其实sed和awk都是每次读入一行来处理的,区别是:sed 适合简单的文本替换和搜索;而awk除了自动给你分列之外,里面丰富的函数大大增强了awk的功能。数据统计,正则表达式搜索,逻辑处理,前后置脚本等。因此基本上sed能做的,awk可以全部完成并且做的更好。 @@ -982,8 +833,6 @@ https://zhuanlan.zhihu.com/p/54593244 https://blog.csdn.net/qingmu0803/article/details/38271077 - - #### 有一个文件被锁住,如何查看锁住它的线程? #### 如何查看一个文件第100行到150行的内容 @@ -996,7 +845,7 @@ https://www.cnblogs.com/freeweb/p/5407105.html https://blog.csdn.net/inuyashaw/article/details/55095545 -- linux 如何查找文件 +#### linux 如何查找文件 linux命令:https://juejin.im/post/5d3857eaf265da1bd04f2437 @@ -1004,27 +853,59 @@ linux命令:https://juejin.im/post/5d3857eaf265da1bd04f2437 https://juejin.im/post/5b624f4d518825068302aee9#heading-13 -### 安全加密 +# 十一 框架其他 -- http://www.ruanyifeng.com/blog/2011/08/what_is_a_digital_signature.html -- https://yq.aliyun.com/articles/54155 +#### Servlet 的 Filter 用的什么设计模式 -#### web安全问题 +https://www.jianshu.com/p/e4197a54828d -https://juejin.im/post/5da44c5de51d45783a772a22 +#### zookeeper 的常用功能,自己用它来做什么 -### 分布式 +#### redis 的操作是不是原子操作 -#### dubbo中的dubbo协议和http协议有什么区别? +https://juejin.im/entry/58f9e22044d9040069d40dca + +#### 秒杀业务场景设计 + +#### 如何设计淘宝秒杀系统(重点关注架构,比如数据一致性,数据库集群一致性哈希,缓存, 分库分表等等) + + +#### 对后台的优化有了解吗?比如负载均衡 + +我给面试官说了 Ngix+Tomcat 负载均 衡,异步处理(消息缓冲服务器),缓存(Redis, Memcache), NoSQL,数据库优化,存储索引优化 + + +#### 对 Restful 了解 Restful 的认识,优点,以及和 soap 的区别 + +https://www.ruanyifeng.com/blog/2011/09/restful.html + +#### lrucache 的基本原理 + + +# 十二 设计模式 + +#### Java常见设计模式 + +- https://www.jianshu.com/p/61b67ca754a3 +- 单例模式(双检锁模式)、简单工厂、观察者模式、适配器模式、职责链模式等等 +- 享元模式模式 选两个画下 UML 图 +- 手写单例 +写的是静态内部类的单例,然后他问我这个地方为什么用 private,这儿为啥用 static, 这就考察你的基本功啦 +- 静态类与单例模式的区别 +- 单例模式 double check 单例模式都有什么,都是否线程安全,怎么改进(从 synchronized 到 双重检验锁 到 枚举 Enum) +- 基本的设计模式及其核心思想 +- 来,我们写一个单例模式的实现 + +# 十三 分布式 -- https://blog.csdn.net/wjw_77/article/details/99696757 +#### dubbo中的dubbo协议和http协议有什么区别? +https://blog.csdn.net/wjw_77/article/details/99696757 #### 负载均衡 https://juejin.im/post/5b39eea0e51d4558c1010e36 - #### 分布式锁的实现方式及优缺点 https://zhuanlan.zhihu.com/p/62158041 @@ -1033,8 +914,32 @@ https://zhuanlan.zhihu.com/p/62158041 https://www.jianshu.com/p/8025e3346734 -### 操作系统 +#### 如何实现分布式缓存 -#### 进程和线程调度方法 +redis如何实现分布式缓存 +https://stor.51cto.com/art/201912/607229.htm + +# 十四 其他 + +##### Java 8 函数式编程 回调函数 + +#### 函数式编程,面向对象之间区别 -https://www.jianshu.com/p/91c8600cb2ae \ No newline at end of file +#### Java 8 中 stream 迭代的优势和区别? + +#### 同步等于可见性吗? + +保证了可见性不等于正确同步,因为还有原子性没考虑。 + +#### git底层数据结构 + +https://blog.csdn.net/leo187/article/details/106233706 + +#### 安全加密 + +http://www.ruanyifeng.com/blog/2011/08/what_is_a_digital_signature.html +https://yq.aliyun.com/articles/54155 + +#### web安全问题 + +https://juejin.im/post/5da44c5de51d45783a772a22 From f1f70515069415881c6510a8abe646b491ec2dee Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Tue, 6 Oct 2020 15:55:59 +0800 Subject: [PATCH 19/38] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E5=B8=B8=E8=A7=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...06\347\261\273\346\261\207\346\200\273.md" | 246 +++++++++++++++++- 1 file changed, 245 insertions(+), 1 deletion(-) diff --git "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" index 6694ca1..9f79df6 100644 --- "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" +++ "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" @@ -1,4 +1,248 @@ -[TOC] +- [一 Java基础](#一-java基础) + - [一致性hash算法](#一致性hash算法) + - [sleep和wait](#sleep和wait) + - [强软弱虚引用](#强软弱虚引用) + - [Arrays.sort原理](#arrayssort原理) + - [创建对象的方式](#创建对象的方式) + - [若hashcode方法永远返回1会产生什么结果](#若hashcode方法永远返回1会产生什么结果) + - [解决hash冲突的三种方法](#解决hash冲突的三种方法) + - [为什么要重写hashCode()方法和equals()方法以及如何进行重写](#为什么要重写hashcode方法和equals方法以及如何进行重写) + - [动态代理](#动态代理) + - [sleep和wait的区别](#sleep和wait的区别) + - [java 地址和值传递的例子](#java-地址和值传递的例子) + - [Java序列化](#java序列化) + - [java NIO,java 多线程、线程池,java 网络编程解决并发量](#java-niojava-多线程线程池java-网络编程解决并发量) + - [JDBC 连接的过程 ,手写 jdbc 连接过程](#jdbc-连接的过程-手写-jdbc-连接过程) + - [说出三个遇到过的程序报异常的情况](#说出三个遇到过的程序报异常的情况) + - [socket 是靠什么协议支持的](#socket-是靠什么协议支持的) + - [java io 用到什么设计模式](#java-io-用到什么设计模式) + - [serviable 的序列化,其中 uuid 的作用](#serviable-的序列化其中-uuid-的作用) + - [什么情景下会用到反射](#什么情景下会用到反射) + - [浅克隆与深克隆有什么区别,如何实现深克隆](#浅克隆与深克隆有什么区别如何实现深克隆) + - [反射能够使用私有的方法属性吗和底层原理?](#反射能够使用私有的方法属性吗和底层原理) + - [处理器指令优化有些什么考虑?](#处理器指令优化有些什么考虑) + - [object 对象的常用方法](#object-对象的常用方法) + - [Stack 和 ArrayList 的区别](#stack-和-arraylist-的区别) + - [statement 和 prestatement 的区别](#statement-和-prestatement-的区别) + - [手写模拟实现一个阻塞队列](#手写模拟实现一个阻塞队列) + - [util 包下有哪几种接口](#util-包下有哪几种接口) + - [很常见的 Nullpointerexception ,你是怎么排查的,怎么解决的;](#很常见的-nullpointerexception-你是怎么排查的怎么解决的) + - [静态内部类和非静态内部类的区别是什么?](#静态内部类和非静态内部类的区别是什么) + - [怎么创建静态内部类和非静态内部类?](#怎么创建静态内部类和非静态内部类) + - [Xml 解析方式,原理优缺点](#xml-解析方式原理优缺点) + - [静态变量和全局变量的区别](#静态变量和全局变量的区别) +- [二 Java集合](#二-java集合) + - [hashmap的jdk1.7和jdk1.8区别](#hashmap的jdk17和jdk18区别) + - [concurrenthashmap的jdk1.7和jdk1.8区别](#concurrenthashmap的jdk17和jdk18区别) + - [HashMap 实现原理,扩容因子过大过小的缺点,扩容过程 采用什么方法能保证每个 bucket 中的数据更均匀 解决冲突的方式,还有没有其他方式(全域哈希)](#hashmap-实现原理扩容因子过大过小的缺点扩容过程-采用什么方法能保证每个-bucket-中的数据更均匀-解决冲突的方式还有没有其他方式全域哈希) + - [Collection 集合类中只能在 Iterator 中删除元素的原因](#collection-集合类中只能在-iterator-中删除元素的原因) + - [ArrayList、LinkedList、Vector](#arraylistlinkedlistvector) + - [还了解除 util 其他包下的 List 吗?](#还了解除-util-其他包下的-list-吗) + - [CopyOnWriteArrayList](#copyonwritearraylist) + - [ConcurrentHashMap 和 LinkedHashMap 差异和适用情形](#concurrenthashmap-和-linkedhashmap-差异和适用情形) + - [ConcurrentHashMap分段锁是如何实现,ConcurrentHashmap jdk1.8 访问的时候是怎么加锁的,插入的时候是怎么加锁的 访问不加 锁插入的时候对头结点加锁](#concurrenthashmap分段锁是如何实现concurrenthashmap-jdk18-访问的时候是怎么加锁的插入的时候是怎么加锁的-访问不加-锁插入的时候对头结点加锁) + - [ArrayDeque 的使用场景](#arraydeque-的使用场景) + - [ArrayBlockingQueue 源码](#arrayblockingqueue-源码) + - [hashmap 和 treemap 的区别](#hashmap-和-treemap-的区别) + - [hashmap](#hashmap) + - [treemap](#treemap) + - [rehash 过程](#rehash-过程) + - [HashMap 的负载因子,为什么容量为2^n](#hashmap-的负载因子为什么容量为2n) + - [list,map,set 之间的区别](#listmapset-之间的区别) + - [什么时候会用到 HashMap](#什么时候会用到-hashmap) + - [常见的线程安全的集合类](#常见的线程安全的集合类) +- [三 JVM](#三-jvm) + - [反射在jvm层面的实现](#反射在jvm层面的实现) + - [jvm的方法区存什么?](#jvm的方法区存什么) + - [JDK1.8 JVM方法区变成了什么,为什么这样做](#jdk18-jvm方法区变成了什么为什么这样做) + - [oom出现的原因](#oom出现的原因) + - [Class.forName和ClassLoader的区别](#classforname和classloader的区别) + - [java对象信息分配](#java对象信息分配) + - [java虚拟机ZGC详解](#java虚拟机zgc详解) + - [java虚拟机CMS详解](#java虚拟机cms详解) + - [java虚拟机G1详解](#java虚拟机g1详解) + - [JVM tomcat 容器启动,jvm 加载情况描述](#jvm-tomcat-容器启动jvm-加载情况描述) +- [四 多线程并发](#四-多线程并发) + - [volitale使用场景](#volitale使用场景) + - [可重入锁,实现原理](#可重入锁实现原理) + - [Java 无锁原理](#java-无锁原理) + - [讲讲多线程,多线程的同步方法](#讲讲多线程多线程的同步方法) + - [synchronized原理](#synchronized原理) + - [reetrantlock](#reetrantlock) + - [java 线程安全都体现在哪些方面](#java-线程安全都体现在哪些方面) + - [如果维护线程安全 如果想实现一个线程安全的队列,可以怎么实现?](#如果维护线程安全-如果想实现一个线程安全的队列可以怎么实现) + - [Java多线程通信方式](#java多线程通信方式) + - [CountDownLatch、CyclicBarrier、Semaphore 用法总结](#countdownlatchcyclicbarriersemaphore-用法总结) + - [juc下的内容](#juc下的内容) + - [AOS等并发相关面试题](#aos等并发相关面试题) + - [threadlocal](#threadlocal) + - [java 线程池达到提交上限的具体情况 ,线程池用法,Java 多线程,线程池有哪几类,每一类的差别](#java-线程池达到提交上限的具体情况-线程池用法java-多线程线程池有哪几类每一类的差别) + - [要你设计的话,如何实现一个线程池](#要你设计的话如何实现一个线程池) + - [线程池的类型,固定大小的线程池内部是如何实现的,等待队列是用了哪一个队列实现 线程池种类和工作流程](#线程池的类型固定大小的线程池内部是如何实现的等待队列是用了哪一个队列实现-线程池种类和工作流程) + - [线程池使用了什么设计模式](#线程池使用了什么设计模式) + - [线程池使用时一般要考虑哪些问题](#线程池使用时一般要考虑哪些问题) + - [线程池的配置](#线程池的配置) + - [Excutor 以及 Connector 的配置](#excutor-以及-connector-的配置) +- [五 Java框架(ssm)](#五-java框架ssm) + - [hibernate](#hibernate) + - [Hibernate 的生成策略](#hibernate-的生成策略) + - [Hibernate 与 Mybatis 区别](#hibernate-与-mybatis-区别) + - [Mybatis原理](#mybatis原理) + - [mybatis执行select的过程](#mybatis执行select的过程) + - [mybatis有哪些executors](#mybatis有哪些executors) + - [mybatis插件原理](#mybatis插件原理) + - [mybatis二级缓存](#mybatis二级缓存) + - [spring&springmvc](#springspringmvc) + - [spring中的设计模式](#spring中的设计模式) + - [spring中bean的作用域](#spring中bean的作用域) + - [BeanFactory和FactoryBean区别](#beanfactory和factorybean区别) + - [aspect的种类](#aspect的种类) + - [spring aop的实际应用](#spring-aop的实际应用) + - [spring实现多线程安全](#spring实现多线程安全) + - [spring的bean的高并发安全问题](#spring的bean的高并发安全问题) + - [ioc aop总结(概述性)](#ioc-aop总结概述性) + - [Spring 的加载流程,Spring 的源码中 Bean 的构造的流程](#spring-的加载流程spring-的源码中-bean-的构造的流程) + - [Spring 事务源码,IOC 源码,AOP 源码](#spring-事务源码ioc-源码aop-源码) + - [spring 的作用及理解 事务怎么配置](#spring-的作用及理解-事务怎么配置) + - [spring事务失效情况](#spring事务失效情况) + - [Spring 的 annotation 如何实现](#spring-的-annotation-如何实现) + - [SpringMVC 工作原理](#springmvc-工作原理) + - [了解 SpringMVC 与 Struct2 区别](#了解-springmvc-与-struct2-区别) + - [springMVC 和 spring 是什么关系](#springmvc-和-spring-是什么关系) + - [项目中 Spring 的 IOC 和 AOP 具体怎么使用的](#项目中-spring-的-ioc-和-aop-具体怎么使用的) + - [spring mvc 底层实现原理](#spring-mvc-底层实现原理) + - [动态代理的原理](#动态代理的原理) + - [如果使用 spring mvc,那 post 请求跟 put 请求有什么区别啊; 然后开始问 springmvc:描述从 tomcat 开始到 springmvc 返回到前端显示的整个流程,接着问 springmvc 中的 handlerMapping 的内部实现,然后又问 spring 中从载入 xml 文件到 getbean 整个流程,描述一遍](#如果使用-spring-mvc那-post-请求跟-put-请求有什么区别啊-然后开始问-springmvc描述从-tomcat-开始到-springmvc-返回到前端显示的整个流程接着问-springmvc-中的-handlermapping-的内部实现然后又问-spring-中从载入-xml-文件到-getbean-整个流程描述一遍) +- [六 微服务(springboot等)](#六-微服务springboot等) + - [springboot](#springboot) + - [springcloud](#springcloud) +- [七 数据结构](#七-数据结构) + - [二叉树相关](#二叉树相关) + - [红黑树](#红黑树) +- [八 数据库](#八-数据库) + - [MySQL](#mysql) + - [数据库死锁问题](#数据库死锁问题) + - [hash索引和B+树索引的区别](#hash索引和b树索引的区别) + - [可重复的原理MVCC](#可重复的原理mvcc) + - [count(1)、count(*)、count(列名)](#count1countcount列名) + - [mysql的undo、redo、binlog的区别](#mysql的undoredobinlog的区别) + - [explain解释](#explain解释) + - [mysql分页查询优化](#mysql分页查询优化) + - [sql注入](#sql注入) + - [为什么用B+树](#为什么用b树) + - [sql执行流程](#sql执行流程) + - [聚集索引与非聚集索引](#聚集索引与非聚集索引) + - [覆盖索引](#覆盖索引) + - [sql总结](#sql总结) + - [有人建议给每张表都建一个自增主键,这样做有什么优点跟缺点](#有人建议给每张表都建一个自增主键这样做有什么优点跟缺点) + - [对 MySQL 的了解,和 oracle 的区别](#对-mysql-的了解和-oracle-的区别) + - [500万数字排序,内存只能容纳5万个,如何排序,如何优化?](#500万数字排序内存只能容纳5万个如何排序如何优化) + - [平时怎么写数据库的模糊查询(由字典树扯到模糊查询,前缀查询,例如“abc%”,还是索引策略的问题)](#平时怎么写数据库的模糊查询由字典树扯到模糊查询前缀查询例如abc还是索引策略的问题) + - [数据库里有 10000000 条用户信息,需要给每位用户发送信息(必须发送成功),要求节省内存](#数据库里有-10000000-条用户信息需要给每位用户发送信息必须发送成功要求节省内存) + - [项目中如何实现事务](#项目中如何实现事务) + - [数据库设计一般设计成第几范式](#数据库设计一般设计成第几范式) + - [mysql 用的什么版本 5.7 跟 5.6 有啥区别](#mysql-用的什么版本-57-跟-56-有啥区别) + - [提升 MySQL 安全性](#提升-mysql-安全性) + - [问了一个这样的表(三个字段:姓名,id,分数)要求查出平均分大于 80 的 id 然后分数降序排序,然后经过提示用聚合函数 avg。](#问了一个这样的表三个字段姓名id分数要求查出平均分大于-80-的-id-然后分数降序排序然后经过提示用聚合函数-avg) + - [为什么 mysql 事务能保证失败回滚](#为什么-mysql-事务能保证失败回滚) + - [主键索引底层的实现原理](#主键索引底层的实现原理) + - [经典的01索引问题?](#经典的01索引问题) + - [如何在长文本中快捷的筛选出你的名字?](#如何在长文本中快捷的筛选出你的名字) + - [多列索引及最左前缀原则和其他使用场景](#多列索引及最左前缀原则和其他使用场景) + - [事务隔离级别](#事务隔离级别) + - [索引的最左前缀原则](#索引的最左前缀原则) + - [数据库悲观锁怎么实现的](#数据库悲观锁怎么实现的) + - [建表的原则](#建表的原则) + - [索引的内涵和用法](#索引的内涵和用法) + - [给了两条 SQL 语句,让根据这两条语句建索引(个人想法:主要考虑复合索引只能匹配前缀列的特点)](#给了两条-sql-语句让根据这两条语句建索引个人想法主要考虑复合索引只能匹配前缀列的特点) + - [那么我们来聊一下数据库。A 和 B 两个表做等值连接(Inner join) 怎么优化](#那么我们来聊一下数据库a-和-b-两个表做等值连接inner-join-怎么优化) + - [数据库连接池的理解和优化](#数据库连接池的理解和优化) + - [Sql语句分组排序](#sql语句分组排序) + - [SQL语句的5个连接概念](#sql语句的5个连接概念) + - [数据库优化和架构(主要是主从分离和分库分表相关)](#数据库优化和架构主要是主从分离和分库分表相关) + - [分库分表](#分库分表) + - [跨库join实现](#跨库join实现) + - [探讨主从分离和分库分表相关](#探讨主从分离和分库分表相关) + - [数据库中间件](#数据库中间件) + - [读写分离在中间件的实现](#读写分离在中间件的实现) + - [限流 and 熔断](#限流-and-熔断) + - [行锁适用场景](#行锁适用场景) + - [Redis](#redis) + - [redis为什么快?](#redis为什么快) + - [Redis 数据结构原理](#redis-数据结构原理) + - [Redis 持久化机制](#redis-持久化机制) + - [Redis 的一致性哈希算法](#redis-的一致性哈希算法) + - [redis了解多少](#redis了解多少) + - [redis五种数据类型,当散列类型的 value 值非常大的时候怎么进行压缩](#redis五种数据类型当散列类型的-value-值非常大的时候怎么进行压缩) + - [用redis怎么实现摇一摇与附近的人功能](#用redis怎么实现摇一摇与附近的人功能) + - [redis 主从复制过程](#redis-主从复制过程) + - [Redis 如何解决 key 冲突](#redis-如何解决-key-冲突) + - [redis 是怎么存储数据的](#redis-是怎么存储数据的) + - [redis 使用场景](#redis-使用场景) +- [九 计算机网络](#九-计算机网络) + - [cookie 禁用怎么办](#cookie-禁用怎么办) + - [Netty new 实例化过程](#netty-new-实例化过程) + - [socket 实现过程,具体用的方法;怎么实现异步 socket.](#socket-实现过程具体用的方法怎么实现异步-socket) + - [浏览器的缓存机制](#浏览器的缓存机制) + - [http相关问题](#http相关问题) + - [TCP三次握手第三次握手时ACK丢失怎么办](#tcp三次握手第三次握手时ack丢失怎么办) + - [dns属于udp还是tcp,原因](#dns属于udp还是tcp原因) + - [http的幂等性](#http的幂等性) + - [建立连接的过程客户端跟服务端会交换什么信息(参考 TCP 报文结构)](#建立连接的过程客户端跟服务端会交换什么信息参考-tcp-报文结构) + - [丢包如何解决重传的消耗](#丢包如何解决重传的消耗) + - [traceroute 实现原理](#traceroute-实现原理) + - [IO多路复用](#io多路复用) + - [select 和 poll 区别?](#select-和-poll-区别) + - [在不使用 WebSocket 情况下怎么实现服务器推送的一种方法](#在不使用-websocket-情况下怎么实现服务器推送的一种方法) + - [可以使用客户端定时刷新请求或者和 TCP 保持心跳连接实现。](#可以使用客户端定时刷新请求或者和-tcp-保持心跳连接实现) + - [查看磁盘读写吞吐量?](#查看磁盘读写吞吐量) + - [PING 位于哪一层](#ping-位于哪一层) + - [网络重定向,说下流程](#网络重定向说下流程) + - [controller 怎么处理的请求:路由](#controller-怎么处理的请求路由) + - [IP 地址分为几类,每类都代表什么,私网是哪些](#ip-地址分为几类每类都代表什么私网是哪些) +- [十 操作系统](#十-操作系统) + - [Java I/O 底层细节,注意是底层细节,而不是怎么用](#java-io-底层细节注意是底层细节而不是怎么用) + - [Java IO 模型(BIO,NIO 等) ,Tomcat 用的哪一种模型](#java-io-模型bionio-等-tomcat-用的哪一种模型) + - [当获取第一个获取锁之后,条件不满足需要释放锁应当怎么做?](#当获取第一个获取锁之后条件不满足需要释放锁应当怎么做) + - [手写一个线程安全的生产者与消费者。](#手写一个线程安全的生产者与消费者) + - [进程和线程调度方法](#进程和线程调度方法) + - [linux](#linux) + - [linux查找命令](#linux查找命令) + - [项目部署常见linux命令](#项目部署常见linux命令) + - [进程文件里有哪些信息](#进程文件里有哪些信息) + - [sed 和 awk 的区别](#sed-和-awk-的区别) + - [linux查看进程并杀死的命令](#linux查看进程并杀死的命令) + - [有一个文件被锁住,如何查看锁住它的线程?](#有一个文件被锁住如何查看锁住它的线程) + - [如何查看一个文件第100行到150行的内容](#如何查看一个文件第100行到150行的内容) + - [如何查看进程消耗的资源](#如何查看进程消耗的资源) + - [如何查看每个进程下的线程?](#如何查看每个进程下的线程) + - [linux 如何查找文件](#linux-如何查找文件) + - [select epoll等问题](#select-epoll等问题) +- [十一 框架其他](#十一-框架其他) + - [Servlet 的 Filter 用的什么设计模式](#servlet-的-filter-用的什么设计模式) + - [zookeeper 的常用功能,自己用它来做什么](#zookeeper-的常用功能自己用它来做什么) + - [redis 的操作是不是原子操作](#redis-的操作是不是原子操作) + - [秒杀业务场景设计](#秒杀业务场景设计) + - [如何设计淘宝秒杀系统(重点关注架构,比如数据一致性,数据库集群一致性哈希,缓存, 分库分表等等)](#如何设计淘宝秒杀系统重点关注架构比如数据一致性数据库集群一致性哈希缓存-分库分表等等) + - [对后台的优化有了解吗?比如负载均衡](#对后台的优化有了解吗比如负载均衡) + - [对 Restful 了解 Restful 的认识,优点,以及和 soap 的区别](#对-restful-了解-restful-的认识优点以及和-soap-的区别) + - [lrucache 的基本原理](#lrucache-的基本原理) +- [十二 设计模式](#十二-设计模式) + - [Java常见设计模式](#java常见设计模式) +- [十三 分布式](#十三-分布式) + - [dubbo中的dubbo协议和http协议有什么区别?](#dubbo中的dubbo协议和http协议有什么区别) + - [负载均衡](#负载均衡) + - [分布式锁的实现方式及优缺点](#分布式锁的实现方式及优缺点) + - [CAP](#cap) + - [如何实现分布式缓存](#如何实现分布式缓存) +- [十四 其他](#十四-其他) + - [Java 8 函数式编程 回调函数](#java-8-函数式编程-回调函数) + - [函数式编程,面向对象之间区别](#函数式编程面向对象之间区别) + - [Java 8 中 stream 迭代的优势和区别?](#java-8-中-stream-迭代的优势和区别) + - [同步等于可见性吗?](#同步等于可见性吗) + - [git底层数据结构](#git底层数据结构) + - [安全加密](#安全加密) + - [web安全问题](#web安全问题) # 一 Java基础 From 4c0ac60f7604c620fb98ba3f1f961c05a85f63ff Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Tue, 6 Oct 2020 16:11:14 +0800 Subject: [PATCH 20/38] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E5=B8=B8=E8=A7=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...06\347\261\273\346\261\207\346\200\273.md" | 498 +++++++++--------- 1 file changed, 253 insertions(+), 245 deletions(-) diff --git "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" index 9f79df6..0aa0960 100644 --- "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" +++ "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" @@ -1,250 +1,258 @@ + + + + + - [一 Java基础](#一-java基础) - - [一致性hash算法](#一致性hash算法) - - [sleep和wait](#sleep和wait) - - [强软弱虚引用](#强软弱虚引用) - - [Arrays.sort原理](#arrayssort原理) - - [创建对象的方式](#创建对象的方式) - - [若hashcode方法永远返回1会产生什么结果](#若hashcode方法永远返回1会产生什么结果) - - [解决hash冲突的三种方法](#解决hash冲突的三种方法) - - [为什么要重写hashCode()方法和equals()方法以及如何进行重写](#为什么要重写hashcode方法和equals方法以及如何进行重写) - - [动态代理](#动态代理) - - [sleep和wait的区别](#sleep和wait的区别) - - [java 地址和值传递的例子](#java-地址和值传递的例子) - - [Java序列化](#java序列化) - - [java NIO,java 多线程、线程池,java 网络编程解决并发量](#java-niojava-多线程线程池java-网络编程解决并发量) - - [JDBC 连接的过程 ,手写 jdbc 连接过程](#jdbc-连接的过程-手写-jdbc-连接过程) - - [说出三个遇到过的程序报异常的情况](#说出三个遇到过的程序报异常的情况) - - [socket 是靠什么协议支持的](#socket-是靠什么协议支持的) - - [java io 用到什么设计模式](#java-io-用到什么设计模式) - - [serviable 的序列化,其中 uuid 的作用](#serviable-的序列化其中-uuid-的作用) - - [什么情景下会用到反射](#什么情景下会用到反射) - - [浅克隆与深克隆有什么区别,如何实现深克隆](#浅克隆与深克隆有什么区别如何实现深克隆) - - [反射能够使用私有的方法属性吗和底层原理?](#反射能够使用私有的方法属性吗和底层原理) - - [处理器指令优化有些什么考虑?](#处理器指令优化有些什么考虑) - - [object 对象的常用方法](#object-对象的常用方法) - - [Stack 和 ArrayList 的区别](#stack-和-arraylist-的区别) - - [statement 和 prestatement 的区别](#statement-和-prestatement-的区别) - - [手写模拟实现一个阻塞队列](#手写模拟实现一个阻塞队列) - - [util 包下有哪几种接口](#util-包下有哪几种接口) - - [很常见的 Nullpointerexception ,你是怎么排查的,怎么解决的;](#很常见的-nullpointerexception-你是怎么排查的怎么解决的) - - [静态内部类和非静态内部类的区别是什么?](#静态内部类和非静态内部类的区别是什么) - - [怎么创建静态内部类和非静态内部类?](#怎么创建静态内部类和非静态内部类) - - [Xml 解析方式,原理优缺点](#xml-解析方式原理优缺点) - - [静态变量和全局变量的区别](#静态变量和全局变量的区别) + - [一致性hash算法](#一致性hash算法) + - [sleep和wait](#sleep和wait) + - [强软弱虚引用](#强软弱虚引用) + - [Arrays.sort原理](#arrayssort原理) + - [创建对象的方式](#创建对象的方式) + - [若hashcode方法永远返回1会产生什么结果](#若hashcode方法永远返回1会产生什么结果) + - [解决hash冲突的三种方法](#解决hash冲突的三种方法) + - [为什么要重写hashCode()方法和equals()方法以及如何进行重写](#为什么要重写hashcode方法和equals方法以及如何进行重写) + - [动态代理](#动态代理) + - [sleep和wait的区别](#sleep和wait的区别) + - [java 地址和值传递的例子](#java-地址和值传递的例子) + - [Java序列化](#java序列化) + - [java NIO,java 多线程、线程池,java 网络编程解决并发量](#java-niojava-多线程线程池java-网络编程解决并发量) + - [JDBC 连接的过程 ,手写 jdbc 连接过程](#jdbc-连接的过程-手写-jdbc-连接过程) + - [说出三个遇到过的程序报异常的情况](#说出三个遇到过的程序报异常的情况) + - [socket 是靠什么协议支持的](#socket-是靠什么协议支持的) + - [java io 用到什么设计模式](#java-io-用到什么设计模式) + - [serviable 的序列化,其中 uuid 的作用](#serviable-的序列化其中-uuid-的作用) + - [什么情景下会用到反射](#什么情景下会用到反射) + - [浅克隆与深克隆有什么区别,如何实现深克隆](#浅克隆与深克隆有什么区别如何实现深克隆) + - [反射能够使用私有的方法属性吗和底层原理?](#反射能够使用私有的方法属性吗和底层原理) + - [处理器指令优化有些什么考虑?](#处理器指令优化有些什么考虑) + - [object 对象的常用方法](#object-对象的常用方法) + - [Stack 和 ArrayList 的区别](#stack-和-arraylist-的区别) + - [statement 和 prestatement 的区别](#statement-和-prestatement-的区别) + - [手写模拟实现一个阻塞队列](#手写模拟实现一个阻塞队列) + - [util 包下有哪几种接口](#util-包下有哪几种接口) + - [很常见的 Nullpointerexception ,你是怎么排查的,怎么解决的;](#很常见的-nullpointerexception-你是怎么排查的怎么解决的) + - [静态内部类和非静态内部类的区别是什么?](#静态内部类和非静态内部类的区别是什么) + - [怎么创建静态内部类和非静态内部类?](#怎么创建静态内部类和非静态内部类) + - [Xml 解析方式,原理优缺点](#xml-解析方式原理优缺点) + - [静态变量和全局变量的区别](#静态变量和全局变量的区别) - [二 Java集合](#二-java集合) - - [hashmap的jdk1.7和jdk1.8区别](#hashmap的jdk17和jdk18区别) - - [concurrenthashmap的jdk1.7和jdk1.8区别](#concurrenthashmap的jdk17和jdk18区别) - - [HashMap 实现原理,扩容因子过大过小的缺点,扩容过程 采用什么方法能保证每个 bucket 中的数据更均匀 解决冲突的方式,还有没有其他方式(全域哈希)](#hashmap-实现原理扩容因子过大过小的缺点扩容过程-采用什么方法能保证每个-bucket-中的数据更均匀-解决冲突的方式还有没有其他方式全域哈希) - - [Collection 集合类中只能在 Iterator 中删除元素的原因](#collection-集合类中只能在-iterator-中删除元素的原因) - - [ArrayList、LinkedList、Vector](#arraylistlinkedlistvector) - - [还了解除 util 其他包下的 List 吗?](#还了解除-util-其他包下的-list-吗) - - [CopyOnWriteArrayList](#copyonwritearraylist) - - [ConcurrentHashMap 和 LinkedHashMap 差异和适用情形](#concurrenthashmap-和-linkedhashmap-差异和适用情形) - - [ConcurrentHashMap分段锁是如何实现,ConcurrentHashmap jdk1.8 访问的时候是怎么加锁的,插入的时候是怎么加锁的 访问不加 锁插入的时候对头结点加锁](#concurrenthashmap分段锁是如何实现concurrenthashmap-jdk18-访问的时候是怎么加锁的插入的时候是怎么加锁的-访问不加-锁插入的时候对头结点加锁) - - [ArrayDeque 的使用场景](#arraydeque-的使用场景) - - [ArrayBlockingQueue 源码](#arrayblockingqueue-源码) - - [hashmap 和 treemap 的区别](#hashmap-和-treemap-的区别) - - [hashmap](#hashmap) - - [treemap](#treemap) - - [rehash 过程](#rehash-过程) - - [HashMap 的负载因子,为什么容量为2^n](#hashmap-的负载因子为什么容量为2n) - - [list,map,set 之间的区别](#listmapset-之间的区别) - - [什么时候会用到 HashMap](#什么时候会用到-hashmap) - - [常见的线程安全的集合类](#常见的线程安全的集合类) + - [hashmap的jdk1.7和jdk1.8区别](#hashmap的jdk17和jdk18区别) + - [concurrenthashmap的jdk1.7和jdk1.8区别](#concurrenthashmap的jdk17和jdk18区别) + - [HashMap 实现原理,扩容因子过大过小的缺点,扩容过程 采用什么方法能保证每个 bucket 中的数据更均匀 解决冲突的方式,还有没有其他方式(全域哈希)](#hashmap-实现原理扩容因子过大过小的缺点扩容过程-采用什么方法能保证每个-bucket-中的数据更均匀-解决冲突的方式还有没有其他方式全域哈希) + - [Collection 集合类中只能在 Iterator 中删除元素的原因](#collection-集合类中只能在-iterator-中删除元素的原因) + - [ArrayList、LinkedList、Vector](#arraylistlinkedlistvector) + - [还了解除 util 其他包下的 List 吗?](#还了解除-util-其他包下的-list-吗) + - [CopyOnWriteArrayList](#copyonwritearraylist) + - [ConcurrentHashMap 和 LinkedHashMap 差异和适用情形](#concurrenthashmap-和-linkedhashmap-差异和适用情形) + - [ConcurrentHashMap分段锁是如何实现,ConcurrentHashmap jdk1.8 访问的时候是怎么加锁的,插入的时候是怎么加锁的 访问不加 锁插入的时候对头结点加锁](#concurrenthashmap分段锁是如何实现concurrenthashmap-jdk18-访问的时候是怎么加锁的插入的时候是怎么加锁的-访问不加-锁插入的时候对头结点加锁) + - [ArrayDeque 的使用场景](#arraydeque-的使用场景) + - [ArrayBlockingQueue 源码](#arrayblockingqueue-源码) + - [hashmap 和 treemap 的区别](#hashmap-和-treemap-的区别) + - [hashmap](#hashmap) + - [treemap](#treemap) + - [rehash 过程](#rehash-过程) + - [HashMap 的负载因子,为什么容量为2^n](#hashmap-的负载因子为什么容量为2n) + - [list,map,set 之间的区别](#listmapset-之间的区别) + - [什么时候会用到 HashMap](#什么时候会用到-hashmap) + - [常见的线程安全的集合类](#常见的线程安全的集合类) - [三 JVM](#三-jvm) - - [反射在jvm层面的实现](#反射在jvm层面的实现) - - [jvm的方法区存什么?](#jvm的方法区存什么) - - [JDK1.8 JVM方法区变成了什么,为什么这样做](#jdk18-jvm方法区变成了什么为什么这样做) - - [oom出现的原因](#oom出现的原因) - - [Class.forName和ClassLoader的区别](#classforname和classloader的区别) - - [java对象信息分配](#java对象信息分配) - - [java虚拟机ZGC详解](#java虚拟机zgc详解) - - [java虚拟机CMS详解](#java虚拟机cms详解) - - [java虚拟机G1详解](#java虚拟机g1详解) - - [JVM tomcat 容器启动,jvm 加载情况描述](#jvm-tomcat-容器启动jvm-加载情况描述) + - [反射在jvm层面的实现](#反射在jvm层面的实现) + - [jvm的方法区存什么?](#jvm的方法区存什么) + - [JDK1.8 JVM方法区变成了什么,为什么这样做](#jdk18-jvm方法区变成了什么为什么这样做) + - [oom出现的原因](#oom出现的原因) + - [Class.forName和ClassLoader的区别](#classforname和classloader的区别) + - [java对象信息分配](#java对象信息分配) + - [java虚拟机ZGC详解](#java虚拟机zgc详解) + - [java虚拟机CMS详解](#java虚拟机cms详解) + - [java虚拟机G1详解](#java虚拟机g1详解) + - [JVM tomcat 容器启动,jvm 加载情况描述](#jvm-tomcat-容器启动jvm-加载情况描述) - [四 多线程并发](#四-多线程并发) - - [volitale使用场景](#volitale使用场景) - - [可重入锁,实现原理](#可重入锁实现原理) - - [Java 无锁原理](#java-无锁原理) - - [讲讲多线程,多线程的同步方法](#讲讲多线程多线程的同步方法) - - [synchronized原理](#synchronized原理) - - [reetrantlock](#reetrantlock) - - [java 线程安全都体现在哪些方面](#java-线程安全都体现在哪些方面) - - [如果维护线程安全 如果想实现一个线程安全的队列,可以怎么实现?](#如果维护线程安全-如果想实现一个线程安全的队列可以怎么实现) - - [Java多线程通信方式](#java多线程通信方式) - - [CountDownLatch、CyclicBarrier、Semaphore 用法总结](#countdownlatchcyclicbarriersemaphore-用法总结) - - [juc下的内容](#juc下的内容) - - [AOS等并发相关面试题](#aos等并发相关面试题) - - [threadlocal](#threadlocal) - - [java 线程池达到提交上限的具体情况 ,线程池用法,Java 多线程,线程池有哪几类,每一类的差别](#java-线程池达到提交上限的具体情况-线程池用法java-多线程线程池有哪几类每一类的差别) - - [要你设计的话,如何实现一个线程池](#要你设计的话如何实现一个线程池) - - [线程池的类型,固定大小的线程池内部是如何实现的,等待队列是用了哪一个队列实现 线程池种类和工作流程](#线程池的类型固定大小的线程池内部是如何实现的等待队列是用了哪一个队列实现-线程池种类和工作流程) - - [线程池使用了什么设计模式](#线程池使用了什么设计模式) - - [线程池使用时一般要考虑哪些问题](#线程池使用时一般要考虑哪些问题) - - [线程池的配置](#线程池的配置) - - [Excutor 以及 Connector 的配置](#excutor-以及-connector-的配置) + - [volitale使用场景](#volitale使用场景) + - [可重入锁,实现原理](#可重入锁实现原理) + - [Java 无锁原理](#java-无锁原理) + - [讲讲多线程,多线程的同步方法](#讲讲多线程多线程的同步方法) + - [synchronized原理](#synchronized原理) + - [reetrantlock](#reetrantlock) + - [java 线程安全都体现在哪些方面](#java-线程安全都体现在哪些方面) + - [如果维护线程安全 如果想实现一个线程安全的队列,可以怎么实现?](#如果维护线程安全-如果想实现一个线程安全的队列可以怎么实现) + - [Java多线程通信方式](#java多线程通信方式) + - [CountDownLatch、CyclicBarrier、Semaphore 用法总结](#countdownlatchcyclicbarriersemaphore-用法总结) + - [juc下的内容](#juc下的内容) + - [AOS等并发相关面试题](#aos等并发相关面试题) + - [threadlocal](#threadlocal) + - [java 线程池达到提交上限的具体情况 ,线程池用法,Java 多线程,线程池有哪几类,每一类的差别](#java-线程池达到提交上限的具体情况-线程池用法java-多线程线程池有哪几类每一类的差别) + - [要你设计的话,如何实现一个线程池](#要你设计的话如何实现一个线程池) + - [线程池的类型,固定大小的线程池内部是如何实现的,等待队列是用了哪一个队列实现 线程池种类和工作流程](#线程池的类型固定大小的线程池内部是如何实现的等待队列是用了哪一个队列实现-线程池种类和工作流程) + - [线程池使用了什么设计模式](#线程池使用了什么设计模式) + - [线程池使用时一般要考虑哪些问题](#线程池使用时一般要考虑哪些问题) + - [线程池的配置](#线程池的配置) + - [Excutor 以及 Connector 的配置](#excutor-以及-connector-的配置) - [五 Java框架(ssm)](#五-java框架ssm) - - [hibernate](#hibernate) - - [Hibernate 的生成策略](#hibernate-的生成策略) - - [Hibernate 与 Mybatis 区别](#hibernate-与-mybatis-区别) - - [Mybatis原理](#mybatis原理) - - [mybatis执行select的过程](#mybatis执行select的过程) - - [mybatis有哪些executors](#mybatis有哪些executors) - - [mybatis插件原理](#mybatis插件原理) - - [mybatis二级缓存](#mybatis二级缓存) - - [spring&springmvc](#springspringmvc) - - [spring中的设计模式](#spring中的设计模式) - - [spring中bean的作用域](#spring中bean的作用域) - - [BeanFactory和FactoryBean区别](#beanfactory和factorybean区别) - - [aspect的种类](#aspect的种类) - - [spring aop的实际应用](#spring-aop的实际应用) - - [spring实现多线程安全](#spring实现多线程安全) - - [spring的bean的高并发安全问题](#spring的bean的高并发安全问题) - - [ioc aop总结(概述性)](#ioc-aop总结概述性) - - [Spring 的加载流程,Spring 的源码中 Bean 的构造的流程](#spring-的加载流程spring-的源码中-bean-的构造的流程) - - [Spring 事务源码,IOC 源码,AOP 源码](#spring-事务源码ioc-源码aop-源码) - - [spring 的作用及理解 事务怎么配置](#spring-的作用及理解-事务怎么配置) - - [spring事务失效情况](#spring事务失效情况) - - [Spring 的 annotation 如何实现](#spring-的-annotation-如何实现) - - [SpringMVC 工作原理](#springmvc-工作原理) - - [了解 SpringMVC 与 Struct2 区别](#了解-springmvc-与-struct2-区别) - - [springMVC 和 spring 是什么关系](#springmvc-和-spring-是什么关系) - - [项目中 Spring 的 IOC 和 AOP 具体怎么使用的](#项目中-spring-的-ioc-和-aop-具体怎么使用的) - - [spring mvc 底层实现原理](#spring-mvc-底层实现原理) - - [动态代理的原理](#动态代理的原理) - - [如果使用 spring mvc,那 post 请求跟 put 请求有什么区别啊; 然后开始问 springmvc:描述从 tomcat 开始到 springmvc 返回到前端显示的整个流程,接着问 springmvc 中的 handlerMapping 的内部实现,然后又问 spring 中从载入 xml 文件到 getbean 整个流程,描述一遍](#如果使用-spring-mvc那-post-请求跟-put-请求有什么区别啊-然后开始问-springmvc描述从-tomcat-开始到-springmvc-返回到前端显示的整个流程接着问-springmvc-中的-handlermapping-的内部实现然后又问-spring-中从载入-xml-文件到-getbean-整个流程描述一遍) +- [hibernate](#hibernate) + - [Hibernate 的生成策略](#hibernate-的生成策略) + - [Hibernate 与 Mybatis 区别](#hibernate-与-mybatis-区别) + - [Mybatis原理](#mybatis原理) + - [mybatis执行select的过程](#mybatis执行select的过程) + - [mybatis有哪些executors](#mybatis有哪些executors) + - [mybatis插件原理](#mybatis插件原理) + - [mybatis二级缓存](#mybatis二级缓存) +- [spring&springmvc](#springspringmvc) + - [spring中的设计模式](#spring中的设计模式) + - [spring中bean的作用域](#spring中bean的作用域) + - [BeanFactory和FactoryBean区别](#beanfactory和factorybean区别) + - [aspect的种类](#aspect的种类) + - [spring aop的实际应用](#spring-aop的实际应用) + - [spring实现多线程安全](#spring实现多线程安全) + - [spring的bean的高并发安全问题](#spring的bean的高并发安全问题) + - [ioc aop总结(概述性)](#ioc-aop总结概述性) + - [Spring 的加载流程,Spring 的源码中 Bean 的构造的流程](#spring-的加载流程spring-的源码中-bean-的构造的流程) + - [Spring 事务源码,IOC 源码,AOP 源码](#spring-事务源码ioc-源码aop-源码) + - [spring 的作用及理解 事务怎么配置](#spring-的作用及理解-事务怎么配置) + - [spring事务失效情况](#spring事务失效情况) + - [Spring 的 annotation 如何实现](#spring-的-annotation-如何实现) + - [SpringMVC 工作原理](#springmvc-工作原理) + - [了解 SpringMVC 与 Struct2 区别](#了解-springmvc-与-struct2-区别) + - [springMVC 和 spring 是什么关系](#springmvc-和-spring-是什么关系) + - [项目中 Spring 的 IOC 和 AOP 具体怎么使用的](#项目中-spring-的-ioc-和-aop-具体怎么使用的) + - [spring mvc 底层实现原理](#spring-mvc-底层实现原理) + - [动态代理的原理](#动态代理的原理) + - [如果使用 spring mvc,那 post 请求跟 put 请求有什么区别啊; 然后开始问 springmvc:描述从 tomcat 开始到 springmvc 返回到前端显示的整个流程,接着问 springmvc 中的 handlerMapping 的内部实现,然后又问 spring 中从载入 xml 文件到 getbean 整个流程,描述一遍](#如果使用-spring-mvc那-post-请求跟-put-请求有什么区别啊-然后开始问-springmvc描述从-tomcat-开始到-springmvc-返回到前端显示的整个流程接着问-springmvc-中的-handlermapping-的内部实现然后又问-spring-中从载入-xml-文件到-getbean-整个流程描述一遍) - [六 微服务(springboot等)](#六-微服务springboot等) - - [springboot](#springboot) - - [springcloud](#springcloud) + - [springboot](#springboot) + - [springcloud](#springcloud) - [七 数据结构](#七-数据结构) - - [二叉树相关](#二叉树相关) - - [红黑树](#红黑树) + - [二叉树相关](#二叉树相关) + - [红黑树](#红黑树) - [八 数据库](#八-数据库) - - [MySQL](#mysql) - - [数据库死锁问题](#数据库死锁问题) - - [hash索引和B+树索引的区别](#hash索引和b树索引的区别) - - [可重复的原理MVCC](#可重复的原理mvcc) - - [count(1)、count(*)、count(列名)](#count1countcount列名) - - [mysql的undo、redo、binlog的区别](#mysql的undoredobinlog的区别) - - [explain解释](#explain解释) - - [mysql分页查询优化](#mysql分页查询优化) - - [sql注入](#sql注入) - - [为什么用B+树](#为什么用b树) - - [sql执行流程](#sql执行流程) - - [聚集索引与非聚集索引](#聚集索引与非聚集索引) - - [覆盖索引](#覆盖索引) - - [sql总结](#sql总结) - - [有人建议给每张表都建一个自增主键,这样做有什么优点跟缺点](#有人建议给每张表都建一个自增主键这样做有什么优点跟缺点) - - [对 MySQL 的了解,和 oracle 的区别](#对-mysql-的了解和-oracle-的区别) - - [500万数字排序,内存只能容纳5万个,如何排序,如何优化?](#500万数字排序内存只能容纳5万个如何排序如何优化) - - [平时怎么写数据库的模糊查询(由字典树扯到模糊查询,前缀查询,例如“abc%”,还是索引策略的问题)](#平时怎么写数据库的模糊查询由字典树扯到模糊查询前缀查询例如abc还是索引策略的问题) - - [数据库里有 10000000 条用户信息,需要给每位用户发送信息(必须发送成功),要求节省内存](#数据库里有-10000000-条用户信息需要给每位用户发送信息必须发送成功要求节省内存) - - [项目中如何实现事务](#项目中如何实现事务) - - [数据库设计一般设计成第几范式](#数据库设计一般设计成第几范式) - - [mysql 用的什么版本 5.7 跟 5.6 有啥区别](#mysql-用的什么版本-57-跟-56-有啥区别) - - [提升 MySQL 安全性](#提升-mysql-安全性) - - [问了一个这样的表(三个字段:姓名,id,分数)要求查出平均分大于 80 的 id 然后分数降序排序,然后经过提示用聚合函数 avg。](#问了一个这样的表三个字段姓名id分数要求查出平均分大于-80-的-id-然后分数降序排序然后经过提示用聚合函数-avg) - - [为什么 mysql 事务能保证失败回滚](#为什么-mysql-事务能保证失败回滚) - - [主键索引底层的实现原理](#主键索引底层的实现原理) - - [经典的01索引问题?](#经典的01索引问题) - - [如何在长文本中快捷的筛选出你的名字?](#如何在长文本中快捷的筛选出你的名字) - - [多列索引及最左前缀原则和其他使用场景](#多列索引及最左前缀原则和其他使用场景) - - [事务隔离级别](#事务隔离级别) - - [索引的最左前缀原则](#索引的最左前缀原则) - - [数据库悲观锁怎么实现的](#数据库悲观锁怎么实现的) - - [建表的原则](#建表的原则) - - [索引的内涵和用法](#索引的内涵和用法) - - [给了两条 SQL 语句,让根据这两条语句建索引(个人想法:主要考虑复合索引只能匹配前缀列的特点)](#给了两条-sql-语句让根据这两条语句建索引个人想法主要考虑复合索引只能匹配前缀列的特点) - - [那么我们来聊一下数据库。A 和 B 两个表做等值连接(Inner join) 怎么优化](#那么我们来聊一下数据库a-和-b-两个表做等值连接inner-join-怎么优化) - - [数据库连接池的理解和优化](#数据库连接池的理解和优化) - - [Sql语句分组排序](#sql语句分组排序) - - [SQL语句的5个连接概念](#sql语句的5个连接概念) - - [数据库优化和架构(主要是主从分离和分库分表相关)](#数据库优化和架构主要是主从分离和分库分表相关) - - [分库分表](#分库分表) - - [跨库join实现](#跨库join实现) - - [探讨主从分离和分库分表相关](#探讨主从分离和分库分表相关) - - [数据库中间件](#数据库中间件) - - [读写分离在中间件的实现](#读写分离在中间件的实现) - - [限流 and 熔断](#限流-and-熔断) - - [行锁适用场景](#行锁适用场景) - - [Redis](#redis) - - [redis为什么快?](#redis为什么快) - - [Redis 数据结构原理](#redis-数据结构原理) - - [Redis 持久化机制](#redis-持久化机制) - - [Redis 的一致性哈希算法](#redis-的一致性哈希算法) - - [redis了解多少](#redis了解多少) - - [redis五种数据类型,当散列类型的 value 值非常大的时候怎么进行压缩](#redis五种数据类型当散列类型的-value-值非常大的时候怎么进行压缩) - - [用redis怎么实现摇一摇与附近的人功能](#用redis怎么实现摇一摇与附近的人功能) - - [redis 主从复制过程](#redis-主从复制过程) - - [Redis 如何解决 key 冲突](#redis-如何解决-key-冲突) - - [redis 是怎么存储数据的](#redis-是怎么存储数据的) - - [redis 使用场景](#redis-使用场景) +- [MySQL](#mysql) + - [数据库死锁问题](#数据库死锁问题) + - [hash索引和B+树索引的区别](#hash索引和b树索引的区别) + - [可重复的原理MVCC](#可重复的原理mvcc) + - [count(1)、count(*)、count(列名)](#count1countcount列名) + - [mysql的undo、redo、binlog的区别](#mysql的undoredobinlog的区别) + - [explain解释](#explain解释) + - [mysql分页查询优化](#mysql分页查询优化) + - [sql注入](#sql注入) + - [为什么用B+树](#为什么用b树) + - [sql执行流程](#sql执行流程) + - [聚集索引与非聚集索引](#聚集索引与非聚集索引) + - [覆盖索引](#覆盖索引) + - [sql总结](#sql总结) + - [有人建议给每张表都建一个自增主键,这样做有什么优点跟缺点](#有人建议给每张表都建一个自增主键这样做有什么优点跟缺点) + - [对 MySQL 的了解,和 oracle 的区别](#对-mysql-的了解和-oracle-的区别) + - [500万数字排序,内存只能容纳5万个,如何排序,如何优化?](#500万数字排序内存只能容纳5万个如何排序如何优化) + - [平时怎么写数据库的模糊查询(由字典树扯到模糊查询,前缀查询,例如“abc%”,还是索引策略的问题)](#平时怎么写数据库的模糊查询由字典树扯到模糊查询前缀查询例如abc还是索引策略的问题) + - [数据库里有 10000000 条用户信息,需要给每位用户发送信息(必须发送成功),要求节省内存](#数据库里有-10000000-条用户信息需要给每位用户发送信息必须发送成功要求节省内存) + - [项目中如何实现事务](#项目中如何实现事务) + - [数据库设计一般设计成第几范式](#数据库设计一般设计成第几范式) + - [mysql 用的什么版本 5.7 跟 5.6 有啥区别](#mysql-用的什么版本-57-跟-56-有啥区别) + - [提升 MySQL 安全性](#提升-mysql-安全性) + - [问了一个这样的表(三个字段:姓名,id,分数)要求查出平均分大于 80 的 id 然后分数降序排序,然后经过提示用聚合函数 avg。](#问了一个这样的表三个字段姓名id分数要求查出平均分大于-80-的-id-然后分数降序排序然后经过提示用聚合函数-avg) + - [为什么 mysql 事务能保证失败回滚](#为什么-mysql-事务能保证失败回滚) + - [主键索引底层的实现原理](#主键索引底层的实现原理) + - [经典的01索引问题?](#经典的01索引问题) + - [如何在长文本中快捷的筛选出你的名字?](#如何在长文本中快捷的筛选出你的名字) + - [多列索引及最左前缀原则和其他使用场景](#多列索引及最左前缀原则和其他使用场景) + - [事务隔离级别](#事务隔离级别) + - [索引的最左前缀原则](#索引的最左前缀原则) + - [数据库悲观锁怎么实现的](#数据库悲观锁怎么实现的) + - [建表的原则](#建表的原则) + - [索引的内涵和用法](#索引的内涵和用法) + - [给了两条 SQL 语句,让根据这两条语句建索引(个人想法:主要考虑复合索引只能匹配前缀列的特点)](#给了两条-sql-语句让根据这两条语句建索引个人想法主要考虑复合索引只能匹配前缀列的特点) + - [那么我们来聊一下数据库。A 和 B 两个表做等值连接(Inner join) 怎么优化](#那么我们来聊一下数据库a-和-b-两个表做等值连接inner-join-怎么优化) + - [数据库连接池的理解和优化](#数据库连接池的理解和优化) + - [Sql语句分组排序](#sql语句分组排序) + - [SQL语句的5个连接概念](#sql语句的5个连接概念) + - [数据库优化和架构(主要是主从分离和分库分表相关)](#数据库优化和架构主要是主从分离和分库分表相关) + - [分库分表](#分库分表) + - [跨库join实现](#跨库join实现) + - [探讨主从分离和分库分表相关](#探讨主从分离和分库分表相关) + - [数据库中间件](#数据库中间件) + - [读写分离在中间件的实现](#读写分离在中间件的实现) + - [限流 and 熔断](#限流-and-熔断) + - [行锁适用场景](#行锁适用场景) +- [Redis](#redis) + - [redis为什么快?](#redis为什么快) + - [Redis 数据结构原理](#redis-数据结构原理) + - [Redis 持久化机制](#redis-持久化机制) + - [Redis 的一致性哈希算法](#redis-的一致性哈希算法) + - [redis了解多少](#redis了解多少) + - [redis五种数据类型,当散列类型的 value 值非常大的时候怎么进行压缩](#redis五种数据类型当散列类型的-value-值非常大的时候怎么进行压缩) + - [用redis怎么实现摇一摇与附近的人功能](#用redis怎么实现摇一摇与附近的人功能) + - [redis 主从复制过程](#redis-主从复制过程) + - [Redis 如何解决 key 冲突](#redis-如何解决-key-冲突) + - [redis 是怎么存储数据的](#redis-是怎么存储数据的) + - [redis 使用场景](#redis-使用场景) - [九 计算机网络](#九-计算机网络) - - [cookie 禁用怎么办](#cookie-禁用怎么办) - - [Netty new 实例化过程](#netty-new-实例化过程) - - [socket 实现过程,具体用的方法;怎么实现异步 socket.](#socket-实现过程具体用的方法怎么实现异步-socket) - - [浏览器的缓存机制](#浏览器的缓存机制) - - [http相关问题](#http相关问题) - - [TCP三次握手第三次握手时ACK丢失怎么办](#tcp三次握手第三次握手时ack丢失怎么办) - - [dns属于udp还是tcp,原因](#dns属于udp还是tcp原因) - - [http的幂等性](#http的幂等性) - - [建立连接的过程客户端跟服务端会交换什么信息(参考 TCP 报文结构)](#建立连接的过程客户端跟服务端会交换什么信息参考-tcp-报文结构) - - [丢包如何解决重传的消耗](#丢包如何解决重传的消耗) - - [traceroute 实现原理](#traceroute-实现原理) - - [IO多路复用](#io多路复用) - - [select 和 poll 区别?](#select-和-poll-区别) - - [在不使用 WebSocket 情况下怎么实现服务器推送的一种方法](#在不使用-websocket-情况下怎么实现服务器推送的一种方法) - - [可以使用客户端定时刷新请求或者和 TCP 保持心跳连接实现。](#可以使用客户端定时刷新请求或者和-tcp-保持心跳连接实现) - - [查看磁盘读写吞吐量?](#查看磁盘读写吞吐量) - - [PING 位于哪一层](#ping-位于哪一层) - - [网络重定向,说下流程](#网络重定向说下流程) - - [controller 怎么处理的请求:路由](#controller-怎么处理的请求路由) - - [IP 地址分为几类,每类都代表什么,私网是哪些](#ip-地址分为几类每类都代表什么私网是哪些) + - [cookie 禁用怎么办](#cookie-禁用怎么办) + - [Netty new 实例化过程](#netty-new-实例化过程) + - [socket 实现过程,具体用的方法;怎么实现异步 socket.](#socket-实现过程具体用的方法怎么实现异步-socket) + - [浏览器的缓存机制](#浏览器的缓存机制) + - [http相关问题](#http相关问题) + - [TCP三次握手第三次握手时ACK丢失怎么办](#tcp三次握手第三次握手时ack丢失怎么办) + - [dns属于udp还是tcp,原因](#dns属于udp还是tcp原因) + - [http的幂等性](#http的幂等性) + - [建立连接的过程客户端跟服务端会交换什么信息(参考 TCP 报文结构)](#建立连接的过程客户端跟服务端会交换什么信息参考-tcp-报文结构) + - [丢包如何解决重传的消耗](#丢包如何解决重传的消耗) + - [traceroute 实现原理](#traceroute-实现原理) + - [IO多路复用](#io多路复用) + - [select 和 poll 区别?](#select-和-poll-区别) + - [在不使用 WebSocket 情况下怎么实现服务器推送的一种方法](#在不使用-websocket-情况下怎么实现服务器推送的一种方法) + - [可以使用客户端定时刷新请求或者和 TCP 保持心跳连接实现。](#可以使用客户端定时刷新请求或者和-tcp-保持心跳连接实现) + - [查看磁盘读写吞吐量?](#查看磁盘读写吞吐量) + - [PING 位于哪一层](#ping-位于哪一层) + - [网络重定向,说下流程](#网络重定向说下流程) + - [controller 怎么处理的请求:路由](#controller-怎么处理的请求路由) + - [IP 地址分为几类,每类都代表什么,私网是哪些](#ip-地址分为几类每类都代表什么私网是哪些) - [十 操作系统](#十-操作系统) - - [Java I/O 底层细节,注意是底层细节,而不是怎么用](#java-io-底层细节注意是底层细节而不是怎么用) - - [Java IO 模型(BIO,NIO 等) ,Tomcat 用的哪一种模型](#java-io-模型bionio-等-tomcat-用的哪一种模型) - - [当获取第一个获取锁之后,条件不满足需要释放锁应当怎么做?](#当获取第一个获取锁之后条件不满足需要释放锁应当怎么做) - - [手写一个线程安全的生产者与消费者。](#手写一个线程安全的生产者与消费者) - - [进程和线程调度方法](#进程和线程调度方法) - - [linux](#linux) - - [linux查找命令](#linux查找命令) - - [项目部署常见linux命令](#项目部署常见linux命令) - - [进程文件里有哪些信息](#进程文件里有哪些信息) - - [sed 和 awk 的区别](#sed-和-awk-的区别) - - [linux查看进程并杀死的命令](#linux查看进程并杀死的命令) - - [有一个文件被锁住,如何查看锁住它的线程?](#有一个文件被锁住如何查看锁住它的线程) - - [如何查看一个文件第100行到150行的内容](#如何查看一个文件第100行到150行的内容) - - [如何查看进程消耗的资源](#如何查看进程消耗的资源) - - [如何查看每个进程下的线程?](#如何查看每个进程下的线程) - - [linux 如何查找文件](#linux-如何查找文件) - - [select epoll等问题](#select-epoll等问题) + - [Java I/O 底层细节,注意是底层细节,而不是怎么用](#java-io-底层细节注意是底层细节而不是怎么用) + - [Java IO 模型(BIO,NIO 等) ,Tomcat 用的哪一种模型](#java-io-模型bionio-等-tomcat-用的哪一种模型) + - [当获取第一个获取锁之后,条件不满足需要释放锁应当怎么做?](#当获取第一个获取锁之后条件不满足需要释放锁应当怎么做) + - [手写一个线程安全的生产者与消费者。](#手写一个线程安全的生产者与消费者) + - [进程和线程调度方法](#进程和线程调度方法) +- [linux](#linux) + - [linux查找命令](#linux查找命令) + - [项目部署常见linux命令](#项目部署常见linux命令) + - [进程文件里有哪些信息](#进程文件里有哪些信息) + - [sed 和 awk 的区别](#sed-和-awk-的区别) + - [linux查看进程并杀死的命令](#linux查看进程并杀死的命令) + - [有一个文件被锁住,如何查看锁住它的线程?](#有一个文件被锁住如何查看锁住它的线程) + - [如何查看一个文件第100行到150行的内容](#如何查看一个文件第100行到150行的内容) + - [如何查看进程消耗的资源](#如何查看进程消耗的资源) + - [如何查看每个进程下的线程?](#如何查看每个进程下的线程) + - [linux 如何查找文件](#linux-如何查找文件) + - [select epoll等问题](#select-epoll等问题) - [十一 框架其他](#十一-框架其他) - - [Servlet 的 Filter 用的什么设计模式](#servlet-的-filter-用的什么设计模式) - - [zookeeper 的常用功能,自己用它来做什么](#zookeeper-的常用功能自己用它来做什么) - - [redis 的操作是不是原子操作](#redis-的操作是不是原子操作) - - [秒杀业务场景设计](#秒杀业务场景设计) - - [如何设计淘宝秒杀系统(重点关注架构,比如数据一致性,数据库集群一致性哈希,缓存, 分库分表等等)](#如何设计淘宝秒杀系统重点关注架构比如数据一致性数据库集群一致性哈希缓存-分库分表等等) - - [对后台的优化有了解吗?比如负载均衡](#对后台的优化有了解吗比如负载均衡) - - [对 Restful 了解 Restful 的认识,优点,以及和 soap 的区别](#对-restful-了解-restful-的认识优点以及和-soap-的区别) - - [lrucache 的基本原理](#lrucache-的基本原理) + - [Servlet 的 Filter 用的什么设计模式](#servlet-的-filter-用的什么设计模式) + - [zookeeper 的常用功能,自己用它来做什么](#zookeeper-的常用功能自己用它来做什么) + - [redis 的操作是不是原子操作](#redis-的操作是不是原子操作) + - [秒杀业务场景设计](#秒杀业务场景设计) + - [如何设计淘宝秒杀系统(重点关注架构,比如数据一致性,数据库集群一致性哈希,缓存, 分库分表等等)](#如何设计淘宝秒杀系统重点关注架构比如数据一致性数据库集群一致性哈希缓存-分库分表等等) + - [对后台的优化有了解吗?比如负载均衡](#对后台的优化有了解吗比如负载均衡) + - [对 Restful 了解 Restful 的认识,优点,以及和 soap 的区别](#对-restful-了解-restful-的认识优点以及和-soap-的区别) + - [lrucache 的基本原理](#lrucache-的基本原理) - [十二 设计模式](#十二-设计模式) - - [Java常见设计模式](#java常见设计模式) + - [Java常见设计模式](#java常见设计模式) - [十三 分布式](#十三-分布式) - - [dubbo中的dubbo协议和http协议有什么区别?](#dubbo中的dubbo协议和http协议有什么区别) - - [负载均衡](#负载均衡) - - [分布式锁的实现方式及优缺点](#分布式锁的实现方式及优缺点) - - [CAP](#cap) - - [如何实现分布式缓存](#如何实现分布式缓存) + - [dubbo中的dubbo协议和http协议有什么区别?](#dubbo中的dubbo协议和http协议有什么区别) + - [负载均衡](#负载均衡) + - [分布式锁的实现方式及优缺点](#分布式锁的实现方式及优缺点) + - [CAP](#cap) + - [如何实现分布式缓存](#如何实现分布式缓存) - [十四 其他](#十四-其他) - - [Java 8 函数式编程 回调函数](#java-8-函数式编程-回调函数) - - [函数式编程,面向对象之间区别](#函数式编程面向对象之间区别) - - [Java 8 中 stream 迭代的优势和区别?](#java-8-中-stream-迭代的优势和区别) - - [同步等于可见性吗?](#同步等于可见性吗) - - [git底层数据结构](#git底层数据结构) - - [安全加密](#安全加密) - - [web安全问题](#web安全问题) + - [Java 8 函数式编程 回调函数](#java-8-函数式编程-回调函数) + - [函数式编程,面向对象之间区别](#函数式编程面向对象之间区别) + - [Java 8 中 stream 迭代的优势和区别?](#java-8-中-stream-迭代的优势和区别) + - [同步等于可见性吗?](#同步等于可见性吗) + - [git底层数据结构](#git底层数据结构) + - [安全加密](#安全加密) + - [web安全问题](#web安全问题) + + + -# 一 Java基础 +### 一 Java基础 #### 一致性hash算法 @@ -374,7 +382,7 @@ https://segmentfault.com/a/1190000013504078?utm_source=tag-newest #### 静态变量和全局变量的区别 -# 二 Java集合 +### 二 Java集合 #### hashmap的jdk1.7和jdk1.8区别 @@ -494,7 +502,7 @@ https://blog.csdn.net/u012102104/article/details/79235938 #### 常见的线程安全的集合类 -# 三 JVM +### 三 JVM #### 反射在jvm层面的实现 @@ -540,7 +548,7 @@ tomcat请求流程:http://objcoding.com/2017/06/12/Tomcat-structure-and-proces https://blog.csdn.net/lduzhenlin/article/details/83013143 https://blog.csdn.net/xlgen157387/article/details/53521928 -# 四 多线程并发 +### 四 多线程并发 #### volitale使用场景 @@ -617,7 +625,7 @@ https://juejin.im/post/5d1882b1f265da1ba84aa676#heading-14 https://www.cnblogs.com/kismetv/p/7806063.html -# 五 Java框架(ssm) +### 五 Java框架(ssm) ### hibernate @@ -748,7 +756,7 @@ https://juejin.im/post/5a3284a75188252970793195 #### 如果使用 spring mvc,那 post 请求跟 put 请求有什么区别啊; 然后开始问 springmvc:描述从 tomcat 开始到 springmvc 返回到前端显示的整个流程,接着问 springmvc 中的 handlerMapping 的内部实现,然后又问 spring 中从载入 xml 文件到 getbean 整个流程,描述一遍 -# 六 微服务(springboot等) +### 六 微服务(springboot等) #### springboot @@ -760,7 +768,7 @@ https://juejin.im/post/5a3284a75188252970793195 [springcloud面试题](https://mp.weixin.qq.com/s/CYfLA9s9zhwcIwJjMFXhQQ) -# 七 数据结构 +### 七 数据结构 #### 二叉树相关 @@ -774,7 +782,7 @@ https://zhuanlan.zhihu.com/p/31805309 -# 八 数据库 +### 八 数据库 ### MySQL @@ -952,7 +960,7 @@ https://blog.csdn.net/smartwu_sir/article/details/80254733 #### redis 使用场景 -# 九 计算机网络 +### 九 计算机网络 #### cookie 禁用怎么办 @@ -1029,7 +1037,7 @@ https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Redirections https://zhuanlan.zhihu.com/p/54593244 -# 十 操作系统 +### 十 操作系统 #### Java I/O 底层细节,注意是底层细节,而不是怎么用 @@ -1097,7 +1105,7 @@ linux命令:https://juejin.im/post/5d3857eaf265da1bd04f2437 https://juejin.im/post/5b624f4d518825068302aee9#heading-13 -# 十一 框架其他 +### 十一 框架其他 #### Servlet 的 Filter 用的什么设计模式 @@ -1126,7 +1134,7 @@ https://www.ruanyifeng.com/blog/2011/09/restful.html #### lrucache 的基本原理 -# 十二 设计模式 +### 十二 设计模式 #### Java常见设计模式 @@ -1140,7 +1148,7 @@ https://www.ruanyifeng.com/blog/2011/09/restful.html - 基本的设计模式及其核心思想 - 来,我们写一个单例模式的实现 -# 十三 分布式 +### 十三 分布式 #### dubbo中的dubbo协议和http协议有什么区别? @@ -1163,7 +1171,7 @@ https://www.jianshu.com/p/8025e3346734 redis如何实现分布式缓存 https://stor.51cto.com/art/201912/607229.htm -# 十四 其他 +### 十四 其他 ##### Java 8 函数式编程 回调函数 From 58394bbc48f78fc7042550246927ec861a3d0282 Mon Sep 17 00:00:00 2001 From: OUYANGSIHAI <1446037005@qq.com> Date: Fri, 16 Oct 2020 11:26:15 +0800 Subject: [PATCH 21/38] =?UTF-8?q?readme=E6=96=B0=E5=A2=9E=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 78 ++++++++++--------- ...06\347\261\273\346\261\207\346\200\273.md" | 10 +-- 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index d5d8684..cb0e7c3 100644 --- a/README.md +++ b/README.md @@ -35,55 +35,57 @@ - [面试知识点](#面试知识点) - [公司面经](#公司面经) - [Java](#java) - * [基础](#基础) - * [容器(包括juc)](#容器包括juc) - * [并发](#并发) - * [JVM](#jvm) - * [Java8](#java8) + - [基础](#基础) + - [容器(包括juc)](#容器包括juc) + - [基础容器](#基础容器) + - [阻塞容器](#阻塞容器) + - [并发](#并发) + - [JVM](#jvm) + - [Java8](#java8) - [计算机网络](#计算机网络) - [计算机操作系统](#计算机操作系统) - [Linux](#linux) - [数据结构与算法](#数据结构与算法) - * [数据结构](#数据结构) - * [算法](#算法) + - [数据结构](#数据结构) + - [算法](#算法) - [数据库](#数据库) - * [MySQL](#mysql) - + [mysql(优化思路)](#mysql优化思路) + - [MySQL](#mysql) + - [MySQL(优化思路)](#mysql优化思路) - [系统设计](#系统设计) - * [秒杀系统相关](#秒杀系统相关) - * [前后端分离](#前后端分离) - * [单点登录](#单点登录) - * [常用框架](#常用框架) - + [Spring](#Spring) - + [SpringBoot](#springboot) + - [秒杀系统相关](#秒杀系统相关) + - [前后端分离](#前后端分离) + - [单点登录](#单点登录) + - [常用框架](#常用框架) + - [Spring](#spring) + - [SpringBoot](#springboot) - [分布式](#分布式) - * [dubbo](#dubbo) - * [zookeeper](#zookeeper) - * [RocketMQ](#rocketmq) - * [RabbitMQ](#rabbitmq) - * [kafka](#kafka) - * [消息中间件](#消息中间件) - * [redis](#redis) - * [分布式系统](#分布式系统) -- [线上问题调优(虚拟机,tomcat)](#线上问题调优虚拟机,tomcat) + - [dubbo](#dubbo) + - [zookeeper](#zookeeper) + - [RocketMQ](#rocketmq) + - [RabbitMQ](#rabbitmq) + - [kafka](#kafka) + - [消息中间件](#消息中间件) + - [redis](#redis) + - [分布式系统](#分布式系统) +- [线上问题调优(虚拟机,tomcat)](#线上问题调优虚拟机tomcat) - [面试指南](#面试指南) - [工具](#工具) - * [Git](#git) - * [Docker](#docker) + - [Git](#git) + - [Docker](#docker) - [其他](#其他) - * [权限控制(设计、shiro)](#权限控制设计、shiro) -- [Java学习资源](#Java学习资源) -- [Java书籍推荐](#Java书籍推荐) + - [权限控制(设计、shiro)](#权限控制设计shiro) +- [Java学习资源](#java学习资源) +- [Java书籍推荐](#java书籍推荐) - [实战项目推荐](#实战项目推荐) - [程序人生](#程序人生) - [说明](#说明) - * [JavaInterview介绍](#JavaInterview介绍) - * [关于转载](#关于转载) - * [如何对该开源文档进行贡献](#如何对该开源文档进行贡献) - * [为什么要做这个开源文档?](#为什么要做这个开源文档?) - * [投稿](#投稿) - * [联系我](#联系我) - * [公众号](#公众号) + - [JavaInterview介绍](#javainterview介绍) + - [关于转载](#关于转载) + - [如何对该开源文档进行贡献](#如何对该开源文档进行贡献) + - [为什么要做这个开源文档?](#为什么要做这个开源文档) + - [投稿](#投稿) + - [联系我](#联系我) + - [公众号](#公众号) ## 项目准备 @@ -126,7 +128,6 @@ - LinkedBlockingQueue源码分析及真实大厂面试题精讲 - PriorityBlockingQueue源码分析及真实大厂面试题精讲 - ### 并发 - Synchronized关键字精讲及真实大厂面试题解析 @@ -163,6 +164,7 @@ - [http面试问题全解析](docs/network/http面试问题全解析.md) - 关于tcp、udp网络模型的问题,这篇文章告诉你 - http、https还不了解,别慌! +- 面试官问我计算机网络的问题,我一个问题给他讲半个小时 ## 计算机操作系统 @@ -180,6 +182,7 @@ - 跳表这种数据结构,你真的清楚吗,面试官可能会问这些问题! - 红黑树你了解多少,不会肯定会被面试官怼坏 - [B树,B+树,你了解多少,面试官问那些问题?](https://blog.ouyangsihai.cn/mian-shi-guan-wen-ni-b-shu-he-b-shu-jiu-ba-zhe-pian-wen-zhang-diu-gei-ta.html) +- 二叉树、二叉搜索树、二叉平衡树、红黑树、B树、B+树 ### 算法 @@ -213,6 +216,7 @@ - [MySQL高频面试题](https://mp.weixin.qq.com/s/KFCkvfF84l6Eu43CH_TmXA) - [MySQL查询优化过程](https://mp.weixin.qq.com/s/jtuLb8uAIHJNvNpwcIZfpA) +- MySQL面试官会怎么死怼你呢,我告诉你回怼他 ## 系统设计 diff --git "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" index 0aa0960..5c926ef 100644 --- "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" +++ "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" @@ -16,7 +16,7 @@ - [sleep和wait的区别](#sleep和wait的区别) - [java 地址和值传递的例子](#java-地址和值传递的例子) - [Java序列化](#java序列化) - - [java NIO,java 多线程、线程池,java 网络编程解决并发量](#java-niojava-多线程线程池java-网络编程解决并发量) + - [java NIO,java 多线程、线程池,java 网络编程解决并发量](#java-niojava-多线程-线程池java-网络编程解决并发量) - [JDBC 连接的过程 ,手写 jdbc 连接过程](#jdbc-连接的过程-手写-jdbc-连接过程) - [说出三个遇到过的程序报异常的情况](#说出三个遇到过的程序报异常的情况) - [socket 是靠什么协议支持的](#socket-是靠什么协议支持的) @@ -41,7 +41,7 @@ - [concurrenthashmap的jdk1.7和jdk1.8区别](#concurrenthashmap的jdk17和jdk18区别) - [HashMap 实现原理,扩容因子过大过小的缺点,扩容过程 采用什么方法能保证每个 bucket 中的数据更均匀 解决冲突的方式,还有没有其他方式(全域哈希)](#hashmap-实现原理扩容因子过大过小的缺点扩容过程-采用什么方法能保证每个-bucket-中的数据更均匀-解决冲突的方式还有没有其他方式全域哈希) - [Collection 集合类中只能在 Iterator 中删除元素的原因](#collection-集合类中只能在-iterator-中删除元素的原因) - - [ArrayList、LinkedList、Vector](#arraylistlinkedlistvector) + - [ArrayList、LinkedList、Vector](#arraylist-linkedlist-vector) - [还了解除 util 其他包下的 List 吗?](#还了解除-util-其他包下的-list-吗) - [CopyOnWriteArrayList](#copyonwritearraylist) - [ConcurrentHashMap 和 LinkedHashMap 差异和适用情形](#concurrenthashmap-和-linkedhashmap-差异和适用情形) @@ -77,7 +77,7 @@ - [java 线程安全都体现在哪些方面](#java-线程安全都体现在哪些方面) - [如果维护线程安全 如果想实现一个线程安全的队列,可以怎么实现?](#如果维护线程安全-如果想实现一个线程安全的队列可以怎么实现) - [Java多线程通信方式](#java多线程通信方式) - - [CountDownLatch、CyclicBarrier、Semaphore 用法总结](#countdownlatchcyclicbarriersemaphore-用法总结) + - [CountDownLatch、CyclicBarrier、Semaphore 用法总结](#countdownlatch-cyclicbarrier-semaphore-用法总结) - [juc下的内容](#juc下的内容) - [AOS等并发相关面试题](#aos等并发相关面试题) - [threadlocal](#threadlocal) @@ -129,8 +129,8 @@ - [数据库死锁问题](#数据库死锁问题) - [hash索引和B+树索引的区别](#hash索引和b树索引的区别) - [可重复的原理MVCC](#可重复的原理mvcc) - - [count(1)、count(*)、count(列名)](#count1countcount列名) - - [mysql的undo、redo、binlog的区别](#mysql的undoredobinlog的区别) + - [count(1)、count(*)、count(列名)](#count1-count-count列名) + - [mysql的undo、redo、binlog的区别](#mysql的undo-redo-binlog的区别) - [explain解释](#explain解释) - [mysql分页查询优化](#mysql分页查询优化) - [sql注入](#sql注入) From 17fa15cfa139418054c31410c1cb843d9c097d4f Mon Sep 17 00:00:00 2001 From: sihai <1446037005@qq.com> Date: Thu, 15 Jul 2021 16:30:09 +0800 Subject: [PATCH 22/38] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 59 +++++++++++++++---- ...21\351\235\242\350\257\225\351\242\230.md" | 33 +++++++++++ 2 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 "docs/operating-system/linux\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" diff --git a/README.md b/README.md index cb0e7c3..7e1c3a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ **[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 是本人在备战春招及这几年学习的知识沉淀,这里面有很多都是自己的原创文章,同时,也有很多是本在备战春招的过程中觉得对面试特别有帮助的文章,**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 不一定可以帮助你进入到 BAT 等大厂,但是,如果你认真研究,仔细思考,我相信你也可以跟我一样幸运的进入到大厂。 -本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,春招目前拿到了大厂offer。 +本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,CSDN博客地址:https://sihai.blog.csdn.net,春招目前拿到了大厂offer。 如果觉得有帮助,给个 **star** 好不好,哈哈(目前还不是很完善,后面会一一补充)。 @@ -8,12 +8,23 @@ **一起冲!!!** -#### Java面试思维导图预警 +#### Java学习资源汇总(个人总结) -这里再分享一些我总结的**Java面试思维导图**,我靠这些导图拿到了一线互联网公司的offer,先来瞧瞧。 +1、Java基础到Java实战全套学习视频教程,包括多个企业级实战项目:https://urlify.cn/YFzABz 密码: pi95 + +![](http://image.ouyangsihai.cn/FodtVWogwlUu5DhV1dZuAEhBznf7) + +2、面试算法资料,这是总结的算法资料,学完基本可以应付80%大厂:https://urlify.cn/N7vIj2 密码: ijoi + +3、大厂面试资料,一年时间总结,覆盖Java所有技术点:https://urlify.cn/Vzmeqy 密码: j9t2 + +4、面试思维导图,手打总结: https://urlify.cn/vUNF7z 密码: adbo + +#### Java面试思维导图(手打) + +这里再分享一些我总结的**Java面试思维导图**,我靠这些导图拿到了一线互联网公司的offer,预览在下方,先来瞧瞧。 ![](http://image.ouyangsihai.cn/FtJ3PbBRdNSa1NaUr96JmUq24_yS) -![](http://image.ouyangsihai.cn/FsTv2aMsGcv8M9Rsyx6POBOGKY1g) **划重点**:想要`导图源文件`,更多`Java面试思维导图`,请关注我的公众号 **程序员的技术圈子**,`微信扫描下面二维码`,回复:**思维导图**,获取思维导图,绿色通道关注福利,等你拿。 @@ -90,25 +101,47 @@ ## 项目准备 -- [如何进行项目的自我介绍呢?](docs/interview/自我介绍和项目介绍.md) - -- [项目需要准备必备知识及方法](docs/project/秒杀项目总结.md) +- [我的个人项目介绍模板](docs/interview/自我介绍和项目介绍.md) +- [本人真实经历:面试了20家大厂之后,发现这样介绍项目经验,显得项目很牛逼!](https://mp.weixin.qq.com/s/JBtCRaw9Nabk9aIImjkFIQ) +- [项目必备知识及解决方案](docs/project/秒杀项目总结.md) ## 面试知识点 -- [各公司面试知识点汇总](docs/interview-experience/面试常见知识.md) -- [面试常见问题分类汇总](docs/interview-experience/面试常见问题分类汇总.md) +- [各大公司面试知识点汇总](docs/interview-experience/各大公司面经.md) +- [Java后端面试常见问题分类汇总(高频考点)](docs/interview-experience/面试常见问题分类汇总.md) ## 公司面经 - [2020年各公司面试经验汇总](docs/interview-experience/各大公司面经.md) +- [最新!!招银网络科技Java面经,整理附答案](https://mp.weixin.qq.com/s/HAUOH-EYS_3Ho2XxYkTGXA) +- [拿了 30K 的 offer!](https://mp.weixin.qq.com/s/R4gZ8IuskxgxA1SZwfCOoA) +- [重磅面经!!四面美团最终拿到了 offer](https://mp.weixin.qq.com/s/P1mDcH5hEXqNp2Jpz5Qjmg) +- [十面阿里,七面头条](https://mp.weixin.qq.com/s/FErQnLvYnuZxiaDkYWPO5A) ## Java ### 基础 -- [Java基础系列文章](https://mp.weixin.qq.com/mp/homepage?__biz=MzI2OTQ4OTQ1NQ==&hid=1&sn=c455e51f87eaa9c12d6b45e0e4d33960&scene=1&devicetype=iOS13.3.1&version=17000c2b&lang=zh_CN&nettype=WIFI&ascene=7&session_us=gh_2bfc34fbf760&fontScale=100&wx_header=1) +这几篇文章虽然是基础,但是确实深入理解基础,如果你能很好的理解这些基础,那么对于Java基础面试题也是没有什么问题的,背面试题不如理解原理,很重要。 + +- [Java基础思维导图](http://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483823&idx=1&sn=4588a874055e8ca54f2bbe1ede12cff4&scene=19#wechat_redirect) +- [Java基础(一) 深入解析基本类型](http://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483948&idx=1&sn=cb0ae3d82a1629e3a0538b6f31e2473b&scene=19#wechat_redirect) +- [Java基础(二) 自增自减与贪心规则](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483951&idx=1&sn=af5b54ed2e26d975f96643d9dfd66fab&scene=19#wechat_redirect) +- [Java基础(三) 加强型for循环与Iterator](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483952&idx=1&sn=43130fdf815970e0e12347d057c6b24f&scene=19#wechat_redirect) +- [Java基础(四) java运算顺序的深入解析](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483955&idx=1&sn=abfb3e8ac31cb84bb78216d9c953abc0&scene=19#wechat_redirect) +- [Java基础(五) String性质深入解析](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483956&idx=1&sn=1c19164967621fa5449a7830d006c8f9&scene=19#wechat_redirect) +- [Java基础(六) switch语句的深入解析](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483999&idx=1&sn=092ad983f87798360ef33b1485f3201b&scene=19#wechat_redirect) +- [Java基础(七) 深入解析java四种访问权限](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484000&idx=1&sn=0b188c70ac54c65a0419ab0d5da14af4&scene=19#wechat_redirect) +- [Java基础(八) 深入解析常量池与装拆箱机制](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484002&idx=1&sn=a1d9ec01c91537aca444408c989f5a50&scene=19#wechat_redirect) +- [Java基础(九) 可变参数列表介绍](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484003&idx=1&sn=84366ed430c332d4b8e2b2d6b54280f4&scene=19#wechat_redirect) +- [Java基础(十) 深入理解数组类型](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484004&idx=1&sn=9c58b6948f05bbeffea3552fec9ee9a6&scene=19#wechat_redirect) +- [Java基础(十一) 枚举类型](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484005&idx=1&sn=5aaec133dca189fcabc86defcd54c5b8&scene=19#wechat_redirect) +- [类与接口(二)java的四种内部类详解](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484075&idx=1&sn=e0fd37cc5c1eb5fb359ed3dc9c15af66&scene=19#wechat_redirect) +- [类与接口(三)java中的接口与嵌套接口](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484076&idx=1&sn=1903edbc469b2660e51e1154a8b63a27&scene=19#wechat_redirect) +- [类与接口(四)方法重载解析](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484078&idx=1&sn=db5f231dc64974057d4ee29af1649e8b&scene=19#wechat_redirect) +- [类与接口(五)java多态、方法重写、隐藏](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484083&idx=1&sn=d5b3d1daca2eb4e8d9583d75e6f6ad6c&scene=19#wechat_redirect) + ### 容器(包括juc) @@ -149,6 +182,7 @@ - [深入理解Java虚拟机-你了解GC算法原理吗](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ni-liao-jie-gc-suan-fa-yuan-li-ma.html) - [几个面试官常问的垃圾回收器,下次面试就拿这篇文章怼回去!](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-jian-de-la-ji-hui-shou-qi.html) - [面试官100%会严刑拷打的 CMS 垃圾回收器,下次面试就拿这篇文章怼回去!](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-cms-la-ji-hui-shou-qi.html) +- [JVM 面试题 87 题详解](https://sihai.blog.csdn.net/article/details/118737581) ### Java8 @@ -162,6 +196,7 @@ ## 计算机网络 - [http面试问题全解析](docs/network/http面试问题全解析.md) +- [计算机网络常见面试题](https://sihai.blog.csdn.net/article/details/118737663) - 关于tcp、udp网络模型的问题,这篇文章告诉你 - http、https还不了解,别慌! - 面试官问我计算机网络的问题,我一个问题给他讲半个小时 @@ -173,6 +208,8 @@ ## Linux - [java工程师linux命令,这篇文章就够了](https://blog.ouyangsihai.cn/java-gong-cheng-shi-linux-ming-ling-zhe-pian-wen-zhang-jiu-gou-liao.html) +- [linux常见面试题(基础版)](https://sihai.blog.csdn.net/article/details/118737736) +- [linux高频面试题](docs/operating-system/linux高频面试题.md) - 常问的几个Linux面试题,通通解决它 ## 数据结构与算法 @@ -182,10 +219,12 @@ - 跳表这种数据结构,你真的清楚吗,面试官可能会问这些问题! - 红黑树你了解多少,不会肯定会被面试官怼坏 - [B树,B+树,你了解多少,面试官问那些问题?](https://blog.ouyangsihai.cn/mian-shi-guan-wen-ni-b-shu-he-b-shu-jiu-ba-zhe-pian-wen-zhang-diu-gei-ta.html) +- [这篇文章带你彻底理解红黑树](https://sihai.blog.csdn.net/article/details/118738496) - 二叉树、二叉搜索树、二叉平衡树、红黑树、B树、B+树 ### 算法 + - [2020年最新算法面试真题汇总](docs/dataStructures-algorithms/算法面试真题汇总.md) - [2020年最新算法题型难点总结](docs/dataStructures-algorithms/算法题目难点题目总结.md) - [关于贪心算法的leetcode题目,这篇文章可以帮你解决80%](https://blog.ouyangsihai.cn/jie-shao-yi-xia-guan-yu-leetcode-de-tan-xin-suan-fa-de-jie-ti-fang-fa.html) diff --git "a/docs/operating-system/linux\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" "b/docs/operating-system/linux\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..bd0a83d --- /dev/null +++ "b/docs/operating-system/linux\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,33 @@ +linux查找命令 + +https://blog.51cto.com/whylinux/2043871 +项目部署常见linux命令 + +https://blog.csdn.net/u010938610/article/details/79625988 +进程文件里有哪些信息 +sed 和 awk 的区别 + +awk用法:https://www.cnblogs.com/isykw/p/6258781.html + +其实sed和awk都是每次读入一行来处理的,区别是:sed 适合简单的文本替换和搜索;而awk除了自动给你分列之外,里面丰富的函数大大增强了awk的功能。数据统计,正则表达式搜索,逻辑处理,前后置脚本等。因此基本上sed能做的,awk可以全部完成并且做的更好。 + +作者:哩掉掉 链接:https://www.zhihu.com/question/297858714/answer/572046422 +linux查看进程并杀死的命令 + +https://blog.csdn.net/qingmu0803/article/details/38271077 +有一个文件被锁住,如何查看锁住它的线程? +如何查看一个文件第100行到150行的内容 + +https://blog.csdn.net/zmx19951103/article/details/78575265 +如何查看进程消耗的资源 + +https://www.cnblogs.com/freeweb/p/5407105.html +如何查看每个进程下的线程? + +https://blog.csdn.net/inuyashaw/article/details/55095545 +linux 如何查找文件 + +linux命令:https://juejin.im/post/5d3857eaf265da1bd04f2437 + +select epoll等问题 +https://juejin.im/post/5b624f4d518825068302aee9#heading-13 \ No newline at end of file From f4991c41141dd471f72cb521f929d47108f265cc Mon Sep 17 00:00:00 2001 From: sihai <1446037005@qq.com> Date: Thu, 15 Jul 2021 16:40:24 +0800 Subject: [PATCH 23/38] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E6=96=87=E7=AB=A0=E4=B8=8E=E8=B5=84=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7e1c3a6..8f796a7 100644 --- a/README.md +++ b/README.md @@ -383,13 +383,13 @@ ## Java学习资源 -- [2020年Java学习资源,你想要的都在这里了](https://mp.weixin.qq.com/s/wLjjy7D57s3UOv4sr8Lxkg) +- [2021 Java 1000G 最新学习资源大汇总](https://mp.weixin.qq.com/s/I0jimqziHqRNaIy0kXRCnw) **截图** -![](http://image.ouyangsihai.cn/Fl0FhkpxLNw0_4-pe8_f8MwAyHzc) -![](http://image.ouyangsihai.cn/Fp3EtjR1FbKPJG2uPdGpMiFjHBNR) -![](http://image.ouyangsihai.cn/FqEKc4i6lsfLCRomFAIksG_rgveY) +![](http://image.ouyangsihai.cn/FkS4oPQKzp9n4x1Lqy-po99DSdwM) +![](http://image.ouyangsihai.cn/FmuFM8wLHGYUt9EKdWSi_TN1utxH) +![](http://image.ouyangsihai.cn/Fs29GQ1AQ8hRJpHC2ctzKr5KfrE6) ## Java书籍推荐 From 3b29fb1d4060bbdd0e61602c533b69daf8fa0e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AC=A7=E9=98=B3=E6=80=9D=E6=B5=B7?= Date: Sat, 24 Jul 2021 10:13:41 +0800 Subject: [PATCH 24/38] =?UTF-8?q?=E4=BF=AE=E6=94=B9readme=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- ...\347\273\234\351\235\242\350\257\225-1.md" | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 "docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" diff --git a/README.md b/README.md index 8f796a7..a2eef96 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ **[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 是本人在备战春招及这几年学习的知识沉淀,这里面有很多都是自己的原创文章,同时,也有很多是本在备战春招的过程中觉得对面试特别有帮助的文章,**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 不一定可以帮助你进入到 BAT 等大厂,但是,如果你认真研究,仔细思考,我相信你也可以跟我一样幸运的进入到大厂。 -本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,CSDN博客地址:https://sihai.blog.csdn.net,春招目前拿到了大厂offer。 +本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,CSDN博客地址:[https://sihai.blog.csdn.net](https://sihai.blog.csdn.net),春招目前拿到了大厂offer。 如果觉得有帮助,给个 **star** 好不好,哈哈(目前还不是很完善,后面会一一补充)。 diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" new file mode 100644 index 0000000..8ff7d76 --- /dev/null +++ "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" @@ -0,0 +1,21 @@ +面试官:OSI与TCP/IP的各层的结构,都有哪些协议呢? +我:哦,我好像知道,我说一下我的理解,这个主要有两种参考模型,一种是基于OSI的参考模型,可以分为7层,分别是:物理层、数据链路层、网络层、运输层、会话层、表示层和应用层;另一种是基于TCP、IP的参考模型,可以分为4层,分别是:网络接口层、网络层、运输层和应用层。 + +关于每一层的协议,我知道的还不少呢,我来说说我知道的吧。 + +应用层:RIP、FTP、DNS、Telnet、SMTP、HTTP、WWW + +我知道的还不少吧,嘚瑟一下,让我一口气说完吧。 + +表示层:JPEG、MPEG、ASCII,MIDI +会话层:RPC、SQL +传输层:TCP、UDP、SPX +网络层:IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGMP +数据链路层:PPP、HDLC、VLAN、MAC +物理层:RJ45、IEEE802.3 + +面试官:你能解释一下RJ45吗? + +我:嗯嗯嗯,你自己查一下吧。。。 + + From 24e6272a8ecc460b8db17e8a4ac1910655877e5e Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Sat, 24 Jul 2021 12:10:57 +0800 Subject: [PATCH 25/38] =?UTF-8?q?=E6=9B=B4=E6=96=B0Java=E5=AD=A6=E4=B9=A0?= =?UTF-8?q?=E8=B7=AF=E7=BA=BF=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a2eef96..6d273d1 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,40 @@ **[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 是本人在备战春招及这几年学习的知识沉淀,这里面有很多都是自己的原创文章,同时,也有很多是本在备战春招的过程中觉得对面试特别有帮助的文章,**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 不一定可以帮助你进入到 BAT 等大厂,但是,如果你认真研究,仔细思考,我相信你也可以跟我一样幸运的进入到大厂。 -本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,CSDN博客地址:[https://sihai.blog.csdn.net](https://sihai.blog.csdn.net),春招目前拿到了大厂offer。 +本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量251W+**,**CSDN 博客专家**,CSDN博客地址:[https://sihai.blog.csdn.net](https://sihai.blog.csdn.net),春招目前拿到了大厂offer。 如果觉得有帮助,给个 **star** 好不好,哈哈(目前还不是很完善,后面会一一补充)。 -**JavaInterview 最新地址**:https://github.com/OUYANGSIHAI/JavaInterview - **一起冲!!!** -#### Java学习资源汇总(个人总结) +👉 如果你不知道该学习什么的话,请看 [Java 学习线路图是怎样的?](https://zhuanlan.zhihu.com/p/392712685) (原创不易,欢迎点赞),这是 2021 最新最完善的 Java 学习路线! + +👉 Java学习资源汇总(个人总结) -1、Java基础到Java实战全套学习视频教程,包括多个企业级实战项目:https://urlify.cn/YFzABz 密码: pi95 +- Java基础到Java实战全套学习视频教程,包括多个企业级实战项目:https://urlify.cn/YFzABz 密码: pi95 -![](http://image.ouyangsihai.cn/FodtVWogwlUu5DhV1dZuAEhBznf7) +- 面试算法资料,这是总结的算法资料,学完基本可以应付80%大厂:https://urlify.cn/N7vIj2 密码: ijoi -2、面试算法资料,这是总结的算法资料,学完基本可以应付80%大厂:https://urlify.cn/N7vIj2 密码: ijoi +- 大厂面试资料,一年时间总结,覆盖Java所有技术点:https://urlify.cn/Vzmeqy 密码: j9t2 -3、大厂面试资料,一年时间总结,覆盖Java所有技术点:https://urlify.cn/Vzmeqy 密码: j9t2 +- 面试思维导图,手打总结: https://urlify.cn/vUNF7z 密码: adbo -4、面试思维导图,手打总结: https://urlify.cn/vUNF7z 密码: adbo +👉 Java各种电子书:如果你需要各种电子书,可以移步这个仓库 [Java电子书合集](https://github.com/hello-go-maker/cs-books) -#### Java面试思维导图(手打) +👉 Java面试思维导图(手打) -这里再分享一些我总结的**Java面试思维导图**,我靠这些导图拿到了一线互联网公司的offer,预览在下方,先来瞧瞧。 +👉 这里再分享一些我总结的**Java面试思维导图**,我靠这些导图拿到了一线互联网公司的offer,预览在下方,先来瞧瞧。 ![](http://image.ouyangsihai.cn/FtJ3PbBRdNSa1NaUr96JmUq24_yS) -**划重点**:想要`导图源文件`,更多`Java面试思维导图`,请关注我的公众号 **程序员的技术圈子**,`微信扫描下面二维码`,回复:**思维导图**,获取思维导图,绿色通道关注福利,等你拿。 +**划重点**:更多`Java面试思维导图`,请关注我的公众号 **程序员的技术圈子**,`微信扫描下面二维码`,回复:**思维导图**,获取思维导图,绿色通道关注福利,等你拿。 + +![](http://image.ouyangsihai.cn/FuRA5sT9JUaVbx-YD-Acor04AWhF) -![](http://image.ouyangsihai.cn/FlL0VJf1Q4gCfrc8RhL-SL-xiiXo) + -

+
@@ -385,12 +387,6 @@ - [2021 Java 1000G 最新学习资源大汇总](https://mp.weixin.qq.com/s/I0jimqziHqRNaIy0kXRCnw) -**截图** - -![](http://image.ouyangsihai.cn/FkS4oPQKzp9n4x1Lqy-po99DSdwM) -![](http://image.ouyangsihai.cn/FmuFM8wLHGYUt9EKdWSi_TN1utxH) -![](http://image.ouyangsihai.cn/Fs29GQ1AQ8hRJpHC2ctzKr5KfrE6) - ## Java书籍推荐 @@ -447,6 +443,8 @@ 如果大家想要实时关注我更新的文章以及分享的干货的话,关注我的公众号 **程序员的技术圈子**。 -![](http://image.ouyangsihai.cn/FlL0VJf1Q4gCfrc8RhL-SL-xiiXo) +![](http://image.ouyangsihai.cn/FuRA5sT9JUaVbx-YD-Acor04AWhF) + + From acea66cd30555c0563c45a97d138afaff220b10d Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Tue, 28 Dec 2021 08:44:52 +0800 Subject: [PATCH 26/38] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 11 + ...42\350\257\225\351\242\230\344\270\200.md" | 700 ++++++++++++++++++ ...56\345\272\223\344\274\230\345\214\226.md" | 12 + ...37\346\234\272\351\235\242\350\257\225.md" | 588 +++++++++++++++ ...42\350\257\225\351\227\256\351\242\230.md" | 169 +++++ ...\347\273\234\351\235\242\350\257\225-1.md" | 21 - ...235\242\350\257\225-TCP\345\222\214UDP.md" | 172 +++++ ...7\273\234\351\235\242\350\257\225-http.md" | 264 +++++++ 8 files changed, 1916 insertions(+), 21 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 "docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" create mode 100644 "docs/database/\346\225\260\346\215\256\345\272\223\344\274\230\345\214\226.md" create mode 100644 "docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" create mode 100644 "docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234-\345\205\266\344\273\226\347\233\270\345\205\263\351\235\242\350\257\225\351\227\256\351\242\230.md" delete mode 100644 "docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" create mode 100644 "docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-TCP\345\222\214UDP.md" create mode 100644 "docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-http.md" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9a17302 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "qiniu.access_key": "5zQ0AuES-7MGr7y89zIyMIK7JsNUlpTLggi5JtGu", + "qiniu.bucket": "sihai", + "qiniu.domain": "image.ouyangsihai.cn", + "qiniu.enable": true, + "qiniu.secret_key": "Mq3GKXAmESb4QGcv8mY8-lNWt42G0AFHpTKgl5Yh", + "pasteImageToQiniu.access_key": "5zQ0AuES-7MGr7y89zIyMIK7JsNUlpTLggi5JtGu", + "pasteImageToQiniu.bucket": "sihai", + "pasteImageToQiniu.domain": "image.ouyangsihai.cn", + "pasteImageToQiniu.secret_key": "Mq3GKXAmESb4QGcv8mY8-lNWt42G0AFHpTKgl5Yh" +} \ No newline at end of file diff --git "a/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" "b/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" new file mode 100644 index 0000000..9297b4d --- /dev/null +++ "b/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" @@ -0,0 +1,700 @@ +### 基础问题 + +> truncate和delete有什么区别? + +truncate table:删除内容、不删除定义、释放空间 +delete table:删除内容、不删除定义、不释放空间 +drop table:删除内容、删除定义、释放空间 + +具体来说,有以下区别: +- truncate table 只能删除表中全部数据,delete from 可以删除表中的全部数据也可以部分删除。 +- delete from记录是一条条删除的,删除的每一行记录都会进日志,而truncate一次性删除整个页,日志只会记录页的释放。 +- truncate删除后不能回滚,delete则可以。 +- truncate的删除速度比delete快。 +- delete删除后,删除的数据占用的存储空间还存在,可以恢复数据;truncate则空间不存在,同时不能恢复数据。 + +总的来说,其差别在于,truncate删除是不可恢复的,同时空间也不存在,不支持回滚,而delete删除正好相反。 + +> select Count(*)、select Count(数字)和select Count(column)的区别? + +count(*)和count(1)返回的结果是记录的总行数,包括对NULL的统计;而count(column)是不会记录NULL的统计。 + +> EXISTS关键词的使用方法 + +EXISTS表示是否存在,使用EXISTS时,如果内存查询语句查询到符合条件的值,就返回一个true,否则,将返回false。 + +例如: +```sql +SELECT * FROM user + where EXISTS + (SELECT name FROM employee WHERE id=100) +``` + +如果employee表中存在id为100的员工,内层查询就会返回一个true,外层查询接收到true后,开始查询user表中的数据,因为where没有设置其他查询条件,所以,将查询出user的全部数据。 + +> 内连接(inner join)和外连接的区别? + +- 内连接:只会查询出两表连接符合条件的记录。 + +```sql +SELECT * FROM user1 u1 INNER JOIN user2 u2 ON u1.id = u2.id; +``` + +如上sql所示,只会查询到user1和user2关联符合条件的记录。 + +外连接分为左外连接、右外连接和全外连接。 + +- 左外连接 + +它的原理是:以左表为基准,去右表匹配数据,找不到匹配的用NULL补齐。其显示左表的全部记录和右表符合连接条件的记录。 + +- 右外连接 + +它的原理是:以右表为基准,去左表匹配数据,找不到匹配的用NULL补齐。其显示右表的全部记录和左表符合连接条件的记录。这正好与左外连接相反。 + +- 全外连接 + +除了显示符合连接条件的记录之外,两个表的其他数据也会显示出来。 + +> inner join 和 left join性能谁优谁劣? + +![](http://image.ouyangsihai.cn/Ft-S3YZNbH0Il2x0K6wbxSUcZ2yE) + +如上图,是mysql的执行顺序,我们可以看出,外查询是在内查询的基础上,进而进行查询操作的。因此,我们从理论上可以得出,内连接的执行效率是更好一些的。 + +但是,外连接也是会进行优化操作的,在编译优化阶段,如果左连接的结果和内连接一样,左连接查询会转换成内连接查询,但这也表明编译优化器也认为内连接的效率是更高的。 + +虽然从查询的结果来看一般不会有太大的区别,但是,如果左右表之间的数据差别很大,内连接的效率是明显更高的,因为左连接以左表为基准,并且会进行回表操作。 + +最后,给出一个结论:在外连接和内连接都可以实现需求时,建议使用内连接进行操作。 + +> 存储过程是什么,优势是什么,为什么不建议使用? + +存储过程:为了完成特定功能的SQL语句集,存储在数据库中,一次编译后永久有效,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库中的一个重要对象。在数据量特别庞大的情况下利用存储过程能达到倍速的效率提升。 + +**优势** +- 存储过程是预编译的,因此执行速度较快; +- 存储过程在服务端执行,减少客户端的压力; +- 减少网络流量,客户端只需要传递存储过程名称和参数即可进行调用,减少了传输的数据量; +- 一次编写,任意次执行,复用的思想简单便捷; +- 安全性较高,因为在服务端执行,管理员可以对存储过程进行权限限制,能够避免非法的访问,保证数据的安全性。 + +**缺点** + +- 调试麻烦,可移植性查。 + +这一个缺点也是存储过程在实际开发中用的不多的原因,现在开发的理念是简便。 + +> 数据库的三级范式 + +1NF:原子性,字段不可再分; +2NF:在满足第一范式的基础上,一个表只能说明一个事物,非主键属性必须玩去哪依赖于主键属性; +3NF:在满足第二范式的基础上,每列都与主键有直接关系,不存在传递依赖,任何非主键属性不依赖于其他非主键属性。 + +> sql语句应该考虑哪些安全性 + +- 防止sql注入,对sql语句尽量进行过滤同时使用预编译的sql语句绑定变量 +- 查询错误信息不要返回给用户,将错误记录到日志 +- 最小用户权限设置,最好不要使用root用户连接数据库 +- 定期做数据备份,避免数据丢失 + +> 什么叫sql注入,如何防止? + +SQL注入是一种将SQL代码添加到输入参数中,传递到服务器解析并执行的一种攻击手法。 + +**SQL注入攻击**是输入参数未经过滤,然后直接拼接到SQL语句当中解析,执行达到预想之外的一种行为,称之为SQL注入攻击。 + +常见的sql注入,有下列几种: +- 数字注入 + +例如,查询的sql语句为:`SELECT * FROM user WHERE id = 1`,正常是没有问题的,如果我们进行sql注入,写成`SELECT * FROM user WHERE id = 1 or 1=1`,那么,这个语句永远都是成立的,这就有了问题,也就是sql注入。 + +- 字符串注入 + +字符串注入是因为注释的原因,导致sql错误的被执行,例如字符`#`、`--`。 + +例如,`SELECT * FROM user WHERE username = 'sihai'#'ADN password = '123456'`,这个sql语句'#'后面都被注释掉了,相当于`SELECT * FROM user WHERE username = 'sihai' `。 + +这种情况我们在mybatis中也是会存在的,所以在服务端写sql时,需要特别注意此类情况。 + +该如何防范此类问题呢? +- 严格检查输入变量的类型和格式,也就是对相关传入的参数进行验证,尽可能降低风险。 +- 过滤和转义特殊字符。 +- 利用mysql的预编译机制,在Java中mybatis也是有预编译的方法的,所以可以采用这种方式避免。 + +> MySQL中InnoDB和MyISAM的区别? + +- 事务处理方面:MyISAM强调的是性能,查询的速度比InnoDB更快,但是,不支持事务,InnoDB是支持事务的。 +- 外键:InnoDB支持外键,MyISAM不支持。 +- 锁:InnoDB支持行锁和表锁,默认会使用行锁,而MyISAM只是支持表锁。由于行锁是能更好的支持并发操作,因此,InnoDB更加适合插入和更新操作较多的情况,而MyISAM适用于频繁查询操作。 +- 全文索引:MyISAM支持全文索引,InnoDB不支持。但是,在5.6版本开始InnoDB也开始支持全文索引。 +- 表主键:MyISAM允许没有主键的表存在,而InnoDB如果不存在主键,会自动生成一个6字节的主键。 +- 查询表的行数差异:InnoDB不保存表的函数信息,因此,select count(*)时会扫描整个表来进行计算;MyISAM内置了计数器,只需要简单的读取保存好的行数即可。 + + +### 事务相关 + +> 数据库事务的四个基本要素? + +事务是指一组SQL语句组成的逻辑处理单元。 + +ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability) + + +- 原子性(Atomicity) + +原子性指的是整个数据库的事务是一个不可分割的工作单位,每一个都应该是一个原子操作。 + +当我们执行一个事务的时候,如果一系列的操作中,有一个操作失败了,那么,需要将这一个事务中的所有操作恢复到执行事务之前的状态,这就是事务的原子性。 + +下面举个简单的例子。 +```java +i++; +``` +上面这个最简单不过的代码经常也会被问到,这是一个原子操作吗?那肯定不是,如果我们把这个代码放到一个事务中来说,当i+1出现问题的时候,回滚的就是整个代码i++(i = i + 1)了,所以回滚之后,i的值也是不会改变的。 + +以上就是原子性的概念。 + +- 一致性(consistency) + +一致性是指事务将数据库从一种状态转变为下一种一致性的状态,也就是说在事务执行前后,这两种状态应该是一样的,也就是数据库的完整性约束不会被破坏。 + +另外,需要注意的是一致性是不关注中间状态的,比如银行转账的过程,你转账给别人,至于中间的状态,你少了500 ,他多了500,这些中间状态不关注,如果分多次转账中间状态也是不可见的,只有最后的成功或者失败的状态是可见的。 + +如果到分布式的一致性问题,又可以分为强一致性、弱一致性和最终一致性,关于这些概念,可以自己查查,还是很有意思的。 + +- 隔离性(isolation) + +事务我们是可以开启很多的,MySQL数据库中可以同时启动很多的事务,但是,事务和事务之间他们是相互分离的,也就是互不影响的,这就是事务的隔离性。 + +- 持久性(durability) + +事务的持久性是指事务一旦提交,就是永久的了,就是发生问题,数据库也是可以恢复的。因此,持久性保证事务的高可靠性。 + +> 并发事务会带来什么问题 + +#### 脏读 + +**脏读:** 在不同的事务下,当前事务可以读到另外事务未提交的数据。另外我们需要注意的是默认的MySQL隔离级别是`REPEATABLE READ`是不会发生脏读的,脏读发生的条件是需要事务的隔离级别为`READ UNCOMMITTED`,所以如果出现脏读,可能就是这种隔离级别导致的。 + +下面我们通过一个例子看一下。 +![](http://image.ouyangsihai.cn/Fh7WDJf8NpXn0QY0sA-vpVyGtGhs) + +从上面这个例子可以看出,当我们的事务的隔离级别为`READ UNCOMMITTED`的时候,在会话A还没有提交时,会话B就能够查询到会话A没有提交的数据。 + + +#### 不可重复读 + +**不可重复读:** 是指在一个事务内多次读取同一集合的数据,但是多次读到的数据是不一样的,这就违反了数据库事务的一致性的原则。但是,这跟脏读还是有区别的,脏读的数据是没有提交的,但是不可重复读的数据是已经提交的数据。 + +我们通过下面的例子来看一下这种问题的发生。 + +![](http://image.ouyangsihai.cn/FuqDKDktSrQTMPP8fAm0cmWdNsR8) + +从上面的例子可以看出,在A的一次会话中,由于会话B插入了数据,导致两次查询的结果不一致,所以就出现了不可重复读的问题。 + +我们需要注意的是不可重复读读取的数据是已经提交的数据,事务的隔离级别为`READ COMMITTED`,这种问题我们是可以接受的。 + +如果我们需要避免不可重复读的问题的发生,那么我们可以使用**Next-Key Lock算法**(设置事务的隔离级别为`READ REPEATABLE`)来避免,在MySQL中,不可重复读问题就是Phantom Problem,也就是**幻像问题**。 + +#### 丢失更新 + +**丢失更新:** 指的是一个事务的更新操作会被另外一个事务的更新操作所覆盖,从而导致数据的不一致。在当前数据库的任何隔离级别下都不会导致丢失更新问题,要出现这个问题,在多用户计算机系统环境下有可能出现这种问题。 + +如何避免丢失更新的问题呢,我们只需要让事务的操作变成串行化,不要并行执行就可以。 + +我们一般使用`SELECT ... FOR UPDATE`语句,给操作加上一个排他X锁。 + +> 数据库事务的隔离级别 + +数据库提供了四种隔离级别。 + +- 读未提交数据(READ UNCOMMITTED) + +允许事务读取未被其他事务提交的变更,可能有脏读、不可重复读和幻读的问题。 + +例如,某个时刻会话a修改了一个数据,但是未提交,此时,会话b读取了该数据,此时,a回滚了事务,这就会出现a、b数据不一致,这就是**脏读**。 + +- 读已提交数据(READ COMMITTED) + +允许事务读取已经被其他事务提交的变更,可能不可重复读和幻读的问题。 + +例如,某个时刻会话a修改了一个数据,提交了,此时的结果是10,此时b对该数据进行了修改为20,并提交了,此时会话a再次读取该数据,发现结果是20了,因此,在同一事务中,出现了两次读取的结果不一致的现象,这就是**不可重复读**。 + +- 可重复读(REPEATABLE READ,默认隔离级别) + +可重复读,从字面上的意思也能明白,就是在同一事务中读取多次,确保每次读取到的数据都是一样的,可以避免脏读和不可重复读,但是可能会出现幻读。 + +- 可串行化(SERIALIZABLE) + +可串行化是指所有的事务都是一个接一个执行,可以避免所有的问题,但是,效果太低。 + +最后,再用一张图来总结一下。 + +![](http://image.ouyangsihai.cn/Fh1arOgS78BnrjBXKwGw8NSP27uM) + + +### 锁相关 + +> 数据库中锁机制,说说数据库中锁的类型 + +对于MySQL来说,锁是一个很重要的特性,数据库的锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性,这样才能保证在高并发的情况下,访问数据库的时候,数据不会出现问题。 + +在数据库中,lock和latch都可以称为锁,但是意义却不同。 + +**Latch**一般称为`闩锁`(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差,在InnoDB引擎中,Latch又可以分为`mutex`(互斥量)和`rwlock`(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。 + +**Lock**的对象是`事务`,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同)。 + +InnoDB存储引擎中存在着不同类型的锁,下面一一介绍一下。 + +**S or X (共享锁、排他锁)** + +数据的操作其实只有两种,也就是读和写,而数据库在实现锁时,也会对这两种操作使用不同的锁;InnoDB 实现了标准的**行级锁**,也就是**共享锁(Shared Lock)和互斥锁(Exclusive Lock)**。 +- 共享锁(读锁)(S Lock),允许事务读一行数据。 +- 排他锁(写锁)(X Lock),允许事务删除或更新一行数据。 + +**IS or IX (共享、排他)意向锁** + +为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB存储引擎支持一种额外的锁方式,就称为**意向锁**,意向锁在 InnoDB 中是**表级锁**,意向锁分为: + + +- 意向共享锁:表达一个事务想要获取一张表中某几行的共享锁。 +- 意向排他锁:表达一个事务想要获取一张表中某几行的排他锁。 + + +另外,这些锁之间的并不是一定可以共存的,有些锁之间是不兼容的,所谓**兼容性**就是指事务 A 获得一个某行某种锁之后,事务 B 同样的在这个行上尝试获取某种锁,如果能立即获取,则称锁兼容,反之叫冲突。 + +下面我们再看一下这两种锁的兼容性。 + +- S or X (共享锁、排他锁)的兼容性 + +![](http://image.ouyangsihai.cn/Fvr5OSfX9nP2Ip30O088kVx-Pdza) + + +- IS or IX (共享、排他)意向锁的兼容性 + +![](http://image.ouyangsihai.cn/Fgf-Pg6JPVJ7CmPyrdcow_5q-VZm) + +> MySQL中锁的粒度 + +在数据库中,锁的粒度的不同可以分为表锁、页锁、行锁,这些锁的粒度之间也是会发生升级的,**锁升级**的意思就是讲当前锁的粒度降低,数据库可以把一个表的1000个行锁升级为一个页锁,或者将页锁升级为表锁,下面分别介绍一下这三种锁的粒度(参考自博客:https://blog.csdn.net/baolingye/article/details/102506072)。 + +##### 表锁 + +表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。 + +当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大打折扣。 + +使用表级锁定的主要是MyISAM,MEMORY,CSV等一些非事务性存储引擎。 + +**特点:** 开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。 + +##### 页锁 + +页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。 +在数据库实现资源锁定的过程中,随着锁定资源颗粒度的减小,锁定相同数据量的数据所需要消耗的内存数量是越来越多的,实现算法也会越来越复杂。不过,随着锁定资源 颗粒度的减小,应用程序的访问请求遇到锁等待的可能性也会随之降低,系统整体并发度也随之提升。 +使用页级锁定的主要是BerkeleyDB存储引擎。 + +**特点:** 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。 + +##### 行锁 + +行级锁定最大的特点就是锁定对象的粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。 + +虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。 + +**特点:** 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 + +比较表锁我们可以发现,这两种锁的特点基本都是相反的,而从锁的角度来说,**表级锁**更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而**行级锁**则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。 + +##### MySQL 不同引擎支持的锁的粒度 + +![](http://image.ouyangsihai.cn/FvpMs-9FmTS7IKUBEv3WCm0IFJJH) + +> 了解一致性非锁定读和一致性锁定读吗? + +#### 一致性锁定读(Locking Reads) + +在一个事务中查询数据时,普通的SELECT语句不会对查询的数据进行加锁,其他事务仍可以对查询的数据执行更新和删除操作。因此,InnoDB提供了两种类型的锁定读来保证额外的安全性: + + - `SELECT ... LOCK IN SHARE MODE` + - `SELECT ... FOR UPDATE` + + `SELECT ... LOCK IN SHARE MODE`: 对读取的行添加S锁,其他事物可以对这些行添加S锁,若添加X锁,则会被阻塞。 + + `SELECT ... FOR UPDATE`: 会对查询的行及相关联的索引记录加X锁,其他事务请求的S锁或X锁都会被阻塞。 当事务提交或回滚后,通过这两个语句添加的锁都会被释放。 注意:只有在自动提交被禁用时,SELECT FOR UPDATE才可以锁定行,若开启自动提交,则匹配的行不会被锁定。 + + #### 一致性非锁定读 + + **一致性非锁定读(consistent nonlocking read)** 是指InnoDB存储引擎通过多版本控制(MVVC)读取当前数据库中行数据的方式。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB会去读取行的一个快照。所以,非锁定读机制大大提高了数据库的并发性。 + + ![来自网络:侵权删](http://image.ouyangsihai.cn/FhF001C8JBPwaEXPgJ9TEzFT8C6X) + +一致性非锁定读是InnoDB默认的读取方式,即读取不会占用和等待行上的锁。在事务隔离级别`READ COMMITTED`和`REPEATABLE READ`下,InnoDB使用一致性非锁定读。 + +然而,对于快照数据的定义却不同。在`READ COMMITTED`事务隔离级别下,一致性非锁定读总是**读取被锁定行的最新一份快照数据**。而在`REPEATABLE READ`事务隔离级别下,则**读取事务开始时的行数据版本**。 + +下面我们通过一个简单的例子来说明一下这两种方式的区别。 + +首先创建一张表; + +![](http://image.ouyangsihai.cn/FqzGMzTgnaAxaSX2Ko9YVUjnmFWt) + +插入一条数据; + +``` +insert into lock_test values(1); +``` + +查看隔离级别; + +``` +select @@tx_isolation; +``` + +![](http://image.ouyangsihai.cn/Fn3A5-fYhDs8rb0VvNMC1OL6B9sW) + +下面分为两种事务进行操作。 + +在`REPEATABLE READ`事务隔离级别下; + +![](http://image.ouyangsihai.cn/FhE1WGAeSkZGAq90Cx1alh3dZTVa) + +在`REPEATABLE READ`事务隔离级别下,读取事务开始时的行数据,所以当会话B修改了数据之后,通过以前的查询,还是可以查询到数据的。 + +在`READ COMMITTED`事务隔离级别下; + +![](http://image.ouyangsihai.cn/FrdXfAw47rFAzRks4-4Y9IoWKtl4) + +在`READ COMMITTED`事务隔离级别下,读取该行版本最新的一个快照数据,所以,由于B会话修改了数据,并且提交了事务,所以,A读取不到数据了。 + +> InnoDB存储引擎行锁的算法了解吗? + +InnoDB存储引擎有3种行锁的算法,其分别是: + +- Record Lock:单个行记录上的锁。 +- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。 +- Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。 + +**Record Lock**:总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。 + +**Next-Key Lock**:结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。举个例子10,20,30,那么该索引可能被Next-Key Locking的区间为: +![](http://image.ouyangsihai.cn/FrOLmJtKBxs70A0QHAc35CccZg2Y) + +除了Next-Key Locking,还有**Previous-Key Locking**技术,这种技术跟Next-Key Lock正好相反,锁定的区间是区间范围和前一个值。同样上述的值,使用Previous-Key Locking技术,那么可锁定的区间为: +![](http://image.ouyangsihai.cn/Fr-S9vxXpA--rHGiPkWMpCdhEKxT) + +不是所有索引都会加上Next-key Lock的,这里有一种**特殊的情况**,在查询的列是唯一索引(包含主键索引)的情况下,`Next-key Lock`会降级为`Record Lock`。 + +接下来,我们来通过一个例子解释一下。 +```java +CREATE TABLE test ( + x INT, + y INT, + PRIMARY KEY(x), // x是主键索引 + KEY(y) // y是普通索引 +); +INSERT INTO test select 3, 2; +INSERT INTO test select 5, 3; +INSERT INTO test select 7, 6; +INSERT INTO test select 10, 8; +``` +我们现在会话A中执行如下语句; +```java +SELECT * FROM test WHERE y = 3 FOR UPDATE +``` + +我们分析一下这时候的加锁情况。 + +- 对于主键x + +![](http://image.ouyangsihai.cn/Fo3ptcBFShRMwLAC1guVV4mUO-93) + + +- 辅助索引y + +![](http://image.ouyangsihai.cn/Fj8hUM6slurRWb5SImLWWDM2YDu0) + + +用户可以通过以下两种方式来显示的关闭Gap Lock: + +- 将事务的隔离级别设为 READ COMMITED。 +- 将参数innodb_locks_unsafe_for_binlog设置为1。 + +**Gap Lock的作用**:是为了阻止多个事务将记录插入到同一个范围内,设计它的目的是用来解决**Phontom Problem(幻读问题)**。在MySQL默认的隔离级别(Repeatable Read)下,InnoDB就是使用它来解决幻读问题。 + +>**幻读**:是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL可能会返回之前不存在的行,也就是第一次执行和第二次执行期间有其他事务往里插入了新的行。 + +> 说说悲观锁和乐观锁 + +##### 悲观锁 + +悲观锁是指在数据处理过程中,从一开始就使数据处于锁定状态,知道更改完成才释放。 + +MySQL中悲观锁使用以下方式:`select...for update` + +例如: +```sql +select name from item where id = 200 for update; +insert into orders(id,item_id) values(null,100) +update item set count = count - 1 where id = 100; +``` +我们使用`select name from item where id = 200 for update;`对id为200的数据进行了锁定,其他要对该条数据进行修改,必须等到该事务提交之后,否则无法修改。这样就保证了并发的安全性。 + +需要注意的是:`select...for update`语句必须在事务中使用。 + +悲观锁虽然能够解决并发安全的问题,但是,这种锁定会导致性能降低,加锁时间过长,并发性不好,影响系统的整体性能。因此,这种方式在实际的开发中用的很少。 + +##### 乐观锁 + +乐观锁是相对悲观锁而言的,认为数据一般情况下不会出现冲突,所以在数据进行更新的时候,才会将数据锁定。 + +**乐观锁的实现方式** + +- 使用数据版本(version)机制实现 + +该方式是在每次更新数据的时候,都要对更新的数据进行version版本+1操作。 + +其具体原理是:读取数据时,将此版本一同读出,之后,更新数据时,对此版本+1,每次提交数据时,如果提交的数据版本大于或等于数据库表中的版本,则可以更新,说明是最新数据,否则,不予更新,说明数据已经过期。 + +- 使用时间戳实现 + +该机制和version是类似的,也是需要再表中增加一个字段,类型使用时间类型即可。 + +原理:在更新数据时,检查数据库中当前的时间戳和更新前取到的时间戳,如果对比一致,就予以更新,否则不更新。 + +##### 使用场景分析 + +悲观锁可以在并发量不大的情况下使用,并发量大的情况下,使用乐观锁,大多数情况下都建议使用乐观锁。 + +### 索引相关 + +> 索引有哪些类型? + +索引有很多中类型:普通索引、唯一索引、主键索引、组合索引、全文索引,下面我们看看如何创建和删除下面这些类型的索引。 + +- 唯一索引:是在表上一个或者多个字段组合建立的索引,这些字段组合的值在表中不可重复。 +- 非唯一索引:是在表上一个或者多个字段组合建立的索引,这些字段组合的值在表中可重复。 +- 主键索引:是唯一索引的特定类型。表中创建主键时,会自动创建主键索引且只有一个。 +- 组合索引:基于多个字段而创建的索引。 + +下面再看看索引的创建和删除的方法。 + +#### 索引的创建方式 + +索引的创建是可以在很多种情况下进行的。 + +- 直接创建索引 + +``` +CREATE [UNIQUE|FULLLTEXT] INDEX index_name ON table_name(column_name(length)) +``` +`[UNIQUE|FULLLTEXT]`:表示可选择的索引类型,唯一索引还是全文索引,不加话就是普通索引。 +`table_name`:表的名称,表示为哪个表添加索引。 +`column_name(length)`:column_name是表的列名,length表示为这一列的前length行记录添加索引。 + +- 修改表结构的方式添加索引 + +``` +ALTER TABLE table_name ADD [UNIQUE|FULLLTEXT] INDEX index_name (column(length)) +``` + +- 创建表的时候同时创建索引 + +``` +CREATE TABLE `table` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` char(255) CHARACTER NOT NULL , + PRIMARY KEY (`id`), + [UNIQUE|FULLLTEXT] INDEX index_name (title(length)) +) +``` + +#### 主键索引和组合索引创建的方式 + +前面讲的都是**普通索引、唯一索引和全文索引**创建的方式,但是,**主键索引和组合索引**创建的方式却是有点不一样的,所以单独拿出来讲一下。 + +**组合索引创建方式** + +- 创建表的时候同时创建索引 + + +``` +CREATE TABLE `table` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` char(255) CHARACTER NOT NULL , + PRIMARY KEY (`id`), + INDEX index_name(id,title) +) +``` + +- 修改表结构的方式添加索引 + +``` +ALTER TABLE table_name ADD INDEX name_city_age (name,city,age); +``` + +**主键索引创建方式** +主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引。 + +``` +CREATE TABLE `table` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` char(255) CHARACTER NOT NULL , + PRIMARY KEY (`id`) +) +``` + +#### 删除索引 + +删除索引可利用`ALTER TABLE`或`DROP INDEX`语句来删除索引。类似于`CREATE INDEX`语句,`DROP INDEX`可以在`ALTER TABLE`内部作为一条语句处理,语法如下。 + +(1)`DROP INDEX index_name ON talbe_name` +(2)`ALTER TABLE table_name DROP INDEX index_name` +(3)`ALTER TABLE table_name DROP PRIMARY KEY` + +第3条语句只在删除`PRIMARY KEY`索引时使用,因为一个表只可能有一个`PRIMARY KEY`索引,因此不需要指定索引名。 + +> 数据库索引的实现原理 + +在讲解本题之前,建议大家先了解一下B+树的原理,这对后面的讲解的理解有很大的帮助,大家可以阅读一些这篇文章:[面试官问你B树和B+树,就把这篇文章丢给他](https://www.java1000.com/mian-shi-guan-wen-ni-b-shu-he-b-shu-jiu-ba-zhe-pian-wen-zhang-diu-gei-ta.html) + +基于MySQL数据库的引擎不同,索引的实现原理也是不相同的,这里主要以最主流的InnoDB和MyISAM搜索引擎举例,来说明索引的实现原理。 + +##### MyISAM索引实现原理 + +首先,我们需要明白一点,MyISAM 引擎的整理结构是采用主键索引和辅助索引构成的。MyISAM 引擎使用 B+ 树作为索引结构,叶节点的 data 域存放的是数据记录的地址。如下图所示。 + +![](http://image.ouyangsihai.cn/FhIggfScI0BhtoxY4xOCh6wLTJD0) + +由上图可知,MyISAM 引擎的叶子节点存放的是**数据记录的地址**。 + +接下来,再来看一下辅助索引。 + +![](http://image.ouyangsihai.cn/Fk9XGH0vXWUgUOqniAqBpkm8ETAf) + +在 MyISAM 中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是**主索引要求 key 是唯一的,而辅助索引的 key 可以重复。** + +辅助索引也是一颗B+树,data域保存数据记录的地址。 + +**基于MyISAM引擎的索引检索算法:** 按照B+树算法搜索索引,假若指定搜索的key存在,则可以直接取出值,然后以data域的值为地址,使用地址获取对应的数据记录。 + +##### InnoDB索引实现原理 + +基于MyISAM引擎实现的索引原理与基于InnoDB实现的索引原理总是分不开的,可以说是一对欢喜冤家,而两者之间最大的区别就在于,**InnoDB的数据文件本身就是索引文件**,怎么理解这句话呢?其实是相对于MyISAM引擎实现的索引而言的。从上分析我们可以知道,**MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址**,而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录,而保存这个索引的key就是数据表的主键,因此InnoDB引擎表数据文件本身就是**主索引**。 + +总结一下,意思就是说InnoDB实现的索引叶子节点保存的是数据记录,而MyISAM引擎实现的索引的叶子节点保存的是数据记录的地址,还需要通过地址去索引对应的数据。 + +![](http://image.ouyangsihai.cn/Fp7rcVb2jKuNnpqcZY58zQYmWman) + +另外,由于基于InnoDB实现的索引的数据文件本身要按主键聚集,因此,基于InnoDB实现的索引是必须有主键存在的。 + +**其主键策略**:如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键;如果没有找到上面符合条件的列,则会生成6个字节的bigint unsigned值作为其主键。 + +**考点:尽量在InnoDB引擎上采用自增字段做表的主键** + +InnoDB引擎数据文件本身是一棵B+树,非自增的主键会造成在插入新记录时数据文件为了维持B+树的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。如果不了解以上B+树的原理,建议阅读上面的B+树的文章。如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。 + +因此,采用在我们平时的开发当中,我们通常会采用自增主键,因为,MySQL的常用的版本中,默认的搜索引擎就是InnoDB,所以,采用自增主键,其实是可以保证比较好的效率的。 + + +**辅助索引** + +在上述MyISAM引擎索引的讲解中提到了辅助索引,MyISAM引擎中的主索引和辅助索引都是指向了同一数据记录的,而在InnoDB引擎中的表现却不一样。 + +**InnoDB的辅助索引data域存储相应记录主键索引的值而不是地址**。搜索辅助索引需要先根据辅助索引获取到主键值,再根据主键到主索引中获取到对应的数据记录。 +![](http://image.ouyangsihai.cn/FjeFUL7Iq4iIXtVOVZqBe4hJmE7G) + + +> 谈谈聚簇索引和非聚簇索引 + +其实,在上面对索引实现原理的分析当中,已经对这两个概念有了很好的讲解了,只是没有明显的指出而已。 + +InnoDB引擎采用的是**聚簇索引**,而MyISAM引擎采用的是**非聚簇索引**。这两个概念的区别就在于**叶节点是否存放一整行记录**,我们都知道,InnoDB引擎叶子节点存放的是数据记录,而MyISAM引擎的叶子节点存放的是数据记录的地址,所以说,只要理解了基于InnoDB引擎和基于MyISAM引擎实现的索引原理,就理解了以上这两个概念。这么说是不是就很容易理解的,不就是对应了两种不同的引擎吗,是不是很简单。 + +如果还不是很理解,再放一张图。 + +![](http://image.ouyangsihai.cn/Ft6BKzPJQ-Le30BjRM44ICGTjAtU) + +左边的是聚集索引(聚簇索引),右边的是非聚集索引(非聚簇索引),这两个是不是就是**基于InnoDB引擎和基于MyISAM引擎实现的索引原理**。 + +**聚簇索引的优势** + +- 当你需要取出一定范围内的数据时,用聚簇索引也比用非聚簇索引好。 +- 当通过聚簇索引查找目标数据时理论上比非聚簇索引要快,原因在于非聚簇索引叶子节点存放的是数据记录的地址,索引定位到对应主键时还要多一次目标记录寻址,即多一次I/O。 +- 采用覆盖索引扫描的查询可以直接使用页节点中的主键值。 + +**聚簇索引的不足** + +- 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。 +- 更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。 +- 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。二级索引的叶节点存储的是主键值,而不是行指针(非聚簇索引存储的是指针或者说是地址),这是为了减少当出现行移动或数据页分裂时二级索引的维护工作,但会让二级索引占用更多的空间。 +- 采用聚簇索引插入新值比采用非聚簇索引插入新值的速度要慢很多,因为插入要保证主键不能重复,判断主键不能重复,采用的方式在不同的索引下面会有很大的性能差距,聚簇索引遍历所有的叶子节点,非聚簇索引也判断所有的叶子节点,但是聚簇索引的叶子节点除了带有主键还有记录值,记录的大小往往比主键要大的多。这样就会导致聚簇索引在判定新记录携带的主键是否重复时进行昂贵的I/O代价。 + +最后,说明一下,如果你很好的理解了索引的原理,这上面的就会很好理解,如果理解不到位,就会发现这都是什么东西?因此,看这个的时候,先看一下上面那一题的解答。 + +> 覆盖索引 + +覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。 + +举个例子 + +```sql +select * from user where name = "sihai"; +``` +以上语句查询是会从数据表中进行查询的,因为,没有对user表中的name字段增加索引的操作。 + +```sql +alter table user add index name_index(name); +``` +我们对user表中的name字段添加了索引。我们再使用sql语句`select * from user where name = "sihai";`进行查询是,就会使用覆盖索引。 + +因此,我们就可以非常清楚的明白,覆盖索引就是如果发现可以走索引的方式得到数据,就不采用回表查询的操作了,从而提高了查询的效率。 + +另外,从上面的索引原理的介绍也可以得到另外一个结论:使用覆盖索引InnoDB引擎比MyISAM引擎效果更佳,原因在于InnoDB采用聚集索引,如果二级索引中包含查询所需的数据,就不再需要在聚集索引中查找了。 + +最后一点,想要使用覆盖索引,就必须要使得查询能够用到索引,因此,也需要注意索引失效的场景。 + +> 建立索引的原则 + +- 最左前缀匹配原则,MySQL会遇到范围查询停止匹配,所以会导致组合索引失效。 +- 索引列不进行函数运算。 +- 注意一个表建立索引的数量,不是索引建的越多越好,维护索引也会有很大的开销。 +- 尽量选择区分度高的字段作为索引。 +- where语句中,经常使用的字段应该考虑建立索引。 +- 分组和排序语句中,经常使用的字段应该考虑建立索引。 +- 两个表关联字段考虑建立索引。 +- like模糊查询中,只有右模糊查询才会使用索引。 +- 在varchar 字段上建立索引时,必须指定索引长度。 +- 禁止建立超过3个字段的联合索引。 +- 尽量采用覆盖索引,避免回表查询。 +- 索引优化的目标:至少要达到range级别,要求是ref级别,如果可以是consts最好。 + +> 索引失效的情况 + +- 使用组合索引,没有满足最左匹配原则,导致失效。 +- or语句所有字段必须都有索引,否则失效。 +- like以%开头,索引失效。 +- 需要类型转换。 +- where中索引列有运算。 +- where中索引列使用了函数。 +- 如果mysql觉得全表扫描更快时(数据少)。 + +### 其他问题 + +> 一张有自增id的表,当数据记录到了20之后,删除了第18,19,20条记录,再把MySQL重启,再插入一条记录,这条记录的id是21还是18呢? + +当表的引擎采用MyISAM时,是21,当表的引擎采用InnoDB时是18。 + +MyISAM引擎会把表自增主键的最大id记录到数据文件中,做了持久化,重启后也不会消失,而InnoDB是将自增主键的最大id记录在内存中,重启后,会丢失。 + +> 关系型数据库和非关系型数据库的区别 + +非关系型数据库的优势在于性能和可扩展性,非关系型数据库一般都是基于键值对的,当然也支持文档形式、图片形式等等,文档形式、图片形式等等,使用灵活,应用场景广泛,同时,也是基于内存进行相关的操作,同时,底层也有较好的数据结构的支持,保证了其操作的效率,性能较高;另外,同样也是因为基于键值对,数据之间没有耦合性,所以非常容易水平扩展。成本低:非关系型数据库部署简单,基本都是开源软件。 + + +关系型数据库的优势在于支持更加复杂的sql操作、事务支持和使用表结构更加易于维护。 + + + diff --git "a/docs/database/\346\225\260\346\215\256\345\272\223\344\274\230\345\214\226.md" "b/docs/database/\346\225\260\346\215\256\345\272\223\344\274\230\345\214\226.md" new file mode 100644 index 0000000..32cea00 --- /dev/null +++ "b/docs/database/\346\225\260\346\215\256\345\272\223\344\274\230\345\214\226.md" @@ -0,0 +1,12 @@ +## 数据库优化 + +### 为什么要做优化 + + +### 从哪几个方面考虑优化 + + +### MySQL语句优化 + + +### 其他 \ No newline at end of file diff --git "a/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" "b/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" new file mode 100644 index 0000000..ce68d03 --- /dev/null +++ "b/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" @@ -0,0 +1,588 @@ +> 简单的说一下Java的垃圾回收机制,解决了什么问题? + +这个题目其实我是不太想写的,原因在于太笼统了,容易让面试者陷入误区,难以正确的回答,但是,如何加上一个解决了什么问题,那么,这个面试题还是有意义的,可以看看面试者的理解。 + +在c语言和c++中,对于内存的管理是非常的让人头疼的,也是很多人放弃的原因,因此,在后面的一些高级语言中,设计者就想解决这一个问题,让开发者专注于自己的业务开发即可,因此,在Java中也引入了垃圾回收机制,它使得开发者在编写程序的时候不再需要考虑内存管理,由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。 + +> 了解JVM的内存模型吗? + +JVM载执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。 + +Java 虚拟机所管理的内存一共分为Method Area(方法区)、VM Stack(虚拟机栈)、Native Method Stack(本地方法栈)、Heap(堆)、Program Counter Register(程序计数器)五个区域。 + +这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。具体如下图所示: + +![](http://image.ouyangsihai.cn/FhxTjHOieWt_ugomW5L33YNkJlJQ) + +上图介绍的是JDK1.8 JVM运行时内存数据区域划分。1.8同1.7比,最大的差别就是:**元数据区取代了永久代**。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:**元数据空间并不在虚拟机中,而是使用本地内存**。 + + +### 1 程序计数器(Program Counter Register) + +**程序计数器(Program Counter Register)**是一块较小的内存空间,可以看作是当前线程所执行的字节码的**行号指示器**。在虚拟机概念模型中,**字节码解释器**工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 + +程序计数器是一块 **“线程私有”** 的内存,每条线程都有一个独立的程序计数器,能够将切换后的线程恢复到正确的执行位置。 + +- 执行的是一个**Java方法** + +计数器记录的是正在执行的**虚拟机字节码指令的地址**。 + +- 执行的是**Native方法** + +**计数器为空(Undefined)**,因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。 + +- 程序计数器也是唯一一个在Java虚拟机规范中没有规定任何**OutOfMemoryError**情况的内存区域。 + +其实,我感觉这块区域,作为我们开发人员来说是不能过多的干预的,我们只需要了解有这个区域的存在就可以,并且也没有虚拟机相应的参数可以进行设置及控制。 + +### 2 Java虚拟机栈(Java Virtual Machine Stacks) + +![](http://image.ouyangsihai.cn/FksaMoFlAkSPkTB84bV4cK7xa8L3) + + +**Java虚拟机栈(Java Virtual Machine Stacks)**描述的是**Java方法执行的内存模型**:每个方法在执行的同时都会创建一个**栈帧(Stack Frame)**,从上图中可以看出,栈帧中存储着**局部变量表**、**操作数栈**、**动态链接**、**方法出口**等信息。每一个方法从调用直至执行完成的过程,会对应一个栈帧在虚拟机栈中入栈到出栈的过程。 + +与程序计数器一样,Java虚拟机栈也是**线程私有**的。 + +而**局部变量表**中存放了编译期可知的各种: + +* **基本数据类型**(boolen、byte、char、short、int、 float、 long、double) +* **对象引用**(reference类型,它不等于对象本身,可能是一个指向对象起始地址的指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置) +* **returnAddress类型**(指向了一条字节码指令的地址) + +其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余数据类型只占用1个。**局部变量表所需的内存空间在编译期间完成分配**,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 + +Java虚拟机规范中对这个区域规定了两种异常状况: + +* **StackOverflowError**:线程请求的栈深度大于虚拟机所允许的深度,将会抛出此异常。 +* **OutOfMemoryError**:当可动态扩展的虚拟机栈在扩展时无法申请到足够的内存,就会抛出该异常。 + +一直觉得上面的概念性的知识还是比较抽象的,下面我们通过JVM参数的方式来控制栈的内存容量,模拟StackOverflowError异常现象。 + + +### 3 本地方法栈(Native Method Stack) + +**本地方法栈(Native Method Stack)** 与Java虚拟机栈作用很相似,它们的区别在于虚拟机栈为虚拟机执行Java方法(即字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 + +在虚拟机规范中对本地方法栈中使用的语言、方式和数据结构并无强制规定,因此具体的虚拟机可实现它。甚至**有的虚拟机(Sun HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一**。与虚拟机一样,本地方法栈会抛出**StackOverflowError**和**OutOfMemoryError**异常。 + +- 使用-Xss参数减少栈内存容量(更多的JVM参数可以参考这篇文章:[深入理解Java虚拟机-常用vm参数分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-yong-vm-can-shu-fen-xi.html)) + +这个例子中,我们将栈内存的容量设置为`256K`(默认1M),并且再定义一个变量查看栈递归的深度。 + +```java +/** + * @ClassName Test_02 + * @Description 设置Jvm参数:-Xss256k + * @Author 欧阳思海 + * @Date 2019/9/30 11:05 + * @Version 1.0 + **/ +public class Test_02 { + + private int len = 1; + + public void stackTest() { + len++; + System.out.println("stack len:" + len); + stackTest(); + } + + public static void main(String[] args) { + Test_02 test = new Test_02(); + try { + test.stackTest(); + } catch (Throwable e) { + e.printStackTrace(); + } + } +} + +``` +运行时设置JVM参数 + +![](http://image.ouyangsihai.cn/Fp8Z9xGi-AN7k7laSOHupU7htMg9) + +输出结果: + +![](http://image.ouyangsihai.cn/FuHgFTcCaWlFjEtorqUbRF3RI_Cx) + + + +### 4 Java堆(Heap) + + + +对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 + + +Java堆是垃圾收集器管理的主要区域,因此也被称为 **“GC堆(Garbage Collected Heap)”** 。从内存回收的角度看内存空间可如下划分: + +![图片摘自https://blog.csdn.net/bruce128/article/details/79357870](http://image.ouyangsihai.cn/FvwbMlmR_k5r4xwnpH5LXDN-4qok) + + +* **新生代(Young)**: 新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低。在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。 + +如果把新生代再分的细致一点,新生代又可细分为**Eden空间**、**From Survivor空间**、**To Survivor空间**,默认比例为8:1:1。 + +* **老年代(Tenured/Old)**:在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。 +* **永久代(Perm)**:永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。 + + +其中**新生代和老年代组成了Java堆的全部内存区域**,而**永久代不属于堆空间,它在JDK 1.8以前被Sun HotSpot虚拟机用作方法区的实现** + +另外,再强调一下堆空间内存分配的大体情况,这对于后面一些Jvm优化的技巧还是有帮助的。 + +- 老年代 : 三分之二的堆空间 +- 年轻代 : 三分之一的堆空间 + eden区: 8/10 的年轻代空间 + survivor0 : 1/10 的年轻代空间 + survivor1 : 1/10 的年轻代空间 + +最后,我们再通过一个简单的例子更加形象化的展示一下**堆溢出**的情况。 + +- JVM参数设置:-Xms10m -Xmx10m + +这里将堆的最小值和最大值都设置为10m,如果不了解这些参数的含义,可以参考这篇文章:[深入理解Java虚拟机-常用vm参数分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-yong-vm-can-shu-fen-xi.html) + +```java +/** + * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError + * @author zzm + */ +public class HeapTest { + + static class HeapObject { + } + + public static void main(String[] args) { + List list = new ArrayList(); + + //不断的向堆中添加对象 + while (true) { + list.add(new HeapObject()); + } + } +} + +``` + +输出结果: +![](http://image.ouyangsihai.cn/Fhky14SMLxHjx9R9ZCcY0jAJ8ljg) + +图中出现了`java.lang.OutOfMemoryError`,并且提示了`Java heap space`,这就说明是Java堆内存溢出的情况。 + +**堆的Dump文件分析** + +我的使用的是VisualVM工具进行分析,关于如何使用这个工具查看这篇文章([深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析 ](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html))。在运行程序之后,会同时打开VisualVM工具,查看堆内存的变化情况。 + +![](http://image.ouyangsihai.cn/Fhdj0VggJwgP-qAOdWBBrWmO5XrM) + +在上图中,可以看到,堆的最大值是30m,但是使用的堆的容量也快接近30m了,所以很容易发生堆内存溢出的情况。 + +接着查看dump文件。 + +![](http://image.ouyangsihai.cn/FpYV2YbCGR3ByPy3vFNJbVNpoLdW) + +如上图,堆中的大部分的对象都是HeapObject,所以,就是因为这个对象的一直产生,所以导致堆内存不够分配,所以出现内存溢出。 + +我们再看GC情况。 + +![](http://image.ouyangsihai.cn/FpB0KxmFAtZOx7g95dlfyAp8mLEV) + +如上图,Eden新生代总共48次minor gc,耗时1.168s,基本满足要求,但是survivor却没有,这不正常,同时Old Gen老年代总共27次full gc,耗时4.266s,耗时长,gc多,这正是因为大量的大对象进入到老年代导致的,所以,导致full gc频繁。 + +### 5 方法区(Method Area) + +**方法区(Method Area)** 与Java堆一样,是各个线程共享的内存区域。它用于存储一杯`虚拟机加载`的**类信息、常量、静态变量、及时编译器编译后的代码**等数据。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 **Non-Heap**。 + + + +**运行时常量池(Runtime Constant Pool)** + +**运行时常量池(Runtime Constant Pool)**是方法区的一部分。**Class文件**中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是**常量池(Constant Pool Table)**,用于存放编译期生成的各种字面量和符号引用,**这部分内容将在类加载后进入方法区的运行时常量池存放**。 + +Java虚拟机对Class文件每一部分(自然包括常量池)的格式有严格规定,每一个字节用于存储那种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行。但**对于运行时常量池,Java虚拟机规范没有做任何有关细节的要求**,不同的提供商实现的虚拟机可以按照自己的需求来实现此内存区域。不过一般而言,除了保存**Class文件中的描述符号引用**外,还会把**翻译出的直接引用**也存储在运行时常量池中。 + +运行时常量池相对于Class文件常量池的另外一个重要特征是具备**动态性**,Java语言并不要求常量一定只有编译器才能产生,也就是**并非置入Class文件中的常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中**。 + +#### 运行时常量池举例 + +上面的**动态性**在开发中用的比较多的便是String类的`intern()` 方法。所以,我们以`intern()` 方法举例,讲解一下**运行时常量池**。 + +`String.intern()`是一个`native`方法,作用是:如果字符串常量池中已经包含有一个等于此String对象的字符串,则直接返回池中的字符串;否则,加入到池中,并返回。 + +```java +/** + * @ClassName MethodTest + * @Description vm参数设置:-Xms512m -Xmx512m -Xmn128m -XX:PermSize=10M -XX:MaxPermSize=10M -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:-HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:+UseConcMarkSweepGC + * @Author 欧阳思海 + * @Date 2019/11/25 20:06 + * @Version 1.0 + **/ + +public class MethodTest { + + public static void main(String[] args) { + List list = new ArrayList(); + long i = 0; + while (i < 1000000000) { + System.out.println(i); + list.add(String.valueOf(i++).intern()); + } + } +} +``` + +vm参数介绍: +>-Xms512m -Xmx512m -Xmn128m -XX:PermSize=10M -XX:MaxPermSize=10M -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:-HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:+UseConcMarkSweepGC +开始堆内存和最大堆内存都是512m,永久代大小10m,新生代和老年代1:4,E:S1:S2=8:1:1,最大经过15次survivor进入老年代,使用的,垃圾收集器是新生代ParNew,老年代CMS。 + +通过这样的设置之后,查看运行结果: +![](http://image.ouyangsihai.cn/FlpAIczI0f-h26J5QGYXdUkW2hbw) + +首先堆内存耗完,然后看看GC情况,设置这些参数之后,GC情况应该会不错,拭目以待。 + +![](http://image.ouyangsihai.cn/FvkHjwUTJcMK0j7KvrgLVWJOHODY) + +上图是GC情况,我们可以看到**新生代** 21 次minor gc,用了1.179秒,平均不到50ms一次,性能不错,**老年代** 117 次full gc,用了45.308s,平均一次不到1s,性能也不错,说明jvm运行是不错的。 + +>**注意:** 在JDK1.6及以前的版本中运行以上代码,因为我们通过`-XX:PermSize=10M -XX:MaxPermSize=10M`设置了方法区的大小,所以也就是设置了常量池的容量,所以运行之后,会报错:`java.lang.OutOfMemoryError:PermGen space`,这说明常量池溢出;在JDK1.7及以后的版本中,将会一直运行下去,不会报错,在前面也说到,JDK1.7及以后,去掉了永久代。 + +### 6 直接内存 + +**直接内存(Direct Memory)**并不是虚拟机**运行时数据区**的一部分,也不是Java虚拟机规范中定义的内存区域。但这部分内存也被频繁运用,而却可能导致**OutOfMemoryError**异常出现。 + +这个我们实际中主要接触到的就是NIO,在NIO中,我们为了能够加快IO操作,采用了一种直接内存的方式,使得相比于传统的IO快了很多。 + +在NIO引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配**堆外内存**,然后通过一个存储在Java堆中的`DirectByteBuffer`对象作为这块内存的引用进行操作。这样能避免在Java堆和Native堆中来回复制数据,在一些场景里显著提高性能。 + +在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统的限制),从而导致动态扩展时出现**OutOfMemoryError**异常。 + +> JVM的四种引用? + +Java把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。 + +- 强引用 + +以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 + +```java + String str = "sihai"; + List list = new Arraylist(); + list.add(str); +``` +以上就是一个强引用的例子,及时内存不足,该对象也不会被回收。 + +- 软引用 + +如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 + +软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。 + +当内存足够大时可以把数组存入软引用,取数据时就可从内存里取数据,提高运行效率。 + +- 弱引用 + +如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:**只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存**。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 + +弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 + +- 虚引用 + +如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:**虚引用必须和引用队列(ReferenceQueue)联合使用。** + +当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 + +**注意:** 在实际程序设计中一般很少使用弱引用与虚引用,使用软用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。 + +> GC用的可达性分析算法中,哪些对象可以作为GC Roots对象? + +- 虚拟机栈(栈帧中的局部变量表,Local Variable Table)中引用的对象。 +- 方法区中类静态属性引用的对象。 +- 方法区中常量引用的对象。 +- 本地方法栈中JNI(即一般说的Native方法)引用的对象。 + +> 如何判断对象是不是垃圾? + +#### 引用计数算法 + +先讲讲第一个算法:**引用计数算法**。 + +其实,这个算法的思想非常的简单,一句话就是:**给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。** + +这些简单的算法现在是否还被大量的使用呢,其实,现在用的已经不多,没有被使用的最主要的原因是他有一个很大的**缺点**:**很难解决对象之间循环引用的问题**。 + +**循环引用**:当A有B的引用,B又有A的引用的时候,这个时候,即使A和B对象都为null,这个时候,引用计数算法也不会将他们进行垃圾回收。 + +```java +/** + * @ClassName Test_02 + * @Description + * @Author 欧阳思海 + * @Date 2019/12/5 16:59 + * @Version 1.0 + **/ +public class Test_02 { + + public static void main(String[] args) { + Instance instanceA = new Instance(); + Instance instanceB = new Instance(); + + instanceA.instance = instanceB; + instanceB.instance = instanceA; + + instanceA = null; + instanceB = null; + + System.gc(); + + Scanner scanner = new Scanner(System.in); + scanner.next(); + } +} + +class Instance{ + public Object instance = null; +} + +``` + +如果使用的是**引用计数算法**,这是不能被回收的,当然,现在的JVM是可以被回收的。 + +#### 可达性分析算法 + +这个算法的思想也是很简单的,这里有一个概念叫做**可达性分析**,如果知道图的数据结构,这里可以把每一个对象当做图中的一个节点,我们把一个节点叫做**GC Roots**,如果一个节点到**GC Roots**没有任何的相连的路径,那么就说明这个节点不可达,也就是这个节点可以被回收。 + +![](http://image.ouyangsihai.cn/FqyjRBThJ5HhXAIEH4UE7znJhOuk) + +上面图中,虽然obj7、8、9相互引用,但是到GC Roots不可达,所以,这种对象也是会被当做垃圾收集的。 + +在Java中,可以作为`GC Roots`的对象包括以下几种: + +- 虚拟机栈(栈帧中的局部变量表,Local Variable Table)中引用的对象。 +- 方法区中类静态属性引用的对象。 +- 方法区中常量引用的对象。 +- 本地方法栈中JNI(即一般说的Native方法)引用的对象。 + +> 介绍一下JVM的堆 + +这个面试题其实和上面的有点重合,但是,这里单独拿出来再介绍一下,因为这个确实是比较常见的,这样大家也都有印象。 + +对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 + +Java堆是垃圾收集器管理的主要区域,因此也被称为 **“GC堆(Garbage Collected Heap)”** 。从内存回收的角度看内存空间可如下划分: + +![图片摘自https://blog.csdn.net/bruce128/article/details/79357870](http://image.ouyangsihai.cn/FvwbMlmR_k5r4xwnpH5LXDN-4qok) + + +* **新生代(Young)**: 新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低。在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。 + +如果把新生代再分的细致一点,新生代又可细分为**Eden空间**、**From Survivor空间**、**To Survivor空间**,默认比例为8:1:1。 + +* **老年代(Tenured/Old)**:在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。 +* **永久代(Perm)**:永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。 + + +其中**新生代和老年代组成了Java堆的全部内存区域**,而**永久代不属于堆空间,它在JDK 1.8以前被Sun HotSpot虚拟机用作方法区的实现** + +另外,再强调一下堆空间内存分配的大体情况,这对于后面一些Jvm优化的技巧还是有帮助的。 + +- 老年代 : 三分之二的堆空间 +- 年轻代 : 三分之一的堆空间 + eden区: 8/10 的年轻代空间 + survivor0 : 1/10 的年轻代空间 + survivor1 : 1/10 的年轻代空间 + +最后,我们再通过一个简单的例子更加形象化的展示一下**堆溢出**的情况。 + +- JVM参数设置:-Xms10m -Xmx10m + +这里将堆的最小值和最大值都设置为10m,如果不了解这些参数的含义,可以参考这篇文章:[深入理解Java虚拟机-常用vm参数分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-yong-vm-can-shu-fen-xi.html) + +```java +/** + * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError + * @author zzm + */ +public class HeapTest { + + static class HeapObject { + } + + public static void main(String[] args) { + List list = new ArrayList(); + + //不断的向堆中添加对象 + while (true) { + list.add(new HeapObject()); + } + } +} + +``` + +输出结果: +![](http://image.ouyangsihai.cn/Fhky14SMLxHjx9R9ZCcY0jAJ8ljg) + +图中出现了`java.lang.OutOfMemoryError`,并且提示了`Java heap space`,这就说明是Java堆内存溢出的情况。 + +> 介绍一下Minor GC和Full GC + +这个概念首先我们要了解JVM的内存分区,在上面的面试题中已经做了介绍,这里就不再介绍了。 + +新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。 + +老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。 + +**内存分配规则** + +- 对象优先分配到Eden区,如果Eden区空间不够,则执行一次minor GC; +- 大对象直接进入到老年代; +- 长期存活的对象可以进入到老年代; +- 动态判断对象年龄。如果在Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象直接进入到老年代。 + +在这里更加详细的规则介绍可以参考这篇文章:[深入理解Java虚拟机-JVM内存分配与回收策略原理,从此告别JVM内存分配文盲](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-jvm-nei-cun-fen-pei-yu-hui-shou-ce-lue-yuan-li-cong-ci-gao-bie-jvm-nei-cun-fen-pei-wen-mang.html) + +> 说一下Java对象创建的方法 + +这个问题其实很简单,但是很多人却只知道new的方式。 + +- new的方法,最常见; +- 调用对象的clone方法; +- 使用反射,Class.forName(); +- 运用反序列化机制,java.io.ObjectInputStream对象的readObject()方法。 + +> 介绍一下几种垃圾收集算法? + +#### 标记-清除(Mark-Sweep)算法 + +**标记-清除(Mark-Sweep)** 算法是最基础的垃圾收集算法,后续的收集算法都是基于它的思路并对其不足进行改进而得到的。顾名思义,算法分成“标记”、“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,标记过程在前一节讲述对象标记判定时已经讲过了。 + +标记-清除算法的不足主要有以下两点: + +* **空间问题**,标记清除之后会产生大量不连续的**内存碎片**,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不触发另一次垃圾收集动作。 +* **效率问题**,因为内存碎片的存在,操作会变得更加费时,因为查找下一个可用空闲块已不再是一个简单操作。 + +标记-清除算法的执行过程如下图所示: + +![](http://image.ouyangsihai.cn/Fn_nJ2vQKuX47-8rBn7IZrV5LfoH) + + +#### 复制(Copying)算法 + +为了解决标记-清除算法的效率问题,一种称为**“复制”(Copying)**的收集算法出现了,思想为:它**将可用内存按容量分成大小相等的两块**,每次只使用其中的一块。**当这一块内存用完,就将还存活着的对象复制到另一块上面**,然后再把已使用过的内存空间一次清理掉。 + +这样做使得**每次都是对整个半区进行内存回收**,内存分配时也就**不用考虑内存碎片**等复杂情况,只要**移动堆顶指针,按顺序分配内存**即可,实现简单,运行高效。只是这种算法的代价是**将内存缩小为原来的一半**,代价可能过高了。复制算法的执行过程如下图所示: + +![](http://image.ouyangsihai.cn/FoyNQk9dft20afZSCIzC7oVoJIHQ) + + + +##### 标记-整理(Mark-Compact)算法 + +复制算法在对象存活率较高时要进行较多的复制操作,效率将会变低。更关键的是:如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在**老年代一般不能直接选用复制算法**。 + +根据老年代的特点,**标记-整理(Mark-Compact)**算法被提出来,主要思想为:此算法的标记过程与**标记-清除**算法一样,但后续步骤不是直接对可回收对象进行清理,而是**让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。** 具体示意图如下所示: + +![](http://image.ouyangsihai.cn/Fov_rN7qL6R_DbGUNChUUOC4lqHS) + + +##### 分代收集(Generational Collection)算法 + +当前商业虚拟机的垃圾收集都采用**分代收集(Generational Collection)算法**,此算法相较于前几种没有什么新的特征,主要思想为:根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法: + +* **新生代** 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用**复制算法**,只需要付出少量存活对象的复制成本就可以完成收集。 + +* **老年代** 在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用**标记-清除**或**标记-整理**算法来进行回收。 + +> 如何减少gc出现的次数 + +上面的面试题已经讲解到了,从年轻代空间(包括Eden和 Survivor 区域)回收内存被称为Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。Major GC的速度一般会比Minor GC慢10倍以上。 + +GC会stop the world。会暂停程序的执行,带来延迟的代价。所以在开发中,我们不希望GC的次数过多。 + +- 对象不用时最好显式置为 Null + +一般而言,为 Null 的对象都会被作为垃圾处理,所以将不用的对象显式地设为 Null,有利于 GC 收集器判定垃圾,从而提高了 GC 的效率。 +- 尽量少用 System.gc() + +此函数建议 JVM 进行主 GC,虽然只是建议而非一定,但很多情况下它会触发主 GC,从而增加主 GC 的频率,也即增加了间歇性停顿的次数。 +- 尽量少用静态变量 + +静态变量属于全局变量,不会被 GC 回收,它们会一直占用内存。 +- 尽量使用 StringBuffer,而不用 String 来累加字符串 + +由于 String 是固定长的字符串对象。累加 String 对象时,并非在一个 String对象中扩增,而是重新创建新的 String 对象,如 Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的 String 对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。 避免这种情况可以改用 StringBuffer 来累加字符串,因 StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象 +- 分散对象创建或删除的时间 + +集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM 在面临这种情况时,只能进行主 GC,以回收内存或整合内存碎片,从而增加主 GC 的频率。 + +集中删除对象,道理也是一样的。 它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主 GC 的机会。 +- 尽量少用 finalize 函数 + +因为它会加大 GC 的工作量,因此尽量少用finalize 方式回收资源。 +- 使用软引用类型 + +如果需要使用经常用到的图片,可以使用软引用类型,它可以尽可能将图片保存在内存中,供程序调用,而不引起 OutOfMemory。 + +- 尽量少用 finalize 函数。 + +因为它会加大 GC 的工作量,因此尽量少用finalize 方式回收资源。 + +如果需要使用经常用到的图片,可以使用软引用类型,它可以尽可能将图片保存在内存中,供程序调用,而不引起 OutOfMemory。 + +- 能用基本类型如 int,long,就不用 Integer,Long 对象 + +基本类型变量占用的内存资源比相应包装类对象占用的少得多,如果没有必要,最好使用基本变量。 + +- 增大-Xmx + +- 老年代代空间不足 + +老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: +```java +java.lang.OutOfMemoryError: Java heap space +``` +为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 + +- 永生区空间不足 + +JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,`Permanet Generation`中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,`Permanet Generation`可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息: +```java +java.lang.OutOfMemoryError: PermGen space +``` +为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。 + +- CMS GC时出现`promotion failed`和`concurrent mode failure` + +对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有`promotion failed`和`concurrent mode failure`两种状况,当这两种状况出现时可能会触发Full GC。 + +promotion failed是在进行Minor GC时,`survivor space`放不下、对象只能放入老年代,而此时老年代也放不下造成的;`concurrent mode failure`是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。 + +对措施为:增大survivor space、老年代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置`-XX: CMSMaxAbortablePrecleanTime=5`(单位为ms)来避免。 + +- 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间 + +这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。 + +例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。 + +当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。 + +除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过`java -Dsun.rmi.dgc.client.gcInterval=3600000`来设置Full GC执行的间隔时间或通过`-XX:+ DisableExplicitGC`来禁止RMI调用`System.gc`。 + +- 堆中分配很大的对象 + +所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。 + +为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即`-XX:+UseCMSCompactAtFullCollection`开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 `-XX:CMSFullGCsBeforeCompaction`,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。 + +> 数组多大会放在JVM老年代,永久代对象如何GC?如果想不被GC怎么办?如果想在GC中生存一次怎么办? + +虚拟机提供了一个`-XX:PretenureSizeThreshold`参数(通常是3MB),令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。(新生代采用复制算法收集内存) + +垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完成垃圾回收(Full GC)。如果仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。 + +让对象实现finalize()方法,一次对象的自我拯救。 + diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234-\345\205\266\344\273\226\347\233\270\345\205\263\351\235\242\350\257\225\351\227\256\351\242\230.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234-\345\205\266\344\273\226\347\233\270\345\205\263\351\235\242\350\257\225\351\227\256\351\242\230.md" new file mode 100644 index 0000000..4282a2b --- /dev/null +++ "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234-\345\205\266\344\273\226\347\233\270\345\205\263\351\235\242\350\257\225\351\227\256\351\242\230.md" @@ -0,0 +1,169 @@ + +> 攻击网站的方法有哪些? + +**一是**,**DDOS攻击(分布式拒绝服务)**:利用攻击软件通过大量的机器同时对服务进行攻击,规模大,危害大。 + +目前有两种主流的DDOS攻击:**SYN Flood攻击**和**TCP全连接攻击**。 + +> - SYN Flood攻击:利用tcp协议的缺陷,发送大量伪造的tcp连接请求,导致被攻击方资源耗尽。出现的原因是:在TCP三次握手过程中,假设用户发送了SYN报文后掉线,那么服务器在发出SYN+ACK是无法收到客户端的ACK报文,这时,服务端是会一直不断的重试并等待一段时间后丢弃这个未完成的连接的,如果有大量的伪造的攻击报文,发送到了服务端,服务端将为了维护一个非常大的半连接队列而消耗过多的CPU时间和内存,这就会导致服务器失去响应。 +> - TCP全连接攻击:这种攻击是为了绕过防火墙设计的,它能够绕过防火墙,导致服务器有大量的TCP连接,最终导致服务器拒绝服务。 + +**解决SYN Flood攻击的方法** + +- 设置SYN timeout时间:SYN timeout时间是指服务端一直不断的重试并等待的这段时间。 +- 设置SYN cookie,判断是否连续收到某个ip的重复SYN报文,如果是,以后就丢弃这个ip地址的包 +- 设置SYN cache,先不分配系统资源,现用cache保存半开连接,直到收到正确的ACK在分配资源 +- 硬件防火墙 + +**解决TCP全连接攻击的方法** + +- 限制SYN流量 +- 定期扫描缺陷 +- 在骨干节点设置防火墙 +- 有足够的机器可以承受攻击 +- 过滤不必要的服务和端口 + +另外,**Land攻击**,该攻击是利用了TCP三次握手时,通过向一个主机发送一个用于建立连接的TCP SYN报文而实现对目标主机的攻击,这种方式与正常的TCP SYN报文不同的是:Land攻击的报文源ip地址和目标ip地址是一样的,都是目标主机的ip地址。 + +由于目标IP地址和源IP地址是一样的,因此,ACK报文就发给了主机本身,如果大量的SYN报文进行发送,目标计算机也不能正常工作。 + +**二是**,**XSS攻击(跨站脚本攻击)**,它是指恶意攻击者在web网页中插入恶意html代码,当用户浏览网页时,嵌入其中的html代码会执行,从而达到恶意攻击的目的。 + +**三是**,**CSRF攻击(跨站请求伪造)**,攻击者盗用了你的身份,以你的名义发送恶意请求,一般是在你登录了A网站以后,携带A网站的信息,然后请求危险B网站,从而导致。 + +对于**CSRF攻击(跨站请求伪造)** 可以使用下列方法进行防御: +- 用户关闭页面要及时清理cookie +- 在url后面添加伪随机数 +- 图片验证码等 + +**四是**,**SQL注入**,SQL注入是一种将SQL代码添加到输入参数中,传递到服务器解析并执行的一种攻击手法。 + +**SQL注入攻击**是输入参数未经过滤,然后直接拼接到SQL语句当中解析,执行达到预想之外的一种行为,称之为SQL注入攻击。 + +常见的sql注入,有下列几种: +- 数字注入 + +例如,查询的sql语句为:`SELECT * FROM user WHERE id = 1`,正常是没有问题的,如果我们进行sql注入,写成`SELECT * FROM user WHERE id = 1 or 1=1`,那么,这个语句永远都是成立的,这就有了问题,也就是sql注入。 + +- 字符串注入 + +字符串注入是因为注释的原因,导致sql错误的被执行,例如字符`#`、`--`。 + +例如,`SELECT * FROM user WHERE username = 'sihai'#'ADN password = '123456'`,这个sql语句'#'后面都被注释掉了,相当于`SELECT * FROM user WHERE username = 'sihai' `。 + +这种情况我们在mybatis中也是会存在的,所以在服务端写sql时,需要特别注意此类情况。 + +该如何防范此类问题呢? +- 严格检查输入变量的类型和格式,也就是对相关传入的参数进行验证,尽可能降低风险。 +- 过滤和转义特殊字符。 +- 利用mysql的预编译机制,在Java中mybatis也是有预编译的方法的,所以可以采用这种方式避免。 + +> mybatis中的 # 与 $ 区别? + +这个问题在面试中时常可能被问到,其实,面试官可能一想考考你对mybatis的熟悉程度,二是,想考考你对sql注入的理解。 + +我们都知道,mybatis中的动态sql经常会用到这两个符号。 + +在动态 SQL 解析阶段, #{ } 和 ${ } 会有不同的表现: +- #{ } 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符。 +- ${ } 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换。 + +例如,`select * from user where name = #{name};`会解析为`select * from user where name = ?;`,而`select * from user where name = ${name};`如果我们传递参数"sihai"会解析为`select * from user where name = "sihai"`。 + +因此,**${ } 的变量的替换阶段是在动态 SQL 解析阶段,而 #{ }的变量的替换是在 DBMS 中**。 + +由上也可得,**尽量使用`#{ }`,可以防止sql注入,除非表名作为变量时,才使用`${ }`。** + + +> session的原理 + +Session是一种可以存储在内存、文件或者数据库中的键值对。 + +在程序需要为客户端创建一个请求的session时,服务器会先检查客户端的请求里是否包含一个session的标识,这个标识通常称为session id,如果已经存在,说明已经创建了session,就可以直接使用;如果不存在,则需要服务端重新创建一个session。 + +浏览器存储session的方式有三种: +- 使用Cookie存储,这是常见的方式,”记住我“的功能就是这种方式实现的。 +- Url重写,这种方式是直接把session id附加在url路径的后面,例如:www.baidu.com?sessionid=xxx。 +- 在页面表单中增加隐藏域。 + +#### session什么时候创建的呢 + +这个其实很简单,一般都是服务端在需要的时候使用某种方式进行创建,而不同语言的创建方式都是不一样的。 + +#### session什么时候删除? + +删除的时机很难说,但你不需要的时候就可以调用服务端的相关api进行删除,例如,注销登录时,我们就可以对用户session进行删除。 + +> Cookie的相关原理 + +在程序中,会话跟踪是很重要的事情。理论上,一个用户的所有请求操作都应该属于同一个会话,而另一个用户的所有请求操作则应该属于另一个会话,二者不能混淆。例如,用户A在超市购买的任何商品都应该放在A的购物车内,不论是用户A什么时间购买的,这都是属于同一个会话的,不能放入用户B或用户C的购物车内,这不属于同一个会话。 + +![](http://image.ouyangsihai.cn/Fv-jlc7N4TgtooXwIwhIdQUW0s9E) + +而Web应用程序是使用HTTP协议传输数据的。HTTP协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接。这就意味着服务器无法从连接上跟踪会话。即用户A购买了一件商品放入购物车内,当再次购买商品时服务器已经无法判断该购买行为是属于用户A的会话还是用户B的会话了。要跟踪该会话,必须引入一种机制。 + +Cookie就是这样的一种机制。它可以弥补HTTP协议无状态的不足。在Session出现之前,基本上所有的网站都采用Cookie来跟踪会话。 + +#### Cookie属性项 + +|属性名 |描述| +|-----|-----| +|String name|该Cookie的名称。Cookie一旦创建,名称便不可更改 +|Object value|该Cookie的值。如果值为Unicode字符,需要为字符编码。如果值为二进制数据,则需要使用BASE64编码 +|int maxAge|该Cookie失效的时间,单位秒。如果为正数,则该Cookie在maxAge秒之后失效。如果为负数,该Cookie为临时Cookie,关闭浏览器即失效,浏览器也不会以任何形式保存该Cookie。如果为0,表示删除该Cookie。默认为–1 +|boolean secure|该Cookie是否仅被使用安全协议传输。安全协议。安全协议有HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false +|String path|该Cookie的使用路径。如果设置为“/sessionWeb/”,则只有contextPath为“/sessionWeb”的程序可以访问该Cookie。如果设置为“/”,则本域名下contextPath都可以访问该Cookie。注意最后一个字符必须为“/” +|String domain|可以访问该Cookie的域名。如果设置为“.google.com”,则所有以“google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为“.” +|String comment|该Cookie的用处说明。浏览器显示Cookie信息的时候显示该说明 +|int version|该Cookie使用的版本号。0表示遵循Netscape的Cookie规范,1表示遵循W3C的RFC 2109规范 + +#### Cookie的有效期 + +Cookie的maxAge决定着Cookie的有效期,单位为秒(Second)。 + +- 如果maxAge属性为正数,则表示该Cookie会在maxAge秒之后自动失效。浏览器会将maxAge为正数的Cookie持久化,即写到对应的Cookie文件中。无论客户关闭了浏览器还是电脑,只要还在maxAge秒之前,登录网站时该Cookie仍然有效。 +- 如果maxAge为负数,则表示该Cookie仅在本浏览器窗口以及本窗口打开的子窗口内有效,关闭窗口后该Cookie即失效。maxAge为负数的Cookie,为临时性Cookie,不会被持久化,不会被写到Cookie文件中。Cookie信息保存在浏览器内存中,因此关闭浏览器该Cookie就消失了。Cookie默认的maxAge值为–1。 +- 如果maxAge为0,则表示删除该Cookie。Cookie机制没有提供删除Cookie的方法,因此通过设置该Cookie即时失效实现删除Cookie的效果。 + +具体的操作方法每种语言都不一样,可以根据不同语言进行设置。 + +#### Cookie的修改和删除 + +Cookie不提供修改、删除的方法。如果要修改某个Cookie,只需要新建一个同名的Cookie,添加到response中覆盖原来的Cookie。 + +如果要删除某个Cookie,只需要新建一个同名的Cookie,并将maxAge设置为0。 + +#### Cookie跨域问题 + +Cookie是不可以跨域名的,隐私安全机制禁止网站非法获取其他网站的Cookie。 + +同一个一级域名下的两个二级域名也不能交互使用Cookie,比如a1.baidu.com和a2.baidu.com,因为二者的域名不完全相同。如果想要baidu.com名下的二级域名都可以使用该Cookie,需要设置Cookie的domain参数为.baidu.com,这样a1.baidu.com和a2.baidu.com就能访问同一个cookie。 + +#### Cookie被浏览器禁用怎么办? + +我们浏览网站的时候,很多网址会让我们选择是否可以使用cookie,如果你选择了禁用是否就没有方法了呢? + +- Url重写,这种方式是直接把session id附加在url路径的后面,例如:www.java1000.com?sessionid=xxx。 +- 在页面表单中增加隐藏域。 + +> Cookie和Session的区别? + +- 存储位置不同。Cookie存储在客户端,Session一般位于服务器上。 +- Session相对更加安全。Cookie是存在于客户端,对客户是可见的;而Session存储在服务端,对客户是透明的,不存在信息安全问题。因此,我们使用Cookie时,是不建议存储敏感信息的。 +- Session会消耗服务器资源,会为每个用户分配一个session,所以,当用户量很大时,会消耗大量的服务器内存;而Cookie存在于客户端,不占用服务器资源,如果用户量大,并发高,Cookie是很好的选择。 +- Cookie的容量有限制,单个Cookie的容量不能超过4KB,而session没有限制。 + +> Session和Cache的区别? + +- Session是单用户的会话状态。当用户访问网站时,就会对其生成一个Seesion,session的sessionid会保存到cookie中,用于后续会话。 +- Cache是服务端的缓存,是对项目的所有用户都是共享的,因为采用数据库的方式有些场景的接口响应不够好,所以采用缓存来进行优化,比如使用redis进行优化,提高访问速度。 + +> 经典面试题:在浏览器输入url到响应结果的整个过程,会用到哪些协议? + +整个请求的流程是这样的。 +- 输入url,进行域名解析,DNS协议解析域名获得IP +- 依据IP地址浏览器向服务器发送HTTP请求,使用TCP协议与服务器建立连接 +- 连接建立时要发送数据,发送数据在网络层使用IP协议 +- 期间IP数据包在路由器间路由选择使用OPSF协议 +- 路由器与服务器通信,需要将IP转换为MAC地址,使用ARP协议 +- 随即服务器处理请求,发回一个HTML响应,浏览器使用HTTP协议显示HTML页面 \ No newline at end of file diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" deleted file mode 100644 index 8ff7d76..0000000 --- "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-1.md" +++ /dev/null @@ -1,21 +0,0 @@ -面试官:OSI与TCP/IP的各层的结构,都有哪些协议呢? -我:哦,我好像知道,我说一下我的理解,这个主要有两种参考模型,一种是基于OSI的参考模型,可以分为7层,分别是:物理层、数据链路层、网络层、运输层、会话层、表示层和应用层;另一种是基于TCP、IP的参考模型,可以分为4层,分别是:网络接口层、网络层、运输层和应用层。 - -关于每一层的协议,我知道的还不少呢,我来说说我知道的吧。 - -应用层:RIP、FTP、DNS、Telnet、SMTP、HTTP、WWW - -我知道的还不少吧,嘚瑟一下,让我一口气说完吧。 - -表示层:JPEG、MPEG、ASCII,MIDI -会话层:RPC、SQL -传输层:TCP、UDP、SPX -网络层:IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGMP -数据链路层:PPP、HDLC、VLAN、MAC -物理层:RJ45、IEEE802.3 - -面试官:你能解释一下RJ45吗? - -我:嗯嗯嗯,你自己查一下吧。。。 - - diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-TCP\345\222\214UDP.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-TCP\345\222\214UDP.md" new file mode 100644 index 0000000..3160539 --- /dev/null +++ "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-TCP\345\222\214UDP.md" @@ -0,0 +1,172 @@ +> OSI与TCP/IP的各层的结构,都有哪些协议呢? + +哦,我好像知道,我说一下我的理解,这个主要有两种参考模型,一种是基于OSI的参考模型,可以分为7层,分别是:物理层、数据链路层、网络层、运输层、会话层、表示层和应用层;另一种是基于TCP、IP的参考模型,可以分为4层,分别是:网络接口层、网络层、运输层和应用层。 + +关于每一层的协议,我知道的还不少呢,我来说说我知道的吧。 + +应用层:RIP、FTP、DNS、Telnet、SMTP、HTTP、WWW + +我知道的还不少吧,嘚瑟一下,让我一口气说完吧。 + +表示层:JPEG、MPEG、ASCII,MIDI +会话层:RPC、SQL +传输层:TCP、UDP、SPX +网络层:IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGMP +数据链路层:PPP、HDLC、VLAN、MAC +物理层:RJ45、IEEE802.3 + +你能解释一下RJ45吗? + +嗯嗯嗯,你自己查一下吧。。。 + +![](https://files.mdnice.com/user/4341/fe41b244-09cc-4fac-83bf-bef92a1388eb.png) + + +> 你能说说三次握手和四次挥手吗? + +![](https://files.mdnice.com/user/4341/0e1881a1-451b-4429-9d0d-8d821cabfcf3.png) + +三次握手需要经过下面的过程状态: +- LISTEN:表示服务器的某个Socket处于监听状态,可以接受连接了。 +- SYN_SENT:当客户端socket执行connet连接时,它首先发送一个SYN报文,紧接着进入SYN_SENT状态,并等待服务端发送三次握手中的第2个报文。 +- SYN_RCVD:这个状态表示收到了SYN报文,在正常状态服务端的socket在建立TCP连接时的三次握手会话中的一个中间状态,但是,需要注意的是netstat是很难看到这种状态。当客户端收到服务端的ACK后,服务端就进入到了ESTABLISHED状态了。 +- ESTABLISHED:表示已经建立连接了。 + +![](https://files.mdnice.com/user/4341/6f201ebf-704b-4cd1-ac1b-dd7e75da8b73.png) + +四次挥手过程状态: +- FIN_WAIT_1:这个状态其实是在建立连接时,一方想要主动关闭连接,然后向对方发送FIN报文,然后自己进入到此状态;同时,当对方回应ACK后,自己就进入到FIN_WAIT_2状态,FIN_WAIT_1也是比较难以看到的。(主动方状态) +- FIN_WAIT_2:这个状态是当主动要求关闭的一方要求关闭时,但暂时还有数据需要传输,稍后再关闭。(主动方状态) +- TIME_WAIT:表示收到了对方的FIN报文,并发送出了ACK报文,等待2MSL后即可回到CLOSED可用状态了。(主动方状态) +- CLOSE_WAIT:该状态表示在等待关闭,当对方发送FIN报文给自己时,你会发送一个ACK报文给对方,此时就进入到了CLOSE_WAIT状态。接下来,实际上你真正需要考虑的就是你是否还有数据发送给对方,如果此时没有了,那么你就可以close这个SOCKET了,发送FIN报文给对方,关闭连接。(被动方状态) +- LAST_ACK:状态是指被动方发送FIN报文后,最后等待对方的ACK报文,收到后,就可以进入到CLOSED状态。(被动方状态) +- CLOSED:表示连接中断。 + +结合上方的解释和两张图,就可以很好的理解三次握手和四次挥手的整个过程的,这个还是比较重要的,很多面试中都会考察,这也是计算机网络最基础的知识。 + +> 在TIME_WAIT状态中,如果TCP客户端的最后一次发送的ACK丢失了,会发生什么? + +如果丢失了,此时会触发重发机制,因为,在此状态下,等待的时间是依赖于实现方法的,一般可以为30s、1min和2min。等待结束后,就关闭连接了,并且所有的资源都会释放。 + +> 为什么收到Server的确认之后,client还需要进行第三次握手? + +可能由于网络连接延时,已经失效的连接到达服务端,这时服务端以为是客户端的请求,所以发送ack到客户端,然后等待客户端的数据传输过来,然而,此时,客户端并没有想要进行连接(因为此时客户端没有发出连接),因此,如果不采用三次握手,进行客户端确认,服务端就会一直等待客户端,导致服务端的资源白白浪费。 + +> 为什么要采用四次挥手 + +确保数据能够完全传输。 +挥手的过程是:主机1发送Fin,表示我没有数据要发送了,然后,主机2发送ACK进行确认,但是,这个时候,主机2还是可以发送数据给主机1的,不能就关闭连接,所以,只有当主机2也发送了Fin之后,才可以进行关闭。 + +> time _wait状态产生的原因 + +- 可靠的实现tcp全双工连接的终止:考虑网络是不可靠的,当客户端发送ACK服务端一定收到了,如果没有收到,可以重发,如果客户端在time-wait状态等待2MSL时长,还没有收到服务的的FIN,就可以关闭自己的连接了。 +- 允许老的重复连接在网络中消逝:网络中可能存在已经失效的连接,time-wait可以使得本连接持续的时间内产生的所有的连接都在网络中消逝,不会出现旧的连接了。 + +> time-wait过多的危害 + +本地端口数量有限,如果有大量的time-wait,会发现本地用于新建连接的端口缺乏,本地很难再建立新的对外连接。 + +> 如何消除大量TCP短连接引发的time—wait + +- 改为长连接 +- 增大可用端口 + +> 当关闭连接时,最后一个ACK丢失怎么办 + +如果在time-wait状态下,最后一个ACK丢失,由于有两个MSL时长等待,所有,会有新的FIN从服务端发送过来,这时,客户端会重新发送ACK。 +如果在closed状态下,客户端收不到服务端重传的FIN,客户端也不会重传ACK,那么服务端就永远无法关闭链接。 + +> TCP如何保证可靠传输 + +- 在传递数据之前,会有**三次握手**来建立连接 +- TCP会将数据分割为最合适发送的数据块,也就是分割为合理的长度 +- TCP发出一个段后,会启动定时器,等待目的端确认,如果不能收到确认,就会重发,这也就是**超时重发机制** +- 当TCP收到另一端的数据时,会向对方发送一个确认,通常会延迟一点发送,这是因为需要对包的完整性进行验证 +- TCP将保持首部和数据的检验和。目的是为了保证数据在传输过程中的任何变化,如果收到的段的检验和由差错,TCP会丢弃这个报文段和不确认收到此报文段 +- TCP会对失序的数据进行数据重新排序,然后再交给应用层 +- TCP会丢去重复的数据 +- TCP会进行**流量控制**。TCP的连接每一方都有一个固定大小的缓冲空间,其只允许接收端只允许另一端发送接收端缓冲区能接纳的数据,这可以防止缓冲区溢出。需要注意的是:TCP使用的流量控制协议是可变大小的滑动窗口协议 +- TCP会进行拥塞控制,当网络拥塞时,会适当的减少数据的发送。 + +> TCP建立连接之后,怎么保持连接? + +目前保持连接的方式有两种技术实现,第一,采用TCP协议层实现的Keepalive机制,第二,由应用层实现的HeartBeat心跳包。 + +**Keepalive机制** +该机制的原理是:TCP协议会向对方发一个keepalive探针包,对方收到包之后,正常会回复一个ACK,出现错误会回复一个RST,如果对方没有任何回复,服务器每隔一段时间再发送keepalive探针包,如果连续发送多个包之后都没有回复,则说明连接断开。 + +**心跳包机制** +该机制原理是:客户端或者服务端会发送一个类似心跳一样每隔固定时间发送一次,客户端在一定时间内没有收到服务端的回应,则可认为服务端不可用,同上,如果服务端在一定时间内没有收到客户端发送的心跳包,则认为客户端掉线。 + +> TCP三次握手有哪些漏洞? + +一是,**DDOS攻击(分布式拒绝服务)**:利用攻击软件通过大量的机器同时对服务进行攻击,规模大,危害大。 + +目前有两种主流的DDOS攻击:**SYN Flood攻击**和**TCP全连接攻击**。 + +> - SYN Flood攻击:利用tcp协议的缺陷,发送大量伪造的tcp连接请求,导致被攻击方资源耗尽。出现的原因是:在TCP三次握手过程中,假设用户发送了SYN报文后掉线,那么服务器在发出SYN+ACK是无法收到客户端的ACK报文,这时,服务端是会一直不断的重试并等待一段时间后丢弃这个未完成的连接的,如果有大量的伪造的攻击报文,发送到了服务端,服务端将为了维护一个非常大的半连接队列而消耗过多的CPU时间和内存,这就会导致服务器失去响应。 +> - TCP全连接攻击:这种攻击是为了绕过防火墙设计的,它能够绕过防火墙,导致服务器有大量的TCP连接,最终导致服务器拒绝服务。 + +**解决SYN Flood攻击的方法** + +- 设置SYN timeout时间:SYN timeout时间是指服务端一直不断的重试并等待的这段时间。 +- 设置SYN cookie,判断是否连续收到某个ip的重复SYN报文,如果是,以后就丢弃这个ip地址的包 +- 设置SYN cache,先不分配系统资源,现用cache保存半开连接,直到收到正确的ACK在分配资源 +- 硬件防火墙 + +**解决TCP全连接攻击的方法** + +- 限制SYN流量 +- 定期扫描缺陷 +- 在骨干节点设置防火墙 +- 有足够的机器可以承受攻击 +- 过滤不必要的服务和端口 + +二是,**Land攻击**,该攻击是利用了TCP三次握手时,通过向一个主机发送一个用于建立连接的TCP SYN报文而实现对目标主机的攻击,这种方式与正常的TCP SYN报文不同的是:Land攻击的报文源ip地址和目标ip地址是一样的,都是目标主机的ip地址。 + +由于目标IP地址和源IP地址是一样的,因此,ACK报文就发给了主机本身,如果大量的SYN报文进行发送,目标计算机也不能正常工作。 + +> tcp如何实现流量控制和拥塞控制? + +流量控制采用**滑动窗口机制**。 +拥塞控制采用**拥塞避免方法**。 + +>**滑动窗口原理** +TCP是全双工通信,因此每一方的滑动窗口都包括了接收窗口+发送窗口,接收窗口负责处理自己接收到的数据,发送窗口负责处理自己要发送出去的数据。滑**动窗口的本质其实就是维护几个变量,通过这些变量将TCP处理的数据分为几类,同时在发送出一个报文、接收一个报文对这些变量做一定的处理维护**。 +(1)N是发送窗口的起始字节,也就是说:字节序号 < N的字节都已经发送出去且已经收到ack,确认无误了; +(2)nextSeq就是下一次发送报文的首部Seq字段(Seq即b第一个字节的序号,这些这里不讲了),表示字节序号在 [N,nextSeq)区间的都已经使用过,发送出去了,但是还未收到ack确认; +(3) N+size就是窗口的最后一个可用字节序号,size是发送窗口的大小,就是每次接收到的报文中的Win字段的值,Win字段其实就是对方接收窗口的大小。 +**如何让维护这几个值呢?** +(1)每接收到一个一个报文要做如下事情:检查接收报文的ack,将N 置为 ack,即往前移到ack这个值;读取报文中的Win字段值,即对方的最新接收窗口大小,从而更新N+size的值。 +(2)每发送一个报文,就更改nextSeq的值,发送了多少个字节就把nextSeq往前移多少,但是不要超出N+size。 +![](https://img2018.cnblogs.com/i-beta/1743446/201912/1743446-20191228103841451-1429620351.png) + +**拥塞避免方法** +慢启动、拥塞避免、快重传和快恢复 + +![](http://image.ouyangsihai.cn/Fs-2RO7AcVWM9MKgh11-wdE8ln10) +![](http://image.ouyangsihai.cn/FuvM3wAfN4tJb7xf2RkSXey7qV0d) + +以上是两张原理图,根据这两张原理图应该可以比较好理解,如果想要深入理解,可以参考这篇文章:https://www.cnblogs.com/a3192048/p/12241296.html。 + +> 经典问题:TCP和UDP的区别? + +- TCP基于连接的协议,UDP是无连接的协议。 +- TCP可以保证数据发送的可靠性,UDP不可靠。 +- TCP可以重排序,UDP不可以。 +- TCP速度慢,UDP较快。 +- TCP是重量级协议,UDP是轻量级协议。 +- TCP有流量控制和拥塞控制机制,UDP没有。 +- TCP面向字节流的协议,UDP面向报文。 +- TCP只能单播,不能发送广播和组播,UDP可以。 + +**TCP应用场景:** 效率要求不高,但需要数据的准确性,例如,文件传输、邮件传输、远程登录等等。 +**UDP应用场景:** 效率要求高,准确性要求不高,例如,微信视频,语音聊天等。 + +> 为什么TCP比UDP安全,还有人用UDP、为什么UDP快? + +- UDP不需要建立连接. +- UDP不需要维护连接的状态. +- UDP头部开销小,只需要8字节. +- UDP没有拥塞控制,不会影响主机的发送频率,所以速度上比tcp有优势. + diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-http.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-http.md" new file mode 100644 index 0000000..582529e --- /dev/null +++ "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225-http.md" @@ -0,0 +1,264 @@ +> Http的请求报文结构和响应报文结构 + +Http的请求报文主要由`请求行、请求头、空行、请求正文`。 + +**请求行**由请求方法、URL以及协议版本组成。 +请求方法:GET、HEAD、PUT、POST、TRACE、OPTIONS、DELETE以及扩展方法 +协议版本:常用的有HTTP/1.0和HTTP/1.1 + +**HTTP请求头** +| Header | 解释 | 示例 | +| --- | --- | --- | +| Accept | 指定客户端能够接收的内容类型 | Accept: text/plain, text/html | +| Accept-Charset | 浏览器可以接受的字符编码集。 | Accept-Charset: iso-8859-5 | +| Accept-Encoding | 指定浏览器可以支持的web服务器返回内容压缩编码类型。 | Accept-Encoding: compress, gzip | +| Accept-Language | 浏览器可接受的语言 | Accept-Language: en,zh | +| Accept-Ranges | 可以请求网页实体的一个或者多个子范围字段 | Accept-Ranges: bytes | +| Authorization | HTTP授权的授权证书 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | +| Cache-Control | 指定请求和响应遵循的缓存机制 | Cache-Control: no-cache | +| Connection | 表示是否需要持久连接。(HTTP 1.1默认进行持久连接) | Connection: close | +| Cookie | HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。 | Cookie: $Version=1; Skin=new; | +| Content-Length | 请求的内容长度 | Content-Length: 348 | +| Content-Type | 请求的与实体对应的MIME信息 | Content-Type: application/x-www-form-urlencoded | +| Date | 请求发送的日期和时间 | Date: Tue, 15 Nov 2010 08:12:31 GMT | +| Expect | 请求的特定的服务器行为 | Expect: 100-continue | +| From | 发出请求的用户的Email | From: user@email.com | +| Host | 指定请求的服务器的域名和端口号 | Host: www.zcmhi.com | +| If-Match | 只有请求内容与实体相匹配才有效 | If-Match: “737060cd8c284d8af7ad3082f209582d” | +| If-Modified-Since | 如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304代码 | If-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT | +| If-None-Match | 如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变 | If-None-Match: “737060cd8c284d8af7ad3082f209582d” | +| If-Range | 如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体。参数也为Etag | If-Range: “737060cd8c284d8af7ad3082f209582d” | +| If-Unmodified-Since | 只在实体在指定时间之后未被修改才请求成功 | If-Unmodified-Since: Sat, 29 Oct 2010 19:43:31 GMT | +| Max-Forwards | 限制信息通过代理和网关传送的时间 | Max-Forwards: 10 | +| Pragma | 用来包含实现特定的指令 | Pragma: no-cache | +| Proxy-Authorization | 连接到代理的授权证书 | Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | +| Range | 只请求实体的一部分,指定范围 | Range: bytes=500-999 | +| Referer | 先前网页的地址,当前请求网页紧随其后,即来路 | Referer: http://www.zcmhi.com/archives/71.html | +| TE | 客户端愿意接受的传输编码,并通知服务器接受接受尾加头信息 | TE: trailers,deflate;q=0.5 | +| Upgrade | 向服务器指定某种传输协议以便服务器进行转换(如果支持) | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 | +| User-Agent | User-Agent的内容包含发出请求的用户信息 | User-Agent: Mozilla/5.0 (Linux; X11) | +| Via | 通知中间网关或代理服务器地址,通信协议 | Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1) | +| Warning | 关于消息实体的警告信息 | Warn: 199 Miscellaneous warning | + +**请求正文** + +请求正文不一定都有的,比如,get请求方法就没有请求正文部分。 + +![](http://image.ouyangsihai.cn/FhKqcF6tPKzSLW_lstiSDGoNgDqa) + +例如,上图就是一个请求的实例,分别有:**请求行、请求头**,由于是一个Get请求,所以是没有请求体的,当然,比如post、put等请求,就会有请求体。 + +那么,http的响应报文结构,也是类似的。 + +主要由**状态行、响应头、空行、响应正文**组成。 + +![](http://image.ouyangsihai.cn/Fu2xla40yiujD2W7i-JaEhzbIIQT) + +常见的响应头有: + + + +响应头 | 说明 | +---------|----------| + Server | 服务器应用程序软件的名称和版本 | + Content-Type | 响应正文的类型(是图片或者二进制字符串等) | + Content-Length | 响应正文长度 | + Content-Charset | 响应正文使用的编码 | + Content-Encoding | 文档的编码(Encode)方法。只有在解码之后才可以得到Content-Type头指定的内容类型。利用gzip压缩文档能够显著地减少HTML文档的下载时间。Java的GZIPOutputStream可以很方便地进行gzip压缩,但只有Unix上的Netscape和Windows上的IE 4、IE 5才支持它。因此,Servlet应该通过查看Accept-Encoding头(即request.getHeader("Accept-Encoding"))检查浏览器是否支持gzip,为支持gzip的浏览器返回经gzip压缩的HTML页面,为其他浏览器返回普通页面。 | + Content-Language | 响应正文使用的语言 | + Expires |应该在什么时候认为文档已经过期,从而不再缓存它? + Last-Modified | 文档的最后改动时间。客户可以通过If-Modified-Since请求头提供一个日期,该请求将被视为一个条件GET,只有改动时间迟于指定时间的文档才会返回,否则返回一个304(Not Modified)状态。Last-Modified也可用setDateHeader方法来设置。 + + > http状态码 + +**HTTP状态码分类** +HTTP状态码共分为5种类型: +| 分类 | 分类描述 | +|-----|-----| +| 1** | 信息,服务器收到请求,需要请求者继续执行操作 | +| 2** | 成功,操作被成功接收并处理 | +| 3** | 重定向,需要进一步的操作以完成请求 | +| 4** | 客户端错误,请求包含语法错误或无法完成请求 | +| 5** | 服务器错误,服务器在处理请求的过程中发生了错误 | + +HTTP的状态码列表: + +| 状态码 | 状态码英文名称 | 中文描述 | +|-----|-----|-----| +| 100 | Continue | 继续。客户端应继续其请求 | +| 101 | Switching Protocols | 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议 | +| | +| 200 | OK | 请求成功。一般用于GET与POST请求 | +| 201 | Created | 已创建。成功请求并创建了新的资源 | +| 202 | Accepted | 已接受。已经接受请求,但未处理完成 | +| 203 | Non-Authoritative Information | 非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本 | +| 204 | No Content | 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档 | +| 205 | Reset Content | 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域 | +| 206 | Partial Content | 部分内容。服务器成功处理了部分GET请求 | +| | +| 300 | Multiple Choices | 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择 | +| 301 | Moved Permanently | 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替 | +| 302 | Found | 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI | +| 303 | See Other | 查看其它地址。与301类似。使用GET和POST请求查看 | +| 304 | Not Modified | 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源 | +| 305 | Use Proxy | 使用代理。所请求的资源必须通过代理访问 | +| 306 | Unused | 已经被废弃的HTTP状态码 | +| 307 | Temporary Redirect | 临时重定向。与302类似。使用GET请求重定向 | +| | +| 400 | Bad Request | 客户端请求的语法错误,服务器无法理解 | +| 401 | Unauthorized | 请求要求用户的身份认证 | +| 402 | Payment Required | 保留,将来使用 | +| 403 | Forbidden | 服务器理解请求客户端的请求,但是拒绝执行此请求 | +| 404 | Not Found | 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面 | +| 405 | Method Not Allowed | 客户端请求中的方法被禁止 | +| 406 | Not Acceptable | 服务器无法根据客户端请求的内容特性完成请求 | +| 407 | Proxy Authentication Required | 请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权 | +| 408 | Request Time-out | 服务器等待客户端发送的请求时间过长,超时 | +| 409 | Conflict | 服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突 | +| 410 | Gone | 客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置 | +| 411 | Length Required | 服务器无法处理客户端发送的不带Content-Length的请求信息 | +| 412 | Precondition Failed | 客户端请求信息的先决条件错误 | +| 413 | Request Entity Too Large | 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息 | +| 414 | Request-URI Too Large | 请求的URI过长(URI通常为网址),服务器无法处理 | +| 415 | Unsupported Media Type | 服务器无法处理请求附带的媒体格式 | +| 416 | Requested range not satisfiable | 客户端请求的范围无效 | +| 417 | Expectation Failed | 服务器无法满足Expect的请求头信息 | +| | +| 500 | Internal Server Error | 服务器内部错误,无法完成请求 | +| 501 | Not Implemented | 服务器不支持请求的功能,无法完成请求 | +| 502 | Bad Gateway | 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应 | +| 503 | Service Unavailable | 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中 | +| 504 | Gateway Time-out | 充当网关或代理的服务器,未及时从远端服务器获取请求 | +| 505 | HTTP Version not supported | 服务器不支持请求的HTTP协议的版本,无法完成处理 | + +> http的缓存机制了解吗?有关缓存的首部字段有哪些呢? + +**Last-Modified和If-Modifed-since** + +Last-Modified:是由服务器发往客户端的最后的时间 +If-Modified-since:是由客户端发往服务器的最后时间 + +用于记录页面最后修改的时间的Http头信息:服务器将请求的Last-Modified发往客户端,客户端如果没有缓存,则缓存在浏览器,然后,客户端每次将If-Modified-Since发往服务端,每次如果服务端发现服务端资源没有发生改变,则自动返回304,内容为空,这样节省了传输的数据量,如果服务端发现有新的资源,则返回200,并且返回新的资源,然后浏览器丢弃旧的资源,缓存新的资源。 + +**ETag和if-None-Match** + +ETag是服务器会为每个资源分配对应的ETag值,当资源内容发生改变时,其值也会发生改变。 + +ETag和if-None-Match的工作原理是在Http的Response中添加ETags,当客户端再次请求该资源时,将在Http的Request中加入if-None-Match信息(ETag值)。如果服务器资源验证的ETags没有改变,将返回一个304状态;否则,服务器返回一个200状态,并返回该资源和新的ETags。 + +**Expires/Cache-Control(优先使用)** + +Expires用于控制缓存的失效时间,但是有一个缺点就是失效时间是服务器返回的时间,跟客户端会存在延时,所以,在http1.1中,加入了Cache-Control,这声明是一种相对秒级,可以避免服务端和客户端时间不一致的问题。 + +Expires的格式:`expires=Fri, 17 Jul 2020 21:15:42 GMT`。 + +Cache-Control的格式:`'Cache-Control': 'max-age=2000000, no-store'` + + +> Last-Modified和Etag 如何帮助提高性能? + +一起使用,利用客户端缓存,服务端判断页面是否被修改。 + +> 有了Last-Modified为什么还要用ETag字段呢? + +- 文件修改非常频繁时,If-Modifed-since能检查到的粒度是秒级的,这种修改无法使用If-Modifed-since。 +- 某些文件周期性的修改,但是内容没有改变,这种无法体现,导致重新get。 +- 某些服务器不能精准的得到文件的最后修改时间,利用ETag提供了更加严格的验证。 + + +> http1.1和http1.0的区别? + +- 长连接与短连接的区别:http1.1使用长连接,在请求消息中会包含Connection:Keep-Alive的头域。 +- 分块传输: +在http1.0中,用于指定实体长度的唯一机制是通过content-length,静态资源没有问题,但对于动态资源,为了获取长度,只有等他完全生成以后才能获取content-length的值,这要求缓存整个响应,延长了响应用户的时间。 +在http1.1中,引入了分块的传输方法,该方法使得发送方可以将消息实体分割为任意大小的组块,在每个组块的前面都加上长度,最后的一块为空块,标识完成传输,这样避免了服务器端大量的占用缓存。 +- http状态码100 Continue: +当客户端发送的post请求的数据大于1024字节是,客户端并不是直接发起POST请求,而是分为两步,1)发送一个请求,包含Expect:100-continue,询问服务端是否接受数据;2)接受到服务端返回的100 continue后,才将数据发送给服务端。 +这是为了避免发送一个冗长的请求,占用服务端的资源。 +- Host域: +http1.1中,随着虚拟主机技术的发展,物理服务器上可以存在多个虚拟主机,所以,在1.1中可以在host中存在多个ip地址。 + +> http常用方法有哪些? + +|方法 | 说明 | 支持的HTTP协议版本| +|-----|------|------| +|GET | 获取资源| 1.0、1.1| +|POST | 传输实体主体 | 1.0、1.1| +|PUT | 传输文件 | 1.0、1.1| +|HEAD | 获得报文首部 | 1.0、1.1| +|DELETE | 删除文件 | 1.0、1.1| +|OPTIONS | 询问支持的方法 | 1.1| +|TRACE | 追踪路径 | 1.1| +|CONNECT | 要求用隧道协议连接代理 | 1.1| + +> http哪些是幂等的呢? + +幂等性是指一次或者多次操作所产生的副作用是一样的。 + +GET、Put、Delete是幂等的,Post不是幂等的。 + +> http的请求方法get和post的区别? + +- 从幂等性和安全性来说:Get是指查询或者获取资源,它是幂等的,同时也是安全的,而post一般是更新资源,既不是幂等的也不是安全的。 +- 从传输方式和传输数量大小限制方面:get请求放在请求行后面,有传输大小限制;post请求放在消息体中,没有传输大小限制。 +- 从是否缓存的角度:get请求的数据会被浏览器缓存起来,post则不会。 + +> 为什么http是无状态的,如何保持状态? + +无状态性是指:对事务处理没有记忆的能力,每一次的请求信息返回之后,就会丢弃,服务器无法知道与上次请求的联系, +好处:不用分配内存记录大量状态,节省服务端资源; +缺点:每次需要重新传输大量的数据。 + +保持状态采用的是会话跟踪技术(解决无状态性),主要的方法有:Cookie、Url重写、session和利用html嵌入表单域。 + +> http短连接和长连接原理 + +http1.0默认使用短连接,http1.1默认使用长连接,长连接是request中添加Connection:keep-alive,这样就是长连接。 + +另外,http的短连接和长连接,实质上就是TCP协议的长连接和短连接。 + +http长连接的优点: +- 通过开启或者关闭更少的连接,节约cpu时间和内存 +- 通过减少TCP开启和关闭引起的包的数量,降低网络阻塞 + +http长连接的缺点:服务器维护一个长连接会增加开销,消耗服务器资源。 + +> 说说http的特点 + +- 支持客户端、服务端通信模式 +- 简单方便快捷:发送请求,只需要简单的输入请求路径和请求方法即可,然后通过浏览器发送就可以了。 +- 灵活:http协议允许客户端和服务端传输任意类型任意合适的数据对象,这些不同类型由Content-Type标记。 +- 无连接:每次客户端发送请求到服务端,服务端响应之后,就会断开连接。 +- 无状态:http是无状态的协议,请求的处理没有记忆能力。 + +> http存在哪些安全性问题?怎么解决 + +- 通过使用明文不加密,内容可能被窃听 +- 不验证通信方的身份,可能遭到伪装 +- 无法验证报文的完整性,可能被篡改 + +可以采用https进行解决。 + +> https的作用,https中的安全性技术有哪些 + +https可以对内容进行加密,进行身份验证,同时可以检验数据完整性。 + +https的安全性技术: +- 对称性加密算法:用于真正传输的数据进行加密。 +- 非对称性加密算法:用于在握手过程中加密生成的密码,非对称性加密算法会生成公钥和私钥。 +- 散列算法:用于验证数据的完整性。 +- 数字证书:使用数字证书进行证明自己的身份。 + +> https和http的区别? + +- https更加安全:https可以通过加密、数据完整性验证和身份验证的方法保证数据的安全。 +- https需要申请证书,在CA申请证书。 +- 端口不同:http采用80,https采用443。 +- http协议运行在tcp之上,https运行在ssl、TLS之上的http协议,ssl、TLS运行在TCP之上。 + +> http和socket的区别? + +- http只能走tcp协议,socket可以走tcp和udp协议。 +- http基于请求响应模式,只有客户端发送给了服务端,服务端才可以响应,socket则可以通过服务端推送给客户端。 +- socket效率高,因为不需要解析报文头部字段等。 + From 2c20613f05cfbebd965a2f8f15a3b36a9286b51c Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Tue, 28 Dec 2021 08:45:51 +0800 Subject: [PATCH 27/38] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=9D=A2=E8=AF=95?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 37d6ef0..fa43602 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ _config.yml .settings .springBeans .sts4-cache +.vscode ### IntelliJ IDEA ### .idea From 4dfdb43eb6867fc25afb92558103f8c7d7df8118 Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Sun, 2 Jan 2022 15:31:17 +0800 Subject: [PATCH 28/38] =?UTF-8?q?update:=20=E6=96=B0=E5=A2=9E=E7=AE=97?= =?UTF-8?q?=E6=B3=95=E6=96=87=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +- ...10\351\235\242\350\257\225\351\242\230.md" | 0 ...37\346\234\272\351\235\242\350\257\225.md" | 961 +++++++++++++++++- 3 files changed, 953 insertions(+), 21 deletions(-) create mode 100644 "docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" diff --git a/README.md b/README.md index 6d273d1..f84ec42 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ ### 目录(ctrl + f 查找更香:不能点击的,还在写) +- [个人经验](#个人经验) - [项目准备](#项目准备) - [面试知识点](#面试知识点) - [公司面经](#公司面经) @@ -100,11 +101,16 @@ - [联系我](#联系我) - [公众号](#公众号) +## 个人经验 + +- [应届生如何准备校招,用我这一年的校招经历告诉你](https://sihai.blog.csdn.net/article/details/114258312?spm=1001.2014.3001.5502) +- [【大学到研究生自学Java的学习路线】这是一份最适合普通大众、非科班的路线,帮你快速找到一份满意的工作](https://sihai.blog.csdn.net/article/details/105964718?spm=1001.2014.3001.5502) +- [两个月的面试真实经历,告诉大家如何能够进入大厂工作?](https://sihai.blog.csdn.net/article/details/105807642) ## 项目准备 - [我的个人项目介绍模板](docs/interview/自我介绍和项目介绍.md) -- [本人真实经历:面试了20家大厂之后,发现这样介绍项目经验,显得项目很牛逼!](https://mp.weixin.qq.com/s/JBtCRaw9Nabk9aIImjkFIQ) +- [本人面试两个月真实经历:面试了20家大厂之后,发现这样介绍项目经验,显得项目很牛逼!](https://sihai.blog.csdn.net/article/details/105854760) - [项目必备知识及解决方案](docs/project/秒杀项目总结.md) ## 面试知识点 @@ -226,11 +232,12 @@ ### 算法 - +- [从大学入门到研究生拿大厂offer,必须看的数据结构与算法书籍推荐,不好不推荐!](https://sihai.blog.csdn.net/article/details/106011624?spm=1001.2014.3001.5502) - [2020年最新算法面试真题汇总](docs/dataStructures-algorithms/算法面试真题汇总.md) - [2020年最新算法题型难点总结](docs/dataStructures-algorithms/算法题目难点题目总结.md) - [关于贪心算法的leetcode题目,这篇文章可以帮你解决80%](https://blog.ouyangsihai.cn/jie-shao-yi-xia-guan-yu-leetcode-de-tan-xin-suan-fa-de-jie-ti-fang-fa.html) -- 回溯算法不会,这篇文章一定得看 +- [dfs题目这样去接题,秒杀leetcode题目](https://sihai.blog.csdn.net/article/details/106895319) +- [回溯算法不会,这篇文章一定得看](https://sihai.blog.csdn.net/article/details/106993339) - 动态规划你了解多少,我来帮你入个们 - 链表的题目真的不难,看了这篇文章你就知道有多简单了 - 还在怕二叉树的题目吗? diff --git "a/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" "b/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..e69de29 diff --git "a/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" "b/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" index ce68d03..d8269e5 100644 --- "a/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" +++ "b/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" @@ -17,7 +17,7 @@ Java 虚拟机所管理的内存一共分为Method Area(方法区)、VM Stac 上图介绍的是JDK1.8 JVM运行时内存数据区域划分。1.8同1.7比,最大的差别就是:**元数据区取代了永久代**。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:**元数据空间并不在虚拟机中,而是使用本地内存**。 -### 1 程序计数器(Program Counter Register) +#### 1 程序计数器(Program Counter Register) **程序计数器(Program Counter Register)**是一块较小的内存空间,可以看作是当前线程所执行的字节码的**行号指示器**。在虚拟机概念模型中,**字节码解释器**工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 @@ -35,7 +35,7 @@ Java 虚拟机所管理的内存一共分为Method Area(方法区)、VM Stac 其实,我感觉这块区域,作为我们开发人员来说是不能过多的干预的,我们只需要了解有这个区域的存在就可以,并且也没有虚拟机相应的参数可以进行设置及控制。 -### 2 Java虚拟机栈(Java Virtual Machine Stacks) +#### 2 Java虚拟机栈(Java Virtual Machine Stacks) ![](http://image.ouyangsihai.cn/FksaMoFlAkSPkTB84bV4cK7xa8L3) @@ -60,7 +60,7 @@ Java虚拟机规范中对这个区域规定了两种异常状况: 一直觉得上面的概念性的知识还是比较抽象的,下面我们通过JVM参数的方式来控制栈的内存容量,模拟StackOverflowError异常现象。 -### 3 本地方法栈(Native Method Stack) +#### 3 本地方法栈(Native Method Stack) **本地方法栈(Native Method Stack)** 与Java虚拟机栈作用很相似,它们的区别在于虚拟机栈为虚拟机执行Java方法(即字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 @@ -109,11 +109,11 @@ public class Test_02 { -### 4 Java堆(Heap) +#### 4 Java堆(Heap) -对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 +对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 Java堆是垃圾收集器管理的主要区域,因此也被称为 **“GC堆(Garbage Collected Heap)”** 。从内存回收的角度看内存空间可如下划分: @@ -192,7 +192,7 @@ public class HeapTest { 如上图,Eden新生代总共48次minor gc,耗时1.168s,基本满足要求,但是survivor却没有,这不正常,同时Old Gen老年代总共27次full gc,耗时4.266s,耗时长,gc多,这正是因为大量的大对象进入到老年代导致的,所以,导致full gc频繁。 -### 5 方法区(Method Area) +#### 5 方法区(Method Area) **方法区(Method Area)** 与Java堆一样,是各个线程共享的内存区域。它用于存储一杯`虚拟机加载`的**类信息、常量、静态变量、及时编译器编译后的代码**等数据。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 **Non-Heap**。 @@ -249,7 +249,7 @@ vm参数介绍: >**注意:** 在JDK1.6及以前的版本中运行以上代码,因为我们通过`-XX:PermSize=10M -XX:MaxPermSize=10M`设置了方法区的大小,所以也就是设置了常量池的容量,所以运行之后,会报错:`java.lang.OutOfMemoryError:PermGen space`,这说明常量池溢出;在JDK1.7及以后的版本中,将会一直运行下去,不会报错,在前面也说到,JDK1.7及以后,去掉了永久代。 -### 6 直接内存 +#### 6 直接内存 **直接内存(Direct Memory)**并不是虚拟机**运行时数据区**的一部分,也不是Java虚拟机规范中定义的内存区域。但这部分内存也被频繁运用,而却可能导致**OutOfMemoryError**异常出现。 @@ -369,7 +369,7 @@ class Instance{ 这个面试题其实和上面的有点重合,但是,这里单独拿出来再介绍一下,因为这个确实是比较常见的,这样大家也都有印象。 -对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 +对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 Java堆是垃圾收集器管理的主要区域,因此也被称为 **“GC堆(Garbage Collected Heap)”** 。从内存回收的角度看内存空间可如下划分: @@ -504,21 +504,21 @@ GC会stop the world。会暂停程序的执行,带来延迟的代价。所以 - 对象不用时最好显式置为 Null -一般而言,为 Null 的对象都会被作为垃圾处理,所以将不用的对象显式地设为 Null,有利于 GC 收集器判定垃圾,从而提高了 GC 的效率。 +一般而言,为 Null 的对象都会被作为垃圾处理,所以将不用的对象显式地设为 Null,有利于 GC 收集器判定垃圾,从而提高了 GC 的效率。 - 尽量少用 System.gc() -此函数建议 JVM 进行主 GC,虽然只是建议而非一定,但很多情况下它会触发主 GC,从而增加主 GC 的频率,也即增加了间歇性停顿的次数。 +此函数建议 JVM 进行主 GC,虽然只是建议而非一定,但很多情况下它会触发主 GC,从而增加主 GC 的频率,也即增加了间歇性停顿的次数。 - 尽量少用静态变量 -静态变量属于全局变量,不会被 GC 回收,它们会一直占用内存。 -- 尽量使用 StringBuffer,而不用 String 来累加字符串 +静态变量属于全局变量,不会被 GC 回收,它们会一直占用内存。 +- 尽量使用 StringBuffer,而不用 String 来累加字符串 -由于 String 是固定长的字符串对象。累加 String 对象时,并非在一个 String对象中扩增,而是重新创建新的 String 对象,如 Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的 String 对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。 避免这种情况可以改用 StringBuffer 来累加字符串,因 StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象 +由于 String 是固定长的字符串对象。累加 String 对象时,并非在一个 String对象中扩增,而是重新创建新的 String 对象,如 Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的 String 对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。 避免这种情况可以改用 StringBuffer 来累加字符串,因 StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象 - 分散对象创建或删除的时间 -集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM 在面临这种情况时,只能进行主 GC,以回收内存或整合内存碎片,从而增加主 GC 的频率。 +集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM 在面临这种情况时,只能进行主 GC,以回收内存或整合内存碎片,从而增加主 GC 的频率。 -集中删除对象,道理也是一样的。 它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主 GC 的机会。 +集中删除对象,道理也是一样的。 它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主 GC 的机会。 - 尽量少用 finalize 函数 因为它会加大 GC 的工作量,因此尽量少用finalize 方式回收资源。 @@ -532,9 +532,9 @@ GC会stop the world。会暂停程序的执行,带来延迟的代价。所以 如果需要使用经常用到的图片,可以使用软引用类型,它可以尽可能将图片保存在内存中,供程序调用,而不引起 OutOfMemory。 -- 能用基本类型如 int,long,就不用 Integer,Long 对象 +- 能用基本类型如 int,long,就不用 Integer,Long 对象 -基本类型变量占用的内存资源比相应包装类对象占用的少得多,如果没有必要,最好使用基本变量。 +基本类型变量占用的内存资源比相应包装类对象占用的少得多,如果没有必要,最好使用基本变量。 - 增大-Xmx @@ -576,7 +576,7 @@ promotion failed是在进行Minor GC时,`survivor space`放不下、对象只 所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。 -为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即`-XX:+UseCMSCompactAtFullCollection`开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 `-XX:CMSFullGCsBeforeCompaction`,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。 +为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即`-XX:+UseCMSCompactAtFullCollection`开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 `-XX:CMSFullGCsBeforeCompaction`,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。 > 数组多大会放在JVM老年代,永久代对象如何GC?如果想不被GC怎么办?如果想在GC中生存一次怎么办? @@ -586,3 +586,928 @@ promotion failed是在进行Minor GC时,`survivor space`放不下、对象只 让对象实现finalize()方法,一次对象的自我拯救。 +> JVM 常见的参数有哪些,介绍几个常见的,并说说在你工作中实际用到的地方? + +首先,JVM 中的参数是非常多的,而且可以分为不同的类型,主要可以分为以下三种类型: +- `标准参数(-)`,所有的JVM实现都必须实现这些参数的功能,而且向后兼容。 +- `非标准参数(-X)`,默认JVM实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容。 +- `非Stable参数(-XX)`,此类参数各个JVM实现会有所不同,将来可能会随时取消,需要慎重使用。 + +虽然是这么分类的,实际上呢,非标准参数和非稳定的参数实际的使用中还是用的非常的多的。 + +#### 标准参数 +这一类参数可以说是我们刚刚开始Java是就用的非常多的参数了,比如`java -version`、`java -jar`等等,我们在CMD中输入`java -help`就可以获得Java当前版本的所有标准参数了。 + +![](http://image.ouyangsihai.cn/FsGtetpK2vpkQRFke44AyrDqbsl2) + +如上图就是JDK1.8的所有标准参数了,下面我们将介绍一些我们会用的比较多的参数。 + +- -client + +以client模式启动JVM,这种方式启动速度快,但运行时性能和内存管理效率不高,适合客户端程序或者开发调试。 + +- -server + +以server模式启动JVM,与client情况恰好相反。适合生产环境,适用于服务器。64位的JVM自动以server模式启动。 + +- -classpath或者-cp + +通知JVM类搜索路径。如果指定了`-classpath`,则JVM就忽略`CLASSPATH`中指定的路径。各路径之间以分号隔开。如果`-classpath`和`CLASSPATH`都没有指定,则JVM从当前路径寻找class。 + +JVM搜索路径的顺序: + +**1.先搜索JVM自带的jar或zip包。** + +Bootstrap,搜索路径可以用`System.getProperty("sun.boot.class.path")`获得; + +**2.搜索`JRE_HOME/lib/ext`下的jar包。** + +Extension,搜索路径可以用`System.getProperty("java.ext.dirs")`获得; + +**3.搜索用户自定义目录,顺序为:当前目录(.),CLASSPATH,-cp。** + +搜索路径用`System.getProperty("java.class.path")`获得。 + +```java +System.out.println(System.getProperty("sun.boot.class.path")); +System.out.println(System.getProperty("java.ext.dirs")); +System.out.println(System.getProperty("java.class.path")); +``` +![](http://image.ouyangsihai.cn/FnHtFC8cd9UucBd39nAiLcCrT5mY) + +如上就是我电脑的JVM的路径。 + +- -DpropertyName=value + +定义系统的全局属性值,如配置文件地址等,如果value有空格,则需要使用双引号。 + +另外用`System.getProperty("hello")`可以获得这些定义的属性值,在代码中也可以用`System.setProperty("hello","world")`的形式来定义属性。 + +如键值对设置为hello=world。 +![](http://image.ouyangsihai.cn/Fp74h47lvnSt4LAq4S7bKH_Qcilr) + +```java +System.out.println(System.getProperty("hello")); +``` +运行结果就是: +![](http://image.ouyangsihai.cn/Fu6bGzdxxsaJ_bOzjDbd1RUxFKIM) + +- -verbose + +查询GC问题最常用的命令之一,参数如下: +**-verbose:class** +输出JVM载入类的相关信息,当JVM报告说找不到类或者类冲突时可此进行诊断。 +**-verbose:gc** +输出每次GC的相关情况。 +**-verbose:jni** +输出native方法调用的相关情况,一般用于诊断jni调用错误信息。 + +另外,控制台**输出GC信息**还可以使用如下命令: + +在JVM的启动参数中加入`-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime`,按照参数的顺序分别输出GC的简要信息,GC的详细信息、GC的时间信息及GC造成的应用暂停的时间。 + +#### 非标准参数 + +非标准的参数主要是关于Java内存区域的设置参数,所以在看这些参数之前,应该先查看Java内存区域的基础知识,可以查看这篇文章:[深入理解Java虚拟机-Java内存区域透彻分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-java-nei-cun-qu-yu-tou-che-fen-xi.html)。 + +非标准参数实在标准参数的基础上的一些扩充参数,可以输入`java -X`,获得当前JVM支持的非标准参数。 + +![](http://image.ouyangsihai.cn/FkMW25ImmKNr7dZDD0pyysui2wYl) + +从图片中可以看出来,这些非标准的参数其实不多的,下面我们再 讲解一些比较常用的参数。 + +- -Xmn + +新生代内存大小的最大值,包括E区和两个S区的总和。设置方法:`-Xmn512m、-Xmn2g`。 + +- -Xms + +初始堆的大小,也是堆大小的最小值,默认值是总共的物理内存/64(且小于1G)。默认情况下,当堆中可用内存小于40%,堆内存会开始增加,一直增加到-Xmx的大小。 + +- -Xmx + +堆的最大值,默认值是总共的物理内存/64(且小于1G),默认情况下,当堆中可用内存大于70%,堆内存会开始减少,一直减小到-Xms的大小。 + +因此,为了避免这种浮动,所以在设置`-Xms`和`-Xmx`参数时,一般会设置成一样的,能够提高性能。 + +另外,官方默认的配置为**年老代大小:年轻代大小=2:1**左右,使用`-XX:NewRatio`可以设置年老代和年轻代之比,例如,`-XX:NewRatio=4`,表示**年老代:年轻代=4:1** + +- -Xss + +设置每个线程的栈内存,默认1M,一般来说是不需要改的。 + +- -Xprof + +跟踪正运行的程序,并将跟踪数据在标准输出输出;适合于开发环境调试。 + +- -Xnoclassgc + +禁用类垃圾收集,关闭针对class的gc功能;因为其阻止内存回收,所以可能会导致OutOfMemoryError错误,慎用。 + +- -Xincgc + +开启增量gc(默认为关闭);这有助于减少长时间GC时应用程序出现的停顿;但由于可能和应用程序并发执行,所以会降低CPU对应用的处理能力。 + +- -Xloggc:file + +与`-verbose:gc`功能类似,只是将每次GC事件的相关情况记录到一个文件中,文件的位置最好在本地,以避免网络的潜在问题。 + 若与verbose命令同时出现在命令行中,则以-Xloggc为准。 + +#### 4 非Stable参数 + +这类参数你一看官网以为不能使用呢,官网给你的建议就是这些参数不稳定,慎用,其实这主要的原因还是因为每个公司的实现都是不一样的,所以就是导致不稳定。但是呢,在实际的使用中却是非常的多的,而且这部分的参数很重要。 + +这些参数大致可以分为三类: + +- 性能参数(Performance Options):用于JVM的性能调优和内存分配控制,如初始化内存大小的设置; +- 行为参数(Behavioral Options):用于改变JVM的基础行为,如GC的方式和算法的选择; +- 调试参数(Debugging Options):用于监控、打印、输出等jvm参数,用于显示jvm更加详细的信息; + +**注意:以下参数都是JDK1.7及以下可以使用。** + +- 性能参数 + +|参数及其默认值| 描述| +|-----|-----| +|-XX:LargePageSizeInBytes=4m |设置用于Java堆的大页面尺寸| +|-XX:MaxHeapFreeRatio=70 |GC后java堆中空闲量占的最大比例| +|-XX:MinHeapFreeRatio=40 | GC后java堆中空闲量占的最小比例| +|**-XX:MaxNewSize=size** | 新生成对象能占用内存的最大值| +|**-XX:MaxPermSize=64m** |老生代对象能占用内存的最大值| +|**-XX:NewRatio=2** |新生代内存容量与老生代内存容量的比例| +|**-XX:NewSize=2.125m** | 新生代对象生成时占用内存的默认值| +|-XX:ReservedCodeCacheSize=32m |保留代码占用的内存容量| +|-XX:ThreadStackSize=512 |设置线程栈大小,若为0则使用系统默认值| +|-XX:+UseLargePages |使用大页面内存| + +- 行为参数 + + +|参数及其默认值 |描述| +|-----|-----| +|-XX:+ScavengeBeforeFullGC |新生代GC优先于Full GC执行| +|-XX:+UseGCOverheadLimit |在抛出OOM之前限制jvm耗费在GC上的时间比例| +|**-XX:-UseParNewGC**| 打开此开关,使用`ParNew+Serial Old`收集器| +|**-XX:-UseConcMarkSweepGC** |使用`ParNew+CMS+Serial Old`收集器对老生代采用并发标记交换算法进行GC| +|**-XX:-UseParallelGC** |启用并行GC,使用`ParallelScavenge+Serial Old`收集器| +|**-XX:-UseParallelOldGC** |对Full GC启用并行,当`-XX:-UseParallelGC`启用时该项自动启用,`ParallelScavenge+Parallel Old`收集器| +|**-XX:-UseSerialGC** |启用串行GC| +|**-XX:+UseG1GC**| 使用垃圾优先(G1)收集器| +|-XX:SurvivorRatio=n |Eden区域与Survivor区域大小之比。预设值为8| +|-XX:PretenureSizeThreshold=n|直接晋升到老年代的**对象大小**,设置这个参数之后,大于这个参数的对象直接进入到老年代分配| +|-XX:MaxTenuringThreshold=n|晋升到老年代的**对象年龄**,每个对象在坚持过一次Minor GC之后,年龄加1,当超过这个值之后就进入老年代。预设值为15| +|-XX:+UseAdaptiveSizePolicy|动态调整Java堆中各个区域的大小以及进入老年代的年龄| +| -XX:ParallelGCThreads=n|设置并行收集器收集时使用的CPU数。并行收集线程数| +|-XX:MaxGCPauseMillis=n|设置并行收集最大暂停时间| +|-XX:GCTimeRatio=n|设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+N)| +|-XX:+UseThreadPriorities |启用本地线程优先级| +|-XX:-DisableExplicitGC |禁止调用`System.gc()`;但jvm的gc仍然有效| +|-XX:+MaxFDLimit |最大化文件描述符的数量限制| + +前面6个参数都是关于**垃圾收集器**的行为参数,也是经常会用到的参数。 + +- 调试参数 + +|参数及其默认值| 描述| +|-----|-----| +|-XX:-CITime | 打印消耗在JIT编译的时间| +|-XX:ErrorFile=./hs_err_pid\.log |保存错误日志或者数据到文件中| +|-XX:HeapDumpPath=./java_pid\.hprof |指定导出堆信息时的路径或文件名| +|**-XX:-HeapDumpOnOutOfMemoryError** |当首次遭遇OOM时导出此时堆中相关信息| +|-XX:OnError="\;\" |出现致命ERROR之后运行自定义命令| +|-XX:OnOutOfMemoryError="\;\" |当首次遭遇OOM时执行自定义命令| +|-XX:-PrintClassHistogram |遇到Ctrl-Break后打印类实例的柱状信息,与`jmap -histo`功能相同| +|-XX:-PrintConcurrentLocks |遇到Ctrl-Break后打印并发锁的相关信息,与`jstack -l`功能相同| +|-XX:-PrintCommandLineFlags |打印在命令行中出现过的标记| +|-XX:-PrintCompilation |当一个方法被编译时打印相关信息| +|**-XX:-PrintGC** |每次GC时打印相关信息| +|**-XX:-PrintGCDetails** |每次GC时打印详细信息| +|-XX:-PrintGCTimeStamps |打印每次GC的时间戳| +|-XX:-TraceClassLoading |跟踪类的加载信息| +|-XX:-TraceClassLoadingPreorder |跟踪被引用到的所有类的加载信息| +|-XX:-TraceClassResolution | 跟踪常量池| +|-XX:-TraceClassUnloading | 跟踪类的卸载信息| +|-XX:-TraceLoaderConstraints | 跟踪类加载器约束的相关信息| + +以上标黑的就是常用的一些参数。 + +最后,给大家一个实例,看看在工作中是怎么去运用这些参数的,怎么在工作中去解决这些问题的。 + +#### **参数实例** + +设置`-Xms`、`-Xmn`和`-Xmx`参数分别为`-Xms512m -Xmx512m -Xmn128m`。同时设置新生代和老生代之比为1:4,E:S0:S1=8:1:1。除此之外,当然,你还可以设置一些其他的参数,比如,前面说到的,性能参数 `-XX:MaxNewSize`、 `-XX:NewRatio=`、,行为参数 `-XX:-UseParNewGC`,调试参数 `-XX:-PrintGCDetails`。 + +这些参数都是可以在 IDEA 中启动时直接设置的。 + +``` +** + * @ClassName MethodTest + * @Description vm参数设置:-Xms512m -Xmx512m -Xmn128m -XX:NewRatio=4 -XX:SurvivorRatio=8 + * @Author 欧阳思海 + * @Date 2019/11/25 20:06 + * @Version 1.0 + **/ + +public class MethodTest { + + public static void main(String[] args) { + List list = new ArrayList(); + long i = 0; + while (i < 1000000000) { + System.out.println(i); + list.add(String.valueOf(i++).intern()); + } + } +} +``` + +运行之后,用VisualVM查看相关信息是否正确。 + +当我们**没有设置**`-XX:NewRatio=4 -XX:SurvivorRatio=8`时,使用官方默认的情况如下: + +![](http://image.ouyangsihai.cn/FgIr8Niw2F9Cs1IK6ABn74oEi66T) + +上图可以看出,**新生代(Eden Space + Survivor 0 + Survivor 1):老年代(Old Gen)≈ 1:2**。 + +当我们**设置了**`-XX:NewRatio=4 -XX:SurvivorRatio=8`时,情况如下: + +![](http://image.ouyangsihai.cn/Fkc4DTkISF1LA9fpO54VEinaYvlc) + +变成了**新生代(Eden Space + Survivor 0 + Survivor 1):老年代(Old Gen)≈ 1:4**,Eden Space:Survivor 0: Survivor 1 = 8:1:1。 + +![](http://image.ouyangsihai.cn/FphOpfRSOQYu-pcR6xf6rDPcQAF9) + +从上图可知,堆的信息是正确的。 + +更多的使用方法可以参考这篇文章 [如何利用VisualVM对高并发项目进行性能分析](http://www.java1000.com/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html),有更加细致的介绍。 + +> 说说你了解的常见的内存调试工具:jps、jmap、jhat等。 + +|名称|解释| +|-----|-----| +|jps|显示指定系统内所有的HotSpot虚拟机的进程| +|jstat|用于收集HotSpot虚拟机各方面的运行数据| +|jinfo|显示虚拟机配置信息| +|jmap|生成虚拟机的内存转存储快照(heapdump文件),利用这个文件就可以分析内存等情况| +|jhat|用于分析上面jmap生成的heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果| +|jstack|显示虚拟机的线程快照| + +#### 1 jps:虚拟机进程状况工具 + +这个工具使用的频率还是非常高的,因为你需要查看你的程序的运行情况的时候,首先需要知道的就是程序所运行的**进程本地虚拟机唯一ID(LVMID)**,知道这个ID之后,才可以进行其他监控的操作。 + +- 命令格式: + +``` +jps [选项] [主机id] +``` + +- jps主要选项: + +|选项|解释| +|-----|-----| +|-q|只输出LVMID,省略主类名称| +|-m|输出虚拟机进程启动时传递给主类main()函数的参数| +|-l|输出主类的全名| +|-v|输出虚拟机进程启动时的 JVM 参数| + + + +- 实例 + +``` +jps -l +jps -v +``` +![](http://image.ouyangsihai.cn/Fq2nwxo46q4EpQ08Ba_qKIU2JN-J) + +![](http://image.ouyangsihai.cn/FmEvrlfb6kTEk8u_Hc-NcGHg8wW9) + +#### 2 jinfo:Java配置信息工具 + +jinfo的功能很简单,主要就是显示和查看调整虚拟机的各种参数。 + +- jinfo命令格式: + +``` +jinfo [选项] pid +``` + +- 相关选项 + +![](http://image.ouyangsihai.cn/FpSz04ZVzS6K6FJ3zVtnoYqMdLt7) + + +- 实例 + +我们在启动程序的时候设置一些JVM参数:`-Xms512m -Xmx512m -Xmn128m -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15`。 + +我们先使用`jps -l`查看虚拟机进程ID; +![](http://image.ouyangsihai.cn/FtsaKW9WE0lT-Vn_OH4CjpqWXqwz) + +再使用pid为1584进行查询参数; + +![](http://image.ouyangsihai.cn/FgOjSz1U0kS2yt2V-k_huM-f0zJ_) + +#### 3 jmap:Java内存映射工具 + +jmap的主要功能就是生成**堆转存储快照**,之后,我们再利用这个快照文件进行分析。 + +- jmap命令格式: + +``` +jmap [选项] vmid +``` + +- 相关选项 + +|选项|解释| +|-----|-----| +|-dump|生成Java堆转存储快照,格式:`-dump:[live,]format=b,file=`,其中`live`子参数说明是否只dump出存活的对象| +|-finalizerinfo|显示在F-Queue中等待Finalizer线程执行finalize方法的对象,**只在linux/Solaris有效**| +|-heap|显示Java堆详细信息,如使用哪种回收期、参数配置、分代状况等等。**只在Linux/Solaris有效**| +|-histo|显示堆中对象统计信息,包括类、实例数量、合计容量| +|-permstat|以ClassLoader为统计口径显示永久代内存状态,**只在Linux/Solaris有效**| +|-F|当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照,**只在Linux/Solaris有效**| + +- 实例 + +首先还是查看虚拟机ID; + +![](http://image.ouyangsihai.cn/FtuNhdGkitnyOGWFegoT2ETIOidf) + +然后再运行下面命令行; +![](http://image.ouyangsihai.cn/Fvi9c3BD9bSt4Qi1ptDkHaVH6jA1) + +打开这个dump文件如下; +![](http://image.ouyangsihai.cn/Fvd2a3KTLCykCyBX7fAosH65tvCw) + +ok,现在已经有了生成的dump文件,所以,我们就需要对这个文件进行解析,看看**jhat命令**。 + +#### 4 jhat:虚拟机堆转存储快照分析工具 + +虽然好像这个命令挺牛逼,但是,其实,由于这个工具的功能不太够,分析也得不到太多的结果,所以我们一般可以会将得到的dump文件用其他的工具进行分析,比如,可视化工具VisualVM等等。 + +所以,这里简单的做一个例子。 +- 实例 + +``` +jhat D:\dump.bin +``` + +![](http://image.ouyangsihai.cn/FsLOf6D2M9Ta8YiA9DCJ-zj6I-_X) + +如果有兴趣查看如何利用VisualVM进行查看,可以查看这篇文章:[深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html)。 + +#### 5 jstack:Java堆栈跟踪工具 + +`jstack` 命令主要用于生成虚拟机当前时刻的**线程快照**。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的**主要目的**:定位线程出现长时间停顿的原因、请求外部资源导致的长时间等待等这些原因。 + +- jstack命令格式: + +``` +jstack [选项] vmid +``` + +- 相关选项 + +|选项|解释| +|-----|-----| +|-F|当正常输出的请求不被响应时,强制输出线程堆栈| +|-l|除堆栈外,显示关于锁的附加信息| +|-m|如果调用到本地方法的话,可以显示C/C++的堆栈| + +- 实例 + +``` +jstack -l 6708 +``` +![](http://image.ouyangsihai.cn/FrcFS7iWpybtIg48OoDjrdmsSDJa) + + +#### 6 jstat:虚拟机统计信息监视工具 + +jstat这个工具还是很有作用的,他可以显示本地或者远程**虚拟机进程中的类装载、内存、垃圾收集、JIT编译**等运行数据,在服务器上,他是运行期定位虚拟机性能问题的首选工具。 + +- jstat命令格式: + +``` +jstat [选项 vmid [interval[s|ms] [count]]] +``` + +- 相关选项 + +|选项|解释| +|-----|-----| +|**-class**|监视类装载、卸载数量、总空间以及类装载所耗费的时间| +|**-gc**|监视Java堆状况,包括Eden区、两个Survivor区、老年代、永久代等容量、已用空间、GC时间合计等信息| +|-gccapacity|监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间| +|**-gcutil**|监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比| +|-gccause|与-gcutil功能一样,但是会输出额外导致上一次GC产生的原因| +|-gcnew|监视新生代GC状况| +|-gcnewcapacity|监控内容与-gcnew一样,输出主要关注使用到的最大最小空间| +|-gcold|监控老年代GC状况| +|-gcoldcapacity|监控内容与-gcold一样,输出主要关注使用到的最大最小空间| +|-gcpermcapacity|输出永久代使用到的最大最小空间| +|-compiler|输出JIT编译器编译过的方法、耗时等信息| +|-printcompilation|输出已经被JIT编译过的方法| + +- 实例 + + +我们这里还关注一下虚拟机GC的状况。 + +``` +jstat -gcutil 9676 +``` +![](http://image.ouyangsihai.cn/FkelCR8JIup_jpEFY9UQvqD2l0EC) + +上面参数的含义: +>S0:第一个Survivor区的空间使用(%)大小 +S1:第二个Survivor区的空间使用(%)大小 +E:Eden区的空间使用(%)大小 +O:老年代空间使用(%)大小 +M:方法区空间使用(%)大小 +CCS:压缩类空间使用(%)大小 +YGC:年轻代垃圾回收次数 +YGCT:年轻代垃圾回收消耗时间 +FGC:老年代垃圾回收次数 +FGCT:老年代垃圾回收消耗时间 +GCT:垃圾回收消耗总时间 + +了解了这些参数的意义之后,我们就可以对虚拟机GC状况进行分析。我们发现年轻代回收次数`12`次,使用时间`1.672`s,老年代回收`0`次,使用时间`0`s,所有GC总耗时`1.672`s。 + +通过以上参数分析,发现老年代Full GC没有,说明没有大对象进入到老年代,整个老年代的GC情况还是不错的。另外,年轻代回收次数`12`次,使用时间`1.672`s,每次用时100ms左右,这个时间稍微长了一点,可以将新生代的空间调低一点,以降低每一次的GC时间。 + +> 常见的垃圾回收器有哪些? + +先上一张图,这张图是Java虚拟机的jdk1.7及以前版本的所有垃圾回收器,也可以说是比较成熟的垃圾回收器,除了这些垃圾回收器,面试的时候最多也就再怼怼G1和ZGC了。 + + + +上面的表示是年轻代的垃圾回收器:Serial、ParNew、Parallel Scavenge,下面表示是老年代的垃圾回收器:CMS、Parallel Old、Serial Old,以及不分老年代和年轻代的G1。之间的相互的连线表示可以相互配合使用。 + +说完是不是一篇明朗,其实也就是那么回事。 + +#### 新生代垃圾回收器 + +##### Serial + +Serial(串行)收集器是**最基本、发展历史最悠久**的收集器,它是采用**复制算法**的新生代收集器,曾经(JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。它是一个**单线程收集器**,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。 + +其实对于这个垃圾回收器,你只要记住是一个**单线程、采用复制算法的,会进行“Stop The World”** 即可,因为面试官一般不问这个,为什么,因为太简单了,没什么可问的呗。 + +好了,再放一张图好吧,说明一下Serial的回收过程,完事。 + + + +说明:这张图的意思就是**单线程,新生代使用复制算法标记、老年代使用标记整理算法标记**,就是这么简单。 + +##### ParNew + +**ParNew收集器就是Serial收集器的**多线程**版本**,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同。 + +需要注意一点是:**除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作。** + +最后再放一张回收过程图; + +![](http://image.ouyangsihai.cn/FvsKnXGzEQd6WYdUmIgLcWoSHG4H) + +*** 是不是很简单,我在这里讲这些知识点并不是为了深入去了解这些原理,基本的知道对于工作已经够了,其实,主要还是应付面试官,哈哈。 + +##### Parallel Scavenge + +Parallel Scavenge收集器也是一个**并行**的**多线程**新生代收集器,它也使用**复制算法**。 + +Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。 + +这里需要注意的唯一的区别是:Parallel Scavenge收集器的目标是**达到一个可控制的吞吐量(Throughput)**。 + +我们知道,停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而**高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务**。 + +#### 老年代垃圾回收器 + +##### Serial Old + +Serial Old 是Serial收集器的老年代版本,它同样是一个**单线程**收集器,使用“**标记-整理**”(Mark-Compact)算法。 + +在这里就可以出一个面试题了。 +- 为什么Serial使用的是**复制算法**,而Serial Old使用是**标记-整理**算法? +同一个爸爸,儿子长的天差地别,当然也有啊,哈哈。 + +> + 其实,看了我前面的文章你可能就知道了,因为在新生代绝大多数的内存都是会被回收的,所以留下来的需要回收的垃圾就很少了,所以复制算法更合适,你可以发现,基本的老年代的都是使用标记整理算法,当然,CMS是个杂种哈。 + + +它的工作流程与Serial收集器相同,下图是Serial/Serial Old配合使用的工作流程图: + + + +##### Parallel Old + +Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用**多线程**和“**标记-整理**”算法,是不是前面说的,老年代出了杂种CMS不是“**标记-整理**”算法,其他都是。 + +另外,有了Parallel Old垃圾回收器后,就出现了以“**吞吐量优先**”著称的“男女朋友”收集器了,这就是:**Parallel Old和Parallel Scavenge收集器的组合**。 + +Parallel Old收集器的工作流程与Parallel Scavenge相同,这里给出Parallel Scavenge/Parallel Old收集器配合使用的流程图: + + + +你是不是以为我还要讲CMS和G1,我任性,我就是要接着讲,哈哈哈。 + + +#### CMS + +##### 小伙子,你说一下 CMS 垃圾回收器吧! + +这个题目一来,吓出一身冷汗,差点就没有复习这个CMS,还好昨晚抱佛脚看了一下哈。 + +于是我。。。一顿操作猛如虎。 + +CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是基于“标记-清除”算法实现的,并且常见的应用场景是**互联网站或者B/S系统的服务端上的Java应用**。 + +结果就一紧张就记得这么多,面试官肯定不满意了,这个时候,面试官的常规操作是,**继续严刑拷打,他想,你可能忘记了,我来提醒提醒你!** + +##### CMS收集器工作的整个流程是怎么样的,你能给我讲讲吗? + +这个时候,面试官还会安慰你说不用紧张,但是,安慰归安慰,最后挂不挂可是另一回事。 + +于是,我又开始回答问题。 + +CMS 处理过程有七个步骤: + +- **初始标记**,会导致stw; +- **并发标记**,与用户线程同时运行; +- **预清理**,与用户线程同时运行; +- **可被终止的预清理**,与用户线程同时运行; +- **重新标记** ,会导致swt; +- **并发清除**,与用户线程同时运行; + +其实,只要回答四个就差不多了,是这几个。 + +- **初始标记**:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。 +- **并发标记**:进行GC Roots Tracing的过程,在整个过程中耗时最长。 +- **重新标记**:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。 +- **并发清除**。 + +你以为这样子就可以了,面试官就会说可以了,如果可以了,那估计你凉了! + +##### 面试官说:CMS这么好,那有没有什么缺点呢? + +我。。。好吧,谁怪我这么强呢,对吧。 + +其实,CMS虽然经过这么些年的考验,已经是一个值得信赖的GC回收器了,但是,其实也是有一些他的不足的, + +第一,**垃圾碎片的问题**,我们都知道CMS是使用的是**标记-清除**算法的,所以不可避免的就是会出现垃圾碎片的问题。 +第二,**一般CMS的GC耗时80%都在remark阶段,remark阶段停顿时间会很长**,在CMS的这四个主要的阶段中,最费时间的就是重新标记阶段。 +第三,**concurrent mode failure**,说出这个的时候,面试官就会觉得,小伙子,哎呦,不错哟,掌握的比较清楚,那这个是什么意思呢,其实是说: + +>这个异常发生在cms正在回收的时候。执行CMS GC的过程中,同时业务线程也在运行,当年轻带空间满了,执行ygc时,需要将存活的对象放入到老年代,而此时老年代空间不足,这时CMS还没有机会回收老年带产生的,或者在做Minor GC的时候,新生代救助空间放不下,需要放入老年代,而老年代也放不下而产生的。 + +第四,**promotion failed**,这个问题是指,在进行Minor GC时,Survivor空间不足,对象只能放入老年代,而此时老年代也放不下造成的,多数是由于老年代有足够的空闲空间,但是由于碎片较多,新生代要转移到老年带的对象比较大,找不到一段连续区域存放这个对象导致的。 + +面试官看到你掌握的这么好,心里已经给你竖起来大拇指,但是,面试官觉得你优秀啊,就还想看看你到底还有多少东西。 + +##### 既然你知道有这么多的缺点,那么你知道怎么解决这些问题吗? + +这个真的被问蒙了,你以为我什么都会吗!!!! + +但是,我还是得给大家讲讲,不然下次被问到,可能会把锅甩给我。 + +- **垃圾碎片的问题**:针对这个问题,这时候我们需要用到这个参数:`-XX:CMSFullGCsBeforeCompaction=n` 意思是说在上一次CMS并发GC执行过后,到底还要再执行多少次`full GC`才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 + +- **concurrent mode failure** + +解决这个问题其实很简单,只需要设置两个参数即可 + +`-XX:+UseCMSInitiatingOccupancyOnly` +`-XX:CMSInitiatingOccupancyFraction=60`:是指设定CMS在对内存占用率达到60%的时候开始GC。 + +为什么设置这两个参数呢?由于在垃圾收集阶段用户线程还需要运行,那也就还需要**预留有足够的内存空间给用户线程使用**,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集。 + +当然也不能设置过高,比如90%,这时候虽然GC次数少,但是,却会导致用于用户线程空间小,效率不高,太低10%,你自己想想会怎么样,体会体会! + +**哈哈,万事大吉,这一点说出了,估计面试官已经爱上我了吧,赶紧把我招进去干活吧。** + +- **remark阶段停顿时间会很长的问题**:解决这个问题巨简单,加入`-XX:+CMSScavengeBeforeRemark`。在执行remark操作之前先做一次`Young GC`,目的在于减少年轻代对老年代的无效引用,降低remark时的开销。 + +#### G1 (Garbage-First) + +G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。 + +- 可以像CMS收集器一样, GC 操作与应用的线程一起并发执行 +- 紧凑的空闲内存区间且没有很长的 GC 停顿时间 +- 需要可预测的GC暂停耗时 +- 不想牺牲太多吞吐量性能. +- 启动后不需要请求更大的Java堆 + +那么 G1 相对于 CMS 的区别在: + +- G1 在压缩空间方面有优势 + +- G1 通过将内存空间分成区域(Region)的方式避免内存碎片问题 + +- Eden, Survivor, Old 区不再固定、在内存使用效率上来说更灵活 + +- G1 可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象 + +- G1 在回收内存后会马上同时做合并空闲内存的工作、而 CMS 默认是在STW(stop the world)的时候做 + +- G1 会在 Young GC 中使用,而 CMS 只能在O区使用 + +就目前而言、CMS 还是默认首选的 GC 策略、可能在以下场景下 G1 更适合: + +- 服务端多核 CPU、JVM 内存占用较大的应用(至少大于4G) + +- 应用在运行过程中会产生大量内存碎片、需要经常压缩空间 + +- 想要更可控、可预期的 GC 停顿周期;防止高并发下应用雪崩现象 + +G1的内存使用示意图: +![](http://image.ouyangsihai.cn/FgFGLjZMCGmjKscUAj3hBfK1HLKM) + +G1在运行过程中主要包含如下4种操作方式: + +- YGC(不同于CMS) + +- 并发阶段 + +- 混合模式 + +- full GC (一般是G1出现问题时发生) + +##### YGC(年轻代GC) +下面是一次 YGC 前后内存区域是示意图: +![](http://image.ouyangsihai.cn/FjX3b5ZidxSGhDRF-qiJ4SXm-ji5) + +图中每个小区块都代表 G1 的一个区域(Region),区块里面的字母代表不同的分代内存空间类型(如[E]Eden,[O]Old,[S]Survivor)空白的区块不属于任何一个分区;G1 可以在需要的时候任意指定这个区域属于 Eden 或是 O 区之类的。 + +YoungGC 在 Eden 充满时触发,在回收之后所有之前属于 Eden 的区块全变成空白。然后至少有一个区块是属于 S 区的(如图半满的那个区域),同时可能有一些数据移到了 O 区。 + +目前大都使用 PrintGCDetails 参数打出GC日志、这个参数对G1同样有效、但日志内容颇为不同。 + +下面是一个Young GC的例子: +``` +23.430: [GC pause (young), 0.23094400 secs] +... +[Eden: 1286M(1286M)->0B(1212M) +Survivors: 78M->152M Heap: 1454M(4096M)->242M(4096M)] +[Times: user=0.85 sys=0.05, real=0.23 secs] +``` +上面日志的内容解析:Young GC实际占用230毫秒、其中GC线程占用850毫秒的CPU时间 + +- E:内存占用从 1286MB 变成 0、都被移出 +- S:从 78M 增长到了 152M、说明从 Eden 移过来 74M +- Heap: 占用从 1454 变成 242M、说明这次 Young GC 一共释放了 1212M 内存空间 + +很多情况下,S 区的对象会有部分晋升到 Old 区,另外如果 S 区已满、Eden 存活的对象会直接晋升到 Old 区,这种情况下 Old 的空间就会涨。 + +##### 并发阶段 +一个并发G1回收周期前后内存占用情况如下图所示: +![](http://image.ouyangsihai.cn/Flo526oI2G_UIbkT-Jibo5ZoX27u) + +从上面的图表可以看出以下几点: + +- Young 区发生了变化、这意味着在 G1 并发阶段内至少发生了一次 YGC(这点和 CMS 就有区别),Eden 在标记之前已经被完全清空,因为在并发阶段应用线程同时在工作、所以可以看到 Eden 又有新的占用 +- 一些区域被X标记,这些区域属于 O 区,此时仍然有数据存放、不同之处在 G1 已标记出这些区域包含的垃圾最多、也就是回收收益最高的区域 +- 在并发阶段完成之后实际上 O 区的容量变得更大了(O+X 的方块)。这时因为这个过程中发生了 YGC 有新的对象进入所致。此外,这个阶段在 O 区没有回收任何对象:它的作用主要是标记出垃圾最多的区块出来。对象实际上是在后面的阶段真正开始被回收 + +G1 并发标记周期可以分成几个阶段、其中有些需要暂停应用线程。第一个阶段是初始标记阶段。这个阶段会暂停所有应用线程-部分原因是这个过程会执行一次 YGC、下面是一个日志示例: +``` +50.541: [GC pause (young) (initial-mark), 0.27767100 secs] +[Eden: 1220M(1220M)->0B(1220M) +Survivors: 144M->144M Heap: 3242M(4096M)->2093M(4096M)] +[Times: user=1.02 sys=0.04, real=0.28 secs] +``` +上面的日志表明发生了 YGC 、应用线程为此暂停了 280 毫秒,Eden 区被清空(71MB 从 Young 区移到了 O 区)。 + +日志里面 initial-mark 的字样表明后台的并发 GC 阶段开始了。因为初始标记阶段本身也是要暂停应用线程的,G1 正好在 YGC 的过程中把这个事情也一起干了。为此带来的额外开销不是很大、增加了 20% 的 CPU ,暂停时间相应的略微变长了些。 + +接下来,G1 开始扫描根区域、日志示例: +``` +50.819: [GC concurrent-root-region-scan-start] +51.408: [GC concurrent-root-region-scan-end, 0.5890230] +``` +一共花了 580 毫秒,这个过程没有暂停应用线程;是后台线程并行处理的。这个阶段不能被 YGC 所打断、因此后台线程有足够的 CPU 时间很关键。如果 Young 区空间恰好在 Root 扫描的时候满了、YGC 必须等待 root 扫描之后才能进行。带来的影响是 YGC 暂停时间会相应的增加。这时的 GC 日志是这样的: +``` +350.994: [GC pause (young) +351.093: [GC concurrent-root-region-scan-end, 0.6100090] +351.093: [GC concurrent-mark-start],0.37559600 secs] +``` +GC 暂停这里可以看出在 root 扫描结束之前就发生了,表明 YGC 发生了等待,等待时间大概是100毫秒。 + +在 root 扫描完成后,G1 进入了一个并发标记阶段。这个阶段也是完全后台进行的;GC 日志里面下面的信息代表这个阶段的开始和结束: +``` +111.382: [GC concurrent-mark-start] +.... +120.905: [GC concurrent-mark-end, 9.5225160 sec] +``` +并发标记阶段是可以被打断的,比如这个过程中发生了 YGC 就会。这个阶段之后会有一个二次标记阶段和清理阶段: +``` +120.910: [GC remark 120.959: +[GC ref-PRC, 0.0000890 secs], 0.0718990 secs] +[Times: user=0.23 sys=0.01, real=0.08 secs] +120.985: [GC cleanup 3510M->3434M(4096M), 0.0111040 secs] +[Times: user=0.04 sys=0.00, real=0.01 secs] +``` +这两个阶段同样会暂停应用线程,但时间很短。接下来还有额外的一次并发清理阶段: +``` +120.996: [GC concurrent-cleanup-start] +120.996: [GC concurrent-cleanup-end, 0.0004520] +``` +到此为止,正常的一个 G1 周期已完成–这个周期主要做的是发现哪些区域包含可回收的垃圾最多(标记为 X ),实际空间释放较少。 + +##### 混合 GC + +接下来 G1 执行一系列的混合 GC。这个时期因为会同时进行 YGC 和清理上面已标记为 X 的区域,所以称之为混合阶段,下面是一个混合 GC 执行的前后示意图: +![](http://image.ouyangsihai.cn/Flo526oI2G_UIbkT-Jibo5ZoX27u) + +像普通的 YGC 那样、G1 完全清空掉 Eden 同时调整 survivor 区。另外,两个标记也被回收了,他们有个共同的特点是包含最多可回收的对象,因此这两个区域绝对部分空间都被释放了。这两个区域任何存活的对象都被移到了其他区域(和 YGC 存活对象晋升到 O 区类似)。这就是为什么 G1 的堆比 CMS 内存碎片要少很多的原因——移动这些对象的同时也就是在压缩对内存。下面是一个混合GC的日志: +``` +79.826: [GC pause (mixed), 0.26161600 secs] +.... +[Eden: 1222M(1222M)->0B(1220M) +Survivors: 142M->144M Heap: 3200M(4096M)->1964M(4096M)] +[Times: user=1.01 sys=0.00, real=0.26 secs] +``` +上面的日志可以注意到 Eden 释放了 1222 MB、但整个堆的空间释放内存要大于这个数目。数量相差看起来比较少、只有 16 MB,但是要考虑同时有 survivor 区的对象晋升到 O 区;另外,每次混合 GC 只是清理一部分的 O 区内存,整个 GC 会一直持续到几乎所有的标记区域垃圾对象都被回收,这个阶段完了之后 G1 会重新回到正常的 YGC 阶段。周期性的,当O区内存占用达到一定数量之后 G1 又会开启一次新的并行 GC 阶段. + +G1来源:https://blog.51cto.com/lqding/1770055 + +### 总结 + +这里把上面的这些垃圾回收器做个总结,看完这个,面试给面试官讲的时候思路就非常清晰了。 + +|收集器|串行、并行or并发|新生代/老年代|算法|目标|适用场景| +|------|------|------|------|------|------| +|**Serial**|串行|新生代|复制算法|响应速度优先|单CPU环境下的Client模式 +|**Serial Old**|串行|老年代|标记-整理|响应速度优先|单CPU环境下的Client模式、CMS的后备预案 +|**ParNew**|并行|新生代|复制算法|响应速度优先|多CPU环境时在Server模式下与CMS配合 +|**Parallel Scavenge**|并行|新生代|复制算法|吞吐量优先|在后台运算而不需要太多交互的任务 +|**Parallel Old**|并行|老年代|标记-整理|吞吐量优先|在后台运算而不需要太多交互的任务 +|**CMS**|并发|老年代|标记-清除|一种以获取最短回收停顿时间为目标的收集器|互联网站或者B/S系统的服务端上的Java应用 +|**G1**|并发|老年代|标记-整理|高吞吐量|面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器 + + +> 内存泄漏和内存溢出,什么时候会出现,怎么解决? + +内存泄漏:(Memory Leak) 不再会被使用的对象的内存不能被回收,就是内存泄露。 + +强引用所指向的对象不会被回收,可能导致内存泄漏,虚拟机宁愿抛出OOM也不会去回收他指向的对象。 + +意思就是你用资源的时候为他开辟了一块空间,当你用完时忘记释放资源了,这时内存还被占用着,一次没关系,但是内存泄漏次数多了就会导致内存溢出。 + +内存溢出:(Out Of Memory —— OOM) 指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储 int 类型数据的存储空间,但是你却存储 long 类型的数据,那么结果就是内存不够用,此时就会报错 OOM ,即所谓的内存溢出,简单来说就是自己所需要使用的空间比我们拥有的内存大内存不够使用所造成的内存溢出。 + +内存的释放:即清理那些不可达的对象,是由 GC 决定和执行的,所以 GC 会监控每一个对象的状态,包括申请、引用、被引用和赋值等。释放对象的根本原则就是对象不会再被使用。 + +#### 内存泄漏的原因 + +- 静态集合类引起内存泄漏; + +- 当集合里面的对象属性被修改后,再调用remove()方法时不起作用。JDK1.8 貌似修正了引用对象修改参数,导致hashCode变更的问题; + +- 监听器 Listener 各种连接 Connection,没有及时关闭; + +- 内部类和外部模块的引用(尽量使用静态内部类); + +- 单例模式(静态类持有引用,导致对象不可回收); + +#### 解决方法 + +- 尽早释放无用对象的引用,及时关闭使用的资源,数据库连接等; +- 特别注意一些像 HashMap 、ArrayList 的集合对象,它们经常会引发内存泄漏。当它们被声明为 static 时,它们的生命周期就会和应用程序一样长。 +- 注意 事件监听 和 回调函数 。当一个监听器在使用的时候被注册,但不再使用之后却未被反注册。 + +#### 内存溢出的情况和解决方法 + +- OOM for Heap (java.lang.OutOfMemoryError: Java heap space) + +此 OOM 是由于 JVM 中 heap 的最大值不满足需要,将设置 heap 的最大值调高即可,如,-Xmx8G。 + +- OOM for StackOverflowError (Exception in thread "main" java.lang.StackOverflowError) + +如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常。 + +如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。 + +检查程序是否有深度递归。 + +- OOM for Perm (java.lang.OutOfMemoryError: PermGen space) + +调高 Perm 的最大值,即 -XX:MaxPermSize 的值调大。 + +- OOM for GC (java.lang.OutOfMemoryError: GC overhead limit exceeded) + +此 OOM 是由于 JVM 在 GC 时,对象过多,导致内存溢出,建议调整 GC 的策略,在一定比例下开始 GC 而不要使用默认的策略,或者将新代和老代设置合适的大小,需要进行微调存活率。 + +改变 GC 策略,在老代 80% 时就是开始 GC ,并且将`-XX:SurvivorRatio(-XX:SurvivorRatio=8)`和`-XX:NewRatio(-XX:NewRatio=4)`设置的更合理。 + +- OOM for native thread created (java.lang.OutOfMemoryError: unable to create new native thread) + +将 heap 及 perm 的最大值下调,并将线程栈调小,即 -Xss 调小,如:-Xss128k。 + +在 JVM 内存不能调小的前提下,将 -Xss 设置较小,如:-Xss:128k。 + +- OOM for allocate huge array (Exception in thread "main": java.lang.OutOfMemoryError: Requested array size exceeds VM limit) + +此类信息表明应用程序试图分配一个大于堆大小的数组。例如,如果应用程序 new 一个数组对象,大小为 512M,但是最大堆大小为 256M,因此 OutOfMemoryError 会抛出,因为数组的大小超过虚拟机的限制。 +1) 首先检查 heap 的 -Xmx 是不是设置的过小; +2) 如果 heap 的 -Xmx 已经足够大,那么请检查应用程序是不是存在 bug,例如:应用程序可能在计算数组的大小时,存在算法错误,导致数组的 size 很大,从而导致巨大的数组被分配。 + +- OOM for small swap (Exception in thread "main": java.lang.OutOfMemoryError: request bytes for . Out of swap space? ) + +由于从 native 堆中分配内存失败,并且堆内存可能接近耗尽。 + +1) 检查 os 的 swap 是不是没有设置或者设置的过小; +2) 检查是否有其他进程在消耗大量的内存,从而导致当前的 JVM 内存不够分配。 + +- OOM for exhausted native memory (java.lang.OutOfMemoryErr java.io.FileInputStream.readBytes(Native Method)) + +从错误日志来看,在 OOM 后面没有提示引起 OOM 的原因,进一步查看 stack trace 发现,导致 OOM 的原因是由Native Method 的调用引起的,另外检查 Java heap ,发现 heap 的使用正常,因而需要考虑问题的发生是由于 Native memory 被耗尽导致的。 + +从根本上来说,解决此问题的方法应该通过检测发生问题时的环境下,native memory 为什么被占用或者说为什么 native memory 越来越小,从而去解决引起 Native memory 减小的问题。但是如果此问题不容易分析时,可以通过以下方法或者结合起来处理。 + +1) cpu 和 os 保证是 64 位的,并且 jdk 也换为 64 位的。 +2) 将 java heap 的 -Xmx 尽量调小,但是保证在不影响应用使用的前提下。 +3) 限制对 native memory 的消耗,比如:将 thread 的 -Xss 调小,并且限制产生大量的线程;限制文件的 io 操作次数和数量;限制网络的使用等等。 + +> Java 的类加载过程 + +JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。 + +![](http://image.ouyangsihai.cn/FpontTFCT65kRlJvGhuMsP9lkRkZ) + + +#### 加载 + +加载过程主要完成三件事情: + +1. 通过类的全限定名来获取定义此类的二进制字节流 +2. 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构 +3. 在堆中生成一个代表此类的 java.lang.Class 对象,作为访问方法区这些数据结构的入口。 + +#### 校验 + +此阶段主要确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。 + +1. 文件格式验证:基于字节流验证。 +2. 元数据验证:基于**_方法区_**的存储结构验证。 +3. 字节码验证:基于方法区的存储结构验证。 +4. 符号引用验证:基于方法区的存储结构验证。 + +#### 准备 + +为类变量分配内存,并将其初始化为默认值。(此时为默认值,在初始化的时候才会给变量赋值)即在方法区中分配这些变量所使用的内存空间。例如: + +```java +public static int value = 123; +``` + +此时在准备阶段过后的初始值为0而不是 123 ;将 value 赋值为 123 的 putstatic 指令是程序被编译后,存放于类构造器方法之中。特例: + +```java +public static final int value = 123; +``` + +此时 value 的值在准备阶段过后就是 123 。 + +#### 解析 + +把类型中的符号引用转换为直接引用。 + +* 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的 Class 文件格式中。 +* 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在 + +主要有以下四种: + +1. 类或接口的解析 +2. 字段解析 +3. 类方法解析 +4. 接口方法解析 + +#### 初始化 + +初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。 + +Java 中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”(加载,验证,准备,自然需要在此之前开始): + +1. 使用 new 关键字实例化对象、访问或者设置一个类的静态字段(被 final 修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。 +2. 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。 +3. 使用 java.lang.reflect 包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。 +4. 虚拟机启动时,用户会先初始化要执行的主类(含有main) +5. jdk 1.7后,如果 java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic 方法句柄,并且这个方法所在类没有初始化,则先初始化。 + +> 聊聊双亲委派机制 + +![](http://image.ouyangsihai.cn/FoFkvh_XGVUdHkp3F6dYBpwE8tgb) + +**工作过程** + +如果一个类加载器收到了类加载器的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每个层次的类加载器都是如此,因此,所有的加载请求最终都会传送到 Bootstrap 类加载器(启动类加载器)中,只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。 + +**优点** + +Java 类随着它的加载器一起具备了一种带有优先级的层次关系. + +例如类 java.lang.Object ,它存放在 rt.jar 之中,无论哪一个类加载器都要加载这个类,最终都是双亲委派模型最顶端的 Bootstrap 类加载器去加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户编写了一个称为 “java.lang.Object” 的类,并存放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,java类型体系中最基础的行为也就无法保证,应用程序也将会一片混乱。 From de13edb3ce98d0ff91366560f6fc3e69ae6b07c3 Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Sun, 13 Feb 2022 12:36:08 +0800 Subject: [PATCH 29/38] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E5=92=8C=E9=A1=B9=E7=9B=AE=E5=AE=9E=E6=88=98=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 ++++++---- ...350\241\250\351\235\242\350\257\225\351\242\230.md" | 6 ++++++ 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 "docs/dataStructures-algorithms/\344\270\200\346\226\207\346\220\236\345\256\232\351\223\276\350\241\250\345\237\272\347\241\200\345\222\214\351\223\276\350\241\250\351\235\242\350\257\225\351\242\230.md" diff --git a/README.md b/README.md index f84ec42..8e5becc 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ 👉 Java学习资源汇总(个人总结) -- Java基础到Java实战全套学习视频教程,包括多个企业级实战项目:https://urlify.cn/YFzABz 密码: pi95 +- Java基础到Java实战全套学习视频教程,包括多个企业级实战项目:https://github.com/hello-go-maker/cs-learn-source - 面试算法资料,这是总结的算法资料,学完基本可以应付80%大厂:https://urlify.cn/N7vIj2 密码: ijoi @@ -233,8 +233,8 @@ ### 算法 - [从大学入门到研究生拿大厂offer,必须看的数据结构与算法书籍推荐,不好不推荐!](https://sihai.blog.csdn.net/article/details/106011624?spm=1001.2014.3001.5502) -- [2020年最新算法面试真题汇总](docs/dataStructures-algorithms/算法面试真题汇总.md) -- [2020年最新算法题型难点总结](docs/dataStructures-algorithms/算法题目难点题目总结.md) +- [2021年面试高频算法题题解](docs/dataStructures-algorithms/高频算法题目总结.md) +- [2021年最新剑指offer难题解析](docs/dataStructures-algorithms/剑指offer难点总结.md) - [关于贪心算法的leetcode题目,这篇文章可以帮你解决80%](https://blog.ouyangsihai.cn/jie-shao-yi-xia-guan-yu-leetcode-de-tan-xin-suan-fa-de-jie-ti-fang-fa.html) - [dfs题目这样去接题,秒杀leetcode题目](https://sihai.blog.csdn.net/article/details/106895319) - [回溯算法不会,这篇文章一定得看](https://sihai.blog.csdn.net/article/details/106993339) @@ -392,19 +392,21 @@ ## Java学习资源 +- [2021年Java视频学习教程+项目实战](https://github.com/hello-go-maker/cs-learn-source) - [2021 Java 1000G 最新学习资源大汇总](https://mp.weixin.qq.com/s/I0jimqziHqRNaIy0kXRCnw) ## Java书籍推荐 - [从入门到拿大厂offer,必须看的数据结构与算法书籍推荐](https://blog.ouyangsihai.cn/cong-ru-men-dao-na-da-han-offer-bi-xu-kan-de-suan-fa-shu-ji-tui-jian-bu-hao-bu-tui-jian.html) +- [全网最全电子书下载](https://github.com/hello-go-maker/cs-books) ## 实战项目推荐 >小心翼翼的告诉你,上面的资源当中就有很多**企业级项目**,没有项目一点不用怕,因为你看到了这个。 - [找工作,没有上的了台面的项目怎么办?](https://mp.weixin.qq.com/s/0oK43_z99pVY9dYVXyIeiw) - +- [Java 实战项目推荐](https://github.com/hello-go-maker/cs-learn-source) ## 程序人生 diff --git "a/docs/dataStructures-algorithms/\344\270\200\346\226\207\346\220\236\345\256\232\351\223\276\350\241\250\345\237\272\347\241\200\345\222\214\351\223\276\350\241\250\351\235\242\350\257\225\351\242\230.md" "b/docs/dataStructures-algorithms/\344\270\200\346\226\207\346\220\236\345\256\232\351\223\276\350\241\250\345\237\272\347\241\200\345\222\214\351\223\276\350\241\250\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..45fdc1a --- /dev/null +++ "b/docs/dataStructures-algorithms/\344\270\200\346\226\207\346\220\236\345\256\232\351\223\276\350\241\250\345\237\272\347\241\200\345\222\214\351\223\276\350\241\250\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,6 @@ +### 链表基础结构 + +### 链表的常见操作 + +### 链表常见面试题 + From b012b6fb2033875a683c59ba4ec2c9f38b4f7c41 Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Sun, 13 Feb 2022 16:40:37 +0800 Subject: [PATCH 30/38] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Java=E7=BB=93=E5=90=88?= =?UTF-8?q?=E9=9D=A2=E8=AF=95=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...10\351\235\242\350\257\225\351\242\230.md" | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git "a/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" "b/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" index e69de29..85a698b 100644 --- "a/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" +++ "b/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,114 @@ +### 集合面试题 + +> ArrayList、LinkedList和Vector的区别和实现原理 + +#### 数据结构实现 + +ArrayList和Vector都是基于可改变大小的数据实现的,而LinkedList是基于双链表实现的。 + +#### 增删改查效率对比 + +ArrayList和Vector都是基于可改变大小的数据实现的,因此,从指定的位置检索对象时,或在集合的末尾插入对象、删除一个对象的时间都是O(1),但是如果在其他位置增加或者删除对象,花费的时间是O(n); + +而LinkedList是基于双链表实现的,因此,在插入、删除集合中的任何位置上的对象,所花费的时间都是O(1),但基于链表的数据结构在查找元素时的效率是更低的,花费的时间为O(n)。 + +因此,从以上分析我们可以知道,查找特定的对象或者在集合末端增加或者删除对象,ArrayList和Vector的效率是ok的,如果在指定的位置删除或者插入,LinkedList的效率则更高。 + +#### 线程安全 + +ArrayList、LinkedList不具有线程安全性,在多线程的问题下是不能使用的,如果想要在多线程的环境下使用怎么办呢?我们可以采用Collections的静态方法synchronizedList包装一下,就可以保证线程安全了,但是在实际情况下,并不会使用这种方式,而是会采用更高级的集合进行线程安全的操作。 + +Vector是线程安全的,其保证线程安全的机制是采用synchronized关键字,我们都知道,这个关键字的效率是不高的,在后续的很多版本中,线程安全的机制都不会采用这种方式,因此,Vector的效率是比ArrayList、LinkedList更低效的。 + +#### 扩容机制 + +ArrayList和Vector都是基于数据这种数据结构实现的,因此,在集合的容量满了时,是需要进行扩容操作的。 + +在扩容时,ArrayList扩容后的容量是原先的1.5倍,扩容后,再将原先的数组中的数据拷贝到新建的数组中。 + +Vector默认情况下,扩容后的容量是原先的2倍,除此之外,Vector还有一种可以设置**容量增量**的机制,在Vector中有capacityIncrement变量用于控制扩容时的增量,具体的规则是:当capacityIncrement大于0时,扩容时增加的大小就是capacityIncrement的大小,如果capacityIncrement小于等于0时,则将容量增加为之前的2倍。 + +> HashMap原理分析 + +在分析HashMap的原理之前,先说明一下,大家应该都知道HashMap在JDK1.7和1.8的实现上是有较大的区别的,而面试官也是非常喜欢考察这一个点,因此,这里也是采用这两个JDK版本对比来进行分析,这样也可以印象更加深刻一些。 + +#### 数据结构 + +在数据结构的实现上,大家应该都知道,JDK1.7是数组+单链表的形式,而1.8采用的是数组+单链表+红黑树,具体的表现如下: + +|版本|数据结构|数组+链表的实现形式|红黑树实现形式| +|-|-|-|-| +|JDK1.8|数组+单链表+红黑树|Node|TreeNode| +|JDK1.7|数组+单链表|Entry|-| + +为了更好的让大家理解后续的讲解,这里先讲解一下HashMap中实现的一些重要参数。 + +- 容量(capacity): HashMap中数组的长度 + - 容量范围:必须是 2 的幂 + - 初始容量 = 哈希表创建时的容量 + - 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的 2^4 = 16 + `static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;` + - 最大容量 = 2的30次方 + `static final int MAXIMUM_CAPACITY = 1 << 30;` + +- 加载因子(Load factor):HashMap在其容量自动增加时,会设置加载因子,当达到设置的值时,就会触发自动扩容。 + - 加载因子越大、填满的元素越多,也就是说,空间利用率高、但冲突的机会加大、查找效率变低 + - 加载因子越小、填满的元素越少,也就是说,空间利用率小、冲突的机会减小、查找效率高 + // 实际加载因子 + `final float loadFactor;` + // 默认加载因子 = 0.75 + `static final float DEFAULT_LOAD_FACTOR = 0.75f;` + +- 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)。 + - 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数 + - 扩容阈值 = 容量 x 加载因子 + +#### 获取数据(get) + +HashMap的获取数据的过程大致如下: + +- 首先,根据key判断是否为空值; +- 如果为空,则到hashmap数组的第1个位置,寻找对应key为null的键; +- 如果不为空,则根据key计算hash值; +- 根据得到的hash值采用`hash & (length - 1)`的计算方式得到key在数组中的位置; +- 结束。 + +以上就是大致的数据获取流程,接下来,我们再对JDK1.7和1.8获取数据的细节做一个对比。 + +|版本|hash值的计算方式| +|-|-| +|JDK1.8|1、hash = (key == null) ? 0 : hash(key);
2、扰动处理 = 2次扰动 = 1次位运算+1次异或运算| +|JDK1.7|1、hash = (key == null) ? 0 : hash(key);
2、扰动处理 = 9次扰动 = 4次位运算+5次异或运算| + +#### 保存数据(put) + +HashMap的保存数据的过程大致如下: + +- 判读HashMap是否初始化,如果没有则进行初始化; +- 判断key是否为null,如果为null,则将key-value的数据存储在数组的第1个位置,这里与获取数据时对应的;否则,进行后续操作; +- 根据key计算数据存放的位置; +- 根据位置判断key是否存在,如果存在,则用新值替换旧值;如果不存在,则直接设置; + +这里也对保存数据的过程进行一个更加细致的对比。 + +|版本|hash值的计算方式|存放数据方式|插入数据方式| +|-|-|-|-| +|JDK1.8|1. hash = (key == null) ? 0 : hash(key);
2. 扰动处理 = 2次扰动 = 1次位运算+1次异或运算|数组+单链表+红黑树
- 无冲突,直接保存数据
- 冲突时,当链表长度小于8时,存放到单链表,当长度大于8时,存到到红黑树|尾插法| +|JDK1.7|1、hash = (key == null) ? 0 : hash(key);
2、扰动处理 = 9次扰动 = 4次位运算+5次异或运算|数组+单链表
- 无冲突,直接保存数据
- 冲突时,存放到单链表|头插法| + +#### 扩容机制 + +HashMap的扩容的过程大致如下: + +- 当发现容量不足时,开始扩容机制; +- 首先,保存旧数组,再根据旧容量的2倍新建数组; +- 遍历旧数组的每个元素,采用头插法的方式,将每个元素保存到新数组; +- 将新数组引用到hashmap的table属性上; +- 重新设置扩容阀值,完成扩容操作。 + +最后,也对扩容的过程进行一个更加细致的对比。 + +|版本|扩容后的位置计算方式|数据转移方式| +|-|-|-| +|JDK1.8|扩容后的位置 = 原位置 or 原位置+旧容量|尾插法| +|JDK1.7|扩容后的位置 = hashCode() -> 扰动处理 -> h & (length - 1)|头插法| From 4748d914b241e6b90656b3dd679a3e0b0b00a211 Mon Sep 17 00:00:00 2001 From: hello-java-maker <1446037005@qq.com> Date: Sun, 19 Jun 2022 09:31:47 +0800 Subject: [PATCH 31/38] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E9=9D=A2=E8=AF=95=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...36\347\216\260\347\211\210\346\234\254.md" | 3 + ...27\346\263\225\345\256\236\347\216\260.md" | 4 + ...5\215\227-Java\345\256\236\347\216\260.md" | 1 + ...72\347\241\200\347\237\245\350\257\206.md" | 1 + docs/java/IO/java IO.md | 1 + ...06\347\202\271\346\200\273\347\273\223.md" | 1 + ...00\345\255\243\347\254\224\350\256\260.md" | 2 + ...11\345\255\243\347\254\224\350\256\260.md" | 1 + ...14\345\255\243\347\254\224\350\256\260.md" | 0 .../Java\345\271\266\345\217\221.md" | 2 + ...04\346\226\231\346\261\207\346\200\273.md" | 25 + .../Dubbo.md" | 516 +++++ .../JavaIO.md" | 949 ++++++++++ .../Java\345\237\272\347\241\200.md" | 1313 +++++++++++++ .../Java\345\271\266\345\217\221.md" | 1478 +++++++++++++++ .../Java\345\274\202\345\270\270.md" | 739 ++++++++ ...va\350\231\232\346\213\237\346\234\272.md" | 627 ++++++ .../Java\351\233\206\345\220\210.md" | 923 +++++++++ .../Linux.md" | 676 +++++++ .../Nginx.md" | 516 +++++ .../elasticSearch.md" | 296 +++ .../kafka.md" | 1 + .../mybatis.md" | 565 ++++++ .../mysql.md" | 1682 +++++++++++++++++ .../netty.md" | 1 + .../rabbitMQ.md" | 358 ++++ .../redis.md" | 774 ++++++++ .../spring mvc.md" | 267 +++ .../spring.md" | 782 ++++++++ .../springboot.md" | 307 +++ .../springcloud.md" | 446 +++++ .../tomcat.md" | 253 +++ .../zookeeper.md" | 438 +++++ ...27\346\234\272\347\275\221\347\273\234.md" | 644 +++++++ ...76\350\256\241\346\250\241\345\274\217.md" | 1620 ++++++++++++++++ 35 files changed, 16212 insertions(+) create mode 100644 "docs/dataStructures-algorithms/\345\211\221\346\214\207offer-Java\345\256\236\347\216\260\347\211\210\346\234\254.md" create mode 100644 "docs/dataStructures-algorithms/\345\267\246\347\245\236\347\233\264\351\200\232 BAT \347\256\227\346\263\225\345\256\236\347\216\260.md" create mode 100644 "docs/dataStructures-algorithms/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227-Java\345\256\236\347\216\260.md" create mode 100644 "docs/java/Basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" create mode 100644 docs/java/IO/java IO.md create mode 100644 "docs/java/JavaFamily/\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" create mode 100644 "docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\200\345\255\243\347\254\224\350\256\260.md" create mode 100644 "docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\211\345\255\243\347\254\224\350\256\260.md" create mode 100644 "docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\272\214\345\255\243\347\254\224\350\256\260.md" create mode 100644 "docs/java/Multithread/Java\345\271\266\345\217\221.md" create mode 100644 "docs/java/collection/\351\233\206\345\220\210\351\235\242\350\257\225\350\265\204\346\226\231\346\261\207\346\200\273.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Dubbo.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/JavaIO.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\237\272\347\241\200.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\271\266\345\217\221.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\274\202\345\270\270.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\350\231\232\346\213\237\346\234\272.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Linux.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Nginx.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/elasticSearch.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/kafka.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mybatis.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mysql.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/netty.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/rabbitMQ.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/redis.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/spring mvc.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/spring.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/springboot.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/springcloud.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/tomcat.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/zookeeper.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" create mode 100644 "docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/\350\256\276\350\256\241\346\250\241\345\274\217.md" diff --git "a/docs/dataStructures-algorithms/\345\211\221\346\214\207offer-Java\345\256\236\347\216\260\347\211\210\346\234\254.md" "b/docs/dataStructures-algorithms/\345\211\221\346\214\207offer-Java\345\256\236\347\216\260\347\211\210\346\234\254.md" new file mode 100644 index 0000000..256f99f --- /dev/null +++ "b/docs/dataStructures-algorithms/\345\211\221\346\214\207offer-Java\345\256\236\347\216\260\347\211\210\346\234\254.md" @@ -0,0 +1,3 @@ +https://blog.csdn.net/weixin_43774841/article/details/112912070 +https://zhuanlan.zhihu.com/p/84481303 +https://zhuanlan.zhihu.com/p/84481166 \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\345\267\246\347\245\236\347\233\264\351\200\232 BAT \347\256\227\346\263\225\345\256\236\347\216\260.md" "b/docs/dataStructures-algorithms/\345\267\246\347\245\236\347\233\264\351\200\232 BAT \347\256\227\346\263\225\345\256\236\347\216\260.md" new file mode 100644 index 0000000..8c53251 --- /dev/null +++ "b/docs/dataStructures-algorithms/\345\267\246\347\245\236\347\233\264\351\200\232 BAT \347\256\227\346\263\225\345\256\236\347\216\260.md" @@ -0,0 +1,4 @@ +https://juejin.cn/post/6844903779289006094 +https://juejin.cn/post/6844903779289022478 +https://juejin.cn/post/6844903779289006093 +https://juejin.cn/post/6844903779289022471 \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227-Java\345\256\236\347\216\260.md" "b/docs/dataStructures-algorithms/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227-Java\345\256\236\347\216\260.md" new file mode 100644 index 0000000..6e99702 --- /dev/null +++ "b/docs/dataStructures-algorithms/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227-Java\345\256\236\347\216\260.md" @@ -0,0 +1 @@ +https://github.com/LyricYang/Internet-Recruiting-Algorithm-Problems/blob/master/CodeInterviewGuide/README.md \ No newline at end of file diff --git "a/docs/java/Basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/java/Basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" new file mode 100644 index 0000000..5a97f4b --- /dev/null +++ "b/docs/java/Basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -0,0 +1 @@ +https://juejin.cn/post/6844904127059738631 \ No newline at end of file diff --git a/docs/java/IO/java IO.md b/docs/java/IO/java IO.md new file mode 100644 index 0000000..c197d76 --- /dev/null +++ b/docs/java/IO/java IO.md @@ -0,0 +1 @@ +https://juejin.cn/post/6844904125700784136 \ No newline at end of file diff --git "a/docs/java/JavaFamily/\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" "b/docs/java/JavaFamily/\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" new file mode 100644 index 0000000..459cd09 --- /dev/null +++ "b/docs/java/JavaFamily/\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" @@ -0,0 +1 @@ +https://github.com/AobingJava/JavaFamily \ No newline at end of file diff --git "a/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\200\345\255\243\347\254\224\350\256\260.md" "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\200\345\255\243\347\254\224\350\256\260.md" new file mode 100644 index 0000000..6052a3f --- /dev/null +++ "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\200\345\255\243\347\254\224\350\256\260.md" @@ -0,0 +1,2 @@ +https://www.yuque.com/books/share/327d9543-85d2-418f-9315-41c3e19d2768/0dca325c876a0e85f0ba4ea48042e61d +https://github.com/shishan100/Java-Interview-Advanced#%E5%88%86%E5%B8%83%E5%BC%8F%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97 \ No newline at end of file diff --git "a/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\211\345\255\243\347\254\224\350\256\260.md" "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\211\345\255\243\347\254\224\350\256\260.md" new file mode 100644 index 0000000..42773c7 --- /dev/null +++ "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\211\345\255\243\347\254\224\350\256\260.md" @@ -0,0 +1 @@ +https://blog.csdn.net/u013073869/article/details/105271345 \ No newline at end of file diff --git "a/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\272\214\345\255\243\347\254\224\350\256\260.md" "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\272\214\345\255\243\347\254\224\350\256\260.md" new file mode 100644 index 0000000..e69de29 diff --git "a/docs/java/Multithread/Java\345\271\266\345\217\221.md" "b/docs/java/Multithread/Java\345\271\266\345\217\221.md" new file mode 100644 index 0000000..4a068da --- /dev/null +++ "b/docs/java/Multithread/Java\345\271\266\345\217\221.md" @@ -0,0 +1,2 @@ +参考:https://juejin.cn/post/6844904063687983111 +参考:https://www.cmsblogs.com/category/1391296887813967872 \ No newline at end of file diff --git "a/docs/java/collection/\351\233\206\345\220\210\351\235\242\350\257\225\350\265\204\346\226\231\346\261\207\346\200\273.md" "b/docs/java/collection/\351\233\206\345\220\210\351\235\242\350\257\225\350\265\204\346\226\231\346\261\207\346\200\273.md" new file mode 100644 index 0000000..e69961f --- /dev/null +++ "b/docs/java/collection/\351\233\206\345\220\210\351\235\242\350\257\225\350\265\204\346\226\231\346\261\207\346\200\273.md" @@ -0,0 +1,25 @@ +## Java 集合 + +参考:https://www.cmsblogs.com/article/1391291996752187392 +参考:https://juejin.cn/post/6844904125939843079 + +- ArrayList + +- LinkedList + +- HashMap + +- TreeMap + +- TreeSet + +- LinkedHashMap + +- ConcurrentHashMap + +- ArrayBlockingQueue + +- LinkedBlockingQueue + +- PriorityBlockingQueue + diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Dubbo.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Dubbo.md" new file mode 100644 index 0000000..ab40459 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Dubbo.md" @@ -0,0 +1,516 @@ + + +## 基础知识 + +### 为什么要用 Dubbo? + +* 随着服务化的进一步发展,服务越来越多,服务之间的调用和依赖关系也越来越复杂,诞生了面向服务的架构体系(SOA),也因此衍生出了一系列相应的技术,如对服务提供、服务调用、连接处理、通信协议、序列化方式、服务发现、服务路由、日志输出等行为进行封装的服务框架。就这样为分布式系统的服务治理框架就出现了,Dubbo 也就这样产生了。 + +### Dubbo 是什么? + +* Dubbo 是一款高性能、轻量级的开源 RPC 框架,提供服务自动注册、自动发现等高效服务治理方案, 可以和 Spring 框架无缝集成。 + +### Dubbo 的使用场景有哪些? + +* 透明化的远程方法调用:就像调用本地方法一样调用远程方法,只需简单配置,没有任何API侵入。 +* 软负载均衡及容错机制:可在内网替代 F5 等硬件负载均衡器,降低成本,减少单点。 +* 服务自动注册与发现:不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。 + +### Dubbo 核心功能有哪些? + +* Remoting:网络通信框架,提供对多种NIO框架抽象封装,包括“同步转异步”和“请求-响应”模式的信息交换方式。 +* Cluster:服务框架,提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。 +* Registry:服务注册,基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。 + +### Dubbo 核心组件有哪些? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c2bd87ef0~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* Provider:暴露服务的服务提供方 +* Consumer:调用远程服务消费方 +* Registry:服务注册与发现注册中心 +* Monitor:监控中心和访问调用统计 +* Container:服务运行容器 + +### Dubbo 服务器注册与发现的流程? + +* 服务容器Container负责启动,加载,运行服务提供者。 +* 服务提供者Provider在启动时,向注册中心注册自己提供的服务。 +* 服务消费者Consumer在启动时,向注册中心订阅自己所需的服务。 +* 注册中心Registry返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 +* 服务消费者Consumer,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 +* 服务消费者Consumer和提供者Provider,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心Monitor。 + +## 架构设计 + +### Dubbo 的整体架构设计有哪些分层? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c2d11703a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 接口服务层(Service):该层与业务逻辑相关,根据 provider 和 consumer 的业务设计对应的接口和实现 +* 配置层(Config):对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心 +* 服务代理层(Proxy):服务接口透明代理,生成服务的客户端 Stub 和 服务端的 Skeleton,以 ServiceProxy 为中心,扩展接口为 ProxyFactory +* 服务注册层(Registry):封装服务地址的注册和发现,以服务 URL 为中心,扩展接口为 RegistryFactory、Registry、RegistryService +* 路由层(Cluster):封装多个提供者的路由和负载均衡,并桥接注册中心,以Invoker 为中心,扩展接口为 Cluster、Directory、Router 和 LoadBlancce +* 监控层(Monitor):RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory、Monitor 和 MonitorService +* 远程调用层(Protocal):封装 RPC 调用,以 Invocation 和 Result 为中心,扩展接口为 Protocal、Invoker 和 Exporter +* 信息交换层(Exchange):封装请求响应模式,同步转异步。以 Request 和Response 为中心,扩展接口为 Exchanger、ExchangeChannel、ExchangeClient 和 ExchangeServer +* 网络 传输 层(Transport):抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel、Transporter、Client、Server 和 Codec +* 数据序列化层(Serialize):可复用的一些工具,扩展接口为 Serialization、ObjectInput、ObjectOutput 和 ThreadPool + +### Dubbo Monitor 实现原理? + +* Consumer 端在发起调用之前会先走 filter 链;provider 端在接收到请求时也是先走 filter 链,然后才进行真正的业务逻辑处理。默认情况下,在 consumer 和 provider 的 filter 链中都会有 Monitorfilter。 + +1. MonitorFilter 向 DubboMonitor 发送数据 + +2. DubboMonitor 将数据进行聚合后(默认聚合 1min 中的统计数据)暂存到ConcurrentMap statisticsMap,然后使用一个含有 3 个线程(线程名字:DubboMonitorSendTimer)的线程池每隔 1min 钟,调用 SimpleMonitorService 遍历发送 statisticsMap 中的统计数据,每发送完毕一个,就重置当前的 Statistics 的 AtomicReference + +3. SimpleMonitorService 将这些聚合数据塞入 BlockingQueue queue 中(队列大写为 100000) + +4. SimpleMonitorService 使用一个后台线程(线程名为:DubboMonitorAsyncWriteLogThread)将 queue 中的数据写入文件(该线程以死循环的形式来写) + +5. SimpleMonitorService 还会使用一个含有 1 个线程(线程名字:DubboMonitorTimer)的线程池每隔 5min 钟,将文件中的统计数据画成图表 + +## 分布式框架 + +### Dubbo 类似的分布式框架还有哪些? + +* 比较著名的就是 Spring Cloud。 + +### Dubbo 和 Spring Cloud 有什么关系? + +* Dubbo 是 SOA 时代的产物,它的关注点主要在于服务的调用,流量分发、流量监控和熔断。而 Spring Cloud 诞生于微服务架构时代,考虑的是微服务治理的方方面面,另外由于依托了 Spring、Spring Boot 的优势之上,两个框架在开始目标就不一致,Dubbo 定位服务治理、Spring Cloud 是打造一个生态。 + +#### Dubbo 和 Spring Cloud 有什么哪些区别? + +* Dubbo 底层是使用 Netty 这样的 NIO 框架,是基于 TCP 协议传输的,配合以 Hession 序列化完成 RPC 通信。 + +* Spring Cloud 是基于 Http 协议 Rest 接口调用远程过程的通信,相对来说 Http 请求会有更大的报文,占的带宽也会更多。但是 REST 相比 RPC 更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖,这在强调快速演化的微服务环境下,显得更为合适,至于注重通信速度还是方便灵活性,具体情况具体考虑。 + +### Dubbo 和 Dubbox 之间的区别? + +* Dubbox 是继 Dubbo 停止维护后,当当网基于 Dubbo 做的一个扩展项目,如加了服务可 Restful 调用,更新了开源组件等。 + +## 注册中心 + +### Dubbo 有哪些注册中心? + +* Multicast 注册中心:Multicast 注册中心不需要任何中心节点,只要广播地址,就能进行服务注册和发现,基于网络中组播传输实现。 +* Zookeeper 注册中心:基于分布式协调系统 Zookeeper 实现,采用 Zookeeper 的 watch 机制实现数据变更。 +* Redis 注册中心:基于 Redis 实现,采用 key/map 存储,key 存储服务名和类型,map 中 key 存储服务 url,value 服务过期时间。基于 Redis 的发布/订阅模式通知数据变更。 +* Simple 注册中心。 +* 推荐使用 Zookeeper 作为注册中心 + +### Dubbo 的注册中心集群挂掉,发布者和订阅者之间还能通信么? + +* 可以通讯。启动 Dubbo 时,消费者会从 Zookeeper 拉取注册的生产者的地址接口等数据,缓存在本地。每次调用时,按照本地存储的地址进行调用。 + +## 集群 + +### Dubbo集群提供了哪些负载均衡策略? + +* Random LoadBalance: 随机选取提供者策略,有利于动态调整提供者权重。截面碰撞率高,调用次数越多,分布越均匀。 + +* RoundRobin LoadBalance: 轮循选取提供者策略,平均分布,但是存在请求累积的问题。 + +* LeastActive LoadBalance: 最少活跃调用策略,解决慢提供者接收更少的请求。 + +* ConstantHash LoadBalance: 一致性 Hash 策略,使相同参数请求总是发到同一提供者,一台机器宕机,可以基于虚拟节点,分摊至其他提供者,避免引起提供者的剧烈变动。 + +`默认为 Random 随机调用。` + +### Dubbo的集群容错方案有哪些? + +* Failover Cluster:失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。 +* Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 +* Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 +* Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 +* Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2″ 来设置最大并行数。 +* Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。 + +`默认的容错方案是 Failover Cluster。` + +## 配置 + +### Dubbo 配置文件是如何加载到 Spring 中的? + +* Spring 容器在启动的时候,会读取到 Spring 默认的一些 schema 以及 Dubbo 自定义的 schema,每个 schema 都会对应一个自己的 NamespaceHandler,NamespaceHandler 里面通过 BeanDefinitionParser 来解析配置信息并转化为需要加载的 bean 对象! + +### 说说核心的配置有哪些? + +| 标签 | 用途 | 解释 | +| --- | --- | --- | +| | 服务配置 | 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心 | +| | 引用配置 | 用于创建一个远程服务代理,一个引用可以指向多个注册中心 | +| | 协议配置 | 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受 | +| | 应用配置 | 用于配置当前应用信息,不管该应用是提供者还是消费者 | +| | 模块配置 | 用于配置当前模块信息,可选 | +| | 注册中心配置 | 用于配置连接注册中心相关信息 | +| | 监控中心配置 | 用于配置连接监控中心相关信息,可选 | +| | 提供方配置 | 当 ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值,可选 | +| | 消费方配置 | 当 ReferenceConfig 某属性没有配置时,采用此缺省值,可选 | +| | 方法配置 | 用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息 | +| | 参数配置 | 用于指定方法参数配置 | + +`如果是SpringBoot项目就只需要注解,或者开Application配置文件!!!` + +### Dubbo 超时设置有哪些方式? + +**Dubbo 超时设置有两种方式:** + +* 服务提供者端设置超时时间,在Dubbo的用户文档中,推荐如果能在服务端多配置就尽量多配置,因为服务提供者比消费者更清楚自己提供的服务特性。 +* 服务消费者端设置超时时间,如果在消费者端设置了超时时间,以消费者端为主,即优先级更高。因为服务调用方设置超时时间控制性更灵活。如果消费方超时,服务端线程不会定制,会产生警告。 + +### 服务调用超时会怎么样? + +* dubbo 在调用服务不成功时,默认是会重试两次。 + +## 通信协议 + +### Dubbo 使用的是什么通信框架? + +* 默认使用 Netty 作为通讯框架。 + +### Dubbo 支持哪些协议,它们的优缺点有哪些? + +* Dubbo: 单一长连接和 NIO 异步通讯,适合大并发小数据量的服务调用,以及消费者远大于提供者。传输协议 TCP,异步 Hessian 序列化。Dubbo推荐使用dubbo协议。 + +* RMI: 采用 JDK 标准的 RMI 协议实现,传输参数和返回参数对象需要实现 Serializable 接口,使用 Java 标准序列化机制,使用阻塞式短连接,传输数据包大小混合,消费者和提供者个数差不多,可传文件,传输协议 TCP。 多个短连接 TCP 协议传输,同步传输,适用常规的远程服务调用和 RMI 互操作。在依赖低版本的 Common-Collections 包,Java 序列化存在安全漏洞。 + +* WebService:基于 WebService 的远程调用协议,集成 CXF 实现,提供和原生 WebService 的互操作。多个短连接,基于 HTTP 传输,同步传输,适用系统集成和跨语言调用。 + +* HTTP: 基于 Http 表单提交的远程调用协议,使用 Spring 的 HttpInvoke 实现。多个短连接,传输协议 HTTP,传入参数大小混合,提供者个数多于消费者,需要给应用程序和浏览器 JS 调用。 + +* Hessian:集成 Hessian 服务,基于 HTTP 通讯,采用 Servlet 暴露服务,Dubbo 内嵌 Jetty 作为服务器时默认实现,提供与 Hession 服务互操作。多个短连接,同步 HTTP 传输,Hessian 序列化,传入参数较大,提供者大于消费者,提供者压力较大,可传文件。 + +* Memcache:基于 Memcache实现的 RPC 协议。 + +* Redis:基于 Redis 实现的RPC协议。 + +## 设计模式 + +### Dubbo 用到哪些设计模式? + +`Dubbo 框架在初始化和通信过程中使用了多种设计模式,可灵活控制类加载、权限控制等功能。` + +* **工厂模式** + + Provider 在 export 服务时,会调用 ServiceConfig 的 export 方法。ServiceConfig中有个字段: + +private static final Protocol protocol = +ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtensi +on(); +复制代码 + +* 工厂模式 + + Provider 在 export 服务时,会调用 ServiceConfig 的 export 方法。ServiceConfig中有个字段: + +private static final Protocol protocol = +ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtensi +on(); +复制代码 + +Dubbo 里有很多这种代码。这也是一种工厂模式,只是实现类的获取采用了 JDKSPI 的机制。这么实现的优点是可扩展性强,想要扩展实现,只需要在 classpath下增加个文件就可以了,代码零侵入。另外,像上面的 Adaptive 实现,可以做到调用时动态决定调用哪个实现,但是由于这种实现采用了动态代理,会造成代码调试比较麻烦,需要分析出实际调用的实现类。 + +* **装饰器模式** + + Dubbo 在启动和调用阶段都大量使用了装饰器模式。以 Provider 提供的调用链为例,具体的调用链代码是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的,具体是将注解中含有 group=provider 的 Filter 实现,按照 order 排序,最后的调用顺序是: + +EchoFilter -> ClassLoaderFilter -> GenericFilter -> ContextFilter -> +ExecuteLimitFilter -> TraceFilter -> TimeoutFilter -> MonitorFilter -> +ExceptionFilter +复制代码 + +更确切地说,这里是装饰器和责任链模式的混合使用。例如,EchoFilter 的作用是判断是否是回声测试请求,是的话直接返回内容,这是一种责任链的体现。而像ClassLoaderFilter 则只是在主功能上添加了功能,更改当前线程的 ClassLoader,这是典型的装饰器模式。 + +* **观察者模式** + + Dubbo 的 Provider 启动时,需要与注册中心交互,先注册自己的服务,再订阅自己的服务,订阅时,采用了观察者模式,开启一个 listener。注册中心会每 5 秒定时检查是否有服务更新,如果有更新,向该服务的提供者发送一个 notify 消息,provider 接受到 notify 消息后,运行 NotifyListener 的 notify 方法,执行监听器方法。 + +* **动态代理模式** + + Dubbo 扩展 JDK SPI 的类 ExtensionLoader 的 Adaptive 实现是典型的动态代理实现。Dubbo 需要灵活地控制实现类,即在调用阶段动态地根据参数决定调用哪个实现类,所以采用先生成代理类的方法,能够做到灵活的调用。生成代理类的代码是 ExtensionLoader 的 createAdaptiveExtensionClassCode 方法。代理类主要逻辑是,获取 URL 参数中指定参数的值作为获取实现类的 key。 + +## 运维管理 + +### 服务上线怎么兼容旧版本? + +* 可以用版本号(version)过渡,多个不同版本的服务注册到注册中心,版本号不同的服务相互间不引用。这个和服务分组的概念有一点类似。 + +### Dubbo telnet 命令能做什么? + +* dubbo 服务发布之后,我们可以利用 telnet 命令进行调试、管理。Dubbo2.0.5 以上版本服务提供端口支持 telnet 命令 + +### Dubbo 支持服务降级吗? + +* 以通过 dubbo:reference 中设置 mock=“return null”。mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+Mock” 后缀。然后在 Mock 类里实现自己的降级逻辑 + +### Dubbo 如何优雅停机? + +* Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的,所以如果使用kill -9 PID 等强制关闭指令,是不会执行优雅停机的,只有通过 kill PID 时,才会执行。 + +## SPI + +### Dubbo SPI 和 Java SPI 区别? + +* JDK SPI: + + JDK 标准的 SPI 会一次性加载所有的扩展实现,如果有的扩展很耗时,但也没用上,很浪费资源。所以只希望加载某个的实现,就不现实了 + +* DUBBO SPI: + + 1、对 Dubbo 进行扩展,不需要改动 Dubbo 的源码 + + 2、延迟加载,可以一次只加载自己想要加载的扩展实现。 + + 3、增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。 + + 4、Dubbo 的扩展机制能很好的支持第三方 IoC 容器,默认支持 Spring Bean。 + +## 其他 + +### Dubbo 支持分布式事务吗? + +* 目前暂时不支持,可与通过 tcc-transaction 框架实现 + +* 介绍:tcc-transaction 是开源的 TCC 补偿性分布式事务框架 + +* TCC-Transaction 通过 Dubbo 隐式传参的功能,避免自己对业务代码的入侵。 + +### Dubbo 可以对结果进行缓存吗? + +* 为了提高数据访问的速度。Dubbo 提供了声明式缓存,以减少用户加缓存的工作量 +* 其实比普通的配置文件就多了一个标签 cache=“true” + +### Dubbo 必须依赖的包有哪些? + +* Dubbo 必须依赖 JDK,其他为可选。 + +### Dubbo 支持哪些序列化方式? + +* 默认使用 Hessian 序列化,还有 Duddo、FastJson、Java 自带序列化。 + +### Dubbo 在安全方面有哪些措施? + +* Dubbo 通过 Token 令牌防止用户绕过注册中心直连,然后在注册中心上管理授权。 +* Dubbo 还提供服务黑白名单,来控制服务所允许的调用方。 + +### 服务调用是阻塞的吗? + +* 默认是阻塞的,可以异步调用,没有返回值的可以这么做。Dubbo 是基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小,异步调用会返回一个 Future 对象。 + +### 服务提供者能实现失效踢出是什么原理? + +* 服务失效踢出基于 zookeeper 的临时节点原理。 + +### 同一个服务多个注册的情况下可以直连某一个服务吗? + +* 可以点对点直连,修改配置即可,也可以通过 telnet 直接某个服务。 + +### Dubbo 服务降级,失败重试怎么做? + +* 可以通过 dubbo:reference 中设置 mock=“return null”。mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+Mock” 后缀。然后在 Mock 类里实现自己的降级逻辑 + +### Dubbo 使用过程中都遇到了些什么问题? + +* 在注册中心找不到对应的服务,检查 service 实现类是否添加了@service 注解无法连接到注册中心,检查配置文件中的对应的测试 ip 是否正确 + +## RPC + +### 为什么要有RPC + +* http接口是在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。利用现成的http协议进行传输。但是如果是一个大型的网站,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了,首先就是长链接,不必每次通信都要像http一样去3次握手什么的,减少了网络开销;其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。第三个来说就是安全性。最后就是最近流行的服务化架构、服务化治理,RPC框架是一个强力的支撑。 + +* socket只是一个简单的网络通信方式,只是创建通信双方的通信通道,而要实现rpc的功能,还需要对其进行封装,以实现更多的功能。 + +* RPC一般配合netty框架、spring自定义注解来编写轻量级框架,其实netty内部是封装了socket的,较新的jdk的IO一般是NIO,即非阻塞IO,在高并发网站中,RPC的优势会很明显 + +### 什么是RPC + +* RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。简言之,RPC使得程序能够像访问本地系统资源一样,去访问远端系统资源。比较关键的一些方面包括:通讯协议、序列化、资源(接口)描述、服务框架、性能、语言支持等。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c2ec735fd~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 简单的说,RPC就是从一台机器(客户端)上通过参数传递的方式调用另一台机器(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果。 + +### PRC架构组件 + +* 一个基本的RPC架构里面应该至少包含以下4个组件: + + 1、客户端(Client):服务调用方(服务消费者) + + 2、客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端 + + 3、服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理4、服务端(Server):服务的真正提供者 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c2fb08565~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 具体调用过程: + + 1、服务消费者(client客户端)通过调用本地服务的方式调用需要消费的服务; + + 2、客户端存根(client stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体; + + 3、客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端; + + 4、服务端存根(server stub)收到消息后进行解码(反序列化操作); + + 5、服务端存根(server stub)根据解码结果调用本地的服务进行相关处理; + + 6、本地服务执行具体业务逻辑并将处理结果返回给服务端存根(server stub); + + 7、服务端存根(server stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方; + + 8、客户端存根(client stub)接收到消息,并进行解码(反序列化); + + 9、服务消费方得到最终结果; + +`而RPC框架的实现目标则是将上面的第2-10步完好地封装起来,也就是把调用、编码/解码的过程给封装起来,让用户感觉上像调用本地服务一样的调用远程服务。` + +### RPC和SOA、SOAP、REST的区别 + +* 1、REST + + 可以看着是HTTP协议的一种直接应用,默认基于JSON作为传输格式,使用简单,学习成本低效率高,但是安全性较低。 + +* 2、SOAP + + SOAP是一种数据交换协议规范,是一种轻量的、简单的、基于XML的协议的规范。而SOAP可以看着是一个重量级的协议,基于XML、SOAP在安全方面是通过使用XML-Security和XML-Signature两个规范组成了WS-Security来实现安全控制的,当前已经得到了各个厂商的支持 。 + + 它有什么优点?简单总结为:易用、灵活、跨语言、跨平台。 + +* 3、SOA + + 面向服务架构,它可以根据需求通过网络对松散耦合的粗粒度应用组件进行分布式部署、组合和使用。服务层是SOA的基础,可以直接被应用调用,从而有效控制系统中与软件代理交互的人为依赖性。 + + SOA是一种粗粒度、松耦合服务架构,服务之间通过简单、精确定义接口进行通讯,不涉及底层编程接口和通讯模型。SOA可以看作是B/S模型、XML(标准通用标记语言的子集)/Web Service技术之后的自然延伸。 + +* 4、REST 和 SOAP、RPC 有何区别呢? + + 没什么太大区别,他们的本质都是提供可支持分布式的基础服务,最大的区别在于他们各自的的特点所带来的不同应用场景 。 + +### RPC框架需要解决的问题? + +* 1、如何确定客户端和服务端之间的通信协议? +* 2、如何更高效地进行网络通信? +* 3、服务端提供的服务如何暴露给客户端? +* 4、客户端如何发现这些暴露的服务? +* 5、如何更高效地对请求对象和响应结果进行序列化和反序列化操作? + +### RPC的实现基础? + +* 1、需要有非常高效的网络通信,比如一般选择Netty作为网络通信框架; +* 2、需要有比较高效的序列化框架,比如谷歌的Protobuf序列化框架; +* 3、可靠的寻址方式(主要是提供服务的发现),比如可以使用Zookeeper来注册服务等等; +* 4、如果是带会话(状态)的RPC调用,还需要有会话和状态保持的功能; + +### RPC使用了哪些关键技术? + +* 1、动态代理 + + 生成Client Stub(客户端存根)和Server Stub(服务端存根)的时候需要用到Java动态代理技术,可以使用JDK提供的原生的动态代理机制,也可以使用开源的:CGLib代理,Javassist字节码生成技术。 + +* 2、序列化和反序列化 + + 在网络中,所有的数据都将会被转化为字节进行传送,所以为了能够使参数对象在网络中进行传输,需要对这些参数进行序列化和反序列化操作。 + + * 序列化:把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。 目前比较高效的开源序列化框架:如Kryo、FastJson和Protobuf等。 + * 反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。 目前比较高效的开源序列化框架:如Kryo、FastJson和Protobuf等。 +* 3、NIO通信 + + 出于并发性能的考虑,传统的阻塞式 IO 显然不太合适,因此我们需要异步的 IO,即 NIO。Java 提供了 NIO 的解决方案,Java 7 也提供了更优秀的 NIO.2 支持。可以选择Netty或者MINA来解决NIO数据传输的问题。 + +* 4、服务注册中心 + + 可选:Redis、Zookeeper、Consul 、Etcd。一般使用ZooKeeper提供服务注册与发现功能,解决单点故障以及分布式部署的问题(注册中心)。 + +### 主流RPC框架有哪些 + +* 1、RMI + + 利用java.rmi包实现,基于Java远程方法协议(Java Remote Method Protocol) 和java的原生序列化。 + +* 2、Hessian + + 是一个轻量级的remoting onhttp工具,使用简单的方法提供了RMI的功能。 基于HTTP协议,采用二进制编解码。 + +* 3、protobuf-rpc-pro + + 是一个Java类库,提供了基于 Google 的 Protocol Buffers 协议的远程方法调用的框架。基于 Netty 底层的 NIO 技术。支持 TCP 重用/ keep-alive、SSL加密、RPC 调用取消操作、嵌入式日志等功能。 + +* 4、Thrift + + 是一种可伸缩的跨语言服务的软件框架。它拥有功能强大的代码生成引擎,无缝地支持C + +,C#,Java,Python和PHP和Ruby。thrift允许你定义一个描述文件,描述数据类型和服务接口。依据该文件,编译器方便地生成RPC客户端和服务器通信代码。 + + 最初由facebook开发用做系统内个语言之间的RPC通信,2007年由facebook贡献到apache基金 ,现在是apache下的opensource之一 。支持多种语言之间的RPC方式的通信:php语言client可以构造一个对象,调用相应的服务方法来调用java语言的服务,跨越语言的C/S RPC调用。底层通讯基于SOCKET。 + +* 5、Avro + + 出自Hadoop之父Doug Cutting, 在Thrift已经相当流行的情况下推出Avro的目标不仅是提供一套类似Thrift的通讯中间件,更是要建立一个新的,标准性的云计算的数据交换和存储的Protocol。支持HTTP,TCP两种协议。 + +* 6、Dubbo + + Dubbo是 阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。 + +### RPC的实现原理架构图 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c300aefca~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。 + +比如说,A服务器想调用B服务器上的一个方法: + +* 1、建立通信 + + 首先要解决通讯的问题:即A机器想要调用B机器,首先得建立起通信连接。 + + 主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。 + + 通常这个连接可以是按需连接(需要调用的时候就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。 + +* 2、服务寻址 + + 要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么。 + + 通常情况下我们需要提供B机器(主机名或IP地址)以及特定的端口,然后指定调用的方法或者函数的名称以及入参出参等信息,这样才能完成服务的一个调用。 + + 可靠的寻址方式(主要是提供服务的发现)是RPC的实现基石,比如可以采用Redis或者Zookeeper来注册服务等等。 + + * 2.1、从服务提供者的角度看: + + 当服务提供者启动的时候,需要将自己提供的服务注册到指定的注册中心,以便服务消费者能够通过服务注册中心进行查找; + + 当服务提供者由于各种原因致使提供的服务停止时,需要向注册中心注销停止的服务; + + 服务的提供者需要定期向服务注册中心发送心跳检测,服务注册中心如果一段时间未收到来自服务提供者的心跳后,认为该服务提供者已经停止服务,则将该服务从注册中心上去掉。 + + * 2.2、从调用者的角度看: + + 服务的调用者启动的时候根据自己订阅的服务向服务注册中心查找服务提供者的地址等信息; + + 当服务调用者消费的服务上线或者下线的时候,注册中心会告知该服务的调用者; + + 服务调用者下线的时候,则取消订阅。 + +* 3、网络传输 + + * 3.1、序列化 + + 当A机器上的应用发起一个RPC调用时,调用方法和其入参等信息需要通过底层的网络协议如TCP传输到B机器,由于网络协议是基于二进制的,所有我们传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输。然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。 + + * 3.2、反序列化 + + 当B机器接收到A机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),即将二进制信息恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用, 通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值。 + +* 4、服务调用 + + B机器进行本地调用(通过代理Proxy和反射调用)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后再交给A机器上的应用进行相关处理(一般是业务逻辑处理操作)。 + +`通常,经过以上四个步骤之后,一次完整的RPC调用算是完成了,另外可能因为网络抖动等原因需要重试等。` + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904127076499463 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/JavaIO.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/JavaIO.md" new file mode 100644 index 0000000..d28772b --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/JavaIO.md" @@ -0,0 +1,949 @@ + + +## BIO、NIO、AIO、Netty + +#### 什么是IO + +* Java中I/O是以流为基础进行数据的输入输出的,所有数据被串行化(所谓串行化就是数据要按顺序进行输入输出)写入输出流。简单来说就是java通过io流方式和外部设备进行交互。 + +* 在Java类库中,IO部分的内容是很庞大的,因为它涉及的领域很广泛:标准输入输出,文件的操作,**网络上的数据传输流**,字符串流,对象流等等等。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b746125c6~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* 比如程序从服务器上下载图片,就是通过流的方式从网络上以流的方式到程序中,在到硬盘中 + +#### 在了解不同的IO之前先了解:同步与异步,阻塞与非阻塞的区别 + +* 同步,一个任务的完成之前不能做其他操作,必须等待(等于在打电话) +* 异步,一个任务的完成之前,可以进行其他操作(等于在聊QQ) +* 阻塞,是相对于CPU来说的, 挂起当前线程,不能做其他操作只能等待 +* 非阻塞,,无须挂起当前线程,可以去执行其他操作 + +#### 什么是BIO + +* BIO:同步并阻塞,服务器实现一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,没处理完之前此线程不能做其他操作(如果是单线程的情况下,我传输的文件很大呢?),当然可以通过线程池机制改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。 + +#### 什么是NIO + +* NIO:同步非阻塞,服务器实现一个连接一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4之后开始支持。 + +#### 什么是AIO + +* AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作系统参与并发操作,编程比较复杂,JDK1.7之后开始支持。. + +* AIO属于NIO包中的类实现,其实IO主要分为BIO和NIO,AIO只是附加品,解决IO不能异步的实现 + +* 在以前很少有Linux系统支持AIO,Windows的IOCP就是该AIO模型。但是现在的服务器一般都是支持AIO操作 + +#### 什么Netty + +* Netty是由JBOSS提供的一个Java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。 + +* Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b74fff5c8~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)Netty是由NIO演进而来,使用过NIO编程的用户就知道NIO编程非常繁重,Netty是能够能跟好的使用NIO + +#### BIO和NIO、AIO的区别 + +* BIO是阻塞的,NIO是非阻塞的. +* BIO是面向流的,只能单向读写,NIO是面向缓冲的, 可以双向读写 +* 使用BIO做Socket连接时,由于单向读写,当没有数据时,会挂起当前线程,阻塞等待,为防止影响其它连接,,需要为每个连接新建线程处理.,然而系统资源是有限的,,不能过多的新建线程,线程过多带来线程上下文的切换,从来带来更大的性能损耗,因此需要使用NIO进行BIO多路复用,使用一个线程来监听所有Socket连接,使用本线程或者其他线程处理连接 +* AIO是非阻塞 以异步方式发起 I/O 操作。当 I/O 操作进行时可以去做其他操作,由操作系统内核空间提醒IO操作已完成(不懂的可以往下看) + +#### IO流的分类 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b771fca9c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)**按照读写的单位大小来分:** + +* `字符流`:以字符为单位,每次次读入或读出是16位数据。其只能读取字符类型数据。 (Java代码接收数据为一般为`char数组,也可以是别的`) +* 字节流:以字节为单位,每次次读入或读出是8位数据。可以读任何类型数据,图片、文件、音乐视频等。 (Java代码接收数据只能为`byte数组`) + +**按照实际IO操作来分:** + +* 输出流:从内存读出到文件。只能进行写操作。 +* 输入流:从文件读入到内存。只能进行读操作。 +* **注意**:输出流可以帮助我们创建文件,而输入流不会。 + +**按照读写时是否直接与硬盘,内存等节点连接分:** + +* 节点流:直接与数据源相连,读入或读出。 +* 处理流:也叫包装流,是对一个对于已存在的流的连接进行封装,通过所封装的流的功能调用实现数据读写。如添加个Buffering缓冲区。(意思就是有个缓存区,等于软件和mysql中的redis) +* **注意**:为什么要有处理流?主要作用是在读入或写出时,对数据进行缓存,以减少I/O的次数,以便下次更好更快的读写文件,才有了处理流。 + +#### 什么是内核空间 + +* 我们的应用程序是不能直接访问硬盘的,我们程序没有权限直接访问,但是操作系统(Windows、Linux......)会给我们一部分权限较高的内存空间,他叫内核空间,和我们的实际硬盘空间是有区别的 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b7790530d~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 五种IO模型 + +* **注意:我这里的用户空间就是应用程序空间** + +##### 1.阻塞BIO(blocking I/O) + +* A拿着一支鱼竿在河边钓鱼,并且一直在鱼竿前等,在等的时候不做其他的事情,十分专心。只有鱼上钩的时,才结束掉等的动作,把鱼钓上来。 + +* 在内核将数据准备好之前,系统调用会一直等待所有的套接字,默认的是阻塞方式。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b82fb2f64~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +##### 2.非阻塞NIO(noblocking I/O) + +* B也在河边钓鱼,但是B不想将自己的所有时间都花费在钓鱼上,在等鱼上钩这个时间段中,B也在做其他的事情(一会看看书,一会读读报纸,一会又去看其他人的钓鱼等),但B在做这些事情的时候,每隔一个固定的时间检查鱼是否上钩。一旦检查到有鱼上钩,就停下手中的事情,把鱼钓上来。 **B在检查鱼竿是否有鱼,是一个轮询的过程。** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b830f1cd3~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +##### 3.异步AIO(asynchronous I/O) + +* C也想钓鱼,但C有事情,于是他雇来了D、E、F,让他们帮他等待鱼上钩,一旦有鱼上钩,就打电话给C,C就会将鱼钓上去。![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2ba0e18c2d~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)当应用程序请求数据时,内核一方面去取数据报内容返回,另一方面将程序控制权还给应用进程,应用进程继续处理其他事情,是一种非阻塞的状态。 + +##### 4.信号驱动IO(signal blocking I/O) + +* G也在河边钓鱼,但与A、B、C不同的是,G比较聪明,他给鱼竿上挂一个铃铛,当有鱼上钩的时候,这个铃铛就会被碰响,G就会将鱼钓上来。![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2ba21e5d95~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)信号驱动IO模型,应用进程告诉内核:当数据报准备好的时候,给我发送一个信号,对SIGIO信号进行捕捉,并且调用我的信号处理函数来获取数据报。 + +##### 5.IO多路转接(I/O multiplexing) + +* H同样也在河边钓鱼,但是H生活水平比较好,H拿了很多的鱼竿,一次性有很多鱼竿在等,H不断的查看每个鱼竿是否有鱼上钩。增加了效率,减少了等待的时间。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2ba9450627~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)IO多路转接是多了一个select函数,select函数有一个参数是文件描述符集合,对这些文件描述符进行循环监听,当某个文件描述符就绪时,就对这个文件描述符进行处理。 +* IO多路转接是属于阻塞IO,但可以对多个文件描述符进行阻塞监听,所以效率较阻塞IO的高。 + +#### 什么是比特(Bit),什么是字节(Byte),什么是字符(Char),它们长度是多少,各有什么区别 + +* Bit最小的二进制单位 ,是计算机的操作部分取值0或者1 +* Byte是计算机中存储数据的单元,是一个8位的二进制数,(计算机内部,一个字节可表示一个英文字母,两个字节可表示一个汉字。) `取值(-128-127)` +* Char是用户的可读写的最小单位,他只是抽象意义上的一个符号。如‘5’,‘中’,‘¥’ 等等等等。在java里面由16位bit组成Char 取值`(0-65535)` +* Bit 是最小单位 计算机他只能认识0或者1 +* Byte是8个字节 是给计算机看的 +* 字符 是看到的东西 一个字符=二个字节 + +#### 什么叫对象序列化,什么是反序列化,实现对象序列化需要做哪些工作 + +* 对象序列化,将对象以二进制的形式保存在硬盘上 +* 反序列化;将二进制的文件转化为对象读取 +* 实现serializable接口,不想让字段放在硬盘上就加transient + +#### 在实现序列化接口是时候一般要生成一个serialVersionUID字段,它叫做什么,一般有什么用 + +* 如果用户没有自己声明一个serialVersionUID,接口会默认生成一个serialVersionUID +* 但是强烈建议用户自定义一个serialVersionUID,因为默认的serialVersinUID对于class的细节非常敏感,反序列化时可能会导致InvalidClassException这个异常。 +* (比如说先进行序列化,然后在反序列化之前修改了类,那么就会报错。因为修改了类,对应的SerialversionUID也变化了,而序列化和反序列化就是通过对比其SerialversionUID来进行的,一旦SerialversionUID不匹配,反序列化就无法成功。 + +#### 怎么生成SerialversionUID + +* 可序列化类可以通过声明名为 "serialVersionUID" 的字段(该字段**必须是静态 (static)、最终 (final) 的 long 型字段**)显式声明其自己的 serialVersionUID + +* 两种显示的生成方式(当你一个类实现了Serializable接口,如果没有显示的定义serialVersionUID,Eclipse会提供这个提示功能告诉你去定义 。在Eclipse中点击类中warning的图标一下,Eclipse就会自动给定两种生成的方式。 + +#### BufferedReader属于哪种流,它主要是用来做什么的,它里面有那些经典的方法 + +* 属于处理流中的缓冲流,可以将读取的内容存在内存里面,有readLine()方法 + +#### Java中流类的超类主要有那些? + +* 超类代表顶端的父类(都是抽象类) + +* java.io.InputStream + +* java.io.OutputStream + +* java.io.Reader + +* java.io.Writer + +#### 为什么图片、视频、音乐、文件等 都是要字节流来读取 + +* 这个很基础,你看看你电脑文件的属性就好了,CPU规定了计算机存储文件都是按字节算的 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2bae24bd8b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### IO的常用类和方法,以及如何使用 + +[注意,如果懂IO的普通文件读写操作可以直接点击此处跳过,直接看网络操作IO编程,那个才是重点,点击即会跳转](#Mark "#Mark") + +前面讲了那么多废话,现在我们开始进入主题,后面很长,从开始的文件操作到后面的**网络IO操作**都会有例子: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2bb4bd288c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)[注意,如果懂IO的普通文件读写操作可以直接点击此处跳过,直接看网络操作IO编程,那个才是重点,点击即会跳转](#Mark "#Mark") +#### IO基本操作讲解 + +* `这里的基本操作就是普通的读取操作,如果想要跟深入的了解不同的IO开发场景必须先了解IO的基本操作` + +##### 1 按`字符`流读取文件 + +###### 1.1 按字符流的·节点流方式读取 + +* 如果我们要取的数据基本单位是字符,那么用(**字符流**)这种方法读取文件就比较适合。比如:读取test.txt文件 + +**注释:** + +* `字符流`:以字符为单位,每次次读入或读出是16位数据。其只能读取字符类型数据。 (Java代码接收数据为一般为`char数组,也可以是别的`) + +* 字节流:以字节为单位,每次次读入或读出是8位数据。可以读任何类型数据,图片、文件、音乐视频等。 (Java代码接收数据只能为`byte数组`) + +* **FileReader 类:**(字符输入流) 注意:new FileReader("D:\test.txt");//文件必须存在 + +```java +package com.test.io; + +import java.io.FileReader; +import java.io.IOException; + +public class TestFileReader { + public static void main(String[] args) throws IOException { + int num=0; + //字符流接收使用的char数组 + char[] buf=new char[1024]; + //字符流、节点流打开文件类 + FileReader fr = new FileReader("D:\\test.txt");//文件必须存在 + //FileReader.read():取出字符存到buf数组中,如果读取为-1代表为空即结束读取。 + //FileReader.read():读取的是一个字符,但是java虚拟机会自动将char类型数据转换为int数据, + //如果你读取的是字符A,java虚拟机会自动将其转换成97,如果你想看到字符可以在返回的字符数前加(char)强制转换如 + while((num=fr.read(buf))!=-1) { } + //检测一下是否取到相应的数据 + for(int i=0;i 0) { + System.out.println(new String(buffer, 0, len)); + } + //向客户端写数据 + out = socket.getOutputStream(); + out.write("hello!".getBytes()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +* TCP协议Socket使用BIO进行通信:客户端(第二执行) + +```java +package com.test.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Scanner; + +//TCP协议Socket使用BIO进行通信:客户端 +public class Client01 { + public static void main(String[] args) throws IOException { + //创建套接字对象socket并封装ip与port + Socket socket = new Socket("127.0.0.1", 8000); + //根据创建的socket对象获得一个输出流 + //基于字节流 + OutputStream outputStream = socket.getOutputStream(); + //控制台输入以IO的形式发送到服务器 + System.out.println("TCP连接成功 \n请输入:"); + String str = new Scanner(System.in).nextLine(); + byte[] car = str.getBytes(); + outputStream.write(car); + System.out.println("TCP协议的Socket发送成功"); + //刷新缓冲区 + outputStream.flush(); + //关闭连接 + socket.close(); + } +} +``` + +* TCP协议Socket使用BIO进行通信:客户端(第三执行) + +```java +package com.test.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Scanner; + +//TCP协议Socket:客户端 +public class Client02 { + public static void main(String[] args) throws IOException { + //创建套接字对象socket并封装ip与port + Socket socket = new Socket("127.0.0.1", 8000); + //根据创建的socket对象获得一个输出流 + //基于字节流 + OutputStream outputStream = socket.getOutputStream(); + //控制台输入以IO的形式发送到服务器 + System.out.println("TCP连接成功 \n请输入:"); + String str = new Scanner(System.in).nextLine(); + byte[] car = str.getBytes(); + outputStream.write(car); + System.out.println("TCP协议的Socket发送成功"); + //刷新缓冲区 + outputStream.flush(); + //关闭连接 + socket.close(); + } +} +``` + +`为了解决堵塞问题,可以使用多线程,请看下面` + +##### 2 多线程解决BIO编程会出现的问题 + +**这时有人就会说,我多线程不就解决了吗?** + +* 使用多线程是可以解决堵塞等待时间很长的问题,因为他可以充分发挥CPU +* 然而系统资源是有限的,不能过多的新建线程,线程过多带来线程上下文的切换,从来带来更大的性能损耗![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dd68f7c10~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +**万一请求越来越多,线程越来越多那我CPU不就炸了?** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2d696381b8~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)**多线程BIO代码示例:** + +* 四个客户端,这次我多复制了俩个一样客户端类![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2d7a9bf162~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)`先启动服务端,在启动所有客户端,测试`,发现连接成功(`后面有代码`)![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2d94da3441~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)在所有客户端输入消息(`Client01、Client02这些是我在客户端输入的消息`):发现没有问题![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2d94251e4b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +**多线程BIO通信代码:** + +* `服务端的代码,客户端的代码还是上面之前的代码` + +```java +package com.test.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; + +//TCP协议Socket使用多线程BIO进行通行:服务端 +public class BIOThreadService { + public static void main(String[] args) { + try { + ServerSocket server = new ServerSocket(8000); + System.out.println("服务端启动成功,监听端口为8000,等待客户端连接... "); + while (true) { + Socket socket = server.accept();//等待客户连接 + System.out.println("客户连接成功,客户信息为:" + socket.getRemoteSocketAddress()); + //针对每个连接创建一个线程, 去处理I0操作 + //创建多线程创建开始 + Thread thread = new Thread(new Runnable() { + public void run() { + try { + InputStream in = socket.getInputStream(); + byte[] buffer = new byte[1024]; + int len = 0; + //读取客户端的数据 + while ((len = in.read(buffer)) > 0) { + System.out.println(new String(buffer, 0, len)); + } + //向客户端写数据 + OutputStream out = socket.getOutputStream(); + out.write("hello".getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + thread.start(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +`为了解决线程太多,这时又来了,线程池` + +##### 3 线程池解决多线程BIO编程会出现的问题 + +**这时有人就会说,我TM用线程池?** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dab69e263~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 线程池固然可以解决这个问题,万一需求量还不够还要扩大线程池。当是这是我们自己靠着自己的思想完成的IO操作,Socket 上来了就去创建线程去抢夺CPU资源,MD,线程都TM做IO去了,CPU也不舒服呀 + +* 这时呢:Jdk官方坐不住了,兄弟BIO的问题交给我,我来给你解决:`NIO的诞生` + +**线程池BIO代码示例:** + +* 四个客户端![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2da7389a42~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)`先启动服务端,在启动所有客户端,测试`,(`后面有代码`)![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dc163e25a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)在所有客户端输入消息(`Client01、Client02这些是我在客户端输入的消息`):发现没有问题![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dc2080d3a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +**线程池BIO通信代码:** + +* `服务端的代码,客户端的代码还是上面的代码` + +```java +package com.test.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +//TCP协议Socket使用线程池BIO进行通行:服务端 +public class BIOThreadPoolService { + public static void main(String[] args) { + //创建线程池 + ExecutorService executorService = Executors.newFixedThreadPool(30); + try { + ServerSocket server = new ServerSocket(8000); + System.out.println("服务端启动成功,监听端口为8000,等待客户端连接..."); + while (true) { + Socket socket = server.accept();//等待客户连接 + System.out.println("客户连接成功,客户信息为:" + socket.getRemoteSocketAddress()); + //使用线程池中的线程去执行每个对应的任务 + executorService.execute(new Thread(new Runnable() { + public void run() { + try { + InputStream in = socket.getInputStream(); + byte[] buffer = new byte[1024]; + int len = 0; + //读取客户端的数据 + while ((len = in.read(buffer)) > 0) { + System.out.println(new String(buffer, 0, len)); + } + //向客户端写数据 + OutputStream out = socket.getOutputStream(); + out.write("hello".getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } + }) + ); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +##### 4 使用NIO实现网络通信 + +* NIO是JDK1.4提供的操作,他的流还是流,没有改变,服务器实现的还是一个连接一个线程,当是:`客户端发送的连接请求都会注册到多路复用器上`,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4之后开始支持。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dc3e76709~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)`看不懂介绍可以认真看看代码实例,其实不难` +###### 什么是通道(Channel) + +* Channel是一个对象,可以通过它读取和写入数据。 通常我们都是将数据写入包含一个或者多个字节的缓冲区,然后再将缓存区的数据写入到通道中,将数据从通道读入缓冲区,再从缓冲区获取数据。 + +* Channel 类似于原I/O中的流(Stream),但有所区别: + + * 流是单向的,通道是双向的,可读可写。 + * 流读写是阻塞的,通道可以异步读写。 + +###### 什么是选择器(Selector) + +* Selector可以称他为通道的集合,每次客户端来了之后我们会把Channel注册到Selector中并且我们给他一个状态,在用死循环来环判断(`判断是否做完某个操作,完成某个操作后改变不一样的状态`)状态是否发生变化,知道IO操作完成后在退出死循环 + +###### 什么是Buffer(缓冲区) + +* Buffer 是一个缓冲数据的对象, 它包含一些要写入或者刚读出的数据。 + +* 在普通的面向流的 I/O 中,一般将数据直接写入或直接读到 Stream 对象中。当是有了Buffer(缓冲区)后,数据第一步到达的是Buffer(缓冲区)中 + +* 缓冲区实质上是一个数组(`底层完全是数组实现的,感兴趣可以去看一下`)。通常它是一个字节数组,内部维护几个状态变量,可以实现在同一块缓冲区上反复读写(不用清空数据再写)。 + +###### 代码实例: + +* 目录结构![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dd02ee59c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* 运行示例,先运行服务端,在运行所有客户端控制台输入消息就好了。:`我这客户端和服务端代码有些修该变,后面有代码`![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2de8321be5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* `服务端示例,先运行,想要搞定NIO请认真看代码示例,真的很清楚` + +```java +package com.test.io; + +import com.lijie.iob.RequestHandler; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; + +public class NIOServer { + public static void main(String[] args) throws IOException { + //111111111 + //Service端的Channel,监听端口的 + ServerSocketChannel serverChannel = ServerSocketChannel.open(); + //设置为非阻塞 + serverChannel.configureBlocking(false); + //nio的api规定这样赋值端口 + serverChannel.bind(new InetSocketAddress(8000)); + //显示Channel是否已经启动成功,包括绑定在哪个地址上 + System.out.println("服务端启动成功,监听端口为8000,等待客户端连接..."+ serverChannel.getLocalAddress()); + + //22222222 + //声明selector选择器 + Selector selector = Selector.open(); + //这句话的含义,是把selector注册到Channel上面, + //每个客户端来了之后,就把客户端注册到Selector选择器上,默认状态是Accepted + serverChannel.register(selector, SelectionKey.OP_ACCEPT); + + //33333333 + //创建buffer缓冲区,声明大小是1024,底层使用数组来实现的 + ByteBuffer buffer = ByteBuffer.allocate(1024); + RequestHandler requestHandler = new RequestHandler(); + + //444444444 + //轮询,服务端不断轮询,等待客户端的连接 + //如果有客户端轮询上来就取出对应的Channel,没有就一直轮询 + while (true) { + int select = selector.select(); + if (select == 0) { + continue; + } + //有可能有很多,使用Set保存Channel + Set selectionKeys = selector.selectedKeys(); + Iterator iterator = selectionKeys.iterator(); + while (iterator.hasNext()) { + //使用SelectionKey来获取连接了客户端和服务端的Channel + SelectionKey key = iterator.next(); + //判断SelectionKey中的Channel状态如何,如果是OP_ACCEPT就进入 + if (key.isAcceptable()) { + //从判断SelectionKey中取出Channel + ServerSocketChannel channel = (ServerSocketChannel) key.channel(); + //拿到对应客户端的Channel + SocketChannel clientChannel = channel.accept(); + //把客户端的Channel打印出来 + System.out.println("客户端通道信息打印:" + clientChannel.getRemoteAddress()); + //设置客户端的Channel设置为非阻塞 + clientChannel.configureBlocking(false); + //操作完了改变SelectionKey中的Channel的状态OP_READ + clientChannel.register(selector, SelectionKey.OP_READ); + } + //到此轮训到的时候,发现状态是read,开始进行数据交互 + if (key.isReadable()) { + //以buffer作为数据桥梁 + SocketChannel channel = (SocketChannel) key.channel(); + //数据要想读要先写,必须先读取到buffer里面进行操作 + channel.read(buffer); + //进行读取 + String request = new String(buffer.array()).trim(); + buffer.clear(); + //进行打印buffer中的数据 + System.out.println(String.format("客户端发来的消息: %s : %s", channel.getRemoteAddress(), request)); + //要返回数据的话也要先返回buffer里面进行返回 + String response = requestHandler.handle(request); + //然后返回出去 + channel.write(ByteBuffer.wrap(response.getBytes())); + } + iterator.remove(); + } + } + } +} +``` + +* 客户端示例:(`我这用的不是之前的了,有修改`)运行起来客户端控制台输入消息就好了。 `要模拟测试,请复制粘贴改一下,修改客户端的类名就行了,四个客户端代码一样的`,![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dee3a5661~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +```java +package com.test.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Scanner; + +//TCP协议Socket:客户端 +public class Client01 { + public static void main(String[] args) throws IOException { + //创建套接字对象socket并封装ip与port + Socket socket = new Socket("127.0.0.1", 8000); + //根据创建的socket对象获得一个输出流 + OutputStream outputStream = socket.getOutputStream(); + //控制台输入以IO的形式发送到服务器 + System.out.println("TCP连接成功 \n请输入:"); + while(true){ + byte[] car = new Scanner(System.in).nextLine().getBytes(); + outputStream.write(car); + System.out.println("TCP协议的Socket发送成功"); + //刷新缓冲区 + outputStream.flush(); + } + } +} +``` + +##### 5 使用Netty实现网络通信 + +* Netty是由JBOSS提供的一个Java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。 + +* Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的Socket服务开发。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2e0284e9ca~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)Netty是由NIO演进而来,使用过NIO编程的用户就知道NIO编程非常繁重,Netty是能够能跟好的使用NIO +* Netty的原里就是NIO,他是基于NIO的一个完美的封装,并且优化了NIO,使用他非常方便,简单快捷 + +* 我直接上代码: + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2e0e2ef4ca~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* 1、先添加依赖: + + + io.netty + netty-all + 4.1.16.Final + +``` + +* 2、NettyServer 模板,看起来代码那么多,`其实只需要添加一行消息就好了` +* `请认真看中间的代码` + +```java +package com.lijie.iob; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.serialization.ClassResolvers; +import io.netty.handler.codec.serialization.ObjectEncoder; +import io.netty.handler.codec.string.StringDecoder; + +public class NettyServer { + public static void main(String[] args) throws InterruptedException { + EventLoopGroup bossGroup = new NioEventLoopGroup(); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + ChannelPipeline pipeline = socketChannel.pipeline(); + pipeline.addLast(new StringDecoder()); + pipeline.addLast("encoder", new ObjectEncoder()); + pipeline.addLast(" decoder", new io.netty.handler.codec.serialization.ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null))); + + //重点,其他的都是复用的 + //这是真正的I0的业务代码,把他封装成一个个的个Hand1e类就行了 + //把他当成 SpringMVC的Controller + pipeline.addLast(new NettyServerHandler()); + + } + }) + .option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + ChannelFuture f = b.bind(8000).sync(); + System.out.println("服务端启动成功,端口号为:" + 8000); + f.channel().closeFuture().sync(); + } finally { + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + } +} +``` + +* 3、需要做的IO操作,重点是继承ChannelInboundHandlerAdapter类就好了 + +```java +package com.lijie.iob; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +public class NettyServerHandler extends ChannelInboundHandlerAdapter { + RequestHandler requestHandler = new RequestHandler(); + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + Channel channel = ctx.channel(); + System.out.println(String.format("客户端信息: %s", channel.remoteAddress())); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Channel channel = ctx.channel(); + String request = (String) msg; + System.out.println(String.format("客户端发送的消息 %s : %s", channel.remoteAddress(), request)); + String response = requestHandler.handle(request); + ctx.write(response); + ctx.flush(); + } +} +``` + +* 4 客户的代码还是之前NIO的代码,我在复制下来一下吧 + +```java +package com.test.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Scanner; + +//TCP协议Socket:客户端 +public class Client01 { + public static void main(String[] args) throws IOException { + //创建套接字对象socket并封装ip与port + Socket socket = new Socket("127.0.0.1", 8000); + //根据创建的socket对象获得一个输出流 + OutputStream outputStream = socket.getOutputStream(); + //控制台输入以IO的形式发送到服务器 + System.out.println("TCP连接成功 \n请输入:"); + while(true){ + byte[] car = new Scanner(System.in).nextLine().getBytes(); + outputStream.write(car); + System.out.println("TCP协议的Socket发送成功"); + //刷新缓冲区 + outputStream.flush(); + } + } +} +``` + +* 运行测试,还是之前那样,启动服务端,在启动所有客户端控制台输入就好了:![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2e4e740749~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125700784136 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\237\272\347\241\200.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\237\272\347\241\200.md" new file mode 100644 index 0000000..e4df817 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\237\272\347\241\200.md" @@ -0,0 +1,1313 @@ + + +## Java概述 + +### 何为编程 + +* 编程就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终得到结果的过程。 + +* 为了使计算机能够理解人的意图,人类就必须要将需解决的问题的思路、方法、和手段通过计算机能够理解的形式告诉计算机,使得计算机能够根据人的指令一步一步去工作,完成某种特定的任务。这种人和计算机之间交流的过程就是编程。 + +### 什么是Java + +* Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。 + +### jdk1.5之后的三大版本 + +* Java SE(J2SE,Java 2 Platform Standard Edition,标准版) + Java SE 以前称为 J2SE。它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的 Java 应用程序。Java SE 包含了支持 Java Web 服务开发的类,并为Java EE和Java ME提供基础。 +* Java EE(J2EE,Java 2 Platform Enterprise Edition,企业版) + Java EE 以前称为 J2EE。企业版本帮助开发和部署可移植、健壮、可伸缩且安全的服务器端Java 应用程序。Java EE 是在 Java SE 的基础上构建的,它提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和 Web2.0应用程序。2018年2月,Eclipse 宣布正式将 JavaEE 更名为 JakartaEE +* Java ME(J2ME,Java 2 Platform Micro Edition,微型版) + Java ME 以前称为 J2ME。Java ME 为在移动设备和嵌入式设备(比如手机、PDA、电视机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。Java ME 包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。基于 Java ME 规范的应用程序只需编写一次,就可以用于许多设备,而且可以利用每个设备的本机功能。 + +### 3 Jdk和Jre和JVM的区别 + +`看Java官方的图片,Jdk中包括了Jre,Jre中包括了JVM` + +* JDK :Jdk还包括了一些Jre之外的东西 ,就是这些东西帮我们编译Java代码的, 还有就是监控Jvm的一些工具 Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等 + +* JRE :Jre大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库 Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包 + + 如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。 + +* Jvm:在倒数第二层 由他可以在(最后一层的)各种平台上运行 Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c434318a82~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 什么是跨平台性?原理是什么 + +* 所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。 + +* 实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。 + +### Java语言有哪些特点 + +* 简单易学(Java语言的语法与C语言和C++语言很接近) + +* 面向对象(封装,继承,多态) + +* 平台无关性(Java虚拟机实现平台无关性) + +* 支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的) + +* 支持多线程(多线程机制使应用程序在同一时间并行执行多项任) + +* 健壮性(Java语言的强类型机制、异常处理、垃圾的自动收集等) + +* 安全性好 + +### 什么是字节码?采用字节码的最大好处是什么 + +* **字节码**:Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。 + +* **采用字节码的好处**: + + Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。 + +* **先看下java中的编译器和解释器**: + + Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。 + + Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。 + +### 什么是Java程序的主类?应用程序和小程序的主类有何不同? + +* 一个程序中可以有多个类,但只能有一个类是主类。在Java应用程序中,这个主类是指包含main()方法的类。而在Java小程序中,这个主类是一个继承自系统类JApplet或Applet的子类。应用程序的主类不一定要求是public类,但小程序的主类要求必须是public类。主类是Java程序执行的入口点。 + +### Java应用程序与小程序之间有那些差别? + +* 简单说应用程序是从主线程启动(也就是main()方法)。applet小程序没有main方法,主要是嵌在浏览器页面上运行(调用init()线程或者run()来启动),嵌入浏览器这点跟flash的小游戏类似。 + +### Java和C++的区别 + +`我知道很多人没学过C++,但是面试官就是没事喜欢拿咱们Java和C++比呀!没办法!!!就算没学过C++,也要记下来!` + +* 都是面向对象的语言,都支持封装、继承和多态 +* Java不提供指针来直接访问内存,程序内存更加安全 +* Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。 +* Java有自动内存管理机制,不需要程序员手动释放无用内存 + +### Oracle JDK 和 OpenJDK 的对比 + +1. Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一次; + +2. OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全开源的; + +3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到Oracle JDK就可以解决问题; + +4. 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能; + +5. Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本; + +6. Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 + +## 基础语法 + +### 数据类型 + +#### Java有哪些数据类型 + +**定义**:Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间。 + +**分类** + +* 基本数据类型 + * 数值型 + * 整数类型(byte,short,int,long) + * 浮点类型(float,double) + * 字符型(char) + * 布尔型(boolean) +* 引用数据类型 + * 类(class) + * 接口(interface) + * 数组([]) + +**Java基本数据类型图** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c434465b69~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上 + +* 在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。 + +#### 用最有效率的方法计算 2 乘以 8 + +* 2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)。 + +#### Math.round(11.5) 等于多少?Math.round(-11.5)等于多少 + +* Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。 + +#### float f=3.4;是否正确 + +* 不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成 float f =3.4F;。 + +#### short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗 + +* 对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。 + +* 而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换。 + +### 编码 + +#### Java语言采用何种编码方案?有何特点? + +* Java语言采用Unicode编码标准,Unicode(标准码),它为每个字符制订了一个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。 + +### 注释 + +#### 什么Java注释 + +**定义**:用于解释说明程序的文字 + +**分类** + +* 单行注释 + 格式: // 注释文字 +* 多行注释 + 格式: /* 注释文字 */ +* 文档注释 + 格式:/** 注释文字 */ + +**作用** + +* 在程序中,尤其是复杂的程序中,适当地加入注释可以增加程序的可读性,有利于程序的修改、调试和交流。注释的内容在程序编译的时候会被忽视,不会产生目标代码,注释的部分不会对程序的执行结果产生任何影响。 + +`注意事项:多行和文档注释都不能嵌套使用。` + +### 访问修饰符 + +#### 访问修饰符 public,private,protected,以及不写(默认)时的区别 + +* **定义**:Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。 + +* **分类** + + * private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类) + + * default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。 + + * protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。 + + * public : 对所有类可见。使用对象:类、接口、变量、方法 + +**访问修饰符图** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c433bcfd38~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 运算符 + +#### &和&&的区别 + +* &运算符有两种用法:(1)按位与;(2)逻辑与。 + +* &&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。 + +`注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。` + +### 关键字 + +#### Java 有没有 goto + +* goto 是 Java 中的保留字,在目前版本的 Java 中没有使用。 + +#### final 有什么用? + +`用于修饰类、属性和方法;` + +* 被final修饰的类不可以被继承 +* 被final修饰的方法不可以被重写 +* 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的 + +#### final finally finalize区别 + +* final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表 示该变量是一个常量不能被重新赋值。 +* finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块 中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。 +* finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调 用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的 最后判断。 + +#### this关键字的用法 + +* this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。 + +* this的用法在java中大体可以分为3种: + + * 1.普通的直接引用,this相当于是指向当前对象本身。 + + * 2.形参与成员名字重名,用this来区分: + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + 复制代码 + * 3.引用本类的构造函数 + + class Person{ + private String name; + private int age; + + public Person() { + } + + public Person(String name) { + this.name = name; + } + public Person(String name, int age) { + this(name); + this.age = age; + } + } + 复制代码 + +#### super关键字的用法 + +* super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。 + +* super也有三种用法: + + * 1.普通的直接引用 + + 与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。 + + * 2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分 + + class Person{ + protected String name; + + public Person(String name) { + this.name = name; + } + + } + + class Student extends Person{ + private String name; + + public Student(String name, String name1) { + super(name); + this.name = name1; + } + + public void getInfo(){ + System.out.println(this.name); //Child + System.out.println(super.name); //Father + } + + } + + public class Test { + public static void main(String[] args) { + Student s1 = new Student("Father","Child"); + s1.getInfo(); + + } + } + 复制代码 + * 3.引用父类构造函数 + + * super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。 + * this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。 + +#### this与super的区别 + +* super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参) +* this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名) +* super()和this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。 +* super()和this()均需放在构造方法内第一行。 +* 尽管可以用this调用一个构造器,但却不能调用两个。 +* this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。 +* this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。 +* 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。 + +#### static存在的主要意义 + +* static的主要意义是在于创建独立于具体对象的域变量或者方法。**以致于即使没有创建对象,也能使用属性和调用方法**! + +* static关键字还有一个比较关键的作用就是 **用来形成静态代码块以优化程序性能**。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。 + +* 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。 + +#### static的独特之处 + +* 1、被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法**不属于任何一个实例对象,而是被类的实例对象所共享**。 + +> 怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的很通俗了,你明白了咩? + +* 2、在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。 + +* 3、static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的! + +* 4、被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。 + +#### static应用场景 + +* 因为static是被类的实例对象所共享,因此如果**某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量**。 + +* 因此比较常见的static应用场景有: + +> 1、修饰成员变量 2、修饰成员方法 3、静态代码块 4、修饰类【只能修饰内部类也就是静态内部类】 5、静态导包 + +#### static注意事项 + +* 1、静态只能访问静态。 +* 2、非静态既可以访问非静态的,也可以访问静态的。 + +### 流程控制语句 + +#### break ,continue ,return 的区别及作用 + +* break 跳出总上一层循环,不再执行循环(结束当前的循环体) + +* continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件) + +* return 程序返回,不再执行下面的代码(结束当前的方法 直接返回) + +#### 在 Java 中,如何跳出当前的多重嵌套循环 + +* 在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如: + + public static void main(String[] args) { + ok: + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + System.out.println("i=" + i + ",j=" + j); + if (j == 5) { + break ok; + } + } + } + } + 复制代码 + +## 面向对象 + +### 面向对象概述 + +#### 面向对象和面向过程的区别 + +* **面向过程**: + + * 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。 + + * 缺点:没有面向对象易维护、易复用、易扩展 + +* **面向对象**: + + * 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护 + + * 缺点:性能比面向过程低 + +`面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。` + +`面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。` + +`面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。` + +### 面向对象三大特性 + +#### 面向对象的特征有哪些方面 + +**面向对象的特征主要有以下几个方面**: + +* **抽象**:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。 + +* **封装**把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 + +* **继承**是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 + + * 关于继承如下 3 点请记住: + + * 子类拥有父类非 private 的属性和方法。 + + * 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 + + * 子类可以用自己的方式实现父类的方法。(以后介绍)。 + +* **多态**:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 + +#### 什么是多态机制?Java语言是如何实现多态的? + +* 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。 + +* 多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。 + +**多态的实现** + +* Java实现多态有三个必要条件:继承、重写、向上转型。 + + * 继承:在多态中必须存在有继承关系的子类和父类。 + + * 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。 + + * 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。 + +`只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。` + +`对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。` + +#### 面向对象五大基本原则是什么(可选) + +* 单一职责原则SRP(Single Responsibility Principle) + 类的功能要单一,不能包罗万象,跟杂货铺似的。 +* 开放封闭原则OCP(Open-Close Principle) + 一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。 +* 里式替换原则LSP(the Liskov Substitution Principle LSP) + 子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~ +* 依赖倒置原则DIP(the Dependency Inversion Principle DIP) + 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。 +* 接口分离原则ISP(the Interface Segregation Principle ISP) + 设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。 + +### 类与接口 + +#### 抽象类和接口的对比 + +* 抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。 + +* 从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。 + +**相同点** + +* 接口和抽象类都不能实例化 +* 都位于继承的顶端,用于被其他实现或继承 +* 都包含抽象方法,其子类都必须覆写这些抽象方法 + +**不同点** + +| 参数 | 抽象类 | 接口 | +| --- | --- | --- | +| 声明 | 抽象类使用abstract关键字声明 | 接口使用interface关键字声明 | +| 实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 | +| 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | +| 访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是public。并且不允许定义为 private 或者 protected | +| 多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 | +| 字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final 的 | + +**备注**:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。 + +`现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。` + +* 接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则: + * 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。 + * 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。 + +#### 普通类和抽象类有哪些区别? + +* 普通类不能包含抽象方法,抽象类可以包含抽象方法。 +* 抽象类不能直接实例化,普通类可以直接实例化。 + +#### 抽象类能使用 final 修饰吗? + +* 不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类 + +#### 创建一个对象用什么关键字?对象实例与对象引用有何不同? + +* new关键字,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球) + +### 变量与方法 + +#### 成员变量与局部变量的区别有哪些 + +* 变量:在程序执行的过程中,在某个范围内其值可以发生改变的量。从本质上讲,变量其实是内存中的一小块区域 + +* 成员变量:方法外部,类内部定义的变量 + +* 局部变量:类的方法中的变量。 + +* 成员变量和局部变量的区别 + +**作用域** + +* 成员变量:针对整个类有效。 +* 局部变量:只在某个范围内有效。(一般指的就是方法,语句体内) + +**存储位置** + +* 成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。 +* 局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完,或者语句结束后,就自动释放。 + +**生命周期** + +* 成员变量:随着对象的创建而存在,随着对象的消失而消失 +* 局部变量:当方法调用完,或者语句结束后,就自动释放。 + +**初始值** + +* 成员变量:有默认初始值。 +* 局部变量:没有默认初始值,使用前必须赋值。 + +#### 在Java中定义一个不做事且没有参数的构造方法的作用 + +* Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。 + +#### 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是? + +* 帮助子类做初始化工作。 + +#### 一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么? + +* 主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。 + +#### 构造方法有哪些特性? + +* 名字与类名相同; + +* 没有返回值,但不能用void声明构造函数; + +* 生成类的对象时自动执行,无需调用。 + +#### 静态变量和实例变量区别 + +* 静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。 + +* 实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。 + +#### 静态变量与普通变量区别 + +* static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。 + +* 还有一点就是static成员变量的初始化顺序按照定义的顺序进行初始化。 + +#### 静态方法和实例方法有何不同? + +`静态方法和实例方法的区别主要体现在两个方面:` + +* 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 + +* 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制 + +#### 在一个静态方法内调用一个非静态成员为什么是非法的? + +* 由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。 + +#### 什么是方法的返回值?返回值的作用是什么? + +* 方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作! + +### 内部类 + +#### 什么是内部类? + +* 在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是**内部类**。内部类本身就是类的一个属性,与其他属性定义方式一致。 + +#### 内部类的分类有哪些 + +`内部类可以分为四种:**成员内部类、局部内部类、匿名内部类和静态内部类**。` + +##### 静态内部类 + +* 定义在类内部的静态类,就是静态内部类。 + + public class Outer { + + private static int radius = 1; + + static class StaticInner { + public void visit() { + System.out.println("visit outer static variable:" + radius); + } + } + } + 复制代码 +* 静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式,`new 外部类.静态内部类()`,如下: + + Outer.StaticInner inner = new Outer.StaticInner(); + inner.visit(); + 复制代码 + +##### 成员内部类 + +* 定义在类内部,成员位置上的非静态类,就是成员内部类。 + + public class Outer { + + private static int radius = 1; + private int count =2; + + class Inner { + public void visit() { + System.out.println("visit outer static variable:" + radius); + System.out.println("visit outer variable:" + count); + } + } + } + 复制代码 +* 成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式`外部类实例.new 内部类()`,如下: + + Outer outer = new Outer(); + Outer.Inner inner = outer.new Inner(); + inner.visit(); + 复制代码 + +##### 局部内部类 + +* 定义在方法中的内部类,就是局部内部类。 + + public class Outer { + + private int out_a = 1; + private static int STATIC_b = 2; + + public void testFunctionClass(){ + int inner_c =3; + class Inner { + private void fun(){ + System.out.println(out_a); + System.out.println(STATIC_b); + System.out.println(inner_c); + } + } + Inner inner = new Inner(); + inner.fun(); + } + public static void testStaticFunctionClass(){ + int d =3; + class Inner { + private void fun(){ + // System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量 + System.out.println(STATIC_b); + System.out.println(d); + } + } + Inner inner = new Inner(); + inner.fun(); + } + } + 复制代码 +* 定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内,`new 内部类()`,如下: + + public static void testStaticFunctionClass(){ + class Inner { + } + Inner inner = new Inner(); + } + 复制代码 + +##### 匿名内部类 + +* 匿名内部类就是没有名字的内部类,日常开发中使用的比较多。 + + public class Outer { + + private void test(final int i) { + new Service() { + public void method() { + for (int j = 0; j < i; j++) { + System.out.println("匿名内部类" ); + } + } + }.method(); + } + } + //匿名内部类必须继承或实现一个已有的接口 + interface Service{ + void method(); + } + 复制代码 +* 除了没有名字,匿名内部类还有以下特点: + + * 匿名内部类必须继承一个抽象类或者实现一个接口。 + * 匿名内部类不能定义任何静态成员和静态方法。 + * 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。 + * 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。 +* 匿名内部类创建方式: + + new 类/接口{ + //匿名内部类实现部分 + } + 复制代码 + +#### 内部类的优点 + +`我们为什么要使用内部类呢?因为它有以下优点:` + +* 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据! +* 内部类不为同一包的其他类所见,具有很好的封装性; +* 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。 +* 匿名内部类可以很方便的定义回调。 + +#### 内部类有哪些应用场景 + +1. 一些多算法场合 +2. 解决一些非面向对象的语句块。 +3. 适当使用内部类,使得代码更加灵活和富有扩展性。 +4. 当某个类除了它的外部类,不再被其他的类使用时。 + +#### 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final? + +* 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢?它内部原理是什么呢?先看这段代码: + + public class Outer { + + void outMethod(){ + final int a =10; + class Inner { + void innerMethod(){ + System.out.println(a); + } + } + } + } + 复制代码 +* 以上例子,为什么要加final呢?是因为**生命周期不一致**, 局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。 + +#### 内部类相关,看程序说出运行结果 + +public class Outer { + private int age = 12; + + class Inner { + private int age = 13; + public void print() { + int age = 14; + System.out.println("局部变量:" + age); + System.out.println("内部类变量:" + this.age); + System.out.println("外部类变量:" + Outer.this.age); + } + } + + public static void main(String[] args) { + Outer.Inner in = new Outer().new Inner(); + in.print(); + } + +} +复制代码 + +运行结果: + +局部变量:14 +内部类变量:13 +外部类变量:12 +复制代码 +### 重写与重载 + +#### 构造器(constructor)是否可被重写(override) + +* 构造器不能被继承,因此不能被重写,但可以被重载。 + +#### 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分? + +* 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。 + +* 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分 + +* 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。 + +### 对象相等判断 + +#### == 和 equals 的区别是什么 + +* **==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址) + +* **equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: + + * 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 + + * 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 + + * **举个例子:** + + public class test1 { + public static void main(String[] args) { + String a = new String("ab"); // a 为一个引用 + String b = new String("ab"); // b为另一个引用,对象的内容一样 + String aa = "ab"; // 放在常量池中 + String bb = "ab"; // 从常量池中查找 + if (aa == bb) // true + System.out.println("aa==bb"); + if (a == b) // false,非同一对象 + System.out.println("a==b"); + if (a.equals(b)) // true + System.out.println("aEQb"); + if (42 == 42.0) { // true + System.out.println("true"); + } + } + } + 复制代码 +* **说明:** + + * String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。 + * 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。 + +#### hashCode 与 equals (重要) + +* HashSet如何检查重复 + +* 两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗? + +* hashCode和equals方法的关系 + +* 面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?” + +**hashCode()介绍** + +* hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。 + +* 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) + +**为什么要有 hashCode** + +`我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:` + +* 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 + +**hashCode()与equals()的相关规定** + +* 如果两个对象相等,则hashcode一定也是相同的 + +* 两个对象相等,对两个对象分别调用equals方法都返回true + +* 两个对象有相同的hashcode值,它们也不一定是相等的 + +`因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖` + +`hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)` + +#### 对象的相等与指向他们的引用相等,两者有什么不同? + +* 对象的相等 比的是内存中存放的内容是否相等而 引用相等 比较的是他们指向的内存地址是否相等。 + +### 值传递 + +#### 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递 + +* 是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的 + +#### 为什么 Java 中只有值传递 + +* 首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。**按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。** 它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。 + +* **Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。** + + * **下面通过 3 个例子来给大家说明** + +##### example 1 + +public static void main(String[] args) { + int num1 = 10; + int num2 = 20; + + swap(num1, num2); + + System.out.println("num1 = " + num1); + System.out.println("num2 = " + num2); +} + +public static void swap(int a, int b) { + int temp = a; + a = b; + b = temp; + + System.out.println("a = " + a); + System.out.println("b = " + b); +} +复制代码 + +* 结果: + + a = 20 b = 10 num1 = 10 num2 = 20 + +* 解析: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c436af3af1~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 在swap方法中,a、b的值进行交换,并不会影响到 num1、num2。因为,a、b中的值,只是从 num1、num2 的复制过来的。也就是说,a、b相当于num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。 + +`通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example.` + +##### example 2 + + public static void main(String[] args) { + int[] arr = { 1, 2, 3, 4, 5 }; + System.out.println(arr[0]); + change(arr); + System.out.println(arr[0]); + } + + public static void change(int[] array) { + // 将数组的第一个元素变为0 + array[0] = 0; + } +复制代码 + +* 结果: + + 1 0 + +* 解析: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c4372f6ac8~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的时同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。 + +`通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。` + +`很多程序设计语言(特别是,C++和Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为Java程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。` + +##### example 3 + +public class Test { + + public static void main(String[] args) { + // TODO Auto-generated method stub + Student s1 = new Student("小张"); + Student s2 = new Student("小李"); + Test.swap(s1, s2); + System.out.println("s1:" + s1.getName()); + System.out.println("s2:" + s2.getName()); + } + + public static void swap(Student x, Student y) { + Student temp = x; + x = y; + y = temp; + System.out.println("x:" + x.getName()); + System.out.println("y:" + y.getName()); + } +} +复制代码 + +* 结果: + + x:小李 y:小张 s1:小张 s2:小李 + +* 解析: + +* 交换之前: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c445af6270~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 交换之后: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c45facc688~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 通过上面两张图可以很清晰的看出:`方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝` + +* 总结 + + * `Java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。` +* 下面再总结一下Java中方法参数的使用情况: + + * 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型》 + * 一个方法可以改变一个对象参数的状态。 + * 一个方法不能让对象参数引用一个新的对象。 + +#### 值传递和引用传递有什么区别 + +* 值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。 + +* 引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。 + +### Java包 + +#### JDK 中常用的包有哪些 + +* java.lang:这个是系统的基础类; +* java.io:这里面是所有输入输出有关的类,比如文件操作等; +* java.nio:为了完善 io 包中的功能,提高 io 包中性能而写的一个新包; +* java.net:这里面是与网络有关的类; +* java.util:这个是系统辅助类,特别是集合类; +* java.sql:这个是数据库操作的类。 + +#### import java和javax有什么区别 + +* 刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来说使用。然而随着时间的推移,javax 逐渐的扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包将是太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。 + +`所以,实际上java和javax没有区别。这都是一个名字。` + +## IO流 + +### java 中 IO 流分为几种? + +* 按照流的流向分,可以分为输入流和输出流; +* 按照操作单元划分,可以划分为字节流和字符流; +* 按照流的角色划分为节点流和处理流。 + +`Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。` + +* InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 +* OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 + +`按操作方式分类结构图:` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c4799a7a74~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +`按操作对象分类结构图:` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c479a04121~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### BIO,NIO,AIO 有什么区别? + +* 简答 + + * BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。 + * NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。 + * AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。 +* 详细回答 + + * **BIO (Blocking I/O):** 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 + * **NIO (New I/O):** NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 + * **AIO (Asynchronous I/O):** AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 + +### Files的常用方法都有哪些? + +* Files. exists():检测文件路径是否存在。 +* Files. createFile():创建文件。 +* Files. createDirectory():创建文件夹。 +* Files. delete():删除一个文件或目录。 +* Files. copy():复制文件。 +* Files. move():移动文件。 +* Files. size():查看文件个数。 +* Files. read():读取文件。 +* Files. write():写入文件。 + +## 反射 + +### 什么是反射机制? + +* JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。 + +* 静态编译和动态编译 + + * 静态编译:在编译时确定类型,绑定对象 + + * 动态编译:运行时确定类型,绑定对象 + +### 反射机制优缺点 + +* **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。 +* **缺点:** 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。 + +### 反射机制的应用场景有哪些? + +* 反射是框架设计的灵魂。 + +* 在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。 + +* 举例:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性 + +### Java获取反射的三种方法 + +1.通过new对象实现反射机制 2.通过路径实现反射机制 3.通过类名实现反射机制 + +public class Student { + private int id; + String name; + protected boolean sex; + public float score; +} + +public class Get { + //获取反射机制三种方式 + public static void main(String[] args) throws ClassNotFoundException { + //方式一(通过建立对象) + Student stu = new Student(); + Class classobj1 = stu.getClass(); + System.out.println(classobj1.getName()); + //方式二(所在通过路径-相对路径) + Class classobj2 = Class.forName("fanshe.Student"); + System.out.println(classobj2.getName()); + //方式三(通过类名) + Class classobj3 = Student.class; + System.out.println(classobj3.getName()); + } +} +复制代码 +## 常用API + +### String相关 + +#### 字符型常量和字符串常量的区别 + +1. 形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符 +2. 含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置) +3. 占内存大小 字符常量只占一个字节 字符串常量占若干个字节(至少一个字符结束标志) + +#### 什么是字符串常量池? + +* 字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。 + +#### String 是最基本的数据类型吗 + +* 不是。Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(referencetype),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。 + +`这是很基础的东西,但是很多初学者却容易忽视,Java 的 8 种基本数据类型中不包括 String,基本数据类型中用来描述文本数据的是 char,但是它只能表示单个字符,比如 ‘a’,‘好’ 之类的,如果要描述一段文本,就需要用多个 char 类型的变量,也就是一个 char 类型数组,比如“你好” 就是长度为2的数组 char\[\] chars = {‘你’,‘好’};` + +`但是使用数组过于麻烦,所以就有了 String,String 底层就是一个 char 类型的数组,只是使用的时候开发者不需要直接操作底层数组,用更加简便的方式即可完成对字符串的使用。` + +#### String有哪些特性 + +* 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。 + +* 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。 + +* final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。 + +#### String为什么是不可变的吗? + +* 简单来说就是String类利用了final修饰的char类型数组存储字符,源码如下图所以: + + /** The value is used for character storage. */ private final char value[]; + +#### String真的是不可变的吗? + +* 我觉得如果别人问这个问题的话,回答不可变就可以了。 下面只是给大家看两个有代表性的例子: + +**1 String不可变但不代表引用不可以变** + +String str = "Hello"; +str = str + " World"; +System.out.println("str=" + str); +复制代码 + +* 结果: + + str=Hello World + +* 解析: + +* 实际上,原来String的内容是不变的,只是str由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。 + +**2.通过反射是可以修改所谓的“不可变”对象** + +// 创建字符串"Hello World", 并赋给引用s +String s = "Hello World"; + +System.out.println("s = " + s); // Hello World + +// 获取String类中的value字段 +Field valueFieldOfString = String.class.getDeclaredField("value"); + +// 改变value属性的访问权限 +valueFieldOfString.setAccessible(true); + +// 获取s对象上的value属性的值 +char[] value = (char[]) valueFieldOfString.get(s); + +// 改变value所引用的数组中的第5个字符 +value[5] = '_'; + +System.out.println("s = " + s); // Hello_World +复制代码 + +* 结果: + + s = Hello World s = Hello_World + +* 解析: + +* 用反射可以访问私有成员, 然后反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。 + +#### 是否可以继承 String 类 + +* String 类是 final 类,不可以被继承。 + +#### String str="i"与 String str=new String(“i”)一样吗? + +* 不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。 + +#### String s = new String(“xyz”);创建了几个字符串对象 + +* 两个对象,一个是静态区的"xyz",一个是用new创建在堆上的对象。 + + String str1 = "hello"; //str1指向静态区 String str2 = new String("hello"); //str2指向堆上的对象 String str3 = "hello"; String str4 = new String("hello"); System.out.println(str1.equals(str2)); //true System.out.println(str2.equals(str4)); //true System.out.println(str1 == str3); //true System.out.println(str1 == str2); //false System.out.println(str2 == str4); //false System.out.println(str2 == "hello"); //false str2 = str1; System.out.println(str2 == "hello"); //true + +#### 如何将字符串反转? + +* 使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。 + +* 示例代码: + + // StringBuffer reverse StringBuffer stringBuffer = new StringBuffer(); stringBuffer. append("abcdefg"); System. out. println(stringBuffer. reverse()); // gfedcba // StringBuilder reverse StringBuilder stringBuilder = new StringBuilder(); stringBuilder. append("abcdefg"); System. out. println(stringBuilder. reverse()); // gfedcba + +#### 数组有没有 length()方法?String 有没有 length()方法 + +* 数组没有 length()方法 ,有 length 的属性。String 有 length()方法。JavaScript中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。 + +#### String 类的常用方法都有那些? + +* indexOf():返回指定字符的索引。 +* charAt():返回指定索引处的字符。 +* replace():字符串替换。 +* trim():去除字符串两端空白。 +* split():分割字符串,返回一个分割后的字符串数组。 +* getBytes():返回字符串的 byte 类型数组。 +* length():返回字符串长度。 +* toLowerCase():将字符串转成小写字母。 +* toUpperCase():将字符串转成大写字符。 +* substring():截取字符串。 +* equals():字符串比较。 + +#### 在使用 HashMap 的时候,用 String 做 key 有什么好处? + +* HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。 + +#### String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的 + +**可变性** + +* String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这两种对象都是可变的。 + +**线程安全性** + +* String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。 + +**性能** + +* 每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 + +**对于三者使用的总结** + +* 如果要操作少量的数据用 = String + +* 单线程操作字符串缓冲区 下操作大量数据 = StringBuilder + +* 多线程操作字符串缓冲区 下操作大量数据 = StringBuffer + +### Date相关 + +### 包装类相关 + +#### 自动装箱与拆箱 + +* **装箱**:将基本类型用它们对应的引用类型包装起来; + +* **拆箱**:将包装类型转换为基本数据类型; + +#### int 和 Integer 有什么区别 + +* Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。 + +* Java 为每个原始类型提供了包装类型: + + * 原始类型: boolean,char,byte,short,int,long,float,double + + * 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double + +#### Integer a= 127 与 Integer b = 127相等吗 + +* 对于对象引用类型:==比较的是对象的内存地址。 +* 对于基本数据类型:==比较的是值。 + +`如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false` + +public static void main(String[] args) { + Integer a = new Integer(3); + Integer b = 3; // 将3自动装箱成Integer类型 + int c = 3; + System.out.println(a == b); // false 两个引用没有引用同一对象 + System.out.println(a == c); // true a自动拆箱成int类型再和c比较 + System.out.println(b == c); // true + + Integer a1 = 128; + Integer b1 = 128; + System.out.println(a1 == b1); // false + + Integer a2 = 127; + Integer b2 = 127; + System.out.println(a2 == b2); // true +} + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904127059738631 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\271\266\345\217\221.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\271\266\345\217\221.md" new file mode 100644 index 0000000..50d9a6a --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\271\266\345\217\221.md" @@ -0,0 +1,1478 @@ +https://juejin.cn/post/6844904119338024974#heading-17 + +## 基础知识 + +#### 为什么要使用并发编程 + +* 提升多核CPU的利用率:一般来说一台主机上的会有多个CPU核心,我们可以创建多个线程,理论上讲操作系统可以将多个线程分配给不同的CPU去执行,每个CPU执行一个线程,这样就提高了CPU的使用效率,如果使用单线程就只能有一个CPU核心被使用。 + +* 比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。 + +* 简单来说就是: + + * 充分利用多核CPU的计算能力; + * 方便进行业务拆分,提升应用性能 + +#### 多线程应用场景 + +* 例如: 迅雷多线程下载、数据库连接池、分批发送短信等。 + +#### 并发编程有什么缺点 + +* 并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题。 + +#### 并发编程三个必要因素是什么? + +* 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。 +* 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile) +* 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序) + +#### 在 Java 程序中怎么保证多线程的运行安全? + +* 出现线程安全问题的原因一般都是三个原因: + + * 线程切换带来的原子性问题 解决办法:使用多线程之间同步synchronized或使用锁(lock)。 + + * 缓存导致的可见性问题 解决办法:synchronized、volatile、LOCK,可以解决可见性问题 + + * 编译优化带来的有序性问题 解决办法:Happens-Before 规则可以解决有序性问题 + +#### 并行和并发有什么区别? + +* 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。 +* 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。 +* 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。 + +**做一个形象的比喻:** + +* 并发 = 俩个人用一台电脑。 + +* 并行 = 俩个人分配了俩台电脑。 + +* 串行 = 俩个人排队使用一台电脑。 + +#### 什么是多线程 + +* 多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。 + +#### 多线程的好处 + +* 可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。 + +#### 多线程的劣势: + +* 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; + +* 多线程需要协调和管理,所以需要 CPU 时间跟踪线程; + +* 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。 + +#### 线程和进程区别 + +* 什么是线程和进程? + + * 进程 + + 一个在内存中运行的应用程序。 每个正在系统上运行的程序都是一个进程 + + * 线程 + + 进程中的一个执行任务(控制单元), 它负责在程序里独立执行。 + +`一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。` + +* 进程与线程的区别 + + * 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位 + + * 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 + + * 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。 + + * 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程与进程之间的地址空间和资源是相互独立的 + + * 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃有可能导致整个进程都死掉。所以多进程要比多线程健壮。 + + * 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行 + +#### 什么是上下文切换? + +* 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 + +* 概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。 + +* 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 + +* Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 + +#### 守护线程和用户线程有什么区别呢? + +* 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程 +* 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作 + +#### 如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高? + +* windows上面用任务管理器看,linux下可以用 top 这个工具看。 + * 找出cpu耗用厉害的进程pid, 终端执行top命令,然后按下shift+p (shift+m是找出消耗内存最高)查找出cpu利用最厉害的pid号 + * 根据上面第一步拿到的pid号,top -H -p pid 。然后按下shift+p,查找出cpu利用率最厉害的线程号,比如top -H -p 1328 + * 将获取到的线程号转换成16进制,去百度转换一下就行 + * 使用jstack工具将进程信息打印输出,jstack pid号 > /tmp/t.dat,比如jstack 31365 > /tmp/t.dat + * 编辑/tmp/t.dat文件,查找线程号对应的信息 + +`或者直接使用JDK自带的工具查看“jconsole” 、“visualVm”,这都是JDK自带的,可以直接在JDK的bin目录下找到直接使用` + +#### 什么是线程死锁 + +* 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。 +* 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 +* 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab440e1912~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 形成死锁的四个必要条件是什么 + +* 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用毕释放。 +* 占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。 +* 不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。 +* 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在等B,B在等C,C在等A) + +#### 如何避免线程死锁 + +1. 避免一个线程同时获得多个锁 +2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源 +3. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制 + +#### 创建线程的四种方式 + +* 继承 Thread 类; + +```java +public class MyThread extends Thread { + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " run()方法正在执行..."); + } +``` +* 实现 Runnable 接口; + +```java +public class MyRunnable implements Runnable { + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " run()方法执行中..."); + } +``` +* 实现 Callable 接口; + +```java +public class MyCallable implements Callable { + @Override + public Integer call() { + System.out.println(Thread.currentThread().getName() + " call()方法执行中..."); + return 1; + } +``` +* 使用匿名内部类方式 + +```java +public class CreateRunnable { + public static void main(String[] args) { + //创建多线程创建开始 + Thread thread = new Thread(new Runnable() { + public void run() { + for (int i = 0; i < 10; i++) { + System.out.println("i:" + i); + } + } + }); + thread.start(); + } + } +``` + +#### 说一下 runnable 和 callable 有什么区别 + +**相同点:** + +* 都是接口 +* 都可以编写多线程程序 +* 都采用Thread.start()启动线程 + +**主要区别:** + +* Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果 +* Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息 注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。 + +#### 线程的 run()和 start()有什么区别? + +* 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。 + +* start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。 + +* start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。 + +* run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。 + +#### 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? + +这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! + +* new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到`时间片`后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 + +* 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 + +总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。 + +#### 什么是 Callable 和 Future? + +* Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。 + +* Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。 + +#### 什么是 FutureTask + +* FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。 + +#### 线程的状态 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab4672a149~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 新建(new):新创建了一个线程对象。 + +* 就绪(可运行状态)(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。 + +* 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中; + +* 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。 + + * 阻塞的情况分三种: + * (一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态; + * (二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态; + * (三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。 +* 死亡(dead)(结束):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。 + +#### Java 中用到的线程调度算法是什么? + +* 计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。(Java是由JVM中的线程计数器来实现线程调度) + +* 有两种调度模型:分时调度模型和抢占式调度模型。 + + * 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。 + + * Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。 + +#### 线程的调度策略 + +`线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:` + +* (1)线程体中调用了 yield 方法让出了对 cpu 的占用权利 + +* (2)线程体中调用了 sleep 方法使线程进入睡眠状态 + +* (3)线程由于 IO 操作受到阻塞 + +* (4)另外一个更高优先级线程出现 + +* (5)在支持时间片的系统中,该线程的时间片用完 + +#### 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )? + +* 线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。 + +* 时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。 + +* 线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。 + +#### 请说出与线程同步以及线程调度相关的方法。 + +* (1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁; + +* (2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常; + +* (3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关; + +* (4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态; + +#### sleep() 和 wait() 有什么区别? + +`两者都可以暂停线程的执行` + +* 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。 +* 是否释放锁:sleep() 不释放锁;wait() 释放锁。 +* 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 +* 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。 + +#### 你是如何调用 wait() 方法的?使用 if 块还是循环?为什么? + +* 处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。 + +* wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码: + +synchronized (monitor) { + // 判断条件谓词是否得到满足 + while(!locked) { + // 等待唤醒 + monitor.wait(); + } + // 处理其他的业务逻辑 +} +复制代码 +#### 为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里? + +* 因为Java所有类的都继承了Object,Java想让任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。 + +* 有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。 + +#### 为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用? + +* 当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。 + +#### Thread 类中的 yield 方法有什么作用? + +* 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。 + +* 当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可能是当前线程,也可能是其他线程,看系统的分配了。 + +#### 为什么 Thread 类的 sleep()和 yield ()方法是静态的? + +* Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。 + +#### 线程的 sleep()方法和 yield()方法有什么区别? + +* (1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会; + +* (2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态; + +* (3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常; + +* (4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。 + +#### 如何停止一个正在运行的线程? + +* 在java中有以下3种方法可以终止正在运行的线程: + * 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。 + * 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。 + * 使用interrupt方法中断线程。 + +#### Java 中 interrupted 和 isInterrupted 方法的区别? + +* interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。 + + 注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。 + +* interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。 + +* isInterrupted:是可以返回当前中断信号是true还是false,与interrupt最大的差别 + +#### 什么是阻塞式方法? + +* 阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。 + +#### Java 中你怎样唤醒一个阻塞的线程? + +* 首先 ,wait()、notify() 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行; + +* 其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。 + +#### notify() 和 notifyAll() 有什么区别? + +* 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。 + +* notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。 + +* notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。 + +#### 如何在两个线程间共享数据? + +* 在两个线程间共享变量即可实现共享。 + +`一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性。` + +#### Java 如何实现多线程之间的通讯和协作? + +* 可以通过中断 和 共享变量的方式实现线程间的通讯和协作 + +* 比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。 + +* Java中线程通信协作的最常见方式: + + * 一.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll() + + * 二.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll() + +* 线程间直接的数据交换: + + * 三.通过管道进行线程间通信:字节流、字符流 + +#### 同步方法和同步块,哪个是更好的选择? + +* 同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。 + +* 同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。 + +`请知道一条原则:同步的范围越小越好。` + +#### 什么是线程同步和线程互斥,有哪几种实现方式? + +* 当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。 + +* 在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。 + +* 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。 + +* 线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。 + +* 用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。 + +* 实现线程同步的方法 + + * 同步代码方法:sychronized 关键字修饰的方法 + + * 同步代码块:sychronized 关键字修饰的代码块 + + * 使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制 + + * 使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义 + +#### 在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步? + +* 在 java 虚拟机中,监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。 + +* 一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码 + +* 另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案 + +#### 如果你提交任务时,线程池队列已满,这时会发生什么 + +* 有俩种可能: + + (1)如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务 + + (2)如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy + +#### 什么叫线程安全?servlet 是线程安全吗? + +* 线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。 + +* Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。 + +* Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。 + +* SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。 + +* Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。 + +#### 在 Java 程序中怎么保证多线程的运行安全? + +* 方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger + +* 方法二:使用自动锁 synchronized。 + +* 方法三:使用手动锁 Lock。 + +* 手动锁 Java 示例代码如下: + +```java +Lock lock = new ReentrantLock(); + lock. lock(); + try { + System. out. println("获得锁"); + } catch (Exception e) { + // TODO: handle exception + } finally { + System. out. println("释放锁"); + lock. unlock(); + } +``` + +#### 你对线程优先级的理解是什么? + +* 每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。 + +* Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。 + +* 当然,如果你真的想设置优先级可以通过setPriority()方法设置,但是设置了不一定会该变,这个是不准确的 + +#### 线程类的构造方法、静态块是被哪个线程调用的 + +* 这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。 + +* 如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么: + +(1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的 + +(2)Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的 + +#### Java 中怎么获取一份线程 dump 文件?你如何在 Java 中获取线程堆栈? + +* Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。 + +* 在 Linux 下,你可以通过命令 kill -3 PID (Java 进程的进程 ID)来获取 Java应用的 dump 文件。 + +* 在 Windows 下,你可以按下 Ctrl + Break 来获取。这样 JVM 就会将线程的 dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。 + +#### 一个线程运行时发生异常会怎样? + +* 如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。 + +#### Java 线程数过多会造成什么异常? + +* 线程的生命周期开销非常高 + +* 消耗过多的 CPU + + 资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。 + +* 降低稳定性JVM + + 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。 + +#### 多线程的常用方法 + +| 方法 名 | 描述 | +| --- | --- | +| sleep() | 强迫一个线程睡眠N毫秒 | +| isAlive() | 判断一个线程是否存活。 | +| join() | 等待线程终止。 | +| activeCount() | 程序中活跃的线程数。 | +| enumerate() | 枚举程序中的线程。 | +| currentThread() | 得到当前线程。 | +| isDaemon() | 一个线程是否为守护线程。 | +| setDaemon() | 设置一个线程为守护线程。 | +| setName() | 为线程设置一个名称。 | +| wait() | 强迫一个线程等待。 | +| notify() | 通知一个线程继续运行。 | +| setPriority() | 设置一个线程的优先级。 | + +## 并发理论 + +#### Java中垃圾回收有什么目的?什么时候进行垃圾回收? + +* 垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行的。 + +* 垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。 + +#### 线程之间如何通信及线程之间如何同步 + +* 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线程之间以如何来交换信息。一般线程之间的通信机制有两种:共享内存和消息传递。 + +* Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。 + +#### Java内存模型 + +* 共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab46986d99~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤: + 1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。 + 2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。 + +**下面通过示意图来说明线程之间的通信** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab7ff494ac~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 总结:什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。 + +#### 如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存? + +* 不会,在下一个垃圾回调周期中,这个对象将是被可回收的。 + +* 也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。 + +#### finalize()方法什么时候被调用?析构函数(finalization)的目的是什么? + +* 1.垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法; finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throws Throwable { } 在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间 + +* 1. GC本来就是内存回收了,应用还需要在finalization做什么呢? 答案是大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。 + * Finalizetion主要用来释放被对象占用的资源(不是指内存,而是指其他资源,比如文件(File Handle)、端口(ports)、数据库连接(DB Connection)等)。然而,它不能真正有效地工作。 + +#### 什么是重排序 + +* 程序执行的顺序按照代码的先后顺序执行。 +* 一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。 + +int a = 5; //语句1 +int r = 3; //语句2 +a = a + 2; //语句3 +r = a*a; //语句4 +复制代码 + +* 则因为重排序,他还可能执行顺序为(这里标注的是语句的执行顺序) 2-1-3-4,1-3-2-4 但绝不可能 2-1-4-3,因为这打破了依赖关系。 +* 显然重排序对单线程运行是不会有任何问题,但是多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。 + +#### 重排序实际执行的指令步骤 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab5818fe21~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 +2. 指令级并行的重排序。现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 +3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 + +* 这些重排序对于单线程没问题,但是多线程都可能会导致多线程程序出现内存可见性问题。 + +#### 重排序遵守的规则 + +* as-if-serial: + 1. 不管怎么排序,结果不能改变 + 2. 不存在数据依赖的可以被编译器和处理器重排序 + 3. 一个操作依赖两个操作,这两个操作如果不存在依赖可以重排序 + 4. 单线程根据此规则不会有问题,但是重排序后多线程会有问题 + +#### as-if-serial规则和happens-before规则的区别 + +* as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。 + +* as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。 + +* as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。 + +#### 并发关键字 synchronized ? + +* 在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。 + +* 另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 + +#### 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 + +**synchronized关键字最主要的三种使用方式:** + +* 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 +* 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。 +* 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 + +`总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!` + +#### 单例模式了解吗?给我解释一下双重检验锁方式实现单例模式!” + +**双重校验锁实现对象单例(线程安全)** + +**说明:** + +* 双锁机制的出现是为了解决前面同步问题和性能问题,看下面的代码,简单分析下确实是解决了多线程并行进来不会出现重复new对象,而且也实现了懒加载 + +```java + public class Singleton { + private volatile static Singleton uniqueInstance; + private Singleton() {} + + public static Singleton getUniqueInstance() { + //先判断对象是否已经实例过,没有实例化过才进入加锁代码 + if (uniqueInstance == null) { + //类对象加锁 + synchronized (Singleton.class) { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + } + } + return uniqueInstance; + } + } +``` +`另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。` + +* uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: + +1. 为 uniqueInstance 分配内存空间 +2. 初始化 uniqueInstance +3. 将 uniqueInstance 指向分配的内存地址 + +`但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。` + +`使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。` + +#### 说一下 synchronized 底层实现原理? + +* Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成, + +* 每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权 ,过程: + + 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 + + 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. + + 3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。 + +`synchronized是可以通过 反汇编指令 javap命令,查看相应的字节码文件。` + +#### synchronized可重入的原理 + +* 重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。 + +#### 什么是自旋 + +* 很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。 +* 忙循环:就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。 + +#### 多线程中 synchronized 锁升级的原理是什么? + +* synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。 + +`锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。` + +* 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。 + +* 轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,轻量级锁就会升级为重量级锁; + +* 重量级锁是synchronized ,是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。 + +#### 线程 B 怎么知道线程 A 修改了变量 + +* (1)volatile 修饰变量 + +* (2)synchronized 修饰修改变量的方法 + +* (3)wait/notify + +* (4)while 轮询 + +#### 当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B? + +* 不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。 + +#### synchronized、volatile、CAS 比较 + +* (1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。 + +* (2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。 + +* (3)CAS 是基于冲突检测的乐观锁(非阻塞) + +#### synchronized 和 Lock 有什么区别? + +* 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类; +* synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。 +* synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。 +* 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。 + +#### synchronized 和 ReentrantLock 区别是什么? + +* synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量 + +* synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。 + +* 相同点:两者都是可重入锁 + + 两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 + +* 主要区别如下: + + * ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作; + * ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁; + * ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。 + * 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word +* Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: + + * 普通同步方法,锁是当前实例对象 + * 静态同步方法,锁是当前类的class对象 + * 同步方法块,锁是括号里面的对象 + +#### volatile 关键字的作用 + +* 对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新值。 + +* 从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。 + +* volatile 常用于多线程环境下的单次操作(单次读或者单次写)。 + +#### Java 中能创建 volatile 数组吗? + +* 能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。 + +#### volatile 变量和 atomic 变量有什么不同? + +* volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。 + +* 而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。 + +#### volatile 能使得一个非原子操作变成原子操作吗? + +* 关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。 + +* 虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。 + +**所以从Oracle Java Spec里面可以看到:** + +* 对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。 +* 如果使用volatile修饰long和double,那么其读写都是原子操作 +* 对于64位的引用地址的读写,都是原子操作 +* 在实现JVM时,可以自由选择是否把读写long和double作为原子操作 +* 推荐JVM实现为原子操作 + +#### synchronized 和 volatile 的区别是什么? + +* synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。 + +* volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。 + +**区别** + +* volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。 + +* volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。 + +* volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。 + +* volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。 + +* volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。 + +#### final不可变对象,它对写并发应用有什么帮助? + +* 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。 + +* 不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。 + +* 只有满足如下状态,一个对象才是不可变的; + + * 它的状态不能在创建后再被修改; + + * 所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)。 + +`不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。` + +#### Lock 接口和synchronized 对比同步它有什么优势? + +* Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。 + +* 它的优势有: + + * (1)可以使锁更公平 + + * (2)可以使线程在等待锁的时候响应中断 + + * (3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间 + + * (4)可以在不同的范围,以不同的顺序获取和释放锁 + +* 整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。 + +#### 乐观锁和悲观锁的理解及如何实现,有哪些实现方式? + +* 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。 + +* 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。 + +#### 什么是 CAS + +* CAS 是 compare and swap 的缩写,即我们所说的比较交换。 + +* cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。 + +* CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。 + +`java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)` + +#### CAS 的会产生什么问题? + +* 1、ABA 问题: + + 比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。 + +* 2、循环时间长开销大: + + 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。 + +* 3、只能保证一个共享变量的原子操作: + + 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。 + +#### 什么是原子类 + +* java.util.concurrent.atomic包:是原子类的小工具包,支持在单个变量上解除锁的线程安全编程 原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读-改-写操作。 + +* 比如:AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(如果该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。 + +`简单来说就是原子类来实现CAS无锁模式的算法` + +#### 原子类的常用类 + +* AtomicBoolean +* AtomicInteger +* AtomicLong +* AtomicReference + +#### 说一下 Atomic的原理? + +* Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。 + +#### 死锁与活锁的区别,死锁与饥饿的区别? + +* 死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 + +* 活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。 + +* 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。 + +* 饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。 + + Java 中导致饥饿的原因: + + * 1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。 + + * 2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。 + + * 3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。 + +## 线程池 + +#### 什么是线程池? + +* Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来许多好处。 + * 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 + * 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 + * 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用 + +#### 线程池作用? + +* 线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。 + +* 如果一个线程所需要执行的时间非常长的话,就没必要用线程池了(不是不能作长时间操作,而是不宜。本来降低线程创建和销毁,结果你那么久我还不好控制还不如直接创建线程),况且我们还不能控制线程池中线程的开始、挂起、和中止。 + +#### 线程池有什么优点? + +* 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。 + +* 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 + +* 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +* 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。 + +#### 什么是ThreadPoolExecutor? + +* **ThreadPoolExecutor就是线程池** + + ThreadPoolExecutor其实也是JAVA的一个类,我们一般通过Executors工厂类的方法,通过传入不同的参数,就可以构造出适用于不同应用场景下的ThreadPoolExecutor(线程池) + +构造参数图: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172babf4c562f1~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)`构造参数参数介绍:`corePoolSize 核心线程数量 +maximumPoolSize 最大线程数量 +keepAliveTime 线程保持时间,N个时间单位 +unit 时间单位(比如秒,分) +workQueue 阻塞队列 +threadFactory 线程工厂 +handler 线程池拒绝策略 +复制代码 +#### 什么是Executors? + +* **Executors框架实现的就是线程池的功能。** + + Executors工厂类中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池, + +Executor工厂类如何创建线程池图: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab8fc18fd3~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 线程池四种创建方式? + +* **Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:** + 1. newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 + 2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 + 3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。 + 4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 + +#### 在 Java 中 Executor 和 Executors 的区别? + +* Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。 + +* Executor 接口对象能执行我们的线程任务。 + +* ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。 + +* 使用 ThreadPoolExecutor 可以创建自定义线程池。 + +#### 四种构建线程池的区别及特点? + +##### 1\. newCachedThreadPool + +* **特点**:newCachedThreadPool创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活的回收空闲的线程,当需要增加时, 它可以灵活的添加新的线程,而不会对池的长度作任何限制 + +* **缺点**:他虽然可以无线的新建线程,但是容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值 + +* **总结**:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。 + +* **代码示例:** + +```java +package com.lijie; + + import java.util.concurrent.ExecutorService; + import java.util.concurrent.Executors; + + public class TestNewCachedThreadPool { + public static void main(String[] args) { + // 创建无限大小线程池,由jvm自动回收 + ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); + for (int i = 0; i < 10; i++) { + final int temp = i; + newCachedThreadPool.execute(new Runnable() { + public void run() { + try { + Thread.sleep(100); + } catch (Exception e) { + } + System.out.println(Thread.currentThread().getName() + ",i==" + temp); + } + }); + } + } + } + +``` + +##### 2.newFixedThreadPool + +* **特点**:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。 + +* **缺点**:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,容易导致OOM(超出内存空间) + +* **总结**:请求的挤压一定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors() + +`Runtime.getRuntime().availableProcessors()方法是查看电脑CPU核心数量)` + +* **代码示例:** + +```java +package com.lijie; + + import java.util.concurrent.ExecutorService; + import java.util.concurrent.Executors; + + public class TestNewFixedThreadPool { + public static void main(String[] args) { + ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3); + for (int i = 0; i < 10; i++) { + final int temp = i; + newFixedThreadPool.execute(new Runnable() { + public void run() { + System.out.println(Thread.currentThread().getName() + ",i==" + temp); + } + }); + } + } + } + +``` + +##### 3.newScheduledThreadPool + +* **特点**:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于Timer(Timer是Java的一个定时器类) + +* **缺点**:由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。 + +* **代码示例:** + +```java +package com.lijie; + + import java.util.concurrent.Executors; + import java.util.concurrent.ScheduledExecutorService; + import java.util.concurrent.TimeUnit; + + public class TestNewScheduledThreadPool { + public static void main(String[] args) { + //定义线程池大小为3 + ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3); + for (int i = 0; i < 10; i++) { + final int temp = i; + newScheduledThreadPool.schedule(new Runnable() { + public void run() { + System.out.println("i:" + temp); + } + }, 3, TimeUnit.SECONDS);//这里表示延迟3秒执行。 + } + } + } + +``` + +##### 4.newSingleThreadExecutor + +* **特点**:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 + +* **缺点**:缺点的话,很明显,他是单线程的,高并发业务下有点无力 + +* **总结**:保证所有任务按照指定顺序执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它 + +* **代码示例:** + +```java +package com.lijie; + + import java.util.concurrent.ExecutorService; + import java.util.concurrent.Executors; + + public class TestNewSingleThreadExecutor { + public static void main(String[] args) { + ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor(); + for (int i = 0; i < 10; i++) { + final int index = i; + newSingleThreadExecutor.execute(new Runnable() { + public void run() { + System.out.println(Thread.currentThread().getName() + " index:" + index); + try { + Thread.sleep(200); + } catch (Exception e) { + } + } + }); + } + } + } + +``` + +#### 线程池都有哪些状态? + +* RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。 +* SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。 +* STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。 +* TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。 +* TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。 + +#### 线程池中 submit() 和 execute() 方法有什么区别? + +* 相同点: + * 相同点就是都可以开启线程执行池中的任务。 +* 不同点: + * 接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。 + * 返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有 + * 异常处理:submit()方便Exception处理 + +#### 什么是线程组,为什么在 Java 中不推荐使用? + +* ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。 + +* 线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。 + +* 为什么不推荐使用线程组?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。 + +#### ThreadPoolExecutor饱和策略有哪些? + +`如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:` + +* ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。 +* ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 +* ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。 +* ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。 + +#### 如何自定义线程线程池? + +* 先看ThreadPoolExecutor(线程池)这个类的构造参数 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab8f7d464b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)构造参数参数介绍: +```java + corePoolSize 核心线程数量 + maximumPoolSize 最大线程数量 + keepAliveTime 线程保持时间,N个时间单位 + unit 时间单位(比如秒,分) + workQueue 阻塞队列 + threadFactory 线程工厂 + handler 线程池拒绝策略 + +``` +* 代码示例: + +```java +package com.lijie; + + import java.util.concurrent.ArrayBlockingQueue; + import java.util.concurrent.ThreadPoolExecutor; + import java.util.concurrent.TimeUnit; + + public class Test001 { + public static void main(String[] args) { + //创建线程池 + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3)); + for (int i = 1; i <= 6; i++) { + TaskThred t1 = new TaskThred("任务" + i); + //executor.execute(t1);是执行线程方法 + executor.execute(t1); + } + //executor.shutdown()不再接受新的任务,并且等待之前提交的任务都执行完再关闭,阻塞队列中的任务不会再执行。 + executor.shutdown(); + } + } + + class TaskThred implements Runnable { + private String taskName; + + public TaskThred(String taskName) { + this.taskName = taskName; + } + public void run() { + System.out.println(Thread.currentThread().getName() + taskName); + } + } + +``` + +#### 线程池的执行原理? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac2446a113~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 提交一个任务到线程池中,线程池的处理流程如下: + + 1. 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。 + + 2. 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。 + + 3. 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。 + +#### 如何合理分配线程池大小? + +* 要合理的分配线程池的大小要根据实际情况来定,简单的来说的话就是根据CPU密集和IO密集来分配 + +##### 什么是CPU密集 + +* CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。 + +* CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那样。 + +##### 什么是IO密集 + +* IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。 + +##### 分配CPU和IO密集: + +1. CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务 + +2. IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数 + +##### 精确来说的话的话: + +* 从以下几个角度分析任务的特性: + + * 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。 + + * 任务的优先级:高、中、低。 + + * 任务的执行时间:长、中、短。 + + * 任务的依赖性:是否依赖其他系统资源,如数据库连接等。 + +**可以得出一个结论:** + +* 线程等待时间比CPU执行时间比例越高,需要越多线程。 +* 线程CPU执行时间比等待时间比例越高,需要越少线程。 + +## 并发容器 + +#### 你经常使用什么并发容器,为什么? + +* 答:Vector、ConcurrentHashMap、HasTable + +* 一般软件开发中容器用的最多的就是HashMap、ArrayList,LinkedList ,等等 + +* 但是在多线程开发中就不能乱用容器,如果使用了未加锁(非同步)的的集合,你的数据就会非常的混乱。由此在多线程开发中需要使用的容器必须是加锁(同步)的容器。 + +#### 什么是Vector + +* Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,访问它比访问ArrayList慢很多 + + (`ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。ArrayList的缺点是每个元素之间不能有间隔。`) + +#### ArrayList和Vector有什么不同之处? + +* Vector方法带上了synchronized关键字,是线程同步的 + +1. ArrayList添加方法源码![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac57d8b760~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +2. Vector添加源码(加锁了synchronized关键字)![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac5bc35f22~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +#### 为什么HashTable是线程安全的? + +* 因为HasTable的内部方法都被synchronized修饰了,所以是线程安全的。其他的都和HashMap一样 + +1. HashMap添加方法的源码![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac5c47d65f~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +2. HashTable添加方法的源码![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac599568da~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +#### 用过ConcurrentHashMap,讲一下他和HashTable的不同之处? + +* ConcurrentHashMap是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。 + +* 看不懂???很正常,我也看不懂 + +* 总结: + + 1. HashTable就是实现了HashMap加上了synchronized,而ConcurrentHashMap底层采用分段的数组+链表实现,线程安全 + 2. ConcurrentHashMap通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。 + 3. 并且读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。 + 4. Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术 + 5. 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容 + +#### Collections.synchronized * 是什么? + +`注意:* 号代表后面是还有内容的` + +* 此方法是干什么的呢,他完完全全的可以把List、Map、Set接口底下的集合变成线程安全的集合 + +* Collections.synchronized * :原理是什么,我猜的话是代理模式:[Java代理模式理解](https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Fweixin_43122090%2Farticle%2Fdetails%2F104883274 "https://blog.csdn.net/weixin_43122090/article/details/104883274") + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac6e2aff60~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### Java 中 ConcurrentHashMap 的并发度是什么? + +* ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。 + +* 在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。 + +#### 什么是并发容器的实现? + +* 何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。 + +* 并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。 + +#### Java 中的同步集合与并发集合有什么区别? + +* 同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5 介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。 + +#### SynchronizedMap 和 ConcurrentHashMap 有什么区别? + +* SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。 + +* ConcurrentHashMap 使用分段锁来保证在多线程下的性能。 + +* ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。 + +* 这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。 + +* 另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。 + +#### CopyOnWriteArrayList 是什么? + +* CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,缺少一个前提条件,那就是非复合场景下操作它是线程安全的。 + +* CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。 + +#### CopyOnWriteArrayList 的使用场景? + +* 合适读多写少的场景。 + +#### CopyOnWriteArrayList 的缺点? + +* 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。 +* 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。 +* 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。 + +#### CopyOnWriteArrayList 的设计思想? + +* 读写分离,读和写分开 +* 最终一致性 +* 使用另外开辟空间的思路,来解决并发冲突 + +## 并发队列 + +#### 什么是并发队列: + +* 消息队列很多人知道:消息队列是分布式系统中重要的组件,是系统与系统直接的通信 + +* 并发队列是什么:并发队列多个线程以有次序共享数据的重要组件 + +#### 并发队列和并发集合的区别: + +`那就有可能要说了,我们并发集合不是也可以实现多线程之间的数据共享吗,其实也是有区别的:` + +* 队列遵循“先进先出”的规则,可以想象成排队检票,队列一般用来解决大数据量采集处理和显示的。 + +* 并发集合就是在多个线程中共享数据的 + +#### 怎么判断并发队列是阻塞队列还是非阻塞队列 + +* 在并发队列上JDK提供了Queue接口,一个是以Queue接口下的BlockingQueue接口为代表的阻塞队列,另一个是高性能(无堵塞)队列。 + +#### 阻塞队列和非阻塞队列区别 + +* 当队列阻塞队列为空的时,从队列中获取元素的操作将会被阻塞。 + +* 或者当阻塞队列是满时,往队列里添加元素的操作会被阻塞。 + +* 或者试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。 + +* 试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来 + +#### 常用并发列队的介绍: + +1. **非堵塞队列:** + + 1. **ArrayDeque, (数组双端队列)** + + ArrayDeque (非堵塞队列)是JDK容器中的一个双端队列实现,内部使用数组进行元素存储,不允许存储null值,可以高效的进行元素查找和尾部插入取出,是用作队列、双端队列、栈的绝佳选择,性能比LinkedList还要好。 + + 2. **PriorityQueue, (优先级队列)** + + PriorityQueue (非堵塞队列) 一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不允许使用 null 元素也不允许插入不可比较的对象 + + 3. **ConcurrentLinkedQueue, (基于链表的并发队列)** + + ConcurrentLinkedQueue (非堵塞队列): 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能。ConcurrentLinkedQueue的性能要好于BlockingQueue接口,它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。该队列不允许null元素。 + +2. **堵塞队列:** + + 1. **DelayQueue, (基于时间优先级的队列,延期阻塞队列)** + + DelayQueue是一个没有边界BlockingQueue实现,加入其中的元素必需实现Delayed接口。当生产者线程调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最早到期的,越往后到期时间赿晚。 + + 2. **ArrayBlockingQueue, (基于数组的并发阻塞队列)** + + ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue是以先进先出的方式存储数据 + + 3. **LinkedBlockingQueue, (基于链表的FIFO阻塞队列)** + + LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。 + + 4. **LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)** + + LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。 + + 相比于其他阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以last结尾的方法,表示插入、获取获移除双端队列的最后一个元素。 + + LinkedBlockingDeque是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE。 + + 5. **PriorityBlockingQueue, (带优先级的无界阻塞队列)** + + priorityBlockingQueue是一个无界队列,它没有限制,在内存允许的情况下可以无限添加元素;它又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现comparable接口。 + + 6. **SynchronousQueue (并发同步阻塞队列)** + + SynchronousQueue是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。 + + 将这个类称为队列有点夸大其词。这更像是一个点。 + +#### 并发队列的常用方法 + +`不管是那种列队,是那个类,当是他们使用的方法都是差不多的` + +| 方法名 | 描述 | +| --- | --- | +| add() | 在不超出队列长度的情况下插入元素,可以立即执行,成功返回true,如果队列满了就抛出异常。 | +| offer() | 在不超出队列长度的情况下插入元素的时候则可以立即在队列的尾部插入指定元素,成功时返回true,如果此队列已满,则返回false。 | +| put() | 插入元素的时候,如果队列满了就进行等待,直到队列可用。 | +| take() | 从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。 | +| poll(long timeout, TimeUnit unit) | 在给定的时间里,从队列中获取值,如果没有取到会抛出异常。 | +| remainingCapacity() | 获取队列中剩余的空间。 | +| remove(Object o) | 从队列中移除指定的值。 | +| contains(Object o) | 判断队列中是否拥有该值。 | +| drainTo(Collection c) | 将队列中值,全部移除,并发设置到给定的集合中。 | + +## 并发工具类 + +#### 常用的并发工具类有哪些? + +* CountDownLatch + + CountDownLatch 类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他3个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。 + +* CyclicBarrier (回环栅栏) CyclicBarrier它的作用就是会让所有线程都等待完成后才会继续下一步行动。 + + CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。 + + CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。 + +* Semaphore (信号量) Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量(允许自定义多少线程同时访问)。就这一点而言,单纯的synchronized 关键字是实现不了的。 + + Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。它的用法如下: + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125755293710 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\274\202\345\270\270.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\274\202\345\270\270.md" new file mode 100644 index 0000000..566eeaa --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\274\202\345\270\270.md" @@ -0,0 +1,739 @@ + + +## Java异常架构与异常关键字 + +### Java异常简介 + +* Java异常是Java提供的一种识别及响应错误的一致性机制。 + Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪”抛出,异常信息回答了“为什么”会抛出。 + +### Java异常架构 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717840de0260d11~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 1\. Throwable + +* Throwable 是 Java 语言中所有错误与异常的超类。 + +* Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。 + +* Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。 + +#### 2\. Error(错误) + +* **定义**:Error 类及其子类。程序中无法处理的错误,表示运行应用程序中出现了严重的错误。 + +* **特点**:此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。 + +* 这些错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的! + +#### 3\. Exception(异常) + +* 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。 + +##### 运行时异常 + +* **定义**:RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 + +* **特点**:Java 编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。比如NullPointerException空指针异常、ArrayIndexOutBoundException数组下标越界异常、ClassCastException类型转换异常、ArithmeticExecption算术异常。此类异常属于不受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。虽然 Java 编译器不会检查运行时异常,但是我们也可以通过 throws 进行声明抛出,也可以通过 try-catch 对它进行捕获处理。如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生! + +* RuntimeException 异常会由 Java 虚拟机自动抛出并自动捕获(**就算我们没写异常捕获语句运行时也会抛出错误**!!),此类异常的出现绝大数情况是代码本身有问题应该从逻辑上去解决并改进代码。 + +##### 编译时异常 + +* **定义**: Exception 中除 RuntimeException 及其子类之外的异常。 + +* **特点**: Java 编译器会检查它。如果程序中出现此类异常,比如 ClassNotFoundException(没有找到指定的类异常),IOException(IO流异常),要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。**该异常我们必须手动在代码里添加捕获语句来处理该异常**。 + +#### 4\. 受检异常与非受检异常 + +* Java 的所有异常可以分为受检异常(checked exception)和非受检异常(unchecked exception)。 + +##### 受检异常 + +* 编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。**除 RuntimeException 及其子类外,其他的 Exception 异常都属于受检异常**。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过。 + +##### 非受检异常 + +* 编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。**该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。** + +### Java异常关键字 + +* **try** – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。 +* **catch** – 用于捕获异常。catch用来捕获try语句块中发生的异常。 +* **finally** – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。 +* **throw** – 用于抛出异常。 +* **throws** – 用在方法签名中,用于声明该方法可能抛出的异常。 + +## Java异常处理 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717840de2de5ccf~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* Java 通过面向对象的方法进行异常处理,一旦方法抛出异常,系统自动根据该异常对象寻找合适异常处理器(Exception Handler)来处理该异常,把各种不同的异常进行分类,并提供了良好的接口。在 Java 中,每个异常都是一个对象,它是 Throwable 类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。Java 的异常处理是通过 5 个关键词来实现的:try、catch、throw、throws 和 finally。 + +* 在Java应用中,异常的处理机制分为声明异常,抛出异常和捕获异常。 + +### 声明异常 + +* 通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 **throws** 关键字声明可能会抛出的异常。 + +**注意** + +* 非检查异常(Error、RuntimeException 或它们的子类)不可使用 throws 关键字来声明要抛出的异常。 +* 一个方法出现编译时异常,就需要 try-catch/ throws 处理,否则会导致编译错误。 + +### 抛出异常 + +* 如果你觉得解决不了某些异常问题,且不需要调用者处理,那么你可以抛出异常。 + +* throw关键字作用是在方法内部抛出一个`Throwable`类型的异常。任何Java代码都可以通过throw语句抛出异常。 + +### 捕获异常 + +* 程序通常在运行之前不报错,但是运行后可能会出现某些未知的错误,但是还不想直接抛出到上一级,那么就需要通过try…catch…的形式进行异常捕获,之后根据不同的异常情况来进行相应的处理。 + +### 如何选择异常类型 + +* 可以根据下图来选择是捕获异常,声明异常还是抛出异常 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717840de32f26f7~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 常见异常处理方式 + +#### 直接抛出异常 + +* 通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 **throws** 关键字声明可能会抛出的异常。 + +private static void readFile(String filePath) throws IOException { + File file = new File(filePath); + String result; + BufferedReader reader = new BufferedReader(new FileReader(file)); + while((result = reader.readLine())!=null) { + System.out.println(result); + } + reader.close(); +} +复制代码 +#### 封装异常再抛出 + +* 有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。 + +private static void readFile(String filePath) throws MyException { + try { + // code + } catch (IOException e) { + MyException ex = new MyException("read file failed."); + ex.initCause(e); + throw ex; + } +} +复制代码 +#### 捕获异常 + +* 在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理 + +private static void readFile(String filePath) { + try { + // code + } catch (FileNotFoundException e) { + // handle FileNotFoundException + } catch (IOException e){ + // handle IOException + } +} +复制代码 + +* 同一个 catch 也可以捕获多种类型异常,用 | 隔开 + +private static void readFile(String filePath) { + try { + // code + } catch (FileNotFoundException | UnknownHostException e) { + // handle FileNotFoundException or UnknownHostException + } catch (IOException e){ + // handle IOException + } +} +复制代码 +#### 自定义异常 + +* 习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用) + +public class MyException extends Exception { + public MyException(){ } + public MyException(String msg){ + super(msg); + } + // ... +} +复制代码 +#### try-catch-finally + +* 当方法中发生异常,异常处之后的代码不会再执行,如果之前获取了一些本地资源需要释放,则需要在方法正常结束时和 catch 语句中都调用释放本地资源的代码,显得代码比较繁琐,finally 语句可以解决这个问题。 + +private static void readFile(String filePath) throws MyException { + File file = new File(filePath); + String result; + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(file)); + while((result = reader.readLine())!=null) { + System.out.println(result); + } + } catch (IOException e) { + System.out.println("readFile method catch block."); + MyException ex = new MyException("read file failed."); + ex.initCause(e); + throw ex; + } finally { + System.out.println("readFile method finally block."); + if (null != reader) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} +复制代码 + +* 调用该方法时,读取文件时若发生异常,代码会进入 catch 代码块,之后进入 finally 代码块;若读取文件时未发生异常,则会跳过 catch 代码块直接进入 finally 代码块。所以无论代码中是否发生异常,fianlly 中的代码都会执行。 + +* 若 catch 代码块中包含 return 语句,finally 中的代码还会执行吗?将以上代码中的 catch 子句修改如下: + +catch (IOException e) { + System.out.println("readFile method catch block."); + return; +} +复制代码 + +* 调用 readFile 方法,观察当 catch 子句中调用 return 语句时,finally 子句是否执行 + +readFile method catch block. +readFile method finally block. +复制代码 + +* 可见,即使 catch 中包含了 return 语句,finally 子句依然会执行。若 finally 中也包含 return 语句,finally 中的 return 会覆盖前面的 return. + +#### try-with-resource + +* 上面例子中,finally 中的 close 方法也可能抛出 IOException, 从而覆盖了原始异常。JAVA 7 提供了更优雅的方式来实现资源的自动释放,自动释放的资源需要是实现了 AutoCloseable 接口的类。 + +private static void tryWithResourceTest(){ + try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){ + // code + } catch (IOException e){ + // handle exception + } +} +复制代码 + +* try 代码块退出时,会自动调用 scanner.close 方法,和把 scanner.close 方法放在 finally 代码块中不同的是,若 scanner.close 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取。 + +## Java异常常见面试题 + +### 1\. Error 和 Exception 区别是什么? + +* Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA 应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复; + +* Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。 + +### 2\. 运行时异常和一般异常(受检异常)区别是什么? + +* 运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。 + +* 受检异常是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检异常。 + +* **RuntimeException异常和受检异常之间的区别**:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。 + +### 3\. JVM 是如何处理异常的? + +* 在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。 + +* JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。 + +### 4\. throw 和 throws 的区别是什么? + +* Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。 + +**throws 关键字和 throw 关键字在使用上的几点区别如下**: + +* throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。 +* throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。 + +### 5\. final、finally、finalize 有什么区别? + +* final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。 +* finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。 +* finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。 + +### 6\. NoClassDefFoundError 和 ClassNotFoundException 区别? + +* NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。 + +* 引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致; + +* ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。 + +### 7\. try-catch-finally 中哪个部分可以省略? + +* 答:catch 可以省略 + +**原因** + +* 更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。 + +* 理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。 + +* 至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。 + +### 8\. try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗? + +* 答:会执行,在 return 前执行。 + +* **注意**:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。 + +**代码示例1:** + +public static int getInt() { + int a = 10; + try { + System.out.println(a / 0); + a = 20; + } catch (ArithmeticException e) { + a = 30; + return a; + /* + * return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了 + * 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40 + * 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30 + */ + } finally { + a = 40; + } + return a; +} +复制代码 + +* 执行结果:30 + +**代码示例2:** + +public static int getInt() { + int a = 10; + try { + System.out.println(a / 0); + a = 20; + } catch (ArithmeticException e) { + a = 30; + return a; + } finally { + a = 40; + //如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40 + return a; + } + +} +复制代码 + +* 执行结果:40 + +### 9\. 类 ExampleA 继承 Exception,类 ExampleB 继承ExampleA。 + +* 有如下代码片断: + +try { + throw new ExampleB("b") +} catch(ExampleA e){ + System.out.println("ExampleA"); +} catch(Exception e){ + System.out.println("Exception"); +} +复制代码 + +* 请问执行此段代码的输出是什么? + +* **答**:输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类型],抓取 ExampleA 类型异常的 catch 块能够抓住 try 块中抛出的 ExampleB 类型的异常) + +* 面试题 - 说出下面代码的运行结果。(此题的出处是《Java 编程思想》一书) + +class Annoyance extends Exception { +} +class Sneeze extends Annoyance { +} +class Human { + public static void main(String[] args) + throws Exception { + try { + try { + throw new Sneeze(); + } catch ( Annoyance a ) { + System.out.println("Caught Annoyance"); + throw a; + } + } catch ( Sneeze s ) { + System.out.println("Caught Sneeze"); + return ; + } finally { + System.out.println("Hello World!"); + } + } +} +复制代码 + +* 结果 + +Caught Annoyance +Caught Sneeze +Hello World! +复制代码 +### 10\. 常见的 RuntimeException 有哪些? + +* ClassCastException(类转换异常) +* IndexOutOfBoundsException(数组越界) +* NullPointerException(空指针) +* ArrayStoreException(数据存储异常,操作数组时类型不一致) +* 还有IO操作的BufferOverflowException异常 + +### 11\. Java常见异常有哪些 + +* java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。 + +* java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常. + +* java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。 + +* java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。 + +* java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。 + +* java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。 + +* java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。 + +* java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。 + +* java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。 + +* java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。 + +* java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。 + +* java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。- + +* java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。 + +* java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。 + +* java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。 + +## Java异常处理最佳实践 + +* 在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范进行异常处理的原因。而团队之间的这些规范往往是截然不同的。 + +* 本文给出几个被很多团队使用的异常处理最佳实践。 + +### 1\. 在 finally 块中清理资源或者使用 try-with-resource 语句 + +* 当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在try块的最后关闭资源。 + +public void doNotCloseResourceInTry() { + FileInputStream inputStream = null; + try { + File file = new File("./tmp.txt"); + inputStream = new FileInputStream(file); + // use the inputStream to read a file + // do NOT do this + inputStream.close(); + } catch (FileNotFoundException e) { + log.error(e); + } catch (IOException e) { + log.error(e); + } +} +复制代码 + +* 问题就是,只有没有异常抛出的时候,这段代码才可以正常工作。try 代码块内代码会正常执行,并且资源可以正常关闭。但是,使用 try 代码块是有原因的,一般调用一个或多个可能抛出异常的方法,而且,你自己也可能会抛出一个异常,这意味着代码可能不会执行到 try 代码块的最后部分。结果就是,你并没有关闭资源。 + +所以,你应该把清理工作的代码放到 finally 里去,或者使用 try-with-resource 特性。 + +#### 1.1 使用 finally 代码块 + +* 与前面几行 try 代码块不同,finally 代码块总是会被执行。不管 try 代码块成功执行之后还是你在 catch 代码块中处理完异常后都会执行。因此,你可以确保你清理了所有打开的资源。 + +public void closeResourceInFinally() { + FileInputStream inputStream = null; + try { + File file = new File("./tmp.txt"); + inputStream = new FileInputStream(file); + // use the inputStream to read a file + } catch (FileNotFoundException e) { + log.error(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + log.error(e); + } + } + } +} +复制代码 +#### 1.2 Java 7 的 try-with-resource 语法 + +* 如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。 + +public void automaticallyCloseResource() { + File file = new File("./tmp.txt"); + try (FileInputStream inputStream = new FileInputStream(file);) { + // use the inputStream to read a file + } catch (FileNotFoundException e) { + log.error(e); + } catch (IOException e) { + log.error(e); + } +} + +复制代码 + +​ + +### 2\. 优先明确的异常 + +* 你抛出的异常越明确越好,永远记住,你的同事或者几个月之后的你,将会调用你的方法并且处理异常。 + +* 因此需要保证提供给他们尽可能多的信息。这样你的 API 更容易被理解。你的方法的调用者能够更好的处理异常并且避免额外的检查。 + +* 因此,总是尝试寻找最适合你的异常事件的类,例如,抛出一个 NumberFormatException 来替换一个 IllegalArgumentException 。避免抛出一个不明确的异常。 + +public void doNotDoThis() throws Exception { + ... +} +public void doThis() throws NumberFormatException { + ... +} + +复制代码 + +​ + +### 3\. 对异常进行文档说明 + +* 当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避免或处理异常。 + 在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景。 + +public void doSomething(String input) throws MyBusinessException { + ... +} + +复制代码 +### 4\. 使用描述性消息抛出异常 + +* 在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。 + +* 但这里并不是说要对错误信息长篇大论,因为本来 Exception 的类名就能够反映错误的原因,因此只需要用一到两句话描述即可。 + +* 如果抛出一个特定的异常,它的类名很可能已经描述了这种错误。所以,你不需要提供很多额外的信息。一个很好的例子是 NumberFormatException 。当你以错误的格式提供 String 时,它将被 java.lang.Long 类的构造函数抛出。 + +try { + new Long("xyz"); +} catch (NumberFormatException e) { + log.error(e); +} + +复制代码 +### 5\. 优先捕获最具体的异常 + +* 大多数 IDE 都可以帮助你实现这个最佳实践。当你尝试首先捕获较不具体的异常时,它们会报告无法访问的代码块。 + +* 但问题在于,只有匹配异常的第一个 catch 块会被执行。 因此,如果首先捕获 IllegalArgumentException ,则永远不会到达应该处理更具体的 NumberFormatException 的 catch 块,因为它是 IllegalArgumentException 的子类。 + +* 总是优先捕获最具体的异常类,并将不太具体的 catch 块添加到列表的末尾。 + +* 你可以在下面的代码片断中看到这样一个 try-catch 语句的例子。 第一个 catch 块处理所有 NumberFormatException 异常,第二个处理所有非 NumberFormatException 异常的IllegalArgumentException 异常。 + +public void catchMostSpecificExceptionFirst() { + try { + doSomething("A message"); + } catch (NumberFormatException e) { + log.error(e); + } catch (IllegalArgumentException e) { + log.error(e) + } +} + +复制代码 + +​ + +### 6\. 不要捕获 Throwable 类 + +* Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做! + +* 如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。 + +* 所以,最好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。 + +public void doNotCatchThrowable() { + try { + // do something + } catch (Throwable t) { + // don't do this! + } +} + +复制代码 +### 7\. 不要忽略异常 + +* 很多时候,开发者很有自信不会抛出异常,因此写了一个catch块,但是没有做任何处理或者记录日志。 + +public void doNotIgnoreExceptions() { + try { + // do something + } catch (NumberFormatException e) { + // this will never happen + } +} + +复制代码 + +* 但现实是经常会出现无法预料的异常,或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。 + +* 合理的做法是至少要记录异常的信息。 + +public void logAnException() { + try { + // do something + } catch (NumberFormatException e) { + log.error("This should never happen: " + e); + } +} + +复制代码 +### 8\. 不要记录并抛出异常 + +* 这可能是本文中最常被忽略的最佳实践。可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。如下: + +try { + new Long("xyz"); +} catch (NumberFormatException e) { + log.error(e); + throw e; +} + +复制代码 + +* 这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志。如下: + +17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz" +Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz" +at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) +at java.lang.Long.parseLong(Long.java:589) +at java.lang.Long.(Long.java:965) +at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63) +at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58) + +复制代码 + +* 如上所示,后面的日志也没有附加更有用的信息。如果想要提供更加有用的信息,那么可以将异常包装为自定义异常。 + +public void wrapException(String input) throws MyBusinessException { + try { + // do something + } catch (NumberFormatException e) { + throw new MyBusinessException("A message that describes the error.", e); + } +} + +复制代码 + +* 因此,仅仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。 + +### 9\. 包装异常时不要抛弃原始的异常 + +* 捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。 + 在你这样做时,请确保将原始异常设置为原因(注:参考下方代码 NumberFormatException e 中的原始异常 e )。Exception 类提供了特殊的构造函数方法,它接受一个 Throwable 作为参数。否则,你将会丢失堆栈跟踪和原始异常的消息,这将会使分析导致异常的异常事件变得困难。 + +public void wrapException(String input) throws MyBusinessException { + try { + // do something + } catch (NumberFormatException e) { + throw new MyBusinessException("A message that describes the error.", e); + } +} + +复制代码 + +​ + +### 10\. 不要使用异常控制程序的流程 + +* 不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处理,这是非常不好的习惯,会严重影响应用的性能。 + +### 11\. 使用标准异常 + +* 如果使用内建的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。 + +### 12\. 异常会影响性能 + +* 异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。 + + * 仅在异常情况下使用异常; + * 在可恢复的异常情况下使用异常; +* 尽管使用异常有利于 Java 开发,但是在应用中最好不要捕获太多的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰到好处的信息。 + +### 13\. 总结 + +* 综上所述,当你抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代码的可读性或者 API 的可用性。 + +* 异常不仅仅是一个错误控制机制,也是一个通信媒介。因此,为了和同事更好的合作,一个团队必须要制定出一个最佳实践和规则,只有这样,团队成员才能理解这些通用概念,同时在工作中使用它。 + +### 异常处理-阿里巴巴Java开发手册 + +1. 【强制】Java 类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException等等。 说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catch NumberFormatException来实现。 正例:if (obj != null) {…} 反例:try { obj.method(); } catch (NullPointerException e) {…} + +2. 【强制】异常不要用来做流程控制,条件控制。 说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。 + +3. 【强制】catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。 说明:对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。 正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。 + +4. 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。 + +5. 【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。 + +6. 【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。 说明:如果JDK7及以上,可以使用try-with-resources方式。 + +7. 【强制】不要在finally块中使用return。 说明:try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。 反例: + + private int x = 0; + public int checkReturn() { + try { + // x等于1,此处不返回 + return ++x; + } finally { + // 返回的结果是2 + return ++x; + } + } + +复制代码 + +1. 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。 说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。 + +2. 【强制】在调用RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用Throwable类来进行拦截。 说明:通过反射机制来调用方法,如果找不到方法,抛出NoSuchMethodException。什么情况会抛出NoSuchMethodError呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出NoSuchMethodError。 + +3. 【推荐】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。 说明:本手册明确防止NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。 + +4. 【推荐】防止NPE,是程序员的基本修养,注意NPE产生的场景: 1) 返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱有可能产生NPE。 反例:public int f() { return Integer对象}, 如果为null,自动解箱抛NPE。 2) 数据库的查询结果可能为null。 3) 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。 4) 远程调用返回对象时,一律要求进行空指针判断,防止NPE。 5) 对于Session中获取的数据,建议进行NPE检查,避免空指针。 6) 级联调用obj.getA().getB().getC();一连串调用,易产生NPE。 + 正例:使用JDK8的Optional类来防止NPE问题。 + +5. 【推荐】定义时区分unchecked / checked 异常,避免直接抛出new RuntimeException(),更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException等。 + +6. 【参考】对于公司外的http/api开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用Result方式,封装isSuccess()方法、“错误码”、“错误简短信息”。 说明:关于RPC方法返回方式使用Result方式的理由: 1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。 2)如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。 + +7. 【参考】避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。 说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。 正例:一个类中有多个public方法,都需要进行数行相同的参数校验操作,这个时候请抽取: + private boolean checkParam(DTO dto) {…} + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904128959741965 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\350\231\232\346\213\237\346\234\272.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\350\231\232\346\213\237\346\234\272.md" new file mode 100644 index 0000000..382dac4 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\350\231\232\346\213\237\346\234\272.md" @@ -0,0 +1,627 @@ + + +## Java内存模型 + +### 我们开发人员编写的Java代码是怎么让电脑认识的 + +* 首先先了解电脑是二进制的系统,他只认识 01010101 + +* 比如我们经常要编写 HelloWord.java 电脑是怎么认识运行的 + +* HelloWord.java是我们程序员编写的,我们人可以认识,但是电脑不认识 + +**Java文件编译的过程** + +1. 程序员编写的.java文件 +2. 由javac编译成字节码文件.class:(为什么编译成class文件,因为JVM只认识.class文件) +3. 在由JVM编译成电脑认识的文件 (对于电脑系统来说 文件代表一切) + +`(这是一个大概的观念 抽象画的概念)` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc7554d11b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 为什么说java是跨平台语言 + +* 这个夸平台是中间语言(JVM)实现的夸平台 +* Java有JVM从软件层面屏蔽了底层硬件、指令层面的细节让他兼容各种系统 + +`难道 C 和 C++ 不能夸平台吗 其实也可以` `C和C++需要在编译器层面去兼容不同操作系统的不同层面,写过C和C++的就知道不同操作系统的有些代码是不一样` + +### Jdk和Jre和JVM的区别 + +* Jdk包括了Jre和Jvm,Jre包括了Jvm + +* Jdk是我们编写代码使用的开发工具包 + +* Jre 是Java的运行时环境,他大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库 + +* Jvm俗称Java虚拟机,他是java运行环境的一部分,它虚构出来的一台计算机,在通过在实际的计算机上仿真模拟各种计算机功能来实现Java应用程序 + +* 看Java官方的图片,Jdk中包括了Jre,Jre中包括了JVM + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc8693d47b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### 说一下 JVM由那些部分组成,运行流程是什么? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc868d44b7~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* JVM包含两个子系统和两个组件: 两个子系统为Class loader(类装载)、Execution engine(执行引擎); 两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。 + + * Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。 + + * Execution engine(执行引擎):执行classes中的指令。 + + * Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。 + + * Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。 + +* **流程** :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。 + +### 说一下 JVM 运行时数据区 + +* Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域: + +`简单的说就是我们java运行时的东西是放在那里的` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc8784541e~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成; + + `为什么要线程计数器?因为线程是不具备记忆功能` + +* Java 虚拟机栈(Java Virtual Machine Stacks):每个方法在执行的同时都会在Java 虚拟机栈中创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息; + + `栈帧就是Java虚拟机栈中的下一个单位` + +* 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的; + + `Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 C和C++ 的代码` + +* Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存; + +* 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。 + +`后面有详细的说明JVM 运行时数据区` + +### 详细的介绍下程序计数器?(重点理解) + +1. 程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号) + +2. 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。 + + `总结:也可以把它叫做线程计数器` + +* **例子**:在java中最小的执行单位是线程,线程是要执行指令的,执行的指令最终操作的就是我们的电脑,就是 CPU。在CPU上面去运行,有个非常不稳定的因素,叫做调度策略,这个调度策略是时基于时间片的,也就是当前的这一纳秒是分配给那个指令的。 + +* **假如**: + + * 线程A在看直播 ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc9acf8957~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + * 突然,线程B来了一个视频电话,就会抢夺线程A的时间片,就会打断了线程A,线程A就会挂起 ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fcc70da181~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + * 然后,视频电话结束,这时线程A究竟该干什么? (线程是最小的执行单位,他不具备记忆功能,他只负责去干,那这个记忆就由:**程序计数器来记录**) ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fcc90c8a88~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### 详细介绍下Java虚拟机栈?(重点理解) + +1. Java虚拟机是线程私有的,它的生命周期和线程相同。 +2. 虚拟机栈描述的是Java方法执行的内存模型:`每个方法在执行的同时`都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 + +* **解释**:虚拟机栈中是有单位的,单位就是**栈帧**,一个方法一个**栈帧**。一个**栈帧**中他又要存储,局部变量,操作数栈,动态链接,出口等。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fccadd7f8b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)**解析栈帧:** + +1. 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。) +2. 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去 +3. 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。 +4. 出口:出口是什呢,出口正常的话就是return 不正常的话就是抛出异常落 + +#### 一个方法调用另一个方法,会创建很多栈帧吗? + +* 答:会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面 + +#### 栈指向堆是什么意思? + +* 栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址 + +#### 递归的调用自己会创建很多栈帧吗? + +* 答:递归的话也会创建多个栈帧,就是在栈中一直从上往下排下去 + +### 你能给我详细的介绍Java堆吗?(重点理解) + +* java堆(Java Heap)是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。 +* 在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。 +* java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。 +* 从内存回收角度来看java堆可分为:新生代和老生代。 +* 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。 +* 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。 +* 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。 + +### 能不能解释一下本地方法栈? + +1. 本地方法栈很好理解,他很栈很像,只不过方法上带了 native 关键字的栈字 +2. 它是虚拟机栈为虚拟机执行Java方法(也就是字节码)的服务方法 +3. native关键字的方法是看不到的,必须要去oracle官网去下载才可以看的到,而且native关键字修饰的大部分源码都是C和C++的代码。 +4. 同理可得,本地方法栈中就是C和C++的代码 + +### 能不能解释一下方法区(重点理解) + +1. 方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 +2. 它有个别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。 + +### 什么是JVM字节码执行引擎 + +* 虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。 + +* “虚拟机”是一个相对于“物理机”的概念,虚拟机的字节码是不能直接在物理机上运行的,需要JVM字节码执行引擎- 编译成机器码后才可在物理机上执行。 + +### 你听过直接内存吗? + +* 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。 +* 我的理解就是直接内存是基于物理内存和Java虚拟机内存的中间内存 + +### 知道垃圾收集系统吗? + +* 程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。 + +* 垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理 + +* 有一部分原因就是因为Java垃圾回收系统的强大导致Java领先市场 + +### 堆栈的区别是什么? + +> | 对比 | JVM堆 | JVM栈 | +> | --- | --- | --- | +> | 物理地址 | 堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩) | 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。 | +> | 内存分别 | 堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。 | 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。 | +> | 存放的内容 | 堆存放的是对象的实例和数组。因此该区更关注的是数据的存储 | 栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。 | +> | 程序的可见度 | 堆对于整个应用程序都是共享、可见的。 | 栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。 | + +* 注意: + * 静态变量放在方法区 + * 静态的对象还是放在堆。 + +### 深拷贝和浅拷贝 + +* 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址, +* 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存, +* 浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。 +* 深复制:在计算机中开辟一块**新的内存地址**用于存放复制的对象。 + +### Java会存在内存泄漏吗?请说明为什么? + +* 内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。 + +* 但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,`尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收`,这就是java中内存泄露的发生场景。 + +## 垃圾回收机制及算法 + +### 简述Java垃圾回收机制 + +* 在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。 + +### GC是什么?为什么要GC + +* GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。 + +### 垃圾回收的优点和缺点 + +* 优点:JVM的垃圾回收器都不需要我们手动处理无引用的对象了,这个就是最大的优点 + +* 缺点:程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。 + +### 垃圾回收器的原理是什么?有什么办法手动进行垃圾回收? + +* 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。 + +* 通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。 + +* 可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。 + +### JVM 中都有哪些引用类型? + +* 强引用:发生 gc 的时候不会被回收。 +* 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。 +* 弱引用:有用但不是必须的对象,在下一次GC时会被回收。 +* 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。 + +### 怎么判断对象是否可以被回收? + +* 垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是存活的,是不可以被回收的;哪些对象已经死掉了,需要被回收。 + +* 一般有两种方法来判断: + + * 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;(这个已经淘汰了) + * 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。(市场上用的非常非常广泛) + +### Full GC是什么 + +* 清理整个堆空间—包括年轻代和老年代和永久代 +* 因为Full GC是清理整个堆空间所以Full GC执行速度非常慢,在Java开发中最好保证少触发Full GC + +### 对象什么时候可以被垃圾器回收 + +* 当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。 +* 垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。 + +### JVM 垃圾回收算法有哪些? + +* 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。 +* 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。 +* 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。 +* 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。 + +#### 标记-清除算法 + +* 标记无用对象,然后进行清除回收。 + +* 标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段: + + * 标记阶段:标记出可以回收的对象。 + * 清除阶段:回收被标记的对象所占用的空间。 +* 标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。 + +* **优点**:实现简单,不需要对象进行移动。 + +* **缺点**:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。 + +* 标记-清除算法的执行的过程如下图所示 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fce4d05e44~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 复制算法 + +* 为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。 + +* **优点**:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。 + +* **缺点**:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。 + +* 复制算法的执行过程如下图所示 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fcd22194ad~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 标记-整理算法 + +* 在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。 + +* **优点**:解决了标记-清理算法存在的内存碎片问题。 + +* **缺点**:仍需要进行局部对象移动,一定程度上降低了效率。 + +* 标记-整理算法的执行过程如下图所示 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fce7bb60b1~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 分代收集算法 + +* 当前商业虚拟机都采用 `分代收集`的垃圾收集算法。分代收集算法,顾名思义是根据对象的`存活周期`将内存划分为几块。一般包括`年轻代`、`老年代`和 `永久代`,如图所示:`(后面有重点讲解)` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fcf6f94e92~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### JVM中的永久代中会发生垃圾回收吗 + +* 垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区 + (注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区) + +## 垃圾收集器以及新生代、老年代、永久代 + +### 讲一下新生代、老年代、永久代的区别 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd069db773~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。而新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。 + +* 新生代中一般保存新出现的对象,所以每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了`复制算法`,只需要付出少量存活对象的复制成本就可以完成收集。 + +* 老年代中一般保存存活了很久的对象,他们存活率高、没有额外空间对它进行分配担保,就必须采用`“标记-清理”或者“标记-整理”`算法。 + +* 永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。 + +### Minor GC、Major GC、Full GC是什么 + +1. Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。(一般采用复制算法回收垃圾) +2. Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。(可采用标记清楚法和标记整理法) +3. Full GC是清理整个堆空间,包括年轻代和老年代 + +### Minor GC、Major GC、Full GC区别及触发条件 + +* **Minor GC 触发条件一般为:** + + 1. eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。 + 2. 新创建的对象大小 > Eden所剩空间时触发Minor GC +* **Major GC和Full GC 触发条件一般为:** `Major GC通常是跟full GC是等价的` + + 1. 每次晋升到老年代的对象平均大小>老年代剩余空间 + + 2. MinorGC后存活的对象超过了老年代剩余空间 + + 3. 永久代空间不足 + + 4. 执行System.gc() + + 5. CMS GC异常 + + 6. 堆内存分配很大的对象 + +### 为什么新生代要分Eden和两个 Survivor 区域? + +* 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。 +* Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。 +* 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生) + +### Java堆老年代( Old ) 和新生代 ( Young ) 的默认比例? + +* 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。 + +* 其中,新生代 ( Young ) 被细分为 Eden 和 **两个 Survivor 区域**,Edem 和俩个Survivor 区域比例是 = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ), + +* 但是JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。 + +### 为什么要这样分代: + +* 其实主要原因就是可以根据各个年代的特点进行对象分区存储,更便于回收,采用最适当的收集算法: + + * 新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。 + + * 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。 + +* 新生代又分为Eden和Survivor (From与To,这里简称一个区)两个区。加上老年代就这三个区。数据会首先分配到Eden区当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代(大对象是指需要大量连续内存空间的java对象)。当Eden没有足够空间的时候就会触发jvm发起一次Minor GC,。如果对象经过一次Minor-GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代中了,当然晋升老年代的年龄是可以设置的。 + +### 什么是垃圾回收器他和垃圾算法有什么区别 + +* 垃圾收集器是垃圾回收算法(标记清楚法、标记整理法、复制算法、分代算法)的具体实现,不同垃圾收集器、不同版本的JVM所提供的垃圾收集器可能会有很在差别。 + +### 说一下 JVM 有哪些垃圾回收器? + +* 如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd022875b5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +> | 垃圾回收器 | 工作区域 | 回收算法 | 工作线程 | 用户线程并行 | 描述 | +> | --- | --- | --- | --- | --- | --- | +> | Serial | 新生带 | 复制算法 | 单线程 | 否 | Client模式下默认新生代收集器。简单高效 | +> | ParNew | 新生带 | 复制算法 | 多线程 | 否 | Serial的多线程版本,Server模式下首选, 可搭配CMS的新生代收集器 | +> | Parallel Scavenge | 新生带 | 复制算法 | 多线程 | 否 | 目标是达到可控制的吞吐量 | +> | Serial Old | 老年带 | 标记-整理 | 单线程 | 否 | Serial老年代版本,给Client模式下的虚拟机使用 | +> | Parallel Old | 老年带 | 标记-整理 | 多线程 | 否 | Parallel Scavenge老年代版本,吞吐量优先 | +> | | | | | | | +> | G1 | 新生带 + 老年带 | 标记-整理 + 复制算法 | 多线程 | 是 | JDK1.9默认垃圾收集器 | + +* Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效; +* ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现; +* Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景; +* Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本; +* Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本; +* CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。 +* G1(Garbage First)收集器 ( `标记整理 + 复制算法来回收垃圾` ): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。 + +### 收集器可以这么分配?(了解就好了) + +Serial / Serial Old +Serial / CMS +ParNew / Serial Old +ParNew / CMS +Parallel Scavenge / Serial Old +Parallel Scavenge / Parallel Old +G1 +复制代码 +### 新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别? + +* 新生代回收器:Serial、ParNew、Parallel Scavenge +* 老年代回收器:Serial Old、Parallel Old、CMS +* 整堆回收器:G1 + +新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。 + +### 简述分代垃圾回收器是怎么工作的? + +* 分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。 + +* 新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下: + + * 把 Eden + From Survivor 存活的对象放入 To Survivor 区; + * 清空 Eden 和 From Survivor 分区; + * From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。 +* 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。 + +* 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。 + +## 内存分配策略 + +### 简述java内存分配与回收策率以及Minor GC和Major GC + +* 所谓自动内存管理,最终要解决的也就是内存分配和内存回收两个问题。前面我们介绍了内存回收,这里我们再来聊聊内存分配。 + +* 对象的内存分配通常是在 Java 堆上分配(随着虚拟机优化技术的诞生,某些场景下也会在栈上分配,后面会详细介绍),对象主要分配在新生代的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数情况下也会直接在老年代上分配。总的来说分配规则不是百分百固定的,其细节取决于哪一种垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵循以下几种「普世」规则: + +#### 对象优先在 Eden 区分配 + +* 多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。 + * 这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从日志中发现 Major GC/Full GC。 + * **Minor GC** 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快; + * **Major GC/Full GC** 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。 + +#### 为什么大对象直接进入老年代 + +* 所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。 + +* 前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。 + +#### 长期存活对象将进入老年代 + +* 虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。 + +## 虚拟机类加载机制 + +### 简述java类加载机制? + +* 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。 + +### 类加载的机制及过程 + +* 程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd24770998~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +##### 1、加载 + +* 加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。 + +* Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。 + +* 类加载器,可以从不同来源加载类的二进制数据,比如:本地Class文件、Jar包Class文件、网络Class文件等等等。 + +* 类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口 + +##### 2、连接过程 + +* 当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)。类连接又可分为如下3个阶段。 + +1. 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。 + +2. 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配 + +3. 解析:虚拟机常量池的符号引用替换为字节引用过程 + +##### 3、初始化 + +* 初始化阶段是执行类构造器``() 方法的过程。类构造器``()方法是由编译器自动收藏类中的`所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。` + +* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 + +* 虚拟机会保证一个类的``() 方法在多线程环境中被正确加锁和同步 + +`初始化的总结就是:初始化是为类的静态变量赋予正确的初始值` + +### 描述一下JVM加载Class文件的原理机制 + +* Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。 + +* 类装载方式,有两种 : + + * 1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中, + + * 2.显式装载, 通过class.forname()等方法,显式加载需要的类 + +* Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。 + +### 什么是类加载器,类加载器有哪些? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd30195e5b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。 + +* 主要有一下四种类加载器: + + 1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。 + 2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。 + 3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。 + 4. 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。 + +### 说一下类装载的执行过程? + +* 类装载分为以下 5 个步骤: + * 加载:根据查找路径找到相应的 class 文件然后导入; + * 验证:检查加载的 class 文件的正确性; + * 准备:给类中的静态变量分配内存空间; + * 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址; + * 初始化:对静态变量和静态代码块执行初始化工作。 + +### 什么是双亲委派模型? + +* 在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd30195e5b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 类加载器分类: + + * 启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库; + * 其他类加载器: + * 扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库; + * 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。 +* 双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。 + +* 总结就是:`当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。` + +## JVM调优 + +### JVM 调优的参数可以在那设置参数值 + +* 可以在IDEA,Eclipse,工具里设置 + +* 如果上线了是WAR包的话可以在Tomcat设置 + +* 如果是Jar包直接 :java -jar 是直接插入JVM命令就好了 + + java -Xms1024m -Xmx1024m ...等等等 JVM参数 -jar springboot_app.jar & + 复制代码 + +### 说一下 JVM 调优的工具? + +* JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。 + + * jconsole:用于对 JVM 中的内存、线程和类等进行监控; ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd4667d98b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + * jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。 ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd4d2a3018~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### 常用的 JVM 调优的参数都有哪些? + +#常用的设置 +-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。 + +-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。 + +-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。 + +-XX:NewSize=n 设置年轻代初始化大小大小 + +-XX:MaxNewSize=n 设置年轻代最大值 + +-XX:NewRatio=n 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代+年老代和的 1/4 + +-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8 + +-Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。 + +-XX:ThreadStackSize=n 线程堆栈大小 + +-XX:PermSize=n 设置持久代初始值 + +-XX:MaxPermSize=n 设置持久代大小 + +-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。 + +#下面是一些不常用的 + +-XX:LargePageSizeInBytes=n 设置堆内存的内存页大小 + +-XX:+UseFastAccessorMethods 优化原始类型的getter方法性能 + +-XX:+DisableExplicitGC 禁止在运行期显式地调用System.gc(),默认启用 + +-XX:+AggressiveOpts 是否启用JVM开发团队最新的调优成果。例如编译优化,偏向锁,并行年老代收集等,jdk6纸之后默认启动 + +-XX:+UseBiasedLocking 是否启用偏向锁,JDK6默认启用 + +-Xnoclassgc 是否禁用垃圾回收 + +-XX:+UseThreadPriorities 使用本地线程的优先级,默认启用 + +等等等...... +复制代码 +### JVM的GC收集器设置 + +* -xx:+Use xxx GC + * xxx 代表垃圾收集器名称 + +-XX:+UseSerialGC:设置串行收集器,年轻带收集器 + +-XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。 + +-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量 + +-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。 + +-XX:+UseConcMarkSweepGC:设置年老代并发收集器 + +-XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器 + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125696573448 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" new file mode 100644 index 0000000..077a179 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" @@ -0,0 +1,923 @@ + + +## 集合容器概述 + +### 什么是集合 + +* 集合就是一个放数据的容器,准确的说是放数据对象引用的容器 + +* 集合类存放的都是对象的引用,而不是对象的本身 + +* 集合类型主要有3种:set(集)、list(列表)和map(映射)。 + +### 集合的特点 + +* 集合的特点主要有如下两点: + * 集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。 + + * 和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小 + +### 集合和数组的区别 + +* 数组是固定长度的;集合可变长度的。 + +* 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。 + +* 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。 + +### 使用集合框架的好处 + +1. 容量自增长; +2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量; +3. 可以方便地扩展或改写集合,提高代码复用性和可操作性。 +4. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。 + +### 常用的集合类有哪些? + +* Map接口和Collection接口是所有集合框架的父接口: + +1. Collection接口的子接口包括:Set接口和List接口 +2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等 +3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等 +4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等 + +### List,Set,Map三者的区别? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e70de4bd~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。 + +* Collection集合主要有List和Set两大接口 + + * List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 + * Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 +* Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。 + + * Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap + +### 集合框架底层数据结构 + +* Collection + + 1. List + + * Arraylist: Object数组 + + * Vector: Object数组 + + * LinkedList: 双向循环链表 + + 2. Set + + * HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素 + * LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。 + * TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。) +* Map + + * HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间 + * LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 + * HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 + * TreeMap: 红黑树(自平衡的排序二叉树) + +### 哪些集合类是线程安全的? + +* Vector:就比Arraylist多了个 synchronized (线程安全),因为效率较低,现在已经不太建议使用。 +* hashTable:就比hashMap多了个synchronized (线程安全),不建议使用。 +* ConcurrentHashMap:是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用) +* ... + +### Java集合的快速失败机制 “fail-fast”? + +* 是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。 + +* 例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。 + +* 原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。 + +* 解决办法: + + 1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。 + + 2. 使用CopyOnWriteArrayList来替换ArrayList + +### 怎么确保一个集合不能被修改? + +* 可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。 + +* 示例代码如下: + + List list = new ArrayList<>(); + list. add("x"); + Collection clist = Collections. unmodifiableCollection(list); + clist. add("y"); // 运行时此行报错 + System. out. println(list. size()); + 复制代码 + +## Collection接口 + +### List接口 + +#### 迭代器 Iterator 是什么? + +* Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。 +* 因为所有Collection接继承了Iterator迭代器 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e6f6342b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### Iterator 怎么使用?有什么特点? + +* Iterator 使用代码如下: + + List list = new ArrayList<>(); + Iterator it = list. iterator(); + while(it. hasNext()){ + String obj = it. next(); + System. out. println(obj); + } + 复制代码 +* Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。 + +#### 如何边遍历边移除 Collection 中的元素? + +* 边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下: + + Iterator it = list.iterator(); + while(it.hasNext()){ + *// do something* + it.remove(); + } + 复制代码 + +一种最常见的**错误**代码如下: + +for(Integer i : list){ + list.remove(i) +} +复制代码 + +* 运行以上错误代码会报 **ConcurrentModificationException 异常**。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。 + +#### Iterator 和 ListIterator 有什么区别? + +* Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。 +* Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。 +* ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。 + +#### 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么? + +* 遍历方式有以下几种: + + 1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。 + 2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。 + 3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。 +* 最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。 + + * 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。 + + * 如果没有实现该接口,表示不支持 Random Access,如LinkedList。 + + * 推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。 + +#### 说一下 ArrayList 的优缺点 + +* ArrayList的优点如下: + + * ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。 + * ArrayList 在顺序添加一个元素的时候非常方便。 +* ArrayList 的缺点如下: + + * 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。 + * 插入元素的时候,也需要做一次元素复制操作,缺点同上。 +* ArrayList 比较适合顺序添加、随机访问的场景。 + +#### 如何实现数组和 List 之间的转换? + +* 数组转 List:使用 Arrays. asList(array) 进行转换。 +* List 转数组:使用 List 自带的 toArray() 方法。 + +* 代码示例: + + // list to array + List list = new ArrayList(); + list.add("123"); + list.add("456"); + list.toArray(); + + // array to list + String[] array = new String[]{"123","456"}; + Arrays.asList(array); + 复制代码 + +#### ArrayList 和 LinkedList 的区别是什么? + +* 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。 +* 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。 +* 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。 +* 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。 +* 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全; + +* 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。 + +* LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。 + +#### ArrayList 和 Vector 的区别是什么? + +* 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合 + + * 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。 + * 性能:ArrayList 在性能方面要优于 Vector。 + * 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。 +* Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。 + +* Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。 + +#### 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性? + +* ArrayList和Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。 + +* Vector 中的方法由于加了 synchronized 修饰,因此 **Vector** **是线程安全容器,但性能上较ArrayList差**。 + +* LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 **LinkedList** **插入速度较快**。 + +#### 多线程场景下如何使用 ArrayList? + +* ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样: + + List synchronizedList = Collections.synchronizedList(list); + synchronizedList.add("aaa"); + synchronizedList.add("bbb"); + + for (int i = 0; i < synchronizedList.size(); i++) { + System.out.println(synchronizedList.get(i)); + } + 复制代码 + +#### 为什么 ArrayList 的 elementData 加上 transient 修饰? + +* ArrayList 中的数组定义如下: + + private transient Object[] elementData; + +* 再看一下 ArrayList 的定义: + + public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable + 复制代码 +* 可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现: + + private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ + *// Write out element count, and any hidden stuff* + int expectedModCount = modCount; + s.defaultWriteObject(); + *// Write out array length* + s.writeInt(elementData.length); + *// Write out all elements in the proper order.* + for (int i=0; i +* 每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。 + +#### List 和 Set 的区别 + +* List , Set 都是继承自Collection 接口 + +* List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 + +* Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 + +* 另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。 + +* Set和List对比 + + * Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。 + * List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变 + +### Set接口 + +#### 说一下 HashSet 的实现原理? + +* HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。 + +#### HashSet如何检查重复?HashSet是如何保证数据不可重复的? + +* 向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。 + +* HashSet 中的add ()方法会使用HashMap 的put()方法。 + +* HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。 + +* 以下是HashSet 部分源码: + + private static final Object PRESENT = new Object(); + private transient HashMap map; + + public HashSet() { + map = new HashMap<>(); + } + + public boolean add(E e) { + // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值 + return map.put(e, PRESENT)==null; + } + 复制代码 + +**hashCode()与equals()的相关规定**: + +1. 如果两个对象相等,则hashcode一定也是相同的 + * hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值 +2. 两个对象相等,对两个equals方法返回true +3. 两个对象有相同的hashcode值,它们也不一定是相等的 +4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖 +5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 + +**==与equals的区别** + +1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同 +2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较 + +#### HashSet与HashMap的区别 + +> | HashMap | HashSet | +> | --- | --- | +> | 实现了Map接口 | 实现Set接口 | +> | 存储键值对 | 仅存储对象 | +> | 调用put()向map中添加元素 | 调用add()方法向Set中添加元素 | +> | HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false | +> | HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 | + +## Map接口 + +### 什么是Hash算法 + +* 哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。 + +### 什么是链表 + +* 链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等功能。 + +* 链表大致分为单链表和双向链表 + + 1. 单链表:每个节点包含两部分,一部分存放数据变量的data,另一部分是指向下一节点的next指针 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e72891e5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + 2. 双向链表:除了包含单链表的部分,还增加的pre前一个节点的指针 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e73f80b0~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* 链表的优点 + + * 插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加删除元素) + * 内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大小),并且在需要空间的时候才创建空间) + * 大小没有固定,拓展很灵活。 +* 链表的缺点 + + * 不能随机查找,必须从第一个开始遍历,查找效率低 + +### 说一下HashMap的实现原理? + +* HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 + +* HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。 + +* HashMap 基于 Hash 算法实现的 + + 1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标 + + 2. 存储时,如果出现hash值相同的key,此时有两种情况。 + + ​ (1)如果key相同,则覆盖原始值; + + ​ (2)如果key不同(出现冲突),则将当前的key-value放入链表中 + + 3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。 + + 4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。 + +* 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn) + +### HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现 + +* 在Java中,保存数据有两种比较简单的数据结构:数组和链表。**数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法**的方式可以解决哈希冲突。 + +#### HashMap JDK1.8之前 + +* JDK1.8之前采用的是拉链法。**拉链法**:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e78f59a7~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### HashMap JDK1.8之后 + +* 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e7c6af15~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### JDK1.7 VS JDK1.8 比较 + +* JDK1.8主要解决或优化了一下问题: + 1. resize 扩容优化 + 2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考 + 3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。 + +> | 不同 | JDK 1.7 | JDK 1.8 | +> | --- | --- | --- | +> | 存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 | +> | 初始化方式 | 单独函数:`inflateTable()` | 直接集成到了扩容函数`resize()`中 | +> | hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 | +> | 存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树 | +> | 插入数据方式 | 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) | +> | 扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) | + +### 什么是红黑树 + +#### 说道红黑树先讲什么是二叉树 + +* 二叉树简单来说就是 每一个节上可以关联俩个子节点 + + * 大概就是这样子: + a + / \ + b c + / \ / \ + d e f g + / \ / \ / \ / \ + h i j k l m n o + 复制代码 + +#### 红黑树 + +* 红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红(Red)或黑(Black)。 ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173552173a8a0c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]。 + +* 如果一个结点是红色的,则它的子结点必须是黑色的。 + +* 每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!] + +* 红黑树的基本操作是**添加、删除**。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。 + +### HashMap的put方法的具体流程? + +* 当我们put的时候,首先计算 `key`的`hash`值,这里调用了 `hash`方法,`hash`方法实际是让`key.hashCode()`与`key.hashCode()>>>16`进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:**高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞**。按照函数注释,因为bucket数组大小是2的幂,计算下标`index = (table.length - 1) & hash`,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。 + +* putVal方法执行流程图 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717355218a84ee7~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} + +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} + +//实现Map.put和相关方法 +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + // 步骤①:tab为空则创建 + // table未初始化或者长度为0,进行扩容 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + // 步骤②:计算index,并对null做处理 + // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + // 桶中已经存在元素 + else { + Node e; K k; + // 步骤③:节点key存在,直接覆盖value + // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + // 将第一个元素赋值给e,用e来记录 + e = p; + // 步骤④:判断该链为红黑树 + // hash值不相等,即key不相等;为红黑树结点 + // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null + else if (p instanceof TreeNode) + // 放入树中 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + // 步骤⑤:该链为链表 + // 为链表结点 + else { + // 在链表最末插入结点 + for (int binCount = 0; ; ++binCount) { + // 到达链表的尾部 + + //判断该链表尾部指针是不是空的 + if ((e = p.next) == null) { + // 在尾部插入新结点 + p.next = newNode(hash, key, value, null); + //判断链表的长度是否达到转化红黑树的临界值,临界值为8 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + //链表结构转树形结构 + treeifyBin(tab, hash); + // 跳出循环 + break; + } + // 判断链表中结点的key值与插入的元素的key值是否相等 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + // 相等,跳出循环 + break; + // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 + p = e; + } + } + //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值 + if (e != null) { + // 记录e的value + V oldValue = e.value; + // onlyIfAbsent为false或者旧值为null + if (!onlyIfAbsent || oldValue == null) + //用新值替换旧值 + e.value = value; + // 访问后回调 + afterNodeAccess(e); + // 返回旧值 + return oldValue; + } + } + // 结构性修改 + ++modCount; + // 步骤⑥:超过最大容量就扩容 + // 实际大小大于阈值则扩容 + if (++size > threshold) + resize(); + // 插入后回调 + afterNodeInsertion(evict); + return null; +} +复制代码 + +1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; +2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; +3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals; +4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5; +5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; +6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。 + +### HashMap的扩容操作是怎么实现的? + +1. 在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容; + +2. 每次扩展的时候,都是扩展2倍; + +3. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。 + +* 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上 + + final Node[] resize() { + Node[] oldTab = table;//oldTab指向hash桶数组 + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空 + if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值 + threshold = Integer.MAX_VALUE; + return oldTab;//返回 + }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold + } + // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂 + // 直接将该值赋给新的容量 + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr; + // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75 + else { // zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + // 新的threshold = 新的cap * 0.75 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + // 计算出新的数组长度后赋给当前成员变量table + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap];//新建hash桶数组 + table = newTab;//将新数组的值复制给旧的hash桶数组 + // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散 + if (oldTab != null) { + // 遍历新数组的所有桶下标 + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收 + oldTab[j] = null; + // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树 + if (e.next == null) + // 用同样的hash映射算法把该元素加入新的数组 + newTab[e.hash & (newCap - 1)] = e; + // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排 + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + // e是链表的头并且e.next!=null,那么处理链表中元素重排 + else { // preserve order + // loHead,loTail 代表扩容后不用变换下标,见注1 + Node loHead = null, loTail = null; + // hiHead,hiTail 代表扩容后变换下标,见注1 + Node hiHead = null, hiTail = null; + Node next; + // 遍历链表 + do { + next = e.next; + if ((e.hash & oldCap) == 0) { + if (loTail == null) + // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead + // 代表下标保持不变的链表的头元素 + loHead = e; + else + // loTail.next指向当前e + loTail.next = e; + // loTail指向当前的元素e + // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时, + // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next..... + // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。 + loTail = e; + } + else { + if (hiTail == null) + // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素 + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。 + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; + } + 复制代码 + +### HashMap是怎么解决哈希冲突的? + +* 答:在解决这个问题之前,我们首先需要知道**什么是哈希冲突**,而在了解哈希冲突之前我们还要知道**什么是哈希**才行; + +#### 什么是哈希? + +* Hash,一般翻译为“散列”,也有直接音译为“哈希”的, Hash就是指使用哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。 + +#### 什么是哈希冲突? + +* **当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)**。 + +#### HashMap的数据结构 + +* 在Java中,保存数据有两种比较简单的数据结构:数组和链表。 + * 数组的特点是:寻址容易,插入和删除困难; + * 链表的特点是:寻址困难,但插入和删除容易; +* 所以我们将数组和链表结合在一起,发挥两者各自的优势,就可以使用俩种方式:链地址法和开放地址法可以解决哈希冲突: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735521c92dc84~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位; +* 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。 +* **但相比于hashCode返回的int类型,我们HashMap初始的容量大小`DEFAULT_INITIAL_CAPACITY = 1 << 4`(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表**,所以我们还需要对hashCode作一定的优化 + +#### hash()函数 + +* 上面提到的问题,主要是因为如果使用hashCode取余,那么相当于**参与运算的只有hashCode的低位**,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为**扰动**,在**JDK 1.8**中的hash()函数如下: + + static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或) + } + 复制代码 +* 这比在**JDK 1.7**中,更为简洁,**相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)**; + +#### 总结 + +* 简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的: + * 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位; + * 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。 + +### 能否使用任何类作为 Map 的 key? + +可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点: + +* 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。 + +* 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。 + +* 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。 + +* 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。 + +### 为什么HashMap中String、Integer这样的包装类适合作为K? + +* 答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率 + * 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况 + * 内部已重写了`equals()`、`hashCode()`等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况; + +### 如果使用Object作为HashMap的Key,应该怎么办呢? + +* 答:重写`hashCode()`和`equals()`方法 + 1. **重写`hashCode()`是因为需要计算存储数据的存储位置**,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞; + 2. **重写`equals()`方法**,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,**目的是为了保证key在哈希表中的唯一性**; + +### HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标? + +* 答:`hashCode()`方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过`hashCode()`计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置; + +* **那怎么解决呢?** + + 1. HashMap自己实现了自己的`hash()`方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均; + + 2. 在保证数组长度为2的幂次方的时候,使用`hash()`运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题; + +### HashMap 的长度为什么是2的幂次方 + +* 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。 + +* **这个算法应该如何设计呢?** + + * 我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。 +* **那为什么是两次扰动呢?** + + * 答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的; + +### HashMap 与 HashTable 有什么区别? + +1. **线程安全**: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 `synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap ); +2. **效率**: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap ); +3. **对Null key 和Null value的支持**: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。 +4. **初始容量大小和每次扩充容量大小的不同** : + 1. 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。 + 2. 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 +5. **底层数据结构**: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 +6. 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。 + +### 什么是TreeMap 简介 + +* TreeMap 是一个**有序的key-value集合**,它是通过红黑树实现的。 +* TreeMap基于**红黑树(Red-Black tree)实现**。该映射根据**其键的自然顺序进行排序**,或者根据**创建映射时提供的 Comparator 进行排序**,具体取决于使用的构造方法。 +* TreeMap是线程**非同步**的。 + +### 如何决定使用 HashMap 还是 TreeMap? + +* 对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。 + +### HashMap 和 ConcurrentHashMap 的区别 + +1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。) +2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。 + +### ConcurrentHashMap 和 Hashtable 的区别? + +* ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 + + * **底层数据结构**: JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; + * **实现线程安全的方式**: + 1. **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本; + 2. ② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 +* **两者的对比图**: + +##### 1、HashTable: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735521ca71b79~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +##### 2、 JDK1.7的ConcurrentHashMap: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735521de4886d~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +##### 3、JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点): + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735522b19186a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。 + +### ConcurrentHashMap 底层具体实现知道吗?实现原理是什么? + +#### JDK1.7 + +* 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 + +* 在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下: + +* 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735524c5089b8~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +1. 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色; +2. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。 + +#### JDK1.8 + +* 在**JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现**,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。 + +* 结构如下: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173552564c22be~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* **附加源码,有需要的可以看看** + +* 插入元素过程(建议去看看源码): + +* 如果相应位置的Node还没有初始化,则调用CAS插入相应的数据; + + else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { + if (casTabAt(tab, i, null, new Node(hash, key, value, null))) + break; // no lock when adding to empty bin + } + 复制代码 +* 如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点; + + if (fh >= 0) { + binCount = 1; + for (Node e = f;; ++binCount) { + K ek; + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + oldVal = e.val; + if (!onlyIfAbsent) + e.val = value; + break; + } + Node pred = e; + if ((e = e.next) == null) { + pred.next = new Node(hash, key, value, null); + break; + } + } + } + 复制代码 + +1. 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值; +2. 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount; + +## 辅助工具类 + +### Array 和 ArrayList 有何区别? + +* Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。 +* Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。 +* Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。 + +`对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。` + +### 如何实现 Array 和 List 之间的转换? + +* Array 转 List: Arrays. asList(array) ; +* List 转 Array:List 的 toArray() 方法。 + +### comparable 和 comparator的区别? + +* comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序 +* comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序 + +* 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort(). + +### Collection 和 Collections 有什么区别? + +* java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。 +* Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。 + +### TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素? + +* TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。 + +* Collections 工具类的 sort 方法有两种重载的形式, + +* 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较; + +? + +* comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序 +* comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序 + +* 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort(). + +### Collection 和 Collections 有什么区别? + +* java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。 +* Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。 + +### TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素? + +* TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。 + +* Collections 工具类的 sort 方法有两种重载的形式, + +* 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较; + +* 第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。 + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125939843079 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Linux.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Linux.md" new file mode 100644 index 0000000..e799b7b --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Linux.md" @@ -0,0 +1,676 @@ + + +## Linux 概述 + +### 什么是Linux + +* Linux是一套免费使用和自由传播的类似Unix操作系统,一般的WEB项目都是部署都是放在Linux操作系统上面。 Linux是一个基于POSIX和Unix的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的Unix工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744a2d148acc2~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### Windows和Linux的区别 + +* Windows是微软开发的操作系统,民用操作系统,可用于娱乐、影音、上网。 Windows操作系统具有强大的日志记录系统和强大的桌面应用。好处是它可以帮我们实现非常多绚丽多彩的效果,可以非常方便去进行娱乐、影音、上网。 +* Linux的应用相对单纯很多,没有什么绚丽多彩的效果,因此Linux的性能是非常出色的,可以完全针对机器的配置有针对性的优化, +* 简单来说Windows适合普通用户进行娱乐办公使用,Linux适合软件开发部署 + +### Unix和Linux有什么区别? + +* Linux和Unix都是功能强大的操作系统,都是应用广泛的服务器操作系统,有很多相似之处,甚至有一部分人错误地认为Unix和Linux操作系统是一样的,然而,事实并非如此,以下是两者的区别。 + 1. 开源性 + Linux是一款开源操作系统,不需要付费,即可使用;Unix是一款对源码实行知识产权保护的传统商业软件,使用需要付费授权使用。 + 2. 跨平台性 + Linux操作系统具有良好的跨平台性能,可运行在多种硬件平台上;Unix操作系统跨平台性能较弱,大多需与硬件配套使用。 + 3. 可视化界面 + Linux除了进行命令行操作,还有窗体管理系统;Unix只是命令行下的系统。 + 4. 硬件环境 + Linux操作系统对硬件的要求较低,安装方法更易掌握;Unix对硬件要求比较苛刻,按照难度较大。 + 5. 用户群体 + Linux的用户群体很广泛,个人和企业均可使用;Unix的用户群体比较窄,多是安全性要求高的大型企业使用,如银行、电信部门等,或者Unix硬件厂商使用,如Sun等。 + 相比于Unix操作系统,Linux操作系统更受广大计算机爱好者的喜爱,主要原因是Linux操作系统具有Unix操作系统的全部功能,并且能够在普通PC计算机上实现全部的Unix特性,开源免费的特性,更容易普及使用! + +### 什么是 Linux 内核? + +* Linux 系统的核心是内核。内核控制着计算机系统上的所有硬件和软件,在必要时分配硬件,并根据需要执行软件。 + 1. 系统内存管理 + 2. 应用程序管理 + 3. 硬件设备管理 + 4. 文件系统管理 + +### Linux的基本组件是什么? + +* 就像任何其他典型的操作系统一样,Linux拥有所有这些组件:内核,shell和GUI,系统实用程序和应用程序。Linux比其他操作系统更具优势的是每个方面都附带其他功能,所有代码都可以免费下载。 + +### Linux 的体系结构 + +* 从大的方面讲,Linux 体系结构可以分为两块: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744a2d1cc127a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 用户空间(User Space) :用户空间又包括用户的应用程序(User Applications)、C 库(C Library) 。 +* 内核空间(Kernel Space) :内核空间又包括系统调用接口(System Call Interface)、内核(Kernel)、平台架构相关的代码(Architecture-Dependent Kernel Code) 。 + +**为什么 Linux 体系结构要分为用户空间和内核空间的原因?** + +* 1、现代 CPU 实现了不同的工作模式,不同模式下 CPU 可以执行的指令和访问的寄存器不同。 +* 2、Linux 从 CPU 的角度出发,为了保护内核的安全,把系统分成了两部分。 + +* 用户空间和内核空间是程序执行的**两种不同的状态**,我们可以通过两种方式完成用户空间到内核空间的转移: + * 系统调用; + * 硬件中断。 + +### BASH和DOS之间的基本区别是什么? + +* BASH和DOS控制台之间的主要区别在于3个方面: + * BASH命令区分大小写,而DOS命令则不区分; + * 在BASH下,/ character是目录分隔符,\作为转义字符。在DOS下,/用作命令参数分隔符,\是目录分隔符 + * DOS遵循命名文件中的约定,即8个字符的文件名后跟一个点,扩展名为3个字符。BASH没有遵循这样的惯例。 + +### Linux 开机启动过程? + +> 了解即可。 + +* 1、主机加电自检,加载 BIOS 硬件信息。 + +* 2、读取 MBR 的引导文件(GRUB、LILO)。 + +* 3、引导 Linux 内核。 + +* 4、运行第一个进程 init (进程号永远为 1 )。 + +* 5、进入相应的运行级别。 + +* 6、运行终端,输入用户名和密码。 + +### Linux系统缺省的运行级别? + +* 关机。 +* 单机用户模式。 +* 字符界面的多用户模式(不支持网络)。 +* 字符界面的多用户模式。 +* 未分配使用。 +* 图形界面的多用户模式。 +* 重启。 + +### Linux 使用的进程间通信方式? + +> 了解即可,不需要太深入。 + +* 1、管道(pipe)、流管道(s_pipe)、有名管道(FIFO)。 +* 2、信号(signal) 。 +* 3、消息队列。 +* 4、共享内存。 +* 5、信号量。 +* 6、套接字(socket) 。 + +### Linux 有哪些系统日志文件? + +* 比较重要的是 `/var/log/messages` 日志文件。 + +> 该日志文件是许多进程日志文件的汇总,从该文件可以看出任何入侵企图或成功的入侵。 +> +> 另外,如果胖友的系统里有 ELK 日志集中收集,它也会被收集进去。 + +### Linux系统安装多个桌面环境有帮助吗? + +* 通常,一个桌面环境,如KDE或Gnome,足以在没有问题的情况下运行。尽管系统允许从一个环境切换到另一个环境,但这对用户来说都是优先考虑的问题。有些程序在一个环境中工作而在另一个环境中无法工作,因此它也可以被视为选择使用哪个环境的一个因素。 + +### 什么是交换空间? + +* 交换空间是Linux使用的一定空间,用于临时保存一些并发运行的程序。当RAM没有足够的内存来容纳正在执行的所有程序时,就会发生这种情况。 + +### 什么是root帐户 + +* root帐户就像一个系统管理员帐户,允许你完全控制系统。你可以在此处创建和维护用户帐户,为每个帐户分配不同的权限。每次安装Linux时都是默认帐户。 + +### 什么是LILO? + +* LILO是Linux的引导加载程序。它主要用于将Linux操作系统加载到主内存中,以便它可以开始运行。 + +### 什么是BASH? + +* BASH是Bourne Again SHell的缩写。它由Steve Bourne编写,作为原始Bourne Shell(由/ bin / sh表示)的替代品。它结合了原始版本的Bourne Shell的所有功能,以及其他功能,使其更容易使用。从那以后,它已被改编为运行Linux的大多数系统的默认shell。 + +### 什么是CLI? + +* **命令行界面**(英语**:command-line interface**,缩写]**:CLI**)是在图形用户界面得到普及之前使用最为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行。也有人称之为**字符用户界面**(CUI)。 + +* 通常认为,命令行界面(CLI)没有图形用户界面(GUI)那么方便用户操作。因为,命令行界面的软件通常需要用户记忆操作的命令,但是,由于其本身的特点,命令行界面要较图形用户界面节约计算机系统的资源。在熟记命令的前提下,使用命令行界面往往要较使用图形用户界面的操作速度要快。所以,图形用户界面的操作系统中,都保留着可选的命令行界面。 + +### 什么是GUI? + +* 图形用户界面(Graphical User Interface,简称 GUI,又称图形用户接口)是指采用图形方式显示的计算机操作用户界面。 + +* 图形用户界面是一种人与计算机通信的界面显示格式,允许用户使用鼠标等输入设备操纵屏幕上的图标或菜单选项,以选择命令、调用文件、启动程序或执行其它一些日常任务。与通过键盘输入文本或字符命令来完成例行任务的字符界面相比,图形用户界面有许多优点。 + +### 开源的优势是什么? + +* 开源允许你将软件(包括源代码)免费分发给任何感兴趣的人。然后,人们可以添加功能,甚至可以调试和更正源代码中的错误。它们甚至可以让它运行得更好,然后再次自由地重新分配这些增强的源代码。这最终使社区中的每个人受益。 + +### GNU项目的重要性是什么? + +* 这种所谓的自由软件运动具有多种优势,例如可以自由地运行程序以及根据你的需要自由学习和修改程序。它还允许你将软件副本重新分发给其他人,以及自由改进软件并将其发布给公众。 + +## 磁盘、目录、文件 + +### 简单 Linux 文件系统? + +**在 Linux 操作系统中,所有被操作系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文件或是目录都被看作是一个文件。** + +* 也就是说在 Linux 系统中有一个重要的概念**:一切都是文件**。其实这是 Unix 哲学的一个体现,而 Linux 是重写 Unix 而来,所以这个概念也就传承了下来。在 Unix 系统中,把一切资源都看作是文件,包括硬件设备。UNIX系统把每个硬件都看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。 + +* Linux 支持 5 种文件类型,如下图所示: + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744a2d70c1faf~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### Linux 的目录结构是怎样的? + +> 这个问题,一般不会问。更多是实际使用时,需要知道。 + +* Linux 文件系统的结构层次鲜明,就像一棵倒立的树,最顶层是其根目录: + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744a2d6e0c867~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)**常见目录说明**: + +> | 目录 | 介绍 | +> | --- | --- | +> | /bin | 存放二进制可执行文件(ls,cat,mkdir等),常用命令一般都在这里; | +> | /etc | 存放系统管理和配置文件; | +> | /home | 存放所有用户文件的根目录,是用户主目录的基点,比如用户user的主目录就是/home/user,可以用~user表示; | +> | /usr | 用于存放系统应用程序; | +> | /opt | 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把tomcat等都安装到这里; | +> | /proc | 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息; | +> | /root | 超级用户(系统管理员)的主目录(特权阶级); | +> | /sbin | 存放二进制可执行文件,只有root才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程序。如ifconfig等; | +> | /dev | 用于存放设备文件; | +> | /mnt | 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统; | +> | /boot | 存放用于系统引导时使用的各种文件; | +> | /lib | 存放着和系统运行相关的库文件 ; | +> | /tmp | 用于存放各种临时文件,是公用的临时文件存储点; | +> | /var | 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启动日志等。)等; | +> | /lost+found | 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows下叫什么.chk)就在这里 | + +### 什么是 inode ? + +> 一般来说,面试不会问 inode 。但是 inode 是一个重要概念,是理解 Unix/Linux 文件系统和硬盘储存的基础。 + +* 理解inode,要从文件储存说起。 + +* 文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。 + +* 操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。 + +* 文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。 + +* 每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。 + +**简述 Linux 文件系统通过 i 节点把文件的逻辑结构和物理结构转换的工作过程?** + +> 如果看的一脸懵逼,也没关系。一般来说,面试官不太会问这个题目。 + +* Linux 通过 inode 节点表将文件的逻辑结构和物理结构进行转换。 + * inode 节点是一个 64 字节长的表,表中包含了文件的相关信息,其中有文件的大小、文件所有者、文件的存取许可方式以及文件的类型等重要信息。在 inode 节点表中最重要的内容是磁盘地址表。在磁盘地址表中有 13 个块号,文件将以块号在磁盘地址表中出现的顺序依次读取相应的块。 + * Linux 文件系统通过把 inode 节点和文件名进行连接,当需要读取该文件时,文件系统在当前目录表中查找该文件名对应的项,由此得到该文件相对应的 inode 节点号,通过该 inode 节点的磁盘地址表把分散存放的文件物理块连接成文件的逻辑结构。 + +### 什么是硬链接和软链接? + +* **硬链接**:由于 Linux 下的文件是通过索引节点(inode)来识别文件,硬链接可以认为是一个指针,指向文件索引节点的指针,系统并不为它重新分配 inode 。每添加一个一个硬链接,文件的链接数就加 1 。 + + * 不足: + 1. 不可以在不同文件系统的文件间建立链接; + 2. 只有超级用户才可以为目录创建硬链接。 +* **软链接**:软链接克服了硬链接的不足,没有任何文件系统的限制,任何用户可以创建指向目录的符号链接。因而现在更为广泛使用,它具有更大的灵活性,甚至可以跨越不同机器、不同网络对文件进行链接。 + + * 不足:因为链接文件包含有原文件的路径信息,所以当原文件从一个目录下移到其他目录中,再访问链接文件,系统就找不到了,而硬链接就没有这个缺陷,你想怎么移就怎么移;还有它要系统分配额外的空间用于建立新的索引节点和保存原文件的路径。 +* **实际场景下,基本是使用软链接**。总结区别如下: + + * 硬链接不可以跨分区,软件链可以跨分区。 + * 硬链接指向一个 inode 节点,而软链接则是创建一个新的 inode 节点。 + * 删除硬链接文件,不会删除原文件,删除软链接文件,会把原文件删除。 + +### RAID 是什么? + +> RAID 全称为独立磁盘冗余阵列(Redundant Array of Independent Disks),基本思想就是把多个相对便宜的硬盘组合起来,成为一个硬盘阵列组,使性能达到甚至超过一个价格昂贵、 容量巨大的硬盘。RAID 通常被用在服务器电脑上,使用完全相同的硬盘组成一个逻辑扇区,因此操作系统只会把它当做一个硬盘。 +> +> RAID 分为不同的等级,各个不同的等级均在数据可靠性及读写性能上做了不同的权衡。在实际应用中,可以依据自己的实际需求选择不同的 RAID 方案。 + +* 当然,因为很多公司都使用云服务,大家很难接触到 RAID 这个概念,更多的可能是普通云盘、SSD 云盘酱紫的概念。 + +## 安全 + +### 一台 Linux 系统初始化环境后需要做一些什么安全工作? + +* 1、添加普通用户登陆,禁止 root 用户登陆,更改 SSH 端口号。 + + > 修改 SSH 端口不一定绝对哈。当然,如果要暴露在外网,建议改下。l + +* 2、服务器使用密钥登陆,禁止密码登陆。 + +* 3、开启防火墙,关闭 SElinux ,根据业务需求设置相应的防火墙规则。 + +* 4、装 fail2ban 这种防止 SSH 暴力破击的软件。 + +* 5、设置只允许公司办公网出口 IP 能登陆服务器(看公司实际需要) + + > 也可以安装 VPN 等软件,只允许连接 VPN 到服务器上。 + +* 6、修改历史命令记录的条数为 10 条。 + +* 7、只允许有需要的服务器可以访问外网,其它全部禁止。 + +* 8、做好软件层面的防护。 + + * 8.1 设置 nginx_waf 模块防止 SQL 注入。 + * 8.2 把 Web 服务使用 www 用户启动,更改网站目录的所有者和所属组为 www 。 + +### 什么叫 CC 攻击?什么叫 DDOS 攻击? + +* CC 攻击,主要是用来攻击页面的,模拟多个用户不停的对你的页面进行访问,从而使你的系统资源消耗殆尽。 + +* DDOS 攻击,中文名叫分布式拒绝服务攻击,指借助服务器技术将多个计算机联合起来作为攻击平台,来对一个或多个目标发动 DDOS 攻击。 + + > 攻击,即是通过大量合法的请求占用大量网络资源,以达到瘫痪网络的目的。 + +**怎么预防 CC 攻击和 DDOS 攻击?** + +* 防 CC、DDOS 攻击,这些只能是用硬件防火墙做流量清洗,将攻击流量引入黑洞。 + +> 流量清洗这一块,主要是买 ISP 服务商的防攻击的服务就可以,机房一般有空余流量,我们一般是买服务,毕竟攻击不会是持续长时间。 + +### 什么是网站数据库注入? + +* 由于程序员的水平及经验参差不齐,大部分程序员在编写代码的时候,没有对用户输入数据的合法性进行判断。 +* 应用程序存在安全隐患。用户可以提交一段数据库查询代码,根据程序返回的结果,获得某些他想得知的数据,这就是所谓的 SQL 注入。 +* SQL注入,是从正常的 WWW 端口访问,而且表面看起来跟一般的 Web 页面访问没什么区别,如果管理员没查看日志的习惯,可能被入侵很长时间都不会发觉。 + +**如何过滤与预防?** + +* 数据库网页端注入这种,可以考虑使用 nginx_waf 做过滤与预防。 + +### Shell 脚本是什么? + +* 一个 Shell 脚本是一个文本文件,包含一个或多个命令。作为系统管理员,我们经常需要使用多个命令来完成一项任务,我们可以添加这些所有命令在一个文本文件(Shell 脚本)来完成这些日常工作任务。 + +## 实战 + +### 如何选择 Linux 操作系统版本? + +**一般来讲,桌面用户首选 Ubuntu ;服务器首选 RHEL 或 CentOS ,两者中首选 CentOS 。** + +* 根据具体要求: + + * 安全性要求较高,则选择 Debian 或者 FreeBSD 。 + + * 需要使用数据库高级服务和电子邮件网络应用的用户可以选择 SUSE 。 + + * 想要新技术新功能可以选择 Feddora ,Feddora 是 RHEL 和 CentOS 的一个测试版和预发布版本。 + + * 【重点】**根据现有状况,绝大多数互联网公司选择 CentOS 。现在比较常用的是 6 系列,现在市场占有大概一半左右。另外的原因是 CentOS 更侧重服务器领域,并且无版权约束**。 + + > CentOS 7 系列,也慢慢使用的会比较多了。 + +### 如何规划一台 Linux 主机,步骤是怎样? + +* 1、确定机器是做什么用的,比如是做 WEB 、DB、还是游戏服务器。 + + > 不同的用途,机器的配置会有所不同。 + +* 2、确定好之后,就要定系统需要怎么安装,默认安装哪些系统、分区怎么做。 + +* 3、需要优化系统的哪些参数,需要创建哪些用户等等的。 + +### 请问当用户反馈网站访问慢,你会如何处理? + +**有哪些方面的因素会导致网站网站访问慢?** + +* 1、服务器出口带宽不够用 + + > * 本身服务器购买的出口带宽比较小。一旦并发量大的话,就会造成分给每个用户的出口带宽就小,访问速度自然就会慢。 + > * 跨运营商网络导致带宽缩减。例如,公司网站放在电信的网络上,那么客户这边对接是长城宽带或联通,这也可能导致带宽的缩减。 + +* 2、服务器负载过大,导致响应不过来 + + > 可以从两个方面入手分析: + > + > * 分析系统负载,使用 w 命令或者 uptime 命令查看系统负载。如果负载很高,则使用 top 命令查看 CPU ,MEM 等占用情况,要么是 CPU 繁忙,要么是内存不够。 + > * 如果这二者都正常,再去使用 sar 命令分析网卡流量,分析是不是遭到了攻击。一旦分析出问题的原因,采取对应的措施解决,如决定要不要杀死一些进程,或者禁止一些访问等。 + +* 3、数据库瓶颈 + + > * 如果慢查询比较多。那么就要开发人员或 DBA 协助进行 SQL 语句的优化。 + > * 如果数据库响应慢,考虑可以加一个数据库缓存,如 Redis 等。然后,也可以搭建 MySQL 主从,一台 MySQL 服务器负责写,其他几台从数据库负责读。 + +* 4、网站开发代码没有优化好 + + > * 例如 SQL 语句没有优化,导致数据库读写相当耗时。 + +**针对网站访问慢,怎么去排查?** + +* 1、首先要确定是用户端还是服务端的问题。当接到用户反馈访问慢,那边自己立即访问网站看看,如果自己这边访问快,基本断定是用户端问题,就需要耐心跟客户解释,协助客户解决问题。 + + > 不要上来就看服务端的问题。一定要从源头开始,逐步逐步往下。 + +* 2、如果访问也慢,那么可以利用浏览器的调试功能,看看加载那一项数据消耗时间过多,是图片加载慢,还是某些数据加载慢。 + +* 3、针对服务器负载情况。查看服务器硬件(网络、CPU、内存)的消耗情况。如果是购买的云主机,比如阿里云,可以登录阿里云平台提供各方面的监控,比如 CPU、内存、带宽的使用情况。 + +* 4、如果发现硬件资源消耗都不高,那么就需要通过查日志,比如看看 MySQL慢查询的日志,看看是不是某条 SQL 语句查询慢,导致网站访问慢。 + +**怎么去解决?** + +* 1、如果是出口带宽问题,那么久申请加大出口带宽。 +* 2、如果慢查询比较多,那么就要开发人员或 DBA 协助进行 SQL 语句的优化。 +* 3、如果数据库响应慢,考虑可以加一个数据库缓存,如 Redis 等等。然后也可以搭建MySQL 主从,一台 MySQL 服务器负责写,其他几台从数据库负责读。 +* 4、申请购买 CDN 服务,加载用户的访问。 +* 5、如果访问还比较慢,那就需要从整体架构上进行优化咯。做到专角色专用,多台服务器提供同一个服务。 + +### Linux 性能调优都有哪几种方法? + +* 1、Disabling daemons (关闭 daemons)。 +* 2、Shutting down the GUI (关闭 GUI)。 +* 3、Changing kernel parameters (改变内核参数)。 +* 4、Kernel parameters (内核参数)。 +* 5、Tuning the processor subsystem (处理器子系统调优)。 +* 6、Tuning the memory subsystem (内存子系统调优)。 +* 7、Tuning the file system (文件系统子系统调优)。 +* 8、Tuning the network subsystem(网络子系统调优)。 + +## 基本命令 + +###### cd (change directory:英文释义是改变目录)切换目录 + +cd ../ ;跳到上级目录 +cd /opt ;不管现在到那直接跳到指定的opt文件夹中 +cd ~ ;切换当前用户的家目录。root用户的家目录就是root目录。 +复制代码 + +###### pwd (print working directory:显示当前工作目录的绝对路径) + +pwd +显示当前的绝对路劲 +复制代码 + +###### ls (ls:list的缩写,查看列表)查看当前目录下的所有文件夹(ls 只列出文件名或目录名) + +ls -a ;显示所有文件夹,隐藏文件也显示出来 +ls -R ;连同子目录一起列出来 +复制代码 + +###### ll (ll:list的缩写,查看列表详情)查看当前目录下的所有详细信息和文件夹(ll 结果是详细,有时间,是否可读写等信息) + +ll -a ;显示所有文件,隐藏文件也显示出来 +ll -R ;连同子目录内容一起列出来 +ll -h ;友好展示详情信息,可以看大小 +ll -al ;即能显示隐藏文件又能显示详细列表。 +复制代码 + +###### touch (touch:创建文件)创建文件 + +touch test.txt ;创建test.txt文件 +touch /opt/java/test.java ;在指定目录创建test.java文件 +复制代码 + +###### mkdir (mkdir:创建目录) 创建目录 + +mkdir 文件夹名称 ;在此目录创建文件夹 +mkdir /opt/java/jdk ;在指定目录创建文件夹 +复制代码 + +###### cat (concatenate:显示或把多个文本文件连接起来)查看文件命令(可以快捷查看当前文件的内容)(不能快速定位到最后一页) + +cat lj.log ;快捷查看文件命令 +Ctrl + c ;暂停显示文件 +Ctrl + d ;退出查看文件命令 +复制代码 + +###### more (more:更多的意思)分页查看文件命令(不能快速定位到最后一页) + +回车:向下n行,需要定义,默认为1行。 +空格键:向下滚动一屏或Ctrl+F +B:返回上一层或Ctrl+B +q:退出more +复制代码 + +###### less (lese:较少的意思)分页查看文件命令(可以快速定位到最后一页) + +less -m 显示类似于more命令的百分比。 +less -N 显示每行的行号。(大写的N) +两参数一起使用如:less -mN 文件名,如此可分页并显示行号。 + +空格键:前下一页或page down。 +回车:向下一行。 +b:后退一页 或 page up。 +q:退出。 +d:前进半页。 +u:后退半页 +复制代码 + +###### tail(尾巴) 查看文件命令(看最后多少行) + +tail -10 ;文件名 看最后10行 +复制代码 + +###### cp(copy单词缩写,复制功能) + +cp /opt/java/java.log /opt/logs/ ;把java.log 复制到/opt/logs/下 +cp /opt/java/java.log /opt/logs/aaa.log ;把java.log 复制到/opt/logs/下并且改名为aaa.log +cp -r /opt/java /opt/logs ;把文件夹及内容复制到logs文件中 +复制代码 + +###### mv(move单词缩写,移动功能,该文件名称功能) + +mv /opt/java/java.log /opt/mysql/ ;移动文件到mysql目录下 +mv java.log mysql.log ;把java.log改名为mysql.log +复制代码 + +###### rm(remove:移除的意思)删除文件,或文件夹 + +-f或--force 强制删除文件或目录。删除文件不包括文件夹的文件 +-r或-R或--recursive 递归处理,将指定目录下的所有文件及子目录一并删除。 +-rf 强制删除文件夹及内容 + +rm 文件名 ;安全删除命令 (yes删除 no取消) +rm -rf 强制删除文件夹及内容 +rm -rf * 删除当前目录下的所有内容。 +rm -rf /* 删除Linux系统根目录下所有的内容。系统将完蛋。 +复制代码 + +###### find (find:找到的意思)查找指定文件或目录 + +* 表示0~多个任意字符。 + +find -name 文件名;按照指定名称查找在当前目录下查找文件 +find / -name 文件名按照指定名称全局查找文件 +find -name '*文件名' ;任意前缀加上文件名在当前目录下查找文件 +find / -name '*文件名*' ;全局进行模糊查询带文件名的文件 +复制代码 + +###### vi (VIsual:视觉)文本编辑器 类似win的记事本 (操作类似于地下的vim命令,看底下vim 的操作) + +###### vim (VI IMproved:改进版视觉)改进版文本编辑器 (不管是文件查看还是文件编辑 按 Shift + 上或者下可以上下移动查看视角) + +输入”vim 文件名” 打开文件,刚刚时是”一般模式”。 + +一般模式:可以浏览文件内容,可以进行文本快捷操作。如单行复制,多行复制,单行删除,多行删除,(退出)等。 +插入模式:可以编辑文件内容。 +底行模式:可以进行强制退出操作,不保存 :q! + 可以进行保存并退出操作 :wq + +按下”i”或”a”或”o”键,从”一般模式”,进入”插入模式(编辑模式)”。 +在编辑模式下按”Esc” 即可到一般模式 +在一般模式下按”:”,冒号进入底行模式。 + +在一般模式下的快捷键 + dd ;删除一整行 + X ;向前删除 等同于windowns系统中的删除键 + x ;向后删除和大写x相反方向 + Ctrl + f ;向后看一页 + Ctrl + b ;向前看一页 + u ;撤销上一步操作 + /word ;向下查找word关键字 输入:n查找下一个,N查找上一个(不管是哪个查找都是全局查找 只不过n的方向相反) + ?log ;向上查找log关键字 输入:n查找上一个,N查找下一个 + :1,90s/redis/Redis/g ;把1-90行的redis替换为Redis。语法n1,n2s/原关键字/新关键字/g,n1代表其实行,n2代表结尾行,g是必须要的 + :0 ;光标移动到第一行 + :$ ;光标移动到最后一行 + :300 ;光标移动到300行,输入多少数字移动到多少行 + :w ;保存 + :w! ;强制保存 + :q ;退出 + :q! ;强制退出 + 5dd ;删除后面5行,打一个参数为自己填写 + 5x ;删除此光标后面5个字符 + d1G ;删除此光标之前的所有 + d0 ;从光标当前位置删除到此行的第一个位置 + yy ;复制 + p ;在光标的下面进行粘贴 + P ;在光标的上门进行粘贴 +复制代码 + +###### | 管道命令(把多个命令组合起来使用) + +管道命令的语法:命令1 | 命令2 | 命令3。 +复制代码 + +###### grep (grep :正则表达式)正则表达式,用于字符串的搜索工作(模糊查询)。不懂可以先过 + +单独使用: +grep String test.java ;在test.java文件中查找String的位置,返回整行 +一般此命令不会单独使用下面列几个常用的命令(地下通过管道命令组合起来使用) + +ps aux|grep java ;查找带java关键字的进程 +ll |grep java ;查找带java关键字的文件夹及文件 +复制代码 + +###### yum install -y lrzsz 命令(实现win到Linux文件互相简单上传文件) + +#(实际上就是在Linux系统中下载了一个插件)下了了此安装包后就可以实现win系统到linux之间拉文件拉文件 +#等待下载完了就可以输入: + +rz 从win系统中选择文件上传到Linux系统中 + +sz 文件名 选择Linux系统的文件复制到win系统中 + +复制代码 + +###### tar (解压 压缩 命令) + +常用的组合命令: +-z 是否需要用gzip压缩。 +-c 建立一个压缩文件的参数指令(create) –压缩 + -x 解开一个压缩文件的参数指令(extract) –解压 + -v 压缩的过程中显示文件(verbose) + -f 使用档名,在f之后要立即接档中(file) + 常用解压参数组合:zxvf + 常用压缩参数组合:zcvf + +解压命令: +tar -zxvf redis-3.2.8.tar.gz ;解压到当前文件夹 +tar -zxvf redis-3.2.8.tar.gz -C /opt/java/ ;解压到指定目录 + +压缩命令:(注意 语法有点反了,我反正每次都搞反) +tar -zcvf redis-3.2.8.tar.gz redis-3.2.8/ ;语法 tar -zcvf 压缩后的名称 要压缩的文件 +tar -zcvf 压缩后的文件(可指定目录) 要压缩的文件(可指定目录) +复制代码 + +###### ps (process status:进程状态,类似于windows的任务管理器) + +常用组合:ps -ef 标准的格式查看系统进程 + ps -aux BSD格式查看系统进程 + ps -aux|grep redis BSD格式查看进程名称带有redis的系统进程(常用技巧) +//显示进程的一些属性,需要了解(ps aux) +USER //用户名 +PID //进程ID号,用来杀死进程的 +%CPU //进程占用的CPU的百分比 +%MEM //占用内存的的百分比 +VSZ //该进程使用的虚拟內存量(KB) +RSS //该进程占用的固定內存量(KB) +STAT //进程的状态 +START //该进程被触发启动时间 +TIME //该进程实际使用CPU运行的时间 +复制代码 + +###### clear 清屏命令。(强迫症患者使用) + +kill 命令用来中止一个进程。(要配合ps命令使用,配合pid关闭进程) +(ps类似于打开任务管理器,kill类似于关闭进程) + kill -5 进程的PID ;推荐,和平关闭进程 + kill -9 PID ;不推荐,强制杀死进程 +复制代码 + +###### ifconfig命令 + +用于查看和更改网络接口的地址和参数,包括IP地址、网络掩码、广播地址,使用权限是超级用户。(一般是用来查看的,很少更改) +如果此命令输入无效,先输入yum -y install net-tools +ifconfig +复制代码 + +###### ping (用于检测与目标的连通性)语法:ping ip地址 + +测试: +1、在Windows操作系统中cmdipconfig,查看本机IP地址: +2、再到LInux系统中输入 ping ip地址 +(公司电脑,我就不暴露Ip了,没图片 自己去试) +按Ctrl + C 可以停止测试。 +复制代码 + +###### free 命令 (显示系统内存) + +#显示系统内存使用情况,包括物理内存、交互区内存(swap)和内核缓冲区内存。 +-b 以Byte显示内存使用情况 +-k 以kb为单位显示内存使用情况 +-m 以mb为单位显示内存使用情况 +-g 以gb为单位显示内存使用情况 +-s<间隔秒数> 持续显示内存 +-t 显示内存使用总合 +复制代码 + +###### top 命令 + +#显示当前系统正在执行的进程的相关信息,包括进程 ID、内存占用率、CPU 占用率等 +-c 显示完整的进程命令 +-s 保密模式 +-p <进程号> 指定进程显示 +-n <次数>循环显示次数 +复制代码 + +###### netstat 命令 + +#Linux netstat命令用于显示网络状态。 +#利用netstat指令可让你得知整个Linux系统的网络情况。 +#语法: +netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip] +复制代码 + +###### file (可查看文件类型) + +file 文件名 +复制代码 + +###### 重启linux + +Linux centos 重启命令:reboot +复制代码 + +###### 关机linux + +Linux centos 关机命令:halt + +复制代码 + +###### 同步时间命令 + +ntpdate ntp1.aliyun.com +复制代码 + +###### 更改为北京时间命令 + +rm -rf /etc/localtime +ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime +复制代码 + +###### 查看时间命令: + +date + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904127059738637 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Nginx.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Nginx.md" new file mode 100644 index 0000000..a1d00e1 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Nginx.md" @@ -0,0 +1,516 @@ + + +### 什么是Nginx? + +* Nginx是一个 轻量级/高性能的反向代理Web服务器,他实现非常高效的反向代理、负载平衡,他可以处理2-3万并发连接数,官方监测能支持5万并发,现在中国使用nginx网站用户有很多,例如:新浪、网易、 腾讯等。 + +### 为什么要用Nginx? + +* 跨平台、配置简单、方向代理、高并发连接:处理2-3万并发连接数,官方监测能支持5万并发,内存消耗小:开启10个nginx才占150M内存 ,nginx处理静态文件好,耗费内存少, + +* 而且Nginx内置的健康检查功能:如果有一个服务器宕机,会做一个健康检查,再发送的请求就不会发送到宕机的服务器了。重新将请求提交到其他的节点上。 + +* 使用Nginx的话还能: + + 1. 节省宽带:支持GZIP压缩,可以添加浏览器本地缓存 + 2. 稳定性高:宕机的概率非常小 + 3. 接收用户请求是异步的 + +### 为什么Nginx性能这么高? + +* 因为他的事件处理机制:异步非阻塞事件处理机制:运用了epoll模型,提供了一个队列,排队解决 + +### Nginx怎么处理请求的? + +* nginx接收一个请求后,首先由listen和server_name指令匹配server模块,再匹配server模块里的location,location就是实际地址 + +```java +server { # 第一个Server区块开始,表示一个独立的虚拟主机站点 + listen 80; # 提供服务的端口,默认80 + server_name localhost; # 提供服务的域名主机名 + location / { # 第一个location区块开始 + root html; # 站点的根目录,相当于Nginx的安装目录 + index index.html index.htm; # 默认的首页文件,多个用空格分开 + } # 第一个location区块结果 + } +``` +### 什么是正向代理和反向代理? + +1. 正向代理就是一个人发送一个请求直接就到达了目标的服务器 +2. 反方代理就是请求统一被Nginx接收,nginx反向代理服务器接收到之后,按照一定的规 则分发给了后端的业务处理服务器进行处理了 + +### 使用“反向代理服务器的优点是什么? + +* 反向代理服务器可以隐藏源服务器的存在和特征。它充当互联网云和web服务器之间的中间层。这对于安全方面来说是很好的,特别是当您使用web托管服务时。 + +### Nginx的优缺点? + +* 优点: + + 1. 占内存小,可实现高并发连接,处理响应快 + 2. 可实现http服务器、虚拟主机、方向代理、负载均衡 + 3. Nginx配置简单 + 4. 可以不暴露正式的服务器IP地址 +* 缺点: 动态处理差:nginx处理静态文件好,耗费内存少,但是处理动态页面则很鸡肋,现在一般前端用nginx作为反向代理抗住压力, + +### Nginx应用场景? + +1. http服务器。Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。 +2. 虚拟主机。可以实现在一台服务器虚拟出多个网站,例如个人网站使用的虚拟机。 +3. 反向代理,负载均衡。当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会应为某台服务器负载高宕机而某台服务器闲置的情况。 +4. nginz 中也可以配置安全管理、比如可以使用Nginx搭建API接口网关,对每个接口服务进行拦截。 + +### Nginx目录结构有哪些? + +```java +[root@localhost ~]# tree /usr/local/nginx +/usr/local/nginx +├── client_body_temp +├── conf # Nginx所有配置文件的目录 +│ ├── fastcgi.conf # fastcgi相关参数的配置文件 +│ ├── fastcgi.conf.default # fastcgi.conf的原始备份文件 +│ ├── fastcgi_params # fastcgi的参数文件 +│ ├── fastcgi_params.default +│ ├── koi-utf +│ ├── koi-win +│ ├── mime.types # 媒体类型 +│ ├── mime.types.default +│ ├── nginx.conf # Nginx主配置文件 +│ ├── nginx.conf.default +│ ├── scgi_params # scgi相关参数文件 +│ ├── scgi_params.default +│ ├── uwsgi_params # uwsgi相关参数文件 +│ ├── uwsgi_params.default +│ └── win-utf +├── fastcgi_temp # fastcgi临时数据目录 +├── html # Nginx默认站点目录 +│ ├── 50x.html # 错误页面优雅替代显示文件,例如当出现502错误时会调用此页面 +│ └── index.html # 默认的首页文件 +├── logs # Nginx日志目录 +│ ├── access.log # 访问日志文件 +│ ├── error.log # 错误日志文件 +│ └── nginx.pid # pid文件,Nginx进程启动后,会把所有进程的ID号写到此文件 +├── proxy_temp # 临时目录 +├── sbin # Nginx命令目录 +│ └── nginx # Nginx的启动命令 +├── scgi_temp # 临时目录 +└── uwsgi_temp # 临时目录 +``` +### Nginx配置文件nginx.conf有哪些属性模块? + +```java +worker_processes 1; # worker进程的数量 +events { # 事件区块开始 + worker_connections 1024; # 每个worker进程支持的最大连接数 +} # 事件区块结束 +http { # HTTP区块开始 + include mime.types; # Nginx支持的媒体类型库文件 + default_type application/octet-stream; # 默认的媒体类型 + sendfile on; # 开启高效传输模式 + keepalive_timeout 65; # 连接超时 + server { # 第一个Server区块开始,表示一个独立的虚拟主机站点 + listen 80; # 提供服务的端口,默认80 + server_name localhost; # 提供服务的域名主机名 + location / { # 第一个location区块开始 + root html; # 站点的根目录,相当于Nginx的安装目录 + index index.html index.htm; # 默认的首页文件,多个用空格分开 + } # 第一个location区块结果 + error_page 500502503504 /50x.html; # 出现对应的http状态码时,使用50x.html回应客户 + location = /50x.html { # location区块开始,访问50x.html + root html; # 指定对应的站点目录为html + } + } + ...... +``` +### Nginx静态资源? + +* 静态资源访问,就是存放在nginx的html页面,我们可以自己编写 + +### 如何用Nginx解决前端跨域问题? + +* 使用Nginx转发请求。把跨域的接口写成调本域的接口,然后将这些接口转发到真正的请求地址。 + +### Nginx虚拟主机怎么配置? + +* 1、基于域名的虚拟主机,通过域名来区分虚拟主机——应用:外部网站 + +* 2、基于端口的虚拟主机,通过端口来区分虚拟主机——应用:公司内部网站,外部网站的管理后台 + +* 3、基于ip的虚拟主机。 + +#### 基于虚拟主机配置域名 + +* 需要建立/data/www /data/bbs目录,windows本地hosts添加虚拟机ip地址对应的域名解析;对应域名网站目录下新增index.html文件; + +```java +#当客户端访问www.lijie.com,监听端口号为80,直接跳转到data/www目录下文件 + server { + listen 80; + server_name www.lijie.com; + location / { + root data/www; + index index.html index.htm; + } + } + + #当客户端访问www.lijie.com,监听端口号为80,直接跳转到data/bbs目录下文件 + server { + listen 80; + server_name bbs.lijie.com; + location / { + root data/bbs; + index index.html index.htm; + } + } +``` +#### 基于端口的虚拟主机 + +* 使用端口来区分,浏览器使用域名或ip地址:端口号 访问 + +```java +#当客户端访问www.lijie.com,监听端口号为8080,直接跳转到data/www目录下文件 + server { + listen 8080; + server_name 8080.lijie.com; + location / { + root data/www; + index index.html index.htm; + } + } + + #当客户端访问www.lijie.com,监听端口号为80直接跳转到真实ip服务器地址 127.0.0.1:8080 + server { + listen 80; + server_name www.lijie.com; + location / { + proxy_pass http://127.0.0.1:8080; + index index.html index.htm; + } + } +``` +### location的作用是什么? + +* location指令的作用是根据用户请求的URI来执行不同的应用,也就是根据用户请求的网站URL进行匹配,匹配成功即进行相关的操作。 + +#### location的语法能说出来吗? + +> 注意:~ 代表自己输入的英文字母 +> +> | 匹配符 | 匹配规则 | 优先级 | +> | --- | --- | --- | +> | = | 精确匹配 | 1 | +> | ^~ | 以某个字符串开头 | 2 | +> | ~ | 区分大小写的正则匹配 | 3 | +> | ~* | 不区分大小写的正则匹配 | 4 | +> | !~ | 区分大小写不匹配的正则 | 5 | +> | !~* | 不区分大小写不匹配的正则 | 6 | +> | / | 通用匹配,任何请求都会匹配到 | 7 | + +#### Location正则案例 + +* 示例: + +```java +#优先级1,精确匹配,根路径 + location =/ { + return 400; + } + + #优先级2,以某个字符串开头,以av开头的,优先匹配这里,区分大小写 + location ^~ /av { + root /data/av/; + } + + #优先级3,区分大小写的正则匹配,匹配/media*****路径 + location ~ /media { + alias /data/static/; + } + + #优先级4 ,不区分大小写的正则匹配,所有的****.jpg|gif|png 都走这里 + location ~* .*\.(jpg|gif|png|js|css)$ { + root /data/av/; + } + + #优先7,通用匹配 + location / { + return 403; + } +``` +### 限流怎么做的? + +* Nginx限流就是限制用户请求速度,防止服务器受不了 + +* 限流有3种 + + 1. 正常限制访问频率(正常流量) + 2. 突发限制访问频率(突发流量) + 3. 限制并发连接数 +* Nginx的限流都是基于漏桶流算法,底下会说道什么是桶铜流 + +**实现三种限流算法** + +##### 1、正常限制访问频率(正常流量): + +* 限制一个用户发送的请求,我Nginx多久接收一个请求。 + +* Nginx中使用ngx_http_limit_req_module模块来限制的访问频率,限制的原理实质是基于漏桶算法原理来实现的。在nginx.conf配置文件中可以使用limit_req_zone命令及limit_req命令限制单个IP的请求处理频率。 + +```java +#定义限流维度,一个用户一分钟一个请求进来,多余的全部漏掉 + limit_req_zone $binary_remote_addr zone=one:10m rate=1r/m; + + #绑定限流维度 + server{ + + location/seckill.html{ + limit_req zone=zone; + proxy_pass http://lj_seckill; + } + + } +``` + +* 1r/s代表1秒一个请求,1r/m一分钟接收一个请求, 如果Nginx这时还有别人的请求没有处理完,Nginx就会拒绝处理该用户请求。 + +##### 2、突发限制访问频率(突发流量): + +* 限制一个用户发送的请求,我Nginx多久接收一个。 + +* 上面的配置一定程度可以限制访问频率,但是也存在着一个问题:如果突发流量超出请求被拒绝处理,无法处理活动时候的突发流量,这时候应该如何进一步处理呢?Nginx提供burst参数结合nodelay参数可以解决流量突发的问题,可以设置能处理的超过设置的请求数外能额外处理的请求数。我们可以将之前的例子添加burst参数以及nodelay参数: + +```java +#定义限流维度,一个用户一分钟一个请求进来,多余的全部漏掉 + limit_req_zone $binary_remote_addr zone=one:10m rate=1r/m; + + #绑定限流维度 + server{ + + location/seckill.html{ + limit_req zone=zone burst=5 nodelay; + proxy_pass http://lj_seckill; + } + + } +``` + +* 为什么就多了一个 burst=5 nodelay; 呢,多了这个可以代表Nginx对于一个用户的请求会立即处理前五个,多余的就慢慢来落,没有其他用户的请求我就处理你的,有其他的请求的话我Nginx就漏掉不接受你的请求 + +##### 3、 限制并发连接数 + +* Nginx中的ngx_http_limit_conn_module模块提供了限制并发连接数的功能,可以使用limit_conn_zone指令以及limit_conn执行进行配置。接下来我们可以通过一个简单的例子来看下: + +```java +http { + limit_conn_zone $binary_remote_addr zone=myip:10m; + limit_conn_zone $server_name zone=myServerName:10m; + } + + server { + location / { + limit_conn myip 10; + limit_conn myServerName 100; + rewrite / http://www.lijie.net permanent; + } + } +``` + +* 上面配置了单个IP同时并发连接数最多只能10个连接,并且设置了整个虚拟服务器同时最大并发数最多只能100个链接。当然,只有当请求的header被服务器处理后,虚拟服务器的连接数才会计数。刚才有提到过Nginx是基于漏桶算法原理实现的,实际上限流一般都是基于漏桶算法和令牌桶算法实现的。接下来我们来看看两个算法的介绍: + +### 漏桶流算法和令牌桶算法知道? + +#### 漏桶算法 + +* 漏桶算法是网络世界中流量整形或速率限制时经常使用的一种算法,它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。也就是我们刚才所讲的情况。漏桶算法提供的机制实际上就是刚才的案例:**突发流量会进入到一个漏桶,漏桶会按照我们定义的速率依次处理请求,如果水流过大也就是突发流量过大就会直接溢出,则多余的请求会被拒绝。所以漏桶算法能控制数据的传输速率。**![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172646dbb8b696~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +#### 令牌桶算法 + +* 令牌桶算法是网络流量整形和速率限制中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法。**令牌桶算法的机制如下:存在一个大小固定的令牌桶,会以恒定的速率源源不断产生令牌。如果令牌消耗速率小于生产令牌的速度,令牌就会一直产生直至装满整个令牌桶。** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172646dbc20c88~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 为什么要做动静分离? + +* Nginx是当下最热的Web容器,网站优化的重要点在于静态化网站,网站静态化的关键点则是是动静分离,动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们则根据静态资源的特点将其做缓存操作。 + +* 让静态的资源只走静态资源服务器,动态的走动态的服务器 + +* Nginx的静态处理能力很强,但是动态处理能力不足,因此,在企业中常用动静分离技术。 + +* 对于静态资源比如图片,js,css等文件,我们则在反向代理服务器nginx中进行缓存。这样浏览器在请求一个静态资源时,代理服务器nginx就可以直接处理,无需将请求转发给后端服务器tomcat。 若用户请求的动态文件,比如servlet,jsp则转发给Tomcat服务器处理,从而实现动静分离。这也是反向代理服务器的一个重要的作用。 + +### Nginx怎么做的动静分离? + +* 只需要指定路径对应的目录。location/可以使用正则表达式匹配。并指定对应的硬盘中的目录。如下:(操作都是在Linux上) + +```java +location /image/ { + root /usr/local/static/; + autoindex on; + } +``` + +1. 创建目录 + +```java +mkdir /usr/local/static/image +``` +1. 进入目录 + +```java +cd /usr/local/static/image +``` +1. 放一张照片上去# + +```java +1.jpg +``` +1. 重启 nginx + +```java +sudo nginx -s reload +``` +1. 打开浏览器 输入 server_name/image/1.jpg 就可以访问该静态图片了 + +### Nginx负载均衡的算法怎么实现的?策略有哪些? + +* 为了避免服务器崩溃,大家会通过负载均衡的方式来分担服务器压力。将对台服务器组成一个集群,当用户访问时,先访问到一个转发服务器,再由转发服务器将访问分发到压力更小的服务器。 + +* Nginx负载均衡实现的策略有以下五种: + +#### 1 轮询(默认) + +* 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某个服务器宕机,能自动剔除故障系统。 + +```java +upstream backserver { + server 192.168.0.12; + server 192.168.0.13; +} +``` +#### 2 权重 weight + +* weight的值越大分配 + +* 到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。其次是为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。 + +```java +upstream backserver { + server 192.168.0.12 weight=2; + server 192.168.0.13 weight=8; +} +``` + +* 权重越高,在被访问的概率越大,如上例,分别是20%,80%。 + +#### 3 ip_hash( IP绑定) + +* 每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,`并且可以有效解决动态网页存在的session共享问题` + +```java +upstream backserver { + ip_hash; + server 192.168.0.12:88; + server 192.168.0.13:80; +} +``` +#### 4 fair(第三方插件) + +* 必须安装upstream_fair模块。 + +* 对比 weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能地进行负载均衡,响应时间短的优先分配。 + +```java +upstream backserver { + server server1; + server server2; + fair; +} + +``` + +* 哪个服务器的响应速度快,就将请求分配到那个服务器上。 + +#### 5、url_hash(第三方插件) + +* 必须安装Nginx的hash软件包 + +* 按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。 + +```java +upstream backserver { + server squid1:3128; + server squid2:3128; + hash $request_uri; + hash_method crc32; +} + +``` +### Nginx配置高可用性怎么配置? + +* 当上游服务器(真实访问服务器),一旦出现故障或者是没有及时相应的话,应该直接轮训到下一台服务器,保证服务器的高可用 + +* Nginx配置代码: + +```java +server { + listen 80; + server_name www.lijie.com; + location / { + ### 指定上游服务器负载均衡服务器 + proxy_pass http://backServer; + ###nginx与上游服务器(真实访问的服务器)超时时间 后端服务器连接的超时时间_发起握手等候响应超时时间 + proxy_connect_timeout 1s; + ###nginx发送给上游服务器(真实访问的服务器)超时时间 + proxy_send_timeout 1s; + ### nginx接受上游服务器(真实访问的服务器)超时时间 + proxy_read_timeout 1s; + index index.html index.htm; + } + } + +``` +### Nginx怎么判断别IP不可访问? + +```java +# 如果访问的ip地址为192.168.9.115,则返回403 +if ($remote_addr = 192.168.9.115) { + return 403; +} +``` +### 怎么限制浏览器访问? + +```java +## 不允许谷歌浏览器访问 如果是谷歌浏览器返回500 +if ($http_user_agent ~ Chrome) { + return 500; +} +``` +### Rewrite全局变量是什么? + +> | 变量 | 含义 | +> | --- | --- | +> | $args | 这个变量等于请求行中的参数,同$query_string | +> | $content length | 请求头中的Content-length字段。 | +> | $content_type | 请求头中的Content-Type字段。 | +> | $document_root | 当前请求在root指令中指定的值。 | +> | $host | 请求主机头字段,否则为服务器名称。 | +> | $http_user_agent | 客户端agent信息 | +> | $http_cookie | 客户端cookie信息 | +> | $limit_rate | 这个变量可以限制连接速率。 | +> | $request_method | 客户端请求的动作,通常为GET或POST。 | +> | $remote_addr | 客户端的IP地址。 | +> | $remote_port | 客户端的端口。 | +> | $remote_user | 已经经过Auth Basic Module验证的用户名。 | +> | $request_filename | 当前请求的文件路径,由root或alias指令与URI请求生成。 | +> | $scheme | HTTP方法(如http,https)。 | +> | $server_protocol | 请求使用的协议,通常是HTTP/1.0或HTTP/1.1。 | +> | $server_addr | 服务器地址,在完成一次系统调用后可以确定这个值。 | +> | $server_name | 服务器名称。 | +> | $server_port | 请求到达服务器的端口号。 | +> | $request_uri | 包含请求参数的原始URI,不包含主机名,如”/foo/bar.php?arg=baz”。 | +> | $uri | 不带请求参数的当前URI,$uri不包含主机名,如”/foo/bar.html”。 | +> | $document_uri | 与$uri相同。 | + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125784653837 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/elasticSearch.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/elasticSearch.md" new file mode 100644 index 0000000..8c213a4 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/elasticSearch.md" @@ -0,0 +1,296 @@ +https://juejin.cn/post/6844904032083902471 + +## 前言 + + ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。ElasticSearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr,也是基于Lucene。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47cdfa683a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +## Elasticsearch 面试题 + + 1、elasticsearch 了解多少,说说你们公司 es 的集群架构,索引数据大小,分片有多少,以及一些调优手段 。 + + 2、elasticsearch 的倒排索引是什么 + + 3、elasticsearch 索引数据多了怎么办,如何调优,部署 + + 4、elasticsearch 是如何实现 master 选举的 + + 5、详细描述一下 Elasticsearch 索引文档的过程 + + 6、详细描述一下 Elasticsearch 搜索的过程? + + 7、Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法 + + 8、lucence 内部结构是什么? + + 9、Elasticsearch 是如何实现 Master 选举的? + + 10、Elasticsearch 中的节点(比如共 20 个),其中的 10 个选了一个master,另外 10 个选了另一个 master,怎么办? + + 11、客户端在和集群连接时,如何选择特定的节点执行请求的? + + 12、详细描述一下 Elasticsearch 索引文档的过程。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47cb162405~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +## 1、elasticsearch 了解多少,说说你们公司 es 的集群架构,索引数据大小,分片有多少,以及一些调优手段 。 + + 面试官:想了解应聘者之前公司接触的 ES 使用场景、规模,有没有做过比较大规模的索引设计、规划、调优。 + + 解答:如实结合自己的实践场景回答即可。 + + 比如:ES 集群架构 13 个节点,索引根据通道不同共 20+索引,根据日期,每日递增 20+,索引:10 分片,每日递增 1 亿+数据,每个通道每天索引大小控制:150GB 之内。 + + 仅索引层面调优手段: + +### 1.1、设计阶段调优 + + (1)根据业务增量需求,采取基于日期模板创建索引,通过 roll over API 滚动索引; + + (2)使用别名进行索引管理; + + (3)每天凌晨定时对索引做 force_merge 操作,以释放空间; + + (4)采取冷热分离机制,热数据存储到 SSD,提高检索效率;冷数据定期进行 shrink操作,以缩减存储; + + (5)采取 curator 进行索引的生命周期管理; + + (6)仅针对需要分词的字段,合理的设置分词器; + + (7)Mapping 阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。…….. + +### 1.2、写入调优 + + (1)写入前副本数设置为 0; + + (2)写入前关闭 refresh_interval 设置为-1,禁用刷新机制; + + (3)写入过程中:采取 bulk 批量写入; + + (4)写入后恢复副本数和刷新间隔; + + (5)尽量使用自动生成的 id。 + +### 1.3、查询调优 + + (1)禁用 wildcard; + + (2)禁用批量 terms(成百上千的场景); + + (3)充分利用倒排索引机制,能 keyword 类型尽量 keyword; + + (4)数据量大时候,可以先基于时间敲定索引再检索; + + (5)设置合理的路由机制。 + +### 1.4、其他调优 + + 部署调优,业务调优等。 + + 上面的提及一部分,面试者就基本对你之前的实践或者运维经验有所评估了。 + +## 2、elasticsearch 的倒排索引是什么 + + 面试官:想了解你对基础概念的认知。 + + 解答:通俗解释一下就可以。 + + 传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。 + + 而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。有了倒排索引,就能实现 o(1)时间复杂度的效率检索文章了,极大的提高了检索效率。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47d11e0d11~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + 学术的解答方式: + + 倒排索引,相反于一篇文章包含了哪些词,它从词出发,记载了这个词在哪些文档中出现过,由两部分组成——词典和倒排表。 + + 加分项:倒排索引的底层实现是基于:FST(Finite State Transducer)数据结构。 + + lucene 从 4+版本后开始大量使用的数据结构是 FST。FST 有两个优点: + + (1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间; + + (2)查询速度快。O(len(str))的查询时间复杂度。 + +## 3、elasticsearch 索引数据多了怎么办,如何调优,部署 + + 面试官:想了解大数据量的运维能力。 + + 解答:索引数据的规划,应在前期做好规划,正所谓“设计先行,编码在后”,这样才能有效的避免突如其来的数据激增导致集群处理能力不足引发的线上客户检索或者其他业务受到影响。 + + 如何调优,正如问题 1 所说,这里细化一下: + +### 3.1 动态索引层面 + + 基于模板+时间+rollover api 滚动创建索引,举例:设计阶段定义:blog 索引的模板格式为:blog_index_时间戳的形式,每天递增数据。这样做的好处:不至于数据量激增导致单个索引数据量非常大,接近于上线 2 的32 次幂-1,索引存储达到了 TB+甚至更大。 + + 一旦单个索引很大,存储等各种风险也随之而来,所以要提前考虑+及早避免。 + +### 3.2 存储层面 + + 冷热数据分离存储,热数据(比如最近 3 天或者一周的数据),其余为冷数据。 + + 对于冷数据不会再写入新数据,可以考虑定期 force_merge 加 shrink 压缩操作,节省存储空间和检索效率。 + +### 3.3 部署层面 + + 一旦之前没有规划,这里就属于应急策略。 + + 结合 ES 自身的支持动态扩展的特点,动态新增机器的方式可以缓解集群压力,注意:如果之前主节点等规划合理,不需要重启集群也能完成动态新增的。 + +## 4、elasticsearch 是如何实现 master 选举的 + + 面试官:想了解 ES 集群的底层原理,不再只关注业务层面了。 + + 解答: + + 前置前提: + + (1)只有候选主节点(master:true)的节点才能成为主节点。 + + (2)最小主节点数(min_master_nodes)的目的是防止脑裂。 + + 核对了一下代码,核心入口为 findMaster,选择主节点成功返回对应 Master,否则返回 null。选举流程大致描述如下: + + 第一步:确认候选主节点数达标,elasticsearch.yml 设置的值 + + discovery.zen.minimum_master_nodes; + + 第二步:比较:先判定是否具备 master 资格,具备候选主节点资格的优先返回; + + 若两节点都为候选主节点,则 id 小的值会主节点。注意这里的 id 为 string 类型。 + + 题外话:获取节点 id 的方法。 + +1GET /_cat/nodes?v&h=ip,port,heapPercent,heapMax,id,name + +2ip port heapPercent heapMax id name复制代码 +## 5、详细描述一下 Elasticsearch 索引文档的过程 + + 面试官:想了解 ES 的底层原理,不再只关注业务层面了。 + + 解答: + + 这里的索引文档应该理解为文档写入 ES,创建索引的过程。 + + 文档写入包含:单文档写入和批量 bulk 写入,这里只解释一下:单文档写入流程。 + + 记住官方文档中的这个图。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47d2b0df73~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + 第一步:客户写集群某节点写入数据,发送请求。(如果没有指定路由/协调节点,请求的节点扮演路由节点的角色。) + + 第二步:节点 1 接受到请求后,使用文档_id 来确定文档属于分片 0。请求会被转到另外的节点,假定节点 3。因此分片 0 的主分片分配到节点 3 上。 + + 第三步:节点 3 在主分片上执行写操作,如果成功,则将请求并行转发到节点 1和节点 2 的副本分片上,等待结果返回。所有的副本分片都报告成功,节点 3 将向协调节点(节点 1)报告成功,节点 1 向请求客户端报告写入成功。 + + 如果面试官再问:第二步中的文档获取分片的过程? + + 回答:借助路由算法获取,路由算法就是根据路由和文档 id 计算目标的分片 id 的过程。 + +1shard = hash(_routing) % (num_of_primary_shards)复制代码 +## 6、详细描述一下 Elasticsearch 搜索的过程? + + 面试官:想了解 ES 搜索的底层原理,不再只关注业务层面了。 + + 解答: + + 搜索拆解为“query then fetch” 两个阶段。 + + query 阶段的目的:定位到位置,但不取。 + + 步骤拆解如下: + + (1)假设一个索引数据有 5 主+1 副本 共 10 分片,一次请求会命中(主或者副本分片中)的一个。 + + (2)每个分片在本地进行查询,结果返回到本地有序的优先队列中。 + + (3)第 2)步骤的结果发送到协调节点,协调节点产生一个全局的排序列表。 + + fetch 阶段的目的:取数据。 + + 路由节点获取所有文档,返回给客户端。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47d2edca5f~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +## 7、Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法 + + 面试官:想了解对 ES 集群的运维能力。 + + 解答: + + (1)关闭缓存 swap; + + (2)堆内存设置为:Min(节点内存/2, 32GB); + + (3)设置最大文件句柄数; + + (4)线程池+队列大小根据业务需要做调整; + + (5)磁盘存储 raid 方式——存储有条件使用 RAID10,增加单节点性能以及避免单节点存储故障。 + +## 8、lucence 内部结构是什么? + + 面试官:想了解你的知识面的广度和深度。 + + 解答: + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47d3590ee5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + Lucene 是有索引和搜索的两个过程,包含索引创建,索引,搜索三个要点。可以基于这个脉络展开一些。 + +## 9、Elasticsearch 是如何实现 Master 选举的? + + (1)Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含 Ping(节点之间通过这个 RPC 来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要 ping 通)这两部分; + + (2)对所有可以成为 master 的节点(node.master: true)根据 nodeId 字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个(第 0 位)节点,暂且认为它是 master 节点。 + + (3)如果对某个节点的投票数达到一定的值(可以成为 master 节点数 n/2+1)并且该节点自己也选举自己,那这个节点就是 master。否则重新选举一直到满足上述条件。 + + (4)补充:master 节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data 节点可以关闭 http 功能*。 + +## 10、Elasticsearch 中的节点(比如共 20 个),其中的 10 个 + + 选了一个 master,另外 10 个选了另一个 master,怎么办? + + (1)当集群 master 候选数量不小于 3 个时,可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes)超过所有候选节点一半以上来解决脑裂问题; + + (3)当候选数量为两个时,只能修改为唯一的一个 master 候选,其他作为 data节点,避免脑裂问题。 + +## 11、客户端在和集群连接时,如何选择特定的节点执行请求的? + + TransportClient 利用 transport 模块远程连接一个 elasticsearch 集群。它并不加入到集群中,只是简单的获得一个或者多个初始化的 transport 地址,并以 轮询 的方式与这些地址进行通信。 + +## 12、详细描述一下 Elasticsearch 索引文档的过程。 + + 协调节点默认使用文档 ID 参与计算(也支持通过 routing),以便为路由提供合适的分片。 + +shard = hash(document_id) % (num_of_primary_shards)复制代码 + + (1)当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 MemoryBuffer,然后定时(默认是每隔 1 秒)写入到 Filesystem Cache,这个从 MomeryBuffer 到 Filesystem Cache 的过程就叫做 refresh; + + (2)当然在某些情况下,存在 Momery Buffer 和 Filesystem Cache 的数据可能会丢失,ES 是通过 translog 的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到 translog 中 ,当 Filesystem cache 中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flush; + + (3)在 flush 过程中,内存中的缓冲将被清除,内容被写入一个新段,段的 fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的 translog 将被删除并开始一个新的 translog。 + + (4)flush 触发的时机是定时触发(默认 30 分钟)或者 translog 变得太大(默认为 512M)时; + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd48799304d6~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + 补充:关于 Lucene 的 Segement: + + (1)Lucene 索引是由多个段组成,段本身是一个功能齐全的倒排索引。 + + (2)段是不可变的,允许 Lucene 将新的文档增量地添加到索引中,而不用从头重建索引。 + + (3)对于每一个搜索请求而言,索引中的所有段都会被搜索,并且每个段会消耗CPU 的时钟周、文件句柄和内存。这意味着段的数量越多,搜索性能会越低。 + + (4)为了解决这个问题,Elasticsearch 会合并小段到一个较大的段,提交新的合并段到磁盘,并删除那些旧的小段。 + +作者:程序员追风 +链接:https://juejin.cn/post/6844904031555420167 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/kafka.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/kafka.md" new file mode 100644 index 0000000..677057b --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/kafka.md" @@ -0,0 +1 @@ +https://juejin.cn/post/6844904025805029383 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mybatis.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mybatis.md" new file mode 100644 index 0000000..b648d26 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mybatis.md" @@ -0,0 +1,565 @@ + + +## MyBatis简介 + +### MyBatis是什么? + +* Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高。 + +* MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。 + +### Mybatis优缺点 + +**优点** + +`与传统的数据库访问技术相比,ORM有以下优点:` + +* 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用 +* 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接 +* 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持) +* 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护 +* 能够与Spring很好的集成 + +**缺点** + +* SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求 +* SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库 + +### Hibernate 和 MyBatis 的区别 + +**相同点** + +* 都是对jdbc的封装,都是持久层的框架,都用于dao层的开发。 + +**不同点** + +* 映射关系 + * MyBatis 是一个半自动映射的框架,配置Java对象与sql语句执行结果的对应关系,多表关联关系配置简单 + * Hibernate 是一个全表映射的框架,配置Java对象与数据库表的对应关系,多表关联关系配置复杂 + +**SQL优化和移植性** + +* Hibernate 对SQL语句封装,提供了日志、缓存、级联(级联比 MyBatis 强大)等特性,此外还提供 HQL(Hibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需要支持多种数据库,代码开发量少,但SQL语句优化困难。 +* MyBatis 需要手动编写 SQL,支持动态 SQL、处理列表、动态生成表名、支持存储过程。开发工作量相对大些。直接使用SQL语句操作数据库,不支持数据库无关性,但sql语句优化容易。 + +### ORM是什么 + +* ORM(Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库数据与简单Java对象(POJO)的映射关系的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。 + +### 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? + +* Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。 + +* 而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 + +### 传统JDBC开发存在什么问题? + +* 频繁创建数据库连接对象、释放,容易造成系统资源浪费,影响系统性能。可以使用连接池解决这个问题。但是使用jdbc需要自己实现连接池。 +* sql语句定义、参数设置、结果集处理存在硬编码。实际项目中sql语句变化的可能性较大,一旦发生变化,需要修改java代码,系统需要重新编译,重新发布。不好维护。 +* 使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护。 +* 结果集处理存在重复代码,处理麻烦。如果可以映射成Java对象会比较方便。 + +### JDBC编程有哪些不足之处,MyBatis是如何解决的? + +* 1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。 + + * 解决:在mybatis-config.xml中配置数据链接池,使用连接池管理数据库连接。 +* 2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。- + + * 解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。 +* 3、向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。 + + * 解决: Mybatis自动将java对象映射至sql语句。 +* 4、对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。 + + * 解决:Mybatis自动将sql执行结果映射至java对象。 + +### MyBatis和Hibernate的适用场景? + +* MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。 +* 对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis将是不错的选择。 + +**开发难易程度和学习成本** + +* Hibernate 是重量级框架,学习使用门槛高,适合于需求相对稳定,中小型的项目,比如:办公自动化系统 + +* MyBatis 是轻量级框架,学习使用门槛低,适合于需求变化频繁,大型的项目,比如:互联网电子商务系统 + +**总结** + +* MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架, + +* Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架。 + +## MyBatis的架构 + +### MyBatis编程步骤是什么样的? + +* 1、 创建SqlSessionFactory + +* 2、 通过SqlSessionFactory创建SqlSession + +* 3、 通过sqlsession执行数据库操作 + +* 4、 调用session.commit()提交事务 + +* 5、 调用session.close()关闭会话 + +### 请说说MyBatis的工作原理 + +* 在学习 MyBatis 程序之前,需要了解一下 MyBatis 工作原理,以便于理解程序。MyBatis 的工作原理如下图 ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717343a66d9566c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +1. 读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。 +2. 加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。 +3. 构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。 +4. 创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。 +5. Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。 +6. MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。 +7. 输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。 +8. 输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。 + +### MyBatis的功能架构是怎样的 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717343a5a426a4d~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 我们把Mybatis的功能架构分为三层: + * API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。 + * 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。 + * 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。 + +### MyBatis的框架架构设计是怎么样的 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717343a5ab904a6~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 这张图从上往下看。MyBatis的初始化,会从mybatis-config.xml配置文件,解析构造成Configuration这个类,就是图中的红框。 + +1. 加载配置:配置来源于两个地方,一处是配置文件,一处是Java代码的注解,将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。 + +2. SQL解析:当API接口层接收到调用请求时,会接收到传入SQL的ID和传入对象(可以是Map、JavaBean或者基本数据类型),Mybatis会根据SQL的ID找到对应的MappedStatement,然后根据传入参数对象对MappedStatement进行解析,解析后可以得到最终要执行的SQL语句和参数。 + +3. SQL执行:将最终得到的SQL和参数拿到数据库进行执行,得到操作数据库的结果。 + +4. 结果映射:将操作数据库的结果按照映射的配置进行转换,可以转换成HashMap、JavaBean或者基本数据类型,并将最终结果返回。 + +### 什么是DBMS + +* DBMS:数据库管理系统(database management system)是一种操纵和管理数据库的大型软件,用于建立、使用和维护数zd据库,简称dbms。它对数据库进行统一的管理和控制,以保证数据库的安全性和完整性。用户通过dbms访问数据库中的数据,数据库管理员也通过dbms进行数据库的维护工作。它可使多个应用程序和用户用不同的方法在同时版或不同时刻去建立,修改和询问数据库。DBMS提供数据定义语言[DDL](https://link.juejin.cn?target=https%3A%2F%2Fwww.baidu.com%2Fs%3Fwd%3DDDL%26tn%3DSE_PcZhidaonwhc_ngpagmjz%26rsv_dl%3Dgh_pc_zhidao "https://www.baidu.com/s?wd=DDL&tn=SE_PcZhidaonwhc_ngpagmjz&rsv_dl=gh_pc_zhidao")(Data Definition Language)与数据操作语言[DML](https://link.juejin.cn?target=https%3A%2F%2Fwww.baidu.com%2Fs%3Fwd%3DDML%26tn%3DSE_PcZhidaonwhc_ngpagmjz%26rsv_dl%3Dgh_pc_zhidao "https://www.baidu.com/s?wd=DML&tn=SE_PcZhidaonwhc_ngpagmjz&rsv_dl=gh_pc_zhidao")(Data Manipulation Language),供用户定义数据库的模式结构与权限约束,实现对数据的追加权、删除等操作。 + +### 为什么需要预编译 + +* 定义: + + * SQL 预编译指的是数据库驱动在发送 SQL 语句和参数给 DBMS 之前对 SQL 语句进行编译,这样 DBMS 执行 SQL 时,就不需要重新编译。 +* 为什么需要预编译 + + * JDBC 中使用对象 PreparedStatement 来抽象预编译语句,使用预编译。预编译阶段可以优化 SQL 的执行。预编译之后的 SQL 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的SQL,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。同时预编译语句对象可以重复利用。把一个 SQL 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个SQL,可以直接使用这个缓存的 PreparedState 对象。Mybatis默认情况下,将对所有的 SQL 进行预编译。 + * 还有一个重要的原因,复制SQL注入 + +### Mybatis都有哪些Executor执行器?它们之间的区别是什么? + +* Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。 + +* **SimpleExecutor**:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。 + +* **ReuseExecutor**:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。 + +* **BatchExecutor**:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。 + +`作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。` + +### Mybatis中如何指定使用哪一种Executor执行器? + +* 在Mybatis配置文件中,在设置(settings)可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数,如SqlSession openSession(ExecutorType execType)。 + +* 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 + +### Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? + +* Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 + +* 它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。 + +* 当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。 + +## 映射器 + +### #{}和${}的区别 + +* #{}是占位符,预编译处理;${}是拼接符,字符串替换,没有预编译处理。 + +* Mybatis在处理#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值。 + +* #{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL 注入 + +* #{} 的变量替换是在DBMS 中;${} 的变量替换是在 DBMS 外 + +### 模糊查询like语句该怎么写 + +* 1 ’%${question}%’ 可能引起SQL注入,不推荐 +* 2 "%"#{question}"%" 注意:因为#{…}解析成sql语句时候,会在变量外侧自动加单引号’ ',所以这里 % 需要使用双引号" ",不能使用单引号 ’ ',不然会查不到任何结果。 +* 3 CONCAT(’%’,#{question},’%’) 使用CONCAT()函数,(推荐) +* 4 使用bind标签(不推荐) + + +复制代码 +### 在mapper中如何传递多个参数 + +**方法1:顺序传参法** + +public User selectUser(String name, int deptId); + + +复制代码 + +* #{}里面的数字代表传入参数的顺序。 + +* 这种方法不建议使用,sql层表达不直观,且一旦顺序调整容易出错。 + +**方法2:@Param注解传参法** + +public User selectUser(@Param("userName") String name, int @Param("deptId") deptId); + + +复制代码 + +* #{}里面的名称对应的是注解@Param括号里面修饰的名称。 + +* 这种方法在参数不多的情况还是比较直观的,(推荐使用)。 + +**方法3:Map传参法** + +public User selectUser(Map params); + + +复制代码 + +* #{}里面的名称对应的是Map里面的key名称。 + +* 这种方法适合传递多个参数,且参数易变能灵活传递的情况。(推荐使用)。 + +**方法4:Java Bean传参法** + +public User selectUser(User user); + + +复制代码 + +* #{}里面的名称对应的是User类里面的成员属性。 + +* 这种方法直观,需要建一个实体类,扩展不容易,需要加属性,但代码可读性强,业务逻辑处理方便,推荐使用。(推荐使用)。 + +### Mybatis如何执行批量操作 + +* **使用foreach标签** +* foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合。foreach标签的属性主要有item,index,collection,open,separator,close。 + * item   表示集合中每一个元素进行迭代时的别名,随便起的变量名; + * index   指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用; + * open   表示该语句以什么开始,常用“(”; + * separator 表示在每次进行迭代之间以什么符号作为分隔符,常用“,”; + * close   表示以什么结束,常用“)”。 +* 在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有一下3种情况: + 1. 如果传入的是单参数且参数类型是一个List的时候,collection属性值为list + 2. 如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array + 3. 如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的, + map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key +* 具体用法如下: + + + //推荐使用 + + + INSERT INTO emp(ename,gender,email,did) + VALUES + + (#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id}) + + +复制代码 + + + + INSERT INTO emp(ename,gender,email,did) + VALUES(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id}) + + +复制代码 + +* **使用ExecutorType.BATCH** + + * Mybatis内置的ExecutorType有3种,默认为simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;而batch模式重复使用已经预处理的语句,并且批量执行所有更新语句,显然batch性能将更优; 但batch模式也有自己的问题,比如在Insert操作时,在事务没有提交之前,是没有办法获取到自增的id,这在某型情形下是不符合业务要求的 + + * 具体用法如下: + + //批量保存方法测试 + @Test + public void testBatch() throws IOException{ + SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); + //可以执行批量操作的sqlSession + SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH); + + //批量保存执行前时间 + long start = System.currentTimeMillis(); + try { + EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class); + for (int i = 0; i < 1000; i++) { + mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1")); + } + + openSession.commit(); + long end = System.currentTimeMillis(); + //批量保存执行后的时间 + System.out.println("执行时长" + (end - start)); + //批量 预编译sql一次==》设置参数==》10000次==》执行1次 677 + //非批量 (预编译=设置参数=执行 )==》10000次 1121 + + } finally { + openSession.close(); + } + } + 复制代码 + * mapper和mapper.xml如下 + + public interface EmployeeMapper { + //批量保存员工 + Long addEmp(Employee employee); + } + + 复制代码 + + insert into employee(lastName,email,gender) + values(#{lastName},#{email},#{gender}) + + + + 复制代码 + +### 如何获取生成的主键 + +* 新增标签中添加:keyProperty=" ID " 即可 + + + insert into user( + user_name, user_password, create_time) + values(#{userName}, #{userPassword} , #{createTime, jdbcType= TIMESTAMP}) + + + 复制代码 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717343a5d83efe4~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 当实体类中的属性名和表中的字段名不一样 ,怎么办 + +* 第1种: 通过在查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。 + + + + 复制代码 +* 第2种: 通过``来映射字段名和实体类属性名的一一对应的关系。 + + + + + + + + + + + + + 复制代码 + +### Mapper 编写有哪几种方式? + +* 第一种:接口实现类继承 SqlSessionDaoSupport:使用此种方法需要编写mapper 接口,mapper 接口实现类、mapper.xml 文件。 + + 1. 在 sqlMapConfig.xml 中配置 mapper.xml 的位置 + + + + + + + 复制代码 + 2. 定义 mapper 接口 + + 3. 实现类集成 SqlSessionDaoSupport + + mapper 方法中可以 this.getSqlSession()进行数据增删改查。 + + 4. spring 配置 + + + + + + 复制代码 +* 第二种:使用 org.mybatis.spring.mapper.MapperFactoryBean: + + 1. 在 sqlMapConfig.xml 中配置 mapper.xml 的位置,如果 mapper.xml 和mappre 接口的名称相同且在同一个目录,这里可以不用配置 + + 2. 定义 mapper 接口: + + + + + + + 复制代码 + 3. mapper.xml 中的 namespace 为 mapper 接口的地址 + + 4. mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致 + + 5. Spring 中定义 + + + + + + + 复制代码 +* 第三种:使用 mapper 扫描器: + + 1. mapper.xml 文件编写: + + mapper.xml 中的 namespace 为 mapper 接口的地址; + + mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致; + + 如果将 mapper.xml 和 mapper 接口的名称保持一致则不用在 sqlMapConfig.xml中进行配置。 + + 2. 定义 mapper 接口: + + 注意 mapper.xml 的文件名和 mapper 的接口名称保持一致,且放在同一个目录 + + 3. 配置 mapper 扫描器: + + + + + + + 复制代码 + 4. 使用扫描器后从 spring 容器中获取 mapper 的实现对象。 + +### 什么是MyBatis的接口绑定?有哪些实现方式? + +* 接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定,我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。 + +* 接口绑定有两种实现方式 + + 1. 通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定; + + 2. 通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。 + +### 使用MyBatis的mapper接口调用时有哪些要求? + +1. Mapper接口方法名和mapper.xml中定义的每个sql的id相同。 +2. Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同。 +3. Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同。 +4. Mapper.xml文件中的namespace即是mapper接口的类路径。 + +### 这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗 + +* Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。 + +* Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。 + +### Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复? + +* 不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。 + +* 原因就是namespace+id是作为Map的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。 + +### 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? + +* 答:Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。在Xml映射文件中,``标签会被解析为ParameterMap对象,其每个子元素会被解析为ParameterMapping对象。``标签会被解析为ResultMap对象,其每个子元素会被解析为ResultMapping对象。每一个`