Merge pull request #3 from CyC2018/master

switch the base
This commit is contained in:
mythsman 2018-03-11 20:20:54 +08:00 committed by GitHub
commit b60cd1313f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 2286 additions and 883 deletions

View File

@ -1,20 +1,15 @@
<!-- <br>
<div align="center">
<img src="https://github.com/CyC2018/InterviewNotes/blob/master/pics/handbook.png" alt="" width="225"/>
<img src="https://github.com/CyC2018/InterviewNotes/blob/master/other/handbook.png" alt="" width="175"/>
<img src="https://img.shields.io/badge/update-today-blue.svg"/> <img src="https://img.shields.io/badge/gitbook-making-yellow.svg"/>
</div>
<br> -->
<!-- <img src="https://github.com/CyC2018/InterviewNotes/blob/master/pics/handbook.png" alt="" width="225"/> -->
<!-- ![](https://img.shields.io/badge/update-today-blue.svg) ![](https://img.shields.io/badge/gitbook-making-yellow.svg) -->
<!-- ![](https://img.shields.io/badge/gitbook-making-green.svg) ![](https://img.shields.io/badge/update-today-blue.svg) -->
![](https://img.shields.io/badge/update-today-blue.svg) ![](https://img.shields.io/badge/gitbook-making-lightgrey.svg)
# 网络 :cloud:
## 网络 :cloud:
> [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md)
@ -24,7 +19,7 @@
整理自《图解 HTTP》
# 操作系统 :computer:
## 操作系统 :computer:
> [计算机操作系统](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机操作系统.md)
@ -34,7 +29,7 @@
整理自《鸟哥的 Linux 私房菜》
# 数据结构与算法 :pencil2:
## 数据结构与算法 :pencil2:
> [算法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/算法.md)
@ -48,7 +43,7 @@
对题目做了一个分类,并对每种题型的解题思路做了总结。已经整理了 300+ 的题目,基本涵盖所有经典题目。
# 面向对象 :couple:
## 面向对象 :couple:
> [设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/设计模式.md)
@ -58,7 +53,11 @@
一些面向对象思想和原则。
# 数据库 :floppy_disk:
## 数据库 :floppy_disk:
> [数据库系统原理](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/数据库系统原理.md)
整理自《数据库系统概论 第四版》
> [SQL 语法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/SQL%20语法.md)
@ -68,7 +67,7 @@
整理自《高性能 MySQL》整理了一些重点内容。
# Java :coffee:
## Java :coffee:
> [JVM](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/JVM.md)
@ -84,13 +83,13 @@
> [Java IO](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20IO.md)
File、InputStream OutputStream、Reader Writer、Serializable、Socket、NIO
File, InputStream OutputStream, Reader Writer, Serializable, Socket, NIO
> [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md)
整理了一些常见考点。
# 编码实践 :hammer:
## 编码实践 :hammer:
> [重构](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/重构.md)
@ -104,25 +103,33 @@ File、InputStream OutputStream、Reader Writer、Serializable、Socket、NIO
Google 开源项目的代码风格规范。
# 资料下载 :arrow_down:
## 资料下载 :arrow_down:
> [百度网盘](https://pan.baidu.com/s/1o9oD1s2#list/path=%2F)
> [Download](https://github.com/CyC2018/Interview-Notebook/blob/master/other/download.md)
一些 PDF 书籍
# 后记 :memo:
## 后记 :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) 进行排版,以保证内容的可读性。这里提供了本人实现的 Markdown 排版工具的下载:[Markdown-Typesetting](https://github.com/CyC2018/Markdown-Typesetting)。
笔记内容按照 [中文文案排版指北](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 目录。并且为了让图片显示效果更好,笔记内容基本使用了 &lt;center> 标记来让图片居中显示,但是 GFM 却不支持 &lt;center> 标记,因此也需要进行一定的转换。这里提供了本人实现的 GFM 文档转换工具的下载:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。
因为大部分内容是一个字一个字打上去的,难免会有一些笔误,如果发现,可以直接在相应的文档上编辑修改。
想要发表反馈意见的话,可以到 [原贴](https://www.nowcoder.com/discuss/66985) 的评论区进行留言
如果觉得内容不够完善或者有写的不好的地方,您可以在 Issues 中发表反馈意见
---
笔记内容可供个人随意使用,转载或者引用请注明出处,毕竟写了很长时间没那么轻松~
<!-- ## Donate
[Alipay](https://github.com/CyC2018/InterviewNotes/blob/master/other/alipay.md)
<img src="./other/alipay.png" alt="" width="150"/> -->
## License
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/3.0/cn/"><img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/3.0/cn/88x31.png" /></a>

View File

@ -57,17 +57,17 @@
URI 包含 URL 和 URN目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。
<br><div align="center"> <img src="../pics//4102b7d0-39b9-48d8-82ae-ac4addb7ebfb.jpg"/> </div><br>
<div align="center"> <img src="../pics//4102b7d0-39b9-48d8-82ae-ac4addb7ebfb.jpg"/> </div><br>
## 请求和响应报文
**请求报文**
<br><div align="center"> <img src="../pics//22b39f77-ac47-4978-91ed-84aaf457644c.jpg"/> </div><br>
<div align="center"> <img src="../pics//22b39f77-ac47-4978-91ed-84aaf457644c.jpg"/> </div><br>
**响应报文**
<br><div align="center"> <img src="../pics//00d8d345-cd4a-48af-919e-209d2788eca7.jpg"/> </div><br>
<div align="center"> <img src="../pics//00d8d345-cd4a-48af-919e-209d2788eca7.jpg"/> </div><br>
# HTTP 方法
@ -120,13 +120,13 @@ GET 的传参方式相比于 POST 安全性较差,因为 GET 传的参数在 U
TRACE 一般不会使用,并且它容易受到 XST 攻击Cross-Site Tracing跨站追踪因此更不会去使用它。
<br><div align="center"> <img src="../pics//c8637fd2-3aaa-46c4-b7d9-f24d3fa04781.jpg"/> </div><br>
<div align="center"> <img src="../pics//c8637fd2-3aaa-46c4-b7d9-f24d3fa04781.jpg"/> </div><br>
## CONNECT要求用隧道协议连接代理
主要使用 SSLSecure Sokets Layer安全套接字和 TLSTransport Layer Security传输层安全协议把通信内容加密后经网络隧道传输。
<br><div align="center"> <img src="../pics//5994928c-3d2d-45bd-abb1-adc4f5f4d775.jpg"/> </div><br>
<div align="center"> <img src="../pics//5994928c-3d2d-45bd-abb1-adc4f5f4d775.jpg"/> </div><br>
# HTTP 状态码
@ -168,7 +168,7 @@ TRACE 一般不会使用,并且它容易受到 XST 攻击Cross-Site Tracing
- **401 Unauthorized** :该状态码表示发送的请求需要有通过 HTTP 认证BASIC 认证、DIGEST 认证)的认证信息。如果之前已进行过一次请求,则表示用户认证失败。
<br><div align="center"> <img src="../pics//b1b4cf7d-c54a-4ff1-9741-cd2eea331123.jpg"/> </div><br>
<div align="center"> <img src="../pics//b1b4cf7d-c54a-4ff1-9741-cd2eea331123.jpg"/> </div><br>
- **403 Forbidden** :请求被拒绝,服务器端没有必要给出拒绝的详细理由。
@ -261,7 +261,7 @@ HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使
服务器发送的响应报文包含 Set-Cookie 字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。下次再发送请求时,从浏览器中读出 Cookie 值,在请求报文中包含 Cookie 字段这样服务器就知道客户端的状态信息了。Cookie 状态信息保存在客户端浏览器中,而不是服务器上。
<br><div align="center"> <img src="../pics//ff17c103-750a-4bb8-9afa-576327023af9.png"/> </div><br>
<div align="center"> <img src="../pics//ff17c103-750a-4bb8-9afa-576327023af9.png"/> </div><br>
Set-Cookie 字段有以下属性:
@ -298,13 +298,13 @@ Expires 字段可以用于告知缓存服务器该资源什么时候会过期。
当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。 **持久连接** 只需要进行一次 TCP 连接就能进行多次 HTTP 通信。HTTP/1.1 开始,所有的连接默认都是持久连接。
<br><div align="center"> <img src="../pics//c73a0b78-5f46-4d2d-a009-dab2a999b5d8.jpg"/> </div><br>
<div align="center"> <img src="../pics//c73a0b78-5f46-4d2d-a009-dab2a999b5d8.jpg"/> </div><br>
持久连接需要使用 Connection 首部字段进行管理。HTTP/1.1 开始 HTTP 默认是持久化连接的,如果要断开 TCP 连接,需要由客户端或者服务器端提出断开,使用 Connection: close而在 HTTP/1.1 之前默认是非持久化连接的,如果要维持持续连接,需要使用 Keep-Alive。
管线化方式可以同时发送多个请求和响应,而不需要发送一个请求然后等待响应之后再发下一个请求。
<br><div align="center"> <img src="../pics//6943e2af-5a70-4004-8bee-b33d60f39da3.jpg"/> </div><br>
<div align="center"> <img src="../pics//6943e2af-5a70-4004-8bee-b33d60f39da3.jpg"/> </div><br>
## 编码
@ -320,7 +320,7 @@ Expires 字段可以用于告知缓存服务器该资源什么时候会过期。
例如,上传多个表单时可以使用如下方式:
<br><div align="center"> <img src="../pics//2279cc60-9714-4e0e-aac9-4c348e0c2165.png"/> </div><br>
<div align="center"> <img src="../pics//2279cc60-9714-4e0e-aac9-4c348e0c2165.png"/> </div><br>
## 范围请求
@ -348,19 +348,19 @@ Expires 字段可以用于告知缓存服务器该资源什么时候会过期。
使用代理的主要目的是:缓存、网络访问控制以及访问日志记录。
<br><div align="center"> <img src="../pics//c07035c3-a9ba-4508-8e3c-d8ae4c6ee9ee.jpg"/> </div><br>
<div align="center"> <img src="../pics//c07035c3-a9ba-4508-8e3c-d8ae4c6ee9ee.jpg"/> </div><br>
**网关**
与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。
<br><div align="center"> <img src="../pics//81375888-6be1-476f-9521-42eea3e3154f.jpg"/> </div><br>
<div align="center"> <img src="../pics//81375888-6be1-476f-9521-42eea3e3154f.jpg"/> </div><br>
**隧道**
使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。
<br><div align="center"> <img src="../pics//64b95403-d976-421a-8b45-bac89c0b5185.jpg"/> </div><br>
<div align="center"> <img src="../pics//64b95403-d976-421a-8b45-bac89c0b5185.jpg"/> </div><br>
# HTTPs
@ -380,7 +380,7 @@ HTTPs 并不是新协议,而是 HTTP 先和 SSLSecure Socket Layer通信
HTTPs 采用 **混合的加密机制** ,使用公开密钥加密用于传输对称密钥,之后使用对称密钥加密进行通信。(下图中,共享密钥即对称密钥)
<br><div align="center"> <img src="../pics//110b1a9b-87cd-45c3-a21d-824623715b33.jpg"/> </div><br>
<div align="center"> <img src="../pics//110b1a9b-87cd-45c3-a21d-824623715b33.jpg"/> </div><br>
## 认证

View File

@ -19,9 +19,9 @@
* [1.3 方法区的回收](#13-方法区的回收)
* [1.4 finalize()](#14-finalize)
* [2. 垃圾收集算法](#2-垃圾收集算法)
* [2.1 标记 - 清除算法](#21-标记---清除算法)
* [2.1 标记-清除算法](#21-标记-清除算法)
* [2.2 复制算法](#22-复制算法)
* [2.3 标记 - 整理算法](#23-标记---整理算法)
* [2.3 标记-整理算法](#23-标记-整理算法)
* [2.4 分代收集算法](#24-分代收集算法)
* [3. 垃圾收集器](#3-垃圾收集器)
* [3.1 Serial 收集器](#31-serial-收集器)
@ -38,12 +38,12 @@
* [4.3 长期存活的对象进入老年代](#43-长期存活的对象进入老年代)
* [4.4 动态对象年龄判定](#44-动态对象年龄判定)
* [4.5 空间分配担保](#45-空间分配担保)
* [4.6 Full GC 的触发条件](#46-full-gc-的触发条件)
* [4.6.1 调用 System.gc()](#461-调用-systemgc)
* [4.6.2 老年代空间不足](#462-老年代空间不足)
* [4.6.3 空间分配担保失败](#463-空间分配担保失败)
* [4.6.4 JDK 1.7 及以前的永久代空间不足](#464-jdk-17-及以前的永久代空间不足)
* [4.6.5 Concurrent Mode Failure](#465-concurrent-mode-failure)
* [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-类初始化时机)
@ -65,7 +65,7 @@
# 内存模型
<br><div align="center"> <img src="../pics//dc695f48-4189-4fc7-b950-ed25f6c80f82.jpg"/> </div><br>
<div align="center"> <img src="../pics//dc695f48-4189-4fc7-b950-ed25f6c80f82.jpg"/> </div><br>
注:白色区域为线程私有的,蓝色区域为线程共享的。
@ -116,19 +116,15 @@
# 垃圾收集
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。
垃圾回收主要是针对 Java 堆和方法区进行。
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。
## 1. 判断一个对象是否可回收
### 1.1 引用计数
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1引用失效时计数器减 1。
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1引用失效时计数器减 1。引用计数为 0 的对象可被回收。
引用计数为 0 的对象可被回收。
两个对象会出现循环引用问题,此时引用计数器永远不为 0导致 GC 收集器无法回收。
两个对象出现循环引用的情况下,此时引用计数器永远不为 0导致 GC 收集器无法回收。
```java
objA.instance = objB;
@ -148,59 +144,61 @@ GC Roots 一般包含以下内容:
### 1.3 引用类型
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定独享是否存活都与“引用”有关。
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。
Java 对引用的概念进行了扩充,引入四种强度不同的引用类型。
#### 1.3.1 强引用
只要强引用存在,垃圾回收器永远不会回收调掉被引用的对象。
使用 new 一个新对象的方式来创建强引用。
```java
Object obj = new Object();
```
#### 1.3.2 软引用
用来描述一些还有用但是并非必需的对象。
非必须引用,内存溢出之前进行回收。
在系统将要发生内存溢出异常之前,将会对这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出溢出异常。
软引用主要用来实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源获取数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源获取这些数据。
使用 SoftReference 类来实现软引用。
```java
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();
WeakReference<Object> wf = new WeakReference<Object>(obj);
```
sf 是对 obj 的一个软引用,通过 sf.get() 方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回 null
软引用主要用来实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
#### 1.3.3 弱引用
只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会被回收。
使用 WeakReference 类来实现弱引用。
```java
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();
wf.isEnQueued();
```
#### 1.3.4 虚引用
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
又称为幽灵引用或者幻影引用.一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
使用 PhantomReference 来实现虚引用。
```java
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();
pf.isEnQueued();
```
### 1.3 方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代差很多,因此在方法区上进行回收性价比不高。
在方法区主要是对常量池的回收和对类的卸载。
常量池的回收和堆中对象回收类似。
@ -217,15 +215,15 @@ pf.isEnQueued();
### 1.4 finalize()
当一个对象可被回收时,如果该对象有必要执行 finalize() 方法,那么就有可能可能通过在该方法中让对象重新被引用,从而实现自救。
finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能可能通过在该方法中让对象重新被引用,从而实现自救。
## 2. 垃圾收集算法
### 2.1 标记 - 清除算法
### 2.1 标记-清除算法
<br><div align="center"> <img src="../pics//a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg"/> </div><br>
<div align="center"> <img src="../pics//a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg"/> </div><br>
将需要回收的对象进行标记,然后清除。
@ -238,38 +236,38 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
### 2.2 复制算法
<br><div align="center"> <img src="../pics//e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg"/> </div><br>
<div align="center"> <img src="../pics//e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg"/> </div><br>
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,需要依赖于老年代进行分配担保,也就是借用老年代的空间。
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间。
### 2.3 标记 - 整理算法
### 2.3 标记-整理算法
<br><div align="center"> <img src="../pics//902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg"/> </div><br>
<div align="center"> <img src="../pics//902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg"/> </div><br>
让所有存活的对象都向一移动,然后直接清理掉端边界以外的内存。
让所有存活的对象都向一移动,然后直接清理掉端边界以外的内存。
### 2.4 分代收集算法
现在的商业虚拟机采用分代收集算法,它使用了前面介绍的几种收集算法,根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将 Java 堆分为新生代和老年代。
1. 新生代使用:复制算法
2. 老年代使用:标记 - 清理 或者 标记 - 整理 算法。
2. 老年代使用:标记-清理 或者 标记-整理 算法。
## 3. 垃圾收集器
<br><div align="center"> <img src="../pics//c625baa0-dde6-449e-93df-c3a67f2f430f.jpg"/> </div><br>
<div align="center"> <img src="../pics//c625baa0-dde6-449e-93df-c3a67f2f430f.jpg"/> </div><br>
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
### 3.1 Serial 收集器
<br><div align="center"> <img src="../pics//22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg"/> </div><br>
<div align="center"> <img src="../pics//22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg"/> </div><br>
它是单线程的收集器,不仅意味着只会使用一个线程进行垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停所有其他工作线程,往往造成过长的等待时间。
@ -279,7 +277,7 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
### 3.2 ParNew 收集器
<br><div align="center"> <img src="../pics//81538cd5-1bcf-4e31-86e5-e198df1e013b.jpg"/> </div><br>
<div align="center"> <img src="../pics//81538cd5-1bcf-4e31-86e5-e198df1e013b.jpg"/> </div><br>
它是 Serial 收集器的多线程版本。
@ -301,7 +299,7 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
### 3.4 Serial Old 收集器
<br><div align="center"> <img src="../pics//08f32fd3-f736-4a67-81ca-295b2a7972f2.jpg"/> </div><br>
<div align="center"> <img src="../pics//08f32fd3-f736-4a67-81ca-295b2a7972f2.jpg"/> </div><br>
Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
@ -310,7 +308,7 @@ Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下
### 3.5 Parallel Old 收集器
<br><div align="center"> <img src="../pics//278fe431-af88-4a95-a895-9c3b80117de3.jpg"/> </div><br>
<div align="center"> <img src="../pics//278fe431-af88-4a95-a895-9c3b80117de3.jpg"/> </div><br>
是 Parallel Scavenge 收集器的老年代版本。
@ -318,9 +316,9 @@ Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下
### 3.6 CMS 收集器
<br><div align="center"> <img src="../pics//62e77997-6957-4b68-8d12-bfd609bb2c68.jpg"/> </div><br>
<div align="center"> <img src="../pics//62e77997-6957-4b68-8d12-bfd609bb2c68.jpg"/> </div><br>
CMSConcurrent Mark Sweep从 Mark Sweep 可以知道它是基于 标记 - 清除 算法实现的。
CMSConcurrent Mark Sweep从 Mark Sweep 可以知道它是基于标记-清除算法实现的。
特点:并发收集、低停顿。
@ -339,11 +337,11 @@ CMSConcurrent 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 收集器
<br><div align="center"> <img src="../pics//f99ee771-c56f-47fb-9148-c0036695b5fe.jpg"/> </div><br>
<div align="center"> <img src="../pics//f99ee771-c56f-47fb-9148-c0036695b5fe.jpg"/> </div><br>
G1Garbage-First收集器是当今收集器技术发展最前沿的成果之一它是一款面向服务端应用的垃圾收集器HotSpot 开发团队赋予它的使命是(在比较长期的)未来可以替换掉 JDK 1.5 中发布的 CMS 收集器。
@ -372,58 +370,65 @@ Region 不可能是孤立的,一个对象分配在某个 Region 中,可以
| 收集器 | 串行、并行 or 并发 | 新生代 / 老年代 | 算法 | 目标 | 适用场景 |
| --- | --- | --- | --- | --- | --- |
| **Serial** | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单 CPU 环境下的 Client 模式 |
| **Serial Old** | 串行 | 老年代 | 标记 - 整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 |
| **Serial Old** | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 |
| **ParNew** | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 |
| **Parallel Scavenge** | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| **Parallel Old** | 并行 | 老年代 | 标记 - 整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| **CMS** | 并发 | 老年代 | 标记 - 清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 |
| **G1** | 并发 | both | 标记 - 整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS |
| **Parallel Old** | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| **CMS** | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 |
| **G1** | 并发 | both | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS |
## 4. 内存分配与回收策略
对象的内存分配,也就是在堆上分配。主要分配在新生代的 Eden 区上,少数情况下也可能直接分配在老年代中。
### 4.1 优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC
关于 Minor GC 和 Full GC
- Minor GC发生在新生代上因为新生代对象存活时间很短因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
- Full GC发生在老年代上老年代对象和新生代的相反其存活时间长因此 Full GC 很少执行,而且执行速度会比 Minor GC 慢很多。
### 4.2 大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
提供 -XX:PretenureSizeThreshold 参数,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制;
### 4.3 长期存活的对象进入老年代
JVM 为对象定义年龄计数器,经过 Minor GC 依然存活且被 Survivor 区容纳的,移动到 Survivor 区,年龄加 1每经历一次 Minor GC 不被清理则年龄加 1增加到一定年龄则移动到老年区默认 15 岁,通过 -XX:MaxTenuringThreshold 设置);
JVM 为对象定义年龄计数器,经过 Minor GC 依然存活,并且能被 Survivor 区容纳的,移被移到 Survivor 区,年龄就增加 1 岁,增加到一定年龄则移动到老年代中(默认 15 岁,通过 -XX:MaxTenuringThreshold 设置);
### 4.4 动态对象年龄判定
若 Survivor 区中同年龄所有对象大小总和大于 Survivor 空间一半,则年龄大于等于该年龄的对象可以直接进入老年代;
JVM 并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无序等待 MaxTenuringThreshold 中要求的年龄。
### 4.5 空间分配担保
在发生 Minor GC 之前JVM 先检查老年代最大可用连续空间是否大于新生代所有对象总空间,成立的话 Minor GC 确认是安全的;否则继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,大于的话进行 Minor GC小于的话进行 Full GC。
在发生 Minor GC 之前JVM 先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的;如果不成立的话 JVM 会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。
## 4.6 Full GC 的触发条件
## 5. Full GC 的触发条件
对于 Minor GC其触发条件非常简单当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
### 4.6.1 调用 System.gc()
### 5.1 调用 System.gc()
此方法的调用是建议 JVM 进行 Full GC虽然只是建议而非一定但很多情况下它会触发 Full GC从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()。
此方法的调用是建议 JVM 进行 Full GC虽然只是建议而非一定但很多情况下它会触发 Full GC从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()。
### 4.6.2 老年代空间不足
### 5.2 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出如下错误: Java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的 Full GC调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
### 4.6.3 空间分配担保失败
### 5.3 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。
### 4.6.4 JDK 1.7 及以前的永久代空间不足
### 5.4 JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation 可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息java.lang.OutOfMemoryError: PermGen space 为避免 PermGen 占满造成 Full GC 现象,可采用的方法为增大 PermGen 空间或转为使用 CMS GC。
在 JDK 1.7 及以前HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出 java.lang.OutOfMemoryError为避免以上原因引起的 Full GC可采用的方法为增大永久代空间或转为使用 CMS GC。
在 JDK 1.8 中用元空间替换了永久代作为方法区的实现,元空间是本地内存,因此减少了一种 Full GC 触发的可能性。
### 4.6.5 Concurrent Mode Failure
### 5.5 Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC便会报 Concurrent Mode Failure 错误,并触发 Full GC。
@ -433,7 +438,7 @@ JVM 为对象定义年龄计数器,经过 Minor GC 依然存活且被 Survivor
## 1 类的生命周期
<br><div align="center"> <img src="../pics//32b8374a-e822-4720-af0b-c0f485095ea2.jpg"/> </div><br>
<div align="center"> <img src="../pics//32b8374a-e822-4720-af0b-c0f485095ea2.jpg"/> </div><br>
包括以下 7 个阶段:
@ -449,7 +454,7 @@ JVM 为对象定义年龄计数器,经过 Minor GC 依然存活且被 Survivor
## 2. 类初始化时机
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化( 加载、验证、准备都会随着发生 )
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随着发生):
1. 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
@ -459,23 +464,23 @@ JVM 为对象定义年龄计数器,经过 Minor GC 依然存活且被 Survivor
4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
5. 当使用 jdk1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
5. 当使用 JDK.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
1\. 通过子类引用父类的静态字段,不会导致子类初始化。
- 通过子类引用父类的静态字段,不会导致子类初始化。
```java
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
```
2\. 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
```java
SuperClass[] sca = new SuperClass[10];
```
3\. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
```java
System.out.println(ConstClass.HELLOWORLD);
@ -510,10 +515,21 @@ System.out.println(ConstClass.HELLOWORLD);
主要有以下 4 个阶段:
1. 文件格式验证
2. 元数据验证(对字节码描述的信息进行语义分析)
3. 字节码验证(通过数据流和控制流分析,确保程序语义是合法、符合逻辑的,将对类的方法体进行校验分析)
4. 符号引用验证
**1. 文件格式验证**
验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
**2. 元数据验证**
对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
**3. 字节码验证**
通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
**4. 符号引用验证**
发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
### 3.3 准备
@ -539,7 +555,7 @@ public static final int value = 123;
### 3.5 初始化
初始化阶段即虚拟机执行类构造器 &lt;clinit>() 方法的过程。
初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 &lt;clinit>() 方法的过程。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
@ -574,7 +590,7 @@ static class Sub extends Parent {
}
public static void main(String[] args) {
System.out.println(Sub.B); // 输出结果是父类中的静态变量 A也就是 2
System.out.println(Sub.B); // 输出结果是父类中的静态变量 A 的值 ,也就是 2
}
```
@ -590,7 +606,7 @@ public static void main(String[] args) {
### 4.1 类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof() 关键字做对象所属关系判定等情况),只有在这两个类由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof() 关键字做对象所属关系判定等情况),只有在这两个类由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
### 4.2 类加载器分类
@ -600,7 +616,7 @@ public static void main(String[] args) {
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
- 启动类加载器Bootstrap ClassLoader 此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用 null 代替即可。
- 启动类加载器Bootstrap ClassLoader 此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
- 扩展类加载器Extension ClassLoader 这个类加载器是由 ExtClassLoadersun.misc.Launcher$ExtClassLoader实现的。它负责将 <Java_Home>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
@ -610,15 +626,15 @@ public static void main(String[] args) {
应用程序都是由三种类加载器相互配合进行加载的如果有必要还可以加入自己定义的类加载器。下图展示的类加载器之间的层次关系称为类加载器的双亲委派模型Parents Delegation Model。该模型要求除了顶层的启动类加载器外其余的类加载器都应有自己的父类加载器这里类加载器之间的父子关系一般通过组合Composition关系来实现而不是通过继承Inheritance的关系实现。
<br><div align="center"> <img src="../pics//2cdc3ce2-fa82-4c22-baaa-000c07d10473.jpg"/> </div><br>
<div align="center"> <img src="../pics//2cdc3ce2-fa82-4c22-baaa-000c07d10473.jpg"/> </div><br>
**工作过程**
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。
**好处**
使用双亲委派模型来组织类加载器之间的关系,使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object它存放 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型,由各个类加载器自行加载的话,如果用户编写了一个称为java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将变得一片混乱。如果开发者尝试编写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现可以正常编译,但是永远无法被加载运行。
使用双亲委派模型来组织类加载器之间的关系,使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object它存放 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反如果没有双亲委派模型由各个类加载器自行加载的话如果用户编写了一个称为java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将变得一片混乱。如果开发者尝试编写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现可以正常编译,但是永远无法被加载运行。
**实现**

View File

@ -15,11 +15,11 @@
* [2.1 通道](#21-通道)
* [2.2 缓冲区](#22-缓冲区)
* [3. 缓冲区状态变量](#3-缓冲区状态变量)
* [4. 读写文件实例](#4-读写文件实例)
* [4. 文件 NIO 实例](#4-文件-nio-实例)
* [5. 阻塞与非阻塞](#5-阻塞与非阻塞)
* [5.1 阻塞式 I/O](#51-阻塞式-io)
* [5.2 非阻塞式 I/O](#52-非阻塞式-io)
* [6. 套接字实例](#6-套接字实例)
* [6. 套接字 NIO 实例](#6-套接字-nio-实例)
* [6.1 ServerSocketChannel](#61-serversocketchannel)
* [6.2 Selectors](#62-selectors)
* [6.3 主循环](#63-主循环)
@ -27,6 +27,7 @@
* [6.5 接受新的连接](#65-接受新的连接)
* [6.6 删除处理过的 SelectionKey](#66-删除处理过的-selectionkey)
* [6.7 传入的 I/O](#67-传入的-io)
* [7. 内存映射文件](#7-内存映射文件)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@ -40,7 +41,7 @@ Java 的 I/O 大概可以分成以下几类
3. 字符操作Reader 和 Writer
4. 对象操作Serializable
5. 网络操作Socket
6. 非阻塞式 IONIO
6. 新的输入/输出NIO
# 磁盘操作
@ -48,9 +49,11 @@ File 类可以用于表示文件和目录,但是它只用于表示文件的信
# 字节操作
<br><div align="center"> <img src="../pics//8143787f-12eb-46ea-9bc3-c66d22d35285.jpg"/> </div><br>
<div align="center"> <img src="../pics//8143787f-12eb-46ea-9bc3-c66d22d35285.jpg"/> </div><br>
Java I/O 使用了装饰者模式来实现。以 InputStream 为例InputStream 是抽象组件FileInputStream 是 InputStream 的子类属于具体组件提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
Java I/O 使用了装饰者模式来实现。以 InputStream 为例InputStream 是抽象组件FileInputStream 是 InputStream 的子类属于具体组件提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
```java
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
@ -58,7 +61,7 @@ BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
批量读入文件中的内容到字节数组中
批量读入文件内容到字节数组:
```java
byte[] buf = new byte[20*1024];
@ -71,9 +74,9 @@ while((bytes = in.read(buf, 0 , buf.length)) != -1) {
# 字符操作
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是在程序中操作的数据通常是字符形式,因此需要提供对字符进行操作的方法。
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
InputStreamReader 实现从文本文件的字节流解码成字符流OutputStreamWriter 实现字符流编码成为文本文件的字节流。它们继承自 Reader 和 Writer。
InputStreamReader 实现从文本文件的字节流解码成字符流OutputStreamWriter 实现字符流编码成为文本文件的字节流。它们继承自 Reader 和 Writer。
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
@ -115,7 +118,12 @@ Java 中的网络支持:
## 1. InetAddress
没有公有构造函数,只能通过静态方法来创建实例,比如 InetAddress.getByName(String host)、InetAddress.getByAddress(byte[] addr)。
没有公有构造函数,只能通过静态方法来创建实例。
```java
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] addr);
```
## 2. URL
@ -138,14 +146,11 @@ is.close();
## 3. Sockets
Socket 通信模型
<br><div align="center"> <img src="../pics//fa4101d7-19ce-4a69-a84f-20bbe64320e5.jpg"/> </div><br>
- ServerSocket服务器端类
- Socket客户端类
- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
<div align="center"> <img src="../pics//fa4101d7-19ce-4a69-a84f-20bbe64320e5.jpg"/> </div><br>
## 4. Datagram
@ -154,17 +159,17 @@ Socket 通信模型
# NIO
NIO 将最耗时的 I/O 操作 ( 即填充和提取缓冲区 ) 转移回操作系统,因而 不需要程序员去控制就可以极大地提高运行速度
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O
## 1. 流与块
I/O 与 NIO 最重要的区别是数据打包和传输的方式。正如前面提到的I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
I/O 与 NIO 最重要的区别是数据打包和传输的方式I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
面向流的 I/O 一次一个字节进行处理数据,一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。
面向流的 I/O 一次处理一个字节数据,一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。
一个面向块的 I/O 系统以块的形式处理数据,每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
一个面向块的 I/O 系统以块的形式处理数据,一次处理数据块。按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如, java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如, java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
## 2. 通道与缓冲区
@ -172,7 +177,7 @@ I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动,(一个流必须是 InputStream 或者 OutputStream 的子类) 而通道是双向的,可以用于读、写或者同时用于读写。
通道与流的不同之处在于,流只能在一个方向上移动,(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
通道包括以下类型:
@ -183,7 +188,7 @@ I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重
### 2.2 缓冲区
发送给一个通道的所有对象都必须首先放到缓冲区中同样地,从通道中读取的任何数据都要读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是先经过缓冲区。
发送给一个通道的所有对象都必须首先放到缓冲区中同样地,从通道中读取的任何数据都要读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是先经过缓冲区。
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
@ -197,7 +202,6 @@ I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重
- FloatBuffer
- DoubleBuffer
## 3. 缓冲区状态变量
- capacity最大容量
@ -206,27 +210,22 @@ I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重
状态变量的改变过程:
1\. 新建一个大小为 8 个字节的缓冲区,此时 position 为 0而 limit == capacity == 9。capacity 变量不会改变,下面的讨论会忽略它。
1. 新建一个大小为 8 个字节的缓冲区,此时 position 为 0而 limit = capacity = 9。capacity 变量不会改变,下面的讨论会忽略它。
<div align="center"> <img src="../pics//1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png"/> </div><br>
<br><div align="center"> <img src="../pics//1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png"/> </div><br>
2. 从输入通道中读取 3 个字节数据写入缓冲区中,此时 position 移动设为 3limit 保持不变。
<div align="center"> <img src="../pics//4628274c-25b6-4053-97cf-d1239b44c43d.png"/> </div><br>
2\. 从输入通道中读取 3 个字节数据写入缓冲区中,此时 position 移动设为 3limit 保持不变。
3. 以下图例为已经从输入通道读取了 5 个字节数据写入缓冲区中。在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position并将 position 设置为 0。
<div align="center"> <img src="../pics//952e06bd-5a65-4cab-82e4-dd1536462f38.png"/> </div><br>
<br><div align="center"> <img src="../pics//4628274c-25b6-4053-97cf-d1239b44c43d.png"/> </div><br>
4. 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
<div align="center"> <img src="../pics//b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png"/> </div><br>
3\. 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position并将 position 设置为 0。
5. 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
<div align="center"> <img src="../pics//67bf5487-c45d-49b6-b9c0-a058d8c68902.png"/> </div><br>
<br><div align="center"> <img src="../pics//952e06bd-5a65-4cab-82e4-dd1536462f38.png"/> </div><br>
4\. 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
<br><div align="center"> <img src="../pics//b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png"/> </div><br>
5\. 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
<br><div align="center"> <img src="../pics//67bf5487-c45d-49b6-b9c0-a058d8c68902.png"/> </div><br>
## 4. 读写文件实例
## 4. 文件 NIO 实例
1\. 为要读取的文件创建 FileInputStream之后通过 FileInputStream 获取输入 FileChannel
@ -235,13 +234,13 @@ FileInputStream fin = new FileInputStream("readandshow.txt");
FileChannel fic = fin.getChannel();
```
2\. 创建一个容量为 1024 的 Buffer
2\. 创建一个容量为 1024 的 Buffer
```java
ByteBuffer buffer = ByteBuffer.allocate(1024);
```
3\. 将数据从输入 FileChannel 写入到 Buffer 中,如果没有数据的话, read() 方法会返回 -1
3\. 将数据从输入 FileChannel 写入到 Buffer 中,如果没有数据的话, read() 方法会返回 -1
```java
int r = fcin.read(buffer);
@ -281,25 +280,27 @@ buffer.clear();
### 5.1 阻塞式 I/O
阻塞式 I/O 在调用 InputStream.read() 方法时会一直等到数据到来时(或超时)才会返回,在调用 ServerSocket.accept() 方法时,也会一直阻塞到有客户端连接才会返回,每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求
阻塞式 I/O 在调用 InputStream.read() 方法时会一直等到数据到来时(或超时)才会返回,在调用 ServerSocket.accept() 方法时,也会一直阻塞到有客户端连接才会返回。
<br><div align="center"> <img src="../pics//edc23f99-c46c-4200-b64e-07516828720d.jpg"/> </div><br>
服务端都会为每个连接的客户端创建一个线程来处理读写请求,阻塞式的特点会造成服务器会创建大量线程,并且大部分线程处于阻塞的状态,因此对服务器的性能会有很大的影响。
<div align="center"> <img src="../pics//edc23f99-c46c-4200-b64e-07516828720d.jpg"/> </div><br>
### 5.2 非阻塞式 I/O
由一个专门的线程来处理所有的 I/O 事件,并负责分发。
事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
事件驱动机制:事件到的时候触发,而不是同步监视事件。
线程通信:线程之间通过 wait()、notify() 等方式通信,保证每次上下文切换都是有意义的,减少无谓的线程切换。
<br><div align="center"> <img src="../pics//7fcb2fb0-2cd9-4396-bc2d-282becf963c3.jpg"/> </div><br>
<div align="center"> <img src="../pics//7fcb2fb0-2cd9-4396-bc2d-282becf963c3.jpg"/> </div><br>
## 6. 套接字实例
## 6. 套接字 NIO 实例
### 6.1 ServerSocketChannel
每一个端口都需要有一个 ServerSocketChannel 用来监听连接。
每一个监听端口都需要有一个 ServerSocketChannel 用来监听连接。
```java
ServerSocketChannel ssc = ServerSocketChannel.open();
@ -314,7 +315,7 @@ ss.bind(address); // 绑定端口号
异步 I/O 通过 Selector 注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字连接等等,在发生这样的事件时,系统将会发送通知。
创建 Selectors 之后,就可以对不同的通道对象调用 register() 方法。register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。
创建 Selectors 之后,就可以对不同的通道对象调用 register() 方法。register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT这里它指定我们想要监听 ACCEPT 事件,也就是在新的连接建立时所发生的事件。
SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。
@ -325,9 +326,9 @@ SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);
### 6.3 主循环
首先,我们调用 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() 方法将返回所发生的事件的数量。
首先,我们调用 Selector 的 select() 方法。这个方法会阻塞直到至少有一个已注册的事件发生。当一个或者更多的事件发生时select() 方法将返回所发生的事件的数量。
接下来,我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个 集合
接下来,我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个集合。
我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。
@ -355,7 +356,7 @@ if ((key.readyOps() & SelectionKey.OP_ACCEPT)
}
```
可以肯定地说, readOps() 方法告诉我们该事件是新的连接。
可以肯定地说readOps() 方法告诉我们该事件是新的连接。
### 6.5 接受新的连接
@ -366,14 +367,14 @@ ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
```
下一步是将新连接的 SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将 SocketChannel 注册到 Selector上如下所示
下一步是将新连接的 SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将 SocketChannel 注册到 Selector 上,如下所示:
```java
sc.configureBlocking( false );
SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );
sc.configureBlocking(false);
SelectionKey newKey = sc.register(selector, SelectionKey.OP_READ);
```
注意我们使用 register() 的 OP_READ 参数,将 SocketChannel 注册用于 读取 而不是 接受 新连接。
注意我们使用 register() 的 OP_READ 参数,将 SocketChannel 注册用于读取而不是接受新连接。
### 6.6 删除处理过的 SelectionKey
@ -383,7 +384,7 @@ SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );
it.remove();
```
现在我们可以返回主循环并接受从一个套接字中传入的数据(或者一个传入的 I/O 事件)了。
现在我们可以返回主循环并接受从一个套接字中传入的数据 (或者一个传入的 I/O 事件) 了。
### 6.7 传入的 I/O
@ -398,10 +399,25 @@ it.remove();
}
```
## 7. 内存映射文件
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
只有文件中实际读取或者写入的部分才会映射到内存中。
现代操作系统一般根据需要将文件的部分映射为内存的部分从而实现文件系统。Java 内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。
向内存映射文件写入可能是危险的,仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 1024 个字节映射到内存中map() 方法返回一个 MappedByteBuffer它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行行映射。
```java
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
```
# 参考资料
- Eckel B, 埃克尔 , 昊鹏 , 等 . Java 编程思想 [M]. 机械工业出版社 , 2002.
- 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)
- [深入分析 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)

View File

@ -8,8 +8,9 @@
* [3. equals()](#3-equals)
* [继承](#继承)
* [1. 访问权限](#1-访问权限)
* [2. 抽象类与接口的区别](#2-抽象类与接口的区别)
* [3. super()](#3-super)
* [2. 抽象类与接口](#2-抽象类与接口)
* [3. super](#3-super)
* [4. 重载与重写](#4-重载与重写)
* [String](#string)
* [1. String, StringBuffer and StringBuilder](#1-string,-stringbuffer-and-stringbuilder)
* [2. String 不可变的原因](#2-string-不可变的原因)
@ -20,6 +21,7 @@
* [反射](#反射)
* [异常](#异常)
* [泛型](#泛型)
* [注解](#注解)
* [特性](#特性)
* [1. 三大特性](#1-三大特性)
* [2. Java 各版本的新特性](#2-java-各版本的新特性)
@ -28,7 +30,6 @@
<!-- GFM-TOC -->
# 关键字
## 1. final
@ -37,7 +38,8 @@
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
对于基本类型final 使数值不变对于引用对象final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
- 对于基本类型final 使数值不变;
- 对于引用类型final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
**方法**
@ -51,15 +53,16 @@ private 方法隐式地被指定为 final如果在子类中定义的方法和
## 2. static
**变量**
**静态变量**
静态变量在内存中只存在一份,只在类第一次实例化时初始化一次,同时类所有的实例都共享静态变量,可以直接通过类名来访问它
静态变量在内存中只存在一份,只在类第一次实例化时初始化一次。
但是实例变量则不同,它是伴随着实例的,每创建一个实例就会产生一个实例变量,它与该实例同生共死。
- 静态变量: 类所有的实例都共享静态变量,可以直接通过类名来访问它;
- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
**方法**
**静态方法**
静态方法在类加载的时候就存在了,它不依赖于任何实例,所以 static 方法必须实现,也就是说他不能是抽象方法 abstract
静态方法在类加载的时候就存在了,它不依赖于任何实例,所以 static 方法必须实现,也就是说它不能是抽象方法abstract
**静态语句块**
@ -75,19 +78,19 @@ public static String staticField = "静态变量";
```java
static {
System.out.println("静态初始化块");
System.out.println("静态语句块");
}
```
实例变量和普通语句块的初始化在静态变量和静态语句块初始化结束之后。
```java
public String field = "变量";
public String field = "实例变量";
```
```java
{
System.out.println("初始化块");
System.out.println("普通语句块");
}
```
@ -95,58 +98,74 @@ public String field = "变量";
```java
public InitialOrderTest() {
System.out.println("构造");
System.out.println("构造函数");
}
```
存在继承的情况下,初始化顺序为:
1. 父类(静态变量、静态初始化块)
2. 子类(静态变量、静态初始化块)
3. 父类(变量、初始化块)
4. 父类(构造器)
5. 子类(变量、初始化块)
6. 子类(构造器)
1. 父类(静态变量、静态语句块块)
2. 子类(静态变量、静态语句块)
3. 父类(实例变量、普通语句块)
4. 父类(构造函数)
5. 子类(实例变量、普通语句块)
6. 子类(构造函数)
# Object 通用方法
## 1. 概览
- public final native Class<?> getClass()
- public native int hashCode()
- public boolean equals(Object obj)
- protected native Object clone() throws CloneNotSupportedException
- public String toString()
- public final native void notify()
- public final native void notifyAll()
- public final native void wait(long timeout) throws InterruptedException
- public final void wait(long timeout, int nanos) throws InterruptedException
- public final void wait() throws InterruptedException
- protected void finalize() throws Throwable { }
```java
public final native Class<?> getClass()
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native void notify()
public final native void notifyAll()
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException
protected void finalize() throws Throwable {}
```
## 2. clone()
**浅拷贝**
引用类型引用的是同一个对象clone() 方法默认就是浅拷贝实现。
引用类型引用同一个对象。clone() 方法默认就是浅拷贝实现。
<br><div align="center"> <img src="../pics//d990c0e7-64d1-4ba3-8356-111bc91e53c5.png"/> </div><br>
<div align="center"> <img src="../pics//d990c0e7-64d1-4ba3-8356-111bc91e53c5.png"/> </div><br>
**深拷贝**
可以使用序列化实现。
<br><div align="center"> <img src="../pics//2e5620c4-b558-46fe-8f12-00c9dd597a61.png"/> </div><br>
<div align="center"> <img src="../pics//2e5620c4-b558-46fe-8f12-00c9dd597a61.png"/> </div><br>
> [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() 是判断引用的对象是否等价。
等价性:[ 散列 ](https://github.com/CyC2018/InterviewNotes/blob/master/notes/Java%20%E5%AE%B9%E5%99%A8.md#%E6%95%A3%E5%88%97)
**等价性**
[散列](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Java%20%E5%AE%B9%E5%99%A8.md#%E6%95%A3%E5%88%97)
# 继承
@ -154,13 +173,13 @@ public InitialOrderTest() {
Java 中有三个访问权限修饰符private、protected 以及 public如果不加访问修饰符表示包级可见。
可以对类或类中的成员(字段以及方法)加上访问修饰符。成员可见表示其它类可以用成员所在类的对象访问到该成员;类可见表示其它类可以用这个类创建对象,可以把类当做包中的一个成员,然后包表示一个类,这样就好理解了
可以对类或类中的成员(字段以及方法)加上访问修饰符。成员可见表示其它类可以用成员所在类的对象访问到该成员;类可见表示其它类可以用这个类创建对象。在理解类的可见性时,可以把类当做包中的一个成员,然后包表示一个类,那么就可以类比成员的可见性
protected 用于修饰成员,表示在继承体系中成员对于子类可见。但是这个访问修饰符对于类没有意义,因为包没有继承体系。
更详细的内容:[ 浅析 Java 中的访问权限控制 ](http://www.importnew.com/18097.html)
更详细的内容:[浅析 Java 中的访问权限控制](http://www.importnew.com/18097.html)
## 2. 抽象类与接口的区别
## 2. 抽象类与接口
抽象类至少包含一个抽象方法,该抽象方法必须在子类中实现。由于抽象类没有抽象方法的具体实现,因此不能对抽象类进行实例化。
@ -176,7 +195,7 @@ public abstract class GenericServlet implements Servlet, ServletConfig, Serializ
}
```
接口定义了一组方法,但是接口都没有方法的实现,也就是说这些方法都是抽象方法。
接口定义了一组方法,但是接口都没有方法的实现,可以理解为这些方法都是抽象方法。
```java
public interface Externalizable extends Serializable {
@ -187,15 +206,37 @@ public interface Externalizable extends Serializable {
}
```
更详细的内容:[Java 抽象类与接口的区别 ](http://www.importnew.com/12399.html)
| **参数** | **抽象类** | **接口** |
| --- | --- | --- |
| 默认的方法实现 | 它可以有默认的方法实现 | 接口完全是抽象的。它根本不存在方法的实现 |
| 实现 | 子类使用 extends 关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字 implements 来实现接口。它需要提供接口中所有声明的方法的实现 |
| 构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
| 与正常 Java 类的区别 | 除了你不能实例化抽象类之外,它和普通 Java 类没有任何区别 | 接口是完全不同的类型 |
| 访问修饰符 | 抽象方法可以有 public、protected 和 default 这些修饰符 | 接口方法默认修饰符是 **public** 。你不可以使用其它修饰符。 |
| main 方法 | 抽象方法可以有 main 方法并且我们可以运行它 | 接口没有 main 方法,因此我们不能运行它。 |
| 多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口只可以继承一个或多个其它接口 |
| 速度 | 它比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。 |
| 添加新方法 | 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 | 如果你往接口中添加方法,那么你必须改变实现该接口的类。 |
## 3. super()
> [Java 抽象类与接口的区别](http://www.importnew.com/12399.html)
用来访问父类的构造函数父类的方法,第二种情况中,子类需要重载父类的方法。
## 3. super
**访问父类的成员**
如果子类覆盖了父类的中某个方法的实现,那么就可以通过使用 super 关键字来引用父类的方法实现。
```java
public class Superclass {
public void printMethod() {
System.out.println("Printed in Superclass.");
}
}
```
```java
public class Subclass extends Superclass {
// overrides printMethod in Superclass
// Overrides printMethod in Superclass
public void printMethod() {
super.printMethod();
System.out.println("Printed in Subclass");
@ -207,8 +248,25 @@ public class Subclass extends Superclass {
}
```
**访问父类的构造函数**
可以使用 super() 函数访问父类的构造函数,从而完成一些初始化的工作。
```java
public MountainBike(int startHeight, int startCadence, int startSpeed, int startGear) {
super(startCadence, startSpeed, startGear);
seatHeight = startHeight;
}
```
> [Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html)
## 4. 重载与重写
- 重写存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法;
- 重载即存在于继承体系中,也存在于同一个类中,指一个方法与已经存在的方法或者父类的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。
# String
## 1. String, StringBuffer and StringBuilder
@ -235,11 +293,11 @@ StringBuilder 不是线程安全的StringBuffer 是线程安全的,使用 s
如果 String 已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
<br><div align="center"> <img src="../pics//f76067a5-7d5f-4135-9549-8199c77d8f1c.jpg"/> </div><br>
<div align="center"> <img src="../pics//f76067a5-7d5f-4135-9549-8199c77d8f1c.jpg"/> </div><br>
**安全性**
String 经常作为参数,例如网络连接参数等,在作为网络连接参数的情况下,如果 String 是可变的那么在网络连接过程中String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。String 不可变性可以保证参数不可变。
String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的那么在网络连接过程中String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
**线程安全**
@ -251,7 +309,7 @@ String 不可变性天生具备线程安全,可以在多个线程中使用。
使用 String.intern() 可以保证所有相同内容的字符串变量引用相同的内存对象。
更详细的内容:[ 揭开 String.intern() 那神秘的面纱 ](https://www.jianshu.com/p/95f516cb75ef)
更详细的内容:[揭开 String.intern() 那神秘的面纱](https://www.jianshu.com/p/95f516cb75ef)
# 基本类型与运算
@ -270,17 +328,13 @@ new Integer(123) 与 Integer.valueOf(123) 的区别在于Integer.valueOf(123)
```java
public static void main(String[] args) {
Integer a = new Integer(1);
Integer b = new Integer(1);
System.out.println("a==b? " + (a==b));
Integer c = Integer.valueOf(1);
Integer d = Integer.valueOf(1);
System.out.println("c==d? " + (c==d));
}
```
@ -322,7 +376,7 @@ System.out.println(c==d); // true
## 2. switch
A switch works with the byte, short, char, and int primitive data types. It also works with enumerated types (discussed in Classes and Inheritance) and a few special classes that "wrap" certain primitive types: Character, Byte, Short, and Integer (discussed in Simple Data Objects).
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.
@ -356,11 +410,6 @@ 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 对象。
@ -373,7 +422,25 @@ Class 和 java.lang.reflect 一起对反射提供了支持java.lang.reflect
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:**
- **Extensibility Features** : An application may make use of external, user-defined classes by creating instances of extensibility objects using their fully-qualified names.
- **Class Browsers and Visual Development Environments** : A class browser needs to be able to enumerate the members of classes. Visual development environments can benefit from making use of type information available in reflection to aid the developer in writing correct code.
- **Debuggers and Test Tools** : Debuggers need to be able to examine private members on classes. Test harnesses can make use of reflection to systematically call a discoverable set APIs defined on a class, to insure a high level of code coverage in a test suite.
**Drawbacks of Reflection**
Reflection is powerful, but should not be used indiscriminately. If it is possible to perform an operation without using reflection, then it is preferable to avoid using it. The following concerns should be kept in mind when accessing code via reflection.
- **Performance Overhead** : Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.
- **Security Restrictions** : Reflection requires a runtime permission which may not be present when running under a security manager. This is in an important consideration for code which has to run in a restricted security context, such as in an Applet.
- **Exposure of Internals** :Since reflection allows code to perform operations that would be illegal in non-reflective code, such as accessing private fields and methods, the use of reflection can result in unexpected side-effects, which may render code dysfunctional and may destroy portability. Reflective code breaks abstractions and therefore may change behavior with upgrades of the platform.
> [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html)
# 异常
@ -381,16 +448,14 @@ Throwable 可以用来表示任何可以作为异常抛出的类,分为两种
Exception 分为两种: **受检异常****非受检异常**。受检异常需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;非受检异常是程序运行时错误,例如除 0 会引发 Arithmetic Exception此时程序奔溃并且无法恢复。
<br><div align="center"> <img src="../pics//48f8f98e-8dfd-450d-8b5b-df4688f0d377.jpg"/> </div><br>
<div align="center"> <img src="../pics//48f8f98e-8dfd-450d-8b5b-df4688f0d377.jpg"/> </div><br>
更详细的内容:
- [Java 入门之异常处理 ](https://www.tianmaying.com/tutorial/Java-Exception)
- [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception)
- [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html)
# 泛型
泛型提供了编译时的类型检测机制,该机制允许程序员在编译时检测到非法的类型。泛型是 Java 中一个非常重要的知识点,在 Java 集合类框架中泛型被广泛应用。
```java
public class Box<T> {
// T stands for "Type"
@ -402,14 +467,20 @@ public class Box<T> {
更详细的内容:
- [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注解是附加在代码中的一些元信息用于一些工具在编译、运行时进行解析和使用起到说明、配置的功能。注解不会也不能影响代码的实际逻辑仅仅起到辅助性的作用。
更多详细内容:[注解Annotation实现原理与自定义注解例子](https://www.cnblogs.com/acm-bingzi/p/javaAnnotation.html)
# 特性
## 1. 三大特性
[ 封装、继承、多态 ](https://github.com/CyC2018/InterviewNotes/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)
## 2. Java 各版本的新特性

View File

@ -10,11 +10,20 @@
* [2. 适配器模式](#2-适配器模式)
* [散列](#散列)
* [源码分析](#源码分析)
* [1. ArraList](#1-arralist)
* [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)
* [参考资料](#参考资料)
@ -23,7 +32,7 @@
# 概览
<br><div align="center"> <img src="../pics//ebf03f56-f957-4435-9f8f-0f605661484d.jpg"/> </div><br>
<div align="center"> <img src="../pics//ebf03f56-f957-4435-9f8f-0f605661484d.jpg"/> </div><br>
容器主要包括 Collection 和 Map 两种Collection 又包含了 List、Set 以及 Queue。
@ -47,21 +56,21 @@
## 4. Map
- HashMap基于 Hash 实现
- HashMap基于 Hash 实现
- LinkedHashMap使用链表来维护元素的顺序顺序为插入顺序或者最近最少使用LRU顺序
- LinkedHashMap使用链表来维护元素的顺序顺序为插入顺序或者最近最少使用LRU顺序
- TreeMap基于红黑树实现
- TreeMap基于红黑树实现
- ConcurrentHashMap线程安全 Map不涉及类似于 HashTable 的同步加锁
- ConcurrentHashMap线程安全 Map不涉及类似于 HashTable 的同步加锁
## 5. Java 1.0/1.1 容器
对于旧的容器,我们决不应该使用它们,只需要对它们进行了解。
- Vector和 ArrayList 类似,但它是线程安全的
- Vector和 ArrayList 类似,但它是线程安全的
- HashTable和 HashMap 类似,但它是线程安全的
- HashTable和 HashMap 类似,但它是线程安全的
# 容器中的设计模式
@ -69,7 +78,7 @@
从概览图可以看到,每个集合类都有一个 Iterator 对象,可以通过这个迭代器对象来遍历集合中的元素。
[Java 中的迭代器模式 ](https://github.com/CyC2018/InterviewNotes/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#92-java-%E5%86%85%E7%BD%AE%E7%9A%84%E8%BF%AD%E4%BB%A3%E5%99%A8)
[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. 适配器模式
@ -97,14 +106,16 @@ java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
# 源码分析
建议先阅读 [ 算法 - 查找 ](https://github.com/CyC2018/InterviewNotes/blob/master/notes/%E7%AE%97%E6%B3%95.md#%E7%AC%AC%E4%B8%89%E7%AB%A0-%E6%9F%A5%E6%89%BE) 部分,对集合类源码的理解有很大帮助。
建议先阅读 [算法 - 查找](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E7%AE%97%E6%B3%95.md#%E6%9F%A5%E6%89%BE) 部分,对集合类源码的理解有很大帮助。
源码下载:[OpenJDK 1.7](http://download.java.net/openjdk/jdk7)
## 1. ArraList
## 1. ArrayList
[ArraList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/ArrayList.java)
### 概览
实现了 RandomAccess 接口,因此支持随机访问,这是理所当然的,因为 ArrayList 是基于数组实现的。
```java
@ -112,13 +123,13 @@ public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
```
基于数组实现,保存元素的数组使用 transient 修饰,这是因为该数组不一定所有位置都占满元素,因此也就没必要全部都进行序列化。需要重写 writeObject() 和 readObject()
基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明该数组默认不会被序列化。这是因为该数组不是所有位置都占满元素因此也就没必要全部都进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那么部分内容
```java
private transient Object[] elementData;
```
数组的默认大小为 10
数组的默认大小为 10
```java
public ArrayList(int initialCapacity) {
@ -133,7 +144,7 @@ public ArrayList() {
}
```
删除元素时调用 System.arraycopy() 对元素进行复制,因此删除操作成本很高,最好在创建时就指定大概的容量大小,减少复制操作的执行次数
删除元素时调用 System.arraycopy() 对元素进行复制,因此删除操作成本很高。
```java
public E remove(int index) {
@ -151,16 +162,9 @@ public E remove(int index) {
}
```
添加元素时使用 ensureCapacity() 方法来保证容量足够,如果不够时,需要进行扩容,使得新容量为旧容量的 1.5 倍。
modCount 用来记录 ArrayList 发生变化的次数,因为每次在进行 add() 和 addAll() 时都需要调用 ensureCapacity(),因此直接在 ensureCapacity() 中对 modCount 进行修改。
添加元素时使用 ensureCapacity() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,使得新容量为旧容量的 1.5 倍。扩容操作需要把原数组整个复制到新数组中,因此最好在创建 ArrayList 时就指定大概的容量大小,减少扩容操作的次数。
```java
public void ensureCapacity(int minCapacity) {
if (minCapacity > 0)
ensureCapacityInternal(minCapacity);
}
private void ensureCapacityInternal(int minCapacity) {
modCount++;
// overflow-conscious code
@ -168,8 +172,6 @@ private void ensureCapacityInternal(int minCapacity) {
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
@ -191,11 +193,14 @@ private static int hugeCapacity(int minCapacity) {
}
```
### Fail-Fast
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();
@ -204,24 +209,23 @@ private void writeObject(java.io.ObjectOutputStream s)
s.writeInt(elementData.length);
// Write out all elements in the proper order.
for (int i=0; i<size; i++)
for (int i = 0; i < size; i++)
s.writeObject(elementData[i]);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
```
**和 Vector 的区别**
### 和 Vector 的区别
1. Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步的,因此开销就比 ArrayList 要大,访问慢。最好使用 ArrayList 而不是 Vector因为同步完全可以由程序员自己来控制
1. Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector因为同步操作完全可以由程序员自己来控制;
2. Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
为了使用线程安全的 ArrayList可以使用 Collections.synchronizedList(new ArrayList<>()); 返回一个线程安全的 ArrayList也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类;
为了获得线程安全的 ArrayList可以调用 Collections.synchronizedList(new ArrayList<>()); 返回一个线程安全的 ArrayList也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类;
**和 LinkedList 的区别**
### 和 LinkedList 的区别
1. ArrayList 基于动态数组实现LinkedList 基于双向循环链表实现;
2. ArrayList 支持随机访问LinkedList 不支持;
@ -243,13 +247,39 @@ private void writeObject(java.io.ObjectOutputStream s)
[HashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java)
使用拉链法来解决冲突。
### 基本数据结构
默认容量 capacity 为 16需要注意的是容量必须保证为 2 的次方。容量就是 Entry[] table 数组的长度size 是数组的实际使用量。
使用拉链法来解决冲突,内部包含了一个 Entry 类型的数组 table数组中的每个位置被当成一个桶。
```java
transient Entry[] table;
```
其中Entry 就是存储数据的键值对,它包含了四个字段。从 next 字段我们可以看出 Entry 是一个链表,即每个桶会存放一个链表。
<div align="center"> <img src="../pics//ce039f03-6588-4f0c-b35b-a494de0eac47.png"/> </div><br>
### 拉链法的工作原理
使用默认构造函数新建一个 HashMap默认大小为 16。Entry 的类型为 &lt;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) 之后。
<div align="center"> <img src="../pics//b9a39d2a-618c-468b-86db-2e851f1a0057.jpg"/> </div><br>
当进行查找时,需要分成两步进行,第一步是先根据 hashcode 计算出所在的桶,第二步是在链表上顺序查找。由于 table 是数组形式的,具有随机读取的特性,因此这一步的时间复杂度为 O(1),而第二步需要在链表上顺序查找,时间复杂度显然和链表的长度成正比。
### 扩容
设 HashMap 的 table 长度为 M需要存储的键值对数量为 N如果哈希函数满足均匀性的要求那么每条链表的长度大约为 N/M因此平均查找次数的数量级为 O(N/M)。
为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,可就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。
和扩容相关的参数主要有capacity、size、threshold 和 load_factor。
capacity 表示 table 的容量大小,默认为 16需要注意的是容量必须保证为 2 的次方。容量就是 table 数组的长度size 是数组的实际使用量。
threshold 规定了一个 size 的临界值size 必须小于 threshold如果大于等于就必须进行扩容操作。
threshold = capacity * load_factor其中 load_factor 为 table 数组能够使用的比例load_factor 过大会导致聚簇的出现,从而影响查询和插入的效率,详见算法笔记。
threshold = capacity * load_factor其中 load_factor 为 table 数组能够使用的比例。
```java
static final int DEFAULT_INITIAL_CAPACITY = 16;
@ -280,18 +310,45 @@ void addEntry(int hash, K key, V value, int bucketIndex) {
}
```
Entry 用来表示一个键值对元素,其中的 next 指针在序列化时会使用
扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把旧 table 的所有键值对重新插入新的 table 中因此这一步是很费时的。但是从均摊分析的角度来考虑HashMap 的查找速度依然在常数级别
```java
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
```
get() 操作需要分成两种情况key 为 null 和 不为 null从中可以看出 HashMap 允许插入 null 作为键。
### null 值
get() 操作需要分成两种情况key 为 null 和不为 null从中可以看出 HashMap 允许插入 null 作为键。
```java
public V get(Object key) {
@ -347,6 +404,16 @@ private V putForNullKey(V value) {
}
```
### 与 HashTable 的区别
- 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。
- 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.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java)
@ -355,7 +422,7 @@ private V putForNullKey(V value) {
[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java)
[ 探索 ConcurrentHashMap 高并发性的实现机制 ](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/)
[探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/)
# 参考资料

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -137,7 +137,6 @@ ps aux | grep threadx
## 查看端口
```html
netstat -anp | grep 80
```
@ -153,7 +152,7 @@ netstat -anp | grep 80
## 运行等级
- 0关机模式
- 1单用户模式可用于破解root密码
- 1单用户模式可用于破解 root 密码)
- 2无网络支持的多用户模式
- 3有网络支持的多用户模式文本模式工作中最常用的模式
- 4保留未使用
@ -164,7 +163,6 @@ netstat -anp | grep 80
使用 sudo 允许一般用户使用 root 可执行的命令,只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。
## GNU
GNU 计划,又译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议,包含了以下内容:
@ -207,21 +205,21 @@ Linux 中每个硬件都被当做一个文件。
### 1. MBR
MBR 中,第一个扇区最重要,里面有主要开机记录Master boot record, MBR及分区表partition table其中 MBR 占 446 bytespartition table 占 64 bytes。
MBR 中第一个扇区最重要里面有主要开机记录Master boot record, MBR及分区表partition table其中 MBR 占 446 bytespartition table 占 64 bytes。
分区表只有 64 bytes最多只能存储 4 个分区,这 4 个分区为主分区Primary和扩展分区Extended。其中扩展分区只有一个它将其它空间用来记录分区表可以记录更多的分区,因此通过扩展分区可以分出更多区分,这些分区称为逻辑分区。
分区表只有 64 bytes最多只能存储 4 个分区,这 4 个分区为主分区Primary和扩展分区Extended。其中扩展分区只有一个它将其它空间用来记录分区表因此通过扩展分区可以分出更多区分这些分区称为逻辑分区。
Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名+编号,例如 /dev/sda1。注意逻辑分区的编号从 5 开始。
Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名 + 编号,例如 /dev/sda1。注意逻辑分区的编号从 5 开始。
### 2. GPT
不同的磁盘有不同的扇区大小,例如 512 bytes 和最新磁盘的 4k。GPT 为了兼容所有磁盘在定义扇区上使用逻辑区块地址Logical Block Address, LBA
不同的磁盘有不同的扇区大小,例如 512 bytes 和最新磁盘的 4 k。GPT 为了兼容所有磁盘在定义扇区上使用逻辑区块地址Logical Block Address, LBA
GPT 第 1 个区块记录了 MBR紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。
GPT 没有扩展分区概念,都是主分区,最多可以分 128 个分区。
<br><div align="center"> <img src="../pics//a5c25452-6fa5-49e7-9322-823077442775.jpg"/> </div><br>
<div align="center"> <img src="../pics//a5c25452-6fa5-49e7-9322-823077442775.jpg"/> </div><br>
## 开机检测程序
@ -231,7 +229,7 @@ BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可
MBR 中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动 MBR 中的开机管理程序时,就可以选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。
<br><div align="center"> <img src="../pics//f900f266-a323-42b2-bc43-218fdb8811a8.jpg"/> </div><br>
<div align="center"> <img src="../pics//f900f266-a323-42b2-bc43-218fdb8811a8.jpg"/> </div><br>
安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉 MBR而 Linux 可以选择将开机管理程序安装在 MBR 或者其它分区的启动扇区,并且可以设置开机管理程序的选单。
@ -243,7 +241,7 @@ UEFI 相比于 BIOS 来说功能更为全面,也更为安全。
挂载利用目录作为分区的进入点,也就是说,进入目录之后就可以读取分区的数据。
<br><div align="center"> <img src="../pics//249f3bb1-feee-4805-a259-a72699d638ca.jpg"/> </div><br>
<div align="center"> <img src="../pics//249f3bb1-feee-4805-a259-a72699d638ca.jpg"/> </div><br>
# 文件权限与目录配置
@ -288,7 +286,7 @@ UEFI 相比于 BIOS 来说功能更为全面,也更为安全。
### 3. 修改权限
可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1即每个权限对应的数字权值为 r4、w2、x1。
可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1即每个权限对应的数字权值为 r : 4、w : 2、x : 1。
```html
# chmod [-R] xyz dirname/filename
@ -342,7 +340,7 @@ UEFI 相比于 BIOS 来说功能更为全面,也更为安全。
完整的目录树如下:
<br><div align="center"> <img src="../pics//27ace615-558f-4dfb-8ad4-7ac769c10118.jpg"/> </div><br>
<div align="center"> <img src="../pics//27ace615-558f-4dfb-8ad4-7ac769c10118.jpg"/> </div><br>
# 文件与目录
@ -451,7 +449,7 @@ cp [-adfilprsu] source destination
-c 更新 ctime若该文件不存在则不建立新文件
-m 更新 mtime
-d 后面可以接欲更新的日期而不用当前的日期,也可以使用 --date="日期或时间"
-t :后面可以接欲更新的时间而不用当前的时间,格式为[YYYYMMDDhhmm]
-t 后面可以接欲更新的时间而不用当前的时间,格式为[YYYYMMDDhhmm]
```
## 指令与文件搜索
@ -496,14 +494,14 @@ find 可以使用文件的属性和权限进行搜索。
```html
-mtime n :列出在 n 天前的那一天修改过内容的文件
-mtime +n :列出在 n 天之前(不含 n 天本身)修改过内容的文件
-mtime -n :列出在 n 天之内(含 n 天本身)修改过内容的文件
-mtime +n :列出在 n 天之前 (不含 n 天本身) 修改过内容的文件
-mtime -n :列出在 n 天之内 (含 n 天本身) 修改过内容的文件
-newer file 列出比 file 更新的文件
```
+4、4 和 -4 的指示的时间范围如下:
<br><div align="center"> <img src="../pics//658fc5e7-79c0-4247-9445-d69bf194c539.png"/> </div><br>
<div align="center"> <img src="../pics//658fc5e7-79c0-4247-9445-d69bf194c539.png"/> </div><br>
#### 4.2 与文件拥有者和所属群组有关的选项
@ -520,7 +518,7 @@ find 可以使用文件的属性和权限进行搜索。
```html
-name filename
-size [+-]SIZE搜寻比 SIZE 还要大(+)或小(-)的文件。这个 SIZE 的规格有c: 代表 bytek: 代表 1024bytes。所以要找比 50KB 还要大的文件,就是 -size +50k
-size [+-]SIZE搜寻比 SIZE 还要大 (+) 或小 (-) 的文件。这个 SIZE 的规格有c: 代表 bytek: 代表 1024bytes。所以要找比 50KB 还要大的文件,就是 -size +50k
-type TYPE
-perm mode :搜索权限等于 mode 的文件
-perm -mode :搜索权限包含 mode 的文件
@ -545,25 +543,25 @@ find 可以使用文件的属性和权限进行搜索。
Ext2 文件系统使用了上述的文件结构,并在此之上加入了 block 群组的概念,也就是将一个文件系统划分为多个 block 群组,方便管理。
<br><div align="center"> <img src="../pics//1974a836-aa6b-4fb8-bce1-6eb11969284a.jpg"/> </div><br>
<div align="center"> <img src="../pics//1974a836-aa6b-4fb8-bce1-6eb11969284a.jpg"/> </div><br>
## inode
Ext2 文件系统支持的 block 大小有 1k、2k 和 4k 三种,不同的 block 大小限制了单一文件的大小。而每个 inode 大小是固定为 128 bytes。
inode 中记录了文件内容所在的 block但是每个 block 非常小,一个大文件随便都需要几十万的 block而一个 inode 大小有限,无法直接引用这么多 block。因此引入了间接、双间接、三间接引用。间接引用是指让 inode 记录的引用 block 块当成 inode 用来记录引用信息。
inode 中记录了文件内容所在的 block但是每个 block 非常小,一个大文件随便都需要几十万的 block而一个 inode 大小有限,无法直接引用这么多 block。因此引入了间接、双间接、三间接引用。间接引用是指让 inode 记录的引用 block 块当成 inode 用来记录引用信息。
<br><div align="center"> <img src="../pics//89091427-7b2b-4923-aff6-44681319a8aa.jpg"/> </div><br>
<div align="center"> <img src="../pics//89091427-7b2b-4923-aff6-44681319a8aa.jpg"/> </div><br>
inode 具体包含以下信息:
- 该文件的存取模式(read/write/excute)
- 该文件的拥有者与群组(owner/group)
- 该文件的存取模式 (read/write/excute)
- 该文件的拥有者与群组 (owner/group)
- 该文件的容量;
- 该文件建立或状态改变的时间(ctime)
- 最近一次的读取时间(atime)
- 最近修改的时间(mtime)
- 定义文件特性的旗标(flag),如 SetUID...
- 该文件建立或状态改变的时间 (ctime)
- 最近一次的读取时间 (atime)
- 最近修改的时间 (mtime)
- 定义文件特性的旗标 (flag),如 SetUID...
- 该文件真正内容的指向 (pointer)。
## 目录的 inode 与 block
@ -637,7 +635,7 @@ $ gzip [-cdtv#] filename
-d :解压缩
-t :检验压缩文件是否出错
-v :显示压缩比等信息
-# # 为数字的意思代表压缩等级数字越大压缩比越高默认为6
-# # 为数字的意思,代表压缩等级,数字越大压缩比越高,默认为 6
```
### 2. bzip2
@ -668,12 +666,12 @@ $ xz [-dtlkc#] filename
压缩指令只能对一个文件进行压缩而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gip、bzip2、xz 将打包文件进行压缩。
```html
$ tar [-z|-j|-J] [cv] [-f 新建的tar文件] filename... ==打包压缩
$ tar [-z|-j|-J] [tv] [-f 已有的tar文件] ==查看
$ tar [-z|-j|-J] [xv] [-f 已有的tar文件] [-C 目录] ==解压缩
-z 使用zip
-j 使用bzip2
-J 使用xz
$ tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename... ==打包压缩
$ tar [-z|-j|-J] [tv] [-f 已有的 tar 文件] ==查看
$ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩
-z :使用 zip
-j :使用 bzip2
-J :使用 xz
-c :新建打包文件;
-t :查看打包文件里面有哪些文件;
-x :解打包或解压缩的功能;
@ -688,7 +686,6 @@ $ tar [-z|-j|-J] [xv] [-f 已有的tar文件] [-C 目录] ==解压缩
| 查 看 | tar -jtv -f filename.tar.bz2 |
| 解压缩 | tar -jxv -f filename.tar.bz2 -C 要解压缩的目录 |
# Bash
可以通过 Shell 请求内核提供服务Bash 正是 Shell 的一种。
@ -725,8 +722,7 @@ $ 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。
@ -739,10 +735,10 @@ $ declare [-aixr] variable
-a 定义为数组类型
-i 定义为整数类型
-x 定义为环境变量
-r 定义为readonly类型
-r 定义为 readonly 类型
```
使用 [ ] 来对数组进行操作:
使用 [ ] 来对数组进行索引操作:
```bash
$ array[1]=a
@ -761,10 +757,9 @@ $ echo ${array[1]}
重定向就是使用文件代替标准输入、标准输出和标准错误输出。
1. 标准输入(stdin) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:代码为 0 ,使用 < <<
2. 标准输出(stdout)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:代码为 1 ,使用 > 或 >>
3. 标准错误输出(stderr):代码为 2 ,使用 2> 或 2>>
1. 标准输入 (stdin) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:代码为 0 ,使用 < <<
2. 标准输出 (stdout)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:代码为 1 ,使用 > 或 >>
3. 标准错误输出 (stderr):代码为 2 ,使用 2> 或 2>>
其中,有一个箭头的表示以覆盖的方式重定向,而有两个箭头的表示以追加的方式重定向。
@ -778,7 +773,7 @@ $ find /home -name .bashrc > list 2>&1
## 管线指令
管线是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的格式时就可以使用管线。在命令之间使用 | 分隔各个管线命令。
管线是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管线。在命令之间使用 | 分隔各个管线命令。
```bash
$ ls -al /etc | less
@ -786,9 +781,7 @@ $ ls -al /etc | less
### 1. 提取指令cut
提取过程一行一行地进行。
cut 对数据进行切分,取出想要的部分。
cut 对数据进行切分,取出想要的部分。提取过程一行一行地进行。
```html
$ cut
@ -877,7 +870,7 @@ $ tee [-a] file
### 4. 字符转换指令tr、col、expand、join、paste
**tr** 用来删除一行中的字符,或者对字符进行替换。
**tr** 用来删除一行中的字符,或者对字符进行替换。
```html
$ tr [-ds] SET1 ...
@ -981,7 +974,7 @@ $ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt)
## awk
```html
$ awk '条件类型1{动作1} 条件类型2{动作2} ...' filename
$ awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename
```
awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$nn 为字段号,从 1 开始,\$0 表示一整行。
@ -1018,7 +1011,7 @@ dmtsai lines: 4 columns: 10
dmtsai lines: 5 columns: 9
```
可以使用大于等于逻辑,其中等于使用 ==。
可以使用条件,其中等于使用 ==。
范例 3/etc/passwd 文件第三个字段为 UID对 UID 小于 10 的数据进行处理。
@ -1031,7 +1024,7 @@ daemon 2
# vim 三个模式
<br><div align="center"> <img src="../pics//341c632a-1fc1-4068-9b9f-bf7ef68ebb4c.jpg"/> </div><br>
<div align="center"> <img src="../pics//341c632a-1fc1-4068-9b9f-bf7ef68ebb4c.jpg"/> </div><br>
在指令列模式下,有以下命令用于离开或者存储文件。
@ -1039,14 +1032,12 @@ daemon 2
| -- | -- |
| :w | 写入磁盘|
| :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 |
| :q | 离开|
| :q! | 强制离开不保存|
| :wq | 写入磁盘后离开|
| :wq!| 强制写入磁盘后离开|
| :q | 离开 |
| :q! | 强制离开不保存 |
| :wq | 写入磁盘后离开 |
| :wq!| 强制写入磁盘后离开 |
# 参考资料
- 鸟哥. 鸟 哥 的 Linux 私 房 菜 基 础 篇 第 三 版[J]. 2009.
- [Linux 平台上的软件包管理](https://www.ibm.com/developerworks/cn/linux/l-cn-rpmdpkg/index.html)

View File

@ -33,9 +33,18 @@
* [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-分表与分区的不同)
* [故障转移和故障恢复](#故障转移和故障恢复)
* [1. 故障转移](#1-故障转移)
* [2. 故障恢复](#2-故障恢复)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@ -238,7 +247,7 @@ customer_id_selectivity: 0.0373
### 3.5 聚簇索引
<br><div align="center"> <img src="../pics//b9e9ae8c-e216-4c01-b267-a50dbeb98fa4.jpg"/> </div><br>
<div align="center"> <img src="../pics//b9e9ae8c-e216-4c01-b267-a50dbeb98fa4.jpg"/> </div><br>
聚簇索引并不是一种索引类型,而是一种数据存储方式。
@ -267,7 +276,7 @@ customer_id_selectivity: 0.0373
### 4. 1 B-Tree
<br><div align="center"> <img src="../pics//5ed71283-a070-4b21-85ae-f2cbfd6ba6e1.jpg"/> </div><br>
<div align="center"> <img src="../pics//5ed71283-a070-4b21-85ae-f2cbfd6ba6e1.jpg"/> </div><br>
为了描述 B-Tree首先定义一条数据记录为一个二元组 [key, data]key 为记录的键data 为数据记录除 key 外的数据。
@ -283,7 +292,7 @@ B-Tree 是满足下列条件的数据结构:
### 4.2 B+Tree
<br><div align="center"> <img src="../pics//63cd5b50-d6d8-4df6-8912-ef4a1dd5ba13.jpg"/> </div><br>
<div align="center"> <img src="../pics//63cd5b50-d6d8-4df6-8912-ef4a1dd5ba13.jpg"/> </div><br>
与 B-Tree 相比B+Tree 有以下不同点:
@ -292,7 +301,7 @@ B-Tree 是满足下列条件的数据结构:
### 4.3 带有顺序访问指针的 B+Tree
<br><div align="center"> <img src="../pics//1ee5f0a5-b8df-43b9-95ab-c516c54ec797.jpg"/> </div><br>
<div align="center"> <img src="../pics//1ee5f0a5-b8df-43b9-95ab-c516c54ec797.jpg"/> </div><br>
一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。
@ -351,72 +360,74 @@ do {
# 分库与分表
**1. 分表与分区的不同**
分表,就是讲一张表分成多个小表,这些小表拥有不同的表名;而分区是将一张表的数据分为多个区块,这些区块可以存储在同一个磁盘上,也可以存储在不同的磁盘上,这种方式下表仍然只有一个。
**2. 使用分库与分表的原因**
## 1. 分库与分表的原因
随着时间和业务的发展,数据库中的表会越来越多,并且表中的数据量也会越来越大,那么读写操作的开销也会随着增大。
**3. 垂直切分**
## 2. 实现方式
### 2.1 垂直切分
将表按功能模块、关系密切程度划分出来,部署到不同的库上。例如,我们会建立商品数据库 payDB、用户数据库 userDB 等,分别用来存储项目与商品有关的表和与用户有关的表。
**4. 水平切分**
### 2.2 水平切分
把表中的数据按照某种规则存储到多个结构相同的表中,例如按 id 的散列值、性别等进行划分
把表中的数据按照某种规则存储到多个结构相同的表中,例如按 id 的散列值、性别等进行划分
**5. 垂直切分与水平切分的选择**
### 2.3 切分的选择
如果数据库中的表太多,并且项目各项业务逻辑清晰,那么垂直切分是首选。
如果数据库的表不多,但是单表的数据量很大,应该选择水平切分。
**6. 水平切分的实现方式**
## 3. Merge 存储引擎
最简单的是使用 merge 存储引擎
该存储引擎支持分表
**7. 分库与分表存在的问题**
## 4. 分库与分表存在的问题
(1) 事务问题
### 4.1 事务问题
在执行分库分表之后,由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。
(2) 跨库跨表连接问题
### 4.2 跨库跨表连接问题
在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上。这时,表的连接操作将受到限制,我们无法连接位于不同分库的表,也无法连接分表粒度不同的表,导致原本只需要一次查询就能够完成的业务需要进行多次才能完成。
### 4.3 额外的数据管理负担和数据运算压力
最显而易见的就是数据的定位问题和数据的增删改查的重复执行问题,这些都可以通过应用程序解决,但必然引起额外的逻辑运算。
## 5. 分表与分区的不同
分表,就是讲一张表分成多个小表,这些小表拥有不同的表名;而分区是将一张表的数据分为多个区块,这些区块可以存储在同一个磁盘上,也可以存储在不同的磁盘上,这种方式下表仍然只有一个。
# 故障转移和故障恢复
故障转移也叫做切换,当主库出现故障时就切换到备库,使备库成为主库。故障恢复顾名思义就是从故障中恢复过来,并且保证数据的正确性。
## 1. 故障转移
**1.1 提升备库或切换角色**
**提升备库或切换角色**
提升一台备库为主库,或者在一个主-主复制结构中调整主动和被动角色。
**1.2 虚拟 IP 地址和 IP 托管**
**虚拟 IP 地址和 IP 托管**
为 MySQL 实例指定一个逻辑 IP 地址,当 MySQL 实例失效时,可以将 IP 地址转移到另一台 MySQL 服务器上。
**1.3 中间件解决方案**
**中间件解决方案**
通过代理,可以路由流量到可以使用的服务器上。
<br><div align="center"> <img src="../pics//fabd5fa0-b75e-48d0-9e2c-31471945ceb9.jpg"/> </div><br>
<div align="center"> <img src="../pics//fabd5fa0-b75e-48d0-9e2c-31471945ceb9.jpg"/> </div><br>
**1.4 在应用中处理故障转移**
**在应用中处理故障转移**
将故障转移整合到应用中可能导致应用变得太过笨拙。
## 2. 故障恢复
# 参考资料
- 高性能 MySQL
- [MySQL 索引背后的数据结构及算法原理 ](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
- [MySQL 索引优化全攻略 ](http://www.runoob.com/w3cnote/mysql-index.html)
- [20+ 条 MySQL 性能优化的最佳经验 ](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html)
- [数据库为什么分库分表mysql的分库分表方案](https://www.i3geek.com/archives/1108)

View File

@ -407,7 +407,7 @@ ORDER BY cust_name;
内连接又称等值连接,使用 INNER JOIN 关键字。
```
```sql
select a, b, c
from A inner join B
on A.key = B.key
@ -415,7 +415,7 @@ on A.key = B.key
可以不明确使用 INNER JOIN而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。
```
```sql
select a, b, c
from A, B
where A.key = B.key
@ -431,7 +431,7 @@ where A.key = B.key
**子查询版本**
```
```sql
select name
from employee
where department = (
@ -442,7 +442,7 @@ where department = (
**自连接版本**
```
```sql
select name
from employee as e1, employee as e2
where e1.department = e2.department
@ -457,7 +457,7 @@ where e1.department = e2.department
内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。
```
```sql
select *
from employee natural join department;
```
@ -468,7 +468,7 @@ from employee natural join department;
检索所有顾客的订单信息,包括还没有订单信息的顾客。
```
```sql
select Customers.cust_id, Orders.order_num
from Customers left outer join Orders
on Customers.cust_id = Orders.curt_id;
@ -476,7 +476,7 @@ select Customers.cust_id, Orders.order_num
如果需要统计顾客的订单数,使用聚集函数。
```
```sql
select Customers.cust_id,
COUNT(Orders.order_num) as num_ord
from Customers left outer join Orders
@ -710,7 +710,7 @@ SHOW GRANTS FOR myuser;
GRANT SELECT, INSERT ON mydatabase.* TO myuser;
```
<br><div align="center"> <img src="../pics//c73aa08e-a987-43c9-92be-adea4a884c25.png"/> </div><br>
<div align="center"> <img src="../pics//c73aa08e-a987-43c9-92be-adea4a884c25.png"/> </div><br>
账户用 username@host 的形式定义username@% 使用的是默认主机名。

View File

@ -44,7 +44,7 @@
用 min、max 表示数量范围;用 first、last 表示访问空间的包含范围begin、end 表示访问空间的排除范围,即 end 不包含尾部。
<br><div align="center"> <img src="../pics//05907ab4-42c5-4b5e-9388-6617f6c97bea.jpg"/> </div><br>
<div align="center"> <img src="../pics//05907ab4-42c5-4b5e-9388-6617f6c97bea.jpg"/> </div><br>
布尔相关的命名加上 is、can、should、has 等前缀。

View File

@ -38,7 +38,7 @@
* [30. 包含 min 函数的栈](#30-包含-min-函数的栈)
* [31. 栈的压入、弹出序列](#31-栈的压入弹出序列)
* [32.1 从上往下打印二叉树](#321-从上往下打印二叉树)
* [32.3 把二叉树打印成多行](#323--把二叉树打印成多行)
* [32.2 把二叉树打印成多行](#322-把二叉树打印成多行)
* [32.3 按之字形顺序打印二叉树](#323-按之字形顺序打印二叉树)
* [33. 二叉搜索树的后序遍历序列](#33-二叉搜索树的后序遍历序列)
* [34. 二叉树中和为某一值的路径](#34-二叉树中和为某一值的路径)
@ -89,7 +89,7 @@
## 2. 实现 Singleton
[ 单例模式 ](https://github.com/CyC2018/InterviewNotes/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E7%AC%AC-5-%E7%AB%A0-%E5%8D%95%E4%BB%B6%E6%A8%A1%E5%BC%8F)
[单例模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E7%AC%AC%E4%BA%94%E7%AB%A0-%E5%8D%95%E4%BB%B6%E6%A8%A1%E5%BC%8F)
## 3. 数组中重复的数字
@ -117,9 +117,7 @@ public boolean duplicate(int[] numbers, int length, int[] duplication) {
}
private void swap(int[] numbers, int i, int j) {
int t = numbers[i];
numbers[i] = numbers[j];
numbers[j] = t;
int t = numbers[i]; numbers[i] = numbers[j]; numbers[j] = t;
}
```
@ -129,6 +127,14 @@ private void swap(int[] numbers, int i, int j) {
在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
```html
[
[ 1, 5, 9],
[10, 11, 13],
[12, 13, 15]
]
```
**解题思路**
从右上角开始查找。因为矩阵中的一个数,它左边的数都比它来的小,下边的数都比它来的大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来改变行和列的下标,从而缩小查找区间。
@ -165,9 +171,10 @@ public boolean Find(int target, int [][] array) {
public String replaceSpace(StringBuffer str) {
int n = str.length();
for (int i = 0; i < n; i++) {
if (str.charAt(i) == ' ') str.append(" "); // 尾部填充两个
if (str.charAt(i) == ' ') {
str.append(" "); // 尾部填充两个
}
}
int idxOfOriginal = n - 1;
int idxOfNew = str.length() - 1;
while (idxOfOriginal >= 0 && idxOfNew > idxOfOriginal) {
@ -287,7 +294,7 @@ private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in,
- 如果一个节点有右子树不为空,那么该节点的下一个节点是右子树的最左节点;
- 否则,向上找第一个左链接指向的树包含该节点的祖先节点。
<br><div align="center"> <img src="../pics//6fec7f56-a685-4232-b03e-c92a8dfba486.png"/> </div><br>
<div align="center"> <img src="../pics//6fec7f56-a685-4232-b03e-c92a8dfba486.png"/> </div><br>
```java
public TreeLinkNode GetNext(TreeLinkNode pNode) {
@ -312,46 +319,54 @@ public TreeLinkNode GetNext(TreeLinkNode pNode) {
**解题思路**
添加到栈中的序列顺序会被反转,如果进行两次反转,那么得到的序列依然是正向的。因此,添加的数据需要同时压入两个栈之后才能出栈,这样就能保证出栈的顺序为先进先出
使用两个栈in 栈用来处理 push 操作out 栈用来处理 pop 操作。一个元素进过 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 pop 栈才能出栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,也就是先进先出,这就是队列的顺序
```java
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
Stack<Integer> in = new Stack<Integer>();
Stack<Integer> out = new Stack<Integer>();
public void push(int node) {
stack1.push(node);
in.push(node);
}
public int pop() {
if (stack2.isEmpty()) {
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
if (out.isEmpty()) {
while (!in.isEmpty()) {
out.push(in.pop());
}
}
return stack2.pop();
return out.pop();
}
```
## 10.1 斐波那契数列
```java
private int[] fib = new int[40];
**题目描述**
public Solution() {
以 O(1) 的时间复杂度求菲波那切数列。
```java
public class Solution {
private int[] fib = new int[40];
public Solution() {
fib[1] = 1;
fib[2] = 2;
for (int i = 2; i < fib.length; i++) {
for(int i = 2; i < fib.length; i++) {
fib[i] = fib[i - 1] + fib[i - 2];
}
}
public int Fibonacci(int n) {
}
public int Fibonacci(int n) {
return fib[n];
}
}
```
## 10.2 跳台阶
**题目描述**
一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
```java
public int JumpFloor(int target) {
if (target == 1) return 1;
@ -367,6 +382,10 @@ public int JumpFloor(int target) {
## 10.3 变态跳台阶
**题目描述**
一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级……它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
```java
public int JumpFloorII(int target) {
int[] dp = new int[target];
@ -399,7 +418,6 @@ public int RectCover(int target) {
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。NOTE给出的所有元素都大于 0若数组大小为 0请返回 0。
O(N) 时间复杂度解法:
```java
@ -474,7 +492,6 @@ private boolean backtracking(char[][] matrix, char[] str, boolean[][] used, int
}
```
## 13. 机器人的运动范围
**题目描述**
@ -529,7 +546,7 @@ private void initDigitSum(int rows, int cols) {
**动态规划解法**
[ 分割整数 ](https://github.com/CyC2018/InterviewNotes/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E5%88%86%E5%89%B2%E6%95%B4%E6%95%B0)
[分割整数](https://github.com/CyC2018/InterviewNotes/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E5%88%86%E5%89%B2%E6%95%B4%E6%95%B0)
**贪心解法**
@ -549,7 +566,11 @@ int maxProductAfterCuttin(int length) {
## 15. 二进制中 1 的个数
使用库函数:
**题目描述**
输入一个整数,输出该数二进制表示中 1 的个数。其中负数用补码表示
**使用库函数**
```java
public int NumberOf1(int n) {
@ -557,7 +578,7 @@ public int NumberOf1(int n) {
}
```
O(lgM) 时间复杂度解法,其中 M 表示 1 的个数
**O(lgM) 时间复杂度解法,其中 M 表示 1 的个数**
n&(n-1) 该位运算是去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110100减去 1 得到 10110011这两个数相与得到 10110000。
@ -705,8 +726,8 @@ public boolean isNumeric(char[] str) {
保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。
时间复杂度 : O(n<sup>2</sup>)
空间复杂度 : O(1)
- 时间复杂度 : O(n<sup>2</sup>)
- 空间复杂度 : O(1)
```java
public void reOrderArray(int[] array) {
@ -726,8 +747,8 @@ public void reOrderArray(int[] array) {
}
```
时间复杂度 : O(n)
空间复杂度 : O(n)
- 时间复杂度 : O(n)
- 空间复杂度 : O(n)
```java
public void reOrderArray(int[] array) {
@ -932,6 +953,10 @@ public int min() {
## 31. 栈的压入、弹出序列
**题目描述**
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列 1,2,3,4,5 是某栈的压入顺序,序列 45,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。
```java
public boolean IsPopOrder(int[] pushA, int[] popA) {
int n = pushA.length;
@ -949,6 +974,10 @@ public boolean IsPopOrder(int[] pushA, int[] popA) {
## 32.1 从上往下打印二叉树
**题目描述**
从上往下打印出二叉树的每个节点,同层节点从左至右打印。
```java
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
@ -968,7 +997,7 @@ public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
}
```
## 32.3 把二叉树打印成多行
## 32.2 把二叉树打印成多行
```java
ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
@ -993,6 +1022,10 @@ ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
## 32.3 按之字形顺序打印二叉树
**题目描述**
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
```java
public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
@ -1021,9 +1054,12 @@ public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
}
```
## 33. 二叉搜索树的后序遍历序列
**题目描述**
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。
```java
public boolean VerifySquenceOfBST(int[] sequence) {
if (sequence.length == 0) return false;
@ -1047,6 +1083,10 @@ private boolean verify(int[] sequence, int start, int end) {
## 34. 二叉树中和为某一值的路径
**题目描述**
输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
```java
private ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
@ -1077,16 +1117,15 @@ private void dfs(TreeNode node, int target, int curSum, ArrayList<Integer> path)
第一步,在每个节点的后面插入复制的节点。
<br><div align="center"> <img src="../pics//f8b12555-967b-423d-a84e-bc9eff104b8b.jpg"/> </div><br>
<div align="center"> <img src="../pics//f8b12555-967b-423d-a84e-bc9eff104b8b.jpg"/> </div><br>
第二步,对复制节点的 random 链接进行赋值。
<br><div align="center"> <img src="../pics//7b877a2a-8fd1-40d8-a34c-c445827300b8.jpg"/> </div><br>
<div align="center"> <img src="../pics//7b877a2a-8fd1-40d8-a34c-c445827300b8.jpg"/> </div><br>
第三步,拆分。
<br><div align="center"> <img src="../pics//b2b6253c-c701-4b30-aff4-bc3c713542a7.jpg"/> </div><br>
<div align="center"> <img src="../pics//b2b6253c-c701-4b30-aff4-bc3c713542a7.jpg"/> </div><br>
```java
public RandomListNode Clone(RandomListNode pHead) {
@ -1147,6 +1186,10 @@ private void inOrder(TreeNode node) {
## 37. 序列化二叉树
**题目描述**
请实现两个函数,分别用来序列化和反序列化二叉树。
```java
private String serizeString = "";
@ -1233,10 +1276,9 @@ public int MoreThanHalfNum_Solution(int[] array) {
}
```
## 40. 最小的 K 个数
构建大小为 k 的小顶堆。
**构建大小为 k 的小顶堆**
时间复杂度O(nlgk)
空间复杂度O(k)
@ -1256,7 +1298,7 @@ public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
}
```
利用快速选择
**利用快速选择**
时间复杂度O(n)
空间复杂度O(1)
@ -1316,7 +1358,6 @@ private boolean less(int v, int w) {
## 41.1 数据流中的中位数
**题目描述**
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
@ -1374,9 +1415,12 @@ public char FirstAppearingOnce() {
}
```
## 42. 连续子数组的最大和
**题目描述**
{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为 8从第 0 个开始,到第 3 个为止)
```java
public int FindGreatestSumOfSubArray(int[] array) {
if(array.length == 0) return 0;
@ -1586,6 +1630,10 @@ public int FirstNotRepeatingChar(String str) {
## 51. 数组中的逆序对
**题目描述**
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数 P。
```java
private long cnt = 0;

View File

@ -0,0 +1,356 @@
<!-- GFM-TOC -->
* [事务四大特性](#事务四大特性)
* [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-图)
* [实体的三种联系](#实体的三种联系)
* [表示出现多次的关系](#表示出现多次的关系)
* [联系的多向性](#联系的多向性)
* [表示子类](#表示子类)
* [一些概念](#一些概念)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 事务四大特性
## 1. 原子性
事务被视为不可分割的最小单元,要么全部提交成功,要么全部失败回滚。
## 2. 一致性
事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。
## 3. 隔离性
一个事务所做的修改在最终提交以前,对其它事务是不可见的。也可以理解为多个事务单独执行,互不影响。
## 4. 持久性
一旦事务提交,则其所在的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。持久性通过数据库备份和恢复来保证。
# 数据不一致
## 1. 丢失修改
T<sub>1</sub> 和 T<sub>2</sub> 两个事务同时对一个数据进行修改T<sub>1</sub> 先修改T<sub>2</sub> 随后修改T<sub>2</sub> 的修改覆盖了 T<sub>1</sub> 的修改。
## 2. 读脏数据
T<sub>1</sub> 做修改后写入数据库T<sub>2</sub> 读取这个修改后的数据,但是如果 T<sub>1</sub> 撤销了这次修改,使得 T<sub>2</sub> 读取的数据是脏数据。
## 3. 不可重复读
T<sub>1</sub> 读入某个数据T<sub>2</sub> 对该数据做了修改,如果 T<sub>1</sub> 再读这个数据,该数据已经改变,和最开始读入的是不一样的。
# 隔离级别
数据库管理系统需要防止出现数据不一致问题,并且有多种级别可以实现,这些级别称为隔离级别。
## 1. 未提交读READ UNCOMMITTED
事务中的修改,即使没有提交,对其它事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读。
## 2. 提交读READ COMMITTED
一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所在的修改在提交之前对其它事务使不可见的。这个级别有时候也叫做不可重复读,因为两次执行同样的查询,可能会得到不一样的结果。
## 3. 可重复读REPEATABLE READ
解决了脏读的问题,保证在同一个事务中多次读取同样的记录结果是一致的。
但是会出现幻读的问题,所谓幻读,指的是某个事务在读取某个范围内的记录时,另一个事务会在范围内插入数据,当之前的事务再次读取该范围的记录时,会产生幻行。
## 4. 可串行化SERIALIXABLE
强制事务串行执行,避免幻读。
# 可串行化调度
如果并行的事务的执行结果和某一个串行的方式执行的结果一样,那么可以认为结果是正确的。
# 封锁类型
排它锁 (X 锁)和共享锁 (S 锁),又称写锁和读锁。
- 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何其它锁;
- 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。
# 封锁粒度
应该尽量只锁定需要修改的部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就更小,则系统的并发程度越高。
但是加锁需要消耗资源,锁的各种操作,包括获取锁,检查所是否已经解除、释放锁,都会增加系统开销。因此需要在锁开销以及数据安全性之间做一个权衡。
MySQL 中主要提供了两种锁粒度:行解锁以及表级锁。
# 封锁协议
## 三级封锁协议
<div align="center"> <img src="../pics//785806ed-c46b-4dca-b756-cebe7bf8ac3a.jpg"/> </div><br>
**1 级封锁协议**
事务 T 要修改数据 A 时必须加 X 锁,直到事务结束才释放锁。
可以解决丢失修改问题;
**2 级封锁协议**
在 1 级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。
可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。
**3 级封锁协议**
在 2 级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。
可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。
## 两段锁协议
加锁和解锁分为两个阶段进行。两段锁是并行事务可串行化的充分条件,但不是必要条件。
```html
lock-x(A)...lock-s(B)...lock-s(c)...unlock(A)...unlock(C)...unlock(B)
```
# 乐观锁和悲观锁
## 悲观锁
假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被阻塞。
## 乐观锁
假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
Java JUC 中的 Atomic 包就是乐观锁的一种实现AtomicInteger 通过 CASCompare And Set操作实现线程安全的自增操作。
乐观锁有两种实现方式,数据版本和时间戳。它们都需要在数据库表中增加一个字段,使用这个字段来判断数据是否过期。例如,数据版本实现方式中,需要在数据库表中增加一个数字类型的 version 字段,当读取数据时,将 version 字段的值一同读出。随后数据每更新一次,对此 version 值加 1。当提交更新的时候判断读出的 version 和数据库表中的 version 是否一致,如果一致,则予以更新;否则认为是过期数据。
## MySQL 隐式和显示锁定
MySQL InnoDB 采用的是两阶段锁协议。在事务执行过程中,随时都可以执行锁定,锁只有在执行 COMMIT 或者 ROLLBACK 的时候才会释放并且所有的锁是在同一时刻被释放。前面描述的锁定都是隐式锁定InnoDB 会根据事务隔离级别在需要的时候自动加锁。
另外InnoDB 也支持通过特定的语句进行显示锁定,这些语句不属于 SQL 规范:
- SELECT ... LOCK IN SHARE MODE
- SELECT ... FOR UPDATE
# 范式
记 A->B 表示 A 函数决定 B也可以说 B 函数依赖于 A。
如果 {A1A2... An} 是关系的一个或多个属性的集合,该集合决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。
对于函数依赖 W->A如果能找到 W 的真子集使得 A 依赖于这个真子集,那么就是部分依赖,否则就是完全依赖;
以下关系中Sno 表示学号Sname 表示学生姓名Sdept 表示学院Cname 表示课程名Mname 表示院长姓名。函数依赖为 (Sno, Cname) -> (Sname, Sdept, Mname)。注:实际开发过程中,不会出现这种表,而是每个实体都放在单独一张表中,然后实体之间的联系表用实体 id 来表示。
<div align="center"> <img src="../pics//b6a678c0-c875-4038-afba-301846620786.jpg"/> </div><br>
不符合范式的关系,会产生很多异常。主要有以下四种异常:
1. 冗余数据
2. 修改异常
3. 删除异常
4. 插入异常,比如如果新插入一个学生的信息,而这个学生还没选课,那么就无法插入该学生。
关系数据库的范式理论就是是为了解决这四种异常。
高级别范式的依赖基于低级别的范式。
## 第一范式 (1NF)
属性不可分。
## 第二范式 (2NF)
每个非主属性完全函数依赖于键码。
可以通过分解来满足。
**分解前**
<div align="center"><img src="https://latex.codecogs.com/gif.latex?S(Sno,Cname,Sname,Sdept,Mname)"/></div> <br>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?(Sno,Cname)->(Sname,Sdept,Mname)"/></div> <br>
**分解后**
<div align="center"><img src="https://latex.codecogs.com/gif.latex?S1(Sno,Sname,Sdept,Mname)"/></div> <br>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?(Sno)->(Sname,Sdept,Mname"/></div> <br>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?(Sdept)->(Mname)"/></div> <br>
<div align="center"> <img src="../pics//8ef22836-8800-4765-b4b8-ade80096b323.jpg"/> </div><br>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?S2(Sno,Cname,Grade)"/></div> <br>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?(Sno,Cname)->(Grade)"/></div> <br>
<div align="center"> <img src="../pics//b0748916-1acd-4138-b24c-69326cb452fe.jpg"/> </div><br>
## 第三范式 (3NF)
非主属性不传递依赖于键码。
上述 S1 存在传递依赖Mname 依赖于 Sdept而 Sdept 又依赖于 Sno可以继续分解。
<div align="center"> <img src="../pics//923896c1-937e-4a38-b8a6-cec3040b4e2a.jpg"/> </div><br>
## BC 范式BCNF
所有属性不传递依赖于键码。
关系模式 STC(Sname, Tname, Cname, Grade),其中四个属性分别为学生姓名、教师姓名、课程名和成绩。有以下函数依赖:
<div align="center"><img src="https://latex.codecogs.com/gif.latex?(Sname,Cname)->(Tname)"/></div> <br>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?(Sname,Cname)->(Grade)"/></div> <br>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?(Sname,Tname)->(Cname)"/></div> <br>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?(Sname,Tname)->(Grade)"/></div> <br>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?(Tname)->(Cname)"/></div> <br>
分解成 SC(Sname, Cname, Grade) 和 ST(Sname, Tname),对于 ST属性之间是多对多关系无函数依赖。
# 约束
## 键码
用于唯一表示一个实体。键码可以由多个属性构成,每个构成键码的属性成为码。
## 单值约束
某个属性的值是唯一的。
## 引用完整性约束
一个实体的属性引用的值在另一个实体的某个属性中存在。
## 域约束
某个属性的值在特定范围之内。
## 一般约束
一般性约束,比如大小约束,数量约束。
# 数据库的三层模式和两层映像
外模式:局部逻辑结构;模式:全局逻辑结构;内模式:物理结构。
## 外模式
又称用户模式,是用户和数据库系统的接口,特定的用户只能访问数据库系统提供给他的外模式中的数据。例如不同的用户创建了不同数据库,那么一个用户只能访问他有权限访问的数据库。一个数据库可以有多个外模式,一个用户只能有一个外模式,但是一个外模式可以给多个用户使用。
## 模式
可以分为概念模式和逻辑模式,概念模式可以用概念 - 关系来描述;逻辑模式使用特定的数据模式(比如关系模型)来描述数据的逻辑结构,这种逻辑结构包括数据的组成、数据项的名称、类型、取值范围。不仅如此,逻辑模式还要描述数据之间的关系,数据的完整性与安全性要求。
## 内模式
又称为存储模式,描述记录的存储方式,例如索引的组织方式、数据是否压缩以及是否加密等等。
## 外模式/模式映像
把外模式的局部逻辑结构和模式的全局逻辑结构联系起来。该映像可以保证数据和应用程序的逻辑独立性。
## 模式/内模式映像
把模式的全局逻辑结构和内模式的物理结构联系起来,该映像可以保证数据和应用程序的物理独立性。
# ER 图
Entity-Relationship包含三个部分实体、属性、联系。
## 实体的三种联系
联系包含 1 对 11 对多,多对多三种。
如果 A 到 B 是 1 对多关系,那么画个带箭头的线段指向 B如果是 1 对 1画两个带箭头的线段如果是多对多画两个不带箭头的线段。
<div align="center"> <img src="../pics//292b4a35-4507-4256-84ff-c218f108ee31.jpg"/> </div><br>
## 表示出现多次的关系
一个实体在联系出现几次,就要用几条线连接。如下表示一个课程的先修关系,先修关系中,应当出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。
<div align="center"> <img src="../pics//8b798007-e0fb-420c-b981-ead215692417.jpg"/> </div><br>
## 联系的多向性
下图中一个联系表示三个实体的关系。虽然老师可以开设多门课,并且可以教授多名学生,但是对于特定的学生和课程,只有一个老师教授,这就构成了一个三元联系。
<div align="center"> <img src="../pics//423f2a40-bee1-488e-b460-8e76c48ee560.png"/> </div><br>
一般只使用二元联系,可以把多元关系转换为二元关系。
<div align="center"> <img src="../pics//de9b9ea0-1327-4865-93e5-6f805c48bc9e.png"/> </div><br>
## 表示子类
用 is-a 联系来表示子类,具体做法是用一个三角形和两条线来连接类和子类。与子类有关的属性和联系都连到子类上,而与父类和子类都有关的连到父类上。
<div align="center"> <img src="../pics//7ec9d619-fa60-4a2b-95aa-bf1a62aad408.jpg"/> </div><br>
# 一些概念
**数据模型** 由数据结构、数据操作和完整性三个要素组成。
**数据库系统** 包括了数据库,数据库管理系统,应用程序以及数据库管理员和用户,还包括相关的硬件和软件。也就是说数据库系统包含所有与数据库相关的内容。
# 参考资料
- 史嘉权. 数据库系统概论[M]. 清华大学出版社有限公司, 2006.
- 施瓦茨. 高性能MYSQL(第3版)[M]. 电子工业出版社, 2013.
- [MySQL 乐观锁与悲观锁 ](https://www.jianshu.com/p/f5ff017db62a)

View File

@ -86,7 +86,7 @@
T(N)=aN<sup>3</sup> 转换为 lg(T(N))=3lgN+lga
<br><div align="center"> <img src="../pics//5510045a-8f32-487f-a756-463e51a6dab0.png"/> </div><br>
<div align="center"> <img src="../pics//5510045a-8f32-487f-a756-463e51a6dab0.png"/> </div><br>
## 2. 数学模型
@ -94,13 +94,13 @@ T(N)=aN<sup>3</sup> 转换为 lg(T(N))=3lgN+lga
使用 \~f(N) 来表示所有随着 N 的增大除以 f(N) 的结果趋近于 1 的函数 , 例如 N<sup>3</sup>/6-N<sup>2</sup>/2+N/3 \~ N<sup>3</sup>/6。
<br><div align="center"> <img src="../pics//ca3a793e-06e5-4ff3-b28e-a9c20540d164.png"/> </div><br>
<div align="center"> <img src="../pics//ca3a793e-06e5-4ff3-b28e-a9c20540d164.png"/> </div><br>
**增长数量级**
增长数量级将算法与它的实现隔离开来,一个算法的增长数量级为 N<sup>3</sup> 与它是否用 Java 实现,是否运行于特定计算机上无关。
<br><div align="center"> <img src="../pics//1ea4dc9a-c4dd-46b5-bb11-49f98d57ded1.png"/> </div><br>
<div align="center"> <img src="../pics//1ea4dc9a-c4dd-46b5-bb11-49f98d57ded1.png"/> </div><br>
**内循环**
@ -166,7 +166,7 @@ public class ThreeSumFast {
如果 T(N) \~ aN<sup>b</sup>lgN那么 T(2N)/T(N) \~ 2<sup>b</sup>,例如对于暴力方法的 ThreeSum 算法,近似时间为 \~N<sup>3</sup>/6对它进行倍率实验得到如下结果
<br><div align="center"> <img src="../pics//6f5ed46f-86d7-4852-a34f-c1cf1b6343a0.png"/> </div><br>
<div align="center"> <img src="../pics//6f5ed46f-86d7-4852-a34f-c1cf1b6343a0.png"/> </div><br>
可见 T(2N)/T(N)\~2<sup>3</sup>,也就是 b 为 3。
@ -221,7 +221,7 @@ private void exch(Comparable[] a, int i, int j){
找到数组中的最小元素,然后将它与数组的第一个元素交换位置。然后再从剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。
<br><div align="center"> <img src="../pics//222768a7-914f-4d64-b874-d98f3b926fb6.jpg"/> </div><br>
<div align="center"> <img src="../pics//222768a7-914f-4d64-b874-d98f3b926fb6.jpg"/> </div><br>
```java
public class Selection {
@ -244,7 +244,7 @@ public class Selection {
将一个元素插入到已排序的数组中,使得插入之后的数组也是有序的。插入排序从左到右插入每个元素,每次插入之后左部的子数组是有序的。
<br><div align="center"> <img src="../pics//065c3bbb-3ea0-4dbf-8f26-01d0e0ba7db7.png"/> </div><br>
<div align="center"> <img src="../pics//065c3bbb-3ea0-4dbf-8f26-01d0e0ba7db7.png"/> </div><br>
```java
public class Insertion {
@ -275,7 +275,7 @@ public class Insertion {
希尔排序使用插入排序对间隔 h 的序列进行排序,如果 h 很大,那么元素就能很快的移到很远的地方。通过不断减小 h最后令 h=1就可以使得整个数组是有序的。
<br><div align="center"> <img src="../pics//8320bad6-3f91-4a15-8e3d-68e8f39649b5.png"/> </div><br>
<div align="center"> <img src="../pics//8320bad6-3f91-4a15-8e3d-68e8f39649b5.png"/> </div><br>
```java
public class Shell {
@ -303,7 +303,7 @@ public class Shell {
归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。
<br><div align="center"> <img src="../pics//dcf265ad-fe35-424d-b4b7-d149cdf239f4.png"/> </div><br>
<div align="center"> <img src="../pics//dcf265ad-fe35-424d-b4b7-d149cdf239f4.png"/> </div><br>
### 2.1 归并方法
@ -345,9 +345,9 @@ private static void sort(Comparable[] a, int lo, int hi) {
}
```
<br><div align="center"> <img src="../pics//6468a541-3a9a-4008-82b6-03a0fe941d2a.png"/> </div><br>
<div align="center"> <img src="../pics//6468a541-3a9a-4008-82b6-03a0fe941d2a.png"/> </div><br>
<br><div align="center"> <img src="../pics//c7665f73-c52f-4ce4-aed3-592bbd76265b.png"/> </div><br>
<div align="center"> <img src="../pics//c7665f73-c52f-4ce4-aed3-592bbd76265b.png"/> </div><br>
很容易看出该排序算法的时间复杂度为 O(Nlg<sub>N</sub>)。
@ -357,7 +357,7 @@ private static void sort(Comparable[] a, int lo, int hi) {
先归并那些微型数组,然后成对归并得到的子数组。
<br><div align="center"> <img src="../pics//c7b9b4c8-83d1-4eb0-8408-ea6576a9ed90.png"/> </div><br>
<div align="center"> <img src="../pics//c7b9b4c8-83d1-4eb0-8408-ea6576a9ed90.png"/> </div><br>
```java
public static void busort(Comparable[] a) {
@ -377,7 +377,7 @@ public static void busort(Comparable[] a) {
归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序;快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。
<br><div align="center"> <img src="../pics//61b4832d-71f3-413c-84b6-237e219b9fdc.png"/> </div><br>
<div align="center"> <img src="../pics//61b4832d-71f3-413c-84b6-237e219b9fdc.png"/> </div><br>
```java
public class QuickSort {
@ -399,7 +399,7 @@ public class QuickSort {
取 a[lo] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于等于它的元素,交换这两个元素,并不断继续这个过程,就可以保证左指针的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[lo] 和左子数组最右侧的元素 a[j] 交换然后返回 j 即可。
<br><div align="center"> <img src="../pics//e198c201-f386-4491-8ad6-f7e433bf992d.png"/> </div><br>
<div align="center"> <img src="../pics//e198c201-f386-4491-8ad6-f7e433bf992d.png"/> </div><br>
```java
private static int partition(Comparable[] a, int lo, int hi) {
@ -440,7 +440,7 @@ private static int partition(Comparable[] a, int lo, int hi) {
三向切分快速排序对于只有若干不同主键的随机数组可以在线性时间内完成排序。
<br><div align="center"> <img src="../pics//9d2226dc-c4a3-40ec-9b3e-a46bf86af499.png"/> </div><br>
<div align="center"> <img src="../pics//9d2226dc-c4a3-40ec-9b3e-a46bf86af499.png"/> </div><br>
```java
public class Quick3Way {
@ -470,7 +470,7 @@ public class Quick3Way {
堆可以用数组来表示,因为堆是一种完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2而它的两个子节点的位置分别为 2k 和 2k+1。这里我们不使用数组索引为 0 的位置,是为了更清晰地理解节点的关系。
<br><div align="center"> <img src="../pics//a9b6c1db-0f4a-4e91-8ac8-6b19bd106b51.png"/> </div><br>
<div align="center"> <img src="../pics//a9b6c1db-0f4a-4e91-8ac8-6b19bd106b51.png"/> </div><br>
```java
public class MaxPQ<Key extends Comparable<Key> {
@ -561,7 +561,7 @@ public Key delMax() {
无序数组建立堆最直接的方法是从左到右遍历数组,然后进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,因此可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。
<br><div align="center"> <img src="../pics//a2670745-a7b1-497b-90a4-dbddc4e2006d.jpg"/> </div><br>
<div align="center"> <img src="../pics//a2670745-a7b1-497b-90a4-dbddc4e2006d.jpg"/> </div><br>
```java
public static void sort(Comparable[] a){
@ -590,7 +590,7 @@ public static void sort(Comparable[] a){
### 5.1 排序算法的比较
<br><div align="center"> <img src="../pics//be53c00b-2534-4dc6-ad03-c55995c47db9.jpg"/> </div><br>
<div align="center"> <img src="../pics//be53c00b-2534-4dc6-ad03-c55995c47db9.jpg"/> </div><br>
快速排序时最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间增长数量级为 \~cNlgN这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分之后,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。
@ -619,17 +619,17 @@ public static Comparable select(Comparable[] a, int k) {
# 查找
本章使用三种经典的数据类型来实现高效的符号表:二叉查找树、红黑树和散列表。
本章使用三种经典的数据结构来实现高效的符号表:二叉查找树、红黑树和散列表。
## 1. 符号表
### 1.1 无序符号表
<br><div align="center"> <img src="../pics//b69d7184-ab62-4957-ba29-fb4fa25f9b65.jpg"/> </div><br>
<div align="center"> <img src="../pics//b69d7184-ab62-4957-ba29-fb4fa25f9b65.jpg"/> </div><br>
### 1.2 有序符号表
<br><div align="center"> <img src="../pics//ba6ae411-82da-4d86-a434-6776d1731e8e.jpg"/> </div><br>
<div align="center"> <img src="../pics//ba6ae411-82da-4d86-a434-6776d1731e8e.jpg"/> </div><br>
有序符号表的键需要实现 Comparable 接口。
@ -708,7 +708,7 @@ public class BinarySearchST<Key extends Comparable<Key>, Value> {
**二叉查找树** BST是一颗二叉树并且每个节点的键都大于其左子树中的任意节点的键而小于右子树的任意节点的键。
<br><div align="center"> <img src="../pics//25226bb2-92cc-40cb-9e7f-c44e79fbb64a.jpg"/> </div><br>
<div align="center"> <img src="../pics//25226bb2-92cc-40cb-9e7f-c44e79fbb64a.jpg"/> </div><br>
二叉查找树的查找操作每次迭代都会让区间减少一半,和二分查找类似。
@ -781,7 +781,7 @@ private Node put(Node x, Key key, Value val) {
二叉查找树的算法运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 lgN。在最坏的情况下树的高度为 N。
<br><div align="center"> <img src="../pics//73a3983d-dd18-4373-897e-64b706a7e370.jpg"/> </div><br>
<div align="center"> <img src="../pics//73a3983d-dd18-4373-897e-64b706a7e370.jpg"/> </div><br>
复杂度:查找和插入操作都为对数级别。
@ -837,7 +837,7 @@ private Node min(Node x) {
令指向最小节点的链接指向最小节点的右子树。
<br><div align="center"> <img src="../pics//6e2cb20a-8d2a-46fe-9ac7-68a2126b7bd5.jpg"/> </div><br>
<div align="center"> <img src="../pics//6e2cb20a-8d2a-46fe-9ac7-68a2126b7bd5.jpg"/> </div><br>
```java
public void deleteMin() {
@ -855,7 +855,7 @@ public Node deleteMin(Node x) {
如果待删除的节点只有一个子树,那么只需要让指向待删除节点的链接指向唯一的子树即可;否则,让右子树的最小节点替换该节点。
<br><div align="center"> <img src="../pics//b488282d-bfe0-464f-9e91-1f5b83a975bd.jpg"/> </div><br>
<div align="center"> <img src="../pics//b488282d-bfe0-464f-9e91-1f5b83a975bd.jpg"/> </div><br>
```java
public void delete(Key key) {
@ -907,7 +907,7 @@ private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
### 3.1 2-3 查找树
<br><div align="center"> <img src="../pics//2548f2ec-7b00-4ec7-b286-20fc3022e084.jpg"/> </div><br>
<div align="center"> <img src="../pics//2548f2ec-7b00-4ec7-b286-20fc3022e084.jpg"/> </div><br>
一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。
@ -915,7 +915,7 @@ private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
当插入之后产生一个临时 4- 节点时,需要将 4- 节点分裂成 3 个 2- 节点,并将中间的 2- 节点移到上层节点中。如果上移操作继续产生临时 4- 节点则一直进行分裂上移,直到不存在临时 4- 节点。
<br><div align="center"> <img src="../pics//912174d8-0786-4222-b7ef-a611d36e5db9.jpg"/> </div><br>
<div align="center"> <img src="../pics//912174d8-0786-4222-b7ef-a611d36e5db9.jpg"/> </div><br>
#### 3.1.2 性质
@ -927,7 +927,7 @@ private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
2-3 查找树需要用到 2- 节点和 3- 节点,红黑树使用红链接来实现 3- 节点。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。
<br><div align="center"> <img src="../pics//7080a928-06ba-4e10-9792-b8dd190dc8e2.jpg"/> </div><br>
<div align="center"> <img src="../pics//7080a928-06ba-4e10-9792-b8dd190dc8e2.jpg"/> </div><br>
红黑树具有以下性质:
@ -936,7 +936,7 @@ private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
画红黑树时可以将红链接画平。
<br><div align="center"> <img src="../pics//62077f5d-a06d-4129-9b43-78715b82cb03.png"/> </div><br>
<div align="center"> <img src="../pics//62077f5d-a06d-4129-9b43-78715b82cb03.png"/> </div><br>
```java
public class RedBlackBST<Key extends Comparable<Key>, Value> {
@ -970,9 +970,9 @@ public class RedBlackBST<Key extends Comparable<Key>, Value> {
因为合法的红链接都为左链接,如果出现右链接为红链接,那么就需要进行左旋转操作。
<br><div align="center"> <img src="../pics//33a4e822-2dd0-481e-ac89-7f6161034402.jpg"/> </div><br>
<div align="center"> <img src="../pics//33a4e822-2dd0-481e-ac89-7f6161034402.jpg"/> </div><br>
<br><div align="center"> <img src="../pics//5e0cef33-4087-4f21-a428-16d5fddda671.jpg"/> </div><br>
<div align="center"> <img src="../pics//5e0cef33-4087-4f21-a428-16d5fddda671.jpg"/> </div><br>
```java
public Node rotateLeft(Node h) {
@ -991,9 +991,9 @@ public Node rotateLeft(Node h) {
进行右旋转是为了转换两个连续的左红链接,这会在之后的插入过程中探讨。
<br><div align="center"> <img src="../pics//dfd078b2-aa4f-4c50-8319-232922d822b8.jpg"/> </div><br>
<div align="center"> <img src="../pics//dfd078b2-aa4f-4c50-8319-232922d822b8.jpg"/> </div><br>
<br><div align="center"> <img src="../pics//3f8d8c9d-a9a9-4d7a-813c-2de05ee5a97e.jpg"/> </div><br>
<div align="center"> <img src="../pics//3f8d8c9d-a9a9-4d7a-813c-2de05ee5a97e.jpg"/> </div><br>
```java
public Node rotateRight(Node h) {
@ -1011,9 +1011,9 @@ public Node rotateRight(Node h) {
一个 4- 节点在红黑树中表现为一个节点的左右子节点都是红色的。分裂 4- 节点除了需要将子节点的颜色由红变黑之外,同时需要将父节点的颜色由黑变红,从 2-3 树的角度看就是将中间节点移到上层节点。
<br><div align="center"> <img src="../pics//de7c5a31-55f5-4e9d-92ec-4ed5b2ec3828.jpg"/> </div><br>
<div align="center"> <img src="../pics//de7c5a31-55f5-4e9d-92ec-4ed5b2ec3828.jpg"/> </div><br>
<br><div align="center"> <img src="../pics//e5ad625e-729d-4a8d-923a-7c3df5773e1c.jpg"/> </div><br>
<div align="center"> <img src="../pics//e5ad625e-729d-4a8d-923a-7c3df5773e1c.jpg"/> </div><br>
```java
void flipColors(Node h){
@ -1031,7 +1031,7 @@ void flipColors(Node h){
- 如果左子节点是红色的且它的左子节点也是红色的,进行右旋转;
- 如果左右子节点均为红色的,进行颜色转换。
<br><div align="center"> <img src="../pics//40639782-5df2-4e96-a4f3-f9dd664d0ca1.jpg"/> </div><br>
<div align="center"> <img src="../pics//40639782-5df2-4e96-a4f3-f9dd664d0ca1.jpg"/> </div><br>
```java
public void put(Key key, Value val) {
@ -1067,11 +1067,11 @@ private Node put(Node x, Key key, Value val) {
2. 如果当前节点的左子节点是 2- 节点而它的兄弟节点不是 2- 节点,向兄弟节点拿一个 key 过来;
3. 如果当前节点的左子节点和它的兄弟节点都是 2- 节点,将左子节点、父节点中的最小键和最近的兄弟节点合并为一个 4- 节点。
<br><div align="center"> <img src="../pics//b001fa64-307c-49af-b4b2-2043fc26154e.png"/> </div><br>
<div align="center"> <img src="../pics//b001fa64-307c-49af-b4b2-2043fc26154e.png"/> </div><br>
最后得到一个含有最小键的 3- 节点或者 4- 节点,直接从中删除。然后再从头分解所有临时的 4- 节点。
<br><div align="center"> <img src="../pics//70b66757-755c-4e17-a7b7-5ce808023643.png"/> </div><br>
<div align="center"> <img src="../pics//70b66757-755c-4e17-a7b7-5ce808023643.png"/> </div><br>
#### 3.2.6 分析
@ -1147,7 +1147,7 @@ public class Transaction{
拉链法使用链表来存储 hash 值相同的键,从而解决冲突。此时查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。
<br><div align="center"> <img src="../pics//540133af-aaaf-4208-8f7f-33cb89ac9621.png"/> </div><br>
<div align="center"> <img src="../pics//540133af-aaaf-4208-8f7f-33cb89ac9621.png"/> </div><br>
对于 N 个键M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M因此未命中的查找和插入操作所需要的比较次数为 \~N/M。
@ -1155,7 +1155,7 @@ public class Transaction{
线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。使用线程探测法,数组的大小 M 应当大于键的个数 NM>N)。
<br><div align="center"> <img src="../pics//2b3410f1-9559-4dd1-bc3d-e3e572247be2.png"/> </div><br>
<div align="center"> <img src="../pics//2b3410f1-9559-4dd1-bc3d-e3e572247be2.png"/> </div><br>
```java
public class LinearProbingHashST<Key, Value> {
@ -1249,7 +1249,7 @@ public void delete(Key key) {
α = N/Mα 称为利用率。理论证明,当 α 小于 1/2 时探测的预计次数只在 1.5 到 2.5 之间。
<br><div align="center"> <img src="../pics//0ddebc5c-7c24-46b1-98db-4fa5e54db16b.png"/> </div><br>
<div align="center"> <img src="../pics//0ddebc5c-7c24-46b1-98db-4fa5e54db16b.png"/> </div><br>
为了保证散列表的性能,应当调整数组的大小,使得 α 在 [1/4, 1/2] 之间。
@ -1274,13 +1274,13 @@ private void resize(int cap) {
虽然每次重新调整数组都需要重新把每个键值对插入到散列表,但是从摊还分析的角度来看,所需要的代价却是很小的。从下图可以看出,每次数组长度加倍后,累计平均值都会增加 1因为表中每个键都需要重新计算散列值但是随后平均值会下降。
<br><div align="center"> <img src="../pics//01658047-0d86-4a7a-a8ca-7ea20fa1fdde.png"/> </div><br>
<div align="center"> <img src="../pics//01658047-0d86-4a7a-a8ca-7ea20fa1fdde.png"/> </div><br>
## 5. 应用
### 5.1 各种符号表实现的比较
<br><div align="center"> <img src="../pics//9ee83c8c-1165-476c-85a6-e6e434e5307a.jpg"/> </div><br>
<div align="center"> <img src="../pics//9ee83c8c-1165-476c-85a6-e6e434e5307a.jpg"/> </div><br>
应当优先考虑散列表,当需要有序性操作时使用红黑树。

View File

@ -46,12 +46,12 @@
* [1. 读者-写者问题](#1-读者-写者问题)
* [2. 哲学家进餐问题](#2-哲学家进餐问题)
* [第三章 死锁](#第三章-死锁)
* [死锁的条件](#死锁的条件)
* [死锁的必要条件](#死锁的必要条件)
* [死锁的处理方法](#死锁的处理方法)
* [1. 鸵鸟策略](#1-鸵鸟策略)
* [2. 死锁预防](#2-死锁预防)
* [2.1 破坏互斥条件](#21-破坏互斥条件)
* [2.2 破坏请求与保持条件](#22-破坏请求与保持条件)
* [2.2 破坏占有和等待条件](#22-破坏占有和等待条件)
* [2.3 破坏不可抢占条件](#23-破坏不可抢占条件)
* [2.4 破坏环路等待](#24-破坏环路等待)
* [3. 死锁避免](#3-死锁避免)
@ -101,11 +101,13 @@
有两种共享方式:互斥共享和同时共享。
互斥共享的资源称为临界资源,例如打印机等,在同一时间只允许一个进程访问,否则会出现错误,需要用同步机制来实现对临界资源的访问。
互斥共享的资源称为临界资源,例如打印机等,在同一时间只允许一个进程访问,需要用同步机制来实现对临界资源的访问。
### 3. 虚拟
虚拟技术把一个物理实体转换为多个逻辑实体。主要有两种虚拟技术:时分复用技术和空分复用技术,例如多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换。
虚拟技术把一个物理实体转换为多个逻辑实体。
主要有两种虚拟技术:时分复用技术和空分复用技术。例如多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换。
### 4. 异步
@ -135,11 +137,15 @@
### 1. 大内核
大内核是将操作系统功能作为一个紧密结合的整体放到内核,由于各模块共享信息,因此有很高的性能。
大内核是将操作系统功能作为一个紧密结合的整体放到内核。
由于各模块共享信息,因此有很高的性能。
### 2. 微内核
由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。但是需要频繁地在用户态和核心态之间进行切换,会有一定的性能损失。
由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。
因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。
# 第二章 进程管理
@ -153,7 +159,9 @@
### 2. 线程
一个进程中可以有多个线程,线程是独立调度的基本单位。同一个进程中的多个线程之间可以并发执行,它们共享进程资源。
一个进程中可以有多个线程,线程是独立调度的基本单位。
同一个进程中的多个线程之间可以并发执行,它们共享进程资源。
### 3. 区别
@ -161,15 +169,15 @@
- 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。
- 系统开销由于创建或撤销进程时系统都要为之分配或回收资源如内存空间、I/O 设备等,因此操作系统所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置而线程切换时只需保存和设置少量寄存器内容,开销很小。
- 系统开销由于创建或撤销进程时系统都要为之分配或回收资源如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置而线程切换时只需保存和设置少量寄存器内容,开销很小。
- 通信方面:进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信。
- 通信方面:进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信。
举例QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
## 进程状态的切换
<br><div align="center"> <img src="../pics//1706ce58-a081-4fed-9b36-c3c0d7e22b3a.jpg"/> </div><br>
<div align="center"> <img src="../pics//1706ce58-a081-4fed-9b36-c3c0d7e22b3a.jpg"/> </div><br>
阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU缺少 CPU 会让进程从运行态转换为就绪态。
@ -219,13 +227,13 @@ shortest remaining time nextSRTN
#### 2.3 多级反馈队列
<br><div align="center"> <img src="../pics//042cf928-3c8e-4815-ae9c-f2780202c68f.png"/> </div><br>
<div align="center"> <img src="../pics//042cf928-3c8e-4815-ae9c-f2780202c68f.png"/> </div><br>
1. 设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权越高的队列中,为每个进程所规定的执行时间片就越小。
2. 当一个新进程进入内存后,首先将它放入第一队列的末尾,按 FCFS 原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入下一个队列的队尾。
3. 仅当前 i -1 个队列均空时,才会调度第 i 个队列中的进程。
3. 仅当前 i-1 个队列均空时,才会调度第 i 个队列中的进程。
优点:实时性好,同时适合运行短作业和长作业。
@ -233,7 +241,7 @@ shortest remaining time nextSRTN
### 3. 实时系统中的调度
实时系统要一个服务请求在一个确定时间内得到响应。
实时系统要一个服务请求在一个确定时间内得到响应。
分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。
@ -253,9 +261,8 @@ shortest remaining time nextSRTN
### 2. 同步与互斥
同步指多个进程按一定顺序执行;互斥指多个进程在同一时刻只有一个进程能进入临界区。
同步是在对临界区互斥访问的基础上,通过其它机制来实现有序访问的。
- 同步指多个进程按一定顺序执行;
- 互斥指多个进程在同一时刻只有一个进程能进入临界区。
### 3. 信号量
@ -286,9 +293,13 @@ void P2() {
**使用信号量实现生产者-消费者问题**
使用一个互斥量 mutex 来对临界资源进行访问empty 记录空缓冲区的数量full 记录满缓冲区的数量
使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品
注意,必须先执行 down 操作再用互斥量对临界区加锁,否则会出现死锁。因为如果都先对临界区加锁,然后再执行 down 操作,那么可能会出现这种情况:生产者对临界区加锁后,执行 down(empty) 操作,发现 empty = 0此时生成者睡眠。消费者此时不能进入临界区因为生产者对临界区加锁了也就无法执行 up(empty) 操作,那么生产者和消费者就会一直等待下去。
需要使用一个互斥量 mutex 来对缓冲区这个临界资源进行互斥访问。
为了控制生产者和消费者的行为需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计这里需要使用两个信号量empty 记录空缓冲区的数量full 记录满缓冲区的数量。其中empty 信号量是在生产者进程中使用,当 empty 不为 0 时生产者才可以放入物品full 信号量是在消费者进行中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty=0此时生成者睡眠。消费者此时不能进入临界区因为生产者对缓冲区加锁了也就无法执行 up(empty) 操作,那么生产者和消费者就会一直等待下去。
```c
#define N 100
@ -333,12 +344,12 @@ monitor ProducerConsumer
procedure insert();
begin
// ...
end;
procedure remove();
begin
// ...
end;
end monitor;
```
@ -360,7 +371,7 @@ monitor ProducerConsumer
if count = N then wait(full);
insert_item(item);
count := count + 1;
if count = 1 ten signal(empty);
if count = 1 then signal(empty);
end;
function remove: integer;
@ -405,13 +416,13 @@ Linux 中管道是通过空文件来实现。
管道有三种:
1. 普通管道:有两个限制:一是只支持半双工通信方式,即只能单向传输;二是只能在父子进程之间使用;
1. 普通管道:有两个限制,一是只能单向传输;二是只能在父子进程之间使用;
2. 流管道:去除第一个限制,支持双向传输;
3. 命名管道:去除第二个限制,可以在不相关进程之间进行通信。
### 2. 信号量
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。主要作为进程间以及同一进程内不同线程之间的同步手段。
### 3. 消息队列
@ -423,7 +434,9 @@ Linux 中管道是通过空文件来实现。
### 5. 共享内存
共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其它 IPC 运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。
共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
共享内存是最快的 IPC 方式,它是针对其它 IPC 运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。
### 6. 套接字
@ -431,7 +444,7 @@ Linux 中管道是通过空文件来实现。
## 经典同步问题
生产者和消费者问题前面已经讨论过。
生产者和消费者问题前面已经讨论过
### 1. 读者-写者问题
@ -470,9 +483,9 @@ void writer() {
### 2. 哲学家进餐问题
<br><div align="center"> <img src="../pics//a9077f06-7584-4f2b-8c20-3a8e46928820.jpg"/> </div><br>
<div align="center"> <img src="../pics//a9077f06-7584-4f2b-8c20-3a8e46928820.jpg"/> </div><br>
五个哲学家围着一张圆,每个哲学家面前放着饭。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先一根一根拿起左右两边的筷子。
五个哲学家围着一张圆,每个哲学家面前放着饭。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先一根一根拿起左右两边的筷子。
下面是一种错误的解法,考虑到如果每个哲学家同时拿起左手边的筷子,那么就无法拿起右手边的筷子,造成死锁。
@ -495,7 +508,7 @@ void philosopher(int i) {
}
```
为了防止死锁的发生,可以加一点限制,只允许同时拿起左右两边的筷子方法是引入一个互斥量,对拿起两个筷子的那段代码加锁。
为了防止死锁的发生,可以加一点限制,只允许同时拿起左右两边的筷子方法是引入一个互斥量,对拿起两个筷子的那段代码加锁。
```c
semaphore mutex = 1;
@ -518,16 +531,14 @@ void philosopher(int i) {
# 第三章 死锁
## 死锁的条件
## 死锁的必要条件
<br><div align="center"> <img src="../pics//c037c901-7eae-4e31-a1e4-9d41329e5c3e.png"/> </div><br>
<div align="center"> <img src="../pics//c037c901-7eae-4e31-a1e4-9d41329e5c3e.png"/> </div><br>
1. 互斥
2. 请求与保持
3. 不可抢占
4. 环路等待
其中,请求与保持是指一个进程因请求资源而阻塞时,对已获得的资源保持不放。
1. 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
2. 占有和等待:已经得到了某个资源的进程可以再请求新的资源。
3. 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显示地释放。
4. 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。
## 死锁的处理方法
@ -545,7 +556,7 @@ void philosopher(int i) {
例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
#### 2.2 破坏请求与保持条件
#### 2.2 破坏占有和等待条件
一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。
@ -561,7 +572,7 @@ void philosopher(int i) {
#### 3.1 安全状态
<br><div align="center"> <img src="../pics//ed523051-608f-4c3f-b343-383e2d194470.png"/> </div><br>
<div align="center"> <img src="../pics//ed523051-608f-4c3f-b343-383e2d194470.png"/> </div><br>
图 a 的第二列 has 表示已拥有的资源数,第三列 max 表示总共需要的资源数free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源,运行结束后释放 B此时 free 变为 4接着以同样的方式运行 C 和 A使得所有进程都能成功运行因此可以称图 a 所示的状态时安全的。
@ -571,13 +582,13 @@ void philosopher(int i) {
一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。
<br><div align="center"> <img src="../pics//d160ec2e-cfe2-4640-bda7-62f53e58b8c0.png"/> </div><br>
<div align="center"> <img src="../pics//d160ec2e-cfe2-4640-bda7-62f53e58b8c0.png"/> </div><br>
上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。
#### 3.3 多个资源的银行家算法
<br><div align="center"> <img src="../pics//62e0dd4f-44c3-43ee-bb6e-fedb9e068519.png"/> </div><br>
<div align="center"> <img src="../pics//62e0dd4f-44c3-43ee-bb6e-fedb9e068519.png"/> </div><br>
上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。
@ -595,7 +606,7 @@ void philosopher(int i) {
死锁检测的基本思想是,如果一个进程所请求的资源能够被满足,那么就让它执行,释放它拥有的所有资源,然后让其它能满足条件的进程执行。
<br><div align="center"> <img src="../pics//e1eda3d5-5ec8-4708-8e25-1a04c5e11f48.png"/> </div><br>
<div align="center"> <img src="../pics//e1eda3d5-5ec8-4708-8e25-1a04c5e11f48.png"/> </div><br>
上图中,有三个进程四个资源,每个数据代表的含义如下:
@ -612,7 +623,9 @@ void philosopher(int i) {
1. 寻找一个没有标记的进程 P<sub>i</sub>,它所请求的资源小于等于 A。
2. 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。
3. 如果有没有这样一个进程,算法终止。
3. 如果没有这样一个进程,算法终止。
可以看到,死锁检测和银行家算法中判断是否为安全状态的方法类似。
#### 4.2 死锁恢复
@ -635,11 +648,11 @@ void philosopher(int i) {
### 2. 分段
<br><div align="center"> <img src="../pics//22de0538-7c6e-4365-bd3b-8ce3c5900216.png"/> </div><br>
<div align="center"> <img src="../pics//22de0538-7c6e-4365-bd3b-8ce3c5900216.png"/> </div><br>
上图为一个编译器在编译过程中建立的多个表,有 4 个表是动态增长的,如果使用分页系统的一维地址空间,动态递增的特点会导致覆盖问题的出现。
<br><div align="center"> <img src="../pics//e0900bb2-220a-43b7-9aa9-1d5cd55ff56e.png"/> </div><br>
<div align="center"> <img src="../pics//e0900bb2-220a-43b7-9aa9-1d5cd55ff56e.png"/> </div><br>
分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,并且可以动态增长。
@ -675,7 +688,7 @@ void philosopher(int i) {
举例:一个系统为某进程分配了三个物理块,并有如下页面引用序列:
70120304230321201701
<div align="center"><img src="https://latex.codecogs.com/gif.latex?70120304230321201701"/></div> <br>
进程运行时,先将 7,0,1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。
@ -691,13 +704,13 @@ void philosopher(int i) {
可以用栈来实现该算法,栈中存储页面的页面号。当进程访问一个页面时,将该页面的页面号从栈移除,并将它压入栈顶。这样,最近被访问的页面的页面号总是在栈顶,而最近最久未使用的页面的页面号总是在栈底。
47071012126
<div align="center"><img src="https://latex.codecogs.com/gif.latex?47071012126"/></div> <br>
<br><div align="center"> <img src="../pics//eb859228-c0f2-4bce-910d-d9f76929352b.png"/> </div><br>
<div align="center"> <img src="../pics//eb859228-c0f2-4bce-910d-d9f76929352b.png"/> </div><br>
### 4. 时钟Clock
Clock 页面置换算法需要用到一个访问位,当一个页面被访问时,将访问置为 1。
Clock 页面置换算法需要用到一个访问位,当一个页面被访问时,将访问置为 1。
首先,将内存中的所有页面链接成一个循环队列,当缺页中断发生时,检查当前指针所指向页面的访问位,如果访问位为 0就将该页面换出否则将该页的访问位设置为 0给该页面第二次的机会移动指针继续检查。
@ -717,7 +730,7 @@ Clock 页面置换算法需要用到一个访问位,当一个页面被访问
### 3. 扫描算法SCAN
SSTF 会出现进行饥饿现象。考虑以下情况,新进程请求访问的磁道与磁头所在磁道的距离总是比一个在等待的进程来的近,那么等待的进程会一直等待下去。
SSTF 会出现饥饿现象。考虑以下情况,新进程请求访问的磁道与磁头所在磁道的距离总是比一个在等待的进程来的近,那么等待的进程会一直等待下去。
SCAN 算法在 SSTF 算法之上考虑了磁头的移动方向,要求所请求访问的磁道在磁头当前移动方向上才能够得到调度。因为考虑了移动方向,那么一个进程请求访问的磁道一定会得到调度。

View File

@ -95,7 +95,7 @@
网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网是网络的网络。
<br><div align="center"> <img src="../pics//f1fb826b-ecf4-4ddb-91f0-2bafecf08869.jpg"/> </div><br>
<div align="center"> <img src="../pics//f1fb826b-ecf4-4ddb-91f0-2bafecf08869.jpg"/> </div><br>
## ISP
@ -105,14 +105,14 @@
互联网交换点 IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。
<br><div align="center"> <img src="../pics//0f8c0a60-d4c6-47f4-978d-1a5c393fedac.jpg"/> </div><br>
<div align="center"> <img src="../pics//0f8c0a60-d4c6-47f4-978d-1a5c393fedac.jpg"/> </div><br>
## 互联网的组成
1. 边缘部分:所有连接在互联网上的主机,用户可以直接使用;
2. 核心部分:由大量的网络和连接这些网络的路由器组成,为边缘部分的主机提供服务。
<br><div align="center"> <img src="../pics//8ab40d6d-bd7c-47d3-afe8-6a8bc9f5d04c.jpg"/> </div><br>
<div align="center"> <img src="../pics//8ab40d6d-bd7c-47d3-afe8-6a8bc9f5d04c.jpg"/> </div><br>
## 主机之间的通信方式
@ -126,7 +126,7 @@
## 电路交换与分组交换
<br><div align="center"> <img src="../pics//c50d230c-8b89-4644-8f62-8708d03aac5b.jpg"/> </div><br>
<div align="center"> <img src="../pics//c50d230c-8b89-4644-8f62-8708d03aac5b.jpg"/> </div><br>
### 1. 电路交换
@ -140,7 +140,7 @@
分组交换也使用了存储转发,但是转发的是分组而不是报文。把整块数据称为一个报文,由于一个报文可能很长,需要先进行切分,来满足分组能处理的大小。在每个切分的数据前面加上首部之后就成为了分组,首部包含了目的地址和源地址等控制信息。
<br><div align="center"> <img src="../pics//2366c2ad-5859-4d4e-805f-7e2b88061cd8.jpg"/> </div><br>
<div align="center"> <img src="../pics//2366c2ad-5859-4d4e-805f-7e2b88061cd8.jpg"/> </div><br>
存储转发允许在一条传输线路上传送多个主机的分组,因此两个用户之间的通信不需要占用端到端的线路资源。
@ -150,7 +150,7 @@
总时延 = 发送时延 + 传播时延 + 处理时延 + 排队时延
<br><div align="center"> <img src="../pics//ceee91c2-da26-4169-94c3-e4608b46b9ac.png"/> </div><br>
<div align="center"> <img src="../pics//ceee91c2-da26-4169-94c3-e4608b46b9ac.png"/> </div><br>
### 1. 发送时延
@ -178,7 +178,7 @@
## 计算机网络体系结构*
<br><div align="center"> <img src="../pics//1005dc9d-9049-4b06-9524-6171e56ebd8c.png"/> </div><br>
<div align="center"> <img src="../pics//1005dc9d-9049-4b06-9524-6171e56ebd8c.png"/> </div><br>
### 1. 七层协议
@ -205,7 +205,7 @@
路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要运输层和应用层。
<br><div align="center"> <img src="../pics//ac106e7e-489a-4082-abd9-dabebe48394c.jpg"/> </div><br>
<div align="center"> <img src="../pics//ac106e7e-489a-4082-abd9-dabebe48394c.jpg"/> </div><br>
### 4. TCP/IP 体系结构
@ -213,11 +213,11 @@
现在的 TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。
<br><div align="center"> <img src="../pics//37b74a34-251c-45f8-88a4-614ec953f7e9.png"/> </div><br>
<div align="center"> <img src="../pics//37b74a34-251c-45f8-88a4-614ec953f7e9.png"/> </div><br>
TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中占用举足轻重的地位。
<br><div align="center"> <img src="../pics//93cbce0c-c37d-429c-815b-861976a46bd8.png"/> </div><br>
<div align="center"> <img src="../pics//93cbce0c-c37d-429c-815b-861976a46bd8.png"/> </div><br>
# 第二章 物理层
@ -231,7 +231,7 @@ TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中
模拟信号是连续的信号,数字信号是离散的信号。带通调制把数字信号转换为模拟信号。
<br><div align="center"> <img src="../pics//d2c55c84-aa1f-43c1-bd97-457bcb7816b3.png"/> </div><br>
<div align="center"> <img src="../pics//d2c55c84-aa1f-43c1-bd97-457bcb7816b3.png"/> </div><br>
## 信道复用技术
@ -241,19 +241,19 @@ TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中
使用这两种方式进行通信,在通信的过程中用户会一直占用一部分信道资源。但是由于计算机数据的突发性质,没必要一直占用信道资源而不让出给其它用户使用,因此这两种方式对信道的利用率都不高。
<br><div align="center"> <img src="../pics//543d47a1-f0dd-414f-b23c-0c142c814854.png"/> </div><br>
<div align="center"> <img src="../pics//543d47a1-f0dd-414f-b23c-0c142c814854.png"/> </div><br>
### 2. 统计时分复用
是对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成统计时分复用帧然后发送。
<br><div align="center"> <img src="../pics//29058e09-bb72-4040-a73d-4c497895e9ce.jpg"/> </div><br>
<div align="center"> <img src="../pics//29058e09-bb72-4040-a73d-4c497895e9ce.jpg"/> </div><br>
### 3. 波分复用
光的频分复用。由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波。
<br><div align="center"> <img src="../pics//78534153-88d1-4f83-a6e0-59064dbdc43a.png"/> </div><br>
<div align="center"> <img src="../pics//78534153-88d1-4f83-a6e0-59064dbdc43a.png"/> </div><br>
### 4. 码分复用
@ -275,7 +275,7 @@ TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中
码分复用需要发送的数据量为原先的 m 倍。
<br><div align="center"> <img src="../pics//0042edad-8e3b-4279-bd93-6906fcd1b640.jpg"/> </div><br>
<div align="center"> <img src="../pics//0042edad-8e3b-4279-bd93-6906fcd1b640.jpg"/> </div><br>
# 第三章 数据链路层
@ -285,7 +285,7 @@ TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中
将网络层传下来的分组添加首部和尾部,用于标记帧的开始和结束。
<br><div align="center"> <img src="../pics//3402d1c0-7020-4249-9a7f-12ea2ea6adf7.jpg"/> </div><br>
<div align="center"> <img src="../pics//3402d1c0-7020-4249-9a7f-12ea2ea6adf7.jpg"/> </div><br>
### 2. 透明传输
@ -293,7 +293,7 @@ TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中
帧中有首部和尾部,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据中出现首部尾部相同的内容前面插入转义字符,如果需要传输的内容正好就是转义字符,那么就在转义字符前面再加个转义字符,在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。
<br><div align="center"> <img src="../pics//4146e14b-56b9-433c-8e3d-74b1b325399c.jpg"/> </div><br>
<div align="center"> <img src="../pics//4146e14b-56b9-433c-8e3d-74b1b325399c.jpg"/> </div><br>
### 3. 差错检测
@ -303,7 +303,7 @@ TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中
互联网用户通常需要连接到某个 ISP 之后才能接入到互联网PPP 协议就是用户计算机和 ISP 进行通信时所使用的数据链路层协议。
<br><div align="center"> <img src="../pics//8393f520-d824-44ea-a5f3-1c1a73d735fb.jpg"/> </div><br>
<div align="center"> <img src="../pics//8393f520-d824-44ea-a5f3-1c1a73d735fb.jpg"/> </div><br>
在 PPP 的帧中
@ -312,11 +312,11 @@ TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中
- FCS 字段是使用 CRC 的检验序列
- 信息部分的长度不超过 1500
<br><div align="center"> <img src="../pics//0f39c274-b79c-4e83-8c7c-94fc2747832d.jpg"/> </div><br>
<div align="center"> <img src="../pics//0f39c274-b79c-4e83-8c7c-94fc2747832d.jpg"/> </div><br>
## 局域网的拓扑
<br><div align="center"> <img src="../pics//8b15e36f-69b4-46b6-a07c-7234ac7c7927.jpg"/> </div><br>
<div align="center"> <img src="../pics//8b15e36f-69b4-46b6-a07c-7234ac7c7927.jpg"/> </div><br>
## 广播信道 - CSMA/CD 协议*
@ -328,7 +328,7 @@ CSMA/CD 表示载波监听多点接入 / 碰撞检测。
- **载波监听** :每个站都必须不停地检听信道。在发送前,如果检听信道正在使用,就必须等待。
- **碰撞检测** :在发送中,如果检听到信道已有其它站正在发送数据,就表示发生了碰撞。虽然每一个站在发送数据之前都已经检听到信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。
<br><div align="center"> <img src="../pics//f9ed4da5-0032-41e6-991a-36d995ec28fd.png"/> </div><br>
<div align="center"> <img src="../pics//f9ed4da5-0032-41e6-991a-36d995ec28fd.png"/> </div><br>
记端到端的传播时延为 τ,最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞,称 2τ 为 **争用期** 。只有经过争用期之后还没有检测到碰撞,才能肯定这次发送不会发生碰撞。
@ -338,15 +338,15 @@ CSMA/CD 表示载波监听多点接入 / 碰撞检测。
从表面上看,使用集线器的局域网在物理上是一个星型网。但是集线器使用电子器件来模拟实际缆线的工作,逻辑上仍是一个总线网,整个系统仍像一个传统以太网那样运行。
<br><div align="center"> <img src="../pics//3294ff06-f942-425e-aecc-ca04e45566d4.png"/> </div><br>
<div align="center"> <img src="../pics//3294ff06-f942-425e-aecc-ca04e45566d4.png"/> </div><br>
<br><div align="center"> <img src="../pics//b56ef52e-3d0f-4cdd-97dc-eaed893444a5.jpg"/> </div><br>
<div align="center"> <img src="../pics//b56ef52e-3d0f-4cdd-97dc-eaed893444a5.jpg"/> </div><br>
## MAC 层*
MAC 地址是 6 字节48 位)的地址,用于唯一表示网络适配器(网卡),一台主机拥有多少个适配器就有多少个 MAC 地址,例如笔记本电脑普遍存在无线网络适配器和有线网络适配器。
<br><div align="center"> <img src="../pics//50d38e84-238f-4081-8876-14ef6d7938b5.jpg"/> </div><br>
<div align="center"> <img src="../pics//50d38e84-238f-4081-8876-14ef6d7938b5.jpg"/> </div><br>
- **类型** :标记上层使用的协议;
- **数据** :长度在 46-1500 之间,如果太小则需要填充;
@ -357,7 +357,7 @@ MAC 地址是 6 字节48 位)的地址,用于唯一表示网络适配器
虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息,例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。
<br><div align="center"> <img src="../pics//a74b70ac-323a-4b31-b4d5-90569b8a944b.png"/> </div><br>
<div align="center"> <img src="../pics//a74b70ac-323a-4b31-b4d5-90569b8a944b.png"/> </div><br>
# 第四章 网络层*
@ -367,7 +367,7 @@ MAC 地址是 6 字节48 位)的地址,用于唯一表示网络适配器
使用 IP 协议,可以把异构的物理网络连接起来,使得在网络层看起来好像是一个统一的网络。
<br><div align="center"> <img src="../pics//fe3d224c-8ffd-40f9-85b1-86ffe1393f6c.jpg"/> </div><br>
<div align="center"> <img src="../pics//fe3d224c-8ffd-40f9-85b1-86ffe1393f6c.jpg"/> </div><br>
与 IP 协议配套使用的还有三个协议:
@ -375,11 +375,11 @@ MAC 地址是 6 字节48 位)的地址,用于唯一表示网络适配器
2. 网际控制报文协议 ICMPInternet Control Message Protocol
3. 网际组管理协议 IGMPInternet Group Management Protocol
<br><div align="center"> <img src="../pics//163cf8b4-5f30-46c9-af00-316a71b3c890.jpg"/> </div><br>
<div align="center"> <img src="../pics//163cf8b4-5f30-46c9-af00-316a71b3c890.jpg"/> </div><br>
## IP 数据报格式
<br><div align="center"> <img src="../pics//8681db55-0873-434b-aa98-83d07e8392ae.jpg"/> </div><br>
<div align="center"> <img src="../pics//8681db55-0873-434b-aa98-83d07e8392ae.jpg"/> </div><br>
- **版本** : 有 4IPv4和 6IPv6两个值
@ -393,7 +393,7 @@ MAC 地址是 6 字节48 位)的地址,用于唯一表示网络适配器
- **片偏移** : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。
<br><div align="center"> <img src="../pics//45c86855-9b18-4cf4-a9a7-f8b6eb78d133.png"/> </div><br>
<div align="center"> <img src="../pics//45c86855-9b18-4cf4-a9a7-f8b6eb78d133.png"/> </div><br>
- **生存时间** TTL它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位当 TTL 为 0 时就丢弃数据报。
@ -415,7 +415,7 @@ IP 地址的编址方式经历了三个历史阶段:
IP 地址 ::= {< 网络号 >, < 主机号 >}
<br><div align="center"> <img src="../pics//2ddd6132-60be-4a72-9daa-3d9756191f4a.png"/> </div><br>
<div align="center"> <img src="../pics//2ddd6132-60be-4a72-9daa-3d9756191f4a.png"/> </div><br>
### 2. 子网划分
@ -443,7 +443,7 @@ CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为
网络层实现主机之间的通信而链路层实现具体每段链路之间的通信。因此在通信过程中IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。
<br><div align="center"> <img src="../pics//86b71296-0d1e-4a63-bcd9-54955b6b781b.jpg"/> </div><br>
<div align="center"> <img src="../pics//86b71296-0d1e-4a63-bcd9-54955b6b781b.jpg"/> </div><br>
## 地址解析协议 ARP
@ -451,7 +451,7 @@ CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为
每个主机都有一个 ARP 高速缓存,存放映射表。如果一个 IP 地址到 MAC 地址的映射不在该表中,主机通过广播的方式发送 ARP 请求分组,匹配 IP 地址的主机会发送 ARP 响应分组告知其 MAC 地址。
<br><div align="center"> <img src="../pics//8bc6fc2c-d198-4759-b06c-18d94d851e97.png"/> </div><br>
<div align="center"> <img src="../pics//8bc6fc2c-d198-4759-b06c-18d94d851e97.png"/> </div><br>
## 路由器的结构
@ -459,11 +459,11 @@ CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为
分组转发部分由三部分组成:交换结构、一组输入端口和一组输出端口。
<br><div align="center"> <img src="../pics//3a676c54-b559-4466-9b21-eb10f1e25879.jpg"/> </div><br>
<div align="center"> <img src="../pics//3a676c54-b559-4466-9b21-eb10f1e25879.jpg"/> </div><br>
交换结构的交换网络有以下三种实现方式:
<br><div align="center"> <img src="../pics//7f82fd18-7f16-4125-ada6-bb6b795b4fda.png"/> </div><br>
<div align="center"> <img src="../pics//7f82fd18-7f16-4125-ada6-bb6b795b4fda.png"/> </div><br>
## 交换机与路由器的区别
@ -479,7 +479,7 @@ CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为
5. 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器;
6. 报告转发分组出错。
<br><div align="center"> <img src="../pics//8d211911-0e62-4190-ab00-d8610adec4a0.jpg"/> </div><br>
<div align="center"> <img src="../pics//8d211911-0e62-4190-ab00-d8610adec4a0.jpg"/> </div><br>
## 路由选择协议
@ -492,7 +492,7 @@ CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为
1. 内部网关协议 IGPInterior Gateway Protocol在 AS 内部使用,如 RIP 和 OSPF。
2. 外部网关协议 EGPExternal Gateway Protocol在 AS 之间使用,如 BGP。
<br><div align="center"> <img src="../pics//e0be6970-5b0e-44a2-bc71-df4d61c42b8f.jpg"/> </div><br>
<div align="center"> <img src="../pics//e0be6970-5b0e-44a2-bc71-df4d61c42b8f.jpg"/> </div><br>
### 1. 内部网关协议 RIP
@ -532,17 +532,17 @@ BGP 只能寻找一条比较好的路由,而不是最佳路由。它采用路
每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。
<br><div align="center"> <img src="../pics//eb6271de-22c9-4f4b-8b31-eab1f560efac.png"/> </div><br>
<div align="center"> <img src="../pics//eb6271de-22c9-4f4b-8b31-eab1f560efac.png"/> </div><br>
## 网际控制报文协议 ICMP
ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。
<br><div align="center"> <img src="../pics//9b5e0fa0-9274-4219-a3a9-84fbb509c735.jpg"/> </div><br>
<div align="center"> <img src="../pics//9b5e0fa0-9274-4219-a3a9-84fbb509c735.jpg"/> </div><br>
ICMP 报文分为差错报告报文和询问报文。
<br><div align="center"> <img src="../pics//6e11b122-95ce-4869-bf7d-3b0d7591707e.jpg"/> </div><br>
<div align="center"> <img src="../pics//6e11b122-95ce-4869-bf7d-3b0d7591707e.jpg"/> </div><br>
## 分组网间探测 PING
@ -557,7 +557,7 @@ PING 的过程:
在一对多的通信中,多播不需要将分组复制多份,从而大大节约网络资源。
<br><div align="center"> <img src="../pics//c77b6a18-dfac-42a2-ac89-7e99481275dc.jpg"/> </div><br>
<div align="center"> <img src="../pics//c77b6a18-dfac-42a2-ac89-7e99481275dc.jpg"/> </div><br>
## 虚拟专用网 VPN
@ -573,7 +573,7 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。
下图中,场所 A 和 B 的通信部经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。
<br><div align="center"> <img src="../pics//bf4ed077-d481-4db7-9e7a-85d841a5a8c3.jpg"/> </div><br>
<div align="center"> <img src="../pics//bf4ed077-d481-4db7-9e7a-85d841a5a8c3.jpg"/> </div><br>
## 网络地址转换 NAT
@ -581,7 +581,7 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。
在以前NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把运输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。
<br><div align="center"> <img src="../pics//0f31bc7a-d60b-48a6-8e3f-597708369e52.png"/> </div><br>
<div align="center"> <img src="../pics//0f31bc7a-d60b-48a6-8e3f-597708369e52.png"/> </div><br>
# 第五章 运输层*
@ -597,13 +597,13 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。
## UDP 首部格式
<br><div align="center"> <img src="../pics//bd6c05f3-02ee-4c8a-b374-40c87154a898.jpg"/> </div><br>
<div align="center"> <img src="../pics//bd6c05f3-02ee-4c8a-b374-40c87154a898.jpg"/> </div><br>
首部字段只有 8 个字节包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和而临时添加的。
## TCP 首部格式
<br><div align="center"> <img src="../pics//21a00b02-c0a6-4bcd-9af0-5ec6bb66e34c.jpg"/> </div><br>
<div align="center"> <img src="../pics//21a00b02-c0a6-4bcd-9af0-5ec6bb66e34c.jpg"/> </div><br>
- **序号** :用于对字节流进行编号,例如序号为 301表示第一个字节的编号为 301如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
@ -621,7 +621,7 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。
## TCP 的三次握手
<br><div align="center"> <img src="../pics//086871db-5871-460f-97b7-126cd738bb0e.jpg"/> </div><br>
<div align="center"> <img src="../pics//086871db-5871-460f-97b7-126cd738bb0e.jpg"/> </div><br>
假设 A 为客户端B 为服务器端。
@ -634,7 +634,7 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。
## TCP 的四次挥手
<br><div align="center"> <img src="../pics//78f65456-666b-4044-b4ee-f7692dbbc0d3.jpg"/> </div><br>
<div align="center"> <img src="../pics//78f65456-666b-4044-b4ee-f7692dbbc0d3.jpg"/> </div><br>
以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK因为 ACK 在连接建立之后都为 1。
@ -652,7 +652,7 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。
## TCP 滑动窗口
<br><div align="center"> <img src="../pics//223fc26e-2fd6-484c-bcb7-443cac134f15.jpg"/> </div><br>
<div align="center"> <img src="../pics//223fc26e-2fd6-484c-bcb7-443cac134f15.jpg"/> </div><br>
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
@ -684,7 +684,7 @@ TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接受,而拥塞控制是为了降低整个网络的拥塞程度。
<br><div align="center"> <img src="../pics//a69af9bb-b5ad-4896-862d-697e5ee4feb1.png"/> </div><br>
<div align="center"> <img src="../pics//a69af9bb-b5ad-4896-862d-697e5ee4feb1.png"/> </div><br>
TCP 主要通过四种算法来进行拥塞控制慢开始、拥塞避免、快重传、快恢复。发送方需要维护有一个叫做拥塞窗口cwnd的状态变量。注意拥塞窗口与发送方窗口的区别拥塞窗口只是一个状态变量实际决定发送方能发送多少数据的是发送方窗口。
@ -693,7 +693,7 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、
1. 接收方有足够大的接收缓存,因此不会发生流量控制;
2. 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。
<br><div align="center"> <img src="../pics//346244ff-98c1-4f12-9a87-d0832e8c04cf.jpg"/> </div><br>
<div align="center"> <img src="../pics//346244ff-98c1-4f12-9a87-d0832e8c04cf.jpg"/> </div><br>
### 慢开始与拥塞避免
@ -711,7 +711,7 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、
在这种情况下,只是丢失个别报文段,而不是网络拥塞,因此执行快恢复,令 ssthresh = cwnd / 2 cwnd = ssthresh注意到此时直接进入拥塞避免。
<br><div align="center"> <img src="../pics//b18d679b-c8e2-4564-88ee-7600090e46da.jpg"/> </div><br>
<div align="center"> <img src="../pics//b18d679b-c8e2-4564-88ee-7600090e46da.jpg"/> </div><br>
# 第六章 应用层*
@ -725,9 +725,9 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、
一个域名由多个层次构成,从上层到下层分别为顶级域名、二级域名、三级域名以及四级域名。所有域名可以画成一颗域名树。
<br><div align="center"> <img src="../pics//c2117f61-1177-4768-bf33-cf4f950d911c.png"/> </div><br>
<div align="center"> <img src="../pics//c2117f61-1177-4768-bf33-cf4f950d911c.png"/> </div><br>
<br><div align="center"> <img src="../pics//a4b162e5-db2a-4a27-b213-1fe481c5a06a.png"/> </div><br>
<div align="center"> <img src="../pics//a4b162e5-db2a-4a27-b213-1fe481c5a06a.png"/> </div><br>
域名服务器可以分为以下四类:
@ -738,11 +738,11 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、
区和域的概念不同,可以在一个域中划分多个区。图 b 在域 abc.com 中划分了两个区abc.com 和 y.abc.com
<br><div align="center"> <img src="../pics//fc0c6b2d-68c7-4de8-aaaa-97355a4f0472.jpg"/> </div><br>
<div align="center"> <img src="../pics//fc0c6b2d-68c7-4de8-aaaa-97355a4f0472.jpg"/> </div><br>
因此就需要两个权限域名服务器:
<br><div align="center"> <img src="../pics//8b335d94-c1ca-42e1-ad48-bb179d28a4f1.jpg"/> </div><br>
<div align="center"> <img src="../pics//8b335d94-c1ca-42e1-ad48-bb179d28a4f1.jpg"/> </div><br>
### 2. 解析过程
@ -750,13 +750,13 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、
迭代的方式下,本地域名服务器向一个域名服务器解析请求解析之后,结果返回到本地域名服务器,然后本地域名服务器继续向其它域名服务器请求解析;而递归地方式下,结果不是直接返回的,而是继续向前请求解析,最后的结果才会返回。
<br><div align="center"> <img src="../pics//6bc61bb8-3b1c-4dc8-ac25-cef925ace0eb.jpg"/> </div><br>
<div align="center"> <img src="../pics//6bc61bb8-3b1c-4dc8-ac25-cef925ace0eb.jpg"/> </div><br>
## 文件传输协议 FTP
FTP 在运输层使用 TCP并且需要建立两个并行的 TCP 连接:控制连接和数据连接。控制连接在整个会话期间一直保持打开,而数据连接在数据传送完毕之后就关闭。控制连接使用端口号 21数据连接使用端口号 20。
<br><div align="center"> <img src="../pics//58633775-8584-4a01-ad3f-eee4d9a466e1.jpg"/> </div><br>
<div align="center"> <img src="../pics//58633775-8584-4a01-ad3f-eee4d9a466e1.jpg"/> </div><br>
## 远程终端协议 TELNET
@ -772,7 +772,7 @@ TELNET 可以适应许多计算机和操作系统的差异,例如不同操作
一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件发送协议和读取协议。其中发送协议常用 SMTP读取协议常用 POP3 和 IMAP。
<br><div align="center"> <img src="../pics//de1e46d2-748f-4da3-a29e-7de7bc840366.jpg"/> </div><br>
<div align="center"> <img src="../pics//de1e46d2-748f-4da3-a29e-7de7bc840366.jpg"/> </div><br>
### POP3
@ -786,7 +786,7 @@ IMAP 协议中客户端和服务器上的邮件保持同步,如果不去手动
SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP而是增加邮件主题的结构定义了非 ASCII 码的编码规则。
<br><div align="center"> <img src="../pics//ed5522bb-3a60-481c-8654-43e7195a48fe.png"/> </div><br>
<div align="center"> <img src="../pics//ed5522bb-3a60-481c-8654-43e7195a48fe.png"/> </div><br>
## 动态主机配置协议 DHCP

View File

@ -32,7 +32,7 @@
需要说明的一点是,文中的 UML 类图和规范的 UML 类图不大相同,其中组合关系使用以下箭头表示:
<br><div align="center"> <img src="../pics//09e398d8-9c6e-48f6-b48b-8b4f9de61d1d.png"/> </div><br>
<div align="center"> <img src="../pics//09e398d8-9c6e-48f6-b48b-8b4f9de61d1d.png"/> </div><br>
# 第一章 设计模式入门
@ -50,7 +50,7 @@
使用继承的解决方案如下,这种方案代码无法复用,如果两个鸭子类拥有同样的飞行方式,就有两份重复的代码。
<br><div align="center"> <img src="../pics//144d28a0-1dc5-4aba-8961-ced5bc88428a.jpg"/> </div><br>
<div align="center"> <img src="../pics//144d28a0-1dc5-4aba-8961-ced5bc88428a.jpg"/> </div><br>
**4. 设计原则**
@ -60,17 +60,17 @@
运用这一原则,将叫和飞行的行为抽象出来,实现多种不同的叫和飞行的子类,让子类去实现具体的叫和飞行方式。
<br><div align="center"> <img src="../pics//1c8ccf5c-7ecd-4b8a-b160-3f72a510ce26.png"/> </div><br>
<div align="center"> <img src="../pics//1c8ccf5c-7ecd-4b8a-b160-3f72a510ce26.png"/> </div><br>
**多用组合,少用继承** 组合也就是 has-a 关系,通过组合,可以在运行时动态改变实现,只要通过改变父类对象具体指向哪个子类即可。而继承就不能做到这些,继承体系在创建类时就已经确定。
运用这一原则,在 Duck 类中组合 FlyBehavior 和 QuackBehavior 类performQuack() 和 performFly() 方法委托给这两个类去处理。通过这种方式,一个 Duck 子类可以根据需要去实例化 FlyBehavior 和 QuackBehavior 的子类对象,并且也可以动态地进行改变。
<br><div align="center"> <img src="../pics//29574e6f-295c-444e-83c7-b162e8a73a83.jpg"/> </div><br>
<div align="center"> <img src="../pics//29574e6f-295c-444e-83c7-b162e8a73a83.jpg"/> </div><br>
**5. 整体设计图**
<br><div align="center"> <img src="../pics//e13833c8-e215-462e-855c-1d362bb8d4a0.jpg"/> </div><br>
<div align="center"> <img src="../pics//e13833c8-e215-462e-855c-1d362bb8d4a0.jpg"/> </div><br>
**6. 模式定义**
@ -185,7 +185,7 @@ FlyBehavior.FlyNoWay
定义了对象之间的一对多依赖当一个对象改变状态时它的所有依赖者都会受到通知并自动更新。主题Subject是被观察的对象而其所有依赖者Observer成为观察者。
<br><div align="center"> <img src="../pics//26cb5e7e-6fa3-44ad-854e-fe24d1a5278c.jpg"/> </div><br>
<div align="center"> <img src="../pics//26cb5e7e-6fa3-44ad-854e-fe24d1a5278c.jpg"/> </div><br>
**2. 模式类图**
@ -193,7 +193,7 @@ FlyBehavior.FlyNoWay
观察者拥有一个主题对象的引用,因为注册、移除还有数据都在主题当中,必须通过操作主题才能完成相应功能。
<br><div align="center"> <img src="../pics//5c558190-fccd-4b5e-98ed-1896653fc97f.jpg"/> </div><br>
<div align="center"> <img src="../pics//5c558190-fccd-4b5e-98ed-1896653fc97f.jpg"/> </div><br>
**3. 问题描述**
@ -201,7 +201,7 @@ FlyBehavior.FlyNoWay
**4. 解决方案类图**
<br><div align="center"> <img src="../pics//760a5d63-d96d-4dd9-bf9a-c3d126b2f401.jpg"/> </div><br>
<div align="center"> <img src="../pics//760a5d63-d96d-4dd9-bf9a-c3d126b2f401.jpg"/> </div><br>
**5. 设计原则**
@ -325,17 +325,17 @@ StatisticsDisplay.update:1.0 1.0 1.0
下图中 DarkRoast 对象被 Mocha 包裹Mocha 对象又被 Whip 包裹,并且他们都继承自相同父类,都有 cost() 方法,但是外层对象的 cost() 方法实现调用了内层对象的 cost() 方法。因此,如果要在 DarkRoast 上添加 Mocha那么只需要用 Mocha 包裹 DarkRoast如果还需要 Whip ,就用 Whip 包裹 Mocha最后调用 cost() 方法能把三种对象的价格都包含进去。
<br><div align="center"> <img src="../pics//41a4cb30-f393-4b3b-abe4-9941ccf8fa1f.jpg"/> </div><br>
<div align="center"> <img src="../pics//41a4cb30-f393-4b3b-abe4-9941ccf8fa1f.jpg"/> </div><br>
**3. 模式类图**
装饰者和具体组件都继承自组件类型,其中具体组件的方法实现不需要依赖于其它对象,而装饰者拥有一个组件类型对象,这样它可以装饰其它装饰者或者具体组件。所谓装饰,就是把这个装饰者套在被装饰的对象之外,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。可以看到,具体组件应当是装饰层次的最低层,因为只有具体组件有直接实现而不需要委托给其它对象去处理。
<br><div align="center"> <img src="../pics//3dc454fb-efd4-4eb8-afde-785b2182caeb.jpg"/> </div><br>
<div align="center"> <img src="../pics//3dc454fb-efd4-4eb8-afde-785b2182caeb.jpg"/> </div><br>
**4. 问题解决方案的类图**
<br><div align="center"> <img src="../pics//9c997ac5-c8a7-44fe-bf45-2c10eb773e53.jpg"/> </div><br>
<div align="center"> <img src="../pics//9c997ac5-c8a7-44fe-bf45-2c10eb773e53.jpg"/> </div><br>
**5. 设计原则**
@ -343,7 +343,7 @@ StatisticsDisplay.update:1.0 1.0 1.0
**6. Java I/O 中的装饰者模式**
<br><div align="center"> <img src="../pics//2a40042a-03c8-4556-ad1f-72d89f8c555c.jpg"/> </div><br>
<div align="center"> <img src="../pics//2a40042a-03c8-4556-ad1f-72d89f8c555c.jpg"/> </div><br>
**7. 代码实现**
@ -428,11 +428,11 @@ public class StartbuzzCoffee {
简单工厂不是设计模式,更像是一种编程习惯。在实例化一个超类的对象时,可以用它的所有子类来进行实例化,要根据具体需求来决定使用哪个子类。在这种情况下,把实例化的操作放到工厂来中,让工厂类来决定应该用哪个子类来实例化。这样做把客户对象和具体子类的实现解耦,客户对象不再需要知道有哪些子类以及实例化哪个子类。因为客户类往往有多个,如果不使用简单工厂,那么所有的客户类都要知道所有子类的细节,一旦子类发生改变,例如增加子类,那么所有的客户类都要发生改变。
<br><div align="center"> <img src="../pics//c470eb9b-fb05-45c5-8bb7-1057dc3c16de.jpg"/> </div><br>
<div align="center"> <img src="../pics//c470eb9b-fb05-45c5-8bb7-1057dc3c16de.jpg"/> </div><br>
**3. 解决方案类图**
<br><div align="center"> <img src="../pics//dc3e704c-7c57-42b8-93ea-ddd068665964.jpg"/> </div><br>
<div align="center"> <img src="../pics//dc3e704c-7c57-42b8-93ea-ddd068665964.jpg"/> </div><br>
**4. 代码实现**
@ -503,11 +503,11 @@ CheesePizza
可以为每个子类创建单独的简单工厂来创建每一个产品类,但是把简单工厂中创建对象的代码放到子类中来可以减少类的数目,因为子类不算是产品类,因此完全可以这么做。
<br><div align="center"> <img src="../pics//903093ec-acc8-4f9b-bf2c-b990b9a5390c.jpg"/> </div><br>
<div align="center"> <img src="../pics//903093ec-acc8-4f9b-bf2c-b990b9a5390c.jpg"/> </div><br>
**4. 解决方案类图**
<br><div align="center"> <img src="../pics//664f8901-5dc7-4644-a072-dad88cc5133a.jpg"/> </div><br>
<div align="center"> <img src="../pics//664f8901-5dc7-4644-a072-dad88cc5133a.jpg"/> </div><br>
**5. 代码实现**
@ -611,7 +611,7 @@ ChicagoStyleCheesePizza is making..
**依赖倒置原则** :要依赖抽象,不要依赖具体类。听起来像是针对接口编程,不针对实现编程,但是这个原则说明了:不能让高层组件依赖底层组件,而且,不管高层或底层组件,两者都应该依赖于抽象。例如,下图中 PizzaStore 属于高层组件,它依赖的是 Pizza 的抽象类,这样就可以不用关心 Pizza 的具体实现细节。
<br><div align="center"> <img src="../pics//ddf72ca9-c0be-49d7-ab81-57a99a974c8e.jpg"/> </div><br>
<div align="center"> <img src="../pics//ddf72ca9-c0be-49d7-ab81-57a99a974c8e.jpg"/> </div><br>
**2. 模式定义**
@ -621,11 +621,11 @@ ChicagoStyleCheesePizza is making..
抽象工厂模式创建的是对象家族也就是很多对象而不是一个对象并且这些对象是相关的也就是说必须一起创建出来。而工厂模式只是用于创建一个对象这和抽象工厂模式有很大不同。并且抽象工厂模式也用到了工厂模式来创建单一对象在类图左部AbstractFactory 中的 CreateProductA 和 CreateProductB 方法都是让子类来实现,这两个方法单独来看就是在创建一个对象,这符合工厂模式的定义。至于创建对象的家族这一概念是在 Client 体现Client 要通过 AbstractFactory 同时调用两个方法来创建出两个对象在这里这两个对象就有很大的相关性Client 需要这两个对象的协作才能完成任务。从高层次来看,抽象工厂使用了组合,即 Cilent 组合了 AbstractFactory ,而工厂模式使用了继承。
<br><div align="center"> <img src="../pics//d301774f-e0d2-41f3-95f4-bfe39859b52e.jpg"/> </div><br>
<div align="center"> <img src="../pics//d301774f-e0d2-41f3-95f4-bfe39859b52e.jpg"/> </div><br>
**4. 解决方案类图**
<br><div align="center"> <img src="../pics//8785dabd-1285-4bd0-b3aa-b05cc060a24a.jpg"/> </div><br>
<div align="center"> <img src="../pics//8785dabd-1285-4bd0-b3aa-b05cc060a24a.jpg"/> </div><br>
**5. 代码实现**
@ -746,7 +746,7 @@ MarinaraSauce
使用一个私有构造器、一个私有静态变量以及一个公有静态函数来实现。私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。
<br><div align="center"> <img src="../pics//59aff6c1-8bc5-48e4-9e9c-082baeb2f274.jpg"/> </div><br>
<div align="center"> <img src="../pics//59aff6c1-8bc5-48e4-9e9c-082baeb2f274.jpg"/> </div><br>
**3. 懒汉式-线程不安全**
@ -828,9 +828,9 @@ public class Singleton {
有非常多的家电,并且之后会增加家电。
<br><div align="center"> <img src="../pics//7b8f0d8e-a4fa-4c9d-b9a0-3e6a11cb3e33.jpg"/> </div><br>
<div align="center"> <img src="../pics//7b8f0d8e-a4fa-4c9d-b9a0-3e6a11cb3e33.jpg"/> </div><br>
<br><div align="center"> <img src="../pics//c3ca36b2-8459-4cf1-98b0-cc95a0e94f20.jpg"/> </div><br>
<div align="center"> <img src="../pics//c3ca36b2-8459-4cf1-98b0-cc95a0e94f20.jpg"/> </div><br>
**2. 模式定义**
@ -846,11 +846,11 @@ public class Singleton {
- RemoteLoader 是客户端,注意它与 RemoteControl 的区别。因为 RemoteControl 不能主动地调用自身的方法,因此也就不能当成是客户端。客户端好比人,只有人才能去真正去使用遥控器。
<br><div align="center"> <img src="../pics//5ef94f62-98ce-464d-a646-842d9c72c8b8.jpg"/> </div><br>
<div align="center"> <img src="../pics//5ef94f62-98ce-464d-a646-842d9c72c8b8.jpg"/> </div><br>
**4. 模式类图**
<br><div align="center"> <img src="../pics//1e09d75f-6268-4425-acf8-8ecd1b4a0ef3.jpg"/> </div><br>
<div align="center"> <img src="../pics//1e09d75f-6268-4425-acf8-8ecd1b4a0ef3.jpg"/> </div><br>
**5. 代码实现**
@ -939,15 +939,15 @@ Light is on!
将一个类的接口,转换为客户期望的另一个接口。适配器让原本不兼容的类可以合作无间。
<br><div align="center"> <img src="../pics//8e8ba824-7a9e-4934-a212-e6a41dcc1602.jpg"/> </div><br>
<div align="center"> <img src="../pics//8e8ba824-7a9e-4934-a212-e6a41dcc1602.jpg"/> </div><br>
**2. 模式类图**
有两种适配器模式的实现一种是对象方式一种是类方式。对象方式是通过组合的方法让适配器类Adapter拥有一个待适配的对象Adaptee从而把相应的处理委托给待适配的对象。类方式用到多重继承Adapter 继承 Target 和 Adaptee先把 Adapter 当成 Adaptee 类型然后实例化一个对象,再把它当成 Target 类型的,这样 Client 就可以把这个对象当成 Target 的对象来处理,同时拥有 Adaptee 的方法。
<br><div align="center"> <img src="../pics//253bd869-ea48-4092-9aed-6906ccb2f3b0.jpg"/> </div><br>
<div align="center"> <img src="../pics//253bd869-ea48-4092-9aed-6906ccb2f3b0.jpg"/> </div><br>
<br><div align="center"> <img src="../pics//a797959a-0ed5-475b-8d97-df157c672019.jpg"/> </div><br>
<div align="center"> <img src="../pics//a797959a-0ed5-475b-8d97-df157c672019.jpg"/> </div><br>
**3. 问题描述**
@ -957,7 +957,7 @@ Light is on!
**4. 解决方案类图**
<br><div align="center"> <img src="../pics//1a511c76-bb6b-40ab-b8aa-39eeb619d673.jpg"/> </div><br>
<div align="center"> <img src="../pics//1a511c76-bb6b-40ab-b8aa-39eeb619d673.jpg"/> </div><br>
**5. 代码实现**
@ -1021,17 +1021,17 @@ gobble!
**2. 模式类图**
<br><div align="center"> <img src="../pics//78f2314e-2643-41df-8f3d-b7e28294094b.jpg"/> </div><br>
<div align="center"> <img src="../pics//78f2314e-2643-41df-8f3d-b7e28294094b.jpg"/> </div><br>
**3. 问题描述**
家庭影院中有众多电器,当要进行观看电影时需要对很多电器进行操作。要求简化这些操作,使得家庭影院类只提供一个简化的接口,例如提供一个看电影相关的接口。
<br><div align="center"> <img src="../pics//106f5585-b2e7-4718-be5d-3b322d1ef42a.jpg"/> </div><br>
<div align="center"> <img src="../pics//106f5585-b2e7-4718-be5d-3b322d1ef42a.jpg"/> </div><br>
**4. 解决方案类图**
<br><div align="center"> <img src="../pics//25387681-89f8-4365-a2fa-83b86449ee84.jpg"/> </div><br>
<div align="center"> <img src="../pics//25387681-89f8-4365-a2fa-83b86449ee84.jpg"/> </div><br>
**5. 设计原则**
@ -1053,19 +1053,19 @@ gobble!
模板方法 templateMethod() 定义了算法的骨架,确定了 primitiveOperation1() 和 primitiveOperation2() 方法执行的顺序,而 primitiveOperation1() 和 primitiveOperation2() 让子类去实现。
<br><div align="center"> <img src="../pics//ed62f400-192c-4185-899b-187958201f0c.jpg"/> </div><br>
<div align="center"> <img src="../pics//ed62f400-192c-4185-899b-187958201f0c.jpg"/> </div><br>
**3. 问题描述**
冲咖啡和冲茶都有类似的流程,但是某些步骤会有点不一样,要求复用那些相同步骤的代码。
<br><div align="center"> <img src="../pics//d8f873fc-00bc-41ee-a87c-c1b4c0172844.png"/> </div><br>
<div align="center"> <img src="../pics//d8f873fc-00bc-41ee-a87c-c1b4c0172844.png"/> </div><br>
**4. 解决方案类图**
其中 prepareRecipe() 方法就是模板方法,它确定了其它四个方法的具体执行步骤。其中 brew() 和 addCondiments() 方法在子类中实现。
<br><div align="center"> <img src="../pics//aa20c123-b6b5-432a-83d3-45dc39172192.jpg"/> </div><br>
<div align="center"> <img src="../pics//aa20c123-b6b5-432a-83d3-45dc39172192.jpg"/> </div><br>
**5. 设计原则**
@ -1171,7 +1171,7 @@ Tea.addCondiments
- Client 需要拥有一个 Aggregate 对象,这是很明显的。为了迭代变量 Aggregate 对象,也需要拥有 Iterator 对象。
<br><div align="center"> <img src="../pics//439deca7-fed0-4c89-87e5-7088d10f1fdb.jpg"/> </div><br>
<div align="center"> <img src="../pics//439deca7-fed0-4c89-87e5-7088d10f1fdb.jpg"/> </div><br>
**3. 代码实现**
@ -1331,7 +1331,7 @@ public class Client {
组合类拥有一个组件对象,因此组合类的操作可以委托给组件对象去处理,而组件对象可以是另一个组合类或者叶子类。
<br><div align="center"> <img src="../pics//f99c019e-7e91-4c2e-b94d-b031c402dcb5.jpg"/> </div><br>
<div align="center"> <img src="../pics//f99c019e-7e91-4c2e-b94d-b031c402dcb5.jpg"/> </div><br>
**4. 代码实现**
@ -1444,7 +1444,7 @@ Composite:root
Context 的 request() 方法委托给 State 对象去处理。当 Context 组合的 State 对象发生改变时,它的行为也就发生了改变。
<br><div align="center"> <img src="../pics//c28fd93a-0d55-4a19-810f-72652feee00d.jpg"/> </div><br>
<div align="center"> <img src="../pics//c28fd93a-0d55-4a19-810f-72652feee00d.jpg"/> </div><br>
**3. 与策略模式的比较**
@ -1460,7 +1460,7 @@ Context 的 request() 方法委托给 State 对象去处理。当 Context 组合
糖果销售机有多种状态,每种状态下销售机有不同的行为,状态可以发生转移,使得销售机的行为也发生改变。
<br><div align="center"> <img src="../pics//f7d880c9-740a-4a16-ac6d-be502281b4b2.jpg"/> </div><br>
<div align="center"> <img src="../pics//f7d880c9-740a-4a16-ac6d-be502281b4b2.jpg"/> </div><br>
**5. 直接解决方案**
@ -1468,7 +1468,7 @@ Context 的 request() 方法委托给 State 对象去处理。当 Context 组合
这种解决方案在需要增加状态的时候,必须对每个操作的代码都进行修改。
<br><div align="center"> <img src="../pics//62ebbb63-8fd7-4488-a866-76a9dc911662.png"/> </div><br>
<div align="center"> <img src="../pics//62ebbb63-8fd7-4488-a866-76a9dc911662.png"/> </div><br>
**6 代码实现**
@ -1761,13 +1761,13 @@ No gumball dispensed
视图使用组合模式,模型使用了观察者模式,控制器使用了策略模式。
<br><div align="center"> <img src="../pics//4f67611d-492f-4958-9fa0-4948010e345f.jpg"/> </div><br>
<div align="center"> <img src="../pics//4f67611d-492f-4958-9fa0-4948010e345f.jpg"/> </div><br>
**Web 中的 MVC**
模式不再使用观察者模式。
<br><div align="center"> <img src="../pics//1dd56e61-2970-4d27-97c2-6e81cee86978.jpg"/> </div><br>
<div align="center"> <img src="../pics//1dd56e61-2970-4d27-97c2-6e81cee86978.jpg"/> </div><br>
# 第十三章 与设计模式相处
@ -1779,6 +1779,6 @@ No gumball dispensed
模式分类:
<br><div align="center"> <img src="../pics//524a237c-ffd7-426f-99c2-929a6bf4c847.jpg"/> </div><br>
<div align="center"> <img src="../pics//524a237c-ffd7-426f-99c2-929a6bf4c847.jpg"/> </div><br>
# 第十四章 剩下的模式

View File

@ -124,7 +124,7 @@
包括三个类Movie、Rental 和 CustomerRental 包含租赁的 Movie 以及天数。
<br><div align="center"> <img src="../pics//25d6d3d4-4726-47b1-a9cb-3316d1ff5dd5.png"/> </div><br>
<div align="center"> <img src="../pics//25d6d3d4-4726-47b1-a9cb-3316d1ff5dd5.png"/> </div><br>
最开始的实现是把所有的计费代码都放在 Customer 类中。
@ -159,19 +159,19 @@ double getTotalCharge() {
以下是继承 Movie 的多态解决方案,这种方案可以解决上述的 switch 问题,因为每种电影类别的计费方式都被放到了对应 Movie 子类中,当变化发生时,只需要去修改对应子类中的代码即可。
<br><div align="center"> <img src="../pics//76b48b4c-8999-4967-893b-832602e73285.png"/> </div><br>
<div align="center"> <img src="../pics//76b48b4c-8999-4967-893b-832602e73285.png"/> </div><br>
但是由于 Movie 可以在其生命周期内修改自己的类别,一个对象却不能在生命周期内修改自己所属的类,因此这种方案不可行。可以使用策略模式来解决这个问题(原书写的是使用状态模式,但是这里应该为策略模式,具体可以参考设计模式内容)。
下图中Price 有多种实现Movie 组合了一个 Price 对象,并且在运行时可以改变组合的 Price 对象,从而使得它的计费方式发生改变。
<br><div align="center"> <img src="../pics//2a842a14-e4ab-4f37-83fa-f82c206fe426.png"/> </div><br>
<div align="center"> <img src="../pics//2a842a14-e4ab-4f37-83fa-f82c206fe426.png"/> </div><br>
重构后整体的类图和时序图如下:
<br><div align="center"> <img src="../pics//9d549816-60b7-4899-9877-23b01503ab13.png"/> </div><br>
<div align="center"> <img src="../pics//9d549816-60b7-4899-9877-23b01503ab13.png"/> </div><br>
<br><div align="center"> <img src="../pics//2c8a7a87-1bf1-4d66-9ba9-225a1add0a51.png"/> </div><br>
<div align="center"> <img src="../pics//2c8a7a87-1bf1-4d66-9ba9-225a1add0a51.png"/> </div><br>
# 重构原则
@ -579,7 +579,7 @@ Hide Delegate 有很大好处,但是它的代价是:每当客户要使用受
将该数据赋值到一个领域对象中,建立一个 Oberver 模式,用以同步领域对象和 GUI 对象内的重复数据。
<br><div align="center"> <img src="../pics//e024bd7e-fb4e-4239-9451-9a6227f50b00.jpg"/> </div><br>
<div align="center"> <img src="../pics//e024bd7e-fb4e-4239-9451-9a6227f50b00.jpg"/> </div><br>
## 7. Change Unidirectional Association to Bidirectional将单向关联改为双向关联
@ -636,13 +636,13 @@ public 字段应当改为 private并提供相应的访问函数。
类中有一个数值类型码,但它并不影响类的行为,就用一个新类替换该数值类型码。如果类型码出现在 switch 语句中,需要使用 Replace Conditional with Polymorphism 去掉 switch首先必须运用 Replace Type Code with Subcalss 或 Replace Type Code with State/Strategy 去掉类型码。
<br><div align="center"> <img src="../pics//27c2e0b3-8f95-453d-bedc-6398a8566ce9.jpg"/> </div><br>
<div align="center"> <img src="../pics//27c2e0b3-8f95-453d-bedc-6398a8566ce9.jpg"/> </div><br>
## 14. Replace Type Code with Subcalsses以子类取代类型码
有一个不可变的类型码,它会影响类的行为,以子类取代这个类型码。
<br><div align="center"> <img src="../pics//c41d3977-e0e7-4ee4-93e1-d84f1ae3e20e.jpg"/> </div><br>
<div align="center"> <img src="../pics//c41d3977-e0e7-4ee4-93e1-d84f1ae3e20e.jpg"/> </div><br>
## 15. Replace Type Code with State/Strategy (以 State/Strategy 取代类型码)
@ -650,13 +650,13 @@ public 字段应当改为 private并提供相应的访问函数。
和 Replace Type Code with Subcalsses 的区别是 Replace Type Code with State/Strategy 的类型码是动态可变的,前者通过继承的方式来实现,后者通过组合的方式来实现。因为类型码可变,如果通过继承的方式,一旦一个对象的类型码改变,那么就要改变用新的对象来取代旧对象,而客户端难以改变新的对象。但是通过组合的方式,改变引用的状态类是很容易的。
<br><div align="center"> <img src="../pics//81fd1d6f-a3b2-4160-9a0a-1f7cb50ba440.jpg"/> </div><br>
<div align="center"> <img src="../pics//81fd1d6f-a3b2-4160-9a0a-1f7cb50ba440.jpg"/> </div><br>
## 16. Replace Subclass with Fields以字段取代子类
各个子类的唯一差别只在“返回常量数据”的函数上。
<br><div align="center"> <img src="../pics//f2e0cee9-ecdc-4a96-853f-d9f6a1ad6ad1.jpg"/> </div><br>
<div align="center"> <img src="../pics//f2e0cee9-ecdc-4a96-853f-d9f6a1ad6ad1.jpg"/> </div><br>
# 简化条件表达式
@ -776,7 +776,7 @@ double getSpeed() {
}
```
<br><div align="center"> <img src="../pics//1c8432c8-2552-457f-b117-1da36c697221.jpg"/> </div><br>
<div align="center"> <img src="../pics//1c8432c8-2552-457f-b117-1da36c697221.jpg"/> </div><br>
## 7. Introduce Null Object引入Null对象
@ -916,7 +916,7 @@ double finalPrice = discountedPrice (basePrice);
以一个对象取代这些参数。
<br><div align="center"> <img src="../pics//08738dd0-ae8e-404a-ba78-a6b1b7d225b3.jpg"/> </div><br>
<div align="center"> <img src="../pics//08738dd0-ae8e-404a-ba78-a6b1b7d225b3.jpg"/> </div><br>
## 10. Remove Setting Method移除设值函数

View File

@ -197,13 +197,13 @@ public class Music {
从具体类中继承
<br><div align="center"> <img src="../pics//29badd92-109f-4f29-abb9-9857f5973928.png"/> </div><br>
<div align="center"> <img src="../pics//29badd92-109f-4f29-abb9-9857f5973928.png"/> </div><br>
② 实现关系 (realize)
从抽象类或者接口中继承
<br><div align="center"> <img src="../pics//4b16e1d3-3a60-472c-9756-2f31b1c48abe.png"/> </div><br>
<div align="center"> <img src="../pics//4b16e1d3-3a60-472c-9756-2f31b1c48abe.png"/> </div><br>
**1.2 整体和部分**
@ -211,13 +211,13 @@ public class Music {
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。以下表示 B 由 A 组成:
<br><div align="center"> <img src="../pics//34259bb8-ca3a-4872-8771-9e946782d9c3.png"/> </div><br>
<div align="center"> <img src="../pics//34259bb8-ca3a-4872-8771-9e946782d9c3.png"/> </div><br>
② 组合关系 (composition)
和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
<br><div align="center"> <img src="../pics//7dda050d-ac35-4f47-9f51-18f18ed6fa9a.png"/> </div><br>
<div align="center"> <img src="../pics//7dda050d-ac35-4f47-9f51-18f18ed6fa9a.png"/> </div><br>
**1.3 相互联系**
@ -225,13 +225,13 @@ public class Music {
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系一个学校可以有很多学生但是一个学生只属于一个学校因此这是一种多对一的关系在运行开始之前就可以确定。
<br><div align="center"> <img src="../pics//4ccd294c-d6b2-421b-839e-d88336ff5fb7.png"/> </div><br>
<div align="center"> <img src="../pics//4ccd294c-d6b2-421b-839e-d88336ff5fb7.png"/> </div><br>
② 依赖关系 (dependency)
和关联关系不同的是 , 依赖关系是在运行过程中起作用的。一般依赖作为类的构造器或者方法的参数传入。双向依赖时一种不好的设计。
<br><div align="center"> <img src="../pics//47ca2614-509f-476e-98fc-50ec9f9d43c0.png"/> </div><br>
<div align="center"> <img src="../pics//47ca2614-509f-476e-98fc-50ec9f9d43c0.png"/> </div><br>
## 2. 时序图
@ -245,7 +245,7 @@ http://www.cnblogs.com/wolf-sun/p/UML-Sequence-diagram.html
从虚线从上往下表示时间的推进。
<br><div align="center"> <img src="../pics//80c5aff8-fc46-4810-aeaa-215b5c60a003.png"/> </div><br>
<div align="center"> <img src="../pics//80c5aff8-fc46-4810-aeaa-215b5c60a003.png"/> </div><br>
可见,通过时序图可以知道每个类具有以下操作:
@ -289,7 +289,7 @@ public class 孙权 {
有三种表现形式
<br><div align="center"> <img src="../pics//25b8adad-2ef6-4f30-9012-c306b4e49897.png"/> </div><br>
<div align="center"> <img src="../pics//25b8adad-2ef6-4f30-9012-c306b4e49897.png"/> </div><br>
在画图时,应该遵循以下原则:
@ -301,7 +301,7 @@ public class 孙权 {
生命线从对象的创建开始到对象销毁时终止
<br><div align="center"> <img src="../pics//b7b0eac6-e7ea-4fb6-8bfb-95fec6f235e2.png"/> </div><br>
<div align="center"> <img src="../pics//b7b0eac6-e7ea-4fb6-8bfb-95fec6f235e2.png"/> </div><br>
③ 消息
@ -311,15 +311,15 @@ public class 孙权 {
1\. 简单消息,不区分同步异步。
<br><div align="center"> <img src="../pics//a13b62da-0fa8-4224-a615-4cadacc08871.png"/> </div><br>
<div align="center"> <img src="../pics//a13b62da-0fa8-4224-a615-4cadacc08871.png"/> </div><br>
2\. 同步消息,发送消息之后需要暂停活动来等待回应。
<br><div align="center"> <img src="../pics//33821037-dc40-4266-901c-e5b38e618426.png"/> </div><br>
<div align="center"> <img src="../pics//33821037-dc40-4266-901c-e5b38e618426.png"/> </div><br>
3\. 异步消息,发送消息之后不需要等待。
<br><div align="center"> <img src="../pics//dec6c6cc-1b5f-44ed-b8fd-464fcf849dac.png"/> </div><br>
<div align="center"> <img src="../pics//dec6c6cc-1b5f-44ed-b8fd-464fcf849dac.png"/> </div><br>
4\. 返回消息,可选。
@ -327,7 +327,7 @@ public class 孙权 {
生命线上的方框表示激活状态,其它时间处于休眠状态。
<br><div align="center"> <img src="../pics//6ab5de9b-1c1e-4118-b2c3-fb6c7ed7de6f.png"/> </div><br>
<div align="center"> <img src="../pics//6ab5de9b-1c1e-4118-b2c3-fb6c7ed7de6f.png"/> </div><br>
# 参考资料

3
other/alipay.md Normal file
View File

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

BIN
other/alipay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

29
other/download.md Normal file
View File

@ -0,0 +1,29 @@
# 网络
<a href="https://pan.baidu.com/s/1EXaJbNckzuQMOCyamzjL_Q"> <img src="s3296854.jpg" width="150"/> </a> <a href="https://pan.baidu.com/s/1M0AHXqG9sP9Bxne6u0JK8A"> <img src="s27283822.jpg" width="150"/> </a>
# 操作系统
<a href="https://pan.baidu.com/s/1C-MgvslLKd1buwmebti6Qg"> <img src="s1650904.jpg" width="150"/> </a> <a href="https://pan.baidu.com/s/1OoyVI90fK1Q9eixzH9jnpQ"> <img src="s4510534.jpg" width="150"/> </a> <a href="https://pan.baidu.com/s/12mTkrpLsb7tz11cGn_KZ4w"> <img src="s3895413.jpg" width="150"/> </a> <a href="https://pan.baidu.com/s/1Qm2G4rghPorQeH5J9fDHTg"> <img src="s4399937.jpg" width="150"/> </a>
# 算法
<a href="https://pan.baidu.com/s/1Va1R66d13ynmita8nfkRPg"> <img src="s8938479.jpg" width="150"/> </a> <a href="https://pan.baidu.com/s/1HmGwXvTcHDrQnUAL1wWE3g"> <img src="s7038106.jpg" width="150"/> </a> <a href="https://pan.baidu.com/s/1SZGUbvKpKOomM-iYxe_GGw"> <img src="s2992671.jpg" width="150"/> </a>
# 设计模式
<a href="https://pan.baidu.com/s/1JOO4M3c6EGB5xHz_-aGtDQ"> <img src="s2686916.jpg" width="150"/> </a>
# 数据库
<a href="https://pan.baidu.com/s/1xhYsZUi2fugLf9jxSWA0pQ"> <img src="s2359163.jpg" width="150"/> </a> <a href="https://pan.baidu.com/s/1aXRWznphuiEc4XRXpM1qLA"> <img src="s4141593.jpg" width="150"/> </a>
# Java
<a href="https://pan.baidu.com/s/1iNBkY9ANUcmeSp4VjBGhRQ"> <img src="s27243455.jpg" width="150"/> </a> <a href="https://pan.baidu.com/s/1zdATX8Qs-RMk6DN7iqECYw"> <img src="s27458236.jpg" width="150"/> </a>
# 打包下载
[百度网盘](https://pan.baidu.com/s/1o9oD1s2#list/path=%2F&parentPath=%2F)

View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

BIN
other/s1650904.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
other/s2359163.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
other/s2686916.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
other/s27243455.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
other/s27283822.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
other/s27458236.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
other/s29195878.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
other/s2992671.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
other/s3296854.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
other/s3895413.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
other/s4141593.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
other/s4399937.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
other/s4510534.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
other/s7038106.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
other/s8938479.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB