未验证 提交 596dce2d 编写于 作者: J jesonzhuang 提交者: GitHub

Merge pull request #2 from CyC2018/master

merge from master
<!-- ![](https://img.shields.io/badge/update-today-blue.svg) ![](https://img.shields.io/badge/gitbook-making-lightgrey.svg)</br> --> <!-- ![](https://img.shields.io/badge/update-today-blue.svg) ![](https://img.shields.io/badge/gitbook-making-lightgrey.svg)</br> -->
| Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ | | Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ |
| :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| | :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:|
|网络[: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) |
</br> </br>
:loudspeaker: 本仓库不参与商业行为,不向读者收取任何费用。 :loudspeaker: 本仓库不参与商业行为,不向读者收取任何费用。
...@@ -9,15 +9,20 @@ ...@@ -9,15 +9,20 @@
:loudspeaker: This repository is not engaging in business activities, and does not charge readers any fee. :loudspeaker: This repository is not engaging in business activities, and does not charge readers any fee.
</br></br> </br></br>
## 网络 :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: ## 操作系统 :computer:
...@@ -29,19 +34,17 @@ ...@@ -29,19 +34,17 @@
整理自《鸟哥的 Linux 私房菜》 整理自《鸟哥的 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: ## 面向对象 :couple:
......
...@@ -507,7 +507,7 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名, ...@@ -507,7 +507,7 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,
### 3. 隧道 ### 3. 隧道
使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。隧道本身不去解析 HTTP 请求。 使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。
# 六、HTTPs # 六、HTTPs
...@@ -803,16 +803,18 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404 ...@@ -803,16 +803,18 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404
2. HTTP/1.1 支持管线化处理 2. HTTP/1.1 支持管线化处理
3. HTTP/1.1 支持虚拟主机 3. HTTP/1.1 支持虚拟主机
4. HTTP/1.1 新增状态码 100 4. HTTP/1.1 新增状态码 100
5. HTTP/1.1 只是分块传输编码 5. HTTP/1.1 支持分块传输编码
6. HTTP/1.1 新增缓存处理指令 max-age 6. HTTP/1.1 新增缓存处理指令 max-age
具体内容见上文 具体内容见上文
## HTTP/1.1 与 HTTP/2.0 的区别 ## HTTP/1.1 与 HTTP/2.0 的区别
> [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
### 1. 多路复用 ### 1. 多路复用
HTTP/2.0 使用多路复用技术,使用同一个 TCP 连接来处理多个请求。 HTTP/2.0 使用多路复用技术,同一个 TCP 连接可以处理多个请求。
### 2. 首部压缩 ### 2. 首部压缩
...@@ -820,7 +822,7 @@ HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 ...@@ -820,7 +822,7 @@ HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0
### 3. 服务端推送 ### 3. 服务端推送
在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发给客户端。 HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发给客户端。
### 4. 二进制格式 ### 4. 二进制格式
...@@ -828,7 +830,7 @@ HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。 ...@@ -828,7 +830,7 @@ HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。
# 参考资料 # 参考资料
- 上野宣. 图解 HTTP[M]. Ren min you dian chu ban she, 2014. - 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014.
- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP) - [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
- [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/) - [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/)
- [HTTP (HyperText Transfer Protocol)](https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html) - [HTTP (HyperText Transfer Protocol)](https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html)
...@@ -848,3 +850,5 @@ HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。 ...@@ -848,3 +850,5 @@ HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。
- [XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) - [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/) - [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) - [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)
...@@ -44,11 +44,11 @@ java.awt.Desktop#getDesktop() ...@@ -44,11 +44,11 @@ java.awt.Desktop#getDesktop()
## 2. 简单工厂模式 ## 2. 简单工厂模式
在不对用户暴露对象内部逻辑的前提下创建对象;使用通用的接口来创建对象; 在不对用户暴露对象内部逻辑的前提下创建对象
## 3. 工厂方法模式 ## 3. 工厂方法模式
定义创建对象的接口,但是让子类来决定应该使用哪个类来创建;使用通用的接口来创建对象; 定义创建对象的接口,但是让子类来决定应该使用哪个类来创建
```java ```java
java.lang.Proxy#newProxyInstance() java.lang.Proxy#newProxyInstance()
...@@ -120,7 +120,7 @@ javax.swing.Action ...@@ -120,7 +120,7 @@ javax.swing.Action
## 3. 解释器模式 ## 3. 解释器模式
为语言创建解释器,通常由语言的语法和语法分析来定义。 为语言创建解释器,通常由语言的语法和语法分析来定义。
```java ```java
java.util.Pattern java.util.Pattern
java.text.Normalizer java.text.Normalizer
...@@ -229,7 +229,7 @@ JDBC ...@@ -229,7 +229,7 @@ JDBC
## 3. 组合模式 ## 3. 组合模式
将对象组合成树形结构来表示整-部分层次关系,允许用户以相同的方式处理单独对象和组合对象。 将对象组合成树形结构来表示整-部分层次关系,允许用户以相同的方式处理单独对象和组合对象。
```java ```java
javax.swing.JComponent#add(Component) javax.swing.JComponent#add(Component)
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
* [访问权限](#访问权限) * [访问权限](#访问权限)
* [抽象类与接口](#抽象类与接口) * [抽象类与接口](#抽象类与接口)
* [super](#super) * [super](#super)
* [重载与重写](#重载与重写) * [覆盖与重载](#覆盖与重载)
* [五、String](#五string) * [五、String](#五string)
* [String, StringBuffer and StringBuilder](#string,-stringbuffer-and-stringbuilder) * [String, StringBuffer and StringBuilder](#string,-stringbuffer-and-stringbuilder)
* [String 不可变的原因](#string-不可变的原因) * [String 不可变的原因](#string-不可变的原因)
...@@ -55,7 +55,7 @@ y.a = 1; ...@@ -55,7 +55,7 @@ y.a = 1;
声明方法不能被子类覆盖。 声明方法不能被子类覆盖。
private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是覆盖基类方法,而是重载了 private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是覆盖基类方法,而是在子类中定义了一个新的方法
**3. 类** **3. 类**
...@@ -65,7 +65,7 @@ private 方法隐式地被指定为 final,如果在子类中定义的方法和 ...@@ -65,7 +65,7 @@ private 方法隐式地被指定为 final,如果在子类中定义的方法和
**1. 静态变量** **1. 静态变量**
静态变量在内存中只存在一份,只在类第一次实例化时初始化一次。 静态变量在内存中只存在一份,只在类初始化时赋值一次。
- 静态变量:类所有的实例都共享静态变量,可以直接通过类名来访问它; - 静态变量:类所有的实例都共享静态变量,可以直接通过类名来访问它;
- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。 - 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
...@@ -83,11 +83,23 @@ public class A { ...@@ -83,11 +83,23 @@ public class A {
**3. 静态语句块** **3. 静态语句块**
静态语句块和静态变量一样在类第一次实例化时运行一次。 静态语句块在类初始化时运行一次。
**4. 初始化顺序** **4. 静态内部类**
静态数据优先于其它数据的初始化,静态变量和静态语句块哪个先运行取决于它们在代码中的顺序。 内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非 static 变量和方法。
**5. 静态导包**
```source-java
import static com.xxx.ClassName.*
```
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
**6. 变量赋值顺序**
静态变量的赋值和静态语句块的运行优先于实例变量的赋值和普通语句块的运行,静态变量的赋值和静态语句块的运行哪个先执行取决于它们在代码中的顺序。
```java ```java
public static String staticField = "静态变量"; public static String staticField = "静态变量";
...@@ -99,8 +111,6 @@ static { ...@@ -99,8 +111,6 @@ static {
} }
``` ```
实例变量和普通语句块的初始化在静态变量和静态语句块初始化结束之后。
```java ```java
public String field = "实例变量"; public String field = "实例变量";
``` ```
...@@ -111,7 +121,7 @@ public String field = "实例变量"; ...@@ -111,7 +121,7 @@ public String field = "实例变量";
} }
``` ```
最后才是构造函数中的数据进行初始化 最后才运行构造函数
```java ```java
public InitialOrderTest() { public InitialOrderTest() {
...@@ -121,25 +131,13 @@ public InitialOrderTest() { ...@@ -121,25 +131,13 @@ public InitialOrderTest() {
存在继承的情况下,初始化顺序为: 存在继承的情况下,初始化顺序为:
1. 父类(静态变量、静态语句块 1. 父类(静态变量、静态语句块)
2. 子类(静态变量、静态语句块) 2. 子类(静态变量、静态语句块)
3. 父类(实例变量、普通语句块) 3. 父类(实例变量、普通语句块)
4. 父类(构造函数) 4. 父类(构造函数)
5. 子类(实例变量、普通语句块) 5. 子类(实例变量、普通语句块)
6. 子类(构造函数) 6. 子类(构造函数)
**5. 静态内部类**
内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非 static 变量和方法。
**6. 静态导包**
```source-java
import static com.xxx.ClassName.*
```
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
# 二、Object 通用方法 # 二、Object 通用方法
## 概览 ## 概览
...@@ -255,9 +253,9 @@ public class EqualExample { ...@@ -255,9 +253,9 @@ public class EqualExample {
## hashCode() ## hashCode()
hasCode() 返回散列值,而 equals() 是用来判断两个实例是否相等。相等的两个实例散列值一定要相同,但是散列值相同的两个实例不一定相等 hasCode() 返回散列值,而 equals() 是用来判断两个实例是否等价。等价的两个实例散列值一定要相同,但是散列值相同的两个实例不一定等价
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证相等的两个实例散列值也相等 在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证相等的两个实例散列值也等价
下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例。 下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例。
...@@ -313,7 +311,7 @@ ToStringExample@4554617c ...@@ -313,7 +311,7 @@ ToStringExample@4554617c
**1. cloneable** **1. cloneable**
clone() 是 Object 的受保护方法,这意味着,如果一个类不显式去重载 clone() 就没有这个方法。 clone() 是 Object 的受保护方法,这意味着,如果一个类不显式去覆盖 clone() 就没有这个方法。
```java ```java
public class CloneExample { public class CloneExample {
...@@ -324,10 +322,10 @@ public class CloneExample { ...@@ -324,10 +322,10 @@ public class CloneExample {
```java ```java
CloneExample e1 = new CloneExample(); 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() 得到以下实现: 接下来覆盖 Object 的 clone() 得到以下实现:
```java ```java
public class CloneExample { public class CloneExample {
...@@ -510,7 +508,7 @@ protected 用于修饰成员,表示在继承体系中成员对于子类可见 ...@@ -510,7 +508,7 @@ protected 用于修饰成员,表示在继承体系中成员对于子类可见
如果子类的方法覆盖了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里式替换原则。 如果子类的方法覆盖了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里式替换原则。
字段决不能是公有的,因为这么做的话就失去了对这个实例域修改行为的控制,客户端可以对其随意修改。可以使用共有的 getter 和 setter 方法来替换共有字段。 字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。可以使用公有的 getter 和 setter 方法来替换公有字段。
```java ```java
public class AccessExample { public class AccessExample {
...@@ -595,7 +593,7 @@ ac2.func1(); ...@@ -595,7 +593,7 @@ ac2.func1();
从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。 从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。
接口也可以包含域,并且这些域隐式都是 static 和 final 的。 接口也可以包含字段,并且这些字段隐式都是 static 和 final 的。
接口中的方法默认都是 public 的,并且不允许定义为 private 或者 protected。 接口中的方法默认都是 public 的,并且不允许定义为 private 或者 protected。
...@@ -634,16 +632,16 @@ System.out.println(InterfaceExample.x); ...@@ -634,16 +632,16 @@ System.out.println(InterfaceExample.x);
**3. 比较** **3. 比较**
- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求子类和父类具有 IS-A 关系; - 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。 - 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
- 接口的域只能是 static 和 final 类型的,而抽象类的域可以有多种访问权限 - 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制
- 接口的方法只能是 public 的,而抽象类的方法可以由多种访问权限。 - 接口的方法只能是 public 的,而抽象类的方法可以由多种访问权限。
**4. 使用选择** **4. 使用选择**
使用抽象类: 使用抽象类:
- 需要在几个相关的类中共享代码 - 需要在几个相关的类中共享代码
- 需要能控制继承来的方法和域的访问权限,而不是都为 public。 - 需要能控制继承来的方法和域的访问权限,而不是都为 public。
- 需要继承非静态(non-static)和非常量(non-final)字段。 - 需要继承非静态(non-static)和非常量(non-final)字段。
...@@ -652,7 +650,7 @@ System.out.println(InterfaceExample.x); ...@@ -652,7 +650,7 @@ System.out.println(InterfaceExample.x);
- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法; - 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
- 需要使用多重继承。 - 需要使用多重继承。
在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次接口要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。 在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
> [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/) </br> [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface) > [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/) </br> [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface)
...@@ -706,11 +704,11 @@ SuperExtendExample.func() ...@@ -706,11 +704,11 @@ SuperExtendExample.func()
> [Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html) > [Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html)
## 重载与重写 ## 覆盖与重载
- 重写存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法; - 覆盖(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法;
- 重载即存在于继承体系中,也存在于同一个类中,指一个方法与已经存在的方法或者父类的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。 - 重载(Overload)存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。
# 五、String # 五、String
...@@ -739,7 +737,7 @@ SuperExtendExample.func() ...@@ -739,7 +737,7 @@ SuperExtendExample.func()
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。 如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
<div align="center"> <img src="../pics//f76067a5-7d5f-4135-9549-8199c77d8f1c.jpg"/> </div><br> <div align="center"> <img src="../pics//f76067a5-7d5f-4135-9549-8199c77d8f1c.jpg" width=""/> </div><br>
**3. 安全性** **3. 安全性**
...@@ -773,7 +771,7 @@ String s5 = "bbb"; ...@@ -773,7 +771,7 @@ String s5 = "bbb";
System.out.println(s4 == s5); // true System.out.println(s4 == s5); // true
``` ```
Java 虚拟机将堆划分成新生代、老年代和永久代(PermGen Space)。在 Java 6 之前,字符串常量池被放在永久代中,而在 Java 7 时,它被放在堆的其它位置。这是因为永久代的空间有限,如果大量使用字符串的场景下会导致 OutOfMemoryError 错误。 在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7,字符串常量池被放在堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
> [What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning) </br> [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html) > [What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning) </br> [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html)
...@@ -946,7 +944,7 @@ Throwable 可以用来表示任何可以作为异常抛出的类,分为两种 ...@@ -946,7 +944,7 @@ Throwable 可以用来表示任何可以作为异常抛出的类,分为两种
1. **受检异常** :需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复; 1. **受检异常** :需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;
2. **非受检异常** :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序奔溃并且无法恢复。 2. **非受检异常** :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序奔溃并且无法恢复。
<div align="center"> <img src="../pics//PPjwP.png"/> </div><br> <div align="center"> <img src="../pics//PPjwP.png" width="600"/> </div><br>
> [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception) </br> [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html) > [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception) </br> [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html)
......
...@@ -9,9 +9,9 @@ ...@@ -9,9 +9,9 @@
* [ArrayList](#arraylist) * [ArrayList](#arraylist)
* [Vector](#vector) * [Vector](#vector)
* [LinkedList](#linkedlist) * [LinkedList](#linkedlist)
* [LinkedHashMap](#linkedhashmap)
* [TreeMap](#treemap) * [TreeMap](#treemap)
* [HashMap](#hashmap) * [HashMap](#hashmap)
* [LinkedHashMap](#linkedhashmap)
* [ConcurrentHashMap - JDK 1.7](#concurrenthashmap---jdk-17) * [ConcurrentHashMap - JDK 1.7](#concurrenthashmap---jdk-17)
* [ConcurrentHashMap - JDK 1.8](#concurrenthashmap---jdk-18) * [ConcurrentHashMap - JDK 1.8](#concurrenthashmap---jdk-18)
* [参考资料](#参考资料) * [参考资料](#参考资料)
...@@ -40,13 +40,13 @@ ...@@ -40,13 +40,13 @@
- Vector:和 ArrayList 类似,但它是线程安全的; - Vector:和 ArrayList 类似,但它是线程安全的;
- LinkedList:基于双向循环链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双队列。 - LinkedList:基于双向循环链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双队列。
### 3. Queue ### 3. Queue
- LinkedList:可以用它来支持双向队列; - LinkedList:可以用它来支持双向队列;
- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。 - PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
## Map ## Map
...@@ -111,7 +111,7 @@ List list = Arrays.asList(1,2,3); ...@@ -111,7 +111,7 @@ List list = Arrays.asList(1,2,3);
## ArrayList ## 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. 概览 ### 1. 概览
...@@ -224,6 +224,10 @@ private void writeObject(java.io.ObjectOutputStream s) ...@@ -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) [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
[TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java) [TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java)
...@@ -302,7 +306,7 @@ map.put("K3", "V3"); ...@@ -302,7 +306,7 @@ map.put("K3", "V3");
- 插入 &lt;K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。 - 插入 &lt;K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
- 插入 &lt;K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 &lt;K2,V2> 后面。 - 插入 &lt;K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 &lt;K2,V2> 后面。
<div align="center"> <img src="../pics//07903a31-0fb3-45fc-86f5-26f0b28fa4e7.png" width="600"/> </div><br> <div align="center"> <img src="../pics//d5c16be7-a1c0-4c8d-b6b9-5999cdc6f9b3.png" width="600"/> </div><br>
查找需要分成两步进行: 查找需要分成两步进行:
...@@ -317,7 +321,7 @@ map.put("K3", "V3"); ...@@ -317,7 +321,7 @@ map.put("K3", "V3");
因为从 JDK 1.8 开始引入了红黑树,因此扩容操作较为复杂,为了便于理解,以下内容使用 JDK 1.7 的内容。 因为从 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 值,使得空间效率和时间效率都能得到保证。 为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。
...@@ -397,11 +401,9 @@ void transfer(Entry[] newTable) { ...@@ -397,11 +401,9 @@ void transfer(Entry[] newTable) {
### 5. 确定桶下标 ### 5. 确定桶下标
很多操作都需要先确定一个键值对所在的桶下标,需要分三步进行。 很多操作都需要先确定一个键值对所在的桶下标,这个操作需要分三步进行。
(一)hashCode()
调用 Key 的 hashCode() 方法得到 hashCode。 (一)调用 hashCode()
```java ```java
public final int hashCode() { public final int hashCode() {
...@@ -420,7 +422,7 @@ static final int hash(Object key) { ...@@ -420,7 +422,7 @@ static final int hash(Object key) {
} }
``` ```
(三)除留余数 (三)除留余数
令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质: 令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:
...@@ -429,7 +431,7 @@ x : 00010000 ...@@ -429,7 +431,7 @@ x : 00010000
x-1 : 00001111 x-1 : 00001111
``` ```
令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数: 令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:
``` ```
y : 10110010 y : 10110010
...@@ -449,7 +451,7 @@ y%x : 00000010 ...@@ -449,7 +451,7 @@ y%x : 00000010
拉链法需要使用除留余数法来得到桶下标,也就是需要进行以下计算:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。 拉链法需要使用除留余数法来得到桶下标,也就是需要进行以下计算:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。
以下操作在 Java 8 中没有,但是原理上相同。 以下操作在 JDK 1.8 中没有,但是原理上相同。
```java ```java
static int indexFor(int h, int length) { static int indexFor(int h, int length) {
...@@ -472,7 +474,9 @@ new capacity : 00100000 ...@@ -472,7 +474,9 @@ new capacity : 00100000
### 7. 扩容-计算数组容量 ### 7. 扩容-计算数组容量
先考虑如何求一个数的补码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到: HashMap 构造函数允许用户传入的容量不是 2 的幂次方,因为它可以自动地将传入的容量转换为 2 的幂次方。
先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到:
``` ```
mask |= mask >> 1 11011000 mask |= mask >> 1 11011000
...@@ -480,9 +484,14 @@ mask |= mask >> 2 11111100 ...@@ -480,9 +484,14 @@ mask |= mask >> 2 11111100
mask |= mask >> 4 11111111 mask |= mask >> 4 11111111
``` ```
如果最后令 mask+1,得到就是大于原始数字的最小的 2 次方。 mask+1 是大于原始数字的最小的 2 幂次方。
以下是 HashMap 中计算一个大小所需要的数组容量的代码: ```
num 10010000
mask+1 100000000
```
以下是 HashMap 中计算数组容量的代码:
```java ```java
static final int tableSizeFor(int cap) { static final int tableSizeFor(int cap) {
...@@ -508,15 +517,11 @@ HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0 ...@@ -508,15 +517,11 @@ HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0
- 由于 Hashtable 是线程安全的也是 synchronized,所以在单线程环境下它比 HashMap 要慢。 - 由于 Hashtable 是线程安全的也是 synchronized,所以在单线程环境下它比 HashMap 要慢。
- HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。 - HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
## LinkedHashMap
[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java)
## ConcurrentHashMap - JDK 1.7 ## ConcurrentHashMap - JDK 1.7
[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/1.7/ConcurrentHashMap.java) [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 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。 相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。
...@@ -533,7 +538,7 @@ static final class HashEntry<K,V> { ...@@ -533,7 +538,7 @@ static final class HashEntry<K,V> {
} }
``` ```
继承自 ReentrantLock,每个 Segment 维护着多个 HashEntry。 Segment 继承自 ReentrantLock,每个 Segment 维护着多个 HashEntry。
```java ```java
static final class Segment<K,V> extends ReentrantLock implements Serializable { static final class Segment<K,V> extends ReentrantLock implements Serializable {
...@@ -567,48 +572,19 @@ static final int DEFAULT_CONCURRENCY_LEVEL = 16; ...@@ -567,48 +572,19 @@ static final int DEFAULT_CONCURRENCY_LEVEL = 16;
<div align="center"> <img src="../pics//image005.jpg"/> </div><br> <div align="center"> <img src="../pics//image005.jpg"/> </div><br>
### 2. HashEntery 的不可变性 ### 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 对 ConcurrentHashMap 做结构性修改,实质上是对某个桶指向的链表做结构性修改。如果能够确保:在读线程遍历一个链表期间,写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表。那么读 / 写线程之间就可以安全并发访问这个 ConcurrentHashMap。
final V remove(Object key, int hash, Object value) {
if (!tryLock()) 结构性修改操作包括 put,remove,clear。下面我们分别分析这三个操作。
scanAndLock(key, hash);
V oldValue = null; clear 操作只是把 ConcurrentHashMap 中所有的桶 “置空”,每个桶之前引用的链表依然存在,只是桶不再引用到这些链表(所有链表的结构并没有被修改)。正在遍历某个链表的读线程依然可以正常执行对该链表的遍历。
try {
HashEntry<K,V>[] tab = table; put 操作如果需要插入一个新节点到链表中时 , 会在链表头部插入这个新节点。此时,链表中的原有节点的链接并没有被修改。也就是说:插入新健 / 值对到链表中的操作不会影响读线程正常遍历这个链表。
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> 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;
}
```
在以下链表中删除 C 节点,C 节点之后的所有节点都原样保留,C 节点之前的所有节点都被克隆到新的链表中,并且顺序被反转。可以注意到,在执行 remove 操作时,原始链表并没有被修改,也就是说,读线程不会受到执行 remove 操作的并发写线程的干扰。 在以下链表中删除 C 节点,C 节点之后的所有节点都原样保留,C 节点之前的所有节点都被克隆到新的链表中,并且顺序被反转。可以注意到,在执行 remove 操作时,原始链表并没有被修改,也就是说,读线程不会受到执行 remove 操作的并发写线程的干扰。
...@@ -616,19 +592,10 @@ final V remove(Object key, int hash, Object value) { ...@@ -616,19 +592,10 @@ final V remove(Object key, int hash, Object value) {
<div align="center"> <img src="../pics//image008.jpg"/> </div><br> <div align="center"> <img src="../pics//image008.jpg"/> </div><br>
除了 remove 操作,其它操作也类似。可以得出一个结论:写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。 综上,可以得出一个结论:写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。
### 3. Volatile 变量 ### 3. Volatile 变量
```java
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
```
由于内存可见性问题,未正确同步的情况下,写线程写入的值可能并不为后续的读线程可见。 由于内存可见性问题,未正确同步的情况下,写线程写入的值可能并不为后续的读线程可见。
下面以写线程 M 和读线程 N 来说明 ConcurrentHashMap 如何协调读 / 写线程间的内存可见性问题。 下面以写线程 M 和读线程 N 来说明 ConcurrentHashMap 如何协调读 / 写线程间的内存可见性问题。
...@@ -674,7 +641,7 @@ V get(Object key, int hash) { ...@@ -674,7 +641,7 @@ V get(Object key, int hash) {
在 ConcurrentHashMap 中,所有执行写操作的方法(put, remove, clear),在对链表做结构性修改之后,在退出写方法前都会去写这个 count 变量。所有未加锁的读操作(get, contains, containsKey)在读方法中,都会首先去读取这个 count 变量。 在 ConcurrentHashMap 中,所有执行写操作的方法(put, remove, clear),在对链表做结构性修改之后,在退出写方法前都会去写这个 count 变量。所有未加锁的读操作(get, contains, containsKey)在读方法中,都会首先去读取这个 count 变量。
根据 Java 内存模型,对 同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程 “看到”。 根据 Java 内存模型,对同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程 “看到”。
这个特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 ,读线程才需要加锁后重读)。 这个特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 ,读线程才需要加锁后重读)。
......
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
# 一、线程状态转换 # 一、线程状态转换
<div align="center"> <img src="../pics//ace830df-9919-48ca-91b5-60b193f593d2.png"/> </div><br> <div align="center"> <img src="../pics//ace830df-9919-48ca-91b5-60b193f593d2.png" width=""/> </div><br>
## 新建(New) ## 新建(New)
...@@ -192,7 +192,7 @@ Executor 管理多个异步任务的执行,而无需程序员显示地管理 ...@@ -192,7 +192,7 @@ Executor 管理多个异步任务的执行,而无需程序员显示地管理
主要有三种 Executor: 主要有三种 Executor:
1. CachedTreadPool:一个任务创建一个线程; 1. CachedThreadPool:一个任务创建一个线程;
2. FixedThreadPool:所有任务只能使用固定大小的线程; 2. FixedThreadPool:所有任务只能使用固定大小的线程;
3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
...@@ -709,7 +709,7 @@ java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J. ...@@ -709,7 +709,7 @@ java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。 维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
<div align="center"> <img src="../pics//CountdownLatch.png"/> </div><br> <div align="center"> <img src="../pics//CountdownLatch.png" width=""/> </div><br>
```java ```java
public class CountdownLatchExample { public class CountdownLatchExample {
...@@ -743,7 +743,7 @@ run..run..run..run..run..run..run..run..run..run..end ...@@ -743,7 +743,7 @@ run..run..run..run..run..run..run..run..run..run..end
下图应该从下往上看才正确。 下图应该从下往上看才正确。
<div align="center"> <img src="../pics//CyclicBarrier.png"/> </div><br> <div align="center"> <img src="../pics//CyclicBarrier.png" width=""/> </div><br>
```java ```java
public class CyclicBarrierExample { public class CyclicBarrierExample {
...@@ -778,7 +778,7 @@ before..before..before..before..before..before..before..before..before..before.. ...@@ -778,7 +778,7 @@ before..before..before..before..before..before..before..before..before..before..
Semaphore 就是操作系统中的信号量,可以控制对互斥资源的访问线程数。 Semaphore 就是操作系统中的信号量,可以控制对互斥资源的访问线程数。
<div align="center"> <img src="../pics//Semaphore.png"/> </div><br> <div align="center"> <img src="../pics//Semaphore.png" width=""/> </div><br>
以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。 以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。
...@@ -982,7 +982,7 @@ public class ForkJoinPool extends AbstractExecutorService ...@@ -982,7 +982,7 @@ public class ForkJoinPool extends AbstractExecutorService
ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。 ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。
<div align="center"> <img src="../pics//15b45dc6-27aa-4519-9194-f4acfa2b077f.jpg"/> </div><br> <div align="center"> <img src="../pics//15b45dc6-27aa-4519-9194-f4acfa2b077f.jpg" width=""/> </div><br>
# 九、线程不安全示例 # 九、线程不安全示例
...@@ -1027,27 +1027,27 @@ public class ThreadUnsafeExample { ...@@ -1027,27 +1027,27 @@ public class ThreadUnsafeExample {
# 十、Java 内存模型 # 十、Java 内存模型
Java 内存模型图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。 Java 内存模型图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
## 主内存与工作内存 ## 主内存与工作内存
处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。 处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致。CPU 使用一致性协议来解决一致性问题。 加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。
<div align="center"> <img src="../pics//68778c1b-15ab-4826-99c0-3b4fd38cb9e9.png"/> </div><br> <div align="center"> <img src="../pics//68778c1b-15ab-4826-99c0-3b4fd38cb9e9.png" width=""/> </div><br>
所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。 所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。 线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
<div align="center"> <img src="../pics//47358f87-bc4c-496f-9a90-8d696de94cee.png"/> </div><br> <div align="center"> <img src="../pics//47358f87-bc4c-496f-9a90-8d696de94cee.png" width=""/> </div><br>
## 内存间交互操作 ## 内存间交互操作
Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。 Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。
<div align="center"> <img src="../pics//536c6dfd-305a-4b95-b12c-28ca5e8aa043.png"/> </div><br> <div align="center"> <img src="../pics//536c6dfd-305a-4b95-b12c-28ca5e8aa043.png" width=""/> </div><br>
- read:把一个变量的值从主内存传输到工作内存中 - read:把一个变量的值从主内存传输到工作内存中
- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中 - load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
...@@ -1070,11 +1070,11 @@ Java 内存模型保证了 read、load、use、assign、store、write、lock 和 ...@@ -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 这些单个操作具备原子性。 下图演示了两个线程同时对 cnt 变量进行操作,load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入该变量的值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。
<div align="center"> <img src="../pics//ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92.png"/> </div><br> <div align="center"> <img src="../pics//ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92.png" width=""/> </div><br>
AtomicInteger 能保证多个线程修改的原子性。 AtomicInteger 能保证多个线程修改的原子性。
<div align="center"> <img src="../pics//952afa9a-458b-44ce-bba9-463e60162945.png"/> </div><br> <div align="center"> <img src="../pics//952afa9a-458b-44ce-bba9-463e60162945.png" width=""/> </div><br>
使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现: 使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现:
...@@ -1153,7 +1153,7 @@ public class AtomicSynchronizedExample { ...@@ -1153,7 +1153,7 @@ public class AtomicSynchronizedExample {
可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。 可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一的对象),那么其它线程就能看见 final 字段的值。 volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一的对象),那么其它线程就能看见 final 字段的值。
对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题。因为 volatile 并不能保证操作的原子性。 对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题。因为 volatile 并不能保证操作的原子性。
...@@ -1181,7 +1181,7 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即 ...@@ -1181,7 +1181,7 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即
在一个线程内,在程序前面的操作先行发生于后面的操作。 在一个线程内,在程序前面的操作先行发生于后面的操作。
<div align="center"> <img src="../pics//single-thread-rule.png"/> </div><br> <div align="center"> <img src="../pics//single-thread-rule.png" width=""/> </div><br>
### 2. 管程锁定规则 ### 2. 管程锁定规则
...@@ -1189,7 +1189,7 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即 ...@@ -1189,7 +1189,7 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
<div align="center"> <img src="../pics//monitor-lock-rule.png"/> </div><br> <div align="center"> <img src="../pics//monitor-lock-rule.png" width=""/> </div><br>
### 3. volatile 变量规则 ### 3. volatile 变量规则
...@@ -1197,15 +1197,15 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即 ...@@ -1197,15 +1197,15 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
<div align="center"> <img src="../pics//volatile-variable-rule.png"/> </div><br> <div align="center"> <img src="../pics//volatile-variable-rule.png" width=""/> </div><br>
### 4. 线程启动规则 ### 4. 线程启动规则
> Thread Start Rule > Thread Start Rule
Thread 对象的 start() 方法先行发生于此线程的每一个动作。 Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
<div align="center"> <img src="../pics//thread-start-rule.png"/> </div><br> <div align="center"> <img src="../pics//thread-start-rule.png" width=""/> </div><br>
### 5. 线程加入规则 ### 5. 线程加入规则
...@@ -1213,7 +1213,7 @@ Thread 对象的 start() 方法先行发生于此线程的每一个动作。 ...@@ -1213,7 +1213,7 @@ Thread 对象的 start() 方法先行发生于此线程的每一个动作。
join() 方法返回先行发生于 Thread 对象的结束。 join() 方法返回先行发生于 Thread 对象的结束。
<div align="center"> <img src="../pics//thread-join-rule.png"/> </div><br> <div align="center"> <img src="../pics//thread-join-rule.png" width=""/> </div><br>
### 6. 线程中断规则 ### 6. 线程中断规则
...@@ -1276,7 +1276,7 @@ public V put(K key, V value) { ...@@ -1276,7 +1276,7 @@ public V put(K key, V value) {
} }
``` ```
多线程环境下,应当尽量使对象为不可变,来满足线程安全。 多线程环境下,应当尽量使对象为不可变,来满足线程安全。
### 2. 绝对线程安全 ### 2. 绝对线程安全
...@@ -1414,7 +1414,7 @@ ABA :如果一个变量 V 初次读取的时候是 A 值,它的值被改成 ...@@ -1414,7 +1414,7 @@ ABA :如果一个变量 V 初次读取的时候是 A 值,它的值被改成
**(二)栈封闭** **(二)栈封闭**
多个线程方法同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在栈中,属于线程私有的。 多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在栈中,属于线程私有的。
```java ```java
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
...@@ -1505,7 +1505,7 @@ public class ThreadLocalExample1 { ...@@ -1505,7 +1505,7 @@ public class ThreadLocalExample1 {
它所对应的底层结构图为: 它所对应的底层结构图为:
<div align="center"> <img src="../pics//3646544a-cb57-451d-9e03-d3c4f5e4434a.png"/> </div><br> <div align="center"> <img src="../pics//3646544a-cb57-451d-9e03-d3c4f5e4434a.png" width=""/> </div><br>
每个 Thread 都有一个 TreadLocal.ThreadLocalMap 对象,Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员。 每个 Thread 都有一个 TreadLocal.ThreadLocalMap 对象,Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员。
...@@ -1601,7 +1601,7 @@ public static String concatString(String s1, String s2, String s3) { ...@@ -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”,即表示此对象处于轻量级锁定状态。 简单地介绍了对象的内存布局后,我们把话题返回到轻量级锁的执行过程上。在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 “01” 状态)虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝(官方把这份拷贝加上了一个 Displaced 前缀,即 Displaced Mark Word)。然后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位(Mark Word 的最后 2bit)将转变为 “00”,即表示此对象处于轻量级锁定状态。
<div align="center"> <img src="../pics//8cc671f0-7134-44b1-a7b5-6d24fe55e1c1.jpg"/> </div><br> <div align="center"> <img src="../pics//8cc671f0-7134-44b1-a7b5-6d24fe55e1c1.jpg" width="600"/> </div><br>
如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是的话只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,所标志的状态变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是的话只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,所标志的状态变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
...@@ -1619,20 +1619,19 @@ public static String concatString(String s1, String s2, String s3) { ...@@ -1619,20 +1619,19 @@ public static String concatString(String s1, String s2, String s3) {
当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行。偏向锁、轻量级锁的状态转换及对象 Mark Word 的关系如图 13-5 所示。 当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行。偏向锁、轻量级锁的状态转换及对象 Mark Word 的关系如图 13-5 所示。
<div align="center"> <img src="../pics//390c913b-5f31-444f-bbdb-2b88b688e7ce.jpg"/> </div><br> <div align="center"> <img src="../pics//390c913b-5f31-444f-bbdb-2b88b688e7ce.jpg" width="600"/> </div><br>
偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。 偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。
# 十三、多线程开发良好的实践 # 十三、多线程开发良好的实践
- 给线程起个有意义的名字,这样可以方便找 Bug。 - 给线程起个有意义的名字,这样可以方便找 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。
- 使用本地变量和不可变类来保证线程安全。 - 使用本地变量和不可变类来保证线程安全。
......
...@@ -142,7 +142,7 @@ Java 对引用的概念进行了扩充,引入四种强度不同的引用类型 ...@@ -142,7 +142,7 @@ Java 对引用的概念进行了扩充,引入四种强度不同的引用类型
**(一)强引用** **(一)强引用**
只要强引用存在,垃圾回收器永远不会回收调掉被引用的对象。 只要强引用存在,垃圾回收器永远不会回收被引用的对象。
使用 new 一个新对象的方式来创建强引用。 使用 new 一个新对象的方式来创建强引用。
...@@ -154,7 +154,7 @@ Object obj = new Object(); ...@@ -154,7 +154,7 @@ Object obj = new Object();
用来描述一些还有用但是并非必需的对象。 用来描述一些还有用但是并非必需的对象。
在系统将要发生内存溢出异常之前,将会对这些对象列进回收范围之中进行第二次回收。 在系统将要发生内存溢出异常之前,会将这些对象列进回收范围之中进行第二次回收。
软引用主要用来实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源获取数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源获取这些数据。 软引用主要用来实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源获取数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源获取这些数据。
...@@ -224,7 +224,13 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 ...@@ -224,7 +224,13 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
1. 标记和清除过程效率都不高; 1. 标记和清除过程效率都不高;
2. 会产生大量碎片,内存碎片过多可能导致无法给大对象分配内存。 2. 会产生大量碎片,内存碎片过多可能导致无法给大对象分配内存。
### 2. 复制 ### 2. 标记 - 整理
<div align="center"> <img src="../pics//902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg" width=""/> </div><br>
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
### 3. 复制
<div align="center"> <img src="../pics//e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg" width=""/> </div><br> <div align="center"> <img src="../pics//e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg" width=""/> </div><br>
...@@ -234,12 +240,6 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 ...@@ -234,12 +240,6 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间。 现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间。
### 3. 标记 - 整理
<div align="center"> <img src="../pics//902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg" width=""/> </div><br>
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
### 4. 分代收集 ### 4. 分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
...@@ -596,7 +596,7 @@ public static void main(String[] args) { ...@@ -596,7 +596,7 @@ public static void main(String[] args) {
### 1. 类与类加载器 ### 1. 类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof() 关键字做对象所属关系判定等情况),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
### 2. 类加载器分类 ### 2. 类加载器分类
......
...@@ -127,7 +127,7 @@ public int arrangeCoins(int n) { ...@@ -127,7 +127,7 @@ public int arrangeCoins(int n) {
int l = 0, h = n; int l = 0, h = n;
while(l <= h){ while(l <= h){
int m = l + (h - l) / 2; int m = l + (h - l) / 2;
long x = m * (m + 1L) / 2; long x = m * (m + 1) / 2;
if(x == n) return m; if(x == n) return m;
else if(x < n) l = m + 1; else if(x < n) l = m + 1;
else h = m - 1; else h = m - 1;
...@@ -190,11 +190,11 @@ You have 3 cookies and their sizes are big enough to gratify all of the children ...@@ -190,11 +190,11 @@ You have 3 cookies and their sizes are big enough to gratify all of the children
You need to output 2. You need to output 2.
``` ```
题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。
因为最小的孩子最容易得到满足,因此先满足最小孩子。给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。 因为最小的孩子最容易得到满足,因此先满足最小孩子。给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。
证明:假设在某次选择中,贪心策略选择给第 i 个孩子分配第 m 个饼干,并且第 i 个孩子满足度最小,第 m 个饼干为可以满足第 i 个孩子的最小饼干,利用贪心策略最终可以满足 k 个孩子。假设最优策略在这次选择中给 i 个孩子分配第 n 个饼干,并且这个饼干大于第 m 个饼干。我们发现使用第 m 个饼干去替代第 n 个饼干完全不影响后续的结果,因此不存在比贪心策略更优的策略,即贪心策略就是最优策略。 证明:假设在某次选择中,贪心策略选择给第 i 个孩子分配第 m 个饼干,并且第 i 个孩子满足度最小,第 m 个饼干为可以满足第 i 个孩子的最小饼干。假设最优策略在这次选择中给 i 个孩子分配第 n 个饼干,并且这个饼干大于第 m 个饼干。我们发现使用第 m 个饼干去替代第 n 个饼干完全不影响后续的结果,因此不存在比贪心策略更优的策略,即贪心策略就是最优策略。
```java ```java
public int findContentChildren(int[] g, int[] s) { public int findContentChildren(int[] g, int[] s) {
...@@ -638,7 +638,7 @@ public int findKthLargest(int[] nums, int k) { ...@@ -638,7 +638,7 @@ public int findKthLargest(int[] nums, int k) {
} }
``` ```
**堆排序** :时间复杂度 O(OlogK),空间复杂度 O(K)。 **堆排序** :时间复杂度 O(NlogK),空间复杂度 O(K)。
```java ```java
public int findKthLargest(int[] nums, int k) { public int findKthLargest(int[] nums, int k) {
...@@ -1813,10 +1813,9 @@ dp[N] 即为所求。 ...@@ -1813,10 +1813,9 @@ dp[N] 即为所求。
```java ```java
public int climbStairs(int n) { public int climbStairs(int n) {
if(n == 1) return 1; if (n <= 2) return n;
if(n == 2) return 2; int pre2 = 1, pre1 = 2;
int pre1 = 2, pre2 = 1; for (int i = 2; i < n; i++) {
for(int i = 2; i < n; i++){
int cur = pre1 + pre2; int cur = pre1 + pre2;
pre2 = pre1; pre2 = pre1;
pre1 = cur; pre1 = cur;
...@@ -1940,7 +1939,7 @@ dp[N] 即为所求。 ...@@ -1940,7 +1939,7 @@ dp[N] 即为所求。
<div align="center"><img src="https://latex.codecogs.com/gif.latex?dp[n]=max\{1,dp[i]+1|S_i<S_n\&\&i<n\}"/></div> <br> <div align="center"><img src="https://latex.codecogs.com/gif.latex?dp[n]=max\{1,dp[i]+1|S_i<S_n\&\&i<n\}"/></div> <br>
对于一个长度为 N 的序列,最长子序列并不一定会以 S<sub>N</sub> 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,即 max{ dp[i] | 1 <= i <= N} 即为所求。 对于一个长度为 N 的序列,最长递增子序列并不一定会以 S<sub>N</sub> 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,即 max{ dp[i] | 1 <= i <= N} 即为所求。
**最长递增子序列** **最长递增子序列**
...@@ -1950,22 +1949,24 @@ dp[N] 即为所求。 ...@@ -1950,22 +1949,24 @@ dp[N] 即为所求。
public int lengthOfLIS(int[] nums) { public int lengthOfLIS(int[] nums) {
int n = nums.length; int n = nums.length;
int[] dp = new int[n]; int[] dp = new int[n];
for(int i = 0; i < n; i++){ for (int i = 0; i < n; i++) {
int max = 1; int max = 1;
for(int j = 0; j < i; j++){ for (int j = 0; j < i; j++) {
if(nums[i] > nums[j]) max = Math.max(max, dp[j] + 1); if (nums[i] > nums[j]) {
max = Math.max(max, dp[j] + 1);
}
} }
dp[i] = max; dp[i] = max;
} }
int ret = 0; int ret = 0;
for(int i = 0; i < n; i++){ for (int i = 0; i < n; i++) {
ret = Math.max(ret, dp[i]); ret = Math.max(ret, dp[i]);
} }
return ret; return ret;
} }
``` ```
以上解法的时间复杂度为 O(n<sup>2</sup>) ,可以使用二分查找使得时间复杂度降低为 O(nlogn)。定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素,例如对于数组 [4,5,6,3],有 以上解法的时间复杂度为 O(N<sup>2</sup>) ,可以使用二分查找将时间复杂度降低为 O(NlogN)。定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。如果有多个长度相等的最长递增子序列,那么 tails[i] 就取最小值。例如对于数组 [4,5,6,3],有
```html ```html
len = 1 : [4], [5], [6], [3] => tails[0] = 3 len = 1 : [4], [5], [6], [3] => tails[0] = 3
...@@ -1973,6 +1974,7 @@ len = 2 : [4, 5], [5, 6] => tails[1] = 5 ...@@ -1973,6 +1974,7 @@ len = 2 : [4, 5], [5, 6] => tails[1] = 5
len = 3 : [4, 5, 6] => tails[2] = 6 len = 3 : [4, 5, 6] => tails[2] = 6
``` ```
对于一个元素 x, 对于一个元素 x,
- 如果它大于 tails 数组所有的值,那么把它添加到 tails 后面,表示最长递增子序列长度加 1; - 如果它大于 tails 数组所有的值,那么把它添加到 tails 后面,表示最长递增子序列长度加 1;
...@@ -2094,7 +2096,7 @@ public int wiggleMaxLength(int[] nums) { ...@@ -2094,7 +2096,7 @@ public int wiggleMaxLength(int[] nums) {
- 针对的是两个序列,求它们的最长公共子序列。 - 针对的是两个序列,求它们的最长公共子序列。
- 在最长递增子序列中,dp[i] 表示以 S<sub>i</sub> 为结尾的最长递增子序列长度,子序列必须包含 S<sub>i</sub> ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1<sub>i</sub> 和 S2<sub>j</sub> - 在最长递增子序列中,dp[i] 表示以 S<sub>i</sub> 为结尾的最长递增子序列长度,子序列必须包含 S<sub>i</sub> ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1<sub>i</sub> 和 S2<sub>j</sub>
- 由于 2 ,在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 S<sub>N</sub> 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。 - 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 S<sub>N</sub> 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。
```java ```java
public int lengthOfLCS(int[] nums1, int[] nums2) { public int lengthOfLCS(int[] nums1, int[] nums2) {
...@@ -2114,7 +2116,7 @@ public int lengthOfLCS(int[] nums1, int[] nums2) { ...@@ -2114,7 +2116,7 @@ public int lengthOfLCS(int[] nums1, int[] nums2) {
有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。 有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示体积不超过 j 的情况下,前 i 件物品能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论: 定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:
- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j] - 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]
- 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。 - 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。
...@@ -2306,9 +2308,7 @@ public int findTargetSumWays(int[] nums, int S) { ...@@ -2306,9 +2308,7 @@ public int findTargetSumWays(int[] nums, int S) {
} }
private int findTargetSumWays(int[] nums, int start, int S) { private int findTargetSumWays(int[] nums, int start, int S) {
if (start == nums.length) { if (start == nums.length) return S == 0 ? 1 : 0;
return S == 0 ? 1 : 0;
}
return findTargetSumWays(nums, start + 1, S + nums[start]) + findTargetSumWays(nums, start + 1, S - nums[start]); return findTargetSumWays(nums, start + 1, S + nums[start]) + findTargetSumWays(nums, start + 1, S - nums[start]);
} }
``` ```
...@@ -2362,7 +2362,7 @@ return -1. ...@@ -2362,7 +2362,7 @@ return -1.
题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。 题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。
这是一个完全背包问题。 这是一个完全背包问题,完全背包问题和 0-1 背包问题在实现上的区别在于,0-1 背包遍历物品的循环在外侧,而完全背包问题遍历物品的循环在内侧,在内侧体现出物品可以使用多次
```java ```java
public int coinChange(int[] coins, int amount) { public int coinChange(int[] coins, int amount) {
...@@ -2371,7 +2371,7 @@ public int coinChange(int[] coins, int amount) { ...@@ -2371,7 +2371,7 @@ public int coinChange(int[] coins, int amount) {
Arrays.fill(dp, amount + 1); Arrays.fill(dp, amount + 1);
dp[0] = 0; dp[0] = 0;
for (int i = 1; i <= amount; i++) { for (int i = 1; i <= amount; i++) {
for (int c : coins) { for (int c : coins) { // 硬币可以使用多次
if (c <= i) { if (c <= i) {
dp[i] = Math.min(dp[i], dp[i - c] + 1); dp[i] = Math.min(dp[i], dp[i - c] + 1);
} }
...@@ -2584,10 +2584,31 @@ public int minDistance(String word1, String word2) { ...@@ -2584,10 +2584,31 @@ public int minDistance(String word1, String word2) {
} }
``` ```
**修改一个字符串为另一个字符串** **修改一个字符串为另一个字符串**
[Leetcode : 72. Edit Distance (Hard)](https://leetcode.com/problems/edit-distance/description/) [Leetcode : 72. Edit Distance (Hard)](https://leetcode.com/problems/edit-distance/description/)
```html
Example 1:
Input: word1 = "horse", word2 = "ros"
Output: 3
Explanation:
horse -> rorse (replace 'h' with 'r')
rorse -> rose (remove 'r')
rose -> ros (remove 'e')
Example 2:
Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation:
intention -> inention (remove 't')
inention -> enention (replace 'i' with 'e')
enention -> exention (replace 'n' with 'x')
exention -> exection (replace 'n' with 'c')
exection -> execution (insert 'u')
```
```java ```java
public int minDistance(String word1, String word2) { public int minDistance(String word1, String word2) {
if (word1 == null || word2 == null) { if (word1 == null || word2 == null) {
...@@ -2646,12 +2667,12 @@ public int numSquares(int n) { ...@@ -2646,12 +2667,12 @@ public int numSquares(int n) {
List<Integer> squareList = generateSquareList(n); List<Integer> squareList = generateSquareList(n);
int[] dp = new int[n + 1]; int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) { for (int i = 1; i <= n; i++) {
int max = Integer.MAX_VALUE; int min = Integer.MAX_VALUE;
for (int square : squareList) { for (int square : squareList) {
if (square > i) break; if (square > i) break;
max = Math.min(max, dp[i - square] + 1); min = Math.min(min, dp[i - square] + 1);
} }
dp[i] = max; dp[i] = min;
} }
return dp[n]; return dp[n];
} }
...@@ -2767,7 +2788,7 @@ public int minPathSum(int[][] grid) { ...@@ -2767,7 +2788,7 @@ public int minPathSum(int[][] grid) {
题目描述:交易之后需要有一天的冷却时间。 题目描述:交易之后需要有一天的冷却时间。
<div align="center"> <img src="../pics//f4cdda3e-324c-49b5-8c14-08a3db634b29.png"/> </div><br> <div align="center"> <img src="../pics//a3da4342-078b-43e2-b748-7e71bec50dc4.png"/> </div><br>
```java ```java
public int maxProfit(int[] prices) { public int maxProfit(int[] prices) {
...@@ -2806,7 +2827,7 @@ The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8. ...@@ -2806,7 +2827,7 @@ The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
题目描述:每交易一次,都要支付一定的费用。 题目描述:每交易一次,都要支付一定的费用。
<div align="center"> <img src="../pics//6f4abf41-3728-4a6b-9b94-85eed7ca8163.png"/> </div><br> <div align="center"> <img src="../pics//61942711-45a0-4e11-bbc9-434e31436f33.png"/> </div><br>
```java ```java
public int maxProfit(int[] prices, int fee) { public int maxProfit(int[] prices, int fee) {
...@@ -2833,7 +2854,7 @@ public int maxProfit(int[] prices, int fee) { ...@@ -2833,7 +2854,7 @@ public int maxProfit(int[] prices, int fee) {
只进行一次交易。 只进行一次交易。
只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看这个价格是否是当前的最大价格 只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益
```java ```java
public int maxProfit(int[] prices) { public int maxProfit(int[] prices) {
......
...@@ -50,8 +50,11 @@ ...@@ -50,8 +50,11 @@
* [九、进程管理](#九进程管理) * [九、进程管理](#九进程管理)
* [查看进程](#查看进程) * [查看进程](#查看进程)
* [进程状态](#进程状态) * [进程状态](#进程状态)
* [SIGCHILD](#sigchild) * [SIGCHLD](#sigchld)
* [孤儿进程和僵死进程](#孤儿进程和僵死进程) * [wait()](#wait)
* [waitpid()](#waitpid)
* [孤儿进程](#孤儿进程)
* [僵死进程](#僵死进程)
* [十、I/O 复用](#十io-复用) * [十、I/O 复用](#十io-复用)
* [概念理解](#概念理解) * [概念理解](#概念理解)
* [I/O 模型](#io-模型) * [I/O 模型](#io-模型)
...@@ -1061,7 +1064,6 @@ daemon 2 ...@@ -1061,7 +1064,6 @@ daemon 2
<div align="center"> <img src="../pics//76a49594323247f21c9b3a69945445ee.png" width=""/> </div><br> <div align="center"> <img src="../pics//76a49594323247f21c9b3a69945445ee.png" width=""/> </div><br>
| 状态 | 说明 | | 状态 | 说明 |
| :---: | --- | | :---: | --- |
| R | running or runnable (on run queue) | | R | running or runnable (on run queue) |
...@@ -1070,26 +1072,56 @@ daemon 2 ...@@ -1070,26 +1072,56 @@ daemon 2
| Z | defunct/zombie, terminated but not reaped by its parent | | Z | defunct/zombie, terminated but not reaped by its parent |
| T | stopped, either by a job control signal or because it is being traced| | T | stopped, either by a job control signal or because it is being traced|
## SIGCHILD ## SIGCHLD
当一个子进程改变了它的状态时:停止运行,继续运行或者退出,有两件事会发生在父进程中: 当一个子进程改变了它的状态时:停止运行,继续运行或者退出,有两件事会发生在父进程中:
- 得到 SIGCHLD 信号; - 得到 SIGCHLD 信号;
- 阻塞的 waitpid(2)(或者 wait)调用会返回。 - waitpid() 或者 wait() 调用会返回。
<div align="center"> <img src="../pics//flow.png" width=""/> </div><br> <div align="center"> <img src="../pics//flow.png" width=""/> </div><br>
## 孤儿进程和僵死进程 其中子进程发送的 SIGCHLD 信号包含了子进程的信息,包含了进程 ID、进程状态、进程使用 CPU 的时间等。
在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息。父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。
## wait()
```c
pid_t wait(int *status)
```
父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,之后 wait() 函数会销毁子进程并返回。
如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 - 1,同时 errno 被置为 ECHILD。
参数 status 用来保存被收集进程退出时的一些状态,如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为 NULL:
```c
pid = wait(NULL);
```
### 1. 孤儿进程 ## 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 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。
由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。 由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。
### 2. 僵死进程 ## 僵死进程
一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait 或 waitpid 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait 或 waitpid,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。 一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。
僵死进程通过 ps 命令显示出来的状态为 Z。 僵死进程通过 ps 命令显示出来的状态为 Z。
...@@ -1118,7 +1150,8 @@ I/O Multiplexing 又被称为 Event Driven I/O,它可以让单个进程具有 ...@@ -1118,7 +1150,8 @@ I/O Multiplexing 又被称为 Event Driven I/O,它可以让单个进程具有
同步异步是获知 I/O 完成的方式,同步需要时刻关心 I/O 是否已经完成,异步无需主动关心,在 I/O 完成时它会收到通知。 同步异步是获知 I/O 完成的方式,同步需要时刻关心 I/O 是否已经完成,异步无需主动关心,在 I/O 完成时它会收到通知。
<div align="center"> <img src="../pics//54cb3f21-485b-4159-8bf5-dcde1c4d4c36.png" width=""/> </div><br> <div align="center"> <img src="../pics//1a231f2a-5c2f-4231-8e0f-915aa5894347.jpg"/> </div><br>
### 1. 同步-阻塞 ### 1. 同步-阻塞
...@@ -1136,13 +1169,7 @@ I/O Multiplexing 又被称为 Event Driven I/O,它可以让单个进程具有 ...@@ -1136,13 +1169,7 @@ I/O Multiplexing 又被称为 Event Driven I/O,它可以让单个进程具有
<div align="center"> <img src="../pics//1582217a-ed46-4cac-811e-90d13a65163b.png" width=""/> </div><br> <div align="center"> <img src="../pics//1582217a-ed46-4cac-811e-90d13a65163b.png" width=""/> </div><br>
### 3. 异步-阻塞 ### 3. 异步
这是 I/O 复用使用的一种模式,通过使用 select,它可以监听多个 I/O 事件,当这些事件至少有一个发生时,用户程序会收到通知。
<div align="center"> <img src="../pics//dbc5c9f1-c13c-4d06-86ba-7cc949eb4c8f.jpg" width=""/> </div><br>
### 4. 异步-非阻塞
该模式下,I/O 操作会立即返回,之后可以处理其它操作,并且在 I/O 完成时会收到一个通知,此时会中断正在处理的操作,然后继续之前的操作。 该模式下,I/O 操作会立即返回,之后可以处理其它操作,并且在 I/O 完成时会收到一个通知,此时会中断正在处理的操作,然后继续之前的操作。
...@@ -1397,3 +1424,4 @@ poll 没有最大描述符数量的限制,如果平台支持应该采用 poll ...@@ -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 之守护进程、僵死进程与孤儿进程](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/) - [Linux process states](https://idea.popcount.org/2012-12-11-linux-process-states/)
- [GUID Partition Table](https://en.wikipedia.org/wiki/GUID_Partition_Table) - [GUID Partition Table](https://en.wikipedia.org/wiki/GUID_Partition_Table)
- [详解 wait 和 waitpid 函数](https://blog.csdn.net/kevinhg/article/details/7001719)
...@@ -30,31 +30,27 @@ ...@@ -30,31 +30,27 @@
InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。 InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。
采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读。 采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ),并且通过间隙锁(next-key locking)策略防止幻读的出现。间隙锁使得 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入
表是基于聚簇索引建立的,它对主键的查询性能有很高的提升。 表是基于聚簇索引建立的,它对主键的查询性能有很高的提升。
内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够自动在内存中创建哈希索引以加速读操作的自适应哈希索引、能够加速插入操作的插入缓冲区等。 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够自动在内存中创建哈希索引以加速读操作的自适应哈希索引、能够加速插入操作的插入缓冲区等。
通过一些机制和工具支持真正的热备份。 通过一些机制和工具支持真正的热备份,其它存储引擎不支持热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取
## MyISAM ## MyISAM
MyISAM 提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等。但 MyISAM 不支持事务和行级锁,而且崩溃后无法安全恢复 提供了大量的特性,包括全文索引、压缩表、空间数据索引等。应该注意的是,MySQL 5.6.4 添加了对 InnoDB 引擎的全文索引支持
只能对整张表加锁,而不是针对行 不支持事务
可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的 不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取查询的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)
可以包含动态或者静态的行 可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的
如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。 如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。
如果表在创建并导入数据以后,不会再进行修改操作,那么这样的表适合采用 MyISAM 压缩表。 MyISAM 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以继续使用 MyISAM。
对于只读数据,或者表比较小、可以容忍修复操作,则依然可以继续使用 MyISAM。
MyISAM 设计简单,数据以紧密格式存储,所以在某些场景下性能很好。
## 比较 ## 比较
...@@ -62,7 +58,7 @@ MyISAM 设计简单,数据以紧密格式存储,所以在某些场景下性 ...@@ -62,7 +58,7 @@ MyISAM 设计简单,数据以紧密格式存储,所以在某些场景下性
2. 备份:InnoDB 支持在线热备份。 2. 备份:InnoDB 支持在线热备份。
3. 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 3. 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。
4. 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 4. 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
5. 其它特性:MyISAM 支持全文索引,地理空间索引。 5. 其它特性:MyISAM 支持压缩表和空间数据索引。
# 二、数据类型 # 二、数据类型
...@@ -146,12 +142,14 @@ InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个 ...@@ -146,12 +142,14 @@ InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个
限制:哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;无法用于分组与排序;只支持精确查找,无法用于部分查找和范围查找;如果哈希冲突很多,查找速度会变得很慢。 限制:哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;无法用于分组与排序;只支持精确查找,无法用于部分查找和范围查找;如果哈希冲突很多,查找速度会变得很慢。
### 3. 空间索引(R-Tree) ### 3. 空间数据索引(R-Tree)
MyISAM 存储引擎支持空间索引,可以用于地理数据存储。 MyISAM 存储引擎支持空间索引,可以用于地理数据存储。
空间索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 空间索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。
必须使用 GIS 相关的函数来维护数据。
### 4. 全文索引 ### 4. 全文索引
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较索引中的值。 MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较索引中的值。
...@@ -162,9 +160,9 @@ 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 ...@@ -216,14 +214,14 @@ customer_id_selectivity: 0.0373
聚簇索引并不是一种索引类型,而是一种数据存储方式。 聚簇索引并不是一种索引类型,而是一种数据存储方式。
术语“聚簇”表示数据行和相邻的键值紧密地存储在一起,InnoDB 的聚簇索引的数据行存放在 B+Tree 的叶子页中 术语“聚簇”表示数据行和相邻的键值紧密地存储在一起,InnoDB 的聚簇索引在同一个结构中保存了 B+Tree 索引和数据行
因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。 因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
**优点** **优点**
1. 可以把相关数据保存在一起,减少 I/O 操作 1. 可以把相关数据保存在一起,减少 I/O 操作。例如电子邮件表可以根据用户 ID 来聚集数据,这样只需要从磁盘读取少数的数据也就能获取某个用户的全部邮件,如果没有使用聚聚簇索引,则每封邮件都可能导致一次磁盘 I/O。
2. 因为数据保存在 B+Tree 中,因此数据访问更快。 2. 数据访问更快。
**缺点** **缺点**
...@@ -249,15 +247,15 @@ customer_id_selectivity: 0.0373 ...@@ -249,15 +247,15 @@ customer_id_selectivity: 0.0373
<div align="center"> <img src="../pics//5ed71283-a070-4b21-85ae-f2cbfd6ba6e1.jpg"/> </div><br> <div align="center"> <img src="../pics//5ed71283-a070-4b21-85ae-f2cbfd6ba6e1.jpg"/> </div><br>
为了描述 B-Tree,首先定义一条数据记录为一个二元组 [key, data],key 为记录的键,data 为数据记录除 key 外的数据 为了描述 B-Tree,首先定义一条数据记录为一个二元组 [key, data]。
B-Tree 是满足下列条件的数据结构: B-Tree 是满足下列条件的数据结构:
- 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的; - 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的;
- 一个节点中的 key 从左到右非递减排列; - 一个节点中的 key 从左到右非递减排列;
- 如果某个指针的左右相邻 key 分别是 key<sub>i</sub> 和 key<sub>i+1</sub>,且不为 null,则该指针指向节点的所有 key 大于 key<sub>i</sub> 且小于 key<sub>i+1</sub> - 如果某个指针的左右相邻 key 分别是 key<sub>i</sub> 和 key<sub>i+1</sub>,且不为 null,则该指针指向节点的所有 key 大于等于 key<sub>i</sub> 且小于等于 key<sub>i+1</sub>
在 B-Tree 中按 key 检索数据的算法非常直观:首先从根节点进行二分查找,如果找到则返回对应节点的 data,否则对相应区间的指针指向的节点递归进行查找,直到找到节点或找到 null 指针,前者查找成功,后者查找失败 在 B-Tree 中按 key 检索数据的算法非常直观:首先在根节点进行二分查找,如果找到则返回对应节点的 data,否则在相应区间的指针指向的节点递归进行查找
由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。 由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。
...@@ -282,7 +280,9 @@ B-Tree 是满足下列条件的数据结构: ...@@ -282,7 +280,9 @@ B-Tree 是满足下列条件的数据结构:
页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页的大小通常为 4k),主存和磁盘以页为单位交换数据。 页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页的大小通常为 4k),主存和磁盘以页为单位交换数据。
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。B-Tree 中一次检索最多需要 h-1 次 I/O(根节点常驻内存),渐进复杂度为 O(h)=O(log<sub>d</sub>N)。一般实际应用中,出度 d 是非常大的数字,通常超过 100,因此 h 非常小(通常不超过 3)。而红黑树这种结构,h 明显要深的多。并且于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,效率明显比 B-Tree 差很多。 一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。
B-Tree 中一次检索最多需要 h-1 次 I/O(根节点常驻内存),渐进复杂度为 O(h)=O(log<sub>d</sub>N)。一般实际应用中,出度 d 是非常大的数字,通常超过 100,因此 h 非常小(通常不超过 3)。而红黑树这种结构,h 明显要深的多。并且于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,效率明显比 B-Tree 差很多。
B+Tree 更适合外存索引,原因和内节点出度 d 有关。由于 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,拥有更好的性能。 B+Tree 更适合外存索引,原因和内节点出度 d 有关。由于 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,拥有更好的性能。
......
...@@ -427,7 +427,7 @@ Redis 这种内存数据库能支持计数器频繁的读写操作。 ...@@ -427,7 +427,7 @@ Redis 这种内存数据库能支持计数器频繁的读写操作。
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | |volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 | | allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 | | allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
| no-envicition | 禁止驱逐数据 | | noeviction | 禁止驱逐数据 |
如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
......
...@@ -196,7 +196,7 @@ WHERE col IS NULL; ...@@ -196,7 +196,7 @@ WHERE col IS NULL;
应该注意到,NULL 与 0 、空字符串都不同。 应该注意到,NULL 与 0 、空字符串都不同。
**AND OR** 用于连接多个过滤条件。优先处理 AND,因此当一个过滤表达式涉及到多个 AND 和 OR 时,应当使用 () 来决定优先级 **AND OR** 用于连接多个过滤条件,优先处理 AND,当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰
**IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。 **IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。
...@@ -246,14 +246,14 @@ FROM mytable ...@@ -246,14 +246,14 @@ FROM mytable
## 文本处理 ## 文本处理
| 函数 | 说明 | | 函数 | 说明 |
| ------------ | ------------ | | :---: | :---: |
| LEFT() RIGHT() | 左边或者右边的字符 | | LEFT() RIGHT() | 左边或者右边的字符 |
| LOWER() UPPER() | 转换为小写或者大写 | | LOWER() UPPER() | 转换为小写或者大写 |
| LTRIM() RTIM() | 去除左边或者右边的空格 | | LTRIM() RTIM() | 去除左边或者右边的空格 |
| LENGTH() | 长度 | | LENGTH() | 长度 |
| SUNDEX() | 转换为语音值 | | SOUNDEX() | 转换为语音值 |
其中, **SOUNDEX()** 是将一个字符串转换为描述其语音表示的字母数字模式的算法,它是根据发音而不是字母比较 其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式
```sql ```sql
SELECT * SELECT *
...@@ -267,7 +267,7 @@ WHERE SOUNDEX(col1) = SOUNDEX('apple') ...@@ -267,7 +267,7 @@ WHERE SOUNDEX(col1) = SOUNDEX('apple')
- 时间格式:HH:MM:SS - 时间格式:HH:MM:SS
|函 数 | 说 明| |函 数 | 说 明|
| --- | --- | | :---: | :---: |
| AddDate() | 增加一个日期(天、周等)| | AddDate() | 增加一个日期(天、周等)|
| AddTime() | 增加一个时间(时、分等)| | AddTime() | 增加一个时间(时、分等)|
| CurDate() | 返回当前日期 | | CurDate() | 返回当前日期 |
...@@ -288,13 +288,16 @@ WHERE SOUNDEX(col1) = SOUNDEX('apple') ...@@ -288,13 +288,16 @@ WHERE SOUNDEX(col1) = SOUNDEX('apple')
```sql ```sql
mysql> SELECT NOW(); mysql> SELECT NOW();
-> '2017-06-28 14:01:52' ```
```
2018-4-14 20:25:11
``` ```
## 数值处理 ## 数值处理
| 函数 | 说明 | | 函数 | 说明 |
| --- | --- | | :---: | :---: |
| SIN() | 正弦 | | SIN() | 正弦 |
| COS() | 余弦 | | COS() | 余弦 |
| TAN() | 正切 | | TAN() | 正切 |
...@@ -308,7 +311,7 @@ mysql> SELECT NOW(); ...@@ -308,7 +311,7 @@ mysql> SELECT NOW();
## 汇总 ## 汇总
|函 数 |说 明| |函 数 |说 明|
| --- | --- | | :---: | :---: |
| AVG() | 返回某列的平均值 | | AVG() | 返回某列的平均值 |
| COUNT() | 返回某列的行数 | | COUNT() | 返回某列的行数 |
| MAX() | 返回某列的最大值 | | MAX() | 返回某列的最大值 |
...@@ -330,7 +333,7 @@ FROM mytable ...@@ -330,7 +333,7 @@ FROM mytable
可以对同一分组数据使用汇总函数进行处理,例如求分组数据的平均值等。 可以对同一分组数据使用汇总函数进行处理,例如求分组数据的平均值等。
指定的分组字段除了能按该字段进行分组,也可以按该字段进行排序,例如按 col 字段排序并分组数据: 指定的分组字段除了能按该字段进行分组,也会自动按按该字段进行排序。
```sql ```sql
SELECT col, COUNT(*) AS num SELECT col, COUNT(*) AS num
...@@ -338,7 +341,7 @@ FROM mytable ...@@ -338,7 +341,7 @@ FROM mytable
GROUP BY col; GROUP BY col;
``` ```
GROUP BY 是按照分组字段进行排序,ORDER BY 也可以以汇总字段来进行排序。 GROUP BY 分组字段进行排序,ORDER BY 也可以以汇总字段来进行排序。
```sql ```sql
SELECT col, COUNT(*) AS num SELECT col, COUNT(*) AS num
...@@ -347,14 +350,14 @@ GROUP BY col ...@@ -347,14 +350,14 @@ GROUP BY col
ORDER BY num; ORDER BY num;
``` ```
WHERE 过滤行,HAVING 过滤分组。行过滤应当先与分组过滤; WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤。
```sql ```sql
SELECT col, COUNT(*) AS num SELECT col, COUNT(*) AS num
FROM mytable FROM mytable
WHERE col > 2 WHERE col > 2
GROUP BY col GROUP BY col
HAVING COUNT(*) >= 2; HAVING num >= 2;
``` ```
分组规定: 分组规定:
...@@ -436,10 +439,10 @@ where department = ( ...@@ -436,10 +439,10 @@ where department = (
自连接版本 自连接版本
```sql ```sql
select e2.name select e1.name
from employee as e1, employee as e2 from employee as e1, employee as e2
where e1.department = e2.department where e1.department = e2.department
and e1.name = "Jim"; and e2.name = "Jim";
``` ```
连接一般比子查询的效率高。 连接一般比子查询的效率高。
...@@ -464,7 +467,7 @@ from employee natural join department; ...@@ -464,7 +467,7 @@ from employee natural join department;
```sql ```sql
select Customers.cust_id, Orders.order_num select Customers.cust_id, Orders.order_num
from Customers left outer join Orders 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; ...@@ -473,15 +476,15 @@ on Customers.cust_id = Orders.curt_id;
select Customers.cust_id, select Customers.cust_id,
COUNT(Orders.order_num) as num_ord COUNT(Orders.order_num) as num_ord
from Customers left outer join Orders 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; group by Customers.cust_id;
``` ```
# 十六、组合查询 # 十六、组合查询
使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果为 M+N 行。 使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。
每个查询必须包含相同的列、表达式或者聚集函数。 每个查询必须包含相同的列、表达式聚集函数。
默认会去除相同行,如果需要保留相同行,使用 UNION ALL。 默认会去除相同行,如果需要保留相同行,使用 UNION ALL。
...@@ -622,7 +625,7 @@ MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储 ...@@ -622,7 +625,7 @@ MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储
不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。 不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。
MySQL 的事务提交默认是隐式提交,也就是每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。 MySQL 的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
通过设置 autocommit 为 0 可以取消自动提交,直到 autocommit 被设置为 1 才会提交;autocommit 标记是针对每个连接而不是针对服务器的。 通过设置 autocommit 为 0 可以取消自动提交,直到 autocommit 被设置为 1 才会提交;autocommit 标记是针对每个连接而不是针对服务器的。
......
...@@ -98,16 +98,18 @@ ...@@ -98,16 +98,18 @@
以 (2, 3, 1, 0, 2, 5) 为例: 以 (2, 3, 1, 0, 2, 5) 为例:
```html ```text-html-basic
position-0 : (2,3,1,0,2,5) // 2 <-> 1 position-0 : (2,3,1,0,2,5) // 2 <-> 1
(1,3,2,0,2,5) // 1 <-> 3 (1,3,2,0,2,5) // 1 <-> 3
(3,1,1,0,2,5) // 3 <-> 0 (3,1,2,0,2,5) // 3 <-> 0
(0,1,1,3,2,5) // already in position (0,1,2,3,2,5) // already in position
position-1 : (0,1,1,3,2,5) // already in position position-1 : (0,1,2,3,2,5) // already in position
position-2 : (0,1,1,3,2,5) // nums[i] == nums[nums[i]], exit 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) 复杂度:O(N) + O(1)
...@@ -1986,6 +1988,7 @@ public int longestSubStringWithoutDuplication(String str) { ...@@ -1986,6 +1988,7 @@ public int longestSubStringWithoutDuplication(String str) {
int curLen = 0; int curLen = 0;
int maxLen = 0; int maxLen = 0;
int[] indexs = new int[26]; int[] indexs = new int[26];
Arrays.fill(indexs, -1);
for (int i = 0; i < str.length(); i++) { for (int i = 0; i < str.length(); i++) {
int c = str.charAt(i) - 'a'; int c = str.charAt(i) - 'a';
int preIndex = indexs[c]; int preIndex = indexs[c];
...@@ -2010,21 +2013,19 @@ public int longestSubStringWithoutDuplication(String str) { ...@@ -2010,21 +2013,19 @@ public int longestSubStringWithoutDuplication(String str) {
## 解题思路 ## 解题思路
```java ```java
public int GetUglyNumber_Solution(int N) { public int GetUglyNumber_Solution(int index) {
if (N <= 6) return N; if (index <= 6) return index;
int i2 = 0, i3 = 0, i5 = 0; int i2 = 0, i3 = 0, i5 = 0;
int cnt = 1; int[] dp = new int[index];
int[] dp = new int[N];
dp[0] = 1; dp[0] = 1;
while (cnt < N) { for (int i = 1; i < index; i++) {
int n2 = dp[i2] * 2, n3 = dp[i3] * 3, n5 = dp[i5] * 5; int n2 = dp[i2] * 2, n3 = dp[i3] * 3, n5 = dp[i5] * 5;
int min = Math.min(n2, Math.min(n3, n5)); dp[i] = Math.min(n2, Math.min(n3, n5));
dp[cnt++] = min; if (dp[i] == n2) i2++;
if (min == n2) i2++; if (dp[i] == n3) i3++;
if (min == n3) i3++; if (dp[i] == n5) i5++;
if (min == n5) i5++;
} }
return dp[N - 1]; return dp[index - 1];
} }
``` ```
......
...@@ -94,7 +94,7 @@ T<sub>2</sub> 读取一个数据,T<sub>1</sub> 对该数据做了修改。如 ...@@ -94,7 +94,7 @@ T<sub>2</sub> 读取一个数据,T<sub>1</sub> 对该数据做了修改。如
T<sub>1</sub> 读取某个范围的数据,T<sub>2</sub> 在这个范围内插入新的数据,T<sub>1</sub> 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 T<sub>1</sub> 读取某个范围的数据,T<sub>2</sub> 在这个范围内插入新的数据,T<sub>1</sub> 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
<div align="center"> <img src="../pics//688dacfe-1057-412f-b3a1-86abb5b0f914.png"/> </div><br> <div align="center"> <img src="../pics//72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png"/> </div><br>
## 解决方法 ## 解决方法
...@@ -110,11 +110,13 @@ T<sub>1</sub> 读取某个范围的数据,T<sub>2</sub> 在这个范围内插 ...@@ -110,11 +110,13 @@ T<sub>1</sub> 读取某个范围的数据,T<sub>2</sub> 在这个范围内插
<div align="center"> <img src="../pics//1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg" width="300"/> </div><br> <div align="center"> <img src="../pics//1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg" width="300"/> </div><br>
MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。 应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。
但是加锁需要消耗资源,锁的各种操作,包括获取锁,检查锁是否已经解除、释放锁,都会增加系统开销。因此封锁粒度越小,系统开销就越大。需要在锁开销以及数据安全性之间做一个权衡。 但是加锁需要消耗资源,锁的各种操作,包括获取锁,检查锁是否已经解除、释放锁,都会增加系统开销。因此封锁粒度越小,系统开销就越大。
MySQL 中提供了两种封锁粒度:行级锁以及表级锁 在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡
## 封锁类型 ## 封锁类型
...@@ -227,13 +229,13 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 ...@@ -227,13 +229,13 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
事务遵循两段锁协议是保证并发操作可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。 事务遵循两段锁协议是保证并发操作可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。
```html ```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 ```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 日志中,该日志通过回 ...@@ -289,33 +291,31 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回
### 1. SELECT ### 1. SELECT
该操作必须保证多个事务读取到同一个数据行的快照,这个快照是最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。 当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。
多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
把没对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。 把没对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。
除了上面的要求,T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。 除了上面的要求,T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
### 2. INSERT ### 2. INSERT
将系统版本号作为数据行快照的创建版本号。 当前系统版本号作为数据行快照的创建版本号。
### 3. DELETE ### 3. DELETE
将系统版本号作为数据行快照的删除版本号。 当前系统版本号作为数据行快照的删除版本号。
### 4. UPDATE ### 4. UPDATE
系统版本号作为更新后的数据行快照的创建版本号,同时将系统版本号作为作为更新前的数据行快照的删除版本号。可以理解为先执行 DELETE 后执行 INSERT。 当前系统版本号作为更新后的数据行快照的创建版本号,同时将当前系统版本号作为更新前的数据行快照的删除版本号。可以理解为先执行 DELETE 后执行 INSERT。
## 快照读与当前读 ## 快照读与当前读
### 1. 快照读 ### 1. 快照读
读取快照中的数据。 读取快照中的数据,可以减少加锁所带来的开销。
引入快照读的目的主要是为了免去加锁操作带来的性能开销,但是当前读需要加锁。
```sql ```sql
select * from table ....; select * from table ....;
...@@ -323,21 +323,19 @@ select * from table ....; ...@@ -323,21 +323,19 @@ select * from table ....;
### 2. 当前读 ### 2. 当前读
读取最新的数据。 读取最新的数据,需要加锁。以下第一个语句需要加 S 锁,其它都需要加 X 锁。
需要加锁,以下第一个语句加 S 锁,其它都加 X 锁。
```sql ```sql
select * from table where ? lock in share mode; select * from table where ? lock in share mode;
select * from table where ? for update; select * from table where ? for update;
insert; insert;
update ; update;
delete; delete;
``` ```
# 六、Next-Key Locks # 六、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 ## Record Locks
...@@ -383,7 +381,7 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; ...@@ -383,7 +381,7 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
记 A->B 表示 A 函数决定 B,也可以说 B 函数依赖于 A。 记 A->B 表示 A 函数决定 B,也可以说 B 函数依赖于 A。
如果 {A1,A2,... ,An} 是关系的一个或多个属性的集合,该集合决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。 如果 {A1,A2,... ,An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。
对于 W->A,如果能找到 W 的真子集 W',使得 W'-> A,那么 W->A 就是部分函数依赖,否则就是完全函数依赖; 对于 W->A,如果能找到 W 的真子集 W',使得 W'-> A,那么 W->A 就是部分函数依赖,否则就是完全函数依赖;
...@@ -399,9 +397,9 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; ...@@ -399,9 +397,9 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
不符合范式的关系,会产生很多异常,主要有以下四种异常: 不符合范式的关系,会产生很多异常,主要有以下四种异常:
1. 冗余数据,例如学生-2 出现了两次。 1. 冗余数据:例如 学生-2 出现了两次。
2. 修改异常修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。 2. 修改异常修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。
3. 删除异常,删除一个信息,那么也会丢失其它信息。例如如果删除了课程-1,需要删除第二行和第三行,那么学生-1 的信息就会丢失。 3. 删除异常:删除一个信息,那么也会丢失其它信息。例如如果删除了 课程-1,需要删除第一行和第三行,那么 学生-1 的信息就会丢失。
4. 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。 4. 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
## 范式 ## 范式
...@@ -431,14 +429,14 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; ...@@ -431,14 +429,14 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
以上学生课程关系中,{Sno, Cname} 为键码,有如下函数依赖: 以上学生课程关系中,{Sno, Cname} 为键码,有如下函数依赖:
- Sno, Cname -> Sname, Sdept, Mname - Sno, Cname -> Sname, Sdept, Mname
- Son -> Sname, Sdept - Sno -> Sname, Sdept
- Sdept -> Mname - Sdept -> Mname
- Sno -> Manme - Sno -> Mname
- Sno, Cname-> Grade - Sno, Cname-> Grade
Grade 完全函数依赖于键码,它没有任何冗余数据,每个学生的每门课都有特定的成绩。 Grade 完全函数依赖于键码,它没有任何冗余数据,每个学生的每门课都有特定的成绩。
Sname, Sdept 和 Manme 都函数依赖于 Sno,而部分依赖于键码。当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。 Sname, Sdept 和 Mname 都函数依赖于 Sno,而部分依赖于键码。当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。
<font size=4> **分解后** </font><br> <font size=4> **分解后** </font><br>
...@@ -470,7 +468,7 @@ Sname, Sdept 和 Manme 都函数依赖于 Sno,而部分依赖于键码。当 ...@@ -470,7 +468,7 @@ Sname, Sdept 和 Manme 都函数依赖于 Sno,而部分依赖于键码。当
非主属性不传递依赖于键码。 非主属性不传递依赖于键码。
上面的关系-1 中存在以下传递依赖:Sno -> Sdept -> Mname,可以进行以下分解: 上面的 关系-1 中存在以下传递依赖:Sno -> Sdept -> Mname,可以进行以下分解:
关系-11 关系-11
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
正则表达式内置于其它语言或者软件产品中,它本身不是一种语言或者软件。 正则表达式内置于其它语言或者软件产品中,它本身不是一种语言或者软件。
[正则表达式在线工具](http://tool.oschina.net/regex/) [正则表达式在线工具](https://regexr.com/)
# 二、匹配单个字符 # 二、匹配单个字符
...@@ -164,7 +164,7 @@ a.+c ...@@ -164,7 +164,7 @@ a.+c
^ 元字符在字符集合中用作求非,在字符集合外用作匹配字符串的开头。 ^ 元字符在字符集合中用作求非,在字符集合外用作匹配字符串的开头。
使用 (?m) 来打开分行匹配模式,在该模式下,换行被当做字符串的边界。 分行匹配模式(multiline)下,换行被当做字符串的边界。
**应用** **应用**
...@@ -173,10 +173,10 @@ a.+c ...@@ -173,10 +173,10 @@ a.+c
**正则表达式** **正则表达式**
``` ```
(?m)^\s*//.*$ ^\s*\/\/.*$
``` ```
如果没用 (?m),则只会匹配 // 注释 1 以及之后的所有内容。用了分行匹配模式之后,换行符被当成是字符串分隔符,因此能正确匹配出两个注释内容。 <div align="center"> <img src="../pics//600e9c75-5033-4dad-ae2b-930957db638e.png"/> </div><br>
**匹配结果** **匹配结果**
...@@ -197,7 +197,7 @@ a.+c ...@@ -197,7 +197,7 @@ a.+c
**正则表达式** **正则表达式**
``` ```
(ab) {2,} (ab){2,}
``` ```
**匹配结果** **匹配结果**
...@@ -222,21 +222,23 @@ a.+c ...@@ -222,21 +222,23 @@ a.+c
匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的: 匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的:
1. 一位或者两位的数字 1. 一位数字
2. 1 开头的三位数 2. 不以 0 开头的两位数字
3. 2 开头,第 2 位是 0-4 的三位数 3. 1 开头的三位数
4. 25 开头,第 3 位是 0-5 的三位数 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** 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 ...@@ -251,7 +253,7 @@ a.+c
\1 将回溯引用子表达式 (h[1-6]) 匹配的内容,也就是说必须和子表达式匹配的内容一致。 \1 将回溯引用子表达式 (h[1-6]) 匹配的内容,也就是说必须和子表达式匹配的内容一致。
``` ```
<(h[1-6])>\w*?</\1> <(h[1-6])>\w*?<\/\1>
``` ```
**匹配结果** **匹配结果**
......
...@@ -581,7 +581,7 @@ public class MergeSort { ...@@ -581,7 +581,7 @@ public class MergeSort {
for (int k = lo; k <= hi; k++) { for (int k = lo; k <= hi; k++) {
if (i > mid) a[k] = aux[j++]; if (i > mid) a[k] = aux[j++];
else if (j > hi) a[k] = aux[i++]; 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++]; else a[k] = aux[j++];
} }
} }
...@@ -1439,7 +1439,7 @@ public class Transaction{ ...@@ -1439,7 +1439,7 @@ public class Transaction{
### 3. 基于线性探测法的散列表 ### 3. 基于线性探测法的散列表
线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。使用线探测法,数组的大小 M 应当大于键的个数 N(M>N)。 线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。使用线探测法,数组的大小 M 应当大于键的个数 N(M>N)。
<div align="center"> <img src="../pics//dbb8516d-37ba-4e2c-b26b-eefd7de21b45.png" width="400"/> </div><br> <div align="center"> <img src="../pics//dbb8516d-37ba-4e2c-b26b-eefd7de21b45.png" width="400"/> </div><br>
......
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
## 电路交换与分组交换 ## 电路交换与分组交换
<div align="center"> <img src="../pics//eebdeb57-8efb-4848-9bb6-97512990897c.jpg"/> </div><br> <div align="center"> <img src="../pics//5e8d3c04-d93b-48a7-875e-41ababed00e0.jpg"/> </div><br>
(以上分别为:电路交换、报文交换以及分组交换) (以上分别为:电路交换、报文交换以及分组交换)
......
...@@ -128,13 +128,13 @@ if (uniqueInstance == null) { ...@@ -128,13 +128,13 @@ if (uniqueInstance == null) {
uniqueInstance 采用 volatile 关键字修饰也是很有必要的。 uniqueInstance 采用 volatile 关键字修饰也是很有必要的。
`uniqueInstance = new Singleton();` 这段代码其实是分为三步执行。 uniqueInstance = new Singleton(); 这段代码其实是分为三步执行。
1. 分配内存空间。 1. 分配内存空间。
2. 初始化对象。 2. 初始化对象。
3. 将 uniqueInstance 指向分配的内存地址。 3. 将 uniqueInstance 指向分配的内存地址。
但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 `1>3>2`,这在单线程情况下自然是没有问题。但如果是多线程就有可能 B 线程获得是一个还没有被初始化的对象以致于程序出错。 但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 1>3>2,这在单线程情况下自然是没有问题。但如果是多线程就有可能 B 线程获得是一个还没有被初始化的对象以致于程序出错。
所以使用 volatile 修饰的目的是禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 所以使用 volatile 修饰的目的是禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
...@@ -178,17 +178,17 @@ public interface Product { ...@@ -178,17 +178,17 @@ public interface Product {
``` ```
```java ```java
public class ConcreteProduct implements Product{ public class ConcreteProduct implements Product {
} }
``` ```
```java ```java
public class ConcreteProduct1 implements Product{ public class ConcreteProduct1 implements Product {
} }
``` ```
```java ```java
public class ConcreteProduct2 implements Product{ public class ConcreteProduct2 implements Product {
} }
``` ```
...@@ -249,7 +249,7 @@ public class ConcreteFactory extends Factory { ...@@ -249,7 +249,7 @@ public class ConcreteFactory extends Factory {
``` ```
```java ```java
public class ConcreteFactory1 extends Factory{ public class ConcreteFactory1 extends Factory {
public Product factoryMethod() { public Product factoryMethod() {
return new ConcreteProduct1(); return new ConcreteProduct1();
} }
...@@ -272,17 +272,16 @@ public class ConcreteFactory2 extends Factory { ...@@ -272,17 +272,16 @@ public class ConcreteFactory2 extends Factory {
## 类图 ## 类图
<div align="center"> <img src="../pics//920c034c-c212-4f79-9ddb-84e4bb6cd088.png"/> </div><br> <div align="center"> <img src="../pics//14cfe8d4-e31b-49e0-ac6a-6f4f7aa06ab6.png"/> </div><br>
抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂模式只是用于创建一个对象,这和抽象工厂模式有很大不同。 抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂模式只是用于创建一个对象,这和抽象工厂模式有很大不同。
抽象工厂模式用到了工厂模式来创建单一对象,在类图左部,AbstractFactory 中的 createProductA 和 createProductB 方法都是让子类来实现,这两个方法单独来看就是在创建一个对象,这符合工厂模式的定义。 抽象工厂模式用到了工厂模式来创建单一对象,AbstractFactory 中的 createProductA 和 createProductB 方法都是让子类来实现,这两个方法单独来看就是在创建一个对象,这符合工厂模式的定义。
至于创建对象的家族这一概念是在 Client 体现,Client 要通过 AbstractFactory 同时调用两个方法来创建出两个对象,在这里这两个对象就有很大的相关性,Client 需要同时创建出这两个对象。 至于创建对象的家族这一概念是在 Client 体现,Client 要通过 AbstractFactory 同时调用两个方法来创建出两个对象,在这里这两个对象就有很大的相关性,Client 需要同时创建出这两个对象。
从高层次来看,抽象工厂使用了组合,即 Cilent 组合了 AbstractFactory,而工厂模式使用了继承。 从高层次来看,抽象工厂使用了组合,即 Cilent 组合了 AbstractFactory,而工厂模式使用了继承。
## 代码实现 ## 代码实现
```java ```java
...@@ -306,12 +305,12 @@ public class ProductA2 extends AbstractProductA { ...@@ -306,12 +305,12 @@ public class ProductA2 extends AbstractProductA {
``` ```
```java ```java
public class ProductB1 extends AbstractProductB{ public class ProductB1 extends AbstractProductB {
} }
``` ```
```java ```java
public class ProductB2 extends AbstractProductB{ public class ProductB2 extends AbstractProductB {
} }
``` ```
...@@ -323,7 +322,7 @@ public abstract class AbstractFactory { ...@@ -323,7 +322,7 @@ public abstract class AbstractFactory {
``` ```
```java ```java
public class ConcreteFactory1 extends AbstractFactory{ public class ConcreteFactory1 extends AbstractFactory {
AbstractProductA createProductA() { AbstractProductA createProductA() {
return new ProductA1(); return new ProductA1();
} }
......
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
- 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块 - 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块
- 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能 - 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能
- 提高软件的可重用性 - 提高软件的可重用性
- 低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的 - 低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的
以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。 以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册