diff --git a/README.md b/README.md index 0493e185..4c2d1c28 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ | Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ | | :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| -|网络[:cloud:](#网络-cloud) |操作系统[:computer:](#操作系统-computer)| 算法[:pencil2:](#数据结构与算法-pencil2)| 面向对象[:couple:](#面向对象-couple) |数据库[:floppy_disk:](#数据库-floppy_disk)| Java [:coffee:](#java-coffee)| 分布式[:sweat_drops:](#分布式-sweat_drops)| 工具[:hammer:](#工具-hammer)| 编码实践[:speak_no_evil:](#编码实践-speak_no_evil)| 后记[:memo:](#后记-memo) | +| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 面向对象[:couple:](#面向对象-couple) |数据库[:floppy_disk:](#数据库-floppy_disk)| Java [:coffee:](#java-coffee)| 分布式[:sweat_drops:](#分布式-sweat_drops)| 工具[:hammer:](#工具-hammer)| 编码实践[:speak_no_evil:](#编码实践-speak_no_evil)| 后记[:memo:](#后记-memo) |
:loudspeaker: 本仓库不参与商业行为,不向读者收取任何费用。 @@ -9,15 +9,20 @@ :loudspeaker: This repository is not engaging in business activities, and does not charge readers any fee.

-## 网络 :cloud: +## 算法 :pencil2: -> [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md) -整理自《计算机网络 第七版》,重点内容会在标题后面加 \*。 +> [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md) -> [HTTP](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/HTTP.md) +《剑指 Offer 第二版》的最优解,在牛客网在线编程中出现的题目都已 AC。 -整理自《图解 HTTP》 +> [Leetcode 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode%20题解.md) + +对题目做了一个分类,并对每种题型的解题思路做了总结。 + +> [算法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/算法.md) + +整理自《算法 第四版》 ## 操作系统 :computer: @@ -29,19 +34,17 @@ 整理自《鸟哥的 Linux 私房菜》 -## 数据结构与算法 :pencil2: -> [算法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/算法.md) +## 网络 :cloud: -整理自《算法 第四版》 +> [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md) -> [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md) +整理自《计算机网络 第七版》,重点内容会在标题后面加 \*。 -《剑指 Offer 第二版》的最优解,在牛客网在线编程中出现的题目都已 AC。 +> [HTTP](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/HTTP.md) -> [Leetcode 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode%20题解.md) +整理自《图解 HTTP》 -对题目做了一个分类,并对每种题型的解题思路做了总结。 ## 面向对象 :couple: diff --git a/notes/HTTP.md b/notes/HTTP.md index 7238542e..97f361c9 100644 --- a/notes/HTTP.md +++ b/notes/HTTP.md @@ -507,7 +507,7 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名, ### 3. 隧道 -使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。隧道本身不去解析 HTTP 请求。 +使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。 # 六、HTTPs @@ -810,9 +810,11 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404 ## HTTP/1.1 与 HTTP/2.0 的区别 +> [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn) + ### 1. 多路复用 -HTTP/2.0 使用多路复用技术,使用同一个 TCP 连接来处理多个请求。 +HTTP/2.0 使用多路复用技术,同一个 TCP 连接可以处理多个请求。 ### 2. 首部压缩 @@ -820,7 +822,7 @@ HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 ### 3. 服务端推送 -在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发给客户端。 +HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发给客户端。 ### 4. 二进制格式 @@ -848,3 +850,5 @@ HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。 - [XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) - [XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?](https://blog.josephscott.org/2009/08/27/xmlhttprequest-xhr-uses-multiple-packets-for-http-post/) - [Symmetric vs. Asymmetric Encryption – What are differences?](https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences) +- [Web 性能优化与 HTTP/2](https://www.kancloud.cn/digest/web-performance-http2) +- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn) diff --git a/notes/Java 基础.md b/notes/Java 基础.md index aa33ce32..ed94785c 100644 --- a/notes/Java 基础.md +++ b/notes/Java 基础.md @@ -65,7 +65,7 @@ private 方法隐式地被指定为 final,如果在子类中定义的方法和 **1. 静态变量** -静态变量在内存中只存在一份,只在类第一次实例化时初始化一次。 +静态变量在内存中只存在一份,只在类初始化时赋值一次。 - 静态变量:类所有的实例都共享静态变量,可以直接通过类名来访问它; - 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。 @@ -83,11 +83,23 @@ public class A { **3. 静态语句块** -静态语句块和静态变量一样在类第一次实例化时运行一次。 +静态语句块在类初始化时运行一次。 -**4. 初始化顺序** +**4. 静态内部类** -静态数据优先于其它数据的初始化,静态变量和静态语句块哪个先运行取决于它们在代码中的顺序。 +内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非 static 变量和方法。 + +**5. 静态导包** + +```source-java +import static com.xxx.ClassName.* +``` + +在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。 + +**6. 变量赋值顺序** + +静态变量的赋值和静态语句块的运行优先于实例变量的赋值和普通语句块的运行,静态变量的赋值和静态语句块的运行哪个先执行取决于它们在代码中的顺序。 ```java public static String staticField = "静态变量"; @@ -99,8 +111,6 @@ static { } ``` -实例变量和普通语句块的初始化在静态变量和静态语句块初始化结束之后。 - ```java public String field = "实例变量"; ``` @@ -111,7 +121,7 @@ public String field = "实例变量"; } ``` -最后才是构造函数中的数据进行初始化 +最后才运行构造函数 ```java public InitialOrderTest() { @@ -121,25 +131,13 @@ public InitialOrderTest() { 存在继承的情况下,初始化顺序为: -1. 父类(静态变量、静态语句块块) +1. 父类(静态变量、静态语句块) 2. 子类(静态变量、静态语句块) 3. 父类(实例变量、普通语句块) 4. 父类(构造函数) 5. 子类(实例变量、普通语句块) 6. 子类(构造函数) -**5. 静态内部类** - -内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非 static 变量和方法。 - -**6. 静态导包** - -```source-java -import static com.xxx.ClassName.* -``` - -在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。 - # 二、Object 通用方法 ## 概览 @@ -324,7 +322,7 @@ public class CloneExample { ```java CloneExample e1 = new CloneExample(); -CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object' +// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object' ``` 接下来覆盖 Object 的 clone() 得到以下实现: @@ -510,7 +508,7 @@ protected 用于修饰成员,表示在继承体系中成员对于子类可见 如果子类的方法覆盖了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里式替换原则。 -字段决不能是公有的,因为这么做的话就失去了对这个实例域修改行为的控制,客户端可以对其随意修改。可以使用共有的 getter 和 setter 方法来替换共有字段。 +字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。可以使用共有的 getter 和 setter 方法来替换共有字段。 ```java public class AccessExample { @@ -634,9 +632,9 @@ System.out.println(InterfaceExample.x); **3. 比较** -- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求子类和父类具有 IS-A 关系; +- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。 - 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。 -- 接口的字段只能是 static 和 final 类型的,而抽象类的域可以有多种访问权限。 +- 接口的字段只能是 static 和 final 类型的,而抽象类的字段可以有多种访问权限。 - 接口的方法只能是 public 的,而抽象类的方法可以由多种访问权限。 **4. 使用选择** @@ -652,7 +650,7 @@ System.out.println(InterfaceExample.x); - 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法; - 需要使用多重继承。 -在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次接口要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。 +在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。 > [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/)
[When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface) @@ -773,7 +771,7 @@ String s5 = "bbb"; System.out.println(s4 == s5); // true ``` -Java 虚拟机将堆划分成新生代、老年代和永久代(PermGen Space)。在 Java 7 之前,字符串常量池被放在永久代中,而在 Java 7,它被放在堆的其它位置。这是因为永久代的空间有限,如果大量使用字符串的场景下会导致 OutOfMemoryError 错误。 +Java 虚拟机将堆划分成新生代、老年代和永久代(PermGen Space)。在 Java 7 之前,字符串常量池被放在永久代中,而在 Java 7,它被放在堆的其它位置。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。 > [What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning)
[深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html) diff --git a/notes/Java 容器.md b/notes/Java 容器.md index 1cb19fe6..9893ee27 100644 --- a/notes/Java 容器.md +++ b/notes/Java 容器.md @@ -9,9 +9,9 @@ * [ArrayList](#arraylist) * [Vector](#vector) * [LinkedList](#linkedlist) + * [LinkedHashMap](#linkedhashmap) * [TreeMap](#treemap) * [HashMap](#hashmap) - * [LinkedHashMap](#linkedhashmap) * [ConcurrentHashMap - JDK 1.7](#concurrenthashmap---jdk-17) * [ConcurrentHashMap - JDK 1.8](#concurrenthashmap---jdk-18) * [参考资料](#参考资料) @@ -40,13 +40,13 @@ - Vector:和 ArrayList 类似,但它是线程安全的; -- LinkedList:基于双向循环链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双端队列。 +- LinkedList:基于双向循环链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 ### 3. Queue - LinkedList:可以用它来支持双向队列; -- PriorityQueue:基于堆结构实现,可以用它来实现优先级队列。 +- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。 ## Map @@ -111,7 +111,7 @@ List list = Arrays.asList(1,2,3); ## ArrayList -[ArraList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/ArrayList.java) +[ArrayList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/ArrayList.java) ### 1. 概览 @@ -224,6 +224,10 @@ private void writeObject(java.io.ObjectOutputStream s) [LinkedList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/LinkedList.java) +## LinkedHashMap + +[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) + ## TreeMap [TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java) @@ -302,7 +306,7 @@ map.put("K3", "V3"); - 插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。 - 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 后面。 -

+

查找需要分成两步进行: @@ -317,7 +321,7 @@ map.put("K3", "V3"); 因为从 JDK 1.8 开始引入了红黑树,因此扩容操作较为复杂,为了便于理解,以下内容使用 JDK 1.7 的内容。 -设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的数量级为 O(N/M)。 +设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。 为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。 @@ -397,11 +401,9 @@ void transfer(Entry[] newTable) { ### 5. 确定桶下标 -很多操作都需要先确定一个键值对所在的桶下标,需要分三步进行。 +很多操作都需要先确定一个键值对所在的桶下标,这个操作需要分三步进行。 -(一)hashCode() - -调用 Key 的 hashCode() 方法得到 hashCode。 +(一)调用 hashCode() ```java public final int hashCode() { @@ -420,7 +422,7 @@ static final int hash(Object key) { } ``` -(三)除留余数法 +(三)除留余数 令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质: @@ -429,7 +431,7 @@ x : 00010000 x-1 : 00001111 ``` -令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位及以上数: +令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数: ``` y : 10110010 @@ -449,7 +451,7 @@ y%x : 00000010 拉链法需要使用除留余数法来得到桶下标,也就是需要进行以下计算:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。 -以下操作在 Java 8 中没有,但是原理上相同。 +以下操作在 JDK 1.8 中没有,但是原理上相同。 ```java static int indexFor(int h, int length) { @@ -472,7 +474,9 @@ new capacity : 00100000 ### 7. 扩容-计算数组容量 -先考虑如何求一个数的补码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到: +HashMap 构造函数允许用户传入的容量不是 2 的幂次方,因为它可以自动地将传入的容量转换为 2 的幂次方。 + +先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到: ``` mask |= mask >> 1 11011000 @@ -480,9 +484,14 @@ mask |= mask >> 2 11111100 mask |= mask >> 4 11111111 ``` -如果最后令 mask+1,得到就是大于原始数字的最小的 2 次方。 +mask+1 是大于原始数字的最小的 2 幂次方。 -以下是 HashMap 中计算一个大小所需要的数组容量的代码: +``` +num 10010000 +mask+1 100000000 +``` + +以下是 HashMap 中计算数组容量的代码: ```java static final int tableSizeFor(int cap) { @@ -508,15 +517,11 @@ HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0 - 由于 Hashtable 是线程安全的也是 synchronized,所以在单线程环境下它比 HashMap 要慢。 - HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。 -## LinkedHashMap - -[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) - ## ConcurrentHashMap - JDK 1.7 [ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/1.7/ConcurrentHashMap.java) -ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶。 +ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶。 相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。 @@ -533,7 +538,7 @@ static final class HashEntry { } ``` -继承自 ReentrantLock,每个 Segment 维护着多个 HashEntry。 +Segment 继承自 ReentrantLock,每个 Segment 维护着多个 HashEntry。 ```java static final class Segment extends ReentrantLock implements Serializable { @@ -569,46 +574,17 @@ static final int DEFAULT_CONCURRENCY_LEVEL = 16; ### 2. HashEntry 的不可变性 -HashEntry 中的 key,hash,next 都声明为 final 型。这意味着,不能把节点添加到链接的中间和尾部,也不能在链接的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变。这个特性可以大大降低处理链表时的复杂性。 +HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程 “看” 到。在 ConcurrentHashMap 中,不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。 -同时,HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程 “看” 到。在 ConcurrentHashMap 中,不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。 +非结构性修改操作只是更改某个 HashEntry 的 value 域的值。由于对 Volatile 变量的写入操作将与随后对这个变量的读操作进行同步。当一个写线程修改了某个 HashEntry 的 value 域后,另一个读线程读这个值域,Java 内存模型能够保证读线程读取的一定是更新后的值。所以,写线程对链表的非结构性修改能够被后续不加锁的读线程 “看到”。 -```java -final V remove(Object key, int hash, Object value) { - if (!tryLock()) - scanAndLock(key, hash); - V oldValue = null; - try { - HashEntry[] tab = table; - int index = (tab.length - 1) & hash; - HashEntry e = entryAt(tab, index); - HashEntry pred = null; - while (e != null) { - K k; - HashEntry next = e.next; - if ((k = e.key) == key || - (e.hash == hash && key.equals(k))) { - V v = e.value; - if (value == null || value == v || value.equals(v)) { - if (pred == null) - setEntryAt(tab, index, next); - else - pred.setNext(next); - ++modCount; - --count; - oldValue = v; - } - break; - } - pred = e; - e = next; - } - } finally { - unlock(); - } - return oldValue; -} -``` +对 ConcurrentHashMap 做结构性修改,实质上是对某个桶指向的链表做结构性修改。如果能够确保:在读线程遍历一个链表期间,写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表。那么读 / 写线程之间就可以安全并发访问这个 ConcurrentHashMap。 + +结构性修改操作包括 put,remove,clear。下面我们分别分析这三个操作。 + +clear 操作只是把 ConcurrentHashMap 中所有的桶 “置空”,每个桶之前引用的链表依然存在,只是桶不再引用到这些链表(所有链表的结构并没有被修改)。正在遍历某个链表的读线程依然可以正常执行对该链表的遍历。 + +put 操作如果需要插入一个新节点到链表中时 , 会在链表头部插入这个新节点。此时,链表中的原有节点的链接并没有被修改。也就是说:插入新健 / 值对到链表中的操作不会影响读线程正常遍历这个链表。 在以下链表中删除 C 节点,C 节点之后的所有节点都原样保留,C 节点之前的所有节点都被克隆到新的链表中,并且顺序被反转。可以注意到,在执行 remove 操作时,原始链表并没有被修改,也就是说,读线程不会受到执行 remove 操作的并发写线程的干扰。 @@ -616,19 +592,10 @@ final V remove(Object key, int hash, Object value) {

-除了 remove 操作,其它操作也类似。可以得出一个结论:写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。 +综上,可以得出一个结论:写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。 ### 3. Volatile 变量 -```java -static final class HashEntry { - final int hash; - final K key; - volatile V value; - volatile HashEntry next; -} -``` - 由于内存可见性问题,未正确同步的情况下,写线程写入的值可能并不为后续的读线程可见。 下面以写线程 M 和读线程 N 来说明 ConcurrentHashMap 如何协调读 / 写线程间的内存可见性问题。 @@ -674,7 +641,7 @@ V get(Object key, int hash) { 在 ConcurrentHashMap 中,所有执行写操作的方法(put, remove, clear),在对链表做结构性修改之后,在退出写方法前都会去写这个 count 变量。所有未加锁的读操作(get, contains, containsKey)在读方法中,都会首先去读取这个 count 变量。 -根据 Java 内存模型,对 同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程 “看到”。 +根据 Java 内存模型,对同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程 “看到”。 这个特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 ,读线程才需要加锁后重读)。 diff --git a/notes/Java 并发.md b/notes/Java 并发.md index a1131c9c..edc9dc0f 100644 --- a/notes/Java 并发.md +++ b/notes/Java 并发.md @@ -58,7 +58,7 @@ # 一、线程状态转换 -

+

## 新建(New) @@ -192,7 +192,7 @@ Executor 管理多个异步任务的执行,而无需程序员显示地管理 主要有三种 Executor: -1. CachedTreadPool:一个任务创建一个线程; +1. CachedThreadPool:一个任务创建一个线程; 2. FixedThreadPool:所有任务只能使用固定大小的线程; 3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 @@ -709,7 +709,7 @@ java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J. 维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。 -

+

```java public class CountdownLatchExample { @@ -743,7 +743,7 @@ run..run..run..run..run..run..run..run..run..run..end 下图应该从下往上看才正确。 -

+

```java public class CyclicBarrierExample { @@ -778,7 +778,7 @@ before..before..before..before..before..before..before..before..before..before.. Semaphore 就是操作系统中的信号量,可以控制对互斥资源的访问线程数。 -

+

以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。 @@ -982,7 +982,7 @@ public class ForkJoinPool extends AbstractExecutorService ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。 -

+

# 九、线程不安全示例 @@ -1027,27 +1027,27 @@ public class ThreadUnsafeExample { # 十、Java 内存模型 -Java 内存模型视图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。 +Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。 ## 主内存与工作内存 处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。 -加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致。CPU 使用一致性协议来解决一致性问题。 +加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。 -

+

所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。 线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。 -

+

## 内存间交互操作 Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。 -

+

- read:把一个变量的值从主内存传输到工作内存中 - load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中 @@ -1070,11 +1070,11 @@ Java 内存模型保证了 read、load、use、assign、store、write、lock 和 下图演示了两个线程同时对 cnt 变量进行操作,load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入该变量的值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。 -

+

AtomicInteger 能保证多个线程修改的原子性。 -

+

使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现: @@ -1153,7 +1153,7 @@ public class AtomicSynchronizedExample { 可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。 -volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一般的对象),那么其它线程就能看见 final 字段的值。 +volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。 对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题。因为 volatile 并不能保证操作的原子性。 @@ -1181,7 +1181,7 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即 在一个线程内,在程序前面的操作先行发生于后面的操作。 -

+

### 2. 管程锁定规则 @@ -1189,7 +1189,7 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。 -

+

### 3. volatile 变量规则 @@ -1197,15 +1197,15 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。 -

+

### 4. 线程启动规则 > Thread Start Rule -Thread 对象的 start() 方法先行发生于此线程的每一个动作。 +Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。 -

+

### 5. 线程加入规则 @@ -1213,7 +1213,7 @@ Thread 对象的 start() 方法先行发生于此线程的每一个动作。 join() 方法返回先行发生于 Thread 对象的结束。 -

+

### 6. 线程中断规则 @@ -1276,7 +1276,7 @@ public V put(K key, V value) { } ``` -多线程环境下,应当尽量使对象称为不可变,来满足线程安全。 +多线程环境下,应当尽量使对象成为不可变,来满足线程安全。 ### 2. 绝对线程安全 @@ -1505,7 +1505,7 @@ public class ThreadLocalExample1 { 它所对应的底层结构图为: -

+

每个 Thread 都有一个 TreadLocal.ThreadLocalMap 对象,Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员。 @@ -1601,7 +1601,7 @@ public static String concatString(String s1, String s2, String s3) { 简单地介绍了对象的内存布局后,我们把话题返回到轻量级锁的执行过程上。在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 “01” 状态)虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝(官方把这份拷贝加上了一个 Displaced 前缀,即 Displaced Mark Word)。然后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位(Mark Word 的最后 2bit)将转变为 “00”,即表示此对象处于轻量级锁定状态。 -

+

如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是的话只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,所标志的状态变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 @@ -1619,20 +1619,19 @@ public static String concatString(String s1, String s2, String s3) { 当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行。偏向锁、轻量级锁的状态转换及对象 Mark Word 的关系如图 13-5 所示。 -

+

偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。 - # 十三、多线程开发良好的实践 - 给线程起个有意义的名字,这样可以方便找 Bug。 -- 缩小同步范围,例如 对于 synchronized,应该尽量使用同步块而不是同步方法。 +- 缩小同步范围,例如对于 synchronized,应该尽量使用同步块而不是同步方法。 -- 多用同步类少用 wait 和 notify。首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait 和 notify 很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。 +- 多用同步类少用 wait() 和 notify()。首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护,在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。 -- 多用并发集合少用同步集合。并发集合比同步集合的可扩展性更好,例如应该使用 ConcurrentHashMap 而不是 Hashttable。 +- 多用并发集合少用同步集合。并发集合比同步集合的可扩展性更好,例如应该使用 ConcurrentHashMap 而不是 Hashtable。 - 使用本地变量和不可变类来保证线程安全。 diff --git a/notes/Java 虚拟机.md b/notes/Java 虚拟机.md index 6fc0432f..5173f2d5 100644 --- a/notes/Java 虚拟机.md +++ b/notes/Java 虚拟机.md @@ -142,7 +142,7 @@ Java 对引用的概念进行了扩充,引入四种强度不同的引用类型 **(一)强引用** -只要强引用存在,垃圾回收器永远不会回收调掉被引用的对象。 +只要强引用存在,垃圾回收器永远不会回收掉被引用的对象。 使用 new 一个新对象的方式来创建强引用。 @@ -154,7 +154,7 @@ Object obj = new Object(); 用来描述一些还有用但是并非必需的对象。 -在系统将要发生内存溢出异常之前,将会对这些对象列进回收范围之中进行第二次回收。 +在系统将要发生内存溢出异常之前,会将这些对象列进回收范围之中进行第二次回收。 软引用主要用来实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源获取数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源获取这些数据。 @@ -224,7 +224,13 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 1. 标记和清除过程效率都不高; 2. 会产生大量碎片,内存碎片过多可能导致无法给大对象分配内存。 -### 2. 复制 +### 2. 标记 - 整理 + +

+ +让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 + +### 3. 复制

@@ -234,12 +240,6 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间。 -### 3. 标记 - 整理 - -

- -让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 - ### 4. 分代收集 现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 diff --git a/notes/Leetcode 题解.md b/notes/Leetcode 题解.md index f3f229d8..73f19098 100644 --- a/notes/Leetcode 题解.md +++ b/notes/Leetcode 题解.md @@ -190,7 +190,7 @@ You have 3 cookies and their sizes are big enough to gratify all of the children You need to output 2. ``` -题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 +题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 因为最小的孩子最容易得到满足,因此先满足最小孩子。给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。 @@ -638,7 +638,7 @@ public int findKthLargest(int[] nums, int k) { } ``` -**堆排序** :时间复杂度 O(OlogK),空间复杂度 O(K)。 +**堆排序** :时间复杂度 O(NlogK),空间复杂度 O(K)。 ```java public int findKthLargest(int[] nums, int k) { diff --git a/notes/Linux.md b/notes/Linux.md index dbeddf16..135467a7 100644 --- a/notes/Linux.md +++ b/notes/Linux.md @@ -50,8 +50,11 @@ * [九、进程管理](#九进程管理) * [查看进程](#查看进程) * [进程状态](#进程状态) - * [SIGCHILD](#sigchild) - * [孤儿进程和僵死进程](#孤儿进程和僵死进程) + * [SIGCHLD](#sigchld) + * [wait()](#wait) + * [waitpid()](#waitpid) + * [孤儿进程](#孤儿进程) + * [僵死进程](#僵死进程) * [十、I/O 复用](#十io-复用) * [概念理解](#概念理解) * [I/O 模型](#io-模型) @@ -1061,7 +1064,6 @@ daemon 2

- | 状态 | 说明 | | :---: | --- | | R | running or runnable (on run queue) | @@ -1070,26 +1072,56 @@ daemon 2 | Z | defunct/zombie, terminated but not reaped by its parent | | T | stopped, either by a job control signal or because it is being traced| -## SIGCHILD +## SIGCHLD 当一个子进程改变了它的状态时:停止运行,继续运行或者退出,有两件事会发生在父进程中: - 得到 SIGCHLD 信号; -- 阻塞的 waitpid(2)(或者 wait)调用会返回。 +- waitpid() 或者 wait() 调用会返回。

-## 孤儿进程和僵死进程 +其中子进程发送的 SIGCHLD 信号包含了子进程的信息,包含了进程 ID、进程状态、进程使用 CPU 的时间等。 -### 1. 孤儿进程 +在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息。父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。 + +## wait() + +```c +pid_t wait(int *status) +``` + +父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,之后 wait() 函数会销毁子进程并返回。 + +如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 - 1,同时 errno 被置为 ECHILD。 + +参数 status 用来保存被收集进程退出时的一些状态,如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为 NULL: + +```c +pid = wait(NULL); +``` + +## waitpid() + +```c +pid_t waitpid(pid_t pid,int *status,int options) +``` + +作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。 + +pid 参数指示一个子进程的 ID,表示只关心这个子进程的退出 SIGCHLD 信号。如果 pid=-1 时,那么贺 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。 + +options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。 + +## 孤儿进程 一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。 由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。 -### 2. 僵死进程 +## 僵死进程 -一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait 或 waitpid 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait 或 waitpid,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。 +一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。 僵死进程通过 ps 命令显示出来的状态为 Z。 @@ -1118,7 +1150,8 @@ I/O Multiplexing 又被称为 Event Driven I/O,它可以让单个进程具有 同步异步是获知 I/O 完成的方式,同步需要时刻关心 I/O 是否已经完成,异步无需主动关心,在 I/O 完成时它会收到通知。 -

+

+ ### 1. 同步-阻塞 @@ -1136,13 +1169,7 @@ I/O Multiplexing 又被称为 Event Driven I/O,它可以让单个进程具有

-### 3. 异步-阻塞 - -这是 I/O 复用使用的一种模式,通过使用 select,它可以监听多个 I/O 事件,当这些事件至少有一个发生时,用户程序会收到通知。 - -

- -### 4. 异步-非阻塞 +### 3. 异步 该模式下,I/O 操作会立即返回,之后可以处理其它操作,并且在 I/O 完成时会收到一个通知,此时会中断正在处理的操作,然后继续之前的操作。 @@ -1397,3 +1424,4 @@ poll 没有最大描述符数量的限制,如果平台支持应该采用 poll - [Linux 之守护进程、僵死进程与孤儿进程](http://liubigbin.github.io/2016/03/11/Linux-%E4%B9%8B%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B%E3%80%81%E5%83%B5%E6%AD%BB%E8%BF%9B%E7%A8%8B%E4%B8%8E%E5%AD%A4%E5%84%BF%E8%BF%9B%E7%A8%8B/) - [Linux process states](https://idea.popcount.org/2012-12-11-linux-process-states/) - [GUID Partition Table](https://en.wikipedia.org/wiki/GUID_Partition_Table) +- [详解 wait 和 waitpid 函数](https://blog.csdn.net/kevinhg/article/details/7001719) diff --git a/notes/MySQL.md b/notes/MySQL.md index 5cc1fd69..56335138 100644 --- a/notes/MySQL.md +++ b/notes/MySQL.md @@ -30,31 +30,27 @@ InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。 -采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读。 +采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ),并且通过间隙锁(next-key locking)策略防止幻读的出现。间隙锁使得 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。 表是基于聚簇索引建立的,它对主键的查询性能有很高的提升。 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够自动在内存中创建哈希索引以加速读操作的自适应哈希索引、能够加速插入操作的插入缓冲区等。 -通过一些机制和工具支持真正的热备份。 +通过一些机制和工具支持真正的热备份,其它存储引擎不支持热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 ## MyISAM -MyISAM 提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等。但 MyISAM 不支持事务和行级锁,而且崩溃后无法安全恢复。应该注意的是,MySQL5.6.4 添加了对 InnoDB 引擎的全文索引支持。 +提供了大量的特性,包括全文索引、压缩表、空间数据索引等。应该注意的是,MySQL 5.6.4 添加了对 InnoDB 引擎的全文索引支持。 -只能对整张表加锁,而不是针对行。 +不支持事务。 + +不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取查询的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。 -可以包含动态或者静态的行。 - 如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。 -如果表在创建并导入数据以后,不会再进行修改操作,那么这样的表适合采用 MyISAM 压缩表。 - -对于只读数据,或者表比较小、可以容忍修复操作,则依然可以继续使用 MyISAM。 - -MyISAM 设计简单,数据以紧密格式存储,所以在某些场景下性能很好。 +MyISAM 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以继续使用 MyISAM。 ## 比较 @@ -62,7 +58,7 @@ MyISAM 设计简单,数据以紧密格式存储,所以在某些场景下性 2. 备份:InnoDB 支持在线热备份。 3. 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 4. 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 -5. 其它特性:MyISAM 支持,地理空间索引。 +5. 其它特性:MyISAM 支持压缩表和空间数据索引。 # 二、数据类型 @@ -146,12 +142,14 @@ InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个 限制:哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;无法用于分组与排序;只支持精确查找,无法用于部分查找和范围查找;如果哈希冲突很多,查找速度会变得很慢。 -### 3. 空间索引(R-Tree) +### 3. 空间数据索引(R-Tree) MyISAM 存储引擎支持空间索引,可以用于地理数据存储。 空间索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 +必须使用 GIS 相关的函数来维护数据。 + ### 4. 全文索引 MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较索引中的值。 @@ -162,9 +160,9 @@ MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而 - 大大减少了服务器需要扫描的数据量; -- 帮助服务器避免进行排序和创建临时表; +- 帮助服务器避免进行排序和创建临时表(B+Tree 索引是有序的,可以用来做 ORDER BY 和 GROUP BY 操作); -- 将随机 I/O 变为顺序 I/O。 +- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,也就将相关的列值都存储在一起)。 ## 索引优化 @@ -216,14 +214,14 @@ customer_id_selectivity: 0.0373 聚簇索引并不是一种索引类型,而是一种数据存储方式。 -术语“聚簇”表示数据行和相邻的键值紧密地存储在一起,InnoDB 的聚簇索引的数据行存放在 B+Tree 的叶子页中。 +术语“聚簇”表示数据行和相邻的键值紧密地存储在一起,InnoDB 的聚簇索引在同一个结构中保存了 B+Tree 索引和数据行。 因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。 **优点** -1. 可以把相关数据保存在一起,减少 I/O 操作; -2. 因为数据保存在 B+Tree 中,因此数据访问更快。 +1. 可以把相关数据保存在一起,减少 I/O 操作。例如电子邮件表可以根据用户 ID 来聚集数据,这样只需要从磁盘读取少数的数据也就能获取某个用户的全部邮件,如果没有使用聚聚簇索引,则每封邮件都可能导致一次磁盘 I/O。 +2. 数据访问更快。 **缺点** @@ -249,15 +247,15 @@ customer_id_selectivity: 0.0373

-为了描述 B-Tree,首先定义一条数据记录为一个二元组 [key, data],key 为记录的键,data 为数据记录除 key 外的数据。 +为了描述 B-Tree,首先定义一条数据记录为一个二元组 [key, data]。 B-Tree 是满足下列条件的数据结构: - 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的; - 一个节点中的 key 从左到右非递减排列; -- 如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于 keyi 且小于 keyi+1。 +- 如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。 -在 B-Tree 中按 key 检索数据的算法非常直观:首先从根节点进行二分查找,如果找到则返回对应节点的 data,否则对相应区间的指针指向的节点递归进行查找,直到找到节点或找到 null 指针,前者查找成功,后者查找失败。 +在 B-Tree 中按 key 检索数据的算法非常直观:首先在根节点进行二分查找,如果找到则返回对应节点的 data,否则在相应区间的指针指向的节点递归进行查找。 由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。 @@ -282,7 +280,9 @@ B-Tree 是满足下列条件的数据结构: 页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页的大小通常为 4k),主存和磁盘以页为单位交换数据。 -一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。B-Tree 中一次检索最多需要 h-1 次 I/O(根节点常驻内存),渐进复杂度为 O(h)=O(logdN)。一般实际应用中,出度 d 是非常大的数字,通常超过 100,因此 h 非常小(通常不超过 3)。而红黑树这种结构,h 明显要深的多。并且于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,效率明显比 B-Tree 差很多。 +一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。 + +B-Tree 中一次检索最多需要 h-1 次 I/O(根节点常驻内存),渐进复杂度为 O(h)=O(logdN)。一般实际应用中,出度 d 是非常大的数字,通常超过 100,因此 h 非常小(通常不超过 3)。而红黑树这种结构,h 明显要深的多。并且于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,效率明显比 B-Tree 差很多。 B+Tree 更适合外存索引,原因和内节点出度 d 有关。由于 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,拥有更好的性能。 diff --git a/notes/Redis.md b/notes/Redis.md index 08b1f54f..d33d6f04 100644 --- a/notes/Redis.md +++ b/notes/Redis.md @@ -427,7 +427,7 @@ Redis 这种内存数据库能支持计数器频繁的读写操作。 |volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | | allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 | | allkeys-random | 从所有数据集中任意选择数据进行淘汰 | -| no-envicition | 禁止驱逐数据 | +| noeviction | 禁止驱逐数据 | 如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 diff --git a/notes/SQL.md b/notes/SQL.md index d582dca2..2eb67e58 100644 --- a/notes/SQL.md +++ b/notes/SQL.md @@ -196,7 +196,7 @@ WHERE col IS NULL; 应该注意到,NULL 与 0 、空字符串都不同。 -**AND OR** 用于连接多个过滤条件。优先处理 AND,因此当一个过滤表达式涉及到多个 AND 和 OR 时,应当使用 () 来决定优先级。 +**AND OR** 用于连接多个过滤条件,优先处理 AND,当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰。 **IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。 @@ -246,14 +246,14 @@ FROM mytable ## 文本处理 | 函数 | 说明 | -| ------------ | ------------ | +| :---: | :---: | | LEFT() RIGHT() | 左边或者右边的字符 | | LOWER() UPPER() | 转换为小写或者大写 | | LTRIM() RTIM() | 去除左边或者右边的空格 | | LENGTH() | 长度 | -| SUNDEX() | 转换为语音值 | +| SOUNDEX() | 转换为语音值 | -其中, **SOUNDEX()** 是将一个字符串转换为描述其语音表示的字母数字模式的算法,它是根据发音而不是字母比较。 +其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。 ```sql SELECT * @@ -267,7 +267,7 @@ WHERE SOUNDEX(col1) = SOUNDEX('apple') - 时间格式:HH:MM:SS |函 数 | 说 明| -| --- | --- | +| :---: | :---: | | AddDate() | 增加一个日期(天、周等)| | AddTime() | 增加一个时间(时、分等)| | CurDate() | 返回当前日期 | @@ -288,13 +288,16 @@ WHERE SOUNDEX(col1) = SOUNDEX('apple') ```sql mysql> SELECT NOW(); - -> '2017-06-28 14:01:52' +``` + +``` +2018-4-14 20:25:11 ``` ## 数值处理 | 函数 | 说明 | -| --- | --- | +| :---: | :---: | | SIN() | 正弦 | | COS() | 余弦 | | TAN() | 正切 | @@ -308,7 +311,7 @@ mysql> SELECT NOW(); ## 汇总 |函 数 |说 明| -| --- | --- | +| :---: | :---: | | AVG() | 返回某列的平均值 | | COUNT() | 返回某列的行数 | | MAX() | 返回某列的最大值 | @@ -330,7 +333,7 @@ FROM mytable 可以对同一分组数据使用汇总函数进行处理,例如求分组数据的平均值等。 -指定的分组字段除了能按该字段进行分组,也可以按该字段进行排序,例如按 col 字段排序并分组数据: +指定的分组字段除了能按该字段进行分组,也会自动按按该字段进行排序。 ```sql SELECT col, COUNT(*) AS num @@ -338,7 +341,7 @@ FROM mytable GROUP BY col; ``` -GROUP BY 是按照分组字段进行排序,ORDER BY 也可以以汇总字段来进行排序。 +GROUP BY 按分组字段进行排序,ORDER BY 也可以以汇总字段来进行排序。 ```sql SELECT col, COUNT(*) AS num @@ -347,14 +350,14 @@ GROUP BY col ORDER BY num; ``` -WHERE 过滤行,HAVING 过滤分组。行过滤应当先与分组过滤; +WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤。 ```sql SELECT col, COUNT(*) AS num FROM mytable WHERE col > 2 GROUP BY col -HAVING COUNT(*) >= 2; +HAVING num >= 2; ``` 分组规定: @@ -436,10 +439,10 @@ where department = ( 自连接版本 ```sql -select e2.name +select e1.name from employee as e1, employee as e2 where e1.department = e2.department - and e1.name = "Jim"; + and e2.name = "Jim"; ``` 连接一般比子查询的效率高。 @@ -464,7 +467,7 @@ from employee natural join department; ```sql select Customers.cust_id, Orders.order_num from Customers left outer join Orders -on Customers.cust_id = Orders.curt_id; +on Customers.cust_id = Orders.cust_id; ``` 如果需要统计顾客的订单数,使用聚集函数。 @@ -473,15 +476,15 @@ on Customers.cust_id = Orders.curt_id; select Customers.cust_id, COUNT(Orders.order_num) as num_ord from Customers left outer join Orders -on Customers.cust_id = Orders.curt_id +on Customers.cust_id = Orders.cust_id group by Customers.cust_id; ``` # 十六、组合查询 -使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果为 M+N 行。 +使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。 -每个查询必须包含相同的列、表达式或者聚集函数。 +每个查询必须包含相同的列、表达式和聚集函数。 默认会去除相同行,如果需要保留相同行,使用 UNION ALL。 @@ -622,7 +625,7 @@ MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储 不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。 -MySQL 的事务提交默认是隐式提交,也就是每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。 +MySQL 的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。 通过设置 autocommit 为 0 可以取消自动提交,直到 autocommit 被设置为 1 才会提交;autocommit 标记是针对每个连接而不是针对服务器的。 diff --git a/notes/剑指 offer 题解.md b/notes/剑指 offer 题解.md index 4a6c375c..4f6bd9ad 100644 --- a/notes/剑指 offer 题解.md +++ b/notes/剑指 offer 题解.md @@ -98,16 +98,18 @@ 以 (2, 3, 1, 0, 2, 5) 为例: -```html +```text-html-basic position-0 : (2,3,1,0,2,5) // 2 <-> 1 (1,3,2,0,2,5) // 1 <-> 3 - (3,1,1,0,2,5) // 3 <-> 0 - (0,1,1,3,2,5) // already in position -position-1 : (0,1,1,3,2,5) // already in position -position-2 : (0,1,1,3,2,5) // nums[i] == nums[nums[i]], exit + (3,1,2,0,2,5) // 3 <-> 0 + (0,1,2,3,2,5) // already in position +position-1 : (0,1,2,3,2,5) // already in position +position-2 : (0,1,2,3,2,5) // already in position +position-3 : (0,1,2,3,2,5) // already in position +position-4 : (0,1,2,3,2,5) // nums[i] == nums[nums[i]], exit ``` -遍历到位置 2 时,该位置上的数为 1,但是第 1 个位置上已经有一个 1 的值了,因此可以知道 1 重复。 +遍历到位置 4 时,该位置上的数为 2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复。 复杂度:O(N) + O(1) diff --git a/notes/数据库系统原理.md b/notes/数据库系统原理.md index 6ebc72a8..19a76d1a 100644 --- a/notes/数据库系统原理.md +++ b/notes/数据库系统原理.md @@ -94,7 +94,7 @@ T2 读取一个数据,T1 对该数据做了修改。如 T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 -

+

## 解决方法 @@ -110,11 +110,13 @@ T1 读取某个范围的数据,T2 在这个范围内插

+MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 + 应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。 -但是加锁需要消耗资源,锁的各种操作,包括获取锁,检查锁是否已经解除、释放锁,都会增加系统开销。因此封锁粒度越小,系统开销就越大。需要在锁开销以及数据安全性之间做一个权衡。 +但是加锁需要消耗资源,锁的各种操作,包括获取锁,检查锁是否已经解除、释放锁,都会增加系统开销。因此封锁粒度越小,系统开销就越大。 -MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 +在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。 ## 封锁类型 @@ -227,13 +229,13 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 事务遵循两段锁协议是保证并发操作可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。 ```html -lock-x(A)...lock-s(B)...lock-s(c)...unlock(A)...unlock(C)...unlock(B) +lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B) ``` 但不是必要条件,例如以下操作不满足两段锁协议,但是它还是可串行化调度。 ```html -lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(c)...unlock(C)... +lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C)... ``` # 四、隔离级别 @@ -289,33 +291,31 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回 ### 1. SELECT -该操作必须保证多个事务读取到同一个数据行的快照,这个快照是最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。 - 当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。 +多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。 + 把没对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。 除了上面的要求,T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。 ### 2. INSERT -将系统版本号作为数据行快照的创建版本号。 +将当前系统版本号作为数据行快照的创建版本号。 ### 3. DELETE -将系统版本号作为数据行快照的删除版本号。 +将当前系统版本号作为数据行快照的删除版本号。 ### 4. UPDATE -将系统版本号作为更新后的数据行快照的创建版本号,同时将系统版本号作为更新前的数据行快照的删除版本号。可以理解为先执行 DELETE 后执行 INSERT。 +将当前系统版本号作为更新后的数据行快照的创建版本号,同时将当前系统版本号作为更新前的数据行快照的删除版本号。可以理解为先执行 DELETE 后执行 INSERT。 ## 快照读与当前读 ### 1. 快照读 -读取快照中的数据。 - -引入快照读的目的主要是为了免去加锁操作带来的性能开销,但是当前读需要加锁。 +读取快照中的数据,可以减少加锁所带来的开销。 ```sql select * from table ....; @@ -323,21 +323,19 @@ select * from table ....; ### 2. 当前读 -读取最新的数据。 - -需要加锁,以下第一个语句加 S 锁,其它都加 X 锁。 +读取最新的数据,需要加锁。以下第一个语句需要加 S 锁,其它都需要加 X 锁。 ```sql select * from table where ? lock in share mode; select * from table where ? for update; insert; -update ; +update; delete; ``` # 六、Next-Key Locks -Next-Key Locks 也是 MySQL 的 InnoDB 存储引擎的一种锁实现。MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读隔离级别下,MVCC + Next-Key Locks,就可以防止幻读的出现。 +Next-Key Locks 也是 MySQL 的 InnoDB 存储引擎的一种锁实现。MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。 ## Record Locks @@ -383,7 +381,7 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; 记 A->B 表示 A 函数决定 B,也可以说 B 函数依赖于 A。 -如果 {A1,A2,... ,An} 是关系的一个或多个属性的集合,该集合决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。 +如果 {A1,A2,... ,An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。 对于 W->A,如果能找到 W 的真子集 W',使得 W'-> A,那么 W->A 就是部分函数依赖,否则就是完全函数依赖; @@ -399,9 +397,9 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; 不符合范式的关系,会产生很多异常,主要有以下四种异常: -1. 冗余数据,例如学生-2 出现了两次。 -2. 修改异常,修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。 -3. 删除异常,删除一个信息,那么也会丢失其它信息。例如如果删除了课程-1,需要删除第一行和第三行,那么学生-1 的信息就会丢失。 +1. 冗余数据:例如 学生-2 出现了两次。 +2. 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。 +3. 删除异常:删除一个信息,那么也会丢失其它信息。例如如果删除了 课程-1,需要删除第一行和第三行,那么 学生-1 的信息就会丢失。 4. 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。 ## 范式 @@ -470,7 +468,7 @@ Sname, Sdept 和 Mname 都函数依赖于 Sno,而部分依赖于键码。当 非主属性不传递依赖于键码。 -上面的关系-1 中存在以下传递依赖:Sno -> Sdept -> Mname,可以进行以下分解: +上面的 关系-1 中存在以下传递依赖:Sno -> Sdept -> Mname,可以进行以下分解: 关系-11 diff --git a/notes/正则表达式.md b/notes/正则表达式.md index bd38a392..e21a85c5 100644 --- a/notes/正则表达式.md +++ b/notes/正则表达式.md @@ -19,7 +19,7 @@ 正则表达式内置于其它语言或者软件产品中,它本身不是一种语言或者软件。 -[正则表达式在线工具](http://tool.oschina.net/regex/) +[正则表达式在线工具](https://regexr.com/) # 二、匹配单个字符 @@ -164,7 +164,7 @@ a.+c ^ 元字符在字符集合中用作求非,在字符集合外用作匹配字符串的开头。 -使用 (?m) 来打开分行匹配模式,在该模式下,换行被当做字符串的边界。 +分行匹配模式(multiline)下,换行被当做字符串的边界。 **应用** @@ -173,10 +173,10 @@ a.+c **正则表达式** ``` -(?m)^\s*//.*$ +^\s*\/\/.*$ ``` -如果没用 (?m),则只会匹配 // 注释 1 以及之后的所有内容。用了分行匹配模式之后,换行符被当成是字符串分隔符,因此能正确匹配出两个注释内容。 +

**匹配结果** @@ -197,7 +197,7 @@ a.+c **正则表达式** ``` -(ab) {2,} +(ab){2,} ``` **匹配结果** @@ -222,21 +222,23 @@ a.+c 匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的: -1. 一位或者两位的数字 -2. 1 开头的三位数 -3. 2 开头,第 2 位是 0-4 的三位数 -4. 25 开头,第 3 位是 0-5 的三位数 +1. 一位数字 +2. 不以 0 开头的两位数字 +3. 1 开头的三位数 +4. 2 开头,第 2 位是 0-4 的三位数 +5. 25 开头,第 3 位是 0-5 的三位数 **正则表达式** ``` -(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.) {3}(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))) +((25[0-5]|(2[0-4]\d)|(1\d{2})|([1-9]\d)|(\d))\.){3}(25[0-5]|(2[0-4]\d)|(1\d{2})|([1-9]\d)|(\d)) ``` **匹配结果** 1. **192.168.0.1** -2. 555.555.555.555 +2. 00.00.00.00 +3. 555.555.555.555 # 八、回溯引用 @@ -251,7 +253,7 @@ a.+c \1 将回溯引用子表达式 (h[1-6]) 匹配的内容,也就是说必须和子表达式匹配的内容一致。 ``` -<(h[1-6])>\w*? +<(h[1-6])>\w*?<\/\1> ``` **匹配结果** diff --git a/notes/算法.md b/notes/算法.md index 997dc245..b6faef12 100644 --- a/notes/算法.md +++ b/notes/算法.md @@ -581,7 +581,7 @@ public class MergeSort { for (int k = lo; k <= hi; k++) { if (i > mid) a[k] = aux[j++]; else if (j > hi) a[k] = aux[i++]; - else if (aux[i].compareTo(a[j]) < 0) a[k] = aux[i++]; // 先进行这一步,保证稳定性 + else if (aux[i].compareTo(a[j]) <= 0) a[k] = aux[i++]; // 先进行这一步,保证稳定性 else a[k] = aux[j++]; } } diff --git a/notes/计算机网络.md b/notes/计算机网络.md index 56623655..d775d73b 100644 --- a/notes/计算机网络.md +++ b/notes/计算机网络.md @@ -86,7 +86,7 @@ ## 电路交换与分组交换 -

+

(以上分别为:电路交换、报文交换以及分组交换) diff --git a/notes/设计模式.md b/notes/设计模式.md index b7e6c76a..93cdd0da 100644 --- a/notes/设计模式.md +++ b/notes/设计模式.md @@ -128,13 +128,13 @@ if (uniqueInstance == null) { uniqueInstance 采用 volatile 关键字修饰也是很有必要的。 -`uniqueInstance = new Singleton();` 这段代码其实是分为三步执行。 +uniqueInstance = new Singleton(); 这段代码其实是分为三步执行。 1. 分配内存空间。 2. 初始化对象。 3. 将 uniqueInstance 指向分配的内存地址。 -但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 `1>3>2`,这在单线程情况下自然是没有问题。但如果是多线程就有可能 B 线程获得是一个还没有被初始化的对象以致于程序出错。 +但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 1>3>2,这在单线程情况下自然是没有问题。但如果是多线程就有可能 B 线程获得是一个还没有被初始化的对象以致于程序出错。 所以使用 volatile 修饰的目的是禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 @@ -178,17 +178,17 @@ public interface Product { ``` ```java -public class ConcreteProduct implements Product{ +public class ConcreteProduct implements Product { } ``` ```java -public class ConcreteProduct1 implements Product{ +public class ConcreteProduct1 implements Product { } ``` ```java -public class ConcreteProduct2 implements Product{ +public class ConcreteProduct2 implements Product { } ``` @@ -249,7 +249,7 @@ public class ConcreteFactory extends Factory { ``` ```java -public class ConcreteFactory1 extends Factory{ +public class ConcreteFactory1 extends Factory { public Product factoryMethod() { return new ConcreteProduct1(); } @@ -272,7 +272,7 @@ public class ConcreteFactory2 extends Factory { ## 类图 -

+

抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂模式只是用于创建一个对象,这和抽象工厂模式有很大不同。 @@ -305,12 +305,12 @@ public class ProductA2 extends AbstractProductA { ``` ```java -public class ProductB1 extends AbstractProductB{ +public class ProductB1 extends AbstractProductB { } ``` ```java -public class ProductB2 extends AbstractProductB{ +public class ProductB2 extends AbstractProductB { } ``` @@ -322,7 +322,7 @@ public abstract class AbstractFactory { ``` ```java -public class ConcreteFactory1 extends AbstractFactory{ +public class ConcreteFactory1 extends AbstractFactory { AbstractProductA createProductA() { return new ProductA1(); } diff --git a/notes/面向对象思想.md b/notes/面向对象思想.md index 5ab84adf..8cd324cb 100644 --- a/notes/面向对象思想.md +++ b/notes/面向对象思想.md @@ -115,7 +115,7 @@ - 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块 - 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能 - 提高软件的可重用性 -- 减低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的 +- 降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的 以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。 diff --git a/pics/14cfe8d4-e31b-49e0-ac6a-6f4f7aa06ab6.png b/pics/14cfe8d4-e31b-49e0-ac6a-6f4f7aa06ab6.png new file mode 100644 index 00000000..54a1f458 Binary files /dev/null and b/pics/14cfe8d4-e31b-49e0-ac6a-6f4f7aa06ab6.png differ diff --git a/pics/1a231f2a-5c2f-4231-8e0f-915aa5894347.jpg b/pics/1a231f2a-5c2f-4231-8e0f-915aa5894347.jpg new file mode 100644 index 00000000..f43e0f25 Binary files /dev/null and b/pics/1a231f2a-5c2f-4231-8e0f-915aa5894347.jpg differ diff --git a/pics/5e8d3c04-d93b-48a7-875e-41ababed00e0.jpg b/pics/5e8d3c04-d93b-48a7-875e-41ababed00e0.jpg new file mode 100644 index 00000000..b8d0adf8 Binary files /dev/null and b/pics/5e8d3c04-d93b-48a7-875e-41ababed00e0.jpg differ diff --git a/pics/600e9c75-5033-4dad-ae2b-930957db638e.png b/pics/600e9c75-5033-4dad-ae2b-930957db638e.png new file mode 100644 index 00000000..bf0834d5 Binary files /dev/null and b/pics/600e9c75-5033-4dad-ae2b-930957db638e.png differ diff --git a/pics/72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png b/pics/72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png new file mode 100644 index 00000000..748980c4 Binary files /dev/null and b/pics/72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png differ diff --git a/pics/d5c16be7-a1c0-4c8d-b6b9-5999cdc6f9b3.png b/pics/d5c16be7-a1c0-4c8d-b6b9-5999cdc6f9b3.png new file mode 100644 index 00000000..df5be529 Binary files /dev/null and b/pics/d5c16be7-a1c0-4c8d-b6b9-5999cdc6f9b3.png differ