diff --git a/notes/Git.md b/notes/Git.md
index 4bb0a543..a096e307 100644
--- a/notes/Git.md
+++ b/notes/Git.md
@@ -1,29 +1,13 @@
-
-* [学习资料](#学习资料)
-* [集中式与分布式](#集中式与分布式)
-* [Git 的中心服务器](#git-的中心服务器)
-* [Git 工作流](#git-工作流)
-* [分支实现](#分支实现)
-* [冲突](#冲突)
-* [Fast forward](#fast-forward)
-* [分支管理策略](#分支管理策略)
-* [储藏(Stashing)](#储藏stashing)
-* [SSH 传输设置](#ssh-传输设置)
-* [.gitignore 文件](#gitignore-文件)
-* [Git 命令一览](#git-命令一览)
-
+# 学习资料
+- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
+- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
+- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
+- [Learn Git Branching](https://learngitbranching.js.org/)
-# 学习资料
+# 集中式与分布式
-- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
-- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
-- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
-- [Learn Git Branching](https://learngitbranching.js.org/)
-
-# 集中式与分布式
-
-Git 属于分布式版本控制系统,而 SVN 属于集中式。
+Git 属于分布式版本控制系统,而 SVN 属于集中式。
集中式版本控制只有中心服务器拥有一份代码,而分布式版本控制每个人的电脑上就有一份完整的代码。
@@ -33,125 +17,125 @@ Git 属于分布式版本控制系统,而 SVN 属于集中式。
分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。
-# Git 的中心服务器
+# Git 的中心服务器
-Git 的中心服务器用来交换每个用户的修改。没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。Github 就是一种 Git 中心服务器。
+Git 的中心服务器用来交换每个用户的修改。没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。Github 就是一种 Git 中心服务器。
-# Git 工作流
+# Git 工作流
-
+![](index_files/a1198642-9159-4d88-8aec-c3b04e7a2563.jpg)
-新建一个仓库之后,当前目录就成为了工作区,工作区下有一个隐藏目录 .git,它属于 Git 的版本库。
+新建一个仓库之后,当前目录就成为了工作区,工作区下有一个隐藏目录 .git,它属于 Git 的版本库。
-Git 版本库有一个称为 stage 的暂存区,还有自动创建的 master 分支以及指向分支的 HEAD 指针。
+Git 版本库有一个称为 stage 的暂存区,还有自动创建的 master 分支以及指向分支的 HEAD 指针。
-
+![](index_files/46f66e88-e65a-4ad0-a060-3c63fe22947c.png)
-- git add files 把文件的修改添加到暂存区
-- git commit 把暂存区的修改提交到当前分支,提交之后暂存区就被清空了
-- git reset -- files 使用当前分支上的修改覆盖暂缓区,用来撤销最后一次 git add files
-- git checkout -- files 使用暂存区的修改覆盖工作目录,用来撤销本地修改
+- git add files 把文件的修改添加到暂存区
+- git commit 把暂存区的修改提交到当前分支,提交之后暂存区就被清空了
+- git reset -- files 使用当前分支上的修改覆盖暂缓区,用来撤销最后一次 git add files
+- git checkout -- files 使用暂存区的修改覆盖工作目录,用来撤销本地修改
-
+![](index_files/17976404-95f5-480e-9cb4-250e6aa1d55f.png)
可以跳过暂存区域直接从分支中取出修改或者直接提交修改到分支中
-- git commit -a 直接把所有文件的修改添加到暂缓区然后执行提交
-- git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作
+- git commit -a 直接把所有文件的修改添加到暂缓区然后执行提交
+- git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作
-# 分支实现
+# 分支实现
-Git 把每次提交都连成一条时间线。分支使用指针来实现,例如 master 分支指针指向时间线的最后一个节点,也就是最后一次提交。HEAD 指针指向的是当前分支。
+Git 把每次提交都连成一条时间线。分支使用指针来实现,例如 master 分支指针指向时间线的最后一个节点,也就是最后一次提交。HEAD 指针指向的是当前分支。
-
+![](index_files/fb546e12-e1fb-4b72-a1fb-8a7f5000dce6.jpg)
-新建分支是新建一个指针指向时间线的最后一个节点,并让 HEAD 指针指向新分支表示新分支成为当前分支。
+新建分支是新建一个指针指向时间线的最后一个节点,并让 HEAD 指针指向新分支表示新分支成为当前分支。
-
+![](index_files/bc775758-89ab-4805-9f9c-78b8739cf780.jpg)
每次提交只会让当前分支向前移动,而其它分支不会移动。
-
+![](index_files/5292faa6-0141-4638-bf0f-bb95b081dcba.jpg)
合并分支也只需要改变指针即可。
-
+![](index_files/1164a71f-413d-494a-9cc8-679fb6a2613d.jpg)
-# 冲突
+# 冲突
当两个分支都对同一个文件的同一行进行了修改,在分支合并时就会产生冲突。
-
+![](index_files/58e57a21-6b6b-40b6-af85-956dd4e0f55a.jpg)
-Git 会使用 <<<<<<< ,======= ,>>>>>>> 标记出不同分支的内容,只需要把不同分支中冲突部分修改成一样就能解决冲突。
+Git 会使用 <<<<<<< ,======= ,>>>>>>> 标记出不同分支的内容,只需要把不同分支中冲突部分修改成一样就能解决冲突。
```
-<<<<<<< HEAD
-Creating a new branch is quick & simple.
+<<<<<<< HEAD
+Creating a new branch is quick & simple.
=======
-Creating a new branch is quick AND simple.
->>>>>>> feature1
+Creating a new branch is quick AND simple.
+>>>>>>> feature1
```
-# Fast forward
+# Fast forward
-"快进式合并"(fast-farward merge),会直接将 master 分支指向合并的分支,这种模式下进行分支合并会丢失分支信息,也就不能在分支历史上看出分支信息。
+"快进式合并"(fast-farward merge),会直接将 master 分支指向合并的分支,这种模式下进行分支合并会丢失分支信息,也就不能在分支历史上看出分支信息。
-可以在合并时加上 --no-ff 参数来禁用 Fast forward 模式,并且加上 -m 参数让合并时产生一个新的 commit。
+可以在合并时加上 --no-ff 参数来禁用 Fast forward 模式,并且加上 -m 参数让合并时产生一个新的 commit。
```
-$ git merge --no-ff -m "merge with no-ff" dev
+$ git merge --no-ff -m "merge with no-ff" dev
```
-
+![](index_files/dd78a1fe-1ff3-4bcf-a56f-8c003995beb6.jpg)
-# 分支管理策略
+# 分支管理策略
-master 分支应该是非常稳定的,只用来发布新版本;
+master 分支应该是非常稳定的,只用来发布新版本;
-日常开发在开发分支 dev 上进行。
+日常开发在开发分支 dev 上进行。
-
+![](index_files/245fd2fb-209c-4ad5-bc5e-eb5664966a0e.jpg)
-# 储藏(Stashing)
+# 储藏(Stashing)
在一个分支上操作之后,如果还没有将修改提交到分支上,此时进行切换分支,那么另一个分支上也能看到新的修改。这是因为所有分支都共用一个工作区的缘故。
-可以使用 git stash 将当前分支的修改储藏起来,此时当前工作区的所有修改都会被存到栈上,也就是说当前工作区是干净的,没有任何未提交的修改。此时就可以安全的切换到其它分支上了。
+可以使用 git stash 将当前分支的修改储藏起来,此时当前工作区的所有修改都会被存到栈上,也就是说当前工作区是干净的,没有任何未提交的修改。此时就可以安全的切换到其它分支上了。
```
-$ git stash
-Saved working directory and index state \ "WIP on master: 049d078 added the index file"
-HEAD is now at 049d078 added the index file (To restore them type "git stash apply")
+$ git stash
+Saved working directory and index state \ "WIP on master: 049d078 added the index file"
+HEAD is now at 049d078 added the index file (To restore them type "git stash apply")
```
-该功能可以用于 bug 分支的实现。如果当前正在 dev 分支上进行开发,但是此时 master 上有个 bug 需要修复,但是 dev 分支上的开发还未完成,不想立即提交。在新建 bug 分支并切换到 bug 分支之前就需要使用 git stash 将 dev 分支的未提交修改储藏起来。
+该功能可以用于 bug 分支的实现。如果当前正在 dev 分支上进行开发,但是此时 master 上有个 bug 需要修复,但是 dev 分支上的开发还未完成,不想立即提交。在新建 bug 分支并切换到 bug 分支之前就需要使用 git stash 将 dev 分支的未提交修改储藏起来。
-# SSH 传输设置
+# SSH 传输设置
-Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密。
+Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密。
-如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key:
+如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key:
```
-$ ssh-keygen -t rsa -C "youremail@example.com"
+$ ssh-keygen -t rsa -C "youremail@example.com"
```
-然后把公钥 id_rsa.pub 的内容复制到 Github "Account settings" 的 SSH Keys 中。
+然后把公钥 id_rsa.pub 的内容复制到 Github "Account settings" 的 SSH Keys 中。
-# .gitignore 文件
+# .gitignore 文件
忽略以下文件:
-- 操作系统自动生成的文件,比如缩略图;
-- 编译生成的中间文件,比如 Java 编译产生的 .class 文件;
-- 自己的敏感信息,比如存放口令的配置文件。
+- 操作系统自动生成的文件,比如缩略图;
+- 编译生成的中间文件,比如 Java 编译产生的 .class 文件;
+- 自己的敏感信息,比如存放口令的配置文件。
-不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。
+不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。
-# Git 命令一览
+# Git 命令一览
-
+![](index_files/7a29acce-f243-4914-9f00-f2988c528412.jpg)
比较详细的地址:http://www.cheat-sheets.org/saved-copy/git-cheat-sheet.pdf
diff --git a/notes/HTTP.md b/notes/HTTP.md
index 86aeeedb..3ab314f4 100644
--- a/notes/HTTP.md
+++ b/notes/HTTP.md
@@ -1,845 +1,780 @@
-
-* [一 、基础概念](#一-基础概念)
- * [Web 基础](#web-基础)
- * [URL](#url)
- * [请求和响应报文](#请求和响应报文)
-* [二、HTTP 方法](#二http-方法)
- * [GET](#get)
- * [HEAD](#head)
- * [POST](#post)
- * [PUT](#put)
- * [PATCH](#patch)
- * [DELETE](#delete)
- * [OPTIONS](#options)
- * [CONNECT](#connect)
- * [TRACE](#trace)
-* [三、HTTP 状态码](#三http-状态码)
- * [1XX 信息](#1xx-信息)
- * [2XX 成功](#2xx-成功)
- * [3XX 重定向](#3xx-重定向)
- * [4XX 客户端错误](#4xx-客户端错误)
- * [5XX 服务器错误](#5xx-服务器错误)
-* [四、HTTP 首部](#四http-首部)
- * [通用首部字段](#通用首部字段)
- * [请求首部字段](#请求首部字段)
- * [响应首部字段](#响应首部字段)
- * [实体首部字段](#实体首部字段)
-* [五、具体应用](#五具体应用)
- * [Cookie](#cookie)
- * [缓存](#缓存)
- * [连接管理](#连接管理)
- * [内容协商](#内容协商)
- * [内容编码](#内容编码)
- * [范围请求](#范围请求)
- * [分块传输编码](#分块传输编码)
- * [多部分对象集合](#多部分对象集合)
- * [虚拟主机](#虚拟主机)
- * [通信数据转发](#通信数据转发)
-* [六、HTTPs](#六https)
- * [加密](#加密)
- * [认证](#认证)
- * [完整性保护](#完整性保护)
- * [HTTPs 的缺点](#https-的缺点)
- * [配置 HTTPs](#配置-https)
-* [七、Web 攻击技术](#七web-攻击技术)
- * [跨站脚本攻击](#跨站脚本攻击)
- * [跨站请求伪造](#跨站请求伪造)
- * [SQL 注入攻击](#sql-注入攻击)
- * [拒绝服务攻击](#拒绝服务攻击)
-* [八、GET 和 POST 的区别](#八get-和-post-的区别)
- * [作用](#作用)
- * [参数](#参数)
- * [安全](#安全)
- * [幂等性](#幂等性)
- * [可缓存](#可缓存)
- * [XMLHttpRequest](#xmlhttprequest)
-* [九、HTTP/1.0 与 HTTP/1.1 的区别](#九http10-与-http11-的区别)
-* [十、HTTP/2.0](#十http20)
- * [HTTP/1.x 缺陷](#http1x-缺陷)
- * [二进制分帧层](#二进制分帧层)
- * [服务端推送](#服务端推送)
- * [首部压缩](#首部压缩)
-* [参考资料](#参考资料)
-
+# 一 、基础概念
+## Web 基础
-# 一 、基础概念
+- WWW(World Wide Web)的三种技术:HTML、HTTP、URL
+- HTML(HyperText Markup Language,超文本标记语言)
+- HTTP(HyperText Transfer Protocol,超文本传输协议)
+- RFC(Request for Comments,征求修正意见书),互联网的设计文档。
-## Web 基础
+## URL
-- WWW(World Wide Web)的三种技术:HTML、HTTP、URL
-- HTML(HyperText Markup Language,超文本标记语言)
-- HTTP(HyperText Transfer Protocol,超文本传输协议)
-- RFC(Request for Comments,征求修正意见书),互联网的设计文档。
+- URI(Uniform Resource Indentifier,统一资源标识符)
+- URL(Uniform Resource Locator,统一资源定位符)
+- URN(Uniform Resource Name,统一资源名称),例如 urn:isbn:0-486-27557-4。
-## URL
+URI 包含 URL 和 URN,目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。
-- URI(Uniform Resource Indentifier,统一资源标识符)
-- URL(Uniform Resource Locator,统一资源定位符)
-- URN(Uniform Resource Name,统一资源名称),例如 urn:isbn:0-486-27557-4。
+
-URI 包含 URL 和 URN,目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。
+## 请求和响应报文
-
+### 1. 请求报文
-## 请求和响应报文
+
-### 1. 请求报文
+### 2. 响应报文
-
+
-### 2. 响应报文
+# 二、HTTP 方法
-
+客户端发送的 **请求报文** 第一行为请求行,包含了方法字段。
-# 二、HTTP 方法
+## GET
-客户端发送的 **请求报文** 第一行为请求行,包含了方法字段。
+> 获取资源
-## GET
+当前网络请求中,绝大部分使用的是 GET 方法。
-> 获取资源
+## HEAD
-当前网络请求中,绝大部分使用的是 GET 方法。
+> 获取报文首部
-## HEAD
+和 GET 方法一样,但是不返回报文实体主体部分。
-> 获取报文首部
+主要用于确认 URL 的有效性以及资源更新的日期时间等。
-和 GET 方法一样,但是不返回报文实体主体部分。
+## POST
-主要用于确认 URL 的有效性以及资源更新的日期时间等。
+> 传输实体主体
-## POST
+POST 主要用来传输数据,而 GET 主要用来获取资源。
-> 传输实体主体
+更多 POST 与 GET 的比较请见第八章。
-POST 主要用来传输数据,而 GET 主要用来获取资源。
+## PUT
-更多 POST 与 GET 的比较请见第八章。
-
-## PUT
-
-> 上传文件
+> 上传文件
由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。
```html
-PUT /new.html HTTP/1.1
-Host: example.com
-Content-type: text/html
-Content-length: 16
+PUT /new.html HTTP/1.1
+Host: example.com
+Content-type: text/html
+Content-length: 16
-New File
+New File
```
-## PATCH
+## PATCH
-> 对资源进行部分修改
+> 对资源进行部分修改
-PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。
+PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。
```html
-PATCH /file.txt HTTP/1.1
-Host: www.example.com
-Content-Type: application/example
-If-Match: "e0023aa4e"
-Content-Length: 100
+PATCH /file.txt HTTP/1.1
+Host: www.example.com
+Content-Type: application/example
+If-Match: "e0023aa4e"
+Content-Length: 100
-[description of changes]
+[description of changes]
```
-## DELETE
+## DELETE
-> 删除文件
+> 删除文件
-与 PUT 功能相反,并且同样不带验证机制。
+与 PUT 功能相反,并且同样不带验证机制。
```html
-DELETE /file.html HTTP/1.1
+DELETE /file.html HTTP/1.1
```
-## OPTIONS
+## OPTIONS
-> 查询支持的方法
+> 查询支持的方法
-查询指定的 URL 能够支持的方法。
+查询指定的 URL 能够支持的方法。
-会返回 Allow: GET, POST, HEAD, OPTIONS 这样的内容。
+会返回 Allow: GET, POST, HEAD, OPTIONS 这样的内容。
-## CONNECT
+## CONNECT
-> 要求用隧道协议连接代理
+> 要求用隧道协议连接代理
-要求在与代理服务器通信时建立隧道,使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。
+要求在与代理服务器通信时建立隧道,使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。
```html
-CONNECT www.example.com:443 HTTP/1.1
+CONNECT www.example.com:443 HTTP/1.1
```
-
+
-## TRACE
+## TRACE
-> 追踪路径
+> 追踪路径
服务器会将通信路径返回给客户端。
-发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。
+发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。
-通常不会使用 TRACE,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪),因此更不会去使用它。
+通常不会使用 TRACE,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪),因此更不会去使用它。
-# 三、HTTP 状态码
+# 三、HTTP 状态码
-服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。
+服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。
-| 状态码 | 类别 | 原因短语 |
-| :---: | :---: | :---: |
-| 1XX | Informational(信息性状态码) | 接收的请求正在处理 |
-| 2XX | Success(成功状态码) | 请求正常处理完毕 |
-| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
-| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
-| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
+| 状态码 | 类别 | 原因短语 |
+| :---: | :---: | :---: |
+| 1XX | Informational(信息性状态码) | 接收的请求正在处理 |
+| 2XX | Success(成功状态码) | 请求正常处理完毕 |
+| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
+| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
+| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
-## 1XX 信息
+## 1XX 信息
-- **100 Continue** :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。
+- **100 Continue**:表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。
-## 2XX 成功
+## 2XX 成功
-- **200 OK**
+- **200 OK**
-- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
+- **204 No Content**:请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
-- **206 Partial Content** :表示客户端进行了范围请求。响应报文包含由 Content-Range 指定范围的实体内容。
+- **206 Partial Content**:表示客户端进行了范围请求。响应报文包含由 Content-Range 指定范围的实体内容。
-## 3XX 重定向
+## 3XX 重定向
-- **301 Moved Permanently** :永久性重定向
+- **301 Moved Permanently**:永久性重定向
-- **302 Found** :临时性重定向
+- **302 Found**:临时性重定向
-- **303 See Other** :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。
+- **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-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。
+- **304 Not Modified**:如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。
-- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。
+- **307 Temporary Redirect**:临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。
-## 4XX 客户端错误
+## 4XX 客户端错误
-- **400 Bad Request** :请求报文中存在语法错误。
+- **400 Bad Request**:请求报文中存在语法错误。
-- **401 Unauthorized** :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
+- **401 Unauthorized**:该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
-- **403 Forbidden** :请求被拒绝,服务器端没有必要给出拒绝的详细理由。
+- **403 Forbidden**:请求被拒绝,服务器端没有必要给出拒绝的详细理由。
-- **404 Not Found**
+- **404 Not Found**
-## 5XX 服务器错误
+## 5XX 服务器错误
-- **500 Internal Server Error** :服务器正在执行请求时发生错误。
+- **500 Internal Server Error**:服务器正在执行请求时发生错误。
-- **503 Service Unavilable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。
+- **503 Service Unavilable**:服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。
-# 四、HTTP 首部
+# 四、HTTP 首部
-有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。
+有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。
各种首部字段及其含义如下(不需要全记,仅供查阅):
-## 通用首部字段
+## 通用首部字段
-| 首部字段名 | 说明 |
-| :--: | :--: |
-| Cache-Control | 控制缓存的行为 |
-| Connection | 控制不再转发给代理的首部字段、管理持久连接|
-| Date | 创建报文的日期时间 |
-| Pragma | 报文指令 |
-| Trailer | 报文末端的首部一览 |
-| Transfer-Encoding | 指定报文主体的传输编码方式 |
-| Upgrade | 升级为其他协议 |
-| Via | 代理服务器的相关信息 |
-| Warning | 错误通知 |
+| 首部字段名 | 说明 |
+| :--: | :--: |
+| Cache-Control | 控制缓存的行为 |
+| Connection | 控制不再转发给代理的首部字段、管理持久连接|
+| Date | 创建报文的日期时间 |
+| Pragma | 报文指令 |
+| Trailer | 报文末端的首部一览 |
+| Transfer-Encoding | 指定报文主体的传输编码方式 |
+| Upgrade | 升级为其他协议 |
+| Via | 代理服务器的相关信息 |
+| Warning | 错误通知 |
-## 请求首部字段
+## 请求首部字段
-| 首部字段名 | 说明 |
-| :--: | :--: |
-| Accept | 用户代理可处理的媒体类型 |
-| Accept-Charset | 优先的字符集 |
-| Accept-Encoding | 优先的内容编码 |
-| Accept-Language | 优先的语言(自然语言) |
-| Authorization | Web 认证信息 |
-| Expect | 期待服务器的特定行为 |
-| From | 用户的电子邮箱地址 |
-| Host | 请求资源所在服务器 |
-| If-Match | 比较实体标记(ETag) |
-| If-Modified-Since | 比较资源的更新时间 |
-| If-None-Match | 比较实体标记(与 If-Match 相反) |
-| If-Range | 资源未更新时发送实体 Byte 的范围请求 |
-| If-Unmodified-Since | 比较资源的更新时间(与 If-Modified-Since 相反) |
-| Max-Forwards | 最大传输逐跳数 |
-| Proxy-Authorization | 代理服务器要求客户端的认证信息 |
-| Range | 实体的字节范围请求 |
-| Referer | 对请求中 URI 的原始获取方 |
-| TE | 传输编码的优先级 |
-| User-Agent | HTTP 客户端程序的信息 |
+| 首部字段名 | 说明 |
+| :--: | :--: |
+| Accept | 用户代理可处理的媒体类型 |
+| Accept-Charset | 优先的字符集 |
+| Accept-Encoding | 优先的内容编码 |
+| Accept-Language | 优先的语言(自然语言) |
+| Authorization | Web 认证信息 |
+| Expect | 期待服务器的特定行为 |
+| From | 用户的电子邮箱地址 |
+| Host | 请求资源所在服务器 |
+| If-Match | 比较实体标记(ETag) |
+| If-Modified-Since | 比较资源的更新时间 |
+| If-None-Match | 比较实体标记(与 If-Match 相反) |
+| If-Range | 资源未更新时发送实体 Byte 的范围请求 |
+| If-Unmodified-Since | 比较资源的更新时间(与 If-Modified-Since 相反) |
+| Max-Forwards | 最大传输逐跳数 |
+| Proxy-Authorization | 代理服务器要求客户端的认证信息 |
+| Range | 实体的字节范围请求 |
+| Referer | 对请求中 URI 的原始获取方 |
+| TE | 传输编码的优先级 |
+| User-Agent | HTTP 客户端程序的信息 |
-## 响应首部字段
+## 响应首部字段
-| 首部字段名 | 说明 |
-| :--: | :--: |
-| Accept-Ranges | 是否接受字节范围请求 |
-| Age | 推算资源创建经过时间 |
-| ETag | 资源的匹配信息 |
-| Location | 令客户端重定向至指定 URI |
-| Proxy-Authenticate | 代理服务器对客户端的认证信息 |
-| Retry-After | 对再次发起请求的时机要求 |
-| Server | HTTP 服务器的安装信息 |
-| Vary | 代理服务器缓存的管理信息 |
-| WWW-Authenticate | 服务器对客户端的认证信息 |
+| 首部字段名 | 说明 |
+| :--: | :--: |
+| Accept-Ranges | 是否接受字节范围请求 |
+| Age | 推算资源创建经过时间 |
+| ETag | 资源的匹配信息 |
+| Location | 令客户端重定向至指定 URI |
+| Proxy-Authenticate | 代理服务器对客户端的认证信息 |
+| Retry-After | 对再次发起请求的时机要求 |
+| Server | HTTP 服务器的安装信息 |
+| Vary | 代理服务器缓存的管理信息 |
+| WWW-Authenticate | 服务器对客户端的认证信息 |
-## 实体首部字段
+## 实体首部字段
-| 首部字段名 | 说明 |
-| :--: | :--: |
-| Allow | 资源可支持的 HTTP 方法 |
-| Content-Encoding | 实体主体适用的编码方式 |
-| Content-Language | 实体主体的自然语言 |
-| Content-Length | 实体主体的大小 |
-| Content-Location | 替代对应资源的 URI |
-| Content-MD5 | 实体主体的报文摘要 |
-| Content-Range | 实体主体的位置范围 |
-| Content-Type | 实体主体的媒体类型 |
-| Expires | 实体主体过期的日期时间 |
-| Last-Modified | 资源的最后修改日期时间 |
+| 首部字段名 | 说明 |
+| :--: | :--: |
+| Allow | 资源可支持的 HTTP 方法 |
+| Content-Encoding | 实体主体适用的编码方式 |
+| Content-Language | 实体主体的自然语言 |
+| Content-Length | 实体主体的大小 |
+| Content-Location | 替代对应资源的 URI |
+| Content-MD5 | 实体主体的报文摘要 |
+| Content-Range | 实体主体的位置范围 |
+| Content-Type | 实体主体的媒体类型 |
+| Expires | 实体主体过期的日期时间 |
+| Last-Modified | 资源的最后修改日期时间 |
-# 五、具体应用
+# 五、具体应用
-## Cookie
+## Cookie
-HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。
+HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。
-Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。它用于告知服务端两个请求是否来自同一浏览器,并保持用户的登录状态。
+Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。它用于告知服务端两个请求是否来自同一浏览器,并保持用户的登录状态。
-### 1. 用途
+### 1. 用途
-- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
-- 个性化设置(如用户自定义设置、主题等)
-- 浏览器行为跟踪(如跟踪分析用户行为等)
+- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
+- 个性化设置(如用户自定义设置、主题等)
+- 浏览器行为跟踪(如跟踪分析用户行为等)
-Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。由于服务器指定 Cookie 后,浏览器的每次请求都会携带 Cookie 数据,会带来额外的性能开销(尤其是在移动环境下)。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API (本地存储和会话存储)或 IndexedDB。
+Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。由于服务器指定 Cookie 后,浏览器的每次请求都会携带 Cookie 数据,会带来额外的性能开销(尤其是在移动环境下)。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API (本地存储和会话存储)或 IndexedDB。
-### 2. 创建过程
+### 2. 创建过程
-服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。
+服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。
```html
-HTTP/1.0 200 OK
-Content-type: text/html
-Set-Cookie: yummy_cookie=choco
-Set-Cookie: tasty_cookie=strawberry
+HTTP/1.0 200 OK
+Content-type: text/html
+Set-Cookie: yummy_cookie=choco
+Set-Cookie: tasty_cookie=strawberry
-[page content]
+[page content]
```
-客户端之后对同一个服务器发送请求时,会从浏览器中读出 Cookie 信息通过 Cookie 请求首部字段发送给服务器。
+客户端之后对同一个服务器发送请求时,会从浏览器中读出 Cookie 信息通过 Cookie 请求首部字段发送给服务器。
```html
-GET /sample_page.html HTTP/1.1
-Host: www.example.org
-Cookie: yummy_cookie=choco; tasty_cookie=strawberry
+GET /sample_page.html HTTP/1.1
+Host: www.example.org
+Cookie: yummy_cookie=choco; tasty_cookie=strawberry
```
-### 3. 分类
+### 3. 分类
-- 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。
-- 持久性 Cookie:指定一个特定的过期时间(Expires)或有效期(Max-Age)之后就成为了持久性的 Cookie。
+- 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。
+- 持久性 Cookie:指定一个特定的过期时间(Expires)或有效期(Max-Age)之后就成为了持久性的 Cookie。
```html
-Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
+Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
```
-### 4. JavaScript 获取 Cookie
+### 4. JavaScript 获取 Cookie
-通过 `Document.cookie` 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。
+通过 `Document.cookie` 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。
```html
-document.cookie = "yummy_cookie=choco";
-document.cookie = "tasty_cookie=strawberry";
+document.cookie = "yummy_cookie=choco";
+document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
```
-### 5. Secure 和 HttpOnly
+### 5. Secure 和 HttpOnly
-标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。
+标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。
-标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。因为跨域脚本 (XSS) 攻击常常使用 JavaScript 的 `Document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。
+标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。因为跨域脚本 (XSS) 攻击常常使用 JavaScript 的 `Document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。
```html
-Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
+Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
```
-### 6. 作用域
+### 6. 作用域
-Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。
+Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。
-Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配:
+Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配:
-- /docs
-- /docs/Web/
-- /docs/Web/HTTP
+- /docs
+- /docs/Web/
+- /docs/Web/HTTP
-### 7. Session
+### 7. Session
-除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。
+除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。
-Session 可以存储在服务器上的文件、数据库或者内存中,现在最常见的是将 Session 存储在内存型数据库中,比如 Redis。
+Session 可以存储在服务器上的文件、数据库或者内存中,现在最常见的是将 Session 存储在内存型数据库中,比如 Redis。
-使用 Session 维护用户登录的过程如下:
+使用 Session 维护用户登录的过程如下:
-- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中;
-- 服务器验证该用户名和密码;
-- 如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 ID 称为 Session ID;
-- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中;
-- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之后的业务操作。
+- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中;
+- 服务器验证该用户名和密码;
+- 如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 ID 称为 Session ID;
+- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中;
+- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之后的业务操作。
-应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。
+应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。
-### 8. 浏览器禁用 Cookie
+### 8. 浏览器禁用 Cookie
-此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。
+此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。
-### 9. Cookie 与 Session 选择
+### 9. Cookie 与 Session 选择
-- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存取任何类型的数据,因此在考虑数据复杂性时 首选 Session;
-- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密;
-- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。
+- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存取任何类型的数据,因此在考虑数据复杂性时 首选 Session;
+- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密;
+- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。
-## 缓存
+## 缓存
-### 1. 优点
+### 1. 优点
-- 缓解服务器压力;
-- 减低客户端获取资源的延迟(缓存资源比服务器上的资源离客户端更近)。
+- 缓解服务器压力;
+- 减低客户端获取资源的延迟(缓存资源比服务器上的资源离客户端更近)。
-### 2. 实现方法
+### 2. 实现方法
-- 让代理服务器进行缓存;
-- 让客户端浏览器进行缓存。
+- 让代理服务器进行缓存;
+- 让客户端浏览器进行缓存。
-### 3. Cache-Control
+### 3. Cache-Control
-HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。
+HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。
-**(一)禁止进行缓存**
+**(一)禁止进行缓存**
-no-store 指令规定不能对请求或响应的任何一部分进行缓存。
+no-store 指令规定不能对请求或响应的任何一部分进行缓存。
```html
-Cache-Control: no-store
+Cache-Control: no-store
```
-**(二)强制确认缓存**
+**(二)强制确认缓存**
-no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。
+no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。
```html
-Cache-Control: no-cache
+Cache-Control: no-cache
```
-**(三)私有缓存和公共缓存**
+**(三)私有缓存和公共缓存**
-private 指令规定了将资源作为私有缓存,只能被单独用户所使用,一般存储在用户浏览器中。
+private 指令规定了将资源作为私有缓存,只能被单独用户所使用,一般存储在用户浏览器中。
```html
-Cache-Control: private
+Cache-Control: private
```
-public 指令规定了将资源作为公共缓存,可以被多个用户所使用,一般存储在代理服务器中。
+public 指令规定了将资源作为公共缓存,可以被多个用户所使用,一般存储在代理服务器中。
```html
-Cache-Control: public
+Cache-Control: public
```
-**(四)缓存过期机制**
+**(四)缓存过期机制**
-max-age 指令出现在请求报文中,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。
+max-age 指令出现在请求报文中,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。
-max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中保存的时间。
+max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中保存的时间。
```html
-Cache-Control: max-age=31536000
+Cache-Control: max-age=31536000
```
-Expires 字段也可以用于告知缓存服务器该资源什么时候会过期。在 HTTP/1.1 中,会优先处理 Cache-Control : max-age 指令;而在 HTTP/1.0 中,Cache-Control : max-age 指令会被忽略掉。
+Expires 字段也可以用于告知缓存服务器该资源什么时候会过期。在 HTTP/1.1 中,会优先处理 Cache-Control : max-age 指令;而在 HTTP/1.0 中,Cache-Control : max-age 指令会被忽略掉。
```html
-Expires: Wed, 04 Jul 2012 08:26:05 GMT
+Expires: Wed, 04 Jul 2012 08:26:05 GMT
```
-### 4. 缓存验证
+### 4. 缓存验证
-需要先了解 ETag 首部字段的含义,它是资源的唯一表示。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一表示。
+需要先了解 ETag 首部字段的含义,它是资源的唯一表示。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一表示。
```html
-ETag: "82e22293907ce725faf67773957acd12"
+ETag: "82e22293907ce725faf67773957acd12"
```
-可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。
+可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。
```html
-If-None-Match: "82e22293907ce725faf67773957acd12"
+If-None-Match: "82e22293907ce725faf67773957acd12"
```
-Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 Not Modified 响应,
+Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 Not Modified 响应,
```html
-Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
+Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
```
```html
-If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
+If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
```
-## 连接管理
+## 连接管理
-
+
-### 1. 短连接与长连接
+### 1. 短连接与长连接
-当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。
+当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。
-HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 Connection : close;而在 HTTP/1.1 之前默认是短连接的,如果需要长连接,则使用 Connection : Keep-Alive。
+HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 Connection : close;而在 HTTP/1.1 之前默认是短连接的,如果需要长连接,则使用 Connection : Keep-Alive。
-### 2. 流水线
+### 2. 流水线
-默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到应答过后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。
+默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到应答过后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。
流水线是在同一条长连接上发出连续的请求,而不用等待响应返回,这样可以避免连接延迟。
-## 内容协商
+## 内容协商
通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。
-### 1. 类型
+### 1. 类型
-**(一)服务端驱动型内容协商**
+**(一)服务端驱动型内容协商**
-客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Languag,服务器根据这些字段返回特定的资源。
+客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Languag,服务器根据这些字段返回特定的资源。
它存在以下问题:
-- 服务器很难知道客户端浏览器的全部信息;
-- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术)。
-- 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。
+- 服务器很难知道客户端浏览器的全部信息;
+- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术)。
+- 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。
-**(二)代理驱动型协商**
+**(二)代理驱动型协商**
-服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。
+服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。
-### 2. Vary
+### 2. Vary
```html
-Vary: Accept-Language
+Vary: Accept-Language
```
在使用内容协商的情况下,只有当缓存服务器中的缓存满足内容协商条件时,才能使用该缓存,否则应该向源服务器请求该资源。
-例如,一个客户端发送了一个包含 Accept-Language 首部字段的请求之后,源服务器返回的响应包含 `Vary: Accept-Language` 内容,缓存服务器对这个响应进行缓存之后,在客户端下一次访问同一个 URL 资源,并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。
+例如,一个客户端发送了一个包含 Accept-Language 首部字段的请求之后,源服务器返回的响应包含 `Vary: Accept-Language` 内容,缓存服务器对这个响应进行缓存之后,在客户端下一次访问同一个 URL 资源,并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。
-## 内容编码
+## 内容编码
内容编码将实体主体进行压缩,从而减少传输的数据量。常用的内容编码有:gzip、compress、deflate、identity。
-浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级,服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应中,Vary 首部中至少要包含 Content-Encoding,这样的话,缓存服务器就可以对资源的不同展现形式进行缓存。
+浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级,服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应中,Vary 首部中至少要包含 Content-Encoding,这样的话,缓存服务器就可以对资源的不同展现形式进行缓存。
-## 范围请求
+## 范围请求
如果网络出现中断,服务器只发送了一部分数据,范围请求可以使得客户端只请求服务器未发送的那部分数据,从而避免服务器重新发送所有数据。
-### 1. Range
+### 1. Range
-在请求报文中添加 Range 首部字段指定请求的范围。
+在请求报文中添加 Range 首部字段指定请求的范围。
```html
-GET /z4d4kWk.jpg HTTP/1.1
-Host: i.imgur.com
-Range: bytes=0-1023
+GET /z4d4kWk.jpg HTTP/1.1
+Host: i.imgur.com
+Range: bytes=0-1023
```
-请求成功的话服务器返回的响应包含 206 Partial Content 状态码。
+请求成功的话服务器返回的响应包含 206 Partial Content 状态码。
```html
-HTTP/1.1 206 Partial Content
-Content-Range: bytes 0-1023/146515
-Content-Length: 1024
+HTTP/1.1 206 Partial Content
+Content-Range: bytes 0-1023/146515
+Content-Length: 1024
...
-(binary content)
+(binary content)
```
-### 2. Accept-Ranges
+### 2. Accept-Ranges
-响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求,可以处理使用 bytes,否则使用 none。
+响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求,可以处理使用 bytes,否则使用 none。
```html
-Accept-Ranges: bytes
+Accept-Ranges: bytes
```
-### 3. 响应状态码
+### 3. 响应状态码
-- 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。
-- 在请求的范围越界的情况下,服务器会返回 416 Requested Range Not Satisfiable 状态码。
-- 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。
+- 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。
+- 在请求的范围越界的情况下,服务器会返回 416 Requested Range Not Satisfiable 状态码。
+- 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。
-## 分块传输编码
+## 分块传输编码
-Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。
+Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。
-## 多部分对象集合
+## 多部分对象集合
-一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。
+一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。
例如,上传多个表单时可以使用如下方式:
```html
-Content-Type: multipart/form-data; boundary=AaB03x
+Content-Type: multipart/form-data; boundary=AaB03x
--AaB03x
-Content-Disposition: form-data; name="submit-name"
+Content-Disposition: form-data; name="submit-name"
Larry
--AaB03x
-Content-Disposition: form-data; name="files"; filename="file1.txt"
-Content-Type: text/plain
+Content-Disposition: form-data; name="files"; filename="file1.txt"
+Content-Type: text/plain
-... contents of file1.txt ...
+... contents of file1.txt ...
--AaB03x--
```
-## 虚拟主机
+## 虚拟主机
-HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。
+HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。
-## 通信数据转发
+## 通信数据转发
-### 1. 代理
+### 1. 代理
代理服务器接受客户端的请求,并且转发给其它服务器。
使用代理的主要目的是:
-- 缓存
-- 网络访问控制
-- 访问日志记录
+- 缓存
+- 网络访问控制
+- 访问日志记录
代理服务器分为正向代理和反向代理两种,用户察觉得到正向代理的存在,而反向代理一般位于内部网络中,用户察觉不到。
-
+
-
+
-### 2. 网关
+### 2. 网关
-与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。
+与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。
-### 3. 隧道
+### 3. 隧道
-使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。
+使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。
-# 六、HTTPs
+# 六、HTTPs
-HTTP 有以下安全性问题:
+HTTP 有以下安全性问题:
-- 使用明文进行通信,内容可能会被窃听;
-- 不验证通信方的身份,通信方的身份有可能遭遇伪装;
-- 无法证明报文的完整性,报文有可能遭篡改。
+- 使用明文进行通信,内容可能会被窃听;
+- 不验证通信方的身份,通信方的身份有可能遭遇伪装;
+- 无法证明报文的完整性,报文有可能遭篡改。
-HTTPs 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信。也就是说 HTTPs 使用了隧道进行通信。
+HTTPs 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信。也就是说 HTTPs 使用了隧道进行通信。
-通过使用 SSL,HTTPs 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。
+通过使用 SSL,HTTPs 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。
-
+
-## 加密
+## 加密
-### 1. 对称密钥加密
+### 1. 对称密钥加密
-对称密钥加密(Symmetric-Key Encryption),加密的加密和解密使用同一密钥。
+对称密钥加密(Symmetric-Key Encryption),加密的加密和解密使用同一密钥。
-- 优点:运算速度快;
-- 缺点:密钥容易被获取。
+- 优点:运算速度快;
+- 缺点:密钥容易被获取。
-
+
-### 2. 公开密钥加密
+### 2. 公开密钥加密
-公开密钥加密(Public-Key Encryption),也称为非对称密钥加密,使用一对密钥用于加密和解密,分别为公开密钥和私有密钥。公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。
+公开密钥加密(Public-Key Encryption),也称为非对称密钥加密,使用一对密钥用于加密和解密,分别为公开密钥和私有密钥。公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。
-- 优点:更为安全;
-- 缺点:运算速度慢;
+- 优点:更为安全;
+- 缺点:运算速度慢;
-
+
-### 3. HTTPs 采用的加密方式
+### 3. HTTPs 采用的加密方式
-HTTPs 采用混合的加密机制,使用公开密钥加密用于传输对称密钥来保证安全性,之后使用对称密钥加密进行通信来保证效率。(下图中的 Session Key 就是对称密钥)
+HTTPs 采用混合的加密机制,使用公开密钥加密用于传输对称密钥来保证安全性,之后使用对称密钥加密进行通信来保证效率。(下图中的 Session Key 就是对称密钥)
-
+
-## 认证
+## 认证
-通过使用 **证书** 来对通信方进行认证。
+通过使用 **证书** 来对通信方进行认证。
-数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。
+数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。
-服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。
+服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。
-进行 HTTPs 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。
+进行 HTTPs 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。
-
+
-## 完整性保护
+## 完整性保护
-SSL 提供报文摘要功能来进行完整性保护。
+SSL 提供报文摘要功能来进行完整性保护。
-HTTP 也提供了 MD5 报文摘要功能,但是却不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生篡改。
+HTTP 也提供了 MD5 报文摘要功能,但是却不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生篡改。
-HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。
+HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。
-## HTTPs 的缺点
+## HTTPs 的缺点
-- 因为需要进行加密解密等过程,因此速度会更慢;
-- 需要支付证书授权的高费用。
+- 因为需要进行加密解密等过程,因此速度会更慢;
+- 需要支付证书授权的高费用。
-## 配置 HTTPs
+## 配置 HTTPs
-[Nginx 配置 HTTPS 服务器](https://aotu.io/notes/2016/08/16/nginx-https/index.html)
+[Nginx 配置 HTTPS 服务器](https://aotu.io/notes/2016/08/16/nginx-https/index.html)
-# 七、Web 攻击技术
+# 七、Web 攻击技术
-## 跨站脚本攻击
+## 跨站脚本攻击
-### 1. 概念
+### 1. 概念
-跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。
+跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。
例如有一个论坛网站,攻击者可以在上面发布以下内容:
```html
-
+
```
之后该内容可能会被渲染成以下形式:
```html
-
+
```
-另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。
+另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。
-### 2. 危害
+### 2. 危害
-- 窃取用户的 Cookie 值
-- 伪造虚假的输入表单骗取个人信息
-- 显示伪造的文章或者图片
+- 窃取用户的 Cookie 值
+- 伪造虚假的输入表单骗取个人信息
+- 显示伪造的文章或者图片
-### 3. 防范手段
+### 3. 防范手段
-**(一)设置 Cookie 为 HttpOnly**
+**(一)设置 Cookie 为 HttpOnly**
-设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,在一定程度上可以防止 XSS 窃取用户的 Cookie 信息。
+设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,在一定程度上可以防止 XSS 窃取用户的 Cookie 信息。
-**(二)过滤特殊字符**
+**(二)过滤特殊字符**
-许多语言都提供了对 HTML 的过滤:
+许多语言都提供了对 HTML 的过滤:
-- PHP 的 htmlentities() 或是 htmlspecialchars()。
-- Python 的 cgi.escape()。
-- Java 的 xssprotect (Open Source Library)。
-- Node.js 的 node-validator。
+- PHP 的 htmlentities() 或是 htmlspecialchars()。
+- Python 的 cgi.escape()。
+- Java 的 xssprotect (Open Source Library)。
+- Node.js 的 node-validator。
-例如 htmlspecialchars() 可以将 `<` 转义为 `<`,将 `>` 转义为 `>`,从而避免 HTML 和 Jascript 代码的运行。
+例如 htmlspecialchars() 可以将 `<` 转义为 `<`,将 `>` 转义为 `>`,从而避免 HTML 和 Jascript 代码的运行。
-**(三)富文本编辑器的处理**
+**(三)富文本编辑器的处理**
-富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。
+富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。
-富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,可以定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。
+富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,可以定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。
-以下例子中,form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。
+以下例子中,form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。
-[XSS 过滤在线测试](http://jsxss.com/zh/try.html)
+[XSS 过滤在线测试](http://jsxss.com/zh/try.html)
```html
-XSS Demo
+XSS Demo
-
-Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist.
+
+Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist.
hello
- http
+ http
Features:
- Specifies HTML tags and their attributes allowed with whitelist
- Handle any tags or attributes using custom function
+ Specifies HTML tags and their attributes allowed with whitelist
+ Handle any tags or attributes using custom function
-
```
```html
-XSS Demo
+XSS Demo
-Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist.
+Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist.
<form>
- <input type="text" name="q" value="test">
- <button id="submit">Submit</button>
+ <input type="text" name="q" value="test">
+ <button id="submit">Submit</button>
</form>
hello
- http
+ http
Features:
- Specifies HTML tags and their attributes allowed with whitelist
- Handle any tags or attributes using custom function
+ Specifies HTML tags and their attributes allowed with whitelist
+ Handle any tags or attributes using custom function
-<script type="text/javascript">
+<script type="text/javascript">
alert(/xss/);
</script>
```
-## 跨站请求伪造
+## 跨站请求伪造
-### 1. 概念
+### 1. 概念
-跨站请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
+跨站请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
-XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户浏览器的信任。
+XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户浏览器的信任。
-假如一家银行用以执行转账操作的 URL 地址如下:
+假如一家银行用以执行转账操作的 URL 地址如下:
```
http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName。
@@ -848,245 +783,262 @@ http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
```
- 。
+ 。
```
-如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。
+如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
-透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。
+透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。
-### 2. 防范手段
+### 2. 防范手段
-**(一)检查 Referer 字段**
+**(一)检查 Referer 字段**
-HTTP 头中有一个 Referer 字段,这个字段用于标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。
+HTTP 头中有一个 Referer 字段,这个字段用于标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。
-这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
+这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
-**(二)添加校验 Token**
+**(二)添加校验 Token**
-由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 Token 的值为空或者错误,拒绝这个可疑请求。
+由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 Token 的值为空或者错误,拒绝这个可疑请求。
也可以要求用户输入验证码来进行校验。
-## SQL 注入攻击
+## SQL 注入攻击
-### 1. 概念
+### 1. 概念
-服务器上的数据库运行非法的 SQL 语句,主要通过拼接来完成。
+服务器上的数据库运行非法的 SQL 语句,主要通过拼接来完成。
-### 2. 攻击原理
+### 2. 攻击原理
-例如一个网站登录验证的 SQL 查询代码为:
+例如一个网站登录验证的 SQL 查询代码为:
```sql
-strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"
+strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"
```
如果填入以下内容:
```sql
-userName = "1' OR '1'='1";
-passWord = "1' OR '1'='1";
+userName = "1' OR '1'='1";
+passWord = "1' OR '1'='1";
```
-那么 SQL 查询字符串为:
+那么 SQL 查询字符串为:
```sql
-strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');"
+strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');"
```
此时无需验证通过就能执行以下查询:
```sql
-strSQL = "SELECT * FROM users;"
+strSQL = "SELECT * FROM users;"
```
-### 3. 防范手段
+### 3. 防范手段
-**(一)使用参数化查询**
+**(一)使用参数化查询**
-以下以 Java 中的 PreparedStatement 为例,它是预先编译的 SQL 语句,可以传入适当参数并且多次执行。由于没有拼接的过程,因此可以防止 SQL 注入的发生。
+以下以 Java 中的 PreparedStatement 为例,它是预先编译的 SQL 语句,可以传入适当参数并且多次执行。由于没有拼接的过程,因此可以防止 SQL 注入的发生。
```java
-PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE userid=? AND password=?");
-stmt.setString(1, userid);
-stmt.setString(2, password);
-ResultSet rs = stmt.executeQuery();
+PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE userid=? AND password=?");
+stmt.setString(1, userid);
+stmt.setString(2, password);
+ResultSet rs = stmt.executeQuery();
```
-**(二)单引号转换**
+**(二)单引号转换**
-将传入的参数中的单引号转换为连续两个单引号,PHP 中的 Magic quote 可以完成这个功能。
+将传入的参数中的单引号转换为连续两个单引号,PHP 中的 Magic quote 可以完成这个功能。
-## 拒绝服务攻击
+## 拒绝服务攻击
-拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。
+拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。
-分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。
+分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。
-> [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A)
+> [维基百科:拒绝服务攻击](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 用于获取资源,而 POST 用于传输实体主体。
+GET 用于获取资源,而 POST 用于传输实体主体。
-## 参数
+## 参数
-GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。
+GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。
```
-GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
+GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
```
```
-POST /test/demo_form.asp HTTP/1.1
-Host: w3schools.com
+POST /test/demo_form.asp HTTP/1.1
+Host: w3schools.com
name1=value1&name2=value2
```
-不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。
+不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。
-因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码,例如`中文`会转换为`%E4%B8%AD%E6%96%87`,而空格会转换为`%20`。POST 支持标准字符集。
+因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码,例如`中文`会转换为`%E4%B8%AD%E6%96%87`,而空格会转换为`%20`。POST 支持标准字符集。
-## 安全
+## 安全
-安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。
+安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。
-GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。
+GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。
-安全的方法除了 GET 之外还有:HEAD、OPTIONS。
+安全的方法除了 GET 之外还有:HEAD、OPTIONS。
-不安全的方法除了 POST 之外还有 PUT、DELETE。
+不安全的方法除了 POST 之外还有 PUT、DELETE。
-## 幂等性
+## 幂等性
-幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。所有的安全方法也都是幂等的。
+幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。在正确实现的条件下,GET,HEAD,PUT 和 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
-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
-POST /add_row HTTP/1.1 -> Adds a 2nd row
-POST /add_row HTTP/1.1 -> Adds a 3rd row
+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 是幂等的,即便不同的请求接收到的状态码不一样:
```
-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
+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
```
-## 可缓存
+## 可缓存
如果要对响应进行缓存,需要满足以下条件:
-- 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。
-- 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。
-- 响应报文的 Cache-Control 首部字段没有指定不进行缓存。
+- 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。
+- 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。
+- 响应报文的 Cache-Control 首部字段没有指定不进行缓存。
-## XMLHttpRequest
+## XMLHttpRequest
-为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest:
+为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest:
-> XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。
+> XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。
-在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。而 GET 方法 Header 和 Data 会一起发送。
+在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。而 GET 方法 Header 和 Data 会一起发送。
-# 九、HTTP/1.0 与 HTTP/1.1 的区别
+# 九、HTTP/1.0 与 HTTP/1.1 的区别
-> 详细内容请见上文
+> 详细内容请见上文
-- HTTP/1.1 默认是长连接
+- HTTP/1.1 默认是长连接
-- HTTP/1.1 支持管线化处理
+- HTTP/1.1 支持管线化处理
-- HTTP/1.1 支持同时打开多个 TCP 连接
+- HTTP/1.1 支持同时打开多个 TCP 连接
-- HTTP/1.1 支持虚拟主机
+- HTTP/1.1 支持虚拟主机
-- HTTP/1.1 新增状态码 100
+- HTTP/1.1 新增状态码 100
-- HTTP/1.1 支持分块传输编码
+- HTTP/1.1 支持分块传输编码
-- HTTP/1.1 新增缓存处理指令 max-age
+- HTTP/1.1 新增缓存处理指令 max-age
-# 十、HTTP/2.0
+# 十、HTTP/2.0
-## HTTP/1.x 缺陷
+## HTTP/1.x 缺陷
- HTTP/1.x 实现简单是以牺牲应用性能为代价的:
+ HTTP/1.x 实现简单是以牺牲应用性能为代价的:
-- 客户端需要使用多个连接才能实现并发和缩短延迟;
-- 不会压缩请求和响应首部,从而导致不必要的网络流量;
-- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。
+- 客户端需要使用多个连接才能实现并发和缩短延迟;
+- 不会压缩请求和响应首部,从而导致不必要的网络流量;
+- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。
-## 二进制分帧层
+## 二进制分帧层
-HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。
+HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。
-
+
-在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。一个数据流都有一个唯一标识符和可选的优先级信息,用于承载双向信息。消息(Message)是与逻辑请求或响应消息对应的完整的一系列帧。帧(Fram)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。
+在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。一个数据流都有一个唯一标识符和可选的优先级信息,用于承载双向信息。消息(Message)是与逻辑请求或响应消息对应的完整的一系列帧。帧(Fram)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。
-
+
-## 服务端推送
+## 服务端推送
-HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。
+HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。
-
+
-## 首部压缩
+## 首部压缩
-HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。
+HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。
-
+
-# 参考资料
+# 参考资料
-- 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014.
-- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
-- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
-- [htmlspecialchars](http://php.net/manual/zh/function.htmlspecialchars.php)
-- [How to Fix SQL Injection Using Java PreparedStatement & CallableStatement](https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement)
-- [浅谈 HTTP 中 Get 与 Post 的区别](https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html)
-- [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/)
-- [Cookie 与 Session 的区别](https://juejin.im/entry/5766c29d6be3ff006a31b84e#comment)
-- [COOKIE 和 SESSION 有什么区别](https://www.zhihu.com/question/19786827)
-- [Cookie/Session 的机制与安全](https://harttle.land/2015/08/10/cookie-session.html)
-- [HTTPS 证书原理](https://shijianan.com/2017/06/11/https/)
-- [维基百科:跨站脚本](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/)
-- [Symmetric vs. Asymmetric Encryption – What are differences?](https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences)
-- [Web 性能优化与 HTTP/2](https://www.kancloud.cn/digest/web-performance-http2)
-- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
+- 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014.
+- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
+- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
+- [htmlspecialchars](http://php.net/manual/zh/function.htmlspecialchars.php)
+- [How to Fix SQL Injection Using Java PreparedStatement & CallableStatement](https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement)
+- [浅谈 HTTP 中 Get 与 Post 的区别](https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html)
+- [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/)
+- [Cookie 与 Session 的区别](https://juejin.im/entry/5766c29d6be3ff006a31b84e#comment)
+- [COOKIE 和 SESSION 有什么区别](https://www.zhihu.com/question/19786827)
+- [Cookie/Session 的机制与安全](https://harttle.land/2015/08/10/cookie-session.html)
+- [HTTPS 证书原理](https://shijianan.com/2017/06/11/https/)
+- [维基百科:跨站脚本](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/)
+- [Symmetric vs. Asymmetric Encryption – What are differences?](https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences)
+- [Web 性能优化与 HTTP/2](https://www.kancloud.cn/digest/web-performance-http2)
+- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
+---bottom---CyC---
+![](index_files/f716427a-94f2-4875-9c86-98793cf5dcc3.jpg)
+![](index_files/HTTP_RequestMessageExample.png)
+![](index_files/HTTP_ResponseMessageExample.png)
+![](index_files/dc00f70e-c5c8-4d20-baf1-2d70014a97e3.jpg)
+![](index_files/HTTP1_x_Connections.png)
+![](index_files/a314bb79-5b18-4e63-a976-3448bffa6f1b.png)
+![](index_files/2d09a847-b854-439c-9198-b29c65810944.png)
+![](index_files/ssl-offloading.jpg)
+![](index_files/7fffa4b8-b36d-471f-ad0c-a88ee763bb76.png)
+![](index_files/39ccb299-ee99-4dd1-b8b4-2f9ec9495cb4.png)
+![](index_files/How-HTTPS-Works.png)
+![](index_files/2017-06-11-ca.png)
+![](index_files/86e6a91d-a285-447a-9345-c5484b8d0c47.png)
+![](index_files/af198da1-2480-4043-b07f-a3b91a88b815.png)
+![](index_files/e3f1657c-80fc-4dfa-9643-bf51abd201c6.png)
+![](index_files/_u4E0B_u8F7D.png)
diff --git a/notes/Java IO.md b/notes/Java IO.md
index 885ecbe1..803bb200 100644
--- a/notes/Java IO.md
+++ b/notes/Java IO.md
@@ -1,91 +1,67 @@
-
-* [一、概览](#一概览)
-* [二、磁盘操作](#二磁盘操作)
-* [三、字节操作](#三字节操作)
-* [四、字符操作](#四字符操作)
-* [五、对象操作](#五对象操作)
-* [六、网络操作](#六网络操作)
- * [InetAddress](#inetaddress)
- * [URL](#url)
- * [Sockets](#sockets)
- * [Datagram](#datagram)
-* [七、NIO](#七nio)
- * [流与块](#流与块)
- * [通道与缓冲区](#通道与缓冲区)
- * [缓冲区状态变量](#缓冲区状态变量)
- * [文件 NIO 实例](#文件-nio-实例)
- * [选择器](#选择器)
- * [套接字 NIO 实例](#套接字-nio-实例)
- * [内存映射文件](#内存映射文件)
- * [对比](#对比)
-* [八、参考资料](#八参考资料)
-
+# 一、概览
+Java 的 I/O 大概可以分成以下几类:
-# 一、概览
+1. 磁盘操作:File
+2. 字节操作:InputStream 和 OutputStream
+3. 字符操作:Reader 和 Writer
+4. 对象操作:Serializable
+5. 网络操作:Socket
+6. 新的输入/输出:NIO
-Java 的 I/O 大概可以分成以下几类:
+# 二、磁盘操作
-1. 磁盘操作:File
-2. 字节操作:InputStream 和 OutputStream
-3. 字符操作:Reader 和 Writer
-4. 对象操作:Serializable
-5. 网络操作:Socket
-6. 新的输入/输出:NIO
+File 类可以用于表示文件和目录,但是它只用于表示文件的信息,而不表示文件的内容。
-# 二、磁盘操作
+# 三、字节操作
-File 类可以用于表示文件和目录,但是它只用于表示文件的信息,而不表示文件的内容。
+
-# 三、字节操作
+Java I/O 使用了装饰者模式来实现。以 InputStream 为例,InputStream 是抽象组件,FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
-
-
-Java I/O 使用了装饰者模式来实现。以 InputStream 为例,InputStream 是抽象组件,FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
-
-实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
+实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
```java
-BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
```
-DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
+DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
批量读入文件内容到字节数组:
```java
-byte[] buf = new byte[20*1024];
-int bytes = 0;
-// 最多读取 buf.length 个字节,返回的是实际读取的个数,返回 -1 的时候表示读到 eof,即文件尾
-while((bytes = in.read(buf, 0 , buf.length)) != -1) {
- // ...
+byte[] buf = new byte[20*1024];
+int bytes = 0;
+// 最多读取 buf.length 个字节,返回的是实际读取的个数,返回 -1 的时候表示读到 eof,即文件尾
+while((bytes = in.read(buf, 0 , buf.length)) != -1) {
+ // ...
}
```
-# 四、字符操作
+# 四、字符操作
-不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
+不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
-InputStreamReader 实现从文本文件的字节流解码成字符流;OutputStreamWriter 实现字符流编码成为文本文件的字节流。它们继承自 Reader 和 Writer。
+InputStreamReader 实现从文本文件的字节流解码成字符流;OutputStreamWriter 实现字符流编码成为文本文件的字节流。它们继承自 Reader 和 Writer。
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
```java
-byte[] bytes = str.getBytes(encoding); // 编码
-String str = new String(bytes, encoding); // 解码
+byte[] bytes = str.getBytes(encoding); // 编码
+String str = new String(bytes, encoding); // 解码
```
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
-- GBK 编码中,中文占 2 个字节,英文占 1 个字节;
-- UTF-8 编码中,中文占 3 个字节,英文占 1 个字节;
-- UTF-16be 编码中,中文和英文都占 2 个字节。
+- GBK 编码中,中文占 2 个字节,英文占 1 个字节;
+- UTF-8 编码中,中文占 3 个字节,英文占 1 个字节;
+- UTF-16be 编码中,中文和英文都占 2 个字节。
-UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
+UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
-Java 使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码正是为了让一个中文或者一个英文都能使用一个 char 来存储。
+Java 使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码正是为了让一个中文或者一个英文都能使用一个 char 来存储。
-# 五、对象操作
+# 五、对象操作
序列化就是将一个对象转换成字节序列,方便存储和传输。
@@ -93,100 +69,100 @@ Java 使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码
反序列化:ObjectInputStream.readObject()
-序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现。
+序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现。
-transient 关键字可以使一些属性不会被序列化。
+transient 关键字可以使一些属性不会被序列化。
-**ArrayList 序列化和反序列化的实现** :ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
+**ArrayList 序列化和反序列化的实现**:ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
```java
-private transient Object[] elementData;
+private transient Object[] elementData;
```
-# 六、网络操作
+# 六、网络操作
-Java 中的网络支持:
+Java 中的网络支持:
-1. InetAddress:用于表示网络上的硬件资源,即 IP 地址;
-2. URL:统一资源定位符,通过 URL 可以直接读取或者写入网络上的数据;
-3. Sockets:使用 TCP 协议实现网络通信;
-4. Datagram:使用 UDP 协议实现网络通信。
+1. InetAddress:用于表示网络上的硬件资源,即 IP 地址;
+2. URL:统一资源定位符,通过 URL 可以直接读取或者写入网络上的数据;
+3. Sockets:使用 TCP 协议实现网络通信;
+4. Datagram:使用 UDP 协议实现网络通信。
-## InetAddress
+## InetAddress
没有公有构造函数,只能通过静态方法来创建实例。
```java
-InetAddress.getByName(String host);
-InetAddress.getByAddress(byte[] addr);
+InetAddress.getByName(String host);
+InetAddress.getByAddress(byte[] addr);
```
-## URL
+## URL
-可以直接从 URL 中读取字节流数据
+可以直接从 URL 中读取字节流数据
```java
-URL url = new URL("http://www.baidu.com");
-InputStream is = url.openStream(); // 字节流
-InputStreamReader isr = new InputStreamReader(is, "utf-8"); // 字符流
-BufferedReader br = new BufferedReader(isr);
-String line = br.readLine();
-while (line != null) {
- System.out.println(line);
- line = br.readLine();
+URL url = new URL("http://www.baidu.com");
+InputStream is = url.openStream(); // 字节流
+InputStreamReader isr = new InputStreamReader(is, "utf-8"); // 字符流
+BufferedReader br = new BufferedReader(isr);
+String line = br.readLine();
+while (line != null) {
+ System.out.println(line);
+ line = br.readLine();
}
br.close();
isr.close();
is.close();
```
-## Sockets
+## Sockets
-- ServerSocket:服务器端类
-- Socket:客户端类
-- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
+- ServerSocket:服务器端类
+- Socket:客户端类
+- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
-
+![](index_files/ClienteServidorSockets1521731145260.jpg)
-## Datagram
+## Datagram
-- DatagramPacket:数据包类
-- DatagramSocket:通信类
+- DatagramPacket:数据包类
+- DatagramSocket:通信类
-# 七、NIO
+# 七、NIO
-- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
-- [Java NIO 浅析](https://tech.meituan.com/nio.html)
-- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
+- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
+- [Java NIO 浅析](https://tech.meituan.com/nio.html)
+- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
-新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
+新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
-## 流与块
+## 流与块
-I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
+I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
-面向流的 I/O 一次处理一个字节数据,一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
+面向流的 I/O 一次处理一个字节数据,一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
-面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
+面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
-I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
+I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
-## 通道与缓冲区
+## 通道与缓冲区
-### 1. 通道
+### 1. 通道
-通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
+通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
-通道与流的不同之处在于,流只能在一个方向上移动,(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
+通道与流的不同之处在于,流只能在一个方向上移动,(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
通道包括以下类型:
-- FileChannel:从文件中读写数据;
-- DatagramChannel:通过 UDP 读写网络中数据;
-- SocketChannel:通过 TCP 读写网络中数据;
-- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
+- FileChannel:从文件中读写数据;
+- DatagramChannel:通过 UDP 读写网络中数据;
+- SocketChannel:通过 TCP 读写网络中数据;
+- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
-### 2. 缓冲区
+### 2. 缓冲区
发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
@@ -194,275 +170,283 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重
缓冲区包括以下类型:
-- ByteBuffer
-- CharBuffer
-- ShortBuffer
-- IntBuffer
-- LongBuffer
-- FloatBuffer
-- DoubleBuffer
+- ByteBuffer
+- CharBuffer
+- ShortBuffer
+- IntBuffer
+- LongBuffer
+- FloatBuffer
+- DoubleBuffer
-## 缓冲区状态变量
+## 缓冲区状态变量
-- capacity:最大容量;
-- position:当前已经读写的字节数;
-- limit:还可以读写的字节数。
+- capacity:最大容量;
+- position:当前已经读写的字节数;
+- limit:还可以读写的字节数。
状态变量的改变过程举例:
-① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
+① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
-
+![](index_files/1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png)
-② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 移动设置为 5,limit 保持不变。
+② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 移动设置为 5,limit 保持不变。
-
+![](index_files/80804f52-8815-4096-b506-48eef3eed5c6.png)
-③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
+③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
-
+![](index_files/952e06bd-5a65-4cab-82e4-dd1536462f38.png)
-④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
+④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
-
+![](index_files/b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png)
-⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
+⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
-
+![](index_files/67bf5487-c45d-49b6-b9c0-a058d8c68902.png)
-## 文件 NIO 实例
+## 文件 NIO 实例
-以下展示了使用 NIO 快速复制文件的实例:
+以下展示了使用 NIO 快速复制文件的实例:
```java
-public class FastCopyFile {
- public static void main(String args[]) throws Exception {
+public class FastCopyFile {
+ public static void main(String args[]) throws Exception {
- String inFile = "data/abc.txt";
- String outFile = "data/abc-copy.txt";
+ String inFile = "data/abc.txt";
+ String outFile = "data/abc-copy.txt";
- // 获得源文件的输入字节流
- FileInputStream fin = new FileInputStream(inFile);
- // 获取输入字节流的文件通道
- FileChannel fcin = fin.getChannel();
+ // 获得源文件的输入字节流
+ FileInputStream fin = new FileInputStream(inFile);
+ // 获取输入字节流的文件通道
+ FileChannel fcin = fin.getChannel();
- // 获取目标文件的输出字节流
- FileOutputStream fout = new FileOutputStream(outFile);
- // 获取输出字节流的通道
- FileChannel fcout = fout.getChannel();
+ // 获取目标文件的输出字节流
+ FileOutputStream fout = new FileOutputStream(outFile);
+ // 获取输出字节流的通道
+ FileChannel fcout = fout.getChannel();
- // 为缓冲区分配 1024 个字节
- ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
+ // 为缓冲区分配 1024 个字节
+ ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
- while (true) {
- // 从输入通道中读取数据到缓冲区中
- int r = fcin.read(buffer);
- // read() 返回 -1 表示 EOF
- if (r == -1)
- break;
- // 切换读写
- buffer.flip();
- // 把缓冲区的内容写入输出文件中
- fcout.write(buffer);
- // 清空缓冲区
- buffer.clear();
- }
- }
+ while (true) {
+ // 从输入通道中读取数据到缓冲区中
+ int r = fcin.read(buffer);
+ // read() 返回 -1 表示 EOF
+ if (r == -1)
+ break;
+ // 切换读写
+ buffer.flip();
+ // 把缓冲区的内容写入输出文件中
+ fcout.write(buffer);
+ // 清空缓冲区
+ buffer.clear();
+ }
+ }
}
```
-## 选择器
+## 选择器
-一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去检查多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
+一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去检查多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件具有更好的性能。
-
+![](index_files/4d930e22-f493-49ae-8dff-ea21cd6895dc.png)
-### 1. 创建选择器
+### 1. 创建选择器
```java
-Selector selector = Selector.open();
+Selector selector = Selector.open();
```
-### 2. 将通道注册到选择器上
+### 2. 将通道注册到选择器上
```java
-ServerSocketChannel ssChannel = ServerSocketChannel.open();
+ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
-ssChannel.register(selector, SelectionKey.OP_ACCEPT);
+ssChannel.register(selector, SelectionKey.OP_ACCEPT);
```
通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它时间,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
-- SelectionKey.OP_CONNECT
-- SelectionKey.OP_ACCEPT
-- SelectionKey.OP_READ
-- SelectionKey.OP_WRITE
+- SelectionKey.OP_CONNECT
+- SelectionKey.OP_ACCEPT
+- SelectionKey.OP_READ
+- SelectionKey.OP_WRITE
-它们在 SelectionKey 的定义如下:
+它们在 SelectionKey 的定义如下:
```java
-public static final int OP_READ = 1 << 0;
-public static final int OP_WRITE = 1 << 2;
-public static final int OP_CONNECT = 1 << 3;
-public static final int OP_ACCEPT = 1 << 4;
+public static final int OP_READ = 1 << 0;
+public static final int OP_WRITE = 1 << 2;
+public static final int OP_CONNECT = 1 << 3;
+public static final int OP_ACCEPT = 1 << 4;
```
可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:
```java
-int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
+int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
```
-### 3. 监听事件
+### 3. 监听事件
```java
-int num = selector.select();
+int num = selector.select();
```
-使用 select() 来监听事件到达,它会一直阻塞直到有至少一个事件到达。
+使用 select() 来监听事件到达,它会一直阻塞直到有至少一个事件到达。
-### 4. 获取到达的事件
+### 4. 获取到达的事件
```java
-Set keys = selector.selectedKeys();
-Iterator keyIterator = keys.iterator();
-while (keyIterator.hasNext()) {
- SelectionKey key = keyIterator.next();
- if (key.isAcceptable()) {
- // ...
- } else if (key.isReadable()) {
- // ...
- }
- keyIterator.remove();
+Set keys = selector.selectedKeys();
+Iterator keyIterator = keys.iterator();
+while (keyIterator.hasNext()) {
+ SelectionKey key = keyIterator.next();
+ if (key.isAcceptable()) {
+ // ...
+ } else if (key.isReadable()) {
+ // ...
+ }
+ keyIterator.remove();
}
```
-### 5. 事件循环
+### 5. 事件循环
-因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。
+因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。
```java
-while (true) {
- int num = selector.select();
- Set keys = selector.selectedKeys();
- Iterator keyIterator = keys.iterator();
- while (keyIterator.hasNext()) {
- SelectionKey key = keyIterator.next();
- if (key.isAcceptable()) {
- // ...
- } else if (key.isReadable()) {
- // ...
- }
- keyIterator.remove();
- }
+while (true) {
+ int num = selector.select();
+ Set keys = selector.selectedKeys();
+ Iterator keyIterator = keys.iterator();
+ while (keyIterator.hasNext()) {
+ SelectionKey key = keyIterator.next();
+ if (key.isAcceptable()) {
+ // ...
+ } else if (key.isReadable()) {
+ // ...
+ }
+ keyIterator.remove();
+ }
}
```
-## 套接字 NIO 实例
+## 套接字 NIO 实例
```java
-public class NIOServer {
+public class NIOServer {
- public static void main(String[] args) throws IOException {
- Selector selector = Selector.open();
+ public static void main(String[] args) throws IOException {
+ Selector selector = Selector.open();
- ServerSocketChannel ssChannel = ServerSocketChannel.open();
- ssChannel.configureBlocking(false);
- ssChannel.register(selector, SelectionKey.OP_ACCEPT);
+ ServerSocketChannel ssChannel = ServerSocketChannel.open();
+ ssChannel.configureBlocking(false);
+ ssChannel.register(selector, SelectionKey.OP_ACCEPT);
- ServerSocket serverSocket = ssChannel.socket();
- InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
- serverSocket.bind(address);
+ ServerSocket serverSocket = ssChannel.socket();
+ InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
+ serverSocket.bind(address);
- while (true) {
- selector.select();
- Set keys = selector.selectedKeys();
- Iterator keyIterator = keys.iterator();
- while (keyIterator.hasNext()) {
- SelectionKey key = keyIterator.next();
- if (key.isAcceptable()) {
- ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
- // 服务器会为每个新连接创建一个 SocketChannel
- SocketChannel sChannel = ssChannel1.accept();
- sChannel.configureBlocking(false);
- // 这个新连接主要用于从客户端读取数据
- sChannel.register(selector, SelectionKey.OP_READ);
- } else if (key.isReadable()) {
- SocketChannel sChannel = (SocketChannel) key.channel();
- System.out.println(readDataFromSocketChannel(sChannel));
- sChannel.close();
- }
- keyIterator.remove();
- }
- }
- }
+ while (true) {
+ selector.select();
+ Set keys = selector.selectedKeys();
+ Iterator keyIterator = keys.iterator();
+ while (keyIterator.hasNext()) {
+ SelectionKey key = keyIterator.next();
+ if (key.isAcceptable()) {
+ ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
+ // 服务器会为每个新连接创建一个 SocketChannel
+ SocketChannel sChannel = ssChannel1.accept();
+ sChannel.configureBlocking(false);
+ // 这个新连接主要用于从客户端读取数据
+ sChannel.register(selector, SelectionKey.OP_READ);
+ } else if (key.isReadable()) {
+ SocketChannel sChannel = (SocketChannel) key.channel();
+ System.out.println(readDataFromSocketChannel(sChannel));
+ sChannel.close();
+ }
+ keyIterator.remove();
+ }
+ }
+ }
- private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
- ByteBuffer buffer = ByteBuffer.allocate(1024);
- StringBuffer data = new StringBuffer();
- while (true) {
- buffer.clear();
- int n = sChannel.read(buffer);
- if (n == -1)
- break;
- buffer.flip();
- int limit = buffer.limit();
- char[] dst = new char[limit];
- for (int i = 0; i < limit; i++)
- dst[i] = (char) buffer.get(i);
- data.append(dst);
- buffer.clear();
- }
- return data.toString();
- }
+ private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
+ ByteBuffer buffer = ByteBuffer.allocate(1024);
+ StringBuffer data = new StringBuffer();
+ while (true) {
+ buffer.clear();
+ int n = sChannel.read(buffer);
+ if (n == -1)
+ break;
+ buffer.flip();
+ int limit = buffer.limit();
+ char[] dst = new char[limit];
+ for (int i = 0; i < limit; i++)
+ dst[i] = (char) buffer.get(i);
+ data.append(dst);
+ buffer.clear();
+ }
+ return data.toString();
+ }
}
```
```java
-public class NIOClient {
+public class NIOClient {
- public static void main(String[] args) throws IOException {
- Socket socket = new Socket("127.0.0.1", 8888);
- OutputStream out = socket.getOutputStream();
- String s = "hello world";
- out.write(s.getBytes());
- out.close();
- }
+ public static void main(String[] args) throws IOException {
+ Socket socket = new Socket("127.0.0.1", 8888);
+ OutputStream out = socket.getOutputStream();
+ String s = "hello world";
+ out.write(s.getBytes());
+ out.close();
+ }
}
```
-## 内存映射文件
+## 内存映射文件
-内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
+内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
只有文件中实际读取或者写入的部分才会映射到内存中。
-现代操作系统一般会根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java 内存映射机制只不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。
+现代操作系统一般会根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java 内存映射机制只不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。
向内存映射文件写入可能是危险的,仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
-下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
+下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
```java
-MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
+MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
```
-## 对比
+## 对比
-NIO 与普通 I/O 的区别主要有以下两点:
+NIO 与普通 I/O 的区别主要有以下两点:
-- NIO 是非阻塞的。应当注意,FileChannel 不能切换到非阻塞模式,套接字 Channel 可以。
-- NIO 面向块,I/O 面向流。
+- NIO 是非阻塞的。应当注意,FileChannel 不能切换到非阻塞模式,套接字 Channel 可以。
+- NIO 面向块,I/O 面向流。
-# 八、参考资料
+# 八、参考资料
-- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002.
-- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
-- [深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
-- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499)
-- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
-- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)
+- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002.
+- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
+- [深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
+- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499)
+- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
+- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)
+---bottom---CyC---
+![](index_files/DP-Decorator-java.io.png)
+![](index_files/ClienteServidorSockets1521731145260.jpg)
+![](index_files/1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png)
+![](index_files/4628274c-25b6-4053-97cf-d1239b44c43d.png)
+![](index_files/952e06bd-5a65-4cab-82e4-dd1536462f38.png)
+![](index_files/b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png)
+![](index_files/67bf5487-c45d-49b6-b9c0-a058d8c68902.png)
diff --git a/notes/Java 基础.md b/notes/Java 基础.md
index 6424600a..4cede783 100644
--- a/notes/Java 基础.md
+++ b/notes/Java 基础.md
@@ -1,304 +1,269 @@
-
-* [一、关键字](#一关键字)
- * [final](#final)
- * [static](#static)
-* [二、Object 通用方法](#二object-通用方法)
- * [概览](#概览)
- * [equals()](#equals)
- * [hashCode()](#hashcode)
- * [toString()](#tostring)
- * [clone()](#clone)
-* [四、继承](#四继承)
- * [访问权限](#访问权限)
- * [抽象类与接口](#抽象类与接口)
- * [super](#super)
- * [覆盖与重载](#覆盖与重载)
-* [五、String](#五string)
- * [String, StringBuffer and StringBuilder](#string,-stringbuffer-and-stringbuilder)
- * [String 不可变的原因](#string-不可变的原因)
- * [String.intern()](#stringintern)
-* [六、基本类型与运算](#六基本类型与运算)
- * [包装类型](#包装类型)
- * [switch](#switch)
-* [七、反射](#七反射)
-* [八、异常](#八异常)
-* [九、泛型](#九泛型)
-* [十、注解](#十注解)
-* [十一、特性](#十一特性)
- * [面向对象三大特性](#面向对象三大特性)
- * [Java 各版本的新特性](#java-各版本的新特性)
- * [Java 与 C++ 的区别](#java-与-c-的区别)
- * [JRE or JDK](#jre-or-jdk)
-* [参考资料](#参考资料)
-
+# 一、关键字
+## final
-# 一、关键字
-
-## final
-
-**1. 数据**
+**1. 数据**
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
-- 对于基本类型,final 使数值不变;
-- 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
+- 对于基本类型,final 使数值不变;
+- 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
```java
-final int x = 1;
-// x = 2; // cannot assign value to final variable 'x'
-final A y = new A();
-y.a = 1;
+final int x = 1;
+// x = 2; // cannot assign value to final variable 'x'
+final A y = new A();
+y.a = 1;
```
-**2. 方法**
+**2. 方法**
声明方法不能被子类覆盖。
-private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是覆盖基类方法,而是在子类中定义了一个新的方法。
+private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是覆盖基类方法,而是在子类中定义了一个新的方法。
-**3. 类**
+**3. 类**
声明类不允许被继承。
-## static
+## static
-**1. 静态变量**
+**1. 静态变量**
静态变量在内存中只存在一份,只在类初始化时赋值一次。
-- 静态变量:类所有的实例都共享静态变量,可以直接通过类名来访问它;
-- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
+- 静态变量:类所有的实例都共享静态变量,可以直接通过类名来访问它;
+- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
```java
-public class A {
- private int x; // 实例变量
- public static int y; // 静态变量
+public class A {
+ private int x; // 实例变量
+ public static int y; // 静态变量
}
```
-**2. 静态方法**
+**2. 静态方法**
静态方法在类加载的时候就存在了,它不依赖于任何实例,所以静态方法必须有实现,也就是说它不能是抽象方法(abstract)。
-**3. 静态语句块**
+**3. 静态语句块**
静态语句块在类初始化时运行一次。
-**4. 静态内部类**
+**4. 静态内部类**
内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非静态的变量和方法。
-**5. 静态导包**
+**5. 静态导包**
```java
-import static com.xxx.ClassName.*
+import static com.xxx.ClassName.*
```
-在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
+在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
-**6. 变量赋值顺序**
+**6. 变量赋值顺序**
静态变量的赋值和静态语句块的运行优先于实例变量的赋值和普通语句块的运行,静态变量的赋值和静态语句块的运行哪个先执行取决于它们在代码中的顺序。
```java
-public static String staticField = "静态变量";
+public static String staticField = "静态变量";
```
```java
-static {
- System.out.println("静态语句块");
+static {
+ System.out.println("静态语句块");
}
```
```java
-public String field = "实例变量";
+public String field = "实例变量";
```
```java
{
- System.out.println("普通语句块");
+ System.out.println("普通语句块");
}
```
最后才运行构造函数
```java
-public InitialOrderTest() {
- System.out.println("构造函数");
+public InitialOrderTest() {
+ System.out.println("构造函数");
}
```
存在继承的情况下,初始化顺序为:
-- 父类(静态变量、静态语句块)
-- 子类(静态变量、静态语句块)
-- 父类(实例变量、普通语句块)
-- 父类(构造函数)
-- 子类(实例变量、普通语句块)
-- 子类(构造函数)
+- 父类(静态变量、静态语句块)
+- 子类(静态变量、静态语句块)
+- 父类(实例变量、普通语句块)
+- 父类(构造函数)
+- 子类(实例变量、普通语句块)
+- 子类(构造函数)
-# 二、Object 通用方法
+# 二、Object 通用方法
-## 概览
+## 概览
```java
-public final native Class> getClass()
+public final native Class> getClass()
-public native int hashCode()
+public native int hashCode()
-public boolean equals(Object obj)
+public boolean equals(Object obj)
-protected native Object clone() throws CloneNotSupportedException
+protected native Object clone() throws CloneNotSupportedException
-public String toString()
+public String toString()
-public final native void notify()
+public final native void notify()
-public final native void notifyAll()
+public final native void notifyAll()
-public final native void wait(long timeout) throws InterruptedException
+public final native void wait(long timeout) throws InterruptedException
-public final void wait(long timeout, int nanos) throws InterruptedException
+public final void wait(long timeout, int nanos) throws InterruptedException
-public final void wait() throws InterruptedException
+public final void wait() throws InterruptedException
-protected void finalize() throws Throwable {}
+protected void finalize() throws Throwable {}
```
-## equals()
+## equals()
-**1. equals() 与 == 的区别**
+**1. equals() 与 == 的区别**
-- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
-- 对于引用类型,== 判断两个实例是否引用同一个对象,而 equals() 判断引用的对象是否等价。
+- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
+- 对于引用类型,== 判断两个实例是否引用同一个对象,而 equals() 判断引用的对象是否等价。
```java
-Integer x = new Integer(1);
-Integer y = new Integer(1);
-System.out.println(x.equals(y)); // true
-System.out.println(x == y); // false
+Integer x = new Integer(1);
+Integer y = new Integer(1);
+System.out.println(x.equals(y)); // true
+System.out.println(x == y); // false
```
-**2. 等价关系**
+**2. 等价关系**
(一)自反性
```java
-x.equals(x); // true
+x.equals(x); // true
```
(二)对称性
```java
-x.equals(y) == y.equals(x); // true
+x.equals(y) == y.equals(x); // true
```
(三)传递性
```java
-if (x.equals(y) && y.equals(z))
- x.equals(z); // true;
+if (x.equals(y) && y.equals(z))
+ x.equals(z); // true;
```
(四)一致性
-多次调用 equals() 方法结果不变
+多次调用 equals() 方法结果不变
```java
-x.equals(y) == x.equals(y); // true
+x.equals(y) == x.equals(y); // true
```
-(五)与 null 的比较
+(五)与 null 的比较
-对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
+对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
```java
-x.euqals(null); // false;
+x.euqals(null); // false;
```
-**3. 实现**
+**3. 实现**
-- 检查是否为同一个对象的引用,如果是直接返回 true;
-- 检查是否是同一个类型,如果不是,直接返回 false;
-- 将 Object 实例进行转型;
-- 判断每个关键域是否相等。
+- 检查是否为同一个对象的引用,如果是直接返回 true;
+- 检查是否是同一个类型,如果不是,直接返回 false;
+- 将 Object 实例进行转型;
+- 判断每个关键域是否相等。
```java
-public class EqualExample {
- private int x;
- private int y;
- private int z;
+public class EqualExample {
+ private int x;
+ private int y;
+ private int z;
- public EqualExample(int x, int y, int z) {
- this.x = x;
- this.y = y;
- this.z = z;
- }
+ public EqualExample(int x, int y, int z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
- EqualExample that = (EqualExample) o;
+ EqualExample that = (EqualExample) o;
- if (x != that.x) return false;
- if (y != that.y) return false;
- return z == that.z;
- }
+ if (x != that.x) return false;
+ if (y != that.y) return false;
+ return z == that.z;
+ }
}
```
-## hashCode()
+## hashCode()
-hasCode() 返回散列值,而 equals() 是用来判断两个实例是否等价。等价的两个实例散列值一定要相同,但是散列值相同的两个实例不一定等价。
+hasCode() 返回散列值,而 equals() 是用来判断两个实例是否等价。等价的两个实例散列值一定要相同,但是散列值相同的两个实例不一定等价。
-在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个实例散列值也相等。
+在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个实例散列值也相等。
-下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例。
+下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例。
```java
-EqualExample e1 = new EqualExample(1, 1, 1);
-EqualExample e2 = new EqualExample(1, 1, 1);
-System.out.println(e1.equals(e2)); // true
-HashSet set = new HashSet<>();
+EqualExample e1 = new EqualExample(1, 1, 1);
+EqualExample e2 = new EqualExample(1, 1, 1);
+System.out.println(e1.equals(e2)); // true
+HashSet set = new HashSet<>();
set.add(e1);
set.add(e2);
-System.out.println(set.size()); // 2
+System.out.println(set.size()); // 2
```
-理想的散列函数应当具有均匀性,即不相等的实例应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
+理想的散列函数应当具有均匀性,即不相等的实例应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
-一个数与 31 相乘可以转换成移位和减法:`31\*x == (x<<5)-x`,编译器会自动进行这个优化。
+一个数与 31 相乘可以转换成移位和减法:`31\*x == (x<<5)-x`,编译器会自动进行这个优化。
```java
@Override
-public int hashCode() {
- int result = 17;
- result = 31 * result + x;
- result = 31 * result + y;
- result = 31 * result + z;
- return result;
+public int hashCode() {
+ int result = 17;
+ result = 31 * result + x;
+ result = 31 * result + y;
+ result = 31 * result + z;
+ return result;
}
```
-## toString()
+## toString()
-默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
+默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
```java
-public class ToStringExample {
- private int number;
+public class ToStringExample {
+ private int number;
- public ToStringExample(int number) {
- this.number = number;
- }
+ public ToStringExample(int number) {
+ this.number = number;
+ }
}
```
```java
-ToStringExample example = new ToStringExample(123);
+ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
```
@@ -306,393 +271,393 @@ System.out.println(example.toString());
ToStringExample@4554617c
```
-## clone()
+## clone()
-**1. cloneable**
+**1. cloneable**
-clone() 是 Object 的受保护方法,这意味着,如果一个类不显式去覆盖 clone() 就没有这个方法。
+clone() 是 Object 的受保护方法,这意味着,如果一个类不显式去覆盖 clone() 就没有这个方法。
```java
-public class CloneExample {
- private int a;
- private int b;
+public class CloneExample {
+ private int a;
+ private int b;
}
```
```java
-CloneExample e1 = new CloneExample();
-// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
+CloneExample e1 = new CloneExample();
+// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
```
-接下来覆盖 Object 的 clone() 得到以下实现:
+接下来覆盖 Object 的 clone() 得到以下实现:
```java
-public class CloneExample {
- private int a;
- private int b;
+public class CloneExample {
+ private int a;
+ private int b;
- @Override
- protected CloneExample clone() throws CloneNotSupportedException {
- return (CloneExample)super.clone();
- }
+ @Override
+ protected CloneExample clone() throws CloneNotSupportedException {
+ return (CloneExample)super.clone();
+ }
}
```
```java
-CloneExample e1 = new CloneExample();
-try {
- CloneExample e2 = e1.clone();
-} catch (CloneNotSupportedException e) {
- e.printStackTrace();
+CloneExample e1 = new CloneExample();
+try {
+ CloneExample e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+ e.printStackTrace();
}
```
```html
-java.lang.CloneNotSupportedException: CloneTest
+java.lang.CloneNotSupportedException: CloneTest
```
-以上抛出了 CloneNotSupportedException,这是因为 CloneTest 没有实现 Cloneable 接口。
+以上抛出了 CloneNotSupportedException,这是因为 CloneTest 没有实现 Cloneable 接口。
```java
-public class CloneExample implements Cloneable {
- private int a;
- private int b;
+public class CloneExample implements Cloneable {
+ private int a;
+ private int b;
- @Override
- protected Object clone() throws CloneNotSupportedException {
- return super.clone();
- }
+ @Override
+ protected Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
}
```
-应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
+应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
-**2. 深拷贝与浅拷贝**
+**2. 深拷贝与浅拷贝**
-- 浅拷贝:拷贝实例和原始实例的引用类型引用同一个对象;
-- 深拷贝:拷贝实例和原始实例的引用类型引用不同对象。
+- 浅拷贝:拷贝实例和原始实例的引用类型引用同一个对象;
+- 深拷贝:拷贝实例和原始实例的引用类型引用不同对象。
```java
-public class ShallowCloneExample implements Cloneable {
- private int[] arr;
+public class ShallowCloneExample implements Cloneable {
+ private int[] arr;
- public ShallowCloneExample() {
- arr = new int[10];
- for (int i = 0; i < arr.length; i++) {
- arr[i] = i;
- }
- }
+ public ShallowCloneExample() {
+ arr = new int[10];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = i;
+ }
+ }
- public void set(int index, int value) {
- arr[index] = value;
- }
+ public void set(int index, int value) {
+ arr[index] = value;
+ }
- public int get(int index) {
- return arr[index];
- }
+ public int get(int index) {
+ return arr[index];
+ }
- @Override
- protected ShallowCloneExample clone() throws CloneNotSupportedException {
- return (ShallowCloneExample) super.clone();
- }
+ @Override
+ protected ShallowCloneExample clone() throws CloneNotSupportedException {
+ return (ShallowCloneExample) super.clone();
+ }
}
```
```java
-ShallowCloneExample e1 = new ShallowCloneExample();
-ShallowCloneExample e2 = null;
-try {
- e2 = e1.clone();
-} catch (CloneNotSupportedException e) {
- e.printStackTrace();
+ShallowCloneExample e1 = new ShallowCloneExample();
+ShallowCloneExample e2 = null;
+try {
+ e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+ e.printStackTrace();
}
-e1.set(2, 222);
-System.out.println(e2.get(2)); // 222
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 222
```
```java
-public class DeepCloneExample implements Cloneable {
- private int[] arr;
+public class DeepCloneExample implements Cloneable {
+ private int[] arr;
- public DeepCloneExample() {
- arr = new int[10];
- for (int i = 0; i < arr.length; i++) {
- arr[i] = i;
- }
- }
+ public DeepCloneExample() {
+ arr = new int[10];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = i;
+ }
+ }
- public void set(int index, int value) {
- arr[index] = value;
- }
+ public void set(int index, int value) {
+ arr[index] = value;
+ }
- public int get(int index) {
- return arr[index];
- }
+ public int get(int index) {
+ return arr[index];
+ }
- @Override
- protected DeepCloneExample clone() throws CloneNotSupportedException {
- DeepCloneExample result = (DeepCloneExample) super.clone();
- result.arr = new int[arr.length];
- for (int i = 0; i < arr.length; i++) {
- result.arr[i] = arr[i];
- }
- return result;
- }
+ @Override
+ protected DeepCloneExample clone() throws CloneNotSupportedException {
+ DeepCloneExample result = (DeepCloneExample) super.clone();
+ result.arr = new int[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ result.arr[i] = arr[i];
+ }
+ return result;
+ }
}
```
```java
-DeepCloneExample e1 = new DeepCloneExample();
-DeepCloneExample e2 = null;
-try {
- e2 = e1.clone();
-} catch (CloneNotSupportedException e) {
- e.printStackTrace();
+DeepCloneExample e1 = new DeepCloneExample();
+DeepCloneExample e2 = null;
+try {
+ e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+ e.printStackTrace();
}
-e1.set(2, 222);
-System.out.println(e2.get(2)); // 2
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 2
```
-使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
+使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
```java
-public class CloneConstructorExample {
- private int[] arr;
+public class CloneConstructorExample {
+ private int[] arr;
- public CloneConstructorExample() {
- arr = new int[10];
- for (int i = 0; i < arr.length; i++) {
- arr[i] = i;
- }
- }
+ public CloneConstructorExample() {
+ arr = new int[10];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = i;
+ }
+ }
- public CloneConstructorExample(CloneConstructorExample original) {
- arr = new int[original.arr.length];
- for (int i = 0; i < original.arr.length; i++) {
- arr[i] = original.arr[i];
- }
- }
+ public CloneConstructorExample(CloneConstructorExample original) {
+ arr = new int[original.arr.length];
+ for (int i = 0; i < original.arr.length; i++) {
+ arr[i] = original.arr[i];
+ }
+ }
- public void set(int index, int value) {
- arr[index] = value;
- }
+ public void set(int index, int value) {
+ arr[index] = value;
+ }
- public int get(int index) {
- return arr[index];
- }
+ public int get(int index) {
+ return arr[index];
+ }
}
```
```java
-CloneConstructorExample e1 = new CloneConstructorExample();
-CloneConstructorExample e2 = new CloneConstructorExample(e1);
-e1.set(2, 222);
-System.out.println(e2.get(2)); // 2
+CloneConstructorExample e1 = new CloneConstructorExample();
+CloneConstructorExample e2 = new CloneConstructorExample(e1);
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 2
```
-# 四、继承
+# 四、继承
-## 访问权限
+## 访问权限
-Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。
+Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。
可以对类或类中的成员(字段以及方法)加上访问修饰符。
-- 成员可见表示其它类可以用这个类的实例访问到该成员;
-- 类可见表示其它类可以用这个类创建对象。
+- 成员可见表示其它类可以用这个类的实例访问到该成员;
+- 类可见表示其它类可以用这个类创建对象。
-protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。
+protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。
-设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。
+设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。
如果子类的方法覆盖了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。
-字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。可以使用公有的 getter 和 setter 方法来替换公有字段。
+字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。可以使用公有的 getter 和 setter 方法来替换公有字段。
```java
-public class AccessExample {
- public int x;
+public class AccessExample {
+ public int x;
}
```
```java
-public class AccessExample {
- private int x;
+public class AccessExample {
+ private int x;
- public int getX() {
- return x;
- }
+ public int getX() {
+ return x;
+ }
- public void setX(int x) {
- this.x = x;
- }
+ public void setX(int x) {
+ this.x = x;
+ }
}
```
但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。
```java
-public class AccessWithInnerClassExample {
- private class InnerClass {
- int x;
- }
+public class AccessWithInnerClassExample {
+ private class InnerClass {
+ int x;
+ }
- private InnerClass innerClass;
+ private InnerClass innerClass;
- public AccessWithInnerClassExample() {
- innerClass = new InnerClass();
- }
+ public AccessWithInnerClassExample() {
+ innerClass = new InnerClass();
+ }
- public int getValue() {
- return innerClass.x; // 直接访问
- }
+ public int getValue() {
+ return innerClass.x; // 直接访问
+ }
}
```
-## 抽象类与接口
+## 抽象类与接口
-**1. 抽象类**
+**1. 抽象类**
-抽象类和抽象方法都使用 abstract 进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。
+抽象类和抽象方法都使用 abstract 进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。
抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。
```java
-public abstract class AbstractClassExample {
+public abstract class AbstractClassExample {
- protected int x;
- private int y;
+ protected int x;
+ private int y;
- public abstract void func1();
+ public abstract void func1();
- public void func2() {
- System.out.println("func2");
- }
+ public void func2() {
+ System.out.println("func2");
+ }
}
```
```java
-public class AbstractExtendClassExample extends AbstractClassExample{
- @Override
- public void func1() {
- System.out.println("func1");
- }
+public class AbstractExtendClassExample extends AbstractClassExample{
+ @Override
+ public void func1() {
+ System.out.println("func1");
+ }
}
```
```java
-// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated
-AbstractClassExample ac2 = new AbstractExtendClassExample();
+// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated
+AbstractClassExample ac2 = new AbstractExtendClassExample();
ac2.func1();
```
-**2. 接口**
+**2. 接口**
-接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。
+接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。
-从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。
+从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。
-接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。
+接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。
-接口的字段默认都是 static 和 final 的。
+接口的字段默认都是 static 和 final 的。
```java
-public interface InterfaceExample {
- void func1();
+public interface InterfaceExample {
+ void func1();
- default void func2(){
- System.out.println("func2");
- }
+ default void func2(){
+ System.out.println("func2");
+ }
- int x = 123;
- // int y; // Variable 'y' might not have been initialized
- public int z = 0; // Modifier 'public' is redundant for interface fields
- // private int k = 0; // Modifier 'private' not allowed here
- // protected int l = 0; // Modifier 'protected' not allowed here
- // private void fun3(); // Modifier 'private' not allowed here
+ int x = 123;
+ // int y; // Variable 'y' might not have been initialized
+ public int z = 0; // Modifier 'public' is redundant for interface fields
+ // private int k = 0; // Modifier 'private' not allowed here
+ // protected int l = 0; // Modifier 'protected' not allowed here
+ // private void fun3(); // Modifier 'private' not allowed here
}
```
```java
-public class InterfaceImplementExample implements InterfaceExample {
- @Override
- public void func1() {
- System.out.println("func1");
- }
+public class InterfaceImplementExample implements InterfaceExample {
+ @Override
+ public void func1() {
+ System.out.println("func1");
+ }
}
```
```java
-// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated
-InterfaceExample ie2 = new InterfaceImplementExample();
+// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated
+InterfaceExample ie2 = new InterfaceImplementExample();
ie2.func1();
System.out.println(InterfaceExample.x);
```
-**3. 比较**
+**3. 比较**
-- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
-- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
-- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
-- 接口的方法只能是 public 的,而抽象类的方法可以由多种访问权限。
+- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
+- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
+- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
+- 接口的方法只能是 public 的,而抽象类的方法可以由多种访问权限。
-**4. 使用选择**
+**4. 使用选择**
使用抽象类:
-- 需要在几个相关的类中共享代码。
-- 需要能控制继承来的方法和域的访问权限,而不是都为 public。
-- 需要继承非静态(non-static)和非常量(non-final)字段。
+- 需要在几个相关的类中共享代码。
+- 需要能控制继承来的方法和域的访问权限,而不是都为 public。
+- 需要继承非静态(non-static)和非常量(non-final)字段。
使用接口:
-- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
-- 需要使用多重继承。
+- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
+- 需要使用多重继承。
-在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
+在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
-> [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/) [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface)
+> [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/) [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface)
-## super
+## super
-- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而完成一些初始化的工作。
-- 访问父类的成员:如果子类覆盖了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。
+- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而完成一些初始化的工作。
+- 访问父类的成员:如果子类覆盖了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。
```java
-public class SuperExample {
- protected int x;
- protected int y;
+public class SuperExample {
+ protected int x;
+ protected int y;
- public SuperExample(int x, int y) {
- this.x = x;
- this.y = y;
- }
+ public SuperExample(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
- public void func() {
- System.out.println("SuperExample.func()");
- }
+ public void func() {
+ System.out.println("SuperExample.func()");
+ }
}
```
```java
-public class SuperExtendExample extends SuperExample {
- private int z;
+public class SuperExtendExample extends SuperExample {
+ private int z;
- public SuperExtendExample(int x, int y, int z) {
- super(x, y);
- this.z = z;
- }
+ public SuperExtendExample(int x, int y, int z) {
+ super(x, y);
+ this.z = z;
+ }
- @Override
- public void func() {
- super.func();
- System.out.println("SuperExtendExample.func()");
- }
+ @Override
+ public void func() {
+ super.func();
+ System.out.println("SuperExtendExample.func()");
+ }
}
```
```java
-SuperExample e = new SuperExtendExample(1, 2, 3);
+SuperExample e = new SuperExtendExample(1, 2, 3);
e.func();
```
@@ -701,330 +666,333 @@ SuperExample.func()
SuperExtendExample.func()
```
-> [Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html)
+> [Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html)
-## 覆盖与重载
+## 覆盖与重载
-- 覆盖(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法;
+- 覆盖(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法;
-- 重载(Overload)存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。
+- 重载(Overload)存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。
-# 五、String
+# 五、String
-## String, StringBuffer and StringBuilder
+## String, StringBuffer and StringBuilder
-**1. 是否可变**
+**1. 是否可变**
-- String 不可变
-- StringBuffer 和 StringBuilder 可变
+- String 不可变
+- StringBuffer 和 StringBuilder 可变
-**2. 是否线程安全**
+**2. 是否线程安全**
-- String 不可变,因此是线程安全的
-- StringBuilder 不是线程安全的
-- StringBuffer 是线程安全的,内部使用 synchronized 来同步
+- String 不可变,因此是线程安全的
+- StringBuilder 不是线程安全的
+- StringBuffer 是线程安全的,内部使用 synchronized 来同步
-> [String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder)
+> [String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder)
-## String 不可变的原因
+## String 不可变的原因
-**1. 可以缓存 hash 值**
+**1. 可以缓存 hash 值**
-因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
+因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
-**2. String Pool 的需要**
+**2. String Pool 的需要**
-如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
+如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
-
+
-**3. 安全性**
+**3. 安全性**
-String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
+String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
-**4. 线程安全**
+**4. 线程安全**
-String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
+String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
-> [Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/)
+> [Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/)
-## String.intern()
+## String.intern()
-使用 String.intern() 可以保证相同内容的字符串实例引用相同的内存对象。
+使用 String.intern() 可以保证相同内容的字符串实例引用相同的内存对象。
-下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用,这个方法首先把 s1 引用的对象放到 String Poll(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。
+下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用,这个方法首先把 s1 引用的对象放到 String Poll(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。
```java
-String s1 = new String("aaa");
-String s2 = new String("aaa");
-System.out.println(s1 == s2); // false
-String s3 = s1.intern();
-System.out.println(s1.intern() == s3); // true
+String s1 = new String("aaa");
+String s2 = new String("aaa");
+System.out.println(s1 == s2); // false
+String s3 = s1.intern();
+System.out.println(s1.intern() == s3); // true
```
-如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Poll 中。
+如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Poll 中。
```java
-String s4 = "bbb";
-String s5 = "bbb";
-System.out.println(s4 == s5); // true
+String s4 = "bbb";
+String s5 = "bbb";
+System.out.println(s4 == s5); // true
```
-在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7,字符串常量池被放在堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
+在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7,字符串常量池被放在堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
-> [What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning) [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html)
+> [What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning) [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html)
-# 六、基本类型与运算
+# 六、基本类型与运算
-## 包装类型
+## 包装类型
八个基本类型:
-- boolean/1
-- byte/8
-- char/16
-- short/16
-- int/32
-- float/32
-- long/64
-- double/64
+- boolean/1
+- byte/8
+- char/16
+- short/16
+- int/32
+- float/32
+- long/64
+- double/64
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。
```java
-Integer x = 2; // 装箱
-int y = x; // 拆箱
+Integer x = 2; // 装箱
+int y = x; // 拆箱
```
-new Integer(123) 与 Integer.valueOf(123) 的区别在于,new Integer(123) 每次都会新建一个对象,而 Integer.valueOf(123) 可能会使用缓存对象,因此多次使用 Integer.valueOf(123) 会取得同一个对象的引用。
+new Integer(123) 与 Integer.valueOf(123) 的区别在于,new Integer(123) 每次都会新建一个对象,而 Integer.valueOf(123) 可能会使用缓存对象,因此多次使用 Integer.valueOf(123) 会取得同一个对象的引用。
```java
-Integer x = new Integer(123);
-Integer y = new Integer(123);
-System.out.println(x == y); // false
-Integer z = Integer.valueOf(123);
-Integer k = Integer.valueOf(123);
-System.out.println(z == k); // true
+Integer x = new Integer(123);
+Integer y = new Integer(123);
+System.out.println(x == y); // false
+Integer z = Integer.valueOf(123);
+Integer k = Integer.valueOf(123);
+System.out.println(z == k); // true
```
-编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
+编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
```java
-Integer m = 123;
-Integer n = 123;
-System.out.println(m == n); // true
+Integer m = 123;
+Integer n = 123;
+System.out.println(m == n); // true
```
-valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接使用缓存池的内容。
+valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接使用缓存池的内容。
```java
-public static Integer valueOf(int i) {
- if (i >= IntegerCache.low && i <= IntegerCache.high)
- return IntegerCache.cache[i + (-IntegerCache.low)];
- return new Integer(i);
+public static Integer valueOf(int i) {
+ if (i >= IntegerCache.low && i <= IntegerCache.high)
+ return IntegerCache.cache[i + (-IntegerCache.low)];
+ return new Integer(i);
}
```
-在 Java 8 中,Integer 缓存池的大小默认为 -128\~127。
+在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
```java
-static final int low = -128;
-static final int high;
-static final Integer cache[];
+static final int low = -128;
+static final int high;
+static final Integer cache[];
-static {
- // high value may be configured by property
- int h = 127;
- String integerCacheHighPropValue =
- sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
- if (integerCacheHighPropValue != null) {
- try {
- int i = parseInt(integerCacheHighPropValue);
- i = Math.max(i, 127);
- // Maximum array size is Integer.MAX_VALUE
- h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
- } catch( NumberFormatException nfe) {
- // If the property cannot be parsed into an int, ignore it.
- }
- }
- high = h;
+static {
+ // high value may be configured by property
+ int h = 127;
+ String integerCacheHighPropValue =
+ sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
+ if (integerCacheHighPropValue != null) {
+ try {
+ int i = parseInt(integerCacheHighPropValue);
+ i = Math.max(i, 127);
+ // Maximum array size is Integer.MAX_VALUE
+ h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
+ } catch( NumberFormatException nfe) {
+ // If the property cannot be parsed into an int, ignore it.
+ }
+ }
+ high = h;
- cache = new Integer[(high - low) + 1];
- int j = low;
- for(int k = 0; k < cache.length; k++)
- cache[k] = new Integer(j++);
+ cache = new Integer[(high - low) + 1];
+ int j = low;
+ for(int k = 0; k < cache.length; k++)
+ cache[k] = new Integer(j++);
- // range [-128, 127] must be interned (JLS7 5.1.7)
- assert IntegerCache.high >= 127;
+ // range [-128, 127] must be interned (JLS7 5.1.7)
+ assert IntegerCache.high >= 127;
}
```
-Java 还将一些其它基本类型的值放在缓冲池中,包含以下这些:
+Java 还将一些其它基本类型的值放在缓冲池中,包含以下这些:
-- boolean values true and false
-- all byte values
-- short values between -128 and 127
-- int values between -128 and 127
-- char in the range \u0000 to \u007F
+- boolean values true and false
+- all byte values
+- short values between -128 and 127
+- int values between -128 and 127
+- char in the range \u0000 to \u007F
因此在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
-> [Differences between new Integer(123), Integer.valueOf(123) and just 123
+> [Differences between new Integer(123), Integer.valueOf(123) and just 123
](https://stackoverflow.com/questions/9030817/differences-between-new-integer123-integer-valueof123-and-just-123)
-## switch
+## switch
-从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。
+从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。
```java
-String s = "a";
-switch (s) {
- case "a":
- System.out.println("aaa");
- break;
- case "b":
- System.out.println("bbb");
- break;
+String s = "a";
+switch (s) {
+ case "a":
+ System.out.println("aaa");
+ break;
+ case "b":
+ System.out.println("bbb");
+ break;
}
```
-switch 不支持 long,是因为 swicth 的设计初衷是为那些只需要对少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
+switch 不支持 long,是因为 swicth 的设计初衷是为那些只需要对少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
```java
-// long x = 111;
-// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum'
-// case 111:
-// System.out.println(111);
-// break;
-// case 222:
-// System.out.println(222);
-// break;
-// }
+// long x = 111;
+// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum'
+// case 111:
+// System.out.println(111);
+// break;
+// case 222:
+// System.out.println(222);
+// break;
+// }
```
-> [Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java)
+> [Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java)
-# 七、反射
+# 七、反射
-每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
+每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
-类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载,该方法会返回一个 Class 对象。
+类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载,该方法会返回一个 Class 对象。
-反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
+反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
-Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:
+Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:
-1. **Field** :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
-2. **Method** :可以使用 invoke() 方法调用与 Method 对象关联的方法;
-3. **Constructor** :可以用 Constructor 创建新的对象。
+1. **Field**:可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
+2. **Method**:可以使用 invoke() 方法调用与 Method 对象关联的方法;
+3. **Constructor**:可以用 Constructor 创建新的对象。
-IDE 使用反射机制获取类的信息,在使用一个类的对象时,能够把类的字段、方法和构造函数等信息列出来供用户选择。
+IDE 使用反射机制获取类的信息,在使用一个类的对象时,能够把类的字段、方法和构造函数等信息列出来供用户选择。
-**Advantages of Using Reflection:**
+**Advantages of Using Reflection:**
-- **Extensibility Features** : An application may make use of external, user-defined classes by creating instances of extensibility objects using their fully-qualified names.
-- **Class Browsers and Visual Development Environments** : A class browser needs to be able to enumerate the members of classes. Visual development environments can benefit from making use of type information available in reflection to aid the developer in writing correct code.
-- **Debuggers and Test Tools** : Debuggers need to be able to examine private members on classes. Test harnesses can make use of reflection to systematically call a discoverable set APIs defined on a class, to insure a high level of code coverage in a test suite.
+- **Extensibility Features** : An application may make use of external, user-defined classes by creating instances of extensibility objects using their fully-qualified names.
+- **Class Browsers and Visual Development Environments** : A class browser needs to be able to enumerate the members of classes. Visual development environments can benefit from making use of type information available in reflection to aid the developer in writing correct code.
+- **Debuggers and Test Tools** : Debuggers need to be able to examine private members on classes. Test harnesses can make use of reflection to systematically call a discoverable set APIs defined on a class, to insure a high level of code coverage in a test suite.
-**Drawbacks of Reflection:**
+**Drawbacks of Reflection:**
-Reflection is powerful, but should not be used indiscriminately. If it is possible to perform an operation without using reflection, then it is preferable to avoid using it. The following concerns should be kept in mind when accessing code via reflection.
+Reflection is powerful, but should not be used indiscriminately. If it is possible to perform an operation without using reflection, then it is preferable to avoid using it. The following concerns should be kept in mind when accessing code via reflection.
-- **Performance Overhead** : Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.
-- **Security Restrictions** : Reflection requires a runtime permission which may not be present when running under a security manager. This is in an important consideration for code which has to run in a restricted security context, such as in an Applet.
-- **Exposure of Internals** :Since reflection allows code to perform operations that would be illegal in non-reflective code, such as accessing private fields and methods, the use of reflection can result in unexpected side-effects, which may render code dysfunctional and may destroy portability. Reflective code breaks abstractions and therefore may change behavior with upgrades of the platform.
+- **Performance Overhead** : Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.
+- **Security Restrictions** : Reflection requires a runtime permission which may not be present when running under a security manager. This is in an important consideration for code which has to run in a restricted security context, such as in an Applet.
+- **Exposure of Internals** :Since reflection allows code to perform operations that would be illegal in non-reflective code, such as accessing private fields and methods, the use of reflection can result in unexpected side-effects, which may render code dysfunctional and may destroy portability. Reflective code breaks abstractions and therefore may change behavior with upgrades of the platform.
-> [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html) [深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/)
+> [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html) [深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/)
-# 八、异常
+# 八、异常
-Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error** 和 **Exception**。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:
+Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:**Error** 和 **Exception**。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:
-1. **受检异常** :需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;
-2. **非受检异常** :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序奔溃并且无法恢复。
+1. **受检异常**:需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;
+2. **非受检异常**:是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序奔溃并且无法恢复。
-
+
-> [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception) [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html)
+> [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception) [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html)
-# 九、泛型
+# 九、泛型
```java
-public class Box {
- // T stands for "Type"
- private T t;
- public void set(T t) { this.t = t; }
- public T get() { return t; }
+public class Box {
+ // T stands for "Type"
+ private T t;
+ public void set(T t) { this.t = t; }
+ public T get() { return t; }
}
```
-> [Java 泛型详解](http://www.importnew.com/24029.html)[10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693)
+> [Java 泛型详解](http://www.importnew.com/24029.html)[10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693)
-# 十、注解
+# 十、注解
-Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。
+Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。
-> [注解 Annotation 实现原理与自定义注解例子](https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html)
+> [注解 Annotation 实现原理与自定义注解例子](https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html)
-# 十一、特性
+# 十一、特性
-## 面向对象三大特性
+## 面向对象三大特性
-> [封装、继承、多态](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E6%80%9D%E6%83%B3.md#%E5%B0%81%E8%A3%85%E7%BB%A7%E6%89%BF%E5%A4%9A%E6%80%81)
+> [封装、继承、多态](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E6%80%9D%E6%83%B3.md#%E5%B0%81%E8%A3%85%E7%BB%A7%E6%89%BF%E5%A4%9A%E6%80%81)
-## Java 各版本的新特性
+## Java 各版本的新特性
-**New highlights in Java SE 8**
+**New highlights in Java SE 8**
-1. Lambda Expressions
-2. Pipelines and Streams
-3. Date and Time API
-4. Default Methods
-5. Type Annotations
-6. Nashhorn JavaScript Engine
-7. Concurrent Accumulators
-8. Parallel operations
-9. PermGen Error Removed
+1. Lambda Expressions
+2. Pipelines and Streams
+3. Date and Time API
+4. Default Methods
+5. Type Annotations
+6. Nashhorn JavaScript Engine
+7. Concurrent Accumulators
+8. Parallel operations
+9. PermGen Error Removed
-**New highlights in Java SE 7**
+**New highlights in Java SE 7**
-1. Strings in Switch Statement
-2. Type Inference for Generic Instance Creation
-3. Multiple Exception Handling
-4. Support for Dynamic Languages
-5. Try with Resources
-6. Java nio Package
-7. Binary Literals, Underscore in literals
-8. Diamond Syntax
+1. Strings in Switch Statement
+2. Type Inference for Generic Instance Creation
+3. Multiple Exception Handling
+4. Support for Dynamic Languages
+5. Try with Resources
+6. Java nio Package
+7. Binary Literals, Underscore in literals
+8. Diamond Syntax
-> [Difference between Java 1.8 and Java 1.7?](http://www.selfgrowth.com/articles/difference-between-java-18-and-java-17) [Java 8 特性 ](http://www.importnew.com/19345.html)
+> [Difference between Java 1.8 and Java 1.7?](http://www.selfgrowth.com/articles/difference-between-java-18-and-java-17) [Java 8 特性 ](http://www.importnew.com/19345.html)
-## Java 与 C++ 的区别
+## Java 与 C++ 的区别
-Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
+Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
-| Java | C++ |
-| -- | -- |
-| Java does not support pointers, templates, unions, operator overloading, structures etc. The Java language promoters initially said "No pointers!", but when many programmers questioned how you can work without pointers, the promoters began saying "Restricted pointers." Java supports what it calls "references". References act a lot like pointers in C++ languages but you cannot perform arithmetic on pointers in Java. References have types, and they're type-safe. These references cannot be interpreted as raw address and unsafe conversion is not allowed. | C++ supports structures, unions, templates, operator overloading, pointers and pointer arithmetic.|
-| Java support automatic garbage collection. It does not support destructors as C++ does. | C++ support destructors, which is automatically invoked when the object is destroyed. |
-| Java does not support conditional compilation and inclusion. | Conditional inclusion (#ifdef #ifndef type) is one of the main features of C++. |
-| Java has built in support for threads. In Java, there is a `Thread` class that you inherit to create a new thread and override the `run()` method. | C++ has no built in support for threads. C++ relies on non-standard third-party libraries for thread support. |
-| Java does not support default arguments. There is no scope resolution operator (::) in Java. The method definitions must always occur within a class, so there is no need for scope resolution there either. | C++ supports default arguments. C++ has scope resolution operator (::) which is used to to define a method outside a class and to access a global variable within from the scope where a local variable also exists with the same name. |
-| There is no _goto_ statement in Java. The keywords `const` and `goto` are reserved, even though they are not used. | C++ has _goto_ statement. However, it is not considered good practice to use of _goto_ statement. |
-| Java doesn't provide multiple inheritance, at least not in the same sense that C++ does. | C++ does support multiple inheritance. The keyword `virtual` is used to resolve ambiguities during multiple inheritance if there is any. |
-| Exception handling in Java is different because there are no destructors. Also, in Java, try/catch must be defined if the function declares that it may throw an exception. | While in C++, you may not include the try/catch even if the function throws an exception. |
-| Java has method overloading, but no operator overloading. The `String` class does use the `+` and `+=` operators to concatenate strings and `String`expressions use automatic type conversion, but that's a special built-in case. | C++ supports both method overloading and operator overloading. |
-| Java has built-in support for documentation comments (`/** ... */`); therefore, Java source files can contain their own documentation, which is read by a separate tool usually `javadoc` and reformatted into HTML. This helps keeping documentation maintained in easy way. | C++ does not support documentation comments. |
-| Java is interpreted for the most part and hence platform independent. | C++ generates object code and the same code may not run on different platforms. |
+| Java | C++ |
+| -- | -- |
+| Java does not support pointers, templates, unions, operator overloading, structures etc. The Java language promoters initially said "No pointers!", but when many programmers questioned how you can work without pointers, the promoters began saying "Restricted pointers." Java supports what it calls "references". References act a lot like pointers in C++ languages but you cannot perform arithmetic on pointers in Java. References have types, and they're type-safe. These references cannot be interpreted as raw address and unsafe conversion is not allowed. | C++ supports structures, unions, templates, operator overloading, pointers and pointer arithmetic.|
+| Java support automatic garbage collection. It does not support destructors as C++ does. | C++ support destructors, which is automatically invoked when the object is destroyed. |
+| Java does not support conditional compilation and inclusion. | Conditional inclusion (#ifdef #ifndef type) is one of the main features of C++. |
+| Java has built in support for threads. In Java, there is a `Thread` class that you inherit to create a new thread and override the `run()` method. | C++ has no built in support for threads. C++ relies on non-standard third-party libraries for thread support. |
+| Java does not support default arguments. There is no scope resolution operator (::) in Java. The method definitions must always occur within a class, so there is no need for scope resolution there either. | C++ supports default arguments. C++ has scope resolution operator (::) which is used to to define a method outside a class and to access a global variable within from the scope where a local variable also exists with the same name. |
+| There is no _goto_ statement in Java. The keywords `const` and `goto` are reserved, even though they are not used. | C++ has _goto_ statement. However, it is not considered good practice to use of _goto_ statement. |
+| Java doesn't provide multiple inheritance, at least not in the same sense that C++ does. | C++ does support multiple inheritance. The keyword `virtual` is used to resolve ambiguities during multiple inheritance if there is any. |
+| Exception handling in Java is different because there are no destructors. Also, in Java, try/catch must be defined if the function declares that it may throw an exception. | While in C++, you may not include the try/catch even if the function throws an exception. |
+| Java has method overloading, but no operator overloading. The `String` class does use the `+` and `+=` operators to concatenate strings and `String`expressions use automatic type conversion, but that's a special built-in case. | C++ supports both method overloading and operator overloading. |
+| Java has built-in support for documentation comments (`/** ... */`); therefore, Java source files can contain their own documentation, which is read by a separate tool usually `javadoc` and reformatted into HTML. This helps keeping documentation maintained in easy way. | C++ does not support documentation comments. |
+| Java is interpreted for the most part and hence platform independent. | C++ generates object code and the same code may not run on different platforms. |
-> [What are the main differences between Java and C++?](http://cs-fundamentals.com/tech-interview/java/differences-between-java-and-cpp.php)
+> [What are the main differences between Java and C++?](http://cs-fundamentals.com/tech-interview/java/differences-between-java-and-cpp.php)
-## JRE or JDK
+## JRE or JDK
-- JRE is the JVM program, Java application need to run on JRE.
-- JDK is a superset of JRE, JRE + tools for developing java programs. e.g, it provides the compiler "javac"
+- JRE is the JVM program, Java application need to run on JRE.
+- JDK is a superset of JRE, JRE + tools for developing java programs. e.g, it provides the compiler "javac"
-# 参考资料
+# 参考资料
-- Eckel B. Java 编程思想[M]. 机械工业出版社, 2002.
-- Bloch J. Effective java[M]. Addison-Wesley Professional, 2017.
+- Eckel B. Java 编程思想[M]. 机械工业出版社, 2002.
+- Bloch J. Effective java[M]. Addison-Wesley Professional, 2017.
+---bottom---CyC---
+![](index_files/f76067a5-7d5f-4135-9549-8199c77d8f1c.jpg)
+![](index_files/PPjwP.png)
diff --git a/notes/Java 容器.md b/notes/Java 容器.md
index b34caa09..6045d378 100644
--- a/notes/Java 容器.md
+++ b/notes/Java 容器.md
@@ -1,851 +1,843 @@
-
-* [一、概览](#一概览)
- * [Collection](#collection)
- * [Map](#map)
-* [二、容器中的设计模式](#二容器中的设计模式)
- * [迭代器模式](#迭代器模式)
- * [适配器模式](#适配器模式)
-* [三、源码分析](#三源码分析)
- * [ArrayList](#arraylist)
- * [Vector](#vector)
- * [LinkedList](#linkedlist)
- * [HashMap](#hashmap)
- * [ConcurrentHashMap](#concurrenthashmap)
- * [LinkedHashMap](#linkedhashmap)
- * [TreeMap](#treemap)
-* [参考资料](#参考资料)
-
+# 一、概览
+容器主要包括 Collection 和 Map 两种,Collection 又包含了 List、Set 以及 Queue。
-# 一、概览
+## Collection
-容器主要包括 Collection 和 Map 两种,Collection 又包含了 List、Set 以及 Queue。
+![](index_files/java-collections.png)
-## Collection
+### 1. Set
-
+- HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的;
-### 1. Set
+- TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logN);
-- HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的;
+- LinkedHashSet:具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序。
-- TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logN);
+### 2. List
-- LinkedHashSet:具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序。
+- ArrayList:基于动态数组实现,支持随机访问;
-### 2. List
+- Vector:和 ArrayList 类似,但它是线程安全的;
-- ArrayList:基于动态数组实现,支持随机访问;
+- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
-- Vector:和 ArrayList 类似,但它是线程安全的;
+### 3. Queue
-- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
+- LinkedList:可以用它来支持双向队列;
-### 3. Queue
+- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
-- LinkedList:可以用它来支持双向队列;
+## Map
-- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
+![](index_files/java-collections1.png)
-## Map
+- HashMap:基于哈希实现;
-
+- HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
-- HashMap:基于哈希实现;
+- LinkedHashMap:使用链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
-- HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
+- TreeMap:基于红黑树实现。
-- LinkedHashMap:使用链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
+# 二、容器中的设计模式
-- TreeMap:基于红黑树实现。
+## 迭代器模式
-# 二、容器中的设计模式
+![](index_files/Iterator-1.jpg)
-## 迭代器模式
+Collection 实现了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。
-
-
-Collection 实现了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。
-
-从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。
+从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。
```java
-List list = new ArrayList<>();
+List list = new ArrayList<>();
list.add("a");
list.add("b");
-for (String item : list) {
- System.out.println(item);
+for (String item : list) {
+ System.out.println(item);
}
```
-## 适配器模式
+## 适配器模式
-java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
+java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
```java
@SafeVarargs
-public static List asList(T... a)
+public static List asList(T... a)
```
-如果要将数组类型转换为 List 类型,应该注意的是 asList() 的参数为泛型的变长参数,因此不能使用基本类型数组作为参数,只能使用相应的包装类型数组。
+如果要将数组类型转换为 List 类型,应该注意的是 asList() 的参数为泛型的变长参数,因此不能使用基本类型数组作为参数,只能使用相应的包装类型数组。
```java
-Integer[] arr = {1, 2, 3};
-List list = Arrays.asList(arr);
+Integer[] arr = {1, 2, 3};
+List list = Arrays.asList(arr);
```
-也可以使用以下方式生成 List。
+也可以使用以下方式生成 List。
```java
-List list = Arrays.asList(1,2,3);
+List list = Arrays.asList(1,2,3);
```
-# 三、源码分析
+# 三、源码分析
-建议先阅读 [算法-查找](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E7%AE%97%E6%B3%95.md#%E6%9F%A5%E6%89%BE) 部分,对容器类源码的理解有很大帮助。
+建议先阅读 [算法-查找](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E7%AE%97%E6%B3%95.md#%E6%9F%A5%E6%89%BE) 部分,对容器类源码的理解有很大帮助。
-至于 ConcurrentHashMap 的理解,需要有并发方面的知识,建议先阅读:[Java 并发](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md)
+至于 ConcurrentHashMap 的理解,需要有并发方面的知识,建议先阅读:[Java 并发](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md)
-以下源码从 JDK 1.8 提取而来,下载地址:[JDK-Source-Code](https://github.com/CyC2018/JDK-Source-Code)。
+以下源码从 JDK 1.8 提取而来,下载地址:[JDK-Source-Code](https://github.com/CyC2018/JDK-Source-Code)。
-## ArrayList
+## ArrayList
[ArrayList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/ArrayList.java)
-### 1. 概览
+### 1. 概览
-实现了 RandomAccess 接口,因此支持随机访问,这是理所当然的,因为 ArrayList 是基于数组实现的。
+实现了 RandomAccess 接口,因此支持随机访问,这是理所当然的,因为 ArrayList 是基于数组实现的。
```java
-public class ArrayList extends AbstractList
- implements List, RandomAccess, Cloneable, java.io.Serializable
+public class ArrayList extends AbstractList
+ implements List, RandomAccess, Cloneable, java.io.Serializable
```
-数组的默认大小为 10。
+数组的默认大小为 10。
```java
-private static final int DEFAULT_CAPACITY = 10;
+private static final int DEFAULT_CAPACITY = 10;
```
-### 2. 序列化
+### 2. 序列化
-基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
+基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
```java
-transient Object[] elementData; // non-private to simplify nested class access
+transient Object[] elementData; // non-private to simplify nested class access
```
-### 3. 扩容
+### 3. 扩容
-添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。
+添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。
-扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
+扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
```java
-public boolean add(E e) {
- ensureCapacityInternal(size + 1); // Increments modCount!!
- elementData[size++] = e;
- return true;
+public boolean add(E e) {
+ ensureCapacityInternal(size + 1); // Increments modCount!!
+ elementData[size++] = e;
+ return true;
}
-private void ensureCapacityInternal(int minCapacity) {
- if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
- minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
- }
+private void ensureCapacityInternal(int minCapacity) {
+ if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
+ minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
+ }
- ensureExplicitCapacity(minCapacity);
+ ensureExplicitCapacity(minCapacity);
}
-private void ensureExplicitCapacity(int minCapacity) {
- modCount++;
+private void ensureExplicitCapacity(int minCapacity) {
+ modCount++;
- // overflow-conscious code
- if (minCapacity - elementData.length > 0)
- grow(minCapacity);
+ // overflow-conscious code
+ if (minCapacity - elementData.length > 0)
+ grow(minCapacity);
}
-private void grow(int minCapacity) {
- // overflow-conscious code
- int oldCapacity = elementData.length;
- int newCapacity = oldCapacity + (oldCapacity >> 1);
- if (newCapacity - minCapacity < 0)
- newCapacity = minCapacity;
- if (newCapacity - MAX_ARRAY_SIZE > 0)
- newCapacity = hugeCapacity(minCapacity);
- // minCapacity is usually close to size, so this is a win:
- elementData = Arrays.copyOf(elementData, newCapacity);
+private void grow(int minCapacity) {
+ // overflow-conscious code
+ int oldCapacity = elementData.length;
+ int newCapacity = oldCapacity + (oldCapacity >> 1);
+ if (newCapacity - minCapacity < 0)
+ newCapacity = minCapacity;
+ if (newCapacity - MAX_ARRAY_SIZE > 0)
+ newCapacity = hugeCapacity(minCapacity);
+ // minCapacity is usually close to size, so this is a win:
+ elementData = Arrays.copyOf(elementData, newCapacity);
}
```
-### 4. 删除元素
+### 4. 删除元素
-需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,复制的代价很高。
+需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,复制的代价很高。
```java
-public E remove(int index) {
- rangeCheck(index);
+public E remove(int index) {
+ rangeCheck(index);
- modCount++;
- E oldValue = elementData(index);
+ modCount++;
+ E oldValue = elementData(index);
- int numMoved = size - index - 1;
- if (numMoved > 0)
- System.arraycopy(elementData, index+1, elementData, index, numMoved);
- elementData[--size] = null; // clear to let GC do its work
+ int numMoved = size - index - 1;
+ if (numMoved > 0)
+ System.arraycopy(elementData, index+1, elementData, index, numMoved);
+ elementData[--size] = null; // clear to let GC do its work
- return oldValue;
+ return oldValue;
}
```
-### 5. Fail-Fast
+### 5. Fail-Fast
-modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
+modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
-在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。
+在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。
```java
-private void writeObject(java.io.ObjectOutputStream s)
- throws java.io.IOException{
- // Write out element count, and any hidden stuff
- int expectedModCount = modCount;
- s.defaultWriteObject();
+private void writeObject(java.io.ObjectOutputStream s)
+ throws java.io.IOException{
+ // Write out element count, and any hidden stuff
+ int expectedModCount = modCount;
+ s.defaultWriteObject();
- // Write out size as capacity for behavioural compatibility with clone()
- s.writeInt(size);
+ // Write out size as capacity for behavioural compatibility with clone()
+ s.writeInt(size);
- // Write out all elements in the proper order.
- for (int i=0; i= elementCount)
- throw new ArrayIndexOutOfBoundsException(index);
+public synchronized E get(int index) {
+ if (index >= elementCount)
+ throw new ArrayIndexOutOfBoundsException(index);
- return elementData(index);
+ return elementData(index);
}
```
-### 2. ArrayList 与 Vector
+### 2. ArrayList 与 Vector
-- Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
-- Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
+- Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
+- Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
-### 3. Vector 替代方案
+### 3. Vector 替代方案
-为了获得线程安全的 ArrayList,可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类;
+为了获得线程安全的 ArrayList,可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类;
```java
-List list = new ArrayList<>();
-List synList = Collections.synchronizedList(list);
+List list = new ArrayList<>();
+List synList = Collections.synchronizedList(list);
```
```java
-List list = new CopyOnWriteArrayList();
+List list = new CopyOnWriteArrayList();
```
-## LinkedList
+## LinkedList
[LinkedList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/LinkedList.java)
-### 1. 概览
+### 1. 概览
-基于双向链表实现,内部使用 Node 来存储链表节点信息。
+基于双向链表实现,内部使用 Node 来存储链表节点信息。
```java
-private static class Node {
- E item;
- Node next;
- Node prev;
+private static class Node {
+ E item;
+ Node next;
+ Node prev;
}
```
-每个链表存储了 Head 和 Tail 指针:
+每个链表存储了 Head 和 Tail 指针:
```java
-transient Node first;
-transient Node last;
+transient Node first;
+transient Node last;
```
-
+![](index_files/HowLinkedListWorks.png)
-### 2. ArrayList 与 LinkedList
+### 2. ArrayList 与 LinkedList
-- ArrayList 基于动态数组实现,LinkedList 基于双向链表实现;
-- ArrayList 支持随机访问,LinkedList 不支持;
-- LinkedList 在任意位置添加删除元素更快。
+- ArrayList 基于动态数组实现,LinkedList 基于双向链表实现;
+- ArrayList 支持随机访问,LinkedList 不支持;
+- LinkedList 在任意位置添加删除元素更快。
-## HashMap
+## HashMap
[HashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java)
-为了便于理解,以下内容以 JDK 1.7 为主。
+为了便于理解,以下内容以 JDK 1.7 为主。
-### 1. 存储结构
+### 1. 存储结构
-使用拉链法来解决冲突,内部包含了一个 Entry 类型的数组 table,数组中的每个位置被当成一个桶。
+使用拉链法来解决冲突,内部包含了一个 Entry 类型的数组 table,数组中的每个位置被当成一个桶。
```java
-transient Entry[] table;
+transient Entry[] table;
```
-其中,Entry 就是存储数据的键值对,它包含了四个字段。从 next 字段我们可以看出 Entry 是一个链表,即每个桶会存放一个链表。
+其中,Entry 就是存储数据的键值对,它包含了四个字段。从 next 字段我们可以看出 Entry 是一个链表,即每个桶会存放一个链表。
-
+
```java
-static class Entry implements Map.Entry {
- final K key;
- V value;
- Entry next;
- int hash;
+static class Entry implements Map.Entry {
+ final K key;
+ V value;
+ Entry next;
+ int hash;
- Entry(int h, K k, V v, Entry n) {
- value = v;
- next = n;
- key = k;
- hash = h;
- }
+ Entry(int h, K k, V v, Entry n) {
+ value = v;
+ next = n;
+ key = k;
+ hash = h;
+ }
- public final K getKey() {
- return key;
- }
+ public final K getKey() {
+ return key;
+ }
- public final V getValue() {
- return value;
- }
+ public final V getValue() {
+ return value;
+ }
- public final V setValue(V newValue) {
- V oldValue = value;
- value = newValue;
- return oldValue;
- }
+ public final V setValue(V newValue) {
+ V oldValue = value;
+ value = newValue;
+ return oldValue;
+ }
- public final boolean equals(Object o) {
- if (!(o instanceof Map.Entry))
- return false;
- Map.Entry e = (Map.Entry)o;
- Object k1 = getKey();
- Object k2 = e.getKey();
- if (k1 == k2 || (k1 != null && k1.equals(k2))) {
- Object v1 = getValue();
- Object v2 = e.getValue();
- if (v1 == v2 || (v1 != null && v1.equals(v2)))
- return true;
- }
- return false;
- }
+ public final boolean equals(Object o) {
+ if (!(o instanceof Map.Entry))
+ return false;
+ Map.Entry e = (Map.Entry)o;
+ Object k1 = getKey();
+ Object k2 = e.getKey();
+ if (k1 == k2 || (k1 != null && k1.equals(k2))) {
+ Object v1 = getValue();
+ Object v2 = e.getValue();
+ if (v1 == v2 || (v1 != null && v1.equals(v2)))
+ return true;
+ }
+ return false;
+ }
- public final int hashCode() {
- return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
- }
+ public final int hashCode() {
+ return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
+ }
- public final String toString() {
- return getKey() + "=" + getValue();
- }
+ public final String toString() {
+ return getKey() + "=" + getValue();
+ }
- /**
- * This method is invoked whenever the value in an entry is
- * overwritten by an invocation of put(k,v) for a key k that's already
- * in the HashMap.
- */
- void recordAccess(HashMap m) {
- }
+ /**
+ * This method is invoked whenever the value in an entry is
+ * overwritten by an invocation of put(k,v) for a key k that's already
+ * in the HashMap.
+ */
+ void recordAccess(HashMap m) {
+ }
- /**
- * This method is invoked whenever the entry is
- * removed from the table.
- */
- void recordRemoval(HashMap m) {
- }
+ /**
+ * This method is invoked whenever the entry is
+ * removed from the table.
+ */
+ void recordRemoval(HashMap m) {
+ }
}
```
-### 2. 拉链法的工作原理
+### 2. 拉链法的工作原理
```java
-HashMap map = new HashMap<>();
-map.put("K1", "V1");
-map.put("K2", "V2");
-map.put("K3", "V3");
+HashMap map = new HashMap<>();
+map.put("K1", "V1");
+map.put("K2", "V2");
+map.put("K3", "V3");
```
-- 新建一个 HashMap,默认大小为 16;
-- 插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。
-- 插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
-- 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 前面。
+- 新建一个 HashMap,默认大小为 16;
+- 插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。
+- 插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
+- 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 前面。
-应该注意到链表的插入是以头插法方式进行的,例如上面的 <K3,V3> 不是插在 <K2,V2> 后面,而是插入在链表头部。
+应该注意到链表的插入是以头插法方式进行的,例如上面的 <K3,V3> 不是插在 <K2,V2> 后面,而是插入在链表头部。
查找需要分成两步进行:
-- 计算键值对所在的桶;
-- 在链表上顺序查找,时间复杂度显然和链表的长度成正比。
+- 计算键值对所在的桶;
+- 在链表上顺序查找,时间复杂度显然和链表的长度成正比。
-
+
-### 3. put 操作
+### 3. put 操作
```java
-public V put(K key, V value) {
- if (table == EMPTY_TABLE) {
- inflateTable(threshold);
- }
- // 键为 null 单独处理
- if (key == null)
- return putForNullKey(value);
- int hash = hash(key);
- // 确定桶下标
- int i = indexFor(hash, table.length);
- // 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
- for (Entry e = table[i]; e != null; e = e.next) {
- Object k;
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
+public V put(K key, V value) {
+ if (table == EMPTY_TABLE) {
+ inflateTable(threshold);
+ }
+ // 键为 null 单独处理
+ if (key == null)
+ return putForNullKey(value);
+ int hash = hash(key);
+ // 确定桶下标
+ int i = indexFor(hash, table.length);
+ // 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
+ for (Entry e = table[i]; e != null; e = e.next) {
+ Object k;
+ if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
+ V oldValue = e.value;
+ e.value = value;
+ e.recordAccess(this);
+ return oldValue;
+ }
+ }
- modCount++;
- // 插入新键值对
- addEntry(hash, key, value, i);
- return null;
+ modCount++;
+ // 插入新键值对
+ addEntry(hash, key, value, i);
+ return null;
}
```
-HashMap 允许插入键位 null 的键值对,因为无法调用 null 的 hashCode(),也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。
+HashMap 允许插入键位 null 的键值对,因为无法调用 null 的 hashCode(),也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。
```java
-private V putForNullKey(V value) {
- for (Entry e = table[0]; e != null; e = e.next) {
- if (e.key == null) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- modCount++;
- addEntry(0, null, value, 0);
- return null;
+private V putForNullKey(V value) {
+ for (Entry e = table[0]; e != null; e = e.next) {
+ if (e.key == null) {
+ V oldValue = e.value;
+ e.value = value;
+ e.recordAccess(this);
+ return oldValue;
+ }
+ }
+ modCount++;
+ addEntry(0, null, value, 0);
+ return null;
}
```
使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。
```java
-void addEntry(int hash, K key, V value, int bucketIndex) {
- if ((size >= threshold) && (null != table[bucketIndex])) {
- resize(2 * table.length);
- hash = (null != key) ? hash(key) : 0;
- bucketIndex = indexFor(hash, table.length);
- }
+void addEntry(int hash, K key, V value, int bucketIndex) {
+ if ((size >= threshold) && (null != table[bucketIndex])) {
+ resize(2 * table.length);
+ hash = (null != key) ? hash(key) : 0;
+ bucketIndex = indexFor(hash, table.length);
+ }
- createEntry(hash, key, value, bucketIndex);
+ createEntry(hash, key, value, bucketIndex);
}
-void createEntry(int hash, K key, V value, int bucketIndex) {
- Entry e = table[bucketIndex];
- // 头插法,链表头部指向新的键值对
- table[bucketIndex] = new Entry<>(hash, key, value, e);
- size++;
+void createEntry(int hash, K key, V value, int bucketIndex) {
+ Entry e = table[bucketIndex];
+ // 头插法,链表头部指向新的键值对
+ table[bucketIndex] = new Entry<>(hash, key, value, e);
+ size++;
}
```
```java
-Entry(int h, K k, V v, Entry n) {
- value = v;
- next = n;
- key = k;
- hash = h;
+Entry(int h, K k, V v, Entry n) {
+ value = v;
+ next = n;
+ key = k;
+ hash = h;
}
```
-### 4. 确定桶下标
+### 4. 确定桶下标
很多操作都需要先确定一个键值对所在的桶下标。
```java
-int hash = hash(key);
-int i = indexFor(hash, table.length);
+int hash = hash(key);
+int i = indexFor(hash, table.length);
```
-(一)计算 hash 值
+(一)计算 hash 值
```java
-final int hash(Object k) {
- int h = hashSeed;
- if (0 != h && k instanceof String) {
- return sun.misc.Hashing.stringHash32((String) k);
- }
+final int hash(Object k) {
+ int h = hashSeed;
+ if (0 != h && k instanceof String) {
+ return sun.misc.Hashing.stringHash32((String) k);
+ }
- h ^= k.hashCode();
+ h ^= k.hashCode();
- // This function ensures that hashCodes that differ only by
- // constant multiples at each bit position have a bounded
- // number of collisions (approximately 8 at default load factor).
- h ^= (h >>> 20) ^ (h >>> 12);
- return h ^ (h >>> 7) ^ (h >>> 4);
+ // This function ensures that hashCodes that differ only by
+ // constant multiples at each bit position have a bounded
+ // number of collisions (approximately 8 at default load factor).
+ h ^= (h >>> 20) ^ (h >>> 12);
+ return h ^ (h >>> 7) ^ (h >>> 4);
}
```
```java
-public final int hashCode() {
- return Objects.hashCode(key) ^ Objects.hashCode(value);
+public final int hashCode() {
+ return Objects.hashCode(key) ^ Objects.hashCode(value);
}
```
(二)取模
-令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:
+令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:
```
-x : 00010000
-x-1 : 00001111
+x : 00010000
+x-1 : 00001111
```
-令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:
+令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:
```
-y : 10110010
-x-1 : 00001111
-y&(x-1) : 00000010
+y : 10110010
+x-1 : 00001111
+y&(x-1) : 00000010
```
-这个性质和 y 对 x 取模效果是一样的:
+这个性质和 y 对 x 取模效果是一样的:
```
-x : 00010000
-y : 10110010
-y%x : 00000010
+x : 00010000
+y : 10110010
+y%x : 00000010
```
我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时能用位运算的话能带来更高的性能。
-确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换为位运算。
+确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换为位运算。
```java
-static int indexFor(int h, int length) {
- return h & (length-1);
+static int indexFor(int h, int length) {
+ return h & (length-1);
}
```
-### 5. 扩容-基本原理
+### 5. 扩容-基本原理
-设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。
+设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。
-为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。
+为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。
-和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。
+和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。
-| 参数 | 含义 |
-| :--: | :-- |
-| capacity | table 的容量大小,默认为 16,需要注意的是 capacity 必须保证为 2 的次方。|
-| size | table 的实际使用量。 |
-| threshold | size 的临界值,size 必须小于 threshold,如果大于等于,就必须进行扩容操作。 |
-| load_factor | table 能够使用的比例,threshold = capacity * load_factor。|
+| 参数 | 含义 |
+| :--: | :-- |
+| capacity | table 的容量大小,默认为 16,需要注意的是 capacity 必须保证为 2 的次方。|
+| size | table 的实际使用量。 |
+| threshold | size 的临界值,size 必须小于 threshold,如果大于等于,就必须进行扩容操作。 |
+| load_factor | table 能够使用的比例,threshold = capacity * load_factor。|
```java
-static final int DEFAULT_INITIAL_CAPACITY = 16;
+static final int DEFAULT_INITIAL_CAPACITY = 16;
-static final int MAXIMUM_CAPACITY = 1 << 30;
+static final int MAXIMUM_CAPACITY = 1 << 30;
-static final float DEFAULT_LOAD_FACTOR = 0.75f;
+static final float DEFAULT_LOAD_FACTOR = 0.75f;
-transient Entry[] table;
+transient Entry[] table;
-transient int size;
+transient int size;
-int threshold;
+int threshold;
-final float loadFactor;
+final float loadFactor;
-transient int modCount;
+transient int modCount;
```
-从下面的添加元素代码中可以看出,当需要扩容时,令 capacity 为原来的两倍。
+从下面的添加元素代码中可以看出,当需要扩容时,令 capacity 为原来的两倍。
```java
-void addEntry(int hash, K key, V value, int bucketIndex) {
- Entry e = table[bucketIndex];
- table[bucketIndex] = new Entry<>(hash, key, value, e);
- if (size++ >= threshold)
- resize(2 * table.length);
+void addEntry(int hash, K key, V value, int bucketIndex) {
+ Entry e = table[bucketIndex];
+ table[bucketIndex] = new Entry<>(hash, key, value, e);
+ if (size++ >= threshold)
+ resize(2 * table.length);
}
```
-扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把旧 table 的所有键值对重新插入新的 table 中,因此这一步是很费时的。
+扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把旧 table 的所有键值对重新插入新的 table 中,因此这一步是很费时的。
```java
-void resize(int newCapacity) {
- Entry[] oldTable = table;
- int oldCapacity = oldTable.length;
- if (oldCapacity == MAXIMUM_CAPACITY) {
- threshold = Integer.MAX_VALUE;
- return;
- }
+void resize(int newCapacity) {
+ Entry[] oldTable = table;
+ int oldCapacity = oldTable.length;
+ if (oldCapacity == MAXIMUM_CAPACITY) {
+ threshold = Integer.MAX_VALUE;
+ return;
+ }
- Entry[] newTable = new Entry[newCapacity];
- transfer(newTable);
- table = newTable;
- threshold = (int)(newCapacity * loadFactor);
+ Entry[] newTable = new Entry[newCapacity];
+ transfer(newTable);
+ table = newTable;
+ threshold = (int)(newCapacity * loadFactor);
}
-void transfer(Entry[] newTable) {
- Entry[] src = table;
- int newCapacity = newTable.length;
- for (int j = 0; j < src.length; j++) {
- Entry e = src[j];
- if (e != null) {
- src[j] = null;
- do {
- Entry next = e.next;
- int i = indexFor(e.hash, newCapacity);
- e.next = newTable[i];
- newTable[i] = e;
- e = next;
- } while (e != null);
- }
- }
+void transfer(Entry[] newTable) {
+ Entry[] src = table;
+ int newCapacity = newTable.length;
+ for (int j = 0; j < src.length; j++) {
+ Entry e = src[j];
+ if (e != null) {
+ src[j] = null;
+ do {
+ Entry next = e.next;
+ int i = indexFor(e.hash, newCapacity);
+ e.next = newTable[i];
+ newTable[i] = e;
+ e = next;
+ } while (e != null);
+ }
+ }
}
```
-### 6. 扩容-重新计算桶下标
+### 6. 扩容-重新计算桶下标
-在进行扩容时,需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。
+在进行扩容时,需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。
-假设原数组长度 capacity 为 8,扩容之后 new capacity 为 16:
+假设原数组长度 capacity 为 8,扩容之后 new capacity 为 16:
```html
-capacity : 00010000
-new capacity : 00100000
+capacity : 00010000
+new capacity : 00100000
```
-对于一个 Key,它的 hash 如果在第 6 位上为 0,那么取模得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 + 8。
+对于一个 Key,它的 hash 如果在第 6 位上为 0,那么取模得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 + 8。
-### 7. 扩容-计算数组容量
+### 7. 扩容-计算数组容量
-HashMap 构造函数允许用户传入的容量不是 2 的幂次方,因为它可以自动地将传入的容量转换为 2 的幂次方。
+HashMap 构造函数允许用户传入的容量不是 2 的幂次方,因为它可以自动地将传入的容量转换为 2 的幂次方。
-先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到:
+先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到:
```
-mask |= mask >> 1 11011000
-mask |= mask >> 2 11111100
-mask |= mask >> 4 11111111
+mask |= mask >> 1 11011000
+mask |= mask >> 2 11111100
+mask |= mask >> 4 11111111
```
-mask+1 是大于原始数字的最小的 2 幂次方。
+mask+1 是大于原始数字的最小的 2 幂次方。
```
-num 10010000
-mask+1 100000000
+num 10010000
+mask+1 100000000
```
-以下是 HashMap 中计算数组容量的代码:
+以下是 HashMap 中计算数组容量的代码:
```java
-static final int tableSizeFor(int cap) {
- int n = cap - 1;
- n |= n >>> 1;
- n |= n >>> 2;
- n |= n >>> 4;
- n |= n >>> 8;
- n |= n >>> 16;
- return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+static final int tableSizeFor(int cap) {
+ int n = cap - 1;
+ n |= n >>> 1;
+ n |= n >>> 2;
+ n |= n >>> 4;
+ n |= n >>> 8;
+ n |= n >>> 16;
+ return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
```
-### 8. 链表转红黑树
+### 8. 链表转红黑树
-应该注意到,从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。
+应该注意到,从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。
-### 9. HashMap 与 HashTable
+### 9. HashMap 与 HashTable
-- HashTable 是同步的,它使用了 synchronized 来进行同步。它也是线程安全的,多个线程可以共享同一个 HashTable。HashMap 不是同步的,但是可以使用 ConcurrentHashMap,它是 HashTable 的替代,而且比 HashTable 可扩展性更好。
-- HashMap 可以插入键为 null 的 Entry。
-- HashMap 的迭代器是 fail-fast 迭代器,而 Hashtable 的 enumerator 迭代器不是 fail-fast 的。
-- 由于 Hashtable 是线程安全的也是 synchronized,所以在单线程环境下它比 HashMap 要慢。
-- HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
+- HashTable 是同步的,它使用了 synchronized 来进行同步。它也是线程安全的,多个线程可以共享同一个 HashTable。HashMap 不是同步的,但是可以使用 ConcurrentHashMap,它是 HashTable 的替代,而且比 HashTable 可扩展性更好。
+- HashMap 可以插入键为 null 的 Entry。
+- HashMap 的迭代器是 fail-fast 迭代器,而 Hashtable 的 enumerator 迭代器不是 fail-fast 的。
+- 由于 Hashtable 是线程安全的也是 synchronized,所以在单线程环境下它比 HashMap 要慢。
+- HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
-## ConcurrentHashMap
+## ConcurrentHashMap
[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/1.7/ConcurrentHashMap.java)
-### 1. 存储结构
+### 1. 存储结构
```java
-static final class HashEntry {
- final int hash;
- final K key;
- volatile V value;
- volatile HashEntry next;
+static final class HashEntry {
+ final int hash;
+ final K key;
+ volatile V value;
+ volatile HashEntry next;
}
```
-Segment 继承自 ReentrantLock,每个 Segment 维护着多个 HashEntry。
+Segment 继承自 ReentrantLock,每个 Segment 维护着多个 HashEntry。
-ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。
+ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。
```java
-static final class Segment extends ReentrantLock implements Serializable {
+static final class Segment extends ReentrantLock implements Serializable {
- private static final long serialVersionUID = 2249069246763182397L;
+ private static final long serialVersionUID = 2249069246763182397L;
- static final int MAX_SCAN_RETRIES =
- Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
+ static final int MAX_SCAN_RETRIES =
+ Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
- transient volatile HashEntry[] table;
+ transient volatile HashEntry[] table;
- transient int count;
+ transient int count;
- transient int modCount;
+ transient int modCount;
- transient int threshold;
+ transient int threshold;
- final float loadFactor;
+ final float loadFactor;
}
```
```java
-final Segment[] segments;
+final Segment[] segments;
```
-默认的并发级别为 16,也就是说默认创建 16 个 Segment。
+默认的并发级别为 16,也就是说默认创建 16 个 Segment。
```java
-static final int DEFAULT_CONCURRENCY_LEVEL = 16;
+static final int DEFAULT_CONCURRENCY_LEVEL = 16;
```
-
+![](index_files/image005.jpg)
-### 2. size 操作
+### 2. size 操作
-每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
+每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
```java
/**
- * The number of elements. Accessed only either within locks
- * or among other volatile reads that maintain visibility.
- */
-transient int count;
+ * The number of elements. Accessed only either within locks
+ * or among other volatile reads that maintain visibility.
+ */
+transient int count;
```
-在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。
+在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。
-ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。
+ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。
-尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。
+尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。
-如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
+如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
```java
/**
- * Number of unsynchronized retries in size and containsValue
- * methods before resorting to locking. This is used to avoid
- * unbounded retries if tables undergo continuous modification
- * which would make it impossible to obtain an accurate result.
- */
-static final int RETRIES_BEFORE_LOCK = 2;
+ * Number of unsynchronized retries in size and containsValue
+ * methods before resorting to locking. This is used to avoid
+ * unbounded retries if tables undergo continuous modification
+ * which would make it impossible to obtain an accurate result.
+ */
+static final int RETRIES_BEFORE_LOCK = 2;
-public int size() {
- // Try a few times to get accurate count. On failure due to
- // continuous async changes in table, resort to locking.
- final Segment[] segments = this.segments;
- int size;
- boolean overflow; // true if size overflows 32 bits
- long sum; // sum of modCounts
- long last = 0L; // previous sum
- int retries = -1; // first iteration isn't retry
- try {
- for (;;) {
- // 超过尝试次数,则对每个 Segment 加锁
- if (retries++ == RETRIES_BEFORE_LOCK) {
- for (int j = 0; j < segments.length; ++j)
- ensureSegment(j).lock(); // force creation
- }
- sum = 0L;
- size = 0;
- overflow = false;
- for (int j = 0; j < segments.length; ++j) {
- Segment seg = segmentAt(segments, j);
- if (seg != null) {
- sum += seg.modCount;
- int c = seg.count;
- if (c < 0 || (size += c) < 0)
- overflow = true;
- }
- }
- // 连续两次得到的结果一致,则认为这个结果是正确的
- if (sum == last)
- break;
- last = sum;
- }
- } finally {
- if (retries > RETRIES_BEFORE_LOCK) {
- for (int j = 0; j < segments.length; ++j)
- segmentAt(segments, j).unlock();
- }
- }
- return overflow ? Integer.MAX_VALUE : size;
+public int size() {
+ // Try a few times to get accurate count. On failure due to
+ // continuous async changes in table, resort to locking.
+ final Segment[] segments = this.segments;
+ int size;
+ boolean overflow; // true if size overflows 32 bits
+ long sum; // sum of modCounts
+ long last = 0L; // previous sum
+ int retries = -1; // first iteration isn't retry
+ try {
+ for (;;) {
+ // 超过尝试次数,则对每个 Segment 加锁
+ if (retries++ == RETRIES_BEFORE_LOCK) {
+ for (int j = 0; j < segments.length; ++j)
+ ensureSegment(j).lock(); // force creation
+ }
+ sum = 0L;
+ size = 0;
+ overflow = false;
+ for (int j = 0; j < segments.length; ++j) {
+ Segment seg = segmentAt(segments, j);
+ if (seg != null) {
+ sum += seg.modCount;
+ int c = seg.count;
+ if (c < 0 || (size += c) < 0)
+ overflow = true;
+ }
+ }
+ // 连续两次得到的结果一致,则认为这个结果是正确的
+ if (sum == last)
+ break;
+ last = sum;
+ }
+ } finally {
+ if (retries > RETRIES_BEFORE_LOCK) {
+ for (int j = 0; j < segments.length; ++j)
+ segmentAt(segments, j).unlock();
+ }
+ }
+ return overflow ? Integer.MAX_VALUE : size;
}
```
-### 3. JDK 1.8 的改动
+### 3. JDK 1.8 的改动
[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/ConcurrentHashMap.java)
-JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发程度与 Segment 数量相等。
+JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发程度与 Segment 数量相等。
-JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。
+JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。
-并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。
+并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。
-## LinkedHashMap
+## LinkedHashMap
[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java)
-## TreeMap
+## TreeMap
[TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java)
-# 参考资料
+# 参考资料
-- 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)
-- [What is difference between HashMap and Hashtable in Java?](http://javarevisited.blogspot.hk/2010/10/difference-between-hashmap-and.html)
-- [Java 集合之 HashMap](http://www.zhangchangle.com/2018/02/07/Java%E9%9B%86%E5%90%88%E4%B9%8BHashMap/)
-- [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)
-- [Java Collection Framework – The LinkedList Class](http://javaconceptoftheday.com/java-collection-framework-linkedlist-class/)
+- 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)
+- [What is difference between HashMap and Hashtable in Java?](http://javarevisited.blogspot.hk/2010/10/difference-between-hashmap-and.html)
+- [Java 集合之 HashMap](http://www.zhangchangle.com/2018/02/07/Java%E9%9B%86%E5%90%88%E4%B9%8BHashMap/)
+- [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)
+- [Java Collection Framework – The LinkedList Class](http://javaconceptoftheday.com/java-collection-framework-linkedlist-class/)
+---bottom---CyC---
+![](index_files/java-collections.png)
+![](index_files/java-collections1.png)
+![](index_files/Iterator-1.jpg)
+![](index_files/HowLinkedListWorks.png)
+![](index_files/8fe838e3-ef77-4f63-bf45-417b6bc5c6bb.png)
+![](index_files/49d6de7b-0d0d-425c-9e49-a1559dc23b10.png)
+![](index_files/image005.jpg)
+![](index_files/image007.jpg)
+![](index_files/image008.jpg)
+![](index_files/image009.jpg)
diff --git a/notes/Java 并发.md b/notes/Java 并发.md
index 8a2c8fcf..00fdc0a9 100644
--- a/notes/Java 并发.md
+++ b/notes/Java 并发.md
@@ -1,618 +1,559 @@
-
-* [一、线程状态转换](#一线程状态转换)
- * [新建(New)](#新建new)
- * [可运行(Runnable)](#可运行runnable)
- * [阻塞(Blocking)](#阻塞blocking)
- * [无限期等待(Waiting)](#无限期等待waiting)
- * [限期等待(Timed Waiting)](#限期等待timed-waiting)
- * [死亡(Terminated)](#死亡terminated)
-* [二、使用线程](#二使用线程)
- * [实现 Runnable 接口](#实现-runnable-接口)
- * [实现 Callable 接口](#实现-callable-接口)
- * [继承 Thread 类](#继承-thread-类)
- * [实现接口 VS 继承 Thread](#实现接口-vs-继承-thread)
-* [三、基础线程机制](#三基础线程机制)
- * [Executor](#executor)
- * [Daemon](#daemon)
- * [sleep()](#sleep)
- * [yield()](#yield)
-* [四、中断](#四中断)
- * [InterruptedException](#interruptedexception)
- * [interrupted()](#interrupted)
- * [Executor 的中断操作](#executor-的中断操作)
-* [五、互斥同步](#五互斥同步)
- * [synchronized](#synchronized)
- * [ReentrantLock](#reentrantlock)
- * [synchronized 和 ReentrantLock 比较](#synchronized-和-reentrantlock-比较)
-* [六、线程之间的协作](#六线程之间的协作)
- * [join()](#join)
- * [wait() notify() notifyAll()](#wait-notify-notifyall)
- * [await() signal() signalAll()](#await-signal-signalall)
-* [七、J.U.C - AQS](#七juc---aqs)
- * [CountdownLatch](#countdownlatch)
- * [CyclicBarrier](#cyclicbarrier)
- * [Semaphore](#semaphore)
-* [八、J.U.C - 其它组件](#八juc---其它组件)
- * [FutureTask](#futuretask)
- * [BlockingQueue](#blockingqueue)
- * [ForkJoin](#forkjoin)
-* [九、线程不安全示例](#九线程不安全示例)
-* [十、Java 内存模型](#十java-内存模型)
- * [主内存与工作内存](#主内存与工作内存)
- * [内存间交互操作](#内存间交互操作)
- * [内存模型三大特性](#内存模型三大特性)
- * [先行发生原则](#先行发生原则)
-* [十一、线程安全](#十一线程安全)
- * [线程安全定义](#线程安全定义)
- * [线程安全分类](#线程安全分类)
- * [线程安全的实现方法](#线程安全的实现方法)
-* [十二、锁优化](#十二锁优化)
- * [自旋锁](#自旋锁)
- * [锁消除](#锁消除)
- * [锁粗化](#锁粗化)
- * [轻量级锁](#轻量级锁)
- * [偏向锁](#偏向锁)
-* [十三、多线程开发良好的实践](#十三多线程开发良好的实践)
-* [参考资料](#参考资料)
-
+# 一、线程状态转换
+
-# 一、线程状态转换
-
-
-
-## 新建(New)
+## 新建(New)
创建后尚未启动。
-## 可运行(Runnable)
+## 可运行(Runnable)
-可能正在运行,也可能正在等待 CPU 时间片。
+可能正在运行,也可能正在等待 CPU 时间片。
-包含了操作系统线程状态中的 Running 和 Ready。
+包含了操作系统线程状态中的 Running 和 Ready。
-## 阻塞(Blocking)
+## 阻塞(Blocking)
等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
-## 无限期等待(Waiting)
+## 无限期等待(Waiting)
-等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
+等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
-| 进入方法 | 退出方法 |
-| --- | --- |
-| 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
-| 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
-| LockSupport.park() 方法 | - |
+| 进入方法 | 退出方法 |
+| --- | --- |
+| 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
+| 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
+| LockSupport.park() 方法 | - |
-## 限期等待(Timed Waiting)
+## 限期等待(Timed Waiting)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
-调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
+调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
-调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
+调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
-阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁;而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
+阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁;而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
-| 进入方法 | 退出方法 |
-| --- | --- |
-| Thread.sleep() 方法 | 时间结束 |
-| 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
-| 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
-| LockSupport.parkNanos() 方法 | - |
-| LockSupport.parkUntil() 方法 | - |
+| 进入方法 | 退出方法 |
+| --- | --- |
+| Thread.sleep() 方法 | 时间结束 |
+| 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
+| 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
+| LockSupport.parkNanos() 方法 | - |
+| LockSupport.parkUntil() 方法 | - |
-## 死亡(Terminated)
+## 死亡(Terminated)
可以是线程结束任务之后自己结束,或者产生了异常而结束。
-# 二、使用线程
+# 二、使用线程
有三种使用线程的方法:
-- 实现 Runnable 接口;
-- 实现 Callable 接口;
-- 继承 Thread 类。
+- 实现 Runnable 接口;
+- 实现 Callable 接口;
+- 继承 Thread 类。
-实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
+实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
-## 实现 Runnable 接口
+## 实现 Runnable 接口
-需要实现 run() 方法。
+需要实现 run() 方法。
-通过 Thread 调用 start() 方法来启动线程。
+通过 Thread 调用 start() 方法来启动线程。
```java
-public class MyRunnable implements Runnable {
- public void run() {
- // ...
- }
+public class MyRunnable implements Runnable {
+ public void run() {
+ // ...
+ }
}
```
```java
-public static void main(String[] args) {
- MyRunnable instance = new MyRunnable();
- Thread thread = new Thread(instance);
- thread.start();
+public static void main(String[] args) {
+ MyRunnable instance = new MyRunnable();
+ Thread thread = new Thread(instance);
+ thread.start();
}
```
-## 实现 Callable 接口
+## 实现 Callable 接口
-与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
+与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
```java
-public class MyCallable implements Callable {
- public Integer call() {
- return 123;
- }
+public class MyCallable implements Callable {
+ public Integer call() {
+ return 123;
+ }
}
```
```java
-public static void main(String[] args) throws ExecutionException, InterruptedException {
- MyCallable mc = new MyCallable();
- FutureTask ft = new FutureTask<>(mc);
- Thread thread = new Thread(ft);
- thread.start();
- System.out.println(ft.get());
+public static void main(String[] args) throws ExecutionException, InterruptedException {
+ MyCallable mc = new MyCallable();
+ FutureTask ft = new FutureTask<>(mc);
+ Thread thread = new Thread(ft);
+ thread.start();
+ System.out.println(ft.get());
}
```
-## 继承 Thread 类
+## 继承 Thread 类
-同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。
+同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。
```java
-public class MyThread extends Thread {
- public void run() {
- // ...
- }
+public class MyThread extends Thread {
+ public void run() {
+ // ...
+ }
}
```
```java
-public static void main(String[] args) {
- MyThread mt = new MyThread();
- mt.start();
+public static void main(String[] args) {
+ MyThread mt = new MyThread();
+ mt.start();
}
```
-## 实现接口 VS 继承 Thread
+## 实现接口 VS 继承 Thread
实现接口会更好一些,因为:
-- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
-- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
+- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
+- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
-# 三、基础线程机制
+# 三、基础线程机制
-## Executor
+## Executor
-Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。
+Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。
-主要有三种 Executor:
+主要有三种 Executor:
-- CachedThreadPool:一个任务创建一个线程;
-- FixedThreadPool:所有任务只能使用固定大小的线程;
-- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
+- CachedThreadPool:一个任务创建一个线程;
+- FixedThreadPool:所有任务只能使用固定大小的线程;
+- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
```java
-public static void main(String[] args) {
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < 5; i++) {
- executorService.execute(new MyRunnable());
- }
- executorService.shutdown();
+public static void main(String[] args) {
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < 5; i++) {
+ executorService.execute(new MyRunnable());
+ }
+ executorService.shutdown();
}
```
-## Daemon
+## Daemon
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
-main() 属于非守护线程。
+main() 属于非守护线程。
-使用 setDaemon() 方法将一个线程设置为守护线程。
+使用 setDaemon() 方法将一个线程设置为守护线程。
```java
-public static void main(String[] args) {
- Thread thread = new Thread(new MyRunnable());
- thread.setDaemon(true);
+public static void main(String[] args) {
+ Thread thread = new Thread(new MyRunnable());
+ thread.setDaemon(true);
}
```
-## sleep()
+## sleep()
-Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
+Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
-sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
+sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
```java
-public void run() {
- try {
- Thread.sleep(3000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
+public void run() {
+ try {
+ Thread.sleep(3000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
}
```
-## yield()
+## yield()
-对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
+对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
```java
-public void run() {
- Thread.yield();
+public void run() {
+ Thread.yield();
}
```
-# 四、中断
+# 四、中断
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
-## InterruptedException
+## InterruptedException
-通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
+通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
-对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
+对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
```java
-public class InterruptExample {
+public class InterruptExample {
- private static class MyThread1 extends Thread {
- @Override
- public void run() {
- try {
- Thread.sleep(2000);
- System.out.println("Thread run");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
+ private static class MyThread1 extends Thread {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(2000);
+ System.out.println("Thread run");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
}
```
```java
-public static void main(String[] args) throws InterruptedException {
- Thread thread1 = new MyThread1();
- thread1.start();
- thread1.interrupt();
- System.out.println("Main run");
+public static void main(String[] args) throws InterruptedException {
+ Thread thread1 = new MyThread1();
+ thread1.start();
+ thread1.interrupt();
+ System.out.println("Main run");
}
```
```html
-Main run
-java.lang.InterruptedException: sleep interrupted
- at java.lang.Thread.sleep(Native Method)
- at InterruptExample.lambda$main$0(InterruptExample.java:5)
- at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
- at java.lang.Thread.run(Thread.java:745)
+Main run
+java.lang.InterruptedException: sleep interrupted
+ at java.lang.Thread.sleep(Native Method)
+ at InterruptExample.lambda$main$0(InterruptExample.java:5)
+ at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
+ at java.lang.Thread.run(Thread.java:745)
```
-## interrupted()
+## interrupted()
-如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
+如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
-但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
+但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
```java
-public class InterruptExample {
+public class InterruptExample {
- private static class MyThread2 extends Thread {
- @Override
- public void run() {
- while (!interrupted()) {
- // ..
- }
- System.out.println("Thread end");
- }
- }
+ private static class MyThread2 extends Thread {
+ @Override
+ public void run() {
+ while (!interrupted()) {
+ // ..
+ }
+ System.out.println("Thread end");
+ }
+ }
}
```
```java
-public static void main(String[] args) throws InterruptedException {
- Thread thread2 = new MyThread2();
- thread2.start();
- thread2.interrupt();
+public static void main(String[] args) throws InterruptedException {
+ Thread thread2 = new MyThread2();
+ thread2.start();
+ thread2.interrupt();
}
```
```html
-Thread end
+Thread end
```
-## Executor 的中断操作
+## Executor 的中断操作
-调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
+调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
-以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。
+以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。
```java
-public static void main(String[] args) {
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> {
- try {
- Thread.sleep(2000);
- System.out.println("Thread run");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
- executorService.shutdownNow();
- System.out.println("Main run");
+public static void main(String[] args) {
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> {
+ try {
+ Thread.sleep(2000);
+ System.out.println("Thread run");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ });
+ executorService.shutdownNow();
+ System.out.println("Main run");
}
```
```html
-Main run
-java.lang.InterruptedException: sleep interrupted
- at java.lang.Thread.sleep(Native Method)
- at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
- at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
- at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
- at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
- at java.lang.Thread.run(Thread.java:745)
+Main run
+java.lang.InterruptedException: sleep interrupted
+ at java.lang.Thread.sleep(Native Method)
+ at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
+ at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
+ at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
+ at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
+ at java.lang.Thread.run(Thread.java:745)
```
-如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
+如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
```java
-Future> future = executorService.submit(() -> {
- // ..
+Future> future = executorService.submit(() -> {
+ // ..
});
future.cancel(true);
```
-# 五、互斥同步
+# 五、互斥同步
-Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
+Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
-## synchronized
+## synchronized
-**1. 同步一个代码块**
+**1. 同步一个代码块**
```java
-public void func () {
- synchronized (this) {
- // ...
- }
+public void func () {
+ synchronized (this) {
+ // ...
+ }
}
```
它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
-对于以下代码,使用 ExecutorService 执行了两个线程(这两个线程使用 Lambda 创建),由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
+对于以下代码,使用 ExecutorService 执行了两个线程(这两个线程使用 Lambda 创建),由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
```java
-public class SynchronizedExample {
+public class SynchronizedExample {
- public void func1() {
- synchronized (this) {
- for (int i = 0; i < 10; i++) {
- System.out.print(i + " ");
- }
- }
- }
+ public void func1() {
+ synchronized (this) {
+ for (int i = 0; i < 10; i++) {
+ System.out.print(i + " ");
+ }
+ }
+ }
}
```
```java
-public static void main(String[] args) {
- SynchronizedExample e1 = new SynchronizedExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> e1.func1());
- executorService.execute(() -> e1.func1());
+public static void main(String[] args) {
+ SynchronizedExample e1 = new SynchronizedExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> e1.func1());
+ executorService.execute(() -> e1.func1());
}
```
```html
-0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
+0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```
对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。
```java
-public static void main(String[] args) {
- SynchronizedExample e1 = new SynchronizedExample();
- SynchronizedExample e2 = new SynchronizedExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> e1.func1());
- executorService.execute(() -> e2.func1());
+public static void main(String[] args) {
+ SynchronizedExample e1 = new SynchronizedExample();
+ SynchronizedExample e2 = new SynchronizedExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> e1.func1());
+ executorService.execute(() -> e2.func1());
}
```
```html
-0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
+0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
```
-**2. 同步一个方法**
+**2. 同步一个方法**
```java
-public synchronized void func () {
- // ...
+public synchronized void func () {
+ // ...
}
```
它和同步代码块一样,只作用于同一个对象。
-**3. 同步一个类**
+**3. 同步一个类**
```java
-public void func() {
- synchronized (SynchronizedExample.class) {
- // ...
- }
+public void func() {
+ synchronized (SynchronizedExample.class) {
+ // ...
+ }
}
```
作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也需要进行同步。
```java
-public class SynchronizedExample {
+public class SynchronizedExample {
- public void func2() {
- synchronized (SynchronizedExample.class) {
- for (int i = 0; i < 10; i++) {
- System.out.print(i + " ");
- }
- }
- }
+ public void func2() {
+ synchronized (SynchronizedExample.class) {
+ for (int i = 0; i < 10; i++) {
+ System.out.print(i + " ");
+ }
+ }
+ }
}
```
```java
-public static void main(String[] args) {
- SynchronizedExample e1 = new SynchronizedExample();
- SynchronizedExample e2 = new SynchronizedExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> e1.func2());
- executorService.execute(() -> e2.func2());
+public static void main(String[] args) {
+ SynchronizedExample e1 = new SynchronizedExample();
+ SynchronizedExample e2 = new SynchronizedExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> e1.func2());
+ executorService.execute(() -> e2.func2());
}
```
```html
-0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
+0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```
-**4. 同步一个静态方法**
+**4. 同步一个静态方法**
```java
-public synchronized static void fun() {
- // ...
+public synchronized static void fun() {
+ // ...
}
```
作用于整个类。
-## ReentrantLock
+## ReentrantLock
```java
-public class LockExample {
+public class LockExample {
- private Lock lock = new ReentrantLock();
+ private Lock lock = new ReentrantLock();
- public void func() {
- lock.lock();
- try {
- for (int i = 0; i < 10; i++) {
- System.out.print(i + " ");
- }
- } finally {
- lock.unlock(); // 确保释放锁,从而避免发生死锁。
- }
- }
+ public void func() {
+ lock.lock();
+ try {
+ for (int i = 0; i < 10; i++) {
+ System.out.print(i + " ");
+ }
+ } finally {
+ lock.unlock(); // 确保释放锁,从而避免发生死锁。
+ }
+ }
}
```
```java
-public static void main(String[] args) {
- LockExample lockExample = new LockExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> lockExample.func());
- executorService.execute(() -> lockExample.func());
+public static void main(String[] args) {
+ LockExample lockExample = new LockExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> lockExample.func());
+ executorService.execute(() -> lockExample.func());
}
```
```html
-0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
+0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```
-ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了以下高级功能:
+ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了以下高级功能:
-**1. 等待可中断**
+**1. 等待可中断**
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
-**2. 可实现公平锁**
+**2. 可实现公平锁**
-公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
+公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
-**3. 锁绑定多个条件**
+**3. 锁绑定多个条件**
-一个 ReentrantLock 对象可以同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无须这样做,只需要多次调用 newCondition() 方法即可。
+一个 ReentrantLock 对象可以同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无须这样做,只需要多次调用 newCondition() 方法即可。
-## synchronized 和 ReentrantLock 比较
+## synchronized 和 ReentrantLock 比较
-**1. 锁的实现**
+**1. 锁的实现**
-synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
+synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
-**2. 性能**
+**2. 性能**
-从性能上来看,新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。
+从性能上来看,新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。
-**3. 功能**
+**3. 功能**
-ReentrantLock 多了一些高级功能。
+ReentrantLock 多了一些高级功能。
-**4. 使用选择**
+**4. 使用选择**
-除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
+除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
-# 六、线程之间的协作
+# 六、线程之间的协作
当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。
-## join()
+## join()
-在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待, 直到目标线程结束。
+在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待, 直到目标线程结束。
-对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,因此 b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先与 b 线程的输出。
+对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,因此 b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先与 b 线程的输出。
```java
-public class JoinExample {
+public class JoinExample {
- private class A extends Thread {
- @Override
- public void run() {
- System.out.println("A");
- }
- }
+ private class A extends Thread {
+ @Override
+ public void run() {
+ System.out.println("A");
+ }
+ }
- private class B extends Thread {
+ private class B extends Thread {
- private A a;
+ private A a;
- B(A a) {
- this.a = a;
- }
+ B(A a) {
+ this.a = a;
+ }
- @Override
- public void run() {
- try {
- a.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("B");
- }
- }
+ @Override
+ public void run() {
+ try {
+ a.join();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println("B");
+ }
+ }
- public void test() {
- A a = new A();
- B b = new B(a);
- b.start();
- a.start();
- }
+ public void test() {
+ A a = new A();
+ B b = new B(a);
+ b.start();
+ a.start();
+ }
}
```
```java
-public static void main(String[] args) {
- JoinExample example = new JoinExample();
- example.test();
+public static void main(String[] args) {
+ JoinExample example = new JoinExample();
+ example.test();
}
```
@@ -621,40 +562,40 @@ A
B
```
-## wait() notify() notifyAll()
+## wait() notify() notifyAll()
-调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
+调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
-它们都属于 Object 的一部分,而不属于 Thread。
+它们都属于 Object 的一部分,而不属于 Thread。
-只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。
+只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。
-使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
+使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
```java
-public class WaitNotifyExample {
- public synchronized void before() {
- System.out.println("before");
- notifyAll();
- }
+public class WaitNotifyExample {
+ public synchronized void before() {
+ System.out.println("before");
+ notifyAll();
+ }
- public synchronized void after() {
- try {
- wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("after");
- }
+ public synchronized void after() {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println("after");
+ }
}
```
```java
-public static void main(String[] args) {
- ExecutorService executorService = Executors.newCachedThreadPool();
- WaitNotifyExample example = new WaitNotifyExample();
- executorService.execute(() -> example.after());
- executorService.execute(() -> example.before());
+public static void main(String[] args) {
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ WaitNotifyExample example = new WaitNotifyExample();
+ executorService.execute(() -> example.after());
+ executorService.execute(() -> example.before());
}
```
@@ -663,52 +604,52 @@ before
after
```
-**wait() 和 sleep() 的区别**
+**wait() 和 sleep() 的区别**
-1. wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
-2. wait() 会释放锁,sleep() 不会。
+1. wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
+2. wait() 会释放锁,sleep() 不会。
-## await() signal() signalAll()
+## await() signal() signalAll()
-java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
+java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
-使用 Lock 来获取一个 Condition 对象。
+使用 Lock 来获取一个 Condition 对象。
```java
-public class AwaitSignalExample {
- private Lock lock = new ReentrantLock();
- private Condition condition = lock.newCondition();
+public class AwaitSignalExample {
+ private Lock lock = new ReentrantLock();
+ private Condition condition = lock.newCondition();
- public void before() {
- lock.lock();
- try {
- System.out.println("before");
- condition.signalAll();
- } finally {
- lock.unlock();
- }
- }
+ public void before() {
+ lock.lock();
+ try {
+ System.out.println("before");
+ condition.signalAll();
+ } finally {
+ lock.unlock();
+ }
+ }
- public void after() {
- lock.lock();
- try {
- condition.await();
- System.out.println("after");
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();
- }
- }
+ public void after() {
+ lock.lock();
+ try {
+ condition.await();
+ System.out.println("after");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ lock.unlock();
+ }
+ }
}
```
```java
-public static void main(String[] args) {
- ExecutorService executorService = Executors.newCachedThreadPool();
- AwaitSignalExample example = new AwaitSignalExample();
- executorService.execute(() -> example.after());
- executorService.execute(() -> example.before());
+public static void main(String[] args) {
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ AwaitSignalExample example = new AwaitSignalExample();
+ executorService.execute(() -> example.after());
+ executorService.execute(() -> example.before());
}
```
@@ -717,35 +658,35 @@ before
after
```
-# 七、J.U.C - AQS
+# 七、J.U.C - AQS
-java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。
+java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。
-## CountdownLatch
+## CountdownLatch
用来控制一个线程等待多个线程。
-维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
+维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
-
+
```java
-public class CountdownLatchExample {
+public class CountdownLatchExample {
- public static void main(String[] args) throws InterruptedException {
- final int totalThread = 10;
- CountDownLatch countDownLatch = new CountDownLatch(totalThread);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < totalThread; i++) {
- executorService.execute(() -> {
- System.out.print("run..");
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- System.out.println("end");
- executorService.shutdown();
- }
+ public static void main(String[] args) throws InterruptedException {
+ final int totalThread = 10;
+ CountDownLatch countDownLatch = new CountDownLatch(totalThread);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < totalThread; i++) {
+ executorService.execute(() -> {
+ System.out.print("run..");
+ countDownLatch.countDown();
+ });
+ }
+ countDownLatch.await();
+ System.out.println("end");
+ executorService.shutdown();
+ }
}
```
@@ -753,37 +694,37 @@ public class CountdownLatchExample {
run..run..run..run..run..run..run..run..run..run..end
```
-## CyclicBarrier
+## CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
-和 CountdownLatch 相似,都是通过维护计数器来实现的。但是它的计数器是递增的,每次执行 await() 方法之后,计数器会加 1,直到计数器的值和设置的值相等,等待的所有线程才会继续执行。和 CountdownLatch 的另一个区别是,CyclicBarrier 的计数器可以循环使用,所以它才叫做循环屏障。
+和 CountdownLatch 相似,都是通过维护计数器来实现的。但是它的计数器是递增的,每次执行 await() 方法之后,计数器会加 1,直到计数器的值和设置的值相等,等待的所有线程才会继续执行。和 CountdownLatch 的另一个区别是,CyclicBarrier 的计数器可以循环使用,所以它才叫做循环屏障。
下图应该从下往上看才正确。
-
+
```java
-public class CyclicBarrierExample {
- public static void main(String[] args) throws InterruptedException {
- final int totalThread = 10;
- CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < totalThread; i++) {
- executorService.execute(() -> {
- System.out.print("before..");
- try {
- cyclicBarrier.await();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (BrokenBarrierException e) {
- e.printStackTrace();
- }
- System.out.print("after..");
- });
- }
- executorService.shutdown();
- }
+public class CyclicBarrierExample {
+ public static void main(String[] args) throws InterruptedException {
+ final int totalThread = 10;
+ CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < totalThread; i++) {
+ executorService.execute(() -> {
+ System.out.print("before..");
+ try {
+ cyclicBarrier.await();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (BrokenBarrierException e) {
+ e.printStackTrace();
+ }
+ System.out.print("after..");
+ });
+ }
+ executorService.shutdown();
+ }
}
```
@@ -791,152 +732,152 @@ public class CyclicBarrierExample {
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..
```
-## Semaphore
+## Semaphore
-Semaphore 就是操作系统中的信号量,可以控制对互斥资源的访问线程数。
+Semaphore 就是操作系统中的信号量,可以控制对互斥资源的访问线程数。
-
+
-以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。
+以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。
```java
-public class SemaphoreExample {
- public static void main(String[] args) {
- final int clientCount = 3;
- final int totalRequestCount = 10;
- Semaphore semaphore = new Semaphore(clientCount);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < totalRequestCount; i++) {
- executorService.execute(()->{
- try {
- semaphore.acquire();
- System.out.print(semaphore.availablePermits() + " ");
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- semaphore.release();
- }
- });
- }
- executorService.shutdown();
- }
+public class SemaphoreExample {
+ public static void main(String[] args) {
+ final int clientCount = 3;
+ final int totalRequestCount = 10;
+ Semaphore semaphore = new Semaphore(clientCount);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < totalRequestCount; i++) {
+ executorService.execute(()->{
+ try {
+ semaphore.acquire();
+ System.out.print(semaphore.availablePermits() + " ");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ semaphore.release();
+ }
+ });
+ }
+ executorService.shutdown();
+ }
}
```
```html
-2 1 2 2 2 2 2 1 2 2
+2 1 2 2 2 2 2 1 2 2
```
-# 八、J.U.C - 其它组件
+# 八、J.U.C - 其它组件
-## FutureTask
+## FutureTask
-在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。
+在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。
```java
-public class FutureTask implements RunnableFuture
+public class FutureTask implements RunnableFuture
```
```java
-public interface RunnableFuture extends Runnable, Future
+public interface RunnableFuture extends Runnable, Future
```
-当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,用一个线程去执行该任务,然后其它线程继续执行其它任务。当需要该任务的计算结果时,再通过 FutureTask 的 get() 方法获取。
+当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,用一个线程去执行该任务,然后其它线程继续执行其它任务。当需要该任务的计算结果时,再通过 FutureTask 的 get() 方法获取。
```java
-public class FutureTaskExample {
- public static void main(String[] args) throws ExecutionException, InterruptedException {
- FutureTask futureTask = new FutureTask(new Callable() {
- @Override
- public Integer call() throws Exception {
- int result = 0;
- for (int i = 0; i < 100; i++) {
- Thread.sleep(10);
- result += i;
- }
- return result;
- }
- });
+public class FutureTaskExample {
+ public static void main(String[] args) throws ExecutionException, InterruptedException {
+ FutureTask futureTask = new FutureTask(new Callable() {
+ @Override
+ public Integer call() throws Exception {
+ int result = 0;
+ for (int i = 0; i < 100; i++) {
+ Thread.sleep(10);
+ result += i;
+ }
+ return result;
+ }
+ });
- Thread computeThread = new Thread(futureTask);
- computeThread.start();
+ Thread computeThread = new Thread(futureTask);
+ computeThread.start();
- Thread otherThread = new Thread(() -> {
- System.out.println("other task is running...");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
- otherThread.start();
- System.out.println(futureTask.get());
- }
+ Thread otherThread = new Thread(() -> {
+ System.out.println("other task is running...");
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ });
+ otherThread.start();
+ System.out.println(futureTask.get());
+ }
}
```
```html
-other task is running...
+other task is running...
4950
```
-## BlockingQueue
+## BlockingQueue
-java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
+java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
-- **FIFO 队列** :LinkedBlockingQueue、ArrayListBlockingQueue(固定长度)
-- **优先级队列** :PriorityBlockingQueue
+- **FIFO 队列**:LinkedBlockingQueue、ArrayListBlockingQueue(固定长度)
+- **优先级队列**:PriorityBlockingQueue
-提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,指到队列有空闲位置。
+提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,指到队列有空闲位置。
-**使用 BlockingQueue 实现生产者消费者问题**
+**使用 BlockingQueue 实现生产者消费者问题**
```java
-public class ProducerConsumer {
+public class ProducerConsumer {
- private static BlockingQueue queue = new ArrayBlockingQueue<>(5);
+ private static BlockingQueue queue = new ArrayBlockingQueue<>(5);
- private static class Producer extends Thread {
- @Override
- public void run() {
- try {
- queue.put("product");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.print("produce..");
- }
- }
+ private static class Producer extends Thread {
+ @Override
+ public void run() {
+ try {
+ queue.put("product");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.print("produce..");
+ }
+ }
- private static class Consumer extends Thread {
+ private static class Consumer extends Thread {
- @Override
- public void run() {
- try {
- String product = queue.take();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.print("consume..");
- }
- }
+ @Override
+ public void run() {
+ try {
+ String product = queue.take();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.print("consume..");
+ }
+ }
}
```
```java
-public static void main(String[] args) {
- for (int i = 0; i < 2; i++) {
- Producer producer = new Producer();
- producer.start();
- }
- for (int i = 0; i < 5; i++) {
- Consumer consumer = new Consumer();
- consumer.start();
- }
- for (int i = 0; i < 3; i++) {
- Producer producer = new Producer();
- producer.start();
- }
+public static void main(String[] args) {
+ for (int i = 0; i < 2; i++) {
+ Producer producer = new Producer();
+ producer.start();
+ }
+ for (int i = 0; i < 5; i++) {
+ Consumer consumer = new Consumer();
+ consumer.start();
+ }
+ for (int i = 0; i < 3; i++) {
+ Producer producer = new Producer();
+ producer.start();
+ }
}
```
@@ -944,98 +885,98 @@ public static void main(String[] args) {
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
```
-## ForkJoin
+## ForkJoin
-主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。
+主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。
```java
-public class ForkJoinExample extends RecursiveTask {
- private final int threhold = 5;
- private int first;
- private int last;
+public class ForkJoinExample extends RecursiveTask {
+ private final int threhold = 5;
+ private int first;
+ private int last;
- public ForkJoinExample(int first, int last) {
- this.first = first;
- this.last = last;
- }
+ public ForkJoinExample(int first, int last) {
+ this.first = first;
+ this.last = last;
+ }
- @Override
- protected Integer compute() {
- int result = 0;
- if (last - first <= threhold) {
- // 任务足够小则直接计算
- for (int i = first; i <= last; i++) {
- result += i;
- }
- } else {
- // 拆分成小任务
- int middle = first + (last - first) / 2;
- ForkJoinExample leftTask = new ForkJoinExample(first, middle);
- ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
- leftTask.fork();
- rightTask.fork();
- result = leftTask.join() + rightTask.join();
- }
- return result;
- }
+ @Override
+ protected Integer compute() {
+ int result = 0;
+ if (last - first <= threhold) {
+ // 任务足够小则直接计算
+ for (int i = first; i <= last; i++) {
+ result += i;
+ }
+ } else {
+ // 拆分成小任务
+ int middle = first + (last - first) / 2;
+ ForkJoinExample leftTask = new ForkJoinExample(first, middle);
+ ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
+ leftTask.fork();
+ rightTask.fork();
+ result = leftTask.join() + rightTask.join();
+ }
+ return result;
+ }
}
```
```java
-public static void main(String[] args) throws ExecutionException, InterruptedException {
- ForkJoinExample example = new ForkJoinExample(1, 10000);
- ForkJoinPool forkJoinPool = new ForkJoinPool();
- Future result = forkJoinPool.submit(example);
- System.out.println(result.get());
+public static void main(String[] args) throws ExecutionException, InterruptedException {
+ ForkJoinExample example = new ForkJoinExample(1, 10000);
+ ForkJoinPool forkJoinPool = new ForkJoinPool();
+ Future result = forkJoinPool.submit(example);
+ System.out.println(result.get());
}
```
-ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。
+ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。
```java
-public class ForkJoinPool extends AbstractExecutorService
+public class ForkJoinPool extends AbstractExecutorService
```
-ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。
+ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。
-
+
-# 九、线程不安全示例
+# 九、线程不安全示例
如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。
-以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值为 997 而不是 1000。
+以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值为 997 而不是 1000。
```java
-public class ThreadUnsafeExample {
+public class ThreadUnsafeExample {
- private int cnt = 0;
+ private int cnt = 0;
- public void add() {
- cnt++;
- }
+ public void add() {
+ cnt++;
+ }
- public int get() {
- return cnt;
- }
+ public int get() {
+ return cnt;
+ }
}
```
```java
-public static void main(String[] args) throws InterruptedException {
- final int threadSize = 1000;
- ThreadUnsafeExample example = new ThreadUnsafeExample();
- final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < threadSize; i++) {
- executorService.execute(() -> {
- example.add();
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- executorService.shutdown();
- System.out.println(example.get());
+public static void main(String[] args) throws InterruptedException {
+ final int threadSize = 1000;
+ ThreadUnsafeExample example = new ThreadUnsafeExample();
+ final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < threadSize; i++) {
+ executorService.execute(() -> {
+ example.add();
+ countDownLatch.countDown();
+ });
+ }
+ countDownLatch.await();
+ executorService.shutdown();
+ System.out.println(example.get());
}
```
@@ -1043,88 +984,88 @@ public static void main(String[] args) throws InterruptedException {
997
```
-# 十、Java 内存模型
+# 十、Java 内存模型
-Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
+Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
-## 主内存与工作内存
+## 主内存与工作内存
处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。
-
+
所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
-
+
-## 内存间交互操作
+## 内存间交互操作
-Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。
+Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。
-
+
-- read:把一个变量的值从主内存传输到工作内存中
-- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
-- use:把工作内存中一个变量的值传递给执行引擎
-- assign:把一个从执行引擎接收到的值赋给工作内存的变量
-- store:把工作内存的一个变量的值传送到主内存中
-- write:在 store 之后执行,把 store 得到的值放入主内存的变量中
-- lock:作用于主内存的变量
-- unlock
+- read:把一个变量的值从主内存传输到工作内存中
+- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
+- use:把工作内存中一个变量的值传递给执行引擎
+- assign:把一个从执行引擎接收到的值赋给工作内存的变量
+- store:把工作内存的一个变量的值传送到主内存中
+- write:在 store 之后执行,把 store 得到的值放入主内存的变量中
+- lock:作用于主内存的变量
+- unlock
-## 内存模型三大特性
+## 内存模型三大特性
-### 1. 原子性
+### 1. 原子性
-Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。
+Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。
-有一个错误认识就是,int 等原子性的变量在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 变量属于 int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。
+有一个错误认识就是,int 等原子性的变量在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 变量属于 int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。
-为了方便讨论,将内存间的交互操作简化为 3 个:load、assign、store。
+为了方便讨论,将内存间的交互操作简化为 3 个:load、assign、store。
-下图演示了两个线程同时对 cnt 变量进行操作,load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入该变量的值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。
+下图演示了两个线程同时对 cnt 变量进行操作,load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入该变量的值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。
-
+
-AtomicInteger 能保证多个线程修改的原子性。
+AtomicInteger 能保证多个线程修改的原子性。
-
+
-使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现:
+使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现:
```java
-public class AtomicExample {
- private AtomicInteger cnt = new AtomicInteger();
+public class AtomicExample {
+ private AtomicInteger cnt = new AtomicInteger();
- public void add() {
- cnt.incrementAndGet();
- }
+ public void add() {
+ cnt.incrementAndGet();
+ }
- public int get() {
- return cnt.get();
- }
+ public int get() {
+ return cnt.get();
+ }
}
```
```java
-public static void main(String[] args) throws InterruptedException {
- final int threadSize = 1000;
- AtomicExample example = new AtomicExample(); // 只修改这条语句
- final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < threadSize; i++) {
- executorService.execute(() -> {
- example.add();
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- executorService.shutdown();
- System.out.println(example.get());
+public static void main(String[] args) throws InterruptedException {
+ final int threadSize = 1000;
+ AtomicExample example = new AtomicExample(); // 只修改这条语句
+ final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < threadSize; i++) {
+ executorService.execute(() -> {
+ example.add();
+ countDownLatch.countDown();
+ });
+ }
+ countDownLatch.await();
+ executorService.shutdown();
+ System.out.println(example.get());
}
```
@@ -1132,37 +1073,37 @@ public static void main(String[] args) throws InterruptedException {
1000
```
-除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的完整性,它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
+除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的完整性,它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
```java
-public class AtomicSynchronizedExample {
- private int cnt = 0;
+public class AtomicSynchronizedExample {
+ private int cnt = 0;
- public synchronized void add() {
- cnt++;
- }
+ public synchronized void add() {
+ cnt++;
+ }
- public synchronized int get() {
- return cnt;
- }
+ public synchronized int get() {
+ return cnt;
+ }
}
```
```java
-public static void main(String[] args) throws InterruptedException {
- final int threadSize = 1000;
- AtomicSynchronizedExample example = new AtomicSynchronizedExample();
- final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < threadSize; i++) {
- executorService.execute(() -> {
- example.add();
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- executorService.shutdown();
- System.out.println(example.get());
+public static void main(String[] args) throws InterruptedException {
+ final int threadSize = 1000;
+ AtomicSynchronizedExample example = new AtomicSynchronizedExample();
+ final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < threadSize; i++) {
+ executorService.execute(() -> {
+ example.add();
+ countDownLatch.countDown();
+ });
+ }
+ countDownLatch.await();
+ executorService.shutdown();
+ System.out.println(example.get());
}
```
@@ -1170,297 +1111,297 @@ public static void main(String[] args) throws InterruptedException {
1000
```
-### 2. 可见性
+### 2. 可见性
-可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
+可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
-volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
+volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
-对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。
+对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。
-### 3. 有序性
+### 3. 有序性
有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。
-在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
+在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
-volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
+volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
-也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。
+也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。
-## 先行发生原则
+## 先行发生原则
-上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
+上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
主要有以下这些原则:
-### 1. 单一线程原则
+### 1. 单一线程原则
-> Single Thread rule
+> Single Thread rule
在一个线程内,在程序前面的操作先行发生于后面的操作。
-
+
-### 2. 管程锁定规则
+### 2. 管程锁定规则
-> Monitor Lock Rule
+> Monitor Lock Rule
-一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
+一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
-
+
-### 3. volatile 变量规则
+### 3. volatile 变量规则
-> Volatile Variable Rule
+> Volatile Variable Rule
-对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
+对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
-
+
-### 4. 线程启动规则
+### 4. 线程启动规则
-> Thread Start Rule
+> Thread Start Rule
-Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
+Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
-
+
-### 5. 线程加入规则
+### 5. 线程加入规则
-> Thread Join Rule
+> Thread Join Rule
-join() 方法返回先行发生于 Thread 对象的结束。
+join() 方法返回先行发生于 Thread 对象的结束。
-
+
-### 6. 线程中断规则
+### 6. 线程中断规则
-> Thread Interruption Rule
+> Thread Interruption Rule
-对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测到是否有中断发生。
+对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测到是否有中断发生。
-### 7. 对象终结规则
+### 7. 对象终结规则
-> Finalizer Rule
+> Finalizer Rule
-一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
+一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
-### 8. 传递性
+### 8. 传递性
-> Transitivity
+> Transitivity
-如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
+如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
-# 十一、线程安全
+# 十一、线程安全
-## 线程安全定义
+## 线程安全定义
一个类在可以被多个线程安全调用时就是线程安全的。
-## 线程安全分类
+## 线程安全分类
线程安全不是一个非真即假的命题,可以将共享数据按照安全程度的强弱顺序分成以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
-### 1. 不可变
+### 1. 不可变
不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施,只要一个不可变的对象被正确地构建出来,那其外部的可见状态永远也不会改变,永远也不会看到它在多个线程之中处于不一致的状态。
不可变的类型:
-- final 关键字修饰的基本数据类型;
-- String
-- 枚举类型
-- Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的子类型的原子类 AtomicInteger 和 AtomicLong 则并非不可变的。
+- final 关键字修饰的基本数据类型;
+- String
+- 枚举类型
+- Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的子类型的原子类 AtomicInteger 和 AtomicLong 则并非不可变的。
-对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
+对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
```java
-public class ImmutableExample {
- public static void main(String[] args) {
- Map map = new HashMap<>();
- Map unmodifiableMap = Collections.unmodifiableMap(map);
- unmodifiableMap.put("a", 1);
- }
+public class ImmutableExample {
+ public static void main(String[] args) {
+ Map map = new HashMap<>();
+ Map unmodifiableMap = Collections.unmodifiableMap(map);
+ unmodifiableMap.put("a", 1);
+ }
}
```
```html
-Exception in thread "main" java.lang.UnsupportedOperationException
- at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
- at ImmutableExample.main(ImmutableExample.java:9)
+Exception in thread "main" java.lang.UnsupportedOperationException
+ at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
+ at ImmutableExample.main(ImmutableExample.java:9)
```
-Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。
+Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。
```java
-public V put(K key, V value) {
- throw new UnsupportedOperationException();
+public V put(K key, V value) {
+ throw new UnsupportedOperationException();
}
```
多线程环境下,应当尽量使对象成为不可变,来满足线程安全。
-### 2. 绝对线程安全
+### 2. 绝对线程安全
不管运行时环境如何,调用者都不需要任何额外的同步措施。
-### 3. 相对线程安全
+### 3. 相对线程安全
相对的线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
-在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。
+在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。
-对于下面的代码,如果删除元素的线程删除了一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。
+对于下面的代码,如果删除元素的线程删除了一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。
```java
-public class VectorUnsafeExample {
- private static Vector vector = new Vector<>();
+public class VectorUnsafeExample {
+ private static Vector vector = new Vector<>();
- public static void main(String[] args) {
- while (true) {
- for (int i = 0; i < 100; i++) {
- vector.add(i);
- }
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> {
- for (int i = 0; i < vector.size(); i++) {
- vector.remove(i);
- }
- });
- executorService.execute(() -> {
- for (int i = 0; i < vector.size(); i++) {
- vector.get(i);
- }
- });
- executorService.shutdown();
- }
- }
+ public static void main(String[] args) {
+ while (true) {
+ for (int i = 0; i < 100; i++) {
+ vector.add(i);
+ }
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> {
+ for (int i = 0; i < vector.size(); i++) {
+ vector.remove(i);
+ }
+ });
+ executorService.execute(() -> {
+ for (int i = 0; i < vector.size(); i++) {
+ vector.get(i);
+ }
+ });
+ executorService.shutdown();
+ }
+ }
}
```
```html
-Exception in thread "Thread-159738" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 3
- at java.util.Vector.remove(Vector.java:831)
- at VectorUnsafeExample.lambda$main$0(VectorUnsafeExample.java:14)
- at VectorUnsafeExample$$Lambda$1/713338599.run(Unknown Source)
- at java.lang.Thread.run(Thread.java:745)
+Exception in thread "Thread-159738" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 3
+ at java.util.Vector.remove(Vector.java:831)
+ at VectorUnsafeExample.lambda$main$0(VectorUnsafeExample.java:14)
+ at VectorUnsafeExample$$Lambda$1/713338599.run(Unknown Source)
+ at java.lang.Thread.run(Thread.java:745)
```
如果要保证上面的代码能正确执行下去,就需要对删除元素和获取元素的代码进行同步。
```java
-executorService.execute(() -> {
- synchronized (vector) {
- for (int i = 0; i < vector.size(); i++) {
- vector.remove(i);
- }
- }
+executorService.execute(() -> {
+ synchronized (vector) {
+ for (int i = 0; i < vector.size(); i++) {
+ vector.remove(i);
+ }
+ }
});
-executorService.execute(() -> {
- synchronized (vector) {
- for (int i = 0; i < vector.size(); i++) {
- vector.get(i);
- }
- }
+executorService.execute(() -> {
+ synchronized (vector) {
+ for (int i = 0; i < vector.size(); i++) {
+ vector.get(i);
+ }
+ }
});
```
-### 4. 线程兼容
+### 4. 线程兼容
-线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。
+线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。
-### 5. 线程对立
+### 5. 线程对立
-线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。
+线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。
-## 线程安全的实现方法
+## 线程安全的实现方法
-### 1. 互斥同步
+### 1. 互斥同步
-synchronized 和 ReentrantLock。
+synchronized 和 ReentrantLock。
-### 2. 非阻塞同步
+### 2. 非阻塞同步
-互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。
+互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。
-从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。
+从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。
-CAS 指令需要有 3 个操作数,分别是内存位置(在 Java 中可以简单理解为变量的内存地址,用 V 表示)、旧的预期值(用 A 表示)和新值(用 B 表示)。CAS 指令执行时,当且仅当 V 符合旧预期值 A 时,处理器用新值 B 更新 V 的值,否则它就不执行更新。但是无论是否更新了 V 的值,都会返回 V 的旧值,上述的处理过程是一个原子操作。
+CAS 指令需要有 3 个操作数,分别是内存位置(在 Java 中可以简单理解为变量的内存地址,用 V 表示)、旧的预期值(用 A 表示)和新值(用 B 表示)。CAS 指令执行时,当且仅当 V 符合旧预期值 A 时,处理器用新值 B 更新 V 的值,否则它就不执行更新。但是无论是否更新了 V 的值,都会返回 V 的旧值,上述的处理过程是一个原子操作。
-J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。
+J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。
-在下面的代码 1 中,使用了 AtomicInteger 执行了自增的操作。代码 2 是 incrementAndGet() 的源码,它调用了 unsafe 的 getAndAddInt() 。代码 3 是 getAndAddInt() 源码,var1 指示内存位置,var2 指示新值,var4 指示操作需要加的数值,这里为 1。在代码 3 的实现中,通过 getIntVolatile(var1, var2) 得到旧的预期值。通过调用 compareAndSwapInt() 来进行 CAS 比较,如果 var2=var5,那么就更新内存地址为 var1 的变量为 var5+var4。可以看到代码 3 是在一个循环中进行,发生冲突的做法是不断的进行重试。
+在下面的代码 1 中,使用了 AtomicInteger 执行了自增的操作。代码 2 是 incrementAndGet() 的源码,它调用了 unsafe 的 getAndAddInt() 。代码 3 是 getAndAddInt() 源码,var1 指示内存位置,var2 指示新值,var4 指示操作需要加的数值,这里为 1。在代码 3 的实现中,通过 getIntVolatile(var1, var2) 得到旧的预期值。通过调用 compareAndSwapInt() 来进行 CAS 比较,如果 var2=var5,那么就更新内存地址为 var1 的变量为 var5+var4。可以看到代码 3 是在一个循环中进行,发生冲突的做法是不断的进行重试。
```java
-// 代码 1
-private AtomicInteger cnt = new AtomicInteger();
+// 代码 1
+private AtomicInteger cnt = new AtomicInteger();
-public void add() {
- cnt.incrementAndGet();
+public void add() {
+ cnt.incrementAndGet();
}
```
```java
-// 代码 2
-public final int incrementAndGet() {
- return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
+// 代码 2
+public final int incrementAndGet() {
+ return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
```
```java
-// 代码 3
-public final int getAndAddInt(Object var1, long var2, int var4) {
- int var5;
- do {
- var5 = this.getIntVolatile(var1, var2);
- } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
+// 代码 3
+public final int getAndAddInt(Object var1, long var2, int var4) {
+ int var5;
+ do {
+ var5 = this.getIntVolatile(var1, var2);
+ } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
- return var5;
+ return var5;
}
```
-ABA :如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
+ABA :如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
-### 3. 无同步方案
+### 3. 无同步方案
要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。
-**(一)可重入代码(Reentrant Code)**
+**(一)可重入代码(Reentrant Code)**
-这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。
+这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。我们可以通过一个简单的原则来判断代码是否具备可重入性:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。
-**(二)栈封闭**
+**(二)栈封闭**
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在栈中,属于线程私有的。
```java
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
-public class StackClosedExample {
- public void add100() {
- int cnt = 0;
- for (int i = 0; i < 100; i++) {
- cnt++;
- }
- System.out.println(cnt);
- }
+public class StackClosedExample {
+ public void add100() {
+ int cnt = 0;
+ for (int i = 0; i < 100; i++) {
+ cnt++;
+ }
+ System.out.println(cnt);
+ }
}
```
```java
-public static void main(String[] args) {
- StackClosedExample example = new StackClosedExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> example.add100());
- executorService.execute(() -> example.add100());
- executorService.shutdown();
+public static void main(String[] args) {
+ StackClosedExample example = new StackClosedExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> example.add100());
+ executorService.execute(() -> example.add100());
+ executorService.shutdown();
}
```
@@ -1469,37 +1410,37 @@ public static void main(String[] args) {
100
```
-**(三)线程本地存储(Thread Local Storage)**
+**(三)线程本地存储(Thread Local Storage)**
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
-符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
+符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
-可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
+可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
-对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。
+对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。
```java
-public class ThreadLocalExample {
- public static void main(String[] args) {
- ThreadLocal threadLocal = new ThreadLocal();
- Thread thread1 = new Thread(() -> {
- threadLocal.set(1);
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(threadLocal.get());
- threadLocal.remove();
- });
- Thread thread2 = new Thread(() -> {
- threadLocal.set(2);
- threadLocal.remove();
- });
- thread1.start();
- thread2.start();
- }
+public class ThreadLocalExample {
+ public static void main(String[] args) {
+ ThreadLocal threadLocal = new ThreadLocal();
+ Thread thread1 = new Thread(() -> {
+ threadLocal.set(1);
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println(threadLocal.get());
+ threadLocal.remove();
+ });
+ Thread thread2 = new Thread(() -> {
+ threadLocal.set(2);
+ threadLocal.remove();
+ });
+ thread1.start();
+ thread2.start();
+ }
}
```
@@ -1507,85 +1448,85 @@ public class ThreadLocalExample {
1
```
-为了理解 ThreadLocal,先看以下代码:
+为了理解 ThreadLocal,先看以下代码:
```java
-public class ThreadLocalExample1 {
- public static void main(String[] args) {
- ThreadLocal threadLocal1 = new ThreadLocal();
- ThreadLocal threadLocal2 = new ThreadLocal();
- Thread thread1 = new Thread(() -> {
- threadLocal1.set(1);
- threadLocal2.set(1);
- });
- Thread thread2 = new Thread(() -> {
- threadLocal1.set(2);
- threadLocal2.set(2);
- });
- thread1.start();
- thread2.start();
- }
+public class ThreadLocalExample1 {
+ public static void main(String[] args) {
+ ThreadLocal threadLocal1 = new ThreadLocal();
+ ThreadLocal threadLocal2 = new ThreadLocal();
+ Thread thread1 = new Thread(() -> {
+ threadLocal1.set(1);
+ threadLocal2.set(1);
+ });
+ Thread thread2 = new Thread(() -> {
+ threadLocal1.set(2);
+ threadLocal2.set(2);
+ });
+ thread1.start();
+ thread2.start();
+ }
}
```
它所对应的底层结构图为:
-
+
-每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象,Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员。
+每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象,Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员。
```java
-/* ThreadLocal values pertaining to this thread. This map is maintained
- * by the ThreadLocal class. */
-ThreadLocal.ThreadLocalMap threadLocals = null;
+/* ThreadLocal values pertaining to this thread. This map is maintained
+ * by the ThreadLocal class. */
+ThreadLocal.ThreadLocalMap threadLocals = null;
```
-当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。
+当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。
```java
-public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
+public void set(T value) {
+ Thread t = Thread.currentThread();
+ ThreadLocalMap map = getMap(t);
+ if (map != null)
+ map.set(this, value);
+ else
+ createMap(t, value);
}
```
-get() 方法类似。
+get() 方法类似。
```java
-public T get() {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null) {
- @SuppressWarnings("unchecked")
- T result = (T)e.value;
- return result;
- }
- }
- return setInitialValue();
+public T get() {
+ Thread t = Thread.currentThread();
+ ThreadLocalMap map = getMap(t);
+ if (map != null) {
+ ThreadLocalMap.Entry e = map.getEntry(this);
+ if (e != null) {
+ @SuppressWarnings("unchecked")
+ T result = (T)e.value;
+ return result;
+ }
+ }
+ return setInitialValue();
}
```
-ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。
+ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。
-# 十二、锁优化
+# 十二、锁优化
-这里的锁优化主要是指虚拟机对 synchronized 的优化。
+这里的锁优化主要是指虚拟机对 synchronized 的优化。
-## 自旋锁
+## 自旋锁
互斥同步的进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
-自选锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。自旋次数的默认值是 10 次,用户可以使用虚拟机参数 -XX:PreBlockSpin 来更改。
+自选锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。自旋次数的默认值是 10 次,用户可以使用虚拟机参数 -XX:PreBlockSpin 来更改。
-在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
+在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
-## 锁消除
+## 锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
@@ -1594,93 +1535,114 @@ ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因
对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:
```java
-public static String concatString(String s1, String s2, String s3) {
- return s1 + s2 + s3;
+public static String concatString(String s1, String s2, String s3) {
+ return s1 + s2 + s3;
}
```
-String 是一个不可变的类,Javac 编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,即上面的代码可能会变成下面的样子:
+String 是一个不可变的类,Javac 编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,即上面的代码可能会变成下面的样子:
```java
-public static String concatString(String s1, String s2, String s3) {
- StringBuffer sb = new StringBuffer();
- sb.append(s1);
- sb.append(s2);
- sb.append(s3);
- return sb.toString();
+public static String concatString(String s1, String s2, String s3) {
+ StringBuffer sb = new StringBuffer();
+ sb.append(s1);
+ sb.append(s2);
+ sb.append(s3);
+ return sb.toString();
}
```
-每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会“逃逸”到 concatString() 方法之外,其他线程无法访问到它。因此,虽然这里有锁,但是可以被安全地消除掉。
+每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会“逃逸”到 concatString() 方法之外,其他线程无法访问到它。因此,虽然这里有锁,但是可以被安全地消除掉。
-## 锁粗化
+## 锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
-上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
+上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
-## 轻量级锁
+## 轻量级锁
-JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
+JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
-以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 mark word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出,应该注意的是 state 表格不是存储在对象头中的。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
+以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 mark word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出,应该注意的是 state 表格不是存储在对象头中的。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
-
+
-下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。
+下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。
-
+
-轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
+轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
-当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
+当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
-
+
-如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
+如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
-## 偏向锁
+## 偏向锁
-偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
+偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
-可以使用 -XX:+UseBiasedLocking=true 开启偏向锁,不过在 JDK 1.6 中它是默认开启的。
+可以使用 -XX:+UseBiasedLocking=true 开启偏向锁,不过在 JDK 1.6 中它是默认开启的。
-当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
+当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
-当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
+当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
-
+
-# 十三、多线程开发良好的实践
+# 十三、多线程开发良好的实践
-- 给线程起个有意义的名字,这样可以方便找 Bug。
+- 给线程起个有意义的名字,这样可以方便找 Bug。
-- 缩小同步范围,例如对于 synchronized,应该尽量使用同步块而不是同步方法。
+- 缩小同步范围,例如对于 synchronized,应该尽量使用同步块而不是同步方法。
-- 多用同步类少用 wait() 和 notify()。首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护,在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
+- 多用同步类少用 wait() 和 notify()。首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护,在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
-- 多用并发集合少用同步集合。并发集合比同步集合的可扩展性更好,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
+- 多用并发集合少用同步集合。并发集合比同步集合的可扩展性更好,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
-- 使用本地变量和不可变类来保证线程安全。
+- 使用本地变量和不可变类来保证线程安全。
-- 使用线程池而不是直接创建 Thread 对象,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。
+- 使用线程池而不是直接创建 Thread 对象,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。
-- 使用 BlockingQueue 实现生产者消费者问题。
+- 使用 BlockingQueue 实现生产者消费者问题。
-# 参考资料
+# 参考资料
-- BruceEckel. Java 编程思想: 第 4 版 [M]. 机械工业出版社, 2007.
-- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
-- [Threads and Locks](https://docs.oracle.com/javase/specs/jvms/se6/html/Threads.doc.html)
-- [线程通信](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)
-- [How is Java's ThreadLocal implemented under the hood?](https://stackoverflow.com/questions/1202444/how-is-javas-threadlocal-implemented-under-the-hood/15653015)
-- [Concurrent](https://sites.google.com/site/webdevelopart/21-compile/06-java/javase/concurrent?tmpl=%2Fsystem%2Fapp%2Ftemplates%2Fprint%2F&showPrintDialog=1)
-- [JAVA FORK JOIN EXAMPLE](http://www.javacreed.com/java-fork-join-example/ "Java Fork Join Example")
-- [聊聊并发(八)——Fork/Join 框架介绍](http://ifeve.com/talk-concurrency-forkjoin/)
-- [Eliminating SynchronizationRelated Atomic Operations with Biased Locking and Bulk Rebiasing](http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf)
+- BruceEckel. Java 编程思想: 第 4 版 [M]. 机械工业出版社, 2007.
+- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
+- [Threads and Locks](https://docs.oracle.com/javase/specs/jvms/se6/html/Threads.doc.html)
+- [线程通信](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)
+- [How is Java's ThreadLocal implemented under the hood?](https://stackoverflow.com/questions/1202444/how-is-javas-threadlocal-implemented-under-the-hood/15653015)
+- [Concurrent](https://sites.google.com/site/webdevelopart/21-compile/06-java/javase/concurrent?tmpl=%2Fsystem%2Fapp%2Ftemplates%2Fprint%2F&showPrintDialog=1)
+- [JAVA FORK JOIN EXAMPLE](http://www.javacreed.com/java-fork-join-example/ "Java Fork Join Example")
+- [聊聊并发(八)——Fork/Join 框架介绍](http://ifeve.com/talk-concurrency-forkjoin/)
+- [Eliminating SynchronizationRelated Atomic Operations with Biased Locking and Bulk Rebiasing](http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf)
+---bottom---CyC---
+![](index_files/ace830df-9919-48ca-91b5-60b193f593d2.png)
+![](index_files/CountdownLatch.png)
+![](index_files/CyclicBarrier.png)
+![](index_files/Semaphore.png)
+![](index_files/15b45dc6-27aa-4519-9194-f4acfa2b077f.jpg)
+![](index_files/68778c1b-15ab-4826-99c0-3b4fd38cb9e9.png)
+![](index_files/47358f87-bc4c-496f-9a90-8d696de94cee.png)
+![](index_files/536c6dfd-305a-4b95-b12c-28ca5e8aa043.png)
+![](index_files/ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92.png)
+![](index_files/952afa9a-458b-44ce-bba9-463e60162945.png)
+![](index_files/single-thread-rule.png)
+![](index_files/monitor-lock-rule.png)
+![](index_files/volatile-variable-rule.png)
+![](index_files/thread-start-rule.png)
+![](index_files/thread-join-rule.png)
+![](index_files/3646544a-cb57-451d-9e03-d3c4f5e4434a.png)
+![](index_files/bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png)
+![](index_files/051e436c-0e46-4c59-8f67-52d89d656182.png)
+![](index_files/baaa681f-7c52-4198-a5ae-303b9386cf47.png)
+![](index_files/390c913b-5f31-444f-bbdb-2b88b688e7ce.jpg)
diff --git a/notes/Java 虚拟机.md b/notes/Java 虚拟机.md
index acb244a7..9d6d20f4 100644
--- a/notes/Java 虚拟机.md
+++ b/notes/Java 虚拟机.md
@@ -1,248 +1,222 @@
-
-* [一、运行时数据区域](#一运行时数据区域)
- * [程序计数器](#程序计数器)
- * [虚拟机栈](#虚拟机栈)
- * [本地方法栈](#本地方法栈)
- * [堆](#堆)
- * [方法区](#方法区)
- * [运行时常量池](#运行时常量池)
- * [直接内存](#直接内存)
-* [二、垃圾收集](#二垃圾收集)
- * [判断一个对象是否可回收](#判断一个对象是否可回收)
- * [垃圾收集算法](#垃圾收集算法)
- * [垃圾收集器](#垃圾收集器)
- * [内存分配与回收策略](#内存分配与回收策略)
-* [三、类加载机制](#三类加载机制)
- * [类的生命周期](#类的生命周期)
- * [类初始化时机](#类初始化时机)
- * [类加载过程](#类加载过程)
- * [类加载器](#类加载器)
-* [四、JVM 参数](#四jvm-参数)
- * [GC 优化配置](#gc-优化配置)
- * [GC 类型设置](#gc-类型设置)
-* [参考资料](#参考资料)
-
+# 一、运行时数据区域
+
-# 一、运行时数据区域
-
-
-
-## 程序计数器
+## 程序计数器
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
-## 虚拟机栈
+## 虚拟机栈
-每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
+每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
-
+
-可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小:
+可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小:
```java
-java -Xss=512M HackTheJava
+java -Xss=512M HackTheJava
```
该区域可能抛出以下异常:
-- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
-- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
+- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
+- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
-## 本地方法栈
+## 本地方法栈
-本地方法不是用 Java 实现,对待这些方法需要特别处理。
+本地方法不是用 Java 实现,对待这些方法需要特别处理。
-与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
+与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
-
+
-## 堆
+## 堆
所有对象实例都在这里分配内存。
-是垃圾收集的主要区域("GC 堆"),现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法,因此虚拟机把 Java 堆分成以下三块:
+是垃圾收集的主要区域("GC 堆"),现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法,因此虚拟机把 Java 堆分成以下三块:
-- 新生代(Young Generation)
-- 老年代(Old Generation)
-- 永久代(Permanent Generation)
+- 新生代(Young Generation)
+- 老年代(Old Generation)
+- 永久代(Permanent Generation)
当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间:
-- Eden
-- From Survivor
-- To Survivor
+- Eden
+- From Survivor
+- To Survivor
-
+
-Java 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
+Java 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
-可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
+可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
```java
-java -Xms=1M -Xmx=2M HackTheJava
+java -Xms=1M -Xmx=2M HackTheJava
```
-## 方法区
+## 方法区
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
-和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
+和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
-JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收,JDK 1.8 之后,取消了永久代,用 metaspace(元数据)区替代。
+JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收,JDK 1.8 之后,取消了永久代,用 metaspace(元数据)区替代。
-## 运行时常量池
+## 运行时常量池
运行时常量池是方法区的一部分。
-Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
+Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
-除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池。
+除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池。
-## 直接内存
+## 直接内存
-在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
+在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
-# 二、垃圾收集
+# 二、垃圾收集
-程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。
+程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。
-## 判断一个对象是否可回收
+## 判断一个对象是否可回收
-### 1. 引用计数算法
+### 1. 引用计数算法
-给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
+给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
-两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
+两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
```java
-public class ReferenceCountingGC {
- public Object instance = null;
+public class ReferenceCountingGC {
+ public Object instance = null;
- public static void main(String[] args) {
- ReferenceCountingGC objectA = new ReferenceCountingGC();
- ReferenceCountingGC objectB = new ReferenceCountingGC();
- objectA.instance = objectB;
- objectB.instance = objectA;
- }
+ public static void main(String[] args) {
+ ReferenceCountingGC objectA = new ReferenceCountingGC();
+ ReferenceCountingGC objectB = new ReferenceCountingGC();
+ objectA.instance = objectB;
+ objectB.instance = objectA;
+ }
}
```
-正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
+正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
-### 2. 可达性分析算法
+### 2. 可达性分析算法
-通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
+通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
-
+
-Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
+Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
-- 虚拟机栈中引用的对象
-- 本地方法栈中引用的对象
-- 方法区中类静态属性引用的对象
-- 方法区中的常量引用的对象
+- 虚拟机栈中引用的对象
+- 本地方法栈中引用的对象
+- 方法区中类静态属性引用的对象
+- 方法区中的常量引用的对象
-### 3. 引用类型
+### 3. 引用类型
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否可被回收都与引用有关。
-Java 具有四种强度不同的引用类型。
+Java 具有四种强度不同的引用类型。
-**(一)强引用**
+**(一)强引用**
被强引用关联的对象不会被垃圾收集器回收。
-使用 new 一个新对象的方式来创建强引用。
+使用 new 一个新对象的方式来创建强引用。
```java
-Object obj = new Object();
+Object obj = new Object();
```
-**(二)软引用**
+**(二)软引用**
被软引用关联的对象,只有在内存不够的情况下才会被回收。
-使用 SoftReference 类来创建软引用。
+使用 SoftReference 类来创建软引用。
```java
-Object obj = new Object();
-SoftReference sf = new SoftReference(obj);
-obj = null; // 使对象只被软引用关联
+Object obj = new Object();
+SoftReference sf = new SoftReference(obj);
+obj = null; // 使对象只被软引用关联
```
-**(三)弱引用**
+**(三)弱引用**
被弱引用关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集发生之前。
-使用 WeakReference 类来实现弱引用。
+使用 WeakReference 类来实现弱引用。
```java
-Object obj = new Object();
-WeakReference wf = new WeakReference(obj);
-obj = null;
+Object obj = new Object();
+WeakReference wf = new WeakReference(obj);
+obj = null;
```
-WeakHashMap 的 Entry 继承自 WeakReference,主要用来实现缓存。
+WeakHashMap 的 Entry 继承自 WeakReference,主要用来实现缓存。
```java
-private static class Entry extends WeakReference implements Map.Entry
+private static class Entry extends WeakReference implements Map.Entry
```
-Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。ConcurrentCache 采取的是分代缓存,经常使用的对象放入 eden 中,而不常用的对象放入 longterm。eden 使用 ConcurrentHashMap 实现,longterm 使用 WeakHashMap,保证了不常使用的对象容易被回收。
+Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。ConcurrentCache 采取的是分代缓存,经常使用的对象放入 eden 中,而不常用的对象放入 longterm。eden 使用 ConcurrentHashMap 实现,longterm 使用 WeakHashMap,保证了不常使用的对象容易被回收。
```java
-public final class ConcurrentCache {
+public final class ConcurrentCache {
- private final int size;
+ private final int size;
- private final Map eden;
+ private final Map eden;
- private final Map longterm;
+ private final Map longterm;
- public ConcurrentCache(int size) {
- this.size = size;
- this.eden = new ConcurrentHashMap<>(size);
- this.longterm = new WeakHashMap<>(size);
- }
+ public ConcurrentCache(int size) {
+ this.size = size;
+ this.eden = new ConcurrentHashMap<>(size);
+ this.longterm = new WeakHashMap<>(size);
+ }
- public V get(K k) {
- V v = this.eden.get(k);
- if (v == null) {
- v = this.longterm.get(k);
- if (v != null)
- this.eden.put(k, v);
- }
- return v;
- }
+ public V get(K k) {
+ V v = this.eden.get(k);
+ if (v == null) {
+ v = this.longterm.get(k);
+ if (v != null)
+ this.eden.put(k, v);
+ }
+ return v;
+ }
- public void put(K k, V v) {
- if (this.eden.size() >= size) {
- this.longterm.putAll(this.eden);
- this.eden.clear();
- }
- this.eden.put(k, v);
- }
+ public void put(K k, V v) {
+ if (this.eden.size() >= size) {
+ this.longterm.putAll(this.eden);
+ this.eden.clear();
+ }
+ this.eden.put(k, v);
+ }
}
```
-**(四)虚引用**
+**(四)虚引用**
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
-使用 PhantomReference 来实现虚引用。
+使用 PhantomReference 来实现虚引用。
```java
-Object obj = new Object();
-PhantomReference pf = new PhantomReference(obj);
-obj = null;
+Object obj = new Object();
+PhantomReference pf = new PhantomReference(obj);
+obj = null;
```
-### 4. 方法区的回收
+### 4. 方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代差很多,因此在方法区上进行回收性价比不高。
@@ -250,575 +224,595 @@ obj = null;
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
-- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
-- 加载该类的 ClassLoader 已经被回收。
-- 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
+- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
+- 加载该类的 ClassLoader 已经被回收。
+- 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
-可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
+可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
-在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
+在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
-### 5. finalize()
+### 5. finalize()
-finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
+finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
-当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。
+当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。
-## 垃圾收集算法
+## 垃圾收集算法
-### 1. 标记 - 清除
+### 1. 标记 - 清除
-
+
将需要存活的对象进行标记,然后清理掉未被标记的对象。
不足:
-- 标记和清除过程效率都不高;
-- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
+- 标记和清除过程效率都不高;
+- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
-### 2. 标记 - 整理
+### 2. 标记 - 整理
-
+
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
-### 3. 复制
+### 3. 复制
-
+
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
-现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。
+现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。
-### 4. 分代收集
+### 4. 分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
-一般将 Java 堆分为新生代和老年代。
+一般将 Java 堆分为新生代和老年代。
-- 新生代使用:复制算法
-- 老年代使用:标记 - 清理 或者 标记 - 整理 算法
+- 新生代使用:复制算法
+- 老年代使用:标记 - 清理 或者 标记 - 整理 算法
-## 垃圾收集器
+## 垃圾收集器
-
+
-以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
+以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
-### 1. Serial 收集器
+### 1. Serial 收集器
-
+
-Serial 翻译为串行,垃圾收集和用户程序不能同时执行,这意味着在执行垃圾收集的时候需要停顿用户程序。除了 CMS 和 G1 之外,其它收集器都是以串行的方式执行。CMS 和 G1 可以使得垃圾收集和用户程序同时执行,被称为并发执行。
+Serial 翻译为串行,垃圾收集和用户程序不能同时执行,这意味着在执行垃圾收集的时候需要停顿用户程序。除了 CMS 和 G1 之外,其它收集器都是以串行的方式执行。CMS 和 G1 可以使得垃圾收集和用户程序同时执行,被称为并发执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
-它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
+它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
-它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
+它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
-### 2. ParNew 收集器
+### 2. ParNew 收集器
-
+
-它是 Serial 收集器的多线程版本。
+它是 Serial 收集器的多线程版本。
-是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
+是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
-默认开始的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。
+默认开始的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。
-### 3. Parallel Scavenge 收集器
+### 3. Parallel Scavenge 收集器
-与 ParNew 一样是并行的多线程收集器。
+与 ParNew 一样是并行的多线程收集器。
-其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
+其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
-停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
+停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
-提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
+提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
-还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。
+还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。
-### 4. Serial Old 收集器
+### 4. Serial Old 收集器
-
+
-是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
+是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
-- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
-- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
+- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
+- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
-### 5. Parallel Old 收集器
+### 5. Parallel Old 收集器
-
+
-是 Parallel Scavenge 收集器的老年代版本。
+是 Parallel Scavenge 收集器的老年代版本。
-在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
+在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
-### 6. CMS 收集器
+### 6. CMS 收集器
-
+
-CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
+CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
-特点:并发收集、低停顿。并发指的是用户线程和 GC 线程同时运行。
+特点:并发收集、低停顿。并发指的是用户线程和 GC 线程同时运行。
分为以下四个流程:
-- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
-- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
-- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
-- 并发清除:不需要停顿。
+- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
+- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
+- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
+- 并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
具有以下缺点:
-- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
-- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。可以使用 -XX:CMSInitiatingOccupancyFraction 来改变触发 CMS 收集器工作的内存占用百分,如果这个值设置的太大,导致预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
-- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
+- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
+- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。可以使用 -XX:CMSInitiatingOccupancyFraction 来改变触发 CMS 收集器工作的内存占用百分,如果这个值设置的太大,导致预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
+- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
-### 7. G1 收集器
+### 7. G1 收集器
-G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
+G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
-Java 堆被分为新生代、老年代和永久代,其它收集器进行收集的范围都是整个新生代或者老生代,而 G1 可以直接对新生代和永久代一起回收。
+Java 堆被分为新生代、老年代和永久代,其它收集器进行收集的范围都是整个新生代或者老生代,而 G1 可以直接对新生代和永久代一起回收。
-
+
-G1 把新生代和老年代划分成多个大小相等的独立区域(Region),新生代和永久代不再物理隔离。
+G1 把新生代和老年代划分成多个大小相等的独立区域(Region),新生代和永久代不再物理隔离。
-
+
-通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
+通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
-每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
+每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
-
+
-如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
+如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
-- 初始标记
-- 并发标记
-- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
-- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
+- 初始标记
+- 并发标记
+- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
+- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
具备如下特点:
-- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
-- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
+- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
+- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
-更详细内容请参考:[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html)
+更详细内容请参考:[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html)
-### 8. 比较
+### 8. 比较
-| 收集器 | 串行/并行/并发 | 新生代/老年代 | 收集算法 | 目标 | 适用场景 |
-| :---: | :---: | :---: | :---: | :---: | :---: |
-| **Serial** | 串行 | 新生代 | 复制 | 响应速度优先 | 单 CPU 环境下的 Client 模式 |
-| **Serial Old** | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 |
-| **ParNew** | 串行 + 并行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 |
-| **Parallel Scavenge** | 串行 + 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
-| **Parallel Old** | 串行 + 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
-| **CMS** | 并行 + 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 |
-| **G1** | 并行 + 并发 | 新生代 + 老年代 | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS |
+| 收集器 | 串行/并行/并发 | 新生代/老年代 | 收集算法 | 目标 | 适用场景 |
+| :---: | :---: | :---: | :---: | :---: | :---: |
+| **Serial** | 串行 | 新生代 | 复制 | 响应速度优先 | 单 CPU 环境下的 Client 模式 |
+| **Serial Old** | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 |
+| **ParNew** | 串行 + 并行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 |
+| **Parallel Scavenge** | 串行 + 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
+| **Parallel Old** | 串行 + 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
+| **CMS** | 并行 + 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 |
+| **G1** | 并行 + 并发 | 新生代 + 老年代 | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS |
-## 内存分配与回收策略
+## 内存分配与回收策略
-对象的内存分配,也就是在堆上分配。主要分配在新生代的 Eden 区上,少数情况下也可能直接分配在老年代中。
+对象的内存分配,也就是在堆上分配。主要分配在新生代的 Eden 区上,少数情况下也可能直接分配在老年代中。
-### 1. Minor GC 和 Full GC
+### 1. Minor GC 和 Full GC
-- Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
-- Full GC:发生在老年代上,老年代对象和新生代的相反,其存活时间长,因此 Full GC 很少执行,而且执行速度会比 Minor GC 慢很多。
+- Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
+- Full GC:发生在老年代上,老年代对象和新生代的相反,其存活时间长,因此 Full GC 很少执行,而且执行速度会比 Minor GC 慢很多。
-### 2. 内存分配策略
+### 2. 内存分配策略
-**(一)对象优先在 Eden 分配**
+**(一)对象优先在 Eden 分配**
-大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
+大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
-**(二)大对象直接进入老年代**
+**(二)大对象直接进入老年代**
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
--XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
+-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
-**(三)长期存活的对象进入老年代**
+**(三)长期存活的对象进入老年代**
-为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
+为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
--XX:MaxTenuringThreshold 用来定义年龄的阈值。
+-XX:MaxTenuringThreshold 用来定义年龄的阈值。
-**(四)动态对象年龄判定**
+**(四)动态对象年龄判定**
-虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
+虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
-**(五)空间分配担保**
+**(五)空间分配担保**
-在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的;如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。
+在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的;如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。
-### 3. Full GC 的触发条件
+### 3. Full GC 的触发条件
-对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
+对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
-**(一)调用 System.gc()**
+**(一)调用 System.gc()**
-此方法的调用是建议虚拟机进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。可通过 -XX:DisableExplicitGC 来禁止 RMI 调用 System.gc()。
+此方法的调用是建议虚拟机进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。可通过 -XX:DisableExplicitGC 来禁止 RMI 调用 System.gc()。
-**(二)老年代空间不足**
+**(二)老年代空间不足**
-老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组。
+老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组。
-**(三)空间分配担保失败**
+**(三)空间分配担保失败**
-使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。
+使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。
-**(四)JDK 1.7 及以前的永久代空间不足**
+**(四)JDK 1.7 及以前的永久代空间不足**
-在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError,为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
+在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError,为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
-**(五)Concurrent Mode Failure**
+**(五)Concurrent Mode Failure**
-执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
+执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
-# 三、类加载机制
+# 三、类加载机制
类是在运行期间动态加载的。
-## 类的生命周期
+## 类的生命周期
-
+
-包括以下 7 个阶段:
+包括以下 7 个阶段:
-- **加载(Loading)**
-- **验证(Verification)**
-- **准备(Preparation)**
-- **解析(Resolution)**
-- **初始化(Initialization)**
-- 使用(Using)
-- 卸载(Unloading)
+- **加载(Loading)**
+- **验证(Verification)**
+- **准备(Preparation)**
+- **解析(Resolution)**
+- **初始化(Initialization)**
+- 使用(Using)
+- 卸载(Unloading)
-其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
+其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
-## 类初始化时机
+## 类初始化时机
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随着发生):
-- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
+- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
-- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
+- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
-- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
+- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
-- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
+- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
-- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
+- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
-以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
+以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
-- 通过子类引用父类的静态字段,不会导致子类初始化。
+- 通过子类引用父类的静态字段,不会导致子类初始化。
```java
-System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
+System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
```
-- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
+- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
```java
-SuperClass[] sca = new SuperClass[10];
+SuperClass[] sca = new SuperClass[10];
```
-- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
+- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
```java
System.out.println(ConstClass.HELLOWORLD);
```
-## 类加载过程
+## 类加载过程
-包含了加载、验证、准备、解析和初始化这 5 个阶段。
+包含了加载、验证、准备、解析和初始化这 5 个阶段。
-### 1. 加载
+### 1. 加载
加载是类加载的一个阶段,注意不要混淆。
加载过程完成以下三件事:
-- 通过一个类的全限定名来获取定义此类的二进制字节流。
-- 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
-- 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。
+- 通过一个类的全限定名来获取定义此类的二进制字节流。
+- 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
+- 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。
其中二进制字节流可以从以下方式中获取:
-- 从 ZIP 包读取,这很常见,最终成为日后 JAR、EAR、WAR 格式的基础。
-- 从网络中获取,这种场景最典型的应用是 Applet。
-- 运行时计算生成,这种场景使用得最多得就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
-- 由其他文件生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。
-- 从数据库读取,这种场景相对少见,例如有些中间件服务器(如 SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
+- 从 ZIP 包读取,这很常见,最终成为日后 JAR、EAR、WAR 格式的基础。
+- 从网络中获取,这种场景最典型的应用是 Applet。
+- 运行时计算生成,这种场景使用得最多得就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
+- 由其他文件生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。
+- 从数据库读取,这种场景相对少见,例如有些中间件服务器(如 SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
...
-### 2. 验证
+### 2. 验证
-确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
+确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
-- 文件格式验证:验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
-- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
-- 字节码验证:通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
-- 符号引用验证:发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
+- 文件格式验证:验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
+- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
+- 字节码验证:通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
+- 符号引用验证:发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
-### 3. 准备
+### 3. 准备
-类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
+类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
-实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。(实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次)
+实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。(实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次)
-初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
+初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
```java
-public static int value = 123;
+public static int value = 123;
```
-如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
+如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
```java
-public static final int value = 123;
+public static final int value = 123;
```
-### 4. 解析
+### 4. 解析
将常量池的符号引用替换为直接引用的过程。
-### 5. 初始化
+### 5. 初始化
-初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 <clinit>() 方法的过程。
+初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 <clinit>() 方法的过程。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
-<clinit>() 方法具有以下特点:
+<clinit>() 方法具有以下特点:
-- 是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
+- 是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
```java
-public class Test {
- static {
- i = 0; // 给变量赋值可以正常编译通过
- System.out.print(i); // 这句编译器会提示“非法向前引用”
- }
- static int i = 1;
+public class Test {
+ static {
+ i = 0; // 给变量赋值可以正常编译通过
+ System.out.print(i); // 这句编译器会提示“非法向前引用”
+ }
+ static int i = 1;
}
```
-- 与类的构造函数(或者说实例构造器 <init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 <clinit>() 方法运行之前,父类的 <clinit>() 方法已经执行结束。因此虚拟机中第一个执行 <clinit>() 方法的类肯定为 java.lang.Object。
+- 与类的构造函数(或者说实例构造器 <init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 <clinit>() 方法运行之前,父类的 <clinit>() 方法已经执行结束。因此虚拟机中第一个执行 <clinit>() 方法的类肯定为 java.lang.Object。
-- 由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作。例如以下代码:
+- 由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作。例如以下代码:
```java
-static class Parent {
- public static int A = 1;
- static {
- A = 2;
- }
+static class Parent {
+ public static int A = 1;
+ static {
+ A = 2;
+ }
}
-static class Sub extends Parent {
- public static int B = A;
+static class Sub extends Parent {
+ public static int B = A;
}
-public static void main(String[] args) {
- System.out.println(Sub.B); // 输出结果是父类中的静态变量 A 的值,也就是 2。
+public static void main(String[] args) {
+ System.out.println(Sub.B); // 输出结果是父类中的静态变量 A 的值,也就是 2。
}
```
-- <clinit>() 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 <clinit>() 方法。
+- <clinit>() 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 <clinit>() 方法。
-- 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
+- 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
-- 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
+- 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
-## 类加载器
+## 类加载器
-实现类的加载动作。在 Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。
+实现类的加载动作。在 Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。
-### 1. 类与类加载器
+### 1. 类与类加载器
两个类相等:类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
-这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
+这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
-### 2. 类加载器分类
+### 2. 类加载器分类
-从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
+从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
-- 启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;
+- 启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;
-- 所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
+- 所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
-从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
+从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
-- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
+- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
-- 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
+- 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
-- 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
+- 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
-### 3. 双亲委派模型
+### 3. 双亲委派模型
应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。
-下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
+下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
-
+
-**(一)工作过程**
+**(一)工作过程**
一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。
-**(二)好处**
+**(二)好处**
-使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
+使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
-例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 ClassPath 中,程序可以编译通过。因为双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。正因为 rt.jar 中的 Object 优先级更高,因为程序中所有的 Object 都是这个 Object。
+例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 ClassPath 中,程序可以编译通过。因为双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。正因为 rt.jar 中的 Object 优先级更高,因为程序中所有的 Object 都是这个 Object。
-**(三)实现**
+**(三)实现**
-以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。
+以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。
```java
-public abstract class ClassLoader {
- // The parent class loader for delegation
- private final ClassLoader parent;
+public abstract class ClassLoader {
+ // The parent class loader for delegation
+ private final ClassLoader parent;
- public Class> loadClass(String name) throws ClassNotFoundException {
- return loadClass(name, false);
- }
+ public Class> loadClass(String name) throws ClassNotFoundException {
+ return loadClass(name, false);
+ }
- protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
- synchronized (getClassLoadingLock(name)) {
- // First, check if the class has already been loaded
- Class> c = findLoadedClass(name);
- if (c == null) {
- try {
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- // ClassNotFoundException thrown if class not found
- // from the non-null parent class loader
- }
+ protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ synchronized (getClassLoadingLock(name)) {
+ // First, check if the class has already been loaded
+ Class> c = findLoadedClass(name);
+ if (c == null) {
+ try {
+ if (parent != null) {
+ c = parent.loadClass(name, false);
+ } else {
+ c = findBootstrapClassOrNull(name);
+ }
+ } catch (ClassNotFoundException e) {
+ // ClassNotFoundException thrown if class not found
+ // from the non-null parent class loader
+ }
- if (c == null) {
- // If still not found, then invoke findClass in order
- // to find the class.
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
- }
+ if (c == null) {
+ // If still not found, then invoke findClass in order
+ // to find the class.
+ c = findClass(name);
+ }
+ }
+ if (resolve) {
+ resolveClass(c);
+ }
+ return c;
+ }
+ }
- protected Class> findClass(String name) throws ClassNotFoundException {
- throw new ClassNotFoundException(name);
- }
+ protected Class> findClass(String name) throws ClassNotFoundException {
+ throw new ClassNotFoundException(name);
+ }
}
```
-### 4. 自定义类加载器实现
+### 4. 自定义类加载器实现
-FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
+FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
-java.lang.ClassLoader 类的方法 loadClass() 实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,而是通过重写 findClass() 方法。
+java.lang.ClassLoader 类的方法 loadClass() 实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,而是通过重写 findClass() 方法。
```java
-public class FileSystemClassLoader extends ClassLoader {
+public class FileSystemClassLoader extends ClassLoader {
- private String rootDir;
+ private String rootDir;
- public FileSystemClassLoader(String rootDir) {
- this.rootDir = rootDir;
- }
+ public FileSystemClassLoader(String rootDir) {
+ this.rootDir = rootDir;
+ }
- protected Class> findClass(String name) throws ClassNotFoundException {
- byte[] classData = getClassData(name);
- if (classData == null) {
- throw new ClassNotFoundException();
- } else {
- return defineClass(name, classData, 0, classData.length);
- }
- }
+ protected Class> findClass(String name) throws ClassNotFoundException {
+ byte[] classData = getClassData(name);
+ if (classData == null) {
+ throw new ClassNotFoundException();
+ } else {
+ return defineClass(name, classData, 0, classData.length);
+ }
+ }
- private byte[] getClassData(String className) {
- String path = classNameToPath(className);
- try {
- InputStream ins = new FileInputStream(path);
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- int bufferSize = 4096;
- byte[] buffer = new byte[bufferSize];
- int bytesNumRead;
- while ((bytesNumRead = ins.read(buffer)) != -1) {
- baos.write(buffer, 0, bytesNumRead);
- }
- return baos.toByteArray();
- } catch (IOException e) {
- e.printStackTrace();
- }
- return null;
- }
+ private byte[] getClassData(String className) {
+ String path = classNameToPath(className);
+ try {
+ InputStream ins = new FileInputStream(path);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ int bufferSize = 4096;
+ byte[] buffer = new byte[bufferSize];
+ int bytesNumRead;
+ while ((bytesNumRead = ins.read(buffer)) != -1) {
+ baos.write(buffer, 0, bytesNumRead);
+ }
+ return baos.toByteArray();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
- private String classNameToPath(String className) {
- return rootDir + File.separatorChar
- + className.replace('.', File.separatorChar) + ".class";
- }
+ private String classNameToPath(String className) {
+ return rootDir + File.separatorChar
+ + className.replace('.', File.separatorChar) + ".class";
+ }
}
```
-# 四、JVM 参数
+# 四、JVM 参数
-## GC 优化配置
+## GC 优化配置
-| 配置 | 描述 |
-| --- | --- |
-| -Xms | 初始化堆内存大小 |
-| -Xmx | 堆内存最大值 |
-| -Xmn | 新生代大小 |
-| -XX:PermSize | 初始化永久代大小 |
-| -XX:MaxPermSize | 永久代最大容量 |
+| 配置 | 描述 |
+| --- | --- |
+| -Xms | 初始化堆内存大小 |
+| -Xmx | 堆内存最大值 |
+| -Xmn | 新生代大小 |
+| -XX:PermSize | 初始化永久代大小 |
+| -XX:MaxPermSize | 永久代最大容量 |
-## GC 类型设置
+## GC 类型设置
-| 配置 | 描述 |
-| --- | --- |
-| -XX:+UseSerialGC | 串行垃圾回收器 |
-| -XX:+UseParallelGC | 并行垃圾回收器 |
-| -XX:+UseConcMarkSweepGC | 并发标记扫描垃圾回收器 |
-| -XX:ParallelCMSThreads= | 并发标记扫描垃圾回收器 = 为使用的线程数量 |
-| -XX:+UseG1GC | G1 垃圾回收器 |
+| 配置 | 描述 |
+| --- | --- |
+| -XX:+UseSerialGC | 串行垃圾回收器 |
+| -XX:+UseParallelGC | 并行垃圾回收器 |
+| -XX:+UseConcMarkSweepGC | 并发标记扫描垃圾回收器 |
+| -XX:ParallelCMSThreads= | 并发标记扫描垃圾回收器 = 为使用的线程数量 |
+| -XX:+UseG1GC | G1 垃圾回收器 |
```java
-java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar
+java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar
```
-# 参考资料
+# 参考资料
-- 周志明. 深入理解 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)
-- [深入探讨 Java 类加载器](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6)
-- [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap)
-- [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml)
+- 周志明. 深入理解 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)
+- [深入探讨 Java 类加载器](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6)
+- [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap)
+- [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml)
+---bottom---CyC---
+![](index_files/540631a4-6018-40a5-aed7-081e2eeeaeea.png)
+![](index_files/f5757d09-88e7-4bbd-8cfb-cecf55604854.png)
+![](index_files/JNIFigure1.gif)
+![](index_files/ppt_img.gif)
+![](index_files/0635cbe8.png)
+![](index_files/a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg)
+![](index_files/902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg)
+![](index_files/e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg)
+![](index_files/c625baa0-dde6-449e-93df-c3a67f2f430f.jpg)
+![](index_files/22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg)
+![](index_files/81538cd5-1bcf-4e31-86e5-e198df1e013b.jpg)
+![](index_files/08f32fd3-f736-4a67-81ca-295b2a7972f2.jpg)
+![](index_files/278fe431-af88-4a95-a895-9c3b80117de3.jpg)
+![](index_files/62e77997-6957-4b68-8d12-bfd609bb2c68.jpg)
+![](index_files/f99ee771-c56f-47fb-9148-c0036695b5fe.jpg)
+![](index_files/4cf711a8-7ab2-4152-b85c-d5c226733807.png)
+![](index_files/9bbddeeb-e939-41f0-8e8e-2b1a0aa7e0a7.png)
+![](index_files/32b8374a-e822-4720-af0b-c0f485095ea2.jpg)
+![](index_files/class_loader_hierarchy.png)
diff --git a/notes/Leetcode 题解.md b/notes/Leetcode 题解.md
index a4bb7a80..87849ef1 100644
--- a/notes/Leetcode 题解.md
+++ b/notes/Leetcode 题解.md
@@ -1,63 +1,8 @@
-
-* [算法思想](#算法思想)
- * [二分查找](#二分查找)
- * [贪心思想](#贪心思想)
- * [双指针](#双指针)
- * [排序](#排序)
- * [快速选择](#快速选择)
- * [堆排序](#堆排序)
- * [桶排序](#桶排序)
- * [荷兰国旗问题](#荷兰国旗问题)
- * [搜索](#搜索)
- * [BFS](#bfs)
- * [DFS](#dfs)
- * [Backtracking](#backtracking)
- * [分治](#分治)
- * [动态规划](#动态规划)
- * [斐波那契数列](#斐波那契数列)
- * [最长递增子序列](#最长递增子序列)
- * [最长公共子序列](#最长公共子序列)
- * [0-1 背包](#0-1-背包)
- * [数组区间](#数组区间)
- * [字符串编辑](#字符串编辑)
- * [分割整数](#分割整数)
- * [矩阵路径](#矩阵路径)
- * [其它问题](#其它问题)
- * [数学](#数学)
- * [素数](#素数)
- * [最大公约数](#最大公约数)
- * [进制转换](#进制转换)
- * [阶乘](#阶乘)
- * [字符串加法减法](#字符串加法减法)
- * [相遇问题](#相遇问题)
- * [多数投票问题](#多数投票问题)
- * [其它](#其它)
-* [数据结构相关](#数据结构相关)
- * [栈和队列](#栈和队列)
- * [哈希表](#哈希表)
- * [字符串](#字符串)
- * [数组与矩阵](#数组与矩阵)
- * [链表](#链表)
- * [树](#树)
- * [递归](#递归)
- * [层次遍历](#层次遍历)
- * [前中后序遍历](#前中后序遍历)
- * [BST](#bst)
- * [Trie](#trie)
- * [图](#图)
- * [二分图](#二分图)
- * [拓扑排序](#拓扑排序)
- * [并查集](#并查集)
- * [位运算](#位运算)
-* [参考资料](#参考资料)
-
-
-
# 算法思想
## 二分查找
-**正常实现**
+**正常实现**
```java
public int binarySearch(int[] nums, int key) {
@@ -75,11 +20,11 @@ public int binarySearch(int[] nums, int key) {
}
```
-**时间复杂度**
+**时间复杂度**
O(logN)
-**计算 mid**
+**计算 mid**
有两种计算 mid 的方式:
@@ -88,14 +33,14 @@ O(logN)
l + h 可能出现加法溢出,最好使用第二种方式。
-**返回值**
+**返回值**
循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值:
- -1:以一个错误码指示没有查找到 key
- l:将 key 插入到 nums 中的正确位置
-**变种**
+**变种**
二分查找可以有很多变种,变种实现要多注意边界值的判断。例如在一个有重复元素的数组中查找 key 的最左位置的实现如下:
@@ -135,7 +80,7 @@ l m h
当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。
-**求开方**
+**求开方**
[69. Sqrt(x) (Easy)](https://leetcode.com/problems/sqrtx/description/)
@@ -148,7 +93,7 @@ Output: 2
Explanation: The square root of 8 is 2.82842..., and since we want to return an integer, the decimal part will be truncated.
```
-一个数 x 的开方 sqrt 一定在 0 \~ x 之间,并且满足 sqrt == x / sqrt。可以利用二分查找在 0 \~ x 之间查找 sqrt。
+一个数 x 的开方 sqrt 一定在 0 ~ x 之间,并且满足 sqrt == x / sqrt。可以利用二分查找在 0 ~ x 之间查找 sqrt。
对于 x = 8,它的开方是 2.82842...,最后应该返回 2 而不是 3。在循环条件为 l <= h 并且循环退出时,h 总是比 l 小 1,也就是说 h = 2,l = 3,因此最后的返回值应该为 h 而不是 l。
@@ -171,7 +116,7 @@ public int mySqrt(int x) {
}
```
-**大于给定元素的最小元素**
+**大于给定元素的最小元素**
[744. Find Smallest Letter Greater Than Target (Easy)](https://leetcode.com/problems/find-smallest-letter-greater-than-target/description/)
@@ -206,7 +151,7 @@ public char nextGreatestLetter(char[] letters, char target) {
}
```
-**有序数组的 Single Element**
+**有序数组的 Single Element**
[540. Single Element in a Sorted Array (Medium)](https://leetcode.com/problems/single-element-in-a-sorted-array/description/)
@@ -239,7 +184,7 @@ public int singleNonDuplicate(int[] nums) {
}
```
-**第一个错误的版本**
+**第一个错误的版本**
[278. First Bad Version (Easy)](https://leetcode.com/problems/first-bad-version/description/)
@@ -263,7 +208,7 @@ public int firstBadVersion(int n) {
}
```
-**旋转数组的最小数字**
+**旋转数组的最小数字**
[153. Find Minimum in Rotated Sorted Array (Medium)](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/description/)
@@ -286,7 +231,7 @@ public int findMin(int[] nums) {
}
```
-**查找区间**
+**查找区间**
[34. Search for a Range (Medium)](https://leetcode.com/problems/search-for-a-range/description/)
@@ -325,7 +270,7 @@ private int binarySearch(int[] nums, int target) {
贪心思想保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。
-**分配饼干**
+**分配饼干**
[455. Assign Cookies (Easy)](https://leetcode.com/problems/assign-cookies/description/)
@@ -358,7 +303,7 @@ public int findContentChildren(int[] g, int[] s) {
}
```
-**股票的最大收益**
+**股票的最大收益**
[122. Best Time to Buy and Sell Stock II (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/description/)
@@ -377,7 +322,7 @@ public int maxProfit(int[] prices) {
}
```
-**种植花朵**
+**种植花朵**
[605. Can Place Flowers (Easy)](https://leetcode.com/problems/can-place-flowers/description/)
@@ -406,7 +351,7 @@ public boolean canPlaceFlowers(int[] flowerbed, int n) {
}
```
-**修改一个数成为非递减数组**
+**修改一个数成为非递减数组**
[665. Non-decreasing Array (Easy)](https://leetcode.com/problems/non-decreasing-array/description/)
@@ -418,7 +363,7 @@ Explanation: You could modify the first 4 to 1 to get a non-decreasing array.
题目描述:判断一个数组能不能只修改一个数就成为非递减数组。
-在出现 nums[i] < nums[i - 1] 时,需要考虑的是应该修改数组的哪个数,使得本次修改能使 i 之前的数组成为非递减数组,并且 **不影响后续的操作** 。优先考虑令 nums[i - 1] = nums[i],因为如果修改 nums[i] = nums[i - 1] 的话,那么 nums[i] 这个数会变大,就有可能比 nums[i + 1] 大,从而影响了后续操作。还有一个比较特别的情况就是 nums[i] < nums[i - 2],只修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组,只能修改 nums[i] = nums[i - 1]。
+在出现 nums[i] < nums[i - 1] 时,需要考虑的是应该修改数组的哪个数,使得本次修改能使 i 之前的数组成为非递减数组,并且 **不影响后续的操作**。优先考虑令 nums[i - 1] = nums[i],因为如果修改 nums[i] = nums[i - 1] 的话,那么 nums[i] 这个数会变大,就有可能比 nums[i + 1] 大,从而影响了后续操作。还有一个比较特别的情况就是 nums[i] < nums[i - 2],只修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组,只能修改 nums[i] = nums[i - 1]。
```java
public boolean checkPossibility(int[] nums) {
@@ -436,7 +381,7 @@ public boolean checkPossibility(int[] nums) {
}
```
-**判断是否为子串**
+**判断是否为子串**
[392. Is Subsequence (Medium)](https://leetcode.com/problems/is-subsequence/description/)
@@ -457,7 +402,7 @@ public boolean isSubsequence(String s, String t) {
}
```
-**不重叠的区间个数**
+**不重叠的区间个数**
[435. Non-overlapping Intervals (Medium)](https://leetcode.com/problems/non-overlapping-intervals/description/)
@@ -502,7 +447,7 @@ public int eraseOverlapIntervals(Interval[] intervals) {
}
```
-**投飞镖刺破气球**
+**投飞镖刺破气球**
[452. Minimum Number of Arrows to Burst Balloons (Medium)](https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons/description/)
@@ -537,7 +482,7 @@ public int findMinArrowShots(int[][] points) {
```
-**分隔字符串使同种字符出现在一起**
+**分隔字符串使同种字符出现在一起**
[763. Partition Labels (Medium)](https://leetcode.com/problems/partition-labels/description/)
@@ -572,7 +517,7 @@ A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits
}
```
-**根据身高和序号重组队列**
+**根据身高和序号重组队列**
[406. Queue Reconstruction by Height(Medium)](https://leetcode.com/problems/queue-reconstruction-by-height/description/)
@@ -610,7 +555,7 @@ public int[][] reconstructQueue(int[][] people) {
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。
-**有序数组的 Tow Sum**
+**有序数组的 Tow Sum**
[Leetcode :167. Two Sum II - Input array is sorted (Easy)](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/description/)
@@ -641,7 +586,7 @@ public int[] twoSum(int[] numbers, int target) {
}
```
-**两数平方和**
+**两数平方和**
[633. Sum of Square Numbers (Easy)](https://leetcode.com/problems/sum-of-square-numbers/description/)
@@ -669,7 +614,7 @@ public boolean judgeSquareSum(int c) {
}
```
-**反转字符串中的元音字符**
+**反转字符串中的元音字符**
[345. Reverse Vowels of a String (Easy)](https://leetcode.com/problems/reverse-vowels-of-a-string/description/)
@@ -701,7 +646,7 @@ public String reverseVowels(String s) {
}
```
-**回文字符串**
+**回文字符串**
[680. Valid Palindrome II (Easy)](https://leetcode.com/problems/valid-palindrome-ii/description/)
@@ -732,7 +677,7 @@ private boolean isPalindrome(String s, int i, int j) {
}
```
-**归并两个有序数组**
+**归并两个有序数组**
[88. Merge Sorted Array (Easy)](https://leetcode.com/problems/merge-sorted-array/description/)
@@ -765,7 +710,7 @@ public void merge(int[] nums1, int m, int[] nums2, int n) {
}
```
-**判断链表是否存在环**
+**判断链表是否存在环**
[141. Linked List Cycle (Easy)](https://leetcode.com/problems/linked-list-cycle/description/)
@@ -786,7 +731,7 @@ public boolean hasCycle(ListNode head) {
}
```
-**最长子序列**
+**最长子序列**
[524. Longest Word in Dictionary through Deleting (Medium)](https://leetcode.com/problems/longest-word-in-dictionary-through-deleting/description/)
@@ -828,19 +773,19 @@ private boolean isValid(String s, String target) {
### 快速选择
-一般用于求解 **Kth Element** 问题,可以在 O(N) 时间复杂度,O(1) 空间复杂度完成求解工作。
+一般用于求解 **Kth Element** 问题,可以在 O(N) 时间复杂度,O(1) 空间复杂度完成求解工作。
与快速排序一样,快速选择一般需要先打乱数组,否则最坏情况下时间复杂度为 O(N2 )。
### 堆排序
-堆排序用于求解 **TopK Elements** 问题,通过维护一个大小为 K 的堆,堆中的元素就是 TopK Elements。当然它也可以用于求解 Kth Element 问题,堆顶元素就是 Kth Element。快速选择也可以求解 TopK Elements 问题,因为找到 Kth Element 之后,再遍历一次数组,所有小于等于 Kth Element 的元素都是 TopK Elements。可以看到,快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题。
+堆排序用于求解 **TopK Elements** 问题,通过维护一个大小为 K 的堆,堆中的元素就是 TopK Elements。当然它也可以用于求解 Kth Element 问题,堆顶元素就是 Kth Element。快速选择也可以求解 TopK Elements 问题,因为找到 Kth Element 之后,再遍历一次数组,所有小于等于 Kth Element 的元素都是 TopK Elements。可以看到,快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题。
-**Kth Element**
+**Kth Element**
[215. Kth Largest Element in an Array (Medium)](https://leetcode.com/problems/kth-largest-element-in-an-array/description/)
-**排序** :时间复杂度 O(NlogN),空间复杂度 O(1)
+**排序**:时间复杂度 O(NlogN),空间复杂度 O(1)
```java
public int findKthLargest(int[] nums, int k) {
@@ -849,7 +794,7 @@ public int findKthLargest(int[] nums, int k) {
}
```
-**堆排序** :时间复杂度 O(NlogK),空间复杂度 O(K)。
+**堆排序**:时间复杂度 O(NlogK),空间复杂度 O(K)。
```java
public int findKthLargest(int[] nums, int k) {
@@ -863,7 +808,7 @@ public int findKthLargest(int[] nums, int k) {
}
```
-**快速选择** :时间复杂度 O(N),空间复杂度 O(1)
+**快速选择**:时间复杂度 O(N),空间复杂度 O(1)
```java
public int findKthLargest(int[] nums, int k) {
@@ -903,7 +848,7 @@ private void swap(int[] a, int i, int j) {
### 桶排序
-**出现频率最多的 k 个数**
+**出现频率最多的 k 个数**
[347. Top K Frequent Elements (Medium)](https://leetcode.com/problems/top-k-frequent-elements/description/)
@@ -936,7 +881,7 @@ public List topKFrequent(int[] nums, int k) {
}
```
-**按照字符出现次数对字符串排序**
+**按照字符出现次数对字符串排序**
[451. Sort Characters By Frequency (Medium)](https://leetcode.com/problems/sort-characters-by-frequency/description/)
@@ -983,9 +928,9 @@ public String frequencySort(String s) {
它其实是三向切分快速排序的一种变种,在三向切分快速排序中,每次切分都将数组分成三个区间:小于切分元素、等于切分元素、大于切分元素,而该算法是将数组分成三个区间:等于红色、等于白色、等于蓝色。
-
+![](index_files/3b49dd67-2c40-4b81-8ad2-7bbb1fe2fcbd.png)
-**按颜色进行排序**
+**按颜色进行排序**
[75. Sort Colors (Medium)](https://leetcode.com/problems/sort-colors/description/)
@@ -1022,7 +967,7 @@ private void swap(int[] nums, int i, int j) {
### BFS
-
+![](index_files/4ff355cf-9a7f-4468-af43-e5b02038facc.jpg)
广度优先搜索的搜索过程有点像一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。
@@ -1042,14 +987,14 @@ private void swap(int[] nums, int i, int j) {
- 4 -> {}
- 3 -> {}
-可以看到,每一轮遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <=dj 。利用这个结论,可以求解最短路径等 **最优解** 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。
+可以看到,每一轮遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <=dj 。利用这个结论,可以求解最短路径等 **最优解** 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。
在程序实现 BFS 时需要考虑以下问题:
- 队列:用来存储每一轮遍历得到的节点;
- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。
-**计算在网格中从原点到特定点的最短路径长度**
+**计算在网格中从原点到特定点的最短路径长度**
```html
[[1,1,0,1],
@@ -1087,7 +1032,7 @@ public int minPathLength(int[][] grids, int tr, int tc) {
}
```
-**组成整数的最小平方数数量**
+**组成整数的最小平方数数量**
[279. Perfect Squares (Medium)](https://leetcode.com/problems/perfect-squares/description/)
@@ -1143,7 +1088,7 @@ private List generateSquares(int n) {
}
```
-**最短单词路径**
+**最短单词路径**
[127. Word Ladder (Medium)](https://leetcode.com/problems/word-ladder/description/)
@@ -1235,20 +1180,20 @@ private int getShortestPath(List[] graphic, int start, int end) {
### DFS
-
+![](index_files/f7f7e3e5-7dd4-4173-9999-576b9e2ac0a2.png)
广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列存储起来以备下一层遍历的时候再遍历。
而深度优先搜索在得到一个新节点时立马对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。
-从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 **可达性** 问题。
+从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 **可达性** 问题。
在程序实现 DFS 时需要考虑以下问题:
- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。
- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。
-**查找最大的连通面积**
+**查找最大的连通面积**
[695. Max Area of Island (Easy)](https://leetcode.com/problems/max-area-of-island/description/)
@@ -1295,7 +1240,7 @@ private int dfs(int[][] grid, int r, int c) {
}
```
-**矩阵中的连通分量数目**
+**矩阵中的连通分量数目**
[200. Number of Islands (Medium)](https://leetcode.com/problems/number-of-islands/description/)
@@ -1342,7 +1287,7 @@ private void dfs(char[][] grid, int i, int j) {
}
```
-**好友关系的连通分量数目**
+**好友关系的连通分量数目**
[547. Friend Circles (Medium)](https://leetcode.com/problems/friend-circles/description/)
@@ -1382,7 +1327,7 @@ private void dfs(int[][] M, int i, boolean[] hasVisited) {
}
```
-**填充封闭区域**
+**填充封闭区域**
[130. Surrounded Regions (Medium)](https://leetcode.com/problems/surrounded-regions/description/)
@@ -1443,7 +1388,7 @@ private void dfs(char[][] board, int r, int c) {
}
```
-**能到达的太平洋和大西洋的区域**
+**能到达的太平洋和大西洋的区域**
[417. Pacific Atlantic Water Flow (Medium)](https://leetcode.com/problems/pacific-atlantic-water-flow/description/)
@@ -1516,19 +1461,19 @@ private void dfs(int r, int c, boolean[][] canReach) {
Backtracking(回溯)属于 DFS。
-- 普通 DFS 主要用在 **可达性问题** ,这种问题只需要执行到特点的位置然后返回即可。
-- 而 Backtracking 主要用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
+- 普通 DFS 主要用在 **可达性问题**,这种问题只需要执行到特点的位置然后返回即可。
+- 而 Backtracking 主要用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
因为 Backtracking 不是立即就返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题:
- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;
- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。
-**数字键盘组合**
+**数字键盘组合**
[17. Letter Combinations of a Phone Number (Medium)](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/)
-
+![](index_files/a3f34241-bb80-4879-8ec9-dff2d81b514e.jpg)
```html
Input:Digit string "23"
@@ -1560,7 +1505,7 @@ private void combination(StringBuilder prefix, String digits, List ret)
}
```
-**IP 地址划分**
+**IP 地址划分**
[93. Restore IP Addresses(Medium)](https://leetcode.com/problems/restore-ip-addresses/description/)
@@ -1598,7 +1543,7 @@ private void doRestore(int k, StringBuilder path, String s, List address
}
```
-**在矩阵中寻找字符串**
+**在矩阵中寻找字符串**
[79. Word Search (Medium)](https://leetcode.com/problems/word-search/description/)
@@ -1655,7 +1600,7 @@ private boolean backtracking(char[][] board, boolean[][] visited, String word, i
}
```
-**输出二叉树中所有从根到叶子的路径**
+**输出二叉树中所有从根到叶子的路径**
[257. Binary Tree Paths (Easy)](https://leetcode.com/problems/binary-tree-paths/description/)
@@ -1709,7 +1654,7 @@ private String buildPath(List values) {
}
```
-**排列**
+**排列**
[46. Permutations (Medium)](https://leetcode.com/problems/permutations/description/)
@@ -1751,7 +1696,7 @@ private void backtracking(List permuteList, boolean[] visited, int[] nu
}
```
-**含有相同元素求排列**
+**含有相同元素求排列**
[47. Permutations II (Medium)](https://leetcode.com/problems/permutations-ii/description/)
@@ -1794,7 +1739,7 @@ private void backtracking(List permuteList, boolean[] visited, int[] nu
}
```
-**组合**
+**组合**
[77. Combinations (Medium)](https://leetcode.com/problems/combinations/description/)
@@ -1832,7 +1777,7 @@ private void backtracking(int start, int n, int k, List combineList, Li
}
```
-**组合求和**
+**组合求和**
[39. Combination Sum (Medium)](https://leetcode.com/problems/combination-sum/description/)
@@ -1864,7 +1809,7 @@ private void doCombination(int[] candidates, int target, int start, List path, int start, List
+![](index_files/1ca52246-c443-48ae-b1f8-1cafc09ec75c.png)
```java
private boolean[][] rowsUsed = new boolean[9][10];
@@ -2129,11 +2074,11 @@ private int cubeNum(int i, int j) {
}
```
-**N 皇后**
+**N 皇后**
[51. N-Queens (Hard)](https://leetcode.com/problems/n-queens/description/)
-
+![](index_files/1f080e53-4758-406c-bb5f-dbedf89b63ce.jpg)
题目描述:在 n\*n 的矩阵中摆放 n 个皇后,并且每个皇后不能在同一行,同一列,同一对角线上,求所有的 n 皇后的解。
@@ -2141,11 +2086,11 @@ private int cubeNum(int i, int j) {
45 度对角线标记数组的维度为 2 \* n - 1,通过下图可以明确 (r, c) 的位置所在的数组下标为 r + c。
-
+![](index_files/85583359-1b45-45f2-9811-4f7bb9a64db7.jpg)
135 度对角线标记数组的维度也是 2 \* n - 1,(r, c) 的位置所在的数组下标为 n - 1 - (r - c)。
-
+![](index_files/9e80f75a-b12b-4344-80c8-1f9ccc2d5246.jpg)
```java
private List> ret;
@@ -2193,7 +2138,7 @@ private void backtracking(int row) {
## 分治
-**给表达式加括号**
+**给表达式加括号**
[241. Different Ways to Add Parentheses (Medium)](https://leetcode.com/problems/different-ways-to-add-parentheses/description/)
@@ -2245,7 +2190,7 @@ public List diffWaysToCompute(String input) {
### 斐波那契数列
-**爬楼梯**
+**爬楼梯**
[70. Climbing Stairs (Easy)](https://leetcode.com/problems/climbing-stairs/description/)
@@ -2253,7 +2198,7 @@ public List diffWaysToCompute(String input) {
定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。
-
+$$dp[i] = dp[i-1] + dp[i-2]$$
dp[N] 即为所求。
@@ -2272,7 +2217,7 @@ public int climbStairs(int n) {
}
```
-**母牛生产**
+**母牛生产**
[程序员代码面试指南-P181](#)
@@ -2280,9 +2225,9 @@ public int climbStairs(int n) {
第 i 年成熟的牛的数量为:
-
+$$dp[i] = dp[i-1] + dp[i-3]$$
-**强盗抢劫**
+**强盗抢劫**
[198. House Robber (Easy)](https://leetcode.com/problems/house-robber/description/)
@@ -2290,7 +2235,7 @@ public int climbStairs(int n) {
定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 和 i - 3 的住户,所以
-
+$$dp[i] = max(dp[i - 2], dp[i - 3]) + nums[i]$$
O(n) 空间复杂度实现方法:
@@ -2330,7 +2275,7 @@ public int rob(int[] nums) {
}
```
-**强盗在环形街区抢劫**
+**强盗在环形街区抢劫**
[213. House Robber II (Medium)](https://leetcode.com/problems/house-robber-ii/description/)
@@ -2358,7 +2303,7 @@ private int rob(int[] nums, int first, int last) {
}
```
-**信件错排**
+**信件错排**
题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。
@@ -2369,7 +2314,7 @@ private int rob(int[] nums, int first, int last) {
综上所述,错误装信数量方式数量为:
-
+$$ dp[i] = (i-1) * dp[i-2] + (i-1) * dp[i-1]$$
dp[N] 即为所求。
@@ -2377,19 +2322,19 @@ dp[N] 即为所求。
### 最长递增子序列
-已知一个序列 {S1 , S2 ,...,Sn } ,取出若干数组成新的序列 {Si1 , Si2 ,..., Sim },其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 **子序列** 。
+已知一个序列 {S1 , S2 ,...,Sn } ,取出若干数组成新的序列 {Si1 , Si2 ,..., Sim },其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个**子序列**。
-如果在子序列中,当下标 ix > iy 时,Six > Siy ,称子序列为原序列的一个 **递增子序列** 。
+如果在子序列中,当下标 ix > iy 时,Six > Siy ,称子序列为原序列的一个**递增子序列**。
定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1 , Si2 ,...,Sim },如果 im < n 并且 Sim < Sn ,此时 {Si1 , Si2 ,..., Sim , Sn } 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。
因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn } 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即:
-
+$$ dp[n] = max\{ 1, dp[i]+1 | S_i < S_n \&\& i < n \} $$
对于一个长度为 N 的序列,最长递增子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,即 max{ dp[i] | 1 <= i <= N} 即为所求。
-**最长递增子序列**
+**最长递增子序列**
[300. Longest Increasing Subsequence (Medium)](https://leetcode.com/problems/longest-increasing-subsequence/description/)
@@ -2454,7 +2399,7 @@ private int binarySearch(int[] nums, int first, int last, int key) {
}
```
-**一组整数对能够构成的最长链**
+**一组整数对能够构成的最长链**
[646. Maximum Length of Pair Chain (Medium)](https://leetcode.com/problems/maximum-length-of-pair-chain/description/)
@@ -2491,7 +2436,7 @@ public int findLongestChain(int[][] pairs) {
}
```
-**最长摆动子序列**
+**最长摆动子序列**
[376. Wiggle Subsequence (Medium)](https://leetcode.com/problems/wiggle-subsequence/description/)
@@ -2536,7 +2481,7 @@ public int wiggleMaxLength(int[] nums) {
综上,最长公共子序列的状态转移方程为:
-
+$$ dp[i][j]=\left\{ \begin{array}{rcl} dp[i-1][j-1] && { S1_i==S2_j }\\ max(dp[i-1][j], dp[i][j-1]) &&{ S1_i <> S2_j } \end{array} \right. $$
对于长度为 N 的序列 S1 和 长度为 M 的序列 S2 ,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。
@@ -2573,7 +2518,7 @@ public int lengthOfLCS(int[] nums1, int[] nums2) {
综上,0-1 背包的状态转移方程为:
-
+$$ dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v) $$
```java
public int knapsack(int W, int N, int[] weights, int[] values) {
@@ -2592,11 +2537,11 @@ public int knapsack(int W, int N, int[] weights, int[] values) {
}
```
-**空间优化**
+**空间优化**
在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅由前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时,
-
+$$ dp[j] = max ( dp[j], dp[j-w] + v ) $$
因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。
@@ -2615,7 +2560,7 @@ public int knapsack(int W, int N, int[] weights, int[] values) {
}
```
-**无法使用贪心算法的解释**
+**无法使用贪心算法的解释**
0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。考虑下面的物品和一个容量为 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的价值为 16,浪费了大小为 2 的空间。最优的方式是存放物品 1 和物品 2,价值为 22.
@@ -2625,7 +2570,7 @@ public int knapsack(int W, int N, int[] weights, int[] values) {
| 1 | 2 | 10 | 5 |
| 2 | 3 | 12 | 4 |
-**变种**
+**变种**
- 完全背包:物品数量为无限个
@@ -2635,7 +2580,7 @@ public int knapsack(int W, int N, int[] weights, int[] values) {
- 其它:物品之间相互约束或者依赖
-**划分数组为和相等的两部分**
+**划分数组为和相等的两部分**
[416. Partition Equal Subset Sum (Medium)](https://leetcode.com/problems/partition-equal-subset-sum/description/)
@@ -2668,7 +2613,7 @@ Explanation: The array can be partitioned as [1, 5, 5] and [11].
}
```
-**字符串按单词列表分割**
+**字符串按单词列表分割**
[139. Word Break (Medium)](https://leetcode.com/problems/word-break/description/)
@@ -2699,7 +2644,7 @@ public boolean wordBreak(String s, List wordDict) {
}
```
-**改变一组数的正负号使得它们的和为一给定数**
+**改变一组数的正负号使得它们的和为一给定数**
[494. Target Sum (Medium)](https://leetcode.com/problems/target-sum/description/)
@@ -2761,7 +2706,7 @@ private int findTargetSumWays(int[] nums, int start, int S) {
}
```
-**01 字符构成最多的字符串**
+**01 字符构成最多的字符串**
[474. Ones and Zeroes (Medium)](https://leetcode.com/problems/ones-and-zeroes/description/)
@@ -2794,7 +2739,7 @@ public int findMaxForm(String[] strs, int m, int n) {
}
```
-**找零钱**
+**找零钱**
[322. Coin Change (Medium)](https://leetcode.com/problems/coin-change/description/)
@@ -2829,7 +2774,7 @@ public int coinChange(int[] coins, int amount) {
}
```
-**组合总和**
+**组合总和**
[377. Combination Sum IV (Medium)](https://leetcode.com/problems/combination-sum-iv/description/)
@@ -2869,7 +2814,7 @@ public int combinationSum4(int[] nums, int target) {
}
```
-**只能进行 k 次的股票交易**
+**只能进行 k 次的股票交易**
[188. Best Time to Buy and Sell Stock IV (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/description/)
@@ -2901,7 +2846,7 @@ public int maxProfit(int k, int[] prices) {
}
```
-**只能进行两次的股票交易**
+**只能进行两次的股票交易**
[123. Best Time to Buy and Sell Stock III (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/description/)
@@ -2921,7 +2866,7 @@ public int maxProfit(int[] prices) {
### 数组区间
-**数组区间和**
+**数组区间和**
[303. Range Sum Query - Immutable (Easy)](https://leetcode.com/problems/range-sum-query-immutable/description/)
@@ -2933,7 +2878,7 @@ sumRange(2, 5) -> -1
sumRange(0, 5) -> -3
```
-求区间 i \~ j 的和,可以转换为 sum[j] - sum[i-1],其中 sum[i] 为 0 \~ i 的和。
+求区间 i ~ j 的和,可以转换为 sum[j] - sum[i-1],其中 sum[i] 为 0 ~ i 的和。
```java
class NumArray {
@@ -2952,7 +2897,7 @@ class NumArray {
}
```
-**子数组最大的和**
+**子数组最大的和**
[53. Maximum Subarray (Easy)](https://leetcode.com/problems/maximum-subarray/description/)
@@ -2974,7 +2919,7 @@ public int maxSubArray(int[] nums) {
}
```
-**数组中等差递增子区间的个数**
+**数组中等差递增子区间的个数**
[413. Arithmetic Slices (Medium)](https://leetcode.com/problems/arithmetic-slices/description/)
@@ -3005,7 +2950,7 @@ public int numberOfArithmeticSlices(int[] A) {
### 字符串编辑
-**删除两个字符串的字符使它们相等**
+**删除两个字符串的字符使它们相等**
[583. Delete Operation for Two Strings (Medium)](https://leetcode.com/problems/delete-operation-for-two-strings/description/)
@@ -3032,7 +2977,7 @@ public int minDistance(String word1, String word2) {
}
```
-**修改一个字符串成为另一个字符串**
+**修改一个字符串成为另一个字符串**
[72. Edit Distance (Hard)](https://leetcode.com/problems/edit-distance/description/)
@@ -3085,7 +3030,7 @@ public int minDistance(String word1, String word2) {
### 分割整数
-**分割整数的最大乘积**
+**分割整数的最大乘积**
[343. Integer Break (Medim)](https://leetcode.com/problems/integer-break/description/)
@@ -3104,7 +3049,7 @@ public int integerBreak(int n) {
}
```
-**按平方数来分割整数**
+**按平方数来分割整数**
[279. Perfect Squares(Medium)](https://leetcode.com/problems/perfect-squares/description/)
@@ -3138,7 +3083,7 @@ private List generateSquareList(int n) {
}
```
-**分割整数构成字母字符串**
+**分割整数构成字母字符串**
[91. Decode Ways (Medium)](https://leetcode.com/problems/decode-ways/description/)
@@ -3164,13 +3109,13 @@ public int numDecodings(String s) {
### 矩阵路径
-**矩阵的总路径数**
+**矩阵的总路径数**
[62. Unique Paths (Medium)](https://leetcode.com/problems/unique-paths/description/)
题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向右或者向下移动。
-
+![](index_files/7c98e1b6-c446-4cde-8513-5c11b9f52aea.jpg)
```java
public int uniquePaths(int m, int n) {
@@ -3199,7 +3144,7 @@ public int uniquePaths(int m, int n) {
}
```
-**矩阵的最小路径和**
+**矩阵的最小路径和**
[64. Minimum Path Sum (Medium)](https://leetcode.com/problems/minimum-path-sum/description/)
@@ -3230,13 +3175,13 @@ public int minPathSum(int[][] grid) {
### 其它问题
-**需要冷却期的股票交易**
+**需要冷却期的股票交易**
[309. Best Time to Buy and Sell Stock with Cooldown(Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/)
题目描述:交易之后需要有一天的冷却时间。
-
+![](index_files/a3da4342-078b-43e2-b748-7e71bec50dc4.png)
```java
public int maxProfit(int[] prices) {
@@ -3258,7 +3203,7 @@ public int maxProfit(int[] prices) {
}
```
-**需要交易费用的股票交易**
+**需要交易费用的股票交易**
[714. Best Time to Buy and Sell Stock with Transaction Fee (Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/)
@@ -3275,7 +3220,7 @@ The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
题目描述:每交易一次,都要支付一定的费用。
-
+![](index_files/61942711-45a0-4e11-bbc9-434e31436f33.png)
```java
public int maxProfit(int[] prices, int fee) {
@@ -3296,7 +3241,7 @@ public int maxProfit(int[] prices, int fee) {
}
```
-**买入和售出股票最大的收益**
+**买入和售出股票最大的收益**
[121. Best Time to Buy and Sell Stock (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/)
@@ -3318,7 +3263,7 @@ public int maxProfit(int[] prices) {
}
```
-**复制粘贴字符**
+**复制粘贴字符**
[650. 2 Keys Keyboard (Medium)](https://leetcode.com/problems/2-keys-keyboard/description/)
@@ -3364,11 +3309,11 @@ public int minSteps(int n) {
### 素数
-**素数分解**
+**素数分解**
每一个数都可以分解成素数的乘积,例如 84 = 22 \* 31 \* 50 \* 71 \* 110 \* 130 \* 170 \* …
-**整除**
+**整除**
令 x = 2m0 \* 3m1 \* 5m2 \* 7m3 \* 11m4 \* …
@@ -3376,13 +3321,13 @@ public int minSteps(int n) {
如果 x 整除 y(y mod x == 0),则对于所有 i,mi <= ni。
-**最大公约数最小公倍数**
+**最大公约数最小公倍数**
x 和 y 的最大公约数为:gcd(x,y) = 2min(m0,n0) \* 3min(m1,n1) \* 5min(m2,n2) \* ...
x 和 y 的最小公倍数为:lcm(x,y) = 2max(m0,n0) \* 3max(m1,n1) \* 5max(m2,n2) \* ...
-**生成素数序列**
+**生成素数序列**
[204. Count Primes (Easy)](https://leetcode.com/problems/count-primes/description/)
@@ -3435,7 +3380,7 @@ int lcm(int a, int b){
### 进制转换
-**7 进制**
+**7 进制**
[504. Base 7 (Easy)](https://leetcode.com/problems/base-7/description/)
@@ -3470,7 +3415,7 @@ public String convertToBase7(int num) {
}
```
-**16 进制**
+**16 进制**
[405. Convert a Number to Hexadecimal (Easy)](https://leetcode.com/problems/convert-a-number-to-hexadecimal/description/)
@@ -3503,7 +3448,7 @@ public String toHex(int num) {
}
```
-**26 进制**
+**26 进制**
[168. Excel Sheet Column Title (Easy)](https://leetcode.com/problems/excel-sheet-column-title/description/)
@@ -3529,7 +3474,7 @@ public String convertToTitle(int n) {
### 阶乘
-**统计阶乘尾部有多少个 0**
+**统计阶乘尾部有多少个 0**
[172. Factorial Trailing Zeroes (Easy)](https://leetcode.com/problems/factorial-trailing-zeroes/description/)
@@ -3547,7 +3492,7 @@ public int trailingZeroes(int n) {
### 字符串加法减法
-**二进制加法**
+**二进制加法**
[67. Add Binary (Easy)](https://leetcode.com/problems/add-binary/description/)
@@ -3571,7 +3516,7 @@ public String addBinary(String a, String b) {
}
```
-**字符串加法**
+**字符串加法**
[415. Add Strings (Easy)](https://leetcode.com/problems/add-strings/description/)
@@ -3593,7 +3538,7 @@ public String addStrings(String num1, String num2) {
### 相遇问题
-**改变数组元素使所有的数组元素都相等**
+**改变数组元素使所有的数组元素都相等**
[462. Minimum Moves to Equal Array Elements II (Medium)](https://leetcode.com/problems/minimum-moves-to-equal-array-elements-ii/description/)
@@ -3618,7 +3563,7 @@ Only two moves are needed (remember each move increments or decrements one eleme
设数组长度为 N,则可以找到 N/2 对 a 和 b 的组合,使它们都移动到 m 的位置。
-**解法 1**
+**解法 1**
先排序,时间复杂度:O(NlogN)
@@ -3636,7 +3581,7 @@ public int minMoves2(int[] nums) {
}
```
-**解法 2**
+**解法 2**
使用快速选择找到中位数,时间复杂度 O(N)
@@ -3682,7 +3627,7 @@ private void swap(int[] nums, int i, int j) {
### 多数投票问题
-**数组中出现次数多于 n / 2 的元素**
+**数组中出现次数多于 n / 2 的元素**
[169. Majority Element (Easy)](https://leetcode.com/problems/majority-element/description/)
@@ -3710,7 +3655,7 @@ public int majorityElement(int[] nums) {
### 其它
-**平方数**
+**平方数**
[367. Valid Perfect Square (Easy)](https://leetcode.com/problems/valid-perfect-square/description/)
@@ -3736,7 +3681,7 @@ public boolean isPerfectSquare(int num) {
}
```
-**3 的 n 次方**
+**3 的 n 次方**
[326. Power of Three (Easy)](https://leetcode.com/problems/power-of-three/description/)
@@ -3746,7 +3691,7 @@ public boolean isPowerOfThree(int n) {
}
```
-**乘积数组**
+**乘积数组**
[238. Product of Array Except Self (Medium)](https://leetcode.com/problems/product-of-array-except-self/description/)
@@ -3777,7 +3722,7 @@ public int[] productExceptSelf(int[] nums) {
}
```
-**找出数组中的乘积最大的三个数**
+**找出数组中的乘积最大的三个数**
[628. Maximum Product of Three Numbers (Easy)](https://leetcode.com/problems/maximum-product-of-three-numbers/description/)
@@ -3816,7 +3761,7 @@ public int maximumProduct(int[] nums) {
## 栈和队列
-**用栈实现队列**
+**用栈实现队列**
[232. Implement Queue using Stacks (Easy)](https://leetcode.com/problems/implement-queue-using-stacks/description/)
@@ -3887,7 +3832,7 @@ class MyQueue {
}
```
-**用队列实现栈**
+**用队列实现栈**
[225. Implement Stack using Queues (Easy)](https://leetcode.com/problems/implement-stack-using-queues/description/)
@@ -3922,7 +3867,7 @@ class MyStack {
}
```
-**最小值栈**
+**最小值栈**
[155. Min Stack (Easy)](https://leetcode.com/problems/min-stack/description/)
@@ -3965,7 +3910,7 @@ class MinStack {
对于实现最小值队列问题,可以先将队列使用栈来实现,然后就将问题转换为最小值栈,这个问题出现在 编程之美:3.7。
-**用栈实现括号匹配**
+**用栈实现括号匹配**
[20. Valid Parentheses (Easy)](https://leetcode.com/problems/valid-parentheses/description/)
@@ -3993,7 +3938,7 @@ public boolean isValid(String s) {
}
```
-**数组中元素与下一个比它大的元素之间的距离**
+**数组中元素与下一个比它大的元素之间的距离**
```html
Input: [73, 74, 75, 71, 69, 72, 76, 73]
@@ -4020,7 +3965,7 @@ public int[] dailyTemperatures(int[] temperatures) {
}
```
-**在另一个数组中比当前元素大的下一个元素**
+**在另一个数组中比当前元素大的下一个元素**
[496. Next Greater Element I (Easy)](https://leetcode.com/problems/next-greater-element-i/description/)
@@ -4048,7 +3993,7 @@ public int[] nextGreaterElement(int[] nums1, int[] nums2) {
}
```
-**循环数组中比当前元素大的下一个元素**
+**循环数组中比当前元素大的下一个元素**
[503. Next Greater Element II (Medium)](https://leetcode.com/problems/next-greater-element-ii/description/)
@@ -4071,17 +4016,17 @@ public int[] nextGreaterElements(int[] nums) {
利用 Hash Table 可以快速查找一个元素是否存在等问题,但是需要一定的空间来存储。在优先考虑时间复杂度的情况下,可以利用 Hash Table 这种空间换取时间的做法。
-Java 中的 **HashSet** 用于存储一个集合,并以 O(1) 的时间复杂度查找元素是否在集合中。
+Java 中的 **HashSet** 用于存储一个集合,并以 O(1) 的时间复杂度查找元素是否在集合中。
如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在,例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。
-Java 中的 **HashMap** 主要用于映射关系,从而把两个元素联系起来。
+Java 中的 **HashMap** 主要用于映射关系,从而把两个元素联系起来。
在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中[Leetcdoe : 535. Encode and Decode TinyURL (Medium)](https://leetcode.com/problems/encode-and-decode-tinyurl/description/),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。
HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。
-**数组中的两个数和为给定值**
+**数组中的两个数和为给定值**
[1. Two Sum (Easy)](https://leetcode.com/problems/two-sum/description/)
@@ -4100,7 +4045,7 @@ public int[] twoSum(int[] nums, int target) {
}
```
-**判断数组是否含有相同元素**
+**判断数组是否含有相同元素**
[217. Contains Duplicate (Easy)](https://leetcode.com/problems/contains-duplicate/description/)
@@ -4114,7 +4059,7 @@ public boolean containsDuplicate(int[] nums) {
}
```
-**最长和谐序列**
+**最长和谐序列**
[594. Longest Harmonious Subsequence (Easy)](https://leetcode.com/problems/longest-harmonious-subsequence/description/)
@@ -4142,7 +4087,7 @@ public int findLHS(int[] nums) {
}
```
-**最长连续序列**
+**最长连续序列**
[128. Longest Consecutive Sequence (Hard)](https://leetcode.com/problems/longest-consecutive-sequence/description/)
@@ -4185,7 +4130,7 @@ private int count(Map numCnts, int num) {
## 字符串
-**两个字符串包含的字符是否完全相同**
+**两个字符串包含的字符是否完全相同**
[242. Valid Anagram (Easy)](https://leetcode.com/problems/valid-anagram/description/)
@@ -4206,7 +4151,7 @@ public boolean isAnagram(String s, String t) {
}
```
-**计算一组字符集合可以组成的回文字符串的最大长度**
+**计算一组字符集合可以组成的回文字符串的最大长度**
[409. Longest Palindrome (Easy)](https://leetcode.com/problems/longest-palindrome/description/)
@@ -4229,7 +4174,7 @@ public int longestPalindrome(String s) {
}
```
-**字符串同构**
+**字符串同构**
[205. Isomorphic Strings (Easy)](https://leetcode.com/problems/isomorphic-strings/description/)
@@ -4255,7 +4200,7 @@ public boolean isIsomorphic(String s, String t) {
}
```
-**判断一个整数是否是回文数**
+**判断一个整数是否是回文数**
[9. Palindrome Number (Easy)](https://leetcode.com/problems/palindrome-number/description/)
@@ -4277,7 +4222,7 @@ public boolean isPalindrome(int x) {
}
```
-**回文子字符串**
+**回文子字符串**
[647. Palindromic Substrings (Medium)](https://leetcode.com/problems/palindromic-substrings/description/)
@@ -4308,7 +4253,7 @@ private void extendSubstrings(String s, int start, int end) {
}
```
-**统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数**
+**统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数**
[696. Count Binary Substrings (Easy)](https://leetcode.com/problems/count-binary-substrings/description/)
@@ -4333,7 +4278,7 @@ public int countBinarySubstrings(String s) {
}
```
-**字符串循环移位包含**
+**字符串循环移位包含**
[编程之美:3.1](#)
@@ -4346,7 +4291,7 @@ Return : true
s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2 是否是 s1s1 的子字符串即可。
-**字符串循环移位**
+**字符串循环移位**
[编程之美:2.17](#)
@@ -4356,7 +4301,7 @@ s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2
将 abcd123 中的 abcd 和 123 单独逆序,得到 dcba321,然后对整个字符串进行逆序,得到 123abcd。
-**字符串中单词的翻转**
+**字符串中单词的翻转**
[程序员代码面试指南](#)
@@ -4366,7 +4311,7 @@ s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2
## 数组与矩阵
-**把数组中的 0 移到末尾**
+**把数组中的 0 移到末尾**
[283. Move Zeroes (Easy)](https://leetcode.com/problems/move-zeroes/description/)
@@ -4382,7 +4327,7 @@ public void moveZeroes(int[] nums) {
}
```
-**调整矩阵**
+**调整矩阵**
[566. Reshape the Matrix (Easy)](https://leetcode.com/problems/reshape-the-matrix/description/)
@@ -4414,7 +4359,7 @@ public int[][] matrixReshape(int[][] nums, int r, int c) {
}
```
-**找出数组中最长的连续 1**
+**找出数组中最长的连续 1**
[485. Max Consecutive Ones (Easy)](https://leetcode.com/problems/max-consecutive-ones/description/)
@@ -4429,7 +4374,7 @@ public int findMaxConsecutiveOnes(int[] nums) {
}
```
-**一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出丢失的数和重复的数**
+**一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出丢失的数和重复的数**
[645. Set Mismatch (Easy)](https://leetcode.com/problems/set-mismatch/description/)
@@ -4471,7 +4416,7 @@ private void swap(int[] nums, int i, int j) {
- [448. Find All Numbers Disappeared in an Array (Easy)](https://leetcode.com/problems/find-all-numbers-disappeared-in-an-array/description/),寻找所有丢失的元素
- [442. Find All Duplicates in an Array (Medium)](https://leetcode.com/problems/find-all-duplicates-in-an-array/description/),寻找所有重复的元素。
-**找出数组中重复的数,数组值在 [1, n] 之间**
+**找出数组中重复的数,数组值在 [1, n] 之间**
[287. Find the Duplicate Number (Medium)](https://leetcode.com/problems/find-the-duplicate-number/description/)
@@ -4513,7 +4458,7 @@ public int findDuplicate(int[] nums) {
}
```
-**有序矩阵查找**
+**有序矩阵查找**
[240. Search a 2D Matrix II (Medium)](https://leetcode.com/problems/search-a-2d-matrix-ii/description/)
@@ -4539,7 +4484,7 @@ public boolean searchMatrix(int[][] matrix, int target) {
}
```
-**有序矩阵的 Kth Element**
+**有序矩阵的 Kth Element**
[378. Kth Smallest Element in a Sorted Matrix ((Medium))](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/description/)
@@ -4605,7 +4550,7 @@ class Tuple implements Comparable {
}
```
-**数组相邻差值的个数**
+**数组相邻差值的个数**
[667. Beautiful Arrangement II (Medium)](https://leetcode.com/problems/beautiful-arrangement-ii/description/)
@@ -4615,7 +4560,7 @@ Output: [1, 3, 2]
Explanation: The [1, 3, 2] has three different positive integers ranging from 1 to 3, and the [2, 1] has exactly 2 distinct integers: 1 and 2.
```
-题目描述:数组元素为 1\~n 的整数,要求构建数组,使得相邻元素的差值不相同的个数为 k。
+题目描述:数组元素为 1~n 的整数,要求构建数组,使得相邻元素的差值不相同的个数为 k。
让前 k+1 个元素构建出 k 个不相同的差值,序列为:1 k+1 2 k 3 k-1 ... k/2 k/2+1.
@@ -4633,7 +4578,7 @@ public int[] constructArray(int n, int k) {
}
```
-**数组的度**
+**数组的度**
[697. Degree of an Array (Easy)](https://leetcode.com/problems/degree-of-an-array/description/)
@@ -4672,7 +4617,7 @@ public int findShortestSubArray(int[] nums) {
}
```
-**对角元素相等的矩阵**
+**对角元素相等的矩阵**
[766. Toeplitz Matrix (Easy)](https://leetcode.com/problems/toeplitz-matrix/description/)
@@ -4710,7 +4655,7 @@ private boolean check(int[][] matrix, int expectValue, int row, int col) {
}
```
-**嵌套数组**
+**嵌套数组**
[565. Array Nesting (Medium)](https://leetcode.com/problems/array-nesting/description/)
@@ -4744,7 +4689,7 @@ public int arrayNesting(int[] nums) {
}
```
-**分隔数组**
+**分隔数组**
[769. Max Chunks To Make Sorted (Medium)](https://leetcode.com/problems/max-chunks-to-make-sorted/description/)
@@ -4775,7 +4720,7 @@ public int maxChunksToSorted(int[] arr) {
链表是空节点,或者有一个值和一个指向下一个链表的指针,因此很多链表问题可以用递归来处理。
-**找出两个链表的交点**
+**找出两个链表的交点**
[160. Intersection of Two Linked Lists (Easy)](https://leetcode.com/problems/intersection-of-two-linked-lists/description/)
@@ -4806,7 +4751,7 @@ public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
如果只是判断是否存在交点,那么就是另一个问题,即 [编程之美:3.6]() 的问题。有两种解法:把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环;或者直接比较第一个链表最后一个节点和第二个链表最后一个节点是否相同。
-**链表反转**
+**链表反转**
[206. Reverse Linked List (Easy)](https://leetcode.com/problems/reverse-linked-list/description/)
@@ -4838,7 +4783,7 @@ public ListNode reverseList(ListNode head) {
}
```
-**归并两个有序的链表**
+**归并两个有序的链表**
[21. Merge Two Sorted Lists (Easy)](https://leetcode.com/problems/merge-two-sorted-lists/description/)
@@ -4856,7 +4801,7 @@ public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
}
```
-**从有序链表中删除重复节点**
+**从有序链表中删除重复节点**
[83. Remove Duplicates from Sorted List (Easy)](https://leetcode.com/problems/remove-duplicates-from-sorted-list/description/)
@@ -4873,7 +4818,7 @@ public ListNode deleteDuplicates(ListNode head) {
}
```
-**删除链表的倒数第 n 个节点**
+**删除链表的倒数第 n 个节点**
[19. Remove Nth Node From End of List (Medium)](https://leetcode.com/problems/remove-nth-node-from-end-of-list/description/)
@@ -4900,7 +4845,7 @@ public ListNode removeNthFromEnd(ListNode head, int n) {
}
```
-**交换链表中的相邻结点**
+**交换链表中的相邻结点**
[24. Swap Nodes in Pairs (Medium)](https://leetcode.com/problems/swap-nodes-in-pairs/description/)
@@ -4927,7 +4872,7 @@ public ListNode swapPairs(ListNode head) {
}
```
-**链表求和**
+**链表求和**
[445. Add Two Numbers II (Medium)](https://leetcode.com/problems/add-two-numbers-ii/description/)
@@ -4966,7 +4911,7 @@ private Stack buildStack(ListNode l) {
}
```
-**回文链表**
+**回文链表**
[234. Palindrome Linked List (Easy)](https://leetcode.com/problems/palindrome-linked-list/description/)
@@ -5019,7 +4964,7 @@ private boolean isEqual(ListNode l1, ListNode l2) {
}
```
-**链表元素按奇偶聚集**
+**链表元素按奇偶聚集**
[328. Odd Even Linked List (Medium)](https://leetcode.com/problems/odd-even-linked-list/description/)
@@ -5046,7 +4991,7 @@ public ListNode oddEvenList(ListNode head) {
}
```
-**分隔链表**
+**分隔链表**
[725. Split Linked List in Parts(Medium)](https://leetcode.com/problems/split-linked-list-in-parts/description/)
@@ -5092,7 +5037,7 @@ public ListNode[] splitListToParts(ListNode root, int k) {
一棵树要么是空树,要么有两个指针,每个指针指向一棵树。树是一种递归结构,很多树的问题可以使用递归来处理。
-**树的高度**
+**树的高度**
[104. Maximum Depth of Binary Tree (Easy)](https://leetcode.com/problems/maximum-depth-of-binary-tree/description/)
@@ -5103,7 +5048,7 @@ public int maxDepth(TreeNode root) {
}
```
-**翻转树**
+**翻转树**
[226. Invert Binary Tree (Easy)](https://leetcode.com/problems/invert-binary-tree/description/)
@@ -5117,7 +5062,7 @@ public TreeNode invertTree(TreeNode root) {
}
```
-**归并两棵树**
+**归并两棵树**
[617. Merge Two Binary Trees (Easy)](https://leetcode.com/problems/merge-two-binary-trees/description/)
@@ -5150,7 +5095,7 @@ public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
}
```
-**判断路径和是否等于一个数**
+**判断路径和是否等于一个数**
[Leetcdoe : 112. Path Sum (Easy)](https://leetcode.com/problems/path-sum/description/)
@@ -5176,7 +5121,7 @@ public boolean hasPathSum(TreeNode root, int sum) {
}
```
-**统计路径和等于一个数的路径数量**
+**统计路径和等于一个数的路径数量**
[437. Path Sum III (Easy)](https://leetcode.com/problems/path-sum-iii/description/)
@@ -5216,7 +5161,7 @@ private int pathSumStartWithRoot(TreeNode root, int sum){
}
```
-**子树**
+**子树**
[572. Subtree of Another Tree (Easy)](https://leetcode.com/problems/subtree-of-another-tree/description/)
@@ -5263,7 +5208,7 @@ private boolean isSubtreeWithRoot(TreeNode s, TreeNode t) {
}
```
-**树的对称**
+**树的对称**
[101. Symmetric Tree (Easy)](https://leetcode.com/problems/symmetric-tree/description/)
@@ -5289,7 +5234,7 @@ private boolean isSymmetric(TreeNode t1, TreeNode t2){
}
```
-**平衡树**
+**平衡树**
[110. Balanced Binary Tree (Easy)](https://leetcode.com/problems/balanced-binary-tree/description/)
@@ -5320,7 +5265,7 @@ public int maxDepth(TreeNode root) {
}
```
-**最小路径**
+**最小路径**
[111. Minimum Depth of Binary Tree (Easy)](https://leetcode.com/problems/minimum-depth-of-binary-tree/description/)
@@ -5336,7 +5281,7 @@ public int minDepth(TreeNode root) {
}
```
-**统计左叶子节点的和**
+**统计左叶子节点的和**
[404. Sum of Left Leaves (Easy)](https://leetcode.com/problems/sum-of-left-leaves/description/)
@@ -5363,7 +5308,7 @@ private boolean isLeaf(TreeNode node){
}
```
-**修剪二叉查找树**
+**修剪二叉查找树**
[669. Trim a Binary Search Tree (Easy)](https://leetcode.com/problems/trim-a-binary-search-tree/description/)
@@ -5390,7 +5335,7 @@ Output:
二叉查找树(BST):根节点大于等于左子树所有节点,小于等于右子树所有节点。
-只保留值在 L \~ R 之间的节点
+只保留值在 L ~ R 之间的节点
```java
public TreeNode trimBST(TreeNode root, int L, int R) {
@@ -5403,7 +5348,7 @@ public TreeNode trimBST(TreeNode root, int L, int R) {
}
```
-**从有序数组中构造二叉查找树**
+**从有序数组中构造二叉查找树**
[108. Convert Sorted Array to Binary Search Tree (Easy)](https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/description/)
@@ -5422,7 +5367,7 @@ private TreeNode toBST(int[] nums, int sIdx, int eIdx){
}
```
-**两节点的最长路径**
+**两节点的最长路径**
[543. Diameter of Binary Tree (Easy)](https://leetcode.com/problems/diameter-of-binary-tree/description/)
@@ -5454,7 +5399,7 @@ private int depth(TreeNode root) {
}
```
-**找出二叉树中第二小的节点**
+**找出二叉树中第二小的节点**
[671. Second Minimum Node In a Binary Tree (Easy)](https://leetcode.com/problems/second-minimum-node-in-a-binary-tree/description/)
@@ -5485,7 +5430,7 @@ public int findSecondMinimumValue(TreeNode root) {
}
```
-**二叉查找树的最近公共祖先**
+**二叉查找树的最近公共祖先**
[235. Lowest Common Ancestor of a Binary Search Tree (Easy)](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/)
@@ -5508,7 +5453,7 @@ public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
}
```
-**二叉树的最近公共祖先**
+**二叉树的最近公共祖先**
[236. Lowest Common Ancestor of a Binary Tree (Medium) ](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/)
@@ -5532,7 +5477,7 @@ public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
}
```
-**相同节点值的最大路径长度**
+**相同节点值的最大路径长度**
[687. Longest Univalue Path (Easy)](https://leetcode.com/problems/longest-univalue-path/)
@@ -5565,7 +5510,7 @@ private int dfs(TreeNode root){
}
```
-**间隔遍历**
+**间隔遍历**
[337. House Robber III (Medium)](https://leetcode.com/problems/house-robber-iii/description/)
@@ -5597,7 +5542,7 @@ public int rob(TreeNode root) {
使用 BFS 进行层次遍历。不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。
-**一棵树每层节点的平均数**
+**一棵树每层节点的平均数**
[637. Average of Levels in Binary Tree (Easy)](https://leetcode.com/problems/average-of-levels-in-binary-tree/description/)
@@ -5622,7 +5567,7 @@ public List averageOfLevels(TreeNode root) {
}
```
-**得到左下角的节点**
+**得到左下角的节点**
[513. Find Bottom Left Tree Value (Easy)](https://leetcode.com/problems/find-bottom-left-tree-value/description/)
@@ -5703,7 +5648,7 @@ void dfs(TreeNode root){
}
```
-**非递归实现二叉树的前序遍历**
+**非递归实现二叉树的前序遍历**
[144. Binary Tree Preorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-preorder-traversal/description/)
@@ -5723,7 +5668,7 @@ public List preorderTraversal(TreeNode root) {
}
```
-**非递归实现二叉树的后序遍历**
+**非递归实现二叉树的后序遍历**
[145. Binary Tree Postorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-postorder-traversal/description/)
@@ -5746,7 +5691,7 @@ public List postorderTraversal(TreeNode root) {
}
```
-**非递归实现二叉树的中序遍历**
+**非递归实现二叉树的中序遍历**
[94. Binary Tree Inorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-inorder-traversal/description/)
@@ -5773,7 +5718,7 @@ public List inorderTraversal(TreeNode root) {
主要利用 BST 中序遍历有序的特点。
-**在 BST 中寻找两个节点,使它们的和为一个给定值**
+**在 BST 中寻找两个节点,使它们的和为一个给定值**
[653. Two Sum IV - Input is a BST (Easy)](https://leetcode.com/problems/two-sum-iv-input-is-a-bst/description/)
@@ -5816,7 +5761,7 @@ private void inOrder(TreeNode root, List nums){
}
```
-**在 BST 中查找两个节点之差的最小绝对值**
+**在 BST 中查找两个节点之差的最小绝对值**
[530. Minimum Absolute Difference in BST (Easy)](https://leetcode.com/problems/minimum-absolute-difference-in-bst/description/)
@@ -5852,7 +5797,7 @@ private void inorder(TreeNode node){
}
```
-**把 BST 每个节点的值都加上比它大的节点的值**
+**把 BST 每个节点的值都加上比它大的节点的值**
[Convert BST to Greater Tree (Easy)](https://leetcode.com/problems/convert-bst-to-greater-tree/description/)
@@ -5887,7 +5832,7 @@ private void traver(TreeNode root) {
}
```
-**寻找 BST 中出现次数最多的节点**
+**寻找 BST 中出现次数最多的节点**
[501. Find Mode in Binary Search Tree (Easy)](https://leetcode.com/problems/find-mode-in-binary-search-tree/description/)
@@ -5936,7 +5881,7 @@ private void inOrder(TreeNode node) {
}
```
-**寻找 BST 的第 k 个元素**
+**寻找 BST 的第 k 个元素**
[230. Kth Smallest Element in a BST (Medium)](https://leetcode.com/problems/kth-smallest-element-in-a-bst/description/)
@@ -5979,7 +5924,7 @@ private void inOrder(TreeNode node, int k) {
}
```
-**根据有序链表构造平衡的 BST**
+**根据有序链表构造平衡的 BST**
[109. Convert Sorted List to Binary Search Tree (Medium)](https://leetcode.com/problems/convert-sorted-list-to-binary-search-tree/description/)
@@ -6027,11 +5972,11 @@ private int size(ListNode node) {
### Trie
-
+![](index_files/5c638d59-d4ae-4ba4-ad44-80bdc30f38dd.jpg)
Trie,又称前缀树或字典树,用于判断字符串是否存在或者是否具有某种字符串前缀。
-**实现一个 Trie**
+**实现一个 Trie**
[208. Implement Trie (Prefix Tree) (Medium)](https://leetcode.com/problems/implement-trie-prefix-tree/description/)
@@ -6093,7 +6038,7 @@ class Trie {
}
```
-**实现一个 Trie,用来求前缀和**
+**实现一个 Trie,用来求前缀和**
[677. Map Sum Pairs (Medium)](https://leetcode.com/problems/map-sum-pairs/description/)
@@ -6164,7 +6109,7 @@ class MapSum {
如果可以用两种颜色对图中的节点进行着色,并且保证相邻的节点颜色不同,那么这个图就是二分图。
-**判断是否为二分图**
+**判断是否为二分图**
[785. Is Graph Bipartite? (Medium)](https://leetcode.com/problems/is-graph-bipartite/description/)
@@ -6221,7 +6166,7 @@ private boolean isBipartite(int[][] graph, int node, int color, int[] colors) {
常用于在具有先序关系的任务规划中。
-**课程安排的合法性**
+**课程安排的合法性**
[207. Course Schedule (Medium)](https://leetcode.com/problems/course-schedule/description/)
@@ -6275,7 +6220,7 @@ private boolean dfs(boolean[] globalMarked, boolean[] localMarked, List
}
```
-**课程安排的顺序**
+**课程安排的顺序**
[210. Course Schedule II (Medium)](https://leetcode.com/problems/course-schedule-ii/description/)
@@ -6333,7 +6278,7 @@ private boolean dfs(boolean[] globalMarked, boolean[] localMarked, List
并查集可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。
-**冗余连接**
+**冗余连接**
[684. Redundant Connection (Medium)](https://leetcode.com/problems/redundant-connection/description/)
@@ -6395,7 +6340,7 @@ private class UF {
## 位运算
-**1. 基本原理**
+**1. 基本原理**
0s 表示一串 0,1s 表示一串 1。
@@ -6405,15 +6350,15 @@ x ^ 1s = ~x x & 1s = x x | 1s = 1s
x ^ x = 0 x & x = x x | x = x
```
-- 利用 x ^ 1s = \~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数。
+- 利用 x ^ 1s = ~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数。
- 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask :00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。
- 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1。
位与运算技巧:
-- n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110 **100** ,减去 1 得到 10110**011**,这两个数相与得到 10110**000**。
-- n-n&(\~n+1) 去除 n 的位级表示中最高的那一位。
-- n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110 **100** ,-n 得到 01001**100**,相与得到 00000**100**
+- n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110**100**,减去 1 得到 10110**011**,这两个数相与得到 10110**000**。
+- n-n&(~n+1) 去除 n 的位级表示中最高的那一位。
+- n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110**100**,-n 得到 01001**100**,相与得到 00000**100**
移位运算:
@@ -6421,17 +6366,17 @@ x ^ x = 0 x & x = x x | x = x
- \>\>\> n 为无符号右移,左边会补上 0。
- << n 为算术左移,相当于乘以 2n 。
-**2. mask 计算**
+**2. mask 计算**
-要获取 111111111,将 0 取反即可,\~0。
+要获取 111111111,将 0 取反即可,~0。
要得到只有第 i 位为 1 的 mask,将 1 向左移动 i-1 位即可,1<<(i-1) 。例如 1<<4 得到只有第 5 位为 1 的 mask :00010000。
要得到 1 到 i 位为 1 的 mask,1<<(i+1)-1 即可,例如将 1<<(4+1)-1 = 00010000-1 = 00001111。
-要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 \~(1<<(i+1)-1)。
+要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 ~(1<<(i+1)-1)。
-**3. Java 中的位操作**
+**3. Java 中的位操作**
```html
static int Integer.bitCount(); // 统计 1 的数量
@@ -6439,7 +6384,7 @@ static int Integer.highestOneBit(); // 获得最高位
static String toBinaryString(int i); // 转换为二进制表示的字符串
```
-**统计两个数的二进制表示有多少位不同**
+**统计两个数的二进制表示有多少位不同**
[461. Hamming Distance (Easy)](https://leetcode.com/problems/hamming-distance/)
@@ -6492,7 +6437,7 @@ public int hammingDistance(int x, int y) {
}
```
-**数组中唯一一个不重复的元素**
+**数组中唯一一个不重复的元素**
[136. Single Number (Easy)](https://leetcode.com/problems/single-number/description/)
@@ -6511,7 +6456,7 @@ public int singleNumber(int[] nums) {
}
```
-**找出数组中缺失的那个数**
+**找出数组中缺失的那个数**
[268. Missing Number (Easy)](https://leetcode.com/problems/missing-number/description/)
@@ -6532,7 +6477,7 @@ public int missingNumber(int[] nums) {
}
```
-**数组中不重复的两个元素**
+**数组中不重复的两个元素**
[260. Single Number III (Medium)](https://leetcode.com/problems/single-number-iii/description/)
@@ -6557,7 +6502,7 @@ public int[] singleNumber(int[] nums) {
}
```
-**翻转一个数的比特位**
+**翻转一个数的比特位**
[190. Reverse Bits (Easy)](https://leetcode.com/problems/reverse-bits/description/)
@@ -6604,7 +6549,7 @@ private int reverseByte(byte b) {
}
```
-**不用额外变量交换两个整数**
+**不用额外变量交换两个整数**
[程序员代码面试指南 :P317](#)
@@ -6614,7 +6559,7 @@ b = a ^ b;
a = a ^ b;
```
-**判断一个数是不是 2 的 n 次方**
+**判断一个数是不是 2 的 n 次方**
[231. Power of Two (Easy)](https://leetcode.com/problems/power-of-two/description/)
@@ -6634,7 +6579,7 @@ public boolean isPowerOfTwo(int n) {
}
```
-**判断一个数是不是 4 的 n 次方**
+**判断一个数是不是 4 的 n 次方**
[342. Power of Four (Easy)](https://leetcode.com/problems/power-of-four/)
@@ -6655,7 +6600,7 @@ public boolean isPowerOfFour(int num) {
```
-**判断一个数的位级表示是否不会出现连续的 0 和 1**
+**判断一个数的位级表示是否不会出现连续的 0 和 1**
[693. Binary Number with Alternating Bits (Easy)](https://leetcode.com/problems/binary-number-with-alternating-bits/description/)
@@ -6680,7 +6625,7 @@ public boolean hasAlternatingBits(int n) {
}
```
-**求一个数的补码**
+**求一个数的补码**
[476. Number Complement (Easy)](https://leetcode.com/problems/number-complement/description/)
@@ -6735,7 +6680,7 @@ public int findComplement(int num) {
}
```
-**实现整数的加法**
+**实现整数的加法**
[371. Sum of Two Integers (Easy)](https://leetcode.com/problems/sum-of-two-integers/description/)
@@ -6749,7 +6694,7 @@ public int getSum(int a, int b) {
}
```
-**字符串数组最大乘积**
+**字符串数组最大乘积**
[318. Maximum Product of Word Lengths (Medium)](https://leetcode.com/problems/maximum-product-of-word-lengths/description/)
@@ -6784,7 +6729,7 @@ public int maxProduct(String[] words) {
}
```
-**统计从 0 \~ n 每个数的二进制表示中 1 的个数**
+**统计从 0 ~ n 每个数的二进制表示中 1 的个数**
[338. Counting Bits (Medium)](https://leetcode.com/problems/counting-bits/description/)
diff --git a/notes/Leetcode-Database 题解.md b/notes/Leetcode-Database 题解.md
index a85d82ed..04e8c953 100644
--- a/notes/Leetcode-Database 题解.md
+++ b/notes/Leetcode-Database 题解.md
@@ -1,176 +1,158 @@
-
-* [595. Big Countries](#595-big-countries)
-* [627. Swap Salary](#627-swap-salary)
-* [620. Not Boring Movies](#620-not-boring-movies)
-* [596. Classes More Than 5 Students](#596-classes-more-than-5-students)
-* [182. Duplicate Emails](#182-duplicate-emails)
-* [196. Delete Duplicate Emails](#196-delete-duplicate-emails)
-* [175. Combine Two Tables](#175-combine-two-tables)
-* [181. Employees Earning More Than Their Managers](#181-employees-earning-more-than-their-managers)
-* [183. Customers Who Never Order](#183-customers-who-never-order)
-* [184. Department Highest Salary](#184-department-highest-salary)
-* [176. Second Highest Salary](#176-second-highest-salary)
-* [177. Nth Highest Salary](#177-nth-highest-salary)
-* [178. Rank Scores](#178-rank-scores)
-* [180. Consecutive Numbers](#180-consecutive-numbers)
-
-
-
-# 595. Big Countries
+# 595. Big Countries
https://leetcode.com/problems/big-countries/description/
-## Description
+## Description
```html
+-----------------+------------+------------+--------------+---------------+
-| name | continent | area | population | gdp |
+| name | continent | area | population | gdp |
+-----------------+------------+------------+--------------+---------------+
-| Afghanistan | Asia | 652230 | 25500100 | 20343000 |
-| Albania | Europe | 28748 | 2831741 | 12960000 |
-| Algeria | Africa | 2381741 | 37100000 | 188681000 |
-| Andorra | Europe | 468 | 78115 | 3712000 |
-| Angola | Africa | 1246700 | 20609294 | 100990000 |
+| Afghanistan | Asia | 652230 | 25500100 | 20343000 |
+| Albania | Europe | 28748 | 2831741 | 12960000 |
+| Algeria | Africa | 2381741 | 37100000 | 188681000 |
+| Andorra | Europe | 468 | 78115 | 3712000 |
+| Angola | Africa | 1246700 | 20609294 | 100990000 |
+-----------------+------------+------------+--------------+---------------+
```
-查找面积超过 3,000,000 或者人口数超过 25,000,000 的国家。
+查找面积超过 3,000,000 或者人口数超过 25,000,000 的国家。
```html
+--------------+-------------+--------------+
-| name | population | area |
+| name | population | area |
+--------------+-------------+--------------+
-| Afghanistan | 25500100 | 652230 |
-| Algeria | 37100000 | 2381741 |
+| Afghanistan | 25500100 | 652230 |
+| Algeria | 37100000 | 2381741 |
+--------------+-------------+--------------+
```
-## SQL Schema
+## SQL Schema
```sql
-DROP TABLE
+DROP TABLE
IF
- EXISTS World;
-CREATE TABLE World ( NAME VARCHAR ( 255 ), continent VARCHAR ( 255 ), area INT, population INT, gdp INT );
-INSERT INTO World ( NAME, continent, area, population, gdp )
+ EXISTS World;
+CREATE TABLE World ( NAME VARCHAR ( 255 ), continent VARCHAR ( 255 ), area INT, population INT, gdp INT );
+INSERT INTO World ( NAME, continent, area, population, gdp )
VALUES
- ( 'Afghanistan', 'Asia', '652230', '25500100', '203430000' ),
- ( 'Albania', 'Europe', '28748', '2831741', '129600000' ),
- ( 'Algeria', 'Africa', '2381741', '37100000', '1886810000' ),
- ( 'Andorra', 'Europe', '468', '78115', '37120000' ),
- ( 'Angola', 'Africa', '1246700', '20609294', '1009900000' );
+ ( 'Afghanistan', 'Asia', '652230', '25500100', '203430000' ),
+ ( 'Albania', 'Europe', '28748', '2831741', '129600000' ),
+ ( 'Algeria', 'Africa', '2381741', '37100000', '1886810000' ),
+ ( 'Andorra', 'Europe', '468', '78115', '37120000' ),
+ ( 'Angola', 'Africa', '1246700', '20609294', '1009900000' );
```
-## Solution
+## Solution
```sql
-SELECT name,
+SELECT name,
population,
area
FROM
World
WHERE
- area > 3000000
- OR population > 25000000;
+ area > 3000000
+ OR population > 25000000;
```
-# 627. Swap Salary
+# 627. Swap Salary
https://leetcode.com/problems/swap-salary/description/
-## Description
+## Description
```html
-| id | name | sex | salary |
+| id | name | sex | salary |
|----|------|-----|--------|
-| 1 | A | m | 2500 |
-| 2 | B | f | 1500 |
-| 3 | C | m | 5500 |
-| 4 | D | f | 500 |
+| 1 | A | m | 2500 |
+| 2 | B | f | 1500 |
+| 3 | C | m | 5500 |
+| 4 | D | f | 500 |
```
-只用一个 SQL 查询,将 sex 字段反转。
+只用一个 SQL 查询,将 sex 字段反转。
```html
-| id | name | sex | salary |
+| id | name | sex | salary |
|----|------|-----|--------|
-| 1 | A | m | 2500 |
-| 2 | B | f | 1500 |
-| 3 | C | m | 5500 |
-| 4 | D | f | 500 |
+| 1 | A | m | 2500 |
+| 2 | B | f | 1500 |
+| 3 | C | m | 5500 |
+| 4 | D | f | 500 |
```
-## SQL Schema
+## SQL Schema
```sql
-DROP TABLE
+DROP TABLE
IF
- EXISTS World;
-CREATE TABLE World ( NAME VARCHAR ( 255 ), continent VARCHAR ( 255 ), area INT, population INT, gdp INT );
-INSERT INTO World ( NAME, continent, area, population, gdp )
+ EXISTS World;
+CREATE TABLE World ( NAME VARCHAR ( 255 ), continent VARCHAR ( 255 ), area INT, population INT, gdp INT );
+INSERT INTO World ( NAME, continent, area, population, gdp )
VALUES
- ( 'Afghanistan', 'Asia', '652230', '25500100', '203430000' ),
- ( 'Albania', 'Europe', '28748', '2831741', '129600000' ),
- ( 'Algeria', 'Africa', '2381741', '37100000', '1886810000' ),
- ( 'Andorra', 'Europe', '468', '78115', '37120000' ),
- ( 'Angola', 'Africa', '1246700', '20609294', '1009900000' );
+ ( 'Afghanistan', 'Asia', '652230', '25500100', '203430000' ),
+ ( 'Albania', 'Europe', '28748', '2831741', '129600000' ),
+ ( 'Algeria', 'Africa', '2381741', '37100000', '1886810000' ),
+ ( 'Andorra', 'Europe', '468', '78115', '37120000' ),
+ ( 'Angola', 'Africa', '1246700', '20609294', '1009900000' );
```
-## Solution
+## Solution
```sql
-UPDATE salary
-SET sex = CHAR ( ASCII(sex) ^ ASCII( 'm' ) ^ ASCII( 'f' ) );
+UPDATE salary
+SET sex = CHAR ( ASCII(sex) ^ ASCII( 'm' ) ^ ASCII( 'f' ) );
```
-# 620. Not Boring Movies
+# 620. Not Boring Movies
https://leetcode.com/problems/not-boring-movies/description/
-## Description
+## Description
邮件地址表:
```html
+---------+-----------+--------------+-----------+
-| id | movie | description | rating |
+| id | movie | description | rating |
+---------+-----------+--------------+-----------+
-| 1 | War | great 3D | 8.9 |
-| 2 | Science | fiction | 8.5 |
-| 3 | irish | boring | 6.2 |
-| 4 | Ice song | Fantacy | 8.6 |
-| 5 | House card| Interesting| 9.1 |
+| 1 | War | great 3D | 8.9 |
+| 2 | Science | fiction | 8.5 |
+| 3 | irish | boring | 6.2 |
+| 4 | Ice song | Fantacy | 8.6 |
+| 5 | House card| Interesting| 9.1 |
+---------+-----------+--------------+-----------+
```
-查找 id 为奇数,并且 description 不是 boring 的电影,按 rating 降序。
+查找 id 为奇数,并且 description 不是 boring 的电影,按 rating 降序。
```html
+---------+-----------+--------------+-----------+
-| id | movie | description | rating |
+| id | movie | description | rating |
+---------+-----------+--------------+-----------+
-| 5 | House card| Interesting| 9.1 |
-| 1 | War | great 3D | 8.9 |
+| 5 | House card| Interesting| 9.1 |
+| 1 | War | great 3D | 8.9 |
+---------+-----------+--------------+-----------+
```
-## SQL Schema
+## SQL Schema
```sql
-DROP TABLE
+DROP TABLE
IF
- EXISTS cinema;
-CREATE TABLE cinema ( id INT, movie VARCHAR ( 255 ), description VARCHAR ( 255 ), rating FLOAT ( 2, 1 ) );
-INSERT INTO cinema ( id, movie, description, rating )
+ EXISTS cinema;
+CREATE TABLE cinema ( id INT, movie VARCHAR ( 255 ), description VARCHAR ( 255 ), rating FLOAT ( 2, 1 ) );
+INSERT INTO cinema ( id, movie, description, rating )
VALUES
- ( 1, 'War', 'great 3D', 8.9 ),
- ( 2, 'Science', 'fiction', 8.5 ),
- ( 3, 'irish', 'boring', 6.2 ),
- ( 4, 'Ice song', 'Fantacy', 8.6 ),
- ( 5, 'House card', 'Interesting', 9.1 );
+ ( 1, 'War', 'great 3D', 8.9 ),
+ ( 2, 'Science', 'fiction', 8.5 ),
+ ( 3, 'irish', 'boring', 6.2 ),
+ ( 4, 'Ice song', 'Fantacy', 8.6 ),
+ ( 5, 'House card', 'Interesting', 9.1 );
```
-## Solution
+## Solution
```sql
SELECT
@@ -178,92 +160,92 @@ SELECT
FROM
cinema
WHERE
- id % 2 = 1
- AND description != 'boring'
-ORDER BY
- rating DESC;
+ id % 2 = 1
+ AND description != 'boring'
+ORDER BY
+ rating DESC;
```
-# 596. Classes More Than 5 Students
+# 596. Classes More Than 5 Students
https://leetcode.com/problems/classes-more-than-5-students/description/
-## Description
+## Description
```html
+---------+------------+
-| student | class |
+| student | class |
+---------+------------+
-| A | Math |
-| B | English |
-| C | Math |
-| D | Biology |
-| E | Math |
-| F | Computer |
-| G | Math |
-| H | Math |
-| I | Math |
+| A | Math |
+| B | English |
+| C | Math |
+| D | Biology |
+| E | Math |
+| F | Computer |
+| G | Math |
+| H | Math |
+| I | Math |
+---------+------------+
```
-查找有五名及以上 student 的 class。
+查找有五名及以上 student 的 class。
```html
+---------+
-| Email |
+| Email |
+---------+
-| a@b.com |
+| a@b.com |
+---------+
```
-## SQL Schema
+## SQL Schema
```sql
-DROP TABLE
+DROP TABLE
IF
- EXISTS courses;
-CREATE TABLE courses ( student VARCHAR ( 255 ), class VARCHAR ( 255 ) );
-INSERT INTO courses ( student, class )
+ EXISTS courses;
+CREATE TABLE courses ( student VARCHAR ( 255 ), class VARCHAR ( 255 ) );
+INSERT INTO courses ( student, class )
VALUES
- ( 'A', 'Math' ),
- ( 'B', 'English' ),
- ( 'C', 'Math' ),
- ( 'D', 'Biology' ),
- ( 'E', 'Math' ),
- ( 'F', 'Computer' ),
- ( 'G', 'Math' ),
- ( 'H', 'Math' ),
- ( 'I', 'Math' );
+ ( 'A', 'Math' ),
+ ( 'B', 'English' ),
+ ( 'C', 'Math' ),
+ ( 'D', 'Biology' ),
+ ( 'E', 'Math' ),
+ ( 'F', 'Computer' ),
+ ( 'G', 'Math' ),
+ ( 'H', 'Math' ),
+ ( 'I', 'Math' );
```
-## Solution
+## Solution
```sql
SELECT
class
FROM
courses
-GROUP BY
+GROUP BY
class
HAVING
- count( DISTINCT student ) >= 5;
+ count( DISTINCT student ) >= 5;
```
-# 182. Duplicate Emails
+# 182. Duplicate Emails
https://leetcode.com/problems/duplicate-emails/description/
-## Description
+## Description
邮件地址表:
```html
+----+---------+
-| Id | Email |
+| Id | Email |
+----+---------+
-| 1 | a@b.com |
-| 2 | c@d.com |
-| 3 | a@b.com |
+| 1 | a@b.com |
+| 2 | c@d.com |
+| 3 | a@b.com |
+----+---------+
```
@@ -271,52 +253,52 @@ https://leetcode.com/problems/duplicate-emails/description/
```html
+---------+
-| Email |
+| Email |
+---------+
-| a@b.com |
+| a@b.com |
+---------+
```
-## SQL Schema
+## SQL Schema
```sql
-DROP TABLE
+DROP TABLE
IF
- EXISTS Person;
-CREATE TABLE Person ( Id INT, Email VARCHAR ( 255 ) );
-INSERT INTO Person ( Id, Email )
+ EXISTS Person;
+CREATE TABLE Person ( Id INT, Email VARCHAR ( 255 ) );
+INSERT INTO Person ( Id, Email )
VALUES
- ( 1, 'a@b.com' ),
- ( 2, 'c@d.com' ),
- ( 3, 'a@b.com' );
+ ( 1, 'a@b.com' ),
+ ( 2, 'c@d.com' ),
+ ( 3, 'a@b.com' );
```
-## Solution
+## Solution
```sql
SELECT
Email
FROM
Person
-GROUP BY
+GROUP BY
Email
HAVING
- COUNT( * ) >= 2;
+ COUNT( * ) >= 2;
```
-# 196. Delete Duplicate Emails
+# 196. Delete Duplicate Emails
-## Description
+## Description
邮件地址表:
```html
+----+---------+
-| Id | Email |
+| Id | Email |
+----+---------+
-| 1 | a@b.com |
-| 2 | c@d.com |
-| 3 | a@b.com |
+| 1 | a@b.com |
+| 2 | c@d.com |
+| 3 | a@b.com |
+----+---------+
```
@@ -324,28 +306,28 @@ HAVING
```html
+---------+
-| Email |
+| Email |
+---------+
-| a@b.com |
+| a@b.com |
+---------+
```
-## SQL Schema
+## SQL Schema
-与 182 相同。
+与 182 相同。
-## Solution
+## Solution
连接:
```sql
-DELETE p1
+DELETE p1
FROM
- Person p1,
- Person p2
+ Person p1,
+ Person p2
WHERE
- p1.Email = p2.Email
- AND p1.Id > p2.Id
+ p1.Email = p2.Email
+ AND p1.Id > p2.Id
```
子查询:
@@ -355,164 +337,164 @@ DELETE
FROM
Person
WHERE
- id NOT IN ( SELECT id FROM ( SELECT min( id ) AS id FROM Person GROUP BY email ) AS m );
+ id NOT IN ( SELECT id FROM ( SELECT min( id ) AS id FROM Person GROUP BY email ) AS m );
```
-应该注意的是上述解法额外嵌套了一个 SELECT 语句,如果不这么做,会出现错误:You can't specify target table 'Person' for update in FROM clause。以下演示了这种错误解法。
+应该注意的是上述解法额外嵌套了一个 SELECT 语句,如果不这么做,会出现错误:You can't specify target table 'Person' for update in FROM clause。以下演示了这种错误解法。
```sql
DELETE
FROM
Person
WHERE
- id NOT IN ( SELECT min( id ) AS id FROM Person GROUP BY email );
+ id NOT IN ( SELECT min( id ) AS id FROM Person GROUP BY email );
```
-参考:[pMySQL Error 1093 - Can't specify target table for update in FROM clause](https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause)
+参考:[pMySQL Error 1093 - Can't specify target table for update in FROM clause](https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause)
-# 175. Combine Two Tables
+# 175. Combine Two Tables
https://leetcode.com/problems/combine-two-tables/description/
-## Description
+## Description
-Person 表:
+Person 表:
```html
+-------------+---------+
-| Column Name | Type |
+| Column Name | Type |
+-------------+---------+
-| PersonId | int |
-| FirstName | varchar |
-| LastName | varchar |
+| PersonId | int |
+| FirstName | varchar |
+| LastName | varchar |
+-------------+---------+
-PersonId is the primary key column for this table.
+PersonId is the primary key column for this table.
```
-Address 表:
+Address 表:
```html
+-------------+---------+
-| Column Name | Type |
+| Column Name | Type |
+-------------+---------+
-| AddressId | int |
-| PersonId | int |
-| City | varchar |
-| State | varchar |
+| AddressId | int |
+| PersonId | int |
+| City | varchar |
+| State | varchar |
+-------------+---------+
-AddressId is the primary key column for this table.
+AddressId is the primary key column for this table.
```
-查找 FirstName, LastName, City, State 数据,而不管一个用户有没有填地址信息。
+查找 FirstName, LastName, City, State 数据,而不管一个用户有没有填地址信息。
-## SQL Schema
+## SQL Schema
```sql
-DROP TABLE
+DROP TABLE
IF
- EXISTS Person;
-CREATE TABLE Person ( PersonId INT, FirstName VARCHAR ( 255 ), LastName VARCHAR ( 255 ) );
-DROP TABLE
+ EXISTS Person;
+CREATE TABLE Person ( PersonId INT, FirstName VARCHAR ( 255 ), LastName VARCHAR ( 255 ) );
+DROP TABLE
IF
- EXISTS Address;
-CREATE TABLE Address ( AddressId INT, PersonId INT, City VARCHAR ( 255 ), State VARCHAR ( 255 ) );
-INSERT INTO Person ( PersonId, LastName, FirstName )
+ EXISTS Address;
+CREATE TABLE Address ( AddressId INT, PersonId INT, City VARCHAR ( 255 ), State VARCHAR ( 255 ) );
+INSERT INTO Person ( PersonId, LastName, FirstName )
VALUES
- ( 1, 'Wang', 'Allen' );
-INSERT INTO Address ( AddressId, PersonId, City, State )
+ ( 1, 'Wang', 'Allen' );
+INSERT INTO Address ( AddressId, PersonId, City, State )
VALUES
- ( 1, 2, 'New York City', 'New York' );
+ ( 1, 2, 'New York City', 'New York' );
```
-## Solution
+## Solution
使用左外连接。
```sql
SELECT
- FirstName,
- LastName,
- City,
- State
+ FirstName,
+ LastName,
+ City,
+ State
FROM
- Person P
- LEFT JOIN Address AS A ON P.PersonId = A.PersonId;
+ Person P
+ LEFT JOIN Address AS A ON P.PersonId = A.PersonId;
```
-# 181. Employees Earning More Than Their Managers
+# 181. Employees Earning More Than Their Managers
https://leetcode.com/problems/employees-earning-more-than-their-managers/description/
-## Description
+## Description
-Employee 表:
+Employee 表:
```html
+----+-------+--------+-----------+
-| Id | Name | Salary | ManagerId |
+| Id | Name | Salary | ManagerId |
+----+-------+--------+-----------+
-| 1 | Joe | 70000 | 3 |
-| 2 | Henry | 80000 | 4 |
-| 3 | Sam | 60000 | NULL |
-| 4 | Max | 90000 | NULL |
+| 1 | Joe | 70000 | 3 |
+| 2 | Henry | 80000 | 4 |
+| 3 | Sam | 60000 | NULL |
+| 4 | Max | 90000 | NULL |
+----+-------+--------+-----------+
```
查找所有员工,他们的薪资大于其经理薪资。
-## SQL Schema
+## SQL Schema
```sql
-DROP TABLE
+DROP TABLE
IF
- EXISTS Employee;
-CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, ManagerId INT );
-INSERT INTO Employee ( Id, NAME, Salary, ManagerId )
+ EXISTS Employee;
+CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, ManagerId INT );
+INSERT INTO Employee ( Id, NAME, Salary, ManagerId )
VALUES
- ( 1, 'Joe', 70000, 3 ),
- ( 2, 'Henry', 80000, 4 ),
- ( 3, 'Sam', 60000, NULL ),
- ( 4, 'Max', 90000, NULL );
+ ( 1, 'Joe', 70000, 3 ),
+ ( 2, 'Henry', 80000, 4 ),
+ ( 3, 'Sam', 60000, NULL ),
+ ( 4, 'Max', 90000, NULL );
```
-## Solution
+## Solution
```sql
SELECT
- E1.NAME AS Employee
+ E1.NAME AS Employee
FROM
- Employee E1
- INNER JOIN Employee E2 ON E1.ManagerId = E2.Id
- AND E1.Salary > E2.Salary;
+ Employee E1
+ INNER JOIN Employee E2 ON E1.ManagerId = E2.Id
+ AND E1.Salary > E2.Salary;
```
-# 183. Customers Who Never Order
+# 183. Customers Who Never Order
https://leetcode.com/problems/customers-who-never-order/description/
-## Description
+## Description
-Curstomers 表:
+Curstomers 表:
```html
+----+-------+
-| Id | Name |
+| Id | Name |
+----+-------+
-| 1 | Joe |
-| 2 | Henry |
-| 3 | Sam |
-| 4 | Max |
+| 1 | Joe |
+| 2 | Henry |
+| 3 | Sam |
+| 4 | Max |
+----+-------+
```
-Orders 表:
+Orders 表:
```html
+----+------------+
-| Id | CustomerId |
+| Id | CustomerId |
+----+------------+
-| 1 | 3 |
-| 2 | 1 |
+| 1 | 3 |
+| 2 | 1 |
+----+------------+
```
@@ -520,155 +502,155 @@ Orders 表:
```html
+-----------+
-| Customers |
+| Customers |
+-----------+
-| Henry |
-| Max |
+| Henry |
+| Max |
+-----------+
```
-## SQL Schema
+## SQL Schema
```sql
-DROP TABLE
+DROP TABLE
IF
- EXISTS Customers;
-CREATE TABLE Customers ( Id INT, NAME VARCHAR ( 255 ) );
-DROP TABLE
+ EXISTS Customers;
+CREATE TABLE Customers ( Id INT, NAME VARCHAR ( 255 ) );
+DROP TABLE
IF
- EXISTS Orders;
-CREATE TABLE Orders ( Id INT, CustomerId INT );
-INSERT INTO Customers ( Id, NAME )
+ EXISTS Orders;
+CREATE TABLE Orders ( Id INT, CustomerId INT );
+INSERT INTO Customers ( Id, NAME )
VALUES
- ( 1, 'Joe' ),
- ( 2, 'Henry' ),
- ( 3, 'Sam' ),
- ( 4, 'Max' );
-INSERT INTO Orders ( Id, CustomerId )
+ ( 1, 'Joe' ),
+ ( 2, 'Henry' ),
+ ( 3, 'Sam' ),
+ ( 4, 'Max' );
+INSERT INTO Orders ( Id, CustomerId )
VALUES
- ( 1, 3 ),
- ( 2, 1 );
+ ( 1, 3 ),
+ ( 2, 1 );
```
-## Solution
+## Solution
左外链接
```sql
SELECT
- C.NAME AS Customers
+ C.NAME AS Customers
FROM
- Customers C
- LEFT JOIN Orders O ON C.Id = O.CustomerId
+ Customers C
+ LEFT JOIN Orders O ON C.Id = O.CustomerId
WHERE
- O.CustomerId IS NULL;
+ O.CustomerId IS NULL;
```
子查询
```sql
SELECT
- C.NAME AS Customers
+ C.NAME AS Customers
FROM
- Customers C
+ Customers C
WHERE
- C.Id NOT IN ( SELECT CustomerId FROM Orders );
+ C.Id NOT IN ( SELECT CustomerId FROM Orders );
```
-# 184. Department Highest Salary
+# 184. Department Highest Salary
https://leetcode.com/problems/department-highest-salary/description/
-## Description
+## Description
-Employee 表:
+Employee 表:
```html
+----+-------+--------+--------------+
-| Id | Name | Salary | DepartmentId |
+| Id | Name | Salary | DepartmentId |
+----+-------+--------+--------------+
-| 1 | Joe | 70000 | 1 |
-| 2 | Henry | 80000 | 2 |
-| 3 | Sam | 60000 | 2 |
-| 4 | Max | 90000 | 1 |
+| 1 | Joe | 70000 | 1 |
+| 2 | Henry | 80000 | 2 |
+| 3 | Sam | 60000 | 2 |
+| 4 | Max | 90000 | 1 |
+----+-------+--------+--------------+
```
-Department 表:
+Department 表:
```html
+----+----------+
-| Id | Name |
+| Id | Name |
+----+----------+
-| 1 | IT |
-| 2 | Sales |
+| 1 | IT |
+| 2 | Sales |
+----+----------+
```
-查找一个 Department 中收入最高者的信息:
+查找一个 Department 中收入最高者的信息:
```html
+------------+----------+--------+
-| Department | Employee | Salary |
+| Department | Employee | Salary |
+------------+----------+--------+
-| IT | Max | 90000 |
-| Sales | Henry | 80000 |
+| IT | Max | 90000 |
+| Sales | Henry | 80000 |
+------------+----------+--------+
```
-## SQL Schema
+## SQL Schema
```sql
-DROP TABLE IF EXISTS Employee;
-CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, DepartmentId INT );
-DROP TABLE IF EXISTS Department;
-CREATE TABLE Department ( Id INT, NAME VARCHAR ( 255 ) );
-INSERT INTO Employee ( Id, NAME, Salary, DepartmentId )
+DROP TABLE IF EXISTS Employee;
+CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, DepartmentId INT );
+DROP TABLE IF EXISTS Department;
+CREATE TABLE Department ( Id INT, NAME VARCHAR ( 255 ) );
+INSERT INTO Employee ( Id, NAME, Salary, DepartmentId )
VALUES
- ( 1, 'Joe', 70000, 1 ),
- ( 2, 'Henry', 80000, 2 ),
- ( 3, 'Sam', 60000, 2 ),
- ( 4, 'Max', 90000, 1 );
-INSERT INTO Department ( Id, NAME )
+ ( 1, 'Joe', 70000, 1 ),
+ ( 2, 'Henry', 80000, 2 ),
+ ( 3, 'Sam', 60000, 2 ),
+ ( 4, 'Max', 90000, 1 );
+INSERT INTO Department ( Id, NAME )
VALUES
- ( 1, 'IT' ),
- ( 2, 'Sales' );
+ ( 1, 'IT' ),
+ ( 2, 'Sales' );
```
-## Solution
+## Solution
-创建一个临时表,包含了部门员工的最大薪资。可以对部门进行分组,然后使用 MAX() 汇总函数取得最大薪资。
+创建一个临时表,包含了部门员工的最大薪资。可以对部门进行分组,然后使用 MAX() 汇总函数取得最大薪资。
之后使用连接将找到一个部门中薪资等于临时表中最大薪资的员工。
```sql
SELECT
- D.NAME Department,
- E.NAME Employee,
+ D.NAME Department,
+ E.NAME Employee,
E.Salary
FROM
- Employee E,
- Department D,
- ( SELECT DepartmentId, MAX( Salary ) Salary FROM Employee GROUP BY DepartmentId ) M
+ Employee E,
+ Department D,
+ ( SELECT DepartmentId, MAX( Salary ) Salary FROM Employee GROUP BY DepartmentId ) M
WHERE
- E.DepartmentId = D.Id
- AND E.DepartmentId = M.DepartmentId
- AND E.Salary = M.Salary;
+ E.DepartmentId = D.Id
+ AND E.DepartmentId = M.DepartmentId
+ AND E.Salary = M.Salary;
```
-# 176. Second Highest Salary
+# 176. Second Highest Salary
https://leetcode.com/problems/second-highest-salary/description/
-## Description
+## Description
```html
+----+--------+
-| Id | Salary |
+| Id | Salary |
+----+--------+
-| 1 | 100 |
-| 2 | 200 |
-| 3 | 300 |
+| 1 | 100 |
+| 2 | 200 |
+| 3 | 300 |
+----+--------+
```
@@ -676,76 +658,76 @@ https://leetcode.com/problems/second-highest-salary/description/
```html
+---------------------+
-| SecondHighestSalary |
+| SecondHighestSalary |
+---------------------+
-| 200 |
+| 200 |
+---------------------+
```
-如果没有找到,那么就返回 null 而不是不返回数据。
+如果没有找到,那么就返回 null 而不是不返回数据。
-## SQL Schema
+## SQL Schema
```sql
-DROP TABLE
+DROP TABLE
IF
- EXISTS Employee;
-CREATE TABLE Employee ( Id INT, Salary INT );
-INSERT INTO Employee ( Id, Salary )
+ EXISTS Employee;
+CREATE TABLE Employee ( Id INT, Salary INT );
+INSERT INTO Employee ( Id, Salary )
VALUES
- ( 1, 100 ),
- ( 2, 200 ),
- ( 3, 300 );
+ ( 1, 100 ),
+ ( 2, 200 ),
+ ( 3, 300 );
```
-## Solution
+## Solution
-为了在没有查找到数据时返回 null,需要在查询结果外面再套一层 SELECT。
+为了在没有查找到数据时返回 null,需要在查询结果外面再套一层 SELECT。
```sql
SELECT
- ( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT 1, 1 ) SecondHighestSalary;
+ ( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT 1, 1 ) SecondHighestSalary;
```
-# 177. Nth Highest Salary
+# 177. Nth Highest Salary
-## Description
+## Description
-查找工资第 N 高的员工。
+查找工资第 N 高的员工。
-## SQL Schema
+## SQL Schema
-同 176。
+同 176。
-## Solution
+## Solution
```sql
-CREATE FUNCTION getNthHighestSalary ( N INT ) RETURNS INT BEGIN
+CREATE FUNCTION getNthHighestSalary ( N INT ) RETURNS INT BEGIN
-SET N = N - 1;
-RETURN ( SELECT ( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT N, 1 ) );
+SET N = N - 1;
+RETURN ( SELECT ( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT N, 1 ) );
END
```
-# 178. Rank Scores
+# 178. Rank Scores
https://leetcode.com/problems/rank-scores/description/
-## Description
+## Description
得分表:
```html
+----+-------+
-| Id | Score |
+| Id | Score |
+----+-------+
-| 1 | 3.50 |
-| 2 | 3.65 |
-| 3 | 4.00 |
-| 4 | 3.85 |
-| 5 | 4.00 |
-| 6 | 3.65 |
+| 1 | 3.50 |
+| 2 | 3.65 |
+| 3 | 4.00 |
+| 4 | 3.85 |
+| 5 | 4.00 |
+| 6 | 3.65 |
+----+-------+
```
@@ -753,68 +735,68 @@ https://leetcode.com/problems/rank-scores/description/
```html
+-------+------+
-| Score | Rank |
+| Score | Rank |
+-------+------+
-| 4.00 | 1 |
-| 4.00 | 1 |
-| 3.85 | 2 |
-| 3.65 | 3 |
-| 3.65 | 3 |
-| 3.50 | 4 |
+| 4.00 | 1 |
+| 4.00 | 1 |
+| 3.85 | 2 |
+| 3.65 | 3 |
+| 3.65 | 3 |
+| 3.50 | 4 |
+-------+------+
```
-## SQL Schema
+## SQL Schema
```sql
-DROP TABLE
+DROP TABLE
IF
- EXISTS Scores;
-CREATE TABLE Scores ( Id INT, Score DECIMAL ( 3, 2 ) );
-INSERT INTO Scores ( Id, Score )
+ EXISTS Scores;
+CREATE TABLE Scores ( Id INT, Score DECIMAL ( 3, 2 ) );
+INSERT INTO Scores ( Id, Score )
VALUES
- ( 1, 3.5 ),
- ( 2, 3.65 ),
- ( 3, 4.0 ),
- ( 4, 3.85 ),
- ( 5, 4.0 ),
- ( 6, 3.65 );
+ ( 1, 3.5 ),
+ ( 2, 3.65 ),
+ ( 3, 4.0 ),
+ ( 4, 3.85 ),
+ ( 5, 4.0 ),
+ ( 6, 3.65 );
```
-## Solution
+## Solution
```sql
SELECT
- S1.score,
- COUNT( DISTINCT S2.score ) Rank
+ S1.score,
+ COUNT( DISTINCT S2.score ) Rank
FROM
- Scores S1
- INNER JOIN Scores S2 ON S1.score <= S2.score
-GROUP BY
- S1.id
-ORDER BY
- S1.score DESC;
+ Scores S1
+ INNER JOIN Scores S2 ON S1.score <= S2.score
+GROUP BY
+ S1.id
+ORDER BY
+ S1.score DESC;
```
-# 180. Consecutive Numbers
+# 180. Consecutive Numbers
https://leetcode.com/problems/consecutive-numbers/description/
-## Description
+## Description
数字表:
```html
+----+-----+
-| Id | Num |
+| Id | Num |
+----+-----+
-| 1 | 1 |
-| 2 | 1 |
-| 3 | 1 |
-| 4 | 2 |
-| 5 | 1 |
-| 6 | 2 |
-| 7 | 2 |
+| 1 | 1 |
+| 2 | 1 |
+| 3 | 1 |
+| 4 | 2 |
+| 5 | 1 |
+| 6 | 2 |
+| 7 | 2 |
+----+-----+
```
@@ -822,41 +804,41 @@ https://leetcode.com/problems/consecutive-numbers/description/
```html
+-----------------+
-| ConsecutiveNums |
+| ConsecutiveNums |
+-----------------+
-| 1 |
+| 1 |
+-----------------+
```
-## SQL Schema
+## SQL Schema
```sql
-DROP TABLE
+DROP TABLE
IF
- EXISTS LOGS;
-CREATE TABLE LOGS ( Id INT, Num INT );
-INSERT INTO LOGS ( Id, Num )
+ EXISTS LOGS;
+CREATE TABLE LOGS ( Id INT, Num INT );
+INSERT INTO LOGS ( Id, Num )
VALUES
- ( 1, 1 ),
- ( 2, 1 ),
- ( 3, 1 ),
- ( 4, 2 ),
- ( 5, 1 ),
- ( 6, 2 ),
- ( 7, 2 );
+ ( 1, 1 ),
+ ( 2, 1 ),
+ ( 3, 1 ),
+ ( 4, 2 ),
+ ( 5, 1 ),
+ ( 6, 2 ),
+ ( 7, 2 );
```
-## Solution
+## Solution
```sql
SELECT
- DISTINCT L1.num ConsecutiveNums
+ DISTINCT L1.num ConsecutiveNums
FROM
- Logs L1,
- Logs L2,
- Logs L3
-WHERE L1.id = l2.id - 1
- AND L2.id = L3.id - 1
- AND L1.num = L2.num
- AND l2.num = l3.num;
+ Logs L1,
+ Logs L2,
+ Logs L3
+WHERE L1.id = l2.id - 1
+ AND L2.id = L3.id - 1
+ AND L1.num = L2.num
+ AND l2.num = l3.num;
```
diff --git a/notes/Linux.md b/notes/Linux.md
index f19f3e11..434c5ed2 100644
--- a/notes/Linux.md
+++ b/notes/Linux.md
@@ -1,1256 +1,1208 @@
-
-* [一、常用操作以及概念](#一常用操作以及概念)
- * [快捷键](#快捷键)
- * [求助](#求助)
- * [关机](#关机)
- * [PATH](#path)
- * [sudo](#sudo)
- * [包管理工具](#包管理工具)
- * [发行版](#发行版)
- * [VIM 三个模式](#vim-三个模式)
- * [GNU](#gnu)
- * [开源协议](#开源协议)
-* [二、磁盘](#二磁盘)
- * [HDD](#hdd)
- * [磁盘接口](#磁盘接口)
- * [磁盘的文件名](#磁盘的文件名)
-* [三、分区](#三分区)
- * [分区表](#分区表)
- * [开机检测程序](#开机检测程序)
-* [四、文件系统](#四文件系统)
- * [分区与文件系统](#分区与文件系统)
- * [组成](#组成)
- * [文件读取](#文件读取)
- * [磁盘碎片](#磁盘碎片)
- * [block](#block)
- * [inode](#inode)
- * [目录](#目录)
- * [日志](#日志)
- * [挂载](#挂载)
- * [目录配置](#目录配置)
-* [五、文件](#五文件)
- * [文件属性](#文件属性)
- * [文件与目录的基本操作](#文件与目录的基本操作)
- * [修改权限](#修改权限)
- * [文件默认权限](#文件默认权限)
- * [目录的权限](#目录的权限)
- * [链接](#链接)
- * [获取文件内容](#获取文件内容)
- * [指令与文件搜索](#指令与文件搜索)
-* [六、压缩与打包](#六压缩与打包)
- * [压缩文件名](#压缩文件名)
- * [压缩指令](#压缩指令)
- * [打包](#打包)
-* [七、Bash](#七bash)
- * [特性](#特性)
- * [变量操作](#变量操作)
- * [指令搜索顺序](#指令搜索顺序)
- * [数据流重定向](#数据流重定向)
-* [八、管线指令](#八管线指令)
- * [提取指令](#提取指令)
- * [排序指令](#排序指令)
- * [双向输出重定向](#双向输出重定向)
- * [字符转换指令](#字符转换指令)
- * [分区指令](#分区指令)
-* [九、正则表达式](#九正则表达式)
- * [grep](#grep)
- * [printf](#printf)
- * [awk](#awk)
-* [十、进程管理](#十进程管理)
- * [查看进程](#查看进程)
- * [进程状态](#进程状态)
- * [SIGCHLD](#sigchld)
- * [wait()](#wait)
- * [waitpid()](#waitpid)
- * [孤儿进程](#孤儿进程)
- * [僵死进程](#僵死进程)
-* [参考资料](#参考资料)
-
+# 一、常用操作以及概念
+## 快捷键
-# 一、常用操作以及概念
+- Tab:命令和文件名补全;
+- Ctrl+C:中断正在运行的程序;
+- Ctrl+D:结束键盘输入(End Of File,EOF)
-## 快捷键
+## 求助
-- Tab:命令和文件名补全;
-- Ctrl+C:中断正在运行的程序;
-- Ctrl+D:结束键盘输入(End Of File,EOF)
-
-## 求助
-
-### 1. --help
+### 1. --help
指令的基本用法与选项介绍。
-### 2. man
+### 2. man
-man 是 manual 的缩写,将指令的具体信息显示出来。
+man 是 manual 的缩写,将指令的具体信息显示出来。
-当执行`man date`时,有 DATE(1) 出现,其中的数字代表指令的类型,常用的数字及其类型如下:
+当执行`man date`时,有 DATE(1) 出现,其中的数字代表指令的类型,常用的数字及其类型如下:
-| 代号 | 类型 |
-| :--: | -- |
-| 1 | 用户在 shell 环境中可以操作的指令或者可执行文件 |
-| 5 | 配置文件 |
-| 8 | 系统管理员可以使用的管理指令 |
+| 代号 | 类型 |
+| :--: | -- |
+| 1 | 用户在 shell 环境中可以操作的指令或者可执行文件 |
+| 5 | 配置文件 |
+| 8 | 系统管理员可以使用的管理指令 |
-### 3. info
+### 3. info
-info 与 man 类似,但是 info 将文档分成一个个页面,每个页面可以进行跳转。
+info 与 man 类似,但是 info 将文档分成一个个页面,每个页面可以进行跳转。
-### 4. doc
+### 4. doc
-/usr/share/doc 存放着软件的一整套说明文件。
+/usr/share/doc 存放着软件的一整套说明文件。
-## 关机
+## 关机
-### 1. who
+### 1. who
-在关机前需要先使用 who 命令查看有没有其它用户在线。
+在关机前需要先使用 who 命令查看有没有其它用户在线。
-### 2. sync
+### 2. sync
-为了加快对磁盘文件的读写速度,位于内存中的文件数据不会立即同步到磁盘上,因此关机之前需要先进行 sync 同步操作。
+为了加快对磁盘文件的读写速度,位于内存中的文件数据不会立即同步到磁盘上,因此关机之前需要先进行 sync 同步操作。
-### 3. shutdown
+### 3. shutdown
```html
-# shutdown [-krhc] 时间 [信息]
--k : 不会关机,只是发送警告信息,通知所有在线的用户
--r : 将系统的服务停掉后就重新启动
--h : 将系统的服务停掉后就立即关机
--c : 取消已经在进行的 shutdown 指令内容
+# shutdown [-krhc] 时间 [信息]
+-k : 不会关机,只是发送警告信息,通知所有在线的用户
+-r : 将系统的服务停掉后就重新启动
+-h : 将系统的服务停掉后就立即关机
+-c : 取消已经在进行的 shutdown 指令内容
```
-## PATH
+## PATH
-可以在环境变量 PATH 中声明可执行文件的路径,路径之间用 : 分隔。
+可以在环境变量 PATH 中声明可执行文件的路径,路径之间用 : 分隔。
```html
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin
```
-## sudo
+## sudo
-sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。
+sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。
-## 包管理工具
+## 包管理工具
-RPM 和 DPKG 为最常见的两类软件包管理工具。RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。与 RPM 进行竞争的是基于 Debian 操作系统 (UBUNTU) 的 DEB 软件包管理工具 DPKG,全称为 Debian Package,功能方面与 RPM 相似。
+RPM 和 DPKG 为最常见的两类软件包管理工具。RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。与 RPM 进行竞争的是基于 Debian 操作系统 (UBUNTU) 的 DEB 软件包管理工具 DPKG,全称为 Debian Package,功能方面与 RPM 相似。
-YUM 基于 RPM,具有依赖管理功能,并具有软件升级的功能。
+YUM 基于 RPM,具有依赖管理功能,并具有软件升级的功能。
-## 发行版
+## 发行版
-Linux 发行版是 Linux 内核及各种应用软件的集成版本。
+Linux 发行版是 Linux 内核及各种应用软件的集成版本。
-| 基于的包管理工具 | 商业发行版 | 社区发行版 |
-| :--: | :--: | :--: |
-| RPM | Red Hat | Fedora / CentOS |
-| DPKG | Ubuntu | Debian |
+| 基于的包管理工具 | 商业发行版 | 社区发行版 |
+| :--: | :--: | :--: |
+| RPM | Red Hat | Fedora / CentOS |
+| DPKG | Ubuntu | Debian |
-## VIM 三个模式
+## VIM 三个模式
-- 一般指令模式(Command mode):VIM 的默认模式,可以用于移动游标查看内容;
-- 编辑模式(Insert mode):按下 "i" 等按键之后进入,可以对文本进行编辑;
-- 指令列模式(Bottom-line mode):按下 ":" 按键之后进入,用于保存退出等操作。
+- 一般指令模式(Command mode):VIM 的默认模式,可以用于移动游标查看内容;
+- 编辑模式(Insert mode):按下 "i" 等按键之后进入,可以对文本进行编辑;
+- 指令列模式(Bottom-line mode):按下 ":" 按键之后进入,用于保存退出等操作。
-
+
在指令列模式下,有以下命令用于离开或者保存文件。
-| 命令 | 作用 |
-| :--: | :--: |
-| :w | 写入磁盘|
-| :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 |
-| :q | 离开 |
-| :q! | 强制离开不保存 |
-| :wq | 写入磁盘后离开 |
-| :wq!| 强制写入磁盘后离开 |
+| 命令 | 作用 |
+| :--: | :--: |
+| :w | 写入磁盘|
+| :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 |
+| :q | 离开 |
+| :q! | 强制离开不保存 |
+| :wq | 写入磁盘后离开 |
+| :wq!| 强制写入磁盘后离开 |
-## GNU
+## GNU
-GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU,其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议,包含了以下内容:
+GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU,其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议,包含了以下内容:
-- 以任何目的运行此程序的自由;
-- 再复制的自由;
-- 改进此程序,并公开发布改进的自由。
+- 以任何目的运行此程序的自由;
+- 再复制的自由;
+- 改进此程序,并公开发布改进的自由。
-## 开源协议
+## 开源协议
-- [Choose an open source license](https://choosealicense.com/)
-- [如何选择开源许可证?](http://www.ruanyifeng.com/blog/2011/05/how_to_choose_free_software_licenses.html)
+- [Choose an open source license](https://choosealicense.com/)
+- [如何选择开源许可证?](http://www.ruanyifeng.com/blog/2011/05/how_to_choose_free_software_licenses.html)
-# 二、磁盘
+# 二、磁盘
-## HDD
+## HDD
-[Decoding UCS Invicta – Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1)
+[Decoding UCS Invicta – Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1)
-Hard Disk Drives(HDD) 俗称硬盘,具有以下结构:
+Hard Disk Drives(HDD) 俗称硬盘,具有以下结构:
-- 盘面(Platter):一个硬盘有多个盘面;
-- 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道;
-- 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小;
-- 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写);
-- 制动手臂(Actuator arm):用于在磁道之间移动磁头;
-- 主轴(Spindle):使整个盘面转动。
+- 盘面(Platter):一个硬盘有多个盘面;
+- 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道;
+- 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小;
+- 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写);
+- 制动手臂(Actuator arm):用于在磁道之间移动磁头;
+- 主轴(Spindle):使整个盘面转动。
-
+
-## 磁盘接口
+## 磁盘接口
-### 1. IDE
+### 1. IDE
-IDE(ATA)全称 Advanced Technology Attachment,接口速度最大为 133MB/s,因为并口线的抗干扰性太差,且排线占用空间较大,不利电脑内部散热,已逐渐被 SATA 所取代。
+IDE(ATA)全称 Advanced Technology Attachment,接口速度最大为 133MB/s,因为并口线的抗干扰性太差,且排线占用空间较大,不利电脑内部散热,已逐渐被 SATA 所取代。
-
+
-### 2. SATA
+### 2. SATA
-SATA 全称 Serial ATA,也就是使用串口的 ATA 接口,因抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能,SATA-II 的接口速度为 300MiB/s,而新的 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。
+SATA 全称 Serial ATA,也就是使用串口的 ATA 接口,因抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能,SATA-II 的接口速度为 300MiB/s,而新的 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。
-
+
-### 3. SCSI
+### 3. SCSI
-SCSI 全称是 Small Computer System Interface(小型机系统接口),经历多代的发展,从早期的 SCSI-II,到目前的 Ultra320 SCSI 以及 Fiber-Channel(光纤通道),接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且资料传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。
+SCSI 全称是 Small Computer System Interface(小型机系统接口),经历多代的发展,从早期的 SCSI-II,到目前的 Ultra320 SCSI 以及 Fiber-Channel(光纤通道),接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且资料传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。
-
+
-### 4. SAS
+### 4. SAS
-SAS(Serial Attached SCSI)是新一代的 SCSI 技术,和 SATA 硬盘相同,都是采取序列式技术以获得更高的传输速度,可达到 6Gb/s。此外也透过缩小连接线改善系统内部空间等。
+SAS(Serial Attached SCSI)是新一代的 SCSI 技术,和 SATA 硬盘相同,都是采取序列式技术以获得更高的传输速度,可达到 6Gb/s。此外也透过缩小连接线改善系统内部空间等。
-
+
-## 磁盘的文件名
+## 磁盘的文件名
-Linux 中每个硬件都被当做一个文件,包括磁盘。磁盘以磁盘接口类型进行命名,常见磁盘的文件名如下:
+Linux 中每个硬件都被当做一个文件,包括磁盘。磁盘以磁盘接口类型进行命名,常见磁盘的文件名如下:
-- IDE 磁盘:/dev/hd[a-d]
-- SATA/SCSI/SAS 磁盘:/dev/sd[a-p]
+- IDE 磁盘:/dev/hd[a-d]
+- SATA/SCSI/SAS 磁盘:/dev/sd[a-p]
其中文件名后面的序号的确定与系统侦测到磁盘的顺序有关,而与磁盘所插入的插槽位置无关。
-# 三、分区
+# 三、分区
-## 分区表
+## 分区表
-磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。
+磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。
-### 1. MBR
+### 1. MBR
-MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中 MBR 占 446 bytes,分区表占 64 bytes。
+MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中 MBR 占 446 bytes,分区表占 64 bytes。
-分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它将其它扇区用来记录分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。
+分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它将其它扇区用来记录分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。
-Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名 + 编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。
+Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名 + 编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。
-### 2. GPT
+### 2. GPT
-不同的磁盘有不同的扇区大小,例如 512 bytes 和最新磁盘的 4 k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA),LBA 默认大小为 512 bytes。
+不同的磁盘有不同的扇区大小,例如 512 bytes 和最新磁盘的 4 k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA),LBA 默认大小为 512 bytes。
-GPT 第 1 个区块记录了 MBR,紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录,这个部份纪录了分区表本身的位置与大小和备份分区的位置,同时放置了分区表的校验码 (CRC32),操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误,可以使用备份分区进行恢复。
+GPT 第 1 个区块记录了 MBR,紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录,这个部份纪录了分区表本身的位置与大小和备份分区的位置,同时放置了分区表的校验码 (CRC32),操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误,可以使用备份分区进行恢复。
-GPT 没有扩展分区概念,都是主分区,每个 LAB 可以分 4 个分区,因此总共可以分 4 * 32 = 128 个分区。
+GPT 没有扩展分区概念,都是主分区,每个 LAB 可以分 4 个分区,因此总共可以分 4 * 32 = 128 个分区。
-MBR 不支持 2.2 TB 以上的硬盘,GPT 则最多支持到 233 TB = 8 ZB。
+MBR 不支持 2.2 TB 以上的硬盘,GPT 则最多支持到 233 TB = 8 ZB。
-
+
-## 开机检测程序
+## 开机检测程序
-### 1. BIOS
+### 1. BIOS
-BIOS(Basic Input/Output System,基本输入输出系统),它是一个固件(嵌入在硬件中的软件),BIOS 程序存放在断电后内容不会丢失的只读内存中。
+BIOS(Basic Input/Output System,基本输入输出系统),它是一个固件(嵌入在硬件中的软件),BIOS 程序存放在断电后内容不会丢失的只读内存中。
-BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的 MBR,由 MBR 执行其中的开机管理程序,这个开机管理程序会加载操作系统的核心文件。
+BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的 MBR,由 MBR 执行其中的开机管理程序,这个开机管理程序会加载操作系统的核心文件。
-
+![](index_files/50831a6f-2777-46ea-a571-29f23c85cc21.jpg)
-MBR 中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动 MBR 中的开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。
+MBR 中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动 MBR 中的开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。
-下图中,第一扇区的 MBR 中的开机管理程序提供了两个选单:M1、M2,M1 指向了 Windows 操作系统,而 M2 指向其它分区的启动扇区,里面包含了另外一个开机管理程序,提供了一个指向 Linux 的选单。
+下图中,第一扇区的 MBR 中的开机管理程序提供了两个选单:M1、M2,M1 指向了 Windows 操作系统,而 M2 指向其它分区的启动扇区,里面包含了另外一个开机管理程序,提供了一个指向 Linux 的选单。
-
+
-安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉 MBR,而 Linux 可以选择将开机管理程序安装在 MBR 或者其它分区的启动扇区,并且可以设置开机管理程序的选单。
+安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉 MBR,而 Linux 可以选择将开机管理程序安装在 MBR 或者其它分区的启动扇区,并且可以设置开机管理程序的选单。
-### 2. UEFI
+### 2. UEFI
-BIOS 不可以读取 GPT 分区表,而 UEFI 可以。
+BIOS 不可以读取 GPT 分区表,而 UEFI 可以。
-# 四、文件系统
+# 四、文件系统
-## 分区与文件系统
+## 分区与文件系统
对分区进行格式化是为了在分区上建立文件系统。一个分区通常只能格式化为一个文件系统,但是磁盘阵列等技术可以将一个分区格式化为多个文件系统。
-## 组成
+## 组成
-
+
最主要的几个组成部分如下:
-- inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号;
-- block:记录文件的内容,文件太大时,会占用多个 block。
+- inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号;
+- block:记录文件的内容,文件太大时,会占用多个 block。
除此之外还包括:
-- superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等;
-- block bitmap:记录 block 是否被使用的位域;
+- superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等;
+- block bitmap:记录 block 是否被使用的位域;
-## 文件读取
+## 文件读取
-对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。
+对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。
-
+![](index_files/83185315-793a-453a-a927-5e8d92b5c0ef.jpg)
-而对于 FAT 文件系统,它没有 inode,每个 block 中存储着下一个 block 的编号。
+而对于 FAT 文件系统,它没有 inode,每个 block 中存储着下一个 block 的编号。
-
+![](index_files/075e1977-7846-4928-96c8-bb5b0268693c.jpg)
-## 磁盘碎片
+## 磁盘碎片
-指一个文件内容所在的 block 过于分散。
+指一个文件内容所在的 block 过于分散。
-## block
+## block
-在 Ext2 文件系统中所支持的 block 大小有 1K,2K 及 4K 三种,不同的大小限制了单个文件和文件系统的最大大小。
+在 Ext2 文件系统中所支持的 block 大小有 1K,2K 及 4K 三种,不同的大小限制了单个文件和文件系统的最大大小。
-| 大小 | 1KB | 2KB | 4KB |
-| :---: | :---: | :---: | :---: |
-| 最大单一文件 | 16GB | 256GB | 2TB |
-| 最大文件系统 | 2TB | 8TB | 16TB |
+| 大小 | 1KB | 2KB | 4KB |
+| :---: | :---: | :---: | :---: |
+| 最大单一文件 | 16GB | 256GB | 2TB |
+| 最大文件系统 | 2TB | 8TB | 16TB |
-一个 block 只能被一个文件所使用,未使用的部分直接浪费了。因此如果需要存储大量的小文件,那么最好选用比较小的 block。
+一个 block 只能被一个文件所使用,未使用的部分直接浪费了。因此如果需要存储大量的小文件,那么最好选用比较小的 block。
-## inode
+## inode
-inode 具体包含以下信息:
+inode 具体包含以下信息:
-- 权限 (read/write/excute);
-- 拥有者与群组 (owner/group);
-- 容量;
-- 建立或状态改变的时间 (ctime);
-- 最近一次的读取时间 (atime);
-- 最近修改的时间 (mtime);
-- 定义文件特性的旗标 (flag),如 SetUID...;
-- 该文件真正内容的指向 (pointer)。
+- 权限 (read/write/excute);
+- 拥有者与群组 (owner/group);
+- 容量;
+- 建立或状态改变的时间 (ctime);
+- 最近一次的读取时间 (atime);
+- 最近修改的时间 (mtime);
+- 定义文件特性的旗标 (flag),如 SetUID...;
+- 该文件真正内容的指向 (pointer)。
-inode 具有以下特点:
+inode 具有以下特点:
-- 每个 inode 大小均固定为 128 bytes (新的 ext4 与 xfs 可设定到 256 bytes);
-- 每个文件都仅会占用一个 inode。
+- 每个 inode 大小均固定为 128 bytes (新的 ext4 与 xfs 可设定到 256 bytes);
+- 每个文件都仅会占用一个 inode。
-inode 中记录了文件内容所在的 block 编号,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用是指,让 inode 记录的引用 block 块当成 inode 用来记录引用信息。
+inode 中记录了文件内容所在的 block 编号,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用是指,让 inode 记录的引用 block 块当成 inode 用来记录引用信息。
-
+
-## 目录
+## 目录
-建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。
+建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。
-## 日志
+## 日志
-如果突然断电,那么文件系统会发生错误,例如断电前只修改了 block bitmap,而还没有将数据真正写入 block 中。
+如果突然断电,那么文件系统会发生错误,例如断电前只修改了 block bitmap,而还没有将数据真正写入 block 中。
-ext3/ext4 文件系统引入了日志功能,可以利用日志来修复文件系统。
+ext3/ext4 文件系统引入了日志功能,可以利用日志来修复文件系统。
-## 挂载
+## 挂载
挂载利用目录作为文件系统的进入点,也就是说,进入目录之后就可以读取文件系统的数据。
-## 目录配置
+## 目录配置
-为了使不同 Linux 发行版本的目录结构保持一致性,Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下:
+为了使不同 Linux 发行版本的目录结构保持一致性,Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下:
-- / (root, 根目录)
-- /usr (unix software resource):所有系统默认软件都会安装到这个目录;
-- /var (variable):存放系统或程序运行过程中的数据文件。
+- / (root, 根目录)
+- /usr (unix software resource):所有系统默认软件都会安装到这个目录;
+- /var (variable):存放系统或程序运行过程中的数据文件。
-
+
-# 五、文件
+# 五、文件
-## 文件属性
+## 文件属性
用户分为三种:文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。
-使用 ls 查看一个文件时,会显示一个文件的信息,例如 `drwxr-xr-x. 3 root root 17 May 6 00:14 .config`,对这个信息的解释如下:
+使用 ls 查看一个文件时,会显示一个文件的信息,例如 `drwxr-xr-x. 3 root root 17 May 6 00:14 .config`,对这个信息的解释如下:
-- drwxr-xr-x:文件类型以及权限,第 1 位为文件类型字段,后 9 位为文件权限字段。
-- 3:链接数;
-- root:文件拥有者;
-- root:所属群组;
-- 17:文件大小;
-- May 6 00:14:文件最后被修改的时间;
-- .config:文件名。
+- drwxr-xr-x:文件类型以及权限,第 1 位为文件类型字段,后 9 位为文件权限字段。
+- 3:链接数;
+- root:文件拥有者;
+- root:所属群组;
+- 17:文件大小;
+- May 6 00:14:文件最后被修改的时间;
+- .config:文件名。
常见的文件类型及其含义有:
-- d:目录;
-- -:文件;
-- l:链接文件;
+- d:目录;
+- -:文件;
+- l:链接文件;
-9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。
+9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。
文件时间有以下三种:
-- modification time (mtime):文件的内容更新就会更新;
-- status time (ctime):文件的状态(权限、属性)更新就会更新;
-- access time (atime):读取文件时就会更新。
+- modification time (mtime):文件的内容更新就会更新;
+- status time (ctime):文件的状态(权限、属性)更新就会更新;
+- access time (atime):读取文件时就会更新。
-## 文件与目录的基本操作
+## 文件与目录的基本操作
-### 1. ls
+### 1. ls
列出文件或者目录的信息,目录的信息就是其中包含的文件。
```html
-# ls [-aAdfFhilnrRSt] file|dir
--a :列出全部的文件
--d :仅列出目录本身
--l :以长数据串行列出,包含文件的属性与权限等等数据
+# ls [-aAdfFhilnrRSt] file|dir
+-a :列出全部的文件
+-d :仅列出目录本身
+-l :以长数据串行列出,包含文件的属性与权限等等数据
```
-### 2. cd
+### 2. cd
更换当前目录。
```
-cd [相对路径或绝对路径]
+cd [相对路径或绝对路径]
```
-### 3. mkdir
+### 3. mkdir
创建目录。
```
-# mkdir [-mp] 目录名称
--m :配置目录权限
--p :递归创建目录
+# mkdir [-mp] 目录名称
+-m :配置目录权限
+-p :递归创建目录
```
-### 4. rmdir
+### 4. rmdir
删除目录,必须为空。
```html
-rmdir [-p] 目录名称
--p :递归删除目录
+rmdir [-p] 目录名称
+-p :递归删除目录
```
-### 5. touch
+### 5. touch
更新文件时间或者建立新文件。
```html
-# touch [-acdmt] filename
--a : 更新 atime
--c : 更新 ctime,若该文件不存在则不建立新文件
--m : 更新 mtime
--d : 后面可以接更新日期而不使用当前日期,也可以使用 --date="日期或时间"
--t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm]
+# touch [-acdmt] filename
+-a : 更新 atime
+-c : 更新 ctime,若该文件不存在则不建立新文件
+-m : 更新 mtime
+-d : 后面可以接更新日期而不使用当前日期,也可以使用 --date="日期或时间"
+-t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm]
```
-### 6. cp
+### 6. cp
复制文件。
如果源文件有两个以上,则目的文件一定要是目录才行。
```html
-cp [-adfilprsu] source destination
--a :相当于 -dr --preserve=all 的意思,至于 dr 请参考下列说明
--d :若来源文件为链接文件,则复制链接文件属性而非文件本身
--i :若目标文件已经存在时,在覆盖前会先询问
--p :连同文件的属性一起复制过去
--r :递归持续复制
--u :destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制
---preserve=all :除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了
+cp [-adfilprsu] source destination
+-a :相当于 -dr --preserve=all 的意思,至于 dr 请参考下列说明
+-d :若来源文件为链接文件,则复制链接文件属性而非文件本身
+-i :若目标文件已经存在时,在覆盖前会先询问
+-p :连同文件的属性一起复制过去
+-r :递归持续复制
+-u :destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制
+--preserve=all :除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了
```
-### 7. rm
+### 7. rm
删除文件。
```html
-# rm [-fir] 文件或目录
--r :递归删除
+# rm [-fir] 文件或目录
+-r :递归删除
```
-### 8. mv
+### 8. mv
移动文件。
```html
-# mv [-fiu] source destination
-# mv [options] source1 source2 source3 .... directory
--f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖
+# mv [-fiu] source destination
+# mv [options] source1 source2 source3 .... directory
+-f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖
```
-## 修改权限
+## 修改权限
-可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1,即每个权限对应的数字权值为 r : 4、w : 2、x : 1。
+可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1,即每个权限对应的数字权值为 r : 4、w : 2、x : 1。
```html
-# chmod [-R] xyz dirname/filename
+# chmod [-R] xyz dirname/filename
```
-范例:将 .bashrc 文件的权限修改为 -rwxr-xr--。
+范例:将 .bashrc 文件的权限修改为 -rwxr-xr--。
```html
-# chmod 754 .bashrc
+# chmod 754 .bashrc
```
也可以使用符号来设定权限。
```html
-# chmod [ugoa] [+-=] [rwx] dirname/filename
-- u:拥有者
-- g:所属群组
-- o:其他人
-- a:所有人
-- +:添加权限
-- -:移除权限
-- =:设定权限
+# chmod [ugoa] [+-=] [rwx] dirname/filename
+- u:拥有者
+- g:所属群组
+- o:其他人
+- a:所有人
+- +:添加权限
+- -:移除权限
+- =:设定权限
```
-范例:为 .bashrc 文件的所有用户添加写权限。
+范例:为 .bashrc 文件的所有用户添加写权限。
```html
-# chmod a+w .bashrc
+# chmod a+w .bashrc
```
-## 文件默认权限
+## 文件默认权限
-- 文件默认权限:文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw- 。
-- 目录默认权限:目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。
+- 文件默认权限:文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw- 。
+- 目录默认权限:目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。
-可以通过 umask 设置或者查看文件的默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r--。
+可以通过 umask 设置或者查看文件的默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r--。
-## 目录的权限
+## 目录的权限
-文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。
+文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。
-目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。
+目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。
-## 链接
+## 链接
```html
-# ln [-sf] source_filename dist_filename
--s :默认是 hard link,加 -s 为 symbolic link
--f :如果目标文件存在时,先删除目标文件
+# ln [-sf] source_filename dist_filename
+-s :默认是 hard link,加 -s 为 symbolic link
+-f :如果目标文件存在时,先删除目标文件
```
-### 1. 实体链接
+### 1. 实体链接
-它和普通文件类似,实体链接文件的 inode 都指向源文件所在的 block 上,也就是说读取文件直接从源文件的 block 上读取。
+它和普通文件类似,实体链接文件的 inode 都指向源文件所在的 block 上,也就是说读取文件直接从源文件的 block 上读取。
-删除任意一个条目,文件还是存在,只要引用数量不为 0。
+删除任意一个条目,文件还是存在,只要引用数量不为 0。
-有以下限制:不能跨越 File System、不能对目录进行链接。
+有以下限制:不能跨越 File System、不能对目录进行链接。
```html
-# ln /etc/crontab .
-# ll -i /etc/crontab crontab
-34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab
-34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab
+# ln /etc/crontab .
+# ll -i /etc/crontab crontab
+34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab
+34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab
```
-### 2. 符号链接
+### 2. 符号链接
-符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 Windows 的快捷方式。
+符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 Windows 的快捷方式。
当源文件被删除了,链接文件就打不开了。
可以为目录建立链接。
```html
-# ll -i /etc/crontab /root/crontab2
-34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab
-53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab
+# ll -i /etc/crontab /root/crontab2
+34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab
+53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab
```
-## 获取文件内容
+## 获取文件内容
-### 1. cat
+### 1. cat
取得文件内容。
```html
-# cat [-AbEnTv] filename
--n :打印出行号,连同空白行也会有行号,-b 不会
+# cat [-AbEnTv] filename
+-n :打印出行号,连同空白行也会有行号,-b 不会
```
-### 2. tac
+### 2. tac
-是 cat 的反向操作,从最后一行开始打印。
+是 cat 的反向操作,从最后一行开始打印。
-### 3. more
+### 3. more
-和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。
+和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。
-### 4. less
+### 4. less
-和 more 类似,但是多了一个向前翻页的功能。
+和 more 类似,但是多了一个向前翻页的功能。
-### 5. head
+### 5. head
取得文件前几行。
```html
-# head [-n number] filename
--n :后面接数字,代表显示几行的意思
+# head [-n number] filename
+-n :后面接数字,代表显示几行的意思
```
-### 6. tail
+### 6. tail
-是 head 的反向操作,只是取得是后几行。
+是 head 的反向操作,只是取得是后几行。
-### 7. od
+### 7. od
以字符或者十六进制的形式显示二进制文件。
-## 指令与文件搜索
+## 指令与文件搜索
-### 1. which
+### 1. which
指令搜索。
```html
-# which [-a] command
--a :将所有指令列出,而不是只列第一个
+# which [-a] command
+-a :将所有指令列出,而不是只列第一个
```
-### 2. whereis
+### 2. whereis
文件搜索。速度比较快,因为它只搜索几个特定的目录。
```html
-# whereis [-bmsu] dirname/filename
+# whereis [-bmsu] dirname/filename
```
-### 3. locate
+### 3. locate
文件搜索。可以用关键字或者正则表达式进行搜索。
-locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。
+locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。
```html
-# locate [-ir] keyword
+# locate [-ir] keyword
-r:正则表达式
```
-### 4. find
+### 4. find
文件搜索。可以使用文件的属性和权限进行搜索。
```html
-# find [basedir] [option]
-example: find . -name "shadow*"
+# find [basedir] [option]
+example: find . -name "shadow*"
```
(一)与时间有关的选项
```html
--mtime n :列出在 n 天前的那一天修改过内容的文件
--mtime +n :列出在 n 天之前 (不含 n 天本身) 修改过内容的文件
--mtime -n :列出在 n 天之内 (含 n 天本身) 修改过内容的文件
--newer file : 列出比 file 更新的文件
+-mtime n :列出在 n 天前的那一天修改过内容的文件
+-mtime +n :列出在 n 天之前 (不含 n 天本身) 修改过内容的文件
+-mtime -n :列出在 n 天之内 (含 n 天本身) 修改过内容的文件
+-newer file : 列出比 file 更新的文件
```
-+4、4 和 -4 的指示的时间范围如下:
++4、4 和 -4 的指示的时间范围如下:
-
+
(二)与文件拥有者和所属群组有关的选项
```html
--uid n
--gid n
--user name
--group name
--nouser :搜索拥有者不存在 /etc/passwd 的文件
--nogroup:搜索所属群组不存在于 /etc/group 的文件
+-uid n
+-gid n
+-user name
+-group name
+-nouser :搜索拥有者不存在 /etc/passwd 的文件
+-nogroup:搜索所属群组不存在于 /etc/group 的文件
```
(三)与文件权限和名称有关的选项
```html
--name filename
--size [+-]SIZE:搜寻比 SIZE 还要大 (+) 或小 (-) 的文件。这个 SIZE 的规格有:c: 代表 byte,k: 代表 1024bytes。所以,要找比 50KB 还要大的文件,就是 -size +50k
--type TYPE
--perm mode :搜索权限等于 mode 的文件
--perm -mode :搜索权限包含 mode 的文件
--perm /mode :搜索权限包含任一 mode 的文件
+-name filename
+-size [+-]SIZE:搜寻比 SIZE 还要大 (+) 或小 (-) 的文件。这个 SIZE 的规格有:c: 代表 byte,k: 代表 1024bytes。所以,要找比 50KB 还要大的文件,就是 -size +50k
+-type TYPE
+-perm mode :搜索权限等于 mode 的文件
+-perm -mode :搜索权限包含 mode 的文件
+-perm /mode :搜索权限包含任一 mode 的文件
```
-# 六、压缩与打包
+# 六、压缩与打包
-## 压缩文件名
+## 压缩文件名
-Linux 底下有很多压缩文件名,常见的如下:
+Linux 底下有很多压缩文件名,常见的如下:
-| 扩展名 | 压缩程序 |
-| -- | -- |
-| \*.Z | compress |
-|\*.zip | zip |
-|\*.gz | gzip|
-|\*.bz2 | bzip2 |
-|\*.xz | xz |
-|\*.tar | tar 程序打包的数据,没有经过压缩 |
-|\*.tar.gz | tar 程序打包的文件,经过 gzip 的压缩 |
-|\*.tar.bz2 | tar 程序打包的文件,经过 bzip2 的压缩 |
-|\*.tar.xz | tar 程序打包的文件,经过 xz 的压缩 |
+| 扩展名 | 压缩程序 |
+| -- | -- |
+| \*.Z | compress |
+|\*.zip | zip |
+|\*.gz | gzip|
+|\*.bz2 | bzip2 |
+|\*.xz | xz |
+|\*.tar | tar 程序打包的数据,没有经过压缩 |
+|\*.tar.gz | tar 程序打包的文件,经过 gzip 的压缩 |
+|\*.tar.bz2 | tar 程序打包的文件,经过 bzip2 的压缩 |
+|\*.tar.xz | tar 程序打包的文件,经过 xz 的压缩 |
-## 压缩指令
+## 压缩指令
-### 1. gzip
+### 1. gzip
-gzip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。
+gzip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。
-经过 gzip 压缩过,源文件就不存在了。
+经过 gzip 压缩过,源文件就不存在了。
-有 9 个不同的压缩等级可以使用。
+有 9 个不同的压缩等级可以使用。
-可以使用 zcat、zmore、zless 来读取压缩文件的内容。
+可以使用 zcat、zmore、zless 来读取压缩文件的内容。
```html
-$ gzip [-cdtv#] filename
--c :将压缩的数据输出到屏幕上
--d :解压缩
--t :检验压缩文件是否出错
--v :显示压缩比等信息
--# : # 为数字的意思,代表压缩等级,数字越大压缩比越高,默认为 6
+$ gzip [-cdtv#] filename
+-c :将压缩的数据输出到屏幕上
+-d :解压缩
+-t :检验压缩文件是否出错
+-v :显示压缩比等信息
+-# : # 为数字的意思,代表压缩等级,数字越大压缩比越高,默认为 6
```
-### 2. bzip2
+### 2. bzip2
-提供比 gzip 更高的压缩比。
+提供比 gzip 更高的压缩比。
查看命令:bzcat、bzmore、bzless、bzgrep。
```html
-$ bzip2 [-cdkzv#] filename
--k :保留源文件
+$ bzip2 [-cdkzv#] filename
+-k :保留源文件
```
-### 3. xz
+### 3. xz
-提供比 bzip2 更佳的压缩比。
+提供比 bzip2 更佳的压缩比。
-可以看到,gzip、bzip2、xz 的压缩比不断优化。不过要注意的是,压缩比越高,压缩的时间也越长。
+可以看到,gzip、bzip2、xz 的压缩比不断优化。不过要注意的是,压缩比越高,压缩的时间也越长。
查看命令:xzcat、xzmore、xzless、xzgrep。
```html
-$ xz [-dtlkc#] filename
+$ xz [-dtlkc#] filename
```
-## 打包
+## 打包
-压缩指令只能对一个文件进行压缩,而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gip、bzip2、xz 将打包文件进行压缩。
+压缩指令只能对一个文件进行压缩,而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gip、bzip2、xz 将打包文件进行压缩。
```html
-$ tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename... ==打包压缩
-$ tar [-z|-j|-J] [tv] [-f 已有的 tar 文件] ==查看
-$ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩
--z :使用 zip;
--j :使用 bzip2;
--J :使用 xz;
--c :新建打包文件;
--t :查看打包文件里面有哪些文件;
--x :解打包或解压缩的功能;
--v :在压缩/解压缩的过程中,显示正在处理的文件名;
--f : filename:要处理的文件;
--C 目录 : 在特定目录解压缩。
+$ tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename... ==打包压缩
+$ tar [-z|-j|-J] [tv] [-f 已有的 tar 文件] ==查看
+$ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩
+-z :使用 zip;
+-j :使用 bzip2;
+-J :使用 xz;
+-c :新建打包文件;
+-t :查看打包文件里面有哪些文件;
+-x :解打包或解压缩的功能;
+-v :在压缩/解压缩的过程中,显示正在处理的文件名;
+-f : filename:要处理的文件;
+-C 目录 : 在特定目录解压缩。
```
-| 使用方式 | 命令 |
-| :---: | --- |
-| 打包压缩 | tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称 |
-| 查 看 | tar -jtv -f filename.tar.bz2 |
-| 解压缩 | tar -jxv -f filename.tar.bz2 -C 要解压缩的目录 |
+| 使用方式 | 命令 |
+| :---: | --- |
+| 打包压缩 | tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称 |
+| 查 看 | tar -jtv -f filename.tar.bz2 |
+| 解压缩 | tar -jxv -f filename.tar.bz2 -C 要解压缩的目录 |
-# 七、Bash
+# 七、Bash
-可以通过 Shell 请求内核提供服务,Bash 正是 Shell 的一种。
+可以通过 Shell 请求内核提供服务,Bash 正是 Shell 的一种。
-## 特性
+## 特性
-- 命令历史:记录使用过的命令。本次登录所执行的命令都会暂时存放到内存中,\~/.bash_history 文件中记录的是前一次登录所执行过的命令。
+- 命令历史:记录使用过的命令。本次登录所执行的命令都会暂时存放到内存中,~/.bash_history 文件中记录的是前一次登录所执行过的命令。
-- 命令与文件补全:快捷键:tab。
+- 命令与文件补全:快捷键:tab。
-- 命名别名:例如 lm 是 ls -al 的别名。
+- 命名别名:例如 lm 是 ls -al 的别名。
-- shell scripts。
+- shell scripts。
-- 通配符:例如 ls -l /usr/bin/X\* 列出 /usr/bin 下面所有以 X 开头的文件。
+- 通配符:例如 ls -l /usr/bin/X\* 列出 /usr/bin 下面所有以 X 开头的文件。
-## 变量操作
+## 变量操作
-对一个变量赋值直接使用 =。
+对一个变量赋值直接使用 =。
-对变量取用需要在变量前加上 \$ ,也可以用 \${} 的形式;
+对变量取用需要在变量前加上 \$ ,也可以用 \${} 的形式;
-输出变量使用 echo 命令。
+输出变量使用 echo 命令。
```bash
-$ var=abc
-$ echo $var
-$ echo ${var}
+$ var=abc
+$ echo $var
+$ echo ${var}
```
变量内容如果有空格,必须使用双引号或者单引号。
-- 双引号内的特殊字符可以保留原本特性,例如 var="lang is \$LANG",则 var 的值为 lang is zh_TW.UTF-8;
-- 单引号内的特殊字符就是特殊字符本身,例如 var='lang is \$LANG',则 var 的值为 lang is \$LANG。
+- 双引号内的特殊字符可以保留原本特性,例如 var="lang is \$LANG",则 var 的值为 lang is zh_TW.UTF-8;
+- 单引号内的特殊字符就是特殊字符本身,例如 var='lang is \$LANG',则 var 的值为 lang is \$LANG。
-可以使用 \`指令\` 或者 \$(指令) 的方式将指令的执行结果赋值给变量。例如 version=\$(uname -r),则 version 的值为 3.10.0-229.el7.x86_64。
+可以使用 \`指令\` 或者 \$(指令) 的方式将指令的执行结果赋值给变量。例如 version=\$(uname -r),则 version 的值为 3.10.0-229.el7.x86_64。
-可以使用 export 命令将自定义变量转成环境变量,环境变量可以在子程序中使用,所谓子程序就是由当前 Bash 而产生的子 Bash。
+可以使用 export 命令将自定义变量转成环境变量,环境变量可以在子程序中使用,所谓子程序就是由当前 Bash 而产生的子 Bash。
-Bash 的变量可以声明为数组和整数数字。注意数字类型没有浮点数。如果不进行声明,默认是字符串类型。变量的声明使用 declare 命令:
+Bash 的变量可以声明为数组和整数数字。注意数字类型没有浮点数。如果不进行声明,默认是字符串类型。变量的声明使用 declare 命令:
```html
-$ declare [-aixr] variable
--a : 定义为数组类型
--i : 定义为整数类型
--x : 定义为环境变量
--r : 定义为 readonly 类型
+$ declare [-aixr] variable
+-a : 定义为数组类型
+-i : 定义为整数类型
+-x : 定义为环境变量
+-r : 定义为 readonly 类型
```
-使用 [ ] 来对数组进行索引操作:
+使用 [ ] 来对数组进行索引操作:
```bash
-$ array[1]=a
-$ array[2]=b
-$ echo ${array[1]}
+$ array[1]=a
+$ array[2]=b
+$ echo ${array[1]}
```
-## 指令搜索顺序
+## 指令搜索顺序
-- 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls ;
-- 由别名找到该指令来执行;
-- 由 Bash 内建的指令来执行;
-- 按 \$PATH 变量指定的搜索路径的顺序找到第一个指令来执行。
+- 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls ;
+- 由别名找到该指令来执行;
+- 由 Bash 内建的指令来执行;
+- 按 \$PATH 变量指定的搜索路径的顺序找到第一个指令来执行。
-## 数据流重定向
+## 数据流重定向
重定向指的是使用文件代替标准输入、标准输出和标准错误输出。
-| 1 | 代码 | 运算符 |
-| :---: | :---: | :---:|
-| 标准输入 (stdin) | 0 | < 或 << |
-| 标准输出 (stdout) | 1 | > 或 >> |
-| 标准错误输出 (stderr) | 2 | 2> 或 2>> |
+| 1 | 代码 | 运算符 |
+| :---: | :---: | :---:|
+| 标准输入 (stdin) | 0 | < 或 << |
+| 标准输出 (stdout) | 1 | > 或 >> |
+| 标准错误输出 (stderr) | 2 | 2> 或 2>> |
其中,有一个箭头的表示以覆盖的方式重定向,而有两个箭头的表示以追加的方式重定向。
-可以将不需要的标准输出以及标准错误输出重定向到 /dev/null,相当于扔进垃圾箱。
+可以将不需要的标准输出以及标准错误输出重定向到 /dev/null,相当于扔进垃圾箱。
-如果需要将标准输出以及标准错误输出同时重定向到一个文件,需要将某个输出转换为另一个输出,例如 2>&1 表示将标准错误输出转换为标准输出。
+如果需要将标准输出以及标准错误输出同时重定向到一个文件,需要将某个输出转换为另一个输出,例如 2>&1 表示将标准错误输出转换为标准输出。
```bash
-$ find /home -name .bashrc > list 2>&1
+$ find /home -name .bashrc > list 2>&1
```
-# 八、管线指令
+# 八、管线指令
-管线是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管线。在命令之间使用 | 分隔各个管线命令。
+管线是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管线。在命令之间使用 | 分隔各个管线命令。
```bash
-$ ls -al /etc | less
+$ ls -al /etc | less
```
-## 提取指令
+## 提取指令
-cut 对数据进行切分,取出想要的部分。切分过程一行一行地进行。
+cut 对数据进行切分,取出想要的部分。切分过程一行一行地进行。
```html
-$ cut
--d :分隔符
--f :经过 -d 分隔后,使用 -f n 取出第 n 个区间
--c :以字符为单位取出区间
+$ cut
+-d :分隔符
+-f :经过 -d 分隔后,使用 -f n 取出第 n 个区间
+-c :以字符为单位取出区间
```
-范例 1:last 将显示的登入者的信息,要求仅显示用户名。
+范例 1:last 将显示的登入者的信息,要求仅显示用户名。
```html
-$ last
-root pts/1 192.168.201.101 Sat Feb 7 12:35 still logged in
-root pts/1 192.168.201.101 Fri Feb 6 12:13 - 18:46 (06:33)
-root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16)
+$ last
+root pts/1 192.168.201.101 Sat Feb 7 12:35 still logged in
+root pts/1 192.168.201.101 Fri Feb 6 12:13 - 18:46 (06:33)
+root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16)
-$ last | cut -d ' ' -f 1
+$ last | cut -d ' ' -f 1
```
-范例 2:将 export 输出的讯息,取得第 12 字符以后的所有字符串。
+范例 2:将 export 输出的讯息,取得第 12 字符以后的所有字符串。
```html
-$ export
-declare -x HISTCONTROL="ignoredups"
-declare -x HISTSIZE="1000"
-declare -x HOME="/home/dmtsai"
-declare -x HOSTNAME="study.centos.vbird"
+$ export
+declare -x HISTCONTROL="ignoredups"
+declare -x HISTSIZE="1000"
+declare -x HOME="/home/dmtsai"
+declare -x HOSTNAME="study.centos.vbird"
.....(其他省略).....
-$ export | cut -c 12
+$ export | cut -c 12
```
-## 排序指令
+## 排序指令
-**sort** 进行排序。
+**sort** 进行排序。
```html
-$ sort [-fbMnrtuk] [file or stdin]
--f :忽略大小写
--b :忽略最前面的空格
--M :以月份的名字来排序,例如 JAN,DEC
--n :使用数字
--r :反向排序
--u :相当于 unique,重复的内容只出现一次
--t :分隔符,默认为 tab
--k :指定排序的区间
+$ sort [-fbMnrtuk] [file or stdin]
+-f :忽略大小写
+-b :忽略最前面的空格
+-M :以月份的名字来排序,例如 JAN,DEC
+-n :使用数字
+-r :反向排序
+-u :相当于 unique,重复的内容只出现一次
+-t :分隔符,默认为 tab
+-k :指定排序的区间
```
-范例:/etc/passwd 文件内容以 : 来分隔,要求以第三列进行排序。
+范例:/etc/passwd 文件内容以 : 来分隔,要求以第三列进行排序。
```html
-$ cat /etc/passwd | sort -t ':' -k 3
+$ cat /etc/passwd | sort -t ':' -k 3
root:x:0:0:root:/root:/bin/bash
dmtsai:x:1000:1000:dmtsai:/home/dmtsai:/bin/bash
alex:x:1001:1002::/home/alex:/bin/bash
arod:x:1002:1003::/home/arod:/bin/bash
```
-**uniq** 可以将重复的数据只取一个。
+**uniq** 可以将重复的数据只取一个。
```html
-$ uniq [-ic]
--i :忽略大小写
--c :进行计数
+$ uniq [-ic]
+-i :忽略大小写
+-c :进行计数
```
范例:取得每个人的登录总次数
```html
-$ last | cut -d ' ' -f 1 | sort | uniq -c
+$ last | cut -d ' ' -f 1 | sort | uniq -c
1
-6 (unknown
-47 dmtsai
-4 reboot
-7 root
-1 wtmp
+6 (unknown
+47 dmtsai
+4 reboot
+7 root
+1 wtmp
```
-## 双向输出重定向
+## 双向输出重定向
-输出重定向会将输出内容重定向到文件中,而 **tee** 不仅能够完成这个功能,还能保留屏幕上的输出。也就是说,使用 tee 指令,一个输出会同时传送到文件和屏幕上。
+输出重定向会将输出内容重定向到文件中,而 **tee** 不仅能够完成这个功能,还能保留屏幕上的输出。也就是说,使用 tee 指令,一个输出会同时传送到文件和屏幕上。
```html
-$ tee [-a] file
+$ tee [-a] file
```
-## 字符转换指令
+## 字符转换指令
-**tr** 用来删除一行中的字符,或者对字符进行替换。
+**tr** 用来删除一行中的字符,或者对字符进行替换。
```html
-$ tr [-ds] SET1 ...
--d : 删除行中 SET1 这个字符串
+$ tr [-ds] SET1 ...
+-d : 删除行中 SET1 这个字符串
```
-范例,将 last 输出的信息所有小写转换为大写。
+范例,将 last 输出的信息所有小写转换为大写。
```html
-$ last | tr '[a-z]' '[A-Z]'
+$ last | tr '[a-z]' '[A-Z]'
```
- **col** 将 tab 字符转为空格字符。
+ **col** 将 tab 字符转为空格字符。
```html
-$ col [-xb]
--x : 将 tab 键转换成对等的空格键
+$ col [-xb]
+-x : 将 tab 键转换成对等的空格键
```
-**expand** 将 tab 转换一定数量的空格,默认是 8 个。
+**expand** 将 tab 转换一定数量的空格,默认是 8 个。
```html
-$ expand [-t] file
--t :tab 转为空格的数量
+$ expand [-t] file
+-t :tab 转为空格的数量
```
-**join** 将有相同数据的那一行合并在一起。
+**join** 将有相同数据的那一行合并在一起。
```html
-$ join [-ti12] file1 file2
--t :分隔符,默认为空格
--i :忽略大小写的差异
--1 :第一个文件所用的比较字段
--2 :第二个文件所用的比较字段
+$ join [-ti12] file1 file2
+-t :分隔符,默认为空格
+-i :忽略大小写的差异
+-1 :第一个文件所用的比较字段
+-2 :第二个文件所用的比较字段
```
-**paste** 直接将两行粘贴在一起。
+**paste** 直接将两行粘贴在一起。
```html
-$ paste [-d] file1 file2
--d :分隔符,默认为 tab
+$ paste [-d] file1 file2
+-d :分隔符,默认为 tab
```
-## 分区指令
+## 分区指令
-**split** 将一个文件划分成多个文件。
+**split** 将一个文件划分成多个文件。
```html
-$ split [-bl] file PREFIX
--b :以大小来进行分区,可加单位,例如 b, k, m 等
--l :以行数来进行分区。
-- PREFIX :分区文件的前导名称
+$ split [-bl] file PREFIX
+-b :以大小来进行分区,可加单位,例如 b, k, m 等
+-l :以行数来进行分区。
+- PREFIX :分区文件的前导名称
```
-# 九、正则表达式
+# 九、正则表达式
-## grep
+## grep
使用正则表示式把匹配的行提取出来。
```html
-$ grep [-acinv] [--color=auto] 搜寻字符串 filename
--a : 将 binary 文件以 text 文件的方式进行搜寻
--c : 计算找到个数
--i : 忽略大小写
--n : 输出行号
--v : 反向选择,亦即显示出没有 搜寻字符串 内容的那一行
---color=auto :找到的关键字加颜色显示
+$ grep [-acinv] [--color=auto] 搜寻字符串 filename
+-a : 将 binary 文件以 text 文件的方式进行搜寻
+-c : 计算找到个数
+-i : 忽略大小写
+-n : 输出行号
+-v : 反向选择,亦即显示出没有 搜寻字符串 内容的那一行
+--color=auto :找到的关键字加颜色显示
```
-范例:把含有 the 字符串的行提取出来(注意默认会有 --color=auto 选项,因此以下内容在 Linux 中有颜色显示 the 字符串)
+范例:把含有 the 字符串的行提取出来(注意默认会有 --color=auto 选项,因此以下内容在 Linux 中有颜色显示 the 字符串)
```html
-$ grep -n 'the' regular_express.txt
-8:I can't finish the test.
-12:the symbol '*' is represented as start.
-15:You are the best is mean you are the no. 1.
-16:The world Happy is the same with "glad".
-18:google is the best tools for search keyword
+$ grep -n 'the' regular_express.txt
+8:I can't finish the test.
+12:the symbol '*' is represented as start.
+15:You are the best is mean you are the no. 1.
+16:The world Happy is the same with "glad".
+18:google is the best tools for search keyword
```
-因为 { 和 } 在 shell 是有特殊意义的,因此必须要使用转义字符进行转义。
+因为 { 和 } 在 shell 是有特殊意义的,因此必须要使用转义字符进行转义。
```html
-$ grep -n 'go\{2,5\}g' regular_express.txt
+$ grep -n 'go\{2,5\}g' regular_express.txt
```
-## printf
+## printf
用于格式化输出。
-它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。
+它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。
```html
-$ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt)
- DmTsai 80 60 92 77.33
- VBird 75 55 80 70.00
- Ken 60 90 70 73.33
+$ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt)
+ DmTsai 80 60 92 77.33
+ VBird 75 55 80 70.00
+ Ken 60 90 70 73.33
```
-## awk
+## awk
可以根据字段的某些条件进行匹配,例如匹配字段小于某个值的那一行数据。
```html
-$ awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename
+$ awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename
```
-awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$n,n 为字段号,从 1 开始,\$0 表示一整行。
+awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$n,n 为字段号,从 1 开始,\$0 表示一整行。
-范例 1:取出登录用户的用户名和 ip
+范例 1:取出登录用户的用户名和 ip
```html
-$ last -n 5
-dmtsai pts/0 192.168.1.100 Tue Jul 14 17:32 still logged in
-dmtsai pts/0 192.168.1.100 Thu Jul 9 23:36 - 02:58 (03:22)
-dmtsai pts/0 192.168.1.100 Thu Jul 9 17:23 - 23:36 (06:12)
-dmtsai pts/0 192.168.1.100 Thu Jul 9 08:02 - 08:17 (00:14)
-dmtsai tty1 Fri May 29 11:55 - 12:11 (00:15)
+$ last -n 5
+dmtsai pts/0 192.168.1.100 Tue Jul 14 17:32 still logged in
+dmtsai pts/0 192.168.1.100 Thu Jul 9 23:36 - 02:58 (03:22)
+dmtsai pts/0 192.168.1.100 Thu Jul 9 17:23 - 23:36 (06:12)
+dmtsai pts/0 192.168.1.100 Thu Jul 9 08:02 - 08:17 (00:14)
+dmtsai tty1 Fri May 29 11:55 - 12:11 (00:15)
-$ last -n 5 | awk '{print $1 "\t" $3}
+$ last -n 5 | awk '{print $1 "\t" $3}
```
-awk 变量:
+awk 变量:
-| 变量名称 | 代表意义 |
-| -- | -- |
-| NF | 每一行拥有的字段总数 |
-| NR | 目前所处理的是第几行数据 |
-| FS | 目前的分隔字符,默认是空格键 |
+| 变量名称 | 代表意义 |
+| -- | -- |
+| NF | 每一行拥有的字段总数 |
+| NR | 目前所处理的是第几行数据 |
+| FS | 目前的分隔字符,默认是空格键 |
-范例 2:输出正在处理的行号,并显示每一行有多少字段
+范例 2:输出正在处理的行号,并显示每一行有多少字段
```html
-$ last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}'
-dmtsai lines: 1 columns: 10
-dmtsai lines: 2 columns: 10
-dmtsai lines: 3 columns: 10
-dmtsai lines: 4 columns: 10
-dmtsai lines: 5 columns: 9
+$ last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}'
+dmtsai lines: 1 columns: 10
+dmtsai lines: 2 columns: 10
+dmtsai lines: 3 columns: 10
+dmtsai lines: 4 columns: 10
+dmtsai lines: 5 columns: 9
```
-可以使用条件,其中等于使用 ==。
+可以使用条件,其中等于使用 ==。
-范例 3:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。
+范例 3:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。
```text
-$ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}'
-root 0
-bin 1
-daemon 2
+$ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}'
+root 0
+bin 1
+daemon 2
```
-# 十、进程管理
+# 十、进程管理
-## 查看进程
+## 查看进程
-### 1. ps
+### 1. ps
查看某个时间点的进程信息
示例一:查看自己的进程
```
-# ps -l
+# ps -l
```
示例二:查看系统所有进程
```
-# ps aux
+# ps aux
```
示例三:查看特定的进程
```
-# ps aux | grep threadx
+# ps aux | grep threadx
```
-### 2. top
+### 2. top
实时显示进程信息
示例:两秒钟刷新一次
```
-# top -d 2
+# top -d 2
```
-### 3. pstree
+### 3. pstree
查看进程树
示例:查看所有进程树
```
-# pstree -A
+# pstree -A
```
-### 4. netstat
+### 4. netstat
查看占用端口的进程
```
-# netstat -anp | grep port
+# netstat -anp | grep port
```
-## 进程状态
+## 进程状态
-
+
-| 状态 | 说明 |
-| :---: | --- |
-| R | running or runnable (on run queue) |
-| 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|
+| 状态 | 说明 |
+| :---: | --- |
+| R | running or runnable (on run queue) |
+| 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|
-## SIGCHLD
+## SIGCHLD
当一个子进程改变了它的状态时:停止运行,继续运行或者退出,有两件事会发生在父进程中:
-- 得到 SIGCHLD 信号;
-- waitpid() 或者 wait() 调用会返回。
+- 得到 SIGCHLD 信号;
+- waitpid() 或者 wait() 调用会返回。
-
+
-其中子进程发送的 SIGCHLD 信号包含了子进程的信息,包含了进程 ID、进程状态、进程使用 CPU 的时间等。
+其中子进程发送的 SIGCHLD 信号包含了子进程的信息,包含了进程 ID、进程状态、进程使用 CPU 的时间等。
-在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息。父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。
+在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息。父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。
-## wait()
+## wait()
```c
-pid_t wait(int *status)
+pid_t wait(int *status)
```
-父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,之后 wait() 函数会销毁子进程并返回。
+父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,之后 wait() 函数会销毁子进程并返回。
-如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 - 1,同时 errno 被置为 ECHILD。
+如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 - 1,同时 errno 被置为 ECHILD。
-参数 status 用来保存被收集进程退出时的一些状态,如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,以设定这个参数为 NULL:
+参数 status 用来保存被收集进程退出时的一些状态,如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,以设定这个参数为 NULL:
```c
-pid = wait(NULL);
+pid = wait(NULL);
```
-## waitpid()
+## waitpid()
```c
-pid_t waitpid(pid_t pid, int *status, int options)
+pid_t waitpid(pid_t pid, int *status, int options)
```
-作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。
+作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。
-pid 参数指示一个子进程的 ID,表示只关心这个子进程的退出 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。
+pid 参数指示一个子进程的 ID,表示只关心这个子进程的退出 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。
-options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。
+options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。
-## 孤儿进程
+## 孤儿进程
-一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。
+一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。
-由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。
+由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。
-## 僵死进程
+## 僵死进程
-一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。
+一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。
-僵死进程通过 ps 命令显示出来的状态为 Z。
+僵死进程通过 ps 命令显示出来的状态为 Z。
系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。
-要消灭系统中大量的僵死进程,只需要将其父进程杀死,此时所有的僵死进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵死进程所占有的资源,从而结束僵死进程。
+要消灭系统中大量的僵死进程,只需要将其父进程杀死,此时所有的僵死进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵死进程所占有的资源,从而结束僵死进程。
-# 参考资料
+# 参考资料
-- 鸟哥. 鸟 哥 的 Linux 私 房 菜 基 础 篇 第 三 版[J]. 2009.
-- [Linux 平台上的软件包管理](https://www.ibm.com/developerworks/cn/linux/l-cn-rpmdpkg/index.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)
-- [详解 wait 和 waitpid 函数](https://blog.csdn.net/kevinhg/article/details/7001719)
-- [IDE、SATA、SCSI、SAS、FC、SSD 硬盘类型介绍](https://blog.csdn.net/tianlesoftware/article/details/6009110)
-- [Akai IB-301S SCSI Interface for S2800,S3000](http://www.mpchunter.com/s3000/akai-ib-301s-scsi-interface-for-s2800s3000/)
-- [Parallel ATA](https://en.wikipedia.org/wiki/Parallel_ATA)
-- [ADATA XPG SX900 256GB SATA 3 SSD Review – Expanded Capacity and SandForce Driven Speed](http://www.thessdreview.com/our-reviews/adata-xpg-sx900-256gb-sata-3-ssd-review-expanded-capacity-and-sandforce-driven-speed/4/)
-- [Decoding UCS Invicta – Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1)
-- [硬盘](https://zh.wikipedia.org/wiki/%E7%A1%AC%E7%9B%98)
-- [Difference between SAS and SATA](http://www.differencebetween.info/difference-between-sas-and-sata)
-- [BIOS](https://zh.wikipedia.org/wiki/BIOS)
-- [File system design case studies](https://www.cs.rutgers.edu/\~pxk/416/notes/13-fs-studies.html)
-- [Programming Project #4](https://classes.soe.ucsc.edu/cmps111/Fall08/proj4.shtml)
-- [FILE SYSTEM DESIGN](http://web.cs.ucla.edu/classes/fall14/cs111/scribe/11a/index.html)
+- 鸟哥. 鸟 哥 的 Linux 私 房 菜 基 础 篇 第 三 版[J]. 2009.
+- [Linux 平台上的软件包管理](https://www.ibm.com/developerworks/cn/linux/l-cn-rpmdpkg/index.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)
+- [详解 wait 和 waitpid 函数](https://blog.csdn.net/kevinhg/article/details/7001719)
+- [IDE、SATA、SCSI、SAS、FC、SSD 硬盘类型介绍](https://blog.csdn.net/tianlesoftware/article/details/6009110)
+- [Akai IB-301S SCSI Interface for S2800,S3000](http://www.mpchunter.com/s3000/akai-ib-301s-scsi-interface-for-s2800s3000/)
+- [Parallel ATA](https://en.wikipedia.org/wiki/Parallel_ATA)
+- [ADATA XPG SX900 256GB SATA 3 SSD Review – Expanded Capacity and SandForce Driven Speed](http://www.thessdreview.com/our-reviews/adata-xpg-sx900-256gb-sata-3-ssd-review-expanded-capacity-and-sandforce-driven-speed/4/)
+- [Decoding UCS Invicta – Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1)
+- [硬盘](https://zh.wikipedia.org/wiki/%E7%A1%AC%E7%9B%98)
+- [Difference between SAS and SATA](http://www.differencebetween.info/difference-between-sas-and-sata)
+- [BIOS](https://zh.wikipedia.org/wiki/BIOS)
+- [File system design case studies](https://www.cs.rutgers.edu/~pxk/416/notes/13-fs-studies.html)
+- [Programming Project #4](https://classes.soe.ucsc.edu/cmps111/Fall08/proj4.shtml)
+- [FILE SYSTEM DESIGN](http://web.cs.ucla.edu/classes/fall14/cs111/scribe/11a/index.html)
+---bottom---CyC---
+![](index_files/5942debd-fc00-477a-b390-7c5692cc8070.jpg)
+![](index_files/014fbc4d-d873-4a12-b160-867ddaed9807.jpg)
+![](index_files/924914c0-660c-4e4a-bbc0-1df1146e7516.jpg)
+![](index_files/f9f2a16b-4843-44d1-9759-c745772e9bcf.jpg)
+![](index_files/f0574025-c514-49f5-a591-6d6a71f271f7.jpg)
+![](index_files/6729baa0-57d7-4817-b3aa-518cbccf824c.jpg)
+![](index_files/GUID_Partition_Table_Scheme.svg.png)
+![](index_files/50831a6f-2777-46ea-a571-29f23c85cc21.jpg)
+![](index_files/f900f266-a323-42b2-bc43-218fdb8811a8.jpg)
+![](index_files/BSD_disk.png)
+![](index_files/83185315-793a-453a-a927-5e8d92b5c0ef.jpg)
+![](index_files/075e1977-7846-4928-96c8-bb5b0268693c.jpg)
+![](index_files/inode_with_signatures.jpg)
+![](index_files/linux-filesystem.png)
+![](index_files/658fc5e7-79c0-4247-9445-d69bf194c539.png)
+![](index_files/76a49594323247f21c9b3a69945445ee.png)
+![](index_files/flow.png)
+![](index_files/1a231f2a-5c2f-4231-8e0f-915aa5894347.jpg)
+![](index_files/5e9b10f3-9504-4483-9667-d4770adebf9f.png)
+![](index_files/1582217a-ed46-4cac-811e-90d13a65163b.png)
+![](index_files/b4b29aa9-dd2c-467b-b75f-ca6541cb25b5.jpg)
diff --git a/notes/MySQL.md b/notes/MySQL.md
index 0e39a69c..15664163 100644
--- a/notes/MySQL.md
+++ b/notes/MySQL.md
@@ -1,38 +1,10 @@
-
-* [一、存储引擎](#一存储引擎)
- * [InnoDB](#innodb)
- * [MyISAM](#myisam)
- * [比较](#比较)
-* [二、数据类型](#二数据类型)
- * [整型](#整型)
- * [浮点数](#浮点数)
- * [字符串](#字符串)
- * [时间和日期](#时间和日期)
-* [三、索引](#三索引)
- * [B-Tree 和 B+Tree 原理](#b-tree-和-btree-原理)
- * [索引分类](#索引分类)
- * [索引的优点](#索引的优点)
- * [索引优化](#索引优化)
-* [四、查询性能优化](#四查询性能优化)
- * [使用 Explain 进行分析](#使用-explain-进行分析)
- * [优化数据访问](#优化数据访问)
- * [重构查询方式](#重构查询方式)
-* [五、切分](#五切分)
- * [水平切分](#水平切分)
- * [垂直切分](#垂直切分)
- * [Sharding 策略](#sharding-策略)
- * [Sharding 存在的问题](#sharding-存在的问题)
-* [参考资料](#参考资料)
-
+# 一、存储引擎
+## InnoDB
-# 一、存储引擎
+InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。
-## InnoDB
-
-InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。
-
-采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ),并且通过间隙锁(next-key locking)策略防止幻读的出现。间隙锁使得 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。
+采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ),并且通过间隙锁(next-key locking)策略防止幻读的出现。间隙锁使得 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。
表是基于聚簇索引建立的,它对主键的查询性能有很大的提升。
@@ -40,79 +12,79 @@ InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支
通过一些机制和工具支持真正的热备份。其它存储引擎不支持热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。
-## MyISAM
+## MyISAM
-MyISAM 提供了大量的特性,包括压缩表、空间数据索引等。
+MyISAM 提供了大量的特性,包括压缩表、空间数据索引等。
不支持事务。
-不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取查询的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。
+不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取查询的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。
可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。
-如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。
+如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。
-MyISAM 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以继续使用 MyISAM。
+MyISAM 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以继续使用 MyISAM。
-## 比较
+## 比较
-- 事务:InnoDB 是事务型的。
+- 事务:InnoDB 是事务型的。
-- 备份:InnoDB 支持在线热备份。
+- 备份:InnoDB 支持在线热备份。
-- 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。
+- 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。
-- 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
+- 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
-- 其它特性:MyISAM 支持压缩表和空间数据索引。
+- 其它特性:MyISAM 支持压缩表和空间数据索引。
-# 二、数据类型
+# 二、数据类型
-## 整型
+## 整型
-TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。
+TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。
-INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。
+INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。
-## 浮点数
+## 浮点数
-FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。
+FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。
-FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。
+FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。
-## 字符串
+## 字符串
-主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。
+主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。
-VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。
+VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。
-VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。
+VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。
-## 时间和日期
+## 时间和日期
-MySQL 提供了两种相似的日期时间类型:DATATIME 和 TIMESTAMP。
+MySQL 提供了两种相似的日期时间类型:DATATIME 和 TIMESTAMP。
-### 1. DATATIME
+### 1. DATATIME
-能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。
+能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。
它与时区无关。
-默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATATIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。
+默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATATIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。
-### 2. TIMESTAMP
+### 2. TIMESTAMP
-和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。
+和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。
它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。
-MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。
+MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。
-默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。
+默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。
-应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。
+应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。
-# 三、索引
+# 三、索引
索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。
@@ -120,312 +92,320 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提
对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。对于中到大型的表,索引就非常有效。但是对于特大型的表,建立和使用索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。
-## B-Tree 和 B+Tree 原理
+## B-Tree 和 B+Tree 原理
-### 1. B-Tree
+### 1. B-Tree
-
+![](index_files/5ed71283-a070-4b21-85ae-f2cbfd6ba6e1.jpg)
-定义一条数据记录为一个二元组 [key, data],B-Tree 是满足下列条件的数据结构:
+定义一条数据记录为一个二元组 [key, data],B-Tree 是满足下列条件的数据结构:
-- 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的;
-- 一个节点中的 key 从左到右非递减排列;
-- 如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1 ,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1 。
+- 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的;
+- 一个节点中的 key 从左到右非递减排列;
+- 如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1 ,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1 。
-查找算法:首先在根节点进行二分查找,如果找到则返回对应节点的 data,否则在相应区间的指针指向的节点递归进行查找。
+查找算法:首先在根节点进行二分查找,如果找到则返回对应节点的 data,否则在相应区间的指针指向的节点递归进行查找。
-由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。
+由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。
-### 2. B+Tree
+### 2. B+Tree
-
+![](index_files/63cd5b50-d6d8-4df6-8912-ef4a1dd5ba13.jpg)
-与 B-Tree 相比,B+Tree 有以下不同点:
+与 B-Tree 相比,B+Tree 有以下不同点:
-- 每个节点的指针上限为 2d 而不是 2d+1(d 为节点的出度);
-- 内节点不存储 data,只存储 key;
-- 叶子节点不存储指针。
+- 每个节点的指针上限为 2d 而不是 2d+1(d 为节点的出度);
+- 内节点不存储 data,只存储 key;
+- 叶子节点不存储指针。
-### 3. 顺序访问指针
+### 3. 顺序访问指针
-
+![](index_files/1ee5f0a5-b8df-43b9-95ab-c516c54ec797.jpg)
-一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。
+一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。
-### 4. B+Tree 和 B-Tree 优势
+### 4. B+Tree 和 B-Tree 优势
-红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+Tree 和 B-Tree 作为索引结构,主要有以下两个原因:
+红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+Tree 和 B-Tree 作为索引结构,主要有以下两个原因:
-**(一)更少的检索次数**
+**(一)更少的检索次数**
-平衡树检索数据的时间复杂度等于树高 h,而树高大致为 O(h)=O(logd N),其中 d 为每个节点的出度。
+平衡树检索数据的时间复杂度等于树高 h,而树高大致为 O(h)=O(logd N),其中 d 为每个节点的出度。
-红黑树的出度为 2,而 B+Tree 与 B-Tree 的出度一般都非常大。红黑树的树高 h 很明显比 B+Tree 和 B-Tree 大非常多,因此检索的次数也就更多。
+红黑树的出度为 2,而 B+Tree 与 B-Tree 的出度一般都非常大。红黑树的树高 h 很明显比 B+Tree 和 B-Tree 大非常多,因此检索的次数也就更多。
-B+Tree 相比于 B-Tree 更适合外存索引,因为 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,检索效率会更高。
+B+Tree 相比于 B-Tree 更适合外存索引,因为 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,检索效率会更高。
-**(二)利用计算机预读特性**
+**(二)利用计算机预读特性**
-为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,因此速度会非常快。
+为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,因此速度会非常快。
-操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,临近的节点也能够被预先载入。
+操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,临近的节点也能够被预先载入。
-更多内容请参考:[MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
+更多内容请参考:[MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
-## 索引分类
+## 索引分类
-### 1. B+Tree 索引
+### 1. B+Tree 索引
-
+![](index_files/c23957e9-a572-44f8-be15-f306c8b92722.jpg)
-《高性能 MySQL》一书使用 B-Tree 进行描述,其实从技术上来说这种索引是 B+Tree,因为只有叶子节点存储数据值。
+《高性能 MySQL》一书使用 B-Tree 进行描述,其实从技术上来说这种索引是 B+Tree,因为只有叶子节点存储数据值。
-B+Tree 索引是大多数 MySQL 存储引擎的默认索引类型。
+B+Tree 索引是大多数 MySQL 存储引擎的默认索引类型。
因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。除了用于查找,还可以用于排序和分组。
-可以指定多个列作为索引列,多个索引列共同组成键。B+Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。
+可以指定多个列作为索引列,多个索引列共同组成键。B+Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。
-### 2. 哈希索引
+### 2. 哈希索引
基于哈希表实现,优点是查找非常快。
-在 MySQL 中只有 Memory 引擎显式支持哈希索引。
+在 MySQL 中只有 Memory 引擎显式支持哈希索引。
-InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
+InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
限制:
-- 哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;
-- 无法用于排序与分组;
-- 只支持精确查找,无法用于部分查找和范围查找;
-- 如果哈希冲突很多,查找速度会变得很慢。
+- 哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;
+- 无法用于排序与分组;
+- 只支持精确查找,无法用于部分查找和范围查找;
+- 如果哈希冲突很多,查找速度会变得很慢。
-### 3. 空间数据索引(R-Tree)
+### 3. 空间数据索引(R-Tree)
-MyISAM 存储引擎支持空间数据索引,可以用于地理数据存储。
+MyISAM 存储引擎支持空间数据索引,可以用于地理数据存储。
空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。
-必须使用 GIS 相关的函数来维护数据。
+必须使用 GIS 相关的函数来维护数据。
-### 4. 全文索引
+### 4. 全文索引
-MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比值是否相等。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。
+MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比值是否相等。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。
-InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
+InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
-## 索引的优点
+## 索引的优点
-- 大大减少了服务器需要扫描的数据量;
+- 大大减少了服务器需要扫描的数据量;
-- 帮助服务器避免进行排序和创建临时表(B+Tree 索引是有序的,可以用来做 ORDER BY 和 GROUP BY 操作);
+- 帮助服务器避免进行排序和创建临时表(B+Tree 索引是有序的,可以用来做 ORDER BY 和 GROUP BY 操作);
-- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,也就将相邻的数据都存储在一起)。
+- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,也就将相邻的数据都存储在一起)。
-## 索引优化
+## 索引优化
-### 1. 独立的列
+### 1. 独立的列
在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。
-例如下面的查询不能使用 actor_id 列的索引:
+例如下面的查询不能使用 actor_id 列的索引:
```sql
-SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
+SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
```
-### 2. 多列索引
+### 2. 多列索引
-在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor_id 和 film_id 设置为多列索引。
+在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor_id 和 film_id 设置为多列索引。
```sql
-SELECT film_id, actor_ id FROM sakila.film_actor
-WhERE actor_id = 1 AND film_id = 1;
+SELECT film_id, actor_ id FROM sakila.film_actor
+WhERE actor_id = 1 AND film_id = 1;
```
-### 3. 索引列的顺序
+### 3. 索引列的顺序
-让选择性最强的索引列放在前面,索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,查询效率也越高。
+让选择性最强的索引列放在前面,索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,查询效率也越高。
-例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。
+例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。
```sql
-SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
-COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
+SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
+COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
COUNT(*)
-FROM payment;
+FROM payment;
```
```html
- staff_id_selectivity: 0.0001
-customer_id_selectivity: 0.0373
- COUNT(*): 16049
+ staff_id_selectivity: 0.0001
+customer_id_selectivity: 0.0373
+ COUNT(*): 16049
```
-### 4. 前缀索引
+### 4. 前缀索引
-对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。
+对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。
对于前缀长度的选取需要根据索引选择性来确定。
-### 5. 覆盖索引
+### 5. 覆盖索引
索引包含所有需要查询的字段的值。
-**优点**
+**优点**
-- 因为索引条目通常远小于数据行的大小,所以若只读取索引,能大大减少数据访问量。
-- 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。
-- 对于 InnoDB 引擎,若二级索引能够覆盖查询,则无需访问聚簇索引。
+- 因为索引条目通常远小于数据行的大小,所以若只读取索引,能大大减少数据访问量。
+- 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。
+- 对于 InnoDB 引擎,若二级索引能够覆盖查询,则无需访问聚簇索引。
-### 6. 聚簇索引
+### 6. 聚簇索引
-
+![](index_files/e800b001-7779-495b-8459-d33a7440d7b8.jpg)
聚簇索引并不是一种索引类型,而是一种数据存储方式。
-术语“聚簇”表示数据行和相邻的键值紧密地存储在一起,InnoDB 的聚簇索引在同一个结构中保存了 B+Tree 索引和数据行。
+术语“聚簇”表示数据行和相邻的键值紧密地存储在一起,InnoDB 的聚簇索引在同一个结构中保存了 B+Tree 索引和数据行。
因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
-**优点**
+**优点**
-- 可以把相关数据保存在一起,减少 I/O 操作。例如电子邮件表可以根据用户 ID 来聚集数据,这样只需要从磁盘读取少数的数据也就能获取某个用户的全部邮件,如果没有使用聚聚簇索引,则每封邮件都可能导致一次磁盘 I/O。
-- 数据访问更快。
+- 可以把相关数据保存在一起,减少 I/O 操作。例如电子邮件表可以根据用户 ID 来聚集数据,这样只需要从磁盘读取少数的数据也就能获取某个用户的全部邮件,如果没有使用聚聚簇索引,则每封邮件都可能导致一次磁盘 I/O。
+- 数据访问更快。
-**缺点**
+**缺点**
-- 聚簇索引最大限度提高了 I/O 密集型应用的性能,但是如果数据全部放在内存,就没必要用聚簇索引。
-- 插入速度严重依赖于插入顺序,按主键的顺序插入是最快的。
-- 更新操作代价很高,因为每个被更新的行都会移动到新的位置。
-- 当插入到某个已满的页中,存储引擎会将该页分裂成两个页面来容纳该行,页分裂会导致表占用更多的磁盘空间。
-- 如果行比较稀疏,或者由于页分裂导致数据存储不连续时,聚簇索引可能导致全表扫描速度变慢。
+- 聚簇索引最大限度提高了 I/O 密集型应用的性能,但是如果数据全部放在内存,就没必要用聚簇索引。
+- 插入速度严重依赖于插入顺序,按主键的顺序插入是最快的。
+- 更新操作代价很高,因为每个被更新的行都会移动到新的位置。
+- 当插入到某个已满的页中,存储引擎会将该页分裂成两个页面来容纳该行,页分裂会导致表占用更多的磁盘空间。
+- 如果行比较稀疏,或者由于页分裂导致数据存储不连续时,聚簇索引可能导致全表扫描速度变慢。
-# 四、查询性能优化
+# 四、查询性能优化
-## 使用 Explain 进行分析
+## 使用 Explain 进行分析
-Explain 用来分析 SELECT 查询语句,开发人员可以通过分析结果来优化查询语句。
+Explain 用来分析 SELECT 查询语句,开发人员可以通过分析结果来优化查询语句。
比较重要的字段有:
-- select_type : 查询类型,有简单查询、联合查询、子查询等
-- key : 使用的索引
-- rows : 扫描的行数
+- select_type : 查询类型,有简单查询、联合查询、子查询等
+- key : 使用的索引
+- rows : 扫描的行数
-更多内容请参考:[MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735)
+更多内容请参考:[MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735)
-## 优化数据访问
+## 优化数据访问
-### 1. 减少请求的数据量
+### 1. 减少请求的数据量
-**(一)只返回必要的列**
+**(一)只返回必要的列**
-最好不要使用 SELECT * 语句。
+最好不要使用 SELECT * 语句。
-**(二)只返回必要的行**
+**(二)只返回必要的行**
-使用 WHERE 语句进行查询过滤,有时候也需要使用 LIMIT 语句来限制返回的数据。
+使用 WHERE 语句进行查询过滤,有时候也需要使用 LIMIT 语句来限制返回的数据。
-**(三)缓存重复查询的数据**
+**(三)缓存重复查询的数据**
使用缓存可以避免在数据库中进行查询,特别要查询的数据经常被重复查询,缓存可以带来的查询性能提升将会是非常明显的。
-### 2. 减少服务器端扫描的行数
+### 2. 减少服务器端扫描的行数
最有效的方式是使用索引来覆盖查询。
-## 重构查询方式
+## 重构查询方式
-### 1. 切分大查询
+### 1. 切分大查询
一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。
```sql
-DELEFT FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
+DELEFT FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
```
```sql
-rows_affected = 0
-do {
- rows_affected = do_query(
- "DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000")
-} while rows_affected > 0
+rows_affected = 0
+do {
+ rows_affected = do_query(
+ "DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000")
+} while rows_affected > 0
```
-### 2. 分解大连接查询
+### 2. 分解大连接查询
将一个大连接查询(JOIN)分解成对每一个表进行一次单表查询,然后将结果在应用程序中进行关联,这样做的好处有:
-- 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
-- 减少锁竞争;
-- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可扩展。
-- 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
-- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。
+- 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
+- 减少锁竞争;
+- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可扩展。
+- 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
+- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。
```sql
-SELECT * FROM tab
-JOIN tag_post ON tag_post.tag_id=tag.id
-JOIN post ON tag_post.post_id=post.id
-WHERE tag.tag='mysql';
+SELECT * FROM tab
+JOIN tag_post ON tag_post.tag_id=tag.id
+JOIN post ON tag_post.post_id=post.id
+WHERE tag.tag='mysql';
```
```sql
-SELECT * FROM tag WHERE tag='mysql';
-SELECT * FROM tag_post WHERE tag_id=1234;
-SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
+SELECT * FROM tag WHERE tag='mysql';
+SELECT * FROM tag_post WHERE tag_id=1234;
+SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
```
-# 五、切分
+# 五、切分
-## 水平切分
+## 水平切分
-
+![](index_files/63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg)
-水平切分就是就是常见的 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。
+水平切分就是就是常见的 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。
-当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。
+当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。
-## 垂直切分
+## 垂直切分
-
+![](index_files/e130e5b8-b19a-4f1e-b860-223040525cf6.jpg)
垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分。也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。
-也可以在数据库的层面使用垂直切分,它按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库 payDB、用户数据库 userBD 等。
+也可以在数据库的层面使用垂直切分,它按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库 payDB、用户数据库 userBD 等。
-## Sharding 策略
+## Sharding 策略
-- 哈希取模:hash(key) % NUM_DB
-- 范围:可以是 ID 范围也可以是时间范围
-- 映射表:使用单独的一个数据库来存储映射关系
+- 哈希取模:hash(key) % NUM_DB
+- 范围:可以是 ID 范围也可以是时间范围
+- 映射表:使用单独的一个数据库来存储映射关系
-## Sharding 存在的问题
+## Sharding 存在的问题
-### 1. 事务问题
+### 1. 事务问题
使用分布式事务。
-### 2. JOIN
+### 2. JOIN
-将原来的 JOIN 查询分解成多个单表查询,然后在用户程序中进行 JOIN。
+将原来的 JOIN 查询分解成多个单表查询,然后在用户程序中进行 JOIN。
-### 3. ID 唯一性
+### 3. ID 唯一性
-- 使用全局唯一 ID:GUID。
-- 为每个分片指定一个 ID 范围。
-- 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)。
+- 使用全局唯一 ID:GUID。
+- 为每个分片指定一个 ID 范围。
+- 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)。
更多内容请参考:
-- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6)
-- [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html)
+- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6)
+- [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html)
-# 参考资料
+# 参考资料
-- BaronScbwartz, PeterZaitsev, VadimTkacbenko, 等. 高性能 MySQL[M]. 电子工业出版社, 2013.
-- [20+ 条 MySQL 性能优化的最佳经验](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html)
-- [服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策](http://blog.720ui.com/2017/mysql_core_09_multi_db_table2/ "服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策")
-- [How to create unique row ID in sharded databases?](https://stackoverflow.com/questions/788829/how-to-create-unique-row-id-in-sharded-databases)
-- [SQL Azure Federation – Introduction](http://geekswithblogs.net/shaunxu/archive/2012/01/07/sql-azure-federation-ndash-introduction.aspx "Title of this entry.")
+- BaronScbwartz, PeterZaitsev, VadimTkacbenko, 等. 高性能 MySQL[M]. 电子工业出版社, 2013.
+- [20+ 条 MySQL 性能优化的最佳经验](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html)
+- [服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策](http://blog.720ui.com/2017/mysql_core_09_multi_db_table2/ "服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策")
+- [How to create unique row ID in sharded databases?](https://stackoverflow.com/questions/788829/how-to-create-unique-row-id-in-sharded-databases)
+- [SQL Azure Federation – Introduction](http://geekswithblogs.net/shaunxu/archive/2012/01/07/sql-azure-federation-ndash-introduction.aspx "Title of this entry.")
+---bottom---CyC---
+![](index_files/5ed71283-a070-4b21-85ae-f2cbfd6ba6e1.jpg)
+![](index_files/63cd5b50-d6d8-4df6-8912-ef4a1dd5ba13.jpg)
+![](index_files/1ee5f0a5-b8df-43b9-95ab-c516c54ec797.jpg)
+![](index_files/c23957e9-a572-44f8-be15-f306c8b92722.jpg)
+![](index_files/e800b001-7779-495b-8459-d33a7440d7b8.jpg)
+![](index_files/63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg)
+![](index_files/e130e5b8-b19a-4f1e-b860-223040525cf6.jpg)
diff --git a/notes/Redis.md b/notes/Redis.md
index b276275f..482ba486 100644
--- a/notes/Redis.md
+++ b/notes/Redis.md
@@ -1,302 +1,255 @@
-
-* [一、概述](#一概述)
-* [二、数据类型](#二数据类型)
- * [STRING](#string)
- * [LIST](#list)
- * [SET](#set)
- * [HASH](#hash)
- * [ZSET](#zset)
-* [三、使用场景](#三使用场景)
- * [缓存](#缓存)
- * [计数器](#计数器)
- * [应用限流](#应用限流)
- * [消息队列](#消息队列)
- * [查找表](#查找表)
- * [交集运算](#交集运算)
- * [排行榜](#排行榜)
- * [分布式 Session](#分布式-session)
- * [分布式锁](#分布式锁)
-* [四、Redis 与 Memcached](#四redis-与-memcached)
- * [数据类型](#数据类型)
- * [数据持久化](#数据持久化)
- * [分布式](#分布式)
- * [内存管理机制](#内存管理机制)
-* [五、键的过期时间](#五键的过期时间)
-* [六、数据淘汰策略](#六数据淘汰策略)
-* [七、持久化](#七持久化)
- * [快照持久化](#快照持久化)
- * [AOF 持久化](#aof-持久化)
-* [八、发布与订阅](#八发布与订阅)
-* [九、事务](#九事务)
-* [十、事件](#十事件)
- * [文件事件](#文件事件)
- * [时间事件](#时间事件)
- * [事件的调度与执行](#事件的调度与执行)
-* [十一、复制](#十一复制)
- * [连接过程](#连接过程)
- * [主从链](#主从链)
-* [十二、Sentinel](#十二sentinel)
-* [十三、分片](#十三分片)
-* [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析)
- * [文章信息](#文章信息)
- * [点赞功能](#点赞功能)
- * [对文章进行排序](#对文章进行排序)
-* [参考资料](#参考资料)
-
+# 一、概述
-
-# 一、概述
-
-Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。
+Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。
键的类型只能为字符串,值支持的五种类型数据类型为:字符串、列表、集合、有序集合、散列表。
-Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
+Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
-# 二、数据类型
+# 二、数据类型
-| 数据类型 | 可以存储的值 | 操作 |
-| :--: | :--: | :--: |
-| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 |
-| LIST | 列表 | 从两端压入或者弹出元素 读取单个或者多个元素 进行修剪,只保留一个范围内的元素 |
-| SET | 无序集合 | 添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素 |
-| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在|
-| ZSET | 有序集合 | 添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名 |
+| 数据类型 | 可以存储的值 | 操作 |
+| :--: | :--: | :--: |
+| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 |
+| LIST | 列表 | 从两端压入或者弹出元素 读取单个或者多个元素 进行修剪,只保留一个范围内的元素 |
+| SET | 无序集合 | 添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素 |
+| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在|
+| ZSET | 有序集合 | 添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名 |
-> [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/)
+> [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/)
-## STRING
+## STRING
-
+
```html
-> set hello world
+> set hello world
OK
-> get hello
+> get hello
"world"
-> del hello
-(integer) 1
-> get hello
+> del hello
+(integer) 1
+> get hello
(nil)
```
-## LIST
+## LIST
-
+
```html
-> rpush list-key item
-(integer) 1
-> rpush list-key item2
-(integer) 2
-> rpush list-key item
-(integer) 3
+> rpush list-key item
+(integer) 1
+> rpush list-key item2
+(integer) 2
+> rpush list-key item
+(integer) 3
-> lrange list-key 0 -1
-1) "item"
-2) "item2"
-3) "item"
+> lrange list-key 0 -1
+1) "item"
+2) "item2"
+3) "item"
-> lindex list-key 1
+> lindex list-key 1
"item2"
-> lpop list-key
+> lpop list-key
"item"
-> lrange list-key 0 -1
-1) "item2"
-2) "item"
+> lrange list-key 0 -1
+1) "item2"
+2) "item"
```
-## SET
+## SET
-
+
```html
-> sadd set-key item
-(integer) 1
-> sadd set-key item2
-(integer) 1
-> sadd set-key item3
-(integer) 1
-> sadd set-key item
-(integer) 0
+> sadd set-key item
+(integer) 1
+> sadd set-key item2
+(integer) 1
+> sadd set-key item3
+(integer) 1
+> sadd set-key item
+(integer) 0
-> smembers set-key
-1) "item"
-2) "item2"
-3) "item3"
+> smembers set-key
+1) "item"
+2) "item2"
+3) "item3"
-> sismember set-key item4
-(integer) 0
-> sismember set-key item
-(integer) 1
+> sismember set-key item4
+(integer) 0
+> sismember set-key item
+(integer) 1
-> srem set-key item2
-(integer) 1
-> srem set-key item2
-(integer) 0
+> srem set-key item2
+(integer) 1
+> srem set-key item2
+(integer) 0
-> smembers set-key
-1) "item"
-2) "item3"
+> smembers set-key
+1) "item"
+2) "item3"
```
-## HASH
+## HASH
-
+
```html
-> hset hash-key sub-key1 value1
-(integer) 1
-> hset hash-key sub-key2 value2
-(integer) 1
-> hset hash-key sub-key1 value1
-(integer) 0
+> hset hash-key sub-key1 value1
+(integer) 1
+> hset hash-key sub-key2 value2
+(integer) 1
+> hset hash-key sub-key1 value1
+(integer) 0
-> hgetall hash-key
-1) "sub-key1"
-2) "value1"
-3) "sub-key2"
-4) "value2"
+> hgetall hash-key
+1) "sub-key1"
+2) "value1"
+3) "sub-key2"
+4) "value2"
-> hdel hash-key sub-key2
-(integer) 1
-> hdel hash-key sub-key2
-(integer) 0
+> hdel hash-key sub-key2
+(integer) 1
+> hdel hash-key sub-key2
+(integer) 0
-> hget hash-key sub-key1
+> hget hash-key sub-key1
"value1"
-> hgetall hash-key
-1) "sub-key1"
-2) "value1"
+> hgetall hash-key
+1) "sub-key1"
+2) "value1"
```
-## ZSET
+## ZSET
-
+
```html
-> zadd zset-key 728 member1
-(integer) 1
-> zadd zset-key 982 member0
-(integer) 1
-> zadd zset-key 982 member0
-(integer) 0
+> zadd zset-key 728 member1
+(integer) 1
+> zadd zset-key 982 member0
+(integer) 1
+> zadd zset-key 982 member0
+(integer) 0
-> zrange zset-key 0 -1 withscores
-1) "member1"
-2) "728"
-3) "member0"
-4) "982"
+> zrange zset-key 0 -1 withscores
+1) "member1"
+2) "728"
+3) "member0"
+4) "982"
-> zrangebyscore zset-key 0 800 withscores
-1) "member1"
-2) "728"
+> zrangebyscore zset-key 0 800 withscores
+1) "member1"
+2) "728"
-> zrem zset-key member1
-(integer) 1
-> zrem zset-key member1
-(integer) 0
+> zrem zset-key member1
+(integer) 1
+> zrem zset-key member1
+(integer) 0
-> zrange zset-key 0 -1 withscores
-1) "member0"
-2) "982"
+> zrange zset-key 0 -1 withscores
+1) "member0"
+2) "982"
```
-# 三、使用场景
+# 三、使用场景
-## 缓存
+## 缓存
将热点数据放到内存中,设置内存的最大使用量以及过期淘汰策略来保证缓存的命中率。
-## 计数器
+## 计数器
-Redis 这种内存数据库能支持计数器频繁的读写操作。
+Redis 这种内存数据库能支持计数器频繁的读写操作。
-## 应用限流
+## 应用限流
限制一个网站访问流量。
-## 消息队列
+## 消息队列
-使用 List 数据类型,它是双向链表。
+使用 List 数据类型,它是双向链表。
-## 查找表
+## 查找表
-使用 HASH 数据类型。
+使用 HASH 数据类型。
-## 交集运算
+## 交集运算
-使用 SET 类型,例如求两个用户的共同好友。
+使用 SET 类型,例如求两个用户的共同好友。
-## 排行榜
+## 排行榜
-使用 ZSET 数据类型。
+使用 ZSET 数据类型。
-## 分布式 Session
+## 分布式 Session
-多个应用服务器的 Session 都存储到 Redis 中来保证 Session 的一致性。
+多个应用服务器的 Session 都存储到 Redis 中来保证 Session 的一致性。
-## 分布式锁
+## 分布式锁
-除了可以使用 SETNX 实现分布式锁之外,还可以使用官方提供的 RedLock 分布式锁实现。
+除了可以使用 SETNX 实现分布式锁之外,还可以使用官方提供的 RedLock 分布式锁实现。
-# 四、Redis 与 Memcached
+# 四、Redis 与 Memcached
两者都是非关系型内存键值数据库。有以下主要不同:
-## 数据类型
+## 数据类型
-Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。
+Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。
-## 数据持久化
+## 数据持久化
-Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。
+Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。
-## 分布式
+## 分布式
-Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
+Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
-Redis Cluster 实现了分布式的支持。
+Redis Cluster 实现了分布式的支持。
-## 内存管理机制
+## 内存管理机制
-在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。
+在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。
-Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
+Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
-# 五、键的过期时间
+# 五、键的过期时间
-Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
+Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。
-# 六、数据淘汰策略
+# 六、数据淘汰策略
-可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。
+可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。
-| 策略 | 描述 |
-| :--: | :--: |
-| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
-| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
-|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
-| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
-| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
-| noeviction | 禁止驱逐数据 |
+| 策略 | 描述 |
+| :--: | :--: |
+| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
+| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
+|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
+| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
+| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
+| noeviction | 禁止驱逐数据 |
-如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
+如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
-作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法(LRU、TTL)实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key,抽样数量可通过 maxmemory-samples 配置。
+作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法(LRU、TTL)实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key,抽样数量可通过 maxmemory-samples 配置。
-# 七、持久化
+# 七、持久化
-Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
+Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
-## 快照持久化
+## 快照持久化
将某个时间点的所有数据都存放到硬盘上。
@@ -306,194 +259,206 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需
如果数据量很大,保存快照的时间会很长。
-## AOF 持久化
+## AOF 持久化
-将写命令添加到 AOF 文件(Append Only File)的末尾。
+将写命令添加到 AOF 文件(Append Only File)的末尾。
-对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。可以看出写入文件的数据不会立即同步到硬盘上,在将写命令添加到 AOF 文件时,要根据需求来保证何时同步到硬盘上。
+对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。可以看出写入文件的数据不会立即同步到硬盘上,在将写命令添加到 AOF 文件时,要根据需求来保证何时同步到硬盘上。
有以下同步选项:
-| 选项 | 同步频率 |
-| :--: | :--: |
-| always | 每个写命令都同步 |
-| everysec | 每秒同步一次 |
-| no | 让操作系统来决定何时同步 |
+| 选项 | 同步频率 |
+| :--: | :--: |
+| always | 每个写命令都同步 |
+| everysec | 每秒同步一次 |
+| no | 让操作系统来决定何时同步 |
-- always 选项会严重减低服务器的性能;
-- everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
-- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。
+- always 选项会严重减低服务器的性能;
+- everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
+- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。
-随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
+随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
-# 八、发布与订阅
+# 八、发布与订阅
订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。
-某个客户端使用 SUBSCRIBE 订阅一个频道,其它客户端可以使用 PUBLISH 向这个频道发送消息。
+某个客户端使用 SUBSCRIBE 订阅一个频道,其它客户端可以使用 PUBLISH 向这个频道发送消息。
发布与订阅模式和观察者模式有以下不同:
-- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。
-- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法;而发布与订阅模式是异步的;
+- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。
+- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法;而发布与订阅模式是异步的;
-
+
-# 九、事务
+# 九、事务
一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。
事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。
-Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
+Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
-# 十、事件
+# 十、事件
-Redis 服务器是一个事件驱动程序。
+Redis 服务器是一个事件驱动程序。
-## 文件事件
+## 文件事件
服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。
-Redis 基于 Reactor 模式开发了自己的网络时间处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的时间传送给文件事件分派器,分派器会根据套接字产生的事件类型调用响应的时间处理器。
+Redis 基于 Reactor 模式开发了自己的网络时间处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的时间传送给文件事件分派器,分派器会根据套接字产生的事件类型调用响应的时间处理器。
-
+![](index_files/9ea86eb5-000a-4281-b948-7b567bd6f1d8.png)
-## 时间事件
+## 时间事件
服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。
时间事件又分为:
-- 定时事件:是让一段程序在指定的时间之内执行一次;
-- 周期性事件:是让一段程序每隔指定时间就执行一次。
+- 定时事件:是让一段程序在指定的时间之内执行一次;
+- 周期性事件:是让一段程序每隔指定时间就执行一次。
-Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用响应的事件处理器。
+Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用响应的事件处理器。
-## 事件的调度与执行
+## 事件的调度与执行
服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能监听太久,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。
-事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:
+事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:
```python
-def aeProcessEvents():
+def aeProcessEvents():
- # 获取到达时间离当前时间最接近的时间事件
- time_event = aeSearchNearestTimer()
+ # 获取到达时间离当前时间最接近的时间事件
+ time_event = aeSearchNearestTimer()
- # 计算最接近的时间事件距离到达还有多少毫秒
- remaind_ms = time_event.when - unix_ts_now()
+ # 计算最接近的时间事件距离到达还有多少毫秒
+ remaind_ms = time_event.when - unix_ts_now()
- # 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0
- if remaind_ms < 0:
- remaind_ms = 0
+ # 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0
+ if remaind_ms < 0:
+ remaind_ms = 0
- # 根据 remaind_ms 的值,创建 timeval
- timeval = create_timeval_with_ms(remaind_ms)
+ # 根据 remaind_ms 的值,创建 timeval
+ timeval = create_timeval_with_ms(remaind_ms)
- # 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定
- aeApiPoll(timeval)
+ # 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定
+ aeApiPoll(timeval)
- # 处理所有已产生的文件事件
- procesFileEvents()
+ # 处理所有已产生的文件事件
+ procesFileEvents()
- # 处理所有已到达的时间事件
- processTimeEvents()
+ # 处理所有已到达的时间事件
+ processTimeEvents()
```
-将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下:
+将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下:
```python
-def main():
+def main():
- # 初始化服务器
- init_server()
+ # 初始化服务器
+ init_server()
- # 一直处理事件,直到服务器关闭为止
- while server_is_not_shutdown():
- aeProcessEvents()
+ # 一直处理事件,直到服务器关闭为止
+ while server_is_not_shutdown():
+ aeProcessEvents()
- # 服务器关闭,执行清理操作
- clean_server()
+ # 服务器关闭,执行清理操作
+ clean_server()
```
从事件处理的角度来看,服务器运行流程如下:
-
+
-# 十一、复制
+# 十一、复制
-通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。
+通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。
一个从服务器只能有一个主服务器,并且不支持主主复制。
-## 连接过程
+## 连接过程
-1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
+1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
-2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
+2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
-3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。
+3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。
-## 主从链
+## 主从链
随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。
-
+
-# 十二、Sentinel
+# 十二、Sentinel
Sentinel(哨兵)可以监听主服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。
-# 十三、分片
+# 十三、分片
分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。
-假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
+假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0~1000 的存储到实例 R0 中,用户 id 从 1001~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
主要有三种分片方式:
-- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
-- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
-- 服务器分片:Redis Cluster。
+- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
+- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
+- 服务器分片:Redis Cluster。
-# 十四、一个简单的论坛系统分析
+# 十四、一个简单的论坛系统分析
该论坛系统功能如下:
-- 可以发布文章;
-- 可以对文章进行点赞;
-- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示;
+- 可以发布文章;
+- 可以对文章进行点赞;
+- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示;
-## 文章信息
+## 文章信息
-文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。
+文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。
-Redis 没有关系型数据库中的表这一概念来将同类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID,通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617,其中 article 为命名空间,ID 为 92617。
+Redis 没有关系型数据库中的表这一概念来将同类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID,通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617,其中 article 为命名空间,ID 为 92617。
-
+
-## 点赞功能
+## 点赞功能
-当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作,还必须记录该用户已经对该文章进行了点赞,防止用户点赞次数超过 1。可以建立文章的已投票用户集合来进行记录。
+当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作,还必须记录该用户已经对该文章进行了点赞,防止用户点赞次数超过 1。可以建立文章的已投票用户集合来进行记录。
为了节约内存,规定一篇文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定。
-
+
-## 对文章进行排序
+## 对文章进行排序
-为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的)
+为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的)
-
+
-# 参考资料
+# 参考资料
-- Carlson J L. Redis in Action[J]. Media.johnwiley.com.au, 2013.
-- [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html)
-- [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)
-- [Redis 应用场景](http://www.scienjus.com/redis-use-case/)
-- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/)
+- Carlson J L. Redis in Action[J]. Media.johnwiley.com.au, 2013.
+- [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html)
+- [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)
+- [Redis 应用场景](http://www.scienjus.com/redis-use-case/)
+- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/)
+---bottom---CyC---
+![](index_files/6019b2db-bc3e-4408-b6d8-96025f4481d6.png)
+![](index_files/fb327611-7e2b-4f2f-9f5b-38592d408f07.png)
+![](index_files/cd5fbcff-3f35-43a6-8ffa-082a93ce0f0e.png)
+![](index_files/7bd202a7-93d4-4f3a-a878-af68ae25539a.png)
+![](index_files/1202b2d6-9469-4251-bd47-ca6034fb6116.png)
+![](index_files/bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg)
+![](index_files/395a9e83-b1a1-4a1d-b170-d081e7bb5bab.png)
+![](index_files/dda1608d-26e0-4f10-8327-a459969b150a.png)
+![](index_files/7c54de21-e2ff-402e-bc42-4037de1c1592.png)
+![](index_files/485fdf34-ccf8-4185-97c6-17374ee719a0.png)
+![](index_files/f7d170a3-e446-4a64-ac2d-cb95028f81a8.png)
diff --git a/notes/SQL.md b/notes/SQL.md
index 107156e0..b304ef3c 100644
--- a/notes/SQL.md
+++ b/notes/SQL.md
@@ -1,350 +1,322 @@
-
-* [一、基础](#一基础)
-* [二、创建表](#二创建表)
-* [三、修改表](#三修改表)
-* [四、插入](#四插入)
-* [五、更新](#五更新)
-* [六、删除](#六删除)
-* [七、查询](#七查询)
-* [八、排序](#八排序)
-* [九、过滤](#九过滤)
-* [十、通配符](#十通配符)
-* [十一、计算字段](#十一计算字段)
-* [十二、函数](#十二函数)
-* [十三、分组](#十三分组)
-* [十四、子查询](#十四子查询)
-* [十五、连接](#十五连接)
-* [十六、组合查询](#十六组合查询)
-* [十七、视图](#十七视图)
-* [十八、存储过程](#十八存储过程)
-* [十九、游标](#十九游标)
-* [二十、触发器](#二十触发器)
-* [二十一、事务处理](#二十一事务处理)
-* [二十二、字符集](#二十二字符集)
-* [二十三、权限管理](#二十三权限管理)
-* [参考资料](#参考资料)
-
-
-
-# 一、基础
+# 一、基础
模式定义了数据如何存储、存储什么样的数据以及数据如何分解等信息,数据库和表都有模式。
主键的值不允许修改,也不允许复用(不能使用已经删除的主键值赋给新数据行的主键)。
-SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。
+SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。
-SQL 语句不区分大小写,但是数据库表名、列名和值是否区分依赖于具体的 DBMS 以及配置。
+SQL 语句不区分大小写,但是数据库表名、列名和值是否区分依赖于具体的 DBMS 以及配置。
-SQL 支持以下三种注释:
+SQL 支持以下三种注释:
```sql
-# 注释
-SELECT *
-FROM mytable; -- 注释
-/* 注释1
- 注释2 */
+# 注释
+SELECT *
+FROM mytable; -- 注释
+/* 注释1
+ 注释2 */
```
数据库创建与使用:
```sql
-CREATE DATABASE test;
-USE test;
+CREATE DATABASE test;
+USE test;
```
-# 二、创建表
+# 二、创建表
```sql
-CREATE TABLE mytable (
- id INT NOT NULL AUTO_INCREMENT,
- col1 INT NOT NULL DEFAULT 1,
- col2 VARCHAR(45) NULL,
- col3 DATE NULL,
- PRIMARY KEY (`id`));
+CREATE TABLE mytable (
+ id INT NOT NULL AUTO_INCREMENT,
+ col1 INT NOT NULL DEFAULT 1,
+ col2 VARCHAR(45) NULL,
+ col3 DATE NULL,
+ PRIMARY KEY (`id`));
```
-# 三、修改表
+# 三、修改表
添加列
```sql
-ALTER TABLE mytable
-ADD col CHAR(20);
+ALTER TABLE mytable
+ADD col CHAR(20);
```
删除列
```sql
-ALTER TABLE mytable
-DROP COLUMN col;
+ALTER TABLE mytable
+DROP COLUMN col;
```
删除表
```sql
-DROP TABLE mytable;
+DROP TABLE mytable;
```
-# 四、插入
+# 四、插入
普通插入
```sql
-INSERT INTO mytable(col1, col2)
-VALUES(val1, val2);
+INSERT INTO mytable(col1, col2)
+VALUES(val1, val2);
```
插入检索出来的数据
```sql
-INSERT INTO mytable1(col1, col2)
-SELECT col1, col2
-FROM mytable2;
+INSERT INTO mytable1(col1, col2)
+SELECT col1, col2
+FROM mytable2;
```
将一个表的内容插入到一个新表
```sql
-CREATE TABLE newtable AS
-SELECT * FROM mytable;
+CREATE TABLE newtable AS
+SELECT * FROM mytable;
```
-# 五、更新
+# 五、更新
```sql
-UPDATE mytable
-SET col = val
-WHERE id = 1;
+UPDATE mytable
+SET col = val
+WHERE id = 1;
```
-# 六、删除
+# 六、删除
```sql
-DELETE FROM mytable
-WHERE id = 1;
+DELETE FROM mytable
+WHERE id = 1;
```
-**TRUNCATE TABLE** 可以清空表,也就是删除所有行。
+**TRUNCATE TABLE** 可以清空表,也就是删除所有行。
```sql
-TRUNCATE TABLE mytable;
+TRUNCATE TABLE mytable;
```
-使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。
+使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。
-# 七、查询
+# 七、查询
-## DISTINCT
+## DISTINCT
相同值只会出现一次。它作用于所有列,也就是说所有列的值都相同才算相同。
```sql
-SELECT DISTINCT col1, col2
-FROM mytable;
+SELECT DISTINCT col1, col2
+FROM mytable;
```
-## LIMIT
+## LIMIT
-限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。
+限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。
-返回前 5 行:
+返回前 5 行:
```sql
-SELECT *
-FROM mytable
-LIMIT 5;
+SELECT *
+FROM mytable
+LIMIT 5;
```
```sql
-SELECT *
-FROM mytable
-LIMIT 0, 5;
+SELECT *
+FROM mytable
+LIMIT 0, 5;
```
-返回第 3 \~ 5 行:
+返回第 3 ~ 5 行:
```sql
-SELECT *
-FROM mytable
-LIMIT 2, 3;
+SELECT *
+FROM mytable
+LIMIT 2, 3;
```
-# 八、排序
+# 八、排序
-- **ASC** :升序(默认)
-- **DESC** :降序
+- **ASC**:升序(默认)
+- **DESC**:降序
可以按多个列进行排序,并且为每个列指定不同的排序方式:
```sql
-SELECT *
-FROM mytable
-ORDER BY col1 DESC, col2 ASC;
+SELECT *
+FROM mytable
+ORDER BY col1 DESC, col2 ASC;
```
-# 九、过滤
+# 九、过滤
-不进行过滤的数据非常大,导致通过网络传输了多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。
+不进行过滤的数据非常大,导致通过网络传输了多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。
```sql
-SELECT *
-FROM mytable
-WHERE col IS NULL;
+SELECT *
+FROM mytable
+WHERE col IS NULL;
```
-下表显示了 WHERE 子句可用的操作符
+下表显示了 WHERE 子句可用的操作符
-| 操作符 | 说明 |
-| :---: | :---: |
-| = | 等于 |
-| < | 小于 |
-| > | 大于 |
-| <> != | 不等于 |
-| <= !> | 小于等于 |
-| >= !< | 大于等于 |
-| BETWEEN | 在两个值之间 |
-| IS NULL | 为 NULL 值 |
+| 操作符 | 说明 |
+| :---: | :---: |
+| = | 等于 |
+| < | 小于 |
+| > | 大于 |
+| <> != | 不等于 |
+| <= !> | 小于等于 |
+| >= !< | 大于等于 |
+| BETWEEN | 在两个值之间 |
+| IS NULL | 为 NULL 值 |
-应该注意到,NULL 与 0、空字符串都不同。
+应该注意到,NULL 与 0、空字符串都不同。
-**AND 和 OR** 用于连接多个过滤条件。优先处理 AND,当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰。
+**AND 和 OR** 用于连接多个过滤条件。优先处理 AND,当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰。
-**IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。
+**IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。
-**NOT** 操作符用于否定一个条件。
+**NOT** 操作符用于否定一个条件。
-# 十、通配符
+# 十、通配符
通配符也是用在过滤语句中,但它只能用于文本字段。
-- **%** 匹配 >=0 个任意字符;
+- **%** 匹配 >=0 个任意字符;
-- **\_** 匹配 ==1 个任意字符;
+- **\_** 匹配 ==1 个任意字符;
-- **[ ]** 可以匹配集合内的字符,例如 [ab] 将匹配字符 a 或者 b。用脱字符 ^ 可以对其进行否定,也就是不匹配集合内的字符。
+- **[ ]** 可以匹配集合内的字符,例如 [ab] 将匹配字符 a 或者 b。用脱字符 ^ 可以对其进行否定,也就是不匹配集合内的字符。
-使用 Like 来进行通配符匹配。
+使用 Like 来进行通配符匹配。
```sql
-SELECT *
-FROM mytable
-WHERE col LIKE '[^AB]%'; -- 不以 A 和 B 开头的任意文本
+SELECT *
+FROM mytable
+WHERE col LIKE '[^AB]%'; -- 不以 A 和 B 开头的任意文本
```
不要滥用通配符,通配符位于开头处匹配会非常慢。
-# 十一、计算字段
+# 十一、计算字段
在数据库服务器上完成数据的转换和格式化的工作往往比客户端上快得多,并且转换和格式化后的数据量更少的话可以减少网络通信量。
-计算字段通常需要使用 **AS** 来取别名,否则输出的时候字段名为计算表达式。
+计算字段通常需要使用 **AS** 来取别名,否则输出的时候字段名为计算表达式。
```sql
-SELECT col1 * col2 AS alias
-FROM mytable;
+SELECT col1 * col2 AS alias
+FROM mytable;
```
-**CONCAT()** 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 **TRIM()** 可以去除首尾空格。
+**CONCAT()** 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 **TRIM()** 可以去除首尾空格。
```sql
-SELECT CONCAT(TRIM(col1), '(', TRIM(col2), ')') AS concat_col
-FROM mytable;
+SELECT CONCAT(TRIM(col1), '(', TRIM(col2), ')') AS concat_col
+FROM mytable;
```
-# 十二、函数
+# 十二、函数
-各个 DBMS 的函数都是不相同的,因此不可移植,以下主要是 MySQL 的函数。
+各个 DBMS 的函数都是不相同的,因此不可移植,以下主要是 MySQL 的函数。
-## 汇总
+## 汇总
-|函 数 |说 明|
-| :---: | :---: |
-| AVG() | 返回某列的平均值 |
-| COUNT() | 返回某列的行数 |
-| MAX() | 返回某列的最大值 |
-| MIN() | 返回某列的最小值 |
-| SUM() |返回某列值之和 |
+|函 数 |说 明|
+| :---: | :---: |
+| AVG() | 返回某列的平均值 |
+| COUNT() | 返回某列的行数 |
+| MAX() | 返回某列的最大值 |
+| MIN() | 返回某列的最小值 |
+| SUM() |返回某列值之和 |
-AVG() 会忽略 NULL 行。
+AVG() 会忽略 NULL 行。
-使用 DISTINCT 可以让汇总函数值汇总不同的值。
+使用 DISTINCT 可以让汇总函数值汇总不同的值。
```sql
-SELECT AVG(DISTINCT col1) AS avg_col
-FROM mytable;
+SELECT AVG(DISTINCT col1) AS avg_col
+FROM mytable;
```
-## 文本处理
+## 文本处理
-| 函数 | 说明 |
-| :---: | :---: |
-| LEFT() | 左边的字符 |
-| RIGHT() | 右边的字符 |
-| LOWER() | 转换为小写字符 |
-| UPPER() | 转换为大写字符 |
-| LTRIM() | 去除左边的空格 |
-| RTRIM() | 去除右边的空格 |
-| LENGTH() | 长度 |
-| SOUNDEX() | 转换为语音值 |
+| 函数 | 说明 |
+| :---: | :---: |
+| LEFT() | 左边的字符 |
+| RIGHT() | 右边的字符 |
+| LOWER() | 转换为小写字符 |
+| UPPER() | 转换为大写字符 |
+| LTRIM() | 去除左边的空格 |
+| RTRIM() | 去除右边的空格 |
+| LENGTH() | 长度 |
+| SOUNDEX() | 转换为语音值 |
-其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。
+其中,**SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。
```sql
-SELECT *
-FROM mytable
-WHERE SOUNDEX(col1) = SOUNDEX('apple')
+SELECT *
+FROM mytable
+WHERE SOUNDEX(col1) = SOUNDEX('apple')
```
-## 日期和时间处理
+## 日期和时间处理
-- 日期格式:YYYY-MM-DD
-- 时间格式:HH:MM:SS
+- 日期格式:YYYY-MM-DD
+- 时间格式:HH:MM:SS
-|函 数 | 说 明|
-| :---: | :---: |
-| AddDate() | 增加一个日期(天、周等)|
-| AddTime() | 增加一个时间(时、分等)|
-| CurDate() | 返回当前日期 |
-| CurTime() | 返回当前时间 |
-| Date() |返回日期时间的日期部分|
-| DateDiff() |计算两个日期之差|
-| Date_Add() |高度灵活的日期运算函数|
-| Date_Format() |返回一个格式化的日期或时间串|
-| Day()| 返回一个日期的天数部分|
-| DayOfWeek() |对于一个日期,返回对应的星期几|
-| Hour() |返回一个时间的小时部分|
-| Minute() |返回一个时间的分钟部分|
-| Month() |返回一个日期的月份部分|
-| Now() |返回当前日期和时间|
-| Second() |返回一个时间的秒部分|
-| Time() |返回一个日期时间的时间部分|
-| Year() |返回一个日期的年份部分|
+|函 数 | 说 明|
+| :---: | :---: |
+| AddDate() | 增加一个日期(天、周等)|
+| AddTime() | 增加一个时间(时、分等)|
+| CurDate() | 返回当前日期 |
+| CurTime() | 返回当前时间 |
+| Date() |返回日期时间的日期部分|
+| DateDiff() |计算两个日期之差|
+| Date_Add() |高度灵活的日期运算函数|
+| Date_Format() |返回一个格式化的日期或时间串|
+| Day()| 返回一个日期的天数部分|
+| DayOfWeek() |对于一个日期,返回对应的星期几|
+| Hour() |返回一个时间的小时部分|
+| Minute() |返回一个时间的分钟部分|
+| Month() |返回一个日期的月份部分|
+| Now() |返回当前日期和时间|
+| Second() |返回一个时间的秒部分|
+| Time() |返回一个日期时间的时间部分|
+| Year() |返回一个日期的年份部分|
```sql
-mysql> SELECT NOW();
+mysql> SELECT NOW();
```
```
-2018-4-14 20:25:11
+2018-4-14 20:25:11
```
-## 数值处理
+## 数值处理
-| 函数 | 说明 |
-| :---: | :---: |
-| SIN() | 正弦 |
-| COS() | 余弦 |
-| TAN() | 正切 |
-| ABS() | 绝对值 |
-| SQRT() | 平方根 |
-| MOD() | 余数 |
-| EXP() | 指数 |
-| PI() | 圆周率 |
-| RAND() | 随机数 |
+| 函数 | 说明 |
+| :---: | :---: |
+| SIN() | 正弦 |
+| COS() | 余弦 |
+| TAN() | 正切 |
+| ABS() | 绝对值 |
+| SQRT() | 平方根 |
+| MOD() | 余数 |
+| EXP() | 指数 |
+| PI() | 圆周率 |
+| RAND() | 随机数 |
-# 十三、分组
+# 十三、分组
分组就是把具有相同的数据值的行放在同一组中。
@@ -353,186 +325,186 @@ mysql> SELECT NOW();
指定的分组字段除了能按该字段进行分组,也会自动按该字段进行排序。
```sql
-SELECT col, COUNT(*) AS num
-FROM mytable
-GROUP BY col;
+SELECT col, COUNT(*) AS num
+FROM mytable
+GROUP BY col;
```
-GROUP BY 自动按分组字段进行排序,ORDER BY 也可以按汇总字段来进行排序。
+GROUP BY 自动按分组字段进行排序,ORDER BY 也可以按汇总字段来进行排序。
```sql
-SELECT col, COUNT(*) AS num
-FROM mytable
-GROUP BY col
-ORDER BY num;
+SELECT col, COUNT(*) AS num
+FROM mytable
+GROUP BY col
+ORDER BY num;
```
-WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤。
+WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤。
```sql
-SELECT col, COUNT(*) AS num
-FROM mytable
-WHERE col > 2
-GROUP BY col
-HAVING num >= 2;
+SELECT col, COUNT(*) AS num
+FROM mytable
+WHERE col > 2
+GROUP BY col
+HAVING num >= 2;
```
分组规定:
-- GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前;
-- 除了汇总字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出;
-- NULL 的行会单独分为一组;
-- 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。
+- GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前;
+- 除了汇总字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出;
+- NULL 的行会单独分为一组;
+- 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。
-# 十四、子查询
+# 十四、子查询
子查询中只能返回一个字段的数据。
-可以将子查询的结果作为 WHRER 语句的过滤条件:
+可以将子查询的结果作为 WHRER 语句的过滤条件:
```sql
-SELECT *
-FROM mytable1
-WHERE col1 IN (SELECT col2
- FROM mytable2);
+SELECT *
+FROM mytable1
+WHERE col1 IN (SELECT col2
+ FROM mytable2);
```
下面的语句可以检索出客户的订单数量,子查询语句会对第一个查询检索出的每个客户执行一次:
```sql
-SELECT cust_name, (SELECT COUNT(*)
- FROM Orders
- WHERE Orders.cust_id = Customers.cust_id)
- AS orders_num
-FROM Customers
-ORDER BY cust_name;
+SELECT cust_name, (SELECT COUNT(*)
+ FROM Orders
+ WHERE Orders.cust_id = Customers.cust_id)
+ AS orders_num
+FROM Customers
+ORDER BY cust_name;
```
-# 十五、连接
+# 十五、连接
-连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 WHERE。
+连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 WHERE。
连接可以替换子查询,并且比子查询的效率一般会更快。
-可以用 AS 给列名、计算字段和表名取别名,给表名取别名是为了简化 SQL 语句以及连接相同表。
+可以用 AS 给列名、计算字段和表名取别名,给表名取别名是为了简化 SQL 语句以及连接相同表。
-## 内连接
+## 内连接
-内连接又称等值连接,使用 INNER JOIN 关键字。
+内连接又称等值连接,使用 INNER JOIN 关键字。
```sql
-SELECT A.value, B.value
-FROM tablea AS A INNER JOIN tableb AS B
-ON A.key = B.key;
+SELECT A.value, B.value
+FROM tablea AS A INNER JOIN tableb AS B
+ON A.key = B.key;
```
-可以不明确使用 INNER JOIN,而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。
+可以不明确使用 INNER JOIN,而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。
```sql
-SELECT A.value, B.value
-FROM tablea AS A, tableb AS B
-WHERE A.key = B.key;
+SELECT A.value, B.value
+FROM tablea AS A, tableb AS B
+WHERE A.key = B.key;
```
在没有条件语句的情况下返回笛卡尔积。
-## 自连接
+## 自连接
自连接可以看成内连接的一种,只是连接的表是自身而已。
-一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。
+一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。
子查询版本
```sql
-SELECT name
-FROM employee
-WHERE department = (
- SELECT department
- FROM employee
- WHERE name = "Jim");
+SELECT name
+FROM employee
+WHERE department = (
+ SELECT department
+ FROM employee
+ WHERE name = "Jim");
```
自连接版本
```sql
-SELECT e1.name
-FROM employee AS e1 INNER JOIN employee AS e2
-ON e1.department = e2.department
- AND e2.name = "Jim";
+SELECT e1.name
+FROM employee AS e1 INNER JOIN employee AS e2
+ON e1.department = e2.department
+ AND e2.name = "Jim";
```
-## 自然连接
+## 自然连接
自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。
内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。
```sql
-SELECT A.value, B.value
-FROM tablea AS A NATURAL JOIN tableb AS B;
+SELECT A.value, B.value
+FROM tablea AS A NATURAL JOIN tableb AS B;
```
-## 外连接
+## 外连接
外连接保留了没有关联的那些行。分为左外连接,右外连接以及全外连接,左外连接就是保留左表没有关联的行。
检索所有顾客的订单信息,包括还没有订单信息的顾客。
```sql
-SELECT Customers.cust_id, Orders.order_num
-FROM Customers LEFT OUTER JOIN Orders
-ON Customers.cust_id = Orders.cust_id;
+SELECT Customers.cust_id, Orders.order_num
+FROM Customers LEFT OUTER JOIN Orders
+ON Customers.cust_id = Orders.cust_id;
```
-customers 表:
+customers 表:
-| cust_id | cust_name |
-| :---: | :---: |
-| 1 | a |
-| 2 | b |
-| 3 | c |
+| cust_id | cust_name |
+| :---: | :---: |
+| 1 | a |
+| 2 | b |
+| 3 | c |
-orders 表:
+orders 表:
-| order_id | cust_id |
-| :---: | :---: |
-|1 | 1 |
-|2 | 1 |
-|3 | 3 |
-|4 | 3 |
+| order_id | cust_id |
+| :---: | :---: |
+|1 | 1 |
+|2 | 1 |
+|3 | 3 |
+|4 | 3 |
结果:
-| cust_id | cust_name | order_id |
-| :---: | :---: | :---: |
-| 1 | a | 1 |
-| 1 | a | 2 |
-| 3 | c | 3 |
-| 3 | c | 4 |
-| 2 | b | Null |
+| cust_id | cust_name | order_id |
+| :---: | :---: | :---: |
+| 1 | a | 1 |
+| 1 | a | 2 |
+| 3 | c | 3 |
+| 3 | c | 4 |
+| 2 | b | Null |
-# 十六、组合查询
+# 十六、组合查询
-使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。
+使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。
每个查询必须包含相同的列、表达式和聚集函数。
-默认会去除相同行,如果需要保留相同行,使用 UNION ALL。
+默认会去除相同行,如果需要保留相同行,使用 UNION ALL。
-只能包含一个 ORDER BY 子句,并且必须位于语句的最后。
+只能包含一个 ORDER BY 子句,并且必须位于语句的最后。
```sql
-SELECT col
-FROM mytable
-WHERE col = 1
+SELECT col
+FROM mytable
+WHERE col = 1
UNION
-SELECT col
-FROM mytable
-WHERE col =2;
+SELECT col
+FROM mytable
+WHERE col =2;
```
-# 十七、视图
+# 十七、视图
视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。
@@ -540,57 +512,57 @@ WHERE col =2;
视图具有如下好处:
-- 简化复杂的 SQL 操作,比如复杂的连接;
-- 只使用实际表的一部分数据;
-- 通过只给用户访问视图的权限,保证数据的安全性;
-- 更改数据格式和表示。
+- 简化复杂的 SQL 操作,比如复杂的连接;
+- 只使用实际表的一部分数据;
+- 通过只给用户访问视图的权限,保证数据的安全性;
+- 更改数据格式和表示。
```sql
-CREATE VIEW myview AS
-SELECT Concat(col1, col2) AS concat_col, col3*col4 AS compute_col
-FROM mytable
-WHERE col5 = val;
+CREATE VIEW myview AS
+SELECT Concat(col1, col2) AS concat_col, col3*col4 AS compute_col
+FROM mytable
+WHERE col5 = val;
```
-# 十八、存储过程
+# 十八、存储过程
-存储过程可以看成是对一系列 SQL 操作的批处理;
+存储过程可以看成是对一系列 SQL 操作的批处理;
使用存储过程的好处:
-- 代码封装,保证了一定的安全性;
-- 代码复用;
-- 由于是预先编译,因此具有很高的性能。
+- 代码封装,保证了一定的安全性;
+- 代码复用;
+- 由于是预先编译,因此具有很高的性能。
-命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。
+命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。
-包含 in、out 和 inout 三种参数。
+包含 in、out 和 inout 三种参数。
-给变量赋值都需要用 select into 语句。
+给变量赋值都需要用 select into 语句。
每次只能给一个变量赋值,不支持集合的操作。
```sql
-delimiter //
+delimiter //
-create procedure myprocedure( out ret int )
- begin
- declare y int;
- select sum(col1)
- from mytable
- into y;
- select y*y into ret;
- end //
+create procedure myprocedure( out ret int )
+ begin
+ declare y int;
+ select sum(col1)
+ from mytable
+ into y;
+ select y*y into ret;
+ end //
-delimiter ;
+delimiter ;
```
```sql
-call myprocedure(@ret);
-select @ret;
+call myprocedure(@ret);
+select @ret;
```
-# 十九、游标
+# 十九、游标
在存储过程中使用游标可以对一个结果集进行移动遍历。
@@ -598,171 +570,171 @@ select @ret;
使用游标的四个步骤:
-1. 声明游标,这个过程没有实际检索出数据;
-2. 打开游标;
-3. 取出数据;
-4. 关闭游标;
+1. 声明游标,这个过程没有实际检索出数据;
+2. 打开游标;
+3. 取出数据;
+4. 关闭游标;
```sql
-delimiter //
-create procedure myprocedure(out ret int)
- begin
- declare done boolean default 0;
+delimiter //
+create procedure myprocedure(out ret int)
+ begin
+ declare done boolean default 0;
- declare mycursor cursor for
- select col1 from mytable;
- # 定义了一个 continue handler,当 sqlstate '02000' 这个条件出现时,会执行 set done = 1
- declare continue handler for sqlstate '02000' set done = 1;
+ declare mycursor cursor for
+ select col1 from mytable;
+ # 定义了一个 continue handler,当 sqlstate '02000' 这个条件出现时,会执行 set done = 1
+ declare continue handler for sqlstate '02000' set done = 1;
- open mycursor;
+ open mycursor;
- repeat
- fetch mycursor into ret;
- select ret;
- until done end repeat;
+ repeat
+ fetch mycursor into ret;
+ select ret;
+ until done end repeat;
- close mycursor;
- end //
- delimiter ;
+ close mycursor;
+ end //
+ delimiter ;
```
-# 二十、触发器
+# 二十、触发器
触发器会在某个表执行以下语句时而自动执行:DELETE、INSERT、UPDATE。
-触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化,AFTER 用于审计跟踪,将修改记录到另外一张表中。
+触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化,AFTER 用于审计跟踪,将修改记录到另外一张表中。
-INSERT 触发器包含一个名为 NEW 的虚拟表。
+INSERT 触发器包含一个名为 NEW 的虚拟表。
```sql
-CREATE TRIGGER mytrigger AFTER INSERT ON mytable
-FOR EACH ROW SELECT NEW.col into @result;
+CREATE TRIGGER mytrigger AFTER INSERT ON mytable
+FOR EACH ROW SELECT NEW.col into @result;
-SELECT @result; -- 获取结果
+SELECT @result; -- 获取结果
```
-DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。
+DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。
-UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改地,而 OLD 是只读的。
+UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改地,而 OLD 是只读的。
-MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。
+MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。
-# 二十一、事务处理
+# 二十一、事务处理
基本术语:
-- 事务(transaction)指一组 SQL 语句;
-- 回退(rollback)指撤销指定 SQL 语句的过程;
-- 提交(commit)指将未存储的 SQL 语句结果写入数据库表;
-- 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),你可以对它发布回退(与回退整个事务处理不同)。
+- 事务(transaction)指一组 SQL 语句;
+- 回退(rollback)指撤销指定 SQL 语句的过程;
+- 提交(commit)指将未存储的 SQL 语句结果写入数据库表;
+- 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),你可以对它发布回退(与回退整个事务处理不同)。
-不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。
+不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。
-MySQL 的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
+MySQL 的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
-通过设置 autocommit 为 0 可以取消自动提交,直到 autocommit 被设置为 1 才会提交;autocommit 标记是针对每个连接而不是针对服务器的。
+通过设置 autocommit 为 0 可以取消自动提交,直到 autocommit 被设置为 1 才会提交;autocommit 标记是针对每个连接而不是针对服务器的。
-如果没有设置保留点,ROLLBACK 会回退到 START TRANSACTION 语句处;如果设置了保留点,并且在 ROLLBACK 中指定该保留点,则会回退到该保留点。
+如果没有设置保留点,ROLLBACK 会回退到 START TRANSACTION 语句处;如果设置了保留点,并且在 ROLLBACK 中指定该保留点,则会回退到该保留点。
```sql
-START TRANSACTION
-// ...
-SAVEPOINT delete1
-// ...
-ROLLBACK TO delete1
-// ...
+START TRANSACTION
+// ...
+SAVEPOINT delete1
+// ...
+ROLLBACK TO delete1
+// ...
COMMIT
```
-# 二十二、字符集
+# 二十二、字符集
基本术语:
-- 字符集为字母和符号的集合;
-- 编码为某个字符集成员的内部表示;
-- 校对字符指定如何比较,主要用于排序和分组。
+- 字符集为字母和符号的集合;
+- 编码为某个字符集成员的内部表示;
+- 校对字符指定如何比较,主要用于排序和分组。
除了给表指定字符集和校对外,也可以给列指定:
```sql
-CREATE TABLE mytable
-(col VARCHAR(10) CHARACTER SET latin COLLATE latin1_general_ci )
-DEFAULT CHARACTER SET hebrew COLLATE hebrew_general_ci;
+CREATE TABLE mytable
+(col VARCHAR(10) CHARACTER SET latin COLLATE latin1_general_ci )
+DEFAULT CHARACTER SET hebrew COLLATE hebrew_general_ci;
```
可以在排序、分组时指定校对:
```sql
-SELECT *
-FROM mytable
-ORDER BY col COLLATE latin1_general_ci;
+SELECT *
+FROM mytable
+ORDER BY col COLLATE latin1_general_ci;
```
-# 二十三、权限管理
+# 二十三、权限管理
-MySQL 的账户信息保存在 mysql 这个数据库中。
+MySQL 的账户信息保存在 mysql 这个数据库中。
```sql
-USE mysql;
-SELECT user FROM user;
+USE mysql;
+SELECT user FROM user;
```
-**创建账户**
+**创建账户**
```sql
-CREATE USER myuser IDENTIFIED BY 'mypassword';
+CREATE USER myuser IDENTIFIED BY 'mypassword';
```
新创建的账户没有任何权限。
-**修改账户名**
+**修改账户名**
```sql
-RENAME myuser TO newuser;
+RENAME myuser TO newuser;
```
-**删除账户**
+**删除账户**
```sql
-DROP USER myuser;
+DROP USER myuser;
```
-**查看权限**
+**查看权限**
```sql
-SHOW GRANTS FOR myuser;
+SHOW GRANTS FOR myuser;
```
-**授予权限**
+**授予权限**
```sql
-GRANT SELECT, INSERT ON mydatabase.* TO myuser;
+GRANT SELECT, INSERT ON mydatabase.* TO myuser;
```
-账户用 username@host 的形式定义,username@% 使用的是默认主机名。
+账户用 username@host 的形式定义,username@% 使用的是默认主机名。
-**删除权限**
+**删除权限**
```sql
-REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;
+REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;
```
-GRANT 和 REVOKE 可在几个层次上控制访问权限:
+GRANT 和 REVOKE 可在几个层次上控制访问权限:
-- 整个服务器,使用 GRANT ALL 和 REVOKE ALL;
-- 整个数据库,使用 ON database.\*;
-- 特定的表,使用 ON database.table;
-- 特定的列;
-- 特定的存储过程。
+- 整个服务器,使用 GRANT ALL 和 REVOKE ALL;
+- 整个数据库,使用 ON database.\*;
+- 特定的表,使用 ON database.table;
+- 特定的列;
+- 特定的存储过程。
-**更改密码**
+**更改密码**
-必须使用 Password() 函数
+必须使用 Password() 函数
```sql
-SET PASSWROD FOR myuser = Password('new_password');
+SET PASSWROD FOR myuser = Password('new_password');
```
-# 参考资料
+# 参考资料
-- BenForta. SQL 必知必会 [M]. 人民邮电出版社, 2013.
+- BenForta. SQL 必知必会 [M]. 人民邮电出版社, 2013.
\ No newline at end of file
diff --git a/notes/Socket.md b/notes/Socket.md
index 1243e6f5..95643050 100644
--- a/notes/Socket.md
+++ b/notes/Socket.md
@@ -1,343 +1,332 @@
-
-* [一、I/O 复用](#一io-复用)
- * [I/O 模型](#io-模型)
- * [select/poll/epoll](#selectpollepoll)
- * [select 和 poll 比较](#select-和-poll-比较)
- * [eopll 工作模式](#eopll-工作模式)
- * [select poll epoll 应用场景](#select-poll-epoll-应用场景)
-* [参考资料](#参考资料)
-
+# 一、I/O 复用
-
-# 一、I/O 复用
-
-## I/O 模型
+## I/O 模型
一个输入操作通常包括两个阶段:
-- 等待数据准备好
-- 从内核向进程复制数据
+- 等待数据准备好
+- 从内核向进程复制数据
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
-Unix 下有五种 I/O 模型:
+Unix 下有五种 I/O 模型:
-- 阻塞式 I/O
-- 非阻塞式 I/O
-- I/O 复用(select 和 poll)
-- 信号驱动式 I/O(SIGIO)
-- 异步 I/O(AIO)
+- 阻塞式 I/O
+- 非阻塞式 I/O
+- I/O 复用(select 和 poll)
+- 信号驱动式 I/O(SIGIO)
+- 异步 I/O(AIO)
-### 1. 阻塞式 I/O
+### 1. 阻塞式 I/O
应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。
-应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。
+应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。
-下图中,recvfrom 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。
+下图中,recvfrom 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。
```c
-ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
+ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
```
-
+![](index_files/1492928416812_4.png)
-### 2. 非阻塞式 I/O
+### 2. 非阻塞式 I/O
-应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式成为轮询(polling)。
+应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式成为轮询(polling)。
-由于 CPU 要处理更多的系统调用,因此这种模型是比较低效的。
+由于 CPU 要处理更多的系统调用,因此这种模型是比较低效的。
-
+![](index_files/1492929000361_5.png)
-### 3. I/O 复用
+### 3. I/O 复用
-使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。
+使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。
-它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。
+它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。
-如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。并且相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。
+如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。并且相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。
-
+![](index_files/1492929444818_6.png)
-### 4. 信号驱动 I/O
+### 4. 信号驱动 I/O
-应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
+应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
-相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。
+相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。
-
+![](index_files/1492929553651_7.png)
-### 5. 异步 I/O
+### 5. 异步 I/O
-进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
+进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
-异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
+异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
-
+![](index_files/1492930243286_8.png)
-### 6. 同步 I/O 与异步 I/O
+### 6. 同步 I/O 与异步 I/O
-- 同步 I/O:应用进程在调用 recvfrom 操作时会阻塞。
-- 异步 I/O:不会阻塞。
+- 同步 I/O:应用进程在调用 recvfrom 操作时会阻塞。
+- 异步 I/O:不会阻塞。
-阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O,虽然非阻塞式 I/O 和信号驱动 I/O 在等待数据阶段不会阻塞,但是在之后的将数据从内核复制到应用进程这个操作会阻塞。
+阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O,虽然非阻塞式 I/O 和信号驱动 I/O 在等待数据阶段不会阻塞,但是在之后的将数据从内核复制到应用进程这个操作会阻塞。
-### 7. 五大 I/O 模型比较
+### 7. 五大 I/O 模型比较
-前四种 I/O 模型的主要区别在于第一个阶段,而第二个阶段是一样的:将数据从内核复制到应用进程过程中,应用进程会被阻塞。
+前四种 I/O 模型的主要区别在于第一个阶段,而第二个阶段是一样的:将数据从内核复制到应用进程过程中,应用进程会被阻塞。
-
+![](index_files/1492928105791_3.png)
-## select/poll/epoll
+## select/poll/epoll
-这三个都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。
+这三个都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。
-### 1. select
+### 1. select
```c
-int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
+int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
```
-fd_set 表示描述符集合类型,有三个参数:readset、writeset 和 exceptset,分别对应读、写、异常条件的描述符集合。
+fd_set 表示描述符集合类型,有三个参数:readset、writeset 和 exceptset,分别对应读、写、异常条件的描述符集合。
-timeout 参数告知内核等待所指定描述符中的任何一个就绪可花多少时间;
+timeout 参数告知内核等待所指定描述符中的任何一个就绪可花多少时间;
-成功调用返回结果大于 0;出错返回结果为 -1;超时返回结果为 0。
+成功调用返回结果大于 0;出错返回结果为 -1;超时返回结果为 0。
-每次调用 select 都需要将 fd_set \*readfds, fd_set \*writefds, fd_set \*exceptfds 链表内容全部从应用进程缓冲复制到内核缓冲。
+每次调用 select 都需要将 fd_set \*readfds, fd_set \*writefds, fd_set \*exceptfds 链表内容全部从应用进程缓冲复制到内核缓冲。
-返回结果中内核并没有声明 fd_set 中哪些描述符已经准备好,所以如果返回值大于 0 时,应用进程需要遍历所有的 fd_set。
+返回结果中内核并没有声明 fd_set 中哪些描述符已经准备好,所以如果返回值大于 0 时,应用进程需要遍历所有的 fd_set。
-select 最多支持 1024 个描述符,其中 1024 由内核的 FD_SETSIZE 决定。如果需要打破该限制可以修改 FD_SETSIZE,然后重新编译内核。
+select 最多支持 1024 个描述符,其中 1024 由内核的 FD_SETSIZE 决定。如果需要打破该限制可以修改 FD_SETSIZE,然后重新编译内核。
```c
-fd_set fd_in, fd_out;
-struct timeval tv;
+fd_set fd_in, fd_out;
+struct timeval tv;
-// Reset the sets
-FD_ZERO( &fd_in );
-FD_ZERO( &fd_out );
+// Reset the sets
+FD_ZERO( &fd_in );
+FD_ZERO( &fd_out );
-// Monitor sock1 for input events
-FD_SET( sock1, &fd_in );
+// Monitor sock1 for input events
+FD_SET( sock1, &fd_in );
-// Monitor sock2 for output events
-FD_SET( sock2, &fd_out );
+// Monitor sock2 for output events
+FD_SET( sock2, &fd_out );
-// Find out which socket has the largest numeric value as select requires it
-int largest_sock = sock1 > sock2 ? sock1 : sock2;
+// Find out which socket has the largest numeric value as select requires it
+int largest_sock = sock1 > sock2 ? sock1 : sock2;
-// Wait up to 10 seconds
-tv.tv_sec = 10;
-tv.tv_usec = 0;
+// Wait up to 10 seconds
+tv.tv_sec = 10;
+tv.tv_usec = 0;
-// Call the select
-int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );
+// Call the select
+int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );
-// Check if select actually succeed
-if ( ret == -1 )
- // report error and abort
-else if ( ret == 0 )
- // timeout; no event detected
+// Check if select actually succeed
+if ( ret == -1 )
+ // report error and abort
+else if ( ret == 0 )
+ // timeout; no event detected
else
{
- if ( FD_ISSET( sock1, &fd_in ) )
- // input event on sock1
+ if ( FD_ISSET( sock1, &fd_in ) )
+ // input event on sock1
- if ( FD_ISSET( sock2, &fd_out ) )
- // output event on sock2
+ if ( FD_ISSET( sock2, &fd_out ) )
+ // output event on sock2
}
```
-### 2. poll
+### 2. poll
```c
-int poll(struct pollfd *fds, unsigned int nfds, int timeout);
+int poll(struct pollfd *fds, unsigned int nfds, int timeout);
```
```c
-struct pollfd {
- int fd; //文件描述符
- short events; //监视的请求事件
- short revents; //已发生的事件
+struct pollfd {
+ int fd; //文件描述符
+ short events; //监视的请求事件
+ short revents; //已发生的事件
};
```
-它和 select 功能基本相同。同样需要每次将描述符从应用进程复制到内核,poll 调用返回后同样需要进行轮询才能知道哪些描述符已经准备好。
+它和 select 功能基本相同。同样需要每次将描述符从应用进程复制到内核,poll 调用返回后同样需要进行轮询才能知道哪些描述符已经准备好。
-poll 取消了 1024 个描述符数量上限,但是数量太大以后不能保证执行效率,因为复制大量内存到内核十分低效,所需时间与描述符数量成正比。
+poll 取消了 1024 个描述符数量上限,但是数量太大以后不能保证执行效率,因为复制大量内存到内核十分低效,所需时间与描述符数量成正比。
-poll 在描述符的重复利用上比 select 的 fd_set 会更好。
+poll 在描述符的重复利用上比 select 的 fd_set 会更好。
-如果在多线程下,如果一个线程对某个描述符调用了 poll 系统调用,但是另一个线程关闭了该描述符,会导致 poll 调用结果不确定,该问题同样出现在 select 中。
+如果在多线程下,如果一个线程对某个描述符调用了 poll 系统调用,但是另一个线程关闭了该描述符,会导致 poll 调用结果不确定,该问题同样出现在 select 中。
```c
-// The structure for two events
-struct pollfd fds[2];
+// The structure for two events
+struct pollfd fds[2];
-// Monitor sock1 for input
-fds[0].fd = sock1;
-fds[0].events = POLLIN;
+// Monitor sock1 for input
+fds[0].fd = sock1;
+fds[0].events = POLLIN;
-// Monitor sock2 for output
-fds[1].fd = sock2;
-fds[1].events = POLLOUT;
+// Monitor sock2 for output
+fds[1].fd = sock2;
+fds[1].events = POLLOUT;
-// Wait 10 seconds
-int ret = poll( &fds, 2, 10000 );
-// Check if poll actually succeed
-if ( ret == -1 )
- // report error and abort
-else if ( ret == 0 )
- // timeout; no event detected
+// Wait 10 seconds
+int ret = poll( &fds, 2, 10000 );
+// Check if poll actually succeed
+if ( ret == -1 )
+ // report error and abort
+else if ( ret == 0 )
+ // timeout; no event detected
else
{
- // If we detect the event, zero it out so we can reuse the structure
- if ( pfd[0].revents & POLLIN )
- pfd[0].revents = 0;
- // input event on sock1
+ // If we detect the event, zero it out so we can reuse the structure
+ if ( pfd[0].revents & POLLIN )
+ pfd[0].revents = 0;
+ // input event on sock1
- if ( pfd[1].revents & POLLOUT )
- pfd[1].revents = 0;
- // output event on sock2
+ if ( pfd[1].revents & POLLOUT )
+ pfd[1].revents = 0;
+ // output event on sock2
}
```
-### 3. epoll
+### 3. epoll
```c
-int epoll_create(int size);
-int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
+int epoll_create(int size);
+int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
+int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
```
-epoll 仅仅适用于 Linux OS。
+epoll 仅仅适用于 Linux OS。
-它是 select 和 poll 的增强版,更加灵活而且没有描述符数量限制。
+它是 select 和 poll 的增强版,更加灵活而且没有描述符数量限制。
它将用户关心的描述符放到内核的一个事件表中,从而只需要在用户空间和内核空间拷贝一次。
-select 和 poll 方式中,进程只有在调用一定的方法后,内核才对所有监视的描述符进行扫描。而 epoll 事先通过 epoll_ctl() 来注册描述符,一旦基于某个描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个描述符,当进程调用 epoll_wait() 时便得到通知。
+select 和 poll 方式中,进程只有在调用一定的方法后,内核才对所有监视的描述符进行扫描。而 epoll 事先通过 epoll_ctl() 来注册描述符,一旦基于某个描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个描述符,当进程调用 epoll_wait() 时便得到通知。
-新版本的 epoll_create(int size) 参数 size 不起任何作用,在旧版本的 epoll 中如果描述符的数量大于 size,不保证服务质量。
+新版本的 epoll_create(int size) 参数 size 不起任何作用,在旧版本的 epoll 中如果描述符的数量大于 size,不保证服务质量。
-epoll_ctl() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。
+epoll_ctl() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。
-epoll_wait() 取出在内核中通过链表维护的 I/O 准备好的描述符,将他们从内核复制到应用进程中,不需要像 select/poll 对注册的所有描述符遍历一遍。
+epoll_wait() 取出在内核中通过链表维护的 I/O 准备好的描述符,将他们从内核复制到应用进程中,不需要像 select/poll 对注册的所有描述符遍历一遍。
-epoll 对多线程编程更有友好,同时多个线程对同一个描述符调用了 epoll_wait() 也不会产生像 select/poll 的不确定情况。或者一个线程调用了 epoll_wait 另一个线程关闭了同一个描述符也不会产生不确定情况。
+epoll 对多线程编程更有友好,同时多个线程对同一个描述符调用了 epoll_wait() 也不会产生像 select/poll 的不确定情况。或者一个线程调用了 epoll_wait 另一个线程关闭了同一个描述符也不会产生不确定情况。
```c
-// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.
-// The function argument is ignored (it was not before, but now it is), so put your favorite number here
-int pollingfd = epoll_create( 0xCAFE );
+// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.
+// The function argument is ignored (it was not before, but now it is), so put your favorite number here
+int pollingfd = epoll_create( 0xCAFE );
-if ( pollingfd < 0 )
- // report error
+if ( pollingfd < 0 )
+ // report error
-// Initialize the epoll structure in case more members are added in future
-struct epoll_event ev = { 0 };
+// Initialize the epoll structure in case more members are added in future
+struct epoll_event ev = { 0 };
-// Associate the connection class instance with the event. You can associate anything
-// you want, epoll does not use this information. We store a connection class pointer, pConnection1
-ev.data.ptr = pConnection1;
+// Associate the connection class instance with the event. You can associate anything
+// you want, epoll does not use this information. We store a connection class pointer, pConnection1
+ev.data.ptr = pConnection1;
-// Monitor for input, and do not automatically rearm the descriptor after the event
-ev.events = EPOLLIN | EPOLLONESHOT;
-// Add the descriptor into the monitoring list. We can do it even if another thread is
-// waiting in epoll_wait - the descriptor will be properly added
-if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 )
- // report error
+// Monitor for input, and do not automatically rearm the descriptor after the event
+ev.events = EPOLLIN | EPOLLONESHOT;
+// Add the descriptor into the monitoring list. We can do it even if another thread is
+// waiting in epoll_wait - the descriptor will be properly added
+if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 )
+ // report error
-// Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen)
-struct epoll_event pevents[ 20 ];
+// Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen)
+struct epoll_event pevents[ 20 ];
-// Wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array
-int ready = epoll_wait( pollingfd, pevents, 20, 10000 );
-// Check if epoll actually succeed
-if ( ret == -1 )
- // report error and abort
-else if ( ret == 0 )
- // timeout; no event detected
+// Wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array
+int ready = epoll_wait( pollingfd, pevents, 20, 10000 );
+// Check if epoll actually succeed
+if ( ret == -1 )
+ // report error and abort
+else if ( ret == 0 )
+ // timeout; no event detected
else
{
- // Check if any events detected
- for ( int i = 0; i < ret; i++ )
- {
- if ( pevents[i].events & EPOLLIN )
- {
- // Get back our connection pointer
- Connection * c = (Connection*) pevents[i].data.ptr;
- c->handleReadEvent();
- }
- }
+ // Check if any events detected
+ for ( int i = 0; i < ret; i++ )
+ {
+ if ( pevents[i].events & EPOLLIN )
+ {
+ // Get back our connection pointer
+ Connection * c = (Connection*) pevents[i].data.ptr;
+ c->handleReadEvent();
+ }
+ }
}
```
-## select 和 poll 比较
+## select 和 poll 比较
-### 1. 功能
+### 1. 功能
它们提供了几乎相同的功能,但是在一些细节上有所不同:
-- select 会修改 fd_set 参数,而 poll 不会;
-- select 默认只能监听 1024 个描述符,如果要监听更多的话,需要修改 FD_SETSIZE 之后重新编译;
-- poll 提供了更多的事件类型。
+- select 会修改 fd_set 参数,而 poll 不会;
+- select 默认只能监听 1024 个描述符,如果要监听更多的话,需要修改 FD_SETSIZE 之后重新编译;
+- poll 提供了更多的事件类型。
-### 2. 速度
+### 2. 速度
-poll 和 select 在速度上都很慢。
+poll 和 select 在速度上都很慢。
-- 它们都采取轮询的方式来找到 I/O 完成的描述符,如果描述符很多,那么速度就会很慢;
-- select 只使用每个描述符的 3 位,而 poll 通常需要使用 64 位,因此 poll 需要复制更多的内核空间。
+- 它们都采取轮询的方式来找到 I/O 完成的描述符,如果描述符很多,那么速度就会很慢;
+- select 只使用每个描述符的 3 位,而 poll 通常需要使用 64 位,因此 poll 需要复制更多的内核空间。
-### 3. 可移植性
+### 3. 可移植性
-几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。
+几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。
-## eopll 工作模式
+## eopll 工作模式
-epoll_event 有两种触发模式:LT(level trigger)和 ET(edge trigger)。
+epoll_event 有两种触发模式:LT(level trigger)和 ET(edge trigger)。
-### 1. LT 模式
+### 1. LT 模式
-当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait() 时,会再次响应应用程序并通知此事件。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
+当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait() 时,会再次响应应用程序并通知此事件。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
-### 2. ET 模式
+### 2. ET 模式
-当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait() 时,不会再次响应应用程序并通知此事件。很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
+当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait() 时,不会再次响应应用程序并通知此事件。很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
-## select poll epoll 应用场景
+## select poll epoll 应用场景
-很容易产生一种错觉认为只要用 epoll 就可以了,select poll 都是历史遗留问题,并没有什么应用场景,其实并不是这样的。
+很容易产生一种错觉认为只要用 epoll 就可以了,select poll 都是历史遗留问题,并没有什么应用场景,其实并不是这样的。
-### 1. select 应用场景
+### 1. select 应用场景
-select() poll() epoll_wait() 都有一个 timeout 参数,在 select() 中 timeout 的精确度为 1ns,而 poll() 和 epoll_wait() 中则为 1ms。所以 select 更加适用于实时要求更高的场景,比如核反应堆的控制。
+select() poll() epoll_wait() 都有一个 timeout 参数,在 select() 中 timeout 的精确度为 1ns,而 poll() 和 epoll_wait() 中则为 1ms。所以 select 更加适用于实时要求更高的场景,比如核反应堆的控制。
-select 历史更加悠久,它的可移植性更好,几乎被所有主流平台所支持。
+select 历史更加悠久,它的可移植性更好,几乎被所有主流平台所支持。
-### 2. poll 应用场景
+### 2. poll 应用场景
-poll 没有最大描述符数量的限制,如果平台支持应该采用 poll 且对实时性要求并不是十分严格,而不是 select。
+poll 没有最大描述符数量的限制,如果平台支持应该采用 poll 且对实时性要求并不是十分严格,而不是 select。
-需要同时监控小于 1000 个描述符。那么也没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。
+需要同时监控小于 1000 个描述符。那么也没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。
-需要监控的描述符状态变化多,而且都是非常短暂的。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。epoll 的描述符存储在内核,不容易调试。
+需要监控的描述符状态变化多,而且都是非常短暂的。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。epoll 的描述符存储在内核,不容易调试。
-### 3. epoll 应用场景
+### 3. epoll 应用场景
-程序只需要运行在 Linux 平台上,有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。
+程序只需要运行在 Linux 平台上,有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。
-### 4. 性能对比
+### 4. 性能对比
-> [epoll Scalability Web Page](http://lse.sourceforge.net/epoll/index.html)
+> [epoll Scalability Web Page](http://lse.sourceforge.net/epoll/index.html)
-# 参考资料
+# 参考资料
-- Stevens W R, Fenner B, Rudoff A M. UNIX network programming[M]. Addison-Wesley Professional, 2004.
-- [Boost application performance using asynchronous I/O](https://www.ibm.com/developerworks/linux/library/l-async/)
-- [Synchronous and Asynchronous I/O](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365683(v=vs.85).aspx)
-- [Linux IO 模式及 select、poll、epoll 详解](https://segmentfault.com/a/1190000003063859)
-- [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html)
+- Stevens W R, Fenner B, Rudoff A M. UNIX network programming[M]. Addison-Wesley Professional, 2004.
+- [Boost application performance using asynchronous I/O](https://www.ibm.com/developerworks/linux/library/l-async/)
+- [Synchronous and Asynchronous I/O](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365683(v=vs.85).aspx)
+- [Linux IO 模式及 select、poll、epoll 详解](https://segmentfault.com/a/1190000003063859)
+- [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html)
\ No newline at end of file
diff --git a/notes/代码可读性.md b/notes/代码可读性.md
index 4ba087cb..dc6e7bab 100644
--- a/notes/代码可读性.md
+++ b/notes/代码可读性.md
@@ -1,22 +1,4 @@
-
-* [一、可读性的重要性](#一可读性的重要性)
-* [二、用名字表达代码含义](#二用名字表达代码含义)
-* [三、名字不能带来歧义](#三名字不能带来歧义)
-* [四、良好的代码风格](#四良好的代码风格)
-* [五、编写注释](#五编写注释)
-* [六、如何编写注释](#六如何编写注释)
-* [七、提高控制流的可读性](#七提高控制流的可读性)
-* [八、拆分长表达式](#八拆分长表达式)
-* [九、变量与可读性](#九变量与可读性)
-* [十、抽取函数](#十抽取函数)
-* [十一、一次只做一件事](#十一一次只做一件事)
-* [十二、用自然语言表述代码](#十二用自然语言表述代码)
-* [十三、减少代码量](#十三减少代码量)
-* [参考资料](#参考资料)
-
-
-
-# 一、可读性的重要性
+# 一、可读性的重要性
编程有很大一部分时间是在阅读代码,不仅要阅读自己的代码,而且要阅读别人的代码。因此,可读性良好的代码能够大大提高编程效率。
@@ -24,50 +6,50 @@
只有在核心领域为了效率才可以放弃可读性,否则可读性是第一位。
-# 二、用名字表达代码含义
+# 二、用名字表达代码含义
一些比较有表达力的单词:
-| 单词 | 可替代单词 |
-| :---: | --- |
-| send | deliver、dispatch、announce、distribute、route |
-| find | search、extract、locate、recover |
-| start| launch、create、begin、open|
-| make | create、set up、build、generate、compose、add、new |
+| 单词 | 可替代单词 |
+| :---: | --- |
+| send | deliver、dispatch、announce、distribute、route |
+| find | search、extract、locate、recover |
+| start| launch、create、begin、open|
+| make | create、set up、build、generate、compose、add、new |
-使用 i、j、k 作为循环迭代器的名字过于简单,user_i、member_i 这种名字会更有表达力。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高。
+使用 i、j、k 作为循环迭代器的名字过于简单,user_i、member_i 这种名字会更有表达力。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高。
为名字添加形容词等信息能让名字更具有表达力,但是名字也会变长。名字长短的准则是:作用域越大,名字越长。因此只有在短作用域才能使用一些简单名字。
-# 三、名字不能带来歧义
+# 三、名字不能带来歧义
起完名字要思考一下别人会对这个名字有何解读,会不会误解了原本想表达的含义。
-用 min、max 表示数量范围;用 first、last 表示访问空间的包含范围,begin、end 表示访问空间的排除范围,即 end 不包含尾部。
+用 min、max 表示数量范围;用 first、last 表示访问空间的包含范围,begin、end 表示访问空间的排除范围,即 end 不包含尾部。
-
+![](index_files/05907ab4-42c5-4b5e-9388-6617f6c97bea.jpg)
-布尔相关的命名加上 is、can、should、has 等前缀。
+布尔相关的命名加上 is、can、should、has 等前缀。
-# 四、良好的代码风格
+# 四、良好的代码风格
适当的空行和缩进。
排列整齐的注释:
```java
-int a = 1; // 注释
-int b = 11; // 注释
-int c = 111; // 注释
+int a = 1; // 注释
+int b = 11; // 注释
+int c = 111; // 注释
```
-语句顺序不能随意,比如与 html 表单相关联的变量的赋值应该和表单在 html 中的顺序一致;
+语句顺序不能随意,比如与 html 表单相关联的变量的赋值应该和表单在 html 中的顺序一致;
把相关的代码按块组织起来放在一起。
-# 五、编写注释
+# 五、编写注释
-阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter 和 setter 方法,为这些方法写注释反而让代码可读性更差。
+阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter 和 setter 方法,为这些方法写注释反而让代码可读性更差。
不能因为有注释就随便起个名字,而是争取起个好名字而不写注释。
@@ -75,211 +57,211 @@ int c = 111; // 注释
注释用来提醒一些特殊情况。
-用 TODO 等做标记:
+用 TODO 等做标记:
-| 标记 | 用法 |
+| 标记 | 用法 |
|---|---|
-|TODO| 待做 |
-|FIXME| 待修复 |
-|HACH| 粗糙的解决方案 |
-|XXX| 危险!这里有重要的问题 |
+|TODO| 待做 |
+|FIXME| 待修复 |
+|HACH| 粗糙的解决方案 |
+|XXX| 危险!这里有重要的问题 |
-# 六、如何编写注释
+# 六、如何编写注释
尽量简洁明了:
```java
-// The first String is student's name
-// The Second Integer is student's score
-Map scoreMap = new HashMap<>();
+// The first String is student's name
+// The Second Integer is student's score
+Map scoreMap = new HashMap<>();
```
```java
-// Student's name -> Student's score
-Map scoreMap = new HashMap<>();
+// Student's name -> Student's score
+Map scoreMap = new HashMap<>();
```
添加测试用例来说明:
```java
-// ...
-// Example: add(1, 2), return 3
-int add(int x, int y) {
- return x + y;
+// ...
+// Example: add(1, 2), return 3
+int add(int x, int y) {
+ return x + y;
}
```
在很复杂的函数调用中对每个参数标上名字:
```java
-int a = 1;
-int b = 2;
-int num = add(\* x = *\ a, \* y = *\ b);
+int a = 1;
+int b = 2;
+int num = add(\* x = *\ a, \* y = *\ b);
```
使用专业名词来缩短概念上的解释,比如用设计模式名来说明代码。
-# 七、提高控制流的可读性
+# 七、提高控制流的可读性
条件表达式中,左侧是变量,右侧是常数。比如下面第一个语句正确:
```java
-if (len < 10)
-if (10 > len)
+if (len < 10)
+if (10 > len)
```
-if / else 条件语句,逻辑的处理顺序为:① 正逻辑;② 关键逻辑;③ 简单逻辑。
+if / else 条件语句,逻辑的处理顺序为:① 正逻辑;② 关键逻辑;③ 简单逻辑。
```java
-if (a == b) {
- // 正逻辑
-} else{
- // 反逻辑
+if (a == b) {
+ // 正逻辑
+} else{
+ // 反逻辑
}
```
-只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑,否则应该拆分成 if / else;
+只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑,否则应该拆分成 if / else;
-do / while 的条件放在后面,不够简单明了,并且会有一些迷惑的地方,最好使用 while 来代替。
+do / while 的条件放在后面,不够简单明了,并且会有一些迷惑的地方,最好使用 while 来代替。
-如果只有一个 goto 目标,那么 goto 尚且还能接受,但是过于复杂的 goto 会让代码可读性特别差,应该避免使用 goto。
+如果只有一个 goto 目标,那么 goto 尚且还能接受,但是过于复杂的 goto 会让代码可读性特别差,应该避免使用 goto。
-在嵌套的循环中,用一些 return 语句往往能减少嵌套的层数。
+在嵌套的循环中,用一些 return 语句往往能减少嵌套的层数。
-# 八、拆分长表达式
+# 八、拆分长表达式
长表达式的可读性很差,可以引入一些解释变量从而拆分表达式:
```python
-if line.split(':')[0].strip() == "root":
- ...
+if line.split(':')[0].strip() == "root":
+ ...
```
```python
-username = line.split(':')[0].strip()
-if username == "root":
- ...
+username = line.split(':')[0].strip()
+if username == "root":
+ ...
```
使用摩根定理简化一些逻辑表达式:
```java
-if (!a && !b) {
- ...
+if (!a && !b) {
+ ...
}
```
```java
-if (!(a || b)) {
- ...
+if (!(a || b)) {
+ ...
}
```
-# 九、变量与可读性
+# 九、变量与可读性
-**去除控制流变量** 。在循环中通过使用 break 或者 return 可以减少控制流变量的使用。
+**去除控制流变量**。在循环中通过使用 break 或者 return 可以减少控制流变量的使用。
```java
-boolean done = false;
-while (/* condition */ && !done) {
- ...
- if ( ... ) {
- done = true;
- continue;
- }
+boolean done = false;
+while (/* condition */ && !done) {
+ ...
+ if ( ... ) {
+ done = true;
+ continue;
+ }
}
```
```java
-while(/* condition */) {
- ...
- if ( ... ) {
- break;
- }
+while(/* condition */) {
+ ...
+ if ( ... ) {
+ break;
+ }
}
```
-**减小变量作用域** 。作用域越小,越容易定位到变量所有使用的地方。
+**减小变量作用域**。作用域越小,越容易定位到变量所有使用的地方。
-JavaScript 可以用闭包减小作用域。以下代码中 submit_form 是函数变量,submitted 变量控制函数不会被提交两次。第一个实现中 submitted 是全局变量,第二个实现把 submitted 放到匿名函数中,从而限制了起作用域范围。
+JavaScript 可以用闭包减小作用域。以下代码中 submit_form 是函数变量,submitted 变量控制函数不会被提交两次。第一个实现中 submitted 是全局变量,第二个实现把 submitted 放到匿名函数中,从而限制了起作用域范围。
```js
-submitted = false;
-var submit_form = function(form_name) {
- if (submitted) {
- return;
- }
- submitted = true;
+submitted = false;
+var submit_form = function(form_name) {
+ if (submitted) {
+ return;
+ }
+ submitted = true;
};
```
```js
-var submit_form = (function() {
- var submitted = false;
- return function(form_name) {
- if(submitted) {
- return;
- }
- submitted = true;
- }
-}()); // () 使得外层匿名函数立即执行
+var submit_form = (function() {
+ var submitted = false;
+ return function(form_name) {
+ if(submitted) {
+ return;
+ }
+ submitted = true;
+ }
+}()); // () 使得外层匿名函数立即执行
```
-JavaScript 中没有用 var 声明的变量都是全局变量,而全局变量很容易造成迷惑,因此应当总是用 var 来声明变量。
+JavaScript 中没有用 var 声明的变量都是全局变量,而全局变量很容易造成迷惑,因此应当总是用 var 来声明变量。
变量定义的位置应当离它使用的位置最近。
-**实例解析**
+**实例解析**
在一个网页中有以下文本输入字段:
```html
-
-
-
-
+
+
+
+
```
-现在要接受一个字符串并把它放到第一个空的 input 字段中,初始实现如下:
+现在要接受一个字符串并把它放到第一个空的 input 字段中,初始实现如下:
```js
-var setFirstEmptyInput = function(new_alue) {
- var found = false;
- var i = 1;
- var elem = document.getElementById('input' + i);
- while (elem != null) {
- if (elem.value === '') {
- found = true;
- break;
- }
- i++;
- elem = document.getElementById('input' + i);
- }
- if (found) elem.value = new_value;
- return elem;
+var setFirstEmptyInput = function(new_alue) {
+ var found = false;
+ var i = 1;
+ var elem = document.getElementById('input' + i);
+ while (elem != null) {
+ if (elem.value === '') {
+ found = true;
+ break;
+ }
+ i++;
+ elem = document.getElementById('input' + i);
+ }
+ if (found) elem.value = new_value;
+ return elem;
}
```
以上实现有以下问题:
-- found 可以去除;
-- elem 作用域过大;
-- 可以用 for 循环代替 while 循环;
+- found 可以去除;
+- elem 作用域过大;
+- 可以用 for 循环代替 while 循环;
```js
-var setFirstEmptyInput = function(new_value) {
- for (var i = 1; true; i++) {
- var elem = document.getElementById('input' + i);
- if (elem === null) {
- return null;
- }
- if (elem.value === '') {
- elem.value = new_value;
- return elem;
- }
- }
+var setFirstEmptyInput = function(new_value) {
+ for (var i = 1; true; i++) {
+ var elem = document.getElementById('input' + i);
+ if (elem === null) {
+ return null;
+ }
+ if (elem.value === '') {
+ elem.value = new_value;
+ return elem;
+ }
+ }
};
```
-# 十、抽取函数
+# 十、抽取函数
工程学就是把大问题拆分成小问题再把这些问题的解决方案放回一起。
@@ -288,38 +270,38 @@ var setFirstEmptyInput = function(new_value) {
介绍性的代码:
```java
-int findClostElement(int[] arr) {
- int clostIdx;
- int clostDist = Interger.MAX_VALUE;
- for (int i = 0; i < arr.length; i++) {
- int x = ...;
- int y = ...;
- int z = ...;
- int value = x * y * z;
- int dist = Math.sqrt(Math.pow(value, 2), Math.pow(arr[i], 2));
- if (dist < clostDist) {
- clostIdx = i;
- clostDist = value;
- }
- }
- return clostIdx;
+int findClostElement(int[] arr) {
+ int clostIdx;
+ int clostDist = Interger.MAX_VALUE;
+ for (int i = 0; i < arr.length; i++) {
+ int x = ...;
+ int y = ...;
+ int z = ...;
+ int value = x * y * z;
+ int dist = Math.sqrt(Math.pow(value, 2), Math.pow(arr[i], 2));
+ if (dist < clostDist) {
+ clostIdx = i;
+ clostDist = value;
+ }
+ }
+ return clostIdx;
}
```
以上代码中循环部分主要计算距离,这部分不属于代码高层次目标,高层次目标是寻找最小距离的值,因此可以把这部分代替提取到独立的函数中。这样做也带来一个额外的好处有:可以单独进行测试、可以快速找到程序错误并修改。
```java
-public int findClostElement(int[] arr) {
- int clostIdx;
- int clostDist = Interger.MAX_VALUE;
- for (int i = 0; i < arr.length; i++) {
- int dist = computDist(arr, i);
- if (dist < clostDist) {
- clostIdx = i;
- clostDist = value;
- }
- }
- return clostIdx;
+public int findClostElement(int[] arr) {
+ int clostIdx;
+ int clostDist = Interger.MAX_VALUE;
+ for (int i = 0; i < arr.length; i++) {
+ int dist = computDist(arr, i);
+ if (dist < clostDist) {
+ clostIdx = i;
+ clostDist = value;
+ }
+ }
+ return clostIdx;
}
```
@@ -327,22 +309,22 @@ public int findClostElement(int[] arr) {
函数抽取也用于减小代码的冗余。
-# 十一、一次只做一件事
+# 十一、一次只做一件事
只做一件事的代码很容易让人知道其要做的事;
基本流程:列出代码所做的所有任务;把每个任务拆分到不同的函数,或者不同的段落。
-# 十二、用自然语言表述代码
+# 十二、用自然语言表述代码
先用自然语言书写代码逻辑,也就是伪代码,然后再写代码,这样代码逻辑会更清晰。
-# 十三、减少代码量
+# 十三、减少代码量
不要过度设计,编码过程会有很多变化,过度设计的内容到最后往往是无用的。
多用标准库实现。
-# 参考资料
+# 参考资料
-- Dustin, Boswell, Trevor, 等. 编写可读代码的艺术 [M]. 机械工业出版社, 2012.
+- Dustin, Boswell, Trevor, 等. 编写可读代码的艺术 [M]. 机械工业出版社, 2012.
\ No newline at end of file
diff --git a/notes/代码风格规范.md b/notes/代码风格规范.md
index 62049b68..a165d3fa 100644
--- a/notes/代码风格规范.md
+++ b/notes/代码风格规范.md
@@ -1,21 +1,14 @@
-
-* [Google Java Style Guide](#google-java-style-guide)
-* [Google C++ Style Guide](#google-c-style-guide)
-* [Google Python Style Guide](#google-python-style-guide)
-
+# Google Java Style Guide
+- http://www.hawstein.com/posts/google-java-style.html
+- http://google.github.io/styleguide/javaguide.html
-# Google Java Style Guide
+# Google C++ Style Guide
-- http://www.hawstein.com/posts/google-java-style.html
-- http://google.github.io/styleguide/javaguide.html
+- http://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/contents/
+- http://google.github.io/styleguide/cppguide.html
-# Google C++ Style Guide
+# Google Python Style Guide
-- http://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/contents/
-- http://google.github.io/styleguide/cppguide.html
-
-# Google Python Style Guide
-
-- http://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/contents/
-- http://google.github.io/styleguide/pyguide.html
+- http://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/contents/
+- http://google.github.io/styleguide/pyguide.html
diff --git a/notes/分布式基础.md b/notes/分布式基础.md
index ba6ecf75..e8110c87 100644
--- a/notes/分布式基础.md
+++ b/notes/分布式基础.md
@@ -1,60 +1,34 @@
-
-* [一、基本概念](#一基本概念)
- * [异常](#异常)
- * [超时](#超时)
- * [衡量指标](#衡量指标)
-* [二、数据分布](#二数据分布)
- * [哈希分布](#哈希分布)
- * [顺序分布](#顺序分布)
- * [负载均衡](#负载均衡)
-* [三、复制](#三复制)
- * [复制原理](#复制原理)
- * [复制协议](#复制协议)
- * [CAP](#cap)
- * [BASE](#base)
-* [四、容错](#四容错)
- * [故障检测](#故障检测)
- * [故障恢复](#故障恢复)
-* [五、一致性协议](#五一致性协议)
- * [Paxos 协议](#paxos-协议)
- * [Raft 协议](#raft-协议)
- * [拜占庭将军问题](#拜占庭将军问题)
-* [六、CDN 架构](#六cdn-架构)
-* [参考资料](#参考资料)
-
+# 一、基本概念
+## 异常
-# 一、基本概念
-
-## 异常
-
-### 1. 服务器宕机
+### 1. 服务器宕机
内存错误、服务器停电等都会导致服务器宕机,此时节点无法正常工作,称为不可用。
服务器宕机会导致节点失去所有内存信息,因此需要将内存信息保存到持久化介质上。
-### 2. 网络异常
+### 2. 网络异常
-有一种特殊的网络异常称为 **网络分区** ,即集群的所有节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。
+有一种特殊的网络异常称为 **网络分区**,即集群的所有节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。
-### 3. 磁盘故障
+### 3. 磁盘故障
磁盘故障是一种发生概率很高的异常。
使用冗余机制,将数据存储到多台服务器。
-## 超时
+## 超时
在分布式系统中,一个请求除了成功和失败两种状态,还存在着超时状态。
-
+![](index_files/b0e8ef47-2f23-4379-8c64-10d5cb44d438.jpg)
-可以将服务器的操作设计为具有 **幂等性** ,即执行多次的结果与执行一次的结果相同。如果使用这种方式,当出现超时的时候,可以不断地重新请求直到成功。
+可以将服务器的操作设计为具有 **幂等性**,即执行多次的结果与执行一次的结果相同。如果使用这种方式,当出现超时的时候,可以不断地重新请求直到成功。
-## 衡量指标
+## 衡量指标
-### 1. 性能
+### 1. 性能
常见的性能指标有:吞吐量、响应时间。
@@ -62,59 +36,59 @@
这两个指标往往是矛盾的,追求高吞吐的系统,往往很难做到低响应时间,解释如下:
-- 在无并发的系统中,吞吐量为响应时间的倒数,例如响应时间为 10 ms,那么吞吐量为 100 req/s,因此高吞吐也就意味着低响应时间。
+- 在无并发的系统中,吞吐量为响应时间的倒数,例如响应时间为 10 ms,那么吞吐量为 100 req/s,因此高吞吐也就意味着低响应时间。
-- 但是在并发的系统中,由于一个请求在调用 I/O 资源的时候,需要进行等待。服务器端一般使用的是异步等待方式,即等待的请求被阻塞之后不需要一直占用 CPU 资源。这种方式能大大提高 CPU 资源的利用率,例如上面的例子中,单个请求在无并发的系统中响应时间为 10 ms,如果在并发的系统中,那么吞吐量将大于 100 req/s。因此为了追求高吞吐量,通常会提高并发程度。但是并发程度的增加,会导致请求的平均响应时间也增加,因为请求不能马上被处理,需要和其它请求一起进行并发处理,响应时间自然就会增高。
+- 但是在并发的系统中,由于一个请求在调用 I/O 资源的时候,需要进行等待。服务器端一般使用的是异步等待方式,即等待的请求被阻塞之后不需要一直占用 CPU 资源。这种方式能大大提高 CPU 资源的利用率,例如上面的例子中,单个请求在无并发的系统中响应时间为 10 ms,如果在并发的系统中,那么吞吐量将大于 100 req/s。因此为了追求高吞吐量,通常会提高并发程度。但是并发程度的增加,会导致请求的平均响应时间也增加,因为请求不能马上被处理,需要和其它请求一起进行并发处理,响应时间自然就会增高。
-### 2. 可用性
+### 2. 可用性
-可用性指系统在面对各种异常时可以提供正常服务的能力。可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。
+可用性指系统在面对各种异常时可以提供正常服务的能力。可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。
-### 3. 一致性
+### 3. 一致性
可以从两个角度理解一致性:从客户端的角度,读写操作是否满足某种特性;从服务器的角度,多个数据副本之间是否一致。
-### 4. 可扩展性
+### 4. 可扩展性
指系统通过扩展集群服务器规模来提高性能的能力。理想的分布式系统需要实现“线性可扩展”,即随着集群规模的增加,系统的整体性能也会线性增加。
-# 二、数据分布
+# 二、数据分布
分布式存储系统的数据分布在多个节点中,常用的数据分布方式有哈希分布和顺序分布。
-数据库的水平切分(Sharding)也是一种分布式存储方法,下面的数据分布方法同样适用于 Sharding。
+数据库的水平切分(Sharding)也是一种分布式存储方法,下面的数据分布方法同样适用于 Sharding。
-## 哈希分布
+## 哈希分布
-哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key,则将该数据分配的节点序号为:hash(key)%N。
+哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key,则将该数据分配的节点序号为:hash(key)%N。
-传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。
+传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。
-**一致性哈希**
+**一致性哈希**
-Distributed Hash Table(DHT):对于哈希空间 [0, 2n -1],将该哈希空间看成一个哈希环,将每个节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。
+Distributed Hash Table(DHT):对于哈希空间 [0, 2n -1],将该哈希空间看成一个哈希环,将每个节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。
-
+![](index_files/d2d34239-e7c1-482b-b33e-3170c5943556.jpg)
-一致性哈希的优点是在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将数据对象 C 重新存放到节点 X 上即可,对于节点 A、B、D 都没有影响。
+一致性哈希的优点是在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将数据对象 C 重新存放到节点 X 上即可,对于节点 A、B、D 都没有影响。
-
+![](index_files/91ef04e4-923a-4277-99c0-6be4ce81e5ac.jpg)
-## 顺序分布
+## 顺序分布
哈希分布式破坏了数据的有序性,顺序分布则不会。
-顺序分布的数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如下图中,User 表的 ID 范围为 1 \~ 7000,使用顺序分布可以将其划分成多个子表,对应的主键范围为 1 \~ 1000,1001 \~ 2000,...,6001 \~ 7000。
+顺序分布的数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如下图中,User 表的 ID 范围为 1 ~ 7000,使用顺序分布可以将其划分成多个子表,对应的主键范围为 1 ~ 1000,1001 ~ 2000,...,6001 ~ 7000。
顺序分布的优点是可以充分利用每个节点的空间,而哈希分布很难控制一个节点存储多少数据。
-但是顺序分布需要使用一个映射表来存储数据到节点的映射,这个映射表通常使用单独的节点来存储。当数据量非常大时,映射表也随着变大,那么一个节点就可能无法存放下整个映射表。并且单个节点维护着整个映射表的开销很大,查找速度也会变慢。为了解决以上问题,引入了一个中间层,也就是 Meta 表,从而分担映射表的维护工作。
+但是顺序分布需要使用一个映射表来存储数据到节点的映射,这个映射表通常使用单独的节点来存储。当数据量非常大时,映射表也随着变大,那么一个节点就可能无法存放下整个映射表。并且单个节点维护着整个映射表的开销很大,查找速度也会变慢。为了解决以上问题,引入了一个中间层,也就是 Meta 表,从而分担映射表的维护工作。
-
+![](index_files/8f64e9c5-7682-4feb-9312-dea09514e160.jpg)
-## 负载均衡
+## 负载均衡
-衡量负载的因素很多,如 CPU、内存、磁盘等资源使用情况、读写请求数等。
+衡量负载的因素很多,如 CPU、内存、磁盘等资源使用情况、读写请求数等。
分布式系统存储应当能够自动负载均衡,当某个节点的负载较高,将它的部分数据迁移到其它节点。
@@ -122,9 +96,9 @@ Distributed Hash Table(DHT):对于哈希空间 [0, 2n -1],将
一个新上线的工作节点,由于其负载较低,如果不加控制,总控节点会将大量数据同时迁移到该节点上,造成该节点一段时间内无法工作。因此负载均衡操作需要平滑进行,新加入的节点需要较长的一段时间来达到比较均衡的状态。
-# 三、复制
+# 三、复制
-## 复制原理
+## 复制原理
复制是保证分布式系统高可用的基础,让一个数据存储多个副本,当某个副本所在的节点出现故障时,能够自动切换到其它副本上,从而实现故障恢复。
@@ -132,13 +106,13 @@ Distributed Hash Table(DHT):对于哈希空间 [0, 2n -1],将
主副本将同步操作日志发送给备副本,备副本通过回放操作日志获取最新修改。
-
+![](index_files/44e4a7ab-215c-41a1-8e34-f55f6c09e517.jpg)
-## 复制协议
+## 复制协议
主备副本之间有两种复制协议,一种是强同步复制协议,一种是异步复制协议。
-### 1. 强同步复制协议
+### 1. 强同步复制协议
要求主副本将同步操作日志发给备副本之后进行等待,要求至少一个备副本返回成功后,才开始修改主副本,修改完成之后通知客户端操作成功。
@@ -146,7 +120,7 @@ Distributed Hash Table(DHT):对于哈希空间 [0, 2n -1],将
缺点:可用性差,因为主副本需要等待,那么整个分布式系统的可用时间就会降低。
-### 2. 异步复制协议
+### 2. 异步复制协议
主副本将同步操作日志发给备副本之后不需要进行等待,直接修改主副本并通知客户端操作成功。
@@ -154,189 +128,215 @@ Distributed Hash Table(DHT):对于哈希空间 [0, 2n -1],将
缺点:一致性差。
-## CAP
+## CAP
-分布式存储系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition tolerance),最多只能同时满足其中两项。
+分布式存储系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition tolerance),最多只能同时满足其中两项。
-在设计分布式系统时,需要根据实际需求弱化某一要求。因此就有了下图中的三种设计:CA、CP 和 AP。
+在设计分布式系统时,需要根据实际需求弱化某一要求。因此就有了下图中的三种设计:CA、CP 和 AP。
需要注意的是,分区容忍性必不可少,因为需要总是假设网络是不可靠的,并且系统需要能够自动容错,因此实际上设计分布式存储系统需要在一致性和可用性之间做权衡。上一节介绍的强同步协议和异步复制协议就是在一致性和可用性做权衡得到的结果。
-
+
-## BASE
+## BASE
-BASE 是 Basically Available(基本可用)、Soft State(软状态)和 Eventually Consistent(最终一致性)三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,是基于 CAP 定理逐步演化而来的。BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
+BASE 是 Basically Available(基本可用)、Soft State(软状态)和 Eventually Consistent(最终一致性)三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,是基于 CAP 定理逐步演化而来的。BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
-
+
-### 1. 基本可用
+### 1. 基本可用
指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。
例如,电商在做促销时,服务层可能只提供降级服务,部分用户可能会被引导到降级页面上。
-### 2. 软状态
+### 2. 软状态
指允许系统存在中间状态,而该中间状态不会影响系统整体可用性,即不同节点的数据副本之间进行同步的过程允许存在延时。
-### 3. 最终一致性
+### 3. 最终一致性
一致性模型包含以下三种:
-- 强一致性:新数据写入之后,在任何数据副本上都能读取到最新值;
-- 弱一致性:新数据写入之后,不能保证在数据副本上能读取到最新值;
-- 最终一致性:新数据写入之后,只能保证过了一个时间窗口后才能在数据副本上读取到最新值;
+- 强一致性:新数据写入之后,在任何数据副本上都能读取到最新值;
+- 弱一致性:新数据写入之后,不能保证在数据副本上能读取到最新值;
+- 最终一致性:新数据写入之后,只能保证过了一个时间窗口后才能在数据副本上读取到最新值;
-强一致性通常运用在需要满足 ACID 的传统数据库系统上,而最终一致性通常运用在大型分布式系统中。应该注意的是,上面介绍的强同步复制协议和异步复制协议都不能保证强一致性,因为它们是分布式系统的复制协议。这两种复制协议如果要满足最终一致性,还需要多加一些控制。
+强一致性通常运用在需要满足 ACID 的传统数据库系统上,而最终一致性通常运用在大型分布式系统中。应该注意的是,上面介绍的强同步复制协议和异步复制协议都不能保证强一致性,因为它们是分布式系统的复制协议。这两种复制协议如果要满足最终一致性,还需要多加一些控制。
-在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。
+在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。
-# 四、容错
+# 四、容错
分布式系统故障发生的概率很大,为了实现高可用以及减少人工运维成本,需要实现自动化容错。
-## 故障检测
+## 故障检测
-通过 **租约机制** 来对故障进行检测。假设节点 A 为主控节点,节点 A 向节点 B 发送租约,节点 B 在租约规定的期限内才能提供服务。期限快到达时,节点 B 需要向 A 重新申请租约。
+通过 **租约机制** 来对故障进行检测。假设节点 A 为主控节点,节点 A 向节点 B 发送租约,节点 B 在租约规定的期限内才能提供服务。期限快到达时,节点 B 需要向 A 重新申请租约。
-如果过期,那么 B 不再提供服务,并且 A 也能知道 B 此时可能发生故障并已经停止服务。可以看到,通过这种机制,A 和 B 都能对 B 发生故障这一事实达成一致。
+如果过期,那么 B 不再提供服务,并且 A 也能知道 B 此时可能发生故障并已经停止服务。可以看到,通过这种机制,A 和 B 都能对 B 发生故障这一事实达成一致。
-## 故障恢复
+## 故障恢复
当某个节点故障时,就将它上面的服务迁移到其它节点。
-# 五、一致性协议
+# 五、一致性协议
-## Paxos 协议
+## Paxos 协议
用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。
主要有三类节点:
-- 提议者(Proposer):提议一个值;
-- 接受者(Acceptor):对每个提议进行投票;
-- 告知者(Learner):被告知投票的结果,不参与投票过程。
+- 提议者(Proposer):提议一个值;
+- 接受者(Acceptor):对每个提议进行投票;
+- 告知者(Learner):被告知投票的结果,不参与投票过程。
-
+![](index_files/0aaf4630-d2a2-4783-b3f7-a2b6a7dfc01b.jpg)
-### 1. 执行过程
+### 1. 执行过程
-规定一个提议包含两个字段:[n, v],其中 n 为序号(具有唯一性),v 为提议值。
+规定一个提议包含两个字段:[n, v],其中 n 为序号(具有唯一性),v 为提议值。
-下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向所有 Acceptor 发送提议请求。
+下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向所有 Acceptor 发送提议请求。
-
+![](index_files/2bf2fd8f-5ade-48ba-a2b3-74195ac77c4b.png)
-当 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 类似。
-
+![](index_files/3f5bba4b-7813-4aea-b578-970c7e3f6bf3.jpg)
-如果 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 类似。
-
+![](index_files/9b829410-86c4-40aa-ba8d-9e8e26c0eeb8.jpg)
-当一个 Proposer 接收到超过一半 Acceptor 的提议响应时,就可以发送接受请求。
+当一个 Proposer 接收到超过一半 Acceptor 的提议响应时,就可以发送接受请求。
-Proposer A 接收到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。
+Proposer A 接收到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。
-Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。
+Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。
-
+![](index_files/2c4556e4-0751-4377-ab08-e7b89d697ca7.png)
-Acceptor 接收到接受请求时,如果序号大于等于该 Acceptor 承诺的最小序号,那么就发送通知给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。
+Acceptor 接收到接受请求时,如果序号大于等于该 Acceptor 承诺的最小序号,那么就发送通知给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。
-
+![](index_files/8adb2591-d3f1-4632-84cb-823fb9c5eb09.jpg)
-### 2. 约束条件
+### 2. 约束条件
-**(一)正确性**
+**(一)正确性**
指只有一个提议值会生效。
-因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收,并且 Acceptor 不会接受两个不同的提议,因此可以保证正确性。
+因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收,并且 Acceptor 不会接受两个不同的提议,因此可以保证正确性。
-**(二)可终止性**
+**(二)可终止性**
指最后总会有一个提议生效。
-Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。
+Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。
-## Raft 协议
+## Raft 协议
-Raft 和 Paxos 类似,但是更容易理解,也更容易实现。
+Raft 和 Paxos 类似,但是更容易理解,也更容易实现。
-Raft 主要是用来竞选主节点。
+Raft 主要是用来竞选主节点。
-### 1. 单个 Candidate 的竞选
+### 1. 单个 Candidate 的竞选
-有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。
+有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。
-- 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
+- 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
-
+![](index_files/111521118015898.gif)
-- 此时 A 发送投票请求给其它所有节点。
+- 此时 A 发送投票请求给其它所有节点。
-
+![](index_files/111521118445538.gif)
-- 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。
+- 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。
-
+![](index_files/111521118483039.gif)
-- 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。
+- 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。
-
+![](index_files/111521118640738.gif)
-### 2. 多个 Candidate 竞选
+### 2. 多个 Candidate 竞选
-* 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。
+* 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。
-
+![](index_files/111521119203347.gif)
-* 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。
+* 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。
-
+![](index_files/111521119368714.gif)
-### 3. 日志复制
+### 3. 日志复制
-- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。
+- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。
-
+![](index_files/7.gif)
-- Leader 会把修改复制到所有 Follower。
+- Leader 会把修改复制到所有 Follower。
-
+![](index_files/9.gif)
-- Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。
+- Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。
-
+![](index_files/10.gif)
-- 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
+- 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
-
+![](index_files/11.gif)
-## 拜占庭将军问题
+## 拜占庭将军问题
-> [拜占庭将军问题深入探讨](http://www.8btc.com/baizhantingjiangjun)
+> [拜占庭将军问题深入探讨](http://www.8btc.com/baizhantingjiangjun)
-# 六、CDN 架构
+# 六、CDN 架构
通过将内容发布到靠近用户的边缘节点,使不同地域的用户在访问相同网页时可以就近获取。不仅可以减轻服务器的负担,也可以提高用户的访问速度。
-从下图可以看出,DNS 在对域名解析时不再向用户返回源服务器的 IP 地址,而是返回边缘节点的 IP 地址,所以用户最终访问的是边缘节点。边缘节点会先从源服务器中获取用户所需的数据,如果请求成功,边缘节点会将页面缓存下来,下次用户访问时可以直接读取。
+从下图可以看出,DNS 在对域名解析时不再向用户返回源服务器的 IP 地址,而是返回边缘节点的 IP 地址,所以用户最终访问的是边缘节点。边缘节点会先从源服务器中获取用户所需的数据,如果请求成功,边缘节点会将页面缓存下来,下次用户访问时可以直接读取。
-
+![](index_files/dbd60b1f-b700-4da6-a993-62578e892333.jpg)
-# 参考资料
+# 参考资料
-- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013.
-- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013.
-- [区块链技术指南](https://www.gitbook.com/book/yeasy/blockchain_guide/details)
-- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/)
-- [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft)
-- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/)
+- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013.
+- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013.
+- [区块链技术指南](https://www.gitbook.com/book/yeasy/blockchain_guide/details)
+- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/)
+- [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft)
+- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/)
+---bottom---CyC---
+![](index_files/b0e8ef47-2f23-4379-8c64-10d5cb44d438.jpg)
+![](index_files/d2d34239-e7c1-482b-b33e-3170c5943556.jpg)
+![](index_files/91ef04e4-923a-4277-99c0-6be4ce81e5ac.jpg)
+![](index_files/8f64e9c5-7682-4feb-9312-dea09514e160.jpg)
+![](index_files/44e4a7ab-215c-41a1-8e34-f55f6c09e517.jpg)
+![](index_files/992faced-afcf-414d-b801-9c16d6570fec.jpg)
+![](index_files/5930aeb8-847d-4e9f-a168-9334d7dec744.png)
+![](index_files/07717718-1230-4347-aa18-2041c315e670.jpg)
+![](index_files/0aaf4630-d2a2-4783-b3f7-a2b6a7dfc01b.jpg)
+![](index_files/2bf2fd8f-5ade-48ba-a2b3-74195ac77c4b.png)
+![](index_files/3f5bba4b-7813-4aea-b578-970c7e3f6bf3.jpg)
+![](index_files/9b829410-86c4-40aa-ba8d-9e8e26c0eeb8.jpg)
+![](index_files/2c4556e4-0751-4377-ab08-e7b89d697ca7.png)
+![](index_files/8adb2591-d3f1-4632-84cb-823fb9c5eb09.jpg)
+![](index_files/111521118015898.gif)
+![](index_files/111521118445538.gif)
+![](index_files/111521118483039.gif)
+![](index_files/111521118640738.gif)
+![](index_files/111521119203347.gif)
+![](index_files/111521119368714.gif)
+![](index_files/7.gif)
+![](index_files/9.gif)
+![](index_files/10.gif)
+![](index_files/11.gif)
+![](index_files/dbd60b1f-b700-4da6-a993-62578e892333.jpg)
diff --git a/notes/分布式问题分析.md b/notes/分布式问题分析.md
index 69a30a4d..7508f7fc 100644
--- a/notes/分布式问题分析.md
+++ b/notes/分布式问题分析.md
@@ -1,285 +1,292 @@
-
-* [一、分布式事务](#一分布式事务)
- * [两阶段提交协议](#两阶段提交协议)
- * [本地消息](#本地消息)
-* [二、分布式锁](#二分布式锁)
- * [原理](#原理)
- * [实现](#实现)
-* [三、分布式 Session](#三分布式-session)
-* [四、负载均衡](#四负载均衡)
- * [算法](#算法)
- * [实现](#实现)
-* [参考资料](#参考资料)
-
+# 一、分布式事务
+指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。例如在下单场景下,库存和订单如果不在同一个节点上,就需要涉及分布式事务。
-# 一、分布式事务
+## 两阶段提交协议
-指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。例如在下单场景下,库存和订单如果不在同一个节点上,就需要涉及分布式事务。
-
-## 两阶段提交协议
-
-Two-phase Commit(2PC)。
+Two-phase Commit(2PC)。
两类节点:协调者(Coordinator)和参与者(Participants),协调者只有一个,参与者可以有多个。
-### 1. 运行过程
+### 1. 运行过程
-① 准备阶段:协调者询问参与者事务是否执行成功;
+① 准备阶段:协调者询问参与者事务是否执行成功;
-
+![](index_files/c8dbff58-d981-48be-8c1c-caa6c2738791.jpg)
-② 提交阶段:如果事务在每个参与者上都执行成功,协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
+② 提交阶段:如果事务在每个参与者上都执行成功,协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
-
+![](index_files/aa844ff0-cd16-4478-b415-da071b615a17.jpg)
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
-### 2. 分析
+### 2. 分析
-2PC 可以保证强一致性,但是因为在准备阶段协调者需要等待所有参与者的结果才能进入提交阶段,因此可用性差。
+2PC 可以保证强一致性,但是因为在准备阶段协调者需要等待所有参与者的结果才能进入提交阶段,因此可用性差。
-### 3. 存在的问题
+### 3. 存在的问题
-- 参与者发生故障。解决方案:可以给事务设置一个超时时间,如果某个参与者一直不响应,那么认为事务执行失败。
-- 协调者发生故障。解决方案:将操作日志同步到备用协调者,让备用协调者接替后续工作。
+- 参与者发生故障。解决方案:可以给事务设置一个超时时间,如果某个参与者一直不响应,那么认为事务执行失败。
+- 协调者发生故障。解决方案:将操作日志同步到备用协调者,让备用协调者接替后续工作。
-### 4. XA 协议
+### 4. XA 协议
-XA 协议是多数数据库的 2PC 协议的实现,包含了事务管理器和本地资源管理器。
+XA 协议是多数数据库的 2PC 协议的实现,包含了事务管理器和本地资源管理器。
-## 本地消息
+## 本地消息
-### 1. 原理
+### 1. 原理
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性。
-1. 在分布式事务操作的一方,它完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
-2. 之后将本地消息表中的消息转发到 Kafka 等消息队列(MQ)中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
-3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
+1. 在分布式事务操作的一方,它完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
+2. 之后将本地消息表中的消息转发到 Kafka 等消息队列(MQ)中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
+3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
-
+![](index_files/e3bf5de4-ab1e-4a9b-896d-4b0ad7e9220a.jpg)
-### 2. 分析
+### 2. 分析
本地消息表利用了本地事务来实现分布式事务,并且使用了消息队列来保证最终一致性。
-# 二、分布式锁
+# 二、分布式锁
-可以使用 Java 提供的内置锁来实现进程同步:由 JVM 实现的 synchronized 和 JDK 提供的 Lock。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁来同步。
+可以使用 Java 提供的内置锁来实现进程同步:由 JVM 实现的 synchronized 和 JDK 提供的 Lock。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁来同步。
-## 原理
+## 原理
-锁可以有阻塞锁和乐观锁两种实现方式,这里主要探讨阻塞锁实现。阻塞锁通常使用互斥量来实现,互斥量为 1 表示有其它进程在使用锁,此时处于锁定状态,互斥量为 0 表示未锁定状态。1 和 0 可以用一个整型值来存储,也可以用某个数据存在或者不存在来存储,某个数据存在表示互斥量为 1,也就是锁定状态。
+锁可以有阻塞锁和乐观锁两种实现方式,这里主要探讨阻塞锁实现。阻塞锁通常使用互斥量来实现,互斥量为 1 表示有其它进程在使用锁,此时处于锁定状态,互斥量为 0 表示未锁定状态。1 和 0 可以用一个整型值来存储,也可以用某个数据存在或者不存在来存储,某个数据存在表示互斥量为 1,也就是锁定状态。
-## 实现
+## 实现
-### 1. 数据库的唯一索引
+### 1. 数据库的唯一索引
当想要获得锁时,就向表中插入一条记录,释放锁时就删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。
这种方式存在以下几个问题:
-- 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。
-- 只能是非阻塞锁,插入失败直接就报错了,无法重试。
-- 不可重入,同一线程在没有释放锁之前无法再获得锁。
+- 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。
+- 只能是非阻塞锁,插入失败直接就报错了,无法重试。
+- 不可重入,同一线程在没有释放锁之前无法再获得锁。
-### 2. Redis 的 SETNX 指令
+### 2. Redis 的 SETNX 指令
-使用 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。
+使用 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。
-SETNX 指令和数据库的唯一索引类似,可以保证只存在一个 Key 的键值对,可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。
+SETNX 指令和数据库的唯一索引类似,可以保证只存在一个 Key 的键值对,可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。
-EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了死锁的发生。
+EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了死锁的发生。
-### 3. Redis 的 RedLock 算法
+### 3. Redis 的 RedLock 算法
-使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。
+使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。
-- 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。
-- 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。
-- 如果锁获取失败,会到每个实例上释放锁。
+- 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。
+- 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。
+- 如果锁获取失败,会到每个实例上释放锁。
-### 4. Zookeeper 的有序节点
+### 4. Zookeeper 的有序节点
-Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。
+Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。
-**(一)抽象模型**
+**(一)抽象模型**
-Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。
+Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。
-
+
-**(二)节点类型**
+**(二)节点类型**
-- 永久节点:不会因为会话结束或者超时而消失;
-- 临时节点:如果会话结束或者超时就会消失;
-- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,依次类推。
+- 永久节点:不会因为会话结束或者超时而消失;
+- 临时节点:如果会话结束或者超时就会消失;
+- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,依次类推。
-**(三)监听器**
+**(三)监听器**
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
-**(四)分布式锁实现**
+**(四)分布式锁实现**
-- 创建一个锁目录 /lock;
-- 在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000,第二个为 /lock/lock-0000000001,以此类推;
-- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
-- 执行业务代码,完成后,删除对应的子节点。
+- 创建一个锁目录 /lock;
+- 在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000,第二个为 /lock/lock-0000000001,以此类推;
+- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
+- 执行业务代码,完成后,删除对应的子节点。
-**(五)会话超时**
+**(五)会话超时**
-如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁的死锁问题。
+如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁的死锁问题。
-**(六)羊群效应**
+**(六)羊群效应**
一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。
-# 三、分布式 Session
+# 三、分布式 Session
-在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。
+在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。
-
+![](index_files/cookiedata.png)
-### 1. Sticky Sessions
+### 1. Sticky Sessions
-需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。
+需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。
-缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。
+缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。
-
+![](index_files/MultiNode-StickySessions.jpg)
-### 2. Session Replication
+### 2. Session Replication
-在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。
+在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。
缺点:需要更好的服务器硬件条件;需要对服务器进行配置。
-
+![](index_files/MultiNode-SessionReplication.jpg)
-### 3. Persistent DataStore
+### 3. Persistent DataStore
-将 Session 信息持久化到一个数据库中。
+将 Session 信息持久化到一个数据库中。
-缺点:有可能需要去实现存取 Session 的代码。
+缺点:有可能需要去实现存取 Session 的代码。
-
+![](index_files/MultiNode-SpringSession.jpg)
-### 4. In-Memory DataStore
+### 4. In-Memory DataStore
-可以使用 Redis 和 Memcached 这种内存型数据库对 Session 进行存储,可以大大提高 Session 的读写效率。内存型数据库同样可以持久化数据到磁盘中来保证数据的安全性。
+可以使用 Redis 和 Memcached 这种内存型数据库对 Session 进行存储,可以大大提高 Session 的读写效率。内存型数据库同样可以持久化数据到磁盘中来保证数据的安全性。
-# 四、负载均衡
+# 四、负载均衡
-## 算法
+## 算法
-### 1. 轮询(Round Robin)
+### 1. 轮询(Round Robin)
-轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。
+轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。
-
+![](index_files/2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg)
-该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2)。
+该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2)。
-
+![](index_files/f7ecbb8d-bb8b-4d45-a3b7-f49425d6d83d.jpg)
-### 2. 加权轮询(Weighted Round Robbin)
+### 2. 加权轮询(Weighted Round Robbin)
-加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。
+加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。
-
+![](index_files/211c60d4-75ca-4acd-8a4f-171458ed58b4.jpg)
-### 3. 最少连接(least Connections)
+### 3. 最少连接(least Connections)
-由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。
+由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。
-
+![](index_files/3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg)
-最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。
+最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。
-
+![](index_files/1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg)
-### 4. 加权最少连接(Weighted Least Connection)
+### 4. 加权最少连接(Weighted Least Connection)
在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。
-
+![](index_files/44edefb7-4b58-4519-b8ee-4aca01697b78.jpg)
-### 5. 随机算法(Random)
+### 5. 随机算法(Random)
把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。
-
+![](index_files/0ee0f61b-c782-441e-bf34-665650198ae0.jpg)
-### 6. 源地址哈希法 (IP Hash)
+### 6. 源地址哈希法 (IP Hash)
-源地址哈希通过对客户端 IP 哈希计算得到的一个数值,用该数值对服务器数量进行取模运算,取模结果便是目标服务器的序号。
+源地址哈希通过对客户端 IP 哈希计算得到的一个数值,用该数值对服务器数量进行取模运算,取模结果便是目标服务器的序号。
-- 优点:保证同一 IP 的客户端都会被 hash 到同一台服务器上。
-- 缺点:不利于集群扩展,后台服务器数量变更都会影响 hash 结果。可以采用一致性 Hash 改进。
+- 优点:保证同一 IP 的客户端都会被 hash 到同一台服务器上。
+- 缺点:不利于集群扩展,后台服务器数量变更都会影响 hash 结果。可以采用一致性 Hash 改进。
-
+![](index_files/2018040302.jpg)
-## 实现
+## 实现
-### 1. HTTP 重定向
+### 1. HTTP 重定向
-HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。
+HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。
缺点:
-- 用户访问的延迟会增加;
-- 如果负载均衡器宕机,就无法访问该站点。
+- 用户访问的延迟会增加;
+- 如果负载均衡器宕机,就无法访问该站点。
-
+![](index_files/10bdf7bf-0daa-4a26-b927-f142b3f8e72b.png)
-### 2. DNS 重定向
+### 2. DNS 重定向
-使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。
+使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。
缺点:
-- DNS 查找表可能会被客户端缓存起来,那么之后的所有请求都会被重定向到同一个服务器。
+- DNS 查找表可能会被客户端缓存起来,那么之后的所有请求都会被重定向到同一个服务器。
-
+![](index_files/f8b16d1e-7363-4544-94d6-4939fdf849dc.png)
-### 3. 修改 MAC 地址
+### 3. 修改 MAC 地址
-使用 LVS(Linux Virtual Server)这种链路层负载均衡器,根据负载情况修改请求的 MAC 地址。
+使用 LVS(Linux Virtual Server)这种链路层负载均衡器,根据负载情况修改请求的 MAC 地址。
-
+![](index_files/f0e35b7a-2948-488a-a5a9-97d3f6b5e2d7.png)
-### 4. 修改 IP 地址
+### 4. 修改 IP 地址
-在网络层修改请求的目的 IP 地址。
+在网络层修改请求的目的 IP 地址。
-
+![](index_files/265a355d-aead-48aa-b455-f33b62fe729f.png)
-### 5. 代理自动配置
+### 5. 代理自动配置
正向代理与反向代理的区别:
-- 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。
-- 反向代理:发生在服务器端,用户不知道代理的存在。
+- 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。
+- 反向代理:发生在服务器端,用户不知道代理的存在。
-PAC 服务器是用来判断一个请求是否要经过代理。
+PAC 服务器是用来判断一个请求是否要经过代理。
-
+![](index_files/52e1af6f-3a7a-4bee-aa8f-fcb5dacebe40.jpg)
-# 参考资料
+# 参考资料
-- [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)
-- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html)
-- [关于分布式事务](http://blog.csdn.net/suifeng3051/article/details/52691210)
-- [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html)
-- [微服务场景下的数据一致性解决方案](https://opentalk.upyun.com/310.html)
-- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html)
+- [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)
+- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html)
+- [关于分布式事务](http://blog.csdn.net/suifeng3051/article/details/52691210)
+- [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html)
+- [微服务场景下的数据一致性解决方案](https://opentalk.upyun.com/310.html)
+- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html)
+---bottom---CyC---
+![](index_files/c8dbff58-d981-48be-8c1c-caa6c2738791.jpg)
+![](index_files/aa844ff0-cd16-4478-b415-da071b615a17.jpg)
+![](index_files/e3bf5de4-ab1e-4a9b-896d-4b0ad7e9220a.jpg)
+![](index_files/31d99967-1171-448e-8531-bccf5c14cffe.jpg)
+![](index_files/cookiedata.png)
+![](index_files/MultiNode-StickySessions.jpg)
+![](index_files/MultiNode-SessionReplication.jpg)
+![](index_files/MultiNode-SpringSession.jpg)
+![](index_files/2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg)
+![](index_files/f7ecbb8d-bb8b-4d45-a3b7-f49425d6d83d.jpg)
+![](index_files/211c60d4-75ca-4acd-8a4f-171458ed58b4.jpg)
+![](index_files/3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg)
+![](index_files/1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg)
+![](index_files/44edefb7-4b58-4519-b8ee-4aca01697b78.jpg)
+![](index_files/0ee0f61b-c782-441e-bf34-665650198ae0.jpg)
+![](index_files/2018040302.jpg)
+![](index_files/10bdf7bf-0daa-4a26-b927-f142b3f8e72b.png)
+![](index_files/f8b16d1e-7363-4544-94d6-4939fdf849dc.png)
+![](index_files/f0e35b7a-2948-488a-a5a9-97d3f6b5e2d7.png)
+![](index_files/265a355d-aead-48aa-b455-f33b62fe729f.png)
+![](index_files/52e1af6f-3a7a-4bee-aa8f-fcb5dacebe40.jpg)
diff --git a/notes/剑指 offer 题解.md b/notes/剑指 offer 题解.md
index f4927da8..5a20c99c 100644
--- a/notes/剑指 offer 题解.md
+++ b/notes/剑指 offer 题解.md
@@ -1,2835 +1,2753 @@
-
-* [2. 实现 Singleton](#2-实现-singleton)
-* [3. 数组中重复的数字](#3-数组中重复的数字)
-* [4. 二维数组中的查找](#4-二维数组中的查找)
-* [5. 替换空格](#5-替换空格)
-* [6. 从尾到头打印链表](#6-从尾到头打印链表)
-* [7. 重建二叉树](#7-重建二叉树)
-* [8. 二叉树的下一个结点](#8-二叉树的下一个结点)
-* [9. 用两个栈实现队列](#9-用两个栈实现队列)
-* [10.1 斐波那契数列](#101-斐波那契数列)
-* [10.2 跳台阶](#102-跳台阶)
-* [10.3 变态跳台阶](#103-变态跳台阶)
-* [10.4 矩形覆盖](#104-矩形覆盖)
-* [11. 旋转数组的最小数字](#11-旋转数组的最小数字)
-* [12. 矩阵中的路径](#12-矩阵中的路径)
-* [13. 机器人的运动范围](#13-机器人的运动范围)
-* [14. 剪绳子](#14-剪绳子)
-* [15. 二进制中 1 的个数](#15-二进制中-1-的个数)
-* [16. 数值的整数次方](#16-数值的整数次方)
-* [17. 打印从 1 到最大的 n 位数](#17-打印从-1-到最大的-n-位数)
-* [18.1 在 O(1) 时间内删除链表节点](#181-在-o1-时间内删除链表节点)
-* [18.2 删除链表中重复的结点](#182-删除链表中重复的结点)
-* [19. 正则表达式匹配](#19-正则表达式匹配)
-* [20. 表示数值的字符串](#20-表示数值的字符串)
-* [21. 调整数组顺序使奇数位于偶数前面](#21-调整数组顺序使奇数位于偶数前面)
-* [22. 链表中倒数第 K 个结点](#22-链表中倒数第-k-个结点)
-* [23. 链表中环的入口结点](#23-链表中环的入口结点)
-* [24. 反转链表](#24-反转链表)
-* [25. 合并两个排序的链表](#25-合并两个排序的链表)
-* [26. 树的子结构](#26-树的子结构)
-* [27. 二叉树的镜像](#27-二叉树的镜像)
-* [28 对称的二叉树](#28-对称的二叉树)
-* [29. 顺时针打印矩阵](#29-顺时针打印矩阵)
-* [30. 包含 min 函数的栈](#30-包含-min-函数的栈)
-* [31. 栈的压入、弹出序列](#31-栈的压入弹出序列)
-* [32.1 从上往下打印二叉树](#321-从上往下打印二叉树)
-* [32.2 把二叉树打印成多行](#322-把二叉树打印成多行)
-* [32.3 按之字形顺序打印二叉树](#323-按之字形顺序打印二叉树)
-* [33. 二叉搜索树的后序遍历序列](#33-二叉搜索树的后序遍历序列)
-* [34. 二叉树中和为某一值的路径](#34-二叉树中和为某一值的路径)
-* [35. 复杂链表的复制](#35-复杂链表的复制)
-* [36. 二叉搜索树与双向链表](#36-二叉搜索树与双向链表)
-* [37. 序列化二叉树](#37-序列化二叉树)
-* [38. 字符串的排列](#38-字符串的排列)
-* [39. 数组中出现次数超过一半的数字](#39-数组中出现次数超过一半的数字)
-* [40. 最小的 K 个数](#40-最小的-k-个数)
-* [41.1 数据流中的中位数](#411-数据流中的中位数)
-* [41.2 字符流中第一个不重复的字符](#412-字符流中第一个不重复的字符)
-* [42. 连续子数组的最大和](#42-连续子数组的最大和)
-* [43. 从 1 到 n 整数中 1 出现的次数](#43-从-1-到-n-整数中-1-出现的次数)
-* [44. 数字序列中的某一位数字](#44-数字序列中的某一位数字)
-* [45. 把数组排成最小的数](#45-把数组排成最小的数)
-* [46. 把数字翻译成字符串](#46-把数字翻译成字符串)
-* [47. 礼物的最大价值](#47-礼物的最大价值)
-* [48. 最长不含重复字符的子字符串](#48-最长不含重复字符的子字符串)
-* [49. 丑数](#49-丑数)
-* [50. 第一个只出现一次的字符位置](#50-第一个只出现一次的字符位置)
-* [51. 数组中的逆序对](#51-数组中的逆序对)
-* [52. 两个链表的第一个公共结点](#52-两个链表的第一个公共结点)
-* [53 数字在排序数组中出现的次数](#53-数字在排序数组中出现的次数)
-* [54. 二叉搜索树的第 K 个结点](#54-二叉搜索树的第-k-个结点)
-* [55.1 二叉树的深度](#551-二叉树的深度)
-* [55.2 平衡二叉树](#552-平衡二叉树)
-* [56. 数组中只出现一次的数字](#56-数组中只出现一次的数字)
-* [57.1 和为 S 的两个数字](#571-和为-s-的两个数字)
-* [57.2 和为 S 的连续正数序列](#572-和为-s-的连续正数序列)
-* [58.1 翻转单词顺序列](#581-翻转单词顺序列)
-* [58.2 左旋转字符串](#582-左旋转字符串)
-* [59. 滑动窗口的最大值](#59-滑动窗口的最大值)
-* [60. n 个骰子的点数](#60-n-个骰子的点数)
-* [61. 扑克牌顺子](#61-扑克牌顺子)
-* [62. 圆圈中最后剩下的数](#62-圆圈中最后剩下的数)
-* [63. 股票的最大利润](#63-股票的最大利润)
-* [64. 求 1+2+3+...+n](#64-求-123n)
-* [65. 不用加减乘除做加法](#65-不用加减乘除做加法)
-* [66. 构建乘积数组](#66-构建乘积数组)
-* [67. 把字符串转换成整数](#67-把字符串转换成整数)
-* [68. 树中两个节点的最低公共祖先](#68-树中两个节点的最低公共祖先)
-* [参考文献](#参考文献)
-
-
-
-# 2. 实现 Singleton
+# 2. 实现 Singleton
[单例模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md)
-# 3. 数组中重复的数字
+# 3. 数组中重复的数字
[NowCoder](https://www.nowcoder.com/practice/623a5ac0ea5b4e5f95552655361ae0a8?tpId=13&tqId=11203&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。例如,如果输入长度为 7 的数组 {2, 3, 1, 0, 2, 5},那么对应的输出是第一个重复的数字 2。
+在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。例如,如果输入长度为 7 的数组 {2, 3, 1, 0, 2, 5},那么对应的输出是第一个重复的数字 2。
-要求复杂度为 O(N) + O(1),也就是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。牛客网讨论区这一题的首票答案使用 nums[i] + length 来将元素标记,这么做会有加法溢出问题。
+要求复杂度为 O(N) + O(1),也就是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。牛客网讨论区这一题的首票答案使用 nums[i] + length 来将元素标记,这么做会有加法溢出问题。
-## 解题思路
+## 解题思路
-这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素放到第 i 个位置上。
+这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素放到第 i 个位置上。
-以 (2, 3, 1, 0, 2, 5) 为例:
+以 (2, 3, 1, 0, 2, 5) 为例:
```text-html-basic
-position-0 : (2,3,1,0,2,5) // 2 <-> 1
- (1,3,2,0,2,5) // 1 <-> 3
- (3,1,2,0,2,5) // 3 <-> 0
- (0,1,2,3,2,5) // already in position
-position-1 : (0,1,2,3,2,5) // already in position
-position-2 : (0,1,2,3,2,5) // already in position
-position-3 : (0,1,2,3,2,5) // already in position
-position-4 : (0,1,2,3,2,5) // nums[i] == nums[nums[i]], exit
+position-0 : (2,3,1,0,2,5) // 2 <-> 1
+ (1,3,2,0,2,5) // 1 <-> 3
+ (3,1,2,0,2,5) // 3 <-> 0
+ (0,1,2,3,2,5) // already in position
+position-1 : (0,1,2,3,2,5) // already in position
+position-2 : (0,1,2,3,2,5) // already in position
+position-3 : (0,1,2,3,2,5) // already in position
+position-4 : (0,1,2,3,2,5) // nums[i] == nums[nums[i]], exit
```
-遍历到位置 4 时,该位置上的数为 2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复。
+遍历到位置 4 时,该位置上的数为 2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复。
```java
-public boolean duplicate(int[] nums, int length, int[] duplication) {
- if (nums == null || length <= 0)
- return false;
- for (int i = 0; i < length; i++) {
- while (nums[i] != i) {
- if (nums[i] == nums[nums[i]]) {
- duplication[0] = nums[i];
- return true;
- }
- swap(nums, i, nums[i]);
- }
- }
- return false;
+public boolean duplicate(int[] nums, int length, int[] duplication) {
+ if (nums == null || length <= 0)
+ return false;
+ for (int i = 0; i < length; i++) {
+ while (nums[i] != i) {
+ if (nums[i] == nums[nums[i]]) {
+ duplication[0] = nums[i];
+ return true;
+ }
+ swap(nums, i, nums[i]);
+ }
+ }
+ return false;
}
-private void swap(int[] nums, int i, int j) {
- int t = nums[i]; nums[i] = nums[j]; nums[j] = t;
+private void swap(int[] nums, int i, int j) {
+ int t = nums[i]; nums[i] = nums[j]; nums[j] = t;
}
```
-# 4. 二维数组中的查找
+# 4. 二维数组中的查找
[NowCoder](https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?tpId=13&tqId=11154&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
```html
-Consider the following matrix:
+Consider the following matrix:
[
- [1, 4, 7, 11, 15],
- [2, 5, 8, 12, 19],
- [3, 6, 9, 16, 22],
- [10, 13, 14, 17, 24],
- [18, 21, 23, 26, 30]
+ [1, 4, 7, 11, 15],
+ [2, 5, 8, 12, 19],
+ [3, 6, 9, 16, 22],
+ [10, 13, 14, 17, 24],
+ [18, 21, 23, 26, 30]
]
-Given target = 5, return true.
-Given target = 20, return false.
+Given target = 5, return true.
+Given target = 20, return false.
```
-## 解题思路
+## 解题思路
-从右上角开始查找。因为矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。
+从右上角开始查找。因为矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。
-复杂度:O(M + N) + O(1)
+复杂度:O(M + N) + O(1)
```java
-public boolean Find(int target, int[][] matrix) {
- if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
- return false;
- int rows = matrix.length, cols = matrix[0].length;
- int r = 0, c = cols - 1; // 从右上角开始
- while (r <= rows - 1 && c >= 0) {
- if (target == matrix[r][c])
- return true;
- else if (target > matrix[r][c])
- r++;
- else
- c--;
- }
- return false;
+public boolean Find(int target, int[][] matrix) {
+ if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
+ return false;
+ int rows = matrix.length, cols = matrix[0].length;
+ int r = 0, c = cols - 1; // 从右上角开始
+ while (r <= rows - 1 && c >= 0) {
+ if (target == matrix[r][c])
+ return true;
+ else if (target > matrix[r][c])
+ r++;
+ else
+ c--;
+ }
+ return false;
}
```
-# 5. 替换空格
+# 5. 替换空格
[NowCoder](https://www.nowcoder.com/practice/4060ac7e3e404ad1a894ef3e17650423?tpId=13&tqId=11155&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为 We Are Happy. 则经过替换之后的字符串为 We%20Are%20Happy。
+请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为 We Are Happy. 则经过替换之后的字符串为 We%20Are%20Happy。
-## 解题思路
+## 解题思路
在字符串尾部填充任意字符,使得字符串的长度等于字符串替换之后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。
-令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。
+令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。
-从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。
+从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。
-复杂度:O(N) + O(1)
+复杂度:O(N) + O(1)
```java
-public String replaceSpace(StringBuffer str) {
- int oldLen = str.length();
- for (int i = 0; i < oldLen; i++)
- if (str.charAt(i) == ' ')
- str.append(" ");
+public String replaceSpace(StringBuffer str) {
+ int oldLen = str.length();
+ for (int i = 0; i < oldLen; i++)
+ if (str.charAt(i) == ' ')
+ str.append(" ");
- int P1 = oldLen - 1, P2 = str.length() - 1;
- while (P1 >= 0 && P2 > P1) {
- char c = str.charAt(P1--);
- if (c == ' ') {
- str.setCharAt(P2--, '0');
- str.setCharAt(P2--, '2');
- str.setCharAt(P2--, '%');
- } else {
- str.setCharAt(P2--, c);
- }
- }
- return str.toString();
+ int P1 = oldLen - 1, P2 = str.length() - 1;
+ while (P1 >= 0 && P2 > P1) {
+ char c = str.charAt(P1--);
+ if (c == ' ') {
+ str.setCharAt(P2--, '0');
+ str.setCharAt(P2--, '2');
+ str.setCharAt(P2--, '%');
+ } else {
+ str.setCharAt(P2--, c);
+ }
+ }
+ return str.toString();
}
```
-# 6. 从尾到头打印链表
+# 6. 从尾到头打印链表
[NowCoder](https://www.nowcoder.com/practice/d0267f7f55b3412ba93bd35cfa8e8035?tpId=13&tqId=11156&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
输入链表的第一个节点,从尾到头反过来打印出每个结点的值。
-
+
-## 解题思路
+## 解题思路
-### 使用栈
+### 使用栈
```java
-public ArrayList printListFromTailToHead(ListNode listNode) {
- Stack stack = new Stack<>();
- while (listNode != null) {
- stack.add(listNode.val);
- listNode = listNode.next;
- }
- ArrayList ret = new ArrayList<>();
- while (!stack.isEmpty())
- ret.add(stack.pop());
- return ret;
+public ArrayList printListFromTailToHead(ListNode listNode) {
+ Stack stack = new Stack<>();
+ while (listNode != null) {
+ stack.add(listNode.val);
+ listNode = listNode.next;
+ }
+ ArrayList ret = new ArrayList<>();
+ while (!stack.isEmpty())
+ ret.add(stack.pop());
+ return ret;
}
```
-### 使用递归
+### 使用递归
```java
-public ArrayList printListFromTailToHead(ListNode listNode) {
- ArrayList ret = new ArrayList<>();
- if (listNode != null) {
- ret.addAll(printListFromTailToHead(listNode.next));
- ret.add(listNode.val);
- }
- return ret;
+public ArrayList printListFromTailToHead(ListNode listNode) {
+ ArrayList ret = new ArrayList<>();
+ if (listNode != null) {
+ ret.addAll(printListFromTailToHead(listNode.next));
+ ret.add(listNode.val);
+ }
+ return ret;
}
```
-### 使用 Collections.reverse()
+### 使用 Collections.reverse()
```java
-public ArrayList printListFromTailToHead(ListNode listNode) {
- ArrayList ret = new ArrayList<>();
- while (listNode != null) {
- ret.add(listNode.val);
- listNode = listNode.next;
- }
- Collections.reverse(ret);
- return ret;
+public ArrayList printListFromTailToHead(ListNode listNode) {
+ ArrayList ret = new ArrayList<>();
+ while (listNode != null) {
+ ret.add(listNode.val);
+ listNode = listNode.next;
+ }
+ Collections.reverse(ret);
+ return ret;
}
```
-### 使用头插法
+### 使用头插法
利用链表头插法为逆序的特点。
头结点和第一个节点的区别:头结点是在头插法中使用的一个额外节点,这个节点不存储值;第一个节点就是链表的第一个真正存储值的节点。
```java
-public ArrayList printListFromTailToHead(ListNode listNode) {
- // 头插法构建逆序链表
- ListNode head = new ListNode(-1);
- while (listNode != null) {
- ListNode memo = listNode.next;
- listNode.next = head.next;
- head.next = listNode;
- listNode = memo;
- }
- // 构建 ArrayList
- ArrayList ret = new ArrayList<>();
- head = head.next;
- while (head != null) {
- ret.add(head.val);
- head = head.next;
- }
- return ret;
+public ArrayList printListFromTailToHead(ListNode listNode) {
+ // 头插法构建逆序链表
+ ListNode head = new ListNode(-1);
+ while (listNode != null) {
+ ListNode memo = listNode.next;
+ listNode.next = head.next;
+ head.next = listNode;
+ listNode = memo;
+ }
+ // 构建 ArrayList
+ ArrayList ret = new ArrayList<>();
+ head = head.next;
+ while (head != null) {
+ ret.add(head.val);
+ head = head.next;
+ }
+ return ret;
}
```
-# 7. 重建二叉树
+# 7. 重建二叉树
[NowCoder](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
```html
-preorder = [3,9,20,15,7]
-inorder = [9,3,15,20,7]
+preorder = [3,9,20,15,7]
+inorder = [9,3,15,20,7]
```
-
+
-## 解题思路
+## 解题思路
前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。
```java
-private Map inOrderNumsIndexs = new HashMap<>(); // 缓存中序遍历数组的每个值对应的索引
+private Map inOrderNumsIndexs = new HashMap<>(); // 缓存中序遍历数组的每个值对应的索引
-public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
- for (int i = 0; i < in.length; i++)
- inOrderNumsIndexs.put(in[i], i);
- return reConstructBinaryTree(pre, 0, pre.length - 1, in, 0, in.length - 1);
+public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
+ for (int i = 0; i < in.length; i++)
+ inOrderNumsIndexs.put(in[i], i);
+ return reConstructBinaryTree(pre, 0, pre.length - 1, in, 0, in.length - 1);
}
-private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in, int inL, int inR) {
- if (preL > preR)
- return null;
- TreeNode root = new TreeNode(pre[preL]);
- int inIndex = inOrderNumsIndexs.get(root.val);
- int leftTreeSize = inIndex - inL;
- root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, in, inL, inL + leftTreeSize - 1);
- root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, in, inL + leftTreeSize + 1, inR);
- return root;
+private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in, int inL, int inR) {
+ if (preL > preR)
+ return null;
+ TreeNode root = new TreeNode(pre[preL]);
+ int inIndex = inOrderNumsIndexs.get(root.val);
+ int leftTreeSize = inIndex - inL;
+ root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, in, inL, inL + leftTreeSize - 1);
+ root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, in, inL + leftTreeSize + 1, inR);
+ return root;
}
```
-# 8. 二叉树的下一个结点
+# 8. 二叉树的下一个结点
[NowCoder](https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e?tpId=13&tqId=11210&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
-## 解题思路
+## 解题思路
-① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点;
+① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点;
-
+
-② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。
+② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。
-
+
```java
-public class TreeLinkNode {
- int val;
- TreeLinkNode left = null;
- TreeLinkNode right = null;
- TreeLinkNode next = null;
+public class TreeLinkNode {
+ int val;
+ TreeLinkNode left = null;
+ TreeLinkNode right = null;
+ TreeLinkNode next = null;
- TreeLinkNode(int val) {
- this.val = val;
- }
+ TreeLinkNode(int val) {
+ this.val = val;
+ }
}
```
```java
-public TreeLinkNode GetNext(TreeLinkNode pNode) {
- if (pNode.right != null) {
- TreeLinkNode node = pNode.right;
- while (node.left != null)
- node = node.left;
- return node;
- } else {
- while (pNode.next != null) {
- TreeLinkNode parent = pNode.next;
- if (parent.left == pNode)
- return parent;
- pNode = pNode.next;
- }
- }
- return null;
+public TreeLinkNode GetNext(TreeLinkNode pNode) {
+ if (pNode.right != null) {
+ TreeLinkNode node = pNode.right;
+ while (node.left != null)
+ node = node.left;
+ return node;
+ } else {
+ while (pNode.next != null) {
+ TreeLinkNode parent = pNode.next;
+ if (parent.left == pNode)
+ return parent;
+ pNode = pNode.next;
+ }
+ }
+ return null;
}
```
-# 9. 用两个栈实现队列
+# 9. 用两个栈实现队列
[NowCoder](https://www.nowcoder.com/practice/54275ddae22f475981afa2244dd448c6?tpId=13&tqId=11158&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。队列中的元素为 int 类型。
+用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。队列中的元素为 int 类型。
-## 解题思路
+## 解题思路
-in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。
+in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。
-
+
```java
-Stack in = new Stack();
-Stack out = new Stack();
+Stack in = new Stack();
+Stack out = new Stack();
-public void push(int node) {
- in.push(node);
+public void push(int node) {
+ in.push(node);
}
-public int pop() throws Exception {
- if (out.isEmpty())
- while (!in.isEmpty())
- out.push(in.pop());
+public int pop() throws Exception {
+ if (out.isEmpty())
+ while (!in.isEmpty())
+ out.push(in.pop());
- if (out.isEmpty())
- throw new Exception("queue is empty");
+ if (out.isEmpty())
+ throw new Exception("queue is empty");
- return out.pop();
+ return out.pop();
}
```
-# 10.1 斐波那契数列
+# 10.1 斐波那契数列
[NowCoder](https://www.nowcoder.com/practice/c6c7742f5ba7442aada113136ddea0c3?tpId=13&tqId=11160&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-求菲波那契数列的第 n 项,n <= 39。
+求菲波那契数列的第 n 项,n <= 39。
-
+$$ f(n)=\left\{ \begin{array}{rcl} 0 && { n=0 }\\ 1 && { n=1 } \\ f(n-1)+f(n-2) && {n>1} \end{array} \right. $$
-## 解题思路
+## 解题思路
-如果使用递归求解,会重复计算一些子问题。例如,计算 f(10) 需要计算 f(9) 和 f(8),计算 f(9) 需要计算 f(8) 和 f(7),可以看到 f(8) 被重复计算了。
+如果使用递归求解,会重复计算一些子问题。例如,计算 f(10) 需要计算 f(9) 和 f(8),计算 f(9) 需要计算 f(8) 和 f(7),可以看到 f(8) 被重复计算了。
-
+
递归方法是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,避免重复求解子问题。
```java
-public int Fibonacci(int n) {
- if (n <= 1)
- return n;
- int[] fib = new int[n + 1];
- fib[1] = 1;
- for (int i = 2; i <= n; i++)
- fib[i] = fib[i - 1] + fib[i - 2];
- return fib[n];
+public int Fibonacci(int n) {
+ if (n <= 1)
+ return n;
+ int[] fib = new int[n + 1];
+ fib[1] = 1;
+ for (int i = 2; i <= n; i++)
+ fib[i] = fib[i - 1] + fib[i - 2];
+ return fib[n];
}
```
-考虑到第 i 项只与第 i-1 和第 i-2 项有关,因此只需要存储前两项的值就能求解第 i 项,从而将空间复杂度由 O(N) 降低为 O(1)。
+考虑到第 i 项只与第 i-1 和第 i-2 项有关,因此只需要存储前两项的值就能求解第 i 项,从而将空间复杂度由 O(N) 降低为 O(1)。
```java
-public int Fibonacci(int n) {
- if (n <= 1)
- return n;
- int pre2 = 0, pre1 = 1;
- int fib = 0;
- for (int i = 2; i <= n; i++) {
- fib = pre2 + pre1;
- pre2 = pre1;
- pre1 = fib;
- }
- return fib;
+public int Fibonacci(int n) {
+ if (n <= 1)
+ return n;
+ int pre2 = 0, pre1 = 1;
+ int fib = 0;
+ for (int i = 2; i <= n; i++) {
+ fib = pre2 + pre1;
+ pre2 = pre1;
+ pre1 = fib;
+ }
+ return fib;
}
```
-由于待求解的 n 小于 40,因此可以将前 40 项的结果先进行计算,之后就能以 O(1) 时间复杂度得到第 n 项的值了。
+由于待求解的 n 小于 40,因此可以将前 40 项的结果先进行计算,之后就能以 O(1) 时间复杂度得到第 n 项的值了。
```java
-public class Solution {
- private int[] fib = new int[40];
+public class Solution {
+ private int[] fib = new int[40];
- public Solution() {
- fib[1] = 1;
- fib[2] = 2;
- for (int i = 2; i < fib.length; i++)
- fib[i] = fib[i - 1] + fib[i - 2];
- }
+ public Solution() {
+ fib[1] = 1;
+ fib[2] = 2;
+ for (int i = 2; i < fib.length; i++)
+ fib[i] = fib[i - 1] + fib[i - 2];
+ }
- public int Fibonacci(int n) {
- return fib[n];
- }
+ public int Fibonacci(int n) {
+ return fib[n];
+ }
}
```
-# 10.2 跳台阶
+# 10.2 跳台阶
[NowCoder](https://www.nowcoder.com/practice/8c82a5b80378478f9484d87d1c5f12a4?tpId=13&tqId=11161&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
+一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
-## 解题思路
+## 解题思路
-复杂度:O(N) + O(N)
+复杂度:O(N) + O(N)
```java
-public int JumpFloor(int n) {
- if (n == 1)
- return 1;
- int[] dp = new int[n];
- dp[0] = 1;
- dp[1] = 2;
- for (int i = 2; i < n; i++)
- dp[i] = dp[i - 1] + dp[i - 2];
- return dp[n - 1];
+public int JumpFloor(int n) {
+ if (n == 1)
+ return 1;
+ int[] dp = new int[n];
+ dp[0] = 1;
+ dp[1] = 2;
+ for (int i = 2; i < n; i++)
+ dp[i] = dp[i - 1] + dp[i - 2];
+ return dp[n - 1];
}
```
-复杂度:O(N) + O(1)
+复杂度:O(N) + O(1)
```java
-public int JumpFloor(int n) {
- if (n <= 1)
- return n;
- int pre2 = 0, pre1 = 1;
- int result = 0;
- for (int i = 1; i <= n; i++) {
- result = pre2 + pre1;
- pre2 = pre1;
- pre1 = result;
- }
- return result;
+public int JumpFloor(int n) {
+ if (n <= 1)
+ return n;
+ int pre2 = 0, pre1 = 1;
+ int result = 0;
+ for (int i = 1; i <= n; i++) {
+ result = pre2 + pre1;
+ pre2 = pre1;
+ pre1 = result;
+ }
+ return result;
}
```
-# 10.3 变态跳台阶
+# 10.3 变态跳台阶
[NowCoder](https://www.nowcoder.com/practice/22243d016f6b47f2a6928b4313c85387?tpId=13&tqId=11162&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级... 它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
+一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级... 它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
-## 解题思路
+## 解题思路
```java
-public int JumpFloorII(int target) {
- int[] dp = new int[target];
- Arrays.fill(dp, 1);
- for (int i = 1; i < target; i++)
- for (int j = 0; j < i; j++)
- dp[i] += dp[j];
- return dp[target - 1];
+public int JumpFloorII(int target) {
+ int[] dp = new int[target];
+ Arrays.fill(dp, 1);
+ for (int i = 1; i < target; i++)
+ for (int j = 0; j < i; j++)
+ dp[i] += dp[j];
+ return dp[target - 1];
}
```
-# 10.4 矩形覆盖
+# 10.4 矩形覆盖
[NowCoder](https://www.nowcoder.com/practice/72a5a919508a4251859fb2cfb987a0e6?tpId=13&tqId=11163&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-我们可以用 2\*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2\*1 的小矩形无重叠地覆盖一个 2\*n 的大矩形,总共有多少种方法?
+我们可以用 2\*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2\*1 的小矩形无重叠地覆盖一个 2\*n 的大矩形,总共有多少种方法?
-## 解题思路
+## 解题思路
-复杂度:O(N) + O(N)
+复杂度:O(N) + O(N)
```java
-public int RectCover(int n) {
- if (n <= 2)
- return n;
- int[] dp = new int[n];
- dp[0] = 1;
- dp[1] = 2;
- for (int i = 2; i < n; i++)
- dp[i] = dp[i - 1] + dp[i - 2];
- return dp[n - 1];
+public int RectCover(int n) {
+ if (n <= 2)
+ return n;
+ int[] dp = new int[n];
+ dp[0] = 1;
+ dp[1] = 2;
+ for (int i = 2; i < n; i++)
+ dp[i] = dp[i - 1] + dp[i - 2];
+ return dp[n - 1];
}
```
-复杂度:O(N) + O(1)
+复杂度:O(N) + O(1)
```java
-public int RectCover(int n) {
- if (n <= 2)
- return n;
- int pre2 = 1, pre1 = 2;
- int result = 0;
- for (int i = 3; i <= n; i++) {
- result = pre2 + pre1;
- pre2 = pre1;
- pre1 = result;
- }
- return result;
+public int RectCover(int n) {
+ if (n <= 2)
+ return n;
+ int pre2 = 1, pre1 = 2;
+ int result = 0;
+ for (int i = 3; i <= n; i++) {
+ result = pre2 + pre1;
+ pre2 = pre1;
+ pre1 = result;
+ }
+ return result;
}
```
-# 11. 旋转数组的最小数字
+# 11. 旋转数组的最小数字
[NowCoder](https://www.nowcoder.com/practice/9f3231a991af4f55b95579b44b7a01ba?tpId=13&tqId=11159&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。NOTE:给出的所有元素都大于 0,若数组大小为 0,请返回 0。
+把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。NOTE:给出的所有元素都大于 0,若数组大小为 0,请返回 0。
-## 解题思路
+## 解题思路
-当 nums[m] <= nums[h] 的情况下,说明解在 [l, m] 之间,此时令 h = m;否则解在 [m + 1, h] 之间,令 l = m + 1。
+当 nums[m] <= nums[h] 的情况下,说明解在 [l, m] 之间,此时令 h = m;否则解在 [m + 1, h] 之间,令 l = m + 1。
-因为 h 的赋值表达式为 h = m,因此循环体的循环条件应该为 l < h,详细解释请见 [Leetcode 题解](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE)。
+因为 h 的赋值表达式为 h = m,因此循环体的循环条件应该为 l < h,详细解释请见 [Leetcode 题解](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE)。
-复杂度:O(logN) + O(1)
+复杂度:O(logN) + O(1)
```java
-public int minNumberInRotateArray(int[] nums) {
- if (nums.length == 0)
- return 0;
- int l = 0, h = nums.length - 1;
- while (l < h) {
- int m = l + (h - l) / 2;
- if (nums[m] <= nums[h])
- h = m;
- else
- l = m + 1;
- }
- return nums[l];
+public int minNumberInRotateArray(int[] nums) {
+ if (nums.length == 0)
+ return 0;
+ int l = 0, h = nums.length - 1;
+ while (l < h) {
+ int m = l + (h - l) / 2;
+ if (nums[m] <= nums[h])
+ h = m;
+ else
+ l = m + 1;
+ }
+ return nums[l];
}
```
-# 12. 矩阵中的路径
+# 12. 矩阵中的路径
[NowCoder](https://www.nowcoder.com/practice/c61c6999eecb4b8f88a98f66b273a3cc?tpId=13&tqId=11218&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。
-例如下面的矩阵包含了一条 bfce 路径。
+例如下面的矩阵包含了一条 bfce 路径。
-
+
-## 解题思路
+## 解题思路
```java
-private final static int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
-private int rows;
-private int cols;
+private final static int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
+private int rows;
+private int cols;
-public boolean hasPath(char[] array, int rows, int cols, char[] str) {
- if (rows == 0 || cols == 0)
- return false;
- this.rows = rows;
- this.cols = cols;
- boolean[][] marked = new boolean[rows][cols];
- char[][] matrix = buildMatrix(array);
- for (int i = 0; i < rows; i++)
- for (int j = 0; j < cols; j++)
- if (backtracking(matrix, str, marked, 0, i, j))
- return true;
- return false;
+public boolean hasPath(char[] array, int rows, int cols, char[] str) {
+ if (rows == 0 || cols == 0)
+ return false;
+ this.rows = rows;
+ this.cols = cols;
+ boolean[][] marked = new boolean[rows][cols];
+ char[][] matrix = buildMatrix(array);
+ for (int i = 0; i < rows; i++)
+ for (int j = 0; j < cols; j++)
+ if (backtracking(matrix, str, marked, 0, i, j))
+ return true;
+ return false;
}
-private boolean backtracking(char[][] matrix, char[] str, boolean[][] marked, int pathLen, int r, int c) {
- if (pathLen == str.length)
- return true;
- if (r < 0 || r >= rows || c < 0 || c >= cols || matrix[r][c] != str[pathLen] || marked[r][c])
- return false;
- marked[r][c] = true;
- for (int[] n : next)
- if (backtracking(matrix, str, marked, pathLen + 1, r + n[0], c + n[1]))
- return true;
- marked[r][c] = false;
- return false;
+private boolean backtracking(char[][] matrix, char[] str, boolean[][] marked, int pathLen, int r, int c) {
+ if (pathLen == str.length)
+ return true;
+ if (r < 0 || r >= rows || c < 0 || c >= cols || matrix[r][c] != str[pathLen] || marked[r][c])
+ return false;
+ marked[r][c] = true;
+ for (int[] n : next)
+ if (backtracking(matrix, str, marked, pathLen + 1, r + n[0], c + n[1]))
+ return true;
+ marked[r][c] = false;
+ return false;
}
-private char[][] buildMatrix(char[] array) {
- char[][] matrix = new char[rows][cols];
- for (int i = 0, idx = 0; i < rows; i++)
- for (int j = 0; j < cols; j++)
- matrix[i][j] = array[idx++];
- return matrix;
+private char[][] buildMatrix(char[] array) {
+ char[][] matrix = new char[rows][cols];
+ for (int i = 0, idx = 0; i < rows; i++)
+ for (int j = 0; j < cols; j++)
+ matrix[i][j] = array[idx++];
+ return matrix;
}
```
-# 13. 机器人的运动范围
+# 13. 机器人的运动范围
[NowCoder](https://www.nowcoder.com/practice/6e5207314b5241fb83f2329e89fdecc8?tpId=13&tqId=11219&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。例如,当 k 为 18 时,机器人能够进入方格(35, 37),因为 3+5+3+7=18。但是,它不能进入方格(35, 38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子?
+地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。例如,当 k 为 18 时,机器人能够进入方格(35, 37),因为 3+5+3+7=18。但是,它不能进入方格(35, 38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子?
-## 解题思路
+## 解题思路
```java
-private static final int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
-private int cnt = 0;
-private int rows;
-private int cols;
-private int threshold;
-private int[][] digitSum;
+private static final int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
+private int cnt = 0;
+private int rows;
+private int cols;
+private int threshold;
+private int[][] digitSum;
-public int movingCount(int threshold, int rows, int cols) {
- this.rows = rows;
- this.cols = cols;
- this.threshold = threshold;
- initDigitSum();
- boolean[][] marked = new boolean[rows][cols];
- dfs(marked, 0, 0);
- return cnt;
+public int movingCount(int threshold, int rows, int cols) {
+ this.rows = rows;
+ this.cols = cols;
+ this.threshold = threshold;
+ initDigitSum();
+ boolean[][] marked = new boolean[rows][cols];
+ dfs(marked, 0, 0);
+ return cnt;
}
-private void dfs(boolean[][] marked, int r, int c) {
- if (r < 0 || r >= rows || c < 0 || c >= cols || marked[r][c])
- return;
- marked[r][c] = true;
- if (this.digitSum[r][c] > this.threshold)
- return;
- cnt++;
- for (int[] n : next)
- dfs(marked, r + n[0], c + n[1]);
+private void dfs(boolean[][] marked, int r, int c) {
+ if (r < 0 || r >= rows || c < 0 || c >= cols || marked[r][c])
+ return;
+ marked[r][c] = true;
+ if (this.digitSum[r][c] > this.threshold)
+ return;
+ cnt++;
+ for (int[] n : next)
+ dfs(marked, r + n[0], c + n[1]);
}
-private void initDigitSum() {
- int[] digitSumOne = new int[Math.max(rows, cols)];
- for (int i = 0; i < digitSumOne.length; i++) {
- int n = i;
- while (n > 0) {
- digitSumOne[i] += n % 10;
- n /= 10;
- }
- }
- digitSum = new int[rows][cols];
- for (int i = 0; i < this.rows; i++)
- for (int j = 0; j < this.cols; j++)
- digitSum[i][j] = digitSumOne[i] + digitSumOne[j];
+private void initDigitSum() {
+ int[] digitSumOne = new int[Math.max(rows, cols)];
+ for (int i = 0; i < digitSumOne.length; i++) {
+ int n = i;
+ while (n > 0) {
+ digitSumOne[i] += n % 10;
+ n /= 10;
+ }
+ }
+ digitSum = new int[rows][cols];
+ for (int i = 0; i < this.rows; i++)
+ for (int j = 0; j < this.cols; j++)
+ digitSum[i][j] = digitSumOne[i] + digitSumOne[j];
}
```
-# 14. 剪绳子
+# 14. 剪绳子
[Leetcode](https://leetcode.com/problems/integer-break/description/)
-## 题目描述
+## 题目描述
把一根绳子剪成多段,并且使得每段的长度乘积最大。
-For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).
+For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).
-## 解题思路
+## 解题思路
-### 动态规划解法
+### 动态规划解法
```java
-public int integerBreak(int n) {
- int[] dp = new int[n + 1];
- dp[1] = 1;
- for (int i = 2; i <= n; i++)
- for (int j = 1; j < i; j++)
- dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j)));
- return dp[n];
+public int integerBreak(int n) {
+ int[] dp = new int[n + 1];
+ dp[1] = 1;
+ for (int i = 2; i <= n; i++)
+ for (int j = 1; j < i; j++)
+ dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j)));
+ return dp[n];
}
```
-### 贪心解法
+### 贪心解法
-尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现,如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。
+尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现,如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。
-证明:当 n >= 5 时,3(n - 3) - 2(n - 2) = n - 5 >= 0。因此把长度大于 5 的绳子切成两段,令其中一段长度为 3 可以使得两段的乘积最大。
+证明:当 n >= 5 时,3(n - 3) - 2(n - 2) = n - 5 >= 0。因此把长度大于 5 的绳子切成两段,令其中一段长度为 3 可以使得两段的乘积最大。
```java
-public int integerBreak(int n) {
- if (n < 2)
- return 0;
- if (n == 2)
- return 1;
- if (n == 3)
- return 2;
- int timesOf3 = n / 3;
- if (n - timesOf3 * 3 == 1)
- timesOf3--;
- int timesOf2 = (n - timesOf3 * 3) / 2;
- return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2));
+public int integerBreak(int n) {
+ if (n < 2)
+ return 0;
+ if (n == 2)
+ return 1;
+ if (n == 3)
+ return 2;
+ int timesOf3 = n / 3;
+ if (n - timesOf3 * 3 == 1)
+ timesOf3--;
+ int timesOf2 = (n - timesOf3 * 3) / 2;
+ return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2));
}
```
-# 15. 二进制中 1 的个数
+# 15. 二进制中 1 的个数
[NowCoder](https://www.nowcoder.com/practice/8ee967e43c2c4ec193b040ea7fbb10b8?tpId=13&tqId=11164&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-输入一个整数,输出该数二进制表示中 1 的个数。
+输入一个整数,输出该数二进制表示中 1 的个数。
-### Integer.bitCount()
+### Integer.bitCount()
```java
-public int NumberOf1(int n) {
- return Integer.bitCount(n);
+public int NumberOf1(int n) {
+ return Integer.bitCount(n);
}
```
-### n&(n-1)
+### n&(n-1)
-O(logM) 时间复杂度解法,其中 M 表示 1 的个数。
+O(logM) 时间复杂度解法,其中 M 表示 1 的个数。
-该位运算是去除 n 的位级表示中最低的那一位。
+该位运算是去除 n 的位级表示中最低的那一位。
```
-n : 10110100
-n-1 : 10110011
-n&(n-1) : 10110000
+n : 10110100
+n-1 : 10110011
+n&(n-1) : 10110000
```
```java
-public int NumberOf1(int n) {
- int cnt = 0;
- while (n != 0) {
- cnt++;
- n &= (n - 1);
- }
- return cnt;
+public int NumberOf1(int n) {
+ int cnt = 0;
+ while (n != 0) {
+ cnt++;
+ n &= (n - 1);
+ }
+ return cnt;
}
```
-# 16. 数值的整数次方
+# 16. 数值的整数次方
[NowCoder](https://www.nowcoder.com/practice/1a834e5e3e1a4b7ba251417554e07c00?tpId=13&tqId=11165&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent。求 base 的 exponent 次方。
+给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent。求 base 的 exponent 次方。
-## 解题思路
+## 解题思路
-下面的讨论中 x 代表 base,n 代表 exponent。
+下面的讨论中 x 代表 base,n 代表 exponent。
-
+$$ x^n=\left\{ \begin{array}{rcl} (x*x)^{n/2} && { n\%2=0}\\ x*(x*x)^{n/2} && { n\%2=1 } \end{array} \right. $$
-因为 (x\*x)n/2 可以通过递归求解,并且每递归一次,n 都减小一半,因此整个算法的时间复杂度为 O(logN)。
+因为 (x\*x)n/2 可以通过递归求解,并且每递归一次,n 都减小一半,因此整个算法的时间复杂度为 O(logN)。
```java
-public double Power(double base, int exponent) {
- if (exponent == 0)
- return 1;
- if (exponent == 1)
- return base;
- boolean isNegative = false;
- if (exponent < 0) {
- exponent = -exponent;
- isNegative = true;
- }
- double pow = Power(base * base, exponent / 2);
- if (exponent % 2 != 0)
- pow = pow * base;
- return isNegative ? 1 / pow : pow;
+public double Power(double base, int exponent) {
+ if (exponent == 0)
+ return 1;
+ if (exponent == 1)
+ return base;
+ boolean isNegative = false;
+ if (exponent < 0) {
+ exponent = -exponent;
+ isNegative = true;
+ }
+ double pow = Power(base * base, exponent / 2);
+ if (exponent % 2 != 0)
+ pow = pow * base;
+ return isNegative ? 1 / pow : pow;
}
```
-# 17. 打印从 1 到最大的 n 位数
+# 17. 打印从 1 到最大的 n 位数
-## 题目描述
+## 题目描述
-输入数字 n,按顺序打印出从 1 最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。
+输入数字 n,按顺序打印出从 1 最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。
-## 解题思路
+## 解题思路
-由于 n 可能会非常大,因此不能直接用 int 表示数字,而是用 char 数组进行存储。
+由于 n 可能会非常大,因此不能直接用 int 表示数字,而是用 char 数组进行存储。
使用回溯法得到所有的数。
```java
-public void print1ToMaxOfNDigits(int n) {
- if (n <= 0)
- return;
- char[] number = new char[n];
- print1ToMaxOfNDigits(number, -1);
+public void print1ToMaxOfNDigits(int n) {
+ if (n <= 0)
+ return;
+ char[] number = new char[n];
+ print1ToMaxOfNDigits(number, -1);
}
-private void print1ToMaxOfNDigits(char[] number, int digit) {
- if (digit == number.length - 1) {
- printNumber(number);
- return;
- }
- for (int i = 0; i < 10; i++) {
- number[digit + 1] = (char) (i + '0');
- print1ToMaxOfNDigits(number, digit + 1);
- }
+private void print1ToMaxOfNDigits(char[] number, int digit) {
+ if (digit == number.length - 1) {
+ printNumber(number);
+ return;
+ }
+ for (int i = 0; i < 10; i++) {
+ number[digit + 1] = (char) (i + '0');
+ 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();
+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) 时间内删除链表节点
+# 18.1 在 O(1) 时间内删除链表节点
-## 解题思路
+## 解题思路
-① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为 O(1)。
+① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为 O(1)。
-
+
-② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null,时间复杂度为 O(N)。
+② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null,时间复杂度为 O(N)。
-
+
-综上,如果进行 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) {
- if (head == null || head.next == null || tobeDelete == null)
- return null;
- if (tobeDelete.next != null) {
- // 要删除的节点不是尾节点
- ListNode next = tobeDelete.next;
- tobeDelete.val = next.val;
- tobeDelete.next = next.next;
- } else {
- ListNode cur = head;
- while (cur.next != tobeDelete)
- cur = cur.next;
- cur.next = null;
- }
- return head;
+public ListNode deleteNode(ListNode head, ListNode tobeDelete) {
+ if (head == null || head.next == null || tobeDelete == null)
+ return null;
+ if (tobeDelete.next != null) {
+ // 要删除的节点不是尾节点
+ ListNode next = tobeDelete.next;
+ tobeDelete.val = next.val;
+ tobeDelete.next = next.next;
+ } else {
+ ListNode cur = head;
+ while (cur.next != tobeDelete)
+ cur = cur.next;
+ cur.next = null;
+ }
+ return head;
}
```
-# 18.2 删除链表中重复的结点
+# 18.2 删除链表中重复的结点
[NowCoder](https://www.nowcoder.com/practice/fc533c45b73a41b0b44ccba763f866ef?tpId=13&tqId=11209&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-
+
-## 解题描述
+## 解题描述
```java
-public ListNode deleteDuplication(ListNode pHead) {
- if (pHead == null || pHead.next == null)
- return pHead;
- ListNode next = pHead.next;
- if (pHead.val == next.val) {
- while (next != null && pHead.val == next.val)
- next = next.next;
- return deleteDuplication(next);
- } else {
- pHead.next = deleteDuplication(pHead.next);
- return pHead;
- }
+public ListNode deleteDuplication(ListNode pHead) {
+ if (pHead == null || pHead.next == null)
+ return pHead;
+ ListNode next = pHead.next;
+ if (pHead.val == next.val) {
+ while (next != null && pHead.val == next.val)
+ next = next.next;
+ return deleteDuplication(next);
+ } else {
+ pHead.next = deleteDuplication(pHead.next);
+ return pHead;
+ }
}
```
-# 19. 正则表达式匹配
+# 19. 正则表达式匹配
[NowCoder](https://www.nowcoder.com/practice/45327ae22b7b413ea21df13ee7d6429c?tpId=13&tqId=11205&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-请实现一个函数用来匹配包括 '.' 和 '\*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '\*' 表示它前面的字符可以出现任意次(包含 0 次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 "aaa" 与模式 "a.a" 和 "ab\*ac\*a" 匹配,但是与 "aa.a" 和 "ab\*a" 均不匹配。
+请实现一个函数用来匹配包括 '.' 和 '\*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '\*' 表示它前面的字符可以出现任意次(包含 0 次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 "aaa" 与模式 "a.a" 和 "ab\*ac\*a" 匹配,但是与 "aa.a" 和 "ab\*a" 均不匹配。
-## 解题思路
+## 解题思路
-应该注意到,'.' 是用来当做一个任意字符,而 '\*' 是用来重复前面的字符。这两个的作用不同,不能把 '.' 的作用和 '\*' 进行类比,从而把它当成重复前面字符一次。
+应该注意到,'.' 是用来当做一个任意字符,而 '\*' 是用来重复前面的字符。这两个的作用不同,不能把 '.' 的作用和 '\*' 进行类比,从而把它当成重复前面字符一次。
```html
-if p.charAt(j) == s.charAt(i) : then dp[i][j] = dp[i-1][j-1];
-if p.charAt(j) == '.' : then dp[i][j] = dp[i-1][j-1];
-if p.charAt(j) == '*' :
- if p.charAt(j-1) != s.charAt(i) : then dp[i][j] = dp[i][j-2] // a* only counts as empty
- if p.charAt(j-1) == s.charAt(i) or
- p.charAt(i-1) == '.' :
- then 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
+if p.charAt(j) == s.charAt(i) : then dp[i][j] = dp[i-1][j-1];
+if p.charAt(j) == '.' : then dp[i][j] = dp[i-1][j-1];
+if p.charAt(j) == '*' :
+ if p.charAt(j-1) != s.charAt(i) : then dp[i][j] = dp[i][j-2] // a* only counts as empty
+ if p.charAt(j-1) == s.charAt(i) or
+ p.charAt(i-1) == '.' :
+ then 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
-public boolean match(char[] str, char[] pattern) {
- int m = str.length, n = pattern.length;
- boolean[][] dp = new boolean[m + 1][n + 1];
- dp[0][0] = true;
- for (int i = 1; i <= n; i++)
- if (pattern[i - 1] == '*')
- dp[0][i] = dp[0][i - 2];
+public boolean match(char[] str, char[] pattern) {
+ int m = str.length, n = pattern.length;
+ boolean[][] dp = new boolean[m + 1][n + 1];
+ dp[0][0] = true;
+ for (int i = 1; i <= n; i++)
+ if (pattern[i - 1] == '*')
+ dp[0][i] = dp[0][i - 2];
- for (int i = 1; i <= m; i++)
- for (int j = 1; j <= n; j++)
- if (str[i - 1] == pattern[j - 1] || pattern[j - 1] == '.')
- dp[i][j] = dp[i - 1][j - 1];
- else if (pattern[j - 1] == '*')
- if (pattern[j - 2] == str[i - 1] || pattern[j - 2] == '.')
- dp[i][j] = dp[i][j - 1] || dp[i][j - 2] || dp[i - 1][j];
- else
- dp[i][j] = dp[i][j - 2];
- return dp[m][n];
+ for (int i = 1; i <= m; i++)
+ for (int j = 1; j <= n; j++)
+ if (str[i - 1] == pattern[j - 1] || pattern[j - 1] == '.')
+ dp[i][j] = dp[i - 1][j - 1];
+ else if (pattern[j - 1] == '*')
+ if (pattern[j - 2] == str[i - 1] || pattern[j - 2] == '.')
+ dp[i][j] = dp[i][j - 1] || dp[i][j - 2] || dp[i - 1][j];
+ else
+ dp[i][j] = dp[i][j - 2];
+ return dp[m][n];
}
```
-# 20. 表示数值的字符串
+# 20. 表示数值的字符串
[NowCoder](https://www.nowcoder.com/practice/6f8c901d091949a5837e24bb82a731f2?tpId=13&tqId=11206&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串 "+100","5e2","-123","3.1416" 和 "-1E-16" 都表示数值。 但是 "12e","1a3.14","1.2.3","+-5" 和 "12e+4.3" 都不是。
+请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串 "+100","5e2","-123","3.1416" 和 "-1E-16" 都表示数值。 但是 "12e","1a3.14","1.2.3","+-5" 和 "12e+4.3" 都不是。
-## 解题思路
+## 解题思路
```java
-public boolean isNumeric(char[] str) {
- if (str == null)
- return false;
- return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?");
+public boolean isNumeric(char[] str) {
+ if (str == null)
+ return false;
+ return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?");
}
```
-# 21. 调整数组顺序使奇数位于偶数前面
+# 21. 调整数组顺序使奇数位于偶数前面
[NowCoder](https://www.nowcoder.com/practice/beb5aa231adc45b2a5dcc5b62c93f593?tpId=13&tqId=11166&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。
-## 解题思路
+## 解题思路
```java
-public void reOrderArray(int[] nums) {
- // 奇数个数
- int oddCnt = 0;
- for (int val : nums)
- if (val % 2 == 1)
- oddCnt++;
- int[] copy = nums.clone();
- int i = 0, j = oddCnt;
- for (int num : copy) {
- if (num % 2 == 1)
- nums[i++] = num;
- else
- nums[j++] = num;
- }
+public void reOrderArray(int[] nums) {
+ // 奇数个数
+ int oddCnt = 0;
+ for (int val : nums)
+ if (val % 2 == 1)
+ oddCnt++;
+ int[] copy = nums.clone();
+ int i = 0, j = oddCnt;
+ for (int num : copy) {
+ if (num % 2 == 1)
+ nums[i++] = num;
+ else
+ nums[j++] = num;
+ }
}
```
-# 22. 链表中倒数第 K 个结点
+# 22. 链表中倒数第 K 个结点
[NowCoder](https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&tqId=11167&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 解题思路
+## 解题思路
-设链表的长度为 N。设两个指针 P1 和 P2,先让 P1 移动 K 个节点,则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动,可以知道当 P1 移动到链表结尾时,P2 移动到 N - K 个节点处,该位置就是倒数第 K 个节点。
+设链表的长度为 N。设两个指针 P1 和 P2,先让 P1 移动 K 个节点,则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动,可以知道当 P1 移动到链表结尾时,P2 移动到 N - K 个节点处,该位置就是倒数第 K 个节点。
-
+
```java
-public ListNode FindKthToTail(ListNode head, int k) {
- if (head == null)
- return null;
- ListNode fast, slow;
- fast = slow = head;
- while (fast != null && k-- > 0)
- fast = fast.next;
- if (k > 0)
- return null;
- while (fast != null) {
- fast = fast.next;
- slow = slow.next;
- }
- return slow;
+public ListNode FindKthToTail(ListNode head, int k) {
+ if (head == null)
+ return null;
+ ListNode fast, slow;
+ fast = slow = head;
+ while (fast != null && k-- > 0)
+ fast = fast.next;
+ if (k > 0)
+ return null;
+ while (fast != null) {
+ fast = fast.next;
+ slow = slow.next;
+ }
+ return slow;
}
```
-# 23. 链表中环的入口结点
+# 23. 链表中环的入口结点
[NowCoder](https://www.nowcoder.com/practice/253d2c59ec3e4bc68da16833f79a38e4?tpId=13&tqId=11208&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 解题思路
+## 解题思路
-使用双指针,一个指针 fast 每次移动两个节点,一个指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。假设相遇点在下图的 z1 位置,此时 fast 移动的节点数为 x+2y+z,slow 为 x+y,由于 fast 速度比 slow 快一倍,因此 x+2y+z=2(x+y),得到 x=z。
+使用双指针,一个指针 fast 每次移动两个节点,一个指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。假设相遇点在下图的 z1 位置,此时 fast 移动的节点数为 x+2y+z,slow 为 x+y,由于 fast 速度比 slow 快一倍,因此 x+2y+z=2(x+y),得到 x=z。
-在相遇点,slow 要到环的入口点还需要移动 z 个节点,如果让 fast 重新从头开始移动,并且速度变为每次移动一个节点,那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z,因此 fast 和 slow 将在环入口点相遇。
+在相遇点,slow 要到环的入口点还需要移动 z 个节点,如果让 fast 重新从头开始移动,并且速度变为每次移动一个节点,那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z,因此 fast 和 slow 将在环入口点相遇。
-
+
```java
-public ListNode EntryNodeOfLoop(ListNode pHead) {
- if (pHead == null || pHead.next == null)
- return null;
- ListNode slow = pHead, fast = pHead;
- while (fast != null && fast.next != null) {
- fast = fast.next.next;
- slow = slow.next;
- if (slow == fast)
- break;
- }
- fast = pHead;
- while (slow != fast) {
- slow = slow.next;
- fast = fast.next;
- }
- return slow;
+public ListNode EntryNodeOfLoop(ListNode pHead) {
+ if (pHead == null || pHead.next == null)
+ return null;
+ ListNode slow = pHead, fast = pHead;
+ while (fast != null && fast.next != null) {
+ fast = fast.next.next;
+ slow = slow.next;
+ if (slow == fast)
+ break;
+ }
+ fast = pHead;
+ while (slow != fast) {
+ slow = slow.next;
+ fast = fast.next;
+ }
+ return slow;
}
```
-# 24. 反转链表
+# 24. 反转链表
[NowCoder](https://www.nowcoder.com/practice/75e878df47f24fdc9dc3e400ec6058ca?tpId=13&tqId=11168&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 解题思路
+## 解题思路
-### 递归
+### 递归
```java
-public ListNode ReverseList(ListNode head) {
- if (head == null || head.next == null)
- return head;
- ListNode next = head.next;
- head.next = null;
- ListNode newHead = ReverseList(next);
- next.next = head;
- return newHead;
+public ListNode ReverseList(ListNode head) {
+ if (head == null || head.next == null)
+ return head;
+ ListNode next = head.next;
+ head.next = null;
+ ListNode newHead = ReverseList(next);
+ next.next = head;
+ return newHead;
}
```
-### 迭代
+### 迭代
```java
-public ListNode ReverseList(ListNode head) {
- ListNode newList = new ListNode(-1);
- while (head != null) {
- ListNode next = head.next;
- head.next = newList.next;
- newList.next = head;
- head = next;
- }
- return newList.next;
+public ListNode ReverseList(ListNode head) {
+ ListNode newList = new ListNode(-1);
+ while (head != null) {
+ ListNode next = head.next;
+ head.next = newList.next;
+ newList.next = head;
+ head = next;
+ }
+ return newList.next;
}
```
-# 25. 合并两个排序的链表
+# 25. 合并两个排序的链表
[NowCoder](https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=13&tqId=11169&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-
+
-## 解题思路
+## 解题思路
-### 递归
+### 递归
```java
-public ListNode Merge(ListNode list1, ListNode list2) {
- if (list1 == null)
- return list2;
- if (list2 == null)
- return list1;
- if (list1.val <= list2.val) {
- list1.next = Merge(list1.next, list2);
- return list1;
- } else {
- list2.next = Merge(list1, list2.next);
- return list2;
- }
+public ListNode Merge(ListNode list1, ListNode list2) {
+ if (list1 == null)
+ return list2;
+ if (list2 == null)
+ return list1;
+ if (list1.val <= list2.val) {
+ list1.next = Merge(list1.next, list2);
+ return list1;
+ } else {
+ list2.next = Merge(list1, list2.next);
+ return list2;
+ }
}
```
-### 迭代
+### 迭代
```java
-public ListNode Merge(ListNode list1, ListNode list2) {
- ListNode head = new ListNode(-1);
- ListNode cur = head;
- while (list1 != null && list2 != null) {
- if (list1.val <= list2.val) {
- cur.next = list1;
- list1 = list1.next;
- } else {
- cur.next = list2;
- list2 = list2.next;
- }
- cur = cur.next;
- }
- if (list1 != null)
- cur.next = list1;
- if (list2 != null)
- cur.next = list2;
- return head.next;
+public ListNode Merge(ListNode list1, ListNode list2) {
+ ListNode head = new ListNode(-1);
+ ListNode cur = head;
+ while (list1 != null && list2 != null) {
+ if (list1.val <= list2.val) {
+ cur.next = list1;
+ list1 = list1.next;
+ } else {
+ cur.next = list2;
+ list2 = list2.next;
+ }
+ cur = cur.next;
+ }
+ if (list1 != null)
+ cur.next = list1;
+ if (list2 != null)
+ cur.next = list2;
+ return head.next;
}
```
-# 26. 树的子结构
+# 26. 树的子结构
[NowCoder](https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-
+
-## 解题思路
+## 解题思路
```java
-public boolean HasSubtree(TreeNode root1, TreeNode root2) {
- if (root1 == null || root2 == null)
- return false;
- return isSubtree(root1, root2) || HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2);
+public boolean HasSubtree(TreeNode root1, TreeNode root2) {
+ if (root1 == null || root2 == null)
+ return false;
+ return isSubtree(root1, root2) || HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2);
}
-private boolean isSubtree(TreeNode root1, TreeNode root2) {
- if (root2 == null)
- return true;
- if (root1 == null)
- return false;
- if (root1.val != root2.val)
- return false;
- return isSubtree(root1.left, root2.left) && isSubtree(root1.right, root2.right);
+private boolean isSubtree(TreeNode root1, TreeNode root2) {
+ if (root2 == null)
+ return true;
+ if (root1 == null)
+ return false;
+ if (root1.val != root2.val)
+ return false;
+ return isSubtree(root1.left, root2.left) && isSubtree(root1.right, root2.right);
}
```
-# 27. 二叉树的镜像
+# 27. 二叉树的镜像
[NowCoder](https://www.nowcoder.com/practice/564f4c26aa584921bc75623e48ca3011?tpId=13&tqId=11171&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-
+
-## 解题思路
+## 解题思路
```java
-public void Mirror(TreeNode root) {
- if (root == null)
- return;
- swap(root);
- Mirror(root.left);
- Mirror(root.right);
+public void Mirror(TreeNode root) {
+ if (root == null)
+ return;
+ swap(root);
+ Mirror(root.left);
+ Mirror(root.right);
}
-private void swap(TreeNode root) {
- TreeNode t = root.left;
- root.left = root.right;
- root.right = t;
+private void swap(TreeNode root) {
+ TreeNode t = root.left;
+ root.left = root.right;
+ root.right = t;
}
```
-# 28 对称的二叉树
+# 28 对称的二叉树
[NowCder](https://www.nowcoder.com/practice/ff05d44dfdb04e1d83bdbdab320efbcb?tpId=13&tqId=11211&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-
+
-## 解题思路
+## 解题思路
```java
-boolean isSymmetrical(TreeNode pRoot) {
- if (pRoot == null)
- return true;
- return isSymmetrical(pRoot.left, pRoot.right);
+boolean isSymmetrical(TreeNode pRoot) {
+ if (pRoot == null)
+ return true;
+ return isSymmetrical(pRoot.left, pRoot.right);
}
-boolean isSymmetrical(TreeNode t1, TreeNode t2) {
- if (t1 == null && t2 == null)
- return true;
- if (t1 == null || t2 == null)
- return false;
- if (t1.val != t2.val)
- return false;
- return isSymmetrical(t1.left, t2.right) && isSymmetrical(t1.right, t2.left);
+boolean isSymmetrical(TreeNode t1, TreeNode t2) {
+ if (t1 == null && t2 == null)
+ return true;
+ if (t1 == null || t2 == null)
+ return false;
+ if (t1.val != t2.val)
+ return false;
+ return isSymmetrical(t1.left, t2.right) && isSymmetrical(t1.right, t2.left);
}
```
-# 29. 顺时针打印矩阵
+# 29. 顺时针打印矩阵
[NowCoder](https://www.nowcoder.com/practice/9b4c81a02cd34f76be2659fa0d54342a?tpId=13&tqId=11172&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-下图的矩阵顺时针打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10
+下图的矩阵顺时针打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10
-
+
-## 解题思路
+## 解题思路
```java
-public ArrayList printMatrix(int[][] matrix) {
- ArrayList ret = new ArrayList<>();
- int r1 = 0, r2 = matrix.length - 1, c1 = 0, c2 = matrix[0].length - 1;
- while (r1 <= r2 && c1 <= c2) {
- for (int i = c1; i <= c2; i++)
- ret.add(matrix[r1][i]);
- for (int i = r1 + 1; i <= r2; i++)
- ret.add(matrix[i][c2]);
- if (r1 != r2)
- for (int i = c2 - 1; i >= c1; i--)
- ret.add(matrix[r2][i]);
- if (c1 != c2)
- for (int i = r2 - 1; i > r1; i--)
- ret.add(matrix[i][c1]);
- r1++; r2--; c1++; c2--;
- }
- return ret;
+public ArrayList printMatrix(int[][] matrix) {
+ ArrayList ret = new ArrayList<>();
+ int r1 = 0, r2 = matrix.length - 1, c1 = 0, c2 = matrix[0].length - 1;
+ while (r1 <= r2 && c1 <= c2) {
+ for (int i = c1; i <= c2; i++)
+ ret.add(matrix[r1][i]);
+ for (int i = r1 + 1; i <= r2; i++)
+ ret.add(matrix[i][c2]);
+ if (r1 != r2)
+ for (int i = c2 - 1; i >= c1; i--)
+ ret.add(matrix[r2][i]);
+ if (c1 != c2)
+ for (int i = r2 - 1; i > r1; i--)
+ ret.add(matrix[i][c1]);
+ r1++; r2--; c1++; c2--;
+ }
+ return ret;
}
```
-# 30. 包含 min 函数的栈
+# 30. 包含 min 函数的栈
[NowCoder](https://www.nowcoder.com/practice/4c776177d2c04c2494f2555c9fcc1e49?tpId=13&tqId=11173&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的 min 函数。
+定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的 min 函数。
-## 解题思路
+## 解题思路
```java
-private Stack stack = new Stack<>();
-private Stack minStack = new Stack<>();
+private Stack stack = new Stack<>();
+private Stack minStack = new Stack<>();
-public void push(int node) {
- stack.push(node);
- minStack.push(minStack.isEmpty() ? node : Math.min(minStack.peek(), node));
+public void push(int node) {
+ stack.push(node);
+ minStack.push(minStack.isEmpty() ? node : Math.min(minStack.peek(), node));
}
-public void pop() {
- stack.pop();
- minStack.pop();
+public void pop() {
+ stack.pop();
+ minStack.pop();
}
-public int top() {
- return stack.peek();
+public int top() {
+ return stack.peek();
}
-public int min() {
- return minStack.peek();
+public int min() {
+ return minStack.peek();
}
```
-# 31. 栈的压入、弹出序列
+# 31. 栈的压入、弹出序列
[NowCoder](https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&tqId=11174&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。
+输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。
-## 解题思路
+## 解题思路
使用一个栈来模拟压入弹出操作。
```java
-public boolean IsPopOrder(int[] pushA, int[] popA) {
- int n = pushA.length;
- Stack stack = new Stack<>();
- for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) {
- stack.push(pushA[pushIndex]);
- while (popIndex < n && stack.peek() == popA[popIndex]) {
- stack.pop();
- popIndex++;
- }
- }
- return stack.isEmpty();
+public boolean IsPopOrder(int[] pushA, int[] popA) {
+ int n = pushA.length;
+ Stack stack = new Stack<>();
+ for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) {
+ stack.push(pushA[pushIndex]);
+ while (popIndex < n && stack.peek() == popA[popIndex]) {
+ stack.pop();
+ popIndex++;
+ }
+ }
+ return stack.isEmpty();
}
```
-# 32.1 从上往下打印二叉树
+# 32.1 从上往下打印二叉树
[NowCoder](https://www.nowcoder.com/practice/7fe2212963db4790b57431d9ed259701?tpId=13&tqId=11175&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
从上往下打印出二叉树的每个节点,同层节点从左至右打印。
例如,以下二叉树层次遍历的结果为:1,2,3,4,5,6,7
-
+
-## 解题思路
+## 解题思路
使用队列来进行层次遍历。
不需要使用两个队列分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。
```java
-public ArrayList PrintFromTopToBottom(TreeNode root) {
- Queue queue = new LinkedList<>();
- ArrayList ret = new ArrayList<>();
- if (root == null)
- return ret;
- queue.add(root);
- while (!queue.isEmpty()) {
- int cnt = queue.size();
- while (cnt-- > 0) {
- TreeNode t = queue.poll();
- if (t.left != null)
- queue.add(t.left);
- if (t.right != null)
- queue.add(t.right);
- ret.add(t.val);
- }
- }
- return ret;
+public ArrayList PrintFromTopToBottom(TreeNode root) {
+ Queue queue = new LinkedList<>();
+ ArrayList ret = new ArrayList<>();
+ if (root == null)
+ return ret;
+ queue.add(root);
+ while (!queue.isEmpty()) {
+ int cnt = queue.size();
+ while (cnt-- > 0) {
+ TreeNode t = queue.poll();
+ if (t.left != null)
+ queue.add(t.left);
+ if (t.right != null)
+ queue.add(t.right);
+ ret.add(t.val);
+ }
+ }
+ return ret;
}
```
-# 32.2 把二叉树打印成多行
+# 32.2 把二叉树打印成多行
[NowCoder](https://www.nowcoder.com/practice/445c44d982d04483b04a54f298796288?tpId=13&tqId=11213&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
和上题几乎一样。
-## 解题思路
+## 解题思路
```java
-ArrayList> Print(TreeNode pRoot) {
- ArrayList> ret = new ArrayList<>();
- if (pRoot == null)
- return ret;
- Queue queue = new LinkedList<>();
- queue.add(pRoot);
- while (!queue.isEmpty()) {
- ArrayList list = new ArrayList<>();
- int cnt = queue.size();
- while (cnt-- > 0) {
- TreeNode node = queue.poll();
- list.add(node.val);
- if (node.left != null)
- queue.add(node.left);
- if (node.right != null)
- queue.add(node.right);
- }
- ret.add(list);
- }
- return ret;
+ArrayList> Print(TreeNode pRoot) {
+ ArrayList> ret = new ArrayList<>();
+ if (pRoot == null)
+ return ret;
+ Queue queue = new LinkedList<>();
+ queue.add(pRoot);
+ while (!queue.isEmpty()) {
+ ArrayList list = new ArrayList<>();
+ int cnt = queue.size();
+ while (cnt-- > 0) {
+ TreeNode node = queue.poll();
+ list.add(node.val);
+ if (node.left != null)
+ queue.add(node.left);
+ if (node.right != null)
+ queue.add(node.right);
+ }
+ ret.add(list);
+ }
+ return ret;
}
```
-# 32.3 按之字形顺序打印二叉树
+# 32.3 按之字形顺序打印二叉树
[NowCoder](https://www.nowcoder.com/practice/91b69814117f4e8097390d107d2efbe0?tpId=13&tqId=11212&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
-## 解题思路
+## 解题思路
```java
-public ArrayList> Print(TreeNode pRoot) {
- ArrayList> ret = new ArrayList<>();
- if (pRoot == null)
- return ret;
- Queue queue = new LinkedList<>();
- queue.add(pRoot);
- boolean reverse = false;
- while (!queue.isEmpty()) {
- ArrayList list = new ArrayList<>();
- int cnt = queue.size();
- while (cnt-- > 0) {
- TreeNode node = queue.poll();
- list.add(node.val);
- if (node.left != null)
- queue.add(node.left);
- if (node.right != null)
- queue.add(node.right);
- }
- if (reverse)
- Collections.reverse(list);
- reverse = !reverse;
- ret.add(list);
- }
- return ret;
+public ArrayList> Print(TreeNode pRoot) {
+ ArrayList> ret = new ArrayList<>();
+ if (pRoot == null)
+ return ret;
+ Queue queue = new LinkedList<>();
+ queue.add(pRoot);
+ boolean reverse = false;
+ while (!queue.isEmpty()) {
+ ArrayList list = new ArrayList<>();
+ int cnt = queue.size();
+ while (cnt-- > 0) {
+ TreeNode node = queue.poll();
+ list.add(node.val);
+ if (node.left != null)
+ queue.add(node.left);
+ if (node.right != null)
+ queue.add(node.right);
+ }
+ if (reverse)
+ Collections.reverse(list);
+ reverse = !reverse;
+ ret.add(list);
+ }
+ return ret;
}
```
-# 33. 二叉搜索树的后序遍历序列
+# 33. 二叉搜索树的后序遍历序列
[NowCoder](https://www.nowcoder.com/practice/a861533d45854474ac791d90e447bafd?tpId=13&tqId=11176&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。假设输入的数组的任意两个数字都互不相同。
-例如,下图是后序遍历序列 3,1,2 所对应的二叉搜索树。
+例如,下图是后序遍历序列 3,1,2 所对应的二叉搜索树。
-
+
-## 解题思路
+## 解题思路
```java
-public boolean VerifySquenceOfBST(int[] sequence) {
- if (sequence == null || sequence.length == 0)
- return false;
- return verify(sequence, 0, sequence.length - 1);
+public boolean VerifySquenceOfBST(int[] sequence) {
+ if (sequence == null || sequence.length == 0)
+ return false;
+ return verify(sequence, 0, sequence.length - 1);
}
-private boolean verify(int[] sequence, int first, int last) {
- if (last - first <= 1)
- return true;
- int rootVal = sequence[last];
- int cutIndex = first;
- while (cutIndex < last && sequence[cutIndex] <= rootVal)
- cutIndex++;
- for (int i = cutIndex + 1; i < last; i++)
- if (sequence[i] < rootVal)
- return false;
- return verify(sequence, first, cutIndex - 1) && verify(sequence, cutIndex, last - 1);
+private boolean verify(int[] sequence, int first, int last) {
+ if (last - first <= 1)
+ return true;
+ int rootVal = sequence[last];
+ int cutIndex = first;
+ while (cutIndex < last && sequence[cutIndex] <= rootVal)
+ cutIndex++;
+ for (int i = cutIndex + 1; i < last; i++)
+ if (sequence[i] < rootVal)
+ return false;
+ return verify(sequence, first, cutIndex - 1) && verify(sequence, cutIndex, last - 1);
}
```
-# 34. 二叉树中和为某一值的路径
+# 34. 二叉树中和为某一值的路径
[NowCoder](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&tqId=11177&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
-下图的二叉树有两条和为 22 的路径:10, 5, 7 和 10, 12
+下图的二叉树有两条和为 22 的路径:10, 5, 7 和 10, 12
-
+
-## 解题思路
+## 解题思路
```java
-private ArrayList> ret = new ArrayList<>();
+private ArrayList> ret = new ArrayList<>();
-public ArrayList> FindPath(TreeNode root, int target) {
- backtracking(root, target, new ArrayList<>());
- return ret;
+public ArrayList> FindPath(TreeNode root, int target) {
+ backtracking(root, target, new ArrayList<>());
+ return ret;
}
-private void backtracking(TreeNode node, int target, ArrayList path) {
- if (node == null)
- return;
- path.add(node.val);
- target -= node.val;
- if (target == 0 && node.left == null && node.right == null) {
- ret.add(new ArrayList(path));
- } else {
- backtracking(node.left, target, path);
- backtracking(node.right, target, path);
- }
- path.remove(path.size() - 1);
+private void backtracking(TreeNode node, int target, ArrayList path) {
+ if (node == null)
+ return;
+ path.add(node.val);
+ target -= node.val;
+ if (target == 0 && node.left == null && node.right == null) {
+ ret.add(new ArrayList(path));
+ } else {
+ backtracking(node.left, target, path);
+ backtracking(node.right, target, path);
+ }
+ path.remove(path.size() - 1);
}
```
-# 35. 复杂链表的复制
+# 35. 复杂链表的复制
[NowCoder](https://www.nowcoder.com/practice/f836b2c43afc4b35ad6adc41ec941dba?tpId=13&tqId=11178&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。
+输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。
-
+
-## 解题思路
+## 解题思路
第一步,在每个节点的后面插入复制的节点。
-
+
-第二步,对复制节点的 random 链接进行赋值。
+第二步,对复制节点的 random 链接进行赋值。
-
+
第三步,拆分。
-
+
```java
-public RandomListNode Clone(RandomListNode pHead) {
- if (pHead == null)
- return null;
- // 插入新节点
- RandomListNode cur = pHead;
- while (cur != null) {
- RandomListNode clone = new RandomListNode(cur.label);
- clone.next = cur.next;
- cur.next = clone;
- cur = clone.next;
- }
- // 建立 random 链接
- cur = pHead;
- while (cur != null) {
- RandomListNode clone = cur.next;
- if (cur.random != null)
- clone.random = cur.random.next;
- cur = clone.next;
- }
- // 拆分
- cur = pHead;
- RandomListNode pCloneHead = pHead.next;
- while (cur.next != null) {
- RandomListNode next = cur.next;
- cur.next = next.next;
- cur = next;
- }
- return pCloneHead;
+public RandomListNode Clone(RandomListNode pHead) {
+ if (pHead == null)
+ return null;
+ // 插入新节点
+ RandomListNode cur = pHead;
+ while (cur != null) {
+ RandomListNode clone = new RandomListNode(cur.label);
+ clone.next = cur.next;
+ cur.next = clone;
+ cur = clone.next;
+ }
+ // 建立 random 链接
+ cur = pHead;
+ while (cur != null) {
+ RandomListNode clone = cur.next;
+ if (cur.random != null)
+ clone.random = cur.random.next;
+ cur = clone.next;
+ }
+ // 拆分
+ cur = pHead;
+ RandomListNode pCloneHead = pHead.next;
+ while (cur.next != null) {
+ RandomListNode next = cur.next;
+ cur.next = next.next;
+ cur = next;
+ }
+ return pCloneHead;
}
```
-# 36. 二叉搜索树与双向链表
+# 36. 二叉搜索树与双向链表
[NowCoder](https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&tqId=11179&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
-
+
-## 解题思路
+## 解题思路
```java
-private TreeNode pre = null;
-private TreeNode head = null;
+private TreeNode pre = null;
+private TreeNode head = null;
-public TreeNode Convert(TreeNode root) {
- if (root == null)
- return null;
- inOrder(root);
- return head;
+public TreeNode Convert(TreeNode root) {
+ if (root == null)
+ return null;
+ inOrder(root);
+ return head;
}
-private void inOrder(TreeNode node) {
- if (node == null)
- return;
- inOrder(node.left);
- node.left = pre;
- if (pre != null)
- pre.right = node;
- pre = node;
- if (head == null)
- head = node;
- inOrder(node.right);
+private void inOrder(TreeNode node) {
+ if (node == null)
+ return;
+ inOrder(node.left);
+ node.left = pre;
+ if (pre != null)
+ pre.right = node;
+ pre = node;
+ if (head == null)
+ head = node;
+ inOrder(node.right);
}
```
-# 37. 序列化二叉树
+# 37. 序列化二叉树
[NowCoder](https://www.nowcoder.com/practice/cf7e25aa97c04cc1a68c8f040e71fb84?tpId=13&tqId=11214&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
请实现两个函数,分别用来序列化和反序列化二叉树。
-## 解题思路
+## 解题思路
```java
-public class Solution {
+public class Solution {
- private String deserializeStr;
+ private String deserializeStr;
- public String Serialize(TreeNode root) {
- if (root == null)
- return "#";
- return root.val + " " + Serialize(root.left) + " " + Serialize(root.right);
- }
+ public String Serialize(TreeNode root) {
+ if (root == null)
+ return "#";
+ return root.val + " " + Serialize(root.left) + " " + Serialize(root.right);
+ }
- public TreeNode Deserialize(String str) {
- deserializeStr = str;
- return Deserialize();
- }
+ public TreeNode Deserialize(String str) {
+ deserializeStr = str;
+ return Deserialize();
+ }
- private TreeNode Deserialize() {
- if (deserializeStr.length() == 0)
- return null;
- int index = deserializeStr.indexOf(" ");
- String node = index == -1 ? deserializeStr : deserializeStr.substring(0, index);
- deserializeStr = index == -1 ? "" : deserializeStr.substring(index + 1);
- if (node.equals("#"))
- return null;
- int val = Integer.valueOf(node);
- TreeNode t = new TreeNode(val);
- t.left = Deserialize();
- t.right = Deserialize();
- return t;
- }
+ private TreeNode Deserialize() {
+ if (deserializeStr.length() == 0)
+ return null;
+ int index = deserializeStr.indexOf(" ");
+ String node = index == -1 ? deserializeStr : deserializeStr.substring(0, index);
+ deserializeStr = index == -1 ? "" : deserializeStr.substring(index + 1);
+ if (node.equals("#"))
+ return null;
+ int val = Integer.valueOf(node);
+ TreeNode t = new TreeNode(val);
+ t.left = Deserialize();
+ t.right = Deserialize();
+ return t;
+ }
}
```
-# 38. 字符串的排列
+# 38. 字符串的排列
[NowCoder](https://www.nowcoder.com/practice/fe6b651b66ae47d7acce78ffdd9a96c7?tpId=13&tqId=11180&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
-输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。
+输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。
-## 解题思路
+## 解题思路
```java
-private ArrayList ret = new ArrayList<>();
+private ArrayList ret = new ArrayList<>();
-public ArrayList Permutation(String str) {
- if (str.length() == 0)
- return ret;
- char[] chars = str.toCharArray();
- Arrays.sort(chars);
- backtracking(chars, new boolean[chars.length], new StringBuffer());
- return ret;
+public ArrayList Permutation(String str) {
+ if (str.length() == 0)
+ return ret;
+ char[] chars = str.toCharArray();
+ Arrays.sort(chars);
+ backtracking(chars, new boolean[chars.length], new StringBuffer());
+ return ret;
}
-private void backtracking(char[] chars, boolean[] hasUsed, StringBuffer s) {
- if (s.length() == chars.length) {
- ret.add(s.toString());
- return;
- }
- for (int i = 0; i < chars.length; i++) {
- if (hasUsed[i])
- continue;
- if (i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) // 保证不重复
- continue;
- hasUsed[i] = true;
- s.append(chars[i]);
- backtracking(chars, hasUsed, s);
- s.deleteCharAt(s.length() - 1);
- hasUsed[i] = false;
- }
+private void backtracking(char[] chars, boolean[] hasUsed, StringBuffer s) {
+ if (s.length() == chars.length) {
+ ret.add(s.toString());
+ return;
+ }
+ for (int i = 0; i < chars.length; i++) {
+ if (hasUsed[i])
+ continue;
+ if (i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) // 保证不重复
+ continue;
+ hasUsed[i] = true;
+ s.append(chars[i]);
+ backtracking(chars, hasUsed, s);
+ s.deleteCharAt(s.length() - 1);
+ hasUsed[i] = false;
+ }
}
```
-# 39. 数组中出现次数超过一半的数字
+# 39. 数组中出现次数超过一半的数字
[NowCoder](https://www.nowcoder.com/practice/e8a1b01a2df14cb2b228b30ee6a92163?tpId=13&tqId=11181&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 解题思路
+## 解题思路
-多数投票问题,可以利用 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。
+使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素不相等时,令 cnt--。如果前面查找了 i 个元素,且 cnt == 0 ,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找就能找出 majority。
```java
-public int MoreThanHalfNum_Solution(int[] nums) {
- int majority = nums[0];
- for (int i = 1, cnt = 1; i < nums.length; i++) {
- cnt = nums[i] == majority ? cnt + 1 : cnt - 1;
- if (cnt == 0) {
- majority = nums[i];
- cnt = 1;
- }
- }
- int cnt = 0;
- for (int val : nums)
- if (val == majority)
- cnt++;
- return cnt > nums.length / 2 ? majority : 0;
+public int MoreThanHalfNum_Solution(int[] nums) {
+ int majority = nums[0];
+ for (int i = 1, cnt = 1; i < nums.length; i++) {
+ cnt = nums[i] == majority ? cnt + 1 : cnt - 1;
+ if (cnt == 0) {
+ majority = nums[i];
+ cnt = 1;
+ }
+ }
+ int cnt = 0;
+ for (int val : nums)
+ if (val == majority)
+ cnt++;
+ return cnt > nums.length / 2 ? majority : 0;
}
```
-# 40. 最小的 K 个数
+# 40. 最小的 K 个数
[NowCoder](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 解题思路
+## 解题思路
-### 快速选择
+### 快速选择
-- 复杂度:O(N) + O(1)
-- 只有当允许修改数组元素时才可以使用
+- 复杂度:O(N) + O(1)
+- 只有当允许修改数组元素时才可以使用
-快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 K 个元素,这种找第 K 个元素的算法称为快速选择算法。
+快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 K 个元素,这种找第 K 个元素的算法称为快速选择算法。
```java
-public ArrayList GetLeastNumbers_Solution(int[] nums, int k) {
- ArrayList ret = new ArrayList<>();
- if (k > nums.length || k <= 0)
- return ret;
- int kthSmallest = findKthSmallest(nums, k - 1);
- // findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数
- for (int i = 0; i < k; i++)
- ret.add(nums[i]);
- return ret;
+public ArrayList GetLeastNumbers_Solution(int[] nums, int k) {
+ ArrayList ret = new ArrayList<>();
+ if (k > nums.length || k <= 0)
+ return ret;
+ int kthSmallest = findKthSmallest(nums, k - 1);
+ // findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数
+ for (int i = 0; i < k; i++)
+ ret.add(nums[i]);
+ return ret;
}
-public int findKthSmallest(int[] nums, int k) {
- int l = 0, h = nums.length - 1;
- while (l < h) {
- int j = partition(nums, l, h);
- if (j == k)
- break;
- if (j > k)
- h = j - 1;
- else
- l = j + 1;
- }
- return nums[k];
+public int findKthSmallest(int[] nums, int k) {
+ int l = 0, h = nums.length - 1;
+ while (l < h) {
+ int j = partition(nums, l, h);
+ if (j == k)
+ break;
+ if (j > k)
+ h = j - 1;
+ else
+ l = j + 1;
+ }
+ return nums[k];
}
-private int partition(int[] nums, int l, int h) {
- // 切分元素
- int parti = nums[l];
- int i = l, j = h + 1;
- while (true) {
- while (i != h && nums[++i] < parti) ;
- while (j != l && nums[--j] > parti) ;
- if (i >= j)
- break;
- swap(nums, i, j);
- }
- swap(nums, l, j);
- return j;
+private int partition(int[] nums, int l, int h) {
+ // 切分元素
+ int parti = nums[l];
+ int i = l, j = h + 1;
+ while (true) {
+ while (i != h && nums[++i] < parti) ;
+ while (j != l && nums[--j] > parti) ;
+ if (i >= j)
+ break;
+ swap(nums, i, j);
+ }
+ swap(nums, l, j);
+ return j;
}
-private void swap(int[] nums, int i, int j) {
- int t = nums[i];
- nums[i] = nums[j];
- nums[j] = t;
+private void swap(int[] nums, int i, int j) {
+ int t = nums[i];
+ nums[i] = nums[j];
+ nums[j] = t;
}
```
-### 大小为 K 的最小堆
+### 大小为 K 的最小堆
-- 复杂度:O(NlogK) + O(K)
-- 特别适合处理海量数据
+- 复杂度:O(NlogK) + O(K)
+- 特别适合处理海量数据
应该使用大顶堆来维护最小堆,而不能直接创建一个小顶堆并设置一个大小,企图让小顶堆中的元素都是最小元素。
-维护一个大小为 K 的最小堆过程如下:在添加一个元素之后,如果大顶堆的大小大于 K,那么需要将大顶堆的堆顶元素去除。
+维护一个大小为 K 的最小堆过程如下:在添加一个元素之后,如果大顶堆的大小大于 K,那么需要将大顶堆的堆顶元素去除。
```java
-public ArrayList GetLeastNumbers_Solution(int[] nums, int k) {
- if (k > nums.length || k <= 0)
- return new ArrayList<>();
- PriorityQueue maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
- for (int num : nums) {
- maxHeap.add(num);
- if (maxHeap.size() > k)
- maxHeap.poll();
- }
- ArrayList ret = new ArrayList<>(maxHeap);
- return ret;
+public ArrayList GetLeastNumbers_Solution(int[] nums, int k) {
+ if (k > nums.length || k <= 0)
+ return new ArrayList<>();
+ PriorityQueue maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
+ for (int num : nums) {
+ maxHeap.add(num);
+ if (maxHeap.size() > k)
+ maxHeap.poll();
+ }
+ ArrayList ret = new ArrayList<>(maxHeap);
+ return ret;
}
```
-# 41.1 数据流中的中位数
+# 41.1 数据流中的中位数
[NowCoder](https://www.nowcoder.com/practice/9be0172896bd43948f8a32fb954e1be1?tpId=13&tqId=11216&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
-## 题目描述
+## 题目描述
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
-## 解题思路
+## 解题思路
```java
-public class Solution {
- // 大顶堆,存储左半边元素
- private PriorityQueue left = new PriorityQueue<>((o1, o2) -> o2 - o1);
- // 小顶堆,存储右半边元素,并且右半边元素都大于左半边
- private PriorityQueue right = new PriorityQueue<>();
- // 当前数据流读入的元素个数
- private int N = 0;
+public class Solution {
+ // 大顶堆,存储左半边元素
+ private PriorityQueue