diff --git a/README.md b/README.md index a1b6f399..7ae8492f 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,7 @@ - - - - -![](https://img.shields.io/badge/update-today-blue.svg) ![](https://img.shields.io/badge/gitbook-making-lightgrey.svg) + +| Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ | Ⅺ | +| :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| :-------:| +|网络[:cloud:](#网络-cloud) |操作系统[:computer:](#操作系统-computer)| 算法[:pencil2:](#数据结构与算法-pencil2)| 面向对象[:couple:](#面向对象-couple) |数据库[:floppy_disk:](#数据库-floppy_disk)| Java [:coffee:](#java-coffee)| 分布式[:sweat_drops:](#分布式-sweat_drops)| 工具[:hammer:](#工具-hammer)| 编码实践[:speak_no_evil:](#编码实践-speak_no_evil)| 资料下载[:arrow_down:](#资料下载-arrow_down)| 后记[:memo:](#后记-memo) | ## 网络 :cloud: @@ -33,7 +27,7 @@ > [算法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/算法.md) -整理自《算法 第四版》,主要整理了面试常问的排序和查找算法。 +整理自《算法 第四版》 > [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md) @@ -41,7 +35,7 @@ > [Leetcode 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode%20题解.md) -对题目做了一个分类,并对每种题型的解题思路做了总结。已经整理了 300+ 的题目,基本涵盖所有经典题目。 +对题目做了一个分类,并对每种题型的解题思路做了总结。 ## 面向对象 :couple: @@ -51,7 +45,7 @@ > [面向对象思想](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/面向对象思想.md) -一些面向对象思想和原则。 +一些面向对象思想和设计原则。 ## 数据库 :floppy_disk: @@ -65,7 +59,7 @@ > [MySQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/MySQL.md) -整理自《高性能 MySQL》,整理了一些重点内容。 +整理自《高性能 MySQL》 > [Redis](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Redis.md) @@ -73,9 +67,9 @@ ## Java :coffee: -> [JVM](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/JVM.md) +> [Java 虚拟机](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20虚拟机.md) -整理自《深入理解 Java 虚拟机》,主要整理了内存模型、垃圾回收以及类加载机制。 +整理自《深入理解 Java 虚拟机》 > [Java 并发](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20并发.md) @@ -85,7 +79,7 @@ 容器的一些总结,包含容器源码的分析。 -> [Java IO](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20IO.md) +> [Java I/O](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20IO.md) File, InputStream OutputStream, Reader Writer, Serializable, Socket, NIO @@ -93,6 +87,24 @@ File, InputStream OutputStream, Reader Writer, Serializable, Socket, NIO 整理了一些常见考点。 +> [JDK 中的设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/JDK%20中的设计模式.md) + +对每种设计模式做了一个总结,并给出在 JDK 中的使用实例。 + +## 分布式 :sweat_drops: + +> [分布式基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式基础.md) + +整理自《大规模分布式存储系统》 + +> [一致性协议](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/一致性协议.md) + +两阶段提交、Paxos、Raft、拜占庭将军问题。 + +> [分布式问题分析](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式问题分析.md) + +分布式事务、复杂均衡算法与实现、分布式锁、分布式 Session、分库分表的分布式困境与应对之策。 + ## 工具 :hammer: > [Git](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Git.md) @@ -121,21 +133,9 @@ Google 开源项目的代码风格规范。 > [Download](https://github.com/CyC2018/Interview-Notebook/blob/master/other/download.md) -一些 PDF 书籍 - ## 后记 :memo: -网上有很多相关的资料,但是这些资料都比较零散。本仓库的笔记是从经典的书籍和材料中整理而来,在整理出重点的同时会尽可能保证知识的系统性,因此比较适合作为应对面试的学习资料。 - -笔记内容按照 [中文文案排版指北](http://mazhuang.org/wiki/chinese-copywriting-guidelines/#%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%9C%B0%E9%81%93%E7%9A%84%E7%BC%A9%E5%86%99) 进行排版,以保证内容的可读性。这里提供了本人实现的文档排版工具的下载:[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting)。 - -由于 Github 使用的 GFM 不支持 MathJax 公式,也不支持 TOC 标记,为了把本地的 Markdown 文档转换为 GFM 支持的格式,需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。并且为了让图片显示效果更好,笔记内容基本使用了 <center> 标记来让图片居中显示,但是 GFM 却不支持 <center> 标记,因此也需要进行一定的转换。这里提供了本人实现的 GFM 文档转换工具的下载:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。 - -因为大部分内容是一个字一个字打上去的,难免会有一些笔误,如果发现,可以直接在相应的文档上编辑修改。 - -如果觉得内容不够完善或者有写的不好的地方,您可以在 Issues 中发表反馈意见。 - -笔记内容可供个人随意使用,转载或者引用请注明出处,毕竟写了很长时间没那么轻松~ +> [Postface](https://github.com/CyC2018/Interview-Notebook/blob/master/other/postface.md) -* [基础概念](#基础概念) +* [一 、基础概念](#一-基础概念) * [Web 基础](#web-基础) * [URL](#url) * [请求和响应报文](#请求和响应报文) -* [HTTP 方法](#http-方法) - * [GET:获取资源](#get获取资源) - * [POST:传输实体主体](#post传输实体主体) - * [HEAD:获取报文首部](#head获取报文首部) - * [PUT:上传文件](#put上传文件) - * [PATCH:对资源进行部分修改](#patch对资源进行部分修改) - * [DELETE:删除文件](#delete删除文件) - * [OPTIONS:查询支持的方法](#options查询支持的方法) - * [CONNECT:要求用隧道协议连接代理](#connect要求用隧道协议连接代理) - * [TRACE:追踪路径](#trace追踪路径) -* [HTTP 状态码](#http-状态码) +* [二、HTTP 方法](#二http-方法) + * [GET](#get) + * [POST](#post) + * [HEAD](#head) + * [PUT](#put) + * [PATCH](#patch) + * [DELETE](#delete) + * [OPTIONS](#options) + * [CONNECT](#connect) + * [TRACE](#trace) +* [三、HTTP 状态码](#三http-状态码) * [2XX 成功](#2xx-成功) * [3XX 重定向](#3xx-重定向) * [4XX 客户端错误](#4xx-客户端错误) * [5XX 服务器错误](#5xx-服务器错误) -* [HTTP 首部](#http-首部) +* [四、HTTP 首部](#四http-首部) * [通用首部字段](#通用首部字段) * [请求首部字段](#请求首部字段) * [响应首部字段](#响应首部字段) * [实体首部字段](#实体首部字段) -* [具体应用](#具体应用) +* [五、具体应用](#五具体应用) * [Cookie](#cookie) * [缓存](#缓存) * [持久连接](#持久连接) @@ -34,18 +34,24 @@ * [内容协商](#内容协商) * [虚拟主机](#虚拟主机) * [通信数据转发](#通信数据转发) -* [HTTPs](#https) +* [六、HTTPs](#六https) * [加密](#加密) * [认证](#认证) * [完整性](#完整性) -* [各版本比较](#各版本比较) +* [七、Web 攻击技术](#七web-攻击技术) + * [攻击模式](#攻击模式) + * [跨站脚本攻击](#跨站脚本攻击) + * [SQL 注入攻击](#sql-注入攻击) + * [跨站点请求伪造](#跨站点请求伪造) + * [拒绝服务攻击](#拒绝服务攻击) +* [八、各版本比较](#八各版本比较) * [HTTP/1.0 与 HTTP/1.1 的区别](#http10-与-http11-的区别) * [HTTP/1.1 与 HTTP/2.0 的区别](#http11-与-http20-的区别) * [参考资料](#参考资料) -# 基础概念 +# 一 、基础概念 ## Web 基础 @@ -65,21 +71,27 @@ URI 包含 URL 和 URN,目前 WEB 只有 URL 比较流行,所以见到的基 ## 请求和响应报文 -**请求报文** +### 1. 请求报文

-**响应报文** +### 2. 响应报文

-# HTTP 方法 +# 二、HTTP 方法 客户端发送的 **请求报文** 第一行为请求行,包含了方法字段。 -## GET:获取资源 +## GET -## POST:传输实体主体 +> 获取资源 + +当前网络请求中,绝大部分使用的是 GET 方法。 + +## POST + +> 传输实体主体 POST 主要目的不是获取资源,而是传输存储在内容实体中的数据。 @@ -97,13 +109,19 @@ name1=value1&name2=value2 GET 的传参方式相比于 POST 安全性较差,因为 GET 传的参数在 URL 中是可见的,可能会泄露私密信息。并且 GET 只支持 ASCII 字符,如果参数为中文则可能会出现乱码,而 POST 支持标准字符集。 -## HEAD:获取报文首部 +GET 和 POST 的另一个区别是,使用 GET 方法,浏览器会把 HTTP Header 和 Data 一并发送出去,服务器响应 200(OK)并返回数据。而使用 POST 方法,浏览器先发送 Header,服务器响应 100(Continue)之后,浏览器再发送 Data,最后服务器响应 200(OK)并返回数据。 + +## HEAD + +> 获取报文首部 和 GET 方法一样,但是不返回报文实体主体部分。 主要用于确认 URL 的有效性以及资源更新的日期时间等。 -## PUT:上传文件 +## PUT + +> 上传文件 由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。 @@ -116,7 +134,9 @@ Content-length: 16

New File

``` -## PATCH:对资源进行部分修改 +## PATCH + +> 对资源进行部分修改 PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。 @@ -130,7 +150,9 @@ Content-Length: 100 [description of changes] ``` -## DELETE:删除文件 +## DELETE + +> 删除文件 与 PUT 功能相反,并且同样不带验证机制。 @@ -138,13 +160,17 @@ Content-Length: 100 DELETE /file.html HTTP/1.1 ``` -## OPTIONS:查询支持的方法 +## OPTIONS + +> 查询支持的方法 查询指定的 URL 能够支持的方法。 会返回 Allow: GET, POST, HEAD, OPTIONS 这样的内容。 -## CONNECT:要求用隧道协议连接代理 +## CONNECT + +> 要求用隧道协议连接代理 要求在于代理服务器通信时建立隧道,使用 SSL(Secure Sokets Layer,安全套接字)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。 @@ -154,7 +180,9 @@ CONNECT www.example.com:443 HTTP/1.1

-## TRACE:追踪路径 +## TRACE + +> 追踪路径 服务器会将通信路径返回给客户端。 @@ -164,12 +192,12 @@ CONNECT www.example.com:443 HTTP/1.1

-# HTTP 状态码 +# 三、HTTP 状态码 服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。 | 状态码 | 类别 | 原因短语 | -| --- | --- | --- | +| :---: | :---: | :---: | | 1XX | Informational(信息性状态码) | 接收的请求正在处理 | | 2XX | Success(成功状态码) | 请求正常处理完毕 | | 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 | @@ -216,7 +244,7 @@ CONNECT www.example.com:443 HTTP/1.1 - **503 Service Unavilable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 -# HTTP 首部 +# 四、HTTP 首部 有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。 @@ -225,9 +253,9 @@ CONNECT www.example.com:443 HTTP/1.1 ## 通用首部字段 | 首部字段名 | 说明 | -| -- | -- | +| :--: | :--: | | Cache-Control | 控制缓存的行为 | -| Connection | 控制不再转发给代理的首部字段;管理持久连接| +| Connection | 控制不再转发给代理的首部字段、管理持久连接| | Date | 创建报文的日期时间 | | Pragma | 报文指令 | | Trailer | 报文末端的首部一览 | @@ -239,7 +267,7 @@ CONNECT www.example.com:443 HTTP/1.1 ## 请求首部字段 | 首部字段名 | 说明 | -| -- | -- | +| :--: | :--: | | Accept | 用户代理可处理的媒体类型 | | Accept-Charset | 优先的字符集 | | Accept-Encoding | 优先的内容编码 | @@ -263,7 +291,7 @@ CONNECT www.example.com:443 HTTP/1.1 ## 响应首部字段 | 首部字段名 | 说明 | -| -- | -- | +| :--: | :--: | | Accept-Ranges | 是否接受字节范围请求 | | Age | 推算资源创建经过时间 | | ETag | 资源的匹配信息 | @@ -277,11 +305,11 @@ CONNECT www.example.com:443 HTTP/1.1 ## 实体首部字段 | 首部字段名 | 说明 | -| -- | -- | +| :--: | :--: | | Allow | 资源可支持的 HTTP 方法 | | Content-Encoding | 实体主体适用的编码方式 | | Content-Language | 实体主体的自然语言 | -| Content-Length | 实体主体的大小(单位:字节) | +| Content-Length | 实体主体的大小 | | Content-Location | 替代对应资源的 URI | | Content-MD5 | 实体主体的报文摘要 | | Content-Range | 实体主体的位置范围 | @@ -289,7 +317,7 @@ CONNECT www.example.com:443 HTTP/1.1 | Expires | 实体主体过期的日期时间 | | Last-Modified | 资源的最后修改日期时间 | -# 具体应用 +# 五、具体应用 ## Cookie @@ -297,7 +325,7 @@ HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使 Cookie 是服务器发送给客户端的数据,该数据会被保存在浏览器中,并且在下一次发送请求时包含该数据。通过 Cookie 可以让服务器知道两个请求是否来自于同一个客户端,从而实现保持登录状态等功能。 -**创建过程** +### 1. 创建过程 服务器发送的响应报文包含 Set-Cookie 字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。 @@ -318,10 +346,10 @@ Host: www.example.org Cookie: yummy_cookie=choco; tasty_cookie=strawberry ``` -**Set-Cookie** +### 2. Set-Cookie | 属性 | 说明 | -| -- | -- | +| :--: | -- | | NAME=VALUE | 赋予 Cookie 的名称和其值(必需项) | | expires=DATE | Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止) | | path=PATH | 将服务器上的文件目录作为 Cookie 的适用对象(若不指定则默认为文档所在的文件目录) | @@ -329,31 +357,31 @@ Cookie: yummy_cookie=choco; tasty_cookie=strawberry | Secure | 仅在 HTTPS 安全通信时才会发送 Cookie | | HttpOnly | 加以限制,使 Cookie 不能被 JavaScript 脚本访问 | -**Session 和 Cookie 区别** +### 3. Session 和 Cookie 区别 Session 是服务器用来跟踪用户的一种手段,每个 Session 都有一个唯一标识:Session ID。当服务器创建了一个 Session 时,给客户端发送的响应报文就包含了 Set-Cookie 字段,其中有一个名为 sid 的键值对,这个键值对就是 Session ID。客户端收到后就把 Cookie 保存在浏览器中,并且之后发送的请求报文都包含 Session ID。HTTP 就是通过 Session 和 Cookie 这两种方式一起合作来实现跟踪用户状态的,Session 用于服务器端,Cookie 用于客户端。 -**浏览器禁用 Cookie 的情况** +### 4. 浏览器禁用 Cookie 的情况 会使用 URL 重写技术,在 URL 后面加上 sid=xxx 。 -**使用 Cookie 实现用户名和密码的自动填写** +### 5. 使用 Cookie 实现用户名和密码的自动填写 网站脚本会自动从保存在浏览器中的 Cookie 读取用户名和密码,从而实现自动填写。 ## 缓存 -**优点** +### 1. 优点 1. 降低服务器的负担; 2. 提高响应速度(缓存资源比服务器上的资源离客户端更近)。 -**实现方法** +### 2. 实现方法 1. 让代理服务器进行缓存; 2. 让客户端浏览器进行缓存。 -**Cache-Control 字段** +### 3. Cache-Control 字段 HTTP 通过 Cache-Control 首部字段来控制缓存。 @@ -361,19 +389,19 @@ HTTP 通过 Cache-Control 首部字段来控制缓存。 Cache-Control: private, max-age=0, no-cache ``` -**no-cache 指令** +### 4. no-cache 指令 该指令出现在请求报文的 Cache-Control 字段中,表示缓存服务器需要先向原服务器验证缓存资源是否过期; 该指令出现在响应报文的 Cache-Control 字段中,表示缓存服务器在进行缓存之前需要先验证缓存资源的有效性。 -**no-store 指令** +### 5. no-store 指令 该指令表示缓存服务器不能对请求或响应的任何一部分进行缓存。 no-cache 不表示不缓存,而是缓存之前需要先进行验证,no-store 才是不进行缓存。 -**max-age 指令** +### 6. max-age 指令 该指令出现在请求报文的 Cache-Control 字段中,如果缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。 @@ -448,15 +476,13 @@ Content-Length: 1024 涉及以下首部字段:Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Language。 -

- ## 虚拟主机 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。 ## 通信数据转发 -**代理** +### 1. 代理 代理服务器接受客户端的请求,并且转发给其它服务器。 @@ -466,19 +492,19 @@ Content-Length: 1024

-**网关** +### 2. 网关 与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。

-**隧道** +### 3. 隧道 使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。

-# HTTPs +# 六、HTTPs HTTP 有以下安全性问题: @@ -514,7 +540,144 @@ HTTPs 采用 **混合的加密机制** ,使用公开密钥加密用于传输 SSL 提供摘要功能来验证完整性。 -# 各版本比较 +# 七、Web 攻击技术 + +Web 攻击的主要目标是使用 HTTP 协议的 Web 应用。 + +## 攻击模式 + +### 1. 主动攻击 + +直接攻击服务器,具有代表性的有 SQL 注入和 OS 命令注入。 + +### 2. 被动攻击 + +设下圈套,让用户发送有攻击代码的 HTTP 请求,那么用户发送了该 HTTP 请求之后就会泄露 Cookie 等个人信息,具有代表性的有跨站脚本攻击和跨站请求伪造。 + +## 跨站脚本攻击 + +### 1. 概念 + +(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包含 HTML 和 JavaScript。通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和 Cookie 等各种内容。 + +### 2. 危害 + +- 伪造虚假的输入表单骗取个人信息 +- 窃取用户的 Cookie 值 +- 显示伪造的文章或者图片 + +### 3. 防范手段 + +**(一)过滤特殊字符** + +许多语言都提供了对 HTML 的过滤: + +- PHP 的 htmlentities() 或是 htmlspecialchars()。 +- Python 的 cgi.escape()。 +- Java 的 xssprotect (Open Source Library)。 +- Node.js 的 node-validator。 + +**(二)指定 HTTP 的 Content-Type** + +通过这种方式,可以避免内容被当成 HTML 解析,比如 PHP 语言可以使用以下代码: + +```php + +``` + +## SQL 注入攻击 + +### 1. 概念 + +服务器上的数据库运行非法的 SQL 语句。 + +### 2. 攻击原理 + +例如一个网站登录验证的 SQL 查询代码为: + +```sql +strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');" +``` + +如果填入以下内容: + +```sql +userName = "1' OR '1'='1"; +passWord = "1' OR '1'='1"; +``` + +那么 SQL 查询字符串为: + +```sql +strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');" +``` + +此时无需验证通过就能执行以下查询: + +```sql +strSQL = "SELECT * FROM users;" +``` + +### 3. 危害 + +- 数据表中的数据外泄,例如个人机密数据,账户数据,密码等。 +- 数据结构被黑客探知,得以做进一步攻击(例如 SELECT * FROM sys.tables)。 +- 数据库服务器被攻击,系统管理员账户被窜改(例如 ALTER LOGIN sa WITH PASSWORD='xxxxxx')。 +- 获取系统较高权限后,有可能得以在网页加入恶意链接、恶意代码以及 XSS 等。 +- 经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统(例如 xp_cmdshell "net stop iisadmin" 可停止服务器的 IIS 服务)。 +- 破坏硬盘数据,瘫痪全系统(例如 xp_cmdshell "FORMAT C:")。 + +### 4. 防范手段 + +- 在设计应用程序时,完全使用参数化查询(Parameterized Query)来设计数据访问功能。 +- 在组合 SQL 字符串时,先针对所传入的参数作字符取代(将单引号字符取代为连续 2 个单引号字符)。 +- 如果使用 PHP 开发网页程序的话,亦可打开 PHP 的魔术引号(Magic quote)功能(自动将所有的网页传入参数,将单引号字符取代为连续 2 个单引号字符)。 +- 其他,使用其他更安全的方式连接 SQL 数据库。例如已修正过 SQL 注入问题的数据库连接组件,例如 ASP.NET 的 SqlDataSource 对象或是 LINQ to SQL。 +- 使用 SQL 防注入系统。 + +## 跨站点请求伪造 + +### 1. 概念 + +(Cross-site request forgery,XSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。 + +XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。 + +假如一家银行用以执行转账操作的 URL 地址如下:`http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName`。 + +
那么,一个恶意攻击者可以在另一个网站上放置如下代码:``。

+ +如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。 + +这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。 + +透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。 + +### 2. 防范手段 + +**(一)检查 Referer 字段** + +HTTP 头中有一个 Referer 字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。 + +这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。 + +**(二)添加校验 Token** + +由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 token 的值为空或者错误,拒绝这个可疑请求。 + +## 拒绝服务攻击 + +### 1. 概念 + +(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。 + +(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) + +# 八、各版本比较 ## HTTP/1.0 与 HTTP/1.1 的区别 @@ -524,23 +687,23 @@ HTTP/1.1 新增了以下内容: - 提供了范围请求功能; - 提供了虚拟主机的功能; - 多了一些缓存处理字段; -- 多了一些状态码; +- 多了一些状态码。 ## HTTP/1.1 与 HTTP/2.0 的区别 -**多路复用** +### 1. 多路复用 HTTP/2.0 使用多路复用技术,使用同一个 TCP 连接来处理多个请求。 -**首部压缩** +### 2. 首部压缩 HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 要求通讯双方各自缓存一份首部字段表,从而避免了重复传输。 -**服务端推送** +### 3. 服务端推送 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发给客户端。 -**二进制格式** +### 4. 二进制格式 HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。 @@ -548,3 +711,7 @@ HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。 - [图解 HTTP](https://pan.baidu.com/s/1M0AHXqG9sP9Bxne6u0JK8A) - [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP) +- [维基百科:跨站脚本](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) diff --git a/notes/JDK 中的设计模式.md b/notes/JDK 中的设计模式.md new file mode 100644 index 00000000..daaa90ae --- /dev/null +++ b/notes/JDK 中的设计模式.md @@ -0,0 +1,283 @@ + +* [一、创建型](#一创建型) + * [1. 单例模式](#1-单例模式) + * [2. 简单工厂模式](#2-简单工厂模式) + * [3. 工厂方法模式](#3-工厂方法模式) + * [4. 抽象工厂](#4-抽象工厂) + * [5. 生成器模式](#5-生成器模式) + * [6. 原型模式](#6-原型模式) +* [二、行为型](#二行为型) + * [1. 责任链](#1-责任链) + * [2. 命令模式](#2-命令模式) + * [3. 解释器模式](#3-解释器模式) + * [4. 迭代器](#4-迭代器) + * [5. 中间人模式](#5-中间人模式) + * [6. 备忘录模式](#6-备忘录模式) + * [7. 观察者模式](#7-观察者模式) + * [8. 策略模式](#8-策略模式) + * [9. 模板方法](#9-模板方法) + * [10. 访问者模式](#10-访问者模式) + * [11. 空对象模式](#11-空对象模式) +* [三、结构型](#三结构型) + * [1. 适配器](#1-适配器) + * [2. 桥接模式](#2-桥接模式) + * [3. 组合模式](#3-组合模式) + * [4. 装饰者模式](#4-装饰者模式) + * [5. 蝇量模式](#5-蝇量模式) + * [6. 动态代理](#6-动态代理) +* [参考资料](#参考资料) + + + +# 一、创建型 + +## 1. 单例模式 + +确保只实例化一个对象,并提供一个对象的全局访问点。 + +```java +java.lang.Runtime#getRuntime() +java.awt.Toolkit#getDefaultToolkit() +java.awt.GraphicsEnvironment#getLocalGraphicsEnvironment() +java.awt.Desktop#getDesktop() +``` + +## 2. 简单工厂模式 + +在不对用户暴露对象内部逻辑的前提下创建对象;使用通用的接口来创建对象; + +## 3. 工厂方法模式 + +定义创建对象的接口,但是让子类来决定应该使用哪个类来创建;使用通用的接口来创建对象; + +```java +java.lang.Proxy#newProxyInstance() +java.lang.Object#toString() +java.lang.Class#newInstance() +java.lang.reflect.Array#newInstance() +java.lang.reflect.Constructor#newInstance() +java.lang.Boolean#valueOf(String) +java.lang.Class#forName() +``` + +## 4. 抽象工厂 + +提供一个创建相关对象家族的接口,而没有明确指明它们的类。 + +```java +java.util.Calendar#getInstance() +java.util.Arrays#asList() +java.util.ResourceBundle#getBundle() +java.sql.DriverManager#getConnection() +java.sql.Connection#createStatement() +java.sql.Statement#executeQuery() +java.text.NumberFormat#getInstance() +javax.xml.transform.TransformerFactory#newInstance() +``` + +## 5. 生成器模式 + +定义一个新的类来构造另一个类的实例,以创建一个复杂的对象。 + +它可以封装一个对象的构造过程,并允许按步骤构造。 + +```java +java.lang.StringBuilder#append() +java.lang.StringBuffer#append() +java.sql.PreparedStatement +javax.swing.GroupLayout.Group#addComponent() +``` + +## 6. 原型模式 + +使用原型实例指定要创建对象的类型;通过复制这个原型来创建新对象。 + +```java +java.lang.Object#clone() +java.lang.Cloneable +``` + +# 二、行为型 + +## 1. 责任链 + +避免将请求的发送者附加到其接受者,从而使其它对象也可以处理请求;将请求以对象的方式把发送到链上直到请求被处理完毕。 + +```java +java.util.logging.Logger#log() +javax.servlet.Filter#doFilter() +``` + +## 2. 命令模式 + +将命令封装进对象中;允许使用命令对象对客户对象进行参数化;允许将命令对象存放到队列中。 + +```java +java.lang.Runnable +javax.swing.Action +``` + +## 3. 解释器模式 + +为语言创建解释器,通常由语言的语法和语法分析来定义。 + +```java +java.util.Pattern +java.text.Normalizer +java.text.Format +``` + +## 4. 迭代器 + +提供一种一致的访问聚合对象元素的方法,并且不暴露聚合对象的内部表示。 + +```java +java.util.Iterator +java.util.Enumeration +``` + +## 5. 中间人模式 + +使用中间人对象来封装对象之间的交互。中间人模式可以让降低交互对象之间的耦合程度。 + +```java +java.util.Timer +java.util.concurrent.Executor#execute() +java.util.concurrent.ExecutorService#submit() +java.lang.reflect.Method#invoke() +``` + +## 6. 备忘录模式 + +在不违反封装的情况下获得对象的内部状态,从而在需要时可以将对象恢复到最初状态。 + +```java +java.util.Date +java.io.Serializable +``` + +## 7. 观察者模式 + +定义对象之间的一对多依赖,当一个对象状态改变时,它的所有依赖都会收到通知并且自动更新状态。 + +```java +java.util.EventListener +javax.servlet.http.HttpSessionBindingListener +javax.servlet.http.HttpSessionAttributeListener +javax.faces.event.PhaseListener +``` + +## 8. 策略模式 + +定义一系列算法,封装每个算法,并使它们可以互换。策略可以让算法独立于使用它的客户端。 + +```java +java.util.Comparator#compare() +javax.servlet.http.HttpServlet +javax.servlet.Filter#doFilter() +``` + +## 9. 模板方法 + +定义算法框架,并将一些步骤的实现延迟到子类。通过模板方法,子类可以重新定义算法的某些步骤,而不用改变算法的结构。 + +```java +java.util.Collections#sort() +java.io.InputStream#skip() +java.io.InputStream#read() +java.util.AbstractList#indexOf() +``` + +## 10. 访问者模式 + +提供便捷的维护方式来操作一组对象。它使你在不改变操作对象的前提下,可以修改或扩展对象的行为。 + +例如集合,它可以包含不同类型的元素,访问者模式允许在不知道具体元素类型的前提下对集合元素进行一些操作。 + +```java +javax.lang.model.element.Element and javax.lang.model.element.ElementVisitor +javax.lang.model.type.TypeMirror and javax.lang.model.type.TypeVisitor +``` + +## 11. 空对象模式 + +使用什么都不做的空对象来替代 NULL。 + +# 三、结构型 + +## 1. 适配器 + +把一个类接口转换成另一个用户需要的接口。 + +```java +java.util.Arrays#asList() +javax.swing.JTable(TableModel) +java.io.InputStreamReader(InputStream) +java.io.OutputStreamWriter(OutputStream) +javax.xml.bind.annotation.adapters.XmlAdapter#marshal() +javax.xml.bind.annotation.adapters.XmlAdapter#unmarshal() +``` + +## 2. 桥接模式 + +将抽象与实现分离开来,使它们可以独立变化。 + +```java +AWT (It provides an abstraction layer which maps onto the native OS the windowing support.) +JDBC +``` + +## 3. 组合模式 + +将对象组合成树形结构来表示整理-部分层次关系,允许用户以相同的方式处理单独对象和组合对象。 + +```java +javax.swing.JComponent#add(Component) +java.awt.Container#add(Component) +java.util.Map#putAll(Map) +java.util.List#addAll(Collection) +java.util.Set#addAll(Collection) +``` + +## 4. 装饰者模式 + +为对象动态添加功能。 + +```java +java.io.BufferedInputStream(InputStream) +java.io.DataInputStream(InputStream) +java.io.BufferedOutputStream(OutputStream) +java.util.zip.ZipOutputStream(OutputStream) +java.util.Collections#checked[List|Map|Set|SortedSet|SortedMap]() +``` + +## 5. 蝇量模式 + +利用共享的方式来支持大量的对象,这些对象一部分内部状态时相同的,而另一份状态可以变化。 + +Java 利用缓存来加速大量小对象的访问时间。 + +```java +java.lang.Integer#valueOf(int) +java.lang.Boolean#valueOf(boolean) +java.lang.Byte#valueOf(byte) +java.lang.Character#valueOf(char) +``` + +## 6. 动态代理 + +提供一个占位符来控制对象的访问。 + +代理可以是一些轻量级的对象,它控制者对重量级对象的访问,只有在真正实例化这些重量级对象时才会去实例化它。 + +```java +java.lang.reflect.Proxy +RMI +``` + +# 参考资料 + +- [The breakdown of design patterns in JDK](http://www.programering.com/a/MTNxAzMwATY.html) +- [Design Patterns](http://www.oodesign.com/) + + diff --git a/notes/Java IO.md b/notes/Java IO.md index 4bfb4d46..6e91761b 100644 --- a/notes/Java IO.md +++ b/notes/Java IO.md @@ -1,40 +1,29 @@ -* [概览](#概览) -* [磁盘操作](#磁盘操作) -* [字节操作](#字节操作) -* [字符操作](#字符操作) -* [对象操作](#对象操作) -* [网络操作](#网络操作) - * [1. InetAddress](#1-inetaddress) - * [2. URL](#2-url) - * [3. Sockets](#3-sockets) - * [4. Datagram](#4-datagram) -* [NIO](#nio) - * [1. 流与块](#1-流与块) - * [2. 通道与缓冲区](#2-通道与缓冲区) - * [2.1 通道](#21-通道) - * [2.2 缓冲区](#22-缓冲区) - * [3. 缓冲区状态变量](#3-缓冲区状态变量) - * [4. 文件 NIO 实例](#4-文件-nio-实例) - * [5. 阻塞与非阻塞](#5-阻塞与非阻塞) - * [5.1 阻塞式 I/O](#51-阻塞式-io) - * [5.2 非阻塞式 I/O](#52-非阻塞式-io) - * [6. 套接字 NIO 实例](#6-套接字-nio-实例) - * [6.1 ServerSocketChannel](#61-serversocketchannel) - * [6.2 Selectors](#62-selectors) - * [6.3 主循环](#63-主循环) - * [6.4 监听新连接](#64-监听新连接) - * [6.5 接受新的连接](#65-接受新的连接) - * [6.6 删除处理过的 SelectionKey](#66-删除处理过的-selectionkey) - * [6.7 传入的 I/O](#67-传入的-io) - * [7. 内存映射文件](#7-内存映射文件) -* [参考资料](#参考资料) +* [一、概览](#一概览) +* [二、磁盘操作](#二磁盘操作) +* [三、字节操作](#三字节操作) +* [四、字符操作](#四字符操作) +* [五、对象操作](#五对象操作) +* [六、网络操作](#六网络操作) + * [InetAddress](#inetaddress) + * [URL](#url) + * [Sockets](#sockets) + * [Datagram](#datagram) +* [七、NIO](#七nio) + * [流与块](#流与块) + * [通道与缓冲区](#通道与缓冲区) + * [缓冲区状态变量](#缓冲区状态变量) + * [文件 NIO 实例](#文件-nio-实例) + * [套接字 NIO 实例](#套接字-nio-实例) + * [内存映射文件](#内存映射文件) + * [对比](#对比) +* [八、参考资料](#八参考资料) -# 概览 +# 一、概览 -Java 的 I/O 大概可以分成以下几类 +Java 的 I/O 大概可以分成以下几类: 1. 磁盘操作:File 2. 字节操作:InputStream 和 OutputStream @@ -43,13 +32,13 @@ Java 的 I/O 大概可以分成以下几类 5. 网络操作:Socket 6. 新的输入/输出:NIO -# 磁盘操作 +# 二、磁盘操作 File 类可以用于表示文件和目录,但是它只用于表示文件的信息,而不表示文件的内容。 -# 字节操作 +# 三、字节操作 -

+

Java I/O 使用了装饰者模式来实现。以 InputStream 为例,InputStream 是抽象组件,FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 @@ -72,7 +61,7 @@ while((bytes = in.read(buf, 0 , buf.length)) != -1) { } ``` -# 字符操作 +# 四、字符操作 不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。 @@ -89,7 +78,7 @@ GBK 编码中,中文占 2 个字节,英文占 1 个字节;UTF-8 编码中 如果编码和解码过程使用不同的编码方式那么就出现了乱码。 -# 对象操作 +# 五、对象操作 序列化就是将一个对象转换成字节序列,方便存储和传输。 @@ -103,11 +92,11 @@ transient 关键字可以使一些属性不会被序列化。 **ArrayList 序列化和反序列化的实现** :ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。 -``` +```java private transient Object[] elementData; ``` -# 网络操作 +# 六、网络操作 Java 中的网络支持: @@ -116,7 +105,7 @@ Java 中的网络支持: 3. Sockets:使用 TCP 协议实现网络通信; 4. Datagram:使用 UDP 协议实现网络通信。 -## 1. InetAddress +## InetAddress 没有公有构造函数,只能通过静态方法来创建实例。 @@ -125,7 +114,7 @@ InetAddress.getByName(String host); InetAddress.getByAddress(byte[] addr); ``` -## 2. URL +## URL 可以直接从 URL 中读取字节流数据 @@ -144,24 +133,24 @@ isr.close(); is.close(); ``` -## 3. Sockets +## Sockets - ServerSocket:服务器端类 - Socket:客户端类 - 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。 -

+

-## 4. Datagram +## Datagram - DatagramPacket:数据包类 - DatagramSocket:通信类 -# NIO +# 七、NIO 新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。 -## 1. 流与块 +## 流与块 I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。 @@ -169,11 +158,11 @@ I/O 与 NIO 最重要的区别是数据打包和传输的方式,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.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。 -## 2. 通道与缓冲区 +## 通道与缓冲区 -### 2.1 通道 +### 1. 通道 通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。 @@ -186,7 +175,7 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重 - SocketChannel:通过 TCP 读写网络中数据; - ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 -### 2.2 缓冲区 +### 2. 缓冲区 发送给一个通道的所有对象都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。 @@ -202,45 +191,50 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重 - FloatBuffer - DoubleBuffer -## 3. 缓冲区状态变量 +## 缓冲区状态变量 - capacity:最大容量; - position:当前已经读写的字节数; - limit:还可以读写的字节数。 -状态变量的改变过程: +状态变量的改变过程举例: + +① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 9。capacity 变量不会改变,下面的讨论会忽略它。 -1. 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 9。capacity 变量不会改变,下面的讨论会忽略它。

-2. 从输入通道中读取 3 个字节数据写入缓冲区中,此时 position 移动设为 3,limit 保持不变。 +② 从输入通道中读取 3 个字节数据写入缓冲区中,此时 position 移动设为 3,limit 保持不变。 +

-3. 以下图例为已经从输入通道读取了 5 个字节数据写入缓冲区中。在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。 +③ 以下图例为已经从输入通道读取了 5 个字节数据写入缓冲区中。在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。 +

-4. 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。 +④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。 +

-5. 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。 +⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。 +

-## 4. 文件 NIO 实例 +## 文件 NIO 实例 -1\. 为要读取的文件创建 FileInputStream,之后通过 FileInputStream 获取输入 FileChannel; +① 为要读取的文件创建 FileInputStream,之后通过 FileInputStream 获取输入 FileChannel; ```java FileInputStream fin = new FileInputStream("readandshow.txt"); FileChannel fic = fin.getChannel(); ``` -2\. 创建一个容量为 1024 的 Buffer; +② 创建一个容量为 1024 的 Buffer; ```java ByteBuffer buffer = ByteBuffer.allocate(1024); ``` -3\. 将数据从输入 FileChannel 写入到 Buffer 中,如果没有数据的话, read() 方法会返回 -1; +③ 将数据从输入 FileChannel 写入到 Buffer 中,如果没有数据的话,read() 方法会返回 -1; ```java int r = fcin.read(buffer); @@ -249,56 +243,34 @@ if (r == -1) { } ``` -4\. 为要写入的文件创建 FileOutputStream,之后通过 FileOutputStream 获取输出 FileChannel +④ 为要写入的文件创建 FileOutputStream,之后通过 FileOutputStream 获取输出 FileChannel ```java FileOutputStream fout = new FileOutputStream("writesomebytes.txt"); FileChannel foc = fout.getChannel(); ``` -5\. 调用 flip() 切换读写 +⑤ 调用 flip() 切换读写 ```java buffer.flip(); ``` -6\. 把 Buffer 中的数据读取到输出 FileChannel 中 +⑥ 把 Buffer 中的数据读取到输出 FileChannel 中 ```java foc.write(buffer); ``` -7\. 最后调用 clear() 重置缓冲区 +⑦ 最后调用 clear() 重置缓冲区 ```java buffer.clear(); ``` -## 5. 阻塞与非阻塞 +## 套接字 NIO 实例 -应当注意,FileChannel 不能切换到非阻塞模式,套接字 Channel 可以。 - -### 5.1 阻塞式 I/O - -阻塞式 I/O 在调用 InputStream.read() 方法时会一直等到数据到来时(或超时)才会返回,在调用 ServerSocket.accept() 方法时,也会一直阻塞到有客户端连接才会返回。 - -服务端都会为每个连接的客户端创建一个线程来处理读写请求,阻塞式的特点会造成服务器会创建大量线程,并且大部分线程处于阻塞的状态,因此对服务器的性能会有很大的影响。 - -

- -### 5.2 非阻塞式 I/O - -由一个专门的线程来处理所有的 I/O 事件,并负责分发。 - -事件驱动机制:事件到的时候触发,而不是同步地监视事件。 - -线程通信:线程之间通过 wait()、notify() 等方式通信,保证每次上下文切换都是有意义的,减少无谓的线程切换。 - -

- -## 6. 套接字 NIO 实例 - -### 6.1 ServerSocketChannel +### 1. ServerSocketChannel 每一个监听端口都需要有一个 ServerSocketChannel 用来监听连接。 @@ -311,7 +283,7 @@ InetSocketAddress address = new InetSocketAddress(ports[i]); ss.bind(address); // 绑定端口号 ``` -### 6.2 Selectors +### 2. Selectors 异步 I/O 通过 Selector 注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字连接等等,在发生这样的事件时,系统将会发送通知。 @@ -324,7 +296,7 @@ Selector selector = Selector.open(); SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT); ``` -### 6.3 主循环 +### 3. 主循环 首先,我们调用 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时,select() 方法将返回所发生的事件的数量。 @@ -344,7 +316,7 @@ while (it.hasNext()) { } ``` -### 6.4 监听新连接 +### 4. 监听新连接 程序执行到这里,我们仅注册了 ServerSocketChannel,并且仅注册它们“接收”事件。为确认这一点,我们对 SelectionKey 调用 readyOps() 方法,并检查发生了什么类型的事件: @@ -358,7 +330,7 @@ if ((key.readyOps() & SelectionKey.OP_ACCEPT) 可以肯定地说,readOps() 方法告诉我们该事件是新的连接。 -### 6.5 接受新的连接 +### 5. 接受新的连接 因为我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;也就是说,不用担心 accept() 操作会阻塞: @@ -376,7 +348,7 @@ SelectionKey newKey = sc.register(selector, SelectionKey.OP_READ); 注意我们使用 register() 的 OP_READ 参数,将 SocketChannel 注册用于读取而不是接受新连接。 -### 6.6 删除处理过的 SelectionKey +### 6. 删除处理过的 SelectionKey 在处理 SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的 remove() 方法来删除处理过的 SelectionKey: @@ -386,7 +358,7 @@ it.remove(); 现在我们可以返回主循环并接受从一个套接字中传入的数据 (或者一个传入的 I/O 事件) 了。 -### 6.7 传入的 I/O +### 7. 传入的 I/O 当来自一个套接字的数据到达时,它会触发一个 I/O 事件。这会导致在主循环中调用 Selector.select(),并返回一个或者多个 I/O 事件。这一次, SelectionKey 将被标记为 OP_READ 事件,如下所示: @@ -399,25 +371,34 @@ it.remove(); } ``` -## 7. 内存映射文件 +## 内存映射文件 内存映射文件 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); ``` -# 参考资料 +## 对比 + +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) diff --git a/notes/Java 基础.md b/notes/Java 基础.md index 33938e9a..f2cc4eb6 100644 --- a/notes/Java 基础.md +++ b/notes/Java 基础.md @@ -1,74 +1,89 @@ -* [关键字](#关键字) - * [1. final](#1-final) - * [2. static](#2-static) -* [Object 通用方法](#object-通用方法) - * [1. 概览](#1-概览) - * [2. clone()](#2-clone) - * [3. equals()](#3-equals) -* [继承](#继承) - * [1. 访问权限](#1-访问权限) - * [2. 抽象类与接口](#2-抽象类与接口) - * [3. super](#3-super) - * [4. 重载与重写](#4-重载与重写) -* [String](#string) - * [1. String, StringBuffer and StringBuilder](#1-string,-stringbuffer-and-stringbuilder) - * [2. String 不可变的原因](#2-string-不可变的原因) - * [3. String.intern()](#3-stringintern) -* [基本类型与运算](#基本类型与运算) - * [1. 包装类型](#1-包装类型) - * [2. switch](#2-switch) -* [反射](#反射) -* [异常](#异常) -* [泛型](#泛型) -* [注解](#注解) -* [特性](#特性) - * [1. 三大特性](#1-三大特性) - * [2. Java 各版本的新特性](#2-java-各版本的新特性) - * [3. Java 与 C++ 的区别](#3-java-与-c++-的区别) - * [4. JRE or JDK](#4-jre-or-jdk) +* [一、关键字](#一关键字) + * [final](#final) + * [static](#static) +* [二、Object 通用方法](#二object-通用方法) + * [概览](#概览) + * [clone()](#clone) + * [equals()](#equals) +* [四、继承](#四继承) + * [访问权限](#访问权限) + * [抽象类与接口](#抽象类与接口) + * [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) +* [参考资料](#参考资料) -# 关键字 +# 一、关键字 -## 1. final +## final -**数据** +**1. 数据** 声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。 - 对于基本类型,final 使数值不变; - 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。 -**方法** +```java +final int x = 1; +x = 2; // cannot assign value to final variable 'x' +final A y = new A(); +y.a = 1; +``` + +**2. 方法**
声明方法不能被子类覆盖。 private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是覆盖基类方法,而是重载了。 -**类** +**3. 类** 声明类不允许被继承。 -## 2. static +## static -**静态变量** +**1. 静态变量** 静态变量在内存中只存在一份,只在类第一次实例化时初始化一次。 -- 静态变量: 类所有的实例都共享静态变量,可以直接通过类名来访问它; +- 静态变量:类所有的实例都共享静态变量,可以直接通过类名来访问它; - 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。 -**静态方法** +```java +public class A { + private int x; // 实例变量 + public static int y; // 静态变量 +} +``` + +**2. 静态方法** 静态方法在类加载的时候就存在了,它不依赖于任何实例,所以 static 方法必须实现,也就是说它不能是抽象方法(abstract)。 -**静态语句块** +**3. 静态语句块** 静态语句块和静态变量一样在类第一次实例化时运行一次。 -**初始化顺序** +**4. 初始化顺序** 静态数据优先于其它数据的初始化,静态变量和静态语句块哪个先运行取决于它们在代码中的顺序。 @@ -111,9 +126,9 @@ public InitialOrderTest() { 5. 子类(实例变量、普通语句块) 6. 子类(构造函数) -# Object 通用方法 +# 二、Object 通用方法 -## 1. 概览 +## 概览 ```java public final native Class getClass() @@ -139,49 +154,101 @@ public final void wait() throws InterruptedException protected void finalize() throws Throwable {} ``` -## 2. clone() +## clone() -**浅拷贝** +**1. cloneable** -引用类型引用同一个对象。clone() 方法默认就是浅拷贝实现。 +clone() 是 Object 的受保护方法,这意味着,如果一个类不显式去重载 clone() 就没有这个方法。 -

+```java +public class CloneTest { + private int a; + private int b; +} +``` -**深拷贝** +```java +CloneTest x = new CloneTest(); +CloneTest y = x.clone(); // 'clone()' has protected access in 'java.lang.Object' +``` -可以使用序列化实现。 +接下来重载 Object 的 clone() 得到以下实现: -

+```java +public class CloneTest{ + private int a; + private int b; + + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} +``` + +```java +CloneTest x = new CloneTest(); +try { + CloneTest y = (CloneTest) x.clone(); +} catch (CloneNotSupportedException e) { + e.printStackTrace(); +} +``` + +```html +java.lang.CloneNotSupportedException: CloneTest +``` + +以上抛出了 CloneNotSupportedException,这是因为 CloneTest 没有实现 Cloneable 接口。应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protect 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。 + +**2. 深拷贝与浅拷贝** + +

+ +- 浅拷贝:拷贝对象和原对象的引用类型引用同一个对象; +- 深拷贝:引用不同对象。 + +实现深拷贝的方法: + +- [Defensive copying](http://www.javapractices.com/topic/TopicAction.do?Id=15) +- [copy constructors](http://www.javapractices.com/topic/TopicAction.do?Id=12) +- [static factory methods](http://www.javapractices.com/topic/TopicAction.do?Id=21). > [How do I copy an object in Java?](https://stackoverflow.com/questions/869033/how-do-i-copy-an-object-in-java) -## 3. equals() +## equals() -**== 与 equals() 区别** +**1. == 与 equals() 区别** -- 对于基本类型,== 就是判断两个值是否相等; +- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 +- 对于引用类型,== 判断两个引用是否引用同一个对象,而 equals() 判断引用的对象是否等价。 -- 对于引用类型,== 是判断两个引用是否引用同一个对象,而 equals() 是判断引用的对象是否等价。 +**2. 等价性** -**等价性** +> [散列](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Java%20%E5%AE%B9%E5%99%A8.md#%E4%B8%89%E6%95%A3%E5%88%977) -[散列](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Java%20%E5%AE%B9%E5%99%A8.md#%E6%95%A3%E5%88%97) +# 四、继承 -# 继承 - -## 1. 访问权限 +## 访问权限 Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。 -可以对类或类中的成员(字段以及方法)加上访问修饰符。成员可见表示其它类可以用成员所在类的对象访问到该成员;类可见表示其它类可以用这个类创建对象。在理解类的可见性时,可以把类当做包中的一个成员,然后包表示一个类,那么就可以类比成员的可见性。 +可以对类或类中的成员(字段以及方法)加上访问修饰符。 + +- 成员可见表示其它类可以用成员所在类的对象访问到该成员; +- 类可见表示其它类可以用这个类创建对象。 + +在理解类的可见性时,可以把类当做包中的一个成员,然后包表示一个类,那么就可以类比成员的可见性。 protected 用于修饰成员,表示在继承体系中成员对于子类可见。但是这个访问修饰符对于类没有意义,因为包没有继承体系。 -更详细的内容:[浅析 Java 中的访问权限控制](http://www.importnew.com/18097.html) +> [浅析 Java 中的访问权限控制](http://www.importnew.com/18097.html) -## 2. 抽象类与接口 +## 抽象类与接口 -抽象类和抽象方法都使用 abstract 进行声明。抽象类一般会包含抽象方法,但是少数情况下可以不包含,例如 HttpServlet 类,但是抽象方法一定位于抽象类中。抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。 +**1. 抽象类** + +抽象类和抽象方法都使用 abstract 进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。 ```java public abstract class GenericServlet implements Servlet, ServletConfig, Serializable { @@ -195,7 +262,11 @@ public abstract class GenericServlet implements Servlet, ServletConfig, Serializ } ``` -接口定义了一组方法,但是接口都没有方法的实现,可以理解为这些方法都是抽象方法。 +> [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/) + +**2. 接口** + +接口是抽象类的延伸。Java 为了安全性而不支持多重继承,一个类只能有一个父类。但是接口不同,一个类可以同时实现多个接口,不管这些接口之间有没有关系,所以接口弥补不支持多重继承的缺陷。 ```java public interface Externalizable extends Serializable { @@ -206,25 +277,41 @@ public interface Externalizable extends Serializable { } ``` -| **参数** | **抽象类** | **接口** | -| --- | --- | --- | -| 默认的方法实现 | 它可以有默认的方法实现 | 接口完全是抽象的。它根本不存在方法的实现 | -| 实现 | 子类使用 extends 关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字 implements 来实现接口。它需要提供接口中所有声明的方法的实现 | -| 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | -| 与正常 Java 类的区别 | 除了你不能实例化抽象类之外,它和普通 Java 类没有任何区别 | 接口是完全不同的类型 | -| 访问修饰符 | 抽象方法可以有 public、protected 和 default 这些修饰符 | 接口方法默认修饰符是 **public** 。你不可以使用其它修饰符。 | -| main 方法 | 抽象方法可以有 main 方法并且我们可以运行它 | 接口没有 main 方法,因此我们不能运行它。 | -| 多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口只可以继承一个或多个其它接口 | -| 速度 | 它比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。 | -| 添加新方法 | 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 | 如果你往接口中添加方法,那么你必须改变实现该接口的类。 | +从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。 -> [Java 抽象类与接口的区别](http://www.importnew.com/12399.html) +```java +public interface InterfaceDefaultTest { + default void func() { + System.out.println("default method in interface!"); + } +} +``` -## 3. super +**3. 比较** -**访问父类的成员** +- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求子类和父类具有 IS-A 关系; +- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。 -如果子类覆盖了父类的中某个方法的实现,那么就可以通过使用 super 关键字来引用父类的方法实现。 +**4. 使用选择** + +使用抽象类: + +- 需要在几个相关的类中共享代码; +- 需要能控制继承来的方法和字段的访问权限,而不是都为 public。 +- 需要继承非静态(non-static)和非常量(non-final)字段。 + +使用接口: + +- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法; +- 需要使用多重继承。 + +> [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface) + +## super + +**1. 访问父类的成员** + +如果子类覆盖了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。 ```java public class Superclass { @@ -241,6 +328,7 @@ public class Subclass extends Superclass { super.printMethod(); System.out.println("Printed in Subclass"); } + public static void main(String[] args) { Subclass s = new Subclass(); s.printMethod(); @@ -248,7 +336,7 @@ public class Subclass extends Superclass { } ``` -**访问父类的构造函数** +**2. 访问父类的构造函数** 可以使用 super() 函数访问父类的构造函数,从而完成一些初始化的工作。 @@ -261,63 +349,64 @@ public MountainBike(int startHeight, int startCadence, int startSpeed, int start > [Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html) -## 4. 重载与重写 +## 重载与重写 - 重写存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法; - 重载即存在于继承体系中,也存在于同一个类中,指一个方法与已经存在的方法或者父类的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。 -# String +# 五、String -## 1. String, StringBuffer and StringBuilder +## String, StringBuffer and StringBuilder -**是否可变** +**1. 是否可变** -String 不可变,StringBuffer 和 StringBuilder 可变。 +- String 不可变 +- StringBuffer 和 StringBuilder 可变 -**是否线程安全** +**2. 是否线程安全** -String 不可变,因此是线程安全的。 - -StringBuilder 不是线程安全的;StringBuffer 是线程安全的,使用 synchronized 来同步。 +- String 不可变,因此是线程安全的 +- StringBuilder 不是线程安全的 +- StringBuffer 是线程安全的,内部使用 synchronized 来同步 > [String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder) -## 2. String 不可变的原因 +## String 不可变的原因 -**可以缓存 hash 值** +**1. 可以缓存 hash 值** -因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 等。不可变的特性可以使得 hash 值也不可变,因此就只需要进行一次计算。 +因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 等情况。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。 -**String Pool 的需要** +**2. String Pool 的需要** -如果 String 已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。 +如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

-**安全性** +**3. 安全性** String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。 -**线程安全** +**4. 线程安全** String 不可变性天生具备线程安全,可以在多个线程中使用。 > [Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/) -## 3. String.intern() +## String.intern() 使用 String.intern() 可以保证所有相同内容的字符串变量引用相同的内存对象。 -更详细的内容:[揭开 String.intern() 那神秘的面纱](https://www.jianshu.com/p/95f516cb75ef) +> [揭开 String.intern() 那神秘的面纱](https://www.jianshu.com/p/95f516cb75ef) -# 基本类型与运算 +# 六、基本类型与运算 -## 1. 包装类型 +## 包装类型 -八个基本类型: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; // 装箱 @@ -330,12 +419,12 @@ new Integer(123) 与 Integer.valueOf(123) 的区别在于,Integer.valueOf(123) public static void main(String[] args) { Integer a = new Integer(1); Integer b = new Integer(1); - System.out.println("a==b? " + (a==b)); + System.out.println("a==b? " + (a == b)); Integer c = Integer.valueOf(1); Integer d = Integer.valueOf(1); - System.out.println("c==d? " + (c==d)); - } + System.out.println("c==d? " + (c == d)); +} ``` ```html @@ -348,14 +437,14 @@ valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中 ```java public static Integer valueOf(int i) { final int offset = 128; - if (i >= -128 && i <= 127) { // must cache + if (i >= -128 && i <= 127) { return IntegerCache.cache[i + offset]; } return new Integer(i); } ``` -The following is the list of primitives stored as immutable objects: +基本类型中可以使用缓存池的值如下: - boolean values true and false - all byte values @@ -363,24 +452,24 @@ The following is the list of primitives stored as immutable objects: - int values between -128 and 127 - char in the range \u0000 to \u007F -自动装箱过程编译器会调用 valueOf() 方法,因此多个 Integer 对象使用装箱来创建并且值相同,那么就会引用相同的对象,这样做很显然是为了节省内存开销。 +自动装箱过程编译器会调用 valueOf() 方法,因此多个 Integer 对象使用装箱来创建并且值相同,那么就会引用相同的对象。这样做很显然是为了节省内存开销。 ```java Integer x = 1; Integer y = 1; -System.out.println(c==d); // true +System.out.println(c == d); // true ``` > [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) -## 2. switch +## switch A switch works with the byte, short, char, and int primitive data types. It also works with enumerated types and a few special classes that "wrap" certain primitive types: Character, Byte, Short, and Integer. In the JDK 7 release, you can use a String object in the expression of a switch statement. -switch 不支持 long,是因为 swicth 的设计初衷是为那些只需要对少数几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。 +switch 不支持 long,是因为 swicth 的设计初衷是为那些只需要对少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。 > [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) @@ -410,7 +499,7 @@ public static void main(java.lang.String[]); > [How does Java's switch work under the hood?](https://stackoverflow.com/questions/12020048/how-does-javas-switch-work-under-the-hood) -# 反射 +# 七、反射 每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。 @@ -418,12 +507,15 @@ public static void main(java.lang.String[]); 反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。 -Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库包含了 **Field** 、**Method** 以及 **Constructor** 类。可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段,可以使用 invoke() 方法调用与 Method 对象关联的方法,可以用 Constructor 创建新的对象。 +Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类: + +1. **Field** :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字; +2. **Method** :可以使用 invoke() 方法调用与 Method 对象关联的方法; +3. **Constructor** :可以用 Constructor 创建新的对象。 IDE 使用反射机制获取类的信息,在使用一个类的对象时,能够把类的字段、方法和构造函数等信息列出来供用户选择。 -更详细的内容:[深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/) - +> [深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/) **Advantages of Using Reflection:** @@ -431,7 +523,7 @@ IDE 使用反射机制获取类的信息,在使用一个类的对象时,能 - **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. @@ -441,20 +533,21 @@ Reflection is powerful, but should not be used indiscriminately. If it is possib > [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html) - -# 异常 +# 八、异常 Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error** 和 **Exception**,其中 Error 用来表示编译时系统错误。 -Exception 分为两种: **受检异常** 和 **非受检异常**。受检异常需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;非受检异常是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序奔溃并且无法恢复。 +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 { @@ -465,26 +558,24 @@ public class Box { } ``` -更详细的内容: +> - [Java 泛型详解](https://www.ziwenxie.site/2017/03/01/java-generic/) +> - [10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693) -- [Java 泛型详解](https://www.ziwenxie.site/2017/03/01/java-generic/) -- [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) +# 十一、特性 -# 特性 +## 面向对象三大特性 -## 1. 三大特性 +> [封装、继承、多态](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 各版本的新特性 -## 2. Java 各版本的新特性 - -New highlights in Java SE 8 +**New highlights in Java SE 8** 1. Lambda Expressions 2. Pipelines and Streams @@ -496,7 +587,7 @@ New highlights in Java SE 8 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 @@ -507,16 +598,13 @@ New highlights in Java SE 7 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) +> - [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 8 特性 ](http://www.importnew.com/19345.html) - -## 3. Java 与 C++ 的区别 +## Java 与 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.| @@ -533,7 +621,12 @@ Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Obje > [What are the main differences between Java and C++?](http://cs-fundamentals.com/tech-interview/java/differences-between-java-and-cpp.php) -## 4. 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" + +# 参考资料 + +- Eckel B. Java 编程思想[M]. 机械工业出版社, 2002. +- Bloch J. Effective java[M]. Addison-Wesley Professional, 2017. diff --git a/notes/Java 容器.md b/notes/Java 容器.md index 2da89cca..bb0a9265 100644 --- a/notes/Java 容器.md +++ b/notes/Java 容器.md @@ -1,86 +1,88 @@ -* [概览](#概览) - * [1. List](#1-list) - * [2. Set](#2-set) - * [3. Queue](#3-queue) - * [4. Map](#4-map) - * [5. Java 1.0/1.1 容器](#5-java-1011-容器) -* [容器中的设计模式](#容器中的设计模式) - * [1. 迭代器模式](#1-迭代器模式) - * [2. 适配器模式](#2-适配器模式) -* [散列](#散列) -* [源码分析](#源码分析) - * [1. ArrayList](#1-arraylist) - * [概览](#概览) - * [Fail-Fast](#fail-fast) - * [和 Vector 的区别](#和-vector-的区别) - * [和 LinkedList 的区别](#和-linkedlist-的区别) - * [2. Vector 与 Stack](#2-vector-与-stack) - * [3. LinkedList](#3-linkedlist) - * [4. TreeMap](#4-treemap) - * [5. HashMap](#5-hashmap) - * [基本数据结构](#基本数据结构) - * [拉链法的工作原理](#拉链法的工作原理) - * [扩容](#扩容) - * [null 值](#null-值) - * [与 HashTable 的区别](#与-hashtable-的区别) - * [6. LinkedHashMap](#6-linkedhashmap) - * [7. ConcurrentHashMap](#7-concurrenthashmap) -* [参考资料](#参考资料) +* [一、概览](#一概览) + * [Collection](#collection) + * [Map](#map) +* [二、容器中的设计模式](#二容器中的设计模式) + * [迭代器模式](#迭代器模式) + * [适配器模式](#适配器模式) +* [三、散列](#三散列) +* [四、源码分析](#四源码分析) + * [ArrayList](#arraylist) + * [Vector](#vector) + * [LinkedList](#linkedlist) + * [TreeMap](#treemap) + * [HashMap](#hashmap) + * [LinkedHashMap](#linkedhashmap) + * [ConcurrentHashMap - JDK 1.7](#concurrenthashmap---jdk-17) + * [ConcurrentHashMap - JDK 1.8](#concurrenthashmap---jdk-18) +* [五、参考资料](#五参考资料) -# 概览 - -

+# 一、概览 容器主要包括 Collection 和 Map 两种,Collection 又包含了 List、Set 以及 Queue。 -## 1. List +## Collection + +

+ +### 1. Set + +- HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 + +- TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logn); + +- LinkedHashSet:具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序。 + +### 2. List - ArrayList:基于动态数组实现,支持随机访问; +- Vector:和 ArrayList 类似,但它是线程安全的; + - LinkedList:基于双向循环链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双端队列。 -## 2. Set +### 3. Queue -- HashSet:基于 Hash 实现,支持快速查找,但是失去有序性; +- LinkedList:可以用它来支持双向队列; -- TreeSet:基于红黑树实现,保持有序,但是查找效率不如 HashSet; +- PriorityQueue 是基于堆结构实现,可以用它来实现优先级队列。 -- LinkedHashSet:具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序,因此具有有序性。 +## Map -## 3. Queue +

-只有两个实现:LinkedList 和 PriorityQueue,其中 LinkedList 支持双向队列,PriorityQueue 是基于堆结构实现。 +- HashMap:基于哈希实现; -## 4. Map - -- HashMap:基于 Hash 实现。 +- HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。 - LinkedHashMap:使用链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 - TreeMap:基于红黑树实现。 -- ConcurrentHashMap:线程安全 Map,不涉及类似于 HashTable 的同步加锁。 +# 二、容器中的设计模式 -## 5. Java 1.0/1.1 容器 +## 迭代器模式 -对于旧的容器,我们决不应该使用它们,只需要对它们进行了解。 +

-- Vector:和 ArrayList 类似,但它是线程安全的。 +Collection 实现了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。 -- HashTable:和 HashMap 类似,但它是线程安全的。 +从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。 -# 容器中的设计模式 +```java +List list = new ArrayList<>(); +list.add("a"); +list.add("b"); +for (String item : list) { + System.out.println(item); +} +``` -## 1. 迭代器模式 +> [迭代器模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E5%8D%81%E4%BA%8C%E8%BF%AD%E4%BB%A3%E5%99%A8%E6%A8%A1%E5%BC%8F) -从概览图可以看到,每个集合类都有一个 Iterator 对象,可以通过这个迭代器对象来遍历集合中的元素。 - -[Java 中的迭代器模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#1-%E8%BF%AD%E4%BB%A3%E5%99%A8%E6%A8%A1%E5%BC%8F) - -## 2. 适配器模式 +## 适配器模式 java.util.Arrays#asList() 可以把数组类型转换为 List 类型。 @@ -90,61 +92,86 @@ java.util.Arrays#asList() 可以把数组类型转换为 List 类型。 list = Arrays.asList(arr); ``` -# 散列 +> [适配器模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E5%8D%81%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F) -使用 hasCode() 来返回散列值,使用的是对象的地址。 +# 三、散列 + +hasCode() 返回散列值,使用的是对象的地址。 而 equals() 是用来判断两个对象是否相等的,相等的两个对象散列值一定要相同,但是散列值相同的两个对象不一定相等。 相等必须满足以下五个性质: -1. 自反性 -2. 对称性 -3. 传递性 -4. 一致性(多次调用 x.equals(y),结果不变) -5. 对任何不是 null 的对象 x 调用 x.equals(nul) 结果都为 false +**1. 自反性** -# 源码分析 +```java +x.equals(x); // true +``` -建议先阅读 [算法 - 查找](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E7%AE%97%E6%B3%95.md#%E6%9F%A5%E6%89%BE) 部分,对集合类源码的理解有很大帮助。 +**2. 对称性** -源码下载:[OpenJDK 1.7](http://download.java.net/openjdk/jdk7) +```java +x.equals(y) == y.equals(x) // true +``` -## 1. ArrayList +**3. 传递性** + +```java +if(x.equals(y) && y.equals(z)) { + x.equals(z); // true; +} +``` + +**4. 一致性** + +多次调用 equals() 方法结果不变 + +```java +x.equals(y) == x.equals(y); // true +``` + +**5. 与 null 的比较** + +对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false + +```java +x.euqals(null); // false; +``` + +# 四、源码分析 + +建议先阅读 [算法-查找](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) + +以下源码从 JDK 1.8 提取而来,下载地址:[JDK-Source-Code](https://github.com/CyC2018/JDK-Source-Code)。 + +## ArrayList [ArraList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/ArrayList.java) -### 概览 +### 1. 概览 实现了 RandomAccess 接口,因此支持随机访问,这是理所当然的,因为 ArrayList 是基于数组实现的。 ```java public class ArrayList extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable + implements List, RandomAccess, Cloneable, java.io.Serializable ``` -基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明该数组默认不会被序列化。这是因为该数组不是所有位置都占满元素,因此也就没必要全部都进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那么部分内容。 +基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。这是 ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那么部分内容。 ```java -private transient Object[] elementData; +transient Object[] elementData; // non-private to simplify nested class access ``` 数组的默认大小为 10。 ```java -public ArrayList(int initialCapacity) { - super(); - if (initialCapacity < 0) - throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); - this.elementData = new Object[initialCapacity]; -} - -public ArrayList() { - this(10); -} +private static final int DEFAULT_CAPACITY = 10; ``` -删除元素时调用 System.arraycopy() 对元素进行复制,因此删除操作成本很高。 +删除元素时需要调用 System.arraycopy() 对元素进行复制,因此删除操作成本很高。 ```java public E remove(int index) { @@ -156,17 +183,18 @@ public E remove(int index) { int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); - elementData[--size] = null; // Let gc do its work + elementData[--size] = null; // clear to let GC do its work return oldValue; } ``` -添加元素时使用 ensureCapacity() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,使得新容量为旧容量的 1.5 倍。扩容操作需要把原数组整个复制到新数组中,因此最好在创建 ArrayList 时就指定大概的容量大小,减少扩容操作的次数。 +添加元素时使用 ensureCapacity() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,使得新容量为旧容量的 1.5 倍(oldCapacity + (oldCapacity >> 1))。扩容操作需要把原数组整个复制到新数组中,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 ```java -private void ensureCapacityInternal(int minCapacity) { +private void ensureExplicitCapacity(int minCapacity) { modCount++; + // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); @@ -183,34 +211,28 @@ private void grow(int minCapacity) { // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } - -private static int hugeCapacity(int minCapacity) { - if (minCapacity < 0) // overflow - throw new OutOfMemoryError(); - return (minCapacity > MAX_ARRAY_SIZE) ? - Integer.MAX_VALUE : - MAX_ARRAY_SIZE; -} ``` -### Fail-Fast +### 2. Fail-Fast -modCount 用来记录 ArrayList 结构发生变化的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 +modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。 ```java -private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ +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 array length - s.writeInt(elementData.length); + // 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 < size; i++) + for (int i=0; i()); 返回一个线程安全的 ArrayList,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类; -### 和 LinkedList 的区别 +### 4. 和 LinkedList 的区别 -1. ArrayList 基于动态数组实现,LinkedList 基于双向循环链表实现; -2. ArrayList 支持随机访问,LinkedList 不支持; -3. LinkedList 在任意位置添加删除元素更快。 +- ArrayList 基于动态数组实现,LinkedList 基于双向循环链表实现; +- ArrayList 支持随机访问,LinkedList 不支持; +- LinkedList 在任意位置添加删除元素更快。 -## 2. Vector 与 Stack +## Vector [Vector.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/Vector.java) -## 3. LinkedList +## LinkedList [LinkedList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/LinkedList.java) -## 4. TreeMap +## TreeMap [TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java) -## 5. HashMap +## HashMap [HashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) -### 基本数据结构 +### 1. 存储结构 使用拉链法来解决冲突,内部包含了一个 Entry 类型的数组 table,数组中的每个位置被当成一个桶。 @@ -257,17 +279,79 @@ transient Entry[] table; 其中,Entry 就是存储数据的键值对,它包含了四个字段。从 next 字段我们可以看出 Entry 是一个链表,即每个桶会存放一个链表。 -

+

-### 拉链法的工作原理 +JDK 1.8 使用 Node 类型存储一个键值对,它依然继承自 Entry,因此可以按照上面的存储结构来理解。 -使用默认构造函数新建一个 HashMap,默认大小为 16。Entry 的类型为 <String, Integer>。先后插入三个元素:("sachin", 30), ("vishal", 20) 和 ("vaibhav", 20)。计算 "sachin" 的 hashcode 为 115,使用除留余数法得到 115 % 16 = 3,因此 ("sachin", 30) 键值对放到第 3 个桶上。同样得到 ("vishal", 20) 和 ("vaibhav", 20) 都应该放到第 6 个桶上,因此需要把 ("vaibhav", 20) 链接到 ("vishal", 20) 之后。 +需要注意的是,Key 类型为 final,这意味着它不可改变,因此每个桶的链表采用头插法实现,也就是说新节点需要只能在链表头部插入。 -

+```java +static class Node implements Map.Entry { + final int hash; + final K key; + V value; + Node next; -当进行查找时,需要分成两步进行,第一步是先根据 hashcode 计算出所在的桶,第二步是在链表上顺序查找。由于 table 是数组形式的,具有随机读取的特性,因此这一步的时间复杂度为 O(1),而第二步需要在链表上顺序查找,时间复杂度显然和链表的长度成正比。 + Node(int hash, K key, V value, Node next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } -### 扩容 + public final K getKey() { return key; } + public final V getValue() { return value; } + public final String toString() { return key + "=" + value; } + + public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); + } + + public final V setValue(V newValue) { + V oldValue = value; + value = newValue; + return oldValue; + } + + public final boolean equals(Object o) { + if (o == this) + return true; + if (o instanceof Map.Entry) { + Map.Entry e = (Map.Entry)o; + if (Objects.equals(key, e.getKey()) && + Objects.equals(value, e.getValue())) + return true; + } + return false; + } +} +``` + +### 2. 拉链法的工作原理 + +```java +HashMap map = new HashMap<>(); // 默认大小为 16 +map.put("sachin", 30); +map.put("vishal", 20); +map.put("vaibhav", 20); +``` + +- 计算 "sachin" 的 hashcode 为 115,使用除留余数法得到 115 % 16 = 3,因此 ("sachin", 30) 键值对放到第 3 个桶上。 +- 同样得到 ("vishal", 20) 和 ("vaibhav", 20) 都应该放到第 6 个桶上。("vishal", 20) 先放入, ("vaibhav", 20) 链接到 ("vishal", 20) 之后。 + +

+ +当进行查找时,需要分成两步进行,第一步是先根据 hashcode 计算出所在的桶,第二步是在链表上顺序查找。由于 table 是数组形式的,具有随机读取的特性,因此第一步的时间复杂度为 O(1),而第二步需要在链表上顺序查找,时间复杂度显然和链表的长度成正比。 + +### 3. 链表转红黑树 + +应该注意到,从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。 + +

+ +### 4. 扩容 + +因为从 JDK 1.8 开始引入了红黑树,因此扩容操作较为复杂,为了便于理解,以下内容使用 JDK 1.7 的内容。 设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的数量级为 O(N/M)。 @@ -275,11 +359,12 @@ transient Entry[] table; 和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。 -capacity 表示 table 的容量大小,默认为 16,需要注意的是容量必须保证为 2 的次方。容量就是 table 数组的长度,size 是数组的实际使用量。 - -threshold 规定了一个 size 的临界值,size 必须小于 threshold,如果大于等于,就必须进行扩容操作。 - -threshold = capacity * load_factor,其中 load_factor 为 table 数组能够使用的比例。 +| 参数 | 含义 | +| :--: | :-- | +| 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; @@ -310,7 +395,7 @@ void addEntry(int hash, K key, V value, int bucketIndex) { } ``` -扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把旧 table 的所有键值对重新插入新的 table 中,因此这一步是很费时的。但是从均摊分析的角度来考虑,HashMap 的查找速度依然在常数级别。 +扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把旧 table 的所有键值对重新插入新的 table 中,因此这一步是很费时的。 ```java void resize(int newCapacity) { @@ -346,84 +431,325 @@ void transfer(Entry[] newTable) { } ``` -### null 值 +### 5. 确定桶下标 -get() 操作需要分成两种情况,key 为 null 和不为 null,从中可以看出 HashMap 允许插入 null 作为键。 +需要三步操作:计算 Key 的 hashCode、高位运算、除留余数法取模。 + +

+ +**(一)hashcode()** ```java -public V get(Object key) { - if (key == null) - return getForNullKey(); - int hash = hash(key.hashCode()); - for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { - Object k; - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) - return e.value; - } - return null; +public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); } ``` -put() 操作也需要根据 key 是否为 null 做不同的处理,需要注意的是如果本来没有 key 为 null 的键值对,新插入一个 key 为 null 的键值对时默认是放在数组的 0 位置,这是因为 null 不能计算 hash 值,也就无法知道应该放在哪个链表上。 +**(二)高位运算** + +通过 hashCode() 的高 16 位异或低 16 位,使得数组比较小时,也能保证高低位都参与到了哈希计算中。 ```java -public V put(K key, V value) { - if (key == null) - return putForNullKey(value); - int hash = hash(key.hashCode()); - int i = indexFor(hash, table.length); - 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; +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` +**(三)除留余数** + +令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质: + +``` +x : 00010000 +x-1 : 00001111 +``` + +令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位及以上数: + +``` +y : 10110010 +x-1 : 00001111 +y&(x-1) : 00000010 +``` + +这个性质和 y 对 x 取模效果是一样的: + +``` +x : 00010000 +y : 10110010 +y%x : 00000010 +``` + +我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时能用位运算的话能带来更高的性能。 + +拉链法需要使用除留余数法来得到桶下标,也就是需要进行以下计算:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。 + +以下操作在 Java 8 中没有,但是原理上相同。 + ```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; +static int indexFor(int h, int length) { + return h & (length-1); } ``` -### 与 HashTable 的区别 +### 6. 扩容-重新计算桶下标 -- HashMap 几乎可以等价于 Hashtable,除了 HashMap 是非 synchronized 的,并可以接受 null(HashMap 可以接受为 null 的键值 (key) 和值 (value),而 Hashtable 则不行)。 -- HashMap 是非 synchronized,而 Hashtable 是 synchronized,这意味着 Hashtable 是线程安全的,多个线程可以共享一个 Hashtable;而如果没有正确的同步的话,多个线程是不能共享 HashMap 的。Java 5 提供了 ConcurrentHashMap,它是 HashTable 的替代,比 HashTable 的扩展性更好。 -- 另一个区别是 HashMap 的迭代器 (Iterator) 是 fail-fast 迭代器,而 Hashtable 的 enumerator 迭代器不是 fail-fast 的。所以当有其它线程改变了 HashMap 的结构(增加或者移除元素),将会抛出 ConcurrentModificationException,但迭代器本身的 remove() 方法移除元素则不会抛出 ConcurrentModificationException 异常。但这并不是一个一定发生的行为,要看 JVM。这条同样也是 Enumeration 和 Iterator 的区别。 -- 由于 Hashtable 是线程安全的也是 synchronized,所以在单线程环境下它比 HashMap 要慢。如果你不需要同步,只需要单一线程,那么使用 HashMap 性能要好过 Hashtable。 +在进行扩容时,需要把 Node 重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。 + +假设原数组长度 capacity 为 8,扩容之后 new capacity 为 16: + +```html +capacity : 00010000 +new capacity : 00100000 +``` + +对于一个 Key,它的 hashCode 如果在第 6 位上为 0,那么除留余数得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 + 8。 + +### 7. 扩容-计算数组容量 + +先考虑如何求一个数的补码,对于 10100000,它的补码为 11111111,可以使用以下方法得到: + +``` +mask |= mask >> 1 11000000 +mask |= mask >> 2 11110000 +mask |= mask >> 4 11111111 +``` + +如果最后令 mask+1,得到就是大于原始数字的最小的 2 次方。 + +以下是 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; +} +``` + +### 7. null 值 + +HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0 个桶的位置,因为这个 Key 无法计算 hashCode(),因此只能规定一个桶让它存放。 + +### 8. 与 HashTable 的区别 + +- HashTable 是同步的,它使用了 synchronized 来进行同步。它也是线程安全的,多个线程可以共享同一个 HashTable。HashMap 不是同步的,但是可以使用 ConcurrentHashMap,它是 HashTable 的替代,而且比 HashTable 可扩展性更好。 +- HashMap 可以插入键为 null 的 Entry。 +- HashMap 的迭代器是 fail-fast 迭代器,而 Hashtable 的 enumerator 迭代器不是 fail-fast 的。 +- 由于 Hashtable 是线程安全的也是 synchronized,所以在单线程环境下它比 HashMap 要慢。 - HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。 -> [What is difference between HashMap and Hashtable in Java?](http://javarevisited.blogspot.hk/2010/10/difference-between-hashmap-and.html) - -## 6. LinkedHashMap +## LinkedHashMap [LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) -## 7. ConcurrentHashMap +## ConcurrentHashMap - JDK 1.7 -[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) +[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/1.7/ConcurrentHashMap.java) -[探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/) +ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶。 -# 参考资料 +相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。 + +### 1. 存储结构 + +和 HashMap 类似。 + +```java +static final class HashEntry { + final int hash; + final K key; + volatile V value; + volatile HashEntry next; +} +``` + +继承自 ReentrantLock,每个 Segment 维护着多个 HashEntry。 + +```java +static final class Segment extends ReentrantLock implements Serializable { + + private static final long serialVersionUID = 2249069246763182397L; + + static final int MAX_SCAN_RETRIES = + Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1; + + transient volatile HashEntry[] table; + + transient int count; + + transient int modCount; + + transient int threshold; + + final float loadFactor; +} +``` + +```java +final Segment[] segments; +``` + +默认的并发级别为 16,也就是说默认创建 16 个 Segment。 + +```java +static final int DEFAULT_CONCURRENCY_LEVEL = 16; +``` + +

+ +### 2. HashEntery 的不可变性 + +HashEntry 中的 key,hash,next 都声明为 final 型。这意味着,不能把节点添加到链接的中间和尾部,也不能在链接的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变。这个特性可以大大降低处理链表时的复杂性。 + +同时,HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程 “看” 到。在 ConcurrentHashMap 中,不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。 + +```java +final V remove(Object key, int hash, Object value) { + if (!tryLock()) + scanAndLock(key, hash); + V oldValue = null; + try { + HashEntry[] tab = table; + int index = (tab.length - 1) & hash; + HashEntry e = entryAt(tab, index); + HashEntry pred = null; + while (e != null) { + K k; + HashEntry next = e.next; + if ((k = e.key) == key || + (e.hash == hash && key.equals(k))) { + V v = e.value; + if (value == null || value == v || value.equals(v)) { + if (pred == null) + setEntryAt(tab, index, next); + else + pred.setNext(next); + ++modCount; + --count; + oldValue = v; + } + break; + } + pred = e; + e = next; + } + } finally { + unlock(); + } + return oldValue; +} +``` + +在以下链表中删除 C 节点,C 节点之后的所有节点都原样保留,C 节点之前的所有节点都被克隆到新的链表中,并且顺序被反转。可以注意到,在执行 remove 操作时,原始链表并没有被修改,也就是说,读线程不会受到执行 remove 操作的并发写线程的干扰。 + +

+ +

+ +除了 remove 操作,其它操作也类似。可以得出一个结论:写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。 + +### 3. Volatile 变量 + +```java +static final class HashEntry { + final int hash; + final K key; + volatile V value; + volatile HashEntry next; +} +``` + +由于内存可见性问题,未正确同步的情况下,写线程写入的值可能并不为后续的读线程可见。 + +下面以写线程 M 和读线程 N 来说明 ConcurrentHashMap 如何协调读 / 写线程间的内存可见性问题。 + +

+ +假设线程 M 在写入了 volatile 型变量 count 后,线程 N 读取了这个 volatile 型变量 count。 + +根据 happens-before 关系法则中的程序次序法则,A appens-before 于 B,C happens-before D。 + +根据 Volatile 变量法则,B happens-before C。 + +根据传递性,连接上面三个 happens-before 关系得到:A appens-before 于 B; B appens-before C;C happens-before D。也就是说:写线程 M 对链表做的结构性修改,在读线程 N 读取了同一个 volatile 变量后,对线程 N 也是可见的了。 + +虽然线程 N 是在未加锁的情况下访问链表。Java 的内存模型可以保证:只要之前对链表做结构性修改操作的写线程 M 在退出写方法前写 volatile 型变量 count,读线程 N 在读取这个 volatile 型变量 count 后,就一定能 “看到” 这些修改。 + +ConcurrentHashMap 中,每个 Segment 都有一个变量 count。它用来统计 Segment 中的 HashEntry 的个数。这个变量被声明为 volatile。 + +```java +transient volatile int count; +``` + +所有不加锁读方法,在进入读方法时,首先都会去读这个 count 变量。比如下面的 get 方法: + +```java +V get(Object key, int hash) { + if(count != 0) { // 首先读 count 变量 + HashEntry e = getFirst(hash); + while(e != null) { + if(e.hash == hash && key.equals(e.key)) { + V v = e.value; + if(v != null) + return v; + // 如果读到 value 域为 null,说明发生了重排序,加锁后重新读取 + return readValueUnderLock(e); + } + e = e.next; + } + } + return null; +} +``` + +在 ConcurrentHashMap 中,所有执行写操作的方法(put, remove, clear),在对链表做结构性修改之后,在退出写方法前都会去写这个 count 变量。所有未加锁的读操作(get, contains, containsKey)在读方法中,都会首先去读取这个 count 变量。 + +根据 Java 内存模型,对 同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程 “看到”。 + +这个特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 ,读线程才需要加锁后重读)。 + +### 4. 小结 + +ConcurrentHashMap 的高并发性主要来自于三个方面: + +- 用分离锁实现多个线程间的更深层次的共享访问。 +- 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。 +- 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。 + +## ConcurrentHashMap - JDK 1.8 + +[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/ConcurrentHashMap.java) + +

+ + +JDK 1.7 分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock。 + +

+ +JDK 1.8 的实现不是用了 Segment,Segment 属于重入锁 ReentrantLock。而是使用了内置锁 synchronized,主要是出于以下考虑: + +1. synchronized 的锁粒度更低; +2. synchronized 优化空间更大; +3. 在大量数据操作的情况下,ReentrantLock 会开销更多的内存。 + +并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。 + +# 五、参考资料 - Java 编程思想 +- [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) + diff --git a/notes/Java 并发.md b/notes/Java 并发.md index 23fc6d1b..59813682 100644 --- a/notes/Java 并发.md +++ b/notes/Java 并发.md @@ -1,71 +1,58 @@ -* [使用线程](#使用线程) - * [1. 实现 Runnable 接口](#1-实现-runnable-接口) - * [2. 实现 Callable 接口](#2-实现-callable-接口) - * [3. 继承 Thread 类](#3-继承-thread-类) - * [4. 实现接口 vs 继承 Thread](#4-实现接口-vs-继承-thread) -* [Executor](#executor) -* [基础线程机制](#基础线程机制) - * [1. sleep()](#1-sleep) - * [2. yield()](#2-yield) - * [3. join()](#3-join) - * [4. deamon](#4-deamon) -* [线程之间的协作](#线程之间的协作) - * [1. 线程通信](#1-线程通信) - * [2. 线程同步](#2-线程同步) - * [2.1 synchronized](#21-synchronized) - * [2.2 Lock](#22-lock) - * [2.3 BlockingQueue](#23-blockingqueue) -* [结束线程](#结束线程) - * [1. 阻塞](#1-阻塞) - * [2. 中断](#2-中断) -* [线程状态转换](#线程状态转换) -* [volatile](#volatile) - * [1. 内存可见性](#1-内存可见性) - * [2. 禁止指令重排](#2-禁止指令重排) -* [内存模型](#内存模型) +* [一、使用线程](#一使用线程) + * [实现 Runnable 接口](#实现-runnable-接口) + * [实现 Callable 接口](#实现-callable-接口) + * [继承 Thread 类](#继承-thread-类) + * [实现接口 VS 继承 Thread](#实现接口-vs-继承-thread) +* [二、基础线程机制](#二基础线程机制) + * [sleep()](#sleep) + * [yield()](#yield) + * [join()](#join) + * [deamon](#deamon) +* [三、结束线程](#三结束线程) + * [阻塞](#阻塞) + * [中断](#中断) +* [四、线程之间的协作](#四线程之间的协作) + * [同步与通信的概念理解](#同步与通信的概念理解) + * [线程同步](#线程同步) + * [线程通信](#线程通信) +* [五、线程状态转换](#五线程状态转换) +* [六、Executor](#六executor) +* [七、volatile](#七volatile) + * [保证内存可见性](#保证内存可见性) + * [禁止指令重排](#禁止指令重排) +* [八、内存模型](#八内存模型) * [1. 硬件的效率与一致性](#1-硬件的效率与一致性) * [2. Java 内存模型](#2-java-内存模型) * [3. 主内存与工作内存](#3-主内存与工作内存) * [4. 内存间交互操作](#4-内存间交互操作) * [5. 内存模型三大特性](#5-内存模型三大特性) - * [5.1 原子性](#51-原子性) - * [5.2 可见性](#52-可见性) - * [5.3 有序性](#53-有序性) * [6. 先行发生原则](#6-先行发生原则) -* [线程安全](#线程安全) +* [九、线程安全](#九线程安全) * [1. Java 语言中的线程安全](#1-java-语言中的线程安全) - * [1.1 不可变](#11-不可变) - * [1.2 绝对线程安全](#12-绝对线程安全) - * [1.3 相对线程安全](#13-相对线程安全) - * [1.4 线程兼容](#14-线程兼容) - * [1.5 线程对立](#15-线程对立) * [2. 线程安全的实现方法](#2-线程安全的实现方法) - * [2.1 互斥同步](#21-互斥同步) - * [2.2 非阻塞同步](#22-非阻塞同步) - * [2.3 无同步方案](#23-无同步方案) -* [锁优化](#锁优化) +* [十、锁优化](#十锁优化) * [1. 自旋锁与自适应自旋](#1-自旋锁与自适应自旋) * [2. 锁消除](#2-锁消除) * [3. 锁粗化](#3-锁粗化) * [4. 轻量级锁](#4-轻量级锁) * [5. 偏向锁](#5-偏向锁) -* [多线程开发良好的实践](#多线程开发良好的实践) +* [十一、多线程开发良好的实践](#十一多线程开发良好的实践) * [参考资料](#参考资料) -# 使用线程 +# 一、使用线程 有三种使用线程的方法: 1. 实现 Runnable 接口; 2. 实现 Callable 接口; -3. 继承 Thread 类; +3. 继承 Thread 类。 实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。 -## 1. 实现 Runnable 接口 +## 实现 Runnable 接口 需要实现 run() 方法。 @@ -84,16 +71,16 @@ public class MyRunnable implements Runnable { } ``` -## 2. 实现 Callable 接口 +## 实现 Callable 接口 与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。 ```java -public class MyCallable implements Callable { +public class MyCallable implements Callable { public Integer call() { // ... } - public static void main(String[] args) { + public static void main(String[] args) { MyCallable mc = new MyCallable(); FutureTask ft = new FutureTask<>(mc); Thread thread = new Thread(ft); @@ -103,51 +90,35 @@ public class MyCallable implements Callable { } ``` -## 3. 继承 Thread 类 +## 继承 Thread 类 同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。 ```java -class MyThread extends Thread { +public class MyThread extends Thread { public void run() { // ... } - public static void main(String[] args) { + public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); } } ``` -## 4. 实现接口 vs 继承 Thread +## 实现接口 VS 继承 Thread 实现接口会更好一些,因为: -1. Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口。 +1. Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口; 2. 类可能只要求可执行即可,继承整个 Thread 类开销会过大。 -# Executor -Executor 管理多个异步任务的执行,而无需程序员显示地管理线程的生命周期。 +# 二、基础线程机制 -主要有三种 Excutor: +## sleep() -1. CachedTreadPool:一个任务创建一个线程; -2. FixedThreadPool:所有任务只能使用固定大小的线程; -3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 - -```java -ExecutorService exec = Executors.newCachedThreadPool(); -for(int i = 0; i < 5; i++) { - exec.execute(new MyRunnable()); -} -``` - -# 基础线程机制 - -## 1. sleep() - -**Thread.sleep(millisec)** 方法会休眠当前正在执行的线程,millisec 单位为毫秒。也可以使用 TimeUnit.TILLISECONDS.sleep(millisec)。 +Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。也可以使用 TimeUnit.TILLISECONDS.sleep(millisec)。 sleep() 可能会抛出 InterruptedException。因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。 @@ -157,15 +128,15 @@ public void run() { // ... Thread.sleep(1000); // ... - } catch(InterruptedException e) { + } catch (InterruptedException e) { System.err.println(e); } } ``` -## 2. yield() +## yield() -对静态方法 **Thread.yield()** 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。 +对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。 ```java public void run() { @@ -174,15 +145,15 @@ public void run() { } ``` -## 3. join() +## join() -在线程中调用另一个线程的 **join()** 方法,会将当前线程挂起,直到目标线程结束。 +在线程中调用另一个线程的 join() 方法,会将当前线程挂起,直到目标线程结束。 可以加一个超时参数。 -## 4. deamon +## deamon -后台线程( **deamon** )是程序运行时在后台提供服务的线程,并不属于程序中不可或缺的部分。 +守护线程(deamon)是程序运行时在后台提供服务的线程,并不属于程序中不可或缺的部分。 当所有非后台线程结束时,程序也就终止,同时会杀死所有后台线程。 @@ -190,22 +161,119 @@ main() 属于非后台线程。 使用 setDaemon() 方法将一个线程设置为后台线程。 -# 线程之间的协作 +# 三、结束线程 -- **线程通信** :保证线程以一定的顺序执行; -- **线程同步** :保证线程对临界资源的互斥访问。 +## 阻塞 -线程通信往往是基于线程同步的基础上完成的,因此很多线程通信问题也是线程同步问题。 +一个线程进入阻塞状态可能有以下原因: -## 1. 线程通信 +1. 调用 Thread.sleep() 方法进入休眠状态; +2. 通过 wait() 使线程挂起,直到线程得到 notify() 或 notifyAll() 消息(或者 java.util.concurrent 类库中等价的 signal() 或 signalAll() 消息; +3. 等待某个 I/O 的完成; +4. 试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个线程已经获得了这个锁。 -**wait()、notify() 和 notifyAll()** 三者实现了线程之间的通信。 +## 中断 + +使用中断机制即可终止阻塞的线程。 + +使用 **interrupt()** 方法来中断某个线程,它会设置线程的中断状态。Object.wait(), Thread.join() 和 Thread.sleep() 三种方法在收到中断请求的时候会清除中断状态,并抛出 InterruptedException。 + +应当捕获这个 InterruptedException 异常,从而做一些清理资源的操作。 + +**1. 不可中断的阻塞** + +不能中断 I/O 阻塞和 synchronized 锁阻塞。 + +**2. Executor 的中断操作** + +Executor 避免对 Thread 对象的直接操作,但是使用 interrupt() 方法必须持有 Thread 对象。Executor 使用 shutdownNow() 方法来中断它里面的所有线程,shutdownNow() 方法会发送 interrupt() 调用给所有线程。 + +如果只想中断一个线程,那么使用 Executor 的 submit() 而不是 executor() 来启动线程,就可以持有线程的上下文。submit() 将返回一个泛型 Futrue,可以在它之上调用 cancel(),如果将 true 传递给 cancel(),那么它将会发送 interrupt() 调用给特定的线程。 + +**3. 检查中断** + +通过中断的方法来终止线程,需要线程进入阻塞状态才能终止。如果编写的 run() 方法循环条件为 true,但是该线程不发生阻塞,那么线程就永远无法终止。 + +interrupt() 方法会设置中断状态,可以通过 interrupted() 方法来检查中断状,从而判断一个线程是否已经被中断。 + +interrupted() 方法在检查完中断状态之后会清除中断状态,这样做是为了确保一次中断操作只会产生一次影响。 + +# 四、线程之间的协作 + +## 同步与通信的概念理解 + +在操作系统中,有三个概念用来描述进程间的协作关系: + +1. 互斥:多个进程在同一时刻只有一个进程能进入临界区; +2. 同步:多个进程按一定顺序执行; +3. 通信:多个进程间的信息传递。 + +通信是一种手段,它可以用来实现同步。也就是说,通过在多个进程间传递信息,可以控制多个进程以一定顺序执行。 + +而同步又可以保证互斥。即进程按一定顺序执行,可以保证在同一时刻只有一个进程能访问临界资源。但是同步不止用来实现互斥,例如生成者消费者问题,生产者和消费者进程之间的同步不是用来控制对临界资源的访问。 + +总结起来就是:通信 --> 同步 --> 互斥。 + +进程和线程在一定程度上类似,也可以用这些概念来描述。 + +在 Java 语言中,这些概念描述有些差别: + +1. 同步:可以和操作系统的互斥等同; +2. 通信:可以和操作系统的同步等同。 + +## 线程同步 + +给定一个进程内的所有线程,都共享同一存储空间,这样有好处又有坏处。这些线程就可以共享数据,非常有用。不过,在两个线程同时修改某一资源时,这也会造成一些问题。Java 提供了同步机制,以控制对共享资源的互斥访问。 + +### 1. synchronized + +**同步一个方法** + +使多个线程不能同时访问该方法。 + +```java +public synchronized void func(String name) { + // ... +} +``` + +**同步一个代码块** + +```java +public void func(String name) { + synchronized(this) { + // ... + } +} +``` + +### 2. Lock + +实现更细粒度的控制。 + +```java +private Lock lock; +public int func(int value) { + try { + lock.lock(); + // ... + } finally { + lock.unlock(); + } +} +``` + +## 线程通信 + +### 1. wait() notify() notifyAll() + +它们都属于 Object 的一部分,而不属于 Thread。 wait() 会在等待时将线程挂起,而不是忙等待,并且只有在 notify() 或者 notifyAll() 到达时才唤醒。 -sleep() 和 yield() 并没有释放锁,但是 wait() 会释放锁。实际上,只有在同步控制方法或同步控制块里才能调用 wait() 、notify() 和 notifyAll()。 +sleep() 和 yield() 并没有释放锁,但是 wait() 会释放锁。 -这几个方法属于基类的一部分,而不属于 Thread。 +实际上,只有在同步控制方法或同步控制块里才能调用 wait() 、notify() 和 notifyAll()。 ```java private boolean flag = false; @@ -228,49 +296,7 @@ public synchronized void before() { 1. wait() 是 Object 类的方法,而 sleep() 是 Thread 的静态方法; 2. wait() 会放弃锁,而 sleep() 不会。 -## 2. 线程同步 - -给定一个进程内的所有线程,都共享同一存储空间,这样有好处又有坏处。这些线程就可以共享数据,非常有用。不过,在两个线程同时修改某一资源时,这也会造成一些问题。Java 提供了同步机制,以控制对共享资源的互斥访问。 - -### 2.1 synchronized - -**同步一个方法** - -使多个线程不能同时访问该方法。 - -```java -public synchronized void func(String name) { - // ... -} -``` - -**同步一个代码块** - -```java -public void func(String name) { - synchronized(this) { - // ... - } -} -``` - -### 2.2 Lock - -若要实现更细粒度的控制,我们可以使用锁(lock)。 - -```java -private Lock lock; -public int func(int value) { - try { - lock.lock(); - // ... - } finally { - lock.unlock(); - } -} -``` - -### 2.3 BlockingQueue +### 2. BlockingQueue java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现: @@ -279,6 +305,8 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现: 提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将一直阻塞到队列中有内容,如果队列为满 put() 将阻塞到队列有空闲位置。它们响应中断,当收到中断请求的时候会抛出 InterruptedException,从而提前结束阻塞状态。 +阻塞队列的 take() 和 put() 方法是线程安全的。 + **使用 BlockingQueue 实现生产者消费者问题** ```java @@ -309,7 +337,7 @@ public class Producer implements Runnable { // 消费者 import java.util.concurrent.BlockingQueue; -public class Consumer implements Runnable{ +public class Consumer implements Runnable { private BlockingQueue queue; public Consumer(BlockingQueue queue) { @@ -364,73 +392,39 @@ Consumer3 is consuming product made by Consumer3... Consumer4 is consuming product made by Consumer4... ``` -# 结束线程 +# 五、线程状态转换 -## 1. 阻塞 +

-一个线程进入阻塞状态可能有以下原因: +1. 新建(New):创建后尚未启动; +2. 可运行(Runnale):可能正在运行,也可能正在等待 CPU 时间片; +3. 无限期等待(Waiting):等待其它线程显示地唤醒,否则不会被分配 CPU 时间片; +4. 限期等待(Timed Waiting):无序等待其它线程显示地唤醒,在一定时间之后会被系统自动唤醒; +5. 阻塞(Blocking):等待获取一个排它锁,如果其线程释放了锁就会结束此状态; +6. 死亡(Terminated) -1. 调用 Thread.sleep() 方法进入休眠状态; -2. 通过 wait() 使线程挂起,直到线程得到 notify() 或 notifyAll() 消息(或者 java.util.concurrent 类库中等价的 signal() 或 signalAll() 消息; -3. 等待某个 I/O 的完成; -4. 试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个线程已经获得了这个锁。 +# 六、Executor -## 2. 中断 +Executor 管理多个异步任务的执行,而无需程序员显示地管理线程的生命周期。 -使用中断机制即可终止阻塞的线程。 +主要有三种 Executor: -使用 **interrupt()** 方法来中断某个线程,它会设置线程的中断状态。Object.wait(), Thread.join() 和 Thread.sleep() 三种方法在收到中断请求的时候会清除中断状态,并抛出 InterruptedException。 +1. CachedTreadPool:一个任务创建一个线程; +2. FixedThreadPool:所有任务只能使用固定大小的线程; +3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 -应当捕获这个 InterruptedException 异常,从而做一些清理资源的操作。 +```java +ExecutorService exec = Executors.newCachedThreadPool(); +for(int i = 0; i < 5; i++) { + exec.execute(new MyRunnable()); +} +``` -**不可中断的阻塞** - -不能中断 I/O 阻塞和 synchronized 锁阻塞。 - -**Executor 的中断操作** - -Executor 避免对 Thread 对象的直接操作,但是使用 interrupt() 方法必须持有 Thread 对象。Executor 使用 shutdownNow() 方法来中断它里面的所有线程,shutdownNow() 方法会发送 interrupt() 调用给所有线程。 - -如果只想中断一个线程,那么使用 Executor 的 submit() 而不是 executor() 来启动线程,就可以持有线程的上下文。submit() 将返回一个泛型 Futrue,可以在它之上调用 cancel(),如果将 true 传递给 cancel(),那么它将会发送 interrupt() 调用给特定的线程。 - -**检查中断** - -通过中断的方法来终止线程,需要线程进入阻塞状态才能终止。如果编写的 run() 方法循环条件为 true,但是该线程不发生阻塞,那么线程就永远无法终止。 - -interrupt() 方法会设置中断状态,可以通过 interrupted() 方法来检查中断状,从而判断一个线程是否已经被中断。 - -interrupted() 方法在检查完中断状态之后会清除中断状态,这样做是为了确保一次中断操作只会产生一次影响。 - -# 线程状态转换 - -

- -1. NEW(新建):创建后尚未启动的线程。 -2. RUNNABLE(运行):处于此状态的线程有可能正在执行,也有可能正在等待着 CPU 为它分配执行时间。 -3. BLOCKED(阻塞):阻塞与等待的区别是,阻塞在等待着获取到一个排它锁,这个时间将在另一个线程放弃这个锁的时候发生;而等待则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。 -4. Waiting(无限期等待):处于这种状态的进行不会被分配 CPU 执行时间,它们要等待其它线程显示地唤醒。以下方法会让线程进入这种状态: -5. TIMED_WAITING(限期等待):处于这种状态的线程也不会被分配 CPU 执行时间,不过无序等待其它线程显示地唤醒,在一定时间之后它们会由系统自动唤醒。 -6. TERMINATED(死亡) - -以下方法会让线程陷入无限期的等待状态: - -- 没有设置 Timeout 参数的 Object.wait() 方法 -- 没有设置 Timeout 参数的 Thread.join() 方法 -- LockSupport.park() 方法 - -以下方法会让线程进入限期等待状体: - -- Thread.sleep() -- 设置了 Timeout 参数的 Object.wait() 方法 -- 设置了 Timeout 参数的 Thread.join() 方法 -- LockSupport.parkNanos() 方法 -- LockSupport.parkUntil() 方法 - -# volatile +# 七、volatile 保证了内存可见性和禁止指令重排,没法保证原子性。 -## 1. 内存可见性 +## 保证内存可见性 普通共享变量被修改之后,什么时候被写入主存是不确定的。 @@ -438,7 +432,7 @@ volatile 关键字会保证每次修改共享变量之后该值会立即更新 synchronized 和 Lock 也能够保证内存可见性。它们能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。不过只有对共享变量的 set() 和 get() 方法都加上 synchronized 才能保证可见性,如果只有 set() 方法加了 synchronized,那么 get() 方法并不能保证会从内存中读取最新的数据。 -## 2. 禁止指令重排 +## 禁止指令重排 在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。 @@ -446,7 +440,7 @@ volatile 关键字通过添加内存屏障的方式来进制指令重排,即 可以通过 synchronized 和 Lock 来保证有序性,它们保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。 -# 内存模型 +# 八、内存模型 ## 1. 硬件的效率与一致性 @@ -460,7 +454,7 @@ volatile 关键字通过添加内存屏障的方式来进制指令重排,即 ## 2. Java 内存模型 -Java 虚拟机规范中试图定义一种 Java 内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。在此之前,主流程序语言(如 C/C++等)直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台上内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错,因此在某些场景就必须针对不同的平台来编写程序。 +Java 虚拟机规范中试图定义一种 Java 内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。在此之前,主流程序语言(如 C/C++等)直接使用物理硬件和操作系统的内存模型,但由于不同平台上内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错,因此在某些场景就必须针对不同的平台来编写程序。 ## 3. 主内存与工作内存 @@ -580,7 +574,7 @@ int j = 2; 上面两个例子综合起来证明了一个结论:时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。 -# 线程安全 +# 九、线程安全 《Java Concurrency In Practice》的作者 Brian Goetz 对“线程安全”有一个比较恰当的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的”。 @@ -638,7 +632,7 @@ public static void main(String[] args) { removeThread.start(); printThread.start(); - //不要同时产生过多的线程,否则会导致操作系统假死 + // 不要同时产生过多的线程,否则会导致操作系统假死 while (Thread.activeCount() > 20); } } @@ -829,7 +823,7 @@ incrementAndGet() 方法在一个无限循环中,不断尝试将一个比当 Java 语言中,如果一个变量要被多线程访问,可以使用 volatile 关键字声明它为“易变的”;如果一个变量要被某个线程独享,Java 中就没有类似 C++中 \_\_declspec(thread)这样的关键字,不过还是可以通过 java.lang.ThreadLocal 类来实现线程本地存储的功能。每一个线程的 Thread 对象中都有一个 ThreadLocalMap 对象,这个对象存储了一组以 ThreadLocal.threadLocalHashCode 为键,以本地线程变量为值的 K-V 值对,ThreadLocal 对象就是当前线程的 ThreadLocalMap 的访问入口,每一个 ThreadLocal 对象都包含了一个独一无二的 threadLocalHashCode 值,使用这个值就可以在线程 K-V 值对中找回对应的本地线程变量。 -# 锁优化 +# 十、锁优化 高效并发是从 JDK 1.5 到 JDK 1.6 的一个重要改进,HotSpot 虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。 @@ -839,7 +833,7 @@ Java 语言中,如果一个变量要被多线程访问,可以使用 volatile 自旋锁在 JDK 1.4.2 中就已经引入,只不过默认是关闭的,可以使用 -XX:+UseSpinning 参数来开启,在 JDK 1.6 就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时候很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是 10 次,用户可以使用参数 -XX:PreBlockSpin 来更改。 -自旋锁在 JDK 1.4.2 中就已经引入,只不过默认是关闭的,可以使用 -XX:+UseSpinning 参数来开启,在 JDK 1.6 就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时候很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是 10 次,用户可以使用参数 -XX:PreBlockSpin 来更改。 +在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越 “聪明” 了。 ## 2. 锁消除 @@ -916,7 +910,7 @@ public static String concatString(String s1, String s2, String s3) { 偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。 -# 多线程开发良好的实践 +# 十一、多线程开发良好的实践 - 给线程命名。 - 最小化同步范围。 @@ -930,4 +924,7 @@ public static String concatString(String s1, String s2, String s3) { - Java 编程思想 - 深入理解 Java 虚拟机 +- [线程通信](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) diff --git a/notes/JVM.md b/notes/Java 虚拟机.md similarity index 81% rename from notes/JVM.md rename to notes/Java 虚拟机.md index a49135b3..52ab99b6 100644 --- a/notes/JVM.md +++ b/notes/Java 虚拟机.md @@ -1,138 +1,125 @@ -* [内存模型](#内存模型) - * [1. 程序计数器](#1-程序计数器) - * [2. Java 虚拟机栈](#2-java-虚拟机栈) - * [3. 本地方法栈](#3-本地方法栈) - * [4. Java 堆](#4-java-堆) - * [5. 方法区](#5-方法区) - * [6. 运行时常量池](#6-运行时常量池) - * [7. 直接内存](#7-直接内存) -* [垃圾收集](#垃圾收集) - * [1. 判断一个对象是否可回收](#1-判断一个对象是否可回收) - * [1.1 引用计数](#11-引用计数) - * [1.2 可达性](#12-可达性) - * [1.3 引用类型](#13-引用类型) - * [1.3.1 强引用](#131-强引用) - * [1.3.2 软引用](#132-软引用) - * [1.3.3 弱引用](#133-弱引用) - * [1.3.4 虚引用](#134-虚引用) - * [1.3 方法区的回收](#13-方法区的回收) - * [1.4 finalize()](#14-finalize) - * [2. 垃圾收集算法](#2-垃圾收集算法) - * [2.1 标记-清除算法](#21-标记-清除算法) - * [2.2 复制算法](#22-复制算法) - * [2.3 标记-整理算法](#23-标记-整理算法) - * [2.4 分代收集算法](#24-分代收集算法) - * [3. 垃圾收集器](#3-垃圾收集器) - * [3.1 Serial 收集器](#31-serial-收集器) - * [3.2 ParNew 收集器](#32-parnew-收集器) - * [3.3 Parallel Scavenge 收集器](#33-parallel-scavenge-收集器) - * [3.4 Serial Old 收集器](#34-serial-old-收集器) - * [3.5 Parallel Old 收集器](#35-parallel-old-收集器) - * [3.6 CMS 收集器](#36-cms-收集器) - * [3.7 G1 收集器](#37-g1-收集器) - * [3.8 七种垃圾收集器的比较](#38-七种垃圾收集器的比较) - * [4. 内存分配与回收策略](#4-内存分配与回收策略) - * [4.1 优先在 Eden 分配](#41-优先在-eden-分配) - * [4.2 大对象直接进入老年代](#42-大对象直接进入老年代) - * [4.3 长期存活的对象进入老年代](#43-长期存活的对象进入老年代) - * [4.4 动态对象年龄判定](#44-动态对象年龄判定) - * [4.5 空间分配担保](#45-空间分配担保) - * [5. Full GC 的触发条件](#5-full-gc-的触发条件) - * [5.1 调用 System.gc()](#51-调用-systemgc) - * [5.2 老年代空间不足](#52-老年代空间不足) - * [5.3 空间分配担保失败](#53-空间分配担保失败) - * [5.4 JDK 1.7 及以前的永久代空间不足](#54-jdk-17-及以前的永久代空间不足) - * [5.5 Concurrent Mode Failure](#55-concurrent-mode-failure) -* [类加载机制](#类加载机制) - * [1 类的生命周期](#1-类的生命周期) - * [2. 类初始化时机](#2-类初始化时机) - * [3. 类加载过程](#3-类加载过程) - * [3.1 加载](#31-加载) - * [3.2 验证](#32-验证) - * [3.3 准备](#33-准备) - * [3.4 解析](#34-解析) - * [3.5 初始化](#35-初始化) - * [4. 类加载器](#4-类加载器) - * [4.1 类与类加载器](#41-类与类加载器) - * [4.2 类加载器分类](#42-类加载器分类) - * [4.3 双亲委派模型](#43-双亲委派模型) -* [JVM 参数](#jvm-参数) +* [一、运行时数据区域](#一运行时数据区域) + * [程序计数器](#程序计数器) + * [Java 虚拟机栈](#java-虚拟机栈) + * [本地方法栈](#本地方法栈) + * [Java 堆](#java-堆) + * [方法区](#方法区) + * [运行时常量池](#运行时常量池) + * [直接内存](#直接内存) +* [二、垃圾收集](#二垃圾收集) + * [判断一个对象是否可回收](#判断一个对象是否可回收) + * [垃圾收集算法](#垃圾收集算法) + * [垃圾收集器](#垃圾收集器) + * [内存分配与回收策略](#内存分配与回收策略) + * [Full GC 的触发条件](#full-gc-的触发条件) +* [三、类加载机制](#三类加载机制) + * [类的生命周期](#类的生命周期) + * [类初始化时机](#类初始化时机) + * [类加载过程](#类加载过程) + * [类加载器](#类加载器) +* [四、JVM 参数](#四jvm-参数) * [GC 优化配置](#gc-优化配置) * [GC 类型设置](#gc-类型设置) +* [参考资料](#参考资料) -# 内存模型 +# 一、运行时数据区域 -

+

-注:白色区域为线程私有的,蓝色区域为线程共享的。 +注:白色区域为线程私有,蓝色区域为线程共享。 -## 1. 程序计数器 +## 程序计数器 -记录正在执行的虚拟机字节码指令的地址(如果正在执行的是 Native 方法则为空)。 +记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。 -## 2. Java 虚拟机栈 +## Java 虚拟机栈 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 +可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小: + +```java +java -Xss=512M HackTheJava +``` + 该区域可能抛出以下异常: 1. 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常; 2. 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。 -## 3. 本地方法栈 +## 本地方法栈 + +本地方法不是用 Java 实现,对待这些方法需要特别处理。 与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。 -## 4. Java 堆 +## Java 堆 所有对象实例都在这里分配内存。 -这块区域是垃圾收集器管理的主要区域("GC 堆 ")。现在收集器基本都是采用分代收集算法,Java 堆还可以分成:新生代和老年代(新生代还可以分成 Eden 空间、From Survivor 空间、To Survivor 空间等)。 +是垃圾收集的主要区域("GC 堆 "),现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法,因此虚拟机把 Java 堆分成以下三块: -不需要连续内存,可以通过 -Xmx 和 -Xms 来控制动态扩展内存大小,如果动态扩展失败会抛出 OutOfMemoryError 异常。 +- 新生代(Young Generation) +- 老年代(Old Generation) +- 永久代(Permanent Generation) +当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间: -## 5. 方法区 +- Eden +- From Survivor +- To Survivor -用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 +

+ +Java 堆不需要连续内存,并且可以通过动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。 + +可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置最小值,第二个参数设置最大值。 + +```java +java -Xms=1M -XmX=2M HackTheJava +``` + +## 方法区 + +用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机把它当成永久代来进行垃圾回收。 -## 6. 运行时常量池 +## 运行时常量池 运行时常量池是方法区的一部分。 -类加载后,Class 文件中的常量池(用于存放编译期生成的各种字面量和符号引用)就会被放到这个区域。 +Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。 -在运行期间也可以用过 String 类的 intern() 方法将新的常量放入该区域。 +除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池。 -## 7. 直接内存 +## 直接内存 在 JDK 1.4 中新加入了 NIO 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 -# 垃圾收集 +# 二、垃圾收集 程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。 -## 1. 判断一个对象是否可回收 +## 判断一个对象是否可回收 -### 1.1 引用计数 +### 1. 引用计数 给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。 -两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致 GC 收集器无法回收。 +两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。 ```java objA.instance = objB; objB.instance = objA; ``` -### 1.2 可达性 +### 2. 可达性 通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是都是可用的,不可达的对象可被回收。 @@ -143,13 +130,13 @@ GC Roots 一般包含以下内容: 3. 方法区中的常量引用的对象 4. 本地方法栈中引用的对象 -### 1.3 引用类型 +### 3. 引用类型 无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。 Java 对引用的概念进行了扩充,引入四种强度不同的引用类型。 -#### 1.3.1 强引用 +**(一)强引用** 只要强引用存在,垃圾回收器永远不会回收调掉被引用的对象。 @@ -159,7 +146,7 @@ Java 对引用的概念进行了扩充,引入四种强度不同的引用类型 Object obj = new Object(); ``` -#### 1.3.2 软引用 +**(二)软引用** 用来描述一些还有用但是并非必需的对象。 @@ -174,7 +161,7 @@ Object obj = new Object(); SoftReference sf = new SoftReference(obj); ``` -#### 1.3.3 弱引用 +**(三)弱引用** 只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会被回收。 @@ -185,9 +172,11 @@ Object obj = new Object(); WeakReference wf = new WeakReference(obj); ``` -#### 1.3.4 虚引用 +**(四)虚引用** -又称为幽灵引用或者幻影引用.一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 +又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。 + +为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 使用 PhantomReference 来实现虚引用。 @@ -196,13 +185,11 @@ Object obj = new Object(); PhantomReference pf = new PhantomReference(obj); ``` -### 1.3 方法区的回收 +### 4. 方法区的回收 因为方法区主要存放永久代对象,而永久代对象的回收率比新生代差很多,因此在方法区上进行回收性价比不高。 -在方法区主要是对常量池的回收和对类的卸载。 - -常量池的回收和堆中对象回收类似。 +主要是对常量池的回收和对类的卸载。 类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载: @@ -214,15 +201,15 @@ PhantomReference pf = new PhantomReference(obj); 在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGo 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。 -### 1.4 finalize() +### 5. finalize() finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。 当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。 -## 2. 垃圾收集算法 +## 垃圾收集算法 -### 2.1 标记-清除算法 +### 1. 标记 - 清除

@@ -231,11 +218,11 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 不足: 1. 标记和清除过程效率都不高 -2. 会产生大量碎片 +2. 会产生大量碎片,内存碎片过多可能导致无法给大对象分配内存 之后的算法都是基于该算法进行改进。 -### 2.2 复制算法 +### 2. 复制

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

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 -### 2.4 分代收集算法 +### 4. 分代收集 现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 一般将 Java 堆分为新生代和老年代。 1. 新生代使用:复制算法 -2. 老年代使用:标记-清理 或者 标记-整理 算法。 +2. 老年代使用:标记 - 清理 或者 标记 - 整理 算法 -## 3. 垃圾收集器 +## 垃圾收集器

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。 -### 3.1 Serial 收集器 +### 1. Serial 收集器

@@ -276,7 +263,7 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 在 Client 应用场景中,分配给虚拟机管理的内存一般来说不会很大,该收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。 -### 3.2 ParNew 收集器 +### 2. ParNew 收集器

@@ -286,7 +273,7 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 默认开始的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。 -### 3.3 Parallel Scavenge 收集器 +### 3. Parallel Scavenge 收集器 是并行的多线程收集器。 @@ -298,7 +285,7 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。自适应调节策略也是它与 ParNew 收集器的一个重要区别。 -### 3.4 Serial Old 收集器 +### 4. Serial Old 收集器

@@ -307,7 +294,7 @@ Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下 1. 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。 2. 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。 -### 3.5 Parallel Old 收集器 +### 5. Parallel Old 收集器

@@ -315,11 +302,11 @@ Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下 在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。 -### 3.6 CMS 收集器 +### 6. CMS 收集器

-CMS(Concurrent Mark Sweep),从 Mark Sweep 可以知道它是基于标记-清除算法实现的。 +CMS(Concurrent Mark Sweep),从 Mark Sweep 可以知道它是基于标记 - 清除算法实现的。 特点:并发收集、低停顿。 @@ -338,9 +325,9 @@ CMS(Concurrent Mark Sweep),从 Mark Sweep 可以知道它是基于标记- 2. 无法处理浮动垃圾。由于并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留到下一次 GC 时再清理掉,这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此它不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。可以使用 -XX:CMSInitiatingOccupancyFraction 的值来改变触发收集器工作的内存占用百分比,JDK 1.5 默认设置下该值为 68,也就是当老年代使用了 68% 的空间之后会触发收集器工作。如果该值设置的太高,导致浮动垃圾无法保存,那么就会出现 Concurrent Mode Failure,此时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集。 -3. 标记-清除算法导致的空间碎片,给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前出发一次 Full GC。 +3. 标记 - 清除算法导致的空间碎片,给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 -### 3.7 G1 收集器 +### 7. G1 收集器

@@ -348,7 +335,7 @@ G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果 具备如下特点: -- 并行与并发:能充分利用多 CPU 环境下的硬件优势,使用多个 CPU 来缩短停顿时间; +- 并行与并发:能充分利用多 CPU 环境下的硬件优势,使用多个 CPU 来缩短停顿时间。 - 分代收集:分代概念依然得以保留,虽然它不需要其它收集器配合就能独立管理整个 GC 堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次 GC 的旧对象来获取更好的收集效果。 - 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。 - 可预测的停顿:这是它相对 CMS 的一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。 @@ -366,10 +353,10 @@ Region 不可能是孤立的,一个对象分配在某个 Region 中,可以 3. 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。 4. 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 -### 3.8 七种垃圾收集器的比较 +### 8. 七种垃圾收集器的比较 | 收集器 | 串行、并行 or 并发 | 新生代 / 老年代 | 算法 | 目标 | 适用场景 | -| --- | --- | --- | --- | --- | --- | +| :---: | :---: | :---: | :---: | :---: | :---: | | **Serial** | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单 CPU 环境下的 Client 模式 | | **Serial Old** | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 | | **ParNew** | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 | @@ -378,66 +365,66 @@ Region 不可能是孤立的,一个对象分配在某个 Region 中,可以 | **CMS** | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 | | **G1** | 并发 | both | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS | -## 4. 内存分配与回收策略 +## 内存分配与回收策略 对象的内存分配,也就是在堆上分配。主要分配在新生代的 Eden 区上,少数情况下也可能直接分配在老年代中。 -### 4.1 优先在 Eden 分配 +### 1. 优先在 Eden 分配 -大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC; +大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。 关于 Minor GC 和 Full GC: - Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 - Full GC:发生在老年代上,老年代对象和新生代的相反,其存活时间长,因此 Full GC 很少执行,而且执行速度会比 Minor GC 慢很多。 -### 4.2 大对象直接进入老年代 +### 2. 大对象直接进入老年代 大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。 -提供 -XX:PretenureSizeThreshold 参数,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制; +提供 -XX:PretenureSizeThreshold 参数,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。 -### 4.3 长期存活的对象进入老年代 +### 3. 长期存活的对象进入老年代 -JVM 为对象定义年龄计数器,经过 Minor GC 依然存活,并且能被 Survivor 区容纳的,移被移到 Survivor 区,年龄就增加 1 岁,增加到一定年龄则移动到老年代中(默认 15 岁,通过 -XX:MaxTenuringThreshold 设置); +JVM 为对象定义年龄计数器,经过 Minor GC 依然存活,并且能被 Survivor 区容纳的,移被移到 Survivor 区,年龄就增加 1 岁,增加到一定年龄则移动到老年代中(默认 15 岁,通过 -XX:MaxTenuringThreshold 设置)。 -### 4.4 动态对象年龄判定 +### 4. 动态对象年龄判定 JVM 并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无序等待 MaxTenuringThreshold 中要求的年龄。 -### 4.5 空间分配担保 +### 5. 空间分配担保 在发生 Minor GC 之前,JVM 先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的;如果不成立的话 JVM 会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。 -## 5. Full GC 的触发条件 +## Full GC 的触发条件 对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件: -### 5.1 调用 System.gc() +### 1. 调用 System.gc() 此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()。 -### 5.2 老年代空间不足 +### 2. 老年代空间不足 老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组。 -### 5.3 空间分配担保失败 +### 3. 空间分配担保失败 使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。 -### 5.4 JDK 1.7 及以前的永久代空间不足 +### 4. JDK 1.7 及以前的永久代空间不足 在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出 java.lang.OutOfMemoryError,为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 -### 5.5 Concurrent Mode Failure +### 5. Concurrent Mode Failure 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。 -# 类加载机制 +# 三、类加载机制 类是在运行期间动态加载的。 -## 1 类的生命周期 +## 类的生命周期

@@ -453,7 +440,7 @@ JVM 并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才 其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。 -## 2. 类初始化时机 +## 类初始化时机 虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随着发生): @@ -487,11 +474,11 @@ SuperClass[] sca = new SuperClass[10]; System.out.println(ConstClass.HELLOWORLD); ``` -## 3. 类加载过程 +## 类加载过程 包含了加载、验证、准备、解析和初始化这 5 个阶段。 -### 3.1 加载 +### 1. 加载 加载是类加载的一个阶段,注意不要混淆。 @@ -510,29 +497,29 @@ System.out.println(ConstClass.HELLOWORLD); - 从数据库读取,这种场景相对少见,例如有些中间件服务器(如 SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。 ... -### 3.2 验证 +### 2. 验证 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 主要有以下 4 个阶段: -**1. 文件格式验证** +**(一)文件格式验证** 验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。 -**2. 元数据验证** +**(二)元数据验证** 对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。 -**3. 字节码验证** +**(三)字节码验证** 通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。 -**4. 符号引用验证** +**(四)符号引用验证** 发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。 -### 3.3 准备 +### 3. 准备 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。 @@ -550,11 +537,11 @@ public static int value = 123; public static final int value = 123; ``` -### 3.4 解析 +### 4. 解析 将常量池的符号引用替换为直接引用的过程。 -### 3.5 初始化 +### 5. 初始化 初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 <clinit>() 方法的过程。 @@ -580,18 +567,18 @@ public class Test { ```java static class Parent { - public static int A = 1; - static { - A = 2; - } + public static int A = 1; + static { + A = 2; + } } static class Sub extends Parent { - public static int B = A; + public static int B = A; } public static void main(String[] args) { - System.out.println(Sub.B); // 输出结果是父类中的静态变量 A 的值 ,也就是 2。 + System.out.println(Sub.B); // 输出结果是父类中的静态变量 A 的值 ,也就是 2。 } ``` @@ -601,43 +588,45 @@ public static void main(String[] args) { - 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个进程阻塞,在实际过程中此种阻塞很隐蔽。 -## 4. 类加载器 +## 类加载器 虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流 ( 即字节码 )”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。 -### 4.1 类与类加载器 +### 1. 类与类加载器 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof() 关键字做对象所属关系判定等情况),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 -### 4.2 类加载器分类 +### 2. 类加载器分类 从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器: -一种是启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;另一种就是所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。 +- 启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分; + +- 所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。 从 Java 开发人员的角度看,类加载器可以划分得更细致一些: -- 启动类加载器(Bootstrap ClassLoader) 此类加载器负责将存放在 \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)实现的。它负责将 /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)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 -### 4.3 双亲委派模型 +### 3. 双亲委派模型 应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。

-**工作过程** +**(一)工作过程** 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归。因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。 -**好处** +**(二)好处** 使用双亲委派模型来组织类加载器之间的关系,使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型,由各个类加载器自行加载的话,如果用户编写了一个称为java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将变得一片混乱。如果开发者尝试编写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现可以正常编译,但是永远无法被加载运行。 -**实现** +**(三)实现** ```java protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException{ @@ -664,7 +653,7 @@ protected synchronized Class loadClass(String name, boolean resolve) throws C } ``` -# JVM 参数 +# 四、JVM 参数 ## GC 优化配置 @@ -689,3 +678,9 @@ protected synchronized Class loadClass(String name, boolean resolve) throws C ```java java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar ``` + +# 参考资料 + +- 深入理解 Java 虚拟机 +- [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/) diff --git a/notes/Leetcode 题解.md b/notes/Leetcode 题解.md index 8ba46162..cf67cdca 100644 --- a/notes/Leetcode 题解.md +++ b/notes/Leetcode 题解.md @@ -1820,7 +1820,7 @@ public int minPathSum(int[][] grid) { 定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。 -


+

dp[N] 即为所求。 @@ -1849,7 +1849,7 @@ public int climbStairs(int n) { 第 i 年成熟的牛的数量为: -


+

**强盗抢劫** @@ -1859,7 +1859,7 @@ public int climbStairs(int n) { 定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 和 i - 3 的住户,所以 -


+

O(n) 空间复杂度实现方法: @@ -1938,7 +1938,7 @@ private int rob(int[] nums, int s, int e) { 综上所述,错误装信数量方式数量为: -


+

dp[N] 即为所求。 @@ -1954,7 +1954,7 @@ dp[N] 即为所求。 因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,因此需要对前面的求解方程做修改,令 dp[n] 最小为 1,即: -


+

对于一个长度为 N 的序列,最长子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,即 max{ dp[i] | 1 <= i <= N} 即为所求。 @@ -2050,7 +2050,7 @@ public int wiggleMaxLength(int[] nums) { 综上,最长公共子系列的状态转移方程为: -


+

对于长度为 N 的序列 S1 和 长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。 @@ -2087,7 +2087,7 @@ public int lengthOfLCS(int[] nums1, int[] nums2) { 综上,0-1 背包的状态转移方程为: -


+

```java public int knapsack(int W, int N, int[] weights, int[] values) { @@ -2111,7 +2111,7 @@ 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-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w] 防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。 diff --git a/notes/Linux.md b/notes/Linux.md index 341de0aa..c592bf24 100644 --- a/notes/Linux.md +++ b/notes/Linux.md @@ -1,121 +1,99 @@ -* [常用操作以及概念](#常用操作以及概念) +* [一、常用操作以及概念](#一常用操作以及概念) * [求助](#求助) * [关机](#关机) - * [查看进程](#查看进程) - * [查看端口](#查看端口) * [PATH](#path) * [运行等级](#运行等级) * [sudo](#sudo) * [GNU](#gnu) * [包管理工具](#包管理工具) - * [常见发行版本](#常见发行版本) -* [分区](#分区) + * [发行版](#发行版) + * [VIM 三个模式](#vim-三个模式) +* [二、分区](#二分区) * [磁盘的文件名](#磁盘的文件名) * [分区表](#分区表) - * [1. MBR](#1-mbr) - * [2. GPT](#2-gpt) * [开机检测程序](#开机检测程序) - * [1. BIOS](#1-bios) - * [2. UEFI](#2-uefi) * [挂载](#挂载) -* [文件权限与目录配置](#文件权限与目录配置) +* [三、文件](#三文件) * [文件权限概念](#文件权限概念) * [文件属性以及权限的修改](#文件属性以及权限的修改) - * [1. 修改文件所属群组](#1-修改文件所属群组) - * [2. 修改文件拥有者](#2-修改文件拥有者) - * [3. 修改权限](#3-修改权限) * [目录的权限](#目录的权限) * [文件默认权限](#文件默认权限) * [目录配置](#目录配置) -* [文件与目录](#文件与目录) * [文件时间](#文件时间) * [文件与目录的基本操作](#文件与目录的基本操作) - * [1. ls](#1-ls) - * [2. cp](#2-cp) - * [3. rm](#3-rm) - * [4. mv](#4-mv) * [获取文件内容](#获取文件内容) - * [1. cat](#1-cat) - * [2. tac](#2-tac) - * [3. more](#3-more) - * [4. less](#4-less) - * [5. head](#5-head) - * [6. tail](#6-tail) - * [7. od](#7-od) - * [8. touch](#8-touch) * [指令与文件搜索](#指令与文件搜索) - * [1. which](#1-which) - * [2. whereis](#2-whereis) - * [3. locate](#3-locate) - * [4. find](#4-find) - * [4.1 与时间有关的选项](#41-与时间有关的选项) - * [4.2 与文件拥有者和所属群组有关的选项](#42-与文件拥有者和所属群组有关的选项) - * [4.3 与文件权限和名称有关的选项](#43-与文件权限和名称有关的选项) -* [磁盘与文件系统](#磁盘与文件系统) +* [四、磁盘与文件系统](#四磁盘与文件系统) * [文件系统的组成](#文件系统的组成) * [inode](#inode) * [目录的 inode 与 block](#目录的-inode-与-block) * [实体链接与符号链接](#实体链接与符号链接) - * [1. 实体链接](#1-实体链接) - * [2. 符号链接](#2-符号链接) -* [压缩与打包](#压缩与打包) +* [五、压缩与打包](#五压缩与打包) * [压缩](#压缩) - * [1. gzip](#1-gzip) - * [2. bzip2](#2-bzip2) - * [3. xz](#3-xz) * [打包](#打包) -* [Bash](#bash) - * [Bash 特性](#bash-特性) +* [六、Bash](#六bash) + * [特性](#特性) * [变量操作](#变量操作) * [指令搜索顺序](#指令搜索顺序) * [数据流重定向](#数据流重定向) - * [管线指令](#管线指令) - * [1. 提取指令:cut](#1-提取指令cut) - * [2. 排序命令:sort、uniq](#2-排序命令sortuniq) - * [3. 双向输出重定向:tee](#3-双向输出重定向tee) - * [4. 字符转换指令:tr、col、expand、join、paste](#4-字符转换指令trcolexpandjoinpaste) - * [5. 分区指令:split](#5-分区指令split) -* [正规表示法与文件格式化处理](#正规表示法与文件格式化处理) +* [七、管线指令](#七管线指令) + * [提取指令](#提取指令) + * [排序指令](#排序指令) + * [双向输出重定向](#双向输出重定向) + * [字符转换指令](#字符转换指令) + * [分区指令](#分区指令) +* [八、正则表达式](#八正则表达式) * [grep](#grep) * [printf](#printf) * [awk](#awk) -* [vim 三个模式](#vim-三个模式) +* [九、进程管理](#九进程管理) + * [查看进程](#查看进程) + * [进程状态](#进程状态) + * [SIGCHILD](#sigchild) + * [孤儿进程和僵死进程](#孤儿进程和僵死进程) +* [十、I/O 复用](#十io-复用) + * [概念理解](#概念理解) + * [I/O 模型](#io-模型) + * [select poll epoll](#select-poll-epoll) + * [select 和 poll 比较](#select-和-poll-比较) + * [eopll 工作模式](#eopll-工作模式) + * [select poll epoll 应用场景](#select-poll-epoll-应用场景) * [参考资料](#参考资料) -# 常用操作以及概念 +# 一、常用操作以及概念 ## 求助 -**1. --help** +### 1. --help 指令的基本用法与选项介绍。 -**2. man** +### 2. man man 是 manual 的缩写,将指令的具体信息显示出来。 当执行 man date 时,有 DATE(1) 出现,其中的数字代表指令的类型,常用的数字及其类型如下: | 代号 | 类型 | -| -- | -- | +| :--: | -- | | 1 | 用户在 shell 环境中可以操作的指令或者可执行文件 | | 5 | 配置文件 | | 8 | 系统管理员可以使用的管理指令 | -**3. info** +### 3. info info 与 man 类似,但是 info 将文档分成一个个页面,每个页面可以进行跳转。 ## 关机 -**1. sync** +### 1. sync -为了加快对磁盘上文件的读写速度,位于内存中的文件数据不会立即同步到磁盘上,因此关机之前需要先进行 sync 同步操作。 +为了加快对磁盘文件的读写速度,位于内存中的文件数据不会立即同步到磁盘上,因此关机之前需要先进行 sync 同步操作。 -**2. shutdown** +### 2. shutdown ```html # /sbin/shutdown [-krhc] [时间] [警告讯息] @@ -125,22 +103,10 @@ info 与 man 类似,但是 info 将文档分成一个个页面,每个页面 -c : 取消已经在进行的 shutdown 指令内容 ``` -**3. 其它关机指令** +### 3. 其它关机指令 reboot、halt、poweroff。 -## 查看进程 - -```html -ps aux | grep threadx -``` - -## 查看端口 - -```html -netstat -anp | grep 80 -``` - ## PATH 可以在环境变量 PATH 中声明可执行文件的路径,路径之间用 : 分隔。 @@ -165,7 +131,7 @@ netstat -anp | grep 80 ## GNU -GNU 计划,又译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU,其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议,包含了以下内容: +GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU,其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议,包含了以下内容: - 以任何目的运行此程序的自由; - 再复制的自由; @@ -177,7 +143,7 @@ RPM 和 DPKG 为最常见的两类软件包管理工具。RPM 全称为 Redhat P YUM 基于 RPM 包管理工具,具有依赖管理功能,并具有软件升级的功能。 -## 常见发行版本 +## 发行版 Linux 发行版是 Linux 内核及各种应用软件的集成版本。 @@ -186,13 +152,30 @@ Linux 发行版是 Linux 内核及各种应用软件的集成版本。 | DPKG | Ubuntu | Debian | | RPM | Red Hat | Fedora / CentOS | -# 分区 +## VIM 三个模式 + +

+ +- 一般指令模式:进入 VIM 的默认模式,可以用于移动游标查看内容; +- 编辑模式:按下 "i" 等按键之后进入,可以对文本进行编辑; +- 指令列模式:按下 ":" 按键之后进入,用于保存退出等操作。 + +在指令列模式下,有以下命令用于离开或者保存文件。 + +| 命令 | 作用 | +| -- | -- | +| :w | 写入磁盘| +| :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 | +| :q | 离开 | +| :q! | 强制离开不保存 | +| :wq | 写入磁盘后离开 | +| :wq!| 强制写入磁盘后离开 | + +# 二、分区 ## 磁盘的文件名 -Linux 中每个硬件都被当做一个文件。 - -常见磁盘的文件名: +Linux 中每个硬件都被当做一个文件,包括磁盘。常见磁盘的文件名如下: - SCSI/SATA/USB 磁盘:/dev/sd[a-p] - IDE 磁盘:/dev/hd[a-d] @@ -205,7 +188,7 @@ Linux 中每个硬件都被当做一个文件。 ### 1. MBR -MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中 MBR 占 446 bytes,partition table 占 64 bytes。 +MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中 MBR 占 446 bytes,分区表占 64 bytes。 分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它将其它空间用来记录分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。 @@ -219,7 +202,7 @@ GPT 第 1 个区块记录了 MBR,紧接着是 33 个区块记录分区信息 GPT 没有扩展分区概念,都是主分区,最多可以分 128 个分区。 -

+

## 开机检测程序 @@ -229,10 +212,10 @@ BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可 MBR 中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动 MBR 中的开机管理程序时,就可以选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。 -

- 安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉 MBR,而 Linux 可以选择将开机管理程序安装在 MBR 或者其它分区的启动扇区,并且可以设置开机管理程序的选单。 +

+ ### 2. UEFI UEFI 相比于 BIOS 来说功能更为全面,也更为安全。 @@ -241,15 +224,15 @@ UEFI 相比于 BIOS 来说功能更为全面,也更为安全。 挂载利用目录作为分区的进入点,也就是说,进入目录之后就可以读取分区的数据。 -

+

-# 文件权限与目录配置 +# 三、文件 ## 文件权限概念 把用户分为三种:文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。 -使用 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:链接数; @@ -325,8 +308,8 @@ UEFI 相比于 BIOS 来说功能更为全面,也更为安全。 ## 文件默认权限 -文件默认权限:文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw- 。 -目录默认权限:目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。 +- 文件默认权限:文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw- 。 +- 目录默认权限:目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。 可以通过 umask 设置或者查看文件的默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r-- 。 @@ -338,11 +321,7 @@ UEFI 相比于 BIOS 来说功能更为全面,也更为安全。 - /usr (unix software resource):所有系统默认软件都会安装到这个目录; - /var (variable):存放系统或程序运行过程中的数据文件。 -完整的目录树如下: - -

- -# 文件与目录 +

## 文件时间 @@ -448,8 +427,8 @@ cp [-adfilprsu] source destination -a : 更新 atime -c : 更新 ctime,若该文件不存在则不建立新文件 -m : 更新 mtime --d : 后面可以接欲更新的日期而不用当前的日期,也可以使用 --date="日期或时间" --t : 后面可以接欲更新的时间而不用当前的时间,格式为[YYYYMMDDhhmm] +-d : 后面可以接更新日期而不使用当前日期,也可以使用 --date="日期或时间" +-t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm] ``` ## 指令与文件搜索 @@ -490,7 +469,7 @@ find 可以使用文件的属性和权限进行搜索。 # find filename [option] ``` -#### 4.1 与时间有关的选项 +**(一)与时间有关的选项** ```html -mtime n :列出在 n 天前的那一天修改过内容的文件 @@ -503,7 +482,7 @@ find 可以使用文件的属性和权限进行搜索。

-#### 4.2 与文件拥有者和所属群组有关的选项 +**(二)与文件拥有者和所属群组有关的选项** ```html -uid n @@ -514,7 +493,7 @@ find 可以使用文件的属性和权限进行搜索。 -nogroup:搜索所属群组不存在于 /etc/group 的文件 ``` -#### 4.3 与文件权限和名称有关的选项 +**(三)与文件权限和名称有关的选项** ```html -name filename @@ -525,7 +504,7 @@ find 可以使用文件的属性和权限进行搜索。 -perm /mode :搜索权限包含任一 mode 的文件 ``` -# 磁盘与文件系统 +# 四、磁盘与文件系统 ## 文件系统的组成 @@ -605,7 +584,7 @@ inode 具体包含以下信息: 53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab ``` -# 压缩与打包 +# 五、压缩与打包 ## 压缩 @@ -690,29 +669,21 @@ $ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩 | 查 看 | tar -jtv -f filename.tar.bz2 | | 解压缩 | tar -jxv -f filename.tar.bz2 -C 要解压缩的目录 | -# Bash +# 六、Bash 可以通过 Shell 请求内核提供服务,Bash 正是 Shell 的一种。 -## Bash 特性 +## 特性 -**1. 命令历史** +1. 命令历史:记录使用过的命令。本次登录所执行的命令都会暂时存放到内存中,\~/.bash_history 文件中记录的是前一次登录所执行过的命令。 -记录使用过的命令。本次登录所执行的命令都会暂时存放到内存中, \~/.bash_history 文件中记录的是前一次登录所执行过的命令。 +2. 命令与文件补全:快捷键:tab。 -**2. 命令与文件补全** +3. 命名别名:例如 lm 是 ls -al 的别名。 -快捷键:tab +4. shell scripts。 -**3. 命名别名** - -例如 lm 是 ls -al 的别名。 - -**4. shell scripts** - -**5. 通配符** - -例如 ls -l /usr/bin/X\* 列出 /usr/bin 下面所有以 X 开头的文件。 +5. 通配符:例如 ls -l /usr/bin/X\* 列出 /usr/bin 下面所有以 X 开头的文件。 ## 变量操作 @@ -726,7 +697,10 @@ $ 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。 @@ -759,11 +733,13 @@ $ echo ${array[1]} ## 数据流重定向 -重定向就是使用文件代替标准输入、标准输出和标准错误输出。 +重定向指的是使用文件代替标准输入、标准输出和标准错误输出。 -1. 标准输入 (stdin)       :代码为 0 ,使用 < 或 << ; -2. 标准输出 (stdout)     :代码为 1 ,使用 > 或 >> ; -3. 标准错误输出 (stderr):代码为 2 ,使用 2> 或 2>> ; +| 1 | 代码 | 运算符 | +| :---: | :---: | :---:| +| 标准输入 (stdin) | 0 | < 或 << | +| 标准输出 (stdout) | 1 | > 或 >> | +| 标准错误输出 (stderr) | 2 | 2> 或 2>> | 其中,有一个箭头的表示以覆盖的方式重定向,而有两个箭头的表示以追加的方式重定向。 @@ -775,7 +751,7 @@ $ echo ${array[1]} $ find /home -name .bashrc > list 2>&1 ``` -## 管线指令 +# 七、管线指令 管线是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管线。在命令之间使用 | 分隔各个管线命令。 @@ -783,7 +759,7 @@ $ find /home -name .bashrc > list 2>&1 $ ls -al /etc | less ``` -### 1. 提取指令:cut +## 提取指令 cut 对数据进行切分,取出想要的部分。提取过程一行一行地进行。 @@ -818,7 +794,7 @@ declare -x HOSTNAME="study.centos.vbird" $ export | cut -c 12 ``` -### 2. 排序命令:sort、uniq +## 排序指令 **sort** 进行排序。 @@ -864,7 +840,7 @@ $ last | cut -d ' ' -f 1 | sort | uniq -c 1 wtmp ``` -### 3. 双向输出重定向:tee +## 双向输出重定向 输出重定向会将输出内容重定向到文件中,而 **tee** 不仅能够完成这个功能,还能保留屏幕上的输出。也就是说,使用 tee 指令,一个输出会同时传送到文件和屏幕上。 @@ -872,7 +848,7 @@ $ last | cut -d ' ' -f 1 | sort | uniq -c $ tee [-a] file ``` -### 4. 字符转换指令:tr、col、expand、join、paste +## 字符转换指令 **tr** 用来删除一行中的字符,或者对字符进行替换。 @@ -918,7 +894,7 @@ $ paste [-d] file1 file2 -d :分隔符,默认为 tab ``` -### 5. 分区指令:split +## 分区指令 **split** 将一个文件划分成多个文件。 @@ -929,7 +905,7 @@ $ split [-bl] file PREFIX - PREFIX :分区文件的前导名称 ``` -# 正规表示法与文件格式化处理 +# 八、正则表达式 ## grep @@ -956,7 +932,7 @@ $ grep -n 'the' regular_express.txt 18:google is the best tools for search keyword ``` -因为 { 与 } 的符号在 shell 是有特殊意义的,因此必须要使用使用转义字符进行转义。 +因为 { 与 } 的符号在 shell 是有特殊意义的,因此必须要使用转义字符进行转义。 ```html $ grep -n 'go\{2,5\}g' regular_express.txt @@ -980,7 +956,7 @@ $ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt) 可以根据字段的某些条件进行匹配,例如匹配字段小于某个值的那一行数据。 ```html -$ awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename +$ awk ' 条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename ``` awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$n,n 为字段号,从 1 开始,\$0 表示一整行。 @@ -1022,28 +998,401 @@ dmtsai lines: 5 columns: 9 范例 3:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。 ```text -cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}' +$ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}' root 0 bin 1 daemon 2 ``` -# vim 三个模式 +# 九、进程管理 -

+## 查看进程 -在指令列模式下,有以下命令用于离开或者存储文件。 +### 1. ps -| 命令 | 作用 | -| -- | -- | -| :w | 写入磁盘| -| :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 | -| :q | 离开 | -| :q! | 强制离开不保存 | -| :wq | 写入磁盘后离开 | -| :wq!| 强制写入磁盘后离开 | +查看某个时间点的进程信息 + +示例一:查看自己的进程 + +``` +# ps -l +``` + +示例二:查看系统所有进程 + +``` +# ps aux +``` + +示例三:查看特定的进程 + +```html +# ps aux | grep threadx +``` + +### 2. top + +实时显示进程信息 + +示例:两秒钟刷新一次 + +``` +# top -d 2 +``` + +### 3. pstree + +查看进程树 + +示例:查看所有进程树 + +``` +# pstree -A +``` + +### 4. netstat + +查看占用端口的进程 + +``` +# 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| + +

+ +## SIGCHILD + +当一个子进程改变了它的状态时:停止运行,继续运行或者退出,有两件事会发生在父进程中: + +- 得到 SIGCHLD 信号; +- 阻塞的 waitpid(2)(或者 wait)调用会返回。 + +

+ +## 孤儿进程和僵死进程 + +### 1. 孤儿进程 + +一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。 + +由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。 + +### 2. 僵死进程 + +一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait 或 waitpid 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait 或 waitpid,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。 + +僵死进程通过 ps 命令显示出来的状态为 Z。 + +系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。 + +要消灭系统中大量的僵死进程,只需要将其父进程杀死,此时所有的僵死进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵死进程所占有的资源,从而结束僵死进程。 + +# 十、I/O 复用 + +## 概念理解 + +I/O Multiplexing 又被称为 Event Driven I/O,它可以让单个进程具有处理多个 I/O 事件的能力。 + +当某个 I/O 事件条件满足时,进程会收到通知。 + +如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时连接几万个连接,那么就需要创建相同数量的线程。并且相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。 + +## I/O 模型 + +- 阻塞(Blocking) +- 非阻塞(Non-blocking) +- 同步(Synchronous) +- 异步(Asynchronous) + +阻塞非阻塞是等待 I/O 完成的方式,阻塞要求用户程序停止执行,直到 I/O 完成,而非阻塞在 I/O 完成之前还可以继续执行。 + +同步异步是获知 I/O 完成的方式,同步需要时刻关心 I/O 是否已经完成,异步无需主动关心,在 I/O 完成时它会收到通知。 + +

+ +### 1. 同步-阻塞 + +这是最常见的一种模型,用户程序在使用 read() 时会执行系统调用从而陷入内核,之后就被阻塞直到系统调用完成。 + +应该注意到,在阻塞的过程中,其他程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。 + +

+ +### 2. 同步-非阻塞 + +非阻塞意味着用户程序在执行系统调用后还可以执行,内核并不是马上执行完 I/O,而是以一个错误码来告知用户程序 I/O 还未完成。为了获得 I/O 完成事件,用户程序必须调用多次系统调用去询问内核,甚至是忙等,也就是在一个循环里面一直询问并等待。 + +由于 CPU 要处理更多的用户程序的询问,因此这种模型的效率是比较低的。 + +

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

+ +### 4. 异步-非阻塞 + +该模式下,I/O 操作会立即返回,之后可以处理其它操作,并且在 I/O 完成时会收到一个通知,此时会中断正在处理的操作,然后完成 I/O 事务。 + +

+ +## select poll epoll + +这三个都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。可以说,新出现的实现是为了修复旧实现的不足。 + +### 1. select + +```c +int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); +``` + +- fd_set 表示描述符集合; +- readset、writeset 和 exceptset 这三个参数指定让操作系统内核测试读、写和异常条件的描述符; +- timeout 参数告知内核等待所指定描述符中的任何一个就绪可花多少时间; +- 成功调用返回结果大于 0;出错返回结果为 -1;超时返回结果为 0。 + +```c +fd_set fd_in, fd_out; +struct timeval tv; + +// Reset the sets +FD_ZERO( &fd_in ); +FD_ZERO( &fd_out ); + +// Monitor sock1 for input events +FD_SET( sock1, &fd_in ); + +// 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; + +// 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 ); + +// 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( sock2, &fd_out ) ) + // output event on sock2 +} +``` + +每次调用 select() 都需要将 fd_set \*readfds, fd_set \*writefds, fd_set \*exceptfds 链表内容全部从用户进程内存中复制到操作系统内核中,内核需要将所有 fd_set 遍历一遍,这个过程非常低效。 + +返回结果中内核并没有声明哪些 fd_set 已经准备好了,所以如果返回值大于 0 时,程序需要遍历所有的 fd_set 判断哪个 I/O 已经准备好。 + +在 Linux 中 select 最多支持 1024 个 fd_set 同时轮询,其中 1024 由 Linux 内核的 FD_SETSIZE 决定。如果需要打破该限制可以修改 FD_SETSIZE,然后重新编译内核。 + +### 2. poll + +```c +int poll (struct pollfd *fds, unsigned int nfds, int timeout); +``` + +```c +struct pollfd { + int fd; //文件描述符 + short events; //监视的请求事件 + short revents; //已发生的事件 +}; +``` + +```c +// The structure for two events +struct pollfd fds[2]; + +// Monitor sock1 for input +fds[0].fd = sock1; +fds[0].events = POLLIN; + +// 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 +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 ( pfd[1].revents & POLLOUT ) + pfd[1].revents = 0; + // output event on sock2 +} +``` + +它和 select() 功能基本相同。同样需要每次将 struct pollfd \*fds 复制到内核,返回后同样需要进行轮询每一个 pollfd 是否已经 I/O 准备好。poll() 取消了 1024 个描述符数量上限,但是数量太大以后不能保证执行效率,因为复制大量内存到内核十分低效,所需时间与描述符数量成正比。poll() 在 pollfd 的重复利用上比 select() 的 fd_set 会更好。 + +如果在多线程下,如果一个线程对某个描述符调用了 poll() 系统调用,但是另一个线程关闭了该描述符,会导致 poll() 调用结果不确定,该问题同样出现在 select() 中。 + +### 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); +``` + +```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 ); + +if ( pollingfd < 0 ) + // report error + +// 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; + +// 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 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(); + } + } +} +``` + +epoll 仅仅适用于 Linux OS。 + +它是 select 和 poll 的增强版,更加灵活而且没有描述符限制。它将用户关心的描述符放到内核的一个事件表中,从而只需要在用户空间和内核空间拷贝一次。 + +select 和 poll 方式中,进程只有在调用一定的方法后,内核才对所有监视的描述符进行扫描。而 epoll 事先通过 epoll_ctl() 来注册描述符,一旦基于某个描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个描述符,当进程调用 epoll_wait() 时便得到通知。 + +新版本的 epoll_create(int size) 参数 size 不起任何作用,在旧版本的 epoll 中如果描述符的数量大于 size,不保证服务质量。 + +epoll_ct() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。 + +epoll_wait() 取出在内核中通过链表维护的 I/O 准备好的描述符,将他们从内核复制到程序中,不需要像 select/poll 对注册的所有描述符遍历一遍。 + +epoll 对多线程编程更有友好,同时多个线程对同一个描述符调用了 epoll_wait 也不会产生像 select/poll 的不确定情况。或者一个线程调用了 epoll_wait 另一个线程关闭了同一个描述符也不会产生不确定情况。 + +## select 和 poll 比较 + +### 1. 功能 + +它们提供了几乎相同的功能,但是在一些细节上有所不同: + +- select 会修改 fd_set 参数,而 poll 不会; +- select 默认只能监听 1024 个描述符,如果要监听更多的话,需要修改 FD_SETSIZE 之后重新编译; +- poll 提供了更多的事件类型。 + +### 2. 速度 + +poll 和 select 在速度上都很慢。 + +- 它们都采取轮询的方式来找到 I/O 完成的描述符,如果描述符很多,那么速度就会很慢; +- select 只使用每个描述符的 3 位,而 poll 通常需要使用 64 位,因此 poll 需要复制更多的内核空间。 + +### 3. 可移植性 + +几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。 + +## eopll 工作模式 + +epoll_event 有两种触发模式:LT(level trigger)和 ET(edge trigger)。 + +### 1. LT 模式 + +当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait() 时,会再次响应应用程序并通知此事件。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。 + +### 2. ET 模式 + +当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait() 时,不会再次响应应用程序并通知此事件。很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。 + +## select poll epoll 应用场景 + +很容易产生一种错觉认为只要用 epoll 就可以了,select poll 都是历史遗留问题,并没有什么应用场景,其实并不是这样的。 + +### 1. select 应用场景 + +select() poll() epoll_wait() 都有一个 timeout 参数,在 select() 中 timeout 的精确度为 1ns,而 poll() 和 epoll_wait() 中则为 1ms。所以 select 更加适用于实时要求更高的场景,比如核反应堆的控制。 + +select 历史更加悠久,它的可移植性更好,几乎被所有主流平台所支持。 + +### 2. poll 应用场景 + +poll 没有最大描述符数量的限制,如果平台支持应该采用 poll 且对实时性要求并不是十分严格,而不是 select。 + +需要同时监控小于 1000 个描述符。那么也没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。 + +需要监控的描述符状态变化多,而且都是非常短暂的。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。epoll 的描述符存储在内核,不容易调试。 + +### 3. epoll 应用场景 + +程序只需要运行在 Linux 平台上,有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。 + +### 4. 性能对比 + +> [epoll Scalability Web Page](http://lse.sourceforge.net/epoll/index.html) # 参考资料 - 鸟哥. 鸟 哥 的 Linux 私 房 菜 基 础 篇 第 三 版[J]. 2009. - [Linux 平台上的软件包管理](https://www.ibm.com/developerworks/cn/linux/l-cn-rpmdpkg/index.html) +- [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) +- [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/) diff --git a/notes/MySQL.md b/notes/MySQL.md index 98a8fde3..fca2798f 100644 --- a/notes/MySQL.md +++ b/notes/MySQL.md @@ -1,57 +1,33 @@ -* [存储引擎](#存储引擎) - * [1. InnoDB](#1-innodb) - * [2. MyISAM](#2-myisam) - * [3. InnoDB 与 MyISAM 的比较](#3-innodb-与-myisam-的比较) -* [数据类型](#数据类型) - * [1. 整型](#1-整型) - * [2. 浮点数](#2-浮点数) - * [3. 字符串](#3-字符串) - * [4. 时间和日期](#4-时间和日期) -* [索引](#索引) - * [1. 索引分类](#1-索引分类) - * [1.1 B-Tree 索引](#11-b-tree-索引) - * [1.2 哈希索引](#12-哈希索引) - * [1.3. 空间索引(R-Tree)](#13-空间索引r-tree) - * [1.4 全文索引](#14-全文索引) - * [2. 索引的优点](#2-索引的优点) - * [3. 索引优化](#3-索引优化) - * [3.1 独立的列](#31-独立的列) - * [3.2 前缀索引](#32-前缀索引) - * [3.3 多列索引](#33-多列索引) - * [3.4 索引列的顺序](#34-索引列的顺序) - * [3.5 聚簇索引](#35-聚簇索引) - * [3.6 覆盖索引](#36-覆盖索引) - * [4. B-Tree 和 B+Tree 原理](#4-b-tree-和-b+tree-原理) - * [4. 1 B-Tree](#4-1-b-tree) - * [4.2 B+Tree](#42-b+tree) - * [4.3 带有顺序访问指针的 B+Tree](#43-带有顺序访问指针的-b+tree) - * [4.4 为什么使用 B-Tree 和 B+Tree](#44-为什么使用-b-tree-和-b+tree) -* [查询性能优化](#查询性能优化) - * [1. Explain](#1-explain) - * [2. 减少返回的列](#2-减少返回的列) - * [3. 减少返回的行](#3-减少返回的行) - * [4. 拆分大的 DELETE 或 INSERT 语句](#4-拆分大的-delete-或-insert-语句) -* [分库与分表](#分库与分表) - * [1. 分库与分表的原因](#1-分库与分表的原因) - * [2. 实现方式](#2-实现方式) - * [2.1 垂直切分](#21-垂直切分) - * [2.2 水平切分](#22-水平切分) - * [2.3 切分的选择](#23-切分的选择) - * [3. Merge 存储引擎](#3-merge-存储引擎) - * [4. 分库与分表存在的问题](#4-分库与分表存在的问题) - * [4.1 事务问题](#41-事务问题) - * [4.2 跨库跨表连接问题](#42-跨库跨表连接问题) - * [4.3 额外的数据管理负担和数据运算压力](#43-额外的数据管理负担和数据运算压力) - * [5. 分表与分区的不同](#5-分表与分区的不同) -* [故障转移和故障恢复](#故障转移和故障恢复) +* [一、存储引擎](#一存储引擎) + * [InnoDB](#innodb) + * [MyISAM](#myisam) + * [比较](#比较) +* [二、数据类型](#二数据类型) + * [整型](#整型) + * [浮点数](#浮点数) + * [字符串](#字符串) + * [时间和日期](#时间和日期) +* [三、索引](#三索引) + * [索引分类](#索引分类) + * [索引的优点](#索引的优点) + * [索引优化](#索引优化) + * [B-Tree 和 B+Tree 原理](#b-tree-和-b+tree-原理) +* [四、查询性能优化](#四查询性能优化) +* [五、分库与分表](#五分库与分表) + * [原因](#原因) + * [实现方式](#实现方式) + * [Merge 存储引擎](#merge-存储引擎) + * [存在的问题](#存在的问题) + * [分表与分区的不同](#分表与分区的不同) +* [六、故障转移和故障恢复](#六故障转移和故障恢复) * [参考资料](#参考资料) -# 存储引擎 +# 一、存储引擎 -## 1. InnoDB +## InnoDB InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。 @@ -63,7 +39,7 @@ InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支 通过一些机制和工具支持真正的热备份。 -## 2. MyISAM +## MyISAM MyISAM 提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等。但 MyISAM 不支持事务和行级锁,而且崩溃后无法安全恢复。 @@ -81,43 +57,29 @@ MyISAM 提供了大量的特性,包括全文索引、压缩、空间函数(G MyISAM 设计简单,数据以紧密格式存储,所以在某些场景下性能很好。 -## 3. InnoDB 与 MyISAM 的比较 +## 比较 -**事务** +1. 事务:InnoDB 是事务型的。 +2. 备份:InnoDB 支持在线热备份。 +3. 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 +4. 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 +5. 其它特性:MyISAM 支持全文索引,地理空间索引。 -InnoDB 是事务型的。 +# 二、数据类型 -**备份** - -InnoDB 支持在线热备份。 - -**崩溃恢复** - -MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 - -**并发** - -MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 - -**其它特性** - -MyISAM 支持全文索引,地理空间索引。 - -# 数据类型 - -## 1. 整型 +## 整型 TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。 INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。 -## 2. 浮点数 +## 浮点数 FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。 FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。 -## 3. 字符串 +## 字符串 主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。 @@ -125,11 +87,11 @@ VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内 VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。 -## 4. 时间和日期 +## 时间和日期 MySQL 提供了两种相似的日期时间类型:DATATIME 和 TIMESTAMP。 -**DATATIME** +### 1. DATATIME 能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。 @@ -137,7 +99,7 @@ MySQL 提供了两种相似的日期时间类型:DATATIME 和 TIMESTAMP。 默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATATIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。 -**TIMESTAMP** +### 2. TIMESTAMP 和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。 @@ -149,7 +111,7 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提 应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。 -# 索引 +# 三、索引 索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 @@ -157,9 +119,9 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。对于中到大型的表,索引就非常有效。但是对于特大型的表,建立和使用索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。 -## 1. 索引分类 +## 索引分类 -### 1.1 B-Tree 索引 +### 1. B-Tree 索引 B-Tree 索引是大多数 MySQL 存储引擎的默认索引类型。 @@ -171,7 +133,7 @@ B-Tree 索引是大多数 MySQL 存储引擎的默认索引类型。 如果不是按照索引列的顺序进行查找,则无法使用索引。 -### 1.2 哈希索引 +### 2. 哈希索引 基于哈希表实现,优点是查找非常快。 @@ -181,19 +143,19 @@ InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个 限制:哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;无法用于分组与排序;只支持精确查找,无法用于部分查找和范围查找;如果哈希冲突很多,查找速度会变得很慢。 -### 1.3. 空间索引(R-Tree) +### 3. 空间索引(R-Tree) MyISAM 存储引擎支持空间索引,可以用于地理数据存储。 空间索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 -### 1.4 全文索引 +### 4. 全文索引 MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较索引中的值。 使用 MATCH AGAINST,而不是普通的 WHERE。 -## 2. 索引的优点 +## 索引的优点 - 大大减少了服务器需要扫描的数据量; @@ -201,9 +163,9 @@ MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而 - 将随机 I/O 变为顺序 I/O。 -## 3. 索引优化 +## 索引优化 -### 3.1 独立的列 +### 1. 独立的列 在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。 @@ -213,13 +175,13 @@ MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而 SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5; ``` -### 3.2 前缀索引 +### 2. 前缀索引 对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。 对于前缀长度的选取需要根据 **索引选择性** 来确定:不重复的索引值和记录总数的比值。选择性越高,查询效率也越高。最大值为 1 ,此时每个记录都有唯一的索引与其对应。 -### 3.3 多列索引 +### 3. 多列索引 在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor_id 和 film_id 设置为多列索引。 @@ -228,7 +190,7 @@ SELECT film_id, actor_ id FROM sakila.film_actor WhERE actor_id = 1 AND film_id = 1; ``` -### 3.4 索引列的顺序 +### 4. 索引列的顺序 让选择性最强的索引列放在前面,例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。 @@ -245,7 +207,7 @@ customer_id_selectivity: 0.0373 COUNT(*): 16049 ``` -### 3.5 聚簇索引 +### 5. 聚簇索引

@@ -255,12 +217,12 @@ customer_id_selectivity: 0.0373 因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。 -**优点** +#### 优点 1. 可以把相关数据保存在一起,减少 I/O 操作; 2. 因为数据保存在 B-Tree 中,因此数据访问更快。 -**缺点** +#### 缺点 1. 聚簇索引最大限度提高了 I/O 密集型应用的性能,但是如果数据全部放在内存,就没必要用聚簇索引。 2. 插入速度严重依赖于插入顺序,按主键的顺序插入是最快的。 @@ -268,19 +230,19 @@ customer_id_selectivity: 0.0373 4. 当插入到某个已满的页中,存储引擎会将该页分裂成两个页面来容纳该行,页分裂会导致表占用更多的磁盘空间。 5. 如果行比较稀疏,或者由于页分裂导致数据存储不连续时,聚簇索引可能导致全表扫描速度变慢。 -### 3.6 覆盖索引 +### 6. 覆盖索引 索引包含所有需要查询的字段的值。 -**优点** +#### 优点 1. 因为索引条目通常远小于数据行的大小,所以若只读取索引,能大大减少数据访问量。 2. 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。 3. 对于 InnoDB 引擎,若二级索引能够覆盖查询,则无需访问聚簇索引。 -## 4. B-Tree 和 B+Tree 原理 +## B-Tree 和 B+Tree 原理 -### 4. 1 B-Tree +### 1. B-Tree

@@ -296,7 +258,7 @@ B-Tree 是满足下列条件的数据结构: 由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。 -### 4.2 B+Tree +### 2. B+Tree

@@ -305,13 +267,13 @@ B-Tree 是满足下列条件的数据结构: - 每个节点的指针上限为 2d 而不是 2d+1; - 内节点不存储 data,只存储 key,叶子节点不存储指针。 -### 4.3 带有顺序访问指针的 B+Tree +### 3. 带有顺序访问指针的 B+Tree

一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。 -### 4.4 为什么使用 B-Tree 和 B+Tree +### 4. 为什么使用 B-Tree 和 B+Tree 红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用 B-/+Tree 作为索引结构。 @@ -321,9 +283,9 @@ B-Tree 是满足下列条件的数据结构: B+Tree 更适合外存索引,原因和内节点出度 d 有关。由于 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,拥有更好的性能。 -# 查询性能优化 +# 四、查询性能优化 -## 1. Explain +### Explain 用来分析 SQL 语句,分析结果中比较重要的字段有: @@ -333,13 +295,13 @@ B+Tree 更适合外存索引,原因和内节点出度 d 有关。由于 B+Tree - rows : 扫描的行数 -## 2. 减少返回的列 +### 减少返回的列 慢查询主要是因为访问了过多数据,除了访问过多行之外,也包括访问过多列。 最好不要使用 SELECT * 语句,要根据需要选择查询的列。 -## 3. 减少返回的行 +### 减少返回的行 最好使用 LIMIT 语句来取出想要的那些行。 @@ -349,7 +311,7 @@ B+Tree 更适合外存索引,原因和内节点出度 d 有关。由于 B+Tree SELECT * FROM sakila.film_actor WHERE film_id = 1; ``` -## 4. 拆分大的 DELETE 或 INSERT 语句 +### 拆分大的 DELETE 或 INSERT 语句 如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。 @@ -364,73 +326,72 @@ do { } while rows_affected > 0 ``` -# 分库与分表 +# 五、分库与分表 -## 1. 分库与分表的原因 +## 原因 随着时间和业务的发展,数据库中的表会越来越多,并且表中的数据量也会越来越大,那么读写操作的开销也会随着增大。 -## 2. 实现方式 +## 实现方式 -### 2.1 垂直切分 +### 1. 垂直切分 将表按功能模块、关系密切程度划分出来,部署到不同的库上。例如,我们会建立商品数据库 payDB、用户数据库 userDB 等,分别用来存储项目与商品有关的表和与用户有关的表。 -### 2.2 水平切分 +### 2. 水平切分 把表中的数据按照某种规则存储到多个结构相同的表中,例如按 id 的散列值、性别等进行划分。 -### 2.3 切分的选择 +### 3. 切分的选择 如果数据库中的表太多,并且项目各项业务逻辑清晰,那么垂直切分是首选。 如果数据库的表不多,但是单表的数据量很大,应该选择水平切分。 -## 3. Merge 存储引擎 +## Merge 存储引擎 该存储引擎支持分表。 -## 4. 分库与分表存在的问题 +## 存在的问题 -### 4.1 事务问题 +### 1. 事务问题 在执行分库分表之后,由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。 -### 4.2 跨库跨表连接问题 +### 2. 跨库跨表连接问题 在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上。这时,表的连接操作将受到限制,我们无法连接位于不同分库的表,也无法连接分表粒度不同的表,导致原本只需要一次查询就能够完成的业务需要进行多次才能完成。 -### 4.3 额外的数据管理负担和数据运算压力 +### 3. 额外的数据管理负担和数据运算压力 最显而易见的就是数据的定位问题和数据的增删改查的重复执行问题,这些都可以通过应用程序解决,但必然引起额外的逻辑运算。 -## 5. 分表与分区的不同 +## 分表与分区的不同 分表,就是将一张表分成多个小表,这些小表拥有不同的表名;而分区是将一张表的数据分为多个区块,这些区块可以存储在同一个磁盘上,也可以存储在不同的磁盘上,这种方式下表仍然只有一个。 -# 故障转移和故障恢复 +# 六、故障转移和故障恢复 故障转移也叫做切换,当主库出现故障时就切换到备库,使备库成为主库。故障恢复顾名思义就是从故障中恢复过来,并且保证数据的正确性。 -**提升备库或切换角色** +### 提升备库或切换角色 提升一台备库为主库,或者在一个主-主复制结构中调整主动和被动角色。 -**虚拟 IP 地址和 IP 托管** +### 虚拟 IP 地址和 IP 托管 为 MySQL 实例指定一个逻辑 IP 地址,当 MySQL 实例失效时,可以将 IP 地址转移到另一台 MySQL 服务器上。 -**中间件解决方案** +### 中间件解决方案 通过代理,可以路由流量到可以使用的服务器上。

-**在应用中处理故障转移** +### 在应用中处理故障转移 将故障转移整合到应用中可能导致应用变得太过笨拙。 - # 参考资料 - 高性能 MySQL diff --git a/notes/Redis.md b/notes/Redis.md index 82373006..b882428b 100644 --- a/notes/Redis.md +++ b/notes/Redis.md @@ -1,29 +1,40 @@ -* [Redis 是什么](#redis-是什么) -* [Redis 的五种基本类型](#redis-的五种基本类型) - * [STRING](#string) - * [LIST](#list) - * [SET](#set) - * [HASH](#hash) - * [ZSET](#zset) -* [键的过期时间](#键的过期时间) -* [发布与订阅](#发布与订阅) -* [事务](#事务) -* [持久化](#持久化) - * [快照持久化](#快照持久化) - * [AOF 持久化](#aof-持久化) -* [复制](#复制) +* [一、Redis 是什么](#一redis-是什么) +* [二、五种基本类型](#二五种基本类型) + * [1. STRING](#1-string) + * [2. LIST](#2-list) + * [3. SET](#3-set) + * [4. HASH](#4-hash) + * [5. ZSET](#5-zset) +* [三、键的过期时间](#三键的过期时间) +* [四、发布与订阅](#四发布与订阅) +* [五、事务](#五事务) +* [六、持久化](#六持久化) + * [1. 快照持久化](#1-快照持久化) + * [2. AOF 持久化](#2-aof-持久化) +* [七、复制](#七复制) * [从服务器连接主服务器的过程](#从服务器连接主服务器的过程) * [主从链](#主从链) -* [处理故障](#处理故障) -* [分片](#分片) -* [事件](#事件) +* [八、处理故障](#八处理故障) +* [九、分片](#九分片) + * [1. 客户端分片](#1-客户端分片) + * [2. 代理分片](#2-代理分片) + * [3. 服务器分片](#3-服务器分片) +* [十、事件](#十事件) * [事件类型](#事件类型) * [事件的调度与执行](#事件的调度与执行) -* [Redis 与 Memcached 的区别](#redis-与-memcached-的区别) -* [Redis 适用场景](#redis-适用场景) -* [数据淘汰策略](#数据淘汰策略) -* [一个简单的论坛系统分析](#一个简单的论坛系统分析) +* [十一、Redis 与 Memcached 的区别](#十一redis-与-memcached-的区别) + * [数据类型](#数据类型) + * [数据持久化](#数据持久化) + * [分布式](#分布式) + * [内存管理机制](#内存管理机制) +* [十二、Redis 适用场景](#十二redis-适用场景) + * [缓存](#缓存) + * [消息队列](#消息队列) + * [计数器](#计数器) + * [好友关系](#好友关系) +* [十三、数据淘汰策略](#十三数据淘汰策略) +* [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析) * [文章信息](#文章信息) * [点赞功能](#点赞功能) * [对文章进行排序](#对文章进行排序) @@ -31,7 +42,7 @@ -# Redis 是什么 +# 一、Redis 是什么 Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。 @@ -39,7 +50,7 @@ Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以 Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。 -# Redis 的五种基本类型 +# 二、五种基本类型 | 数据类型 | 可以存储的值 | 操作 | | -- | -- | -- | @@ -51,7 +62,7 @@ Redis 支持很多特性,例如将内存中的数据持久化到硬盘中, > [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 +## 1. STRING

@@ -66,7 +77,7 @@ OK (nil) ``` -## LIST +## 2. LIST

@@ -94,7 +105,7 @@ OK 2) "item" ``` -## SET +## 3. SET

@@ -128,7 +139,7 @@ OK 2) "item3" ``` -## HASH +## 4. HASH

@@ -159,8 +170,7 @@ OK 2) "value1" ``` -## ZSET - +## 5. ZSET

@@ -192,7 +202,7 @@ OK 2) "982" ``` -# 键的过期时间 +# 三、键的过期时间 Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。 @@ -200,7 +210,7 @@ Redis 可以为每个键设置过期时间,当键过期时,会自动删除 过期时间对于清理缓存数据非常有用。 -# 发布与订阅 +# 四、发布与订阅 发布与订阅实际上是观察者模式,订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。 @@ -209,17 +219,17 @@ Redis 可以为每个键设置过期时间,当键过期时,会自动删除 1. 如果订阅者读取消息的速度很慢,会使得消息不断积压在发布者的输出缓存区中,造成内存占用过多; 2. 如果订阅者在执行订阅的过程中网络出现问题,那么就会丢失断线期间发送的所有消息。 -# 事务 +# 五、事务 Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。 MULTI 和 EXEC 中的操作将会一次性发送给服务器,而不是一条一条发送,这种方式称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。 -# 持久化 +# 六、持久化 Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。 -## 快照持久化 +## 1. 快照持久化 将某个时间点的所有数据都存放到硬盘上。 @@ -227,7 +237,7 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 如果系统发生故障,将会丢失最后一次创建快照之后的数据。并且如果数据量很大,保存快照的时间也会很长。 -## AOF 持久化 +## 2. AOF 持久化 AOF 持久化将写命令添加到 AOF 文件(Append Only File)的末尾。 @@ -243,7 +253,7 @@ always 选项会严重减低服务器的性能;everysec 选项比较合适, 随着服务器写请求的增多,AOF 文件会越来越大;Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。 -# 复制 +# 七、复制 通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。 @@ -263,7 +273,7 @@ always 选项会严重减低服务器的性能;everysec 选项比较合适,

-# 处理故障 +# 八、处理故障 要用到持久化文件来恢复服务器的数据。 @@ -271,33 +281,33 @@ always 选项会严重减低服务器的性能;everysec 选项比较合适, 当主服务器出现故障时,Redis 常用的做法是新开一台服务器作为主服务器,具体步骤如下:假设 A 为主服务器,B 为从服务器,当 A 出现故障时,让 B 生成一个快照文件,将快照文件发送给 C,并让 C 恢复快照文件的数据。最后,让 B 成为 C 的从服务器。 -# 分片 +# 九、分片 Redis 中的分片类似于 MySQL 的分表操作,分片是将数据划分为多个部分的方法,对数据的划分可以基于键包含的 ID、基于键的哈希值,或者基于以上两者的某种组合。通过对数据进行分片,用户可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。 假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 -**客户端分片** +## 1. 客户端分片 客户端使用一致性哈希等算法决定键应当分布到哪个节点。 -**代理分片** +## 2. 代理分片 将客户端请求发送到代理上,由代理转发请求到正确的节点上。 -**服务器分片** +## 3. 服务器分片 Redis Cluster。 -# 事件 +# 十、事件 ## 事件类型 -**文件事件** +### 1. 文件事件 服务器有许多套接字,事件产生时会对这些套接字进行操作,服务器通过监听套接字来处理事件。常见的文件事件有:客户端的连接事件;客户端的命令请求事件;服务器向客户端返回命令结果的事件; -**时间事件** +### 2. 时间事件 又分为两类:定时事件是让一段程序在指定的时间之内执行一次;周期性时间是让一段程序每隔指定时间就执行一次。 @@ -353,49 +363,49 @@ def main():

-# Redis 与 Memcached 的区别 +# 十一、Redis 与 Memcached 的区别 两者都是非关系型内存键值数据库。有以下主要不同: -**数据类型** +## 数据类型 Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。 -**数据持久化** +## 数据持久化 Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。 -**分布式** +## 分布式 Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 Redis Cluster 实现了分布式的支持。 -**内存管理机制** +## 内存管理机制 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。 Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 -# Redis 适用场景 +# 十二、Redis 适用场景 -**缓存** +## 缓存 适用 Redis 作为缓存,将热点数据放到内存中。 -**消息队列** +## 消息队列 Redis 的 List 类型是双向链表,很适合用于消息队列。 -**计数器** +## 计数器 Redis 这种内存数据库才能支持计数器的频繁读写操作。 -**好友关系** +## 好友关系 使用 set 类型的交集很容易就可以知道两个用户的共同好友。 -# 数据淘汰策略 +# 十三、数据淘汰策略 可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。 @@ -410,7 +420,7 @@ Redis 这种内存数据库才能支持计数器的频繁读写操作。 如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 -# 一个简单的论坛系统分析 +# 十四、一个简单的论坛系统分析 该论坛系统功能如下: diff --git a/notes/SQL.md b/notes/SQL.md index 20d5c00d..651c784a 100644 --- a/notes/SQL.md +++ b/notes/SQL.md @@ -1,39 +1,31 @@ -* [基础](#基础) -* [创建表](#创建表) -* [插入](#插入) -* [更新](#更新) -* [删除](#删除) -* [修改表](#修改表) -* [查询](#查询) -* [排序](#排序) -* [过滤](#过滤) -* [通配符](#通配符) -* [计算字段](#计算字段) -* [函数](#函数) - * [文本处理](#文本处理) - * [日期和时间处理](#日期和时间处理) - * [数值处理](#数值处理) - * [汇总](#汇总) -* [分组](#分组) -* [子查询](#子查询) -* [连接](#连接) - * [内连接](#内连接) - * [自连接](#自连接) - * [自然连接](#自然连接) - * [外连接](#外连接) -* [组合查询](#组合查询) -* [视图](#视图) -* [存储过程](#存储过程) -* [游标](#游标) -* [触发器](#触发器) -* [事务处理](#事务处理) -* [字符集](#字符集) -* [权限管理](#权限管理) +* [一、基础](#一基础) +* [二、创建表](#二创建表) +* [三、修改表](#三修改表) +* [四、插入](#四插入) +* [五、更新](#五更新) +* [六、删除](#六删除) +* [七、查询](#七查询) +* [八、排序](#八排序) +* [九、过滤](#九过滤) +* [十、通配符](#十通配符) +* [十一、计算字段](#十一计算字段) +* [十二、函数](#十二函数) +* [十三、分组](#十三分组) +* [十四、子查询](#十四子查询) +* [十五、连接](#十五连接) +* [十六、组合查询](#十六组合查询) +* [十七、视图](#十七视图) +* [十八、存储过程](#十八存储过程) +* [十九、游标](#十九游标) +* [二十、触发器](#二十触发器) +* [二十一、事务处理](#二十一事务处理) +* [二十二、字符集](#二十二字符集) +* [二十三、权限管理](#二十三权限管理) -# 基础 +# 一、基础 模式定义了数据如何存储、存储什么样的数据以及数据如何分解等信息,数据库和表都有模式。 @@ -53,7 +45,7 @@ FROM mytable; -- 注释 注释2 */ ``` -# 创建表 +# 二、创建表 ```sql CREATE TABLE mytable ( @@ -64,16 +56,38 @@ CREATE TABLE mytable ( PRIMARY KEY (`id`)); ``` -# 插入 +# 三、修改表 -**普通插入** +添加列 + +```sql +ALTER TABLE mytable +ADD col CHAR(20); +``` + +删除列 + +```sql +ALTER TABLE mytable +DROP COLUMN col; +``` + +删除表 + +```sql +DROP TABLE mytable; +``` + +# 四、插入 + +普通插入 ```sql INSERT INTO mytable(col1, col2) VALUES(val1, val2); ``` -**插入检索出来的数据** +插入检索出来的数据 ```sql INSERT INTO mytable1(col1, col2) @@ -81,14 +95,14 @@ SELECT col1, col2 FROM mytable2; ``` -**将一个表的内容复制到一个新表** +将一个表的内容插入到一个新表 ```sql CREATE TABLE newtable AS SELECT * FROM mytable; ``` -# 更新 +# 五、更新 ```sql UPDATE mytable @@ -96,7 +110,7 @@ SET col = val WHERE id = 1; ``` -# 删除 +# 六、删除 ```sql DELETE FROM mytable @@ -107,31 +121,9 @@ WHERE id = 1; 使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。 -# 修改表 +# 七、查询 -**添加列** - -```sql -ALTER TABLE mytable -ADD col CHAR(20); -``` - -**删除列** - -```sql -ALTER TABLE mytable -DROP COLUMN col; -``` - -**删除表** - -```sql -DROP TABLE mytable; -``` - -# 查询 - -**DISTINCT** +## DISTINCT 相同值只会出现一次。它作用于所有列,也就是说所有列的值都相同才算相同。 @@ -140,7 +132,7 @@ SELECT DISTINCT col1, col2 FROM mytable; ``` -**LIMIT** +## LIMIT 限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。 @@ -167,7 +159,7 @@ LIMIT 2, 3; ``` -# 排序 +# 八、排序 - **ASC** :升序(默认) - **DESC** :降序 @@ -180,7 +172,7 @@ FROM mytable ORDER BY col1 DESC, col2 ASC; ``` -# 过滤 +# 九、过滤 不进行过滤的数据非常大,导致通过网络传输了很多多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。 @@ -209,7 +201,7 @@ WHERE col IS NULL; **NOT** 操作符用于否定一个条件。 -# 通配符 +# 十、通配符 通配符也是用在过滤语句中,但它只能用于文本字段。 @@ -228,7 +220,7 @@ WHERE col LIKE '[^AB]%' -- 不以 A 和 B 开头的任意文本 ``` 不要滥用通配符,通配符位于开头处匹配会非常慢。 -# 计算字段 +# 十一、计算字段 在数据库服务器上完成数据的转换和格式化的工作往往比客户端上快得多,并且转换和格式化后的数据量更少的话可以减少网络通信量。 @@ -239,14 +231,14 @@ SELECT col1*col2 AS alias FROM mytable ``` -**Concat()** 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 **TRIM()** 可以去除首尾空格。 +**CONCAT()** 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 **TRIM()** 可以去除首尾空格。 ```sql -SELECT Concat(TRIM(col1), ' (', TRIM(col2), ')') +SELECT CONCAT(TRIM(col1), ' (', TRIM(col2), ')') FROM mytable ``` -# 函数 +# 十二、函数 各个 DBMS 的函数都是不相同的,因此不可移植。 @@ -331,7 +323,7 @@ SELECT AVG(DISTINCT col1) AS avg_col FROM mytable ``` -# 分组 +# 十三、分组 分组就是把具有相同的数据值的行放在同一组中。 @@ -371,7 +363,7 @@ ORDER BY num; 3. NULL 的行会单独分为一组; 4. 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。 -# 子查询 +# 十四、子查询 子查询中只能返回一个字段的数据。 @@ -395,7 +387,7 @@ FROM Customers ORDER BY cust_name; ``` -# 连接 +# 十五、连接 连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 Where。 @@ -429,7 +421,7 @@ where A.key = B.key 一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。 -**子查询版本** +子查询版本 ```sql select name @@ -440,7 +432,7 @@ where department = ( where name = "Jim"); ``` -**自连接版本** +自连接版本 ```sql select name @@ -484,7 +476,7 @@ on Customers.cust_id = Orders.curt_id group by Customers.cust_id; ``` -# 组合查询 +# 十六、组合查询 使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果为 M+N 行。 @@ -504,7 +496,7 @@ FROM mytable WHERE col =2; ``` -# 视图 +# 十七、视图 视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。 @@ -522,17 +514,19 @@ FROM mytable WHERE col5 = val; ``` -# 存储过程 +# 十八、存储过程 存储过程可以看成是对一系列 SQL 操作的批处理; -**使用存储过程的好处** +## 使用存储过程的好处 1. 代码封装,保证了一定的安全性; + 2. 代码复用; + 3. 由于是预先编译,因此具有很高的性能。 -**创建存储过程** +## 创建存储过程 命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。 @@ -561,13 +555,13 @@ call myprocedure(@ret); select @ret; ``` -# 游标 +# 十九、游标 在存储过程中使用游标可以对一个结果集进行移动遍历。 游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。 -**使用游标的四个步骤:** +使用游标的四个步骤: 1. 声明游标,这个过程没有实际检索出数据; 2. 打开游标; @@ -597,7 +591,7 @@ create procedure myprocedure(out ret int) delimiter ; ``` -# 触发器 +# 二十、触发器 触发器会在某个表执行以下语句时而自动执行:DELETE、INSERT、UPDATE @@ -618,9 +612,9 @@ UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储过程。 -# 事务处理 +# 二十一、事务处理 -**基本术语** +基本术语: 1. 事务(transaction)指一组 SQL 语句; 2. 回退(rollback)指撤销指定 SQL 语句的过程; @@ -645,9 +639,9 @@ ROLLBACK TO delete1 COMMIT ``` -# 字符集 +# 二十二、字符集 -**基本术语** +基本术语: 1. 字符集为字母和符号的集合; 2. 编码为某个字符集成员的内部表示; @@ -669,7 +663,7 @@ FROM mytable ORDER BY col COLLATE latin1_general_ci; ``` -# 权限管理 +# 二十三、权限管理 MySQL 的账户信息保存在 mysql 这个数据库中。 @@ -678,7 +672,7 @@ USE mysql; SELECT user FROM user; ``` -**创建账户** +## 创建账户 ```sql CREATE USER myuser IDENTIFIED BY 'mypassword'; @@ -686,25 +680,25 @@ CREATE USER myuser IDENTIFIED BY 'mypassword'; 新创建的账户没有任何权限。 -**修改账户名** +## 修改账户名 ```sql RENAME myuser TO newuser; ``` -**删除账户** +## 删除账户 ```sql DROP USER myuser; ``` -**查看权限** +## 查看权限 ```sql SHOW GRANTS FOR myuser; ``` -**授予权限** +## 授予权限 ```sql GRANT SELECT, INSERT ON mydatabase.* TO myuser; @@ -714,7 +708,7 @@ GRANT SELECT, INSERT ON mydatabase.* TO myuser; 账户用 username@host 的形式定义,username@% 使用的是默认主机名。 -**删除权限** +## 删除权限 ```sql REVOKE SELECT, INSERT ON mydatabase.* FROM myuser; @@ -728,7 +722,7 @@ GRANT 和 REVOKE 可在几个层次上控制访问权限: - 特定的列; - 特定的存储过程。 -**更改密码** +## 更改密码 必须使用 Password() 函数 diff --git a/notes/一致性协议.md b/notes/一致性协议.md new file mode 100644 index 00000000..e2b2486d --- /dev/null +++ b/notes/一致性协议.md @@ -0,0 +1,155 @@ + +* [一、两阶段提交协议](#一两阶段提交协议) +* [二、Paxos 协议](#二paxos-协议) +* [三、Raft 协议](#三raft-协议) +* [四、拜占庭将军问题](#四拜占庭将军问题) +* [五、参考资料](#五参考资料) + + + +# 一、两阶段提交协议 + +Two-phase Commit(2PC)。 + +可以保证一个事务跨越多个节点时保持 ACID 特性。 + +两类节点:协调者(Coordinator)和参与者(Participants),协调者只有一个,参与者可以有多个。 + +## 运行过程 + +1. 准备阶段:协调者询问参与者事务是否执行成功; + +2. 提交阶段:如果事务在每个参与者上都执行成功,协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。 + +

+ + +需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段协接收到协调者发来的通知后,才进行提交或者回滚。 + +## 存在的问题 + +- 参与者发生故障。解决方案:可以给事务设置一个超时时间,如果某个参与者一直不响应,那么认为事务执行失败。 + +- 协调者发生故障。解决方案:将操作日志同步到备用协调者,让备用协调者接替后续工作。 + +# 二、Paxos 协议 + +用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。 + +主要有三类节点: + +1. 提议者(Proposer):提议一个值; +2. 接受者(Acceptor):对每个提议进行投票; +3. 告知者(Learner):被告知投票的结果,不参与投票的过程。 + +

+ +## 执行过程 + +规定一个提议包含两个字段:[n, v],其中 n 为序号(具有唯一性),v 为提议值。 + +下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向每个 Acceptor 发送提议请求。 + +

+ +当 Acceptor 接收到一个提议请求,包含的提议为 [n1, v1],并且之前还未接收过提议请求,那么发送一个提议响应,设置当前接收的提议为 [n1, v1],并且保证以后不会再接受提议值小于 n1 的提议。 + +如下图,Acceptor X 在收到 [n=2, v=8] 的提议请求时,由于之前没有接收过提议,因此就发送一个 [no previous] 的提议响应,并且设置当前接收的提议为 [n=2, v=8],并且保证以后不会再接受提议值小于 2 的提议。其它的 Acceptor 类似。 + +

+ +如果 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 类似。 + +

+ +当一个 Proposer 接收到超过一半 Acceptor 的提议响应时,就可以发送接受请求。 + +如下图,Proposer A 接受到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受提议值小于 4 的提议。Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。 + +

+ +Acceptor 接收到接受请求时,如果提议号大于等于该 Acceptor 承诺的最小提议号,那么就发送通知给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。 + +

+ +## 约束条件 + +### 1. 正确性 + +只有一个提议值会生效。 + +因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收,并且 Acceptor 不会接受两个不同的提议,因此可以保证正确性。 + +### 2. 可终止性 + +最后总会有一个提议生效。 + +Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。 + +# 三、Raft 协议 + +Raft 和 Poxas 类似,但是更容易理解,也更容易实现。 + +Raft 主要是用来竞选主节点。 + +## 单个 Candidate 的竞选 + +有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。 + +① 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。 + +

+ +② 此时 A 发送投票请求给其它所有节点。 + +

+ +③ 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。 + +

+ +④ 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。 + +

+ +## 多个 Candidate 竞选 + +① 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。 + +

+ +② 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。 + +

+ +## 日志复制 + +① 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。 + +

+ +② Leader 会把修改复制到所有 Follower。 + +

+ +③ Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。 + +

+ +④ 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。 + +

+ +# 四、拜占庭将军问题 + +> [拜占庭将军问题深入探讨](http://www.8btc.com/baizhantingjiangjun) + +# 五、参考资料 + +- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[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/) diff --git a/notes/代码可读性.md b/notes/代码可读性.md index 0c5ab772..5d40b41b 100644 --- a/notes/代码可读性.md +++ b/notes/代码可读性.md @@ -1,21 +1,21 @@ -* [可读性的重要性](#可读性的重要性) -* [用名字表达代码含义](#用名字表达代码含义) -* [名字不能带来歧义](#名字不能带来歧义) -* [良好的代码风格](#良好的代码风格) -* [编写注释](#编写注释) -* [如何编写注释](#如何编写注释) -* [提高控制流的可读性](#提高控制流的可读性) -* [拆分长表达式](#拆分长表达式) -* [变量与可读性](#变量与可读性) -* [抽取函数](#抽取函数) -* [一次只做一件事](#一次只做一件事) -* [用自然语言表述代码](#用自然语言表述代码) -* [减少代码量](#减少代码量) +* [一、可读性的重要性](#一可读性的重要性) +* [二、用名字表达代码含义](#二用名字表达代码含义) +* [三、名字不能带来歧义](#三名字不能带来歧义) +* [四、良好的代码风格](#四良好的代码风格) +* [五、编写注释](#五编写注释) +* [六、如何编写注释](#六如何编写注释) +* [七、提高控制流的可读性](#七提高控制流的可读性) +* [八、拆分长表达式](#八拆分长表达式) +* [九、变量与可读性](#九变量与可读性) +* [十、抽取函数](#十抽取函数) +* [十一、一次只做一件事](#十一一次只做一件事) +* [十二、用自然语言表述代码](#十二用自然语言表述代码) +* [十三、减少代码量](#十三减少代码量) -# 可读性的重要性 +# 一、可读性的重要性 编程有很大一部分时间是在阅读代码,不仅要阅读自己的代码,而且要阅读别人的代码。因此,可读性良好的代码能够大大提高编程效率。 @@ -23,7 +23,7 @@ 只有在核心领域为了效率才可以放弃可读性,否则可读性是第一位。 -# 用名字表达代码含义 +# 二、用名字表达代码含义 一些比较有表达力的单词: @@ -38,7 +38,7 @@ 为名字添加形容词等信息能让名字更具有表达力,但是名字也会变长。名字长短的准则是:作用域越大,名字越长。因此只有在短作用域才能使用一些简单名字。 -# 名字不能带来歧义 +# 三、名字不能带来歧义 起完名字要思考一下别人会对这个名字有何解读,会不会误解了原本想表达的含义。 @@ -48,13 +48,13 @@ 布尔相关的命名加上 is、can、should、has 等前缀。 -# 良好的代码风格 +# 四、良好的代码风格 适当的空行和缩进。 排列整齐的注释: -``` +```java int a = 1; // 注释 int b = 11; // 注释 int c = 111; // 注释 @@ -64,7 +64,7 @@ int c = 111; // 注释 把相关的代码按块组织起来放在一起。 -# 编写注释 +# 五、编写注释 阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter 和 setter 方法,为这些方法写注释反而让代码可读性更差。 @@ -83,24 +83,24 @@ int c = 111; // 注释 |HACH| 粗糙的解决方案 | |XXX| 危险!这里有重要的问题 | -# 如何编写注释 +# 六、如何编写注释 尽量简洁明了: -``` +```java // The first String is student's name // The Second Integer is student's score Map scoreMap = new HashMap<>(); ``` -``` +```java // Student' name -> Student's score Map scoreMap = new HashMap<>(); ``` 添加测试用例来说明: -``` +```java //... // Example: add(1, 2), return 3 int add(int x, int y) { @@ -110,7 +110,7 @@ int add(int x, int y) { 在很复杂的函数调用中对每个参数标上名字: -``` +```java int a = 1; int b = 2; int num = add(\* x = *\ a, \* y = *\ b); @@ -118,17 +118,18 @@ int num = add(\* x = *\ a, \* y = *\ b); 使用专业名词来缩短概念上的解释,比如用设计模式名来说明代码。 -# 提高控制流的可读性 +# 七、提高控制流的可读性 条件表达式中,左侧是变量,右侧是常数。比如下面第一个语句正确: -``` +```java if(len < 10) if(10 > len) ``` if / else 条件语句,逻辑的处理顺序为:① 正逻辑;② 关键逻辑;③ 简单逻辑。 -``` + +```java if(a == b) { // 正逻辑 } else{ @@ -144,15 +145,15 @@ do / while 的条件放在后面,不够简单明了,并且会有一些迷惑 在嵌套的循环中,用一些 return 语句往往能减少嵌套的层数。 -# 拆分长表达式 +# 八、拆分长表达式 长表达式的可读性很差,可以引入一些解释变量从而拆分表达式: -``` +```python if line.split(':')[0].strip() == "root": ... ``` -``` +```python username = line.split(':')[0].strip() if username == "root": ... @@ -160,22 +161,22 @@ if username == "root": 使用摩根定理简化一些逻辑表达式: -``` +```java if(!a && !b) { ... } ``` -``` -if(a || b) { +```java +if(!(a || b)) { ... } ``` -# 变量与可读性 +# 九、变量与可读性 **去除控制流变量** 。在循环中通过使用 break 或者 return 可以减少控制流变量的使用。 -``` +```java boolean done = false; while(/* condition */ && !done) { ... @@ -198,7 +199,7 @@ while(/* condition */) { JavaScript 可以用闭包减小作用域。以下代码中 submit_form 是函数变量,submitted 变量控制函数不会被提交两次。第一个实现中 submitted 是全局变量,第二个实现把 submitted 放到匿名函数中,从而限制了起作用域范围。 -``` +```js submitted = false; var submit_form = function(form_name) { if(submitted) { @@ -208,7 +209,7 @@ var submit_form = function(form_name) { }; ``` -``` +```js var submit_form = (function() { var submitted = false; return function(form_name) { @@ -228,7 +229,7 @@ JavaScript 中没有用 var 声明的变量都是全局变量,而全局变量 在一个网页中有以下文本输入字段: -``` +```html @@ -237,7 +238,7 @@ JavaScript 中没有用 var 声明的变量都是全局变量,而全局变量 现在要接受一个字符串并把它放到第一个空的 input 字段中,初始实现如下: -``` +```js var setFirstEmptyInput = function(new_alue) { var found = false; var i = 1; @@ -261,7 +262,7 @@ var setFirstEmptyInput = function(new_alue) { - elem 作用域过大; - 可以用 for 循环代替 while 循环; -``` +```js var setFirstEmptyInput = function(new_value) { for(var i = 1; true; i++) { var elem = document.getElementById('input' + i); @@ -276,7 +277,7 @@ var setFirstEmptyInput = function(new_value) { }; ``` -# 抽取函数 +# 十、抽取函数 工程学就是把大问题拆分成小问题再把这些问题的解决方案放回一起。 @@ -284,7 +285,7 @@ var setFirstEmptyInput = function(new_value) { 介绍性的代码: -``` +```java int findClostElement(int[] arr) { int clostIdx; int clostDist = Interger.MAX_VALUE; @@ -305,7 +306,7 @@ int findClostElement(int[] arr) { 以上代码中循环部分主要计算距离,这部分不属于代码高层次目标,高层次目标是寻找最小距离的值,因此可以把这部分代替提取到独立的函数中。这样做也带来一个额外的好处有:可以单独进行测试、可以快速找到程序错误并修改。 -``` +```java public int findClostElement(int[] arr) { int clostIdx; int clostDist = Interger.MAX_VALUE; @@ -324,17 +325,17 @@ public int findClostElement(int[] arr) { 函数抽取也用于减小代码的冗余。 -# 一次只做一件事 +# 十一、一次只做一件事 只做一件事的代码很容易让人知道其要做的事; 基本流程:列出代码所做的所有任务;把每个任务拆分到不同的函数,或者不同的段落。 -# 用自然语言表述代码 +# 十二、用自然语言表述代码 先用自然语言书写代码逻辑,也就是伪代码,然后再写代码,这样代码逻辑会更清晰。 -# 减少代码量 +# 十三、减少代码量 不要过度设计,编码过程会有很多变化,过度设计的内容到最后往往是无用的。 diff --git a/notes/分布式基础.md b/notes/分布式基础.md new file mode 100644 index 00000000..2b1a696d --- /dev/null +++ b/notes/分布式基础.md @@ -0,0 +1,206 @@ + +* [一、基本概念](#一基本概念) + * [异常](#异常) + * [超时](#超时) + * [衡量指标](#衡量指标) +* [二、数据分布](#二数据分布) + * [哈希分布](#哈希分布) + * [顺序分布](#顺序分布) +* [三、负载均衡](#三负载均衡) +* [四、复制](#四复制) + * [强同步复制协议](#强同步复制协议) + * [异步复制协议](#异步复制协议) +* [五、CAP](#五cap) +* [六、BASE](#六base) + * [基本可用](#基本可用) + * [软状态](#软状态) + * [最终一致性](#最终一致性) +* [七、容错](#七容错) + * [故障检测](#故障检测) + * [故障恢复](#故障恢复) +* [八、CDN 架构](#八cdn-架构) +* [参考资料](#参考资料) + + + +# 一、基本概念 + +## 异常 + +### 1. 服务器宕机 + +内存错误、服务器停电等都会导致服务器宕机,此时节点无法正常工作,称为不可用。 + +服务器宕机会导致节点失去所有内存信息,因此需要将内存信息保存到持久化介质上。 + +### 2. 网络异常 + +有一种特殊的网络异常称为 **网络分区** ,即集群的所有节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。 + +### 3. 磁盘故障 + +磁盘故障是一种发生概率很高的异常。 + +使用冗余机制,将数据存储到多台服务器。 + +## 超时 + +在分布式系统中,一个请求除了成功和失败两种状态,还存在着超时状态。 + +

+ +可以将服务器的操作设计为具有 **幂等性** ,即执行多次的结果与执行一次的结果相同。如果使用这种方式,当出现超时的时候,可以不断地重新请求直到成功。 + +## 衡量指标 + +### 1. 性能 + +常见的性能指标有:吞吐量、响应时间。 + +其中,吞吐量指系统在某一段时间可以处理的请求总数,通常为每秒的读操作数或者写操作数;响应时间指从某个请求发出到接收到返回结果消耗的时间。 + +这两个指标往往是矛盾的,追求高吞吐的系统,往往很难做到低响应时间,解释如下: + +- 在无并发的系统中,吞吐量为响应时间的倒数,例如响应时间为 10 ms,那么吞吐量为 100 req/s,因此高吞吐也就意味着低响应时间。 + +- 但是在并发的系统中,由于一个请求在调用 I/O 资源的时候,需要进行等待。服务器端一般使用的是异步等待方式,即等待的请求被阻塞之后不需要一直占用 CPU 资源。这种方式能大大提高 CPU 资源的利用率,例如上面的例子中,单个请求在无并发的系统中响应时间为 10 ms,如果在并发的系统中,那么吞吐量将大于 100 req/s。因此为了追求高吞吐量,通常会提高并发程度。但是并发程度的增加,会导致请求的平均响应时间也增加,因为请求不能马上被处理,需要和其它请求一起进行并发处理,响应时间自然就会增高。 + +### 2. 可用性 + +可用性指系统在面对各种异常时可以提供正常服务的能力。可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。 + +### 3. 一致性 + +可以从两个角度理解一致性:从客户端的角度,读写操作是否满足某种特性;从服务器的角度,多个数据副本之间是否一致。 + +有以下三种一致性模型: + +1. 强一致性:新数据写入之后,在任何数据副本上都能读取到最新值; +2. 弱一致性:新数据写入之后,不能保证在数据副本上能读取到最新值; +3. 最终一致性:新数据写入之后,只能保证过一了一个时间窗口才能读取到最新值; + +### 4. 可扩展性 + +指系统通过扩展集群服务器规模来提高性能的能力。理想的分布式系统需要实现“线性可扩展”,即随着集群规模的增加,系统的整体性能也会线程增加。 + +# 二、数据分布 + +分布式系统的数据分布在多个节点中,常用的数据分布方式有哈希分布和顺序分布。 + +## 哈希分布 + +哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key,则将该数据分配的节点序号为:hash(key)%N。 + +传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。 + +#### 一致性哈希 + +Distributed Hash Table(DHT):对于哈希空间 0\~2n,将该哈希空间看成一个哈希环,将每个节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。 + +

+ +一致性哈希的优点是在加入或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将数据对象 C 重新存放到节点 X 上即可,对于节点 A、B、D 都没有影响。 + +

+ +## 顺序分布 + +哈希分布式破坏了数据的有序性,顺序分布则不会。 + +顺序分布的数据划分为多个连续的部分,按一定策略分布到不同节点上。例如下图中,User 表的主键范围为 1 \~ 7000,使用顺序分布可以将其划分成多个子表,对应的主键范围为 1 \~ 1000,1001 \~ 2000,...,6001 \~ 7000。 + +其中 Meta 表是为了支持更大的集群规模,它将原来的一层索引结分成两层,使用 Meta 表来维护 User 子表所在的节点,从而减轻 Root 节点的负担。 + +

+ +# 三、负载均衡 + +衡量负载的因素很多,如 CPU、内存、磁盘等资源使用情况、读写请求数等。分布式系统应当能够自动负载均衡,当某个节点的负载较高,将它的部分数据迁移到其它节点。 + +每个集群都有一个总控节点,其它节点为工作节点,由总控节点根据全局负载信息进行整体调度,工作节点定时发送心跳包(Heartbeat)将节点负载相关的信息发送给总控节点。 + +一个新上线的工作节点,由于其负载较低,如果不加控制,总控节点会将大量数据同时迁移到该节点上,造成该节点一段时间内无法工作。因此负载均衡操作需要平滑进行,新加入的节点需要较长的一段时间来达到比较均衡的状态。 + +# 四、复制 + +复制是保证分布式系统高可用的基础,让一个数据存储多个副本,当某个副本所在的节点出现故障时,能够自动切换到其它副本上,从而实现故障恢复。 + +多个副本通常有一个为主副本,其它为备副本。主副本用来处理写请求,备副本主要用来处理读请求,实现读写分离。主副本将同步操作日志发送给备副本,备副本通过回放操作日志获取最新修改。 + +

+ +主备副本之间有两种复制协议,一种是强同步复制协议,一种是异步复制协议。 + +## 强同步复制协议 + +要求主副本将同步操作日志发给备副本之后进行等待,要求至少一个备副本返回成功后,才开始修改主副本,修改完成之后通知客户端操作成功。 + +优点:至少有一个备副本拥有完整的数据,出现故障时可以安全地切换到该备副本,因此一致性好。 + +缺点:可用性差,因为主副本需要等待,那么整个分布式系统的可用时间就会降低。 + +## 异步复制协议 + +主副本将同步操作日志发给备副本之后不需要进行等待,直接修改主副本并通知客户端操作成功。 + +优点:可用性好。 + +缺点:一致性差。 + +# 五、CAP + +分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition tolerance),最多只能同时满足其中两项。这三个概念上文中已经提到。 + +在设计分布式系统时,需要根据实际需求弱化某一要求。因此就有了下图中的三种设计:CA、CP 和 AP。 + +

+ +需要注意的是,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此实际上设计分布式系统需要在一致性和可用性之间做权衡。 + +# 六、BASE + +BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,是基于 CAP 定理逐步演化而来的。BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 + +## 基本可用 + +指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。 + +例如,电商在做促销时,服务层可能只提供降级服务,部分用户可能会被引导到降级页面上。 + +## 软状态 + +指允许系统存在中间状态,而该中间状态不会影响系统整体可用性,即不同节点的数据副本之间进行同步的过程允许存在延时。 + +## 最终一致性 + +指所有的数据副本,在经过一段时间的同步之后,最终都能够达到一致的状态。 + +强一致性需要保证数据副本实时一致,而最终一致性只需要保证过一段时间是一致的。 + +ACID 是传统数据库系统常用的设计理论,追求强一致性模型。BASE 常用于大型分布式系统,只需要保证最终一致性。在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。 + +# 七、容错 + +分布式系统故障发生的概率很大,为了实现高可用以及减少人工运维成本,需要实现自动化容错。 + +## 故障检测 + +通过 **租约机制** 来对故障进行检测。假设节点 A 为主控节点,节点 A 向节点 B 发送租约,节点 B 在租约规定的期限内才能提供服务。期限快到达时,节点 B 需要向 A 重新申请租约。 + +如果过期,那么 B 不再提供服务,并且 A 也能知道 B 此时可能发生故障并已经停止服务。可以看到,通过这种机制,A 和 B 都能对 B 发生故障这一事实达成一致。 + +## 故障恢复 + +当某个节点故障时,就将它上面的服务迁移到其它节点。 + +# 八、CDN 架构 + +CND 通过将内容发布到靠近用户的边缘节点,使不同地域的用户在访问相同网页时可以就近获取。不仅可以减轻服务器的负担,也可以提高用户的访问速度。 + +从下图可以看出,DNS 在对域名解析时不再向用户返回源服务器的 IP 地址,而是返回边缘节点的 IP 地址,所以用户最终访问的是边缘节点。边缘节点会先从源服务器中获取用户所需的数据,如果请求成功,边缘节点会将页面缓存下来,下次用户访问时可以直接读取。 + +

+ +# 参考资料 + +- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013. diff --git a/notes/分布式问题分析.md b/notes/分布式问题分析.md new file mode 100644 index 00000000..228d3fb6 --- /dev/null +++ b/notes/分布式问题分析.md @@ -0,0 +1,364 @@ + +* [一、谈谈业务中使用分布式的场景](#一谈谈业务中使用分布式的场景) +* [二、分布式事务](#二分布式事务) + * [产生原因](#产生原因) + * [应用场景](#应用场景) + * [解决方案](#解决方案) +* [三、负载均衡的算法与实现](#三负载均衡的算法与实现) + * [算法](#算法) + * [实现](#实现) +* [四、分布式锁](#四分布式锁) + * [使用场景](#使用场景) + * [实现方式](#实现方式) +* [五、分布式 Session](#五分布式-session) + * [1. 粘性 Session](#1-粘性-session) + * [2. 服务器 Session 复制](#2-服务器-session-复制) + * [3. Session 共享机制](#3-session-共享机制) + * [4. Session 持久化到数据库](#4-session-持久化到数据库) + * [5. Terracotta 实现 Session 复制](#5-terracotta-实现-session-复制) +* [六、分库与分表带来的分布式困境与应对之策](#六分库与分表带来的分布式困境与应对之策) + * [事务问题](#事务问题) + * [查询问题](#查询问题) + * [ID 唯一性](#id-唯一性) +* [参考资料](#参考资料) + + + +# 一、谈谈业务中使用分布式的场景 + +分布式主要是为了提供可扩展性以及高可用性,业务中使用分布式的场景主要有分布式存储以及分布式计算。 + +分布式存储中可以将数据分片到多个节点上,不仅可以提高性能(可扩展性),同时也可以使用多个节点对同一份数据进行备份。 + +至于分布式计算,就是将一个大的计算任务分解成小任务分配到多台节点上去执行,再汇总每个小任务的执行结果得到最终结果。MapReduce 是分布式计算的最好例子。 + +# 二、分布式事务 + +指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。 + +## 产生原因 + +- 数据库分库分表; +- SOA 架构,比如一个电商网站将订单业务和库存业务分离出来放到不同的节点上。 + +## 应用场景 + +- 下单:减少库存、更新订单状态。库存和订单不在不同一个数据库,因此涉及分布式事务。 +- 支付:买家账户扣款、卖家账户入账。买家和卖家账户信息不在同一个数据库,因此涉及分布式事务。 + +## 解决方案 + +### 1. 两阶段提交协议 + +[两阶段提交](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE.md#%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4%E5%8D%8F%E8%AE%AE) + +两阶段提交协议可以很好得解决分布式事务问题,它可以使用 XA 来实现,XA 它包含两个部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如 Oracle、DB2 这些商业数据库都实现了 XA 接口;而事务管理器作为全局的协调者,负责各个本地资源的提交和回滚。 + +### 2. 消息中间件 + +消息中间件也可称作消息系统 (MQ),它本质上是一个暂存转发消息的一个中间件。在分布式应用当中,我们可以把一个业务操作转换成一个消息,比如支付宝的余额转入余额宝操作,支付宝系统执行减少余额操作之后向消息系统发送一个消息,余额宝系统订阅这条消息然后进行增加余额宝操作。 + +**(一)消息处理模型** + + **点对点**
+ +

+ + **发布/订阅**
+ +

+ +**(二)消息的可靠性** + +消息的发送端的可靠性:发送端完成操作后一定能将消息成功发送到消息系统。 + +消息的接收端的可靠性:接收端仅且能够从消息中间件成功消费一次消息。 + + **发送端的可靠性**
+ +在本地数据建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。 + + **接收端的可靠性**
+ +保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。 + +保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。 + +# 三、负载均衡的算法与实现 + +## 算法 + +### 1. 轮询(Round Robin) + +轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。 + +

+ +该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担多大的负载。下图中,服务器 2 的性能比服务器 1 差,那么服务器 2 可能无法承担多大的负载。 + +

+ +### 2. 加权轮询(Weighted Round Robbin) + +加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。 + +

+ +### 3. 最少连接(least Connections) + +由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数多大,而另一台服务器的连接多小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担多大的负载。 + +

+ +最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。 + +

+ +### 4. 加权最小连接(Weighted Least Connection) + +在最小连接的基础上,根据服务器的性能为每台服务器分配权重,根据权重计算出每台服务器能处理的连接数。 + +

+ +### 5. 随机算法(Random) + +把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。 + +

+ +## 实现 + +### 1. DNS 解析 + +使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式最为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。 + +

+ +### 2. 修改 MAC 地址 + +使用 LVS(Linux Virtual Server)这种链路层负载均衡器,根据负载情况修改请求的 MAC 地址。 + +

+ +### 3. 修改 IP 地址 + +在网络层修改请求的目的 IP 地址。 + +

+ +### 4. HTTP 重定向 + +HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。 + +

+ +### 5. 反向代理 + +正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。 + +反向代理:发生在服务器端,用户不知道代理的存在。 + +

+ +# 四、分布式锁 + +Java 提供了两种内置的锁的实现,一种是由 JVM 实现的 synchronized 和 JDK 提供的 Lock,当你的应用是单机或者说单进程应用时,可以使用 synchronized 或 Lock 来实现锁。当应用涉及到多机、多进程共同完成时,那么这时候就需要一个全局锁来实现多个进程之间的同步。 + +## 使用场景 + +在服务器端使用分布式部署的情况下,一个服务可能分布在不同的节点撒花姑娘,比如订单服务分布式在节点 A 和节点 B 上。如果多个客户端同时对一个服务进行请求时,就需要使用分布式锁。例如当 APP 端的请求路由到了节点 A,WEB 端被路由到了 B 服务,这时对共享资源进行使用时需要使用分布式锁。 + +## 实现方式 + +### 1. 数据库分布式锁 + +**(一)基于 MySQL 锁表** + +该实现方式完全依靠数据库唯一索引来实现。当想要获得锁时,就向数据库中插入一条记录,释放锁时就删除这条记录。如果记录具有唯一索引,就不会同时插入同一条记录。这种方式存在以下几个问题: + +1. 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。 +2. 只能是非阻塞锁,插入失败直接就报错了,无法重试。 +3. 不可重入,同一线程在没有释放锁之前无法再获得锁。 + +**(二)采用乐观锁增加版本号** + +根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。 + +### 2. Redis 分布式锁 + +**(一)基于 SETNX、EXPIRE** + +使用 SETNX(set if not exist)命令插入一个键值对时,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。因此客户端在尝试获得锁时,先使用 SETNX 向 Redis 中插入一个记录,如果返回 True 表示获得锁,返回 False 表示已经有客户端占用锁。 + +EXPIRE 可以为一个键值对设置一个过期时间,从而避免了死锁的发生。 + +**(二)RedLock 算法** + +ReadLock 算法使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时还可用。 + +1. 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。 +2. 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。 +3. 如果锁获取失败,会到每个实例上释放锁。 + +### 3. Zookeeper 分布式锁 + +Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。 + +**(一)抽象模型** + +Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。 + +

+ +**(二)节点类型** + +- 永久节点:不会因为会话结束或者超时而消失; +- 临时节点:如果会话结束或者超时就会消失; +- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,依次类推。 + +**(三)监听器** + +为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。 + +**(四)分布式锁实现** + +1. 创建一个锁目录 /lock。 +1. 在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000,第二个为 /lock/lock-0000000001,以此类推。 +2. 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁; +3. 执行业务代码,完成后,删除对应的子节点。 + +**(五)会话超时** + +如果一个已经获得锁的会话超时了,因为创建的是临时节点,因此该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库分布式锁的死锁问题。 + +**(六)羊群效应** + +在步骤二,一个节点未获得锁,需要监听监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知,而我们只希望它的下一个子节点收到通知。 + +# 五、分布式 Session + +如果不做任何处理的话,用户将出现频繁登录的现象,比如集群中存在 A、B 两台服务器,用户在第一次访问网站时,Nginx 通过其负载均衡机制将用户请求转发到 A 服务器,这时 A 服务器就会给用户创建一个 Session。当用户第二次发送请求时,Nginx 将其负载均衡到 B 服务器,而这时候 B 服务器并不存在 Session,所以就会将用户踢到登录页面。这将大大降低用户体验度,导致用户的流失,这种情况是项目绝不应该出现的。 + +## 1. 粘性 Session + +### 原理 + +粘性 Session 是指将用户锁定到某一个服务器上,比如上面说的例子,用户第一次请求时,负载均衡器将用户的请求转发到了 A 服务器上,如果负载均衡器设置了粘性 Session 的话,那么用户以后的每次请求都会转发到 A 服务器上,相当于把用户和 A 服务器粘到了一块,这就是粘性 Session 机制。 + +### 优点 + +简单,不需要对 Session 做任何处理。 + +### 缺点 + +缺乏容错性,如果当前访问的服务器发生故障,用户被转移到第二个服务器上时,他的 Session 信息都将失效。 + +### 适用场景 + +- 发生故障对客户产生的影响较小; +- 服务器发生故障是低概率事件。 + +## 2. 服务器 Session 复制 + +### 原理 + +任何一个服务器上的 Session 发生改变,该节点会把这个 Session 的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要 Session,以此来保证 Session 同步。 + +### 优点 + +可容错,各个服务器间 Session 能够实时响应。 + +### 缺点 + +会对网络负荷造成一定压力,如果 Session 量大的话可能会造成网络堵塞,拖慢服务器性能。 + +### 实现方式 + +1. 设置 Tomcat 的 server.xml 开启 tomcat 集群功能。 +2. 在应用里增加信息:通知应用当前处于集群环境中,支持分布式,即在 web.xml 中添加<distributable/> 选项。 + +## 3. Session 共享机制 + +使用分布式缓存方案比如 Memcached、Redis,但是要求 Memcached 或 Redis 必须是集群。 + +使用 Session 共享也分两种机制,两种情况如下: + +### 3.1 粘性 Session 共享机制 + +和粘性 Session 一样,一个用户的 Session 会绑定到一个 Tomcat 上。Memcached 只是起到备份作用。 + +

+ +### 3.2 非粘性 Session 共享机制 + +#### 原理 + +Tomcat 本身不存储 Session,而是存入 Memcached 中。Memcached 集群构建主从复制架构。 + +

+ +#### 优点 + +可容错,Session 实时响应。 + +#### 实现方式 + +用开源的 msm 插件解决 Tomcat 之间的 Session 共享:Memcached_Session_Manager(MSM) + +## 4. Session 持久化到数据库 + +### 原理 + +拿出一个数据库,专门用来存储 Session 信息。保证 Session 的持久化。 + +### 优点 + +服务器出现问题,Session 不会丢失 + +### 缺点 + +如果网站的访问量很大,把 Session 存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库。 + +## 5. Terracotta 实现 Session 复制 + +### 原理 + +Terracotta 的基本原理是对于集群间共享的数据,当在一个节点发生变化的时候,Terracotta 只把变化的部分发送给 Terracotta 服务器,然后由服务器把它转发给真正需要这个数据的节点。它是服务器 Session 复制的优化。 + +

+ +### 优点 + +这样对网络的压力就非常小,各个节点也不必浪费 CPU 时间和内存进行大量的序列化操作。把这种集群间数据共享的机制应用在 Session 同步上,既避免了对数据库的依赖,又能达到负载均衡和灾难恢复的效果。 + +# 六、分库与分表带来的分布式困境与应对之策 + +

+ +## 事务问题 + +使用分布式事务。 + +## 查询问题 + +使用汇总表。 + +## ID 唯一性 + +- 使用全局唯一 ID:GUID。 +- 为每个分片指定一个 ID 范围。 +- 分布式 ID 生成器 (如 Twitter 的 [Snowflake](https://twitter.github.io/twitter-server/) 算法)。 + +# 参考资料 + +- [Comparing Load Balancing Algorithms](http://www.jscape.com/blog/load-balancing-algorithms) +- [负载均衡算法及手段](https://segmentfault.com/a/1190000004492447) +- [集群/分布式环境下 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) +- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6) +- [服务端指南 数据存储篇 | 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) diff --git a/notes/剑指 offer 题解.md b/notes/剑指 offer 题解.md index f7492747..b367d103 100644 --- a/notes/剑指 offer 题解.md +++ b/notes/剑指 offer 题解.md @@ -1,205 +1,242 @@ -* [第二章 面试需要的基础知识](#第二章-面试需要的基础知识) - * [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.1 对称的二叉树](#281-对称的二叉树) - * [28.2 平衡二叉树](#282-平衡二叉树) - * [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 二叉树的深度](#55-二叉树的深度) - * [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-求-1+2+3++n) - * [65. 不用加减乘除做加法](#65-不用加减乘除做加法) - * [66. 构建乘积数组](#66-构建乘积数组) -* [第七章 两个面试案例](#第七章-两个面试案例) - * [67. 把字符串转换成整数](#67-把字符串转换成整数) - * [68. 树中两个节点的最低公共祖先](#68-树中两个节点的最低公共祖先) +* [前言](#前言) +* [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.1 对称的二叉树](#281-对称的二叉树) +* [28.2 平衡二叉树](#282-平衡二叉树) +* [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 二叉树的深度](#55-二叉树的深度) +* [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-求-1+2+3++n) +* [65. 不用加减乘除做加法](#65-不用加减乘除做加法) +* [66. 构建乘积数组](#66-构建乘积数组) +* [67. 把字符串转换成整数](#67-把字符串转换成整数) +* [68. 树中两个节点的最低公共祖先](#68-树中两个节点的最低公共祖先) -# 第二章 面试需要的基础知识 +# 前言 -## 2. 实现 Singleton +## 变量命名约定 -[单例模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E7%AC%AC%E4%BA%94%E7%AB%A0-%E5%8D%95%E4%BB%B6%E6%A8%A1%E5%BC%8F) +- nums 表示数字数组,array 表示通用数组,matrix 表示矩阵; +- n 表示数组长度、字符串长度、树节点个数,以及其它具有一维性质的数据结构的元素个数; +- m, n 表示矩阵的行数和列数; +- first, last 表示闭区间,在需要作为函数参数时使用:[first, last]; +- l, h 也表示闭区间,在只作为局部变量时使用:[l, h]; +- begin, end 表示左闭右开区间:[begin, end); +- ret 表示结果相关的变量; +- dp 表示动态规划保存子问题的数组; -## 3. 数组中重复的数字 +## 复杂度简写说明 -**题目描述** +O(nlogn) + O(n2),第一个指时间复杂度,第二个指空间复杂度。 + +# 2. 实现 Singleton + +> [单例模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E5%85%AB%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F) + +# 3. 数组中重复的数字 + +## 题目描述 在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。例如,如果输入长度为 7 的数组 {2, 3, 1, 0, 2, 5, 3},那么对应的输出是第一个重复的数字 2。 -**解题思路** +## 解题思路 这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素放到第 i 个位置上。 +以 (2, 3, 1, 0, 2, 5) 为例,以下代码的运行过程为: + +```html +position-0 : (2,3,1,0,2,5) // 2 <-> 1 + (1,3,2,0,2,5) // 1 <-> 3 + (3,1,1,0,2,5) // 3 <-> 0 + (0,1,1,3,2,5) // already in position +position-1 : (0,1,1,3,2,5) // already in position +position-2 : (0,1,1,3,2,5) // nums[i] == nums[nums[i]], exit +``` + +遍历到位置 2 时,该位置上的数为 1,但是第 1 个位置上已经有一个 1 的值了,因此可以知道 1 重复。 + +复杂度:O(n) + O(1) + ```java -public boolean duplicate(int[] numbers, int length, int[] duplication) { - if(numbers == null || length <= 0) 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 (numbers[i] != i && numbers[i] != numbers[numbers[i]]) { - swap(numbers, i, numbers[i]); + while (nums[i] != i && nums[i] != nums[nums[i]]) { + swap(nums, i, nums[i]); } - if (numbers[i] != i && numbers[i] == numbers[numbers[i]]) { - duplication[0] = numbers[i]; + if (nums[i] != i && nums[i] == nums[nums[i]]) { + duplication[0] = nums[i]; return true; } } return false; } -private void swap(int[] numbers, int i, int j) { - int t = numbers[i]; numbers[i] = numbers[j]; numbers[j] = t; +private void swap(int[] nums, int i, int j) { + int t = nums[i]; nums[i] = nums[j]; nums[j] = t; } ``` -## 4. 二维数组中的查找 +# 4. 二维数组中的查找 -**题目描述** +## 题目描述 在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 ```html +Consider the following matrix: [ - [ 1, 5, 9], - [10, 11, 13], - [12, 13, 15] + [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. ``` -**解题思路** +## 解题思路 -从右上角开始查找。因为矩阵中的一个数,它左边的数都比它来的小,下边的数都比它来的大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来改变行和列的下标,从而缩小查找区间。 +从右上角开始查找。因为矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。 + +复杂度:O(m + n) + O(1) ```java -public boolean Find(int target, int [][] array) { - if (array == null || array.length == 0 || array[0].length == 0) return false; - int m = array.length, n = array[0].length; - int row = 0, col = n - 1; - while (row < m && col >= 0) { - if (target == array[row][col]) return true; - else if (target < array[row][col]) col--; - else row++; +public boolean Find(int target, int[][] matrix) { + if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return false; + int m = matrix.length, n = matrix[0].length; + int r = 0, c = n - 1; // 从右上角开始 + while (r <= m - 1 && c >= 0) { + if (target == matrix[r][c]) return true; + else if (target > matrix[r][c]) r++; + else c--; } return false; } ``` -## 5. 替换空格 +# 5. 替换空格 -**题目描述** +## 题目描述 请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为 We Are Happy. 则经过替换之后的字符串为 We%20Are%20Happy。 -**题目要求** +## 解题思路 -以 O(1) 的空间复杂度和 O(n) 的时间复杂度来求解。 +在字符串尾部填充任意字符,使得字符串的长度等于字符串替换之后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。 -**解题思路** +令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2 从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。 -从后向前改变字符串。 +从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。 + +复杂度:O(n) + O(1) + +

```java public String replaceSpace(StringBuffer str) { - int n = str.length(); - for (int i = 0; i < n; i++) { + int oldLen = str.length(); + for (int i = 0; i < oldLen; i++) { if (str.charAt(i) == ' ') { - str.append(" "); // 尾部填充两个 + str.append(" "); } } - int idxOfOriginal = n - 1; + int idxOfOld = oldLen - 1; int idxOfNew = str.length() - 1; - while (idxOfOriginal >= 0 && idxOfNew > idxOfOriginal) { - if (str.charAt(idxOfOriginal) == ' ') { + while (idxOfOld >= 0 && idxOfNew > idxOfOld) { + char c = str.charAt(idxOfOld--); + if (c == ' ') { str.setCharAt(idxOfNew--, '0'); str.setCharAt(idxOfNew--, '2'); str.setCharAt(idxOfNew--, '%'); } else { - str.setCharAt(idxOfNew--, str.charAt(idxOfOriginal)); + str.setCharAt(idxOfNew--, c); } - idxOfOriginal--; } return str.toString(); } ``` -## 6. 从尾到头打印链表 +# 6. 从尾到头打印链表 -**题目描述** +## 题目描述 输入链表的第一个节点,从尾到头反过来打印出每个结点的值。 -**解题思路** +## 解题思路 -栈 +### 使用栈 ```java public ArrayList printListFromTailToHead(ListNode listNode) { @@ -216,7 +253,7 @@ public ArrayList printListFromTailToHead(ListNode listNode) { } ``` -递归 +### 使用递归 ```java public ArrayList printListFromTailToHead(ListNode listNode) { @@ -229,7 +266,7 @@ public ArrayList printListFromTailToHead(ListNode listNode) { } ``` -正向遍历然后调用 Collections.reverse()。 +### 使用 Collections.reverse() ```java public ArrayList printListFromTailToHead(ListNode listNode) { @@ -243,20 +280,23 @@ public ArrayList printListFromTailToHead(ListNode listNode) { } ``` -不使用库函数,并且不使用递归。利用链表头插法为逆序的特点。 +### 使用头插法 + +利用链表头插法为逆序的特点。 头结点和第一个节点的区别:头结点是在头插法中使用的一个额外节点,这个节点不存储值;第一个节点就是链表的第一个真正存储值的节点。 ```java public ArrayList printListFromTailToHead(ListNode listNode) { - ListNode head = new ListNode(-1); // 头结点 - ListNode cur = listNode; - while (cur != null) { - ListNode next = cur.next; - cur.next = head.next; - head.next = cur; - cur = next; + // 头插法构建逆序链表 + 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) { @@ -267,14 +307,38 @@ public ArrayList printListFromTailToHead(ListNode listNode) { } ``` -## 7. 重建二叉树 +# 7. 重建二叉树 -**题目描述** +## 题目描述 根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。 +```html +For example, given + +preorder = [3,9,20,15,7] +inorder = [9,3,15,20,7] + +Return the following binary tree: + + 3 + / \ + 9 20 + / \ + 15 7 +``` + +## 解题思路 + +前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。 + ```java +private Map inOrderNumsIdx = new HashMap<>(); // 缓存中序遍历数组的每个值对应的索引 + public TreeNode reConstructBinaryTree(int[] pre, int[] in) { + for (int i = 0; i < in.length; i++) { + inOrderNumsIdx.put(in[i], i); + } return reConstructBinaryTree(pre, 0, pre.length - 1, in, 0, in.length - 1); } @@ -282,52 +346,65 @@ private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in, if (preL == preR) return new TreeNode(pre[preL]); if (preL > preR || inL > inR) return null; TreeNode root = new TreeNode(pre[preL]); - int midIdx = inL; - while (midIdx <= inR && in[midIdx] != root.val) midIdx++; - int leftTreeSize = midIdx - inL; + int inIdx = inOrderNumsIdx.get(root.val); + int leftTreeSize = inIdx - 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. 二叉树的下一个结点 -**题目描述** +## 题目描述 给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 -**解题思路** +## 解题思路 -- 如果一个节点有右子树不为空,那么该节点的下一个节点是右子树的最左节点; -- 否则,向上找第一个左链接指向的树包含该节点的祖先节点。 +① 如果一个节点有右子树不为空,那么该节点的下一个节点是右子树的最左节点; -

+

+ +② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。 + +

+ +```java +public class TreeLinkNode { + int val; + TreeLinkNode left = null; + TreeLinkNode right = null; + TreeLinkNode next = null; + + TreeLinkNode(int val) { + this.val = val; + } +} +``` ```java public TreeLinkNode GetNext(TreeLinkNode pNode) { - if (pNode == null) return null; if (pNode.right != null) { - pNode = pNode.right; - while (pNode.left != null) pNode = pNode.left; - return pNode; + TreeLinkNode node = pNode.right; + while (node.left != null) node = node.left; + return node; } else { - TreeLinkNode parent = pNode.next; - while (parent != null) { + while (pNode.next != null) { + TreeLinkNode parent = pNode.next; if (parent.left == pNode) return parent; pNode = pNode.next; - parent = pNode.next; } } return null; } ``` -## 9. 用两个栈实现队列 +# 9. 用两个栈实现队列 -**解题思路** +## 解题思路 -使用两个栈,in 栈用来处理 push 操作,out 栈用来处理 pop 操作。一个元素进过 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 pop 栈才能出栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,也就是先进先出,这就是队列的顺序。 +in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,此时先进入的元素先退出,这就是队列的顺序。 ```java Stack in = new Stack(); @@ -347,12 +424,22 @@ public int pop() { } ``` -## 10.1 斐波那契数列 +# 10.1 斐波那契数列 -**题目描述** +## 题目描述 以 O(1) 的时间复杂度求菲波那切数列。 +

+ +## 解题思路 + +如果使用递归求解,那么会重复计算一些子问题。例如,求 f(10) 需要计算 f(9) 和 f(8),计算 f(9) 需要计算 f(8) 和 f(7),可以看到 f(8) 被重复计算了。 + +

+ +递归方法是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,避免重复求解子问题。 + ```java public class Solution { private int[] fib = new int[40]; @@ -369,166 +456,202 @@ public class Solution { } ``` -## 10.2 跳台阶 +# 10.2 跳台阶 -**题目描述** +## 题目描述 一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 +## 解题思路 + ```java -public int JumpFloor(int target) { - if (target == 1) return 1; - int[] dp = new int[target]; +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 < dp.length; i++) { + for (int i = 2; i < n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } - return dp[target - 1]; + return dp[n - 1]; } ``` -## 10.3 变态跳台阶 +# 10.3 变态跳台阶 -**题目描述** +## 题目描述 一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级……它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 +## 解题思路 + ```java -public int JumpFloorII(int target) { - int[] dp = new int[target]; +public int JumpFloorII(int n) { + int[] dp = new int[n]; Arrays.fill(dp, 1); - for (int i = 1; i < target; i++) { - for (int j = 0; j < i; j++) { + for(int i = 1; i < n; i++) { + for(int j = 0; j < i; j++) { dp[i] += dp[j]; } } - return dp[target - 1]; + return dp[n - 1]; } ``` -## 10.4 矩形覆盖 +# 10.4 矩形覆盖 -**题目描述** +## 题目描述 我们可以用 2\*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2\*1 的小矩形无重叠地覆盖一个 2\*n 的大矩形,总共有多少种方法? +## 解题思路 + ```java -public int RectCover(int target) { - if (target <= 2) return target; - return RectCover(target - 1) + RectCover(target - 2); +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]; } ``` -## 11. 旋转数组的最小数字 +# 11. 旋转数组的最小数字 -**题目描述** +## 题目描述 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。NOTE:给出的所有元素都大于 0,若数组大小为 0,请返回 0。 -O(N) 时间复杂度解法: +## 解题思路 + +### 分治 + +复杂度:O(logn) + O(1),其实空间复杂度不止 O(1),因为分治使用了递归栈,用到了额外的空间,如果对空间有要求就不能用这种方法。 ```java -public int minNumberInRotateArray(int[] array) { - if (array.length == 0) return 0; - for (int i = 0; i < array.length - 1; i++) { - if (array[i] > array[i + 1]) return array[i + 1]; - } - return 0; +public int minNumberInRotateArray(int[] nums) { + return minNumberInRotateArray(nums, 0, nums.length - 1); +} + +private int minNumberInRotateArray(int[] nums, int first, int last) { + if (nums[first] < nums[last]) return nums[first]; + if (first == last) return nums[first]; + int mid = first + (last - first) / 2; + return Math.min(minNumberInRotateArray(nums, first, mid), minNumberInRotateArray(nums, mid + 1, last)); } ``` -O(lgN) 时间复杂度解法: +### 双指针 + +复杂度:O(logn) + O(1) ```java -public int minNumberInRotateArray(int[] array) { - if (array.length == 0) return 0; - int l = 0, r = array.length - 1; - int mid = -1; - while (array[l] >= array[r]) { - if (r - l == 1) return array[r]; - mid = l + (r - l) / 2; - if (array[mid] >= array[l]) l = mid; - else if (array[mid] <= array[r]) r = mid; +public int minNumberInRotateArray(int[] nums) { + if (nums.length == 0) return 0; + int l = 0, h = nums.length - 1; + while (nums[l] >= nums[h]) { + if (h - l == 1) return nums[h]; + int mid = l + (h - l) / 2; + if (nums[mid] >= nums[l]) l = mid; + else if (nums[mid] <= nums[h]) h = mid; } - return array[mid]; + return nums[l]; } ``` -## 12. 矩阵中的路径 +# 12. 矩阵中的路径 -**题目描述** +## 题目描述 请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。例如 a b c e s f c s a d e e 矩阵中包含一条字符串 "bcced" 的路径,但是矩阵中不包含 "abcb" 路径,因为字符串的第一个字符 b 占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。 +## 解题思路 + ```java private int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; private int rows; private int cols; -public boolean hasPath(char[] matrix, int rows, int cols, char[] str) { +public boolean hasPath(char[] array, int rows, int cols, char[] str) { if (rows == 0 || cols == 0) return false; this.rows = rows; this.cols = cols; - // 一维数组重建二维矩阵 - char[][] newMatrix = new char[rows][cols]; - for (int i = 0, idx = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - newMatrix[i][j] = matrix[idx++]; - } - } + boolean[][] hasUsed = new boolean[rows][cols]; + char[][] matrix = buildMatrix(array); for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { - if (backtracking(newMatrix, str, new boolean[rows][cols], 0, i, j)) return true; + if (backtracking(matrix, str, hasUsed, 0, i, j)) return true; } } return false; } -private boolean backtracking(char[][] matrix, char[] str, boolean[][] used, int pathLen, int curR, int curC) { +private boolean backtracking(char[][] matrix, char[] str, boolean[][] hasUsed, int pathLen, int row, int col) { if (pathLen == str.length) return true; - if (curR < 0 || curR >= rows || curC < 0 || curC >= cols) return false; - if (matrix[curR][curC] != str[pathLen]) return false; - if (used[curR][curC]) return false; - used[curR][curC] = true; + if (row < 0 || row >= rows || col < 0 || col >= cols) return false; + if (matrix[row][col] != str[pathLen]) return false; + if (hasUsed[row][col]) return false; + hasUsed[row][col] = true; for (int i = 0; i < next.length; i++) { - if (backtracking(matrix, str, used, pathLen + 1, curR + next[i][0], curC + next[i][1])) + if (backtracking(matrix, str, hasUsed, pathLen + 1, row + next[i][0], col + next[i][1])) return true; } - used[curR][curC] = false; + hasUsed[row][col] = 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; +} ``` -## 13. 机器人的运动范围 +# 13. 机器人的运动范围 -**题目描述** +## 题目描述 地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。例如,当 k 为 18 时,机器人能够进入方格(35, 37),因为 3+5+3+7=18。但是,它不能进入方格(35, 38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子? +## 解题思路 + ```java private int cnt = 0; private int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; +private int rows; +private int cols; +private int threshold; private int[][] digitSum; public int movingCount(int threshold, int rows, int cols) { - initDigitSum(rows, cols); - dfs(new boolean[rows][cols], threshold, rows, cols, 0, 0); + this.rows = rows; + this.cols = cols; + this.threshold = threshold; + initDigitSum(); + boolean[][] hasVisited = new boolean[rows][cols]; + dfs(hasVisited, 0, 0); return cnt; } -private void dfs(boolean[][] visited, int threshold, int rows, int cols, int r, int c) { - if (r < 0 || r >= rows || c < 0 || c >= cols) return; - if (visited[r][c]) return; - visited[r][c] = true; - if (this.digitSum[r][c] > threshold) return; +private void dfs(boolean[][] hasVisited, int r, int c) { + if (r < 0 || r >= this.rows || c < 0 || c >= this.cols) return; + if (hasVisited[r][c]) return; + hasVisited[r][c] = true; + if (this.digitSum[r][c] > this.threshold) return; this.cnt++; for (int i = 0; i < this.next.length; i++) { - dfs(visited, threshold, rows, cols, r + next[i][0], c + next[i][1]); + dfs(hasVisited, r + next[i][0], c + next[i][1]); } } -private void initDigitSum(int rows, int cols) { +private void initDigitSum() { int[] digitSumOne = new int[Math.max(rows, cols)]; for (int i = 0; i < digitSumOne.length; i++) { int n = i; @@ -538,47 +661,62 @@ private void initDigitSum(int rows, int cols) { } } this.digitSum = new int[rows][cols]; - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { + for (int i = 0; i < this.rows; i++) { + for (int j = 0; j < this.cols; j++) { this.digitSum[i][j] = digitSumOne[i] + digitSumOne[j]; } } } ``` -## 14. 剪绳子 +# 14. 剪绳子 -**题目描述** +## 题目描述 把一根绳子剪成多段,并且使得每段的长度乘积最大。 -**动态规划解法** +## 解题思路 -[分割整数](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E5%88%86%E5%89%B2%E6%95%B4%E6%95%B0) +### 动态规划解法 -**贪心解法** +```java +public int maxProductAfterCutting(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 的绳子。 +证明:当 n >= 5 时,3(n - 3) - 2(n - 2) = n - 5 >= 0。因此把长度大于 5 的绳子切成两段,令其中一段长度为 3 可以使得两段的乘积最大。 + ```java -int maxProductAfterCuttin(int length) { - if (length < 2) return 0; - if (length == 2) return 1; - if (length == 3) return 2; - int timesOf3 = length / 3; - if (length - timesOf3 * 3 == 1) timesOf3--; - int timesOf2 = (length - timesOf3 * 3) / 2; +public int maxProductAfterCutting(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 的个数 -**题目描述** +## 题目描述 -输入一个整数,输出该数二进制表示中 1 的个数。其中负数用补码表示 +输入一个整数,输出该数二进制表示中 1 的个数。 -**使用库函数** +### Integer.bitCount() ```java public int NumberOf1(int n) { @@ -586,9 +724,17 @@ public int NumberOf1(int n) { } ``` -**O(lgM) 时间复杂度解法,其中 M 表示 1 的个数** +### n&(n-1) -n&(n-1) 该位运算是去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110100,减去 1 得到 10110011,这两个数相与得到 10110000。 +O(logM) 时间复杂度解法,其中 m 表示 1 的个数。 + +该位运算是去除 n 的位级表示中最低的那一位。 + +``` +n : 10110100 +n-1 : 10110011 +n&(n-1) : 10110000 +``` ```java public int NumberOf1(int n) { @@ -601,22 +747,19 @@ public int NumberOf1(int n) { } ``` -# 第三章 高质量的代码 +# 16. 数值的整数次方 -## 16. 数值的整数次方 - -**题目描述** +## 题目描述 给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent。求 base 的 exponent 次方。 -**解题思路** +## 解题思路 -下面的讨论中 x 代表 base,N 代表 exponent。 +下面的讨论中 x 代表 base,n 代表 exponent。 -- 当 x 为偶数时,xN = (x \* x)N / 2; -- 当 x 为奇数时,xN = x \* (x \* x)N / 2; +

-因为 (x \* x)N / 2 可以通过递归求解,并且每递归一次,N 都减小一半,因此整个算法的时间复杂度为 logN。 +因为 (x\*x)n/2 可以通过递归求解,并且每递归一次,n 都减小一半,因此整个算法的时间复杂度为 O(logn)。 ```java public double Power(double base, int exponent) { @@ -633,15 +776,15 @@ public double Power(double base, int exponent) { } ``` -## 17. 打印从 1 到最大的 n 位数 +# 17. 打印从 1 到最大的 n 位数 -**题目描述** +## 题目描述 输入数字 n,按顺序打印出从 1 最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。 -**解题思路** +## 解题思路 -由于 n 可能会非常大,因此不能直接用 int 存储数,而是用 char 数组进行存储。 +由于 n 可能会非常大,因此不能直接用 int 表示数字,而是用 char 数组进行存储。 使用回溯法得到所有的数。 @@ -652,41 +795,31 @@ public void print1ToMaxOfNDigits(int n) { print1ToMaxOfNDigits(number, -1); } -private void print1ToMaxOfNDigits(char[] number, int idx) { - if (idx == number.length - 1) { +private void print1ToMaxOfNDigits(char[] number, int digit) { + if (digit == number.length - 1) { printNumber(number); return; } for (int i = 0; i < 10; i++) { - number[idx + 1] = (char) (i + '0'); - print1ToMaxOfNDigits(number, idx + 1); + number[digit + 1] = (char) (i + '0'); + print1ToMaxOfNDigits(number, digit + 1); } } - -private void printNumber(char[] number) { - boolean isBeginWith0 = true; - for (char c : number) { - if (isBeginWith0 && c != '0') isBeginWith0 = false; - if(!isBeginWith0) System.out.print(c); - } - System.out.println(); -} ``` -## 18.1 在 O(1) 时间内删除链表节点 +# 18.1 在 O(1) 时间内删除链表节点 -**解题思路** +## 解题思路 -- 如果链表不是尾节点,那么可以直接将下一个节点的值赋给节点,令节点指向下下个节点,然后删除下一个节点,时间复杂度为 O(1)。 +① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,令该节点指向下下个节点,然后删除下一个节点,时间复杂度为 O(1)。

- -- 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向节点的下一个节点,时间复杂度为 O(N)。 +② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null,时间复杂度为 O(N)。

-- 综上,如果进行 N 次操作,那么大约需要移动节点的次数为 N-1+N=2N-1,其中 N-1 表示不是链表尾节点情况下的移动次数,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) { @@ -705,58 +838,68 @@ public ListNode deleteNode(ListNode head, ListNode tobeDelete) { } ``` -## 18.2 删除链表中重复的结点 +# 18.2 删除链表中重复的结点 -**题目描述** +## 题目描述 ```html Input : 1->2->3->3->4->4->5 Output : 1->2->5 ``` -**解题描述** - -递归。 +## 解题描述 ```java public ListNode deleteDuplication(ListNode pHead) { if (pHead == null) return null; - if (pHead.next == null) return pHead; - if (pHead.val == pHead.next.val) { - ListNode next = pHead.next; - while (next != null && pHead.val == next.val) { - next = next.next; - } + ListNode next = pHead.next; + if (next == null) return pHead; + 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; } + pHead.next = deleteDuplication(pHead.next); + return pHead; } ``` -## 19. 正则表达式匹配 +# 19. 正则表达式匹配 -**题目描述** +## 题目描述 -请实现一个函数用来匹配包括 '.' 和 '\*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '\*' 表示它前面的字符可以出现任意次(包含 0 次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 "aaa" 与模式 "a.a" 和 "ab\*ac\*a" 匹配,但是与 "aa.a" 和 "ab\*a" 均不匹配. +请实现一个函数用来匹配包括 '.' 和 '\*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '\*' 表示它前面的字符可以出现任意次(包含 0 次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 "aaa" 与模式 "a.a" 和 "ab\*ac\*a" 匹配,但是与 "aa.a" 和 "ab\*a" 均不匹配。 + +## 解题思路 + +应该注意到,'.' 是用来代替一个任意字符,而 '\*' 是用来重复前面的字符。这两个的作用不同,不能把 '.' 的作用和 '\*' 进行类比,从而它当成重复前面字符一次。 + +```html +p.charAt(j) == s.charAt(i) : dp[i][j] = dp[i-1][j-1]; +p.charAt(j) == '.' : dp[i][j] = dp[i-1][j-1]; +p.charAt(j) == '*' : + p.charAt(j-1) != s.charAt(i) : dp[i][j] = dp[i][j-2] // in this case, a* only counts as empty + p.charAt(j-1) == s.charAt(i) or p.charAt(i-1) == '.': + dp[i][j] = dp[i-1][j] // in this case, a* counts as multiple a + or dp[i][j] = dp[i][j-1] // in this case, a* counts as single a + or dp[i][j] = dp[i][j-2] // in this case, a* counts as empty +``` ```java -public boolean match(char[] s, char[] p) { - int n = s.length, m = p.length; - boolean[][] dp = new boolean[n + 1][m + 1]; +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 <= m; i++) { - if (p[i - 1] == '*') { + for (int i = 1; i <= n; i++) { + if (pattern[i - 1] == '*') { dp[0][i] = dp[0][i - 2]; } } - for (int i = 1; i <= n; i++) { - for (int j = 1; j <= m; j++) { - if (s[i - 1] == p[j - 1] || p[j - 1] == '.') { + 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 (p[j - 1] == '*') { - if (p[j - 2] == s[i - 1] || p[j - 2] == '.') { + } 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]; @@ -764,74 +907,75 @@ public boolean match(char[] s, char[] p) { } } } - return dp[n][m]; + return dp[m][n]; } ``` -## 20. 表示数值的字符串 +# 20. 表示数值的字符串 -**题目描述** +## 题目描述 请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串 "+100","5e2","-123","3.1416" 和 "-1E-16" 都表示数值。 但是 "12e","1a3.14","1.2.3","+-5" 和 "12e+4.3" 都不是。 +## 解题思路 + ```java public boolean isNumeric(char[] str) { - String string = String.valueOf(str); - return string.matches("[\\+-]?[0-9]*(\\.[0-9]*)?([eE][\\+-]?[0-9]+)?"); + return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?"); } ``` -## 21. 调整数组顺序使奇数位于偶数前面 +# 21. 调整数组顺序使奇数位于偶数前面 -**题目要求** +## 题目描述 保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。 -**解题思路** +## 解题思路 -- 时间复杂度 : O(n2) -- 空间复杂度 : O(1) +复杂度:O(n2) + O(1) ```java -public void reOrderArray(int[] array) { - int n = array.length; +public void reOrderArray(int[] nums) { + int n = nums.length; for (int i = 0; i < n; i++) { - if (array[i] % 2 == 0) { + if (nums[i] % 2 == 0) { int nextOddIdx = i + 1; - while (nextOddIdx < n && array[nextOddIdx] % 2 == 0) nextOddIdx++; + while (nextOddIdx < n && nums[nextOddIdx] % 2 == 0) nextOddIdx++; if (nextOddIdx == n) break; - int nextOddVal = array[nextOddIdx]; + int nextOddVal = nums[nextOddIdx]; for (int j = nextOddIdx; j > i; j--) { - array[j] = array[j - 1]; + nums[j] = nums[j - 1]; } - array[i] = nextOddVal; + nums[i] = nextOddVal; } } } ``` -- 时间复杂度 : O(n) -- 空间复杂度 : O(n) +复杂度:O(n) + O(n) ```java -public void reOrderArray(int[] array) { +public void reOrderArray(int[] nums) { int oddCnt = 0; - for (int num : array) if (num % 2 == 1) oddCnt++; - int[] copy = array.clone(); + 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) array[i++] = num; - else array[j++] = num; + if (num % 2 == 1) nums[i++] = num; + else nums[j++] = num; } } ``` -## 22. 链表中倒数第 K 个结点 +# 22. 链表中倒数第 K 个结点 -**解题思路** +## 解题思路 设链表的长度为 N。设两个指针 P1 和 P2,先让 P1 移动 K 个节点,则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动,可以知道当 P1 移动到链表结尾时,P2 移动到 N - K 个节点处,该位置就是倒数第 K 个节点。 +## 解题思路 +

```java @@ -849,9 +993,9 @@ public ListNode FindKthToTail(ListNode head, int k) { } ``` -## 23. 链表中环的入口结点 +# 23. 链表中环的入口结点 -**解题思路** +## 解题思路 使用双指针,一个指针 fast 每次移动两个节点,一个指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。此时 fast 移动的节点数为 x+2y+z,slow 为 x+y,由于 fast 速度比 slow 快一倍,因此 x+2y+z=2(x+y),得到 x=z。 @@ -879,11 +1023,24 @@ public ListNode EntryNodeOfLoop(ListNode pHead) { } ``` -## 24. 反转链表 +# 24. 反转链表 -**解题思路** +## 解题思路 -头插法 +### 递归 + +```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; +} +``` + +### 迭代 ```java public ListNode ReverseList(ListNode head) { @@ -898,12 +1055,32 @@ public ListNode ReverseList(ListNode head) { } ``` -## 25. 合并两个排序的链表 +# 25. 合并两个排序的链表 -**题目描述** +## 题目描述

+## 解题思路 + +### 递归 + +```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; + } +} +``` + +### 迭代 + ```java public ListNode Merge(ListNode list1, ListNode list2) { ListNode head = new ListNode(-1); @@ -924,12 +1101,14 @@ public ListNode Merge(ListNode list1, ListNode list2) { } ``` -## 26. 树的子结构 +# 26. 树的子结构 -**题目描述** +## 题目描述

+## 解题思路 + ```java public boolean HasSubtree(TreeNode root1, TreeNode root2) { if (root1 == null || root2 == null) return false; @@ -945,28 +1124,32 @@ private boolean isSubtree(TreeNode root1, TreeNode root2) { } ``` -# 第四章 解决面试题的思路 +# 27. 二叉树的镜像 -## 27. 二叉树的镜像 - -**题目描述** +## 题目描述

+## 解题思路 + ```java public void Mirror(TreeNode root) { if (root == null) return; - TreeNode t = root.left; - root.left = root.right; - root.right = t; + swap(root); Mirror(root.left); Mirror(root.right); } + +private void swap(TreeNode root) { + TreeNode t = root.left; + root.left = root.right; + root.right = t; +} ``` -## 28.1 对称的二叉树 +# 28.1 对称的二叉树 -**题目描述** +## 题目描述 ```html 1 @@ -976,6 +1159,8 @@ public void Mirror(TreeNode root) { 3 4 4 3 ``` +## 解题思路 + ```java boolean isSymmetrical(TreeNode pRoot) { if (pRoot == null) return true; @@ -990,9 +1175,9 @@ boolean isSymmetrical(TreeNode t1, TreeNode t2) { } ``` -## 28.2 平衡二叉树 +# 28.2 平衡二叉树 -**题目描述** +## 题目描述 ```html 3 @@ -1004,6 +1189,8 @@ boolean isSymmetrical(TreeNode t1, TreeNode t2) { 平衡二叉树左右子树高度差不超过 1。 +## 解题思路 + ```java private boolean isBalanced = true; @@ -1021,14 +1208,18 @@ private int height(TreeNode root) { } ``` -## 29. 顺时针打印矩阵 +# 29. 顺时针打印矩阵 -**题目描述** +## 题目描述 下图的矩阵顺时针打印结果为: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<>(); @@ -1044,12 +1235,14 @@ public ArrayList printMatrix(int[][] matrix) { } ``` -## 30. 包含 min 函数的栈 +# 30. 包含 min 函数的栈 -**题目描述** +## 题目描述 定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的 min 函数。 +## 解题思路 + ```java private Stack stack = new Stack<>(); private Stack minStack = new Stack<>(); @@ -1076,30 +1269,32 @@ public int min() { } ``` -## 31. 栈的压入、弹出序列 +# 31. 栈的压入、弹出序列 -**题目描述** +## 题目描述 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列 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 i = 0, j = 0; i < n; i++) { - stack.push(pushA[i]); - while (j < n && stack.peek() == popA[j]) { + for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) { + stack.push(pushA[pushIndex]); + while (popIndex < n && stack.peek() == popA[popIndex]) { stack.pop(); - j++; + popIndex++; } } return stack.isEmpty(); } ``` -## 32.1 从上往下打印二叉树 +# 32.1 从上往下打印二叉树 -**题目描述** +## 题目描述 从上往下打印出二叉树的每个节点,同层节点从左至右打印。 @@ -1107,11 +1302,11 @@ public boolean IsPopOrder(int[] pushA, int[] popA) {

-**解题思路** +## 解题思路 使用队列来进行层次遍历。 -不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。 +不需要使用两个队列分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。 ```java public ArrayList PrintFromTopToBottom(TreeNode root) { @@ -1132,12 +1327,14 @@ public ArrayList PrintFromTopToBottom(TreeNode root) { } ``` -## 32.2 把二叉树打印成多行 +# 32.2 把二叉树打印成多行 -**题目描述** +## 题目描述 和上题几乎一样。 +## 解题思路 + ```java ArrayList> Print(TreeNode pRoot) { ArrayList> ret = new ArrayList<>(); @@ -1159,12 +1356,14 @@ ArrayList> Print(TreeNode pRoot) { } ``` -## 32.3 按之字形顺序打印二叉树 +# 32.3 按之字形顺序打印二叉树 -**题目描述** +## 题目描述 请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。 +## 解题思路 + ```java public ArrayList> Print(TreeNode pRoot) { ArrayList> ret = new ArrayList<>(); @@ -1181,21 +1380,17 @@ public ArrayList> Print(TreeNode pRoot) { if (node.left != null) queue.add(node.left); if (node.right != null) queue.add(node.right); } - if (reverse) { - Collections.reverse(list); - reverse = false; - } else { - reverse = true; - } + if (reverse) Collections.reverse(list); + reverse = !reverse; ret.add(list); } return ret; } ``` -## 33. 二叉搜索树的后序遍历序列 +# 33. 二叉搜索树的后序遍历序列 -**题目描述** +## 题目描述 输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。 @@ -1203,9 +1398,11 @@ public ArrayList> Print(TreeNode pRoot) {

+## 解题思路 + ```java public boolean VerifySquenceOfBST(int[] sequence) { - if (sequence.length == 0) return false; + if (sequence == null || sequence.length == 0) return false; return verify(sequence, 0, sequence.length - 1); } @@ -1221,9 +1418,9 @@ private boolean verify(int[] sequence, int first, int last) { } ``` -## 34. 二叉树中和为某一值的路径 +# 34. 二叉树中和为某一值的路径 -**题目描述** +## 题目描述 输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。 @@ -1231,37 +1428,39 @@ private boolean verify(int[] sequence, int first, int last) {

+## 解题思路 + ```java private ArrayList> ret = new ArrayList<>(); public ArrayList> FindPath(TreeNode root, int target) { - dfs(root, target, 0, new ArrayList<>()); + dfs(root, target, new ArrayList<>()); return ret; } -private void dfs(TreeNode node, int target, int curSum, ArrayList path) { +private void dfs(TreeNode node, int target, ArrayList path) { if (node == null) return; - curSum += node.val; path.add(node.val); - if (curSum == target && node.left == null && node.right == null) { + target -= node.val; + if (target == 0 && node.left == null && node.right == null) { ret.add(new ArrayList(path)); } else { - dfs(node.left, target, curSum, path); - dfs(node.right, target, curSum, path); + dfs(node.left, target, path); + dfs(node.right, target, path); } path.remove(path.size() - 1); } ``` -## 35. 复杂链表的复制 +# 35. 复杂链表的复制 -**题目描述** +## 题目描述 -输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空) +输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。

-**解题思路** +## 解题思路 第一步,在每个节点的后面插入复制的节点。 @@ -1309,39 +1508,45 @@ public RandomListNode Clone(RandomListNode pHead) { } ``` -## 36. 二叉搜索树与双向链表 +# 36. 二叉搜索树与双向链表 -**题目描述** +## 题目描述 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

+## 解题思路 + ```java private TreeNode pre = null; -public TreeNode Convert(TreeNode pRootOfTree) { - if(pRootOfTree == null) return null; - inOrder(pRootOfTree); - while(pRootOfTree.left != null) pRootOfTree = pRootOfTree.left; - return pRootOfTree; +private TreeNode head = null; + +public TreeNode Convert(TreeNode root) { + if (root == null) return null; + inOrder(root); + return head; } private void inOrder(TreeNode node) { - if(node == null) return; + if (node == null) return; inOrder(node.left); node.left = pre; - if(pre != null) pre.right = node; + if (pre != null) pre.right = node; pre = node; + if (head == null) head = node; inOrder(node.right); } ``` -## 37. 序列化二叉树 +# 37. 序列化二叉树 -**题目描述** +## 题目描述 请实现两个函数,分别用来序列化和反序列化二叉树。 +## 解题思路 + ```java public class Solution { @@ -1372,11 +1577,13 @@ public class Solution { } ``` -## 38. 字符串的排列 +# 38. 字符串的排列 -**题目描述** +## 题目描述 -输入一个字符串 , 按字典序打印出该字符串中字符的所有排列。例如输入字符串 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<>(); @@ -1406,67 +1613,63 @@ private void backtracking(char[] chars, boolean[] hasUsed, StringBuffer s) { } ``` -# 第五章 优化时间和空间效率 +# 39. 数组中出现次数超过一半的数字 -## 39. 数组中出现次数超过一半的数字 - -**解题思路** +## 解题思路 多数投票问题,可以利用 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 cnt = 1, num = nums[0]; - for (int i = 1; i < nums.length; i++) { - if (nums[i] == num) cnt++; - else cnt--; + 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) { - num = nums[i]; + majority = nums[i]; cnt = 1; } } - cnt = 0; - for (int i = 0; i < nums.length; i++) { - if (num == nums[i]) cnt++; - } - return cnt > nums.length / 2 ? num : 0; + int cnt = 0; + for (int val : nums) if (val == majority) cnt++; + return cnt > nums.length / 2 ? majority : 0; } ``` -## 40. 最小的 K 个数 +# 40. 最小的 K 个数 -**快速选择** +## 解题思路 -- 时间复杂度:O(N) -- 空间复杂度:O(1) -- 只有当可以修改数组元素时才可以使用 +### 快速选择 -快速排序的 partition() 方法,会返回一个整数 j 使得 a[lo..j-1] 小于等于 a[j],且 a[j+1..hi] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素,可以利用这个特性找出数组的第 K 个元素,这种找第 K 个元素的算法称为快速选择算法。 +- 复杂度:O(N) + O(1) +- 只有当允许修改数组元素时才可以使用 -找到第 K 个元素之后,就可以再遍历一次数组,所有小于等于该元素的数组元素都在最小的 K 个数中。 +快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 K 个元素,这种找第 K 个元素的算法称为快速选择算法。 + +找到第 K 个元素之后,就可以再遍历一次数组,所有小于等于该元素的数组元素都是最小的 K 个数。 ```java public ArrayList GetLeastNumbers_Solution(int[] nums, int k) { if (k > nums.length || k <= 0) return new ArrayList<>(); int kthSmallest = findKthSmallest(nums, k - 1); ArrayList ret = new ArrayList<>(); - for (int num : nums) { - if (num <= kthSmallest && ret.size() < k) ret.add(num); + for (int val : nums) { + if (val <= kthSmallest && ret.size() < k) ret.add(val); } return ret; } public int findKthSmallest(int[] nums, int k) { - int lo = 0; - int hi = nums.length - 1; - while (lo < hi) { - int j = partition(nums, lo, hi); + int l = 0; + int h = nums.length - 1; + while (l < h) { + int j = partition(nums, l, h); if (j < k) { - lo = j + 1; + l = j + 1; } else if (j > k) { - hi = j - 1; + h = j - 1; } else { break; } @@ -1474,47 +1677,42 @@ public int findKthSmallest(int[] nums, int k) { return nums[k]; } -private int partition(int[] nums, int lo, int hi) { - int i = lo; - int j = hi + 1; +private int partition(int[] nums, int l, int h) { + int i = l; + int j = h + 1; while (true) { - while (i < hi && less(nums[++i], nums[lo])) ; - while (j > lo && less(nums[lo], nums[--j])) ; + while (i < h && nums[++i] < nums[l]) ; + while (j > l && nums[l] < nums[--j]) ; if (i >= j) { break; } - exch(nums, i, j); + swap(nums, i, j); } - exch(nums, lo, j); + swap(nums, l, j); return j; } -private void exch(int[] nums, int i, int j) { - final int tmp = nums[i]; +private void swap(int[] nums, int i, int j) { + int t = nums[i]; nums[i] = nums[j]; - nums[j] = tmp; -} - -private boolean less(int v, int w) { - return v < w; + nums[j] = t; } ``` -**大小为 K 的最小堆** +### 大小为 K 的最小堆 -- 时间复杂度:O(NlgK) -- 空间复杂度:O(K) +- 复杂度:O(NlogK) + O(K) - 特别适合处理海量数据 -应该注意的是,应该使用大顶堆来维护最小堆,而不能直接创建一个小顶堆并设置一个大小,企图让小顶堆中的元素都是最小元素。 +应该使用大顶堆来维护最小堆,而不能直接创建一个小顶堆并设置一个大小,企图让小顶堆中的元素都是最小元素。 -维护一个大小为 K 的最小堆过程如下:先添加一个元素,添加完之后如果大顶堆的大小大于 K,那么需要将大顶堆的堆顶元素去除。 +维护一个大小为 K 的最小堆过程如下:在添加一个元素之后,如果大顶堆的大小大于 K,那么需要将大顶堆的堆顶元素去除。 ```java -public ArrayList GetLeastNumbers_Solution(int[] input, int k) { - if (k > input.length || k <= 0) return new ArrayList<>(); +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 : input) { + for (int num : nums) { maxHeap.add(num); if (maxHeap.size() > k) { maxHeap.poll(); @@ -1525,12 +1723,14 @@ public ArrayList GetLeastNumbers_Solution(int[] input, int k) { } ``` -## 41.1 数据流中的中位数 +# 41.1 数据流中的中位数 -**题目描述** +## 题目描述 如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。 +## 解题思路 + ```java public class Solution { // 大顶堆,存储左半边元素 @@ -1540,16 +1740,16 @@ public class Solution { // 当前数据流读入的元素个数 private int N = 0; - public void Insert(Integer num) { + public void Insert(Integer val) { // 插入要保证两个堆存于平衡状态 if (N % 2 == 0) { // N 为偶数的情况下插入到右半边。 // 因为右半边元素都要大于左半边,但是新插入的元素不一定比左半边元素来的大, // 因此需要先将元素插入左半边,然后利用左半边为大顶堆的特点,取出堆顶元素即为最大元素,此时插入右半边 - left.add(num); + left.add(val); right.add(left.poll()); } else { - right.add(num); + right.add(val); left.add(right.poll()); } N++; @@ -1565,12 +1765,14 @@ public class Solution { } ``` -## 41.2 字符流中第一个不重复的字符 +# 41.2 字符流中第一个不重复的字符 -**题目描述** +## 题目描述 请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符 "go" 时,第一个只出现一次的字符是 "g"。当从该字符流中读出前六个字符“google" 时,第一个只出现一次的字符是 "l"。 +## 解题思路 + ```java public class Solution { private int[] cnts = new int[256]; @@ -1591,29 +1793,33 @@ public class Solution { } ``` -## 42. 连续子数组的最大和 +# 42. 连续子数组的最大和 -**题目描述** +## 题目描述 -{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为 8(从第 0 个开始,到第 3 个为止) +{6,-3,-2,7,-15,1,2,2},连续子数组的最大和为 8(从第 0 个开始,到第 3 个为止)。 + +## 解题思路 ```java public int FindGreatestSumOfSubArray(int[] nums) { if (nums.length == 0) return 0; int ret = Integer.MIN_VALUE; int sum = 0; - for (int num : nums) { - if (sum <= 0) sum = num; - else sum += num; + for (int val : nums) { + if (sum <= 0) sum = val; + else sum += val; ret = Math.max(ret, sum); } return ret; } ``` -## 43. 从 1 到 n 整数中 1 出现的次数 +# 43. 从 1 到 n 整数中 1 出现的次数 -解题参考:[Leetcode : 233. Number of Digit One](https://leetcode.com/problems/number-of-digit-one/discuss/64381/4+-lines-O(log-n)-C++JavaPython) +## 解题思路 + +> [Leetcode : 233. Number of Digit One](https://leetcode.com/problems/number-of-digit-one/discuss/64381/4+-lines-O(log-n)-C++JavaPython) ```java public int NumberOf1Between1AndN_Solution(int n) { @@ -1626,12 +1832,14 @@ public int NumberOf1Between1AndN_Solution(int n) { } ``` -## 44. 数字序列中的某一位数字 +# 44. 数字序列中的某一位数字 -**题目描述** +## 题目描述 数字以 0123456789101112131415... 的格式序列化到一个字符串中,求这个字符串的第 index 位。 +## 解题思路 + ```java public int digitAtIndex(int index) { if (index < 0) return -1; @@ -1649,7 +1857,7 @@ public int digitAtIndex(int index) { /** * digit 位数的数字组成的字符串长度 - * 例如 digit = 2, return 90 + * 例如 digit = 2,return 90 */ private int getAmountOfDigit(int digit) { if (digit == 1) return 10; @@ -1675,15 +1883,15 @@ private int beginNumber(int digit) { } ``` -## 45. 把数组排成最小的数 +# 45. 把数组排成最小的数 -**题目描述** +## 题目描述 输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组 {3,32,321},则打印出这三个数字能排成的最小数字为 321323。 -**解题思路** +## 解题思路 -可以看成是一个排序问题,在比较两个字符串 S1 和 S2 的大小时,应该比较的是 S1+S2 和 S2+S1 的大小,如果 S1+S2 >= S2+S1,那么应该把 S1 排在前面,否则应该把 S2 排在前面。 +可以看成是一个排序问题,在比较两个字符串 S1 和 S2 的大小时,应该比较的是 S1+S2 和 S2+S1 的大小,如果 S1+S2 < S2+S1,那么应该把 S1 排在前面,否则应该把 S2 排在前面。 ```java public String PrintMinNumber(int[] numbers) { @@ -1697,11 +1905,13 @@ public String PrintMinNumber(int[] numbers) { } ``` -## 46. 把数字翻译成字符串 +# 46. 把数字翻译成字符串 -**题目描述** +## 题目描述 -给定一个数字,按照如下规则翻译成字符串:0 翻译成“a”,1 翻译成“b”...25 翻译成“z”。一个数字有多种翻译可能,例如 12258 一共有 5 种,分别是 bccfi,bwfi,bczi,mcfi,mzi。实现一个函数,用来计算一个数字有多少种不同的翻译方法。 +给定一个数字,按照如下规则翻译成字符串:0 翻译成“a”,1 翻译成“b”... 25 翻译成“z”。一个数字有多种翻译可能,例如 12258 一共有 5 种,分别是 bccfi,bwfi,bczi,mcfi,mzi。实现一个函数,用来计算一个数字有多少种不同的翻译方法。 + +## 解题思路 ```java public int getTranslationCount(String number) { @@ -1719,9 +1929,9 @@ public int getTranslationCount(String number) { } ``` -## 47. 礼物的最大价值 +# 47. 礼物的最大价值 -**题目描述** +## 题目描述 在一个 m\*n 的棋盘的每一个格都放有一个礼物,每个礼物都有一定价值(大于 0)。从左上角开始拿礼物,每次向右或向下移动一格,直到右下角结束。给定一个棋盘,求拿到礼物的最大价值。例如,对于如下棋盘 @@ -1734,7 +1944,7 @@ public int getTranslationCount(String number) { 礼物的最大价值为 1+12+5+7+7+16+5=53。 -**解题思路** +## 解题思路 应该用动态规划求解,而不是深度优先搜索,深度优先搜索过于复杂,不是最优解。 @@ -1754,12 +1964,14 @@ public int getMaxValue(int[][] values) { } ``` -## 48. 最长不含重复字符的子字符串 +# 48. 最长不含重复字符的子字符串 -**题目描述** +## 题目描述 输入一个字符串(只包含 a\~z 的字符),求其最长不含重复字符的子字符串的长度。例如对于 arabcacfr,最长不含重复字符的子字符串为 acfr,长度为 4。 +## 解题思路 + ```java public int longestSubStringWithoutDuplication(String str) { int curLen = 0; @@ -1780,12 +1992,14 @@ public int longestSubStringWithoutDuplication(String str) { } ``` -## 49. 丑数 +# 49. 丑数 -**题目描述** +## 题目描述 把只包含因子 2、3 和 5 的数称作丑数(Ugly Number)。例如 6、8 都是丑数,但 14 不是,因为它包含因子 7。 习惯上我们把 1 当做是第一个丑数。求按从小到大的顺序的第 N 个丑数。 +## 解题思路 + ```java public int GetUglyNumber_Solution(int N) { if (N <= 6) return N; @@ -1805,7 +2019,13 @@ public int GetUglyNumber_Solution(int N) { } ``` -## 50. 第一个只出现一次的字符位置 +# 50. 第一个只出现一次的字符位置 + +## 题目描述 + +在一个字符串 (1<=字符串长度 <=10000,全部由字母组成) 中找到第一个只出现一次的字符,并返回它的位置。 + +## 解题思路 ```java public int FirstNotRepeatingChar(String str) { @@ -1816,12 +2036,14 @@ public int FirstNotRepeatingChar(String str) { } ``` -## 51. 数组中的逆序对 +# 51. 数组中的逆序对 -**题目描述** +## 题目描述 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数 P。 +## 解题思路 + ```java private long cnt = 0; private int[] tmp; // 在这里创建辅助数组,而不是在 merge() 递归函数中创建 @@ -1832,19 +2054,19 @@ public int InversePairs(int[] nums) { return (int) (cnt % 1000000007); } -private void mergeSortUp2Down(int[] nums, int start, int end) { - if (end - start < 1) return; - int mid = start + (end - start) / 2; - mergeSortUp2Down(nums, start, mid); - mergeSortUp2Down(nums, mid + 1, end); - merge(nums, start, mid, end); +private void mergeSortUp2Down(int[] nums, int first, int last) { + if (last - first < 1) return; + int mid = first + (last - first) / 2; + mergeSortUp2Down(nums, first, mid); + mergeSortUp2Down(nums, mid + 1, last); + merge(nums, first, mid, last); } -private void merge(int[] nums, int start, int mid, int end) { - int i = start, j = mid + 1, k = start; - while (i <= mid || j <= end) { +private void merge(int[] nums, int first, int mid, int last) { + int i = first, j = mid + 1, k = first; + while (i <= mid || j <= last) { if (i > mid) tmp[k] = nums[j++]; - else if (j > end) tmp[k] = nums[i++]; + else if (j > last) tmp[k] = nums[i++]; else if (nums[i] < nums[j]) tmp[k] = nums[i++]; else { tmp[k] = nums[j++]; @@ -1852,15 +2074,15 @@ private void merge(int[] nums, int start, int mid, int end) { } k++; } - for (k = start; k <= end; k++) { + for (k = first; k <= last; k++) { nums[k] = tmp[k]; } } ``` -## 52. 两个链表的第一个公共结点 +# 52. 两个链表的第一个公共结点 -**题目描述** +## 题目描述 ```html A: a1 → a2 @@ -1870,7 +2092,7 @@ A: a1 → a2 B: b1 → b2 → b3 ``` -**解题思路** +## 解题思路 设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。 @@ -1887,11 +2109,9 @@ public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { } ``` -# 第六章 面试中的各项能力 +# 53 数字在排序数组中出现的次数 -## 53 数字在排序数组中出现的次数 - -**题目描述** +## 题目描述 ```html Input: @@ -1899,9 +2119,9 @@ Input: 3 Output: 4 -```` +``` -**解题思路** +## 解题思路 可以用二分查找找出数字在数组的最左端和最右端。 @@ -1935,23 +2155,24 @@ private int getLastK(int[] nums, int K) { } ``` -## 54. 二叉搜索树的第 k 个结点 +# 54. 二叉搜索树的第 k 个结点 -**解题思路** +## 解题思路 利用二叉搜索数中序遍历有序的特点。 ```java -TreeNode ret; -int cnt = 0; +private TreeNode ret; +private int cnt = 0; -TreeNode KthNode(TreeNode pRoot, int k) { +public TreeNode KthNode(TreeNode pRoot, int k) { inOrder(pRoot, k); return ret; } private void inOrder(TreeNode root, int k) { - if (root == null || cnt > k) return; + if (root == null) return; + if (cnt > k) return; inOrder(root.left, k); cnt++; if (cnt == k) ret = root; @@ -1959,7 +2180,7 @@ private void inOrder(TreeNode root, int k) { } ``` -## 55 二叉树的深度 +# 55 二叉树的深度 ```java public int TreeDepth(TreeNode root) { @@ -1968,13 +2189,13 @@ public int TreeDepth(TreeNode root) { } ``` -## 56. 数组中只出现一次的数字 +# 56. 数组中只出现一次的数字 -**题目描述** +## 题目描述 一个整型数组里除了两个数字之外,其他的数字都出现了两次,找出这两个数。 -**解题思路** +## 解题思路 两个不相等的元素在位级表示上必定会有一位存在不同。 @@ -1995,13 +2216,13 @@ public void FindNumsAppearOnce(int[] array, int num1[], int num2[]) { } ``` -## 57.1 和为 S 的两个数字 +# 57.1 和为 S 的两个数字 -**题目描述** +## 题目描述 输入一个递增排序的数组和一个数字 S,在数组中查找两个数,是的他们的和正好是 S,如果有多对数字的和等于 S,输出两个数的乘积最小的。 -**解题思路** +## 解题思路 使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。 @@ -2020,11 +2241,13 @@ public ArrayList FindNumbersWithSum(int[] array, int sum) { } ``` -## 57.2 和为 S 的连续正数序列 +# 57.2 和为 S 的连续正数序列 -**题目描述** +## 题目描述 -和为 100 的连续序列有 18, 19, 20, 21, 22 +和为 100 的连续序列有 18, 19, 20, 21, 22。 + +## 解题思路 ```java public ArrayList> FindContinuousSequence(int sum) { @@ -2055,15 +2278,15 @@ public ArrayList> FindContinuousSequence(int sum) { } ``` -## 58.1 翻转单词顺序列 +# 58.1 翻转单词顺序列 -**题目描述** +## 题目描述 输入:"I am a student." 输出:"student. a am I" -**解题思路** +## 解题思路 题目应该有一个隐含条件,就是不能用额外的空间。虽然 Java 的题目输入参数为 String 类型,需要先创建一个字符数组使得空间复杂度为 O(n),但是正确的参数类型应该和原书一样,为字符数组,并且只能使用该字符数组的空间。任何使用了额外空间的解法在面试时都会大打折扣,包括递归解法。正确的解法应该是和书上一样,先旋转每个单词,再旋转整个字符串。 @@ -2095,12 +2318,14 @@ private void reverse(char[] c, int start, int end) { } ``` -## 58.2 左旋转字符串 +# 58.2 左旋转字符串 -**题目描述** +## 题目描述 对于一个给定的字符序列 S,请你把其循环左移 K 位后的序列输出。例如,字符序列 S=”abcXYZdef”, 要求输出循环左移 3 位后的结果,即“XYZdefabc”。 +## 解题思路 + ```java public String LeftRotateString(String str, int k) { if (str.length() == 0) return ""; @@ -2122,11 +2347,13 @@ private void reverse(char[] c, int i, int j) { } ``` -## 59. 滑动窗口的最大值 +# 59. 滑动窗口的最大值 -**题目描述** +## 题目描述 -给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4, 4, 6, 6, 6, 5}; +给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4, 4, 6, 6, 6, 5}。 + +## 解题思路 ```java public ArrayList maxInWindows(int[] num, int size) { @@ -2145,13 +2372,17 @@ public ArrayList maxInWindows(int[] num, int size) { } ``` -## 60. n 个骰子的点数 +# 60. n 个骰子的点数 -**题目描述** +## 题目描述 把 n 个骰子仍在地上,求点数和为 s 的概率。 -最直观的动态规划解法,O(n2) 的空间复杂度。 +## 解题思路 + +### 动态规划解法 + +空间复杂度:O(n2) ```java private static int face = 6; @@ -2177,7 +2408,9 @@ public double countProbability(int n, int s) { } ``` -使用旋转数组将空间复杂度降低为 O(n) +### 动态规划解法 + 旋转数组 + +空间复杂度:O(n) ```java private static int face = 6; @@ -2205,12 +2438,14 @@ public double countProbability(int n, int s) { } ``` -## 61. 扑克牌顺子 +# 61. 扑克牌顺子 -**题目描述** +## 题目描述 五张牌,其中大小鬼为癞子,牌面大小为 0。判断是否能组成顺子。 +## 解题思路 + ```java public boolean isContinuous(int [] numbers) { if(numbers.length < 5) return false; @@ -2227,13 +2462,13 @@ public boolean isContinuous(int [] numbers) { } ``` -## 62. 圆圈中最后剩下的数 +# 62. 圆圈中最后剩下的数 -**题目描述** +## 题目描述 让小朋友们围成一个大圈。然后 , 他随机指定一个数 m, 让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌 , 然后可以在礼品箱中任意的挑选礼物 , 并且不再回到圈中 , 从他的下一个小朋友开始 , 继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友 , 可以不用表演。 -**解题思路** +## 解题思路 约瑟夫环 @@ -2245,12 +2480,14 @@ public int LastRemaining_Solution(int n, int m) { } ``` -## 63. 股票的最大利润 +# 63. 股票的最大利润 -**题目描述** +## 题目描述 可以有一次买入和一次卖出,买入必须在前。求最大收益。 +## 解题思路 + ```java public int maxProfit(int[] prices) { int n = prices.length; @@ -2265,11 +2502,13 @@ public int maxProfit(int[] prices) { } ``` -## 64. 求 1+2+3+...+n +# 64. 求 1+2+3+...+n -**题目描述** +## 题目描述 -求 1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句(A?B:C) +求 1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句(A?B:C)。 + +## 解题思路 ```java public int Sum_Solution(int n) { @@ -2279,7 +2518,7 @@ public int Sum_Solution(int n) { } ``` -## 65. 不用加减乘除做加法 +# 65. 不用加减乘除做加法 a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。 @@ -2290,12 +2529,14 @@ public int Add(int num1, int num2) { } ``` -## 66. 构建乘积数组 +# 66. 构建乘积数组 -**题目描述** +## 题目描述 给定一个数组 A[0, 1,..., n-1], 请构建一个数组 B[0, 1,..., n-1], 其中 B 中的元素 B[i]=A[0]\*A[1]\*...\*A[i-1]\*A[i+1]\*...\*A[n-1]。不能使用除法。 +## 解题思路 + ```java public int[] multiply(int[] A) { int n = A.length; @@ -2310,9 +2551,7 @@ public int[] multiply(int[] A) { } ``` -# 第七章 两个面试案例 - -## 67. 把字符串转换成整数 +# 67. 把字符串转换成整数 ```java public int StrToInt(String str) { @@ -2329,7 +2568,7 @@ public int StrToInt(String str) { } ``` -## 68. 树中两个节点的最低公共祖先 +# 68. 树中两个节点的最低公共祖先 树是二叉查找树的最低公共祖先问题: diff --git a/notes/数据库系统原理.md b/notes/数据库系统原理.md index a38880b7..cd13daa5 100644 --- a/notes/数据库系统原理.md +++ b/notes/数据库系统原理.md @@ -1,140 +1,175 @@ -* [事务四大特性](#事务四大特性) - * [1. 原子性](#1-原子性) - * [2. 一致性](#2-一致性) - * [3. 隔离性](#3-隔离性) - * [4. 持久性](#4-持久性) -* [数据不一致](#数据不一致) - * [1. 丢失修改](#1-丢失修改) - * [2. 读脏数据](#2-读脏数据) - * [3. 不可重复读](#3-不可重复读) -* [隔离级别](#隔离级别) - * [1. 未提交读(READ UNCOMMITTED)](#1-未提交读read-uncommitted) - * [2. 提交读(READ COMMITTED)](#2-提交读read-committed) - * [3. 可重复读(REPEATABLE READ)](#3-可重复读repeatable-read) - * [4. 可串行化(SERIALIXABLE)](#4-可串行化serialixable) -* [可串行化调度](#可串行化调度) -* [封锁类型](#封锁类型) -* [封锁粒度](#封锁粒度) -* [封锁协议](#封锁协议) - * [三级封锁协议](#三级封锁协议) - * [两段锁协议](#两段锁协议) -* [乐观锁和悲观锁](#乐观锁和悲观锁) - * [悲观锁](#悲观锁) - * [乐观锁](#乐观锁) - * [MySQL 隐式和显示锁定](#mysql-隐式和显示锁定) -* [范式](#范式) - * [第一范式 (1NF)](#第一范式-1nf) - * [第二范式 (2NF)](#第二范式-2nf) - * [第三范式 (3NF)](#第三范式-3nf) - * [BC 范式(BCNF)](#bc-范式bcnf) -* [约束](#约束) - * [键码](#键码) - * [单值约束](#单值约束) - * [引用完整性约束](#引用完整性约束) - * [域约束](#域约束) - * [一般约束](#一般约束) -* [数据库的三层模式和两层映像](#数据库的三层模式和两层映像) - * [外模式](#外模式) - * [模式](#模式) - * [内模式](#内模式) - * [外模式/模式映像](#外模式模式映像) - * [模式/内模式映像](#模式内模式映像) -* [ER 图](#er-图) - * [实体的三种联系](#实体的三种联系) - * [表示出现多次的关系](#表示出现多次的关系) - * [联系的多向性](#联系的多向性) - * [表示子类](#表示子类) -* [一些概念](#一些概念) +* [一、事务](#一事务) + * [概念](#概念) + * [四大特性](#四大特性) +* [二、并发一致性问题](#二并发一致性问题) + * [问题](#问题) + * [解决方法](#解决方法) +* [三、封锁](#三封锁) + * [封锁粒度](#封锁粒度) + * [封锁类型](#封锁类型) + * [封锁协议](#封锁协议) +* [四、隔离级别](#四隔离级别) +* [五、多版本并发控制](#五多版本并发控制) + * [版本号](#版本号) + * [Undo 日志](#undo-日志) + * [实现过程](#实现过程) + * [快照读与当前读](#快照读与当前读) +* [六、Next-Key Locks](#六next-key-locks) + * [Record Locks](#record-locks) + * [Grap Locks](#grap-locks) + * [Next-Key Locks](#next-key-locks) +* [七、关系数据库设计理论](#七关系数据库设计理论) + * [函数依赖](#函数依赖) + * [异常](#异常) + * [范式](#范式) +* [八、数据库系统概述](#八数据库系统概述) + * [基本术语](#基本术语) + * [数据库的三层模式和两层映像](#数据库的三层模式和两层映像) +* [九、关系数据库建模](#九关系数据库建模) + * [ER 图](#er-图) + * [十、约束](#十约束) * [参考资料](#参考资料) -# 事务四大特性 +# 一、事务 -## 1. 原子性 +## 概念 + +

+ +事务指的是满足 ACID 特性的一系列操作。在数据库中,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。 + +## 四大特性 + +

+ +### 1. 原子性(Atomicity) 事务被视为不可分割的最小单元,要么全部提交成功,要么全部失败回滚。 -## 2. 一致性 +### 2. 一致性(Consistency) 事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。 -## 3. 隔离性 +### 3. 隔离性(Isolation) -一个事务所做的修改在最终提交以前,对其它事务是不可见的。也可以理解为多个事务单独执行,互不影响。 +一个事务所做的修改在最终提交以前,对其它事务是不可见的。 -## 4. 持久性 +### 4. 持久性(Durability) -一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。持久性通过数据库备份和恢复来保证。 +一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。可以通过数据库备份和恢复来保证持久性。 -# 数据不一致 +# 二、并发一致性问题 -## 1. 丢失修改 +在并发环境下,一个事务如果受到另一个事务的影响,那么事务操作就无法满足一致性条件。 -T1 和 T2 两个事务同时对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。 +## 问题 -## 2. 读脏数据 +### 1. 丢失修改 -T1 修改后写入数据库,T2 读取这个修改后的数据,但是如果 T1 撤销了这次修改,使得 T2 读取的数据是脏数据。 +T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。 -## 3. 不可重复读 +### 2. 读脏数据 -T1 读入某个数据,T2 对该数据做了修改,如果 T1 再读这个数据,该数据已经改变,和最开始读入的是不一样的。 +T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。 -# 隔离级别 +

-数据库管理系统需要防止出现数据不一致问题,并且有多种级别可以实现,这些级别称为隔离级别。 +### 3. 不可重复读 -## 1. 未提交读(READ UNCOMMITTED) +T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和和第一次读取的结果不同。 -事务中的修改,即使没有提交,对其它事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读。 +

-## 2. 提交读(READ COMMITTED) +### 4. 幻影读 -一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所在的修改在提交之前对其它事务是不可见的。这个级别有时候也叫做不可重复读,因为两次执行同样的查询,可能会得到不一样的结果。 +T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 -## 3. 可重复读(REPEATABLE READ) +

-解决了脏读的问题,保证在同一个事务中多次读取同样的记录结果是一致的。 +## 解决方法 -但是会出现幻读的问题,所谓幻读,指的是某个事务在读取某个范围内的记录时,另一个事务会在范围内插入数据,当之前的事务再次读取该范围的记录时,会产生幻行。 +产生并发不一致性问题主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。 -## 4. 可串行化(SERIALIXABLE) +在没有并发的情况下,事务以串行的方式执行,互不干扰,因此可以保证隔离性。在并发的情况下,如果能通过并发控制,让事务的执行结果和某一个串行执行的结果相同,就认为事务的执行结果满足隔离性要求,也就是说是正确的。把这种事务执行方式称为 **可串行化调度** 。 -强制事务串行执行,避免幻读。 +**并发控制可以通过封锁来实现,但是封锁操作都要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。** -# 可串行化调度 +# 三、封锁 -如果并行的事务的执行结果和某一个串行的方式执行的结果一样,那么可以认为结果是正确的。 +## 封锁粒度 -# 封锁类型 - -排它锁 (X 锁)和共享锁 (S 锁),又称写锁和读锁。 - -- 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁; - -- 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。 - -# 封锁粒度 +

应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。 -但是加锁需要消耗资源,锁的各种操作,包括获取锁,检查所是否已经解除、释放锁,都会增加系统开销。因此封锁粒度越小,系统开销就越大。需要在锁开销以及数据安全性之间做一个权衡。 +但是加锁需要消耗资源,锁的各种操作,包括获取锁,检查锁是否已经解除、释放锁,都会增加系统开销。因此封锁粒度越小,系统开销就越大。需要在锁开销以及数据安全性之间做一个权衡。 -MySQL 中主要提供了两种封锁粒度:行级锁以及表级锁。 +MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 -# 封锁协议 +## 封锁类型 -## 三级封锁协议 +### 1. 排它锁与共享锁 -

+- 排它锁(Exclusive),简写为 X 锁,又称写锁。 +- 共享锁(Shared),简写为 S 锁,又称读锁。 + +有以下两个规定: + +1. 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。 +2. 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。 + +锁的兼容关系如下: + +| - | X | S | +| :--: | :--: | :--: | +|X|Yes|No| +|S|No|No| + +### 2. 意向锁 + +意向锁(Intention Locks)可以支持多粒度封锁。它本身是一个表锁,通过在原来的 X/S 锁之上引入了 IX/IS,用来表示一个事务想要在某个数据行上加 X 锁或 S 锁。 + +有以下两个规定: + +1. 一个事务在获得某个数据行对象的 S 锁之前,必须先获得 IS 锁或者更强的锁; +2. 一个事务在获得某个数据行对象的 X 锁之前,必须先获得 IX 锁。 + +各种锁的兼容关系如下: + +| - | X | IX | S | IS | +| :--: | :--: | :--: | :--: | :--: | +|X |No |No |No | No| +|IX |No |YES|No | Yes| +|S |No |No |Yes| Yes| +|IS |No |Yes|Yes| Yes| + +## 封锁协议 + +### 1. 三级封锁协议 **一级封锁协议** 事务 T 要修改数据 A 时必须加 X 锁,直到事务结束才释放锁。 -可以解决丢失修改问题; +可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么一个事务的修改就不会被覆盖。 + +| T1 | T1 | +| :--: | :--: | +| lock-x(A) | | +| read A=20 | | +| | lock-x(A) | +| | wait | +| write A=19 | | +| commit | | +| unlock-x(A) | | +| | obtain | +| | read A=19 | +| | write A=21 | +| | commit | +| | unlock-x(A)| **二级封锁协议** @@ -142,46 +177,198 @@ MySQL 中主要提供了两种封锁粒度:行级锁以及表级锁。 可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。 +| T1 | T1 | +| :--: | :--: | +| lock-x(A) | | +| read A=20 | | +| write A=19 | | +| | lock-s(A) | +| | wait | +| rollback | | +| A=20 | | +| unlock-x(A) | | +| | obtain | +| | read A=20 | +| | commit | +| | unlock-s(A)| + **三级封锁协议** 在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。 可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。 -## 两段锁协议 +| T1 | T1 | +| :--: | :--: | +| lock-s(A) | | +| read A=20 | | +| |lock-x(A) | +| | wait | +| read A=20| | +| commit | | +| unlock-s(A) | | +| | obtain | +| | read A=20 | +| | write A=19| +| | commit | +| | unlock-X(A)| -加锁和解锁分为两个阶段进行。两段锁是并行事务可串行化的充分条件,但不是必要条件。 +### 2. 两段锁协议 + +加锁和解锁分为两个阶段进行,事务 T 对数据 A 进行读或者写操作之前,必须先获得对 A 的封锁,并且在释放一个封锁之前,T 不能再获得任何的其它锁。 + +事务遵循两段锁协议是保证并发操作可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。 ```html lock-x(A)...lock-s(B)...lock-s(c)...unlock(A)...unlock(C)...unlock(B) ``` -# 乐观锁和悲观锁 +但不是必要条件,例如以下操作不满足两段锁协议,但是它还是可串行化调度。 -## 悲观锁 +```html +lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(c)...unlock(C)... +``` -假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。 +# 四、隔离级别 -Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被阻塞。 + **1. 未提交读(READ UNCOMMITTED)**
-## 乐观锁 +事务中的修改,即使没有提交,对其它事务也是可见的。 -假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。 + **2. 提交读(READ COMMITTED)**
-Java JUC 中的 Atomic 包就是乐观锁的一种实现,AtomicInteger 通过 CAS(Compare And Set)操作实现线程安全的自增操作。 +一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。 -乐观锁有两种实现方式,数据版本和时间戳。它们都需要在数据库表中增加一个字段,使用这个字段来判断数据是否过期。例如,数据版本实现方式中,需要在数据库表中增加一个数字类型的 version 字段,当读取数据时,将 version 字段的值一同读出。随后数据每更新一次,对此 version 值加 1。当提交更新的时候,判断读出的 version 和数据库表中的 version 是否一致,如果一致,则予以更新;否则认为是过期数据。 + **3. 可重复读(REPEATABLE READ)**
-## MySQL 隐式和显示锁定 +保证在同一个事务中多次读取同样数据的结果是一样的。 -MySQL InnoDB 采用的是两阶段锁协议。在事务执行过程中,随时都可以执行锁定,锁只有在执行 COMMIT 或者 ROLLBACK 的时候才会释放,并且所有的锁是在同一时刻被释放。前面描述的锁定都是隐式锁定,InnoDB 会根据事务隔离级别在需要的时候自动加锁。 + **4. 可串行化(SERIALIXABLE)**
-另外,InnoDB 也支持通过特定的语句进行显示锁定,这些语句不属于 SQL 规范: +强制事务串行执行。 -- SELECT ... LOCK IN SHARE MODE -- SELECT ... FOR UPDATE + **四个隔离级别的对比**
-# 范式 +| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | +| :---: | :---: | :---:| :---: | +| 未提交读 | YES | YES | YES | +| 提交读 | NO | YES | YES | +| 可重复读 | NO | NO | YES | +| 可串行化 | NO | NO | NO | + +# 五、多版本并发控制 + +(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,它的基本思想是通过保存每个数据行的多个版本,一个事务对数据行做修改时,其它事务可以读取之前的一个版本,并且都是读取相同的版本,从而保证多个事务对同一个数据行读取的结果是一致的。 + +用于实现提交读和可重复读这两种隔离级别。而对于未提交读隔离级别,它总是读取最新的数据行,无需使用 MVCC;可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 + +## 版本号 + +- 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。 +- 事务版本号:事务开始时的系统版本号。 + +InooDB 的 MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号: + +- 创建版本号:指示创建一个数据行的快照时的系统版本号; +- 删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。 + +## Undo 日志 + +InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。 + +

+ +## 实现过程 + +### 1. SELECT + +该操作必须保证多个事务读取到同一个数据行的快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。 + +当开始新一个事务时,该事务的版本号肯定会大于所有数据行快照的创建版本号,理解这一点很关键。 + +把没对一个数据行做修改的事务称为 T1,T1 所要读取的数据行快照的创建版本号必须小于当前事务的版本号,因为如果大于或者等于当前事务的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。 + +除了上面的要求,T1 所要读取的数据行快照的删除版本号必须小于当前事务版本号,因为如果大于或者等于当前事务版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。 + +### 2. INSERT + +将系统版本号作为数据行快照的创建版本号。 + +### 3. DELETE + +将系统版本号作为数据行快照的删除版本号。 + +### 4. UPDATE + +将系统版本号作为更新后的数据行快照的创建版本号,同时将系统版本号作为作为更新前的数据行快照的删除版本号。可以理解为新执行 DELETE 后执行 INSERT。 + +## 快照读与当前读 + +快照读读指的是读取快照中的数据,而当前读指的是读取最新的数据。 + +当前读: + +```sql +select * from table ....; +``` + +快照读: + +```sql +select * from table where ? lock in share mode; +select * from table where ? for update; +insert; +update ; +delete; +``` + +引入当前读的目的主要是为了免去加锁操作带来的性能开销,但是快照读需要加锁。 + +# 六、Next-Key Locks + +Next-Key Locks 也是 MySQL 的 InnoDB 存储引擎的一种实现。MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读隔离级别下,MVCC + Next-Key Locks,就可以防止幻读的出现。 + +## Record Locks + +锁定的对象时索引,而不是数据。如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Lock 依然可以使用。 + +## Grap Locks + +锁定一个范围内的索引,例如当一个事务执行以下语句,其它事务就不能在 t.c1 中插入 15。 + +```sql +SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; +``` + +## Next-Key Locks + +它是 Record Locks 和 Gap Locks 的结合。在 user 中有以下记录: + +```sql +| id | last_name | first_name | age | +|------|-------------|--------------|-------| +| 4 | stark | tony | 21 | +| 1 | tom | hiddleston | 30 | +| 3 | morgan | freeman | 40 | +| 5 | jeff | dean | 50 | +| 2 | donald | trump | 80 | ++------|-------------|--------------|-------+ +``` + +那么就需要锁定以下范围: + +```sql +(-∞, 21] +(21, 30] +(30, 40] +(40, 50] +(50, 80] +(80, ∞) +``` + +# 七、关系数据库设计理论 + +## 函数依赖 记 A->B 表示 A 函数决定 B,也可以说 B 函数依赖于 A。 @@ -189,150 +376,186 @@ MySQL InnoDB 采用的是两阶段锁协议。在事务执行过程中,随时 对于 W->A,如果能找到 W 的真子集 W',使得 W'-> A,那么 W->A 就是部分函数依赖,否则就是完全函数依赖; -以下关系中,Sno 表示学号,Sname 表示学生姓名,Sdept 表示学院,Cname 表示课程名,Mname 表示院长姓名。函数依赖为 {Sno, Cname} -> {Sname, Sdept, Mname}。 +## 异常 -

+以下的学生课程关系的函数依赖为 Sno, Cname -> Sname, Sdept, Mname, Grade,键码为 {Sno, Cname}。也就是说,确定学生和课程之后,就能确定其它信息。 -不符合范式的关系,会产生很多异常。主要有以下四种异常: +| Sno | Sname | Sdept | Mname | Cname | Grade | +| :---: | :---: | :---: | :---: | :---: |:---:| +| 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 | +| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 | +| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 | -1. 冗余数据 -2. 修改异常 -3. 删除异常 -4. 插入异常,比如如果新插入一个学生的信息,而这个学生还没选课,那么就无法插入该学生。 +不符合范式的关系,会产生很多异常,主要有以下四种异常: -关系数据库的范式理论就是是为了解决这四种异常。 +1. 冗余数据,例如学生-2 出现了两次。 +2. 修改异常,修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。 +3. 删除异常,删除一个信息,那么也会丢失其它信息。例如如果删除了课程-1,需要删除第二行和第三行,那么学生-1 的信息就会丢失。 +4. 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。 -高级别范式的依赖基于低级别的范式。 +## 范式 -## 第一范式 (1NF) +范式理论是为了解决以上提到四种异常。高级别范式的依赖于低级别的范式。 -属性不可分。 +

-## 第二范式 (2NF) +### 1. 第一范式 (1NF) + +属性不可分; + +### 2. 第二范式 (2NF) 每个非主属性完全函数依赖于键码。 可以通过分解来满足。 -**分解前** + **分解前**
-


+| Sno | Sname | Sdept | Mname | Cname | Grade | +| :---: | :---: | :---: | :---: | :---: |:---:| +| 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 | +| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 | +| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 | -


+以上学生课程关系中,{Sno, Cname} 为键码,有如下函数依赖: -**分解后** +- Sno, Cname -> Sname, Sdept, Mname +- Son -> Sname, Sdept +- Sdept -> Mname +- Sno -> Manme +- Sno, Cname-> Grade -


+Grade 完全函数依赖于键码,它没有任何冗余数据,每个学生的每门课都有特定的成绩。 -


+Sname, Sdept 和 Manme 都函数依赖于 Sno,而部分依赖于键码。当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。 -


+ **分解后**
-

+关系-1 -


+| Sno | Sname | Sdept | Mname | +| :---: | :---: | :---: | :---: | +| 1 | 学生-1 | 学院-1 | 院长-1 | +| 2 | 学生-2 | 学院-2 | 院长-2 | -


+有以下函数依赖: -

+- Sno -> Sname, Sdept, Mname +- Sdept -> Mname -## 第三范式 (3NF) +关系-2 + +| Sno | Cname | Grade | +| :---: | :---: |:---:| +| 1 | 课程-1 | 90 | +| 2 | 课程-2 | 80 | +| 2 | 课程-1 | 100 | + +有以下函数依赖: + +- Sno, Cname -> Grade + +### 3. 第三范式 (3NF) 非主属性不传递依赖于键码。 -上述 S1 存在传递依赖,Mname 依赖于 Sdept,而 Sdept 又依赖于 Sno,可以继续分解。 +上面的关系-1 中存在以下传递依赖:Sno -> Sdept -> Mname,可以进行以下分解: -

+关系-11 -## BC 范式(BCNF) +| Sno | Sname | Sdept | +| :---: | :---: | :---: | +| 1 | 学生-1 | 学院-1 | +| 2 | 学生-2 | 学院-2 | + +关系-12 + +| Sdept | Mname | +| :---: | :---: | +| 学院-1 | 院长-1 | +| 学院-2 | 院长-2 | + +### 4. BC 范式(BCNF) 所有属性不传递依赖于键码。 -关系模式 STC(Sname, Tname, Cname, Grade),其中四个属性分别为学生姓名、教师姓名、课程名和成绩。有以下函数依赖: +关系 STC(Sname, Tname, Cname, Grade) 的四个属性分别为学生姓名、教师姓名、课程名和成绩,它的键码为 (Sname, Cname, Tname),有以下函数依赖: -


+- Sname, Cname -> Tname +- Sname, Cname -> Grade +- Sname, Tname -> Cname +- Sname, Tname -> Grade +- Tname -> Cname -


+存在着以下函数传递依赖: -


+- Sname -> Tname -> Cname -


+可以分解成 SC(Sname, Cname, Grade) 和 ST(Sname, Tname),对于 ST,属性之间是多对多关系,无函数依赖。 -


+# 八、数据库系统概述 -分解成 SC(Sname, Cname, Grade) 和 ST(Sname, Tname),对于 ST,属性之间是多对多关系,无函数依赖。 +## 基本术语 -# 约束 +### 1. 数据模型 -## 键码 +由数据结构、数据操作和完整性三个要素组成。 -用于唯一表示一个实体。 +### 2. 数据库系统 -键码可以由多个属性构成,每个构成键码的属性称为码。 +数据库系统包含所有与数据库相关的内容,包括数据库、数据库管理系统、应用程序以及数据库管理员和用户,还包括相关的硬件和软件。 -## 单值约束 - -某个属性的值是唯一的。 - -## 引用完整性约束 - -一个实体的属性引用的值在另一个实体的某个属性中存在。 - -## 域约束 - -某个属性的值在特定范围之内。 - -## 一般约束 - -一般性约束,比如大小约束,数量约束。 - -# 数据库的三层模式和两层映像 +## 数据库的三层模式和两层映像 - 外模式:局部逻辑结构 - 模式:全局逻辑结构 - 内模式:物理结构 -## 外模式 +

+ +### 1. 外模式 又称用户模式,是用户和数据库系统的接口,特定的用户只能访问数据库系统提供给他的外模式中的数据。例如不同的用户创建了不同数据库,那么一个用户只能访问他有权限访问的数据库。 一个数据库可以有多个外模式,一个用户只能有一个外模式,但是一个外模式可以给多个用户使用。 -## 模式 +### 2. 模式 -可以分为概念模式和逻辑模式,概念模式可以用概念-关系来描述;逻辑模式使用特定的数据模式(比如关系模型)来描述数据的逻辑结构,这种逻辑结构包括数据的组成、数据项的名称、类型、取值范围。不仅如此,逻辑模式还要描述数据之间的关系,数据的完整性与安全性要求。 +可以分为概念模式和逻辑模式,概念模式可以用概念-关系来描述;逻辑模式使用特定的数据模式(比如关系模型)来描述数据的逻辑结构,这种逻辑结构包括数据的组成、数据项的名称、类型、取值范围。不仅如此,逻辑模式还要描述数据之间的关系、数据的完整性与安全性要求。 -## 内模式 +### 3. 内模式 又称为存储模式,描述记录的存储方式,例如索引的组织方式、数据是否压缩以及是否加密等等。 -## 外模式/模式映像 +### 4. 外模式/模式映像 把外模式的局部逻辑结构和模式的全局逻辑结构联系起来。该映像可以保证数据和应用程序的逻辑独立性。 -## 模式/内模式映像 +### 5. 模式/内模式映像 把模式的全局逻辑结构和内模式的物理结构联系起来,该映像可以保证数据和应用程序的物理独立性。 -# ER 图 +# 九、关系数据库建模 + +## ER 图 Entity-Relationship,有三个组成部分:实体、属性、联系。 -## 实体的三种联系 +### 1. 实体的三种联系 -联系包含 1 对 1,1 对多,多对多三种。 +联系包含一对一,一对多,多对多三种。 -如果 A 到 B 是 1 对多关系,那么画个带箭头的线段指向 B;如果是 1 对 1,画两个带箭头的线段;如果是多对多,画两个不带箭头的线段。下图的 Course 和 Student 是 1 对多的关系。 +如果 A 到 B 是一对多关系,那么画个带箭头的线段指向 B;如果是一对一,画两个带箭头的线段;如果是多对多,画两个不带箭头的线段。下图的 Course 和 Student 是一对多的关系。

-## 表示出现多次的关系 +### 2. 表示出现多次的关系 一个实体在联系出现几次,就要用几条线连接。下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。

-## 联系的多向性 +### 3. 联系的多向性 虽然老师可以开设多门课,并且可以教授多名学生,但是对于特定的学生和课程,只有一个老师教授,这就构成了一个三元联系。 @@ -342,24 +565,47 @@ Entity-Relationship,有三个组成部分:实体、属性、联系。

-## 表示子类 +### 4. 表示子类 -用 IS-A 联系来表示子类,具体做法是用一个三角形和两条线来连接类和子类。与子类有关的属性和联系都连到子类上,而与父类和子类都有关的连到父类上。 +用一个三角形和两条线来连接类和子类,与子类有关的属性和联系都连到子类上,而与父类和子类都有关的连到父类上。

-# 一些概念 +## 十、约束 -**数据模型** +### 1. 键码 -由数据结构、数据操作和完整性三个要素组成。 +用于唯一表示一个实体。 -**数据库系统** +键码可以由多个属性构成,每个构成键码的属性称为码。 -数据库系统包含所有与数据库相关的内容,包括数据库、数据库管理系统、应用程序以及数据库管理员和用户,还包括相关的硬件和软件。 +### 2. 单值约束 + +某个属性的值是唯一的。 + +### 3. 引用完整性约束 + +一个实体的属性引用的值在另一个实体的某个属性中存在。 + +### 4. 域约束 + +某个属性的值在特定范围之内。 + +### 5. 一般约束 + +比如大小约束,数量约束。 # 参考资料 - 史嘉权. 数据库系统概论[M]. 清华大学出版社有限公司, 2006. - 施瓦茨. 高性能MYSQL(第3版)[M]. 电子工业出版社, 2013. -- [MySQL 乐观锁与悲观锁 ](https://www.jianshu.com/p/f5ff017db62a) +- [The InnoDB Storage Engine](https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html) +- [Transaction isolation levels](https://www.slideshare.net/ErnestoHernandezRodriguez/transaction-isolation-levels) +- [Concurrency Control](http://scanftree.com/dbms/2-phase-locking-protocol) +- [The Nightmare of Locking, Blocking and Isolation Levels!](https://www.slideshare.net/brshristov/the-nightmare-of-locking-blocking-and-isolation-levels-46391666) +- [三级模式与两级映像](http://blog.csdn.net/d2457638978/article/details/48783923) +- [Database Normalization and Normal Forms with an Example](https://aksakalli.github.io/2012/03/12/database-normalization-and-normal-forms-with-an-example.html) +- [The basics of the InnoDB undo logging and history system](https://blog.jcole.us/2014/04/16/the-basics-of-the-innodb-undo-logging-and-history-system/) +- [MySQL locking for the busy web developer](https://www.brightbox.com/blog/2013/10/31/on-mysql-locks/) +- [浅入浅出 MySQL 和 InnoDB](https://draveness.me/mysql-innodb) +- [fd945daf-4a6c-4f20-b9c2-5390f5955ce5.jpg](https://tech.meituan.com/innodb-lock.html) diff --git a/notes/正则表达式.md b/notes/正则表达式.md index 52c30ff1..74c20087 100644 --- a/notes/正则表达式.md +++ b/notes/正则表达式.md @@ -1,27 +1,18 @@ -* [概述](#概述) -* [匹配单个字符](#匹配单个字符) -* [匹配一组字符](#匹配一组字符) -* [使用元字符](#使用元字符) - * [匹配空白字符](#匹配空白字符) - * [匹配特定的字符类别](#匹配特定的字符类别) - * [使用 POSIX 字符类](#使用-posix-字符类) -* [重复匹配](#重复匹配) -* [位置匹配](#位置匹配) - * [单词边界](#单词边界) - * [字符串边界](#字符串边界) -* [使用子表达式](#使用子表达式) -* [回溯引用](#回溯引用) - * [替换](#替换) - * [大小写转换](#大小写转换) -* [前后查找](#前后查找) -* [嵌入条件](#嵌入条件) - * [回溯引用条件](#回溯引用条件) - * [前后查找条件](#前后查找条件) +* [一、概述](#一概述) +* [二、匹配单个字符](#二匹配单个字符) +* [三、匹配一组字符](#三匹配一组字符) +* [四、使用元字符](#四使用元字符) +* [五、重复匹配](#五重复匹配) +* [六、位置匹配](#六位置匹配) +* [七、使用子表达式](#七使用子表达式) +* [八、回溯引用](#八回溯引用) +* [九、前后查找](#九前后查找) +* [十、嵌入条件](#十嵌入条件) -# 概述 +# 一、概述 正则表达式用于文本内容的查找和替换。 @@ -31,7 +22,7 @@ [正则表达式在线工具](http://tool.chinaz.com/regex) -# 匹配单个字符 +# 二、匹配单个字符 正则表达式一般是区分大小写的,但是也有些实现是不区分。 @@ -49,7 +40,7 @@ nam. My **name** is Zheng. -# 匹配一组字符 +# 三、匹配一组字符 **[ ]** 定义一个字符集合; @@ -73,7 +64,7 @@ abc[^0-9] 2. abc1 3. abc2 -# 使用元字符 +# 四、使用元字符 ## 匹配空白字符 @@ -92,21 +83,21 @@ abc[^0-9] ## 匹配特定的字符类别 -**1. 数字元字符** +### 1. 数字元字符 | 元字符 | 说明 | | ------------ | ------------ | | \d | 数字字符,等价于 [0-9] | | \D | 非数字字符,等价于 [^0-9] | -**2. 字母数字元字符** +### 2. 字母数字元字符 | 元字符 | 说明 | | ------------ | ------------ | | \w | 大小写字母,下划线和数字,等价于 [a-zA-Z0-9\_] | | \W | 对 \w 取非 | -**3. 空白字符元字符** +### 3. 空白字符元字符 | 元字符 | 说明 | | ------------ | ------------ | @@ -135,7 +126,7 @@ abc[^0-9] 使用时需要用两对方括号,例如 [[:alpha:]]。 -# 重复匹配 +# 五、重复匹配 **\+** 匹配 1 个或者多个字符, **\*** 匹配 0 个或者多个,**?** 匹配 0 个或者 1 个。 @@ -178,7 +169,7 @@ a.+c **abcabcabc** -# 位置匹配 +# 六、位置匹配 ## 单词边界 @@ -216,7 +207,7 @@ a.+c 6.      int c = a + b; 7. } -# 使用子表达式 +# 七、使用子表达式 使用 **( )** 定义一个子表达式。子表达式的内容可以当成一个独立元素,即可以将它看成一个字符,并且使用 * 等元字符。 @@ -264,7 +255,7 @@ a.+c 1. **192.168.0.1** 2. 555.555.555.555 -# 回溯引用 +# 八、回溯引用 回溯引用使用 **\n** 来引用某个子表达式,其中 n 代表的是子表达式的序号,从 1 开始。它和子表达式匹配的内容一致,比如子表达式匹配到 abc ,那么回溯引用部分也需要匹配 abc 。 @@ -350,7 +341,7 @@ $1\U$2\E$3 aBCd -# 前后查找 +# 九、前后查找 前后查找规定了匹配的内容首尾应该匹配的内容,但是又不包含首尾匹配的内容。向前查找用 **?=** 来定义,它规定了尾部匹配的内容,这个匹配的内容在 ?= 之后定义。所谓向前查找,就是规定了一个匹配的内容,然后以这个内容为尾部向前面查找需要匹配的内容。向后匹配用 ?<= 定义。 @@ -370,7 +361,7 @@ aBCd 对向前和向后查找取非,只要把 = 替换成 ! 即可,比如 (?=) 替换成 (?!) 。取非操作使得匹配那些首尾不符合要求的内容。 -# 嵌入条件 +# 十、嵌入条件 ## 回溯引用条件 diff --git a/notes/算法.md b/notes/算法.md index 5c57bf1e..df40054a 100644 --- a/notes/算法.md +++ b/notes/算法.md @@ -1,133 +1,77 @@ -* [算法分析](#算法分析) - * [1. 函数转换](#1-函数转换) - * [2. 数学模型](#2-数学模型) - * [3. ThreeSum](#3-threesum) - * [4. 倍率实验](#4-倍率实验) - * [5. 注意事项](#5-注意事项) -* [栈和队列](#栈和队列) - * [1. 栈](#1-栈) - * [2. 队列](#2-队列) -* [union-find](#union-find) - * [1. quick-find 算法](#1-quick-find-算法) - * [2. quick-union 算法](#2-quick-union-算法) - * [3. 加权 quick-union 算法](#3-加权-quick-union-算法) - * [4. 路径压缩的加权 quick-union 算法](#4-路径压缩的加权-quick-union-算法) - * [5. 各种 union-find 算法的比较](#5-各种-union-find-算法的比较) -* [排序](#排序) - * [1. 初级排序算法](#1-初级排序算法) - * [1.1 约定](#11-约定) - * [1.2 选择排序](#12-选择排序) - * [1.3 插入排序](#13-插入排序) - * [1.4 选择排序和插入排序的比较](#14-选择排序和插入排序的比较) - * [1.5 希尔排序](#15-希尔排序) - * [2 归并排序](#2-归并排序) - * [2.1 归并方法](#21-归并方法) - * [2.2 自顶向下归并排序](#22-自顶向下归并排序) - * [2.3 自底向上归并排序](#23-自底向上归并排序) - * [3. 快速排序](#3-快速排序) - * [3.1 基本算法](#31-基本算法) - * [3.2 切分](#32-切分) - * [3.3 性能分析](#33-性能分析) - * [3.4 算法改进](#34-算法改进) - * [3.4.1 切换到插入排序](#341-切换到插入排序) - * [3.4.2 三取样](#342-三取样) - * [3.4.3 三向切分](#343-三向切分) - * [4. 优先队列](#4-优先队列) - * [4.1 堆](#41-堆) - * [4.2 上浮和下沉](#42-上浮和下沉) - * [4.3 插入元素](#43-插入元素) - * [4.4 删除最大元素](#44-删除最大元素) - * [4.5 堆排序](#45-堆排序) - * [4.6 分析](#46-分析) - * [5. 应用](#5-应用) - * [5.1 排序算法的比较](#51-排序算法的比较) - * [5.2 Java 的排序算法实现](#52-java-的排序算法实现) - * [5.3 基于切分的快速选择算法](#53-基于切分的快速选择算法) -* [查找](#查找) - * [1. 符号表](#1-符号表) - * [1.1 无序符号表](#11-无序符号表) - * [1.2 有序符号表](#12-有序符号表) - * [1.3 二分查找实现有序符号表](#13-二分查找实现有序符号表) - * [2. 二叉查找树](#2-二叉查找树) - * [2.1 get()](#21-get) - * [2.2 put()](#22-put) - * [2.3 分析](#23-分析) - * [2.4 floor()](#24-floor) - * [2.5 rank()](#25-rank) - * [2.6 min()](#26-min) - * [2.7 deleteMin()](#27-deletemin) - * [2.8 delete()](#28-delete) - * [2.9 keys()](#29-keys) - * [2.10 性能分析](#210-性能分析) - * [3. 平衡查找树](#3-平衡查找树) - * [3.1 2-3 查找树](#31-2-3-查找树) - * [3.1.1 插入操作](#311-插入操作) - * [3.1.2 性质](#312-性质) - * [3.2 红黑二叉查找树](#32-红黑二叉查找树) - * [3.2.1 左旋转](#321-左旋转) - * [3.2.2 右旋转](#322-右旋转) - * [3.2.3 颜色转换](#323-颜色转换) - * [3.2.4 插入](#324-插入) - * [3.2.5 删除最小键](#325-删除最小键) - * [3.2.6 分析](#326-分析) - * [4. 散列表](#4-散列表) - * [4.1 散列函数](#41-散列函数) - * [4.2 基于拉链法的散列表](#42-基于拉链法的散列表) - * [4.3 基于线性探测法的散列表](#43-基于线性探测法的散列表) - * [4.3.1 查找](#431-查找) - * [4.3.2 插入](#432-插入) - * [4.3.3 删除](#433-删除) - * [4.3.4 调整数组大小](#434-调整数组大小) - * [5. 应用](#5-应用) - * [5.1 各种符号表实现的比较](#51-各种符号表实现的比较) - * [5.2 Java 的符号表实现](#52-java-的符号表实现) - * [5.3 集合类型](#53-集合类型) - * [5.4 稀疏向量乘法](#54-稀疏向量乘法) +* [一、算法分析](#一算法分析) + * [函数转换](#函数转换) + * [数学模型](#数学模型) + * [ThreeSum](#threesum) + * [倍率实验](#倍率实验) + * [注意事项](#注意事项) +* [二、栈和队列](#二栈和队列) + * [栈](#栈) + * [队列](#队列) +* [三、union-find](#三union-find) + * [quick-find](#quick-find) + * [quick-union](#quick-union) + * [加权 quick-union](#加权-quick-union) + * [路径压缩的加权 quick-union](#路径压缩的加权-quick-union) + * [各种 union-find 算法的比较](#各种-union-find-算法的比较) +* [四、排序](#四排序) + * [选择排序](#选择排序) + * [插入排序](#插入排序) + * [希尔排序](#希尔排序) + * [归并排序](#归并排序) + * [快速排序](#快速排序) + * [优先队列](#优先队列) + * [应用](#应用) +* [五、查找](#五查找) + * [符号表](#符号表) + * [二叉查找树](#二叉查找树) + * [2-3 查找树](#2-3-查找树) + * [红黑二叉查找树](#红黑二叉查找树) + * [散列表](#散列表) + * [应用](#应用) -# 算法分析 +# 一、算法分析 -## 1. 函数转换 +## 函数转换 指数函数可以转换为线性函数,从而在函数图像上显示的更直观。例如 -


+

可以在其两端同时取对数,得到: -


+

-

+

-## 2. 数学模型 +## 数学模型 -**近似** +### 1. 近似 -使用 \~f(N) 来表示所有随着 N 的增大除以 f(N) 的结果趋近于 1 的函数 , 例如 N3/6-N2/2+N/3 \~ N3/6。 +使用 \~f(N) 来表示所有随着 N 的增大除以 f(N) 的结果趋近于 1 的函数,例如 N3/6-N2/2+N/3 \~ N3/6。 -

+

-**增长数量级** +### 2. 增长数量级 增长数量级将算法与它的实现隔离开来,一个算法的增长数量级为 N3 与它是否用 Java 实现,是否运行于特定计算机上无关。 -

+

-**内循环** +### 3. 内循环 执行最频繁的指令决定了程序执行的总时间,把这些指令称为程序的内循环。 -**成本模型** +### 4. 成本模型 使用成本模型来评估算法,例如数组的访问次数就是一种成本模型。 -## 3. ThreeSum +## ThreeSum ThreeSum 用于统计一个数组中三元组的和为 0 的数量。 - ```java public class ThreeSum { public static int count(int[] a) { @@ -149,7 +93,7 @@ public class ThreeSum { 该算法的内循环为`if (a[i] + a[j] + a[k] == 0)`语句,总共执行的次数为 N(N-1)(N-2) = N3/6 - N2/2 + N/3,因此它的近似执行次数为 \~N3/6,增长数量级为 N3。 -**改进** + **改进**
通过将数组先排序,对两个元素求和,并用二分查找方法查找是否存在该和的相反数,如果存在,就说明存在三元组的和为 0。 @@ -175,47 +119,47 @@ public class ThreeSumFast { } ``` -## 4. 倍率实验 +## 倍率实验 -如果 T(N) \~ aNblgN,那么 T(2N)/T(N) \~ 2b。 +如果 T(N) \~ aNblogN,那么 T(2N)/T(N) \~ 2b。 例如对于暴力方法的 ThreeSum 算法,近似时间为 \~N3/6。进行如下实验:多次运行该算法,每次取的 N 值为前一次的两倍,统计每次执行的时间,并统计本次运行时间与前一次运行时间的比值,得到如下结果: -

+

-可以看到,T(2N)/T(N)\~23,因此可以确定 T(N) \~ aN2lgN。 +可以看到,T(2N)/T(N) \~ 23,因此可以确定 T(N) \~ aN2logN。 -## 5. 注意事项 +## 注意事项 -**大常数** +### 1. 大常数 在求近似时,如果低级项的常数系数很大,那么近似的结果就是错误的。 -**缓存** +### 2. 缓存 计算机系统会使用缓存技术来组织内存,访问数组相邻的元素会比访问不相邻的元素快很多。 -**对最坏情况下的性能的保证** +### 3. 对最坏情况下的性能的保证 在核反应堆、心脏起搏器或者刹车控制器中的软件,最坏情况下的性能是十分重要的。 -**随机化算法** +### 4. 随机化算法 通过打乱输入,去除算法对输入的依赖。 -**均摊分析** +### 5. 均摊分析 将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的元素为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素,其余的都是调整数组大小时进行复制需要的访问数组操作),均摊后每次操作访问数组的平均次数为常数。 -# 栈和队列 +# 二、栈和队列 -## 1. 栈 +## 栈 first-in-last-out(FILO) -

+

-**数组实现** + **1. 数组实现**
```java public class ResizeArrayStack implements Iterable { @@ -282,9 +226,9 @@ public class ResizeArrayStack implements Iterable { Item[] arr = (Item[]) new Object[N]; ``` -**链表实现** + **2. 链表实现**
-需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素使就可以让前一个压入栈的元素称为栈顶元素。 +需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素使就可以通过 next 指针遍历到前一个压入栈的元素从而让这个元素称为新的栈顶元素。 ```java public class Stack { @@ -321,16 +265,15 @@ public class Stack { } } ``` -## 2. 队列 +## 队列 first-in-first-out(FIFO) -

- +

下面是队列的链表实现,需要维护 first 和 last 节点指针,分别指向队首和队尾。 -这里需要考虑让哪个指针指针链表头部节点,哪个指针指向链表尾部节点。因为出队列操作需要让队首元素的下一个元素成为队首,就需要容易获取下一个元素,而链表的头部节点的 next 指针指向下一个元素,因此让队首指针 first 指针链表的开头。 +这里需要考虑 first 和 last 指针哪个作为链表的开头。因为出队列操作需要让队首元素的下一个元素成为队首,所以需要容易获取下一个元素,而链表的头部节点的 next 指针指向下一个元素,因此可以让 first 指针链表的开头。 ```java public class Queue { @@ -375,19 +318,19 @@ public class Queue { } ``` -# union-find +# 三、union-find -**概览** + **概览**
-用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连接。 +用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连通。 -

+

-**API** + **API**
-

+

-**基本数据结构** + **基本数据结构**
```java public class UF { @@ -407,11 +350,11 @@ public class UF { } ``` -## 1. quick-find 算法 +## quick-find -保证在同一连通分量的所有触点的 id 值相等。 +保证在同一连通分量的所有节点的 id 值相等。 -这种方法可以快速取得一个触点的 id 值,并且判断两个触点是否连通,但是 union 的操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。 +这种方法可以快速取得一个节点的 id 值,并且判断两个节点是否连通。但是 union 的操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。 ```java public int find(int p) { @@ -428,11 +371,11 @@ public class UF { } ``` -## 2. quick-union 算法 +## quick-union -在 union 时只将触点的 id 值指向另一个触点 id 值,不直接用 id 来存储所属的连通分量。这样就构成一个倒置的树形结构,根节点需要指向自己。在进行查找一个节点所属的连通分量时,要一直向上查找直到根节点,并使用根节点的 id 值作为本连通分量的 id 值。 +在 union 时只将节点的 id 值指向另一个节点 id 值,不直接用 id 来存储所属的连通分量。这样就构成一个倒置的树形结构,应该注意的是根节点需要指向自己。查找一个节点所属的连通分量时,要一直向上查找直到根节点,并使用根节点的 id 值作为本连通分量的 id 值。 -

+

```java public int find(int p) { @@ -450,15 +393,15 @@ public class UF { 这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为触点的数目。 -

+

-## 3. 加权 quick-union 算法 +## 加权 quick-union 为了解决 quick-union 的树通常会很高的问题,加权 quick-union 在 union 操作时会让较小的树连接较大的树上面。 -理论研究证明,加权 quick-union 算法构造的树深度最多不超过 lgN。 +理论研究证明,加权 quick-union 算法构造的树深度最多不超过 logN。 -

+

```java public class WeightedQuickUnionUF { @@ -499,21 +442,19 @@ public class WeightedQuickUnionUF { } ``` -## 4. 路径压缩的加权 quick-union 算法 +## 路径压缩的加权 quick-union 在检查节点的同时将它们直接链接到根节点,只需要在 find 中添加一个循环即可。 -## 5. 各种 union-find 算法的比较 +## 各种 union-find 算法的比较 -

+

-# 排序 +# 四、排序 -## 1. 初级排序算法 + **约定**
-### 1.1 约定 - -待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法。 +待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法,可以用它来判断两个元素的大小关系。 研究排序算法的成本模型时,计算的是比较和交换的次数。 @@ -531,11 +472,11 @@ private void exch(Comparable[] a, int i, int j){ } ``` -### 1.2 选择排序 +## 选择排序 找到数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 -

+

```java public class Selection { @@ -554,11 +495,11 @@ public class Selection { 选择排序需要 \~N2/2 次比较和 \~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。 -### 1.3 插入排序 +## 插入排序 -入排序从左到右进行,每次都将当前元素插入到左部已经排序的数组中,使得插入之后左部数组依然有序。 +插入排序从左到右进行,每次都将当前元素插入到左部已经排序的数组中,使得插入之后左部数组依然有序。 -

+

```java public class Insertion { @@ -577,11 +518,11 @@ public class Insertion { 插入排序对于部分有序数组和小规模数组特别高效。 -### 1.4 选择排序和插入排序的比较 + **选择排序和插入排序的比较**
-对于随机排序的无重复主键的数组,插入排序和选择排序的运行时间是平方级别的,两者之比是一个较小的常数。 +对于随机排列的无重复主键的数组,插入排序和选择排序的运行时间是平方级别的,两者之比是一个较小的常数。 -### 1.5 希尔排序 +## 希尔排序 对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,如果要把元素从一端移到另一端,就需要很多次操作。 @@ -589,7 +530,9 @@ public class Insertion { 希尔排序使用插入排序对间隔 h 的序列进行排序,如果 h 很大,那么元素就能很快的移到很远的地方。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。 -

+

+ +

```java public class Shell { @@ -613,13 +556,15 @@ public class Shell { 希尔排序的运行时间达不到平方级别,使用递增序列 1, 4, 13, 40, ... 的希尔排序所需要的比较次数不会超过 N 的若干倍乘于递增序列的长度。后面介绍的高级排序算法只会比希尔排序快两倍左右。 -## 2 归并排序 +## 归并排序 归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。 -

+

-### 2.1 归并方法 +

+ +### 1. 归并方法 归并方法将数组中两个已经排序的部分归并成一个。 @@ -644,7 +589,9 @@ public class MergeSort { } ``` -### 2.2 自顶向下归并排序 +### 2. 自顶向下归并排序 + +

```java public static void sort(Comparable[] a) { @@ -661,19 +608,15 @@ private static void sort(Comparable[] a, int lo, int hi) { } ``` -

+因为每次都将问题对半分成两个子问题,而这种对半分的算法复杂度一般为 O(NlogN),因此该归并排序方法的时间复杂度也为 O(NlogN)。 -

+因为小数组的递归操作会过于频繁,因此可以在数组过小时切换到插入排序来提高性能。 -因为每次都将问题对半分成两个子问题,而这种对半分的算法复杂度一般为 O(NlgN),因此该归并排序方法的时间复杂度也为 O(NlgN)。 - -因为小数组的递归操作会过于频繁,因此使用插入排序来处理小数组将会获得更高的性能。 - -### 2.3 自底向上归并排序 +### 3. 自底向上归并排序 先归并那些微型数组,然后成对归并得到的子数组。 -

+

```java public static void busort(Comparable[] a) { @@ -687,13 +630,13 @@ public static void busort(Comparable[] a) { } ``` -## 3. 快速排序 +## 快速排序 -### 3.1 基本算法 +### 1. 基本算法 归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序;快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。 -

+

```java public class QuickSort { @@ -711,11 +654,11 @@ public class QuickSort { } ``` -### 3.2 切分 +### 2. 切分 -取 a[lo] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于等于它的元素,交换这两个元素,并不断继续这个过程,就可以保证左指针的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[lo] 和左子数组最右侧的元素 a[j] 交换然后返回 j 即可。 +取 a[lo] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于等于它的元素,交换这两个元素,并不断进行这个过程,就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[lo] 和左子数组最右侧的元素 a[j] 交换然后返回 j 即可。 -

+

```java private static int partition(Comparable[] a, int lo, int hi) { @@ -732,31 +675,31 @@ private static int partition(Comparable[] a, int lo, int hi) { } ``` -### 3.3 性能分析 +### 3. 性能分析 快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。 -快速排序最好的情况下是每次都正好能将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为 CN=2CN/2+N,也就是复杂度为 O(NlgN)。 +快速排序最好的情况下是每次都正好能将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为 CN=2CN/2+N,复杂度为 O(NlogN)。 最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较 N2/2。为了防止数组最开始就是有序的,在进行快速排序时需要随机打乱数组。 -### 3.4 算法改进 +### 4. 算法改进 -#### 3.4.1 切换到插入排序 +**(一)切换到插入排序** 因为快速排序在小数组中也会调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。 -#### 3.4.2 三取样 +**(二)三取样** 最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。人们发现取 3 个元素并将大小居中的元素作为切分元素的效果最好。 -#### 3.4.3 三向切分 +**(三)三向切分** 对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。 三向切分快速排序对于只有若干不同主键的随机数组可以在线性时间内完成排序。 -

+

```java public class Quick3Way { @@ -776,17 +719,17 @@ public class Quick3Way { } ``` -## 4. 优先队列 +## 优先队列 优先队列主要用于处理最大元素。 -### 4.1 堆 +### 1. 堆 定义:一颗二叉树的每个节点都大于等于它的两个子节点。 堆可以用数组来表示,因为堆是一种完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里我们不使用数组索引为 0 的位置,是为了更清晰地理解节点的关系。 -

+

```java public class MaxPQ { @@ -817,7 +760,7 @@ public class MaxPQ { } ``` -### 4.2 上浮和下沉 +### 2. 上浮和下沉 在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作。把这种操作称为上浮。 @@ -844,7 +787,7 @@ private void sink(int k) { } ``` -### 4.3 插入元素 +### 3. 插入元素 将新元素放到数组末尾,然后上浮到合适的位置。 @@ -855,7 +798,7 @@ public void insert(Key v) { } ``` -### 4.4 删除最大元素 +### 4. 删除最大元素 从数组顶端删除最大的元素,并将数组的最后一个元素放到顶端,并让这个元素下沉到合适的位置。 @@ -869,7 +812,7 @@ public Key delMax() { } ``` -### 4.5 堆排序 +### 5. 堆排序 由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序,并且堆排序是原地排序,不占用额外空间。 @@ -877,7 +820,7 @@ public Key delMax() { 无序数组建立堆最直接的方法是从左到右遍历数组,然后进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,因此可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。 -

+

```java public static void sort(Comparable[] a){ @@ -892,29 +835,29 @@ public static void sort(Comparable[] a){ } ``` -### 4.6 分析 +### 6. 分析 -一个堆的高度为 lgN,因此在堆中插入元素和删除最大元素的复杂度都为 lgN。 +一个堆的高度为 logN,因此在堆中插入元素和删除最大元素的复杂度都为 logN。 -对于堆排序,由于要对 N 个节点进行下沉操作,因此复杂度为 NlgN。 +对于堆排序,由于要对 N 个节点进行下沉操作,因此复杂度为 NlogN。 堆排序时一种原地排序,没有利用额外的空间。 现代操作系统很少使用堆排序,因为它无法利用缓存,也就是数组元素很少和相邻的元素进行比较。 -## 5. 应用 +## 应用 -### 5.1 排序算法的比较 +### 1. 排序算法的比较 -

+

-快速排序时最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间增长数量级为 \~cNlgN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分之后,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 +快速排序时最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间增长数量级为 \~cNlogN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分之后,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 -### 5.2 Java 的排序算法实现 +### 2. Java 的排序算法实现 Java 系统库中的主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。 -### 5.3 基于切分的快速选择算法 +### 3. 基于切分的快速选择算法 快速排序的 partition() 方法,会返回一个整数 j 使得 a[lo..j-1] 小于等于 a[j],且 a[j+1..hi] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。 @@ -935,33 +878,31 @@ public static Comparable select(Comparable[] a, int k) { 该算法是线性级别的,因为每次正好将数组二分,那么比较的总次数为 (N+N/2+N/4+..),直到找到第 k 个元素,这个和显然小于 2N。 -# 查找 +# 五、查找 -本章使用三种经典的数据结构来实现高效的符号表:二叉查找树、红黑树和散列表。 +符号表是一种存储键值对的数据结构,支持两种操作:插入一个新的键值对;根据给定键得到值。 -## 1. 符号表 +## 符号表 -### 1.1 无序符号表 +### 1. 无序符号表 -

+

-### 1.2 有序符号表 +### 2. 有序符号表 -

+

+ +有序指的是支持 min() max() 等根据键的大小关系来实现的操作。 有序符号表的键需要实现 Comparable 接口。 -查找的成本模型:键的比较次数,在不进行比较时使用数组的访问次数。 +### 3. 二分查找实现有序符号表 -### 1.3 二分查找实现有序符号表 - -使用一对平行数组,一个存储键一个存储值。 - -需要创建一个 Key 类型的 Comparable 对象数组和一个 Value 类型的 Object 对象数组。 +使用一对平行数组,一个存储键一个存储值。其中键的数组为 Comparable 数组,值的数字为 Object 数组。 rank() 方法至关重要,当键在表中时,它能够知道该键的位置;当键不在表中时,它也能知道在何处插入新键。 -复杂度:二分查找最多需要 lgN+1 次比较,使用二分查找实现的符号表的查找操作所需要的时间最多是对数级别的。但是插入操作需要移动数组元素,是线性级别的。 +复杂度:二分查找最多需要 logN+1 次比较,使用二分查找实现的符号表的查找操作所需要的时间最多是对数级别的。但是插入操作需要移动数组元素,是线性级别的。 ```java public class BinarySearchST, Value> { @@ -1020,15 +961,21 @@ public class BinarySearchST, Value> { } ``` -## 2. 二叉查找树 +## 二叉查找树 **二叉树** 定义为一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一颗子二叉树。 -**二叉查找树** (BST)是一颗二叉树,并且每个节点的键都大于其左子树中的任意节点的键而小于右子树的任意节点的键。 +

-

+**二叉查找树** (BST)是一颗二叉树,并且每个节点的值都大于其左子树中的所有节点的值而小于右子树的所有节点的值。 -二叉查找树的查找操作每次迭代都会让区间减少一半,和二分查找类似。 +

+ +BST 有一个重要性质,就是它的前序遍历结果递增排序。 + +

+ +基本数据结构: ```java public class BST, Value> { @@ -1059,9 +1006,13 @@ public class BST, Value> { } ``` -### 2.1 get() +### 1. get() -如果树是空的,则查找未命中;如果被查找的键和根节点的键相等,查找命中,否则递归地在子树中查找:如果被查找的键较小就在左子树中查找,较大就在右子树中查找。 +- 如果树是空的,则查找未命中; +- 如果被查找的键和根节点的键相等,查找命中; +- 否则递归地在子树中查找:如果被查找的键较小就在左子树中查找,较大就在右子树中查找。 + +BST 的查找操作每次递归都会让区间减少一半,和二分查找类似,因此查找的复杂度为 O(logn)。 ```java public Value get(Key key) { @@ -1076,10 +1027,12 @@ private Value get(Node x, Key key) { } ``` -### 2.2 put() +### 2. put() 当插入的键不存在于树中,需要创建一个新节点,并且更新上层节点的链接使得该节点正确链接到树中。 +

+ ```java public void put(Key key, Value val) { root = put(root, key, val); @@ -1095,17 +1048,20 @@ private Node put(Node x, Key key, Value val) { } ``` -### 2.3 分析 +### 3. 分析 -二叉查找树的算法运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 lgN。在最坏的情况下,树的高度为 N。 +二叉查找树的算法运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 logN。在最坏的情况下,树的高度为 N。 -

+

-复杂度:查找和插入操作都为对数级别。 +### 4. floor() -### 2.4 floor() +floor(key):小于等于键的最大键 -如果 key 小于根节点的 key,那么小于等于 key 的最大键节点一定在左子树中;如果 key 大于根节点的 key,只有当根节点右子树中存在小于等于 key 的节点,小于等于 key 的最大键节点才在右子树中,否则根节点就是小于等于 key 的最大键节点。 +- 如果键小于根节点的键,那么 floor(key) 一定在左子树中; +- 如果键大于根节点的键,需要先判断右子树中是否存在 floor(key),如果存在就找到,否则根节点就是 floor(key)。 + +

```java public Key floor(Key key) { @@ -1127,7 +1083,13 @@ private Node floor(Node x, Key key) { } ``` -### 2.5 rank() +### 5. rank() + +rank(key) 返回 key 的排名。 + +- 如果键和根节点的键相等,返回左子树的节点数; +- 如果小于,递归计算在左子树中的排名; +- 如果大于,递归计算在右子树中的排名,并加上左子树的节点数,再加上 1(根节点)。 ```java public int rank(Key key) { @@ -1142,7 +1104,7 @@ private int rank(Key key, Node x) { } ``` -### 2.6 min() +### 6. min() ```java private Node min(Node x) { @@ -1151,11 +1113,11 @@ private Node min(Node x) { } ``` -### 2.7 deleteMin() +### 7. deleteMin() 令指向最小节点的链接指向最小节点的右子树。 -

+

```java public void deleteMin() { @@ -1169,11 +1131,12 @@ public Node deleteMin(Node x) { } ``` -### 2.8 delete() +### 8. delete() -如果待删除的节点只有一个子树,那么只需要让指向待删除节点的链接指向唯一的子树即可;否则,让右子树的最小节点替换该节点。 +- 如果待删除的节点只有一个子树,那么只需要让指向待删除节点的链接指向唯一的子树即可; +- 否则,让右子树的最小节点替换该节点。 -

+

```java public void delete(Key key) { @@ -1197,9 +1160,9 @@ private Node delete(Node x, Key key) { } ``` -### 2.9 keys() +### 9. keys() -利用二叉查找树中序遍历的结果为有序序列的特点。 +利用二叉查找树中序遍历的结果为递增的特点。 ```java public Iterable keys(Key lo, Key hi) { @@ -1217,35 +1180,43 @@ private void keys(Node x, Queue queue, Key lo, Key hi) { } ``` -### 2.10 性能分析 +### 10. 性能分析 复杂度:二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。 -## 3. 平衡查找树 +## 2-3 查找树 -### 3.1 2-3 查找树 +

-

+2-3 查找树引入了 2- 节点和 3- 节点,目的是为了让树更平衡。一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。 -一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。 +### 1. 插入操作 -#### 3.1.1 插入操作 +插入操作和 BST 的插入操作有很大区别,BST 的插入操作是先进行一次未命中的查找,然后再将节点插入到对应的空链接上。但是 2-3 查找树如果也这么做的话,那么就会破坏了平衡性。它是将新节点插入到叶子节点上。 -当插入之后产生一个临时 4- 节点时,需要将 4- 节点分裂成 3 个 2- 节点,并将中间的 2- 节点移到上层节点中。如果上移操作继续产生临时 4- 节点则一直进行分裂上移,直到不存在临时 4- 节点。 +根据叶子节点的类型不同,有不同的处理方式。 -

+插入到 2- 节点上,那么直接将新节点和原来的节点组成 3- 节点即可。 -#### 3.1.2 性质 +

+ +如果是插入到 3- 节点上,就会产生一个临时 4- 节点时,需要将 4- 节点分裂成 3 个 2- 节点,并将中间的 2- 节点移到上层节点中。如果上移操作继续产生临时 4- 节点则一直进行分裂上移,直到不存在临时 4- 节点。 + +

+ +### 2. 性质 2-3 查找树插入操作的变换都是局部的,除了相关的节点和链接之外不必修改或者检查树的其它部分,而这些局部变换不会影响树的全局有序性和平衡性。 -2-3 查找树的查找和插入操作复杂度和插入顺序 **无关** ,在最坏的情况下查找和插入操作访问的节点必然不超过 logN 个,含有 10 亿个节点的 2-3 查找树最多只需要访问 30 个节点就能进行任意的查找和插入操作。 +2-3 查找树的查找和插入操作复杂度和插入顺序无关,在最坏的情况下查找和插入操作访问的节点必然不超过 logN 个,含有 10 亿个节点的 2-3 查找树最多只需要访问 30 个节点就能进行任意的查找和插入操作。 -### 3.2 红黑二叉查找树 +

+ +## 红黑二叉查找树 2-3 查找树需要用到 2- 节点和 3- 节点,红黑树使用红链接来实现 3- 节点。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。 -

+

红黑树具有以下性质: @@ -1254,7 +1225,7 @@ private void keys(Node x, Queue queue, Key lo, Key hi) { 画红黑树时可以将红链接画平。 -

+

```java public class RedBlackBST, Value> { @@ -1284,13 +1255,13 @@ public class RedBlackBST, Value> { } ``` -#### 3.2.1 左旋转 +### 1. 左旋转 因为合法的红链接都为左链接,如果出现右链接为红链接,那么就需要进行左旋转操作。 -

+

-

+

```java public Node rotateLeft(Node h) { @@ -1305,13 +1276,13 @@ public Node rotateLeft(Node h) { } ``` -#### 3.2.2 右旋转 +### 2. 右旋转 进行右旋转是为了转换两个连续的左红链接,这会在之后的插入过程中探讨。 -

+

-

+

```java public Node rotateRight(Node h) { @@ -1325,13 +1296,13 @@ public Node rotateRight(Node h) { } ``` -#### 3.2.3 颜色转换 +### 3. 颜色转换 一个 4- 节点在红黑树中表现为一个节点的左右子节点都是红色的。分裂 4- 节点除了需要将子节点的颜色由红变黑之外,同时需要将父节点的颜色由黑变红,从 2-3 树的角度看就是将中间节点移到上层节点。 -

+

-

+

```java void flipColors(Node h){ @@ -1341,15 +1312,15 @@ void flipColors(Node h){ } ``` -#### 3.2.4 插入 +### 4. 插入 先将一个节点按二叉查找树的方法插入到正确位置,然后再进行如下颜色操作: - 如果右子节点是红色的而左子节点是黑色的,进行左旋转; -- 如果左子节点是红色的且它的左子节点也是红色的,进行右旋转; +- 如果左子节点是红色的,而且左子节点的左子节点也是红色的,进行右旋转; - 如果左右子节点均为红色的,进行颜色转换。 -

+

```java public void put(Key key, Value val) { @@ -1377,33 +1348,35 @@ private Node put(Node x, Key key, Value val) { 根节点一定为黑色,因为根节点没有上层节点,也就没有上层节点的左链接指向根节点。flipColors() 有可能会使得根节点的颜色变为红色,每当根节点由红色变成黑色时树的黑链接高度加 1. -#### 3.2.5 删除最小键 +### 5. 删除最小键 -如果最小键在一个 2- 节点中,那么删除该键会留下一个空链接,就破坏了平衡性,因此要确保最小键不在 2- 节点中。将 2- 节点转换成 3- 节点或者 4- 节点有两种方法,一种是向上层节点拿一个 key,一种是向兄弟节点拿一个 key。如果上层节点是 2- 节点,那么就没办法从上层节点拿 key 了,因此要保证删除路径上的所有节点都不是 2- 节点。在向下删除的过程中,保证以下情况之一发生: +如果最小键在一个 2- 节点中,那么删除该键会留下一个空链接,就破坏了平衡性,因此要确保最小键不在 2- 节点中。 + +将 2- 节点转换成 3- 节点或者 4- 节点有两种方法,一种是向上层节点拿一个 key,一种是向兄弟节点拿一个 key。如果上层节点是 2- 节点,那么就没办法从上层节点拿 key 了,因此要保证删除路径上的所有节点都不是 2- 节点。在向下删除的过程中,保证以下情况之一发生: 1. 如果当前节点的左子节点不是 2- 节点,完成; 2. 如果当前节点的左子节点是 2- 节点而它的兄弟节点不是 2- 节点,向兄弟节点拿一个 key 过来; 3. 如果当前节点的左子节点和它的兄弟节点都是 2- 节点,将左子节点、父节点中的最小键和最近的兄弟节点合并为一个 4- 节点。 -

+

最后得到一个含有最小键的 3- 节点或者 4- 节点,直接从中删除。然后再从头分解所有临时的 4- 节点。 -

+

-#### 3.2.6 分析 +### 6. 分析 -一颗大小为 N 的红黑树的高度不会超过 2lgN。最坏的情况下是它所对应的 2-3 树中构成最左边的路径节点全部都是 3- 节点而其余都是 2- 节点。 +一颗大小为 N 的红黑树的高度不会超过 2logN。最坏的情况下是它所对应的 2-3 树,构成最左边的路径节点全部都是 3- 节点而其余都是 2- 节点。 红黑树大多数的操作所需要的时间都是对数级别的。 -## 4. 散列表 +## 散列表 -散列表类似于数组,可以把散列表的散列值看成数组的索引值。访问散列表和访问数组元素一样快速,它可以在常数时间内实现查找和插入的符号表。 +散列表类似于数组,可以把散列表的散列值看成数组的索引值。访问散列表和访问数组元素一样快速,它可以在常数时间内实现查找和插入操作。 由于无法通过散列值知道键的大小关系,因此散列表无法实现有序性操作。 -### 4.1 散列函数 +### 1. 散列函数 对于一个大小为 M 的散列表,散列函数能够把任意键转换为 [0, M-1] 内的正整数,该正整数即为 hash 值。 @@ -1429,13 +1402,13 @@ for(int i = 0; i < s.length(); i++) hash = (R * hash + s.charAt(i)) % M; ``` -再比如,拥有多个成员的自定义类的哈希函数如下 +再比如,拥有多个成员的自定义类的哈希函数如下: ```java int hash = (((day * R + month) % M) * R + year) % M; ``` -R 的值不是很重要,通常取 31。 +R 通常取 31。 Java 中的 hashCode() 实现了 hash 函数,但是默认使用对象的内存地址值。在使用 hashCode() 函数时,应当结合除留余数法来使用。因为内存地址是 32 位整数,我们只需要 31 位的非负整数,因此应当屏蔽符号位之后再使用除留余数法。 @@ -1461,19 +1434,19 @@ public class Transaction{ } ``` -### 4.2 基于拉链法的散列表 +### 2. 基于拉链法的散列表 拉链法使用链表来存储 hash 值相同的键,从而解决冲突。此时查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。 -

+

对于 N 个键,M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。 -### 4.3 基于线性探测法的散列表 +### 3. 基于线性探测法的散列表 线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。使用线程探测法,数组的大小 M 应当大于键的个数 N(M>N)。 -

+

```java public class LinearProbingHashST { @@ -1502,7 +1475,7 @@ public class LinearProbingHashST { } ``` -#### 4.3.1 查找 +**(一)查找** ```java public Value get(Key key) { @@ -1515,7 +1488,7 @@ public Value get(Key key) { } ``` -#### 4.3.2 插入 +**(二)插入** ```java public void put(Key key, Value val) { @@ -1533,9 +1506,9 @@ public void put(Key key, Value val) { } ``` -#### 4.3.3 删除 +**(三)删除** -删除操作应当将右侧所有相邻的键值重新插入散列表中。 +删除操作应当将右侧所有相邻的键值对重新插入散列表中。 ```java public void delete(Key key) { @@ -1561,13 +1534,13 @@ public void delete(Key key) { } ``` -#### 4.3.4 调整数组大小 +**(四)调整数组大小** 线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。 α = N/M,把 α 称为利用率。理论证明,当 α 小于 1/2 时探测的预计次数只在 1.5 到 2.5 之间。 -

+

为了保证散列表的性能,应当调整数组的大小,使得 α 在 [1/4, 1/2] 之间。 @@ -1592,31 +1565,30 @@ private void resize(int cap) { 虽然每次重新调整数组都需要重新把每个键值对插入到散列表,但是从摊还分析的角度来看,所需要的代价却是很小的。从下图可以看出,每次数组长度加倍后,累计平均值都会增加 1,因为表中每个键都需要重新计算散列值,但是随后平均值会下降。 -

+

-## 5. 应用 +## 应用 -### 5.1 各种符号表实现的比较 +### 1. 各种符号表实现的比较 -

+

应当优先考虑散列表,当需要有序性操作时使用红黑树。 -### 5.2 Java 的符号表实现 +### 2. Java 的符号表实现 -Java 的 java.util.TreeMap 和 java.util.HashMap 分别是基于红黑树和拉链法的散列表的符号表实现。 +- java.util.TreeMap:红黑树 +- java.util.HashMap:拉链法的散列表 -### 5.3 集合类型 +### 3. 集合类型 除了符号表,集合类型也经常使用,它只有键没有值,可以用集合类型来存储一系列的键然后判断一个键是否在集合中。 -### 5.4 稀疏向量乘法 +### 4. 稀疏向量乘法 当向量为稀疏向量时,可以使用符号表来存储向量中的非 0 索引和值,使得乘法运算只需要对那些非 0 元素进行即可。 ```java -import java.util.HashMap; - public class SparseVector { private HashMap hashMap; @@ -1642,3 +1614,4 @@ public class SparseVector { } } ``` + diff --git a/notes/计算机操作系统.md b/notes/计算机操作系统.md index 6a85d3ab..5ac1d886 100644 --- a/notes/计算机操作系统.md +++ b/notes/计算机操作系统.md @@ -1,89 +1,37 @@ -* [第一章 概述](#第一章-概述) +* [一、 概述](#一-概述) * [操作系统基本特征](#操作系统基本特征) - * [1. 并发](#1-并发) - * [2. 共享](#2-共享) - * [3. 虚拟](#3-虚拟) - * [4. 异步](#4-异步) + * [操作系统基本功能](#操作系统基本功能) * [系统调用](#系统调用) - * [中断分类](#中断分类) - * [1. 外中断](#1-外中断) - * [2. 异常](#2-异常) - * [3. 陷入](#3-陷入) * [大内核和微内核](#大内核和微内核) - * [1. 大内核](#1-大内核) - * [2. 微内核](#2-微内核) -* [第二章 进程管理](#第二章-进程管理) + * [中断分类](#中断分类) +* [二、进程管理](#二进程管理) * [进程与线程](#进程与线程) - * [1. 进程](#1-进程) - * [2. 线程](#2-线程) - * [3. 区别](#3-区别) * [进程状态的切换](#进程状态的切换) * [调度算法](#调度算法) - * [1. 批处理系统中的调度](#1-批处理系统中的调度) - * [1.1 先来先服务](#11-先来先服务) - * [1.2 短作业优先](#12-短作业优先) - * [1.3 最短剩余时间优先](#13-最短剩余时间优先) - * [2. 交互式系统中的调度](#2-交互式系统中的调度) - * [2.1 优先级调度](#21-优先级调度) - * [2.2 时间片轮转](#22-时间片轮转) - * [2.3 多级反馈队列](#23-多级反馈队列) - * [3. 实时系统中的调度](#3-实时系统中的调度) * [进程同步](#进程同步) - * [1. 临界区](#1-临界区) - * [2. 同步与互斥](#2-同步与互斥) - * [3. 信号量](#3-信号量) - * [4. 管程](#4-管程) - * [进程通信](#进程通信) - * [1. 管道](#1-管道) - * [2. 信号量](#2-信号量) - * [3. 消息队列](#3-消息队列) - * [4. 信号](#4-信号) - * [5. 共享内存](#5-共享内存) - * [6. 套接字](#6-套接字) * [经典同步问题](#经典同步问题) - * [1. 读者-写者问题](#1-读者-写者问题) - * [2. 哲学家进餐问题](#2-哲学家进餐问题) -* [第三章 死锁](#第三章-死锁) + * [进程通信](#进程通信) +* [三、死锁](#三死锁) * [死锁的必要条件](#死锁的必要条件) * [死锁的处理方法](#死锁的处理方法) - * [1. 鸵鸟策略](#1-鸵鸟策略) - * [2. 死锁检测与死锁恢复](#2-死锁检测与死锁恢复) - * [2.1 每种类型一个资源的死锁检测](#21-每种类型一个资源的死锁检测) - * [2.2 每种类型多个资源的死锁检测](#22-每种类型多个资源的死锁检测) - * [2.3 死锁恢复](#23-死锁恢复) - * [3. 死锁预防](#3-死锁预防) - * [3.1 破坏互斥条件](#31-破坏互斥条件) - * [3.2 破坏占有和等待条件](#32-破坏占有和等待条件) - * [3.3 破坏不可抢占条件](#33-破坏不可抢占条件) - * [3.4 破坏环路等待](#34-破坏环路等待) - * [4. 死锁避免](#4-死锁避免) - * [4.1 安全状态](#41-安全状态) - * [4.2 单个资源的银行家算法](#42-单个资源的银行家算法) - * [4.3 多个资源的银行家算法](#43-多个资源的银行家算法) -* [第四章 存储器管理](#第四章-存储器管理) +* [四、内存管理](#四内存管理) * [虚拟内存](#虚拟内存) * [分页与分段](#分页与分段) - * [1. 分页](#1-分页) - * [2. 分段](#2-分段) - * [3. 段页式](#3-段页式) - * [4. 分页与分段区别](#4-分页与分段区别) + * [分页系统地址映射](#分页系统地址映射) * [页面置换算法](#页面置换算法) - * [1. 最佳(Optimal)](#1-最佳optimal) - * [2. 先进先出(FIFO)](#2-先进先出fifo) - * [3. 最近最久未使用(LRU, Least Recently Used)](#3-最近最久未使用lru,-least-recently-used) - * [4. 时钟(Clock)](#4-时钟clock) -* [第五章 设备管理](#第五章-设备管理) +* [五、设备管理](#五设备管理) * [磁盘调度算法](#磁盘调度算法) - * [1. 先来先服务(FCFS, First Come First Serverd)](#1-先来先服务fcfs,-first-come-first-serverd) - * [2. 最短寻道时间优先(SSTF, Shortest Seek Time First)](#2-最短寻道时间优先sstf,-shortest-seek-time-first) - * [3. 扫描算法(SCAN)](#3-扫描算法scan) - * [4. 循环扫描算法(CSCAN)](#4-循环扫描算法cscan) +* [六、链接](#六链接) + * [编译系统](#编译系统) + * [目标文件](#目标文件) + * [静态链接](#静态链接) + * [动态链接](#动态链接) * [参考资料](#参考资料) -# 第一章 概述 +# 一、 概述 ## 操作系统基本特征 @@ -111,13 +59,60 @@ ### 4. 异步 -异步是指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。 +异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。 + +## 操作系统基本功能 + +### 1. 进程管理 + +进程控制、进程同步、进程通信、死锁处理、处理机调度等。 + +### 2. 内存管理 + +内存分配、地址映射、内存保护与共享和内存扩充等功能。 + +### 3. 文件管理 + +文件存储空间的管理、目录管理及文件读写管理和保护等。 + +### 4. 设备管理 + +完成用户的 I/O 请求,方便用户使用各种设备,并提高设备的利用率,主要包括缓冲管理、设备分配、设备处理和虛拟设备等功能。 ## 系统调用 -如果一个进程在用户态需要用到操作系统的一些功能,就需要使用系统调用从而陷入内核,由操作系统代为完成。 +如果一个进程在用户态需要使用内核态的功能,就进行系统调用从而陷入内核,由操作系统代为完成。 -可以由系统调用请求的功能有设备管理、文件管理、进程管理、进程通信、存储器管理等。 +

+ +Linux 的系统调用主要有以下这些: + +| Task | Commands | +| :---: | --- | +| 进程控制 | fork(); exit(); wait(); | +| 进程通信 | pipe(); shmget(); mmap(); | +| 文件操作 | open(); read(); write(); | +| 设备操作 | ioctl(); read(); write(); | +| 信息维护 | getpid(); alarm(); sleep(); | +| 安全 | chmod(); umask(); chown(); | + +## 大内核和微内核 + +### 1. 大内核 + +大内核是将操作系统功能作为一个紧密结合的整体放到内核。 + +由于各模块共享信息,因此有很高的性能。 + +### 2. 微内核 + +由于操作系统不断复杂,为了实现因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。 + +在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态。 + +因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。 + +

## 中断分类 @@ -133,35 +128,27 @@ 在用户程序中使用系统调用。 -## 大内核和微内核 - -### 1. 大内核 - -大内核是将操作系统功能作为一个紧密结合的整体放到内核。 - -由于各模块共享信息,因此有很高的性能。 - -### 2. 微内核 - -由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。 - -因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。 - -# 第二章 进程管理 +# 二、进程管理 ## 进程与线程 ### 1. 进程 -进程是操作系统进行资源分配的基本单位。 +进程是资源分配的基本单位。 进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。 +下图显示了 4 个程序创建了 4 个进程,这 4 个进程可以并发地执行。 + +

+ ### 2. 线程 -一个进程中可以有多个线程,线程是独立调度的基本单位。 +线程是独立调度的基本单位。 -同一个进程中的多个线程之间可以并发执行,它们共享进程资源。 +一个进程中可以有多个线程,它们共享进程资源。 + +

### 3. 区别 @@ -177,11 +164,16 @@ ## 进程状态的切换 -

+

-阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU,缺少 CPU 会让进程从运行态转换为就绪态。 +- 就绪状态(ready):等待被调度 +- 运行状态(running) +- 阻塞状态(waiting):等待资源 -只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。 +应该注意以下内容: + +- 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。 +- 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。 ## 调度算法 @@ -191,7 +183,7 @@ #### 1.1 先来先服务 -first-come first-serverd(FCFS)。 +> first-come first-serverd(FCFS) 调度最先进入就绪队列的作业。 @@ -199,15 +191,15 @@ first-come first-serverd(FCFS)。 #### 1.2 短作业优先 -shortest job first(SJF)。 +> shortest job first(SJF) 调度估计运行时间最短的作业。 -长作业有可能会饿死,处于一直等待短作业执行完毕的状态。如果一直有短作业到来,那么长作业永远得不到调度。 +长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。 #### 1.3 最短剩余时间优先 -shortest remaining time next(SRTN)。 +> shortest remaining time next(SRTN) ### 2. 交互式系统中的调度 @@ -217,21 +209,21 @@ shortest remaining time next(SRTN)。 响应比 = (等待时间 + 要求服务时间) / 要求服务时间 = 响应时间 / 要求服务时间 -这种调度算法主要是为了解决 SJF 中长作业可能会饿死的问题,因为随着等待时间的增长,响应比也会越来越高。 +这种调度算法主要是为了解决短作业优先调度算法长作业可能会饿死的问题,因为随着等待时间的增长,响应比也会越来越高。 #### 2.2 时间片轮转 -将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 分配给队首的进程。 +将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。 -时间片轮转算法的效率和时间片的大小有很大关系。因为每次进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,进程切换太频繁,在进程切换上就会花过多时间。 +时间片轮转算法的效率和时间片的大小有很大关系。因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 #### 2.3 多级反馈队列

-如果一个进程需要执行 100 个时间片,如果采用轮转调度算法,那么需要交换 100 次。多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要 7 (包括最初的装入)的交换。 +如果一个进程需要执行 100 个时间片,如果采用轮转调度算法,那么需要交换 100 次。多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。 -每个队列的优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。 +每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。 ### 3. 实时系统中的调度 @@ -255,15 +247,15 @@ shortest remaining time next(SRTN)。 ### 2. 同步与互斥 -- 同步指多个进程按一定顺序执行; -- 互斥指多个进程在同一时刻只有一个进程能进入临界区。 +- 同步:多个进程按一定顺序执行; +- 互斥:多个进程在同一时刻只有一个进程能进入临界区。 ### 3. 信号量 -**信号量(Semaphore)** 是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。 +信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。 -- **down** : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,将进程睡眠,等待信号量大于 0; -- **up** :对信号量执行 +1 操作,并且唤醒睡眠的进程,让进程完成 down 操作。 +- **down** : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0; +- **up** :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。 down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。 @@ -285,13 +277,13 @@ void P2() { } ``` -**使用信号量实现生产者-消费者问题** + **使用信号量实现生产者-消费者问题**
-使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。 +问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。 -需要使用一个互斥量 mutex 来对缓冲区这个临界资源进行互斥访问。 +因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。 -为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进行中使用,当 full 信号量不为 0 时,消费者才可以取走物品。 +为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。 注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,也就无法执行 up(empty) 操作,empty 永远都为 0,那么生产者和消费者就会一直等待下去,造成死锁。 @@ -329,7 +321,7 @@ void consumer() { 使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。 -c 语言不支持管程,下面的示例代码使用了类 Pascal 语言来描述管程。示例代码中的管程提供了 insert() 和 remove() 方法,客户端代码通过调用这两个方法来解决生产者-消费者问题。 +c 语言不支持管程,下面的示例代码使用了类 Pascal 语言来描述管程。示例代码的管程提供了 insert() 和 remove() 方法,客户端代码通过调用这两个方法来解决生产者-消费者问题。 ```pascal monitor ProducerConsumer @@ -350,11 +342,12 @@ end monitor; 管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否者其它进程永远不能使用管程。 -管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来让另一个进程持有。signal() 操作用于唤醒被阻塞的进程。 +管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。 -**使用管程实现生成者-消费者问题** + **使用管程实现生成者-消费者问题**
```pascal +// 管程 monitor ProducerConsumer condition full, empty; integer count := 0; @@ -377,6 +370,7 @@ monitor ProducerConsumer end; end monitor; +// 生产者客户端 procedure producer begin while true do @@ -386,6 +380,7 @@ begin end end; +// 消费者客户端 procedure consumer begin while true do @@ -396,44 +391,6 @@ begin end; ``` -## 进程通信 - -进程通信可以看成是不同进程间的线程通信,对于同一个进程内线程的通信方式,主要使用信号量、条件变量等同步机制。 - -### 1. 管道 - -写进程在管道的尾端写入数据,读进程在管道的首端读出数据。管道提供了简单的流控制机制,进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。 - -Linux 中管道是通过空文件来实现。 - -管道有三种: - -1. 普通管道:有两个限制,一是只能单向传输;二是只能在父子进程之间使用; -2. 流管道:去除第一个限制,支持双向传输; -3. 命名管道:去除第二个限制,可以在不相关进程之间进行通信。 - -### 2. 信号量 - -信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止一个进程正在访问共享资源时,其它进程也访问该资源。主要作为进程间以及同一进程内不同线程之间的同步手段。 - -### 3. 消息队列 - -消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 - -### 4. 信号 - -信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 - -### 5. 共享内存 - -共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。 - -共享内存是最快的 IPC 方式,它是针对其它 IPC 运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。 - -### 6. 套接字 - -套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。 - ## 经典同步问题 生产者和消费者问题前面已经讨论过了。 @@ -496,7 +453,7 @@ void philosopher(int i) { } ``` -为了防止死锁的发生,可以两个条件: +为了防止死锁的发生,可以设置两个条件: 1. 必须同时拿起左右两个筷子; 2. 只有在两个邻居都没有进餐的情况下才允许进餐。 @@ -533,7 +490,7 @@ void take_two(int i) { void put_tow(i) { down(&mutex); state[i] = THINKING; - text(LEFT); + test(LEFT); test(RIGHT); up(&mutex); } @@ -546,10 +503,60 @@ void test(i) { // 尝试拿起两把筷子 } ``` +## 进程通信 +### 1. 进程同步与进程通信的区别 +- 进程同步:控制多个进程按一定顺序执行; +- 进程通信:进程间传输信息。 -# 第三章 死锁 +进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。 + +在进程同步中介绍的信号量也属于进程通信的一种方式,但是属于低级别的进程通信,因为它传输的信息非常小。 + +### 2. 进程通信方式 + +#### 2.1 消息传递 + +操作系统提供了用于通信的通道(Channel),进程可以通过读写这个通道进行通信。 + +

+ + **(一)管道**
+ +写进程在管道的尾端写入数据,读进程在管道的首端读出数据。管道提供了简单的流控制机制,进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。 + +Linux 中管道通过空文件实现。 + +管道有三种: + +1. 普通管道:有两个限制,一是只能单向传输;二是只能在父子进程之间使用; +2. 流管道:去除第一个限制,支持双向传输; +3. 命名管道:去除第二个限制,可以在不相关进程之间进行通信。 + +

+ + **(二)消息队列**
+ +消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 + +

+ + **(三)套接字**
+ +套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。 + +

+ +#### 2.2 共享内存 + +操作系统建立一块共享内存,并将其映射到每个进程的地址空间上,进程就可以直接对这块共享内存进行读写。 + +共享内存是最快的进程通信方式。 + +

+ +# 三、死锁 ## 死锁的必要条件 @@ -574,7 +581,7 @@ void test(i) { // 尝试拿起两把筷子 不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。 -#### 2.1 每种类型一个资源的死锁检测 +**(一)每种类型一个资源的死锁检测**

@@ -584,7 +591,7 @@ void test(i) { // 尝试拿起两把筷子 每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。 -#### 2.2 每种类型多个资源的死锁检测 +**(二)每种类型多个资源的死锁检测**

@@ -595,7 +602,7 @@ void test(i) { // 尝试拿起两把筷子 - C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量 - R 矩阵:每个进程请求的资源数量 -进程 P1 和 P2 所请求的资源都得不到满足,只有进程 P3 可以,让 P3 执行,之后释放 P3 拥有的资源,此时 A = (2 2 2 0)。P1 可以执行,执行后释放 P1 拥有的资源,A = (4 2 2 2) ,P2 也可以执行。所有进程都可以顺利执行,没有死锁。 +进程 P1 和 P2 所请求的资源都得不到满足,只有进程 P3 可以,让 P3 执行,之后释放 P3 拥有的资源,此时 A = (2 2 2 0)。P2 可以执行,执行后释放 P2 拥有的资源,A = (4 2 2 1) 。P1 也可以执行。所有进程都可以顺利执行,没有死锁。 算法总结如下: @@ -605,7 +612,7 @@ void test(i) { // 尝试拿起两把筷子 2. 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。 3. 如果没有这样一个进程,算法终止。 -#### 2.3 死锁恢复 +**(三)死锁恢复** - 利用抢占恢复 - 利用回滚恢复 @@ -615,17 +622,17 @@ void test(i) { // 尝试拿起两把筷子 在程序运行之前预防发生死锁。 -#### 3.1 破坏互斥条件 +**(一)破坏互斥条件** 例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。 -#### 3.2 破坏占有和等待条件 +**(二)破坏占有和等待条件** 一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。 -#### 3.3 破坏不可抢占条件 +**(三)破坏不可抢占条件** -#### 3.4 破坏环路等待 +**(四)破坏环路等待** 给资源统一编号,进程只能按编号顺序来请求资源。 @@ -633,7 +640,7 @@ void test(i) { // 尝试拿起两把筷子 在程序运行时避免发生死锁。 -#### 4.1 安全状态 +**(一)安全状态**

@@ -643,7 +650,7 @@ void test(i) { // 尝试拿起两把筷子 安全状态的检测与死锁的检测类似,因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类似,可以结合着做参考对比。 -#### 4.2 单个资源的银行家算法 +**(二)单个资源的银行家算法** 一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。 @@ -651,7 +658,7 @@ void test(i) { // 尝试拿起两把筷子 上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。 -#### 4.3 多个资源的银行家算法 +**(三)多个资源的银行家算法**

@@ -663,7 +670,9 @@ void test(i) { // 尝试拿起两把筷子 - 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。 - 重复以上两步,直到所有进程都标记为终止,则状态时安全的。 -# 第四章 存储器管理 +如果一个状态不是安全的,也需要拒绝进入这个状态。 + +# 四、内存管理 ## 虚拟内存 @@ -711,59 +720,90 @@ void test(i) { // 尝试拿起两把筷子 - 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。 +## 分页系统地址映射 + +- 内存管理单元(MMU):管理着虚拟地址空间和物理内存的转换。 +- 页表(Page table):页(虚拟地址空间)和页框(物理内存空间)的映射表。例如下图中,页表的第 0 个表项为 010,表示第 0 个页映射到第 2 个页框。页表项的最后一位用来标记页是否在内存中。 + +下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位。因此对于虚拟地址(0010 000000000100),前 4 位是用来存储页面号,而后 12 位存储在页中的偏移量。 + +(0010 000000000100)根据前 4 位得到页号为 2,读取表项内容为(110 1),它的前 3 为为页框号,最后 1 位表示该页在内存中。最后映射得到物理内存地址为(110 000000000100)。 + +

+ ## 页面置换算法 -在程序运行过程中,若其所要访问的页面不在内存而需要把它们调入内存,但是内存已无空闲空间时,系统必须从内存中调出一个页面到磁盘对换区中,并且将程序所需要的页面调入内存中。页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。 +在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。 -### 1. 最佳(Optimal) +页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。 + +### 1. 最佳 + +> Optimal 所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。 -是一种理论上的算法,因为无法知道一个页面多长时间会被再访问到。 +是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。 举例:一个系统为某进程分配了三个物理块,并有如下页面引用序列: -


+

-进程运行时,先将 7,0,1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。 +开始运行时,先将 7, 0, 1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。 -### 2. 先进先出(FIFO) +### 2. 先进先出 + +> FIFO, First In First Out 所选择换出的页面是最先进入的页面。 该算法会将那些经常被访问的页面也被换出,从而使缺页率升高。 -### 3. 最近最久未使用(LRU, Least Recently Used) +### 3. 最近最久未使用 + +> LRU, Least Recently Used 虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。LRU 将最近最久未使用的页面换出。 -可以用栈来实现该算法,栈中存储页面的页面号。当进程访问一个页面时,将该页面的页面号从栈移除,并将它压入栈顶。这样,最近被访问的页面的页面号总是在栈顶,而最近最久未使用的页面的页面号总是在栈底。 +可以用栈来实现该算法,栈中存储页面的页面号。当进程访问一个页面时,将该页面的页面号从栈移除,并将它压入栈顶。这样,最近被访问的页面总是在栈顶,而最近最久未使用的页面总是在栈底。 -


+


-### 4. 时钟(Clock) +### 4. 时钟 -Clock 页面置换算法需要用到一个访问位,当一个页面被访问时,将访问位置为 1。 +> Clock + +需要用到一个访问位,当一个页面被访问时,将访问位置为 1。 首先,将内存中的所有页面链接成一个循环队列,当缺页中断发生时,检查当前指针所指向页面的访问位,如果访问位为 0,就将该页面换出;否则将该页的访问位设置为 0,给该页面第二次的机会,移动指针继续检查。 -# 第五章 设备管理 +

+ +# 五、设备管理 ## 磁盘调度算法 -当多个进程同时请求访问磁盘时,需要进行磁盘调度来控制对磁盘的访问。磁盘调度的主要目标是使磁盘的平均寻道时间最少。 +当多个进程同时请求访问磁盘时,需要进行磁盘调度来控制对磁盘的访问。 -### 1. 先来先服务(FCFS, First Come First Serverd) +磁盘调度的主要目标是使磁盘的平均寻道时间最少。 + +### 1. 先来先服务 + +> FCFS, First Come First Served 根据进程请求访问磁盘的先后次序来进行调度。优点是公平和简单,缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。 -### 2. 最短寻道时间优先(SSTF, Shortest Seek Time First) +### 2. 最短寻道时间优先 + +> SSTF, Shortest Seek Time First 要求访问的磁道与当前磁头所在磁道距离最近的优先进行调度。这种算法并不能保证平均寻道时间最短,但是比 FCFS 好很多。 -### 3. 扫描算法(SCAN) +### 3. 扫描算法 + +> SCAN SSTF 会出现饥饿现象。考虑以下情况,新进程请求访问的磁道与磁头所在磁道的距离总是比一个在等待的进程来的近,那么等待的进程会一直等待下去。 @@ -771,13 +811,79 @@ SCAN 算法在 SSTF 算法之上考虑了磁头的移动方向,要求所请求 当一个磁头自里向外移动时,移到最外侧会改变移动方向为自外向里,这种移动的规律类似于电梯的运行,因此又常称 SCAN 算法为电梯调度算法。 -### 4. 循环扫描算法(CSCAN) +### 4. 循环扫描算法 + +> CSCAN CSCAN 对 SCAN 进行了改动,要求磁头始终沿着一个方向移动。 +# 六、链接 + +## 编译系统 + +以下是一个 hello.c 程序: + +```c +#include + +int main() +{ + printf("hello, world\n"); + return 0; +} +``` + +在 Unix 系统上,由编译器把源文件转换为目标文件。 + +```bash +gcc -o hello hello.c +``` + +这个过程大致如下: + +

+ +1. 预处理阶段:处理以 # 开头的预处理命令; +2. 编译阶段:翻译成汇编程序; +3. 汇编阶段:将汇编程序翻译可重定向目标程序,它是二进制的; +4. 链接阶段:将可重定向目标程序和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标程序。 + +## 目标文件 + +1. 可执行目标文件:可以直接在内存中执行; +2. 可重定向目标文件:可与其他可重定向目标文件在链接阶段合并,创建一个可执行目标文件; +3. 共享目标文件:可以在运行时被动态加载进内存并链接; + +## 静态链接 + +静态连接器以一组可重定向目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务: + +1. 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用于一个符号定义关联起来。 +2. 重定位:编译器和汇编器生成从地址 0 开始的代码和数据节,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。 + +

+ +## 动态链接 + +静态库有以下两个问题: + +- 当静态库更新时那么整个程序都要重新进行链接; +- 对于 printf 这种标准函数库,如果每个程序都要有代码,这会极大浪费资源。 + +共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示,Windows 系统上它们被称为 DLL。它具有以下特点: + +1. 在给定的文件系统中一个库只有一个 .so 文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中; +2. 在内存中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享。 + +

+ # 参考资料 - Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Press, 2014. - 汤子瀛, 哲凤屏, 汤小丹. 计算机操作系统[M]. 西安电子科技大学出版社, 2001. - Bryant, R. E., & O’Hallaron, D. R. (2004). 深入理解计算机系统. +- [Operating System Notes](https://applied-programming.github.io/Operating-Systems-Notes/) - [进程间的几种通信方式](http://blog.csdn.net/yufaw/article/details/7409596) +- [Operating-System Structures](https://www.cs.uic.edu/\~jbell/CourseNotes/OperatingSystems/2_Structures.html) +- [Processes](http://cse.csusb.edu/tongyu/courses/cs460/notes/process.php) +- [Inter Process Communication Presentation[1]](https://www.slideshare.net/rkolahalam/inter-process-communication-presentation1) diff --git a/notes/计算机网络.md b/notes/计算机网络.md index a9ebd753..d99e12b8 100644 --- a/notes/计算机网络.md +++ b/notes/计算机网络.md @@ -1,65 +1,38 @@ -* [第一章 概述](#第一章-概述) +* [一、概述](#一概述) * [网络的网络](#网络的网络) * [ISP](#isp) * [互联网的组成](#互联网的组成) * [主机之间的通信方式](#主机之间的通信方式) * [电路交换与分组交换](#电路交换与分组交换) - * [1. 电路交换](#1-电路交换) - * [2. 报文交换](#2-报文交换) - * [3. 分组交换](#3-分组交换) * [时延](#时延) - * [1. 发送时延](#1-发送时延) - * [2. 传播时延](#2-传播时延) - * [3. 处理时延](#3-处理时延) - * [4. 排队时延](#4-排队时延) - * [计算机网络体系结构*](#计算机网络体系结构) - * [1. 七层协议](#1-七层协议) - * [2. 五层协议](#2-五层协议) - * [3. 数据在各层之间的传递过程](#3-数据在各层之间的传递过程) - * [4. TCP/IP 体系结构](#4-tcpip-体系结构) -* [第二章 物理层](#第二章-物理层) + * [计算机网络体系结构 *](#计算机网络体系结构-) +* [二、物理层](#二物理层) * [通信方式](#通信方式) * [带通调制](#带通调制) * [信道复用技术](#信道复用技术) - * [1. 频分复用、时分复用](#1-频分复用时分复用) - * [2. 统计时分复用](#2-统计时分复用) - * [3. 波分复用](#3-波分复用) - * [4. 码分复用](#4-码分复用) -* [第三章 数据链路层](#第三章-数据链路层) +* [三、数据链路层](#三数据链路层) * [三个基本问题](#三个基本问题) - * [1. 封装成帧](#1-封装成帧) - * [2. 透明传输](#2-透明传输) - * [3. 差错检测](#3-差错检测) * [点对点信道 - PPP 协议](#点对点信道---ppp-协议) * [局域网的拓扑](#局域网的拓扑) - * [广播信道 - CSMA/CD 协议*](#广播信道---csmacd-协议) - * [扩展局域网*](#扩展局域网) - * [1. 在物理层进行扩展](#1-在物理层进行扩展) - * [2. 在链路层进行扩展](#2-在链路层进行扩展) - * [3. 虚拟局域网](#3-虚拟局域网) - * [MAC 层*](#mac-层) -* [第四章 网络层*](#第四章-网络层) + * [广播信道- CSMA/CD 协议 *](#广播信道--csmacd-协议-) + * [扩展局域网 *](#扩展局域网-) + * [MAC 层 *](#mac-层-) +* [四、网络层 *](#四网络层-) * [网际协议 IP 概述](#网际协议-ip-概述) * [IP 数据报格式](#ip-数据报格式) * [IP 地址编址](#ip-地址编址) - * [1. 分类](#1-分类) - * [2. 子网划分](#2-子网划分) - * [3. 无分类](#3-无分类) * [IP 地址和 MAC 地址](#ip-地址和-mac-地址) * [地址解析协议 ARP](#地址解析协议-arp) * [路由器的结构](#路由器的结构) * [路由器分组转发流程](#路由器分组转发流程) * [路由选择协议](#路由选择协议) - * [1. 内部网关协议 RIP](#1-内部网关协议-rip) - * [2. 内部网关协议 OSPF](#2-内部网关协议-ospf) - * [3. 外部网关协议 BGP](#3-外部网关协议-bgp) * [网际控制报文协议 ICMP](#网际控制报文协议-icmp) * [分组网间探测 PING](#分组网间探测-ping) * [IP 多播](#ip-多播) * [虚拟专用网 VPN](#虚拟专用网-vpn) * [网络地址转换 NAT](#网络地址转换-nat) -* [第五章 运输层*](#第五章-运输层) +* [五、运输层 *](#五运输层-) * [UDP 和 TCP 的特点](#udp-和-tcp-的特点) * [UDP 首部格式](#udp-首部格式) * [TCP 首部格式](#tcp-首部格式) @@ -69,32 +42,21 @@ * [TCP 可靠传输](#tcp-可靠传输) * [TCP 流量控制](#tcp-流量控制) * [TCP 拥塞控制](#tcp-拥塞控制) - * [慢开始与拥塞避免](#慢开始与拥塞避免) - * [快重传与快恢复](#快重传与快恢复) -* [第六章 应用层*](#第六章-应用层) +* [六、应用层 *](#六应用层-) * [域名系统 DNS](#域名系统-dns) - * [1. 层次结构](#1-层次结构) - * [2. 解析过程](#2-解析过程) * [文件传输协议 FTP](#文件传输协议-ftp) * [远程终端协议 TELNET](#远程终端协议-telnet) * [万维网 WWW](#万维网-www) * [电子邮件协议](#电子邮件协议) - * [POP3](#pop3) - * [IMAP](#imap) - * [SMTP](#smtp) * [动态主机配置协议 DHCP](#动态主机配置协议-dhcp) * [点对点传输 P2P](#点对点传输-p2p) * [Web 页面请求过程](#web-页面请求过程) - * [1. DHCP 配置主机信息](#1-dhcp-配置主机信息) - * [2. ARP 解析 MAC 地址](#2-arp-解析-mac-地址) - * [3. DNS 解析域名](#3-dns-解析域名) - * [4. HTTP 请求页面](#4-http-请求页面) * [常用端口](#常用端口) * [参考资料](#参考资料) -# 第一章 概述 +# 一、概述 ## 网络的网络 @@ -115,19 +77,16 @@ ## 互联网的组成 1. 边缘部分:所有连接在互联网上的主机,用户可以直接使用; + 2. 核心部分:由大量的网络和连接这些网络的路由器组成,为边缘部分的主机提供服务。

## 主机之间的通信方式 -**1. 客户-服务器(C/S)** +1. 客户-服务器(C/S):客户是服务的请求方,服务器是服务的提供方。 -客户是服务的请求方,服务器是服务的提供方。 - -**2. 对等(P2P)** - -不区分客户和服务器。 +2. 对等(P2P):不区分客户和服务器。 ## 电路交换与分组交换 @@ -161,7 +120,7 @@ 主机或路由器发送数据帧所需要的时间。 -


+

其中 l 表示数据帧的长度,v 表示发送速率。 @@ -169,7 +128,7 @@ 电磁波在信道中传播一定的距离需要花费的时间,电磁波传播速度接近光速。 -


+

其中 l 表示信道长度,v 表示电磁波在信道上的传播速率。 @@ -181,7 +140,7 @@ 分组在路由器的输入队列和输出队列中排队等待的时间,取决于网络当前的通信量。 -## 计算机网络体系结构* +## 计算机网络体系结构 *

@@ -198,11 +157,11 @@ 2. 运输层:提供的是进程间的通用数据传输服务。由于应用层协议很多,定义通用的运输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。TCP 主要提供完整性服务,UDP 主要提供及时性服务。 -3. 网络层:为主机之间提供服务,而不是像运输层协议那样是为主机中的进程提供服务。网络层把运输层传递下来的报文段或者用户数据报封装成分组来进行传输。 +3. 网络层:为主机之间提供数据传输服务,而像运输层协议那样是为主机中的进程提供服务。网络层把运输层传递下来的报文段或者用户数据报封装成分组。 -4. 数据链路层:网络层针对的还是主机之间,而主机之间可以有很多链路,链路层协议就是为相邻结点之间提供服务。数据链路层把网络层传来的分组封装成帧。 +4. 数据链路层:网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为相邻结点之间提供服务。数据链路层把网络层传来的分组封装成帧。 -5. 物理层:考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使物理层上的数据链路层感觉不到这些差异。 +5. 物理层:考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。 ### 3. 数据在各层之间的传递过程 @@ -224,7 +183,7 @@ TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中

-# 第二章 物理层 +# 二、物理层 ## 通信方式 @@ -246,43 +205,43 @@ TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中 使用这两种方式进行通信,在通信的过程中用户会一直占用一部分信道资源。但是由于计算机数据的突发性质,没必要一直占用信道资源而不让出给其它用户使用,因此这两种方式对信道的利用率都不高。 -

+

### 2. 统计时分复用 是对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成统计时分复用帧然后发送。 -

+

### 3. 波分复用 光的频分复用。由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波。 -

+

### 4. 码分复用 -
为每个用户分配 m bit 的码片,并且所有的码片正交,对于任意两个码片

+为每个用户分配 m bit 的码片,并且所有的码片正交,对于任意两个码片 有 -


+

-
为了方便,取 m=8,设码片 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。

+为了方便,取 m=8,设码片 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。 在计算时将 00011011 记作 (-1 -1 -1 +1 +1 -1 +1 +1),可以得到 -


+

-


+

-
其中 的反码。

+其中 的反码。 -
利用上面的式子我们知道,当接收端使用码片 对接收到的数据进行内积运算时,结果为 0 的是其它用户发送的数据,结果为 1 的是用户发送的比特 1,结果为 -1 的是用户发送的比特 0。

+利用上面的式子我们知道,当接收端使用码片 对接收到的数据进行内积运算时,结果为 0 的是其它用户发送的数据,结果为 1 的是用户发送的比特 1,结果为 -1 的是用户发送的比特 0。 码分复用需要发送的数据量为原先的 m 倍。

-# 第三章 数据链路层 +# 三、数据链路层 ## 三个基本问题 @@ -296,7 +255,7 @@ TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中 透明表示一个实际存在的事物看起来好像不存在一样。 -帧中有首部和尾部,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据中出现首部尾部相同的内容前面插入转义字符,如果需要传输的内容正好就是转义字符,那么就在转义字符前面再加个转义字符,在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。 +帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符,如果出现转移字符,那么就在转义字符前面再加个转义字符,在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。

@@ -323,15 +282,17 @@ TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中

-## 广播信道 - CSMA/CD 协议* +## 广播信道- CSMA/CD 协议 * 在广播信道上,同一时间只能允许一台计算机发送数据。 CSMA/CD 表示载波监听多点接入 / 碰撞检测。 - **多点接入** :说明这是总线型网络,许多计算机以多点的方式连接到总线上。 + - **载波监听** :每个站都必须不停地监听信道。在发送前,如果检听信道正在使用,就必须等待。 -- **碰撞检测** :在发送中,如果监听 到信道已有其它站正在发送数据,就表示发生了碰撞。虽然每一个站在发送数据之前都已经检听到信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。 + +- **碰撞检测** :在发送中,如果监听 到信道已有其它站正在发送数据,就表示发生了碰撞。虽然每一个站在发送数据之前都已经监听到信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。

@@ -339,7 +300,7 @@ CSMA/CD 表示载波监听多点接入 / 碰撞检测。 当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 **截断二进制指数退避算法** 来确定,从离散的整数集合 {0, 1, .., (2k-1)} 中随机取出一个数,记作 r,然后取 r 倍的争用期作为重传等待时间。 -## 扩展局域网* +## 扩展局域网 * ### 1. 在物理层进行扩展 @@ -355,13 +316,13 @@ CSMA/CD 表示载波监听多点接入 / 碰撞检测。 ### 2. 在链路层进行扩展 -最开始使用的是网桥,它收到一个帧时,根据帧的 MAC 地址,查找网桥中的地址表,然后确定将帧转发到哪一个接口。 +最开始使用的是网桥,它收到一个帧时,根据帧的 MAC 地址,查找网桥中的地址表,确定将帧转发的接口。 网桥不是共享式设备,因此性能比集线器这种共享式设备更高。 交换机的问世很快就淘汰了网桥,它实质上是一个多接口网桥,而网桥是两接口。交换机的每个接口都能直接与一个主机或者另一个交换机相连,并且一般都工作在全双工方式。 -交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到 接口的映射。下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B,先查交换表,此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧,主机 B 收下之后,查找交换表得到主机 A 映射的接口为 1,因此就把帧发送给主机 A,同时交换机添加主机 B 到接口 3 的映射。 +交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B,先查交换表,此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧,主机 B 收下之后,查找交换表得到主机 A 映射的接口为 1,因此就把帧发送给主机 A,同时交换机添加主机 B 到接口 3 的映射。

@@ -371,18 +332,21 @@ CSMA/CD 表示载波监听多点接入 / 碰撞检测。

-## MAC 层* +## MAC 层 * -MAC 地址是 6 字节(48 位)的地址,用于唯一表示网络适配器(网卡),一台主机拥有多少个适配器就有多少个 MAC 地址,例如笔记本电脑普遍存在无线网络适配器和有线网络适配器。 +MAC 地址是 6 字节(48 位)的地址,用于唯一标识网络适配器(网卡),一台主机拥有多少个适配器就有多少个 MAC 地址,例如笔记本电脑普遍存在无线网络适配器和有线网络适配器。

- **类型** :标记上层使用的协议; + - **数据** :长度在 46-1500 之间,如果太小则需要填充; + - **FCS** :帧检验序列,使用的是 CRC 检验方法; + - **前同步码** :只是为了计算 FCS 临时加入的,计算结束之后会丢弃。 -# 第四章 网络层* +# 四、网络层 * ## 网际协议 IP 概述 @@ -495,11 +459,16 @@ CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为 ## 路由器分组转发流程 -1. 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。(路由表项是网络号而不是 IP 地址,这样做大大减少了路由表条目数量) +1. 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。(路由表项是网络号而不是 IP 地址,这样做大大减少了路由表条目数量); + 2. 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付; + 3. 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器; + 4. 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器; + 5. 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器; + 6. 报告转发分组出错。

@@ -606,7 +575,7 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。

-# 第五章 运输层* +# 五、运输层 * 网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。 @@ -622,7 +591,7 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。

-首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和而临时添加的。 +首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。 ## TCP 首部格式 @@ -649,8 +618,11 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。 假设 A 为客户端,B 为服务器端。 1. 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。 + 2. A 向 B 发送连接请求报文段,SYN=1,ACK=0,选择一个初始的序号 x。 + 3. B 收到连接请求报文段,如果同意建立连接,则向 A 发送连接确认报文段,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。 + 4. A 收到 B 的连接确认报文段后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。 5. B 收到 A 的确认后,连接建立。 @@ -662,15 +634,19 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。 以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。 1. A 发送连接释放报文段,FIN=1; + 2. B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据; + 3. 当 B 要不再需要连接时,发送连接释放请求报文段,FIN=1; + 4. A 收到后发出确认,此时连接释放。 -**TIME_WAIT** +### TIME_WAIT 客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间。这么做有两个理由: 1. 确保最后一个确认报文段能够到达。如果 B 没收到 A 发送来的确认报文段,那么就会重新发送连接释放请求报文段,A 等待一段时间就是为了处理这种情况的发生。 + 2. 可能存在“已失效的连接请求报文段”,为了防止这种报文段出现在本次连接之外,需要等待一段时间。 ## TCP 滑动窗口 @@ -689,13 +665,13 @@ TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文 一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下: -


+

-超时时间 RTO 应该略大于 RRTs,TCP 使用的超时时间计算如下: +超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下: -


+

-其中 RTTd 为偏差,它与新的 RRT 和 RRTs 有关。 +其中 RTTd 为偏差。 ## TCP 流量控制 @@ -718,7 +694,7 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、

-### 慢开始与拥塞避免 +### 1. 慢开始与拥塞避免 发送的最初执行慢开始,令 cwnd=1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段为:2、4、8 ... @@ -726,7 +702,7 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、 如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。 -### 快重传与快恢复 +### 2. 快重传与快恢复 在接收方,要求每次接收到报文段都应该发送对已收到有序报文段的确认,例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。 @@ -736,7 +712,7 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、

-# 第六章 应用层* +# 六、应用层 * ## 域名系统 DNS @@ -797,15 +773,15 @@ TELNET 可以适应许多计算机和操作系统的差异,例如不同操作

-### POP3 +### 1. POP3 POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。 -### IMAP +### 2. IMAP IMAP 协议中客户端和服务器上的邮件保持同步,如果不去手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。IMAP 协议也支持创建自定义的文件夹。 -### SMTP +### 3. SMTP SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主题的结构,定义了非 ASCII 码的编码规则。 diff --git a/notes/设计模式.md b/notes/设计模式.md index 4da4c967..13815198 100644 --- a/notes/设计模式.md +++ b/notes/设计模式.md @@ -1,79 +1,79 @@ -* [前言](#前言) -* [设计模式入门](#设计模式入门) -* [观察者模式](#观察者模式) -* [装饰模式](#装饰模式) -* [工厂模式](#工厂模式) - * [1. 简单工厂](#1-简单工厂) - * [2. 工厂方法模式](#2--工厂方法模式) - * [3. 抽象工厂模式](#3--抽象工厂模式) -* [单件模式](#单件模式) -* [命令模式](#命令模式) -* [适配器模式](#适配器模式) -* [外观模式](#外观模式) -* [模板方法模式](#模板方法模式) -* [迭代器模式](#迭代器模式) -* [组合模式](#组合模式) -* [状态模式](#状态模式) -* [代理模式](#代理模式) -* [复合模式](#复合模式) - * [MVC](#mvc) -* [与设计模式相处](#与设计模式相处) -* [剩下的模式](#剩下的模式) +* [一、前言](#一前言) +* [二、设计模式概念](#二设计模式概念) +* [三、策略模式](#三策略模式) +* [三、观察者模式](#三观察者模式) +* [四、装饰模式](#四装饰模式) +* [五、简单工厂](#五简单工厂) +* [六、工厂方法模式](#六工厂方法模式) +* [七、抽象工厂模式](#七抽象工厂模式) +* [八、单例模式](#八单例模式) +* [九、命令模式](#九命令模式) +* [十、适配器模式](#十适配器模式) +* [十、外观模式](#十外观模式) +* [十一、模板方法模式](#十一模板方法模式) +* [十二、迭代器模式](#十二迭代器模式) +* [十三、组合模式](#十三组合模式) +* [十四、状态模式](#十四状态模式) +* [十五、代理模式](#十五代理模式) +* [十六、MVC](#十六mvc) +* [十七、与设计模式相处](#十七与设计模式相处) -# 前言 +# 一、前言 -文中涉及一些 UML 类图,为了更好地理解,可以先阅读 [UML 类图](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#1-%E7%B1%BB%E5%9B%BE)。 +文中涉及一些 UML 类图,为了更好地理解,可以先阅读 [UML 类图](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#%E7%AC%AC%E4%B8%89%E7%AB%A0-uml)。 需要说明的一点是,文中的 UML 类图和规范的 UML 类图不大相同,其中组合关系使用以下箭头表示:

-# 设计模式入门 - -**1. 设计模式概念** +# 二、设计模式概念 设计模式不是代码,而是解决问题的方案,学习现有的设计模式可以做到经验复用。 拥有设计模式词汇,在沟通时就能用更少的词汇来讨论,并且不需要了解底层细节。 -**2. 问题描述** +# 三、策略模式 + +## 模式定义 + +定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。 + +## 问题描述 设计不同种类的鸭子拥有不同的叫声和飞行方式。 -**3. 简单实现方案** +## 简单实现方案 使用继承的解决方案如下,这种方案代码无法复用,如果两个鸭子类拥有同样的飞行方式,就有两份重复的代码。 -

+

-**4. 设计原则** +## 设计原则 -**封装变化** 在这里变化的是鸭子叫和飞行的行为方式。 +**封装变化** :在这里变化的是鸭子叫和飞行的行为方式。 -**针对接口编程,而不是针对实现编程** 变量声明的类型为父类,而不是具体的某个子类。父类中的方法实现不在父类,而是在各个子类。程序在运行时可以动态改变变量所指向的子类类型。 +**针对接口编程,而不是针对实现编程** :变量声明的类型为父类,而不是具体的某个子类。父类中的方法实现不在父类,而是在各个子类。程序在运行时可以动态改变变量所指向的子类类型。 运用这一原则,将叫和飞行的行为抽象出来,实现多种不同的叫和飞行的子类,让子类去实现具体的叫和飞行方式。

-**多用组合,少用继承** 组合也就是 has-a 关系,通过组合,可以在运行时动态改变实现,只要通过改变父类对象具体指向哪个子类即可。而继承就不能做到这些,继承体系在创建类时就已经确定。 +**多用组合,少用继承** :组合也就是 HAS-A 关系,通过组合,可以在运行时动态改变实现,只要通过改变父类对象具体指向哪个子类即可。而继承就不能做到这些,继承体系在创建类时就已经确定。 -运用这一原则,在 Duck 类中组合 FlyBehavior 和 QuackBehavior 类,performQuack() 和 performFly() 方法委托给这两个类去处理。通过这种方式,一个 Duck 子类可以根据需要去实例化 FlyBehavior 和 QuackBehavior 的子类对象,并且也可以动态地进行改变。 +运用这一原则,在 Duck 类中组合 FlyBehavior 和 QuackBehavior 类,performQuack() 和 performFly() 方法委托给这两个类去处理。通过这种方式,一个 Duck 子类可以根据需要去初始化 FlyBehavior 和 QuackBehavior 的子类对象,并且也可以动态地进行改变。

-**5. 整体设计图** +## 问题的解决方案类图 + +(可放大网页查看)

-**6. 模式定义** - -**策略模式** :定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。 - -**7. 实现代码** +## 代码实现 ```java public abstract class Duck { @@ -101,8 +101,8 @@ public abstract class Duck { } ``` ```java -public class MallarDuck extends Duck{ - public MallarDuck(){ +public class MallardDuck extends Duck{ + public MallardDuck(){ flyBehavior = new FlyWithWings(); quackBehavior = new Quack(); } @@ -161,11 +161,11 @@ public class Squeak implements QuackBehavior{ ```java public class MiniDuckSimulator { public static void main(String[] args) { - Duck mallarDuck = new MallarDuck(); - mallarDuck.performQuack(); - mallarDuck.performFly(); - mallarDuck.setFlyBehavior(new FlyNoWay()); - mallarDuck.performFly(); + Duck mallardDuck = new MallardDuck(); + mallardDuck.performQuack(); + mallardDuck.performFly(); + mallardDuck.setFlyBehavior(new FlyNoWay()); + mallardDuck.performFly(); } } ``` @@ -176,35 +176,35 @@ FlyBehavior.FlyWithWings FlyBehavior.FlyNoWay ``` -# 观察者模式 +# 三、观察者模式 -**1. 模式定义** +## 模式定义 -定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。主题(Subject)是被观察的对象,而其所有依赖者(Observer)成为观察者。 +定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。主题(Subject)是被观察的对象,而其所有依赖者(Observer)称为观察者。

-**2. 模式类图** +## 模式类图 -主题中具有注册和移除观察者,并通知所有注册者的功能,主题是通过维护一张观察者列表来实现这些操作的。 +主题具有注册和移除观察者、并通知所有注册者的功能,主题是通过维护一张观察者列表来实现这些操作的。 -观察者拥有一个主题对象的引用,因为注册、移除还有数据都在主题当中,必须通过操作主题才能完成相应功能。 +观察者拥有一个主题对象的引用,因为注册、移除观察者功能,还有数据都在主题当中,必须通过操作主题才能完成相应操作。

-**3. 问题描述** +## 问题描述 天气数据布告板会在天气信息发生改变时更新其内容,布告板有多个,并且在将来会继续增加。 -**4. 解决方案类图** +## 问题的解决方案类图

-**5. 设计原则** +## 设计原则 -**为交互对象之间的松耦合设计而努力** 当两个对象之间松耦合,它们依然可以交互,但是不太清楚彼此的细节。由于松耦合的两个对象之间互相依赖程度很低,因此系统具有弹性,能够应对变化。 +为交互对象之间的松耦合设计而努力:当两个对象之间松耦合,它们依然可以交互,但是不清楚彼此的细节。由于松耦合的两个对象之间互相依赖程度很低,因此系统具有弹性,能够应对变化。 -**6. 实现代码** +## 代码实现 ```java public interface Subject { @@ -308,39 +308,39 @@ CurrentConditionsDisplay.update:1.0 1.0 1.0 StatisticsDisplay.update:1.0 1.0 1.0 ``` -# 装饰模式 +# 四、装饰模式 -**1. 问题描述** +## 问题描述 -设计不同种类的饮料,并且每种饮料可以动态添加新的材料,比如可以添加牛奶。计算一种饮料的价格。 +设计不同种类的饮料,饮料可以添加配料,比如可以添加牛奶,并且支持动态添加新配料。每增加一种配料,该饮料的价格就会增加,要求计算一种饮料的价格。 -**2. 模式定义** +## 模式定义 动态地将责任附加到对象上。在扩展功能上,装饰者提供了比继承更有弹性的替代方案。 -下图中 DarkRoast 对象被 Mocha 包裹,Mocha 对象又被 Whip 包裹,并且他们都继承自相同父类,都有 cost() 方法,但是外层对象的 cost() 方法实现调用了内层对象的 cost() 方法。因此,如果要在 DarkRoast 上添加 Mocha,那么只需要用 Mocha 包裹 DarkRoast,如果还需要 Whip ,就用 Whip 包裹 Mocha,最后调用 cost() 方法能把三种对象的价格都包含进去。 +下图表示在 DarkRoast 饮料上新增新添加 Mocha 配料,之后又添加了 Whip 配料。DarkRoast 被 Mocha 包裹,Mocha 又被 Whip 包裹。它们都继承自相同父类,都有 cost() 方法,外层类的 cost() 方法调用了内层类的 cost() 方法。

-**3. 模式类图** +## 模式类图 -装饰者和具体组件都继承自组件类型,其中具体组件的方法实现不需要依赖于其它对象,而装饰者拥有一个组件类型对象,这样它可以装饰其它装饰者或者具体组件。所谓装饰,就是把这个装饰者套在被装饰的对象之外,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。可以看到,具体组件应当是装饰层次的最低层,因为只有具体组件有直接实现而不需要委托给其它对象去处理。 +装饰者(Decorator)和具体组件(ConcreteComponent)都继承自组件(Component),具体组件的方法实现不需要依赖于其它对象,而装饰者组合了一个组件,这样它可以装饰其它装饰者或者具体组件。所谓装饰,就是把这个装饰者套在被装饰上,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。可以看到,具体组件应当是装饰层次的最低层,因为只有具体组件的方法实现不需要依赖于其它对象。

-**4. 问题解决方案的类图** +## 问题的解决方案类图

-**5. 设计原则** +## 设计原则 -**类应该对扩展开放,对修改关闭。** 也就是添加新功能时不需要修改代码。在本章问题中该原则体现在,在饮料中添加新的材料,而不需要去修改饮料的代码。观察则模式也符合这个原则。不可能所有类都能实现这个原则,应当把该原则应用于设计中最有可能改变的地方。 +类应该对扩展开放,对修改关闭:也就是添加新功能时不需要修改代码。在本章问题中该原则体现在,饮料可以动态添加新的配料,而不需要去修改饮料的代码。观察则模式也符合这个原则。不可能把所有的类设计成都满足这一原则,应当把该原则应用于最有可能发生改变的地方。 -**6. Java I/O 中的装饰者模式** +## Java I/O 中的装饰者模式

-**7. 代码实现** +## 代码实现 ```java public interface Beverage { @@ -411,33 +411,32 @@ public class StartbuzzCoffee { 3.0 ``` -# 工厂模式 +# 五、简单工厂 -## 1. 简单工厂 +## 问题描述 -**1. 问题描述** +Pizza 类有很多子类,要求根据不同的情况用不同的子类实例化一个 Pizza 对象。 -有不同的 Pizza,根据不同的情况用不同的子类实例化一个 Pizza 对象。 +## 模式定义 -**2. 定义** +简单工厂不是设计模式,更像是一种编程习惯。它把实例化的操作单独放到一个类中,这个类就成为简单工厂类,让简单工厂类来决定应该用哪个子类来实例化。 -简单工厂不是设计模式,更像是一种编程习惯。在实例化一个超类的对象时,可以用它的所有子类来进行实例化,要根据具体情况来决定使用哪个子类。在这种情况下,把实例化的操作放到简单工厂来中,让简单工厂来决定应该用哪个子类来实例化。 - -这样做能把客户类和具体子类的实现解耦,客户类不再需要知道有哪些子类以及实例化哪个子类。因为客户类往往有多个,如果不使用简单工厂,所有的客户类都要知道所有子类的细节。一旦子类发生改变,例如增加子类,那么所有的客户类都要发生改变。 +这样做能把客户类和具体子类的实现解耦,客户类不再需要知道有哪些子类以及应当实例化哪个子类。因为客户类往往有多个,如果不使用简单工厂,所有的客户类都要知道所有子类的细节。而且一旦子类发生改变,例如增加子类,那么所有的客户类都要进行修改。

-**3. 解决方案类图** +## 问题的解决方案类图

-**4. 代码实现** +## 代码实现 ```java public interface Pizza { public void make(); } ``` + ```java public class CheesePizza implements Pizza{ @Override @@ -446,6 +445,7 @@ public class CheesePizza implements Pizza{ } } ``` + ```java public class GreekPizza implements Pizza{ @Override @@ -454,6 +454,7 @@ public class GreekPizza implements Pizza{ } } ``` + ```java public class SimplePizzaFactory { public Pizza createPizza(String type) { @@ -467,6 +468,7 @@ public class SimplePizzaFactory { } } ``` + ```java public class PizzaStore { public static void main(String[] args) { @@ -483,35 +485,37 @@ public class PizzaStore { CheesePizza ``` -## 2. 工厂方法模式 +# 六、工厂方法模式 -**1. 问题描述** +## 问题描述 -每个地区的 Pizza 店虽然种类相同,但是都有自己的风味,需要单独区分。例如,一个客户点了纽约的 cheese 种类的 Pizza 和在芝加哥点的相同种类的 Pizza 是不同的。 +每个地区的 PizzaStore 卖的 Pizza 虽然种类相同,但是都有自己的风味。一个客户点了纽约的 cheese 种类的 Pizza 和在芝加哥点的相同种类的 Pizza 是不同的。要求设计出满足条件的 PizzaStore。 -**2. 模式定义** +## 模式定义 -定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法让类把实例化推迟到子类。 +定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法把实例化推迟到子类。 -**3. 模式类图** +## 模式类图 -在简单工厂中,创建对象的是另一个类,而在工厂方法中,是由子类来创建对象。下图中,Creator 有一个 anOperation() 方法,这个方法需要用到一组产品类,这组产品类由 factoryMethod() 方法创建。该方法是抽象的,需要由子类去实现。 +在简单工厂中,创建对象的是另一个类,而在工厂方法中,是由子类来创建对象。 + +下图中,Creator 有一个 anOperation() 方法,这个方法需要用到一组产品对象,这组产品对象由 factoryMethod() 方法创建。该方法是抽象的,需要由子类去实现。

-**4. 解决方案类图** +## 问题的解决方案类图 -PizzaStore 由 orderPizza() 方法,顾客可以用它来下单。下单之后需要先使用 createPizza() 来制作 Pizza,这里的 createPizza() 就是 factoryMethod(),不同的 PizzaStore 子类实现了不同的 createPizza()。 +PizzaStore 有 orderPizza() 方法,顾客可以用它来下单。下单之后需要先使用 createPizza() 来制作 Pizza,这里的 createPizza() 就是 factoryMethod(),不同的 PizzaStore 子类实现了不同的 createPizza()。

-**5. 设计原则** +## 设计原则 -**依赖倒置原则** :要依赖抽象,不要依赖具体类。听起来像是针对接口编程,不针对实现编程,但是这个原则说明了:不能让高层组件依赖底层组件,而且,不管高层或底层组件,两者都应该依赖于抽象。例如,下图中 Pizza 是抽象类,PizzaStore 和 Pizza 子类都依赖于 Pizza 这个抽象类。 +依赖倒置原则:要依赖抽象,不要依赖具体类。听起来像是针对接口编程,不针对实现编程,但是这个原则说明了:不能让高层组件依赖底层组件,而且,不管高层或底层组件,两者都应该依赖于抽象。例如,下图中 Pizza 是抽象类,PizzaStore 和 Pizza 子类都依赖于 Pizza 这个抽象类。

-**6. 代码实现** +## 代码实现 ```java public interface Pizza { @@ -607,29 +611,29 @@ NYStyleCheesePizza is making.. ChicagoStyleCheesePizza is making.. ``` -## 3. 抽象工厂模式 +# 七、抽象工厂模式 -**1. 模式定义** +## 模式定义 -提供一个接口,用于创建 **相关对象家族** ,而不需要明确指定具体类。 +提供一个接口,用于创建 **相关的对象家族** 。 -**2. 模式类图** +## 模式类图 抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂模式只是用于创建一个对象,这和抽象工厂模式有很大不同。 抽象工厂模式用到了工厂模式来创建单一对象,在类图左部,AbstractFactory 中的 CreateProductA 和 CreateProductB 方法都是让子类来实现,这两个方法单独来看就是在创建一个对象,这符合工厂模式的定义。 -至于创建对象的家族这一概念是在 Client 体现,Client 要通过 AbstractFactory 同时调用两个方法来创建出两个对象,在这里这两个对象就有很大的相关性,Client 需要这两个对象的协作才能完成任务。 +至于创建对象的家族这一概念是在 Client 体现,Client 要通过 AbstractFactory 同时调用两个方法来创建出两个对象,在这里这两个对象就有很大的相关性,Client 需要同时创建出这两个对象。 -从高层次来看,抽象工厂使用了组合,即 Cilent 组合了 AbstractFactory ,而工厂模式使用了继承。 +从高层次来看,抽象工厂使用了组合,即 Cilent 组合了 AbstractFactory,而工厂模式使用了继承。

-**3. 解决方案类图** +## 解决方案类图

-**4. 代码实现** +## 代码实现 ```java public interface Dough { @@ -738,23 +742,25 @@ ThickCrustDough MarinaraSauce ``` -# 单件模式 +# 八、单例模式 -**1. 模式定义** +## 模式定义 确保一个类只有一个实例,并提供了一个全局访问点。 -**2. 模式类图** +## 模式类图 -使用一个私有构造器、一个私有静态变量以及一个公有静态函数来实现。私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。 +使用一个私有构造器、一个私有静态变量以及一个公有静态函数来实现。 -

+私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。 -**3. 懒汉式-线程不安全** +

-以下实现中,私有静态变量被延迟化实例化,这样做的好处是,如果没有用到该类,那么就不会创建私有静态变量,从而节约资源。 +## 懒汉式-线程不安全 -这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if(uniqueInstance == null) 内的语句块,那么就会多次实例化 uniqueInstance 私有静态变量。 +以下实现中,私有静态变量 uniqueInstance 被延迟化实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。 + +这个实现在多线程环境下是不安全的,如果多个线程能够同时进入`if(uniqueInstance == null)` ,那么就会多次实例化 uniqueInstance。 ```java public class Singleton { @@ -773,32 +779,34 @@ public class Singleton { } ``` -**4. 懒汉式-线程安全** +## 懒汉式-线程安全 -只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了对 uniqueInstance 进行多次实例化的问题。但是这样有一个问题,就是当一个线程进入该方法之后,其它线程视图进入该方法都必须等待,因此性能上有一定的损耗。 +只需要对 `getUniqueInstance()` 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了对 uniqueInstance 进行多次实例化的问题。 + +但是这样有一个问题,就是当一个线程进入该方法之后,其它线程试图进入该方法都必须等待,因此性能上有一定的损耗。 ```java - public static synchronized Singleton getUniqueInstance() { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - return uniqueInstance; +public static synchronized Singleton getUniqueInstance() { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); } + return uniqueInstance; +} ``` -**5. 饿汉式-线程安全** +## 饿汉式-线程安全 -线程不安全问题主要是由于静态实例变量被初始化了多次,那么静态实例变量采用直接实例化就可以解决问题。但是直接初始化的方法也丢失了延迟初始化节约资源的优势。 +线程不安全问题主要是由于 uniqueInstance 被实例化了多次,如果 uniqueInstance 采用直接实例化的话,就不会被实例化多次,也就不会产生线程不安全问题。但是直接实例化的方式也丢失了延迟实例化带来的节约资源的优势。 ```java private static Singleton uniqueInstance = new Singleton(); ``` -**6. 双重校验锁-线程安全** +## 双重校验锁-线程安全 -因为 uniqueInstance 只需要被初始化一次,之后就可以直接使用了。加锁操作只需要对初始化那部分的代码进行,也就是说,只有当 uniqueInstance 没有被初始化时,才需要进行加锁。 +uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行。也就是说,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。 -双重校验锁先判断 uniqueInstance 是否已经被初始化了,如果没有被初始化,那么才对初始化的语句进行加锁。 +双重校验锁先判断 uniqueInstance 是否已经被初始化了,如果没有被实例化,那么才对实例化语句进行加锁。 ```java public class Singleton { @@ -821,7 +829,7 @@ public class Singleton { } ``` -考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 uniqueInstance = new Singleton(); 这条语句,只是早晚的问题,也就是说会进行两次实例化,从而产生了两个实例。 +考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行`uniqueInstance = new Singleton();`这条语句,只是早晚的问题,也就是说会进行两次实例化,从而产生了两个实例。因此必须使用双重校验锁,也就是需要使用两个 if 判断。 ```java if (uniqueInstance == null) { @@ -831,39 +839,39 @@ if (uniqueInstance == null) { } ``` -# 命令模式 +# 九、命令模式 -**1. 问题描述** +## 问题描述 -设计一个遥控器,它有很多按钮,每个按钮可以发起一个命令,让一个家电完成相应操作。 +设计一个遥控器,它有很多按钮,每个按钮可以发起一个命令,命令会让一个家电完成相应操作。有非常多的家电,并且之后会增加家电。 + +

有非常多的家电,并且之后会增加家电。 -

+

-

- -**2. 模式定义** +## 模式定义 将命令封装成对象,以便使用不同的命令来参数化其它对象。 -**3. 解决方案类图** +## 问题的解决方案类图 -- RemoteControl 是遥控器,它可以为每个按钮设置命令对象,并且调用命令对象的 execute() 方法。 +- RemoteControl 是遥控器,它可以为每个按钮设置命令对象,并且执行命令。 -- Command 就是命令对象,命令模式正式将各种命令封装成 Commad 对象来实现的。 +- Command 是命令对象。 -- Light 是命令真正的执行者。可以注意到 LightOnCommand 和 LightOffCommand 类组合了一个 Light 对象,通过组合的方法,就可以将 excute() 方法委托给 Light 对象来执行。 +- Light(电灯)是命令真正的执行者。 -- RemoteLoader 是客户端,注意它与 RemoteControl 的区别。因为 RemoteControl 不能主动地调用自身的方法,因此也就不能当成是客户端。客户端好比人,只有人才能去真正去使用遥控器。 +- RemoteLoader 是客户端,应该注意它与 RemoteControl 的区别。因为 RemoteControl 不能主动地调用自身的方法,因此也就不能当成是客户端。客户端好比人,只有人才能去真正去使用遥控器。 -

+

-**4. 模式类图** +## 模式类图 -

+

-**5. 代码实现** +## 代码实现 ```java public interface Command { @@ -942,33 +950,31 @@ public class RemoteLoader { Light is on! ``` -# 适配器模式 +# 十、适配器模式 -**1. 模式定义** +## 模式定义 将一个类的接口,转换为客户期望的另一个接口。适配器让原本不兼容的类可以合作无间。 -

+

-**2. 模式类图** +## 模式类图 -有两种适配器模式的实现,一种是对象方式,一种是类方式。对象方式是通过组合的方法,让适配器类(Adapter)拥有一个待适配的对象(Adaptee),从而把相应的处理委托给待适配的对象。类方式用到多重继承,Adapter 继承 Target 和 Adaptee,先把 Adapter 当成 Adaptee 类型然后实例化一个对象,再把它当成 Target 类型的,这样 Client 就可以把这个对象当成 Target 的对象来处理,同时拥有 Adaptee 的方法。 +适配器(Adapter)组合一个适配者(Adaptee),Adapter 把操作委托给 Adaptee。

-

+## 问题描述 -**3. 问题描述** +鸭子(Duck)和火鸡(Turkey)拥有不同的叫声,Duck 的叫声调用 quack() 方法,而 Turkey 调用 gobble() 方法。 -鸭子(Duck)和火鸡(Turkey)拥有不同的叫声,Duck 调用的是 quack() 方法,而 Turkey 调用 gobble() 方法。 +要求将 Turkey 的 gobble() 方法适配成 Duck 的 quack() 方法,从而让火鸡冒充鸭子! -要求将 Turkey 的 gobble() 方法适配成 Duck 的 quack() 方法,从而让火鸡冒充鸭子。 - -**4. 解决方案类图** +## 问题的解决方案类图

-**5. 代码实现** +## 代码实现 ```java public interface Duck { @@ -1022,73 +1028,69 @@ public class DuckTestDrive { gobble! ``` -**6. Enumration 适配成 Iterator** +# 十、外观模式 -

- -# 外观模式 - -**1. 模式定义** +## 模式定义 提供了一个统一的接口,用来访问子系统中的一群接口,从而让子系统更容易使用。 -**2. 模式类图** +## 模式类图 -

+

-**3. 问题描述** +## 问题描述 家庭影院中有众多电器,当要进行观看电影时需要对很多电器进行操作。要求简化这些操作,使得家庭影院类只提供一个简化的接口,例如提供一个看电影相关的接口。 -

+

-**4. 解决方案类图** +## 解决方案类图

-**5. 设计原则** +## 设计原则 -**最少知识原则** :只和你的密友谈话。也就是应当使得客户对象所需要交互的对象尽可能少。 +**最少知识原则** :只和你的密友谈话。也就是客户对象所需要交互的对象应当尽可能少。 -**6. 代码实现** +## 代码实现 过于简单,无实现。 -# 模板方法模式 +# 十一、模板方法模式 -**1. 模式定义** +## 模式定义 在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。 -**2. 模式类图** +## 模式类图 模板方法 templateMethod() 定义了算法的骨架,确定了 primitiveOperation1() 和 primitiveOperation2() 方法执行的顺序,而 primitiveOperation1() 和 primitiveOperation2() 让子类去实现。

-**3. 问题描述** +## 问题描述 冲咖啡和冲茶都有类似的流程,但是某些步骤会有点不一样,要求复用那些相同步骤的代码。

-**4. 解决方案类图** +## 问题的解决方案类图 -其中 prepareRecipe() 方法就是模板方法,它确定了其它四个方法的具体执行步骤。其中 brew() 和 addCondiments() 方法在子类中实现。 +prepareRecipe() 方法就是模板方法,它确定了其它四个方法的具体执行步骤。其中 brew() 和 addCondiments() 方法在子类中实现。

-**5. 设计原则** +## 设计原则 **好莱坞原则** :别调用(打电话给)我们,我们会调用(打电话给)你。这一原则可以防止依赖腐败,即防止高层组件依赖低层组件,低层组件又依赖高层组件。该原则在模板方法的体现为,只有父类会调用子类,子类不会调用父类。 -**6. 钩子** +## 钩子 某些步骤在不同实现中可有可无,可以先定义一个什么都不做的方法,把它加到模板方法中,如果子类需要它就覆盖默认实现并加上自己的实现。 -**7. 代码实现** +## 代码实现 ```java public abstract class CaffeineBeverage { @@ -1168,23 +1170,23 @@ pourInCup Tea.addCondiments ``` -# 迭代器模式 +# 十二、迭代器模式 -**1. 模式定义** +## 模式定义 提供顺序访问一个聚合对象中的各个元素的方法,而又不暴露聚合对象内部的表示。 -**2. 模式类图** +## 模式类图 -- Aggregate 是聚合类,其中 createIterator() 方法可以产生一个 Iterator 对象; +- Aggregate 是聚合类,其中 createIterator() 方法可以产生一个 Iterator; - Iterator 主要定义了 hasNext() 和 next() 方法。 -- Client 需要拥有一个 Aggregate 对象,这是很明显的。为了迭代变量 Aggregate 对象,也需要拥有 Iterator 对象。 +- Client 组合了 Aggregate,为了迭代遍历 Aggregate,也需要组合 Iterator。

-**3. 代码实现** +## 代码实现 ```java public class Aggregate { @@ -1258,11 +1260,11 @@ public class Client { 9 ``` -**4. Java 内置的迭代器** +## Java 内置的迭代器 -在使用 Java 的迭代器实现时,需要让聚合对象去实现 Iterable 接口,该接口有一个 iterator() 方法会返回一个 Iterator 对象。 +需要让聚合类实现 Iterable 接口,该接口有一个 iterator() 方法会返回一个 Iterator 对象。 -使用 Java 内置的迭代器实现,客户对象可以使用 foreach 循环来遍历聚合对象中的每个元素。 +可以使用 foreach 循环来顺序访问聚合对象中的每个元素。 Java 中的集合类基本都实现了 Iterable 接口。 @@ -1320,27 +1322,27 @@ public class Client { } ``` -# 组合模式 +# 十三、组合模式 -**1. 设计原则** +## 设计原则 -一个类应该只有一个引起改变的原因。 +一个类应该只有一个引起它改变的原因。 -**2. 模式定义** +## 模式定义 -允许将对象组合成树形结构来表现“整体/部分”层次结构。 +允许将对象组合成树形结构来表现“整体/部分”关系。 组合能让客户以一致的方式处理个别对象以及组合对象。 -**3. 模式类图** +## 模式类图 组件(Component)类是组合类(Composite)和叶子类(Leaf)的父类,可以把组合类看成是树的中间节点。 -组合类拥有一个组件对象,因此组合类的操作可以委托给组件对象去处理,而组件对象可以是另一个组合类或者叶子类。 +组合对象拥有一个组件对象,因此组合对象的操作可以委托给组件对象去处理,而组件对象可以是另一个组合对象或者叶子对象。

-**4. 代码实现** +## 代码实现 ```java public abstract class Component { @@ -1441,19 +1443,19 @@ Composite:root --left:3 ``` -# 状态模式 +# 十四、状态模式 -**1. 模式定义** +## 模式定义 允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它所属的类。 -**2. 模式类图** +## 模式类图 Context 的 request() 方法委托给 State 对象去处理。当 Context 组合的 State 对象发生改变时,它的行为也就发生了改变。

-**3. 与策略模式的比较** +## 与策略模式的比较 状态模式的类图和策略模式一样,并且都是能够动态改变对象的行为。 @@ -1463,13 +1465,13 @@ Context 的 request() 方法委托给 State 对象去处理。当 Context 组合 状态模式主要是用来解决状态转移的问题,当状态发生转移了,那么 Context 对象就会改变它的行为;而策略模式主要是用来封装一组可以互相替代的算法族,并且可以根据需要动态地去替换 Context 需要使用哪个算法。 -**4. 问题描述** +## 问题描述 糖果销售机有多种状态,每种状态下销售机有不同的行为,状态可以发生转移,使得销售机的行为也发生改变。

-**5. 直接解决方案** +## 直接解决方案 在糖果机的每个操作函数里面,判断当前的状态,根据不同的状态进行不同的处理,并且发生不同的状态转移。 @@ -1477,7 +1479,7 @@ Context 的 request() 方法委托给 State 对象去处理。当 Context 组合

-**6 代码实现** +## 代码实现 糖果销售机即 Context。 @@ -1763,34 +1765,36 @@ You turned, but there are no gumballs No gumball dispensed ``` -# 代理模式 +# 十五、代理模式 -# 复合模式 +# 十六、MVC -## MVC - -**传统 MVC** +## 传统 MVC 视图使用组合模式,模型使用了观察者模式,控制器使用了策略模式。

-**Web 中的 MVC** +## Web 中的 MVC 模式不再使用观察者模式。

-# 与设计模式相处 +# 十七、与设计模式相处 -定义:在某 **情境** 下,针对某 **问题** 的某种 **解决方案**。 +## 定义 + +在某情境下,针对某问题的某种解决方案。 + +## 何时使用 过度使用设计模式可能导致代码被过度工程化,应该总是用最简单的解决方案完成工作,并在真正需要模式的地方才使用它。 -反模式:不好的解决方案来解决一个问题。主要作用是为了警告人们不要使用这些解决方案。 +## 反模式 -模式分类: +不好的解决方案来解决一个问题。主要作用是为了警告人们不要使用这些解决方案。 + +## 模式分类

- -# 剩下的模式 diff --git a/notes/重构.md b/notes/重构.md index 8cf08472..bc05e60a 100644 --- a/notes/重构.md +++ b/notes/重构.md @@ -1,6 +1,6 @@ -* [第一个案例](#第一个案例) -* [重构原则](#重构原则) +* [一、第一个案例](#一第一个案例) +* [二、重构原则](#二重构原则) * [定义](#定义) * [为何重构](#为何重构) * [三次法则](#三次法则) @@ -9,108 +9,108 @@ * [何时不该重构](#何时不该重构) * [重构与设计](#重构与设计) * [重构与性能](#重构与性能) -* [代码的坏味道](#代码的坏味道) - * [1. Duplicated Code(重复代码)](#1-duplicated-code重复代码) - * [2. Long Method(过长函数)](#2-long-method过长函数) - * [3. Large Class(过大的类)](#3-large-class过大的类) - * [4. Long Parameter List(过长的参数列表)](#4-long-parameter-list过长的参数列表) - * [5. Divergent Change(发散式变化)](#5-divergent-change发散式变化) - * [6. Shotgun Surgery(散弹式修改)](#6-shotgun-surgery散弹式修改) - * [7. Feature Envy(依恋情结)](#7-feature-envy依恋情结) - * [8. Data Clumps(数据泥团)](#8-data-clumps数据泥团) - * [9. Primitive Obsession(基本类型偏执)](#9-primitive-obsession基本类型偏执) - * [10. Switch Statements(switch 惊悚现身)](#10-switch-statementsswitch-惊悚现身) - * [11. Parallel Inheritance Hierarchies(平行继承体系)](#11-parallel-inheritance-hierarchies平行继承体系) - * [12. Lazy Class(冗余类)](#12-lazy-class冗余类) - * [13. Speculative Generality(夸夸其谈未来性)](#13-speculative-generality夸夸其谈未来性) - * [14. Temporary Field(令人迷惑的暂时字段)](#14-temporary-field令人迷惑的暂时字段) - * [15. Message Chains(过度耦合的消息链)](#15-message-chains过度耦合的消息链) - * [16. Middle Man(中间人)](#16-middle-man中间人) - * [17. Inappropriate Intimacy(狎昵关系)](#17-inappropriate-intimacy狎昵关系) - * [18. Alernative Classes with Different Interfaces(异曲同工的类)](#18-alernative-classes-with-different-interfaces异曲同工的类) - * [19. Incomplete Library Class(不完美的类库)](#19-incomplete-library-class不完美的类库) - * [20. Data Class(幼稚的数据类)](#20-data-class幼稚的数据类) - * [21. Refused Bequest(被拒绝的馈赠)](#21-refused-bequest被拒绝的馈赠) - * [22. Comments(过多的注释)](#22-comments过多的注释) -* [构筑测试体系](#构筑测试体系) -* [重新组织函数](#重新组织函数) - * [1. Extract Method(提炼函数)](#1-extract-method提炼函数) - * [2. Inline Method(内联函数)](#2-inline-method内联函数) - * [3. Inline Temp(内联临时变量)](#3-inline-temp内联临时变量) - * [4. Replace Temp with Query(以查询取代临时变量)](#4-replace-temp-with-query以查询取代临时变量) - * [5. Introduce Explaining Variable(引起解释变量)](#5-introduce-explaining-variable引起解释变量) - * [6. Split Temporary Variable(分解临时变量)](#6-split-temporary-variable分解临时变量) - * [7. Remove Assigments to Parameters(移除对参数的赋值)](#7-remove-assigments-to-parameters移除对参数的赋值) - * [8. Replace Method with Method Object(以函数对象取代函数)](#8-replace-method-with-method-object以函数对象取代函数) - * [9. Subsititute Algorithn(替换算法)](#9-subsititute-algorithn替换算法) -* [在对象之间搬移特性](#在对象之间搬移特性) - * [1. Move Method(搬移函数)](#1-move-method搬移函数) - * [2. Move Field(搬移字段)](#2-move-field搬移字段) - * [3. Extract Class(提炼类)](#3-extract-class提炼类) - * [4. Inline Class(将类内联化)](#4-inline-class将类内联化) - * [5. Hide Delegate(隐藏“委托关系”)](#5-hide-delegate隐藏“委托关系”) - * [6. Remove Middle Man(移除中间人)](#6-remove-middle-man移除中间人) - * [7. Introduce Foreign Method(引入外加函数)](#7-introduce-foreign-method引入外加函数) - * [8. Introduce Local Extension(引入本地扩展)](#8-introduce-local-extension引入本地扩展) -* [重新组织数据](#重新组织数据) - * [1. Self Encapsulate Field(自封装字段)](#1-self-encapsulate-field自封装字段) - * [2. Replace Data Value with Object(以对象取代数据值)](#2-replace-data-value-with-object以对象取代数据值) - * [3. Change Value to Reference(将值对象改成引用对象)](#3-change-value-to-reference将值对象改成引用对象) - * [4. Change Reference to value(将引用对象改为值对象)](#4-change-reference-to-value将引用对象改为值对象) - * [5. Replace Array with Object(以对象取代数组)](#5-replace-array-with-object以对象取代数组) - * [6. Duplicate Observed Data(赋值“被监视数据”)](#6-duplicate-observed-data赋值“被监视数据”) - * [7. Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)](#7-change-unidirectional-association-to-bidirectional将单向关联改为双向关联) - * [8. Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)](#8-change-bidirectional-association-to-unidirectional将双向关联改为单向关联) - * [9. Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)](#9-replace-magic-number-with-symbolic-constant以字面常量取代魔法数) - * [10. Encapsulate Field(封装字段)](#10-encapsulate-field封装字段) - * [11. Encapsulate Collection(封装集合)](#11-encapsulate-collection封装集合) - * [12. Replace Record with Data Class(以数据类取代记录)](#12-replace-record-with-data-class以数据类取代记录) - * [13. Replace Type Code with Class(以类取代类型码)](#13-replace-type-code-with-class以类取代类型码) - * [14. Replace Type Code with Subcalsses(以子类取代类型码)](#14-replace-type-code-with-subcalsses以子类取代类型码) - * [15. Replace Type Code with State/Strategy (以 State/Strategy 取代类型码)](#15-replace-type-code-with-statestrategy-以-statestrategy-取代类型码) - * [16. Replace Subclass with Fields(以字段取代子类)](#16-replace-subclass-with-fields以字段取代子类) -* [简化条件表达式](#简化条件表达式) - * [1. Decompose Conditional(分解条件表达式)](#1-decompose-conditional分解条件表达式) - * [2. Consolidate Conditional Expression(合并条件表达式)](#2-consolidate-conditional-expression合并条件表达式) - * [3. Consolidate Duplicate Conditional Fragments (合并重复的条件片段)](#3-consolidate-duplicate-conditional-fragments-合并重复的条件片段) - * [4. Remove Control Flag(移除控制标记)](#4-remove-control-flag移除控制标记) - * [5. Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件表达式)](#5-replace-nested-conditional-with-guard-clauses-以卫语句取代嵌套条件表达式) - * [6. Replace Conditional with Polymorphism (以多态取代条件表达式)](#6-replace-conditional-with-polymorphism-以多态取代条件表达式) - * [7. Introduce Null Object(引入Null对象)](#7-introduce-null-object引入null对象) - * [8. Introduce Assertion(引入断言)](#8-introduce-assertion引入断言) -* [简化函数调用](#简化函数调用) - * [1. Rename Method(函数改名)](#1-rename-method函数改名) - * [2. Add Parameter(添加参数)](#2-add-parameter添加参数) - * [3. Remove Parameter(移除参数)](#3-remove-parameter移除参数) - * [4. Separate Query from Modifier(将查询函数和修改函数分离)](#4-separate-query-from-modifier将查询函数和修改函数分离) - * [5. Parameterize Method(令函数携带参数)](#5-parameterize-method令函数携带参数) - * [6. Replace Parameter with Explicit Methods(以明确函数取代参数)](#6-replace-parameter-with-explicit-methods以明确函数取代参数) - * [7. Preserve Whole Object(保持对象完整)](#7-preserve-whole-object保持对象完整) - * [8. Replace Parameter with Methods(以函数取代参数)](#8-replace-parameter-with-methods以函数取代参数) - * [9. Introduce Parameter Object(引入参数对象)](#9-introduce-parameter-object引入参数对象) - * [10. Remove Setting Method(移除设值函数)](#10-remove-setting-method移除设值函数) - * [11. Hide Method(隐藏函数)](#11-hide-method隐藏函数) - * [12. Replace Constructor with Factory Method (以工厂函数取代构造函数)](#12-replace-constructor-with-factory-method-以工厂函数取代构造函数) - * [13. Encapsulate Downcast(封装向下转型)](#13-encapsulate-downcast封装向下转型) - * [14. Replace Error Code with Exception (以异常取代错误码)](#14-replace-error-code-with-exception-以异常取代错误码) - * [15. Replace Exception with Test(以测试取代异常)](#15-replace-exception-with-test以测试取代异常) -* [处理概括关系](#处理概括关系) - * [1. Pull Up Field(字段上移)](#1-pull-up-field字段上移) - * [2. Pull Up Method(函数上移)](#2-pull-up-method函数上移) - * [3. Pull Up Constructor Body(构造函数本体上移)](#3-pull-up-constructor-body构造函数本体上移) - * [4. Push Down Method(函数下移)](#4-push-down-method函数下移) - * [5. Push Down Field(字段下移)](#5-push-down-field字段下移) - * [6. Extract Subclass(提炼子类)](#6-extract-subclass提炼子类) - * [7. Extract Superclass(提炼超类)](#7-extract-superclass提炼超类) - * [8. Extract Interface(提炼接口)](#8-extract-interface提炼接口) - * [9. Collapse Hierarchy(折叠继承体系)](#9-collapse-hierarchy折叠继承体系) - * [10. Form Template Method(塑造模板函数)](#10-form-template-method塑造模板函数) - * [11. Replace Inheritance with Delegation (以委托取代继承)](#11-replace-inheritance-with-delegation-以委托取代继承) - * [12. Replace Delegation with Inheritance (以继承取代委托)](#12-replace-delegation-with-inheritance-以继承取代委托) +* [三、代码的坏味道](#三代码的坏味道) + * [1. 重复代码](#1-重复代码) + * [2. 过长函数](#2-过长函数) + * [3. 过大的类](#3-过大的类) + * [4. 过长的参数列表](#4-过长的参数列表) + * [5. 发散式变化](#5-发散式变化) + * [6. 散弹式修改](#6-散弹式修改) + * [7. 依恋情结](#7-依恋情结) + * [8. 数据泥团](#8-数据泥团) + * [9. 基本类型偏执](#9-基本类型偏执) + * [10. switch 惊悚现身](#10-switch-惊悚现身) + * [11. 平行继承体系](#11-平行继承体系) + * [12. 冗余类](#12-冗余类) + * [13. 夸夸其谈未来性](#13-夸夸其谈未来性) + * [14. 令人迷惑的暂时字段](#14-令人迷惑的暂时字段) + * [15. 过度耦合的消息链](#15-过度耦合的消息链) + * [16. 中间人](#16-中间人) + * [17. 狎昵关系](#17-狎昵关系) + * [18. 异曲同工的类](#18-异曲同工的类) + * [19. 不完美的类库](#19-不完美的类库) + * [20. 幼稚的数据类](#20-幼稚的数据类) + * [21. 被拒绝的馈赠](#21-被拒绝的馈赠) + * [22. 过多的注释](#22-过多的注释) +* [四、构筑测试体系](#四构筑测试体系) +* [五、重新组织函数](#五重新组织函数) + * [1. 提炼函数](#1-提炼函数) + * [2. 内联函数](#2-内联函数) + * [3. 内联临时变量](#3-内联临时变量) + * [4. 以查询取代临时变量](#4-以查询取代临时变量) + * [5. 引起解释变量](#5-引起解释变量) + * [6. 分解临时变量](#6-分解临时变量) + * [7. 移除对参数的赋值](#7-移除对参数的赋值) + * [8. 以函数对象取代函数](#8-以函数对象取代函数) + * [9. 替换算法](#9-替换算法) +* [六、在对象之间搬移特性](#六在对象之间搬移特性) + * [1. 搬移函数](#1-搬移函数) + * [2. 搬移字段](#2-搬移字段) + * [3. 提炼类](#3-提炼类) + * [4. 将类内联化](#4-将类内联化) + * [5. 隐藏委托关系](#5-隐藏委托关系) + * [6. 移除中间人](#6-移除中间人) + * [7. 引入外加函数](#7-引入外加函数) + * [8. 引入本地扩展](#8-引入本地扩展) +* [七、重新组织数据](#七重新组织数据) + * [1. 自封装字段](#1-自封装字段) + * [2. 以对象取代数据值](#2-以对象取代数据值) + * [3. 将值对象改成引用对象](#3-将值对象改成引用对象) + * [4. 将引用对象改为值对象](#4-将引用对象改为值对象) + * [5. 以对象取代数组](#5-以对象取代数组) + * [6. 赋值被监视数据](#6-赋值被监视数据) + * [7. 将单向关联改为双向关联](#7-将单向关联改为双向关联) + * [8. 将双向关联改为单向关联](#8-将双向关联改为单向关联) + * [9. 以字面常量取代魔法数](#9-以字面常量取代魔法数) + * [10. 封装字段](#10-封装字段) + * [11. 封装集合](#11-封装集合) + * [12. 以数据类取代记录](#12-以数据类取代记录) + * [13. 以类取代类型码](#13-以类取代类型码) + * [14. 以子类取代类型码](#14-以子类取代类型码) + * [15. 以 State/Strategy 取代类型码](#15-以-statestrategy-取代类型码) + * [16. 以字段取代子类](#16-以字段取代子类) +* [八、简化条件表达式](#八简化条件表达式) + * [1. 分解条件表达式](#1-分解条件表达式) + * [2. 合并条件表达式](#2-合并条件表达式) + * [3. 合并重复的条件片段](#3-合并重复的条件片段) + * [4. 移除控制标记](#4-移除控制标记) + * [5. 以卫语句取代嵌套条件表达式](#5-以卫语句取代嵌套条件表达式) + * [6. 以多态取代条件表达式](#6-以多态取代条件表达式) + * [7. 引入 Null 对象](#7-引入-null-对象) + * [8. 引入断言](#8-引入断言) +* [九、简化函数调用](#九简化函数调用) + * [1. 函数改名](#1-函数改名) + * [2. 添加参数](#2-添加参数) + * [3. 移除参数](#3-移除参数) + * [4. 将查询函数和修改函数分离](#4-将查询函数和修改函数分离) + * [5. 令函数携带参数](#5-令函数携带参数) + * [6. 以明确函数取代参数](#6-以明确函数取代参数) + * [7. 保持对象完整](#7-保持对象完整) + * [8. 以函数取代参数](#8-以函数取代参数) + * [9. 引入参数对象](#9-引入参数对象) + * [10. 移除设值函数](#10-移除设值函数) + * [11. 隐藏函数](#11-隐藏函数) + * [12. 以工厂函数取代构造函数](#12-以工厂函数取代构造函数) + * [13. 封装向下转型](#13-封装向下转型) + * [14. 以异常取代错误码](#14-以异常取代错误码) + * [15. 以测试取代异常](#15-以测试取代异常) +* [十、处理概括关系](#十处理概括关系) + * [1. 字段上移](#1-字段上移) + * [2. 函数上移](#2-函数上移) + * [3. 构造函数本体上移](#3-构造函数本体上移) + * [4. 函数下移](#4-函数下移) + * [5. 字段下移](#5-字段下移) + * [6. 提炼子类](#6-提炼子类) + * [7. 提炼超类](#7-提炼超类) + * [8. 提炼接口](#8-提炼接口) + * [9. 折叠继承体系](#9-折叠继承体系) + * [10. 塑造模板函数](#10-塑造模板函数) + * [11. 以委托取代继承](#11-以委托取代继承) + * [12. 以继承取代委托](#12-以继承取代委托) -# 第一个案例 +# 一、第一个案例 如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构这个程序。 @@ -130,7 +130,6 @@ 可以发现,该代码没有使用 Customer 类中的任何信息,更多的是使用 Rental 类的信息,因此第一个可以重构的点就是把具体计费的代码移到 Rental 类中,然后 Customer 类的 getTotalCharge() 方法只需要调用 Rental 类中的计费方法即可。 - ```java class Customer... double getTotalCharge() { @@ -173,7 +172,7 @@ double getTotalCharge() {

-# 重构原则 +# 二、重构原则 ## 定义 @@ -229,13 +228,15 @@ double getTotalCharge() { 应当只关注关键代码的性能,因为只有一小部分的代码是关键代码。 -# 代码的坏味道 +# 三、代码的坏味道 本章主要介绍一些不好的代码,也就是说这些代码应该被重构。 文中提到的具体重构原则可以先忽略。 -## 1. Duplicated Code(重复代码) +## 1. 重复代码 + +> Duplicated Code 同一个类的两个函数有相同表达式,则用 Extract Method 提取出重复代码; @@ -245,7 +246,9 @@ double getTotalCharge() { 如果两个毫不相关的类出现重复代码,则使用 Extract Class 方法将重复代码提取到一个独立类中。 -## 2. Long Method(过长函数) +## 2. 过长函数 + +> Long Method 函数应该尽可能小,因为小函数具有解释能力、共享能力、选择能力。 @@ -255,111 +258,151 @@ Extract Method 会把很多参数和临时变量都当做参数,可以用 Repl 条件和循环语句往往也需要提取到新的函数中。 -## 3. Large Class(过大的类) +## 3. 过大的类 + +> Large Class 应该尽可能让一个类只做一件事,而过大的类做了过多事情,需要使用 Extract Class 或 Extract Subclass。 先确定客户端如何使用该类,然后运用 Extract Interface 为每一种使用方式提取出一个接口。 -## 4. Long Parameter List(过长的参数列表) +## 4. 过长的参数列表 + +> Long Parameter List 太长的参数列表往往会造成前后不一致,不易使用。 面向对象程序中,函数所需要的数据通常内在宿主类中找到。 -## 5. Divergent Change(发散式变化) +## 5. 发散式变化 + +> Divergent Change 设计原则:一个类应该只有一个引起改变的原因。也就是说,针对某一外界变化所有相应的修改,都只应该发生在单一类中。 针对某种原因的变化,使用 Extract Class 将它提炼到一个类中。 -## 6. Shotgun Surgery(散弹式修改) +## 6. 散弹式修改 + +> Shotgun Surgery 一个变化引起多个类修改; 使用 Move Method 和 Move Field 把所有需要修改的代码放到同一个类中。 -## 7. Feature Envy(依恋情结) +## 7. 依恋情结 + +> Feature Envy 一个函数对某个类的兴趣高于对自己所处类的兴趣,通常是过多访问其它类的数据, 使用 Move Method 将它移到该去的地方,如果对多个类都有 Feature Envy,先用 Extract Method 提取出多个函数。 -## 8. Data Clumps(数据泥团) +## 8. 数据泥团 + +> Data Clumps 有些数据经常一起出现,比如两个类具有相同的字段、许多函数有相同的参数,这些绑定在一起出现的数据应该拥有属于它们自己的对象。 使用 Extract Class 将它们放在一起。 -## 9. Primitive Obsession(基本类型偏执) +## 9. 基本类型偏执 + +> Primitive Obsession 使用类往往比使用基本类型更好,使用 Replace Data Value with Object 将数据值替换为对象。 -## 10. Switch Statements(switch 惊悚现身) +## 10. switch 惊悚现身 + +> Switch Statements 具体参见第一章的案例。 -## 11. Parallel Inheritance Hierarchies(平行继承体系) +## 11. 平行继承体系 + +> Parallel Inheritance Hierarchies 每当为某个类增加一个子类,必须也为另一个类相应增加一个子类。 这种结果会带来一些重复性,消除重复性的一般策略:让一个继承体系的实例引用另一个继承体系的实例。 -## 12. Lazy Class(冗余类) +## 12. 冗余类 + +> Lazy Class 如果一个类没有做足够多的工作,就应该消失。 -## 13. Speculative Generality(夸夸其谈未来性) +## 13. 夸夸其谈未来性 + +> Speculative Generality 有些内容是用来处理未来可能发生的变化,但是往往会造成系统难以理解和维护,并且预测未来可能发生的改变很可能和最开始的设想相反。因此,如果不是必要,就不要这么做。 -## 14. Temporary Field(令人迷惑的暂时字段) +## 14. 令人迷惑的暂时字段 + +> Temporary Field 某个字段仅为某种特定情况而设,这样的代码不易理解,因为通常认为对象在所有时候都需要它的所有字段。 把这种字段和特定情况的处理操作使用 Extract Class 提炼到一个独立类中。 -## 15. Message Chains(过度耦合的消息链) +## 15. 过度耦合的消息链 + +> Message Chains 一个对象请求另一个对象,然后再向后者请求另一个对象,然后...,这就是消息链。采用这种方式,意味着客户代码将与对象间的关系紧密耦合。 改用函数链,用函数委托另一个对象来处理。 -## 16. Middle Man(中间人) +## 16. 中间人 + +> Middle Man 中间人负责处理委托给它的操作,如果一个类中有过多的函数都委托给其它类,那就是过度运用委托,应当 Remove Middle Man,直接与负责的对象打交道。 -## 17. Inappropriate Intimacy(狎昵关系) +## 17. 狎昵关系 + +> Inappropriate Intimacy 两个类多于亲密,花费太多时间去探讨彼此的 private 成分。 -## 18. Alernative Classes with Different Interfaces(异曲同工的类) +## 18. 异曲同工的类 + +> Alernative Classes with Different Interfaces 两个函数做同一件事,却有着不同的签名。 使用 Rename Method 根据它们的用途重新命名。 -## 19. Incomplete Library Class(不完美的类库) +## 19. 不完美的类库 + +> Incomplete Library Class 类库的设计者不可能设计出完美的类库,当我们需要对类库进行一些修改时,可以使用以下两种方法:如果只是修改一两个函数,使用 Introduce Foreign Method;如果要添加一大堆额外行为,使用 Introduce Local Extension。 -## 20. Data Class(幼稚的数据类) +## 20. 幼稚的数据类 + +> Data Class 它只拥有一些数据字段,以及用于访问这些字段的函数,除此之外一无长物。 找出字段使用的地方,然后把相应的操作移到 Data Class 中。 -## 21. Refused Bequest(被拒绝的馈赠) +## 21. 被拒绝的馈赠 + +> Refused Bequest 子类不想继承超类的所有函数和数据。 为子类新建一个兄弟类,不需要的函数或数据使用 Push Down Method 和 Push Down Field 下推给那个兄弟。 -## 22. Comments(过多的注释) +## 22. 过多的注释 + +> Comments 使用 Extract Method 提炼出需要注释的部分,然后用函数名来解释函数的行为。 -# 构筑测试体系 +# 四、构筑测试体系 Java 可以使用 Junit 进行单元测试。 @@ -371,19 +414,25 @@ Java 可以使用 Junit 进行单元测试。 应当集中测试可能出错的边界条件。 -# 重新组织函数 +# 五、重新组织函数 -## 1. Extract Method(提炼函数) +## 1. 提炼函数 + +> Extract Method 将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。 -## 2. Inline Method(内联函数) +## 2. 内联函数 + +> Inline Method 一个函数的本体与名称同样清楚易懂。 在函数调用点插入函数本体,然后移除该函数。 -## 3. Inline Temp(内联临时变量) +## 3. 内联临时变量 + +> Inline Temp 一个临时变量,只被简单表达式赋值一次,而它妨碍了其它重构手法。 @@ -398,7 +447,9 @@ return basePrice > 1000; return anOrder.basePrice() > 1000; ``` -## 4. Replace Temp with Query(以查询取代临时变量) +## 4. 以查询取代临时变量 + +> Replace Temp with Query 以临时变量保存某一表达式的运算结果,将这个表达式提炼到一个独立函数中,将所有对临时变量的引用点替换为对新函数的调用。Replace Temp with Query 往往是 Extract Method 之前必不可少的一个步骤,因为局部变量会使代码难以提炼。 @@ -422,7 +473,9 @@ double basePrice(){ } ``` -## 5. Introduce Explaining Variable(引起解释变量) +## 5. 引起解释变量 + +> Introduce Explaining Variable 将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。 @@ -444,13 +497,17 @@ if(isMacOS && isIEBrower && wasInitialized() && wasResized) { } ``` -## 6. Split Temporary Variable(分解临时变量) +## 6. 分解临时变量 + +> Split Temporary Variable 某个临时变量被赋值超过一次,它既不是循环变量,也不是用于收集计算结果。 针对每次赋值,创造一个独立、对应的临时变量,每个临时变量只承担一个责任。 -## 7. Remove Assigments to Parameters(移除对参数的赋值) +## 7. 移除对参数的赋值 + +> Remove Assigments to Parameters 以一个临时变量取代对该参数的赋值。 @@ -465,37 +522,51 @@ int discount (int inputVal, int quentity, int yearToDate){ if (inputVal > 50) result -= 2; ``` -## 8. Replace Method with Method Object(以函数对象取代函数) +## 8. 以函数对象取代函数 + +> Replace Method with Method Object 当对一个大型函数采用 Extract Method 时,由于包含了局部变量使得很难进行该操作。 将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。然后可以在同一个对象中将这个大型函数分解为多个小型函数。 -## 9. Subsititute Algorithn(替换算法) +## 9. 替换算法 -# 在对象之间搬移特性 +> Subsititute Algorithn -## 1. Move Method(搬移函数) +# 六、在对象之间搬移特性 + +## 1. 搬移函数 + +> Move Method 类中的某个函数与另一个类进行更多交流:调用后者或者被后者调用。 将这个函数搬移到另一个类中。 -## 2. Move Field(搬移字段) +## 2. 搬移字段 + +> Move Field 类中的某个字段被另一个类更多地用到,这里的用到是指调用取值设值函数,应当把该字段移到另一个类中。 -## 3. Extract Class(提炼类) +## 3. 提炼类 + +> Extract Class 某个类做了应当由两个类做的事。 应当建立一个新类,将相关的字段和函数从旧类搬移到新类。 -## 4. Inline Class(将类内联化) +## 4. 将类内联化 + +> Inline Class 与 Extract Class 相反。 -## 5. Hide Delegate(隐藏“委托关系”) +## 5. 隐藏委托关系 + +> Hide Delegate 建立所需的函数,隐藏委托关系。 @@ -531,57 +602,77 @@ public Person getManager(){ } ``` -## 6. Remove Middle Man(移除中间人) +## 6. 移除中间人 + +> Remove Middle Man 与 Hide Delegate 相反,本方法需要移除委托函数,让客户直接调用委托类。 Hide Delegate 有很大好处,但是它的代价是:每当客户要使用受托类的新特性时,就必须在服务器端添加一个简单的委托函数。随着受委托的特性越来越多,服务器类完全变成了一个“中间人”。 -## 7. Introduce Foreign Method(引入外加函数) +## 7. 引入外加函数 + +> Introduce Foreign Method 需要为提供服务的类添加一个函数,但是无法修改这个类。 可以在客户类中建立一个函数,并以第一参数形式传入一个服务类的实例,让客户类组合服务器实例。 -## 8. Introduce Local Extension(引入本地扩展) +## 8. 引入本地扩展 + +> Introduce Local Extension 和 Introduce Foreign Method 目的一样,但是 Introduce Local Extension 通过建立新的类来实现。有两种方式:子类或者包装类,子类就是通过继承实现,包装类就是通过组合实现。 -# 重新组织数据 +# 七、重新组织数据 -## 1. Self Encapsulate Field(自封装字段) +## 1. 自封装字段 + +> Self Encapsulate Field 为字段建立取值/设值函数,并用这些函数来访问字段。只有当子类想访问超类的一个字段,又想在子类中将对这个字段访问改为一个计算后的值,才使用这种方式,否则直接访问字段的方式简洁明了。 -## 2. Replace Data Value with Object(以对象取代数据值) +## 2. 以对象取代数据值 + +> Replace Data Value with Object 在开发初期,往往会用简单的数据项表示简单的情况,但是随着开发的进行,一些简单数据项会具有一些特殊行为。比如一开始会把电话号码存成字符串,但是随后发现电话号码需要“格式化”、“抽取区号”之类的特殊行为。 -## 3. Change Value to Reference(将值对象改成引用对象) +## 3. 将值对象改成引用对象 + +> Change Value to Reference 将彼此相等的实例替换为同一个对象。这就要用一个工厂来创建这种唯一对象,工厂类中需要保留一份已经创建对象的列表,当要创建一个对象时,先查找这份列表中是否已经存在该对象,如果存在,则返回列表中的这个对象;否则,新建一个对象,添加到列表中,并返回该对象。 -## 4. Change Reference to value(将引用对象改为值对象) +## 4. 将引用对象改为值对象 + +> Change Reference to value 以 Change Value to Reference 相反。值对象有个非常重要的特性:它是不可变的,不可变表示如果要改变这个对象,必须用一个新的对象来替换旧对象,而不是修改旧对象。 需要为值对象实现 equals() 和 hashCode() 方法 -## 5. Replace Array with Object(以对象取代数组) +## 5. 以对象取代数组 + +> Replace Array with Object 有一个数组,其中的元素各自代表不同的东西。 以对象替换数组,对于数组中的每个元素,以一个字段来表示,这样方便操作,也更容易理解。 -## 6. Duplicate Observed Data(赋值“被监视数据”) +## 6. 赋值被监视数据 + +> Duplicate Observed Data 一些领域数据置身于 GUI 控件中,而领域函数需要访问这些数据。 将该数据赋值到一个领域对象中,建立一个 Oberver 模式,用以同步领域对象和 GUI 对象内的重复数据。 -

+

-## 7. Change Unidirectional Association to Bidirectional(将单向关联改为双向关联) +## 7. 将单向关联改为双向关联 + +> Change Unidirectional Association to Bidirectional 当两个类都需要对方的特性时,可以使用双向关联。 @@ -612,55 +703,75 @@ class Curstomer{ 注意到,这里让 Curstomer 类来控制关联关系。有以下原则来决定哪个类来控制关联关系:如果某个对象是组成另一个对象的部件,那么由后者负责控制关联关系;如果是一对多关系,则由单一引用那一方来控制关联关系。 -## 8. Change Bidirectional Association to Unidirectional(将双向关联改为单向关联) +## 8. 将双向关联改为单向关联 + +> Change Bidirectional Association to Unidirectional 和 Change Unidirectional Association to Bidirectiona 为反操作。 双向关联维护成本高,并且也不易于理解。大量的双向连接很容易造成“僵尸对象”:某个对象本身已经死亡了,却保留在系统中,因为它的引用还没有全部完全清除。 -## 9. Replace Magic Number with Symbolic Constant(以字面常量取代魔法数) +## 9. 以字面常量取代魔法数 + +> Replace Magic Number with Symbolic Constant 创建一个常量,根据其意义为它命名,并将字面常量换位这个常量。 -## 10. Encapsulate Field(封装字段) +## 10. 封装字段 + +> Encapsulate Field public 字段应当改为 private,并提供相应的访问函数。 -## 11. Encapsulate Collection(封装集合) +## 11. 封装集合 + +> Encapsulate Collection 函数返回集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。如果函数返回集合自身,会让用户得以修改集合内容而集合拥有者却一无所知。 -## 12. Replace Record with Data Class(以数据类取代记录) +## 12. 以数据类取代记录 -## 13. Replace Type Code with Class(以类取代类型码) +> Replace Record with Data Class + +## 13. 以类取代类型码 + +> Replace Type Code with Class 类中有一个数值类型码,但它并不影响类的行为,就用一个新类替换该数值类型码。如果类型码出现在 switch 语句中,需要使用 Replace Conditional with Polymorphism 去掉 switch,首先必须运用 Replace Type Code with Subcalss 或 Replace Type Code with State/Strategy 去掉类型码。 -

+

-## 14. Replace Type Code with Subcalsses(以子类取代类型码) +## 14. 以子类取代类型码 + +> Replace Type Code with Subcalsses 有一个不可变的类型码,它会影响类的行为,以子类取代这个类型码。 -

+

-## 15. Replace Type Code with State/Strategy (以 State/Strategy 取代类型码) +## 15. 以 State/Strategy 取代类型码 + +> Replace Type Code with State/Strategy 有一个可变的类型码,它会影响类的行为,以状态对象取代类型码。 和 Replace Type Code with Subcalsses 的区别是 Replace Type Code with State/Strategy 的类型码是动态可变的,前者通过继承的方式来实现,后者通过组合的方式来实现。因为类型码可变,如果通过继承的方式,一旦一个对象的类型码改变,那么就要改变用新的对象来取代旧对象,而客户端难以改变新的对象。但是通过组合的方式,改变引用的状态类是很容易的。 -

+

-## 16. Replace Subclass with Fields(以字段取代子类) +## 16. 以字段取代子类 + +> Replace Subclass with Fields 各个子类的唯一差别只在“返回常量数据”的函数上。 -

+

-# 简化条件表达式 +# 八、简化条件表达式 -## 1. Decompose Conditional(分解条件表达式) +## 1. 分解条件表达式 + +> Decompose Conditional 对于一个复杂的条件语句,可以从 if、then、else 三个段落中分别提炼出独立函数。 @@ -676,7 +787,9 @@ if(notSummer(date)) else charge = summerCharge(quantity); ``` -## 2. Consolidate Conditional Expression(合并条件表达式) +## 2. 合并条件表达式 + +> Consolidate Conditional Expression 有一系列条件测试,都得到相同结果。 @@ -697,7 +810,9 @@ double disabilityAmount(){ } ``` -## 3. Consolidate Duplicate Conditional Fragments (合并重复的条件片段) +## 3. 合并重复的条件片段 + +> Consolidate Duplicate Conditional Fragments 在条件表达式的每个分支上有着相同的一段代码。 @@ -722,13 +837,17 @@ if (isSpecialDeal()) { send(); ``` -## 4. Remove Control Flag(移除控制标记) +## 4. 移除控制标记 + +> Remove Control Flag 在一系列布尔表达式中,某个变量带有“控制标记”的作用。 -用 break语 句或 return 语句来取代控制标记。 +用 break 语 句或 return 语句来取代控制标记。 -## 5. Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件表达式) +## 5. 以卫语句取代嵌套条件表达式 + +> Replace Nested Conditional with Guard Clauses 如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回,这样的单独检查常常被称为“卫语句”(guard clauses)。 @@ -758,7 +877,9 @@ double getPayAmount() { }; ``` -## 6. Replace Conditional with Polymorphism (以多态取代条件表达式) +## 6. 以多态取代条件表达式 + +> Replace Conditional with Polymorphism 将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。需要先使用 Replace Type Code with Subclass 或 Replace Type Code with State/Strategy 来建立继承结果。 @@ -776,9 +897,12 @@ double getSpeed() { } ``` -

+

-## 7. Introduce Null Object(引入Null对象) + +## 7. 引入 Null 对象 + +> Introduce Null Object 将 null 值替换为 null 对象。这样做的好处在于,不需要询问对象是否为空,直接调用就行。 @@ -787,7 +911,9 @@ if (customer == null) plan = BillingPlan.basic(); else plan = customer.getPlan(); ``` -## 8. Introduce Assertion(引入断言) +## 8. 引入断言 + +> Introduce Assertion 以断言明确表现某种假设。断言只能用于开发过程中,产品代码中不会有断言。 @@ -805,21 +931,29 @@ double getExpenseLimit() { } ``` -# 简化函数调用 +# 九、简化函数调用 -## 1. Rename Method(函数改名) +## 1. 函数改名 + +> Rename Method 使函数名能解释函数的用途。 -## 2. Add Parameter(添加参数) +## 2. 添加参数 + +> Add Parameter 使函数不需要通过调用获得某个信息。 -## 3. Remove Parameter(移除参数) +## 3. 移除参数 + +> Remove Parameter 与 Add Parameter 相反,改用调用的方式来获得某个信息。 -## 4. Separate Query from Modifier(将查询函数和修改函数分离) +## 4. 将查询函数和修改函数分离 + +> Separate Query from Modifier 某个函数即返回对象状态值,又修改对象状态。 @@ -834,7 +968,9 @@ getTotalOutstanding(); setReadyForSummaries(); ``` -## 5. Parameterize Method(令函数携带参数) +## 5. 令函数携带参数 + +> Parameterize Method 若干函数做了类似的工作,但在函数本体中却包含了不同的值。 @@ -848,7 +984,9 @@ tenPercentRaise(); raise(percentage); ``` -## 6. Replace Parameter with Explicit Methods(以明确函数取代参数) +## 6. 以明确函数取代参数 + +> Replace Parameter with Explicit Methods 有一个函数,完全取决于参数值而采取不同行为。 @@ -877,7 +1015,9 @@ void setWidth(int arg){ } ``` -## 7. Preserve Whole Object(保持对象完整) +## 7. 保持对象完整 + +> Preserve Whole Object 从某个对象中取出若干值,将它们作为某一次函数调用时的参数。 @@ -893,7 +1033,9 @@ withinPlan = plan.withinRange(low,high); withinPlan = plan.withinRange(daysTempRange()); ``` -## 8. Replace Parameter with Methods(以函数取代参数) +## 8. 以函数取代参数 + +> Replace Parameter with Methods 对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。 @@ -910,33 +1052,43 @@ int basePrice = _quantity * _itemPrice; double finalPrice = discountedPrice (basePrice); ``` -## 9. Introduce Parameter Object(引入参数对象) +## 9. 引入参数对象 + +> Introduce Parameter Object 某些参数总是很自然地同时出现,这些参数就是 Data Clumps。 以一个对象取代这些参数。 -

+

-## 10. Remove Setting Method(移除设值函数) +## 10. 移除设值函数 + +> Remove Setting Method 类中的某个字段应该在对象创建时被设值,然后就不再改变。 去掉该字段的所有设值函数,并将该字段设为 final。 -## 11. Hide Method(隐藏函数) +## 11. 隐藏函数 + +> Hide Method 有一个函数,从来没有被其他任何类用到。 将这个函数修改为 private。 -## 12. Replace Constructor with Factory Method (以工厂函数取代构造函数) +## 12. 以工厂函数取代构造函数 + +> Replace Constructor with Factory Method 希望在创建对象时不仅仅是做简单的建构动作。 将构造函数替换为工厂函数。 -## 13. Encapsulate Downcast(封装向下转型) +## 13. 封装向下转型 + +> Encapsulate Downcast 某个函数返回的对象,需要由函数调用者执行向下转型(downcast)。 @@ -953,13 +1105,17 @@ Reading lastReading(){ } ``` -## 14. Replace Error Code with Exception (以异常取代错误码) +## 14. 以异常取代错误码 + +> Replace Error Code with Exception 某个函数返回一个特定的代码,用以表示某种错误情况。 改用异常,异常将普通程序和错误处理分开,使代码更容易理解。 -## 15. Replace Exception with Test(以测试取代异常) +## 15. 以测试取代异常 + +> Replace Exception with Test 面对一个调用者可以预先检查的条件,你抛出了一个异常。 @@ -980,21 +1136,27 @@ double getValueForPeriod(int periodNumber) { return values[periodNumber]; ``` -# 处理概括关系 +# 十、处理概括关系 -## 1. Pull Up Field(字段上移) +## 1. 字段上移 + +> Pull Up Field 两个子类拥有相同的字段。 将该字段移至超类。 -## 2. Pull Up Method(函数上移) +## 2. 函数上移 + +> Pull Up Method 有些函数,在各个子类中产生完全相同的结果。 将该函数移至超类。 -## 3. Pull Up Constructor Body(构造函数本体上移) +## 3. 构造函数本体上移 + +> Pull Up Constructor Body 你在各个子类中拥有一些构造函数,它们的本体几乎完全一致。 @@ -1017,55 +1179,73 @@ public Manager(String name, String id, int grade) { } ``` -## 4. Push Down Method(函数下移) +## 4. 函数下移 + +> Push Down Method 超类中的某个函数只与部分子类有关。 将这个函数移到相关的那些子类去。 -## 5. Push Down Field(字段下移) +## 5. 字段下移 + +> Push Down Field 超类中的某个字段只被部分子类用到。 将这个字段移到需要它的那些子类去。 -## 6. Extract Subclass(提炼子类) +## 6. 提炼子类 + +> Extract Subclass 类中的某些特性只被某些实例用到。 新建一个子类,将上面所说的那一部分特性移到子类中。 -## 7. Extract Superclass(提炼超类) +## 7. 提炼超类 + +> Extract Superclass 两个类有相似特性。 为这两个类建立一个超类,将相同特性移至超类。 -## 8. Extract Interface(提炼接口) +## 8. 提炼接口 + +> Extract Interface 若干客户使用类接口中的同一子集,或者两个类的接口有部分相同。 将相同的子集提炼到一个独立接口中。 -## 9. Collapse Hierarchy(折叠继承体系) +## 9. 折叠继承体系 + +> Collapse Hierarchy 超类和子类之间无太大区别。 将它们合为一体。 -## 10. Form Template Method(塑造模板函数) +## 10. 塑造模板函数 + +> Form Template Method 你有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同。 将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了。然后将原函数上移至超类。(模板方法模式) -## 11. Replace Inheritance with Delegation (以委托取代继承) +## 11. 以委托取代继承 + +> Replace Inheritance with Delegation 某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据。 在子类中新建一个字段用以保存超类,调整子类函数,令它改而委托超类,然后去掉两者之间的继承关系。 -## 12. Replace Delegation with Inheritance (以继承取代委托) +## 12. 以继承取代委托 + +> Replace Delegation with Inheritance 你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数。 diff --git a/notes/面向对象思想.md b/notes/面向对象思想.md index 1b88525f..b4e5f032 100644 --- a/notes/面向对象思想.md +++ b/notes/面向对象思想.md @@ -1,41 +1,23 @@ -* [设计原则](#设计原则) - * [1. S.O.L.I.D](#1-solid) - * [1.1 单一责任原则](#11-单一责任原则) - * [1.2 开放封闭原则](#12-开放封闭原则) - * [1.3 里氏替换原则](#13-里氏替换原则) - * [1.4 接口分离原则](#14-接口分离原则) - * [1.5 依赖倒置原则](#15-依赖倒置原则) - * [2. 其他常见原则](#2-其他常见原则) - * [2.1 迪米特法则](#21-迪米特法则) - * [2.2 合成复用原则](#22-合成复用原则) - * [2.3 共同封闭原则](#23-共同封闭原则) - * [2.4 稳定抽象原则](#24-稳定抽象原则) - * [2.5 稳定依赖原则](#25-稳定依赖原则) -* [三大特性](#三大特性) - * [1. 封装](#1-封装) - * [2. 继承](#2-继承) - * [3. 多态](#3-多态) -* [UML](#uml) - * [1. 类图](#1-类图) - * [1.1 继承相关](#11-继承相关) - * [1.2 整体和部分](#12-整体和部分) - * [1.3 相互联系](#13-相互联系) - * [2. 时序图](#2-时序图) - * [2.1 定义](#21-定义) - * [2.2 赤壁之战时序图](#22-赤壁之战时序图) - * [2.3 活动图、时序图之间的关系](#23-活动图时序图之间的关系) - * [2.4 类图与时序图的关系](#24-类图与时序图的关系) - * [2.5 时序图的组成](#25-时序图的组成) +* [一、设计原则](#一设计原则) + * [S.O.L.I.D](#solid) + * [其他常见原则](#其他常见原则) +* [二、三大特性](#二三大特性) + * [封装](#封装) + * [继承](#继承) + * [多态](#多态) +* [三、UML](#三uml) + * [类图](#类图) + * [时序图](#时序图) * [参考资料](#参考资料) -# 设计原则 +# 一、设计原则 -设计原则可以帮助我们避免那些糟糕的设计,这些原则被归纳在《敏捷软件开发:原则、模式与实践》这本书中。 +设计原则可以帮助我们避免那些糟糕的设计。 -## 1. S.O.L.I.D +## S.O.L.I.D | 简写 | 全拼 | 中文翻译 | | -- | -- | -- | @@ -45,40 +27,40 @@ | ISP | The Interface Segregation Principle | 接口分离原则 | | DIP | The Dependency Inversion Principle | 依赖倒置原则 | -### 1.1 单一责任原则 +### 1. 单一责任原则 -**修改一个类的原因应该只有一个。** +> 修改一个类的原因应该只有一个。 换句话说就是让一个类只负责一件事,当这个类需要做过多事情的时候,就需要分解这个类。 如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力。 -### 1.2 开放封闭原则 +### 2. 开放封闭原则 -**类应该对扩展开放,对修改关闭。** +> 类应该对扩展开放,对修改关闭。 扩展就是添加新功能的意思,因此该原则要求在添加新功能时不需要修改代码。 符合开闭原则最典型的设计模式是装饰者模式,它可以动态地将责任附加到对象上,而不用去修改类的代码。 -### 1.3 里氏替换原则 +### 3. 里氏替换原则 -**子类对象必须能够替换掉所有父类对象。** +> 子类对象必须能够替换掉所有父类对象。 继承是一种 IS-A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。 如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度。 -### 1.4 接口分离原则 +### 4. 接口分离原则 -**不应该强迫客户依赖于它们不用的方法。** +> 不应该强迫客户依赖于它们不用的方法。 因此使用多个专门的接口比使用单一的总接口总要好。 -### 1.5 依赖倒置原则 +### 5. 依赖倒置原则 -- **高层模块不应该依赖于低层模块,二者都应该依赖于抽象** -- **抽象不应该依赖于细节,细节应该依赖于抽象** +> 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。 +> 抽象不应该依赖于细节,细节应该依赖于抽象。 高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于底层模块,那么底层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。 @@ -88,41 +70,41 @@ - 任何类都不应该从具体类派生; - 任何方法都不应该覆写它的任何基类中的已经实现的方法。 -## 2. 其他常见原则 +## 其他常见原则 除了上述的经典原则,在实际开发中还有下面这些常见的设计原则。 | 简写 | 全拼 | 中文翻译 | | -- | -- | -- | -|LoD| The Law of Demeter | 迪米特法则 | +|LOD| The Law of Demeter | 迪米特法则 | |CRP| The Composite Reuse Principle | 合成复用原则 | |CCP| The Common Closure Principle | 共同封闭原则 | |SAP| The Stable Abstractions Principle | 稳定抽象原则 | |SDP| The Stable Dependencies Principle | 稳定依赖原则 | -### 2.1 迪米特法则 +### 1. 迪米特法则 迪米特法则又叫作最少知道原则(Least Knowledge Principle 简写LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。 -### 2.2 合成复用原则 +### 2. 合成复用原则 尽量使用对象组合,而不是继承来达到复用的目的。 -### 2.3 共同封闭原则 +### 3. 共同封闭原则 一起修改的类,应该组合在一起(同一个包里)。如果必须修改应用程序里的代码,我们希望所有的修改都发生在一个包里(修改关闭),而不是遍布在很多包里。 -### 2.4 稳定抽象原则 +### 4. 稳定抽象原则 最稳定的包应该是最抽象的包,不稳定的包应该是具体的包,即包的抽象程度跟它的稳定性成正比。 -### 2.5 稳定依赖原则 +### 5. 稳定依赖原则 包之间的依赖关系都应该是稳定方向依赖的,包要依赖的包要比自己更具有稳定性。 -# 三大特性 +# 二、三大特性 -## 1. 封装 +## 封装 利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。 @@ -160,7 +142,7 @@ public class Person { } ``` -## 2. 继承 +## 继承 继承实现了 **IS-A** 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。 @@ -172,7 +154,7 @@ Animal animal = new Cat(); 继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。 -## 3. 多态 +## 多态 多态分为编译时多态和运行时多态。编译时多态主要指方法的重载,运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。 @@ -215,61 +197,61 @@ public class Music { } ``` -# UML +# 三、UML -## 1. 类图 +## 类图 -### 1.1 继承相关 +### 1. 继承相关 继承有两种形式 : 泛化(Generalize)和实现(Realize),表现为 IS-A 关系。 -**泛化关系 (Generalize)** +#### 泛化关系 (Generalize) 从具体类中继承。

-**实现关系 (Realize)** +#### 实现关系 (Realize) 从抽象类或者接口中继承。

-### 1.2 整体和部分 +### 2. 整体和部分 -**聚合关系 (Aggregation)** +#### 聚合关系 (Aggregation) 表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。以下表示 B 由 A 组成:

-**组合关系 (Composition)** +#### 组合关系 (Composition) 和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。

-### 1.3 相互联系 +### 3. 相互联系 -**关联关系 (Association)** +#### 关联关系 (Association) 表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。

-**依赖关系 (Dependency)** +#### 依赖关系 (Dependency) 和关联关系不同的是 , 依赖关系是在运行过程中起作用的。一般依赖作为类的构造器或者方法的参数传入。双向依赖时一种不好的设计。

-## 2. 时序图 +## 时序图 -### 2.1 定义 +### 1. 定义 时序图描述了对象之间传递消息的时间顺序,它用来表示用例的行为顺序。它的主要作用是通过对象间的交互来描述用例(注意是对象),从而寻找类的操作。 -### 2.2 赤壁之战时序图 +### 2. 赤壁之战时序图 从虚线从上往下表示时间的推进。 @@ -301,19 +283,19 @@ public class 孙权 { } ``` -### 2.3 活动图、时序图之间的关系 +### 3. 活动图、时序图之间的关系 活动图示从用户的角度来描述用例; 时序图是从计算机的角度(对象间的交互)描述用例。 -### 2.4 类图与时序图的关系 +### 4. 类图与时序图的关系 类图描述系统的静态结构,时序图描述系统的动态行为。 -### 2.5 时序图的组成 +### 5. 时序图的组成 -**对象** +#### 对象 有三种表现形式 @@ -325,13 +307,13 @@ public class 孙权 { 2. 把初始化整个交互活动的对象(有时是一个参与者)放置在最左边。 -**生命线** +#### 生命线 生命线从对象的创建开始到对象销毁时终止

-**消息** +#### 消息 对象之间的交互式通过发送消息来实现的。 @@ -351,7 +333,7 @@ public class 孙权 { 4\. 返回消息,可选。 -**激活** +#### 激活 生命线上的方框表示激活状态,其它时间处于休眠状态。 diff --git a/other/download.md b/other/download.md index d7f6c5ef..96d0863d 100644 --- a/other/download.md +++ b/other/download.md @@ -1,5 +1,5 @@ -# 网络 +# 网络          @@ -21,7 +21,7 @@ # Redis -    +    # Java diff --git a/other/postface.md b/other/postface.md new file mode 100644 index 00000000..0a3918d9 --- /dev/null +++ b/other/postface.md @@ -0,0 +1,29 @@ +# 关于仓库 + +本仓库是笔者在准备 2018 年春招实习过程中的学习总结,内容以计算机书籍的学习笔记为主,在整理重点知识的同时会尽量保证知识的系统性。 + +# 关于贡献 + +因为大部分内容是笔者一个字一个字打上去的,所有难免会有一些笔误。如果发现,可以直接在相应的文档上编辑修改。 + +笔者能力有限,很多内容还不够完善。如果您希望和笔者一起完善这个仓库,可以在发表一个 Issue,表明您想要添加的内容,笔者会及时查看。 + +因为不打算将这个仓库做成一个大而全的面试宝典,因此只希望添加一些比较通用的基础知识,或者是和 Java 与分布式相关的内容,但是不添加 Java Web 相关的内容。 + +您也可以在 Issues 中发表一些关于改进本仓库的反馈意见。 + +# 关于上传 + +笔者在本地使用为知笔记软件进行书写,为了方便将本地笔记内容上传到 Github 上,实现了一整套自动化上传方案,包括文本文件的导出、提取图片、Markdown 文档转换、Git 同步。 + +进行 Markdown 文档转换的原因是 Github 使用的 GFM 不支持 MathJax 公式,也不支持 TOC 标记,因此需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。这里提供了笔者实现的 GFM 文档转换工具的下载:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。 + +# 关于排版 + +笔记内容按照 [中文文案排版指北](http://mazhuang.org/wiki/chinese-copywriting-guidelines/#%E4%B8%8D%E8%A6%81%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%9C%B0%E9%81%93%E7%9A%84%E7%BC%A9%E5%86%99) 进行排版,以保证内容的可读性。这里提供了笔者实现的中英混排文档在线工具:[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting),目前实现了加空格的功能,之后打算实现对英文专有名词提示首字母大写的功能。 + +不使用 `![]()` 这种方式来引用图片的原因是为了能够控制图片以合适的大小显示,并且在 GFM 中无法使用 `
![]()
` 让图片居中显示,只能使用 `
` 达到此目的。 + +# 关于转载 + +本仓库内容使用到的资料都会在最后面的参考资料中给出引用链接,希望您在使用本仓库的内容时也能给出相应的引用链接。 diff --git a/pics/00eda100-dba1-4ec2-9140-5fe5f3855951.jpg b/pics/00eda100-dba1-4ec2-9140-5fe5f3855951.jpg new file mode 100644 index 00000000..4b063372 Binary files /dev/null and b/pics/00eda100-dba1-4ec2-9140-5fe5f3855951.jpg differ diff --git a/pics/037c3a0b-332d-434d-a374-f343ef72c8e1.jpg b/pics/037c3a0b-332d-434d-a374-f343ef72c8e1.jpg new file mode 100644 index 00000000..68950556 Binary files /dev/null and b/pics/037c3a0b-332d-434d-a374-f343ef72c8e1.jpg differ diff --git a/pics/037f84c6-b470-42e0-a2d9-fd8cd7e96fa8.png b/pics/037f84c6-b470-42e0-a2d9-fd8cd7e96fa8.png new file mode 100644 index 00000000..11ca467d Binary files /dev/null and b/pics/037f84c6-b470-42e0-a2d9-fd8cd7e96fa8.png differ diff --git a/pics/04662fa2-d19b-4de3-a829-50acb7af75d7.png b/pics/04662fa2-d19b-4de3-a829-50acb7af75d7.png new file mode 100644 index 00000000..18c04f3b Binary files /dev/null and b/pics/04662fa2-d19b-4de3-a829-50acb7af75d7.png differ diff --git a/pics/051760ba-e658-401f-9a1c-15adcb405191.png b/pics/051760ba-e658-401f-9a1c-15adcb405191.png new file mode 100644 index 00000000..8c50f3be Binary files /dev/null and b/pics/051760ba-e658-401f-9a1c-15adcb405191.png differ diff --git a/pics/061c29ce-e2ed-425a-911e-56fbba1efce3.jpg b/pics/061c29ce-e2ed-425a-911e-56fbba1efce3.jpg new file mode 100644 index 00000000..29d32e72 Binary files /dev/null and b/pics/061c29ce-e2ed-425a-911e-56fbba1efce3.jpg differ diff --git a/pics/07717718-1230-4347-aa18-2041c315e670.jpg b/pics/07717718-1230-4347-aa18-2041c315e670.jpg new file mode 100644 index 00000000..456bd952 Binary files /dev/null and b/pics/07717718-1230-4347-aa18-2041c315e670.jpg differ diff --git a/pics/080f488c-75ef-49a8-a49d-78fa372ad422.png b/pics/080f488c-75ef-49a8-a49d-78fa372ad422.png new file mode 100644 index 00000000..c40ea2d2 Binary files /dev/null and b/pics/080f488c-75ef-49a8-a49d-78fa372ad422.png differ diff --git a/pics/0aaf4630-d2a2-4783-b3f7-a2b6a7dfc01b.jpg b/pics/0aaf4630-d2a2-4783-b3f7-a2b6a7dfc01b.jpg new file mode 100644 index 00000000..bee1bae5 Binary files /dev/null and b/pics/0aaf4630-d2a2-4783-b3f7-a2b6a7dfc01b.jpg differ diff --git a/pics/0b304499-0d7c-49cc-b784-3e7a805c9fba.jpg b/pics/0b304499-0d7c-49cc-b784-3e7a805c9fba.jpg new file mode 100644 index 00000000..2a77e0c3 Binary files /dev/null and b/pics/0b304499-0d7c-49cc-b784-3e7a805c9fba.jpg differ diff --git a/pics/0c723f4c-e13d-42f7-aeb2-1f160c7cc4b6.png b/pics/0c723f4c-e13d-42f7-aeb2-1f160c7cc4b6.png new file mode 100644 index 00000000..3252d0af Binary files /dev/null and b/pics/0c723f4c-e13d-42f7-aeb2-1f160c7cc4b6.png differ diff --git a/pics/0dd97d9e-7a38-460e-a2ac-3e4511145240.png b/pics/0dd97d9e-7a38-460e-a2ac-3e4511145240.png new file mode 100644 index 00000000..8a3e23d0 Binary files /dev/null and b/pics/0dd97d9e-7a38-460e-a2ac-3e4511145240.png differ diff --git a/pics/0ee0f61b-c782-441e-bf34-665650198ae0.jpg b/pics/0ee0f61b-c782-441e-bf34-665650198ae0.jpg new file mode 100644 index 00000000..f3e7163b Binary files /dev/null and b/pics/0ee0f61b-c782-441e-bf34-665650198ae0.jpg differ diff --git a/pics/0f86eb11-3724-48de-9f27-499dfc7e96f1.png b/pics/0f86eb11-3724-48de-9f27-499dfc7e96f1.png new file mode 100644 index 00000000..defa899e Binary files /dev/null and b/pics/0f86eb11-3724-48de-9f27-499dfc7e96f1.png differ diff --git a/pics/10.gif b/pics/10.gif new file mode 100644 index 00000000..d52a911e Binary files /dev/null and b/pics/10.gif differ diff --git a/pics/11.gif b/pics/11.gif new file mode 100644 index 00000000..5d9c6f00 Binary files /dev/null and b/pics/11.gif differ diff --git a/pics/111521118015898.gif b/pics/111521118015898.gif new file mode 100644 index 00000000..5c31da1d Binary files /dev/null and b/pics/111521118015898.gif differ diff --git a/pics/111521118445538.gif b/pics/111521118445538.gif new file mode 100644 index 00000000..323d129c Binary files /dev/null and b/pics/111521118445538.gif differ diff --git a/pics/111521118483039.gif b/pics/111521118483039.gif new file mode 100644 index 00000000..a81124dd Binary files /dev/null and b/pics/111521118483039.gif differ diff --git a/pics/111521118640738.gif b/pics/111521118640738.gif new file mode 100644 index 00000000..7a7b05a9 Binary files /dev/null and b/pics/111521118640738.gif differ diff --git a/pics/111521119203347.gif b/pics/111521119203347.gif new file mode 100644 index 00000000..37cdb5a5 Binary files /dev/null and b/pics/111521119203347.gif differ diff --git a/pics/111521119368714.gif b/pics/111521119368714.gif new file mode 100644 index 00000000..216c3033 Binary files /dev/null and b/pics/111521119368714.gif differ diff --git a/pics/12b458dd-526d-46e2-acca-cf3b501d580e.png b/pics/12b458dd-526d-46e2-acca-cf3b501d580e.png new file mode 100644 index 00000000..7f671c19 Binary files /dev/null and b/pics/12b458dd-526d-46e2-acca-cf3b501d580e.png differ diff --git a/pics/1582217a-ed46-4cac-811e-90d13a65163b.png b/pics/1582217a-ed46-4cac-811e-90d13a65163b.png new file mode 100644 index 00000000..c89551d1 Binary files /dev/null and b/pics/1582217a-ed46-4cac-811e-90d13a65163b.png differ diff --git a/pics/1984a822-da4e-4461-b826-55b798e2d419.jpg b/pics/1984a822-da4e-4461-b826-55b798e2d419.jpg new file mode 100644 index 00000000..04eda35a Binary files /dev/null and b/pics/1984a822-da4e-4461-b826-55b798e2d419.jpg differ diff --git a/pics/1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg b/pics/1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg new file mode 100644 index 00000000..4809984f Binary files /dev/null and b/pics/1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg differ diff --git a/pics/1c012d74-6b9d-4f25-a016-7ad4f1f1521898780376.png b/pics/1c012d74-6b9d-4f25-a016-7ad4f1f1521898780376.png new file mode 100644 index 00000000..0c959d2a Binary files /dev/null and b/pics/1c012d74-6b9d-4f25-a016-7ad4f1f1521898780376.png differ diff --git a/pics/1c012d74-6b9d-4f25-a016-7ad4f1fdeee1.png b/pics/1c012d74-6b9d-4f25-a016-7ad4f1fdeee1.png new file mode 100644 index 00000000..0c959d2a Binary files /dev/null and b/pics/1c012d74-6b9d-4f25-a016-7ad4f1fdeee1.png differ diff --git a/pics/1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg b/pics/1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg new file mode 100644 index 00000000..e4f0f3ac Binary files /dev/null and b/pics/1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg differ diff --git a/pics/20150928140509757.png b/pics/20150928140509757.png new file mode 100644 index 00000000..60684377 Binary files /dev/null and b/pics/20150928140509757.png differ diff --git a/pics/202cd47c-ef2f-4c0e-a511-53359553387d.png b/pics/202cd47c-ef2f-4c0e-a511-53359553387d.png new file mode 100644 index 00000000..25e0d877 Binary files /dev/null and b/pics/202cd47c-ef2f-4c0e-a511-53359553387d.png differ diff --git a/pics/20368ec9-972e-4d6a-8050-3948334bcda0.jpg b/pics/20368ec9-972e-4d6a-8050-3948334bcda0.jpg new file mode 100644 index 00000000..c6067730 Binary files /dev/null and b/pics/20368ec9-972e-4d6a-8050-3948334bcda0.jpg differ diff --git a/pics/2093ccfa-e560-44f3-84c7-487d66451708.png b/pics/2093ccfa-e560-44f3-84c7-487d66451708.png new file mode 100644 index 00000000..9ddb56f2 Binary files /dev/null and b/pics/2093ccfa-e560-44f3-84c7-487d66451708.png differ diff --git a/pics/20a9ec8c-0c19-4196-96f0-b4a0ea43075d.jpg b/pics/20a9ec8c-0c19-4196-96f0-b4a0ea43075d.jpg new file mode 100644 index 00000000..6b1a6900 Binary files /dev/null and b/pics/20a9ec8c-0c19-4196-96f0-b4a0ea43075d.jpg differ diff --git a/pics/211c60d4-75ca-4acd-8a4f-171458ed58b4.jpg b/pics/211c60d4-75ca-4acd-8a4f-171458ed58b4.jpg new file mode 100644 index 00000000..efb47ffc Binary files /dev/null and b/pics/211c60d4-75ca-4acd-8a4f-171458ed58b4.jpg differ diff --git a/pics/2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg b/pics/2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg new file mode 100644 index 00000000..f9a9489b Binary files /dev/null and b/pics/2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg differ diff --git a/pics/299a8cd3-7cfa-45d0-8821-4aa577de4692.png b/pics/299a8cd3-7cfa-45d0-8821-4aa577de4692.png new file mode 100644 index 00000000..23fe5d4a Binary files /dev/null and b/pics/299a8cd3-7cfa-45d0-8821-4aa577de4692.png differ diff --git a/pics/2_14_microkernelArchitecture.jpg b/pics/2_14_microkernelArchitecture.jpg new file mode 100644 index 00000000..21c2a58f Binary files /dev/null and b/pics/2_14_microkernelArchitecture.jpg differ diff --git a/pics/2bf2fd8f-5ade-48ba-a2b3-74195ac77c4b.png b/pics/2bf2fd8f-5ade-48ba-a2b3-74195ac77c4b.png new file mode 100644 index 00000000..590a4299 Binary files /dev/null and b/pics/2bf2fd8f-5ade-48ba-a2b3-74195ac77c4b.png differ diff --git a/pics/2c4556e4-0751-4377-ab08-e7b89d697ca7.png b/pics/2c4556e4-0751-4377-ab08-e7b89d697ca7.png new file mode 100644 index 00000000..c7f87138 Binary files /dev/null and b/pics/2c4556e4-0751-4377-ab08-e7b89d697ca7.png differ diff --git a/pics/31d99967-1171-448e-8531-bccf5c14cffe.jpg b/pics/31d99967-1171-448e-8531-bccf5c14cffe.jpg new file mode 100644 index 00000000..61e00649 Binary files /dev/null and b/pics/31d99967-1171-448e-8531-bccf5c14cffe.jpg differ diff --git a/pics/350048d6-20f5-4ca9-8452-3957a09ef3af.png b/pics/350048d6-20f5-4ca9-8452-3957a09ef3af.png new file mode 100644 index 00000000..df34cbdb Binary files /dev/null and b/pics/350048d6-20f5-4ca9-8452-3957a09ef3af.png differ diff --git a/pics/35b0caf8-6f34-49db-93ed-d505e9eb3d99.png b/pics/35b0caf8-6f34-49db-93ed-d505e9eb3d99.png new file mode 100644 index 00000000..71505568 Binary files /dev/null and b/pics/35b0caf8-6f34-49db-93ed-d505e9eb3d99.png differ diff --git a/pics/392fb173-9713-433c-b37b-ea63ba76eae4.png b/pics/392fb173-9713-433c-b37b-ea63ba76eae4.png new file mode 100644 index 00000000..6f3bbdbb Binary files /dev/null and b/pics/392fb173-9713-433c-b37b-ea63ba76eae4.png differ diff --git a/pics/3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg b/pics/3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg new file mode 100644 index 00000000..f7e9f145 Binary files /dev/null and b/pics/3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg differ diff --git a/pics/3c99f603-c471-49df-86df-0a0c750f1948.png b/pics/3c99f603-c471-49df-86df-0a0c750f1948.png new file mode 100644 index 00000000..3a911bef Binary files /dev/null and b/pics/3c99f603-c471-49df-86df-0a0c750f1948.png differ diff --git a/pics/3cd630ea-017c-488d-ad1d-732b4efeddf5.png b/pics/3cd630ea-017c-488d-ad1d-732b4efeddf5.png new file mode 100644 index 00000000..9dc77733 Binary files /dev/null and b/pics/3cd630ea-017c-488d-ad1d-732b4efeddf5.png differ diff --git a/pics/3e87de44-1c69-4365-8139-b22e8d4be347.png b/pics/3e87de44-1c69-4365-8139-b22e8d4be347.png new file mode 100644 index 00000000..47753730 Binary files /dev/null and b/pics/3e87de44-1c69-4365-8139-b22e8d4be347.png differ diff --git a/pics/3efca49f-eecf-41fc-83aa-6a4a95e025ea.png b/pics/3efca49f-eecf-41fc-83aa-6a4a95e025ea.png new file mode 100644 index 00000000..94f728b5 Binary files /dev/null and b/pics/3efca49f-eecf-41fc-83aa-6a4a95e025ea.png differ diff --git a/pics/3f5bba4b-7813-4aea-b578-970c7e3f6bf3.jpg b/pics/3f5bba4b-7813-4aea-b578-970c7e3f6bf3.jpg new file mode 100644 index 00000000..c9702daf Binary files /dev/null and b/pics/3f5bba4b-7813-4aea-b578-970c7e3f6bf3.jpg differ diff --git a/pics/40fe0669-6ac2-46f1-8666-a03785ad7412.png b/pics/40fe0669-6ac2-46f1-8666-a03785ad7412.png new file mode 100644 index 00000000..aa3a94d6 Binary files /dev/null and b/pics/40fe0669-6ac2-46f1-8666-a03785ad7412.png differ diff --git a/pics/42e17a80-b9fc-42a2-9ba8-68364fae3710.jpg b/pics/42e17a80-b9fc-42a2-9ba8-68364fae3710.jpg new file mode 100644 index 00000000..34c83548 Binary files /dev/null and b/pics/42e17a80-b9fc-42a2-9ba8-68364fae3710.jpg differ diff --git a/pics/44e4a7ab-215c-41a1-8e34-f55f6c09e517.jpg b/pics/44e4a7ab-215c-41a1-8e34-f55f6c09e517.jpg new file mode 100644 index 00000000..a14ecacf Binary files /dev/null and b/pics/44e4a7ab-215c-41a1-8e34-f55f6c09e517.jpg differ diff --git a/pics/44edefb7-4b58-4519-b8ee-4aca01697b78.jpg b/pics/44edefb7-4b58-4519-b8ee-4aca01697b78.jpg new file mode 100644 index 00000000..32d0f3d2 Binary files /dev/null and b/pics/44edefb7-4b58-4519-b8ee-4aca01697b78.jpg differ diff --git a/pics/47d98583-8bb0-45cc-812d-47eefa0a4a40.jpg b/pics/47d98583-8bb0-45cc-812d-47eefa0a4a40.jpg new file mode 100644 index 00000000..f6ddd2c2 Binary files /dev/null and b/pics/47d98583-8bb0-45cc-812d-47eefa0a4a40.jpg differ diff --git a/pics/4e3faf22-fa80-445e-a57d-594c37bb76e7.png b/pics/4e3faf22-fa80-445e-a57d-594c37bb76e7.png new file mode 100644 index 00000000..3e188a9f Binary files /dev/null and b/pics/4e3faf22-fa80-445e-a57d-594c37bb76e7.png differ diff --git a/pics/4f4deaf4-8487-4de2-9d62-5ad017ee9589.png b/pics/4f4deaf4-8487-4de2-9d62-5ad017ee9589.png new file mode 100644 index 00000000..d1589cd8 Binary files /dev/null and b/pics/4f4deaf4-8487-4de2-9d62-5ad017ee9589.png differ diff --git a/pics/4ff7eacd-0081-452e-9686-60a81e11bf73.jpg b/pics/4ff7eacd-0081-452e-9686-60a81e11bf73.jpg new file mode 100644 index 00000000..1a1a7871 Binary files /dev/null and b/pics/4ff7eacd-0081-452e-9686-60a81e11bf73.jpg differ diff --git a/pics/5942debd-fc00-477a-b390-7c5692cc8070.jpg b/pics/5942debd-fc00-477a-b390-7c5692cc8070.jpg new file mode 100644 index 00000000..62b39e4b Binary files /dev/null and b/pics/5942debd-fc00-477a-b390-7c5692cc8070.jpg differ diff --git a/pics/5bbb64ff-bb9a-473e-ab53-8a26a394f813.jpg b/pics/5bbb64ff-bb9a-473e-ab53-8a26a394f813.jpg new file mode 100644 index 00000000..f5c7b17d Binary files /dev/null and b/pics/5bbb64ff-bb9a-473e-ab53-8a26a394f813.jpg differ diff --git a/pics/5c0bb285-b917-446b-84a2-9810ee40d041.png b/pics/5c0bb285-b917-446b-84a2-9810ee40d041.png new file mode 100644 index 00000000..a064cc93 Binary files /dev/null and b/pics/5c0bb285-b917-446b-84a2-9810ee40d041.png differ diff --git a/pics/5c0bb285-b917-446b-84a2-9810ee41521898714517.png b/pics/5c0bb285-b917-446b-84a2-9810ee41521898714517.png new file mode 100644 index 00000000..a064cc93 Binary files /dev/null and b/pics/5c0bb285-b917-446b-84a2-9810ee41521898714517.png differ diff --git a/pics/5e8493be-72cc-4a76-a68f-4852eacb2811.png b/pics/5e8493be-72cc-4a76-a68f-4852eacb2811.png new file mode 100644 index 00000000..5404e6d2 Binary files /dev/null and b/pics/5e8493be-72cc-4a76-a68f-4852eacb2811.png differ diff --git a/pics/5e9b10f3-9504-4483-9667-d4770adebf9f.png b/pics/5e9b10f3-9504-4483-9667-d4770adebf9f.png new file mode 100644 index 00000000..4ac499de Binary files /dev/null and b/pics/5e9b10f3-9504-4483-9667-d4770adebf9f.png differ diff --git a/pics/5f5ef0b6-98ea-497c-a007-f6c55288eab1.png b/pics/5f5ef0b6-98ea-497c-a007-f6c55288eab1.png new file mode 100644 index 00000000..a3ea0a24 Binary files /dev/null and b/pics/5f5ef0b6-98ea-497c-a007-f6c55288eab1.png differ diff --git a/pics/64b70b59-5bae-4dd2-addd-7f687ea5b64b.png b/pics/64b70b59-5bae-4dd2-addd-7f687ea5b64b.png new file mode 100644 index 00000000..f21a3bc5 Binary files /dev/null and b/pics/64b70b59-5bae-4dd2-addd-7f687ea5b64b.png differ diff --git a/pics/654acfed-a6a5-4fc7-8f40-3fdcae57bae8.jpg b/pics/654acfed-a6a5-4fc7-8f40-3fdcae57bae8.jpg new file mode 100644 index 00000000..ed98e7e6 Binary files /dev/null and b/pics/654acfed-a6a5-4fc7-8f40-3fdcae57bae8.jpg differ diff --git a/pics/691e8da5-fa65-4ee0-a4a9-bd9adba1521899256460.jpg b/pics/691e8da5-fa65-4ee0-a4a9-bd9adba1521899256460.jpg new file mode 100644 index 00000000..e65a4083 Binary files /dev/null and b/pics/691e8da5-fa65-4ee0-a4a9-bd9adba1521899256460.jpg differ diff --git a/pics/691e8da5-fa65-4ee0-a4a9-bd9adba945ff.jpg b/pics/691e8da5-fa65-4ee0-a4a9-bd9adba945ff.jpg new file mode 100644 index 00000000..e65a4083 Binary files /dev/null and b/pics/691e8da5-fa65-4ee0-a4a9-bd9adba945ff.jpg differ diff --git a/pics/699b4f96-d63f-46ea-a581-2b3d95eceb6a.jpg b/pics/699b4f96-d63f-46ea-a581-2b3d95eceb6a.jpg new file mode 100644 index 00000000..313b0f92 Binary files /dev/null and b/pics/699b4f96-d63f-46ea-a581-2b3d95eceb6a.jpg differ diff --git a/pics/6afa9796-af1a-4495-9f02-63349ab68a19.png b/pics/6afa9796-af1a-4495-9f02-63349ab68a19.png new file mode 100644 index 00000000..ee2ee55e Binary files /dev/null and b/pics/6afa9796-af1a-4495-9f02-63349ab68a19.png differ diff --git a/pics/6e874d3e-9999-4d14-b6c9-fc7776c7ce30.png b/pics/6e874d3e-9999-4d14-b6c9-fc7776c7ce30.png new file mode 100644 index 00000000..6946dc22 Binary files /dev/null and b/pics/6e874d3e-9999-4d14-b6c9-fc7776c7ce30.png differ diff --git a/pics/6e8f16d1-7dea-4331-aea0-3dd739db00a4.png b/pics/6e8f16d1-7dea-4331-aea0-3dd739db00a4.png new file mode 100644 index 00000000..2bc0a265 Binary files /dev/null and b/pics/6e8f16d1-7dea-4331-aea0-3dd739db00a4.png differ diff --git a/pics/6fea6dd4-9232-4df8-abdf-f1b528a18b17.jpg b/pics/6fea6dd4-9232-4df8-abdf-f1b528a18b17.jpg new file mode 100644 index 00000000..99c96910 Binary files /dev/null and b/pics/6fea6dd4-9232-4df8-abdf-f1b528a18b17.jpg differ diff --git a/pics/7.gif b/pics/7.gif new file mode 100644 index 00000000..fad88513 Binary files /dev/null and b/pics/7.gif differ diff --git a/pics/76a49594323247f21c9b3a69945445ee.png b/pics/76a49594323247f21c9b3a69945445ee.png new file mode 100644 index 00000000..788ba0b1 Binary files /dev/null and b/pics/76a49594323247f21c9b3a69945445ee.png differ diff --git a/pics/76dc7769-1aac-4888-9bea-064f1caa8e77.jpg b/pics/76dc7769-1aac-4888-9bea-064f1caa8e77.jpg new file mode 100644 index 00000000..642aba6b Binary files /dev/null and b/pics/76dc7769-1aac-4888-9bea-064f1caa8e77.jpg differ diff --git a/pics/7779232-1e8ed39548081a1f.png b/pics/7779232-1e8ed39548081a1f.png new file mode 100644 index 00000000..95e9027e Binary files /dev/null and b/pics/7779232-1e8ed39548081a1f.png differ diff --git a/pics/7779232-96822582feb08651.png b/pics/7779232-96822582feb08651.png new file mode 100644 index 00000000..d45ba85c Binary files /dev/null and b/pics/7779232-96822582feb08651.png differ diff --git a/pics/78570a06-0781-4f9d-9093-70a87111521898809910.jpg b/pics/78570a06-0781-4f9d-9093-70a87111521898809910.jpg new file mode 100644 index 00000000..2aa55f8f Binary files /dev/null and b/pics/78570a06-0781-4f9d-9093-70a87111521898809910.jpg differ diff --git a/pics/78570a06-0781-4f9d-9093-70a8711785b5.jpg b/pics/78570a06-0781-4f9d-9093-70a8711785b5.jpg new file mode 100644 index 00000000..2aa55f8f Binary files /dev/null and b/pics/78570a06-0781-4f9d-9093-70a8711785b5.jpg differ diff --git a/pics/7be0abf9-687c-4451-becd-626b0be7ec22.jpg b/pics/7be0abf9-687c-4451-becd-626b0be7ec22.jpg new file mode 100644 index 00000000..002f50ac Binary files /dev/null and b/pics/7be0abf9-687c-4451-becd-626b0be7ec22.jpg differ diff --git a/pics/7ca30d08-f7fa-4e39-b386-93c70631a900.png b/pics/7ca30d08-f7fa-4e39-b386-93c70631a900.png new file mode 100644 index 00000000..dd852349 Binary files /dev/null and b/pics/7ca30d08-f7fa-4e39-b386-93c70631a900.png differ diff --git a/pics/7e0b2961-2ffe-4a24-bef1-1b07a78af268.jpg b/pics/7e0b2961-2ffe-4a24-bef1-1b07a78af268.jpg new file mode 100644 index 00000000..fcc0e3e3 Binary files /dev/null and b/pics/7e0b2961-2ffe-4a24-bef1-1b07a78af268.jpg differ diff --git a/pics/7e9d0ef2-acd8-44c0-a76b-f0d2f5e76738.png b/pics/7e9d0ef2-acd8-44c0-a76b-f0d2f5e76738.png new file mode 100644 index 00000000..3e5b9448 Binary files /dev/null and b/pics/7e9d0ef2-acd8-44c0-a76b-f0d2f5e76738.png differ diff --git a/pics/7f642a65-b167-4c8f-b382-8322c6322b2c.jpg b/pics/7f642a65-b167-4c8f-b382-8322c6322b2c.jpg new file mode 100644 index 00000000..27751a37 Binary files /dev/null and b/pics/7f642a65-b167-4c8f-b382-8322c6322b2c.jpg differ diff --git a/pics/7f9a9342-1491-436d-bdd2-0fa94eb0e4f1.png b/pics/7f9a9342-1491-436d-bdd2-0fa94eb0e4f1.png new file mode 100644 index 00000000..0c7f97e9 Binary files /dev/null and b/pics/7f9a9342-1491-436d-bdd2-0fa94eb0e4f1.png differ diff --git a/pics/81eb9879-40f2-421a-87de-2b953cfe8c32.png b/pics/81eb9879-40f2-421a-87de-2b953cfe8c32.png new file mode 100644 index 00000000..d47cadb5 Binary files /dev/null and b/pics/81eb9879-40f2-421a-87de-2b953cfe8c32.png differ diff --git a/pics/8459a13f-b0b8-4387-85a9-2525482bc25a.png b/pics/8459a13f-b0b8-4387-85a9-2525482bc25a.png new file mode 100644 index 00000000..3735f081 Binary files /dev/null and b/pics/8459a13f-b0b8-4387-85a9-2525482bc25a.png differ diff --git a/pics/867abc3c-8403-43ef-8847-c1fea32996a5.png b/pics/867abc3c-8403-43ef-8847-c1fea32996a5.png new file mode 100644 index 00000000..e4cf4731 Binary files /dev/null and b/pics/867abc3c-8403-43ef-8847-c1fea32996a5.png differ diff --git a/pics/88ae5997-50ab-4204-891f-88e212ba892e.png b/pics/88ae5997-50ab-4204-891f-88e212ba892e.png new file mode 100644 index 00000000..d17ca4c6 Binary files /dev/null and b/pics/88ae5997-50ab-4204-891f-88e212ba892e.png differ diff --git a/pics/8a116e69-3d57-4987-a215-0197dd044a14.png b/pics/8a116e69-3d57-4987-a215-0197dd044a14.png new file mode 100644 index 00000000..636459da Binary files /dev/null and b/pics/8a116e69-3d57-4987-a215-0197dd044a14.png differ diff --git a/pics/8adb2591-d3f1-4632-84cb-823fb9c5eb09.jpg b/pics/8adb2591-d3f1-4632-84cb-823fb9c5eb09.jpg new file mode 100644 index 00000000..9083d22b Binary files /dev/null and b/pics/8adb2591-d3f1-4632-84cb-823fb9c5eb09.jpg differ diff --git a/pics/8d13ee52-e881-41eb-ad70-678c411f2718.png b/pics/8d13ee52-e881-41eb-ad70-678c411f2718.png new file mode 100644 index 00000000..90bbe673 Binary files /dev/null and b/pics/8d13ee52-e881-41eb-ad70-678c411f2718.png differ diff --git a/pics/8f64e9c5-7682-4feb-9312-dea09514e160.jpg b/pics/8f64e9c5-7682-4feb-9312-dea09514e160.jpg new file mode 100644 index 00000000..6f7e9e35 Binary files /dev/null and b/pics/8f64e9c5-7682-4feb-9312-dea09514e160.jpg differ diff --git a/pics/9.gif b/pics/9.gif new file mode 100644 index 00000000..f81aa264 Binary files /dev/null and b/pics/9.gif differ diff --git a/pics/91ef04e4-923a-4277-99c0-6be4ce81e5ac.jpg b/pics/91ef04e4-923a-4277-99c0-6be4ce81e5ac.jpg new file mode 100644 index 00000000..f4de7074 Binary files /dev/null and b/pics/91ef04e4-923a-4277-99c0-6be4ce81e5ac.jpg differ diff --git a/pics/9208563b-014d-4745-aa1c-492c9f7f7a7f.jpg b/pics/9208563b-014d-4745-aa1c-492c9f7f7a7f.jpg new file mode 100644 index 00000000..dadacb8a Binary files /dev/null and b/pics/9208563b-014d-4745-aa1c-492c9f7f7a7f.jpg differ diff --git a/pics/931e112e-97d3-4a47-ac64-a86d70844e58.png b/pics/931e112e-97d3-4a47-ac64-a86d70844e58.png new file mode 100644 index 00000000..b6be65ec Binary files /dev/null and b/pics/931e112e-97d3-4a47-ac64-a86d70844e58.png differ diff --git a/pics/93a28704-6401-4671-9758-051fadfbeb47.jpg b/pics/93a28704-6401-4671-9758-051fadfbeb47.jpg new file mode 100644 index 00000000..15f5f7cc Binary files /dev/null and b/pics/93a28704-6401-4671-9758-051fadfbeb47.jpg differ diff --git a/pics/9653b0c6-4232-4299-9f5c-79a616abafb8.png b/pics/9653b0c6-4232-4299-9f5c-79a616abafb8.png new file mode 100644 index 00000000..0a04997f Binary files /dev/null and b/pics/9653b0c6-4232-4299-9f5c-79a616abafb8.png differ diff --git a/pics/96b63e13-e2d8-4ddb-9aa1-a38959ca96e5.jpg b/pics/96b63e13-e2d8-4ddb-9aa1-a38959ca96e5.jpg new file mode 100644 index 00000000..3dbe14f1 Binary files /dev/null and b/pics/96b63e13-e2d8-4ddb-9aa1-a38959ca96e5.jpg differ diff --git a/pics/98fb8a54-e916-440e-ab4f-73b2955838c9.jpg b/pics/98fb8a54-e916-440e-ab4f-73b2955838c9.jpg new file mode 100644 index 00000000..4919ca2c Binary files /dev/null and b/pics/98fb8a54-e916-440e-ab4f-73b2955838c9.jpg differ diff --git a/pics/9b829410-86c4-40aa-ba8d-9e8e26c0eeb8.jpg b/pics/9b829410-86c4-40aa-ba8d-9e8e26c0eeb8.jpg new file mode 100644 index 00000000..470580c3 Binary files /dev/null and b/pics/9b829410-86c4-40aa-ba8d-9e8e26c0eeb8.jpg differ diff --git a/pics/CLone_20_281_29.png b/pics/CLone_20_281_29.png new file mode 100644 index 00000000..8b5b9308 Binary files /dev/null and b/pics/CLone_20_281_29.png differ diff --git a/pics/ClienteServidorSockets.jpg b/pics/ClienteServidorSockets.jpg new file mode 100644 index 00000000..18976fb3 Binary files /dev/null and b/pics/ClienteServidorSockets.jpg differ diff --git a/pics/ClienteServidorSockets1521731145260.jpg b/pics/ClienteServidorSockets1521731145260.jpg new file mode 100644 index 00000000..18976fb3 Binary files /dev/null and b/pics/ClienteServidorSockets1521731145260.jpg differ diff --git a/pics/DP-Decorator-java.io.png b/pics/DP-Decorator-java.io.png new file mode 100644 index 00000000..897a510b Binary files /dev/null and b/pics/DP-Decorator-java.io.png differ diff --git a/pics/Iterator-1.jpg b/pics/Iterator-1.jpg new file mode 100644 index 00000000..f9f818c6 Binary files /dev/null and b/pics/Iterator-1.jpg differ diff --git a/pics/ProcessState.png b/pics/ProcessState.png new file mode 100644 index 00000000..39269579 Binary files /dev/null and b/pics/ProcessState.png differ diff --git a/pics/ZnJvbT1jc2RuJnVybD15VkdkdVYyUXZ.jpg b/pics/ZnJvbT1jc2RuJnVybD15VkdkdVYyUXZ.jpg new file mode 100644 index 00000000..3027986e Binary files /dev/null and b/pics/ZnJvbT1jc2RuJnVybD15VkdkdVYyUXZ.jpg differ diff --git a/pics/a259182b-91b7-4b63-98c9-9d9cc6e199d4.png b/pics/a259182b-91b7-4b63-98c9-9d9cc6e199d4.png new file mode 100644 index 00000000..42be8f02 Binary files /dev/null and b/pics/a259182b-91b7-4b63-98c9-9d9cc6e199d4.png differ diff --git a/pics/a451b523-7e24-4fae-8e35-c46b14beed68.png b/pics/a451b523-7e24-4fae-8e35-c46b14beed68.png new file mode 100644 index 00000000..45ca0ed2 Binary files /dev/null and b/pics/a451b523-7e24-4fae-8e35-c46b14beed68.png differ diff --git a/pics/a46cf05d-e665-4937-a939-a3ab783bc8ee.png b/pics/a46cf05d-e665-4937-a939-a3ab783bc8ee.png new file mode 100644 index 00000000..f8a0bb48 Binary files /dev/null and b/pics/a46cf05d-e665-4937-a939-a3ab783bc8ee.png differ diff --git a/pics/a574c5c8-142d-4f6c-8fd7-8ca9b16b7546.png b/pics/a574c5c8-142d-4f6c-8fd7-8ca9b16b7546.png new file mode 100644 index 00000000..0b90bef5 Binary files /dev/null and b/pics/a574c5c8-142d-4f6c-8fd7-8ca9b16b7546.png differ diff --git a/pics/a6ac2b08-3861-4e85-baa8-382287bfee9f.png b/pics/a6ac2b08-3861-4e85-baa8-382287bfee9f.png new file mode 100644 index 00000000..26b0bd03 Binary files /dev/null and b/pics/a6ac2b08-3861-4e85-baa8-382287bfee9f.png differ diff --git a/pics/a9098783-c24a-45b2-a226-725a59b6768e.png b/pics/a9098783-c24a-45b2-a226-725a59b6768e.png new file mode 100644 index 00000000..a369861d Binary files /dev/null and b/pics/a9098783-c24a-45b2-a226-725a59b6768e.png differ diff --git a/pics/a9339620-4689-414f-8e26-19821039614a.jpg b/pics/a9339620-4689-414f-8e26-19821039614a.jpg new file mode 100644 index 00000000..a5c1634d Binary files /dev/null and b/pics/a9339620-4689-414f-8e26-19821039614a.jpg differ diff --git a/pics/aa202729-769d-4d2a-b103-f45b412ceaf4.jpg b/pics/aa202729-769d-4d2a-b103-f45b412ceaf4.jpg new file mode 100644 index 00000000..a0c32b7a Binary files /dev/null and b/pics/aa202729-769d-4d2a-b103-f45b412ceaf4.jpg differ diff --git a/pics/aa62b91f-3540-4a28-8ea9-045f62ab3bcc.png b/pics/aa62b91f-3540-4a28-8ea9-045f62ab3bcc.png new file mode 100644 index 00000000..ddbc1204 Binary files /dev/null and b/pics/aa62b91f-3540-4a28-8ea9-045f62ab3bcc.png differ diff --git a/pics/aaa40d0a-0aff-4161-adde-8d7bf56dc4b9.jpg b/pics/aaa40d0a-0aff-4161-adde-8d7bf56dc4b9.jpg new file mode 100644 index 00000000..f34d7ec5 Binary files /dev/null and b/pics/aaa40d0a-0aff-4161-adde-8d7bf56dc4b9.jpg differ diff --git a/pics/ae1f3f27-cb47-436d-b8a2-185618851b57.png b/pics/ae1f3f27-cb47-436d-b8a2-185618851b57.png new file mode 100644 index 00000000..4e0f5c93 Binary files /dev/null and b/pics/ae1f3f27-cb47-436d-b8a2-185618851b57.png differ diff --git a/pics/b0e8ef47-2f23-4379-8c64-10d5cb44d438.jpg b/pics/b0e8ef47-2f23-4379-8c64-10d5cb44d438.jpg new file mode 100644 index 00000000..cb17fdcb Binary files /dev/null and b/pics/b0e8ef47-2f23-4379-8c64-10d5cb44d438.jpg differ diff --git a/pics/b15ed62e-b955-44ac-b5cb-6fa7a16c79b5.png b/pics/b15ed62e-b955-44ac-b5cb-6fa7a16c79b5.png new file mode 100644 index 00000000..2bb67d95 Binary files /dev/null and b/pics/b15ed62e-b955-44ac-b5cb-6fa7a16c79b5.png differ diff --git a/pics/b396d726-b75f-4a32-89a2-03a7b6e19f6f.jpg b/pics/b396d726-b75f-4a32-89a2-03a7b6e19f6f.jpg new file mode 100644 index 00000000..a3e1a656 Binary files /dev/null and b/pics/b396d726-b75f-4a32-89a2-03a7b6e19f6f.jpg differ diff --git a/pics/b43437dd-16b6-4023-bdda-d6f7d6db762e.png b/pics/b43437dd-16b6-4023-bdda-d6f7d6db762e.png new file mode 100644 index 00000000..5dcc9708 Binary files /dev/null and b/pics/b43437dd-16b6-4023-bdda-d6f7d6db762e.png differ diff --git a/pics/b4b29aa9-dd2c-467b-b75f-ca6541cb25b5.jpg b/pics/b4b29aa9-dd2c-467b-b75f-ca6541cb25b5.jpg new file mode 100644 index 00000000..15f6fce8 Binary files /dev/null and b/pics/b4b29aa9-dd2c-467b-b75f-ca6541cb25b5.jpg differ diff --git a/pics/b5fed547-a989-4ead-81d5-ea72660faf99.png b/pics/b5fed547-a989-4ead-81d5-ea72660faf99.png new file mode 100644 index 00000000..cb04f7cb Binary files /dev/null and b/pics/b5fed547-a989-4ead-81d5-ea72660faf99.png differ diff --git a/pics/b6acae0d-7148-41de-adc3-ff5ff8dca3ae.jpg b/pics/b6acae0d-7148-41de-adc3-ff5ff8dca3ae.jpg new file mode 100644 index 00000000..852e4fdd Binary files /dev/null and b/pics/b6acae0d-7148-41de-adc3-ff5ff8dca3ae.jpg differ diff --git a/pics/b750eb3e-8a80-475c-95df-2e971b277603.jpg b/pics/b750eb3e-8a80-475c-95df-2e971b277603.jpg new file mode 100644 index 00000000..56868362 Binary files /dev/null and b/pics/b750eb3e-8a80-475c-95df-2e971b277603.jpg differ diff --git a/pics/bed9d745-f971-405d-ba57-bcfa7986c8bd.png b/pics/bed9d745-f971-405d-ba57-bcfa7986c8bd.png new file mode 100644 index 00000000..f1f9adba Binary files /dev/null and b/pics/bed9d745-f971-405d-ba57-bcfa7986c8bd.png differ diff --git a/pics/c2d343f7-604c-4856-9a3c-c71d6f67fecc.png b/pics/c2d343f7-604c-4856-9a3c-c71d6f67fecc.png new file mode 100644 index 00000000..84b2898b Binary files /dev/null and b/pics/c2d343f7-604c-4856-9a3c-c71d6f67fecc.png differ diff --git a/pics/c95a18ae-4b97-4fa9-9806-d9ed7b345b42.png b/pics/c95a18ae-4b97-4fa9-9806-d9ed7b345b42.png new file mode 100644 index 00000000..a7e3e6c3 Binary files /dev/null and b/pics/c95a18ae-4b97-4fa9-9806-d9ed7b345b42.png differ diff --git a/pics/cae894a9-2424-4de4-ab41-c15d7054a5e7.png b/pics/cae894a9-2424-4de4-ab41-c15d7054a5e7.png new file mode 100644 index 00000000..2000b9e2 Binary files /dev/null and b/pics/cae894a9-2424-4de4-ab41-c15d7054a5e7.png differ diff --git a/pics/cc7bfdeb-452e-4fae-9bc8-323338b0dedb.png b/pics/cc7bfdeb-452e-4fae-9bc8-323338b0dedb.png new file mode 100644 index 00000000..4ca86259 Binary files /dev/null and b/pics/cc7bfdeb-452e-4fae-9bc8-323338b0dedb.png differ diff --git a/pics/ce0fa5d0-866b-46e6-a873-8eb1f78c2882.jpg b/pics/ce0fa5d0-866b-46e6-a873-8eb1f78c2882.jpg new file mode 100644 index 00000000..218c91b1 Binary files /dev/null and b/pics/ce0fa5d0-866b-46e6-a873-8eb1f78c2882.jpg differ diff --git a/pics/cf4386a1-58c9-4eca-a17f-e12b1e9770eb.png b/pics/cf4386a1-58c9-4eca-a17f-e12b1e9770eb.png new file mode 100644 index 00000000..9b9f3838 Binary files /dev/null and b/pics/cf4386a1-58c9-4eca-a17f-e12b1e9770eb.png differ diff --git a/pics/d0175e0c-859e-4991-b263-8378e52f7ee5.jpg b/pics/d0175e0c-859e-4991-b263-8378e52f7ee5.jpg new file mode 100644 index 00000000..5c7aa860 Binary files /dev/null and b/pics/d0175e0c-859e-4991-b263-8378e52f7ee5.jpg differ diff --git a/pics/d1ab24fa-1a25-4804-aa91-513df55cbaa6.jpg b/pics/d1ab24fa-1a25-4804-aa91-513df55cbaa6.jpg new file mode 100644 index 00000000..bbf38872 Binary files /dev/null and b/pics/d1ab24fa-1a25-4804-aa91-513df55cbaa6.jpg differ diff --git a/pics/d2d34239-e7c1-482b-b33e-3170c5943556.jpg b/pics/d2d34239-e7c1-482b-b33e-3170c5943556.jpg new file mode 100644 index 00000000..22f5062a Binary files /dev/null and b/pics/d2d34239-e7c1-482b-b33e-3170c5943556.jpg differ diff --git a/pics/d3352e6a-483a-44f2-930e-28c1d677f9b9.png b/pics/d3352e6a-483a-44f2-930e-28c1d677f9b9.png new file mode 100644 index 00000000..0c3cf565 Binary files /dev/null and b/pics/d3352e6a-483a-44f2-930e-28c1d677f9b9.png differ diff --git a/pics/d3b06a52-cce1-4e05-aeff-1680a282b178.png b/pics/d3b06a52-cce1-4e05-aeff-1680a282b178.png new file mode 100644 index 00000000..7faf9bfb Binary files /dev/null and b/pics/d3b06a52-cce1-4e05-aeff-1680a282b178.png differ diff --git a/pics/d49466db-fdd3-4d36-8a86-47dc45c07a1e.jpg b/pics/d49466db-fdd3-4d36-8a86-47dc45c07a1e.jpg new file mode 100644 index 00000000..c4c7a915 Binary files /dev/null and b/pics/d49466db-fdd3-4d36-8a86-47dc45c07a1e.jpg differ diff --git a/pics/d589eca6-c7cf-49c5-ac96-8e4ca0cccadd.jpg b/pics/d589eca6-c7cf-49c5-ac96-8e4ca0cccadd.jpg new file mode 100644 index 00000000..9ccdace0 Binary files /dev/null and b/pics/d589eca6-c7cf-49c5-ac96-8e4ca0cccadd.jpg differ diff --git a/pics/d7c6c42d-a4d8-4b85-82fb-c21250bf5ca1.png b/pics/d7c6c42d-a4d8-4b85-82fb-c21250bf5ca1.png new file mode 100644 index 00000000..20ef25cf Binary files /dev/null and b/pics/d7c6c42d-a4d8-4b85-82fb-c21250bf5ca1.png differ diff --git a/pics/d9efd6bd-3f34-497e-911c-16d5ea38ce88.png b/pics/d9efd6bd-3f34-497e-911c-16d5ea38ce88.png new file mode 100644 index 00000000..7a08e344 Binary files /dev/null and b/pics/d9efd6bd-3f34-497e-911c-16d5ea38ce88.png differ diff --git a/pics/da5dbeae-f247-400b-84d8-af48f0241bc9.png b/pics/da5dbeae-f247-400b-84d8-af48f0241bc9.png new file mode 100644 index 00000000..b06e1ef1 Binary files /dev/null and b/pics/da5dbeae-f247-400b-84d8-af48f0241bc9.png differ diff --git a/pics/dbc5c9f1-c13c-4d06-86ba-7cc949eb4c8f.jpg b/pics/dbc5c9f1-c13c-4d06-86ba-7cc949eb4c8f.jpg new file mode 100644 index 00000000..6ff8388d Binary files /dev/null and b/pics/dbc5c9f1-c13c-4d06-86ba-7cc949eb4c8f.jpg differ diff --git a/pics/dbd60b1f-b700-4da6-a993-62578e892333.jpg b/pics/dbd60b1f-b700-4da6-a993-62578e892333.jpg new file mode 100644 index 00000000..aa076b2a Binary files /dev/null and b/pics/dbd60b1f-b700-4da6-a993-62578e892333.jpg differ diff --git a/pics/dc695f48-4189-4fc7-b950-ed25f6c1521708518830.jpg b/pics/dc695f48-4189-4fc7-b950-ed25f6c1521708518830.jpg new file mode 100644 index 00000000..d612005a Binary files /dev/null and b/pics/dc695f48-4189-4fc7-b950-ed25f6c1521708518830.jpg differ diff --git a/pics/dcbd1473-96c3-4ace-8b69-2c9342615e7e.png b/pics/dcbd1473-96c3-4ace-8b69-2c9342615e7e.png new file mode 100644 index 00000000..0a0be2ea Binary files /dev/null and b/pics/dcbd1473-96c3-4ace-8b69-2c9342615e7e.png differ diff --git a/pics/dd137585-00bf-428e-8b88-475a8627e8bc.jpg b/pics/dd137585-00bf-428e-8b88-475a8627e8bc.jpg new file mode 100644 index 00000000..4bf55a05 Binary files /dev/null and b/pics/dd137585-00bf-428e-8b88-475a8627e8bc.jpg differ diff --git a/pics/e41405a8-7c05-4f70-8092-e961e28d3112.jpg b/pics/e41405a8-7c05-4f70-8092-e961e28d3112.jpg new file mode 100644 index 00000000..7fef7ba5 Binary files /dev/null and b/pics/e41405a8-7c05-4f70-8092-e961e28d3112.jpg differ diff --git a/pics/e4ca3383-910a-4936-8ac2-9e8fd31b736b.png b/pics/e4ca3383-910a-4936-8ac2-9e8fd31b736b.png new file mode 100644 index 00000000..8ff86a82 Binary files /dev/null and b/pics/e4ca3383-910a-4936-8ac2-9e8fd31b736b.png differ diff --git a/pics/ebc59de6-fa62-4118-92e5-7744b8ae893d.jpg b/pics/ebc59de6-fa62-4118-92e5-7744b8ae893d.jpg new file mode 100644 index 00000000..3bb64136 Binary files /dev/null and b/pics/ebc59de6-fa62-4118-92e5-7744b8ae893d.jpg differ diff --git a/pics/ed23a7cd-d55f-453d-b5bc-076d7b1c002e.jpg b/pics/ed23a7cd-d55f-453d-b5bc-076d7b1c002e.jpg new file mode 100644 index 00000000..30c81ddd Binary files /dev/null and b/pics/ed23a7cd-d55f-453d-b5bc-076d7b1c002e.jpg differ diff --git a/pics/f2af8957-d498-462f-9d11-f1c17876ba8e.png b/pics/f2af8957-d498-462f-9d11-f1c17876ba8e.png new file mode 100644 index 00000000..89c48683 Binary files /dev/null and b/pics/f2af8957-d498-462f-9d11-f1c17876ba8e.png differ diff --git a/pics/f3d3e072-e947-43e9-b999-22385fd569a0.jpg b/pics/f3d3e072-e947-43e9-b999-22385fd569a0.jpg new file mode 100644 index 00000000..7b08be6d Binary files /dev/null and b/pics/f3d3e072-e947-43e9-b999-22385fd569a0.jpg differ diff --git a/pics/f47f9729-29ed-4c17-9924-76139342fac7.png b/pics/f47f9729-29ed-4c17-9924-76139342fac7.png new file mode 100644 index 00000000..d67672f7 Binary files /dev/null and b/pics/f47f9729-29ed-4c17-9924-76139342fac7.png differ diff --git a/pics/f50bc364-fdc2-4a46-9b8f-f8f5b6add3b8.jpg b/pics/f50bc364-fdc2-4a46-9b8f-f8f5b6add3b8.jpg new file mode 100644 index 00000000..ec059ca1 Binary files /dev/null and b/pics/f50bc364-fdc2-4a46-9b8f-f8f5b6add3b8.jpg differ diff --git a/pics/f7ecbb8d-bb8b-4d45-a3b7-f49425d6d83d.jpg b/pics/f7ecbb8d-bb8b-4d45-a3b7-f49425d6d83d.jpg new file mode 100644 index 00000000..ab51d486 Binary files /dev/null and b/pics/f7ecbb8d-bb8b-4d45-a3b7-f49425d6d83d.jpg differ diff --git a/pics/fd20f4f9-e5d8-43cf-bf4f-2057c18d01fb.png b/pics/fd20f4f9-e5d8-43cf-bf4f-2057c18d01fb.png new file mode 100644 index 00000000..1bb5f234 Binary files /dev/null and b/pics/fd20f4f9-e5d8-43cf-bf4f-2057c18d01fb.png differ diff --git a/pics/fd945daf-4a6c-4f20-b9c2-5390f5955ce5.jpg b/pics/fd945daf-4a6c-4f20-b9c2-5390f5955ce5.jpg new file mode 100644 index 00000000..f9b95f53 Binary files /dev/null and b/pics/fd945daf-4a6c-4f20-b9c2-5390f5955ce5.jpg differ diff --git a/pics/flow.png b/pics/flow.png new file mode 100644 index 00000000..aa0492a4 Binary files /dev/null and b/pics/flow.png differ diff --git a/pics/hashMap_u54C8_u5E0C_u7B97_u6CD5_u4F8B_u56FE.png b/pics/hashMap_u54C8_u5E0C_u7B97_u6CD5_u4F8B_u56FE.png new file mode 100644 index 00000000..f19ceb66 Binary files /dev/null and b/pics/hashMap_u54C8_u5E0C_u7B97_u6CD5_u4F8B_u56FE.png differ diff --git a/pics/image005.jpg b/pics/image005.jpg new file mode 100644 index 00000000..a20df7d8 Binary files /dev/null and b/pics/image005.jpg differ diff --git a/pics/image007.jpg b/pics/image007.jpg new file mode 100644 index 00000000..822aa76e Binary files /dev/null and b/pics/image007.jpg differ diff --git a/pics/image008.jpg b/pics/image008.jpg new file mode 100644 index 00000000..952f4aa8 Binary files /dev/null and b/pics/image008.jpg differ diff --git a/pics/image009.jpg b/pics/image009.jpg new file mode 100644 index 00000000..6e956fec Binary files /dev/null and b/pics/image009.jpg differ diff --git a/pics/java-collections.png b/pics/java-collections.png new file mode 100644 index 00000000..6a68af89 Binary files /dev/null and b/pics/java-collections.png differ diff --git a/pics/java-collections1.png b/pics/java-collections1.png new file mode 100644 index 00000000..852282ef Binary files /dev/null and b/pics/java-collections1.png differ diff --git a/pics/linux-filesystem.png b/pics/linux-filesystem.png new file mode 100644 index 00000000..ae965295 Binary files /dev/null and b/pics/linux-filesystem.png differ diff --git a/pics/n2U3N.png b/pics/n2U3N.png new file mode 100644 index 00000000..88b27864 Binary files /dev/null and b/pics/n2U3N.png differ diff --git a/pics/ppt_img.gif b/pics/ppt_img.gif new file mode 100644 index 00000000..8e5ad145 Binary files /dev/null and b/pics/ppt_img.gif differ diff --git a/pics/tGPV0.png b/pics/tGPV0.png new file mode 100644 index 00000000..ee6a3c08 Binary files /dev/null and b/pics/tGPV0.png differ diff --git a/pics/xss-attack.png b/pics/xss-attack.png new file mode 100644 index 00000000..a3025d4d Binary files /dev/null and b/pics/xss-attack.png differ