diff --git a/README.md b/README.md
index 580b86a62bd21e8027b46f93eb5796c28044a591..722c681ecb08e86864b89891601e19394bd0d714 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,10 @@
整理自《图解 HTTP》
+> [Socket](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Socket.md)
+
+整理自《Unix 网络编程》
+
## 面向对象 :couple:
@@ -63,6 +67,10 @@
整理自《SQL 必知必会》
+> [Leetcode-Database 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode-Database%20题解.md)
+
+Leetcode 上数据库题目的解题记录。
+
> [MySQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/MySQL.md)
整理自《高性能 MySQL》
@@ -73,13 +81,17 @@
## Java :coffee:
+> [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md)
+
+整理了一些常见考点。
+
> [Java 虚拟机](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20虚拟机.md)
整理自《深入理解 Java 虚拟机》
> [Java 并发](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20并发.md)
-只整理了一些比较基础的概念,之后会继续添加更多内容。
+整理了一些并发的基本概念。
> [Java 容器](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20容器.md)
@@ -89,10 +101,6 @@
File, InputStream OutputStream, Reader Writer, Serializable, Socket, NIO
-> [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md)
-
-整理了一些常见考点。
-
> [JDK 中的设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/JDK%20中的设计模式.md)
对每种设计模式做了一个总结,并给出在 JDK 中的使用实例。
@@ -103,10 +111,6 @@ File, InputStream OutputStream, Reader Writer, Serializable, Socket, NIO
整理自《大规模分布式存储系统》
-> [一致性协议](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/一致性协议.md)
-
-两阶段提交、Paxos、Raft、拜占庭将军问题。
-
> [分布式问题分析](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式问题分析.md)
分布式事务、负载均衡算法与实现、分布式锁、分布式 Session、分库分表的分布式困境与应对之策。
diff --git a/notes/Git.md b/notes/Git.md
index 1695faadade0acd17b0ef52a411ead3e913d8e90..4bb0a54352b6d64397da88779fd31d29c57ba7da 100644
--- a/notes/Git.md
+++ b/notes/Git.md
@@ -129,7 +129,7 @@ HEAD is now at 049d078 added the index file (To restore them type "git stash app
# SSH 传输设置
-Git 仓库和 Github 中心仓库之间是通过 SSH 加密。
+Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密。
如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key:
@@ -143,9 +143,9 @@ $ ssh-keygen -t rsa -C "youremail@example.com"
忽略以下文件:
-1. 操作系统自动生成的文件,比如缩略图;
-2. 编译生成的中间文件,比如 Java 编译产生的 .class 文件;
-3. 自己的敏感信息,比如存放口令的配置文件。
+- 操作系统自动生成的文件,比如缩略图;
+- 编译生成的中间文件,比如 Java 编译产生的 .class 文件;
+- 自己的敏感信息,比如存放口令的配置文件。
不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。
diff --git a/notes/HTTP.md b/notes/HTTP.md
index 1c70e89da8ed3df90116a11e65e4fa3cb0ab54ce..11045573a9a8c2504fb1050bc50b5fc49b014b6b 100644
--- a/notes/HTTP.md
+++ b/notes/HTTP.md
@@ -27,34 +27,38 @@
* [五、具体应用](#五具体应用)
* [Cookie](#cookie)
* [缓存](#缓存)
- * [持久连接](#持久连接)
- * [管线化处理](#管线化处理)
- * [编码](#编码)
+ * [连接管理](#连接管理)
+ * [内容协商](#内容协商)
+ * [内容编码](#内容编码)
+ * [范围请求](#范围请求)
* [分块传输编码](#分块传输编码)
* [多部分对象集合](#多部分对象集合)
- * [范围请求](#范围请求)
- * [内容协商](#内容协商)
* [虚拟主机](#虚拟主机)
* [通信数据转发](#通信数据转发)
* [六、HTTPs](#六https)
* [加密](#加密)
* [认证](#认证)
- * [完整性](#完整性)
+ * [完整性保护](#完整性保护)
+ * [HTTPs 的缺点](#https-的缺点)
+ * [配置 HTTPs](#配置-https)
* [七、Web 攻击技术](#七web-攻击技术)
- * [攻击模式](#攻击模式)
* [跨站脚本攻击](#跨站脚本攻击)
- * [跨站点请求伪造](#跨站点请求伪造)
+ * [跨站请求伪造](#跨站请求伪造)
* [SQL 注入攻击](#sql-注入攻击)
* [拒绝服务攻击](#拒绝服务攻击)
* [八、GET 和 POST 的区别](#八get-和-post-的区别)
+ * [作用](#作用)
* [参数](#参数)
* [安全](#安全)
* [幂等性](#幂等性)
* [可缓存](#可缓存)
* [XMLHttpRequest](#xmlhttprequest)
-* [九、各版本比较](#九各版本比较)
- * [HTTP/1.0 与 HTTP/1.1 的区别](#http10-与-http11-的区别)
- * [HTTP/1.1 与 HTTP/2.0 的区别](#http11-与-http20-的区别)
+* [九、HTTP/1.0 与 HTTP/1.1 的区别](#九http10-与-http11-的区别)
+* [十、HTTP/2.0](#十http20)
+ * [HTTP/1.x 缺陷](#http1x-缺陷)
+ * [二进制分帧层](#二进制分帧层)
+ * [服务端推送](#服务端推送)
+ * [首部压缩](#首部压缩)
* [参考资料](#参考资料)
@@ -63,8 +67,9 @@
## Web 基础
-- HTTP(HyperText Transfer Protocol,超文本传输协议)
- WWW(World Wide Web)的三种技术:HTML、HTTP、URL
+- HTML(HyperText Markup Language,超文本标记语言)
+- HTTP(HyperText Transfer Protocol,超文本传输协议)
- RFC(Request for Comments,征求修正意见书),互联网的设计文档。
## URL
@@ -218,7 +223,7 @@ CONNECT www.example.com:443 HTTP/1.1
- 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。
-- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-ModifiedSince,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。
+- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。
- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。
@@ -317,11 +322,19 @@ CONNECT www.example.com:443 HTTP/1.1
HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。
-Cookie 是服务器发送给客户端的数据,该数据会被保存在浏览器中,并且客户端的下一次请求报文会包含该数据。通过 Cookie 可以让服务器知道两个请求是否来自于同一个客户端,从而实现保持登录状态等功能。
+Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。它用于告知服务端两个请求是否来自同一浏览器,并保持用户的登录状态。
-### 1. 创建过程
+### 1. 用途
-服务器发送的响应报文包含 Set-Cookie 字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。
+- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
+- 个性化设置(如用户自定义设置、主题等)
+- 浏览器行为跟踪(如跟踪分析用户行为等)
+
+Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。由于服务器指定 Cookie 后,浏览器的每次请求都会携带 Cookie 数据,会带来额外的性能开销(尤其是在移动环境下)。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API (本地存储和会话存储)或 IndexedDB。
+
+### 2. 创建过程
+
+服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。
```html
HTTP/1.0 200 OK
@@ -332,7 +345,7 @@ Set-Cookie: tasty_cookie=strawberry
[page content]
```
-客户端之后发送请求时,会从浏览器中读出 Cookie 值,在请求报文中包含 Cookie 字段。
+客户端之后对同一个服务器发送请求时,会从浏览器中读出 Cookie 信息通过 Cookie 请求首部字段发送给服务器。
```html
GET /sample_page.html HTTP/1.1
@@ -340,7 +353,7 @@ Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
```
-### 2. 分类
+### 3. 分类
- 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。
- 持久性 Cookie:指定一个特定的过期时间(Expires)或有效期(Max-Age)之后就成为了持久性的 Cookie。
@@ -349,117 +362,207 @@ Cookie: yummy_cookie=choco; tasty_cookie=strawberry
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
```
-### 3. Set-Cookie
+### 4. JavaScript 获取 Cookie
+
+通过 `Document.cookie` 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。
+
+```html
+document.cookie = "yummy_cookie=choco";
+document.cookie = "tasty_cookie=strawberry";
+console.log(document.cookie);
+```
+
+### 5. Secure 和 HttpOnly
+
+标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。
+
+标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。因为跨域脚本 (XSS) 攻击常常使用 JavaScript 的 `Document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。
+
+```html
+Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
+```
+
+### 6. 作用域
+
+Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。
-| 属性 | 说明 |
-| :--: | -- |
-| NAME=VALUE | 赋予 Cookie 的名称和其值(必需项) |
-| expires=DATE | Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止) |
-| path=PATH | 将服务器上的文件目录作为 Cookie 的适用对象(若不指定则默认为文档所在的文件目录) |
-| domain=域名 | 作为 Cookie 适用对象的域名(若不指定则默认为创建 Cookie 的服务器的域名) |
-| Secure | 仅在 HTTPs 安全通信时才会发送 Cookie |
-| HttpOnly | 加以限制,使 Cookie 不能被 JavaScript 脚本访问 |
+Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配:
-### 4. Session 和 Cookie 区别
+- /docs
+- /docs/Web/
+- /docs/Web/HTTP
-Session 是服务器用来跟踪用户的一种手段,每个 Session 都有一个唯一标识:Session ID。当服务器创建了一个 Session 时,给客户端发送的响应报文包含了 Set-Cookie 字段,其中有一个名为 sid 的键值对,这个键值对就是 Session ID。客户端收到后就把 Cookie 保存在浏览器中,并且之后发送的请求报文都包含 Session ID。HTTP 就是通过 Session 和 Cookie 这两种方式一起合作来实现跟踪用户状态的,Session 用于服务器端,Cookie 用于客户端。
+### 7. Session
-### 5. 浏览器禁用 Cookie 的情况
+除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。
-会使用 URL 重写技术,在 URL 后面加上 sid=xxx 。
+Session 可以存储在服务器上的文件、数据库或者内存中,现在最常见的是将 Session 存储在内存型数据库中,比如 Redis。
-### 6. 使用 Cookie 实现用户名和密码的自动填写
+使用 Session 维护用户登录的过程如下:
-网站脚本会自动从保存在浏览器中的 Cookie 读取用户名和密码,从而实现自动填写。
+- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中;
+- 服务器验证该用户名和密码;
+- 如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 ID 称为 Session ID;
+- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中;
+- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之后的业务操作。
-但是如果 Set-Cookie 指定了 HttpOnly 属性,就无法通过 Javascript 脚本获取 Cookie 信息,这是出于安全性考虑。
+应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。
+
+### 8. 浏览器禁用 Cookie
+
+此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。
+
+### 9. Cookie 与 Session 选择
+
+- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存取任何类型的数据,因此在考虑数据复杂性时 首选 Session;
+- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密;
+- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。
## 缓存
### 1. 优点
-1. 降低服务器的负担;
-2. 提高响应速度(缓存资源比服务器上的资源离客户端更近)。
+- 缓解服务器压力;
+- 减低客户端获取资源的延迟(缓存资源比服务器上的资源离客户端更近)。
### 2. 实现方法
-1. 让代理服务器进行缓存;
-2. 让客户端浏览器进行缓存。
+- 让代理服务器进行缓存;
+- 让客户端浏览器进行缓存。
+
+### 3. Cache-Control
+
+HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。
+
+**(一)禁止进行缓存**
+
+no-store 指令规定不能对请求或响应的任何一部分进行缓存。
+
+```html
+Cache-Control: no-store
+```
-### 3. Cache-Control 字段
+**(二)强制确认缓存**
-HTTP 通过 Cache-Control 首部字段来控制缓存。
+no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。
```html
-Cache-Control: private, max-age=0, no-cache
+Cache-Control: no-cache
```
-### 4. no-cache 指令
+**(三)私有缓存和公共缓存**
-该指令出现在请求报文的 Cache-Control 字段中,表示缓存服务器需要先向原服务器验证缓存资源是否过期;
+private 指令规定了将资源作为私有缓存,只能被单独用户所使用,一般存储在用户浏览器中。
-该指令出现在响应报文的 Cache-Control 字段中,表示缓存服务器在进行缓存之前需要先验证缓存资源的有效性。
+```html
+Cache-Control: private
+```
-### 5. no-store 指令
+public 指令规定了将资源作为公共缓存,可以被多个用户所使用,一般存储在代理服务器中。
-该指令表示缓存服务器不能对请求或响应的任何一部分进行缓存。
+```html
+Cache-Control: public
+```
-no-cache 不表示不缓存,而是缓存之前需要先进行验证,no-store 才是不进行缓存。
+**(四)缓存过期机制**
-### 6. max-age 指令
+max-age 指令出现在请求报文中,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。
-该指令出现在请求报文的 Cache-Control 字段中,如果缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。
+max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中保存的时间。
-该指令出现在响应报文的 Cache-Control 字段中,表示缓存资源在缓存服务器中保存的时间。
+```html
+Cache-Control: max-age=31536000
+```
Expires 字段也可以用于告知缓存服务器该资源什么时候会过期。在 HTTP/1.1 中,会优先处理 Cache-Control : max-age 指令;而在 HTTP/1.0 中,Cache-Control : max-age 指令会被忽略掉。
-## 持久连接
+```html
+Expires: Wed, 04 Jul 2012 08:26:05 GMT
+```
+
+### 4. 缓存验证
-当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。持久连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。
+需要先了解 ETag 首部字段的含义,它是资源的唯一表示。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一表示。
-
+```html
+ETag: "82e22293907ce725faf67773957acd12"
+```
-持久连接需要使用 Connection 首部字段进行管理。HTTP/1.1 开始 HTTP 默认是持久化连接的,如果要断开 TCP 连接,需要由客户端或者服务器端提出断开,使用 Connection : close;而在 HTTP/1.1 之前默认是非持久化连接的,如果要维持持续连接,需要使用 Connection : Keep-Alive。
+可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。
-## 管线化处理
+```html
+If-None-Match: "82e22293907ce725faf67773957acd12"
+```
-HTTP/1.1 支持管线化处理,可以同时发送多个请求和响应,而不需要发送一个请求然后等待响应之后再发下一个请求。
+Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 Not Modified 响应,
-## 编码
+```html
+Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
+```
-编码(Encoding)主要是为了对实体进行压缩。常用的编码有:gzip、compress、deflate、identity,其中 identity 表示不执行压缩的编码格式。
+```html
+If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
+```
-## 分块传输编码
+## 连接管理
-Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。
+
-## 多部分对象集合
+### 1. 短连接与长连接
-一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。
+当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。
-例如,上传多个表单时可以使用如下方式:
+HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 Connection : close;而在 HTTP/1.1 之前默认是短连接的,如果需要长连接,则使用 Connection : Keep-Alive。
-```html
-Content-Type: multipart/form-data; boundary=AaB03x
+### 2. 流水线
---AaB03x
-Content-Disposition: form-data; name="submit-name"
+默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到应答过后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。
-Larry
---AaB03x
-Content-Disposition: form-data; name="files"; filename="file1.txt"
-Content-Type: text/plain
+流水线是在同一条长连接上发出连续的请求,而不用等待响应返回,这样可以避免连接延迟。
-... contents of file1.txt ...
---AaB03x--
+## 内容协商
+
+通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。
+
+### 1. 类型
+
+**(一)服务端驱动型内容协商**
+
+客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Languag,服务器根据这些字段返回特定的资源。
+
+它存在以下问题:
+
+- 服务器很难知道客户端浏览器的全部信息;
+- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术)。
+- 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。
+
+**(二)代理驱动型协商**
+
+服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。
+
+### 2. Vary
+
+```html
+Vary: Accept-Language
```
+在使用内容协商的情况下,只有当缓存服务器中的缓存满足内容协商条件时,才能使用该缓存,否则应该向源服务器请求该资源。
+
+例如,一个客户端发送了一个包含 Accept-Language 首部字段的请求之后,源服务器返回的响应包含 `Vary: Accept-Language` 内容,缓存服务器对这个响应进行缓存之后,在客户端下一次访问同一个 URL 资源,并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。
+
+## 内容编码
+
+内容编码将实体主体进行压缩,从而减少传输的数据量。常用的内容编码有:gzip、compress、deflate、identity。
+
+浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级,服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应中,Vary 首部中至少要包含 Content-Encoding,这样的话,缓存服务器就可以对资源的不同展现形式进行缓存。
+
## 范围请求
-如果网络出现中断,服务器只发送了一部分数据,范围请求使得客户端能够只请求未发送的那部分数据,从而避免服务器端重新发送所有数据。
+如果网络出现中断,服务器只发送了一部分数据,范围请求可以使得客户端只请求服务器未发送的那部分数据,从而避免服务器重新发送所有数据。
-在请求报文首部中添加 Range 字段指定请求的范围,请求成功的话服务器发送 206 Partial Content 状态。
+### 1. Range
+
+在请求报文中添加 Range 首部字段指定请求的范围。
```html
GET /z4d4kWk.jpg HTTP/1.1
@@ -467,6 +570,8 @@ Host: i.imgur.com
Range: bytes=0-1023
```
+请求成功的话服务器返回的响应包含 206 Partial Content 状态码。
+
```html
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/146515
@@ -475,25 +580,60 @@ Content-Length: 1024
(binary content)
```
-## 内容协商
+### 2. Accept-Ranges
-通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。
+响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求,可以处理使用 bytes,否则使用 none。
+
+```html
+Accept-Ranges: bytes
+```
+
+### 3. 响应状态码
+
+- 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。
+- 在请求的范围越界的情况下,服务器会返回 416 Requested Range Not Satisfiable 状态码。
+- 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。
+
+## 分块传输编码
-涉及以下首部字段:Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Language。
+Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。
+
+## 多部分对象集合
+
+一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。
+
+例如,上传多个表单时可以使用如下方式:
+
+```html
+Content-Type: multipart/form-data; boundary=AaB03x
+
+--AaB03x
+Content-Disposition: form-data; name="submit-name"
+
+Larry
+--AaB03x
+Content-Disposition: form-data; name="files"; filename="file1.txt"
+Content-Type: text/plain
+
+... contents of file1.txt ...
+--AaB03x--
+```
## 虚拟主机
HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。
-使用 Host 首部字段进行处理。
-
## 通信数据转发
### 1. 代理
代理服务器接受客户端的请求,并且转发给其它服务器。
-使用代理的主要目的是:缓存、网络访问控制以及访问日志记录。
+使用代理的主要目的是:
+
+- 缓存
+- 网络访问控制
+- 访问日志记录
代理服务器分为正向代理和反向代理两种,用户察觉得到正向代理的存在,而反向代理一般位于内部网络中,用户察觉不到。
@@ -513,13 +653,13 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,
HTTP 有以下安全性问题:
-1. 使用明文进行通信,内容可能会被窃听;
-2. 不验证通信方的身份,通信方的身份有可能遭遇伪装;
-3. 无法证明报文的完整性,报文有可能遭篡改。
+- 使用明文进行通信,内容可能会被窃听;
+- 不验证通信方的身份,通信方的身份有可能遭遇伪装;
+- 无法证明报文的完整性,报文有可能遭篡改。
-HTTPs 并不是新协议,而是 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信。也就是说 HTTPs 使用了隧道进行通信。
+HTTPs 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信。也就是说 HTTPs 使用了隧道进行通信。
-通过使用 SSL,HTTPs 具有了加密、认证和完整性保护。
+通过使用 SSL,HTTPs 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。
@@ -545,7 +685,7 @@ HTTPs 并不是新协议,而是 HTTP 先和 SSL(Secure Sockets Layer)通
### 3. HTTPs 采用的加密方式
-HTTPs 采用混合的加密机制,使用公开密钥加密用于传输对称密钥,之后使用对称密钥加密进行通信。(下图中的 Session Key 就是对称密钥)
+HTTPs 采用混合的加密机制,使用公开密钥加密用于传输对称密钥来保证安全性,之后使用对称密钥加密进行通信来保证效率。(下图中的 Session Key 就是对称密钥)
@@ -553,59 +693,66 @@ HTTPs 采用混合的加密机制,使用公开密钥加密用于传输对称
通过使用 **证书** 来对通信方进行认证。
-数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。
+数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。
-进行 HTTPs 通信时,服务器会把证书发送给客户端,客户端取得其中的公开密钥之后,先进行验证,如果验证通过,就可以开始通信。
+服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。
-
+进行 HTTPs 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。
-使用 OpenSSL 这套开源程序,每个人都可以构建一套属于自己的认证机构,从而自己给自己颁发服务器证书。浏览器在访问该服务器时,会显示“无法确认连接安全性”或“该网站的安全证书存在问题”等警告消息。
+
-## 完整性
+## 完整性保护
-SSL 提供报文摘要功能来验证完整性。
+SSL 提供报文摘要功能来进行完整性保护。
-# 七、Web 攻击技术
+HTTP 也提供了 MD5 报文摘要功能,但是却不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生篡改。
-## 攻击模式
+HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。
-### 1. 主动攻击
+## HTTPs 的缺点
-直接攻击服务器,具有代表性的有 SQL 注入和 OS 命令注入。
+- 因为需要进行加密解密等过程,因此速度会更慢;
+- 需要支付证书授权的高费用。
-### 2. 被动攻击
+## 配置 HTTPs
-设下圈套,让用户发送有攻击代码的 HTTP 请求,用户会泄露 Cookie 等个人信息,具有代表性的有跨站脚本攻击和跨站请求伪造。
+[Nginx 配置 HTTPS 服务器](https://aotu.io/notes/2016/08/16/nginx-https/index.html)
+
+# 七、Web 攻击技术
## 跨站脚本攻击
### 1. 概念
-跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和 Cookie 等各种内容。
+跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。
-例如有一个论坛网站,攻击者可以在上面发表以下内容:
+例如有一个论坛网站,攻击者可以在上面发布以下内容:
-```
+```html
```
之后该内容可能会被渲染成以下形式:
-```
+```html
```
-另一个用户浏览了含有这个内容的页面将会跳往 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。
+另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。
### 2. 危害
-- 伪造虚假的输入表单骗取个人信息
- 窃取用户的 Cookie 值
+- 伪造虚假的输入表单骗取个人信息
- 显示伪造的文章或者图片
### 3. 防范手段
-(一)过滤特殊字符
+**(一)设置 Cookie 为 HttpOnly**
+
+设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,在一定程度上可以防止 XSS 窃取用户的 Cookie 信息。
+
+**(二)过滤特殊字符**
许多语言都提供了对 HTML 的过滤:
@@ -614,23 +761,83 @@ SSL 提供报文摘要功能来验证完整性。
- Java 的 xssprotect (Open Source Library)。
- Node.js 的 node-validator。
-(二)指定 HTTP 的 Content-Type
+例如 htmlspecialchars() 可以将 `<` 转义为 `<`,将 `>` 转义为 `>`,从而避免 HTML 和 Jascript 代码的运行。
+
+**(三)富文本编辑器的处理**
-通过这种方式,可以避免内容被当成 HTML 解析,比如 PHP 语言可以使用以下代码:
+富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。
+
+富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,可以定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。
+
+以下例子中,form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。
+
+[XSS 过滤在线测试](http://jsxss.com/zh/try.html)
+
+```html
+XSS Demo
-```php
-
+
+Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist.
+
+
+
+
+hello
+
+
+ http
+
+
+Features:
+
+ - Specifies HTML tags and their attributes allowed with whitelist
+ - Handle any tags or attributes using custom function
+
+
+
```
-## 跨站点请求伪造
+```html
+XSS Demo
+
+
+Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist.
+
+
+<form>
+ <input type="text" name="q" value="test">
+ <button id="submit">Submit</button>
+</form>
+
+hello
+
+
+ http
+
+
+Features:
+
+ - Specifies HTML tags and their attributes allowed with whitelist
+ - Handle any tags or attributes using custom function
+
+
+<script type="text/javascript">
+alert(/xss/);
+</script>
+```
+
+## 跨站请求伪造
### 1. 概念
-跨站点请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
+跨站请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
-XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
+XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户浏览器的信任。
假如一家银行用以执行转账操作的 URL 地址如下:
@@ -652,21 +859,23 @@ http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
### 2. 防范手段
-(一)检查 Referer 字段
+**(一)检查 Referer 字段**
-HTTP 头中有一个 Referer 字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。
+HTTP 头中有一个 Referer 字段,这个字段用于标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
-(二)添加校验 Token
+**(二)添加校验 Token**
由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 Token 的值为空或者错误,拒绝这个可疑请求。
+也可以要求用户输入验证码来进行校验。
+
## SQL 注入攻击
### 1. 概念
-服务器上的数据库运行非法的 SQL 语句。
+服务器上的数据库运行非法的 SQL 语句,主要通过拼接来完成。
### 2. 攻击原理
@@ -695,26 +904,24 @@ strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'
strSQL = "SELECT * FROM users;"
```
-### 3. 危害
+### 3. 防范手段
-- 数据表中的数据外泄,例如个人机密数据,账户数据,密码等。
-- 数据结构被黑客探知,得以做进一步攻击(例如 SELECT * FROM sys.tables)。
-- 数据库服务器被攻击,系统管理员账户被窜改(例如 ALTER LOGIN sa WITH PASSWORD='xxxxxx')。
-- 获取系统较高权限后,有可能得以在网页加入恶意链接、恶意代码以及 XSS 等。
-- 经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统(例如 xp_cmdshell "net stop iisadmin" 可停止服务器的 IIS 服务)。
-- 破坏硬盘数据,瘫痪全系统(例如 xp_cmdshell "FORMAT C:")。
+**(一)使用参数化查询**
-### 4. 防范手段
+以下以 Java 中的 PreparedStatement 为例,它是预先编译的 SQL 语句,可以传入适当参数并且多次执行。由于没有拼接的过程,因此可以防止 SQL 注入的发生。
-- 在设计应用程序时,完全使用参数化查询(Parameterized Query)来设计数据访问功能。
-- 在组合 SQL 字符串时,先针对所传入的参数作字符取代(将单引号字符取代为连续 2 个单引号字符)。
-- 如果使用 PHP 开发网页程序的话,亦可打开 PHP 的魔术引号(Magic quote)功能(自动将所有的网页传入参数,将单引号字符取代为连续 2 个单引号字符)。
-- 其他,使用其他更安全的方式连接 SQL 数据库。例如已修正过 SQL 注入问题的数据库连接组件,例如 ASP.NET 的 SqlDataSource 对象或是 LINQ to SQL。
-- 使用 SQL 防注入系统。
+```java
+PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE userid=? AND password=?");
+stmt.setString(1, userid);
+stmt.setString(2, password);
+ResultSet rs = stmt.executeQuery();
+```
-## 拒绝服务攻击
+**(二)单引号转换**
-### 1. 概念
+将传入的参数中的单引号转换为连续两个单引号,PHP 中的 Magic quote 可以完成这个功能。
+
+## 拒绝服务攻击
拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。
@@ -724,11 +931,13 @@ strSQL = "SELECT * FROM users;"
# 八、GET 和 POST 的区别
-## 参数
+## 作用
-GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在内容实体中。
+GET 用于获取资源,而 POST 用于传输实体主体。
-GET 的传参方式相比于 POST 安全性较差,因为 GET 传的参数在 URL 中是可见的,可能会泄露私密信息。并且 GET 只支持 ASCII 字符,如果参数为中文则可能会出现乱码,而 POST 支持标准字符集。
+## 参数
+
+GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。
```
GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
@@ -740,6 +949,10 @@ Host: w3schools.com
name1=value1&name2=value2
```
+不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。
+
+因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码,例如`中文`会转换为`%E4%B8%AD%E6%96%87`,而空格会转换为`%20`。POST 支持标准字符集。
+
## 安全
安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。
@@ -771,7 +984,7 @@ POST /add_row HTTP/1.1 -> Adds a 2nd row
POST /add_row HTTP/1.1 -> Adds a 3rd row
```
-DELETE /idX/delete HTTP/1.1 是幂等的,即便是不同请求之间接收到的状态码不一样:
+DELETE /idX/delete HTTP/1.1 是幂等的,即便不同的请求接收到的状态码不一样:
```
DELETE /idX/delete HTTP/1.1 -> Returns 200 if idX exists
@@ -783,9 +996,9 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404
如果要对响应进行缓存,需要满足以下条件:
-1. 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。
-2. 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。
-3. 响应报文的 Cache-Control 首部字段没有指定不进行缓存。
+- 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。
+- 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。
+- 响应报文的 Cache-Control 首部字段没有指定不进行缓存。
## XMLHttpRequest
@@ -793,45 +1006,57 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404
> XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。
-在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。
+在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。而 GET 方法 Header 和 Data 会一起发送。
+
+# 九、HTTP/1.0 与 HTTP/1.1 的区别
+
+- HTTP/1.1 默认是持久连接
+- HTTP/1.1 支持管线化处理
+- HTTP/1.1 支持虚拟主机
+- HTTP/1.1 新增状态码 100
+- HTTP/1.1 支持分块传输编码
+- HTTP/1.1 新增缓存处理指令 max-age
-# 九、各版本比较
+# 十、HTTP/2.0
-## HTTP/1.0 与 HTTP/1.1 的区别
+## HTTP/1.x 缺陷
-1. HTTP/1.1 默认是持久连接
-2. HTTP/1.1 支持管线化处理
-3. HTTP/1.1 支持虚拟主机
-4. HTTP/1.1 新增状态码 100
-5. HTTP/1.1 支持分块传输编码
-6. HTTP/1.1 新增缓存处理指令 max-age
+ HTTP/1.x 实现简单是以牺牲应用性能为代价的:
-具体内容见上文
+- 客户端需要使用多个连接才能实现并发和缩短延迟;
+- 不会压缩请求和响应首部,从而导致不必要的网络流量;
+- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。
-## HTTP/1.1 与 HTTP/2.0 的区别
+## 二进制分帧层
-> [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
+HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。
-### 1. 多路复用
+
-HTTP/2.0 使用多路复用技术,同一个 TCP 连接可以处理多个请求。
+在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。一个数据流都有一个唯一标识符和可选的优先级信息,用于承载双向信息。消息(Message)是与逻辑请求或响应消息对应的完整的一系列帧。帧(Fram)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。
-### 2. 首部压缩
+
-HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 要求通讯双方各自缓存一份首部字段表,从而避免了重复传输。
+## 服务端推送
-### 3. 服务端推送
+HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。
-HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发给客户端。
+
-### 4. 二进制格式
+## 首部压缩
-HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。
+HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。
+
+
# 参考资料
- 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014.
- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
+- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
+- [htmlspecialchars](http://php.net/manual/zh/function.htmlspecialchars.php)
+- [How to Fix SQL Injection Using Java PreparedStatement & CallableStatement](https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement)
+- [浅谈 HTTP 中 Get 与 Post 的区别](https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html)
- [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/)
- [HTTP (HyperText Transfer Protocol)](https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html)
- [Web-VPN: Secure Proxies with SPDY & Chrome](https://www.igvita.com/2011/12/01/web-vpn-secure-proxies-with-spdy-chrome/)
@@ -842,6 +1067,10 @@ HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。
- [Sun Directory Server Enterprise Edition 7.0 Reference - Key Encryption](https://docs.oracle.com/cd/E19424-01/820-4811/6ng8i26bn/index.html)
- [An Introduction to Mutual SSL Authentication](https://www.codeproject.com/Articles/326574/An-Introduction-to-Mutual-SSL-Authentication)
- [The Difference Between URLs and URIs](https://danielmiessler.com/study/url-uri/)
+- [Cookie 与 Session 的区别](https://juejin.im/entry/5766c29d6be3ff006a31b84e#comment)
+- [COOKIE 和 SESSION 有什么区别](https://www.zhihu.com/question/19786827)
+- [Cookie/Session 的机制与安全](https://harttle.land/2015/08/10/cookie-session.html)
+- [HTTPS 证书原理](https://shijianan.com/2017/06/11/https/)
- [维基百科:跨站脚本](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC)
- [维基百科:SQL 注入攻击](https://zh.wikipedia.org/wiki/SQL%E8%B3%87%E6%96%99%E9%9A%B1%E7%A2%BC%E6%94%BB%E6%93%8A)
- [维基百科:跨站点请求伪造](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0)
diff --git "a/notes/JDK \344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/notes/JDK \344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md"
index 92f53cb3b92726f2cf058483de88e7b8f2729758..0d858e4bd7f695d8bda1801d2df1d6226e9949c8 100644
--- "a/notes/JDK \344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md"
+++ "b/notes/JDK \344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md"
@@ -138,7 +138,7 @@ java.util.Enumeration
## 5. 中间人模式
-使用中间人对象来封装对象之间的交互。中间人模式可以让降低交互对象之间的耦合程度。
+使用中间人对象来封装对象之间的交互。中间人模式可以降低交互对象之间的耦合程度。
```java
java.util.Timer
diff --git a/notes/Java IO.md b/notes/Java IO.md
index b48c90c3041e46d49b470ecd7e2423b63bf092fa..885ecbe1b0eef98e705fcf2e03a5981f36f99089 100644
--- a/notes/Java IO.md
+++ b/notes/Java IO.md
@@ -14,6 +14,7 @@
* [通道与缓冲区](#通道与缓冲区)
* [缓冲区状态变量](#缓冲区状态变量)
* [文件 NIO 实例](#文件-nio-实例)
+ * [选择器](#选择器)
* [套接字 NIO 实例](#套接字-nio-实例)
* [内存映射文件](#内存映射文件)
* [对比](#对比)
@@ -74,10 +75,16 @@ byte[] bytes = str.getBytes(encoding); // 编码
String str = new String(bytes, encoding); // 解码
```
-GBK 编码中,中文占 2 个字节,英文占 1 个字节;UTF-8 编码中,中文占 3 个字节,英文占 1 个字节;Java 使用双字节编码 UTF-16be,中文和英文都占 2 个字节。
-
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
+- GBK 编码中,中文占 2 个字节,英文占 1 个字节;
+- UTF-8 编码中,中文占 3 个字节,英文占 1 个字节;
+- UTF-16be 编码中,中文和英文都占 2 个字节。
+
+UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
+
+Java 使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码正是为了让一个中文或者一个英文都能使用一个 char 来存储。
+
# 五、对象操作
序列化就是将一个对象转换成字节序列,方便存储和传输。
@@ -148,15 +155,19 @@ is.close();
# 七、NIO
-新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。
+- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
+- [Java NIO 浅析](https://tech.meituan.com/nio.html)
+- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
+
+新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
## 流与块
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.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
@@ -177,7 +188,7 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重
### 2. 缓冲区
-发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
+发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
@@ -203,11 +214,11 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重
-② 从输入通道中读取 3 个字节数据写入缓冲区中,此时 position 移动设为 3,limit 保持不变。
+② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 移动设置为 5,limit 保持不变。
-
+
-③ 以下图例为已经从输入通道读取了 5 个字节数据写入缓冲区中。在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
+③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
@@ -221,153 +232,206 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重
## 文件 NIO 实例
-① 为要读取的文件创建 FileInputStream,之后通过 FileInputStream 获取输入 FileChannel;
+以下展示了使用 NIO 快速复制文件的实例:
```java
-FileInputStream fin = new FileInputStream("readandshow.txt");
-FileChannel fic = fin.getChannel();
-```
-
-② 创建一个容量为 1024 的 Buffer;
-
-```java
-ByteBuffer buffer = ByteBuffer.allocate(1024);
-```
-
-③ 将数据从输入 FileChannel 写入到 Buffer 中,如果没有数据的话,read() 方法会返回 -1;
-
-```java
-int r = fcin.read(buffer);
-if (r == -1) {
- break;
+public class FastCopyFile {
+ public static void main(String args[]) throws Exception {
+
+ String inFile = "data/abc.txt";
+ String outFile = "data/abc-copy.txt";
+
+ // 获得源文件的输入字节流
+ FileInputStream fin = new FileInputStream(inFile);
+ // 获取输入字节流的文件通道
+ FileChannel fcin = fin.getChannel();
+
+ // 获取目标文件的输出字节流
+ FileOutputStream fout = new FileOutputStream(outFile);
+ // 获取输出字节流的通道
+ FileChannel fcout = fout.getChannel();
+
+ // 为缓冲区分配 1024 个字节
+ ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
+
+ while (true) {
+ // 从输入通道中读取数据到缓冲区中
+ int r = fcin.read(buffer);
+ // read() 返回 -1 表示 EOF
+ if (r == -1)
+ break;
+ // 切换读写
+ buffer.flip();
+ // 把缓冲区的内容写入输出文件中
+ fcout.write(buffer);
+ // 清空缓冲区
+ buffer.clear();
+ }
+ }
}
```
-④ 为要写入的文件创建 FileOutputStream,之后通过 FileOutputStream 获取输出 FileChannel
+## 选择器
-```java
-FileOutputStream fout = new FileOutputStream("writesomebytes.txt");
-FileChannel foc = fout.getChannel();
-```
+一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去检查多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
-⑤ 调用 flip() 切换读写
+因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件具有更好的性能。
-```java
-buffer.flip();
-```
+
-⑥ 把 Buffer 中的数据读取到输出 FileChannel 中
+### 1. 创建选择器
```java
-foc.write(buffer);
+Selector selector = Selector.open();
```
-⑦ 最后调用 clear() 重置缓冲区
+### 2. 将通道注册到选择器上
```java
-buffer.clear();
+ServerSocketChannel ssChannel = ServerSocketChannel.open();
+ssChannel.configureBlocking(false);
+ssChannel.register(selector, SelectionKey.OP_ACCEPT);
```
-## 套接字 NIO 实例
+通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它时间,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
-### 1. ServerSocketChannel
+在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
-每一个监听端口都需要有一个 ServerSocketChannel 用来监听连接。
+- SelectionKey.OP_CONNECT
+- SelectionKey.OP_ACCEPT
+- SelectionKey.OP_READ
+- SelectionKey.OP_WRITE
-```java
-ServerSocketChannel ssc = ServerSocketChannel.open();
-ssc.configureBlocking(false); // 设置为非阻塞
+它们在 SelectionKey 的定义如下:
-ServerSocket ss = ssc.socket();
-InetSocketAddress address = new InetSocketAddress(ports[i]);
-ss.bind(address); // 绑定端口号
+```java
+public static final int OP_READ = 1 << 0;
+public static final int OP_WRITE = 1 << 2;
+public static final int OP_CONNECT = 1 << 3;
+public static final int OP_ACCEPT = 1 << 4;
```
-### 2. Selectors
-
-异步 I/O 通过 Selector 注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字连接等等,在发生这样的事件时,系统将会发送通知。
-
-创建 Selectors 之后,就可以对不同的通道对象调用 register() 方法。register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 ACCEPT 事件,也就是在新的连接建立时所发生的事件。
-
-SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。
+可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:
```java
-Selector selector = Selector.open();
-SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);
+int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
```
-### 3. 主循环
-
-首先,我们调用 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时,select() 方法将返回所发生的事件的数量。
-
-接下来,我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个集合。
-
-我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。
+### 3. 监听事件
```java
int num = selector.select();
-
-Set selectedKeys = selector.selectedKeys();
-Iterator it = selectedKeys.iterator();
-
-while (it.hasNext()) {
- SelectionKey key = (SelectionKey)it.next();
- // ... deal with I/O event ...
-}
```
-### 4. 监听新连接
+使用 select() 来监听事件到达,它会一直阻塞直到有至少一个事件到达。
-程序执行到这里,我们仅注册了 ServerSocketChannel,并且仅注册它们“接收”事件。为确认这一点,我们对 SelectionKey 调用 readyOps() 方法,并检查发生了什么类型的事件:
+### 4. 获取到达的事件
```java
-if ((key.readyOps() & SelectionKey.OP_ACCEPT)
- == SelectionKey.OP_ACCEPT) {
- // Accept the new connection
- // ...
+Set keys = selector.selectedKeys();
+Iterator keyIterator = keys.iterator();
+while (keyIterator.hasNext()) {
+ SelectionKey key = keyIterator.next();
+ if (key.isAcceptable()) {
+ // ...
+ } else if (key.isReadable()) {
+ // ...
+ }
+ keyIterator.remove();
}
```
-可以肯定地说,readOps() 方法告诉我们该事件是新的连接。
-
-### 5. 接受新的连接
-
-因为我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;也就是说,不用担心 accept() 操作会阻塞:
+### 5. 事件循环
-```java
-ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
-SocketChannel sc = ssc.accept();
-```
-
-下一步是将新连接的 SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将 SocketChannel 注册到 Selector 上,如下所示:
+因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。
```java
-sc.configureBlocking(false);
-SelectionKey newKey = sc.register(selector, SelectionKey.OP_READ);
+while (true) {
+ int num = selector.select();
+ Set keys = selector.selectedKeys();
+ Iterator keyIterator = keys.iterator();
+ while (keyIterator.hasNext()) {
+ SelectionKey key = keyIterator.next();
+ if (key.isAcceptable()) {
+ // ...
+ } else if (key.isReadable()) {
+ // ...
+ }
+ keyIterator.remove();
+ }
+}
```
-注意我们使用 register() 的 OP_READ 参数,将 SocketChannel 注册用于读取而不是接受新连接。
-
-### 6. 删除处理过的 SelectionKey
-
-在处理 SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的 remove() 方法来删除处理过的 SelectionKey:
+## 套接字 NIO 实例
```java
-it.remove();
+public class NIOServer {
+
+ public static void main(String[] args) throws IOException {
+ Selector selector = Selector.open();
+
+ ServerSocketChannel ssChannel = ServerSocketChannel.open();
+ ssChannel.configureBlocking(false);
+ ssChannel.register(selector, SelectionKey.OP_ACCEPT);
+
+ ServerSocket serverSocket = ssChannel.socket();
+ InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
+ serverSocket.bind(address);
+
+ while (true) {
+ selector.select();
+ Set keys = selector.selectedKeys();
+ Iterator keyIterator = keys.iterator();
+ while (keyIterator.hasNext()) {
+ SelectionKey key = keyIterator.next();
+ if (key.isAcceptable()) {
+ ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
+ // 服务器会为每个新连接创建一个 SocketChannel
+ SocketChannel sChannel = ssChannel1.accept();
+ sChannel.configureBlocking(false);
+ // 这个新连接主要用于从客户端读取数据
+ sChannel.register(selector, SelectionKey.OP_READ);
+ } else if (key.isReadable()) {
+ SocketChannel sChannel = (SocketChannel) key.channel();
+ System.out.println(readDataFromSocketChannel(sChannel));
+ sChannel.close();
+ }
+ keyIterator.remove();
+ }
+ }
+ }
+
+ private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
+ ByteBuffer buffer = ByteBuffer.allocate(1024);
+ StringBuffer data = new StringBuffer();
+ while (true) {
+ buffer.clear();
+ int n = sChannel.read(buffer);
+ if (n == -1)
+ break;
+ buffer.flip();
+ int limit = buffer.limit();
+ char[] dst = new char[limit];
+ for (int i = 0; i < limit; i++)
+ dst[i] = (char) buffer.get(i);
+ data.append(dst);
+ buffer.clear();
+ }
+ return data.toString();
+ }
+}
```
-现在我们可以返回主循环并接受从一个套接字中传入的数据 (或者一个传入的 I/O 事件) 了。
-
-### 7. 传入的 I/O
-
-当来自一个套接字的数据到达时,它会触发一个 I/O 事件。这会导致在主循环中调用 Selector.select(),并返回一个或者多个 I/O 事件。这一次, SelectionKey 将被标记为 OP_READ 事件,如下所示:
-
```java
-} else if ((key.readyOps() & SelectionKey.OP_READ)
- == SelectionKey.OP_READ) {
- // Read the data
- SocketChannel sc = (SocketChannel)key.channel();
- // ...
+public class NIOClient {
+
+ public static void main(String[] args) throws IOException {
+ Socket socket = new Socket("127.0.0.1", 8888);
+ OutputStream out = socket.getOutputStream();
+ String s = "hello world";
+ out.write(s.getBytes());
+ out.close();
+ }
}
```
diff --git "a/notes/Java \345\237\272\347\241\200.md" "b/notes/Java \345\237\272\347\241\200.md"
index 34bd383e848b0d4f4966435cf64ffe72f5d9b811..6424600aad576316e2533e7c4aedfaab0eed3088 100644
--- "a/notes/Java \345\237\272\347\241\200.md"
+++ "b/notes/Java \345\237\272\347\241\200.md"
@@ -27,7 +27,7 @@
* [十一、特性](#十一特性)
* [面向对象三大特性](#面向对象三大特性)
* [Java 各版本的新特性](#java-各版本的新特性)
- * [Java 与 C++ 的区别](#java-与-c++-的区别)
+ * [Java 与 C++ 的区别](#java-与-c-的区别)
* [JRE or JDK](#jre-or-jdk)
* [参考资料](#参考资料)
@@ -51,7 +51,7 @@ final A y = new A();
y.a = 1;
```
-**2. 方法**
+**2. 方法**
声明方法不能被子类覆盖。
@@ -79,7 +79,7 @@ public class A {
**2. 静态方法**
-静态方法在类加载的时候就存在了,它不依赖于任何实例,所以 static 方法必须实现,也就是说它不能是抽象方法(abstract)。
+静态方法在类加载的时候就存在了,它不依赖于任何实例,所以静态方法必须有实现,也就是说它不能是抽象方法(abstract)。
**3. 静态语句块**
@@ -87,11 +87,11 @@ public class A {
**4. 静态内部类**
-内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非 static 变量和方法。
+内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非静态的变量和方法。
**5. 静态导包**
-```source-java
+```java
import static com.xxx.ClassName.*
```
@@ -131,12 +131,12 @@ public InitialOrderTest() {
存在继承的情况下,初始化顺序为:
-1. 父类(静态变量、静态语句块)
-2. 子类(静态变量、静态语句块)
-3. 父类(实例变量、普通语句块)
-4. 父类(构造函数)
-5. 子类(实例变量、普通语句块)
-6. 子类(构造函数)
+- 父类(静态变量、静态语句块)
+- 子类(静态变量、静态语句块)
+- 父类(实例变量、普通语句块)
+- 父类(构造函数)
+- 子类(实例变量、普通语句块)
+- 子类(构造函数)
# 二、Object 通用方法
@@ -191,15 +191,14 @@ x.equals(x); // true
(二)对称性
```java
-x.equals(y) == y.equals(x) // true
+x.equals(y) == y.equals(x); // true
```
(三)传递性
```java
-if(x.equals(y) && y.equals(z)) {
+if (x.equals(y) && y.equals(z))
x.equals(z); // true;
-}
```
(四)一致性
@@ -255,7 +254,7 @@ public class EqualExample {
hasCode() 返回散列值,而 equals() 是用来判断两个实例是否等价。等价的两个实例散列值一定要相同,但是散列值相同的两个实例不一定等价。
-在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证相等的两个实例散列值也等价。
+在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个实例散列值也相等。
下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例。
@@ -271,7 +270,7 @@ System.out.println(set.size()); // 2
理想的散列函数应当具有均匀性,即不相等的实例应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
-一个数与 31 相乘可以转换成移位和减法:31\*x == (x<<5)-x。
+一个数与 31 相乘可以转换成移位和减法:`31\*x == (x<<5)-x`,编译器会自动进行这个优化。
```java
@Override
@@ -593,9 +592,9 @@ ac2.func1();
从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。
-接口也可以包含字段,并且这些字段隐式都是 static 和 final 的。
+接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。
-接口中的方法默认都是 public 的,并且不允许定义为 private 或者 protected。
+接口的字段默认都是 static 和 final 的。
```java
public interface InterfaceExample {
@@ -606,7 +605,7 @@ public interface InterfaceExample {
}
int x = 123;
- //int y; // Variable 'y' might not have been initialized
+ // int y; // Variable 'y' might not have been initialized
public int z = 0; // Modifier 'public' is redundant for interface fields
// private int k = 0; // Modifier 'private' not allowed here
// protected int l = 0; // Modifier 'protected' not allowed here
@@ -892,15 +891,15 @@ switch (s) {
switch 不支持 long,是因为 swicth 的设计初衷是为那些只需要对少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
```java
-// long x = 111;
-// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum'
-// case 111:
-// System.out.println(111);
-// break;
-// case 222:
-// System.out.println(222);
-// break;
-// }
+// long x = 111;
+// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum'
+// case 111:
+// System.out.println(111);
+// break;
+// case 222:
+// System.out.println(222);
+// break;
+// }
```
> [Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java)
@@ -959,7 +958,7 @@ public class Box {
}
```
-> [Java 泛型详解](https://www.ziwenxie.site/2017/03/01/java-generic/)[10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693)
+> [Java 泛型详解](http://www.importnew.com/24029.html)[10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693)
# 十、注解
diff --git "a/notes/Java \345\256\271\345\231\250.md" "b/notes/Java \345\256\271\345\231\250.md"
index 6791e2ed8ffb92eeec6fdb6401b72d6a48631b5d..03130b849be94e4a786e566a0981cb21fd4886a1 100644
--- "a/notes/Java \345\256\271\345\231\250.md"
+++ "b/notes/Java \345\256\271\345\231\250.md"
@@ -9,11 +9,10 @@
* [ArrayList](#arraylist)
* [Vector](#vector)
* [LinkedList](#linkedlist)
+ * [HashMap](#hashmap)
+ * [ConcurrentHashMap](#concurrenthashmap)
* [LinkedHashMap](#linkedhashmap)
* [TreeMap](#treemap)
- * [HashMap](#hashmap)
- * [ConcurrentHashMap - JDK 1.7](#concurrenthashmap---jdk-17)
- * [ConcurrentHashMap - JDK 1.8](#concurrenthashmap---jdk-18)
* [参考资料](#参考资料)
@@ -28,7 +27,7 @@
### 1. Set
-- HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
+- HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的;
- TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logN);
@@ -40,7 +39,7 @@
- Vector:和 ArrayList 类似,但它是线程安全的;
-- LinkedList:基于双向循环链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
+- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
### 3. Queue
@@ -122,39 +121,41 @@ public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
```
-基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
-
-```java
-transient Object[] elementData; // non-private to simplify nested class access
-```
-
数组的默认大小为 10。
```java
private static final int DEFAULT_CAPACITY = 10;
```
-删除元素时需要调用 System.arraycopy() 对元素进行复制,因此删除操作成本很高。
+### 2. 序列化
+
+基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
```java
-public E remove(int index) {
- rangeCheck(index);
+transient Object[] elementData; // non-private to simplify nested class access
+```
- modCount++;
- E oldValue = elementData(index);
+### 3. 扩容
- int numMoved = size - index - 1;
- if (numMoved > 0)
- System.arraycopy(elementData, index+1, elementData, index, numMoved);
- elementData[--size] = null; // clear to let GC do its work
+添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。
- return oldValue;
+扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
+
+```java
+public boolean add(E e) {
+ ensureCapacityInternal(size + 1); // Increments modCount!!
+ elementData[size++] = e;
+ return true;
}
-```
-添加元素时使用 ensureCapacity() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,使得新容量为旧容量的 1.5 倍(oldCapacity + (oldCapacity >> 1))。扩容操作需要把原数组整个复制到新数组中,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
+private void ensureCapacityInternal(int minCapacity) {
+ if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
+ minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
+ }
+
+ ensureExplicitCapacity(minCapacity);
+}
-```java
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
@@ -176,7 +177,27 @@ private void grow(int minCapacity) {
}
```
-### 2. Fail-Fast
+### 4. 删除元素
+
+需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,复制的代价很高。
+
+```java
+public E remove(int index) {
+ rangeCheck(index);
+
+ modCount++;
+ E oldValue = elementData(index);
+
+ int numMoved = size - index - 1;
+ if (numMoved > 0)
+ System.arraycopy(elementData, index+1, elementData, index, numMoved);
+ elementData[--size] = null; // clear to let GC do its work
+
+ return oldValue;
+}
+```
+
+### 5. Fail-Fast
modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
@@ -203,39 +224,85 @@ private void writeObject(java.io.ObjectOutputStream s)
}
```
-### 3. 和 Vector 的区别
+## Vector
+
+[Vector.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/Vector.java)
+
+### 1. 同步
+
+它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。
+
+```java
+public synchronized boolean add(E e) {
+ modCount++;
+ ensureCapacityHelper(elementCount + 1);
+ elementData[elementCount++] = e;
+ return true;
+}
+
+public synchronized E get(int index) {
+ if (index >= elementCount)
+ throw new ArrayIndexOutOfBoundsException(index);
+
+ return elementData(index);
+}
+```
+
+### 2. ArrayList 与 Vector
- Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
- Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
-为了获得线程安全的 ArrayList,可以调用 Collections.synchronizedList(new ArrayList<>()); 返回一个线程安全的 ArrayList,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类;
+### 3. Vector 替代方案
-### 4. 和 LinkedList 的区别
+为了获得线程安全的 ArrayList,可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类;
-- ArrayList 基于动态数组实现,LinkedList 基于双向链表实现;
-- ArrayList 支持随机访问,LinkedList 不支持;
-- LinkedList 在任意位置添加删除元素更快。
-
-## Vector
+```java
+List list = new ArrayList<>();
+List synList = Collections.synchronizedList(list);
+```
-[Vector.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/Vector.java)
+```java
+List list = new CopyOnWriteArrayList();
+```
## LinkedList
[LinkedList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/LinkedList.java)
-## LinkedHashMap
+### 1. 概览
-[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java)
+基于双向链表实现,内部使用 Node 来存储链表节点信息。
-## TreeMap
+```java
+private static class Node {
+ E item;
+ Node next;
+ Node prev;
+}
+```
-[TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java)
+每个链表存储了 Head 和 Tail 指针:
+
+```java
+transient Node first;
+transient Node last;
+```
+
+
+
+### 2. ArrayList 与 LinkedList
+
+- ArrayList 基于动态数组实现,LinkedList 基于双向链表实现;
+- ArrayList 支持随机访问,LinkedList 不支持;
+- LinkedList 在任意位置添加删除元素更快。
## HashMap
[HashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java)
+为了便于理解,以下内容以 JDK 1.7 为主。
+
### 1. 存储结构
使用拉链法来解决冲突,内部包含了一个 Entry 类型的数组 table,数组中的每个位置被当成一个桶。
@@ -248,28 +315,26 @@ transient Entry[] table;
-JDK 1.8 使用 Node 类型存储一个键值对,它依然继承自 Entry,因此可以按照上面的存储结构来理解。
-
```java
-static class Node implements Map.Entry {
- final int hash;
+static class Entry implements Map.Entry {
final K key;
V value;
- Node next;
-
- Node(int hash, K key, V value, Node next) {
- this.hash = hash;
- this.key = key;
- this.value = value;
- this.next = next;
+ Entry next;
+ int hash;
+
+ Entry(int h, K k, V v, Entry n) {
+ value = v;
+ next = n;
+ key = k;
+ hash = h;
}
- public final K getKey() { return key; }
- public final V getValue() { return value; }
- public final String toString() { return key + "=" + value; }
+ public final K getKey() {
+ return key;
+ }
- public final int hashCode() {
- return Objects.hashCode(key) ^ Objects.hashCode(value);
+ public final V getValue() {
+ return value;
}
public final V setValue(V newValue) {
@@ -279,16 +344,42 @@ static class Node implements Map.Entry {
}
public final boolean equals(Object o) {
- if (o == this)
- return true;
- if (o instanceof Map.Entry) {
- Map.Entry,?> e = (Map.Entry,?>)o;
- if (Objects.equals(key, e.getKey()) &&
- Objects.equals(value, e.getValue()))
+ if (!(o instanceof Map.Entry))
+ return false;
+ Map.Entry e = (Map.Entry)o;
+ Object k1 = getKey();
+ Object k2 = e.getKey();
+ if (k1 == k2 || (k1 != null && k1.equals(k2))) {
+ Object v1 = getValue();
+ Object v2 = e.getValue();
+ if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
+
+ public final int hashCode() {
+ return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
+ }
+
+ public final String toString() {
+ return getKey() + "=" + getValue();
+ }
+
+ /**
+ * This method is invoked whenever the value in an entry is
+ * overwritten by an invocation of put(k,v) for a key k that's already
+ * in the HashMap.
+ */
+ void recordAccess(HashMap m) {
+ }
+
+ /**
+ * This method is invoked whenever the entry is
+ * removed from the table.
+ */
+ void recordRemoval(HashMap m) {
+ }
}
```
@@ -304,22 +395,166 @@ map.put("K3", "V3");
- 新建一个 HashMap,默认大小为 16;
- 插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。
- 插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
-- 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 后面。
+- 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 前面。
-
+应该注意到链表的插入是以头插法方式进行的,例如上面的 不是插在 后面,而是插入在链表头部。
查找需要分成两步进行:
- 计算键值对所在的桶;
- 在链表上顺序查找,时间复杂度显然和链表的长度成正比。
-### 3. 链表转红黑树
+
-应该注意到,从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。
+### 3. put 操作
+
+```java
+public V put(K key, V value) {
+ if (table == EMPTY_TABLE) {
+ inflateTable(threshold);
+ }
+ // 键为 null 单独处理
+ if (key == null)
+ return putForNullKey(value);
+ int hash = hash(key);
+ // 确定桶下标
+ int i = indexFor(hash, table.length);
+ // 先找出是否已经存在键位 key 的键值对,如果存在的话就更新这个键值对的值为 value
+ for (Entry e = table[i]; e != null; e = e.next) {
+ Object k;
+ if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
+ V oldValue = e.value;
+ e.value = value;
+ e.recordAccess(this);
+ return oldValue;
+ }
+ }
+
+ modCount++;
+ // 插入新键值对
+ addEntry(hash, key, value, i);
+ return null;
+}
+```
+
+HashMap 允许插入键位 null 的键值对,因为无法调用 null 的 hashCode(),也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。
+
+```java
+private V putForNullKey(V value) {
+ for (Entry e = table[0]; e != null; e = e.next) {
+ if (e.key == null) {
+ V oldValue = e.value;
+ e.value = value;
+ e.recordAccess(this);
+ return oldValue;
+ }
+ }
+ modCount++;
+ addEntry(0, null, value, 0);
+ return null;
+}
+```
+
+使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。
+
+```java
+void addEntry(int hash, K key, V value, int bucketIndex) {
+ if ((size >= threshold) && (null != table[bucketIndex])) {
+ resize(2 * table.length);
+ hash = (null != key) ? hash(key) : 0;
+ bucketIndex = indexFor(hash, table.length);
+ }
+
+ createEntry(hash, key, value, bucketIndex);
+}
+
+void createEntry(int hash, K key, V value, int bucketIndex) {
+ Entry e = table[bucketIndex];
+ // 头插法,链表头部指向新的键值对
+ table[bucketIndex] = new Entry<>(hash, key, value, e);
+ size++;
+}
+```
+
+```java
+Entry(int h, K k, V v, Entry n) {
+ value = v;
+ next = n;
+ key = k;
+ hash = h;
+}
+```
-### 4. 扩容
+### 4. 确定桶下标
-因为从 JDK 1.8 开始引入了红黑树,因此扩容操作较为复杂,为了便于理解,以下内容使用 JDK 1.7 的内容。
+很多操作都需要先确定一个键值对所在的桶下标。
+
+```java
+int hash = hash(key);
+int i = indexFor(hash, table.length);
+```
+
+(一)计算 hash 值
+
+```java
+final int hash(Object k) {
+ int h = hashSeed;
+ if (0 != h && k instanceof String) {
+ return sun.misc.Hashing.stringHash32((String) k);
+ }
+
+ h ^= k.hashCode();
+
+ // This function ensures that hashCodes that differ only by
+ // constant multiples at each bit position have a bounded
+ // number of collisions (approximately 8 at default load factor).
+ h ^= (h >>> 20) ^ (h >>> 12);
+ return h ^ (h >>> 7) ^ (h >>> 4);
+}
+```
+
+```java
+public final int hashCode() {
+ return Objects.hashCode(key) ^ Objects.hashCode(value);
+}
+```
+
+(二)取模
+
+令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:
+
+```
+x : 00010000
+x-1 : 00001111
+```
+
+令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:
+
+```
+y : 10110010
+x-1 : 00001111
+y&(x-1) : 00000010
+```
+
+这个性质和 y 对 x 取模效果是一样的:
+
+```
+x : 00010000
+y : 10110010
+y%x : 00000010
+```
+
+我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时能用位运算的话能带来更高的性能。
+
+确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。
+
+```java
+static int indexFor(int h, int length) {
+ return h & (length-1);
+}
+```
+
+### 5. 扩容-基本原理
设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。
@@ -399,69 +634,9 @@ void transfer(Entry[] newTable) {
}
```
-### 5. 确定桶下标
-
-很多操作都需要先确定一个键值对所在的桶下标,这个操作需要分三步进行。
-
-(一)调用 hashCode()
-
-```java
-public final int hashCode() {
- return Objects.hashCode(key) ^ Objects.hashCode(value);
-}
-```
-
-(二)高位运算
-
-将 hashCode 的高 16 位和低 16 位进行异或操作,使得在数组比较小时,也能保证高低位都参与到了哈希计算中。
-
-```java
-static final int hash(Object key) {
- int h;
- return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
-}
-```
-
-(三)除留余数
-
-令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:
-
-```
-x : 00010000
-x-1 : 00001111
-```
-
-令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:
-
-```
-y : 10110010
-x-1 : 00001111
-y&(x-1) : 00000010
-```
-
-这个性质和 y 对 x 取模效果是一样的:
-
-```
-x : 00010000
-y : 10110010
-y%x : 00000010
-```
-
-我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时能用位运算的话能带来更高的性能。
-
-拉链法需要使用除留余数法来得到桶下标,也就是需要进行以下计算:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。
-
-以下操作在 JDK 1.8 中没有,但是原理上相同。
-
-```java
-static int indexFor(int h, int length) {
- return h & (length-1);
-}
-```
-
### 6. 扩容-重新计算桶下标
-在进行扩容时,需要把 Node 重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。
+在进行扩容时,需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。
假设原数组长度 capacity 为 8,扩容之后 new capacity 为 16:
@@ -470,7 +645,7 @@ capacity : 00010000
new capacity : 00100000
```
-对于一个 Key,它的 hashCode 如果在第 6 位上为 0,那么除留余数得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 + 8。
+对于一个 Key,它的 hash 如果在第 6 位上为 0,那么取模得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 + 8。
### 7. 扩容-计算数组容量
@@ -505,11 +680,11 @@ static final int tableSizeFor(int cap) {
}
```
-### 7. null 值
+### 8. 链表转红黑树
-HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0 个桶的位置,因为这个 Key 无法计算 hashCode(),因此只能规定一个桶让它存放。
+应该注意到,从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。
-### 8. 与 HashTable 的区别
+### 9. HashMap 与 HashTable
- HashTable 是同步的,它使用了 synchronized 来进行同步。它也是线程安全的,多个线程可以共享同一个 HashTable。HashMap 不是同步的,但是可以使用 ConcurrentHashMap,它是 HashTable 的替代,而且比 HashTable 可扩展性更好。
- HashMap 可以插入键为 null 的 Entry。
@@ -517,18 +692,12 @@ HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0
- 由于 Hashtable 是线程安全的也是 synchronized,所以在单线程环境下它比 HashMap 要慢。
- HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
-## ConcurrentHashMap - JDK 1.7
+## ConcurrentHashMap
[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/1.7/ConcurrentHashMap.java)
-ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶。
-
-相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。
-
### 1. 存储结构
-和 HashMap 类似。
-
```java
static final class HashEntry {
final int hash;
@@ -540,6 +709,8 @@ static final class HashEntry {
Segment 继承自 ReentrantLock,每个 Segment 维护着多个 HashEntry。
+ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。
+
```java
static final class Segment extends ReentrantLock implements Serializable {
@@ -572,100 +743,97 @@ static final int DEFAULT_CONCURRENCY_LEVEL = 16;
-### 2. HashEntry 的不可变性
-
-HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程 “看” 到。在 ConcurrentHashMap 中,不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。
-
-非结构性修改操作只是更改某个 HashEntry 的 value 域的值。由于对 Volatile 变量的写入操作将与随后对这个变量的读操作进行同步。当一个写线程修改了某个 HashEntry 的 value 域后,另一个读线程读这个值域,Java 内存模型能够保证读线程读取的一定是更新后的值。所以,写线程对链表的非结构性修改能够被后续不加锁的读线程 “看到”。
-
-对 ConcurrentHashMap 做结构性修改,实质上是对某个桶指向的链表做结构性修改。如果能够确保:在读线程遍历一个链表期间,写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表。那么读 / 写线程之间就可以安全并发访问这个 ConcurrentHashMap。
-
-结构性修改操作包括 put,remove,clear。下面我们分别分析这三个操作。
-
-clear 操作只是把 ConcurrentHashMap 中所有的桶 “置空”,每个桶之前引用的链表依然存在,只是桶不再引用到这些链表(所有链表的结构并没有被修改)。正在遍历某个链表的读线程依然可以正常执行对该链表的遍历。
-
-put 操作如果需要插入一个新节点到链表中时 , 会在链表头部插入这个新节点。此时,链表中的原有节点的链接并没有被修改。也就是说:插入新健 / 值对到链表中的操作不会影响读线程正常遍历这个链表。
-
-在以下链表中删除 C 节点,C 节点之后的所有节点都原样保留,C 节点之前的所有节点都被克隆到新的链表中,并且顺序被反转。可以注意到,在执行 remove 操作时,原始链表并没有被修改,也就是说,读线程不会受到执行 remove 操作的并发写线程的干扰。
+### 2. size 操作
-
+每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
-
-
-综上,可以得出一个结论:写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。
-
-### 3. Volatile 变量
-
-由于内存可见性问题,未正确同步的情况下,写线程写入的值可能并不为后续的读线程可见。
-
-下面以写线程 M 和读线程 N 来说明 ConcurrentHashMap 如何协调读 / 写线程间的内存可见性问题。
-
-
-
-假设线程 M 在写入了 volatile 型变量 count 后,线程 N 读取了这个 volatile 型变量 count。
-
-根据 happens-before 关系法则中的程序次序法则,A appens-before 于 B,C happens-before D。
+```java
+/**
+ * The number of elements. Accessed only either within locks
+ * or among other volatile reads that maintain visibility.
+ */
+transient int count;
+```
-根据 Volatile 变量法则,B happens-before C。
+在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。
-根据传递性,连接上面三个 happens-before 关系得到:A appens-before 于 B; B appens-before C;C happens-before D。也就是说:写线程 M 对链表做的结构性修改,在读线程 N 读取了同一个 volatile 变量后,对线程 N 也是可见的了。
+ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。
-虽然线程 N 是在未加锁的情况下访问链表。Java 的内存模型可以保证:只要之前对链表做结构性修改操作的写线程 M 在退出写方法前写 volatile 型变量 count,读线程 N 在读取这个 volatile 型变量 count 后,就一定能 “看到” 这些修改。
+尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。
-ConcurrentHashMap 中,每个 Segment 都有一个变量 count。它用来统计 Segment 中的 HashEntry 的个数。这个变量被声明为 volatile。
+如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
```java
-transient volatile int count;
-```
-
-所有不加锁读方法,在进入读方法时,首先都会去读这个 count 变量。比如下面的 get 方法:
-```java
-V get(Object key, int hash) {
- if(count != 0) { // 首先读 count 变量
- HashEntry e = getFirst(hash);
- while(e != null) {
- if(e.hash == hash && key.equals(e.key)) {
- V v = e.value;
- if(v != null)
- return v;
- // 如果读到 value 域为 null,说明发生了重排序,加锁后重新读取
- return readValueUnderLock(e);
- }
- e = e.next;
- }
- }
- return null;
+/**
+ * Number of unsynchronized retries in size and containsValue
+ * methods before resorting to locking. This is used to avoid
+ * unbounded retries if tables undergo continuous modification
+ * which would make it impossible to obtain an accurate result.
+ */
+static final int RETRIES_BEFORE_LOCK = 2;
+
+public int size() {
+ // Try a few times to get accurate count. On failure due to
+ // continuous async changes in table, resort to locking.
+ final Segment[] segments = this.segments;
+ int size;
+ boolean overflow; // true if size overflows 32 bits
+ long sum; // sum of modCounts
+ long last = 0L; // previous sum
+ int retries = -1; // first iteration isn't retry
+ try {
+ for (;;) {
+ // 超过尝试次数,则对每个 Segment 加锁
+ if (retries++ == RETRIES_BEFORE_LOCK) {
+ for (int j = 0; j < segments.length; ++j)
+ ensureSegment(j).lock(); // force creation
+ }
+ sum = 0L;
+ size = 0;
+ overflow = false;
+ for (int j = 0; j < segments.length; ++j) {
+ Segment seg = segmentAt(segments, j);
+ if (seg != null) {
+ sum += seg.modCount;
+ int c = seg.count;
+ if (c < 0 || (size += c) < 0)
+ overflow = true;
+ }
+ }
+ // 连续两次得到的结果一致,则认为这个结果是正确的
+ if (sum == last)
+ break;
+ last = sum;
+ }
+ } finally {
+ if (retries > RETRIES_BEFORE_LOCK) {
+ for (int j = 0; j < segments.length; ++j)
+ segmentAt(segments, j).unlock();
+ }
+ }
+ return overflow ? Integer.MAX_VALUE : size;
}
```
-在 ConcurrentHashMap 中,所有执行写操作的方法(put, remove, clear),在对链表做结构性修改之后,在退出写方法前都会去写这个 count 变量。所有未加锁的读操作(get, contains, containsKey)在读方法中,都会首先去读取这个 count 变量。
-
-根据 Java 内存模型,对同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程 “看到”。
-
-这个特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 ,读线程才需要加锁后重读)。
-### 4. 小结
+### 3. JDK 1.8 的改动
-ConcurrentHashMap 的高并发性主要来自于三个方面:
+[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/ConcurrentHashMap.java)
-- 用分离锁实现多个线程间的更深层次的共享访问。
-- 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
-- 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。
+JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发程度与 Segment 数量相等。
-## ConcurrentHashMap - JDK 1.8
+JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。
-[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/ConcurrentHashMap.java)
+并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。
-JDK 1.7 分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock。
+## LinkedHashMap
-JDK 1.8 的实现不是用了 Segment,Segment 属于重入锁 ReentrantLock。而是使用了内置锁 synchronized,主要是出于以下考虑:
+[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java)
-1. synchronized 的锁粒度更低;
-2. synchronized 优化空间更大;
-3. 在大量数据操作的情况下,ReentrantLock 会开销更多的内存。
+## TreeMap
-并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。
+[TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java)
# 参考资料
@@ -679,4 +847,5 @@ JDK 1.8 的实现不是用了 Segment,Segment 属于重入锁 ReentrantLock。
- [探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/)
- [HashMap 相关面试题及其解答](https://www.jianshu.com/p/75adf47958a7)
- [Java 集合细节(二):asList 的缺陷](http://wiki.jikexueyuan.com/project/java-enhancement/java-thirtysix.html)
+- [Java Collection Framework – The LinkedList Class](http://javaconceptoftheday.com/java-collection-framework-linkedlist-class/)
diff --git "a/notes/Java \345\271\266\345\217\221.md" "b/notes/Java \345\271\266\345\217\221.md"
index 61c9fdb8b2db16ba5a7189f039b438a12958cc88..8a2c8fcf5cd4ada1bbd8e2300d4705183d04db74 100644
--- "a/notes/Java \345\271\266\345\217\221.md"
+++ "b/notes/Java \345\271\266\345\217\221.md"
@@ -47,7 +47,7 @@
* [线程安全分类](#线程安全分类)
* [线程安全的实现方法](#线程安全的实现方法)
* [十二、锁优化](#十二锁优化)
- * [自旋锁与自适应自旋](#自旋锁与自适应自旋)
+ * [自旋锁](#自旋锁)
* [锁消除](#锁消除)
* [锁粗化](#锁粗化)
* [轻量级锁](#轻量级锁)
@@ -93,6 +93,10 @@
调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
+睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
+
+阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁;而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
+
| 进入方法 | 退出方法 |
| --- | --- |
| Thread.sleep() 方法 | 时间结束 |
@@ -109,9 +113,9 @@
有三种使用线程的方法:
-1. 实现 Runnable 接口;
-2. 实现 Callable 接口;
-3. 继承 Thread 类。
+- 实现 Runnable 接口;
+- 实现 Callable 接口;
+- 继承 Thread 类。
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
@@ -182,8 +186,8 @@ public static void main(String[] args) {
实现接口会更好一些,因为:
-1. Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
-2. 类可能只要求可执行就行,继承整个 Thread 类开销会过大。
+- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
+- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
# 三、基础线程机制
@@ -193,9 +197,9 @@ Executor 管理多个异步任务的执行,而无需程序员显式地管理
主要有三种 Executor:
-1. CachedThreadPool:一个任务创建一个线程;
-2. FixedThreadPool:所有任务只能使用固定大小的线程;
-3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
+- CachedThreadPool:一个任务创建一个线程;
+- FixedThreadPool:所有任务只能使用固定大小的线程;
+- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
```java
public static void main(String[] args) {
@@ -258,16 +262,10 @@ public void run() {
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
-对于以下代码,在 Main 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
+对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
```java
public class InterruptExample {
- public static void main(String[] args) throws InterruptedException {
- Thread thread1 = new MyThread1();
- thread1.start();
- thread1.interrupt();
- System.out.println("Main run");
- }
private static class MyThread1 extends Thread {
@Override
@@ -281,7 +279,15 @@ public class InterruptExample {
}
}
}
+```
+```java
+public static void main(String[] args) throws InterruptedException {
+ Thread thread1 = new MyThread1();
+ thread1.start();
+ thread1.interrupt();
+ System.out.println("Main run");
+}
```
```html
@@ -301,11 +307,6 @@ java.lang.InterruptedException: sleep interrupted
```java
public class InterruptExample {
- public static void main(String[] args) throws InterruptedException {
- Thread thread2 = new MyThread2();
- thread2.start();
- thread2.interrupt();
- }
private static class MyThread2 extends Thread {
@Override
@@ -319,6 +320,14 @@ public class InterruptExample {
}
```
+```java
+public static void main(String[] args) throws InterruptedException {
+ Thread thread2 = new MyThread2();
+ thread2.start();
+ thread2.interrupt();
+}
+```
+
```html
Thread end
```
@@ -330,20 +339,18 @@ Thread end
以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。
```java
-public class ExecutorInterruptExample {
- public static void main(String[] args) {
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> {
- try {
- Thread.sleep(2000);
- System.out.println("Thread run");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
- executorService.shutdownNow();
- System.out.println("Main run");
- }
+public static void main(String[] args) {
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> {
+ try {
+ Thread.sleep(2000);
+ System.out.println("Thread run");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ });
+ executorService.shutdownNow();
+ System.out.println("Main run");
}
```
@@ -385,7 +392,7 @@ public void func () {
它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
-对于以下代码,使用 ExecutorService 执行了两个线程(这两个线程使用 Lambda 创建),由于调用的是同一个对象的同步语句块,因此这两个线程就需要进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
+对于以下代码,使用 ExecutorService 执行了两个线程(这两个线程使用 Lambda 创建),由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
```java
public class SynchronizedExample {
@@ -397,13 +404,15 @@ public class SynchronizedExample {
}
}
}
+}
+```
- public static void main(String[] args) {
- SynchronizedExample e1 = new SynchronizedExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> e1.func1());
- executorService.execute(() -> e1.func1());
- }
+```java
+public static void main(String[] args) {
+ SynchronizedExample e1 = new SynchronizedExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> e1.func1());
+ executorService.execute(() -> e1.func1());
}
```
@@ -460,14 +469,16 @@ public class SynchronizedExample {
}
}
}
+}
+```
- public static void main(String[] args) {
- SynchronizedExample e1 = new SynchronizedExample();
- SynchronizedExample e2 = new SynchronizedExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> e1.func2());
- executorService.execute(() -> e2.func2());
- }
+```java
+public static void main(String[] args) {
+ SynchronizedExample e1 = new SynchronizedExample();
+ SynchronizedExample e2 = new SynchronizedExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> e1.func2());
+ executorService.execute(() -> e2.func2());
}
```
@@ -518,7 +529,7 @@ public static void main(String[] args) {
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
```
-ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了一些高级功能:
+ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了以下高级功能:
**1. 等待可中断**
@@ -540,7 +551,7 @@ synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
**2. 性能**
-从性能上来看,在新版本的 JDK 中对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由,而且 synchronized 有更大的优化空间,因此优先考虑 synchronized。
+从性能上来看,新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。
**3. 功能**
@@ -556,7 +567,7 @@ ReentrantLock 多了一些高级功能。
## join()
-在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
+在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待, 直到目标线程结束。
对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,因此 b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先与 b 线程的输出。
@@ -595,11 +606,13 @@ public class JoinExample {
b.start();
a.start();
}
+}
+```
- public static void main(String[] args) {
- JoinExample example = new JoinExample();
- example.test();
- }
+```java
+public static void main(String[] args) {
+ JoinExample example = new JoinExample();
+ example.test();
}
```
@@ -633,13 +646,15 @@ public class WaitNotifyExample {
}
System.out.println("after");
}
+}
+```
- public static void main(String[] args) {
- ExecutorService executorService = Executors.newCachedThreadPool();
- WaitNotifyExample example = new WaitNotifyExample();
- executorService.execute(() -> example.after());
- executorService.execute(() -> example.before());
- }
+```java
+public static void main(String[] args) {
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ WaitNotifyExample example = new WaitNotifyExample();
+ executorService.execute(() -> example.after());
+ executorService.execute(() -> example.before());
}
```
@@ -685,13 +700,15 @@ public class AwaitSignalExample {
lock.unlock();
}
}
+}
+```
- public static void main(String[] args) {
- ExecutorService executorService = Executors.newCachedThreadPool();
- AwaitSignalExample example = new AwaitSignalExample();
- executorService.execute(() -> example.after());
- executorService.execute(() -> example.before());
- }
+```java
+public static void main(String[] args) {
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ AwaitSignalExample example = new AwaitSignalExample();
+ executorService.execute(() -> example.after());
+ executorService.execute(() -> example.before());
}
```
@@ -875,9 +892,6 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
**使用 BlockingQueue 实现生产者消费者问题**
```java
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
-
public class ProducerConsumer {
private static BlockingQueue queue = new ArrayBlockingQueue<>(5);
@@ -906,20 +920,22 @@ public class ProducerConsumer {
System.out.print("consume..");
}
}
+}
+```
- public static void main(String[] args) {
- for (int i = 0; i < 2; i++) {
- Producer producer = new Producer();
- producer.start();
- }
- for (int i = 0; i < 5; i++) {
- Consumer consumer = new Consumer();
- consumer.start();
- }
- for (int i = 0; i < 3; i++) {
- Producer producer = new Producer();
- producer.start();
- }
+```java
+public static void main(String[] args) {
+ for (int i = 0; i < 2; i++) {
+ Producer producer = new Producer();
+ producer.start();
+ }
+ for (int i = 0; i < 5; i++) {
+ Consumer consumer = new Consumer();
+ consumer.start();
+ }
+ for (int i = 0; i < 3; i++) {
+ Producer producer = new Producer();
+ producer.start();
}
}
```
@@ -1002,22 +1018,24 @@ public class ThreadUnsafeExample {
public int get() {
return cnt;
}
+}
+```
- public static void main(String[] args) throws InterruptedException {
- final int threadSize = 1000;
- ThreadUnsafeExample example = new ThreadUnsafeExample();
- final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < threadSize; i++) {
- executorService.execute(() -> {
- example.add();
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- executorService.shutdown();
- System.out.println(example.get());
+```java
+public static void main(String[] args) throws InterruptedException {
+ final int threadSize = 1000;
+ ThreadUnsafeExample example = new ThreadUnsafeExample();
+ final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < threadSize; i++) {
+ executorService.execute(() -> {
+ example.add();
+ countDownLatch.countDown();
+ });
}
+ countDownLatch.await();
+ executorService.shutdown();
+ System.out.println(example.get());
}
```
@@ -1089,24 +1107,25 @@ public class AtomicExample {
public int get() {
return cnt.get();
}
+}
+```
- public static void main(String[] args) throws InterruptedException {
- final int threadSize = 1000;
- AtomicExample example = new AtomicExample();
- final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < threadSize; i++) {
- executorService.execute(() -> {
- example.add();
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- executorService.shutdown();
- System.out.println(example.get());
+```java
+public static void main(String[] args) throws InterruptedException {
+ final int threadSize = 1000;
+ AtomicExample example = new AtomicExample(); // 只修改这条语句
+ final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < threadSize; i++) {
+ executorService.execute(() -> {
+ example.add();
+ countDownLatch.countDown();
+ });
}
+ countDownLatch.await();
+ executorService.shutdown();
+ System.out.println(example.get());
}
-
```
```html
@@ -1126,22 +1145,24 @@ public class AtomicSynchronizedExample {
public synchronized int get() {
return cnt;
}
+}
+```
- public static void main(String[] args) throws InterruptedException {
- final int threadSize = 1000;
- AtomicSynchronizedExample example = new AtomicSynchronizedExample();
- final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < threadSize; i++) {
- executorService.execute(() -> {
- example.add();
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- executorService.shutdown();
- System.out.println(example.get());
+```java
+public static void main(String[] args) throws InterruptedException {
+ final int threadSize = 1000;
+ AtomicSynchronizedExample example = new AtomicSynchronizedExample();
+ final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ for (int i = 0; i < threadSize; i++) {
+ executorService.execute(() -> {
+ example.add();
+ countDownLatch.countDown();
+ });
}
+ countDownLatch.await();
+ executorService.shutdown();
+ System.out.println(example.get());
}
```
@@ -1155,9 +1176,7 @@ public class AtomicSynchronizedExample {
volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
-对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题。因为 volatile 并不能保证操作的原子性。
-
-// TODO:volatile 不能解决线程不安全问题的示例代码。
+对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。
### 3. 有序性
@@ -1404,7 +1423,7 @@ public final int getAndAddInt(Object var1, long var2, int var4) {
}
```
-ABA :如果一个变量 V 初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
+ABA :如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
### 3. 无同步方案
@@ -1432,14 +1451,16 @@ public class StackClosedExample {
}
System.out.println(cnt);
}
+}
+```
- public static void main(String[] args) {
- StackClosedExample example = new StackClosedExample();
- ExecutorService executorService = Executors.newCachedThreadPool();
- executorService.execute(() -> example.add100());
- executorService.execute(() -> example.add100());
- executorService.shutdown();
- }
+```java
+public static void main(String[] args) {
+ StackClosedExample example = new StackClosedExample();
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.execute(() -> example.add100());
+ executorService.execute(() -> example.add100());
+ executorService.shutdown();
}
```
@@ -1554,21 +1575,23 @@ ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因
# 十二、锁优化
-高效并发是从 JDK 1.5 到 JDK 1.6 的一个重要改进,HotSpot 虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。
+这里的锁优化主要是指虚拟机对 synchronized 的优化。
-## 自旋锁与自适应自旋
+## 自旋锁
-前面我们讨论互斥同步的时候,提到了互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
+互斥同步的进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
-自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时候很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是 10 次,用户可以使用参数 -XX:PreBlockSpin 来更改。
+自选锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。自旋次数的默认值是 10 次,用户可以使用虚拟机参数 -XX:PreBlockSpin 来更改。
-在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。
+在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
## 锁消除
-锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判定在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把他们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
+锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
-也许读者会有疑问,变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?答案是有许多同步措施并不是程序员自己加入的。同步的代码在 Java 程序中的普遍程度也许超过了大部分读者的想象。下面段非常简单的代码仅仅是输出 3 个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。
+锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们上的锁进行消除。
+
+对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:
```java
public static String concatString(String s1, String s2, String s3) {
@@ -1576,7 +1599,7 @@ public static String concatString(String s1, String s2, String s3) {
}
```
-我们也知道,由于 String 是一个不可变的类,对字符串的连接操作总是通过生成新的 String 对象来进行的,因此 Javac 编译器会对 String 连接做自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,即上面的代码可能会变成下面的样子:
+String 是一个不可变的类,Javac 编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,即上面的代码可能会变成下面的样子:
```java
public static String concatString(String s1, String s2, String s3) {
@@ -1587,46 +1610,47 @@ public static String concatString(String s1, String s2, String s3) {
return sb.toString();
}
```
-每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会 “逃逸” 到 concatString() 方法之外,其他线程无法访问到它。因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。
-## 锁粗化
+每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会“逃逸”到 concatString() 方法之外,其他线程无法访问到它。因此,虽然这里有锁,但是可以被安全地消除掉。
-原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小:只在共享数据的实际作用域中才进行同步。这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。
+## 锁粗化
-大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
+如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
-上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
+上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
## 轻量级锁
-轻量级锁是 JDK 1.6 之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
+JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
+
+以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 mark word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出,应该注意的是 state 表格不是存储在对象头中的。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
-要理解轻量级锁,以及后面会讲到的偏向锁的原理和运作过程,必须从 HotSpot 虚拟机的对象(对象头部分)的内存布局开始介绍。HotSpot 虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄(Generational GC Age)等,这部分数据是长度在 32 位和 64 位的虚拟机中分别为 32 bit 和 64 bit,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。
+
-简单地介绍了对象的内存布局后,我们把话题返回到轻量级锁的执行过程上。在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 “01” 状态)虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝(官方把这份拷贝加上了一个 Displaced 前缀,即 Displaced Mark Word)。然后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位(Mark Word 的最后 2bit)将转变为 “00”,即表示此对象处于轻量级锁定状态。
+下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。
-
+
-如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是的话只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,所标志的状态变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
+轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
-上面描述的是轻量级锁的加锁过程,它的解锁过程也是通过 CAS 操作来进行的,如果对象的 Mark Word 仍然指向着线程的锁记录,那就用 CAS 操作把对象当前的 Mark Word 和线程中复制的 Displaced Mark Word 替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要释放锁的同时,唤醒被挂起的线程。
+当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
-轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。
+
+
+如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
## 偏向锁
-偏向锁也是 JDK 1.6 中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不做了。
+偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
-偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
+可以使用 -XX:+UseBiasedLocking=true 开启偏向锁,不过在 JDK 1.6 中它是默认开启的。
-假设当前虚拟机启用了偏向锁(启用参数 -XX:+UseBiasedLocking,这是 JDK 1.6 的默认值),那么,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用 CAS 操作把获取到这个锁的线程 ID 记录在对象的 Mark Word 之中,如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行如何同步操作(例如 Locking、Unlocking 及对 Mark Word 的 Update 等)。
+当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
-当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行。偏向锁、轻量级锁的状态转换及对象 Mark Word 的关系如图 13-5 所示。
+当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
-偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。
-
# 十三、多线程开发良好的实践
- 给线程起个有意义的名字,这样可以方便找 Bug。
@@ -1659,3 +1683,4 @@ public static String concatString(String s1, String s2, String s3) {
- [Concurrent](https://sites.google.com/site/webdevelopart/21-compile/06-java/javase/concurrent?tmpl=%2Fsystem%2Fapp%2Ftemplates%2Fprint%2F&showPrintDialog=1)
- [JAVA FORK JOIN EXAMPLE](http://www.javacreed.com/java-fork-join-example/ "Java Fork Join Example")
- [聊聊并发(八)——Fork/Join 框架介绍](http://ifeve.com/talk-concurrency-forkjoin/)
+- [Eliminating SynchronizationRelated Atomic Operations with Biased Locking and Bulk Rebiasing](http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf)
diff --git "a/notes/Java \350\231\232\346\213\237\346\234\272.md" "b/notes/Java \350\231\232\346\213\237\346\234\272.md"
index 4b72b865bc918e58171a03e742c8f49b1936c830..6f157ce8931cf352272411db082370383b745519 100644
--- "a/notes/Java \350\231\232\346\213\237\346\234\272.md"
+++ "b/notes/Java \350\231\232\346\213\237\346\234\272.md"
@@ -12,7 +12,6 @@
* [垃圾收集算法](#垃圾收集算法)
* [垃圾收集器](#垃圾收集器)
* [内存分配与回收策略](#内存分配与回收策略)
- * [Full GC 的触发条件](#full-gc-的触发条件)
* [三、类加载机制](#三类加载机制)
* [类的生命周期](#类的生命周期)
* [类初始化时机](#类初始化时机)
@@ -47,8 +46,8 @@ java -Xss=512M HackTheJava
该区域可能抛出以下异常:
-1. 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
-2. 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
+- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
+- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
## 本地方法栈
@@ -90,7 +89,9 @@ java -Xms=1M -Xmx=2M HackTheJava
和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
-对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机把它当成永久代来进行垃圾回收。
+对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
+
+JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收,JDK 1.8 之后,取消了永久代,用 metaspace(元数据)区替代。
## 运行时常量池
@@ -110,39 +111,49 @@ Class 文件中的常量池(编译器生成的各种字面量和符号引用
## 判断一个对象是否可回收
-### 1. 引用计数
+### 1. 引用计数算法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
```java
-objA.instance = objB;
-objB.instance = objA;
+public class ReferenceCountingGC {
+ public Object instance = null;
+
+ public static void main(String[] args) {
+ ReferenceCountingGC objectA = new ReferenceCountingGC();
+ ReferenceCountingGC objectB = new ReferenceCountingGC();
+ objectA.instance = objectB;
+ objectB.instance = objectA;
+ }
+}
```
-### 2. 可达性
+正因为循环引用的存在,因此 Java 虚拟机不适用引用计数算法。
-通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是可用的,不可达的对象可被回收。
+### 2. 可达性分析算法
+
+通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
-GC Roots 一般包含以下内容:
+Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
-1. 虚拟机栈中引用的对象
-2. 方法区中类静态属性引用的对象
-3. 方法区中的常量引用的对象
-4. 本地方法栈中引用的对象
+- 虚拟机栈中引用的对象
+- 本地方法栈中引用的对象
+- 方法区中类静态属性引用的对象
+- 方法区中的常量引用的对象
### 3. 引用类型
-无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与引用有关。
+无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否可被回收都与引用有关。
-Java 对引用的概念进行了扩充,引入四种强度不同的引用类型。
+Java 具有四种强度不同的引用类型。
**(一)强引用**
-只要强引用存在,垃圾回收器永远不会回收被引用的对象。
+被强引用关联的对象不会被垃圾收集器回收。
使用 new 一个新对象的方式来创建强引用。
@@ -152,28 +163,69 @@ Object obj = new Object();
**(二)软引用**
-用来描述一些还有用但是并非必需的对象。
-
-在系统将要发生内存溢出异常之前,会将这些对象列进回收范围之中进行第二次回收。
-
-软引用主要用来实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源获取数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源获取这些数据。
+被软引用关联的对象,只有在内存不够的情况下才会被回收。
-使用 SoftReference 类来实现软引用。
+使用 SoftReference 类来创建软引用。
```java
Object obj = new Object();
SoftReference