diff --git a/BOOKLIST.md b/BOOKLIST.md index 4805a7dc..e88ba706 100644 --- a/BOOKLIST.md +++ b/BOOKLIST.md @@ -54,7 +54,7 @@ - [STL 源码剖析](https://book.douban.com/subject/1110934/) - [深度探索 C++ 对象模型](https://book.douban.com/subject/1091086/) -# 网站架构/分布式 +# 系统设计 - [大规模分布式存储系统](https://book.douban.com/subject/25723658/) - [从 Paxos 到 Zookeeper](https://book.douban.com/subject/26292004/) diff --git a/README.md b/README.md index d511fa7c..27edaf24 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ -## :pencil2: 算法 +### :pencil2: 算法 - [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md) @@ -25,7 +25,7 @@ 排序、并查集、栈和队列、红黑树、散列表。 -## :computer: 操作系统 +### :computer: 操作系统 - [计算机操作系统](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机操作系统.md) @@ -35,7 +35,7 @@ 基本实现原理以及基本操作。 -## :cloud: 网络 +### :cloud: 网络 - [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md) @@ -49,7 +49,7 @@ I/O 模型、I/O 多路复用。 -## :couple: 面向对象 +### :couple: 面向对象 - [设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/设计模式.md) @@ -59,7 +59,7 @@ 三大原则(继承、封装、多态)、类图、设计原则。 -## :floppy_disk: 数据库 +### :floppy_disk: 数据库 - [数据库系统原理](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/数据库系统原理.md) @@ -81,7 +81,7 @@ 五种数据类型、字典和跳跃表数据结构、使用场景、和 Memcache 的比较、淘汰策略、持久化、文件事件的 Reactor 模式、复制。 -## :coffee: Java +### :coffee: Java - [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md) @@ -103,7 +103,7 @@ NIO 的原理以及实例。 -## :bulb: 系统设计 +### :bulb: 系统设计 - [系统设计基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/系统设计基础.md) @@ -129,7 +129,7 @@ 消息处理模型、使用场景、可靠性 -## :hammer: 工具 +### :hammer: 工具 - [Git](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Git.md) @@ -147,7 +147,7 @@ 构建工具的基本概念、主流构建工具介绍。 -## :speak_no_evil: 编码实践 +### :speak_no_evil: 编码实践 - [重构](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/重构.md) @@ -161,9 +161,9 @@ Google 开源项目的代码风格规范。 -## :memo: 后记 +### :memo: 后记 -### About +#### About 这个仓库是笔者的一个学习笔记,主要总结一些比较重要的知识点,希望对大家有所帮助。 @@ -171,7 +171,7 @@ [BOOKLIST](https://github.com/CyC2018/Interview-Notebook/blob/master/BOOKLIST.md),这个书单是笔者至今看的一些比较好的技术书籍,虽然没有全都看完,但每本书多多少少都看了一部分。 -### How To Contribute +#### How To Contribute 笔记内容是笔者一个字一个字打上去的,难免会有一些笔误,如果发现笔误可直接在相应文档进行编辑修改。 @@ -179,7 +179,7 @@ 欢迎在 Issue 中提交对本仓库的改进建议~ -### Typesetting +#### Typesetting 笔记内容按照 [中文文案排版指北](http://mazhuang.org/wiki/chinese-copywriting-guidelines/) 进行排版,以保证内容的可读性。 @@ -187,7 +187,7 @@ 笔者将自己实现的文档排版功能提取出来,放在 Github Page 中,无需下载安装即可免费使用:[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting)。 -### Uploading +#### Uploading 笔者在本地使用为知笔记软件进行书写,为了方便将本地笔记内容上传到 Github 上,实现了一整套自动化上传方案,包括文本文件的导出、提取图片、Markdown 文档转换、Git 同步。 @@ -195,15 +195,15 @@ 笔者将自己实现文档转换功能提取出来,方便大家在需要将本地 Markdown 上传到 Github,或者制作项目 README 文档时生成目录时使用:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。 -### Statement +#### Statement 本仓库不参与商业行为,不向读者收取任何费用。(This repository is not engaging in business activities, and does not charge readers any fee.) -### Logo +#### Logo Power by [logomakr](https://logomakr.com/). -### Acknowledgements +#### Acknowledgements 感谢以下人员对本仓库做出的贡献,当然不仅仅只有这些贡献者,这里就不一一列举了。如果你希望被添加到这个名单中,并且提交过 Issue 或者 PR,请与笔者联系。 @@ -229,7 +229,7 @@ Power by [logomakr](https://logomakr.com/). -### License +#### License 在对本作品进行演绎时,请署名并以相同方式共享。 diff --git a/notes/Docker.md b/notes/Docker.md index 80108896..1b61a7fe 100644 --- a/notes/Docker.md +++ b/notes/Docker.md @@ -4,6 +4,7 @@ * [三、优势](#三优势) * [四、使用场景](#四使用场景) * [五、镜像与容器](#五镜像与容器) +* [参考资料](#参考资料) @@ -15,11 +16,6 @@ Docker 主要解决环境配置问题,它是一种虚拟化技术,对进程进行隔离,被隔离的进程独立于宿主操作系统和其它隔离的进程。使用 Docker 可以不修改应用程序代码,不需要开发人员学习特定环境下的技术,就能够将现有的应用程序部署在其他机器中。 -参考资料: - -- [DOCKER 101: INTRODUCTION TO DOCKER WEBINAR RECAP](https://blog.docker.com/2017/08/docker-101-introduction-docker-webinar-recap/) -- [Docker 入门教程](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html) - # 二、与虚拟机的比较 虚拟机也是一种虚拟化技术,它与 Docker 最大的区别在于它是通过模拟硬件,并在硬件上安装操作系统来实现。 @@ -40,30 +36,22 @@ Docker 主要解决环境配置问题,它是一种虚拟化技术,对进程 而 Docker 只是一个进程,只需要将应用以及相关的组件打包,在运行时占用很少的资源,一台机器可以开启成千上万个 Docker。 -参考资料: - -- [Docker container vs Virtual machine](http://www.bogotobogo.com/DevOps/Docker/Docker_Container_vs_Virtual_Machine.php) - # 三、优势 除了启动速度快以及占用资源少之外,Docker 具有以下优势: ## 更容易迁移 -Docker 可以提供一致性的运行环境,可以在不同的机器上进行迁移,而不用担心环境变化导致无法运行。 +提供一致性的运行环境,可以在不同的机器上进行迁移,而不用担心环境变化导致无法运行。 ## 更容易维护 -Docker 使用分层技术和镜像,使得应用可以更容易复用重复部分。复用程度越高,维护工作也越容易。 +使用分层技术和镜像,使得应用可以更容易复用重复部分。复用程度越高,维护工作也越容易。 ## 更容易扩展 可以使用基础镜像进一步扩展得到新的镜像,并且官方和开源社区提供了大量的镜像,通过扩展这些镜像可以非常容易得到我们想要的镜像。 -参考资料: - -- [为什么要使用 Docker?](https://yeasy.gitbooks.io/docker_practice/introduction/why.html) - # 四、使用场景 ## 持续集成 @@ -80,11 +68,6 @@ Docker 具有轻量级以及隔离性的特点,在将代码集成到一个 Doc Docker 轻量级的特点使得它很适合用于部署、维护、组合微服务。 -参考资料: - -- [What is Docker](https://www.docker.com/what-docker) -- [持续集成是什么?](http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html) - # 五、镜像与容器 镜像是一种静态的结构,可以看成面向对象里面的类,而容器是镜像的一个实例。 @@ -95,9 +78,14 @@ Docker 轻量级的特点使得它很适合用于部署、维护、组合微服

-参考资料: +# 参考资料 +- [DOCKER 101: INTRODUCTION TO DOCKER WEBINAR RECAP](https://blog.docker.com/2017/08/docker-101-introduction-docker-webinar-recap/) +- [Docker 入门教程](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html) +- [Docker container vs Virtual machine](http://www.bogotobogo.com/DevOps/Docker/Docker_Container_vs_Virtual_Machine.php) - [How to Create Docker Container using Dockerfile](https://linoxide.com/linux-how-to/dockerfile-create-docker-container/) - [理解 Docker(2):Docker 镜像](http://www.cnblogs.com/sammyliu/p/5877964.html) - +- [为什么要使用 Docker?](https://yeasy.gitbooks.io/docker_practice/introduction/why.html) +- [What is Docker](https://www.docker.com/what-docker) +- [持续集成是什么?](http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html) diff --git a/notes/Git.md b/notes/Git.md index 4bb0a543..71ff502c 100644 --- a/notes/Git.md +++ b/notes/Git.md @@ -1,8 +1,7 @@ -* [学习资料](#学习资料) * [集中式与分布式](#集中式与分布式) -* [Git 的中心服务器](#git-的中心服务器) -* [Git 工作流](#git-工作流) +* [中心服务器](#中心服务器) +* [工作流](#工作流) * [分支实现](#分支实现) * [冲突](#冲突) * [Fast forward](#fast-forward) @@ -11,16 +10,10 @@ * [SSH 传输设置](#ssh-传输设置) * [.gitignore 文件](#gitignore-文件) * [Git 命令一览](#git-命令一览) +* [参考资料](#参考资料) -# 学习资料 - -- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html) -- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html) -- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000) -- [Learn Git Branching](https://learngitbranching.js.org/) - # 集中式与分布式 Git 属于分布式版本控制系统,而 SVN 属于集中式。 @@ -33,11 +26,13 @@ Git 属于分布式版本控制系统,而 SVN 属于集中式。 分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。 -# Git 的中心服务器 +# 中心服务器 -Git 的中心服务器用来交换每个用户的修改。没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。Github 就是一种 Git 中心服务器。 +中心服务器用来交换每个用户的修改,没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。 -# Git 工作流 +Github 就是一个中心服务器。 + +# 工作流

@@ -54,14 +49,14 @@ Git 版本库有一个称为 stage 的暂存区,还有自动创建的 master

-可以跳过暂存区域直接从分支中取出修改或者直接提交修改到分支中 +可以跳过暂存区域直接从分支中取出修改,或者直接提交修改到分支中。 - git commit -a 直接把所有文件的修改添加到暂缓区然后执行提交 - git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作 # 分支实现 -Git 把每次提交都连成一条时间线。分支使用指针来实现,例如 master 分支指针指向时间线的最后一个节点,也就是最后一次提交。HEAD 指针指向的是当前分支。 +使用指针将每个提交连接成一条时间线,HEAD 指针指向当前分支指针。

@@ -69,7 +64,7 @@ Git 把每次提交都连成一条时间线。分支使用指针来实现,例

-每次提交只会让当前分支向前移动,而其它分支不会移动。 +每次提交只会让当前分支指针向前移动,而其它分支指针不会移动。

@@ -155,4 +150,9 @@ $ ssh-keygen -t rsa -C "youremail@example.com" 比较详细的地址:http://www.cheat-sheets.org/saved-copy/git-cheat-sheet.pdf +# 参考资料 +- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html) +- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html) +- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000) +- [Learn Git Branching](https://learngitbranching.js.org/) diff --git a/notes/HTTP.md b/notes/HTTP.md index 537b24fe..aae807e6 100644 --- a/notes/HTTP.md +++ b/notes/HTTP.md @@ -25,6 +25,7 @@ * [实体首部字段](#实体首部字段) * [五、具体应用](#五具体应用) * [Cookie](#cookie) + * [6. Secure](#6-secure) * [缓存](#缓存) * [连接管理](#连接管理) * [内容协商](#内容协商) @@ -310,7 +311,7 @@ HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使 Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 -Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API (本地存储和会话存储)或 IndexedDB。 +Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。 ### 1. 用途 @@ -348,7 +349,17 @@ Cookie: yummy_cookie=choco; tasty_cookie=strawberry Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; ``` -### 4. JavaScript 获取 Cookie +### 4. 作用域 + +Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。 + +Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配: + +- /docs +- /docs/Web/ +- /docs/Web/HTTP + +### 5. JavaScript 通过 `Document.cookie` 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。 @@ -358,9 +369,7 @@ document.cookie = "tasty_cookie=strawberry"; console.log(document.cookie); ``` -### 5. Secure 和 HttpOnly - -标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。 +### 6. HttpOnly 标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。跨站脚本攻击 (XSS) 常常使用 JavaScript 的 `Document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。 @@ -368,15 +377,9 @@ console.log(document.cookie); Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly ``` -### 6. 作用域 +## 6. Secure -Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。 - -Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配: - -- /docs -- /docs/Web/ -- /docs/Web/HTTP +标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。 ### 7. Session @@ -387,8 +390,7 @@ Session 可以存储在服务器上的文件、数据库或者内存中。也可 使用 Session 维护用户登录状态的过程如下: - 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; -- 服务器验证该用户名和密码; -- 如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; +- 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; - 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; - 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 @@ -462,13 +464,13 @@ Cache-Control: max-age=31536000 Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。 -- 在 HTTP/1.1 中,会优先处理 max-age 指令; -- 在 HTTP/1.0 中,max-age 指令会被忽略掉。 - ```html Expires: Wed, 04 Jul 2012 08:26:05 GMT ``` +- 在 HTTP/1.1 中,会优先处理 max-age 指令; +- 在 HTTP/1.0 中,max-age 指令会被忽略掉。 + ### 4. 缓存验证 需要先了解 ETag 首部字段的含义,它是资源的唯一标识。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一标识。 @@ -727,7 +729,7 @@ HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认 ## HTTP/1.x 缺陷 - HTTP/1.x 实现简单是以牺牲性能为代价的: +HTTP/1.x 实现简单是以牺牲性能为代价的: - 客户端需要使用多个连接才能实现并发和缩短延迟; - 不会压缩请求和响应首部,从而导致不必要的网络流量; @@ -741,9 +743,9 @@ HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式 在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。 -- 一个数据流都有一个唯一标识符和可选的优先级信息,用于承载双向信息。 -- 消息(Message)是与逻辑请求或响应消息对应的完整的一系列帧。 -- 帧(Fram)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。 +- 一个数据流(Stream)都有一个唯一标识符和可选的优先级信息,用于承载双向信息。 +- 消息(Message)是与逻辑请求或响应对应的完整的一系列帧。 +- 帧(Frame)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。

diff --git a/notes/Java IO.md b/notes/Java IO.md index a14ea902..ee23a4bb 100644 --- a/notes/Java IO.md +++ b/notes/Java IO.md @@ -6,7 +6,7 @@ * [装饰者模式](#装饰者模式) * [四、字符操作](#四字符操作) * [编码与解码](#编码与解码) - * [String](#string) + * [String 的编码方式](#string-的编码方式) * [Reader 与 Writer](#reader-与-writer) * [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容) * [五、对象操作](#五对象操作) @@ -121,7 +121,7 @@ UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF- Java 使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。 -## String +## String 的编码方式 String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。 @@ -371,7 +371,7 @@ public static void fastCopy(String src, String dist) throws IOException { /* 获取目标文件的输出字节流 */ FileOutputStream fout = new FileOutputStream(dist); - /* 获取输出字节流的通道 */ + /* 获取输出字节流的文件通道 */ FileChannel fcout = fout.getChannel(); /* 为缓冲区分配 1024 个字节 */ @@ -392,7 +392,7 @@ public static void fastCopy(String src, String dist) throws IOException { /* 把缓冲区的内容写入输出文件中 */ fcout.write(buffer); - + /* 清空缓冲区 */ buffer.clear(); } @@ -407,7 +407,7 @@ NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用 通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。 -因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件具有更好的性能。 +因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。 应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。 @@ -601,8 +601,8 @@ MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024); NIO 与普通 I/O 的区别主要有以下两点: -- NIO 是非阻塞的 -- NIO 面向块,I/O 面向流 +- NIO 是非阻塞的; +- NIO 面向块,I/O 面向流。 # 八、参考资料 diff --git a/notes/Java 基础.md b/notes/Java 基础.md index 44ca873a..5b689cf8 100644 --- a/notes/Java 基础.md +++ b/notes/Java 基础.md @@ -6,7 +6,8 @@ * [概览](#概览) * [不可变的好处](#不可变的好处) * [String, StringBuffer and StringBuilder](#string,-stringbuffer-and-stringbuilder) - * [String.intern()](#stringintern) + * [String Pool](#string-pool) + * [new String("abc")](#new-string"abc") * [三、运算](#三运算) * [参数传递](#参数传递) * [float 与 double](#float-与-double) @@ -64,7 +65,7 @@ int y = x; // 拆箱 new Integer(123) 与 Integer.valueOf(123) 的区别在于: -- new Integer(123) 每次都会新建一个对象 +- new Integer(123) 每次都会新建一个对象; - Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。 ```java @@ -193,33 +194,90 @@ String 不可变性天生具备线程安全,可以在多个线程中安全地 [StackOverflow : String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder) -## String.intern() +## String Pool -使用 String.intern() 可以保证相同内容的字符串变量引用同一的内存对象。 +字符串常量池(String Poll)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Poll 中。 -下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用。intern() 首先把 s1 引用的对象放到 String Pool(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。 +当一个字符串调用 intern() 方法时,如果 String Poll 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Poll 中字符串的引用;否则,就会在 String Poll 中添加一个新的字符串,并返回这个新字符串的引用。 + +下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。 ```java String s1 = new String("aaa"); String s2 = new String("aaa"); System.out.println(s1 == s2); // false String s3 = s1.intern(); -System.out.println(s1.intern() == s3); // true +String s4 = s1.intern(); +System.out.println(s3 == s4); // true ``` -如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。 +如果是采用 "bbb" 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。 ```java -String s4 = "bbb"; String s5 = "bbb"; +String s6 = "bbb"; System.out.println(s4 == s5); // true ``` -在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7,字符串常量池被移到 Native Method 中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。 +在 Java 7 之前,String Poll 被放在运行时常量池中,它属于永久代。而在 Java 7,String Poll 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。 - [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning) - [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html) +## new String("abc") + +使用这种方式一共会创建两个字符串对象(前提是 String Poll 中还没有 "abc" 字符串对象)。 + +- "abc" 属于字符串字面量,因此编译时期会在 String Poll 中创建一个字符串对象,指向这个 "abc" 字符串字面量; +- 而使用 new 的方式会在堆中创建一个字符串对象。 + +创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。 + +```java +public class NewStringTest { + public static void main(String[] args) { + String s = new String("abc"); + } +} +``` + +使用 javap -verbose 进行反编译,得到以下内容: + +```java +// ... +Constant pool: +// ... + #2 = Class #18 // java/lang/String + #3 = String #19 // abc +// ... + #18 = Utf8 java/lang/String + #19 = Utf8 abc +// ... + + public static void main(java.lang.String[]); + descriptor: ([Ljava/lang/String;)V + flags: ACC_PUBLIC, ACC_STATIC + Code: + stack=3, locals=2, args_size=1 + 0: new #2 // class java/lang/String + 3: dup + 4: ldc #3 // String abc + 6: invokespecial #4 // Method java/lang/String."":(Ljava/lang/String;)V + 9: astore_1 +// ... +``` + +在 Constant Poll 中,#19 存储这字符串字面量 "abc",#3 是 String Poll 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Poll 中的字符串对象作为 String 构造函数的参数。 + +以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。 + +```java +public String(String original) { + this.value = original.value; + this.hash = original.hash; +} +``` + # 三、运算 ## 参数传递 @@ -403,6 +461,7 @@ public class AccessExample { ```java public class AccessWithInnerClassExample { + private class InnerClass { int x; } @@ -468,6 +527,7 @@ ac2.func1(); ```java public interface InterfaceExample { + void func1(); default void func2(){ @@ -519,7 +579,7 @@ System.out.println(InterfaceExample.x); - 需要能控制继承来的成员的访问权限,而不是都为 public。 - 需要继承非静态和非常量字段。 -在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。 +在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。 - [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/) - [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface) @@ -660,7 +720,7 @@ x.equals(y) == x.equals(y); // true x.equals(null); // false; ``` -**2. equals() 与 ==** +**2. 等价与相等** - 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 - 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。 @@ -723,7 +783,7 @@ set.add(e2); System.out.println(set.size()); // 2 ``` -理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。 +理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。 一个数与 31 相乘可以转换成移位和减法:`31*x == (x<<5)-x`,编译器会自动进行这个优化。 @@ -828,6 +888,7 @@ public class CloneExample implements Cloneable { ```java public class ShallowCloneExample implements Cloneable { + private int[] arr; public ShallowCloneExample() { @@ -870,6 +931,7 @@ System.out.println(e2.get(2)); // 222 ```java public class DeepCloneExample implements Cloneable { + private int[] arr; public DeepCloneExample() { @@ -917,6 +979,7 @@ System.out.println(e2.get(2)); // 2 ```java public class CloneConstructorExample { + private int[] arr; public CloneConstructorExample() { @@ -982,7 +1045,7 @@ private 方法隐式地被指定为 final,如果在子类中定义的方法和 **1. 静态变量** -- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它;静态变量在内存中只存在一份。 +- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。 - 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。 ```java @@ -1001,7 +1064,7 @@ public class A { **2. 静态方法** -静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法(abstract)。 +静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。 ```java public abstract class A { @@ -1124,7 +1187,7 @@ public InitialOrderTest() { 每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。 -类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载,该方法会返回一个 Class 对象。 +类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载,该方法会返回一个 Class 对象。 反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。 @@ -1221,7 +1284,7 @@ Java 注解是附加在代码中的一些元信息,用于一些工具在编译 - Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。 - Java 支持自动垃圾回收,而 C++ 需要手动回收。 - Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。 -- Java 不支持操作符重载,虽然可以对两个 String 对象支持加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。 +- Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。 - Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。 - Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。 diff --git a/notes/Java 容器.md b/notes/Java 容器.md index 001f864c..fd1ee97e 100644 --- a/notes/Java 容器.md +++ b/notes/Java 容器.md @@ -126,7 +126,91 @@ public class ArrayList extends AbstractList private static final int DEFAULT_CAPACITY = 10; ``` -### 2. 序列化 +### 2. 扩容 + +添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。 + +扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 + +```java +public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; + return true; +} + +private void ensureCapacityInternal(int minCapacity) { + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); + } + ensureExplicitCapacity(minCapacity); +} + +private void ensureExplicitCapacity(int minCapacity) { + modCount++; + // overflow-conscious code + if (minCapacity - elementData.length > 0) + grow(minCapacity); +} + +private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + // minCapacity is usually close to size, so this is a win: + elementData = Arrays.copyOf(elementData, newCapacity); +} +``` + +### 3. 删除元素 + +需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。 + +```java +public E remove(int index) { + rangeCheck(index); + modCount++; + E oldValue = elementData(index); + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, numMoved); + elementData[--size] = null; // clear to let GC do its work + return oldValue; +} +``` + +### 4. Fail-Fast + +modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 + +在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。 + +```java +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException{ + // Write out element count, and any hidden stuff + int expectedModCount = modCount; + s.defaultWriteObject(); + + // 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> 1)`,也就是旧容量的 1.5 倍。 - -扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 - -```java -public boolean add(E e) { - ensureCapacityInternal(size + 1); // Increments modCount!! - elementData[size++] = e; - return true; -} - -private void ensureCapacityInternal(int minCapacity) { - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); - } - ensureExplicitCapacity(minCapacity); -} - -private void ensureExplicitCapacity(int minCapacity) { - modCount++; - // overflow-conscious code - if (minCapacity - elementData.length > 0) - grow(minCapacity); -} - -private void grow(int minCapacity) { - // overflow-conscious code - int oldCapacity = elementData.length; - int newCapacity = oldCapacity + (oldCapacity >> 1); - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); -} -``` - -### 4. 删除元素 - -需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。 - -```java -public E remove(int index) { - rangeCheck(index); - modCount++; - E oldValue = elementData(index); - int numMoved = size - index - 1; - if (numMoved > 0) - System.arraycopy(elementData, index+1, elementData, index, numMoved); - elementData[--size] = null; // clear to let GC do its work - return oldValue; -} -``` - -### 5. Fail-Fast - -modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 - -在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。 - -```java -private void writeObject(java.io.ObjectOutputStream s) - throws java.io.IOException{ - // Write out element count, and any hidden stuff - int expectedModCount = modCount; - s.defaultWriteObject(); - - // 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 implements Map.Entry { public final String toString() { return getKey() + "=" + getValue(); } - - /** - * This method is invoked whenever the value in an entry is - * overwritten by an invocation of put(k,v) for a key k that's already - * in the HashMap. - */ - void recordAccess(HashMap m) { - } - - /** - * This method is invoked whenever the entry is - * removed from the table. - */ - void recordRemoval(HashMap m) { - } } ``` @@ -929,7 +914,7 @@ JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败 public class LinkedHashMap extends HashMap implements Map ``` -内存维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。 +内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。 ```java /** @@ -1197,7 +1182,7 @@ ListIterator <-- List - Eckel B. Java 编程思想 [M]. 机械工业出版社, 2002. - [Java Collection Framework](https://www.w3resource.com/java-tutorial/java-collections.php) - [Iterator 模式](https://openhome.cc/Gossip/DesignPattern/IteratorPattern.htm) -- [Java 8 系列之重新认识 HashMap](https://tech.meituan.com/java-hashmap.html) +- [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) diff --git a/notes/Java 并发.md b/notes/Java 并发.md index 1403f764..03aaa9fd 100644 --- a/notes/Java 并发.md +++ b/notes/Java 并发.md @@ -637,6 +637,7 @@ B ```java public class WaitNotifyExample { + public synchronized void before() { System.out.println("before"); notifyAll(); @@ -674,12 +675,15 @@ after ## await() signal() signalAll() -java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。 +java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。 + +相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。 使用 Lock 来获取一个 Condition 对象。 ```java public class AwaitSignalExample { + private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); @@ -809,7 +813,7 @@ before..before..before..before..before..before..before..before..before..before.. ## Semaphore -Semaphore 就是操作系统中的信号量,可以控制对互斥资源的访问线程数。 +Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。

@@ -1098,11 +1102,11 @@ Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互 Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。 -有一个错误认识就是,int 等原子性的变量在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 变量属于 int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。 +有一个错误认识就是,int 等原子性的类型在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 属于 int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。 为了方便讨论,将内存间的交互操作简化为 3 个:load、assign、store。 -下图演示了两个线程同时对 cnt 变量进行操作,load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入该变量的值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。 +下图演示了两个线程同时对 cnt 进行操作,load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入旧值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。

@@ -1200,9 +1204,7 @@ public static void main(String[] args) throws InterruptedException { ### 3. 有序性 -有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。 - -在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。 +有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。 volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。 @@ -1413,7 +1415,7 @@ synchronized 和 ReentrantLock。 **(二)AtomicInteger** -J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。 +J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。 以下代码使用了 AtomicInteger 执行了自增的操作。 @@ -1425,7 +1427,7 @@ public void add() { } ``` -以下代码是 incrementAndGet() 的源码,它调用了 unsafe 的 getAndAddInt() 。 +以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。 ```java public final int incrementAndGet() { @@ -1463,9 +1465,6 @@ J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。 ```java -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - public class StackClosedExample { public void add100() { int cnt = 0; @@ -1555,7 +1554,7 @@ public class ThreadLocalExample1 {

-每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象,Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员。 +每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。 ```java /* ThreadLocal values pertaining to this thread. This map is maintained @@ -1686,15 +1685,15 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态: - 缩小同步范围,从而减少锁争用。例如对于 synchronized,应该尽量使用同步块而不是同步方法。 -- 多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。 +- 多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善。 + +- 使用 BlockingQueue 实现生产者消费者问题。 - 多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。 - 使用本地变量和不可变类来保证线程安全。 -- 使用线程池而不是直接创建 Thread 对象,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。 - -- 使用 BlockingQueue 实现生产者消费者问题。 +- 使用线程池而不是直接创建线程,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。 # 参考资料 diff --git a/notes/Java 虚拟机.md b/notes/Java 虚拟机.md index a4a4ba12..333c1418 100644 --- a/notes/Java 虚拟机.md +++ b/notes/Java 虚拟机.md @@ -38,7 +38,7 @@ ## Java 虚拟机栈 -每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息,从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 +每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

@@ -55,27 +55,21 @@ java -Xss512M HackTheJava ## 本地方法栈 -本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。 - 本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。 +本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。 +

## 堆 所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。 -现代的垃圾收集器基本都是采用分代收集算法,针对不同类型的对象采取不同的垃圾回收算法,可以将堆分成两块: +现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法,可以将堆分成两块: - 新生代(Young Generation) - 老年代(Old Generation) -新生代可以继续划分成以下三个空间: - -- Eden(伊甸园) -- From Survivor(幸存者) -- To Survivor - 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。 可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。 @@ -92,7 +86,7 @@ java -Xms1M -Xmx2M HackTheJava 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。 -JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收。但是从 JDK 1.7 开始,已经把原本放在永久代的字符串常量池移到 Native Method 中。 +HotSpot 虚拟机把它当成永久代来进行垃圾回收。但是很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。 ## 运行时常量池 @@ -104,7 +98,7 @@ Class 文件中的常量池(编译器生成的各种字面量和符号引用 ## 直接内存 -在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。 +在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存(Native 堆),然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 @@ -142,15 +136,15 @@ public class ReferenceCountingGC { 通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。 -

- Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容: -- 虚拟机栈中引用的对象 -- 本地方法栈中引用的对象 +- 虚拟机栈中局部变量表中引用的对象 +- 本地方法栈中 JNI 中引用的对象 - 方法区中类静态属性引用的对象 - 方法区中的常量引用的对象 +

+ ### 3. 方法区的回收 因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。 @@ -171,13 +165,13 @@ Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。 -当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。 +当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。 ## 引用类型 无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。 -Java 具有四种强度不同的引用类型。 +Java 提供了四种强度不同的引用类型。 ### 1. 强引用 @@ -274,7 +268,7 @@ HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了 以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。 - 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程; -- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。 +- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。 ### 1. Serial 收集器 @@ -286,7 +280,7 @@ Serial 翻译为串行,也就是说它以串行的方式执行。 它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。 -它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。 +它是 Client 模式下的默认新生代收集器,因为在该应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。 ### 2. ParNew 收集器 @@ -304,11 +298,11 @@ Serial 翻译为串行,也就是说它以串行的方式执行。 其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。 -停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 +停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。 缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 -可以通过一个开关参数打卡 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 。 +可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。 ### 4. Serial Old 收集器 @@ -378,8 +372,6 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和 - 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。 - 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。 -更详细内容请参考:[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html) - # 三、内存分配与回收策略 ## Minor GC 和 Full GC @@ -539,7 +531,7 @@ public class Test { - 与类的构造函数(或者说实例构造器 <init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 <clinit>() 方法运行之前,父类的 <clinit>() 方法已经执行结束。因此虚拟机中第一个执行 <clinit>() 方法的类肯定为 java.lang.Object。 -- 由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。例如以下代码: +- 由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码: ```java static class Parent { @@ -744,6 +736,7 @@ public class FileSystemClassLoader extends ClassLoader { - 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011. - [Chapter 2. The Structure of the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4) - [Jvm memory](https://www.slideshare.net/benewu/jvm-memory) +[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html) - [JNI Part1: Java Native Interface Introduction and “Hello World” application](http://electrofriends.com/articles/jni/jni-part1-java-native-interface/) - [Memory Architecture Of JVM(Runtime Data Areas)](https://hackthejava.wordpress.com/2015/01/09/memory-architecture-by-jvmruntime-data-areas/) - [JVM Run-Time Data Areas](https://www.programcreek.com/2013/04/jvm-run-time-data-areas/) diff --git a/notes/Linux.md b/notes/Linux.md index 8ff3e2c6..4ba647a7 100644 --- a/notes/Linux.md +++ b/notes/Linux.md @@ -644,7 +644,7 @@ locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内 example: find . -name "shadow*" ``` -(一)与时间有关的选项 +**① 与时间有关的选项** ```html -mtime n :列出在 n 天前的那一天修改过内容的文件 @@ -657,7 +657,7 @@ example: find . -name "shadow*"

-(二)与文件拥有者和所属群组有关的选项 +**② 与文件拥有者和所属群组有关的选项** ```html -uid n @@ -668,7 +668,7 @@ example: find . -name "shadow*" -nogroup:搜索所属群组不存在于 /etc/group 的文件 ``` -(三)与文件权限和名称有关的选项 +**③ 与文件权限和名称有关的选项** ```html -name filename @@ -1038,9 +1038,7 @@ $ grep -n 'go\{2,5\}g' regular_express.txt ## printf -用于格式化输出。 - -它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。 +用于格式化输出。它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。 ```html $ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt) @@ -1169,7 +1167,7 @@ dmtsai lines: 5 columns: 9 | S | interruptible sleep (waiting for an event to complete) | | Z | zombie (terminated but not reaped by its parent) | | T | stopped (either by a job control signal or because it is being traced) | - +

## SIGCHLD @@ -1179,12 +1177,12 @@ dmtsai lines: 5 columns: 9 - 得到 SIGCHLD 信号; - waitpid() 或者 wait() 调用会返回。 -

- 其中子进程发送的 SIGCHLD 信号包含了子进程的信息,包含了进程 ID、进程状态、进程使用 CPU 的时间等。 在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。 +

+ ## wait() ```c diff --git a/notes/MySQL.md b/notes/MySQL.md index 54067977..f115ce92 100644 --- a/notes/MySQL.md +++ b/notes/MySQL.md @@ -4,7 +4,7 @@ * [MySQL 索引](#mysql-索引) * [索引优化](#索引优化) * [索引的优点](#索引的优点) - * [索引的使用场景](#索引的使用场景) + * [索引的使用条件](#索引的使用条件) * [二、查询性能优化](#二查询性能优化) * [使用 Explain 进行分析](#使用-explain-进行分析) * [优化数据访问](#优化数据访问) @@ -48,7 +48,7 @@ B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具 进行查找操作时,首先在根节点进行二分查找,找到一个 key 所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的 data。 -插入删除操作记录会破坏平衡树的平衡性,因此在插入删除操作之后,需要对树进行一个分裂、合并、旋转等操作来维护平衡性。 +插入删除操作会破坏平衡树的平衡性,因此在插入删除操作之后,需要对树进行一个分裂、合并、旋转等操作来维护平衡性。 ### 3. 与红黑树的比较 @@ -58,13 +58,13 @@ B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具 平衡树查找操作的时间复杂度等于树高 h,而树高大致为 O(h)=O(logdN),其中 d 为每个节点的出度。 -红黑树的出度为 2,而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多,检索的次数也就更多。 +红黑树的出度为 2,而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多,查找的次数也就更多。 -(二)利用计算机预读特性 +(二)利用磁盘预读特性 -为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,因此速度会非常快。 +为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,速度会非常快。 -操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,相邻的节点也能够被预先载入。 +操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点。并且可以利用预读特性,相邻的节点也能够被预先载入。 ## MySQL 索引 @@ -74,15 +74,15 @@ B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具 是大多数 MySQL 存储引擎的默认索引类型。 -因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。除了用于查找,还可以用于排序和分组。 +因为不再需要进行全表扫描,只需要对树进行搜索即可,所以查找速度快很多。 + +除了用于查找,还可以用于排序和分组。 可以指定多个列作为索引列,多个索引列共同组成键。 适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。 -InnoDB 的 B+Tree 索引分为主索引和辅助索引。 - -主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。 +InnoDB 的 B+Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。

@@ -92,7 +92,7 @@ InnoDB 的 B+Tree 索引分为主索引和辅助索引。 ### 2. 哈希索引 -哈希索引能以 O(1) 时间进行查找,但是失去了有序性,它具有以下限制: +哈希索引能以 O(1) 时间进行查找,但是失去了有序性: - 无法用于排序与分组; - 只支持精确查找,无法用于部分查找和范围查找。 @@ -101,9 +101,11 @@ InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当 ### 3. 全文索引 -MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。 +MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。 -全文索引一般使用倒排索引实现,它记录着关键词到其所在文档的映射。 +查找条件使用 MATCH AGAINST,而不是普通的 WHERE。 + +全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。 InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。 @@ -136,7 +138,9 @@ WHERE actor_id = 1 AND film_id = 1; ### 3. 索引列的顺序 -让选择性最强的索引列放在前面,索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,查询效率也越高。 +让选择性最强的索引列放在前面。 + +索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,查询效率也越高。 例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。 @@ -173,14 +177,16 @@ customer_id_selectivity: 0.0373 - 大大减少了服务器需要扫描的数据行数。 -- 帮助服务器避免进行排序和分组,也就不需要创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,因为不需要排序和分组,也就不需要创建临时表)。 +- 帮助服务器避免进行排序和分组,以及避免创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,因为不需要排序和分组,也就不需要创建临时表)。 -- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,也就将相邻的数据都存储在一起)。 +- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,会将相邻的数据都存储在一起)。 -## 索引的使用场景 +## 索引的使用条件 + +- 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效; + +- 对于中到大型的表,索引就非常有效; -- 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。 -- 对于中到大型的表,索引就非常有效。 - 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。 # 二、查询性能优化 @@ -227,7 +233,7 @@ do { ### 2. 分解大连接查询 -将一个大连接查询分解成对每一个表进行一次单表查询,然后将结果在应用程序中进行关联,这样做的好处有: +将一个大连接查询分解成对每一个表进行一次单表查询,然后在应用程序中进行关联,这样做的好处有: - 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。 - 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。 @@ -326,7 +332,7 @@ MySQL 提供了两种相似的日期时间类型:DATETIME 和 TIMESTAMP。 ### 2. TIMESTAMP -和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。 +和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年到 2038 年。 它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。 @@ -348,17 +354,17 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提 ## 垂直切分 -

- 垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。 在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。 +

+ ## Sharding 策略 -- 哈希取模:hash(key) % NUM_DB -- 范围:可以是 ID 范围也可以是时间范围 -- 映射表:使用单独的一个数据库来存储映射关系 +- 哈希取模:hash(key) % N; +- 范围:可以是 ID 范围也可以是时间范围; +- 映射表:使用单独的一个数据库来存储映射关系。 ## Sharding 存在的问题及解决方案 @@ -366,9 +372,9 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提 使用分布式事务来解决,比如 XA 接口。 -### 2. 链接 +### 2. JOIN -可以将原来的 JOIN 分解成多个单表查询,然后在用户程序中进行 JOIN。 +可以将原来的 JOIN 分解成多个单表 JOIN 查询,然后在用户程序中进行 JOIN。 ### 3. ID 唯一性 @@ -376,20 +382,15 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提 - 为每个分片指定一个 ID 范围 - 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法) -更多内容请参考: - -- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6) -- [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html) - # 六、复制 ## 主从复制 主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。 -- **binlog 线程** :负责将主服务器上的数据更改写入二进制日志中。 -- **I/O 线程** :负责从主服务器上读取二进制日志,并写入从服务器的中继日志中。 -- **SQL 线程** :负责读取中继日志并重放其中的 SQL 语句。 +- **binlog 线程** :负责将主服务器上的数据更改写入二进制日志(Binary log)中。 +- **I/O 线程** :负责从主服务器上读取二进制日志,并写入从服务器的重放日志(Replay log)中。 +- **SQL 线程** :负责读取重放日志并重放其中的 SQL 语句。

@@ -417,3 +418,5 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提 - [SQL Azure Federation – Introduction](http://geekswithblogs.net/shaunxu/archive/2012/01/07/sql-azure-federation-ndash-introduction.aspx "Title of this entry.") - [MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html) - [MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735) +- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6) +- [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html) diff --git a/notes/Redis.md b/notes/Redis.md index 8f231382..58320dfa 100644 --- a/notes/Redis.md +++ b/notes/Redis.md @@ -49,7 +49,7 @@ Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。 -键的类型只能为字符串,值支持的五种类型数据类型为:字符串、列表、集合、散列表、有序集合。 +键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。 Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。 @@ -58,7 +58,7 @@ Redis 支持很多特性,例如将内存中的数据持久化到硬盘中, | 数据类型 | 可以存储的值 | 操作 | | :--: | :--: | :--: | | STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作 | -| LIST | 列表 | 从两端压入或者弹出元素
对单个或者多个元素
进行修剪,只保留一个范围内的元素 | +| LIST | 列表 | 从两端压入或者弹出元素
对单个或者多个元素
进行修剪,只保留一个范围内的元素 | | SET | 无序集合 | 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素 | | HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在| | ZSET | 有序集合 | 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名 | @@ -357,7 +357,7 @@ List 是一个双向链表,可以通过 lpop 和 lpush 写入和读取消息 在分布式场景下具有多个应用服务器,可以使用 Redis 来统一存储这些应用服务器的会话信息。 -当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器。 +当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。 ## 分布式锁实现 @@ -393,7 +393,7 @@ Redis Cluster 实现了分布式的支持。 - 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。 -- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 +- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 # 六、键的过期时间 @@ -549,7 +549,7 @@ def main(): # 十二、Sentinel -Sentinel(哨兵)可以监听主服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。 +Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。 # 十三、分片 @@ -605,5 +605,4 @@ Redis 没有关系型数据库中的表这一概念来将同种类型的数据 - [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html) - [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide) - [Redis 应用场景](http://www.scienjus.com/redis-use-case/) -- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/) - [Using Redis as an LRU cache](https://redis.io/topics/lru-cache) diff --git a/notes/Socket.md b/notes/Socket.md index 8e7c0b89..482bafe6 100644 --- a/notes/Socket.md +++ b/notes/Socket.md @@ -25,9 +25,9 @@ - 等待数据准备好 - 从内核向进程复制数据 -对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。 +对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。 -Unix 下有五种 I/O 模型: +Unix 有五种 I/O 模型: - 阻塞式 I/O - 非阻塞式 I/O @@ -39,7 +39,7 @@ Unix 下有五种 I/O 模型: 应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。 -应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。 +应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的 CPU 利用率效率会比较高。 下图中,recvfrom 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。 @@ -53,13 +53,13 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * 应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。 -由于 CPU 要处理更多的系统调用,因此这种模型是比较低效的。 +由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率是比较低的。

## I/O 复用 -使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。 +使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。 它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。 @@ -77,7 +77,7 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * ## 异步 I/O -进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。 +应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。 异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。 @@ -198,7 +198,7 @@ else select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。 - select 会修改描述符,而 poll 不会; -- select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 的描述符类型使用链表实现,没有描述符的数量的限制; +- select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 的描述符类型使用链表实现,没有描述符数量的限制; - poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。 - 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。 diff --git a/notes/分布式.md b/notes/分布式.md index e98f2860..113837ce 100644 --- a/notes/分布式.md +++ b/notes/分布式.md @@ -29,18 +29,18 @@ # 一、分布式锁 -在单机场景下,可以使用 Java 提供的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。 +在单机场景下,可以使用语言的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。 阻塞锁通常使用互斥量来实现: -- 互斥量为 1 表示有其它进程在使用锁,此时处于锁定状态; -- 互斥量为 0 表示未锁定状态。 +- 互斥量为 0 表示有其它进程在使用锁,此时处于锁定状态; +- 互斥量为 1 表示未锁定状态。 -1 和 0 可以用一个整型值表示,也可以用某个数据是否存在表示,存在表示互斥量为 1。 +1 和 0 可以用一个整型值表示,也可以用某个数据是否存在表示。 ## 数据库的唯一索引 -当想要获得锁时,就向表中插入一条记录,释放锁时就删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。 +获得锁时向表中插入一条记录,释放锁时删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。 存在以下几个问题: @@ -91,11 +91,11 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点的父 ### 5. 会话超时 -如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁的释放锁失败问题。 +如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现的分布式锁释放锁失败问题。 ### 6. 羊群效应 -一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。 +一个节点未获得锁,只需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。 # 二、分布式事务 @@ -159,9 +159,7 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点的父 ## 一致性 -一致性指的是多个数据副本是否能保持一致的特性。 - -在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。 +一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。 对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。 @@ -169,7 +167,7 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点的父 可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。 -在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。 +在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操,请求总是能够在有限的时间内返回结果。 ## 分区容忍性 @@ -179,9 +177,9 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点的父 ## 权衡 -在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际在是要在可用性和一致性之间做权衡。 +在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际上是要在可用性和一致性之间做权衡。 -可用性和一致性往往是冲突的,很难都使它们同时满足。在多个节点之间进行数据同步时, +可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时, - 为了保证一致性(CP),就需要让所有节点下线成为不可用的状态,等待同步完成; - 为了保证可用性(AP),在同步过程中允许读取所有节点的数据,但是数据可能不一致。 @@ -204,7 +202,7 @@ BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思 ## 软状态 -指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在延时。 +指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在时延。 ## 最终一致性 diff --git a/notes/剑指 offer 题解.md b/notes/剑指 offer 题解.md index 1bac6bbb..63cf170e 100644 --- a/notes/剑指 offer 题解.md +++ b/notes/剑指 offer 题解.md @@ -230,7 +230,7 @@ Output: ```java public String replaceSpace(StringBuffer str) { int P1 = str.length() - 1; - for (int i = 0; i < str.length(); i++) + for (int i = 0; i < P1 + 1; i++) if (str.charAt(i) == ' ') str.append(" "); @@ -1082,7 +1082,7 @@ false ```java public boolean isNumeric(char[] str) { - if (str == null) + if (str == null || str.length == 0) return false; return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?"); } @@ -2779,19 +2779,25 @@ public List> dicesSum(int n) { ```java public boolean isContinuous(int[] nums) { + if (nums.length < 5) return false; + Arrays.sort(nums); + + // 统计癞子数量 int cnt = 0; - for (int num : nums) /* 统计癞子数量 */ + for (int num : nums) if (num == 0) cnt++; + // 使用癞子去补全不连续的顺子 for (int i = cnt; i < nums.length - 1; i++) { if (nums[i + 1] == nums[i]) return false; - cnt -= nums[i + 1] - nums[i] - 1; /* 使用癞子去补全不连续的顺子 */ + cnt -= nums[i + 1] - nums[i] - 1; } + return cnt >= 0; } ``` @@ -2824,7 +2830,7 @@ public int LastRemaining_Solution(int n, int m) { ## 题目描述 -可以有一次买入和一次卖出,买入必须在前。求最大收益。 +可以有一次买入和一次卖出,那么买入必须在前。求最大收益。 ## 解题思路 diff --git a/notes/构建工具.md b/notes/构建工具.md index c0e4acd5..34fef54b 100644 --- a/notes/构建工具.md +++ b/notes/构建工具.md @@ -1,11 +1,12 @@ -* [一、什么是构建工具](#一什么是构建工具) +* [一、构建工具的作用](#一构建工具的作用) * [二、Java 主流构建工具](#二java-主流构建工具) * [三、Maven](#三maven) +* [参考资料](#参考资料) -# 一、什么是构建工具 +# 一、构建工具的作用 构建工具是用于构建项目的自动化工具,主要包含以下工作: @@ -15,7 +16,7 @@ ## 运行单元测试 -不再需要在项目代码中添加测试代码,从而污染项目代码。 +不再需要在项目代码中添加测试代码,从而避免了污染项目代码。 ## 将源代码转化为可执行文件 @@ -29,10 +30,6 @@ 不再需要通过 FTP 将 Jar 包上传到服务器上。 -参考资料: - -- [What is a build tool?](https://stackoverflow.com/questions/7249871/what-is-a-build-tool) - # 二、Java 主流构建工具 主要包括 Ant、Maven 和 Gradle。 @@ -72,12 +69,6 @@ dependencies { } ``` -参考资料: - -- [Java Build Tools Comparisons: Ant vs Maven vs Gradle](https://programmingmitra.blogspot.com/2016/05/java-build-tools-comparisons-ant-vs.html) -- [maven 2 gradle](http://sagioto.github.io/maven2gradle/) -- [新一代构建工具 gradle](https://www.imooc.com/learn/833) - # 三、Maven ## 概述 @@ -114,7 +105,7 @@ POM 代表项目对象模型,它是一个 XML 文件,保存在项目根目 ## 依赖原则 -### 依赖路径最短优先原则 +### 1. 依赖路径最短优先原则 ```html A -> B -> C -> X(1.0) @@ -122,7 +113,7 @@ A -> D -> X(2.0) ``` 由于 X(2.0) 路径最短,所以使用 X(2.0)。 -### 声明顺序优先原则 +### 2. 声明顺序优先原则 ```html A -> B -> X(1.0) @@ -131,7 +122,7 @@ A -> C -> X(2.0) 在 POM 中最先声明的优先,上面的两个依赖如果先声明 B,那么最后使用 X(1.0)。 -### 覆写优先原则 +### 3. 覆写优先原则 子 POM 内声明的依赖优先于父 POM 中声明的依赖。 @@ -139,9 +130,11 @@ A -> C -> X(2.0) 找到 Maven 加载的 Jar 包版本,使用 `mvn dependency:tree` 查看依赖树,根据依赖原则来调整依赖在 POM 文件的声明顺序。 -参考资料: +# 参考资料 - [POM Reference](http://maven.apache.org/pom.html#Dependency_Version_Requirement_Specification) - - +- [What is a build tool?](https://stackoverflow.com/questions/7249871/what-is-a-build-tool) +- [Java Build Tools Comparisons: Ant vs Maven vs Gradle](https://programmingmitra.blogspot.com/2016/05/java-build-tools-comparisons-ant-vs.html) +- [maven 2 gradle](http://sagioto.github.io/maven2gradle/) +- [新一代构建工具 gradle](https://www.imooc.com/learn/833) diff --git a/notes/消息队列.md b/notes/消息队列.md index 209d962a..bc80894d 100644 --- a/notes/消息队列.md +++ b/notes/消息队列.md @@ -40,7 +40,7 @@ 发送者将消息发送给消息队列之后,不需要同步等待消息接收者处理完毕,而是立即返回进行其它操作。消息接收者从消息队列中订阅消息之后异步处理。 -例如在注册流程中通常需要发送验证邮件来确保注册用户的身份合法,可以使用消息队列使发送验证邮件的操作异步处理,用户在填写完注册信息之后就可以完成注册,而将发送验证邮件这一消息发送到消息队列中。 +例如在注册流程中通常需要发送验证邮件来确保注册用户身份的合法性,可以使用消息队列使发送验证邮件的操作异步处理,用户在填写完注册信息之后就可以完成注册,而将发送验证邮件这一消息发送到消息队列中。 只有在业务流程允许异步处理的情况下才能这么做,例如上面的注册流程中,如果要求用户对验证邮件进行点击之后才能完成注册的话,就不能再使用消息队列。 diff --git a/notes/算法.md b/notes/算法.md index 25d1550e..cb83830b 100644 --- a/notes/算法.md +++ b/notes/算法.md @@ -23,7 +23,7 @@ * [五、栈和队列](#五栈和队列) * [栈](#栈) * [队列](#队列) -* [六、查找](#六查找) +* [六、符号表](#六符号表) * [初级实现](#初级实现) * [二叉查找树](#二叉查找树) * [2-3 查找树](#2-3-查找树) @@ -104,17 +104,21 @@ public class ThreeSumSlow implements ThreeSum { public int count(int[] nums) { int N = nums.length; int cnt = 0; - for (int i = 0; i < N; i++) - for (int j = i + 1; j < N; j++) - for (int k = j + 1; k < N; k++) - if (nums[i] + nums[j] + nums[k] == 0) + for (int i = 0; i < N; i++) { + for (int j = i + 1; j < N; j++) { + for (int k = j + 1; k < N; k++) { + if (nums[i] + nums[j] + nums[k] == 0) { cnt++; + } + } + } + } return cnt; } } ``` -### 2. ThreeSumFast +### 2. ThreeSumBinarySearch 通过将数组先排序,对两个元素求和,并用二分查找方法查找是否存在该和的相反数,如果存在,就说明存在三元组的和为 0。 @@ -123,19 +127,23 @@ public class ThreeSumSlow implements ThreeSum { 该方法可以将 ThreeSum 算法增长数量级降低为 O(N2logN)。 ```java -public class ThreeSumFast { - public static int count(int[] nums) { +public class ThreeSumBinarySearch implements ThreeSum { + + @Override + public int count(int[] nums) { Arrays.sort(nums); int N = nums.length; int cnt = 0; - for (int i = 0; i < N; i++) + for (int i = 0; i < N; i++) { for (int j = i + 1; j < N; j++) { int target = -nums[i] - nums[j]; int index = BinarySearch.search(nums, target); // 应该注意这里的下标必须大于 j,否则会重复统计。 - if (index > j) + if (index > j) { cnt++; + } } + } return cnt; } } @@ -143,22 +151,59 @@ public class ThreeSumFast { ```java public class BinarySearch { + public static int search(int[] nums, int target) { int l = 0, h = nums.length - 1; while (l <= h) { int m = l + (h - l) / 2; - if (target == nums[m]) + if (target == nums[m]) { return m; - else if (target > nums[m]) + } else if (target > nums[m]) { l = m + 1; - else + } else { h = m - 1; + } } return -1; } } ``` +### 3. ThreeSumTwoPointer + +更有效的方法是先将数组排序,然后使用双指针进行查找,时间复杂度为 O(N2)。 + +```java +public class ThreeSumTwoPointer implements ThreeSum { + + @Override + public int count(int[] nums) { + int N = nums.length; + int cnt = 0; + Arrays.sort(nums); + for (int i = 0; i < N - 2; i++) { + int l = i + 1, h = N - 1, target = -nums[i]; + if (i > 0 && nums[i] == nums[i - 1]) continue; + while (l < h) { + int sum = nums[l] + nums[h]; + if (sum == target) { + cnt++; + while (l < h && nums[l] == nums[l + 1]) l++; + while (l < h && nums[h] == nums[h - 1]) h--; + l++; + h--; + } else if (sum < target) { + l++; + } else { + h--; + } + } + } + return cnt; + } +} +``` + ## 倍率实验 如果 T(N) \~ aNblogN,那么 T(2N)/T(N) \~ 2b。 @@ -180,29 +225,20 @@ public class BinarySearch { public class RatioTest { public static void main(String[] args) { - int N = 500; int loopTimes = 7; double preTime = -1; - while (loopTimes-- > 0) { - int[] nums = new int[N]; - StopWatch.start(); - ThreeSum threeSum = new ThreeSumSlow(); - int cnt = threeSum.count(nums); System.out.println(cnt); - double elapsedTime = StopWatch.elapsedTime(); double ratio = preTime == -1 ? 0 : elapsedTime / preTime; System.out.println(N + " " + elapsedTime + " " + ratio); - preTime = elapsedTime; N *= 2; - } } } @@ -343,7 +379,7 @@ public class Insertion> extends Sort { 对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,每次只能将逆序数量减少 1。 -希尔排序的出现就是为了改进插入排序的这种局限性,它通过交换不相邻的元素,每次可以将逆序数量减少大于 1。 +希尔排序的出现就是为了解决插入排序的这种局限性,它通过交换不相邻的元素,每次可以将逆序数量减少大于 1。 希尔排序使用插入排序对间隔 h 的序列进行排序。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。 @@ -535,15 +571,15 @@ private int partition(T[] nums, int l, int h) { ### 4. 算法改进 -(一)切换到插入排序 +#### 4.1 切换到插入排序 因为快速排序在小数组中也会递归调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。 -(二)三数取中 +#### 4.2 三数取中 最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。人们发现取 3 个元素并将大小居中的元素作为切分元素的效果最好。 -(三)三向切分 +#### 4.3 三向切分 对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。 @@ -609,7 +645,7 @@ public T select(T[] nums, int k) { 堆的某个节点的值总是大于等于子节点的值,并且堆是一颗完全二叉树。 -堆可以用数组来表示,因为堆是完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。 +堆可以用数组来表示,这是因为堆是完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。

@@ -703,15 +739,15 @@ public T delMax() { ### 5. 堆排序 -由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序。并且堆排序是原地排序,不占用额外空间。 +把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列,这就是堆排序。 -(一)构建堆 +#### 5.1 构建堆 -无序数组建立堆最直接的方法是从左到右遍历数组,然后进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。 +无序数组建立堆最直接的方法是从左到右遍历数组进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。

-(二)交换堆顶元素与最后一个元素 +#### 5.2 交换堆顶元素与最后一个元素 交换之后需要进行下沉操作维持堆的有序状态。 @@ -768,7 +804,7 @@ public class HeapSort> extends Sort { ### 1. 排序算法的比较 -| 算法 | 稳定 | 时间复杂度 | 空间复杂度 | 备注 | +| 算法 | 稳定性 | 时间复杂度 | 空间复杂度 | 备注 | | :---: | :---: |:---: | :---: | :---: | | 选择排序 | × | N2 | 1 | | | 冒泡排序 | √ | N2 | 1 | | @@ -779,7 +815,7 @@ public class HeapSort> extends Sort { | 归并排序 | √ | NlogN | N | | | 堆排序 | × | NlogN | 1 | | | -快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 +快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN,这里的 c 比其它线性对数级别的排序算法都要小。使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 ### 2. Java 的排序算法实现 @@ -846,7 +882,6 @@ public class QuickFindUF extends UF { @Override public void union(int p, int q) { - int pID = find(p); int qID = find(q); @@ -881,7 +916,6 @@ public class QuickUnionUF extends UF { @Override public int find(int p) { - while (p != id[p]) { p = id[p]; } @@ -891,7 +925,6 @@ public class QuickUnionUF extends UF { @Override public void union(int p, int q) { - int pRoot = find(p); int qRoot = find(q); @@ -902,7 +935,7 @@ public class QuickUnionUF extends UF { } ``` -这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为触点的数目。 +这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为节点的数目。

@@ -1290,7 +1323,7 @@ public class ListQueue implements MyQueue { -# 六、查找 +# 六、符号表 符号表(Symbol Table)是一种存储键值对的数据结构,可以支持快速查找操作。 @@ -1411,9 +1444,9 @@ public class ListUnorderedST implements UnorderedST { 使用一对平行数组,一个存储键一个存储值。 -rank() 方法至关重要,当键在表中时,它能够知道该键的位置;当键不在表中时,它也能知道在何处插入新键。 +二分查找的 rank() 方法至关重要,当键在表中时,它能够知道该键的位置;当键不在表中时,它也能知道在何处插入新键。 -复杂度:二分查找最多需要 logN+1 次比较,使用二分查找实现的符号表的查找操作所需要的时间最多是对数级别的。但是插入操作需要移动数组元素,是线性级别的。 +二分查找最多需要 logN+1 次比较,使用二分查找实现的符号表的查找操作所需要的时间最多是对数级别的。但是插入操作需要移动数组元素,是线性级别的。 ```java public class BinarySearchOrderedST, Value> implements OrderedST { @@ -1552,7 +1585,7 @@ public class BST, Value> implements OrderedST
@@ -1770,9 +1803,9 @@ private List keys(Node x, Key l, Key h) { } ``` -### 10. 性能分析 +### 10. 分析 -复杂度:二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。 +二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。 ## 2-3 查找树 @@ -1802,7 +1835,7 @@ private List keys(Node x, Key l, Key h) { ## 红黑树 -2-3 查找树需要用到 2- 节点和 3- 节点,红黑树使用红链接来实现 3- 节点。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。 +红黑树是 2-3 查找树,但它不需要分别定义 2- 节点和 3- 节点,而是在普通的二叉查找树之上,为节点添加颜色。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。

@@ -1817,6 +1850,7 @@ private List keys(Node x, Key l, Key h) { ```java public class RedBlackBST, Value> extends BST { + private static final boolean RED = true; private static final boolean BLACK = false; @@ -1972,16 +2006,17 @@ int hash = (((day * R + month) % M) * R + year) % M; R 通常取 31。 -Java 中的 hashCode() 实现了 hash 函数,但是默认使用对象的内存地址值。在使用 hashCode() 函数时,应当结合除留余数法来使用。因为内存地址是 32 位整数,我们只需要 31 位的非负整数,因此应当屏蔽符号位之后再使用除留余数法。 +Java 中的 hashCode() 实现了哈希函数,但是默认使用对象的内存地址值。在使用 hashCode() 时,应当结合除留余数法来使用。因为内存地址是 32 位整数,我们只需要 31 位的非负整数,因此应当屏蔽符号位之后再使用除留余数法。 ```java int hash = (x.hashCode() & 0x7fffffff) % M; ``` -使用 Java 自带的 HashMap 等自带的哈希表实现时,只需要去实现 Key 类型的 hashCode() 函数即可。Java 规定 hashCode() 能够将键均匀分布于所有的 32 位整数,Java 中的 String、Integer 等对象的 hashCode() 都能实现这一点。以下展示了自定义类型如何实现 hashCode(): +使用 Java 的 HashMap 等自带的哈希表实现时,只需要去实现 Key 类型的 hashCode() 函数即可。Java 规定 hashCode() 能够将键均匀分布于所有的 32 位整数,Java 中的 String、Integer 等对象的 hashCode() 都能实现这一点。以下展示了自定义类型如何实现 hashCode(): ```java public class Transaction { + private final String who; private final Date when; private final double amount; @@ -2003,17 +2038,17 @@ public class Transaction { } ``` -### 2. 基于拉链法的散列表 +### 2. 拉链法 拉链法使用链表来存储 hash 值相同的键,从而解决冲突。 查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。 -对于 N 个键,M 条链表 (N>M),如果 hash 函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。 +对于 N 个键,M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。

-### 3. 基于线性探测法的散列表 +### 3. 线性探测法 线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。 @@ -2023,6 +2058,7 @@ public class Transaction { ```java public class LinearProbingHashST implements UnorderedST { + private int N = 0; private int M = 16; private Key[] keys; @@ -2048,7 +2084,7 @@ public class LinearProbingHashST implements UnorderedST } ``` -**(一)查找** +#### 3.1 查找 ```java public Value get(Key key) { @@ -2060,7 +2096,7 @@ public Value get(Key key) { } ``` -**(二)插入** +#### 3.2 插入 ```java public void put(Key key, Value value) { @@ -2082,7 +2118,7 @@ private void putInternal(Key key, Value value) { } ``` -**(三)删除** +#### 3.3 删除 删除操作应当将右侧所有相邻的键值对重新插入散列表中。 @@ -2115,7 +2151,7 @@ public void delete(Key key) { } ``` -**(四)调整数组大小** +#### 3.5 调整数组大小 线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。例如下图中 2\~5 位置就是一个聚簇。 @@ -2199,15 +2235,15 @@ public class SparseVector { 这是一个经典的递归问题,分为三步求解: -- 将 n-1 个圆盘从 from -> buffer +① 将 n-1 个圆盘从 from -> buffer

-- 将 1 个圆盘从 from -> to +② 将 1 个圆盘从 from -> to

-- 将 n-1 个圆盘从 buffer -> to +③ 将 n-1 个圆盘从 buffer -> to

@@ -2245,20 +2281,20 @@ from H1 to H3 ## 哈夫曼编码 -哈夫曼编码根据数据出现的频率对数据进行编码,从而压缩原始数据。 +根据数据出现的频率对数据进行编码,从而压缩原始数据。 -例如对于文本文件,其中各种字符出现的次数如下: +例如对于一个文本文件,其中各种字符出现的次数如下: - a : 10 - b : 20 - c : 40 - d : 80 -可以将每种字符转换成二进制编码,例如将 a 转换为 00,b 转换为 01,c 转换为 10,d 转换为 11。这是最简单的一种编码方式,没有考虑各个字符的权值(出现频率)。而哈夫曼编码能让出现频率最高的字符的编码最短,从而保证整体的编码长度最短。 +可以将每种字符转换成二进制编码,例如将 a 转换为 00,b 转换为 01,c 转换为 10,d 转换为 11。这是最简单的一种编码方式,没有考虑各个字符的权值(出现频率)。而哈夫曼编码采用了贪心策略,使出现频率最高的字符的编码最短,从而保证整体的编码长度最短。 首先生成一颗哈夫曼树,每次生成过程中选取频率最少的两个节点,生成一个新节点作为它们的父节点,并且新节点的频率为两个节点的和。选取频率最少的原因是,生成过程使得先选取的节点在树的最底层,那么需要的编码长度更长,频率更少可以使得总编码长度更少。 -生成编码时,从根节点出发,向左遍历则添加二进制位 0,向右则添加二进制位 1,直到遍历到根节点,根节点代表的字符的编码就是这个路径编码。 +生成编码时,从根节点出发,向左遍历则添加二进制位 0,向右则添加二进制位 1,直到遍历到叶子节点,叶子节点代表的字符的编码就是这个路径编码。

diff --git a/notes/缓存.md b/notes/缓存.md index ed834253..26d96be1 100644 --- a/notes/缓存.md +++ b/notes/缓存.md @@ -26,16 +26,16 @@ ## 淘汰策略 -- FIFO(First In First Out):先进先出策略,在实时性的场景下,需要经常访问最新的数据,那么就可以使用 FIFO,使最先进入的数据(最晚的数据)被淘汰。 +- FIFO(First In First Out):先进先出策略,在实时性的场景下,需要经常访问最新的数据,那么就可以使用 FIFO,使得最先进入的数据(最晚的数据)被淘汰。 -- LRU(Least Recently Used):最近最久未使用策略,优先淘汰最久未使用的数据,也就是上次被访问时间距离现在最远的数据。该策略可以保证内存中的数据都是热点数据,也就是经常被访问的数据,从而保证缓存命中率。 +- LRU(Least Recently Used):最近最久未使用策略,优先淘汰最久未使用的数据,也就是上次被访问时间距离现在最久的数据。该策略可以保证内存中的数据都是热点数据,也就是经常被访问的数据,从而保证缓存命中率。 # 二、LRU -以下是一个基于 双向链表 + HashMap 的 LRU 算法实现,对算法的解释如下: +以下是基于 双向链表 + HashMap 的 LRU 算法实现,对算法的解释如下: -- 最基本的思路是当访问某个节点时,将其从原来的位置删除,并重新插入到链表头部,这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就删除链表尾部的节点。 -- 为了使删除操作时间复杂度为 O(1),那么就不能采用遍历的方式找到某个节点。HashMap 存储着 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。 +- 访问某个节点时,将其从原来的位置删除,并重新插入到链表头部。这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就淘汰链表尾部的节点。 +- 为了使删除操作时间复杂度为 O(1),就不能采用遍历的方式找到某个节点。HashMap 存储着 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。 ```java public class LRU implements Iterable { @@ -152,7 +152,7 @@ public class LRU implements Iterable { ## 反向代理 -反向代理位于服务器之前,请求与响应都需要经过反向代理。通过将数据缓存在反向代理,在用户请求时就可以直接使用缓存进行响应。 +反向代理位于服务器之前,请求与响应都需要经过反向代理。通过将数据缓存在反向代理,在用户请求反向代理时就可以直接使用缓存进行响应。 ## 本地缓存 @@ -166,7 +166,7 @@ public class LRU implements Iterable { ## 数据库缓存 -MySQL 等数据库管理系统具有自己的查询缓存机制来提高 SQL 查询效率。 +MySQL 等数据库管理系统具有自己的查询缓存机制来提高查询效率。 # 四、CDN @@ -193,9 +193,9 @@ CDN 主要有以下优点: ## 缓存雪崩 -指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都去到达数据库。 +指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。 -在存在缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。 +在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。 解决方案: @@ -233,7 +233,7 @@ CDN 主要有以下优点: # 七、一致性哈希 -Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据失效的问题。 +Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据迁移的问题。 ## 基本原理 @@ -241,7 +241,7 @@ Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了

-一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将它前一个节点 C 上的数据重新进行分布即可,对于节点 A、B、D 都没有影响。 +一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将它后一个节点 C 上的数据重新进行分布即可,对于节点 A、B、D 都没有影响。

@@ -249,7 +249,9 @@ Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了 上面描述的一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。 -数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得大,那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。 +数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。 + +解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得多,那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。 # 参考资料 diff --git a/notes/计算机操作系统.md b/notes/计算机操作系统.md index 6bfb559c..73f67c62 100644 --- a/notes/计算机操作系统.md +++ b/notes/计算机操作系统.md @@ -58,7 +58,11 @@ 虚拟技术把一个物理实体转换为多个逻辑实体。 -主要有两种虚拟技术:时分复用技术和空分复用技术。例如多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换。 +主要有两种虚拟技术:时分复用技术和空分复用技术。 + +多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换。 + +虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间和物理内存使用页进行交换,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。 ### 4. 异步 @@ -791,12 +795,11 @@ FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问

- ### 6. 时钟 > Clock -第二次机会算法需要在链表中移动页面,降低了效率。时钟算法使用环形链表将页面链接起来,再使用一个指针指向最老的页面。 +第二次机会算法需要在链表中移动页面,降低了效率。时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面。

diff --git a/notes/计算机网络.md b/notes/计算机网络.md index e80ba7f8..3a08e2a0 100644 --- a/notes/计算机网络.md +++ b/notes/计算机网络.md @@ -59,7 +59,7 @@ 网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网是网络的网络。 -

+

## ISP @@ -67,9 +67,7 @@

-目前的互联网是一种多层次 ISP 结构,ISP 根据覆盖面积的大小分为第一层 ISP、区域 ISP 和接入 ISP。 - -互联网交换点 IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。 +目前的互联网是一种多层次 ISP 结构,ISP 根据覆盖面积的大小分为第一层 ISP、区域 ISP 和接入 ISP。互联网交换点 IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。

@@ -125,7 +123,7 @@ ## 计算机网络体系结构* -

+

### 1. 五层协议 @@ -143,7 +141,7 @@ 其中表示层和会话层用途如下: -- **表示层** :数据压缩、加密以及数据描述,这使得应用程序不必担心在各台主机中数据内部格式不同的问题。 +- **表示层** :数据压缩、加密以及数据描述,这使得应用程序不必关心在各台主机中数据内部格式不同的问题。 - **会话层** :建立及管理会话。 @@ -306,7 +304,7 @@ PPP 的帧格式: MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标识网络适配器(网卡)。 -一台主机拥有多少个适配器就有多少个 MAC 地址。例如笔记本电脑普遍存在无线网络适配器和有线网络适配器,因此就有两个 MAC 地址。 +一台主机拥有多少个网络适配器就有多少个 MAC 地址。例如笔记本电脑普遍存在无线网络适配器和有线网络适配器,因此就有两个 MAC 地址。 ## 局域网 @@ -322,7 +320,7 @@ MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标 以太网是一种星型拓扑结构局域网。 -早期使用集线器进行连接,集线器是一种物理层设备,作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离,之后再将这个比特发送到其它所有接口。如果集线器同时收到同时从两个不同接口的帧,那么就发生了碰撞。 +早期使用集线器进行连接,集线器是一种物理层设备, 作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离,之后再将这个比特发送到其它所有接口。如果集线器同时收到同时从两个不同接口的帧,那么就发生了碰撞。 目前以太网使用交换机替代了集线器,交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。 @@ -490,7 +488,7 @@ Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报 - 172.16.0.0 \~ 172.31.255.255 - 192.168.0.0 \~ 192.168.255.255 -VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指“好像是”,而实际上并不是,它有经过公用的互联网。 +VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指好像是,而实际上并不是,它有经过公用的互联网。 下图中,场所 A 和 B 的通信经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信,IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1,R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。 @@ -700,7 +698,7 @@ TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文

-TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。 +TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。 发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。 @@ -713,11 +711,11 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、 ### 1. 慢开始与拥塞避免 -发送的最初执行慢开始,令 cwnd=1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 ... +发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 ... -注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。 +注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能性也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。 -如果出现了超时,则令 ssthresh = cwnd/2,然后重新执行慢开始。 +如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。 ### 2. 快重传与快恢复 @@ -725,7 +723,7 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、 在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。 -在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。 +在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。 慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。 @@ -743,8 +741,8 @@ DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转 DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传来保证可靠性。在两种情况下会使用 TCP 进行传输: -- 如果返回的响应超过的 512 字节就改用 TCP 进行传输(UDP 最大只支持 512 字节的数据)。 -- 区域传送需要使用 TCP 进行传输(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。 +- 如果返回的响应超过的 512 字节(UDP 最大只支持 512 字节的数据)。 +- 区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。 ## 文件传送协议 @@ -806,7 +804,7 @@ POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删 ### 3. IMAP -IMAP 协议中客户端和服务器上的邮件保持同步,如果不去手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。 +IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。 ## 常用端口 diff --git a/notes/设计模式.md b/notes/设计模式.md index 531595e3..82c09e6d 100644 --- a/notes/设计模式.md +++ b/notes/设计模式.md @@ -44,11 +44,11 @@ ## 1. 单例(Singleton) -### 意图 +### Intent 确保一个类只有一个实例,并提供该实例的全局访问点。 -### 类图 +### Class Diagram 使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。 @@ -56,13 +56,13 @@

-### 实现 +### Implementation -(一)懒汉式-线程不安全 +#### Ⅰ 懒汉式-线程不安全 以下实现中,私有静态变量 uniqueInstance 被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。 -这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 `if (uniqueInstance == null)` ,并且此时 uniqueInstance 为 null,那么会有多个线程执行 `uniqueInstance = new Singleton();` 语句,这将导致多次实例化 uniqueInstance。 +这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 `if (uniqueInstance == null)` ,并且此时 uniqueInstance 为 null,那么会有多个线程执行 `uniqueInstance = new Singleton();` 语句,这将导致实例化多次 uniqueInstance。 ```java public class Singleton { @@ -81,9 +81,9 @@ public class Singleton { } ``` -(二)饿汉式-线程安全 +#### Ⅱ 饿汉式-线程安全 -线程不安全问题主要是由于 uniqueInstance 被多次实例化,采取直接实例化 uniqueInstance 的方式就不会产生线程不安全问题。 +线程不安全问题主要是由于 uniqueInstance 被实例化多次,采取直接实例化 uniqueInstance 的方式就不会产生线程不安全问题。 但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。 @@ -91,11 +91,11 @@ public class Singleton { private static Singleton uniqueInstance = new Singleton(); ``` -(三)懒汉式-线程安全 +#### Ⅲ 懒汉式-线程安全 -只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了多次实例化 uniqueInstance 的问题。 +只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 uniqueInstance。 -但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,因此性能上有一定的损耗。 +但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 uniqueInstance 已经被实例化了。这会让线程阻塞时间过程,因此该方法有性能问题,不推荐使用。 ```java public static synchronized Singleton getUniqueInstance() { @@ -106,7 +106,7 @@ public static synchronized Singleton getUniqueInstance() { } ``` -(四)双重校验锁-线程安全 +#### Ⅳ 双重校验锁-线程安全 uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。 @@ -133,7 +133,7 @@ public class Singleton { } ``` -考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 `uniqueInstance = new Singleton();` 这条语句,只是先后的问题,那么就会进行两次实例化,从而产生了两个实例。因此必须使用双重校验锁,也就是需要使用两个 if 语句。 +考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 `uniqueInstance = new Singleton();` 这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if 语句。 ```java if (uniqueInstance == null) { @@ -145,19 +145,19 @@ if (uniqueInstance == null) { uniqueInstance 采用 volatile 关键字修饰也是很有必要的。`uniqueInstance = new Singleton();` 这段代码其实是分为三步执行。 -1. 分配内存空间 -2. 初始化对象 +1. 为 uniqueInstance 分配内存空间 +2. 初始化 uniqueInstance 3. 将 uniqueInstance 指向分配的内存地址 -但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 1>3>2,这在单线程情况下自然是没有问题。但如果是多线程下,有可能获得是一个还没有被初始化的实例,以致于程序出错。 +但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 -(五)静态内部类实现 +#### Ⅴ 静态内部类实现 -当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 `getUniqueInstance()` 方法从而触发 `SingletonHolder.INSTANCE` 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例。 +当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 `getUniqueInstance()` 方法从而触发 `SingletonHolder.INSTANCE` 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。 -这种方式不仅具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。 +这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。 ```java public class Singleton { @@ -175,40 +175,11 @@ public class Singleton { } ``` -(六)枚举实现 +该实现在多次序列化再进行反序列化之后,不会得到多个实例。而其它实现,为了保证不会出现反序列化之后出现多个实例,需要使用 transient 修饰所有字段,并且实现序列化和反序列化的方法。 -这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次。 +该实现可以防止反射攻击。在其它实现中,通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象,如果要防止这种攻击,需要在构造函数中添加防止实例化第二个对象的代码。但是该实现是由 JVM 保证只会实例化一次,因此不会出现上述的反射攻击。 -```java -public enum Singleton { - uniqueInstance; -} -``` - -考虑以下单例模式的实现,该 Singleton 在每次序列化的时候都会创建一个新的实例,为了保证只创建一个实例,必须声明所有字段都是 transient,并且提供一个 readResolve() 方法。 - -```java -public class Singleton implements Serializable { - - private static Singleton uniqueInstance; - - private Singleton() { - } - - public static synchronized Singleton getUniqueInstance() { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - return uniqueInstance; - } -} -``` - -如果不使用枚举来实现单例模式,会出现反射攻击,因为通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象。如果要防止这种攻击,需要在构造函数中添加防止实例化第二个对象的代码。 - -从上面的讨论可以看出,解决序列化和反射攻击很麻烦,而枚举实现不会出现这两种问题,所以说枚举实现单例模式是最佳实践。 - -### 使用场景 +### Examples - Logger Classes - Configuration Classes @@ -223,11 +194,11 @@ public class Singleton implements Serializable { ## 2. 简单工厂(Simple Factory) -### 意图 +### Intent 在创建一个对象时不向客户暴露内部细节,并提供一个创建对象的通用接口。 -### 类图 +### Class Diagram 简单工厂不是设计模式,更像是一种编程习惯。它把实例化的操作单独放到一个类中,这个类就成为简单工厂类,让简单工厂类来决定应该用哪个具体子类来实例化。 @@ -235,7 +206,7 @@ public class Singleton implements Serializable {

-### 实现 +### Implementation ```java public interface Product { @@ -303,11 +274,11 @@ public class Client { ## 3. 工厂方法(Factory Method) -### 意图 +### Intent 定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法把实例化操作推迟到子类。 -### 类图 +### Class Diagram 在简单工厂中,创建对象的是另一个类,而在工厂方法中,是由子类来创建对象。 @@ -315,7 +286,7 @@ public class Client {

-### 实现 +### Implementation ```java public abstract class Factory { @@ -363,11 +334,11 @@ public class ConcreteFactory2 extends Factory { ## 4. 抽象工厂(Abstract Factory) -### 意图 +### Intent 提供一个接口,用于创建 **相关的对象家族** 。 -### 类图 +### Class Diagram 抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂方法模式只是用于创建一个对象,这和抽象工厂模式有很大不同。 @@ -379,7 +350,7 @@ public class ConcreteFactory2 extends Factory {

-### 代码实现 +### Implementation ```java public class AbstractProductA { @@ -461,15 +432,15 @@ public class Client { ## 5. 生成器(Builder) -### 意图 +### Intent 封装一个对象的构造过程,并允许按步骤构造。 -### 类图 +### Class Diagram

-### 实现 +### Implementation 以下是一个简易的 StringBuilder 实现,参考了 JDK 1.8 源码。 @@ -551,15 +522,15 @@ abcdefghijklmnopqrstuvwxyz ## 6. 原型模式(Prototype) -### 意图 +### Intent 使用原型实例指定要创建对象的类型,通过复制这个原型来创建新对象。 -### 类图 +### Class Diagram

-### 实现 +### Implementation ```java public abstract class Prototype { @@ -610,17 +581,17 @@ abc ## 1. 责任链(Chain Of Responsibility) -### 意图 +### Intent 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链发送该请求,直到有一个对象处理它为止。 -### 类图 +### Class Diagram - Handler:定义处理请求的接口,并且实现后继链(successor)

-### 实现 +### Implementation ```java public abstract class Handler { @@ -724,11 +695,16 @@ request2 is handle by ConcreteHandler2 ## 2. 命令(Command) -### 意图 +### Intent -将命令封装成对象中,以便使用命令来参数化其它对象,或者将命令对象放入队列中进行排队,或者将命令对象的操作记录到日志中,以及支持可撤销的操作。 +将命令封装成对象中,具有以下作用: -### 类图 +- 使用命令来参数化其它对象 +- 将命令放入队列中进行排队 +- 将命令的操作记录到日志中 +- 支持可撤销的操作 + +### Class Diagram - Command:命令 - Receiver:命令接收者,也就是命令真正的执行者 @@ -737,7 +713,7 @@ request2 is handle by ConcreteHandler2

-### 实现 +### Implementation 设计一个遥控器,可以控制电灯开关。 @@ -847,18 +823,18 @@ public class Client { ## 3. 解释器(Interpreter) -### 意图 +### Intent 为语言创建解释器,通常由语言的语法和语法分析来定义。 -### 类图 +### Class Diagram -- TerminalExpression:终结符表达式,每个终结符都需要一个 TerminalExpression -- Context:上下文,包含解释器之外的一些全局信息 +- TerminalExpression:终结符表达式,每个终结符都需要一个 TerminalExpression。 +- Context:上下文,包含解释器之外的一些全局信息。

-### 实现 +### Implementation 以下是一个规则检验器实现,具有 and 和 or 规则,通过规则可以构建一颗解析树,用来检验一个文本是否满足解析树定义的规则。 @@ -971,11 +947,11 @@ false ## 4. 迭代器(Iterator) -### 意图 +### Intent 提供一种顺序访问聚合对象元素的方法,并且不暴露聚合对象的内部表示。 -### 类图 +### Class Diagram - Aggregate 是聚合类,其中 createIterator() 方法可以产生一个 Iterator; - Iterator 主要定义了 hasNext() 和 next() 方法。 @@ -983,7 +959,7 @@ false

-### 实现 +### Implementation ```java public interface Aggregate { @@ -1059,18 +1035,18 @@ public class Client { ## 5. 中介者(Mediator) -### 意图 +### Intent 集中相关对象之间复杂的沟通和控制方式。 -### 类图 +### Class Diagram - Mediator:中介者,定义一个接口用于与各同事(Colleague)对象通信。 - Colleague:同事,相关对象

-### 实现 +### Implementation Alarm(闹钟)、CoffeePot(咖啡壶)、Calendar(日历)、Sprinkler(喷头)是一组相关的对象,在某个对象的事件产生时需要去操作其它对象,形成了下面这种依赖结构: @@ -1228,11 +1204,11 @@ doSprinkler() ## 6. 备忘录(Memento) -### 意图 +### Intent 在不违反封装的情况下获得对象的内部状态,从而在需要时可以将对象恢复到最初状态。 -### 类图 +### Class Diagram - Originator:原始对象 - Caretaker:负责保存好备忘录 @@ -1240,7 +1216,7 @@ doSprinkler()

-### 实现 +### Implementation 以下实现了一个简单计算器程序,可以输入两个值,然后计算这两个值的和。备忘录模式允许将这两个值存储起来,然后在某个时刻用存储的状态进行恢复。 @@ -1405,7 +1381,7 @@ public class Client { ## 7. 观察者(Observer) -### 意图 +### Intent 定义对象之间的一对多依赖,当一个对象状态改变时,它的所有依赖都会收到通知并且自动更新状态。 @@ -1413,7 +1389,7 @@ public class Client {

-### 类图 +### Class Diagram 主题(Subject)具有注册和移除观察者、并通知所有观察者的功能,主题是通过维护一张观察者列表来实现这些操作的。 @@ -1421,7 +1397,7 @@ public class Client {

-### 实现 +### Implementation 天气数据布告板会在天气信息发生改变时更新其内容,布告板有多个,并且在将来会继续增加。 @@ -1540,15 +1516,15 @@ StatisticsDisplay.update: 1.0 1.0 1.0 ## 8. 状态(State) -### 意图 +### Intent 允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它所属的类。 -### 类图 +### Class Diagram

-### 实现 +### Implementation 糖果销售机有多种状态,每种状态下销售机有不同的行为,状态可以发生转移,使得销售机的行为也发生改变。 @@ -1842,16 +1818,16 @@ No gumball dispensed ## 9. 策略(Strategy) -### 意图 +### Intent 定义一系列算法,封装每个算法,并使它们可以互换。 策略模式可以让算法独立于使用它的客户端。 -### 类图 +### Class Diagram - Strategy 接口定义了一个算法族,它们都具有 behavior() 方法。 -- Context 是使用到该算法族的类,其中的 doSomething() 方法会调用 behavior(),setStrategy(in Strategy) 方法可以动态地改变 strategy 对象,也就是说能动态地改变 Context 所使用的算法。 +- Context 是使用到该算法族的类,其中的 doSomething() 方法会调用 behavior(),setStrategy(Strategy) 方法可以动态地改变 strategy 对象,也就是说能动态地改变 Context 所使用的算法。

@@ -1861,7 +1837,7 @@ No gumball dispensed 状态模式主要是用来解决状态转移的问题,当状态发生转移了,那么 Context 对象就会改变它的行为;而策略模式主要是用来封装一组可以互相替代的算法族,并且可以根据需要动态地去替换 Context 使用的算法。 -### 实现 +### Implementation 设计一个鸭子,它可以动态地改变叫声。这里的算法族是鸭子的叫声行为。 @@ -1930,17 +1906,17 @@ quack! ## 10. 模板方法(Template Method) -### 意图 +### Intent 定义算法框架,并将一些步骤的实现延迟到子类。 通过模板方法,子类可以重新定义算法的某些步骤,而不用改变算法的结构。 -### 类图 +### Class Diagram

-### 实现 +### Implementation 冲咖啡和冲茶都有类似的流程,但是某些步骤会有点不一样,要求复用那些相同步骤的代码。 @@ -2031,11 +2007,11 @@ Tea.addCondiments ## 11. 访问者(Visitor) -### 意图 +### Intent 为一个对象结构(比如组合结构)增加新能力。 -### 类图 +### Class Diagram - Visitor:访问者,为每一个 ConcreteElement 声明一个 visit 操作 - ConcreteVisitor:具体访问者,存储遍历过程中的累计结果 @@ -2043,7 +2019,7 @@ Tea.addCondiments

-### 实现 +### Implementation ```java public interface Element { @@ -2238,17 +2214,17 @@ Number of items: 6 ## 12. 空对象(Null) -### 意图 +### Intent 使用什么都不做的空对象来代替 NULL。 一个方法返回 NULL,意味着方法的调用端需要去检查返回值是否是 NULL,这么做会导致非常多的冗余的检查代码。并且如果某一个调用端忘记了做这个检查返回值,而直接使用返回的对象,那么就有可能抛出空指针异常。 -### 类图 +### Class Diagram

-### 实现 +### Implementation ```java public abstract class AbstractOperation { @@ -2294,17 +2270,17 @@ public class Client { ## 1. 适配器(Adapter) -### 意图 +### Intent 把一个类接口转换成另一个用户需要的接口。

-### 类图 +### Class Diagram

-### 实现 +### Implementation 鸭子(Duck)和火鸡(Turkey)拥有不同的叫声,Duck 的叫声调用 quack() 方法,而 Turkey 调用 gobble() 方法。 @@ -2365,18 +2341,18 @@ public class Client { ## 2. 桥接(Bridge) -### 意图 +### Intent 将抽象与实现分离开来,使它们可以独立变化。 -### 类图 +### Class Diagram - Abstraction:定义抽象类的接口 - Implementor:定义实现类接口

-### 实现 +### Implementation RemoteControl 表示遥控器,指代 Abstraction。 @@ -2518,11 +2494,11 @@ public class Client { ## 3. 组合(Composite) -### 意图 +### Intent 将对象组合成树形结构来表示“整体/部分”层次关系,允许用户以相同的方式处理单独对象和组合对象。 -### 类图 +### Class Diagram 组件(Component)类是组合类(Composite)和叶子类(Leaf)的父类,可以把组合类看成是树的中间节点。 @@ -2530,7 +2506,7 @@ public class Client {

-### 实现 +### Implementation ```java public abstract class Component { @@ -2652,17 +2628,17 @@ Composite:root ## 4. 装饰(Decorator) -### 意图 +### Intent 为对象动态添加功能。 -### 类图 +### Class Diagram 装饰者(Decorator)和具体组件(ConcreteComponent)都继承自组件(Component),具体组件的方法实现不需要依赖于其它对象,而装饰者组合了一个组件,这样它可以装饰其它装饰者或者具体组件。所谓装饰,就是把这个装饰者套在被装饰者之上,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。可以看到,具体组件应当是装饰层次的最低层,因为只有具体组件的方法实现不需要依赖于其它对象。

-### 实现 +### Implementation 设计不同种类的饮料,饮料可以添加配料,比如可以添加牛奶,并且支持动态添加新配料。每增加一种配料,该饮料的价格就会增加,要求计算一种饮料的价格。 @@ -2759,15 +2735,15 @@ public class Client { ## 5. 外观(Facade) -### 意图 +### Intent 提供了一个统一的接口,用来访问子系统中的一群接口,从而让子系统更容易使用。 -### 类图 +### Class Diagram

-### 实现 +### Implementation 观看电影需要操作很多电器,使用外观模式实现一键看电影功能。 @@ -2814,11 +2790,11 @@ public class Client { ## 6. 享元(Flyweight) -### 意图 +### Intent 利用共享的方式来支持大量细粒度的对象,这些对象一部分内部状态是相同的。 -### 类图 +### Class Diagram - Flyweight:享元对象 - IntrinsicState:内部状态,享元对象共享内部状态 @@ -2826,7 +2802,7 @@ public class Client {

-### 实现 +### Implementation ```java public interface Flyweight { @@ -2899,11 +2875,11 @@ Java 利用缓存来加速大量小对象的访问时间。 ## 7. 代理(Proxy) -### 意图 +### Intent 控制对其它对象的访问。 -### 类图 +### Class Diagram 代理有以下四类: @@ -2914,7 +2890,7 @@ Java 利用缓存来加速大量小对象的访问时间。

-### 实现 +### Implementation 以下是一个虚拟代理的实现,模拟了图片延迟加载的情况下使用与图片大小相等的临时内容去替换原始图片,直到图片加载完成才将图片显示出来。 diff --git a/notes/重构.md b/notes/重构.md index b5a834e0..e90fa84e 100644 --- a/notes/重构.md +++ b/notes/重构.md @@ -161,6 +161,7 @@ class Customer { ```java class Rental { + private int daysRented; private Movie movie; @@ -199,6 +200,7 @@ class Movie { ```java public class App { + public static void main(String[] args) { Customer customer = new Customer(); Rental rental1 = new Rental(1, new Movie(Movie.Type1)); @@ -236,6 +238,7 @@ public class App { ```java class Customer { + private List rentals = new ArrayList<>(); void addRental(Rental rental) { @@ -254,6 +257,7 @@ class Customer { ```java class Rental { + private int daysRented; private Movie movie; @@ -294,8 +298,6 @@ class Price2 implements Price { ``` ```java -package imp2; - class Price3 implements Price { @Override public double getCharge() { diff --git a/notes/集群.md b/notes/集群.md index 15657e43..338ddcb4 100644 --- a/notes/集群.md +++ b/notes/集群.md @@ -119,7 +119,7 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服 首先了解一下正向代理与反向代理的区别: - 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端; -- 反向代理:发生在服务器端,用户不知道代理的存在。 +- 反向代理:发生在服务器端,用户不知道反向代理的存在。 反向代理服务器位于源服务器前面,用户的请求需要先经过反向代理服务器才能到达源服务器。反向代理可以用来进行缓存、日志记录等,同时也可以用来做为负载均衡服务器。 @@ -153,7 +153,7 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服 通过配置源服务器的虚拟 IP 地址和负载均衡服务器的 IP 地址一致,从而不需要修改 IP 地址就可以进行转发。也正因为 IP 地址一样,所以源服务器的响应不需要转发回负载均衡服务器,可以直接转发给客户端,避免了负载均衡服务器的成为瓶颈。 -这是一种三角传输模式,被称为直接路由,对于提供下载和视频服务的网站来说,直接路由避免了大量的网络传输数据经过负载均衡服务器。 +这是一种三角传输模式,被称为直接路由。对于提供下载和视频服务的网站来说,直接路由避免了大量的网络传输数据经过负载均衡服务器。 这是目前大型网站使用最广负载均衡转发方式,在 Linux 平台可以使用的负载均衡服务器为 LVS(Linux Virtual Server)。 @@ -185,7 +185,6 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服 - 占用过多内存; - 同步过程占用网络带宽以及服务器处理器时间。 -

## Session Server @@ -206,3 +205,4 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服 - [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/) + diff --git a/notes/面向对象思想.md b/notes/面向对象思想.md index 203c7ebf..87b3b033 100644 --- a/notes/面向对象思想.md +++ b/notes/面向对象思想.md @@ -89,24 +89,28 @@ Animal animal = new Cat(); ```java public class Instrument { + public void play() { System.out.println("Instument is playing..."); } } public class Wind extends Instrument { + public void play() { System.out.println("Wind is playing..."); } } public class Percussion extends Instrument { + public void play() { System.out.println("Percussion is playing..."); } } public class Music { + public static void main(String[] args) { List instruments = new ArrayList<>(); instruments.add(new Wind()); @@ -353,4 +357,3 @@ Vihicle .. N - [看懂 UML 类图和时序图](http://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html#generalization) - [UML 系列——时序图(顺序图)sequence diagram](http://www.cnblogs.com/wolf-sun/p/UML-Sequence-diagram.html) - [面向对象编程三大特性 ------ 封装、继承、多态](http://blog.csdn.net/jianyuerensheng/article/details/51602015) -