└── docs ├── .DS_Store ├── .nojekyll ├── README.md ├── _coverpage.md ├── image └── codegeek.png ├── index.html └── notes ├── data-structures-and-algorithms ├── 【手撕数据结构与算法】-开篇.md ├── 【手撕数据结构与算法】-数组.md └── 【手撕数据结构与算法】-链表.md ├── distributed └── 了解分布式架构.md ├── docker └── Docker、K8s简介.md ├── java ├── JUnit4教程+实践.md ├── Java11都有哪些特性.md ├── Java线程、锁、线程池总结.md ├── Java集合总结.md ├── 【Java8系列】Lambda表达式.md ├── 【Java8系列】NPE神器Optional.md ├── 【Java8系列】函数式接口.md └── 【Java8系列】流式编程Stream.md ├── jvm ├── JVM 进阶 | java字节码.md └── JVM 进阶 | 基础知识.md ├── mybatis ├── Mybatis入门.md ├── Mybatis开发方式和配置.md ├── Mybatis缓存.md └── Mybatis高级应用.md └── practice └── 通用点赞设计思路.md /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/li-xiao-shuang/blog/99ddf93e0320ecdbbd7b2a29e7bf43ae0aac7c92/docs/.DS_Store -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/li-xiao-shuang/blog/99ddf93e0320ecdbbd7b2a29e7bf43ae0aac7c92/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | ![https://img.shields.io/badge/%E5%85%AC%E4%BC%97%E5%8F%B7-CodeGeek-green](https://img.shields.io/badge/公众号-俩右-green) ![https://img.shields.io/badge/original-%E5%8F%8C%E5%93%A5-yellow](https://img.shields.io/badge/original-XiaoshuangLi-yellow) 4 | 5 | 此项目是利用业余时间,对一些技术知识点进行整理,用来记录个人学习笔记。这个项目和 [study](https://github.com/xiaoshuanglee/study) 项目的不同在于 [study](https://github.com/xiaoshuanglee/study) 是用来动手实践,对于一些技术的实际搭建和造轮子的项目,正所谓实践出真知。相关的源码都会在上边。两个项目结合就是理论+实践。欢迎大家Star和follow! 6 | 7 | 8 | 9 | #### Java 10 | 11 | - [「Java8系列」Lambda表达式](notes/java/【Java8系列】Lambda表达式.md) 12 | - [「Java8系列」函数式接口](notes/java/【Java8系列】函数式接口.md) 13 | - [「Java8系列」流式编程Stream](notes/java/【Java8系列】流式编程Stream.md) 14 | - [「Java8系列」NPE神器Optional](notes/java/【Java8系列】NPE神器Optional.md) 15 | - [Java11都有哪些特性?](notes/java/Java11都有哪些特性.md) 16 | - [Java集合总结](notes/java/Java集合总结.md) 17 | - [Java线程、锁、线程池总结](notes/java/Java线程、锁、线程池总结.md) 18 | #### 数据结构与算法 19 | 20 | - [手撕数据结构与算法-开篇](notes/data-structures-and-algorithms/【手撕数据结构与算法】-开篇.md) 21 | - [手撕数据结构与算法-数组](notes/data-structures-and-algorithms/【手撕数据结构与算法】-数组.md) 22 | - [手撕数据结构与算法-链表](notes/data-structures-and-algorithms/【手撕数据结构与算法】-链表.md) 23 | 24 | #### JVM 25 | 26 | - [JVM进阶|基础知识](notes/jvm/JVM%20进阶%20%7C%20基础知识.md) 27 | - [JVM进阶|java字节码](notes/jvm/JVM%20进阶%20%7C%20java字节码.md) 28 | 29 | #### Mybatis 30 | 31 | - [「Mybatis系列」Mybatis入门](notes/mybatis/Mybatis入门.md) 32 | - [「Mybatis系列」Mybatis开发方式和配置](notes/mybatis/Mybatis开发方式和配置.md) 33 | - [「Mybatis系列」Mybatis高级应用](notes/mybatis/Mybatis高级应用.md) 34 | - [「Mybatis系列」Mybatis缓存](notes/mybatis/Mybatis缓存.md) 35 | 36 | #### 分布式 37 | 38 | - [了解分布式架构](notes/distributed/了解分布式架构.md) 39 | 40 | #### 实践 41 | 42 | - [JUnit4教程+实践](notes/java/JUnit4教程+实践.md) 43 | - [通用点赞设计思路](notes/practice/通用点赞设计思路.md) 44 | - [JAVA SPI原理](https://github.com/xiaoshuanglee/study/tree/master/study-java-spi) 45 | - [Java虚拟机关闭钩子(Shutdown hook)](https://github.com/xiaoshuanglee/study/tree/master/study-jvm-hook) 46 | 47 | 48 | 49 | #### 公众号 50 | 51 | ![iShot2020-10-29 10.42.39](https://tva1.sinaimg.cn/large/0081Kckwly1gl87d0b8gej31b60swwsk.jpg) 52 | 53 | -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - 此项目是利用业余时间,对一些技术知识点进行整理,用来记录个人学习笔记。这个项目和 [study](https://github.com/xiaoshuanglee/study) 项目的不同在于 [study](https://github.com/xiaoshuanglee/study) 是用来动手实践,对于一些技术的实际搭建和造轮子的项目,正所谓实践出真知。相关的源码都会在上边。两个项目结合就是理论+实践。欢迎大家Star和follow! 5 | 6 | 7 | 8 | [![stars](https://badgen.net/github/stars/CodeGeekLee/RoadToGrowth?icon=github&color=4ab8a1)](https://github.com/CodeGeekLee/RoadToGrowth) 9 | [![forks](https://badgen.net/github/forks/CodeGeekLee/RoadToGrowth?icon=github&color=4ab8a1)](https://github.com/CodeGeekLee/RoadToGrowth) 10 | 11 | [开始阅读](README.md) -------------------------------------------------------------------------------- /docs/image/codegeek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/li-xiao-shuang/blog/99ddf93e0320ecdbbd7b2a29e7bf43ae0aac7c92/docs/image/codegeek.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/notes/data-structures-and-algorithms/【手撕数据结构与算法】-开篇.md: -------------------------------------------------------------------------------- 1 | ### 1. 前言 2 | 3 | > 开篇一张图 4 | 5 | ![](https://i.loli.net/2019/11/25/EvK4mpuixgl89I6.png) 6 | 7 | 《数据结构与算法》在我学生时代就是一门让我望而止步的课程。听着名字就感觉很晦涩难懂、需要大量的数学知识做铺垫。相信很多人也都和我一样,上学的时候学的一知半解,到了工作以后也很少用到就不了了之了。但是它却成为了你面试、寻找好的平台的障碍。很多大厂都很看中程序员的`基本功`,所以在面试中算法就编程了常考题目,为什么呢?因为基础知识就像是一座大楼的地基,它能够决定你技术的`高度`与`深度`。所以一般大厂都是看中你有没有这个技术发展的潜力。`("所以大家要夯实基本功了。")` 8 | 9 | 10 | ### 2. 数学知识复习 11 | 12 | 在我们系统的学习数据结构与算法之前,我们先简单的复习几个数学知识,相信大家也都忘的差不多了,是不是都学完了又还给老师了呢?嫑急,跟我一起来复习一下。 13 | 14 | #### 2.1. 指数 15 | 16 | 指数是幂运算aⁿ(a≠0)中的一个参数,a为底数,n为指数,指数位于底数的右上角,幂运算表示指数个底数相乘。当n是一个正整数,a**ⁿ**表示n个a连乘。当n=0时,aⁿ=1。《百度百科》 17 | 18 | - `指数:就是aⁿ中的n。` 19 | - `底数:就是aⁿ的a` 20 | - `幂运算:指数个底数相乘。` 21 | 22 | **幂运算公式:** 23 | ![](https://i.loli.net/2019/11/24/kQhfwzRWBpTs6a5.png) 24 | 25 | #### 2.2. 对数 26 | 27 | $ a^{x}=n$ 如果a的x次方等于N(a>0,且a不等于1),那么数x叫做以a为底N的对数(logarithm),记作$x=log_{a}N$。其中,a叫做对数的[底数](https://baike.baidu.com/item/底数/5416651),N叫做[真数](https://baike.baidu.com/item/真数/326681)。《百度百科》 28 | 29 | `在计算机科学中,除非有特别的声明,否则所有的对数都是以2为底的。` 30 | 31 | **公式:** 32 | ![](https://i.loli.net/2019/11/24/7eu3RETys2XxK4V.png) 33 | 34 | 简单列了两个公式,大家看看就行了,知道一下啥是`对数`。 35 | 36 | ### 3. 时间复杂度 37 | 38 | 对于算法`时间复杂度`,可能有的朋友可能想了,不就是估算一段代码的执行时间嘛,我们可以搞个监控啊,看看一下每个接口的耗时不就好了,何必那么麻烦,还要分析下时间复杂度。但是这个监控属于事后操作,只有代码在`运行时`,才能知道你写的代码`效率`高不高,那么如何在写代码的时候就评估一段代码的执行效率呢,这个时候就需要时间复杂度来分析了。大家平常写代码可以结合时间复杂度和监控做好`事前`、`事后`的分析,更好的优化代码。 39 | 40 | ![](https://i.loli.net/2019/11/24/OyqhU6kNTZ3bWoP.png) 41 | 42 | #### 3.1 大O表示法 43 | 44 | 因为渐进时间复杂度使用大写O来表示,所以也称`大O表示法`。例如: `O(f(n))`。 45 | 46 | **常见时间复杂度:** 47 | ![](https://i.loli.net/2019/11/24/h3jDXVgYOZLn2tw.png) 48 | 49 | 常见时间复杂度所耗费时间从小到大依次是: 50 | 51 | ![](https://i.loli.net/2019/11/24/vupy9IiUl7Tc6JV.png) 52 | 53 | **推导大O的方法:** 54 | ![](https://i.loli.net/2019/11/24/Ily3YAdVpaSFqj8.png) 55 | 56 | #### 3.2 如何分析时间复杂度 57 | 58 | - `O(1)` 59 | 60 | ```java 61 | int i = 5; /*执行一次*/ 62 | int j = 6; /*执行一次*/ 63 | int sum = j + i; /*执行一次*/ 64 | ``` 65 | 66 | 这段代码的运行函数应该是f(n)=3 ,用来大O来表示的话应该是O(f(n))=O(3) ,但是根据我们的推导大O表示法中的第一条,要用1代替函数中的常数,所以O(3)=>O(1),那么这段代码的时间复杂度就是O(1)而不是O(3)。 67 | 68 | - `O(logn)` 69 | 70 | ```java 71 | int count = 1; /*执行一次*/ 72 | int n = 100; /*执行一次*/ 73 | while (count < n) { 74 | count = count * 2; /*执行多次*/ 75 | } 76 | ``` 77 | 78 | ![](https://i.loli.net/2019/11/24/FPQGyjz2XoAml3b.png) 79 | 80 | - `O(n)` 81 | 82 | ```java 83 | for (int k = 0; k < n; k++) { 84 | System.out.println(k); /*执行n次*/ 85 | } 86 | ``` 87 | 88 | 这段代码的执行次数会随着n的增大而增大,也就是说会执行n次,所以他的时间复杂度就是O(n)。 89 | 90 | - $O(n^{2})$ 91 | 92 | ```java 93 | for (int k = 0; k < n; k++) { 94 | for (int l = 0; l < n; l++) { 95 | System.out.println(l); /*执行了n*n次/ 96 | } 97 | } 98 | ``` 99 | 100 | ![](https://i.loli.net/2019/11/24/kGgw3SzorpCQYI4.png) 101 | 102 | `读到这里不知道大家学会了没有?其实分析一段代码的时间复杂度,就找到你代码中执行次数最多的地方,分析一下它的时间复杂度是什么,那么你整段代码的时间复杂度就是什么。以最大为准。` 103 | 104 | #### 3.3 时间复杂度量级 105 | 106 | ```java 107 | public int find(int[] arrays, int findValue) { 108 | int result = -1; /*执行一次*/ 109 | int n = arrays.length; 110 | for (int i = 0; i < n; i++) { 111 | if (arrays[i] == findValue) { /*执行arrays.length次*/ 112 | result = arrays[i]; 113 | break; 114 | } 115 | } 116 | return result; /*执行一次*/ 117 | } 118 | ``` 119 | 120 | 我们来分析一下上边这个方法,这个方法的作用是从一个数组中查找到它想要的值。其实一个算法的复杂度还会根据实际的执行情况有一定的变化,就比如上边这段代码,假如数组的长度是100,里面存的是1-100的数。 121 | 122 | - 最好情况时间复杂度 123 | 124 | 如果我在这个数组里面查找数字1,那么在它第一次遍历的时候就找到了这个值,然后就执行`break`结束当前循环,此时所有的代码只执行了一次,属于`常数阶O(1)`,这就是最好情况下这段代码的时间复杂度。 125 | 126 | - 最坏情况时间复杂度 127 | 128 | 如果我在这个数组里面查找数字100,那么这个数组就要被遍历一边才能找到并返回,这样的话这个方法就要受到数组大小的影响了,如果数组的大小为n,那么就是n越大,执行次数越多。属于`线性阶O(n)` ,这就是最坏情况下的时间复杂度。 129 | 130 | - 平均情况时间复杂度 131 | 132 | 我们都知道最好、最坏时间复杂度都是在两种极端情况下的代码复杂度,发生的概率并不高,因次我们引入另一个概念`“平均时间复杂度”`。我们还看上边的这个方法,要查找个一个数有n+1中情况:在数组0 ~ n-1的的位置中和不再数组中,所以我们将所有代码的执行次数累加起来((1+2+3+...+n)+n),然后再除以所有情况n(n+1),就得到需要执行次数的平均值了。 133 | 134 | ![](https://i.loli.net/2019/11/24/4LemwMDaQpY3db2.png) 135 | 136 | **推导过程:** 137 | ![](https://i.loli.net/2019/11/24/p1Kr6CXulHFxsve.png) 138 | 139 | 大O表示法,会**省略系数、低阶、常量,**所以平均情况时间复杂度是**O(n)**。 140 | 141 | 142 | 143 | 但是这个平均复杂度没有考虑各自情况的发生概率,这里的n+1个情况,它们的发生概率是不一样的,所以还需要引入各自情况发生的概率再具体分析。findValue要么在1~n中,要么不在1~n中,所以他们的概率都是$\frac{1}{2}$,同时数据在1~n中的各个位置的概率一样为$\frac{1}{n}$ ,根据概率乘法法则,findValue在1~n中的任意位置的概率是$\frac{1}{2n}$ ,因此在上边推导的基础上需要在加入概率的的发生情况。 144 | 145 | **考虑概率的平均情况复杂度为:** 146 | 147 | ![](https://i.loli.net/2019/11/24/ncv1fzChk2iRBqK.png) 148 | 149 | **推导过程:** 150 | ![](https://i.loli.net/2019/11/24/IzptajFCVxfvhDB.png) 151 | 152 | 这就是概率论中的加权平均值,也叫做期望值,所以平均时间复杂度全称叫:**加权平均时间复杂度**或者**期望时间复杂度**。平均复杂度变为$O(\frac{3n+1}{4})$,忽略系数及常量后,最终得到加权平均时间复杂度为O(n)。 153 | 154 | ### 4. 空间复杂度 155 | 156 | 算法的空间复杂度是对运行过程中临时占用存储空间大小的度量,算法空间复杂度的计算公式记作:`S(n) = O(f(n))`,n为问题规模,f(n)为语句关于n所占存储空间函数。由于空间复杂度和时间复杂度的大O表示法相同,所以我们就简单介绍下。 157 | 158 | 常见的空间复杂度从低到高是: 159 | 160 | ![](https://i.loli.net/2019/11/24/vVuKNFEJnqgRl6w.png) 161 | 162 | #### 4.1 如何分析空间复杂度 163 | 164 | - `O(1)` 165 | 166 | ```java 167 | public static void intFun(int n) { 168 | var intValue = n; 169 | //... 170 | } 171 | ``` 172 | 173 | 当算发的存储空间大小固定,和输入的规模没有直接的关系时,空间复杂度就记作O(1),就像上边这个方法,不管你是输入10,还是100,它占用的内存都是4字节。 174 | 175 | - `O(n)` 176 | 177 | ```java 178 | public static void arrayFun(int n) { 179 | var array = new int[n]; 180 | //... 181 | } 182 | ``` 183 | 184 | 当算法分配的空间是一个集合或者数组时,并且它的大小和输入规模n成正比时,此时空间复杂度记为$O(n)$。 185 | 186 | - $O(n^{2})$ 187 | 188 | ```java 189 | public static void matrixFun(int n) { 190 | var matrix = new int[n][n]; 191 | //... 192 | } 193 | ``` 194 | 195 | 当算法分配的空间是一个二维数组,并且它的第一维度和第二维度的大小都和输入规模n成正比时,此时空间复杂度记为$O(n^{2})$ 。 196 | 197 | ### 5. 总结 198 | 199 | 对于`时间`和`空间`的取舍,我们就要根据具体的业务实际情况而定,有的时候就需要牺牲时间来换空间,有的时候就需要牺牲空间来换时间,在现在这个计算机硬件性能飙升的时代,当然我们还是喜欢选择牺牲空间来换时间,毕竟内存还是有的,也不贵。并且可以提高效率给用户更好的体验。 200 | 201 | - 什么是时间复杂度? 202 | 203 | 时间复杂度就是对算法运行时间长短的度量,用大O表示为`T(n) = O(f(n))` 。常见的时间复杂度从低到高的顺序是: 204 | ![](https://i.loli.net/2019/11/24/r3HWCiDvwQTISFj.png) 205 | 206 | - 什么是空间复杂度? 207 | 208 | 空间复杂度是对算法运行时所占用的临时存储空间的度量,用大O标识为`S(n)= O(f(n))` 。常见的空间复杂度从低到高的顺序是: 209 | ![](https://i.loli.net/2019/11/24/rKguVnMTkY8mfjU.png) 210 | 211 | ### 6. 参考 212 | 213 | 1. 《数据结构与算法分析》 214 | 2. 《大话数据结构》 215 | 3. 《漫画算法》 -------------------------------------------------------------------------------- /docs/notes/data-structures-and-algorithms/【手撕数据结构与算法】-数组.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 前言 4 | 5 | > 开篇一张图 6 | 7 | ![](https://i.loli.net/2019/11/25/EvK4mpuixgl89I6.png) 8 | 9 | **本系列文章已收录到github:** 10 | 11 | - [手撕数据结构与算法](https://github.com/CodeGeekLee/data-structures-and-algorithms) 12 | 13 | ## 1. 什么是数组? 14 | 15 | 数组是数据结构中`最简单`、`最常用`的数据结构,是一种线性表数据结构,在内存中是一块`连续`的存储空间,是有限个`相同类型`变量所组成的`有序集合`。数组中的每一个变量叫做`元素`。 16 | 17 | > 线性表:线性表从字面意义上来理解是数据的排列像一条线的结构,只有前后两个方向。线性表中的元素都是一对一的关系,除了首尾元素外,其他元素都是首尾相连的。除了数组,链表、队列、栈也是线性表结构的。 18 | 19 | 以整型数组为例,我们new一个整型数组`int[] array = new int[]{1,2,3,4};`,数组内的元素存储的元素是1、2、3、4。那么数组的存储形势就如下图: 20 | 21 | ![数组图解.png](https://i.loli.net/2019/11/30/Q3PuZyrgFpCiIaB.png) 22 | 23 | 在上图中`粉色`的格子代表已经被占用了的存储单元,`绿色`的格子代表数组的存储位置,`白色`的格子代表空闲的存储单元。数组的下标是从0开始的。所以元素和下标的对应关系是: 24 | 25 | ![](https://i.loli.net/2019/11/30/ypVgOr35hamS8D9.png) 26 | 27 | ## 2. 数组的优缺点 28 | 29 | 谈起数组的优点,我相信大部分的人都会说`随机访问`这个堪称杀手锏的特性,那么它为什么能够做到随机访问呢? 30 | 31 | 我认为主要有两点: 32 | 33 | - 连续的存储空间 34 | - 线性表结构 35 | 36 | 正因为它是在内存中是一块连续的存储空间,并且是线性表结构,前后元素都是一一对应的,所以才能够让他拥有随机访问的特性。在上一篇文章[数据结构与算法-开篇](https://mp.weixin.qq.com/s/sWpbqEWHCZCimjRcy2pMKw)当中我们介绍了时间复杂度和空间复杂度,这里就不对说了,比如我们要查找上边的数组中的第三个元素,那么打印出`array[2]`就能够获取到第三个元素值,这里输出的是3,因为数组支持随机访问,所以根据下标随机访问的时间复杂度为`O(1)`,因为它的查找操作只执行了一次。 37 | 38 | 这样的结构使它的查询操作非常的方便,有利也有弊,它的插入、删除操作就会变得低效,因为要保证数据的连续性,所以执行插入、删除操作就需要做大量的数据搬移工作。如果这个时候一个数组的随机访问正好访问到没有值得下标上就会获取不到值。如果不搬移数据将中间的空洞补充上,那么内存就不连续了。我们在数组的操作中在详细介绍。 39 | 40 | ## 3. 数组的基本操作 41 | 42 | ### 3.1 添加元素 43 | 44 | - **中间插入** 45 | 46 | 中间插入稍微复杂一些,每个元素都有自己的下标,如果一个元素想要插入到数组的中的除首尾的位置,那么插入的该位置上的元素都要向后移动,给新的位置腾出空间,保证连续性。 47 | 48 | ![](https://s2.ax1x.com/2019/11/30/QZM4nx.png) 49 | 50 | 51 | 52 | ![](https://s2.ax1x.com/2019/11/30/QZtetx.png) 53 | 54 | - **尾部插入** 55 | 56 | 尾部插入这种情况比较简单,直接把元素放到数组尾部的空闲位置即可,等同于更新元素的操作。 57 | 58 | ![](https://s2.ax1x.com/2019/11/30/QZlULq.png) 59 | 60 | ![QZJyrj.png](https://s2.ax1x.com/2019/11/30/QZJyrj.png) 61 | 62 | ​ 63 | 64 | ### 3.2 删除元素 65 | 66 | 删除操作和插入操作的过程正好相反,如果删除的元素在数组的中间,那么其后的元素都要向前移动。 67 | 68 | ![QZNRII.png](https://s2.ax1x.com/2019/11/30/QZNRII.png) 69 | 70 |  ![QZNqds.png](https://s2.ax1x.com/2019/11/30/QZNqds.png) 71 | 72 | ### 3.3 更新元素 73 | 74 | ​ ![carbon (1).png](https://i.loli.net/2019/11/30/kbqy2ualjm4ZSRr.png) 75 | 76 | > 这里更新元素的时间复杂度为`O(1)`。 77 | 78 | ### 3.4 读取元素 79 | 80 | ![carbon.png](https://i.loli.net/2019/11/30/3qtLAzoUmJfvyjZ.png) 81 | 82 | > 这里读元素的时间复杂度为`O(1)`。 83 | 84 | ## 4. 容器能够代替数组吗? 85 | 86 | 针对数组类型,很多语言都提供了容器类,例如Java的List,如果你是一个Java程序员,那么你应该清楚ArrayList,对它应该非常的熟悉,和数组对比它有哪些优势呢?为什么开发的过程中经常使用它,最大的优势就是封装了对数组的操作,例如前面说的插入和删除,如果使用ArrayList还有一个优势是它支持动态扩容,当容器不够大的时候会自动扩容1.5倍,我们完全不需要关心底层的实现逻辑。那么什么时候使用数组更合适呢?有一下几点: 87 | 88 | - ArrayList无法存储基本数据类型,例如int、long需要封装为Integer、Long。这里的装箱、拆箱操作有一定的性能损耗,如果特别关注性能,希望使用基本类型,那么就可以选择数组。 89 | - 对数据只是简单的存储操作,那么选择数组效率更好些。 90 | - 当做一些底层开发的时候数组可能用的比较多,比如一些框架。都是比较要求性能的。 91 | 92 | ## 5. 参考 93 | 94 | 《漫画算法》 95 | 96 | 《数据结构与算法之美》 -------------------------------------------------------------------------------- /docs/notes/data-structures-and-algorithms/【手撕数据结构与算法】-链表.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | > 底层基础决定上层发展。 4 | > 5 | > 点个赞在看,让我知道你在关注技术。 6 | > 7 | > 本系列文章Github [后端进阶指南 ](https://github.com/CodeGeekLee/data-structures-and-algorithms) 已收录,此项目正在完善中,欢迎star。 8 | 9 | **本系列内容预览:** 10 | 11 | ![](https://user-gold-cdn.xitu.io/2019/12/9/16eeb2515423ba3e?w=1664&h=1276&f=png&s=411513) 12 | 13 | ## 1. 什么是链表? 14 | 15 | > 链表也是`线性表`中的一种,数组是线性表中的`顺序结构`,而这次说的链表是线性表的`链式存储结构`,它在内存中是非连续、非顺序性的数据结构,由若干个节点组成。它每个节点中都会存储数据和下一个节点的地址,存储数据的叫做数据域,存储地址的叫做指针域。指针分为前驱指针、后继指针,分别用来记录前一个节点和后一个节点的位置。 16 | > 17 | > 指针:将某个变量赋值给指针,实际上就是将变量的地址值赋值给指针,可以看做Java当中的引用。 18 | 19 | ### 1.1 单向链表 20 | 21 | ![](https://user-gold-cdn.xitu.io/2019/12/4/16ed0675e43fef78?w=1476&h=326&f=png&s=28582) 22 | 23 | 单向链表,顾名思义就是只有一个方向的链表,从上图中来看,一个单向链表由若干个节点组成,每个节点又分为两个部分,一部分存放数据,一部分存放下一个节点的位置。用图来说话就是橘色的方块叫做`数据域`,里面用来存放数据data。而黄色的方块叫做`指针域`,用来存放下一个节点的位置next(`注意是下一个节点的位置,不是下一个指针的位置`),这个next又被称为后继指针。 24 | 25 | 大家观察上面的图,有两个地方比较特殊,那就是第一个节点和最后一个节点。我们还可以称作`头结点`、`尾结点`。这两个节点有什么特别之处呢?那就是头结点可以不存数据,作为链表的开始,而尾结点的后继指针指向null,代表这是链表的最后一个节点。 26 | 27 | > 头节点:链表中第一个节点,头节点中可以不存储数据。 28 | > 29 | > 头指针:链表中第一个节点的指针,用来存储链表的基地址,是整个链表的开始。 30 | > 31 | > 尾节点:链表中最后一个节点,指向一个空null,代表这是链表的最后一个节点。 32 | 33 | ### 1.2 单向循环链表 34 | 35 | ![](https://user-gold-cdn.xitu.io/2019/12/4/16ed0689d59690cd?w=1588&h=444&f=png&s=42339) 36 | 37 | 单向循环链表是从单向链表衍生出来的,它和单向链表的唯一区别就是单向链表的尾结点的后继指针指向了null,而单向循环链表的尾结点后继指针指向了头节点。这种首尾相连的单链表称`单向循环链表`。循环链表的优点就是从链尾到链头比较方便,处理环形结构问题时比较适用,比如著名的[约瑟夫问题]([https://baike.baidu.com/item/%E7%BA%A6%E7%91%9F%E5%A4%AB%E9%97%AE%E9%A2%98](https://baike.baidu.com/item/约瑟夫问题))。 38 | 39 | ### 1.3 双向链表 40 | 41 | ![](https://user-gold-cdn.xitu.io/2019/12/4/16ed06bb63cdf646?w=1502&h=342&f=png&s=31917) 42 | 43 | 双向链表稍微复杂一些,它和单向链表相比除了后继指针以外还多了个前驱指针。如果存储同样多的数据,双向链表比单向链表占用更多的空间,虽然双向链表中的两个指针很浪费空间,但可以支持双向遍历,也给链表本身带来了更多的灵活性。 44 | 45 | ### 1.4 双向循环链表 46 | 47 | ![](https://user-gold-cdn.xitu.io/2019/12/4/16ed0b8f695d0ba7?w=1580&h=571&f=png&s=60582) 48 | 49 | 了解了循环链表和双向链表之后,把这两个组合在一起就是双向循环链表,大家看图就知道了,头节点的前驱指针指向尾节点,尾节点的后继指针指向头节点。这里就不做过多介绍了,大家知道链表都有哪几种就可以了。 50 | 51 | ## 2. 链表VS数组 52 | 53 | 说了半天链表,不知道大家了解了没有,我自己都感觉很枯燥。可是基础就是这样,只有学好了基础才能更好的向上爬。现在我们来看下链表和我们之前讲的数组有什么区别。首先它们两个的存储结构就不一样,数组是顺序存储结构,也就是说它在内存中是一块连续的存储空间,而链表是链式存储结构,也就是非连续的。我们来看下它们在内存中表现: 54 | 55 | ![](https://user-gold-cdn.xitu.io/2019/12/5/16ed67dc1d83ecd9?w=1192&h=462&f=png&s=44534) 56 | 57 | 通过图片,相信大家已经看出来区别了,由于数组是连续的存储结构,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。由于数据结构的不同,导致数组和链表的插入、删除、随机访问等操作的时间复杂度正好相反。 58 | 59 | | | 数组 | 链表 | 60 | | :--------- | ------ | ------ | 61 | | 插入、删除 | `O(n)` | `O(1)` | 62 | | 随机访问 | `O(1)` | `O(n)` | 63 | 64 | 链表天然的支持动态扩容,因为它不是预先生成内存空间,只有真正使用的时候才会去开辟一块内存空间。而数组就不行,数组的缺点就是大小固定,申请多了浪费,申请少了还得频繁的扩容、搬移数组,如果数据量大了很耗时。所以大家在使用List的时候也是,如果能够事先预估数据量的大小,那么在初始化的时候最好指定下大小,避免扩容时搬移数据带来影响。 65 | 66 | ## 3. 链表的基本操作 67 | 68 | ### 3.1 链表的增加 69 | 70 | 链表的增加操作,一共有三种情况: 71 | 72 | - 增加到头部 73 | 74 | ![](https://user-gold-cdn.xitu.io/2019/12/8/16ee62db258947f7?w=1257&h=341&f=png&s=20299) 75 | 76 | 增加到头部一共分为两步,第一步将新节点的后继指针指向原头节点,第二步是将新节点变为头节点。这样就完成了头部添加。 77 | 78 | - 增加到中间 79 | 80 | ![](https://user-gold-cdn.xitu.io/2019/12/8/16ee6366a77e1650?w=1186&h=451&f=png&s=33518) 81 | 82 | 中间插入也分为两步,第一步将插入位置的前边节点的后继指针指向新节点,第二步是将新节点后继指针指向插入位置的原节点。 83 | 84 | - 增加到尾部 85 | 86 | ![](https://user-gold-cdn.xitu.io/2019/12/8/16ee63b1414962b4?w=1476&h=421&f=png&s=27386) 87 | 88 | 链表的尾部插入最简单,只需要将最后一个节点的后继指针指向新节点就可以了。 89 | 90 | > 我们来看下代码,如果大家时间充沛,建议自己手动敲一遍,这样会理解的更深刻。 91 | 92 | ```java 93 | /** 94 | * @author: lixiaoshuang 95 | * @create: 2019-12-08 23:11 96 | **/ 97 | public class LinkedListAddDemo { 98 | 99 | //头节点 100 | private static Node headNode = null; 101 | //尾节点 102 | private static Node lastNode = null; 103 | //链表的长度 104 | private static int size; 105 | 106 | public static void main(String[] args) { 107 | //初始化一个链表 108 | addByIndex(1, 0); 109 | addByIndex(2, 1); 110 | addByIndex(3, 2); 111 | addByIndex(4, 3); 112 | 113 | //头部插入 114 | addByIndex(5, 0); 115 | printNode(); //输出 5、1、2、3、4 116 | //中间插入 117 | addByIndex(5, 2); 118 | printNode(); //输出 1、2、5、3、4 119 | //尾部插入 120 | addByIndex(5, 4); 121 | printNode(); //输出 1、2、3、4、5 122 | } 123 | 124 | private static void addByIndex(int data, int index) { 125 | if (index < 0 || index > size) { 126 | throw new IndexOutOfBoundsException("超出链表节点范围"); 127 | } 128 | Node node = new Node(data); 129 | if (index == 0) { 130 | //插入到头部 131 | node.setNext(headNode); 132 | headNode = node; 133 | if (size == 0) { 134 | lastNode = node; 135 | } 136 | } else if (index == size) { 137 | //插入到尾部 138 | lastNode.setNext(node); 139 | lastNode = node; 140 | } else { 141 | //插入到中间 142 | Node prevNode = get(index - 1); 143 | Node nextNode = prevNode.getNext(); 144 | prevNode.setNext(node); 145 | node.setNext(nextNode); 146 | } 147 | size++; 148 | } 149 | 150 | 151 | /** 152 | * 通过位置查找链表节点 153 | * 154 | * @param index 155 | * @return 156 | */ 157 | private static Node get(int index) { 158 | if (index < 0 || index > size) { 159 | throw new IndexOutOfBoundsException("超出链表节点范围"); 160 | } 161 | Node temp = headNode; 162 | for (int i = 0; i < index; i++) { 163 | temp = temp.getNext(); 164 | } 165 | return temp; 166 | } 167 | 168 | /** 169 | * 打印节点 170 | */ 171 | private static void printNode() { 172 | while (headNode != null) { 173 | System.out.println(headNode.getDate()); 174 | headNode = headNode.getNext(); 175 | } 176 | } 177 | 178 | } 179 | 180 | 181 | /** 182 | * 定义一个节点 183 | * 184 | * @param 185 | */ 186 | class Node { 187 | /** 188 | * 节点中的数据 189 | */ 190 | private T date; 191 | /** 192 | * 下一个节点的指针 193 | */ 194 | private Node next; 195 | 196 | public Node(T date) { 197 | this.date = date; 198 | } 199 | 200 | public Node getNext() { 201 | return next; 202 | } 203 | 204 | public void setNext(Node next) { 205 | this.next = next; 206 | } 207 | 208 | public T getDate() { 209 | return date; 210 | } 211 | 212 | public void setDate(T date) { 213 | this.date = date; 214 | } 215 | } 216 | ``` 217 | 218 | 219 | 220 | ### 3.2 链表的删除 221 | 222 | 链表删除和增加一样,也有三种情况: 223 | 224 | - 删除头部 225 | 226 | ![](https://user-gold-cdn.xitu.io/2019/12/9/16ee90f71abf0618?w=1086&h=136&f=png&s=15566) 227 | 228 | 删除头部操作只需要将头部节点设置为当前头部节点的下一个节点就可以了。 229 | 230 | - 删除中间 231 | 232 | ![](https://user-gold-cdn.xitu.io/2019/12/9/16ee91339550fa83?w=1186&h=266&f=png&s=19394) 233 | 234 | 删除中间操作只需要将被删除节点的前一个节点的后继指针指向被删除节点的下一个节点就可以了。 235 | 236 | - 删除尾部 237 | 238 | ![](https://user-gold-cdn.xitu.io/2019/12/9/16ee9104dfa40021?w=1186&h=236&f=png&s=17709) 239 | 240 | 尾部删除只需要将倒数第二个节点的后继指针指向null就可以。 241 | 242 | ```java 243 | /** 244 | * @author: lixiaoshuang 245 | * @create: 2019-12-08 23:11 246 | **/ 247 | public class LinkedListAddDemo { 248 | 249 | //头节点 250 | private static Node headNode = null; 251 | //尾节点 252 | private static Node lastNode = null; 253 | //链表的长度 254 | private static int size; 255 | 256 | public static void main(String[] args) { 257 | //初始化一个链表 258 | addByIndex(1, 0); 259 | addByIndex(2, 1); 260 | addByIndex(3, 2); 261 | addByIndex(4, 3); 262 | 263 | //头部删除 264 | delete(0); 265 | printNode(); //输出 2、3、4 266 | //尾部删除 267 | delete(3); 268 | printNode(); //输出 1、2、3 269 | //中间删除 270 | delete(2); 271 | printNode(); //输出 1、2、4 272 | } 273 | 274 | /** 275 | * 链表删除操作 276 | * 277 | * @param index 278 | */ 279 | private static void delete(int index) { 280 | if (index < 0 || index >= size) { 281 | throw new IndexOutOfBoundsException("超出链表节点范围"); 282 | } 283 | if (index == 0) { 284 | //删除头部 285 | headNode = headNode.getNext(); 286 | } else if (index == size - 1) { 287 | //删除尾部 288 | Node prevNode = get(index - 1); 289 | prevNode.setNext(null); 290 | lastNode = prevNode; 291 | } else { 292 | //删除中间 293 | Node prevNode = get(index - 1); 294 | Node nextNode = prevNode.getNext().getNext(); 295 | prevNode.setNext(nextNode); 296 | } 297 | size--; 298 | } 299 | 300 | /** 301 | * 通过位置查找链表节点 302 | * 303 | * @param index 304 | * @return 305 | */ 306 | private static Node get(int index) { 307 | if (index < 0 || index > size) { 308 | throw new IndexOutOfBoundsException("超出链表节点范围"); 309 | } 310 | Node temp = headNode; 311 | for (int i = 0; i < index; i++) { 312 | temp = temp.getNext(); 313 | } 314 | return temp; 315 | } 316 | 317 | /** 318 | * 打印节点 319 | */ 320 | private static void printNode() { 321 | while (headNode != null) { 322 | System.out.println(headNode.getDate()); 323 | headNode = headNode.getNext(); 324 | } 325 | } 326 | 327 | } 328 | ``` 329 | 330 | 331 | 332 | ### 3.3 链表的修改 333 | 334 | 修改链表节点就直接将要修改的节点替换为新节点,第一步先将被修改的节点的前一个节点的后继指针指向新节点,然后将新节点的后继指针指向被修改节点的下一个节点,这里讲的是如何修改一个节点,逻辑和上边的增加删除差不多,这里就举一个中间修改的图例吧。如果想不替换节点修改节点中的数据,这个比较简单,大家可以自己实现下。 335 | 336 | ![](https://user-gold-cdn.xitu.io/2019/12/9/16ee928290c8acee?w=1186&h=401&f=png&s=27659) 337 | 338 | ```java 339 | /** 340 | * @author: lixiaoshuang 341 | * @create: 2019-12-08 23:11 342 | **/ 343 | public class LinkedListOperationDemo { 344 | 345 | //头节点 346 | private static Node headNode = null; 347 | //尾节点 348 | private static Node lastNode = null; 349 | //链表的长度 350 | private static int size; 351 | 352 | public static void main(String[] args) { 353 | //初始化一个链表 354 | addByIndex(1, 0); 355 | addByIndex(2, 1); 356 | addByIndex(3, 2); 357 | addByIndex(4, 3); 358 | 359 | //修改头部 360 | update(5, 0); 361 | printNode(); //输出 5、2、3、4 362 | //修改尾部 363 | update(5, 3); 364 | printNode(); //输出 1、2、3、5 365 | //修改中间 366 | update(5, 1); 367 | printNode(); //输出 1、5、3、4 368 | } 369 | 370 | /** 371 | * 链表的修改 372 | * 373 | * @param data 374 | * @param index 375 | */ 376 | private static void update(int data, int index) { 377 | if (index < 0 || index >= size) { 378 | throw new IndexOutOfBoundsException("超出链表节点范围"); 379 | } 380 | Node newNode = new Node(data); 381 | if (index == 0) { 382 | //修改头部 383 | Node next = headNode.getNext(); 384 | newNode.setNext(next); 385 | headNode = newNode; 386 | } else if (index == size) { 387 | //修改尾部 388 | Node prevNode = get(index - 1); 389 | prevNode.setNext(newNode); 390 | lastNode = newNode; 391 | } else { 392 | //修改中间 393 | Node prevNode = get(index - 1); 394 | Node nextNode = prevNode.getNext().getNext(); 395 | prevNode.setNext(newNode); 396 | newNode.setNext(nextNode); 397 | } 398 | } 399 | 400 | /** 401 | * 通过位置查找链表节点 402 | * 403 | * @param index 404 | * @return 405 | */ 406 | private static Node get(int index) { 407 | if (index < 0 || index > size) { 408 | throw new IndexOutOfBoundsException("超出链表节点范围"); 409 | } 410 | Node temp = headNode; 411 | for (int i = 0; i < index; i++) { 412 | temp = temp.getNext(); 413 | } 414 | return temp; 415 | } 416 | 417 | /** 418 | * 打印节点 419 | */ 420 | private static void printNode() { 421 | while (headNode != null) { 422 | System.out.println(headNode.getDate()); 423 | headNode = headNode.getNext(); 424 | } 425 | } 426 | 427 | } 428 | ``` 429 | 430 | 431 | 432 | ### 3.4 链表的查询 433 | 434 | 说道查询,不知道大家发现没有,上边的代码中已经有过实现了😭,这里在贴过来,大家看下: 435 | 436 | ```java 437 | /** 438 | * 通过位置查找链表节点 439 | * 440 | * @param index 441 | * @return 442 | */ 443 | private static Node get(int index) { 444 | if (index < 0 || index > size) { 445 | throw new IndexOutOfBoundsException("超出链表节点范围"); 446 | } 447 | Node temp = headNode; 448 | for (int i = 0; i < index; i++) { 449 | temp = temp.getNext(); 450 | } 451 | return temp; 452 | } 453 | ``` 454 | 455 | 456 | 457 | ## 4. 面试题 458 | 459 | ### 4.1 怎么判断一个单向链表是否有环? 460 | 461 | 我们这里采用`快慢指针`的方法来判断一个链表是否有环,首先创建两个指针同时指向链表的头节点,慢指针走一步,快指针走两步,判断两个指针是否相等,如果相等,则说明有环。这里就是我们上边介绍的单向环形链表了。只要这个链表有环,那么利用快慢指针遍历它,两个指针肯定会有相等的时候(这里的指针是指节点)。这就好比周末公园里跑步一样,年轻人跑的快一些,老年人跑的慢一些,年轻人就会在某一个地方追赶上老人并超越他,原因很简单,因为公园的跑道是环形,哈哈。 462 | 463 | 大家看下面的代码我把链表插入操作中的尾部插入做了一个小修改,就是让每一个尾部节点的后继都指向头节点,这样就把单向链表变成了单向循环链表。 464 | 465 | ```java 466 | /** 467 | * @author: lixiaoshuang 468 | * @create: 2019-12-08 23:11 469 | **/ 470 | public class LinkedListOperationDemo { 471 | 472 | //头节点 473 | private static Node headNode = null; 474 | //尾节点 475 | private static Node lastNode = null; 476 | //链表的长度 477 | private static int size; 478 | 479 | public static void main(String[] args) { 480 | //初始化一个链表 481 | addByIndex(1, 0); 482 | addByIndex(2, 1); 483 | addByIndex(3, 2); 484 | addByIndex(4, 3); 485 | 486 | //判断链表是否有环 487 | boolean ringed = isRinged(); 488 | System.out.println(ringed); //输出为true 489 | } 490 | 491 | /** 492 | * 判断链表是否有环 493 | * 494 | * @return 495 | */ 496 | private static boolean isRinged() { 497 | if (headNode == null) { 498 | return false; 499 | } 500 | //定义快慢指针 501 | Node slowPointer, fastPointer; 502 | slowPointer = fastPointer = headNode; 503 | while (slowPointer != null && fastPointer != null) { 504 | slowPointer = slowPointer.getNext(); 505 | fastPointer = fastPointer.getNext().getNext(); 506 | 507 | //如果两个指针有相等则有环 508 | if (slowPointer == fastPointer) { 509 | return true; 510 | } 511 | } 512 | return false; 513 | } 514 | 515 | /** 516 | * 链表插入操作 517 | * 518 | * @param data 519 | * @param index 520 | */ 521 | private static void addByIndex(int data, int index) { 522 | if (index < 0 || index > size) { 523 | throw new IndexOutOfBoundsException("超出链表节点范围"); 524 | } 525 | Node node = new Node(data); 526 | if (index == 0) { 527 | //插入到头部 528 | node.setNext(headNode); 529 | headNode = node; 530 | if (size == 0) { 531 | lastNode = node; 532 | } 533 | } else if (index == size) { 534 | //插入到尾部 535 | lastNode.setNext(node); 536 | lastNode = node; 537 | node.setNext(headNode); //这里尾部插入将最后一个节点的后继指向头节点,这样链表就是循环链表了。 538 | } else { 539 | //插入到中间 540 | Node prevNode = get(index - 1); 541 | Node nextNode = prevNode.getNext(); 542 | prevNode.setNext(node); 543 | node.setNext(nextNode); 544 | } 545 | size++; 546 | } 547 | } 548 | ``` 549 | 550 | ### 4.2 如何实现链表的反转? 551 | 552 | > 这个也是一个高频面试题,这里我就不写实现方式了,当给大家留个思考题,有时间的朋友可以尝试自己解一下,这道题虽然百度下解题方法就出来了,但还是希望大家先自己思考下,如果实在想不出来在查找一下解题方法。也可以去《[手撕数据结构与算法](https://github.com/CodeGeekLee/data-structures-and-algorithms)》中查看源码,我已经手敲了一遍。 553 | 554 | ## 5. 参考 555 | 556 | 《漫画算法》 557 | 558 | 《数据结构与算法之美》 559 | 560 | 《大话数据结构》 561 | 562 | ## 6.结尾 563 | 564 | 在我看来后端程序员应该学的有三大基础知识`"数据结构与算法"`、`"计算机系统"`、`"操作系统Linux"`。在这个互联网寒冬时代,是不是我们的衣服穿得不够多?彻夜难眠的我(`纯属扯淡,哈哈`)决定带领大家一起学习三大基础知识,本次开篇系列是《手撕数据结构与算法》,每一个系列更完就会开启下一个系列,大家不要着急。可以关注我的公众号,持续追更、持续学习。 565 | 566 | > 本系列文章Github [后端进阶指南 ](https://github.com/CodeGeekLee/data-structures-and-algorithms) 已收录,此项目正在完善中,欢迎star。 567 | > 568 | > 公众号内文章都是博主原创,并且会一直更新。如果你想见证或和博主一起成长,欢迎关注! 569 | 570 | ![欢迎扫码关注哦!!!](https://user-gold-cdn.xitu.io/2019/12/9/16eeb502a1f2d09c?w=300&h=300&f=png&s=11666) 571 | 572 | 最后 最后,能够看到这里的朋友,都是有着乐于学习的精神,谢谢你们能够看完我写的文章,博主不太会总结,有什么问题欢迎留言指正,如果感觉读了以后有点收获 **点赞、点关注** 。写作路上需要各位老铁的支持,你们的支持就是下一篇得动力。 -------------------------------------------------------------------------------- /docs/notes/distributed/了解分布式架构.md: -------------------------------------------------------------------------------- 1 | # 1. 分布式架构解决什么问题 2 | 3 | 主要是两个: 4 | 5 | - 大流量的处理 6 | 7 | 通过集群技术奖大规模并发请求负载均衡到不同的机器上。 8 | 9 | - 关键业务的保护 10 | 11 | 提高后台服务的可用性,把故障隔离起来,阻止多米诺骨牌效应,如果流量过大,需要对业务降级。已保证关键业务的流转。 12 | 13 | 说白了就是干两件事、一是提高整体架构的吞吐量,二是提高系统的稳定性,让系统的可用性更高。 14 | 15 | # 2. 如何提高架构性能 16 | 17 | - 缓存系统 18 | - 异步调用 19 | - 负载均衡 20 | - 数据分区 21 | - 数据镜像 22 | 23 | # 3. 如何提高架构稳定性 24 | 25 | - 服务拆分 26 | - 服务冗余 27 | - 限流降级 28 | - 高可用架构 29 | - 高可用运维 30 | 31 | # 4. 分布式系统的核心 32 | 33 | ![img](https://static001.geekbang.org/resource/image/89/f2/8958a432f32dd742b6503b60f97cc3f2.png) 34 | 35 | # 5. 全栈监控 36 | 37 | ![img](https://static001.geekbang.org/resource/image/cf/66/cf6fe8ee30a3ac3b693d1188b46e4e66.png) 38 | 39 | - 基础层:监控主机和底层资源。比如:CPU、内存、网络吞吐、硬盘I/O、硬盘使用等。 40 | - 中间层:就是中间件层的监控。比如:Nginx、Reids、ActiveMQ、Kafka、MySQL、Tomcat等。 41 | - 应用层:监控应用层的使用。比如:HTTP访问吞吐量、响应时间、返回码、调用链路分析、性能瓶颈、还包括用户端的监控。 42 | 43 | # 6. 服务治理 44 | 45 | - 梳理服务之间的依赖关系 (zipkin) 46 | - 服务状态和服务声明周期管理 (服务发现) 47 | - 整体架构版本管理 (类似于Springboot和Spring clound之间的版本对应) 48 | - 资源/服务调度 49 | - 服务状态的维持和拟合(一种不预期的变化会维持服务状态,例如服务挂掉。预期的变化会拟合服务状态、例如服务启动) 50 | - 服务的弹性伸缩和故障迁移 (docker、kubernetes) 51 | - 服务工作流和编排 52 | 53 | # 7. 总结 54 | 55 | ## 7.1 构建分布式系统面临的问题 56 | 57 | - 分布式系统的硬件故障发生率搞。故障发生是常态,需要尽可能地将运维流程自动化。 58 | - 需要良好的设计服务,避免某服务的单点故障对依赖它的其他服务造成大面积影响。 59 | - 为了容量的可伸缩性,服务的拆分、自治和无状态变得更加重要,可能需要对老的软件逻辑做大的修改。 60 | - 老的服务可能是异构的,此时需要让他们使用标准的协议,以便可以被调度、编排、且互相之间可以通信。 61 | - 服务软件故障的处理也变得复杂,需要优化的流程,以加快故障的恢复。 62 | - 为了管理各个服务的容量,让分布式系统发挥出最佳性能,需要有流量调度技术。 63 | - 分布式存储会让事务处理变得复杂;在事务遇到故障无法被自动恢复的情况下,手动恢复流程也会变得复杂。 64 | - 测试和查错的复杂度增大。 65 | - 系统的吞吐量会变大,但响应时间会变长。 66 | 67 | ## 7.2 了解一些解决方案 68 | 69 | - 需要有完善的监控系统,以便对服务运行状态有全面的了解。 70 | - 设计服务时要分析其依赖链;当非关键服务故障时,其他服务要自动降级功能,避免调用该服务。 71 | - 重构老的软件,使其能被服务化;可以参考 SOA 和微服务的设计方式,目标是微服务化;使用 Docker 和 Kubernetes 来调度服务。 72 | - 为老的服务编写接口逻辑来使用标准协议,或在必要时重构老的服务以使得它们有这些功能。 73 | - 自动构建服务的依赖地图,并引入好的处理流程,让团队能以最快速度定位和恢复故障,详见《故障处理最佳实践:应对故障》一文。 74 | - 使用一个 API Gateway,它具备服务流向控制、流量控制和管理的功能。 75 | - 事务处理建议在存储层实现;根据业务需求,或者降级使用更简单、吞吐量更大的最终一致性方案,或者通过二阶段提交、Paxos、Raft、NWR 等方案之一,使用吞吐量小的强一致性方案。 76 | - 通过更真实地模拟生产环境,乃至在生产环境中做灰度发布,从而增加测试强度;同时做充分的单元测试和集成测试以发现和消除缺陷;最后,在服务故障发生时,相关的多个团队同时上线自查服务状态,以最快地定位故障原因。 77 | - 通过异步调用来减少对短响应时间的依赖;对关键服务提供专属硬件资源,并优化软件逻辑以缩短响应时间。 -------------------------------------------------------------------------------- /docs/notes/docker/Docker、K8s简介.md: -------------------------------------------------------------------------------- 1 | # Docker 2 | 3 | ## **Docker原理** 4 | 5 | ![https://img.draveness.me/2017-11-30-docker-core-techs.png](https://img.draveness.me/2017-11-30-docker-core-techs.png) 6 | 7 | **Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。**它是目前最流行的 Linux 容器解决方案。 8 | 9 | Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。 10 | 11 | **Namespace机制:** 12 | 13 | 容器内看不到宿主机的其他进程,通过Linux的Namespace机制实现环境隔离。过 Namespace 技术,我们实现了容器和容器间,容器与宿主机之间的隔离 14 | 15 | **容器的限制:Cgroups:** 16 | 17 | `Cgroups` 就是 Linux 内核中用来为进程设置资源的一个技术。 18 | 19 | Linux Cgroups 全称是 Linux Control Group,主要的作用就是限制进程组使用的资源上限,包括 CPU,内存,磁盘,网络带宽。还可以对进程进行优先级设置,审计,挂起和恢复等操作 20 | 21 | ## **Docker 的主要用途,目前有三大类** 22 | 23 | **(1)提供一次性的环境。**比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。 24 | 25 | **(2)提供弹性的云服务。**因为 Docker 容器可以随开随关,很适合动态扩容和缩容。 26 | 27 | **(3)组建微服务架构。**通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。 28 | 29 | ## **Docker安装** 30 | 31 | - yum install docker 32 | - docker -v 33 | - systemctl start docker 34 | 35 | ## **常用命令** 36 | 37 | - docker version 查看docker版本 38 | - docker image list 查看本地镜像列表 39 | - docker ps 查看运行中的镜像进程 40 | - docker search xx 查找镜像 41 | - docker pull xx 拉取镜像到本地 42 | - docker run xx 启动镜像 43 | - docker stop xx 关闭镜像 44 | - docker exec -it xx /bin/bash 进入容器命令 45 | 46 | ## **Dockerfile怎么编写** 47 | 48 | - 先创建一个Dockerfile文件,写入如下 49 | 50 | ![https://tva1.sinaimg.cn/large/0081Kckwly1gm02zfjkxyj30q608ygn1.jpg](https://tva1.sinaimg.cn/large/0081Kckwly1gm02zfjkxyj30q608ygn1.jpg) 51 | 52 | - `FROM` :需要构建镜像的项目所需要依赖的`基础镜像`,`SpringBoot`项目是跑在`JDK`之上的 53 | 54 | - `VOLUME` :定义匿名数据卷,容器在运行的时候,会将数据写入到这个数据卷中,这里设置为一个临时目录 55 | 56 | - `ADD` :将target目录下的springboot-docker-0.0.1-SNAPSHOT.jar`包添加到`docker容器中,并将名称进行修改为docker.jar 57 | 58 | - `RUN`:执行后其后面的命令 59 | 60 | - `ENTRYPOINT`;在容器启动之前的预定义执行脚本命令 61 | 62 | - `EXPOSE` : 启动的端口 63 | 64 | - docker build -t springboot-docker/springboot-docker:1.0 . 构建镜像 65 | 66 | ## **Docker 私有仓库搭建** 67 | 68 | 1. docker pull registry 69 | 70 | 2. docker run -d -p 5000:5000 --name=docker_registry --restart=always --privileged=true -v /usr/local/docker_registry:/var/lib/registry 71 | 72 | docker.io/registry 73 | 74 | - -p 5000:5000 端口 75 | - -d 在后台运行 76 | - --name=jackspeedregistry 运行的容器名称 77 | - --restart=always 自动重启 78 | - --privileged=true centos7中的安全模块selinux把权限禁止了,加上这行是给容器增加执行权限 79 | - v /usr/local/docker_registry:/usr/local/docker_registry 把主机的/usr/local/docker_registry 目录挂载到registry容器的/usr/local/docker_registry目录下,假如有删除容器操作,我们的镜像也不会被删除 [docker.io/registry](http://docker.io/registry) 镜像名称查看启动的容器 80 | 81 | # **Kubernetes 集群** 82 | 83 | ![https://d33wubrfki0l68.cloudfront.net/99d9808dcbf2880a996ed50d308a186b5900cec9/40b94/docs/tutorials/kubernetes-basics/public/images/module_01_cluster.svg](https://d33wubrfki0l68.cloudfront.net/99d9808dcbf2880a996ed50d308a186b5900cec9/40b94/docs/tutorials/kubernetes-basics/public/images/module_01_cluster.svg) 84 | 85 | **Kubernetes 协调一个高可用计算机集群,每个计算机作为独立单元互相连接工作。** **Kubernetes 以更高效的方式跨集群自动分发和调度应用容器。** Kubernetes 是一个开源平台,并且可应用于生产环境。 86 | 87 | 一个 Kubernetes 集群包含两种类型的资源: 88 | 89 | - **Master** 调度整个集群 90 | - **Nodes** 负责运行应用 91 | 92 | **Master 负责管理整个集群。** Master 协调集群中的所有活动,例如调度应用、维护应用的所需状态、应用扩容以及推出新的更新。 93 | 94 | **Node 是一个虚拟机或者物理机,它在 Kubernetes 集群中充当工作机器的角色** 每个Node都有 Kubelet , 它管理 Node 而且是 Node 与 Master 通信的代理。 Node 还应该具有用于处理容器操作的工具,例如 Docker 或 rkt 。处理生产级流量的 Kubernetes 集群至少应具有三个 Node 。 95 | 96 | ![https://www.redhat.com/cms/managed-files/kubernetes-diagram-2-824x437.png](https://www.redhat.com/cms/managed-files/kubernetes-diagram-2-824x437.png) -------------------------------------------------------------------------------- /docs/notes/java/JUnit4教程+实践.md: -------------------------------------------------------------------------------- 1 | ##### 一、什么是JUnit? 2 | 3 | JUnit是Java编程语言的单元测试框架,用于编写和可重复运行的自动化测试。 4 | 5 | 6 | ##### 二、JUnit特点: 7 | 8 | - JUnit 是一个开放的资源框架,用于编写和运行测试。 9 | - 提供注解来识别测试方法。 10 | - 提供断言来测试预期结果。 11 | - JUnit 测试允许你编写代码更快,并能提高质量。 12 | - JUnit 优雅简洁。没那么复杂,花费时间较少。 13 | - JUnit测试可以自动运行并且检查自身结果并提供即时反馈。所以也没有必要人工梳理测试结果的报告。 14 | - JUnit测试可以被组织为测试套件,包含测试用例,甚至其他的测试套件。 15 | - JUnit在一个条中显示进度。如果运行良好则是绿色;如果运行失败,则变成红色。 16 | 17 | ##### 三、JUnit注解 18 | 19 | | 注解 | 描述 | 20 | | ------------- | ------------------------------------------------------------ | 21 | | @Test | 测试注解,标记一个方法可以作为一个测试用例 。 | 22 | | @Before | Before注解表示,该方法必须在类中的每个测试之前执行,以便执行某些必要的先决条件。 | 23 | | @BeforeClass | BeforeClass注解指出这是附着在静态方法必须执行一次并在类的所有测试之前,这种情况一般用于测试计算、共享配制方法(如数据库连接)。 | 24 | | @After | After注释表示,该方法在每项测试后执行(如执行每一个测试后重置某些变量,删除临时变量等)。 | 25 | | @AfterClass | 当需要执行所有测试在JUnit测试用例类后执行,AlterClass注解可以使用以清理一些资源(如数据库连接),注意:方法必须为静态方法。 | 26 | | @Ignore | 当想暂时禁用特定的测试执行可以使用这个注解,每个被注解为@Ignore的方法将不再执行 | 27 | | @Runwith | @Runwith就是放在测试类名之前,用来确定这个类怎么运行的。也可以不标注,会使用默认运行器。 | 28 | | @Parameters | 用于使用参数化功能。 | 29 | | @SuiteClasses | 用于套件测试 | 30 | 31 | ##### 四、JUnit断言 32 | 33 | | 断言 | 描述 | 34 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 35 | | void assertEquals([String message],expected value,actual value) | 断言两个值相等。值类型可能是int,short,long,byte,char,Object,第一个参数是一个可选字符串消息 | 36 | | void assertTrue([String message],boolean condition) | 断言一个条件为真 | 37 | | void assertFalse([String message],boolean condition) | 断言一个条件为假 | 38 | | void assertNotNull([String message],java.lang.Object object) | 断言一个对象不为空(null) | 39 | | void assertNull([String message],java.lang.Object object) | 断言一个对象为空(null) | 40 | | void assertSame([String message],java.lang.Object expected,java.lang.Object actual) | 断言两个对象引用相同的对象 | 41 | | void assertNotSame([String message],java.lang.Object unexpected,java.lang.Object actual) | 断言两个对象不是引用同一个对象 | 42 | | void assertArrayEquals([String message],expectedArray,resultArray) | 断言预期数组和结果数组相等,数组类型可能是int,short,long,byte,char,Object | 43 | 44 | 让我们看下使用断言的例子。 45 | AssertionTest.java 46 | 47 | ```java 48 | public class AssertionTest { 49 | 50 | @Test 51 | public void test() { 52 | String obj1 = "junit"; 53 | String obj2 = "junit"; 54 | String obj3 = "test"; 55 | String obj4 = "test"; 56 | String obj5 = null; 57 | 58 | int var1 = 1; 59 | int var2 = 2; 60 | 61 | int[] array1 = {1, 2, 3}; 62 | int[] array2 = {1, 2, 3}; 63 | 64 | Assert.assertEquals(obj1, obj2); 65 | 66 | Assert.assertSame(obj3, obj4); 67 | Assert.assertNotSame(obj2, obj4); 68 | 69 | Assert.assertNotNull(obj1); 70 | Assert.assertNull(obj5); 71 | 72 | Assert.assertTrue(var1 < var2); 73 | Assert.assertFalse(var1 > var2); 74 | 75 | Assert.assertArrayEquals(array1, array2); 76 | 77 | } 78 | } 79 | ``` 80 | 81 | 在以上类中我们可以看到,这些断言方法是可以工作的。 82 | 83 | - assertEquals() 如果比较的两个对象是相等的,此方法将正常返回;否则失败显示在JUnit的窗口测试将中止。 84 | - assertSame() 和 assertNotSame() 方法测试两个对象引用指向完全相同的对象。 85 | - assertNull() 和 assertNotNull() 方法测试一个变量是否为空或不为空(null)。 86 | - assertTrue() 和 assertFalse() 方法测试if条件或变量是true还是false。 87 | - assertArrayEquals() 将比较两个数组,如果它们相等,则该方法将继续进行不会发出错误。否则失败将显示在JUnit窗口和中止测试。 88 | 89 | ##### 五、JUnit执行过程 90 | 91 | JuntiTest.java 92 | 93 | ```java 94 | public class JunitTest { 95 | 96 | @BeforeClass 97 | public static void beforeClass() { 98 | System.out.println("in before class"); 99 | } 100 | 101 | @AfterClass 102 | public static void afterClass() { 103 | System.out.println("in after class"); 104 | } 105 | 106 | @Before 107 | public void before() { 108 | System.out.println("in before"); 109 | } 110 | 111 | @After 112 | public void after() { 113 | System.out.println("in after"); 114 | } 115 | 116 | @Test 117 | public void testCase1() { 118 | System.out.println("in test case 1"); 119 | } 120 | 121 | @Test 122 | public void testCase2() { 123 | System.out.println("in test case 2"); 124 | } 125 | 126 | } 127 | ``` 128 | 129 | 通过idea执行整个测试类后,执行结果: 130 | 131 | ```java 132 | in before class 133 | in before 134 | in test case 1 135 | in after 136 | in before 137 | in test case 2 138 | in after 139 | in after class 140 | ``` 141 | 142 | ##### 六、忽略测试 143 | 144 | - 一个带有@Ignore注解的测试方法不会被执行 145 | - 如果一个测试类带有@Ignore注解,则它的测试方法将不会被执行 146 | 147 | 我们把刚才测试类中的testCase2()方法标记为@Ignore, 148 | 149 | ```java 150 | @Ignore 151 | @Test 152 | public void testCase2() { 153 | System.out.println("in test case 2"); 154 | } 155 | ``` 156 | 157 | 然后在执行测试类的时候就会忽视这个方法,结果为: 158 | 159 | ```java 160 | in before class 161 | in before 162 | in test case 1 163 | in after 164 | 165 | Test ignored. 166 | in after class 167 | ``` 168 | 169 | ##### 七、时间测试 170 | 171 | JUnit提供了一个暂停的方便选项,如果一个测试用例比起指定的毫秒数花费了更多的时间,那么JUnit将自动将它标记为失败,timeout参数和@Test注解一起使用,例如@Test(timeout=1000)。 172 | 继续使用刚才的例子,现在将testCase1的执行时间延长到5000毫秒,并加上时间参数,设置超时为1000毫秒,然后执行测试类 173 | 174 | ```java 175 | @Test(timeout = 1000) 176 | public void testCase1() throws InterruptedException { 177 | TimeUnit.SECONDS.sleep(5000); 178 | System.out.println("in test case 1"); 179 | } 180 | ``` 181 | 182 | testCase1被标记为失败,并且抛出异常,执行结果: 183 | 184 | ```java 185 | in before class 186 | in before 187 | in after 188 | 189 | org.junit.runners.model.TestTimedOutException: test timed out after 1000 milliseconds 190 | 191 | at java.lang.Thread.sleep(Native Method) 192 | at java.lang.Thread.sleep(Thread.java:340) 193 | at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) 194 | at com.lxs.JUnit.JunitTest.testCase1(JunitTest.java:35) 195 | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 196 | at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 197 | at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 198 | at java.lang.reflect.Method.invoke(Method.java:498) 199 | at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) 200 | at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) 201 | at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) 202 | at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) 203 | at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:298) 204 | at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:292) 205 | at java.util.concurrent.FutureTask.run(FutureTask.java:266) 206 | at java.lang.Thread.run(Thread.java:748) 207 | 208 | in before 209 | in test case 2 210 | in after 211 | in after class 212 | ``` 213 | 214 | ##### 八、异常测试 215 | 216 | Junit 用代码处理提供了一个追踪异常的选项。你可以测试代码是否它抛出了想要得到的异常。expected 参数和 @Test 注释一起使用。现在让我们看看 @Test(expected)。新建测试方法testCase3()。 217 | 218 | ```java 219 | @Test(expected = ArithmeticException.class) 220 | public void testCase3() { 221 | System.out.println("in test case 3"); 222 | int a = 0; 223 | int b = 1 / a; 224 | } 225 | ``` 226 | 227 | 单独执行testCase3()方法,由于得到了一个预期异常,所以测试通过,结果为 228 | 229 | ```java 230 | in before class 231 | in before 232 | in test case 3 233 | in after 234 | in after class 235 | ``` 236 | 237 | 如果没有得到预期异常: 238 | 239 | ```java 240 | in before class 241 | in before 242 | in test case 3 243 | in after 244 | 245 | java.lang.AssertionError: Expected exception: java.lang.ArithmeticException 246 | 247 | in after class 248 | ``` 249 | 250 | ##### 九、参数化测试 251 | 252 | Junit 4 引入了一个新的功能参数化测试。参数化测试允许开发人员使用不同的值反复运行同 一个测试。你将遵循 5 个步骤来创建参数化测试: 253 | 254 | -为准备使用参数化测试的测试类指定特殊的运行器 org.junit.runners.Parameterized。 255 | 256 | - 为测试类声明几个变量,分别用于存放期望值和测试所用数据。 257 | - 为测试类声明一个带有参数的公共构造函数,并在其中为第二个环节中声明的几个变量赋值。 258 | - 为测试类声明一个使用注解 org.junit.runners.Parameterized.Parameters 修饰的,返回值为 java.util.Collection 的公共静态方法,并在此方法中初始化所有需要测试的参数对。 259 | - 编写测试方法,使用定义的变量作为参数进行测试。 260 | 261 | 262 | 263 | ###### 什么是@RunWith? 264 | 265 | 首先要分清几个概念:测试方法、测试类、测试集、测试运行器。 266 | 267 | - 其中测试方法就是用@Test注解的一些函数。 268 | - 测试类是包含一个或多个测试方法的一个**Test.java文件, 269 | - 测试集是一个suite,可能包含多个测试类。 270 | - 测试运行器则决定了用什么方式偏好去运行这些测试集/类/方法。 271 | 272 | 而@Runwith就是放在测试类名之前,用来确定这个类怎么运行的。也可以不标注,会使用默认运行器。常见的运行器有: 273 | 274 | - @RunWith(Parameterized.class) 参数化运行器,配合@Parameters使用JUnit的参数化功能 275 | - @RunWith(Suite.class) 276 | @SuiteClasses({ATest.class,BTest.class,CTest.class}) 277 | 测试集运行器配合使用测试集功能 278 | - @RunWith(JUnit4.class), junit4的默认运行器 279 | - @RunWith(JUnit38ClassRunner.class),用于兼容junit3.8的运行器 280 | - 一些其它运行器具备更多功能。例如@RunWith(SpringJUnit4ClassRunner.class)集成了spring的一些功能 281 | 282 | 283 | PrimeNumberCheckerTest.java 284 | 285 | ```java 286 | /** 287 | * 步骤一: 指定定参数运行器 288 | */ 289 | @RunWith(Parameterized.class) 290 | public class PrimeNumberCheckerTest { 291 | 292 | /** 293 | * 步骤二:声明变量 294 | */ 295 | private Integer inputNumber; 296 | private Boolean expectedResult; 297 | private PrimeNumberChecker primeNumberChecker; 298 | 299 | /** 300 | * 步骤三:为测试类声明一个带有参数的公共构造函数,为变量赋值 301 | */ 302 | public PrimeNumberCheckerTest(Integer inputNumber, 303 | Boolean expectedResult) { 304 | this.inputNumber = inputNumber; 305 | this.expectedResult = expectedResult; 306 | } 307 | 308 | /** 309 | * 步骤四:为测试类声明一个使用注解 org.junit.runners.Parameterized.Parameters 修饰的,返回值为 310 | * java.util.Collection 的公共静态方法,并在此方法中初始化所有需要测试的参数对 311 | * 1)该方法必须由Parameters注解修饰 312 | 2)该方法必须为public static的 313 | 3)该方法必须返回Collection类型 314 | 4)该方法的名字不做要求 315 | 5)该方法没有参数 316 | */ 317 | @Parameterized.Parameters 318 | public static Collection primeNumbers() { 319 | return Arrays.asList(new Object[][]{ 320 | {2, true}, 321 | {6, false}, 322 | {19, true}, 323 | {22, false}, 324 | {23, true} 325 | }); 326 | } 327 | 328 | @Before 329 | public void initialize() { 330 | primeNumberChecker = new PrimeNumberChecker(); 331 | } 332 | 333 | /** 334 | * 步骤五:编写测试方法,使用自定义变量进行测试 335 | */ 336 | @Test 337 | public void testPrimeNumberChecker() { 338 | System.out.println("Parameterized Number is : " + inputNumber); 339 | Assert.assertEquals(expectedResult, 340 | primeNumberChecker.validate(inputNumber)); 341 | } 342 | } 343 | ``` 344 | 345 | PrimeNumberChecker.java 346 | 347 | ```java 348 | public class PrimeNumberChecker { 349 | 350 | public Boolean validate(final Integer parimeNumber) { 351 | for (int i = 2; i < (parimeNumber / 2); i++) { 352 | if (parimeNumber % i == 0) { 353 | return false; 354 | } 355 | } 356 | return true; 357 | } 358 | } 359 | ``` 360 | 361 | JUnit会按照设置的参数多次执行,执行结果: 362 | 363 | ```java 364 | Parameterized Number is : 2 365 | Parameterized Number is : 6 366 | Parameterized Number is : 19 367 | Parameterized Number is : 22 368 | Parameterized Number is : 23 369 | ``` 370 | 371 | ##### 十、套件测试 372 | 373 | “套件测试”是指捆绑了几个单元测试用例并运行起来。在JUnit中,@RunWith 和 @Suite 这两个注解是用来运行套件测试。先来创建几个测试类 374 | 375 | ```java 376 | public class JunitTest1 { 377 | 378 | @Test 379 | public void printMessage(){ 380 | System.out.println("in JunitTest1"); 381 | } 382 | } 383 | ``` 384 | 385 | ```java 386 | public class JunitTest2 { 387 | 388 | @Test 389 | public void printMessage(){ 390 | System.out.println("in JunitTest2"); 391 | } 392 | } 393 | ``` 394 | 395 | ```java 396 | @RunWith(Suite.class) 397 | @Suite.SuiteClasses({ 398 | /** 399 | * 此处类的配置顺序会影响执行顺序 400 | */ 401 | JunitTest1.class, 402 | JunitTest2.class 403 | }) 404 | public class JunitSuite { 405 | 406 | } 407 | ``` 408 | 409 | 执行JunitSuite测试类,执行结果: 410 | 411 | ```java 412 | in JunitTest1 413 | in JunitTest2 414 | ``` -------------------------------------------------------------------------------- /docs/notes/java/Java11都有哪些特性.md: -------------------------------------------------------------------------------- 1 | ### 为什么选择Java11 2 | 3 | - 容器环境支持,GC等领域的增强。 4 | - 进行了瘦身,更轻量级,安装包体积小。 5 | - JDK11 是一个长期支持版。 6 | 7 | ### 特性介绍 8 | 9 | #### Jshell @since 9 10 | 11 | Jshell在Java9中就被提出来了,可以直接在终端写Java程序,回车就可以执行。Jshell默认会导入下面的一些包,所以在Jshell环境中这些包的内容都是可以使用的。 12 | 13 | ``` 14 | import java.lang.*; 15 | import java.io.*; 16 | import java.math.*; 17 | import java.net.*; 18 | import java.nio.file.*; 19 | import java.util.*; 20 | import java.util.concurrent.*; 21 | import java.util.function.*; 22 | import java.util.prefs.*; 23 | import java.util.regex.*; 24 | import java.util.stream.*; 25 | ``` 26 | 27 | 28 | 29 | ##### 1.什么是Jshell? 30 | 31 | Jshell是在 Java 9 中引入的。它提供了一个交互式 shell,用于快速原型、调试、学习 Java 及 Java API,所有这些都不需要 public static void main 方法,也不需要在执行之前编译代码。 32 | 33 | ##### 2.Jshell的使用 34 | 35 | 打开终端,键入jshell进入jshell环境,然后输入/help intro可以查看Jshell的介绍。 36 | 37 | ``` 38 | lixiaoshuang@localhost  ~  jshell 39 | | 欢迎使用 JShell -- 版本 11.0.2 40 | | 要大致了解该版本, 请键入: /help intro 41 | 42 | jshell> /help intro 43 | | 44 | | intro 45 | | ===== 46 | | 47 | | 使用 jshell 工具可以执行 Java 代码,从而立即获取结果。 48 | | 您可以输入 Java 定义(变量、方法、类等等),例如:int x = 8 49 | | 或 Java 表达式,例如:x + x 50 | | 或 Java 语句或导入。 51 | | 这些小块的 Java 代码称为“片段”。 52 | | 53 | | 这些 jshell 工具命令还可以让您了解和 54 | | 控制您正在执行的操作,例如:/list 55 | | 56 | | 有关命令的列表,请执行:/help 57 | 58 | jshell> 59 | ``` 60 | 61 | Jshell确实是一个好用的小工具,这里不做过多介绍,我就举一个例子,剩下的大家自己体会。比如我们现在就想随机生成一个UUID,以前需要这么做: 62 | 63 | - 创建一个类。 64 | - 创建一个main方法。 65 | - 然后写一个生成UUID的逻辑,执行。 66 | 67 | 现在只需要,进入打开终端键入jshell,然后直接输入`var uuid = UUID.randomUUID()`回车。就可以看到uuid的回显,这样我们就得到了一个uuid。并不需要public static void main(String[] args); 68 | 69 | ``` 70 | lixiaoshuang@localhost  ~  jshell 71 | | 欢迎使用 JShell -- 版本 11.0.2 72 | | 要大致了解该版本, 请键入: /help intro 73 | 74 | jshell> var uuid = UUID.randomUUID(); 75 | uuid ==> 9dac239e-c572-494f-b06d-84576212e012 76 | jshell> 77 | ``` 78 | 79 | ##### 3.怎么退出Jshell? 80 | 81 | 在Jshell环境中键入`/exit`就可以退出。 82 | 83 | ``` 84 | lixiaoshuang@localhost  ~  85 | lixiaoshuang@localhost  ~  jshell 86 | | 欢迎使用 JShell -- 版本 11.0.2 87 | | 要大致了解该版本, 请键入: /help intro 88 | 89 | jshell> var uuid = UUID.randomUUID(); 90 | uuid ==> 9dac239e-c572-494f-b06d-84576212e012 91 | 92 | jshell> /exit 93 | | 再见 94 | lixiaoshuang@localhost  ~  95 | ``` 96 | 97 | 98 | 99 | #### 模块化(Module)@since 9 100 | 101 | ##### 1.什么是模块化? 102 | 103 | 模块化就是增加了更高级别的聚合,是Package的封装体。Package是一些类路径名字的约定,而模块是一个或多个Package组成的封装体。 104 | 105 | java9以前 :package => class/interface。 106 | 107 | java9以后 :module => package => class/interface。 108 | 109 | 那么JDK被拆为了哪些模块呢?打开终端执行`java --list-modules`查看。 110 | 111 | ``` 112 | lixiaoshuang@localhost  ~ java --list-modules 113 | java.base@11.0.2 114 | java.compiler@11.0.2 115 | java.datatransfer@11.0.2 116 | java.desktop@11.0.2 117 | java.instrument@11.0.2 118 | java.logging@11.0.2 119 | java.management@11.0.2 120 | java.management.rmi@11.0.2 121 | java.naming@11.0.2 122 | java.net.http@11.0.2 123 | java.prefs@11.0.2 124 | java.rmi@11.0.2 125 | java.scripting@11.0.2 126 | java.se@11.0.2 127 | java.security.jgss@11.0.2 128 | java.security.sasl@11.0.2 129 | java.smartcardio@11.0.2 130 | java.sql@11.0.2 131 | java.sql.rowset@11.0.2 132 | java.transaction.xa@11.0.2 133 | java.xml@11.0.2 134 | java.xml.crypto@11.0.2 135 | jdk.accessibility@11.0.2 136 | jdk.aot@11.0.2 137 | jdk.attach@11.0.2 138 | jdk.charsets@11.0.2 139 | jdk.compiler@11.0.2 140 | jdk.crypto.cryptoki@11.0.2 141 | jdk.crypto.ec@11.0.2 142 | jdk.dynalink@11.0.2 143 | jdk.editpad@11.0.2 144 | jdk.hotspot.agent@11.0.2 145 | jdk.httpserver@11.0.2 146 | jdk.internal.ed@11.0.2 147 | jdk.internal.jvmstat@11.0.2 148 | jdk.internal.le@11.0.2 149 | jdk.internal.opt@11.0.2 150 | jdk.internal.vm.ci@11.0.2 151 | jdk.internal.vm.compiler@11.0.2 152 | jdk.internal.vm.compiler.management@11.0.2 153 | jdk.jartool@11.0.2 154 | jdk.javadoc@11.0.2 155 | jdk.jcmd@11.0.2 156 | jdk.jconsole@11.0.2 157 | jdk.jdeps@11.0.2 158 | jdk.jdi@11.0.2 159 | jdk.jdwp.agent@11.0.2 160 | jdk.jfr@11.0.2 161 | jdk.jlink@11.0.2 162 | jdk.jshell@11.0.2 163 | jdk.jsobject@11.0.2 164 | jdk.jstatd@11.0.2 165 | jdk.localedata@11.0.2 166 | jdk.management@11.0.2 167 | jdk.management.agent@11.0.2 168 | jdk.management.jfr@11.0.2 169 | jdk.naming.dns@11.0.2 170 | jdk.naming.rmi@11.0.2 171 | jdk.net@11.0.2 172 | jdk.pack@11.0.2 173 | jdk.rmic@11.0.2 174 | jdk.scripting.nashorn@11.0.2 175 | jdk.scripting.nashorn.shell@11.0.2 176 | jdk.sctp@11.0.2 177 | jdk.security.auth@11.0.2 178 | jdk.security.jgss@11.0.2 179 | jdk.unsupported@11.0.2 180 | jdk.unsupported.desktop@11.0.2 181 | jdk.xml.dom@11.0.2 182 | jdk.zipfs@11.0.2 183 | ``` 184 | 185 | 186 | 187 | ##### 2.为什么这么做? 188 | 189 | 大家都知道JRE中有一个超级大的rt.jar(60多M),tools.jar也有几十兆,以前运行一个hello world也需要上百兆的环境。 190 | 191 | - 让Java SE程序更加容易轻量级部署。 192 | - 强大的封装能力。 193 | - 改进组件间的依赖管理,引入比jar粒度更大的Module。 194 | - 改进性能和安全性。 195 | 196 | ##### 3.怎么定义模块? 197 | 198 | 模块的是通过module-info.java进行定义,编译后打包后,就成为一个模块的实体。下面来看下最简单的模块定义。 199 | 200 | ![](https://i.loli.net/2019/11/03/eaJ9cAKDjTWipo8.png) 201 | 202 | ![](https://i.loli.net/2019/11/03/FKVhUwTfy4AraqP.png) 203 | 204 | ##### 4.模块的关键字 205 | 206 | - open 207 | 208 | 用来指定开放模块,开放模块的所有包都是公开的,public的可以直接引用使用,其他类型可以通过反射得到。 209 | 210 | ``` 211 | open module module.one { 212 | //导入日志包 213 | requires java.logging; 214 | 215 | } 216 | ``` 217 | 218 | - opens 219 | 220 | opens 用来指定开放的包,其中public类型是可以直接访问的,其他类型可以通过反射得到。 221 | 222 | ``` 223 | module module.one { 224 | 225 | opens ; 226 | } 227 | ``` 228 | 229 | - exports 230 | 231 | exports用于指定模块下的哪些包可以被其他模块访问。 232 | 233 | ``` 234 | module module.one { 235 | 236 | exports ; 237 | 238 | exports to , ...; 239 | } 240 | ``` 241 | 242 | - requires 243 | 244 | 该关键字声明当前模块与另一个模块的依赖关系。 245 | 246 | ``` 247 | module module.one { 248 | 249 | requires ; 250 | 251 | } 252 | ``` 253 | 254 | - uses、provides…with… 255 | 256 | uses语句使用服务接口的名字,当前模块就会发现它,使用java.util.ServiceLoader类进行加载,必须是本模块中的,不能是其他模块中的.其实现类可以由其他模块提供。 257 | 258 | ``` 259 | module module.one { 260 | 261 | //对外提供的接口服务 ,下面指定的接口以及提供服务的impl,如果有多个实现类,用用逗号隔开 262 | uses <接口名>; 263 | 264 | provides <接口名> with <接口实现类>,<接口实现类>; 265 | 266 | } 267 | ``` 268 | 269 | 270 | 271 | #### var关键字 @since 10 272 | 273 | ##### 1.var是什么? 274 | 275 | var是Java10中新增的局部类型变量推断。它会根据后面的值来推断变量的类型,所以var必须要初始化。 276 | 277 | 例: 278 | 279 | ``` 280 | var a; ❌ 281 | var a = 1; ✅ 282 | ``` 283 | 284 | ##### 2.var使用示例 285 | 286 | - var定义局部变量 287 | 288 | ``` 289 | var a = 1; 290 | 等于 291 | int a = 1; 292 | ``` 293 | 294 | - var接收方法返回时 295 | 296 | ``` 297 | var result = this.getResult(); 298 | 等于 299 | String result = this.getResult(); 300 | ``` 301 | 302 | - var循环中定义局部变量 303 | 304 | ``` 305 | for (var i = 0; i < 5; i++) { 306 | System.out.println(i); 307 | } 308 | 等于 309 | for (int i = 0; i < 5; i++) { 310 | System.out.println(i); 311 | } 312 | ``` 313 | 314 | - var结合泛型 315 | 316 | ``` 317 | var list1 = new ArrayList(); //在<>中指定了list类型为String 318 | 等于 319 | List list1 = new ArrayList<>(); 320 | 321 | var list2 = new ArrayList<>(); //<>里默认会是Object 322 | ``` 323 | 324 | - var在Lambda中使用(java11才可以使用) 325 | 326 | ``` 327 | Consumer Consumer = (var i) -> System.out.println(i); 328 | 等于 329 | Consumer Consumer = (String i) -> System.out.println(i); 330 | ``` 331 | 332 | ##### 3.var不能再哪里使用? 333 | 334 | - 类成员变量类型。 335 | - 方法返回值类型。 336 | - Java10中Lambda不能使用var,Java11中可以使用。 337 | 338 | #### 增强api 339 | 340 | ##### 1.字符串增强 @since 11 341 | 342 | ``` 343 | // 判断字符串是否为空白 344 | " ".isBlank(); // true 345 | 346 | // 去除首尾空格 347 | " Hello Java11 ".strip(); // "Hello Java11" 348 | 349 | // 去除尾部空格 350 | " Hello Java11 ".stripTrailing(); // " Hello Java11" 351 | 352 | // 去除首部空格 353 | " Hello Java11 ".stripLeading(); // "Hello Java11 " 354 | 355 | // 复制字符串 356 | "Java11".repeat(3); // "Java11Java11Java11" 357 | 358 | // 行数统计 359 | "A\nB\nC".lines().count(); // 3 360 | ``` 361 | 362 | 363 | 364 | ##### 2.集合增强 365 | 366 | 从Java 9 开始,jdk里面就为集合(List、Set、Map)增加了of和copyOf方法。它们用来创建不可变集合。 367 | 368 | - of() @since 9 369 | - copyOf() @since 10 370 | 371 | 示例一: 372 | 373 | ``` 374 | var list = List.of("Java", "Python", "C"); //不可变集合 375 | var copy = List.copyOf(list); //copyOf判断是否是不可变集合类型,如果是直接返回 376 | System.out.println(list == copy); // true 377 | 378 | var list = new ArrayList(); // 这里返回正常的集合 379 | var copy = List.copyOf(list); // 这里返回一个不可变集合 380 | System.out.println(list == copy); // false 381 | ``` 382 | 383 | 示例二: 384 | 385 | ``` 386 | var set = Set.of("Java", "Python", "C"); 387 | var copy = Set.copyOf(set); 388 | System.out.println(set == copy); // true 389 | 390 | var set1 = new HashSet(); 391 | var copy1 = List.copyOf(set1); 392 | System.out.println(set1 == copy1); // false 393 | ``` 394 | 395 | 示例三: 396 | 397 | ``` 398 | var map = Map.of("Java", 1, "Python", 2, "C", 3); 399 | var copy = Map.copyOf(map); 400 | System.out.println(map == copy); // true 401 | 402 | var map1 = new HashMap(); 403 | var copy1 = Map.copyOf(map1); 404 | System.out.println(map1 == copy1); // false 405 | ``` 406 | 407 | `注意:使用 of 和 copyOf 创建的集合为不可变集合,不能进行添加、删除、替换、排序等操作,不然会报java.lang.UnsupportedOperationException异常,使用Set.of()不能出现重复元素、Map.of()不能出现重复key,否则回报java.lang.IllegalArgumentException。`。 408 | 409 | ##### 3.Stream增强 @since 9 410 | 411 | Stream是Java 8 中的特性,在Java 9 中为其新增了4个方法: 412 | 413 | - ofNullable(T t) 414 | 415 | 此方法可以接收null来创建一个空流 416 | 417 | ``` 418 | 以前 419 | Stream.of(null); //报错 420 | 现在 421 | Stream.ofNullable(null); 422 | ``` 423 | 424 | - takeWhile(Predicate predicate) 425 | 426 | 此方法根据Predicate接口来判断如果为true就 `取出` 来生成一个新的流,只要碰到false就终止,不管后边的元素是否符合条件。 427 | 428 | ``` 429 | Stream integerStream = Stream.of(6, 10, 11, 15, 20); 430 | Stream takeWhile = integerStream.takeWhile(t -> t % 2 == 0); 431 | takeWhile.forEach(System.out::println); // 6,10 432 | ``` 433 | 434 | - dropWhile(Predicate predicate) 435 | 436 | 此方法根据Predicate接口来判断如果为true就 `丢弃` 来生成一个新的流,只要碰到false就终止,不管后边的元素是否符合条件。 437 | 438 | ``` 439 | Stream integerStream = Stream.of(6, 10, 11, 15, 20); 440 | Stream takeWhile = integerStream.dropWhile(t -> t % 2 == 0); 441 | takeWhile.forEach(System.out::println); //11,15,20 442 | ``` 443 | 444 | - iterate重载 445 | 446 | 以前使用iterate方法生成无限流需要配合limit进行截断 447 | 448 | ``` 449 | Stream limit = Stream.iterate(1, i -> i + 1).limit(5); 450 | limit.forEach(System.out::println); //1,2,3,4,5 451 | ``` 452 | 453 | 现在重载后这个方法增加了个判断参数 454 | 455 | ``` 456 | Stream iterate = Stream.iterate(1, i -> i <= 5, i -> i + 1); 457 | iterate.forEach(System.out::println); //1,2,3,4,5 458 | ``` 459 | 460 | ##### 4.Optional增强 @since 9 461 | 462 | - stream() 463 | 464 | 如果为空返回一个空流,如果不为空将Optional的值转成一个流。 465 | 466 | ``` 467 | //返回Optional值的流 468 | Stream stream = Optional.of("Java 11").stream(); 469 | stream.forEach(System.out::println); // Java 11 470 | 471 | //返回空流 472 | Stream stream = Optional.ofNullable(null).stream(); 473 | stream.forEach(System.out::println); // 474 | ``` 475 | 476 | 477 | 478 | - ifPresentOrElse(Consumer action, Runnable emptyAction) 479 | 480 | 个人感觉这个方法就是结合isPresent()对Else的增强,ifPresentOrElse 方法的用途是,如果一个 Optional 包含值,则对其包含的值调用函数 action,即 action.accept(value),这与 ifPresent 一致;与 ifPresent 方法的区别在于,ifPresentOrElse 还有第二个参数 emptyAction —— 如果 Optional 不包含值,那么 ifPresentOrElse 便会调用 emptyAction,即 emptyAction.run()。 481 | 482 | ``` 483 | Optional optional = Optional.of(1); 484 | optional.ifPresentOrElse( x -> System.out.println("Value: " + x),() -> 485 | System.out.println("Not Present.")); //Value: 1 486 | 487 | optional = Optional.empty(); 488 | optional.ifPresentOrElse( x -> System.out.println("Value: " + x),() -> 489 | System.out.println("Not Present.")); //Not Present. 490 | ``` 491 | 492 | - or(Supplier> supplier) 493 | 494 | ``` 495 | Optional optional1 = Optional.of("Java"); 496 | Supplier> supplierString = () -> Optional.of("Not Present"); 497 | optional1 = optional1.or( supplierString); 498 | optional1.ifPresent( x -> System.out.println("Value: " + x)); //Value: Java 499 | 500 | optional1 = Optional.empty(); 501 | optional1 = optional1.or( supplierString); 502 | optional1.ifPresent( x -> System.out.println("Value: " + x)); //Value: Not Present 503 | ``` 504 | 505 | ##### 5.InputStream增强 @since 9 506 | 507 | ``` 508 | String lxs = "java"; 509 | try (var inputStream = new ByteArrayInputStream(lxs.getBytes()); 510 | var outputStream = new ByteArrayOutputStream()) { 511 | inputStream.transferTo(outputStream); 512 | System.out.println(outputStream); //java 513 | } 514 | ``` 515 | 516 | 517 | 518 | #### HTTP Client API 519 | 520 | 改api支持同步和异步两种方式,下面是两种方式的示例: 521 | 522 | ``` 523 | var request = HttpRequest.newBuilder() 524 | .uri(URI.create("https://www.baidu.com/")) 525 | .build(); 526 | var client = HttpClient.newHttpClient(); 527 | // 同步 528 | HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); 529 | System.out.println(response.body()); 530 | 531 | // 异步 532 | CompletableFuture> sendAsync = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()); 533 | //这里会阻塞 534 | HttpResponse response1 = sendAsync.get(); 535 | System.out.println(response1.body()); 536 | ``` 537 | 538 | 539 | 540 | #### 直接运行java文件 541 | 542 | 我们都知道以前要运行一个.java文件,首先要javac编译成.class文件,然后在java执行: 543 | 544 | ``` 545 | //编译 546 | javac Java11.java 547 | //运行 548 | java Java11 549 | ``` 550 | 551 | 在java11中,只需要通过java一个命令就可以搞定 552 | 553 | ``` 554 | java Java11.java 555 | ``` 556 | 557 | 558 | 559 | ### 移除内容 560 | 561 | - com.sun.awt.AWTUtilities。 562 | - sun.misc.Unsafe.defineClass 使用java.lang.invoke.MethodHandles.Lookup.defineClass来替代。 563 | - Thread.destroy() 以及 Thread.stop(Throwable) 方法。 564 | - sun.nio.ch.disableSystemWideOverlappingFileLockCheck 属性。 565 | - sun.locale.formatasdefault 属性。 566 | - jdk snmp 模块。 567 | - javafx,openjdk 是从java10版本就移除了,oracle java10还尚未移除javafx ,而java11版本将javafx也移除了 568 | - Java Mission Control,从JDK中移除之后,需要自己单独下载。 569 | - Root Certificates :Baltimore Cybertrust Code Signing CA,SECOM ,AOL and Swisscom。 570 | - 在java11中将java9标记废弃的Java EE及CORBA模块移除掉。 571 | 572 | ### 完全支持Linux容器(包括docker) 573 | 574 | 许多运行在Java虚拟机中的应用程序(包括Apache Spark和Kafka等数据服务以及传统的企业应用程序)都可以在Docker容器中运行。但是在Docker容器中运行Java应用程序一直存在一个问题,那就是在容器中运行JVM程序在设置内存大小和CPU使用率后,会导致应用程序的性能下降。这是因为Java应用程序没有意识到它正在容器中运行。随着Java 10的发布,这个问题总算得以解决,JVM现在可以识别由容器控制组(cgroups)设置的约束。可以在容器中使用内存和CPU约束来直接管理Java应用程序,其中包括: 575 | 576 | - 遵守容器中设置的内存限制 577 | - 在容器中设置可用的CPU 578 | - 在容器中设置CPU约束 579 | 580 | `Java 10的这个改进在Docker for Mac、Docker for Windows以及Docker Enterprise Edition等环境均有效。` 581 | 582 | ### 总结 583 | 584 | ![Java版本特性.png](https://i.loli.net/2019/11/05/uesYIQnMUgOdJpN.png) 585 | 586 | -------------------------------------------------------------------------------- /docs/notes/java/Java线程、锁、线程池总结.md: -------------------------------------------------------------------------------- 1 | # **并发的源头** 2 | 3 | ## **源头之一:缓存导致的可见性问题** 4 | 5 | ### 单核时代 6 | 7 | ![https://tva1.sinaimg.cn/large/00831rSTly1gdk18hw6ivj30vq0hswla.jpg](https://tva1.sinaimg.cn/large/008eGmZEly1gnhnc15g20j30vq0hsack.jpg) 8 | 9 | ### 多核时代 10 | 11 | ![https://tva1.sinaimg.cn/large/00831rSTly1gdk19xa23uj30vq0hs10y.jpg](https://tva1.sinaimg.cn/large/008eGmZEly1gnhnbxi4pzj30vq0hsmz8.jpg) 12 | 13 | ## **源头之二:线程切换带来的原子性问题** 14 | 15 | 操作系统允许某个进程执行一小段时间,例如 50 毫秒,过了 50 毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个 50 毫秒称为“**时间片**”。 16 | 17 | ![https://tva1.sinaimg.cn/large/00831rSTly1gdk1fgw2twj30vq0hsgn0.jpg](https://tva1.sinaimg.cn/large/008eGmZEly1gnhnc1jumaj30vq0hsjrx.jpg) 18 | 19 | ## **源头之三:编译优化带来的有序性问题** 20 | 21 | ![https://tva1.sinaimg.cn/large/00831rSTly1gdk1ssotakj30vq0hstct.jpg](https://tva1.sinaimg.cn/large/008eGmZEly1gnhnc23dwsj30vq0hsdhp.jpg) 22 | 23 | # 线程的生命周期 24 | 25 | ## **通用生命周期** 26 | 27 | 通用的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是:**初始状态、可运行状态、运行状态、休眠状态**和**终止状态**。 28 | 29 | ![https://tva1.sinaimg.cn/large/00831rSTgy1gdm751yl2ej30vq0jumy8.jpg](https://tva1.sinaimg.cn/large/008eGmZEly1gnhnc062ilj30vq0ju3z0.jpg) 30 | 31 | 这“五态模型”的详细情况如下所示。 32 | 33 | 1. **初始状态**,指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。 34 | 2. **可运行状态**,指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。 35 | 3. 当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了**运行状态**。 36 | 4. 运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到**休眠状态**,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。 37 | 5. 线程执行完或者出现异常就会进入**终止状态**,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。 38 | 39 | ## **Java 中线程的生命周期** 40 | 41 | Java 语言中线程共有六种状态,分别是: 42 | 43 | 1. NEW(初始化状态) 44 | 2. RUNNABLE(可运行 / 运行状态) 45 | 3. BLOCKED(阻塞状态) 46 | 4. WAITING(无时限等待) 47 | 5. TIMED_WAITING(有时限等待) 48 | 6. TERMINATED(终止状态) 49 | 50 | 这看上去挺复杂的,状态类型也比较多。但其实在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说**只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权**。 51 | 52 | 所以 Java 线程的生命周期可以简化为下图: 53 | 54 | ![https://tva1.sinaimg.cn/large/00831rSTgy1gdm7c0x45rj30vq0ju0ty.jpg](https://tva1.sinaimg.cn/large/008eGmZEly1gnhnbz7ijij30vq0ju0tc.jpg) 55 | 56 | ## **方法是如何被执行的** 57 | 58 | ``` 59 | int a = 7; 60 | int[] b = fibonacci(a); 61 | int[] c = b; 62 | ``` 63 | 64 | ![https://tva1.sinaimg.cn/large/00831rSTly1gdmc3str9nj30vq0kljtd.jpg](https://tva1.sinaimg.cn/large/008eGmZEly1gnhnc0noclj30vq0klgms.jpg) 65 | 66 | # 线程通信 67 | 68 | ## 等待通知机制 wait()、notify()、notifyAll() 69 | 70 | 有一些需要注意: 71 | 72 | - wait() 、notify()、notifyAll() 调用的前提都是获得了对象的锁(也可称为对象监视器)。 73 | - 调用 wait() 方法后线程会释放锁,进入 `WAITING` 状态,该线程也会被移动到**等待队列**中。 74 | - 调用 notify() 方法会将**等待队列**中的线程移动到**同步队列**中,线程状态也会更新为 `BLOCKED` 75 | - 从 wait() 方法返回的前提是调用 notify() 方法的线程释放锁,wait() 方法的线程获得锁。 76 | 77 | 等待通知有着一个经典范式: 78 | 79 | 线程 A 作为消费者: 80 | 81 | 1. 获取对象的锁。 82 | 2. 进入 while(判断条件),并调用 wait() 方法。 83 | 3. 当条件满足跳出循环执行具体处理逻辑。 84 | 85 | 线程 B 作为生产者: 86 | 87 | 1. 获取对象锁。 88 | 2. 更改与线程 A 共用的判断条件。 89 | 3. 调用 notify() 方法。 90 | 91 | 伪代码如下: 92 | 93 | ```java 94 | //Thread A 95 | 96 | synchronized(Object){ 97 | while(条件){ 98 | Object.wait(); 99 | } 100 | //do something 101 | } 102 | 103 | //Thread B 104 | synchronized(Object){ 105 | 条件=false;//改变条件 106 | Object.notify(); 107 | } 108 | ``` 109 | 110 | ## Semaphore 信号量 111 | 112 | 使用场景:同一时间控制线程并发数量 113 | 114 | ```java 115 | public static void main(String[] args) { 116 | int N = 8; //工人数 117 | Semaphore semaphore = new Semaphore(1); //机器数目 118 | for (int i = 0; i < N; i++) { 119 | int finalI = i; 120 | new Thread(() -> { 121 | try { 122 | semaphore.acquire(); // 在子线程里控制资源占用 123 | System.out.println("工人" + finalI + "占用一个机器在生产..."); 124 | Thread.sleep(1000); 125 | System.out.println("工人" + finalI + "释放出机器"); 126 | semaphore.release(); // 在子线程里控制释放资源占用 127 | } catch (InterruptedException e) { 128 | e.printStackTrace(); 129 | } 130 | }).start(); 131 | } 132 | } 133 | ``` 134 | 135 | ## join()方法 136 | 137 | 在 t1.join() 时会一直阻塞到 t1 执行完毕,所以最终主线程会等待 t1 线程执行完毕。 138 | 139 | 其实从源码可以看出,join() 也是利用的等待通知机制: 140 | 141 | 核心逻辑: 142 | 143 | ```java 144 | while (isAlive()) { 145 | wait(0); 146 | } 147 | ``` 148 | 149 | 在 join 线程完成后会调用 notifyAll() 方法,是在 JVM 实现中调用,所以这里看不出来。 150 | 151 | ## volatile共享内存 152 | 153 | 采用 volatile 修饰主要是为了内存可见性 154 | 155 | ## CountDownLatch 156 | 157 | ```java 158 | public static void main(String[] args) throws InterruptedException { 159 | CountDownLatch countDownLatch = new CountDownLatch(10); 160 | for (int i = 0; i < 10; i++) { 161 | int finalI = i; 162 | new Thread(() -> { 163 | synchronized (CountDownLatchDemo.class) { 164 | System.out.println("id:" + finalI + "," + Thread.currentThread().getName()); 165 | //latch.countDown(); 166 | System.out.println("线程任务" + finalI + "结束,其他任务继续"); 167 | countDownLatch.countDown(); 168 | } 169 | }).start(); 170 | } 171 | countDownLatch.await(); // 注意跟CyclicBarrier不同,这里在主线程await 172 | System.out.println("==>主线程执行结束。。。。"); 173 | } 174 | ``` 175 | 176 | CountDownLatch 也是基于 AQS(AbstractQueuedSynchronizer) 实现的,初始化数量,然后递减直到为0。 177 | 178 | - 初始化一个 CountDownLatch 时告诉并发的线程,然后在每个线程处理完毕之后调用 countDown() 方法。 179 | - 该方法会将 AQS 内置的一个 state 状态 -1 。 180 | - 最终在主线程调用 await() 方法,它会阻塞直到 `state == 0` 的时候返回。 181 | 182 | ## CyclicBarrier 183 | 184 | ```java 185 | public static void main(String[] args) throws InterruptedException { 186 | 187 | CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> { 188 | System.out.println("回调>>" + Thread.currentThread().getName()); 189 | System.out.println("回调>>线程执行结束"); 190 | }); 191 | 192 | for (int i = 0; i < 5; i++) { 193 | int finalI = i; 194 | new Thread(() -> { 195 | System.out.println("id:" + finalI + "," + Thread.currentThread().getName()); 196 | try { 197 | System.out.println("线程组任务" + finalI + "结束,其他任务继续"); 198 | cyclicBarrier.await(); // 注意跟CountDownLatch不同,这里在子线程await 199 | } catch (Exception e) { 200 | e.printStackTrace(); 201 | } 202 | }).start(); 203 | } 204 | 205 | System.out.println("==>主线程执行结束。。。。"); 206 | } 207 | ``` 208 | 209 | CyclicBarrier 中文名叫做屏障或者是栅栏,也可以用于线程间通信。CyclicBarrier可以重复利用。它可以等待 N 个线程都达到某个状态后继续运行的效果。 210 | 211 | - 首先初始化线程参与者。 212 | - 调用 `await()` 将会在所有参与者线程都调用之前等待。 213 | - 直到所有参与者都调用了 `await()` 后,所有线程从 `await()` 返回继续后续逻辑。 214 | 215 | ## 线程响应中断 216 | 217 | 可以采用中断线程的方式来通信,调用了 `thread.interrupt()` 方法其实就是将 thread 中的一个标志属性置为了 true。 218 | 219 | 并不是说调用了该方法就可以中断线程,如果不对这个标志进行响应其实是没有什么作用(这里对这个标志进行了判断)。 220 | 221 | **但是如果抛出了 InterruptedException 异常,该标志就会被 JVM 重置为 false。** 222 | 223 | ## 线程池awaitTermination()方法 224 | 225 | 如果是用线程池来管理线程,可以使用以下方式来让主线程等待线程池中所有任务执行完毕: 226 | 227 | ```java 228 | private static void executorService() throws Exception{ 229 | BlockingQueue queue = new LinkedBlockingQueue<>(10) ; 230 | ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,5,1, TimeUnit.MILLISECONDS,queue) ; 231 | poolExecutor.execute(new Runnable() { 232 | @Override 233 | public void run() { 234 | LOGGER.info("running"); 235 | try { 236 | Thread.sleep(3000); 237 | } catch (InterruptedException e) { 238 | e.printStackTrace(); 239 | } 240 | } 241 | }); 242 | poolExecutor.execute(new Runnable() { 243 | @Override 244 | public void run() { 245 | LOGGER.info("running2"); 246 | try { 247 | Thread.sleep(2000); 248 | } catch (InterruptedException e) { 249 | e.printStackTrace(); 250 | } 251 | } 252 | }); 253 | 254 | poolExecutor.shutdown(); 255 | while (!poolExecutor.awaitTermination(1,TimeUnit.SECONDS)){ 256 | LOGGER.info("线程还在执行。。。"); 257 | } 258 | LOGGER.info("main over"); 259 | } 260 | ``` 261 | 262 | 输出结果: 263 | 264 | ``` 265 | 2018-03-16 20:18:01.273 [pool-1-thread-2] INFO c.c.actual.ThreadCommunication - running2 266 | 2018-03-16 20:18:01.273 [pool-1-thread-1] INFO c.c.actual.ThreadCommunication - running 267 | 2018-03-16 20:18:02.273 [main] INFO c.c.actual.ThreadCommunication - 线程还在执行。。。 268 | 2018-03-16 20:18:03.278 [main] INFO c.c.actual.ThreadCommunication - 线程还在执行。。。 269 | 2018-03-16 20:18:04.278 [main] INFO c.c.actual.ThreadCommunication - main over 270 | ``` 271 | 272 | 使用这个 `awaitTermination()` 方法的前提需要关闭线程池,如调用了 `shutdown()` 方法。 273 | 274 | 调用了 `shutdown()` 之后线程池会停止接受新任务,并且会平滑的关闭线程池中现有的任务。 275 | 276 | ## 管道通信 277 | 278 | ```java 279 | public static void piped() throws IOException { 280 | //面向于字符 PipedInputStream 面向于字节 281 | PipedWriter writer = new PipedWriter(); 282 | PipedReader reader = new PipedReader(); 283 | 284 | //输入输出流建立连接 285 | writer.connect(reader); 286 | Thread t1 = new Thread(new Runnable() { 287 | @Override 288 | public void run() { 289 | LOGGER.info("running"); 290 | try { 291 | for (int i = 0; i < 10; i++) { 292 | 293 | writer.write(i+""); 294 | Thread.sleep(10); 295 | } 296 | } catch (Exception e) { 297 | 298 | } finally { 299 | try { 300 | writer.close(); 301 | } catch (IOException e) { 302 | e.printStackTrace(); 303 | } 304 | } 305 | } 306 | }); 307 | Thread t2 = new Thread(new Runnable() { 308 | @Override 309 | public void run() { 310 | LOGGER.info("running2"); 311 | int msg = 0; 312 | try { 313 | while ((msg = reader.read()) != -1) { 314 | LOGGER.info("msg={}", (char) msg); 315 | } 316 | } catch (Exception e) { 317 | } 318 | } 319 | }); 320 | t1.start(); 321 | t2.start(); 322 | } 323 | ``` 324 | 325 | 输出结果: 326 | 327 | ``` 328 | 2018-03-16 19:56:43.014 [Thread-0] INFO c.c.actual.ThreadCommunication - running 329 | 2018-03-16 19:56:43.014 [Thread-1] INFO c.c.actual.ThreadCommunication - running2 330 | 2018-03-16 19:56:43.130 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=0 331 | 2018-03-16 19:56:43.132 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=1 332 | 2018-03-16 19:56:43.132 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=2 333 | 2018-03-16 19:56:43.133 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=3 334 | 2018-03-16 19:56:43.133 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=4 335 | 2018-03-16 19:56:43.133 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=5 336 | 2018-03-16 19:56:43.133 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=6 337 | 2018-03-16 19:56:43.134 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=7 338 | 2018-03-16 19:56:43.134 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=8 339 | 2018-03-16 19:56:43.134 [Thread-1] INFO c.c.actual.ThreadCommunication - msg=9 340 | ``` 341 | 342 | Java 虽说是基于内存通信的,但也可以使用管道通信。 343 | 344 | 需要注意的是,输入流和输出流需要首先建立连接。这样线程 B 就可以收到线程 A 发出的消息了。 345 | 346 | 实际开发中可以灵活根据需求选择最适合的线程通信方式。 347 | 348 | # 锁原理 349 | 350 | ## 锁分类 351 | 352 | ### 同一进程内 353 | 354 | - synchronized (隐式锁,加锁、释放锁都不需要手动操作) 355 | - 重入锁 ReentrantLock (显示锁) 356 | - 读写锁 ReentrantReadWriteLock (显示锁) 357 | 358 | ### 不同进程内(分布式锁) 359 | 360 | - 基于数据库 361 | - reids分布式锁 362 | - zk分布式锁 363 | 364 | ## **简单锁模型** 365 | 366 | ![https://tva1.sinaimg.cn/large/00831rSTly1gdl71l90uij30vq0hs3zh.jpg](https://tva1.sinaimg.cn/large/008eGmZEly1gnhnbwflicj30vq0hsmxk.jpg) 367 | 368 | ## **改进后锁模型** 369 | 370 | ![https://tva1.sinaimg.cn/large/00831rSTly1gdl72drxk2j30vq0hswg5.jpg](https://tva1.sinaimg.cn/large/008eGmZEly1gnhnc2k0gqj30vq0hsgmi.jpg) 371 | 372 | ## **Java 语言提供的锁技术:synchronized** 373 | 374 | ![https://tva1.sinaimg.cn/large/00831rSTly1gdl73ribnvj30vq0hs0uq.jpg](https://tva1.sinaimg.cn/large/008eGmZEly1gnhnby9y3aj30vq0hs3zn.jpg) 375 | 376 | ### **synchronized的使用方式有三种** 377 | 378 | - 静态方法(锁类) 379 | - 普通方法(锁类对象) 380 | - 代码块(锁类对象) 381 | 382 | ### **synchronized的实现原理** 383 | 384 | **jvm 层面:** 385 | 386 | synchronized依赖于monitorenter和monitorexit两个指令实现的。 387 | 388 | - monitorenter 389 | 390 | 每个对象都有一把锁,当一个线程进入同步代码块,都会去获取这个对象所持有monitor对象锁(C++实现),如果当前线程获取锁,会把monitor对象进入数自增1次。如果该线程重复进入,会把monitor对象进入数再次自增1次。 391 | 392 | 当有其他线程进入,会把其他线程放入等待队列排队,直到获取锁的线程将monitor对象的进入数设置为0释放锁,其他线程才有机会获取锁。 393 | 394 | - monitorexit 395 | 396 | **synchronized优化层面:** 397 | 398 | 线程竞争锁会引起用户态和内核态的频繁切换,造成资源浪费且效率不高,在jdk1.6以后synchronized做了性能优化。 399 | 400 | https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1e4c75b2c2fc46edab356ceca24bae00~tplv-k3u1fbpfcp-zoom-1.image 401 | 402 | - 无锁 403 | 404 | 如果不加synchronized关键字,表示无锁,很好理解。 405 | 406 | - 偏向锁 407 | 408 | 升级过程:当线程进入同步块时,Markword会存储偏向线程的id并且cas将Markword锁状态标识为01,是否偏向用1表示当前处于偏向锁(对着上图来看),如果是偏向线程下次进入同步代码只要比较Markword的线程id是否和当前线程id相等,如果相等不用做任何操作就可以进入同步代码执行,如果比较后不相等说明有其他线程竞争锁,synchronized会升级成轻量级锁。这个过程中在操作系统层面不用做内核态和用户态的切换,减少切换线程带来的资源消耗。 409 | 410 | 膨胀过程:当有另外线程进入,偏向锁会升级成轻量级锁。比如线程A是偏向锁,这是B线程进入,就会成轻量级锁, **只要有两个线程就会升级成轻量级锁** 。 411 | 412 | - 轻量级锁 413 | 414 | 升级过程:在线程运行获取锁后,会在栈帧中创造锁记录并将MarkWord复制到锁记录,然后将MarkWord指向锁记录,如果当前线程持有锁,其他线程再进入,此时其他线程会cas自旋,直到获取锁,轻量级锁适合多线程交替执行,效率高(cas只消耗cpu)。 415 | 416 | 膨胀过程:有两种情况会膨胀成重量级锁。1种情况是cas自旋10次还没获取锁。第2种情况其他线程正在cas获取锁,第三个线程竞争获取锁,锁也会膨胀变成重量级锁。 417 | 418 | - 重量级锁 419 | 420 | 重量级锁升级后是不可逆的,也就是说重量锁不可以再变为轻量级锁。 421 | 422 | ### **[适应性自旋](https://crossoverjie.top/JCSprout/#/thread/Synchronize?id=适应性自旋)** 423 | 424 | 在使用 `CAS` 时,如果操作失败,`CAS` 会自旋再次尝试。由于自旋是需要消耗 `CPU` 资源的,所以如果长期自旋就白白浪费了 `CPU`。`JDK1.6`加入了适应性自旋,如果某个锁自旋很少成功获得,那么下一次就会减少自旋。 425 | 426 | # ReentrantLock 实现原理 427 | 428 | ReentrantLock是可重入锁,主要基于AQS来实现公平锁和非公平锁,获取锁的时候通过CAS修改AQS的state字段,默认为0,线程获取到锁后会更新成1,并且设置独占线程为当前线程,如果更新成功代表当前线程拿到了锁。由于支持可重入,所以同一个线程拿锁state会继续增加,在释放锁的时候要将state减到0才算释放成功,并唤醒其他线程。 429 | 430 | ## 锁类型 431 | 432 | ReentrantLock 分为公平锁和非公平锁,可以通过构造方法来指定具体类型 433 | 434 | ```java 435 | /** 436 | * Creates an instance of {@code ReentrantLock}. 437 | * This is equivalent to using {@code ReentrantLock(false)}. 438 | */ 439 | public ReentrantLock() { 440 | // 默认是非公平锁 441 | sync = new NonfairSync(); 442 | } 443 | 444 | /** 445 | * Creates an instance of {@code ReentrantLock} with the 446 | * given fairness policy. 447 | * 448 | * @param fair {@code true} if this lock should use a fair ordering policy 449 | */ 450 | public ReentrantLock(boolean fair) { 451 | sync = fair ? new FairSync() : new NonfairSync(); 452 | } 453 | ``` 454 | 455 | 使用方式: 456 | 457 | ```java 458 | private ReentrantLock lock = new ReentrantLock(); 459 | public void run() { 460 | lock.lock(); 461 | try { 462 | //do bussiness 463 | } catch (InterruptedException e) { 464 | e.printStackTrace(); 465 | } finally { 466 | lock.unlock(); 467 | } 468 | } 469 | ``` 470 | 471 | ## 公平锁实现原理 472 | 473 | ```java 474 | public void lock() { 475 | sync.acquire(1); 476 | } 477 | 478 | public final void acquire(int arg) { 479 | if (!tryAcquire(arg) && 480 | acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 481 | selfInterrupt(); 482 | } 483 | 484 | /** 485 | * Sync object for fair locks 486 | */ 487 | static final class FairSync extends Sync { 488 | private static final long serialVersionUID = -3000897897090466540L; 489 | /** 490 | * Fair version of tryAcquire. Don't grant access unless 491 | * recursive call or no waiters or is first. 492 | */ 493 | @ReservedStackAccess 494 | protected final boolean tryAcquire(int acquires) { 495 | final Thread current = Thread.currentThread(); 496 | int c = getState(); 497 | if (c == 0) { 498 | if (!hasQueuedPredecessors() && 499 | compareAndSetState(0, acquires)) { 500 | setExclusiveOwnerThread(current); 501 | return true; 502 | } 503 | } 504 | else if (current == getExclusiveOwnerThread()) { 505 | int nextc = c + acquires; 506 | if (nextc < 0) 507 | throw new Error("Maximum lock count exceeded"); 508 | setState(nextc); 509 | return true; 510 | } 511 | return false; 512 | } 513 | } 514 | ``` 515 | 516 | - 通过ReentrantLock构造方法设置为公平锁 517 | - 通过tryAcquire方法尝试获取锁 518 | - 首先会判断 AQS 中的 state 是否等于 0,0表示目前没有其他线程获得锁,当前线程就可以尝试获取锁。尝试之前会利用 hasQueuedPredecessors() 方法来判断 AQS 的队列中中是否有其他线程,如果有则不会尝试获取锁(这是公平锁特有的情况)。 519 | - 如果队列中没有线程就利用 CAS 来将 AQS 中的 state 修改为1,也就是获取锁,获取成功则将当前线程置为获得锁的独占线程(setExclusiveOwnerThread(current))。 520 | - 如果 state 大于 0 时,说明锁已经被获取了,则需要判断获取锁的线程是否为当前线程(ReentrantLock 支持重入),是则需要将 state + 1,并将值更新。 521 | - 如果 tryAcquire(arg) 获取锁失败,则需要用 addWaiter(Node.EXCLUSIVE) 将当前线程写入队列中。 522 | 523 | ## 非公平锁实现原理 524 | 525 | ```java 526 | public boolean tryLock() { 527 | return sync.nonfairTryAcquire(1); 528 | } 529 | 530 | /** 531 | * Performs non-fair tryLock. tryAcquire is implemented in 532 | * subclasses, but both need nonfair try for trylock method. 533 | */ 534 | @ReservedStackAccess 535 | final boolean nonfairTryAcquire(int acquires) { 536 | final Thread current = Thread.currentThread(); 537 | int c = getState(); 538 | if (c == 0) { 539 | if (compareAndSetState(0, acquires)) { 540 | setExclusiveOwnerThread(current); 541 | return true; 542 | } 543 | } 544 | else if (current == getExclusiveOwnerThread()) { 545 | int nextc = c + acquires; 546 | if (nextc < 0) // overflow 547 | throw new Error("Maximum lock count exceeded"); 548 | setState(nextc); 549 | return true; 550 | } 551 | return false; 552 | } 553 | ``` 554 | 555 | - 通过nonfairTryAcquire方法获取锁 556 | - 首先会判断 AQS 中的 state 是否等于 0,0表示目前没有其他线程获得锁,当前线程就可以尝试获取锁 557 | - 如果队列中没有线程就利用 CAS 来将 AQS 中的 state 修改为1,也就是获取锁,获取成功则将当前线程置为获得锁的独占线程(setExclusiveOwnerThread(current))。 558 | - 如果 state 大于 0 时,说明锁已经被获取了,则需要判断获取锁的线程是否为当前线程(ReentrantLock 支持重入),是则需要将 state + 1,并将值更新。 559 | 560 | ## 释放锁原理 561 | 562 | ```java 563 | public void unlock() { 564 | sync.release(1); 565 | } 566 | 567 | public final boolean release(int arg) { 568 | if (tryRelease(arg)) { 569 | Node h = head; 570 | if (h != null && h.waitStatus != 0) 571 | unparkSuccessor(h); 572 | return true; 573 | } 574 | return false; 575 | } 576 | 577 | @ReservedStackAccess 578 | protected final boolean tryRelease(int releases) { 579 | int c = getState() - releases; 580 | if (Thread.currentThread() != getExclusiveOwnerThread()) 581 | throw new IllegalMonitorStateException(); 582 | boolean free = false; 583 | if (c == 0) { 584 | free = true; 585 | setExclusiveOwnerThread(null); 586 | } 587 | setState(c); 588 | return free; 589 | } 590 | ``` 591 | 592 | - 公平锁和非公平锁的释放流程都是一样的 593 | - 首先会判断当前线程是否为获得锁的线程,由于是重入锁所以需要将 state 减到 0 才认为完全释放锁。 594 | - 释放之后需要调用 unparkSuccessor(h) 来唤醒被挂起的线程 595 | 596 | # **线程池总结** 597 | 598 | ## 线程池的拒绝策略 599 | 600 | - 丢弃任务抛出RejectedExecutionException异常,这个也是线程池的默认拒绝策略。 601 | - 丢弃任务,但是不抛出异常。 602 | - 丢弃队列最前面的任务,然后重新提交被拒绝的任务。 603 | - 由调用线程(提交任务的线程)处理该任务。 604 | 605 | ## 创建线程池 606 | 607 | - Executors.newFixedThreadPool 608 | 609 | ```java 610 | public static ExecutorService newFixedThreadPool(int nThreads) { 611 | return new ThreadPoolExecutor(nThreads, nThreads, 612 | 0L, TimeUnit.MILLISECONDS, 613 | new LinkedBlockingQueue()); 614 | } 615 | ``` 616 | 617 | 创建一个线程池,该线程池重用在共享的无边界队列上运行的固定数量的线程。 在任何时候,最多nThreads线程都是活动的处理任务。 如果在所有线程都处于活动状态时提交了其他任务,则它们将在队列中等待,直到某个线程可用为止。 如果在关闭之前执行过程中由于执行失败导致任何线程终止,则在执行后续任务时将使用新线程代替。 池中的线程将一直存在,直到将其显式shutdown为止。 618 | 619 | - Executors.newSingleThreadExecutor() 620 | 621 | ```java 622 | public static ExecutorService newSingleThreadExecutor() { 623 | return new FinalizableDelegatedExecutorService 624 | (new ThreadPoolExecutor(1, 1, 625 | 0L, TimeUnit.MILLISECONDS, 626 | new LinkedBlockingQueue())); 627 | } 628 | ``` 629 | 630 | 创建一个执行程序,该执行程序使用在不受限制的队列上操作的单个工作线程。 (但是请注意,如果该单线程由于在关闭之前的执行期间失败而终止,则在需要执行新任务时将替换为新线程。)保证任务按顺序执行,并且活动的任务不超过一个在任何给定时间。 与其他等效的newFixedThreadPool(1)不同,保证返回的执行程序不能重新配置为使用其他线程。 631 | 632 | - Executors.newScheduledThreadPool 633 | 634 | ```java 635 | public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { 636 | return new ScheduledThreadPoolExecutor(corePoolSize); 637 | } 638 | 639 | public ScheduledThreadPoolExecutor(int corePoolSize) { 640 | super(corePoolSize, Integer.MAX_VALUE, 641 | DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, 642 | new DelayedWorkQueue()); 643 | } 644 | ``` 645 | 646 | 创建一个线程池,该线程池可以安排命令在给定的延迟后运行或定期执行 647 | 648 | - Executors.newCachedThreadPool() 649 | 650 | ```java 651 | public static ExecutorService newCachedThreadPool() { 652 | return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 653 | 60L, TimeUnit.SECONDS, 654 | new SynchronousQueue()); 655 | } 656 | ``` 657 | 658 | 创建一个线程池,该线程池根据需要创建新线程,但是将在先前构造的线程可用时重用它们。 这些池通常将提高执行许多短暂的异步任务的程序的性能。 如果可用, execute将重用以前构造的线程。 如果没有可用的现有线程,则将创建一个新线程并将其添加到池中。 六十秒内未使用的线程将终止并从缓存中删除。 因此,保持空闲时间足够长的池不会消耗任何资源。 请注意,可以使用ThreadPoolExecutor构造函数创建具有相似属性但不同细节(例如,超时参数)的ThreadPoolExecutor 。 -------------------------------------------------------------------------------- /docs/notes/java/Java集合总结.md: -------------------------------------------------------------------------------- 1 | # 集合 2 | 3 | # ArrayList 4 | 5 | ArrayList 实现于 List、RandomAccess 接口。可以插入空数据,也支持随机访问。其中最重要的两个属性分别是: elementData 数组,以及 size 大小。 默认初始化容量为10,每次扩容会扩容1.5倍(新容量=旧容量+旧容量>>1)。有序、非线程安全的。 6 | 7 | **执行add(E)方法:** 8 | 9 | ```java 10 | /** 11 | * Appends the specified element to the end of this list. 12 | * 13 | * @param e element to be appended to this list 14 | * @return {@code true} (as specified by {@link Collection#add}) 15 | */ 16 | public boolean add(E e) { 17 | modCount++; 18 | add(e, elementData, size); 19 | return true; 20 | } 21 | 22 | /** 23 | * This helper method split out from add(E) to keep method 24 | * bytecode size under 35 (the -XX:MaxInlineSize default value), 25 | * which helps when add(E) is called in a C1-compiled loop. 26 | */ 27 | private void add(E e, Object[] elementData, int s) { 28 | if (s == elementData.length) 29 | elementData = grow(); 30 | elementData[s] = e; 31 | size = s + 1; 32 | } 33 | ``` 34 | 35 | - 首先记录对该列表进行结构修改的次数 36 | - 然后执行添加元素,默认添加到末尾 37 | - 判断数组的容量是否满了,如果是就先进行扩容 38 | - 将元素添加到指定位置,修改size大小 39 | 40 | **执行add(index,e)方法,添加元素到指定位置:** 41 | 42 | ```java 43 | /** 44 | * Inserts the specified element at the specified position in this 45 | * list. Shifts the element currently at that position (if any) and 46 | * any subsequent elements to the right (adds one to their indices). 47 | * 48 | * @param index index at which the specified element is to be inserted 49 | * @param element element to be inserted 50 | * @throws IndexOutOfBoundsException {@inheritDoc} 51 | */ 52 | public void add(int index, E element) { 53 | rangeCheckForAdd(index); 54 | modCount++; 55 | final int s; 56 | Object[] elementData; 57 | if ((s = size) == (elementData = this.elementData).length) 58 | elementData = grow(); 59 | System.arraycopy(elementData, index, 60 | elementData, index + 1, 61 | s - index); 62 | elementData[index] = element; 63 | size = s + 1; 64 | } 65 | ``` 66 | 67 | - check下标是否越界,并记录列表结构修改次数 68 | - 判断数组是否需要扩容 69 | - 通过System.arraycopy方法复制指定的元素向后移动 70 | - 将添加的元素赋值给指定的下标 ,修改size大小 71 | 72 | **扩容方法grow():** 73 | 74 | ```java 75 | private Object[] grow() { 76 | return grow(size + 1); 77 | } 78 | /** 79 | * Increases the capacity to ensure that it can hold at least the 80 | * number of elements specified by the minimum capacity argument. 81 | * 82 | * @param minCapacity the desired minimum capacity 83 | * @throws OutOfMemoryError if minCapacity is less than zero 84 | */ 85 | private Object[] grow(int minCapacity) { 86 | return elementData = Arrays.copyOf(elementData, 87 | newCapacity(minCapacity)); 88 | } 89 | 90 | /** 91 | * Returns a capacity at least as large as the given minimum capacity. 92 | * Returns the current capacity increased by 50% if that suffices. 93 | * Will not return a capacity greater than MAX_ARRAY_SIZE unless 94 | * the given minimum capacity is greater than MAX_ARRAY_SIZE. 95 | * 96 | * @param minCapacity the desired minimum capacity 97 | * @throws OutOfMemoryError if minCapacity is less than zero 98 | */ 99 | private int newCapacity(int minCapacity) { 100 | // overflow-conscious code 101 | int oldCapacity = elementData.length; 102 | int newCapacity = oldCapacity + (oldCapacity >> 1); 103 | if (newCapacity - minCapacity <= 0) { 104 | if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) 105 | return Math.max(DEFAULT_CAPACITY, minCapacity); 106 | if (minCapacity < 0) // overflow 107 | throw new OutOfMemoryError(); 108 | return minCapacity; 109 | } 110 | return (newCapacity - MAX_ARRAY_SIZE <= 0) 111 | ? newCapacity 112 | : hugeCapacity(minCapacity); 113 | } 114 | 115 | private static int hugeCapacity(int minCapacity) { 116 | if (minCapacity < 0) // overflow 117 | throw new OutOfMemoryError(); 118 | return (minCapacity > MAX_ARRAY_SIZE) 119 | ? Integer.MAX_VALUE 120 | : MAX_ARRAY_SIZE; 121 | } 122 | ``` 123 | 124 | - 主要是通过旧的容量+旧的容量,然后右位移1位来计算新的容量 125 | - 通过容量创建一个新的数组,进行数组复制。 126 | 127 | # Vector 128 | 129 | 底层使用数组实现,扩容方式与List不同,如果初始化Vector时没有指定容量增量,那么会默认扩容2倍(新容量=旧容量+旧容量),如果指定了容量增量,那么扩容的容量就是(新容量=旧容量+容量增量),使用synchronized包装了类的方法,所以是线程安全的。并且有序。 130 | 131 | **执行add(E e)方法:** 132 | 133 | ```java 134 | /** 135 | * Appends the specified element to the end of this Vector. 136 | * 137 | * @param e element to be appended to this Vector 138 | * @return {@code true} (as specified by {@link Collection#add}) 139 | * @since 1.2 140 | */ 141 | public synchronized boolean add(E e) { 142 | modCount++; 143 | add(e, elementData, elementCount); 144 | return true; 145 | } 146 | /** 147 | * This helper method split out from add(E) to keep method 148 | * bytecode size under 35 (the -XX:MaxInlineSize default value), 149 | * which helps when add(E) is called in a C1-compiled loop. 150 | */ 151 | private void add(E e, Object[] elementData, int s) { 152 | if (s == elementData.length) 153 | elementData = grow(); 154 | elementData[s] = e; 155 | elementCount = s + 1; 156 | } 157 | ``` 158 | 159 | - 判断是否需要扩容 160 | - 赋值元素到指定的下标 161 | - 修改容器大小 162 | 163 | # LinkedList 164 | 165 | 采用双向链表实现,get指定索引的值会先对链表的大小进行右位移1,来判断获取的索引值在链表的上半部分还是下半部分,如果是上半部分会从头部节点开始遍历查找,如果是下半部分会从尾部节点开始遍历查找,有序、非线程安全。 166 | 167 | **执行add(E e)方法:** 168 | 169 | ```java 170 | /** 171 | * Appends the specified element to the end of this list. 172 | * 173 | *

This method is equivalent to {@link #addLast}. 174 | * 175 | * @param e element to be appended to this list 176 | * @return {@code true} (as specified by {@link Collection#add}) 177 | */ 178 | public boolean add(E e) { 179 | linkLast(e); 180 | return true; 181 | } 182 | /** 183 | * Links e as last element. 184 | */ 185 | void linkLast(E e) { 186 | final Node l = last; 187 | final Node newNode = new Node<>(l, e, null); 188 | last = newNode; 189 | if (l == null) 190 | first = newNode; 191 | else 192 | l.next = newNode; 193 | size++; 194 | modCount++; 195 | } 196 | ``` 197 | 198 | - 默认添加到链表的最后面,先取出lastNode的一个临时变量。 199 | - 将要添加的元素包装成一个newNode节点,将newNode节点的前置节点设置为当前链表的最后一个节点 200 | - 将newNode设置为新的last节点 201 | - 判断lastNode是否为空,如果为空,那么此时添加的是链表的第一个节点,那么直接设置firstNode等于newNode。如果不是就将newNode链接到lastNode后边 202 | - 修改改链表大小及结构修改次数 203 | 204 | **执行get(int index)方法:** 205 | 206 | ```java 207 | /** 208 | * Returns the element at the specified position in this list. 209 | * 210 | * @param index index of the element to return 211 | * @return the element at the specified position in this list 212 | * @throws IndexOutOfBoundsException {@inheritDoc} 213 | */ 214 | public E get(int index) { 215 | checkElementIndex(index); 216 | return node(index).item; 217 | } 218 | /** 219 | * Returns the (non-null) Node at the specified element index. 220 | */ 221 | Node node(int index) { 222 | // assert isElementIndex(index); 223 | 224 | if (index < (size >> 1)) { 225 | Node x = first; 226 | for (int i = 0; i < index; i++) 227 | x = x.next; 228 | return x; 229 | } else { 230 | Node x = last; 231 | for (int i = size - 1; i > index; i--) 232 | x = x.prev; 233 | return x; 234 | } 235 | } 236 | ``` 237 | 238 | - 先判断要获取的索引是否超出链表大小 239 | - 通过将size左位移一位来判断index是在链表的上半部分还是下半部分 240 | - 如果在上半部分则通过头节点开始遍历查找 241 | - 如果在下半部分则通过尾节点开始遍历查找 242 | 243 | # HashSet 244 | 245 | 底层使用HashMap实现,所有添加到set中的元素最终都会添加到map的key中,value用一个final的object对象填充。无序不重复,非线程安全 246 | 247 | # TreeSet 248 | 249 | 底层使用NavigableMap实现,所有添加到set中的元素最终都会添加到map的key中,value用一个final的object对象填充。有序不重复,非线程安全 250 | 251 | # HashMap 252 | 253 | HashMap初始容量为16的Note数组,数组内的链表容量大于8时会自动转换为红黑树,只有当这个数大于2并值至少有8个才能满足树的假设,当这个值缩小到6的时候就会转换为链表。 254 | 255 | **在hashMap中get操作:** 256 | 257 | ```java 258 | public V get(Object key) { 259 | Node e; 260 | return (e = getNode(hash(key), key)) == null ? null : e.value; 261 | } 262 | 263 | /** 264 | * Implements Map.get and related methods. 265 | * 266 | * @param hash hash for key 267 | * @param key the key 268 | * @return the node, or null if none 269 | */ 270 | final Node getNode(int hash, Object key) { 271 | Node[] tab; Node first, e; int n; K k; 272 | if ((tab = table) != null && (n = tab.length) > 0 && 273 | (first = tab[(n - 1) & hash]) != null) { 274 | if (first.hash == hash && // always check first node 275 | ((k = first.key) == key || (key != null && key.equals(k)))) 276 | return first; 277 | if ((e = first.next) != null) { 278 | if (first instanceof TreeNode) 279 | return ((TreeNode)first).getTreeNode(hash, key); 280 | do { 281 | if (e.hash == hash && 282 | ((k = e.key) == key || (key != null && key.equals(k)))) 283 | return e; 284 | } while ((e = e.next) != null); 285 | } 286 | } 287 | return null; 288 | } 289 | ``` 290 | 291 | - 计算key的hash值,判断get的元素是不是firstNoe,如果直接返回 292 | - 如果不是firstNode,那么判断是否是树,如果是的话通过遍历树查找。 293 | - 否则遍历链表找到key相等的值。 294 | 295 | **在hashMap中put操作:** 296 | 297 | ```java 298 | public V put(K key, V value) { 299 | return putVal(hash(key), key, value, false, true); 300 | } 301 | 302 | /** 303 | * Implements Map.put and related methods. 304 | * 305 | * @param hash hash for key 306 | * @param key the key 307 | * @param value the value to put 308 | * @param onlyIfAbsent if true, don't change existing value 309 | * @param evict if false, the table is in creation mode. 310 | * @return previous value, or null if none 311 | */ 312 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 313 | boolean evict) { 314 | Node[] tab; Node p; int n, i; 315 | if ((tab = table) == null || (n = tab.length) == 0) 316 | n = (tab = resize()).length; 317 | if ((p = tab[i = (n - 1) & hash]) == null) 318 | tab[i] = newNode(hash, key, value, null); 319 | else { 320 | Node e; K k; 321 | if (p.hash == hash && 322 | ((k = p.key) == key || (key != null && key.equals(k)))) 323 | e = p; 324 | else if (p instanceof TreeNode) 325 | e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); 326 | else { 327 | for (int binCount = 0; ; ++binCount) { 328 | if ((e = p.next) == null) { 329 | p.next = newNode(hash, key, value, null); 330 | if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 331 | treeifyBin(tab, hash); 332 | break; 333 | } 334 | if (e.hash == hash && 335 | ((k = e.key) == key || (key != null && key.equals(k)))) 336 | break; 337 | p = e; 338 | } 339 | } 340 | if (e != null) { // existing mapping for key 341 | V oldValue = e.value; 342 | if (!onlyIfAbsent || oldValue == null) 343 | e.value = value; 344 | afterNodeAccess(e); 345 | return oldValue; 346 | } 347 | } 348 | ++modCount; 349 | if (++size > threshold) 350 | resize(); 351 | afterNodeInsertion(evict); 352 | return null; 353 | } 354 | ``` 355 | 356 | - 计算key的hash值,算出元素在底层数组中的下标位置。如果下标位置为空,直接插入。 357 | - 通过下标位置定位到底层数组里的元素(也有可能是链表也有可能是树)。 358 | - 取到元素,判断放入元素的key是否==或equals当前位置的key,成立则替换value值,返回旧值。 359 | - 如果是树,循环树中的节点,判断放入元素的key是否==或equals节点的key,成立则替换树里的value,并返回旧值,不成立就添加到树里。 360 | - 否则就顺着元素的链表结构循环节点,判断放入元素的key是否==或equals节点的key,成立则替换链表里value,并返回旧值,找不到就添加到链表的最后。 361 | - 精简一下,判断放入HashMap中的元素要不要替换当前节点的元素,key满足以下两个条件即可替换: 362 | - **hash值相等。** 363 | - **==或equals的结果为true。** 364 | 365 | # LinkedHashMap 366 | 367 | 主体的实现都是借助于 HashMap 来完成的,只是对其中的 recordAccess(), addEntry(), createEntry() 进行了重写。 368 | 369 | 总的来说 `LinkedHashMap` 其实就是对 `HashMap` 进行了拓展,使用了双向链表来保证了顺序性。因为是继承与 `HashMap` 的,所以一些 `HashMap` 存在的问题 `LinkedHashMap` 也会存在,比如不支持并发等。 370 | 371 | `LinkedHashMap` 的排序方式有两种: 372 | 373 | - 根据写入顺序排序。 374 | - 根据访问顺序排序。 375 | 376 | # ConcurrentHashMap 377 | 378 | ## jdk1.7实现 379 | 380 | ![%E9%9B%86%E5%90%88%206550f84d83244b8e84fe1bf2b22a7720/Untitled.png](https://tva1.sinaimg.cn/large/008eGmZEly1gng6z2y6irj30dw0730v0.jpg) 381 | 382 | 如图所示,是由 `Segment` 数组、`HashEntry` 数组组成,和 `HashMap` 一样,仍然是数组加链表组成。 383 | 384 | `ConcurrentHashMap` 采用了分段锁技术,其中 `Segment` 继承于 `ReentrantLock`。不会像 `HashTable` 那样不管是 `put` 还是 `get` 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 `CurrencyLevel` (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 `Segment` 时,不会影响到其他的 `Segment`。 385 | 386 | ### get方法 387 | 388 | `ConcurrentHashMap` 的 `get` 方法是非常高效的,因为整个过程都不需要加锁。 389 | 390 | 只需要将 `Key` 通过 `Hash` 之后定位到具体的 `Segment` ,再通过一次 `Hash` 定位到具体的元素上。由于 `HashEntry` 中的 `value` 属性是用 `volatile` 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值(**[volatile 相关知识点](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Threadcore.md#%E5%8F%AF%E8%A7%81%E6%80%A7)**)。 391 | 392 | ### put 方法 393 | 394 | 内部利用HashEntry类存储数据。 395 | 396 | 虽然 HashEntry 中的 value 是用 `volatile` 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。 397 | 398 | 首先也是通过 Key 的 Hash 定位到具体的 Segment,在 put 之前会进行一次扩容校验。这里比 HashMap 要好的一点是:HashMap 是插入元素之后再看是否需要扩容,有可能扩容之后后续就没有插入就浪费了本次扩容(扩容非常消耗性能)。 399 | 400 | 而 ConcurrentHashMap 不一样,它是在将数据插入之前检查是否需要扩容,之后再做插入操作。 401 | 402 | ## JDK1.8 实现 403 | 404 | ![%E9%9B%86%E5%90%88%206550f84d83244b8e84fe1bf2b22a7720/Untitled%201.png](https://tva1.sinaimg.cn/large/008eGmZEly1gng6yyf4jmj30lp0drgp3.jpg) 405 | 406 | 1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。其中的 val和next 字段都用了 volatile 修饰,保证了可见性。 407 | 408 | ### put方法 409 | 410 | ![%E9%9B%86%E5%90%88%206550f84d83244b8e84fe1bf2b22a7720/Untitled%202.png](https://tva1.sinaimg.cn/large/008eGmZEly1gng6ytzyv6j30oc0rb7hq.jpg) 411 | 412 | - 根据 key 计算出 hashcode 。 413 | - 判断是否需要进行初始化。 414 | - `f` 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 415 | - 如果当前位置的 `hashcode == MOVED == -1`,则需要进行扩容。 416 | - 如果都不满足,则利用 synchronized 锁写入数据。 417 | - 如果数量大于 `TREEIFY_THRESHOLD` 则要转换为红黑树 418 | 419 | ### get方法 420 | 421 | ![%E9%9B%86%E5%90%88%206550f84d83244b8e84fe1bf2b22a7720/Untitled%203.png](https://tva1.sinaimg.cn/large/008eGmZEly1gng6yo59lpj30o409hwj0.jpg) 422 | 423 | - 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 424 | - 如果是红黑树那就按照树的方式获取值。 425 | - 都不满足那就按照链表的方式遍历获取值。 -------------------------------------------------------------------------------- /docs/notes/java/【Java8系列】Lambda表达式.md: -------------------------------------------------------------------------------- 1 | #### 什么是Lambda? 2 | 3 | Lambda是一个匿名函数,我们可以把Lambda表达式理解为是一段可以传递的代码(将代码像参数一样进行传递,称为行为参数化)。Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中),要做到这一点就需要了解,什么是函数式接口,这里先不做介绍,等下一篇在讲解。 4 | 5 | 首先先看一下lambda长什么样? 正常写法: 6 | 7 | ``` 8 | new Thread(new Runnable() { 9 | @Override 10 | public void run() { 11 | System.out.println("hello lambda"); 12 | } 13 | }).start(); 14 | ``` 15 | 16 | lambda写法: 17 | 18 | ``` 19 | new Thread( 20 | () -> System.out.println("hello lambda") 21 | ).start(); 22 | ``` 23 | 24 | 怎么样?是不是感觉很简洁,没错,这就是lambda的魅力,他可以让你写出来的代码更简单、更灵活。 25 | 26 | #### Lambda怎么写? 27 | 28 | ![](https://user-gold-cdn.xitu.io/2019/7/16/16bf81c30ea9b895?w=798&h=434&f=png&s=59257) 大家看一些上面的这个图,这就是lambda的语法,一个lambda分为三部分:参数列表、操作符、lambda体。以下是lambda表达式的重要特征: 29 | 30 | - `可选类型声明:` 不需要声明参数类型,编译器可以统一识别参数值。也就说(s) -> System.out.println(s)和 (String s) -> System.out.println(s)是一样的编译器会进行类型推断所以不需要添加参数类型。 31 | 32 | - `可选的参数圆括号:` 一个参数无需定义圆括号,但多个参数需要定义圆括号。例如: 33 | 34 | - s -> System.out.println(s) 一个参数不需要添加圆括号。 35 | - (x, y) -> Integer.compare(y, x) 两个参数添加了圆括号,否则编译器报错。 36 | 37 | - `可选的大括号:` 如果主体包含了一个语句,就不需要使用大括号。 38 | 39 | - s -> System.out.println(s) , 不需要大括号. 40 | - (s) -> { if (s.equals("s")){ System.out.println(s); } }; 需要大括号 41 | 42 | - `可选的返回关键字:` 如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。 43 | 44 | Lambda体不加{ }就不用写return: 45 | 46 | ``` 47 | Comparator com = (x, y) -> Integer.compare(y, x); 48 | ``` 49 | 50 | Lambda体加上{ }就需要添加return: 51 | 52 | ``` 53 | Comparator com = (x, y) -> { 54 | int compare = Integer.compare(y, x); 55 | return compare; 56 | }; 57 | ``` 58 | 59 | #### 类型推断 60 | 61 | 上面我们看到了一个lambda表达式应该怎么写,但lambda中有一个重要特征是 `可选参数类型声明`,就是说不用写参数的类型,那么为什么不用写呢?它是怎么知道的参数类型呢?这就涉及到类型推断了。 62 | 63 | **java8的泛型类型推断改进:** 64 | 65 | - 支持通过方法上下文推断泛型目标类型 66 | - 支持在方法调用链路中,泛型类型推断传递到最后一个方法 67 | 68 | ``` 69 | List ps = ... 70 | Stream names = ps.stream().map(p -> p.getName()); 71 | ``` 72 | 73 | 在上面的代码中,ps的类型是List ``,所以ps.stream()的返回类型是Stream ``。map()方法接收一个类型为Function的函数式接口,这里T的类型即是Stream元素的类型,也就是Person,而R的类型未知。由于在重载解析之后lambda表达式的目标类型仍然未知,我们就需要推导R的类型:通过对lambda表达式lambda进行类型检查,我们发现lambda体返回String,因此R的类型是String,因而map()返回Stream ``。绝大多数情况下编译器都能解析出正确的类型,但如果碰到无法解析的情况,我们则需要: 74 | 75 | - 使用显式lambda表达式(为参数p提供显式类型)以提供额外的类型信息 76 | - 把lambda表达式转型为Function 77 | - 为泛型参数R提供一个实际类型。( map(p -> p.getName())) 78 | 79 | #### 方法引用 80 | 81 | 方法引用是用来直接访问类或者实例已经存在的方法或构造方法,提供了一种引用而不执行方法的方式。是一种更简洁更易懂的Lambda表达式,当Lambda表达式中只是执行一个方法调用时,直接使用方法引用的形式可读性更高一些。 方法引用使用 “ :: ” 操作符来表示,左边是类名或实例名,右边是方法名。 `(注意:方法引用::右边的方法名是不需要加()的,例:User::getName)` 82 | 83 | **方法引用的几种形式:** 84 | 85 | - 类 :: 静态方法 86 | - 类 :: 实例方法 87 | - 对象 :: 实例方法 88 | 89 | ``` 90 | 例如: 91 | Consumer consumer = (s) -> System.out.println(s); 92 | 等同于: 93 | Consumer consumer = System.out::println; 94 | 95 | 例如: 96 | Function stringToInteger = (String s) -> Integer.parseInt(s); 97 | 等同于: 98 | Function stringToInteger = Integer::parseInt; 99 | 100 | 例如: 101 | BiPredicate, String> contains = (list, element) -> list.contains(element); 102 | 等同于: 103 | BiPredicate, String> contains = List::contains; 104 | ``` 105 | 106 | **`注意:`** 107 | 108 | - Lambda体中调用方法的参数列表与返回值类型,要与函数式接口中抽象方法的函数列表和返回值类型保存一致 109 | - 若Lambda参数列表中的第一个参数是实例方法的调用者,而第二个参数是实例方法的参数时,可以使用ClassName::method 110 | 111 | #### 构造方法引用 112 | 113 | 语法格式:类名::new 114 | 115 | ``` 116 | 例如: 117 | Supplier supplier = ()->new User(); 118 | 119 | 等同于: 120 | Supplier supplier = User::new; 121 | ``` 122 | 123 | **`注意:`** 需要调用的构造器方法与函数式接口中抽象方法的参数列表保持一致。 124 | 125 | #### Lambda是怎么实现的? 126 | 127 | 研究了半天Lambda怎么写,可是它的原理是什么?我们简单看个例子,看看真相到底是什么: 128 | 129 | ``` 130 | public class StreamTest { 131 | 132 | public static void main(String[] args) { 133 | printString("hello lambda", (String s) -> System.out.println(s)); 134 | 135 | } 136 | 137 | public static void printString(String s, Print print) { 138 | print.print(s); 139 | } 140 | } 141 | 142 | @FunctionalInterface 143 | interface Print { 144 | public void print(T t); 145 | } 146 | ``` 147 | 148 | 上面的代码自定义了一个函数式接口,定义一个静态方法然后用这个函数式接口来接收参数。编写完这个类以后,我们到终端界面javac进行编译,然后用javap(javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。)进行解析,如下图: 149 | 150 | - 执行javap -p 命令 ( -p -private 显示所有类和成员) 151 | 152 | 看上图发现在编译Lambda表达式生成了一个 `lambda$main$0`静态方法,这个静态方法实现了Lambda表达式的逻辑,现在我们知道原来Lambda表达式被编译成了一个静态方法,那么这个静态方式是怎么调用的呢?我们继续进行 153 | 154 | - 执行javap -v -p 命令 ( -v -verbose 输出附加信息) 155 | 156 | ``` 157 | public com.lxs.stream.StreamTest(); 158 | descriptor: ()V 159 | flags: ACC_PUBLIC 160 | Code: 161 | stack=1, locals=1, args_size=1 162 | 0: aload_0 163 | 1: invokespecial #1 // Method java/lang/Object."":()V 164 | 4: return 165 | LineNumberTable: 166 | line 7: 0 167 | 168 | public static void main(java.lang.String[]); 169 | descriptor: ([Ljava/lang/String;)V 170 | flags: ACC_PUBLIC, ACC_STATIC 171 | Code: 172 | stack=2, locals=1, args_size=1 173 | 0: ldc #2 // String hello lambda 174 | 2: invokedynamic #3, 0 // InvokeDynamic #0:print:()Lcom/lxs/stream/Print; 175 | 7: invokestatic #4 // Method printString:(Ljava/lang/String;Lcom/lxs/stream/Print;)V 176 | 10: return 177 | LineNumberTable: 178 | line 10: 0 179 | line 12: 10 180 | 181 | public static void printString(java.lang.String, com.lxs.stream.Print); 182 | descriptor: (Ljava/lang/String;Lcom/lxs/stream/Print;)V 183 | flags: ACC_PUBLIC, ACC_STATIC 184 | Code: 185 | stack=2, locals=2, args_size=2 186 | 0: aload_1 187 | 1: aload_0 188 | 2: invokeinterface #5, 2 // InterfaceMethod com/lxs/stream/Print.print:(Ljava/lang/Object;)V 189 | 7: return 190 | LineNumberTable: 191 | line 15: 0 192 | line 16: 7 193 | Signature: #19 // (Ljava/lang/String;Lcom/lxs/stream/Print;)V 194 | 195 | private static void lambda$main$0(java.lang.String); 196 | descriptor: (Ljava/lang/String;)V 197 | flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC 198 | Code: 199 | stack=2, locals=1, args_size=1 200 | 0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 201 | 3: aload_0 202 | 4: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 203 | 7: return 204 | LineNumberTable: 205 | line 10: 0 206 | } 207 | SourceFile: "StreamTest.java" 208 | InnerClasses: 209 | public static final #58= #57 of #61; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles 210 | BootstrapMethods: 211 | 0: #27 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; 212 | Method arguments: 213 | #28 (Ljava/lang/Object;)V 214 | #29 invokestatic com/lxs/stream/StreamTest.lambda$main$0:(Ljava/lang/String;)V 215 | #30 (Ljava/lang/String;)V 216 | ``` 217 | 218 | 这里只贴出了一部分的字节码结构,由于常量池定义太长了,就没有粘贴。 219 | 220 | ``` 221 | InnerClasses: 222 | public static final #58= #57 of #61; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles 223 | BootstrapMethods: 224 | 0: #27 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; 225 | Method arguments: 226 | #28 (Ljava/lang/Object;)V 227 | #29 invokestatic com/lxs/stream/StreamTest.lambda$main$0:(Ljava/lang/String;)V 228 | #30 (Ljava/lang/String;)V 229 | ``` 230 | 231 | 通过这段字节码结构发现是要生成一个内部类,使用invokestatic调用了一个LambdaMetafactory.metafactory方法,并把 `lambda$main$0`作为参数传了进去,我们来看metafactory 的方法里的实现代码: 232 | 233 | ``` 234 | public static CallSite metafactory(MethodHandles.Lookup caller, 235 | String invokedName, 236 | MethodType invokedType, 237 | MethodType samMethodType, 238 | MethodHandle implMethod, 239 | MethodType instantiatedMethodType) 240 | throws LambdaConversionException { 241 | AbstractValidatingLambdaMetafactory mf; 242 | mf = new InnerClassLambdaMetafactory(caller, invokedType, 243 | invokedName, samMethodType, 244 | implMethod, instantiatedMethodType, 245 | false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY); 246 | mf.validateMetafactoryArgs(); 247 | return mf.buildCallSite(); 248 | } 249 | ``` 250 | 251 | 在buildCallSite的函数中,是函数spinInnerClass 构建了这个内部类。也就是生成了一个StreamTest$$Lambda$1.class这样的内部类,这个类是在运行的时候构建的,并不会保存在磁盘中。 252 | 253 | ``` 254 | @Override 255 | CallSite buildCallSite() throws LambdaConversionException { 256 | final Class innerClass = spinInnerClass(); 257 | 以下省略。。。 258 | } 259 | ``` 260 | 261 | 如果想看到这个构建的类,可以通过设置环境参数 System.setProperty("jdk.internal.lambda.dumpProxyClasses", " . "); 会在你指定的路径 . 当前运行路径上生成这个内部类。我们看下一下生成的类长什么样 ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/7/16/16bf81c31b9be099?w=1772&h=286&f=png&s=297718) 从图中可以看出动态生成的内部类实现了我自定义的函数式接口,并且重写了函数式接口中的方法。 262 | 263 | 我们在javap -v -p StreamTest\$\$Lambda\$1.class看下: 264 | 265 | ``` 266 | { 267 | private com.lxs.stream.StreamTest$$Lambda$1(); 268 | descriptor: ()V 269 | flags: ACC_PRIVATE 270 | Code: 271 | stack=1, locals=1, args_size=1 272 | 0: aload_0 273 | 1: invokespecial #10 // Method java/lang/Object."":()V 274 | 4: return 275 | 276 | public void print(java.lang.Object); 277 | descriptor: (Ljava/lang/Object;)V 278 | flags: ACC_PUBLIC 279 | Code: 280 | stack=1, locals=2, args_size=2 281 | 0: aload_1 282 | 1: checkcast #15 // class java/lang/String 283 | 4: invokestatic #21 // Method com/lxs/stream/StreamTest.lambda$main$0:(Ljava/lang/String;)V 284 | 7: return 285 | RuntimeVisibleAnnotations: 286 | 0: #13() 287 | } 288 | ``` 289 | 290 | 发现在重写的parint方法中使用invokestatic指令调用了lambda$main$0方法。 291 | 292 | **总结:** 这样实现了Lambda表达式,使用invokedynamic指令,运行时调用LambdaMetafactory.metafactory动态的生成内部类,实现了函数式接口,并在重写函数式接口中的方法,在方法内调用 `lambda$main$0`,内部类里的调用方法块并不是动态生成的,只是在原class里已经编译生成了一个静态的方法,内部类只需要调用该静态方法。 -------------------------------------------------------------------------------- /docs/notes/java/【Java8系列】NPE神器Optional.md: -------------------------------------------------------------------------------- 1 | #### Optional类入门 2 | 3 | Optional`` 类(java.util.Optional) 是一个容器类,代表一个值存在或不存在,原来用 null 表示一个值不存在,现在Optional可以更好的表达这个概念。并且可以避免空指针异常。你可以把Optional对象看成一种特殊的集合数据,它至多包含一个元素。 4 | 5 | **常用方法**: 6 | 7 | - Optional.of(T t) : 将指定值用 Optional 封装之后返回,如果该值为 null,则抛出一个 NullPointerException 异常。 8 | - Optional.empty() : 创建一个空的 Optional 实例。 9 | - Optional.ofNullable(T t) : 将指定值用 Optional 封装之后返回,如果该值为 null,则返回一个空的 Optional 对象。 10 | - get() : 如果该值存在,将该值用 Optional 封装返回,否则抛出一个 NoSuchElementException 异常。 11 | - orElse(T t) : 如果调用对象包含值,返回该值,否则返回t。 12 | - orElseGet(Supplier s) : 如果调用对象包含值,返回该值,否则返回 s 获取的值。 13 | - orElseThrow() :它会在对象为空的时候抛出异常。 14 | - map(Function f) : 如果值存在,就对该值执行提供的 mapping 函数调用。 15 | - flatMap(Function mapper) : 如果值存在,就对该值执行提供的mapping 函数调用,返回一个 Optional 类型的值,否则就返回一个空的 Optional 对象。 16 | 17 | `注意:Optional类的设计初衷仅仅是要支持能返回Optional对象的语法,并未考虑作为类的字段使用,也没有实现序列化接口,在领域模型中使用Optional,有可能引发程序故障。` 18 | 19 | #### 使用Optional实战 20 | 21 | 用Optional封装可能为null的值,我们在项目中很多时候都会遇到,掉一个方法然后返回一个null,最后需要不断的判空。比如获取Map中的不含指定键的值,它的get方法返回的就是一个null。 22 | 23 | ```java 24 | //例如: 25 | Object value = map.get("key"); 26 | 27 | //使用Optional封装结果后可以这么写: 28 | Optional value = Optional.ofNullable(map.get("key")); 29 | 30 | /** 31 | * 如果想在获取为null以后给个默认值,可以这么写: 32 | * orElse和orElseGet的区别是当Optional的值是空值时,无论orElse还是orElseGet都会执行;而当返回的 Optional有值时,orElse会执行,而orElseGet不会执行。 33 | */ 34 | Object value = Optional.ofNullable(map.get("key")).orElse("value"); 35 | Object value1 = Optional.ofNullable(map.get("key")).orElseGet(()->"value"); 36 | ``` 37 | 38 | 由于某种原因,函数无法返回某个值,这时除了返回null,Java API比较常见的替代做法是抛出一个异常。这种情况比较典型的例子是使用静态方法Integer.parseInt(String),将String转换为int。在这个例子中,如果String无法解析到对应的整型,该方法就抛出一个NumberFormatException。最后的效果是,发生String无法转换为int时,代码发出一个遭遇非法参数的信号,唯一的不同是,这次你需要使用try/catch 语句,而不是使用if条件判断来控制一个变量的值是否非空。 39 | 40 | 你也可以用空的Optional对象,对遭遇无法转换的String时返回的非法值进行建模,这时你期望parseInt的返回值是一个optional。我们无法修改最初的Java方法,但是这无碍我们进 行需要的改进,你可以实现一个工具方法,将这部分逻辑封装于其中,最终返回一个我们希望的 Optional对象,代码如下所示。 41 | 42 | ```java 43 | public static Optional stringToInt(String s) { 44 | try { 45 | //如果String能转换为对应的Integer,将其封装在Optioal对象中返回 46 | return Optional.of(Integer.parseInt(s)); 47 | } catch (NumberFormatException e) { 48 | //否则返回一个空的Optional对象 49 | return Optional.empty(); 50 | } 51 | } 52 | ``` 53 | 54 | Optional就是讲到这里,这个实在没什么好说的了,大家自己实践吧。 -------------------------------------------------------------------------------- /docs/notes/java/【Java8系列】函数式接口.md: -------------------------------------------------------------------------------- 1 | #### 函数式接口是什么? 2 | 3 | 有且只有一个抽象方法的接口被称为函数式接口,函数式接口适用于函数式编程的场景,Lambda就是Java中函数式编程的体现,可以使用Lambda表达式创建一个函数式接口的对象,一定要确保接口中有且只有一个抽象方法,这样Lambda才能顺利的进行推导。 4 | 5 | #### @FunctionalInterface注解 6 | 7 | 与@Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface 。该注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。但是这个注解不是必须的,只要符合函数式接口的定义,那么这个接口就是函数式接口。 8 | 9 | #### static方法和default方法 10 | 11 | 实在不知道该在哪介绍这两个方法了,所以就穿插在这里了。 12 | 13 | ##### static方法: 14 | 15 | java8中为接口新增了一项功能,定义一个或者多个静态方法。用法和普通的static方法一样,例如: 16 | 17 | ```java 18 | public interface Interface { 19 | /** 20 | * 静态方法 21 | */ 22 | static void staticMethod() { 23 | System.out.println("static method"); 24 | } 25 | } 26 | ``` 27 | 28 | `注意:实现接口的类或者子接口不会继承接口中的静态方法。` 29 | 30 | ##### default方法: 31 | 32 | java8在接口中新增default方法,是为了在现有的类库中中新增功能而不影响他们的实现类,试想一下,如果不增加默认实现的话,接口的所有实现类都要实现一遍这个方法,这会出现兼容性问题,如果定义了默认实现的话,那么实现类直接调用就可以了,并不需要实现这个方法。default方法怎么定义? 33 | 34 | ```java 35 | public interface Interface { 36 | /** 37 | * default方法 38 | */ 39 | default void print() { 40 | System.out.println("hello default"); 41 | } 42 | } 43 | ``` 44 | 45 | `注意:如果接口中的默认方法不能满足某个实现类需要,那么实现类可以覆盖默认方法。不用加default关键字,`例如: 46 | 47 | ```java 48 | public class InterfaceImpl implements Interface { 49 | @Override 50 | public void print() { 51 | System.out.println("hello default 2"); 52 | } 53 | } 54 | ``` 55 | 56 | 在函数式接口的定义中是只允许有一个抽象方法,但是可以有多个static方法和default方法。 57 | 58 | 59 | #### 自定义函数式接口 60 | 61 | 按照下面的格式定义,你也能写出函数式接口: 62 | 63 | ```java 64 | @FunctionalInterface 65 | 修饰符 interface 接口名称 { 66 | 返回值类型 方法名称(可选参数信息); 67 | // 其他非抽象方法内容 68 | } 69 | ``` 70 | 71 | 虽然@FunctionalInterface注解不是必须的,但是自定义函数式接口最好还是都加上,一是养成良好的编程习惯,二是防止他人修改,一看到这个注解就知道是函数式接口,避免他人往接口内添加抽象方法造成不必要的麻烦。 72 | 73 | ```java 74 | @FunctionalInterface 75 | public interface MyFunction { 76 | void print(String s); 77 | } 78 | ``` 79 | 80 | 看上图是我自定义的一个函数式接口,那么这个接口的作用是什么呢?就是输出一串字符串,属于消费型接口,是模仿Consumer接口写的,只不过这个没有使用泛型,而是将参数具体类型化了,不知道Consumer没关系,下面会介绍到,其实java8中提供了很多常用的函数式接口,Consumer就是其中之一,一般情况下都不需要自己定义,直接使用就好了。那么怎么使用这个自定义的函数式接口呢?我们可以用函数式接口作为参数,调用时传递Lambda表达式。如果一个方法的参数是Lambda,那么这个参数的类型一定是函数式接口。例如: 81 | 82 | ```java 83 | public class MyFunctionTest { 84 | public static void main(String[] args) { 85 | String text = "试试自定义函数好使不"; 86 | printString(text, System.out::print); 87 | } 88 | 89 | private static void printString(String text, MyFunction myFunction) { 90 | myFunction.print(text); 91 | } 92 | } 93 | ``` 94 | 95 | 执行以后就会输出“试试自定义函数好使不”这句话,如果某天需求变了,我不想输出这句话了,想输出别的,那么直接替换text就好了。函数式编程是没有副作用的,最大的好处就是函数的内部是无状态的,既输入确定输出就确定。函数式编程还有更多好玩的套路,这就需要靠大家自己探索了。😝 96 | 97 | #### 常用函数式接口 98 | 99 | ##### Consumer``:消费型接口 100 | 101 | **抽象方法:** void accept(T t),接收一个参数进行消费,但无需返回结果。 102 | 103 | **使用方式:** 104 | 105 | ```java 106 | Consumer consumer = System.out::println; 107 | consumer.accept("hello function"); 108 | ``` 109 | 110 | **默认方法:** andThen(Consumer after),先消费然后在消费,先执行调用andThen接口的accept方法,然后在执行andThen方法参数after中的accept方法。 111 | 112 | **使用方式:** 113 | 114 | ```java 115 | Consumer consumer1 = s -> System.out.print("车名:"+s.split(",")[0]); 116 | Consumer consumer2 = s -> System.out.println("-->颜色:"+s.split(",")[1]); 117 | 118 | String[] strings = {"保时捷,白色", "法拉利,红色"}; 119 | for (String string : strings) { 120 | consumer1.andThen(consumer2).accept(string); 121 | } 122 | ``` 123 | 124 | **输出:** 125 | 车名:保时捷-->颜色:白色 126 | 车名:法拉利-->颜色:红色 127 | 128 | ##### Supplier``: 供给型接口 129 | 130 | **抽象方法**:T get(),无参数,有返回值。 131 | 132 | **使用方式:** 133 | 134 | ```java 135 | Supplier supplier = () -> "我要变的很有钱"; 136 | System.out.println(supplier.get()); 137 | ``` 138 | 139 | 最后输出就是“我要变得很有钱”,这类接口适合提供数据的场景。 140 | 141 | ##### Function``: 函数型接口 142 | 143 | **抽象方法:** R apply(T t),传入一个参数,返回想要的结果。 144 | 145 | **使用方式:** 146 | 147 | ```java 148 | Function function1 = e -> e * 6; 149 | System.out.println(function1.apply(2)); 150 | ``` 151 | 152 | 很简单的一个乘法例子,显然最后输出是12。 153 | 154 | **默认方法:** 155 | 156 | - compose(Function before),先执行compose方法参数before中的apply方法,然后将执行结果传递给调用compose函数中的apply方法在执行。 157 | 158 | **使用方式:** 159 | 160 | ```java 161 | Function function1 = e -> e * 2; 162 | Function function2 = e -> e * e; 163 | 164 | Integer apply2 = function1.compose(function2).apply(3); 165 | System.out.println(apply2); 166 | ``` 167 | 168 | 还是举一个乘法的例子,compose方法执行流程是先执行function2的表达式也就是3*3=9,然后在将执行结果传给function1的表达式也就是9*2=18,所以最终的结果是18。 169 | 170 | - andThen(Function after),先执行调用andThen函数的apply方法,然后在将执行结果传递给andThen方法after参数中的apply方法在执行。它和compose方法整好是相反的执行顺序。 171 | 172 | **使用方式:** 173 | 174 | ```java 175 | Function function1 = e -> e * 2; 176 | Function function2 = e -> e * e; 177 | 178 | Integer apply3 = function1.andThen(function2).apply(3); 179 | System.out.println(apply3); 180 | ``` 181 | 182 | 这里我们和compose方法使用一个例子,所以是一模一样的例子,由于方法的不同,执行顺序也就不相同,那么结果是大大不同的。andThen方法是先执行function1表达式,也就是3*2=6,然后在执行function2表达式也就是6*6=36。结果就是36。 183 | **静态方法:**identity(),获取一个输入参数和返回结果相同的Function实例。 184 | 185 | **使用方式:** 186 | 187 | ```java 188 | Function identity = Function.identity(); 189 | Integer apply = identity.apply(3); 190 | System.out.println(apply); 191 | ``` 192 | 193 | 平常没有遇到过使用这个方法的场景,总之这个方法的作用就是输入什么返回结果就是什么。 194 | 195 | ##### Predicate`` : 断言型接口 196 | 197 | **抽象方法:** boolean test(T t),传入一个参数,返回一个布尔值。 198 | 199 | **使用方式:** 200 | 201 | ```java 202 | Predicate predicate = t -> t > 0; 203 | boolean test = predicate.test(1); 204 | System.out.println(test); 205 | ``` 206 | 207 | 当predicate函数调用test方法的时候,就会执行拿test方法的参数进行t -> t > 0的条件判断,1肯定是大于0的,最终结果为true。 208 | 209 | **默认方法:** 210 | 211 | - and(Predicate other),相当于逻辑运算符中的&&,当两个Predicate函数的返回结果都为true时才返回true。 212 | 213 | **使用方式:** 214 | 215 | ```java 216 | Predicate predicate1 = s -> s.length() > 0; 217 | Predicate predicate2 = Objects::nonNull; 218 | boolean test = predicate1.and(predicate2).test("&&测试"); 219 | System.out.println(test); 220 | ``` 221 | 222 | - or(Predicate other) ,相当于逻辑运算符中的||,当两个Predicate函数的返回结果有一个为true则返回true,否则返回false。 223 | 224 | **使用方式:** 225 | 226 | ```java 227 | Predicate predicate1 = s -> false; 228 | Predicate predicate2 = Objects::nonNull; 229 | boolean test = predicate1.and(predicate2).test("||测试"); 230 | System.out.println(test); 231 | ``` 232 | 233 | - negate(),这个方法的意思就是取反。 234 | 235 | **使用方式:** 236 | 237 | ```java 238 | Predicate predicate = s -> s.length() > 0; 239 | boolean result = predicate.negate().test("取反"); 240 | System.out.println(result); 241 | ``` 242 | 243 | 很明显正常执行test方法的话应该为true,但是调用negate方法后就返回为false了。 244 | **静态方法:**isEqual(Object targetRef),对当前操作进行"="操作,即取等操作,可以理解为 A == B。 245 | 246 | **使用方式:** 247 | 248 | ```java 249 | boolean test1 = Predicate.isEqual("test").test("test"); 250 | boolean test2 = Predicate.isEqual("test").test("equal"); 251 | System.out.println(test1); //true 252 | System.out.println(test2); //false 253 | ``` 254 | 255 | #### 其他函数式接口 256 | 257 | ##### Bi类型接口 258 | 259 | BiConsumer、BiFunction、BiPrediate 是 Consumer、Function、Predicate 的扩展,可以传入多个参数,没有 BiSupplier 是因为 Supplier 没有入参。 260 | 261 | ##### 操作基本数据类型的接口 262 | 263 | IntConsumer、IntFunction、IntPredicate、IntSupplier、LongConsumer、LongFunction、LongPredicate、LongSupplier、DoubleConsumer、DoubleFunction、DoublePredicate、DoubleSupplier。 264 | 其实常用的函数式接口就那四大接口Consumer、Function、Prediate、Supplier,其他的函数式接口就不一一列举了,有兴趣的可以去java.util.function这个包下详细的看。 -------------------------------------------------------------------------------- /docs/notes/java/【Java8系列】流式编程Stream.md: -------------------------------------------------------------------------------- 1 | #### 什么是Stream? 2 | 3 | Stream它并不是一个容器,它只是对容器的功能进行了增强,添加了很多便利的操作,例如查找、过滤、分组、排序等一系列的操作。并且有串行、并行两种执行模式,并行模式充分的利用了多核处理器的优势,使用fork/join框架进行了任务拆分,同时提高了执行速度。简而言之,Stream就是提供了一种高效且易于使用的处理数据的方式。 4 | 5 | - 特点: 6 | 7 | 1. Stream自己不会存储元素。 8 | 2. Stream的操作不会改变源对象。相反,他们会返回一个持有结果的新Stream。 9 | 3. Stream 操作是延迟执行的。它会等到需要结果的时候才执行。也就是执行终端操作的时候。 10 | 11 | - 图解: 12 | ![](https://user-gold-cdn.xitu.io/2019/7/22/16c198ac61885693?w=1090&h=436&f=png&s=38873) 13 | 一个Stream的操作就如上图,在一个管道内,分为三个步骤,第一步是创建Stream,从集合、数组中获取一个流,第二步是中间操作链,对数据进行处理。第三步是终端操作,用来执行中间操作链,返回结果。 14 | 15 | #### 怎么创建Stream? 16 | 17 | - 由集合创建: 18 | Java8 中的 Collection 接口被扩展,提供了两个获取流的方法,这两个方法是default方法,也就是说所有实现Collection接口的接口都不需要实现就可以直接使用: 19 | 20 | 1. default Stream stream() : 返回一个顺序流。 21 | 22 | 2. default Stream parallelStream() : 返回一个并行流。 23 | 24 | ```java 25 | 例如: 26 | List integerList = new ArrayList<>(); 27 | integerList.add(1); 28 | integerList.add(2); 29 | Stream stream = integerList.stream(); 30 | Stream stream1 = integerList.parallelStream(); 31 | ``` 32 | 33 | - 由数组创建: 34 | Java8 中的 Arrays 的静态方法 stream() 可以获取数组流: 35 | 36 | 1. static Stream stream(T[] array): 返回一个流 37 | 38 | 2. 重载形式,能够处理对应基本类型的数组: 39 | 40 | public static IntStream stream(int[] array) 41 | 42 | public static LongStream stream(long[] array) 43 | 44 | public static DoubleStream stream(double[] array) 45 | 46 | ```java 47 | 例如: 48 | int[] intArray = {1,2,3}; 49 | IntStream stream = Arrays.stream(intArray); 50 | ``` 51 | 52 | - 由值创建: 53 | 可以使用静态方法 Stream.of(), 通过显示值 创建一个流。它可以接收任意数量的参数。 54 | 55 | 1. public static Stream of(T... values) : 返回一个流。 56 | 57 | ``` 58 | 例如: 59 | Stream integerStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8); 60 | ``` 61 | 62 | - 由函数创建:创建无限流 63 | 可以使用静态方法 Stream.iterate() 和 Stream.generate()创建无限流。 64 | 65 | 1. 迭代 66 | public static Stream iterate(final T seed, final UnaryOperator f) 67 | 68 | 2. 生成 69 | public static Stream generate(Supplier s) 70 | 71 | ``` 72 | 例如: 73 | Stream.generate(Math::random).limit(5).forEach(System.out::print); 74 | List collect = Stream.iterate(0,i -> i + 1).limit(5).collect(Collectors.toList()); 75 | ``` 76 | 77 | `注意:使用无限流一定要配合limit截断,不然会无限制创建下去。` 78 | 79 | #### Stream的中间操作 80 | 81 | 如果Stream只有中间操作是不会执行的,当执行终端操作的时候才会执行中间操作,这种方式称为延迟加载或惰性求值。多个中间操作组成一个中间操作链,只有当执行终端操作的时候才会执行一遍中间操作链,具体是因为什么我们在后面再说明。下面看下Stream有哪些中间操作。 82 | 83 | - Stream`` distinct(): 84 | 去重,通过流所生成元素的 hashCode() 和 equals() 去除重复元素。 85 | ![](https://user-gold-cdn.xitu.io/2019/7/22/16c1a26c389e630e?w=403&h=205&f=jpeg&s=30693) 86 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1ce3dc266d996?w=1464&h=1346&f=png&s=269768) 87 | - Stream`` filter(Predicate predicate): 88 | Predicate函数在上一篇当中我们已经讲过,它是断言型接口,所以filter方法中是接收一个和Predicate函数对应Lambda表达式,返回一个布尔值,从流中过滤某些元素。 89 | ![](https://user-gold-cdn.xitu.io/2019/7/22/16c1a2418a587f98?w=403&h=206&f=jpeg&s=26361) 90 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1ce5ccf0970d0?w=1464&h=1310&f=png&s=263394) 91 | - Stream`` sorted(Comparator comparator): 92 | 指定比较规则进行排序。 93 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1ce00179fc96a?w=1464&h=1310&f=png&s=264409) 94 | - Stream`` limit(long maxSize): 95 | 截断流,使其元素不超过给定数量。如果元素的个数小于maxSize,那就获取所有元素。 96 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1ccc8ee241e40?w=403&h=205&f=jpeg&s=30573) 97 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1cf0a4636351b?w=1572&h=1346&f=png&s=290113) 98 | - Stream`` skip(long n): 99 | 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补。 100 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1ccc8eff0c379?w=403&h=205&f=jpeg&s=26993) 101 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1cf3c9ddadd22?w=1724&h=1346&f=png&s=299965) 102 | - Stream`` map(Function mapper): 103 | 接收一个Function函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。也就是转换操作,map还有三个应用于具体类型方法,分别是:mapToInt,mapToLong和mapToDouble。这三个方法也比较好理解,比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的Stream中的元素都是int类型。这三个方法可以免除自动装箱/拆箱的额外消耗。 104 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1ccc8f03ae276?w=403&h=202&f=bmp&s=325678) 105 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1cfdc901cae3f?w=1220&h=1454&f=png&s=269788) 106 | - Stream`` flatMap(Function> mapper): 107 | 接收一个Function函数作为参数,将流中的每个值都转换成另一个流,然后把所有流连接成一个流。flatMap也有三个应用于具体类型的方法,分别是:flatMapToInt、flatMapToLong、flatMapToDouble,其作用于map的三个衍生方法相同。 108 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1ccc8f01d9be4?w=403&h=205&f=jpeg&s=30536) 109 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1d233eb214db4?w=1870&h=2066&f=png&s=451994) 110 | 111 | #### Stream的终端操作 112 | 113 | 终端操作执行中间操作链,并返回结果。终端操作我们就不一一介绍了,只介绍一下常用的操作。详细可看java.util.stream.Stream接口中的方法。 114 | 115 | - void forEach(Consumer action): 116 | 内部迭代(需要用户去做迭代,称为外部迭代。相反,Stream API使用内部迭代帮你把迭代做了) 117 | 118 | ```java 119 | users.stream().forEach(user -> System.out.println(user.getName())); 120 | ``` 121 | 122 | - R collect(Collector collector): 123 | 收集、将流转换为其他形式,比如转换成List、Set、Map。collect方法是用Collector作为参数,Collector接口中方法的实现决定了如何对流执行收集操作(如收集到 List、Set、Map)。但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例。例举一些常用的: 124 | 125 | ```java 126 | List users = Lists.newArrayList(); 127 | users.add(new User(15, "A", ImmutableList.of("1元", "5元"))); 128 | users.add(new User(25, "B", ImmutableList.of("10元", "50元"))); 129 | users.add(new User(21, "C", ImmutableList.of("100元"))); 130 | //收集名称到List 131 | List nameList = users.stream().map(User::getName).collect(Collectors.toList()); 132 | //收集名称到List 133 | Set nameSet = users.stream().map(User::getName).collect(Collectors.toSet()); 134 | //收集到map,名字作为key,user对象作为value 135 | Map userMap = users.stream() 136 | .collect(Collectors.toMap(User::getName, Function.identity(), (k1, k2) -> k2)); 137 | ``` 138 | 139 | - 其他终端操作: 140 | 141 | 1. boolean allMatch(Predicate predicate); 检查是否匹配所有元素。 142 | 2. boolean anyMatch(Predicate predicate); 检查是否至少匹配一个元素。 143 | 3. boolean noneMatch(Predicate predicate); 检查是否没有匹配所有元素。 144 | 4. Optional`` findFirst(); 返回当前流中的第一个元素。 145 | 5. Optional`` findAny(); 返回当前流中的任意元素。 146 | 6. long count(); 返回流中元素总数。 147 | 7. Optional`` max(Comparator comparator); 返回流中最大值。 148 | 8. Optional`` min(Comparator comparator); 返回流中最小值。 149 | 9. T reduce(T identity, BinaryOperator`` accumulator); 可以将流中元素反复结合起来,得到一个值。 返回 T。这是一个归约操作。 150 | 151 | #### Fork/Join框架 152 | 153 | 上面我们提到过,说Stream的并行模式使用了Fork/Join框架,这里简单说下Fork/Join框架是什么?Fork/Join框架是java7中加入的一个并行任务框架,可以将任务拆分为多个小任务,每个小任务执行完的结果在合并成为一个结果。在任务的执行过程中使用工作窃取(work-stealing)算法,减少线程之间的竞争。 154 | 155 | - Fork/Join图解 156 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1e40b7eac4b89?w=800&h=426&f=png&s=85559) 157 | - 工作窃取图解 158 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1e414bbc9585b?w=844&h=294&f=png&s=82893) 159 | 160 | #### Stream是怎么实现的 161 | 162 | 先看下整体类图:蓝色箭头代表继承,绿色箭头代表实现,红色箭头代表内部类。 163 | ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1ef0992b6e39c?w=982&h=854&f=png&s=760332) 164 | 实际上Stream只有两种操作,中间操作、终端操作,中间操作只是一种标记,只有终端操作才会实际触发执行。所以Stream流水线式的操作大致应该是用某种方式记录中间操作,只有调用终端操作才会将所有的中间操作叠加在一起在一次迭代中全部执行。这里只做简单的介绍,想详细了解的可以参考下面的参考资料中的链接。 165 | 166 | - 操作怎么记录?
167 | Stream的操作记录是通过ReferencePipeline记录的,ReferencePipeline有三个内部类Head、StatelessOp、StatefulOp,Stream中使用Stage的概念来描述一个完整的操作,并用某种实例化后的ReferencePipeline来代表Stage,Head用于表示第一个Stage,即调用诸如Collection.stream()方法产生的Stage,很显然这个Stage里不包含任何操作,StatelessOp和StatefulOp分别表示无状态和有状态的Stage,对应于无状态和有状态的中间操作。 168 | ![](https://user-gold-cdn.xitu.io/2019/7/24/16c1f94ee4740035?w=1000&h=265&f=png&s=37101) 169 | - 操作怎么叠加?
170 | 操作是记录完了,但是前面的Stage并不知道后面Stage到底执行了哪种操作,以及回调函数是哪种形式。这就需要有某种协议来协调相邻Stage之间的调用关系。 171 | 这种协议由Sink接口完成,Sink接口包含的方法如下表所示: 172 | 173 | 1. void begin(long size),开始遍历元素之前调用该方法,通知Sink做好准备。 174 | 2. void end(),所有元素遍历完成之后调用,通知Sink没有更多的元素了。 175 | 3. boolean cancellationRequested(),是否可以结束操作,可以让短路操作尽早结束。 176 | 4. void accept(T t),遍历元素时调用,接受一个待处理元素,并对元素进行处理。Stage把自己包含的操作和回调方法封装到该方法里,前一个Stage只需要调用当前Stage.accept(T t)方法就行了。
177 | 178 | 每个Stage都会将自己的操作封装到一个Sink里,前一个Stage只需调用后一个Stage的accept()方法即可,并不需要知道其内部是如何处理的。有了Sink对操作的包装,Stage之间的调用问题就解决了,执行时只需要从流水线的head开始对数据源依次调用每个Stage对应的Sink.{begin(), accept(), cancellationRequested(), end()}方法就可以了。 179 | 180 | - 操作怎么执行? 181 | ![](https://user-gold-cdn.xitu.io/2019/7/24/16c1fac591c62598?w=600&h=701&f=png&s=60869) 182 | Sink完美封装了Stream每一步操作,并给出了[处理->转发]的模式来叠加操作。这一连串的齿轮已经咬合,就差最后一步拨动齿轮启动执行。是什么启动这一连串的操作呢?也许你已经想到了启动的原始动力就是结束操作(Terminal Operation),一旦调用某个结束操作,就会触发整个流水线的执行。 183 | 184 | #### 参考资料 185 | 186 | https://ifeve.com/stream
187 | https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/
188 | https://segmentfault.com/a/1190000016781127
189 | https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/6-Stream%20Pipelines.md
-------------------------------------------------------------------------------- /docs/notes/jvm/JVM 进阶 | java字节码.md: -------------------------------------------------------------------------------- 1 | > 点击 [blog](https://github.com/xiaoshuanglee/blog)即可查看原文和更多的文章,欢迎star。 2 | 3 | ## 什么是Java字节码 4 | 5 | ![img](https://tva1.sinaimg.cn/large/0081Kckwly1gl7g063uc6j30oy0dpdgf.jpg) 6 | 7 | Java字节码是由(.Java)文件编译成(.class)的文件。之所以叫字节码是因为(.class)文件是由十六进制组成的。而JVM以两个十六进制值为一组,即以字节为单位进行读取。java之所以能够做到一次编译、到处运行,就是因为不同的平台都会编译成相同的(.class)文件,所以才能在不同的平台执行。这种跨平台执行的实现,极大的提高了开发和维护的成本。 8 | 9 | ## 怎么查看字节码 10 | 11 | 查看字节码有很多种方法,网上也有一些插件可以查看。我们这里直说一种就是通过javap命令来查看。 12 | 13 | 先通过javap -help来查看下这个命令怎么使用: 14 | 15 | ```java 16 | 用法: javap 17 | 其中, 可能的选项包括: 18 | -? -h --help -help 输出此帮助消息 19 | -version 版本信息 20 | -v -verbose 输出附加信息 21 | -l 输出行号和本地变量表 22 | -public 仅显示公共类和成员 23 | -protected 显示受保护的/公共类和成员 24 | -package 显示程序包/受保护的/公共类 25 | 和成员 (默认) 26 | -p -private 显示所有类和成员 27 | -c 对代码进行反汇编 28 | -s 输出内部类型签名 29 | -sysinfo 显示正在处理的类的 30 | 系统信息 (路径, 大小, 日期, MD5 散列) 31 | -constants 显示最终常量 32 | --module <模块>, -m <模块> 指定包含要反汇编的类的模块 33 | --module-path <路径> 指定查找应用程序模块的位置 34 | --system 指定查找系统模块的位置 35 | --class-path <路径> 指定查找用户类文件的位置 36 | -classpath <路径> 指定查找用户类文件的位置 37 | -cp <路径> 指定查找用户类文件的位置 38 | -bootclasspath <路径> 覆盖引导类文件的位置 39 | 40 | ``` 41 | 42 | 接下来我们定义一个简单的类 43 | 44 | ```java 45 | /** 46 | * @author: lixiaoshuang 47 | * @create: 2020-11-30 19:57 48 | **/ 49 | public class HelloByteCode { 50 | public static void main(String[] args) { 51 | int a = 1; 52 | int b = 2; 53 | int c = a + b; 54 | System.out.println(c); 55 | } 56 | } 57 | ``` 58 | 59 | ![image-20201130215033695](https://tva1.sinaimg.cn/large/0081Kckwly1gl7jfcifq4j30l608m0vp.jpg) 60 | 61 | 然后执行 **javac HelloByteCode.java** ,这样就的到了HelloByteCode.class文件,它就是我们所说的字节码文件。编译完成后我们先用文本工具打开(.class)文件看下: 62 | 63 | ![image-20201130220049549](https://tva1.sinaimg.cn/large/0081Kckwly1gl7jq31pybj30oe0ri43u.jpg) 64 | 65 | ### 魔数 66 | 67 | 打开后是一堆十六进制数,可以看到上图中用蓝色框标记起来的cafe babe就是魔数,所有的字节码文件都是以这个为开头的,魔数的固定值为:0xCAFEBABE,魔数放在文件的开头是为了让jvm识别这个文件是不是一个.class文件,如果不是就不会进行下一步的操作。 68 | 69 | ### 版本号 70 | 71 | 同样还是上边的字节码图,黄色框圈起来的是版本号,0000 0037,0000为次版本号,0037位主版本,次版本号转化为十进制为0,主版本号转化为十进制为55,通过Oracle官网查询可知,55对应的版本号是jdk 11。 72 | 73 | 74 | 75 | ### 查看反编译 76 | 77 | 接下来使用 **javap -v -l -c HelloByteCode** 命令对(.class)文件进行反编译。具体每一块是干什么我在图中详细的标出来了,大家可以仔细看下面的图片。就不一一介绍了,主要包括版本号、访问标志、接口信息、常量池、方法描述、操作指令、行号表、本地变量表(图中没有体现出来,大家可以用命令将本地变量表输出出来自己看下) 78 | 79 | ![](https://tva1.sinaimg.cn/large/0081Kckwly1gl7lhcf1ugj30u015jteh.jpg) 80 | 81 | ![](https://tva1.sinaimg.cn/large/0081Kckwly1gl870t2lelj31bi0hc76t.jpg) 82 | 83 | -------------------------------------------------------------------------------- /docs/notes/jvm/JVM 进阶 | 基础知识.md: -------------------------------------------------------------------------------- 1 | > 点击 [blog](https://github.com/xiaoshuanglee/blog)即可查看原文和更多的文章,欢迎star。 2 | 3 | # **1. JDK、JRE、JVM的关系** 4 | 5 | ## **1.1 JDK** 6 | 7 | JDK(Java Development Kit) 是用于开发 Java 应用程序的软件开发工具集合,包括 了 Java 运行时的环境(JRE)、解释器(Java)、编译器(javac)、Java 归档 (jar)、文档生成器(Javadoc)等工具。简单的说我们要开发Java程序,就需要安装某个版本的JDK工具包。 8 | 9 | ## **1.2 JRE** 10 | 11 | JRE(Java Runtime Enviroment )提供 Java 应用程序执行时所需的环境,由 Java 虚拟机(JVM)、核心类、支持文件等组成。简单的说,我们要是想在某个机器上运 行Java程序,可以安装JDK,也可以只安装JRE,后者体积比较小。 12 | 13 | ## **1.3 JVM** 14 | 15 | Java Virtual Machine(Java 虚拟机)有三层含义,分别是: 16 | 17 | JVM规范要求 18 | 19 | 满足 JVM 规范要求的一种具体实现(一种计算机程序) 20 | 21 | 一个 JVM 运行实例,在命令提示符下编写 Java 命令以运行 Java 类时,都会创建一 个 JVM 实例,我们下面如果只记到JVM则指的是这个含义;如果我们带上了某种JVM 的名称,比如说是Zing JVM,则表示上面第二种含义 22 | 23 | ## **1.4 JDK 与 JRE、JVM 之间的关系** 24 | 25 | 就范围来说,JDK > JRE > JVM: 26 | 27 | - JDK = JRE + 开发工具 28 | - JRE = JVM + 类库 29 | 30 | ![img](https://tva1.sinaimg.cn/large/0081Kckwly1gk505kaff4j30lr0h574x.jpg) 31 | 32 | ![img](https://tva1.sinaimg.cn/large/0081Kckwly1gk505iwhpoj30pe0jeta2.jpg) 33 | 34 | Java程序的开发运行过程为: 35 | 36 | 我们利用 JDK (调用 Java API)开发Java程序,编译成字节码或者打包程序 然后可以用 JRE 则启动一个JVM实例,加载、验证、执行 Java 字节码以及依赖库, 运行Java程序。 37 | 38 | 而JVM 将程序和依赖库的Java字节码解析并变成本地代码执行,产生结果 。 39 | 40 | ## **1.5 如果不知道自动安装/别人安装的JDK在哪个目录怎么办?** 41 | 42 | 最简单/最麻烦的查询方式是询问相关人员。 43 | 44 | 查找的方式很多,比如,可以使用 which , whereis , ls ‐l 跟踪软连接, 或者 find 命令全局查找(可能需要sudo权限), 例如: 45 | 46 | - jps ‐v 47 | - whereis javac 48 | - ls ‐l /usr/bin/javac 49 | - find / ‐name javac 50 | 51 | # **2. 常用性能指标** 52 | 53 | > **没有量化就没有改进** 54 | 55 | - 分析系统性能问题: 比如是不是达到了我们预期性能指标,判断资源层面有没有问题,JVM层面有没有问题,系统的关键处理流程有没有问题,业务流程是否需要优化 56 | - 通过工具收集系统的状态,日志,包括打点做内部的指标收集,监控并得出关键性能指标数据,也包括进行压测,得到一些相关的压测数据和性能内部分析数据 57 | - 根据分析结果和性能指标,进行资源配置调整,并持续进行监控和分析,以优化性能,直到满足系统要求,达到系统的最佳性能状态 58 | 59 | ## **2.1 计算机系统中,性能相关的资源主要分为这几类:** 60 | 61 | - CPU:CPU是系统最关键的计算资源,在单位时间内有限,也是比较容易由于业务逻辑处理不合理而出现瓶颈的地方,浪费了CPU资源和过渡消耗CPU资源都不 是理想状态,我们需要监控相关指标; 62 | - 内存:内存则对应程序运行时直接可使用的数据快速暂存空间,也是有限的,使用过程随着时间的不断的申请内存又释放内存,好在JVM的GC帮我们处理了这些事情,但是如果GC配置的不合理,一样会在一定的时间后,产生包括OOM宕 机之类的各种问题,所以内存指标也需要关注; 63 | - IO(存储+网络):CPU在内存中把业务逻辑计算以后,为了长期保存,就必须通过磁盘存储介质持久化,如果多机环境、分布式部署、对外提供网络服务能 力,那么很多功能还需要直接使用网络,这两块的IO都会比CPU和内存速度更慢,所以也是我们关注的重点。 64 | 65 | ## **2.2 性能优化中常见的套路** 66 | 67 | 性能优化一般要存在瓶颈问题,而瓶颈问题都遵循80/20原则。既我们把所有的整个处理过程中比较慢的因素都列一个清单,并按照对性能的影响排序,那么前20%的瓶颈问题,至少会对性能的影响占到80%比重。换句话说,我们优先解决了最重要的几个问题,那么性能就能好一大半。 68 | 69 | 我们一般先排查基础资源是否成为瓶颈。看资源够不够,只要成本允许,加配置可能是最快速的解决方案,还可能是最划算,最有效的解决方案。 与JVM有关的系统资源,主要是 CPU 和 内存 这两部分。 如果发生资源告警/不足, 就需要评估系统容量,分析原因。 70 | 71 | 一般衡量系统性能的维度有3个: 72 | 73 | - 延迟(Latency): 一般衡量的是响应时间(Response Time),比如平均响应时间。 但是有时候响应时间抖动的特别厉害,也就是说有部分用户的响应时间特别高, 这时我们一般假设我们要保障95%的用户在可接受的范围内响应,从而提供绝大多数用户具有良好的用户体验,这就是延迟的95线(P95,平均100个用户请求中95个已经响应的时间),同理还有99线,最大响应时间等(95线和99线比较常用;用户访问量大的时候,对网络有任何抖动都可能会导致最大响应时间变得非常大,最大响应时间这个指标不可控,一般不用)。 74 | - 吞吐量(Throughput): 一般对于交易类的系统我们使用每秒处理的事务数(TPS) 来衡量吞吐能力,对于查询搜索类的系统我们也可以使用每秒处理的请求数 (QPS)。 75 | - 系统容量(Capacity): 也叫做设计容量,可以理解为硬件配置,成本约束。 76 | 77 | **性能指标还可分为两类:** 78 | 79 | - 业务需求指标:如吞吐量(QPS、TPS)、响应时间(RT)、并发数、业务成功率等。 80 | - 资源约束指标:如CPU、内存、I/O等资源的消耗情况。 81 | 82 | ## **2.3性能调优总结** 83 | 84 | ![img](https://tva1.sinaimg.cn/large/0081Kckwly1gk505jcfqfj30nz07c3yv.jpg) 85 | 86 | 性能调优的第一步是制定指标,收集数据,第二步是找瓶颈,然后分析解决瓶颈问题。通过这些手段,找当前的性能极限值。压测调优到不能再优化了的 TPS和QPS, 就是极限值。知道了极限值,我们就可以按业务发展测算流量和系统压力,以此做容量规划,准备机器资源和预期的扩容计划。最后在系统的日常运行过程中,持续观察,逐步重做和调整以上步骤,长期改善改进系统性能。 87 | 88 | 我们经常说“ 脱离场景谈性能都是耍流氓 ”,实际的性能分析调优过程中,我们需要根据具体的业务场景,综合考虑成本和性能,使用最合适的办法去处理。系统的性能优化到3000TPS如果已经可以在成本可以承受的范围内满足业务发展的需求,那么再花几个人月优化到3100TPS就没有什么意义,同样地如果花一倍成本去优化到5000TPS 也没有意义。 89 | 90 | Donald Knuth曾说过“ 过早的优化是万恶之源 ”,我们需要考虑在恰当的时机去优化系统。在业务发展的早期,量不大,性能没那么重要。我们做一个新系统,先考虑整体设计是不是OK,功能实现是不是OK,然后基本的功能都做得差不多的时候(当然整体的框架是不是满足性能基准,可能需要在做项目的准备阶段就通过POC(概念证明)阶段验证。),最后再考虑性能的优化工作。因为如果一开始就考虑优化,就可 能要想太多导致过度设计了。而且主体框架和功能完成之前,可能会有比较大的改动,一旦提前做了优化,可能这些改动导致原来的优化都失效了,又要重新优化,多做了很多无用功。 91 | 92 | # **3. JVM基础知识** 93 | 94 | ## **3.1 常见的编程语言类型** 95 | 96 | 首先,我们可以把形形色色的编程从底向上划分为最基本的三大类:机器语言、汇编 语言、高级语言。 97 | 98 | ![img](https://tva1.sinaimg.cn/large/0081Kckwly1gk505m4vgxj30db0dpwer.jpg) 99 | 100 | 按《计算机编程语言的发展与应用》一文里的定义:计算机编程语言能够实现人与机器之间的交流和沟通,而计算机编程语言主要包括汇编语言、机器语言以及高级语言,具体内容如下: 101 | 102 | - 机器语言:这种语言主要是利用二进制编码进行指令的发送,能够被计算机快速地识别,其灵活性相对较高,且执行速度较为可观,机器语言与汇编语言之间的相似性较高,但由于具有局限性,所以在使用上存在一定的约束性。 103 | - 汇编语言:该语言主要是以缩写英文作为标符进行编写的,运用汇编语言进行编 写的一般都是较为简练的小程序,其在执行方面较为便利,但汇编语言在程序方面较为冗长,所以具有较高的出错率。 104 | - 高级语言:所谓的高级语言,其实是由多种编程语言结合之后的总称,其可以对多条指令进行整合,将其变为单条指令完成输送,其在操作细节指令以及中间过 程等方面都得到了适当的简化,所以,整个程序更为简便,具有较强的操作性, 而这种编码方式的简化,使得计算机编程对于相关工作人员的专业水平要求不断放宽。 105 | 106 | ## **3.2 高级语言分类** 107 | 108 | - 如果按照有没有虚拟机来划分,高级编程语言可分为两类: 109 | 110 | - 有虚拟机:Java,Lua,Ruby,部分JavaScript的实现等等 111 | - 无虚拟机:C,C++,C#,Golang,以及大部分常见的编程语言 112 | 113 | - 如果按照变量是不是有确定的类型,还是类型可以随意变化来划分,高级编程语言可 以分为: 114 | 115 | - 静态类型:Java,C,C++等等 116 | - 动态类型:所有脚本类型的语言 117 | 118 | - 如果按照是编译执行,还是解释执行,可以分为: 119 | 120 | - 编译执行:C,C++,Golang,Rust,C#,Java,Scala,Clojure,Kotlin, Swift...等等 121 | - 解释执行:JavaScript的部分实现和NodeJS,Python,Perl,Ruby...等等 122 | 123 | - 此外,我们还可以按照语言特点分类: 124 | 125 | - 面向过程:C,Basic,Pascal,Fortran等等 126 | - 面向对象:C++,Java,Ruby,Smalltalk等等 127 | - 函数式编程:LISP、Haskell、Erlang、OCaml、Clojure、F#等等 128 | 129 | 有的甚至可以划分为纯面向对象语言,例如Ruby,所有的东西都是对象(Java不是所有东西都是对象,比如基本类型 int 、 long 等等,就不是对象,但是它们的包装 类 Integer 、 Long 则是对象)。 还有既可以当做编译语言又可以当做脚本语言的,例如Groovy等语言。 130 | 131 | ## **3.3 关于跨平台** 132 | 133 | 现在我们聊聊跨平台,为什么要跨平台,因为我们希望所编写的代码和程序,在源代 码级别或者编译后,可以运行在多种不同的系统平台上,而不需要为了各个平台的不 同点而去实现两套代码。典型地,我们编写一个web程序,自然希望可以把它部署到 Windows平台上,也可以部署到Linux平台上,甚至是MacOS系统上。 这就是跨平台的能力,极大地节省了开发和维护成本,赢得了商业市场上的一致好评。 134 | 135 | 这样来看,一般来说解释型语言都是跨平台的,同一份脚本代码,可以由不同平台上的解释器解释执行。但是对于编译型语言,存在两种级别的跨平台: 源码跨平台和二进制跨平台。 136 | 137 | 1、典型的源码跨平台(C++): 138 | 139 | ![img](https://tva1.sinaimg.cn/large/0081Kckwly1gk505iexr8j30nr0et74z.jpg) 140 | 141 | 142 | 143 | 2、典型的二进制跨平台(Java字节码): 144 | 145 | ![img](https://tva1.sinaimg.cn/large/0081Kckwly1gk505kqidfj30oy0dp750.jpg) 146 | 147 | 148 | 149 | 可以看到,C++里我们需要把一份源码,在不同平台上分别编译,生成这个平台相关的二进制可执行文件,然后才能在相应的平台上运行。 这样就需要在各个平台都有开发工具和编译器,而且在各个平台所依赖的开发库都需要是一致或兼容的。 这一点在过去的年代里非常痛苦,被戏称为 “依赖地狱”。 C++的口号是“一次编写,到处(不同平台)编译”,但实际情况上是一编译就报错,变 成了 “一次编写,到处调试,到处找依赖、改配置”。 大家可以想象,你编译一份代 码,发现缺了几十个依赖,到处找还找不到,或者找到了又跟本地已有的版本不兼 容,这是一件怎样令人绝望的事情。 150 | 151 | 而Java语言通过虚拟机技术率先解决了这个难题。 源码只需要编译一次,然后把编译 后的class文件或jar包,部署到不同平台,就可以直接通过安装在这些系统中的JVM上 面执行。 同时可以把依赖库(jar文件)一起复制到目标机器,慢慢地又有了可以在各个平台都直接使用的Maven中央库(类似于linux里的yum或apt­get源,macos里的 homebrew,现代的各种编程语言一般都有了这种包依赖管理机制:python的pip, dotnet的nuget,NodeJS的npm,golang的dep,rust的cargo等等)。这样就实现了 让同一个应用程序在不同的平台上直接运行的能力。 152 | 153 | 总结一下跨平台: 154 | 155 | - 脚本语言直接使用不同平台的解释器执行,称之为脚本跨平台,平台间的差异由 不同平台上的解释器去解决。这样的话代码很通用,但是需要解释和翻译,效率较低。 156 | - 编译型语言的代码跨平台,同一份代码,需要被不同平台的编译器编译成相应的二进制文件,然后再去分发和执行,不同平台间的差异由编译器去解决。编译产 生的文件是直接针对平台的可执行指令,运行效率很高。但是在不同平台上编译 复杂软件,依赖配置可能会产生很多环境方面问题,导致开发和维护的成本较 高。 157 | - 编译型语言的二进制跨平台,同一份代码,先编译成一份通用的二进制文件,然后分发到不同平台,由虚拟机运行时来加载和执行,这样就会综合另外两种跨平台语言的优势,方便快捷地运行于各种平台,虽然运行效率可能比起本地编译类 型语言要稍低一点。 而这些优缺点也是Java虚拟机的优缺点。 158 | 159 | ## **3.4 关于运行时(Runtime)与虚拟机(VM)** 160 | 161 | 我们前面提到了很多次 Java运行时 和 JVM虚拟机 ,简单的说JRE就是Java的运行 时,包括虚拟机和相关的库等资源。 可以说运行时提供了程序运行的基本环境,JVM在启动时需要加载所有运行时的核心库等资源,然后再加载我们的应用程序字节码,才能让应用程序字节码运行在JVM这 个容器里。 162 | 163 | 但也有一些语言是没有虚拟机的,编译打包时就把依赖的核心库和其他特性支持,一 起静态打包或动态链接到程序中,比如Golang和Rust,C#等。 这样运行时就和程序指令组合在一起,成为了一个完整的应用程序,好处就是不需要虚拟机环境,坏处是编译后的二进制文件没法直接跨平台了。 164 | 165 | ## **3.5 关于内存管理和垃圾回收(GC)** 166 | 167 | 内存管理就是内存的生命周期管理,包括内存的申请、压缩、回收等操作。 Java的内存管理就是GC,JVM的GC模块不仅管理内存的回收,也负责内存的分配和压缩整理。 168 | 169 | # 4. Java字节码 170 | 171 | Java中的字节码,英文名为 bytecode , 是Java代码编译后的中间代码格式。JVM需要读取并解析字节码才能执行相应的任务。 由单字节( byte )的指令组成, 理论上最多支持 256 个操作码(opcode)。实际上Java只使用了200左右的操作码, 还有一些操作码则保留给调试操作。 172 | 173 | 操作码, 下面称为指令 , 主要由类型前缀和操作名称两部分组成。 174 | 175 | > 例如,' i ' 前缀代表 ‘ integer ’,所以,' iadd ' 很容易理解, 表示对整数执行加法运算。 176 | 177 | ## 4.1 根据指令的性质,主要分为四个大类: 178 | 179 | - 栈操作指令,包括与局部变量交互的指令 180 | - 程序流程控制指令 181 | - 对象操作指令,包括方法调用指令 182 | - 算数运算以及类型转换指令 183 | 184 | 此外还有一些执行专门任务的指令,比如同步(synchronization)指令,以及抛出异常相关的指令等等 185 | 186 | ## 4.2 对象初始化指令:new指令, init 以及 clinit 简介 187 | 188 | 我们都知道 new 是Java编程语言中的一个关键字, 但其实在字节码中,也有一个指令叫做 new 。 当我们创建类的实例时, 编译器会生成类似下面这样的操作码: 189 | 190 | ``` 191 | ​``` 192 | 0: new #2 // class demo/jvm0104/HelloByteCode 193 | 3: dup 194 | 4: invokespecial #3 // Method "":()V 195 | ​``` 196 | ``` 197 | 198 | 当你同时看到 new, dup 和 invokespecial 指令在一起时,那么一定是在创建类的实例对象! 为什么是三条指令而不是一条呢?这是因为: 199 | 200 | - new 指令只是创建对象,但没有调用构造函数。 201 | - invokespecial 指令用来调用某些特殊方法的, 当然这里调用的是构造函数。 202 | - dup 指令用于复制栈顶的值。 203 | - 由于构造函数调用不会返回值,所以如果没有dup指令, 在对象上调用方法并初始化之后,操作数栈就会是空的,在初始化之后就会出问题, 接下来的代码就无法对其进行处理。 204 | 205 | 在调用构造函数的时候,其实还会执行另一个类似的方法 ,甚至在执行构造函数之前就执行了。还有一个可能执行的方法是该类的静态初始化方法 ,但 并不能被直接调用,而是由这些指令触发的: new , getstatic , putstatic or invokestatic。 206 | 207 | ## 4.3 栈内存操作指令 208 | 209 | 有很多指令可以操作方法栈。 前面也提到过一些基本的栈操作指令: 他们将值压入栈,或者从栈中获取值。 除了这些基础操作之外也还有一些指令可以操作栈内存; 比如 swap 指令用来交换栈顶两个元素的值。下面是一些示例: 210 | 211 | 最基础的是 dup 和 pop 指令。 212 | 213 | - dup 指令复制栈顶元素的值。 214 | - pop 指令则从栈中删除最顶部的值。 215 | 216 | 还有复杂一点的指令:比如, swap , dup_x1 和 dup2_x1 。 217 | 218 | - 顾名思义, swap 指令可交换栈顶两个元素的值,例如A和B交换位置(图中示例 4); 219 | - dup_x1 将复制栈顶元素的值,并在插入在最上面两个值后(图中示例5); 220 | - dup2_x1 则复制栈顶两个元素的值,并插入最上面三个值后(图中示例6)。 221 | 222 | ![img](https://tva1.sinaimg.cn/large/0081Kckwly1gk505hx4hdj313s0lojtm.jpg) 223 | 224 | dup , dup_x1 , dup2_x1 指令补充说明 : 225 | 226 | - dup 指令:官方说明是,复制栈顶的值, 并将复制的值压入栈. 227 | - dup_x1 指令 : 官方说明是,复制栈顶的值, 并将复制的值插入到最上面2个值的下方。 228 | - dup2_x1 指令: 官方说明是,复制栈顶 1个64位/或2个32位的值, 并将复制的值按照原始顺序,插入原始值下面一个32位值的下方。 229 | 230 | # 5. 算术运算指令与类型转换指令 231 | 232 | Java字节码中有许多指令可以执行算术运算。实际上,指令集中有很大一部分表示都是关于数学运算的。对于所有数值类型( int , long , double , float ),都有加, 减,乘,除,取反的指令。 那么 byte 和 char , boolean 呢? JVM 是当做 int 来处理的。另外还有部分指令用于数据类型之间的转换。 233 | 234 | ![img](https://tva1.sinaimg.cn/large/0081Kckwly1gk505jrwabj31360ps0ve.jpg) 235 | 236 | 当我们想将 int 类型的值赋值给 long 类型的变量时,就会发生类型转换。 237 | 238 | ![img](https://tva1.sinaimg.cn/large/0081Kckwly1gk505l9l7ej311y0mwgnk.jpg) 239 | 240 | 241 | 242 | # 6. 方法调用指令和参数传递 243 | 244 | - invokestatic ,顾名思义,这个指令用于调用某个类的静态方法,这也是方法调用指令中最快的一个。 245 | - invokespecial , 我们已经学过了, invokespecial 指令用来调用构造函数, 但也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。 246 | - invokevirtual ,如果是具体类型的目标对象, invokevirtual 用于调用公共,受保护和打包私有方法。 247 | - invokeinterface ,当要调用的方法属于某个接口时,将使用invokeinterface 指令。 248 | 249 | > 那么 invokevirtual 和 invokeinterface 有什么区别呢?这确实是个好问 题。 为什么需要 invokevirtual 和 invokeinterface 这两种指令呢? 毕竟 所有的接口方法都是公共方法, 直接使用 invokevirtual 不就可以了吗? 这么做是源于对方法调用的优化。JVM必须先解析该方法,然后才能调用它 250 | 251 | - 使用 invokestatic 指令,JVM就确切地知道要调用的是哪个方法:因为调用的是静态方法,只能属于一个类。 252 | - 使用 invokespecial 时, 查找的数量也很少, 解析也更加容易,那么运行时就能更快地找到所需的方法。 253 | - ava虚拟机的字节码指令集在JDK7之前一直就只有前面提到的4种指令 (invokestatic,invokespecial,invokevirtual,invokeinterface)。随着JDK 7的发 布,字节码指令集新增了 invokedynamic 指令。这条新增加的指令是实现“动态类型 语言”(Dynamically Typed Language)支持而进行的改进之一,同时也是JDK 8以后 支持的lambda表达式的实现基础。 254 | 255 | # 7. Java类加载器 256 | 257 | ## 7.1 类的生命周期和加载过程 258 | 259 | ![img](https://tva1.sinaimg.cn/large/0081Kckwly1gk505n5cokj31bi0pyjuh.jpg) 260 | 261 | 一个类在JVM里的生命周期有7个阶段,分别是加载(Loading)、验证 (Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)、卸载(Unloading)。 其中前五个部分(加载,验证,准备,解析,初始化)统称为类加载,下面我们就分 别来说一下这五个过程。 262 | 263 | ### 7.1.1 加载 264 | 265 | 加载阶段也可以称为“装载”阶段。 这个阶段主要的操作是: 根据明确知道的class完全限定名, 来获取二进制classfile格式的字节流,简单点说就是 找到文件系统中/jar包中/或存在于任何地方的“ class文件 ”。 如果找不到二进制表示形式,则会抛出NoClassDefFound 错误。装载阶段并不会检查 classfile 的语法和格式。类加载的整个过程主要由JVM和Java 的类加载系统共同完成, 当然具体到loading 阶 段则是由JVM与具体的某一个类加载器(java.lang.classLoader)协作完成的。 266 | 267 | ### 7.1.2 校验 268 | 269 | 链接过程的第一个阶段是校验 ,确保class文件里的字节流信息符合当前虚拟机的要求,不会危害虚拟机的安全。校验过程检classfile 的语义,判断常量池中的符号,并执行类型检查, 主要目的是判断字节码的合法性,比如 magic number, 对版本号进行验证。 这些检查 过程中可能会抛出 VerifyError , ClassFormatError 或 UnsupportedClassVersionError 。 因为classfile的验证属是链接阶段的一部分,所以这个过程中可能需要加载其他类, 在某个类的加载过程中,JVM必须加载其所有的超类和接口。 如果类层次结构有问题(例如,该类是自己的超类或接口,死循环了),则JVM将抛出 ClassCircularityError 。 而如果实现的接口并不是一个 interface,或者声明的超类是一个 interface,也会抛出 IncompatibleClassChangeError 。 270 | 271 | ### 7.1.3 准备 272 | 273 | 然后进入准备阶段,这个阶段将会创建静态字段, 并将其初始化为标准默认值(比如 null 或者 0值 ),并分配方法表,即在方法区中分配这些变量所使用的内存空间。 请注意,准备阶段并未执行任何Java代码。 274 | 275 | 例如: 276 | 277 | ``` 278 | public static int i = 1; 279 | ``` 280 | 281 | 在准备阶段 i 的值会被初始化为0,后面在类初始化阶段才会执行赋值为1; 但是下面如果使用final作为静态常量,某些JVM的行为就不一样了: 282 | 283 | ``` 284 | public static final int i = 1; 285 | ``` 286 | 287 | 对应常量i,在准备阶段就会被赋值1,其实这样还是比较puzzle,例如其他语言 (C#)有直接的常量关键字const,让告诉编译器在编译阶段就替换成常量,类似 于宏指令,更简单。 288 | 289 | ### 7.1.4 解析 290 | 291 | 然后进入可选的解析符号引用阶段。 也就是解析常量池,主要有以下四种:类或接口的解析、字段解析、类方法解析、接 口方法解析。 292 | 293 | 简单的来说就是我们编写的代码中,当一个变量引用某个对象的时候,这个引用在 .class 文件中是以符号引用来存储的(相当于做了一个索引记录)。 在解析阶段就需要将其解析并链接为直接引用(相当于指向实际对象)。如果有了直 接引用,那引用的目标必定在堆中存在。加载一个class时, 需要加载所有的super类和super接口。 294 | 295 | ### 7.1.5 初始化 296 | 297 | JVM规范明确规定, 必须在类的首次“主动使用”时才能执行类初始化。 初始化的过程包括执行: 298 | 299 | - 类构造器方法 300 | - static静态变量赋值语句 301 | - static静态代码块 302 | 303 | 如果是一个子类进行初始化会先对其父类进行初始化,保证其父类在子类之前进行初 始化。所以其实在java中初始化一个类,那么必然先初始化过 java.lang.Object 类,因为所有的java类都继承自java.lang.Object。 304 | 305 | ## 7.2 类加载时机 306 | 307 | 了解了类的加载过程,我们再看看类的初始化何时会被触发呢?JVM 规范枚举了下述多种触发情况: 308 | 309 | - 当虚拟机启动时,初始化用户指定的主类,就是启动执行的 main方法所在的类; 310 | - 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类,就是 new一个类的时候要初始化 311 | - 当遇到调用静态方法的指令时,初始化该静态方法所在的类; 312 | - 当遇到访问静态字段的指令时,初始化该静态字段所在的类; 313 | - 子类的初始化会触发父类的初始化; 314 | - 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化; 315 | - 使用反射 API 对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化; 316 | - 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。 317 | 318 | 同时以下几种情况不会执行类初始化: 319 | 320 | - 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。 321 | - 定义对象数组,不会触发该类的初始化。 322 | - 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。 323 | - 通过类名获取Class对象,不会触发类的初始化,Hello.class不会让Hello类初始化。 324 | - 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。 Class.forName(“jvm.Hello”)默认会加载Hello类。 325 | - 通过ClassLoader默认的loadClass方法,也不会触发初始化动作(加载了,但是不初始化)。 326 | 327 | ### 7.3 类加载机制 328 | 329 | 类加载过程可以描述为“通过一个类的全限定名a.b.c.XXClass来获取描述此类的Class 对象”,这个过程由“类加载器(ClassLoader)”来完成。这样的好处在于,子类加载器可以复用父加载器加载的类。系统自带的类加载器分为三种 : 330 | 331 | ![img](https://tva1.sinaimg.cn/large/0081Kckwly1gk505hgpgrj31cw0somyw.jpg) 332 | 333 | 334 | 335 | - 启动类加载器(BootstrapClassLoader) 336 | 337 | ​ 启动类加载器(bootstrap class loader): 它用来加载 Java 的核心类,是用原生 C++代码来实现的,并不继承自 338 | 339 | ​ java.lang.ClassLoader(负责加载JDK中 jre/lib/rt.jar里所有的class)。它可以看做是JVM自带的,我们再代码层面无法直接获取到 340 | 341 | ​ 启动类加载器的引用,所以不允许直接操作它, 如果打印出来就是个 null 。举例来说,java.lang.String是由启动类加载器加载 342 | 343 | ​ 的,所以 String.class.getClassLoader()就会返回null。但是后面可以看到可以通过命令行 参数影响它加载什么。 344 | 345 | - 扩展类加载器(ExtClassLoader) 346 | 347 | - ​ 扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext 或者由java.ext.dirs系统属性指定的目录中的JAR包的类,代码里直接获取它的父 类加载器为null(因为无法拿到启动类加载器)。 348 | 349 | - 应用类加载器(AppClassLoader) 350 | 351 | - ​ 应用类加载器(app class loader):它负责在JVM启动时加载来自Java命令的­classpath或者­cp选项、java.class.path系统属性指定的jar包和类路径。在应用程序代码里可以通过ClassLoader的静态方法getSystemClassLoader()来获取应用类加载器。如果没有特别指定,则在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载。 352 | 353 | **类加载机制有三个特点:** 354 | 355 | - 双亲委托:当一个自定义类加载器需要加载一个类,比如java.lang.String,它很懒,不会一上来就直接试图加载它,而是先委托自己的父加载器去加载,父加载 器如果发现自己还有父加载器,会一直往前找,这样只要上级加载器,比如启动类加载器已经加载了某个类比如java.lang.String,所有的子加载器都不需要自己加载了。如果几个类加载器都没有加载到指定名称的类,那么会抛出 ClassNotFountException异常。 356 | - 负责依赖:如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖项。 357 | - 缓存加载:为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。 358 | 359 | ![](https://tva1.sinaimg.cn/large/0081Kckwly1gk60dxz3wuj31b60swwsk.jpg) -------------------------------------------------------------------------------- /docs/notes/mybatis/Mybatis入门.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | > 本系列文章Github [后端进阶指南 ](https://github.com/CodeGeekLee/data-structures-and-algorithms)已收录,此项目正在完善中,欢迎star。 4 | 5 | ## 认识MyBatis 6 | 7 | mybatis参考网址:http://www.mybatis.org/mybatis-3/zh/index.html 8 | 9 | Github源码地址:https://github.com/mybatis/mybatis-3 10 | 11 | ### Mybatis是什么 12 | 13 | MyBatis 是一款优秀的**持久层框架**,它支持定制化SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC代码和手动设置参数以及获取结果集,它可以使用简单的**XML**或**注解**来配置和映射SQL信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。 14 | 15 | ### Mybatis的由来 16 | 17 | - MyBatis 本是apache的一个开源项目iBatis。 18 | - 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。 19 | - 2013年11月迁移到Github。 20 | 21 | ### ORM是什么 22 | 23 | 对象-关系映射(OBJECT/RELATIONALMAPPING,简称ORM),是随着面向对象的[软件开发方法](https://baike.baidu.com/item/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91%E6%96%B9%E6%B3%95)发展而产生的。用来把对象模型表示的对象映射到基于SQL 的关系模型数据库结构中去。这样,我们在具体的操作实体对象的时候,就不需要再去和复杂的 SQL 语句打交道,只需简单的操作实体对象的属性和方法 。ORM 技术是在对象和关系之间提供了一条桥梁,前台的对象型数据和数据库中的关系型的数据通过这个桥梁来相互转化。 24 | 25 | ### ORM框架和MyBatis的区别 26 | 27 | | 对比项 | Mybatis | Hibernate | 28 | | ------------ | ---------------------- | ---------------------- | 29 | | 市场占有率 | 高 | 高 | 30 | | 适合的行业 | 互联网 电商 项目 | 传统的(ERP CRM OA) | 31 | | 性能 | 高 | 低 | 32 | | Sql灵活性 | 高 | 低 | 33 | | 学习门槛 | 低 | 高 | 34 | | Sql配置文件 | 全局配置文件、映射文件 | 全局配置文件、映射文件 | 35 | | ORM | 半自动化 | 完全的自动化 | 36 | | 数据库无关性 | 低 | 高 | 37 | 38 | ## 编码流程 39 | 40 | 1. 编写全局配置文件:xxxConfig.xml 41 | 2. POJO类 42 | 3. 映射文件:xxxMapper.xml 43 | 4. 编写dao代码:xxxDao接口、xxxDaoImpl实现类 44 | 5. 单元测试类 45 | 46 | ## 需求 47 | 48 | 1、根据用户id查询一个用户信息 49 | 50 | 2、根据用户名称模糊查询用户信息列表 51 | 52 | 3、添加用户 53 | 54 | ## 项目搭建 55 | 56 | - 创建maven工程:mybatis-demo 57 | - POM文件 58 | 59 | ```xml 60 | 61 | 62 | 63 | org.mybatis 64 | mybatis 65 | 3.4.6 66 | 67 | 68 | 69 | mysql 70 | mysql-connector-java 71 | 5.1.35 72 | 73 | 74 | 75 | 76 | junit 77 | junit 78 | 4.12 79 | 80 | 81 | 82 | ``` 83 | 84 | - SqlMapConfig.xml 85 | 86 | ```xml 87 | 88 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | ``` 110 | 111 | - UserMapper.xml 112 | 113 | ```xml 114 | 115 | 118 | 119 | 120 | ``` 121 | 122 | - PO类 123 | 124 | ```java 125 | public class User { 126 | 127 | private int id; 128 | private String username; 129 | private Date birthday; 130 | private String sex; 131 | private String address; 132 | // getter\setter方法 133 | } 134 | ``` 135 | 136 | 137 | 138 | ## 需求实现 139 | 140 | ### 查询用户 141 | 142 | #### 映射文件 143 | 144 | ```xml 145 | 146 | 149 | 150 | 151 | 155 | ``` 156 | 157 | **配置说明:** 158 | 159 | ``` 160 | - parameterType:定义输入参数的Java类型, 161 | - resultType:定义结果映射类型。 162 | - #{}:相当于JDBC中的?占位符 163 | - #{id}表示使用preparedstatement设置占位符号并将输入变量id传到sql。 164 | - ${value}:取出参数名为value的值。将${value}占位符替换。 165 | 166 | 注意:如果是取简单数量类型的参数,括号中的参数名称必须为value 167 | ``` 168 | 169 | #### dao接口和实现类 170 | 171 | ```java 172 | public interface UserDao { 173 | public User findUserById(int id) throws Exception; 174 | public List findUsersByName(String name) throws Exception; 175 | } 176 | ``` 177 | 178 | - 生命周期(作用范围) 179 | 180 | 1. sqlsession:方法级别 181 | 2. sqlsessionFactory:全局范围(应用级别) 182 | 3. sqlsessionFactoryBuilder:方法级别 183 | 184 | ```java 185 | public class UserDaoImpl implements UserDao { 186 | //注入SqlSessionFactory 187 | public UserDaoImpl(SqlSessionFactory sqlSessionFactory){ 188 | this. sqlSessionFactory = sqlSessionFactory; 189 | } 190 | 191 | private SqlSessionFactory sqlSessionFactory; 192 | 193 | @Override 194 | public User findUserById(int id) throws Exception { 195 | SqlSession session = sqlSessionFactory.openSession(); 196 | User user = null; 197 | try { 198 | //通过sqlsession调用selectOne方法获取一条结果集 199 | //参数1:指定定义的statement的id,参数2:指定向statement中传递的参数 200 | user = session.selectOne("test.findUserById", id); 201 | System.out.println(user); 202 | } finally{ 203 | session.close(); 204 | } 205 | return user; 206 | } 207 | 208 | 209 | @Override 210 | public List findUsersByName(String name) throws Exception { 211 | SqlSession session = sqlSessionFactory.openSession(); 212 | List users = null; 213 | try { 214 | users = session.selectList("test.findUsersByName", name); 215 | System.out.println(users); 216 | } finally{ 217 | session.close(); 218 | } 219 | return users; 220 | } 221 | } 222 | ``` 223 | 224 | 225 | 226 | #### 测试代码 227 | 228 | ```java 229 | public class MybatisTest { 230 | 231 | private SqlSessionFactory sqlSessionFactory; 232 | 233 | @Before 234 | public void init() throws Exception { 235 | SqlSessionFactoryBuilder sessionFactoryBuilder = new SqlSessionFactoryBuilder(); 236 | InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml"); 237 | sqlSessionFactory = sessionFactoryBuilder.build(inputStream); 238 | } 239 | 240 | @Test 241 | public void testFindUserById() { 242 | UserDao userDao = new UserDaoImpl(sqlSessionFactory); 243 | User user = userDao.findUserById(22); 244 | System.out.println(user); 245 | } 246 | @Test 247 | public void testFindUsersByName() { 248 | UserDao userDao = new UserDaoImpl(sqlSessionFactory); 249 | List users = userDao.findUsersByName("老郭"); 250 | System.out.println(users); 251 | } 252 | } 253 | ``` 254 | 255 | 256 | 257 | #### #{}和${}区别 258 | 259 | - 区别1 260 | 261 | ``` 262 | #{} :相当于JDBC SQL语句中的占位符? (PreparedStatement) 263 | 264 | ${} : 相当于JDBC SQL语句中的连接符合 + (Statement) 265 | ``` 266 | 267 | - 区别2 268 | 269 | ``` 270 | #{} : 进行输入映射的时候,会对参数进行类型解析(如果是String类型,那么SQL语句会自动加上’’) 271 | 272 | ${} :进行输入映射的时候,将参数原样输出到SQL语句中 273 | ``` 274 | 275 | - 区别3 276 | 277 | ``` 278 | #{} : 如果进行简单类型(String、Date、8种基本类型的包装类)的输入映射时,#{}中参数名称可以任意 279 | 280 | ${} : 如果进行简单类型(String、Date、8种基本类型的包装类)的输入映射时,${}中参数名称必须是value 281 | ``` 282 | 283 | - 区别4 284 | 285 | ``` 286 | ${} :存在SQL注入问题 ,使用OR 1=1 关键字将查询条件忽略 287 | ``` 288 | 289 | 290 | 291 | ### 添加用户 292 | 293 | **#{}:是通过反射获取数据的---StaticSqlSource** 294 | 295 | **${}:是通过OGNL表达式会随着对象的嵌套而相应的发生层级变化 --DynamicSqlSource** 296 | 297 | #### 映射文件 298 | 299 | ```xml 300 | 301 | 302 | insert into user(username,birthday,sex,address) 303 | values(#{username},#{birthday},#{sex},#{address}) 304 | 305 | ``` 306 | 307 | #### dao接口和实现类 308 | 309 | ```java 310 | public interface UserDao { 311 | public void insertUser(User user) throws Exception; 312 | } 313 | ``` 314 | 315 | 316 | 317 | ```java 318 | public class UserDaoImpl implements UserDao { 319 | //注入SqlSessionFactory 320 | public UserDaoImpl(SqlSessionFactory sqlSessionFactory){ 321 | this. sqlSessionFactory = sqlSessionFactory; 322 | } 323 | 324 | private SqlSessionFactory sqlSessionFactory; 325 | 326 | @Override 327 | Public void insertUser(User user) throws Exception { 328 | SqlSession sqlSession = sqlSessionFactory.openSession(); 329 | try { 330 | sqlSession.insert("test.insertUser", user); 331 | sqlSession.commit(); 332 | } finally{ 333 | session.close(); 334 | } 335 | } 336 | } 337 | 338 | ``` 339 | 340 | 341 | 342 | #### 测试代码 343 | 344 | ```java 345 | @Override 346 | Public void insertUser(User user) throws Exception { 347 | SqlSession sqlSession = sqlSessionFactory.openSession(); 348 | try { 349 | sqlSession.insert("insertUser", user); 350 | sqlSession.commit(); 351 | } finally{ 352 | session.close(); 353 | } 354 | 355 | } 356 | ``` 357 | 358 | #### 主键返回 359 | 360 | ```xml 361 | 362 | 363 | 364 | select LAST_INSERT_ID() 365 | 366 | insert into user(username,birthday,sex,address) 367 | values(#{username},#{birthday},#{sex},#{address}); 368 | 369 | 370 | ``` 371 | 372 | 添加selectKey标签实现主键返回。 373 | 374 | - **keyProperty**:指定返回的主键,存储在pojo中的哪个属性 375 | - **order**:selectKey标签中的sql的执行顺序,是相对与insert语句来说。由于mysql的自增原理,执行完insert语句之后才将主键生成,所以这里selectKey的执行顺序为after。 376 | - **resultType**:返回的主键对应的JAVA类型 377 | - **LAST_INSERT_ID()**:是mysql的函数,返回auto_increment自增列新记录id值。 378 | 379 | 380 | 381 | > 本系列文章Github [后端进阶指南 ](https://github.com/CodeGeekLee/data-structures-and-algorithms)已收录,此项目正在完善中,欢迎star。 382 | > 383 | > 公众号内文章都是博主原创,并且会一直更新。如果你想见证或和博主一起成长,欢迎关注! 384 | > 385 | > ![欢迎扫码关注哦!!!](https://i.loli.net/2019/11/24/fXyOTLCBcGMNKoj.png) -------------------------------------------------------------------------------- /docs/notes/mybatis/Mybatis开发方式和配置.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | > 本系列文章Github [后端进阶指南 ](https://github.com/CodeGeekLee/data-structures-and-algorithms)已收录,此项目正在完善中,欢迎star。 4 | 5 | ## 1. Mybatis的开发方式 6 | 7 | 此处使用的是JDK的动态代理方式,延迟加载使用的cglib动态代理方式 8 | 9 | ### 1.1 代理理解 10 | 11 | 代理分为静态代理和动态代理。此处先不说静态代理,因为Mybatis中使用的代理方式是动态代理。 12 | 13 | 动态代理分为两种方式: 14 | 15 | - 基于JDK的动态代理--针对有**接口的类**进行动态代理 16 | - 基于CGLIB的动态代理--通过**子类**继承**父类**的方式去进行代理。 17 | 18 | ### 1.2 XML方式 19 | 20 | - 开发方式 21 | 22 | 只需要开发Mapper接口(dao接口)和Mapper映射文件,不需要编写实现类。 23 | 24 | - 开发规范 25 | 26 | Mapper接口开发方式需要遵循以下规范: 27 | 28 | 1、 Mapper接口的类路径与Mapper.xml文件中的namespace相同。 29 | 30 | 2、 Mapper接口方法名称和Mapper.xml中定义的每个statement的id相同。 31 | 32 | 3、 Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同。 33 | 34 | 4、 Mapper接口方法的返回值类型和mapper.xml中定义的每个sql的resultType的类型相同。 35 | 36 | - mapper映射文件 37 | 38 | ```xml 39 | 40 | 43 | 44 | 45 | 48 | 49 | ``` 50 | 51 | - mapper接口 52 | 53 | ```java 54 | /** 55 | * 用户管理mapper 56 | */ 57 | public interface UserMapper { 58 | //根据用户id查询用户信息 59 | public User findUserById(int id) throws Exception; 60 | } 61 | ``` 62 | 63 | - 全局配置文件中加载映射文件 64 | 65 | ```xml 66 | 67 | 68 | 69 | 70 | 71 | ``` 72 | 73 | - 测试代码 74 | 75 | ```java 76 | public class UserMapperTest{ 77 | 78 | private SqlSessionFactory sqlSessionFactory; 79 | 80 | @Before 81 | public void setUp() throws Exception { 82 | //mybatis配置文件 83 | String resource = "SqlMapConfig.xml"; 84 | InputStream inputStream = Resources.getResourceAsStream(resource); 85 | //使用SqlSessionFactoryBuilder创建sessionFactory 86 | sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 87 | } 88 | @Test 89 | public void testFindUserById() throws Exception { 90 | //获取session 91 | SqlSession session = sqlSessionFactory.openSession(); 92 | //获取mapper接口的代理对象 93 | UserMapper userMapper = session.getMapper(UserMapper.class); 94 | //调用代理对象方法 95 | User user = userMapper.findUserById(1); 96 | System.out.println(user); 97 | //关闭session 98 | session.close(); 99 | 100 | } 101 | } 102 | 103 | ``` 104 | 105 | ### 1.3 注解方式 106 | 107 | * 开发方式 108 | 109 | 只需要编写mapper接口文件接口。 110 | 111 | - mapper接口 112 | 113 | ```java 114 | public interface AnnotationUserMapper { 115 | // 查询 116 | @Select("SELECT * FROM user WHERE id = #{id}") 117 | public User findUserById(int id); 118 | 119 | // 模糊查询用户列表 120 | @Select("SELECT * FROM user WHERE username LIKE '%${value}%'") 121 | public List findUserList(String username); 122 | 123 | // 添加并实现主键返回 124 | @Insert("INSERT INTO user (username,birthday,sex,address) VALUES (#{username},#{birthday},#{sex},#{address})") 125 | @SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", resultType = int.class, before = false) 126 | public void insertUser(User user); 127 | 128 | } 129 | ``` 130 | 131 | 132 | 133 | - 测试代码 134 | 135 | ```java 136 | public class AnnotationUserMapperTest { 137 | 138 | private SqlSessionFactory sqlSessionFactory; 139 | 140 | /** 141 | * @Before注解的方法会在@Test注解的方法之前执行 142 | * 143 | * @throws Exception 144 | */ 145 | @Before 146 | public void init() throws Exception { 147 | // 指定全局配置文件路径 148 | String resource = "SqlMapConfig.xml"; 149 | // 加载资源文件(全局配置文件和映射文件) 150 | InputStream inputStream = Resources.getResourceAsStream(resource); 151 | // 还有构建者模式,去创建SqlSessionFactory对象 152 | sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 153 | } 154 | 155 | @Test 156 | public void testFindUserById() { 157 | SqlSession sqlSession = sqlSessionFactory.openSession(); 158 | AnnotationUserMapper userMapper = sqlSession.getMapper(AnnotationUserMapper.class); 159 | 160 | User user = userMapper.findUserById(1); 161 | System.out.println(user); 162 | } 163 | 164 | @Test 165 | public void testFindUserList() { 166 | SqlSession sqlSession = sqlSessionFactory.openSession(); 167 | AnnotationUserMapper userMapper = sqlSession.getMapper(AnnotationUserMapper.class); 168 | 169 | List list = userMapper.findUserList("老郭"); 170 | System.out.println(list); 171 | } 172 | 173 | @Test 174 | public void testInsertUser() { 175 | SqlSession sqlSession = sqlSessionFactory.openSession(); 176 | AnnotationUserMapper userMapper = sqlSession.getMapper(AnnotationUserMapper.class); 177 | 178 | User user = new User(); 179 | user.setUsername("开课吧-2"); 180 | user.setSex("1"); 181 | user.setAddress("致真大厦"); 182 | userMapper.insertUser(user); 183 | System.out.println(user.getId()); 184 | } 185 | 186 | } 187 | ``` 188 | 189 | 190 | 191 | ## 2. 全局配置文件 192 | 193 | ### 2.1 配置内容 194 | 195 | SqlMapConfig.xml中配置的内容和顺序如下: 196 | 197 | ``` 198 | properties(属性) 199 | 200 | settings(全局配置参数) 201 | 202 | typeAliases(类型别名) 203 | 204 | typeHandlers(类型处理器)--Java类型--JDBC类型--->数据库类型转换 205 | 206 | objectFactory(对象工厂) 207 | 208 | plugins(插件)--可以在Mybatis执行SQL语句的流程中,横叉一脚去实现一些功能增强,比如PageHelper分页插件,就是第三方实现的一个插件 209 | 210 | environments(环境集合属性对象) 211 | 212 | environment(环境子属性对象) 213 | transactionManager(事务管理) 214 | dataSource(数据源) 215 | mappers(映射器) 216 | ``` 217 | 218 | ### 2.2 properties标签 219 | 220 | SqlMapConfig.xml可以引用java属性文件中的配置信息。 221 | 222 | 1、在classpath下定义db.properties文件, 223 | 224 | ```properties 225 | jdbc.driver=com.mysql.jdbc.Driver 226 | jdbc.url=jdbc:mysql://localhost:3306/ssm?characterEncoding=utf-8 227 | jdbc.username=root 228 | jdbc.password=root 229 | ``` 230 | 231 | 2、在SqlMapConfig.xml文件中,引用db.properties中的属性,具体如下: 232 | 233 | ```xml 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | ``` 247 | 248 | properties标签除了可以使用resource属性,引用properties文件中的属性。还可以在properties标签内定义property子标签来定义属性和属性值,具体如下: 249 | 250 | ```xml 251 | 252 | 253 | 254 | ``` 255 | 256 | **注意: MyBatis 将按照下面的顺序来加载属性**: 257 | 258 | - 读取properties 元素体内定义的属性。 259 | - 读取properties 元素中resource或 url 加载的属性,它会覆盖已读取的同名属性。 260 | 261 | ### 2.3 typeAlias标签 262 | 263 | **别名的作用**:就是为了简化映射文件中parameterType和ResultType中的POJO类型名称编写。 264 | 265 | #### 2.3.1 默认支持别名 266 | 267 | | 别名 | 映射的类型 | 268 | | ---------- | ---------- | 269 | | _byte | byte | 270 | | _long | long | 271 | | _short | short | 272 | | _int | int | 273 | | _integer | int | 274 | | _double | double | 275 | | _float | float | 276 | | _boolean | boolean | 277 | | string | String | 278 | | byte | Byte | 279 | | long | Long | 280 | | short | Short | 281 | | int | Integer | 282 | | integer | Integer | 283 | | double | Double | 284 | | float | Float | 285 | | boolean | Boolean | 286 | | date | Date | 287 | | decimal | BigDecimal | 288 | | bigdecimal | BigDecimal | 289 | | map | Map | 290 | 291 | #### 2.3.2 自定义别名 292 | 293 | 在SqlMapConfig.xml中进行如下配置: 294 | 295 | ```xml 296 | 297 | 298 | 299 | 300 | 301 | 302 | ``` 303 | 304 | ### 2.4 mappers标签 305 | 306 | #### \ 307 | 308 | 使用相对于类路径的资源 309 | 310 | 如: 311 | 312 | ``` 313 | 314 | ``` 315 | 316 | #### \ 317 | 318 | 使用绝对路径加载资源 319 | 320 | 如: 321 | 322 | ``` 323 | 324 | ``` 325 | 326 | #### \ 327 | 328 | 使用mapper接口类路径,加载映射文件。 329 | 330 | 如: 331 | 332 | ``` 333 | 334 | ``` 335 | 336 | **注意:此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。** 337 | 338 | #### \ 339 | 340 | 注册指定包下的所有mapper接口,来加载映射文件。 341 | 342 | 如: 343 | 344 | ``` 345 | 346 | ``` 347 | 348 | **注意:此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。** 349 | 350 | 351 | 352 | ## 3. 输入映射和输出映射 353 | 354 | ### 3.1 parameterType(输入类型) 355 | 356 | parameterType属性可以映射的输入参数Java类型有:**简单类型、POJO类型、Map类型、List类型(数组)**。 357 | 358 | * Map类型和POJO类型的用法类似,这里只讲POJO类型的相关配置。 359 | 360 | * List类型在动态SQL部分进行讲解。 361 | 362 | #### 传递简单类型 363 | 364 | 参考《Mybatis基础》(在我的主页内查找)中用户查询的案例。 365 | 366 | #### 传递pojo对象 367 | 368 | 参考《Mybatis基础》(在我的主页内查找)中的添加用户的案例。 369 | 370 | #### 传递pojo包装对象 371 | 372 | 包装对象:pojo类中嵌套pojo。 373 | 374 | 375 | 376 | ##### 需求 377 | 378 | 通过包装POJO传递参数,完成用户查询。 379 | 380 | ##### QueryVO 381 | 382 | 定义包装对象QueryVO 383 | 384 | ```java 385 | public class QueryVO { 386 | private User user; 387 | } 388 | ``` 389 | 390 | ##### SQL语句 391 | 392 | ```XML 393 | SELECT * FROM user where username like '%小明%' 394 | ``` 395 | 396 | ##### Mapper文件 397 | 398 | ```xml 399 | 402 | 405 | 406 | ``` 407 | 408 | ##### Mapper接口 409 | 410 | ```java 411 | /** 412 | * 用户管理mapper 413 | */ 414 | public interface UserMapper { 415 | //综合查询用户列表 416 | public List findUserList(QueryVo queryVo)throws Exception; 417 | } 418 | ``` 419 | 420 | 421 | 422 | ##### 测试方法 423 | 424 | 在UserMapperTest测试类中,添加以下测试代码: 425 | 426 | ```java 427 | @Test 428 | public void testFindUserList() throws Exception { 429 | SqlSession sqlSession = sqlSessionFactory.openSession(); 430 | //获得mapper的代理对象 431 | UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 432 | //创建QueryVo对象 433 | QueryVo queryVo = new QueryVo(); 434 | //创建user对象 435 | User user = new User(); 436 | user.setUsername("小明"); 437 | 438 | queryVo.setUser(user); 439 | //根据queryvo查询用户 440 | List list = userMapper.findUserList(queryVo); 441 | System.out.println(list); 442 | sqlSession.close(); 443 | } 444 | 445 | ``` 446 | 447 | 448 | 449 | ### 3.2 resultType(输出类型) 450 | 451 | resultType属性可以映射的java类型有:**简单类型、POJO类型、Map类型**。 452 | 453 | 不过Map类型和POJO类型的使用情况类似,所以只需讲解POJO类型即可。 454 | 455 | #### 3.2.1 使用要求 456 | 457 | 使用resultType进行输出映射时,要求sql语句中**查询的列名**和要映射的**pojo的属性名**一致。 458 | 459 | #### 3.2.2 映射简单类型 460 | 461 | ##### 案例需求 462 | 463 | 查询用户记录总数。 464 | 465 | ##### Mapper映射文件 466 | 467 | ```xml 468 | 469 | 472 | ``` 473 | 474 | ##### Mapper接口 475 | 476 | ```java 477 | //查询用户总数 478 | public int findUserCount() throws Exception; 479 | ``` 480 | 481 | ##### 测试代码 482 | 483 | 在UserMapperTest测试类中,添加以下测试代码: 484 | 485 | ```java 486 | @Test 487 | public void testFindUserCount() throws Exception { 488 | SqlSession sqlSession = sessionFactory.openSession(); 489 | //获得mapper的代理对象 490 | UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 491 | 492 | int count = userMapper.findUserCount(queryVo); 493 | System.out.println(count); 494 | 495 | sqlSession.close(); 496 | } 497 | 498 | 499 | ``` 500 | 501 | **注意:输出简单类型必须查询出来的结果集只有一列。** 502 | 503 | 504 | 505 | #### 3.2.3 映射pojo对象 506 | 507 | **注意:不管是单个POJO还是POJO集合,在使用resultType完成映射时,用法一样。** 508 | 509 | 参考《Mybatis基础》(在我的主页内查找)中根据用户ID查询用户信息和根据名称模糊查询用户列表的案例 510 | 511 | 512 | 513 | ### 4. resultMap 514 | 515 | #### 4.1 使用要求 516 | 517 | 如果sql查询列名和pojo的属性名可以不一致,通过resultMap将列名和属性名作一个对应关系,最终将查询结果映射到指定的pojo对象中。 518 | 519 | **注意:resultType底层也是通过resultMap完成映射的。** 520 | 521 | #### 4.2 需求 522 | 523 | 将以下sql的查询结果进行映射: 524 | 525 | ``` 526 | SELECT id id_,username username_,birthday birthday_ FROM user 527 | ``` 528 | 529 | 530 | 531 | #### 4.3 Mapper接口 532 | 533 | ```java 534 | // resultMap入门 535 | public List findUserListResultMap() throws Exception; 536 | ``` 537 | 538 | #### 4.4 Mapper映射文件 539 | 540 | 由于sql查询列名和User类属性名不一致,所以不能使用resultType进行结构映射。 541 | 542 | 需要定义一个resultMap将sql查询列名和User类的属性名对应起来,完成结果映射。 543 | 544 | ```xml 545 | 546 | 550 | 551 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 566 | 567 | 568 | ``` 569 | 570 | 571 | 572 | - \:表示查询结果集的唯一标识,非常重要。如果是多个字段为复合唯一约束则定义多个 573 | - Property:表示User类的属性。 574 | - Column:表示sql查询出来的字段名。 575 | - Column和property放在一块儿表示将sql查询出来的字段映射到指定的pojo类属性上。 576 | 577 | - \:普通结果,即pojo的属性。 578 | 579 | 580 | 581 | > 本系列文章Github [后端进阶指南 ](https://github.com/CodeGeekLee/data-structures-and-algorithms)已收录,此项目正在完善中,欢迎star。 582 | > 583 | > 公众号内文章都是博主原创,并且会一直更新。如果你想见证或和博主一起成长,欢迎关注! 584 | > 585 | > ![欢迎扫码关注哦!!!](https://i.loli.net/2019/11/24/fXyOTLCBcGMNKoj.png) -------------------------------------------------------------------------------- /docs/notes/mybatis/Mybatis缓存.md: -------------------------------------------------------------------------------- 1 | ### 1. 缓存介绍 2 | 3 | Mybatis提供**查询缓存**,如果缓存中有数据就不用从数据库中获取,用于减轻数据压力,提高系统性能。 4 | 5 | Mybatis的查询**缓存总共有两级**,我们称之为一级缓存和二级缓存: 6 | 7 | - 一级缓存是**SqlSession级别**的缓存。在操作数据库时需要构造 sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。 8 | 9 | - 二级缓存是Mapper(namespace)级别的缓存。多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。 10 | 11 | ### 2. 一级缓存 12 | 13 | **Mybatis默认开启了一级缓存** 14 | 15 | ![img](https://awps-assets.meituan.net/mit-x/blog-images-bundle-2018a/6e38df6a.jpg) 16 | 17 | **说明:** 18 | 19 | - 第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息,将查询到的用户信息存储到一级缓存中。 20 | - 如果中间sqlSession去执行commit操作(执行插入、更新、删除),清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。 21 | - 第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。 22 | 23 | #### 2.1 测试1 24 | 25 | ```java 26 | @Test 27 | public void testOneLevelCache() { 28 | SqlSession sqlSession = sqlSessionFactory.openSession(); 29 | UserMapper mapper = sqlSession.getMapper(UserMapper.class); 30 | // 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库 31 | User user1 = mapper.findUserById(1); 32 | System.out.println(user1); 33 | 34 | // 第二次查询ID为1的用户 35 | User user2 = mapper.findUserById(1); 36 | System.out.println(user2); 37 | 38 | sqlSession.close(); 39 | } 40 | 41 | ``` 42 | 43 | 44 | 45 | #### 2.2 测试2 46 | 47 | ```java 48 | @Test 49 | public void testOneLevelCache() { 50 | SqlSession sqlSession = sqlSessionFactory.openSession(); 51 | UserMapper mapper = sqlSession.getMapper(UserMapper.class); 52 | // 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库 53 | User user1 = mapper.findUserById(1); 54 | System.out.println(user1); 55 | 56 | User user = new User(); 57 | user.setUsername("隔壁老詹1"); 58 | user.setAddress("洛杉矶湖人"); 59 | //执行增删改操作,清空缓存 60 | mapper.insertUser(user); 61 | 62 | // 第二次查询ID为1的用户 63 | User user2 = mapper.findUserById(1); 64 | System.out.println(user2); 65 | 66 | sqlSession.close(); 67 | } 68 | 69 | ``` 70 | 71 | 72 | 73 | #### 2.3 具体应用 74 | 75 | 正式开发,是将mybatis和spring进行整合开发,事务控制在service中。 76 | 77 | 一个service方法中包括 很多mapper方法调用: 78 | 79 | ``` 80 | service{ 81 | //开始执行时,开启事务,创建SqlSession对象 82 | //第一次调用mapper的方法findUserById(1) 83 | 84 | //第二次调用mapper的方法findUserById(1),从一级缓存中取数据 85 | //方法结束,sqlSession关闭 86 | } 87 | ``` 88 | 89 | 如果是执行两次service调用查询相同 的用户信息,是不走一级缓存的,因为mapper方法结束,sqlSession就关闭,一级缓存就清空。 90 | 91 | ### 3. 二级缓存 92 | 93 | #### 3.1 原理 94 | 95 | 二级缓存是mapper(namespace)级别的。 96 | 97 | ![img](https://awps-assets.meituan.net/mit-x/blog-images-bundle-2018a/28399eba.png) 98 | 99 | **说明:** 100 | 101 | 1. 第一次调用mapper下的SQL去查询用户信息。查询到的信息会存到该mapper对应的**二级缓存区域**内。 102 | 2. 第二次调用相同namespace下的mapper映射文件中相同的SQL去查询用户信息。会去对应的二级缓存内取结果。 103 | 3. 如果调用相同namespace下的mapper映射文件中的增删改SQL,并执行了commit操作。此时会清空该namespace下的二级缓存。 104 | 105 | #### 3.2 开启二级缓存 106 | 107 | **Mybatis默认是没有开启二级缓存,开启步骤如下:** 108 | 109 | 1. 在核心配置文件SqlMapConfig.xml中加入以下内容(开启二级缓存总开关): 110 | 111 | ```xml 112 | 113 | 114 | 115 | 116 | ``` 117 | 118 | 119 | 120 | 1. 在UserMapper映射文件中,加入以下内容,开启二级缓存: 121 | 122 | ```xml 123 | 124 | 125 | ``` 126 | 127 | 128 | 129 | #### 3.3 实现序列化 130 | 131 | 由于二级缓存的数据不一定都是存储到内存中,它的存储介质多种多样,比如说存储到文件系统中,所以需要给缓存的对象执行序列化。如果该类存在父类,那么父类也要实现序列化。 132 | 133 | #### 3.4 测试1 134 | 135 | ```java 136 | @Test 137 | public void testTwoLevelCache() { 138 | SqlSession sqlSession1 = sqlSessionFactory.openSession(); 139 | SqlSession sqlSession2 = sqlSessionFactory.openSession(); 140 | SqlSession sqlSession3 = sqlSessionFactory.openSession(); 141 | 142 | UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class); 143 | UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class); 144 | UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class); 145 | // 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库 146 | User user1 = mapper1.findUserById(1); 147 | System.out.println(user1); 148 | // 关闭SqlSession1 149 | sqlSession1.close(); 150 | 151 | // 第二次查询ID为1的用户 152 | User user2 = mapper2.findUserById(1); 153 | System.out.println(user2); 154 | // 关闭SqlSession2 155 | sqlSession2.close(); 156 | } 157 | 158 | ``` 159 | 160 | 161 | 162 | #### 3.5 测试2 163 | 164 | ```mysql 165 | @Test 166 | public void testTwoLevelCache() { 167 | SqlSession sqlSession1 = sqlSessionFactory.openSession(); 168 | SqlSession sqlSession2 = sqlSessionFactory.openSession(); 169 | SqlSession sqlSession3 = sqlSessionFactory.openSession(); 170 | 171 | UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class); 172 | UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class); 173 | UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class); 174 | // 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库 175 | User user1 = mapper1.findUserById(1); 176 | System.out.println(user1); 177 | // 关闭SqlSession1 178 | sqlSession1.close(); 179 | 180 | //修改查询出来的user1对象,作为插入语句的参数 181 | user1.setUsername("隔壁老詹1"); 182 | user1.setAddress("洛杉矶湖人"); 183 | 184 | mapper3.insertUser(user1); 185 | 186 | // 提交事务 187 | sqlSession3.commit(); 188 | // 关闭SqlSession3 189 | sqlSession3.close(); 190 | 191 | // 第二次查询ID为1的用户 192 | User user2 = mapper2.findUserById(1); 193 | System.out.println(user2); 194 | // 关闭SqlSession2 195 | sqlSession2.close(); 196 | } 197 | 198 | ``` 199 | 200 | 201 | 202 | #### 3.6 禁用二级缓存 203 | 204 | 默认二级缓存的粒度是Mapper级别的,但是如果在同一个Mapper文件中某个查询不想使用二级缓存的话,就需要对缓存的控制粒度更细。 205 | 206 | 在select标签中设置**useCache=false**,可以禁用当前select语句的二级缓存,即每次查询都是去数据库中查询,**默认情况下是true**,即该statement使用二级缓存。 207 | 208 | ```xml 209 | 213 | ``` 214 | 215 | #### 3.7 刷新二级缓存 216 | 217 | **通过flushCache属性,可以控制select、insert、update、delete标签的是否属性二级缓存** 218 | 219 | **默认设置** 220 | 221 | - 默认情况下如果是select语句,那么flushCache是false。 222 | 223 | - 如果是insert、update、delete语句,那么flushCache是true。 224 | 225 | **默认配置解读** 226 | 227 | - 如果查询语句设置成true,那么每次查询都是去数据库查询,即意味着该查询的二级缓存失效。 228 | 229 | - 如果增删改语句设置成false,即使用二级缓存,那么如果在数据库中修改了数据,而缓存数据还是原来的,这个时候就会出现脏读。 230 | 231 | flushCache设置如下: 232 | 233 | ```xml 234 | 238 | ``` 239 | 240 | #### 3.8 应用场景 241 | 242 | - 使用场景: 243 | 244 | 对于访问响应速度要求高,但是实时性不高的查询,可以采用二级缓存技术。 245 | 246 | - 注意事项: 247 | 248 | 在使用二级缓存的时候,要设置一下**刷新间隔**(cache标签中有一个**flashInterval**属性)来定时刷新二级缓存,这个刷新间隔根据具体需求来设置,比如设置30分钟、60分钟等,**单位为毫秒**。 249 | 250 | #### 3.9 局限性 251 | 252 | **Mybatis二级缓存对细粒度的数据级别的缓存实现不好。** 253 | 254 | - 场景: 255 | 256 | 对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次查询都是最新的商品信息,此时如果使用二级缓存,就无法实现当一个商品发生变化只刷新该商品的缓存信息而不刷新其他商品缓存信息,因为二级缓存是mapper级别的,当一个商品的信息发送更新,所有的商品信息缓存数据都会清空。 257 | 258 | - 解决方法 259 | 260 | 此类问题,需要在业务层根据需要对数据有针对性的缓存。 261 | 262 | 比如可以对经常变化的 数据操作单独放到另一个namespace的mapper中。 -------------------------------------------------------------------------------- /docs/notes/mybatis/Mybatis高级应用.md: -------------------------------------------------------------------------------- 1 | > 本系列文章Github [后端进阶指南 ](https://github.com/CodeGeekLee/data-structures-and-algorithms)已收录,此项目正在完善中,欢迎star。 2 | 3 | ## 1. 关联查询 4 | 5 | 举例:因为一个订单信息只会是一个人下的订单,所以从查询订单信息出发,关联查询用户信息为一对一查询。如果从用户信息出发,查询用户下的订单信息则为一对多查询,因为一个用户可以下多个订单。 6 | 7 | ### 1.1 一对一查询 8 | 9 | #### 需求 10 | 11 | 查询所有订单信息,关联查询下单用户信息。 12 | 13 | #### SQL语句 14 | 15 | **主信息:订单表** 16 | 17 | **从信息:用户表** 18 | 19 | ```mysql 20 | SELECT 21 | orders.*, 22 | user.username, 23 | user.address 24 | FROM 25 | orders LEFT JOIN user 26 | ON orders.user_id = user.id 27 | 28 | ``` 29 | 30 | 31 | 32 | #### 方法一:resultType 33 | 34 | 返回resultType方式比较简单,也比较常用,就不做介绍了。 35 | 36 | #### 方法二:resultMap 37 | 38 | 使用resultMap进行结果映射,定义专门的resultMap用于映射一对一查询结果。 39 | 40 | ##### 创建扩展po类 41 | 42 | 创建OrdersExt类(**该类用于结果集封装**),加入User属性,user属性中用于存储关联查询的用户信息,因为订单关联查询用户是一对一关系,所以这里使用单个User对象存储关联查询的用户信息。 43 | 44 | ```java 45 | public class OrdersExt extends Orders { 46 | 47 | private User user;// 用户对象 48 | // get/set。。。。 49 | } 50 | ``` 51 | 52 | ##### Mapper映射文件 53 | 54 | 在UserMapper.xml中,添加以下代码: 55 | 56 | ```xml 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 89 | 90 | ``` 91 | 92 | - **association**:表示进行一对一关联查询映射 93 | - **property**:表示关联查询的结果存储在com.kkb.mybatis.po.Orders的user属性中 94 | - **javaType**:表示关联查询的映射结果类型 95 | 96 | 97 | 98 | ##### Mapper接口 99 | 100 | 在UserMapper接口中,添加以下接口方法: 101 | 102 | ```java 103 | public List findOrdersAndUserRstMap() throws Exception; 104 | ``` 105 | 106 | 107 | 108 | ##### 测试代码 109 | 110 | 在UserMapperTest测试类中,添加测试代码: 111 | 112 | ```java 113 | public void testfindOrdersAndUserRstMap()throws Exception{ 114 | //获取session 115 | SqlSession session = sqlSessionFactory.openSession(); 116 | //获限mapper接口实例 117 | UserMapper userMapper = session.getMapper(UserMapper.class); 118 | //查询订单信息 119 | List list = userMapper.findOrdersAndUserRstMap(); 120 | System.out.println(list); 121 | //关闭session 122 | session.close(); 123 | } 124 | ``` 125 | 126 | 127 | 128 | ##### 小结 129 | 130 | 使用resultMap进行结果映射时,具体是使用association完成关联查询的映射,将关联查询信息映射到pojo对象中。 131 | 132 | ### 1.2 一对多查询 133 | 134 | #### 需求 135 | 136 | 查询所有用户信息及用户关联的订单信息。 137 | 138 | #### SQL语句 139 | 140 | **主信息:用户信息** 141 | 142 | **从信息:订单信息** 143 | 144 | ```mysql 145 | SELECT 146 | u.*, 147 | o.id oid, 148 | o.number, 149 | o.createtime, 150 | o.note 151 | FROM 152 | `user` u 153 | LEFT JOIN orders o ON u.id = o.user_id 154 | ``` 155 | 156 | 157 | 158 | #### 分析 159 | 160 | 在一对多关联查询时,只能使用resultMap进行结果映射: 161 | 162 | 1、一对多关联查询时,sql查询结果有多条,而映射对象是一个。 163 | 164 | 2、resultType完成结果映射的方式的一条记录映射一个对象。 165 | 166 | 3、resultMap完成结果映射的方式是以[主信息]为主对象,[从信息]映射为集合或者对象,然后封装到主对象中。 167 | 168 | #### 修改po类 169 | 170 | 在User类中加入List orders属性。 171 | 172 | #### Mapper映射文件 173 | 174 | 在UserMapper.xml文件中,添加以下代码: 175 | 176 | ```xml 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 204 | ``` 205 | 206 | 207 | 208 | Collection标签:定义了一对多关联的结果映射。 209 | 210 | - **property="orders"**:关联查询的结果集存储在User对象的上哪个属性。 211 | - **ofType="orders"**:指定关联查询的结果集中的对象类型即List中的对象类型。此处可以使用别名,也可以使用全限定名。 212 | 213 | #### Mapper接口 214 | 215 | ```java 216 | // resultMap入门 217 | public List findUserAndOrdersRstMap() throws Exception; 218 | 219 | ``` 220 | 221 | #### 测试代码 222 | 223 | ```java 224 | @Test 225 | public void testFindUserAndOrdersRstMap() { 226 | SqlSession session = sqlSessionFactory.openSession(); 227 | UserMapper userMapper = session.getMapper(UserMapper.class); 228 | List result = userMapper.findUserAndOrdersRstMap(); 229 | for (User user : result) { 230 | System.out.println(user); 231 | } 232 | session.close(); 233 | } 234 | 235 | ``` 236 | 237 | ## 2. 延迟加载 238 | 239 | ### 2.1 什么是延迟加载 240 | 241 | * MyBatis中的延迟加载,也称为**懒加载**,是指在进行关联查询时,按照设置延迟规则推迟对关联对象的select查询。延迟加载可以有效的减少数据库压力。 242 | 243 | * Mybatis的延迟加载,需要通过**resultMap标签中的association和collection**子标签才能演示成功。 244 | 245 | * Mybatis的延迟加载,也被称为是嵌套查询,对应的还有**嵌套结果**的概念,可以参考一对多关联的案例。 246 | 247 | * 注意:**MyBatis的延迟加载只是对关联对象的查询有延迟设置,对于主加载对象都是直接执行查询语句的sql**。 248 | 249 | ### 2.2 延迟加载的分类 250 | 251 | MyBatis根据对关联对象查询的select语句的**执行时机**,分为三种类型:**直接加载、侵入式加载与深度延迟加载** 252 | 253 | - **直接加载:** 执行完对主加载对象的select语句,马上执行对关联对象的select查询。 254 | - **侵入式延迟**:执行对主加载对象的查询时,不会执行对关联对象的查询。但当要访问主加载对象的某个属性(该属性不是关联对象的属性)时,就会马上执行关联对象的select查询。 255 | - **深度延迟:**执行对主加载对象的查询时,不会执行对关联对象的查询。访问主加载对象的详情时也不会执行关联对象的select查询。只有当真正访问关联对象的详情时,才会执行对关联对象的select查询。 256 | 257 | > 延迟加载策略需要在Mybatis的全局配置文件中,通过标签进行设置。 258 | 259 | ### 2.3 案例准备 260 | 261 | 查询订单信息及它的下单用户信息。 262 | 263 | ### 2.4 直接加载 264 | 265 | 通过对全局参数:lazyLoadingEnabled进行设置,默认就是false。 266 | 267 | ```xml 268 | 269 | 270 | 271 | 272 | ``` 273 | 274 | ### 2.5 侵入式延迟加载 275 | 276 | ```xml 277 | 278 | 279 | 280 | 281 | 282 | 283 | ``` 284 | 285 | ### 2.6 深度延迟加载 286 | 287 | ```xml 288 | 289 | 290 | 291 | 292 | 293 | 294 | ``` 295 | 296 | 297 | 298 | ### 2.7 N+1问题 299 | 300 | - 深度延迟加载的使用会提升性能。 301 | - 如果延迟加载的表数据太多,此时会产生N+1问题,主信息加载一次算1次,而从信息是会根据主信息传递过来的条件,去查询从表多次。 302 | 303 | ## 3. 动态SQL 304 | 305 | 动态SQL的思想:就是使用不同的动态SQL标签去完成字符串的拼接处理、循环判断。 306 | 307 | 解决的问题是: 308 | 309 | 1. 在映射文件中,会编写很多有重叠部分的SQL语句,比如SELECT语句和WHERE语句等这些重叠语句,该如何处理 310 | 311 | 2. SQL语句中的where条件有多个,但是页面只传递过来一个条件参数,此时会发生问题。 312 | 313 | ### 3.1 if标签 314 | 315 | 综合查询的案例中,查询条件是由页面传入,页面中的查询条件可能输入用户名称,也可能不输入用户名称。 316 | 317 | ```xml 318 | 326 | ``` 327 | 328 | 329 | 330 | **注意:要做『不等于空』字符串校验。** 331 | 332 | ### 3.2 where标签 333 | 334 | 上边的sql中的1=1,虽然可以保证sql语句的完整性:但是存在性能问题。Mybatis提供where标签解决该问题。 335 | 336 | 代码修改如下: 337 | 338 | 339 | 340 | ```xml 341 | 352 | ``` 353 | 354 | 355 | 356 | ### 3.3 sql片段 357 | 358 | 在映射文件中可使用sql标签将重复的sql提取出来,然后使用include标签引用即可,最终达到sql重用的目的,具体实现如下: 359 | 360 | 361 | 362 | - 原映射文件中的代码: 363 | 364 | ```xml 365 | 376 | 377 | ``` 378 | 379 | - 将where条件抽取出来: 380 | 381 | ```xml 382 | 383 | 384 | 385 | AND username like '%${user.username}%' 386 | 387 | 388 | 389 | 390 | ``` 391 | 392 | - 使用include引用: 393 | 394 | ```xml 395 | 396 | 404 | 405 | ``` 406 | 407 | 408 | 409 | **注意:** 410 | 411 | **1、如果引用其它mapper.xml的sql片段,则在引用时需要加上namespace,如下:** 412 | 413 | ``` 414 | 441 | 442 | 443 | AND username like '%${user.username}%' 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 454 | #{id} 455 | 456 | 457 | 458 | 459 | ``` 460 | 461 | 462 | 463 | #### 测试代码 464 | 465 | 在UserMapperTest测试代码中,修改testFindUserList方法,如下: 466 | 467 | ```java 468 | @Test 469 | public void testFindUserList() throws Exception { 470 | SqlSession sqlSession = sqlSessionFactory.openSession(); 471 | // 获得mapper的代理对象 472 | UserMapper userMapper = sqlSession.getMapper(UserMapper.class); 473 | // 创建QueryVo对象 474 | QueryVo queryVo = new QueryVo(); 475 | // 创建user对象 476 | User user = new User(); 477 | user.setUsername("老郭"); 478 | 479 | queryVo.setUser(user); 480 | 481 | List ids = new ArrayList(); 482 | ids.add(1);// 查询id为1的用户 483 | ids.add(10); // 查询id为10的用户 484 | queryVo.setIds(ids); 485 | 486 | // 根据queryvo查询用户 487 | List list = userMapper.findUserList(queryVo); 488 | System.out.println(list); 489 | sqlSession.close(); 490 | } 491 | 492 | ``` 493 | 494 | 495 | 496 | #### 注意事项 497 | 498 | **如果parameterType不是POJO类型,而是List或者Array的话,那么foreach语句中,collection属性值需要固定写死为list或者array。** 499 | 500 | 501 | 502 | > 本系列文章Github [后端进阶指南 ](https://github.com/CodeGeekLee/data-structures-and-algorithms)已收录,此项目正在完善中,欢迎star。 503 | > 504 | > 公众号内文章都是博主原创,并且会一直更新。如果你想见证或和博主一起成长,欢迎关注! 505 | > 506 | > ![](https://user-gold-cdn.xitu.io/2020/2/19/1705c47c1b2eb7b8?w=300&h=300&f=png&s=11666) 507 | -------------------------------------------------------------------------------- /docs/notes/practice/通用点赞设计思路.md: -------------------------------------------------------------------------------- 1 | ### 前言 2 | 点赞作为一个高频率的操作,如果每次操作都读写数据库会增加数据库的压力,所以采用缓存+定时任务来实现。点赞数据是在redis中缓存半小时,同时定时任务是每隔5分钟执行一次,做持久化存储,这里的缓存时间和任务执行时间可根据项目情况而定。 3 | ### 优点 4 | 1.降低对数据库的影响 5 | 2.提高点赞的效率 6 | ### 缺点 7 | 1.如果任务挂了,会丢失点赞数据 8 | 2.持久化存储不是实时的 9 | ### 时序图 10 | ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2018/11/26/1674e3856a21faac?w=1155&h=1487&f=png&s=123171) 11 | ### 数据库设计 12 | ``` 13 | create table user_like( 14 | id bigint(20) unsigned not null auto_increment comment 'id', 15 | user_id bigint(20) not null default 0 comment '用户id', 16 | liked_id varchar(21) not null default '' comment '被点赞的id', 17 | liked_status int(11) not null default 0 comment '点赞状态,0未点赞,1已点赞', 18 | liked_type int(11) not null default 0 comment '点赞的类型', 19 | liked_time timestamp not null default '0000-00-00 00:00:00.000000' comment '点赞时间', 20 | is_delete tinyint not null default '0' comment '是否逻辑删除', 21 | create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', 22 | update_time timestamp not null default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP comment '更新时间', 23 | primary key (id), 24 | unique uniq_user_id_liked_id_type(user_id,liked_id,liked_type), 25 | key idx_liked_id (liked_id), 26 | key idx_create_time (create_time), 27 | key idx_update_time (update_time) 28 | )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='用户点赞表'; 29 | 30 | create table user_like_stat( 31 | id bigint(20) unsigned not null auto_increment comment 'id', 32 | liked_id varchar(21) not null default '' comment '被点赞id', 33 | liked_count int(11) not null default 0 comment '点赞总数量', 34 | is_delete tinyint not null default '0' comment '是否逻辑删除', 35 | create_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间', 36 | update_time timestamp not null default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP comment '更新时间', 37 | primary key (id), 38 | unique uniq_info_num(liked_id), 39 | key idx_create_time (create_time), 40 | key idx_update_time (update_time) 41 | )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='点赞统计表'; 42 | ``` 43 | ### 实现步骤 44 | #### 1.设计缓存数据格式 45 | 整个点赞模块主要采用缓存来完成,所以要选择合适数据结构,我选择hash数据结构来实现,应为它可以添加、获取、移除单个键值对,并且可以获取所有键值对。主要缓存两种数据,一种是用户的点赞状态,一种是被点赞id的点赞数量。这两种数据分别用两个key存储,这两个key中都是存储的多个键值对。键值对格式如下: 46 | 47 | 用户的点赞状态key-value------>{"被点赞的id::用户id" :"点赞状态::点赞时间::点赞类型"} 48 | 49 | 被点赞id的点赞数量key-value------>{"被点赞id" : "点赞数量"} 50 | #### 2.大key拆分 51 | 点赞的数据量比较大的情况下,上面的设计会造成单个key存储的value很大,由于redis是单线程运行,如果一次操作的value很大,会对整个redis的响应时间有影响,所以我们这里在将上面的两个key做拆分。固定key的数量,每次存取时都先在本地计算出落在了哪个key上,这个操作就类似于redis分区、分片。有利于降低单次操作的压力,将压力平分到多个key上。 52 | ``` 53 | //点赞状态key拆分 54 | newHashKey = hashKey +"_"+ (userId% 5); 55 | hset (newHashKey, field, value) ; 56 | hget(newHashKey, field) 57 | 58 | 59 | //点赞数量key拆分 60 | newHashKey = hashKey +"_"+ Math.abs((hash*(被点赞id)) % 5); 61 | hset (newHashKey, field, value) ; 62 | hget(newHashKey, field) 63 | ``` 64 | #### 3.代码实现 65 | 以下值截取了部分代码,提供思路。 66 | 67 | 1.点赞状态枚举 68 | ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2018/11/26/1674e3856a41d9fa?w=1170&h=988&f=png&s=949175) 69 | 2.点赞类型枚举 70 | ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2018/11/26/1674e3856a3a702f?w=1132&h=1012&f=png&s=941416) 71 | 3.用户点赞类 72 | ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2018/11/26/1674e3856a408f69?w=638&h=926&f=png&s=504060) 73 | 4.点赞接口实现 74 | 这里使用策略设计模式来实现,方便以后的扩展,对这个设计模式不了解的请点击 75 | 76 | https://juejin.im/post/5bdc1e77e51d4502b064e893 77 | 78 | ![这里进行策略的选择](https://user-gold-cdn.xitu.io/2018/11/26/1674e3856aba32a9?w=1364&h=588&f=png&s=733563)![在这里插入图片描述](https://user-gold-cdn.xitu.io/2018/11/26/1674e3856ada39d1?w=1222&h=630&f=png&s=695643)![在这里插入图片描述](https://user-gold-cdn.xitu.io/2018/11/26/1674e38598e76d58?w=1146&h=430&f=png&s=412751) 79 | 5.逻辑 80 | 取消点赞和这个接口相同,只需要替换下点赞状态和redis增量 81 | ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2018/11/26/1674e38599e90563?w=1830&h=1112&f=png&s=1588595) 82 | 6.定时任务 83 | 定时任务采用Azkaban任务调度系统,每个5分种运行一次任务,把点赞数据从redis缓存中取出做持久化到mysql。 84 | #### 4.改进点 85 | 现在的读取都是用的一个key,接下来可以优化为把key做读写分离。写入和读取分别用不同的key,这样做可以减少资源的浪费,要不每次跑定时任务都会把已经持久化并且缓存未失效的数据拿出来做一遍查询。 86 | 87 | 以上就是点赞的一个实现思路,大家有什么更好的方法或者改进的点,欢迎提出来。 88 | --------------------------------------------------------------------------------