Merge pull request #1 from CyC2018/master

update
This commit is contained in:
igofreely 2018-09-03 08:29:21 +08:00 committed by GitHub
commit 8cabe10165
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 5287 additions and 4039 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
.DS_Store
*.txt

View File

@ -54,7 +54,7 @@
- [STL 源码剖析](https://book.douban.com/subject/1110934/)
- [深度探索 C++ 对象模型](https://book.douban.com/subject/1091086/)
# 网站架构/分布式
# 系统设计
- [大规模分布式存储系统](https://book.douban.com/subject/25723658/)
- [从 Paxos 到 Zookeeper](https://book.douban.com/subject/26292004/)
@ -79,4 +79,4 @@
- [JavaScript 语言精粹](https://book.douban.com/subject/3590768/)
- [利用 Python 进行数据分析](https://book.douban.com/subject/25779298/)
- [概率论与数理统计](https://book.douban.com/subject/2201479/)
- [概率论与数理统计](https://book.douban.com/subject/2201479/)

196
README.md
View File

@ -1,197 +1,211 @@
| | Ⅱ | Ⅲ | Ⅳ | | Ⅵ | Ⅶ | Ⅷ | Ⅸ | |
| :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:|
| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 面向对象[:couple:](#面向对象-couple) |数据库[:floppy_disk:](#数据库-floppy_disk)| Java [:coffee:](#java-coffee)| 系统设计[:bulb:](#系统设计-bulb)| 工具[:hammer:](#工具-hammer)| 编码实践[:speak_no_evil:](#编码实践-speak_no_evil)| 后记[:memo:](#后记-memo) |
| 算法[:pencil2:](#pencil2-算法) | 操作系统[:computer:](#computer-操作系统)|网络[:cloud:](#cloud-网络) | 面向对象[:couple:](#couple-面向对象) |数据库[:floppy_disk:](#floppy_disk-数据库)| Java [:coffee:](#coffee-java)| 系统设计[:bulb:](#bulb-系统设计)| 工具[:hammer:](#hammer-工具)| 编码实践[:speak_no_evil:](#speak_no_evil-编码实践)| 后记[:memo:](#memo-后记) |
<br>
<div align="center">
<img src="other/LogoMakr_0zpEzN.png" width="150px">
<br>
<a href="https://gitter.im/CyC2018-Interview-Notebook/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link"> <img src="https://img.shields.io/badge/>-gitter-4ab8a1.svg"></a> <a href="https://legacy.gitbook.com/book/cyc2018/interview-notebook/details"> <img src="https://img.shields.io/badge/_-gitbook-4ab8a1.svg"></a>
<a href="other/Group.md"> <img src="https://img.shields.io/badge/>-group-4ab8a1.svg"></a> <a href="https://legacy.gitbook.com/book/cyc2018/interview-notebook/details"> <img src="https://img.shields.io/badge/_-gitbook-4ab8a1.svg"></a>
</div>
<!-- [![](https://img.shields.io/badge/>-gitter-blue.svg)](https://gitter.im/CyC2018-Interview-Notebook/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link) [![](https://img.shields.io/badge/_-gitbook-4ab8a1.svg)](https://legacy.gitbook.com/book/cyc2018/interview-notebook/details) -->
## 算法 :pencil2:
### :pencil2: 算法
> [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md)
- [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md)
目录根据原书第二版进行编排。
目录根据原书第二版进行编排,代码和原书有所不同,尽量比原书更简洁
> [Leetcode 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode%20题解.md)
- [Leetcode 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode%20题解.md)
做了一个大致分类,并对每种分类题型的解题思路做了总结。
对题目做了一个大致分类,并对每种题型的解题思路做了总结。
- [算法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/算法.md)
> [算法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/算法.md)
排序、并查集、栈和队列、红黑树、散列表。
主要参考 Robert Sedgewick 的算法书进行实现,源代码以及测试代码可在另一个仓库获取。
### :computer: 操作系统
## 操作系统 :computer:
- [计算机操作系统](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机操作系统.md)
> [计算机操作系统](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机操作系统.md)
进程管理、内存管理、设备管理、链接。
参考 现代操作系统、Unix 环境高级编程、深入理解计算机系统。
- [Linux](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Linux.md)
> [Linux](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Linux.md)
基本实现原理以及基本操作。
参考 鸟哥的 Linux 私房菜。
### :cloud: 网络
## 网络 :cloud:
- [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md)
> [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md)
物理层、链路层、网络层、运输层、应用层。
参考 谢希仁的计算机网络、计算机网络 自顶向下方法、TCP/IP 详解。
- [HTTP](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/HTTP.md)
> [HTTP](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/HTTP.md)
方法、状态码、Cookie、缓存、连接管理、HTTPs、HTTP 2.0。
参考 图解 HTTP更多的是参考网上的文档比如 MDN、维基百科等。
- [Socket](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Socket.md)
> [Socket](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Socket.md)
I/O 模型、I/O 多路复用。
参考 Unix 网络编程。
### :couple: 面向对象
## 面向对象 :couple:
- [设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/设计模式.md)
> [设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/设计模式.md)
实现了 Gof 的 23 种设计模式。
参考 Head First 设计模式、设计模式 可复用面向对象软件的基础,实现了 Gof 的 23 种设计模式。
- [面向对象思想](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/面向对象思想.md)
> [面向对象思想](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/面向对象思想.md)
三大原则(继承、封装、多态)、类图、设计原则。
内容包括三大原则(继承、封装、多态)、类图、设计原则。
### :floppy_disk: 数据库
## 数据库 :floppy_disk:
- [数据库系统原理](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/数据库系统原理.md)
> [数据库系统原理](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/数据库系统原理.md)
事务、锁、隔离级别、MVCC、间隙锁、范式。
参考 数据库系统原理。
- [SQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/SQL.md)
> [SQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/SQL.md)
SQL 基本语法。
参考 SQL 必知必会。
- [Leetcode-Database 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode-Database%20题解.md)
> [Leetcode-Database 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode-Database%20题解.md)
Leetcode 上数据库题目的解题记录。
Leetcode 上数据库题目的解题记录。
- [MySQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/MySQL.md)
> [MySQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/MySQL.md)
存储引擎、索引、查询优化、切分、复制。
参考 高性能 MySQL。
- [Redis](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Redis.md)
> [Redis](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Redis.md)
五种数据类型、字典和跳跃表数据结构、使用场景、和 Memcache 的比较、淘汰策略、持久化、文件事件的 Reactor 模式、复制。
参考 Redis 设计与实现、Redis 实战。
### :coffee: Java
## 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)
不会涉及很多基本语法介绍,主要是一些实现原理以及关键特性。
参考 Effective Java、Java 编程思想,也有部分内容参考官方文档以及 StackOverflow。
- [Java 容器](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20容器.md)
> [Java 容器](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20容器.md)
源码分析ArrayList、Vector、CopyOnWriteArrayList、LinkedList、HashMap、ConcurrentHashMap、LinkedHashMap、WeekHashMap。
包含容器源码分析。
- [Java 并发](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20并发.md)
> [Java 并发](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20并发.md)
线程使用方式、两种互斥同步方法、线程协作、JUC、线程安全、内存模型、锁优化。
参考 Java 编程思想、深入理解 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)
运行时数据区域、垃圾收集、类加载。
参考 深入理解 Java 虚拟机。
- [Java I/O](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20IO.md)
> [Java I/O](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20IO.md)
NIO 的原理以及实例。
包含 NIO 的原理以及实例。
### :bulb: 系统设计
## 系统设计 :bulb:
- [系统设计基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/系统设计基础.md)
> [分布式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式.md)
性能、伸缩性、扩展性、可用性、安全性
分布式锁、分布式事务、CAP、BASE、Paxos、Raft
- [分布式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式.md)
> [集群](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/集群.md)
分布式锁、分布式事务、CAP、BASE、Paxos、Raft
负载均衡、Session 管理
- [集群](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/集群.md)
> [安全性](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/安全性.md)
负载均衡、Session 管理
XSS、CSRF、SQL 注入、DDos
- [攻击技术](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/攻击技术.md)
> [消息队列](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/消息队列.md)
XSS、CSRF、SQL 注入、DDoS
消息处理模型、使用场景、可靠性
- [缓存](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/缓存.md)
## 工具 :hammer:
缓存特征、缓存位置、缓存问题、数据分布、一致性哈希、LRU、CDN
> [Git](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Git.md)
- [消息队列](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/消息队列.md)
一些 Git 的使用和概念。
消息处理模型、使用场景、可靠性
> [正则表达式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/正则表达式.md)
### :hammer: 工具
参考 正则表达式必知必会。
- [Git](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Git.md)
## 编码实践 :speak_no_evil:
一些 Git 的使用和概念。
> [重构](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/重构.md)
- [Docker](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Docker.md)
参考 重构 改善既有代码的设计
Docker 基本原理
> [代码可读性](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/代码可读性.md)
- [正则表达式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/正则表达式.md)
参考 编写可读代码的艺术
正则表达式基本语法
> [代码风格规范](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/代码风格规范.md)
- [构建工具](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/构建工具.md)
Google 开源项目的代码风格规范
构建工具的基本概念、主流构建工具介绍
## 后记 :memo:
### :speak_no_evil: 编码实践
**About**
- [重构](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/重构.md)
这个仓库是笔者的一个学习笔记,主要总结一些比较重要的知识点,希望对大家有所帮助
参考 重构 改善既有代码的设计
笔记不是从网上到处复制粘贴拼凑而来,虽然有少部分内容会直接引入书上原文或者官方技术文档的原文,但是没有直接摘抄其他人的博客文章,只做了参考,参考的文章会在最后给出链接。
- [代码可读性](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/代码可读性.md)
[BOOKLIST](https://github.com/CyC2018/Interview-Notebook/blob/master/BOOKLIST.md),这个书单是笔者至今看的一些比较好的技术书籍,虽然没有全都看完,但每本书多多少少都看了一部分
参考 编写可读代码的艺术
**How To Contribute**
- [代码风格规范](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/代码风格规范.md)
笔记内容是笔者一个字一个字打上去的,难免会有一些笔误,如果发现笔误可直接在相应文档进行编辑修改
Google 开源项目的代码风格规范
欢迎提交对本仓库的改进建议~
### :memo: 后记
**Authorization**
#### About
虽然没有加开源协议,但是允许非商业性使用
本仓库主要是根据计算机经典书籍以及官方技术文档进行总结的学习笔记,希望对大家有所帮助
转载使用请注明出处,谢谢!
学习笔记不是从网上到处拼凑而来,除了少部分引用书上和技术文档的原文,其余都是笔者的原创。在您引用本仓库内容或者对内容进行修改演绎时,请遵循文末的开源协议,谢谢。
**Uploading**
#### BookList
笔者在本地使用为知笔记软件进行书写,为了方便将本地笔记内容上传到 Github 上实现了一整套自动化上传方案包括文本文件的导出、提取图片、Markdown 文档转换、Git 同步
本仓库参考的书目:[BOOKLIST](https://github.com/CyC2018/Interview-Notebook/blob/master/BOOKLIST.md)
进行 Markdown 文档转换是因为 Github 使用的 GFM 不支持 MathJax 公式和 TOC 标记,所以需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。
#### How To Contribute
这里提供了笔者实现的 GFM 文档转换工具的链接:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)
笔记内容是笔者一个字一个字打上去的,难免会有一些笔误,如果发现笔误可直接对相应文档进行编辑修改
**Typesetting**
如果想要提交一个仓库现在还没有的全新内容,可以先将相应的文档放到 other 目录下。
欢迎在 Issue 中提交对本仓库的改进建议~
#### Typesetting
笔记内容按照 [中文文案排版指北](http://mazhuang.org/wiki/chinese-copywriting-guidelines/) 进行排版,以保证内容的可读性。
笔记不使用 `![]()` 这种方式来引用图片,而是用 `<img>` 标签。一方面是为了能够控制图片以合适的大小显示,另一方面是因为 GFM 不支持 `<center> ![]() </center>` 让图片居中显示,只能使用 `<div align="center"> <img src=""/> </div>` 达到居中的效果。
这里提供了笔者实现的中英混排文档在线排版工具的链接[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting)。
笔者将自己实现的文档排版功能提取出来,放在 Github Page 中,无需下载安装即可免费使用[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting)。
**Statement**
#### Uploading
笔者在本地使用为知笔记软件进行书写,为了方便将本地笔记内容上传到 Github 上实现了一整套自动化上传方案包括文本文件的导出、提取图片、Markdown 文档转换、Git 同步。
进行 Markdown 文档转换是因为 Github 使用的 GFM 不支持 MathJax 公式和 TOC 标记,所以需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。
笔者将自己实现文档转换功能提取出来,方便大家在需要将本地 Markdown 上传到 Github或者制作项目 README 文档时生成目录时使用:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。
#### Statement
本仓库不参与商业行为,不向读者收取任何费用。(This repository is not engaging in business activities, and does not charge readers any fee.)
**Logo**
#### Logo
Power by [logomakr](https://logomakr.com/).
**Acknowledgements**
#### Acknowledgements
感谢以下人员对本仓库做出的贡献,当然不仅仅只有这些贡献者,这里就不一一列举了。如果你希望被添加到这个名单中,并且提交过 Issue 或者 PR请与笔者联系。
@ -217,5 +231,9 @@ Power by [logomakr](https://logomakr.com/).
<img src="other/10072416.jpg" width="50px">
</a>
#### License
在对本作品进行演绎时,请署名并以相同方式共享。
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" /></a>

View File

@ -1,3 +1,5 @@
This file used to generate gitbook catalogue.
# Summary
* 算法
@ -32,5 +34,3 @@

View File

@ -0,0 +1,16 @@
<!-- GFM-TOC -->
* [0. 进程内存空间中,堆和栈的区别](#0-进程内存空间中,堆和栈的区别)
<!-- GFM-TOC -->
# 0. 进程内存空间中,堆和栈的区别
> C++
动态、malloc()、new、链式分配、向上生长函数调用、编译器分配回收、向下生长。
https://www.cnblogs.com/sunziying/p/6510030.html
By @CyC
---

91
notes/Docker.md Normal file
View File

@ -0,0 +1,91 @@
<!-- GFM-TOC -->
* [一、解决的问题](#一解决的问题)
* [二、与虚拟机的比较](#二与虚拟机的比较)
* [三、优势](#三优势)
* [四、使用场景](#四使用场景)
* [五、镜像与容器](#五镜像与容器)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
<div align="center"> <img src="../pics//011f3ef6-d824-4d43-8b2c-36dab8eaaa72-1.png"/> </div><br>
# 一、解决的问题
由于不同的机器有不同的操作系统,以及不同的库和组件,在将一个应用部署到多台机器上需要进行大量的环境配置操作。
Docker 主要解决环境配置问题,它是一种虚拟化技术,对进程进行隔离,被隔离的进程独立于宿主操作系统和其它隔离的进程。使用 Docker 可以不修改应用程序代码,不需要开发人员学习特定环境下的技术,就能够将现有的应用程序部署在其他机器中。
# 二、与虚拟机的比较
虚拟机也是一种虚拟化技术,它与 Docker 最大的区别在于它是通过模拟硬件,并在硬件上安装操作系统来实现。
<div align="center"> <img src="../pics//71f61bc3-582d-4c27-8bdd-dc7fb135bf8f.png"/> </div><br>
<div align="center"> <img src="../pics//7e873b60-44dc-4911-b080-defd5b8f0b49.png"/> </div><br>
## 启动速度
启动虚拟机需要启动虚拟机的操作系统,再启动应用,这个过程非常慢;
而启动 Docker 相当于启动宿主操作系统上的一个进程。
## 占用资源
虚拟机是一个完整的操作系统,需要占用大量的磁盘、内存和 CPU一台机器只能开启几十个的虚拟机。
而 Docker 只是一个进程,只需要将应用以及相关的组件打包,在运行时占用很少的资源,一台机器可以开启成千上万个 Docker。
# 三、优势
除了启动速度快以及占用资源少之外Docker 具有以下优势:
## 更容易迁移
提供一致性的运行环境,可以在不同的机器上进行迁移,而不用担心环境变化导致无法运行。
## 更容易维护
使用分层技术和镜像,使得应用可以更容易复用重复部分。复用程度越高,维护工作也越容易。
## 更容易扩展
可以使用基础镜像进一步扩展得到新的镜像,并且官方和开源社区提供了大量的镜像,通过扩展这些镜像可以非常容易得到我们想要的镜像。
# 四、使用场景
## 持续集成
持续集成指的是频繁地将代码集成到主干上,这样能够更快地发现错误。
Docker 具有轻量级以及隔离性的特点,在将代码集成到一个 Docker 中不会对其它 Docker 产生影响。
## 提供可伸缩的云服务
根据应用的负载情况,可以很容易地增加或者减少 Docker。
## 搭建微服务架构
Docker 轻量级的特点使得它很适合用于部署、维护、组合微服务。
# 五、镜像与容器
镜像是一种静态的结构,可以看成面向对象里面的类,而容器是镜像的一个实例。
镜像包含着容器运行时所需要的代码以及其它组件它是一种分层结构每一层都是只读的read-only layers。构建镜像时会一层一层构建前一层是后一层的基础。镜像的这种分层存储结构很适合镜像的复用以及定制。
构建容器时通过在镜像的基础上添加一个可写层writable layer用来保存着容器运行过程中的修改。
<div align="center"> <img src="../pics//docker-filesystems-busyboxrw.png"/> </div><br>
# 参考资料
- [DOCKER 101: INTRODUCTION TO DOCKER WEBINAR RECAP](https://blog.docker.com/2017/08/docker-101-introduction-docker-webinar-recap/)
- [Docker 入门教程](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html)
- [Docker container vs Virtual machine](http://www.bogotobogo.com/DevOps/Docker/Docker_Container_vs_Virtual_Machine.php)
- [How to Create Docker Container using Dockerfile](https://linoxide.com/linux-how-to/dockerfile-create-docker-container/)
- [理解 Docker2Docker 镜像](http://www.cnblogs.com/sammyliu/p/5877964.html)
- [为什么要使用 Docker](https://yeasy.gitbooks.io/docker_practice/introduction/why.html)
- [What is Docker](https://www.docker.com/what-docker)
- [持续集成是什么?](http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html)

View File

@ -1,8 +1,7 @@
<!-- GFM-TOC -->
* [学习资料](#学习资料)
* [集中式与分布式](#集中式与分布式)
* [Git 的中心服务器](#git-的中心服务器)
* [Git 工作流](#git-工作流)
* [中心服务器](#中心服务器)
* [工作流](#工作流)
* [分支实现](#分支实现)
* [冲突](#冲突)
* [Fast forward](#fast-forward)
@ -11,16 +10,10 @@
* [SSH 传输设置](#ssh-传输设置)
* [.gitignore 文件](#gitignore-文件)
* [Git 命令一览](#git-命令一览)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 学习资料
- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
- [Learn Git Branching](https://learngitbranching.js.org/)
# 集中式与分布式
Git 属于分布式版本控制系统,而 SVN 属于集中式。
@ -33,11 +26,13 @@ Git 属于分布式版本控制系统,而 SVN 属于集中式。
分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。
# Git 的中心服务器
# 中心服务器
Git 的中心服务器用来交换每个用户的修改。没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。Github 就是一种 Git 中心服务器。
中心服务器用来交换每个用户的修改,没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。
# Git 工作流
Github 就是一个中心服务器。
# 工作流
<div align="center"> <img src="../pics//a1198642-9159-4d88-8aec-c3b04e7a2563.jpg"/> </div><br>
@ -54,14 +49,14 @@ Git 版本库有一个称为 stage 的暂存区,还有自动创建的 master
<div align="center"> <img src="../pics//17976404-95f5-480e-9cb4-250e6aa1d55f.png"/> </div><br>
可以跳过暂存区域直接从分支中取出修改或者直接提交修改到分支中
可以跳过暂存区域直接从分支中取出修改或者直接提交修改到分支中
- git commit -a 直接把所有文件的修改添加到暂缓区然后执行提交
- git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作
# 分支实现
Git 把每次提交都连成一条时间线。分支使用指针来实现,例如 master 分支指针指向时间线的最后一个节点也就是最后一次提交。HEAD 指针指向的是当前分支
使用指针将每个提交连接成一条时间线HEAD 指针指向当前分支指针
<div align="center"> <img src="../pics//fb546e12-e1fb-4b72-a1fb-8a7f5000dce6.jpg"/> </div><br>
@ -69,7 +64,7 @@ Git 把每次提交都连成一条时间线。分支使用指针来实现,例
<div align="center"> <img src="../pics//bc775758-89ab-4805-9f9c-78b8739cf780.jpg"/> </div><br>
每次提交只会让当前分支向前移动,而其它分支不会移动。
每次提交只会让当前分支指针向前移动,而其它分支指针不会移动。
<div align="center"> <img src="../pics//5292faa6-0141-4638-bf0f-bb95b081dcba.jpg"/> </div><br>
@ -155,4 +150,9 @@ $ ssh-keygen -t rsa -C "youremail@example.com"
比较详细的地址http://www.cheat-sheets.org/saved-copy/git-cheat-sheet.pdf
# 参考资料
- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
- [Learn Git Branching](https://learngitbranching.js.org/)

View File

@ -25,6 +25,7 @@
* [实体首部字段](#实体首部字段)
* [五、具体应用](#五具体应用)
* [Cookie](#cookie)
* [6. Secure](#6-secure)
* [缓存](#缓存)
* [连接管理](#连接管理)
* [内容协商](#内容协商)
@ -45,7 +46,7 @@
* [二进制分帧层](#二进制分帧层)
* [服务端推送](#服务端推送)
* [首部压缩](#首部压缩)
* [八、GET 和 POST 的区别](#八get-和-post-的区别)
* [八、GET 和 POST 比较](#八get-和-post-比较)
* [作用](#作用)
* [参数](#参数)
* [安全](#安全)
@ -61,13 +62,13 @@
## URL
- URIUniform Resource Identifier统一资源标识符
- URLUniform Resource Locator统一资源定位符
- URNUniform Resource Name统一资源名称例如 urn:isbn:0-486-27557-4。
URI 包含 URL 和 URN目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。
<div align="center"> <img src="../pics//f716427a-94f2-4875-9c86-98793cf5dcc3.jpg" width="400"/> </div><br>
- URIUniform Resource Identifier统一资源标识符
- URLUniform Resource Locator统一资源定位符
- URNUniform Resource Name统一资源名称
<div align="center"> <img src="../pics//urlnuri.jpg" width="600"/> </div><br>
## 请求和响应报文
@ -197,7 +198,7 @@ CONNECT www.example.com:443 HTTP/1.1
- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
- **206 Partial Content** :表示客户端进行了范围请求响应报文包含由 Content-Range 指定范围的实体内容。
- **206 Partial Content** :表示客户端进行了范围请求响应报文包含由 Content-Range 指定范围的实体内容。
## 3XX 重定向
@ -219,7 +220,7 @@ CONNECT www.example.com:443 HTTP/1.1
- **401 Unauthorized** 该状态码表示发送的请求需要有认证信息BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
- **403 Forbidden** :请求被拒绝,服务器端没有必要给出拒绝的详细理由
- **403 Forbidden** :请求被拒绝。
- **404 Not Found**
@ -310,7 +311,7 @@ HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使
Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。
Cookie 曾一度用于客户端数据的存储因为当时并没有其它合适的存储办法而作为唯一的存储手段但现在随着现代浏览器开始支持各种各样的存储方式Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API (本地存储和会话存储)或 IndexedDB。
Cookie 曾一度用于客户端数据的存储因为当时并没有其它合适的存储办法而作为唯一的存储手段但现在随着现代浏览器开始支持各种各样的存储方式Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API本地存储和会话存储或 IndexedDB。
### 1. 用途
@ -331,7 +332,7 @@ Set-Cookie: tasty_cookie=strawberry
[page content]
```
客户端之后对同一个服务器发送请求时,会从浏览器中读出 Cookie 信息通过 Cookie 请求首部字段发送给服务器。
客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。
```html
GET /sample_page.html HTTP/1.1
@ -348,27 +349,7 @@ Cookie: yummy_cookie=choco; tasty_cookie=strawberry
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
```
### 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. 作用域
### 4. 作用域
Domain 标识指定了哪些主机可以接受 Cookie。如果不指定默认为当前文档的主机不包含子域名。如果指定了 Domain则一般包含子域名。例如如果设置 Domain=mozilla.org则 Cookie 也包含在子域名中(如 developer.mozilla.org
@ -378,19 +359,40 @@ Path 标识指定了主机下的哪些路径可以接受 Cookie该 URL 路径
- /docs/Web/
- /docs/Web/HTTP
### 5. JavaScript
通过 `Document.cookie` 属性可创建新的 Cookie也可通过该属性访问非 HttpOnly 标记的 Cookie。
```html
document.cookie = "yummy_cookie=choco";
document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
```
### 6. HttpOnly
标记为 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. Secure
标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性Secure 标记也无法提供确实的安全保障。
### 7. Session
除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。
Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在内存型数据库中,比如 Redis。
Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高
使用 Session 维护用户登录的过程如下:
使用 Session 维护用户登录状态的过程如下:
- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中;
- 服务器验证该用户名和密码;
- 如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 ID 称为 Session ID
- 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID
- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID客户端收到响应报文之后将该 Cookie 值存入浏览器中;
- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID从 Redis 中取出用户信息,继续之后的业务操作。
- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID从 Redis 中取出用户信息,继续之的业务操作。
应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下例如转账等操作除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。
@ -409,7 +411,7 @@ Session 可以存储在服务器上的文件、数据库或者内存中。也可
### 1. 优点
- 缓解服务器压力;
- 降低客户端获取资源的延迟(缓存资源比服务器上的资源离客户端更近)
- 降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存在地理位置上也有可能比源服务器来得近,例如浏览器缓存
### 2. 实现方法
@ -460,12 +462,15 @@ max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中
Cache-Control: max-age=31536000
```
Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。在 HTTP/1.1 中,会优先处理 Cache-Control : max-age 指令;而在 HTTP/1.0 中Cache-Control : max-age 指令会被忽略掉。
Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。
```html
Expires: Wed, 04 Jul 2012 08:26:05 GMT
```
- 在 HTTP/1.1 中,会优先处理 max-age 指令;
- 在 HTTP/1.0 中max-age 指令会被忽略掉。
### 4. 缓存验证
需要先了解 ETag 首部字段的含义它是资源的唯一标识。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一标识。
@ -496,13 +501,16 @@ If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
### 1. 短连接与长连接
当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信
当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大
从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 Connection : close而在 HTTP/1.1 之前默认是短连接的,如果需要长连接,则使用 Connection : Keep-Alive。
长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。
- 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 `Connection : close`
- 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 `Connection : Keep-Alive`
### 2. 流水线
默认情况下HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到应之后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。
默认情况下HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到应之后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。
流水线是在同一条长连接上发出连续的请求,而不用等待响应返回,这样可以避免连接延迟。
@ -512,17 +520,17 @@ If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
### 1. 类型
**(一)服务端驱动型内容协商**
**(一)服务端驱动型**
客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Languag服务器根据这些字段返回特定的资源。
它存在以下问题:
- 服务器很难知道客户端浏览器的全部信息;
- 客户端提供的信息相当冗长HTTP/2 协议的首部压缩机制缓解了这个问题并且存在隐私风险HTTP 指纹识别技术)
- 客户端提供的信息相当冗长HTTP/2 协议的首部压缩机制缓解了这个问题并且存在隐私风险HTTP 指纹识别技术)
- 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。
**(二)代理驱动型协商**
**(二)代理驱动型**
服务器返回 300 Multiple Choices 或者 406 Not Acceptable客户端从中选出最合适的那个资源。
@ -538,9 +546,11 @@ Vary: Accept-Language
## 内容编码
内容编码将实体主体进行压缩,从而减少传输的数据量。常用的内容编码有gzip、compress、deflate、identity。
内容编码将实体主体进行压缩,从而减少传输的数据量。
浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级,服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的在响应中Vary 首部中至少要包含 Content-Encoding这样的话缓存服务器就可以对资源的不同展现形式进行缓存。
常用的内容编码有gzip、compress、deflate、identity。
浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级。服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应的 Vary 首部至少要包含 Content-Encoding。
## 范围请求
@ -622,10 +632,14 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,
- 网络访问控制
- 访问日志记录
代理服务器分为正向代理和反向代理两种,用户察觉得到正向代理的存在;而反向代理一般位于内部网络中,用户察觉不到。
代理服务器分为正向代理和反向代理两种:
- 用户察觉得到正向代理的存在。
<div align="center"> <img src="../pics//a314bb79-5b18-4e63-a976-3448bffa6f1b.png" width=""/> </div><br>
- 而反向代理一般位于内部网络中,用户察觉不到。
<div align="center"> <img src="../pics//2d09a847-b854-439c-9198-b29c65810944.png" width=""/> </div><br>
### 2. 网关
@ -634,7 +648,7 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,
### 3. 隧道
使用 SSL 等加密手段,客户端和服务器之间建立一条安全的通信线路。
使用 SSL 等加密手段,客户端和服务器之间建立一条安全的通信线路。
# 六、HTTPs
@ -644,7 +658,7 @@ HTTP 有以下安全性问题:
- 不验证通信方的身份,通信方的身份有可能遭遇伪装;
- 无法证明报文的完整性,报文有可能遭篡改。
HTTPs 并不是新协议,而是让 HTTP 先和 SSLSecure Sockets Layer通信再由 SSL 和 TCP 通信也就是说 HTTPs 使用了隧道进行通信。
HTTPs 并不是新协议,而是让 HTTP 先和 SSLSecure Sockets Layer通信再由 SSL 和 TCP 通信也就是说 HTTPs 使用了隧道进行通信。
通过使用 SSLHTTPs 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。
@ -676,7 +690,7 @@ HTTPs 并不是新协议,而是让 HTTP 先和 SSLSecure Sockets Layer
### 3. HTTPs 采用的加密方式
HTTPs 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证安全性,之后使用对称密钥加密进行通信来保证效率。(下图中的 Session Key 就是对称密钥)
HTTPs 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。(下图中的 Session Key 就是对称密钥)
<div align="center"> <img src="../pics//How-HTTPS-Works.png" width="600"/> </div><br>
@ -705,7 +719,7 @@ HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认
## HTTPs 的缺点
- 因为需要进行加密解密等过程,因此速度会更慢;
- 需要支付证书授权的高费用。
- 需要支付证书授权的高费用。
## 配置 HTTPs
@ -715,7 +729,7 @@ HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认
## HTTP/1.x 缺陷
HTTP/1.x 实现简单是以牺牲应用性能为代价的:
HTTP/1.x 实现简单是以牺牲性能为代价的:
- 客户端需要使用多个连接才能实现并发和缩短延迟;
- 不会压缩请求和响应首部,从而导致不必要的网络流量;
@ -727,7 +741,11 @@ HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式
<div align="center"> <img src="../pics//86e6a91d-a285-447a-9345-c5484b8d0c47.png" width="400"/> </div><br>
在通信过程中,只会有一个 TCP 连接存在它承载了任意数量的双向数据流Stream。一个数据流都有一个唯一标识符和可选的优先级信息用于承载双向信息。消息Message是与逻辑请求或响应消息对应的完整的一系列帧。帧Fram是最小的通信单位来自不同数据流的帧可以交错发送然后再根据每个帧头的数据流标识符重新组装。
在通信过程中,只会有一个 TCP 连接存在它承载了任意数量的双向数据流Stream
- 一个数据流Stream都有一个唯一标识符和可选的优先级信息用于承载双向信息。
- 消息Message是与逻辑请求或响应对应的完整的一系列帧。
- 帧Frame是最小的通信单位来自不同数据流的帧可以交错发送然后再根据每个帧头的数据流标识符重新组装。
<div align="center"> <img src="../pics//af198da1-2480-4043-b07f-a3b91a88b815.png" width="600"/> </div><br>
@ -739,11 +757,15 @@ HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送
## 首部压缩
HTTP/1.1 的首部带有大量信息而且每次都要重复发送。HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表从而避免了重复传输。不仅如此HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。
HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。
HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。
不仅如此HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。
<div align="center"> <img src="../pics//_u4E0B_u8F7D.png" width="600"/> </div><br>
# 八、GET 和 POST 的区别
# 八、GET 和 POST 比较
## 作用
@ -751,7 +773,9 @@ GET 用于获取资源,而 POST 用于传输实体主体。
## 参数
GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。
GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高因为照样可以通过一些抓包工具Fiddler查看。
因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 `中文` 会转换为 `%E4%B8%AD%E6%96%87`,而空格会转换为 `%20`。POST 参考支持标准字符集。
```
GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
@ -763,10 +787,6 @@ Host: w3schools.com
name1=value1&name2=value2
```
不能因为 POST 参数存储在实体主体中就认为它的安全性更高因为照样可以通过一些抓包工具Fiddler查看。
因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码,例如`中文`会转换为`%E4%B8%AD%E6%96%87`,而空格会转换为`%20`。POST 支持标准字符集。
## 安全
安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。
@ -779,9 +799,13 @@ GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实
## 幂等性
幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。在正确实现的条件下GETHEADPUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。所有的安全方法也都是幂等的。
幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。
GET /pageX HTTP/1.1 是幂等的。连续调用多次,客户端接收到的结果都是一样的:
所有的安全方法也都是幂等的。
在正确实现的条件下GETHEADPUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。
GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的:
```
GET /pageX HTTP/1.1
@ -790,7 +814,7 @@ GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
```
POST /add_row HTTP/1.1 不是幂等的如果调用多次,就会增加多行记录:
POST /add_row HTTP/1.1 不是幂等的如果调用多次,就会增加多行记录:
```
POST /add_row HTTP/1.1 -> Adds a 1nd row
@ -820,7 +844,8 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404
> XMLHttpRequest 是一个 API它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。
在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做例如火狐就不会。而 GET 方法 Header 和 Data 会一起发送。
- 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做例如火狐就不会。
- 而 GET 方法 Header 和 Data 会一起发送。
# 九、HTTP/1.0 与 HTTP/1.1 的区别
@ -847,6 +872,7 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404
- [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)
- [Difference between file URI and URL in java](http://java2db.com/java-io/how-to-get-and-the-difference-between-file-uri-and-url-in-java)
- [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/)

View File

@ -2,8 +2,17 @@
* [一、概览](#一概览)
* [二、磁盘操作](#二磁盘操作)
* [三、字节操作](#三字节操作)
* [实现文件复制](#实现文件复制)
* [装饰者模式](#装饰者模式)
* [四、字符操作](#四字符操作)
* [编码与解码](#编码与解码)
* [String 的编码方式](#string-的编码方式)
* [Reader 与 Writer](#reader-与-writer)
* [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容)
* [五、对象操作](#五对象操作)
* [序列化](#序列化)
* [Serializable](#serializable)
* [transient](#transient)
* [六、网络操作](#六网络操作)
* [InetAddress](#inetaddress)
* [URL](#url)
@ -37,11 +46,10 @@ Java 的 I/O 大概可以分成以下几类:
File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。
递归地出一个目录下所有文件:
递归地出一个目录下所有文件:
```java
public static void listAllFiles(File dir)
{
public static void listAllFiles(File dir) {
if (dir == null || !dir.exists()) {
return;
}
@ -57,28 +65,36 @@ public static void listAllFiles(File dir)
# 三、字节操作
使用字节流操作进行文件复制:
## 实现文件复制
```java
public static void copyFile(String src, String dist) throws IOException
{
public static void copyFile(String src, String dist) throws IOException {
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dist);
byte[] buffer = new byte[20 * 1024];
// read() 最多读取 buffer.length 个字节
// 返回的是实际读取的个数
// 返回 -1 的时候表示读到 eof即文件尾
while (in.read(buffer, 0, buffer.length) != -1) {
out.write(buffer);
}
in.close();
out.close();
}
```
<div align="center"> <img src="../pics//DP-Decorator-java.io.png" width="500"/> </div><br>
## 装饰者模式
Java I/O 使用了装饰者模式来实现。以 InputStream 为例InputStream 是抽象组件FileInputStream 是 InputStream 的子类属于具体组件提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
- InputStream 是抽象组件;
- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
<div align="center"> <img src="../pics//DP-Decorator-java.io.png" width="500"/> </div><br>
实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
@ -91,28 +107,7 @@ DataInputStream 装饰者提供了对更多数据类型进行输入的操作,
# 四、字符操作
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
- InputStreamReader 实现从字节流解码成字符流;
- OutputStreamWriter 实现字符流编码成为字节流。
逐行输出文本文件的内容:
```java
public static void readFileContent(String filePath) throws IOException
{
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
// 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
// 在调用 BufferedReader 的 close() 方法时会去调用 fileReader 的 close() 方法
// 因此只要一个 close() 调用即可
bufferedReader.close();
}
```
## 编码与解码
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
@ -126,7 +121,9 @@ UTF-16be 中的 be 指的是 Big Endian也就是大端。相应地也有 UTF-
Java 使用双字节编码 UTF-16be这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位也就是两个字节Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。
String 可以看成一个字符序列,可以指定一个编码方式将它转换为字节序列,也可以指定一个编码方式将一个字节序列转换为 String。
## String 的编码方式
String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。
```java
String str1 = "中文";
@ -141,18 +138,50 @@ System.out.println(str2);
byte[] bytes = str1.getBytes();
```
## Reader 与 Writer
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
- InputStreamReader 实现从字节流解码成字符流;
- OutputStreamWriter 实现字符流编码成为字节流。
## 实现逐行输出文本文件的内容
```java
public static void readFileContent(String filePath) throws IOException {
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
// 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
// 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
// 因此只要一个 close() 调用即可
bufferedReader.close();
}
```
# 五、对象操作
## 序列化
序列化就是将一个对象转换成字节序列,方便存储和传输。
- 序列化ObjectOutputStream.writeObject()
- 反序列化ObjectInputStream.readObject()
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
## Serializable
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。
```java
public static void main(String[] args) throws IOException, ClassNotFoundException
{
public static void main(String[] args) throws IOException, ClassNotFoundException {
A a1 = new A(123, "abc");
String objectFile = "file/a1";
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
@ -165,30 +194,27 @@ public static void main(String[] args) throws IOException, ClassNotFoundExceptio
System.out.println(a2);
}
private static class A implements Serializable
{
private static class A implements Serializable {
private int x;
private String y;
A(int x, String y)
{
A(int x, String y) {
this.x = x;
this.y = y;
}
@Override
public String toString()
{
public String toString() {
return "x = " + x + " " + "y = " + y;
}
}
```
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
## transient
transient 关键字可以使一些属性不会被序列化。
**ArrayList 序列化和反序列化的实现** ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
```java
private transient Object[] elementData;
@ -205,7 +231,7 @@ Java 中的网络支持:
## InetAddress
没有公有构造函数,只能通过静态方法来创建实例。
没有公有构造函数,只能通过静态方法来创建实例。
```java
InetAddress.getByName(String host);
@ -217,19 +243,24 @@ InetAddress.getByAddress(byte[] address);
可以直接从 URL 中读取字节流数据。
```java
public static void main(String[] args) throws IOException
{
public static void main(String[] args) throws IOException {
URL url = new URL("http://www.baidu.com");
// 字节流
/* 字节流 */
InputStream is = url.openStream();
// 字符流
/* 字符流 */
InputStreamReader isr = new InputStreamReader(is, "utf-8");
/* 提供缓存功能 */
BufferedReader br = new BufferedReader(isr);
String line = br.readLine();
while (line != null) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
line = br.readLine();
}
br.close();
}
```
@ -244,8 +275,8 @@ public static void main(String[] args) throws IOException
## Datagram
- DatagramPacket数据包类
- DatagramSocket通信类
- DatagramPacket数据包类
# 七、NIO
@ -253,7 +284,7 @@ public static void main(String[] args) throws IOException
- [Java NIO 浅析](https://tech.meituan.com/nio.html)
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
## 流与块
@ -271,7 +302,7 @@ I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
通道包括以下类型:
@ -329,21 +360,41 @@ I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重
以下展示了使用 NIO 快速复制文件的实例:
```java
public static void fastCopy(String src, String dist) throws IOException
{
FileInputStream fin = new FileInputStream(src); /* 获取源文件的输入字节流 */
FileChannel fcin = fin.getChannel(); /* 获取输入字节流的文件通道 */
FileOutputStream fout = new FileOutputStream(dist); /* 获取目标文件的输出字节流 */
FileChannel fcout = fout.getChannel(); /* 获取输出字节流的通道 */
ByteBuffer buffer = ByteBuffer.allocateDirect(1024); /* 为缓冲区分配 1024 个字节 */
public static void fastCopy(String src, String dist) throws IOException {
/* 获得源文件的输入字节流 */
FileInputStream fin = new FileInputStream(src);
/* 获取输入字节流的文件通道 */
FileChannel fcin = fin.getChannel();
/* 获取目标文件的输出字节流 */
FileOutputStream fout = new FileOutputStream(dist);
/* 获取输出字节流的文件通道 */
FileChannel fcout = fout.getChannel();
/* 为缓冲区分配 1024 个字节 */
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
int r = fcin.read(buffer); /* 从输入通道中读取数据到缓冲区中 */
if (r == -1) { /* read() 返回 -1 表示 EOF */
/* 从输入通道中读取数据到缓冲区中 */
int r = fcin.read(buffer);
/* read() 返回 -1 表示 EOF */
if (r == -1) {
break;
}
buffer.flip(); /* 切换读写 */
fcout.write(buffer); /* 把缓冲区的内容写入输出文件中 */
buffer.clear(); /* 清空缓冲区 */
/* 切换读写 */
buffer.flip();
/* 把缓冲区的内容写入输出文件中 */
fcout.write(buffer);
/* 清空缓冲区 */
buffer.clear();
}
}
```
@ -356,7 +407,7 @@ NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用
通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel找到 IO 事件已经到达的 Channel 执行。
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件具有更好的性能。
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。
@ -448,10 +499,10 @@ while (true) {
## 套接字 NIO 实例
```java
public class NIOServer
{
public static void main(String[] args) throws IOException
{
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
@ -463,32 +514,45 @@ public class NIOServer
serverSocket.bind(address);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> 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);
StringBuilder data = new StringBuilder();
while (true) {
buffer.clear();
int n = sChannel.read(buffer);
if (n == -1) {
@ -509,10 +573,9 @@ public class NIOServer
```
```java
public class NIOClient
{
public static void main(String[] args) throws IOException
{
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";
@ -526,9 +589,9 @@ public class NIOClient
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 1024 个字节映射到内存中map() 方法返回一个 MappedByteBuffer它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
下面代码行将文件的前 1024 个字节映射到内存中map() 方法返回一个 MappedByteBuffer它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
```java
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
@ -538,8 +601,8 @@ MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
NIO 与普通 I/O 的区别主要有以下两点:
- NIO 是非阻塞的
- NIO 面向块I/O 面向流
- NIO 是非阻塞的
- NIO 面向块I/O 面向流
# 八、参考资料

View File

@ -4,9 +4,10 @@
* [缓存池](#缓存池)
* [二、String](#二string)
* [概览](#概览)
* [String 不可变的好处](#string-不可变的好处)
* [不可变的好处](#不可变的好处)
* [String, StringBuffer and StringBuilder](#string,-stringbuffer-and-stringbuilder)
* [String.intern()](#stringintern)
* [String Pool](#string-pool)
* [new String("abc")](#new-string"abc")
* [三、运算](#三运算)
* [参数传递](#参数传递)
* [float 与 double](#float-与-double)
@ -62,7 +63,10 @@ int y = x; // 拆箱
## 缓存池
new Integer(123) 与 Integer.valueOf(123) 的区别在于new Integer(123) 每次都会新建一个对象,而 Integer.valueOf(123) 可能会使用缓存对象,因此多次使用 Integer.valueOf(123) 会取得同一个对象的引用。
new Integer(123) 与 Integer.valueOf(123) 的区别在于:
- new Integer(123) 每次都会新建一个对象;
- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
```java
Integer x = new Integer(123);
@ -73,14 +77,6 @@ Integer k = Integer.valueOf(123);
System.out.println(z == k); // true
```
编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
```java
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true
```
valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。
```java
@ -125,7 +121,15 @@ static {
}
```
Java 还将一些其它基本类型的值放在缓冲池中,包含以下这些:
编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
```java
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true
```
基本类型对应的缓冲池如下:
- boolean values true and false
- all byte values
@ -133,7 +137,7 @@ Java 还将一些其它基本类型的值放在缓冲池中,包含以下这些
- int values between -128 and 127
- char in the range \u0000 to \u007F
因此在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
[StackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123
](https://stackoverflow.com/questions/9030817/differences-between-new-integer123-integer-valueof123-and-just-123)
@ -153,7 +157,7 @@ public final class String
private final char value[];
```
## String 不可变的好处
## 不可变的好处
**1. 可以缓存 hash 值**
@ -186,44 +190,101 @@ String 不可变性天生具备线程安全,可以在多个线程中安全地
- String 不可变,因此是线程安全的
- StringBuilder 不是线程安全的
- StringBuffer 是线程安全的,内部使用 synchronized 同步
- StringBuffer 是线程安全的,内部使用 synchronized 进行同步
[StackOverflow : String, StringBuffer, and StringBuilder](https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder)
## String.intern()
## String Pool
使用 String.intern() 可以保证相同内容的字符串变量引用相同的内存对象
字符串常量池String Poll保存着所有字符串字面量literal strings这些字面量在编译时期就确定。不仅如此还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Poll 中
下面示例中s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用,这个方法首先把 s1 引用的对象放到 String Pool字符串常量池然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。
当一个字符串调用 intern() 方法时,如果 String Poll 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Poll 中字符串的引用;否则,就会在 String Poll 中添加一个新的字符串,并返回这个新字符串的引用。
下面示例中s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。
```java
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
System.out.println(s1.intern() == s3); // true
String s4 = s1.intern();
System.out.println(s3 == s4); // true
```
如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。
如果是采用 "bbb" 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。
```java
String s4 = "bbb";
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s4 == s5); // true
```
在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7字符串常量池被放在堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
在 Java 7 之前,String Poll 被放在运行时常量池中,它属于永久代。而在 Java 7String Poll 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
- [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning)
- [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning)
- [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html)
## new String("abc")
使用这种方式一共会创建两个字符串对象(前提是 String Poll 中还没有 "abc" 字符串对象)。
- "abc" 属于字符串字面量,因此编译时期会在 String Poll 中创建一个字符串对象,指向这个 "abc" 字符串字面量;
- 而使用 new 的方式会在堆中创建一个字符串对象。
创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。
```java
public class NewStringTest {
public static void main(String[] args) {
String s = new String("abc");
}
}
```
使用 javap -verbose 进行反编译,得到以下内容:
```java
// ...
Constant pool:
// ...
#2 = Class #18 // java/lang/String
#3 = String #19 // abc
// ...
#18 = Utf8 java/lang/String
#19 = Utf8 abc
// ...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String abc
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
// ...
```
在 Constant Poll 中,#19 存储这字符串字面量 "abc"#3 是 String Poll 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Poll 中的字符串对象作为 String 构造函数的参数。
以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。
```java
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
```
# 三、运算
## 参数传递
Java 的参数是以值传递的形式传入方法中,而不是引用传递。
Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。
以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中改变指针引用的对象,那么这两个指针此时指向的是完全不同的对象,一方改变其所指向对象的内容对另一方没有影响。
```java
public class Dog {
@ -234,7 +295,11 @@ public class Dog {
}
String getName() {
return name;
return this.name;
}
void setName(String name) {
this.name = name;
}
String getObjectAddress() {
@ -262,6 +327,22 @@ public class PassByValueExample {
}
```
但是如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。
```java
class PassByValueExample {
public static void main(String[] args) {
Dog dog = new Dog("A");
func(dog);
System.out.println(dog.getName()); // B
}
private static void func(Dog dog) {
dog.setName("B");
}
}
```
[StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?](https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value)
## float 与 double
@ -317,7 +398,7 @@ switch (s) {
}
```
switch 不支持 long是因为 switch 的设计初衷是为那些只需要对少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
switch 不支持 long是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
```java
// long x = 111;
@ -341,33 +422,37 @@ Java 中有三个访问权限修饰符private、protected 以及 public
可以对类或类中的成员(字段以及方法)加上访问修饰符。
- 成员可见表示其它类可以用这个类的实例对象访问到该成员;
- 类可见表示其它类可以用这个类创建实例对象。
- 成员可见表示其它类可以用这个类的实例对象访问到该成员;
protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。
设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。
如果子类的方法覆盖了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。
如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。
字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。可以使用公有的 getter 和 setter 方法来替换公有字段
字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中AccessExample 拥有 id 共有字段,如果在某个时刻,我们想要使用 int 去存储 id 字段,那么就需要去修改所有的客户端代码
```java
public class AccessExample {
public int x;
public String id;
}
```
可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。
```java
public class AccessExample {
private int x;
public int getX() {
return x;
private int id;
public String getId() {
return id + "";
}
public void setX(int x) {
this.x = x;
public void setId(String id) {
this.id = Integer.valueOf(id);
}
}
```
@ -376,6 +461,7 @@ public class AccessExample {
```java
public class AccessWithInnerClassExample {
private class InnerClass {
int x;
}
@ -387,7 +473,7 @@ public class AccessWithInnerClassExample {
}
public int getValue() {
return innerClass.x; // 直接访问
return innerClass.x; // 直接访问
}
}
```
@ -396,7 +482,7 @@ public class AccessWithInnerClassExample {
**1. 抽象类**
抽象类和抽象方法都使用 abstract 进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。
抽象类和抽象方法都使用 abstract 关键字进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。
抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。
@ -415,7 +501,7 @@ public abstract class AbstractClassExample {
```
```java
public class AbstractExtendClassExample extends AbstractClassExample{
public class AbstractExtendClassExample extends AbstractClassExample {
@Override
public void func1() {
System.out.println("func1");
@ -441,6 +527,7 @@ ac2.func1();
```java
public interface InterfaceExample {
void func1();
default void func2(){
@ -448,7 +535,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
@ -477,30 +564,30 @@ System.out.println(InterfaceExample.x);
- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
- 接口的方法只能是 public 的,而抽象类的方法可以有多种访问权限。
- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
**4. 使用选择**
使用抽象类:
- 需要在几个相关的类中共享代码。
- 需要能控制继承来的成员的访问权限,而不是都为 public。
- 需要继承非静态non-static和非常量non-final字段。
使用接口:
- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
- 需要使用多重继承。
在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
使用抽象类:
- 需要在几个相关的类中共享代码。
- 需要能控制继承来的成员的访问权限,而不是都为 public。
- 需要继承非静态和非常量字段。
在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
- [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/)
- [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface)
## super
- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而完成一些初始化的工作。
- 访问父类的成员:如果子类覆盖了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。
- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
- 访问父类的成员:如果子类重写了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。
```java
public class SuperExample {
@ -549,9 +636,22 @@ SuperExtendExample.func()
## 重写与重载
- 重写Override存在于继承体系中指子类实现了一个与父类在方法声明上完全相同的一个方法
**1. 重写Override**
- 重载Overload存在于同一个类中指一个方法与已经存在的方法名称上相同但是参数类型、个数、顺序至少有一个不同。应该注意的是返回值不同其它都相同不算是重载。
存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
为了满足里式替换原则,重写有有以下两个限制:
- 子类方法的访问权限必须大于等于父类方法;
- 子类方法的返回类型必须是父类方法返回类型或为其子类型。
使用 @Override 注解,可以让编译器帮忙检查是否满足上面的两个限制条件。
**2. 重载Overload**
存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
应该注意的是,返回值不同,其它都相同不算是重载。
# 五、Object 通用方法
@ -583,19 +683,7 @@ protected void finalize() throws Throwable {}
## equals()
**1. equals() 与 == 的区别**
- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
- 对于引用类型,== 判断两个实例是否引用同一个对象,而 equals() 判断引用的对象是否等价。
```java
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y); // false
```
**2. 等价关系**
**1. 等价关系**
(一)自反性
@ -632,11 +720,23 @@ x.equals(y) == x.equals(y); // true
x.equals(null); // false;
```
**2. 等价与相等**
- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
- 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
```java
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y); // false
```
**3. 实现**
- 检查是否为同一个对象的引用,如果是直接返回 true
- 检查是否是同一个类型,如果不是,直接返回 false
- 将 Object 实例进行转型;
- 将 Object 对象进行转型;
- 判断每个关键域是否相等。
```java
@ -667,11 +767,11 @@ public class EqualExample {
## hashCode()
hasCode() 返回散列值,而 equals() 是用来判断两个实例是否等价。等价的两个实例散列值一定要相同,但是散列值相同的两个实例不一定等价。
hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个实例散列值也相等。
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。
下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例
下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象
```java
EqualExample e1 = new EqualExample(1, 1, 1);
@ -683,7 +783,7 @@ set.add(e2);
System.out.println(set.size()); // 2
```
理想的散列函数应当具有均匀性,即不相等的实例应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31因为它是一个奇素数如果是偶数的话当出现乘法溢出信息就会丢失因为与 2 相乘相当于向左移一位。
理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31因为它是一个奇素数如果是偶数的话当出现乘法溢出信息就会丢失因为与 2 相乘相当于向左移一位。
一个数与 31 相乘可以转换成移位和减法:`31*x == (x<<5)-x`编译器会自动进行这个优化
@ -725,7 +825,7 @@ ToStringExample@4554617c
**1. cloneable**
clone() 是 Object 的 protect 方法,它不是 public一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
clone() 是 Object 的 protected 方法,它不是 public一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
```java
public class CloneExample {
@ -768,6 +868,8 @@ java.lang.CloneNotSupportedException: CloneExample
以上抛出了 CloneNotSupportedException这是因为 CloneExample 没有实现 Cloneable 接口。
应该注意的是clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
```java
public class CloneExample implements Cloneable {
private int a;
@ -780,15 +882,13 @@ public class CloneExample implements Cloneable {
}
```
应该注意的是clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
**2. 浅拷贝**
**2. 深拷贝与浅拷贝**
- 浅拷贝:拷贝实例和原始实例的引用类型引用同一个对象;
- 深拷贝:拷贝实例和原始实例的引用类型引用不同对象。
拷贝对象和原始对象的引用类型引用同一个对象。
```java
public class ShallowCloneExample implements Cloneable {
private int[] arr;
public ShallowCloneExample() {
@ -825,8 +925,13 @@ e1.set(2, 222);
System.out.println(e2.get(2)); // 222
```
**3. 深拷贝**
拷贝对象和原始对象的引用类型引用不同对象。
```java
public class DeepCloneExample implements Cloneable {
private int[] arr;
public DeepCloneExample() {
@ -868,10 +973,13 @@ e1.set(2, 222);
System.out.println(e2.get(2)); // 2
```
**4. clone() 的替代方案**
使用 clone() 方法来拷贝一个对象即复杂又有风险它会抛出异常并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
```java
public class CloneConstructorExample {
private int[] arr;
public CloneConstructorExample() {
@ -937,7 +1045,7 @@ private 方法隐式地被指定为 final如果在子类中定义的方法和
**1. 静态变量**
- 静态变量:类所有的实例都共享静态变量,可以直接通过类名来访问它静态变量在内存中只存在一份。
- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它静态变量在内存中只存在一份。
- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
```java
@ -956,7 +1064,7 @@ public class A {
**2. 静态方法**
静态方法在类加载的时候就存在了,它不依赖于任何实例所以静态方法必须有实现,也就是说它不能是抽象方法abstract
静态方法在类加载的时候就存在了,它不依赖于任何实例所以静态方法必须有实现,也就是说它不能是抽象方法。
```java
public abstract class A {
@ -996,7 +1104,6 @@ public class A {
A a2 = new A();
}
}
```
```html
@ -1005,7 +1112,7 @@ public class A {
**4. 静态内部类**
非静态内部类依赖于需要外部类的实例,而静态内部类不需要。
非静态内部类依赖于外部类的实例,而静态内部类不需要。
```java
public class OuterClass {
@ -1028,12 +1135,12 @@ public class OuterClass {
**5. 静态导包**
在使用静态变量和方法时不用再指明 ClassName从而简化代码但可读性大大降低。
```java
import static com.xxx.ClassName.*
```
在使用静态变量和方法时不用再指明 ClassName从而简化代码但可读性大大降低。
**6. 初始化顺序**
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。
@ -1075,11 +1182,12 @@ public InitialOrderTest() {
- 子类(实例变量、普通语句块)
- 子类(构造函数)
# 七、反射
每个类都有一个 **Class** 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载,该方法会返回一个 Class 对象。
类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载,该方法会返回一个 Class 对象。
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
@ -1176,7 +1284,7 @@ Java 注解是附加在代码中的一些元信息,用于一些工具在编译
- Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
- Java 支持自动垃圾回收,而 C++ 需要手动回收。
- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
- Java 不支持操作符重载,虽然可以对两个 String 对象支持加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
- Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
- Java 的 goto 是保留字但是不可用C++ 可以使用 goto。
- Java 不支持条件编译C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。

View File

@ -14,25 +14,26 @@
* [ConcurrentHashMap](#concurrenthashmap)
* [LinkedHashMap](#linkedhashmap)
* [WeekHashMap](#weekhashmap)
* [附录](#附录)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、概览
容器主要包括 Collection 和 Map 两种Collection 又包含了 List、Set 以及 Queue
容器主要包括 Collection 和 Map 两种Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表
## Collection
<div align="center"> <img src="../pics//java-collections.png"/> </div><br>
<div align="center"> <img src="../pics//VP4n3i8m34Ntd28NQ4_0KCJ2q044Oez.png"/> </div><br>
### 1. Set
- HashSet基于哈希实现支持快速查找但不支持有序性操作例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的
- TreeSet基于红黑树实现支持有序性操作例如根据一个范围查找元素的操作。但是查找效率不如 HashSetHashSet 查找的时间复杂度为 O(1)TreeSet 则为 O(logN)
- TreeSet基于红黑树实现支持有序性操作但是查找效率不如 HashSetHashSet 查找时间复杂度为 O(1)TreeSet 则为 O(logN)
- HashSet基于哈希表实现支持快速查找但不支持有序性操作。并且失去了元素的插入顺序信息也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的
- LinkedHashSet具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序。
- LinkedHashSet具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
### 2. List
@ -50,21 +51,22 @@
## Map
<div align="center"> <img src="../pics//java-collections1.png"/> </div><br>
<div align="center"> <img src="../pics//SoWkIImgAStDuUBAp2j9BKfBJ4vLy4q.png"/> </div><br>
- HashMap基于哈希实现
- TreeMap基于红黑树实现。
- HashMap基于哈希表实现。
- HashTable和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
- LinkedHashMap使用链表来维护元素的顺序顺序为插入顺序或者最近最少使用LRU顺序。
- LinkedHashMap使用双向链表来维护元素的顺序顺序为插入顺序或者最近最少使用LRU顺序。
- TreeMap基于红黑树实现。
# 二、容器中的设计模式
## 迭代器模式
<div align="center"> <img src="../pics//Iterator-1.jpg"/> </div><br>
<div align="center"> <img src="../pics//SoWkIImgAStDuUBAp2j9BKfBJ4vLy0G.png"/> </div><br>
Collection 实现了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。
@ -95,7 +97,7 @@ Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
```
也可以使用以下方式使用 asList()
也可以使用以下方式用 asList()
```java
List list = Arrays.asList(1,2,3);
@ -111,7 +113,7 @@ List list = Arrays.asList(1,2,3);
### 1. 概览
实现了 RandomAccess 接口,因此支持随机访问这是理所当然的,因为 ArrayList 是基于数组实现的。
实现了 RandomAccess 接口,因此支持随机访问这是理所当然的,因为 ArrayList 是基于数组实现的。
```java
public class ArrayList<E> extends AbstractList<E>
@ -124,15 +126,7 @@ public class ArrayList<E> extends AbstractList<E>
private static final int DEFAULT_CAPACITY = 10;
```
### 2. 序列化
基于数组实现,保存元素的数组使用 transient 修饰该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性因此保存元素的数组不一定都会被使用那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
```java
transient Object[] elementData; // non-private to simplify nested class access
```
### 3. 扩容
### 2. 扩容
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。
@ -172,9 +166,9 @@ private void grow(int minCapacity) {
}
```
### 4. 删除元素
### 3. 删除元素
需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上。
需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的
```java
public E remove(int index) {
@ -189,7 +183,7 @@ public E remove(int index) {
}
```
### 5. Fail-Fast
### 4. Fail-Fast
modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
@ -216,6 +210,71 @@ private void writeObject(java.io.ObjectOutputStream s)
}
```
### 5. 序列化
ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。
保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。
```java
transient Object[] elementData; // non-private to simplify nested class access
```
ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
```java
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
```
```java
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
```
序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
```java
ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);
```
## Vector
### 1. 同步
@ -238,14 +297,14 @@ public synchronized E get(int index) {
}
```
### 2. 与 ArrayList 的区别
### 2. 与 ArrayList 的比较
- Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector因为同步操作完全可以由程序员自己来控制
- Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
### 3. 替代方案
为了获得线程安全的 ArrayList可以使用 `Collections.synchronizedList();` 得到一个线程安全的 ArrayList。
可以使用 `Collections.synchronizedList();` 得到一个线程安全的 ArrayList。
```java
List<String> list = new ArrayList<>();
@ -264,7 +323,7 @@ List<String> list = new CopyOnWriteArrayList<>();
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
写操作需要加锁,防止同时并发写入时导致写入数据丢失。
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后需要把原始数组指向新的复制数组。
@ -311,7 +370,7 @@ CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读
### 1. 概览
基于双向链表实现,内部使用 Node 存储链表节点信息。
基于双向链表实现,使用 Node 存储链表节点信息。
```java
private static class Node<E> {
@ -328,9 +387,9 @@ transient Node<E> first;
transient Node<E> last;
```
<div align="center"> <img src="../pics//5158bc2f-83a6-4351-817e-c9b07f955d76.png"/> </div><br>
<div align="center"> <img src="../pics//49495c95-52e5-4c9a-b27b-92cf235ff5ec.png" width="500"/> </div><br>
### 2. ArrayList 与 LinkedList
### 2. 与 ArrayList 的比较
- ArrayList 基于动态数组实现LinkedList 基于双向链表实现;
- ArrayList 支持随机访问LinkedList 不支持;
@ -348,7 +407,7 @@ transient Node<E> last;
transient Entry[] table;
```
其中Entry 就是存储数据的键值对,它包含了四个字段。从 next 字段我们可以看出 Entry 是一个链表,即数组中的每个位置被当成一个桶,一个桶存放一个链表,链表中存放哈希值相同的 Entry也就是说,HashMap 使用拉链法来解决冲突。
Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值相同的 Entry
<div align="center"> <img src="../pics//8fe838e3-ef77-4f63-bf45-417b6bc5c6bb.png" width="600"/> </div><br>
@ -402,21 +461,6 @@ static class Entry<K,V> implements Map.Entry<K,V> {
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<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
```
@ -474,7 +518,7 @@ public V put(K key, V value) {
}
```
HashMap 允许插入键为 null 的键值对。因为无法调用 null 的 hashCode()也就无法确定该键值对的桶下标只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。
HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法也就无法确定该键值对的桶下标只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。
```java
private V putForNullKey(V value) {
@ -576,8 +620,8 @@ y&(x-1) : 00000010
这个性质和 y 对 x 取模效果是一样的:
```
x : 00010000
y : 10110010
x : 00010000
y%x : 00000010
```
@ -601,10 +645,10 @@ static int indexFor(int h, int length) {
| 参数 | 含义 |
| :--: | :-- |
| capacity | table 的容量大小,默认为 16需要注意的是 capacity 必须保证为 2 的 n 次方。|
| capacity | table 的容量大小,默认为 16需要注意的是 capacity 必须保证为 2 的 n 次方。|
| size | table 的实际使用量。 |
| threshold | size 的临界值size 必须小于 threshold如果大于等于就必须进行扩容操作。 |
| load_factor | 装载因子table 能够使用的比例threshold = capacity * load_factor。|
| loadFactor | 装载因子table 能够使用的比例threshold = capacity * loadFactor。|
```java
static final int DEFAULT_INITIAL_CAPACITY = 16;
@ -635,7 +679,7 @@ void addEntry(int hash, K key, V value, int bucketIndex) {
}
```
扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把旧 table 的所有键值对重新插入新的 table 中,因此这一步是很费时的。
扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。
```java
void resize(int newCapacity) {
@ -674,14 +718,17 @@ void transfer(Entry[] newTable) {
在进行扩容时需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。
假设原数组长度 capacity 为 8扩容之后 new capacity 为 16
假设原数组长度 capacity 为 16扩容之后 new capacity 为 32
```html
capacity : 00010000
new capacity : 00100000
```
对于一个 Key它的哈希值如果在第 6 位上为 0那么取模得到的结果和之前一样如果为 1那么得到的结果为原来的结果 + 8。
对于一个 Key
- 它的哈希值如果在第 5 位上为 0那么取模得到的结果和之前一样
- 如果为 1那么得到的结果为原来的结果 +16。
### 7. 扩容-计算数组容量
@ -720,7 +767,7 @@ static final int tableSizeFor(int cap) {
从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。
### 9. HashMap 与 HashTable
### 9. 与 HashTable 的比较
- HashTable 使用 synchronized 来进行同步。
- HashMap 可以插入键为 null 的 Entry。
@ -851,7 +898,7 @@ public int size() {
### 3. JDK 1.8 的改动
JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment它继承自重入锁 ReentrantLock并发度与 Segment 数量相等。
JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment它继承自重入锁 ReentrantLock并发度与 Segment 数量相等。
JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。
@ -867,7 +914,7 @@ JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
```
存维护了一个双向循环链表,用来维护插入顺序或者 LRU 顺序。
部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。
```java
/**
@ -881,13 +928,13 @@ transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
```
顺序使用 accessOrder 来决定,默认为 false此时使用的是插入顺序。
accessOrder 决定了顺序,默认为 false此时维护的是插入顺序。
```java
final boolean accessOrder;
```
LinkedHashMap 最重要的是以下用于记录顺序的函数,它们会在 put、get 等方法中调用。
LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。
```java
void afterNodeAccess(Node<K,V> p) { }
@ -896,7 +943,7 @@ void afterNodeInsertion(boolean evict) { }
### afterNodeAccess()
当一个 Node 被访问时,如果 accessOrder 为 true会将它移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。
当一个节点被访问时,如果 accessOrder 为 true则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。
```java
void afterNodeAccess(Node<K,V> e) { // move node to last
@ -945,13 +992,17 @@ removeEldestEntry() 默认为 false如果需要让它为 true需要继承
```java
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
return false;
}
```
### LRU 缓存
以下是使用 LinkedHashMap 实现的一个 LRU 缓存,设定最大缓存空间 MAX_ENTRIES 为 3。使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true开启 LUR 顺序。覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
以下是使用 LinkedHashMap 实现的一个 LRU 缓存:
- 设定最大缓存空间 MAX_ENTRIES 为 3
- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true开启 LRU 顺序;
- 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
```java
class LRUCache<K, V> extends LinkedHashMap<K, V> {
@ -997,12 +1048,14 @@ private static class Entry<K,V> extends WeakReference<Object> implements Map.Ent
### ConcurrentCache
Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
ConcurrentCache 采取的是分代缓存:
- 经常使用的对象放入 eden 中eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);
- 不常用的对象放入 longtermlongterm 使用 WeakHashMap 实现,用来存放比较老的对象,这些老对象会被垃圾收集器回收。
- 不常用的对象放入 longtermlongterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。
- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收。
- 当调用 put() 方法时,如果 eden 的大小超过了 size那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。
```java
public final class ConcurrentCache<K, V> {
@ -1039,13 +1092,97 @@ public final class ConcurrentCache<K, V> {
}
```
# 附录
Collection 绘图源码:
```
@startuml
interface Collection
interface Set
interface List
interface Queue
interface SortSet
class HashSet
class LinkedHashSet
class TreeSet
class ArrayList
class Vector
class LinkedList
class PriorityQueue
Collection <|-- Set
Collection <|-- List
Collection <|-- Queue
Set <|-- SortSet
Set <|.. HashSet
Set <|.. LinkedHashSet
SortSet <|.. TreeSet
List <|.. ArrayList
List <|.. Vector
List <|.. LinkedList
Queue <|.. LinkedList
Queue <|.. PriorityQueue
@enduml
```
Map 绘图源码
```
@startuml
interface Map
interface SortMap
class HashTable
class LinkedHashMap
class HashMap
class TreeMap
Map <|.. HashTable
Map <|.. LinkedHashMap
Map <|.. HashMap
Map <|-- SortMap
SortMap <|.. TreeMap
@enduml
```
迭代器类图
```
@startuml
interface Iterable
interface Collection
interface List
interface Set
interface Queue
interface Iterator
interface ListIterator
Iterable <|-- Collection
Collection <|.. List
Collection <|.. Set
Collection <|-- Queue
Iterator <-- Iterable
Iterator <|.. ListIterator
ListIterator <-- List
@enduml
```
# 参考资料
- Eckel B. Java 编程思想 [M]. 机械工业出版社, 2002.
- [Java Collection Framework](https://www.w3resource.com/java-tutorial/java-collections.php)
- [Iterator 模式](https://openhome.cc/Gossip/DesignPattern/IteratorPattern.htm)
- [Java 8 系列之重新认识 HashMap](https://tech.meituan.com/java-hashmap.html)
- [Java 8 系列之重新认识 HashMap](https://tech.meituan.com/java_hashmap.html)
- [What is difference between HashMap and Hashtable in Java?](http://javarevisited.blogspot.hk/2010/10/difference-between-hashmap-and.html)
- [Java 集合之 HashMap](http://www.zhangchangle.com/2018/02/07/Java%E9%9B%86%E5%90%88%E4%B9%8BHashMap/)
- [The principle of ConcurrentHashMap analysis](http://www.programering.com/a/MDO3QDNwATM.html)

View File

@ -168,6 +168,8 @@ public static void main(String[] args) throws ExecutionException, InterruptedExc
同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。
当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
```java
public class MyThread extends Thread {
public void run() {
@ -635,6 +637,7 @@ B
```java
public class WaitNotifyExample {
public synchronized void before() {
System.out.println("before");
notifyAll();
@ -672,12 +675,15 @@ after
## await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式await() 可以指定等待的条件,因此更加灵活。
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。
相比于 wait() 这种等待方式await() 可以指定等待的条件,因此更加灵活。
使用 Lock 来获取一个 Condition 对象。
```java
public class AwaitSignalExample {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
@ -733,7 +739,6 @@ java.util.concurrentJ.U.C大大提高了并发性能AQS 被认为是 J.
```java
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
@ -759,15 +764,30 @@ run..run..run..run..run..run..run..run..run..run..end
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。但是它的计数器是递增的,每次执行 await() 方法之后,计数器会加 1直到计数器的值和设置的值相等等待的所有线程才会继续执行。和 CountdownLatch 的另一个区别是CyclicBarrier 的计数器可以循环使用,所以它才叫做循环屏障
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1并进行等待直到计数器为 0所有调用 awati() 方法而在等待的线程才能继续执行
下图应该从下往上看才正确。
CyclicBarrier 和 CountdownLatch 的一个区别是CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值barrierAction 在所有线程都到达屏障的时候会执行一次。
```java
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
```
<div align="center"> <img src="../pics//CyclicBarrier.png" width=""/> </div><br>
```java
public class CyclicBarrierExample {
public static void main(String[] args) throws InterruptedException {
public static void main(String[] args) {
final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
@ -776,9 +796,7 @@ public class CyclicBarrierExample {
System.out.print("before..");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.print("after..");
@ -795,7 +813,7 @@ before..before..before..before..before..before..before..before..before..before..
## Semaphore
Semaphore 就是操作系统中的信号量,可以控制对互斥资源的访问线程数。
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
<div align="center"> <img src="../pics//Semaphore.png" width=""/> </div><br>
@ -1006,7 +1024,7 @@ ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线
如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。
以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值为 997 而不是 1000。
以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。
```java
public class ThreadUnsafeExample {
@ -1084,11 +1102,11 @@ Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据longdouble的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。
有一个错误认识就是int 等原子性的变量在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中cnt 变量属于 int 类型变量1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。
有一个错误认识就是int 等原子性的类型在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中cnt 属于 int 类型变量1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。
为了方便讨论,将内存间的交互操作简化为 3 个load、assign、store。
下图演示了两个线程同时对 cnt 变量进行操作load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存T2 依然可以读入该变量的值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。
下图演示了两个线程同时对 cnt 进行操作load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存T2 依然可以读入值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。
<div align="center"> <img src="../pics//ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92.png" width=""/> </div><br>
@ -1186,9 +1204,7 @@ public static void main(String[] args) throws InterruptedException {
### 3. 有序性
有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。
在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
@ -1198,8 +1214,6 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即
上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
主要有以下这些原则:
### 1. 单一线程原则
> Single Thread rule
@ -1270,14 +1284,16 @@ Thread 对象的结束先行发生于 join() 方法返回。
### 1. 不可变
不可变Immutable的对象一定是线程安全的无论是对象的方法实现还是方法的调用者都不需要再采取任何的线程安全保障措施只要一个不可变的对象被正确地构建出来那其外部的可见状态永远也不会改变永远也不会看到它在多个线程之中处于不一致的状态。
不可变Immutable的对象一定是线程安全的不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来永远也不会看到它在多个线程之中处于不一致的状态。
多线程环境下,应当尽量使对象成为不可变,来满足线程安全。
不可变的类型:
- final 关键字修饰的基本数据类型
- final 关键字修饰的基本数据类型
- String
- 枚举类型
- Number 部分子类,如 Long 和 Double 等数值包装类型BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的子类型的原子类 AtomicInteger 和 AtomicLong 则并非不可变的。
- Number 部分子类,如 Long 和 Double 等数值包装类型BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。
对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
@ -1305,21 +1321,19 @@ public V put(K key, V value) {
}
```
多线程环境下,应当尽量使对象成为不可变,来满足线程安全。
### 2. 绝对线程安全
不管运行时环境如何,调用者都不需要任何额外的同步措施。
### 3. 相对线程安全
相对线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
相对线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。
对于下面的代码,如果删除元素的线程删除了一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。
对于下面的代码,如果删除元素的线程删除了 Vector 的一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。
```java
```Java
public class VectorUnsafeExample {
private static Vector<Integer> vector = new Vector<>();
@ -1389,17 +1403,19 @@ synchronized 和 ReentrantLock。
### 2. 非阻塞同步
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
**CAS**
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步
硬件支持的原子性操作最典型的是比较并交换Compare-and-SwapCAS。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时只有当 V 的值等于 A才将 V 的值更新为 B。
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是比较并交换Compare-and-SwapCAS。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时只有当 V 的值等于 A才将 V 的值更新为 B。
J.U.C 包里面的整数原子类 AtomicInteger其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。
**AtomicInteger**
J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。
以下代码使用了 AtomicInteger 执行了自增的操作。
@ -1411,7 +1427,7 @@ public void add() {
}
```
以下代码是 incrementAndGet() 的源码,它调用了 unsafe 的 getAndAddInt() 。
以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。
```java
public final int incrementAndGet() {
@ -1419,7 +1435,9 @@ public final int incrementAndGet() {
}
```
以下代码是 getAndAddInt() 源码var1 指示内存地址var2 指示旧值var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果 var2==var5那么就更新内存地址为 var1 的变量为 var5+var4。可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。
以下代码是 getAndAddInt() 源码var1 指示对象内存地址var2 指示该字段相对对象内存地址的偏移var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5那么就更新内存地址为 var1+var2 的变量为 var5+var4。
可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。
```java
public final int getAndAddInt(Object var1, long var2, int var4) {
@ -1432,28 +1450,21 @@ public final int getAndAddInt(Object var1, long var2, int var4) {
}
```
ABA :如果一个变量初次读取的时候是 A 值,它的值被改成了 B后来又被改回为 A那 CAS 操作就会误认为它从来没有被改变过。
**ABA**
如果一个变量初次读取的时候是 A 值,它的值被改成了 B后来又被改回为 A那 CAS 操作就会误认为它从来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
### 3. 无同步方案
要保证线程安全,并不是一定就要进行同步,两者没有因果关系同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
**(一)可重入代码Reentrant Code**
**(一)栈封闭**
这种代码也叫做纯代码Pure Code可以在代码执行的任何时刻中断它转而去执行另外一段代码包括递归调用它本身而在控制权返回后原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
**(二)栈封闭**
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在栈中,属于线程私有的。
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
```java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class StackClosedExample {
public void add100() {
int cnt = 0;
@ -1480,11 +1491,11 @@ public static void main(String[] args) {
100
```
**线程本地存储Thread Local Storage**
**线程本地存储Thread Local Storage**
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”Thread-per-Request的处理方式这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”Thread-per-Request的处理方式这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
@ -1543,7 +1554,7 @@ public class ThreadLocalExample1 {
<div align="center"> <img src="../pics//3646544a-cb57-451d-9e03-d3c4f5e4434a.png" width=""/> </div><br>
每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员
每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。
```java
/* ThreadLocal values pertaining to this thread. This map is maintained
@ -1582,17 +1593,25 @@ public T get() {
}
```
ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。
ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。
在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。
**可重入代码Reentrant Code**
这种代码也叫做纯代码Pure Code可以在代码执行的任何时刻中断它转而去执行另外一段代码包括递归调用它本身而在控制权返回后原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
# 十二、锁优化
这里的锁优化主要是指虚拟机对 synchronized 的优化。
这里的锁优化主要是指 JVM 对 synchronized 的优化。
## 自旋锁
互斥同步的进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
@ -1622,7 +1641,7 @@ public static String concatString(String s1, String s2, String s3) {
}
```
每个 append() 方法中都有一个同步块。虚拟机观察变量 sb很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。
每个 append() 方法中都有一个同步块。虚拟机观察变量 sb很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。
## 锁粗化
@ -1634,9 +1653,9 @@ public static String concatString(String s1, String s2, String s3) {
JDK 1.6 引入了偏向锁和轻量级锁从而让锁拥有了四个状态无锁状态unlocked、偏向锁状态biasble、轻量级锁状态lightweight locked和重量级锁状态inflated
以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 mark word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出,应该注意的是 state 表格不是存储在对象头中的。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
<div align="center"> <img src="../pics//bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png" width="600"/> </div><br>
<div align="center"> <img src="../pics//bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png" width="500"/> </div><br>
下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象包含了 Mark Word 和其它信息。
@ -1644,9 +1663,9 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
当尝试获取一个锁对象时,如果锁对象标记为 0 01说明锁对象的锁未锁定unlocked状态。此时虚拟机在当前线程栈中创建 Lock Record然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00表示该对象处于轻量级锁状态。
当尝试获取一个锁对象时,如果锁对象标记为 0 01说明锁对象的锁未锁定unlocked状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00表示该对象处于轻量级锁状态。
<div align="center"> <img src="../pics//baaa681f-7c52-4198-a5ae-303b9386cf47.png" width="500"/> </div><br>
<div align="center"> <img src="../pics//baaa681f-7c52-4198-a5ae-303b9386cf47.png" width="400"/> </div><br>
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
@ -1664,17 +1683,17 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:
- 给线程起个有意义的名字,这样可以方便找 Bug。
- 缩小同步范围,例如对于 synchronized应该尽量使用同步块而不是同步方法。
- 缩小同步范围,从而减少锁争用。例如对于 synchronized应该尽量使用同步块而不是同步方法。
- 多用同步类少用 wait() 和 notify()。首先CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现对复杂的控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
- 多用同步工具少用 wait() 和 notify()。首先CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善。
- 使用 BlockingQueue 实现生产者消费者问题。
- 多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
- 使用本地变量和不可变类来保证线程安全。
- 使用线程池而不是直接创建 Thread 对象,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。
- 使用 BlockingQueue 实现生产者消费者问题。
- 使用线程池而不是直接创建线程,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。
# 参考资料

View File

@ -1,44 +1,51 @@
<!-- GFM-TOC -->
* [一、运行时数据区域](#一运行时数据区域)
* [程序计数器](#程序计数器)
* [虚拟机栈](#虚拟机栈)
* [Java 虚拟机栈](#java-虚拟机栈)
* [本地方法栈](#本地方法栈)
* [](#堆)
* [方法区](#方法区)
* [运行时常量池](#运行时常量池)
* [直接内存](#直接内存)
* [二、垃圾收集](#二垃圾收集)
* [判断一个对象是否存活](#判断一个对象是否存活)
* [判断一个对象是否可被回收](#判断一个对象是否可被回收)
* [引用类型](#引用类型)
* [垃圾收集算法](#垃圾收集算法)
* [垃圾收集器](#垃圾收集器)
* [内存分配与回收策略](#内存分配与回收策略)
* [三、类加载机制](#三类加载机制)
* [三、内存分配与回收策略](#三内存分配与回收策略)
* [Minor GC 和 Full GC](#minor-gc-和-full-gc)
* [内存分配策略](#内存分配策略)
* [Full GC 的触发条件](#full-gc-的触发条件)
* [四、类加载机制](#四类加载机制)
* [类的生命周期](#类的生命周期)
* [类初始化时机](#类初始化时机)
* [类加载过程](#类加载过程)
* [类加载器](#类加载器)
* [类初始化时机](#类初始化时机)
* [类与类加载器](#类与类加载器)
* [类加载器分类](#类加载器分类)
* [双亲委派模型](#双亲委派模型)
* [自定义类加载器实现](#自定义类加载器实现)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、运行时数据区域
<div align="center"> <img src="../pics//540631a4-6018-40a5-aed7-081e2eeeaeea.png" width="500"/> </div><br>
<div align="center"> <img src="../pics//c9ad2bf4-5580-4018-bce4-1b9a71804d9c.png" width="400"/> </div><br>
## 程序计数器
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
## 虚拟机栈
## Java 虚拟机栈
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
<div align="center"> <img src="../pics//f5757d09-88e7-4bbd-8cfb-cecf55604854.png" width=""/> </div><br>
<div align="center"> <img src="../pics//926c7438-c5e1-4b94-840a-dcb24ff1dafe.png" width="450"/> </div><br>
可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小:
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:
```java
java -Xss=512M HackTheJava
java -Xss512M HackTheJava
```
该区域可能抛出以下异常:
@ -48,49 +55,38 @@ java -Xss=512M HackTheJava
## 本地方法栈
本地方法不是用 Java 实现,对待这些方法需要特别处理
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务
与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务
本地方法一般是用其它语言C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理
<div align="center"> <img src="../pics//JNIFigure1.gif" width="350"/> </div><br>
<div align="center"> <img src="../pics//JNI-Java-Native-Interface.jpg" width="350"/> </div><br>
## 堆
所有对象实例都在这里分配内存。
所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆"
是垃圾收集的主要区域("GC 堆")。现代的垃圾收集器基本都是采用分代收集算法,主要思想是针对不同的对象采取不同的垃圾回收算法。虚拟机把 Java 堆分成以下三块:
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法,可以将堆分成两块:
- 新生代Young Generation
- 老年代Old Generation
- 永久代Permanent Generation
当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常
新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间:
- Eden伊甸园
- From Survivor幸存者
- To Survivor
<div align="center"> <img src="../pics//ppt_img.gif" width=""/> </div><br>
Java 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
```java
java -Xms=1M -Xmx=2M HackTheJava
java -Xms1M -Xmx2M HackTheJava
```
## 方法区
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收JDK 1.8 之后,取消了永久代,用 metaspace元数据区替代
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但是很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中
## 运行时常量池
@ -102,22 +98,29 @@ Class 文件中的常量池(编译器生成的各种字面量和符号引用
## 直接内存
在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存Native 堆),然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。
这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
# 二、垃圾收集
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。
垃圾收集主要是针对堆和方法区进行。
## 判断一个对象是否存活
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。
## 判断一个对象是否可被回收
### 1. 引用计数算法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1引用失效时计数器减 1。引用计数不为 0 的对象仍然存活
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1引用失效时计数器减 1。引用计数为 0 的对象可被回收
两个对象出现循环引用的情况下,此时引用计数器永远不为 0导致无法对它们进行回收。
正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
```java
public class ReferenceCountingGC {
public Object instance = null;
public static void main(String[] args) {
@ -129,30 +132,50 @@ public class ReferenceCountingGC {
}
```
正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
### 2. 可达性分析算法
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
<div align="center"> <img src="../pics//0635cbe8.png" width=""/> </div><br>
Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中 JNI 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
### 3. 引用类型
<div align="center"> <img src="../pics//0635cbe8.png" width=""/> </div><br>
### 3. 方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
- 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
### 4. finalize()
finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。
## 引用类型
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java 具有四种强度不同的引用类型。
Java 提供了四种强度不同的引用类型。
**(一)强引用**
### 1. 强引用
被强引用关联的对象不会被垃圾收集器回收。
被强引用关联的对象不会被回收。
使用 new 一个新对象的方式来创建强引用。
@ -160,9 +183,9 @@ Java 具有四种强度不同的引用类型。
Object obj = new Object();
```
**(二)软引用**
### 2. 软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
被软引用关联的对象只有在内存不够的情况下才会被回收。
使用 SoftReference 类来创建软引用。
@ -172,9 +195,9 @@ SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
```
**(三)弱引用**
### 3. 弱引用
被弱引用关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前
使用 WeakReference 类来实现弱引用。
@ -184,11 +207,11 @@ WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
```
**(四)虚引用**
### 4. 虚引用
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
使用 PhantomReference 来实现虚引用。
@ -198,28 +221,6 @@ PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
```
### 4. 方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代差很多,因此在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
### 5. finalize()
finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。
## 垃圾收集算法
### 1. 标记 - 清除
@ -247,16 +248,18 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。
### 4. 分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将 Java 堆分为新生代和老年代。
一般将堆分为新生代和老年代。
- 新生代使用:复制算法
- 老年代使用:标记 - 清 或者 标记 - 整理 算法
- 老年代使用:标记 - 清 或者 标记 - 整理 算法
## 垃圾收集器
@ -264,8 +267,8 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
- 单线程与并行(多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而并行使用多个线程。
- 串行与并:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;
- 串行与并:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
### 1. Serial 收集器
@ -277,7 +280,7 @@ Serial 翻译为串行,也就是说它以串行的方式执行。
它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
它是 Client 模式下的默认新生代收集器,因为在应用场景下分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
### 2. ParNew 收集器
@ -291,15 +294,15 @@ Serial 翻译为串行,也就是说它以串行的方式执行。
### 3. Parallel Scavenge 收集器
与 ParNew 一样是并行的多线程收集器。
与 ParNew 一样是多线程收集器。
其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
还提供了一个参数 -XX:+UseAdaptiveSizePolicy这是一个开关参数打开参数后就不需要手工指定新生代的大小-Xmn、Eden 和 Survivor 区的比例(-XX:SurvivorRatio、晋升老年代对象年龄-XX:PretenureSizeThreshold等细节参数了虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略GC Ergonomics
可以通过一个开关参数打开 GC 自适应的调节策略GC Ergonomics就不需要手工指定新生代的大小-Xmn、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
### 4. Serial Old 收集器
@ -324,8 +327,6 @@ Serial 翻译为串行,也就是说它以串行的方式执行。
CMSConcurrent Mark SweepMark Sweep 指的是标记 - 清除算法。
特点:并发收集、低停顿。
分为以下四个流程:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
@ -345,7 +346,7 @@ CMSConcurrent Mark SweepMark Sweep 指的是标记 - 清除算法。
G1Garbage-First它是一款面向服务端应用的垃圾收集器在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
Java 堆被分为新生代、老年代和永久代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
<div align="center"> <img src="../pics//4cf711a8-7ab2-4152-b85c-d5c226733807.png" width="600"/> </div><br>
@ -371,34 +372,21 @@ G1 把堆划分成多个大小相等的独立区域Region新生代和
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
更详细内容请参考:[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html)
# 三、内存分配与回收策略
### 8. 比较
| 收集器 | 单线程/并行 | 串行/并发 | 新生代/老年代 | 收集算法 | 目标 | 适用场景 |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| **Serial** | 单线程 | 串行 | 新生代 | 复制 | 响应速度优先 | 单 CPU 环境下的 Client 模式 |
| **Serial Old** | 单线程 | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 |
| **ParNew** | 并行 |串行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 |
| **Parallel Scavenge** | 并行 | 串行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| **Parallel Old** | 并行 | 串行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| **CMS** | 并行 | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 |
| **G1** | 并行 | 并发 | 新生代 + 老年代 | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS |
## 内存分配与回收策略
### 1. Minor GC 和 Full GC
## Minor GC 和 Full GC
- Minor GC发生在新生代上因为新生代对象存活时间很短因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
- Full GC发生在老年代上老年代对象其存活时间长因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
### 2. 内存分配策略
## 内存分配策略
(一)对象优先在 Eden 分配
### 1. 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
(二)大对象直接进入老年代
### 2. 大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
@ -406,41 +394,41 @@ G1 把堆划分成多个大小相等的独立区域Region新生代和
-XX:PretenureSizeThreshold大于此值的对象直接在老年代分配避免在 Eden 区和 Survivor 区之间的大量内存复制。
(三)长期存活的对象进入老年代
### 3. 长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
(四)动态对象年龄判定
### 4. 动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
(五)空间分配担保
### 5. 空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC如果小于或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。
### 3. Full GC 的触发条件
## Full GC 的触发条件
对于 Minor GC其触发条件非常简单当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
(一)调用 System.gc()
### 1. 调用 System.gc()
只是建议虚拟机执行 Full GC但是虚拟机不一定真正去执行。不建议使用这种方式而是让虚拟机管理内存。
(二)老年代空间不足
### 2. 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC应当尽量不要创建过大的对象以及数组。除此之外可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
(三)空间分配担保失败
### 3. 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节。
(四)JDK 1.7 及以前的永久代空间不足
### 4. JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
@ -448,13 +436,13 @@ G1 把堆划分成多个大小相等的独立区域Region新生代和
为避免以上原因引起的 Full GC可采用的方法为增大永久代空间或转为使用 CMS GC。
(五)Concurrent Mode Failure
### 5. Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
# 、类加载机制
# 、类加载机制
类是在运行期间动态加载的。
类是在运行期间第一次使用时动态加载的,而不是编译时期一次性加载。因为如果在编译时期一次性加载,那么会占用很多的内存
## 类的生命周期
@ -470,10 +458,108 @@ G1 把堆划分成多个大小相等的独立区域Region新生代和
- 使用Using
- 卸载Unloading
## 类加载过程
包含了加载、验证、准备、解析和初始化这 5 个阶段。
### 1. 加载
加载是类加载的一个阶段,注意不要混淆。
加载过程完成以下三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
- 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。
其中二进制字节流可以从以下方式中获取:
- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
- 从网络中获取,最典型的应用是 Applet。
- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
### 2. 验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
### 3. 准备
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在堆中。
注意,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
```java
public static int value = 123;
```
如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
```java
public static final int value = 123;
```
### 4. 解析
将常量池的符号引用替换为直接引用的过程。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
### 5. 初始化
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 &lt;clinit>() 方法的过程。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
&lt;clinit>() 方法具有以下特点:
- 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
```java
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
```
- 与类的构造函数(或者说实例构造器 &lt;init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 &lt;clinit>() 方法运行之前,父类的 &lt;clinit>() 方法已经执行结束。因此虚拟机中第一个执行 &lt;clinit>() 方法的类肯定为 java.lang.Object。
- 由于父类的 &lt;clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:
```java
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 2
}
```
- &lt;clinit>() 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 &lt;clinit>() 方法。
- 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 &lt;clinit>() 方法。但接口与类不同的是,执行接口的 &lt;clinit>() 方法不需要先执行父接口的 &lt;clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 &lt;clinit>() 方法。
- 虚拟机会保证一个类的 &lt;clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 &lt;clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 &lt;clinit>() 方法完毕。如果在一个类的 &lt;clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
## 类初始化时机
### 1. 主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
@ -486,6 +572,8 @@ G1 把堆划分成多个大小相等的独立区域Region新生代和
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
### 2. 被动引用
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
- 通过子类引用父类的静态字段,不会导致子类初始化。
@ -506,118 +594,13 @@ SuperClass[] sca = new SuperClass[10];
System.out.println(ConstClass.HELLOWORLD);
```
## 类加载过程
## 类与类加载器
包含了加载、验证、准备、解析和初始化这 5 个阶段。
### 1. 加载
加载是类加载的一个阶段,注意不要混淆。
加载过程完成以下三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
- 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。
其中二进制字节流可以从以下方式中获取:
- 从 ZIP 包读取,这很常见,最终成为日后 JAR、EAR、WAR 格式的基础。
- 从网络中获取,这种场景最典型的应用是 Applet。
- 运行时计算生成,这种场景使用得最多得就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
- 由其他文件生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。
- 从数据库读取,这种场景相对少见,例如有些中间件服务器(如 SAP Netweaver可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
...
### 2. 验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 文件格式验证:验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
- 字节码验证:通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
- 符号引用验证:发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
### 3. 准备
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。(实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次)
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
```java
public static int value = 123;
```
如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
```java
public static final int value = 123;
```
### 4. 解析
将常量池的符号引用替换为直接引用的过程。
### 5. 初始化
初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 &lt;clinit>() 方法的过程。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
&lt;clinit>() 方法具有以下特点:
- 是由编译器自动收集类中所有类变量的赋值动作和静态语句块static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
```java
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
```
- 与类的构造函数(或者说实例构造器 &lt;init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 &lt;clinit>() 方法运行之前,父类的 &lt;clinit>() 方法已经执行结束。因此虚拟机中第一个执行 &lt;clinit>() 方法的类肯定为 java.lang.Object。
- 由于父类的 &lt;clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作。例如以下代码:
```java
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 输出结果是父类中的静态变量 A 的值,也就是 2。
}
```
- &lt;clinit>() 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 &lt;clinit>() 方法。
- 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 &lt;clinit>() 方法。但接口与类不同的是,执行接口的 &lt;clinit>() 方法不需要先执行父接口的 &lt;clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 &lt;clinit>() 方法。
- 虚拟机会保证一个类的 &lt;clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 &lt;clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 &lt;clinit>() 方法完毕。如果在一个类的 &lt;clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
## 类加载器
在 Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。
### 1. 类与类加载器
两个类相等:类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
两个类相等需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
### 2. 类加载器分类
## 类加载器分类
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
@ -627,13 +610,13 @@ public static void main(String[] args) {
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
- 启动类加载器Bootstrap ClassLoader此类加载器负责将存放在 &lt;JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
- 启动类加载器Bootstrap ClassLoader此类加载器负责将存放在 &lt;JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
- 扩展类加载器Extension ClassLoader这个类加载器是由 ExtClassLoadersun.misc.Launcher$ExtClassLoader实现的。它负责将 &lt;JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
- 应用程序类加载器Application ClassLoader这个类加载器是由 AppClassLoadersun.misc.Launcher$AppClassLoader实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值因此一般称为系统类加载器。它负责加载用户类路径ClassPath上所指定的类库开发者可以直接使用这个类加载器如果应用程序中没有自定义过自己的类加载器一般情况下这个就是程序中默认的类加载器。
### 3. 双亲委派模型
## 双亲委派模型
应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。
@ -641,17 +624,17 @@ public static void main(String[] args) {
<div align="center"> <img src="../pics//class_loader_hierarchy.png" width="600"/> </div><br>
(一)工作过程
### 1. 工作过程
一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。
(二)好处
### 2. 好处
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 ClassPath 中,程序可以编译通过。因为双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。正因为 rt.jar 中的 Object 优先级更高,因为程序中所有的 Object 都是这个 Object。
例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
(三)实现
### 3. 实现
以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException此时尝试自己去加载。
@ -699,7 +682,7 @@ public abstract class ClassLoader {
}
```
### 4. 自定义类加载器实现
## 自定义类加载器实现
FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
@ -751,7 +734,10 @@ public class FileSystemClassLoader extends ClassLoader {
# 参考资料
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
- [Chapter 2. The Structure of the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4)
- [Jvm memory](https://www.slideshare.net/benewu/jvm-memory)
[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html)
- [JNI Part1: Java Native Interface Introduction and “Hello World” application](http://electrofriends.com/articles/jni/jni-part1-java-native-interface/)
- [Memory Architecture Of JVM(Runtime Data Areas)](https://hackthejava.wordpress.com/2015/01/09/memory-architecture-by-jvmruntime-data-areas/)
- [JVM Run-Time Data Areas](https://www.programcreek.com/2013/04/jvm-run-time-data-areas/)
- [Android on x86: Java Native Interface and the Android Native Development Kit](http://www.drdobbs.com/architecture-and-design/android-on-x86-java-native-interface-and/240166271)

File diff suppressed because it is too large Load Diff

View File

@ -461,7 +461,7 @@ Employee 表:
+----+-------+--------+-----------+
```
查找所有员工,他们的薪资大于其经理薪资。
查找薪资大于其经理薪资的员工信息
## SQL Schema
@ -924,27 +924,27 @@ VALUES
```sql
SELECT
s1.id - 1 AS id,
s1.student
s1.student
FROM
seat s1
seat s1
WHERE
s1.id MOD 2 = 0 UNION
SELECT
s2.id + 1 AS id,
s2.student
s2.student
FROM
seat s2
seat s2
WHERE
s2.id MOD 2 = 1
s2.id MOD 2 = 1
AND s2.id != ( SELECT max( s3.id ) FROM seat s3 ) UNION
SELECT
s4.id AS id,
s4.student
s4.student
FROM
seat s4
seat s4
WHERE
s4.id MOD 2 = 1
AND s4.id = ( SELECT max( s5.id ) FROM seat s5 )
s4.id MOD 2 = 1
AND s4.id = ( SELECT max( s5.id ) FROM seat s5 )
ORDER BY
id;
```

View File

@ -11,7 +11,6 @@
* [GNU](#gnu)
* [开源协议](#开源协议)
* [二、磁盘](#二磁盘)
* [HDD](#hdd)
* [磁盘接口](#磁盘接口)
* [磁盘的文件名](#磁盘的文件名)
* [三、分区](#三分区)
@ -46,7 +45,7 @@
* [变量操作](#变量操作)
* [指令搜索顺序](#指令搜索顺序)
* [数据流重定向](#数据流重定向)
* [八、管线指令](#八管线指令)
* [八、管道指令](#八管道指令)
* [提取指令](#提取指令)
* [排序指令](#排序指令)
* [双向输出重定向](#双向输出重定向)
@ -130,15 +129,16 @@ info 与 man 类似,但是 info 将文档分成一个个页面,每个页面
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin
```
env 命令可以获取当前终端的环境变量。
## sudo
sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。
## 包管理工具
RPM 和 DPKG 为最常见的两类软件包管理工具。RPM 全称为 Redhat Package Manager最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。与 RPM 进行竞争的是基于 Debian 操作系统 (UBUNTU) 的 DEB 软件包管理工具 DPKG全称为 Debian Package功能方面与 RPM 相似。
RPM 和 DPKG 为最常见的两类软件包管理工具:
- RPM 全称为 Redhat Package Manager最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。
- 与 RPM 进行竞争的是基于 Debian 操作系统 (Ubuntu) 的 DEB 软件包管理工具 DPKG全称为 Debian Package功能方面与 RPM 相似。
YUM 基于 RPM具有依赖管理功能并具有软件升级的功能。
@ -185,21 +185,6 @@ GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操
# 二、磁盘
## HDD
Hard Disk Drives(HDD) 俗称硬盘,具有以下结构:
- 盘面Platter一个硬盘有多个盘面
- 磁道Track盘面上的圆形带状区域一个盘面可以有多个磁道
- 扇区Track Sector磁道上的一个弧段一个磁道可以有多个扇区它是最小的物理储存单位目前主要有 512 bytes 与 4 K 两种大小;
- 磁头Head与盘面非常接近能够将盘面上的磁场转换为电信号或者将电信号转换为盘面的磁场
- 制动手臂Actuator arm用于在磁道之间移动磁头
- 主轴Spindle使整个盘面转动。
<div align="center"> <img src="../pics//014fbc4d-d873-4a12-b160-867ddaed9807.jpg" width=""/> </div><br>
[Decoding UCS Invicta Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1)
## 磁盘接口
### 1. IDE
@ -210,13 +195,13 @@ IDEATA全称 Advanced Technology Attachment接口速度最大为 133MB/
### 2. SATA
SATA 全称 Serial ATA也就是使用串口的 ATA 接口,抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能SATA-II 的接口速度为 300MiB/s而新的 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。
SATA 全称 Serial ATA也就是使用串口的 ATA 接口,抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能SATA-II 的接口速度为 300MiB/s而新的 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。
<div align="center"> <img src="../pics//f9f2a16b-4843-44d1-9759-c745772e9bcf.jpg" width=""/> </div><br>
### 3. SCSI
SCSI 全称是 Small Computer System Interface小型机系统接口经历多代的发展从早期的 SCSI-II到目前的 Ultra320 SCSI 以及 Fiber-Channel光纤通道接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且资料传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。
SCSI 全称是 Small Computer System Interface小型机系统接口经历多代的发展从早期的 SCSI-II 到目前的 Ultra320 SCSI 以及 Fiber-Channel光纤通道接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。
<div align="center"> <img src="../pics//f0574025-c514-49f5-a591-6d6a71f271f7.jpg" width=""/> </div><br>
@ -245,7 +230,7 @@ Linux 中每个硬件都被当做一个文件,包括磁盘。磁盘以磁盘
MBR 中第一个扇区最重要里面有主要开机记录Master boot record, MBR及分区表partition table其中主要开机记录占 446 bytes分区表占 64 bytes。
分区表只有 64 bytes最多只能存储 4 个分区,这 4 个分区为主分区Primary和扩展分区Extended。其中扩展分区只有一个将其它扇区用来记录分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。
分区表只有 64 bytes最多只能存储 4 个分区,这 4 个分区为主分区Primary和扩展分区Extended。其中扩展分区只有一个使用其它扇区用记录额外的分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。
Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名 + 编号,例如 /dev/sda1。注意逻辑分区的编号从 5 开始。
@ -267,10 +252,10 @@ MBR 不支持 2.2 TB 以上的硬盘GPT 则最多支持到 2<sup>33</sup> TB
BIOSBasic Input/Output System基本输入输出系统它是一个固件嵌入在硬件中的软件BIOS 程序存放在断电后内容不会丢失的只读内存中。
BIOS 是开机的时候计算机执行的第一个程序这个程序知道可以开机的磁盘并读取磁盘第一个扇区的主要开机记录MBR由主要开机记录MBR执行其中的开机管理程序这个开机管理程序会加载操作系统的核心文件。
<div align="center"> <img src="../pics//50831a6f-2777-46ea-a571-29f23c85cc21.jpg"/> </div><br>
BIOS 是开机的时候计算机执行的第一个程序这个程序知道可以开机的磁盘并读取磁盘第一个扇区的主要开机记录MBR由主要开机记录MBR执行其中的开机管理程序这个开机管理程序会加载操作系统的核心文件。
主要开机记录MBR中的开机管理程序提供以下功能选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上在启动开机管理程序时就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。
下图中第一扇区的主要开机记录MBR中的开机管理程序提供了两个选单M1、M2M1 指向了 Windows 操作系统,而 M2 指向其它分区的启动扇区,里面包含了另外一个开机管理程序,提供了一个指向 Linux 的选单。
@ -291,8 +276,6 @@ BIOS 不可以读取 GPT 分区表,而 UEFI 可以。
## 组成
<div align="center"> <img src="../pics//BSD_disk.png" width="800"/> </div><br>
最主要的几个组成部分如下:
- inode一个文件占用一个 inode记录文件的属性同时记录此文件的内容所在的 block 编号;
@ -301,7 +284,9 @@ BIOS 不可以读取 GPT 分区表,而 UEFI 可以。
除此之外还包括:
- superblock记录文件系统的整体信息包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等;
- block bitmap记录 block 是否被使用的位域;
- block bitmap记录 block 是否被使用的位域。
<div align="center"> <img src="../pics//BSD_disk.png" width="800"/> </div><br>
## 文件读取
@ -352,7 +337,9 @@ inode 中记录了文件内容所在的 block 编号,但是每个 block 非常
## 目录
建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。
建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。
可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。
## 日志
@ -657,7 +644,7 @@ locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内
example: find . -name "shadow*"
```
(一)与时间有关的选项
**① 与时间有关的选项**
```html
-mtime n :列出在 n 天前的那一天修改过内容的文件
@ -670,7 +657,7 @@ example: find . -name "shadow*"
<div align="center"> <img src="../pics//658fc5e7-79c0-4247-9445-d69bf194c539.png" width=""/> </div><br>
(二)与文件拥有者和所属群组有关的选项
**② 与文件拥有者和所属群组有关的选项**
```html
-uid n
@ -681,7 +668,7 @@ example: find . -name "shadow*"
-nogroup搜索所属群组不存在于 /etc/group 的文件
```
(三)与文件权限和名称有关的选项
**③ 与文件权限和名称有关的选项**
```html
-name filename
@ -836,7 +823,7 @@ $ echo ${array[1]}
- 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls
- 由别名找到该指令来执行;
- 由 Bash 内的指令来执行;
- 由 Bash 内的指令来执行;
- 按 \$PATH 变量指定的搜索路径的顺序找到第一个指令来执行。
## 数据流重定向
@ -859,11 +846,11 @@ $ echo ${array[1]}
$ find /home -name .bashrc > list 2>&1
```
# 八、管线指令
# 八、管指令
线是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管线
是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管
在命令之间使用 | 分隔各个管线命令。
在命令之间使用 | 分隔各个管命令。
```bash
$ ls -al /etc | less
@ -1025,10 +1012,10 @@ g/re/pglobally search a regular expression and print),使用正则表示式
```html
$ grep [-acinv] [--color=auto] 搜寻字符串 filename
-c 算找到个数
-c 计个数
-i 忽略大小写
-n 输出行号
-v 反向选择,亦即显示出没有 搜寻字符串 内容的那一行
-v 反向选择,也就是显示出没有 搜寻字符串 内容的那一行
--color=auto :找到的关键字加颜色显示
```
@ -1051,9 +1038,7 @@ $ grep -n 'go\{2,5\}g' regular_express.txt
## printf
用于格式化输出。
它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。
用于格式化输出。它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。
```html
$ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt)
@ -1068,7 +1053,7 @@ $ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt)
awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$nn 为字段号,从 1 开始,\$0 表示一整行。
示例 1取出登录用户的用户名和 ip
示例:取出最近五个登录用户的用户名和 IP
```html
$ last -n 5
@ -1077,8 +1062,10 @@ dmtsai pts/0 192.168.1.100 Thu Jul 9 23:36 - 02:58 (03:22)
dmtsai pts/0 192.168.1.100 Thu Jul 9 17:23 - 23:36 (06:12)
dmtsai pts/0 192.168.1.100 Thu Jul 9 08:02 - 08:17 (00:14)
dmtsai tty1 Fri May 29 11:55 - 12:11 (00:15)
```
$ last -n 5 | awk '{print $1 "\t" $3}
```html
$ last -n 5 | awk '{print $1 "\t" $3}'
```
可以根据字段的某些条件进行匹配,例如匹配字段小于某个值的那一行数据。
@ -1087,7 +1074,7 @@ $ last -n 5 | awk '{print $1 "\t" $3}
$ awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename
```
示例 2/etc/passwd 文件第三个字段为 UID对 UID 小于 10 的数据进行处理。
示例:/etc/passwd 文件第三个字段为 UID对 UID 小于 10 的数据进行处理。
```text
$ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}'
@ -1104,7 +1091,7 @@ awk 变量:
| NR | 目前所处理的是第几行数据 |
| FS | 目前的分隔字符,默认是空格键 |
示例 3输出正在处理的行号并显示每一行有多少字段
示例:显示正在处理的行号以及每一行有多少字段
```html
$ last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}'
@ -1125,56 +1112,54 @@ dmtsai lines: 5 columns: 9
示例一:查看自己的进程
```
```sh
# ps -l
```
示例二:查看系统所有进程
```
```sh
# ps aux
```
示例三:查看特定的进程
```
```sh
# ps aux | grep threadx
```
### 2. top
实时显示进程信息
示例:两秒钟刷新一次
```
# top -d 2
```
### 3. pstree
### 2. pstree
查看进程树
示例:查看所有进程树
```
```sh
# pstree -A
```
### 3. top
实时显示进程信息
示例:两秒钟刷新一次
```sh
# top -d 2
```
### 4. netstat
查看占用端口的进程
示例:查看特定端口的进程
```
```sh
# netstat -anp | grep port
```
## 进程状态
<div align="center"> <img src="../pics//76a49594323247f21c9b3a69945445ee.png" width=""/> </div><br>
| 状态 | 说明 |
| :---: | --- |
| R | running or runnable (on run queue) |
@ -1182,19 +1167,21 @@ dmtsai lines: 5 columns: 9
| S | interruptible sleep (waiting for an event to complete) |
| Z | zombie (terminated but not reaped by its parent) |
| T | stopped (either by a job control signal or because it is being traced) |
<br>
<div align="center"> <img src="../pics//76a49594323247f21c9b3a69945445ee.png" width=""/> </div><br>
## SIGCHLD
当一个子进程改变了它的状态时:停止运行,继续运行或者退出,有两件事会发生在父进程中:
当一个子进程改变了它的状态时(停止运行,继续运行或者退出),有两件事会发生在父进程中:
- 得到 SIGCHLD 信号;
- waitpid() 或者 wait() 调用会返回。
<div align="center"> <img src="../pics//flow.png" width=""/> </div><br>
其中子进程发送的 SIGCHLD 信号包含了子进程的信息,包含了进程 ID、进程状态、进程使用 CPU 的时间等。
在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息。父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。
在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。
<div align="center"> <img src="../pics//flow.png" width=""/> </div><br>
## wait()
@ -1206,11 +1193,7 @@ pid_t wait(int *status)
如果成功,返回被收集的子进程的进程 ID如果调用进程没有子进程调用就会失败此时返回 -1同时 errno 被置为 ECHILD。
参数 status 用来保存被收集的子进程退出时的一些状态,如果对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL
```c
pid = wait(NULL);
```
参数 status 用来保存被收集的子进程退出时的一些状态,如果对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL。
## waitpid()
@ -1238,9 +1221,9 @@ options 参数主要有 WNOHANG 和 WUNTRACED 两个选项WNOHANG 可以使 w
僵尸进程通过 ps 命令显示出来的状态为 Zzombie
系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。
系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。
要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵进程所占有的资源,从而结束僵尸进程。
要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵进程所占有的资源,从而结束僵尸进程。
# 参考资料

View File

@ -1,27 +1,28 @@
<!-- GFM-TOC -->
* [一、存储引擎](#一存储引擎)
* [一、索引](#一索引)
* [B+ Tree 原理](#b-tree-原理)
* [MySQL 索引](#mysql-索引)
* [索引优化](#索引优化)
* [索引的优点](#索引的优点)
* [索引的使用条件](#索引的使用条件)
* [二、查询性能优化](#二查询性能优化)
* [使用 Explain 进行分析](#使用-explain-进行分析)
* [优化数据访问](#优化数据访问)
* [重构查询方式](#重构查询方式)
* [三、存储引擎](#三存储引擎)
* [InnoDB](#innodb)
* [MyISAM](#myisam)
* [比较](#比较)
* [二、数据类型](#二数据类型)
* [四、数据类型](#四数据类型)
* [整型](#整型)
* [浮点数](#浮点数)
* [字符串](#字符串)
* [时间和日期](#时间和日期)
* [三、索引](#三索引)
* [B Tree 原理](#b-tree-原理)
* [索引分类](#索引分类)
* [索引的优点](#索引的优点)
* [索引优化](#索引优化)
* [四、查询性能优化](#四查询性能优化)
* [使用 Explain 进行分析](#使用-explain-进行分析)
* [优化数据访问](#优化数据访问)
* [重构查询方式](#重构查询方式)
* [五、切分](#五切分)
* [水平切分](#水平切分)
* [垂直切分](#垂直切分)
* [Sharding 策略](#sharding-策略)
* [Sharding 存在的问题及解决方案](#sharding-存在的问题及解决方案)
* [Sharding 存在的问题](#sharding-存在的问题)
* [六、复制](#六复制)
* [主从复制](#主从复制)
* [读写分离](#读写分离)
@ -29,171 +30,59 @@
<!-- GFM-TOC -->
# 一、存储引擎
# 一、索引
## InnoDB
## B+ Tree 原理
InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。
### 1. 数据结构
实现了四个标准的隔离级别默认级别是可重复读REPEATABLE READ。在可重复读隔离级别下通过多版本并发控制MVCC+ 间隙锁next-key locking防止幻影读
B Tree 指的是 Balance Tree也就是平衡树。平衡树是一颗查找树并且所有叶子节点位于同一层
主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升
B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能
内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。
支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。
## MyISAM
MyISAM 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用 MyISAM。
MyISAM 提供了大量的特性,包括压缩表、空间数据索引等。
不支持事务。
不支持行级锁只能对整张表加锁读取时会对需要读到的所有表加共享锁写入时则对表加排它锁。但在表有读取操作的同时也可以往表中插入新的记录这被称为并发插入CONCURRENT INSERT
可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。
如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。
## 比较
- 事务InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。
- 并发MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
- 外键InnoDB 支持外键。
- 备份InnoDB 支持在线热备份。
- 崩溃恢复MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。
- 其它特性MyISAM 支持压缩表和空间数据索引。
# 二、数据类型
## 整型
TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。
INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。
## 浮点数
FLOAT 和 DOUBLE 为浮点类型DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。
FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。
## 字符串
主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。
VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长当超出一个页所能容纳的大小时就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。
VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。
## 时间和日期
MySQL 提供了两种相似的日期时间类型DATATIME 和 TIMESTAMP。
### 1. DATATIME
能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。
它与时区无关。
默认情况下MySQL 以一种可排序的、无歧义的格式显示 DATATIME 值例如“2008-01-16 22:37:08”这是 ANSI 标准定义的日期和时间表示方法。
### 2. TIMESTAMP
和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。
它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。
MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。
默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。
应该尽量使用 TIMESTAMP因为它比 DATETIME 空间效率更高。
# 三、索引
索引能够轻易将查询性能提升几个数量级。
对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。对于中到大型的表,索引就非常有效。但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。
索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。
## B Tree 原理
### 1. B-Tree
<div align="center"> <img src="../pics//06976908-98ab-46e9-a632-f0c2760ec46c.png"/> </div><br>
定义一条数据记录为一个二元组 [key, data]B-Tree 是满足下列条件的数据结构:
- 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的;
- 一个节点中的 key 从左到右非递减排列;
- 如果某个指针的左右相邻 key 分别是 key<sub>i</sub> 和 key<sub>i+1</sub>,且不为 null则该指针指向节点的所有 key 大于等于 key<sub>i</sub> 且小于等于 key<sub>i+1</sub>
查找算法:首先在根节点进行二分查找,如果找到则返回对应节点的 data否则在相应区间的指针指向的节点递归进行查找。
由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、旋转等操作以保持 B-Tree 性质。
### 2. B+Tree
<div align="center"> <img src="../pics//7299afd2-9114-44e6-9d5e-4025d0b2a541.png"/> </div><br>
与 B-Tree 相比B+Tree 有以下不同点:
- 每个节点的指针上限为 2d 而不是 2d+1d 为节点的出度);
- 内节点不存储 data只存储 key
- 叶子节点不存储指针。
### 3. 顺序访问指针
在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 key<sub>i</sub> 和 key<sub>i+1</sub>,且不为 null则该指针指向节点的所有 key 大于等于 key<sub>i</sub> 且小于等于 key<sub>i+1</sub>
<div align="center"> <img src="../pics//061c88c1-572f-424f-b580-9cbce903a3fe.png"/> </div><br>
一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。
### 2. 操作
### 4. 优势
进行查找操作时,首先在根节点进行二分查找,找到一个 key 所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的 data。
红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B Tree 作为索引结构,主要有以下两个原因:
插入删除操作会破坏平衡树的平衡性,因此在插入删除操作之后,需要对树进行一个分裂、合并、旋转等操作来维护平衡性。
(一)更少的检索次数
### 3. 与红黑树的比较
平衡树检索数据的时间复杂度等于树高 h而树高大致为 O(h)=O(log<sub>d</sub>N),其中 d 为每个节点的出度。
红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构,主要有以下两个原因:
红黑树的出度为 2而 B Tree 的出度一般都非常大。红黑树的树高 h 很明显比 B Tree 大非常多,因此检索的次数也就更多。
(一)更少的查找次数
B+Tree 相比于 B-Tree 更适合外存索引,因为 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,检索效率会更高
平衡树查找操作的时间复杂度等于树高 h而树高大致为 O(h)=O(log<sub>d</sub>N),其中 d 为每个节点的出度。
(二)利用计算机预读特性
红黑树的出度为 2而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多,查找的次数也就更多。
为了减少磁盘 I/O磁盘往往不是严格按需读取而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理当一个数据被用到时其附近的数据也通常会马上被使用。预读过程中磁盘进行顺序读取顺序读取不需要进行磁盘寻道并且只需要很短的旋转时间因此速度会非常快。
(二)利用磁盘预读特性
操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,相邻的节点也能够被预先载入
为了减少磁盘 I/O磁盘往往不是严格按需读取而是每次都会预读。预读过程中磁盘进行顺序读取顺序读取不需要进行磁盘寻道并且只需要很短的旋转时间速度会非常快。
更多内容请参考:[MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点。并且可以利用预读特性,相邻的节点也能够被预先载入。
## 索引分类
## MySQL 索引
索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。
### 1. B+Tree 索引
B+Tree 索引是大多数 MySQL 存储引擎的默认索引类型。
是大多数 MySQL 存储引擎的默认索引类型。
因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。除了用于查找,还可以用于排序和分组。
因为不再需要进行全表扫描,只需要对树进行搜索即可,所以查找速度快很多。
除了用于查找,还可以用于排序和分组。
可以指定多个列作为索引列,多个索引列共同组成键。
B+Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。
适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。
如果不是按照索引列的顺序进行查找,则无法使用索引。
InnoDB 的 B+Tree 索引分为主索引和辅助索引。
主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
InnoDB 的 B+Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
<div align="center"> <img src="../pics//c28c6fbc-2bc1-47d9-9b2e-cf3d4034f877.jpg"/> </div><br>
@ -203,35 +92,29 @@ InnoDB 的 B+Tree 索引分为主索引和辅助索引。
### 2. 哈希索引
InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
哈希索引能以 O(1) 时间进行查找,但是失去了有序性,它具有以下限制:
哈希索引能以 O(1) 时间进行查找,但是失去了有序性:
- 无法用于排序与分组;
- 只支持精确查找,无法用于部分查找和范围查找;
- 只支持精确查找,无法用于部分查找和范围查找。
InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
### 3. 全文索引
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。查找条件使用 MATCH AGAINST而不是普通的 WHERE。
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。
全文索引一般使用倒排索引实现,它记录着关键词到其所在文档的映射。
查找条件使用 MATCH AGAINST而不是普通的 WHERE。
全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。
InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
### 4. 空间数据索引R-Tree
### 4. 空间数据索引
MyISAM 存储引擎支持空间数据索引,可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。
MyISAM 存储引擎支持空间数据索引R-Tree,可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。
必须使用 GIS 相关的函数来维护数据。
## 索引的优点
- 大大减少了服务器需要扫描的数据行数。
- 帮助服务器避免进行排序和创建临时表B+Tree 索引是有序的,可以用来做 ORDER BY 和 GROUP BY 操作);
- 将随机 I/O 变为顺序 I/OB+Tree 索引是有序的,也就将相邻的数据都存储在一起)。
## 索引优化
### 1. 独立的列
@ -255,7 +138,9 @@ WHERE actor_id = 1 AND film_id = 1;
### 3. 索引列的顺序
让选择性最强的索引列放在前面,索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1此时每个记录都有唯一的索引与其对应。选择性越高查询效率也越高。
让选择性最强的索引列放在前面。
索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1此时每个记录都有唯一的索引与其对应。选择性越高查询效率也越高。
例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。
@ -284,11 +169,27 @@ customer_id_selectivity: 0.0373
具有以下优点:
- 因为索引条目通常远小于数据行的大小,所以若只读取索引能大大减少数据访问量。
- 索引通常远小于数据行的大小,只读取索引能大大减少数据访问量。
- 一些存储引擎(例如 MyISAM在内存中只缓存索引而数据依赖于操作系统来缓存。因此只访问索引可以不使用系统调用通常比较费时
- 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。
# 四、查询性能优化
## 索引的优点
- 大大减少了服务器需要扫描的数据行数。
- 帮助服务器避免进行排序和分组以及避免创建临时表B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,因为不需要排序和分组,也就不需要创建临时表)。
- 将随机 I/O 变为顺序 I/OB+Tree 索引是有序的,会将相邻的数据都存储在一起)。
## 索引的使用条件
- 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效;
- 对于中到大型的表,索引就非常有效;
- 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。
# 二、查询性能优化
## 使用 Explain 进行分析
@ -300,23 +201,13 @@ Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explai
- key : 使用的索引
- rows : 扫描的行数
更多内容请参考:[MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735)
## 优化数据访问
### 1. 减少请求的数据量
(一)只返回必要的列
最好不要使用 SELECT * 语句。
(二)只返回必要的行
使用 WHERE 语句进行查询过滤,有时候也需要使用 LIMIT 语句来限制返回的数据。
(三)缓存重复查询的数据
使用缓存可以避免在数据库中进行查询,特别要查询的数据经常被重复查询,缓存可以带来的查询性能提升将会是非常明显的。
- 只返回必要的列:最好不要使用 SELECT * 语句。
- 只返回必要的行:使用 LIMIT 语句来限制返回的数据。
- 缓存重复查询的数据:使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的。
### 2. 减少服务器端扫描的行数
@ -342,12 +233,12 @@ do {
### 2. 分解大连接查询
将一个大连接查询JOIN分解成对每一个表进行一次单表查询,然后将结果在应用程序中进行关联,这样做的好处有:
将一个大连接查询分解成对每一个表进行一次单表查询,然后在应用程序中进行关联,这样做的好处有:
- 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。
- 减少锁竞争;
- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可扩展
- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可伸缩
- 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
```sql
@ -363,50 +254,133 @@ SELECT * FROM tag_post WHERE tag_id=1234;
SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
```
# 三、存储引擎
## InnoDB
是 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。
实现了四个标准的隔离级别默认级别是可重复读REPEATABLE READ。在可重复读隔离级别下通过多版本并发控制MVCC+ 间隙锁Next-Key Locking防止幻影读。
主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。
内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。
支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。
## MyISAM
设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。
提供了大量的特性,包括压缩表、空间数据索引等。
不支持事务。
不支持行级锁只能对整张表加锁读取时会对需要读到的所有表加共享锁写入时则对表加排它锁。但在表有读取操作的同时也可以往表中插入新的记录这被称为并发插入CONCURRENT INSERT
可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。
如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。
## 比较
- 事务InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。
- 并发MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
- 外键InnoDB 支持外键。
- 备份InnoDB 支持在线热备份。
- 崩溃恢复MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。
- 其它特性MyISAM 支持压缩表和空间数据索引。
# 四、数据类型
## 整型
TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。
INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。
## 浮点数
FLOAT 和 DOUBLE 为浮点类型DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。
FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。
## 字符串
主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。
VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长当超出一个页所能容纳的大小时就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。
VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。
## 时间和日期
MySQL 提供了两种相似的日期时间类型DATETIME 和 TIMESTAMP。
### 1. DATETIME
能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。
它与时区无关。
默认情况下MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值例如“2008-01-16 22:37:08”这是 ANSI 标准定义的日期和时间表示方法。
### 2. TIMESTAMP
和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年到 2038 年。
它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。
MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。
默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。
应该尽量使用 TIMESTAMP因为它比 DATETIME 空间效率更高。
# 五、切分
## 水平切分
<div align="center"> <img src="../pics//63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg"/> </div><br>
水平切分又称为 Sharding它是将同一个表中的记录拆分到多个结构相同的表中。
当一个表的数据不断增多时Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。
## 垂直切分
<div align="center"> <img src="../pics//63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg"/> </div><br>
<div align="center"> <img src="../pics//e130e5b8-b19a-4f1e-b860-223040525cf6.jpg"/> </div><br>
## 垂直切分
垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。
在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库 payDB、用户数据库 userDB 等。
在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。
<div align="center"> <img src="../pics//e130e5b8-b19a-4f1e-b860-223040525cf6.jpg"/> </div><br>
## Sharding 策略
- 哈希取模hash(key) % NUM_DB
- 范围:可以是 ID 范围也可以是时间范围
- 映射表:使用单独的一个数据库来存储映射关系
- 哈希取模hash(key) % N
- 范围:可以是 ID 范围也可以是时间范围
- 映射表:使用单独的一个数据库来存储映射关系
## Sharding 存在的问题及解决方案
## Sharding 存在的问题
### 1. 事务问题
使用分布式事务来解决,比如 XA 接口。
### 2. JOIN
### 2. 连接
可以将原来的 JOIN 查询分解成多个单表查询,然后在用户程序中进行 JOIN。
可以将原来的连接分解成多个单表连接查询,然后在用户程序中进行连接
### 3. ID 唯一性
- 使用全局唯一 IDGUID。
- 为每个分片指定一个 ID 范围。
- 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)。
更多内容请参考:
- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6)
- [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html)
- 使用全局唯一 IDGUID
- 为每个分片指定一个 ID 范围
- 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)
# 六、复制
@ -414,24 +388,24 @@ SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
主要涉及三个线程binlog 线程、I/O 线程和 SQL 线程。
- **binlog 线程** :负责将主服务器上的数据更改写入二进制文件binlog中。
- **I/O 线程** :负责从主服务器上读取二进制日志文件,并写入从服务器的中继日志中。
- **SQL 线程** :负责读取中继日志并重放其中的 SQL 语句。
- **binlog 线程** :负责将主服务器上的数据更改写入二进制日志Binary log中。
- **I/O 线程** :负责从主服务器上读取二进制日志并写入从服务器的重放日志Replay log中。
- **SQL 线程** :负责读取重放日志并重放其中的 SQL 语句。
<div align="center"> <img src="../pics//master-slave.png"/> </div><br>
## 读写分离
主服务器用来处理写操作以及实时性要求比较高的读操作,而从服务器用来处理读操作。
主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。
读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。
MySQL 读写分离能提高性能的原因在于:
读写分离能提高性能的原因在于:
- 主从服务器负责各自的读和写,极大程度缓解了锁的争用;
- 从服务器可以配置 MyISAM 引擎,提升查询性能以及节约系统开销;
- 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销;
- 增加冗余,提高可用性。
读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。
<div align="center"> <img src="../pics//master-slave-proxy.png"/> </div><br>
# 参考资料
@ -442,3 +416,7 @@ MySQL 读写分离能提高性能的原因在于:
- [服务端指南 数据存储篇 | MySQL09 分库与分表带来的分布式困境与应对之策](http://blog.720ui.com/2017/mysql_core_09_multi_db_table2/ "服务端指南 数据存储篇 | MySQL09 分库与分表带来的分布式困境与应对之策")
- [How to create unique row ID in sharded databases?](https://stackoverflow.com/questions/788829/how-to-create-unique-row-id-in-sharded-databases)
- [SQL Azure Federation Introduction](http://geekswithblogs.net/shaunxu/archive/2012/01/07/sql-azure-federation-ndash-introduction.aspx "Title of this entry.")
- [MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
- [MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735)
- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6)
- [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html)

View File

@ -49,7 +49,7 @@
Redis 是速度非常快的非关系型NoSQL内存键值数据库可以存储键和五种不同类型的值之间的映射。
键的类型只能为字符串,值支持五种类型数据类型:字符串、列表、集合、有序集合、散列表
键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。
Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
@ -58,7 +58,7 @@ Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,
| 数据类型 | 可以存储的值 | 操作 |
| :--: | :--: | :--: |
| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作</br> 对整数和浮点数执行自增或者自减操作 |
| LIST | 列表 | 从两端压入或者弹出元素</br> 读取单个或者多个元素</br> 进行修剪,只保留一个范围内的元素 |
| LIST | 列表 | 从两端压入或者弹出元素 </br>单个或者多个元素</br> 进行修剪,只保留一个范围内的元素 |
| SET | 无序集合 | 添加、获取、移除单个元素</br> 检查一个元素是否存在于集合中</br> 计算交集、并集、差集</br> 从集合里面随机获取元素 |
| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对</br> 获取所有键值对</br> 检查某个键是否存在|
| ZSET | 有序集合 | 添加、获取、删除元素</br> 根据分值范围或者成员来获取元素</br> 计算一个键的排名 |
@ -357,7 +357,7 @@ List 是一个双向链表,可以通过 lpop 和 lpush 写入和读取消息
在分布式场景下具有多个应用服务器,可以使用 Redis 来统一存储这些应用服务器的会话信息。
当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器。
当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性
## 分布式锁实现
@ -393,7 +393,7 @@ Redis Cluster 实现了分布式的支持。
- 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。
- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
# 六、键的过期时间
@ -416,10 +416,12 @@ Reids 具体有 6 种淘汰策略:
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
| noeviction | 禁止驱逐数据 |
作为内存数据库出于对性能和内存消耗的考虑Redis 的淘汰算法实际实现上并非针对所有 key而是抽样一小部分 key 从中选出被淘汰 key。
作为内存数据库出于对性能和内存消耗的考虑Redis 的淘汰算法实际实现上并非针对所有 key而是抽样一小部分并且从中选出被淘汰的 key。
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。
# 八、持久化
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
@ -438,7 +440,7 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需
将写命令添加到 AOF 文件Append Only File的末尾。
使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上。这是因为对硬盘的文件进行写入并不会马上将内容同步到磁盘文件上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到盘。有以下同步选项:
使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到盘。有以下同步选项:
| 选项 | 同步频率 |
| :--: | :--: |
@ -447,8 +449,8 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需
| no | 让操作系统来决定何时同步 |
- always 选项会严重减低服务器的性能;
- everysec 选项比较合适,可以保证系统溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统溃时数据丢失的数量。
- everysec 选项比较合适,可以保证系统溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统溃时数据丢失的数量。
随着服务器写请求的增多AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
@ -547,13 +549,16 @@ def main():
# 十二、Sentinel
Sentinel哨兵可以监听服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。
Sentinel哨兵可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。
# 十三、分片
分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。
分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。
假设有 4 个 Reids 实例 R0R1R2R3还有很多表示用户的键 user:1user:2... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
假设有 4 个 Reids 实例 R0R1R2R3还有很多表示用户的键 user:1user:2... ,有不同的方式来选择一个指定的键存储在哪个实例中。
- 最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。
- 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
根据执行分片的位置,可以分为三种分片方式:
@ -600,4 +605,4 @@ Redis 没有关系型数据库中的表这一概念来将同种类型的数据
- [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html)
- [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide)
- [Redis 应用场景](http://www.scienjus.com/redis-use-case/)
- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/)
- [Using Redis as an LRU cache](https://redis.io/topics/lru-cache)

View File

@ -19,7 +19,7 @@
* [十八、存储过程](#十八存储过程)
* [十九、游标](#十九游标)
* [二十、触发器](#二十触发器)
* [二十一、事务处理](#二十一事务处理)
* [二十一、事务管理](#二十一事务管理)
* [二十二、字符集](#二十二字符集)
* [二十三、权限管理](#二十三权限管理)
* [参考资料](#参考资料)
@ -553,7 +553,7 @@ WHERE col5 = val;
# 十八、存储过程
存储过程可以看成是对一系列 SQL 操作的批处理
存储过程可以看成是对一系列 SQL 操作的批处理
使用存储过程的好处:
@ -642,11 +642,11 @@ SELECT @result; -- 获取结果
DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。
UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改,而 OLD 是只读的。
UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改,而 OLD 是只读的。
MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。
# 二十一、事务
# 二十一、事务
基本术语:
@ -708,12 +708,12 @@ SELECT user FROM user;
**创建账户**
新创建的账户没有任何权限。
```sql
CREATE USER myuser IDENTIFIED BY 'mypassword';
```
新创建的账户没有任何权限。
**修改账户名**
```sql
@ -734,18 +734,14 @@ SHOW GRANTS FOR myuser;
**授予权限**
账户用 username@host 的形式定义username@% 使用的是默认主机名。
```sql
GRANT SELECT, INSERT ON mydatabase.* TO myuser;
```
账户用 username@host 的形式定义username@% 使用的是默认主机名。
**删除权限**
```sql
REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;
```
GRANT 和 REVOKE 可在几个层次上控制访问权限:
- 整个服务器,使用 GRANT ALL 和 REVOKE ALL
@ -754,6 +750,10 @@ GRANT 和 REVOKE 可在几个层次上控制访问权限:
- 特定的列;
- 特定的存储过程。
```sql
REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;
```
**更改密码**
必须使用 Password() 函数

View File

@ -25,9 +25,9 @@
- 等待数据准备好
- 从内核向进程复制数据
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
Unix 有五种 I/O 模型:
Unix 有五种 I/O 模型:
- 阻塞式 I/O
- 非阻塞式 I/O
@ -39,7 +39,7 @@ Unix 下有五种 I/O 模型:
应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。
应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。
应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的 CPU 利用率效率会比较高。
下图中recvfrom 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。
@ -53,13 +53,13 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成这种方式称为轮询polling
由于 CPU 要处理更多的系统调用,因此这种模型是比较低的。
由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率是比较低的。
<div align="center"> <img src="../pics//1492929000361_5.png"/> </div><br>
## I/O 复用
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O即事件驱动 I/O。
@ -77,7 +77,7 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *
## 异步 I/O
进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
应用程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
@ -198,7 +198,7 @@ else
select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。
- select 会修改描述符,而 poll 不会;
- select 的描述符类型使用数组实现FD_SETSIZE 大小默认为 1024因此默认只能监听 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 的描述符类型使用链表实现,没有描述符数量的限制;
- select 的描述符类型使用数组实现FD_SETSIZE 大小默认为 1024因此默认只能监听 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 的描述符类型使用链表实现,没有描述符数量的限制;
- poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。
- 如果一个线程对某个描述符调用了 select 或者 poll另一个线程关闭了该描述符会导致调用结果不确定。

View File

@ -19,31 +19,32 @@
* [五、Paxos](#五paxos)
* [执行过程](#执行过程)
* [约束条件](#约束条件)
* [五、Raft](#五raft)
* [六、Raft](#六raft)
* [单个 Candidate 的竞选](#单个-candidate-的竞选)
* [多个 Candidate 竞选](#多个-candidate-竞选)
* [日志复制](#日志复制)
* [数据同步](#数据同步)
* [参考](#参考)
<!-- GFM-TOC -->
# 一、分布式锁
在单机场景下,可以使用 Java 提供的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。
在单机场景下,可以使用语言的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。
阻塞锁通常使用互斥量来实现:
- 互斥量为 1 表示有其它进程在使用锁,此时处于锁定状态;
- 互斥量为 0 表示未锁定状态。
- 互斥量为 0 表示有其它进程在使用锁,此时处于锁定状态;
- 互斥量为 1 表示未锁定状态。
1 和 0 可以用一个整型值表示,也可以用某个数据存在或者不存在表示,存在表示互斥量为 1
1 和 0 可以用一个整型值表示,也可以用某个数据是否存在表示
## 数据库的唯一索引
当想要获得锁时,就向表中插入一条记录,释放锁时删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。
获得锁时向表中插入一条记录,释放锁时删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。
存在以下几个问题:
- 锁没有失效时间,解锁失败的话其它进程无法再获得锁。
- 锁没有失效时间,解锁失败的话其它进程无法再获得锁。
- 只能是非阻塞锁,插入失败直接就报错了,无法重试。
- 不可重入,已经获得锁的进程也必须重新获取锁。
@ -59,15 +60,15 @@ EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了
使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。
- 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个
- 尝试从 N 个相互独立 Redis 实例获取锁;
- 计算获取锁消耗的时间只有当这个时间小于锁的过期时间并且从大多数N / 2 + 1实例上获取了锁那么就认为锁获取成功了
- 如果锁获取失败,到每个实例上释放锁。
- 如果锁获取失败,到每个实例上释放锁。
## Zookeeper 的有序节点
### 1. Zookeeper 抽象模型
Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。
Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点的父节点为 /app1。
<div align="center"> <img src="../pics//31d99967-1171-448e-8531-bccf5c14cffe.jpg" width="400"/> </div><br>
@ -90,27 +91,21 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示
### 5. 会话超时
如果一个已经获得锁的会话超时了因为创建的是临时节点所以该会话对应的临时节点会被删除其它会话就可以获得锁了。可以看到Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁释放锁失败问题。
如果一个已经获得锁的会话超时了因为创建的是临时节点所以该会话对应的临时节点会被删除其它会话就可以获得锁了。可以看到Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁释放锁失败问题。
### 6. 羊群效应
一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。
参考:
- [Distributed locks with Redis](https://redis.io/topics/distlock)
- [浅谈分布式锁](http://www.linkedkeeper.com/detail/blog.action?bid=1023)
- [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html)
一个节点未获得锁,只需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。
# 二、分布式事务
指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。
指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。
例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。
## 本地消息表
### 1. 原理
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性。
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
2. 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
@ -118,25 +113,19 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示
<div align="center"> <img src="../pics//e3bf5de4-ab1e-4a9b-896d-4b0ad7e9220a.jpg"/> </div><br>
### 2. 分析
本地消息表利用了本地事务来实现分布式事务,并且使用了消息队列来保证最终一致性。
## 2PC
两阶段提交Two-phase Commit2PC
通过引入协调者Coordinator来协调参与者的行为并最终决定这些参与者是否要真正执行事务。
两阶段提交Two-phase Commit2PC通过引入协调者Coordinator来协调参与者的行为并最终决定这些参与者是否要真正执行事务。
### 1. 运行过程
(一)准备阶段
#### 1.1 准备阶段
协调者询问参与者事务是否执行成功,参与者发回事务执行结果。
<div align="center"> <img src="../pics//04f41228-375d-4b7d-bfef-738c5a7c8f07.jpg"/> </div><br>
(二)提交阶段
#### 1.2 提交阶段
如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
@ -146,28 +135,22 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示
### 2. 存在的问题
(一)同步阻塞
#### 2.1 同步阻塞
所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
(二)单点问题
#### 2.2 单点问题
协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。
协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。
(三)数据不一致
#### 2.3 数据不一致
在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
(四)太过保守
#### 2.4 太过保守
任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
参考:
- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html)
- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html)
- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be)
# 三、CAP
分布式系统不可能同时满足一致性CConsistency、可用性AAvailability和分区容忍性PPartition Tolerance最多只能同时满足其中两项。
@ -176,9 +159,7 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示
## 一致性
一致性指的是多个数据副本是否能保持一致的特性。
在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。
一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。
对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。
@ -186,7 +167,7 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示
可用性指分布式系统在面对各种异常时可以提供正常服务的能力可以用系统可用时间占总时间的比值来衡量4 个 9 的可用性表示系统 99.99% 的时间是可用的。
在可用性条件下,系统提供的服务一直处于可用的状态,对于用户的每一个操请求总是能够在有限的时间内返回结果。
在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操请求总是能够在有限的时间内返回结果。
## 分区容忍性
@ -196,25 +177,20 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示
## 权衡
在分布式系统中分区容忍性必不可少因为需要总是假设网络是不可靠的。因此CAP 理论实际是要在可用性和一致性之间做权衡。
在分布式系统中分区容忍性必不可少因为需要总是假设网络是不可靠的。因此CAP 理论实际是要在可用性和一致性之间做权衡。
可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时,
可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时,
* 为了保证一致性CP就需要让所有节点下线成为不可用的状态等待同步完成
* 为了保证可用性AP在同步过程中允许读取所有节点的数据但是数据可能不一致。
- 为了保证一致性CP就需要让所有节点下线成为不可用的状态等待同步完成
- 为了保证可用性AP在同步过程中允许读取所有节点的数据但是数据可能不一致。
<div align="center"> <img src="../pics//0b587744-c0a8-46f2-8d72-e8f070d67b4b.jpg"/> </div><br>
参考:
- 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015.
- [What is CAP theorem in distributed database system?](http://www.colooshiki.com/index.php/2017/04/20/what-is-cap-theorem-in-distributed-database-system/)
# 四、BASE
BASE 是基本可用Basically Available、软状态Soft State和最终一致性Eventually Consistent三个短语的缩写。
BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
<div align="center"> <img src="../pics//bc603930-d74d-4499-a3e7-2d740fc07f33.png"/> </div><br>
@ -226,7 +202,7 @@ BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的理论的
## 软状态
指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在时。
指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在时
## 最终一致性
@ -294,66 +270,69 @@ Acceptor 接收到接受请求时,如果序号大于等于该 Acceptor 承诺
Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。
参考:
# 六、Raft
- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/)
- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/)
# 五、Raft
Raft 和 Paxos 类似,但是更容易理解,也更容易实现。
Raft 主要是用来竞选主节点。
Raft 也是分布式一致性协议,主要是用来竞选主节点。
## 单个 Candidate 的竞选
有三种节点Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate进入竞选阶段。
* 下图表示一个分布式系统的最初阶段,此时只有 Follower没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
- 下图展示一个分布式系统的最初阶段,此时只有 Follower 没有 Leader。Node A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
<div align="center"> <img src="../pics//111521118015898.gif"/> </div><br>
* 此时 A 发送投票请求给其它所有节点。
- 此时 Node A 发送投票请求给其它所有节点。
<div align="center"> <img src="../pics//111521118445538.gif"/> </div><br>
* 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。
- 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。
<div align="center"> <img src="../pics//111521118483039.gif"/> </div><br>
* 之后 Leader 会周期性地发送心跳包给 FollowerFollower 接收到心跳包,会重新开始计时。
- 之后 Leader 会周期性地发送心跳包给 FollowerFollower 接收到心跳包,会重新开始计时。
<div align="center"> <img src="../pics//111521118640738.gif"/> </div><br>
## 多个 Candidate 竞选
* 如果有多个 Follower 成为 Candidate并且所获得票数相同那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。
- 如果有多个 Follower 成为 Candidate并且所获得票数相同那么就需要重新开始投票。例如下图中 Node B 和 Node D 都获得两票,需要重新开始投票。
<div align="center"> <img src="../pics//111521119203347.gif"/> </div><br>
* 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此下一次再次出现多个 Candidate 并获得同样票数的概率很低。
- 由于每个节点设置的随机竞选超时时间不同,因此下一次再次出现多个 Candidate 并获得同样票数的概率很低。
<div align="center"> <img src="../pics//111521119368714.gif"/> </div><br>
## 日志复制
## 数据同步
* 来自客户端的修改都会被传入 Leader。注意该修改还未被提交只是写入日志中。
- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交只是写入日志中。
<div align="center"> <img src="../pics//7.gif"/> </div><br>
* Leader 会把修改复制到所有 Follower。
- Leader 会把修改复制到所有 Follower。
<div align="center"> <img src="../pics//9.gif"/> </div><br>
* Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。
- Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。
<div align="center"> <img src="../pics//10.gif"/> </div><br>
* 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
- 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
<div align="center"> <img src="../pics//11.gif"/> </div><br>
参考
# 参考
- 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015.
- [Distributed locks with Redis](https://redis.io/topics/distlock)
- [浅谈分布式锁](http://www.linkedkeeper.com/detail/blog.action?bid=1023)
- [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html)
- [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft)
- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html)
- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html)
- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be)
- [What is CAP theorem in distributed database system?](http://www.colooshiki.com/index.php/2017/04/20/what-is-cap-theorem-in-distributed-database-system/)
- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/)
- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/)

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@
## 危害
- 窃取用户的 Cookie
- 窃取用户的 Cookie
- 伪造虚假的输入表单骗取个人信息
- 显示伪造的文章或者图片
@ -47,7 +47,7 @@
富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。
富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,可以定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。
富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,通过定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。
以下例子中form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。
@ -131,7 +131,7 @@ http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
```
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 美元
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
@ -141,7 +141,7 @@ http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
### 1. 检查 Referer 首部字段
Referer 首部字段位于 HTTP 报文中,用于标识请求来源的地址。检查这个首部字段并要求请求来源的地址在同一个域名下,可以极大的防止 XSRF 攻击。
Referer 首部字段位于 HTTP 报文中,用于标识请求来源的地址。检查这个首部字段并要求请求来源的地址在同一个域名下,可以极大的防止 CSRF 攻击。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
@ -153,8 +153,6 @@ Referer 首部字段位于 HTTP 报文中,用于标识请求来源的地址。
因为 CSRF 攻击是在用户无意识的情况下发生的,所以要求用户输入验证码可以让用户知道自己正在做的操作。
也可以要求用户输入验证码来进行校验。
# 三、SQL 注入攻击
## 概念
@ -209,7 +207,7 @@ ResultSet rs = stmt.executeQuery();
拒绝服务攻击denial-of-service attackDoS亦称洪水攻击其目的在于使目标电脑的网络或系统资源耗尽使服务暂时中断或停止导致其正常用户无法访问。
分布式拒绝服务攻击distributed denial-of-service attackDDoS指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。
分布式拒绝服务攻击distributed denial-of-service attackDDoS指攻击者使用两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。
# 参考资料

View File

@ -20,6 +20,7 @@
* [可串行化SERIALIZABLE](#可串行化serializable)
* [五、多版本并发控制](#五多版本并发控制)
* [版本号](#版本号)
* [隐藏的列](#隐藏的列)
* [Undo 日志](#undo-日志)
* [实现过程](#实现过程)
* [快照读与当前读](#快照读与当前读)
@ -44,10 +45,10 @@
## 概念
<div align="center"> <img src="../pics//185b9c49-4c13-4241-a848-fbff85c03a64.png" width="400"/> </div><br>
事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
<div align="center"> <img src="../pics//185b9c49-4c13-4241-a848-fbff85c03a64.png" width="400"/> </div><br>
## ACID
### 1. 原子性Atomicity
@ -58,9 +59,7 @@
### 2. 一致性Consistency
数据库在事务执行前后都保持一致性状态。
在一致性状态下,所有事务对一个数据的读取结果都是相同的。
数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。
### 3. 隔离性Isolation
@ -77,11 +76,11 @@
事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系:
- 只有满足一致性,事务的执行结果才是正确的。
- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
- 在并发的情况下,多个事务并执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
- 在并发的情况下,多个事务并执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
- 事务满足持久化是为了能应对数据库崩溃的情况。
<div align="center"> <img src="../pics//a58e294a-615d-4ea0-9fbf-064a6daec4b2.png" width="400"/> </div><br>
<div align="center"> <img src="../pics//a58e294a-615d-4ea0-9fbf-064a6daec4b2.png" width="450"/> </div><br>
## AUTOCOMMIT
@ -95,25 +94,25 @@ MySQL 默认采用自动提交模式。也就是说,如果不显式使用`STAR
T<sub>1</sub> 和 T<sub>2</sub> 两个事务都对一个数据进行修改T<sub>1</sub> 先修改T<sub>2</sub> 随后修改T<sub>2</sub> 的修改覆盖了 T<sub>1</sub> 的修改。
<div align="center"> <img src="../pics//88ff46b3-028a-4dbb-a572-1f062b8b96d3.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//88ff46b3-028a-4dbb-a572-1f062b8b96d3.png" width="350"/> </div><br>
## 读脏数据
T<sub>1</sub> 修改一个数据T<sub>2</sub> 随后读取这个数据。如果 T<sub>1</sub> 撤销了这次修改,那么 T<sub>2</sub> 读取的数据是脏数据。
<div align="center"> <img src="../pics//dd782132-d830-4c55-9884-cfac0a541b8e.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//dd782132-d830-4c55-9884-cfac0a541b8e.png" width="400"/> </div><br>
## 不可重复读
T<sub>2</sub> 读取一个数据T<sub>1</sub> 对该数据做了修改。如果 T<sub>2</sub> 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
<div align="center"> <img src="../pics//c8d18ca9-0b09-441a-9a0c-fb063630d708.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//c8d18ca9-0b09-441a-9a0c-fb063630d708.png" width="350"/> </div><br>
## 幻影读
T<sub>1</sub> 读取某个范围的数据T<sub>2</sub> 在这个范围内插入新的数据T<sub>1</sub> 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
<div align="center"> <img src="../pics//72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png" width="300"/> </div><br>
<div align="center"> <img src="../pics//72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png" width="350"/> </div><br>
----
@ -123,8 +122,6 @@ T<sub>1</sub> 读取某个范围的数据T<sub>2</sub> 在这个范围内插
## 封锁粒度
<div align="center"> <img src="../pics//1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg" width="300"/> </div><br>
MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。
@ -133,6 +130,8 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。
<div align="center"> <img src="../pics//1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg" width="300"/> </div><br>
## 封锁类型
### 1. 读写锁
@ -149,8 +148,8 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
| - | X | S |
| :--: | :--: | :--: |
|X|NO|NO|
|S|NO|YES|
|X|×|×|
|S|×|√|
### 2. 意向锁
@ -169,10 +168,10 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
| - | X | IX | S | IS |
| :--: | :--: | :--: | :--: | :--: |
|X |NO |NO |NO | NO|
|IX |NO |YES |NO | YES|
|S |NO |NO |YES | YES|
|IS |NO |YES |YES | YES|
|X |× |× |× | ×|
|IX |× |√ |× | √|
|S |× |× |√ | √|
|IS |× |√ |√ | √|
解释如下:
@ -297,28 +296,30 @@ SELECT ... FOR UPDATE;
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 |
| :---: | :---: | :---:| :---: |
| 未提交读 | YES | YES | YES |
| 提交读 | NO | YES | YES |
| 可重复读 | NO | NO | YES |
| 可串行化 | NO | NO | NO |
| 未提交读 | √ | √ | √ |
| 提交读 | × | √ | √ |
| 可重复读 | × | × | √ |
| 可串行化 | × | × | × |
# 五、多版本并发控制
多版本并发控制Multi-Version Concurrency Control, MVCC是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。
多版本并发控制Multi-Version Concurrency Control, MVCC是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。
## 版本号
- 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
- 事务版本号:事务开始时的系统版本号。
InooDB 的 MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
## 隐藏的列
MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
- 创建版本号:指示创建一个数据行的快照时的系统版本号;
- 删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
## Undo 日志
InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中该日志通过回滚指针把一个数据行Record的所有快照连接起来。
MVCC 使用到的快照存储在 Undo 日志中该日志通过回滚指针把一个数据行Record的所有快照连接起来。
<div align="center"> <img src="../pics//e41405a8-7c05-4f70-8092-e961e28d3112.jpg" width=""/> </div><br>
@ -326,15 +327,13 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回
以下实现过程针对可重复读隔离级别。
### 1. SELECT
当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。
### 1. SELECT
多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
把没有对一个数据行做修改的事务称为 TT 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。
除了上面的要求T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
把没有对一个数据行做修改的事务称为 TT 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号那么表示该数据行快照是其它事务的最新修改因此不能去读取它。除此之外T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
### 2. INSERT
@ -372,15 +371,19 @@ delete;
# 六、Next-Key Locks
Next-Key Locks 也是 MySQL 的 InnoDB 存储引擎的一种锁实现。MVCC 不能解决幻读的问题Next-Key Locks 就是为了解决这个问题而存在的。在可重复读REPEATABLE READ隔离级别下使用 MVCC + Next-Key Locks 可以解决幻读问题。
Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。
MVCC 不能解决幻读的问题Next-Key Locks 就是为了解决这个问题而存在的。在可重复读REPEATABLE READ隔离级别下使用 MVCC + Next-Key Locks 可以解决幻读问题。
## Record Locks
锁定整个记录。锁定的对象是记录的索引而不是记录本身。如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Locks 依然可以使用。
锁定一个记录上的索引,而不是记录本身。
如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
## Gap Locks
锁定一个范围内的索引,例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
```sql
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
@ -388,28 +391,14 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
## Next-Key Locks
它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录,也锁定范围内的索引。在 user 中有以下记录
它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引也锁定索引之间的间隙。例如一个索引包含以下值10, 11, 13, and 20那么就需要锁定以下区间
```sql
| id | last_name | first_name | age |
|------|-------------|--------------|-------|
| 4 | stark | tony | 21 |
| 1 | tom | hiddleston | 30 |
| 3 | morgan | freeman | 40 |
| 5 | jeff | dean | 50 |
| 2 | donald | trump | 80 |
+------|-------------|--------------|-------+
```
那么就需要锁定以下范围:
```sql
(-∞, 21]
(21, 30]
(30, 40]
(40, 50]
(50, 80]
(80, ∞)
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
```
# 七、关系数据库设计理论
@ -420,7 +409,7 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
如果 {A1A2... An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。
对于 A->B如果能找到 A 的真子集 A',使得 A'-> B那么 A->B 就是部分函数依赖,否则就是完全函数依赖
对于 A->B如果能找到 A 的真子集 A',使得 A'-> B那么 A->B 就是部分函数依赖,否则就是完全函数依赖
对于 A->BB->C则 A->C 是一个传递函数依赖。
@ -437,10 +426,10 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
不符合范式的关系,会产生很多异常,主要有以下四种异常:
- 冗余数据:例如 学生-2 出现了两次。
- 冗余数据:例如 `学生-2` 出现了两次。
- 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。
- 删除异常:删除一个信息,那么也会丢失其它信息。例如如果删除了 课程-1需要删除第一行和第三行那么 学生-1 的信息就会丢失。
- 插入异常例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
- 删除异常:删除一个信息,那么也会丢失其它信息。例如删除了 `课程-1` 需要删除第一行和第三行,那么 `学生-1` 的信息就会丢失。
- 插入异常例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
## 范式
@ -452,7 +441,7 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
### 1. 第一范式 (1NF)
属性不可分
属性不可分
### 2. 第二范式 (2NF)
@ -511,7 +500,11 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门
非主属性不传递函数依赖于键码。
上面的 关系-1 中存在以下传递函数依赖Sno -> Sdept -> Mname可以进行以下分解
上面的 关系-1 中存在以下传递函数依赖:
- Sno -> Sdept -> Mname
可以进行以下分解:
关系-11
@ -538,13 +531,19 @@ Entity-Relationship有三个组成部分实体、属性、联系。
包含一对一,一对多,多对多三种。
如果 A 到 B 是一对多关系,那么画个带箭头的线段指向 B如果是一对一画两个带箭头的线段如果是多对多画两个不带箭头的线段。下图的 Course 和 Student 是一对多的关系。
- 如果 A 到 B 是一对多关系,那么画个带箭头的线段指向 B
- 如果是一对一,画两个带箭头的线段;
- 如果是多对多,画两个不带箭头的线段。
下图的 Course 和 Student 是一对多的关系。
<div align="center"> <img src="../pics//292b4a35-4507-4256-84ff-c218f108ee31.jpg" width=""/> </div><br>
## 表示出现多次的关系
一个实体在联系出现几次,就要用几条线连接。下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。
一个实体在联系出现几次,就要用几条线连接。
下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。
<div align="center"> <img src="../pics//8b798007-e0fb-420c-b981-ead215692417.jpg" width=""/> </div><br>
@ -554,7 +553,7 @@ Entity-Relationship有三个组成部分实体、属性、联系。
<div align="center"> <img src="../pics//423f2a40-bee1-488e-b460-8e76c48ee560.png" width=""/> </div><br>
一般只使用二元联系,可以把多元关系转换为二元关系。
一般只使用二元联系,可以把多元联系转换为二元联系。
<div align="center"> <img src="../pics//de9b9ea0-1327-4865-93e5-6f805c48bc9e.png" width=""/> </div><br>

140
notes/构建工具.md Normal file
View File

@ -0,0 +1,140 @@
<!-- GFM-TOC -->
* [一、构建工具的作用](#一构建工具的作用)
* [二、Java 主流构建工具](#二java-主流构建工具)
* [三、Maven](#三maven)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、构建工具的作用
构建工具是用于构建项目的自动化工具,主要包含以下工作:
## 依赖管理
不再需要手动导入 Jar 依赖包,并且可以自动处理依赖关系,也就是说某个依赖如果依赖于其它依赖,构建工具可以帮助我们自动处理这种依赖管理。
## 运行单元测试
不再需要在项目代码中添加测试代码,从而避免了污染项目代码。
## 将源代码转化为可执行文件
包含预处理、编译、汇编、链接等步骤。
## 将可执行文件进行打包
不再需要使用 IDE 将应用程序打包成 Jar 包。
## 发布到生产服务器上
不再需要通过 FTP 将 Jar 包上传到服务器上。
# 二、Java 主流构建工具
主要包括 Ant、Maven 和 Gradle。
<div align="center"> <img src="../pics//897503d0-59e3-4752-903d-529fbdb72fee.jpg"/> </div><br>
Gradle 和 Maven 的区别是,它使用 Groovy 这种特定领域语言DSL来管理构建脚本而不再使用 XML 这种标记性语言。因为项目如果庞大的话XML 很容易就变得臃肿。
例如要在项目中引入 JunitMaven 的代码如下:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jizg.study.maven.hello</groupId>
<artifactId>hello-first</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
```
而 Gradle 只需要几行代码:
```java
dependencies {
testCompile "junit:junit:4.10"
}
```
# 三、Maven
## 概述
提供了项目对象模型POM文件来管理项目的构建。
## 仓库
仓库的搜索顺序为:本地仓库、中央仓库、远程仓库。
- 本地仓库用来存储项目的依赖库;
- 中央仓库是下载依赖库的默认位置;
- 远程仓库,因为并非所有的库存储在中央仓库,或者中央仓库访问速度很慢,远程仓库是中央仓库的补充。
## POM
POM 代表项目对象模型,它是一个 XML 文件,保存在项目根目录的 pom.xml 文件中。
```xml
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
```
[groupId, artifactId, version, packaging, classfier] 称为一个项目的坐标,其中 groupId、artifactId、version 必须定义packaging 可选(默认为 Jarclassfier 不能直接定义的,需要结合插件使用。
- groupId项目组 Id必须全球唯一
- artifactId项目 Id即项目名
- version项目版本
- packaging项目打包方式。
## 依赖原则
### 1. 依赖路径最短优先原则
```html
A -> B -> C -> X(1.0)
A -> D -> X(2.0)
```
由于 X(2.0) 路径最短,所以使用 X(2.0)。
### 2. 声明顺序优先原则
```html
A -> B -> X(1.0)
A -> C -> X(2.0)
```
在 POM 中最先声明的优先,上面的两个依赖如果先声明 B那么最后使用 X(1.0)。
### 3. 覆写优先原则
子 POM 内声明的依赖优先于父 POM 中声明的依赖。
## 解决依赖冲突
找到 Maven 加载的 Jar 包版本,使用 `mvn dependency:tree` 查看依赖树,根据依赖原则来调整依赖在 POM 文件的声明顺序。
# 参考资料
- [POM Reference](http://maven.apache.org/pom.html#Dependency_Version_Requirement_Specification)
- [What is a build tool?](https://stackoverflow.com/questions/7249871/what-is-a-build-tool)
- [Java Build Tools Comparisons: Ant vs Maven vs Gradle](https://programmingmitra.blogspot.com/2016/05/java-build-tools-comparisons-ant-vs.html)
- [maven 2 gradle](http://sagioto.github.io/maven2gradle/)
- [新一代构建工具 gradle](https://www.imooc.com/learn/833)

View File

@ -23,11 +23,11 @@
# 二、匹配单个字符
正则表达式一般是区分大小写的,但是也有些实现是不区分。
**.** 可以用来匹配任何的单个字符,但是在绝大多数实现里面,不能匹配换行符;
**\\** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \ 。
**.** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \ 。
正则表达式一般是区分大小写的,但是也有些实现是不区分。
**正则表达式**
@ -43,11 +43,11 @@ My **name** is Zheng.
**[ ]** 定义一个字符集合;
0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定,字符区间只能用在 [ ] 之间
0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定,字符区间在 [ ] 中使用
**-** 元字符只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符;
**-** 只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符;
**^** 在 [ ] 字符集合中是取非操作。
**^** 在 [ ] 中是取非操作。
**应用**
@ -78,9 +78,9 @@ abc[^0-9]
| \t | 制表符 |
| \v | 垂直制表符 |
\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n \r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行;
\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n
. 是元字符前提是没有对它们进行转义f 和 n 也是元字符,但是前提是对它们进行了转义。
\r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行;
## 匹配特定的字符类别
@ -105,11 +105,13 @@ abc[^0-9]
| \s | 任何一个空白字符,等价于 [\f\n\r\t\v] |
| \S | 对 \s 取非 |
\x 匹配十六进制字符,\0 匹配八进制,例如 \x0A 对应 ASCII 字符 10 ,等价于 \n,也就是它会匹配 \n
\x 匹配十六进制字符,\0 匹配八进制,例如 \x0A 对应 ASCII 字符 10等价于 \n。
# 五、重复匹配
**\+** 匹配 1 个或者多个字符, **\*** 匹配 0 个或者多个,**?** 匹配 0 个或者 1 个。
- **\+** 匹配 1 个或者多个字符
- **\** * 匹配 0 个或者多个
- **?** 匹配 0 个或者 1 个
**应用**
@ -127,16 +129,11 @@ abc[^0-9]
**abc.def<span>@</span>qq.com**
为了可读性,常常把转义的字符放到字符集合 [ ] 中,但是含义是相同的。
- **{n}** 匹配 n 个字符
- **{m, n}** 匹配 m\~n 个字符
- **{m,}** 至少匹配 m 个字符
```
[\w.]+@\w+\.\w+
[\w.]+@[\w]+[\.][\w]+
```
**{n}** 匹配 n 个字符,**{m, n}** 匹配 m\~n 个字符,**{m,}** 至少匹配 m 个字符;
\* 和 + 都是贪婪型元字符,会匹配最多的内容,在元字符后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 。
\* 和 + 都是贪婪型元字符,会匹配最多的内容。在后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 。
**正则表达式**
@ -220,7 +217,9 @@ a.+c
**应用**
匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的:
匹配 IP 地址。
IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的:
- 一位数字
- 不以 0 开头的两位数字

View File

@ -9,6 +9,7 @@
* [三、可靠性](#三可靠性)
* [发送端的可靠性](#发送端的可靠性)
* [接收端的可靠性](#接收端的可靠性)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@ -18,7 +19,7 @@
消息生产者向消息队列中发送了一个消息之后,只能被一个消费者消费一次。
<div align="center"> <img src="../pics//09b52bcb-88ba-4e36-8244-b375f16ad116.jpg"/> </div><br>
<div align="center"> <img src="../pics//685a692f-8f76-4cac-baac-b68e2df9a30f.jpg"/> </div><br>
## 发布/订阅
@ -29,34 +30,29 @@
发布与订阅模式和观察者模式有以下不同:
- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。
- 观察者模式是同步的,当事件触发时,主题会调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,发布者向频道发送一个消息之后,就不需要关心订阅者何时去订阅这个消息。
- 观察者模式是同步的,当事件触发时,主题会调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,发布者向频道发送一个消息之后,就不需要关心订阅者何时去订阅这个消息,可以立即返回
<div align="center"> <img src="../pics//bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg"/> </div><br>
参考:
- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/)
- [消息队列中点对点与发布订阅区别](https://blog.csdn.net/lizhitao/article/details/47723105)
# 二、使用场景
## 异步处理
发送者将消息发送给消息队列之后,不需要同步等待消息接收者处理完毕,而是立即返回进行其它操作。消息接收者从消息队列中订阅消息之后异步处理。
例如在注册流程中通常需要发送验证邮件来确保注册用户身份合法,可以使用消息队列使发送验证邮件的操作异步处理,用户在填写完注册信息之后就可以完成注册,而将发送验证邮件这一消息发送到消息队列中。
例如在注册流程中通常需要发送验证邮件来确保注册用户身份合法,可以使用消息队列使发送验证邮件的操作异步处理,用户在填写完注册信息之后就可以完成注册,而将发送验证邮件这一消息发送到消息队列中。
只有在业务流程允许异步处理的情况下才能这么做,例如上面的注册流程中,如果要求用户对验证邮件进行点击之后才能完成操作的话,就不能再使用消息队列。
只有在业务流程允许异步处理的情况下才能这么做,例如上面的注册流程中,如果要求用户对验证邮件进行点击之后才能完成注册的话,就不能再使用消息队列。
## 流量削锋
在高并发的场景下,如果短时间有大量的请求会压垮服务器。
在高并发的场景下,如果短时间有大量的请求到达会压垮服务器。
可以将请求发送到消息队列中,服务器按照其处理能力从消息队列中订阅消息进行处理。
## 应用解耦
如果模块之间不直接进行调用,模块之间耦合度很低,那么修改一个模块或者新增一个模块对其它模块的影响会很小,从而实现可扩展性。
如果模块之间不直接进行调用,模块之间耦合度就会很低,那么修改一个模块或者新增一个模块对其它模块的影响会很小,从而实现可扩展性。
通过使用消息队列,一个模块只需要向消息队列中发送消息,其它模块可以选择性地从消息队列中订阅消息从而完成调用。
@ -72,9 +68,14 @@
## 接收端的可靠性
接收端能够从消息中间件成功消费一次消息。
接收端能够从消息队列成功消费一次消息。
实现方法:
- 保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。
- 保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。
# 参考资料
- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/)
- [消息队列中点对点与发布订阅区别](https://blog.csdn.net/lizhitao/article/details/47723105)

File diff suppressed because it is too large Load Diff

104
notes/系统设计基础.md Normal file
View File

@ -0,0 +1,104 @@
<!-- GFM-TOC -->
* [一、性能](#一性能)
* [二、伸缩性](#二伸缩性)
* [三、扩展性](#三扩展性)
* [四、可用性](#四可用性)
* [五、安全性](#五安全性)
<!-- GFM-TOC -->
# 一、性能
## 性能指标
### 1. 响应时间
指某个请求从发出到接收到响应消耗的时间。
在对响应时间进行测试时,通常采用重复请求方式,然后计算平均响应时间。
### 2. 吞吐量
指系统在单位时间内可以处理的请求数量,通常使用每秒的请求数来衡量。
### 3. 并发用户数
指系统能同时处理的并发用户请求数量。
在没有并发存在的系统中,请求被顺序执行,此时响应时间为吞吐量的倒数。例如系统支持的吞吐量为 100 req/s那么平均响应时间应该为 0.01s。
目前的大型系统都支持多线程来处理并发请求,多线程能够提高吞吐量以及缩短响应时间,主要有两个原因:
- 多 CPU
- IO 等待时间
使用 IO 多路复用等方式,系统在等待一个 IO 操作完成的这段时间内不需要被阻塞,可以去处理其它请求。通过将这个等待时间利用起来,使得 CPU 利用率大大提高。
并发用户数不是越高越好,因为如果并发用户数太高,系统来不及处理这么多的请求,会使得过多的请求需要等待,那么响应时间就会大大提高。
## 性能优化
### 1. 集群
将多台服务器组成集群,使用负载均衡将请求转发到集群中,避免单一服务器的负载压力过大导致性能降低。
### 2. 缓存
缓存能够提高性能的原因如下:
- 缓存数据通常位于内存等介质中,这种介质对于读操作特别快;
- 缓存数据可以位于靠近用户的地理位置上;
- 可以将计算结果进行缓存,从而避免重复计算。
### 3. 异步
某些流程可以将操作转换为消息,将消息发送到消息队列之后立即返回,之后这个操作会被异步处理。
# 二、伸缩性
指不断向集群中添加服务器来缓解不断上升的用户并发访问压力和不断增长的数据存储需求。
## 伸缩性与性能
如果系统存在性能问题,那么单个用户的请求总是很慢的;
如果系统存在伸缩性问题,那么单个用户的请求可能会很快,但是在并发数很高的情况下系统会很慢。
## 实现伸缩性
应用服务器只要不具有状态,那么就可以很容易地通过负载均衡器向集群中添加新的服务器。
关系型数据库的伸缩性通过 Sharding 来实现,将数据按一定的规则分布到不同的节点上,从而解决单台存储服务器的存储空间限制。
对于非关系型数据库,它们天生就是为海量数据而诞生,对伸缩性的支持特别好。
# 三、扩展性
指的是添加新功能时对现有系统的其它应用无影响,这就要求不同应用具备低耦合的特点。
实现可扩展主要有两种方式:
- 使用消息队列进行解耦,应用之间通过消息传递进行通信;
- 使用分布式服务将业务和可复用的服务分离开来,业务使用分布式服务框架调用可复用的服务。新增的产品可以通过调用可复用的服务来实现业务逻辑,对其它产品没有影响。
# 四、可用性
## 冗余
保证高可用的主要手段是使用冗余,当某个服务器故障时就请求其它服务器。
应用服务器的冗余比较容易实现,只要保证应用服务器不具有状态,那么某个应用服务器故障时,负载均衡器将该应用服务器原先的用户请求转发到另一个应用服务器上,不会对用户有任何影响。
存储服务器的冗余需要使用主从复制来实现,当主服务器故障时,需要提升从服务器为主服务器,这个过程称为切换。
## 监控
对 CPU、内存、磁盘、网络等系统负载信息进行监控当某个数据达到一定阈值时通知运维人员从而在系统发生故障之前及时发现问题。
## 服务降级
服务降级是系统为了应对大量的请求,主动关闭部分功能,从而保证核心功能可用。
# 五、安全性
要求系统的应对各种攻击手段时能够有可靠的应对措施。

262
notes/缓存.md Normal file
View File

@ -0,0 +1,262 @@
<!-- GFM-TOC -->
* [一、缓存特征](#一缓存特征)
* [二、LRU](#二lru)
* [三、缓存位置](#三缓存位置)
* [四、CDN](#四cdn)
* [五、缓存问题](#五缓存问题)
* [六、数据分布](#六数据分布)
* [七、一致性哈希](#七一致性哈希)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、缓存特征
## 命中率
当某个请求能够通过访问缓存而得到响应时,称为缓存命中。
缓存命中率越高,缓存的利用率也就越高。
## 最大空间
缓存通常位于内存中,内存的空间通常比磁盘空间小的多,因此缓存的最大空间不可能非常大。
当缓存存放的数据量超过最大空间时,就需要淘汰部分数据来存放新到达的数据。
## 淘汰策略
- FIFOFirst In First Out先进先出策略在实时性的场景下需要经常访问最新的数据那么就可以使用 FIFO使得最先进入的数据最晚的数据被淘汰。
- LRULeast Recently Used最近最久未使用策略优先淘汰最久未使用的数据也就是上次被访问时间距离现在最久的数据。该策略可以保证内存中的数据都是热点数据也就是经常被访问的数据从而保证缓存命中率。
# 二、LRU
以下是基于 双向链表 + HashMap 的 LRU 算法实现,对算法的解释如下:
- 访问某个节点时,将其从原来的位置删除,并重新插入到链表头部。这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就淘汰链表尾部的节点。
- 为了使删除操作时间复杂度为 O(1)就不能采用遍历的方式找到某个节点。HashMap 存储着 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。
```java
public class LRU<K, V> implements Iterable<K> {
private Node head;
private Node tail;
private HashMap<K, Node> map;
private int maxSize;
private class Node {
Node pre;
Node next;
K k;
V v;
public Node(K k, V v) {
this.k = k;
this.v = v;
}
}
public LRU(int maxSize) {
this.maxSize = maxSize;
this.map = new HashMap<>(maxSize * 4 / 3);
head = new Node(null, null);
tail = new Node(null, null);
head.next = tail;
tail.pre = head;
}
public V get(K key) {
if (!map.containsKey(key)) {
return null;
}
Node node = map.get(key);
unlink(node);
appendHead(node);
return node.v;
}
public void put(K key, V value) {
if (map.containsKey(key)) {
Node node = map.get(key);
unlink(node);
}
Node node = new Node(key, value);
map.put(key, node);
appendHead(node);
if (map.size() > maxSize) {
Node toRemove = removeTail();
map.remove(toRemove);
}
}
private void unlink(Node node) {
Node pre = node.pre;
node.pre = node.next;
node.next = pre;
}
private void appendHead(Node node) {
node.next = head.next;
head.next = node;
}
private Node removeTail() {
Node node = tail.pre;
node.pre = tail;
return node;
}
@Override
public Iterator<K> iterator() {
return new Iterator<K>() {
private Node cur = head.next;
@Override
public boolean hasNext() {
return cur != tail;
}
@Override
public K next() {
Node node = cur;
cur = cur.next;
return node.k;
}
};
}
}
```
# 三、缓存位置
## 浏览器
当 HTTP 响应允许进行缓存时,浏览器会将 HTML、CSS、JavaScript、图片等静态资源进行缓存。
## ISP
网络服务提供商ISP是网络访问的第一跳通过将数据缓存在 ISP 中能够大大提高用户的访问速度。
## 反向代理
反向代理位于服务器之前,请求与响应都需要经过反向代理。通过将数据缓存在反向代理,在用户请求反向代理时就可以直接使用缓存进行响应。
## 本地缓存
使用 Guava Cache 将数据缓存在服务器本地内存中,服务器代码可以直接读取本地内存中的缓存,速度非常快。
## 分布式缓存
使用 Redis、Memcache 等分布式缓存将数据缓存在分布式缓存系统中。
相对于本地缓存来说,分布式缓存单独部署,可以根据需求分配硬件资源。不仅如此,服务器集群都可以访问分布式缓存,而本地缓存需要在服务器集群之间进行同步,实现和性能开销上都非常大。
## 数据库缓存
MySQL 等数据库管理系统具有自己的查询缓存机制来提高查询效率。
# 四、CDN
内容分发网络Content distribution networkCDN是一种互连的网络系统它利用更靠近用户的服务器从而更快更可靠地将 HTML、CSS、JavaScript、音乐、图片、视频等静态资源分发给用户。
CDN 主要有以下优点:
- 更快地将数据分发给用户;
- 通过部署多台服务器,从而提高系统整体的带宽性能;
- 多台服务器可以看成是一种冗余机制,从而具有高可用性。
<div align="center"> <img src="../pics//15313ed8-a520-4799-a300-2b6b36be314f.jpg"/> </div><br>
# 五、缓存问题
## 缓存穿透
指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。
解决方案:
- 对这些不存在的数据缓存一个空数据;
- 对这类请求进行过滤。
## 缓存雪崩
指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。
在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。
解决方案:
- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;
- 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。
- 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。
## 缓存一致性
缓存一致性要求数据更新的同时缓存数据也能够实时更新。
解决方案:
- 在数据更新的同时立即去更新缓存;
- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。
要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据。
# 六、数据分布
## 哈希分布
哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key则将该数据分配的节点序号为hash(key)%N。
传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。
## 顺序分布
将数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如 User 表的 ID 范围为 1 \~ 7000使用顺序分布可以将其划分成多个子表对应的主键范围为 1 \~ 10001001 \~ 2000...6001 \~ 7000。
顺序分布相比于哈希分布的主要优点如下:
- 能保持数据原有的顺序;
- 并且能够准确控制每台服务器存储的数据量,从而使得存储空间的利用率最大。
# 七、一致性哈希
Distributed Hash TableDHT 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据迁移的问题。
## 基本原理
将哈希空间 [0, 2<sup>n</sup>-1] 看成一个哈希环,每个服务器节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。
<div align="center"> <img src="../pics//68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg"/> </div><br>
一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X只需要将它后一个节点 C 上的数据重新进行分布即可,对于节点 A、B、D 都没有影响。
<div align="center"> <img src="../pics//66402828-fb2b-418f-83f6-82153491bcfe.jpg"/> </div><br>
## 虚拟节点
上面描述的一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。
数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。
解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得多,那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。
# 参考资料
- 大规模分布式存储系统
- [缓存那些事](https://tech.meituan.com/cache_about.html)
- [一致性哈希算法](https://my.oschina.net/jayhu/blog/732849)
- [内容分发网络](https://zh.wikipedia.org/wiki/%E5%85%A7%E5%AE%B9%E5%82%B3%E9%81%9E%E7%B6%B2%E8%B7%AF)
- [How Aspiration CDN helps to improve your website loading speed?](https://www.aspirationhosting.com/aspiration-cdn/)

View File

@ -1,7 +1,7 @@
<!-- GFM-TOC -->
* [一、概述](#一概述)
* [操作系统基本特征](#操作系统基本特征)
* [操作系统基本功能](#操作系统基本功能)
* [基本特征](#基本特征)
* [基本功能](#基本功能)
* [系统调用](#系统调用)
* [大内核和微内核](#大内核和微内核)
* [中断分类](#中断分类)
@ -15,6 +15,10 @@
* [三、死锁](#三死锁)
* [死锁的必要条件](#死锁的必要条件)
* [死锁的处理方法](#死锁的处理方法)
* [鸵鸟策略](#鸵鸟策略)
* [死锁检测与死锁恢复](#死锁检测与死锁恢复)
* [死锁预防](#死锁预防)
* [死锁避免](#死锁避免)
* [四、内存管理](#四内存管理)
* [虚拟内存](#虚拟内存)
* [分页系统地址映射](#分页系统地址映射)
@ -23,6 +27,7 @@
* [段页式](#段页式)
* [分页与分段的比较](#分页与分段的比较)
* [五、设备管理](#五设备管理)
* [磁盘结构](#磁盘结构)
* [磁盘调度算法](#磁盘调度算法)
* [六、链接](#六链接)
* [编译系统](#编译系统)
@ -35,7 +40,7 @@
# 一、概述
## 操作系统基本特征
## 基本特征
### 1. 并发
@ -57,13 +62,17 @@
虚拟技术把一个物理实体转换为多个逻辑实体。
主要有两种虚拟技术:时分复用技术和空分复用技术。例如多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换。
主要有两种虚拟技术:时分复用技术和空分复用技术。
多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换。
虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间和物理内存使用页进行交换,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。
### 4. 异步
异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。
## 操作系统基本功能
## 基本功能
### 1. 进程管理
@ -152,19 +161,27 @@ Linux 的系统调用主要有以下这些:
一个进程中可以有多个线程,它们共享进程资源。
QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
<div align="center"> <img src="../pics//3cd630ea-017c-488d-ad1d-732b4efeddf5.png"/> </div><br>
### 3. 区别
- 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
拥有资源
- 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换
进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源
- 系统开销由于创建或撤销进程时系统都要为之分配或回收资源如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
Ⅱ 调度
- 通信方面:进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性。而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信
线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换
举例QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
Ⅲ 系统开销
由于创建或撤销进程时系统都要为之分配或回收资源如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
Ⅳ 通信方面
进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性。而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信。
## 进程状态的切换
@ -211,7 +228,10 @@ Linux 的系统调用主要有以下这些:
将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
时间片轮转算法的效率和时间片的大小有很大关系。因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
时间片轮转算法的效率和时间片的大小有很大关系:
- 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
- 而如果时间片过长,那么实时性就不能得到保证。
<div align="center"> <img src="../pics//8c662999-c16c-481c-9f40-1fdba5bc9167.png"/> </div><br>
@ -293,7 +313,7 @@ void P2() {
为了同步生产者和消费者的行为需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计这里需要使用两个信号量empty 记录空缓冲区的数量full 记录满缓冲区的数量。其中empty 信号量是在生产者进程中使用,当 empty 不为 0 时生产者才可以放入物品full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0此时生产者睡眠。消费者不能进入临界区因为生产者对缓冲区加锁了也就无法执行 up(empty) 操作empty 永远都为 0那么生产者和消费者就会一直等待下去造成死锁
注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0此时生产者睡眠。消费者不能进入临界区因为生产者对缓冲区加锁了消费者就无法执行 up(empty) 操作empty 永远都为 0导致生产者永远等待下不会释放锁消费者因此也会永远等待下去
```c
#define N 100
@ -303,7 +323,7 @@ semaphore empty = N;
semaphore full = 0;
void producer() {
while(TRUE){
while(TRUE) {
int item = produce_item();
down(&empty);
down(&mutex);
@ -314,7 +334,7 @@ void producer() {
}
void consumer() {
while(TRUE){
while(TRUE) {
down(&full);
down(&mutex);
int item = remove_item();
@ -531,7 +551,7 @@ int pipe(int fd[2]);
它具有以下限制:
- 只支持半双工通信(单向传输);
- 只支持半双工通信(单向交替传输);
- 只能在父子进程中使用。
<div align="center"> <img src="../pics//53cd9ade-b0a6-4399-b4de-7f1fbd06cdfb.png"/> </div><br>
@ -587,19 +607,28 @@ FIFO 常用于客户-服务器应用程序中FIFO 用作汇聚点,在客户
## 死锁的处理方法
### 1. 鸵鸟策略
主要有以下四种方法:
- 鸵鸟策略;
- 死锁检测与死锁恢复
- 死锁预防
- 死锁避免
## 鸵鸟策略
把头埋在沙子里,假装根本没发生问题。
因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。
因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。
当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。
大多数操作系统,包括 UnixLinux 和 Windows处理死锁问题的办法仅仅是忽略它。
### 2. 死锁检测与死锁恢复
## 死锁检测与死锁恢复
不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。
(一)每种类型一个资源的死锁检测
### 1. 每种类型一个资源的死锁检测
<div align="center"> <img src="../pics//b1fa0453-a4b0-4eae-a352-48acca8fff74.png"/> </div><br>
@ -609,7 +638,7 @@ FIFO 常用于客户-服务器应用程序中FIFO 用作汇聚点,在客户
每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。
(二)每种类型多个资源的死锁检测
### 2. 每种类型多个资源的死锁检测
<div align="center"> <img src="../pics//e1eda3d5-5ec8-4708-8e25-1a04c5e11f48.png"/> </div><br>
@ -630,35 +659,35 @@ FIFO 常用于客户-服务器应用程序中FIFO 用作汇聚点,在客户
2. 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。
3. 如果没有这样一个进程,算法终止。
(三)死锁恢复
### 3. 死锁恢复
- 利用抢占恢复
- 利用回滚恢复
- 通过杀死进程恢复
### 3. 死锁预防
## 死锁预防
在程序运行之前预防发生死锁。
(一)破坏互斥条件
### 1. 破坏互斥条件
例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
(二)破坏占有和等待条件
### 2. 破坏占有和等待条件
一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。
(三)破坏不可抢占条件
### 3. 破坏不可抢占条件
(四)破坏环路等待
### 4. 破坏环路等待
给资源统一编号,进程只能按编号顺序来请求资源。
### 4. 死锁避免
## 死锁避免
在程序运行时避免发生死锁。
(一)安全状态
### 1. 安全状态
<div align="center"> <img src="../pics//ed523051-608f-4c3f-b343-383e2d194470.png"/> </div><br>
@ -668,7 +697,7 @@ FIFO 常用于客户-服务器应用程序中FIFO 用作汇聚点,在客户
安全状态的检测与死锁的检测类似,因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类似,可以结合着做参考对比。
(二)单个资源的银行家算法
### 2. 单个资源的银行家算法
一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。
@ -676,7 +705,7 @@ FIFO 常用于客户-服务器应用程序中FIFO 用作汇聚点,在客户
上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。
(三)多个资源的银行家算法
### 3. 多个资源的银行家算法
<div align="center"> <img src="../pics//62e0dd4f-44c3-43ee-bb6e-fedb9e068519.png"/> </div><br>
@ -696,7 +725,7 @@ FIFO 常用于客户-服务器应用程序中FIFO 用作汇聚点,在客户
虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。
为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到一部分不在物理内存中的地址空间时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。
为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。
从上面的描述中可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序称为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0\~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。
@ -704,12 +733,11 @@ FIFO 常用于客户-服务器应用程序中FIFO 用作汇聚点,在客户
## 分页系统地址映射
- 内存管理单元MMU管理着地址空间和物理内存的转换。
- 页表Page table地址空间和页框物理内存空间的映射表。例如下图中页表的第 0 个表项为 010表示第 0 个页映射到第 2 个页框。页表项的最后一位用来标记页是否在内存中。
内存管理单元MMU管理着地址空间和物理内存的转换其中的页表Page table存储着页程序地址空间和页框物理内存空间的映射表。
下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位。因此对于虚拟地址0010 000000000100前 4 位是用来存储页面号,而后 12 位存储在页中的偏移量。
下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位,也就是存储页面号,剩下 12 个比特位存储偏移量。
0010 000000000100根据前 4 位得到页号为 2读取表项内容为110 1它的前 3 为为页框号,最后 1 位表示该页在内存中。最后映射得到物理内存地址为110 000000000100
例如对于虚拟地址0010 000000000100前 4 位是存储页面号 2读取表项内容为110 1。该页在内存中并且页框的地址为 110 000000000100
<div align="center"> <img src="../pics//cf4386a1-58c9-4eca-a17f-e12b1e9770eb.png" width="500"/> </div><br>
@ -780,12 +808,11 @@ FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问
<div align="center"> <img src="../pics//ecf8ad5d-5403-48b9-b6e7-f2e20ffe8fca.png"/> </div><br>
### 6. 时钟
> Clock
第二次机会算法需要在链表中移动页面,降低了效率。时钟算法使用环形链表将页面接起来,再使用一个指针指向最老的页面。
第二次机会算法需要在链表中移动页面,降低了效率。时钟算法使用环形链表将页面接起来,再使用一个指针指向最老的页面。
<div align="center"> <img src="../pics//5f5ef0b6-98ea-497c-a007-f6c55288eab1.png"/> </div><br>
@ -817,11 +844,22 @@ FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问
# 五、设备管理
## 磁盘结构
- 盘面Platter一个磁盘有多个盘面
- 磁道Track盘面上的圆形带状区域一个盘面可以有多个磁道
- 扇区Track Sector磁道上的一个弧段一个磁道可以有多个扇区它是最小的物理储存单位目前主要有 512 bytes 与 4 K 两种大小;
- 磁头Head与盘面非常接近能够将盘面上的磁场转换为电信号或者将电信号转换为盘面的磁场
- 制动手臂Actuator arm用于在磁道之间移动磁头
- 主轴Spindle使整个盘面转动。
<div align="center"> <img src="../pics//014fbc4d-d873-4a12-b160-867ddaed9807.jpg"/> </div><br>
## 磁盘调度算法
读写一个磁盘块的时间的影响因素有:
- 旋转时间(主轴旋转磁盘,使得磁头移动到适当的扇区上)
- 旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上)
- 寻道时间(制动手臂移动,使得磁头移动到适当的磁道上)
- 实际的数据传输时间
@ -922,8 +960,9 @@ gcc -o hello hello.c
- Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Press, 2014.
- 汤子瀛, 哲凤屏, 汤小丹. 计算机操作系统[M]. 西安电子科技大学出版社, 2001.
- Bryant, R. E., & OHallaron, D. R. (2004). 深入理解计算机系统.
- 史蒂文斯. UNIX 环境高级编程 [M]. 人民邮电出版社, 2014.
- [Operating System Notes](https://applied-programming.github.io/Operating-Systems-Notes/)
- [进程间的几种通信方式](http://blog.csdn.net/yufaw/article/details/7409596)
- [Operating-System Structures](https://www.cs.uic.edu/\~jbell/CourseNotes/OperatingSystems/2_Structures.html)
- [Processes](http://cse.csusb.edu/tongyu/courses/cs460/notes/process.php)
- [Inter Process Communication Presentation[1]](https://www.slideshare.net/rkolahalam/inter-process-communication-presentation1)
- [Decoding UCS Invicta Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1)

View File

@ -37,16 +37,16 @@
* [TCP 首部格式](#tcp-首部格式)
* [TCP 的三次握手](#tcp-的三次握手)
* [TCP 的四次挥手](#tcp-的四次挥手)
* [TCP 滑动窗口](#tcp-滑动窗口)
* [TCP 可靠传输](#tcp-可靠传输)
* [TCP 滑动窗口](#tcp-滑动窗口)
* [TCP 流量控制](#tcp-流量控制)
* [TCP 拥塞控制](#tcp-拥塞控制)
* [六、应用层](#六应用层)
* [域名系统](#域名系统)
* [文件传送协议](#文件传送协议)
* [动态主机配置协议](#动态主机配置协议)
* [远程登录协议](#远程登录协议)
* [电子邮件协议](#电子邮件协议)
* [动态主机配置协议](#动态主机配置协议)
* [常用端口](#常用端口)
* [Web 页面请求过程](#web-页面请求过程)
* [参考资料](#参考资料)
@ -59,7 +59,7 @@
网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网是网络的网络。
<div align="center"> <img src="../pics//network-of-networks.gif" width="500"/> </div><br>
<div align="center"> <img src="../pics//network-of-networks.gif" width="450"/> </div><br>
## ISP
@ -67,9 +67,7 @@
<div align="center"> <img src="../pics//46cec213-3048-4a80-aded-fdd577542801.jpg" width="500"/> </div><br>
目前的互联网是一种多层次 ISP 结构ISP 根据覆盖面积的大小分为第一层 ISP、区域 ISP 和接入 ISP。
互联网交换点 IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。
目前的互联网是一种多层次 ISP 结构ISP 根据覆盖面积的大小分为第一层 ISP、区域 ISP 和接入 ISP。互联网交换点 IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。
<div align="center"> <img src="../pics//168e893c-e4a0-4ba4-b81f-9d993483abd0.jpg" width="500"/> </div><br>
@ -91,7 +89,7 @@
每个分组都有首部和尾部,包含了源地址和目的地址等控制信息,在同一个传输线路上同时传输多个分组互相不会影响,因此在同一条传输线路上允许同时传输多个分组,也就是说分组交换不需要占用传输线路。
考虑在一个邮局通信系统中,邮局收到一份邮件之后,先存储下来,然后把相同目的地的邮件一起转发到下一个目的地,这个过程就是存储转发过程,分组交换也使用了存储转发过程。
在一个邮局通信系统中,邮局收到一份邮件之后,先存储下来,然后把相同目的地的邮件一起转发到下一个目的地,这个过程就是存储转发过程,分组交换也使用了存储转发过程。
## 时延
@ -109,11 +107,11 @@
### 2. 传播时延
电磁波在信道中传播一定的距离需要花费的时间,电磁波传播速度接近光速。
电磁波在信道中传播所需要花费的时间,电磁波传播的速度接近光速。
<div align="center"><img src="https://latex.codecogs.com/gif.latex?delay=\frac{l(m)}{v(m/s)}"/></div> <br>
其中 l 表示信道长度v 表示电磁波在信道上的传播速
其中 l 表示信道长度v 表示电磁波在信道上的传播速
### 3. 处理时延
@ -125,7 +123,7 @@
## 计算机网络体系结构*
<div align="center"> <img src="../pics//426df589-6f97-4622-b74d-4a81fcb1da8e.png" width="800"/> </div><br>
<div align="center"> <img src="../pics//426df589-6f97-4622-b74d-4a81fcb1da8e.png" width="600"/> </div><br>
### 1. 五层协议
@ -135,7 +133,7 @@
- **网络层** :为主机间提供数据传输服务,而运输层协议是为主机中的进程提供服务。网络层把运输层传递下来的报文段或者用户数据报封装成分组。
- **数据链路层** :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的节点提供服务。数据链路层把网络层传来的分组封装成帧。
- **数据链路层** :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供服务。数据链路层把网络层传下来的分组封装成帧。
- **物理层** :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。
@ -143,7 +141,7 @@
其中表示层和会话层用途如下:
- **表示层** :数据压缩、加密以及数据描述,这使得应用程序不必心在各台主机中数据内部格式不同的问题。
- **表示层** :数据压缩、加密以及数据描述,这使得应用程序不必心在各台主机中数据内部格式不同的问题。
- **会话层** :建立及管理会话。
@ -153,7 +151,7 @@
它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。
现在的 TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。
TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。
<div align="center"> <img src="../pics//45e0e0bf-386d-4280-a341-a0b9496c7674.png" width="400"/> </div><br>
@ -306,7 +304,7 @@ PPP 的帧格式:
MAC 地址是链路层地址,长度为 6 字节48 位),用于唯一标识网络适配器(网卡)。
一台主机拥有多少个适配器就有多少个 MAC 地址。例如笔记本电脑普遍存在无线网络适配器和有线网络适配器,因此就有两个 MAC 地址。
一台主机拥有多少个网络适配器就有多少个 MAC 地址。例如笔记本电脑普遍存在无线网络适配器和有线网络适配器,因此就有两个 MAC 地址。
## 局域网
@ -314,7 +312,7 @@ MAC 地址是链路层地址,长度为 6 字节48 位),用于唯一标
主要有以太网、令牌环网、FDDI 和 ATM 等局域网技术,目前以太网占领着有线局域网市场。
可以按照网络拓结构对局域网进行分类:
可以按照网络拓结构对局域网进行分类:
<div align="center"> <img src="../pics//a6026bb4-3daf-439f-b1ec-a5a24e19d2fb.jpg" width="600"/> </div><br>
@ -322,9 +320,9 @@ MAC 地址是链路层地址,长度为 6 字节48 位),用于唯一标
以太网是一种星型拓扑结构局域网。
早期使用集线器进行连接。集线器是一种物理层设备,作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离之后再将这个比特发送到其它所有接口。如果集线器同时收到同时从两个不同接口的帧,那么就发生了碰撞。
早期使用集线器进行连接,集线器是一种物理层设备, 作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离之后再将这个比特发送到其它所有接口。如果集线器同时收到同时从两个不同接口的帧,那么就发生了碰撞。
目前以太网使用交换机替代了集线器交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。
目前以太网使用交换机替代了集线器交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。
以太网帧格式:
@ -337,7 +335,9 @@ MAC 地址是链路层地址,长度为 6 字节48 位),用于唯一标
## 交换机*
交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。正是由于这种自学习能力,因此交换机是一种即插即可即用设备,不需要网络管理员手动配置交换表内容。
交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。
正是由于这种自学习能力,因此交换机是一种即插即用设备,不需要网络管理员手动配置交换表内容。
下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B先查交换表此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧。主机 B 收下之后,查找交换表得到主机 A 映射的接口为 1就发送数据帧到接口 1同时交换机添加主机 B 到接口 3 的映射。
@ -345,9 +345,11 @@ MAC 地址是链路层地址,长度为 6 字节48 位),用于唯一标
## 虚拟局域网
虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。
虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。
使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊端口被设置为干线端口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了 4 字节首部 VLAN 标签,用于表示该帧属于哪一个虚拟局域网。
例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。
使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊接口被设置为干线接口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了 4 字节首部 VLAN 标签,用于表示该帧属于哪一个虚拟局域网。
<div align="center"> <img src="../pics//a74b70ac-323a-4b31-b4d5-90569b8a944b.png" width="500"/> </div><br>
@ -411,12 +413,14 @@ IP 地址 ::= {< 网络号 >, < 主机号 >}
### 2. 子网划分
通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。注意,外部网络看不到子网的存在。
通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。
IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >}
要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 00000000也就是 255.255.192.0。
注意,外部网络看不到子网的存在。
### 3. 无分类
无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。
@ -441,7 +445,7 @@ ARP 实现由 IP 地址得到 MAC 地址。
<div align="center"> <img src="../pics//b9d79a5a-e7af-499b-b989-f10483e71b8b.jpg" width="500"/> </div><br>
每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到硬件地址的映射表。
每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到 MAC 地址的映射表。
如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。
@ -461,12 +465,14 @@ ICMP 报文分为差错报告报文和询问报文。
Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。
Ping 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报
Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率
### 2. Traceroute
Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。
Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报,并由目的主机发送终点不可达差错报告报文。
- 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1当 P1 到达路径上的第一个路由器 R1 时R1 收下它并把 TTL 减 1此时 TTL 等于 0R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文;
- 源主机接着发送第二个数据报 P2并把 TTL 设置为 2。P2 先到达 R1R1 收下后把 TTL 减 1 再转发给 R2R2 收下后也把 TTL 减 1由于此时 TTL 等于 0R2 就丢弃 P2并向源主机发送一个 ICMP 时间超过差错报文。
- 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。
@ -482,7 +488,7 @@ Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终
- 172.16.0.0 \~ 172.31.255.255
- 192.168.0.0 \~ 192.168.255.255
VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指好像是,而实际上并不是,它有经过公用的互联网。
VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指好像是,而实际上并不是,它有经过公用的互联网。
下图中,场所 A 和 B 的通信经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。
@ -614,11 +620,11 @@ BGP 只能寻找一条比较好的路由,而不是最佳路由。
- 首先 B 处于 LISTEN监听状态等待客户的连接请求。
- A 向 B 发送连接请求报文SYN=1ACK=0选择一个初始的序号 x。
- A 向 B 发送连接请求报文SYN=1ACK=0选择一个初始的序号 x。
- B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文SYN=1ACK=1确认号为 x+1同时也选择一个初始的序号 y。
- B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文SYN=1ACK=1确认号为 x+1同时也选择一个初始的序号 y。
- A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1序号为 x+1。
- A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1序号为 x+1。
- B 收到 A 的确认后,连接建立。
@ -634,11 +640,11 @@ BGP 只能寻找一条比较好的路由,而不是最佳路由。
以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK因为 ACK 在连接建立之后都为 1。
- A 发送连接释放报文FIN=1。
- A 发送连接释放报文FIN=1。
- B 收到之后发出确认,此时 TCP 属于半关闭状态B 能向 A 发送数据但是 A 不能向 B 发送数据。
- 当 B 不再需要连接时,发送连接释放请求报文FIN=1。
- 当 B 不再需要连接时发送连接释放报文FIN=1。
- A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL最大报文存活时间后释放连接。
@ -652,19 +658,9 @@ BGP 只能寻找一条比较好的路由,而不是最佳路由。
客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由
- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文A 等待一段时间就是为了处理这种情况的发生。
- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文那么就会重新发送连接释放请求报文A 等待一段时间就是为了处理这种情况的发生。
- 等待一段时间是为了让本连接持续时间内所产生的所有报文段都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文段。
## TCP 滑动窗口
<div align="center"> <img src="../pics//a3253deb-8d21-40a1-aae4-7d178e4aa319.jpg" width="800"/> </div><br>
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。
- 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。
## TCP 可靠传输
@ -680,6 +676,16 @@ TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文
其中 RTT<sub>d</sub> 为偏差。
## TCP 滑动窗口
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。
<div align="center"> <img src="../pics//a3253deb-8d21-40a1-aae4-7d178e4aa319.jpg" width="800"/> </div><br>
## TCP 流量控制
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
@ -692,7 +698,7 @@ TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文
<div align="center"> <img src="../pics//51e2ed95-65b8-4ae9-8af3-65602d452a25.jpg" width="500"/> </div><br>
TCP 主要通过四算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。
TCP 主要通过四算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。
发送方需要维护一个叫做拥塞窗口cwnd的状态变量注意拥塞窗口与发送方窗口的区别拥塞窗口只是一个状态变量实际决定发送方能发送多少数据的是发送方窗口。
@ -705,11 +711,11 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、
### 1. 慢开始与拥塞避免
发送的最初执行慢开始,令 cwnd=1发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍因此之后发送方能够发送的报文段数量为2、4、8 ...
发送的最初执行慢开始,令 cwnd = 1发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍因此之后发送方能够发送的报文段数量为2、4、8 ...
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢开始门限 ssthresh当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢开始门限 ssthresh当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
如果出现了超时,则令 ssthresh = cwnd/2然后重新执行慢开始。
如果出现了超时,则令 ssthresh = cwnd / 2然后重新执行慢开始。
### 2. 快重传与快恢复
@ -717,7 +723,9 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、
在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M<sub>2</sub>,则 M<sub>3</sub> 丢失,立即重传 M<sub>3</sub>
在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd/2 cwnd = ssthresh注意到此时直接进入拥塞避免。慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1而快恢复 cwnd 设定为 ssthresh。
在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd / 2 cwnd = ssthresh注意到此时直接进入拥塞避免。
慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1而快恢复 cwnd 设定为 ssthresh。
<div align="center"> <img src="../pics//f61b5419-c94a-4df1-8d4d-aed9ae8cc6d5.png" width="600"/> </div><br>
@ -725,23 +733,23 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、
## 域名系统
DNS 是一个分布式数据库,提供了主机名和 IP 地址之间的转换。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。
DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。
域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。
域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。
<div align="center"> <img src="../pics//b54eeb16-0b0e-484c-be62-306f57c40d77.jpg"/> </div><br>
DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传来保证可靠性。在两种情况下会使用 TCP 进行传输:
- 因为 UDP 最大只支持 512 字节的数据,如果返回的响应超过的 512 字节就改用 TCP 进行传输
- 区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据,区域传送需要使用 TCP 进行传输
- 如果返回的响应超过的 512 字节UDP 最大只支持 512 字节的数据)
- 区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据
## 文件传送协议
FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件:
- 控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。
- 数据连接:用来传送一个文件。
- 控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。
- 数据连接:用来传送一个文件数据
根据数据连接是否是服务器端主动建立FTP 有主动和被动两种模式:
@ -755,6 +763,21 @@ FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件:
主动模式要求客户端开放端口号给服务器端,需要去配置客户端的防火墙。被动模式只需要服务器端开放端口号即可,无需客户端配置防火墙。但是被动模式会导致服务器端的安全性减弱,因为开放了过多的端口号。
## 动态主机配置协议
DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要去手动配置 IP 地址等信息。
DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址。
DHCP 工作过程如下:
1. 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67源地址为 0.0.0.0:68被放入 UDP 中,该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。
2. DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。
3. 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。
4. DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。
<div align="center"> <img src="../pics//bf16c541-0717-473b-b75d-4115864f4fbf.jpg"/> </div><br>
## 远程登录协议
TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。
@ -769,34 +792,19 @@ TELNET 可以适应许多计算机和操作系统的差异,例如不同操作
<div align="center"> <img src="../pics//7b3efa99-d306-4982-8cfb-e7153c33aab4.png" width="700"/> </div><br>
### 1. POP3
POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。
### 2. IMAP
IMAP 协议中客户端和服务器上的邮件保持同步如果不去手动删除邮件那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。
### 3. SMTP
### 1. SMTP
SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP而是增加邮件主体的结构定义了非 ASCII 码的编码规则。
<div align="center"> <img src="../pics//ed5522bb-3a60-481c-8654-43e7195a48fe.png" width=""/> </div><br>
## 动态主机配置协议
### 2. POP3
DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要去手动配置 IP 地址等信息。DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址
POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。
DHCP 工作过程如下:
### 3. IMAP
1. 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67源地址为 0.0.0.0:68被放入 UDP 中,该报文被广播到同一个子网的所有主机上。
2. DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。
3. 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。
4. DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。
<div align="center"> <img src="../pics//bf16c541-0717-473b-b75d-4115864f4fbf.jpg"/> </div><br>
如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。
IMAP 协议中客户端和服务器上的邮件保持同步如果不手动删除邮件那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。
## 常用端口
@ -879,7 +887,9 @@ DHCP 工作过程如下:
- JamesF.Kurose, KeithW.Ross, 库罗斯, 等. 计算机网络: 自顶向下方法 [M]. 机械工业出版社, 2014.
- W.RichardStevens. TCP/IP 详解. 卷 1, 协议 [M]. 机械工业出版社, 2006.
- [Active vs Passive FTP Mode: Which One is More Secure?](https://securitywing.com/active-vs-passive-ftp-mode/)
- [Active and Passive FTP Transfers Defined - KB Article #1138](http://www.serv-u.com/kb/1138/active-and-passive-ftp-transfers-defined)
- [Active and Passive FTP Transfers Defined - KB Article #1138](http://www.serv-u.com/kb/1138/active-and-passive-ftp-transfers-defined)
- [Traceroute](https://zh.wikipedia.org/wiki/Traceroute)
- [ping](https://zh.wikipedia.org/wiki/Ping)
- [How DHCP works and DHCP Interview Questions and Answers](http://webcache.googleusercontent.com/search?q=cache:http://anandgiria.blogspot.com/2013/09/windows-dhcp-interview-questions-and.html)
- [What is process of DORA in DHCP?](https://www.quora.com/What-is-process-of-DORA-in-DHCP)
- [What is DHCP Server ?](https://tecadmin.net/what-is-dhcp-server/)

View File

@ -44,11 +44,11 @@
## 1. 单例Singleton
### 意图
### Intent
确保一个类只有一个实例,并提供该实例的全局访问点。
### 类图
### Class Diagram
使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。
@ -56,13 +56,13 @@
<div align="center"> <img src="../pics//562f2844-d77c-40e0-887a-28a7128abd42.png"/> </div><br>
### 实现
### Implementation
(一)懒汉式-线程不安全
#### 懒汉式-线程不安全
以下实现中,私有静态变量 uniqueInstance 被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance从而节约资源。
以下实现中,私有静态变量 uniqueInstance 被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance从而节约资源。
这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 `if (uniqueInstance == null)` ,并且此时 uniqueInstance 为 null那么多个线程执行 `uniqueInstance = new Singleton();` 语句,这将导致多次实例化 uniqueInstance。
这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 `if (uniqueInstance == null)` ,并且此时 uniqueInstance 为 null那么会有多个线程执行 `uniqueInstance = new Singleton();` 语句,这将导致实例化多次 uniqueInstance。
```java
public class Singleton {
@ -81,11 +81,21 @@ public class Singleton {
}
```
(二)懒汉式-线程安全
#### Ⅱ 饿汉式-线程安全
只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了对 uniqueInstance 进行多次实例化的问题。
线程不安全问题主要是由于 uniqueInstance 被实例化多次,采取直接实例化 uniqueInstance 的方式就不会产生线程不安全问题。
但是这样有一个问题,就是当一个线程进入该方法之后,其它线程试图进入该方法都必须等待,因此性能上有一定的损耗。
但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。
```java
private static Singleton uniqueInstance = new Singleton();
```
#### Ⅲ 懒汉式-线程安全
只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 uniqueInstance。
但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 uniqueInstance 已经被实例化了。这会让线程阻塞时间过程,因此该方法有性能问题,不推荐使用。
```java
public static synchronized Singleton getUniqueInstance() {
@ -96,17 +106,9 @@ public static synchronized Singleton getUniqueInstance() {
}
```
(三)饿汉式-线程安全
#### Ⅳ 双重校验锁-线程安全
线程不安全问题主要是由于 uniqueInstance 被实例化了多次,如果 uniqueInstance 采用直接实例化的话,就不会被实例化多次,也就不会产生线程不安全问题。但是直接实例化的方式也丢失了延迟实例化带来的节约资源的优势。
```java
private static Singleton uniqueInstance = new Singleton();
```
(四)双重校验锁-线程安全
uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行。也就是说,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。
uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。
双重校验锁先判断 uniqueInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。
@ -131,7 +133,7 @@ public class Singleton {
}
```
考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 `uniqueInstance = new Singleton();` 这条语句,只是先后的问题,也就是说会进行两次实例化,从而产生了两个实例。因此必须使用双重校验锁,也就是需要使用两个 if 语句。
考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 `uniqueInstance = new Singleton();` 这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if 语句。
```java
if (uniqueInstance == null) {
@ -143,21 +145,21 @@ if (uniqueInstance == null) {
uniqueInstance 采用 volatile 关键字修饰也是很有必要的。`uniqueInstance = new Singleton();` 这段代码其实是分为三步执行。
1. 分配内存空间
2. 初始化对象
1. 为 uniqueInstance 分配内存空间
2. 初始化 uniqueInstance
3. 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 1>3>2这在单线程情况下自然是没有问题。但如果是多线程下有可能获得是一个还没有被初始化的实例以致于程序出错
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出先问题但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如线程 T<sub>1</sub> 执行了 1 和 3此时 T<sub>2</sub> 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance但此时 uniqueInstance 还未被初始化
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
(五)静态内部类实现
#### 静态内部类实现
当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 `getUniqueInstance()` 方法从而触发 `SingletonHolder.INSTANCE` 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例。
当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 `getUniqueInstance()` 方法从而触发 `SingletonHolder.INSTANCE` 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次
这种方式不仅具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。
这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。
```source-java
```java
public class Singleton {
private Singleton() {
@ -173,40 +175,11 @@ public class Singleton {
}
```
(五)枚举实现
该实现在多次序列化再进行反序列化之后,不会得到多个实例。而其它实现,为了保证不会出现反序列化之后出现多个实例,需要使用 transient 修饰所有字段,并且实现序列化和反序列化的方法。
这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次
该实现可以防止反射攻击。在其它实现中,通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public然后调用构造函数从而实例化对象如果要防止这种攻击需要在构造函数中添加防止实例化第二个对象的代码。但是该实现是由 JVM 保证只会实例化一次,因此不会出现上述的反射攻击
```java
public enum Singleton {
uniqueInstance;
}
```
考虑以下单例模式的实现,该 Singleton 在每次序列化的时候都会创建一个新的实例,为了保证只创建一个实例,必须声明所有字段都是 transient并且提供一个 readResolve() 方法。
```java
public class Singleton implements Serializable {
private static Singleton uniqueInstance;
private Singleton() {
}
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
```
如果不使用枚举来实现单例模式,会出现反射攻击,因为通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public然后调用构造函数从而实例化对象。如果要防止这种攻击需要在构造函数中添加防止实例化第二个对象的代码。
从上面的讨论可以看出,解决序列化和反射攻击很麻烦,而枚举实现不会出现这两种问题,所以说枚举实现单例模式是最佳实践。
### 使用场景
### Examples
- Logger Classes
- Configuration Classes
@ -221,37 +194,19 @@ public class Singleton implements Serializable {
## 2. 简单工厂Simple Factory
### 意图
### Intent
在创建一个对象时不向客户暴露内部细节,并提供一个创建对象的通用接口。
### 类图
### Class Diagram
简单工厂不是设计模式,更像是一种编程习惯。它把实例化的操作单独放到一个类中,这个类就成为简单工厂类,让简单工厂类来决定应该用哪个具体子类来实例化。
这样做能把客户类和具体子类的实现解耦,客户类不再需要知道有哪些子类以及应当实例化哪个子类。客户类往往有多个,如果不使用简单工厂,那么所有的客户类都要知道所有子类的细节。而且一旦子类发生改变,例如增加子类,那么所有的客户类都要进行修改。
<div align="center"> <img src="../pics//c79da808-0f28-4a36-bc04-33ccc5b83c13.png"/> </div><br>
这样做能把客户类和具体子类的实现解耦,客户类不再需要知道有哪些子类以及应当实例化哪个子类。因为客户类往往有多个,如果不使用简单工厂,所有的客户类都要知道所有子类的细节。而且一旦子类发生改变,例如增加子类,那么所有的客户类都要进行修改。
如果存在下面这种代码,就需要使用简单工厂将对象实例化的部分放到简单工厂中。
```java
public class Client {
public static void main(String[] args) {
int type = 1;
Product product;
if (type == 1) {
product = new ConcreteProduct1();
} else if (type == 2) {
product = new ConcreteProduct2();
} else {
product = new ConcreteProduct();
}
}
}
```
### 实现
### Implementation
```java
public interface Product {
@ -273,6 +228,27 @@ public class ConcreteProduct2 implements Product {
}
```
以下的 Client 类包含了实例化的代码,这是一种错误的实现。如果在客户类中存在这种实例化代码,就需要考虑将代码放到简单工厂中。
```java
public class Client {
public static void main(String[] args) {
int type = 1;
Product product;
if (type == 1) {
product = new ConcreteProduct1();
} else if (type == 2) {
product = new ConcreteProduct2();
} else {
product = new ConcreteProduct();
}
// do something with the product
}
}
```
以下的 SimpleFactory 是简单工厂实现,它被所有需要进行实例化的客户类调用。
```java
public class SimpleFactory {
public Product createProduct(int type) {
@ -291,17 +267,18 @@ public class Client {
public static void main(String[] args) {
SimpleFactory simpleFactory = new SimpleFactory();
Product product = simpleFactory.createProduct(1);
// do something with the product
}
}
```
## 3. 工厂方法Factory Method
### 意图
### Intent
定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法把实例化推迟到子类。
定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法把实例化操作推迟到子类。
### 类图
### Class Diagram
在简单工厂中,创建对象的是另一个类,而在工厂方法中,是由子类来创建对象。
@ -309,7 +286,7 @@ public class Client {
<div align="center"> <img src="../pics//1818e141-8700-4026-99f7-900a545875f5.png"/> </div><br>
### 实现
### Implementation
```java
public abstract class Factory {
@ -357,11 +334,11 @@ public class ConcreteFactory2 extends Factory {
## 4. 抽象工厂Abstract Factory
### 意图
### Intent
提供一个接口,用于创建 **相关的对象家族**
### 类图
### Class Diagram
抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂方法模式只是用于创建一个对象,这和抽象工厂模式有很大不同。
@ -373,7 +350,7 @@ public class ConcreteFactory2 extends Factory {
<div align="center"> <img src="../pics//8668a3e1-c9c7-4fcb-98b2-a96a5d841579.png"/> </div><br>
### 代码实现
### Implementation
```java
public class AbstractProductA {
@ -455,15 +432,15 @@ public class Client {
## 5. 生成器Builder
### 意图
### Intent
封装一个对象的构造过程,并允许按步骤构造。
### 类图
### Class Diagram
<div align="center"> <img src="../pics//13b0940e-d1d7-4b17-af4f-b70cb0a75e08.png"/> </div><br>
### 实现
### Implementation
以下是一个简易的 StringBuilder 实现,参考了 JDK 1.8 源码。
@ -545,15 +522,15 @@ abcdefghijklmnopqrstuvwxyz
## 6. 原型模式Prototype
### 意图
### Intent
使用原型实例指定要创建对象的类型,通过复制这个原型来创建新对象。
### 类图
### Class Diagram
<div align="center"> <img src="../pics//a40661e4-1a71-46d2-a158-ff36f7fc3331.png"/> </div><br>
### 实现
### Implementation
```java
public abstract class Prototype {
@ -604,17 +581,17 @@ abc
## 1. 责任链Chain Of Responsibility
### 意图
### Intent
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链发送该请求,直到有一个对象处理它为止。
### 类图
### Class Diagram
- Handler定义处理请求的接口并且实现后继链successor
<div align="center"> <img src="../pics//691f11eb-31a7-46be-9de1-61f433c4b3c7.png"/> </div><br>
### 实现
### Implementation
```java
public abstract class Handler {
@ -718,11 +695,16 @@ request2 is handle by ConcreteHandler2
## 2. 命令Command
### 意图
### Intent
将命令封装成对象中,以便使用命令来参数化其它对象,或者将命令对象放入队列中进行排队,或者将命令对象的操作记录到日志中,以及支持可撤销的操作。
将命令封装成对象中,具有以下作用:
### 类图
- 使用命令来参数化其它对象
- 将命令放入队列中进行排队
- 将命令的操作记录到日志中
- 支持可撤销的操作
### Class Diagram
- Command命令
- Receiver命令接收者也就是命令真正的执行者
@ -731,7 +713,7 @@ request2 is handle by ConcreteHandler2
<div align="center"> <img src="../pics//ae1b27b8-bc13-42e7-ac12-a2242e125499.png"/> </div><br>
### 实现
### Implementation
设计一个遥控器,可以控制电灯开关。
@ -841,18 +823,18 @@ public class Client {
## 3. 解释器Interpreter
### 意图
### Intent
为语言创建解释器,通常由语言的语法和语法分析来定义。
### 类图
### Class Diagram
- TerminalExpression终结符表达式每个终结符都需要一个 TerminalExpression
- Context上下文包含解释器之外的一些全局信息
- TerminalExpression终结符表达式每个终结符都需要一个 TerminalExpression
- Context上下文包含解释器之外的一些全局信息
<div align="center"> <img src="../pics//794239e3-4baf-4aad-92df-f02f59b2a6fe.png"/> </div><br>
### 实现
### Implementation
以下是一个规则检验器实现,具有 and 和 or 规则,通过规则可以构建一颗解析树,用来检验一个文本是否满足解析树定义的规则。
@ -965,11 +947,11 @@ false
## 4. 迭代器Iterator
### 意图
### Intent
提供一种顺序访问聚合对象元素的方法,并且不暴露聚合对象的内部表示。
### 类图
### Class Diagram
- Aggregate 是聚合类,其中 createIterator() 方法可以产生一个 Iterator
- Iterator 主要定义了 hasNext() 和 next() 方法。
@ -977,7 +959,7 @@ false
<div align="center"> <img src="../pics//b0f61ac2-a4b6-4042-9cf0-ccf4238c1ff7.png"/> </div><br>
### 实现
### Implementation
```java
public interface Aggregate {
@ -1006,6 +988,7 @@ public class ConcreteAggregate implements Aggregate {
```java
public interface Iterator<Item> {
Item next();
boolean hasNext();
@ -1036,6 +1019,7 @@ public class ConcreteIterator<Item> implements Iterator {
```java
public class Client {
public static void main(String[] args) {
Aggregate aggregate = new ConcreteAggregate();
Iterator<Integer> iterator = aggregate.createIterator();
@ -1053,18 +1037,18 @@ public class Client {
## 5. 中介者Mediator
### 意图
### Intent
集中相关对象之间复杂的沟通和控制方式。
### 类图
### Class Diagram
- Mediator中介者定义一个接口用于与各同事Colleague对象通信。
- Colleague同事相关对象
<div align="center"> <img src="../pics//d0afdd23-c9a5-4d1c-9b3d-404bff3bd0d1.png"/> </div><br>
### 实现
### Implementation
Alarm闹钟、CoffeePot咖啡壶、Calendar日历、Sprinkler喷头是一组相关的对象在某个对象的事件产生时需要去操作其它对象形成了下面这种依赖结构
@ -1222,11 +1206,11 @@ doSprinkler()
## 6. 备忘录Memento
### 意图
### Intent
在不违反封装的情况下获得对象的内部状态,从而在需要时可以将对象恢复到最初状态。
### 类图
### Class Diagram
- Originator原始对象
- Caretaker负责保存好备忘录
@ -1234,7 +1218,7 @@ doSprinkler()
<div align="center"> <img src="../pics//867e93eb-3161-4f39-b2d2-c0cd3788e194.png"/> </div><br>
### 实现
### Implementation
以下实现了一个简单计算器程序,可以输入两个值,然后计算这两个值的和。备忘录模式允许将这两个值存储起来,然后在某个时刻用存储的状态进行恢复。
@ -1399,7 +1383,7 @@ public class Client {
## 7. 观察者Observer
### 意图
### Intent
定义对象之间的一对多依赖,当一个对象状态改变时,它的所有依赖都会收到通知并且自动更新状态。
@ -1407,7 +1391,7 @@ public class Client {
<div align="center"> <img src="../pics//7a3c6a30-c735-4edb-8115-337288a4f0f2.jpg" width="600"/> </div><br>
### 类图
### Class Diagram
主题Subject具有注册和移除观察者、并通知所有观察者的功能主题是通过维护一张观察者列表来实现这些操作的。
@ -1415,7 +1399,7 @@ public class Client {
<div align="center"> <img src="../pics//0df5d84c-e7ca-4e3a-a688-bb8e68894467.png"/> </div><br>
### 实现
### Implementation
天气数据布告板会在天气信息发生改变时更新其内容,布告板有多个,并且在将来会继续增加。
@ -1534,15 +1518,15 @@ StatisticsDisplay.update: 1.0 1.0 1.0
## 8. 状态State
### 意图
### Intent
允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它所属的类。
### 类图
### Class Diagram
<div align="center"> <img src="../pics//c5085437-54df-4304-b62d-44b961711ba7.png"/> </div><br>
### 实现
### Implementation
糖果销售机有多种状态,每种状态下销售机有不同的行为,状态可以发生转移,使得销售机的行为也发生改变。
@ -1836,30 +1820,26 @@ No gumball dispensed
## 9. 策略Strategy
### 意图
### Intent
定义一系列算法,封装每个算法,并使它们可以互换。
策略模式可以让算法独立于使用它的客户端。
### 类图
### Class Diagram
- Strategy 接口定义了一个算法族,它们都具有 behavior() 方法。
- Context 是使用到该算法族的类,其中的 doSomething() 方法会调用 behavior()setStrategy(in Strategy) 方法可以动态地改变 strategy 对象,也就是说能动态地改变 Context 所使用的算法。
- Strategy 接口定义了一个算法族,它们都实现了 behavior() 方法。
- Context 是使用到该算法族的类,其中的 doSomething() 方法会调用 behavior()setStrategy(Strategy) 方法可以动态地改变 strategy 对象,也就是说能动态地改变 Context 所使用的算法。
<div align="center"> <img src="../pics//1fc969e4-0e7c-441b-b53c-01950d2f2be5.png"/> </div><br>
### 与状态模式的比较
状态模式的类图和策略模式类似,并且都是能够动态改变对象的行为。
但是状态模式是通过状态转移来改变 Context 所组合的 State 对象,而策略模式是通过 Context 本身的决策来改变组合的 Strategy 对象。
所谓的状态转移,是指 Context 在运行过程中由于一些条件发生改变而使得 State 对象发生改变,注意必须要是在运行过程中。
状态模式的类图和策略模式类似,并且都是能够动态改变对象的行为。但是状态模式是通过状态转移来改变 Context 所组合的 State 对象,而策略模式是通过 Context 本身的决策来改变组合的 Strategy 对象。所谓的状态转移,是指 Context 在运行过程中由于一些条件发生改变而使得 State 对象发生改变,注意必须要是在运行过程中。
状态模式主要是用来解决状态转移的问题,当状态发生转移了,那么 Context 对象就会改变它的行为;而策略模式主要是用来封装一组可以互相替代的算法族,并且可以根据需要动态地去替换 Context 使用的算法。
### 实现
### Implementation
设计一个鸭子,它可以动态地改变叫声。这里的算法族是鸭子的叫声行为。
@ -1889,6 +1869,7 @@ public class Squeak implements QuackBehavior{
```java
public class Duck {
private QuackBehavior quackBehavior;
public void performQuack() {
@ -1905,6 +1886,7 @@ public class Duck {
```java
public class Client {
public static void main(String[] args) {
Duck duck = new Duck();
duck.setQuackBehavior(new Squeak());
@ -1928,17 +1910,17 @@ quack!
## 10. 模板方法Template Method
### 意图
### Intent
定义算法框架,并将一些步骤的实现延迟到子类。
通过模板方法,子类可以重新定义算法的某些步骤,而不用改变算法的结构。
### 类图
### Class Diagram
<div align="center"> <img src="../pics//c3c1c0e8-3a78-4426-961f-b46dd0879dd8.png"/> </div><br>
### 实现
### Implementation
冲咖啡和冲茶都有类似的流程,但是某些步骤会有点不一样,要求复用那些相同步骤的代码。
@ -1969,7 +1951,7 @@ public abstract class CaffeineBeverage {
```
```java
public class Coffee extends CaffeineBeverage{
public class Coffee extends CaffeineBeverage {
@Override
void brew() {
System.out.println("Coffee.brew");
@ -1983,7 +1965,7 @@ public class Coffee extends CaffeineBeverage{
```
```java
public class Tea extends CaffeineBeverage{
public class Tea extends CaffeineBeverage {
@Override
void brew() {
System.out.println("Tea.brew");
@ -2029,11 +2011,11 @@ Tea.addCondiments
## 11. 访问者Visitor
### 意图
### Intent
为一个对象结构(比如组合结构)增加新能力。
### 类图
### Class Diagram
- Visitor访问者为每一个 ConcreteElement 声明一个 visit 操作
- ConcreteVisitor具体访问者存储遍历过程中的累计结果
@ -2041,7 +2023,7 @@ Tea.addCondiments
<div align="center"> <img src="../pics//ec923dc7-864c-47b0-a411-1f2c48d084de.png"/> </div><br>
### 实现
### Implementation
```java
public interface Element {
@ -2236,17 +2218,17 @@ Number of items: 6
## 12. 空对象Null
### 意图
### Intent
使用什么都不做的空对象来代 NULL。
使用什么都不做的空对象来代 NULL。
一个方法返回 NULL意味着方法的调用端需要去检查返回值是否是 NULL这么做会导致非常多的冗余的检查代码。并且如果某一个调用端忘记了做这个检查返回值而直接使用返回的对象那么就有可能抛出空指针异常。
### 类图
### Class Diagram
<div align="center"> <img src="../pics//dd3b289c-d90e-44a6-a44c-4880517eb1de.png"/> </div><br>
### 实现
### Implementation
```java
public abstract class AbstractOperation {
@ -2292,17 +2274,17 @@ public class Client {
## 1. 适配器Adapter
### 意图
### Intent
把一个类接口转换成另一个用户需要的接口。
<div align="center"> <img src="../pics//3d5b828e-5c4d-48d8-a440-281e4a8e1c92.png"/> </div><br>
### 类图
### Class Diagram
<div align="center"> <img src="../pics//0f754c1d-b5cb-48cd-90e0-4a86034290a1.png"/> </div><br>
### 实现
### Implementation
鸭子Duck和火鸡Turkey拥有不同的叫声Duck 的叫声调用 quack() 方法,而 Turkey 调用 gobble() 方法。
@ -2363,18 +2345,18 @@ public class Client {
## 2. 桥接Bridge
### 意图
### Intent
将抽象与实现分离开来,使它们可以独立变化。
### 类图
### Class Diagram
- Abstraction定义抽象类的接口
- Implementor定义实现类接口
<div align="center"> <img src="../pics//c2cbf5d2-82af-4c78-bd43-495da5adf55f.png"/> </div><br>
### 实现
### Implementation
RemoteControl 表示遥控器,指代 Abstraction。
@ -2393,7 +2375,7 @@ public abstract class TV {
```
```java
public class Sony extends TV{
public class Sony extends TV {
@Override
public void on() {
System.out.println("Sony.on()");
@ -2412,7 +2394,7 @@ public class Sony extends TV{
```
```java
public class RCA extends TV{
public class RCA extends TV {
@Override
public void on() {
System.out.println("RCA.on()");
@ -2516,11 +2498,11 @@ public class Client {
## 3. 组合Composite
### 意图
### Intent
将对象组合成树形结构来表示“整体/部分”层次关系,允许用户以相同的方式处理单独对象和组合对象。
### 类图
### Class Diagram
组件Component类是组合类Composite和叶子类Leaf的父类可以把组合类看成是树的中间节点。
@ -2528,7 +2510,7 @@ public class Client {
<div align="center"> <img src="../pics//3fb5b255-b791-45b6-8754-325c8741855a.png"/> </div><br>
### 实现
### Implementation
```java
public abstract class Component {
@ -2551,9 +2533,6 @@ public abstract class Component {
```
```java
import java.util.ArrayList;
import java.util.List;
public class Composite extends Component {
private List<Component> child;
@ -2653,17 +2632,17 @@ Composite:root
## 4. 装饰Decorator
### 意图
### Intent
为对象动态添加功能。
### 类图
### Class Diagram
装饰者Decorator和具体组件ConcreteComponent都继承自组件Component具体组件的方法实现不需要依赖于其它对象而装饰者组合了一个组件这样它可以装饰其它装饰者或者具体组件。所谓装饰就是把这个装饰者套在被装饰上从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的这属于它的功能然后调用被装饰者的方法实现从而也保留了被装饰者的功能。可以看到具体组件应当是装饰层次的最低层因为只有具体组件的方法实现不需要依赖于其它对象。
装饰者Decorator和具体组件ConcreteComponent都继承自组件Component具体组件的方法实现不需要依赖于其它对象而装饰者组合了一个组件这样它可以装饰其它装饰者或者具体组件。所谓装饰就是把这个装饰者套在被装饰者之上,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。可以看到,具体组件应当是装饰层次的最低层,因为只有具体组件的方法实现不需要依赖于其它对象。
<div align="center"> <img src="../pics//137c593d-0a9e-47b8-a9e6-b71f540b82dd.png"/> </div><br>
### 实现
### Implementation
设计不同种类的饮料,饮料可以添加配料,比如可以添加牛奶,并且支持动态添加新配料。每增加一种配料,该饮料的价格就会增加,要求计算一种饮料的价格。
@ -2731,6 +2710,7 @@ public class Mocha extends CondimentDecorator {
```java
public class Client {
public static void main(String[] args) {
Beverage beverage = new HouseBlend();
beverage = new Mocha(beverage);
@ -2760,17 +2740,17 @@ public class Client {
## 5. 外观Facade
### 意图
### Intent
提供了一个统一的接口,用来访问子系统中的一群接口,从而让子系统更容易使用。
### 类图
### Class Diagram
<div align="center"> <img src="../pics//f9978fa6-9f49-4a0f-8540-02d269ac448f.png"/> </div><br>
### 实现
### Implementation
观看电影需要操作很多电器,使用外观模式可以实现一键看电影功能。
观看电影需要操作很多电器,使用外观模式实现一键看电影功能。
```java
public class SubSystem {
@ -2811,23 +2791,23 @@ public class Client {
### 设计原则
最少知识原则:只和你的密友谈话。也就是客户对象所需要交互的对象应当尽可能少。
最少知识原则:只和你的密友谈话。也就是客户对象所需要交互的对象应当尽可能少。
## 6. 享元Flyweight
### 意图
### Intent
利用共享的方式来支持大量细粒度的对象,这些对象一部分内部状态是相同的。
### 类图
### Class Diagram
- Flyweight享元对象
- IntrinsicState内部状态相同的项元对象共享
- ExtrinsicState外部状态
- IntrinsicState内部状态享元对象共享内部状态
- ExtrinsicState外部状态,每个享元对象的外部状态不同
<div align="center"> <img src="../pics//d52270b4-9097-4667-9f18-f405fc661c99.png"/> </div><br>
### 实现
### Implementation
```java
public interface Flyweight {
@ -2854,8 +2834,6 @@ public class ConcreteFlyweight implements Flyweight {
```
```java
import java.util.HashMap;
public class FlyweightFactory {
private HashMap<String, Flyweight> flyweights = new HashMap<>();
@ -2872,6 +2850,7 @@ public class FlyweightFactory {
```java
public class Client {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
Flyweight flyweight1 = factory.getFlyweight("aa");
@ -2902,22 +2881,22 @@ Java 利用缓存来加速大量小对象的访问时间。
## 7. 代理Proxy
### 意图
### Intent
控制对其它对象的访问。
### 类图
### Class Diagram
代理有以下四类:
- 远程代理Remote Proxy控制对远程对象不同地址空间的访问它负责将请求及其参数进行编码并向不同地址空间中的对象发送已经编码的请求。
- 虚拟代理Virtual Proxy根据需要创建开销很大的对象它可以缓存实体的附加信息以便延迟对它的访问例如在网站加载一个很大图片时不能马上完成可以用虚拟代理缓存图片的大小信息然后生成一张临时图片代替原始图片。
- 保护代理Protection Proxy按权限控制对象的访问它负责检查调用者是否具有实现一个请求所必须的访问权限。
- 智能代理Smart Reference取代了简单的指针它在访问对象时执行一些附加操作记录对象的引用次数,比如智能智能;当第一次引用一个持久化对象时,将它装入内存;在访问一个实际对象前,检查是否已经锁定了它,以确保其它对象不能改变它。
- 智能代理Smart Reference取代了简单的指针它在访问对象时执行一些附加操作记录对象的引用次数当第一次引用一个持久化对象时将它装入内存在访问一个实际对象前检查是否已经锁定了它以确保其它对象不能改变它。
<div align="center"> <img src="../pics//a6c20f60-5eba-427d-9413-352ada4b40fe.png"/> </div><br>
### 实现
### Implementation
以下是一个虚拟代理的实现,模拟了图片延迟加载的情况下使用与图片大小相等的临时内容去替换原始图片,直到图片加载完成才将图片显示出来。

View File

@ -107,6 +107,7 @@
* [10. 塑造模板函数](#10-塑造模板函数)
* [11. 以委托取代继承](#11-以委托取代继承)
* [12. 以继承取代委托](#12-以继承取代委托)
* [重构练习](#重构练习)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@ -160,6 +161,7 @@ class Customer {
```java
class Rental {
private int daysRented;
private Movie movie;
@ -198,6 +200,7 @@ class Movie {
```java
public class App {
public static void main(String[] args) {
Customer customer = new Customer();
Rental rental1 = new Rental(1, new Movie(Movie.Type1));
@ -235,6 +238,7 @@ public class App {
```java
class Customer {
private List<Rental> rentals = new ArrayList<>();
void addRental(Rental rental) {
@ -253,6 +257,7 @@ class Customer {
```java
class Rental {
private int daysRented;
private Movie movie;
@ -293,8 +298,6 @@ class Price2 implements Price {
```
```java
package imp2;
class Price3 implements Price {
@Override
public double getCharge() {
@ -1415,6 +1418,10 @@ public Manager(String name, String id, int grade) {
让委托类继承受托类。
# 重构练习
- [Refactoring Kata](https://github.com/aikin/refactoring-kata)
# 参考资料
- MartinFowler, 福勒, 贝克, 等. 重构: 改善既有代码的设计 [M]. 电子工业出版社, 2011.

View File

@ -1,9 +1,9 @@
<!-- GFM-TOC -->
* [一、负载均衡](#一负载均衡)
* [算法实现](#算法实现)
* [负载均衡算法](#负载均衡算法)
* [转发实现](#转发实现)
* [二、集群下的 Session 管理](#二集群下的-session-管理)
* [Sticky Sessions](#sticky-sessions)
* [Sticky Session](#sticky-session)
* [Session Replication](#session-replication)
* [Session Server](#session-server)
<!-- GFM-TOC -->
@ -11,25 +11,27 @@
# 一、负载均衡
集群中的应用服务器通常被设计成无状态,用户可以请求任何一个节点(应用服务器
集群中的应用服务器(节点)通常被设计成无状态,用户可以请求任何一个应用服务器。
负载均衡器会根据集群中每个节点的负载情况,将用户请求转发到合适的节点上。
负载均衡器可以用来实现高可用以及伸缩性:
- 高可用:当某个节点故障时,负载均衡器不会将用户请求转发到该节点上,从而保证所有服务持续可用;
- 高可用:当某个节点故障时,负载均衡器会将用户请求转发到另外的节点上,从而保证所有服务持续可用;
- 伸缩性:可以很容易地添加移除节点。
负载均衡运行过程包含两个部分:
1. 根据负载均衡算法得到应该将请求转发到哪个节点上
2. 将请求进行转发
1. 根据负载均衡算法得到请求转发的节点
2. 将请求进行转发
## 算法实现
## 负载均衡算法
### 1. 轮询Round Robin
轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1(2, 4, 6) 的请求会被发送到服务器 2。
轮询算法把每个请求轮流发送到每个服务器上。
下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。(1, 3, 5) 的请求会被发送到服务器 1(2, 4, 6) 的请求会被发送到服务器 2。
<div align="center"> <img src="../pics//2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg"/> </div><br>
@ -39,17 +41,23 @@
### 2. 加权轮询Weighted Round Robbin
加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值,性能高的服务器分配更高的权值。例如下图中,服务器 1 被赋予的权值为 5服务器 2 被赋予的权值为 1那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1(6) 请求会被发送到服务器 2。
加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值,性能高的服务器分配更高的权值。
例如下图中,服务器 1 被赋予的权值为 5服务器 2 被赋予的权值为 1那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1(6) 请求会被发送到服务器 2。
<div align="center"> <img src="../pics//211c60d4-75ca-4acd-8a4f-171458ed58b4.jpg"/> </div><br>
### 3. 最少连接least Connections
由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1(2, 4, 6) 请求被发送到服务器 2只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。
由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。
例如下图中,(1, 3, 5) 请求会被发送到服务器 1但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1(2, 4, 6) 请求被发送到服务器 2只有 (2) 的连接断开,此时 (6, 4) 请求连接服务器 2。该系统继续运行时服务器 2 会承担过大的负载。
<div align="center"> <img src="../pics//3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg"/> </div><br>
最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。
最少连接算法就是将请求发送给当前最少连接数的服务器上。
例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。
<div align="center"> <img src="../pics//1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg"/> </div><br>
@ -61,13 +69,17 @@
### 5. 随机算法Random
把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。
把请求随机发送到服务器上。
和轮询算法类似,该算法比较适合服务器性能差不多的场景。
<div align="center"> <img src="../pics//0ee0f61b-c782-441e-bf34-665650198ae0.jpg"/> </div><br>
### 6. 源地址哈希法 (IP Hash)
源地址哈希通过对客户端 IP 计算哈希值之后,再对服务器数量进行取模运算得到目标服务器的序号。可以保证同一 IP 的客户端的请求会转发到同一台服务器上可以用来实现会话粘滞Sticky Session
源地址哈希通过对客户端 IP 计算哈希值之后,再对服务器数量取模得到目标服务器的序号。
可以保证同一 IP 的客户端的请求会转发到同一台服务器上用来实现会话粘滞Sticky Session
<div align="center"> <img src="../pics//2018040302.jpg"/> </div><br>
@ -88,7 +100,7 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服
### 2. DNS 域名解析
在 DNS 解析域名的同时使用负载均衡算法计算服务器地址。
在 DNS 解析域名的同时使用负载均衡算法计算服务器 IP 地址。
优点:
@ -96,7 +108,7 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服
缺点:
- 由于 DNS 具有多级结构,每一级的域名记录都可能被缓存,当下线一台服务器需要修改 DNS 记录时,需要过很长一段时间才能生效
- 由于 DNS 具有多级结构,每一级的域名记录都可能被缓存,当下线一台服务器需要修改 DNS 记录时,需要过很长一段时间才能生效
大型网站基本使用了 DNS 做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。也就是说,域名解析的结果为内部的负载均衡服务器 IP 地址。
@ -107,7 +119,7 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服
首先了解一下正向代理与反向代理的区别:
- 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端;
- 反向代理:发生在服务器端,用户不知道代理的存在。
- 反向代理:发生在服务器端,用户不知道反向代理的存在。
反向代理服务器位于源服务器前面,用户的请求需要先经过反向代理服务器才能到达源服务器。反向代理可以用来进行缓存、日志记录等,同时也可以用来做为负载均衡服务器。
@ -123,7 +135,7 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服
### 4. 网络层
负载均衡服务器在操作系统内核进程获取网络数据包,根据负载均衡算法计算源服务器的 IP 地址,并修改请求数据包的目的 IP 地址,最后进行转发。
在操作系统内核进程获取网络数据包,根据负载均衡算法计算源服务器的 IP 地址,并修改请求数据包的目的 IP 地址,最后进行转发。
源服务器返回的响应也需要经过负载均衡服务器,通常是让负载均衡服务器同时作为集群的网关服务器来实现。
@ -133,15 +145,17 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服
缺点:
- 和反向代理一样,所有的请求和应都经过负载均衡服务器,会成为性能瓶颈。
- 和反向代理一样,所有的请求和应都经过负载均衡服务器,会成为性能瓶颈。
### 5. 链路层
在链路层根据负载均衡算法计算源服务器的 MAC 地址,并修改请求数据包的目的 MAC 地址,并进行转发。
通过配置源服务器的虚拟 IP 地址和负载均衡服务器的 IP 地址一致,从而不需要需要 IP 地址就可以进行转发。也正因为 IP 地址一样,所以源服务器的响应不需要转发回负载均衡服务器,直接转发给客户端,避免了负载均衡服务器的成为瓶颈。这是一种三角传输模式,被称为直接路由,对于提供下载和视频服务的网站来说,直接路由避免了大量的网络传输数据经过负载均衡服务器。
通过配置源服务器的虚拟 IP 地址和负载均衡服务器的 IP 地址一致,从而不需要修改 IP 地址就可以进行转发。也正因为 IP 地址一样,所以源服务器的响应不需要转发回负载均衡服务器,可以直接转发给客户端,避免了负载均衡服务器的成为瓶颈。
这是目前大型网站使用最广负载均衡转发方式,在 Linux 平台可以使用 LVSLinux Virtual Server
这是一种三角传输模式,被称为直接路由。对于提供下载和视频服务的网站来说,直接路由避免了大量的网络传输数据经过负载均衡服务器。
这是目前大型网站使用最广负载均衡转发方式,在 Linux 平台可以使用的负载均衡服务器为 LVSLinux Virtual Server
参考:
@ -152,7 +166,7 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服
一个用户的 Session 信息如果存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器,由于服务器没有用户的 Session 信息,那么该用户就需要重新进行登录等操作。
## Sticky Sessions
## Sticky Session
需要配置负载均衡器,使得一个用户的所有请求都路由到同一个服务器,这样就可以把用户的 Session 存放在该服务器中。
@ -171,12 +185,11 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服
- 占用过多内存;
- 同步过程占用网络带宽以及服务器处理器时间。
<div align="center"> <img src="../pics//MultiNode-SessionReplication.jpg"/> </div><br>
## Session Server
使用一个单独的服务器存储 Session 数据,可以使用 MySQL也使用 Redis 或者 Memcached 这种内存型数据库。
使用一个单独的服务器存储 Session 数据,可以使用传统的 MySQL也使用 Redis 或者 Memcached 这种内存型数据库。
优点:
@ -192,3 +205,4 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服
- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/)

View File

@ -37,6 +37,7 @@
```java
public class Person {
private String name;
private int gender;
private int age;
@ -63,17 +64,20 @@ public class Person {
继承实现了 **IS-A** 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal从而获得 Animal 非 private 的属性和方法。
继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 **向上转型**
```java
Animal animal = new Cat();
```
继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
## 多态
多态分为编译时多态和运行时多态。编译时多态主要指方法的重载,运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。
多态分为编译时多态和运行时多态:
- 编译时多态主要指方法的重载
- 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
运行时多态有三个条件:
@ -85,24 +89,28 @@ Animal animal = new Cat();
```java
public class Instrument {
public void play() {
System.out.println("Instument is playing...");
}
}
public class Wind extends Instrument {
public void play() {
System.out.println("Wind is playing...");
}
}
public class Percussion extends Instrument {
public void play() {
System.out.println("Percussion is playing...");
}
}
public class Music {
public static void main(String[] args) {
List<Instrument> instruments = new ArrayList<>();
instruments.add(new Wind());
@ -116,7 +124,7 @@ public class Music {
# 二、类图
以下类图使用 [PlantUML](https://www.planttext.com/) 绘制更多语法及使用请参考http://plantuml.com/
以下类图使用 [PlantUML](https://www.planttext.com/) 绘制更多语法及使用请参考http://plantuml.com/
## 泛化关系 (Generalization)
@ -227,9 +235,9 @@ School "1" - "n" Student
和关联关系不同的是依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:
- A 类是 B 类中的(某中方法的局部变量;
- A 类是 B 类方法的局部变量;
- A 类是 B 类方法当中的一个参数;
- A 类向 B 类发送消息,从而影响 B 类发生变化
- A 类向 B 类发送消息,从而影响 B 类发生变化
<div align="center"> <img src="../pics//LOun2W9134NxVugmbJPp15d4LalxC4O.png"/> </div><br>
@ -327,7 +335,7 @@ Vihicle .. N
### 2. 合成复用原则
尽量使用对象组合,而不是继承来达到复用的目的。
尽量使用对象组合,而不是通过继承来达到复用的目的。
### 3. 共同封闭原则

15
other/Group.md Normal file
View File

@ -0,0 +1,15 @@
创建交流群的主要目的是为了给大家提供一个交流平台,方便大家在学习的过程中互相讨论。
这个交流群不是一个笔者的问题回答群,我更希望大家能够愿意积极回答,我相信提问和回答的过程都可以提高大家对知识的掌握程度。
因为笔者白天要上班,因此不能及时进行回复,大部分时间会处于潜水状态。
至于交流群和 Issue 有什么区别,主要是两方面:一是交流群实时性高一些,二是交流群会更活跃一些。
另外Issue 主要是用来发布一些项目中的错误和一些改进建议,当然也可以发布一些可以讨论的问题。
交流群可以讨论的内容比较广,例如在阅读本项目过程中不理解的地方可以在交流群中寻求别人的帮助、新技术的讨论、招聘信息、学习和工作的感受等等。
交流群不讨论政治,不讨论有争议性的话题,不发表仇视言论,不传播谣言,不发布广告(招聘信息之类的可以)。
</br> <div align="center"><img src="group.png" width="300px"></div> </br>

3
other/README.md Normal file
View File

@ -0,0 +1,3 @@
- 其他人添加的全新内容
- 主页 README 引用的图片
- 微信群描述文件

BIN
other/group.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
other/group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

37
other/leetcode 总结.md Normal file
View File

@ -0,0 +1,37 @@
# LeetCode 面试必备
- 💪 就是干如果你觉得有帮助请点个star谢谢
> **欢迎任何人参与和完善:一个人可以走的很快,但是一群人却可以走的更远**
## LeetCode 习题集合
* [LeetCode 解题集合](https://github.com/apachecn/LeetCode/tree/master/docs/Leetcode_Solutions)
## 模版要求
> 提交PR基本要求满足任意一种即可
* 1. 不一样的思路
* 2. 优化时间复杂度和空间复杂度或者解决题目的Follow up
* 3. 有意义的简化代码
* 4. 未提交过的题目
> **案例模版**
[模版007. Reverse Integer 反转整数](https://github.com/apachecn/LeetCode/tree/master/docs/Leetcode_Solutions/007._Reverse_Integer.md)
## 项目贡献者
> 项目发起人
* [@Lisanaaa](https://github.com/Lisanaaa)
* [@片刻](https://github.com/jiangzhonglian)
> 贡献者(欢迎大家来追加)
* [@Lisanaaa](https://github.com/Lisanaaa)
* [@片刻](https://github.com/jiangzhonglian)
* [@小瑶](https://github.com/chenyyx)

View File

@ -430,3 +430,54 @@ where SCORE.SNO = STUDENT.SNO and SSEX = '男' and CNO = (
-- 46、使用游标方式来同时查询每位同学的名字他所选课程及成绩。
declare
cursor student_cursor is
select S.SNO,S.SNAME,C.CNAME,SC.DEGREE as DEGREE
from STUDENT S, COURSE C, SCORE SC
where S.SNO=SC.SNO
and SC.CNO=C.CNO;
student_row student_cursor%ROWTYPE;
begin
open student_cursor;
loop
fetch student_cursor INTO student_row;
exit when student_cursor%NOTFOUND;
dbms_output.put_line( student_row.SNO || '' ||
student_row.SNAME|| '' || student_row.CNAME || '' ||
student_row.DEGREE);
end loop;
close student_cursor;
END;
/
-- 47、 声明触发器指令,每当有同学转换班级时执行触发器显示当前和之前所在班级。
CREATE OR REPLACE TRIGGER display_class_changes
AFTER DELETE OR INSERT OR UPDATE ON student
FOR EACH ROW
WHEN (NEW.sno > 0)
BEGIN
dbms_output.put_line('Old class: ' || :OLD.class);
dbms_output.put_line('New class: ' || :NEW.class);
END;
/
Update student
set class=95031
where sno=109;
-- 48、 删除已设置的触发器指令
DROP TRIGGER display_class_changes;

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
pics/urlnuri.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB