├── Java ├── JVM.md ├── JavaEE.md ├── Java基础.md ├── Java并发 │ ├── Java并发.md │ ├── leetcode-多线程.md │ ├── 并发容器.md │ └── 线程池.md ├── Java网络 │ ├── Java IO与网络编程.md │ └── socket.md └── Java集合.md ├── JavaWeb ├── Spring.md └── SpringBoot.md ├── README.md ├── 操作系统 ├── Linux.md └── 操作系统.md ├── 数据库 ├── MySQL.md ├── Redis.md ├── SQL.md ├── leedcode-database.md └── 数据库系统原理.md ├── 计算机网络 ├── HTTP.md └── 计算机网络.md └── 面向对象与设计模式 ├── JDK中的设计模式.md ├── 创建型模式.md ├── 结构型模式.md ├── 行为型模式.md └── 面向对象思想.md /Java/JVM.md: -------------------------------------------------------------------------------- 1 | # 一、运行时数据区域 # 2 | 3 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/5778d113-8e13-4c53-b5bf-801e58080b97.png) 4 | 5 | ## 程序计数器 ## 6 | 7 | 记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。 8 | 9 | ## Java 虚拟机栈 ## 10 | 11 | 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 12 | 13 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/8442519f-0b4d-48f4-8229-56f984363c69.png) 14 | 15 | 可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M: 16 | 17 | java -Xss2M HackTheJava 18 | 19 | 该区域可能抛出以下异常: 20 | 21 | - 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常; 22 | - 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。 23 | 24 | ## 本地方法栈 ## 25 | 26 | 本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。 27 | 28 | 本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。 29 | 30 | JNI,全称为Java Native Interface,即Java本地接口,JNI就是Java调用C++的规范,JAVA和C++通过JNI交互。 31 | 32 | ## 堆 ## 33 | 34 | 所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。 35 | 36 | 现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块: 37 | 38 | - 新生代(Young Generation) 39 | - 老年代(Old Generation) 40 | 41 | 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。 42 | 43 | 可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。 44 | 45 | java -Xms1M -Xmx2M HackTheJava 46 | 47 | ## 方法区 ## 48 | 49 | 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 50 | 51 | 和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。 52 | 53 | 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。 54 | 55 | HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。 56 | 57 | 方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。 58 | 59 | ## 运行时常量池 ## 60 | 61 | 运行时常量池是方法区的一部分。 62 | 63 | Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。 64 | 65 | 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。 66 | 67 | ## 直接内存 ## 68 | 69 | 在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。 70 | 71 | # 二、垃圾收集 # 72 | 73 | 垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。 74 | 75 | ## 判断一个对象是否可被回收 ## 76 | 77 | ### 1. 引用计数算法 ### 78 | 79 | 为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。 80 | 81 | 在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。 82 | 83 | ### 2. 可达性分析算法 ### 84 | 85 | 以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。 86 | 87 | Java中以一系列"GC Roots"对象作为起点,如果一个对象的引用链可以最终追溯到"GC Roots"对象,那就天下太平。 88 | 否则如果只是A对象引用B,B对象又引用A,A,B引用链均为能达到"GC Roots"的话,那它俩将会被虚拟机宣判符合死亡条件,具有被垃圾回收器回收的资格。 89 | 90 | Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容: 91 | 92 | - 虚拟机栈中局部变量表中引用的对象 93 | - 本地方法栈中 JNI 中引用的对象 94 | - 方法区中类静态属性引用的对象 95 | - 方法区中的常量引用的对象 96 | 97 | 一个对象可以属于多个root,GC Roots有以下几种: 98 | - Class 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的Java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots. 99 | - Thread 活着的线程 100 | - Stack Local Java方法的local变量或参数 101 | - JNI Local JNI方法的local变量或参数 102 | - JNI Global 全局JNI引用 103 | - Monitor Used 用于同步的监控对象 104 | - Held by JVM 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。 105 | 106 | ### 3. 方法区的回收 ### 107 | 108 | 因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。 109 | 110 | 主要是对常量池的回收和对类的卸载。 111 | 112 | 为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。 113 | 114 | 类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载: 115 | 116 | - 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。 117 | - 加载该类的 ClassLoader 已经被回收。 118 | - 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 119 | 120 | ### 4. finalize() ### 121 | 122 | finalize()是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它finalize()方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运)。 123 | 124 | 类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。 125 | 126 | 当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。 127 | 128 | ## 引用类型 ## 129 | 130 | 无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。 131 | 132 | Java 提供了四种强度不同的引用类型。 133 | 134 | ### 1. 强引用 ### 135 | 136 | 被强引用关联的对象不会被回收。 137 | 138 | 使用 new 一个新对象的方式来创建强引用。 139 | 140 | Object obj = new Object(); 141 | 142 | ### 2. 软引用 ### 143 | 144 | 被软引用关联的对象只有在内存不够的情况下才会被回收。 145 | 146 | 使用 SoftReference 类来创建软引用。 147 | 148 | 用于实现对内存敏感的高速缓存。 149 | 150 | Object obj = new Object(); 151 | SoftReference sf = new SoftReference(obj); 152 | obj = null; // 使对象只被软引用关联 153 | 154 | ### 3. 弱引用 ### 155 | 156 | 被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。 157 | 158 | 使用 WeakReference 类来创建弱引用。 159 | 160 | 用于引用占用内存空间较大的对象。 161 | 162 | Object obj = new Object(); 163 | WeakReference wf = new WeakReference(obj); 164 | obj = null; 165 | 166 | ### 4. 虚引用 ### 167 | 168 | 又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。 169 | 170 | 为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。 171 | 172 | 使用 PhantomReference 来创建虚引用。 173 | 174 | Object obj = new Object(); 175 | PhantomReference pf = new PhantomReference(obj, null); 176 | obj = null; 177 | 178 | ## 垃圾收集算法 ## 179 | 180 | ### 1. 标记 - 清除 ### 181 | 182 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/005b481b-502b-4e3f-985d-d043c2b330aa.png) 183 | 184 | 在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。 185 | 186 | 在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。 187 | 188 | 在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。 189 | 190 | 不足: 191 | 192 | - 标记和清除过程效率都不高; 193 | - 会产生大量不连续的内存碎片,导致无法给大对象分配内存。 194 | 195 | ### 2. 标记 - 整理 ### 196 | 197 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ccd773a5-ad38-4022-895c-7ac318f31437.png) 198 | 199 | 让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 200 | 201 | 优点: 202 | 203 | - 不会产生内存碎片 204 | 205 | 不足: 206 | 207 | - 需要移动大量对象,处理效率比较低。 208 | 209 | ### 3. 复制 ### 210 | 211 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/b2b77b9e-958c-4016-8ae5-9c6edd83871e.png) 212 | 213 | 将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。 214 | 215 | 主要不足是只使用了内存的一半。 216 | 217 | 现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。 218 | 219 | HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。 220 | 221 | ### 4. 分代收集 ### 222 | 223 | 现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 224 | 225 | 一般将堆分为新生代和老年代。 226 | 227 | - 新生代使用:复制算法 228 | - 老年代使用:标记-清除 或者 标记-整理 算法 229 | 230 | ## 垃圾收集器 ## 231 | 232 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c625baa0-dde6-449e-93df-c3a67f2f430f.jpg) 233 | 234 | 以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。 235 | 236 | - 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程; 237 | - 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。 238 | 239 | ### 1. Serial 收集器 ### 240 | 241 | Serial 翻译为串行,也就是说它以串行的方式执行。 242 | 243 | 它是单线程的收集器,只会使用一个线程进行垃圾收集工作。 244 | 245 | 配合CMS使用。 246 | 247 | 它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。 248 | 249 | 它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。 250 | 251 | ### 2. ParNew 收集器 ### 252 | 253 | 它是 Serial 收集器的多线程版本。 254 | 255 | 它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。 256 | 257 | ### 3. Parallel Scavenge 收集器 ### 258 | 259 | 与 ParNew 一样是多线程收集器。 260 | 261 | 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。 262 | 263 | ### 4. Serial Old 收集器 ### 264 | 265 | 是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。单线程,串行。 266 | 267 | ### 5. Parallel Old 收集器 ### 268 | 269 | 是 Parallel Scavenge 收集器的老年代版本。 270 | 271 | ### 6. CMS 收集器 ### 272 | 273 | CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。 274 | 275 | 老年代垃圾收集器。 276 | 277 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/62e77997-6957-4b68-8d12-bfd609bb2c68.jpg) 278 | 279 | 分为以下四个流程: 280 | 281 | - 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 282 | - 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。 283 | - 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。 284 | - 并发清除:不需要停顿。 285 | 286 | 在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。 287 | 288 | 具有以下缺点: 289 | 290 | - 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。 291 | - 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。 292 | - 标记 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 293 | 294 | ### 7. G1 收集器 ### 295 | 296 | G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。 297 | 298 | 堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。 299 | 300 | # 三、内存分配与回收策略 # 301 | 302 | ## Minor GC 和 Full GC ## 303 | 304 | - Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 305 | 306 | - Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。 307 | 308 | ## 内存分配策略 ## 309 | 310 | ### 1. 对象优先在 Eden 分配 ### 311 | 312 | 大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。 313 | 314 | ### 2. 大对象直接进入老年代 ### 315 | 316 | 大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。 317 | 318 | 经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。 319 | 320 | -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。 321 | 322 | ### 3. 长期存活的对象进入老年代 ### 323 | 324 | 为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。 325 | 326 | -XX:MaxTenuringThreshold 用来定义年龄的阈值。 327 | 328 | ### 4. 动态对象年龄判定 ### 329 | 330 | 虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。 331 | 332 | ### 5. 空间分配担保 ### 333 | 334 | 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。 335 | 336 | 如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 337 | 338 | ## Full GC 的触发条件 ## 339 | 340 | 对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件: 341 | 342 | ### 1. 调用 System.gc() ### 343 | 344 | 只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。 345 | 346 | ### 2. 老年代空间不足 ### 347 | 348 | 老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。 349 | 350 | 为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。 351 | 352 | ### 3. 空间分配担保失败 ### 353 | 354 | 使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。 355 | 356 | ### 4. JDK 1.7 及以前的永久代空间不足 ### 357 | 358 | 在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。 359 | 360 | 当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。 361 | 362 | 为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 363 | 364 | ### 5. Concurrent Mode Failure ### 365 | 366 | 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。 367 | 368 | # 四、类加载机制 # 369 | 370 | 类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。 371 | 372 | ## 类的生命周期 ## 373 | 374 | 包括以下 7 个阶段: 375 | 376 | - 加载(Loading) 377 | - 验证(Verification) 378 | - 准备(Preparation) 379 | - 解析(Resolution) 380 | - 初始化(Initialization) 381 | - 使用(Using) 382 | - 卸载(Unloading) 383 | 384 | ## 类加载过程 ## 385 | 386 | 包含了加载、验证、准备、解析和初始化这 5 个阶段。 387 | 388 | ### 1. 加载 ### 389 | 390 | 加载是类加载的一个阶段,注意不要混淆。 391 | 392 | 加载过程完成以下三件事: 393 | 394 | - 通过类的完全限定名称获取定义该类的二进制字节流。 395 | - 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。 396 | - 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。 397 | 398 | ### 2. 验证 ### 399 | 400 | 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 401 | 402 | ### 3. 准备 ### 403 | 404 | 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。 405 | 406 | 实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。 407 | 408 | ### 4. 解析 ### 409 | 410 | 将常量池的符号引用替换为直接引用的过程。 411 | 412 | 其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。 413 | 414 | ### 5. 初始化 ### 415 | 416 | 初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 () 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。 417 | 418 | ## 双亲委派机制 ## 419 | 420 | 当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。 421 | 422 | ### 作用 ### 423 | 424 | 1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。 425 | 426 | 虚拟机只有在两个类的类名相同且加载该类的加载器均相同的情况下才判定这是一个类。若不采用双亲委派机制,同一个类有可能被多个类加载器加载,这样该类会被识别为两个不同的类,相互赋值时会有问题。 427 | 428 | 双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。 429 | 430 | 2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。 431 | 432 | -------------------------------------------------------------------------------- /Java/JavaEE.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scu-zzy/Java-note/7fba805f591b59e7766413dc7524f626a99b9238/Java/JavaEE.md -------------------------------------------------------------------------------- /Java/Java并发/leetcode-多线程.md: -------------------------------------------------------------------------------- 1 | ## 生产者消费者 ## 2 | 3 | ### wait()和notify()方法的实现 ### 4 | 5 | public class Test1 { 6 | private static Integer count = 0; 7 | private static final Integer FULL = 10; 8 | private static String LOCK = "lock"; 9 | 10 | public static void main(String[] args) { 11 | Test1 test1 = new Test1(); 12 | new Thread(test1.new Producer()).start(); 13 | new Thread(test1.new Consumer()).start(); 14 | new Thread(test1.new Producer()).start(); 15 | new Thread(test1.new Consumer()).start(); 16 | } 17 | 18 | class Producer implements Runnable { 19 | @Override 20 | public void run() { 21 | while (true){ 22 | synchronized (LOCK) { 23 | while (count == FULL) { 24 | try { 25 | LOCK.wait(); 26 | } catch (Exception e) { 27 | e.printStackTrace(); 28 | } 29 | } 30 | count++; 31 | System.out.println(Thread.currentThread().getName() + "生产者生产,目前总共有" + count); 32 | LOCK.notifyAll(); 33 | } 34 | } 35 | } 36 | } 37 | 38 | class Consumer implements Runnable { 39 | @Override 40 | public void run() { 41 | while (true){ 42 | synchronized (LOCK) { 43 | while (count == 0) { 44 | try { 45 | LOCK.wait(); 46 | } catch (Exception e) { 47 | e.printStackTrace(); 48 | } 49 | } 50 | count--; 51 | System.out.println(Thread.currentThread().getName() + "消费者消费,目前总共有" + count); 52 | LOCK.notifyAll(); 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | ### 可重入锁ReentrantLock的实现(await和signal) ### 60 | 61 | java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,通过对lock的lock()方法和unlock()方法实现了对锁的显示控制,而synchronize()则是对锁的隐性控制。 62 | 可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响,简单来说,该锁维护这一个与获取锁相关的计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,函数调用结束计数器就减1,然后锁需要被释放两次才能获得真正释放。已经获取锁的线程进入其他需要相同锁的同步代码块不会被阻塞。 63 | 64 | import java.util.concurrent.locks.Condition; 65 | import java.util.concurrent.locks.Lock; 66 | import java.util.concurrent.locks.ReentrantLock; 67 | 68 | public class Test2 { 69 | private static Integer count = 0; 70 | private static final Integer FULL = 10; 71 | //创建一个锁对象 72 | private Lock lock = new ReentrantLock(); 73 | //创建两个条件变量,一个为缓冲区非满,一个为缓冲区非空 74 | private final Condition notFull = lock.newCondition(); 75 | private final Condition notEmpty = lock.newCondition(); 76 | 77 | public static void main(String[] args) { 78 | Test2 test2 = new Test2(); 79 | new Thread(test2.new Producer()).start(); 80 | new Thread(test2.new Consumer()).start(); 81 | new Thread(test2.new Producer()).start(); 82 | new Thread(test2.new Consumer()).start(); 83 | new Thread(test2.new Producer()).start(); 84 | new Thread(test2.new Consumer()).start(); 85 | new Thread(test2.new Producer()).start(); 86 | new Thread(test2.new Consumer()).start(); 87 | } 88 | class Producer implements Runnable { 89 | @Override 90 | public void run() { 91 | while (true){ 92 | //获取锁 93 | lock.lock(); 94 | try { 95 | while (count == FULL) { 96 | try { 97 | notFull.await(); 98 | } catch (InterruptedException e) { 99 | e.printStackTrace(); 100 | } 101 | } 102 | count++; 103 | System.out.println(Thread.currentThread().getName() 104 | + "生产者生产,目前总共有" + count); 105 | //唤醒消费者 106 | notEmpty.signal(); 107 | } finally { 108 | //释放锁 109 | lock.unlock(); 110 | } 111 | } 112 | } 113 | } 114 | 115 | class Consumer implements Runnable { 116 | @Override 117 | public void run() { 118 | while (true){ 119 | lock.lock(); 120 | try { 121 | while (count == 0) { 122 | try { 123 | notEmpty.await(); 124 | } catch (Exception e) { 125 | 126 | e.printStackTrace(); 127 | } 128 | } 129 | count--; 130 | System.out.println(Thread.currentThread().getName() 131 | + "消费者消费,目前总共有" + count); 132 | notFull.signal(); 133 | } finally { 134 | lock.unlock(); 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | ### 信号量 ### 142 | 143 | import java.util.concurrent.Semaphore; 144 | 145 | public class Test4 { 146 | private static Integer count = 0; 147 | //创建三个信号量 148 | final Semaphore notFull = new Semaphore(10); 149 | final Semaphore notEmpty = new Semaphore(0); 150 | final Semaphore mutex = new Semaphore(1); 151 | 152 | public static void main(String[] args) { 153 | Test4 test4 = new Test4(); 154 | new Thread(test4.new Producer()).start(); 155 | new Thread(test4.new Consumer()).start(); 156 | new Thread(test4.new Producer()).start(); 157 | new Thread(test4.new Consumer()).start(); 158 | } 159 | class Producer implements Runnable { 160 | @Override 161 | public void run() { 162 | while (true){ 163 | try { 164 | notFull.acquire(); 165 | mutex.acquire(); 166 | count++; 167 | System.out.println(Thread.currentThread().getName() 168 | + "生产者生产,目前总共有" + count); 169 | } catch (InterruptedException e) { 170 | e.printStackTrace(); 171 | } finally { 172 | mutex.release(); 173 | notEmpty.release(); 174 | } 175 | } 176 | } 177 | } 178 | 179 | class Consumer implements Runnable { 180 | 181 | @Override 182 | public void run() { 183 | while (true){ 184 | try { 185 | notEmpty.acquire(); 186 | mutex.acquire(); 187 | count--; 188 | System.out.println(Thread.currentThread().getName() 189 | + "消费者消费,目前总共有" + count); 190 | } catch (InterruptedException e) { 191 | e.printStackTrace(); 192 | } finally { 193 | mutex.release(); 194 | notFull.release(); 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | ### 阻塞队列 ### 202 | 203 | 有点问题 204 | 205 | 下面来看由阻塞队列实现的生产者消费者模型,这里我们使用take()和put()方法,这里生产者和生产者,消费者和消费者之间不存在同步,所以会出现连续生成和连续消费的现象 206 | 207 | import java.util.concurrent.ArrayBlockingQueue; 208 | import java.util.concurrent.BlockingQueue; 209 | 210 | public class Test3 { 211 | 212 | //创建一个阻塞队列 213 | final BlockingQueue blockingQueue = new ArrayBlockingQueue<>(10); 214 | 215 | public static void main(String[] args) { 216 | Test3 test3 = new Test3(); 217 | new Thread(test3.new Producer()).start(); 218 | new Thread(test3.new Consumer()).start(); 219 | new Thread(test3.new Producer()).start(); 220 | new Thread(test3.new Consumer()).start(); 221 | } 222 | class Producer implements Runnable { 223 | @Override 224 | public void run() { 225 | while (true){ 226 | try { 227 | blockingQueue.put(1); 228 | System.out.println(Thread.currentThread().getName() 229 | + "生产者生产,目前总共有" + blockingQueue.size()); 230 | } catch (InterruptedException e) { 231 | e.printStackTrace(); 232 | } 233 | } 234 | } 235 | } 236 | 237 | class Consumer implements Runnable { 238 | 239 | @Override 240 | public void run() { 241 | while (true){ 242 | try { 243 | blockingQueue.take(); 244 | System.out.println(Thread.currentThread().getName() 245 | + "消费者消费,目前总共有" + blockingQueue.size()); 246 | } catch (InterruptedException e) { 247 | e.printStackTrace(); 248 | } 249 | } 250 | } 251 | } 252 | } 253 | 254 | ## 读者写者问题 ## 255 | 256 | 信号量 257 | 258 | import java.util.concurrent.Semaphore; 259 | 260 | public class Test5 { 261 | private static Integer count = 0;//记录读者的数量 262 | //创建信号量 263 | final Semaphore countMutex = new Semaphore(1);//对count加锁 264 | final Semaphore dataMutex = new Semaphore(1);//对读者和写者加锁 265 | 266 | public static void main(String[] args) { 267 | Test5 test5 = new Test5(); 268 | new Thread(test5.new Writer()).start(); 269 | new Thread(test5.new Reader()).start(); 270 | } 271 | class Reader implements Runnable { 272 | @Override 273 | public void run() { 274 | while (true){ 275 | try { 276 | countMutex.acquire(); 277 | count++; 278 | if(count == 1) dataMutex.acquire();//第一个读者来时,加锁,阻止写者访问 279 | countMutex.release(); 280 | System.out.println("read"); 281 | countMutex.acquire(); 282 | count--; 283 | if(count == 0) dataMutex.release();//最后一个读者走时,释放锁 284 | countMutex.release(); 285 | } catch (InterruptedException e) { 286 | e.printStackTrace(); 287 | } 288 | } 289 | } 290 | } 291 | 292 | class Writer implements Runnable { 293 | 294 | @Override 295 | public void run() { 296 | while (true){ 297 | try { 298 | dataMutex.acquire(); 299 | System.out.println("Write"); 300 | dataMutex.release(); 301 | } catch (InterruptedException e) { 302 | e.printStackTrace(); 303 | } 304 | } 305 | } 306 | } 307 | } 308 | 309 | ## 哲学家进餐问题 ## 310 | 311 | 每个哲学家必须确定自己左右手的筷子都可用的时候,才能同时拿起两只筷子进餐,吃完之后同时放下两只筷子。 312 | 313 | public class Test6 { 314 | /*每个哲学家相当于一个线程*/ 315 | static class Philosopher extends Thread{ 316 | private String name; 317 | private Fork fork; 318 | public Philosopher(String name,Fork fork){ 319 | super(name); 320 | this.name=name; 321 | this.fork=fork; 322 | } 323 | 324 | public void run(){ 325 | while(true){ 326 | thinking(); 327 | fork.takeFork(); 328 | eating(); 329 | fork.putFork(); 330 | } 331 | 332 | } 333 | 334 | public void eating(){ 335 | System.out.println("I am Eating:"+name); 336 | try { 337 | sleep(1000);//模拟吃饭,占用一段时间资源 338 | } catch (InterruptedException e) { 339 | // TODO Auto-generated catch block 340 | e.printStackTrace(); 341 | } 342 | } 343 | 344 | 345 | public void thinking(){ 346 | System.out.println("I am Thinking:"+name); 347 | try { 348 | sleep(1000);//模拟思考 349 | } catch (InterruptedException e) { 350 | // TODO Auto-generated catch block 351 | e.printStackTrace(); 352 | } 353 | } 354 | } 355 | 356 | static class Fork{ 357 | /*5只筷子,初始为都未被用*/ 358 | private boolean[] used={false,false,false,false,false,false}; 359 | 360 | /*只有当左右手的筷子都未被使用时,才允许获取筷子,且必须同时获取左右手筷子*/ 361 | public synchronized void takeFork(){ 362 | String name = Thread.currentThread().getName(); 363 | int i = Integer.parseInt(name); 364 | while(used[i]||used[(i+1)%5]){ 365 | try { 366 | wait();//如果左右手有一只正被使用,等待 367 | } catch (InterruptedException e) { 368 | // TODO Auto-generated catch block 369 | e.printStackTrace(); 370 | } 371 | } 372 | used[i ]= true; 373 | used[(i+1)%5]=true; 374 | } 375 | 376 | /*必须同时释放左右手的筷子*/ 377 | public synchronized void putFork(){ 378 | String name = Thread.currentThread().getName(); 379 | int i = Integer.parseInt(name); 380 | 381 | used[i ]= false; 382 | used[(i+1)%5]=false; 383 | notifyAll();//唤醒其他线程 384 | } 385 | } 386 | 387 | public static void main(String []args){ 388 | Fork fork = new Fork(); 389 | new Philosopher("0",fork).start(); 390 | new Philosopher("1",fork).start(); 391 | new Philosopher("2",fork).start(); 392 | new Philosopher("3",fork).start(); 393 | new Philosopher("4",fork).start(); 394 | } 395 | } 396 | 397 | ## 1114. 按序打印 ## 398 | 399 | 我们提供了一个类: 400 | 401 | public class Foo { 402 | public void one() { print("one"); } 403 | public void two() { print("two"); } 404 | public void three() { print("three"); } 405 | } 406 | 407 | 三个不同的线程将会共用一个 Foo 实例。 408 | 409 | 410 | - 线程 A 将会调用 one() 方法 411 | - 线程 B 将会调用 two() 方法 412 | - 线程 C 将会调用 three() 方法 413 | 414 | 415 | 请设计修改程序,以确保 two() 方法在 one() 方法之后被执行,three() 方法在 two() 方法之后被执行。 416 | 417 | 示例 1: 418 | 419 | 输入: [1,2,3] 420 | 输出: "onetwothree" 421 | 解释: 422 | 有三个线程会被异步启动。 423 | 输入 [1,2,3] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 two() 方法,线程 C 将会调用 three() 方法。 424 | 正确的输出是 "onetwothree"。 425 | 426 | 示例 2: 427 | 428 | 输入: [1,3,2] 429 | 输出: "onetwothree" 430 | 解释: 431 | 输入 [1,3,2] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 three() 方法,线程 C 将会调用 two() 方法。 432 | 正确的输出是 "onetwothree"。 433 | 434 | 注意: 435 | 尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。 436 | 你看到的输入格式主要是为了确保测试的全面性。 437 | 438 | 思路: 439 | 440 | 方法一:原子类 441 | 442 | - 首先初始化共享变量 firstJobDone 和 secondJobDone,初始值表示所有方法未执行。 443 | 444 | 445 | - 方法 first() 没有依赖关系,可以直接执行。在方法最后更新变量 firstJobDone 表示该方法执行完成。 446 | 447 | 448 | - 方法 second() 中,检查 firstJobDone 的状态。如果未更新则进入等待状态,否则执行方法 second()。在方法末尾,更新变量 secondJobDone 表示方法 second() 执行完成。 449 | 450 | 451 | - 方法 third() 中,检查 secondJobDone 的状态。与方法 second() 类似,执行 third() 之前,需要先等待 secondJobDone 的状态。 452 | 453 | class Foo { 454 | 455 | private AtomicInteger firstJobDone = new AtomicInteger(0); 456 | private AtomicInteger secondJobDone = new AtomicInteger(0); 457 | 458 | public Foo() {} 459 | 460 | public void first(Runnable printFirst) throws InterruptedException { 461 | // printFirst.run() outputs "first". 462 | printFirst.run(); 463 | // mark the first job as done, by increasing its count. 464 | firstJobDone.incrementAndGet(); 465 | } 466 | 467 | public void second(Runnable printSecond) throws InterruptedException { 468 | while (firstJobDone.get() != 1) { 469 | // waiting for the first job to be done. 470 | } 471 | // printSecond.run() outputs "second". 472 | printSecond.run(); 473 | // mark the second as done, by increasing its count. 474 | secondJobDone.incrementAndGet(); 475 | } 476 | 477 | public void third(Runnable printThird) throws InterruptedException { 478 | while (secondJobDone.get() != 1) { 479 | // waiting for the second job to be done. 480 | } 481 | // printThird.run() outputs "third". 482 | printThird.run(); 483 | } 484 | } 485 | 486 | 方法二:锁加wait 487 | 488 | class Foo { 489 | 490 | private boolean firstFinished; 491 | private boolean secondFinished; 492 | private Object lock = new Object(); 493 | 494 | public Foo() { 495 | 496 | } 497 | 498 | public void first(Runnable printFirst) throws InterruptedException { 499 | 500 | synchronized (lock) { 501 | // printFirst.run() outputs "first". Do not change or remove this line. 502 | printFirst.run(); 503 | firstFinished = true; 504 | lock.notifyAll(); 505 | } 506 | } 507 | 508 | public void second(Runnable printSecond) throws InterruptedException { 509 | 510 | synchronized (lock) { 511 | while (!firstFinished) { 512 | lock.wait(); 513 | } 514 | 515 | // printSecond.run() outputs "second". Do not change or remove this line. 516 | printSecond.run(); 517 | secondFinished = true; 518 | lock.notifyAll(); 519 | } 520 | } 521 | 522 | public void third(Runnable printThird) throws InterruptedException { 523 | 524 | synchronized (lock) { 525 | while (!secondFinished) { 526 | lock.wait(); 527 | } 528 | 529 | // printThird.run() outputs "third". Do not change or remove this line. 530 | printThird.run(); 531 | } 532 | } 533 | } 534 | 535 | 536 | ## 1115. 交替打印FooBar ## 537 | 538 | 我们提供一个类: 539 | 540 | class FooBar { 541 | public void foo() { 542 | for (int i = 0; i < n; i++) { 543 | print("foo"); 544 | } 545 | } 546 | 547 | public void bar() { 548 | for (int i = 0; i < n; i++) { 549 | print("bar"); 550 | } 551 | } 552 | } 553 | 554 | 两个不同的线程将会共用一个 FooBar 实例。其中一个线程将会调用 foo() 方法,另一个线程将会调用 bar() 方法。 555 | 556 | 请设计修改程序,以确保 "foobar" 被输出 n 次。 557 | 558 | 1、信号量 559 | 560 | class FooBar { 561 | private int n; 562 | 563 | public FooBar(int n) { 564 | this.n = n; 565 | } 566 | 567 | Semaphore foo = new Semaphore(1); 568 | Semaphore bar = new Semaphore(0); 569 | 570 | public void foo(Runnable printFoo) throws InterruptedException { 571 | for (int i = 0; i < n; i++) { 572 | foo.acquire(); 573 | printFoo.run(); 574 | bar.release(); 575 | } 576 | } 577 | 578 | public void bar(Runnable printBar) throws InterruptedException { 579 | for (int i = 0; i < n; i++) { 580 | bar.acquire(); 581 | printBar.run(); 582 | foo.release(); 583 | } 584 | } 585 | } 586 | 587 | 2、volite变量控制 588 | 589 | class FooBar { 590 | private int n; 591 | 592 | public FooBar(int n) { 593 | this.n = n; 594 | } 595 | 596 | volatile boolean permitFoo = true; 597 | 598 | public void foo(Runnable printFoo) throws InterruptedException { 599 | for (int i = 0; i < n; ) { 600 | if(permitFoo) { 601 | printFoo.run(); 602 | i++; 603 | permitFoo = false; 604 | } 605 | } 606 | } 607 | 608 | public void bar(Runnable printBar) throws InterruptedException { 609 | for (int i = 0; i < n; ) { 610 | if(!permitFoo) { 611 | printBar.run(); 612 | i++; 613 | permitFoo = true; 614 | } 615 | } 616 | } 617 | } 618 | 619 | 3、锁+wait 620 | 621 | class FooBar { 622 | private int n; 623 | private int count = 0; 624 | 625 | public FooBar(int n) { 626 | this.n = n; 627 | } 628 | 629 | public synchronized void foo(Runnable printFoo) throws InterruptedException { 630 | 631 | for (int i = 0; i < n; i++) { 632 | while(count == 1) this.wait(); 633 | // printFoo.run() outputs "foo". Do not change or remove this line. 634 | printFoo.run(); 635 | count++; 636 | this.notifyAll(); 637 | } 638 | } 639 | 640 | public synchronized void bar(Runnable printBar) throws InterruptedException { 641 | 642 | for (int i = 0; i < n; i++) { 643 | while(count == 0) this.wait(); 644 | // printBar.run() outputs "bar". Do not change or remove this line. 645 | printBar.run(); 646 | count--; 647 | this.notifyAll(); 648 | } 649 | } 650 | } -------------------------------------------------------------------------------- /Java/Java并发/并发容器.md: -------------------------------------------------------------------------------- 1 | # 一 JDK 提供的并发容器总结 # 2 | 3 | JDK 提供的这些容器大部分在 java.util.concurrent 包中。 4 | 5 | - ConcurrentHashMap: 线程安全的 HashMap 6 | - CopyOnWriteArrayList: 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector. 7 | - ConcurrentLinkedQueue: 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。 8 | - BlockingQueue: 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。 9 | - ConcurrentSkipListMap: 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。 10 | 11 | # 二 ConcurrentHashMap # 12 | 13 | 我们知道 HashMap 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 Collections.synchronizedMap() 方法来包装我们的 HashMap。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。 14 | 15 | ### 写操作put ### 16 | 17 | 所以就有了 HashMap 的线程安全版本—— ConcurrentHashMap 的诞生。在 ConcurrentHashMap 中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时,1.7使用通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问,1.8的并发控制使用 synchronized 和 CAS 来操作。 18 | 19 | ### 读操作get ### 20 | 21 | 对读不加锁,get操作可以无锁是由于结点Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。 22 | 23 | ### 扩容操作 ### 24 | 25 | 首先每个线程承担不小于 16 个槽中的元素的扩容,然后从右向左划分 16 个槽给当前线程去迁移,每当开始迁移一个槽中的元素的时候,线程会锁住当前槽中列表的头元素,假设这时候正好有 get 请求过来会仍旧在旧的列表中访问,如果是插入、修改、删除、合并、compute 等操作时遇到 ForwardingNode,当前线程会加入扩容大军帮忙一起扩容,扩容结束后再做元素的更新操作,如果没有遇到正在迁移的结点,那么可以进行添加。 26 | 27 | ### ConcurrentHashMap 和 Hashtable 的区别 28 | 29 | ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 30 | 31 | - 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; 32 | 33 | 34 | - 实现线程安全的方式(重要): ① 在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 35 | 36 | ### JDK1.7 37 | 38 | 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 39 | 40 | ### JDK1.8 41 | 42 | ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。 43 | 44 | synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。 45 | 46 | # 三 CopyOnWriteArrayList # 47 | 48 | CopyOnWriteArrayList 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。 49 | 50 | CopyOnWriteArrayList 类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。 51 | 52 | 读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。 53 | 54 | CopyOnWriteArrayList 写入操作 add() 方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。 55 | 56 | # 四 ConcurrentLinkedQueue # 57 | 58 | Java 提供的线程安全的 Queue 可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。 59 | 60 | 从名字可以看出,ConcurrentLinkedQueue这个队列使用链表作为其数据结构.ConcurrentLinkedQueue 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。 61 | 62 | ConcurrentLinkedQueue 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。 63 | 64 | # 五 BlockingQueue # 65 | 66 | 阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /Java/Java并发/线程池.md: -------------------------------------------------------------------------------- 1 | # 一 使用线程池的好处 # 2 | 3 | 线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。 4 | 5 | - **降低资源消耗。**通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 6 | - **提高响应速度。**当任务到达时,任务可以不需要的等到线程创建就能立即执行。 7 | - **提高线程的可管理性。**线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 8 | 9 | # 二 Executor 框架 # 10 | 11 | ## 2.1 简介 ## 12 | 13 | Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。 14 | 15 | Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。 16 | 17 | > 补充:this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。 18 | 19 | Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。 20 | 21 | ## 2.2 Executor 框架结构(主要由三大部分组成) ## 22 | 23 | ### 1) 任务(Runnable /Callable) ### 24 | 25 | 执行任务需要实现的 Runnable 接口 或 Callable接口。Runnable 接口或 Callable 接口 实现类都可以被 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。 26 | 27 | ### 2) 任务的执行(Executor) ### 28 | 29 | 包括任务执行机制的核心接口 Executor ,以及继承自 Executor 接口的 ExecutorService 接口。ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 这两个关键类实现了 ExecutorService 接口。 30 | 31 | 这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 ThreadPoolExecutor 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。 32 | 33 | ThreadPoolExecutor 类描述: 34 | 35 | //AbstractExecutorService实现了ExecutorService接口 36 | public class ThreadPoolExecutor extends AbstractExecutorService 37 | 38 | ScheduledThreadPoolExecutor 类描述: 39 | 40 | //ScheduledExecutorService实现了ExecutorService接口 41 | public class ScheduledThreadPoolExecutor 42 | extends ThreadPoolExecutor 43 | implements ScheduledExecutorService 44 | 45 | ### 3) 异步计算的结果(Future) ### 46 | 47 | Future 接口以及 Future 接口的实现类 FutureTask 类都可以代表异步计算的结果。 48 | 49 | 当我们把 Runnable接口 或 Callable 接口 的实现类提交给 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。(调用 submit() 方法时会返回一个 FutureTask 对象) 50 | 51 | ## 2.3 Executor 框架的使用 ## 52 | 53 | 1. **主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象。** 54 | 1. **把创建完成的实现 Runnable/Callable接口的 对象直接交给 ExecutorService 执行**: ExecutorService.execute(Runnable command))或者也可以把 Runnable 对象或Callable 对象提交给 ExecutorService 执行(ExecutorService.submit(Runnable task)或 ExecutorService.submit(Callable task))。 55 | 1. **如果执行 ExecutorService.submit(…),ExecutorService 将返回一个实现Future接口的对象**(我们刚刚也提到过了执行 execute()方法和 submit()方法的区别,submit()会返回一个 FutureTask 对象)。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。 56 | 1. **最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。** 57 | 58 | # 三 (重要)ThreadPoolExecutor 类简单介绍 # 59 | 60 | 线程池实现类 ThreadPoolExecutor 是 Executor 框架最核心的类。 61 | 62 | ## 3.1 ThreadPoolExecutor 类分析 ## 63 | 64 | ThreadPoolExecutor 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生 65 | 66 | /** 67 | * 用给定的初始参数创建一个新的ThreadPoolExecutor。 68 | */ 69 | public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 70 | int maximumPoolSize,//线程池的最大线程数 71 | long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间 72 | TimeUnit unit,//时间单位 73 | BlockingQueue workQueue,//任务队列,用来储存等待执行任务的队列 74 | ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可 75 | RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 76 | ) { 77 | if (corePoolSize < 0 || 78 | maximumPoolSize <= 0 || 79 | maximumPoolSize < corePoolSize || 80 | keepAliveTime < 0) 81 | throw new IllegalArgumentException(); 82 | if (workQueue == null || threadFactory == null || handler == null) 83 | throw new NullPointerException(); 84 | this.corePoolSize = corePoolSize; 85 | this.maximumPoolSize = maximumPoolSize; 86 | this.workQueue = workQueue; 87 | this.keepAliveTime = unit.toNanos(keepAliveTime); 88 | this.threadFactory = threadFactory; 89 | this.handler = handler; 90 | } 91 | 92 | ThreadPoolExecutor 3 个最重要的参数: 93 | 94 | - corePoolSize : 线程池的核心线程数量。核心线程数线程数定义了最小可以同时运行的线程数量。 95 | - maximumPoolSize : 线程池的最大线程数。当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 96 | - workQueue: 任务队列。当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 97 | 98 | ThreadPoolExecutor其他常见参数: 99 | 100 | - keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁; 101 | - unit : keepAliveTime 参数的时间单位。 102 | - threadFactory :executor 创建新线程的时候会用到。 103 | - handler :饱和策略。关于饱和策略下面单独介绍一下。 104 | 105 | ThreadPoolExecutor 饱和策略定义: 106 | 107 | 如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略: 108 | 109 | - ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。 110 | - ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 111 | - ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。 112 | - ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。 113 | 114 | ## 3.2 推荐使用 ThreadPoolExecutor 构造函数创建线程池 ## 115 | 116 | 方式一:通过ThreadPoolExecutor构造函数实现(推荐) 117 | 118 | 方式二:通过 Executor 框架的工具类 Executors 来实现 119 | 120 | - FixedThreadPool 121 | - SingleThreadExecutor 122 | - CachedThreadPool 123 | 124 | 1、《阿里巴巴 Java 开发手册》明确指出,线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 125 | 126 | 2、《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 127 | 128 | > Executors 返回线程池对象的弊端如下: 129 | > 130 | > FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM(内存溢出)。 131 | > CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 132 | 133 | # 四 (重要)ThreadPoolExecutor 使用示例 # 134 | 135 | ## 4.1 示例代码:Runnable+ThreadPoolExecutor ## 136 | 137 | 首先创建一个 Runnable 接口的实现类 138 | 139 | MyRunnable.java 140 | 141 | import java.util.Date; 142 | 143 | /** 144 | * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 145 | */ 146 | public class MyRunnable implements Runnable { 147 | 148 | private String command; 149 | 150 | public MyRunnable(String s) { 151 | this.command = s; 152 | } 153 | 154 | @Override 155 | public void run() { 156 | System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); 157 | processCommand(); 158 | System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); 159 | } 160 | 161 | private void processCommand() { 162 | try { 163 | Thread.sleep(5000); 164 | } catch (InterruptedException e) { 165 | e.printStackTrace(); 166 | } 167 | } 168 | 169 | @Override 170 | public String toString() { 171 | return this.command; 172 | } 173 | } 174 | 175 | ThreadPoolExecutorDemo.java 176 | 177 | import java.util.concurrent.ArrayBlockingQueue; 178 | import java.util.concurrent.ThreadPoolExecutor; 179 | import java.util.concurrent.TimeUnit; 180 | 181 | public class ThreadPoolExecutorDemo { 182 | 183 | private static final int CORE_POOL_SIZE = 5; 184 | private static final int MAX_POOL_SIZE = 10; 185 | private static final int QUEUE_CAPACITY = 100; 186 | private static final Long KEEP_ALIVE_TIME = 1L; 187 | public static void main(String[] args) { 188 | 189 | //通过ThreadPoolExecutor构造函数自定义参数创建 190 | ThreadPoolExecutor executor = new ThreadPoolExecutor( 191 | CORE_POOL_SIZE, 192 | MAX_POOL_SIZE, 193 | KEEP_ALIVE_TIME, 194 | TimeUnit.SECONDS, 195 | new ArrayBlockingQueue<>(QUEUE_CAPACITY), 196 | new ThreadPoolExecutor.CallerRunsPolicy()); 197 | 198 | for (int i = 0; i < 10; i++) { 199 | //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) 200 | Runnable worker = new MyRunnable("" + i); 201 | //执行Runnable 202 | executor.execute(worker); 203 | } 204 | //终止线程池 205 | executor.shutdown(); 206 | while (!executor.isTerminated()) { 207 | } 208 | System.out.println("Finished all threads"); 209 | } 210 | } 211 | 212 | 可以看到我们上面的代码指定了: 213 | 214 | - corePoolSize: 核心线程数为 5。 215 | - maximumPoolSize :最大线程数 10 216 | - keepAliveTime : 等待时间为 1L。 217 | - unit: 等待时间的单位为 TimeUnit.SECONDS。 218 | - workQueue:任务队列为 ArrayBlockingQueue,并且容量为 100; 219 | - handler:饱和策略为 CallerRunsPolicy。 220 | 221 | Output: 222 | 223 | pool-1-thread-1 Start. Time = Thu Jul 02 16:08:20 CST 2020 224 | pool-1-thread-5 Start. Time = Thu Jul 02 16:08:20 CST 2020 225 | pool-1-thread-2 Start. Time = Thu Jul 02 16:08:20 CST 2020 226 | pool-1-thread-4 Start. Time = Thu Jul 02 16:08:20 CST 2020 227 | pool-1-thread-3 Start. Time = Thu Jul 02 16:08:20 CST 2020 228 | pool-1-thread-5 End. Time = Thu Jul 02 16:08:25 CST 2020 229 | pool-1-thread-1 End. Time = Thu Jul 02 16:08:25 CST 2020 230 | pool-1-thread-5 Start. Time = Thu Jul 02 16:08:25 CST 2020 231 | pool-1-thread-3 End. Time = Thu Jul 02 16:08:25 CST 2020 232 | pool-1-thread-2 End. Time = Thu Jul 02 16:08:25 CST 2020 233 | pool-1-thread-4 End. Time = Thu Jul 02 16:08:25 CST 2020 234 | pool-1-thread-2 Start. Time = Thu Jul 02 16:08:25 CST 2020 235 | pool-1-thread-3 Start. Time = Thu Jul 02 16:08:25 CST 2020 236 | pool-1-thread-1 Start. Time = Thu Jul 02 16:08:25 CST 2020 237 | pool-1-thread-4 Start. Time = Thu Jul 02 16:08:25 CST 2020 238 | pool-1-thread-5 End. Time = Thu Jul 02 16:08:30 CST 2020 239 | pool-1-thread-4 End. Time = Thu Jul 02 16:08:30 CST 2020 240 | pool-1-thread-3 End. Time = Thu Jul 02 16:08:30 CST 2020 241 | pool-1-thread-2 End. Time = Thu Jul 02 16:08:30 CST 2020 242 | pool-1-thread-1 End. Time = Thu Jul 02 16:08:30 CST 2020 243 | Finished all threads 244 | 245 | ## 4.2 几个常见的对比 ## 246 | 247 | ### Runnable vs Callable ### 248 | 249 | Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是**Callable 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。 250 | 251 | 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。(Executors.callable(Runnable task)或 Executors.callable(Runnable task,Object resule))。 252 | 253 | Runnable.java 254 | 255 | @FunctionalInterface 256 | public interface Runnable { 257 | /** 258 | * 被线程执行,没有返回值也无法抛出异常 259 | */ 260 | public abstract void run(); 261 | } 262 | 263 | Callable.java 264 | 265 | @FunctionalInterface 266 | public interface Callable { 267 | /** 268 | * 计算结果,或在无法这样做时抛出异常。 269 | * @return 计算得出的结果 270 | * @throws 如果无法计算结果,则抛出异常 271 | */ 272 | V call() throws Exception; 273 | } 274 | 275 | ### execute() vs submit() ### 276 | 277 | - **execute()**方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否; 278 | - **submit()**方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 279 | 280 | ### shutdown() VS shutdownNow() ### 281 | 282 | - **shutdown()** :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。 283 | - **shutdownNow()** :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。 284 | 285 | ### isTerminated() VS isShutdown() ### 286 | 287 | - **isShutDown** 当调用 shutdown() 方法后返回为 true。 288 | - **isTerminated** 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true 289 | 290 | ## 4.3 Callable+ThreadPoolExecutor示例代码 ## 291 | 292 | MyCallable.java 293 | 294 | import java.util.concurrent.Callable; 295 | 296 | public class MyCallable implements Callable { 297 | @Override 298 | public String call() throws Exception { 299 | Thread.sleep(1000); 300 | //返回执行当前 Callable 的线程名字 301 | return Thread.currentThread().getName(); 302 | } 303 | } 304 | 305 | CallableDemo.java 306 | 307 | import java.util.ArrayList; 308 | import java.util.Date; 309 | import java.util.List; 310 | import java.util.concurrent.ArrayBlockingQueue; 311 | import java.util.concurrent.Callable; 312 | import java.util.concurrent.ExecutionException; 313 | import java.util.concurrent.Future; 314 | import java.util.concurrent.ThreadPoolExecutor; 315 | import java.util.concurrent.TimeUnit; 316 | 317 | public class CallableDemo { 318 | 319 | private static final int CORE_POOL_SIZE = 5; 320 | private static final int MAX_POOL_SIZE = 10; 321 | private static final int QUEUE_CAPACITY = 100; 322 | private static final Long KEEP_ALIVE_TIME = 1L; 323 | 324 | public static void main(String[] args) { 325 | 326 | //通过ThreadPoolExecutor构造函数自定义参数创建 327 | ThreadPoolExecutor executor = new ThreadPoolExecutor( 328 | CORE_POOL_SIZE, 329 | MAX_POOL_SIZE, 330 | KEEP_ALIVE_TIME, 331 | TimeUnit.SECONDS, 332 | new ArrayBlockingQueue<>(QUEUE_CAPACITY), 333 | new ThreadPoolExecutor.CallerRunsPolicy()); 334 | 335 | List> futureList = new ArrayList<>(); 336 | Callable callable = new MyCallable(); 337 | for (int i = 0; i < 10; i++) { 338 | //提交任务到线程池 339 | Future future = executor.submit(callable); 340 | //将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值 341 | futureList.add(future); 342 | } 343 | for (Future fut : futureList) { 344 | try { 345 | System.out.println(new Date() + "::" + fut.get()); 346 | } catch (InterruptedException | ExecutionException e) { 347 | e.printStackTrace(); 348 | } 349 | } 350 | //关闭线程池 351 | executor.shutdown(); 352 | } 353 | } 354 | 355 | Output: 356 | 357 | Thu Jul 02 16:26:20 CST 2020::pool-1-thread-1 358 | Thu Jul 02 16:26:21 CST 2020::pool-1-thread-2 359 | Thu Jul 02 16:26:21 CST 2020::pool-1-thread-3 360 | Thu Jul 02 16:26:21 CST 2020::pool-1-thread-4 361 | Thu Jul 02 16:26:21 CST 2020::pool-1-thread-5 362 | Thu Jul 02 16:26:21 CST 2020::pool-1-thread-1 363 | Thu Jul 02 16:26:22 CST 2020::pool-1-thread-4 364 | Thu Jul 02 16:26:22 CST 2020::pool-1-thread-2 365 | Thu Jul 02 16:26:22 CST 2020::pool-1-thread-5 366 | Thu Jul 02 16:26:22 CST 2020::pool-1-thread-3 367 | 368 | # 五 几种常见的线程池详解 # 369 | 370 | ## 5.1 FixedThreadPool ## 371 | 372 | ### 5.1.1 介绍 ### 373 | 374 | FixedThreadPool 被称为可重用固定线程数的线程池。通过 Executors 类中的相关源代码来看一下相关实现: 375 | 376 | /** 377 | * 创建一个可重用固定数量线程的线程池 378 | */ 379 | public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { 380 | return new ThreadPoolExecutor(nThreads, nThreads, 381 | 0L, TimeUnit.MILLISECONDS, 382 | new LinkedBlockingQueue(), 383 | threadFactory); 384 | } 385 | 386 | 另外还有一个 FixedThreadPool 的实现方法,和上面的类似,所以这里不多做阐述: 387 | 388 | public static ExecutorService newFixedThreadPool(int nThreads) { 389 | return new ThreadPoolExecutor(nThreads, nThreads, 390 | 0L, TimeUnit.MILLISECONDS, 391 | new LinkedBlockingQueue()); 392 | } 393 | 394 | 从上面源代码可以看出新创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的。 395 | 396 | ### 5.1.2 执行任务过程介绍 ### 397 | 398 | 1. 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务; 399 | 1. 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 LinkedBlockingQueue; 400 | 1. 线程池中的线程执行完 手头的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行; 401 | 402 | ### 5.1.3 为什么不推荐使用FixedThreadPool? ### 403 | 404 | FixedThreadPool 使用无界队列 LinkedBlockingQueue(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 : 405 | 406 | 1. 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize; 407 | 1. 由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置为同一个值。 408 | 1. 由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数; 409 | 1. 运行中的 FixedThreadPool(未执行 shutdown()或 shutdownNow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。 410 | 411 | ## 5.2 SingleThreadExecutor 详解 ## 412 | 413 | ### 5.2.1 介绍 ### 414 | 415 | SingleThreadExecutor 是只有一个线程的线程池。下面看看SingleThreadExecutor 的实现: 416 | 417 | /** 418 | *返回只有一个线程的线程池 419 | */ 420 | public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { 421 | return new FinalizableDelegatedExecutorService 422 | (new ThreadPoolExecutor(1, 1, 423 | 0L, TimeUnit.MILLISECONDS, 424 | new LinkedBlockingQueue(), 425 | threadFactory)); 426 | } 427 | 428 | 429 | ---------- 430 | 431 | public static ExecutorService newSingleThreadExecutor() { 432 | return new FinalizableDelegatedExecutorService 433 | (new ThreadPoolExecutor(1, 1, 434 | 0L, TimeUnit.MILLISECONDS, 435 | new LinkedBlockingQueue())); 436 | } 437 | 438 | 从上面源代码可以看出新创建的 SingleThreadExecutor 的 corePoolSize 和 maximumPoolSize 都被设置为 1.其他参数和 FixedThreadPool 相同。 439 | 440 | ### 5.2.2 执行任务过程介绍 ### 441 | 442 | 1. 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务; 443 | 1. 当前线程池中有一个运行的线程后,将任务加入 LinkedBlockingQueue 444 | 1. 线程执行完当前的任务后,会在循环中反复从LinkedBlockingQueue 中获取任务来执行; 445 | 446 | ### 5.2.3 为什么不推荐使用SingleThreadExecutor? ### 447 | 448 | SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。SingleThreadExecutor 使用无界队列作为线程池的工作队列会对线程池带来的影响与 FixedThreadPool 相同。说简单点就是可能会导致 OOM 449 | 450 | ## 5.3 CachedThreadPool 详解 ## 451 | 452 | ### 5.3.1 介绍 ### 453 | 454 | CachedThreadPool 是一个会根据需要创建新线程的线程池。下面通过源码来看看 CachedThreadPool 的实现: 455 | 456 | /** 457 | * 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。 458 | */ 459 | public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { 460 | return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 461 | 60L, TimeUnit.SECONDS, 462 | new SynchronousQueue(), 463 | threadFactory); 464 | } 465 | 466 | ---------- 467 | 468 | public static ExecutorService newCachedThreadPool() { 469 | return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 470 | 60L, TimeUnit.SECONDS, 471 | new SynchronousQueue()); 472 | } 473 | 474 | CachedThreadPool 的corePoolSize 被设置为空(0),maximumPoolSize被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。 475 | 476 | ### 5.3.2 执行任务过程介绍 ### 477 | 478 | 1. 首先执行 SynchronousQueue.offer(Runnable task) 提交任务到任务队列。如果当前 maximumPool 中有闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行下面的步骤 2; 479 | 1. 当初始 maximumPool 为空,或者 maximumPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1 将失败,此时 CachedThreadPool 会创建新线程执行任务,execute 方法执行完成; 480 | 481 | ### 5.3.3 为什么不推荐使用CachedThreadPool? ### 482 | 483 | CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 484 | 485 | # 六 ScheduledThreadPoolExecutor 详解 # 486 | 487 | ScheduledThreadPoolExecutor 主要用来在给定的延迟后运行任务,或者定期执行任务。 488 | 489 | 这个在实际项目中基本不会被用到。 490 | 491 | # 七 线程池大小确定 # 492 | 493 | 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了上下文切换成本。 494 | 495 | > 上下文切换: 496 | > 497 | > 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。 498 | > 499 | > 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 500 | > 501 | > Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 502 | 503 | 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。 504 | 505 | 但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。 506 | 507 | 有一个简单并且适用面比较广的公式: 508 | 509 | - **CPU 密集型任务(N+1):** 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 510 | - **I/O 密集型任务(2N):** 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。 511 | 512 | ### 如何判断是 CPU 密集任务还是 IO 密集任务? ### 513 | 514 | CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。 -------------------------------------------------------------------------------- /Java/Java网络/socket.md: -------------------------------------------------------------------------------- 1 | # 一、I/O 模型 # 2 | 3 | 一个输入操作通常包括两个阶段: 4 | 5 | - 等待数据准备好 6 | - 从内核向进程复制数据 7 | 8 | 对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。 9 | 10 | Unix 有五种 I/O 模型: 11 | 12 | - 阻塞式 I/O 13 | - 非阻塞式 I/O 14 | - I/O 复用(select 和 poll) 15 | - 信号驱动式 I/O(SIGIO) 16 | - 异步 I/O(AIO) 17 | 18 | ## 阻塞式 I/O ## 19 | 20 | 应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。 21 | 22 | 应该注意到,在阻塞的过程中,其它应用进程还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其它应用进程还可以执行,所以不消耗 CPU 时间,这种模型的 CPU 利用率会比较高。 23 | 24 | 下图中,recvfrom() 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。 25 | 26 | ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); 27 | 28 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1492928416812_4.png) 29 | 30 | ## 非阻塞式 I/O ## 31 | 32 | 应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。 33 | 34 | 由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低。 35 | 36 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1492929000361_5.png) 37 | 38 | ## I/O 复用 ## 39 | 40 | 使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。 41 | 42 | 它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。 43 | 44 | 如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。 45 | 46 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1492929444818_6.png) 47 | 48 | ## 信号驱动 I/O ## 49 | 50 | 应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。 51 | 52 | 相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。 53 | 54 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1492929553651_7.png) 55 | 56 | ## 异步 I/O ## 57 | 58 | 应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。 59 | 60 | 异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。 61 | 62 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1492930243286_8.png) 63 | 64 | ## 五大 I/O 模型比较 ## 65 | 66 | - 同步 I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞。 67 | - 异步 I/O:第二阶段应用进程不会阻塞。 68 | 69 | 同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ,它们的主要区别在第一个阶段。 70 | 71 | 非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。 72 | 73 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1492928105791_3.png) 74 | 75 | # I/O复用 # 76 | 77 | select/poll/epoll 都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。 78 | 79 | ## select和poll ## 80 | 81 | - 注册待侦听的fd(这里的fd创建时最好使用非阻塞) 82 | - 每次调用都去检查这些fd的状态,当有一个或者多个fd就绪的时候返回 83 | - 返回结果中包括已就绪和未就绪的fd 84 | 85 | ### fd数量限制 ### 86 | 87 | 相比select,poll解决了单个进程能够打开的文件描述符数量有限制这个问题:select受限于FD_SIZE的限制,如果修改则需要修改这个宏重新编译内核;而poll通过一个pollfd数组向内核传递需要关注的事件,避开了文件描述符数量限制。 88 | 89 | ### 开销 ### 90 | 91 | 此外,select和poll共同具有的一个很大的缺点就是包含大量fd的数组被整体复制于应用进程缓冲区复制和内核缓冲区之间,开销会随着fd数量增多而线性增大。 92 | 93 | ### 可移植性 ### 94 | 95 | 几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。 96 | 97 | ### 轮询 ### 98 | 99 | 进程不需要通过轮询来获得事件完成的描述符。 100 | 101 | ## epoll ## 102 | 103 | int epoll_create(int size); 104 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 105 | int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 106 | 107 | 事件驱动IO就绪通知的方式-epoll。 108 | 109 | epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符,并通知进程。epoll_wait只返回就绪的fd。 110 | 111 | 从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且基于事件驱动的方式,进程不需要通过轮询来获得事件完成的描述符。 112 | 113 | epoll 仅适用于 Linux OS。 114 | 115 | epoll 比 select 和 poll 更加灵活而且没有描述符数量限制。epoll的fd数量上限是操作系统的最大文件句柄数目,这个数目一般和内存有关,通常远大于1024。 116 | 117 | ### 工作模式 ### 118 | 119 | epoll 的描述符事件有两种触发模式:LT(level trigger)和 ET(edge trigger)。 120 | 121 | #### 1. LT 模式 #### 122 | 123 | 当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。 124 | 125 | #### 2. ET 模式 #### 126 | 127 | 和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。 128 | 129 | 很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。 130 | 131 | -------------------------------------------------------------------------------- /Java/Java集合.md: -------------------------------------------------------------------------------- 1 | # Java集合类总结 # 2 | 3 | 4 | 1. 底层数据结构 5 | 1. 增删改查方式 6 | 1. 初始容量,扩容方式,扩容时机。 7 | 1. 线程安全与否 8 | 1. 是否允许空,是否允许重复,是否有序 9 | 10 | ## Colletion,iterator,comparable ## 11 | 12 | 一般认为Collection是最上层接口,但是hashmap实际上实现的是Map接口。 13 | 14 | iterator是迭代器,是实现iterable接口的类必须要提供的一个东西,能够使用for(i : A) 这种方式实现的类型能提供迭代器。 15 | 16 | 实现comparable接口可以让一个类的实例互相使用compareTo方法进行比较大小,可以自定义比较规则,comparator则是一个通用的比较器,比较指定类型的两个元素之间的大小关系。 17 | 18 | ### iterator 19 | 20 | 1、在Java中Iterator为一个接口,它只提供了迭代了基本规则,在JDK中他是这样定义的:对 collection 进行迭代的迭代器。 21 | 22 | 2、fail-fast机制 23 | 24 | 当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。 25 | 26 | 记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException异常,从而产生fail-fast机制。 27 | 28 | 判断机制: 29 | 30 | 检测 modCount == expectedModCount ,若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。 31 | 32 | expectedModCount 是在Itr中定义的: 33 | 34 | int expectedModCount = ArrayList.this.modCount; 35 | 所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量: 36 | 37 | protected transient int modCount = 0; 38 | ArrayList中无论add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。 39 | 40 | 41 | ## List-ArrayList-Vector-Stack-LinkedList ## 42 | 43 | List接口下的实现类有ArrayList,linkedlist,vector等等。 44 | 45 | ArrayList的扩容方式是1.5倍扩容,这样扩容避免2倍扩容可能浪费空间,是一种折中的方案。 另外他不是线程安全,vector则是线程安全的,它是两倍扩容的。 46 | 47 | linkedlist多用于实现链表。 48 | 49 | ### ArrayList 50 | 51 | 1、ArrayList继承AbstractList抽象父类,实现了List接口(规定了List的操作规范)、RandomAccess(可随机访问)、Cloneable(可拷贝)、Serializable(可序列化)。 52 | 53 | 2、底层数据结构:ArrayList的底层是一个object数组,并且由transient修饰(不会参与序列化)。 54 | 55 | transient Object[] elementData; 56 | 57 | 3、增删改查 58 | 59 | - 增:void add(int index, E element) 60 | 61 | 添加元素时,首先判断索引是否合法,然后检测是否需要扩容,最后使用System.arraycopy方法来完成数组的复制。 62 | 63 | - 删: E remove(int index) 64 | 65 | 删除元素时,同样判断索引是否和法,删除的方式是把被删除元素右边的元素左移,方法同样是使用System.arraycopy进行拷贝。 66 | 67 | clear():ArrayList提供一个清空数组的办法,方法是将所有元素置为null,这样就可以让GC自动回收掉没有被引用的元素了。 68 | 69 | - 改: E set(int index, E element) 70 | 71 | 修改元素时,只需要检查下标即可进行修改操作。 72 | 73 | - 查: E get(int index) 74 | 75 | 只需要检查下标 76 | 77 | 4、扩容: 78 | 79 | 初始容量是10。扩容方式是让新容量等于旧容量的1.5倍。 80 | 81 | 在每次添加新的元素时,ArrayList都会检查是否需要进行扩容操作,扩容操作带来数据向新数组的重新拷贝,所以如果我们知道具体业务数据量,在构造ArrayList时可以给ArrayList指定一个初始容量,这样就会减少扩容时数据的拷贝问题。 82 | 83 | 5、线程安全:ArrayList是线程不安全的。在其迭代器iteator中,如果有多线程操作导致modcount改变,会执行fail-fast。抛出异常。 84 | 85 | 为了保证同步,最好的办法是在创建时完成: 86 | 87 | List list = Collections.synchronizedList(new ArrayList(...)); 88 | 89 | 6、优点:访问很快、顺序添加很快;缺点:删除和插入涉及到元素复制,很耗费性能 90 | 91 | 因此,ArrayList比较适合顺序添加、随机访问的场景。 92 | 93 | ### Vector 94 | 95 | 1、Vector实现List接口,继承AbstractList类,实现RandmoAccess接口,即提供了随机访问功能,提供提供快速访问功能,实现了Cloneable接口,支持clone()方法,可以被克隆。 96 | 97 | 2、底层数据结构:vector底层数组不加transient,序列化时会全部复制 98 | 99 | 3、扩容:扩容方式与ArrayList基本一样,但是扩容时不是1.5倍扩容,而是有一个扩容增量。 100 | 101 | 如果在创建Vector时,指定了扩容增量的大小;则,每次当Vector中动态数组容量增加时,增加的大小都是扩容增量。如果容量的增量小于等于零,则每次需要增大容量时,向量的容量将增大一倍。 102 | 103 | 4、线程安全:vector大部分方法都使用了synchronized修饰符,所以他是线层安全的集合类。 104 | 105 | ### ArrayList和Vector的区别 106 | 107 | 1、Vector是线程安全的,ArrayList是线程非安全的 108 | 109 | 2、Vector可以指定增长因子,如果不指定增长因子,则扩容两倍;ArrayList扩容1.5倍。 110 | 111 | ### stack 112 | 113 | 1、继承Vector类 114 | 115 | 2、Stack通过五个操作对Vector进行扩展 116 | 117 | empty() 118 | 测试堆栈是否为空。 119 | peek() 120 | 查看堆栈顶部的对象,但不从堆栈中移除它。 121 | pop() 122 | 移除堆栈顶部的对象,并作为此函数的值返回该对象。 123 | push(E item) 124 | 把项压入堆栈顶部。 125 | search(Object o) 126 | 返回对象在堆栈中的位置,以 1 为基数。 127 | 128 | ### LinkedList 129 | 130 | 1、数据结构 131 | 132 | LinkedList是一个双向链表。 133 | 134 | #### 属性 135 | 136 | 在LinkedList中提供了两个基本属性size、header。 137 | 138 | private transient Entry header = new Entry(null, null, null); 139 | private transient int size = 0; 140 | 141 | 其中size表示的LinkedList的大小,header表示链表的表头,Entry为节点对象。 142 | 143 | private static class Entry { 144 | E element; //元素节点 145 | Entry next; //下一个元素 146 | Entry previous; //上一个元素 147 | 148 | Entry(E element, Entry next, Entry previous) { 149 | this.element = element; 150 | this.next = next; 151 | this.previous = previous; 152 | } 153 | } 154 | 155 | #### 构造方法 156 | 157 | LinkedList提供了两个构造方法:LinkedList()和LinkedList(Collection c)。 158 | 159 | /** 160 | * 构造一个空列表。 161 | */ 162 | public LinkedList() { 163 | header.next = header.previous = header; 164 | } 165 | 166 | /** 167 | * 构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。 168 | */ 169 | public LinkedList(Collection c) { 170 | this(); 171 | addAll(c); 172 | } 173 | 174 | 175 | 176 | 2、增删改查 177 | 178 | - 增: 179 | 180 | add(E e): 将指定元素添加到此列表的结尾。 181 | 182 | add(int index, E element):在此列表中指定的位置插入指定的元素。 183 | 184 | addAll(Collection c):添加指定 collection 中的所有元素到此列表的结尾,顺序是指定 collection 的迭代器返回这些元素的顺序。 185 | 186 | addAll(int index, Collection c):将指定 collection 中的所有元素从指定位置开始插入此列表。 187 | 188 | AddFirst(E e): 将指定元素插入此列表的开头。 189 | 190 | addLast(E e): 将指定元素添加到此列表的结尾。 191 | 192 | 193 | - 删 194 | 195 | remove(Object o):从此列表中移除首次出现的指定元素(如果存在)。 196 | 197 | clear(): 从此列表中移除所有元素。 198 | 199 | remove():获取并移除此列表的头(第一个元素)。 200 | 201 | remove(int index):移除此列表中指定位置处的元素。 202 | 203 | remove(Objec o):从此列表中移除首次出现的指定元素(如果存在)。 204 | 205 | removeFirst():移除并返回此列表的第一个元素。 206 | 207 | removeFirstOccurrence(Object o):从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。 208 | 209 | removeLast():移除并返回此列表的最后一个元素。 210 | 211 | removeLastOccurrence(Object o):从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时。 212 | 213 | - 查 214 | 215 | 216 | get(int index):返回此列表中指定位置处的元素。 217 | 218 | getFirst():返回此列表的第一个元素。 219 | 220 | getLast():返回此列表的最后一个元素。 221 | 222 | indexOf(Object o):返回此列表中首次出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。 223 | 224 | lastIndexOf(Object o):返回此列表中最后出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。 225 | 226 | 3、线程不安全 227 | 228 | 4、优点:插入和删除消耗的资源少;缺点:查找消耗的资源大,效率低,不能随机访问。 229 | 230 | ## Queue-Deque-ArrayDeque-PriorityQueue 231 | 232 | Queue接口定义了队列数据结构,元素是有序的(按插入顺序),先进先出。 233 | 234 | ### Deque 235 | 236 | Deque接口,继承了Queue接口,创建双向队列,灵活性更强,可以前向或后向迭代,在队头队尾均可心插入或删除元素。它的两个主要实现类是ArrayDeque和LinkedList。 237 | 238 | ### ArrayDeque 239 | 240 | 底层使用循环数组实现双向队列 241 | 242 | ### PriorityQueue 243 | 244 | 1、底层用数组实现堆的结构 245 | 246 | 优先队列跟普通的队列不一样,普通队列是一种遵循FIFO规则的队列,拿数据的时候按照加入队列的顺序拿取。 而优先队列每次拿数据的时候都会拿出优先级最高的数据。 247 | 248 | 优先队列内部维护着一个堆,每次取数据的时候都从堆顶拿数据(堆顶的优先级最高),这就是优先队列的原理。 249 | 250 | 2、增删改查 251 | 252 | - add(E e);添加到最后一个节点,然后上滤。 253 | - poll();得到堆顶元素,然后将最后一个节点放到堆顶,下滤 254 | - remove(Object o);删除元素,然后将最后一个节点放到该位置,先下滤,再上滤。 255 | 256 | 3、线程安全:PriorityQueue不是一个线程安全的类,如果要在多线程环境下使用,可以使用 PriorityBlockingQueue 这个优先阻塞队列。其中add、poll、remove方法都使用 ReentrantLock 锁来保持同步,take() 方法中如果元素为空,则会一直保持阻塞。 257 | 258 | ## Map-HashMap-HashTable ## 259 | 260 | ### 定义 261 | 262 | HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作 263 | 264 | ### 数据结构 265 | 266 | hashmap是数组和链表的组合结构。 267 | 268 | 数组是一个Entry数组,entry是k-V键值对类型(包括hash、key、val、next指针),所以一个entry数组存着很多entry节点,一个entry的位置通过key的hashcode方法,再进行hash,最后与表长-1进行相与操作,其实就是取hash值到的后n - 1位,n代表表长是2的n次方。JDK1.8在链表长度超过8时会转换为红黑树,进一步提升了性能。 269 | 270 | HashMap提供了三个构造函数: 271 | 272 | HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。 273 | 274 | HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。 275 | 276 | HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。 277 | 278 | 在这里提到了两个参数:初始容量,加载因子。 279 | 280 | 这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。 281 | 282 | 对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。 283 | 284 | 285 | ### 扩容 286 | 287 | hashmap的默认负载因子是0.75,阈值是16 * 0.75 = 12;初始长度为16;进行两倍扩容。 288 | 289 | HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,是因为容量是2的n次幂,可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞。 290 | 291 | Map扩容操作的核心在于重哈希。所谓重哈希是指重新计算原HashMap中的元素在新table数组中的位置并进行复制处理的过程。 292 | 293 | 通过resize扩容 294 | 295 | 1. 扩容:创建一个新的Entry空数组,长度是原数组的2倍。 296 | 2. ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。 297 | 298 | JDK1.8的优化: 299 | 300 | 不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+原容量”。 301 | 302 | ### 增删改查 303 | 304 | hashmap的增删改查方式比较简单,都是遍历,替换。有一点要注意的是key相等时,替换元素,不相等时连成链表。 305 | 306 | #### 存储实现:put(key,vlaue) 307 | 308 | public V put(K key, V value) { 309 | //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因 310 | if (key == null) 311 | return putForNullKey(value); 312 | //计算key的hash值,此处对原来元素的hashcode进行了再次hash 313 | int hash = hash(key.hashCode()); ------(1) 314 | //计算key hash 值在 table 数组中的位置 315 | int i = indexFor(hash, table.length); ------(2) 316 | //从i出开始迭代 e,找到 key 保存的位置 317 | for (Entry e = table[i]; e != null; e = e.next) { 318 | Object k; 319 | //判断该条链上是否有hash值相同的(key相同) 320 | //若存在相同,则直接覆盖value,返回旧value 321 | if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 322 | V oldValue = e.value; //旧值 = 新值 323 | e.value = value; 324 | e.recordAccess(this); 325 | return oldValue; //返回旧值 326 | } 327 | } 328 | //修改次数增加1 329 | modCount++; 330 | //将key、value添加至i位置处 331 | addEntry(hash, key, value, i); 332 | return null; 333 | } 334 | 335 | 1、寻找位置。计算key的hashcode的hash值,再搜索在table数组中的索引位置,其中indexFor方法如下: 336 | 337 | static int indexFor(int h, int length) { 338 | return h & (length-1); 339 | } 340 | 与表长-1进行相与操作,其实就是取hash值到的后n - 1位,n代表表长是2的n次方。 341 | 342 | 2、如果table数组在该位置处有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。 343 | 344 | #### 读取实现:get(key) 345 | 346 | 通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。 347 | 348 | public V get(Object key) { 349 | // 若为null,调用getForNullKey方法返回相对应的value 350 | if (key == null) 351 | return getForNullKey(); 352 | // 根据该 key 的 hashCode 值计算它的 hash 码 353 | int hash = hash(key.hashCode()); 354 | // 取出 table 数组中指定索引处的值 355 | for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { 356 | Object k; 357 | //若搜索的key与查找的key相同,则返回相对应的value 358 | if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 359 | return e.value; 360 | } 361 | return null; 362 | } 363 | 364 | 365 | ### 线程安全 ### 366 | 367 | hashmap的扩容操作,由于hashmap非线程安全,扩容时如果多线程并发进行操作,则可能有两个线程分别操作新表和旧表,导致节点成环,查询时会形成死锁。chm避免了这个问题。 368 | 369 | ### CHM ### 370 | 371 | concurrenthashmap 372 | 373 | chm1.7使用分段锁来控制并发。查询时不加锁。 374 | 375 | 1.8则放弃使用分段锁,改用cas+synchronized方式实现并发控制,查询时不加锁,插入时如果没有冲突直接cas到成功为止,有冲突则使用synchronized插入。cas:比较和交换(Conmpare And Swap)。 376 | 377 | #### JDK1.7 378 | 379 | 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 380 | 381 | #### JDK1.8 382 | 383 | ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。 384 | 385 | synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。 386 | 387 | ### HashMap和HashTable的区别 388 | 389 | 1、定义:HashTable基于Dictionary类,而HashMap是基于AbstractMap。 390 | 391 | 2、null值:HashMap可以允许存在一个为null的key和任意个为null的value,但是HashTable中的key和value都不允许为null。 392 | 393 | 3、线程安全:HashMap线程不安全,HashTable是线程安全的。HashMap内部实现没有任何线程同步相关的代码,所以相对而言性能要好一点。如果在多线程中使用HashMap需要自己管理线程同步。HashTable大部分对外接口都使用synchronized包裹,所以是线程安全的,但是性能会相对差一些。 394 | 395 | 4、初始长度:HashMap的初始长度为16,而HashTable的初始长度为11。HashMap中初始容量必须是2的幂,如果初始化传入的初始容量不是2的幂,将会自动调整为大于出入的初始容量最小的2的幂。 396 | 397 | 5、hash值计算:HashMap使用自己的计算hash的方法(会依赖key的hashCode方法),HashTable则使用key的hashCode方法得到。 398 | 399 | ### ConcurrentHashMap 和 Hashtable 的区别 400 | 401 | ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 402 | 403 | - 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; 404 | 405 | - 实现线程安全的方式(重要): ① 在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 406 | 407 | ## Set-HashSet ## 408 | 409 | set就是hashmap将value固定为一个object,只存key元素包装成一个entry即可,其他不变。 410 | 411 | ### 定义 412 | 413 | HashSet继承AbstractSet类,实现Set、Cloneable、Serializable接口。其中AbstractSet提供 Set 接口的骨干实现,从而最大限度地减少了实现此接口所需的工作。Set接口是一种不包括重复元素的Collection,它维持它自己的内部排序,所以随机访问没有任何意义。 414 | 415 | ### 数据结构 416 | 基于HashMap实现,底层使用HashMap保存所有元素,方法的实现过程也是基于HashMap的 417 | 418 | 构造函数 419 | 420 | /** 421 | * 默认构造函数 422 | * 初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。 423 | */ 424 | public HashSet() { 425 | map = new HashMap<>(); 426 | } 427 | 428 | /** 429 | * 构造一个包含指定 collection 中的元素的新 set。 430 | */ 431 | public HashSet(Collection c) { 432 | map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); 433 | addAll(c); 434 | } 435 | 436 | /** 437 | * 构造一个新的空 set,其底层 HashMap 实例具有指定的初始容量和指定的加载因子 438 | */ 439 | public HashSet(int initialCapacity, float loadFactor) { 440 | map = new HashMap<>(initialCapacity, loadFactor); 441 | } 442 | 443 | /** 444 | * 构造一个新的空 set,其底层 HashMap 实例具有指定的初始容量和默认的加载因子(0.75)。 445 | */ 446 | public HashSet(int initialCapacity) { 447 | map = new HashMap<>(initialCapacity); 448 | } 449 | 450 | /** 451 | * 在API中我没有看到这个构造函数,今天看源码才发现(原来访问权限为包权限,不对外公开的) 452 | * 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。 453 | * dummy 为标识 该构造函数主要作用是对LinkedHashSet起到一个支持作用 454 | */ 455 | HashSet(int initialCapacity, float loadFactor, boolean dummy) { 456 | map = new LinkedHashMap<>(initialCapacity, loadFactor); 457 | } 458 | 459 | ## LinkedHashMap-LRU ## 460 | 461 | 在原来hashmap基础上将所有的节点依据插入的次序另外连成一个链表。用来保持顺序,可以使用它实现lru缓存,当访问命中时将节点移到队头,当插入元素超过长度时,删除队尾元素即可。 462 | 463 | ### 概述 464 | 465 | 本质上,HashMap和双向链表合二为一即是LinkedHashMap。 466 | 467 | 虽然LinkedHashMap增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序。    468 | 469 | 特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将LinkedHashMap分为:保持插入顺序的LinkedHashMap和保持访问顺序的LinkedHashMap,其中LinkedHashMap的默认实现是按插入顺序排序的。 470 | 471 | ### 数据结构 472 | 473 | 1、LinkedHashMap = HashMap + 双向链表。 474 | 475 | 2、与HashMap相比,LinkedHashMap增加了两个属性用于保证迭代顺序,分别是 双向链表头结点header 和 标志位accessOrder (值为true时,表示按照访问顺序迭代;值为false时,表示按照插入顺序迭代)。 476 | 477 | 3、LinkedHashMap中的Entry增加了两个指针 before 和 after,它们分别用于维护双向链接列表。即Entry包括:before、hash、key、value、next、after。 478 | 479 | 4、构造函数 480 | 481 | 构造一个指定初始容量和指定负载因子的具有指定迭代顺序的LinkedHashMap 482 | 483 | LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) 484 | 485 | 构造一个与指定 Map 具有相同映射的 LinkedHashMap,其 初始容量不小于 16 (具体依赖于指定Map的大小),负载因子是 0.75 486 | 487 | LinkedHashMap(Map m) 488 | 489 | ### LinkedHashMap存取 490 | 491 | LinkedHashMap的存取过程基本与HashMap基本类似,只是在细节实现上稍有不同,这是由LinkedHashMap本身的特性所决定的,因为它要额外维护一个双向链表用于保持迭代顺序。 492 | 493 | 在put操作上,虽然LinkedHashMap完全继承了HashMap的put操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中。 494 | 495 | 在扩容操作上,虽然LinkedHashMap完全继承了HashMap的resize操作,但是鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对其中的重哈希过程(transfer方法)进行了重写。 496 | 497 | 在读取操作上,LinkedHashMap中重写了HashMap中的get方法,通过HashMap中的getEntry方法获取Entry对象。在此基础上,进一步获取指定键对应的值。 498 | 499 | ### LRU 500 | 501 | accessOrder标志位的作用 502 | 503 | - recordAccess方法判断accessOrder是否为true,如果是,则将当前访问的Entry(put进来的Entry或get出来的Entry)移到双向链表的尾部(key不相同时,put新Entry时,会调用addEntry,它会调用createEntry,该方法同样将新插入的元素放入到双向链表的尾部,既符合插入的先后顺序,又符合访问的先后顺序,因为这时该Entry也被访问了); 504 | 505 | - 当标志位accessOrder的值为false时,表示双向链表中的元素按照Entry插入LinkedHashMap到中的先后顺序排序,即每次put到LinkedHashMap中的Entry都放在双向链表的尾部,这样遍历双向链表时,Entry的输出顺序便和插入的顺序一致,这也是默认的双向链表的存储顺序。 506 | 507 | 使用LinkedHashMap实现LRU的必要前提是将accessOrder标志位设为true以便开启按访问顺序排序的模式。无论是put方法还是get方法,都会导致目标Entry成为最近访问的Entry,因此就把该Entry加入到了双向链表的末尾:get方法通过调用recordAccess方法来实现。 508 | 509 | 这样,我们便把最近使用的Entry放入到了双向链表的后面。多次操作后,双向链表前面的Entry便是最近没有使用的,这样当节点个数满的时候,删除最前面的Entry(head后面的那个Entry)即可,因为它就是最近最少使用的Entry。 510 | 511 | public class LRU extends LinkedHashMap implements Map{ 512 | 513 | private static final long serialVersionUID = 1L; 514 | 515 | public LRU(int initialCapacity, 516 | float loadFactor, 517 | boolean accessOrder) { 518 | super(initialCapacity, loadFactor, accessOrder); 519 | } 520 | 521 | /** 522 | * @description 重写LinkedHashMap中的removeEldestEntry方法,当LRU中元素多余6个时, 523 | * 删除最不经常使用的元素 524 | */ 525 | @Override 526 | protected boolean removeEldestEntry(java.util.Map.Entry eldest) { 527 | // TODO Auto-generated method stub 528 | if(size() > 6){ 529 | return true; 530 | } 531 | return false; 532 | } 533 | 534 | public static void main(String[] args) { 535 | 536 | LRU lru = new LRU( 537 | 16, 0.75f, true); 538 | 539 | String s = "abcdefghijkl"; 540 | for (int i = 0; i < s.length(); i++) { 541 | lru.put(s.charAt(i), i); 542 | } 543 | System.out.println("LRU中key为h的Entry的值为: " + lru.get('h')); 544 | System.out.println("LRU的大小 :" + lru.size()); 545 | System.out.println("LRU :" + lru); 546 | } 547 | } 548 | 549 | 550 | ## Collections和Arrays工具类 ## 551 | 552 | 两个工具类分别操作集合和数组,可以进行常用的排序,合并等操作。 553 | 554 | - 反转 reverse 555 | - 混淆 shuffle 556 | - 排序 sort 557 | - 交换 swap 558 | - 滚动 rotate 559 | - 线程安全化 synchronizedList 560 | 561 | -------------------------------------------------------------------------------- /JavaWeb/Spring.md: -------------------------------------------------------------------------------- 1 | # 1. 什么是 Spring 框架? # 2 | 3 | Spring 是一种轻量级开发框架,旨在提高开发人员的开发效率以及系统的可维护性。 4 | 5 | 我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是:核心容器、数据访问/集成,、Web、AOP(面向切面编程)、工具、消息和测试模块。比如:Core Container 中的 Core 组件是Spring 所有组件的核心,Beans 组件和 Context 组件是实现IOC和依赖注入的基础,AOP组件用来实现面向切面编程。 6 | 7 | Spring 官网列出的 Spring 的 6 个特征: 8 | 9 | - 核心技术 :依赖注入(DI),AOP,事件(events),资源,i18n,验证,数据绑定,类型转换,SpEL。 10 | - 测试 :模拟对象,TestContext框架,Spring MVC 测试,WebTestClient。 11 | - 数据访问 :事务,DAO支持,JDBC,ORM,编组XML。 12 | - Web支持 : Spring MVC和Spring WebFlux Web框架。 13 | - 集成 :远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 14 | - 语言 :Kotlin,Groovy,动态语言。 15 | 16 | # 2. 列举一些重要的Spring模块? # 17 | 18 | - Spring Core: 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 IoC 依赖注入功能。 19 | - Spring Aspects : 该模块为与AspectJ的集成提供支持。 20 | - Spring AOP :提供了面向切面的编程实现。 21 | - Spring JDBC : Java数据库连接。 22 | - Spring JMS :Java消息服务。 23 | - Spring ORM : 用于支持Hibernate等ORM工具。 24 | - Spring Web : 为创建Web应用程序提供支持。 25 | - Spring Test : 提供了对 JUnit 和 TestNG 测试的支持。 26 | 27 | # 3. @RestController vs @Controller # 28 | 29 | ## Controller 返回一个页面 ## 30 | 31 | 单独使用 @Controller 不加 @ResponseBody的话一般使用在要返回一个视图的情况,这种情况属于比较传统的Spring MVC 的应用,对应于前后端不分离的情况。 32 | 33 | ## @RestController 返回JSON 或 XML 形式数据 ## 34 | 35 | @Controller + @ResponseBody= @RestController 36 | 37 | @RestController只返回对象,对象数据直接以 JSON 或 XML 形式写入 HTTP 响应(Response)中,这种情况属于 RESTful Web服务,这也是目前日常开发所接触的最常用的情况(前后端分离)。 38 | 39 | > @ResponseBody 注解的作用是将 Controller 的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到HTTP 响应(Response)对象的 body 中,通常用来返回 JSON 或者 XML 数据,返回 JSON 数据的情况比较多。 40 | 41 | # 4. Spring IOC & AOP # 42 | 43 | ## 4.1 谈谈自己对于 Spring IoC 和 AOP 的理解 ## 44 | 45 | ### IoC ### 46 | 47 | IoC(Inverse of Control:控制反转)是一种**设计思想**,就是** 将原本在程序中手动创建对象的控制权,交由Spring框架来管理。** IoC 在其他语言中也有应用,并非 Spring 特有。 **IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。** 48 | 49 | 将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 **IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。** 在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。 50 | 51 | Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。 52 | 53 | #### Spring IoC的初始化过程: #### 54 | 55 | 读取XML资源,并解析,最终注册到Bean Factory中: 56 | 57 | XML---读取--->Resource---解析--->BeanDefinition---注册--->BeanFactory 58 | 59 | 在完成初始化的过程后,Bean们就在BeanFactory中蓄势以待地等调用了。 60 | 61 | ![](https://camo.githubusercontent.com/faa0326fb04c227b78b8e640b7a75da76a612f1f/68747470733a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f323031392d372f537072696e67494f432545352538382539442545352541372538422545352538432539362545382542462538372545372541382538422e706e67) 62 | 63 | ### AOP ### 64 | 65 | AOP(Aspect-Oriented Programming:面向切面编程)能够**将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。** 66 | 67 | **Spring AOP就是基于动态代理的**,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理。 68 | 69 | #### Java代理 #### 70 | 71 | Java中代理的实现一般分为三种:JDK静态代理、JDK动态代理以及CGLIB动态代理。在Spring的AOP实现中,主要应用了JDK动态代理以及CGLIB动态代理。 72 | 73 | **JDK静态代理**:创建一个接口,然后创建被代理的类实现该接口并且实现该接口中的抽象方法。之后再创建一个代理类,同时使其也实现这个接口。在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。 74 | 75 | **JDK动态代理**:JDK静态代理是通过直接编码创建的,而JDK动态代理是利用反射机制在运行时创建代理类的。 76 | 77 | **CGLIB**:JDK代理要求被代理的类必须实现接口,有很强的局限性。而CGLIB动态代理则没有此类强制性要求。简单的说,CGLIB会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。 78 | 79 | ## 4.2 Spring AOP 和 AspectJ AOP 有什么区别? ## 80 | 81 | Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。 82 | 83 | Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单, 84 | 85 | 如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。 86 | 87 | # 5. Spring bean # 88 | 89 | ## 5.1 Spring 中的 bean 的作用域有哪些? ## 90 | 91 | Bean不仅可以控制注入到对象中的各种依赖和配置值,还可以控制该对象的作用域。这样可以灵活选择所建对象的作用域,而不必在Java Class级定义作用域。 92 | 93 | - singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。 94 | - prototype : 每次请求都会创建一个新的 bean 实例。 95 | - request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 96 | - session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。 97 | - global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话。 98 | 99 | ## 5.2 Spring 中的单例 bean 的线程安全问题了解吗? ## 100 | 101 | 单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 102 | 103 | 常见的有两种解决办法: 104 | 105 | 1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。 106 | 107 | 1. 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。 108 | 109 | ## 5.3 @Component 和 @Bean 的区别是什么? ## 110 | 111 | 1. 作用对象不同: @Component 注解作用于类,而@Bean注解作用于方法。 112 | 1. @Component通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了Spring这是某个类的示例,当我需要用它的时候还给我。 113 | 1. @Bean 注解比 Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册bean。比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。 114 | 115 | ## 5.4 将一个类声明为Spring的 bean 的注解有哪些? ## 116 | 117 | 我们一般使用 @Autowired 注解自动装配 bean,要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,采用以下注解可实现: 118 | 119 | - @Component :通用的注解,可标注任意类为 Spring 组件。如果一个Bean不知道属于哪个层,可以使用@Component 注解标注。 120 | - @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。 121 | - @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。 122 | - @Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。 123 | 124 | ## 5.5 Spring 中的 bean 生命周期? ## 125 | 126 | 1、spring容器读取配置文件并解析,将其注册到Bean工厂。 127 | 2、需要时,通过反射创建bean实例 128 | 3、进行属性值的填充 129 | 4、使用配置文件中的init方法进行初始化 130 | 5、如果要销毁,根据配置文件中的destroy-method执行指定的方法。 131 | 132 | - Bean 容器找到配置文件中 Spring Bean 的定义。 133 | - Bean 容器利用 Java Reflection API 创建一个Bean的实例。 134 | - 如果涉及到一些属性值 利用 set()方法设置一些属性值。 135 | - 如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入Bean的名字。 136 | - 如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。 137 | - 与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法。 138 | - 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法 139 | - 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。 140 | - 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。 141 | - 如果有和加载这个 Bean的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法 142 | - 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。 143 | - 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。 144 | 145 | ## 5.6 @Resource 和 @Autowired 的区别 ## 146 | 147 | 1、 @Autowired与@Resource都可以用来装配bean. 都可以写在字段上,或写在setter方法上。 148 | 149 | 2、 @Autowired默认按类型(byType)装配(这个注解是属于spring的),默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,如:@Autowired(required=false) 。 150 | 151 | 3、@Resource(这个注解属于J2EE的),默认按照名称(byName)进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行安装名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。 152 | 153 | 154 | 155 | > @Autowired是根据类型进行自动装配的。如果当Spring上下文中存在不止一个UserDao类型的bean时,就会抛出BeanCreationException异常;如果Spring上下文中不存在UserDao类型的bean,也会抛出BeanCreationException异常。 156 | 157 | # 6. Spring MVC # 158 | 159 | Spring MVC 的入口函数也就是前端控制器 DispatcherServlet 的作用是接收请求,响应结果。 160 | 161 | **工作流程:** 162 | 163 | 1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet。 164 | 1. DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。 165 | 1. 解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。 166 | 1. HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑。 167 | 1. 处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View。 168 | 1. ViewResolver 会根据逻辑 View 查找实际的 View。 169 | 1. DispaterServlet 把返回的 Model 传给 View(视图渲染)。 170 | 1. 把 View 返回给请求者(浏览器) 171 | 172 | # 7. Spring 框架中用到了哪些设计模式? # 173 | 174 | - 工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。 175 | - 代理设计模式 : Spring AOP 功能的实现。 176 | - 单例设计模式 : Spring 中的 Bean 默认都是单例的。 177 | - 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 178 | - 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 179 | - 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。 180 | - 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。 181 | 182 | # 8. Spring 事务 # 183 | 184 | ## 8.1 Spring 管理事务的方式有几种? ## 185 | 186 | 1. 编程式事务,在代码中硬编码。(不推荐使用) 187 | 1. 声明式事务,在配置文件中配置(推荐使用) 188 | 189 | **声明式事务又分为两种:** 190 | 191 | 1. 基于XML的声明式事务 192 | 1. 基于注解的声明式事务 193 | 194 | ## 8.2 Spring 事务中的隔离级别有哪几种? ## 195 | 196 | TransactionDefinition 接口中定义了五个表示隔离级别的常量: 197 | 198 | - TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别. 199 | - TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 200 | - TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 201 | - TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 202 | - TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 203 | 204 | ## 8.3 Spring 事务中哪几种事务传播行为? ## 205 | 206 | #### 支持当前事务的情况: #### 207 | 208 | - TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 209 | - TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 210 | - TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性) 211 | 212 | #### 不支持当前事务的情况: #### 213 | 214 | - TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 215 | - TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 216 | - TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 217 | 218 | #### 其他情况: #### 219 | 220 | - TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。 221 | 222 | ## 8.4 @Transactional(rollbackFor = Exception.class)注解了解吗? ## 223 | 224 | 我们知道:Exception分为运行时异常RuntimeException和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。 225 | 226 | 当@Transactional注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。 227 | 228 | 在@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚。 -------------------------------------------------------------------------------- /JavaWeb/SpringBoot.md: -------------------------------------------------------------------------------- 1 | # 1. 什么是 Spring Boot? # 2 | 3 | 首先,重要的是要理解 Spring Boot 并不是一个框架,它是一种创建独立应用程序的更简单方法,只需要很少或没有配置(相比于 Spring 来说)。Spring Boot最好的特性之一是它利用现有的 Spring 项目和第三方项目来开发适合生产的应用程序。 4 | 5 | # 2. 说出使用Spring Boot的主要优点 # 6 | 7 | 1. 开发基于 Spring 的应用程序很容易。 8 | 1. Spring Boot 项目所需的开发或工程时间明显减少,通常会提高整体生产力。 9 | 1. Spring Boot不需要编写大量样板代码、XML配置和注释。 10 | 1. Spring引导应用程序可以很容易地与Spring生态系统集成,如Spring JDBC、Spring ORM、Spring Data、Spring Security等。 11 | 1. Spring Boot遵循“固执己见的默认配置”,以减少开发工作(默认配置可以修改)。 12 | 1. Spring Boot 应用程序提供嵌入式HTTP服务器,如Tomcat和Jetty,可以轻松地开发和测试web应用程序。(这点很赞!普通运行Java程序的方式就能运行基于Spring Boot web 项目,省事很多) 13 | 1. Spring Boot提供命令行接口(CLI)工具,用于开发和测试Spring Boot应用程序,如Java或Groovy。 14 | 1. Spring Boot提供了多种插件,可以使用内置工具(如Maven和Gradle)开发和测试Spring Boot应用程序。 15 | 16 | # 3. 为什么需要Spring Boot? # 17 | 18 | Spring Framework旨在简化J2EE企业应用程序开发。Spring Boot Framework旨在简化Spring开发。 19 | 20 | # 4. 什么是 Spring Boot Starters? # 21 | 22 | Spring Boot Starters 是一系列依赖关系的集合,因为它的存在,项目的依赖之间的关系对我们来说变的更加简单了。举个例子:在没有Spring Boot Starters之前,我们开发REST服务或Web应用程序时; 我们需要使用像Spring MVC,Tomcat和Jackson这样的库,这些依赖我们需要手动一个一个添加。但是,有了 Spring Boot Starters 我们只需要一个只需添加一个spring-boot-starter-web一个依赖就可以了,这个依赖包含的字依赖中包含了我们开发REST 服务需要的所有依赖。 23 | 24 | # 5. 如何在Spring Boot应用程序中使用Jetty而不是Tomcat? # 25 | 26 | Spring Boot Web starter使用Tomcat作为默认的嵌入式servlet容器, 如果你想使用 Jetty 的话只需要修改pom.xml(Maven)或者build.gradle(Gradle)就可以了。 27 | 28 | # 6. 介绍一下@SpringBootApplication注解 # 29 | 30 | package org.springframework.boot.autoconfigure; 31 | @Target(ElementType.TYPE) 32 | @Retention(RetentionPolicy.RUNTIME) 33 | @Documented 34 | @Inherited 35 | @SpringBootConfiguration 36 | @EnableAutoConfiguration 37 | @ComponentScan(excludeFilters = { 38 | @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), 39 | @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) 40 | public @interface SpringBootApplication { 41 | ...... 42 | } 43 | 44 | ---------- 45 | 46 | package org.springframework.boot; 47 | @Target(ElementType.TYPE) 48 | @Retention(RetentionPolicy.RUNTIME) 49 | @Documented 50 | @Configuration 51 | public @interface SpringBootConfiguration { 52 | 53 | } 54 | 55 | 可以看出大概可以把 @SpringBootApplication 看作是 @SpringBootConfiguration(内部为@Configuration)、@EnableAutoConfiguration、@ComponentScan 注解的集合。根据 SpringBoot官网,这三个注解的作用分别是: 56 | 57 | - @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制 58 | - @ComponentScan: 扫描被@Component (@Service,@Controller)注解的bean,注解默认会扫描该类所在的包下所有的类。 59 | - @SpringBootConfiguration(内部为@Configuration):被标注的类等于在spring的XML配置文件中(applicationContext.xml),装配所有bean事务,提供了一个spring的上下文环境。 60 | 61 | # 7.SpringBoot如何启动的 # 62 | 63 | 每个SpringBoot程序都有一个主入口,也就是main方法,main里面调用SpringApplication.run()启动整个spring-boot程序,该方法所在类需要使用@SpringBootApplication注解,以及@ImportResource注解(if need),@SpringBootApplication包括三个注解,功能如下: 64 | 65 | - @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制 66 | - @ComponentScan: 扫描被@Component (@Service,@Controller)注解的bean,注解默认会扫描该类所在的包下所有的类。 67 | - @SpringBootConfiguration(内部为@Configuration):被标注的类等于在spring的XML配置文件中(applicationContext.xml),装配所有bean事务,提供了一个spring的上下文环境。 68 | 69 | 启动流程主要分为三个部分: 70 | 71 | 1. 第一部分进行SpringApplication的初始化模块,配置一些基本的环境变量、资源、构造器、监听器; 72 | 1. 第二部分实现了应用具体的启动方案,包括启动流程的监听模块、加载配置环境模块、及核心的创建上下文环境模块; 73 | 1. 第三部分是自动化配置模块,该模块作为springboot自动配置核心,下一个问题详细讲解。 74 | 75 | # 8. Spring Boot 的自动配置是如何实现的? # 76 | 77 | 主要是 @SpringBootApplication 中的 @EnableAutoConfiguration 注解 78 | 79 | import java.lang.annotation.Documented; 80 | import java.lang.annotation.ElementType; 81 | import java.lang.annotation.Inherited; 82 | import java.lang.annotation.Retention; 83 | import java.lang.annotation.RetentionPolicy; 84 | import java.lang.annotation.Target; 85 | import org.springframework.context.annotation.Import; 86 | 87 | @Target({ElementType.TYPE}) 88 | @Retention(RetentionPolicy.RUNTIME) 89 | @Documented 90 | @Inherited 91 | @AutoConfigurationPackage 92 | @Import({AutoConfigurationImportSelector.class}) 93 | public @interface EnableAutoConfiguration { 94 | String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; 95 | 96 | Class[] exclude() default {}; 97 | 98 | String[] excludeName() default {}; 99 | } 100 | 101 | @EnableAutoConfiguration 注解通过Spring 提供的 @Import 注解导入了AutoConfigurationImportSelector类(@Import 注解可以导入配置类或者Bean到当前类中)。 102 | 103 | AutoConfigurationImportSelector类中getCandidateConfigurations方法会将所有自动配置类的信息以 List 的形式返回。这些配置信息会被 Spring 容器当作 bean 来管理。 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java-note 2 | Java学习笔记 3 | 4 | ### java相关 ### 5 | 6 | - Java基础 7 | - Java集合 8 | - JVM 9 | - Java并发 10 | - 线程池 11 | - Java网络 12 | 13 | ### 计算机基础 ### 14 | 15 | - 数据库 16 | - 计算机网络 17 | - 操作系统 18 | 19 | ### 面向对象思想 ### 20 | 21 | - 面向对象 22 | - 设计模式 23 | 24 | ### Java Web ### 25 | 26 | - Spring 27 | - SpringBoot 28 | -------------------------------------------------------------------------------- /操作系统/Linux.md: -------------------------------------------------------------------------------- 1 | # 重要知识点 # 2 | 3 | - 能简单使用 cat,grep,cut 等命令进行一些操作; 4 | - 文件系统相关的原理,inode 和 block 等概念,数据恢复; 5 | - 硬链接与软链接; 6 | - 进程管理相关,僵尸进程与孤儿进程,SIGCHLD 。 7 | 8 | # 一、常用操作以及概念 # 9 | 10 | ## 快捷键 ## 11 | 12 | - Tab:命令和文件名补全; 13 | - Ctrl+C:中断正在运行的程序; 14 | - Ctrl+D:结束键盘输入(End Of File,EOF) 15 | 16 | ## 求助 ## 17 | 18 | ### 1. --help ### 19 | 20 | 指令的基本用法与选项介绍。 21 | 22 | ### 2. man ### 23 | 24 | man 是 manual 的缩写,将指令的具体信息显示出来。 25 | 26 | 当执行 man date 时,有 DATE(1) 出现,其中的数字代表指令的类型,常用的数字及其类型如下: 27 | 28 | 代号| 类型 29 | -|- 30 | 1| 用户在 shell 环境中可以操作的指令或者可执行文件 31 | 5| 配置文件 32 | 8| 系统管理员可以使用的管理指令 33 | 34 | ### 3. info ### 35 | 36 | info 与 man 类似,但是 info 将文档分成一个个页面,每个页面可以跳转。 37 | 38 | ### 4. doc ### 39 | 40 | /usr/share/doc 存放着软件的一整套说明文件。 41 | 42 | ## 关机 ## 43 | 44 | ### 1. who ### 45 | 46 | 在关机前需要先使用 who 命令查看有没有其它用户在线。 47 | 48 | ### 2. sync ### 49 | 50 | 为了加快对磁盘文件的读写速度,位于内存中的文件数据不会立即同步到磁盘,因此关机之前需要先进行 sync 同步操作。 51 | 52 | ### 3. shutdown ### 53 | 54 | # shutdown [-krhc] 时间 [信息] 55 | -k : 不会关机,只是发送警告信息,通知所有在线的用户 56 | -r : 将系统的服务停掉后就重新启动 57 | -h : 将系统的服务停掉后就立即关机 58 | -c : 取消已经在进行的 shutdown 59 | 60 | ## PATH ## 61 | 62 | 可以在环境变量 PATH 中声明可执行文件的路径,路径之间用 : 分隔。 63 | 64 | /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin 65 | 66 | ## sudo ## 67 | 68 | sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。 69 | 70 | ## 包管理工具 ## 71 | 72 | RPM 和 DPKG 为最常见的两类软件包管理工具: 73 | 74 | - RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为许多 Linux 系统的既定软件标准。YUM 基于 RPM,具有依赖管理和软件升级功能。 75 | - 与 RPM 竞争的是基于 Debian 操作系统的 DEB 软件包管理工具 DPKG,全称为 Debian Package,功能方面与 RPM 相似。 76 | 77 | ## VIM 三个模式 ## 78 | 79 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191209002818626.png) 80 | 81 | - 一般指令模式(Command mode):VIM 的默认模式,可以用于移动游标查看内容; 82 | - 编辑模式(Insert mode):按下 "i" 等按键之后进入,可以对文本进行编辑; 83 | - 指令列模式(Bottom-line mode):按下 ":" 按键之后进入,用于保存退出等操作。 84 | 85 | 在指令列模式下,有以下命令用于离开或者保存文件。 86 | 87 | 命令| 作用 88 | -|- 89 | :w| 写入磁盘 90 | :w!| 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 91 | :q| 离开 92 | :q!| 强制离开不保存 93 | :wq| 写入磁盘后离开 94 | :wq!| 强制写入磁盘后离开 95 | 96 | # 二、分区 # 97 | 98 | ## 分区表 ## 99 | 100 | 磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。 101 | 102 | ### 1. MBR ### 103 | 104 | MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中主要开机记录占 446 bytes,分区表占 64 bytes。 105 | 106 | 分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它使用其它扇区来记录额外的分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。 107 | 108 | Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名 + 编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。 109 | 110 | 111 | ### 2. GPT ### 112 | 113 | 扇区是磁盘的最小存储单位,旧磁盘的扇区大小通常为 512 bytes,而最新的磁盘支持 4 k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA),LBA 默认大小为 512 bytes。 114 | 115 | GPT 第 1 个区块记录了主要开机记录(MBR),紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录,这个部份纪录了分区表本身的位置与大小和备份分区的位置,同时放置了分区表的校验码 (CRC32),操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误,可以使用备份分区进行恢复。 116 | 117 | GPT 没有扩展分区概念,都是主分区,每个 LBA 可以分 4 个分区,因此总共可以分 4 * 32 = 128 个分区。 118 | 119 | MBR 不支持 2.2 TB 以上的硬盘,GPT 则最多支持到 233 TB = 8 ZB。 120 | 121 | ## 开机检测程序 ## 122 | 123 | ### 1. BIOS ### 124 | 125 | BIOS(Basic Input/Output System,基本输入输出系统),它是一个固件(嵌入在硬件中的软件),BIOS 程序存放在断电后内容不会丢失的只读内存中。 126 | 127 | BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的主要开机记录(MBR),由主要开机记录(MBR)执行其中的开机管理程序,这个开机管理程序会加载操作系统的核心文件。 128 | 129 | 主要开机记录(MBR)中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。 130 | 131 | ### 2. UEFI ### 132 | 133 | BIOS 不可以读取 GPT 分区表,而 UEFI 可以。 134 | 135 | 136 | # 三、文件 # 137 | 138 | ## 文件属性 ## 139 | 140 | 用户分为三种:文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。 141 | 142 | 使用 ls 查看一个文件时,会显示一个文件的信息,例如 drwxr-xr-x 3 root root 17 May 6 00:14 .config,对这个信息的解释如下: 143 | 144 | - drwxr-xr-x:文件类型以及权限,第 1 位为文件类型字段,后 9 位为文件权限字段 145 | - 3:链接数 146 | - root:文件拥有者 147 | - root:所属群组 148 | - 17:文件大小 149 | - May 6 00:14:文件最后被修改的时间 150 | - .config:文件名 151 | 152 | 常见的文件类型及其含义有: 153 | 154 | - d:目录 155 | - -:文件 156 | - l:链接文件 157 | 158 | 9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。 159 | 160 | 文件时间有以下三种: 161 | 162 | - modification time (mtime):文件的内容更新就会更新; 163 | - status time (ctime):文件的状态(权限、属性)更新就会更新; 164 | - access time (atime):读取文件时就会更新。 165 | 166 | ## 文件与目录的基本操作 ## 167 | 168 | ### 1. ls ### 169 | 170 | 列出文件或者目录的信息,目录的信息就是其中包含的文件。 171 | 172 | # ls [-aAdfFhilnrRSt] file|dir 173 | -a :列出全部的文件 174 | -d :仅列出目录本身 175 | -l :以长数据串行列出,包含文件的属性与权限等等数据 176 | 177 | ### 2. cd ### 178 | 179 | 更换当前目录。 180 | 181 | cd [相对路径或绝对路径] 182 | 183 | ### 3. mkdir ### 184 | 185 | 创建目录。 186 | 187 | # mkdir [-mp] 目录名称 188 | -m :配置目录权限 189 | -p :递归创建目录 190 | 191 | ### 4. rmdir ### 192 | 193 | 删除目录,目录必须为空。 194 | 195 | rmdir [-p] 目录名称 196 | -p :递归删除目录 197 | 198 | ### 5. touch ### 199 | 200 | 更新文件时间或者建立新文件。 201 | 202 | # touch [-acdmt] filename 203 | -a : 更新 atime 204 | -c : 更新 ctime,若该文件不存在则不建立新文件 205 | -m : 更新 mtime 206 | -d : 后面可以接更新日期而不使用当前日期,也可以使用 --date="日期或时间" 207 | -t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm] 208 | 209 | ## 6. cp ## 210 | 211 | 复制文件。如果源文件有两个以上,则目的文件一定要是目录才行。 212 | 213 | cp [-adfilprsu] source destination 214 | -a :相当于 -dr --preserve=all 215 | -d :若来源文件为链接文件,则复制链接文件属性而非文件本身 216 | -i :若目标文件已经存在时,在覆盖前会先询问 217 | -p :连同文件的属性一起复制过去 218 | -r :递归复制 219 | -u :destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制 220 | --preserve=all :除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了 221 | 222 | ## 7. rm ## 223 | 224 | 删除文件。 225 | 226 | # rm [-fir] 文件或目录 227 | -r :递归删除 228 | 229 | ### 8. mv ### 230 | 231 | 移动文件。 232 | 233 | # mv [-fiu] source destination 234 | # mv [options] source1 source2 source3 .... directory 235 | -f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖 236 | 237 | ## 修改权限 ## 238 | 239 | 可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1,即每个权限对应的数字权值为 r : 4、w : 2、x : 1。 240 | 241 | # chmod [-R] xyz dirname/filename 242 | 243 | 示例:将 .bashrc 文件的权限修改为 -rwxr-xr--。 244 | 245 | # chmod 754 .bashrc 246 | 247 | 也可以使用符号来设定权限。 248 | 249 | # chmod [ugoa] [+-=] [rwx] dirname/filename 250 | - u:拥有者 251 | - g:所属群组 252 | - o:其他人 253 | - a:所有人 254 | - +:添加权限 255 | - -:移除权限 256 | - =:设定权限 257 | 258 | ## 默认权限 ## 259 | 260 | - 文件默认权限:文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw。 261 | - 目录默认权限:目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。 262 | 263 | 可以通过 umask 设置或者查看默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r--。 264 | 265 | ## 目录的权限 ## 266 | 267 | 文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。 268 | 269 | 目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。 270 | 271 | ## 获取文件内容 ## 272 | 273 | ### 1. cat ### 274 | 275 | 取得文件内容。 276 | 277 | # cat [-AbEnTv] filename 278 | -n :打印出行号,连同空白行也会有行号,-b 不会 279 | 280 | ### 2. tac ### 281 | 282 | 是 cat 的反向操作,从最后一行开始打印。 283 | 284 | ### 3. more ### 285 | 286 | 和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。 287 | 288 | ### 4. less ### 289 | 290 | 和 more 类似,但是多了一个向前翻页的功能。 291 | 292 | ### 5. head ### 293 | 294 | 取得文件前几行。 295 | 296 | ### 6. tail ### 297 | 298 | 是 head 的反向操作,只是取得是后几行。 299 | 300 | ### 7. od ### 301 | 302 | 以字符或者十六进制的形式显示二进制文件。 303 | 304 | ## 指令与文件搜索 ## 305 | 306 | ### 1. which ### 307 | 308 | 指令搜索。 309 | 310 | # which [-a] command 311 | -a :将所有指令列出,而不是只列第一个 312 | 313 | ### 2. whereis ### 314 | 315 | 文件搜索。速度比较快,因为它只搜索几个特定的目录。 316 | 317 | # whereis [-bmsu] dirname/filename 318 | 319 | ### 3. locate ### 320 | 321 | 文件搜索。可以用关键字或者正则表达式进行搜索。 322 | 323 | locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。 324 | 325 | # locate [-ir] keyword 326 | -r:正则表达式 327 | 328 | ### 4. find ### 329 | 330 | 文件搜索。可以使用文件的属性和权限进行搜索。 331 | 332 | # find [basedir] [option] 333 | example: find . -name "shadow*" 334 | 335 | # 四、压缩与打包 # 336 | 337 | ## 压缩文件名 ## 338 | 339 | 扩展名| 压缩程序 340 | -|- 341 | *.Z| compress 342 | *.zip| zip 343 | *.gz| gzip 344 | *.bz2| bzip2 345 | *.xz| xz 346 | *.tar| tar 程序打包的数据,没有经过压缩 347 | *.tar.gz| tar 程序打包的文件,经过 gzip 的压缩 348 | *.tar.bz2| tar 程序打包的文件,经过 bzip2 的压缩 349 | *.tar.xz| tar 程序打包的文件,经过 xz 的压缩 350 | 351 | ## 压缩指令 ## 352 | 353 | ### 1. gzip ### 354 | 355 | gzip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。 356 | 357 | 经过 gzip 压缩过,源文件就不存在了。 358 | 359 | 有 9 个不同的压缩等级可以使用。 360 | 361 | 可以使用 zcat、zmore、zless 来读取压缩文件的内容。 362 | 363 | 364 | $ gzip [-cdtv#] filename 365 | -c :将压缩的数据输出到屏幕上 366 | -d :解压缩 367 | -t :检验压缩文件是否出错 368 | -v :显示压缩比等信息 369 | -# : # 为数字的意思,代表压缩等级,数字越大压缩比越高,默认为 6 370 | 371 | ### 2. bzip2 ### 372 | 373 | 提供比 gzip 更高的压缩比。 374 | 375 | 查看命令:bzcat、bzmore、bzless、bzgrep。 376 | 377 | $ bzip2 [-cdkzv#] filename 378 | -k :保留源文件 379 | 380 | ### 3. xz ### 381 | 382 | 提供比 bzip2 更佳的压缩比。 383 | 384 | 可以看到,gzip、bzip2、xz 的压缩比不断优化。不过要注意的是,压缩比越高,压缩的时间也越长。 385 | 386 | 查看命令:xzcat、xzmore、xzless、xzgrep。 387 | 388 | $ xz [-dtlkc#] filename 389 | 390 | ## 打包 ## 391 | 392 | 压缩指令只能对一个文件进行压缩,而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gzip、bzip2、xz 将打包文件进行压缩。 393 | 394 | $ tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename... ==打包压缩 395 | $ tar [-z|-j|-J] [tv] [-f 已有的 tar 文件] ==查看 396 | $ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩 397 | -z :使用 zip; 398 | -j :使用 bzip2; 399 | -J :使用 xz; 400 | -c :新建打包文件; 401 | -t :查看打包文件里面有哪些文件; 402 | -x :解打包或解压缩的功能; 403 | -v :在压缩/解压缩的过程中,显示正在处理的文件名; 404 | -f : filename:要处理的文件; 405 | -C 目录 : 在特定目录解压缩。 406 | 407 | 使用方式| 命令 408 | -|- 409 | 打包压缩| tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称 410 | 查 看| tar -jtv -f filename.tar.bz2 411 | 解压缩| tar -jxv -f filename.tar.bz2 -C 要解压缩的目录 412 | 413 | # 五、Bash # 414 | 415 | 可以通过 Shell 请求内核提供服务,Bash 正是 Shell 的一种。 416 | 417 | ## 特性 ## 418 | 419 | - 命令历史:记录使用过的命令 420 | - 命令与文件补全:快捷键:tab 421 | - 命名别名:例如 ll 是 ls -al 的别名 422 | - shell scripts 423 | - 通配符:例如 ls -l /usr/bin/X* 列出 /usr/bin 下面所有以 X 开头的文件 424 | 425 | ## 变量操作 ## 426 | 427 | 对一个变量赋值直接使用 =。 428 | 429 | 对变量取用需要在变量前加上 $ ,也可以用 ${} 的形式; 430 | 431 | 输出变量使用 echo 命令。 432 | 433 | $ x=abc 434 | $ echo $x 435 | $ echo ${x} 436 | 437 | 变量内容如果有空格,必须使用双引号或者单引号。 438 | 439 | - 双引号内的特殊字符可以保留原本特性,例如 x="lang is $LANG",则 x 的值为 lang is zh_TW.UTF-8; 440 | - 单引号内的特殊字符就是特殊字符本身,例如 x='lang is $LANG',则 x 的值为 lang is $LANG。 441 | 442 | 443 | 可以使用 `指令` 或者 $(指令) 的方式将指令的执行结果赋值给变量。例如 version=$(uname -r),则 version 的值为 4.15.0-22-generic。 444 | 445 | 可以使用 export 命令将自定义变量转成环境变量,环境变量可以在子程序中使用,所谓子程序就是由当前 Bash 而产生的子 Bash。 446 | 447 | Bash 的变量可以声明为数组和整数数字。注意数字类型没有浮点数。如果不进行声明,默认是字符串类型。变量的声明使用 declare 命令: 448 | 449 | $ declare [-aixr] variable 450 | -a : 定义为数组类型 451 | -i : 定义为整数类型 452 | -x : 定义为环境变量 453 | -r : 定义为 readonly 类型 454 | 455 | 使用 [ ] 来对数组进行索引操作: 456 | 457 | $ array[1]=a 458 | $ array[2]=b 459 | $ echo ${array[1]} 460 | 461 | ## 指令搜索顺序 ## 462 | 463 | - 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls ; 464 | - 由别名找到该指令来执行; 465 | - 由 Bash 内置的指令来执行; 466 | - 按 $PATH 变量指定的搜索路径的顺序找到第一个指令来执行。 467 | 468 | ## 数据流重定向 ## 469 | 470 | 重定向指的是使用文件代替标准输入、标准输出和标准错误输出。 471 | 472 | 1| 代码 运算符 473 | -|- 474 | 标准输入 (stdin)| 0| < 或 << 475 | 标准输出 (stdout)| 1| > 或 >> 476 | 标准错误输出 (stderr)| 2| 2> 或 2>> 477 | 478 | 其中,有一个箭头的表示以覆盖的方式重定向,而有两个箭头的表示以追加的方式重定向。 479 | 480 | 可以将不需要的标准输出以及标准错误输出重定向到 /dev/null,相当于扔进垃圾箱。 481 | 482 | 如果需要将标准输出以及标准错误输出同时重定向到一个文件,需要将某个输出转换为另一个输出,例如 2>&1 表示将标准错误输出转换为标准输出。 483 | 484 | $ find /home -name .bashrc > list 2>&1 485 | 486 | # 六、管道指令 # 487 | 488 | 管道是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管道。 489 | 490 | 在命令之间使用 | 分隔各个管道命令。 491 | 492 | 493 | $ ls -al /etc | less 494 | 495 | # 七、正则表达式 # 496 | 497 | ## grep ## 498 | 499 | g/re/p(globally search a regular expression and print),使用正则表示式进行全局查找并打印。 500 | 501 | $ grep [-acinv] [--color=auto] 搜寻字符串 filename 502 | -c : 统计匹配到行的个数 503 | -i : 忽略大小写 504 | -n : 输出行号 505 | -v : 反向选择,也就是显示出没有 搜寻字符串 内容的那一行 506 | --color=auto :找到的关键字加颜色显示 507 | 508 | # 八、进程管理 # 509 | 510 | ## 查看进程 ## 511 | 512 | ### 1. ps ### 513 | 514 | 查看某个时间点的进程信息。 515 | 516 | 示例:查看自己的进程 517 | 518 | # ps -l 519 | 520 | 示例:查看系统所有进程 521 | 522 | # ps aux 523 | 524 | 示例:查看特定的进程 525 | 526 | # ps aux | grep threadx 527 | 528 | ### 2. pstree ### 529 | 530 | 查看进程树。 531 | 532 | 示例:查看所有进程树 533 | 534 | # pstree -A 535 | 536 | ### 3. top ### 537 | 538 | 实时显示进程信息。 539 | 540 | 并且top可以了解到CPU消耗,可以根据用户指定的时间来更新显示。 541 | 542 | 示例:两秒钟刷新一次 543 | 544 | # top -d 2 545 | 546 | ### 4. netstat ### 547 | 548 | 查看占用端口的进程 549 | 550 | 示例:查看特定端口的进程 551 | 552 | # netstat -anp | grep port 553 | 554 | ## 进程状态 ## 555 | 556 | 状态| 说明 557 | -|- 558 | R| running or runnable (on run queue),正在执行或者可执行,此时进程位于执行队列中。 559 | D| uninterruptible sleep (usually I/O),不可中断阻塞,通常为 IO 阻塞。 560 | S| interruptible sleep (waiting for an event to complete),可中断阻塞,此时进程正在等待某个事件完成。 561 | Z| zombie (terminated but not reaped by its parent),僵死,进程已经终止但是尚未被其父进程获取信息。 562 | T| stopped (either by a job control signal or because it is being traced),结束,进程既可以被作业控制信号结束,也可能是正在被追踪。 563 | 564 | ## 进程创建 ## 565 | 566 | ### 1.system函数-调用shell进程,开启新进程 ### 567 | 568 | system函数,是通过启动shell进程,然后执行shell命令进程。 569 | 570 | 原型: 571 | 572 | int system(const char *string); 573 | 574 | string:shell命令字符串 575 | 576 | 返回值:成功返回命令退出码,无法启动shell,返回127错误码,其他错误,返回-1。 577 | 578 | ### 2.exec系列函数-替换进程映像 ### 579 | 580 | exec系列函数调用时,启动新进程,替换掉当前进程。即程序不会再返回到原进程,除非exec调用失败。 581 | 582 | exec启动的新进程继承了原进程的许多特性,如在原进程中打开的文件描述符在新进程中仍保持打开。 583 | 584 | 需要注意的是,在原进程中打开的文件流在新进程中将关闭。原因在于,我们在前面讲过进程间通信的方式,进程之间需要管道才能通信。 585 | 586 | 原型: 587 | 588 | int execl(const char *path,const char *arg0,...,(char*)0); 589 | int execlp(const char *file,const char *arg0,...,(char*)0); 590 | int execle(const char *path,const char *arg0,...,(char*)0,char *const envp[]); 591 | int execv(cosnt char *path,char *const argv[]); 592 | int execvp(cosnt char *file,char *const argv[]); 593 | int execve(cosnt char *path,char *const argv[],char *const envp[]); 594 | 595 | path/file:进程命令路径/进程命令名 596 | argc:命令参数列表 597 | envp:新进程的环境变量 598 | 599 | ### 3.fork函数-复制进程映像 ### 600 | 601 | fork和exec的替换不同,调用fork函数,可复制一个和父进程一模一样的子进程。 602 | 603 | 执行的代码也完全相同,但子进程有自己的数据空间,环境和文件描述符。 604 | 605 | 原型: 606 | 607 | pid_t fork(); 608 | 609 | 父进程执行时,返回子进程的PID 610 | 子进程执行时,返回0 611 | 612 | ## SIGCHLD ## 613 | 614 | 当一个子进程改变了它的状态时(停止运行,继续运行或者退出),有两件事会发生在父进程中: 615 | 616 | - 得到 SIGCHLD 信号; 617 | - waitpid() 或者 wait() 调用会返回。 618 | 619 | 其中子进程发送的 SIGCHLD 信号包含了子进程的信息,比如进程 ID、进程状态、进程使用 CPU 的时间等。 620 | 621 | 在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。 622 | 623 | ## wait() ## 624 | 625 | pid_t wait(int *status) 626 | 627 | 父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,之后 wait() 函数会销毁子进程并返回。 628 | 629 | 如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 -1,同时 errno 被置为 ECHILD。 630 | 631 | 参数 status 用来保存被收集的子进程退出时的一些状态,如果对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL。 632 | 633 | ## waitpid() ## 634 | 635 | pid_t waitpid(pid_t pid, int *status, int options) 636 | 637 | 作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。 638 | 639 | pid 参数指示一个子进程的 ID,表示只关心这个子进程退出的 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。 640 | 641 | options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。 642 | 643 | ## 孤儿进程 ## 644 | 645 | 一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。 646 | 647 | 孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。 648 | 649 | 由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。 650 | 651 | ## 僵尸进程 ## 652 | 653 | 一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。 654 | 655 | 僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。 656 | 657 | 系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。 658 | 659 | 要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 进程所收养,这样 init 进程就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。 660 | 661 | 662 | -------------------------------------------------------------------------------- /操作系统/操作系统.md: -------------------------------------------------------------------------------- 1 | # 概述 2 | 3 | ## 基本特征 4 | 5 | ### 1.并发 6 | 7 | 并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。 8 | 9 | 并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。 10 | 11 | 操作系统通过引入进程和线程,使得程序能够并发运行。 12 | 13 | ### 2.共享 14 | 15 | 共享是指系统中的资源可以被多个并发进程共同使用。 16 | 17 | 有两种共享方式:互斥共享和同时共享。 18 | 19 | 互斥共享的资源称为临界资源,例如打印机等,在同一时刻只允许一个进程访问,需要用同步机制来实现互斥访问。 20 | 21 | ### 3.虚拟 22 | 23 | 虚拟技术把一个物理实体转换为多个逻辑实体。 24 | 25 | 主要有两种虚拟技术:时(时间)分复用技术和空(空间)分复用技术。 26 | 27 | 多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占用处理器,每次只执行一小个时间片并快速切换。 28 | 29 | 虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间的页被映射到物理内存,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。 30 | 31 | ### 4.异步 32 | 33 | 异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。 34 | 35 | ## 基本功能 36 | 37 | ### 1.进程管理 38 | 39 | 进程控制、进程同步、进程通信、死锁处理、处理机调度 40 | 41 | ### 2.内存管理 42 | 43 | 内存分配、地址映射、内存保护与共享、虚拟内存 44 | 45 | ### 3.文件管理 46 | 47 | 文件存储空间管理、目录管理、文件读写管理和保护 48 | 49 | ### 4.设备管理 50 | 51 | 完成用户的I/O请求,方便用户使用各种设备,并提高设备的利用率 52 | 53 | ## 系统调用 54 | 55 | 如果一个进程在用户态需要使用内核态的功能,就进行系统调用从而陷入内核,由操作系统代为完成。 56 | 57 | ## 大内核和微内核 58 | 59 | ### 大内核 60 | 61 | 大内核是将操作系统功能作为一个紧密结合的整体放到内核。 62 | 63 | 由于各模块共享信息,因此有很高的性能。 64 | 65 | ### 微内核 66 | 67 | 由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。 68 | 69 | 在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态。 70 | 71 | 因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。 72 | 73 | > 内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。 74 | > 75 | > 用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。 76 | 77 | ## 中断分类 78 | 79 | ### 外中断 80 | 81 | 由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。 82 | 83 | ### 异常 84 | 85 | 由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。 86 | 87 | ### 陷入 88 | 89 | 在用户程序中使用系统调用。 90 | 91 | # 进程管理 92 | 93 | ## 进程与线程 94 | 95 | ### 1.进程 96 | 97 | 进程是资源分配的基本单位。 98 | 99 | 进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。 100 | 101 | ### 2.线程 102 | 103 | 线程是独立调度的基本单位。 104 | 105 | 一个进程中可以有多个线程,它们共享进程资源。 106 | 107 | ### 3.区别 108 | 109 | Ⅰ 拥有资源 110 | 111 | 进程是资源分配的基本单位,但是线程不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),线程可以访问隶属进程的资源。 112 | 113 | > 在很多现代操作系统中,一个进程的虚拟地址空间大小为4G,分为系统(内核?)空间和用户空间两部分,系统空间为所有进程共享,而用户空间是独立的,一般WINDOWS进程的用户空间为2G。 114 | 115 | 一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的(/私有的)栈(stack),Windows线程的缺省堆栈大小为1M。堆(heap)的分配与栈有所不同,一般是一个进程有一个C运行时堆,这个堆为本进程中所有线程共享,windows进程还有所谓进程默认堆,用户也可以创建自己的堆。 116 | 117 | 进程拥有的资源: 118 | 119 | 1、虚拟地址空间 120 | 2、一个全局唯一的进程ID (PID) 121 | 3、一个可执行映像(image),也就是该进程的程序文件在内存中的表示 122 | 4、一个或多个线程 123 | 5、一个位于内核空间中的名为EPROCESS(executive process block,进程执行块)的数据结构,用以记录该进程的关键信息,包括进程的创建时间、映像文件名称等。 124 | 6、一个位于内核空间中的对象句柄表,用以记录和索引该进程所创建/打开的内核对象。操作系统根据该表格将用户模式下的句柄翻译为指向内核对象的指针。 125 | 7、一个位于描述内存目录表起始位置的基地址,简称页目录基地址(DirBase),当CPU切换到该进程/任务时,会将该地址加载到CR3寄存器,这样当前进程的虚拟地址才会被翻译为正确的物理地址。 126 | 8、一个位于用户空间中的进程环境块(Process Environment Block, PEB)。 127 | 9、一个访问权限令牌(access token),用于表示该进程的用户、安全组,以及优先级别。 128 | 129 | 130 | Ⅱ 调度 131 | 132 | 线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。 133 | 134 | 首先应该明白进程的调度,创建等实质上都是由操作系统实现的,所以说进程的实现只能由操作系统内核来实现,而不存在用户态实现的情况。但是对于线程就不同了,线程的管理者可以是用户也可以是操作系统本身,线程是进程内部的东西,当然存在由进程直接管理线程的可能性。因此线程的实现就应该分为内核态线程实现和用户态线程实现。 135 | 136 | Ⅲ 系统开销 137 | 138 | 由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。 139 | 140 | Ⅳ 通信方面 141 | 142 | 线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。 143 | 144 | ## 进程状态的切换 145 | 146 | 五种状态 147 | 148 | - 创建状态(new) :进程正在被创建,尚未到就绪状态。 149 | - 就绪状态(ready) :等待被调度。进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。 150 | - 运行状态(running) :进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。 151 | - 阻塞状态(waiting) :等待资源。又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。 152 | - 结束状态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。 153 | 154 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ProcessState.png) 155 | 156 | - 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。 157 | - 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。 158 | 159 | ## 进程调度的算法 160 | 161 | ### 1.批处理系统 162 | 163 | 批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。 164 | 165 | #### 1.1 先来先服务 first-come first-serverd(FCFS) 166 | 167 | 非抢占式的调度算法,按照请求的顺序进行调度。 168 | 169 | 有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。 170 | 171 | #### 1.2 短作业优先 shortest job first(SJF) 172 | 173 | 非抢占式的调度算法,按估计运行时间最短的顺序进行调度。 174 | 175 | 长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。 176 | 177 | #### 1.3 最短剩余时间优先 shortest remaining time next(SRTN) 178 | 179 | 最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 180 | 181 | 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。 182 | 183 | ### 2. 交互式系统 184 | 185 | 交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。 186 | 187 | #### 2.1 时间片轮转 188 | 189 | 将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。 190 | 191 | 时间片轮转算法的效率和时间片的大小有很大关系: 192 | 193 | - 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 194 | - 而如果时间片过长,那么实时性就不能得到保证。 195 | 196 | #### 2.2 优先级调度 197 | 198 | 为每个进程分配一个优先级,按优先级进行调度。 199 | 200 | 为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。 201 | 202 | #### 2.3 多级反馈队列 203 | 204 | 一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。 205 | 206 | 多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。 207 | 208 | 每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。 209 | 210 | 可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。 211 | 212 | ### 3.实时系统 213 | 214 | 实时系统要求一个请求在一个确定时间内得到响应。 215 | 216 | 分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。 217 | 218 | ## 进程同步 219 | 220 | ### 1. 临界区 221 | 222 | 对临界资源进行访问的那段代码称为临界区。 223 | 224 | 为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。 225 | 226 | ### 2.同步与互斥 227 | 228 | - 同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。 229 | 230 | - 互斥:多个进程在同一时刻只有一个进程能进入临界区。 231 | 232 | ### 3.线程间同步的方式 233 | 234 | - 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。 235 | 236 | - 信号量(Semphares) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量 237 | 238 | - 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作 239 | 240 | ### 4.信号量 241 | 242 | 信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。 243 | 244 | - down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0; 245 | - up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。 246 | 247 | down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。 248 | 249 | 如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。 250 | 251 | ## 同步问题 252 | 253 | ### 1.生产者-消费者问题 254 | 255 | 问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。 256 | 257 | 因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。 258 | 259 | #define N 100 260 | typedef int semaphore; 261 | semaphore mutex = 1; 262 | semaphore empty = N; 263 | semaphore full = 0; 264 | 265 | void producer() { 266 | while(TRUE) { 267 | int item = produce_item(); 268 | down(&empty); 269 | down(&mutex); 270 | insert_item(item); 271 | up(&mutex); 272 | up(&full); 273 | } 274 | } 275 | 276 | void consumer() { 277 | while(TRUE) { 278 | down(&full); 279 | down(&mutex); 280 | int item = remove_item(); 281 | consume_item(item); 282 | up(&mutex); 283 | up(&empty); 284 | } 285 | } 286 | 287 | ### 2.读者-写者问题 288 | 289 | 允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。 290 | 291 | 一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。 292 | 293 | typedef int semaphore; 294 | semaphore count_mutex = 1; 295 | semaphore data_mutex = 1; 296 | int count = 0; 297 | 298 | void reader() { 299 | while(TRUE) { 300 | down(&count_mutex); 301 | count++; 302 | if(count == 1) down(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问 303 | up(&count_mutex); 304 | read(); 305 | down(&count_mutex); 306 | count--; 307 | if(count == 0) up(&data_mutex); 308 | up(&count_mutex); 309 | } 310 | } 311 | 312 | void writer() { 313 | while(TRUE) { 314 | down(&data_mutex); 315 | write(); 316 | up(&data_mutex); 317 | } 318 | } 319 | 320 | ### 3.哲学家进餐问题 321 | 322 | 五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。 323 | 324 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/a9077f06-7584-4f2b-8c20-3a8e46928820.jpg) 325 | 326 | 为了防止死锁的发生,可以设置两个条件: 327 | 328 | - 必须同时拿起左右两根筷子; 329 | - 只有在两个邻居都没有进餐的情况下才允许进餐。 330 | 331 | #define N 5 332 | #define LEFT (i + N - 1) % N // 左邻居 333 | #define RIGHT (i + 1) % N // 右邻居 334 | #define THINKING 0 335 | #define HUNGRY 1 336 | #define EATING 2 337 | typedef int semaphore; 338 | int state[N]; // 跟踪每个哲学家的状态 339 | semaphore mutex = 1; // 临界区的互斥,临界区是 state 数组,对其修改需要互斥 340 | semaphore s[N]; // 每个哲学家一个信号量 341 | 342 | void philosopher(int i) { 343 | while(TRUE) { 344 | think(i); 345 | take_two(i); 346 | eat(i); 347 | put_two(i); 348 | } 349 | } 350 | 351 | void take_two(int i) { 352 | down(&mutex); 353 | state[i] = HUNGRY; 354 | check(i); 355 | up(&mutex); 356 | down(&s[i]); // 只有收到通知之后才可以开始吃,否则会一直等下去 357 | } 358 | 359 | void put_two(i) { 360 | down(&mutex); 361 | state[i] = THINKING; 362 | check(LEFT); // 尝试通知左右邻居,自己吃完了,你们可以开始吃了 363 | check(RIGHT); 364 | up(&mutex); 365 | } 366 | 367 | void eat(int i) { 368 | down(&mutex); 369 | state[i] = EATING; 370 | up(&mutex); 371 | } 372 | 373 | // 检查两个邻居是否都没有用餐,如果是的话,就 up(&s[i]),使得 down(&s[i]) 能够得到通知并继续执行 374 | void check(i) { 375 | if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) { 376 | state[i] = EATING; 377 | up(&s[i]); 378 | } 379 | } 380 | 381 | ## 进程通信 382 | 383 | 进程同步与进程通信很容易混淆,它们的区别在于: 384 | 385 | - 进程同步:控制多个进程按一定顺序执行; 386 | - 进程通信:进程间传输信息。 387 | 388 | 进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。 389 | 390 | ### 1. 管道 391 | 392 | 管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。 393 | 394 | 它具有以下限制: 395 | 396 | - 只支持半双工通信(单向交替传输); 397 | - 只能在父子进程或者兄弟进程中使用。 398 | 399 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/53cd9ade-b0a6-4399-b4de-7f1fbd06cdfb.png) 400 | 401 | ### 2. FIFO 402 | 403 | 也称为命名管道,去除了管道只能在父子进程中使用的限制。 404 | 405 | FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。 406 | 407 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/2ac50b81-d92a-4401-b9ec-f2113ecc3076.png) 408 | 409 | ### 3.消息队列 410 | 411 | 相比于 FIFO,消息队列具有以下优点: 412 | 413 | - 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难; 414 | - 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法; 415 | - 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。 416 | 417 | ### 4. 信号量 418 | 419 | 它是一个计数器,用于为多个进程提供对共享数据对象的访问。 420 | 421 | ### 5. 共享存储 422 | 423 | 允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。 424 | 425 | 需要使用信号量用来同步对共享存储的访问。 426 | 427 | 多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用内存的匿名段。 428 | 429 | ### 6. 套接字 430 | 431 | 与其它通信机制不同的是,它可用于不同机器间的进程通信。 432 | 433 | # 死锁 434 | 435 | ## 必要条件 436 | 437 | - 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。 438 | - 占有和等待:已经得到了某个资源的进程可以再请求新的资源。 439 | - 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。 440 | - 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。 441 | 442 | ## 处理方法 443 | 444 | 主要有以下四种方法: 445 | 446 | - 鸵鸟策略 447 | - 死锁检测与死锁恢复 448 | - 死锁预防 449 | - 死锁避免 450 | 451 | ## 鸵鸟策略 452 | 453 | 把头埋在沙子里,假装根本没发生问题。 454 | 455 | 因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。 456 | 457 | 当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。 458 | 459 | 大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。 460 | 461 | ## 死锁检测与死锁恢复 462 | 463 | 不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。 464 | 465 | ### 1. 每种类型一个资源的死锁检测 466 | 467 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/b1fa0453-a4b0-4eae-a352-48acca8fff74.png) 468 | 469 | 上图为资源分配图,其中方框表示资源,圆圈表示进程。资源指向进程表示该资源已经分配给该进程,进程指向资源表示进程请求获取该资源。 470 | 471 | 图 a 可以抽取出环,如图 b,它满足了环路等待条件,因此会发生死锁。 472 | 473 | 每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。 474 | 475 | ### 2. 每种类型多个资源的死锁检测 476 | 477 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e1eda3d5-5ec8-4708-8e25-1a04c5e11f48.png) 478 | 479 | 上图中,有三个进程四个资源,每个数据代表的含义如下: 480 | 481 | - E 向量:资源总量 482 | - A 向量:资源剩余量 483 | - C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量 484 | - R 矩阵:每个进程请求的资源数量 485 | 486 | 进程 P1 和 P2 所请求的资源都得不到满足,只有进程 P3 可以,让 P3 执行,之后释放 P3 拥有的资源,此时 A = (2 2 2 0)。P2 可以执行,执行后释放 P2 拥有的资源,A = (4 2 2 1) 。P1 也可以执行。所有进程都可以顺利执行,没有死锁。 487 | 488 | 算法总结如下: 489 | 490 | 每个进程最开始时都不被标记,执行过程有可能被标记。当算法结束时,任何没有被标记的进程都是死锁进程。 491 | 492 | 1. 寻找一个没有标记的进程 Pi,它所请求的资源小于等于 A。 493 | 1. 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。 494 | 1. 如果没有这样一个进程,算法终止。 495 | 496 | ### 3. 死锁恢复 497 | 498 | - 利用抢占恢复 499 | - 利用回滚恢复 500 | - 通过杀死进程恢复 501 | 502 | ## 死锁预防 503 | 504 | 在程序运行之前预防发生死锁。 505 | 506 | ### 1. 破坏互斥条件 507 | 508 | 例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。 509 | 510 | ### 2. 破坏占有和等待条件 511 | 512 | 一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。 513 | 514 | ### 3. 破坏不可抢占条件 515 | 516 | ### 4. 破坏环路等待 517 | 518 | 给资源统一编号,进程只能按编号顺序来请求资源。 519 | 520 | ## 死锁避免 521 | 522 | 在程序运行时避免发生死锁。 523 | 524 | ### 1. 安全状态 525 | 526 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ed523051-608f-4c3f-b343-383e2d194470.png) 527 | 528 | 图 a 的第二列 Has 表示已拥有的资源数,第三列 Max 表示总共需要的资源数,Free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源(图 b),运行结束后释放 B,此时 Free 变为 5(图 c);接着以同样的方式运行 C 和 A,使得所有进程都能成功运行,因此可以称图 a 所示的状态时安全的。 529 | 530 | 定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。 531 | 532 | 安全状态的检测与死锁的检测类似,因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类似,可以结合着做参考对比。 533 | 534 | ### 2. 单个资源的银行家算法 535 | 536 | 一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。 537 | 538 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/d160ec2e-cfe2-4640-bda7-62f53e58b8c0.png) 539 | 540 | 上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。 541 | 542 | ### 3. 多个资源的银行家算法 543 | 544 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/62e0dd4f-44c3-43ee-bb6e-fedb9e068519.png) 545 | 546 | 上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。 547 | 548 | 检查一个状态是否安全的算法如下: 549 | 550 | - 查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。 551 | - 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。 552 | - 重复以上两步,直到所有进程都标记为终止,则状态时安全的。 553 | 554 | 如果一个状态不是安全的,需要拒绝进入这个状态。 555 | 556 | # 内存管理 557 | 558 | ## 虚拟内存 559 | 560 | 虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。 561 | 562 | 为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 563 | 564 | 从上面的描述中可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。 565 | 566 | ## 分页系统地址映射 567 | 568 | 内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表(Page table)存储着页(程序地址空间)和页框(物理内存空间)的映射表。 569 | 570 | 一个虚拟地址分成两个部分,一部分存储页面号,一部分存储偏移量。 571 | 572 | 下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位。例如对于虚拟地址(0010 000000000100),前 4 位是存储页面号 2,读取表项内容为(110 1),页表项最后一位表示是否存在于内存中,1 表示存在。后 12 位存储偏移量。这个页对应的页框的地址为 (110 000000000100)。 573 | 574 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/cf4386a1-58c9-4eca-a17f-e12b1e9770eb.png) 575 | 576 | ## 页面置换算法 577 | 578 | 在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。 579 | 580 | 页面置换算法和缓存淘汰策略类似,可以将内存看成磁盘的缓存。在缓存系统中,缓存的大小有限,当有新的缓存到达时,需要淘汰一部分已经存在的缓存,这样才有空间存放新的缓存数据。 581 | 582 | 页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。 583 | 584 | ### 1. 最佳 OPT 585 | 586 | 所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。 587 | 588 | 是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。 589 | 590 | 举例:一个系统为某进程分配了三个物理块,并有如下页面引用序列: 591 | 592 | 7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1 593 | 594 | 开始运行时,先将 7, 0, 1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。 595 | 596 | ### 2. 最近最少使用 LRU 597 | 598 | 虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。LRU 将最近最少使用的页面换出。 599 | 600 | 为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。 601 | 602 | 因为每次访问都需要更新链表,因此这种方式实现的 LRU 代价很高。 603 | 604 | ### 3. 最近未使用 NRU 605 | 606 | 每个页面都有两个状态位:R 与 M,当页面被访问时设置页面的 R=1,当页面被修改时设置 M=1。其中 R 位会定时被清零。可以将页面分成以下四类: 607 | 608 | - R=0,M=0 609 | - R=0,M=1 610 | - R=1,M=0 611 | - R=1,M=1 612 | 613 | 当发生缺页中断时,NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。 614 | 615 | NRU 优先换出已经被修改的脏页面(R=0,M=1),而不是被频繁使用的干净页面(R=1,M=0)。 616 | 617 | ### 4. 先进先出 FIFO 618 | 619 | 选择换出的页面是最先进入的页面。 620 | 621 | 该算法会将那些经常被访问的页面换出,导致缺页率升高。 622 | 623 | ### 5. 第二次机会算法 624 | 625 | FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改: 626 | 627 | 当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索。 628 | 629 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ecf8ad5d-5403-48b9-b6e7-f2e20ffe8fca.png) 630 | 631 | ### 6.时钟 Clock 632 | 633 | 第二次机会算法需要在链表中移动页面,降低了效率。时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面。 634 | 635 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/5f5ef0b6-98ea-497c-a007-f6c55288eab1.png) 636 | 637 | ## 分段 638 | 639 | 虚拟内存采用的是分页技术,也就是将地址空间划分成固定大小的页,每一页再与内存进行映射。 640 | 641 | 下图为一个编译器在编译过程中建立的多个表,有 4 个表是动态增长的,如果使用分页系统的一维地址空间,动态增长的特点会导致覆盖问题的出现。 642 | 643 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/22de0538-7c6e-4365-bd3b-8ce3c5900216.png) 644 | 645 | 分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,并且可以动态增长。 646 | 647 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e0900bb2-220a-43b7-9aa9-1d5cd55ff56e.png) 648 | 649 | ## 段页式 650 | 651 | 程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页。这样既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能。 652 | 653 | ## 分页与分段的比较 654 | 655 | - 对程序员的透明性:分页透明,但是分段需要程序员显式划分每个段。 656 | 657 | - 地址空间的维度:分页是一维地址空间,分段是二维的。 658 | 659 | - 大小是否可以改变:页的大小不可变,段的大小可以动态改变。 660 | 661 | - 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。 662 | 663 | 664 | 665 | 666 | 667 | -------------------------------------------------------------------------------- /数据库/MySQL.md: -------------------------------------------------------------------------------- 1 | # 一、索引 # 2 | 3 | MySQL数据库的架构可以分为客户端,服务端,存储引擎和文件系统。 4 | 5 | 最高层的客户端,通过tcp连接mysql的服务器,然后执行sql语句,其中涉及了查询缓存,执行计划处理和优化,接下来再到存储引擎层执行查询,底层实际上访问的是主机的文件系统。 6 | 7 | ## B+ Tree 原理 ## 8 | 9 | ### 1. 数据结构 ### 10 | 11 | B Tree 指的是 Balance Tree,也就是平衡树。平衡树是一颗查找树,并且所有叶子节点位于同一层。 12 | 13 | B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。 14 | 15 | 在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。 16 | 17 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/33576849-9275-47bb-ada7-8ded5f5e7c73.png) 18 | 19 | ### 2. 操作 ### 20 | 21 | 进行查找操作时,首先在根节点进行二分查找,找到一个 key 所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的 data。 22 | 23 | 插入删除操作会破坏平衡树的平衡性,因此在进行插入删除操作之后,需要对树进行分裂、合并、旋转等操作来维护平衡性。 24 | 25 | ### 3. 与红黑树的比较 ### 26 | 27 | 红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构,这是因为使用 B+ 树访问磁盘数据有更高的性能。 28 | 29 | (一)B+ 树有更低的树高 30 | 31 | 平衡树的树高 O(h)=O(logdN),其中 d 为每个节点的出度。红黑树的出度为 2,而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多。 32 | 33 | (二)磁盘访问原理 34 | 35 | 操作系统一般将内存和磁盘分割成固定大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点。 36 | 37 | 如果数据不在同一个磁盘块上,那么通常需要移动制动手臂进行寻道,而制动手臂因为其物理结构导致了移动效率低下,从而增加磁盘数据读取时间。B+ 树相对于红黑树有更低的树高,进行寻道的次数与树高成正比,在同一个磁盘块上进行访问只需要很短的磁盘旋转时间,所以 B+ 树更适合磁盘数据的读取。 38 | 39 | (三)磁盘预读特性 40 | 41 | 为了减少磁盘 I/O 操作,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的磁盘旋转时间,速度会非常快。并且可以利用预读特性,相邻的节点也能够被预先载入。 42 | 43 | ## MySQL 索引 ## 44 | 45 | 索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 46 | 47 | ### 1. B+Tree 索引 ### 48 | 49 | 是大多数 MySQL 存储引擎的默认索引类型。 50 | 51 | 因为不再需要进行全表扫描,只需要对树进行搜索即可,所以查找速度快很多。 52 | 53 | 因为 B+ Tree 的有序性,所以除了用于查找,还可以用于排序和分组。 54 | 55 | 可以指定多个列作为索引列,多个索引列共同组成键。 56 | 57 | 适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。 58 | 59 | InnoDB 的 B+Tree 索引分为聚簇索引和辅助索引(非聚簇索引)。 60 | 61 | #### 聚簇索引 #### 62 | 63 | 聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分,每张表只能拥有一个聚簇索引。 64 | 65 | Innodb通过主键聚集数据,如果没有定义主键,innodb会选择非空的唯一索引代替。如果没有这样的索引,innodb会隐式的定义一个主键来作为聚簇索引。 66 | 67 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/45016e98-6879-4709-8569-262b2d6d60b9.png) 68 | 69 | #### 辅助索引(非聚簇索引) #### 70 | 71 | 在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找。辅助索引叶子节点存储的不再是行的物理位置,而是主键值。通过辅助索引首先找到的是主键值,再通过主键值找到数据行的数据页,再通过数据页中的Page Directory找到数据行。 72 | 73 | Innodb辅助索引的叶子节点并不包含行记录的全部数据,叶子节点除了包含键值外,还包含了相应行数据的聚簇索引键。 74 | 75 | 辅助索引的存在不影响数据在聚簇索引中的组织,所以一张表可以有多个辅助索引。在innodb中有时也称辅助索引为二级索引。 76 | 77 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7c349b91-050b-4d72-a7f8-ec86320307ea.png) 78 | 79 | ### 2. 哈希索引 ### 80 | 81 | 哈希索引能以 O(1) 时间进行查找,但是失去了有序性: 82 | 83 | - 无法用于排序与分组; 84 | - 只支持精确查找,无法用于部分查找和范围查找。 85 | 86 | InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 87 | 88 | ### 3. 全文索引 ### 89 | 90 | MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。 91 | 92 | 查找条件使用 MATCH AGAINST,而不是普通的 WHERE。 93 | 94 | 全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。 95 | 96 | InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。 97 | 98 | ### 4. 空间数据索引 ### 99 | 100 | MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 101 | 102 | 必须使用 GIS 相关的函数来维护数据。 103 | 104 | ## 索引优化 ## 105 | 106 | ### 1. 独立的列 ### 107 | 108 | 在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。 109 | 110 | 例如下面的查询不能使用 actor_id 列的索引: 111 | 112 | SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5; 113 | 114 | ### 2. 多列索引 ### 115 | 116 | 在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor_id 和 film_id 设置为多列索引。 117 | 118 | SELECT film_id, actor_ id FROM sakila.film_actor 119 | WHERE actor_id = 1 AND film_id = 1; 120 | 121 | ### 3. 索引列的顺序 ### 122 | 123 | 让选择性最强的索引列放在前面。 124 | 125 | 索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,每个记录的区分度越高,查询效率也越高。 126 | 127 | 例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。 128 | 129 | SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity, 130 | COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity, 131 | COUNT(*) 132 | FROM payment; 133 | 134 | ---------- 135 | 136 | staff_id_selectivity: 0.0001 137 | customer_id_selectivity: 0.0373 138 | COUNT(*): 16049 139 | 140 | ### 4. 前缀索引 ### 141 | 142 | 对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。 143 | 144 | 前缀长度的选取需要根据索引选择性来确定。 145 | 146 | ### 5. 覆盖索引 ### 147 | 148 | 索引包含所有需要查询的字段的值。 149 | 150 | 具有以下优点: 151 | 152 | - 索引通常远小于数据行的大小,只读取索引能大大减少数据访问量。 153 | - 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。 154 | - 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。 155 | 156 | ## 索引的优点 ## 157 | 158 | - 大大减少了服务器需要扫描的数据行数。 159 | 160 | - 帮助服务器避免进行排序和分组,以及避免创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,不需要排序和分组,也就不需要创建临时表)。 161 | 162 | - 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,会将相邻的数据都存储在一起)。 163 | 164 | ## 索引的使用条件 ## 165 | 166 | - 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效; 167 | 168 | - 对于中到大型的表,索引就非常有效; 169 | 170 | - 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。 171 | 172 | # 二、存储引擎 # 173 | 174 | ## InnoDB ## 175 | 176 | 是 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。 177 | 178 | 实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ)。在可重复读隔离级别下,通过多版本并发控制(MVCC)+ Next-Key Locking 防止幻影读。 179 | 180 | 主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。 181 | 182 | 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。 183 | 184 | 支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 185 | 186 | ## MyISAM ## 187 | 188 | 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。 189 | 190 | 提供了大量的特性,包括压缩表、空间数据索引等。 191 | 192 | 不支持事务。 193 | 194 | 不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 195 | 196 | 可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。 197 | 198 | 如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。 199 | 200 | ## 比较 ## 201 | 202 | - 事务:InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。 203 | - 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 204 | - 外键:InnoDB 支持外键。 205 | - 备份:InnoDB 支持在线热备份。 206 | - 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 207 | - 其它特性:MyISAM 支持压缩表和空间数据索引。 208 | 209 | # 三、数据类型 # 210 | 211 | ## 整型 ## 212 | 213 | TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。 214 | INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。 215 | 216 | ## 浮点数 ## 217 | 218 | FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。 219 | FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。 220 | 221 | ## 字符串 ## 222 | 223 | 主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。 224 | 225 | VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。 226 | 227 | 在进行存储和检索时,会保留 VARCHAR 末尾的空格,而会删除 CHAR 末尾的空格。 228 | 229 | ## 时间和日期 ## 230 | 231 | MySQL 提供了两种相似的日期时间类型:DATETIME 和 TIMESTAMP。 232 | 233 | ### 1. DATETIME ### 234 | 235 | 能够保存从 1000 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。 236 | 237 | 它与时区无关。 238 | 239 | 默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。 240 | 241 | ### 2. TIMESTAMP ### 242 | 243 | 和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年到 2038 年。 244 | 245 | 它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。 246 | 247 | MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。 248 | 249 | 默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。 250 | 251 | 应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。 252 | 253 | # 四、日志 # 254 | 255 | ## 重做日志(redo log) ## 256 | 257 | ### 重做日志-作用 ### 258 | 259 | 确保事务的持久性。 260 | 261 | 防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。 262 | 263 | ### 重做日志-内容 ### 264 | 265 | 物理格式的日志,记录的是物理数据页面的修改的信息,其redo log是顺序写入redo log file的物理文件中去的。 266 | 267 | redo log在事务没有提交前,每一个修改操作都会记录变更后的数据。 268 | 269 | 当更新一条数据时,InnoDB会找到要更新的行数据,把做了什么修改记录写到redolog中,并把这行数据更新到内存中,整个过程就算完成了。 270 | 271 | redolog是固定大小的,所以它只能循环记录做了什么修改,write pos为当前记录的位置,check point为当前可以擦除的位置,代表更新的行已经完成数据库的磁盘更改。如果前者速度,只能等待。 272 | 273 | ### 重做日志-什么时候产生 ### 274 | 275 | 事务开始之后就产生redo log,redo log的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo log文件中。 276 | 277 | ### 重做日志-什么时候释放 ### 278 | 279 | 当对应事务的脏页写入到磁盘之后,redo log的使命也就完成了,重做日志占用的空间就可以重用(被覆盖)。 280 | 281 | ### 重做日志-其他 ### 282 | 283 | 很重要一点,redo log是什么时候写盘的?前面说了是在事物开始之后逐步写盘的。 284 | 之所以说重做日志是在事务开始之后逐步写入重做日志文件,而不一定是事务提交才写入重做日志缓存, 285 | 原因就是,重做日志有一个缓存区Innodb_log_buffer,Innodb_log_buffer的默认大小为8M(这里设置的16M),Innodb存储引擎先将重做日志写入innodb_log_buffer中。 286 | 287 | 然后会通过以下三种方式将innodb日志缓冲区的日志刷新到磁盘 288 | 1,Master Thread 每秒一次执行刷新Innodb_log_buffer到重做日志文件。 289 | 2,每个事务提交时会将重做日志刷新到重做日志文件。 290 | 3,当重做日志缓存可用空间 少于一半时,重做日志缓存被刷新到重做日志文件 291 | 由此可以看出,重做日志通过不止一种方式写入到磁盘,尤其是对于第一种方式,Innodb_log_buffer到重做日志文件是Master Thread线程的定时任务。 292 | 因此重做日志的写盘,并不一定是随着事务的提交才写入重做日志文件的,而是随着事务的开始,逐步开始的。 293 | 另外引用《MySQL技术内幕 Innodb 存储引擎》(page37)上的原话: 294 | 即使某个事务还没有提交,Innodb存储引擎仍然每秒会将重做日志缓存刷新到重做日志文件。 295 | 这一点是必须要知道的,因为这可以很好地解释再大的事务的提交(commit)的时间也是很短暂的。 296 | 297 | ## 回滚日志(undo log) ## 298 | 299 | ### 回滚日志-作用 ### 300 | 301 | 保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读 302 | 303 | ### 回滚日志-内容 ### 304 | 305 | 逻辑格式的日志,在执行undo的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于redo log的。 306 | 307 | ### 回滚日志-什么时候产生 ### 308 | 309 | 事务开始之前,将当前是的版本生成undo log,undo 也会产生 redo 来保证undo log的可靠性 310 | 311 | ### 回滚日志-什么时候释放 ### 312 | 313 | 当事务提交之后,undo log并不能立马被删除, 314 | 而是放入待清理的链表,由purge线程判断是否由其他事务在使用undo段中表的上一个事务之前的版本信息,决定是否可以清理undo log的日志空间。 315 | 316 | ## 二进制日志(binlog) ## 317 | 318 | 1,用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。 319 | 2,用于数据库的基于时间点的还原。 320 | 321 | ### 二进制日志-内容 ### 322 | 323 | 逻辑格式的日志,可以简单认为就是执行过的事务中的sql语句。 324 | 但又不完全是sql语句这么简单,而是包括了执行的sql语句(增删改)反向的信息, 325 | 也就意味着delete对应着delete本身和其反向的insert;update对应着update执行前后的版本的信息;insert对应着delete和insert本身的信息。 326 | 327 | ### 二进制日志-什么时候产生 ### 328 | 329 | 事务提交的时候,一次性将事务中的sql语句(一个事物可能对应多个sql语句)按照一定的格式记录到binlog中。 330 | 331 | ### 二进制日志-什么时候释放 ### 332 | 333 | binlog的默认是保持时间由参数expire_logs_days配置,也就是说对于非活动的日志文件,在生成时间超过expire_logs_days配置的天数之后,会被自动删除。 334 | 335 | ### 二进制日志-其他 ### 336 | 337 | 二进制日志的作用之一是还原数据库的,这与redo log很类似,很多人混淆过,但是两者有本质的不同 338 | 1,作用不同:redo log是保证事务的持久性的,是事务层面的,作为异常宕机或者介质故障后的数据恢复使用。binlog作为还原的功能,是数据库层面的(当然也可以精确到事务层面的),可以作为恢复数据使用,主从复制搭建​​​​​​​,虽然都有还原的意思,但是其保护数据的层次是不一样的。 339 | 2,内容不同:redo log是物理日志,是数据页面的修改之后的物理记录,binlog是逻辑日志,可以简单认为记录的就是sql语句 340 | 3,另外,两者日志产生的时间,可以释放的时间,在可释放的情况下清理机制,都是完全不同的。 341 | 4,恢复数据时候的效率,基于物理日志的redo log恢复数据的效率要高于语句逻辑日志的binlog 342 | 5,redo log是循环写,日志空间大小固定;binlog是追加写,是指一份写到一定大小的时候会更换下一个文件,不会覆盖。 343 | 344 | # 五、切分 # 345 | 346 | ## 水平切分 ## 347 | 348 | 水平切分又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。 349 | 350 | 当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。 351 | 352 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg) 353 | 354 | ## 垂直切分 ## 355 | 356 | 垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。 357 | 358 | 在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。 359 | 360 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e130e5b8-b19a-4f1e-b860-223040525cf6.jpg) 361 | 362 | ## 水平切分策略 ## 363 | 364 | - 哈希取模:hash(key) % N; 365 | - 范围:可以是 ID 范围也可以是时间范围; 366 | - 映射表:使用单独的一个数据库来存储映射关系。 367 | 368 | ## 水平切分存在的问题 ## 369 | 370 | ### 1. 事务问题 ### 371 | 372 | 使用分布式事务来解决,比如 XA 接口。 373 | 374 | ### 2. 连接 ### 375 | 376 | 可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接。 377 | 378 | ### 3. ID 唯一性 ### 379 | 380 | - 使用全局唯一 ID(GUID) 381 | - 为每个分片指定一个 ID 范围 382 | - 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法) 383 | 384 | # 六、复制 # 385 | 386 | ## 主从复制 ## 387 | 388 | 主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。 389 | 390 | - binlog 线程 :负责将主服务器上的数据更改写入二进制日志(Binary log)中。 391 | - I/O 线程 :负责从主服务器上读取二进制日志,并写入从服务器的中继日志(Relay log)。 392 | - SQL 线程 :负责读取中继日志,解析出主服务器已经执行的数据更改并在从服务器中重放(Replay)。 393 | 394 | 395 | ## 读写分离 ## 396 | 397 | 主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。 398 | 399 | 读写分离能提高性能的原因在于: 400 | 401 | - 主从服务器负责各自的读和写,极大程度缓解了锁的争用; 402 | - 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销; 403 | - 增加冗余,提高可用性。 404 | 405 | 读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。 -------------------------------------------------------------------------------- /数据库/Redis.md: -------------------------------------------------------------------------------- 1 | # 一、概述 # 2 | 3 | Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。 4 | 5 | 键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。 6 | 7 | Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。 8 | 9 | ## 关系型数据库与非关系型数据库 ## 10 | 11 | 1、数据存储方式不同。 12 | 13 | 关系型和非关系型数据库的主要差异是数据存储的方式。关系型数据天然就是表格式的,因此存储在数据表的行和列中。数据表可以彼此关联协作存储,也很容易提取数据。 14 | 15 | 与其相反,非关系型数据不适合存储在数据表的行和列中,而是大块组合在一起。非关系型数据通常存储在数据集中,就像文档、键值对或者图结构。你的数据及其特性是选择数据存储和提取方式的首要影响因素。 16 | 17 | 2、扩展方式不同。 18 | 19 | SQL和NoSQL数据库最大的差别可能是在扩展方式上,要支持日益增长的需求当然要扩展。要支持更多并发量,SQL数据库是纵向扩展,也就是说提高处理能力,使用速度更快速的计算机,这样处理相同的数据集就更快了。因为数据存储在关系表中,操作的性能瓶颈可能涉及很多个表,这都需要通过提高计算机性能来客服。虽然SQL数据库有很大扩展空间,但最终肯定会达到纵向扩展的上限。 20 | 21 | 而NoSQL数据库是横向扩展的。而非关系型数据存储天然就是分布式的,NoSQL数据库的扩展可以通过给资源池添加更多普通的数据库服务器(节点)来分担负载。 22 | 23 | 3、对事务性的支持不同。 24 | 25 | 如果数据操作需要高事务性或者复杂数据查询需要控制执行计划,那么传统的SQL数据库从性能和稳定性方面考虑是你的最佳选择。SQL数据库支持对事务原子性细粒度控制,并且易于回滚事务。 26 | 27 | 虽然NoSQL数据库也可以使用事务操作,但稳定性方面没法和关系型数据库比较,所以它们真正闪亮的价值是在操作的扩展性和大数据量处理方面。 28 | 29 | # 二、数据类型 # 30 | 31 | 数据类型|可以存储的值|操作 32 | -|-|- 33 | STRING|字符串、整数或者浮点数| 对整个字符串或者字符串的其中一部分执行操作,对整数和浮点数执行自增或者自减操作 34 | LIST| 列表 |从两端压入或者弹出元素,对单个或者多个元素进行修剪,只保留一个范围内的元素 35 | SET| 无序集合| 添加、获取、移除单个元素,检查一个元素是否存在于集合中,计算交集、并集、差集,从集合里面随机获取元素 36 | HASH| 包含键值对的无序散列表| 添加、获取、移除单个键值对,获取所有键值对,检查某个键是否存在 37 | ZSET| 有序集合| 添加、获取、删除元素,根据分值范围或者成员来获取元素,计算一个键的排名 38 | 39 | ## STRING ## 40 | 41 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/6019b2db-bc3e-4408-b6d8-96025f4481d6.png) 42 | 43 | > set hello world 44 | OK 45 | > get hello 46 | "world" 47 | > del hello 48 | (integer) 1 49 | > get hello 50 | (nil) 51 | 52 | ## LIST ## 53 | 54 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/fb327611-7e2b-4f2f-9f5b-38592d408f07.png) 55 | 56 | > rpush list-key item 57 | (integer) 1 58 | > rpush list-key item2 59 | (integer) 2 60 | > rpush list-key item 61 | (integer) 3 62 | 63 | > lrange list-key 0 -1 64 | 1) "item" 65 | 2) "item2" 66 | 3) "item" 67 | 68 | > lindex list-key 1 69 | "item2" 70 | 71 | > lpop list-key 72 | "item" 73 | 74 | > lrange list-key 0 -1 75 | 1) "item2" 76 | 2) "item" 77 | 78 | ## SET ## 79 | 80 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/cd5fbcff-3f35-43a6-8ffa-082a93ce0f0e.png) 81 | 82 | > sadd set-key item 83 | (integer) 1 84 | > sadd set-key item2 85 | (integer) 1 86 | > sadd set-key item3 87 | (integer) 1 88 | > sadd set-key item 89 | (integer) 0 90 | 91 | > smembers set-key 92 | 1) "item" 93 | 2) "item2" 94 | 3) "item3" 95 | 96 | > sismember set-key item4 97 | (integer) 0 98 | > sismember set-key item 99 | (integer) 1 100 | 101 | > srem set-key item2 102 | (integer) 1 103 | > srem set-key item2 104 | (integer) 0 105 | 106 | > smembers set-key 107 | 1) "item" 108 | 2) "item3" 109 | 110 | ## HASH ## 111 | 112 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7bd202a7-93d4-4f3a-a878-af68ae25539a.png) 113 | 114 | > hset hash-key sub-key1 value1 115 | (integer) 1 116 | > hset hash-key sub-key2 value2 117 | (integer) 1 118 | > hset hash-key sub-key1 value1 119 | (integer) 0 120 | 121 | > hgetall hash-key 122 | 1) "sub-key1" 123 | 2) "value1" 124 | 3) "sub-key2" 125 | 4) "value2" 126 | 127 | > hdel hash-key sub-key2 128 | (integer) 1 129 | > hdel hash-key sub-key2 130 | (integer) 0 131 | 132 | > hget hash-key sub-key1 133 | "value1" 134 | 135 | > hgetall hash-key 136 | 1) "sub-key1" 137 | 2) "value1" 138 | 139 | ## ZSET ## 140 | 141 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1202b2d6-9469-4251-bd47-ca6034fb6116.png) 142 | 143 | > zadd zset-key 728 member1 144 | (integer) 1 145 | > zadd zset-key 982 member0 146 | (integer) 1 147 | > zadd zset-key 982 member0 148 | (integer) 0 149 | 150 | > zrange zset-key 0 -1 withscores 151 | 1) "member1" 152 | 2) "728" 153 | 3) "member0" 154 | 4) "982" 155 | 156 | > zrangebyscore zset-key 0 800 withscores 157 | 1) "member1" 158 | 2) "728" 159 | 160 | > zrem zset-key member1 161 | (integer) 1 162 | > zrem zset-key member1 163 | (integer) 0 164 | 165 | > zrange zset-key 0 -1 withscores 166 | 1) "member0" 167 | 2) "982" 168 | 169 | # 三、数据结构 # 170 | 171 | ## 字典 ## 172 | 173 | dictht 是一个散列表结构,使用拉链法解决哈希冲突。 174 | 175 | /* This is our hash table structure. Every dictionary has two of this as we 176 | * implement incremental rehashing, for the old to the new table. */ 177 | typedef struct dictht { 178 | dictEntry **table; 179 | unsigned long size; 180 | unsigned long sizemask; 181 | unsigned long used; 182 | } dictht; 183 | 184 | typedef struct dictEntry { 185 | void *key; 186 | union { 187 | void *val; 188 | uint64_t u64; 189 | int64_t s64; 190 | double d; 191 | } v; 192 | struct dictEntry *next; 193 | } dictEntry; 194 | 195 | Redis 的字典 dict 中包含两个哈希表 dictht,这是为了方便进行 rehash 操作。在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。 196 | 197 | typedef struct dict { 198 | dictType *type; 199 | void *privdata; 200 | dictht ht[2]; 201 | long rehashidx; /* rehashing not in progress if rehashidx == -1 */ 202 | unsigned long iterators; /* number of iterators currently running */ 203 | } dict; 204 | 205 | rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。 206 | 207 | 渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始,然后每执行一次 rehash 都会递增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,并令 rehashidx++。 208 | 209 | 在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。 210 | 211 | 采用渐进式 rehash 会导致字典中的数据分散在两个 dictht 上,因此对字典的查找操作也需要到对应的 dictht 去执行。 212 | 213 | ## 跳跃表 ## 214 | 215 | 是有序集合的底层实现之一。 216 | 217 | 跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。 218 | 219 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/beba612e-dc5b-4fc2-869d-0b23408ac90a.png) 220 | 221 | 在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。 222 | 223 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0ea37ee2-c224-4c79-b895-e131c6805c40.png) 224 | 225 | 与红黑树等平衡树相比,跳跃表具有以下优点: 226 | 227 | - 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性; 228 | - 更容易实现; 229 | - 支持无锁操作。 230 | 231 | # 四、使用场景 # 232 | 233 | ## 计数器 ## 234 | 235 | 可以对 String 进行自增自减运算,从而实现计数器功能。 236 | 237 | Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。 238 | 239 | ## 缓存 ## 240 | 241 | 将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。 242 | 243 | ## 查找表 ## 244 | 245 | 例如 DNS 记录就很适合使用 Redis 进行存储。 246 | 247 | 查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。 248 | 249 | ## 消息队列 ## 250 | 251 | List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息 252 | 253 | 不过最好使用 Kafka、RabbitMQ 等消息中间件。 254 | 255 | ## 会话缓存 ## 256 | 257 | 可以使用 Redis 来统一存储多台应用服务器的会话信息。 258 | 259 | 当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。 260 | 261 | ## 分布式锁实现 ## 262 | 263 | 在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。 264 | 265 | 可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。 266 | 267 | ## 其它 ## 268 | 269 | Set 可以实现交集、并集等操作,从而实现共同好友等功能。 270 | 271 | ZSet 可以实现有序性操作,从而实现排行榜等功能。 272 | 273 | # 五、Redis 与 Memcached # 274 | 275 | 两者都是非关系型内存键值数据库,主要有以下不同: 276 | 277 | ### 数据类型 ### 278 | 279 | Memcached 仅支持字符串类型,而 Redis 支持五种不同的数据类型,可以更灵活地解决问题。 280 | 281 | ### 数据持久化 ### 282 | 283 | Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。 284 | 285 | ### 分布式 ### 286 | 287 | Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 288 | 289 | Redis Cluster 实现了分布式的支持。 290 | 291 | ### 内存管理机制 ### 292 | 293 | - 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。 294 | 295 | - Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 296 | 297 | # 六、键的过期时间 # 298 | 299 | Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。 300 | 301 | 对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。 302 | 303 | # 七、数据淘汰策略 # 304 | 305 | 可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。 306 | 307 | Redis 具体有 6 种淘汰策略: 308 | 309 | 策略 |描述 310 | -|- 311 | volatile-lru| 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 312 | volatile-ttl| 从已设置过期时间的数据集中挑选将要过期的数据淘汰 313 | volatile-random| 从已设置过期时间的数据集中任意选择数据淘汰 314 | allkeys-lru| 从所有数据集中挑选最近最少使用的数据淘汰 315 | allkeys-random| 从所有数据集中任意选择数据进行淘汰 316 | noeviction| 禁止驱逐数据 317 | 318 | 作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。 319 | 320 | 使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 321 | 322 | Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。 323 | 324 | # 八、持久化 # 325 | 326 | Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。 327 | 328 | ## RDB 持久化 ## 329 | 330 | 将某个时间点的所有数据都存放到硬盘上。 331 | 332 | 可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。 333 | 334 | 如果系统发生故障,将会丢失最后一次创建快照之后的数据。 335 | 336 | 如果数据量很大,保存快照的时间会很长。 337 | 338 | ## AOF 持久化 ## 339 | 340 | 将写命令添加到 AOF 文件(Append Only File)的末尾。 341 | 342 | 使用 AOF 持久化需要设置同步选项,从而确保写命令同步到磁盘文件上的时机。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项: 343 | 344 | 选项| 同步频率 345 | -|- 346 | always| 每个写命令都同步 347 | everysec| 每秒同步一次 348 | no| 让操作系统来决定何时同步 349 | 350 | - always 选项会严重减低服务器的性能; 351 | - everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响; 352 | - no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。 353 | 354 | 随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。 355 | 356 | # 九、事务 # 357 | 358 | 一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。 359 | 360 | 事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。 361 | 362 | Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。 363 | 364 | # 十、事件 # 365 | 366 | Redis 服务器是一个事件驱动程序。 367 | 368 | ## 文件事件 ## 369 | 370 | 服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。 371 | 372 | Redis 基于 Reactor 模式开发了自己的网络事件处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的事件传送给文件事件分派器,分派器会根据套接字产生的事件类型调用相应的事件处理器。 373 | 374 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/9ea86eb5-000a-4281-b948-7b567bd6f1d8.png) 375 | 376 | ## 时间事件 ## 377 | 378 | 服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。 379 | 380 | 时间事件又分为: 381 | 382 | - 定时事件:是让一段程序在指定的时间之内执行一次; 383 | - 周期性事件:是让一段程序每隔指定时间就执行一次。 384 | 385 | Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用相应的事件处理器。 386 | 387 | ## 事件的调度与执行 ## 388 | 389 | 服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能一直监听,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。 390 | 391 | 事件调度与执行由 aeProcessEvents 函数负责,伪代码如下: 392 | 393 | def aeProcessEvents(): 394 | # 获取到达时间离当前时间最接近的时间事件 395 | time_event = aeSearchNearestTimer() 396 | # 计算最接近的时间事件距离到达还有多少毫秒 397 | remaind_ms = time_event.when - unix_ts_now() 398 | # 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0 399 | if remaind_ms < 0: 400 | remaind_ms = 0 401 | # 根据 remaind_ms 的值,创建 timeval 402 | timeval = create_timeval_with_ms(remaind_ms) 403 | # 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定 404 | aeApiPoll(timeval) 405 | # 处理所有已产生的文件事件 406 | procesFileEvents() 407 | # 处理所有已到达的时间事件 408 | processTimeEvents() 409 | 410 | 将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下: 411 | 412 | def main(): 413 | # 初始化服务器 414 | init_server() 415 | # 一直处理事件,直到服务器关闭为止 416 | while server_is_not_shutdown(): 417 | aeProcessEvents() 418 | # 服务器关闭,执行清理操作 419 | clean_server() 420 | 421 | 从事件处理的角度来看,服务器运行流程如下: 422 | 423 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c0a9fa91-da2e-4892-8c9f-80206a6f7047.png) 424 | 425 | # 十一、复制 # 426 | 427 | 通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。 428 | 429 | 一个从服务器只能有一个主服务器,并且不支持主主复制。 430 | 431 | ## 连接过程 ## 432 | 433 | 1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令; 434 | 435 | 1. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令; 436 | 437 | 1. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。 438 | 439 | ## 主从链 ## 440 | 441 | 随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。 442 | 443 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/395a9e83-b1a1-4a1d-b170-d081e7bb5bab.png) 444 | 445 | 446 | # 十二、哨兵模式 # 447 | 448 | Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。 449 | 450 | # 十三、分片 # 451 | 452 | 分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。 453 | 454 | 假设有 4 个 Redis 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... ,有不同的方式来选择一个指定的键存储在哪个实例中。 455 | 456 | - 最简单的方式是范围分片,例如用户 id 从 0~1000 的存储到实例 R0 中,用户 id 从 1001~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。 457 | - 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 458 | 459 | 根据执行分片的位置,可以分为三种分片方式: 460 | 461 | - 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。 462 | - 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。 463 | - 服务器分片:Redis Cluster。 464 | 465 | # 十四、一个简单的论坛系统分析 # 466 | 467 | 该论坛系统功能如下: 468 | 469 | - 可以发布文章; 470 | - 可以对文章进行点赞; 471 | - 在首页可以按文章的发布时间或者文章的点赞数进行排序显示。 472 | 473 | ## 文章信息 ## 474 | 475 | 文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。 476 | 477 | Redis 没有关系型数据库中的表这一概念来将同种类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID,通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617,其中 article 为命名空间,ID 为 92617。 478 | 479 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/7c54de21-e2ff-402e-bc42-4037de1c1592.png) 480 | 481 | ## 点赞功能 ## 482 | 483 | 当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作,还必须记录该用户已经对该文章进行了点赞,防止用户点赞次数超过 1。可以建立文章的已投票用户集合来进行记录。 484 | 485 | 为了节约内存,规定一篇文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定。 486 | 487 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/485fdf34-ccf8-4185-97c6-17374ee719a0.png) 488 | 489 | ## 对文章进行排序 ## 490 | 491 | 为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的) 492 | 493 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f7d170a3-e446-4a64-ac2d-cb95028f81a8.png) -------------------------------------------------------------------------------- /数据库/SQL.md: -------------------------------------------------------------------------------- 1 | # 一、基础 # 2 | 3 | 模式定义了数据如何存储、存储什么样的数据以及数据如何分解等信息,数据库和表都有模式。 4 | 5 | 主键的值不允许修改,也不允许复用(不能将已经删除的主键值赋给新数据行的主键)。 6 | 7 | SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。 8 | 9 | SQL 语句不区分大小写,但是数据库表名、列名和值是否区分依赖于具体的 DBMS 以及配置。 10 | 11 | SQL 支持以下三种注释: 12 | 13 | # 注释 14 | SELECT * 15 | FROM mytable; -- 注释 16 | /* 注释1 17 | 注释2 */ 18 | 19 | 数据库创建与使用: 20 | 21 | CREATE DATABASE test; 22 | USE test; 23 | 24 | # 二、创建表 # 25 | 26 | CREATE TABLE mytable ( 27 | # int 类型,不为空,自增 28 | id INT NOT NULL AUTO_INCREMENT, 29 | # int 类型,不可为空,默认值为 1,不为空 30 | col1 INT NOT NULL DEFAULT 1, 31 | # 变长字符串类型,最长为 45 个字符,可以为空 32 | col2 VARCHAR(45) NULL, 33 | # 日期类型,可为空 34 | col3 DATE NULL, 35 | # 设置主键为 id 36 | PRIMARY KEY (`id`)); 37 | 38 | # 三、修改表 # 39 | 40 | ### 添加列 ### 41 | 42 | ALTER TABLE mytable 43 | ADD col CHAR(20); 44 | 45 | ### 删除列 ### 46 | 47 | ALTER TABLE mytable 48 | DROP COLUMN col; 49 | 50 | ### 删除表 ### 51 | 52 | DROP TABLE mytable; 53 | 54 | # 四、插入 # 55 | 56 | ### 普通插入 ### 57 | 58 | INSERT INTO mytable(col1, col2) 59 | VALUES(val1, val2); 60 | 61 | ### 插入检索出来的数据 ### 62 | 63 | INSERT INTO mytable1(col1, col2) 64 | SELECT col1, col2 65 | FROM mytable2; 66 | 67 | ### 将一个表的内容插入到一个新表 ### 68 | 69 | CREATE TABLE newtable AS 70 | SELECT * FROM mytable; 71 | 72 | # 五、更新 # 73 | 74 | UPDATE mytable 75 | SET col = val 76 | WHERE id = 1; 77 | 78 | # 六、删除 # 79 | 80 | DELETE FROM mytable 81 | WHERE id = 1; 82 | 83 | TRUNCATE TABLE 可以清空表,也就是删除所有行。 84 | 85 | TRUNCATE TABLE mytable; 86 | 87 | 使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。 88 | 89 | # 七、查询 # 90 | 91 | ## DISTINCT ## 92 | 93 | 相同值只会出现一次。它作用于所有列,也就是说所有列的值都相同才算相同。 94 | 95 | SELECT DISTINCT col1, col2 96 | FROM mytable; 97 | 98 | ## LIMIT ## 99 | 100 | 限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。 101 | 102 | 返回前 5 行: 103 | 104 | SELECT * 105 | FROM mytable 106 | LIMIT 5; 107 | 108 | ---------- 109 | SELECT * 110 | FROM mytable 111 | LIMIT 0, 5; 112 | 113 | 返回第 3 ~ 5 行: 114 | 115 | SELECT * 116 | FROM mytable 117 | LIMIT 2, 3; 118 | 119 | # 八、排序 # 120 | 121 | - ASC :升序(默认) 122 | - DESC :降序 123 | 124 | 可以按多个列进行排序,并且为每个列指定不同的排序方式: 125 | 126 | SELECT * 127 | FROM mytable 128 | ORDER BY col1 DESC, col2 ASC; 129 | 130 | # 九、过滤 # 131 | 132 | 不进行过滤的数据非常大,导致通过网络传输了多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。 133 | 134 | SELECT * 135 | FROM mytable 136 | WHERE col IS NULL; 137 | 138 | 下表显示了 WHERE 子句可用的操作符 139 | 140 | 操作符| 说明 141 | -|- 142 | =| 等于 143 | <| 小于 144 | >| 大于 145 | <> !=| 不等于 146 | <= !>| 小于等于 147 | >= !<| 大于等于 148 | BETWEEN| 在两个值之间 149 | IS NULL| 为 NULL 值 150 | 151 | 应该注意到,NULL 与 0、空字符串都不同。 152 | 153 | **AND 和 OR** 用于连接多个过滤条件。优先处理 AND,当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰。 154 | 155 | **IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。 156 | 157 | **NOT** 操作符用于否定一个条件。 158 | 159 | # 十、通配符 # 160 | 161 | 通配符也是用在过滤语句中,但它只能用于文本字段。 162 | 163 | - % 匹配 >=0 个任意字符; 164 | 165 | - _ 匹配 ==1 个任意字符; 166 | 167 | - [ ] 可以匹配集合内的字符,例如 [ab] 将匹配字符 a 或者 b。用脱字符 ^ 可以对其进行否定,也就是不匹配集合内的字符。 168 | 169 | 使用 Like 来进行通配符匹配。 170 | 171 | SELECT * 172 | FROM mytable 173 | WHERE col LIKE '[^AB]%'; -- 不以 A 和 B 开头的任意文本 174 | 175 | 不要滥用通配符,通配符位于开头处匹配会非常慢。 176 | 177 | # 十一、计算字段 # 178 | 179 | 在数据库服务器上完成数据的转换和格式化的工作往往比客户端上快得多,并且转换和格式化后的数据量更少的话可以减少网络通信量。 180 | 181 | 计算字段通常需要使用 AS 来取别名,否则输出的时候字段名为计算表达式。 182 | 183 | SELECT col1 * col2 AS alias 184 | FROM mytable; 185 | 186 | CONCAT() 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 TRIM() 可以去除首尾空格。 187 | 188 | SELECT CONCAT(TRIM(col1), '(', TRIM(col2), ')') AS concat_col 189 | FROM mytable; 190 | 191 | # 十二、函数 # 192 | 193 | 各个 DBMS 的函数都是不相同的,因此不可移植,以下主要是 MySQL 的函数。 194 | 195 | ## 汇总 ## 196 | 197 | 函 数| 说 明 198 | -|- 199 | AVG()| 返回某列的平均值 200 | COUNT()| 返回某列的行数 201 | MAX()| 返回某列的最大值 202 | MIN()| 返回某列的最小值 203 | SUM()| 返回某列值之和 204 | 205 | AVG() 会忽略 NULL 行。 206 | 207 | 使用 DISTINCT 可以汇总不同的值。 208 | 209 | SELECT AVG(DISTINCT col1) AS avg_col 210 | FROM mytable; 211 | 212 | ## 数值处理 ## 213 | 214 | 函数| 说明 215 | -|- 216 | SIN()| 正弦 217 | COS()| 余弦 218 | TAN()| 正切 219 | ABS()| 绝对值 220 | SQRT()| 平方根 221 | MOD()| 余数 222 | EXP()| 指数 223 | PI()| 圆周率 224 | RAND()| 随机数 225 | 226 | # 十三、分组 # 227 | 228 | 把具有相同的数据值的行放在同一组中。 229 | 230 | 可以对同一分组数据使用汇总函数进行处理,例如求分组数据的平均值等。 231 | 232 | 指定的分组字段除了能按该字段进行分组,也会自动按该字段进行排序。 233 | 234 | SELECT col, COUNT(*) AS num 235 | FROM mytable 236 | GROUP BY col; 237 | 238 | GROUP BY 自动按分组字段进行排序,ORDER BY 也可以按汇总字段来进行排序。 239 | 240 | SELECT col, COUNT(*) AS num 241 | FROM mytable 242 | GROUP BY col 243 | ORDER BY num; 244 | 245 | WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤。 246 | 247 | SELECT col, COUNT(*) AS num 248 | FROM mytable 249 | WHERE col > 2 250 | GROUP BY col 251 | HAVING num >= 2; 252 | 253 | 分组规定: 254 | 255 | - GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前; 256 | - 除了汇总字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出; 257 | - NULL 的行会单独分为一组; 258 | - 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。 259 | 260 | # 十四、子查询 # 261 | 262 | 子查询中只能返回一个字段的数据。 263 | 264 | 可以将子查询的结果作为 WHRER 语句的过滤条件: 265 | 266 | SELECT * 267 | FROM mytable1 268 | WHERE col1 IN (SELECT col2 269 | FROM mytable2); 270 | 271 | 下面的语句可以检索出客户的订单数量,子查询语句会对第一个查询检索出的每个客户执行一次: 272 | 273 | SELECT cust_name, (SELECT COUNT(*) 274 | FROM Orders 275 | WHERE Orders.cust_id = Customers.cust_id) 276 | AS orders_num 277 | FROM Customers 278 | ORDER BY cust_name; 279 | 280 | # 十五、连接 # 281 | 282 | 连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 WHERE。 283 | 284 | 连接可以替换子查询,并且比子查询的效率一般会更快。 285 | 286 | 可以用 AS 给列名、计算字段和表名取别名,给表名取别名是为了简化 SQL 语句以及连接相同表。 287 | 288 | ## 内连接 ## 289 | 290 | 内连接又称等值连接,使用 INNER JOIN 关键字。 291 | 292 | SELECT A.value, B.value 293 | FROM tablea AS A INNER JOIN tableb AS B 294 | ON A.key = B.key; 295 | 296 | 可以不明确使用 INNER JOIN,而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。 297 | 298 | SELECT A.value, B.value 299 | FROM tablea AS A, tableb AS B 300 | WHERE A.key = B.key; 301 | 302 | ## 自连接 ## 303 | 304 | 自连接可以看成内连接的一种,只是连接的表是自身而已。 305 | 306 | 一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。 307 | 308 | ### 子查询版本 ### 309 | 310 | SELECT name 311 | FROM employee 312 | WHERE department = ( 313 | SELECT department 314 | FROM employee 315 | WHERE name = "Jim"); 316 | 317 | 318 | ### 自连接版本 ### 319 | 320 | SELECT e1.name 321 | FROM employee AS e1 INNER JOIN employee AS e2 322 | ON e1.department = e2.department 323 | AND e2.name = "Jim"; 324 | 325 | ## 自然连接 ## 326 | 327 | 自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。 328 | 329 | 内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。 330 | 331 | SELECT A.value, B.value 332 | FROM tablea AS A NATURAL JOIN tableb AS B; 333 | 334 | ## 外连接 ## 335 | 336 | 外连接保留了没有关联的那些行。分为左外连接,右外连接以及全外连接,左外连接就是保留左表没有关联的行。 337 | 338 | 检索所有顾客的订单信息,包括还没有订单信息的顾客。 339 | 340 | SELECT Customers.cust_id, Customer.cust_name, Orders.order_id 341 | FROM Customers LEFT OUTER JOIN Orders 342 | ON Customers.cust_id = Orders.cust_id; 343 | 344 | ### customers 表: ### 345 | 346 | cust_id| cust_name 347 | -|- 348 | 1| a 349 | 2| b 350 | 3| c 351 | 352 | ### orders 表: ### 353 | 354 | order_id| cust_id 355 | -|- 356 | 1| 1 357 | 2| 1 358 | 3| 3 359 | 4| 3 360 | 361 | ### 结果: ### 362 | 363 | cust_id| cust_name| order_id 364 | -|-|- 365 | 1| a| 1 366 | 1| a| 2 367 | 3| c| 3 368 | 3| c| 4 369 | 2| b| Null 370 | 371 | # 十六、组合查询 # 372 | 373 | 使用 UNION 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。 374 | 375 | 每个查询必须包含相同的列、表达式和聚集函数。 376 | 377 | 默认会去除相同行,如果需要保留相同行,使用 UNION ALL。 378 | 379 | 只能包含一个 ORDER BY 子句,并且必须位于语句的最后。 380 | 381 | SELECT col 382 | FROM mytable 383 | WHERE col = 1 384 | UNION 385 | SELECT col 386 | FROM mytable 387 | WHERE col =2; 388 | 389 | # 十七、视图 # 390 | 391 | 视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。 392 | 393 | 对视图的操作和对普通表的操作一样。 394 | 395 | 视图具有如下好处: 396 | 397 | - 简化复杂的 SQL 操作,比如复杂的连接; 398 | - 只使用实际表的一部分数据; 399 | - 通过只给用户访问视图的权限,保证数据的安全性; 400 | - 更改数据格式和表示。 401 | 402 | CREATE VIEW myview AS 403 | SELECT Concat(col1, col2) AS concat_col, col3*col4 AS compute_col 404 | FROM mytable 405 | WHERE col5 = val; 406 | -------------------------------------------------------------------------------- /数据库/leedcode-database.md: -------------------------------------------------------------------------------- 1 | # 175. 组合两个表 # 2 | 3 | 表1: Person 4 | 5 | +-------------+---------+ 6 | | 列名 | 类型 | 7 | +-------------+---------+ 8 | | PersonId | int | 9 | | FirstName | varchar | 10 | | LastName | varchar | 11 | +-------------+---------+ 12 | PersonId 是上表主键 13 | 14 | 表2: Address 15 | 16 | +-------------+---------+ 17 | | 列名 | 类型 | 18 | +-------------+---------+ 19 | | AddressId | int | 20 | | PersonId | int | 21 | | City | varchar | 22 | | State | varchar | 23 | +-------------+---------+ 24 | AddressId 是上表主键 25 | 26 | 编写一个 SQL 查询,满足条件:无论 person 是否有地址信息,都需要基于上述两表提供 person 的以下信息: 27 | 28 | FirstName, LastName, City, State 29 | 30 | ---------- 31 | 32 | SELECT FirstName, LastName, City, State 33 | FROM Person 34 | LEFT JOIN Address 35 | ON Person.PersonId = Address.PersonId 36 | 37 | # 176. 第二高的薪水 # 38 | 39 | 编写一个 SQL 查询,获取 Employee 表中第二高的薪水(Salary) 。 40 | 41 | +----+--------+ 42 | | Id | Salary | 43 | +----+--------+ 44 | | 1 | 100 | 45 | | 2 | 200 | 46 | | 3 | 300 | 47 | +----+--------+ 48 | 49 | 例如上述 Employee 表,SQL查询应该返回 200 作为第二高的薪水。如果不存在第二高的薪水,那么查询应返回 null。 50 | 51 | +---------------------+ 52 | | SecondHighestSalary | 53 | +---------------------+ 54 | | 200 | 55 | +---------------------+ 56 | 57 | 58 | ---------- 59 | 60 | SELECT 61 | IFNULL( 62 | (SELECT DISTINCT Salary 63 | FROM Employee 64 | ORDER BY Salary DESC 65 | LIMIT 1, 1), 66 | NULL) AS SecondHighestSalary 67 | 68 | # 177. 第N高的薪水 # 69 | 70 | 编写一个 SQL 查询,获取 Employee 表中第 n 高的薪水(Salary)。 71 | 72 | +----+--------+ 73 | | Id | Salary | 74 | +----+--------+ 75 | | 1 | 100 | 76 | | 2 | 200 | 77 | | 3 | 300 | 78 | +----+--------+ 79 | 80 | 例如上述 Employee 表,n = 2 时,应返回第二高的薪水 200。如果不存在第 n 高的薪水,那么查询应返回 null。 81 | 82 | +------------------------+ 83 | | getNthHighestSalary(2) | 84 | +------------------------+ 85 | | 200 | 86 | +------------------------+ 87 | 88 | 89 | ---------- 90 | 91 | CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INT 92 | BEGIN 93 | RETURN ( 94 | # Write your MySQL query statement below. 95 | SELECT DISTINCT a.Salary 96 | FROM Employee a 97 | WHERE N-1 = ( 98 | SELECT COUNT(DISTINCT b.Salary) 99 | FROM Employee b 100 | WHERE b.Salary > a.Salary) 101 | ); 102 | END 103 | 104 | # 178. 分数排名 # 105 | 106 | 编写一个 SQL 查询来实现分数排名。 107 | 108 | 如果两个分数相同,则两个分数排名(Rank)相同。请注意,平分后的下一个名次应该是下一个连续的整数值。换句话说,名次之间不应该有“间隔”。 109 | 110 | +----+-------+ 111 | | Id | Score | 112 | +----+-------+ 113 | | 1 | 3.50 | 114 | | 2 | 3.65 | 115 | | 3 | 4.00 | 116 | | 4 | 3.85 | 117 | | 5 | 4.00 | 118 | | 6 | 3.65 | 119 | +----+-------+ 120 | 121 | 例如,根据上述给定的 Scores 表,你的查询应该返回(按分数从高到低排列): 122 | 123 | +-------+------+ 124 | | Score | Rank | 125 | +-------+------+ 126 | | 4.00 | 1 | 127 | | 4.00 | 1 | 128 | | 3.85 | 2 | 129 | | 3.65 | 3 | 130 | | 3.65 | 3 | 131 | | 3.50 | 4 | 132 | +-------+------+ 133 | 134 | 重要提示:对于 MySQL 解决方案,如果要转义用作列名的保留字,可以在关键字之前和之后使用撇号。例如 `Rank` 135 | 136 | SElECT a.Score AS Score, 137 | (SElECT COUNT(DISTINCT b.Score) FROM Scores b WHERE b.Score >= a.Score) AS 'Rank' 138 | FROM Scores a 139 | ORDER BY a.Score DESC 140 | 141 | # 180. 连续出现的数字 # 142 | 143 | 编写一个 SQL 查询,查找所有至少连续出现三次的数字。 144 | 145 | +----+-----+ 146 | | Id | Num | 147 | +----+-----+ 148 | | 1 | 1 | 149 | | 2 | 1 | 150 | | 3 | 1 | 151 | | 4 | 2 | 152 | | 5 | 1 | 153 | | 6 | 2 | 154 | | 7 | 2 | 155 | +----+-----+ 156 | 157 | 例如,给定上面的 Logs 表, 1 是唯一连续出现至少三次的数字。 158 | 159 | +-----------------+ 160 | | ConsecutiveNums | 161 | +-----------------+ 162 | | 1 | 163 | +-----------------+ 164 | 165 | 166 | ---------- 167 | 168 | SELECT DISTINCT l1.Num AS ConsecutiveNums 169 | FROM Logs l1, Logs l2, Logs l3 170 | WHERE l1.Id = l2.Id - 1 171 | AND l2.Id = l3.Id - 1 172 | AND l1.Num = l2.Num 173 | AND l1.Num = l3.Num 174 | 175 | # 181. 超过经理收入的员工 # 176 | 177 | Employee 表包含所有员工,他们的经理也属于员工。每个员工都有一个 Id,此外还有一列对应员工的经理的 Id。 178 | 179 | +----+-------+--------+-----------+ 180 | | Id | Name | Salary | ManagerId | 181 | +----+-------+--------+-----------+ 182 | | 1 | Joe | 70000 | 3 | 183 | | 2 | Henry | 80000 | 4 | 184 | | 3 | Sam | 60000 | NULL | 185 | | 4 | Max | 90000 | NULL | 186 | +----+-------+--------+-----------+ 187 | 188 | 给定 Employee 表,编写一个 SQL 查询,该查询可以获取收入超过他们经理的员工的姓名。在上面的表格中,Joe 是唯一一个收入超过他的经理的员工。 189 | 190 | +----------+ 191 | | Employee | 192 | +----------+ 193 | | Joe | 194 | +----------+ 195 | 196 | SELECT a.Name AS Employee 197 | FROM Employee a, Employee b 198 | WHERE a.Salary > b.Salary 199 | AND a.ManagerId = B.Id 200 | 201 | # 182. 查找重复的电子邮箱 # 202 | 203 | 编写一个 SQL 查询,查找 Person 表中所有重复的电子邮箱。 204 | 205 | +----+---------+ 206 | | Id | Email | 207 | +----+---------+ 208 | | 1 | a@b.com | 209 | | 2 | c@d.com | 210 | | 3 | a@b.com | 211 | +----+---------+ 212 | 213 | 根据以上输入,你的查询应返回以下结果: 214 | 215 | +---------+ 216 | | Email | 217 | +---------+ 218 | | a@b.com | 219 | +---------+ 220 | 221 | 222 | ---------- 223 | 224 | SELECT DISTINCT Email 225 | FROM Person 226 | GROUP BY Email 227 | HAVING COUNT(*) > 1 228 | 229 | # 183. 从不订购的客户 # 230 | 231 | 某网站包含两个表,Customers 表和 Orders 表。编写一个 SQL 查询,找出所有从不订购任何东西的客户。 232 | 233 | Customers 表: 234 | 235 | +----+-------+ 236 | | Id | Name | 237 | +----+-------+ 238 | | 1 | Joe | 239 | | 2 | Henry | 240 | | 3 | Sam | 241 | | 4 | Max | 242 | +----+-------+ 243 | 244 | Orders 表: 245 | 246 | +----+------------+ 247 | | Id | CustomerId | 248 | +----+------------+ 249 | | 1 | 3 | 250 | | 2 | 1 | 251 | +----+------------+ 252 | 253 | 例如给定上述表格,你的查询应返回: 254 | 255 | +-----------+ 256 | | Customers | 257 | +-----------+ 258 | | Henry | 259 | | Max | 260 | +-----------+ 261 | 262 | 263 | ---------- 264 | 265 | select customers.name as 'Customers' 266 | from customers 267 | where customers.id not in 268 | ( 269 | select customerid from orders 270 | ); 271 | 272 | # 184. 部门工资最高的员工 # 273 | 274 | Employee 表包含所有员工信息,每个员工有其对应的 Id, salary 和 department Id。 275 | 276 | +----+-------+--------+--------------+ 277 | | Id | Name | Salary | DepartmentId | 278 | +----+-------+--------+--------------+ 279 | | 1 | Joe | 70000 | 1 | 280 | | 2 | Jim | 90000 | 1 | 281 | | 3 | Henry | 80000 | 2 | 282 | | 4 | Sam | 60000 | 2 | 283 | | 5 | Max | 90000 | 1 | 284 | +----+-------+--------+--------------+ 285 | 286 | Department 表包含公司所有部门的信息。 287 | 288 | +----+----------+ 289 | | Id | Name | 290 | +----+----------+ 291 | | 1 | IT | 292 | | 2 | Sales | 293 | +----+----------+ 294 | 295 | 编写一个 SQL 查询,找出每个部门工资最高的员工。对于上述表,您的 SQL 查询应返回以下行(行的顺序无关紧要)。 296 | 297 | +------------+----------+--------+ 298 | | Department | Employee | Salary | 299 | +------------+----------+--------+ 300 | | IT | Max | 90000 | 301 | | IT | Jim | 90000 | 302 | | Sales | Henry | 80000 | 303 | +------------+----------+--------+ 304 | 305 | 306 | ---------- 307 | 308 | 309 | SELECT Department.Name AS 'Department', Employee.Name AS 'Employee', Salary 310 | FROM Employee JOIN Department 311 | On Employee.DepartmentId = Department.id 312 | WHERE (Employee.DepartmentId, Salary) IN 313 | ( 314 | SELECT DepartmentId, MAX(Salary) 315 | FROM Employee 316 | GROUP BY DepartmentId 317 | ) 318 | 319 | # 196. 删除重复的电子邮箱 # 320 | 321 | 编写一个 SQL 查询,来删除 Person 表中所有重复的电子邮箱,重复的邮箱里只保留 Id 最小 的那个。 322 | 323 | +----+------------------+ 324 | | Id | Email | 325 | +----+------------------+ 326 | | 1 | john@example.com | 327 | | 2 | bob@example.com | 328 | | 3 | john@example.com | 329 | +----+------------------+ 330 | Id 是这个表的主键。 331 | 332 | 例如,在运行你的查询语句之后,上面的 Person 表应返回以下几行: 333 | 334 | +----+------------------+ 335 | | Id | Email | 336 | +----+------------------+ 337 | | 1 | john@example.com | 338 | | 2 | bob@example.com | 339 | +----+------------------+ 340 | 341 | 342 | ---------- 343 | 344 | DELETE a 345 | FROM Person a, Person b 346 | WHERE a.Email = b.Email AND a.Id > b.Id 347 | 348 | # 197. 上升的温度 # 349 | 350 | 给定一个 Weather 表,编写一个 SQL 查询,来查找与之前(昨天的)日期相比温度更高的所有日期的 Id。 351 | 352 | +---------+------------------+------------------+ 353 | | Id(INT) | RecordDate(DATE) | Temperature(INT) | 354 | +---------+------------------+------------------+ 355 | | 1 | 2015-01-01 | 10 | 356 | | 2 | 2015-01-02 | 25 | 357 | | 3 | 2015-01-03 | 20 | 358 | | 4 | 2015-01-04 | 30 | 359 | +---------+------------------+------------------+ 360 | 361 | 例如,根据上述给定的 Weather 表格,返回如下 Id: 362 | 363 | +----+ 364 | | Id | 365 | +----+ 366 | | 2 | 367 | | 4 | 368 | +----+ 369 | 370 | ---------- 371 | 372 | SELECT a.Id AS Id 373 | FROM WEATHER a, WEATHER b 374 | WHERE DATEDIFF(a.RecordDate, b.RecordDate) = 1 375 | AND a.Temperature > b.Temperature 376 | 377 | # 595. 大的国家 # 378 | 379 | 这里有张 World 表 380 | 381 | +-----------------+------------+------------+--------------+---------------+ 382 | | name | continent | area | population | gdp | 383 | +-----------------+------------+------------+--------------+---------------+ 384 | | Afghanistan | Asia | 652230 | 25500100 | 20343000 | 385 | | Albania | Europe | 28748 | 2831741 | 12960000 | 386 | | Algeria | Africa | 2381741 | 37100000 | 188681000 | 387 | | Andorra | Europe | 468 | 78115 | 3712000 | 388 | | Angola | Africa | 1246700 | 20609294 | 100990000 | 389 | +-----------------+------------+------------+--------------+---------------+ 390 | 391 | 如果一个国家的面积超过300万平方公里,或者人口超过2500万,那么这个国家就是大国家。 392 | 393 | 编写一个SQL查询,输出表中所有大国家的名称、人口和面积。 394 | 395 | 例如,根据上表,我们应该输出: 396 | 397 | +--------------+-------------+--------------+ 398 | | name | population | area | 399 | +--------------+-------------+--------------+ 400 | | Afghanistan | 25500100 | 652230 | 401 | | Algeria | 37100000 | 2381741 | 402 | +--------------+-------------+--------------+ 403 | 404 | 405 | ---------- 406 | 407 | SELECT name, population, area 408 | FROM World 409 | WHERE area > 3000000 OR population > 25000000 410 | 411 | # 596. 超过5名学生的课 # 412 | 413 | 有一个courses 表 ,有: student (学生) 和 class (课程)。 414 | 请列出所有超过或等于5名学生的课。 415 | 例如,表: 416 | 417 | +---------+------------+ 418 | | student | class | 419 | +---------+------------+ 420 | | A | Math | 421 | | B | English | 422 | | C | Math | 423 | | D | Biology | 424 | | E | Math | 425 | | F | Computer | 426 | | G | Math | 427 | | H | Math | 428 | | I | Math | 429 | +---------+------------+ 430 | 431 | 应该输出: 432 | 433 | +---------+ 434 | | class | 435 | +---------+ 436 | | Math | 437 | +---------+ 438 | 439 | 440 | ---------- 441 | 442 | SELECT class 443 | FROM courses 444 | GROUP BY class 445 | HAVING COUNT(DISTINCT student) >= 5 446 | 447 | # 627. 交换工资 # 448 | 449 | 给定一个 salary 表,如下所示,有 m = 男性 和 f = 女性 的值。交换所有的 f 和 m 值(例如,将所有 f 值更改为 m,反之亦然)。要求只使用一个更新(Update)语句,并且没有中间的临时表。 450 | 451 | 注意,您必只能写一个 Update 语句,请不要编写任何 Select 语句。 452 | 453 | | id | name | sex | salary | 454 | |----|------|-----|--------| 455 | | 1 | A | m | 2500 | 456 | | 2 | B | f | 1500 | 457 | | 3 | C | m | 5500 | 458 | | 4 | D | f | 500 | 459 | 460 | 461 | ---------- 462 | 463 | 464 | UPDATE salary 465 | SET 466 | sex = CASE sex 467 | WHEN 'm' THEN 'f' 468 | ELSE 'm' 469 | END; -------------------------------------------------------------------------------- /数据库/数据库系统原理.md: -------------------------------------------------------------------------------- 1 | # 一、事务 # 2 | 3 | ## 概念 ## 4 | 5 | 事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。 6 | 7 | ## ACID ## 8 | 9 | ### 1. 原子性(Atomicity) ### 10 | 11 | 事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。 12 | 13 | ### 2. 一致性(Consistency) ### 14 | 15 | 数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。 16 | 17 | ### 3. 隔离性(Isolation) ### 18 | 19 | 一个事务所做的修改在最终提交以前,对其它事务是不可见的。 20 | 21 | ### 4. 持久性(Durability) ### 22 | 23 | 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。 24 | 25 | 系统发生奔溃可以用重做日志(Redo Log)进行恢复,从而实现持久性。与回滚日志记录数据的逻辑修改不同,重做日志记录的是数据页的物理修改。 26 | 27 | 事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系: 28 | 29 | - 只有满足一致性,事务的执行结果才是正确的。 30 | - 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。 31 | - 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。 32 | - 事务满足持久化是为了能应对系统崩溃的情况。 33 | 34 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207210437023.png) 35 | 36 | ## AUTOCOMMIT ## 37 | 38 | MySQL 默认采用自动提交模式。也就是说,如果不显式使用START TRANSACTION语句来开始一个事务,那么每个查询操作都会被当做一个事务并自动提交。 39 | 40 | # 二、并发一致性问题 # 41 | 42 | 在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。 43 | 44 | ## 丢失修改 ## 45 | 46 | 丢失修改指一个事务的更新操作被另外一个事务的更新操作替换。一般在现实生活中常会遇到,例如:T1 和 T2 两个事务都对一个数据进行修改,T1 先修改并提交生效,T2 随后修改,T2 的修改覆盖了 T1 的修改。 47 | 48 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207221744244.png) 49 | 50 | ## 读脏数据 ## 51 | 52 | 读脏数据指在不同的事务下,当前事务可以读到另外事务未提交的数据。例如:T1 修改一个数据但未提交,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。 53 | 54 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207221920368.png) 55 | 56 | ## 不可重复读 ## 57 | 58 | 不可重复读指在一个事务内多次读取同一数据集合。在这一事务还未结束前,另一事务也访问了该同一数据集合并做了修改,由于第二个事务的修改,第一次事务的两次读取的数据可能不一致。例如:T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 59 | 60 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207222102010.png) 61 | 62 | ## 幻影读 ## 63 | 64 | 幻读本质上也属于不可重复读的情况,T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 65 | 66 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207222134306.png) 67 | 68 | 产生并发不一致性问题的主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。 69 | 70 | # 三、封锁 # 71 | 72 | ## 封锁粒度 ## 73 | 74 | MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 75 | 76 | 应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。 77 | 78 | 但是加锁需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。因此封锁粒度越小,系统开销就越大。 79 | 80 | 在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。 81 | 82 | ## 封锁类型 ## 83 | 84 | ### 1. 读写锁 ### 85 | 86 | - 互斥锁(Exclusive),简写为 X 锁,又称写锁。 87 | - 共享锁(Shared),简写为 S 锁,又称读锁。 88 | 89 | 有以下两个规定: 90 | 91 | - 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。 92 | - 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。 93 | 94 | 锁的兼容关系如下: 95 | 96 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207213523777.png) 97 | 98 | ### 2. 意向锁 ### 99 | 100 | 使用意向锁(Intention Locks)可以更容易地支持多粒度封锁。 101 | 102 | 在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。 103 | 104 | 意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定: 105 | 106 | - 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁; 107 | - 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。 108 | 109 | 通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。 110 | 111 | 各种锁的兼容关系如下: 112 | 113 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207214442687.png) 114 | 115 | ## 封锁协议 ## 116 | 117 | ### 两段锁协议 ### 118 | 119 | 加锁和解锁分为两个阶段进行。 120 | 121 | 可串行化调度是指,通过并发控制,使得并发执行的事务结果与某个串行执行的事务结果相同。串行执行的事务互不干扰,不会出现并发一致性问题。 122 | 123 | 事务遵循两段锁协议是保证可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。 124 | 125 | lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B) 126 | 127 | 但不是必要条件,例如以下操作不满足两段锁协议,但它还是可串行化调度。 128 | 129 | lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C) 130 | 131 | ## MySQL 隐式与显示锁定 ## 132 | 133 | MySQL 的 InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。 134 | 135 | InnoDB 也可以使用特定的语句进行显示锁定: 136 | 137 | SELECT ... LOCK In SHARE MODE; 138 | SELECT ... FOR UPDATE; 139 | 140 | # 四、隔离级别 # 141 | 142 | ## 未提交读(READ UNCOMMITTED) ## 143 | 144 | 事务中的修改,即使没有提交,对其它事务也是可见的。 145 | 146 | ## 提交读(READ COMMITTED) ## 147 | 148 | 一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。 149 | 150 | ## 可重复读(REPEATABLE READ) ## 151 | 152 | 保证在同一个事务中多次读取同一数据的结果是一样的。 153 | 154 | ## 可串行化(SERIALIZABLE) ## 155 | 156 | 强制事务串行执行,这样多个事务互不干扰,不会出现并发一致性问题。 157 | 158 | 该隔离级别需要加锁实现,因为要使用加锁机制保证同一时间只有一个事务执行,也就是保证事务串行执行。 159 | 160 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207223400787.png) 161 | 162 | # 五、多版本并发控制 # 163 | 164 | 多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 165 | 166 | ## 基本思想 ## 167 | 168 | 在封锁一节中提到,加锁能解决多个事务同时执行时出现的并发一致性问题。在实际场景中读操作往往多于写操作,因此又引入了读写锁来避免不必要的加锁操作,例如读和读没有互斥关系。读写锁中读和写操作仍然是互斥的,而 MVCC 利用了多版本的思想,写操作更新最新的版本快照,而读操作去读旧版本快照,没有互斥关系,这一点和 CopyOnWrite 类似。 169 | 170 | 在 MVCC 中事务的修改操作(DELETE、INSERT、UPDATE)会为数据行新增一个版本快照。 171 | 172 | 脏读和不可重复读最根本的原因是事务读取到其它事务未提交的修改。在事务进行读取操作时,为了解决脏读和不可重复读问题,MVCC 规定只能读取已经提交的快照。当然一个事务可以读取自身未提交的快照,这不算是脏读。 173 | 174 | ## 版本号 ## 175 | 176 | - 系统版本号 SYS_ID:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。 177 | - 事务版本号 TRX_ID :事务开始时的系统版本号。 178 | 179 | ## Undo log(回滚日志) ## 180 | 181 | MVCC 的多版本指的是多个版本的快照,快照存储在 Undo 日志中,该日志通过回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。 182 | 183 | 例如在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。我们先插入一个数据行,然后对该数据行执行两次更新操作。 184 | 185 | INSERT INTO t(id, x) VALUES(1, "a"); 186 | UPDATE t SET x="b" WHERE id=1; 187 | UPDATE t SET x="c" WHERE id=1; 188 | 189 | 因为没有使用 START TRANSACTION 将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面的操作总共涉及到三个事务。快照中除了记录事务版本号 TRX_ID 和操作之外,还记录了一个 bit 的 DEL 字段,用于标记是否被删除。 190 | 191 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208164808217.png) 192 | 193 | INSERT、UPDATE、DELETE 操作会创建一个日志,并将事务版本号 TRX_ID 写入。DELETE 可以看成是一个特殊的 UPDATE,还会额外将 DEL 字段设置为 1。 194 | 195 | ## ReadView ## 196 | 197 | MVCC 维护了一个 ReadView 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, ...},还有该列表的最小值 TRX_ID_MIN 和 TRX_ID_MAX。 198 | 199 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208171445674.png) 200 | 201 | 在进行 SELECT 操作时,根据数据行快照的 TRX_ID 与 TRX_ID_MIN 和 TRX_ID_MAX 之间的关系,从而判断数据行快照是否可以使用: 202 | 203 | - TRX_ID < TRX_ID_MIN,表示该数据行快照时在当前所有未提交事务之前进行更改的,因此可以使用。 204 | 205 | - TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。 206 | 207 | - TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断: 208 | 209 | - 提交读:如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。 210 | - 可重复读:都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。 211 | 212 | 在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。 213 | 214 | ## 快照读与当前读 ## 215 | 216 | ### 1. 快照读 ### 217 | 218 | MVCC 的 SELECT 操作是快照中的数据,不需要进行加锁操作。 219 | 220 | SELECT * FROM table ...; 221 | 222 | ### 2. 当前读 ### 223 | 224 | MVCC 其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。可以看到 MVCC 并不是完全不用加锁,而只是避免了 SELECT 的加锁操作。 225 | 226 | # 六、Next-Key Locks # 227 | 228 | Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。 229 | 230 | MVCC 不能解决幻影读问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。 231 | 232 | ## Record Locks ## 233 | 234 | 锁定一个记录上的索引,而不是记录本身。 235 | 236 | 如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。 237 | 238 | ## Gap Locks ## 239 | 240 | 锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。 241 | 242 | SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; 243 | 244 | ## Next-Key Locks ## 245 | 246 | 它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。它锁定一个前开后闭区间,例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间: 247 | 248 | (-∞, 10] 249 | (10, 11] 250 | (11, 13] 251 | (13, 20] 252 | (20, +∞) 253 | 254 | # 七、关系数据库设计理论 # 255 | 256 | ## 范式 ## 257 | 258 | ### 1. 第一范式 (1NF) ### 259 | 260 | 属性不可分。 261 | 262 | ### 2. 第二范式 (2NF) ### 263 | 264 | 每个非主属性完全函数依赖于键码。 265 | 266 | 可以通过分解来满足。 267 | 268 | ### 3. 第三范式 (3NF) ### 269 | 270 | 非主属性不传递函数依赖于键码。 -------------------------------------------------------------------------------- /计算机网络/HTTP.md: -------------------------------------------------------------------------------- 1 | # 一、基础概念 # 2 | 3 | ## URI ## 4 | 5 | URI 包含 URL 和 URN。 6 | 7 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/8441b2c4-dca7-4d6b-8efb-f22efccaf331.png) 8 | 9 | ## 请求和响应报文 ## 10 | 11 | ### 1. 请求报文 ### 12 | 13 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/HTTP_RequestMessageExample.png) 14 | 15 | ### 2. 响应报文 ### 16 | 17 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/HTTP_ResponseMessageExample.png) 18 | 19 | # 二、HTTP 方法 # 20 | 21 | 客户端发送的 **请求报文** 第一行为**请求行**,包含了方法字段。 22 | 23 | ## GET ## 24 | 25 | 获取资源 26 | 27 | 当前网络请求中,绝大部分使用的是 GET 方法。 28 | 29 | ## HEAD ## 30 | 31 | 获取报文首部 32 | 33 | 和 GET 方法类似,但是不返回报文实体主体部分。 34 | 35 | 主要用于确认 URL 的有效性以及资源更新的日期时间等。 36 | 37 | ## POST ## 38 | 39 | 传输实体主体 40 | 41 | POST 主要用来传输数据,而 GET 主要用来获取资源。 42 | 43 | ## PUT ## 44 | 45 | 上传文件 46 | 47 | 由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。 48 | 49 | PUT /new.html HTTP/1.1 50 | Host: example.com 51 | Content-type: text/html 52 | Content-length: 16 53 | 54 |

New File

55 | 56 | ## DELETE ## 57 | 58 | 删除文件 59 | 60 | 与 PUT 功能相反,并且同样不带验证机制。 61 | 62 | DELETE /file.html HTTP/1.1 63 | 64 | 65 | # 三、HTTP 状态码 # 66 | 67 | 服务器返回的 **响应报文** 中第一行为**状态行**,包含了状态码以及原因短语,用来告知客户端请求的结果。 68 | 69 | |状态码|类别|含义| 70 | |-|-|-| 71 | |1XX|Informational(信息性状态码)|接收的请求正在处理| 72 | |2XX|Success(成功状态码)|请求正常处理完毕| 73 | |3XX|Redirection(重定向状态码)|需要进行附加操作以完成请求| 74 | |4XX|Client Error(客户端错误状态码)|服务器无法处理请求| 75 | |5XX|Server Error(服务器错误状态码)|服务器处理请求出错| 76 | 77 | ### 1XX 信息 ### 78 | 79 | - 100 Continue :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。 80 | 81 | ### 2XX 成功 ### 82 | 83 | - 200 OK 84 | - 204 No Content :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 85 | - 206 Partial Content :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。 86 | 87 | ### 3XX 重定向 ### 88 | 89 | - 301 Moved Permanently :永久性重定向 90 | - 302 Found :临时性重定向 91 | - 303 See Other :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。 92 | - 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。 93 | - 304 Not Modified :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。 94 | - 307 Temporary Redirect :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。 95 | 96 | ### 4XX 客户端错误 ### 97 | 98 | - 400 Bad Request :请求报文中存在语法错误。 99 | - 401 Unauthorized :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。 100 | - 403 Forbidden :请求被拒绝。 101 | - 404 Not Found 102 | 103 | ### 5XX 服务器错误 ### 104 | 105 | - 500 Internal Server Error :服务器正在执行请求时发生错误。 106 | - 503 Service Unavailable :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 107 | 108 | # 四、HTTP 首部 # 109 | 110 | 有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。 111 | 112 | ## 通用首部字段 ## 113 | 114 | 首部字段名| 说明 115 | -|- 116 | Cache-Control| 控制缓存的行为 117 | Connection| 控制不再转发给代理的首部字段、管理持久连接 118 | Date| 创建报文的日期时间 119 | Pragma| 报文指令 120 | Trailer| 报文末端的首部一览 121 | Transfer-Encoding| 指定报文主体的传输编码方式 122 | Upgrade| 升级为其他协议 123 | Via| 代理服务器的相关信息 124 | Warning| 错误通知 125 | 126 | ## 请求首部字段 ## 127 | 128 | 首部字段名| 说明 129 | -|- 130 | Accept| 用户代理可处理的媒体类型 131 | Accept-Charset| 优先的字符集 132 | Accept-Encoding| 优先的内容编码 133 | Accept-Language| 优先的语言(自然语言) 134 | Authorization| Web 认证信息 135 | Expect| 期待服务器的特定行为 136 | From| 用户的电子邮箱地址 137 | Host| 请求资源所在服务器 138 | If-Match| 比较实体标记(ETag) 139 | If-Modified-Since| 比较资源的更新时间 140 | If-None-Match| 比较实体标记(与 If-Match 相反) 141 | If-Range| 资源未更新时发送实体 Byte 的范围请求 142 | If-Unmodified-Since| 比较资源的更新时间(与 If-Modified-Since 相反) 143 | Max-Forwards| 最大传输逐跳数 144 | Proxy-Authorization| 代理服务器要求客户端的认证信息 145 | Range| 实体的字节范围请求 146 | Referer| 对请求中 URI 的原始获取方 147 | TE| 传输编码的优先级 148 | User-Agent| HTTP 客户端程序的信息 149 | 150 | ## 响应首部字段 ## 151 | 152 | 首部字段名| 说明 153 | -|- 154 | Accept-Ranges| 是否接受字节范围请求 155 | Age| 推算资源创建经过时间 156 | ETag| 资源的匹配信息 157 | Location| 令客户端重定向至指定 URI 158 | Proxy-Authenticate| 代理服务器对客户端的认证信息 159 | Retry-After| 对再次发起请求的时机要求 160 | Server| HTTP 服务器的安装信息 161 | Vary| 代理服务器缓存的管理信息 162 | WWW-Authenticate| 服务器对客户端的认证信息 163 | 164 | ## 实体首部字段 ## 165 | 166 | 首部字段名| 说明 167 | -|- 168 | Allow| 资源可支持的 HTTP 方法 169 | Content-Encoding| 实体主体适用的编码方式 170 | Content-Language| 实体主体的自然语言 171 | Content-Length| 实体主体的大小 172 | Content-Location| 替代对应资源的 URI 173 | Content-MD5| 实体主体的报文摘要 174 | Content-Range| 实体主体的位置范围 175 | Content-Type| 实体主体的媒体类型 176 | Expires| 实体主体过期的日期时间 177 | Last-Modified| 资源的最后修改日期时间 178 | 179 | # 五、具体应用 # 180 | 181 | ## 连接管理 ## 182 | 183 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/HTTP1_x_Connections.png) 184 | 185 | ### 1. 短连接与长连接 ### 186 | 187 | 当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问的 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。 188 | 189 | 长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。 190 | 191 | - 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 Connection : close; 192 | - 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 Connection : Keep-Alive。 193 | 194 | ### 2. 流水线 ### 195 | 196 | 默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。 197 | 198 | 流水线是在同一条长连接上连续发出请求,而不用等待响应返回,这样可以减少延迟。 199 | 200 | ## Cookie ## 201 | 202 | HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 203 | 204 | Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 205 | 206 | Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。 207 | 208 | ### 1. 用途 ### 209 | 210 | - 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) 211 | - 个性化设置(如用户自定义设置、主题等) 212 | - 浏览器行为跟踪(如跟踪分析用户行为等) 213 | 214 | ### 2. 创建过程 ### 215 | 216 | 服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。 217 | 218 | HTTP/1.0 200 OK 219 | Content-type: text/html 220 | Set-Cookie: yummy_cookie=choco 221 | Set-Cookie: tasty_cookie=strawberry 222 | 223 | [page content] 224 | 225 | 客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。 226 | 227 | GET /sample_page.html HTTP/1.1 228 | Host: www.example.org 229 | Cookie: yummy_cookie=choco; tasty_cookie=strawberry 230 | 231 | ### 3. 分类 ### 232 | 233 | - 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。 234 | - 持久性 Cookie:指定过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie。 235 | 236 | ### 4. 作用域 ### 237 | 238 | Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。 239 | 240 | Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配: 241 | 242 | - /docs 243 | - /docs/Web/ 244 | - /docs/Web/HTTP 245 | 246 | ### 5. JavaScript ### 247 | 248 | 浏览器通过 document.cookie 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。 249 | 250 | document.cookie = "yummy_cookie=choco"; 251 | document.cookie = "tasty_cookie=strawberry"; 252 | console.log(document.cookie); 253 | 254 | ### 6. HttpOnly ### 255 | 256 | 标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。跨站脚本攻击 (XSS) 常常使用 JavaScript 的 document.cookie API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。 257 | 258 | Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly 259 | 260 | ### 7. Secure ### 261 | 262 | 标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。 263 | 264 | ### 8. Session ### 265 | 266 | 除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 267 | 268 | Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 269 | 270 | 使用 Session 维护用户登录状态的过程如下: 271 | 272 | - 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; 273 | - 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; 274 | - 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; 275 | - 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 276 | 277 | 应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 278 | 279 | ### 9. Cookie 与 Session 选择 ### 280 | 281 | - Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选 Session; 282 | - Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; 283 | - 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 284 | 285 | ## 缓存 ## 286 | 287 | HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。 288 | 289 | ### 禁止进行缓存 ### 290 | 291 | no-store 指令规定不能对请求或响应的任何一部分进行缓存。 292 | 293 | Cache-Control: no-store 294 | 295 | ### 3.2 强制确认缓存 ### 296 | 297 | no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效时才能使用该缓存对客户端的请求进行响应。 298 | 299 | Cache-Control: no-cache 300 | 301 | ### 3.3 私有缓存和公共缓存 ### 302 | 303 | private 指令规定了将资源作为私有缓存,只能被单独用户使用,一般存储在用户浏览器中。 304 | 305 | Cache-Control: private 306 | 307 | public 指令规定了将资源作为公共缓存,可以被多个用户使用,一般存储在代理服务器中。 308 | 309 | Cache-Control: public 310 | 311 | ### 3.4 缓存过期机制 ### 312 | 313 | max-age 指令出现在请求报文,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。 314 | 315 | max-age 指令出现在响应报文,表示缓存资源在缓存服务器中保存的时间。 316 | 317 | Cache-Control: max-age=31536000 318 | 319 | # 六、HTTPS # 320 | 321 | HTTP 有以下安全性问题: 322 | 323 | - 使用明文进行通信,内容可能会被窃听; 324 | - 不验证通信方的身份,通信方的身份有可能遭遇伪装; 325 | - 无法证明报文的完整性,报文有可能遭篡改。 326 | 327 | HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。 328 | 329 | 通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 330 | 331 | ## SSL的三次握手 ## 332 | 333 | ### 第一次握手 ### 334 | 335 | 有客户端生成随机数,并且携带着 版本号 以及 加密的方式 336 | 由服务器判断是否是 可用的/支持的 加密方式 337 | if 版本号+加密方式 可用 那么可以 继续进行下一步操作 338 | 反之 本次 握手结束 339 | 340 | ### 第二次握手 ### 341 | 342 | 服务器会 生成第二个 随机数 并且 携带着CA证书 发送给客户端 343 | if 证书有效/可用 344 | 继续下一步操作 345 | 反之 本次握手结束 346 | 347 | ### 第三次握手 ### 348 | 349 | 客户端 生成随机密钥,并且使用CA证书中获得的公钥进行加密 再次发送给服务器。 350 | 351 | 同时发送前2次的信息摘要 由服务器再次验证。 352 | 353 | 服务器会接收到客户端的密钥 并且利用私钥将其解密。 354 | 最后双方都会生成一个对话秘钥。 355 | 356 | ## 加密 ## 357 | 358 | HTTPS 采用混合的加密机制: 359 | 360 | - 使用非对称密钥加密方式,传输对称密钥加密方式所需要的 Secret Key,从而保证安全性;服务端持有私钥,客户端持有公钥(从CA证书中取得),客户端生成一个随机密钥 k,并用这个公钥加密得到 k',客户端把 k' 发送给服务端,服务端收到 k' 后用自己的私钥解密得到 k。 361 | - 获取到 Secret Key 后,再使用对称密钥加密方式进行通信,从而保证效率。 362 | 363 | ## 认证 ## 364 | 365 | 通过使用 **证书** 来对通信方进行认证。 366 | 367 | 数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。 368 | 369 | 服务器产生的公钥就交给CA,CA用CA自己的私钥加密,即数字签名,加密生会生成证书,证书还是要交给服务端,放在服务端那边。当客户端访问服务端时,服务端就会把这个证书安装到客户端上。 370 | 371 | 客户端就会用CA提供的CA自己的公钥来解密这个证书,(当然这个CA是浏览器预装时嵌入的可信的CA,如果不是预装时嵌入的CA,此时就没有CA的公钥,就解不了,就会弹出告警。)解得开就说明这个证书是某个CA认证过了的,是可信的,解开后就会得到数据,而这个数据就是服务端的公钥,此时用这个公钥与服务端进行数据传输。 372 | 373 | ## 完整性保护 ## 374 | 375 | SSL 提供报文摘要功能来进行完整性保护。 376 | 377 | HTTP 也提供了 MD5 报文摘要功能,但不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生了篡改。 378 | 379 | HTTPS 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。 380 | 381 | ## HTTPS 的缺点 ## 382 | 383 | - 因为需要进行加密解密等过程,因此速度会更慢; 384 | - 需要支付证书授权的高额费用。 385 | 386 | # 七、HTTP/2.0 # 387 | 388 | ## HTTP/1.x 缺陷 ## 389 | 390 | HTTP/1.x 实现简单是以牺牲性能为代价的: 391 | 392 | - 客户端需要使用多个连接才能实现并发和缩短延迟; 393 | - 不会压缩请求和响应首部,从而导致不必要的网络流量; 394 | - 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。 395 | 396 | ## 二进制分帧层 ## 397 | 398 | HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。 399 | 400 | 在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。 401 | 402 | ## 服务端推送 ## 403 | 404 | HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。 405 | 406 | ## 首部压缩 ## 407 | 408 | HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。 409 | 410 | HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。 411 | 412 | 不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。 413 | 414 | # 八、HTTP/1.1 新特性 # 415 | 416 | - 默认是长连接 417 | - 支持流水线 418 | - 支持同时打开多个 TCP 连接 419 | - 支持虚拟主机 420 | - 新增状态码 100 421 | - 支持分块传输编码 422 | - 新增缓存处理指令 max-age 423 | 424 | # 九、GET 和 POST 比较 # 425 | 426 | ## 作用 ## 427 | 428 | GET 用于获取资源,而 POST 用于传输实体主体。 429 | 430 | ## 参数 ## 431 | 432 | GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。 433 | 434 | 因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 中文 会转换为 %E4%B8%AD%E6%96%87,而空格会转换为 %20。POST 参数支持标准字符集。 435 | 436 | GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1 437 | 438 | POST /test/demo_form.asp HTTP/1.1 439 | Host: w3schools.com 440 | name1=value1&name2=value2 441 | 442 | ## 安全 ## 443 | 444 | 安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 445 | 446 | GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。 447 | 448 | 安全的方法除了 GET 之外还有:HEAD、OPTIONS。 449 | 450 | 不安全的方法除了 POST 之外还有 PUT、DELETE。 451 | 452 | ## 幂等性 ## 453 | 454 | 幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。 455 | 456 | 所有的安全方法也都是幂等的。 457 | 458 | 在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。 459 | 460 | ## 可缓存 ## 461 | 462 | 如果要对响应进行缓存,需要满足以下条件: 463 | 464 | - 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。 465 | - 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。 466 | - 响应报文的 Cache-Control 首部字段没有指定不进行缓存。 467 | 468 | ## XMLHttpRequest ## 469 | 470 | > XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。 471 | 472 | - 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。 473 | - 而 GET 方法 Header 和 Data 会一起发送。 -------------------------------------------------------------------------------- /计算机网络/计算机网络.md: -------------------------------------------------------------------------------- 1 | # 计算机的网络体系结构 2 | 3 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/0fa6c237-a909-4e2a-a771-2c5485cd8ce0.png) 4 | 5 | ## 1.五层协议 6 | 7 | - **应用层** :为特定应用程序提供数据传输服务,例如 HTTP、DNS 等协议。数据单位为**报文**。 8 | 9 | - **传输层** :为进程提供通用数据传输服务。由于应用层协议很多,定义通用的传输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为**报文段** ;用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户**数据报**。TCP 主要提供完整性服务,UDP 主要提供及时性服务。 10 | 11 | - **网络层** :为主机提供数据传输服务。而传输层协议是为主机中的进程提供数据传输服务。网络层把传输层传递下来的报文段或者用户数据报封装成**分组**。 12 | 13 | - **数据链路层** :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成**帧**。 14 | 15 | - **物理层** :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。 16 | 17 | ## 2. OSI 18 | 19 | 其中表示层和会话层用途如下: 20 | 21 | - **表示层** :数据压缩、加密以及数据描述,这使得应用程序不必关心在各台主机中数据内部格式不同的问题。 22 | - **会话层** :建立及管理会话。 23 | 24 | 五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。 25 | 26 | ## 3. TCP/IP 27 | 28 | 它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。 29 | 30 | TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。 31 | 32 | ## 4. 数据在各层之间的传递过程 33 | 34 | 在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。 35 | 36 | 路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要传输层和应用层。 37 | 38 | # 应用层 # 39 | 40 | ## 域名系统-DNS ## 41 | 42 | DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。 43 | 44 | 域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。 45 | 46 | DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传从而保证可靠性。在两种情况下会使用 TCP 进行传输: 47 | 48 | - 如果返回的响应超过的 512 字节(UDP 最大只支持 512 字节的数据)。 49 | - 区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。 50 | 51 | ## 文件传送协议-FTP ## 52 | 53 | FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件: 54 | 55 | - 控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。 56 | - 数据连接:20端口用来传送一个文件数据。 57 | 58 | ## HTTP与FTP的异同 ## 59 | 60 | 1、同:应用层协议,基于TCP。 61 | 62 | 2、异: 63 | 64 | - HTTP是超文本传输协议,面向网页的;FTP是文本传输协议,面向文件的。 65 | - 端口号,HTTP:80,FTP:21,20 66 | - FTP控制信息带外传送,HTTP控制信息带内传送。 67 | 68 | ## 动态主机配置协议-DHCP ## 69 | 70 | DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要手动配置 IP 地址等信息。 71 | 72 | DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址。 73 | 74 | DHCP 工作过程如下: 75 | 76 | 1. 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67,源地址为 0.0.0.0:68,被放入 UDP 中,该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。 77 | 1. DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。 78 | 1. 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。 79 | 1. DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。 80 | 81 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/23219e4c-9fc0-4051-b33a-2bd95bf054ab.jpg) 82 | 83 | ## 远程登录协议 ## 84 | 85 | TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。 86 | 87 | ## 电子邮件协议 ## 88 | 89 | 邮件协议包含发送协议和读取协议,发送协议常用 SMTP,读取协议常用 POP3 和 IMAP。 90 | 91 | ## Web 页面请求过程 ## 92 | 93 | ### 1. DHCP 配置主机信息 ### 94 | 95 | - 假设主机最开始没有 IP 地址以及其它信息,那么就需要先使用 DHCP 来获取。 96 | 97 | - 主机生成一个 DHCP 请求报文,并将这个报文放入具有目的端口 67 和源端口 68 的 UDP 报文段中。 98 | 99 | - 该报文段则被放入在一个具有广播 IP 目的地址(255.255.255.255) 和源 IP 地址(0.0.0.0)的 IP 数据报中。 100 | 101 | - 该数据报则被放置在 MAC 帧中,该帧具有目的地址 FF:FF:FF:FF:FF:FF,将广播到与交换机连接的所有设备。 102 | 103 | - 连接在交换机的 DHCP 服务器收到广播帧之后,不断地向上分解得到 IP 数据报、UDP 报文段、DHCP 请求报文,之后生成 DHCP ACK 报文,该报文包含以下信息:IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码。该报文被放入 UDP 报文段中,UDP 报文段有被放入 IP 数据报中,最后放入 MAC 帧中。 104 | 105 | - 该帧的目的地址是请求主机的 MAC 地址,因为交换机具有自学习能力,之前主机发送了广播帧之后就记录了 MAC 地址到其转发接口的交换表项,因此现在交换机就可以直接知道应该向哪个接口发送该帧。 106 | 107 | - 主机收到该帧后,不断分解得到 DHCP 报文。之后就配置它的 IP 地址、子网掩码和 DNS 服务器的 IP 地址,并在其 IP 转发表中安装默认网关。 108 | 109 | ### 2. ARP 解析 MAC 地址 ### 110 | 111 | - 主机通过浏览器生成一个 TCP 套接字,套接字向 HTTP 服务器发送 HTTP 请求。为了生成该套接字,主机需要知道网站的域名对应的 IP 地址。 112 | 113 | - 浏览器先检查自身缓存中有没有被解析过的这个域名对应的ip地址,如果有,解析结束。同时域名被缓存的时间也可通过TTL属性来设置。 114 | 115 | - 如果浏览器缓存中没有(专业点叫还没命中),浏览器会检查操作系统缓存中有没有对应的已解析过的结果。而操作系统也有一个域名解析的过程。在windows中可通过c盘里一个叫hosts的文件来设置,如果你在这里指定了一个域名对应的ip地址,那浏览器会首先使用这个ip地址。 116 | 117 | 但是这种操作系统级别的域名解析规程也被很多黑客利用,通过修改你的hosts文件里的内容把特定的域名解析到他指定的ip地址上,造成所谓的域名劫持。所以在windows7中将hosts文件设置成了readonly,防止被恶意篡改。 118 | 119 | - 如果至此还没有命中域名,才会真正的DNS服务器来解析这个域名。 120 | 121 | - 主机生成一个 DNS 查询报文,该报文具有 53 号端口,因为 DNS 服务器的端口号是 53。 122 | 123 | - 该 DNS 查询报文被放入目的地址为 DNS 服务器 IP 地址的 IP 数据报中。 124 | 125 | - 该 IP 数据报被放入一个以太网帧中,该帧将发送到网关路由器。 126 | 127 | - DHCP 过程只知道网关路由器的 IP 地址,为了获取网关路由器的 MAC 地址,需要使用 ARP 协议。 128 | 129 | - 主机生成一个包含目的地址为网关路由器 IP 地址的 ARP 查询报文,将该 ARP 查询报文放入一个具有广播目的地址(FF:FF:FF:FF:FF:FF)的以太网帧中,并向交换机发送该以太网帧,交换机将该帧转发给所有的连接设备,包括网关路由器。 130 | 131 | - 网关路由器接收到该帧后,不断向上分解得到 ARP 报文,发现其中的 IP 地址与其接口的 IP 地址匹配,因此就发送一个 ARP 回答报文,包含了它的 MAC 地址,发回给主机。 132 | 133 | ### 3. DNS 解析域名 ### 134 | 135 | - 知道了网关路由器的 MAC 地址之后,就可以继续 DNS 的解析过程了。 136 | 137 | - 网关路由器接收到包含 DNS 查询报文的以太网帧后,抽取出 IP 数据报,并根据转发表决定该 IP 数据报应该转发的路由器。 138 | 139 | - 因为路由器具有内部网关协议(RIP、OSPF)和外部网关协议(BGP)这两种路由选择协议,因此路由表中已经配置了网关路由器到达 DNS 服务器的路由表项。 140 | 141 | - 到达 DNS 服务器之后,DNS 服务器抽取出 DNS 查询报文,并在 DNS 数据库中查找待解析的域名。 142 | 143 | - 找到 DNS 记录之后,发送 DNS 回答报文,将该回答报文放入 UDP 报文段中,然后放入 IP 数据报中,通过路由器反向转发回网关路由器,并经过以太网交换机到达主机。 144 | 145 | ### 4. HTTP 请求页面 ### 146 | 147 | - 有了 HTTP 服务器的 IP 地址之后,主机就能够生成 TCP 套接字,该套接字将用于向 Web 服务器发送 HTTP GET 报文。 148 | 149 | - 在生成 TCP 套接字之前,必须先与 HTTP 服务器进行三次握手来建立连接。生成一个具有目的端口 80 的 TCP SYN 报文段,并向 HTTP 服务器发送该报文段。 150 | 151 | - HTTP 服务器收到该报文段之后,生成 TCP SYN ACK 报文段,发回给主机。 152 | 153 | - 连接建立之后,浏览器生成 HTTP GET 报文,并交付给 HTTP 服务器。 154 | 155 | - HTTP 服务器从 TCP 套接字读取 HTTP GET 报文,生成一个 HTTP 响应报文,将 Web 页面内容放入报文主体中,发回给主机。 156 | 157 | - 浏览器收到 HTTP 响应报文后,抽取出 Web 页面内容,之后进行渲染,显示 Web 页面。 158 | 159 | 160 | # 传输层 # 161 | 162 | ## UDP 和 TCP 的特点 ## 163 | 164 | - 用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。应用场景:效率要求较高,准确性要求低的场景,比如网络视频、网络电话、广播通信这些。DNS、DHCP的传输层协议就是UDP。 165 | 166 | - 传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。应用场景:准确性要求高,效率要求低的场景,文件传输这些。HTTP、FTP传输层协议就是TCP。 167 | 168 | ## UDP 首部格式 169 | 170 | 首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。 171 | 172 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/d4c3a4a1-0846-46ec-9cc3-eaddfca71254.jpg) 173 | 174 | ## TCP 首部格式 ## 175 | 176 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/55dc4e84-573d-4c13-a765-52ed1dd251f9.png) 177 | 178 | - **序号** :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。 179 | - **确认号** :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。 180 | - **数据偏移** :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。 181 | - **确认 ACK** :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。 182 | - **同步 SYN** :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。 183 | - **终止 FIN** :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接。 184 | - **窗口** :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。 185 | 186 | ## TCP 的三次握手 ## 187 | 188 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e92d0ebc-7d46-413b-aec1-34a39602f787.png) 189 | 190 | 假设 A 为客户端,B 为服务器端。 191 | 192 | - 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。 193 | - A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x,这个包又叫SYN包。 194 | - B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y,这个包又叫SYN-ACK包。 195 | - A 收到 B 的连接确认报文后,还要向 B 发出确认,ACK=1,确认号为 y+1,序号为 x+1,这个包又叫ACK包。 196 | - B 收到 A 的确认后,连接建立。 197 | 198 | ### 三次握手的原因 ### 199 | 200 | 第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。 201 | 202 | 客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。 203 | 204 | ## TCP 的四次挥手 ## 205 | 206 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f87afe72-c2df-4c12-ac03-9b8d581a8af8.jpg) 207 | 208 | 以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。 209 | 210 | - A 发送连接释放报文,FIN=1。 211 | - B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。 212 | - 当 B 不再需要连接时,发送连接释放报文,FIN=1。 213 | - A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。 214 | - B 收到 A 的确认后释放连接。 215 | 216 | ### 四次挥手的原因 ### 217 | 218 | CLOSE-WAIT 219 | 220 | 客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。 221 | 222 | TIME_WAIT 223 | 224 | 客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由: 225 | 226 | - 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。 227 | - 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。 228 | 229 | ## TCP 可靠传输 ## 230 | 231 | TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。 232 | 233 | 一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下: 234 | 235 | RTTs=(1-a)*(RTTs)+a*RTT 236 | 237 | 其中,0 ≤ a < 1,RTTs 随着 a 的增加更容易受到 RTT 的影响。 238 | 239 | 超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下: 240 | 241 | RTO = RTTs + 4*RRTd 242 | 243 | 其中 RTTd 为偏差的加权平均值。 244 | 245 | ## TCP 滑动窗口 ## 246 | 247 | 窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。 248 | 249 | 发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。 250 | 251 | 接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。 252 | 253 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/a3253deb-8d21-40a1-aae4-7d178e4aa319.jpg) 254 | 255 | ## TCP 流量控制 ## 256 | 257 | 流量控制是为了控制发送方发送速率,保证接收方来得及接收。 258 | 259 | 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。 260 | 261 | ## TCP 拥塞控制 ## 262 | 263 | 如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。 264 | 265 | TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。 266 | 267 | 发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。 268 | 269 | 为了便于讨论,做如下假设: 270 | 271 | - 接收方有足够大的接收缓存,因此不会发生流量控制; 272 | - 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。 273 | 274 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/910f613f-514f-4534-87dd-9b4699d59d31.png) 275 | 276 | ### 1. 慢开始与拥塞避免 ### 277 | 278 | 发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 ... 279 | 280 | 注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能性也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。 281 | 282 | 如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。 283 | 284 | ### 2. 快重传与快恢复 ### 285 | 286 | 在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。 287 | 288 | 在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。 289 | 290 | 在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。 291 | 292 | 慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。 293 | # 网络层 # 294 | 295 | ## IP 数据报格式 ## 296 | 297 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/85c05fb1-5546-4c50-9221-21f231cdc8c5.jpg) 298 | 299 | - **版本** : 有 4(IPv4)和 6(IPv6)两个值; 300 | - **首部长度** : 占 4 位,因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为固定部分长度为 20 字节,因此该值最小为 5。如果可选字段的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。 301 | - **区分服务** : 用来获得更好的服务,一般情况下不使用。 302 | - **总长度** : 包括首部长度和数据部分长度。 303 | - **生存时间** :TTL,它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位,当 TTL 为 0 时就丢弃数据报。 304 | - **协议** :指出携带的数据应该上交给哪个协议进行处理,例如 ICMP、TCP、UDP 等。 305 | - **首部检验和** :因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。 306 | - **标识** : 在数据报长度过长从而发生分片的情况下,相同数据报的不同分片具有相同的标识符。 307 | - **片偏移** : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。 308 | 309 | ## IP地址 ## 310 | 311 | ipv4,32位 312 | 313 | ## 地址解析协议 ARP ## 314 | 315 | 网络层实现主机之间的通信,而链路层实现具体每段链路之间的通信。因此在通信过程中,IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。 316 | 317 | ARP 实现由 IP 地址得到 MAC 地址。 318 | 319 | 每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到 MAC 地址的映射表。 320 | 321 | 如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。 322 | 323 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/8006a450-6c2f-498c-a928-c927f758b1d0.png) 324 | 325 | ## 网际控制报文协议 ICMP ## 326 | 327 | ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。 328 | 329 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e3124763-f75e-46c3-ba82-341e6c98d862.jpg) 330 | 331 | ICMP 报文分为差错报告报文和询问报文。 332 | 333 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/aa29cc88-7256-4399-8c7f-3cf4a6489559.png) 334 | 335 | ### Ping ### 336 | 337 | Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。 338 | 339 | Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。 340 | 341 | ## 网络地址转换 NAT ## 342 | 343 | 专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时,可以使用 NAT 来将本地 IP 转换为全球 IP。 344 | 345 | 在以前,NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把传输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。 346 | 347 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/2719067e-b299-4639-9065-bed6729dbf0b.png) 348 | 349 | ## 路由器 ## 350 | 351 | 路由器位于网络层 352 | 353 | 路由器从功能上可以划分为:路由选择和分组转发。 354 | 355 | 分组转发结构由三个部分组成:交换结构、一组输入端口和一组输出端口。 356 | 357 | # 链路层 # 358 | 359 | ## MAC 地址 ## 360 | 361 | MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标识网络适配器(网卡)。 362 | 363 | 一台主机拥有多少个网络适配器就有多少个 MAC 地址。例如笔记本电脑普遍存在无线网络适配器和有线网络适配器,因此就有两个 MAC 地址。 364 | 365 | 366 | ## 交换机 ## 367 | 368 | 交换机位于链路层。 369 | 370 | 交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。 371 | 372 | 正是由于这种自学习能力,因此交换机是一种即插即用设备,不需要网络管理员手动配置交换表内容。 373 | -------------------------------------------------------------------------------- /面向对象与设计模式/JDK中的设计模式.md: -------------------------------------------------------------------------------- 1 | # 单例 2 | 3 | java.lang.Runtime#getRuntime() 4 | 5 | # 静态工厂 # 6 | 7 | Class.forName 8 | 9 | # 工厂方法 # 10 | 11 | Collection.iterator方法 12 | 13 | ![](https://camo.githubusercontent.com/1ac6c5e11dcb240d2ddca8599a61b9cc9041ba3b/687474703a2f2f68692e6373646e2e6e65742f6174746163686d656e742f3230313130312f332f305f31323934303538373737393945682e676966) 14 | 15 | # 策略 16 | 17 | java.util.Comparator#compare() 18 | 19 | # 适配器模式 # 20 | 21 | (1)java.io.InputStreamReader(InputStream) 22 | 23 | (2)java.io.OutputStreamWriter(OutputStream) 24 | 25 | ![](https://camo.githubusercontent.com/3f1940c4ee9de66f0ce2b316448570d5ac6c9185/687474703a2f2f68692e6373646e2e6e65742f6174746163686d656e742f3230313130312f332f305f3132393430353839393963764a532e676966) 26 | 27 | # 装饰者模式 # 28 | 29 | java.util.Collections#synchronizedList(List) 30 | 31 | 32 | # 策略模式 33 | 34 | ThreadPoolExecutor中的四种拒绝策略 35 | 36 | ![](https://camo.githubusercontent.com/d001eaca7c88e9df33900a02fa95dc1324927e63/687474703a2f2f68692e6373646e2e6e65742f6174746163686d656e742f3230313130312f332f305f31323934303539363335575a31332e676966) 37 | 38 | -------------------------------------------------------------------------------- /面向对象与设计模式/创建型模式.md: -------------------------------------------------------------------------------- 1 | # 创建型模式 # 2 | 3 | 创建型模式的作用就是创建对象,说到创建一个对象,最熟悉的就是 new 一个对象,然后 set 相关属性。但是,在很多场景下,我们需要给客户端提供更加友好的创建对象的方式,尤其是那种我们定义了类,但是需要提供给其他开发者用的时候。 4 | 5 | ## 单例 6 | 7 | ### Intent 8 | 9 | 确保一个类只有一个实例,并提供该实例的全局访问点。 10 | 11 | ### Class Diagram 12 | 13 | 使用一个私有构造函数,一个私有静态变量,一个公有静态函数实现。 14 | 15 | 私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。 16 | 17 | ### 懒汉式 18 | 19 | #### 1.线程不安全 20 | 21 | 以下实现中,私有静态变量 uniqueInstance 被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。 22 | 23 | 这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (uniqueInstance == null) ,并且此时 uniqueInstance 为 null,那么会有多个线程执行 uniqueInstance = new Singleton(); 语句,这将导致实例化多次 uniqueInstance。 24 | 25 | public class Singleton { 26 | 27 | private static Singleton uniqueInstance; 28 | 29 | private Singleton() { 30 | } 31 | 32 | public static Singleton getUniqueInstance() { 33 | if (uniqueInstance == null) { 34 | uniqueInstance = new Singleton(); 35 | } 36 | return uniqueInstance; 37 | } 38 | } 39 | 40 | #### 2.双重校验锁 41 | 42 | uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。 43 | 44 | 双重校验锁先判断 uniqueInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。 45 | 46 | public class Singleton { 47 | 48 | private volatile static Singleton uniqueInstance; 49 | 50 | private Singleton() { 51 | } 52 | 53 | public static Singleton getUniqueInstance() { 54 | if (uniqueInstance == null) { 55 | synchronized (Singleton.class) { 56 | if (uniqueInstance == null) { 57 | uniqueInstance = new Singleton(); 58 | } 59 | } 60 | } 61 | return uniqueInstance; 62 | } 63 | } 64 | 65 | 因此必须使用双重校验锁,也就是需要使用两个 if 语句:第一个 if 语句用来避免 uniqueInstance 已经被实例化之后的加锁操作,而第二个 if 语句进行了加锁,所以只能有一个线程进入,就不会出现 uniqueInstance == null 时两个线程同时进行实例化操作。 66 | 67 | uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行 68 | 69 | 使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 70 | 71 | ### 饿汉式-线程安全 72 | 73 | 线程不安全问题主要是由于 uniqueInstance 被实例化多次,采取直接实例化 uniqueInstance 的方式就不会产生线程不安全问题。 74 | 75 | 但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。 76 | 77 | private static Singleton uniqueInstance = new Singleton(); 78 | 79 | ### JDK 80 | 81 | java.lang.Runtime#getRuntime() 82 | 83 | 84 | ## 工厂 85 | 86 | 使用工厂模式好处:可以降低耦合,即想要修改某个具体的实现类,只需要修改工厂,对客户端(调用方)而言是完全不用修改的。 87 | 88 | ## 简单工厂 89 | 90 | ### Intent 91 | 92 | 在创建一个对象时不向客户暴露内部细节,并提供一个创建对象的通用接口。 93 | 94 | ### Class Diagram 95 | 96 | 简单工厂把实例化的操作单独放到一个类中,这个类就成为简单工厂类,让简单工厂类来决定应该用哪个具体子类来实例化。 97 | 98 | 这样做能把客户类和具体子类的实现解耦,客户类不再需要知道有哪些子类以及应当实例化哪个子类。客户类往往有多个,如果不使用简单工厂,那么所有的客户类都要知道所有子类的细节。而且一旦子类发生改变,例如增加子类,那么所有的客户类都要进行修改。 99 | 100 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/40c0c17e-bba6-4493-9857-147c0044a018.png) 101 | 102 | ### Implementation 103 | 104 | public interface Product { 105 | } 106 | 107 | ---------- 108 | 109 | public class ConcreteProduct implements Product { 110 | } 111 | 112 | ---------- 113 | 114 | public class ConcreteProduct1 implements Product { 115 | } 116 | 117 | ---------- 118 | 119 | public class ConcreteProduct2 implements Product { 120 | } 121 | 122 | 以下的 SimpleFactory 是简单工厂实现,它被所有需要进行实例化的客户类调用。 123 | 124 | public class SimpleFactory { 125 | 126 | public Product createProduct(int type) { 127 | if (type == 1) { 128 | return new ConcreteProduct1(); 129 | } else if (type == 2) { 130 | return new ConcreteProduct2(); 131 | } 132 | return new ConcreteProduct(); 133 | } 134 | } 135 | 136 | 137 | ---------- 138 | 139 | public class Client { 140 | 141 | public static void main(String[] args) { 142 | SimpleFactory simpleFactory = new SimpleFactory(); 143 | Product product = simpleFactory.createProduct(1); 144 | // do something with the product 145 | } 146 | } 147 | 148 | ### JDK 149 | 150 | Class.forName 151 | 152 | ## 工厂方法 ## 153 | 154 | ### Intent 155 | 156 | 定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法把实例化操作推迟到子类。 157 | 158 | ### Class Diagram 159 | 160 | 在简单工厂中,创建对象的是另一个类,而在工厂方法中,是由子类来创建对象。 161 | 162 | Factory 有一个 doSomething() 方法,这个方法需要用到一个产品对象,这个产品对象由 factoryMethod() 方法创建。该方法是抽象的,需要由子类去实现。 163 | 164 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f4d0afd0-8e78-4914-9e60-4366eaf065b5.png) 165 | 166 | ### Implementation 167 | 168 | public abstract class Factory { 169 | abstract public Product factoryMethod(); 170 | public void doSomething() { 171 | Product product = factoryMethod(); 172 | // do something with the product 173 | } 174 | } 175 | 176 | 177 | ---------- 178 | 179 | public class ConcreteFactory extends Factory { 180 | public Product factoryMethod() { 181 | return new ConcreteProduct(); 182 | } 183 | } 184 | 185 | ---------- 186 | 187 | public class ConcreteFactory1 extends Factory { 188 | public Product factoryMethod() { 189 | return new ConcreteProduct1(); 190 | } 191 | } 192 | 193 | 194 | ---------- 195 | public class ConcreteFactory2 extends Factory { 196 | public Product factoryMethod() { 197 | return new ConcreteProduct2(); 198 | } 199 | } 200 | 201 | ### JDK 202 | 203 | Collection.iterator 204 | 205 | ## 抽象工厂 206 | 207 | ### Intent 208 | 209 | 提供一个接口,用于创建 相关的对象家族 。 210 | 211 | ### Class Diagram 212 | 213 | 抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂方法模式只是用于创建一个对象,这和抽象工厂模式有很大不同。 214 | 215 | 抽象工厂模式用到了工厂方法模式来创建单一对象,AbstractFactory 中的 createProductA() 和 createProductB() 方法都是让子类来实现,这两个方法单独来看就是在创建一个对象,这符合工厂方法模式的定义。 216 | 217 | 至于创建对象的家族这一概念是在 Client 体现,Client 要通过 AbstractFactory 同时调用两个方法来创建出两个对象,在这里这两个对象就有很大的相关性,Client 需要同时创建出这两个对象。 218 | 219 | 从高层次来看,抽象工厂使用了组合,即 Cilent 组合了 AbstractFactory,而工厂方法模式使用了继承。 220 | 221 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e2190c36-8b27-4690-bde5-9911020a1294.png) 222 | 223 | ### Implementation 224 | 225 | public class AbstractProductA { 226 | } 227 | 228 | 229 | ---------- 230 | 231 | public class AbstractProductB { 232 | } 233 | 234 | 235 | ---------- 236 | 237 | public class ProductA1 extends AbstractProductA { 238 | } 239 | 240 | 241 | ---------- 242 | 243 | public class ProductA2 extends AbstractProductA { 244 | } 245 | 246 | 247 | ---------- 248 | 249 | public class ProductB1 extends AbstractProductB { 250 | } 251 | 252 | 253 | ---------- 254 | public class ProductB2 extends AbstractProductB { 255 | } 256 | 257 | ---------- 258 | public abstract class AbstractFactory { 259 | abstract AbstractProductA createProductA(); 260 | abstract AbstractProductB createProductB(); 261 | } 262 | 263 | ---------- 264 | 265 | public class ConcreteFactory1 extends AbstractFactory { 266 | AbstractProductA createProductA() { 267 | return new ProductA1(); 268 | } 269 | 270 | AbstractProductB createProductB() { 271 | return new ProductB1(); 272 | } 273 | } 274 | 275 | ---------- 276 | public class ConcreteFactory2 extends AbstractFactory { 277 | AbstractProductA createProductA() { 278 | return new ProductA2(); 279 | } 280 | 281 | AbstractProductB createProductB() { 282 | return new ProductB2(); 283 | } 284 | } 285 | 286 | ---------- 287 | 288 | public class Client { 289 | public static void main(String[] args) { 290 | AbstractFactory abstractFactory = new ConcreteFactory1(); 291 | AbstractProductA productA = abstractFactory.createProductA(); 292 | AbstractProductB productB = abstractFactory.createProductB(); 293 | // do something with productA and productB 294 | } 295 | } 296 | 297 | 298 | -------------------------------------------------------------------------------- /面向对象与设计模式/结构型模式.md: -------------------------------------------------------------------------------- 1 | # 结构型模式 # 2 | 3 | 结构型模式旨在通过改变代码结构来达到解耦的目的,使得我们的代码容易维护和扩展。 4 | 5 | ## 代理 6 | 7 | 代理类与委托类实现同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。具体方法由委托类实现,在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。 8 | 9 | Java中代理的实现一般分为三种:JDK静态代理、JDK动态代理以及CGLIB动态代理。 10 | 11 | **JDK静态代理**:创建一个接口,然后创建被代理的类实现该接口并且实现该接口中的抽象方法。之后再创建一个代理类,同时使其也实现这个接口。在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。 12 | 13 | 优势: 14 | 15 | 在不修改目标对象的前提下,可以通过代理类对被代理类功能扩展。 16 | 17 | 18 | 19 | **JDK动态代理**:JDK静态代理是通过直接编码创建的,而JDK动态代理是利用反射机制在运行时创建代理类及其对象。 20 | 21 | 动态代理相对于静态代理的优势: 22 | 23 | 就静态代理而言,在委托类特别多的应用场景,就要相应的添加许多的代理类,这显然增加了应用程序的复杂度,而使用动态代理就可以减少代理类的数量,相对降低了应用程序的复杂度。 24 | 25 | 假如你想干三件事(相当于三段代码),安排好以后如果你想调换顺序,换做以前,你必须去代码里进行改动,改动代码就意味着你要重新测试。而如果你用动态代理就不会,他把每件事看作一个方面,每个方面是“织入”的,改变顺序不影响整体。 26 | 27 | **CGLIB**:JDK代理要求被代理的类必须实现接口,有很强的局限性。而CGLIB动态代理则没有此类强制性要求。简单的说,CGLIB会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。 28 | 29 | ## 外观 30 | 31 | ### Intent 32 | 33 | 提供了一个统一的接口,用来访问子系统中的一群接口,从而让子系统更容易使用。 34 | 35 | ### Class Diagram 36 | 37 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f9978fa6-9f49-4a0f-8540-02d269ac448f.png) 38 | 39 | ### Implementation 40 | 41 | 观看电影需要操作很多电器,使用外观模式实现一键看电影功能。 42 | 43 | public class SubSystem { 44 | public void turnOnTV() { 45 | System.out.println("turnOnTV()"); 46 | } 47 | 48 | public void setCD(String cd) { 49 | System.out.println("setCD( " + cd + " )"); 50 | } 51 | 52 | public void startWatching(){ 53 | System.out.println("startWatching()"); 54 | } 55 | } 56 | 57 | ---------- 58 | public class Facade { 59 | private SubSystem subSystem = new SubSystem(); 60 | 61 | public void watchMovie() { 62 | subSystem.turnOnTV(); 63 | subSystem.setCD("a movie"); 64 | subSystem.startWatching(); 65 | } 66 | } 67 | 68 | ---------- 69 | public class Client { 70 | public static void main(String[] args) { 71 | Facade facade = new Facade(); 72 | facade.watchMovie(); 73 | } 74 | } 75 | 76 | ### 设计原则 77 | 78 | 最少知识原则:只和你的密友谈话。也就是说客户对象所需要交互的对象应当尽可能少。 79 | 80 | ## 适配器 ## 81 | 82 | ### Intent 83 | 84 | 把一个类接口转换成另一个用户需要的接口。 85 | 86 | ### Class Diagram 87 | 88 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ff5152fc-4ff3-44c4-95d6-1061002c364a.png) 89 | 90 | ### Implementation 91 | 92 | 鸭子(Duck)和火鸡(Turkey)拥有不同的叫声,Duck 的叫声调用 quack() 方法,而 Turkey 调用 gobble() 方法。 93 | 94 | 要求将 Turkey 的 gobble() 方法适配成 Duck 的 quack() 方法,从而让火鸡冒充鸭子! 95 | 96 | public interface Duck { 97 | void quack(); 98 | } 99 | 100 | ---------- 101 | public interface Turkey { 102 | void gobble(); 103 | } 104 | 105 | ---------- 106 | public class WildTurkey implements Turkey { 107 | @Override 108 | public void gobble() { 109 | System.out.println("gobble!"); 110 | } 111 | } 112 | 113 | ---------- 114 | public class TurkeyAdapter implements Duck { 115 | Turkey turkey; 116 | 117 | public TurkeyAdapter(Turkey turkey) { 118 | this.turkey = turkey; 119 | } 120 | 121 | @Override 122 | public void quack() { 123 | turkey.gobble(); 124 | } 125 | } 126 | 127 | ---------- 128 | public class Client { 129 | public static void main(String[] args) { 130 | Turkey turkey = new WildTurkey(); 131 | Duck duck = new TurkeyAdapter(turkey); 132 | duck.quack(); 133 | } 134 | } 135 | 136 | ### JDK 137 | 138 | (1)java.io.InputStreamReader(InputStream) 139 | 140 | (2)java.io.OutputStreamWriter(OutputStream) 141 | 142 | ## 桥接 143 | 144 | ### Intent 145 | 146 | 将抽象与实现分离开来,使它们可以独立变化。 147 | 148 | ### Class Diagram 149 | 150 | Abstraction:定义抽象类的接口 151 | Implementor:定义实现类接口 152 | 153 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/2a1f8b0f-1dd7-4409-b177-a381c58066ad.png) 154 | 155 | ### Implementation 156 | 157 | RemoteControl 表示遥控器,指代 Abstraction。 158 | 159 | TV 表示电视,指代 Implementor。 160 | 161 | 桥接模式将遥控器和电视分离开来,从而可以独立改变遥控器或者电视的实现 162 | 163 | public abstract class TV { 164 | public abstract void on(); 165 | 166 | public abstract void off(); 167 | 168 | public abstract void tuneChannel(); 169 | } 170 | 171 | ---------- 172 | public class Sony extends TV { 173 | @Override 174 | public void on() { 175 | System.out.println("Sony.on()"); 176 | } 177 | 178 | @Override 179 | public void off() { 180 | System.out.println("Sony.off()"); 181 | } 182 | 183 | @Override 184 | public void tuneChannel() { 185 | System.out.println("Sony.tuneChannel()"); 186 | } 187 | } 188 | 189 | ---------- 190 | public class RCA extends TV { 191 | @Override 192 | public void on() { 193 | System.out.println("RCA.on()"); 194 | } 195 | 196 | @Override 197 | public void off() { 198 | System.out.println("RCA.off()"); 199 | } 200 | 201 | @Override 202 | public void tuneChannel() { 203 | System.out.println("RCA.tuneChannel()"); 204 | } 205 | } 206 | 207 | ---------- 208 | public abstract class RemoteControl { 209 | protected TV tv; 210 | 211 | public RemoteControl(TV tv) { 212 | this.tv = tv; 213 | } 214 | 215 | public abstract void on(); 216 | 217 | public abstract void off(); 218 | 219 | public abstract void tuneChannel(); 220 | } 221 | 222 | ---------- 223 | public class ConcreteRemoteControl1 extends RemoteControl { 224 | public ConcreteRemoteControl1(TV tv) { 225 | super(tv); 226 | } 227 | 228 | @Override 229 | public void on() { 230 | System.out.println("ConcreteRemoteControl1.on()"); 231 | tv.on(); 232 | } 233 | 234 | @Override 235 | public void off() { 236 | System.out.println("ConcreteRemoteControl1.off()"); 237 | tv.off(); 238 | } 239 | 240 | @Override 241 | public void tuneChannel() { 242 | System.out.println("ConcreteRemoteControl1.tuneChannel()"); 243 | tv.tuneChannel(); 244 | } 245 | } 246 | 247 | ---------- 248 | public class ConcreteRemoteControl2 extends RemoteControl { 249 | public ConcreteRemoteControl2(TV tv) { 250 | super(tv); 251 | } 252 | 253 | @Override 254 | public void on() { 255 | System.out.println("ConcreteRemoteControl2.on()"); 256 | tv.on(); 257 | } 258 | 259 | @Override 260 | public void off() { 261 | System.out.println("ConcreteRemoteControl2.off()"); 262 | tv.off(); 263 | } 264 | 265 | @Override 266 | public void tuneChannel() { 267 | System.out.println("ConcreteRemoteControl2.tuneChannel()"); 268 | tv.tuneChannel(); 269 | } 270 | } 271 | 272 | ---------- 273 | public class Client { 274 | public static void main(String[] args) { 275 | RemoteControl remoteControl1 = new ConcreteRemoteControl1(new RCA()); 276 | remoteControl1.on(); 277 | remoteControl1.off(); 278 | remoteControl1.tuneChannel(); 279 | RemoteControl remoteControl2 = new ConcreteRemoteControl2(new Sony()); 280 | remoteControl2.on(); 281 | remoteControl2.off(); 282 | remoteControl2.tuneChannel(); 283 | } 284 | } 285 | 286 | ## 装饰 ## 287 | 288 | ### Intent 289 | 290 | 为对象动态添加功能。 291 | 292 | ### Class Diagram 293 | 294 | 装饰者(Decorator)和具体组件(ConcreteComponent)都继承自组件(Component),具体组件的方法实现不需要依赖于其它对象,而装饰者组合了一个组件,这样它可以装饰其它装饰者或者具体组件。所谓装饰,就是把这个装饰者套在被装饰者之上,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。可以看到,具体组件应当是装饰层次的最低层,因为只有具体组件的方法实现不需要依赖于其它对象。 295 | 296 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/6b833bc2-517a-4270-8a5e-0a5f6df8cd96.png) 297 | 298 | ### 设计原则 299 | 300 | 类应该对扩展开放,对修改关闭:也就是添加新功能时不需要修改代码。 301 | 302 | 不可能把所有的类设计成都满足这一原则,应当把该原则应用于最有可能发生改变的地方。 303 | 304 | 305 | ### JDK 306 | 307 | java.util.Collections#synchronizedList(List) -------------------------------------------------------------------------------- /面向对象与设计模式/行为型模式.md: -------------------------------------------------------------------------------- 1 | # 行为型模式 # 2 | 3 | 行为型模式关注的是各个类之间的相互作用,将职责划分清楚,使得我们的代码更加地清晰。 4 | 5 | ## 策略 ## 6 | 7 | ### Intent 8 | 9 | 定义一系列算法,封装每个算法,并使它们可以互换。 10 | 11 | 策略模式可以让算法独立于使用它的客户端。 12 | 13 | ### Class Diagram 14 | 15 | - Strategy 接口定义了一个算法族,它们都实现了 behavior() 方法。 16 | - Context 是使用到该算法族的类,其中的 doSomething() 方法会调用 behavior(),setStrategy(Strategy) 方法可以动态地改变 strategy 对象,也就是说能动态地改变 Context 所使用的算法。 17 | 18 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/cd1be8c2-755a-4a66-ad92-2e30f8f47922.png) 19 | 20 | ### Implementation 21 | 22 | 设计一个鸭子,它可以动态地改变叫声。这里的算法族是鸭子的叫声行为。 23 | 24 | public interface QuackBehavior { 25 | void quack(); 26 | } 27 | 28 | ---------- 29 | public class Quack implements QuackBehavior { 30 | @Override 31 | public void quack() { 32 | System.out.println("quack!"); 33 | } 34 | } 35 | 36 | ---------- 37 | public class Squeak implements QuackBehavior{ 38 | @Override 39 | public void quack() { 40 | System.out.println("squeak!"); 41 | } 42 | } 43 | 44 | ---------- 45 | public class Duck { 46 | 47 | private QuackBehavior quackBehavior; 48 | 49 | public void performQuack() { 50 | if (quackBehavior != null) { 51 | quackBehavior.quack(); 52 | } 53 | } 54 | 55 | public void setQuackBehavior(QuackBehavior quackBehavior) { 56 | this.quackBehavior = quackBehavior; 57 | } 58 | } 59 | 60 | ---------- 61 | public class Client { 62 | 63 | public static void main(String[] args) { 64 | Duck duck = new Duck(); 65 | duck.setQuackBehavior(new Squeak()); 66 | duck.performQuack(); 67 | duck.setQuackBehavior(new Quack()); 68 | duck.performQuack(); 69 | } 70 | } 71 | 72 | ---------- 73 | squeak! 74 | quack! 75 | 76 | ### JDK 77 | 78 | java.util.Comparator#compare() 79 | 80 | ## 观察者 81 | 82 | ### Intent 83 | 84 | 定义对象之间的一对多依赖,当一个对象状态改变时,它的所有依赖都会收到通知并且自动更新状态。 85 | 86 | 主题(Subject)是被观察的对象,而其所有依赖者(Observer)称为观察者。 87 | 88 | ### Class Diagram 89 | 90 | 主题(Subject)具有注册和移除观察者、并通知所有观察者的功能,主题是通过维护一张观察者列表来实现这些操作的。 91 | 92 | 观察者(Observer)的注册功能需要调用主题的 registerObserver() 方法。 93 | 94 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/a8c8f894-a712-447c-9906-5caef6a016e3.png) 95 | 96 | ### Implementation 97 | 98 | 天气数据布告板会在天气信息发生改变时更新其内容,布告板有多个,并且在将来会继续增加。 99 | 100 | public interface Subject { 101 | void registerObserver(Observer o); 102 | 103 | void removeObserver(Observer o); 104 | 105 | void notifyObserver(); 106 | } 107 | 108 | ---------- 109 | public class WeatherData implements Subject { 110 | private List observers; 111 | private float temperature; 112 | private float humidity; 113 | private float pressure; 114 | 115 | public WeatherData() { 116 | observers = new ArrayList<>(); 117 | } 118 | 119 | public void setMeasurements(float temperature, float humidity, float pressure) { 120 | this.temperature = temperature; 121 | this.humidity = humidity; 122 | this.pressure = pressure; 123 | notifyObserver(); 124 | } 125 | 126 | @Override 127 | public void registerObserver(Observer o) { 128 | observers.add(o); 129 | } 130 | 131 | @Override 132 | public void removeObserver(Observer o) { 133 | int i = observers.indexOf(o); 134 | if (i >= 0) { 135 | observers.remove(i); 136 | } 137 | } 138 | 139 | @Override 140 | public void notifyObserver() { 141 | for (Observer o : observers) { 142 | o.update(temperature, humidity, pressure); 143 | } 144 | } 145 | } 146 | 147 | ---------- 148 | public interface Observer { 149 | void update(float temp, float humidity, float pressure); 150 | } 151 | 152 | ---------- 153 | public class StatisticsDisplay implements Observer { 154 | 155 | public StatisticsDisplay(Subject weatherData) { 156 | weatherData.reisterObserver(this); 157 | } 158 | 159 | @Override 160 | public void update(float temp, float humidity, float pressure) { 161 | System.out.println("StatisticsDisplay.update: " + temp + " " + humidity + " " + pressure); 162 | } 163 | } 164 | 165 | ---------- 166 | public class CurrentConditionsDisplay implements Observer { 167 | 168 | public CurrentConditionsDisplay(Subject weatherData) { 169 | weatherData.registerObserver(this); 170 | } 171 | 172 | @Override 173 | public void update(float temp, float humidity, float pressure) { 174 | System.out.println("CurrentConditionsDisplay.update: " + temp + " " + humidity + " " + pressure); 175 | } 176 | } 177 | 178 | ---------- 179 | public class WeatherStation { 180 | public static void main(String[] args) { 181 | WeatherData weatherData = new WeatherData(); 182 | CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData); 183 | StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData); 184 | 185 | weatherData.setMeasurements(0, 0, 0); 186 | weatherData.setMeasurements(1, 1, 1); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /面向对象与设计模式/面向对象思想.md: -------------------------------------------------------------------------------- 1 | 具备某种功能的实体,称为对象。面向对象最小的程序单元是:类。面向对象思想是站在对象的角度思考问题,把多个功能合理放到不同对象里,强调的是具备某些功能的对象。 2 | 3 | 面向对象更加符合常规的思维方式,稳定性好,可重用性强,易于开发大型软件产品,有良好的可维护性。 4 | 5 | 在软件工程上,面向对象可以使工程更加模块化,实现更低的耦合和更高的内聚。 6 | 7 | # 三大特性 # 8 | 9 | ## 封装 ## 10 | 11 | 利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外的接口使其与外部发生联系。用户无需关心对象内部的细节,但可以通过对象对外提供的接口来访问该对象。 12 | 13 | ## 继承 14 | 15 | 继承实现了 IS-A 关系,是类与类之间的关系。 16 | 17 | ## 多态 ## 18 | 19 | 多态分为编译时多态和运行时多态: 20 | 21 | - 编译时多态主要指方法的重载 22 | - 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定 23 | 24 | 运行时多态有三个条件: 25 | 26 | - 继承 27 | - 覆盖(重写) 28 | - 向上转型 29 | 30 | # 类图 31 | 32 | ## 泛化关系 33 | 34 | 用来描述继承关系,在 Java 中使用 extends 关键字。 35 | 36 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/c0874e0a-dba3-467e-9c86-dd9313e0843e.jpg) 37 | 38 | ## 实现关系 ## 39 | 40 | 用来实现一个接口,在 Java 中使用 implements 关键字。 41 | 42 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/83d466bd-946b-4430-854a-cf7b0696d4c8.jpg) 43 | 44 | ## 聚合关系 ## 45 | 46 | 表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。 47 | 48 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/a0ce43b7-afa8-4397-a96e-5c12a070f2ae.jpg) 49 | 50 | ## 组合关系 51 | 52 | 和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。 53 | 54 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/6a88a398-c494-41f5-bb62-9f7fb811df7c.jpg) 55 | 56 | ## 关联关系 57 | 58 | 表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。 59 | 60 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/a3e4dc62-0da5-4d22-94f2-140078281812.jpg) 61 | 62 | ## 依赖关系 63 | 64 | 和关联关系不同的是,依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式: 65 | 66 | - A 类是 B 类方法的局部变量; 67 | - A 类是 B 类方法的参数; 68 | - A 类向 B 类发送消息,从而影响 B 类发生变化。 69 | 70 | ![](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/379444c9-f1d1-45cd-b7aa-b0c18427d388.jpg) 71 | 72 | # 设计原则 # 73 | 74 | # S.O.L.I.D # 75 | 76 | ## 1. 单一责任原则 ## 77 | 78 | 修改一个类的原因应该只有一个。 79 | 80 | 换句话说就是让一个类只负责一件事,当这个类需要做过多事情的时候,就需要分解这个类。 81 | 82 | 如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力。 83 | 84 | ## 2. 开放封闭原则 85 | 86 | 类应该对扩展开放,对修改关闭。 87 | 88 | 扩展就是添加新功能的意思,因此该原则要求在添加新功能时不需要修改代码。 89 | 90 | 符合开闭原则最典型的设计模式是装饰者模式,它可以动态地将责任附加到对象上,而不用去修改类的代码。 91 | 92 | ## 3.里氏替换原则 93 | 94 | 子类对象必须能够替换掉所有父类对象。 95 | 96 | 继承是一种 IS-A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。 97 | 98 | 如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度。 99 | 100 | ## 4.迪米特法则 ## 101 | 102 | 又叫最少知识原则。 103 | 104 | 一个类对于其他类知道的越少越好。 105 | 106 | 在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。 107 | 108 | ## 5.接口分离原则 109 | 110 | 不应该强迫客户依赖于它们不用的方法。 111 | 112 | 因此使用多个专门的接口比使用单一的总接口要好。 113 | 114 | ## 6.依赖倒置原则 115 | 116 | 高层模块不应该依赖于低层模块,二者都应该依赖于抽象; 117 | 118 | 抽象不应该依赖于细节,细节应该依赖于抽象。 119 | 120 | 高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于低层模块,那么低层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。 121 | 122 | 依赖于抽象意味着: 123 | 124 | - 任何变量都不应该持有一个指向具体类的指针或者引用; 125 | - 任何类都不应该从具体类派生; 126 | - 任何方法都不应该覆写它的任何基类中的已经实现的方法。 --------------------------------------------------------------------------------